@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,172 @@
|
|
|
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
|
+
export type NodePackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun'
|
|
7
|
+
|
|
8
|
+
const LOCKFILES: ReadonlyArray<readonly [string, NodePackageManager]> = [
|
|
9
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
10
|
+
['bun.lockb', 'bun'],
|
|
11
|
+
['bun.lock', 'bun'],
|
|
12
|
+
['yarn.lock', 'yarn'],
|
|
13
|
+
['package-lock.json', 'npm']
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
async function detectPackageManager(dir: string): Promise<{ pm: NodePackageManager; reason: string }> {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(path.join(dir, 'package.json'), 'utf-8')
|
|
19
|
+
const field = JSON.parse(raw)?.packageManager
|
|
20
|
+
if (typeof field === 'string') {
|
|
21
|
+
const name = field.split('@')[0]
|
|
22
|
+
if (['npm', 'pnpm', 'yarn', 'bun'].includes(name)) {
|
|
23
|
+
return { pm: name as NodePackageManager, reason: `package.json packageManager (${field})` }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// continue
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const [lockfile, pm] of LOCKFILES) {
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(path.join(dir, lockfile))
|
|
33
|
+
return { pm, reason: lockfile }
|
|
34
|
+
} catch {
|
|
35
|
+
// continue
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { pm: 'npm', reason: 'default fallback' }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
43
|
+
try {
|
|
44
|
+
await fs.access(filePath)
|
|
45
|
+
return true
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function isYarnBerry(dir: string): Promise<boolean> {
|
|
52
|
+
if (await fileExists(path.join(dir, '.yarnrc.yml')) || await fileExists(path.join(dir, '.yarnrc.yaml'))) {
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const raw = await fs.readFile(path.join(dir, 'package.json'), 'utf-8')
|
|
57
|
+
const pm = JSON.parse(raw)?.packageManager
|
|
58
|
+
if (typeof pm === 'string' && /^yarn@([2-9]|\d{2,})/.test(pm)) {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
} catch {}
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class NodeToolchain implements ToolchainProvider {
|
|
66
|
+
readonly name: string
|
|
67
|
+
readonly priority: number = 10
|
|
68
|
+
private explicitPm?: NodePackageManager
|
|
69
|
+
|
|
70
|
+
constructor(pmName?: NodePackageManager) {
|
|
71
|
+
this.name = pmName ?? 'node'
|
|
72
|
+
this.explicitPm = pmName
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async detect(projectDir: string): Promise<DetectionResult | null> {
|
|
76
|
+
try {
|
|
77
|
+
await fs.access(path.join(projectDir, 'package.json'))
|
|
78
|
+
} catch {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { pm, reason } = await detectPackageManager(projectDir)
|
|
83
|
+
return {
|
|
84
|
+
name: this.explicitPm ?? pm,
|
|
85
|
+
reason: `found package.json and ${reason}`
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async projectInfo(projectDir: string): Promise<ProjectInfo> {
|
|
90
|
+
const raw = await fs.readFile(path.join(projectDir, 'package.json'), 'utf-8')
|
|
91
|
+
const pkg = JSON.parse(raw)
|
|
92
|
+
|
|
93
|
+
const rawName: string = pkg.name || path.basename(projectDir)
|
|
94
|
+
let name = rawName
|
|
95
|
+
let namespace: string | undefined
|
|
96
|
+
|
|
97
|
+
if (rawName.startsWith('@') && rawName.includes('/')) {
|
|
98
|
+
const [scope, bare] = rawName.split('/')
|
|
99
|
+
namespace = scope.slice(1)
|
|
100
|
+
name = bare
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
name,
|
|
105
|
+
version: pkg.version || '0.0.0',
|
|
106
|
+
fullName: rawName,
|
|
107
|
+
namespace
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async install(projectDir: string, flags: string[] = []): Promise<void> {
|
|
112
|
+
const pm = await this.resolvePm(projectDir)
|
|
113
|
+
|
|
114
|
+
if (pm === 'npm') {
|
|
115
|
+
const hasLock = await fileExists(path.join(projectDir, 'package-lock.json'))
|
|
116
|
+
if (hasLock) {
|
|
117
|
+
await shell.run({ cwd: projectDir })`npm ci ${flags}`
|
|
118
|
+
} else {
|
|
119
|
+
await shell.run({ cwd: projectDir })`npm install ${flags}`
|
|
120
|
+
}
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (pm === 'pnpm') {
|
|
125
|
+
const hasLock = await fileExists(path.join(projectDir, 'pnpm-lock.yaml'))
|
|
126
|
+
const hasFrozenFlag = flags.some(f => f.includes('frozen-lockfile'))
|
|
127
|
+
const frozenArgs = (hasLock && !hasFrozenFlag) ? ['--frozen-lockfile'] : []
|
|
128
|
+
await shell.run({ cwd: projectDir })`pnpm install ${frozenArgs} ${flags}`
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (pm === 'yarn') {
|
|
133
|
+
const hasLock = await fileExists(path.join(projectDir, 'yarn.lock'))
|
|
134
|
+
if (!hasLock) {
|
|
135
|
+
await shell.run({ cwd: projectDir })`yarn install ${flags}`
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
const isBerry = await isYarnBerry(projectDir)
|
|
139
|
+
const hasFrozenFlag = flags.some(f => f.includes('frozen-lockfile') || f.includes('immutable'))
|
|
140
|
+
const frozenArgs = !hasFrozenFlag ? [isBerry ? '--immutable' : '--frozen-lockfile'] : []
|
|
141
|
+
await shell.run({ cwd: projectDir })`yarn install ${frozenArgs} ${flags}`
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (pm === 'bun') {
|
|
146
|
+
const hasLock = (await fileExists(path.join(projectDir, 'bun.lockb'))) ||
|
|
147
|
+
(await fileExists(path.join(projectDir, 'bun.lock')))
|
|
148
|
+
const hasFrozenFlag = flags.some(f => f.includes('frozen-lockfile'))
|
|
149
|
+
const frozenArgs = (hasLock && !hasFrozenFlag) ? ['--frozen-lockfile'] : []
|
|
150
|
+
await shell.run({ cwd: projectDir })`bun install ${frozenArgs} ${flags}`
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await shell.run({ cwd: projectDir })`${pm} install ${flags}`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async build(projectDir: string, flags: string[] = []): Promise<void> {
|
|
158
|
+
const pm = await this.resolvePm(projectDir)
|
|
159
|
+
await shell.run({ cwd: projectDir })`${pm} run build ${flags}`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async run(projectDir: string, task: string, flags: string[] = []): Promise<void> {
|
|
163
|
+
const pm = await this.resolvePm(projectDir)
|
|
164
|
+
await shell.run({ cwd: projectDir })`${pm} run ${task} ${flags}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async resolvePm(projectDir: string): Promise<NodePackageManager> {
|
|
168
|
+
if (this.explicitPm) return this.explicitPm
|
|
169
|
+
const { pm } = await detectPackageManager(projectDir)
|
|
170
|
+
return pm
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML parsing helper for reading coordinates out of a pom.xml without general XML parser dependencies.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface XmlNode {
|
|
6
|
+
text: string
|
|
7
|
+
children: Record<string, XmlNode[]>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ENTITIES: Record<string, string> = {
|
|
11
|
+
'<': '<',
|
|
12
|
+
'>': '>',
|
|
13
|
+
'&': '&',
|
|
14
|
+
'"': '"',
|
|
15
|
+
''': "'"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function decodeEntities(text: string): string {
|
|
19
|
+
return text.replace(/&(?:lt|gt|amp|quot|apos|#\d+|#x[0-9a-fA-F]+);/g, match => {
|
|
20
|
+
if (ENTITIES[match]) return ENTITIES[match]
|
|
21
|
+
const numeric = match.startsWith('&#x') || match.startsWith('&#X')
|
|
22
|
+
? parseInt(match.slice(3, -1), 16)
|
|
23
|
+
: parseInt(match.slice(2, -1), 10)
|
|
24
|
+
return Number.isFinite(numeric) ? String.fromCodePoint(numeric) : match
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stripNoise(xml: string): string {
|
|
29
|
+
return xml
|
|
30
|
+
.replace(/<\?[\s\S]*?\?>/g, '')
|
|
31
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
32
|
+
.replace(/<!DOCTYPE[^>[]*(\[[\s\S]*?\])?[^>]*>/gi, '')
|
|
33
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_, body: string) =>
|
|
34
|
+
body.replace(/&/g, '&').replace(/</g, '<'))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseXml(xml: string): { name: string; node: XmlNode } {
|
|
38
|
+
const source = stripNoise(xml)
|
|
39
|
+
const tag = /<\s*(\/?)\s*([A-Za-z_][\w.:-]*)((?:"[^"]*"|'[^']*'|[^>"'])*?)(\/?)\s*>/g
|
|
40
|
+
|
|
41
|
+
let root: { name: string; node: XmlNode } | undefined
|
|
42
|
+
const stack: { name: string; node: XmlNode }[] = []
|
|
43
|
+
let textFrom = 0
|
|
44
|
+
let match: RegExpExecArray | null
|
|
45
|
+
|
|
46
|
+
const addText = (upTo: number) => {
|
|
47
|
+
const top = stack[stack.length - 1]
|
|
48
|
+
if (!top) return
|
|
49
|
+
top.node.text += source.slice(textFrom, upTo)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
while ((match = tag.exec(source)) !== null) {
|
|
53
|
+
const [whole, closing, name, , selfClosing] = match
|
|
54
|
+
|
|
55
|
+
addText(match.index)
|
|
56
|
+
textFrom = match.index + whole.length
|
|
57
|
+
|
|
58
|
+
if (closing) {
|
|
59
|
+
const open = stack.pop()
|
|
60
|
+
if (!open || open.name !== name) {
|
|
61
|
+
throw new Error(`Malformed XML: </${name}> does not close <${open?.name ?? 'nothing'}>`)
|
|
62
|
+
}
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const node: XmlNode = { text: '', children: {} }
|
|
67
|
+
const entry = { name, node }
|
|
68
|
+
|
|
69
|
+
const parent = stack[stack.length - 1]
|
|
70
|
+
if (parent) {
|
|
71
|
+
;(parent.node.children[name] ??= []).push(node)
|
|
72
|
+
} else if (root) {
|
|
73
|
+
break
|
|
74
|
+
} else {
|
|
75
|
+
root = entry
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!selfClosing) stack.push(entry)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!root) throw new Error('Malformed XML: no root element')
|
|
82
|
+
if (stack.length > 0) {
|
|
83
|
+
throw new Error(`Malformed XML: unclosed tag <${stack[stack.length - 1].name}>`)
|
|
84
|
+
}
|
|
85
|
+
return root
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function childText(node: XmlNode, name: string): string | undefined {
|
|
89
|
+
const child = node.children[name]?.[0]
|
|
90
|
+
if (!child) return undefined
|
|
91
|
+
const text = decodeEntities(child.text).trim()
|
|
92
|
+
return text || undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function properties(project: XmlNode): Record<string, string> {
|
|
96
|
+
const out: Record<string, string> = {}
|
|
97
|
+
const block = project.children['properties']?.[0]
|
|
98
|
+
if (!block) return out
|
|
99
|
+
for (const [name, nodes] of Object.entries(block.children)) {
|
|
100
|
+
const text = decodeEntities(nodes[0].text).trim()
|
|
101
|
+
if (text) out[name] = text
|
|
102
|
+
}
|
|
103
|
+
return out
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function expand(value: string, vars: Record<string, string>): string {
|
|
107
|
+
let out = value
|
|
108
|
+
for (let depth = 0; depth < 5 && out.includes('${'); depth++) {
|
|
109
|
+
const next = out.replace(/\$\{([^}]+)\}/g, (whole, name: string) => vars[name] ?? whole)
|
|
110
|
+
if (next === out) break
|
|
111
|
+
out = next
|
|
112
|
+
}
|
|
113
|
+
return out
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface PomCoordinates {
|
|
117
|
+
groupId?: string
|
|
118
|
+
artifactId?: string
|
|
119
|
+
version?: string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function readPom(xml: string): PomCoordinates {
|
|
123
|
+
const { node: project } = parseXml(xml)
|
|
124
|
+
const parent = project.children['parent']?.[0]
|
|
125
|
+
|
|
126
|
+
const vars = properties(project)
|
|
127
|
+
if (parent) {
|
|
128
|
+
const parentVersion = childText(parent, 'version')
|
|
129
|
+
if (parentVersion) {
|
|
130
|
+
vars['project.parent.version'] = parentVersion
|
|
131
|
+
}
|
|
132
|
+
const parentGroupId = childText(parent, 'groupId')
|
|
133
|
+
if (parentGroupId) {
|
|
134
|
+
vars['project.parent.groupId'] = parentGroupId
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const resolve = (value: string | undefined) => value === undefined ? undefined : expand(value, vars)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
groupId: resolve(childText(project, 'groupId') ?? (parent && childText(parent, 'groupId'))),
|
|
142
|
+
artifactId: resolve(childText(project, 'artifactId')),
|
|
143
|
+
version: resolve(childText(project, 'version') ?? (parent && childText(parent, 'version')))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { MavenToolchain } from './providers/MavenToolchain.js'
|
|
2
|
+
import { GradleToolchain } from './providers/GradleToolchain.js'
|
|
3
|
+
import { NodeToolchain } from './providers/NodeToolchain.js'
|
|
4
|
+
import type { ToolchainProvider, DetectionResult } from './types.js'
|
|
5
|
+
|
|
6
|
+
export interface ResolveToolchainOptions {
|
|
7
|
+
/** Explicitly preferred toolchain (takes precedence over automatic detection) */
|
|
8
|
+
preferred?: string
|
|
9
|
+
/** Fallback toolchain used only when automatic detection yields no match */
|
|
10
|
+
fallback?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ToolchainRegistry {
|
|
14
|
+
private providers: Map<string, ToolchainProvider> = new Map()
|
|
15
|
+
|
|
16
|
+
constructor(registerDefaults = true) {
|
|
17
|
+
if (registerDefaults) {
|
|
18
|
+
this.registerDefaultProviders()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private registerDefaultProviders(): void {
|
|
23
|
+
// Built-in order: Maven, Gradle, Node
|
|
24
|
+
this.register(new MavenToolchain())
|
|
25
|
+
this.register(new GradleToolchain())
|
|
26
|
+
this.register(new NodeToolchain())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register a new or overriding ToolchainProvider.
|
|
31
|
+
*/
|
|
32
|
+
register(provider: ToolchainProvider): void {
|
|
33
|
+
this.providers.set(provider.name, provider)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Remove a registered provider by name.
|
|
38
|
+
*/
|
|
39
|
+
unregister(name: string): boolean {
|
|
40
|
+
return this.providers.delete(name)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Look up a registered provider by name.
|
|
45
|
+
*/
|
|
46
|
+
get(name: string): ToolchainProvider | undefined {
|
|
47
|
+
const direct = this.providers.get(name)
|
|
48
|
+
if (direct) return direct
|
|
49
|
+
if (['pnpm', 'npm', 'yarn', 'bun'].includes(name)) {
|
|
50
|
+
return new NodeToolchain(name as import('./providers/NodeToolchain.js').NodePackageManager)
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* List all registered providers sorted by priority (highest first).
|
|
57
|
+
*/
|
|
58
|
+
list(): ToolchainProvider[] {
|
|
59
|
+
return Array.from(this.providers.values()).sort(
|
|
60
|
+
(a, b) => (b.priority ?? 0) - (a.priority ?? 0)
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detect which toolchain matches the directory.
|
|
66
|
+
*/
|
|
67
|
+
async detect(projectDir: string): Promise<{ provider: ToolchainProvider; detection: DetectionResult } | null> {
|
|
68
|
+
const sorted = this.list()
|
|
69
|
+
for (const provider of sorted) {
|
|
70
|
+
const detection = await provider.detect(projectDir)
|
|
71
|
+
if (detection) {
|
|
72
|
+
return { provider, detection }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve a toolchain for a directory, respecting an optional preferred toolchain name or options.
|
|
80
|
+
* If preferredName is provided, it takes precedence (explicit beats implicit detection).
|
|
81
|
+
* If fallback is provided, it is used only when automatic detection finds no matching manifest.
|
|
82
|
+
*/
|
|
83
|
+
async resolve(
|
|
84
|
+
projectDir: string,
|
|
85
|
+
preferredOrOptions?: string | ResolveToolchainOptions
|
|
86
|
+
): Promise<{ provider: ToolchainProvider; detection: DetectionResult }> {
|
|
87
|
+
let preferredName: string | undefined
|
|
88
|
+
let fallbackName: string | undefined
|
|
89
|
+
|
|
90
|
+
if (typeof preferredOrOptions === 'string') {
|
|
91
|
+
preferredName = preferredOrOptions
|
|
92
|
+
} else if (typeof preferredOrOptions === 'object' && preferredOrOptions !== null) {
|
|
93
|
+
preferredName = preferredOrOptions.preferred
|
|
94
|
+
fallbackName = preferredOrOptions.fallback
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (preferredName) {
|
|
98
|
+
const provider = this.get(preferredName)
|
|
99
|
+
if (!provider) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Preferred toolchain '${preferredName}' is not registered. Available: ${Array.from(this.providers.keys()).join(', ')}`
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
const detection = await provider.detect(projectDir)
|
|
105
|
+
if (!detection) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Preferred toolchain '${preferredName}' does not match project in '${projectDir}'.`
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
provider,
|
|
112
|
+
detection
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const detected = await this.detect(projectDir)
|
|
117
|
+
if (detected) {
|
|
118
|
+
return detected
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (fallbackName) {
|
|
122
|
+
const provider = this.get(fallbackName)
|
|
123
|
+
if (!provider) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Fallback toolchain '${fallbackName}' is not registered. Available: ${Array.from(this.providers.keys()).join(', ')}`
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
provider,
|
|
130
|
+
detection: { name: fallbackName, reason: `configured default (${fallbackName})` }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Could not determine toolchain for directory '${projectDir}'. ` +
|
|
136
|
+
`No pom.xml, build.gradle, or package.json found, and no matching registered toolchain detected.`
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reset registry to defaults.
|
|
142
|
+
*/
|
|
143
|
+
reset(): void {
|
|
144
|
+
this.providers.clear()
|
|
145
|
+
this.registerDefaultProviders()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Global default ToolchainRegistry singleton */
|
|
150
|
+
export const defaultToolchainRegistry = new ToolchainRegistry()
|
|
151
|
+
|
|
152
|
+
export function registerToolchain(provider: ToolchainProvider): void {
|
|
153
|
+
defaultToolchainRegistry.register(provider)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function resolveToolchain(
|
|
157
|
+
projectDir: string,
|
|
158
|
+
preferredOrOptions?: string | ResolveToolchainOptions
|
|
159
|
+
): Promise<{ provider: ToolchainProvider; detection: DetectionResult }> {
|
|
160
|
+
return defaultToolchainRegistry.resolve(projectDir, preferredOrOptions)
|
|
161
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toolchain SPI definitions for project type detection and build execution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ProjectInfo {
|
|
6
|
+
name: string
|
|
7
|
+
version: string
|
|
8
|
+
fullName: string
|
|
9
|
+
namespace?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DetectionResult {
|
|
13
|
+
name: string
|
|
14
|
+
reason: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ToolchainProvider {
|
|
18
|
+
/** Identifier of this toolchain, e.g. 'maven', 'gradle', 'node', 'go' */
|
|
19
|
+
readonly name: string
|
|
20
|
+
/**
|
|
21
|
+
* Detection and resolution priority. Higher priority is checked earlier.
|
|
22
|
+
* Enables custom plugins to override built-in toolchains.
|
|
23
|
+
*/
|
|
24
|
+
readonly priority?: number
|
|
25
|
+
|
|
26
|
+
/** Check if a project directory matches this toolchain */
|
|
27
|
+
detect(projectDir: string): Promise<DetectionResult | null>
|
|
28
|
+
|
|
29
|
+
/** Extract project identity (name, version) from project manifests */
|
|
30
|
+
projectInfo(projectDir: string): Promise<ProjectInfo>
|
|
31
|
+
|
|
32
|
+
/** Install or fetch dependencies */
|
|
33
|
+
install(projectDir: string, flags?: string[]): Promise<void>
|
|
34
|
+
|
|
35
|
+
/** Run the primary build/package task */
|
|
36
|
+
build(projectDir: string, flags?: string[]): Promise<void>
|
|
37
|
+
|
|
38
|
+
/** Run an arbitrary task or script */
|
|
39
|
+
run?(projectDir: string, task: string, flags?: string[]): Promise<void>
|
|
40
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret masking heuristics and masking stream utilities.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const MASK = '********'
|
|
6
|
+
|
|
7
|
+
const SECRET_WORDS = /(PASSWORD|PASSWD|PASSPHRASE|SECRET|TOKEN|CREDENTIAL|PRIVATE_KEY|PRIVATEKEY|APIKEY|KEYSTORE|SIGNATURE)/i
|
|
8
|
+
const SECRET_TOKEN_BOUNDED = /(?:^|[_\-])(PASS|KEY|AUTH)(?:$|[_\-])/i
|
|
9
|
+
const CAMEL_SECRET = /(?:Password|Passwd|Passphrase|Secret|Token|Credential|PrivateKey|ApiKey|Keystore|Signature)/
|
|
10
|
+
const CAMEL_BOUNDED = /(?:^|[a-z0-9])(?:Pass|Key|Auth)(?:$|[A-Z0-9])/
|
|
11
|
+
const LOCATION_NAME = /(_FILE|_PATH|_DIR|_DIRECTORY|_URL|_URI|_LOCATION|_NAME|_ID|_SOCK)$/i
|
|
12
|
+
|
|
13
|
+
/** Does this variable or key name suggest its value is a secret? */
|
|
14
|
+
export function isSecretKey(key: string): boolean {
|
|
15
|
+
if (!key) return false
|
|
16
|
+
if (LOCATION_NAME.test(key)) return false
|
|
17
|
+
if (SECRET_WORDS.test(key)) return true
|
|
18
|
+
if (SECRET_TOKEN_BOUNDED.test(key)) return true
|
|
19
|
+
if (CAMEL_SECRET.test(key)) return true
|
|
20
|
+
if (CAMEL_BOUNDED.test(key)) return true
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mask secret substrings within a string log line.
|
|
26
|
+
*/
|
|
27
|
+
export function maskString(text: string, secretValues: string[]): string {
|
|
28
|
+
if (!text || secretValues.length === 0) return text
|
|
29
|
+
let result = text
|
|
30
|
+
for (const secret of secretValues) {
|
|
31
|
+
if (secret && secret.length >= 3) {
|
|
32
|
+
result = result.replaceAll(secret, MASK)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Recursively mask values of secret-looking keys in an object.
|
|
40
|
+
*/
|
|
41
|
+
export function maskObject<T>(obj: T): T {
|
|
42
|
+
if (obj === null || typeof obj !== 'object') return obj
|
|
43
|
+
|
|
44
|
+
// Preserve Date, Buffer, RegExp, Error
|
|
45
|
+
if (
|
|
46
|
+
obj instanceof Date ||
|
|
47
|
+
(typeof Buffer !== 'undefined' && Buffer.isBuffer(obj)) ||
|
|
48
|
+
obj instanceof RegExp ||
|
|
49
|
+
obj instanceof Error
|
|
50
|
+
) {
|
|
51
|
+
return obj
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (Array.isArray(obj)) {
|
|
55
|
+
return obj.map(item => maskObject(item)) as unknown as T
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const output: Record<string, unknown> = {}
|
|
59
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
60
|
+
if (isSecretKey(key)) {
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
output[key] = value.map(() => MASK)
|
|
63
|
+
} else {
|
|
64
|
+
output[key] = MASK
|
|
65
|
+
}
|
|
66
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
67
|
+
output[key] = maskObject(value)
|
|
68
|
+
} else {
|
|
69
|
+
output[key] = value
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return output as T
|
|
73
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for template string interpolation into command arguments.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Turn a tagged template into an argument list (argv).
|
|
7
|
+
*
|
|
8
|
+
* Whitespace in literal parts separates arguments, whereas an interpolated value
|
|
9
|
+
* is preserved as a single argument regardless of whitespace or special characters.
|
|
10
|
+
* Array values expand into one argument per element.
|
|
11
|
+
*/
|
|
12
|
+
export function templateToArgv(strings: readonly string[], values: readonly unknown[]): string[] {
|
|
13
|
+
const argv: string[] = []
|
|
14
|
+
let token = ''
|
|
15
|
+
let started = false
|
|
16
|
+
|
|
17
|
+
const flush = () => {
|
|
18
|
+
if (started) {
|
|
19
|
+
argv.push(token)
|
|
20
|
+
token = ''
|
|
21
|
+
started = false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const append = (value: string) => {
|
|
26
|
+
token += value
|
|
27
|
+
started = true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < strings.length; i++) {
|
|
31
|
+
for (const char of strings[i]) {
|
|
32
|
+
if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
|
|
33
|
+
flush()
|
|
34
|
+
} else {
|
|
35
|
+
append(char)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (i < values.length) {
|
|
40
|
+
const value = values[i]
|
|
41
|
+
|
|
42
|
+
if (value === undefined || value === null) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Command interpolation contains ${value === null ? 'null' : 'undefined'} ` +
|
|
45
|
+
`(value at index ${i}). Verify your variables are configured.`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
flush()
|
|
51
|
+
for (const item of value) {
|
|
52
|
+
argv.push(String(item))
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
append(String(value))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
flush()
|
|
61
|
+
return argv
|
|
62
|
+
}
|