@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.
- package/LICENSE +21 -0
- package/README.md +112 -54
- package/README_CN.md +113 -55
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/plugin/PluginContext.d.ts +39 -0
- package/dist/plugin/PluginContext.d.ts.map +1 -0
- package/dist/plugin/PluginContext.js +8 -0
- package/dist/plugin/PluginContext.js.map +1 -0
- package/dist/primitives/docker.d.ts.map +1 -1
- package/dist/primitives/docker.js +9 -0
- package/dist/primitives/docker.js.map +1 -1
- package/dist/primitives/git.d.ts +58 -16
- package/dist/primitives/git.d.ts.map +1 -1
- package/dist/primitives/git.js +91 -33
- package/dist/primitives/git.js.map +1 -1
- package/dist/primitives/shell.d.ts +4 -0
- package/dist/primitives/shell.d.ts.map +1 -1
- package/dist/primitives/shell.js +20 -60
- package/dist/primitives/shell.js.map +1 -1
- package/dist/primitives/ssh.d.ts +8 -5
- package/dist/primitives/ssh.d.ts.map +1 -1
- package/dist/primitives/ssh.js +49 -14
- package/dist/primitives/ssh.js.map +1 -1
- package/dist/primitives/subprocess.d.ts +52 -0
- package/dist/primitives/subprocess.d.ts.map +1 -0
- package/dist/primitives/subprocess.js +353 -0
- package/dist/primitives/subprocess.js.map +1 -0
- package/dist/toolchain/providers/GradleToolchain.d.ts +1 -1
- package/dist/toolchain/providers/GradleToolchain.d.ts.map +1 -1
- package/dist/toolchain/providers/GradleToolchain.js +4 -3
- package/dist/toolchain/providers/GradleToolchain.js.map +1 -1
- package/dist/toolchain/providers/MavenToolchain.d.ts +1 -1
- package/dist/toolchain/providers/MavenToolchain.d.ts.map +1 -1
- package/dist/toolchain/providers/MavenToolchain.js +3 -3
- package/dist/toolchain/providers/MavenToolchain.js.map +1 -1
- package/dist/toolchain/providers/NodeToolchain.d.ts +1 -1
- package/dist/toolchain/providers/NodeToolchain.d.ts.map +1 -1
- package/dist/toolchain/providers/NodeToolchain.js +61 -0
- package/dist/toolchain/providers/NodeToolchain.js.map +1 -1
- package/dist/toolchain/providers/pom.d.ts.map +1 -1
- package/dist/toolchain/providers/pom.js +13 -0
- package/dist/toolchain/providers/pom.js.map +1 -1
- package/dist/toolchain/registry.d.ts +11 -3
- package/dist/toolchain/registry.d.ts.map +1 -1
- package/dist/toolchain/registry.js +33 -8
- package/dist/toolchain/registry.js.map +1 -1
- package/dist/utils/mask.d.ts.map +1 -1
- package/dist/utils/mask.js +30 -5
- package/dist/utils/mask.js.map +1 -1
- package/docs/toolchain-extension.md +301 -0
- package/docs/toolchain-extension_CN.md +304 -0
- package/package.json +23 -1
- package/src/context/index.ts +74 -0
- package/src/context/storage.ts +8 -0
- package/src/context/types.ts +69 -0
- package/src/index.ts +97 -0
- package/src/plugin/PluginContext.ts +57 -0
- package/src/primitives/docker.ts +164 -0
- package/src/primitives/git.ts +172 -0
- package/src/primitives/index.ts +4 -0
- package/src/primitives/shell.ts +157 -0
- package/src/primitives/ssh.ts +249 -0
- package/src/primitives/subprocess.ts +389 -0
- package/src/toolchain/index.ts +6 -0
- package/src/toolchain/providers/GradleToolchain.ts +137 -0
- package/src/toolchain/providers/MavenToolchain.ts +64 -0
- package/src/toolchain/providers/NodeToolchain.ts +172 -0
- package/src/toolchain/providers/pom.ts +145 -0
- package/src/toolchain/registry.ts +161 -0
- package/src/toolchain/types.ts +40 -0
- package/src/utils/mask.ts +73 -0
- package/src/utils/template.ts +62 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { Transform } from 'node:stream'
|
|
4
|
+
import type { execa } from 'execa'
|
|
5
|
+
import { tryGetContext } from '../context/index.js'
|
|
6
|
+
import { isSecretKey, maskString } from '../utils/mask.js'
|
|
7
|
+
|
|
8
|
+
export interface SubprocessResult {
|
|
9
|
+
stdout: string
|
|
10
|
+
stderr: string
|
|
11
|
+
exitCode: number
|
|
12
|
+
failed: boolean
|
|
13
|
+
signal?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SubprocessRunOptions {
|
|
17
|
+
subprocess: ReturnType<typeof execa>
|
|
18
|
+
displayCmd: string
|
|
19
|
+
cmdText?: string
|
|
20
|
+
errorPrefix?: string
|
|
21
|
+
reject?: boolean
|
|
22
|
+
secrets?: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve process.env merged with context.env and any explicit overrides.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveEnv(explicitEnv?: Record<string, string>): Record<string, string> {
|
|
29
|
+
const ctx = tryGetContext()
|
|
30
|
+
const base = { ...process.env }
|
|
31
|
+
if (ctx?.env) {
|
|
32
|
+
Object.assign(base, ctx.env)
|
|
33
|
+
}
|
|
34
|
+
if (explicitEnv) {
|
|
35
|
+
Object.assign(base, explicitEnv)
|
|
36
|
+
}
|
|
37
|
+
return base as Record<string, string>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract active secret values from execution context.
|
|
42
|
+
*/
|
|
43
|
+
export function getSecretValues(): string[] {
|
|
44
|
+
const ctx = tryGetContext()
|
|
45
|
+
if (!ctx?.env) return []
|
|
46
|
+
const secrets: string[] = []
|
|
47
|
+
for (const [k, v] of Object.entries(ctx.env)) {
|
|
48
|
+
if (isSecretKey(k) && typeof v === 'string' && v.length >= 3) {
|
|
49
|
+
secrets.push(v)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return secrets
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a stateful Transform stream that masks secrets across chunk boundaries.
|
|
57
|
+
* Retains up to maxSecretLen - 1 bytes in tail buffer to prevent boundary leaks.
|
|
58
|
+
*/
|
|
59
|
+
export function createMaskTransform(secrets: string[]): Transform {
|
|
60
|
+
const activeSecrets = secrets
|
|
61
|
+
.filter(s => typeof s === 'string' && s.length >= 3)
|
|
62
|
+
.flatMap(s => s.includes('\n') ? s.split(/\r?\n/).map(l => l.trim()).filter(l => l.length >= 3) : [s])
|
|
63
|
+
|
|
64
|
+
if (activeSecrets.length === 0) {
|
|
65
|
+
return new Transform({
|
|
66
|
+
transform(chunk, _encoding, callback) {
|
|
67
|
+
callback(null, chunk)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Sort descending by length so longer matches are prioritized
|
|
73
|
+
activeSecrets.sort((a, b) => b.length - a.length)
|
|
74
|
+
|
|
75
|
+
const maxSecretLen = Math.max(...activeSecrets.map(s => s.length))
|
|
76
|
+
const keepLen = Math.max(0, maxSecretLen - 1)
|
|
77
|
+
let tail = ''
|
|
78
|
+
|
|
79
|
+
return new Transform({
|
|
80
|
+
transform(chunk, _encoding, callback) {
|
|
81
|
+
const text = tail + chunk.toString('utf8')
|
|
82
|
+
const masked = maskString(text, activeSecrets)
|
|
83
|
+
|
|
84
|
+
if (masked.length > keepLen) {
|
|
85
|
+
const emitLen = masked.length - keepLen
|
|
86
|
+
const toEmit = masked.slice(0, emitLen)
|
|
87
|
+
tail = masked.slice(emitLen)
|
|
88
|
+
callback(null, Buffer.from(toEmit, 'utf8'))
|
|
89
|
+
} else {
|
|
90
|
+
tail = masked
|
|
91
|
+
callback()
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
flush(callback) {
|
|
95
|
+
if (tail.length > 0) {
|
|
96
|
+
const finalMasked = maskString(tail, activeSecrets)
|
|
97
|
+
tail = ''
|
|
98
|
+
callback(null, Buffer.from(finalMasked, 'utf8'))
|
|
99
|
+
} else {
|
|
100
|
+
callback()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Prefix every line in the stream with a tag (e.g. PID).
|
|
108
|
+
*/
|
|
109
|
+
export function createLinePrefixTransform(prefix: string): Transform {
|
|
110
|
+
if (!prefix) {
|
|
111
|
+
return new Transform({
|
|
112
|
+
transform(chunk, _encoding, callback) {
|
|
113
|
+
callback(null, chunk)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let remainder = ''
|
|
119
|
+
return new Transform({
|
|
120
|
+
transform(chunk, _encoding, callback) {
|
|
121
|
+
const text = remainder + chunk.toString('utf8')
|
|
122
|
+
const lines = text.split('\n')
|
|
123
|
+
remainder = lines.pop() ?? ''
|
|
124
|
+
const out = lines.map(l => `${prefix}${l}\n`).join('')
|
|
125
|
+
callback(null, Buffer.from(out, 'utf8'))
|
|
126
|
+
},
|
|
127
|
+
flush(callback) {
|
|
128
|
+
if (remainder.length > 0) {
|
|
129
|
+
callback(null, Buffer.from(`${prefix}${remainder}\n`, 'utf8'))
|
|
130
|
+
} else {
|
|
131
|
+
callback()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Open appendable write stream to the log file, auto-creating directories.
|
|
139
|
+
*/
|
|
140
|
+
export function openLogStream(logFile: string, logger?: { warn?: (msg: string) => void }): fs.WriteStream | undefined {
|
|
141
|
+
try {
|
|
142
|
+
const logDir = path.dirname(logFile)
|
|
143
|
+
if (!fs.existsSync(logDir)) {
|
|
144
|
+
fs.mkdirSync(logDir, { recursive: true })
|
|
145
|
+
}
|
|
146
|
+
if (fs.existsSync(logFile) && fs.statSync(logFile).isDirectory()) {
|
|
147
|
+
logger?.warn?.(`Failed to open logFile ${logFile}: path is a directory`)
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
const logStream = fs.createWriteStream(logFile, { flags: 'a' })
|
|
151
|
+
logStream.on('error', (err) => {
|
|
152
|
+
logger?.warn?.(`Failed to write to logFile ${logFile}: ${err.message}`)
|
|
153
|
+
})
|
|
154
|
+
return logStream
|
|
155
|
+
} catch (err: any) {
|
|
156
|
+
logger?.warn?.(`Failed to open logFile ${logFile}: ${err?.message}`)
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Mask all known string and stdio properties on an ExecaError.
|
|
163
|
+
*/
|
|
164
|
+
export function maskExecaError(thrownError: any, secrets: string[]): any {
|
|
165
|
+
if (!thrownError) return thrownError
|
|
166
|
+
if (secrets.length > 0) {
|
|
167
|
+
const props = ['message', 'shortMessage', 'originalMessage', 'command', 'escapedCommand', 'stack', 'all']
|
|
168
|
+
for (const prop of props) {
|
|
169
|
+
if (typeof thrownError[prop] === 'string') {
|
|
170
|
+
thrownError[prop] = maskString(thrownError[prop], secrets)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (Array.isArray(thrownError.stdio)) {
|
|
174
|
+
thrownError.stdio = thrownError.stdio.map((item: unknown) => {
|
|
175
|
+
if (typeof item === 'string') {
|
|
176
|
+
return maskString(item, secrets)
|
|
177
|
+
}
|
|
178
|
+
if (item && typeof (item as any).toString === 'function' && Buffer.isBuffer(item)) {
|
|
179
|
+
return Buffer.from(maskString(item.toString('utf-8'), secrets))
|
|
180
|
+
}
|
|
181
|
+
return item
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return thrownError
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let cmdCounter = 0
|
|
189
|
+
|
|
190
|
+
export function resetCmdCounter(): void {
|
|
191
|
+
cmdCounter = 0
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Unified subprocess runner: pipes stdout/stderr to logFile with PID prefix and masking,
|
|
196
|
+
* manages stream flushing, exit code logging, failure log level downgrade, and ExecaError masking.
|
|
197
|
+
*/
|
|
198
|
+
export async function runSubprocess(options: SubprocessRunOptions): Promise<SubprocessResult> {
|
|
199
|
+
const { subprocess, displayCmd, reject = true } = options
|
|
200
|
+
const ctx = tryGetContext()
|
|
201
|
+
const secrets = options.secrets ?? getSecretValues()
|
|
202
|
+
const pid = subprocess.pid
|
|
203
|
+
const pidTag = pid ? `[PID ${pid}] ` : `[CMD ${++cmdCounter}] `
|
|
204
|
+
const cmdText = options.cmdText ?? displayCmd
|
|
205
|
+
const errorPrefix = options.errorPrefix ?? 'Command'
|
|
206
|
+
|
|
207
|
+
let logStream: fs.WriteStream | undefined
|
|
208
|
+
if (ctx?.logFile) {
|
|
209
|
+
logStream = openLogStream(ctx.logFile, ctx.logger)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (logStream && !logStream.destroyed && logStream.writable) {
|
|
213
|
+
try {
|
|
214
|
+
logStream.write(`${pidTag}${displayCmd}\n`)
|
|
215
|
+
} catch {}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let lineStdout: Transform | undefined
|
|
219
|
+
let lineStderr: Transform | undefined
|
|
220
|
+
const streamDone: Promise<void>[] = []
|
|
221
|
+
|
|
222
|
+
const unpipeAndDrain = () => {
|
|
223
|
+
if (lineStdout && logStream) {
|
|
224
|
+
try {
|
|
225
|
+
lineStdout.unpipe(logStream)
|
|
226
|
+
lineStdout.resume()
|
|
227
|
+
} catch {}
|
|
228
|
+
}
|
|
229
|
+
if (lineStderr && logStream) {
|
|
230
|
+
try {
|
|
231
|
+
lineStderr.unpipe(logStream)
|
|
232
|
+
lineStderr.resume()
|
|
233
|
+
} catch {}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (logStream) {
|
|
238
|
+
logStream.on('error', unpipeAndDrain)
|
|
239
|
+
logStream.on('close', unpipeAndDrain)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (subprocess.stdout && logStream) {
|
|
243
|
+
const maskStdout = createMaskTransform(secrets)
|
|
244
|
+
lineStdout = createLinePrefixTransform(pidTag)
|
|
245
|
+
maskStdout.pipe(lineStdout).pipe(logStream, { end: false })
|
|
246
|
+
subprocess.stdout.pipe(maskStdout)
|
|
247
|
+
streamDone.push(new Promise(resolve => {
|
|
248
|
+
let resolved = false
|
|
249
|
+
const done = () => {
|
|
250
|
+
if (!resolved) {
|
|
251
|
+
resolved = true
|
|
252
|
+
resolve()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
lineStdout!.once('end', done)
|
|
256
|
+
lineStdout!.once('close', done)
|
|
257
|
+
lineStdout!.once('error', done)
|
|
258
|
+
if (logStream) {
|
|
259
|
+
logStream.once('error', done)
|
|
260
|
+
logStream.once('close', done)
|
|
261
|
+
}
|
|
262
|
+
}))
|
|
263
|
+
}
|
|
264
|
+
if (subprocess.stderr && logStream) {
|
|
265
|
+
const maskStderr = createMaskTransform(secrets)
|
|
266
|
+
lineStderr = createLinePrefixTransform(pidTag)
|
|
267
|
+
maskStderr.pipe(lineStderr).pipe(logStream, { end: false })
|
|
268
|
+
subprocess.stderr.pipe(maskStderr)
|
|
269
|
+
streamDone.push(new Promise(resolve => {
|
|
270
|
+
let resolved = false
|
|
271
|
+
const done = () => {
|
|
272
|
+
if (!resolved) {
|
|
273
|
+
resolved = true
|
|
274
|
+
resolve()
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
lineStderr!.once('end', done)
|
|
278
|
+
lineStderr!.once('close', done)
|
|
279
|
+
lineStderr!.once('error', done)
|
|
280
|
+
if (logStream) {
|
|
281
|
+
logStream.once('error', done)
|
|
282
|
+
logStream.once('close', done)
|
|
283
|
+
}
|
|
284
|
+
}))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let execResult: any
|
|
288
|
+
let thrownError: any
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
execResult = await subprocess
|
|
292
|
+
} catch (err: any) {
|
|
293
|
+
thrownError = err
|
|
294
|
+
execResult = err
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Always wait for stream completion regardless of subprocess exit status, with timeout safety
|
|
298
|
+
if (streamDone.length > 0) {
|
|
299
|
+
let timeoutId: NodeJS.Timeout | undefined
|
|
300
|
+
const drainTimeout = new Promise<void>(resolve => {
|
|
301
|
+
timeoutId = setTimeout(resolve, 3000)
|
|
302
|
+
timeoutId.unref?.()
|
|
303
|
+
})
|
|
304
|
+
await Promise.race([Promise.all(streamDone), drainTimeout])
|
|
305
|
+
if (timeoutId) {
|
|
306
|
+
clearTimeout(timeoutId)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const isFailed = Boolean(execResult.failed || (execResult.exitCode !== undefined && execResult.exitCode !== 0))
|
|
312
|
+
let exitCode = execResult.exitCode
|
|
313
|
+
if (exitCode === undefined) {
|
|
314
|
+
exitCode = isFailed ? -1 : 0
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (logStream && !logStream.destroyed && logStream.writable) {
|
|
318
|
+
try {
|
|
319
|
+
logStream.write(`${pidTag}[Process exited with code ${exitCode}]\n`)
|
|
320
|
+
} catch {}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (isFailed) {
|
|
324
|
+
if (reject === false) {
|
|
325
|
+
ctx?.logger?.debug?.(`${errorPrefix} exited with code ${exitCode} (reject: false): ${cmdText}`)
|
|
326
|
+
} else {
|
|
327
|
+
ctx?.logger?.error?.(`${errorPrefix} failed with exit code ${exitCode}: ${cmdText}`)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const rawStdout = typeof execResult.stdout === 'string' ? execResult.stdout : (execResult.stdout ? String(execResult.stdout) : '')
|
|
332
|
+
const rawStderr = typeof execResult.stderr === 'string' ? execResult.stderr : (execResult.stderr ? String(execResult.stderr) : '')
|
|
333
|
+
|
|
334
|
+
const stdout = secrets.length > 0 ? maskString(rawStdout, secrets) : rawStdout
|
|
335
|
+
const stderr = secrets.length > 0 ? maskString(rawStderr, secrets) : rawStderr
|
|
336
|
+
|
|
337
|
+
if (thrownError) {
|
|
338
|
+
maskExecaError(thrownError, secrets)
|
|
339
|
+
thrownError.stdout = stdout
|
|
340
|
+
thrownError.stderr = stderr
|
|
341
|
+
thrownError.exitCode = exitCode
|
|
342
|
+
thrownError.failed = true
|
|
343
|
+
|
|
344
|
+
if (reject === false) {
|
|
345
|
+
return {
|
|
346
|
+
stdout,
|
|
347
|
+
stderr,
|
|
348
|
+
exitCode,
|
|
349
|
+
failed: true,
|
|
350
|
+
signal: execResult.signal
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
throw thrownError
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
stdout,
|
|
358
|
+
stderr,
|
|
359
|
+
exitCode,
|
|
360
|
+
failed: isFailed,
|
|
361
|
+
signal: execResult.signal
|
|
362
|
+
}
|
|
363
|
+
} finally {
|
|
364
|
+
if (logStream) {
|
|
365
|
+
await new Promise<void>(resolve => {
|
|
366
|
+
if (logStream!.destroyed || logStream!.closed || !logStream!.writable) {
|
|
367
|
+
resolve()
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
let settled = false
|
|
371
|
+
let timer: NodeJS.Timeout | undefined
|
|
372
|
+
const done = () => {
|
|
373
|
+
if (!settled) {
|
|
374
|
+
settled = true
|
|
375
|
+
if (timer) {
|
|
376
|
+
clearTimeout(timer)
|
|
377
|
+
}
|
|
378
|
+
resolve()
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
timer = setTimeout(done, 1000)
|
|
382
|
+
timer.unref?.()
|
|
383
|
+
logStream!.once('error', done)
|
|
384
|
+
logStream!.once('close', done)
|
|
385
|
+
logStream!.end(done)
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './types.js'
|
|
2
|
+
export * from './registry.js'
|
|
3
|
+
export { MavenToolchain } from './providers/MavenToolchain.js'
|
|
4
|
+
export { GradleToolchain } from './providers/GradleToolchain.js'
|
|
5
|
+
export { NodeToolchain } from './providers/NodeToolchain.js'
|
|
6
|
+
export { readPom, parseXml } from './providers/pom.js'
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import { shell } from '../../primitives/shell.js'
|
|
4
|
+
import type { ToolchainProvider, DetectionResult, ProjectInfo } from '../types.js'
|
|
5
|
+
|
|
6
|
+
const BUILD_FILES = ['build.gradle', 'build.gradle.kts']
|
|
7
|
+
const SETTINGS_FILES = ['settings.gradle', 'settings.gradle.kts']
|
|
8
|
+
|
|
9
|
+
async function readIfPresent(file: string): Promise<string | undefined> {
|
|
10
|
+
try {
|
|
11
|
+
return await fs.readFile(file, 'utf-8')
|
|
12
|
+
} catch {
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fromProperties(text: string, key: string): string | undefined {
|
|
18
|
+
for (const line of text.split('\n')) {
|
|
19
|
+
const trimmed = line.trim()
|
|
20
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('!')) continue
|
|
21
|
+
const eq = trimmed.indexOf('=')
|
|
22
|
+
if (eq === -1) continue
|
|
23
|
+
if (trimmed.slice(0, eq).trim() !== key) continue
|
|
24
|
+
return trimmed.slice(eq + 1).trim() || undefined
|
|
25
|
+
}
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fromBuildScript(text: string, key: string): string | undefined {
|
|
30
|
+
const assignment = new RegExp(`^\\s*${key}(?:\\s*=|\\s+)\\s*['"]([^'"]+)['"]`, 'm')
|
|
31
|
+
return assignment.exec(text)?.[1]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class GradleToolchain implements ToolchainProvider {
|
|
35
|
+
readonly name = 'gradle'
|
|
36
|
+
readonly priority: number = 20
|
|
37
|
+
|
|
38
|
+
async detect(projectDir: string): Promise<DetectionResult | null> {
|
|
39
|
+
for (const file of BUILD_FILES) {
|
|
40
|
+
try {
|
|
41
|
+
await fs.access(path.join(projectDir, file))
|
|
42
|
+
return { name: 'gradle', reason: `found ${file}` }
|
|
43
|
+
} catch {
|
|
44
|
+
// continue
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async projectInfo(projectDir: string): Promise<ProjectInfo> {
|
|
51
|
+
let name = await this.staticName(projectDir)
|
|
52
|
+
let version = await this.staticVersion(projectDir)
|
|
53
|
+
|
|
54
|
+
if (!name) name = path.basename(projectDir)
|
|
55
|
+
if (!version || version === 'unspecified') {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Could not determine project version for Gradle project in '${projectDir}'. ` +
|
|
58
|
+
`Please specify 'version' in build.gradle(.kts) or gradle.properties.`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const group = await this.staticGroup(projectDir)
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
version,
|
|
66
|
+
fullName: group ? `${group}:${name}` : name,
|
|
67
|
+
namespace: group
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async install(projectDir: string, _flags: string[] = []): Promise<void> {
|
|
72
|
+
// Gradle resolves dependencies as part of tasks, no dedicated install needed
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async build(projectDir: string, flags: string[] = []): Promise<void> {
|
|
76
|
+
const cmd = await this.getCommand(projectDir)
|
|
77
|
+
await shell.run({ cwd: projectDir })`${cmd} build ${flags}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async run(projectDir: string, task: string, flags: string[] = []): Promise<void> {
|
|
81
|
+
const cmd = await this.getCommand(projectDir)
|
|
82
|
+
await shell.run({ cwd: projectDir })`${cmd} ${task} ${flags}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async getCommand(projectDir: string): Promise<string> {
|
|
86
|
+
const wrapper = path.join(projectDir, process.platform === 'win32' ? 'gradlew.bat' : 'gradlew')
|
|
87
|
+
try {
|
|
88
|
+
await fs.access(wrapper, fs.constants.X_OK)
|
|
89
|
+
return wrapper
|
|
90
|
+
} catch {
|
|
91
|
+
return 'gradle'
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async staticVersion(projectDir: string): Promise<string | undefined> {
|
|
96
|
+
const props = await readIfPresent(path.join(projectDir, 'gradle.properties'))
|
|
97
|
+
if (props) {
|
|
98
|
+
const found = fromProperties(props, 'version')
|
|
99
|
+
if (found) return found
|
|
100
|
+
}
|
|
101
|
+
for (const file of BUILD_FILES) {
|
|
102
|
+
const text = await readIfPresent(path.join(projectDir, file))
|
|
103
|
+
if (text) {
|
|
104
|
+
const found = fromBuildScript(text, 'version')
|
|
105
|
+
if (found) return found
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async staticGroup(projectDir: string): Promise<string | undefined> {
|
|
112
|
+
const props = await readIfPresent(path.join(projectDir, 'gradle.properties'))
|
|
113
|
+
if (props) {
|
|
114
|
+
const found = fromProperties(props, 'group')
|
|
115
|
+
if (found) return found
|
|
116
|
+
}
|
|
117
|
+
for (const file of BUILD_FILES) {
|
|
118
|
+
const text = await readIfPresent(path.join(projectDir, file))
|
|
119
|
+
if (text) {
|
|
120
|
+
const found = fromBuildScript(text, 'group')
|
|
121
|
+
if (found) return found
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async staticName(projectDir: string): Promise<string | undefined> {
|
|
128
|
+
for (const file of SETTINGS_FILES) {
|
|
129
|
+
const text = await readIfPresent(path.join(projectDir, file))
|
|
130
|
+
if (text) {
|
|
131
|
+
const found = /rootProject\.name\s*=\s*['"]([^'"]+)['"]/.exec(text)?.[1]
|
|
132
|
+
if (found) return found
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return undefined
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import { shell } from '../../primitives/shell.js'
|
|
4
|
+
import { readPom } from './pom.js'
|
|
5
|
+
import type { ToolchainProvider, DetectionResult, ProjectInfo } from '../types.js'
|
|
6
|
+
|
|
7
|
+
export class MavenToolchain implements ToolchainProvider {
|
|
8
|
+
readonly name = 'maven'
|
|
9
|
+
readonly priority: number = 30
|
|
10
|
+
|
|
11
|
+
async detect(projectDir: string): Promise<DetectionResult | null> {
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(path.join(projectDir, 'pom.xml'))
|
|
14
|
+
return { name: 'maven', reason: 'found pom.xml' }
|
|
15
|
+
} catch {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async projectInfo(projectDir: string): Promise<ProjectInfo> {
|
|
21
|
+
const pomPath = path.join(projectDir, 'pom.xml')
|
|
22
|
+
const content = await fs.readFile(pomPath, 'utf-8')
|
|
23
|
+
const { groupId, artifactId, version } = readPom(content)
|
|
24
|
+
|
|
25
|
+
if (!artifactId) {
|
|
26
|
+
throw new Error(`Invalid pom.xml (missing <artifactId>): ${pomPath}`)
|
|
27
|
+
}
|
|
28
|
+
if (!version || version.includes('${')) {
|
|
29
|
+
throw new Error(`Invalid pom.xml (missing or unexpanded <version> '${version ?? ''}'): ${pomPath}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
name: artifactId,
|
|
34
|
+
version,
|
|
35
|
+
fullName: groupId ? `${groupId}:${artifactId}` : artifactId,
|
|
36
|
+
namespace: groupId
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async install(projectDir: string, flags: string[] = []): Promise<void> {
|
|
41
|
+
const cmd = await this.getCommand(projectDir)
|
|
42
|
+
await shell.run({ cwd: projectDir })`${cmd} -B dependency:go-offline ${flags}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async build(projectDir: string, flags: string[] = []): Promise<void> {
|
|
46
|
+
const cmd = await this.getCommand(projectDir)
|
|
47
|
+
await shell.run({ cwd: projectDir })`${cmd} -B package ${flags}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async run(projectDir: string, task: string, flags: string[] = []): Promise<void> {
|
|
51
|
+
const cmd = await this.getCommand(projectDir)
|
|
52
|
+
await shell.run({ cwd: projectDir })`${cmd} -B ${task} ${flags}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async getCommand(projectDir: string): Promise<string> {
|
|
56
|
+
const wrapper = path.join(projectDir, process.platform === 'win32' ? 'mvnw.cmd' : 'mvnw')
|
|
57
|
+
try {
|
|
58
|
+
await fs.access(wrapper, fs.constants.X_OK)
|
|
59
|
+
return wrapper
|
|
60
|
+
} catch {
|
|
61
|
+
return 'mvn'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|