@llblab/pi-actors 0.19.10 → 0.20.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 (55) hide show
  1. package/AGENTS.md +1 -1
  2. package/BACKLOG.md +40 -1
  3. package/CHANGELOG.md +15 -0
  4. package/dist/lib/actor-inspector-tui.d.ts +55 -0
  5. package/dist/lib/actor-inspector-tui.js +559 -0
  6. package/dist/lib/actor-messages.d.ts +25 -0
  7. package/dist/lib/actor-messages.js +122 -0
  8. package/dist/lib/actor-recipe-context.d.ts +14 -0
  9. package/dist/lib/actor-recipe-context.js +79 -0
  10. package/dist/lib/actor-rooms.d.ts +81 -0
  11. package/dist/lib/actor-rooms.js +468 -0
  12. package/dist/lib/async-runs.d.ts +101 -0
  13. package/dist/lib/async-runs.js +612 -0
  14. package/dist/lib/command-templates.d.ts +70 -0
  15. package/dist/lib/command-templates.js +592 -0
  16. package/dist/lib/config.d.ts +34 -0
  17. package/dist/lib/config.js +226 -0
  18. package/dist/lib/execution.d.ts +63 -0
  19. package/dist/lib/execution.js +450 -0
  20. package/dist/lib/file-state.d.ts +6 -0
  21. package/dist/lib/file-state.js +25 -0
  22. package/dist/lib/identity.d.ts +9 -0
  23. package/dist/lib/identity.js +27 -0
  24. package/dist/lib/observability.d.ts +86 -0
  25. package/dist/lib/observability.js +534 -0
  26. package/dist/lib/output.d.ts +25 -0
  27. package/dist/lib/output.js +89 -0
  28. package/dist/lib/paths.d.ts +11 -0
  29. package/dist/lib/paths.js +28 -0
  30. package/dist/lib/prompts.d.ts +23 -0
  31. package/dist/lib/prompts.js +50 -0
  32. package/dist/lib/recipe-discovery.d.ts +50 -0
  33. package/dist/lib/recipe-discovery.js +317 -0
  34. package/dist/lib/recipe-migration.d.ts +21 -0
  35. package/dist/lib/recipe-migration.js +90 -0
  36. package/dist/lib/recipe-references.d.ts +67 -0
  37. package/dist/lib/recipe-references.js +542 -0
  38. package/dist/lib/recipe-usage.d.ts +6 -0
  39. package/dist/lib/recipe-usage.js +57 -0
  40. package/dist/lib/registry.d.ts +47 -0
  41. package/dist/lib/registry.js +222 -0
  42. package/dist/lib/runtime.d.ts +36 -0
  43. package/dist/lib/runtime.js +126 -0
  44. package/dist/lib/schema.d.ts +48 -0
  45. package/dist/lib/schema.js +355 -0
  46. package/dist/lib/temp.d.ts +10 -0
  47. package/dist/lib/temp.js +90 -0
  48. package/dist/lib/tools.d.ts +39 -0
  49. package/dist/lib/tools.js +982 -0
  50. package/lib/async-runs.ts +20 -4
  51. package/package.json +6 -3
  52. package/scripts/async-runner.mjs +26 -6
  53. package/scripts/validate-recipe.mjs +19 -2
  54. package/skills/actors/SKILL.md +1 -1
  55. package/skills/swarm/SKILL.md +1 -1
