@piewf/cli 3.4.2

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.
@@ -0,0 +1,746 @@
1
+ #!/usr/bin/env node
2
+ import { randomUUID } from "node:crypto";
3
+ import { chmodSync, linkSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { ProjectTrustStore, SessionManager, SettingsManager, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources } from "@earendil-works/pi-coding-agent";
8
+ import { Value } from "typebox/value";
9
+ import { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
10
+ import { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
11
+ import workflowExtension, { formatWorkflowProgress, loadAgentDefinitions, registeredWorkflowFunctions, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "pi-extensible-workflows";
12
+ import { portableEngineVersion, portablePiVersion, writePortableWorkflowBundle } from "./bundles.js";
13
+ import { runSessionInspector, transcriptFileLines } from "./session-inspector.js";
14
+ function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
15
+ function has(value, key) { return Object.prototype.hasOwnProperty.call(value, key); }
16
+ function clone(value) { return structuredClone(value); }
17
+ function kebabCase(value) { return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase(); }
18
+ function scalarType(schema) {
19
+ if (!object(schema) || typeof schema.type !== "string")
20
+ return undefined;
21
+ return ["string", "integer", "number", "boolean"].includes(schema.type) ? schema.type : undefined;
22
+ }
23
+ function schemaPlan(schema) {
24
+ if (!object(schema) || schema.type !== "object")
25
+ return { fields: [] };
26
+ const properties = object(schema.properties) ? schema.properties : {};
27
+ const required = new Set(Array.isArray(schema.required) ? schema.required.filter((name) => typeof name === "string") : []);
28
+ const fields = [];
29
+ for (const [name, property] of Object.entries(properties)) {
30
+ if (!object(property))
31
+ continue;
32
+ const directType = scalarType(property);
33
+ const itemType = object(property.items) ? scalarType(property.items) : undefined;
34
+ const type = directType ?? itemType ? directType ?? "array" : undefined;
35
+ if (!type)
36
+ continue;
37
+ fields.push({ name, option: `--${kebabCase(name)}`, schema: property, type, ...(itemType ? { itemType } : {}), required: required.has(name) });
38
+ }
39
+ const requiredScalars = fields.filter((field) => field.required && field.type !== "array");
40
+ return { fields, ...(requiredScalars.length === 1 ? { positional: requiredScalars[0] } : {}) };
41
+ }
42
+ function scalarLabel(type) { return type === "integer" ? "integer" : type; }
43
+ function scalarFieldType(field) { return field.type === "array" ? field.itemType : field.type; }
44
+ function fieldLabel(field) { return field.type === "array" ? `${field.option} <${scalarLabel(field.itemType)}>` : `${field.option}${field.type === "boolean" ? "" : ` <${scalarLabel(field.type)}>`}`; }
45
+ function fieldDescription(field) {
46
+ const description = typeof field.schema.description === "string" ? field.schema.description.trim() : "";
47
+ const required = field.required ? "required" : "optional";
48
+ const defaultValue = has(field.schema, "default") ? ` default=${JSON.stringify(field.schema.default)}` : "";
49
+ const enumSchema = field.type === "array" && object(field.schema.items) ? field.schema.items : field.schema;
50
+ const enumValue = Array.isArray(enumSchema.enum) ? ` enum=${enumSchema.enum.map((value) => JSON.stringify(value)).join(",")}` : "";
51
+ return [description, required, defaultValue, enumValue].filter(Boolean).join("; ");
52
+ }
53
+ export function formatWorkflowCliHelp(fn, command = "piewf") {
54
+ const plan = schemaPlan(fn.input);
55
+ const lines = [`Usage: ${command} run ${fn.name}${plan.positional ? ` <${plan.positional.name}>` : ""} [options]`, "", fn.description];
56
+ if (plan.positional) {
57
+ lines.push("", "Arguments:", ` <${plan.positional.name}> ${scalarLabel(scalarFieldType(plan.positional))}; ${fieldDescription(plan.positional)}`);
58
+ }
59
+ lines.push("", "Options:");
60
+ for (const field of plan.fields) {
61
+ const label = field === plan.positional ? `${field.option} <${scalarLabel(scalarFieldType(field))}>` : fieldLabel(field);
62
+ lines.push(` ${label.padEnd(24)}${fieldDescription(field)}`);
63
+ }
64
+ lines.push(" --input <json>".padEnd(28) + "JSON input escape hatch for complex schemas", ...launcherHelpLines(), " -h, --help".padEnd(28) + "Show this help");
65
+ return `${lines.join("\n")}\n`;
66
+ }
67
+ function enumAllows(schema, value) {
68
+ return !Array.isArray(schema.enum) || schema.enum.some((candidate) => JSON.stringify(candidate) === JSON.stringify(value));
69
+ }
70
+ function coerce(raw, type, schema) {
71
+ let value;
72
+ if (type === "string")
73
+ value = raw;
74
+ else if (type === "integer") {
75
+ if (!/^-?(?:0|[1-9]\d*)$/.test(raw))
76
+ throw new Error(`Invalid integer: ${raw}`);
77
+ value = Number(raw);
78
+ if (!Number.isSafeInteger(value))
79
+ throw new Error(`Invalid integer: ${raw}`);
80
+ }
81
+ else if (type === "number") {
82
+ value = Number(raw);
83
+ if (!Number.isFinite(value))
84
+ throw new Error(`Invalid number: ${raw}`);
85
+ }
86
+ else {
87
+ if (raw !== "true" && raw !== "false")
88
+ throw new Error(`Invalid boolean: ${raw}`);
89
+ value = raw === "true";
90
+ }
91
+ if (!enumAllows(schema, value))
92
+ throw new Error(`Invalid value for enum: ${raw}`);
93
+ return value;
94
+ }
95
+ function parseJsonInput(value) {
96
+ try {
97
+ return clone(JSON.parse(value));
98
+ }
99
+ catch {
100
+ throw new Error("Invalid JSON passed to --input");
101
+ }
102
+ }
103
+ export function parseWorkflowCliArgs(schema, rawArgs) {
104
+ const plan = schemaPlan(schema);
105
+ const fields = new Map(plan.fields.map((field) => [field.option, field]));
106
+ const result = {};
107
+ let input;
108
+ let positionalUsed = false;
109
+ let endOptions = false;
110
+ const assign = (field, raw) => {
111
+ if (field.type === "array") {
112
+ const values = Array.isArray(result[field.name]) ? result[field.name] : [];
113
+ values.push(coerce(raw, field.itemType, field.schema.items));
114
+ result[field.name] = values;
115
+ }
116
+ else
117
+ result[field.name] = coerce(raw, field.type, field.schema);
118
+ };
119
+ for (let index = 0; index < rawArgs.length; index += 1) {
120
+ const token = rawArgs[index];
121
+ if (token === "--") {
122
+ endOptions = true;
123
+ continue;
124
+ }
125
+ if (!endOptions && (token === "--input" || token.startsWith("--input="))) {
126
+ if (input !== undefined)
127
+ throw new Error("--input may only be provided once");
128
+ const raw = token.startsWith("--input=") ? token.slice("--input=".length) : rawArgs[++index];
129
+ if (raw === undefined)
130
+ throw new Error("Missing value for --input");
131
+ input = parseJsonInput(raw);
132
+ continue;
133
+ }
134
+ if (!endOptions && token.startsWith("--")) {
135
+ const equals = token.indexOf("=");
136
+ const option = equals >= 0 ? token.slice(0, equals) : token;
137
+ const negated = equals < 0 && option.startsWith("--no-");
138
+ const field = fields.get(negated ? `--${option.slice("--no-".length)}` : option);
139
+ if (!field)
140
+ throw new Error(`Unknown option: ${option}`);
141
+ if (negated) {
142
+ if (field.type !== "boolean")
143
+ throw new Error(`Invalid boolean option: ${option}`);
144
+ result[field.name] = false;
145
+ }
146
+ else if (field.type === "boolean") {
147
+ if (equals >= 0)
148
+ assign(field, token.slice(equals + 1));
149
+ else if (rawArgs[index + 1] === "true" || rawArgs[index + 1] === "false")
150
+ assign(field, rawArgs[++index]);
151
+ else
152
+ result[field.name] = true;
153
+ }
154
+ else {
155
+ const raw = equals >= 0 ? token.slice(equals + 1) : rawArgs[++index];
156
+ if (raw === undefined || raw.startsWith("--"))
157
+ throw new Error(`Missing value for ${option}`);
158
+ assign(field, raw);
159
+ }
160
+ continue;
161
+ }
162
+ const positional = plan.positional;
163
+ const numericNegative = positional && (positional.type === "integer" || positional.type === "number") && /^-\d/.test(token);
164
+ if (!endOptions && token.startsWith("-") && !numericNegative)
165
+ throw new Error(`Unknown option: ${token}`);
166
+ if (!positional || positionalUsed)
167
+ throw new Error(`Unexpected argument: ${token}`);
168
+ assign(positional, token);
169
+ positionalUsed = true;
170
+ }
171
+ if (input !== undefined) {
172
+ if (Object.keys(result).length || positionalUsed)
173
+ throw new Error("--input cannot be combined with CLI arguments");
174
+ if (!object(input))
175
+ throw new Error("Workflow input must be a JSON object");
176
+ for (const field of plan.fields)
177
+ if (!has(input, field.name) && has(field.schema, "default"))
178
+ input[field.name] = clone(field.schema.default);
179
+ return input;
180
+ }
181
+ for (const field of plan.fields)
182
+ if (!has(result, field.name) && has(field.schema, "default"))
183
+ result[field.name] = clone(field.schema.default);
184
+ for (const field of plan.fields)
185
+ if (field.required && !has(result, field.name))
186
+ throw new Error(`Missing required argument: ${field.name}`);
187
+ return result;
188
+ }
189
+ function launcherHelpLines() {
190
+ return [
191
+ " --approve".padEnd(28) + "Trust project resources for this launch",
192
+ " --no-approve".padEnd(28) + "Do not trust project resources for this launch",
193
+ " --".padEnd(28) + "End launcher option parsing; pass later tokens to workflow input",
194
+ ];
195
+ }
196
+ function workflowUsage() { return [`Usage: piewf run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
197
+ function exportUsage() { return [`Usage: piewf export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
198
+ function bundleUsage() { return [`Usage: piewf bundle <workflow-name> [--name <command>] [--output <directory>] [--force]`, "", "The bundle contains a launcher, manifest, workflow payload, and external-runtime setup instructions.", "Repeat --role, --alias, --tool, --command, or --environment to declare recipient requirements.", "Use --extension, --skill, --resource, and --dependency to copy selected payload resources."].join("\n") + "\n"; }
199
+ function parseInspectArgs(rawArgs) {
200
+ let sessionId;
201
+ let mode = "tui";
202
+ let failedOnly = false;
203
+ for (const arg of rawArgs) {
204
+ if (arg === "--json" || arg === "--summary") {
205
+ const next = arg === "--json" ? "json" : "summary";
206
+ if (mode !== "tui" && mode !== next)
207
+ throw new Error("inspect accepts only one output mode");
208
+ mode = next;
209
+ }
210
+ else if (arg === "--failed")
211
+ failedOnly = true;
212
+ else if (arg.startsWith("--"))
213
+ throw new Error(`Unknown inspect option: ${arg}`);
214
+ else if (sessionId !== undefined)
215
+ throw new Error(`Unexpected argument: ${arg}`);
216
+ else
217
+ sessionId = arg;
218
+ }
219
+ return { ...(sessionId ? { sessionId } : {}), mode: failedOnly && mode === "tui" ? "summary" : mode, failedOnly };
220
+ }
221
+ export function parseDoctorCleanupArgs(rawArgs) {
222
+ let olderThanDays = 90;
223
+ let yes = false;
224
+ let seenDays = false;
225
+ for (let index = 0; index < rawArgs.length; index += 1) {
226
+ const token = rawArgs[index];
227
+ if (token === "--yes") {
228
+ yes = true;
229
+ continue;
230
+ }
231
+ const inline = token.startsWith("--older-than-days=") ? token.slice("--older-than-days=".length) : undefined;
232
+ if (token === "--older-than-days" || inline !== undefined) {
233
+ if (seenDays)
234
+ throw new Error("--older-than-days may only be provided once");
235
+ const raw = inline ?? rawArgs[++index];
236
+ if (raw === undefined || !/^[1-9]\d*$/.test(raw))
237
+ throw new Error("older-than-days must be a positive integer");
238
+ const parsed = Number(raw);
239
+ if (!Number.isSafeInteger(parsed) || parsed < 1)
240
+ throw new Error("older-than-days must be a positive integer");
241
+ olderThanDays = parsed;
242
+ seenDays = true;
243
+ continue;
244
+ }
245
+ throw new Error(`Unknown cleanup option: ${token}`);
246
+ }
247
+ return { olderThanDays, yes };
248
+ }
249
+ function stripTrustOptions(rawArgs) {
250
+ const args = [];
251
+ let trustOverride;
252
+ let endOptions = false;
253
+ for (const arg of rawArgs) {
254
+ if (arg === "--") {
255
+ endOptions = true;
256
+ args.push(arg);
257
+ continue;
258
+ }
259
+ if (!endOptions && (arg === "--approve" || arg === "--no-approve")) {
260
+ const next = arg === "--approve";
261
+ if (trustOverride !== undefined && trustOverride !== next)
262
+ throw new Error("--approve and --no-approve cannot be combined");
263
+ trustOverride = next;
264
+ }
265
+ else
266
+ args.push(arg);
267
+ }
268
+ return { args, ...(trustOverride !== undefined ? { trustOverride } : {}) };
269
+ }
270
+ async function createWorkflowRuntime(options, shutdownHandlers = []) {
271
+ const cwd = options.cwd ?? process.cwd();
272
+ const agentDir = options.agentDir ?? getAgentDir();
273
+ const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
274
+ const requiredTrust = hasTrustRequiringProjectResources(cwd);
275
+ const trustStore = new ProjectTrustStore(agentDir);
276
+ const defaultProjectTrust = settingsManager.getDefaultProjectTrust();
277
+ const resolveProjectTrust = async ({ extensionsResult }) => {
278
+ if (options.trustOverride !== undefined)
279
+ return options.trustOverride;
280
+ if (!requiredTrust)
281
+ return true;
282
+ const projectTrustContext = {
283
+ cwd,
284
+ mode: "print",
285
+ hasUI: false,
286
+ ui: { select: async () => undefined, confirm: async () => false, input: async () => undefined, notify: () => { } },
287
+ };
288
+ for (const extension of extensionsResult.extensions) {
289
+ for (const handler of extension.handlers.get("project_trust") ?? []) {
290
+ try {
291
+ const result = await handler({ type: "project_trust", cwd }, projectTrustContext);
292
+ if (result.trusted === "undecided")
293
+ continue;
294
+ if (result.trusted !== "yes" && result.trusted !== "no")
295
+ continue;
296
+ const trusted = result.trusted === "yes";
297
+ if (result.remember === true)
298
+ trustStore.set(cwd, trusted);
299
+ return trusted;
300
+ }
301
+ catch { /* Project trust extensions are best effort, as in Pi. */ }
302
+ }
303
+ }
304
+ const savedTrust = trustStore.get(cwd);
305
+ if (savedTrust !== null)
306
+ return savedTrust;
307
+ return defaultProjectTrust === "always";
308
+ };
309
+ const services = await createAgentSessionServices({
310
+ cwd,
311
+ agentDir,
312
+ settingsManager,
313
+ resourceLoaderOptions: { ...(options.skillPaths?.length ? { additionalSkillPaths: [...options.skillPaths] } : {}) },
314
+ resourceLoaderReloadOptions: { resolveProjectTrust },
315
+ });
316
+ const extensions = services.resourceLoader.getExtensions();
317
+ const tools = [];
318
+ const activeTools = [...new Set(["read", "bash", "edit", "write"].concat(extensions.extensions.flatMap((extension) => [...extension.tools.keys()]), ["workflow"]))];
319
+ const headlessPi = {
320
+ registerTool(tool) { tools.push(tool); },
321
+ registerCommand() { },
322
+ getThinkingLevel: () => services.settingsManager.getDefaultThinkingLevel() ?? "medium",
323
+ getActiveTools: () => activeTools,
324
+ on(name, handler) { if (name === "session_shutdown" && typeof handler === "function")
325
+ shutdownHandlers.push(handler); },
326
+ appendEntry() { },
327
+ sendMessage() { },
328
+ events: { emit() { } },
329
+ };
330
+ workflowExtension(headlessPi, homedir(), undefined, undefined, agentDir, options.skillPaths);
331
+ const workflowTool = tools.find((tool) => object(tool) && tool.name === "workflow");
332
+ if (!workflowTool)
333
+ throw new Error("The workflow runtime could not be initialized");
334
+ return { catalog: workflowCatalog({ cwd, projectTrusted: settingsManager.isProjectTrusted(), globalSettingsPath: workflowSettingsPath(agentDir) }), services, workflowTool, shutdownHandlers };
335
+ }
336
+ function availableModelInfo(services, available = false) {
337
+ const models = available ? services.modelRuntime.getAvailableSnapshot() : services.modelRuntime.getModels();
338
+ return models.map(({ provider, id }) => ({ provider, id }));
339
+ }
340
+ async function selectedModel(services) {
341
+ const { session } = await createAgentSessionFromServices({ services, sessionManager: SessionManager.inMemory(), noTools: "all" });
342
+ try {
343
+ const model = session.model;
344
+ return model ? { provider: model.provider, id: model.id } : undefined;
345
+ }
346
+ finally {
347
+ session.dispose();
348
+ }
349
+ }
350
+ function commandName(value) { return value.trim() && !value.includes("/") && !value.includes("\\") ? value.trim() : ""; }
351
+ function writeLauncher(destination, workflowName, force) {
352
+ const parent = dirname(destination);
353
+ mkdirSync(parent, { recursive: true });
354
+ const tempDir = mkdtempSync(join(parent, ".pi-extensible-workflows-"));
355
+ const tempPath = join(tempDir, "launcher");
356
+ try {
357
+ const source = `#!/usr/bin/env node
358
+ import { spawnSync } from "node:child_process";
359
+ import { homedir } from "node:os";
360
+ import { join } from "node:path";
361
+ import { pathToFileURL } from "node:url";
362
+ let cli;
363
+ try { cli = await import(import.meta.resolve("@piewf/cli")); } catch {}
364
+ if (!cli) try { cli = await import(pathToFileURL(join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "npm", "node_modules", "@piewf/cli", "dist", "src", "cli.js")).href); } catch {}
365
+ if (cli) process.exitCode = await cli.runCli(["run", ${JSON.stringify(workflowName)}, ...process.argv.slice(2)]);
366
+ else { const result = spawnSync("piewf", ["run", ${JSON.stringify(workflowName)}, ...process.argv.slice(2)], { stdio: "inherit" }); if (result.error) { console.error("Could not resolve @piewf/cli; install it or put piewf on PATH."); process.exitCode = 1; } else process.exitCode = result.status ?? 1; }
367
+ `;
368
+ writeFileSync(tempPath, source, { mode: 0o755 });
369
+ chmodSync(tempPath, 0o755);
370
+ if (force)
371
+ renameSync(tempPath, destination);
372
+ else {
373
+ try {
374
+ linkSync(tempPath, destination);
375
+ }
376
+ catch (error) {
377
+ if (error.code === "EEXIST")
378
+ throw new Error(`Destination already exists: ${destination}; use --force to replace it`, { cause: error });
379
+ throw error;
380
+ }
381
+ }
382
+ }
383
+ finally {
384
+ rmSync(tempDir, { recursive: true, force: true });
385
+ }
386
+ }
387
+ function terminalProgressStyles(enabled) {
388
+ const style = (code) => enabled ? (text) => `\x1b[${String(code)}m${text}\x1b[0m` : (text) => text;
389
+ return { accent: style(36), success: style(32), error: style(31), warning: style(33), muted: style(90), dim: style(2), bold: style(1) };
390
+ }
391
+ class CliProgress {
392
+ stderr;
393
+ onRunId;
394
+ #lastStable = "";
395
+ #lines = 0;
396
+ #frame = 0;
397
+ #run;
398
+ #runId;
399
+ #runtimeStartedAt = 0;
400
+ #runtimeBaseMs = 0;
401
+ #timer;
402
+ #interactive;
403
+ #styles;
404
+ constructor(stderr, tty, onRunId) {
405
+ this.stderr = stderr;
406
+ this.onRunId = onRunId;
407
+ this.#interactive = tty && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
408
+ this.#styles = terminalProgressStyles(this.#interactive);
409
+ }
410
+ update(run) {
411
+ if (this.#runId !== run.id) {
412
+ this.#runId = run.id;
413
+ this.onRunId(run.id);
414
+ this.#runtimeStartedAt = Date.now();
415
+ this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
416
+ }
417
+ else if (this.#run && this.#run.state !== "running" && run.state === "running") {
418
+ this.#runtimeStartedAt = Date.now();
419
+ this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
420
+ }
421
+ this.#run = run;
422
+ if (!this.#interactive) {
423
+ this.#timer ??= setInterval(() => { this.render(); }, 1000);
424
+ this.#timer.unref();
425
+ this.render();
426
+ return;
427
+ }
428
+ this.#timer ??= setInterval(() => { this.render(); }, 80);
429
+ this.#timer.unref();
430
+ this.render();
431
+ }
432
+ render() {
433
+ if (!this.#run)
434
+ return;
435
+ const run = this.#run.state !== "running" ? this.#run : { ...this.#run, usage: { ...(this.#run.usage ?? { tokens: 0, costUsd: 0, durationMs: 0, agentLaunches: 0 }), durationMs: Math.max(this.#run.usage?.durationMs ?? 0, this.#runtimeBaseMs + Date.now() - this.#runtimeStartedAt) } };
436
+ if (!this.#interactive) {
437
+ const stable = formatWorkflowProgress(run, "◇", this.#styles);
438
+ if (stable !== this.#lastStable) {
439
+ this.#lastStable = stable;
440
+ this.stderr(`${stable}\n`);
441
+ }
442
+ return;
443
+ }
444
+ const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][this.#frame++ % 10] ?? "◇";
445
+ const width = process.stderr.columns || 80;
446
+ const text = truncateWorkflowProgress(formatWorkflowProgress(run, spinner, this.#styles), width).join("\n");
447
+ this.stderr(`${this.#lines ? `\x1b[${String(this.#lines)}A` : ""}${this.#lines ? "" : "\x1b[?25l"}\x1b[0J${text}\n`);
448
+ this.#lines = text.split("\n").length;
449
+ }
450
+ finish() {
451
+ if (this.#timer) {
452
+ clearInterval(this.#timer);
453
+ this.#timer = undefined;
454
+ }
455
+ if (this.#interactive && this.#lines) {
456
+ this.stderr(`\x1b[${String(this.#lines)}A\x1b[0J\x1b[?25h`);
457
+ this.#lines = 0;
458
+ }
459
+ this.#run = undefined;
460
+ }
461
+ }
462
+ async function invokeWorkflow(fn, args, runtime, options, context) {
463
+ if (!Value.Check(fn.input, args))
464
+ throw new Error(`Invalid input for ${fn.name}`);
465
+ let announcedRunId;
466
+ const announceRunId = (runId) => { if (announcedRunId === runId)
467
+ return; announcedRunId = runId; options.stderr(`Run ID: ${runId}\n`); };
468
+ const progress = new CliProgress(options.stderr, options.isTTY ?? process.stderr.isTTY, announceRunId);
469
+ try {
470
+ const result = await runtime.workflowTool.execute(randomUUID(), { workflow: fn.name, args, foreground: true }, options.signal, (update) => { if (object(update) && object(update.details) && object(update.details.run))
471
+ progress.update(update.details.run); }, context);
472
+ const details = object(result.details) ? result.details : {};
473
+ const runId = typeof details.runId === "string" ? details.runId : undefined;
474
+ if (runId)
475
+ announceRunId(runId);
476
+ if (has(details, "value"))
477
+ return { value: details.value, ...(runId ? { runId } : {}) };
478
+ const first = result.content[0];
479
+ if (!first || first.type !== "text")
480
+ throw new Error("Workflow returned no result");
481
+ try {
482
+ return { value: parseJsonInput(first.text), ...(runId ? { runId } : {}) };
483
+ }
484
+ catch {
485
+ throw new Error("Workflow returned invalid JSON");
486
+ }
487
+ }
488
+ finally {
489
+ progress.finish();
490
+ }
491
+ }
492
+ async function createWorkflowContext(runtime, options) {
493
+ const model = await selectedModel(runtime.services);
494
+ const sessionManager = SessionManager.inMemory();
495
+ const modelRegistry = { getAll: () => availableModelInfo(runtime.services), getAvailable: () => availableModelInfo(runtime.services, true) };
496
+ return { cwd: options.cwd ?? process.cwd(), mode: "print", hasUI: false, ...(model ? { model } : {}), modelRegistry, sessionManager, isProjectTrusted: () => runtime.services.settingsManager.isProjectTrusted(), ui: { select: async () => undefined, confirm: async () => false, input: async () => undefined, notify: () => { }, onTerminalInput: () => () => { }, setStatus: () => { }, setWorkingMessage: () => { }, setWorkingVisible: () => { }, setWorkingIndicator: () => { }, setHiddenThinkingLabel: () => { }, setWidget: () => { }, setFooter: () => { }, setHeader: () => { }, setTitle: () => { }, custom: async () => undefined, pasteToEditor: () => { }, setEditorText: () => { }, getEditorText: () => "", editor: async () => undefined, addAutocompleteProvider: () => { } }, headless: true };
497
+ }
498
+ async function shutdownWorkflowRuntime(handlers, context) {
499
+ for (const handler of handlers) {
500
+ try {
501
+ await handler({ type: "session_shutdown", reason: "quit" }, context);
502
+ }
503
+ catch { /* Shutdown is best effort. */ }
504
+ }
505
+ }
506
+ async function withWorkflowRuntime(options, action) {
507
+ const shutdownHandlers = [];
508
+ let context = { cwd: options.cwd ?? process.cwd(), mode: "print", hasUI: false, headless: true };
509
+ try {
510
+ const runtime = await createWorkflowRuntime(options, shutdownHandlers);
511
+ context = await createWorkflowContext(runtime, options);
512
+ return await action(runtime, context);
513
+ }
514
+ finally {
515
+ await shutdownWorkflowRuntime(shutdownHandlers, context);
516
+ }
517
+ }
518
+ async function runWorkflowCli(rawArgs, options) {
519
+ const parsed = stripTrustOptions(rawArgs);
520
+ const args = parsed.args;
521
+ if (!args.length || args[0] === "--help" || args[0] === "-h") {
522
+ options.write(workflowUsage());
523
+ return args.length ? 0 : 1;
524
+ }
525
+ const name = args[0];
526
+ return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime, context) => {
527
+ const help = args.slice(1).some((arg) => arg === "--help" || arg === "-h");
528
+ const fn = runtime.catalog.functions.find((candidate) => candidate.name === name);
529
+ if (!fn)
530
+ throw new Error(`Unknown workflow function: ${name}`);
531
+ if (help) {
532
+ options.write(formatWorkflowCliHelp(fn));
533
+ return 0;
534
+ }
535
+ const input = parseWorkflowCliArgs(fn.input, args.slice(1));
536
+ const result = await invokeWorkflow(fn, input, runtime, options, context);
537
+ options.write(`${JSON.stringify(result.value)}\n`);
538
+ return 0;
539
+ });
540
+ }
541
+ async function exportWorkflowCli(rawArgs, options) {
542
+ const parsed = stripTrustOptions(rawArgs);
543
+ const args = parsed.args;
544
+ if (args.includes("--bundle"))
545
+ return bundleWorkflowCli(args.filter((arg) => arg !== "--bundle"), options);
546
+ if (!args.length || args[0] === "--help" || args[0] === "-h") {
547
+ options.write(exportUsage());
548
+ return args.length ? 0 : 1;
549
+ }
550
+ const workflowName = args[0];
551
+ return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
552
+ let name;
553
+ let output;
554
+ let force = false;
555
+ for (let index = 1; index < args.length; index += 1) {
556
+ const arg = args[index];
557
+ if (arg === "--force") {
558
+ force = true;
559
+ continue;
560
+ }
561
+ const equals = arg.indexOf("=");
562
+ const option = equals >= 0 ? arg.slice(0, equals) : arg;
563
+ if (option === "--name" || option === "--output") {
564
+ const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
565
+ if (!value)
566
+ throw new Error(`Missing value for ${option}`);
567
+ if (option === "--name")
568
+ name = value;
569
+ else
570
+ output = value;
571
+ continue;
572
+ }
573
+ if (arg === "--help" || arg === "-h") {
574
+ options.write(exportUsage());
575
+ return 0;
576
+ }
577
+ throw new Error(`Unknown option: ${arg}`);
578
+ }
579
+ if (!runtime.catalog.functions.some((candidate) => candidate.name === workflowName))
580
+ throw new Error(`Unknown workflow function: ${workflowName}`);
581
+ const command = commandName(name ?? kebabCase(workflowName));
582
+ if (!command)
583
+ throw new Error("Command name must be a non-empty name without path separators");
584
+ const destination = output ? output : join(homedir(), ".local", "bin", command);
585
+ writeLauncher(destination, workflowName, force);
586
+ if (!output) {
587
+ const binDir = join(homedir(), ".local", "bin");
588
+ const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean).map((entry) => { try {
589
+ return realpathSync(entry);
590
+ }
591
+ catch {
592
+ return entry;
593
+ } });
594
+ if (!pathEntries.includes(binDir))
595
+ options.stderr(`Warning: ${binDir} is not in PATH\n`);
596
+ }
597
+ options.write(`Exported ${destination}\n`);
598
+ return 0;
599
+ });
600
+ }
601
+ async function bundleWorkflowCli(rawArgs, options) {
602
+ const parsed = stripTrustOptions(rawArgs);
603
+ const args = parsed.args;
604
+ if (!args.length || args[0] === "--help" || args[0] === "-h") {
605
+ options.write(bundleUsage());
606
+ return args.length ? 0 : 1;
607
+ }
608
+ const workflowName = args[0];
609
+ return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
610
+ let name;
611
+ let output;
612
+ let force = false;
613
+ const requirements = { roles: [], aliases: [], tools: [], commands: [], environment: [] };
614
+ const resources = { extensions: [], skills: [], static: [], dependencies: [] };
615
+ for (let index = 1; index < args.length; index += 1) {
616
+ const arg = args[index];
617
+ if (arg === "--force") {
618
+ force = true;
619
+ continue;
620
+ }
621
+ const equals = arg.indexOf("=");
622
+ const option = equals >= 0 ? arg.slice(0, equals) : arg;
623
+ const requirementOptions = { "--role": "roles", "--alias": "aliases", "--tool": "tools", "--command": "commands", "--environment": "environment" };
624
+ const resourceOptions = { "--extension": "extensions", "--skill": "skills", "--resource": "static", "--dependency": "dependencies" };
625
+ if (option === "--name" || option === "--output" || Object.prototype.hasOwnProperty.call(requirementOptions, option) || Object.prototype.hasOwnProperty.call(resourceOptions, option)) {
626
+ const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
627
+ if (!value)
628
+ throw new Error(`Missing value for ${option}`);
629
+ if (option === "--name")
630
+ name = value;
631
+ else if (option === "--output")
632
+ output = value;
633
+ else if (Object.prototype.hasOwnProperty.call(requirementOptions, option))
634
+ requirements[requirementOptions[option]].push(value);
635
+ else
636
+ resources[resourceOptions[option]].push(value);
637
+ continue;
638
+ }
639
+ if (arg === "--help" || arg === "-h") {
640
+ options.write(bundleUsage());
641
+ return 0;
642
+ }
643
+ throw new Error(`Unknown option: ${arg}`);
644
+ }
645
+ const fn = runtime.catalog.functions.find((candidate) => candidate.name === workflowName);
646
+ if (!fn)
647
+ throw new Error(`Unknown workflow function: ${workflowName}`);
648
+ const registered = registeredWorkflowFunctions()[workflowName];
649
+ if (!registered)
650
+ throw new Error(`Workflow ${workflowName} is not exportable because its registered implementation is unavailable`);
651
+ const definitions = requirements.roles.length ? loadAgentDefinitions(options.cwd ?? process.cwd(), options.agentDir ?? getAgentDir(), runtime.services.settingsManager.isProjectTrusted()) : {};
652
+ const roles = Object.fromEntries(requirements.roles.map((role) => {
653
+ if (!role || role === "." || role === ".." || role.includes("/") || role.includes("\\"))
654
+ throw new Error(`Invalid role name for bundle: ${role}`);
655
+ const definition = definitions[role];
656
+ if (!definition)
657
+ throw new Error(`Unknown role for bundle: ${role}`);
658
+ return [role, definition];
659
+ }));
660
+ const command = commandName(name ?? kebabCase(workflowName));
661
+ if (!command)
662
+ throw new Error("Command name must be a non-empty name without path separators");
663
+ const destination = output ?? join(homedir(), ".local", "share", "pi-extensible-workflows", "bundles", command);
664
+ const aliasTargets = Object.fromEntries(requirements.aliases.flatMap((name) => {
665
+ const target = runtime.catalog.modelAliases?.[name];
666
+ return typeof target === "string" ? [[name, target]] : [];
667
+ }));
668
+ const selectedResources = Object.values(resources).some((entries) => entries.length) ? resources : undefined;
669
+ writePortableWorkflowBundle({ destination, command, workflow: fn, functionSource: registered.run.toString(), requirements, aliasTargets, roles, ...(selectedResources ? { resources: selectedResources } : {}), piVersion: portablePiVersion(), engineVersion: portableEngineVersion(), force });
670
+ options.write(`Bundled ${workflowName} at ${destination}\n`);
671
+ options.write(`Run ${join(destination, command)} setup before launching the workflow.\n`);
672
+ return 0;
673
+ });
674
+ }
675
+ export async function runCli(args, options = {}, write = (text) => { process.stdout.write(text); }) {
676
+ const stderr = options.stderr ?? ((text) => { process.stderr.write(text); });
677
+ if (args[0] === "doctor" && args.length === 1) {
678
+ const report = await doctor(options);
679
+ write(formatDoctorReport(report));
680
+ return doctorExitCode(report);
681
+ }
682
+ if (args[0] === "doctor" && args[1] === "cleanup") {
683
+ if (args.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
684
+ write("Usage: piewf doctor cleanup [--older-than-days <days>] [--yes]\n");
685
+ return 0;
686
+ }
687
+ try {
688
+ const parsed = parseDoctorCleanupArgs(args.slice(2));
689
+ const cleanupOptions = { ...parsed, ...(options.cwd !== undefined ? { cwd: options.cwd } : {}) };
690
+ const report = await doctorCleanup(cleanupOptions);
691
+ write(formatDoctorCleanupReport(report));
692
+ return doctorCleanupExitCode(report);
693
+ }
694
+ catch (error) {
695
+ stderr(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
696
+ return 1;
697
+ }
698
+ }
699
+ if (args[0] === "inspect") {
700
+ try {
701
+ const parsed = parseInspectArgs(args.slice(1));
702
+ if (options.inspect)
703
+ await options.inspect(parsed.sessionId, parsed.mode, parsed.failedOnly);
704
+ else
705
+ await runSessionInspector(parsed.sessionId, parsed.mode, options.cwd ?? process.cwd(), undefined, write, parsed.failedOnly);
706
+ return 0;
707
+ }
708
+ catch (error) {
709
+ write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
710
+ return 1;
711
+ }
712
+ }
713
+ if (args[0] === "transcript" && args.length === 2) {
714
+ try {
715
+ if (options.transcript)
716
+ await options.transcript(args[1]);
717
+ else
718
+ write(`${transcriptFileLines(args[1]).join("\n")}\n`);
719
+ return 0;
720
+ }
721
+ catch (error) {
722
+ write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
723
+ return 1;
724
+ }
725
+ }
726
+ if (args[0] === "bundle" || args[0] === "run" || args[0] === "export") {
727
+ try {
728
+ const workflowOptions = { write, stderr, ...(options.cwd !== undefined ? { cwd: options.cwd } : {}), ...(options.agentDir !== undefined ? { agentDir: options.agentDir } : {}), ...(options.signal ? { signal: options.signal } : {}), ...(options.trustOverride !== undefined ? { trustOverride: options.trustOverride } : {}), ...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}), ...(options.skillPaths?.length ? { skillPaths: [...options.skillPaths] } : {}) };
729
+ if (args[0] === "bundle")
730
+ return await bundleWorkflowCli(args.slice(1), workflowOptions);
731
+ return args[0] === "run" ? await runWorkflowCli(args.slice(1), workflowOptions) : await exportWorkflowCli(args.slice(1), workflowOptions);
732
+ }
733
+ catch (error) {
734
+ stderr(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
735
+ return 1;
736
+ }
737
+ }
738
+ write("Usage: piewf doctor | inspect [session-id] [--json|--summary] [--failed] | transcript <session-file> | bundle <workflow-name> [--name <command>] [--output <path>] [--force] | run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]\n");
739
+ return 1;
740
+ }
741
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
742
+ const controller = new AbortController();
743
+ const onSignal = () => { controller.abort(); };
744
+ process.once("SIGINT", onSignal);
745
+ process.exitCode = await runCli(process.argv.slice(2), { signal: controller.signal });
746
+ }