@tagma/sdk 0.2.1 → 0.2.3
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 +244 -5
- package/package.json +1 -1
- package/src/drivers/claude-code.ts +30 -9
- package/src/runner.ts +34 -9
package/README.md
CHANGED
|
@@ -64,38 +64,217 @@ console.log(result.success ? 'Done' : 'Failed');
|
|
|
64
64
|
|
|
65
65
|
## Pipeline YAML Reference
|
|
66
66
|
|
|
67
|
+
### Full Structure
|
|
68
|
+
|
|
67
69
|
```yaml
|
|
68
70
|
pipeline:
|
|
69
71
|
name: my-pipeline
|
|
70
|
-
driver: claude-code
|
|
71
|
-
timeout: 30m
|
|
72
|
-
plugins:
|
|
72
|
+
driver: claude-code
|
|
73
|
+
timeout: 30m
|
|
74
|
+
plugins:
|
|
73
75
|
- "@tagma/driver-codex"
|
|
74
76
|
hooks:
|
|
75
77
|
pipeline_start: "echo starting"
|
|
78
|
+
task_start: "echo task begin"
|
|
79
|
+
task_success: "echo task ok"
|
|
76
80
|
task_failure: "notify-slack.sh"
|
|
81
|
+
pipeline_complete: "echo done"
|
|
82
|
+
pipeline_error: "alert.sh"
|
|
77
83
|
tracks:
|
|
78
84
|
- id: track-1
|
|
79
85
|
name: Track One
|
|
80
|
-
|
|
86
|
+
color: "#3b82f6"
|
|
87
|
+
driver: claude-code
|
|
88
|
+
model_tier: high
|
|
89
|
+
agent_profile: senior
|
|
90
|
+
cwd: ./services/backend
|
|
81
91
|
permissions:
|
|
82
92
|
read: true
|
|
83
93
|
write: true
|
|
84
94
|
execute: false
|
|
85
|
-
on_failure: skip_downstream
|
|
95
|
+
on_failure: skip_downstream
|
|
96
|
+
middlewares:
|
|
97
|
+
- type: static_context
|
|
98
|
+
file: ./context.md
|
|
99
|
+
label: Architecture Guide
|
|
86
100
|
tasks:
|
|
87
101
|
- id: task-a
|
|
88
102
|
name: Do something
|
|
89
103
|
prompt: "Your prompt here"
|
|
90
104
|
output: ./output/task-a.txt
|
|
91
105
|
timeout: 10m
|
|
106
|
+
driver: claude-code
|
|
107
|
+
model_tier: high
|
|
108
|
+
agent_profile: senior
|
|
109
|
+
cwd: ./src
|
|
110
|
+
permissions:
|
|
111
|
+
read: true
|
|
112
|
+
write: true
|
|
113
|
+
execute: false
|
|
114
|
+
middlewares:
|
|
115
|
+
- type: static_context
|
|
116
|
+
file: ./ref.md
|
|
117
|
+
trigger:
|
|
118
|
+
type: manual
|
|
119
|
+
message: "Approve before running"
|
|
120
|
+
options: [approve, reject]
|
|
121
|
+
timeout: 5m
|
|
122
|
+
completion:
|
|
123
|
+
type: exit_code
|
|
124
|
+
expect: 0
|
|
92
125
|
- id: task-b
|
|
93
126
|
name: Follow up
|
|
94
127
|
prompt: "Continue the work"
|
|
95
128
|
continue_from: task-a
|
|
96
129
|
depends_on: [task-a]
|
|
130
|
+
- id: task-c
|
|
131
|
+
name: Templated task
|
|
132
|
+
use: "@tagma/template-lint"
|
|
133
|
+
with:
|
|
134
|
+
src: ./src
|
|
97
135
|
```
|
|
98
136
|
|
|
137
|
+
### Pipeline Fields
|
|
138
|
+
|
|
139
|
+
| Field | Type | Required | Description |
|
|
140
|
+
|---|---|---|---|
|
|
141
|
+
| `name` | `string` | Yes | Pipeline name, used in logs and run IDs |
|
|
142
|
+
| `driver` | `string` | No | Default driver for all tracks/tasks (inherited). Built-in: `claude-code` |
|
|
143
|
+
| `timeout` | `string` | No | Pipeline-level timeout. Format: `"30s"`, `"5m"`, `"2h"` |
|
|
144
|
+
| `plugins` | `string[]` | No | External plugin packages to load, e.g. `["@tagma/driver-codex"]` |
|
|
145
|
+
| `hooks` | `HooksConfig` | No | Shell commands to run at lifecycle events (see Hooks below) |
|
|
146
|
+
| `tracks` | `TrackConfig[]` | Yes | List of parallel execution tracks |
|
|
147
|
+
|
|
148
|
+
### Hooks Fields
|
|
149
|
+
|
|
150
|
+
Each hook value can be a single command string or an array of commands.
|
|
151
|
+
|
|
152
|
+
| Field | Type | Description |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `pipeline_start` | `string \| string[]` | Runs when the pipeline begins |
|
|
155
|
+
| `task_start` | `string \| string[]` | Runs before each task starts |
|
|
156
|
+
| `task_success` | `string \| string[]` | Runs after a task succeeds |
|
|
157
|
+
| `task_failure` | `string \| string[]` | Runs after a task fails |
|
|
158
|
+
| `pipeline_complete` | `string \| string[]` | Runs when the pipeline finishes successfully |
|
|
159
|
+
| `pipeline_error` | `string \| string[]` | Runs when the pipeline finishes with errors |
|
|
160
|
+
|
|
161
|
+
### Track Fields
|
|
162
|
+
|
|
163
|
+
| Field | Type | Required | Default | Description |
|
|
164
|
+
|---|---|---|---|---|
|
|
165
|
+
| `id` | `string` | Yes | — | Unique track identifier |
|
|
166
|
+
| `name` | `string` | Yes | — | Display name |
|
|
167
|
+
| `color` | `string` | No | — | Color hint for UI rendering (e.g. `"#3b82f6"`) |
|
|
168
|
+
| `driver` | `string` | No | Inherited from pipeline | Driver for all tasks in this track |
|
|
169
|
+
| `model_tier` | `string` | No | Inherited from pipeline | AI model tier: `high`, `medium`, `low` |
|
|
170
|
+
| `agent_profile` | `string` | No | — | Named agent configuration profile |
|
|
171
|
+
| `cwd` | `string` | No | Pipeline workDir | Working directory for tasks in this track (relative path) |
|
|
172
|
+
| `permissions` | `Permissions` | No | Inherited from pipeline | Default permissions for tasks (see Permissions) |
|
|
173
|
+
| `on_failure` | `OnFailure` | No | `skip_downstream` | Failure strategy: `skip_downstream`, `stop_all`, `ignore` |
|
|
174
|
+
| `middlewares` | `MiddlewareConfig[]` | No | — | Middlewares applied to all tasks (task-level overrides track-level) |
|
|
175
|
+
| `tasks` | `TaskConfig[]` | Yes | — | Ordered list of tasks in this track |
|
|
176
|
+
|
|
177
|
+
### Task Fields
|
|
178
|
+
|
|
179
|
+
| Field | Type | Required | Default | Description |
|
|
180
|
+
|---|---|---|---|---|
|
|
181
|
+
| `id` | `string` | Yes | — | Unique task identifier (unique within the pipeline) |
|
|
182
|
+
| `name` | `string` | No | — | Display name |
|
|
183
|
+
| `prompt` | `string` | No* | — | AI prompt to send to the driver. *Mutually exclusive with `command` |
|
|
184
|
+
| `command` | `string` | No* | — | Shell command to execute directly. *Mutually exclusive with `prompt` |
|
|
185
|
+
| `depends_on` | `string[]` | No | — | Task IDs that must complete before this task runs. Cross-track refs use `trackId.taskId` |
|
|
186
|
+
| `continue_from` | `string` | No | — | Task ID whose output/session to continue from (session handoff). Cross-track refs use `trackId.taskId` |
|
|
187
|
+
| `output` | `string` | No | — | File path to write task stdout to (relative to workDir) |
|
|
188
|
+
| `driver` | `string` | No | Inherited from track | Driver override for this task |
|
|
189
|
+
| `model_tier` | `string` | No | Inherited from track | Model tier override for this task |
|
|
190
|
+
| `agent_profile` | `string` | No | Inherited from track | Agent profile override |
|
|
191
|
+
| `cwd` | `string` | No | Inherited from track | Working directory override (relative path) |
|
|
192
|
+
| `timeout` | `string` | No | — | Task-level timeout. Format: `"30s"`, `"5m"`, `"2h"` |
|
|
193
|
+
| `permissions` | `Permissions` | No | Inherited from track | Permission override (see Permissions) |
|
|
194
|
+
| `middlewares` | `MiddlewareConfig[]` | No | Inherited from track | Middleware override. Set `[]` to disable inherited middlewares |
|
|
195
|
+
| `trigger` | `TriggerConfig` | No | — | Gate that must resolve before the task runs (see Triggers) |
|
|
196
|
+
| `completion` | `CompletionConfig` | No | — | Post-execution check to validate task output (see Completions) |
|
|
197
|
+
| `use` | `string` | No | — | Template package to expand (e.g. `"@tagma/template-lint"`). Mutually exclusive with `prompt`/`command` |
|
|
198
|
+
| `with` | `Record<string, unknown>` | No | — | Parameters passed to the template referenced by `use` |
|
|
199
|
+
|
|
200
|
+
### Permissions
|
|
201
|
+
|
|
202
|
+
| Field | Type | Default | Description |
|
|
203
|
+
|---|---|---|---|
|
|
204
|
+
| `read` | `boolean` | — | Allow the agent to read files |
|
|
205
|
+
| `write` | `boolean` | — | Allow the agent to write files |
|
|
206
|
+
| `execute` | `boolean` | — | Allow the agent to execute commands |
|
|
207
|
+
|
|
208
|
+
### Inheritance
|
|
209
|
+
|
|
210
|
+
Fields are inherited top-down: **pipeline → track → task**. A value set at a lower level overrides the inherited value.
|
|
211
|
+
|
|
212
|
+
Inherited fields: `driver`, `model_tier`, `permissions`, `cwd`, `middlewares`.
|
|
213
|
+
|
|
214
|
+
Track-level `middlewares` apply to all tasks in the track. Setting task-level `middlewares` **replaces** (not appends) the track-level list. Use `middlewares: []` to disable all inherited middlewares for a task.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### Built-in Triggers
|
|
219
|
+
|
|
220
|
+
#### `manual` — Human approval gate
|
|
221
|
+
|
|
222
|
+
| Field | Type | Required | Default | Description |
|
|
223
|
+
|---|---|---|---|---|
|
|
224
|
+
| `type` | `"manual"` | Yes | — | Trigger type |
|
|
225
|
+
| `message` | `string` | No | `"Manual confirmation required for task \"{taskId}\""` | Message shown to the approver |
|
|
226
|
+
| `options` | `string[]` | No | — | Choice options (e.g. `[approve, reject]`) |
|
|
227
|
+
| `timeout` | `string` | No | — | How long to wait for a decision before timing out |
|
|
228
|
+
| `metadata` | `object` | No | — | Arbitrary metadata passed to the approval gateway |
|
|
229
|
+
|
|
230
|
+
#### `file` — File watcher gate
|
|
231
|
+
|
|
232
|
+
| Field | Type | Required | Default | Description |
|
|
233
|
+
|---|---|---|---|---|
|
|
234
|
+
| `type` | `"file"` | Yes | — | Trigger type |
|
|
235
|
+
| `path` | `string` | Yes | — | File path to watch (relative to workDir) |
|
|
236
|
+
| `timeout` | `string` | No | — | How long to wait for the file to appear/change |
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### Built-in Completions
|
|
241
|
+
|
|
242
|
+
#### `exit_code` — Exit code check
|
|
243
|
+
|
|
244
|
+
| Field | Type | Required | Default | Description |
|
|
245
|
+
|---|---|---|---|---|
|
|
246
|
+
| `type` | `"exit_code"` | Yes | — | Completion type |
|
|
247
|
+
| `expect` | `number \| number[]` | No | `0` | Expected exit code(s). Pass an array for multiple acceptable codes |
|
|
248
|
+
|
|
249
|
+
#### `file_exists` — File existence check
|
|
250
|
+
|
|
251
|
+
| Field | Type | Required | Default | Description |
|
|
252
|
+
|---|---|---|---|---|
|
|
253
|
+
| `type` | `"file_exists"` | Yes | — | Completion type |
|
|
254
|
+
| `path` | `string` | Yes | — | File or directory path to check (relative to workDir) |
|
|
255
|
+
| `kind` | `"file" \| "dir" \| "any"` | No | `"any"` | Entity type constraint |
|
|
256
|
+
| `min_size` | `number` | No | — | Minimum file size in bytes (files only) |
|
|
257
|
+
|
|
258
|
+
#### `output_check` — Command-based output validation
|
|
259
|
+
|
|
260
|
+
| Field | Type | Required | Default | Description |
|
|
261
|
+
|---|---|---|---|---|
|
|
262
|
+
| `type` | `"output_check"` | Yes | — | Completion type |
|
|
263
|
+
| `check` | `string` | Yes | — | Shell command to run. Task stdout is piped to its stdin; exits 0 = pass |
|
|
264
|
+
| `timeout` | `string` | No | `"30s"` | Max time to wait for the check command |
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### Built-in Middlewares
|
|
269
|
+
|
|
270
|
+
#### `static_context` — Prepend file content to prompt
|
|
271
|
+
|
|
272
|
+
| Field | Type | Required | Default | Description |
|
|
273
|
+
|---|---|---|---|---|
|
|
274
|
+
| `type` | `"static_context"` | Yes | — | Middleware type |
|
|
275
|
+
| `file` | `string` | Yes | — | Path to the context file (relative to workDir) |
|
|
276
|
+
| `label` | `string` | No | `"Reference: {filename}"` | Label for the injected context section |
|
|
277
|
+
|
|
99
278
|
## API
|
|
100
279
|
|
|
101
280
|
### `bootstrapBuiltins()`
|
|
@@ -150,6 +329,32 @@ Properties:
|
|
|
150
329
|
|
|
151
330
|
Dynamically loads and registers external plugin packages.
|
|
152
331
|
|
|
332
|
+
### `registerPlugin(category, type, handler): void`
|
|
333
|
+
|
|
334
|
+
Registers a plugin handler manually. Idempotent — duplicate registrations are silently ignored.
|
|
335
|
+
|
|
336
|
+
### `getHandler(category, type): PluginType`
|
|
337
|
+
|
|
338
|
+
Retrieves a registered plugin handler. Throws if the plugin is not registered.
|
|
339
|
+
|
|
340
|
+
### `hasHandler(category, type): boolean`
|
|
341
|
+
|
|
342
|
+
Returns `true` if a handler is registered for the given category and type.
|
|
343
|
+
|
|
344
|
+
### `listRegistered(category): string[]`
|
|
345
|
+
|
|
346
|
+
Lists all registered handler type names for a plugin category (`'drivers'`, `'triggers'`, `'completions'`, `'middlewares'`).
|
|
347
|
+
|
|
348
|
+
### `resolveConfig(raw: RawPipelineConfig, workDir: string): PipelineConfig`
|
|
349
|
+
|
|
350
|
+
Resolves a raw pipeline config into a fully resolved `PipelineConfig` — applies inheritance (pipeline → track → task) for driver, model_tier, permissions, and cwd. Validates and resolves all file paths against `workDir`.
|
|
351
|
+
|
|
352
|
+
Use `loadPipeline` for the common parse-and-resolve flow. Use `resolveConfig` directly when you need to manipulate the raw config between parsing and resolution.
|
|
353
|
+
|
|
354
|
+
### `expandTemplates(tasks, instancePrefix): Promise<RawTaskConfig[]>`
|
|
355
|
+
|
|
356
|
+
Expands `use:` template references in a task list. Loads template packages (`@tagma/template-*`), resolves parameters, and namespaces task IDs and dependencies. Called internally by `loadPipeline`.
|
|
357
|
+
|
|
153
358
|
### `attachStdinApprovalAdapter(gateway): StdinApprovalAdapter`
|
|
154
359
|
|
|
155
360
|
Attaches an interactive stdin-based approval handler.
|
|
@@ -249,6 +454,40 @@ const { nodes, edges } = buildRawDag(draftConfig);
|
|
|
249
454
|
|
|
250
455
|
Use `buildDag` instead when you have a fully resolved `PipelineConfig` and need topological sort order.
|
|
251
456
|
|
|
457
|
+
### `Logger`
|
|
458
|
+
|
|
459
|
+
Dual-channel logger — console + file. Creates a per-run log file at `<workDir>/.tagma/logs/<runId>/pipeline.log`.
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
const logger = new Logger(workDir, runId);
|
|
463
|
+
logger.info('[track]', 'message'); // console + file
|
|
464
|
+
logger.warn('[track]', 'message'); // console + file
|
|
465
|
+
logger.error('[track]', 'message'); // console + file
|
|
466
|
+
logger.debug('[track]', 'message'); // file only
|
|
467
|
+
logger.section('Title'); // file only — visual separator
|
|
468
|
+
logger.quiet(bulkText); // file only — bulk payload
|
|
469
|
+
logger.path; // log file path
|
|
470
|
+
logger.dir; // run artifact directory
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### `tailLines(text: string, n: number): string`
|
|
474
|
+
|
|
475
|
+
Returns the last `n` non-empty lines of `text`, joined with newlines.
|
|
476
|
+
|
|
477
|
+
### `clip(text: string, maxBytes?: number): string`
|
|
478
|
+
|
|
479
|
+
Truncates `text` to at most `maxBytes` UTF-8 bytes (default 16 KB), appending a `…[truncated N bytes]` marker when truncation occurs. Multi-byte characters (CJK, emoji) are counted correctly.
|
|
480
|
+
|
|
481
|
+
### Utilities
|
|
482
|
+
|
|
483
|
+
| Function | Description |
|
|
484
|
+
|---|---|
|
|
485
|
+
| `parseDuration(input)` | Parses `"30s"`, `"5m"`, `"2h"` → milliseconds |
|
|
486
|
+
| `validatePath(filePath, projectRoot)` | Resolves path, throws if it escapes project root |
|
|
487
|
+
| `generateRunId()` | Generates a unique run ID (`run_<ts>_<seq>_<rand>`) |
|
|
488
|
+
| `nowISO()` | Returns `new Date().toISOString()` |
|
|
489
|
+
| `truncateForName(text, maxLen?)` | Truncates first line to `maxLen` (default 40) for display |
|
|
490
|
+
|
|
252
491
|
## Related Packages
|
|
253
492
|
|
|
254
493
|
| Package | Description |
|
package/package.json
CHANGED
|
@@ -151,24 +151,28 @@ export const ClaudeCodeDriver: DriverPlugin = {
|
|
|
151
151
|
const tools = resolveTools(permissions);
|
|
152
152
|
const permissionMode = resolvePermissionMode(permissions);
|
|
153
153
|
|
|
154
|
+
// Pass the prompt via stdin instead of as a -p argument value. On Windows,
|
|
155
|
+
// multi-line strings in CLI arguments break cmd.exe argument parsing when
|
|
156
|
+
// the executable is a .cmd wrapper — newlines cause all subsequent flags
|
|
157
|
+
// (--output-format, --model, etc.) to be silently dropped.
|
|
158
|
+
const stdin = task.prompt!;
|
|
159
|
+
|
|
154
160
|
const args: string[] = [
|
|
155
161
|
'claude',
|
|
156
|
-
'-p',
|
|
162
|
+
'-p', // no value — prompt is piped via stdin
|
|
157
163
|
'--model', model,
|
|
158
164
|
'--allowedTools', tools,
|
|
159
165
|
'--permission-mode', permissionMode,
|
|
160
166
|
'--output-format', 'json',
|
|
161
|
-
|
|
167
|
+
// NOTE: do NOT use --verbose here. It changes stdout from a single JSON
|
|
168
|
+
// result object to a JSON event-stream array, breaking parseResult's
|
|
169
|
+
// session_id extraction (needed for continue_from) and normalizedOutput.
|
|
170
|
+
// The engine already captures stdout/stderr for pipeline logs.
|
|
162
171
|
// Pin to project+local settings only; don't inherit arbitrary user-level
|
|
163
172
|
// config (hooks, MCP servers, etc.) into pipeline automation.
|
|
164
173
|
'--setting-sources', 'project,local',
|
|
165
174
|
];
|
|
166
175
|
|
|
167
|
-
const profile = task.agent_profile ?? track.agent_profile;
|
|
168
|
-
if (profile) {
|
|
169
|
-
args.push('--append-system-prompt', profile);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
176
|
// If the task runs in a subdirectory of the project, grant read/edit
|
|
173
177
|
// access to the project root via --add-dir so Claude can still see
|
|
174
178
|
// shared files (configs, types, etc.) outside task.cwd.
|
|
@@ -185,12 +189,29 @@ export const ClaudeCodeDriver: DriverPlugin = {
|
|
|
185
189
|
}
|
|
186
190
|
}
|
|
187
191
|
|
|
188
|
-
|
|
192
|
+
// --append-system-prompt MUST be last: its value may contain newlines,
|
|
193
|
+
// and on Windows cmd.exe can silently drop any flags that follow a
|
|
194
|
+
// newline-containing argument.
|
|
195
|
+
const profile = task.agent_profile ?? track.agent_profile;
|
|
196
|
+
if (profile) {
|
|
197
|
+
args.push('--append-system-prompt', profile);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { args, cwd: effectiveCwd, env: resolveGitBashEnv(), stdin };
|
|
189
201
|
},
|
|
190
202
|
|
|
191
203
|
parseResult(stdout: string): DriverResultMeta {
|
|
192
204
|
try {
|
|
193
|
-
|
|
205
|
+
let json = JSON.parse(stdout);
|
|
206
|
+
|
|
207
|
+
// --verbose produces a JSON array of events; extract the final "result"
|
|
208
|
+
// event so session_id and normalizedOutput are correctly populated.
|
|
209
|
+
if (Array.isArray(json)) {
|
|
210
|
+
const resultEvent = json.findLast((e: Record<string, unknown>) => e.type === 'result');
|
|
211
|
+
if (!resultEvent) return { normalizedOutput: stdout };
|
|
212
|
+
json = resultEvent;
|
|
213
|
+
}
|
|
214
|
+
|
|
194
215
|
// Extract canonical text: strip JSON envelope so downstream drivers
|
|
195
216
|
// get the actual AI response, not metadata
|
|
196
217
|
const normalizedOutput = json.result ?? json.text ?? json.content ?? stdout;
|
package/src/runner.ts
CHANGED
|
@@ -6,6 +6,24 @@ import { shellArgs } from './utils';
|
|
|
6
6
|
// Delay before escalating SIGTERM to SIGKILL when killing a timed-out process.
|
|
7
7
|
const SIGKILL_DELAY_MS = 3_000;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* On Windows, proc.kill('SIGTERM') / proc.kill('SIGKILL') only terminate the
|
|
11
|
+
* direct child process. When the child is a .cmd/.bat wrapper (e.g. claude.cmd),
|
|
12
|
+
* cmd.exe spawns the real process as a grandchild — proc.kill misses it entirely.
|
|
13
|
+
* `taskkill /F /T /PID` kills the entire process tree rooted at the given PID.
|
|
14
|
+
*/
|
|
15
|
+
function killProcessTree(pid: number): void {
|
|
16
|
+
if (process.platform !== 'win32') return;
|
|
17
|
+
try {
|
|
18
|
+
Bun.spawnSync(['taskkill', '/F', '/T', '/PID', String(pid)], {
|
|
19
|
+
stdout: 'ignore',
|
|
20
|
+
stderr: 'ignore',
|
|
21
|
+
});
|
|
22
|
+
} catch {
|
|
23
|
+
/* best-effort — process may have already exited */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
9
27
|
export interface RunOptions {
|
|
10
28
|
readonly timeoutMs?: number;
|
|
11
29
|
readonly signal?: AbortSignal; // pipeline-level abort
|
|
@@ -128,15 +146,22 @@ export async function runSpawn(
|
|
|
128
146
|
const killGracefully = () => {
|
|
129
147
|
if (killedByUs) return;
|
|
130
148
|
killedByUs = true;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
|
|
150
|
+
if (process.platform === 'win32') {
|
|
151
|
+
// On Windows, kill the entire process tree via taskkill. This handles
|
|
152
|
+
// .cmd wrappers and nested child processes that proc.kill() misses.
|
|
153
|
+
killProcessTree(proc.pid);
|
|
154
|
+
} else {
|
|
155
|
+
proc.kill('SIGTERM');
|
|
156
|
+
// If the child ignores SIGTERM, escalate to SIGKILL after 3 s.
|
|
157
|
+
forceTimer = setTimeout(() => {
|
|
158
|
+
try {
|
|
159
|
+
proc.kill('SIGKILL');
|
|
160
|
+
} catch {
|
|
161
|
+
/* already exited */
|
|
162
|
+
}
|
|
163
|
+
}, SIGKILL_DELAY_MS);
|
|
164
|
+
}
|
|
140
165
|
};
|
|
141
166
|
|
|
142
167
|
if (timeoutMs && timeoutMs > 0) {
|