@output.ai/cli 0.0.4 → 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,62 +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
- type: 'template',
28
- from: 'meta/pre_flight.md.template',
29
- to: '.outputai/meta/pre_flight.md'
30
- },
31
- {
32
- type: 'template',
33
- from: 'meta/post_flight.md.template',
34
- to: '.outputai/meta/post_flight.md'
35
- }
36
- ]
37
- },
38
- 'claude-code': {
39
- id: 'claude-code',
40
- name: 'Claude Code',
41
- mappings: [
42
- {
43
- type: 'symlink',
44
- from: '.outputai/AGENTS.md',
45
- to: 'CLAUDE.md'
46
- },
47
- {
48
- type: 'symlink',
49
- from: '.outputai/agents/workflow_planner.md',
50
- to: '.claude/agents/workflow_planner.md'
51
- },
52
- {
53
- type: 'symlink',
54
- from: '.outputai/commands/plan_workflow.md',
55
- to: '.claude/commands/plan_workflow.md'
56
- }
57
- ]
58
- }
59
- };
2
+ import { AGENT_CONFIG_DIR } from '../../config.js';
3
+ import { initializeAgentConfig, AGENT_CONFIGS } from '../../services/coding_agents.js';
60
4
  export default class Init extends Command {
61
5
  static description = 'Initialize agent configuration files for AI assistant integration';
62
6
  static examples = [
@@ -81,13 +25,15 @@ export default class Init extends Command {
81
25
  const { flags } = await this.parse(Init);
82
26
  this.log('Initializing agent configuration for Claude Code...');
83
27
  try {
84
- const variables = this.prepareTemplateVariables();
85
- await this.processMappings(AGENT_CONFIGS.outputai, variables, flags.force);
86
- 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
+ });
87
33
  this.log('✅ Agent configuration initialized successfully!');
88
34
  this.log('');
89
35
  this.log('Created:');
90
- this.log('.outputai/ directory with agent, command, and meta configurations');
36
+ this.log(`${AGENT_CONFIG_DIR}/ directory with agent and command configurations`);
91
37
  this.log(' • .claude/ directory with symlinks for Claude Code integration');
92
38
  this.log('');
93
39
  this.log('Claude Code will automatically detect and use these configurations.');
@@ -99,87 +45,4 @@ export default class Init extends Command {
99
45
  this.error(`Failed to initialize agent configuration: ${error.message}`);
100
46
  }
101
47
  }
102
- prepareTemplateVariables() {
103
- return {
104
- date: new Date().toLocaleDateString('en-US', {
105
- year: 'numeric',
106
- month: 'long',
107
- day: 'numeric'
108
- })
109
- };
110
- }
111
- async processMappings(config, variables, force) {
112
- for (const mapping of config.mappings) {
113
- const dir = path.dirname(mapping.to);
114
- await this.ensureDirectoryExists(dir);
115
- if (!force && await this.fileExists(mapping.to)) {
116
- this.warn(`File already exists: ${mapping.to} (use --force to overwrite)`);
117
- continue;
118
- }
119
- switch (mapping.type) {
120
- case 'template':
121
- await this.createFromTemplate(mapping.from, mapping.to, variables);
122
- break;
123
- case 'symlink':
124
- await this.createSymlink(mapping.from, mapping.to);
125
- break;
126
- case 'copy':
127
- await this.copyFile(mapping.from, mapping.to);
128
- break;
129
- }
130
- }
131
- }
132
- async ensureDirectoryExists(dir) {
133
- try {
134
- await fs.mkdir(dir, { recursive: true });
135
- this.debug(`Created directory: ${dir}`);
136
- }
137
- catch (error) {
138
- if (error.code !== 'EEXIST') {
139
- throw error;
140
- }
141
- }
142
- }
143
- async fileExists(filePath) {
144
- try {
145
- await fs.stat(filePath);
146
- return true;
147
- }
148
- catch {
149
- return false;
150
- }
151
- }
152
- async createFromTemplate(template, output, variables) {
153
- const templateDir = getTemplateDir('agent_instructions');
154
- const templatePath = path.join(templateDir, template);
155
- const content = await fs.readFile(templatePath, 'utf-8');
156
- const processed = processTemplate(content, variables);
157
- await fs.writeFile(output, processed, 'utf-8');
158
- this.debug(`Created from template: ${output}`);
159
- }
160
- async createSymlink(source, target) {
161
- try {
162
- if (await this.fileExists(target)) {
163
- await fs.unlink(target);
164
- }
165
- const relativePath = path.relative(path.dirname(target), source);
166
- await fs.symlink(relativePath, target);
167
- this.debug(`Created symlink: ${target} -> ${source}`);
168
- }
169
- catch (error) {
170
- const code = error.code;
171
- if (code === 'ENOTSUP' || code === 'EPERM') {
172
- this.debug(`Symlinks not supported, creating copy: ${target}`);
173
- const content = await fs.readFile(source, 'utf-8');
174
- await fs.writeFile(target, content, 'utf-8');
175
- return;
176
- }
177
- throw error;
178
- }
179
- }
180
- async copyFile(source, target) {
181
- const content = await fs.readFile(source, 'utf-8');
182
- await fs.writeFile(target, content, 'utf-8');
183
- this.debug(`Copied file: ${source} -> ${target}`);
184
- }
185
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);
@@ -108,7 +109,7 @@ describe('agents init', () => {
108
109
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
109
110
  await cmd.run();
110
111
  expect(mockReadFile).toHaveBeenCalledWith(expect.stringContaining('AGENTS.md.template'), 'utf-8');
111
- 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');
112
113
  });
