@output.ai/cli 0.3.0-dev.pr156.f70e0a1 → 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 (33) hide show
  1. package/README.md +0 -53
  2. package/dist/api/generated/api.d.ts +16 -24
  3. package/dist/assets/docker/docker-compose-dev.yml +0 -1
  4. package/dist/commands/workflow/generate.js +3 -10
  5. package/dist/commands/workflow/generate.spec.js +6 -4
  6. package/dist/services/coding_agents.js +30 -0
  7. package/dist/services/coding_agents.spec.js +36 -61
  8. package/dist/services/messages.d.ts +1 -0
  9. package/dist/services/messages.js +65 -1
  10. package/dist/services/workflow_generator.spec.js +77 -0
  11. package/dist/templates/agent_instructions/AGENTS.md.template +209 -19
  12. package/dist/templates/agent_instructions/agents/context_fetcher.md.template +82 -0
  13. package/dist/templates/agent_instructions/agents/prompt_writer.md.template +595 -0
  14. package/dist/templates/agent_instructions/agents/workflow_planner.md.template +13 -4
  15. package/dist/templates/agent_instructions/agents/workflow_quality.md.template +244 -0
  16. package/dist/templates/agent_instructions/commands/build_workflow.md.template +52 -9
  17. package/dist/templates/agent_instructions/commands/plan_workflow.md.template +4 -4
  18. package/dist/templates/project/package.json.template +2 -2
  19. package/dist/templates/project/src/simple/scenarios/question_ada_lovelace.json.template +3 -0
  20. package/dist/templates/workflow/scenarios/test_input.json.template +7 -0
  21. package/dist/utils/template.spec.js +6 -0
  22. package/package.json +1 -2
  23. package/dist/commands/workflow/debug.d.ts +0 -14
  24. package/dist/commands/workflow/debug.js +0 -51
  25. package/dist/commands/workflow/debug.test.js +0 -72
  26. package/dist/services/trace_reader.d.ts +0 -42
  27. package/dist/services/trace_reader.js +0 -57
  28. package/dist/services/trace_reader.test.d.ts +0 -1
  29. package/dist/services/trace_reader.test.js +0 -163
  30. package/dist/utils/trace_formatter.d.ts +0 -43
  31. package/dist/utils/trace_formatter.js +0 -492
  32. /package/dist/{commands/workflow/debug.test.d.ts → services/workflow_generator.spec.d.ts} +0 -0
  33. /package/dist/templates/workflow/{prompt@v1.prompt.template → prompts/prompt@v1.prompt.template} +0 -0
package/README.md CHANGED
@@ -95,59 +95,6 @@ output workflow list --filter simple
95
95
 
96
96
  The list command connects to the API server and retrieves all available workflows. By default, it displays a simple list of workflow names (like `ls`). Use `--format table` for detailed information.
97
97
 
