@wrongstack/tools 0.3.3 → 0.3.7
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/dist/bash.js +33 -3
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +151 -46
- package/dist/builtin.js.map +1 -1
- package/dist/exec.js +37 -1
- package/dist/exec.js.map +1 -1
- package/dist/index.js +151 -46
- package/dist/index.js.map +1 -1
- package/dist/install.js +16 -0
- package/dist/install.js.map +1 -1
- package/dist/logs.js +9 -0
- package/dist/logs.js.map +1 -1
- package/dist/pack.js +151 -46
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +2 -1
- package/dist/patch.js.map +1 -1
- package/dist/read.js +8 -1
- package/dist/read.js.map +1 -1
- package/dist/scaffold.js +10 -3
- package/dist/scaffold.js.map +1 -1
- package/dist/write.js +4 -4
- package/dist/write.js.map +1 -1
- package/package.json +2 -2
package/dist/bash.js
CHANGED
|
@@ -64,12 +64,41 @@ var bashTool = {
|
|
|
64
64
|
signal: opts.signal
|
|
65
65
|
});
|
|
66
66
|
if (input.background) {
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
let buf2 = "";
|
|
68
|
+
let truncated = false;
|
|
69
|
+
const child2 = spawn(shell, args, {
|
|
70
|
+
cwd: ctx.projectRoot,
|
|
71
|
+
env,
|
|
72
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
73
|
+
detached: true,
|
|
74
|
+
signal: opts.signal
|
|
75
|
+
});
|
|
76
|
+
const pid = child2.pid;
|
|
77
|
+
child2.stdout?.on("data", (chunk) => {
|
|
78
|
+
if (!truncated) {
|
|
79
|
+
const remain = MAX_OUTPUT - buf2.length;
|
|
80
|
+
if (remain > 0) {
|
|
81
|
+
buf2 += chunk.toString().slice(0, remain);
|
|
82
|
+
}
|
|
83
|
+
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
child2.stderr?.on("data", (chunk) => {
|
|
87
|
+
if (!truncated) {
|
|
88
|
+
const remain = MAX_OUTPUT - buf2.length;
|
|
89
|
+
if (remain > 0) {
|
|
90
|
+
buf2 += chunk.toString().slice(0, remain);
|
|
91
|
+
}
|
|
92
|
+
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
child2.on("close", () => {
|
|
96
|
+
});
|
|
97
|
+
if (typeof pid === "number") child2.unref();
|
|
69
98
|
yield {
|
|
70
99
|
type: "final",
|
|
71
100
|
output: {
|
|
72
|
-
output:
|
|
101
|
+
output: truncated ? buf2.slice(0, MAX_OUTPUT) + "\u2026[truncated]" : buf2,
|
|
73
102
|
exit_code: null,
|
|
74
103
|
timed_out: false,
|
|
75
104
|
pid
|
|
@@ -114,6 +143,7 @@ var bashTool = {
|
|
|
114
143
|
}
|
|
115
144
|
}, 2e3);
|
|
116
145
|
timers.push(killTimer);
|
|
146
|
+
killTimer.unref?.();
|
|
117
147
|
} catch {
|
|
118
148
|
}
|
|
119
149
|
}
|
package/dist/bash.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/_util.ts","../src/bash.ts"],"names":["resolve"],"mappings":"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","file":"bash.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import { spawn } from 'node:child_process';\nimport * as os from 'node:os';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { stripAnsi } from '@wrongstack/core';\nimport { buildChildEnv } from './_env.js';\nimport { truncateMiddle } from './_util.js';\n\ninterface BashInput {\n command: string;\n timeout_ms?: number;\n background?: boolean;\n}\n\ninterface BashOutput {\n output: string;\n exit_code: number | null;\n timed_out: boolean;\n pid?: number;\n}\n\nconst MAX_OUTPUT = 32_768;\nconst DEFAULT_TIMEOUT = 30_000;\n// Flush partial_output every 200ms or when 4 KiB accumulates — whichever\n// comes first. Smaller batches make the TUI feel responsive; larger ones\n// keep EventBus traffic reasonable on chatty processes.\nconst STREAM_FLUSH_INTERVAL_MS = 200;\nconst STREAM_FLUSH_BYTES = 4 * 1024;\n\nexport const bashTool: Tool<BashInput, BashOutput> = {\n name: 'bash',\n category: 'Shell',\n description: 'Run a shell command. stdout and stderr are merged.',\n usageHint:\n 'Runs via `bash -c` (or `cmd /c` on Windows). Cwd is the project root. Default timeout 30s. Output truncated from the middle if oversized. Use for git, npm, builds, tests.',\n permission: 'confirm',\n mutating: true,\n // Trust rules match on the literal `command` string. Without subjectKey\n // the policy heuristic would have done the same here, but declaring it\n // explicitly removes the implicit cross-tool aliasing.\n subjectKey: 'command',\n timeoutMs: 30_000,\n maxOutputBytes: MAX_OUTPUT,\n estimatedDurationMs: 3_000,\n inputSchema: {\n type: 'object',\n properties: {\n command: { type: 'string' },\n timeout_ms: { type: 'integer' },\n background: { type: 'boolean' },\n },\n required: ['command'],\n },\n async execute(input, ctx, opts) {\n let final: BashOutput | undefined;\n for await (const ev of bashTool.executeStream!(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('bash: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<BashOutput>> {\n if (!input?.command) throw new Error('bash: command is required');\n const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT, 600_000));\n\n const isWin = os.platform() === 'win32';\n const shell = isWin\n ? (process.env['COMSPEC'] ?? 'cmd.exe')\n : (process.env['SHELL'] ?? '/bin/bash');\n const args = isWin ? ['/c', input.command] : ['-c', input.command];\n\n const env = buildChildEnv(ctx.session?.id);\n\n // On POSIX we put the shell in its own process group so that timeout /\n // abort can kill the entire group with `process.kill(-pid)`. Otherwise\n // `bash -c \"sleep 9999 & disown\"` would leave the grandchild running.\n // `detached: true` is also reused for the user-facing background mode;\n // we always want detached on POSIX, only on Windows is it tied to the\n // explicit background flag.\n const detached = isWin ? !!input.background : true;\n const child = spawn(shell, args, {\n cwd: ctx.projectRoot,\n env,\n stdio: input.background ? 'ignore' : ['ignore', 'pipe', 'pipe'],\n detached,\n signal: opts.signal,\n });\n\n if (input.background) {\n const pid = child.pid;\n if (typeof pid === 'number') child.unref();\n yield {\n type: 'final',\n output: {\n output: `[background] pid=${pid ?? 'unknown'}`,\n exit_code: null,\n timed_out: false,\n pid,\n },\n };\n return;\n }\n\n let buf = '';\n let pending = '';\n let timedOut = false;\n const timers: NodeJS.Timeout[] = [];\n const timer = setTimeout(() => {\n timedOut = true;\n if (isWin) {\n try {\n child.kill();\n } catch {\n /* ignore */\n }\n } else {\n try {\n // Kill the process group, not just the shell — pid is positive,\n // group id is the negated pid. Without this a runaway grandchild\n // ('sleep 9999 & disown') survives bash termination.\n if (typeof child.pid === 'number') {\n try {\n process.kill(-child.pid, 'SIGTERM');\n } catch {\n child.kill('SIGTERM');\n }\n } else {\n child.kill('SIGTERM');\n }\n const killTimer = setTimeout(() => {\n try {\n if (typeof child.pid === 'number') {\n try {\n process.kill(-child.pid, 'SIGKILL');\n } catch {\n child.kill('SIGKILL');\n }\n } else {\n child.kill('SIGKILL');\n }\n } catch {\n /* ignore */\n }\n }, 2000);\n timers.push(killTimer);\n } catch {\n /* ignore */\n }\n }\n }, timeoutMs);\n timers.push(timer);\n timer.unref?.();\n\n // Bridge the EventEmitter-style child to an async iterator. We push\n // chunks into a queue and let the generator pull them; this lets us\n // yield 'partial_output' events to the executor at flush boundaries.\n type Chunk =\n | { kind: 'data'; text: string }\n | { kind: 'end'; code: number | null }\n | { kind: 'error'; err: Error };\n const queue: Chunk[] = [];\n let resolveNext: ((c: Chunk) => void) | null = null;\n const push = (c: Chunk) => {\n if (resolveNext) {\n const r = resolveNext;\n resolveNext = null;\n r(c);\n } else {\n queue.push(c);\n }\n };\n const next = (): Promise<Chunk> =>\n new Promise((resolve) => {\n const c = queue.shift();\n if (c) resolve(c);\n else resolveNext = resolve;\n });\n\n let lastFlush = Date.now();\n const flush = () => {\n if (pending.length === 0) return null;\n const text = pending;\n pending = '';\n lastFlush = Date.now();\n return text;\n };\n\n child.stdout?.on('data', (chunk) => {\n const text = chunk.toString();\n buf += text;\n pending += text;\n push({ kind: 'data', text });\n });\n child.stderr?.on('data', (chunk) => {\n const text = chunk.toString();\n buf += text;\n pending += text;\n push({ kind: 'data', text });\n });\n child.on('error', (err) => {\n for (const t of timers) clearTimeout(t);\n push({ kind: 'error', err });\n });\n child.on('close', (code) => {\n for (const t of timers) clearTimeout(t);\n push({ kind: 'end', code });\n });\n\n try {\n while (true) {\n const c = await next();\n if (c.kind === 'error') throw c.err;\n if (c.kind === 'end') {\n const remainder = flush();\n if (remainder !== null) {\n yield { type: 'partial_output', text: remainder };\n }\n const cleaned = stripAnsi(buf).replace(/\\r\\n?/g, '\\n');\n yield {\n type: 'final',\n output: {\n output: truncateMiddle(cleaned, MAX_OUTPUT),\n exit_code: c.code,\n timed_out: timedOut,\n },\n };\n return;\n }\n // Decide whether to flush. Time-based OR size-based to keep latency\n // low for slow-emitting commands without overwhelming the TUI for\n // chatty ones.\n const now = Date.now();\n if (pending.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {\n const text = flush();\n if (text) yield { type: 'partial_output', text };\n }\n }\n } finally {\n for (const t of timers) clearTimeout(t);\n }\n },\n};\n\n// Re-export types so consumers can narrow on stream events.\nexport type { BashInput, BashOutput };\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/_util.ts","../src/bash.ts"],"names":["buf","child","resolve"],"mappings":";;;;;;AAqBO,SAAS,cAAA,CAAe,GAAW,GAAA,EAAqB;AAC7D,EAAA,IAAI,OAAO,UAAA,CAAW,CAAA,EAAG,MAAM,CAAA,IAAK,KAAK,OAAO,CAAA;AAChD,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,CAAC,CAAA;AAC/B,EAAA,OACE,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA,GACf;AAAA,iBAAA,EAAiB,MAAA,CAAO,UAAA,CAAW,CAAA,EAAG,MAAM,IAAI,GAAG,CAAA;AAAA,CAAA,GACnD,CAAA,CAAE,KAAA,CAAM,CAAC,IAAI,CAAA;AAEjB;;;ACTA,IAAM,UAAA,GAAa,KAAA;AACnB,IAAM,eAAA,GAAkB,GAAA;AAIxB,IAAM,wBAAA,GAA2B,GAAA;AACjC,IAAM,qBAAqB,CAAA,GAAI,IAAA;AAExB,IAAM,QAAA,GAAwC;AAAA,EACnD,IAAA,EAAM,MAAA;AAAA,EACN,QAAA,EAAU,OAAA;AAAA,EACV,WAAA,EAAa,oDAAA;AAAA,EACb,SAAA,EACE,4KAAA;AAAA,EACF,UAAA,EAAY,SAAA;AAAA,EACZ,QAAA,EAAU,IAAA;AAAA;AAAA;AAAA;AAAA,EAIV,UAAA,EAAY,SAAA;AAAA,EACZ,SAAA,EAAW,GAAA;AAAA,EACX,cAAA,EAAgB,UAAA;AAAA,EAChB,mBAAA,EAAqB,GAAA;AAAA,EACrB,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY;AAAA,MACV,OAAA,EAAS,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MAC1B,UAAA,EAAY,EAAE,IAAA,EAAM,SAAA,EAAU;AAAA,MAC9B,UAAA,EAAY,EAAE,IAAA,EAAM,SAAA;AAAU,KAChC;AAAA,IACA,QAAA,EAAU,CAAC,SAAS;AAAA,GACtB;AAAA,EACA,MAAM,OAAA,CAAQ,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM;AAC9B,IAAA,IAAI,KAAA;AACJ,IAAA,WAAA,MAAiB,MAAM,QAAA,CAAS,aAAA,CAAe,KAAA,EAAO,GAAA,EAAK,IAAI,CAAA,EAAG;AAChE,MAAA,IAAI,EAAA,CAAG,IAAA,KAAS,OAAA,EAAS,KAAA,GAAQ,EAAA,CAAG,MAAA;AAAA,IACtC;AACA,IAAA,IAAI,CAAC,KAAA,EAAO,MAAM,IAAI,MAAM,wCAAwC,CAAA;AACpE,IAAA,OAAO,KAAA;AAAA,EACT,CAAA;AAAA,EACA,OAAO,aAAA,CAAc,KAAA,EAAO,GAAA,EAAK,IAAA,EAAmD;AAClF,IAAA,IAAI,CAAC,KAAA,EAAO,OAAA,EAAS,MAAM,IAAI,MAAM,2BAA2B,CAAA;AAChE,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,IAAI,KAAA,CAAM,UAAA,IAAc,eAAA,EAAiB,GAAO,CAAC,CAAA;AAEpF,IAAA,MAAM,KAAA,GAAW,aAAS,KAAM,OAAA;AAChC,IAAA,MAAM,KAAA,GAAQ,KAAA,GACT,OAAA,CAAQ,GAAA,CAAI,SAAS,KAAK,SAAA,GAC1B,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA,IAAK,WAAA;AAC7B,IAAA,MAAM,IAAA,GAAO,KAAA,GAAQ,CAAC,IAAA,EAAM,KAAA,CAAM,OAAO,CAAA,GAAI,CAAC,IAAA,EAAM,KAAA,CAAM,OAAO,CAAA;AAEjE,IAAA,MAAM,GAAA,GAAM,aAAA,CAAc,GAAA,CAAI,OAAA,EAAS,EAAE,CAAA;AAQzC,IAAA,MAAM,QAAA,GAAW,KAAA,GAAQ,CAAC,CAAC,MAAM,UAAA,GAAa,IAAA;AAC9C,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,EAAO,IAAA,EAAM;AAAA,MAC/B,KAAK,GAAA,CAAI,WAAA;AAAA,MACT,GAAA;AAAA,MACA,OAAO,KAAA,CAAM,UAAA,GAAa,WAAW,CAAC,QAAA,EAAU,QAAQ,MAAM,CAAA;AAAA,MAC9D,QAAA;AAAA,MACA,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AAED,IAAA,IAAI,MAAM,UAAA,EAAY;AAGpB,MAAA,IAAIA,IAAAA,GAAM,EAAA;AACV,MAAA,IAAI,SAAA,GAAY,KAAA;AAChB,MAAA,MAAMC,MAAAA,GAAQ,KAAA,CAAM,KAAA,EAAO,IAAA,EAAM;AAAA,QAC/B,KAAK,GAAA,CAAI,WAAA;AAAA,QACT,GAAA;AAAA,QACA,KAAA,EAAO,CAAC,QAAA,EAAU,MAAA,EAAQ,MAAM,CAAA;AAAA,QAChC,QAAA,EAAU,IAAA;AAAA,QACV,QAAQ,IAAA,CAAK;AAAA,OACd,CAAA;AACD,MAAA,MAAM,MAAMA,MAAAA,CAAM,GAAA;AAClB,MAAAA,MAAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAkB;AAC1C,QAAA,IAAI,CAAC,SAAA,EAAW;AACd,UAAA,MAAM,MAAA,GAAS,aAAaD,IAAAA,CAAI,MAAA;AAChC,UAAA,IAAI,SAAS,CAAA,EAAG;AACd,YAAAA,QAAO,KAAA,CAAM,QAAA,EAAS,CAAE,KAAA,CAAM,GAAG,MAAM,CAAA;AAAA,UACzC;AACA,UAAA,IAAIA,IAAAA,CAAI,MAAA,IAAU,UAAA,EAAY,SAAA,GAAY,IAAA;AAAA,QAC5C;AAAA,MACF,CAAC,CAAA;AACD,MAAAC,MAAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAkB;AAC1C,QAAA,IAAI,CAAC,SAAA,EAAW;AACd,UAAA,MAAM,MAAA,GAAS,aAAaD,IAAAA,CAAI,MAAA;AAChC,UAAA,IAAI,SAAS,CAAA,EAAG;AACd,YAAAA,QAAO,KAAA,CAAM,QAAA,EAAS,CAAE,KAAA,CAAM,GAAG,MAAM,CAAA;AAAA,UACzC;AACA,UAAA,IAAIA,IAAAA,CAAI,MAAA,IAAU,UAAA,EAAY,SAAA,GAAY,IAAA;AAAA,QAC5C;AAAA,MACF,CAAC,CAAA;AACD,MAAAC,MAAAA,CAAM,EAAA,CAAG,OAAA,EAAS,MAAM;AAAA,MAExB,CAAC,CAAA;AACD,MAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,EAAUA,OAAM,KAAA,EAAM;AACzC,MAAA,MAAM;AAAA,QACJ,IAAA,EAAM,OAAA;AAAA,QACN,MAAA,EAAQ;AAAA,UACN,QAAQ,SAAA,GAAYD,IAAAA,CAAI,MAAM,CAAA,EAAG,UAAU,IAAI,mBAAA,GAAiBA,IAAAA;AAAA,UAChE,SAAA,EAAW,IAAA;AAAA,UACX,SAAA,EAAW,KAAA;AAAA,UACX;AAAA;AACF,OACF;AACA,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,GAAA,GAAM,EAAA;AACV,IAAA,IAAI,OAAA,GAAU,EAAA;AACd,IAAA,IAAI,QAAA,GAAW,KAAA;AACf,IAAA,MAAM,SAA2B,EAAC;AAClC,IAAA,MAAM,KAAA,GAAQ,WAAW,MAAM;AAC7B,MAAA,QAAA,GAAW,IAAA;AACX,MAAA,IAAI,KAAA,EAAO;AACT,QAAA,IAAI;AACF,UAAA,KAAA,CAAM,IAAA,EAAK;AAAA,QACb,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF,CAAA,MAAO;AACL,QAAA,IAAI;AAIF,UAAA,IAAI,OAAO,KAAA,CAAM,GAAA,KAAQ,QAAA,EAAU;AACjC,YAAA,IAAI;AACF,cAAA,OAAA,CAAQ,IAAA,CAAK,CAAC,KAAA,CAAM,GAAA,EAAK,SAAS,CAAA;AAAA,YACpC,CAAA,CAAA,MAAQ;AACN,cAAA,KAAA,CAAM,KAAK,SAAS,CAAA;AAAA,YACtB;AAAA,UACF,CAAA,MAAO;AACL,YAAA,KAAA,CAAM,KAAK,SAAS,CAAA;AAAA,UACtB;AACA,UAAA,MAAM,SAAA,GAAY,WAAW,MAAM;AACjC,YAAA,IAAI;AACF,cAAA,IAAI,OAAO,KAAA,CAAM,GAAA,KAAQ,QAAA,EAAU;AACjC,gBAAA,IAAI;AACF,kBAAA,OAAA,CAAQ,IAAA,CAAK,CAAC,KAAA,CAAM,GAAA,EAAK,SAAS,CAAA;AAAA,gBACpC,CAAA,CAAA,MAAQ;AACN,kBAAA,KAAA,CAAM,KAAK,SAAS,CAAA;AAAA,gBACtB;AAAA,cACF,CAAA,MAAO;AACL,gBAAA,KAAA,CAAM,KAAK,SAAS,CAAA;AAAA,cACtB;AAAA,YACF,CAAA,CAAA,MAAQ;AAAA,YAER;AAAA,UACF,GAAG,GAAI,CAAA;AACP,UAAA,MAAA,CAAO,KAAK,SAAS,CAAA;AACrB,UAAA,SAAA,CAAU,KAAA,IAAQ;AAAA,QACpB,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,GAAG,SAAS,CAAA;AACZ,IAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AACjB,IAAA,KAAA,CAAM,KAAA,IAAQ;AASd,IAAA,MAAM,QAAiB,EAAC;AACxB,IAAA,IAAI,WAAA,GAA2C,IAAA;AAC/C,IAAA,MAAM,IAAA,GAAO,CAAC,CAAA,KAAa;AAKzB,MAAA,IAAI,WAAA,EAAa;AACf,QAAA,MAAM,CAAA,GAAI,WAAA;AACV,QAAA,WAAA,GAAc,IAAA;AACd,QAAA,CAAA,CAAE,CAAC,CAAA;AAAA,MACL,CAAA,MAAO;AACL,QAAA,KAAA,CAAM,KAAK,CAAC,CAAA;AAAA,MACd;AAAA,IACF,CAAA;AACA,IAAA,MAAM,IAAA,GAAO,MACX,IAAI,OAAA,CAAQ,CAACE,QAAAA,KAAY;AACvB,MAAA,MAAM,CAAA,GAAI,MAAM,KAAA,EAAM;AACtB,MAAA,IAAI,CAAA,EAAGA,QAAAA,CAAQ,CAAC,CAAA;AAAA,WACX,WAAA,GAAcA,QAAAA;AAAA,IACrB,CAAC,CAAA;AAEH,IAAA,IAAI,SAAA,GAAY,KAAK,GAAA,EAAI;AACzB,IAAA,MAAM,QAAQ,MAAM;AAClB,MAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AACjC,MAAA,MAAM,IAAA,GAAO,OAAA;AACb,MAAA,OAAA,GAAU,EAAA;AACV,MAAA,SAAA,GAAY,KAAK,GAAA,EAAI;AACrB,MAAA,OAAO,IAAA;AAAA,IACT,CAAA;AAEA,IAAA,KAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAU;AAClC,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,EAAS;AAC5B,MAAA,GAAA,IAAO,IAAA;AACP,MAAA,OAAA,IAAW,IAAA;AACX,MAAA,IAAA,CAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,CAAA;AAAA,IAC7B,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAU;AAClC,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,EAAS;AAC5B,MAAA,GAAA,IAAO,IAAA;AACP,MAAA,OAAA,IAAW,IAAA;AACX,MAAA,IAAA,CAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,CAAA;AAAA,IAC7B,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,CAAC,GAAA,KAAQ;AACzB,MAAA,KAAA,MAAW,CAAA,IAAK,MAAA,EAAQ,YAAA,CAAa,CAAC,CAAA;AACtC,MAAA,IAAA,CAAK,EAAE,IAAA,EAAM,OAAA,EAAS,GAAA,EAAK,CAAA;AAAA,IAC7B,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,CAAC,IAAA,KAAS;AAC1B,MAAA,KAAA,MAAW,CAAA,IAAK,MAAA,EAAQ,YAAA,CAAa,CAAC,CAAA;AACtC,MAAA,IAAA,CAAK,EAAE,IAAA,EAAM,KAAA,EAAO,IAAA,EAAM,CAAA;AAAA,IAC5B,CAAC,CAAA;AAED,IAAA,IAAI;AACF,MAAA,OAAO,IAAA,EAAM;AACX,QAAA,MAAM,CAAA,GAAI,MAAM,IAAA,EAAK;AACrB,QAAA,IAAI,CAAA,CAAE,IAAA,KAAS,OAAA,EAAS,MAAM,CAAA,CAAE,GAAA;AAChC,QAAA,IAAI,CAAA,CAAE,SAAS,KAAA,EAAO;AACpB,UAAA,MAAM,YAAY,KAAA,EAAM;AACxB,UAAA,IAAI,cAAc,IAAA,EAAM;AACtB,YAAA,MAAM,EAAE,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,SAAA,EAAU;AAAA,UAClD;AACA,UAAA,MAAM,UAAU,SAAA,CAAU,GAAG,CAAA,CAAE,OAAA,CAAQ,UAAU,IAAI,CAAA;AACrD,UAAA,MAAM;AAAA,YACJ,IAAA,EAAM,OAAA;AAAA,YACN,MAAA,EAAQ;AAAA,cACN,MAAA,EAAQ,cAAA,CAAe,OAAA,EAAS,UAAU,CAAA;AAAA,cAC1C,WAAW,CAAA,CAAE,IAAA;AAAA,cACb,SAAA,EAAW;AAAA;AACb,WACF;AACA,UAAA;AAAA,QACF;AAIA,QAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,QAAA,IAAI,OAAA,CAAQ,MAAA,IAAU,kBAAA,IAAsB,GAAA,GAAM,aAAa,wBAAA,EAA0B;AACvF,UAAA,MAAM,OAAO,KAAA,EAAM;AACnB,UAAA,IAAI,IAAA,EAAM,MAAM,EAAE,IAAA,EAAM,kBAAkB,IAAA,EAAK;AAAA,QACjD;AAAA,MACF;AAAA,IACF,CAAA,SAAE;AACA,MAAA,KAAA,MAAW,CAAA,IAAK,MAAA,EAAQ,YAAA,CAAa,CAAC,CAAA;AAAA,IACxC;AAAA,EACF;AACF","file":"bash.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import { spawn } from 'node:child_process';\nimport * as os from 'node:os';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { stripAnsi } from '@wrongstack/core';\nimport { buildChildEnv } from './_env.js';\nimport { truncateMiddle } from './_util.js';\n\ninterface BashInput {\n command: string;\n timeout_ms?: number;\n background?: boolean;\n}\n\ninterface BashOutput {\n output: string;\n exit_code: number | null;\n timed_out: boolean;\n pid?: number;\n}\n\nconst MAX_OUTPUT = 32_768;\nconst DEFAULT_TIMEOUT = 30_000;\n// Flush partial_output every 200ms or when 4 KiB accumulates — whichever\n// comes first. Smaller batches make the TUI feel responsive; larger ones\n// keep EventBus traffic reasonable on chatty processes.\nconst STREAM_FLUSH_INTERVAL_MS = 200;\nconst STREAM_FLUSH_BYTES = 4 * 1024;\n\nexport const bashTool: Tool<BashInput, BashOutput> = {\n name: 'bash',\n category: 'Shell',\n description: 'Run a shell command. stdout and stderr are merged.',\n usageHint:\n 'Runs via `bash -c` (or `cmd /c` on Windows). Cwd is the project root. Default timeout 30s. Output truncated from the middle if oversized. Use for git, npm, builds, tests.',\n permission: 'confirm',\n mutating: true,\n // Trust rules match on the literal `command` string. Without subjectKey\n // the policy heuristic would have done the same here, but declaring it\n // explicitly removes the implicit cross-tool aliasing.\n subjectKey: 'command',\n timeoutMs: 30_000,\n maxOutputBytes: MAX_OUTPUT,\n estimatedDurationMs: 3_000,\n inputSchema: {\n type: 'object',\n properties: {\n command: { type: 'string' },\n timeout_ms: { type: 'integer' },\n background: { type: 'boolean' },\n },\n required: ['command'],\n },\n async execute(input, ctx, opts) {\n let final: BashOutput | undefined;\n for await (const ev of bashTool.executeStream!(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('bash: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<BashOutput>> {\n if (!input?.command) throw new Error('bash: command is required');\n const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT, 600_000));\n\n const isWin = os.platform() === 'win32';\n const shell = isWin\n ? (process.env['COMSPEC'] ?? 'cmd.exe')\n : (process.env['SHELL'] ?? '/bin/bash');\n const args = isWin ? ['/c', input.command] : ['-c', input.command];\n\n const env = buildChildEnv(ctx.session?.id);\n\n // On POSIX we put the shell in its own process group so that timeout /\n // abort can kill the entire group with `process.kill(-pid)`. Otherwise\n // `bash -c \"sleep 9999 & disown\"` would leave the grandchild running.\n // `detached: true` is also reused for the user-facing background mode;\n // we always want detached on POSIX, only on Windows is it tied to the\n // explicit background flag.\n const detached = isWin ? !!input.background : true;\n const child = spawn(shell, args, {\n cwd: ctx.projectRoot,\n env,\n stdio: input.background ? 'ignore' : ['ignore', 'pipe', 'pipe'],\n detached,\n signal: opts.signal,\n });\n\n if (input.background) {\n // Background mode: capture stdout/stderr with bounded buffers so a\n // malicious command can't write unbounded output. Apply MAX_OUTPUT cap.\n let buf = '';\n let truncated = false;\n const child = spawn(shell, args, {\n cwd: ctx.projectRoot,\n env,\n stdio: ['ignore', 'pipe', 'pipe'],\n detached: true,\n signal: opts.signal,\n });\n const pid = child.pid;\n child.stdout?.on('data', (chunk: Buffer) => {\n if (!truncated) {\n const remain = MAX_OUTPUT - buf.length;\n if (remain > 0) {\n buf += chunk.toString().slice(0, remain);\n }\n if (buf.length >= MAX_OUTPUT) truncated = true;\n }\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n if (!truncated) {\n const remain = MAX_OUTPUT - buf.length;\n if (remain > 0) {\n buf += chunk.toString().slice(0, remain);\n }\n if (buf.length >= MAX_OUTPUT) truncated = true;\n }\n });\n child.on('close', () => {\n /* async generator — nothing to yield after close in background mode */\n });\n if (typeof pid === 'number') child.unref();\n yield {\n type: 'final',\n output: {\n output: truncated ? buf.slice(0, MAX_OUTPUT) + '…[truncated]' : buf,\n exit_code: null,\n timed_out: false,\n pid,\n },\n };\n return;\n }\n\n let buf = '';\n let pending = '';\n let timedOut = false;\n const timers: NodeJS.Timeout[] = [];\n const timer = setTimeout(() => {\n timedOut = true;\n if (isWin) {\n try {\n child.kill();\n } catch {\n /* ignore */\n }\n } else {\n try {\n // Kill the process group, not just the shell — pid is positive,\n // group id is the negated pid. Without this a runaway grandchild\n // ('sleep 9999 & disown') survives bash termination.\n if (typeof child.pid === 'number') {\n try {\n process.kill(-child.pid, 'SIGTERM');\n } catch {\n child.kill('SIGTERM');\n }\n } else {\n child.kill('SIGTERM');\n }\n const killTimer = setTimeout(() => {\n try {\n if (typeof child.pid === 'number') {\n try {\n process.kill(-child.pid, 'SIGKILL');\n } catch {\n child.kill('SIGKILL');\n }\n } else {\n child.kill('SIGKILL');\n }\n } catch {\n /* ignore */\n }\n }, 2000);\n timers.push(killTimer);\n killTimer.unref?.(); // Don't keep event loop alive on clean exit\n } catch {\n /* ignore */\n }\n }\n }, timeoutMs);\n timers.push(timer);\n timer.unref?.();\n\n // Bridge the EventEmitter-style child to an async iterator. We push\n // chunks into a queue and let the generator pull them; this lets us\n // yield 'partial_output' events to the executor at flush boundaries.\n type Chunk =\n | { kind: 'data'; text: string }\n | { kind: 'end'; code: number | null }\n | { kind: 'error'; err: Error };\n const queue: Chunk[] = [];\n let resolveNext: ((c: Chunk) => void) | null = null;\n const push = (c: Chunk) => {\n // Node.js EventEmitter guarantees no 'data' events fire after 'close',\n // so resolveNext can only be set when the consumer loop is alive.\n // Theoretically a custom stream could violate this, but the bash tool\n // only uses node:child_process streams which follow the contract.\n if (resolveNext) {\n const r = resolveNext;\n resolveNext = null;\n r(c);\n } else {\n queue.push(c);\n }\n };\n const next = (): Promise<Chunk> =>\n new Promise((resolve) => {\n const c = queue.shift();\n if (c) resolve(c);\n else resolveNext = resolve;\n });\n\n let lastFlush = Date.now();\n const flush = () => {\n if (pending.length === 0) return null;\n const text = pending;\n pending = '';\n lastFlush = Date.now();\n return text;\n };\n\n child.stdout?.on('data', (chunk) => {\n const text = chunk.toString();\n buf += text;\n pending += text;\n push({ kind: 'data', text });\n });\n child.stderr?.on('data', (chunk) => {\n const text = chunk.toString();\n buf += text;\n pending += text;\n push({ kind: 'data', text });\n });\n child.on('error', (err) => {\n for (const t of timers) clearTimeout(t);\n push({ kind: 'error', err });\n });\n child.on('close', (code) => {\n for (const t of timers) clearTimeout(t);\n push({ kind: 'end', code });\n });\n\n try {\n while (true) {\n const c = await next();\n if (c.kind === 'error') throw c.err;\n if (c.kind === 'end') {\n const remainder = flush();\n if (remainder !== null) {\n yield { type: 'partial_output', text: remainder };\n }\n const cleaned = stripAnsi(buf).replace(/\\r\\n?/g, '\\n');\n yield {\n type: 'final',\n output: {\n output: truncateMiddle(cleaned, MAX_OUTPUT),\n exit_code: c.code,\n timed_out: timedOut,\n },\n };\n return;\n }\n // Decide whether to flush. Time-based OR size-based to keep latency\n // low for slow-emitting commands without overwhelming the TUI for\n // chatty ones.\n const now = Date.now();\n if (pending.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {\n const text = flush();\n if (text) yield { type: 'partial_output', text };\n }\n }\n } finally {\n for (const t of timers) clearTimeout(t);\n }\n },\n};\n\n// Re-export types so consumers can narrow on stream events.\nexport type { BashInput, BashOutput };\n"]}
|
package/dist/builtin.js
CHANGED
|
@@ -61,8 +61,8 @@ async function* spawnStream(opts) {
|
|
|
61
61
|
let spawnFailed = false;
|
|
62
62
|
for (; ; ) {
|
|
63
63
|
while (queue.length === 0) {
|
|
64
|
-
await new Promise((
|
|
65
|
-
waiter =
|
|
64
|
+
await new Promise((resolve5) => {
|
|
65
|
+
waiter = resolve5;
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
const chunk = queue.shift();
|
|
@@ -284,12 +284,41 @@ var bashTool = {
|
|
|
284
284
|
signal: opts.signal
|
|
285
285
|
});
|
|
286
286
|
if (input.background) {
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
let buf2 = "";
|
|
288
|
+
let truncated = false;
|
|
289
|
+
const child2 = spawn(shell, args, {
|
|
290
|
+
cwd: ctx.projectRoot,
|
|
291
|
+
env,
|
|
292
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
293
|
+
detached: true,
|
|
294
|
+
signal: opts.signal
|
|
295
|
+
});
|
|
296
|
+
const pid = child2.pid;
|
|
297
|
+
child2.stdout?.on("data", (chunk) => {
|
|
298
|
+
if (!truncated) {
|
|
299
|
+
const remain = MAX_OUTPUT - buf2.length;
|
|
300
|
+
if (remain > 0) {
|
|
301
|
+
buf2 += chunk.toString().slice(0, remain);
|
|
302
|
+
}
|
|
303
|
+
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
child2.stderr?.on("data", (chunk) => {
|
|
307
|
+
if (!truncated) {
|
|
308
|
+
const remain = MAX_OUTPUT - buf2.length;
|
|
309
|
+
if (remain > 0) {
|
|
310
|
+
buf2 += chunk.toString().slice(0, remain);
|
|
311
|
+
}
|
|
312
|
+
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
child2.on("close", () => {
|
|
316
|
+
});
|
|
317
|
+
if (typeof pid === "number") child2.unref();
|
|
289
318
|
yield {
|
|
290
319
|
type: "final",
|
|
291
320
|
output: {
|
|
292
|
-
output:
|
|
321
|
+
output: truncated ? buf2.slice(0, MAX_OUTPUT) + "\u2026[truncated]" : buf2,
|
|
293
322
|
exit_code: null,
|
|
294
323
|
timed_out: false,
|
|
295
324
|
pid
|
|
@@ -334,6 +363,7 @@ var bashTool = {
|
|
|
334
363
|
}
|
|
335
364
|
}, 2e3);
|
|
336
365
|
timers.push(killTimer);
|
|
366
|
+
killTimer.unref?.();
|
|
337
367
|
} catch {
|
|
338
368
|
}
|
|
339
369
|
}
|
|
@@ -351,10 +381,10 @@ var bashTool = {
|
|
|
351
381
|
queue.push(c);
|
|
352
382
|
}
|
|
353
383
|
};
|
|
354
|
-
const next = () => new Promise((
|
|
384
|
+
const next = () => new Promise((resolve5) => {
|
|
355
385
|
const c = queue.shift();
|
|
356
|
-
if (c)
|
|
357
|
-
else resolveNext =
|
|
386
|
+
if (c) resolve5(c);
|
|
387
|
+
else resolveNext = resolve5;
|
|
358
388
|
});
|
|
359
389
|
let lastFlush = Date.now();
|
|
360
390
|
const flush = () => {
|
|
@@ -589,7 +619,7 @@ function findGitDir(cwd) {
|
|
|
589
619
|
return null;
|
|
590
620
|
}
|
|
591
621
|
function runGit(args, cwd, signal) {
|
|
592
|
-
return new Promise((
|
|
622
|
+
return new Promise((resolve5) => {
|
|
593
623
|
let stdout = "";
|
|
594
624
|
let stderr = "";
|
|
595
625
|
const child = spawn("git", args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -599,8 +629,8 @@ function runGit(args, cwd, signal) {
|
|
|
599
629
|
child.stderr?.on("data", (c) => {
|
|
600
630
|
stderr += c.toString();
|
|
601
631
|
});
|
|
602
|
-
child.on("close", (code) =>
|
|
603
|
-
child.on("error", (e) =>
|
|
632
|
+
child.on("close", (code) => resolve5({ stdout, stderr, exitCode: code ?? 0 }));
|
|
633
|
+
child.on("error", (e) => resolve5({ stdout: "", stderr: e.message, exitCode: 1 }));
|
|
604
634
|
});
|
|
605
635
|
}
|
|
606
636
|
async function fileDiff(input, ctx, signal) {
|
|
@@ -934,7 +964,7 @@ var ALLOWED_COMMANDS = {
|
|
|
934
964
|
cargo: ["--version", "build", "test", "check"],
|
|
935
965
|
rustc: ["--version"],
|
|
936
966
|
go: ["version", "run", "build", "test"],
|
|
937
|
-
python: ["--version"
|
|
967
|
+
python: ["--version"],
|
|
938
968
|
pip: ["--version", "install", "list"],
|
|
939
969
|
docker: ["--version", "ps", "images", "build"],
|
|
940
970
|
kubectl: ["version", "get", "describe", "logs"]
|
|
@@ -942,6 +972,30 @@ var ALLOWED_COMMANDS = {
|
|
|
942
972
|
var MAX_ARGS = 20;
|
|
943
973
|
var MAX_OUTPUT2 = 2e5;
|
|
944
974
|
var TIMEOUT_MS = 3e4;
|
|
975
|
+
var BLOCKED_ARG_PATTERNS = {
|
|
976
|
+
// python -c/--command executes arbitrary code; python -m runs modules
|
|
977
|
+
python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
|
|
978
|
+
// git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack
|
|
979
|
+
git: [/^--exec=/, /^--upload-pack=/, /^--receive-pack=/],
|
|
980
|
+
// node -r/--require preloads arbitrary modules; --eval executes code
|
|
981
|
+
node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],
|
|
982
|
+
// go run could execute arbitrary .go files; -ldflags could inject build-time code
|
|
983
|
+
go: [/^-ldflags$/],
|
|
984
|
+
// bun --preload is similar to node --require
|
|
985
|
+
bun: [/^--preload$/]
|
|
986
|
+
};
|
|
987
|
+
function validateArgs(cmd, args) {
|
|
988
|
+
const blocked = BLOCKED_ARG_PATTERNS[cmd];
|
|
989
|
+
if (!blocked) return null;
|
|
990
|
+
for (const arg of args) {
|
|
991
|
+
for (const pattern of blocked) {
|
|
992
|
+
if (pattern.test(arg)) {
|
|
993
|
+
return `Blocked argument "${arg}" for command "${cmd}" (matches security pattern ${pattern})`;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
945
999
|
var execTool = {
|
|
946
1000
|
name: "exec",
|
|
947
1001
|
category: "Shell",
|
|
@@ -985,6 +1039,18 @@ var execTool = {
|
|
|
985
1039
|
}
|
|
986
1040
|
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
987
1041
|
const timeout = Math.max(1, Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS));
|
|
1042
|
+
const argError = validateArgs(cmd, args);
|
|
1043
|
+
if (argError) {
|
|
1044
|
+
return {
|
|
1045
|
+
command: cmd,
|
|
1046
|
+
args,
|
|
1047
|
+
stdout: "",
|
|
1048
|
+
stderr: argError,
|
|
1049
|
+
exitCode: 1,
|
|
1050
|
+
truncated: false,
|
|
1051
|
+
allowed: false
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
988
1054
|
const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
|
|
989
1055
|
const rel = path.relative(ctx.projectRoot, requestedCwd);
|
|
990
1056
|
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
@@ -1004,7 +1070,7 @@ var execTool = {
|
|
|
1004
1070
|
}
|
|
1005
1071
|
};
|
|
1006
1072
|
function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
1007
|
-
return new Promise((
|
|
1073
|
+
return new Promise((resolve5) => {
|
|
1008
1074
|
let stdout = "";
|
|
1009
1075
|
let stderr = "";
|
|
1010
1076
|
let killed = false;
|
|
@@ -1026,7 +1092,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1026
1092
|
});
|
|
1027
1093
|
child.on("close", (code) => {
|
|
1028
1094
|
clearTimeout(timer);
|
|
1029
|
-
|
|
1095
|
+
resolve5({
|
|
1030
1096
|
command: cmd,
|
|
1031
1097
|
args,
|
|
1032
1098
|
stdout: stdout.slice(0, MAX_OUTPUT2),
|
|
@@ -1038,7 +1104,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1038
1104
|
});
|
|
1039
1105
|
child.on("error", (err) => {
|
|
1040
1106
|
clearTimeout(timer);
|
|
1041
|
-
|
|
1107
|
+
resolve5({
|
|
1042
1108
|
command: cmd,
|
|
1043
1109
|
args,
|
|
1044
1110
|
stdout: stdout.slice(0, MAX_OUTPUT2),
|
|
@@ -1565,7 +1631,7 @@ function buildArgs(input) {
|
|
|
1565
1631
|
}
|
|
1566
1632
|
}
|
|
1567
1633
|
function runGit2(args, cwd, signal) {
|
|
1568
|
-
return new Promise((
|
|
1634
|
+
return new Promise((resolve5) => {
|
|
1569
1635
|
let stdout = "";
|
|
1570
1636
|
let stderr = "";
|
|
1571
1637
|
const child = spawn("git", args, {
|
|
@@ -1584,7 +1650,7 @@ function runGit2(args, cwd, signal) {
|
|
|
1584
1650
|
}
|
|
1585
1651
|
});
|
|
1586
1652
|
child.on("error", (err) => {
|
|
1587
|
-
|
|
1653
|
+
resolve5({
|
|
1588
1654
|
command: args[0],
|
|
1589
1655
|
stdout,
|
|
1590
1656
|
stderr: err.message,
|
|
@@ -1593,7 +1659,7 @@ function runGit2(args, cwd, signal) {
|
|
|
1593
1659
|
});
|
|
1594
1660
|
});
|
|
1595
1661
|
child.on("close", (code) => {
|
|
1596
|
-
|
|
1662
|
+
resolve5({
|
|
1597
1663
|
command: args[0],
|
|
1598
1664
|
stdout: stdout.slice(0, MAX_OUTPUT3),
|
|
1599
1665
|
stderr: stderr.slice(0, MAX_OUTPUT3),
|
|
@@ -1774,13 +1840,13 @@ var grepTool = {
|
|
|
1774
1840
|
}
|
|
1775
1841
|
};
|
|
1776
1842
|
async function detectRg(signal) {
|
|
1777
|
-
return new Promise((
|
|
1843
|
+
return new Promise((resolve5) => {
|
|
1778
1844
|
try {
|
|
1779
1845
|
const p = spawn("rg", ["--version"], { stdio: "ignore", signal });
|
|
1780
|
-
p.on("error", () =>
|
|
1781
|
-
p.on("close", (code) =>
|
|
1846
|
+
p.on("error", () => resolve5(false));
|
|
1847
|
+
p.on("close", (code) => resolve5(code === 0));
|
|
1782
1848
|
} catch {
|
|
1783
|
-
|
|
1849
|
+
resolve5(false);
|
|
1784
1850
|
}
|
|
1785
1851
|
});
|
|
1786
1852
|
}
|
|
@@ -2039,6 +2105,22 @@ var installTool = {
|
|
|
2039
2105
|
const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
|
|
2040
2106
|
(p) => p.trim()
|
|
2041
2107
|
) : [];
|
|
2108
|
+
const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
|
|
2109
|
+
for (const pkg of pkgList) {
|
|
2110
|
+
if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-")) {
|
|
2111
|
+
yield {
|
|
2112
|
+
type: "final",
|
|
2113
|
+
output: {
|
|
2114
|
+
packages: pkgList,
|
|
2115
|
+
exit_code: 1,
|
|
2116
|
+
output: `Invalid package name "${pkg}". Names must match ${PKG_NAME_RE} and not start with "-".`,
|
|
2117
|
+
dry_run: Boolean(input.dry_run),
|
|
2118
|
+
truncated: false
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2042
2124
|
if (pkgList.length > 0) args.push(...pkgList);
|
|
2043
2125
|
yield {
|
|
2044
2126
|
type: "log",
|
|
@@ -2361,8 +2443,17 @@ var logsTool = {
|
|
|
2361
2443
|
async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
2362
2444
|
const args = ["logs"];
|
|
2363
2445
|
if (lines > 0) args.push("--tail", String(lines));
|
|
2446
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]+$/.test(service)) {
|
|
2447
|
+
return {
|
|
2448
|
+
source: `docker:${service}`,
|
|
2449
|
+
entries: [],
|
|
2450
|
+
total: 0,
|
|
2451
|
+
truncated: false,
|
|
2452
|
+
stream_mode: false
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2364
2455
|
args.push("--timestamps", service);
|
|
2365
|
-
return new Promise((
|
|
2456
|
+
return new Promise((resolve5) => {
|
|
2366
2457
|
let stdout = "";
|
|
2367
2458
|
let stderr = "";
|
|
2368
2459
|
const MAX = 2e5;
|
|
@@ -2376,7 +2467,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
2376
2467
|
child.on("close", (code) => {
|
|
2377
2468
|
const output = stdout + stderr;
|
|
2378
2469
|
const entries = parseLogLines(output, filterRe);
|
|
2379
|
-
|
|
2470
|
+
resolve5({
|
|
2380
2471
|
source: `docker:${service}`,
|
|
2381
2472
|
entries,
|
|
2382
2473
|
total: entries.length,
|
|
@@ -2386,7 +2477,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
2386
2477
|
});
|
|
2387
2478
|
child.on(
|
|
2388
2479
|
"error",
|
|
2389
|
-
(e) =>
|
|
2480
|
+
(e) => resolve5({
|
|
2390
2481
|
source: `docker:${service}`,
|
|
2391
2482
|
entries: [],
|
|
2392
2483
|
total: 0,
|
|
@@ -2520,7 +2611,7 @@ async function detectManager2(cwd) {
|
|
|
2520
2611
|
return "npm";
|
|
2521
2612
|
}
|
|
2522
2613
|
function runOutdated(manager, args, cwd, signal) {
|
|
2523
|
-
return new Promise((
|
|
2614
|
+
return new Promise((resolve5) => {
|
|
2524
2615
|
let stdout = "";
|
|
2525
2616
|
let stderr = "";
|
|
2526
2617
|
const MAX = 1e5;
|
|
@@ -2533,11 +2624,11 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
2533
2624
|
});
|
|
2534
2625
|
child.on("close", (code) => {
|
|
2535
2626
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
2536
|
-
|
|
2627
|
+
resolve5(result);
|
|
2537
2628
|
});
|
|
2538
2629
|
child.on(
|
|
2539
2630
|
"error",
|
|
2540
|
-
(e) =>
|
|
2631
|
+
(e) => resolve5({
|
|
2541
2632
|
exit_code: 1,
|
|
2542
2633
|
packages: [],
|
|
2543
2634
|
total: 0,
|
|
@@ -2620,7 +2711,7 @@ var patchTool = {
|
|
|
2620
2711
|
};
|
|
2621
2712
|
}
|
|
2622
2713
|
}
|
|
2623
|
-
const tmpDir = await fs9.mkdtemp(path.join(
|
|
2714
|
+
const tmpDir = await fs9.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
|
|
2624
2715
|
try {
|
|
2625
2716
|
await fs9.chmod(tmpDir, 448).catch(() => {
|
|
2626
2717
|
});
|
|
@@ -2667,7 +2758,7 @@ function stripPathComponents(p, strip) {
|
|
|
2667
2758
|
return parts.slice(strip).join("/");
|
|
2668
2759
|
}
|
|
2669
2760
|
function runPatch(args, cwd, signal) {
|
|
2670
|
-
return new Promise((
|
|
2761
|
+
return new Promise((resolve5) => {
|
|
2671
2762
|
let stdout = "";
|
|
2672
2763
|
let stderr = "";
|
|
2673
2764
|
const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
|
|
@@ -2678,8 +2769,8 @@ function runPatch(args, cwd, signal) {
|
|
|
2678
2769
|
child.stderr?.on("data", (c) => {
|
|
2679
2770
|
stderr += c.toString();
|
|
2680
2771
|
});
|
|
2681
|
-
child.on("close", (code) =>
|
|
2682
|
-
child.on("error", (e) =>
|
|
2772
|
+
child.on("close", (code) => resolve5({ exitCode: code ?? 1, stdout, stderr }));
|
|
2773
|
+
child.on("error", (e) => resolve5({ exitCode: 1, stdout: "", stderr: e.message }));
|
|
2683
2774
|
});
|
|
2684
2775
|
}
|
|
2685
2776
|
function extractPatchedFiles(output) {
|
|
@@ -2810,7 +2901,14 @@ var readTool = {
|
|
|
2810
2901
|
async execute(input, ctx) {
|
|
2811
2902
|
if (!input?.path) throw new Error("read: path is required");
|
|
2812
2903
|
const absPath = safeResolve(input.path, ctx);
|
|
2813
|
-
|
|
2904
|
+
let stat9;
|
|
2905
|
+
try {
|
|
2906
|
+
stat9 = await fs9.stat(absPath);
|
|
2907
|
+
} catch (err) {
|
|
2908
|
+
const code = err.code;
|
|
2909
|
+
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
2910
|
+
throw new Error(`read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`);
|
|
2911
|
+
}
|
|
2814
2912
|
if (!stat9.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
2815
2913
|
if (stat9.size > MAX_BYTES2) {
|
|
2816
2914
|
throw new Error(`read: file too large (${stat9.size} bytes, limit ${MAX_BYTES2})`);
|
|
@@ -2975,13 +3073,13 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
2975
3073
|
return await globNative(pattern, base, extraGlob);
|
|
2976
3074
|
}
|
|
2977
3075
|
function checkRg() {
|
|
2978
|
-
return new Promise((
|
|
3076
|
+
return new Promise((resolve5) => {
|
|
2979
3077
|
try {
|
|
2980
3078
|
const p = spawn("rg", ["--version"], { stdio: "ignore" });
|
|
2981
|
-
p.on("error", () =>
|
|
2982
|
-
p.on("close", (code) =>
|
|
3079
|
+
p.on("error", () => resolve5(false));
|
|
3080
|
+
p.on("close", (code) => resolve5(code === 0));
|
|
2983
3081
|
} catch {
|
|
2984
|
-
|
|
3082
|
+
resolve5(false);
|
|
2985
3083
|
}
|
|
2986
3084
|
});
|
|
2987
3085
|
}
|
|
@@ -2993,10 +3091,10 @@ function spawnRgFind(pattern, base) {
|
|
|
2993
3091
|
buf += chunk.toString();
|
|
2994
3092
|
});
|
|
2995
3093
|
return {
|
|
2996
|
-
promise: new Promise((
|
|
3094
|
+
promise: new Promise((resolve5, reject) => {
|
|
2997
3095
|
child.on("error", reject);
|
|
2998
3096
|
child.on("close", () => {
|
|
2999
|
-
|
|
3097
|
+
resolve5(buf.split("\n").filter(Boolean));
|
|
3000
3098
|
});
|
|
3001
3099
|
})
|
|
3002
3100
|
};
|
|
@@ -3160,7 +3258,7 @@ var scaffoldTool = {
|
|
|
3160
3258
|
const vars = { name, ...input.vars };
|
|
3161
3259
|
const builtIn = BUILT_IN_TEMPLATES[input.template];
|
|
3162
3260
|
if (builtIn) {
|
|
3163
|
-
return await handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);
|
|
3261
|
+
return await handleBuiltIn(name, builtIn.files, cwd, ctx, input.dry_run ?? false, vars);
|
|
3164
3262
|
}
|
|
3165
3263
|
return {
|
|
3166
3264
|
template: input.template,
|
|
@@ -3172,12 +3270,19 @@ var scaffoldTool = {
|
|
|
3172
3270
|
};
|
|
3173
3271
|
}
|
|
3174
3272
|
};
|
|
3175
|
-
async function handleBuiltIn(name, templateFiles, cwd, dryRun, vars) {
|
|
3273
|
+
async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
3176
3274
|
const files = [];
|
|
3177
3275
|
let filesCreated = 0;
|
|
3178
3276
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
3179
3277
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
3180
|
-
const
|
|
3278
|
+
const joinedPath = path.join(cwd, resolvedPath);
|
|
3279
|
+
const root = path.resolve(ctx.projectRoot);
|
|
3280
|
+
const target = path.resolve(joinedPath);
|
|
3281
|
+
const rel = path.relative(root, target);
|
|
3282
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
3283
|
+
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
3284
|
+
}
|
|
3285
|
+
const fullPath = target;
|
|
3181
3286
|
if (!dryRun) {
|
|
3182
3287
|
await fs9.mkdir(path.dirname(fullPath), { recursive: true });
|
|
3183
3288
|
await fs9.writeFile(fullPath, substituteVars(content, name, vars), "utf8");
|
|
@@ -4147,11 +4252,11 @@ var writeTool = {
|
|
|
4147
4252
|
existed = stat10.isFile();
|
|
4148
4253
|
if (existed) {
|
|
4149
4254
|
if (!ctx.hasRead(absPath)) {
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4255
|
+
prev = await fs9.readFile(absPath, "utf8");
|
|
4256
|
+
ctx.recordRead(absPath, stat10.mtimeMs);
|
|
4257
|
+
} else {
|
|
4258
|
+
prev = await fs9.readFile(absPath, "utf8");
|
|
4153
4259
|
}
|
|
4154
|
-
prev = await fs9.readFile(absPath, "utf8");
|
|
4155
4260
|
}
|
|
4156
4261
|
} catch (err) {
|
|
4157
4262
|
if (err.code !== "ENOENT") {
|