@ryanfw/prompt-orchestration-pipeline 0.0.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +415 -24
  2. package/package.json +45 -8
  3. package/src/api/files.js +48 -0
  4. package/src/api/index.js +149 -53
  5. package/src/api/validators/seed.js +141 -0
  6. package/src/cli/index.js +456 -29
  7. package/src/cli/run-orchestrator.js +39 -0
  8. package/src/cli/update-pipeline-json.js +47 -0
  9. package/src/components/DAGGrid.jsx +649 -0
  10. package/src/components/JobCard.jsx +96 -0
  11. package/src/components/JobDetail.jsx +159 -0
  12. package/src/components/JobTable.jsx +202 -0
  13. package/src/components/Layout.jsx +134 -0
  14. package/src/components/TaskFilePane.jsx +570 -0
  15. package/src/components/UploadSeed.jsx +239 -0
  16. package/src/components/ui/badge.jsx +20 -0
  17. package/src/components/ui/button.jsx +43 -0
  18. package/src/components/ui/card.jsx +20 -0
  19. package/src/components/ui/focus-styles.css +60 -0
  20. package/src/components/ui/progress.jsx +26 -0
  21. package/src/components/ui/select.jsx +27 -0
  22. package/src/components/ui/separator.jsx +6 -0
  23. package/src/config/paths.js +99 -0
  24. package/src/core/config.js +270 -9
  25. package/src/core/file-io.js +202 -0
  26. package/src/core/module-loader.js +157 -0
  27. package/src/core/orchestrator.js +275 -294
  28. package/src/core/pipeline-runner.js +95 -41
  29. package/src/core/progress.js +66 -0
  30. package/src/core/status-writer.js +331 -0
  31. package/src/core/task-runner.js +719 -73
  32. package/src/core/validation.js +120 -1
  33. package/src/lib/utils.js +6 -0
  34. package/src/llm/README.md +139 -30
  35. package/src/llm/index.js +222 -72
  36. package/src/pages/PipelineDetail.jsx +111 -0
  37. package/src/pages/PromptPipelineDashboard.jsx +223 -0
  38. package/src/providers/deepseek.js +3 -15
  39. package/src/ui/client/adapters/job-adapter.js +258 -0
  40. package/src/ui/client/bootstrap.js +120 -0
  41. package/src/ui/client/hooks/useJobDetailWithUpdates.js +619 -0
  42. package/src/ui/client/hooks/useJobList.js +50 -0
  43. package/src/ui/client/hooks/useJobListWithUpdates.js +335 -0
  44. package/src/ui/client/hooks/useTicker.js +26 -0
  45. package/src/ui/client/index.css +31 -0
  46. package/src/ui/client/index.html +18 -0
  47. package/src/ui/client/main.jsx +38 -0
  48. package/src/ui/config-bridge.browser.js +149 -0
  49. package/src/ui/config-bridge.js +149 -0
  50. package/src/ui/config-bridge.node.js +310 -0
  51. package/src/ui/dist/assets/index-BDABnI-4.js +33399 -0
  52. package/src/ui/dist/assets/style-Ks8LY8gB.css +28496 -0
  53. package/src/ui/dist/index.html +19 -0
  54. package/src/ui/endpoints/job-endpoints.js +300 -0
  55. package/src/ui/file-reader.js +216 -0
  56. package/src/ui/job-change-detector.js +83 -0
  57. package/src/ui/job-index.js +231 -0
  58. package/src/ui/job-reader.js +274 -0
  59. package/src/ui/job-scanner.js +188 -0
  60. package/src/ui/public/app.js +3 -1
  61. package/src/ui/server.js +1636 -59
  62. package/src/ui/sse-enhancer.js +149 -0
  63. package/src/ui/sse.js +204 -0
  64. package/src/ui/state-snapshot.js +252 -0
  65. package/src/ui/transformers/list-transformer.js +347 -0
  66. package/src/ui/transformers/status-transformer.js +307 -0
  67. package/src/ui/watcher.js +61 -7
  68. package/src/utils/dag.js +101 -0
  69. package/src/utils/duration.js +126 -0
  70. package/src/utils/id-generator.js +30 -0
  71. package/src/utils/jobs.js +7 -0
  72. package/src/utils/pipelines.js +44 -0
  73. package/src/utils/task-files.js +271 -0
  74. package/src/utils/ui.jsx +76 -0
  75. package/src/ui/public/index.html +0 -53
  76. package/src/ui/public/style.css +0 -341
