@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.
- package/README.md +44 -12
- package/dist/api/generated/api.d.ts +13 -13
- package/dist/api/generated/api.js +1 -1
- package/dist/commands/agents/init.d.ts +18 -0
- package/dist/commands/agents/init.js +175 -0
- package/dist/commands/agents/init.spec.d.ts +1 -0
- package/dist/commands/agents/init.spec.js +227 -0
- package/dist/commands/workflow/generate.js +1 -2
- package/dist/commands/workflow/generate.spec.js +0 -6
- package/dist/commands/workflow/list.d.ts +1 -1
- package/dist/commands/workflow/list.js +26 -42
- package/dist/commands/workflow/output.d.ts +13 -0
- package/dist/commands/workflow/output.js +49 -0
- package/dist/commands/workflow/output.test.d.ts +1 -0
- package/dist/commands/workflow/output.test.js +23 -0
- package/dist/commands/workflow/run.d.ts +15 -0
- package/dist/commands/workflow/run.js +66 -0
- package/dist/commands/workflow/run.test.d.ts +1 -0
- package/dist/commands/workflow/run.test.js +26 -0
- package/dist/commands/workflow/start.d.ts +14 -0
- package/dist/commands/workflow/start.js +57 -0
- package/dist/commands/workflow/start.test.d.ts +1 -0
- package/dist/commands/workflow/start.test.js +23 -0
- package/dist/commands/workflow/status.d.ts +13 -0
- package/dist/commands/workflow/status.js +56 -0
- package/dist/commands/workflow/status.test.d.ts +1 -0
- package/dist/commands/workflow/status.test.js +33 -0
- package/dist/commands/workflow/stop.d.ts +10 -0
- package/dist/commands/workflow/stop.js +31 -0
- package/dist/commands/workflow/stop.test.d.ts +1 -0
- package/dist/commands/workflow/stop.test.js +17 -0
- package/dist/templates/agent_instructions/AGENTS.md.template +30 -0
- package/dist/templates/agent_instructions/agents/workflow_planner.md.template +104 -0
- package/dist/templates/agent_instructions/commands/plan_workflow.md.template +466 -0
- package/dist/templates/agent_instructions/meta/post_flight.md.template +94 -0
- package/dist/templates/agent_instructions/meta/pre_flight.md.template +60 -0
- package/dist/templates/workflow/README.md.template +5 -5
- package/dist/utils/constants.d.ts +5 -0
- package/dist/utils/constants.js +4 -0
- package/dist/utils/error_handler.d.ts +8 -0
- package/dist/utils/error_handler.js +25 -0
- package/dist/utils/input_parser.d.ts +1 -0
- package/dist/utils/input_parser.js +19 -0
- package/dist/utils/output_formatter.d.ts +2 -0
- package/dist/utils/output_formatter.js +11 -0
- package/dist/utils/paths.d.ts +5 -0
- package/dist/utils/paths.js +8 -1
- 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
|
|
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
|
|
21
|
+
output workflow list
|
|
22
22
|
|
|
23
23
|
# List workflows with custom API URL
|
|
24
|
-
API_URL=http://localhost:3001 output
|
|
24
|
+
API_URL=http://localhost:3001 output workflow list
|
|
25
25
|
|
|
26
26
|
# List workflows with authentication
|
|
27
|
-
API_AUTH_TOKEN=your-token output
|
|
27
|
+
API_AUTH_TOKEN=your-token output workflow list
|
|
28
28
|
|
|
29
29
|
# Show detailed table view with all information
|
|
30
|
-
output
|
|
30
|
+
output workflow list --format table
|
|
31
31
|
|
|
32
32
|
# Show detailed table with expanded parameter info
|
|
33
|
-
output
|
|
33
|
+
output workflow list --format table --detailed
|
|
34
34
|
|
|
35
35
|
# List workflows in JSON format
|
|
36
|
-
output
|
|
36
|
+
output workflow list --format json
|
|
37
37
|
|
|
38
38
|
# Filter workflows by name (partial match)
|
|
39
|
-
output
|
|
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
|
|
54
|
+
output workflow generate my-workflow --description "My awesome workflow"
|
|
55
55
|
|
|
56
56
|
# Generate a minimal skeleton workflow
|
|
57
|
-
output
|
|
57
|
+
output workflow generate my-workflow --skeleton
|
|
58
58
|
|
|
59
59
|
# Generate in a specific directory
|
|
60
|
-
output
|
|
60
|
+
output workflow generate my-workflow --output-dir ./src/workflows
|
|
61
61
|
|
|
62
62
|
# Force overwrite existing workflow
|
|
63
|
-
output
|
|
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
|
|
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:
|
|
160
|
-
export declare const getWorkflowIdStatus: (id:
|
|
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:
|
|
180
|
-
export declare const patchWorkflowIdStop: (id:
|
|
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:
|
|
200
|
-
export declare const getWorkflowIdOutput: (id:
|
|
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:
|
|
220
|
-
export declare const getWorkflowIdTrace: (id:
|
|
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:
|
|
233
|
-
export declare const getWorkflowCatalogId: (id:
|
|
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:
|
|
259
|
-
export declare const postWorkflowIdFeedback: (id:
|
|
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
|
|
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
|
|
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',
|