@ticatec/omniflow-core 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -54
  3. package/README_CN.md +113 -55
  4. package/dist/index.d.ts +5 -4
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/plugin/PluginContext.d.ts +39 -0
  9. package/dist/plugin/PluginContext.d.ts.map +1 -0
  10. package/dist/plugin/PluginContext.js +8 -0
  11. package/dist/plugin/PluginContext.js.map +1 -0
  12. package/dist/primitives/docker.d.ts.map +1 -1
  13. package/dist/primitives/docker.js +9 -0
  14. package/dist/primitives/docker.js.map +1 -1
  15. package/dist/primitives/git.d.ts +58 -16
  16. package/dist/primitives/git.d.ts.map +1 -1
  17. package/dist/primitives/git.js +91 -33
  18. package/dist/primitives/git.js.map +1 -1
  19. package/dist/primitives/shell.d.ts +4 -0
  20. package/dist/primitives/shell.d.ts.map +1 -1
  21. package/dist/primitives/shell.js +20 -60
  22. package/dist/primitives/shell.js.map +1 -1
  23. package/dist/primitives/ssh.d.ts +8 -5
  24. package/dist/primitives/ssh.d.ts.map +1 -1
  25. package/dist/primitives/ssh.js +49 -14
  26. package/dist/primitives/ssh.js.map +1 -1
  27. package/dist/primitives/subprocess.d.ts +52 -0
  28. package/dist/primitives/subprocess.d.ts.map +1 -0
  29. package/dist/primitives/subprocess.js +353 -0
  30. package/dist/primitives/subprocess.js.map +1 -0
  31. package/dist/toolchain/providers/GradleToolchain.d.ts +1 -1
  32. package/dist/toolchain/providers/GradleToolchain.d.ts.map +1 -1
  33. package/dist/toolchain/providers/GradleToolchain.js +4 -3
  34. package/dist/toolchain/providers/GradleToolchain.js.map +1 -1
  35. package/dist/toolchain/providers/MavenToolchain.d.ts +1 -1
  36. package/dist/toolchain/providers/MavenToolchain.d.ts.map +1 -1
  37. package/dist/toolchain/providers/MavenToolchain.js +3 -3
  38. package/dist/toolchain/providers/MavenToolchain.js.map +1 -1
  39. package/dist/toolchain/providers/NodeToolchain.d.ts +1 -1
  40. package/dist/toolchain/providers/NodeToolchain.d.ts.map +1 -1
  41. package/dist/toolchain/providers/NodeToolchain.js +61 -0
  42. package/dist/toolchain/providers/NodeToolchain.js.map +1 -1
  43. package/dist/toolchain/providers/pom.d.ts.map +1 -1
  44. package/dist/toolchain/providers/pom.js +13 -0
  45. package/dist/toolchain/providers/pom.js.map +1 -1
  46. package/dist/toolchain/registry.d.ts +11 -3
  47. package/dist/toolchain/registry.d.ts.map +1 -1
  48. package/dist/toolchain/registry.js +33 -8
  49. package/dist/toolchain/registry.js.map +1 -1
  50. package/dist/utils/mask.d.ts.map +1 -1
  51. package/dist/utils/mask.js +30 -5
  52. package/dist/utils/mask.js.map +1 -1
  53. package/docs/toolchain-extension.md +301 -0
  54. package/docs/toolchain-extension_CN.md +304 -0
  55. package/package.json +23 -1
  56. package/src/context/index.ts +74 -0
  57. package/src/context/storage.ts +8 -0
  58. package/src/context/types.ts +69 -0
  59. package/src/index.ts +97 -0
  60. package/src/plugin/PluginContext.ts +57 -0
  61. package/src/primitives/docker.ts +164 -0
  62. package/src/primitives/git.ts +172 -0
  63. package/src/primitives/index.ts +4 -0
  64. package/src/primitives/shell.ts +157 -0
  65. package/src/primitives/ssh.ts +249 -0
  66. package/src/primitives/subprocess.ts +389 -0
  67. package/src/toolchain/index.ts +6 -0
  68. package/src/toolchain/providers/GradleToolchain.ts +137 -0
  69. package/src/toolchain/providers/MavenToolchain.ts +64 -0
  70. package/src/toolchain/providers/NodeToolchain.ts +172 -0
  71. package/src/toolchain/providers/pom.ts +145 -0
  72. package/src/toolchain/registry.ts +161 -0
  73. package/src/toolchain/types.ts +40 -0
  74. package/src/utils/mask.ts +73 -0
  75. package/src/utils/template.ts +62 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ticatec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # @omniflow/core
1
+ # @ticatec/omniflow-core
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org/)
5
5
 
6
6
  [中文文档](./README_CN.md)
7
7
 
8
- **@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.
8
+ **@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.
9
9
 
10
10
  ---
11
11
 
@@ -14,7 +14,7 @@
14
14
  - **Static Decoupled Primitives**: Import `shell`, `ssh`, `git`, and `docker` directly. No cumbersome `ctx.commands` or object passing required.
15
15
  - **Transparent Context Injection (`AsyncLocalStorage`)**: Primitives automatically resolve `cwd`, environment variables `env`, and `logger` from the active async call context.
