@llblab/pi-telegram 0.52.2 → 0.53.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.
- package/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +61 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +249 -126
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +125 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +511 -300
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
package/lib/command-templates.ts
CHANGED
|
@@ -42,8 +42,15 @@ export type CommandTemplateConfig = string | CommandTemplateObjectConfig;
|
|
|
42
42
|
|
|
43
43
|
export interface CommandTemplateLeafConfig extends CommandTemplateObjectConfig {
|
|
44
44
|
template: string;
|
|
45
|
+
/** `when` guards of the composition nodes enclosing this leaf; every one must pass. */
|
|
46
|
+
inheritedWhen?: readonly (boolean | string)[];
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
type CommandTemplateExpansionContext = Pick<
|
|
50
|
+
CommandTemplateLeafConfig,
|
|
51
|
+
"args" | "defaults" | "inheritedWhen"
|
|
52
|
+
>;
|
|
53
|
+
|
|
47
54
|
export interface CommandTemplateInvocation {
|
|
48
55
|
command: string;
|
|
49
56
|
args: string[];
|
|
@@ -56,6 +63,8 @@ export interface CommandTemplateExecOptions {
|
|
|
56
63
|
stdin?: string;
|
|
57
64
|
killGrace?: number;
|
|
58
65
|
retry?: number;
|
|
66
|
+
/** Cleanup between a failed attempt and the next retry; a thrown error stops retries. */
|
|
67
|
+
recover?: () => Promise<void>;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
export interface CommandTemplateExecResult {
|
|
@@ -65,29 +74,6 @@ export interface CommandTemplateExecResult {
|
|
|
65
74
|
killed: boolean;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
export type CommandTemplateRiskLabel =
|
|
69
|
-
| "risk.shell"
|
|
70
|
-
| "risk.eval"
|
|
71
|
-
| "risk.broad_fs_write"
|
|
72
|
-
| "risk.destructive_fs"
|
|
73
|
-
| "risk.network"
|
|
74
|
-
| "risk.external_side_effect"
|
|
75
|
-
| "risk.long_running"
|
|
76
|
-
| "risk.platform_specific"
|
|
77
|
-
| "risk.secret_touching";
|
|
78
|
-
|
|
79
|
-
const COMMAND_TEMPLATE_RISK_LABEL_ORDER: CommandTemplateRiskLabel[] = [
|
|
80
|
-
"risk.shell",
|
|
81
|
-
"risk.eval",
|
|
82
|
-
"risk.destructive_fs",
|
|
83
|
-
"risk.broad_fs_write",
|
|
84
|
-
"risk.external_side_effect",
|
|
85
|
-
"risk.secret_touching",
|
|
86
|
-
"risk.network",
|
|
87
|
-
"risk.long_running",
|
|
88
|
-
"risk.platform_specific",
|
|
89
|
-
];
|
|
90
|
-
|
|
91
77
|
function normalizeCommandTemplateArgs(value: string[] | undefined): string[] {
|
|
92
78
|
if (!Array.isArray(value)) return [];
|
|
93
79
|
return value.map(String).map((item) => item.trim());
|
|
@@ -167,202 +153,6 @@ function resolveCommandTemplateRepeat(
|
|
|
167
153
|
);
|
|
168
154
|
}
|
|
169
155
|
|
|
170
|
-
function getExecutableName(command: string | undefined): string {
|
|
171
|
-
if (!command) return "";
|
|
172
|
-
return command.split(/[\\/]/).pop()?.toLowerCase() ?? "";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function matchesFlag(arg: string, flag: string): boolean {
|
|
176
|
-
if (arg === flag) return true;
|
|
177
|
-
if (/^-[A-Za-z]$/.test(flag) && /^-[A-Za-z]+$/.test(arg))
|
|
178
|
-
return arg.slice(1).includes(flag.slice(1));
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function hasAnyFlag(args: string[], flags: string[]): boolean {
|
|
183
|
-
return args.some((arg) => flags.some((flag) => matchesFlag(arg, flag)));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function hasRiskyPathArg(args: string[]): boolean {
|
|
187
|
-
return args.some(
|
|
188
|
-
(arg) =>
|
|
189
|
-
arg === "/" ||
|
|
190
|
-
arg === "~" ||
|
|
191
|
-
arg === "./" ||
|
|
192
|
-
arg === "../" ||
|
|
193
|
-
arg.includes("{") ||
|
|
194
|
-
arg.startsWith("~/") ||
|
|
195
|
-
arg.startsWith("/"),
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function sortRiskLabels(
|
|
200
|
-
labels: Iterable<CommandTemplateRiskLabel>,
|
|
201
|
-
): CommandTemplateRiskLabel[] {
|
|
202
|
-
const unique = new Set(labels);
|
|
203
|
-
return COMMAND_TEMPLATE_RISK_LABEL_ORDER.filter((label) => unique.has(label));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function hasAnyArg(args: string[], values: string[]): boolean {
|
|
207
|
-
return args.some((arg) => values.includes(arg.toLowerCase()));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function hasSecretTouchingText(parts: string[]): boolean {
|
|
211
|
-
return parts.some((part) =>
|
|
212
|
-
/(^|[{}._\-\s/])(?:secret|token|password|passwd|credential|api[_-]?key|private[_-]?key|\.env|ssh[_-]?key)(?:[{}._\-\s/]|$)/i.test(
|
|
213
|
-
part,
|
|
214
|
-
),
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function getLeafCommandTemplateRiskLabels(
|
|
219
|
-
config: CommandTemplateLeafConfig,
|
|
220
|
-
): CommandTemplateRiskLabel[] {
|
|
221
|
-
const parts = splitCommandTemplate(config.template);
|
|
222
|
-
const command = getExecutableName(parts[0]);
|
|
223
|
-
const args = parts.slice(1);
|
|
224
|
-
const labels = new Set<CommandTemplateRiskLabel>();
|
|
225
|
-
if (["bash", "sh", "zsh", "fish"].includes(command)) {
|
|
226
|
-
labels.add("risk.shell");
|
|
227
|
-
if (hasAnyFlag(args, ["-c"])) labels.add("risk.eval");
|
|
228
|
-
}
|
|
229
|
-
if (
|
|
230
|
-
["node", "deno", "bun"].includes(command) &&
|
|
231
|
-
hasAnyFlag(args, ["-e", "--eval"])
|
|
232
|
-
) {
|
|
233
|
-
labels.add("risk.eval");
|
|
234
|
-
}
|
|
235
|
-
if (
|
|
236
|
-
["python", "python3", "perl", "ruby"].includes(command) &&
|
|
237
|
-
hasAnyFlag(args, ["-c", "-e"])
|
|
238
|
-
) {
|
|
239
|
-
labels.add("risk.eval");
|
|
240
|
-
}
|
|
241
|
-
if (
|
|
242
|
-
command === "rm" &&
|
|
243
|
-
(args.some((arg) => /^-[^-]*r/.test(arg) || /^-[^-]*f/.test(arg)) ||
|
|
244
|
-
hasRiskyPathArg(args))
|
|
245
|
-
) {
|
|
246
|
-
labels.add("risk.destructive_fs");
|
|
247
|
-
}
|
|
248
|
-
if (["mv", "cp", "rsync"].includes(command) && hasRiskyPathArg(args)) {
|
|
249
|
-
labels.add("risk.broad_fs_write");
|
|
250
|
-
}
|
|
251
|
-
if (
|
|
252
|
-
[
|
|
253
|
-
"curl",
|
|
254
|
-
"wget",
|
|
255
|
-
"ssh",
|
|
256
|
-
"scp",
|
|
257
|
-
"sftp",
|
|
258
|
-
"rsync",
|
|
259
|
-
"nc",
|
|
260
|
-
"ncat",
|
|
261
|
-
"telnet",
|
|
262
|
-
"ftp",
|
|
263
|
-
].includes(command) ||
|
|
264
|
-
(command === "git" &&
|
|
265
|
-
hasAnyArg(args, ["clone", "fetch", "pull", "push", "ls-remote"])) ||
|
|
266
|
-
["npm", "pnpm", "yarn", "pip", "cargo"].includes(command)
|
|
267
|
-
) {
|
|
268
|
-
labels.add("risk.network");
|
|
269
|
-
}
|
|
270
|
-
if (
|
|
271
|
-
["gh", "glab", "hub", "kubectl", "terraform"].includes(command) ||
|
|
272
|
-
(command === "git" && hasAnyArg(args, ["push"])) ||
|
|
273
|
-
(["npm", "pnpm", "yarn"].includes(command) &&
|
|
274
|
-
hasAnyArg(args, ["publish", "login", "logout", "deprecate"]))
|
|
275
|
-
) {
|
|
276
|
-
labels.add("risk.external_side_effect");
|
|
277
|
-
}
|
|
278
|
-
if (
|
|
279
|
-
command === "sleep" ||
|
|
280
|
-
command === "watch" ||
|
|
281
|
-
(command === "tail" && hasAnyFlag(args, ["-f"])) ||
|
|
282
|
-
hasAnyArg(args, ["--watch", "--serve", "serve"])
|
|
283
|
-
) {
|
|
284
|
-
labels.add("risk.long_running");
|
|
285
|
-
}
|
|
286
|
-
if (
|
|
287
|
-
[
|
|
288
|
-
"systemctl",
|
|
289
|
-
"launchctl",
|
|
290
|
-
"osascript",
|
|
291
|
-
"open",
|
|
292
|
-
"xdg-open",
|
|
293
|
-
"powershell",
|
|
294
|
-
"pwsh",
|
|
295
|
-
"cmd.exe",
|
|
296
|
-
"apt",
|
|
297
|
-
"apt-get",
|
|
298
|
-
"dnf",
|
|
299
|
-
"yum",
|
|
300
|
-
"brew",
|
|
301
|
-
"pacman",
|
|
302
|
-
"apk",
|
|
303
|
-
"xclip",
|
|
304
|
-
"wl-copy",
|
|
305
|
-
].includes(command)
|
|
306
|
-
) {
|
|
307
|
-
labels.add("risk.platform_specific");
|
|
308
|
-
}
|
|
309
|
-
if (
|
|
310
|
-
["pass", "gpg", "ssh-add"].includes(command) ||
|
|
311
|
-
hasSecretTouchingText(parts)
|
|
312
|
-
) {
|
|
313
|
-
labels.add("risk.secret_touching");
|
|
314
|
-
}
|
|
315
|
-
return sortRiskLabels(labels);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function getLeafCommandTemplateWarnings(
|
|
319
|
-
config: CommandTemplateLeafConfig,
|
|
320
|
-
): string[] {
|
|
321
|
-
const parts = splitCommandTemplate(config.template);
|
|
322
|
-
const command = getExecutableName(parts[0]);
|
|
323
|
-
const args = parts.slice(1);
|
|
324
|
-
const warnings: string[] = [];
|
|
325
|
-
if (["bash", "sh", "zsh", "fish"].includes(command)) {
|
|
326
|
-
const shellContent = hasAnyFlag(args, ["-c"])
|
|
327
|
-
? "shell command strings"
|
|
328
|
-
: "shell scripts";
|
|
329
|
-
warnings.push(
|
|
330
|
-
`${config.label ?? command}: invokes ${command}; ${shellContent} are trusted executable content and are not sandboxed by command-template argv splitting. Mitigation: keep scripts local, reviewed, and parameterized with explicit placeholders.`,
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
if (
|
|
334
|
-
["node", "deno", "bun"].includes(command) &&
|
|
335
|
-
hasAnyFlag(args, ["-e", "--eval"])
|
|
336
|
-
) {
|
|
337
|
-
warnings.push(
|
|
338
|
-
`${config.label ?? command}: invokes ${command} eval mode; code strings are trusted executable content and are not sandboxed. Mitigation: prefer a checked-in script file or keep eval input fixed and reviewed.`,
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
if (
|
|
342
|
-
["python", "python3", "perl", "ruby"].includes(command) &&
|
|
343
|
-
hasAnyFlag(args, ["-c", "-e"])
|
|
344
|
-
) {
|
|
345
|
-
warnings.push(
|
|
346
|
-
`${config.label ?? command}: invokes ${command} code-eval mode; code strings are trusted executable content and are not sandboxed. Mitigation: prefer a checked-in script file or keep eval input fixed and reviewed.`,
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
if (
|
|
350
|
-
command === "rm" &&
|
|
351
|
-
(args.some((arg) => /^-[^-]*r/.test(arg) || /^-[^-]*f/.test(arg)) ||
|
|
352
|
-
hasRiskyPathArg(args))
|
|
353
|
-
) {
|
|
354
|
-
warnings.push(
|
|
355
|
-
`${config.label ?? command}: removes filesystem paths; verify placeholders and paths before running trusted destructive commands. Mitigation: constrain path placeholders and consider dry-run or explicit confirmation.`,
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
if (["mv", "cp", "rsync"].includes(command) && hasRiskyPathArg(args)) {
|
|
359
|
-
warnings.push(
|
|
360
|
-
`${config.label ?? command}: mutates broad filesystem paths; verify placeholders and paths before running trusted commands. Mitigation: constrain path placeholders and prefer narrow source/destination paths.`,
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
return warnings;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
156
|
function pad(value: number, width: number): string {
|
|
367
157
|
return String(value).padStart(width, "0");
|
|
368
158
|
}
|
|
@@ -415,7 +205,7 @@ function expandRepeatConfig(
|
|
|
415
205
|
|
|
416
206
|
export function expandCommandTemplateConfigs(
|
|
417
207
|
config: CommandTemplateConfig,
|
|
418
|
-
inherited:
|
|
208
|
+
inherited: CommandTemplateExpansionContext = {},
|
|
419
209
|
): CommandTemplateLeafConfig[] {
|
|
420
210
|
const normalizedConfig = normalizeCommandTemplateConfig(config);
|
|
421
211
|
const inheritedDefaults = normalizeCommandTemplateDefaults(
|
|
@@ -425,7 +215,10 @@ export function expandCommandTemplateConfigs(
|
|
|
425
215
|
normalizeCommandTemplateDefaults(normalizedConfig.defaults),
|
|
426
216
|
inheritedDefaults,
|
|
427
217
|
);
|
|
428
|
-
const context = {
|
|
218
|
+
const context: CommandTemplateExpansionContext = {
|
|
219
|
+
...(inherited.inheritedWhen?.length
|
|
220
|
+
? { inheritedWhen: inherited.inheritedWhen }
|
|
221
|
+
: {}),
|
|
429
222
|
...(inherited.args !== undefined ? { args: inherited.args } : {}),
|
|
430
223
|
...(inheritedDefaults ? { defaults: inheritedDefaults } : {}),
|
|
431
224
|
...(normalizedConfig.args !== undefined
|
|
@@ -441,19 +234,23 @@ export function expandCommandTemplateConfigs(
|
|
|
441
234
|
expandCommandTemplateConfigs(step, context),
|
|
442
235
|
);
|
|
443
236
|
}
|
|
444
|
-
|
|
445
|
-
const recoverSteps = recoverConfig
|
|
446
|
-
? expandCommandTemplateConfigs(recoverConfig, context)
|
|
447
|
-
: [];
|
|
237
|
+
// `recover` is cleanup between failed retries, never a sequence step; runners attach it per leaf.
|
|
448
238
|
if (Array.isArray(normalizedConfig.template)) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
239
|
+
const childContext: CommandTemplateExpansionContext =
|
|
240
|
+
normalizedConfig.when === undefined
|
|
241
|
+
? context
|
|
242
|
+
: {
|
|
243
|
+
...context,
|
|
244
|
+
inheritedWhen: [
|
|
245
|
+
...(context.inheritedWhen ?? []),
|
|
246
|
+
normalizedConfig.when,
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
return normalizedConfig.template.flatMap((step) =>
|
|
250
|
+
expandCommandTemplateConfigs(step, childContext),
|
|
251
|
+
);
|
|
455
252
|
}
|
|
456
|
-
if (typeof normalizedConfig.template !== "string") return
|
|
253
|
+
if (typeof normalizedConfig.template !== "string") return [];
|
|
457
254
|
return [
|
|
458
255
|
{
|
|
459
256
|
...normalizedConfig,
|
|
@@ -461,30 +258,71 @@ export function expandCommandTemplateConfigs(
|
|
|
461
258
|
template: normalizedConfig.template,
|
|
462
259
|
retry: normalizedConfig.retry,
|
|
463
260
|
},
|
|
464
|
-
...recoverSteps,
|
|
465
261
|
];
|
|
466
262
|
}
|
|
467
263
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
264
|
+
/** Whether a node's own and enclosing `when` guards all pass for its resolved placeholder values. */
|
|
265
|
+
export function shouldRunCommandTemplateConfig(
|
|
266
|
+
config: CommandTemplateConfig | CommandTemplateLeafConfig,
|
|
267
|
+
values: Record<string, unknown>,
|
|
268
|
+
): boolean {
|
|
269
|
+
if (typeof config === "string") return true;
|
|
270
|
+
const inheritedWhen =
|
|
271
|
+
"inheritedWhen" in config ? (config.inheritedWhen ?? []) : [];
|
|
272
|
+
if (config.when === undefined && inheritedWhen.length === 0) return true;
|
|
273
|
+
const resolved = { ...getCommandTemplateDefaults(config), ...values };
|
|
274
|
+
return [...inheritedWhen, config.when].every((condition) =>
|
|
275
|
+
shouldRunCommandTemplateNode(condition, resolved),
|
|
276
|
+
);
|
|
478
277
|
}
|
|
479
278
|
|
|
480
|
-
|
|
279
|
+
/** A node's `recover` template as one cleanup run: output ignored, failure stops retries unless a leaf opts into `continue`. */
|
|
280
|
+
export function createCommandTemplateRecovery(
|
|
481
281
|
config: CommandTemplateConfig,
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
282
|
+
values: Record<string, unknown>,
|
|
283
|
+
options: {
|
|
284
|
+
cwd: string;
|
|
285
|
+
timeout?: number;
|
|
286
|
+
execCommand: (
|
|
287
|
+
command: string,
|
|
288
|
+
args: string[],
|
|
289
|
+
options?: CommandTemplateExecOptions,
|
|
290
|
+
) => Promise<CommandTemplateExecResult>;
|
|
291
|
+
},
|
|
292
|
+
): (() => Promise<void>) | undefined {
|
|
293
|
+
if (typeof config === "string") return undefined;
|
|
294
|
+
const recoverConfig = normalizeRecoverConfig(config.recover);
|
|
295
|
+
if (!recoverConfig) return undefined;
|
|
296
|
+
const steps = expandCommandTemplateConfigs(recoverConfig, {
|
|
297
|
+
...(config.args !== undefined ? { args: config.args } : {}),
|
|
298
|
+
...(config.defaults !== undefined ? { defaults: config.defaults } : {}),
|
|
299
|
+
});
|
|
300
|
+
return async () => {
|
|
301
|
+
for (const step of steps) {
|
|
302
|
+
if (!shouldRunCommandTemplateConfig(step, values)) continue;
|
|
303
|
+
const invocation = buildCommandTemplateInvocation(
|
|
304
|
+
step,
|
|
305
|
+
values,
|
|
306
|
+
options.cwd,
|
|
307
|
+
{ missingLabel: "command template recover" },
|
|
308
|
+
);
|
|
309
|
+
const result = await options.execCommand(
|
|
310
|
+
invocation.command,
|
|
311
|
+
invocation.args,
|
|
312
|
+
{
|
|
313
|
+
cwd: options.cwd,
|
|
314
|
+
...(options.timeout !== undefined
|
|
315
|
+
? { timeout: options.timeout }
|
|
316
|
+
: {}),
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
if (result.code !== 0 && step.failure !== "continue") {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Command template recover exited with code ${result.code}${result.killed ? " (killed)" : ""}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
488
326
|
}
|
|
489
327
|
|
|
490
328
|
function parseCommandTemplateArgToken(value: string): {
|
|
@@ -551,10 +389,11 @@ export function splitCommandTemplate(input: string): string[] {
|
|
|
551
389
|
}
|
|
552
390
|
if (char === "\\" && quote !== "'") {
|
|
553
391
|
const next = input[index + 1];
|
|
554
|
-
const escapesNext =
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
392
|
+
const escapesNext =
|
|
393
|
+
quote === '"'
|
|
394
|
+
? next === '"' || next === "\\"
|
|
395
|
+
: next !== undefined &&
|
|
396
|
+
(/\s/u.test(next) || next === "'" || next === '"' || next === "\\");
|
|
558
397
|
if (escapesNext) {
|
|
559
398
|
escaped = true;
|
|
560
399
|
} else {
|
|
@@ -588,10 +427,7 @@ export function splitCommandTemplate(input: string): string[] {
|
|
|
588
427
|
return words;
|
|
589
428
|
}
|
|
590
429
|
|
|
591
|
-
function expandCommandTemplateExecutable(
|
|
592
|
-
command: string,
|
|
593
|
-
cwd: string,
|
|
594
|
-
): string {
|
|
430
|
+
function expandCommandTemplateExecutable(command: string, cwd: string): string {
|
|
595
431
|
if (command === "~") return homedir();
|
|
596
432
|
if (command.startsWith("~/")) return resolve(homedir(), command.slice(2));
|
|
597
433
|
if (command.includes("/") && !isAbsolute(command))
|
|
@@ -816,7 +652,59 @@ function resolveCommandTemplateValue(
|
|
|
816
652
|
return undefined;
|
|
817
653
|
}
|
|
818
654
|
|
|
819
|
-
|
|
655
|
+
/** Resolve one optional non-negative numeric control field, substituting a template token when given as text. */
|
|
656
|
+
export function resolveCommandTemplateNumericField(
|
|
657
|
+
value: number | string | undefined,
|
|
658
|
+
values: Record<string, unknown>,
|
|
659
|
+
label: string,
|
|
660
|
+
): number | undefined {
|
|
661
|
+
if (value === undefined) return undefined;
|
|
662
|
+
const resolved =
|
|
663
|
+
typeof value === "string"
|
|
664
|
+
? substituteCommandTemplateToken(value, values, label)
|
|
665
|
+
: value;
|
|
666
|
+
if (resolved === "") return undefined;
|
|
667
|
+
const numeric = Number(resolved);
|
|
668
|
+
if (!Number.isFinite(numeric) || numeric < 0)
|
|
669
|
+
throw new Error(`Command template ${label} must be a non-negative number.`);
|
|
670
|
+
return numeric;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** A node's own `timeout`; string leaves carry none. */
|
|
674
|
+
export function getCommandTemplateConfiguredTimeout(
|
|
675
|
+
config: CommandTemplateConfig | undefined,
|
|
676
|
+
): number | undefined {
|
|
677
|
+
const timeout = typeof config === "string" ? undefined : config?.timeout;
|
|
678
|
+
return resolveCommandTemplateNumericField(timeout, {}, "timeout");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Bound one composed step by its own timeout and the handler budget left after `elapsedMs`.
|
|
683
|
+
* Without `elapsedMs` the step receives the full budget.
|
|
684
|
+
*/
|
|
685
|
+
export function getCommandTemplateStepTimeout(
|
|
686
|
+
budgetMs: number,
|
|
687
|
+
step: CommandTemplateConfig,
|
|
688
|
+
elapsedMs?: number,
|
|
689
|
+
): number {
|
|
690
|
+
const remaining =
|
|
691
|
+
elapsedMs === undefined ? budgetMs : Math.max(1, budgetMs - elapsedMs);
|
|
692
|
+
const stepTimeout = getCommandTemplateConfiguredTimeout(step);
|
|
693
|
+
return stepTimeout === undefined
|
|
694
|
+
? remaining
|
|
695
|
+
: Math.min(stepTimeout, remaining);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Array templates expand into ordered composition steps; single-command templates have none. */
|
|
699
|
+
export function getCommandTemplateCompositionSteps(
|
|
700
|
+
config: CommandTemplateObjectConfig,
|
|
701
|
+
): CommandTemplateLeafConfig[] {
|
|
702
|
+
return Array.isArray(config.template)
|
|
703
|
+
? expandCommandTemplateConfigs(config)
|
|
704
|
+
: [];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function substituteCommandTemplateToken(
|
|
820
708
|
token: string,
|
|
821
709
|
values: Record<string, unknown>,
|
|
822
710
|
missingLabel = "command template",
|
|
@@ -850,6 +738,7 @@ export async function execCommandTemplate(
|
|
|
850
738
|
const result = await execCommandTemplateOnce(command, args, options);
|
|
851
739
|
if (result.code === 0) return result;
|
|
852
740
|
lastResult = result;
|
|
741
|
+
if (attempt < maxAttempts) await options.recover?.();
|
|
853
742
|
}
|
|
854
743
|
return lastResult;
|
|
855
744
|
}
|
|
@@ -865,7 +754,7 @@ function escapeWindowsCommandArgument(
|
|
|
865
754
|
doubleEscapeMetaChars: boolean,
|
|
866
755
|
): string {
|
|
867
756
|
let escaped = value
|
|
868
|
-
.replace(/(?=(\\+?)?)\1"/g,
|
|
757
|
+
.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"')
|
|
869
758
|
.replace(/(?=(\\+?)?)\1$/g, "$1$1");
|
|
870
759
|
escaped = `"${escaped}"`.replace(WINDOWS_COMMAND_META_CHARS, "^$1");
|
|
871
760
|
return doubleEscapeMetaChars
|
|
@@ -888,13 +777,11 @@ function resolveCommandTemplateSpawn(
|
|
|
888
777
|
return { command, args };
|
|
889
778
|
}
|
|
890
779
|
const normalizedCommand = normalize(command);
|
|
891
|
-
const isNodeModulesShim =
|
|
892
|
-
.test(normalizedCommand);
|
|
780
|
+
const isNodeModulesShim =
|
|
781
|
+
/[\\/]node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/iu.test(normalizedCommand);
|
|
893
782
|
const shellCommand = [
|
|
894
783
|
escapeWindowsCommand(normalizedCommand),
|
|
895
|
-
...args.map((arg) =>
|
|
896
|
-
escapeWindowsCommandArgument(arg, isNodeModulesShim)
|
|
897
|
-
),
|
|
784
|
+
...args.map((arg) => escapeWindowsCommandArgument(arg, isNodeModulesShim)),
|
|
898
785
|
].join(" ");
|
|
899
786
|
return {
|
|
900
787
|
command: process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe",
|