98
- ### Debug a Workflow Execution
99
-
100
- ```bash
101
- # Display trace information for a workflow run
102
- output workflow debug <workflowId>
103
-
104
- # Display trace in JSON format
105
- output workflow debug <workflowId> --format json
106
-
107
- # Prefer remote (S3) trace over local
108
- output workflow debug <workflowId> --remote
109
-
110
- # Save trace to a custom directory
111
- output workflow debug <workflowId> --download-dir ./my-traces
112
-
113
- # Force re-download of remote traces (bypass cache)
114
- output workflow debug <workflowId> --remote --force-download
115
- ```
116
-
117
- #### What It Does
118
-
119
- The `debug` command retrieves and displays detailed execution traces for debugging workflow runs. It shows:
120
- - Complete execution timeline with all events
121
- - Hierarchical execution tree showing workflow structure
122
- - Step and activity inputs/outputs
123
- - Error details and stack traces
124
- - Performance metrics and durations
125
-
126
- #### Trace Sources
127
-
128
- The command automatically handles both local and remote traces:
129
- - **Local traces**: Stored on your machine when `TRACE_LOCAL_ON=true`
130
- - **Remote traces**: Stored in S3 when `TRACE_REMOTE_ON=true` (requires AWS credentials)
131
-
132
- By default, it tries local traces first for faster access, then falls back to remote if needed.
133
-
134
- #### Command Options
135
-
136
- - `--format, -f` - Output format: `text` (default, human-readable) or `json` (raw trace data)
137
- - `--remote, -r` - Prefer remote S3 trace even if local exists
138
- - `--download-dir, -d` - Directory for saving downloaded traces (default: `.output/traces`)
139
- - `--open, -o` - Save trace to file for viewing in external JSON viewer
140
- - `--force-download` - Force re-download from S3, bypassing local cache
141
-
142
- #### AWS Configuration
143
-
144
- For remote traces, configure AWS credentials:
145
- ```bash
146
- export AWS_ACCESS_KEY_ID=your-access-key
147
- export AWS_SECRET_ACCESS_KEY=your-secret-key
148
- export AWS_REGION=us-east-1 # or your region
149
- ```
150
-
151
98
  ### Generate a Workflow
152
99
 
