@quintinshaw/pi-dynamic-workflows 1.0.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 +159 -0
- package/dist/adversarial-review.d.ts +20 -0
- package/dist/adversarial-review.js +87 -0
- package/dist/agent.d.ts +29 -0
- package/dist/agent.js +90 -0
- package/dist/auto-workflow.d.ts +26 -0
- package/dist/auto-workflow.js +121 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +17 -0
- package/dist/deep-research.d.ts +22 -0
- package/dist/deep-research.js +110 -0
- package/dist/display.d.ts +62 -0
- package/dist/display.js +163 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.js +63 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +15 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.js +67 -0
- package/dist/model-routing.d.ts +33 -0
- package/dist/model-routing.js +57 -0
- package/dist/run-persistence.d.ts +53 -0
- package/dist/run-persistence.js +78 -0
- package/dist/structured-output.d.ts +19 -0
- package/dist/structured-output.js +30 -0
- package/dist/workflow-manager.d.ts +74 -0
- package/dist/workflow-manager.js +241 -0
- package/dist/workflow-saved.d.ts +35 -0
- package/dist/workflow-saved.js +91 -0
- package/dist/workflow-tool.d.ts +22 -0
- package/dist/workflow-tool.js +216 -0
- package/dist/workflow.d.ts +75 -0
- package/dist/workflow.js +364 -0
- package/extensions/workflow.ts +14 -0
- package/package.json +70 -0
- package/src/adversarial-review.ts +107 -0
- package/src/agent.ts +135 -0
- package/src/auto-workflow.ts +146 -0
- package/src/config.ts +24 -0
- package/src/deep-research.ts +128 -0
- package/src/display.ts +236 -0
- package/src/errors.ts +85 -0
- package/src/index.ts +55 -0
- package/src/logger.ts +89 -0
- package/src/model-routing.ts +80 -0
- package/src/run-persistence.ts +132 -0
- package/src/structured-output.ts +47 -0
- package/src/workflow-manager.ts +294 -0
- package/src/workflow-saved.ts +131 -0
- package/src/workflow-tool.ts +254 -0
- package/src/workflow.ts +492 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Static, TSchema } from "typebox";
|
|
3
|
+
|
|
4
|
+
export interface StructuredOutputCapture<T = unknown> {
|
|
5
|
+
value: T | undefined;
|
|
6
|
+
called: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface StructuredOutputToolOptions<TSchemaDef extends TSchema> {
|
|
10
|
+
schema: TSchemaDef;
|
|
11
|
+
capture: StructuredOutputCapture<Static<TSchemaDef>>;
|
|
12
|
+
name?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a terminating tool that captures validated params as the subagent result.
|
|
17
|
+
*
|
|
18
|
+
* Pi validates `params` against `schema` before execute() is called. Returning
|
|
19
|
+
* `terminate: true` lets the subagent finish on this tool call without paying for
|
|
20
|
+
* an extra assistant follow-up turn.
|
|
21
|
+
*/
|
|
22
|
+
export function createStructuredOutputTool<TSchemaDef extends TSchema>({
|
|
23
|
+
schema,
|
|
24
|
+
capture,
|
|
25
|
+
name = "structured_output",
|
|
26
|
+
}: StructuredOutputToolOptions<TSchemaDef>): ToolDefinition<TSchemaDef, Static<TSchemaDef>> {
|
|
27
|
+
return defineTool({
|
|
28
|
+
name,
|
|
29
|
+
label: "Structured Output",
|
|
30
|
+
description: "Return the final machine-readable result for this subagent task.",
|
|
31
|
+
promptSnippet: "Return final machine-readable output",
|
|
32
|
+
promptGuidelines: [
|
|
33
|
+
`${name} is the final answer channel for this task; call ${name} exactly once when done.`,
|
|
34
|
+
`Do not write a prose final answer after calling ${name}.`,
|
|
35
|
+
],
|
|
36
|
+
parameters: schema,
|
|
37
|
+
async execute(_toolCallId, params) {
|
|
38
|
+
capture.value = params;
|
|
39
|
+
capture.called = true;
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: "Structured output received." }],
|
|
42
|
+
details: params,
|
|
43
|
+
terminate: true,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow manager for background execution, pause/resume, and run management.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
6
|
+
import type { WorkflowSnapshot } from "./display.js";
|
|
7
|
+
import { WorkflowError, WorkflowErrorCode } from "./errors.js";
|
|
8
|
+
import {
|
|
9
|
+
createRunPersistence,
|
|
10
|
+
generateRunId,
|
|
11
|
+
type PersistedRunState,
|
|
12
|
+
type RunPersistence,
|
|
13
|
+
type RunStatus,
|
|
14
|
+
} from "./run-persistence.js";
|
|
15
|
+
import { parseWorkflowScript, runWorkflow, type WorkflowRunResult } from "./workflow.js";
|
|
16
|
+
|
|
17
|
+
export interface ManagedRun {
|
|
18
|
+
runId: string;
|
|
19
|
+
status: RunStatus;
|
|
20
|
+
snapshot: WorkflowSnapshot;
|
|
21
|
+
result?: WorkflowRunResult;
|
|
22
|
+
error?: WorkflowError;
|
|
23
|
+
controller: AbortController;
|
|
24
|
+
startedAt: Date;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WorkflowManagerOptions {
|
|
28
|
+
cwd?: string;
|
|
29
|
+
concurrency?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class WorkflowManager extends EventEmitter {
|
|
33
|
+
private runs = new Map<string, ManagedRun>();
|
|
34
|
+
private persistence: RunPersistence;
|
|
35
|
+
private cwd: string;
|
|
36
|
+
private concurrency: number;
|
|
37
|
+
|
|
38
|
+
constructor(options: WorkflowManagerOptions = {}) {
|
|
39
|
+
super();
|
|
40
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
41
|
+
this.concurrency = options.concurrency ?? 8;
|
|
42
|
+
this.persistence = createRunPersistence(this.cwd);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Start a workflow in the background.
|
|
47
|
+
* Returns immediately with a run ID; the workflow executes asynchronously.
|
|
48
|
+
*/
|
|
49
|
+
startInBackground(script: string, args?: unknown): { runId: string; promise: Promise<WorkflowRunResult> } {
|
|
50
|
+
const runId = generateRunId();
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const parsed = parseWorkflowScript(script);
|
|
53
|
+
|
|
54
|
+
const managed: ManagedRun = {
|
|
55
|
+
runId,
|
|
56
|
+
status: "running",
|
|
57
|
+
snapshot: {
|
|
58
|
+
name: parsed.meta.name,
|
|
59
|
+
description: parsed.meta.description,
|
|
60
|
+
phases: parsed.meta.phases?.map((p) => p.title) ?? [],
|
|
61
|
+
logs: [],
|
|
62
|
+
agents: [],
|
|
63
|
+
agentCount: 0,
|
|
64
|
+
runningCount: 0,
|
|
65
|
+
doneCount: 0,
|
|
66
|
+
errorCount: 0,
|
|
67
|
+
},
|
|
68
|
+
controller,
|
|
69
|
+
startedAt: new Date(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.runs.set(runId, managed);
|
|
73
|
+
|
|
74
|
+
// Persist initial state
|
|
75
|
+
this.persistence.save({
|
|
76
|
+
runId,
|
|
77
|
+
workflowName: parsed.meta.name,
|
|
78
|
+
script,
|
|
79
|
+
args,
|
|
80
|
+
status: "running",
|
|
81
|
+
phases: managed.snapshot.phases,
|
|
82
|
+
agents: [],
|
|
83
|
+
logs: [],
|
|
84
|
+
startedAt: managed.startedAt.toISOString(),
|
|
85
|
+
updatedAt: managed.startedAt.toISOString(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Run workflow asynchronously
|
|
89
|
+
const promise = this.executeRun(managed, script, args);
|
|
90
|
+
|
|
91
|
+
return { runId, promise };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Execute a workflow synchronously (blocking).
|
|
96
|
+
*/
|
|
97
|
+
async runSync(script: string, args?: unknown): Promise<WorkflowRunResult> {
|
|
98
|
+
const runId = generateRunId();
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const parsed = parseWorkflowScript(script);
|
|
101
|
+
|
|
102
|
+
const managed: ManagedRun = {
|
|
103
|
+
runId,
|
|
104
|
+
status: "running",
|
|
105
|
+
snapshot: {
|
|
106
|
+
name: parsed.meta.name,
|
|
107
|
+
description: parsed.meta.description,
|
|
108
|
+
phases: parsed.meta.phases?.map((p) => p.title) ?? [],
|
|
109
|
+
logs: [],
|
|
110
|
+
agents: [],
|
|
111
|
+
agentCount: 0,
|
|
112
|
+
runningCount: 0,
|
|
113
|
+
doneCount: 0,
|
|
114
|
+
errorCount: 0,
|
|
115
|
+
},
|
|
116
|
+
controller,
|
|
117
|
+
startedAt: new Date(),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
this.runs.set(runId, managed);
|
|
121
|
+
return this.executeRun(managed, script, args);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async executeRun(managed: ManagedRun, script: string, args?: unknown): Promise<WorkflowRunResult> {
|
|
125
|
+
try {
|
|
126
|
+
const result = await runWorkflow(script, {
|
|
127
|
+
cwd: this.cwd,
|
|
128
|
+
args,
|
|
129
|
+
signal: managed.controller.signal,
|
|
130
|
+
concurrency: this.concurrency,
|
|
131
|
+
onLog: (message) => {
|
|
132
|
+
managed.snapshot.logs.push(message);
|
|
133
|
+
this.emit("log", { runId: managed.runId, message });
|
|
134
|
+
},
|
|
135
|
+
onPhase: (title) => {
|
|
136
|
+
managed.snapshot.currentPhase = title;
|
|
137
|
+
if (!managed.snapshot.phases.includes(title)) {
|
|
138
|
+
managed.snapshot.phases.push(title);
|
|
139
|
+
}
|
|
140
|
+
this.emit("phase", { runId: managed.runId, title });
|
|
141
|
+
},
|
|
142
|
+
onAgentStart: (event) => {
|
|
143
|
+
managed.snapshot.agents.push({
|
|
144
|
+
id: managed.snapshot.agents.length + 1,
|
|
145
|
+
label: event.label,
|
|
146
|
+
phase: event.phase,
|
|
147
|
+
prompt: event.prompt,
|
|
148
|
+
status: "running",
|
|
149
|
+
});
|
|
150
|
+
this.emit("agentStart", { runId: managed.runId, ...event });
|
|
151
|
+
},
|
|
152
|
+
onAgentEnd: (event) => {
|
|
153
|
+
const agent = [...managed.snapshot.agents]
|
|
154
|
+
.reverse()
|
|
155
|
+
.find((a) => a.label === event.label && a.status === "running");
|
|
156
|
+
if (agent) {
|
|
157
|
+
agent.status = event.result === null ? "error" : "done";
|
|
158
|
+
}
|
|
159
|
+
this.emit("agentEnd", { runId: managed.runId, ...event });
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
managed.status = "completed";
|
|
164
|
+
managed.result = result;
|
|
165
|
+
this.emit("complete", { runId: managed.runId, result });
|
|
166
|
+
|
|
167
|
+
// Persist final state
|
|
168
|
+
this.persistRun(managed);
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const workflowError =
|
|
173
|
+
error instanceof WorkflowError
|
|
174
|
+
? error
|
|
175
|
+
: new WorkflowError(
|
|
176
|
+
error instanceof Error ? error.message : String(error),
|
|
177
|
+
WorkflowErrorCode.WORKFLOW_ABORTED,
|
|
178
|
+
{ recoverable: true },
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (managed.controller.signal.aborted) {
|
|
182
|
+
managed.status = "aborted";
|
|
183
|
+
} else {
|
|
184
|
+
managed.status = "failed";
|
|
185
|
+
}
|
|
186
|
+
managed.error = workflowError;
|
|
187
|
+
this.emit("error", { runId: managed.runId, error: workflowError });
|
|
188
|
+
|
|
189
|
+
// Persist final state
|
|
190
|
+
this.persistRun(managed);
|
|
191
|
+
|
|
192
|
+
throw workflowError;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private persistRun(managed: ManagedRun) {
|
|
197
|
+
this.persistence.save({
|
|
198
|
+
runId: managed.runId,
|
|
199
|
+
workflowName: managed.snapshot.name,
|
|
200
|
+
script: "", // Don't persist script for security
|
|
201
|
+
status: managed.status,
|
|
202
|
+
phases: managed.snapshot.phases,
|
|
203
|
+
currentPhase: managed.snapshot.currentPhase,
|
|
204
|
+
agents: managed.snapshot.agents.map((a) => ({
|
|
205
|
+
...a,
|
|
206
|
+
startedAt: managed.startedAt.toISOString(),
|
|
207
|
+
endedAt: new Date().toISOString(),
|
|
208
|
+
})),
|
|
209
|
+
logs: managed.snapshot.logs,
|
|
210
|
+
result: managed.result?.result,
|
|
211
|
+
startedAt: managed.startedAt.toISOString(),
|
|
212
|
+
updatedAt: new Date().toISOString(),
|
|
213
|
+
completedAt: managed.status === "completed" ? new Date().toISOString() : undefined,
|
|
214
|
+
durationMs: managed.result?.durationMs,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Pause a running workflow.
|
|
220
|
+
*/
|
|
221
|
+
pause(runId: string): boolean {
|
|
222
|
+
const managed = this.runs.get(runId);
|
|
223
|
+
if (managed?.status !== "running") return false;
|
|
224
|
+
|
|
225
|
+
managed.controller.abort();
|
|
226
|
+
managed.status = "paused";
|
|
227
|
+
this.emit("paused", { runId });
|
|
228
|
+
this.persistRun(managed);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Resume a paused workflow.
|
|
234
|
+
*/
|
|
235
|
+
async resume(runId: string): Promise<boolean> {
|
|
236
|
+
const persisted = this.persistence.load(runId);
|
|
237
|
+
if (persisted?.status !== "paused") return false;
|
|
238
|
+
|
|
239
|
+
// For now, resume creates a fresh run with completed agents' results cached
|
|
240
|
+
// Full resume would require re-executing the script with cached results
|
|
241
|
+
this.emit("resumed", { runId });
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Stop a running workflow.
|
|
247
|
+
*/
|
|
248
|
+
stop(runId: string): boolean {
|
|
249
|
+
const managed = this.runs.get(runId);
|
|
250
|
+
if (!managed || (managed.status !== "running" && managed.status !== "paused")) return false;
|
|
251
|
+
|
|
252
|
+
managed.controller.abort();
|
|
253
|
+
managed.status = "aborted";
|
|
254
|
+
this.emit("stopped", { runId });
|
|
255
|
+
this.persistRun(managed);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get status of a specific run.
|
|
261
|
+
*/
|
|
262
|
+
getRun(runId: string): ManagedRun | undefined {
|
|
263
|
+
return this.runs.get(runId);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* List all runs (active + persisted).
|
|
268
|
+
*/
|
|
269
|
+
listRuns(): PersistedRunState[] {
|
|
270
|
+
return this.persistence.list();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get snapshot of a run.
|
|
275
|
+
*/
|
|
276
|
+
getSnapshot(runId: string): WorkflowSnapshot | null {
|
|
277
|
+
return this.runs.get(runId)?.snapshot ?? null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Delete a persisted run.
|
|
282
|
+
*/
|
|
283
|
+
deleteRun(runId: string): boolean {
|
|
284
|
+
this.runs.delete(runId);
|
|
285
|
+
return this.persistence.delete(runId);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get the persistence layer (for saving workflows).
|
|
290
|
+
*/
|
|
291
|
+
getPersistence(): RunPersistence {
|
|
292
|
+
return this.persistence;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save and load reusable workflow commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { USER_WORKFLOW_SAVED_DIR, WORKFLOW_SAVED_DIR } from "./config.js";
|
|
8
|
+
|
|
9
|
+
export interface SavedWorkflow {
|
|
10
|
+
/** Command name (filename without extension). */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Human-readable description. */
|
|
13
|
+
description: string;
|
|
14
|
+
/** The workflow script. */
|
|
15
|
+
script: string;
|
|
16
|
+
/** Optional parameter schema for parameterized workflows. */
|
|
17
|
+
parameters?: Record<string, { type: string; description?: string; required?: boolean; default?: unknown }>;
|
|
18
|
+
/** Where this workflow is saved. */
|
|
19
|
+
location: "project" | "user";
|
|
20
|
+
/** Full file path. */
|
|
21
|
+
path: string;
|
|
22
|
+
/** When it was saved. */
|
|
23
|
+
savedAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WorkflowStorage {
|
|
27
|
+
/** Save a workflow. */
|
|
28
|
+
save(workflow: Omit<SavedWorkflow, "path" | "savedAt">, location?: "project" | "user"): SavedWorkflow;
|
|
29
|
+
/** Load a workflow by name. */
|
|
30
|
+
load(name: string): SavedWorkflow | null;
|
|
31
|
+
/** List all saved workflows. */
|
|
32
|
+
list(): SavedWorkflow[];
|
|
33
|
+
/** Delete a saved workflow. */
|
|
34
|
+
delete(name: string, location?: "project" | "user"): boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createWorkflowStorage(cwd: string): WorkflowStorage {
|
|
38
|
+
const projectDir = join(cwd, WORKFLOW_SAVED_DIR);
|
|
39
|
+
const userDir = USER_WORKFLOW_SAVED_DIR.replace("~", process.env.HOME ?? "");
|
|
40
|
+
|
|
41
|
+
const ensureDir = (dir: string) => {
|
|
42
|
+
if (!existsSync(dir)) {
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const workflowPath = (name: string, location: "project" | "user") => {
|
|
48
|
+
const dir = location === "project" ? projectDir : userDir;
|
|
49
|
+
return join(dir, `${name}.json`);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const loadFromFile = (path: string, location: "project" | "user"): SavedWorkflow | null => {
|
|
53
|
+
try {
|
|
54
|
+
if (!existsSync(path)) return null;
|
|
55
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
56
|
+
return {
|
|
57
|
+
...data,
|
|
58
|
+
location,
|
|
59
|
+
path,
|
|
60
|
+
};
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
save(workflow, location = "project") {
|
|
68
|
+
const dir = location === "project" ? projectDir : userDir;
|
|
69
|
+
ensureDir(dir);
|
|
70
|
+
|
|
71
|
+
const path = workflowPath(workflow.name, location);
|
|
72
|
+
const saved: SavedWorkflow = {
|
|
73
|
+
...workflow,
|
|
74
|
+
location,
|
|
75
|
+
path,
|
|
76
|
+
savedAt: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
writeFileSync(path, JSON.stringify(saved, null, 2));
|
|
80
|
+
return saved;
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
load(name: string): SavedWorkflow | null {
|
|
84
|
+
// Project takes precedence over user
|
|
85
|
+
const projectPath = workflowPath(name, "project");
|
|
86
|
+
const project = loadFromFile(projectPath, "project");
|
|
87
|
+
if (project) return project;
|
|
88
|
+
|
|
89
|
+
const userPath = workflowPath(name, "user");
|
|
90
|
+
return loadFromFile(userPath, "user");
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
list(): SavedWorkflow[] {
|
|
94
|
+
const workflows: SavedWorkflow[] = [];
|
|
95
|
+
|
|
96
|
+
// Load project workflows
|
|
97
|
+
if (existsSync(projectDir)) {
|
|
98
|
+
for (const file of readdirSync(projectDir).filter((f) => f.endsWith(".json"))) {
|
|
99
|
+
const wf = loadFromFile(join(projectDir, file), "project");
|
|
100
|
+
if (wf) workflows.push(wf);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Load user workflows
|
|
105
|
+
if (existsSync(userDir)) {
|
|
106
|
+
for (const file of readdirSync(userDir).filter((f) => f.endsWith(".json"))) {
|
|
107
|
+
const wf = loadFromFile(join(userDir, file), "user");
|
|
108
|
+
if (wf) workflows.push(wf);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return workflows.sort((a, b) => a.name.localeCompare(b.name));
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
delete(name: string, location?: "project" | "user"): boolean {
|
|
116
|
+
const locations = location ? [location] : (["project", "user"] as const);
|
|
117
|
+
let deleted = false;
|
|
118
|
+
|
|
119
|
+
for (const loc of locations) {
|
|
120
|
+
const path = workflowPath(name, loc);
|
|
121
|
+
if (existsSync(path)) {
|
|
122
|
+
const { unlinkSync } = require("node:fs");
|
|
123
|
+
unlinkSync(path);
|
|
124
|
+
deleted = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return deleted;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|