113
114
  it('should create all agent configuration files', async () => {
114
115
  const mockWriteFile = vi.mocked(fs.writeFile);
@@ -119,7 +120,7 @@ describe('agents init', () => {
119
120
  '.outputai/agents/workflow_planner.md'
120
121
  ];
121
122
  for (const file of agentFiles) {
122
- expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
123
+ expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
123
124
  }
124
125
  });
125
126
  it('should create all command configuration files', async () => {
@@ -131,7 +132,7 @@ describe('agents init', () => {
131
132
  '.outputai/commands/plan_workflow.md'
132
133
  ];
133
134
  for (const file of commandFiles) {
134
- expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
135
+ expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
135
136
  }
136
137
  });
137
138
  it('should create all meta configuration files', async () => {
@@ -144,50 +145,52 @@ describe('agents init', () => {
144
145
  '.outputai/meta/post_flight.md'
145
146
  ];
146
147
  for (const file of metaFiles) {
147
- expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
148
+ expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
148
149
  }
149
150
  });
150
151
  });
151
152
  describe('symlink creation', () => {
152
153
  it('should create symlink from CLAUDE.md to .outputai/AGENTS.md', async () => {
153
154
  const mockSymlink = vi.mocked(fs.symlink);
154
- const mockStat = vi.mocked(fs.stat);
155
- mockStat.mockImplementation(async (path) => {
156
- if (typeof path === 'string' && path.startsWith('.outputai')) {
157
- return { isFile: () => true };
158
- }
159
- throw { code: 'ENOENT' };
160
- });
161
155
  const cmd = createTestCommand();
162
156
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
163
157
  await cmd.run();
164
- 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'));
165
160
  });
166
161
  it('should create symlinks for all agent files', async () => {
167
162
  const mockSymlink = vi.mocked(fs.symlink);
168
163
  const cmd = createTestCommand();
169
164
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
170
165
  await cmd.run();
171
- 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'));
172
167
  });
173
168
  it('should create symlinks for all command files', async () => {
174
169
  const mockSymlink = vi.mocked(fs.symlink);
175
170
  const cmd = createTestCommand();
176
171
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
177
172
  await cmd.run();
178
- 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'));
179
174
  });
180
175
  it('should handle Windows by copying files when symlinks are not supported', async () => {
181
176
  const mockSymlink = vi.mocked(fs.symlink);
182
177
  const mockWriteFile = vi.mocked(fs.writeFile);
183
178
  const mockReadFile = vi.mocked(fs.readFile);
184
179
  mockSymlink.mockRejectedValue({ code: 'ENOTSUP' });
180
+ // readFile is called for both templates AND fallback copy
185
181
  mockReadFile.mockResolvedValue('File content');
186
182
  const cmd = createTestCommand();
187
183
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
188
184
  await cmd.run();
189
- expect(mockReadFile).toHaveBeenCalledWith('.outputai/AGENTS.md', 'utf-8');
190
- 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);
191
194
  });
192
195
  });
193
196
  describe('error handling', () => {
@@ -247,12 +250,12 @@ describe('agents init', () => {
247
250
  '.outputai/meta/post_flight.md'
248
251
  ];
249
252
  for (const file of expectedFiles) {
250
- expect(mockWriteFile).toHaveBeenCalledWith(file, expect.any(String), 'utf-8');
253
+ expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(file), expect.any(String), 'utf-8');
251
254
  }
252
255
  // Verify symlinks are created
253
- expect(mockSymlink).toHaveBeenCalledWith('.outputai/AGENTS.md', 'CLAUDE.md');
254
- expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/agents/workflow_planner.md'), '.claude/agents/workflow_planner.md');
255
- expect(mockSymlink).toHaveBeenCalledWith(expect.stringContaining('.outputai/commands/plan_workflow.md'), '.claude/commands/plan_workflow.md');
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'));
256
259
  });
257
260
  });
258
261
  describe('force flag', () => {
@@ -267,13 +270,15 @@ describe('agents init', () => {
267
270
  expect(mockWriteFile).toHaveBeenCalled();
268
271
  expect(cmd.warn).not.toHaveBeenCalled();
269
272
  });
270
- it('should warn about existing files without force flag', async () => {
271
- const mockStat = vi.mocked(fs.stat);
272
- 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);
273
276
  const cmd = createTestCommand();
274
277
  cmd.parse.mockResolvedValue({ flags: { 'agent-provider': 'claude-code', force: false }, args: {} });
275
278
  await cmd.run();
276
- 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'));
277
282
  });
278
283
  });
279
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 {};