153
100
  ```bash
@@ -32,28 +32,6 @@ export interface Workflow {
32
32
  inputSchema?: JSONSchema;
33
33
  outputSchema?: JSONSchema;
34
34
  }
35
- /**
36
- * File destinations for trace data
37
- */
38
- export type TraceInfoDestinations = {
39
- /**
40
- * Absolute path to local trace file, or null if not saved locally
41
- * @nullable
42
- */
43
- local: string | null;
44
- /**
45
- * Remote trace location (e.g., S3 URI), or null if not saved remotely
46
- * @nullable
47
- */
48
- remote: string | null;
49
- };
50
- /**
51
- * An object with information about the trace generated by the execution
52
- */
53
- export interface TraceInfo {
54
- /** File destinations for trace data */
55
- destinations?: TraceInfoDestinations;
56
- }
57
35
  export type PostWorkflowRunBody = {
58
36
  /** The name of the workflow to execute */
59
37
  workflowName: string;
@@ -64,12 +42,19 @@ export type PostWorkflowRunBody = {
64
42
  /** The name of the task queue to send the workflow to */
65
43
  taskQueue?: string;
66
44
  };
45
+ /**
46
+ * An object with information about the trace generated by the execution
47
+ */
48
+ export type PostWorkflowRun200Trace = {
49
+ [key: string]: unknown;
50
+ };
67
51
  export type PostWorkflowRun200 = {
68
52
  /** The workflow execution id */
69
53
  workflowId?: string;
70
54
  /** The output of the workflow */
71
55
  output?: unknown;
72
- trace?: TraceInfo;
56
+ /** An object with information about the trace generated by the execution */
57
+ trace?: PostWorkflowRun200Trace;
73
58
  };
74
59
  export type PostWorkflowStartBody = {
75
60
  /** The name of the workflow to execute */
@@ -109,12 +94,19 @@ export type GetWorkflowIdStatus200 = {
109
94
  /** An epoch timestamp representing when the workflow ended */
110
95
  completedAt?: number;
111
96
  };
97
+ /**
98
+ * An object with information about the trace generated by the execution
99
+ */
100
+ export type GetWorkflowIdOutput200Trace = {
101
+ [key: string]: unknown;
102
+ };
112
103
  export type GetWorkflowIdOutput200 = {
113
104
  /** The workflow execution id */
114
105
  workflowId?: string;
115
106
  /** The output of workflow */
116
107
  output?: unknown;
117
- trace?: TraceInfo;
108
+ /** An object with information about the trace generated by the execution */
109
+ trace?: GetWorkflowIdOutput200Trace;
118
110
  };
119
111
  export type GetWorkflowCatalogId200 = {
120
112
  /** Each workflow available in this catalog */
@@ -104,7 +104,6 @@ services:
104
104
  - REDIS_URL=redis://redis:6379
105
105
  - TEMPORAL_ADDRESS=temporal:7233
106
106
  - TRACE_LOCAL_ON=true
107
- - HOST_TRACE_PATH=${PWD}/logs
108
107
  command: npm run start-worker
109
108
  working_dir: /app
110
109
  volumes:
@@ -2,6 +2,7 @@ import { Args, Command, Flags, ux } from '@oclif/core';
2
2
  import { generateWorkflow } from '#services/workflow_generator.js';
3
3
  import { buildWorkflow, buildWorkflowInteractiveLoop } from '#services/workflow_builder.js';
4
4
  import { ensureOutputAIStructure } from '#services/coding_agents.js';
5
+ import { getWorkflowGenerateSuccessMessage } from '#services/messages.js';
5
6
  import { DEFAULT_OUTPUT_DIRS } from '#utils/paths.js';
6
7
  import path from 'node:path';
7
8
  export default class Generate extends Command {
@@ -78,15 +79,7 @@ export default class Generate extends Command {
78
79
  }
79
80
  }
80
81
  displaySuccess(result) {
81
- this.log(`\n[SUCCESS] Workflow "${result.workflowName}" created successfully!`);
82
- this.log(`\nLocation: ${result.targetDir}`);
83
- this.log('\n-- Next steps --');
84
- this.log(` 1. cd ${result.targetDir}`);
85
- this.log(' 2. Edit the workflow files to customize your implementation');
86
- this.log(' 3. If using @output.ai/llm, configure your .env file with LLM provider credentials:');
87
- this.log(' - ANTHROPIC_API_KEY for Claude');
88
- this.log(' - OPENAI_API_KEY for OpenAI');
89
- this.log(' 4. Build and run with the worker');
90
- this.log('\nCheck the README.md for workflow-specific documentation.');
82
+ const message = getWorkflowGenerateSuccessMessage(result.workflowName, result.targetDir, result.filesCreated);
83
+ this.log(message);
91
84
  }
92
85
  }
@@ -46,7 +46,8 @@ describe('Generate Command', () => {
46
46
  skeleton: true,
47
47
  force: false
48
48
  });
49
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('[SUCCESS] Workflow "test-workflow" created successfully!'));
49
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('SUCCESS!'));
50
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('test-workflow'));
50
51
  });
51
52
  it('should require skeleton flag and reject without it', async () => {
52
53
  const cmd = createCommand();
@@ -105,9 +106,10 @@ describe('Generate Command', () => {
105
106
  filesCreated: ['index.ts', 'steps.ts', 'types.ts']
106
107
  });
107
108
  await cmd.run();
108
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('[SUCCESS] Workflow "my-workflow" created successfully!'));
109
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Location: /custom/path/my-workflow'));
110
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('-- Next steps --'));
109
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('SUCCESS!'));
110
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('my-workflow'));
111
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('/custom/path/my-workflow'));
112
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('NEXT STEPS'));
111
113
  });
112
114
  });
113
115
  });
@@ -29,6 +29,21 @@ export const AGENT_CONFIGS = {
29
29
  from: 'agents/workflow_planner.md.template',
30
30
  to: `${AGENT_CONFIG_DIR}/agents/workflow_planner.md`
31
31
  },
32
+ {
33
+ type: 'template',
34
+ from: 'agents/workflow_quality.md.template',
35
+ to: `${AGENT_CONFIG_DIR}/agents/workflow_quality.md`
36
+ },
37
+ {
38
+ type: 'template',
39
+ from: 'agents/context_fetcher.md.template',
40
+ to: `${AGENT_CONFIG_DIR}/agents/context_fetcher.md`
41
+ },
42
+ {
43
+ type: 'template',
44
+ from: 'agents/prompt_writer.md.template',
45
+ to: `${AGENT_CONFIG_DIR}/agents/prompt_writer.md`
46
+ },
32
47
  {
33
48
  type: 'template',
34
49
  from: 'commands/plan_workflow.md.template',
@@ -65,6 +80,21 @@ export const AGENT_CONFIGS = {
65
80
  from: `${AGENT_CONFIG_DIR}/agents/workflow_planner.md`,
66
81
  to: '.claude/agents/workflow_planner.md'
67
82
  },
83
+ {
84
+ type: 'symlink',
85
+ from: `${AGENT_CONFIG_DIR}/agents/workflow_quality.md`,
86
+ to: '.claude/agents/workflow_quality.md'
87
+ },
88
+ {
89
+ type: 'symlink',
90
+ from: `${AGENT_CONFIG_DIR}/agents/context_fetcher.md`,
91
+ to: '.claude/agents/context_fetcher.md'
92
+ },
93
+ {
94
+ type: 'symlink',
95
+ from: `${AGENT_CONFIG_DIR}/agents/prompt_writer.md`,
96
+ to: '.claude/agents/prompt_writer.md'
97
+ },
68
98
  {
69
99
  type: 'symlink',
70
100
  from: `${AGENT_CONFIG_DIR}/commands/plan_workflow.md`,
@@ -39,11 +39,11 @@ describe('coding_agents service', () => {
39
39
  const expectedCount = AGENT_CONFIGS.outputai.mappings.length +
40
40
  AGENT_CONFIGS['claude-code'].mappings.length;
41
41
  expect(files.length).toBe(expectedCount);
42
- expect(files.length).toBe(10);
42
+ expect(files.length).toBe(16);
43
43
  });
44
44
  it('should have outputai files with .outputai prefix', () => {
45
45
  const files = getRequiredFiles();
46
- const outputaiFiles = files.slice(0, 6);
46
+ const outputaiFiles = files.slice(0, 9);
47
47
  outputaiFiles.forEach(file => {
48
48
  expect(file).toMatch(/^\.outputai\//);
49
49
  });
@@ -77,12 +77,18 @@ describe('coding_agents service', () => {
77
77
  missingFiles: [
78
78
  '.outputai/AGENTS.md',
79
79
  '.outputai/agents/workflow_planner.md',
80
+ '.outputai/agents/workflow_quality.md',
81
+ '.outputai/agents/context_fetcher.md',
82
+ '.outputai/agents/prompt_writer.md',
80
83
  '.outputai/commands/plan_workflow.md',
81
84
  '.outputai/commands/build_workflow.md',
82
85
  '.outputai/meta/pre_flight.md',
83
86
  '.outputai/meta/post_flight.md',
84
87
  'CLAUDE.md',
85
88
  '.claude/agents/workflow_planner.md',
89
+ '.claude/agents/workflow_quality.md',
90
+ '.claude/agents/context_fetcher.md',
91
+ '.claude/agents/prompt_writer.md',
86
92
  '.claude/commands/plan_workflow.md',
87
93
  '.claude/commands/build_workflow.md'
88
94
  ],
@@ -98,54 +104,21 @@ describe('coding_agents service', () => {
98
104
  missingFiles: [],
99
105
  isComplete: true
100
106
  });
101
- expect(access).toHaveBeenCalledTimes(11); // dir + 6 outputai + 4 claude-code
107
+ expect(access).toHaveBeenCalledTimes(17); // dir + 9 outputai + 7 claude-code
102
108
  });
103
109
  it('should return missing files when some files do not exist', async () => {
104
- const calls = [];
105
- vi.mocked(access).mockImplementation(async () => {
106
- const callNum = calls.length + 1;
107
- calls.push(callNum);
108
- // Call 1: directory exists
109
- if (callNum === 1) {
110
- return undefined;
111
- }
112
- // Call 2: .outputai/AGENTS.md exists
113
- if (callNum === 2) {
114
- return undefined;
115
- }
116
- // Call 3: .outputai/agents/workflow_planner.md missing
117
- if (callNum === 3) {
118
- throw { code: 'ENOENT' };
119
- }
120
- // Call 4: .outputai/commands/plan_workflow.md exists
121
- if (callNum === 4) {
122
- return undefined;
123
- }
124
- // Call 5: .outputai/commands/build_workflow.md exists
125
- if (callNum === 5) {
126
- return undefined;
127
- }
128
- // Call 6: .outputai/meta/pre_flight.md exists
129
- if (callNum === 6) {
130
- return undefined;
131
- }
132
- // Call 7: .outputai/meta/post_flight.md exists
133
- if (callNum === 7) {
134
- return undefined;
135
- }
136
- // Call 8: CLAUDE.md exists
137
- if (callNum === 8) {
138
- return undefined;
139
- }
140
- // Call 9: .claude/agents/workflow_planner.md missing
141
- if (callNum === 9) {
142
- throw { code: 'ENOENT' };
143
- }
144
- // Call 10: .claude/commands/plan_workflow.md exists
145
- if (callNum === 10) {
146
- return undefined;
110
+ const missingFiles = new Set([
111
+ '.outputai/agents/workflow_planner.md',
112
+ '.claude/agents/workflow_planner.md'
113
+ ]);
114
+ vi.mocked(access).mockImplementation(async (path) => {
115
+ const pathStr = path.toString();
116
+ // Check if this path ends with any of the missing files
117
+ for (const missing of missingFiles) {
118
+ if (pathStr.endsWith(missing)) {
119
+ throw { code: 'ENOENT' };
120
+ }
147
121
  }
148
- // Call 11: .claude/commands/build_workflow.md exists
149
122
  return undefined;
150
123
  });
151
124
  const result = await checkAgentStructure('/test/project');
@@ -159,18 +132,20 @@ describe('coding_agents service', () => {
159
132
  });
160
133
  });
161
134
  it('should check all required files even when directory exists', async () => {
162
- vi.mocked(access)
163
- .mockResolvedValueOnce(undefined) // directory
164
- .mockResolvedValueOnce(undefined) // .outputai/AGENTS.md
165
- .mockRejectedValueOnce({ code: 'ENOENT' }) // .outputai/agents/workflow_planner.md
166
- .mockRejectedValueOnce({ code: 'ENOENT' }) // .outputai/commands/plan_workflow.md
167
- .mockResolvedValueOnce(undefined) // .outputai/commands/build_workflow.md
168
- .mockResolvedValueOnce(undefined) // .outputai/meta/pre_flight.md
169
- .mockResolvedValueOnce(undefined) // .outputai/meta/post_flight.md
170
- .mockResolvedValueOnce(undefined) // CLAUDE.md
171
- .mockResolvedValueOnce(undefined) // .claude/agents/workflow_planner.md
172
- .mockRejectedValueOnce({ code: 'ENOENT' }) // .claude/commands/plan_workflow.md
173
- .mockResolvedValueOnce(undefined); // .claude/commands/build_workflow.md
135
+ const missingFiles = new Set([
136
+ '.outputai/agents/workflow_planner.md',
137
+ '.outputai/commands/plan_workflow.md',
138
+ '.claude/commands/plan_workflow.md'
139
+ ]);
140
+ vi.mocked(access).mockImplementation(async (path) => {
141
+ const pathStr = path.toString();
142
+ for (const missing of missingFiles) {
143
+ if (pathStr.endsWith(missing)) {
144
+ throw { code: 'ENOENT' };
145
+ }
146
+ }
147
+ return undefined;
148
+ });
174
149
  const result = await checkAgentStructure('/test/project');
175
150
  expect(result.dirExists).toBe(true);
176
151
  expect(result.missingFiles).toEqual([
@@ -207,8 +182,8 @@ describe('coding_agents service', () => {
207
182
  });
208
183
  // Should create outputai files (6 templates)
209
184
  expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining('AGENTS.md'), expect.any(String), 'utf-8');
210
- // Should create symlinks (4 symlinks for claude-code)
211
- expect(fs.symlink).toHaveBeenCalledTimes(4);
185
+ // Should create symlinks (7 symlinks for claude-code)
186
+ expect(fs.symlink).toHaveBeenCalledTimes(7);
212
187
  });
213
188
  it('should skip existing files when force is false', async () => {
214
189
  // Mock some files exist
@@ -3,3 +3,4 @@
3
3
  */
4
4
  export declare const getEjectSuccessMessage: (destPath: string, outputFile: string, binName: string) => string;
5
5
  export declare const getProjectSuccessMessage: (folderName: string, installSuccess: boolean, envConfigured?: boolean) => string;
6
+ export declare const getWorkflowGenerateSuccessMessage: (workflowName: string, targetDir: string, filesCreated: string[]) => string;
@@ -176,7 +176,7 @@ export const getProjectSuccessMessage = (folderName, installSuccess, envConfigur
176
176
  note: 'Launches Temporal, Redis, PostgreSQL, API, Worker, and UI'
177
177
  }, {
178
178
  step: 'Run example workflow',
179
- command: 'output workflow run simple --input \'{"question": "who really is ada lovelace?"}\'',
179
+ command: 'output workflow run simple --input src/simple/scenarios/question_ada_lovelace.json',
180
180
  note: 'Execute in a new terminal after services are running'
181
181
  }, {
182
182
  step: 'Monitor workflows',
@@ -231,3 +231,67 @@ ${ux.colorize('dim', ' with AI assistance.')}
231
231
  ${ux.colorize('green', ux.colorize('bold', 'Happy building with Output SDK! 🚀'))}
232
232
  `;