@@ -0,0 +1,592 @@
1
+ /**
2
+ * Command-template standard helpers
3
+ * Zones: shared utils, local process execution, automation standard
4
+ * Owns shell-free command-template splitting, placeholder defaults, composition expansion, executable path expansion, and direct execution
5
+ */
6
+ import { spawn } from "node:child_process";
7
+ import { homedir } from "node:os";
8
+ import { isAbsolute, resolve } from "node:path";
9
+ function normalizeCommandTemplateArgs(value) {
10
+ if (!Array.isArray(value))
11
+ return [];
12
+ return value.map(String).map((item) => item.trim());
13
+ }
14
+ export function normalizeCommandTemplateConfig(config) {
15
+ return typeof config === "string" ? { template: config } : config;
16
+ }
17
+ function normalizeRecoverConfig(config) {
18
+ if (config === undefined)
19
+ return undefined;
20
+ return Array.isArray(config) ? { template: config } : config;
21
+ }
22
+ function normalizeCommandTemplateDefaults(defaults) {
23
+ if (!defaults)
24
+ return undefined;
25
+ const normalized = {};
26
+ for (const [key, value] of Object.entries(defaults)) {
27
+ normalized[key] = Array.isArray(value)
28
+ ? value
29
+ : value === undefined || value === null
30
+ ? ""
31
+ : String(value);
32
+ }
33
+ return normalized;
34
+ }
35
+ export function resolveInheritedDefaultReferences(ownDefaults, inheritedDefaults, runtimeValues = {}) {
36
+ if (!ownDefaults || !inheritedDefaults)
37
+ return ownDefaults;
38
+ const resolved = { ...ownDefaults };
39
+ for (const [key, value] of Object.entries(ownDefaults)) {
40
+ if (typeof value !== "string")
41
+ continue;
42
+ const exact = /^\{([A-Za-z_][A-Za-z0-9_-]*)\}$/.exec(value);
43
+ if (!exact ||
44
+ Object.hasOwn(runtimeValues, exact[1]) ||
45
+ !Object.hasOwn(inheritedDefaults, exact[1]))
46
+ continue;
47
+ resolved[key] = inheritedDefaults[exact[1]];
48
+ }
49
+ return resolved;
50
+ }
51
+ export function resolveCommandTemplateRepeat(value, values = {}) {
52
+ if (value === undefined)
53
+ return undefined;
54
+ if (typeof value === "number") {
55
+ if (!Number.isInteger(value) || value < 1)
56
+ throw new Error("Command template repeat must be a positive integer.");
57
+ return value;
58
+ }
59
+ const trimmed = value.trim();
60
+ if (/^\d+$/.test(trimmed))
61
+ return Number(trimmed);
62
+ const lengthMatch = trimmed.match(/^\{?([A-Za-z_][A-Za-z0-9_-]*)\.length\}?$/);
63
+ if (lengthMatch) {
64
+ const source = values[lengthMatch[1]];
65
+ if (Array.isArray(source))
66
+ return source.length;
67
+ if (source === undefined)
68
+ return undefined;
69
+ }
70
+ throw new Error("Command template repeat must be a positive integer or {array.length}.");
71
+ }
72
+ function getExecutableName(command) {
73
+ if (!command)
74
+ return "";
75
+ return command.split(/[\\/]/).pop()?.toLowerCase() ?? "";
76
+ }
77
+ function hasAnyFlag(args, flags) {
78
+ return args.some((arg) => flags.includes(arg));
79
+ }
80
+ function hasRiskyPathArg(args) {
81
+ return args.some((arg) => arg === "/" ||
82
+ arg === "~" ||
83
+ arg === "./" ||
84
+ arg === "../" ||
85
+ arg.includes("{") ||
86
+ arg.startsWith("~/") ||
87
+ arg.startsWith("/"));
88
+ }
89
+ function getLeafCommandTemplateWarnings(config) {
90
+ const parts = splitCommandTemplate(config.template);
91
+ const command = getExecutableName(parts[0]);
92
+ const args = parts.slice(1);
93
+ const warnings = [];
94
+ if (["bash", "sh", "zsh", "fish"].includes(command)) {
95
+ const shellContent = hasAnyFlag(args, ["-c"])
96
+ ? "shell command strings"
97
+ : "shell scripts";
98
+ warnings.push(`${config.label ?? command}: invokes ${command}; ${shellContent} are trusted executable content and are not sandboxed by command-template argv splitting.`);
99
+ }
100
+ if (["node", "deno", "bun"].includes(command) &&
101
+ hasAnyFlag(args, ["-e", "--eval"])) {
102
+ warnings.push(`${config.label ?? command}: invokes ${command} eval mode; code strings are trusted executable content and are not sandboxed.`);
103
+ }
104
+ if (["python", "python3", "perl", "ruby"].includes(command) &&
105
+ hasAnyFlag(args, ["-c", "-e"])) {
106
+ warnings.push(`${config.label ?? command}: invokes ${command} code-eval mode; code strings are trusted executable content and are not sandboxed.`);
107
+ }
108
+ if (command === "rm" &&
109
+ (args.some((arg) => /^-[^-]*r/.test(arg) || /^-[^-]*f/.test(arg)) ||
110
+ hasRiskyPathArg(args))) {
111
+ warnings.push(`${config.label ?? command}: removes filesystem paths; verify placeholders and paths before running trusted destructive commands.`);
112
+ }
113
+ if (["mv", "cp", "rsync"].includes(command) && hasRiskyPathArg(args)) {
114
+ warnings.push(`${config.label ?? command}: mutates broad filesystem paths; verify placeholders and paths before running trusted commands.`);
115
+ }
116
+ return warnings;
117
+ }
118
+ function pad(value, width) {
119
+ return String(value).padStart(width, "0");
120
+ }
121
+ export function isCommandTemplateRepeatPlaceholder(name) {
122
+ return /^_{0,6}(?:index|prev|next|repeat)$/.test(name);
123
+ }
124
+ export function getCommandTemplateRepeatDefaults(index, repeat) {
125
+ const prev = (index - 1 + repeat) % repeat;
126
+ const next = (index + 1) % repeat;
127
+ const values = {
128
+ index: String(index),
129
+ next: String(next),
130
+ prev: String(prev),
131
+ repeat: String(repeat),
132
+ };
133
+ for (const name of ["index", "prev", "next", "repeat"]) {
134
+ const numeric = Number(values[name]);
135
+ for (let underscores = 1; underscores <= 6; underscores += 1) {
136
+ values[`${"_".repeat(underscores)}${name}`] = pad(numeric, underscores + 1);
137
+ }
138
+ }
139
+ return values;
140
+ }
141
+ function expandRepeatConfig(config, context) {
142
+ const repeat = resolveCommandTemplateRepeat(config.repeat, context.defaults ?? {});
143
+ if (repeat === undefined)
144
+ return undefined;
145
+ return Array.from({ length: repeat }, (_unused, index0) => {
146
+ const { repeat: _repeat, ...rest } = config;
147
+ return {
148
+ ...rest,
149
+ defaults: {
150
+ ...(context.defaults ?? {}),
151
+ ...(rest.defaults ?? {}),
152
+ ...getCommandTemplateRepeatDefaults(index0, repeat),
153
+ },
154
+ };
155
+ });
156
+ }
157
+ export function expandCommandTemplateConfigs(config, inherited = {}) {
158
+ const normalizedConfig = normalizeCommandTemplateConfig(config);
159
+ const inheritedDefaults = normalizeCommandTemplateDefaults(inherited.defaults);
160
+ const ownDefaults = resolveInheritedDefaultReferences(normalizeCommandTemplateDefaults(normalizedConfig.defaults), inheritedDefaults);
161
+ const context = {
162
+ ...(inherited.args !== undefined ? { args: inherited.args } : {}),
163
+ ...(inheritedDefaults ? { defaults: inheritedDefaults } : {}),
164
+ ...(normalizedConfig.args !== undefined
165
+ ? { args: normalizedConfig.args }
166
+ : {}),
167
+ ...(ownDefaults
168
+ ? { defaults: { ...(inheritedDefaults ?? {}), ...ownDefaults } }
169
+ : {}),
170
+ };
171
+ const repeated = expandRepeatConfig(normalizedConfig, context);
172
+ if (repeated) {
173
+ return repeated.flatMap((step) => expandCommandTemplateConfigs(step, context));
174
+ }
175
+ const recoverConfig = normalizeRecoverConfig(normalizedConfig.recover);
176
+ const recoverSteps = recoverConfig
177
+ ? expandCommandTemplateConfigs(recoverConfig, context)
178
+ : [];
179
+ if (Array.isArray(normalizedConfig.template)) {
180
+ return [
181
+ ...normalizedConfig.template.flatMap((step) => expandCommandTemplateConfigs(step, context)),
182
+ ...recoverSteps,
183
+ ];
184
+ }
185
+ if (typeof normalizedConfig.template !== "string")
186
+ return recoverSteps;
187
+ return [
188
+ {
189
+ ...normalizedConfig,
190
+ ...context,
191
+ template: normalizedConfig.template,
192
+ retry: normalizedConfig.retry,
193
+ },
194
+ ...recoverSteps,
195
+ ];
196
+ }
197
+ export function getCommandTemplateWarnings(config) {
198
+ return [
199
+ ...new Set(expandCommandTemplateConfigs(config).flatMap((leaf) => getLeafCommandTemplateWarnings(leaf))),
200
+ ];
201
+ }
202
+ function parseCommandTemplateArgToken(value) {
203
+ const separatorIndex = value.indexOf("=");
204
+ const rawName = separatorIndex === -1 ? value : value.slice(0, separatorIndex);
205
+ const colonIndex = rawName.indexOf(":");
206
+ return {
207
+ name: (colonIndex === -1 ? rawName : rawName.slice(0, colonIndex)).trim(),
208
+ ...(separatorIndex === -1
209
+ ? {}
210
+ : { defaultValue: value.slice(separatorIndex + 1).trim() }),
211
+ };
212
+ }
213
+ function parseCommandTemplatePlaceholderContent(content) {
214
+ const match = content.match(/^([A-Za-z_][A-Za-z0-9_-]*)(?::(?:string|path|int|number|bool|array|enum\([^)]*\)))?(?:=([^}]*))?$/);
215
+ if (!match)
216
+ return undefined;
217
+ return {
218
+ name: match[1],
219
+ ...(match[2] !== undefined ? { inlineDefault: match[2] } : {}),
220
+ };
221
+ }
222
+ export function getCommandTemplateDefaults(config) {
223
+ const normalizedConfig = config
224
+ ? normalizeCommandTemplateConfig(config)
225
+ : undefined;
226
+ const defaults = {};
227
+ for (const item of normalizeCommandTemplateArgs(normalizedConfig?.args)) {
228
+ if (!item)
229
+ continue;
230
+ const parsed = parseCommandTemplateArgToken(item);
231
+ if (!parsed.name || parsed.defaultValue === undefined)
232
+ continue;
233
+ defaults[parsed.name] = parsed.defaultValue;
234
+ }
235
+ for (const [key, value] of Object.entries(normalizedConfig?.defaults ?? {})) {
236
+ defaults[key] = value === undefined || value === null ? "" : String(value);
237
+ }
238
+ return defaults;
239
+ }
240
+ export function splitCommandTemplate(input) {
241
+ const words = [];
242
+ let current = "";
243
+ let quote;
244
+ let escaped = false;
245
+ let active = false;
246
+ for (const char of input) {
247
+ if (escaped) {
248
+ current += char;
249
+ escaped = false;
250
+ active = true;
251
+ continue;
252
+ }
253
+ if (char === "\\" && quote !== "'") {
254
+ escaped = true;
255
+ active = true;
256
+ continue;
257
+ }
258
+ if (quote) {
259
+ if (char === quote)
260
+ quote = undefined;
261
+ else
262
+ current += char;
263
+ active = true;
264
+ continue;
265
+ }
266
+ if (char === "'" || char === '"') {
267
+ quote = char;
268
+ active = true;
269
+ continue;
270
+ }
271
+ if (/\s/.test(char)) {
272
+ if (active)
273
+ words.push(current);
274
+ if (active)
275
+ current = "";
276
+ active = false;
277
+ continue;
278
+ }
279
+ current += char;
280
+ active = true;
281
+ }
282
+ if (escaped)
283
+ current += "\\";
284
+ if (active || current)
285
+ words.push(current);
286
+ return words;
287
+ }
288
+ export function expandCommandTemplateExecutable(command, cwd) {
289
+ if (command === "~")
290
+ return homedir();
291
+ if (command.startsWith("~/"))
292
+ return resolve(homedir(), command.slice(2));
293
+ if (command.includes("/") && !isAbsolute(command))
294
+ return resolve(cwd, command);
295
+ return command;
296
+ }
297
+ function evaluateCommandTemplateExpression(expression, values) {
298
+ let index = 0;
299
+ const source = expression.replace(/\s+/g, "");
300
+ function peek() {
301
+ return source[index];
302
+ }
303
+ function consume(char) {
304
+ if (peek() !== char)
305
+ return false;
306
+ index += 1;
307
+ return true;
308
+ }
309
+ function parsePrimary() {
310
+ if (consume("(")) {
311
+ const value = parseExpression();
312
+ if (!consume(")"))
313
+ throw new Error(`Invalid command template expression: ${expression}`);
314
+ return value;
315
+ }
316
+ const numberMatch = source.slice(index).match(/^\d+/);
317
+ if (numberMatch) {
318
+ index += numberMatch[0].length;
319
+ return Number(numberMatch[0]);
320
+ }
321
+ const nameMatch = source.slice(index).match(/^[A-Za-z_][A-Za-z0-9_-]*/);
322
+ if (nameMatch) {
323
+ index += nameMatch[0].length;
324
+ const value = values[nameMatch[0]];
325
+ if (value === undefined || !/^-?\d+$/.test(String(value)))
326
+ throw new Error(`Invalid command template expression variable: ${nameMatch[0]}`);
327
+ return Number(value);
328
+ }
329
+ throw new Error(`Invalid command template expression: ${expression}`);
330
+ }
331
+ function parseTerm() {
332
+ let value = parsePrimary();
333
+ while (true) {
334
+ if (consume("*"))
335
+ value *= parsePrimary();
336
+ else if (consume("/"))
337
+ value = Math.trunc(value / parsePrimary());
338
+ else if (consume("%"))
339
+ value %= parsePrimary();
340
+ else
341
+ return value;
342
+ }
343
+ }
344
+ function parseExpression() {
345
+ let value = parseTerm();
346
+ while (true) {
347
+ if (consume("+"))
348
+ value += parseTerm();
349
+ else if (consume("-"))
350
+ value -= parseTerm();
351
+ else
352
+ return value;
353
+ }
354
+ }
355
+ const value = parseExpression();
356
+ if (index !== source.length)
357
+ throw new Error(`Invalid command template expression: ${expression}`);
358
+ return value;
359
+ }
360
+ function substituteCommandTemplateExpression(content, values) {
361
+ const padded = content.match(/^(_{1,6})\((.+)\)$/);
362
+ if (padded) {
363
+ return pad(evaluateCommandTemplateExpression(padded[2], values), padded[1].length + 1);
364
+ }
365
+ if (!/[()+\-*\/%]/.test(content))
366
+ return undefined;
367
+ return String(evaluateCommandTemplateExpression(content, values));
368
+ }
369
+ function shouldResolveEmbeddedCommandTemplateToken(token, values) {
370
+ const matches = [...token.matchAll(/\{([^{}]+)\}/g)];
371
+ if (matches.length === 0)
372
+ return false;
373
+ return matches.every((match) => {
374
+ const content = match[1];
375
+ if (resolveCommandTemplateNullish(content, values) !== undefined)
376
+ return true;
377
+ if (resolveCommandTemplateTernary(content, values) !== undefined)
378
+ return true;
379
+ const indexed = content.match(/^([A-Za-z_][A-Za-z0-9_-]*)\[([A-Za-z_][A-Za-z0-9_-]*|\d+)\]$/);
380
+ if (indexed)
381
+ return Object.hasOwn(values, indexed[1]);
382
+ const simple = parseCommandTemplatePlaceholderContent(content);
383
+ if (simple)
384
+ return (Object.hasOwn(values, simple.name) || simple.inlineDefault !== undefined);
385
+ try {
386
+ return substituteCommandTemplateExpression(content, values) !== undefined;
387
+ }
388
+ catch {
389
+ return false;
390
+ }
391
+ });
392
+ }
393
+ function isFalsyCommandTemplateValue(value) {
394
+ if (value === undefined || value === null || value === false)
395
+ return true;
396
+ const normalized = String(value).trim().toLowerCase();
397
+ return (normalized === "" ||
398
+ normalized === "0" ||
399
+ normalized === "false" ||
400
+ normalized === "no");
401
+ }
402
+ function resolveCommandTemplateCondition(condition, values) {
403
+ const trimmed = condition.trim();
404
+ const negated = trimmed.startsWith("!");
405
+ const name = negated ? trimmed.slice(1).trim() : trimmed;
406
+ const value = /^[A-Za-z_][A-Za-z0-9_-]*$/.test(name)
407
+ ? values[name]
408
+ : undefined;
409
+ return negated ? isFalsyCommandTemplateValue(value) : value;
410
+ }
411
+ export function shouldRunCommandTemplateNode(value, values) {
412
+ if (value === undefined)
413
+ return true;
414
+ if (typeof value === "boolean")
415
+ return value;
416
+ const trimmed = value.trim();
417
+ if (!trimmed)
418
+ return false;
419
+ const exact = /^\{([^{}]+)\}$/.exec(trimmed);
420
+ const resolved = exact
421
+ ? resolveCommandTemplateValue(exact[1], values, "command template when")
422
+ : resolveCommandTemplateCondition(trimmed, values);
423
+ return !isFalsyCommandTemplateValue(resolved);
424
+ }
425
+ function resolveCommandTemplateNullish(content, values) {
426
+ const coalescing = content.match(/^([A-Za-z_][A-Za-z0-9_-]*)\?\?(.*)$/);
427
+ if (!coalescing)
428
+ return undefined;
429
+ const value = values[coalescing[1]];
430
+ return isFalsyCommandTemplateValue(value) ? coalescing[2] : String(value);
431
+ }
432
+ function resolveCommandTemplateTernary(content, values) {
433
+ const ternary = content.match(/^([^?:]+)\?([^:]*):(.*)$/);
434
+ if (!ternary)
435
+ return undefined;
436
+ const condition = resolveCommandTemplateCondition(ternary[1], values);
437
+ return isFalsyCommandTemplateValue(condition) ? ternary[3] : ternary[2];
438
+ }
439
+ function resolveCommandTemplateValue(content, values, missingLabel, depth = 0) {
440
+ if (depth > 5)
441
+ throw new Error(`Command template value recursion exceeded: ${content}`);
442
+ const nullish = resolveCommandTemplateNullish(content, values);
443
+ if (nullish !== undefined)
444
+ return nullish;
445
+ const ternary = resolveCommandTemplateTernary(content, values);
446
+ if (ternary !== undefined)
447
+ return ternary;
448
+ const indexed = content.match(/^([A-Za-z_][A-Za-z0-9_-]*)\[([A-Za-z_][A-Za-z0-9_-]*|\d+)\]$/);
449
+ if (indexed) {
450
+ const source = values[indexed[1]];
451
+ const indexValue = /^\d+$/.test(indexed[2])
452
+ ? indexed[2]
453
+ : values[indexed[2]];
454
+ const index = Number(indexValue);
455
+ if (!Array.isArray(source) ||
456
+ !Number.isInteger(index) ||
457
+ index < 0 ||
458
+ index >= source.length) {
459
+ throw new Error(`Missing ${missingLabel} value: ${content}`);
460
+ }
461
+ return String(source[index] ?? "");
462
+ }
463
+ const simple = parseCommandTemplatePlaceholderContent(content);
464
+ if (simple) {
465
+ if (Object.hasOwn(values, simple.name)) {
466
+ const raw = values[simple.name] ?? "";
467
+ if (typeof raw === "string" &&
468
+ shouldResolveEmbeddedCommandTemplateToken(raw, values)) {
469
+ return substituteCommandTemplateToken(raw, values, missingLabel, depth + 1);
470
+ }
471
+ return Array.isArray(raw) ? JSON.stringify(raw) : String(raw);
472
+ }
473
+ if (simple.inlineDefault !== undefined)
474
+ return simple.inlineDefault;
475
+ }
476
+ const expression = substituteCommandTemplateExpression(content, values);
477
+ if (expression !== undefined)
478
+ return expression;
479
+ return undefined;
480
+ }
481
+ export function substituteCommandTemplateToken(token, values, missingLabel = "command template", depth = 0) {
482
+ return token.replace(/\{([^{}]+)\}/g, (_match, content) => {
483
+ const resolved = resolveCommandTemplateValue(content, values, missingLabel, depth);
484
+ if (resolved !== undefined)
485
+ return resolved;
486
+ throw new Error(`Missing ${missingLabel} value: ${content}`);
487
+ });
488
+ }
489
+ export async function execCommandTemplate(command, args, options = {}) {
490
+ const maxAttempts = options.retry ?? 1;
491
+ let lastResult = {
492
+ stdout: "",
493
+ stderr: "",
494
+ code: 1,
495
+ killed: false,
496
+ };
497
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
498
+ const result = await execCommandTemplateOnce(command, args, options);
499
+ if (result.code === 0)
500
+ return result;
501
+ lastResult = result;
502
+ }
503
+ return lastResult;
504
+ }
505
+ function execCommandTemplateOnce(command, args, options = {}) {
506
+ return new Promise((resolve) => {
507
+ const proc = spawn(command, args, {
508
+ cwd: options.cwd,
509
+ shell: false,
510
+ stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
511
+ });
512
+ let stdout = "";
513
+ let stderr = "";
514
+ let killed = false;
515
+ let settled = false;
516
+ let timeoutId;
517
+ let killTimeoutId;
518
+ const killProcess = () => {
519
+ if (killed)
520
+ return;
521
+ killed = true;
522
+ proc.kill("SIGTERM");
523
+ killTimeoutId = setTimeout(() => {
524
+ if (!settled)
525
+ proc.kill("SIGKILL");
526
+ }, options.killGrace ?? 5000);
527
+ };
528
+ const settle = (code) => {
529
+ if (settled)
530
+ return;
531
+ settled = true;
532
+ if (timeoutId)
533
+ clearTimeout(timeoutId);
534
+ if (killTimeoutId)
535
+ clearTimeout(killTimeoutId);
536
+ if (options.signal)
537
+ options.signal.removeEventListener("abort", killProcess);
538
+ resolve({ stdout, stderr, code, killed });
539
+ };
540
+ if (options.signal) {
541
+ if (options.signal.aborted)
542
+ killProcess();
543
+ else
544
+ options.signal.addEventListener("abort", killProcess, { once: true });
545
+ }
546
+ if (options.timeout !== undefined && options.timeout > 0)
547
+ timeoutId = setTimeout(killProcess, options.timeout);
548
+ proc.stdout?.on("data", (data) => {
549
+ stdout += data.toString();
550
+ });
551
+ proc.stderr?.on("data", (data) => {
552
+ stderr += data.toString();
553
+ });
554
+ proc.stdin?.on("error", () => { });
555
+ if (options.stdin !== undefined)
556
+ proc.stdin?.end(options.stdin);
557
+ proc.on("error", (error) => {
558
+ stderr += error instanceof Error ? error.message : String(error);
559
+ settle(1);
560
+ });
561
+ proc.on("close", (code) => {
562
+ settle(code ?? (killed ? 1 : 0));
563
+ });
564
+ });
565
+ }
566
+ export function buildCommandTemplateInvocation(config, values, cwd, options = {}) {
567
+ const normalizedConfig = normalizeCommandTemplateConfig(config);
568
+ if (Array.isArray(normalizedConfig.template)) {
569
+ throw new Error(options.emptyMessage ??
570
+ "Command template sequence cannot be executed as one command");
571
+ }
572
+ if (!normalizedConfig.template)
573
+ throw new Error(options.emptyMessage ?? "Command template is required");
574
+ if (typeof normalizedConfig.template !== "string") {
575
+ throw new Error(options.emptyMessage ??
576
+ "Command template object cannot be executed as one command");
577
+ }
578
+ const parts = splitCommandTemplate(normalizedConfig.template);
579
+ const commandPart = parts[0];
580
+ if (!commandPart)
581
+ throw new Error(options.emptyMessage ?? "Command template is empty");
582
+ const resolvedValues = {
583
+ ...getCommandTemplateDefaults(normalizedConfig),
584
+ ...values,
585
+ };
586
+ const command = expandCommandTemplateExecutable(substituteCommandTemplateToken(commandPart, resolvedValues, options.missingLabel), cwd);
587
+ const args = parts
588
+ .slice(1)
589
+ .map((part) => substituteCommandTemplateToken(part, resolvedValues, options.missingLabel))
590
+ .filter((part) => part !== "");
591
+ return { command, args };
592
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Persistent tool registry config helpers
3
+ * Zones: registry config, persistence, migration boundary
4
+ * Owns registered-tool config loading, normalization, unsupported-shape rejection, and serialization
5
+ */
6
+ import type { CommandTemplateValue } from "./command-templates.ts";
7
+ import * as RecipeReferences from "./recipe-references.ts";
8
+ import * as Schema from "./schema.ts";
9
+ export interface RegisteredTool {
10
+ name: string;
11
+ description: string;
12
+ args: string[];
13
+ defaults: Record<string, string>;
14
+ argTypes?: Record<string, Schema.ToolArgType>;
15
+ recipe?: RecipeReferences.TemplateRecipeConfig;
16
+ template?: CommandTemplateValue;
17
+ storedArgs?: string[];
18
+ storedDefaults?: Record<string, string>;
19
+ sourcePath?: string;
20
+ }
21
+ export interface LoadConfigResult {
22
+ tools: Map<string, RegisteredTool>;
23
+ warnings: string[];
24
+ changed: boolean;
25
+ }
26
+ export declare function serializeTools(source: Map<string, RegisteredTool>): Record<string, unknown>;
27
+ export declare function saveTools(path: string, source: Map<string, RegisteredTool>): string | undefined;
28
+ export declare function getStoredEntries(raw: unknown): Array<[string | undefined, unknown]>;
29
+ export declare function normalizeStoredTool(key: string | undefined, value: unknown, reservedToolNames: Set<string>): {
30
+ cfg?: RegisteredTool;
31
+ changed: boolean;
32
+ warning?: string;
33
+ };
34
+ export declare function loadToolConfig(path: string, reservedToolNames: Set<string>): LoadConfigResult;