@studio-foundation/cli 0.3.0-beta.1 → 0.3.0-beta.6

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 (88) hide show
  1. package/package.json +9 -5
  2. package/ARCHITECTURE.md +0 -40
  3. package/src/commands/api.ts +0 -126
  4. package/src/commands/config.ts +0 -433
  5. package/src/commands/init.ts +0 -879
  6. package/src/commands/install.ts +0 -23
  7. package/src/commands/integrations.ts +0 -332
  8. package/src/commands/list.ts +0 -197
  9. package/src/commands/logs.ts +0 -119
  10. package/src/commands/ollama.ts +0 -168
  11. package/src/commands/project.ts +0 -167
  12. package/src/commands/registry/audit.ts +0 -87
  13. package/src/commands/registry/index.ts +0 -63
  14. package/src/commands/registry/install.ts +0 -222
  15. package/src/commands/registry/publish.ts +0 -112
  16. package/src/commands/registry/remove.ts +0 -89
  17. package/src/commands/registry/search.ts +0 -93
  18. package/src/commands/registry/sync.ts +0 -24
  19. package/src/commands/registry/update.ts +0 -63
  20. package/src/commands/replay.ts +0 -559
  21. package/src/commands/run.ts +0 -454
  22. package/src/commands/status.ts +0 -163
  23. package/src/commands/template/index.ts +0 -59
  24. package/src/commands/template/validate.ts +0 -175
  25. package/src/commands/templates.ts +0 -99
  26. package/src/commands/tools.ts +0 -227
  27. package/src/commands/users.ts +0 -127
  28. package/src/commands/validate.ts +0 -46
  29. package/src/config.ts +0 -101
  30. package/src/index.ts +0 -201
  31. package/src/models-cache.ts +0 -127
  32. package/src/output/file-changes.ts +0 -97
  33. package/src/output/formatter.ts +0 -85
  34. package/src/output/formatters.ts +0 -303
  35. package/src/output/logger.ts +0 -18
  36. package/src/output/parallel-progress.ts +0 -103
  37. package/src/output/progress.ts +0 -414
  38. package/src/provider-validator.ts +0 -109
  39. package/src/registry/cache.ts +0 -46
  40. package/src/registry/client.ts +0 -84
  41. package/src/registry/lockfile.ts +0 -68
  42. package/src/registry/resolver.ts +0 -113
  43. package/src/registry/types.ts +0 -66
  44. package/src/run-logger.ts +0 -66
  45. package/src/run-store-factory.ts +0 -30
  46. package/src/studio-dir.ts +0 -28
  47. package/src/utils/input-wizard.ts +0 -73
  48. package/src/utils/placeholders.ts +0 -10
  49. package/tests/__stubs__/studio-runner.ts +0 -104
  50. package/tests/commands/api.test.ts +0 -110
  51. package/tests/commands/config.test.ts +0 -221
  52. package/tests/commands/init.test.ts +0 -919
  53. package/tests/commands/install.test.ts +0 -52
  54. package/tests/commands/integrations.test.ts +0 -158
  55. package/tests/commands/ollama.test.ts +0 -139
  56. package/tests/commands/project.test.ts +0 -179
  57. package/tests/commands/registry/audit.test.ts +0 -56
  58. package/tests/commands/registry/install.test.ts +0 -200
  59. package/tests/commands/registry/publish.test.ts +0 -56
  60. package/tests/commands/registry/remove.test.ts +0 -103
  61. package/tests/commands/registry/search.test.ts +0 -44
  62. package/tests/commands/registry/sync.test.ts +0 -37
  63. package/tests/commands/registry/update.test.ts +0 -62
  64. package/tests/commands/replay.test.ts +0 -283
  65. package/tests/commands/template/validate.test.ts +0 -150
  66. package/tests/commands/templates.test.ts +0 -42
  67. package/tests/commands/tools.test.ts +0 -106
  68. package/tests/config.test.ts +0 -142
  69. package/tests/formatter.test.ts +0 -158
  70. package/tests/integration/sigint.test.ts +0 -188
  71. package/tests/models-cache.test.ts +0 -250
  72. package/tests/output/file-changes.test.ts +0 -178
  73. package/tests/output/formatters.test.ts +0 -448
  74. package/tests/output/progress-spinner.test.ts +0 -232
  75. package/tests/output/progress-timer.test.ts +0 -230
  76. package/tests/provider-validator.test.ts +0 -182
  77. package/tests/registry/cache.test.ts +0 -66
  78. package/tests/registry/client.test.ts +0 -70
  79. package/tests/registry/lockfile.test.ts +0 -87
  80. package/tests/registry/resolver.test.ts +0 -122
  81. package/tests/run-logger-events.test.ts +0 -326
  82. package/tests/run-logger.test.ts +0 -64
  83. package/tests/run-store-factory.test.ts +0 -51
  84. package/tests/studio-dir.test.ts +0 -31
  85. package/tests/utils/input-wizard.test.ts +0 -153
  86. package/tests/utils/placeholders.test.ts +0 -36
  87. package/tsconfig.json +0 -24
  88. package/vitest.config.ts +0 -20