package/src/cli/index.js CHANGED
@@ -1,63 +1,298 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
+ import { submitJobWithValidation } from "../api/index.js";
3
4
  import { PipelineOrchestrator } from "../api/index.js";
4
5
  import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { spawn } from "node:child_process";
8
+ import { updatePipelineJson } from "./update-pipeline-json.js";
9
+
10
+ // Canonical stage names that must match src/core/task-runner.js
11
+ const STAGE_NAMES = [
12
+ "ingestion",
13
+ "preProcessing",
14
+ "promptTemplating",
15
+ "inference",
16
+ "parsing",
17
+ "validateStructure",
18
+ "validateQuality",
19
+ "critique",
20
+ "refine",
21
+ "finalValidation",
22
+ "integration",
23
+ ];
5
24
 
6
25
  const program = new Command();
7
26
 
8
27
  program
9
28
  .name("pipeline-orchestrator")
10
29
  .description("Pipeline orchestration system")
11
- .version("1.0.0");
30
+ .version("1.0.0")
31
+ .option("-r, --root <path>", "Pipeline root (PO_ROOT)")
32
+ .option("-p, --port <port>", "UI server port", "4000");
12
33
 
13
34
  program
14
35
  .command("init")
15
36
  .description("Initialize pipeline configuration")
