@output.ai/cli 0.0.1 → 0.0.3

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 (48) hide show
  1. package/README.md +44 -12
  2. package/dist/api/generated/api.d.ts +13 -13
  3. package/dist/api/generated/api.js +1 -1
  4. package/dist/commands/agents/init.d.ts +18 -0
  5. package/dist/commands/agents/init.js +175 -0
  6. package/dist/commands/agents/init.spec.d.ts +1 -0
  7. package/dist/commands/agents/init.spec.js +227 -0
  8. package/dist/commands/workflow/generate.js +1 -2
  9. package/dist/commands/workflow/generate.spec.js +0 -6
  10. package/dist/commands/workflow/list.d.ts +1 -1
  11. package/dist/commands/workflow/list.js +26 -42
  12. package/dist/commands/workflow/output.d.ts +13 -0
  13. package/dist/commands/workflow/output.js +49 -0
  14. package/dist/commands/workflow/output.test.d.ts +1 -0
  15. package/dist/commands/workflow/output.test.js +23 -0
  16. package/dist/commands/workflow/run.d.ts +15 -0
  17. package/dist/commands/workflow/run.js +66 -0
  18. package/dist/commands/workflow/run.test.d.ts +1 -0
  19. package/dist/commands/workflow/run.test.js +26 -0
  20. package/dist/commands/workflow/start.d.ts +14 -0
  21. package/dist/commands/workflow/start.js +57 -0
  22. package/dist/commands/workflow/start.test.d.ts +1 -0
  23. package/dist/commands/workflow/start.test.js +23 -0
  24. package/dist/commands/workflow/status.d.ts +13 -0
  25. package/dist/commands/workflow/status.js +56 -0
  26. package/dist/commands/workflow/status.test.d.ts +1 -0
  27. package/dist/commands/workflow/status.test.js +33 -0
  28. package/dist/commands/workflow/stop.d.ts +10 -0
  29. package/dist/commands/workflow/stop.js +31 -0
  30. package/dist/commands/workflow/stop.test.d.ts +1 -0
  31. package/dist/commands/workflow/stop.test.js +17 -0
  32. package/dist/templates/agent_instructions/AGENTS.md.template +30 -0
  33. package/dist/templates/agent_instructions/agents/workflow_planner.md.template +104 -0
  34. package/dist/templates/agent_instructions/commands/plan_workflow.md.template +466 -0
  35. package/dist/templates/agent_instructions/meta/post_flight.md.template +94 -0
  36. package/dist/templates/agent_instructions/meta/pre_flight.md.template +60 -0
  37. package/dist/templates/workflow/README.md.template +5 -5
  38. package/dist/utils/constants.d.ts +5 -0
  39. package/dist/utils/constants.js +4 -0
  40. package/dist/utils/error_handler.d.ts +8 -0
  41. package/dist/utils/error_handler.js +25 -0
  42. package/dist/utils/input_parser.d.ts +1 -0
  43. package/dist/utils/input_parser.js +19 -0
  44. package/dist/utils/output_formatter.d.ts +2 -0
  45. package/dist/utils/output_formatter.js +11 -0
  46. package/dist/utils/paths.d.ts +5 -0
  47. package/dist/utils/paths.js +8 -1
  48. package/package.json +28 -30
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @output.ai/cli
2
2
 
3
- CLI tool for generating Output.ai workflows.
3
+ CLI tool for generating Output workflows.
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,25 +18,25 @@ npx @output.ai/cli
18
18
 
19
19
  ```bash
20
20
  # List all available workflows (simple list, default)
21
- output-cli workflow list
21
+ output workflow list
22
22
 
23
23
  # List workflows with custom API URL
24
- API_URL=http://localhost:3001 output-cli workflow list
24
+ API_URL=http://localhost:3001 output workflow list
25
25
 
26
26
  # List workflows with authentication
27
- API_AUTH_TOKEN=your-token output-cli workflow list
27
+ API_AUTH_TOKEN=your-token output workflow list
28
28
 
29
29
  # Show detailed table view with all information
30
- output-cli workflow list --format table
30
+ output workflow list --format table
31
31
 
32
32
  # Show detailed table with expanded parameter info
33
- output-cli workflow list --format table --detailed
33
+ output workflow list --format table --detailed
34
34
 
35
35
  # List workflows in JSON format
36
- output-cli workflow list --format json
36
+ output workflow list --format json
37
37
 
38
38
  # Filter workflows by name (partial match)
39
- output-cli workflow list --filter simple
39
+ output workflow list --filter simple
40
40
  ```
