@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
@@ -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
+ '&lt;': '<',
12
+ '&gt;': '>',
13
+ '&amp;': '&',
14
+ '&quot;': '"',
15
+ '&apos;': "'"
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, '&amp;').replace(/</g, '&lt;'))
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
+ }