16
37
  .action(async () => {
17
- const template = {
18
- pipeline: { name: "my-pipeline", version: "1.0.0", tasks: ["example-task"] },
19
- tasks: {
20
- "example-task": {
21
- `ingestion`: `export async function ingestion(context) { return { data: "example" }; }`,
22
- `inference`: `export async function inference(context) { return { output: context.data }; }`
23
- }
24
- };
25
- await fs.mkdir("pipeline-config/tasks/example-task", { recursive: true });
26
- await fs.writeFile("pipeline-config/pipeline.json", JSON.stringify(template.pipeline, null, 2));
27
- await fs.writeFile("pipeline-config/tasks/index.js", `export default {\n 'example-task': './example-task/index.js'\n};`);
28
- await fs.writeFile("pipeline-config/tasks/example-task/index.js", `${template.tasks["example-task"].ingestion}\n\n${template.tasks["example-task"].inference}\n`);
29
- console.log("Pipeline configuration initialized");
38
+ const globalOptions = program.opts();
39
+ const root = globalOptions.root || path.resolve(process.cwd(), "pipelines");
40
+
41
+ // Create directories
42
+ await fs.mkdir(path.join(root, "pipeline-config"), { recursive: true });
43
+ await fs.mkdir(path.join(root, "pipeline-data", "pending"), {
44
+ recursive: true,
45
+ });
46
+ await fs.mkdir(path.join(root, "pipeline-data", "current"), {
47
+ recursive: true,
48
+ });
49
+ await fs.mkdir(path.join(root, "pipeline-data", "complete"), {
50
+ recursive: true,
51
+ });
52
+ await fs.mkdir(path.join(root, "pipeline-data", "rejected"), {
53
+ recursive: true,
54
+ });
55
+
56
+ // Create .gitkeep files
57
+ await fs.writeFile(
58
+ path.join(root, "pipeline-data", "pending", ".gitkeep"),
59
+ ""
60
+ );
61
+ await fs.writeFile(
62
+ path.join(root, "pipeline-data", "current", ".gitkeep"),
63
+ ""
64
+ );
65
+ await fs.writeFile(
66
+ path.join(root, "pipeline-data", "complete", ".gitkeep"),
67
+ ""
68
+ );
69
+ await fs.writeFile(
70
+ path.join(root, "pipeline-data", "rejected", ".gitkeep"),
71
+ ""
72
+ );
73
+
74
+ // Write registry.json with exact required content
75
+ const registryContent = { pipelines: {} };
76
+ await fs.writeFile(
77
+ path.join(root, "pipeline-config", "registry.json"),
78
+ JSON.stringify(registryContent, null, 2) + "\n"
79
+ );
80
+
81
+ console.log(`Pipeline configuration initialized at ${root}`);
30
82
  });
31
83
 
32
84
  program
33
85
  .command("start")
34
- .description("Start the pipeline orchestrator")
35
- .option("-u, --ui", "Start with UI server")
36
- .option("-p, --port <port>", "UI server port", "3000")
37
- .action(async (options) => {
38
- const orchestrator = new PipelineOrchestrator({ ui: options.ui, uiPort: parseInt(options.port) });
39
- await orchestrator.initialize();
40
- console.log("Pipeline orchestrator started");
41
- process.on("SIGINT", async () => { await orchestrator.stop(); process.exit(0); });
86
+ .description("Start the pipeline orchestrator with UI server")
87
+ .action(async () => {
88
+ const globalOptions = program.opts();
89
+ let root = globalOptions.root || process.env.PO_ROOT;
90
+ const port = globalOptions.port || "4000";
91
+
92
+ // Resolve absolute root path
93
+ if (!root) {
94
+ console.error(
95
+ "PO_ROOT is required. Use --root or set PO_ROOT to your pipeline root (e.g., ./demo)."
96
+ );
97
+ process.exit(1);
98
+ }
99
+
100
+ const absoluteRoot = path.isAbsolute(root)
101
+ ? root
102
+ : path.resolve(process.cwd(), root);
103
+ process.env.PO_ROOT = absoluteRoot;
104
+
105
+ console.log(`Using PO_ROOT=${absoluteRoot}`);
106
+ console.log(`UI port=${port}`);
107
+
108
+ let uiChild = null;
109
+ let orchestratorChild = null;
110
+ let childrenExited = 0;
111
+ let exitCode = 0;
112
+
113
+ // Cleanup function to kill remaining children
114
+ const cleanup = () => {
115
+ if (uiChild && !uiChild.killed) {
116
+ uiChild.kill("SIGTERM");
117
+ setTimeout(() => {
118
+ if (!uiChild.killed) uiChild.kill("SIGKILL");
119
+ }, 5000);
120
+ }
121
+ if (orchestratorChild && !orchestratorChild.killed) {
122
+ orchestratorChild.kill("SIGTERM");
123
+ setTimeout(() => {
124
+ if (!orchestratorChild.killed) orchestratorChild.kill("SIGKILL");
125
+ }, 5000);
126
+ }
127
+ };
128
+
129
+ // Handle parent process signals
130
+ process.on("SIGINT", () => {
131
+ console.log("\nReceived SIGINT, shutting down...");
132
+ cleanup();
133
+ process.exit(exitCode);
134
+ });
135
+
136
+ process.on("SIGTERM", () => {
137
+ console.log("\nReceived SIGTERM, shutting down...");
138
+ cleanup();
139
+ process.exit(exitCode);
140
+ });
141
+
142
+ try {
143
+ // Step d: Build UI once if dist/ doesn't exist
144
+ const distPath = path.join(process.cwd(), "dist");
145
+ try {
146
+ await fs.access(distPath);
147
+ console.log("UI build found, skipping build step");
148
+ } catch {
149
+ console.log("Building UI...");
150
+ await new Promise((resolve, reject) => {
151
+ const vitePath = path.resolve(
152
+ process.cwd(),
153
+ "node_modules/vite/bin/vite.js"
154
+ );
155
+ const buildChild = spawn("node", [vitePath, "build"], {
156
+ stdio: "inherit",
157
+ env: { ...process.env, NODE_ENV: "development" },
158
+ });
159
+
160
+ buildChild.on("exit", (code) => {
161
+ if (code === 0) {
162
+ console.log("UI build completed");
163
+ resolve();
164
+ } else {
165
+ reject(new Error(`UI build failed with code ${code}`));
166
+ }
167
+ });
168
+
169
+ buildChild.on("error", reject);
170
+ });
171
+ }
172
+
173
+ // Step e: Spawn UI server
174
+ console.log("Starting UI server...");
175
+ const uiServerPath = path.resolve(process.cwd(), "src/ui/server.js");
176
+ uiChild = spawn("node", [uiServerPath], {
177
+ stdio: "pipe",
178
+ env: {
179
+ ...process.env,
180
+ NODE_ENV: "production",
181
+ PO_ROOT: absoluteRoot,
182
+ PO_UI_PORT: port,
183
+ },
184
+ });
185
+
186
+ // Pipe UI output with prefix
187
+ uiChild.stdout.on("data", (data) => {
188
+ console.log(`[ui] ${data.toString().trim()}`);
189
+ });
190
+
191
+ uiChild.stderr.on("data", (data) => {
192
+ console.error(`[ui] ${data.toString().trim()}`);
193
+ });
194
+
195
+ // Step f: Spawn orchestrator
196
+ console.log("Starting orchestrator...");
197
+ const orchestratorPath = path.resolve(
198
+ process.cwd(),
199
+ "src/cli/run-orchestrator.js"
200
+ );
201
+ orchestratorChild = spawn("node", [orchestratorPath], {
202
+ stdio: "pipe",
203
+ env: {
204
+ ...process.env,
205
+ NODE_ENV: "production",
206
+ PO_ROOT: absoluteRoot,
207
+ },
208
+ });
209
+
210
+ // Pipe orchestrator output with prefix
211
+ orchestratorChild.stdout.on("data", (data) => {
212
+ console.log(`[orc] ${data.toString().trim()}`);
213
+ });
214
+
215
+ orchestratorChild.stderr.on("data", (data) => {
216
+ console.error(`[orc] ${data.toString().trim()}`);
217
+ });
218
+
219
+ // Step h: Kill-others-on-fail behavior
220
+ const handleChildExit = (child, name) => {
221
+ return (code, signal) => {
222
+ console.log(
223
+ `${name} process exited with code ${code}, signal ${signal}`
224
+ );
225
+ childrenExited++;
226
+
227
+ if (code !== 0) {
228
+ exitCode = code;
229
+ console.log(`${name} failed, terminating other process...`);
230
+ cleanup();
231
+ }
232
+
233
+ if (childrenExited === 2 || (code !== 0 && childrenExited === 1)) {
234
+ process.exit(exitCode);
235
+ }
236
+ };
237
+ };
238
+
239
+ uiChild.on("exit", handleChildExit(uiChild, "UI"));
240
+ orchestratorChild.on(
241
+ "exit",
242
+ handleChildExit(orchestratorChild, "Orchestrator")
243
+ );
244
+
245
+ // Handle child process errors
246
+ uiChild.on("error", (error) => {
247
+ console.error(`UI process error: ${error.message}`);
248
+ exitCode = 1;
249
+ cleanup();
250
+ process.exit(1);
251
+ });
252
+
253
+ orchestratorChild.on("error", (error) => {
254
+ console.error(`Orchestrator process error: ${error.message}`);
255
+ exitCode = 1;
256
+ cleanup();
257
+ process.exit(1);
258
+ });
259
+ } catch (error) {
260
+ console.error(`Failed to start pipeline: ${error.message}`);
261
+ cleanup();
262
+ process.exit(1);
263
+ }
42
264
  });
43
265
 
44
266
  program
45
267
  .command("submit <seed-file>")
46
268
  .description("Submit a new job")
47
269
  .action(async (seedFile) => {
48
- const seed = JSON.parse(await fs.readFile(seedFile, "utf8"));
49
- const orchestrator = new PipelineOrchestrator({ autoStart: false });
50
- await orchestrator.initialize();
51
- const job = await orchestrator.submitJob(seed);
52
- console.log(`Job submitted: ${job.name}`);
270
+ try {
271
+ const seed = JSON.parse(await fs.readFile(seedFile, "utf8"));
272
+ const result = await submitJobWithValidation({
273
+ dataDir: process.cwd(),
274
+ seedObject: seed,
275
+ });
276
+
277
+ if (result.success) {
278
+ console.log(`Job submitted: ${result.jobId} (${result.jobName})`);
279
+ } else {
280
+ console.error(`Failed to submit job: ${result.message}`);
281
+ process.exit(1);
282
+ }
283
+ } catch (error) {
284
+ console.error(`Error submitting job: ${error.message}`);
285
+ process.exit(1);
286
+ }
53
287
  });
54
288
 
55
289
  program
56
290
  .command("status [job-name]")
57
291
  .description("Get job status")
58
292
  .action(async (jobName) => {
59
- const orchestrator = new PipelineOrchestrator({ autoStart: false });
60
- await orchestrator.initialize();
293
+ const orchestrator = await PipelineOrchestrator.create({
294
+ autoStart: false,
295
+ });
61
296
  if (jobName) {
62
297
  const status = await orchestrator.getStatus(jobName);
63
298
  console.log(JSON.stringify(status, null, 2));
@@ -67,4 +302,196 @@ program
67
302
  }
68
303
  });
69
304
 
305
+ program
306
+ .command("add-pipeline <pipeline-slug>")
307
+ .description("Add a new pipeline configuration")
308
+ .action(async (pipelineSlug) => {
309
+ const globalOptions = program.opts();
310
+ const root = globalOptions.root || path.resolve(process.cwd(), "pipelines");
311
+
312
+ // Validate pipeline-slug is kebab-case
313
+ const kebabCaseRegex = /^[a-z0-9-]+$/;
314
+ if (!kebabCaseRegex.test(pipelineSlug)) {
315
+ console.error("Invalid pipeline slug: must be kebab-case (a-z0-9-)");
316
+ process.exit(1);
317
+ }
318
+
319
+ try {
320
+ // Ensure directories exist
321
+ const pipelineConfigDir = path.join(
322
+ root,
323
+ "pipeline-config",
324
+ pipelineSlug
325
+ );
326
+ const tasksDir = path.join(pipelineConfigDir, "tasks");
327
+ await fs.mkdir(tasksDir, { recursive: true });
328
+
329
+ // Write pipeline.json
330
+ const pipelineConfig = {
331
+ name: pipelineSlug,
332
+ version: "1.0.0",
333
+ description: "New pipeline",
334
+ tasks: [],
335
+ };
336
+ await fs.writeFile(
337
+ path.join(pipelineConfigDir, "pipeline.json"),
338
+ JSON.stringify(pipelineConfig, null, 2) + "\n"
339
+ );
340
+
341
+ // Write tasks/index.js
342
+ await fs.writeFile(
343
+ path.join(tasksDir, "index.js"),
344
+ "export default {};\n"
345
+ );
346
+
347
+ // Update registry.json
348
+ const registryPath = path.join(root, "pipeline-config", "registry.json");
349
+ let registry = { pipelines: {} };
350
+
351
+ try {
352
+ const registryContent = await fs.readFile(registryPath, "utf8");
353
+ registry = JSON.parse(registryContent);
354
+ if (!registry.pipelines) {
355
+ registry.pipelines = {};
356
+ }
357
+ } catch (error) {
358
+ // If registry doesn't exist or is invalid, use empty registry
359
+ registry = { pipelines: {} };
360
+ }
361
+
362
+ // Add/replace pipeline entry
363
+ registry.pipelines[pipelineSlug] = {
364
+ name: pipelineSlug,
365
+ description: "New pipeline",
366
+ pipelinePath: `pipeline-config/${pipelineSlug}/pipeline.json`,
367
+ taskRegistryPath: `pipeline-config/${pipelineSlug}/tasks/index.js`,
368
+ };
369
+
370
+ // Write back registry
371
+ await fs.writeFile(
372
+ registryPath,
373
+ JSON.stringify(registry, null, 2) + "\n"
374
+ );
375
+
376
+ console.log(`Pipeline "${pipelineSlug}" added successfully`);
377
+ } catch (error) {
378
+ console.error(`Error adding pipeline: ${error.message}`);
379
+ process.exit(1);
380
+ }
381
+ });
382
+
383
+ program
384
+ .command("add-pipeline-task <pipeline-slug> <task-slug>")
385
+ .description("Add a new task to a pipeline")
386
+ .action(async (pipelineSlug, taskSlug) => {
387
+ const globalOptions = program.opts();
388
+ const root = globalOptions.root || path.resolve(process.cwd(), "pipelines");
389
+
390
+ // Validate both slugs are kebab-case
391
+ const kebabCaseRegex = /^[a-z0-9-]+$/;
392
+ if (!kebabCaseRegex.test(pipelineSlug)) {
393
+ console.error("Invalid pipeline slug: must be kebab-case (a-z0-9-)");
394
+ process.exit(1);
395
+ }
396
+ if (!kebabCaseRegex.test(taskSlug)) {
397
+ console.error("Invalid task slug: must be kebab-case (a-z0-9-)");
398
+ process.exit(1);
399
+ }
400
+
401
+ // Check if pipeline tasks directory exists
402
+ const tasksDir = path.join(root, "pipeline-config", pipelineSlug, "tasks");
403
+ try {
404
+ await fs.access(tasksDir);
405
+ } catch (error) {
406
+ console.error(
407
+ `Pipeline "${pipelineSlug}" not found. Run add-pipeline first.`
408
+ );
409
+ process.exit(1);
410
+ }
411
+
412
+ try {
413
+ // Create task file with all stage exports
414
+ const taskFileContent = STAGE_NAMES.map((stageName) => {
415
+ if (stageName === "ingestion") {
416
+ return `// Step 1: Ingestion, ${getStagePurpose(stageName)}
417
+ export const ingestion = async ({ io, llm, data: { seed }, meta, flags }) => {
418
+
419
+ return { output: {}, flags };
420
+ }`;
421
+ }
422
+ const stepNumber = STAGE_NAMES.indexOf(stageName) + 1;
423
+ return `// Step ${stepNumber}: ${stageName.charAt(0).toUpperCase() + stageName.slice(1)}, ${getStagePurpose(stageName)}
424
+ export const ${stageName} = async ({ io, llm, data, meta, flags }) => {
425
+
426
+ return { output: {}, flags };
427
+ }`;
428
+ }).join("\n\n");
429
+
430
+ await fs.writeFile(
431
+ path.join(tasksDir, `${taskSlug}.js`),
432
+ taskFileContent + "\n"
433
+ );
434
+
435
+ // Update tasks/index.js
436
+ const indexFilePath = path.join(tasksDir, "index.js");
437
+ let taskIndex = {};
438
+
439
+ try {
440
+ const indexContent = await fs.readFile(indexFilePath, "utf8");
441
+ // Parse the default export from the file
442
+ const exportMatch = indexContent.match(
443
+ /export default\s+({[\s\S]*?})\s*;?\s*$/
444
+ );
445
+ if (exportMatch) {
446
+ // Use eval to parse the object (safe in this controlled context)
447
+ taskIndex = eval(`(${exportMatch[1]})`);
448
+ }
449
+ } catch (error) {
450
+ // If file is missing or invalid, start with empty object
451
+ taskIndex = {};
452
+ }
453
+
454
+ // Add/replace task mapping
455
+ taskIndex[taskSlug] = `./${taskSlug}.js`;
456
+
457
+ // Sort keys alphabetically for stable output
458
+ const sortedKeys = Object.keys(taskIndex).sort();
459
+ const sortedIndex = {};
460
+ for (const key of sortedKeys) {
461
+ sortedIndex[key] = taskIndex[key];
462
+ }
463
+
464
+ // Write back the index file with proper formatting
465
+ const indexContent = `export default ${JSON.stringify(sortedIndex, null, 2)};\n`;
466
+ await fs.writeFile(indexFilePath, indexContent);
467
+
468
+ // Update pipeline.json to include the new task
469
+ await updatePipelineJson(root, pipelineSlug, taskSlug);
470
+
471
+ console.log(`Task "${taskSlug}" added to pipeline "${pipelineSlug}"`);
472
+ } catch (error) {
473
+ console.error(`Error adding task: ${error.message}`);
474
+ process.exit(1);
475
+ }
476
+ });
477
+
478
+ // Helper function to get stage purpose descriptions
479
+ function getStagePurpose(stageName) {
480
+ const purposes = {
481
+ ingestion:
482
+ "load/shape input for downstream stages (no external side-effects required)",
483
+ preProcessing: "prepare and clean data for main processing",
484
+ promptTemplating: "generate or format prompts for LLM interaction",
485
+ inference: "execute LLM calls or other model inference",
486
+ parsing: "extract and structure results from model outputs",
487
+ validateStructure: "ensure output meets expected format and schema",
488
+ validateQuality: "check content quality and completeness",
489
+ critique: "analyze and evaluate results against criteria",
490
+ refine: "improve and optimize outputs based on feedback",
491
+ finalValidation: "perform final checks before completion",
492
+ integration: "integrate results into downstream systems or workflows",
493
+ };
494
+ return purposes[stageName] || "handle stage-specific processing";
495
+ }
496
+
70
497
  program.parse();
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import { startOrchestrator } from "../core/orchestrator.js";
3
+
4
+ async function main() {
5
+ const root = process.env.PO_ROOT;
6
+
7
+ if (!root) {
8
+ console.error(
9
+ "PO_ROOT environment variable is required. Please set PO_ROOT to your pipeline root directory (e.g., ./demo)."
10
+ );
11
+ process.exit(1);
12
+ }
13
+
14
+ try {
15
+ console.log(`Starting orchestrator with dataDir: ${root}`);
16
+ const { stop } = await startOrchestrator({ dataDir: root });
17
+
18
+ // Handle graceful shutdown
19
+ process.on("SIGINT", async () => {
20
+ console.log("\nReceived SIGINT, shutting down orchestrator...");
21
+ await stop();
22
+ process.exit(0);
23
+ });
24
+
25
+ process.on("SIGTERM", async () => {
26
+ console.log("\nReceived SIGTERM, shutting down orchestrator...");
27
+ await stop();
28
+ process.exit(0);
29
+ });
30
+ } catch (error) {
31
+ console.error("Orchestrator failed to start:", error.message);
32
+ process.exit(1);
33
+ }
34
+ }
35
+
36
+ main().catch((error) => {
37
+ console.error("Unhandled error in orchestrator runner:", error);
38
+ process.exit(1);
39
+ });
@@ -0,0 +1,47 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Updates pipeline.json to include a new task
6
+ * @param {string} root - The pipeline root directory
7
+ * @param {string} pipelineSlug - The pipeline slug
8
+ * @param {string} taskSlug - The task slug to add
9
+ */
10
+ export async function updatePipelineJson(root, pipelineSlug, taskSlug) {
11
+ const pipelineConfigPath = path.join(
12
+ root,
13
+ "pipeline-config",
14
+ pipelineSlug,
15
+ "pipeline.json"
16
+ );
17
+ let pipelineConfig = {};
18
+
19
+ try {
20
+ const pipelineContent = await fs.readFile(pipelineConfigPath, "utf8");
21
+ pipelineConfig = JSON.parse(pipelineContent);
22
+ } catch (error) {
23
+ // If file is missing or invalid, create minimal config
24
+ pipelineConfig = {
25
+ name: pipelineSlug,
26
+ version: "1.0.0",
27
+ description: "New pipeline",
28
+ tasks: [],
29
+ };
30
+ }
31
+
32
+ // Ensure tasks array exists
33
+ if (!Array.isArray(pipelineConfig.tasks)) {
34
+ pipelineConfig.tasks = [];
35
+ }
36
+
37
+ // Add task to the end of the list if not already present
38
+ if (!pipelineConfig.tasks.includes(taskSlug)) {
39
+ pipelineConfig.tasks.push(taskSlug);
40
+ }
41
+
42
+ // Write back pipeline.json
43
+ await fs.writeFile(
44
+ pipelineConfigPath,
45
+ JSON.stringify(pipelineConfig, null, 2) + "\n"
46
+ );
47
+ }