@ticatec/omniflow-core
v0.2.1
Published
Core primitives, execution context, and toolchain SPI for OmniFlow CI/CD orchestrator
Readme
@ticatec/omniflow-core
@ticatec/omniflow-core is the foundational core package for the OmniFlow CI/CD orchestrator. It provides execution context management, standard operational primitives (shell, ssh, git, docker), sensitive credential masking, and an extensible Toolchain SPI.
Highlights
- Static Decoupled Primitives: Import
shell,ssh,git, anddockerdirectly. No cumbersomectx.commandsor object passing required. - Transparent Context Injection (
AsyncLocalStorage): Primitives automatically resolvecwd, environment variablesenv, andloggerfrom the active async call context. - Automatic
--dry-runInterception: When running in simulation mode, destructive operations are intercepted by the core layer automatically, logged safely, and mocked without extra boilerplate in plugins. - Automatic Secret Masking: Credentials and sensitive tokens in logs and commands are automatically masked (
********). - Extensible Toolchain SPI: Built-in support for Maven, Gradle, and Node.js (pnpm / yarn / npm / bun), with priority-based override and registration for custom language toolchains (e.g. Go, Rust, Python).
Architecture Overview
┌────────────────────────────────────────────────────────┐
│ Plugin / Pipeline Command │
│ import { shell, ssh, git, docker } from '@ticatec/omniflow-core'│
└─────────────────────────┬──────────────────────────────┘
│ calls primitives
▼
┌────────────────────────────────────────────────────────┐
│ @ticatec/omniflow-core │
│ ┌──────────────────────────────────────────────────┐ │
│ │ AsyncLocalStorage Context (cwd, env, dryRun, log)│ │
│ └──────────────────────┬───────────────────────────┘ │
│ ▼ │
│ [Dry-Run Check] ──► [Credential Masker] │
│ ▼ │
│ ┌───────────────┬──────────────┬───────────────────┐ │
│ │ shell.run/sh │ ssh.exec/cp │ git & docker ops │ │
│ └───────────────┴──────────────┴───────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Toolchain SPI (Maven, Gradle, Node, Custom...) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘Installation
pnpm add @ticatec/omniflow-core
# or
npm install @ticatec/omniflow-coreRequires Node.js >= 20.0.0.
Core Primitives
1. shell
Executes local shell commands using safe tagged template literals or string execution.
import { shell } from '@ticatec/omniflow-core'
// Tagged template execution (safely splits arguments without shell injection vulnerabilities)
await shell.run`mvn clean package -DskipTests`
// Explicit cwd or options
await shell.run({ cwd: '/workspace/service-a' })`npm run build`
// Shell execution for compound commands (pipes, redirections, &&, ||)
await shell.sh('cat coverage/lcov.info | grep -v "test" > coverage/filtered.info')
// Capture output without throwing on non-zero exit
const res = await shell.output('git status --porcelain')
console.log(res.stdout, res.exitCode)Dry-Run & Context Awareness:
Inside a runWithContext scope with dryRun: true, shell.run, shell.sh, and ssh.exec will print the planned command to the context logger and return clean mock results ({ stdout: '', stderr: '', exitCode: 0, failed: false }) without executing or polluting stdout.
[!NOTE] Credential Masking & Tail Buffering: Output streams piped to
logFilepass through a stateful transform that prevents secret leakage across chunk boundaries by buffering a sliding tail up tomaxSecretLen - 1bytes. When exceptionally large single-line secrets (e.g. 64KB tokens or keys) are configured in the environment, log output chunks smaller than the tail buffer are retained until the buffer capacity is reached or the stream completes (flush), guaranteeing 100% masking coverage before emitting to disk.
2. ssh
Executes remote commands and securely copies files via SSH/SCP with configuration-based target resolution.
import { ssh } from '@ticatec/omniflow-core'
// Option A: Explicit connection target
const target = {
host: 'prod-app-01.internal',
user: 'deploy',
port: 22,
privateKey: process.env.SSH_PRIVATE_KEY
}
// Remote command execution
await ssh.exec(target, 'systemctl restart my-app.service')
// File transfer (SCP)
await ssh.cp(target, 'dist/app.tar.gz', '/opt/deploy/app.tar.gz')
// Option B: Named targets via ssh.configure()
ssh.configure('prod', {
host: 'prod-app-01.internal',
user: 'deploy',
port: 22,
privateKeyFile: '/home/deploy/.ssh/id_rsa'
})
// Call by target name directly
await ssh.exec('prod', 'uptime')
await ssh.cp('prod', 'dist/app.tar.gz', '/opt/deploy/app.tar.gz')
// Clean up singleton config in tests:
ssh.reset()
// Option C: Isolated instance without touching module-level singleton
const client = ssh.createClient({
staging: { host: 'staging.internal', user: 'ci', privateKeyFile: '~/.ssh/id_rsa' }
})
await client.exec('staging', 'hostname')3. git
Provides essential Git repository inspection and workspace lifecycle management.
import { git } from '@ticatec/omniflow-core'
const branch = await git.currentBranch()
const commit = await git.currentCommit()
const dirty = await git.isDirty()
// Workspace promotion and synchronization
await git.fetch({ remote: 'origin', branch: 'main' })
await git.resetHard('origin/main')
await git.clean()
await git.checkout('release/v1.0')4. docker
Encapsulates common container build, tag, push, and compose orchestration.
import { docker } from '@ticatec/omniflow-core'
// Build and tag
await docker.build({
image: 'registry.internal/api:v1.2.0',
dockerfile: 'Dockerfile.prod',
buildArgs: { NODE_ENV: 'production' }
})
// Push to registry
await docker.push('registry.internal/api:v1.2.0')
// Docker Compose management
await docker.composeUp({ files: 'docker-compose.prod.yml', detach: true })
await docker.composeDown({ files: 'docker-compose.prod.yml' })Execution Context & AsyncLocalStorage
The execution context manages runtime configuration, logging, and environment variables across asynchronous chains.
Runner / Orchestrator Example:
import { runWithContext, createMockContext } from '@ticatec/omniflow-core'
const ctx = createMockContext({
runId: 'run-20260912-001',
project: 'order-service',
environment: 'staging',
dryRun: false,
workspace: '/workspaces/order-service',
projectRoot: '/workspaces/order-service',
env: {
REGISTRY: 'harbor.company.com',
DOCKER_TOKEN: 'secret-token-value'
}
})
await runWithContext(ctx, async () => {
// Any function called here (directly or deeply nested) can call getContext()
// and primitives automatically use ctx.workspace, ctx.env, ctx.dryRun, and ctx.logger
await executePipelineSteps()
})Plugin / Step Example:
import { getContext, tryGetContext, shell } from '@ticatec/omniflow-core'
export async function myPluginTask() {
const ctx = getContext() // Throws if outside runWithContext
ctx.logger.info(`Building in ${ctx.workspace} for environment ${ctx.environment}`)
await shell.run`npm test`
}Toolchain SPI
OmniFlow Core abstracts language build systems using a clean Service Provider Interface (SPI).
Built-in Toolchains
- Maven (
MavenToolchain): Detectspom.xml, parses groupId/artifactId/version, runs./mvnwormvn(priority: 30). - Gradle (
GradleToolchain): Detectsbuild.gradle/build.gradle.kts, resolves Gradle Wrapper./gradlew, generates build commands (priority: 20). - Node (
NodeToolchain): Detectspackage.json, auto-selectspnpm,yarn,npm, orbunfrom lockfiles, parses package coordinates (priority: 10).
Detecting and Using Toolchains:
import { resolveToolchain } from '@ticatec/omniflow-core'
const { provider, detection } = await resolveToolchain('/path/to/project')
console.log(`Detected toolchain ${provider.name} because: ${detection.reason}`)
const info = await provider.projectInfo('/path/to/project')
console.log(`Project: ${info.name}@${info.version}`)
// Execute dependency install and build
await provider.install('/path/to/project')
await provider.build('/path/to/project', ['-DskipTests'])[!NOTE] Reproducible CI Installation:
NodeToolchain.install()automatically enforces immutable lockfile installations (npm ci,pnpm install --frozen-lockfile,yarn install --frozen-lockfile/--immutable,bun install --frozen-lockfile) whenever a lockfile is present in the project directory, preventing accidental lockfile mutation or dependency drift during automated CI/CD runs. If no lockfile exists in a new project, it falls back to standard package installation without error.
Registering Custom / Overriding Toolchains:
import {
registerToolchain,
shell,
type ToolchainProvider,
type DetectionResult,
type ProjectInfo
} from '@ticatec/omniflow-core'
import fs from 'node:fs/promises'
import path from 'node:path'
export class GoToolchain implements ToolchainProvider {
readonly name = 'go'
readonly priority: number = 50 // Higher priority takes precedence during automatic detection
async detect(projectDir: string): Promise<DetectionResult | null> {
try {
await fs.access(path.join(projectDir, 'go.mod'))
return { name: this.name, reason: 'found go.mod' }
} catch {
return null
}
}
async projectInfo(projectDir: string): Promise<ProjectInfo> {
return { name: 'user-service', version: '1.0.0', fullName: 'user-service' }
}
async install(projectDir: string, flags: string[] = []): Promise<void> {
await shell.run({ cwd: projectDir })`go mod download ${flags}`
}
async build(projectDir: string, flags: string[] = []): Promise<void> {
await shell.run({ cwd: projectDir })`go build -v -o dist/app ${flags}`
}
}
registerToolchain(new GoToolchain())For a complete step-by-step guide on creating custom toolchains (Go, Rust) and enterprise overrides, see Toolchain Extension Guide.
Breaking Changes in v0.2.0
If upgrading from an earlier experimental version, note the following breaking API and behavior changes:
--dry-runsimulation output: In dry-run mode, commands now return{ stdout: '', stderr: '', exitCode: 0, failed: false }instead of synthetic strings like"[DRY-RUN] ...". The simulated command is logged cleanly toctx.logger.infowithout polluting stdout.docker.buildimage requirement: Theimageproperty inDockerBuildOptionsis now strictly required. Implicit defaulting to'latest'and undocumented alias parameters (tag,tags) have been removed to avoid silent misconfiguration.docker.composeUpproperty names: Standardized onfiles?: string | string[]anddetach?: boolean. Legacy aliases (file,detached) have been removed.sshconfiguration naming: Target SSH configuration adheres strictly to camelCase:privateKey,privateKeyFile. Deprecated snake_case properties (private_key,private_key_file) have been removed.resolveToolchainoptions: Options are normalized to{ preferred?: string; fallback?: string }. Deprecated aliasconfiguredhas been removed.- Command spawn failure exit code: When a subprocess fails to spawn (e.g. executable not found), the runner returns
exitCode: -1andfailed: trueinstead of mistakenly reporting exit code0. git.resetHard&git.checkoutstring argument: In v0.1.x, passing a single string argument was interpreted as the working directory (resetHard(dir?, target?),checkout(dir?, ref?)). In v0.2.0, a bare string argument designates the target commit/ref (resetHard(target?: string | GitResetOptions),checkout(ref?: string | GitCheckoutOptions)). To specify an explicit working directory, pass an options object withdir:{ dir: '/path/to/repo' }.
Subpath Exports
@ticatec/omniflow-core supports clean subpath imports in accordance with modern ESM standards:
| Export Path | Description |
|:---|:---|
| @ticatec/omniflow-core | Full bundle: context, primitives, toolchain SPI, and utils |
| @ticatec/omniflow-core/context | Execution context, AsyncLocalStorage, and mocking utilities |
| @ticatec/omniflow-core/primitives | Core commands (shell, ssh, git, docker) |
| @ticatec/omniflow-core/toolchain | Toolchain SPI, registry, and built-in providers |
License
MIT © Ticatec
