@output.ai/cli 0.5.6 → 0.7.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/README.md +1 -0
- package/dist/api/generated/api.d.ts +25 -0
- package/dist/api/generated/api.js +11 -0
- 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/commands/workflow/terminate.d.ts +13 -0
- package/dist/commands/workflow/terminate.js +39 -0
- 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 +5 -3
- 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
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ OUTPUT_CLI_ENV=/etc/output/production.env npx output workflow status wf-123
|
|
|
54
54
|
| `output workflow status` | Get workflow execution status |
|
|
55
55
|
| `output workflow result` | Get workflow execution result |
|
|
56
56
|
| `output workflow stop` | Stop a workflow execution |
|
|
57
|
+
| `output workflow terminate` | Terminate a workflow execution (force stop) |
|
|
57
58
|
| `output workflow debug` | Display workflow execution trace |
|
|
58
59
|
|
|
59
60
|
## Development Services
|
|
@@ -198,6 +198,10 @@ export type GetWorkflowIdStatus200 = {
|
|
|
198
198
|
/** An epoch timestamp representing when the workflow ended */
|
|
199
199
|
completedAt?: number;
|
|
200
200
|
};
|
|
201
|
+
export type PostWorkflowIdTerminateBody = {
|
|
202
|
+
/** Optional reason for termination */
|
|
203
|
+
reason?: string;
|
|
204
|
+
};
|
|
201
205
|
export type GetWorkflowIdResult200 = {
|
|
202
206
|
/** The workflow execution id */
|
|
203
207
|
workflowId?: string;
|
|
@@ -316,6 +320,27 @@ export type patchWorkflowIdStopResponseError = (patchWorkflowIdStopResponse404)
|
|
|
316
320
|
export type patchWorkflowIdStopResponse = (patchWorkflowIdStopResponseSuccess | patchWorkflowIdStopResponseError);
|
|
317
321
|
export declare const getPatchWorkflowIdStopUrl: (id: string) => string;
|
|
318
322
|
export declare const patchWorkflowIdStop: (id: string, options?: ApiRequestOptions) => Promise<patchWorkflowIdStopResponse>;
|
|
323
|
+
/**
|
|
324
|
+
* Force terminates a workflow. Unlike stop/cancel, terminate immediately stops the workflow without allowing cleanup.
|
|
325
|
+
* @summary Terminate a workflow execution (force stop)
|
|
326
|
+
*/
|
|
327
|
+
export type postWorkflowIdTerminateResponse200 = {
|
|
328
|
+
data: void;
|
|
329
|
+
status: 200;
|
|
330
|
+
};
|
|
331
|
+
export type postWorkflowIdTerminateResponse404 = {
|
|
332
|
+
data: void;
|
|
333
|
+
status: 404;
|
|
334
|
+
};
|
|
335
|
+
export type postWorkflowIdTerminateResponseSuccess = (postWorkflowIdTerminateResponse200) & {
|
|
336
|
+
headers: Headers;
|
|
337
|
+
};
|
|
338
|
+
export type postWorkflowIdTerminateResponseError = (postWorkflowIdTerminateResponse404) & {
|
|
339
|
+
headers: Headers;
|
|
340
|
+
};
|
|
341
|
+
export type postWorkflowIdTerminateResponse = (postWorkflowIdTerminateResponseSuccess | postWorkflowIdTerminateResponseError);
|
|
342
|
+
export declare const getPostWorkflowIdTerminateUrl: (id: string) => string;
|
|
343
|
+
export declare const postWorkflowIdTerminate: (id: string, postWorkflowIdTerminateBody: PostWorkflowIdTerminateBody, options?: ApiRequestOptions) => Promise<postWorkflowIdTerminateResponse>;
|
|
319
344
|
/**
|
|
320
345
|
* @summary Return the result of a workflow
|
|
321
346
|
*/
|
|
@@ -87,6 +87,17 @@ export const patchWorkflowIdStop = async (id, options) => {
|
|
|
87
87
|
method: 'PATCH'
|
|
88
88
|
});
|
|
89
89
|
};
|
|
90
|
+
export const getPostWorkflowIdTerminateUrl = (id) => {
|
|
91
|
+
return `/workflow/${id}/terminate`;
|
|
92
|
+
};
|
|
93
|
+
export const postWorkflowIdTerminate = async (id, postWorkflowIdTerminateBody, options) => {
|
|
94
|
+
return customFetchInstance(getPostWorkflowIdTerminateUrl(id), {
|
|
95
|
+
...options,
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
98
|
+
body: JSON.stringify(postWorkflowIdTerminateBody)
|
|
99
|
+
});
|
|
100
|
+
};
|
|
90
101
|
export const getGetWorkflowIdResultUrl = (id) => {
|
|
91
102
|
return `/workflow/${id}/result`;
|
|
92
103
|
};
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class WorkflowTerminate extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
workflowId: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
reason: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
catch(error: Error): Promise<void>;
|
|
13
|
+
}
|