233
233
  };
234
+ export const getWorkflowGenerateSuccessMessage = (workflowName, targetDir, filesCreated) => {
235
+ const divider = ux.colorize('dim', '─'.repeat(80));
236
+ const bulletPoint = ux.colorize('green', '▸');
237
+ const formattedFiles = filesCreated.map(file => {
238
+ return ` ${bulletPoint} ${formatPath(file)}`;
239
+ }).join('\n');
240
+ const steps = [
241
+ {
242
+ step: 'Navigate to workflow directory',
243
+ command: `cd ${targetDir}`
244
+ },
245
+ {
246
+ step: 'Edit workflow files',
247
+ note: 'Customize workflow.ts, steps.ts, and prompts to match your requirements'
248
+ },
249
+ {
250
+ step: 'Configure environment',
251
+ command: 'Edit .env file',
252
+ note: 'Add your LLM provider credentials (ANTHROPIC_API_KEY or OPENAI_API_KEY)'
253
+ },
254
+ {
255
+ step: 'Test with example scenario',
256
+ command: `output workflow run ${workflowName} --input ${targetDir}/scenarios/test_input.json`,
257
+ note: 'Run after starting services with "output dev"'
258
+ }
259
+ ];
260
+ const formattedSteps = steps.map((item, index) => {
261
+ const stepNumber = ux.colorize('dim', `${index + 1}.`);
262
+ const stepText = ux.colorize('white', item.step);
263
+ const command = item.command ? `\n ${bulletPoint} ${formatCommand(item.command)}` : '';
264
+ const note = item.note ? `\n ${ux.colorize('dim', ` ${item.note}`)}` : '';
265
+ return ` ${stepNumber} ${stepText}${command}${note}`;
266
+ }).join('\n\n');
267
+ return `
268
+ ${divider}
269
+
270
+ ${ux.colorize('bold', ux.colorize('green', '✅ SUCCESS!'))} ${ux.colorize('bold', `Workflow "${workflowName}" created`)}
271
+
272
+ ${divider}
273
+
274
+ ${createSectionHeader('WORKFLOW DETAILS', '📁')}
275
+
276
+ ${bulletPoint} ${ux.colorize('white', 'Name:')} ${formatPath(workflowName)}
277
+ ${bulletPoint} ${ux.colorize('white', 'Location:')} ${formatPath(targetDir)}
278
+
279
+ ${divider}
280
+
281
+ ${createSectionHeader('FILES CREATED', '📄')}
282
+
283
+ ${formattedFiles}
284
+
285
+ ${divider}
286
+
287
+ ${createSectionHeader('NEXT STEPS', '🚀')}
288
+
289
+ ${formattedSteps}
290
+
291
+ ${divider}
292
+
293
+ ${ux.colorize('dim', '💡 Tip: Check the README.md in your workflow directory for detailed documentation.')}
294
+
295
+ ${ux.colorize('green', ux.colorize('bold', 'Happy building! 🛠️'))}
296
+ `;
297
+ };
@@ -0,0 +1,77 @@
1
+ /* eslint-disable no-restricted-syntax, init-declarations */
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
+ import * as fs from 'node:fs/promises';
4
+ import * as path from 'node:path';
5
+ import * as os from 'node:os';
6
+ import { generateWorkflow } from './workflow_generator.js';
7
+ describe('Workflow Generator', () => {
8
+ let tempDir;
9
+ beforeEach(async () => {
10
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'workflow-gen-test-'));
11
+ });
12
+ afterEach(async () => {
13
+ await fs.rm(tempDir, { recursive: true, force: true });
14
+ });
15
+ describe('skeleton generation', () => {
16
+ it('should create prompts folder with prompt template', async () => {
17
+ const result = await generateWorkflow({
18
+ name: 'testWorkflow',
19
+ description: 'Test workflow',
20
+ outputDir: tempDir,
21
+ skeleton: true,
22
+ force: false
23
+ });
24
+ const promptPath = path.join(result.targetDir, 'prompts', 'prompt@v1.prompt');
25
+ const promptExists = await fs.access(promptPath).then(() => true).catch(() => false);
26
+ expect(promptExists).toBe(true);
27
+ expect(result.filesCreated).toContain('prompts/prompt@v1.prompt');
28
+ });
29
+ it('should create scenarios folder with test_input.json', async () => {
30
+ const result = await generateWorkflow({
31
+ name: 'testWorkflow',
32
+ description: 'Test workflow',
33
+ outputDir: tempDir,
34
+ skeleton: true,
35
+ force: false
36
+ });
37
+ const scenarioPath = path.join(result.targetDir, 'scenarios', 'test_input.json');
38
+ const scenarioExists = await fs.access(scenarioPath).then(() => true).catch(() => false);
39
+ expect(scenarioExists).toBe(true);
40
+ expect(result.filesCreated).toContain('scenarios/test_input.json');
41
+ });
42
+ it('should create valid JSON in scenario file', async () => {
43
+ const result = await generateWorkflow({
44
+ name: 'testWorkflow',
45
+ description: 'Test workflow',
46
+ outputDir: tempDir,
47
+ skeleton: true,
48
+ force: false
49
+ });
50
+ const scenarioPath = path.join(result.targetDir, 'scenarios', 'test_input.json');
51
+ const content = await fs.readFile(scenarioPath, 'utf-8');
52
+ const parsed = JSON.parse(content);
53
+ expect(parsed).toHaveProperty('prompt');
54
+ expect(parsed).toHaveProperty('data');
55
+ });
56
+ it('should create all expected skeleton files', async () => {
57
+ const result = await generateWorkflow({
58
+ name: 'testWorkflow',
59
+ description: 'Test workflow',
60
+ outputDir: tempDir,
61
+ skeleton: true,
62
+ force: false
63
+ });
64
+ const expectedFiles = [
65
+ 'workflow.ts',
66
+ 'steps.ts',
67
+ 'README.md',
68
+ '.env',
69
+ 'prompts/prompt@v1.prompt',
70
+ 'scenarios/test_input.json'
71
+ ];
72
+ for (const file of expectedFiles) {
73
+ expect(result.filesCreated).toContain(file);
74
+ }
75
+ });
76
+ });
77
+ });