41
41
 
42
42
  #### Command Options
@@ -51,16 +51,16 @@ The list command connects to the API server and retrieves all available workflow
51
51
 
52
52
  ```bash
53
53
  # Generate a complete workflow with example steps
54
- output-cli workflow generate my-workflow --description "My awesome workflow"
54
+ output workflow generate my-workflow --description "My awesome workflow"
55
55
 
56
56
  # Generate a minimal skeleton workflow
57
- output-cli workflow generate my-workflow --skeleton
57
+ output workflow generate my-workflow --skeleton
58
58
 
59
59
  # Generate in a specific directory
60
- output-cli workflow generate my-workflow --output-dir ./src/workflows
60
+ output workflow generate my-workflow --output-dir ./src/workflows
61
61
 
62
62
  # Force overwrite existing workflow
63
- output-cli workflow generate my-workflow --force
63
+ output workflow generate my-workflow --force
64
64
  ```
65
65
 
66
66
  #### Command Options
@@ -83,6 +83,38 @@ my-workflow/
83
83
  └── README.md # Workflow documentation
84
84
  ```
85
85
 
86
+ ### Initialize Agent Configuration
87
+
88
+ ```bash
89
+ # Initialize agent configuration for your coding agent
90
+ output-cli agents init
91
+
92
+ # Specify your agent provider (default: claude-code)
93
+ output-cli agents init --agent-provider claude-code
94
+
95
+ # Force overwrite existing configuration
96
+ output-cli agents init --force
97
+ ```
98
+
99
+ #### What It Does
100
+
101
+ Sets up your coding agent of choice with context files, sub-agents, and slash commands for working with the Output SDK effectively. It creates a `.outputai/` directory and wires up your coding agent to the context files.
102
+
103
+ **Note:** Currently this only works for [Claude Code](https://claude.ai/code), but we plan to add support for other coding agents over time.
104
+
105
+ #### Command Options
106
+
107
+ - `--agent-provider` - Specify the coding agent provider (default: `claude-code`)
108
+ - `--force, -f` - Overwrite existing agent configuration files
109
+
110
+ #### Output Agent Commands
111
+
112
+ **`/plan_workflow`** - Guides you through structured workflow planning to create comprehensive implementation blueprints before writing code.
113
+
114
+ #### Output Sub-Agents
115
+
116
+ **`workflow_planner`** - A specialized AI agent that helps with workflow architecture and planning, including requirements analysis, schema design, and testing strategy definition.
117
+
86
118
  ## About
87
119
 
88
120
  Built with [OCLIF](https://oclif.io) - see their documentation for advanced CLI features, plugins, and configuration options.
@@ -35,7 +35,7 @@ export type PostWorkflowRunBody = {
35
35
  export type PostWorkflowRun200 = {
36
36
  /** The workflow execution id */
37
37
  workflowId?: string;
38
- /** The output of the the workflow */
38
+ /** The output of the workflow */
39
39
  output?: unknown;
40
40
  };
41
41
  export type PostWorkflowStartBody = {
@@ -156,8 +156,8 @@ export type getWorkflowIdStatusResponseError = (getWorkflowIdStatusResponse404)
156
156
  headers: Headers;
157
157
  };
158
158
  export type getWorkflowIdStatusResponse = (getWorkflowIdStatusResponseSuccess | getWorkflowIdStatusResponseError);
159
- export declare const getGetWorkflowIdStatusUrl: (id: unknown) => string;
160
- export declare const getWorkflowIdStatus: (id: unknown, options?: RequestInit) => Promise<getWorkflowIdStatusResponse>;
159
+ export declare const getGetWorkflowIdStatusUrl: (id: string) => string;
160
+ export declare const getWorkflowIdStatus: (id: string, options?: RequestInit) => Promise<getWorkflowIdStatusResponse>;
161
161
  /**
162
162
  * @summary Stop a workflow execution
163
163
  */
@@ -176,8 +176,8 @@ export type patchWorkflowIdStopResponseError = (patchWorkflowIdStopResponse404)
176
176
  headers: Headers;
177
177
  };
178
178
  export type patchWorkflowIdStopResponse = (patchWorkflowIdStopResponseSuccess | patchWorkflowIdStopResponseError);
179
- export declare const getPatchWorkflowIdStopUrl: (id: unknown) => string;
180
- export declare const patchWorkflowIdStop: (id: unknown, options?: RequestInit) => Promise<patchWorkflowIdStopResponse>;
179
+ export declare const getPatchWorkflowIdStopUrl: (id: string) => string;
180
+ export declare const patchWorkflowIdStop: (id: string, options?: RequestInit) => Promise<patchWorkflowIdStopResponse>;
181
181
  /**
182
182
  * @summary Return the output of a workflow
183
183
  */
@@ -196,8 +196,8 @@ export type getWorkflowIdOutputResponseError = (getWorkflowIdOutputResponse404)
196
196
  headers: Headers;
197
197
  };
198
198
  export type getWorkflowIdOutputResponse = (getWorkflowIdOutputResponseSuccess | getWorkflowIdOutputResponseError);
199
- export declare const getGetWorkflowIdOutputUrl: (id: unknown) => string;
200
- export declare const getWorkflowIdOutput: (id: unknown, options?: RequestInit) => Promise<getWorkflowIdOutputResponse>;
199
+ export declare const getGetWorkflowIdOutputUrl: (id: string) => string;
200
+ export declare const getWorkflowIdOutput: (id: string, options?: RequestInit) => Promise<getWorkflowIdOutputResponse>;
201
201
  /**
202
202
  * @summary Return the trace of a workflow execution
203
203
  */
@@ -216,8 +216,8 @@ export type getWorkflowIdTraceResponseError = (getWorkflowIdTraceResponse404) &
216
216
  headers: Headers;
217
217
  };
218
218
  export type getWorkflowIdTraceResponse = (getWorkflowIdTraceResponseSuccess | getWorkflowIdTraceResponseError);
219
- export declare const getGetWorkflowIdTraceUrl: (id: unknown) => string;
220
- export declare const getWorkflowIdTrace: (id: unknown, options?: RequestInit) => Promise<getWorkflowIdTraceResponse>;
219
+ export declare const getGetWorkflowIdTraceUrl: (id: string) => string;
220
+ export declare const getWorkflowIdTrace: (id: string, options?: RequestInit) => Promise<getWorkflowIdTraceResponse>;
221
221
  /**
222
222
  * @summary Get a specific workflow catalog by ID
223
223
  */
@@ -229,8 +229,8 @@ export type getWorkflowCatalogIdResponseSuccess = (getWorkflowCatalogIdResponse2
229
229
  headers: Headers;
230
230
  };
231
231
  export type getWorkflowCatalogIdResponse = (getWorkflowCatalogIdResponseSuccess);
232
- export declare const getGetWorkflowCatalogIdUrl: (id: unknown) => string;
233
- export declare const getWorkflowCatalogId: (id: unknown, options?: RequestInit) => Promise<getWorkflowCatalogIdResponse>;
232
+ export declare const getGetWorkflowCatalogIdUrl: (id: string) => string;
233
+ export declare const getWorkflowCatalogId: (id: string, options?: RequestInit) => Promise<getWorkflowCatalogIdResponse>;
234
234
  /**
235
235
  * @summary Get the default workflow catalog
236
236
  */
@@ -255,8 +255,8 @@ export type postWorkflowIdFeedbackResponseSuccess = (postWorkflowIdFeedbackRespo
255
255
  headers: Headers;
256
256
  };
257
257
  export type postWorkflowIdFeedbackResponse = (postWorkflowIdFeedbackResponseSuccess);
258
- export declare const getPostWorkflowIdFeedbackUrl: (id: unknown) => string;
259
- export declare const postWorkflowIdFeedback: (id: unknown, postWorkflowIdFeedbackBody: PostWorkflowIdFeedbackBody, options?: RequestInit) => Promise<postWorkflowIdFeedbackResponse>;
258
+ export declare const getPostWorkflowIdFeedbackUrl: (id: string) => string;
259
+ export declare const postWorkflowIdFeedback: (id: string, postWorkflowIdFeedbackBody: PostWorkflowIdFeedbackBody, options?: RequestInit) => Promise<postWorkflowIdFeedbackResponse>;
260
260
  /**
261
261
  * @summary A dummy post endpoint for test only
262
262
  */
@@ -2,7 +2,7 @@
2
2
  * Generated by orval v7.13.0 🍺
3
3
  * Do not edit manually.
4
4
  * Output.ai SDK API
5
- * API for managing and executing Temporal workflows through Flow SDK
5
+ * API for managing and executing Temporal workflows through Output SDK
6
6
  * OpenAPI spec version: 1.0.0
7
7
  */
8
8
  import { customFetchInstance } from '../http_client.js';
@@ -0,0 +1,18 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Init extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {};
6
+ static flags: {
7
+ 'agent-provider': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
+ force: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
+ };
10
+ run(): Promise<void>;
11
+ private prepareTemplateVariables;
12
+ private processMappings;
13
+ private ensureDirectoryExists;
14
+ private fileExists;
15
+ private createFromTemplate;
16
+ private createSymlink;
17
+ private copyFile;
18
+ }
@@ -0,0 +1,175 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { getTemplateDir } from '../../utils/paths.js';
5
+ import { processTemplate } from '../../utils/template.js';
6
+ const AGENT_CONFIGS = {
7
+ outputai: {
8
+ id: 'outputai',
9
+ name: 'OutputAI Core Files',
10
+ mappings: [
11
+ {
12
+ type: 'template',
13
+ from: 'AGENTS.md.template',
14
+ to: '.outputai/AGENTS.md'
15
+ },
16
+ {
17
+ type: 'template',
18
+ from: 'agents/workflow_planner.md.template',
19
+ to: '.outputai/agents/workflow_planner.md'
20
+ },
21
+ {
22
+ type: 'template',
23
+ from: 'commands/plan_workflow.md.template',
24
+ to: '.outputai/commands/plan_workflow.md'
25
+ }
26
+ ]
27
+ },
28
+ 'claude-code': {
29
+ id: 'claude-code',
30
+ name: 'Claude Code',
31
+ mappings: [
32
+ {
33
+ type: 'symlink',
34
+ from: '.outputai/AGENTS.md',
35
+ to: 'CLAUDE.md'
36
+ },
37
+ {
38
+ type: 'symlink',
39
+ from: '.outputai/agents/workflow_planner.md',
40
+ to: '.claude/agents/workflow_planner.md'
41
+ },
42
+ {
43
+ type: 'symlink',
44
+ from: '.outputai/commands/plan_workflow.md',
45
+ to: '.claude/commands/plan_workflow.md'
46
+ }
47
+ ]
48
+ }
49
+ };
50
+ export default class Init extends Command {
51
+ static description = 'Initialize agent configuration files for AI assistant integration';
52
+ static examples = [
53
+ '<%= config.bin %> <%= command.id %>',
54
+ '<%= config.bin %> <%= command.id %> --agent-provider claude-code',
55
+ '<%= config.bin %> <%= command.id %> --force'
56
+ ];
57
+ static args = {};
58
+ static flags = {
59
+ 'agent-provider': Flags.string({
60
+ description: 'Specify the coding agent provider',
61
+ default: AGENT_CONFIGS['claude-code'].id,
62
+ options: [AGENT_CONFIGS['claude-code'].id]
63
+ }),
64
+ force: Flags.boolean({
65
+ char: 'f',
66
+ description: 'Overwrite existing files',
67
+ default: false
68
+ })
69
+ };
70
+ async run() {
71
+ const { flags } = await this.parse(Init);
72
+ this.log('Initializing agent configuration for Claude Code...');
73
+ try {
74
+ const variables = this.prepareTemplateVariables();
75
+ await this.processMappings(AGENT_CONFIGS.outputai, variables, flags.force);
76
+ await this.processMappings(AGENT_CONFIGS[flags['agent-provider']], variables, flags.force);
77
+ this.log('✅ Agent configuration initialized successfully!');
78
+ this.log('');
79
+ this.log('Created:');
80
+ this.log(' • .outputai/ directory with agent and command configurations');
81
+ this.log(' • .claude/ directory with symlinks for Claude Code integration');
82
+ this.log('');
83
+ this.log('Claude Code will automatically detect and use these configurations.');
84
+ }
85
+ catch (error) {
86
+ if (error.code === 'EACCES') {
87
+ this.error('Permission denied. Please check file permissions and try again.');
88
+ }
89
+ this.error(`Failed to initialize agent configuration: ${error.message}`);
90
+ }
91
+ }
92
+ prepareTemplateVariables() {
93
+ return {
94
+ date: new Date().toLocaleDateString('en-US', {
95
+ year: 'numeric',
96
+ month: 'long',
97
+ day: 'numeric'
98
+ })
99
+ };
100
+ }
101
+ async processMappings(config, variables, force) {
102
+ for (const mapping of config.mappings) {
103
+ const dir = path.dirname(mapping.to);
104
+ await this.ensureDirectoryExists(dir);
105
+ if (!force && await this.fileExists(mapping.to)) {
106
+ this.warn(`File already exists: ${mapping.to} (use --force to overwrite)`);
107
+ continue;
108
+ }
109
+ switch (mapping.type) {
110
+ case 'template':
111
+ await this.createFromTemplate(mapping.from, mapping.to, variables);
112
+ break;
113
+ case 'symlink':
114
+ await this.createSymlink(mapping.from, mapping.to);
115
+ break;
116
+ case 'copy':
117
+ await this.copyFile(mapping.from, mapping.to);
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ async ensureDirectoryExists(dir) {
123
+ try {
124
+ await fs.mkdir(dir, { recursive: true });
125
+ this.debug(`Created directory: ${dir}`);
126
+ }
127
+ catch (error) {
128
+ if (error.code !== 'EEXIST') {
129
+ throw error;
130
+ }
131
+ }
132
+ }
133
+ async fileExists(filePath) {
134
+ try {
135
+ await fs.stat(filePath);
136
+ return true;
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ }
142
+ async createFromTemplate(template, output, variables) {
143
+ const templateDir = getTemplateDir('agent_instructions');
144
+ const templatePath = path.join(templateDir, template);
145
+ const content = await fs.readFile(templatePath, 'utf-8');
146
+ const processed = processTemplate(content, variables);
147
+ await fs.writeFile(output, processed, 'utf-8');
148
+ this.debug(`Created from template: ${output}`);
149
+ }
150
+ async createSymlink(source, target) {
151
+ try {
152
+ if (await this.fileExists(target)) {
153
+ await fs.unlink(target);
154
+ }
155
+ const relativePath = path.relative(path.dirname(target), source);
156
+ await fs.symlink(relativePath, target);
157
+ this.debug(`Created symlink: ${target} -> ${source}`);
158
+ }
159
+ catch (error) {
160
+ const code = error.code;
161
+ if (code === 'ENOTSUP' || code === 'EPERM') {
162
+ this.debug(`Symlinks not supported, creating copy: ${target}`);
163
+ const content = await fs.readFile(source, 'utf-8');
164
+ await fs.writeFile(target, content, 'utf-8');
165
+ return;
166
+ }
167
+ throw error;
168
+ }
169
+ }
170
+ async copyFile(source, target) {
171
+ const content = await fs.readFile(source, 'utf-8');
172
+ await fs.writeFile(target, content, 'utf-8');
173
+ this.debug(`Copied file: ${source} -> ${target}`);
174
+ }
175
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,227 @@
1
+ /* eslint-disable no-restricted-syntax, @typescript-eslint/no-explicit-any, init-declarations */
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import Init from './init.js';
6
+ import { getTemplateDir } from '../../utils/paths.js';
7
+ import { processTemplate } from '../../utils/template.js';
8
+ vi.mock('node:fs/promises');
9
+ vi.mock('node:path', async () => {
10
+ const actual = await vi.importActual('node:path');
11
+ return {
12
+ ...actual,
13
+ join: vi.fn((...args) => args.join('/')),
14
+ dirname: vi.fn(p => p.split('/').slice(0, -1).join('/') || '.'),
15
+ relative: vi.fn((from, to) => {
16
+ if (to === 'CLAUDE.md' && from === '.') {
17
+ return '.outputai/AGENTS.md';
18
+ }
19
+ if (to.includes('.outputai/agents')) {
20
+ return `../../.outputai/agents/${path.basename(to)}`;
21
+ }
22
+ if (to.includes('.outputai/commands')) {
23
+ return `../../.outputai/commands/${path.basename(to)}`;
24
+ }
25
+ return to;
26
+ })
27
+ };
28
+ });
29
+ vi.mock('../../utils/paths.js');
30
+ vi.mock('../../utils/template.js');
31
+ describe('agents init', () => {
32
+ let mockGetTemplateDir;
33
+ let mockProcessTemplate;
34
+ const createTestCommand = (args = []) => {
35
+ const cmd = new Init(args, {});
36
+ cmd.log = vi.fn();
37
+ cmd.warn = vi.fn();
38
+ cmd.error = vi.fn();
39
+ cmd.debug = vi.fn();
40
+ cmd.parse = vi.fn();
41
+ return cmd;
42
+ };
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ vi.mocked(fs.mkdir).mockResolvedValue(undefined);
46
+ vi.mocked(fs.writeFile).mockResolvedValue(undefined);
47
+ vi.mocked(fs.symlink).mockResolvedValue(undefined);
48
+ vi.mocked(fs.stat).mockRejectedValue({ code: 'ENOENT' });
49
+ vi.mocked(fs.unlink).mockResolvedValue(undefined);
50
+ vi.mocked(fs.readFile).mockResolvedValue('Template content with {{date}}');
51
+ mockGetTemplateDir = vi.mocked(getTemplateDir);
52
+ mockGetTemplateDir.mockReturnValue('/templates/agent_instructions');
53
+ mockProcessTemplate = vi.mocked(processTemplate);
54
+ mockProcessTemplate.mockImplementation((content, variables) => {
55
+ return content.replace(/\{\{(\w+)\}\}/g, (_match, key) => variables[key] || _match);
56
+ });
57
+ });
58
+ afterEach(() => {
59
+ vi.restoreAllMocks();
60
+ });
61
+ describe('command structure', () => {
62
+ it('should have correct description', () => {
63
+ expect(Init.description).toBeDefined();
64
+ expect(Init.description).toContain('agent configuration');
65
+ });
66
+ it('should have correct examples', () => {
67
+ expect(Init.examples).toBeDefined();
68
+ expect(Array.isArray(Init.examples)).toBe(true);
69
+ });
70
+ it('should have no required arguments', () => {
71
+ expect(Init.args).toBeDefined();
72
+ expect(Object.keys(Init.args)).toHaveLength(0);
73
+ });
74
+ it('should have appropriate flags', () => {
75
+ expect(Init.flags).toBeDefined();
76
+ expect(Init.flags.force).toBeDefined();
77
+ });
78
+ });
79
+ describe('directory creation', () => {
80
+ it('should create .outputai directory structure', async () => {
81
+ const mockMkdir = vi.mocked(fs.mkdir);
82
+ mockMkdir.mockResolvedValue(undefined);
83
+ const cmd = createTestCommand();
84
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
85
+ await cmd.run();
86
+ expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai'), expect.objectContaining({ recursive: true }));
87
+ expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents'), expect.objectContaining({ recursive: true }));
88
+ expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands'), expect.objectContaining({ recursive: true }));
89
+ });
90
+ it('should create .claude directory structure', async () => {
91
+ const mockMkdir = vi.mocked(fs.mkdir);
92
+ mockMkdir.mockResolvedValue(undefined);
93
+ const cmd = createTestCommand();
94
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
95
+ await cmd.run();
96
+ expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.claude'), expect.objectContaining({ recursive: true }));
97
+ expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.claude/agents'), expect.objectContaining({ recursive: true }));
98
+ expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.claude/commands'), expect.objectContaining({ recursive: true }));
99
+ });
100
+ });
101
+ describe('file creation from templates', () => {
102
+ it('should create AGENTS.md file from template', async () => {
103
+ const mockWriteFile = vi.mocked(fs.writeFile);
104
+ const mockReadFile = vi.mocked(fs.readFile);
105
+ mockReadFile.mockResolvedValue('Agent instructions {{date}}');
106
+ const cmd = createTestCommand();
107
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
108
+ await cmd.run();
109
+ expect(mockReadFile).toHaveBeenCalledWith(expect.stringContaining('AGENTS.md.template'), 'utf-8');
110
+ expect(mockWriteFile).toHaveBeenCalledWith('.outputai/AGENTS.md', expect.stringContaining('Agent instructions'), 'utf-8');
111
+ });
112
+ it('should create all agent configuration files', async () => {
113
+ const mockWriteFile = vi.mocked(fs.writeFile);
114
+ const cmd = createTestCommand();
115
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
116
+ await cmd.run();
117
+ const agentFiles = [
118
+ '.outputai/agents/workflow_planner.md'
119
+ ];
120
+ for (const file of agentFiles) {
121
+ expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
122
+ }
123
+ });
124
+ it('should create all command configuration files', async () => {
125
+ const mockWriteFile = vi.mocked(fs.writeFile);
126
+ const cmd = createTestCommand();
127
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
128
+ await cmd.run();
129
+ const commandFiles = [
130
+ '.outputai/commands/plan_workflow.md'
131
+ ];
132
+ for (const file of commandFiles) {
133
+ expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
134
+ }
135
+ });
136
+ });
137
+ describe('symlink creation', () => {
138
+ it('should create symlink from CLAUDE.md to .outputai/AGENTS.md', async () => {
139
+ const mockSymlink = vi.mocked(fs.symlink);
140
+ const mockStat = vi.mocked(fs.stat);
141
+ mockStat.mockImplementation(async (path) => {
142
+ if (typeof path === 'string' && path.startsWith('.outputai')) {
143
+ return { isFile: () => true };
144
+ }
145
+ throw { code: 'ENOENT' };
146
+ });
147
+ const cmd = createTestCommand();
148
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
149
+ await cmd.run();
150
+ expect(mockSymlink).toHaveBeenCalledWith('.outputai/AGENTS.md', 'CLAUDE.md');
151
+ });
152
+ it('should create symlinks for all agent files', async () => {
153
+ const mockSymlink = vi.mocked(fs.symlink);
154
+ const cmd = createTestCommand();
155
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
156
+ await cmd.run();
157
+ expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents/workflow_planner.md'), '.claude/agents/workflow_planner.md');
158
+ });
159
+ it('should create symlinks for all command files', async () => {
160
+ const mockSymlink = vi.mocked(fs.symlink);
161
+ const cmd = createTestCommand();
162
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
163
+ await cmd.run();
164
+ expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands/plan_workflow.md'), '.claude/commands/plan_workflow.md');
165
+ });
166
+ it('should handle Windows by copying files when symlinks are not supported', async () => {
167
+ const mockSymlink = vi.mocked(fs.symlink);
168
+ const mockWriteFile = vi.mocked(fs.writeFile);
169
+ const mockReadFile = vi.mocked(fs.readFile);
170
+ mockSymlink.mockRejectedValue({ code: 'ENOTSUP' });
171
+ mockReadFile.mockResolvedValue('File content');
172
+ const cmd = createTestCommand();
173
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
174
+ await cmd.run();
175
+ expect(mockReadFile).toHaveBeenCalledWith('.outputai/AGENTS.md', 'utf-8');
176
+ expect(mockWriteFile).toHaveBeenCalledWith('CLAUDE.md', 'File content', 'utf-8');
177
+ });
178
+ });
179
+ describe('error handling', () => {
180
+ it('should handle existing directory gracefully', async () => {
181
+ const mockMkdir = vi.mocked(fs.mkdir);
182
+ mockMkdir.mockRejectedValue({ code: 'EEXIST' });
183
+ const cmd = createTestCommand();
184
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
185
+ await cmd.run();
186
+ expect(cmd.error).not.toHaveBeenCalled();
187
+ });
188
+ it('should handle permission errors', async () => {
189
+ const mockMkdir = vi.mocked(fs.mkdir);
190
+ mockMkdir.mockRejectedValue({ code: 'EACCES' });
191
+ const cmd = createTestCommand();
192
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
193
+ await cmd.run();
194
+ expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Permission denied'));
195
+ });
196
+ it('should be idempotent (safe to run multiple times)', async () => {
197
+ const mockWriteFile = vi.mocked(fs.writeFile);
198
+ mockWriteFile.mockResolvedValue(undefined);
199
+ const cmd = createTestCommand();
200
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
201
+ await cmd.run();
202
+ await cmd.run();
203
+ expect(cmd.error).not.toHaveBeenCalled();
204
+ });
205
+ });
206
+ describe('force flag', () => {
207
+ it('should overwrite existing files when force flag is set', async () => {
208
+ const mockWriteFile = vi.mocked(fs.writeFile);
209
+ const mockStat = vi.mocked(fs.stat);
210
+ mockStat.mockResolvedValue({ isFile: () => true });
211
+ mockWriteFile.mockResolvedValue(undefined);
212
+ const cmd = createTestCommand(['--force']);
213
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: true }, args: {} });
214
+ await cmd.run();
215
+ expect(mockWriteFile).toHaveBeenCalled();
216
+ expect(cmd.warn).not.toHaveBeenCalled();
217
+ });
218
+ it('should warn about existing files without force flag', async () => {
219
+ const mockStat = vi.mocked(fs.stat);
220
+ mockStat.mockResolvedValue({ isFile: () => true });
221
+ const cmd = createTestCommand();
222
+ cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
223
+ await cmd.run();
224
+ expect(cmd.warn).toHaveBeenCalledWith(expect.stringContaining('already exists'));
225
+ });
226
+ });
227
+ });
@@ -2,7 +2,7 @@ import { Args, Command, Flags } from '@oclif/core';
2
2
  import { generateWorkflow } from '../../services/workflow_generator.js';
3
3
  import { DEFAULT_OUTPUT_DIRS } from '../../utils/paths.js';
4
4
  export default class Generate extends Command {
5
- static description = 'Generate a new Flow SDK workflow';
5
+ static description = 'Generate a new Output SDK workflow';
6
6
  static examples = [
7
7
  '<%= config.bin %> <%= command.id %> my-workflow',
8
8
  '<%= config.bin %> <%= command.id %> my-workflow --skeleton',
@@ -38,7 +38,6 @@ export default class Generate extends Command {
38
38
  };
39
39
  async run() {
40
40
  const { args, flags } = await this.parse(Generate);
41
- // Check if skeleton flag is required
42
41
  if (!flags.skeleton) {
43
42
  this.error('Full workflow generation not implemented yet. Please use --skeleton flag');
44
43
  }
@@ -3,32 +3,27 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
3
3
  import Generate from './generate.js';
4
4
  import { generateWorkflow } from '../../services/workflow_generator.js';
5
5
  import { InvalidNameError, WorkflowExistsError } from '../../types/errors.js';
6
- // Mock the generateWorkflow function
7
6
  vi.mock('../../services/workflow_generator.js');
8
7
  describe('Generate Command', () => {
9
8
  let mockGenerateWorkflow;
10
9
  let logSpy;
11
10
  const createCommand = () => {
12
11
  const cmd = new Generate([], {});
13
- // Mock OCLIF methods
14
12
  cmd.log = vi.fn();
15
13
  cmd.error = vi.fn((message) => {
16
14
  throw new Error(message);
17
15
  });
18
- // Mock parse method
19
16
  cmd.parse = vi.fn();
20
17
  logSpy = cmd.log;
21
18
  return cmd;
22
19
  };
23
20
  beforeEach(() => {
24
21
  vi.clearAllMocks();
25
- // Mock generateWorkflow function
26
22
  mockGenerateWorkflow = vi.mocked(generateWorkflow);
27
23
  });
28
24
  describe('successful workflow generation', () => {
29
25
  it('should generate workflow with skeleton flag', async () => {
30
26
  const cmd = createCommand();
31
- // Mock parse return
32
27
  cmd.parse.mockResolvedValue({
33
28
  args: { name: 'test-workflow' },
34
29
  flags: {
@@ -38,7 +33,6 @@ describe('Generate Command', () => {
38
33
  force: false
39
34
  }
40
35
  });
41
- // Mock successful generation
42
36
  mockGenerateWorkflow.mockResolvedValue({
43
37
  workflowName: 'test-workflow',
44
38
  targetDir: '/tmp/test-workflow',