@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.
@@ -79,7 +79,7 @@ services:
79
79
  depends_on:
80
80
  temporal:
81
81
  condition: service_healthy
82
- image: growthxteam/output-api:latest
82
+ image: growthxteam/output-api:${OUTPUT_API_VERSION:-1.9}
83
83
  init: true
84
84
  networks:
85
85
  - main
@@ -1,10 +1,8 @@
1
1
  import { Command } from '@oclif/core';
2
- export default class Init extends Command {
2
+ export default class Update extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static args: {};
6
- static flags: {
7
- force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
- };
6
+ static flags: {};
9
7
  run(): Promise<void>;
10
8
  }
@@ -0,0 +1,29 @@
1
+ import { Command } from '@oclif/core';
2
+ import { ensureClaudePlugin } from '#services/coding_agents.js';
3
+ import { getErrorMessage, getErrorCode } from '#utils/error_utils.js';
4
+ export default class Update extends Command {
5
+ static description = 'Update Claude Code plugin for Output.ai';
6
+ static examples = [
7
+ '<%= config.bin %> <%= command.id %>'
8
+ ];
9
+ static args = {};
10
+ static flags = {};
11
+ async run() {
12
+ this.log('Updating Claude Code plugin...');
13
+ try {
14
+ await ensureClaudePlugin(process.cwd());
15
+ this.log('Claude Code plugin updated successfully!');
16
+ this.log('');
17
+ this.log('Updated:');
18
+ this.log(' - Registered marketplace: growthxai/output-claude-plugins');
19
+ this.log(' - Updated marketplace: outputai');
20
+ this.log(' - Installed plugin: outputai@outputai');
21
+ }
22
+ catch (error) {
23
+ if (getErrorCode(error) === 'EACCES') {
24
+ this.warn('Permission denied. Please check file permissions and try again.');
25
+ }
26
+ this.error(`Failed to update Claude Code plugin: ${getErrorMessage(error)}`);
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,84 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
+ import Update from './update.js';
4
+ import { ensureClaudePlugin } from '#services/coding_agents.js';
5
+ vi.mock('#services/coding_agents.js', () => ({
6
+ ensureClaudePlugin: vi.fn()
7
+ }));
8
+ describe('agents update', () => {
9
+ const createTestCommand = (args = []) => {
10
+ const cmd = new Update(args, {});
11
+ cmd.log = vi.fn();
12
+ cmd.warn = vi.fn();
13
+ cmd.error = vi.fn();
14
+ cmd.debug = vi.fn();
15
+ cmd.parse = vi.fn();
16
+ return cmd;
17
+ };
18
+ beforeEach(() => {
19
+ vi.clearAllMocks();
20
+ vi.mocked(ensureClaudePlugin).mockResolvedValue(undefined);
21
+ });
22
+ afterEach(() => {
23
+ vi.restoreAllMocks();
24
+ });
25
+ describe('command structure', () => {
26
+ it('should have correct description', () => {
27
+ expect(Update.description).toBeDefined();
28
+ expect(Update.description).toContain('Update Claude Code plugin');
29
+ });
30
+ it('should have correct examples', () => {
31
+ expect(Update.examples).toBeDefined();
32
+ expect(Array.isArray(Update.examples)).toBe(true);
33
+ });
34
+ it('should have no required arguments', () => {
35
+ expect(Update.args).toBeDefined();
36
+ expect(Object.keys(Update.args)).toHaveLength(0);
37
+ });
38
+ it('should have no flags', () => {
39
+ expect(Update.flags).toBeDefined();
40
+ expect(Object.keys(Update.flags)).toHaveLength(0);
41
+ });
42
+ });
43
+ describe('successful execution', () => {
44
+ it('should call ensureClaudePlugin with process.cwd()', async () => {
45
+ const cmd = createTestCommand();
46
+ cmd.parse.mockResolvedValue({ flags: {}, args: {} });
47
+ await cmd.run();
48
+ expect(ensureClaudePlugin).toHaveBeenCalledWith(expect.any(String));
49
+ });
50
+ it('should display success messages', async () => {
51
+ const cmd = createTestCommand();
52
+ cmd.parse.mockResolvedValue({ flags: {}, args: {} });
53
+ await cmd.run();
54
+ expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('updated successfully'));
55
+ expect(cmd.log).toHaveBeenCalledWith('Updated:');
56
+ expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('marketplace'));
57
+ });
58
+ });
59
+ describe('error handling', () => {
60
+ it('should handle permission errors', async () => {
61
+ vi.mocked(ensureClaudePlugin).mockRejectedValue({ code: 'EACCES' });
62
+ const cmd = createTestCommand();
63
+ cmd.parse.mockResolvedValue({ flags: {}, args: {} });
64
+ await cmd.run();
65
+ expect(cmd.warn).toHaveBeenCalledWith(expect.stringContaining('Permission denied'));
66
+ expect(cmd.error).toHaveBeenCalled();
67
+ });
68
+ it('should handle general errors with message', async () => {
69
+ vi.mocked(ensureClaudePlugin).mockRejectedValue(new Error('Something went wrong'));
70
+ const cmd = createTestCommand();
71
+ cmd.parse.mockResolvedValue({ flags: {}, args: {} });
72
+ await cmd.run();
73
+ expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Failed to update Claude Code plugin'));
74
+ expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Something went wrong'));
75
+ });
76
+ it('should handle Claude CLI not found error', async () => {
77
+ vi.mocked(ensureClaudePlugin).mockRejectedValue(new Error('Claude CLI not found. Please install Claude Code CLI and ensure \'claude\' is in your PATH.'));
78
+ const cmd = createTestCommand();
79
+ cmd.parse.mockResolvedValue({ flags: {}, args: {} });
80
+ await cmd.run();
81
+ expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Claude CLI not found'));
82
+ });
83
+ });
84
+ });
@@ -5,6 +5,7 @@ import logUpdate from 'log-update';
5
5
  import { validateDockerEnvironment, startDockerCompose, stopDockerCompose, getServiceStatus, DockerComposeConfigNotFoundError, getDefaultDockerComposePath, SERVICE_HEALTH, SERVICE_STATE } from '#services/docker.js';
6
6
  import { getErrorMessage } from '#utils/error_utils.js';
7
7
  import { getDevSuccessMessage } from '#services/messages.js';
8
+ import { ensureClaudePlugin } from '#services/coding_agents.js';
8
9
  const ANSI = {
9
10
  RESET: '\x1b[0m',
10
11
  DIM: '\x1b[2m',
@@ -96,6 +97,8 @@ export default class Dev extends Command {
96
97
  dockerProcess = null;
97
98
  async run() {
98
99
  const { flags } = await this.parse(Dev);
100
+ // Ensure Claude plugin is configured (fire-and-forget, silent)
101
+ ensureClaudePlugin(process.cwd(), { silent: true }).catch(() => { });
99
102
  validateDockerEnvironment();
100
103
  const dockerComposePath = flags['compose-file'] ?
101
104
  path.resolve(process.cwd(), flags['compose-file']) :
@@ -2,7 +2,11 @@
2
2
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
3
  import fs from 'node:fs/promises';
4
4
  import * as dockerService from '#services/docker.js';
5
+ import * as codingAgentsService from '#services/coding_agents.js';
5
6
  import Dev from './index.js';
7
+ vi.mock('#services/coding_agents.js', () => ({
8
+ ensureClaudePlugin: vi.fn().mockResolvedValue(undefined)
9
+ }));
6
10
  vi.mock('#services/docker.js', () => ({
7
11
  validateDockerEnvironment: vi.fn(),
8
12
  startDockerCompose: vi.fn(),
@@ -48,6 +52,8 @@ describe('dev command', () => {
48
52
  vi.mocked(dockerService.startDockerCompose).mockResolvedValue(createMockDockerProcess());
49
53
  // By default, fs.access succeeds (file exists)
50
54
  vi.mocked(fs).access.mockResolvedValue(undefined);
55
+ // By default, ensureClaudePlugin succeeds
56
+ vi.mocked(codingAgentsService.ensureClaudePlugin).mockResolvedValue(undefined);
51
57
  });
52
58
  afterEach(() => {
53
59
  vi.restoreAllMocks();
@@ -128,6 +134,36 @@ describe('dev command', () => {
128
134
  expect(vi.mocked(dockerService.validateDockerEnvironment)).toBeDefined();
129
135
  });
130
136
  });
137
+ describe('Claude plugin update', () => {
138
+ it('should call ensureClaudePlugin on startup', async () => {
139
+ const cmd = new Dev([], {});
140
+ cmd.log = vi.fn();
141
+ cmd.error = vi.fn();
142
+ Object.defineProperty(cmd, 'parse', {
143
+ value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined, 'image-pull-policy': 'always' }, args: {} }),
144
+ configurable: true
145
+ });
146
+ const runPromise = cmd.run();
147
+ await new Promise(resolve => setImmediate(resolve));
148
+ expect(codingAgentsService.ensureClaudePlugin).toHaveBeenCalledWith(process.cwd(), { silent: true });
149
+ runPromise.catch(() => { });
150
+ });
151
+ it('should not block dev if ensureClaudePlugin fails', async () => {
152
+ vi.mocked(codingAgentsService.ensureClaudePlugin).mockRejectedValue(new Error('Plugin update failed'));
153
+ const cmd = new Dev([], {});
154
+ cmd.log = vi.fn();
155
+ cmd.error = vi.fn();
156
+ Object.defineProperty(cmd, 'parse', {
157
+ value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined, 'image-pull-policy': 'always' }, args: {} }),
158
+ configurable: true
159
+ });
160
+ const runPromise = cmd.run();
161
+ await new Promise(resolve => setImmediate(resolve));
162
+ // Docker compose should still be called even if plugin update fails
163
+ expect(dockerService.startDockerCompose).toHaveBeenCalled();
164
+ runPromise.catch(() => { });
165
+ });
166
+ });
131
167
  describe('watch functionality', () => {
132
168
  it('should enable watch by default', async () => {
133
169
  const cmd = new Dev([], {});
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.1.0"
2
+ "framework": "0.1.1"
3
3
  }
@@ -87,7 +87,7 @@ function displaySystemValidationWarnings(validation) {
87
87
  ux.warn(`Missing required claude-code slash command: /${command}`);
88
88
  });
89
89
  ux.warn('Your claude-code agent is missing key configurations, it may not behave as expected.');
90
- ux.warn('Please run "npx output agents init" to fix this.');
90
+ ux.warn('Please run "npx output agents update" to fix this.');
91
91
  }
92
92
  function applyDefaultOptions(options) {
93
93
  return {
@@ -6,31 +6,31 @@ export interface InitOptions {
6
6
  projectRoot: string;
7
7
  force: boolean;
8
8
  }
9
- /**
10
- * Get the full path to the agent configuration directory
11
- */
12
- export declare function getAgentConfigDir(projectRoot: string): string;
13
- /**
14
- * Check if .outputai directory exists
15
- */
16
- export declare function checkAgentConfigDirExists(projectRoot: string): Promise<boolean>;
9
+ interface EnsureClaudePluginOptions {
10
+ silent?: boolean;
11
+ }
17
12
  export declare function checkAgentStructure(projectRoot: string): Promise<StructureCheckResult>;
18
13
  /**
19
14
  * Prepare template variables for file generation
20
15
  */
21
16
  export declare function prepareTemplateVariables(): Record<string, string>;
17
+ /**
18
+ * Ensure Claude Code plugin is configured
19
+ * Registers marketplace, updates it, and installs the plugin
20
+ */
21
+ export declare function ensureClaudePlugin(projectRoot: string, options?: EnsureClaudePluginOptions): Promise<void>;
22
22
  /**
23
23
  * Initialize agent configuration files and register Claude Code plugin
24
- * Creates 3 files:
25
- * - .outputai/AGENTS.md (from template with Handlebars processing)
24
+ * Creates:
26
25
  * - .claude/settings.json (static JSON)
27
- * - CLAUDE.md symlink to .outputai/AGENTS.md
26
+ * - CLAUDE.md (from template - user-customizable file)
28
27
  * Then runs Claude CLI commands to register the plugin marketplace and install the plugin
29
28
  */
30
29
  export declare function initializeAgentConfig(options: InitOptions): Promise<void>;
31
30
  /**
32
- * Ensure OutputAI system is initialized by invoking agents init if needed
31
+ * Ensure OutputAI system is initialized
33
32
  * Creates configuration files and registers Claude Code plugin
34
33
  * @param projectRoot - Root directory of the project
35
34
  */
36
35
  export declare function ensureOutputAISystem(projectRoot: string): Promise<void>;
36
+ export {};
@@ -8,30 +8,15 @@ import path from 'node:path';
8
8
  import { join } from 'node:path';
9
9
  import { ux } from '@oclif/core';
10
10
  import { confirm } from '@inquirer/prompts';
11
- import { config } from '#config.js';
11
+ import debugFactory from 'debug';
12
12
  import { getTemplateDir } from '#utils/paths.js';
13
13
  import { executeClaudeCommand } from '#utils/claude.js';
14
14
  import { processTemplate } from '#utils/template.js';
15
15
  import { ClaudePluginError, UserCancelledError } from '#types/errors.js';
16
+ const debug = debugFactory('output-cli:agent');
16
17
  const EXPECTED_MARKETPLACE_REPO = 'growthxai/output-claude-plugins';
17
- /**
18
- * Get the full path to the agent configuration directory
19
- */
20
- export function getAgentConfigDir(projectRoot) {
21
- return join(projectRoot, config.agentConfigDir);
22
- }
23
- /**
24
- * Check if .outputai directory exists
25
- */
26
- export async function checkAgentConfigDirExists(projectRoot) {
27
- const agentConfigDir = getAgentConfigDir(projectRoot);
28
- try {
29
- await access(agentConfigDir);
30
- return true;
31
- }
32
- catch {
33
- return false;
34
- }
18
+ function createLogger(silent) {
19
+ return silent ? debug : (msg) => ux.stdout(ux.colorize('gray', msg));
35
20
  }
36
21
  async function fileExists(filePath) {
37
22
  try {
@@ -59,7 +44,6 @@ async function validateSettingsJson(projectRoot) {
59
44
  }
60
45
  }
61
46
  export async function checkAgentStructure(projectRoot) {
62
- const outputaiDirExists = await checkAgentConfigDirExists(projectRoot);
63
47
  const settingsValid = await validateSettingsJson(projectRoot);
64
48
  const claudeMdExists = await fileExists(join(projectRoot, 'CLAUDE.md'));
65
49
  if (!settingsValid) {
@@ -68,11 +52,8 @@ export async function checkAgentStructure(projectRoot) {
68
52
  if (!claudeMdExists) {
69
53
  ux.warn('CLAUDE.md missing.');
70
54
  }
71
- if (!outputaiDirExists) {
72
- ux.warn('.outputai/ directory missing.');
73
- }
74
- const isComplete = outputaiDirExists && settingsValid && claudeMdExists;
75
- const needsInit = !outputaiDirExists || !settingsValid;
55
+ const isComplete = settingsValid && claudeMdExists;
56
+ const needsInit = !settingsValid;
76
57
  return { isComplete, needsInit };
77
58
  }
78
59
  /**
@@ -122,51 +103,6 @@ async function createStaticFile(templateSubpath, output) {
122
103
  await fs.writeFile(output, content, 'utf-8');
123
104
  ux.stdout(ux.colorize('gray', `Created file: ${output}`));
124
105
  }
125
- /**
126
- * Create a symlink, falling back to copying if symlinks are not supported
127
- */
128
- async function createSymlink(source, target, projectRoot) {
129
- try {
130
- if (await fileExists(target)) {
131
- await fs.unlink(target);
132
- }
133
- // Resolve source path relative to project root if it's relative
134
- const resolvedSource = path.isAbsolute(source) ?
135
- source :
136
- path.join(projectRoot, source);
137
- // Calculate relative path from target directory to resolved source
138
- const relativePath = path.relative(path.dirname(target), resolvedSource);
139
- await fs.symlink(relativePath, target);
140
- ux.stdout(ux.colorize('gray', `Created symlink: ${target} -> ${source}`));
141
- }
142
- catch (error) {
143
- const code = error.code;
144
- if (code === 'ENOTSUP' || code === 'EPERM') {
145
- ux.stdout(ux.colorize('gray', `Symlinks not supported, creating copy: ${target}`));
146
- const resolvedSource = path.isAbsolute(source) ?
147
- source :
148
- path.join(projectRoot, source);
149
- const content = await fs.readFile(resolvedSource, 'utf-8');
150
- await fs.writeFile(target, content, 'utf-8');
151
- return;
152
- }
153
- throw error;
154
- }
155
- }
156
- /**
157
- * Create .outputai/AGENTS.md file from template
158
- */
159
- async function createAgentsMdFile(projectRoot, force, variables) {
160
- const outputaiDir = join(projectRoot, config.agentConfigDir);
161
- await ensureDirectoryExists(outputaiDir);
162
- const agentsMdPath = join(outputaiDir, 'AGENTS.md');
163
- if (force || !await fileExists(agentsMdPath)) {
164
- await createFromTemplate('dotoutputai/AGENTS.md.template', agentsMdPath, variables);
165
- }
166
- else {
167
- ux.warn(`File already exists: ${config.agentConfigDir}/AGENTS.md (use --force to overwrite)`);
168
- }
169
- }
170
106
  /**
171
107
  * Create .claude/settings.json file from template
172
108
  */
@@ -182,13 +118,12 @@ async function createSettingsFile(projectRoot, force) {
182
118
  }
183
119
  }
184
120
  /**
185
- * Create CLAUDE.md symlink pointing to .outputai/AGENTS.md
186
- * Only checks if CLAUDE.md exists, not AGENTS.md - developers can remove AGENTS.md freely
121
+ * Create CLAUDE.md file from template
187
122
  */
188
- async function createClaudeMdSymlink(projectRoot, force) {
123
+ async function createClaudeMdFile(projectRoot, force, variables) {
189
124
  const claudeMdPath = join(projectRoot, 'CLAUDE.md');
190
125
  if (force || !await fileExists(claudeMdPath)) {
191
- await createSymlink(`${config.agentConfigDir}/AGENTS.md`, claudeMdPath, projectRoot);
126
+ await createFromTemplate('CLAUDE.md.template', claudeMdPath, variables);
192
127
  }
193
128
  else {
194
129
  ux.warn('File already exists: CLAUDE.md (use --force to overwrite)');
@@ -198,10 +133,15 @@ async function createClaudeMdSymlink(projectRoot, force) {
198
133
  * Handle Claude plugin command errors with user confirmation to proceed
199
134
  * @param error - The error that occurred
200
135
  * @param commandName - Name of the command that failed
136
+ * @param silent - If true, log to debug and re-throw without user confirmation
201
137
  * @throws UserCancelledError if user declines to proceed or presses Ctrl+C
202
138
  */
203
- async function handlePluginError(error, commandName) {
139
+ async function handlePluginError(error, commandName, silent = false) {
204
140
  const pluginError = new ClaudePluginError(commandName, error instanceof Error ? error : undefined);
141
+ if (silent) {
142
+ debug('Plugin error: %s', pluginError.message);
143
+ throw error;
144
+ }
205
145
  ux.warn(pluginError.message);
206
146
  try {
207
147
  const shouldProceed = await confirm({
@@ -225,54 +165,62 @@ async function handlePluginError(error, commandName) {
225
165
  /**
226
166
  * Register and update the OutputAI plugin marketplace
227
167
  */
228
- async function registerPluginMarketplace(projectRoot) {
229
- ux.stdout(ux.colorize('gray', 'Registering plugin marketplace...'));
168
+ async function registerPluginMarketplace(projectRoot, silent = false) {
169
+ const log = createLogger(silent);
170
+ log('Registering plugin marketplace...');
230
171
  try {
231
172
  await executeClaudeCommand(['plugin', 'marketplace', 'add', 'growthxai/output-claude-plugins'], projectRoot, { ignoreFailure: true });
232
173
  }
233
174
  catch (error) {
234
- await handlePluginError(error, 'plugin marketplace add');
175
+ await handlePluginError(error, 'plugin marketplace add', silent);
235
176
  return;
236
177
  }
237
- ux.stdout(ux.colorize('gray', 'Updating plugin marketplace...'));
178
+ log('Updating plugin marketplace...');
238
179
  try {
239
180
  await executeClaudeCommand(['plugin', 'marketplace', 'update', 'outputai'], projectRoot);
240
181
  }
241
182
  catch (error) {
242
- await handlePluginError(error, 'plugin marketplace update outputai');
183
+ await handlePluginError(error, 'plugin marketplace update outputai', silent);
243
184
  }
244
185
  }
245
186
  /**
246
187
  * Install the OutputAI plugin
247
188
  */
248
- async function installOutputAIPlugin(projectRoot) {
249
- ux.stdout(ux.colorize('gray', 'Installing OutputAI plugin...'));
189
+ async function installOutputAIPlugin(projectRoot, silent = false) {
190
+ const log = createLogger(silent);
191
+ log('Installing OutputAI plugin...');
250
192
  try {
251
193
  await executeClaudeCommand(['plugin', 'install', 'outputai@outputai', '--scope', 'project'], projectRoot);
252
194
  }
253
195
  catch (error) {
254
- await handlePluginError(error, 'plugin install outputai@outputai');
196
+ await handlePluginError(error, 'plugin install outputai@outputai', silent);
255
197
  }
256
198
  }
199
+ /**
200
+ * Ensure Claude Code plugin is configured
201
+ * Registers marketplace, updates it, and installs the plugin
202
+ */
203
+ export async function ensureClaudePlugin(projectRoot, options = {}) {
204
+ const { silent = false } = options;
205
+ await registerPluginMarketplace(projectRoot, silent);
206
+ await installOutputAIPlugin(projectRoot, silent);
207
+ }
257
208
  /**
258
209
  * Initialize agent configuration files and register Claude Code plugin
259
- * Creates 3 files:
260
- * - .outputai/AGENTS.md (from template with Handlebars processing)
210
+ * Creates:
261
211
  * - .claude/settings.json (static JSON)
262
- * - CLAUDE.md symlink to .outputai/AGENTS.md
212
+ * - CLAUDE.md (from template - user-customizable file)
263
213
  * Then runs Claude CLI commands to register the plugin marketplace and install the plugin
264
214
  */
265
215
  export async function initializeAgentConfig(options) {
266
216
  const { projectRoot, force } = options;
267
217
  const variables = prepareTemplateVariables();
268
- await createAgentsMdFile(projectRoot, force, variables);
269
218
  await createSettingsFile(projectRoot, force);
270
- await createClaudeMdSymlink(projectRoot, force);
271
- await registerPluginMarketplace(projectRoot);
272
- await installOutputAIPlugin(projectRoot);
219
+ await createClaudeMdFile(projectRoot, force, variables);
220
+ await ensureClaudePlugin(projectRoot);
273
221
  }
274
222
  /**
275
- * Ensure OutputAI system is initialized by invoking agents init if needed
223
+ * Ensure OutputAI system is initialized
276
224
  * Creates configuration files and registers Claude Code plugin
277
225
  * @param projectRoot - Root directory of the project
278
226
  */