@@ -1,454 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { resolve, join } from 'node:path';
3
- import yaml from 'js-yaml';
4
- import chalk from 'chalk';
5
- import type { EngineEvents } from '@studio-foundation/engine';
6
- import { PipelineEngine, loadPipelineByName, DirectEngineSpawner } from '@studio-foundation/engine';
7
- import { createDefaultRegistry, ToolRegistry, loadProjectTools, loadPlugins, MCPClient } from '@studio-foundation/runner';
8
- import { resolveRepoPath } from '@studio-foundation/engine';
9
- import { loadConfig } from '../config.js';
10
- import { ProgressDisplay } from '../output/progress.js';
11
- import { createRunLogger } from '../run-logger.js';
12
- import { FileChangeCollector, formatFileChanges } from '../output/file-changes.js';
13
- import { formatResult } from '../output/formatter.js';
14
- import { validateInputSchema, collectStructuredInput } from '../utils/input-wizard.js';
15
- import { createRunStore } from '../run-store-factory.js';
16
- import type { AnyRunStore } from '@studio-foundation/engine';
17
-
18
- interface RunOptions {
19
- input?: string;
20
- inputFile?: string;
21
- repo?: string;
22
- repoUrl?: string;
23
- config?: string;
24
- json?: boolean;
25
- verbose?: boolean;
26
- live?: boolean;
27
- provider?: string;
28
- anonymize?: boolean;
29
- }
30
-
31
- export function mergeEvents(
32
- progressEvents: EngineEvents,
33
- logger: ReturnType<typeof createRunLogger>,
34
- pipeline: string,
35
- input: string | Record<string, unknown>
36
- ): EngineEvents {
37
- let totalStages = 0;
38
- return {
39
- onPipelineStart: (e) => {
40
- logger.start(e.run_id, pipeline);
41
- progressEvents.onPipelineStart?.(e);
42
- logger.log({
43
- event: 'pipeline_start',
44
- run_id: e.run_id,
45
- pipeline,
46
- input,
47
- });
48
- },
49
- onPipelineComplete: (e) => {
50
- progressEvents.onPipelineComplete?.(e);
51
- logger.log({
52
- event: 'pipeline_complete',
53
- run_id: e.run_id,
54
- pipeline_name: e.pipeline_name,
55
- status: e.status,
56
- duration_ms: e.duration_ms,
57
- total_tokens: e.total_tokens,
58
- total_tool_calls: e.total_tool_calls,
59
- total_stages: totalStages,
60
- });
61
- },
62
- onStageStart: (e) => {
63
- totalStages = e.total_stages;
64
- progressEvents.onStageStart?.(e);
65
- logger.log({
66
- event: 'stage_start',
67
- stage: e.stage_name,
68
- stage_index: e.stage_index,
69
- total_stages: e.total_stages,
70
- max_attempts: e.max_attempts,
71
- });
72
- },
73
- onStageContext: (e) => {
74
- logger.log({
75
- event: 'stage_context',
76
- stage: e.stage,
77
- run_id: e.run_id,
78
- context_keys: e.context_keys,
79
- ...(e.context_content !== undefined ? { context_content: e.context_content } : {}),
80
- ...(e.system_prompt !== undefined ? { system_prompt: e.system_prompt } : {}),
81
- });
82
- },
83
- onStageComplete: (e) => {
84
- progressEvents.onStageComplete?.(e);
85
- logger.log({
86
- event: 'stage_complete',
87
- stage: e.stage_name,
88
- status: e.status,
89
- attempts: e.attempts,
90
- duration_ms: e.duration_ms,
91
- tokens: e.token_usage
92
- ? {
93
- prompt: e.token_usage.prompt_tokens,
94
- completion: e.token_usage.completion_tokens,
95
- total: e.token_usage.total_tokens,
96
- }
97
- : undefined,
98
- tool_calls: e.tool_calls,
99
- output: e.output,
100
- ...(e.rejection_reason ? { rejection_reason: e.rejection_reason } : {}),
101
- ...(e.rejection_details?.length ? { rejection_details: e.rejection_details } : {}),
102
- ...(e.skipped_reason ? { skipped_reason: e.skipped_reason } : {}),
103
- });
104
- },
105
- onTaskRetry: (e) => {
106
- progressEvents.onTaskRetry?.(e);
107
- logger.log({
108
- event: 'stage_retry',
109
- stage: e.stage,
110
- attempt: e.attempt,
111
- max_attempts: e.max_attempts,
112
- failures: e.failures,
113
- ...(e.agent_output_raw ? { agent_output_raw: e.agent_output_raw } : {}),
114
- ...(e.tool_calls_count !== undefined ? { tool_calls_count: e.tool_calls_count } : {}),
115
- });
116
- },
117
- onGroupStart: (e) => {
118
- progressEvents.onGroupStart?.(e);
119
- logger.log({
120
- event: 'group_start',
121
- group: e.group_name,
122
- max_iterations: e.max_iterations,
123
- });
124
- },
125
- onGroupIteration: (e) => {
126
- progressEvents.onGroupIteration?.(e);
127
- logger.log({
128
- event: 'group_iteration',
129
- group: e.group_name,
130
- iteration: e.iteration,
131
- max_iterations: e.max_iterations,
132
- });
133
- },
134
- onGroupFeedback: (e) => {
135
- progressEvents.onGroupFeedback?.(e);
136
- logger.log({
137
- event: 'group_feedback',
138
- group: e.group_name,
139
- iteration: e.iteration,
140
- rejection_reason: e.rejection_reason,
141
- rejection_details: e.rejection_details,
142
- });
143
- },
144
- onGroupComplete: (e) => {
145
- progressEvents.onGroupComplete?.(e);
146
- logger.log({
147
- event: 'group_complete',
148
- group: e.group_name,
149
- iterations: e.iterations,
150
- status: e.status,
151
- });
152
- },
153
- onPipelineCancelled: (e) => {
154
- logger.log({
155
- event: 'pipeline_cancelled',
156
- run_id: e.run_id,
157
- cancelled_at_stage: e.cancelled_at_stage,
158
- duration_ms: e.duration_ms,
159
- });
160
- },
161
- onToolCallStart: (e) => {
162
- progressEvents.onToolCallStart?.(e);
163
- logger.log({
164
- event: 'tool_call_start',
165
- tool: e.tool,
166
- params: e.params,
167
- });
168
- },
169
- onToolCallComplete: (e) => {
170
- progressEvents.onToolCallComplete?.(e);
171
- logger.log({
172
- event: 'tool_call_complete',
173
- tool: e.tool,
174
- result: e.result,
175
- ...(e.error ? { error: e.error } : {}),
176
- duration_ms: e.duration_ms,
177
- });
178
- },
179
- onAgentThinking: (e) => progressEvents.onAgentThinking?.(e),
180
- onAgentProgress: (e) => progressEvents.onAgentProgress?.(e),
181
- };
182
- }
183
-
184
- export async function runCommand(pipelineName: string, options: RunOptions): Promise<void> {
185
- try {
186
- const config = await loadConfig(options.config);
187
-
188
- // Create run store — fail-silent so a broken SQLite never blocks a run
189
- let runStore: AnyRunStore | null = null;
190
- try {
191
- runStore = await createRunStore(config);
192
- } catch (err) {
193
- console.warn(chalk.yellow(`⚠ Run store unavailable: ${err instanceof Error ? err.message : String(err)}. Continuing with JSONL logging only.`));
194
- }
195
-
196
- // Resolve configs dir and parse project/pipeline
197
- const configsDir = config.paths?.configs
198
- ? resolve(config.paths.configs)
199
- : config.resolvedStudioDir
200
- ? resolve(config.resolvedStudioDir)
201
- : resolve('./configs');
202
- const pipelinesDir = join(configsDir, 'pipelines');
203
-
204
- // Load pipeline early (needed for input_schema and repo URL)
205
- const pipelineDef = await loadPipelineByName(pipelineName, pipelinesDir);
206
-
207
- // Resolve input: --input-file > --input > wizard > error
208
- let input: string | Record<string, unknown>;
209
- let inputFileRepoUrl: string | undefined;
210
-
211
- if (options.inputFile) {
212
- const inputPath = resolve(options.inputFile);
213
- let raw: string;
214
- try {
215
- raw = await readFile(inputPath, 'utf-8');
216
- } catch {
217
- console.error(`Error: Input file not found: ${inputPath}`);
218
- process.exit(1);
219
- }
220
- const parsed = yaml.load(raw);
221
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
222
- const parsedObj = parsed as Record<string, unknown>;
223
- inputFileRepoUrl = typeof parsedObj['repo_url'] === 'string' ? parsedObj['repo_url'] : undefined;
224
- delete parsedObj['repo_url'];
225
- input = parsedObj;
226
- } else {
227
- console.error('Error: Input file must contain a YAML object (key-value pairs)');
228
- process.exit(1);
229
- }
230
- } else if (options.input) {
231
- input = options.input;
232
- } else if (pipelineDef.input_schema?.type === 'structured') {
233
- try {
234
- const schema = validateInputSchema(pipelineDef.input_schema);
235
- input = await collectStructuredInput(schema);
236
- } catch (err) {
237
- console.error(`Error: Invalid input_schema in pipeline: ${(err as Error).message}`);
238
- process.exit(1);
239
- }
240
- } else {
241
- console.error('Error: --input or --input-file is required');
242
- process.exit(1);
243
- }
244
-
245
- if (!options.json) {
246
- console.log(chalk.green('\n✓ Input collected\n'));
247
- }
248
-
249
- // Resolve repo path: --repo > --repo-url > pipeline.repo.url > CWD
250
- const effectiveRepoUrl = options.repo
251
- ? undefined
252
- : (options.repoUrl || inputFileRepoUrl || pipelineDef.repo?.url);
253
- const effectiveBranch = pipelineDef.repo?.branch;
254
-
255
- if (effectiveRepoUrl && !options.json) {
256
- console.log(`Cloning ${effectiveRepoUrl}...`);
257
- }
258
-
259
- let repoPath: string;
260
- try {
261
- repoPath = await resolveRepoPath({
262
- repoPathOverride: options.repo,
263
- repoUrl: effectiveRepoUrl,
264
- rawProjectsDir: config.paths?.projects_dir || process.env.STUDIO_PROJECTS_DIR,
265
- pipelineName,
266
- branch: effectiveBranch,
267
- });
268
- } catch (err) {
269
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
270
- process.exit(1);
271
- }
272
-
273
- if (effectiveRepoUrl && !options.json) {
274
- console.log(`Cloned to: ${repoPath}\n`);
275
- }
276
-
277
- const providerRegistry = createDefaultRegistry(
278
- options.provider === 'mock' ? {} : {
279
- openai: config.providers?.openai ? { apiKey: config.providers.openai.apiKey } : undefined,
280
- anthropic: config.providers?.anthropic ? { apiKey: config.providers.anthropic.apiKey } : undefined,
281
- openaiResponses: config.providers?.openai ? { apiKey: config.providers.openai.apiKey } : undefined,
282
- ollama: config.providers?.ollama ? { baseUrl: config.providers.ollama.baseUrl } : undefined,
283
- }
284
- );
285
-
286
- // Handle --provider override
287
- if (options.provider === 'mock') {
288
- const mockYamlPath = join(configsDir, 'mock.yaml');
289
- let mockRaw: string;
290
- try {
291
- mockRaw = await readFile(mockYamlPath, 'utf-8');
292
- } catch {
293
- console.error(`Error: --provider mock requires ${mockYamlPath}`);
294
- process.exit(1);
295
- }
296
-
297
- const mockConfig = yaml.load(mockRaw) as {
298
- stages: Record<string, {
299
- output: Record<string, unknown>;
300
- tool_calls: Array<{ name: string; arguments: Record<string, unknown> }>;
301
- }>;
302
- };
303
- const stagesMap = new Map(Object.entries(mockConfig.stages));
304
- const { MockProvider } = await import('@studio-foundation/runner');
305
- const mockProvider = new MockProvider(stagesMap);
306
- providerRegistry.register(mockProvider);
307
- } else if (options.provider && !providerRegistry.has(options.provider)) {
308
- console.error(`Error: Unknown provider "${options.provider}". Available: ${providerRegistry.list().join(', ')}`);
309
- process.exit(1);
310
- }
311
-
312
- const toolsDir = resolve(configsDir, 'tools');
313
- const loadedPlugins = await loadProjectTools(toolsDir, repoPath);
314
- const toolRegistry = new ToolRegistry();
315
- for (const plugin of loadedPlugins) {
316
- toolRegistry.registerPlugin(plugin.name, plugin.tools, plugin.promptSnippet);
317
- }
318
-
319
- // Load plugins from .studio/plugins/ and start MCP servers
320
- const pluginsDir = resolve(configsDir, 'plugins');
321
- const pluginManifests = await loadPlugins(pluginsDir);
322
- const mcpClients: MCPClient[] = [];
323
- for (const manifest of pluginManifests) {
324
- for (const [serverName, serverDef] of Object.entries(manifest.mcpServers)) {
325
- const client = new MCPClient(manifest.name, serverName, serverDef);
326
- try {
327
- await client.start();
328
- const mcpTools = await client.getTools();
329
- toolRegistry.registerPlugin(`${manifest.name}-${serverName}`, mcpTools);
330
- mcpClients.push(client);
331
- } catch (err) {
332
- console.warn(chalk.yellow(`⚠ Plugin '${manifest.name}': failed to start MCP server '${serverName}': ${(err as Error).message}`));
333
- }
334
- }
335
- }
336
-
337
- // Build skill map for engine skill injection
338
- const pluginSkills: Record<string, string[]> = {};
339
- for (const manifest of pluginManifests) {
340
- if (manifest.skills.length > 0) {
341
- pluginSkills[manifest.name] = manifest.skills.map(
342
- (s) => `## Skill: ${s.name}\n\n${s.content}`
343
- );
344
- }
345
- }
346
-
347
- const progress = new ProgressDisplay(!!options.json, {
348
- live: !!options.live,
349
- verbose: !!options.verbose,
350
- });
351
- const runLogger = createRunLogger(process.cwd());
352
- const fileCollector = new FileChangeCollector();
353
- const baseEvents = mergeEvents(
354
- progress.getEvents(),
355
- runLogger,
356
- pipelineName,
357
- input
358
- );
359
- const events: EngineEvents = {
360
- ...baseEvents,
361
- onToolCallComplete: (e) => {
362
- fileCollector.onToolCallComplete(e);
363
- baseEvents.onToolCallComplete?.(e);
364
- },
365
- };
366
-
367
- const engineConfig = {
368
- configsDir,
369
- repoPath,
370
- providerRegistry,
371
- toolRegistry,
372
- pluginSkills,
373
- db: runStore ?? undefined,
374
- defaultProvider: config.defaults?.provider,
375
- defaultModel: config.defaults?.model,
376
- ...(options.provider ? { providerOverride: options.provider } : {}),
377
- };
378
-
379
- const spawner = new DirectEngineSpawner(engineConfig);
380
-
381
- const engine = new PipelineEngine(
382
- {
383
- ...engineConfig,
384
- spawner,
385
- maxDepth: 3,
386
- },
387
- events
388
- );
389
-
390
- const controller = new AbortController();
391
- let forceExitOnNextInterrupt = false;
392
-
393
- const onInterrupt = () => {
394
- if (forceExitOnNextInterrupt) {
395
- // Second Ctrl-C: force exit
396
- process.exit(130);
397
- }
398
- forceExitOnNextInterrupt = true;
399
- controller.abort();
400
- progress.interrupt();
401
- process.stderr.write('\n' + chalk.yellow('⚠ Cancelling run...') + '\n');
402
- };
403
- process.on('SIGINT', onInterrupt);
404
- process.on('SIGTERM', onInterrupt);
405
-
406
- let result;
407
- try {
408
- result = await engine.run({
409
- pipeline: pipelineName,
410
- input,
411
- anonymize: options.anonymize,
412
- signal: controller.signal,
413
- });
414
- } finally {
415
- process.off('SIGINT', onInterrupt);
416
- process.off('SIGTERM', onInterrupt);
417
- await runLogger.close();
418
- if (runStore && result) {
419
- await runStore.saveLogPath(result.id, runLogger.getLogPath());
420
- }
421
- await runStore?.close?.();
422
- // Stop all MCP servers (even if pipeline failed)
423
- await Promise.allSettled(mcpClients.map((c) => c.close()));
424
- }
425
-
426
- if (options.json) {
427
- console.log(JSON.stringify(result, null, 2));
428
- } else {
429
- if (result.status === 'cancelled') {
430
- const lastStage = result.stages[result.stages.length - 1];
431
- const stageName = lastStage?.stage_name ?? 'unknown';
432
- const stageIdx = result.stages.length;
433
- console.error(chalk.red(`✗ Run cancelled at stage [${stageIdx}] ${stageName}`));
434
- } else {
435
- formatResult(result);
436
- const changes = fileCollector.computeSummary(repoPath);
437
- if (changes) {
438
- console.log(formatFileChanges(changes));
439
- }
440
- }
441
- console.log('');
442
- console.log(chalk.gray(`Run ID: ${progress.runId}`));
443
- console.log(chalk.gray(`View details: studio status ${progress.runId}`));
444
- }
445
-
446
- if (result.status === 'cancelled') {
447
- process.exit(130);
448
- }
449
- process.exit(result.status === 'success' ? 0 : 1);
450
- } catch (error) {
451
- console.error('Error:', error instanceof Error ? error.message : error);
452
- process.exit(1);
453
- }
454
- }
@@ -1,163 +0,0 @@
1
- import chalk from 'chalk';
2
- import { readdir, readFile } from 'node:fs/promises';
3
- import { resolve } from 'node:path';
4
- import type { PipelineRun, StageRun, TaskRun } from '@studio-foundation/contracts';
5
- import { loadConfig } from '../config.js';
6
- import { createRunStore } from '../run-store-factory.js';
7
- import { formatResult } from '../output/formatter.js';
8
-
9
- interface StatusOptions {
10
- json?: boolean;
11
- }
12
-
13
- const RUNS_DIR = '.studio/runs';
14
-
15
- function runIdShort(runId: string): string {
16
- return runId.replace(/-/g, '').slice(0, 8);
17
- }
18
-
19
- async function getRunFromJsonl(runId: string | undefined): Promise<PipelineRun | null> {
20
- const runsPath = resolve(process.cwd(), RUNS_DIR);
21
- let entries: import('node:fs').Dirent[];
22
- try {
23
- entries = await readdir(runsPath, { withFileTypes: true });
24
- } catch {
25
- return null;
26
- }
27
-
28
- const jsonlFiles = entries
29
- .filter((e) => e.isFile() && e.name.endsWith('.jsonl'))
30
- .map((e) => e.name);
31
-
32
- const shortId = runId ? runIdShort(runId) : null;
33
- const candidates = (shortId
34
- ? jsonlFiles.filter((n) => n.endsWith(`-${shortId}.jsonl`))
35
- : jsonlFiles
36
- ).sort()
37
- .reverse();
38
-
39
- for (const filename of candidates) {
40
- const filePath = resolve(runsPath, filename);
41
- const content = await readFile(filePath, 'utf-8');
42
- const lines = content
43
- .split('\n')
44
- .filter((l) => l.trim().length > 0)
45
- .map((l) => {
46
- try {
47
- return JSON.parse(l) as Record<string, unknown>;
48
- } catch {
49
- return null;
50
- }
51
- })
52
- .filter((r): r is Record<string, unknown> => r !== null);
53
-
54
- const forRun = shortId
55
- ? lines.filter((r) => (r.run_id as string) === shortId || (r.run_id as string)?.startsWith(shortId))
56
- : lines;
57
- const records = forRun.length > 0 ? forRun : lines;
58
-
59
- let pipeline_name = '';
60
- let started_at = '';
61
- let completed_at: string | undefined;
62
- let status = 'running';
63
- const stageCompletes: Array<{ stage_name: string; status: string; attempts: number; skipped_reason?: string }> = [];
64
-
65
- for (const r of records) {
66
- const event = r.event as string;
67
- const ts = r.ts as string;
68
- if (event === 'pipeline_start') {
69
- pipeline_name = `${r.project}/${r.pipeline}`;
70
- started_at = ts ?? '';
71
- } else if (event === 'pipeline_complete') {
72
- status = (r.status as string) ?? 'running';
73
- completed_at = ts;
74
- } else if (event === 'stage_complete') {
75
- stageCompletes.push({
76
- stage_name: (r.stage as string) ?? '',
77
- status: (r.status as string) ?? 'unknown',
78
- attempts: (r.attempts as number) ?? 1,
79
- skipped_reason: r.skipped_reason as string | undefined,
80
- });
81
- }
82
- }
83
-
84
- if (!pipeline_name) continue;
85
-
86
- const runIdFromFile = shortId ?? (records[0]?.run_id as string) ?? '';
87
-
88
- const taskStatus = (s: string): TaskRun['status'] =>
89
- s === 'rejected' ? 'failed' : (s as TaskRun['status']);
90
-
91
- const stages: StageRun[] = stageCompletes.map((s, i) => ({
92
- id: `stage-${i}`,
93
- stage_name: s.stage_name,
94
- status: s.status as StageRun['status'],
95
- started_at: started_at,
96
- completed_at: completed_at,
97
- skipped_reason: s.skipped_reason,
98
- tasks: [
99
- {
100
- id: `task-${i}`,
101
- task_name: s.stage_name,
102
- status: taskStatus(s.status),
103
- started_at: started_at,
104
- completed_at: completed_at,
105
- agent_runs: Array.from({ length: s.attempts }, (_, j) => ({
106
- id: `ar-${i}-${j}`,
107
- agent_name: '',
108
- attempt: j + 1,
109
- status: 'success' as const,
110
- tool_calls: 0,
111
- started_at: started_at,
112
- completed_at: completed_at,
113
- })),
114
- },
115
- ],
116
- }));
117
-
118
- return {
119
- id: runIdFromFile,
120
- pipeline_name: pipeline_name || 'unknown',
121
- status: status as PipelineRun['status'],
122
- started_at,
123
- completed_at,
124
- stages,
125
- };
126
- }
127
- return null;
128
- }
129
-
130
- export async function statusCommand(
131
- runId: string | undefined,
132
- options: StatusOptions
133
- ): Promise<void> {
134
- try {
135
- let run: PipelineRun | null = null;
136
- try {
137
- const config = await loadConfig();
138
- const store = await createRunStore(config);
139
- run = runId ? await store.getPipelineRun(runId) : await store.getLatestRun();
140
- await store.close?.();
141
- } catch {
142
- // DB not available or not initialized
143
- }
144
-
145
- if (!run && (runId || true)) {
146
- run = await getRunFromJsonl(runId);
147
- }
148
-
149
- if (!run) {
150
- console.log(chalk.yellow(runId ? `Run not found: ${runId}` : 'No runs found'));
151
- process.exit(1);
152
- }
153
-
154
- if (options.json) {
155
- console.log(JSON.stringify(run, null, 2));
156
- } else {
157
- formatResult(run);
158
- }
159
- } catch (error) {
160
- console.error('Error:', error instanceof Error ? error.message : error);
161
- process.exit(1);
162
- }
163
- }
@@ -1,59 +0,0 @@
1
- import { resolve } from 'node:path';
2
- import chalk from 'chalk';
3
- import { validateTemplateDir } from './validate.js';
4
-
5
- export async function templateCommand(action: string, args: string[]): Promise<void> {
6
- try {
7
- switch (action) {
8
- case 'validate': {
9
- const pathArg = args[0];
10
- if (!pathArg) {
11
- console.error('Usage: studio template validate <path>');
12
- process.exit(1);
13
- }
14
- const templatePath = resolve(pathArg);
15
- console.log('');
16
- console.log(`Validating template at: ${chalk.cyan(templatePath)}`);
17
- console.log('');
18
-
19
- const result = await validateTemplateDir(templatePath);
20
-
21
- if (result.valid) {
22
- console.log(chalk.green('✓ Structural validation passed'));
23
- console.log(chalk.green('✓ Semantic validation passed'));
24
- } else {
25
- if (result.structuralErrors.length > 0) {
26
- console.log(chalk.red('✗ Structural validation failed'));
27
- } else {
28
- console.log(chalk.green('✓ Structural validation passed'));
29
- console.log(chalk.red('✗ Semantic validation failed'));
30
- }
31
- console.log('');
32
- const allErrors = [...result.structuralErrors, ...result.semanticErrors];
33
- for (const error of allErrors) {
34
- for (const line of error.split('\n')) {
35
- console.log(` ${chalk.red(line)}`);
36
- }
37
- }
38
- }
39
-
40
- if (result.warnings.length > 0) {
41
- console.log('');
42
- for (const warning of result.warnings) {
43
- console.log(` ${chalk.yellow('⚠')} ${chalk.gray(warning)}`);
44
- }
45
- }
46
-
47
- console.log('');
48
- process.exit(result.valid ? 0 : 1);
49
- }
50
-
51
- default:
52
- console.error(`Unknown template action: ${action}. Available: validate`);
53
- process.exit(1);
54
- }
55
- } catch (error) {
56
- console.error('Error:', error instanceof Error ? error.message : error);
57
- process.exit(1);
58
- }
59
- }