@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.
Files changed (57) hide show
  1. package/README.md +1 -0
  2. package/dist/api/generated/api.d.ts +25 -0
  3. package/dist/api/generated/api.js +11 -0
  4. package/dist/commands/agents/init.d.ts +0 -1
  5. package/dist/commands/agents/init.js +9 -16
  6. package/dist/commands/agents/init.spec.js +49 -224
  7. package/dist/commands/workflow/generate.js +2 -2
  8. package/dist/commands/workflow/plan.js +3 -3
  9. package/dist/commands/workflow/plan.spec.js +13 -13
  10. package/dist/commands/workflow/terminate.d.ts +13 -0
  11. package/dist/commands/workflow/terminate.js +39 -0
  12. package/dist/services/claude_client.d.ts +4 -4
  13. package/dist/services/claude_client.integration.test.js +2 -2
  14. package/dist/services/claude_client.js +7 -7
  15. package/dist/services/claude_client.spec.js +3 -3
  16. package/dist/services/coding_agents.d.ts +10 -24
  17. package/dist/services/coding_agents.js +112 -368
  18. package/dist/services/coding_agents.spec.js +101 -290
  19. package/dist/services/project_scaffold.js +3 -3
  20. package/dist/services/workflow_builder.d.ts +1 -1
  21. package/dist/services/workflow_builder.js +1 -1
  22. package/dist/services/workflow_planner.js +5 -3
  23. package/dist/services/workflow_planner.spec.js +4 -5
  24. package/dist/templates/agent_instructions/dotclaude/settings.json.template +29 -0
  25. package/dist/templates/agent_instructions/{AGENTS.md.template → dotoutputai/AGENTS.md.template} +12 -10
  26. package/dist/utils/claude.d.ts +5 -0
  27. package/dist/utils/claude.js +19 -0
  28. package/dist/utils/claude.spec.d.ts +1 -0
  29. package/dist/utils/claude.spec.js +119 -0
  30. package/dist/utils/paths.d.ts +0 -4
  31. package/dist/utils/paths.js +0 -6
  32. package/package.json +3 -3
  33. package/dist/templates/agent_instructions/agents/workflow_context_fetcher.md.template +0 -82
  34. package/dist/templates/agent_instructions/agents/workflow_debugger.md.template +0 -98
  35. package/dist/templates/agent_instructions/agents/workflow_planner.md.template +0 -113
  36. package/dist/templates/agent_instructions/agents/workflow_prompt_writer.md.template +0 -595
  37. package/dist/templates/agent_instructions/agents/workflow_quality.md.template +0 -244
  38. package/dist/templates/agent_instructions/commands/build_workflow.md.template +0 -290
  39. package/dist/templates/agent_instructions/commands/debug_workflow.md.template +0 -198
  40. package/dist/templates/agent_instructions/commands/plan_workflow.md.template +0 -261
  41. package/dist/templates/agent_instructions/meta/post_flight.md.template +0 -94
  42. package/dist/templates/agent_instructions/meta/pre_flight.md.template +0 -60
  43. package/dist/templates/agent_instructions/skills/output-error-direct-io/SKILL.md.template +0 -249
  44. package/dist/templates/agent_instructions/skills/output-error-http-client/SKILL.md.template +0 -298
  45. package/dist/templates/agent_instructions/skills/output-error-missing-schemas/SKILL.md.template +0 -265
  46. package/dist/templates/agent_instructions/skills/output-error-nondeterminism/SKILL.md.template +0 -252
  47. package/dist/templates/agent_instructions/skills/output-error-try-catch/SKILL.md.template +0 -226
  48. package/dist/templates/agent_instructions/skills/output-error-zod-import/SKILL.md.template +0 -209
  49. package/dist/templates/agent_instructions/skills/output-services-check/SKILL.md.template +0 -128
  50. package/dist/templates/agent_instructions/skills/output-workflow-list/SKILL.md.template +0 -117
  51. package/dist/templates/agent_instructions/skills/output-workflow-result/SKILL.md.template +0 -199
  52. package/dist/templates/agent_instructions/skills/output-workflow-run/SKILL.md.template +0 -228
  53. package/dist/templates/agent_instructions/skills/output-workflow-runs-list/SKILL.md.template +0 -141
  54. package/dist/templates/agent_instructions/skills/output-workflow-start/SKILL.md.template +0 -201
  55. package/dist/templates/agent_instructions/skills/output-workflow-status/SKILL.md.template +0 -151
  56. package/dist/templates/agent_instructions/skills/output-workflow-stop/SKILL.md.template +0 -164
  57. 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 { AGENT_CONFIG_DIR } from '#config.js';
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 AI assistant integration';
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('Agent configuration initialized successfully!');
26
+ this.log('Agent configuration initialized successfully!');
35
27
  this.log('');
