@output.ai/cli 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents/{init.d.ts → update.d.ts} +2 -4
- package/dist/commands/agents/update.js +29 -0
- package/dist/commands/agents/update.spec.js +84 -0
- package/dist/commands/dev/index.js +3 -0
- package/dist/commands/dev/index.spec.js +36 -0
- package/dist/generated/framework_version.json +3 -0
- package/dist/services/claude_client.js +1 -1
- package/dist/services/coding_agents.d.ts +12 -12
- package/dist/services/coding_agents.js +39 -91
- package/dist/services/coding_agents.spec.js +43 -59
- package/dist/services/project_scaffold.js +4 -7
- package/dist/services/workflow_planner.d.ts +1 -1
- package/dist/services/workflow_planner.js +1 -1
- package/dist/services/workflow_planner.spec.js +2 -2
- package/dist/templates/agent_instructions/CLAUDE.md.template +19 -0
- package/dist/templates/project/package.json.template +1 -4
- package/dist/test_helpers/mocks.d.ts +1 -1
- package/dist/test_helpers/mocks.js +1 -1
- package/dist/utils/framework_version.d.ts +4 -0
- package/dist/utils/framework_version.js +4 -0
- package/dist/utils/framework_version.spec.js +13 -0
- package/dist/utils/process.js +4 -2
- package/package.json +1 -1
- package/dist/commands/agents/init.js +0 -43
- package/dist/commands/agents/init.spec.js +0 -109
- package/dist/generated/sdk_versions.json +0 -6
- package/dist/templates/agent_instructions/dotoutputai/AGENTS.md.template +0 -435
- package/dist/utils/sdk_versions.d.ts +0 -7
- package/dist/utils/sdk_versions.js +0 -4
- package/dist/utils/sdk_versions.spec.js +0 -19
- /package/dist/commands/agents/{init.spec.d.ts → update.spec.d.ts} +0 -0
- /package/dist/utils/{sdk_versions.spec.d.ts → framework_version.spec.d.ts} +0 -0
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
export default class
|
|
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([], {});
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
25
|
-
* - .outputai/AGENTS.md (from template with Handlebars processing)
|
|
24
|
+
* Creates:
|
|
26
25
|
* - .claude/settings.json (static JSON)
|
|
27
|
-
* - CLAUDE.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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
123
|
+
async function createClaudeMdFile(projectRoot, force, variables) {
|
|
189
124
|
const claudeMdPath = join(projectRoot, 'CLAUDE.md');
|
|
190
125
|
if (force || !await fileExists(claudeMdPath)) {
|
|
191
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
260
|
-
* - .outputai/AGENTS.md (from template with Handlebars processing)
|
|
210
|
+
* Creates:
|
|
261
211
|
* - .claude/settings.json (static JSON)
|
|
262
|
-
* - CLAUDE.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
|
|
271
|
-
await
|
|
272
|
-
await installOutputAIPlugin(projectRoot);
|
|
219
|
+
await createClaudeMdFile(projectRoot, force, variables);
|
|
220
|
+
await ensureClaudePlugin(projectRoot);
|
|
273
221
|
}
|
|
274
222
|
/**
|
|
275
|
-
* Ensure OutputAI system is initialized
|
|
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
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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');
|