16
16
  - **Automatic `--dry-run` Interception**: When running in simulation mode, destructive operations are intercepted by the core layer automatically, logged safely, and mocked without extra boilerplate in plugins.
17
- - **Automatic Secret Masking**: Credentials and sensitive tokens in logs and commands are automatically masked (`***`).
17
+ - **Automatic Secret Masking**: Credentials and sensitive tokens in logs and commands are automatically masked (`********`).
18
18
  - **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).
19
19
 
20
20
  ---
@@ -24,12 +24,12 @@
24
24
  ```
25
25
  ┌────────────────────────────────────────────────────────┐
26
26
  │ Plugin / Pipeline Command │
27
- │ import { shell, ssh, git, docker } from '@omniflow/core'│
27
+ │ import { shell, ssh, git, docker } from '@ticatec/omniflow-core'│
28
28
  └─────────────────────────┬──────────────────────────────┘
29
29
  │ calls primitives
30
30
 
31
31
  ┌────────────────────────────────────────────────────────┐
32
- │ @omniflow/core │
32
+ │ @ticatec/omniflow-core │
33
33
  │ ┌──────────────────────────────────────────────────┐ │
34
34
  │ │ AsyncLocalStorage Context (cwd, env, dryRun, log)│ │
35
35
  │ └──────────────────────┬───────────────────────────┘ │
@@ -51,9 +51,9 @@
51
51
  ## Installation
52
52
 
53
53
  ```bash
54
- pnpm add @omniflow/core
54
+ pnpm add @ticatec/omniflow-core
55
55
  # or
56
- npm install @omniflow/core
56
+ npm install @ticatec/omniflow-core
57
57
  ```
58
58
 
59
59
  Requires **Node.js >= 20.0.0**.
@@ -66,7 +66,7 @@ Requires **Node.js >= 20.0.0**.
66
66
  Executes local shell commands using safe tagged template literals or string execution.
67
67
 
68
68
  ```typescript
69
- import { shell } from '@omniflow/core'
69
+ import { shell } from '@ticatec/omniflow-core'
70
70
 
71
71
  // Tagged template execution (safely splits arguments without shell injection vulnerabilities)
72
72
  await shell.run`mvn clean package -DskipTests`
@@ -83,16 +83,20 @@ console.log(res.stdout, res.exitCode)
83
83
  ```
84
84
 
85
85
  **Dry-Run & Context Awareness**:
86
- Inside a `runWithContext` scope with `dryRun: true`, `shell.run` and `shell.sh` will print the planned command to the context logger and return mock results (`{ stdout: '[dry-run] ...', exitCode: 0 }`) without executing.
86
+ 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.
87
+
88
+ > [!NOTE]
89
+ > **Credential Masking & Tail Buffering**: Output streams piped to `logFile` pass through a stateful transform that prevents secret leakage across chunk boundaries by buffering a sliding tail up to `maxSecretLen - 1` bytes. 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.
87
90
 
88
91
  ---
89
92
 
90
93
  ### 2. `ssh`
91
- Executes remote commands and securely copies files via SSH/SCP with built-in connection pooling.
94
+ Executes remote commands and securely copies files via SSH/SCP with configuration-based target resolution.
92
95
 
93
96
  ```typescript
94
- import { ssh } from '@omniflow/core'
97
+ import { ssh } from '@ticatec/omniflow-core'
95
98
 
