@output.ai/cli 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { checkAgentConfigDirExists, checkAgentStructure, getAgentConfigDir, prepareTemplateVariables, initializeAgentConfig, ensureOutputAISystem } from './coding_agents.js';
2
+ import { checkAgentStructure, prepareTemplateVariables, initializeAgentConfig, ensureOutputAISystem, ensureClaudePlugin } from './coding_agents.js';
3
3
  import { access } from 'node:fs/promises';
4
4
  import fs from 'node:fs/promises';
5
5
  vi.mock('node:fs/promises');
@@ -26,35 +26,17 @@ describe('coding_agents service', () => {
26
26
  beforeEach(() => {
27
27
  vi.clearAllMocks();
28
28
  });
29
- describe('getAgentConfigDir', () => {
30
- it('should return the correct path to .outputai directory', () => {
31
- const result = getAgentConfigDir('/test/project');
32
- expect(result).toBe('/test/project/.outputai');
33
- });
34
- });
35
- describe('checkAgentConfigDirExists', () => {
36
- it('should return true when .outputai directory exists', async () => {
37
- vi.mocked(access).mockResolvedValue(undefined);
38
- const result = await checkAgentConfigDirExists('/test/project');
39
- expect(result).toBe(true);
40
- expect(access).toHaveBeenCalledWith('/test/project/.outputai');
41
- });
42
- it('should return false when .outputai directory does not exist', async () => {
43
- vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
44
- const result = await checkAgentConfigDirExists('/test/project');
45
- expect(result).toBe(false);
46
- });
47
- });
48
29
  describe('checkAgentStructure', () => {
49
- it('should return needsInit true when directory does not exist', async () => {
30
+ it('should return needsInit true when settings.json does not exist', async () => {
50
31
  vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
32
+ vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' });
51
33
  const result = await checkAgentStructure('/test/project');
52
34
  expect(result).toEqual({
53
35
  isComplete: false,
54
36
  needsInit: true
55
37
  });
56
38
  });
57
- it('should return complete when all files exist with valid settings', async () => {
39
+ it('should return complete when settings and CLAUDE.md exist with valid configuration', async () => {
58
40
  vi.mocked(access).mockResolvedValue(undefined);
59
41
  vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
60
42
  extraKnownMarketplaces: {
@@ -113,19 +95,6 @@ describe('coding_agents service', () => {
113
95
  expect(result.isComplete).toBe(false);
114
96
  expect(result.needsInit).toBe(true);
115
97
  });
116
- it('should return needsInit true when settings.json does not exist', async () => {
117
- vi.mocked(access).mockImplementation(async (path) => {
118
- const pathStr = path.toString();
119
- if (pathStr.endsWith('.claude/settings.json')) {
120
- throw { code: 'ENOENT' };
121
- }
122
- return undefined;
123
- });
124
- vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' });
125
- const result = await checkAgentStructure('/test/project');
126
- expect(result.isComplete).toBe(false);
127
- expect(result.needsInit).toBe(true);
128
- });
129
98
  });
130
99
  describe('prepareTemplateVariables', () => {
131
100
  it('should return template variables with formatted date', () => {
@@ -141,19 +110,18 @@ describe('coding_agents service', () => {
141
110
  vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
142
111
  vi.mocked(fs.readFile).mockResolvedValue('template content');
143
112
  vi.mocked(fs.writeFile).mockResolvedValue(undefined);
144
- vi.mocked(fs.symlink).mockResolvedValue(undefined);
145
113
  });
146
- it('should create exactly 3 outputs: AGENTS.md, settings.json, and CLAUDE.md symlink', async () => {
114
+ it('should create exactly 2 outputs: settings.json and CLAUDE.md file', async () => {
147
115
  await initializeAgentConfig({
148
116
  projectRoot: '/test/project',
149
117
  force: false
150
118
  });
151
- expect(fs.mkdir).toHaveBeenCalledTimes(2);
152
- expect(fs.mkdir).toHaveBeenCalledWith('/test/project/.outputai', expect.objectContaining({ recursive: true }));
119
+ expect(fs.mkdir).toHaveBeenCalledTimes(1);
153
120
  expect(fs.mkdir).toHaveBeenCalledWith('/test/project/.claude', expect.objectContaining({ recursive: true }));
154
- expect(fs.writeFile).toHaveBeenCalledWith('/test/project/.outputai/AGENTS.md', expect.any(String), 'utf-8');
155
121
  expect(fs.writeFile).toHaveBeenCalledWith('/test/project/.claude/settings.json', expect.any(String), 'utf-8');
156
- expect(fs.symlink).toHaveBeenCalledTimes(1);
122
+ expect(fs.writeFile).toHaveBeenCalledWith('/test/project/CLAUDE.md', expect.any(String), 'utf-8');
123
+ // No symlink should be created - CLAUDE.md is now a real file
124
+ expect(fs.symlink).not.toHaveBeenCalled();
157
125
  });
158
126
  it('should skip existing files when force is false', async () => {
159
127
  vi.mocked(access).mockResolvedValue(undefined);
@@ -162,7 +130,6 @@ describe('coding_agents service', () => {
162
130
  force: false
163
131
  });
164
132
  expect(fs.writeFile).not.toHaveBeenCalled();
165
- expect(fs.symlink).not.toHaveBeenCalled();
166
133
  });
167
134
  it('should overwrite existing files when force is true', async () => {
168
135
  vi.mocked(access).mockResolvedValue(undefined);
@@ -173,15 +140,40 @@ describe('coding_agents service', () => {
173
140
  });
174
141
  expect(fs.writeFile).toHaveBeenCalled();
175
142
  });
176
- it('should handle symlink errors by falling back to copy', async () => {
177
- vi.mocked(fs.symlink).mockRejectedValue({ code: 'ENOTSUP' });
178
- await initializeAgentConfig({
179
- projectRoot: '/test/project',
180
- force: false
181
- });
182
- const writeFileCalls = vi.mocked(fs.writeFile).mock.calls;
183
- const claudeMdCalls = writeFileCalls.filter(call => call[0].toString().includes('CLAUDE.md'));
184
- expect(claudeMdCalls.length).toBe(1);
143
+ });
144
+ describe('ensureClaudePlugin', () => {
145
+ beforeEach(() => {
146
+ vi.mocked(fs.mkdir).mockResolvedValue(undefined);
147
+ vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
148
+ vi.mocked(fs.readFile).mockResolvedValue('template content');
149
+ vi.mocked(fs.writeFile).mockResolvedValue(undefined);
150
+ });
151
+ it('should call registerPluginMarketplace and installOutputAIPlugin', async () => {
152
+ const { executeClaudeCommand } = await import('../utils/claude.js');
153
+ await ensureClaudePlugin('/test/project');
154
+ expect(executeClaudeCommand).toHaveBeenCalledWith(['plugin', 'marketplace', 'add', 'growthxai/output-claude-plugins'], '/test/project', { ignoreFailure: true });
155
+ expect(executeClaudeCommand).toHaveBeenCalledWith(['plugin', 'marketplace', 'update', 'outputai'], '/test/project');
156
+ expect(executeClaudeCommand).toHaveBeenCalledWith(['plugin', 'install', 'outputai@outputai', '--scope', 'project'], '/test/project');
157
+ });
158
+ it('should show error and prompt user when plugin commands fail', async () => {
159
+ const { executeClaudeCommand } = await import('../utils/claude.js');
160
+ const { confirm } = await import('@inquirer/prompts');
161
+ vi.mocked(executeClaudeCommand)
162
+ .mockResolvedValueOnce(undefined) // marketplace add
163
+ .mockRejectedValueOnce(new Error('Plugin update failed')); // marketplace update
164
+ vi.mocked(confirm).mockResolvedValue(true);
165
+ await expect(ensureClaudePlugin('/test/project')).resolves.not.toThrow();
166
+ expect(confirm).toHaveBeenCalledWith(expect.objectContaining({
167
+ message: expect.stringContaining('proceed')
168
+ }));
169
+ });
170
+ it('should allow user to proceed without plugin setup if they confirm', async () => {
171
+ const { executeClaudeCommand } = await import('../utils/claude.js');
172
+ const { confirm } = await import('@inquirer/prompts');
173
+ vi.mocked(executeClaudeCommand)
174
+ .mockRejectedValue(new Error('All plugin commands fail'));
175
+ vi.mocked(confirm).mockResolvedValue(true);
176
+ await expect(ensureClaudePlugin('/test/project')).resolves.not.toThrow();
185
177
  });
186
178
  });
187
179
  describe('ensureOutputAISystem', () => {
@@ -190,7 +182,6 @@ describe('coding_agents service', () => {
190
182
  vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
191
183
  vi.mocked(fs.readFile).mockResolvedValue('template content');
192
184
  vi.mocked(fs.writeFile).mockResolvedValue(undefined);
193
- vi.mocked(fs.symlink).mockResolvedValue(undefined);
194
185
  });
195
186
  it('should return immediately when agent structure is complete', async () => {
196
187
  vi.mocked(access).mockResolvedValue(undefined);
@@ -205,12 +196,6 @@ describe('coding_agents service', () => {
205
196
  await ensureOutputAISystem('/test/project');
206
197
  expect(fs.mkdir).not.toHaveBeenCalled();
207
198
  });
208
- it('should auto-initialize when directory does not exist', async () => {
209
- vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
210
- await ensureOutputAISystem('/test/project');
211
- expect(fs.mkdir).toHaveBeenCalled();
212
- expect(fs.writeFile).toHaveBeenCalled();
213
- });
214
199
  it('should auto-initialize when settings.json is invalid', async () => {
215
200
  vi.mocked(access).mockResolvedValue(undefined);
216
201
  vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
@@ -231,7 +216,6 @@ describe('coding_agents service', () => {
231
216
  vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
232
217
  vi.mocked(fs.readFile).mockResolvedValue('template content');
233
218
  vi.mocked(fs.writeFile).mockResolvedValue(undefined);
234
- vi.mocked(fs.symlink).mockResolvedValue(undefined);
235
219
  });
236
220
  it('should show error and prompt user when registerPluginMarketplace fails', async () => {
237
221
  const { executeClaudeCommand } = await import('../utils/claude.js');
@@ -8,7 +8,7 @@ export declare function generatePlanName(description: string, date?: Date): Prom
8
8
  */
9
9
  export declare function writePlanFile(planName: string, content: string, projectRoot: string): Promise<string>;
10
10
  /**
11
- * Update agent templates by invoking agents init with force flag
11
+ * Update agent templates by reinitializing with force flag
12
12
  * This recreates all agent configuration files, overwriting existing ones
13
13
  * @param projectRoot - Root directory of the project
14
14
  */
@@ -36,7 +36,7 @@ export async function writePlanFile(planName, content, projectRoot) {
36
36
  return planFilePath;
37
37
  }
38
38
  /**
39
- * Update agent templates by invoking agents init with force flag
39
+ * Update agent templates by reinitializing with force flag
40
40
  * This recreates all agent configuration files, overwriting existing ones
41
41
  * @param projectRoot - Root directory of the project
42
42
  */
@@ -98,7 +98,7 @@ describe('workflow-planner service', () => {
98
98
  });
99
99
  });
100
100
  describe('updateAgentTemplates', () => {
101
- it('should invoke agents init with force flag', async () => {
101
+ it('should invoke initializeAgentConfig with force flag', async () => {
102
102
  vi.mocked(initializeAgentConfig).mockResolvedValue();
103
103
  await updateAgentTemplates('/test/project');
104
104
  expect(initializeAgentConfig).toHaveBeenCalledWith({
@@ -106,7 +106,7 @@ describe('workflow-planner service', () => {
106
106
  force: true
107
107
  });
108
108
  });
109
- it('should propagate errors from agents init', async () => {
109
+ it('should propagate errors from initializeAgentConfig', async () => {
110
110
  vi.mocked(initializeAgentConfig).mockRejectedValue(new Error('Failed to write templates'));
111
111
  await expect(updateAgentTemplates('/test/project'))
112
112
  .rejects.toThrow('Failed to write templates');
@@ -0,0 +1,19 @@
1
+ # CLAUDE.md
2
+
3
+ This is an **Output.ai** project - a framework for building reliable, production-ready LLM workflows and agents.
4
+
5
+ ## Getting Started
6
+
7
+ For full framework documentation, commands, and AI-assisted workflow development, install our Claude Code plugins:
8
+
9
+ ```bash
10
+ claude plugin marketplace add growthxai/output-claude-plugins
11
+ claude plugin install outputai@outputai --scope project
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Project-Specific Instructions
17
+
18
+ <!-- Add your project-specific instructions below -->
19
+
@@ -11,7 +11,7 @@ export declare const mockLLM: {
11
11
  generateText: import("vitest").Mock<(...args: any[]) => any>;
12
12
  };
13
13
  /**
14
- * Mock for child_process spawn (for agents init)
14
+ * Mock for child_process spawn (for agent commands)
15
15
  */
16
16
  export declare const mockSpawn: import("vitest").Mock<(...args: any[]) => any>;
17
17
  /**
@@ -24,7 +24,7 @@ export const mockLLM = {
24
24
  })
25
25
  };
26
26
  /**
27
- * Mock for child_process spawn (for agents init)
27
+ * Mock for child_process spawn (for agent commands)
28
28
  */
29
29
  export const mockSpawn = vi.fn().mockImplementation(() => {
30
30
  const mockChild = {
@@ -1,20 +1,22 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { ux } from '@oclif/core';
3
+ import debugFactory from 'debug';
3
4
  import { getErrorMessage } from './error_utils.js';
5
+ const debug = debugFactory('output-cli:process');
4
6
  export async function executeCommand(command, args, cwd) {
5
7
  const stderrLines = [];
6
8
  const proc = spawn(command, args, { cwd });
7
9
  const handleStdout = (data) => {
8
10
  const line = data.toString().trim();
9
11
  if (line) {
10
- ux.stdout(line);
12
+ debug(line);
11
13
  }
12
14
  };
13
15
  const handleStderr = (data) => {
14
16
  const line = data.toString().trim();
15
17
  if (line) {
16
18
  stderrLines.push(line);
17
- ux.stdout(line);
19
+ debug(line);
18
20
  }
19
21
  };
20
22
  proc.stdout.on('data', handleStdout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,8 +18,7 @@
18
18
  "copy-assets": "./bin/copyassets.sh",
19
19
  "test": "vitest run",
20
20
  "generate:api": "orval --config ./orval.config.ts",
21
- "update:versions": "node scripts/update_sdk_versions.js",
22
- "prebuild": "npm run generate:api && npm run update:versions"
21
+ "prebuild": "npm run generate:api"
23
22
  },
24
23
  "dependencies": {
25
24
  "@anthropic-ai/claude-agent-sdk": "0.1.71",
@@ -1,43 +0,0 @@
1
- import { Command, Flags } from '@oclif/core';
2
- import { initializeAgentConfig } from '#services/coding_agents.js';
3
- import { getErrorMessage, getErrorCode } from '#utils/error_utils.js';
4
- export default class Init extends Command {
5
- static description = 'Initialize agent configuration files for Claude Code plugin integration';
6
- static examples = [
7
- '<%= config.bin %> <%= command.id %>',
8
- '<%= config.bin %> <%= command.id %> --force'
9
- ];
10
- static args = {};
11
- static flags = {
12
- force: Flags.boolean({
13
- char: 'f',
14
- description: 'Overwrite existing files',
15
- default: false
16
- })
17
- };
18
- async run() {
19
- const { flags } = await this.parse(Init);
20
- this.log('Initializing agent configuration for Claude Code...');
21
- try {
22
- await initializeAgentConfig({
23
- projectRoot: process.cwd(),
24
- force: flags.force
25
- });
26
- this.log('Agent configuration initialized successfully!');
27
- 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');
32
- this.log('');
33
- this.log('Claude Code will automatically use the OutputAI plugin.');
34
- }
35
- catch (error) {
36
- if (getErrorCode(error) === 'EACCES') {
37
- this.error('Permission denied. Please check file permissions and try again.');
38
- return;
39
- }
40
- this.error(`Failed to initialize agent configuration: ${getErrorMessage(error)}`);
41
- }
42
- }
43
- }
@@ -1,109 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
- import Init from './init.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
- }));
12
- describe('agents init', () => {
13
- const createTestCommand = (args = []) => {
14
- const cmd = new Init(args, {});
15
- cmd.log = vi.fn();
16
- cmd.warn = vi.fn();
17
- cmd.error = vi.fn();
18
- cmd.debug = vi.fn();
19
- cmd.parse = vi.fn();
20
- return cmd;
21
- };
22
- beforeEach(() => {
23
- vi.clearAllMocks();
24
- vi.mocked(initializeAgentConfig).mockResolvedValue(undefined);
25
- });
26
- afterEach(() => {
27
- vi.restoreAllMocks();
28
- });
29
- describe('command structure', () => {
30
- it('should have correct description', () => {
31
- expect(Init.description).toBeDefined();
32
- expect(Init.description).toContain('agent configuration');
33
- });
34
- it('should have correct examples without agent-provider flag', () => {
35
- expect(Init.examples).toBeDefined();
36
- expect(Array.isArray(Init.examples)).toBe(true);
37
- const examplesStr = Init.examples.join(' ');
38
- expect(examplesStr).not.toContain('agent-provider');
39
- });
40
- it('should have no required arguments', () => {
41
- expect(Init.args).toBeDefined();
42
- expect(Object.keys(Init.args)).toHaveLength(0);
43
- });
44
- it('should only have force flag', () => {
45
- expect(Init.flags).toBeDefined();
46
- expect(Init.flags.force).toBeDefined();
47
- expect(Object.keys(Init.flags)).toHaveLength(1);
48
- });
49
- });
50
- describe('successful execution', () => {
51
- it('should call initializeAgentConfig with correct options', async () => {
52
- const cmd = createTestCommand();
53
- cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
54
- await cmd.run();
55
- expect(initializeAgentConfig).toHaveBeenCalledWith({
56
- projectRoot: expect.any(String),
57
- force: false
58
- });
59
- });
60
- it('should pass force flag to initializeAgentConfig', async () => {
61
- const cmd = createTestCommand(['--force']);
62
- cmd.parse.mockResolvedValue({ flags: { force: true }, args: {} });
63
- await cmd.run();
64
- expect(initializeAgentConfig).toHaveBeenCalledWith({
65
- projectRoot: expect.any(String),
66
- force: true
67
- });
68
- });
69
- it('should display success messages', async () => {
70
- const cmd = createTestCommand();
71
- cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
72
- await cmd.run();
73
- expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('initialized successfully'));
74
- expect(cmd.log).toHaveBeenCalledWith('Configured:');
75
- expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('OutputAI plugin'));
76
- });
77
- it('should display plugin configuration messages', async () => {
78
- const cmd = createTestCommand();
79
- cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
80
- await cmd.run();
81
- expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('marketplace'));
82
- expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('plugin'));
83
- });
84
- });
85
- describe('error handling', () => {
86
- it('should handle permission errors', async () => {
87
- vi.mocked(initializeAgentConfig).mockRejectedValue({ code: 'EACCES' });
88
- const cmd = createTestCommand();
89
- cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
90
- await cmd.run();
91
- expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Permission denied'));
92
- });
93
- it('should handle general errors with message', async () => {
94
- vi.mocked(initializeAgentConfig).mockRejectedValue(new Error('Something went wrong'));
95
- const cmd = createTestCommand();
96
- cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
97
- await cmd.run();
98
- expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Failed to initialize agent configuration'));
99
- expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Something went wrong'));
100
- });
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.'));
103
- const cmd = createTestCommand();
104
- cmd.parse.mockResolvedValue({ flags: { force: false }, args: {} });
105
- await cmd.run();
106
- expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Claude CLI not found'));
107
- });
108
- });
109
- });