@output.ai/cli 0.0.3 → 0.0.5

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.
@@ -8,11 +8,4 @@ export default class Init extends Command {
8
8
  force: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
9
  };
10
10
  run(): Promise<void>;
11
- private prepareTemplateVariables;
12
- private processMappings;
13
- private ensureDirectoryExists;
14
- private fileExists;
15
- private createFromTemplate;
16
- private createSymlink;
17
- private copyFile;
18
11
  }
@@ -1,52 +1,6 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { getTemplateDir } from '../../utils/paths.js';
5
- import { processTemplate } from '../../utils/template.js';
6
- const AGENT_CONFIGS = {
7
- outputai: {
8
- id: 'outputai',
9
- name: 'OutputAI Core Files',
10
- mappings: [
11
- {
12
- type: 'template',
13
- from: 'AGENTS.md.template',
14
- to: '.outputai/AGENTS.md'
15
- },
16
- {
17
- type: 'template',
18
- from: 'agents/workflow_planner.md.template',
19
- to: '.outputai/agents/workflow_planner.md'
20
- },
21
- {
22
- type: 'template',
23
- from: 'commands/plan_workflow.md.template',
24
- to: '.outputai/commands/plan_workflow.md'
25
- }
26
- ]
27
- },
28
- 'claude-code': {
29
- id: 'claude-code',
30
- name: 'Claude Code',
31
- mappings: [
32
- {
33
- type: 'symlink',
34
- from: '.outputai/AGENTS.md',
35
- to: 'CLAUDE.md'
36
- },
37
- {
38
- type: 'symlink',
39
- from: '.outputai/agents/workflow_planner.md',
40
- to: '.claude/agents/workflow_planner.md'
41
- },
42
- {
43
- type: 'symlink',
44
- from: '.outputai/commands/plan_workflow.md',
45
- to: '.claude/commands/plan_workflow.md'
46
- }
47
- ]
48
- }
49
- };
2
+ import { AGENT_CONFIG_DIR } from '../../config.js';
3
+ import { initializeAgentConfig, AGENT_CONFIGS } from '../../services/coding_agents.js';
50
4
  export default class Init extends Command {
51
5
  static description = 'Initialize agent configuration files for AI assistant integration';
52
6
  static examples = [
@@ -71,13 +25,15 @@ export default class Init extends Command {
71
25
  const { flags } = await this.parse(Init);
72
26
  this.log('Initializing agent configuration for Claude Code...');
73
27
  try {
74
- const variables = this.prepareTemplateVariables();
75
- await this.processMappings(AGENT_CONFIGS.outputai, variables, flags.force);
76
- await this.processMappings(AGENT_CONFIGS[flags['agent-provider']], variables, flags.force);
28
+ await initializeAgentConfig({
29
+ projectRoot: process.cwd(),
30
+ force: flags.force,
31
+ agentProvider: flags['agent-provider']
32
+ });
77
33
  this.log('✅ Agent configuration initialized successfully!');
78
34
  this.log('');
79
35
  this.log('Created:');
80
- this.log('.outputai/ directory with agent and command configurations');
36
+ this.log(`${AGENT_CONFIG_DIR}/ directory with agent and command configurations`);
81
37
  this.log(' • .claude/ directory with symlinks for Claude Code integration');
82
38
  this.log('');
83
39
  this.log('Claude Code will automatically detect and use these configurations.');
@@ -89,87 +45,4 @@ export default class Init extends Command {
89
45
  this.error(`Failed to initialize agent configuration: ${error.message}`);
90
46
  }
91
47
  }
92
- prepareTemplateVariables() {
93
- return {
94
- date: new Date().toLocaleDateString('en-US', {
95
- year: 'numeric',
96
- month: 'long',
97
- day: 'numeric'
98
- })
99
- };
100
- }
101
- async processMappings(config, variables, force) {
102
- for (const mapping of config.mappings) {
103
- const dir = path.dirname(mapping.to);
104
- await this.ensureDirectoryExists(dir);
105
- if (!force && await this.fileExists(mapping.to)) {
106
- this.warn(`File already exists: ${mapping.to} (use --force to overwrite)`);
107
- continue;
108
- }
109
- switch (mapping.type) {
110
- case 'template':
111
- await this.createFromTemplate(mapping.from, mapping.to, variables);
112
- break;
113
- case 'symlink':
114
- await this.createSymlink(mapping.from, mapping.to);
115
- break;
116
- case 'copy':
117
- await this.copyFile(mapping.from, mapping.to);
118
- break;
119
- }
120
- }
121
- }
122
- async ensureDirectoryExists(dir) {
123
- try {
124
- await fs.mkdir(dir, { recursive: true });
125
- this.debug(`Created directory: ${dir}`);
126
- }
127
- catch (error) {
128
- if (error.code !== 'EEXIST') {
129
- throw error;
130
- }
131
- }
132
- }
133
- async fileExists(filePath) {
134
- try {
135
- await fs.stat(filePath);
136
- return true;
137
- }
138
- catch {
139
- return false;
140
- }
141
- }
142
- async createFromTemplate(template, output, variables) {
143
- const templateDir = getTemplateDir('agent_instructions');
144
- const templatePath = path.join(templateDir, template);
145
- const content = await fs.readFile(templatePath, 'utf-8');
146
- const processed = processTemplate(content, variables);
147
- await fs.writeFile(output, processed, 'utf-8');
148
- this.debug(`Created from template: ${output}`);
149
- }
150
- async createSymlink(source, target) {
151
- try {
152
- if (await this.fileExists(target)) {
153
- await fs.unlink(target);
154
- }
155
- const relativePath = path.relative(path.dirname(target), source);
156
- await fs.symlink(relativePath, target);
157
- this.debug(`Created symlink: ${target} -> ${source}`);
158
- }
159
- catch (error) {
160
- const code = error.code;
161
- if (code === 'ENOTSUP' || code === 'EPERM') {
162
- this.debug(`Symlinks not supported, creating copy: ${target}`);
163
- const content = await fs.readFile(source, 'utf-8');
164
- await fs.writeFile(target, content, 'utf-8');
165
- return;
166
- }
167
- throw error;
168
- }
169
- }
170
- async copyFile(source, target) {
171
- const content = await fs.readFile(source, 'utf-8');
172
- await fs.writeFile(target, content, 'utf-8');
173
- this.debug(`Copied file: ${source} -> ${target}`);
174
- }
175
48
  }
@@ -46,6 +46,7 @@ describe('agents init', () => {
46
46
  vi.mocked(fs.writeFile).mockResolvedValue(undefined);
47
47
  vi.mocked(fs.symlink).mockResolvedValue(undefined);
48
48
  vi.mocked(fs.stat).mockRejectedValue({ code: 'ENOENT' });
49
+ vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' }); // Files don't exist by default
49
50
  vi.mocked(fs.unlink).mockResolvedValue(undefined);
50
51
  vi.mocked(fs.readFile).mockResolvedValue('Template content with {{date}}');
51
52
  mockGetTemplateDir = vi.mocked(getTemplateDir);
@@ -86,6 +87,7 @@ describe('agents init', () => {
86
87
  expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai'), expect.objectContaining({ recursive: true }));
87
88
  expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents'), expect.objectContaining({ recursive: true }));
88
89
  expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands'), expect.objectContaining({ recursive: true }));
90
+ expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('.outputai/meta'), expect.objectContaining({ recursive: true }));
89
91
  });
90
92
  it('should create .claude directory structure', async () => {
91
93
  const mockMkdir = vi.mocked(fs.mkdir);
@@ -107,7 +109,7 @@ describe('agents init', () => {
107
109
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
108
110
  await cmd.run();
109
111
  expect(mockReadFile).toHaveBeenCalledWith(expect.stringContaining('AGENTS.md.template'), 'utf-8');
110
- expect(mockWriteFile).toHaveBeenCalledWith('.outputai/AGENTS.md', expect.stringContaining('Agent instructions'), 'utf-8');
112
+ expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining('.outputai/AGENTS.md'), expect.stringContaining('Agent instructions'), 'utf-8');
111
113
  });
112
114
  it('should create all agent configuration files', async () => {
113
115
  const mockWriteFile = vi.mocked(fs.writeFile);
@@ -118,7 +120,7 @@ describe('agents init', () => {
118
120
  '.outputai/agents/workflow_planner.md'
119
121
  ];
120
122
  for (const file of agentFiles) {
121
- expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
123
+ expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
122
124
  }
123
125
  });
124
126
  it('should create all command configuration files', async () => {
@@ -130,50 +132,65 @@ describe('agents init', () => {
130
132
  '.outputai/commands/plan_workflow.md'
131
133
  ];
132
134
  for (const file of commandFiles) {
133
- expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
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');
134
149
  }
135
150
  });
136
151
  });
137
152
  describe('symlink creation', () => {
138
153
  it('should create symlink from CLAUDE.md to .outputai/AGENTS.md', async () => {
139
154
  const mockSymlink = vi.mocked(fs.symlink);
140
- const mockStat = vi.mocked(fs.stat);
141
- mockStat.mockImplementation(async (path) => {
142
- if (typeof path === 'string' && path.startsWith('.outputai')) {
143
- return { isFile: () => true };
144
- }
145
- throw { code: 'ENOENT' };
146
- });
147
155
  const cmd = createTestCommand();
148
156
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
149
157
  await cmd.run();
150
- expect(mockSymlink).toHaveBeenCalledWith('.outputai/AGENTS.md', 'CLAUDE.md');
158
+ // Symlink creates a relative link from target to source
159
+ expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/AGENTS.md'), expect.stringContaining('CLAUDE.md'));
151
160
  });
152
161
  it('should create symlinks for all agent files', async () => {
153
162
  const mockSymlink = vi.mocked(fs.symlink);
154
163
  const cmd = createTestCommand();
155
164
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
156
165
  await cmd.run();
157
- expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents/workflow_planner.md'), '.claude/agents/workflow_planner.md');
166
+ expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents/workflow_planner.md'), expect.stringContaining('.claude/agents/workflow_planner.md'));
158
167
  });
159
168
  it('should create symlinks for all command files', async () => {
160
169
  const mockSymlink = vi.mocked(fs.symlink);
161
170
  const cmd = createTestCommand();
162
171
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
163
172
  await cmd.run();
164
- expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands/plan_workflow.md'), '.claude/commands/plan_workflow.md');
173
+ expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands/plan_workflow.md'), expect.stringContaining('.claude/commands/plan_workflow.md'));
165
174
  });
166
175
  it('should handle Windows by copying files when symlinks are not supported', async () => {
167
176
  const mockSymlink = vi.mocked(fs.symlink);
168
177
  const mockWriteFile = vi.mocked(fs.writeFile);
169
178
  const mockReadFile = vi.mocked(fs.readFile);
170
179
  mockSymlink.mockRejectedValue({ code: 'ENOTSUP' });
180
+ // readFile is called for both templates AND fallback copy
171
181
  mockReadFile.mockResolvedValue('File content');
172
182
  const cmd = createTestCommand();
173
183
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
174
184
  await cmd.run();
175
- expect(mockReadFile).toHaveBeenCalledWith('.outputai/AGENTS.md', 'utf-8');
176
- expect(mockWriteFile).toHaveBeenCalledWith('CLAUDE.md', 'File content', 'utf-8');
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);
177
194
  });
178
195
  });
179
196
  describe('error handling', () => {
@@ -203,6 +220,44 @@ describe('agents init', () => {
203
220
  expect(cmd.error).not.toHaveBeenCalled();
204
221
  });
205
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);
228
+ 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
+ });
206
261
  describe('force flag', () => {
207
262
  it('should overwrite existing files when force flag is set', async () => {
208
263
  const mockWriteFile = vi.mocked(fs.writeFile);
@@ -215,13 +270,15 @@ describe('agents init', () => {
215
270
  expect(mockWriteFile).toHaveBeenCalled();
216
271
  expect(cmd.warn).not.toHaveBeenCalled();
217
272
  });
218
- it('should warn about existing files without force flag', async () => {
219
- const mockStat = vi.mocked(fs.stat);
220
- mockStat.mockResolvedValue({ isFile: () => true });
273
+ it('should skip existing files without force flag', async () => {
274
+ const mockAccess = vi.mocked(fs.access);
275
+ mockAccess.mockResolvedValue(undefined);
221
276
  const cmd = createTestCommand();
222
277
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
223
278
  await cmd.run();
224
- expect(cmd.warn).toHaveBeenCalledWith(expect.stringContaining('already exists'));
279
+ // Files exist, so they should be skipped
280
+ // The command should complete without errors
281
+ expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('initialized successfully'));
225
282
  });
226
283
  });
227
284
  });
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class WorkflowPlan extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ 'force-agent-file-write': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
7
+ description: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
+ };
9
+ run(): Promise<void>;
10
+ private planModificationLoop;
11
+ private planGenerationLoop;
12
+ }
@@ -0,0 +1,65 @@
1
+ import { Command, Flags, ux } from '@oclif/core';
2
+ import { input } from '@inquirer/prompts';
3
+ import { ensureOutputAIStructure, generatePlanName, updateAgentTemplates, writePlanFile } from '../../services/workflow_planner.js';
4
+ import { invokePlanWorkflow, replyToClaude } from '../../services/claude_client.js';
5
+ export default class WorkflowPlan extends Command {
6
+ static description = 'Generate a workflow plan from a description';
7
+ static examples = [
8
+ '<%= config.bin %> <%= command.id %>',
9
+ '<%= config.bin %> <%= command.id %> --description "A workflow to take a question and answer it"',
10
+ '<%= config.bin %> <%= command.id %> --force-agent-file-write'
11
+ ];
12
+ static flags = {
13
+ 'force-agent-file-write': Flags.boolean({
14
+ description: 'Force overwrite of agent template files',
15
+ default: false
16
+ }),
17
+ description: Flags.string({
18
+ char: 'd',
19
+ description: 'Workflow description',
20
+ required: false
21
+ })
22
+ };
23
+ async run() {
24
+ const { flags } = await this.parse(WorkflowPlan);
25
+ const projectRoot = process.cwd();
26
+ this.log('Checking .outputai directory structure...');
27
+ await ensureOutputAIStructure(projectRoot);
28
+ if (flags['force-agent-file-write']) {
29
+ this.log('Updating agent templates...');
30
+ await updateAgentTemplates(projectRoot);
31
+ this.log('Templates updated successfully\n');
32
+ }
33
+ const description = flags.description ?? await input({
34
+ message: 'Describe the workflow you want to create:',
35
+ validate: (value) => value.length >= 10
36
+ });
37
+ this.log('\nGenerating plan name...');
38
+ const planName = await generatePlanName(description);
39
+ this.log(`Plan name: ${planName}`);
40
+ await this.planGenerationLoop(description, planName, projectRoot);
41
+ }
42
+ async planModificationLoop(originalPlanContent) {
43
+ const acceptKey = 'ACCEPT';
44
+ this.log('=========');
45
+ this.log(originalPlanContent);
46
+ this.log('=========');
47
+ const modifications = await input({
48
+ message: ux.colorize('gray', `Reply or type ${acceptKey} to accept the plan as is: `),
49
+ validate: (value) => value.length >= 10 || value === acceptKey
50
+ });
51
+ if (modifications === acceptKey) {
52
+ return originalPlanContent;
53
+ }
54
+ const modifiedPlanContent = await replyToClaude(modifications);
55
+ return this.planModificationLoop(modifiedPlanContent);
56
+ }
57
+ async planGenerationLoop(promptDescription, planName, projectRoot) {
58
+ this.log('\nInvoking the /plan_workflow command...');
59
+ this.log('This may take a moment...\n');
60
+ const planContent = await invokePlanWorkflow(promptDescription);
61
+ const modifiedPlanContent = await this.planModificationLoop(planContent);
62
+ const modifiedSavedPath = await writePlanFile(planName, modifiedPlanContent, projectRoot);
63
+ this.log(`✅ Plan saved to: ${modifiedSavedPath}\n`);
64
+ }
65
+ }
@@ -0,0 +1 @@
1
+ export {};