36
- this.log('Created:');
37
- this.log(` ${AGENT_CONFIG_DIR}/ directory with agent and command configurations`);
38
- this.log(' .claude/ directory with symlinks for Claude Code integration');
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 detect and use these configurations.');
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 no-restricted-syntax, @typescript-eslint/no-explicit-any, init-declarations */
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 { getTemplateDir } from '#utils/paths.js';
7
- import { processTemplate } from '#utils/template.js';
8
- vi.mock('node:fs/promises');
9
- vi.mock('node:path', async () => {
10
- const actual = await vi.importActual('node:path');
11
- return {
12
- ...actual,
13
- join: vi.fn((...args) => args.join('/')),
14
- dirname: vi.fn(p => p.split('/').slice(0, -1).join('/') || '.'),
15
- relative: vi.fn((from, to) => {
16
- if (to === 'CLAUDE.md' && from === '.') {
17
- return '.outputai/AGENTS.md';
18
- }
19
- if (to.includes('.outputai/agents')) {
20
- return `../../.outputai/agents/${path.basename(to)}`;
21
- }
22
- if (to.includes('.outputai/commands')) {
23
- return `../../.outputai/commands/${path.basename(to)}`;
24
- }
25
- return to;
26
- })
27
- };
28
- });
29
- vi.mock('../../utils/paths.js');
30
- vi.mock('../../utils/template.js');
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(fs.mkdir).mockResolvedValue(undefined);
46
- vi.mocked(fs.writeFile).mockResolvedValue(undefined);
47
- vi.mocked(fs.symlink).mockResolvedValue(undefined);
48
- vi.mocked(fs.stat).mockRejectedValue({ code: 'ENOENT' });
49
- vi.mocked(fs.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 appropriate flags', () => {
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('directory creation', () => {
81
- it('should create .outputai directory structure', async () => {
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: { 'agent-provider': 'claude-code', force: false }, args: {} });
53
+ cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
110
54
  await cmd.run();
111
- expect(mockReadFile).toHaveBeenCalledWith(expect.stringContaining('AGENTS.md.template'), 'utf-8');
112
- expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining('.outputai/AGENTS.md'), expect.stringContaining('Agent instructions'), 'utf-8');
55
+ expect(initializeAgentConfig).toHaveBeenCalledWith({
56
+ projectRoot: expect.any(String),
57
+ force: false
58
+ });
113
59
  });
114
- it('should create all agent configuration files', async () => {
115
- const mockWriteFile = vi.mocked(fs.writeFile);
116
- const cmd = createTestCommand();
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(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents/workflow_planner.md'), expect.stringContaining('.claude/agents/workflow_planner.md'));
64
+ expect(initializeAgentConfig).toHaveBeenCalledWith({
65
+ projectRoot: expect.any(String),
66
+ force: true
67
+ });
167
68
  });
168
- it('should create symlinks for all command files', async () => {
169
- const mockSymlink = vi.mocked(fs.symlink);
69
+ it('should display success messages', async () => {
170
70
  const cmd = createTestCommand();
171
- cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
71
+ cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
172
72
  await cmd.run();
173
- expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands/plan_workflow.md'), expect.stringContaining('.claude/commands/plan_workflow.md'));
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 handle Windows by copying files when symlinks are not supported', async () => {
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: { 'agent-provider': 'claude-code', force: false }, args: {} });
79
+ cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
184
80
  await cmd.run();
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);
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
- const mockMkdir = vi.mocked(fs.mkdir);
207
- mockMkdir.mockRejectedValue({ code: 'EACCES' });
87
+ vi.mocked(initializeAgentConfig).mockRejectedValue({ code: 'EACCES' });
208
88
  const cmd = createTestCommand();
209
- cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
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 be idempotent (safe to run multiple times)', async () => {
214
- const mockWriteFile = vi.mocked(fs.writeFile);
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: { 'agent-provider': 'claude-code', force: false }, args: {} });
218
- await cmd.run();
96
+ cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
219
97
  await cmd.run();
220
- expect(cmd.error).not.toHaveBeenCalled();
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
- describe('complete initialization', () => {
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: { 'agent-provider': 'claude-code', force: false }, args: {} });
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(mockWriteFile).toHaveBeenCalled();
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 { ensureOutputAIStructure } from '#services/coding_agents.js';
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 ensureOutputAIStructure(projectRoot);
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 { ensureOutputAIStructure } from '#services/coding_agents.js';
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 ensureOutputAIStructure(projectRoot);
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 { ensureOutputAIStructure } from '#services/coding_agents.js';
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(ensureOutputAIStructure).mockResolvedValue();
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(ensureOutputAIStructure).toHaveBeenCalled();
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(ensureOutputAIStructure).mockResolvedValue();
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(ensureOutputAIStructure).mockResolvedValue();
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(ensureOutputAIStructure).mockResolvedValue();
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(ensureOutputAIStructure).mockResolvedValue();
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(ensureOutputAIStructure).mockResolvedValue();
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(ensureOutputAIStructure).mockResolvedValue();
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(ensureOutputAIStructure).mockResolvedValue();
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(ensureOutputAIStructure).mockRejectedValueOnce(Object.assign(new Error('Directory does not exist'), { code: 'ENOENT' }));
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(ensureOutputAIStructure).mockResolvedValue();
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(ensureOutputAIStructure).mock.invocationCallOrder[0];
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
+ }