@ticatec/omniflow-core 0.1.0 → 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
@@ -0,0 +1,157 @@
1
+ import path from 'node:path'
2
+ import { execa, type Options as ExecaOptions } from 'execa'
3
+ import { tryGetContext } from '../context/index.js'
4
+ import { templateToArgv } from '../utils/template.js'
5
+ import { maskString } from '../utils/mask.js'
6
+ import {
7
+ resolveEnv,
8
+ getSecretValues,
9
+ runSubprocess,
10
+ createMaskTransform
11
+ } from './subprocess.js'
12
+
13
+ export { createMaskTransform }
14
+
15
+ export interface RunOptions {
16
+ /** Working directory (defaults to context's active module or workspace) */
17
+ cwd?: string
18
+ /** Extra environment variables merged over process and context env */
19
+ env?: Record<string, string>
20
+ /** Reject promise on non-zero exit code (default: true) */
21
+ reject?: boolean
22
+ /** Capture and return stdout / stderr */
23
+ capture?: boolean
24
+ }
25
+
26
+ export interface ShellOutputResult {
27
+ stdout: string
28
+ stderr: string
29
+ exitCode: number
30
+ failed?: boolean
31
+ signal?: string
32
+ }
33
+
34
+ function resolveCwd(explicitCwd?: string): string {
35
+ if (explicitCwd) return explicitCwd
36
+ const ctx = tryGetContext()
37
+ if (ctx) {
38
+ if (ctx.moduleFolder) {
39
+ return path.resolve(ctx.workspace, ctx.moduleFolder)
40
+ }
41
+ if (ctx.workspace) {
42
+ return ctx.workspace
43
+ }
44
+ }
45
+ return process.cwd()
46
+ }
47
+
48
+ /**
49
+ * Execute command via execa, piping output and handling dryRun.
50
+ */
51
+ async function executeInternal(
52
+ fileOrCmd: string,
53
+ args: string[] | undefined,
54
+ options: RunOptions & { isShell?: boolean }
55
+ ): Promise<ShellOutputResult> {
56
+ const ctx = tryGetContext()
57
+ const cwd = resolveCwd(options.cwd)
58
+ const env = resolveEnv(options.env)
59
+ const secrets = getSecretValues()
60
+ const cmdDisplay = args ? [fileOrCmd, ...args].join(' ') : fileOrCmd
61
+ const maskedCmd = maskString(cmdDisplay, secrets)
62
+
63
+ // Dry-run interception
64
+ if (ctx?.dryRun) {
65
+ ctx.logger.info(`[DRY-RUN] Executing: ${maskedCmd} (cwd: ${cwd})`)
66
+ return {
67
+ stdout: '',
68
+ stderr: '',
69
+ exitCode: 0,
70
+ failed: false
71
+ }
72
+ }
73
+
74
+ const execaOpts: ExecaOptions = {
75
+ cwd,
76
+ env,
77
+ reject: options.reject ?? true,
78
+ shell: options.isShell ?? false
79
+ }
80
+
81
+ const displayCmd = `$ ${maskedCmd}`
82
+ ctx?.logger?.info?.(displayCmd)
83
+
84
+ const subprocess = args
85
+ ? execa(fileOrCmd, args, execaOpts)
86
+ : execa(fileOrCmd, execaOpts)
87
+
88
+ return runSubprocess({
89
+ subprocess,
90
+ displayCmd,
91
+ cmdText: maskedCmd,
92
+ errorPrefix: 'Command',
93
+ reject: options.reject ?? true,
94
+ secrets
95
+ })
96
+ }
97
+
98
+ /**
99
+ * Tagged template runner signature.
100
+ */
101
+ export interface TemplateRunner {
102
+ (template: TemplateStringsArray, ...values: unknown[]): Promise<ShellOutputResult>
103
+ }
104
+
105
+ /**
106
+ * `shell.run` runner function supporting:
107
+ * 1. `shell.run`docker build -t ${img} .``
108
+ * 2. `shell.run({ cwd: '/app' })`docker build -t ${img} .``
109
+ */
110
+ export function run(options: RunOptions): TemplateRunner
111
+ export function run(template: TemplateStringsArray, ...values: unknown[]): Promise<ShellOutputResult>
112
+ export function run(
113
+ templateOrOptions: TemplateStringsArray | RunOptions,
114
+ ...values: unknown[]
115
+ ): Promise<ShellOutputResult> | TemplateRunner {
116
+ // Overload 2: options passed first, returns a TemplateRunner
117
+ if (!Array.isArray(templateOrOptions) || !('raw' in templateOrOptions)) {
118
+ const options = (templateOrOptions as RunOptions) ?? {}
119
+ return async (template: TemplateStringsArray, ...innerValues: unknown[]) => {
120
+ const argv = templateToArgv(template, innerValues)
121
+ if (argv.length === 0) {
122
+ throw new Error('Command is empty')
123
+ }
124
+ const [cmd, ...args] = argv
125
+ return executeInternal(cmd, args, options)
126
+ }
127
+ }
128
+
129
+ // Overload 1: direct tagged template
130
+ const template = templateOrOptions as TemplateStringsArray
131
+ const argv = templateToArgv(template, values)
132
+ if (argv.length === 0) {
133
+ throw new Error('Command is empty')
134
+ }
135
+ const [cmd, ...args] = argv
136
+ return executeInternal(cmd, args, {})
137
+ }
138
+
139
+ /**
140
+ * Run a command string via the shell (supports pipes, redirects, &&, ||).
141
+ */
142
+ export async function sh(cmd: string, options: RunOptions = {}): Promise<ShellOutputResult> {
143
+ return executeInternal(cmd, undefined, { ...options, isShell: true })
144
+ }
145
+
146
+ /**
147
+ * Run a command and capture its stdout, stderr and exitCode without throwing on failure.
148
+ */
149
+ export async function output(cmd: string, options: RunOptions = {}): Promise<ShellOutputResult> {
150
+ return executeInternal(cmd, undefined, { ...options, isShell: true, reject: false, capture: true })
151
+ }
152
+
153
+ export const shell = {
154
+ run,
155
+ sh,
156
+ output
157
+ }
@@ -0,0 +1,249 @@
1
+ import * as fs from 'node:fs/promises'
2
+ import * as path from 'node:path'
3
+ import { tmpdir } from 'node:os'
4
+ import { execa } from 'execa'
5
+ import { tryGetContext } from '../context/index.js'
6
+ import { maskString } from '../utils/mask.js'
7
+ import {
8
+ getSecretValues,
9
+ resolveEnv,
10
+ runSubprocess
11
+ } from './subprocess.js'
12
+
13
+ export interface SshServerConfig {
14
+ host?: string
15
+ server?: string
16
+ user: string
17
+ privateKeyFile?: string
18
+ privateKey?: string
19
+ port?: number
20
+ tty?: boolean
21
+ connectTimeout?: number
22
+ }
23
+
24
+ export interface SshAuthConfig {
25
+ user: string
26
+ privateKey?: string
27
+ privateKeyFile?: string
28
+ }
29
+
30
+ export interface SshConnectionConfig extends SshAuthConfig {
31
+ host: string
32
+ port?: number
33
+ tty?: boolean
34
+ connectTimeout?: number
35
+ }
36
+
37
+ export type SshTarget = string | SshConnectionConfig
38
+
39
+ export interface SshResult {
40
+ stdout: string
41
+ stderr: string
42
+ exitCode: number
43
+ failed?: boolean
44
+ signal?: string
45
+ }
46
+
47
+ function resolveTarget(
48
+ target: SshTarget,
49
+ configMap: Record<string, SshServerConfig> = {}
50
+ ): SshConnectionConfig {
51
+ if (typeof target !== 'string') {
52
+ if (!target.host || !target.user) {
53
+ throw new Error('SSH target config requires both "host" and "user"')
54
+ }
55
+ return target
56
+ }
57
+
58
+ const config = configMap[target]
59
+ if (!config) {
60
+ const known = Object.keys(configMap)
61
+ throw new Error(
62
+ `SSH target configuration not found for key: "${target}". ` +
63
+ (known.length ? `Configured targets: ${known.join(', ')}` : 'No SSH targets configured.')
64
+ )
65
+ }
66
+
67
+ const host = config.host || config.server
68
+ if (!host) {
69
+ throw new Error(`SSH config '${target}' is missing host/server address`)
70
+ }
71
+
72
+ return {
73
+ host,
74
+ user: config.user,
75
+ privateKeyFile: config.privateKeyFile,
76
+ privateKey: config.privateKey,
77
+ port: config.port,
78
+ connectTimeout: config.connectTimeout,
79
+ tty: config.tty
80
+ }
81
+ }
82
+
83
+ async function createTempKeyFile(keyContent: string): Promise<string> {
84
+ const dir = await fs.mkdtemp(path.join(tmpdir(), 'omniflow-ssh-'))
85
+ const keyFile = path.join(dir, 'id_rsa')
86
+ await fs.writeFile(keyFile, keyContent, { mode: 0o600 })
87
+ return keyFile
88
+ }
89
+
90
+ function connectionArgs(
91
+ config: SshConnectionConfig,
92
+ keyFile: string | undefined,
93
+ portFlag: '-p' | '-P'
94
+ ): string[] {
95
+ const args = [
96
+ '-o', 'BatchMode=yes',
97
+ '-o', 'StrictHostKeyChecking=accept-new',
98
+ '-o', `ConnectTimeout=${config.connectTimeout ?? 15}`,
99
+ portFlag, String(config.port ?? 22)
100
+ ]
101
+ if (keyFile) {
102
+ args.push('-i', keyFile)
103
+ }
104
+ return args
105
+ }
106
+
107
+ function shellQuote(str: string): string {
108
+ if (/^[a-zA-Z0-9_./-]+$/.test(str)) return str
109
+ return `'${str.replace(/'/g, "'\\''")}'`
110
+ }
111
+
112
+ export class SshClient {
113
+ private readonly configMap: Record<string, SshServerConfig>
114
+
115
+ constructor(configMap: Record<string, SshServerConfig> = {}) {
116
+ this.configMap = configMap
117
+ }
118
+
119
+ async exec(
120
+ target: SshTarget,
121
+ remoteCommand: string,
122
+ remoteDir?: string
123
+ ): Promise<SshResult> {
124
+ const ctx = tryGetContext()
125
+ const config = resolveTarget(target, this.configMap)
126
+ const secrets = getSecretValues()
127
+ const maskedCmd = maskString(remoteCommand, secrets)
128
+
129
+ if (ctx?.dryRun) {
130
+ ctx.logger.info(`[DRY-RUN] SSH to ${config.user}@${config.host}: ${maskedCmd} (remoteDir: ${remoteDir ?? '~'})`)
131
+ return { stdout: '', stderr: '', exitCode: 0, failed: false }
132
+ }
133
+
134
+ let tempKeyFile: string | undefined
135
+ try {
136
+ if (config.privateKey) {
137
+ tempKeyFile = await createTempKeyFile(config.privateKey)
138
+ }
139
+
140
+ const keyFile = tempKeyFile || config.privateKeyFile
141
+ const args = connectionArgs(config, keyFile, '-p')
142
+
143
+ if (config.tty) {
144
+ args.push('-tt')
145
+ }
146
+
147
+ args.push(`${config.user}@${config.host}`)
148
+
149
+ const script = remoteDir
150
+ ? `cd ${shellQuote(remoteDir)} || exit 1\n${remoteCommand}`
151
+ : remoteCommand
152
+
153
+ args.push(script)
154
+
155
+ const displayCmd = `SSH (${config.user}@${config.host}) $ ${maskedCmd}`
156
+ ctx?.logger?.info?.(displayCmd)
157
+
158
+ const subprocess = execa('ssh', args, { reject: true, env: resolveEnv() })
159
+
160
+ return await runSubprocess({
161
+ subprocess,
162
+ displayCmd,
163
+ cmdText: maskedCmd,
164
+ errorPrefix: 'SSH command',
165
+ reject: true,
166
+ secrets
167
+ })
168
+ } finally {
169
+ if (tempKeyFile) {
170
+ await fs.rm(path.dirname(tempKeyFile), { recursive: true, force: true }).catch(() => {})
171
+ }
172
+ }
173
+ }
174
+
175
+ async cp(
176
+ target: SshTarget,
177
+ srcFile: string,
178
+ targetFolder: string
179
+ ): Promise<void> {
180
+ const ctx = tryGetContext()
181
+ const config = resolveTarget(target, this.configMap)
182
+ const secrets = getSecretValues()
183
+
184
+ if (ctx?.dryRun) {
185
+ ctx.logger.info(`[DRY-RUN] SCP: Copy ${srcFile} -> ${config.user}@${config.host}:${targetFolder}`)
186
+ return
187
+ }
188
+
189
+ let tempKeyFile: string | undefined
190
+ try {
191
+ if (config.privateKey) {
192
+ tempKeyFile = await createTempKeyFile(config.privateKey)
193
+ }
194
+
195
+ const keyFile = tempKeyFile || config.privateKeyFile
196
+ const args = connectionArgs(config, keyFile, '-P')
197
+
198
+ args.push(srcFile)
199
+ args.push(`${config.user}@${config.host}:${targetFolder}`)
200
+
201
+ const displayCmd = `SCP ${srcFile} -> ${config.user}@${config.host}:${targetFolder}`
202
+ ctx?.logger?.info?.(displayCmd)
203
+
204
+ const subprocess = execa('scp', args, { reject: true, env: resolveEnv() })
205
+
206
+ await runSubprocess({
207
+ subprocess,
208
+ displayCmd,
209
+ cmdText: displayCmd,
210
+ errorPrefix: 'SCP',
211
+ reject: true,
212
+ secrets
213
+ })
214
+ } finally {
215
+ if (tempKeyFile) {
216
+ await fs.rm(path.dirname(tempKeyFile), { recursive: true, force: true }).catch(() => {})
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ /** Default singleton instance helper */
223
+ const defaultClientConfigMap: Record<string, SshServerConfig> = {}
224
+ const defaultClient = new SshClient(defaultClientConfigMap)
225
+
226
+ export const ssh = {
227
+ exec: (target: SshTarget, command: string, remoteDir?: string) =>
228
+ defaultClient.exec(target, command, remoteDir),
229
+ cp: (target: SshTarget, srcFile: string, targetFolder: string) =>
230
+ defaultClient.cp(target, srcFile, targetFolder),
231
+ createClient: (configMap: Record<string, SshServerConfig>) =>
232
+ new SshClient(configMap),
233
+ configure: (
234
+ nameOrMap: string | Record<string, SshServerConfig>,
235
+ config?: SshServerConfig
236
+ ) => {
237
+ if (typeof nameOrMap === 'string' && config) {
238
+ defaultClientConfigMap[nameOrMap] = config
239
+ } else if (typeof nameOrMap === 'object' && nameOrMap !== null) {
240
+ Object.assign(defaultClientConfigMap, nameOrMap)
241
+ }
242
+ },
243
+ /** Reset/clear all configured SSH targets on the singleton instance */
244
+ reset: () => {
245
+ for (const key of Object.keys(defaultClientConfigMap)) {
246
+ delete defaultClientConfigMap[key]
247
+ }
248
+ }
249
+ }