@output.ai/cli 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents/init.d.ts +0 -1
- package/dist/commands/agents/init.js +9 -16
- package/dist/commands/agents/init.spec.js +49 -224
- package/dist/commands/workflow/generate.js +2 -2
- package/dist/commands/workflow/plan.js +3 -3
- package/dist/commands/workflow/plan.spec.js +13 -13
- package/dist/services/claude_client.d.ts +4 -4
- package/dist/services/claude_client.integration.test.js +2 -2
- package/dist/services/claude_client.js +7 -7
- package/dist/services/claude_client.spec.js +3 -3
- package/dist/services/coding_agents.d.ts +10 -24
- package/dist/services/coding_agents.js +112 -368
- package/dist/services/coding_agents.spec.js +101 -290
- package/dist/services/project_scaffold.js +3 -3
- package/dist/services/workflow_builder.d.ts +1 -1
- package/dist/services/workflow_builder.js +1 -1
- package/dist/services/workflow_planner.js +1 -2
- package/dist/services/workflow_planner.spec.js +4 -5
- package/dist/templates/agent_instructions/dotclaude/settings.json.template +29 -0
- package/dist/templates/agent_instructions/{AGENTS.md.template → dotoutputai/AGENTS.md.template} +12 -10
- package/dist/utils/claude.d.ts +5 -0
- package/dist/utils/claude.js +19 -0
- package/dist/utils/claude.spec.d.ts +1 -0
- package/dist/utils/claude.spec.js +119 -0
- package/dist/utils/paths.d.ts +0 -4
- package/dist/utils/paths.js +0 -6
- package/package.json +3 -3
- package/dist/templates/agent_instructions/agents/workflow_context_fetcher.md.template +0 -82
- package/dist/templates/agent_instructions/agents/workflow_debugger.md.template +0 -98
- package/dist/templates/agent_instructions/agents/workflow_planner.md.template +0 -113
- package/dist/templates/agent_instructions/agents/workflow_prompt_writer.md.template +0 -595
- package/dist/templates/agent_instructions/agents/workflow_quality.md.template +0 -244
- package/dist/templates/agent_instructions/commands/build_workflow.md.template +0 -290
- package/dist/templates/agent_instructions/commands/debug_workflow.md.template +0 -198
- package/dist/templates/agent_instructions/commands/plan_workflow.md.template +0 -261
- package/dist/templates/agent_instructions/meta/post_flight.md.template +0 -94
- package/dist/templates/agent_instructions/meta/pre_flight.md.template +0 -60
- package/dist/templates/agent_instructions/skills/output-error-direct-io/SKILL.md.template +0 -249
- package/dist/templates/agent_instructions/skills/output-error-http-client/SKILL.md.template +0 -298
- package/dist/templates/agent_instructions/skills/output-error-missing-schemas/SKILL.md.template +0 -265
- package/dist/templates/agent_instructions/skills/output-error-nondeterminism/SKILL.md.template +0 -252
- package/dist/templates/agent_instructions/skills/output-error-try-catch/SKILL.md.template +0 -226
- package/dist/templates/agent_instructions/skills/output-error-zod-import/SKILL.md.template +0 -209
- package/dist/templates/agent_instructions/skills/output-services-check/SKILL.md.template +0 -128
- package/dist/templates/agent_instructions/skills/output-workflow-list/SKILL.md.template +0 -117
- package/dist/templates/agent_instructions/skills/output-workflow-result/SKILL.md.template +0 -199
- package/dist/templates/agent_instructions/skills/output-workflow-run/SKILL.md.template +0 -228
- package/dist/templates/agent_instructions/skills/output-workflow-runs-list/SKILL.md.template +0 -141
- package/dist/templates/agent_instructions/skills/output-workflow-start/SKILL.md.template +0 -201
- package/dist/templates/agent_instructions/skills/output-workflow-status/SKILL.md.template +0 -151
- package/dist/templates/agent_instructions/skills/output-workflow-stop/SKILL.md.template +0 -164
- package/dist/templates/agent_instructions/skills/output-workflow-trace/SKILL.md.template +0 -134
|
@@ -4,7 +4,6 @@ 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").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
7
|
force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
9
8
|
};
|
|
10
9
|
run(): Promise<void>;
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
|
-
import {
|
|
3
|
-
import { initializeAgentConfig, AGENT_CONFIGS } from '#services/coding_agents.js';
|
|
2
|
+
import { initializeAgentConfig } from '#services/coding_agents.js';
|
|
4
3
|
import { getErrorMessage, getErrorCode } from '#utils/error_utils.js';
|
|
5
4
|
export default class Init extends Command {
|
|
6
|
-
static description = 'Initialize agent configuration files for
|
|
5
|
+
static description = 'Initialize agent configuration files for Claude Code plugin integration';
|
|
7
6
|
static examples = [
|
|
8
7
|
'<%= config.bin %> <%= command.id %>',
|
|
9
|
-
'<%= config.bin %> <%= command.id %> --agent-provider claude-code',
|
|
10
8
|
'<%= config.bin %> <%= command.id %> --force'
|
|
11
9
|
];
|
|
12
10
|
static args = {};
|
|
13
11
|
static flags = {
|
|
14
|
-
'agent-provider': Flags.string({
|
|
15
|
-
description: 'Specify the coding agent provider',
|
|
16
|
-
default: AGENT_CONFIGS['claude-code'].id,
|
|
17
|
-
options: [AGENT_CONFIGS['claude-code'].id]
|
|
18
|
-
}),
|
|
19
12
|
force: Flags.boolean({
|
|
20
13
|
char: 'f',
|
|
21
14
|
description: 'Overwrite existing files',
|
|
@@ -28,16 +21,16 @@ export default class Init extends Command {
|
|
|
28
21
|
try {
|
|
29
22
|
await initializeAgentConfig({
|
|
30
23
|
projectRoot: process.cwd(),
|
|
31
|
-
force: flags.force
|
|
32
|
-
agentProvider: flags['agent-provider']
|
|
24
|
+
force: flags.force
|
|
33
25
|
});
|
|
34
|
-
this.log('
|
|
26
|
+
this.log('Agent configuration initialized successfully!');
|
|
35
27
|
this.log('');
|
|
36
|
-
this.log('
|
|
37
|
-
this.log(
|
|
38
|
-
this.log('
|
|
28
|
+
this.log('Configured:');
|
|
29
|
+
this.log(' - Registered marketplace: growthxai/output-claude-plugins');
|
|
30
|
+
this.log(' - Updated marketplace: outputai');
|
|
31
|
+
this.log(' - Installed plugin: outputai@outputai');
|
|
39
32
|
this.log('');
|
|
40
|
-
this.log('Claude Code will automatically
|
|
33
|
+
this.log('Claude Code will automatically use the OutputAI plugin.');
|
|
41
34
|
}
|
|
42
35
|
catch (error) {
|
|
43
36
|
if (getErrorCode(error) === 'EACCES') {
|
|
@@ -1,36 +1,15 @@
|
|
|
1
|
-
/* eslint-disable
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
3
|
import Init from './init.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
vi.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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');
|
|
4
|
+
import { initializeAgentConfig } from '#services/coding_agents.js';
|
|
5
|
+
vi.mock('#services/coding_agents.js', () => ({
|
|
6
|
+
initializeAgentConfig: vi.fn(),
|
|
7
|
+
AGENT_CONFIGS: { 'claude-code': { id: 'claude-code' } }
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('#config.js', () => ({
|
|
10
|
+
AGENT_CONFIG_DIR: '.outputai'
|
|
11
|
+
}));
|
|
31
12
|
describe('agents init', () => {
|
|
32
|
-
let mockGetTemplateDir;
|
|
33
|
-
let mockProcessTemplate;
|
|
34
13
|
const createTestCommand = (args = []) => {
|
|
35
14
|
const cmd = new Init(args, {});
|
|
36
15
|
cmd.log = vi.fn();
|
|
@@ -42,19 +21,7 @@ describe('agents init', () => {
|
|
|
42
21
|
};
|
|
43
22
|
beforeEach(() => {
|
|
44
23
|
vi.clearAllMocks();
|
|
45
|
-
vi.mocked(
|
|
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.access).mockRejectedValue({ code: 'ENOENT' }); // Files don't exist by default
|
|
50
|
-
vi.mocked(fs.unlink).mockResolvedValue(undefined);
|
|
51
|
-
vi.mocked(fs.readFile).mockResolvedValue('Template content with {{date}}');
|
|
52
|
-
mockGetTemplateDir = vi.mocked(getTemplateDir);
|
|
53
|
-
mockGetTemplateDir.mockReturnValue('/templates/agent_instructions');
|
|
54
|
-
mockProcessTemplate = vi.mocked(processTemplate);
|
|
55
|
-
mockProcessTemplate.mockImplementation((content, variables) => {
|
|
56
|
-
return content.replace(/\{\{(\w+)\}\}/g, (_match, key) => variables[key] || _match);
|
|
57
|
-
});
|
|
24
|
+
vi.mocked(initializeAgentConfig).mockResolvedValue(undefined);
|
|
58
25
|
});
|
|
59
26
|
afterEach(() => {
|
|
60
27
|
vi.restoreAllMocks();
|
|
@@ -64,221 +31,79 @@ describe('agents init', () => {
|
|
|
64
31
|
expect(Init.description).toBeDefined();
|
|
65
32
|
expect(Init.description).toContain('agent configuration');
|
|
66
33
|
});
|
|
67
|
-
it('should have correct examples', () => {
|
|
34
|
+
it('should have correct examples without agent-provider flag', () => {
|
|
68
35
|
expect(Init.examples).toBeDefined();
|
|
69
36
|
expect(Array.isArray(Init.examples)).toBe(true);
|
|
37
|
+
const examplesStr = Init.examples.join(' ');
|
|
38
|
+
expect(examplesStr).not.toContain('agent-provider');
|
|
70
39
|
});
|
|
71
40
|
it('should have no required arguments', () => {
|
|
72
41
|
expect(Init.args).toBeDefined();
|
|
73
42
|
expect(Object.keys(Init.args)).toHaveLength(0);
|
|
74
43
|
});
|
|
75
|
-
it('should have
|
|
44
|
+
it('should only have force flag', () => {
|
|
76
45
|
expect(Init.flags).toBeDefined();
|
|
77
46
|
expect(Init.flags.force).toBeDefined();
|
|
47
|
+
expect(Object.keys(Init.flags)).toHaveLength(1);
|
|
78
48
|
});
|
|
79
49
|
});
|
|
80
|
-
describe('
|
|
81
|
-
it('should
|
|
82
|
-
const mockMkdir = vi.mocked(fs.mkdir);
|
|
83
|
-
mockMkdir.mockResolvedValue(undefined);
|
|
84
|
-
const cmd = createTestCommand();
|
|
85
|
-
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
86
|
-
await cmd.run();
|
|
87
|
-
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai'), expect.objectContaining({ recursive: true }));
|
|
88
|
-
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents'), expect.objectContaining({ recursive: true }));
|
|
89
|
-
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands'), expect.objectContaining({ recursive: true }));
|
|
90
|
-
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai/meta'), expect.objectContaining({ recursive: true }));
|
|
91
|
-
});
|
|
92
|
-
it('should create .claude directory structure', async () => {
|
|
93
|
-
const mockMkdir = vi.mocked(fs.mkdir);
|
|
94
|
-
mockMkdir.mockResolvedValue(undefined);
|
|
95
|
-
const cmd = createTestCommand();
|
|
96
|
-
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
97
|
-
await cmd.run();
|
|
98
|
-
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.claude'), expect.objectContaining({ recursive: true }));
|
|
99
|
-
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.claude/agents'), expect.objectContaining({ recursive: true }));
|
|
100
|
-
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.claude/commands'), expect.objectContaining({ recursive: true }));
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
describe('file creation from templates', () => {
|
|
104
|
-
it('should create AGENTS.md file from template', async () => {
|
|
105
|
-
const mockWriteFile = vi.mocked(fs.writeFile);
|
|
106
|
-
const mockReadFile = vi.mocked(fs.readFile);
|
|
107
|
-
mockReadFile.mockResolvedValue('Agent instructions {{date}}');
|
|
50
|
+
describe('successful execution', () => {
|
|
51
|
+
it('should call initializeAgentConfig with correct options', async () => {
|
|
108
52
|
const cmd = createTestCommand();
|
|
109
|
-
cmd.parse.mockResolvedValue({ flags: {
|
|
53
|
+
cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
|
|
110
54
|
await cmd.run();
|
|
111
|
-
expect(
|
|
112
|
-
|
|
55
|
+
expect(initializeAgentConfig).toHaveBeenCalledWith({
|
|
56
|
+
projectRoot: expect.any(String),
|
|
57
|
+
force: false
|
|
58
|
+
});
|
|
113
59
|
});
|
|
114
|
-
it('should
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
118
|
-
await cmd.run();
|
|
119
|
-
const agentFiles = [
|
|
120
|
-
'.outputai/agents/workflow_planner.md'
|
|
121
|
-
];
|
|
122
|
-
for (const file of agentFiles) {
|
|
123
|
-
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
it('should create all command configuration files', async () => {
|
|
127
|
-
const mockWriteFile = vi.mocked(fs.writeFile);
|
|
128
|
-
const cmd = createTestCommand();
|
|
129
|
-
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
130
|
-
await cmd.run();
|
|
131
|
-
const commandFiles = [
|
|
132
|
-
'.outputai/commands/plan_workflow.md'
|
|
133
|
-
];
|
|
134
|
-
for (const file of commandFiles) {
|
|
135
|
-
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
it('should create all meta configuration files', async () => {
|
|
139
|
-
const mockWriteFile = vi.mocked(fs.writeFile);
|
|
140
|
-
const cmd = createTestCommand();
|
|
141
|
-
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
142
|
-
await cmd.run();
|
|
143
|
-
const metaFiles = [
|
|
144
|
-
'.outputai/meta/pre_flight.md',
|
|
145
|
-
'.outputai/meta/post_flight.md'
|
|
146
|
-
];
|
|
147
|
-
for (const file of metaFiles) {
|
|
148
|
-
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
describe('symlink creation', () => {
|
|
153
|
-
it('should create symlink from CLAUDE.md to .outputai/AGENTS.md', async () => {
|
|
154
|
-
const mockSymlink = vi.mocked(fs.symlink);
|
|
155
|
-
const cmd = createTestCommand();
|
|
156
|
-
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
157
|
-
await cmd.run();
|
|
158
|
-
// Symlink creates a relative link from target to source
|
|
159
|
-
expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/AGENTS.md'), expect.stringContaining('CLAUDE.md'));
|
|
160
|
-
});
|
|
161
|
-
it('should create symlinks for all agent files', async () => {
|
|
162
|
-
const mockSymlink = vi.mocked(fs.symlink);
|
|
163
|
-
const cmd = createTestCommand();
|
|
164
|
-
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
60
|
+
it('should pass force flag to initializeAgentConfig', async () => {
|
|
61
|
+
const cmd = createTestCommand(['--force']);
|
|
62
|
+
cmd.parse.mockResolvedValue({ flags: { force: true }, args: {} });
|
|
165
63
|
await cmd.run();
|
|
166
|
-
expect(
|
|
64
|
+
expect(initializeAgentConfig).toHaveBeenCalledWith({
|
|
65
|
+
projectRoot: expect.any(String),
|
|
66
|
+
force: true
|
|
67
|
+
});
|
|
167
68
|
});
|
|
168
|
-
it('should
|
|
169
|
-
const mockSymlink = vi.mocked(fs.symlink);
|
|
69
|
+
it('should display success messages', async () => {
|
|
170
70
|
const cmd = createTestCommand();
|
|
171
|
-
cmd.parse.mockResolvedValue({ flags: {
|
|
71
|
+
cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
|
|
172
72
|
await cmd.run();
|
|
173
|
-
expect(
|
|
73
|
+
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('initialized successfully'));
|
|
74
|
+
expect(cmd.log).toHaveBeenCalledWith('Configured:');
|
|
75
|
+
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('OutputAI plugin'));
|
|
174
76
|
});
|
|
175
|
-
it('should
|
|
176
|
-
const mockSymlink = vi.mocked(fs.symlink);
|
|
177
|
-
const mockWriteFile = vi.mocked(fs.writeFile);
|
|
178
|
-
const mockReadFile = vi.mocked(fs.readFile);
|
|
179
|
-
mockSymlink.mockRejectedValue({ code: 'ENOTSUP' });
|
|
180
|
-
// readFile is called for both templates AND fallback copy
|
|
181
|
-
mockReadFile.mockResolvedValue('File content');
|
|
77
|
+
it('should display plugin configuration messages', async () => {
|
|
182
78
|
const cmd = createTestCommand();
|
|
183
|
-
cmd.parse.mockResolvedValue({ flags: {
|
|
79
|
+
cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
|
|
184
80
|
await cmd.run();
|
|
185
|
-
|
|
186
|
-
|
|
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);
|
|
81
|
+
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('marketplace'));
|
|
82
|
+
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('plugin'));
|
|
194
83
|
});
|
|
195
84
|
});
|
|
196
85
|
describe('error handling', () => {
|
|
197
|
-
it('should handle existing directory gracefully', async () => {
|
|
198
|
-
const mockMkdir = vi.mocked(fs.mkdir);
|
|
199
|
-
mockMkdir.mockRejectedValue({ code: 'EEXIST' });
|
|
200
|
-
const cmd = createTestCommand();
|
|
201
|
-
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
202
|
-
await cmd.run();
|
|
203
|
-
expect(cmd.error).not.toHaveBeenCalled();
|
|
204
|
-
});
|
|
205
86
|
it('should handle permission errors', async () => {
|
|
206
|
-
|
|
207
|
-
mockMkdir.mockRejectedValue({ code: 'EACCES' });
|
|
87
|
+
vi.mocked(initializeAgentConfig).mockRejectedValue({ code: 'EACCES' });
|
|
208
88
|
const cmd = createTestCommand();
|
|
209
|
-
cmd.parse.mockResolvedValue({ flags: {
|
|
89
|
+
cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
|
|
210
90
|
await cmd.run();
|
|
211
91
|
expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Permission denied'));
|
|
212
92
|
});
|
|
213
|
-
it('should
|
|
214
|
-
|
|
215
|
-
mockWriteFile.mockResolvedValue(undefined);
|
|
93
|
+
it('should handle general errors with message', async () => {
|
|
94
|
+
vi.mocked(initializeAgentConfig).mockRejectedValue(new Error('Something went wrong'));
|
|
216
95
|
const cmd = createTestCommand();
|
|
217
|
-
cmd.parse.mockResolvedValue({ flags: {
|
|
218
|
-
await cmd.run();
|
|
96
|
+
cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
|
|
219
97
|
await cmd.run();
|
|
220
|
-
expect(cmd.error).
|
|
98
|
+
expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Failed to initialize agent configuration'));
|
|
99
|
+
expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Something went wrong'));
|
|
221
100
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
it('should create all required files and directories', async () => {
|
|
225
|
-
const mockMkdir = vi.mocked(fs.mkdir);
|
|
226
|
-
const mockWriteFile = vi.mocked(fs.writeFile);
|
|
227
|
-
const mockSymlink = vi.mocked(fs.symlink);
|
|
101
|
+
it('should handle Claude CLI not found error', async () => {
|
|
102
|
+
vi.mocked(initializeAgentConfig).mockRejectedValue(new Error('Claude CLI not found. Please install Claude Code CLI and ensure \'claude\' is in your PATH.'));
|
|
228
103
|
const cmd = createTestCommand();
|
|
229
|
-
cmd.parse.mockResolvedValue({ flags: {
|
|
230
|
-
await cmd.run();
|
|
231
|
-
// Verify all expected directories are created
|
|
232
|
-
const expectedDirs = [
|
|
233
|
-
'.outputai',
|
|
234
|
-
'.outputai/agents',
|
|
235
|
-
'.outputai/commands',
|
|
236
|
-
'.outputai/meta',
|
|
237
|
-
'.claude',
|
|
238
|
-
'.claude/agents',
|
|
239
|
-
'.claude/commands'
|
|
240
|
-
];
|
|
241
|
-
for (const dir of expectedDirs) {
|
|
242
|
-
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining(dir), expect.objectContaining({ recursive: true }));
|
|
243
|
-
}
|
|
244
|
-
// Verify all expected template files are created
|
|
245
|
-
const expectedFiles = [
|
|
246
|
-
'.outputai/AGENTS.md',
|
|
247
|
-
'.outputai/agents/workflow_planner.md',
|
|
248
|
-
'.outputai/commands/plan_workflow.md',
|
|
249
|
-
'.outputai/meta/pre_flight.md',
|
|
250
|
-
'.outputai/meta/post_flight.md'
|
|
251
|
-
];
|
|
252
|
-
for (const file of expectedFiles) {
|
|
253
|
-
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
|
|
254
|
-
}
|
|
255
|
-
// Verify symlinks are created
|
|
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'));
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
describe('force flag', () => {
|
|
262
|
-
it('should overwrite existing files when force flag is set', async () => {
|
|
263
|
-
const mockWriteFile = vi.mocked(fs.writeFile);
|
|
264
|
-
const mockStat = vi.mocked(fs.stat);
|
|
265
|
-
mockStat.mockResolvedValue({ isFile: () => true });
|
|
266
|
-
mockWriteFile.mockResolvedValue(undefined);
|
|
267
|
-
const cmd = createTestCommand(['--force']);
|
|
268
|
-
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: true }, args: {} });
|
|
104
|
+
cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
|
|
269
105
|
await cmd.run();
|
|
270
|
-
expect(
|
|
271
|
-
expect(cmd.warn).not.toHaveBeenCalled();
|
|
272
|
-
});
|
|
273
|
-
it('should skip existing files without force flag', async () => {
|
|
274
|
-
const mockAccess = vi.mocked(fs.access);
|
|
275
|
-
mockAccess.mockResolvedValue(undefined);
|
|
276
|
-
const cmd = createTestCommand();
|
|
277
|
-
cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
|
|
278
|
-
await cmd.run();
|
|
279
|
-
// Files exist, so they should be skipped
|
|
280
|
-
// The command should complete without errors
|
|
281
|
-
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('initialized successfully'));
|
|
106
|
+
expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Claude CLI not found'));
|
|
282
107
|
});
|
|
283
108
|
});
|
|
284
109
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Args, Command, Flags, ux } from '@oclif/core';
|
|
2
2
|
import { generateWorkflow } from '#services/workflow_generator.js';
|
|
3
3
|
import { buildWorkflow, buildWorkflowInteractiveLoop } from '#services/workflow_builder.js';
|
|
4
|
-
import {
|
|
4
|
+
import { ensureOutputAISystem } from '#services/coding_agents.js';
|
|
5
5
|
import { getWorkflowGenerateSuccessMessage } from '#services/messages.js';
|
|
6
6
|
import { DEFAULT_OUTPUT_DIRS } from '#utils/paths.js';
|
|
7
7
|
import path from 'node:path';
|
|
@@ -63,7 +63,7 @@ export default class Generate extends Command {
|
|
|
63
63
|
if (planFile) {
|
|
64
64
|
this.log('\nStarting AI-assisted workflow implementation...\n');
|
|
65
65
|
const projectRoot = process.cwd();
|
|
66
|
-
await
|
|
66
|
+
await ensureOutputAISystem(projectRoot);
|
|
67
67
|
const absolutePlanPath = path.resolve(projectRoot, planFile);
|
|
68
68
|
const buildOutput = await buildWorkflow(absolutePlanPath, result.targetDir, args.name);
|
|
69
69
|
await buildWorkflowInteractiveLoop(buildOutput);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command, Flags, ux } from '@oclif/core';
|
|
2
2
|
import { input } from '@inquirer/prompts';
|
|
3
3
|
import { generatePlanName, updateAgentTemplates, writePlanFile } from '#services/workflow_planner.js';
|
|
4
|
-
import {
|
|
4
|
+
import { ensureOutputAISystem } from '#services/coding_agents.js';
|
|
5
5
|
import { invokePlanWorkflow, PLAN_COMMAND_OPTIONS, replyToClaude } from '#services/claude_client.js';
|
|
6
6
|
export default class WorkflowPlan extends Command {
|
|
7
7
|
static description = 'Generate a workflow plan from a description';
|
|
@@ -25,7 +25,7 @@ export default class WorkflowPlan extends Command {
|
|
|
25
25
|
const { flags } = await this.parse(WorkflowPlan);
|
|
26
26
|
const projectRoot = process.cwd();
|
|
27
27
|
this.log('Checking .outputai directory structure...');
|
|
28
|
-
await
|
|
28
|
+
await ensureOutputAISystem(projectRoot);
|
|
29
29
|
if (flags['force-agent-file-write']) {
|
|
30
30
|
this.log('Updating agent templates...');
|
|
31
31
|
await updateAgentTemplates(projectRoot);
|
|
@@ -56,7 +56,7 @@ export default class WorkflowPlan extends Command {
|
|
|
56
56
|
return this.planModificationLoop(modifiedPlanContent);
|
|
57
57
|
}
|
|
58
58
|
async planGenerationLoop(promptDescription, planName, projectRoot) {
|
|
59
|
-
this.log('\nInvoking the /plan_workflow command...');
|
|
59
|
+
this.log('\nInvoking the /outputai:plan_workflow command...');
|
|
60
60
|
this.log('This may take a moment...\n');
|
|
61
61
|
const planContent = await invokePlanWorkflow(promptDescription);
|
|
62
62
|
const modifiedPlanContent = await this.planModificationLoop(planContent);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
2
|
import WorkflowPlan from './plan.js';
|
|
3
3
|
import { generatePlanName, writePlanFile, updateAgentTemplates } from '#services/workflow_planner.js';
|
|
4
|
-
import {
|
|
4
|
+
import { ensureOutputAISystem } from '#services/coding_agents.js';
|
|
5
5
|
import { invokePlanWorkflow, replyToClaude, ClaudeInvocationError } from '#services/claude_client.js';
|
|
6
6
|
import { input } from '@inquirer/prompts';
|
|
7
7
|
vi.mock('#services/workflow_planner.js');
|
|
@@ -28,7 +28,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
28
28
|
}
|
|
29
29
|
return 'ACCEPT'; // Always accept on second call to stop recursion
|
|
30
30
|
}));
|
|
31
|
-
vi.mocked(
|
|
31
|
+
vi.mocked(ensureOutputAISystem).mockResolvedValue();
|
|
32
32
|
vi.mocked(generatePlanName).mockResolvedValue(planName);
|
|
33
33
|
vi.mocked(invokePlanWorkflow).mockResolvedValue(planContent);
|
|
34
34
|
vi.mocked(replyToClaude).mockResolvedValue(planContent);
|
|
@@ -69,7 +69,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
69
69
|
});
|
|
70
70
|
setupSuccessfulMocks('Build a user authentication system', '2025_10_06_user_authentication', '# Workflow Plan\n\nPlan content here');
|
|
71
71
|
await command.run();
|
|
72
|
-
expect(
|
|
72
|
+
expect(ensureOutputAISystem).toHaveBeenCalled();
|
|
73
73
|
expect(generatePlanName).toHaveBeenCalledWith('Build a user authentication system');
|
|
74
74
|
expect(invokePlanWorkflow).toHaveBeenCalledWith('Build a user authentication system');
|
|
75
75
|
expect(writePlanFile).toHaveBeenCalledWith('2025_10_06_user_authentication', '# Workflow Plan\n\nPlan content here', expect.any(String));
|
|
@@ -106,7 +106,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
106
106
|
return 'ACCEPT'; // Second call from modification loop
|
|
107
107
|
});
|
|
108
108
|
vi.mocked(input).mockImplementation(inputMock);
|
|
109
|
-
vi.mocked(
|
|
109
|
+
vi.mocked(ensureOutputAISystem).mockResolvedValue();
|
|
110
110
|
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
111
111
|
vi.mocked(invokePlanWorkflow).mockResolvedValue('# Plan');
|
|
112
112
|
vi.mocked(replyToClaude).mockResolvedValue('# Plan');
|
|
@@ -122,7 +122,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
122
122
|
args: {},
|
|
123
123
|
flags: { 'force-agent-file-write': false }
|
|
124
124
|
});
|
|
125
|
-
vi.mocked(
|
|
125
|
+
vi.mocked(ensureOutputAISystem).mockResolvedValue();
|
|
126
126
|
vi.mocked(input).mockResolvedValue('Test workflow');
|
|
127
127
|
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
128
128
|
vi.mocked(invokePlanWorkflow).mockRejectedValue(new Error('ANTHROPIC_API_KEY environment variable is required'));
|
|
@@ -134,7 +134,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
134
134
|
args: {},
|
|
135
135
|
flags: { 'force-agent-file-write': false }
|
|
136
136
|
});
|
|
137
|
-
vi.mocked(
|
|
137
|
+
vi.mocked(ensureOutputAISystem).mockResolvedValue();
|
|
138
138
|
vi.mocked(input).mockResolvedValue('Test workflow');
|
|
139
139
|
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
140
140
|
const claudeError = new ClaudeInvocationError('Failed to invoke claude-code: API error');
|
|
@@ -158,7 +158,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
158
158
|
state.inputCallCount++;
|
|
159
159
|
return state.inputCallCount === 1 ? 'Test workflow' : 'ACCEPT';
|
|
160
160
|
}));
|
|
161
|
-
vi.mocked(
|
|
161
|
+
vi.mocked(ensureOutputAISystem).mockResolvedValue();
|
|
162
162
|
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
163
163
|
vi.mocked(invokePlanWorkflow).mockResolvedValue('# Plan');
|
|
164
164
|
vi.mocked(replyToClaude).mockResolvedValue('# Plan');
|
|
@@ -247,7 +247,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
247
247
|
args: {},
|
|
248
248
|
flags: { 'force-agent-file-write': false }
|
|
249
249
|
});
|
|
250
|
-
vi.mocked(
|
|
250
|
+
vi.mocked(ensureOutputAISystem).mockResolvedValue();
|
|
251
251
|
vi.mocked(input).mockResolvedValue('Test workflow');
|
|
252
252
|
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
253
253
|
const timeoutError = new Error('Request timeout');
|
|
@@ -261,7 +261,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
261
261
|
args: {},
|
|
262
262
|
flags: { 'force-agent-file-write': false }
|
|
263
263
|
});
|
|
264
|
-
vi.mocked(
|
|
264
|
+
vi.mocked(ensureOutputAISystem).mockResolvedValue();
|
|
265
265
|
vi.mocked(input).mockResolvedValue('Test workflow');
|
|
266
266
|
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
267
267
|
const rateLimitError = new ClaudeInvocationError('Rate limit exceeded');
|
|
@@ -279,7 +279,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
279
279
|
state.inputCallCount++;
|
|
280
280
|
return state.inputCallCount === 1 ? 'Test workflow' : 'ACCEPT';
|
|
281
281
|
}));
|
|
282
|
-
vi.mocked(
|
|
282
|
+
vi.mocked(ensureOutputAISystem).mockResolvedValue();
|
|
283
283
|
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
284
284
|
vi.mocked(invokePlanWorkflow).mockResolvedValue('# Plan');
|
|
285
285
|
vi.mocked(replyToClaude).mockResolvedValue('# Plan');
|
|
@@ -295,7 +295,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
295
295
|
flags: { 'force-agent-file-write': false }
|
|
296
296
|
});
|
|
297
297
|
// First call fails with ENOENT, second succeeds
|
|
298
|
-
vi.mocked(
|
|
298
|
+
vi.mocked(ensureOutputAISystem).mockRejectedValueOnce(Object.assign(new Error('Directory does not exist'), { code: 'ENOENT' }));
|
|
299
299
|
await expect(command.run()).rejects.toThrow();
|
|
300
300
|
});
|
|
301
301
|
it('should handle malformed API responses', async () => {
|
|
@@ -304,7 +304,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
304
304
|
args: {},
|
|
305
305
|
flags: { 'force-agent-file-write': false }
|
|
306
306
|
});
|
|
307
|
-
vi.mocked(
|
|
307
|
+
vi.mocked(ensureOutputAISystem).mockResolvedValue();
|
|
308
308
|
vi.mocked(input).mockResolvedValue('Test workflow');
|
|
309
309
|
vi.mocked(generatePlanName).mockResolvedValue('2025_10_06_test');
|
|
310
310
|
const malformedError = new ClaudeInvocationError('Invalid JSON response');
|
|
@@ -323,7 +323,7 @@ describe('WorkflowPlan Command', () => {
|
|
|
323
323
|
vi.mocked(updateAgentTemplates).mockResolvedValue();
|
|
324
324
|
await command.run();
|
|
325
325
|
// Verify templates are updated before plan generation
|
|
326
|
-
const ensureCall = vi.mocked(
|
|
326
|
+
const ensureCall = vi.mocked(ensureOutputAISystem).mock.invocationCallOrder[0];
|
|
327
327
|
const updateCall = vi.mocked(updateAgentTemplates).mock.invocationCallOrder[0];
|
|
328
328
|
expect(updateCall).toBeGreaterThan(ensureCall);
|
|
329
329
|
});
|
|
@@ -10,17 +10,17 @@ export declare class ClaudeInvocationError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
export declare function replyToClaude(message: string, options?: Options): Promise<string>;
|
|
12
12
|
/**
|
|
13
|
-
* Invoke claude-code with /plan_workflow slash command
|
|
13
|
+
* Invoke claude-code with /outputai:plan_workflow slash command
|
|
14
14
|
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
15
|
-
*
|
|
15
|
+
* ensureOutputAISystem() scaffolds the command files to that location.
|
|
16
16
|
* @param description - Workflow description
|
|
17
17
|
* @returns Plan output from claude-code
|
|
18
18
|
*/
|
|
19
19
|
export declare function invokePlanWorkflow(description: string): Promise<string>;
|
|
20
20
|
/**
|
|
21
|
-
* Invoke claude-code with /build_workflow slash command
|
|
21
|
+
* Invoke claude-code with /outputai:build_workflow slash command
|
|
22
22
|
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
23
|
-
*
|
|
23
|
+
* ensureOutputAISystem() scaffolds the command files to that location.
|
|
24
24
|
* @param planFilePath - Absolute path to the plan file
|
|
25
25
|
* @param workflowDir - Absolute path to the workflow directory
|
|
26
26
|
* @param workflowName - Name of the workflow
|
|
@@ -15,7 +15,7 @@ describe('invokePlanWorkflow - Integration Tests', () => {
|
|
|
15
15
|
const messages = [];
|
|
16
16
|
try {
|
|
17
17
|
for await (const message of query({
|
|
18
|
-
prompt: `/plan_workflow ${description}`,
|
|
18
|
+
prompt: `/outputai:plan_workflow ${description}`,
|
|
19
19
|
options: { maxTurns: 1 }
|
|
20
20
|
})) {
|
|
21
21
|
console.log('\nReceived message:', JSON.stringify(message, null, 2));
|
|
@@ -31,7 +31,7 @@ describe('invokePlanWorkflow - Integration Tests', () => {
|
|
|
31
31
|
// This test is just for debugging - we expect messages
|
|
32
32
|
expect(messages.length).toBeGreaterThan(0);
|
|
33
33
|
}, 60000); // 60 second timeout
|
|
34
|
-
it('should successfully invoke /plan_workflow slash command and return content', async () => {
|
|
34
|
+
it('should successfully invoke /outputai:plan_workflow slash command and return content', async () => {
|
|
35
35
|
const description = 'Simple workflow that takes a number and doubles it';
|
|
36
36
|
const result = await invokePlanWorkflow(description);
|
|
37
37
|
console.log('\n===== PLAN RESULT =====');
|
|
@@ -33,8 +33,8 @@ const ADDITIONAL_INSTRUCTIONS_BUILD = `
|
|
|
33
33
|
|
|
34
34
|
4. After you mark all todos as complete, provide a summary of what was implemented.
|
|
35
35
|
`;
|
|
36
|
-
const PLAN_COMMAND = 'plan_workflow';
|
|
37
|
-
const BUILD_COMMAND = 'build_workflow';
|
|
36
|
+
const PLAN_COMMAND = 'outputai:plan_workflow';
|
|
37
|
+
const BUILD_COMMAND = 'outputai:build_workflow';
|
|
38
38
|
const GLOBAL_CLAUDE_OPTIONS = {
|
|
39
39
|
settingSources: ['user', 'project', 'local']
|
|
40
40
|
};
|
|
@@ -86,7 +86,7 @@ function displaySystemValidationWarnings(validation) {
|
|
|
86
86
|
ux.warn(`Missing required claude-code slash command: /${command}`);
|
|
87
87
|
});
|
|
88
88
|
ux.warn('Your claude-code agent is missing key configurations, it may not behave as expected.');
|
|
89
|
-
ux.warn('Please run "output
|
|
89
|
+
ux.warn('Please run "npx output agents init" to fix this.');
|
|
90
90
|
}
|
|
91
91
|
function applyDefaultOptions(options) {
|
|
92
92
|
return {
|
|
@@ -186,9 +186,9 @@ export async function replyToClaude(message, options = {}) {
|
|
|
186
186
|
return singleQuery(applyInstructions(message), { continue: true, ...options });
|
|
187
187
|
}
|
|
188
188
|
/**
|
|
189
|
-
* Invoke claude-code with /plan_workflow slash command
|
|
189
|
+
* Invoke claude-code with /outputai:plan_workflow slash command
|
|
190
190
|
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
191
|
-
*
|
|
191
|
+
* ensureOutputAISystem() scaffolds the command files to that location.
|
|
192
192
|
* @param description - Workflow description
|
|
193
193
|
* @returns Plan output from claude-code
|
|
194
194
|
*/
|
|
@@ -196,9 +196,9 @@ export async function invokePlanWorkflow(description) {
|
|
|
196
196
|
return singleQuery(applyInstructions(`/${PLAN_COMMAND} ${description}`), PLAN_COMMAND_OPTIONS);
|
|
197
197
|
}
|
|
198
198
|
/**
|
|
199
|
-
* Invoke claude-code with /build_workflow slash command
|
|
199
|
+
* Invoke claude-code with /outputai:build_workflow slash command
|
|
200
200
|
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
201
|
-
*
|
|
201
|
+
* ensureOutputAISystem() scaffolds the command files to that location.
|
|
202
202
|
* @param planFilePath - Absolute path to the plan file
|
|
203
203
|
* @param workflowDir - Absolute path to the workflow directory
|
|
204
204
|
* @param workflowName - Name of the workflow
|