@output.ai/cli 0.0.4 → 0.0.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.
- package/dist/api/http_client.js +1 -1
- package/dist/commands/agents/init.d.ts +2 -9
- package/dist/commands/agents/init.js +8 -145
- package/dist/commands/agents/init.spec.js +31 -26
- package/dist/commands/workflow/generate.d.ts +5 -5
- package/dist/commands/workflow/generate.js +2 -2
- package/dist/commands/workflow/generate.spec.js +2 -2
- package/dist/commands/workflow/list.d.ts +4 -4
- package/dist/commands/workflow/list.js +3 -3
- package/dist/commands/workflow/output.d.ts +2 -2
- package/dist/commands/workflow/output.js +4 -4
- package/dist/commands/workflow/plan.d.ts +12 -0
- package/dist/commands/workflow/plan.js +65 -0
- package/dist/commands/workflow/plan.spec.d.ts +1 -0
- package/dist/commands/workflow/plan.spec.js +339 -0
- package/dist/commands/workflow/run.d.ts +4 -4
- package/dist/commands/workflow/run.js +5 -5
- package/dist/commands/workflow/start.d.ts +3 -3
- package/dist/commands/workflow/start.js +3 -3
- package/dist/commands/workflow/status.d.ts +2 -2
- package/dist/commands/workflow/status.js +4 -4
- package/dist/commands/workflow/stop.d.ts +1 -1
- package/dist/commands/workflow/stop.js +2 -2
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -0
- package/dist/services/claude_client.d.ts +13 -0
- package/dist/services/claude_client.integration.test.d.ts +1 -0
- package/dist/services/claude_client.integration.test.js +43 -0
- package/dist/services/claude_client.js +155 -0
- package/dist/services/claude_client.spec.d.ts +1 -0
- package/dist/services/claude_client.spec.js +141 -0
- package/dist/services/coding_agents.d.ts +43 -0
- package/dist/services/coding_agents.js +230 -0
- package/dist/services/coding_agents.spec.d.ts +1 -0
- package/dist/services/coding_agents.spec.js +254 -0
- package/dist/services/generate_plan_name@v1.prompt +24 -0
- package/dist/services/template_processor.d.ts +1 -1
- package/dist/services/template_processor.js +1 -1
- package/dist/services/workflow_generator.d.ts +1 -1
- package/dist/services/workflow_generator.js +4 -4
- package/dist/services/workflow_planner.d.ts +20 -0
- package/dist/services/workflow_planner.js +83 -0
- package/dist/services/workflow_planner.spec.d.ts +1 -0
- package/dist/services/workflow_planner.spec.js +208 -0
- package/dist/templates/agent_instructions/commands/plan_workflow.md.template +180 -386
- package/dist/test_helpers/mocks.d.ts +37 -0
- package/dist/test_helpers/mocks.js +67 -0
- package/dist/utils/error_handler.js +1 -1
- package/dist/utils/validation.js +1 -1
- package/package.json +13 -3
package/dist/api/http_client.js
CHANGED
|
@@ -4,15 +4,8 @@ export default class Init extends Command {
|
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static args: {};
|
|
6
6
|
static flags: {
|
|
7
|
-
'agent-provider': import("@oclif/core/lib/interfaces
|
|
8
|
-
force: import("@oclif/core/lib/interfaces
|
|
7
|
+
'agent-provider': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
};
|
|
10
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
11
|
}
|
|
@@ -1,62 +1,6 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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
|
-
type: 'template',
|
|
28
|
-
from: 'meta/pre_flight.md.template',
|
|
29
|
-
to: '.outputai/meta/pre_flight.md'
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
type: 'template',
|
|
33
|
-
from: 'meta/post_flight.md.template',
|
|
34
|
-
to: '.outputai/meta/post_flight.md'
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
},
|
|
38
|
-
'claude-code': {
|
|
39
|
-
id: 'claude-code',
|
|
40
|
-
name: 'Claude Code',
|
|
41
|
-
mappings: [
|
|
42
|
-
{
|
|
43
|
-
type: 'symlink',
|
|
44
|
-
from: '.outputai/AGENTS.md',
|
|
45
|
-
to: 'CLAUDE.md'
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: 'symlink',
|
|
49
|
-
from: '.outputai/agents/workflow_planner.md',
|
|
50
|
-
to: '.claude/agents/workflow_planner.md'
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
type: 'symlink',
|
|
54
|
-
from: '.outputai/commands/plan_workflow.md',
|
|
55
|
-
to: '.claude/commands/plan_workflow.md'
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
};
|
|
2
|
+
import { AGENT_CONFIG_DIR } from '#config.js';
|
|
3
|
+
import { initializeAgentConfig, AGENT_CONFIGS } from '#services/coding_agents.js';
|
|
60
4
|
export default class Init extends Command {
|
|
61
5
|
static description = 'Initialize agent configuration files for AI assistant integration';
|
|
62
6
|
static examples = [
|
|
@@ -81,13 +25,15 @@ export default class Init extends Command {
|
|
|
81
25
|
const { flags } = await this.parse(Init);
|
|
82
26
|
this.log('Initializing agent configuration for Claude Code...');
|
|
83
27
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
28
|
+
await initializeAgentConfig({
|
|
29
|
+
projectRoot: process.cwd(),
|
|
30
|
+
force: flags.force,
|
|
31
|
+
agentProvider: flags['agent-provider']
|
|
32
|
+
});
|
|
87
33
|
this.log('✅ Agent configuration initialized successfully!');
|
|
88
34
|
this.log('');
|
|
89
35
|
this.log('Created:');
|
|
90
|
-
this.log(
|
|
36
|
+
this.log(` • ${AGENT_CONFIG_DIR}/ directory with agent and command configurations`);
|
|
91
37
|
this.log(' • .claude/ directory with symlinks for Claude Code integration');
|
|
92
38
|
this.log('');
|
|
93
39
|
this.log('Claude Code will automatically detect and use these configurations.');
|
|
@@ -99,87 +45,4 @@ export default class Init extends Command {
|
|
|
99
45
|
this.error(`Failed to initialize agent configuration: ${error.message}`);
|
|
100
46
|
}
|
|
101
47
|
}
|
|
102
|
-
prepareTemplateVariables() {
|
|
103
|
-
return {
|
|
104
|
-
date: new Date().toLocaleDateString('en-US', {
|
|
105
|
-
year: 'numeric',
|
|
106
|
-
month: 'long',
|
|
107
|
-
day: 'numeric'
|
|
108
|
-
})
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
async processMappings(config, variables, force) {
|
|
112
|
-
for (const mapping of config.mappings) {
|
|
113
|
-
const dir = path.dirname(mapping.to);
|
|
114
|
-
await this.ensureDirectoryExists(dir);
|
|
115
|
-
if (!force && await this.fileExists(mapping.to)) {
|
|
116
|
-
this.warn(`File already exists: ${mapping.to} (use --force to overwrite)`);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
switch (mapping.type) {
|
|
120
|
-
case 'template':
|
|
121
|
-
await this.createFromTemplate(mapping.from, mapping.to, variables);
|
|
122
|
-
break;
|
|
123
|
-
case 'symlink':
|
|
124
|
-
await this.createSymlink(mapping.from, mapping.to);
|
|
125
|
-
break;
|
|
126
|
-
case 'copy':
|
|
127
|
-
await this.copyFile(mapping.from, mapping.to);
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
async ensureDirectoryExists(dir) {
|
|
133
|
-
try {
|
|
134
|
-
await fs.mkdir(dir, { recursive: true });
|
|
135
|
-
this.debug(`Created directory: ${dir}`);
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
if (error.code !== 'EEXIST') {
|
|
139
|
-
throw error;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
async fileExists(filePath) {
|
|
144
|
-
try {
|
|
145
|
-
await fs.stat(filePath);
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
async createFromTemplate(template, output, variables) {
|
|
153
|
-
const templateDir = getTemplateDir('agent_instructions');
|
|
154
|
-
const templatePath = path.join(templateDir, template);
|
|
155
|
-
const content = await fs.readFile(templatePath, 'utf-8');
|
|
156
|
-
const processed = processTemplate(content, variables);
|
|
157
|
-
await fs.writeFile(output, processed, 'utf-8');
|
|
158
|
-
this.debug(`Created from template: ${output}`);
|
|
159
|
-
}
|
|
160
|
-
async createSymlink(source, target) {
|
|
161
|
-
try {
|
|
162
|
-
if (await this.fileExists(target)) {
|
|
163
|
-
await fs.unlink(target);
|
|
164
|
-
}
|
|
165
|
-
const relativePath = path.relative(path.dirname(target), source);
|
|
166
|
-
await fs.symlink(relativePath, target);
|
|
167
|
-
this.debug(`Created symlink: ${target} -> ${source}`);
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
const code = error.code;
|
|
171
|
-
if (code === 'ENOTSUP' || code === 'EPERM') {
|
|
172
|
-
this.debug(`Symlinks not supported, creating copy: ${target}`);
|
|
173
|
-
const content = await fs.readFile(source, 'utf-8');
|
|
174
|
-
await fs.writeFile(target, content, 'utf-8');
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async copyFile(source, target) {
|
|
181
|
-
const content = await fs.readFile(source, 'utf-8');
|
|
182
|
-
await fs.writeFile(target, content, 'utf-8');
|
|
183
|
-
this.debug(`Copied file: ${source} -> ${target}`);
|
|
184
|
-
}
|
|
185
48
|
}
|
|
@@ -3,8 +3,8 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import Init from './init.js';
|
|
6
|
-
import { getTemplateDir } from '
|
|
7
|
-
import { processTemplate } from '
|
|
6
|
+
import { getTemplateDir } from '#utils/paths.js';
|
|
7
|
+
import { processTemplate } from '#utils/template.js';
|
|
8
8
|
vi.mock('node:fs/promises');
|
|
9
9
|
vi.mock('node:path', async () => {
|
|
10
10
|
const actual = await vi.importActual('node:path');
|
|
@@ -46,6 +46,7 @@ describe('agents init', () => {
|
|
|
46
46
|
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
47
47
|
vi.mocked(fs.symlink).mockResolvedValue(undefined);
|
|
48
48
|
vi.mocked(fs.stat).mockRejectedValue({ code: 'ENOENT' });
|
|
49
|
+
vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' }); // Files don't exist by default
|
|
49
50
|
vi.mocked(fs.unlink).mockResolvedValue(undefined);
|
|
50
51
|
vi.mocked(fs.readFile).mockResolvedValue('Template content with {{date}}');
|
|
51
52
|
mockGetTemplateDir = vi.mocked(getTemplateDir);
|
|
@@ -108,7 +109,7 @@ describe('agents init', () => {
|
|
|
108
109
|
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
109
110
|
await cmd.run();
|
|
110
111
|
expect(mockReadFile).toHaveBeenCalledWith(expect.stringContaining('AGENTS.md.template'), 'utf-8');
|
|
111
|
-
expect(mockWriteFile).toHaveBeenCalledWith('.outputai/AGENTS.md', expect.stringContaining('Agent instructions'), 'utf-8');
|
|
112
|
+
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining('.outputai/AGENTS.md'), expect.stringContaining('Agent instructions'), 'utf-8');
|
|
112
113
|
});
|
|
113
114
|
it('should create all agent configuration files', async () => {
|
|
114
115
|
const mockWriteFile = vi.mocked(fs.writeFile);
|
|
@@ -119,7 +120,7 @@ describe('agents init', () => {
|
|
|
119
120
|
'.outputai/agents/workflow_planner.md'
|
|
120
121
|
];
|
|
121
122
|
for (const file of agentFiles) {
|
|
122
|
-
expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
|
|
123
|
+
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
|
|
123
124
|
}
|
|
124
125
|
});
|
|
125
126
|
it('should create all command configuration files', async () => {
|
|
@@ -131,7 +132,7 @@ describe('agents init', () => {
|
|
|
131
132
|
'.outputai/commands/plan_workflow.md'
|
|
132
133
|
];
|
|
133
134
|
for (const file of commandFiles) {
|
|
134
|
-
expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
|
|
135
|
+
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
|
|
135
136
|
}
|
|
136
137
|
});
|
|
137
138
|
it('should create all meta configuration files', async () => {
|
|
@@ -144,50 +145,52 @@ describe('agents init', () => {
|
|
|
144
145
|
'.outputai/meta/post_flight.md'
|
|
145
146
|
];
|
|
146
147
|
for (const file of metaFiles) {
|
|
147
|
-
expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
|
|
148
|
+
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
|
|
148
149
|
}
|
|
149
150
|
});
|
|
150
151
|
});
|
|
151
152
|
describe('symlink creation', () => {
|
|
152
153
|
it('should create symlink from CLAUDE.md to .outputai/AGENTS.md', async () => {
|
|
153
154
|
const mockSymlink = vi.mocked(fs.symlink);
|
|
154
|
-
const mockStat = vi.mocked(fs.stat);
|
|
155
|
-
mockStat.mockImplementation(async (path) => {
|
|
156
|
-
if (typeof path === 'string' && path.startsWith('.outputai')) {
|
|
157
|
-
return { isFile: () => true };
|
|
158
|
-
}
|
|
159
|
-
throw { code: 'ENOENT' };
|
|
160
|
-
});
|
|
161
155
|
const cmd = createTestCommand();
|
|
162
156
|
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
163
157
|
await cmd.run();
|
|
164
|
-
|
|
158
|
+
// Symlink creates a relative link from target to source
|
|
159
|
+
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/AGENTS.md'), expect.stringContaining('CLAUDE.md'));
|
|
165
160
|
});
|
|
166
161
|
it('should create symlinks for all agent files', async () => {
|
|
167
162
|
const mockSymlink = vi.mocked(fs.symlink);
|
|
168
163
|
const cmd = createTestCommand();
|
|
169
164
|
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
170
165
|
await cmd.run();
|
|
171
|
-
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents/workflow_planner.md'), '.claude/agents/workflow_planner.md');
|
|
166
|
+
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents/workflow_planner.md'), expect.stringContaining('.claude/agents/workflow_planner.md'));
|
|
172
167
|
});
|
|
173
168
|
it('should create symlinks for all command files', async () => {
|
|
174
169
|
const mockSymlink = vi.mocked(fs.symlink);
|
|
175
170
|
const cmd = createTestCommand();
|
|
176
171
|
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
177
172
|
await cmd.run();
|
|
178
|
-
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands/plan_workflow.md'), '.claude/commands/plan_workflow.md');
|
|
173
|
+
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands/plan_workflow.md'), expect.stringContaining('.claude/commands/plan_workflow.md'));
|
|
179
174
|
});
|
|
180
175
|
it('should handle Windows by copying files when symlinks are not supported', async () => {
|
|
181
176
|
const mockSymlink = vi.mocked(fs.symlink);
|
|
182
177
|
const mockWriteFile = vi.mocked(fs.writeFile);
|
|
183
178
|
const mockReadFile = vi.mocked(fs.readFile);
|
|
184
179
|
mockSymlink.mockRejectedValue({ code: 'ENOTSUP' });
|
|
180
|
+
// readFile is called for both templates AND fallback copy
|
|
185
181
|
mockReadFile.mockResolvedValue('File content');
|
|
186
182
|
const cmd = createTestCommand();
|
|
187
183
|
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
188
184
|
await cmd.run();
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
// When symlinks fail, it falls back to copying the source files
|
|
186
|
+
// Source files are the .outputai files, targets are CLAUDE.md and .claude/ files
|
|
187
|
+
const writeCalls = mockWriteFile.mock.calls;
|
|
188
|
+
// Should have written template files + fallback copies
|
|
189
|
+
expect(writeCalls.length).toBeGreaterThan(3); // At least 3 templates + 3 symlink fallbacks
|
|
190
|
+
// Verify at least one fallback copy happened
|
|
191
|
+
const claudeFileCalls = writeCalls.filter(call => typeof call[0] === 'string' && (call[0].includes('CLAUDE.md') ||
|
|
192
|
+
call[0].includes('.claude/')));
|
|
193
|
+
expect(claudeFileCalls.length).toBeGreaterThan(0);
|
|
191
194
|
});
|
|
192
195
|
});
|
|
193
196
|
describe('error handling', () => {
|
|
@@ -247,12 +250,12 @@ describe('agents init', () => {
|
|
|
247
250
|
'.outputai/meta/post_flight.md'
|
|
248
251
|
];
|
|
249
252
|
for (const file of expectedFiles) {
|
|
250
|
-
expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
|
|
253
|
+
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
|
|
251
254
|
}
|
|
252
255
|
// Verify symlinks are created
|
|
253
|
-
expect(mockSymlink).toHaveBeenCalledWith('.outputai/AGENTS.md', 'CLAUDE.md');
|
|
254
|
-
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents/workflow_planner.md'), '.claude/agents/workflow_planner.md');
|
|
255
|
-
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands/plan_workflow.md'), '.claude/commands/plan_workflow.md');
|
|
256
|
+
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/AGENTS.md'), expect.stringContaining('CLAUDE.md'));
|
|
257
|
+
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents/workflow_planner.md'), expect.stringContaining('.claude/agents/workflow_planner.md'));
|
|
258
|
+
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands/plan_workflow.md'), expect.stringContaining('.claude/commands/plan_workflow.md'));
|
|
256
259
|
});
|
|
257
260
|
});
|
|
258
261
|
describe('force flag', () => {
|
|
@@ -267,13 +270,15 @@ describe('agents init', () => {
|
|
|
267
270
|
expect(mockWriteFile).toHaveBeenCalled();
|
|
268
271
|
expect(cmd.warn).not.toHaveBeenCalled();
|
|
269
272
|
});
|
|
270
|
-
it('should
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
+
it('should skip existing files without force flag', async () => {
|
|
274
|
+
const mockAccess = vi.mocked(fs.access);
|
|
275
|
+
mockAccess.mockResolvedValue(undefined);
|
|
273
276
|
const cmd = createTestCommand();
|
|
274
277
|
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
275
278
|
await cmd.run();
|
|
276
|
-
|
|
279
|
+
// Files exist, so they should be skipped
|
|
280
|
+
// The command should complete without errors
|
|
281
|
+
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('initialized successfully'));
|
|
277
282
|
});
|
|
278
283
|
});
|
|
279
284
|
});
|
|
@@ -3,13 +3,13 @@ export default class Generate extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
skeleton: import("@oclif/core/lib/interfaces
|
|
7
|
-
description: import("@oclif/core/lib/interfaces
|
|
8
|
-
'output-dir': import("@oclif/core/lib/interfaces
|
|
9
|
-
force: import("@oclif/core/lib/interfaces
|
|
6
|
+
skeleton: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
description: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
'output-dir': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
|
+
force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
};
|
|
11
11
|
static args: {
|
|
12
|
-
name: import("@oclif/core/lib/interfaces
|
|
12
|
+
name: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
13
13
|
};
|
|
14
14
|
run(): Promise<void>;
|
|
15
15
|
private displaySuccess;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
-
import { generateWorkflow } from '
|
|
3
|
-
import { DEFAULT_OUTPUT_DIRS } from '
|
|
2
|
+
import { generateWorkflow } from '#services/workflow_generator.js';
|
|
3
|
+
import { DEFAULT_OUTPUT_DIRS } from '#utils/paths.js';
|
|
4
4
|
export default class Generate extends Command {
|
|
5
5
|
static description = 'Generate a new Output SDK workflow';
|
|
6
6
|
static examples = [
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* eslint-disable no-restricted-syntax, @typescript-eslint/no-explicit-any, init-declarations */
|
|
2
2
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
3
3
|
import Generate from './generate.js';
|
|
4
|
-
import { generateWorkflow } from '
|
|
5
|
-
import { InvalidNameError, WorkflowExistsError } from '
|
|
4
|
+
import { generateWorkflow } from '#services/workflow_generator.js';
|
|
5
|
+
import { InvalidNameError, WorkflowExistsError } from '#types/errors.js';
|
|
6
6
|
vi.mock('../../services/workflow_generator.js');
|
|
7
7
|
describe('Generate Command', () => {
|
|
8
8
|
let mockGenerateWorkflow;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { type Workflow } from '
|
|
2
|
+
import { type Workflow } from '#api/generated/api.js';
|
|
3
3
|
interface WorkflowDisplay {
|
|
4
4
|
name: string;
|
|
5
5
|
description: string;
|
|
@@ -11,9 +11,9 @@ export default class WorkflowList extends Command {
|
|
|
11
11
|
static description: string;
|
|
12
12
|
static examples: string[];
|
|
13
13
|
static flags: {
|
|
14
|
-
format: import("@oclif/core/lib/interfaces
|
|
15
|
-
detailed: import("@oclif/core/lib/interfaces
|
|
16
|
-
filter: import("@oclif/core/lib/interfaces
|
|
14
|
+
format: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
15
|
+
detailed: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
filter: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
17
17
|
};
|
|
18
18
|
run(): Promise<void>;
|
|
19
19
|
catch(error: Error): Promise<void>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import Table from 'cli-table3';
|
|
3
|
-
import { getWorkflowCatalog } from '
|
|
4
|
-
import { parseWorkflowDefinition, formatParameters } from '
|
|
5
|
-
import { handleApiError } from '
|
|
3
|
+
import { getWorkflowCatalog } from '#api/generated/api.js';
|
|
4
|
+
import { parseWorkflowDefinition, formatParameters } from '#api/parser.js';
|
|
5
|
+
import { handleApiError } from '#utils/error_handler.js';
|
|
6
6
|
const OUTPUT_FORMAT = {
|
|
7
7
|
LIST: 'list',
|
|
8
8
|
TABLE: 'table',
|
|
@@ -3,10 +3,10 @@ export default class WorkflowOutput extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static args: {
|
|
6
|
-
workflowId: import("@oclif/core/lib/interfaces
|
|
6
|
+
workflowId: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
7
|
};
|
|
8
8
|
static flags: {
|
|
9
|
-
format: import("@oclif/core/lib/interfaces
|
|
9
|
+
format: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
10
10
|
};
|
|
11
11
|
run(): Promise<void>;
|
|
12
12
|
catch(error: Error): Promise<void>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
-
import { getWorkflowIdOutput } from '
|
|
3
|
-
import { OUTPUT_FORMAT } from '
|
|
4
|
-
import { formatOutput } from '
|
|
5
|
-
import { handleApiError } from '
|
|
2
|
+
import { getWorkflowIdOutput } from '#api/generated/api.js';
|
|
3
|
+
import { OUTPUT_FORMAT } from '#utils/constants.js';
|
|
4
|
+
import { formatOutput } from '#utils/output_formatter.js';
|
|
5
|
+
import { handleApiError } from '#utils/error_handler.js';
|
|
6
6
|
export default class WorkflowOutput extends Command {
|
|
7
7
|
static description = 'Get workflow execution output';
|
|
8
8
|
static examples = [
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class WorkflowPlan extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'force-agent-file-write': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
description: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
private planModificationLoop;
|
|
11
|
+
private planGenerationLoop;
|
|
12
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command, Flags, ux } from '@oclif/core';
|
|
2
|
+
import { input } from '@inquirer/prompts';
|
|
3
|
+
import { ensureOutputAIStructure, generatePlanName, updateAgentTemplates, writePlanFile } from '#services/workflow_planner.js';
|
|
4
|
+
import { invokePlanWorkflow, replyToClaude } from '#services/claude_client.js';
|
|
5
|
+
export default class WorkflowPlan extends Command {
|
|
6
|
+
static description = 'Generate a workflow plan from a description';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> <%= command.id %>',
|
|
9
|
+
'<%= config.bin %> <%= command.id %> --description "A workflow to take a question and answer it"',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --force-agent-file-write'
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
'force-agent-file-write': Flags.boolean({
|
|
14
|
+
description: 'Force overwrite of agent template files',
|
|
15
|
+
default: false
|
|
16
|
+
}),
|
|
17
|
+
description: Flags.string({
|
|
18
|
+
char: 'd',
|
|
19
|
+
description: 'Workflow description',
|
|
20
|
+
required: false
|
|
21
|
+
})
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { flags } = await this.parse(WorkflowPlan);
|
|
25
|
+
const projectRoot = process.cwd();
|
|
26
|
+
this.log('Checking .outputai directory structure...');
|
|
27
|
+
await ensureOutputAIStructure(projectRoot);
|
|
28
|
+
if (flags['force-agent-file-write']) {
|
|
29
|
+
this.log('Updating agent templates...');
|
|
30
|
+
await updateAgentTemplates(projectRoot);
|
|
31
|
+
this.log('Templates updated successfully\n');
|
|
32
|
+
}
|
|
33
|
+
const description = flags.description ?? await input({
|
|
34
|
+
message: 'Describe the workflow you want to create:',
|
|
35
|
+
validate: (value) => value.length >= 10
|
|
36
|
+
});
|
|
37
|
+
this.log('\nGenerating plan name...');
|
|
38
|
+
const planName = await generatePlanName(description);
|
|
39
|
+
this.log(`Plan name: ${planName}`);
|
|
40
|
+
await this.planGenerationLoop(description, planName, projectRoot);
|
|
41
|
+
}
|
|
42
|
+
async planModificationLoop(originalPlanContent) {
|
|
43
|
+
const acceptKey = 'ACCEPT';
|
|
44
|
+
this.log('=========');
|
|
45
|
+
this.log(originalPlanContent);
|
|
46
|
+
this.log('=========');
|
|
47
|
+
const modifications = await input({
|
|
48
|
+
message: ux.colorize('gray', `Reply or type ${acceptKey} to accept the plan as is: `),
|
|
49
|
+
validate: (value) => value.length >= 10 || value === acceptKey
|
|
50
|
+
});
|
|
51
|
+
if (modifications === acceptKey) {
|
|
52
|
+
return originalPlanContent;
|
|
53
|
+
}
|
|
54
|
+
const modifiedPlanContent = await replyToClaude(modifications);
|
|
55
|
+
return this.planModificationLoop(modifiedPlanContent);
|
|
56
|
+
}
|
|
57
|
+
async planGenerationLoop(promptDescription, planName, projectRoot) {
|
|
58
|
+
this.log('\nInvoking the /plan_workflow command...');
|
|
59
|
+
this.log('This may take a moment...\n');
|
|
60
|
+
const planContent = await invokePlanWorkflow(promptDescription);
|
|
61
|
+
const modifiedPlanContent = await this.planModificationLoop(planContent);
|
|
62
|
+
const modifiedSavedPath = await writePlanFile(planName, modifiedPlanContent, projectRoot);
|
|
63
|
+
this.log(`✅ Plan saved to: ${modifiedSavedPath}\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|