99
+ // Option A: Explicit connection target
96
100
  const target = {
97
101
  host: 'prod-app-01.internal',
98
102
  user: 'deploy',
@@ -105,6 +109,27 @@ await ssh.exec(target, 'systemctl restart my-app.service')
105
109
 
106
110
  // File transfer (SCP)
107
111
  await ssh.cp(target, 'dist/app.tar.gz', '/opt/deploy/app.tar.gz')
112
+
113
+ // Option B: Named targets via ssh.configure()
114
+ ssh.configure('prod', {
115
+ host: 'prod-app-01.internal',
116
+ user: 'deploy',
117
+ port: 22,
118
+ privateKeyFile: '/home/deploy/.ssh/id_rsa'
119
+ })
120
+
121
+ // Call by target name directly
122
+ await ssh.exec('prod', 'uptime')
123
+ await ssh.cp('prod', 'dist/app.tar.gz', '/opt/deploy/app.tar.gz')
124
+
125
+ // Clean up singleton config in tests:
126
+ ssh.reset()
127
+
128
+ // Option C: Isolated instance without touching module-level singleton
129
+ const client = ssh.createClient({
130
+ staging: { host: 'staging.internal', user: 'ci', privateKeyFile: '~/.ssh/id_rsa' }
131
+ })
132
+ await client.exec('staging', 'hostname')
108
133
  ```
109
134
 
110
135
  ---
@@ -113,7 +138,7 @@ await ssh.cp(target, 'dist/app.tar.gz', '/opt/deploy/app.tar.gz')
113
138
  Provides essential Git repository inspection and workspace lifecycle management.
114
139
 
115
140
  ```typescript
116
- import { git } from '@omniflow/core'
141
+ import { git } from '@ticatec/omniflow-core'
117
142
 
118
143
  const branch = await git.currentBranch()
119
144
  const commit = await git.currentCommit()
@@ -132,12 +157,11 @@ await git.checkout('release/v1.0')
132
157
  Encapsulates common container build, tag, push, and compose orchestration.
133
158
 
134
159
  ```typescript
135
- import { docker } from '@omniflow/core'
160
+ import { docker } from '@ticatec/omniflow-core'
136
161
 
137
162
  // Build and tag
138
163
  await docker.build({
139
- image: 'registry.internal/api',
140
- tag: 'v1.2.0',
164
+ image: 'registry.internal/api:v1.2.0',
141
165
  dockerfile: 'Dockerfile.prod',
142
166
  buildArgs: { NODE_ENV: 'production' }
143
167
  })
@@ -146,8 +170,8 @@ await docker.build({
146
170
  await docker.push('registry.internal/api:v1.2.0')
147
171
 
148
172
  // Docker Compose management
149
- await docker.composeUp({ file: 'docker-compose.prod.yml', detached: true })
150
- await docker.composeDown({ file: 'docker-compose.prod.yml' })
173
+ await docker.composeUp({ files: 'docker-compose.prod.yml', detach: true })
174
+ await docker.composeDown({ files: 'docker-compose.prod.yml' })
151
175
  ```
152
176
 
153
177
  ---
@@ -158,23 +182,20 @@ The execution context manages runtime configuration, logging, and environment va
158
182
 
159
183
  ### Runner / Orchestrator Example:
160
184
  ```typescript
161
- import { runWithContext, createMockContext } from '@omniflow/core'
185
+ import { runWithContext, createMockContext } from '@ticatec/omniflow-core'
162
186
 
163
- const ctx = {
187
+ const ctx = createMockContext({
164
188
  runId: 'run-20260912-001',
165
189
  project: 'order-service',
166
- env: 'staging',
190
+ environment: 'staging',
167
191
  dryRun: false,
168
192
  workspace: '/workspaces/order-service',
169
- variables: { REGISTRY: 'harbor.company.com' },
170
- secrets: { DOCKER_TOKEN: 'secret-token-value' },
171
- logger: {
172
- info: (msg) => console.log(`[INFO] ${msg}`),
173
- warn: (msg) => console.warn(`[WARN] ${msg}`),
174
- error: (msg) => console.error(`[ERROR] ${msg}`),
175
- debug: (msg) => console.debug(`[DEBUG] ${msg}`)
193
+ projectRoot: '/workspaces/order-service',
194
+ env: {
195
+ REGISTRY: 'harbor.company.com',
196
+ DOCKER_TOKEN: 'secret-token-value'
176
197
  }
177
- }
198
+ })
178
199
 
179
200
  await runWithContext(ctx, async () => {
180
201
  // Any function called here (directly or deeply nested) can call getContext()
@@ -185,11 +206,11 @@ await runWithContext(ctx, async () => {
185
206
 
186
207
  ### Plugin / Step Example:
187
208
  ```typescript
188
- import { getContext, tryGetContext, shell } from '@omniflow/core'
209
+ import { getContext, tryGetContext, shell } from '@ticatec/omniflow-core'
189
210
 
190
211
  export async function myPluginTask() {
191
212
  const ctx = getContext() // Throws if outside runWithContext
192
- ctx.logger.info(`Building in ${ctx.workspace} for ${ctx.env}`)
213
+ ctx.logger.info(`Building in ${ctx.workspace} for environment ${ctx.environment}`)
193
214
 
194
215
  await shell.run`npm test`
195
216
  }
@@ -202,13 +223,13 @@ export async function myPluginTask() {
202
223
  OmniFlow Core abstracts language build systems using a clean Service Provider Interface (SPI).
203
224
 
204
225
  ### Built-in Toolchains
205
- 1. **Maven** (`MavenToolchain`): Detects `pom.xml`, parses groupId/artifactId/version, runs `./mvnw` or `mvn`.
206
- 2. **Gradle** (`GradleToolchain`): Detects `build.gradle` / `build.gradle.kts`, resolves Gradle Wrapper `./gradlew`, generates build commands.
207
- 3. **Node** (`NodeToolchain`): Detects `package.json`, auto-selects `pnpm`, `yarn`, `npm`, or `bun` from lockfiles, parses package coordinates.
226
+ 1. **Maven** (`MavenToolchain`): Detects `pom.xml`, parses groupId/artifactId/version, runs `./mvnw` or `mvn` (priority: 30).
227
+ 2. **Gradle** (`GradleToolchain`): Detects `build.gradle` / `build.gradle.kts`, resolves Gradle Wrapper `./gradlew`, generates build commands (priority: 20).
228
+ 3. **Node** (`NodeToolchain`): Detects `package.json`, auto-selects `pnpm`, `yarn`, `npm`, or `bun` from lockfiles, parses package coordinates (priority: 10).
208
229
 
209
230
  ### Detecting and Using Toolchains:
210
231
  ```typescript
211
- import { resolveToolchain } from '@omniflow/core'
232
+ import { resolveToolchain } from '@ticatec/omniflow-core'
212
233
 
213
234
  const { provider, detection } = await resolveToolchain('/path/to/project')
214
235
  console.log(`Detected toolchain ${provider.name} because: ${detection.reason}`)
@@ -221,41 +242,78 @@ await provider.install('/path/to/project')
221
242
  await provider.build('/path/to/project', ['-DskipTests'])
222
243
  ```
223
244
 
245
+ > [!NOTE]
246
+ > **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.
247
+
224
248
  ### Registering Custom / Overriding Toolchains:
225
249
  ```typescript
226
- import { registerToolchain, type ToolchainProvider } from '@omniflow/core'
227
-
228
- const goToolchain: ToolchainProvider = {
229
- name: 'go',
230
- priority: 20, // Higher priority takes precedence over lower priority providers
231
- detect: async (cwd) => {
232
- // Check if go.mod exists
233
- return { detected: true, confidence: 0.95 }
234
- },
235
- getProjectInfo: async (cwd) => ({
236
- name: 'user-service',
237
- version: '1.0.0'
238
- }),
239
- getBuildCommand: (cwd, options) => 'go build -v -o dist/app ./cmd/main.go'
250
+ import {
251
+ registerToolchain,
252
+ shell,
253
+ type ToolchainProvider,
254
+ type DetectionResult,
255
+ type ProjectInfo
256
+ } from '@ticatec/omniflow-core'
257
+ import fs from 'node:fs/promises'
258
+ import path from 'node:path'
259
+
260
+ export class GoToolchain implements ToolchainProvider {
261
+ readonly name = 'go'
262
+ readonly priority: number = 50 // Higher priority takes precedence during automatic detection
263
+
264
+ async detect(projectDir: string): Promise<DetectionResult | null> {
265
+ try {
266
+ await fs.access(path.join(projectDir, 'go.mod'))
267
+ return { name: this.name, reason: 'found go.mod' }
268
+ } catch {
269
+ return null
270
+ }
271
+ }
272
+
273
+ async projectInfo(projectDir: string): Promise<ProjectInfo> {
274
+ return { name: 'user-service', version: '1.0.0', fullName: 'user-service' }
275
+ }
276
+
277
+ async install(projectDir: string, flags: string[] = []): Promise<void> {
278
+ await shell.run({ cwd: projectDir })`go mod download ${flags}`
279
+ }
280
+
281
+ async build(projectDir: string, flags: string[] = []): Promise<void> {
282
+ await shell.run({ cwd: projectDir })`go build -v -o dist/app ${flags}`
283
+ }
240
284
  }
241
285
 
242
- registerToolchain(goToolchain)
286
+ registerToolchain(new GoToolchain())
243
287
  ```
244
288
 
245
289
  > For a complete step-by-step guide on creating custom toolchains (Go, Rust) and enterprise overrides, see [Toolchain Extension Guide](./docs/toolchain-extension.md).
246
290
 
247
291
  ---
248
292
 
293
+ ## Breaking Changes in v0.2.0
294
+
295
+ If upgrading from an earlier experimental version, note the following breaking API and behavior changes:
296
+
297
+ 1. **`--dry-run` simulation 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 to `ctx.logger.info` without polluting stdout.
298
+ 2. **`docker.build` image requirement**: The `image` property in `DockerBuildOptions` is now strictly required. Implicit defaulting to `'latest'` and undocumented alias parameters (`tag`, `tags`) have been removed to avoid silent misconfiguration.
299
+ 3. **`docker.composeUp` property names**: Standardized on `files?: string | string[]` and `detach?: boolean`. Legacy aliases (`file`, `detached`) have been removed.
300
+ 4. **`ssh` configuration naming**: Target SSH configuration adheres strictly to camelCase: `privateKey`, `privateKeyFile`. Deprecated snake_case properties (`private_key`, `private_key_file`) have been removed.
301
+ 5. **`resolveToolchain` options**: Options are normalized to `{ preferred?: string; fallback?: string }`. Deprecated alias `configured` has been removed.
302
+ 6. **Command spawn failure exit code**: When a subprocess fails to spawn (e.g. executable not found), the runner returns `exitCode: -1` and `failed: true` instead of mistakenly reporting exit code `0`.
303
+ 7. **`git.resetHard` & `git.checkout` string 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 with `dir`: `{ dir: '/path/to/repo' }`.
304
+
305
+ ---
306
+
249
307
  ## Subpath Exports
250
308
 
251
- `@omniflow/core` supports clean subpath imports in accordance with modern ESM standards:
309
+ `@ticatec/omniflow-core` supports clean subpath imports in accordance with modern ESM standards:
252
310
 
253
311
  | Export Path | Description |
254
312
  |:---|:---|
255
- | `@omniflow/core` | Full bundle: context, primitives, toolchain SPI, and utils |
256
- | `@omniflow/core/context` | Execution context, `AsyncLocalStorage`, and mocking utilities |
257
- | `@omniflow/core/primitives` | Core commands (`shell`, `ssh`, `git`, `docker`) |
258
- | `@omniflow/core/toolchain` | Toolchain SPI, registry, and built-in providers |
313
+ | `@ticatec/omniflow-core` | Full bundle: context, primitives, toolchain SPI, and utils |
314
+ | `@ticatec/omniflow-core/context` | Execution context, `AsyncLocalStorage`, and mocking utilities |
315
+ | `@ticatec/omniflow-core/primitives` | Core commands (`shell`, `ssh`, `git`, `docker`) |
316
+ | `@ticatec/omniflow-core/toolchain` | Toolchain SPI, registry, and built-in providers |
259
317
 
260
318
  ---
261
319
 
package/README_CN.md CHANGED
@@ -1,20 +1,20 @@
1
- # @omniflow/core
1
+ # @ticatec/omniflow-core
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org/)
5
5
 
6
6
  [English](./README.md)
7
7
 
8
- **@omniflow/core** 是 OmniFlow CI/CD 统一编排器的基础核心库。它提供了运行时执行上下文管理、标准化命令原语(`shell`、`ssh`、`git`、`docker`)、敏感凭据脱敏以及可扩展的构建工具链 SPI(Toolchain SPI)。
8
+ **@ticatec/omniflow-core** 是 OmniFlow CI/CD 统一编排器的基础核心库。它提供了运行时执行上下文管理、标准化命令原语(`shell`、`ssh`、`git`、`docker`)、敏感凭据脱敏以及可扩展的构建工具链 SPI(Toolchain SPI)。
9
9
 
10
10
  ---
11
11
 
12
12
  ## 核心特性
13
13
 
14
- - **原语与上下文解耦**:开发者与插件作者可直接通过静态 `import { shell, ssh, git, docker } from '@omniflow/core'` 导入命令,无需在函数间层层显式传递 `ctx`,移除原有的 `ctx.commands` 耦合。
14
+ - **原语与上下文解耦**:开发者与插件作者可直接通过静态 `import { shell, ssh, git, docker } from '@ticatec/omniflow-core'` 导入命令,无需在函数间层层显式传递 `ctx`,移除原有的 `ctx.commands` 耦合。
15
15
  - **透明上下文注入 (`AsyncLocalStorage`)**:底层基于 Node.js 原生 `AsyncLocalStorage`,基础原语在被调用时,会自动从当前的异步执行链路中解析 `cwd`(工作目录)、`env`(环境变量)、`dryRun`(模拟模式)与 `logger`。
16
16
  - **开箱即用的 `--dry-run` 拦截保护**:在模拟运行模式下,破坏性操作(执行脚本、远程 SSH、推送镜像等)均由 Core 层底层自动拦截,安全记录日志并返回模拟结果,插件层无需编写冗余的 `if (ctx.dryRun)` 逻辑。
17
- - **敏感信息自动脱敏 (Masking)**:命令参数与环境变量中包含的敏感词(如 `token`、`secret`、`password`、`key`)在格式化及输出至日志时自动替换为 `***`,杜绝日志泄露。
17
+ - **敏感信息自动脱敏 (Masking)**:命令参数与环境变量中包含的敏感词(如 `token`、`secret`、`password`、`key`)在格式化及输出至日志时自动替换为 `********`,杜绝日志泄露。
18
18
  - **统一可扩展的 Toolchain SPI**:内置对 Maven、Gradle、Node.js(自动识别 pnpm / yarn / npm / bun)的识别与命令生成,同时支持第三方插件按优先级注册与覆盖扩展(如 Go、Rust、Python 等)。
19
19
 
20
20
  ---
@@ -24,12 +24,12 @@
24
24
  ```
25
25
  ┌────────────────────────────────────────────────────────┐
26
26
  │ 插件 / 流水线命令 │
27
- │ import { shell, ssh, git, docker } from '@omniflow/core'│
27
+ │ import { shell, ssh, git, docker } from '@ticatec/omniflow-core'│
28
28
  └─────────────────────────┬──────────────────────────────┘
29
29
  │ 直接调用底层原语
30
30
 
31
31
  ┌────────────────────────────────────────────────────────┐
32
- │ @omniflow/core │
32
+ │ @ticatec/omniflow-core │
33
33
  │ ┌──────────────────────────────────────────────────┐ │
34
34
  │ │ AsyncLocalStorage 上下文 (cwd, env, dryRun, log) │ │
35
35
  │ └──────────────────────┬───────────────────────────┘ │
@@ -51,9 +51,9 @@
51
51
  ## 安装
52
52
 
53
53
  ```bash
54
- pnpm add @omniflow/core
54
+ pnpm add @ticatec/omniflow-core
55
55
  # 或
56
- npm install @omniflow/core
56
+ npm install @ticatec/omniflow-core
57
57
  ```
58
58
 
59
59
  运行环境要求:**Node.js >= 20.0.0**。
@@ -66,7 +66,7 @@ npm install @omniflow/core
66
66
  用于执行本地 Shell 命令,推荐使用标签模板字符串(Tagged Template Literals)方式执行,原生防止命令行注入。
67
67
 
68
68
  ```typescript
69
- import { shell } from '@omniflow/core'
69
+ import { shell } from '@ticatec/omniflow-core'
70
70
 
71
71
  // 标签模板安全执行(自动拆分参数,防止参数注入)
72
72
  await shell.run`mvn clean package -DskipTests`
@@ -83,16 +83,20 @@ console.log(res.stdout, res.exitCode)
83
83
  ```
84
84
 
85
85
  **Dry-Run 机制**:
86
- 在包含 `dryRun: true` 的 `runWithContext` 环境中,`shell.run` `shell.sh` 会自动将待执行命令格式化并打入当前日志,同时返回 `{ stdout: '[dry-run] ...', exitCode: 0 }`,不会在系统上实际派生子进程。
86
+ 在包含 `dryRun: true` 的 `runWithContext` 环境中,`shell.run`、`shell.sh` `ssh.exec` 会自动将待执行命令格式化并打入当前日志,同时返回纯净的默认结果(`{ stdout: '', stderr: '', exitCode: 0, failed: false }`),不会在系统上实际派生子进程,也不会用假字符串污染 stdout。
87
+
88
+ > [!NOTE]
89
+ > **凭据脱敏与尾缓冲机制**:输出写入 `logFile` 时会经过有状态 Transform 流处理,在数据块(chunk)边界保留长度为 `maxSecretLen - 1` 的滑动尾缓冲以防止凭据跨块泄漏。当环境中配置了极长单行凭据(如 64KB 的 Token 或密钥段)时,小于尾缓冲容量的日志输出会暂存在流缓冲中,直到后续输出填满缓冲或流结束(flush)时全量脱敏刷盘,确保跨块切分下 100% 零泄露。
87
90
 
88
91
  ---
89
92
 
90
93
  ### 2. `ssh`
91
- 提供跨节点远程命令执行与基于 SCP 的文件安全传输,内置连接池复用机制。
94
+ 提供跨节点远程命令执行与基于 SCP 的文件安全传输,支持配置化目标解析与安全执行。
92
95
 
93
96
  ```typescript
94
- import { ssh } from '@omniflow/core'
97
+ import { ssh } from '@ticatec/omniflow-core'
95
98
 
99
+ // 方式 A:显式连接目标对象
96
100
  const target = {
97
101
  host: 'prod-app-01.internal',
98
102
  user: 'deploy',
@@ -105,6 +109,27 @@ await ssh.exec(target, 'systemctl restart my-app.service')
105
109
 
106
110
  // 文件安全分发 (SCP)
107
111
  await ssh.cp(target, 'dist/app.tar.gz', '/opt/deploy/app.tar.gz')
112
+
113
+ // 方式 B:通过 ssh.configure() 注册命名主机
114
+ ssh.configure('prod', {
115
+ host: 'prod-app-01.internal',
116
+ user: 'deploy',
117
+ port: 22,
118
+ privateKeyFile: '/home/deploy/.ssh/id_rsa'
119
+ })
120
+
121
+ // 直接按别名调用
122
+ await ssh.exec('prod', 'uptime')
123
+ await ssh.cp('prod', 'dist/app.tar.gz', '/opt/deploy/app.tar.gz')
124
+
125
+ // 测试清理单例配置:
126
+ ssh.reset()
127
+
128
+ // 方式 C:创建独立实例,避免污染模块级单例
129
+ const client = ssh.createClient({
130
+ staging: { host: 'staging.internal', user: 'ci', privateKeyFile: '~/.ssh/id_rsa' }
131
+ })
132
+ await client.exec('staging', 'hostname')
108
133
  ```
109
134
 
110
135
  ---
@@ -113,7 +138,7 @@ await ssh.cp(target, 'dist/app.tar.gz', '/opt/deploy/app.tar.gz')
113
138
  提供仓库状态感知、分支切换、远端同步与工作区重置原语。
114
139
 
115
140
  ```typescript
116
- import { git } from '@omniflow/core'
141
+ import { git } from '@ticatec/omniflow-core'
117
142
 
118
143
  const branch = await git.currentBranch()
119
144
  const commit = await git.currentCommit()
@@ -132,12 +157,11 @@ await git.checkout('release/v1.0')
132
157
  封装常见的容器构建、打标签、推送与 Compose 编排生命周期。
133
158
 
134
159
  ```typescript
135
- import { docker } from '@omniflow/core'
160
+ import { docker } from '@ticatec/omniflow-core'
136
161
 
137
162
  // 镜像构建
138
163
  await docker.build({
139
- image: 'registry.internal/api',
140
- tag: 'v1.2.0',
164
+ image: 'registry.internal/api:v1.2.0',
141
165
  dockerfile: 'Dockerfile.prod',
142
166
  buildArgs: { NODE_ENV: 'production' }
143
167
  })
@@ -146,8 +170,8 @@ await docker.build({
146
170
  await docker.push('registry.internal/api:v1.2.0')
147
171
 
148
172
  // Docker Compose 服务生命周期管理
149
- await docker.composeUp({ file: 'docker-compose.prod.yml', detached: true })
150
- await docker.composeDown({ file: 'docker-compose.prod.yml' })
173
+ await docker.composeUp({ files: 'docker-compose.prod.yml', detach: true })
174
+ await docker.composeDown({ files: 'docker-compose.prod.yml' })
151
175
  ```
152
176
 
153
177
  ---
@@ -158,23 +182,20 @@ await docker.composeDown({ file: 'docker-compose.prod.yml' })
158
182
 
159
183
  ### 运行器 (Runner) 示例:
160
184
  ```typescript
161
- import { runWithContext, createMockContext } from '@omniflow/core'
185
+ import { runWithContext, createMockContext } from '@ticatec/omniflow-core'
162
186
 
163
- const ctx = {
187
+ const ctx = createMockContext({
164
188
  runId: 'run-20260912-001',
165
189
  project: 'order-service',
166
- env: 'staging',
190
+ environment: 'staging',
167
191
  dryRun: false,
168
192
  workspace: '/workspaces/order-service',
169
- variables: { REGISTRY: 'harbor.company.com' },
170
- secrets: { DOCKER_TOKEN: 'secret-token-value' },
171
- logger: {
172
- info: (msg) => console.log(`[INFO] ${msg}`),
173
- warn: (msg) => console.warn(`[WARN] ${msg}`),
174
- error: (msg) => console.error(`[ERROR] ${msg}`),
175
- debug: (msg) => console.debug(`[DEBUG] ${msg}`)
193
+ projectRoot: '/workspaces/order-service',
194
+ env: {
195
+ REGISTRY: 'harbor.company.com',
196
+ DOCKER_TOKEN: 'secret-token-value'
176
197
  }
177
- }
198
+ })
178
199
 
179
200
  await runWithContext(ctx, async () => {
180
201
  // 在此处调用链中的任何深层异步函数,均可安全访问上下文
@@ -184,11 +205,11 @@ await runWithContext(ctx, async () => {
184
205
 
185
206
  ### 插件 (Plugin) 内部调用示例:
186
207
  ```typescript
187
- import { getContext, tryGetContext, shell } from '@omniflow/core'
208
+ import { getContext, tryGetContext, shell } from '@ticatec/omniflow-core'
188
209
 
189
210
  export async function myPluginTask() {
190
211
  const ctx = getContext() // 若不在 runWithContext 环境中则抛出明确错误
191
- ctx.logger.info(`正在构建工作区 ${ctx.workspace},目标环境:${ctx.env}`)
212
+ ctx.logger.info(`正在构建工作区 ${ctx.workspace},目标环境:${ctx.environment}`)
192
213
 
193
214
  // 底层 shell 自动采用 ctx.workspace 作为 cwd,自动继承环境变量
194
215
  await shell.run`npm test`
@@ -202,13 +223,13 @@ export async function myPluginTask() {
202
223
  OmniFlow Core 将不同编程语言的构建体系抽象为统一的 Service Provider Interface (SPI)。
203
224
 
204
225
  ### 内置工具链
205
- 1. **Maven** (`MavenToolchain`):检测 `pom.xml`,解析 groupId/artifactId/version 坐标,优先调用 `./mvnw` 或 `mvn`。
206
- 2. **Gradle** (`GradleToolchain`):检测 `build.gradle` / `build.gradle.kts`,优先使用 Gradle Wrapper `./gradlew`,支持多项目构建。
207
- 3. **Node** (`NodeToolchain`):检测 `package.json`,根据 lock 文件自动选择 `pnpm`、`yarn`、`npm` 或 `bun`,提取包名与版本。
226
+ 1. **Maven** (`MavenToolchain`):检测 `pom.xml`,解析 groupId/artifactId/version 坐标,优先调用 `./mvnw` 或 `mvn`(优先级:30)。
227
+ 2. **Gradle** (`GradleToolchain`):检测 `build.gradle` / `build.gradle.kts`,优先使用 Gradle Wrapper `./gradlew`,支持多项目构建(优先级:20)。
228
+ 3. **Node** (`NodeToolchain`):检测 `package.json`,根据 lock 文件自动选择 `pnpm`、`yarn`、`npm` 或 `bun`,提取包名与版本(优先级:10)。
208
229
 
209
230
  ### 使用与解析工具链:
210
231
  ```typescript
211
- import { resolveToolchain } from '@omniflow/core'
232
+ import { resolveToolchain } from '@ticatec/omniflow-core'
212
233
 
213
234
  const { provider, detection } = await resolveToolchain('/path/to/project')
214
235
  console.log(`检测到工具链 ${provider.name},原因: ${detection.reason}`)
@@ -221,42 +242,79 @@ await provider.install('/path/to/project')
221
242
  await provider.build('/path/to/project', ['-DskipTests'])
222
243
  ```
223
244
 
245
+ > [!NOTE]
246
+ > **可重现的 CI 安装策略**:`NodeToolchain.install()` 默认遵循 CI/CD 环境的最佳实践——当工程目录下存在 lockfile 时自动启用严格锁定模式(`npm ci`、`pnpm install --frozen-lockfile`、`yarn install --frozen-lockfile` / `--immutable`、`bun install --frozen-lockfile`),防止流水线静默篡改 lockfile 或引入未经锁定的依赖版本;新仓库无 lockfile 时则安全降级为普通安装,不会直接报错。
247
+
224
248
  ### 注册自定义工具链或覆盖默认行为:
225
249
  ```typescript
226
- import { registerToolchain, type ToolchainProvider } from '@omniflow/core'
227
-
228
- const goToolchain: ToolchainProvider = {
229
- name: 'go',
230
- priority: 20, // 优先级高于内置默认 Provider (10)
231
- detect: async (cwd) => {
232
- // 检测是否包含 go.mod
233
- return { detected: true, confidence: 0.95 }
234
- },
235
- getProjectInfo: async (cwd) => ({
236
- name: 'user-service',
237
- version: '1.0.0'
238
- }),
239
- getBuildCommand: (cwd, options) => 'go build -v -o dist/app ./cmd/main.go'
250
+ import {
251
+ registerToolchain,
252
+ shell,
253
+ type ToolchainProvider,
254
+ type DetectionResult,
255
+ type ProjectInfo
256
+ } from '@ticatec/omniflow-core'
257
+ import fs from 'node:fs/promises'
258
+ import path from 'node:path'
259
+
260
+ export class GoToolchain implements ToolchainProvider {
261
+ readonly name = 'go'
262
+ readonly priority: number = 50 // 优先级高于内置默认 Provider,在自动探测链中优先执行
263
+
264
+ async detect(projectDir: string): Promise<DetectionResult | null> {
265
+ try {
266
+ await fs.access(path.join(projectDir, 'go.mod'))
267
+ return { name: this.name, reason: 'found go.mod' }
268
+ } catch {
269
+ return null
270
+ }
271
+ }
272
+
273
+ async projectInfo(projectDir: string): Promise<ProjectInfo> {
274
+ return { name: 'user-service', version: '1.0.0', fullName: 'user-service' }
275
+ }
276
+
277
+ async install(projectDir: string, flags: string[] = []): Promise<void> {
278
+ await shell.run({ cwd: projectDir })`go mod download ${flags}`
279
+ }
280
+
281
+ async build(projectDir: string, flags: string[] = []): Promise<void> {
282
+ await shell.run({ cwd: projectDir })`go build -v -o dist/app ${flags}`
283
+ }
240
284
  }
241
285
 
242
286
  // 注册至全局注册表
243
- registerToolchain(goToolchain)
287
+ registerToolchain(new GoToolchain())
244
288
  ```
245
289
 
246
290
  > 关于创建自定义工具链(如 Go、Rust)及企业定制覆写的完整教程,详见 [Toolchain SPI 扩展指南](./docs/toolchain-extension_CN.md)。
247
291
 
248
292
  ---
249
293
 
294
+ ## v0.2.0 不兼容变更 (Breaking Changes)
295
+
296
+ 从早期实验性版本升级至 `0.2.0` 时,请注意以下行为与 API 变更:
297
+
298
+ 1. **`--dry-run` 模拟输出**:在空跑模式下,命令原语返回 `{ stdout: '', stderr: '', exitCode: 0, failed: false }`,不再向 stdout 注入 `"[DRY-RUN] ..."` 等伪造输出字符串。模拟执行的命令行直接通过 `ctx.logger.info` 记录。
299
+ 2. **`docker.build` 必填 image 参数**:`DockerBuildOptions` 中的 `image` 参数现为必填项。移除了隐式兜底至 `'latest'` 的行为及未文档化的别名参数(`tag`、`tags`),防止生产环境产生静默镜像错标。
300
+ 3. **`docker.composeUp` 参数命名规范化**:参数属性统一为 `files?: string | string[]` 与 `detach?: boolean`。移除了未经规范的遗留别名(`file`、`detached`)。
301
+ 4. **`ssh` 配置属性驼峰化**:SSH 目标配置严格遵循 camelCase 命名:`privateKey`、`privateKeyFile`。废弃并移除了 snake_case 别名(`private_key`、`private_key_file`)。
302
+ 5. **`resolveToolchain` 选项规范化**:选项类型统一为 `{ preferred?: string; fallback?: string }`。移除了历史遗留别名 `configured`。
303
+ 6. **命令启动失败退出码**:当子进程无法启动时(如可执行文件不存在导致 `exitCode` 为 `undefined`),统一返回 `exitCode: -1` 且 `failed: true`,修复了先前被误判为退出码 `0` 的缺陷。
304
+ 7. **`git.resetHard` 与 `git.checkout` 字符串参数语义**:在 v0.1.x 中,传入的单个字符串参数被视作工作目录路径 `dir`(如 `resetHard(dir?, target?)`、`checkout(dir?, ref?)`)。在 v0.2.0 中,裸字符串统一代表目标 commit 或 ref(如 `resetHard(target?: string | GitResetOptions)`、`checkout(ref?: string | GitCheckoutOptions)`)。若需显式指定工作目录,需使用 options 对象传入:`{ dir: '/path/to/repo' }`。
305
+
306
+ ---
307
+
250
308
  ## 模块子路径导出
251
309
 
252
- `@omniflow/core` 遵循现代 ESM 规范,支持精细化的子路径导入:
310
+ `@ticatec/omniflow-core` 遵循现代 ESM 规范,支持精细化的子路径导入:
253
311
 
254
312
  | 导出子路径 | 包含功能说明 |
255
313
  |:---|:---|
256
- | `@omniflow/core` | 全量包入口:包含上下文、原语、工具链 SPI 以及工具函数 |
257
- | `@omniflow/core/context` | 执行上下文、`AsyncLocalStorage` 封装、Mock 测试辅助工具 |
258
- | `@omniflow/core/primitives` | 命令原语(`shell`、`ssh`、`git`、`docker`) |
259
- | `@omniflow/core/toolchain` | Toolchain SPI 接口、注册表与内置 Provider |
314
+ | `@ticatec/omniflow-core` | 全量包入口:包含上下文、原语、工具链 SPI 以及工具函数 |
315
+ | `@ticatec/omniflow-core/context` | 执行上下文、`AsyncLocalStorage` 封装、Mock 测试辅助工具 |
316
+ | `@ticatec/omniflow-core/primitives` | 命令原语(`shell`、`ssh`、`git`、`docker`) |
317
+ | `@ticatec/omniflow-core/toolchain` | Toolchain SPI 接口、注册表与内置 Provider |
260
318
 
261
319
  ---
262
320