pi-extensible-workflows 2.0.0 → 3.1.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/README.md +18 -11
- package/dist/src/agent-execution.d.ts +4 -94
- package/dist/src/agent-execution.js +34 -208
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +160 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +60 -8
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +597 -0
- package/dist/src/doctor.d.ts +7 -2
- package/dist/src/doctor.js +127 -22
- package/dist/src/execution.d.ts +17 -0
- package/dist/src/execution.js +630 -0
- package/dist/src/host.d.ts +101 -0
- package/dist/src/host.js +3084 -0
- package/dist/src/index.d.ts +13 -634
- package/dist/src/index.js +10 -4432
- package/dist/src/persistence.d.ts +9 -19
- package/dist/src/persistence.js +175 -61
- package/dist/src/registry.d.ts +43 -0
- package/dist/src/registry.js +279 -0
- package/dist/src/session-inspector.js +5 -3
- package/dist/src/types.d.ts +692 -0
- package/dist/src/types.js +27 -0
- package/dist/src/utils.d.ts +32 -0
- package/dist/src/utils.js +168 -0
- package/dist/src/validation.d.ts +28 -0
- package/dist/src/validation.js +832 -0
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +69 -34
- package/src/agent-execution.ts +37 -189
- package/src/budget.ts +75 -0
- package/src/cli.ts +47 -7
- package/src/doctor-cleanup.ts +337 -0
- package/src/doctor.ts +117 -24
- package/src/execution.ts +540 -0
- package/src/host.ts +2598 -0
- package/src/index.ts +14 -3856
- package/src/persistence.ts +122 -37
- package/src/registry.ts +240 -0
- package/src/session-inspector.ts +5 -3
- package/src/types.ts +158 -0
- package/src/utils.ts +142 -0
- package/src/validation.ts +688 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type BudgetDimension, type BudgetEvent, type WorkflowBudget, type WorkflowBudgetPatch, type WorkflowBudgetUsage, type AgentAccounting, type RunState } from "./types.js";
|
|
2
|
+
export declare function validateBudget(value: unknown): WorkflowBudget | undefined;
|
|
3
|
+
export declare function validateBudgetPatch(value: unknown): WorkflowBudgetPatch;
|
|
4
|
+
export declare function budgetUsage(value?: Partial<WorkflowBudgetUsage>): WorkflowBudgetUsage;
|
|
5
|
+
export declare class WorkflowBudgetRuntime {
|
|
6
|
+
#private;
|
|
7
|
+
readonly budget: WorkflowBudget | undefined;
|
|
8
|
+
readonly version: number;
|
|
9
|
+
constructor(budget: WorkflowBudget | undefined, version?: number, usage?: Partial<WorkflowBudgetUsage>, events?: readonly BudgetEvent[], options?: {
|
|
10
|
+
now?: () => number;
|
|
11
|
+
onChange?: () => void;
|
|
12
|
+
active?: boolean;
|
|
13
|
+
});
|
|
14
|
+
get usage(): WorkflowBudgetUsage;
|
|
15
|
+
get events(): readonly BudgetEvent[];
|
|
16
|
+
get hardExhausted(): boolean;
|
|
17
|
+
checkAgentLaunch(): void;
|
|
18
|
+
beforeAttempt(): void;
|
|
19
|
+
beforeTurn(): void;
|
|
20
|
+
afterTurn(accounting: AgentAccounting, final: boolean): void;
|
|
21
|
+
instruction(agentId?: string): string | undefined;
|
|
22
|
+
forAgent(agentId: string): {
|
|
23
|
+
beforeAttempt: () => void;
|
|
24
|
+
beforeTurn: () => void;
|
|
25
|
+
afterTurn: (accounting: AgentAccounting, final: boolean) => void;
|
|
26
|
+
instruction: () => string | undefined;
|
|
27
|
+
};
|
|
28
|
+
transition(state: RunState): void;
|
|
29
|
+
recordEvent(event: BudgetEvent): void;
|
|
30
|
+
snapshot(): {
|
|
31
|
+
usage: WorkflowBudgetUsage;
|
|
32
|
+
budgetEvents: readonly BudgetEvent[];
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export declare function mergeBudget(budget: WorkflowBudget | undefined, patch: WorkflowBudgetPatch): WorkflowBudget | undefined;
|
|
36
|
+
export declare function budgetRelaxed(previous: WorkflowBudget | undefined, next: WorkflowBudget | undefined): boolean;
|
|
37
|
+
export declare function exhaustedBudgetDimensions(budget: WorkflowBudget | undefined, usage: WorkflowBudgetUsage): BudgetDimension[];
|
|
38
|
+
export declare function resumeBudgetAllowed(budget: WorkflowBudget | undefined, usage: WorkflowBudgetUsage): boolean;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { WorkflowError } from "./types.js";
|
|
2
|
+
import { fail, object } from "./utils.js";
|
|
3
|
+
function nonNegativeInteger(value) { return Number.isInteger(value) && value >= 0; }
|
|
4
|
+
function nonNegativeFinite(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
|
|
5
|
+
export function validateBudget(value) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (!object(value))
|
|
9
|
+
fail("INVALID_METADATA", "budget must be an object");
|
|
10
|
+
const result = {};
|
|
11
|
+
for (const [dimension, raw] of Object.entries(value)) {
|
|
12
|
+
if (!["tokens", "costUsd", "durationMs", "agentLaunches"].includes(dimension))
|
|
13
|
+
fail("INVALID_METADATA", `Unknown budget dimension: ${dimension}`);
|
|
14
|
+
if (!object(raw))
|
|
15
|
+
fail("INVALID_METADATA", `${dimension} budget must be an object`);
|
|
16
|
+
if (Object.keys(raw).some((key) => key !== "soft" && key !== "hard"))
|
|
17
|
+
fail("INVALID_METADATA", `${dimension} budget has an unknown limit`);
|
|
18
|
+
const isCost = dimension === "costUsd";
|
|
19
|
+
for (const key of ["soft", "hard"])
|
|
20
|
+
if (raw[key] !== undefined && !(isCost ? nonNegativeFinite(raw[key]) : nonNegativeInteger(raw[key])))
|
|
21
|
+
fail("INVALID_METADATA", `${dimension}.${key} must be a non-negative ${isCost ? "finite number" : "integer"}`);
|
|
22
|
+
if (raw.soft !== undefined && raw.soft !== null && raw.hard !== undefined && raw.hard !== null && raw.soft >= raw.hard)
|
|
23
|
+
fail("INVALID_METADATA", `${dimension}.soft must be less than hard`);
|
|
24
|
+
const limits = {};
|
|
25
|
+
if (raw.soft !== undefined)
|
|
26
|
+
limits.soft = raw.soft;
|
|
27
|
+
if (raw.hard !== undefined)
|
|
28
|
+
limits.hard = raw.hard;
|
|
29
|
+
if (Object.keys(limits).length)
|
|
30
|
+
result[dimension] = limits;
|
|
31
|
+
}
|
|
32
|
+
return Object.freeze(result);
|
|
33
|
+
}
|
|
34
|
+
export function validateBudgetPatch(value) {
|
|
35
|
+
if (!object(value))
|
|
36
|
+
fail("INVALID_METADATA", "budget patch must be an object");
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const [dimension, raw] of Object.entries(value)) {
|
|
39
|
+
if (!["tokens", "costUsd", "durationMs", "agentLaunches"].includes(dimension))
|
|
40
|
+
fail("INVALID_METADATA", `Unknown budget dimension: ${dimension}`);
|
|
41
|
+
if (raw === null) {
|
|
42
|
+
result[dimension] = null;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!object(raw) || Object.keys(raw).some((key) => key !== "soft" && key !== "hard"))
|
|
46
|
+
fail("INVALID_METADATA", `${dimension} budget patch must contain only soft and hard`);
|
|
47
|
+
const limits = {};
|
|
48
|
+
for (const key of ["soft", "hard"])
|
|
49
|
+
if (Object.prototype.hasOwnProperty.call(raw, key)) {
|
|
50
|
+
if (raw[key] === null)
|
|
51
|
+
limits[key] = null;
|
|
52
|
+
else {
|
|
53
|
+
const checked = validateBudget({ [dimension]: { [key]: raw[key] } })?.[dimension];
|
|
54
|
+
if (checked?.[key] !== undefined)
|
|
55
|
+
limits[key] = checked[key];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (limits.soft !== null && limits.hard !== null && limits.soft !== undefined && limits.hard !== undefined && limits.soft >= limits.hard)
|
|
59
|
+
fail("INVALID_METADATA", `${dimension}.soft must be less than hard`);
|
|
60
|
+
result[dimension] = limits;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
export function budgetUsage(value) { return { tokens: value?.tokens ?? 0, costUsd: value?.costUsd ?? 0, durationMs: value?.durationMs ?? 0, agentLaunches: value?.agentLaunches ?? 0 }; }
|
|
65
|
+
export class WorkflowBudgetRuntime {
|
|
66
|
+
budget;
|
|
67
|
+
version;
|
|
68
|
+
#now;
|
|
69
|
+
#onChange;
|
|
70
|
+
#injected = new Set();
|
|
71
|
+
#seen = new Set();
|
|
72
|
+
#active;
|
|
73
|
+
#activeSince;
|
|
74
|
+
#usage;
|
|
75
|
+
#events;
|
|
76
|
+
#turnAccounting;
|
|
77
|
+
constructor(budget, version = 1, usage, events = [], options = {}) {
|
|
78
|
+
this.budget = budget;
|
|
79
|
+
this.version = version;
|
|
80
|
+
this.#now = options.now ?? (() => Date.now());
|
|
81
|
+
this.#onChange = options.onChange;
|
|
82
|
+
this.#active = options.active ?? true;
|
|
83
|
+
this.#activeSince = this.#active ? this.#now() : undefined;
|
|
84
|
+
this.#usage = budgetUsage(usage);
|
|
85
|
+
this.#events = [...events];
|
|
86
|
+
for (const event of events)
|
|
87
|
+
if (event.budgetVersion === version)
|
|
88
|
+
this.#seen.add(event.type);
|
|
89
|
+
}
|
|
90
|
+
get usage() { this.#syncDuration(); return { ...this.#usage }; }
|
|
91
|
+
get events() { return this.#events; }
|
|
92
|
+
get hardExhausted() { return this.#events.some((event) => event.type === "hard_exhausted" && event.budgetVersion === this.version); }
|
|
93
|
+
checkAgentLaunch() { this.#checkHard(["agentLaunches"]); }
|
|
94
|
+
beforeAttempt() { this.#checkHard(["agentLaunches"]); this.#usage.agentLaunches += 1; this.#evaluate(); }
|
|
95
|
+
beforeTurn() { this.#syncDuration(); this.#evaluate(); this.#checkHard(["tokens", "costUsd", "durationMs"]); }
|
|
96
|
+
afterTurn(accounting, final) { this.#syncDuration(); this.#applyTurn(accounting, final, this.#turnAccounting); this.#turnAccounting = { input: accounting.input, output: accounting.output, cost: accounting.cost }; }
|
|
97
|
+
#applyTurn(accounting, final, previous = { input: 0, output: 0, cost: 0 }) { this.#usage.tokens += Math.max(0, accounting.input - previous.input) + Math.max(0, accounting.output - previous.output); this.#usage.costUsd += Math.max(0, accounting.cost - previous.cost); this.#evaluate(); if (!final)
|
|
98
|
+
this.#checkHard(["tokens", "costUsd", "durationMs"]); }
|
|
99
|
+
instruction(agentId = "agent") { if (!this.#hasSoftCrossed() || this.#injected.has(agentId))
|
|
100
|
+
return undefined; this.#injected.add(agentId); return `The workflow budget soft limit has been reached. Finish the requested output now, preserving any required output schema. Current usage: ${JSON.stringify(this.usage)}. Do not start additional model work unless it is required to produce the final requested result.`; }
|
|
101
|
+
forAgent(agentId) { let attempt = 0; let previous; return { beforeAttempt: () => { attempt += 1; previous = undefined; this.beforeAttempt(); }, beforeTurn: () => { this.beforeTurn(); }, afterTurn: (accounting, final) => { this.#applyTurn(accounting, final, previous); previous = { input: accounting.input, output: accounting.output, cost: accounting.cost }; }, instruction: () => this.instruction(`${agentId}:${String(attempt + 1)}`) }; }
|
|
102
|
+
transition(state) { const active = state === "running"; if (active === this.#active)
|
|
103
|
+
return; if (active) {
|
|
104
|
+
this.#active = true;
|
|
105
|
+
this.#activeSince = this.#now();
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.#syncDuration();
|
|
109
|
+
this.#evaluate();
|
|
110
|
+
this.#active = false;
|
|
111
|
+
this.#activeSince = undefined;
|
|
112
|
+
} this.#onChange?.(); }
|
|
113
|
+
#syncDuration() { if (this.#active && this.#activeSince !== undefined) {
|
|
114
|
+
const now = this.#now();
|
|
115
|
+
this.#usage.durationMs += Math.max(0, now - this.#activeSince);
|
|
116
|
+
this.#activeSince = now;
|
|
117
|
+
} }
|
|
118
|
+
#hasSoftCrossed() { return !!this.budget && Object.entries(this.budget).some(([dimension, limits]) => limits.soft !== undefined && this.#usage[dimension] >= limits.soft); }
|
|
119
|
+
#checkHard(dimensions) { const exhausted = dimensions.filter((dimension) => { const hard = this.budget?.[dimension]?.hard; return hard !== undefined && this.#usage[dimension] >= hard; }); if (!exhausted.length)
|
|
120
|
+
return; this.#record("hard_exhausted", exhausted); const detail = exhausted.map((dimension) => `${dimension} usage=${String(this.#usage[dimension])} hard=${String(this.budget?.[dimension]?.hard)}`).join(", "); throw new WorkflowError("BUDGET_EXHAUSTED", `Budget version ${String(this.version)} exhausted: ${detail}`); }
|
|
121
|
+
#evaluate() { const budget = this.budget; if (!budget)
|
|
122
|
+
return; const soft = Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.soft !== undefined && this.#usage[dimension] >= limits.soft; }); if (soft.length)
|
|
123
|
+
this.#record("soft_crossed", soft); const overrun = Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.hard !== undefined && this.#usage[dimension] > limits.hard; }); if (overrun.length)
|
|
124
|
+
this.#record("hard_overrun", overrun); }
|
|
125
|
+
#record(type, dimensions) { if (this.#seen.has(type))
|
|
126
|
+
return; this.#seen.add(type); this.#events.push({ type, budgetVersion: this.version, dimensions: [...dimensions], usage: this.usage, limits: structuredClone(this.budget ?? {}), at: this.#now() }); this.#onChange?.(); }
|
|
127
|
+
recordEvent(event) { this.#events.push(structuredClone(event)); }
|
|
128
|
+
snapshot() { return { usage: this.usage, budgetEvents: [...this.#events] }; }
|
|
129
|
+
}
|
|
130
|
+
export function mergeBudget(budget, patch) { const merged = structuredClone(budget ?? {}); for (const dimension of ["tokens", "costUsd", "durationMs", "agentLaunches"])
|
|
131
|
+
if (Object.prototype.hasOwnProperty.call(patch, dimension)) {
|
|
132
|
+
const value = patch[dimension];
|
|
133
|
+
if (value === null) {
|
|
134
|
+
Reflect.deleteProperty(merged, dimension);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const next = { ...(merged[dimension] ?? {}) };
|
|
138
|
+
for (const key of ["soft", "hard"])
|
|
139
|
+
if (value && Object.prototype.hasOwnProperty.call(value, key)) {
|
|
140
|
+
const limit = value[key];
|
|
141
|
+
if (limit === null)
|
|
142
|
+
Reflect.deleteProperty(next, key);
|
|
143
|
+
else if (limit !== undefined)
|
|
144
|
+
next[key] = limit;
|
|
145
|
+
}
|
|
146
|
+
if (Object.keys(next).length)
|
|
147
|
+
merged[dimension] = next;
|
|
148
|
+
else
|
|
149
|
+
Reflect.deleteProperty(merged, dimension);
|
|
150
|
+
} return validateBudget(merged); }
|
|
151
|
+
export function budgetRelaxed(previous, next) { for (const dimension of ["tokens", "costUsd", "durationMs", "agentLaunches"]) {
|
|
152
|
+
const oldLimit = previous?.[dimension];
|
|
153
|
+
const newLimit = next?.[dimension];
|
|
154
|
+
for (const key of ["soft", "hard"])
|
|
155
|
+
if ((oldLimit?.[key] !== undefined && newLimit?.[key] === undefined) || (oldLimit?.[key] !== undefined && newLimit?.[key] !== undefined && newLimit[key] > oldLimit[key]))
|
|
156
|
+
return true;
|
|
157
|
+
} return false; }
|
|
158
|
+
export function exhaustedBudgetDimensions(budget, usage) { if (!budget)
|
|
159
|
+
return []; return Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.hard !== undefined && usage[dimension] >= limits.hard; }); }
|
|
160
|
+
export function resumeBudgetAllowed(budget, usage) { return exhaustedBudgetDimensions(budget, usage).length === 0; }
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type DoctorOptions } from "./doctor.js";
|
|
3
|
+
import { type DoctorCleanupOptions } from "./doctor-cleanup.js";
|
|
3
4
|
import { type JsonSchema, type JsonValue } from "./index.js";
|
|
4
5
|
import type { WorkflowCatalogFunction } from "./index.js";
|
|
5
6
|
export interface CliOptions extends DoctorOptions {
|
|
@@ -12,4 +13,5 @@ export interface CliOptions extends DoctorOptions {
|
|
|
12
13
|
}
|
|
13
14
|
export declare function formatWorkflowCliHelp(fn: WorkflowCatalogFunction, command?: string): string;
|
|
14
15
|
export declare function parseWorkflowCliArgs(schema: JsonSchema, rawArgs: readonly string[]): Record<string, JsonValue>;
|
|
16
|
+
export declare function parseDoctorCleanupArgs(rawArgs: readonly string[]): Required<Pick<DoctorCleanupOptions, "olderThanDays" | "yes">>;
|
|
15
17
|
export declare function runCli(args: readonly string[], options?: CliOptions, write?: (text: string) => void): Promise<number>;
|
package/dist/src/cli.js
CHANGED
|
@@ -7,7 +7,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
7
7
|
import { ProjectTrustStore, SessionManager, SettingsManager, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Value } from "typebox/value";
|
|
9
9
|
import { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
|
|
10
|
-
import
|
|
10
|
+
import { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
|
|
11
|
+
import workflowExtension, { formatWorkflowProgress, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "./index.js";
|
|
11
12
|
import { runSessionInspector, transcriptFileLines } from "./session-inspector.js";
|
|
12
13
|
function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
13
14
|
function has(value, key) { return Object.prototype.hasOwnProperty.call(value, key); }
|
|
@@ -193,6 +194,34 @@ function launcherHelpLines() {
|
|
|
193
194
|
}
|
|
194
195
|
function workflowUsage() { return [`Usage: pi-extensible-workflows run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
|
|
195
196
|
function exportUsage() { return [`Usage: pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
|
|
197
|
+
export function parseDoctorCleanupArgs(rawArgs) {
|
|
198
|
+
let olderThanDays = 90;
|
|
199
|
+
let yes = false;
|
|
200
|
+
let seenDays = false;
|
|
201
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
202
|
+
const token = rawArgs[index];
|
|
203
|
+
if (token === "--yes") {
|
|
204
|
+
yes = true;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const inline = token.startsWith("--older-than-days=") ? token.slice("--older-than-days=".length) : undefined;
|
|
208
|
+
if (token === "--older-than-days" || inline !== undefined) {
|
|
209
|
+
if (seenDays)
|
|
210
|
+
throw new Error("--older-than-days may only be provided once");
|
|
211
|
+
const raw = inline ?? rawArgs[++index];
|
|
212
|
+
if (raw === undefined || !/^[1-9]\d*$/.test(raw))
|
|
213
|
+
throw new Error("older-than-days must be a positive integer");
|
|
214
|
+
const parsed = Number(raw);
|
|
215
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1)
|
|
216
|
+
throw new Error("older-than-days must be a positive integer");
|
|
217
|
+
olderThanDays = parsed;
|
|
218
|
+
seenDays = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`Unknown cleanup option: ${token}`);
|
|
222
|
+
}
|
|
223
|
+
return { olderThanDays, yes };
|
|
224
|
+
}
|
|
196
225
|
function stripTrustOptions(rawArgs) {
|
|
197
226
|
const args = [];
|
|
198
227
|
let trustOverride;
|
|
@@ -278,7 +307,7 @@ async function createWorkflowRuntime(options, shutdownHandlers = []) {
|
|
|
278
307
|
const workflowTool = tools.find((tool) => object(tool) && tool.name === "workflow");
|
|
279
308
|
if (!workflowTool)
|
|
280
309
|
throw new Error("The workflow runtime could not be initialized");
|
|
281
|
-
return { catalog: workflowCatalog(), services, workflowTool, shutdownHandlers };
|
|
310
|
+
return { catalog: workflowCatalog({ cwd, projectTrusted: settingsManager.isProjectTrusted(), globalSettingsPath: workflowSettingsPath(agentDir) }), services, workflowTool, shutdownHandlers };
|
|
282
311
|
}
|
|
283
312
|
function availableModelInfo(services, available = false) {
|
|
284
313
|
const models = available ? services.modelRuntime.getAvailableSnapshot() : services.modelRuntime.getModels();
|
|
@@ -321,21 +350,27 @@ function writeLauncher(destination, workflowName, force) {
|
|
|
321
350
|
rmSync(tempDir, { recursive: true, force: true });
|
|
322
351
|
}
|
|
323
352
|
}
|
|
353
|
+
function terminalProgressStyles(enabled) {
|
|
354
|
+
const style = (code) => enabled ? (text) => `\x1b[${String(code)}m${text}\x1b[0m` : (text) => text;
|
|
355
|
+
return { accent: style(36), success: style(32), error: style(31), warning: style(33), muted: style(90), dim: style(2), bold: style(1) };
|
|
356
|
+
}
|
|
324
357
|
class CliProgress {
|
|
325
358
|
stderr;
|
|
326
|
-
tty;
|
|
327
359
|
#lastStable = "";
|
|
328
360
|
#lines = 0;
|
|
329
361
|
#frame = 0;
|
|
330
362
|
#run;
|
|
331
363
|
#timer;
|
|
364
|
+
#interactive;
|
|
365
|
+
#styles;
|
|
332
366
|
constructor(stderr, tty) {
|
|
333
367
|
this.stderr = stderr;
|
|
334
|
-
this
|
|
368
|
+
this.#interactive = tty && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
|
|
369
|
+
this.#styles = terminalProgressStyles(this.#interactive);
|
|
335
370
|
}
|
|
336
371
|
update(run) {
|
|
337
|
-
const stable = formatWorkflowProgress(run, "◇");
|
|
338
|
-
if (!this
|
|
372
|
+
const stable = formatWorkflowProgress(run, "◇", this.#styles);
|
|
373
|
+
if (!this.#interactive) {
|
|
339
374
|
if (stable !== this.#lastStable) {
|
|
340
375
|
this.#lastStable = stable;
|
|
341
376
|
this.stderr(`${stable}\n`);
|
|
@@ -352,7 +387,7 @@ class CliProgress {
|
|
|
352
387
|
return;
|
|
353
388
|
const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][this.#frame++ % 10] ?? "◇";
|
|
354
389
|
const width = process.stderr.columns || 80;
|
|
355
|
-
const text = formatWorkflowProgress(this.#run, spinner
|
|
390
|
+
const text = truncateWorkflowProgress(formatWorkflowProgress(this.#run, spinner, this.#styles), width).join("\n");
|
|
356
391
|
this.stderr(`${this.#lines ? `\x1b[${String(this.#lines)}A` : ""}${this.#lines ? "" : "\x1b[?25l"}\x1b[0J${text}\n`);
|
|
357
392
|
this.#lines = text.split("\n").length;
|
|
358
393
|
}
|
|
@@ -361,7 +396,7 @@ class CliProgress {
|
|
|
361
396
|
clearInterval(this.#timer);
|
|
362
397
|
this.#timer = undefined;
|
|
363
398
|
}
|
|
364
|
-
if (this
|
|
399
|
+
if (this.#interactive && this.#lines) {
|
|
365
400
|
this.stderr(`\x1b[${String(this.#lines)}A\x1b[0J\x1b[?25h`);
|
|
366
401
|
this.#lines = 0;
|
|
367
402
|
}
|
|
@@ -506,6 +541,23 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
506
541
|
write(formatDoctorReport(report));
|
|
507
542
|
return doctorExitCode(report);
|
|
508
543
|
}
|
|
544
|
+
if (args[0] === "doctor" && args[1] === "cleanup") {
|
|
545
|
+
if (args.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
|
|
546
|
+
write("Usage: pi-extensible-workflows doctor cleanup [--older-than-days <days>] [--yes]\n");
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const parsed = parseDoctorCleanupArgs(args.slice(2));
|
|
551
|
+
const cleanupOptions = { ...parsed, ...(options.cwd !== undefined ? { cwd: options.cwd } : {}) };
|
|
552
|
+
const report = await doctorCleanup(cleanupOptions);
|
|
553
|
+
write(formatDoctorCleanupReport(report));
|
|
554
|
+
return doctorCleanupExitCode(report);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
stderr(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
558
|
+
return 1;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
509
561
|
if (args[0] === "inspect" && args.length <= 2) {
|
|
510
562
|
try {
|
|
511
563
|
await (options.inspect ?? runSessionInspector)(args[1]);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface DoctorCleanupOptions {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
home?: string;
|
|
4
|
+
olderThanDays?: number;
|
|
5
|
+
yes?: boolean;
|
|
6
|
+
now?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CleanupRunResult {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
runId: string;
|
|
11
|
+
action: "candidate" | "skipped" | "deleted" | "failed";
|
|
12
|
+
state: string;
|
|
13
|
+
stateMtimeMs: number;
|
|
14
|
+
path: string;
|
|
15
|
+
reason?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CleanupFailure {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
runId?: string;
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CleanupSessionReport {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
path: string;
|
|
25
|
+
status: "preview" | "cleaned" | "skipped" | "failed";
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface DoctorCleanupReport {
|
|
29
|
+
cwd: string;
|
|
30
|
+
cutoffMs: number;
|
|
31
|
+
olderThanDays: number;
|
|
32
|
+
yes: boolean;
|
|
33
|
+
sessions: readonly CleanupSessionReport[];
|
|
34
|
+
candidates: readonly CleanupRunResult[];
|
|
35
|
+
skipped: readonly CleanupRunResult[];
|
|
36
|
+
deleted: readonly CleanupRunResult[];
|
|
37
|
+
failures: readonly CleanupFailure[];
|
|
38
|
+
}
|
|
39
|
+
export declare function doctorCleanup(options?: DoctorCleanupOptions): Promise<DoctorCleanupReport>;
|
|
40
|
+
export declare function doctorCleanupExitCode(report: DoctorCleanupReport): 0 | 1;
|
|
41
|
+
export declare function formatDoctorCleanupReport(report: DoctorCleanupReport): string;
|