@tagma/sdk 0.1.7 → 0.1.9
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/README.md +23 -5
- package/package.json +1 -1
- package/src/adapters/stdin-approval.ts +117 -117
- package/src/adapters/websocket-approval.ts +175 -144
- package/src/approval.ts +4 -1
- package/src/completions/exit-code.ts +19 -19
- package/src/completions/file-exists.ts +39 -39
- package/src/completions/output-check.ts +57 -57
- package/src/config-ops.ts +239 -183
- package/src/dag.ts +222 -137
- package/src/drivers/claude-code.ts +207 -207
- package/src/engine.ts +743 -698
- package/src/hooks.ts +147 -138
- package/src/logger.ts +112 -107
- package/src/middlewares/static-context.ts +29 -29
- package/src/pipeline-runner.ts +126 -113
- package/src/runner.ts +213 -195
- package/src/schema.ts +386 -358
- package/src/sdk.ts +2 -2
- package/src/triggers/file.ts +105 -94
- package/src/triggers/manual.ts +61 -61
- package/src/utils.ts +154 -147
- package/src/validate-raw.ts +223 -203
package/src/runner.ts
CHANGED
|
@@ -1,196 +1,214 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, join } from 'node:path';
|
|
3
|
-
import type { SpawnSpec, DriverPlugin, TaskResult, TaskConfig } from './types';
|
|
4
|
-
import { shellArgs } from './utils';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
|
+
import type { SpawnSpec, DriverPlugin, TaskResult, TaskConfig } from './types';
|
|
4
|
+
import { shellArgs } from './utils';
|
|
5
|
+
|
|
6
|
+
// Delay before escalating SIGTERM to SIGKILL when killing a timed-out process.
|
|
7
|
+
const SIGKILL_DELAY_MS = 3_000;
|
|
8
|
+
|
|
9
|
+
export interface RunOptions {
|
|
10
|
+
readonly timeoutMs?: number;
|
|
11
|
+
readonly signal?: AbortSignal; // pipeline-level abort
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* On Windows, Bun.spawn does NOT auto-append PATHEXT extensions like
|
|
16
|
+
* CreateProcess does. A bare command like `claude` fails with ENOENT if the
|
|
17
|
+
* actual file on disk is `claude.cmd` / `claude.bat` / `claude.ps1`. We
|
|
18
|
+
* manually resolve the command against PATH + PATHEXT here so Drivers can
|
|
19
|
+
* keep using short names (`claude`, `npx`, etc.) cross-platform.
|
|
20
|
+
*
|
|
21
|
+
* Results are cached by (cmd, envPath) key so repeated spawns of the same
|
|
22
|
+
* command don't block the event loop with synchronous PATH scans.
|
|
23
|
+
*
|
|
24
|
+
* Returns the original name if resolution fails; Bun will raise the same
|
|
25
|
+
* ENOENT it would have otherwise.
|
|
26
|
+
*/
|
|
27
|
+
const resolvedExeCache = new Map<string, string | null>();
|
|
28
|
+
|
|
29
|
+
function resolveWindowsExe(
|
|
30
|
+
args: readonly string[],
|
|
31
|
+
envPath: string,
|
|
32
|
+
): readonly string[] {
|
|
33
|
+
if (process.platform !== 'win32' || args.length === 0) return args;
|
|
34
|
+
const cmd = args[0]!;
|
|
35
|
+
// Already a full path or has an extension → trust caller.
|
|
36
|
+
if (isAbsolute(cmd) || /\.[a-z0-9]+$/i.test(cmd)) return args;
|
|
37
|
+
|
|
38
|
+
const cacheKey = `${cmd}\x00${envPath}`;
|
|
39
|
+
if (resolvedExeCache.has(cacheKey)) {
|
|
40
|
+
// ?? null coerces undefined→null so cached is string|null and the !== null
|
|
41
|
+
// check narrows it to string without a spurious 'undefined' arm.
|
|
42
|
+
const cached = resolvedExeCache.get(cacheKey) ?? null;
|
|
43
|
+
return cached !== null ? [cached, ...args.slice(1)] : args;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const exts = (
|
|
47
|
+
process.env.PATHEXT ??
|
|
48
|
+
'.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC'
|
|
49
|
+
)
|
|
50
|
+
.split(';')
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
const dirs = envPath.split(';').filter(Boolean);
|
|
53
|
+
|
|
54
|
+
for (const dir of dirs) {
|
|
55
|
+
for (const ext of exts) {
|
|
56
|
+
const candidate = join(dir, cmd + ext);
|
|
57
|
+
if (existsSync(candidate)) {
|
|
58
|
+
resolvedExeCache.set(cacheKey, candidate);
|
|
59
|
+
return [candidate, ...args.slice(1)];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
resolvedExeCache.set(cacheKey, null);
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Build a "failed before spawn" result. */
|
|
68
|
+
function failResult(stderr: string, durationMs: number): TaskResult {
|
|
69
|
+
return {
|
|
70
|
+
exitCode: -1,
|
|
71
|
+
stdout: '',
|
|
72
|
+
stderr,
|
|
73
|
+
outputPath: null,
|
|
74
|
+
stderrPath: null,
|
|
75
|
+
durationMs,
|
|
76
|
+
sessionId: null,
|
|
77
|
+
normalizedOutput: null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function runSpawn(
|
|
82
|
+
spec: SpawnSpec,
|
|
83
|
+
driver: DriverPlugin | null,
|
|
84
|
+
opts: RunOptions = {},
|
|
85
|
+
): Promise<TaskResult> {
|
|
86
|
+
const { timeoutMs, signal } = opts;
|
|
87
|
+
const start = performance.now();
|
|
88
|
+
const elapsed = () => Math.round(performance.now() - start);
|
|
89
|
+
|
|
90
|
+
const mergedEnv = { ...process.env, ...(spec.env ?? {}) };
|
|
91
|
+
const resolvedArgs = resolveWindowsExe(
|
|
92
|
+
spec.args,
|
|
93
|
+
mergedEnv.PATH ?? process.env.PATH ?? '',
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// ── 1. Spawn (catch ENOENT / bad-cwd up front) ────────────────────────
|
|
97
|
+
let proc: ReturnType<typeof Bun.spawn>;
|
|
98
|
+
try {
|
|
99
|
+
proc = Bun.spawn(resolvedArgs as string[], {
|
|
100
|
+
cwd: spec.cwd,
|
|
101
|
+
env: mergedEnv,
|
|
102
|
+
stdout: 'pipe',
|
|
103
|
+
stderr: 'pipe',
|
|
104
|
+
stdin: spec.stdin ? 'pipe' : undefined,
|
|
105
|
+
});
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return failResult(String(err), elapsed());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── 2. Write stdin ─────────────────────────────────────────────────────
|
|
111
|
+
// Child may exit before reading (e.g. quick-fail commands that don't
|
|
112
|
+
// touch stdin) → swallow EPIPE rather than surfacing it as an
|
|
113
|
+
// engine-level error.
|
|
114
|
+
if (spec.stdin && proc.stdin && typeof proc.stdin !== 'number') {
|
|
115
|
+
try {
|
|
116
|
+
proc.stdin.write(spec.stdin);
|
|
117
|
+
proc.stdin.end();
|
|
118
|
+
} catch {
|
|
119
|
+
/* ignore EPIPE / closed-pipe errors */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── 3. Timeout & abort handling ────────────────────────────────────────
|
|
124
|
+
let killedByUs = false;
|
|
125
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
126
|
+
let forceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
127
|
+
|
|
128
|
+
const killGracefully = () => {
|
|
129
|
+
if (killedByUs) return;
|
|
130
|
+
killedByUs = true;
|
|
131
|
+
proc.kill('SIGTERM');
|
|
132
|
+
// If the child ignores SIGTERM, escalate to SIGKILL after 3 s.
|
|
133
|
+
forceTimer = setTimeout(() => {
|
|
134
|
+
try {
|
|
135
|
+
proc.kill('SIGKILL');
|
|
136
|
+
} catch {
|
|
137
|
+
/* already exited */
|
|
138
|
+
}
|
|
139
|
+
}, SIGKILL_DELAY_MS);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
143
|
+
timer = setTimeout(killGracefully, timeoutMs);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const onAbort = () => killGracefully();
|
|
147
|
+
if (signal) {
|
|
148
|
+
if (signal.aborted) {
|
|
149
|
+
killGracefully();
|
|
150
|
+
} else {
|
|
151
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── 4. Collect output & wait (parallel to avoid pipe-buffer deadlock) ─
|
|
156
|
+
const stdoutStream = typeof proc.stdout === 'object' ? proc.stdout : undefined;
|
|
157
|
+
const stderrStream = typeof proc.stderr === 'object' ? proc.stderr : undefined;
|
|
158
|
+
|
|
159
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
160
|
+
proc.exited,
|
|
161
|
+
stdoutStream ? new Response(stdoutStream).text() : Promise.resolve(''),
|
|
162
|
+
stderrStream ? new Response(stderrStream).text() : Promise.resolve(''),
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
// ── 5. Cleanup timers & listeners ──────────────────────────────────────
|
|
166
|
+
if (timer) clearTimeout(timer);
|
|
167
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
168
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
169
|
+
|
|
170
|
+
const durationMs = elapsed();
|
|
171
|
+
|
|
172
|
+
// We initiated the kill (timeout or abort) — always treat as non-success
|
|
173
|
+
// regardless of exit code. A process that catches SIGTERM and exits 0 still
|
|
174
|
+
// hit the timeout; letting it pass as success would unblock downstream tasks
|
|
175
|
+
// incorrectly.
|
|
176
|
+
if (killedByUs) {
|
|
177
|
+
return {
|
|
178
|
+
exitCode: -1,
|
|
179
|
+
stdout,
|
|
180
|
+
stderr,
|
|
181
|
+
outputPath: null,
|
|
182
|
+
stderrPath: null,
|
|
183
|
+
durationMs,
|
|
184
|
+
sessionId: null,
|
|
185
|
+
normalizedOutput: null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── 6. Let driver extract metadata ─────────────────────────────────────
|
|
190
|
+
const meta = driver?.parseResult?.(stdout, stderr) ?? {};
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
exitCode,
|
|
194
|
+
stdout,
|
|
195
|
+
stderr,
|
|
196
|
+
outputPath: null,
|
|
197
|
+
stderrPath: null,
|
|
198
|
+
durationMs,
|
|
199
|
+
sessionId: meta.sessionId ?? null,
|
|
200
|
+
normalizedOutput: meta.normalizedOutput ?? null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function runCommand(
|
|
205
|
+
command: string,
|
|
206
|
+
cwd: string,
|
|
207
|
+
opts: RunOptions = {},
|
|
208
|
+
): Promise<TaskResult> {
|
|
209
|
+
const spec: SpawnSpec = {
|
|
210
|
+
args: shellArgs(command),
|
|
211
|
+
cwd,
|
|
212
|
+
};
|
|
213
|
+
return runSpawn(spec, null, opts);
|
|
196
214
|
}
|