@output.ai/cli 0.5.6 → 0.7.0

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.
Files changed (57) hide show
  1. package/README.md +1 -0
  2. package/dist/api/generated/api.d.ts +25 -0
  3. package/dist/api/generated/api.js +11 -0
  4. package/dist/commands/agents/init.d.ts +0 -1
  5. package/dist/commands/agents/init.js +9 -16
  6. package/dist/commands/agents/init.spec.js +49 -224
  7. package/dist/commands/workflow/generate.js +2 -2
  8. package/dist/commands/workflow/plan.js +3 -3
  9. package/dist/commands/workflow/plan.spec.js +13 -13
  10. package/dist/commands/workflow/terminate.d.ts +13 -0
  11. package/dist/commands/workflow/terminate.js +39 -0
  12. package/dist/services/claude_client.d.ts +4 -4
  13. package/dist/services/claude_client.integration.test.js +2 -2
  14. package/dist/services/claude_client.js +7 -7
  15. package/dist/services/claude_client.spec.js +3 -3
  16. package/dist/services/coding_agents.d.ts +10 -24
  17. package/dist/services/coding_agents.js +112 -368
  18. package/dist/services/coding_agents.spec.js +101 -290
  19. package/dist/services/project_scaffold.js +3 -3
  20. package/dist/services/workflow_builder.d.ts +1 -1
  21. package/dist/services/workflow_builder.js +1 -1
  22. package/dist/services/workflow_planner.js +5 -3
  23. package/dist/services/workflow_planner.spec.js +4 -5
  24. package/dist/templates/agent_instructions/dotclaude/settings.json.template +29 -0
  25. package/dist/templates/agent_instructions/{AGENTS.md.template → dotoutputai/AGENTS.md.template} +12 -10
  26. package/dist/utils/claude.d.ts +5 -0
  27. package/dist/utils/claude.js +19 -0
  28. package/dist/utils/claude.spec.d.ts +1 -0
  29. package/dist/utils/claude.spec.js +119 -0
  30. package/dist/utils/paths.d.ts +0 -4
  31. package/dist/utils/paths.js +0 -6
  32. package/package.json +3 -3
  33. package/dist/templates/agent_instructions/agents/workflow_context_fetcher.md.template +0 -82
  34. package/dist/templates/agent_instructions/agents/workflow_debugger.md.template +0 -98
  35. package/dist/templates/agent_instructions/agents/workflow_planner.md.template +0 -113
  36. package/dist/templates/agent_instructions/agents/workflow_prompt_writer.md.template +0 -595
  37. package/dist/templates/agent_instructions/agents/workflow_quality.md.template +0 -244
  38. package/dist/templates/agent_instructions/commands/build_workflow.md.template +0 -290
  39. package/dist/templates/agent_instructions/commands/debug_workflow.md.template +0 -198
  40. package/dist/templates/agent_instructions/commands/plan_workflow.md.template +0 -261
  41. package/dist/templates/agent_instructions/meta/post_flight.md.template +0 -94
  42. package/dist/templates/agent_instructions/meta/pre_flight.md.template +0 -60
  43. package/dist/templates/agent_instructions/skills/output-error-direct-io/SKILL.md.template +0 -249
  44. package/dist/templates/agent_instructions/skills/output-error-http-client/SKILL.md.template +0 -298
  45. package/dist/templates/agent_instructions/skills/output-error-missing-schemas/SKILL.md.template +0 -265
  46. package/dist/templates/agent_instructions/skills/output-error-nondeterminism/SKILL.md.template +0 -252
  47. package/dist/templates/agent_instructions/skills/output-error-try-catch/SKILL.md.template +0 -226
  48. package/dist/templates/agent_instructions/skills/output-error-zod-import/SKILL.md.template +0 -209
  49. package/dist/templates/agent_instructions/skills/output-services-check/SKILL.md.template +0 -128
  50. package/dist/templates/agent_instructions/skills/output-workflow-list/SKILL.md.template +0 -117
  51. package/dist/templates/agent_instructions/skills/output-workflow-result/SKILL.md.template +0 -199
  52. package/dist/templates/agent_instructions/skills/output-workflow-run/SKILL.md.template +0 -228
  53. package/dist/templates/agent_instructions/skills/output-workflow-runs-list/SKILL.md.template +0 -141
  54. package/dist/templates/agent_instructions/skills/output-workflow-start/SKILL.md.template +0 -201
  55. package/dist/templates/agent_instructions/skills/output-workflow-status/SKILL.md.template +0 -151
  56. package/dist/templates/agent_instructions/skills/output-workflow-stop/SKILL.md.template +0 -164
  57. package/dist/templates/agent_instructions/skills/output-workflow-trace/SKILL.md.template +0 -134
@@ -1,8 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { getRequiredFiles, checkAgentConfigDirExists, checkAgentStructure, getAgentConfigDir, prepareTemplateVariables, initializeAgentConfig, ensureOutputAIStructure, AGENT_CONFIGS } from './coding_agents.js';
2
+ import { checkAgentConfigDirExists, checkAgentStructure, getAgentConfigDir, prepareTemplateVariables, initializeAgentConfig, ensureOutputAISystem } from './coding_agents.js';
3
3
  import { access } from 'node:fs/promises';
4
4
  import fs from 'node:fs/promises';
5
- import { getErrorMessage } from '#utils/error_utils.js';
6
5
  vi.mock('node:fs/promises');
7
6
  vi.mock('../utils/paths.js', () => ({
8
7
  getTemplateDir: vi.fn().mockReturnValue('/templates')
@@ -10,8 +9,8 @@ vi.mock('../utils/paths.js', () => ({
10
9
  vi.mock('../utils/template.js', () => ({
11
10
  processTemplate: vi.fn().mockImplementation((content) => content)
12
11
  }));
13
- vi.mock('@inquirer/prompts', () => ({
14
- confirm: vi.fn()
12
+ vi.mock('../utils/claude.js', () => ({
13
+ executeClaudeCommand: vi.fn().mockResolvedValue(undefined)
15
14
  }));
16
15
  vi.mock('@oclif/core', () => ({
17
16
  ux: {
@@ -24,37 +23,6 @@ describe('coding_agents service', () => {
24
23
  beforeEach(() => {
25
24
  vi.clearAllMocks();
26
25
  });
27
- describe('getRequiredFiles', () => {
28
- it('should derive required files from both AGENT_CONFIGS', () => {
29
- const files = getRequiredFiles();
30
- expect(files).toContain('.outputai/AGENTS.md');
31
- expect(files).toContain('.outputai/agents/workflow_planner.md');
32
- expect(files).toContain('.outputai/commands/plan_workflow.md');
33
- expect(files).toContain('CLAUDE.md');
34
- expect(files).toContain('.claude/agents/workflow_planner.md');
35
- expect(files).toContain('.claude/commands/plan_workflow.md');
36
- // Spot check skill files (all at top level - no nesting)
37
- expect(files).toContain('.outputai/skills/output-services-check/SKILL.md');
38
- expect(files).toContain('.outputai/skills/output-workflow-list/SKILL.md');
39
- expect(files).toContain('.outputai/skills/output-error-zod-import/SKILL.md');
40
- expect(files).toContain('.claude/skills/output-workflow-trace/SKILL.md');
41
- expect(files).toContain('.claude/skills/output-workflow-run/SKILL.md');
42
- expect(files).toContain('.claude/skills/output-error-nondeterminism/SKILL.md');
43
- });
44
- it('should include both outputai and claude-code files', () => {
45
- const files = getRequiredFiles();
46
- const expectedCount = AGENT_CONFIGS.outputai.mappings.length +
47
- AGENT_CONFIGS['claude-code'].mappings.length;
48
- expect(files.length).toBe(expectedCount);
49
- });
50
- it('should have outputai files with .outputai prefix', () => {
51
- const files = getRequiredFiles();
52
- const outputaiFiles = files.slice(0, 26);
53
- outputaiFiles.forEach(file => {
54
- expect(file).toMatch(/^\.outputai\//);
55
- });
56
- });
57
- });
58
26
  describe('getAgentConfigDir', () => {
59
27
  it('should return the correct path to .outputai directory', () => {
60
28
  const result = getAgentConfigDir('/test/project');
@@ -75,128 +43,85 @@ describe('coding_agents service', () => {
75
43
  });
76
44
  });
77
45
  describe('checkAgentStructure', () => {
78
- it('should return all files missing when directory does not exist', async () => {
46
+ it('should return needsInit true when directory does not exist', async () => {
79
47
  vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
80
48
  const result = await checkAgentStructure('/test/project');
81
49
  expect(result).toEqual({
82
- dirExists: false,
83
- missingFiles: [
84
- '.outputai/AGENTS.md',
85
- '.outputai/agents/workflow_planner.md',
86
- '.outputai/agents/workflow_quality.md',
87
- '.outputai/agents/workflow_context_fetcher.md',
88
- '.outputai/agents/workflow_prompt_writer.md',
89
- '.outputai/agents/workflow_debugger.md',
90
- '.outputai/commands/plan_workflow.md',
91
- '.outputai/commands/debug_workflow.md',
92
- '.outputai/commands/build_workflow.md',
93
- '.outputai/meta/pre_flight.md',
94
- '.outputai/meta/post_flight.md',
95
- // Skills (all at top level - no nesting allowed)
96
- '.outputai/skills/output-services-check/SKILL.md',
97
- '.outputai/skills/output-workflow-trace/SKILL.md',
98
- '.outputai/skills/output-workflow-list/SKILL.md',
99
- '.outputai/skills/output-workflow-runs-list/SKILL.md',
100
- '.outputai/skills/output-workflow-run/SKILL.md',
101
- '.outputai/skills/output-workflow-start/SKILL.md',
102
- '.outputai/skills/output-workflow-status/SKILL.md',
103
- '.outputai/skills/output-workflow-result/SKILL.md',
104
- '.outputai/skills/output-workflow-stop/SKILL.md',
105
- '.outputai/skills/output-error-zod-import/SKILL.md',
106
- '.outputai/skills/output-error-nondeterminism/SKILL.md',
107
- '.outputai/skills/output-error-try-catch/SKILL.md',
108
- '.outputai/skills/output-error-missing-schemas/SKILL.md',
109
- '.outputai/skills/output-error-direct-io/SKILL.md',
110
- '.outputai/skills/output-error-http-client/SKILL.md',
111
- // Claude-code symlinks
112
- 'CLAUDE.md',
113
- '.claude/agents/workflow_planner.md',
114
- '.claude/agents/workflow_quality.md',
115
- '.claude/agents/workflow_context_fetcher.md',
116
- '.claude/agents/workflow_prompt_writer.md',
117
- '.claude/agents/workflow_debugger.md',
118
- '.claude/commands/plan_workflow.md',
119
- '.claude/commands/debug_workflow.md',
120
- '.claude/commands/build_workflow.md',
121
- // Claude-code skill symlinks (all at top level - no nesting allowed)
122
- '.claude/skills/output-services-check/SKILL.md',
123
- '.claude/skills/output-workflow-trace/SKILL.md',
124
- '.claude/skills/output-workflow-list/SKILL.md',
125
- '.claude/skills/output-workflow-runs-list/SKILL.md',
126
- '.claude/skills/output-workflow-run/SKILL.md',
127
- '.claude/skills/output-workflow-start/SKILL.md',
128
- '.claude/skills/output-workflow-status/SKILL.md',
129
- '.claude/skills/output-workflow-result/SKILL.md',
130
- '.claude/skills/output-workflow-stop/SKILL.md',
131
- '.claude/skills/output-error-zod-import/SKILL.md',
132
- '.claude/skills/output-error-nondeterminism/SKILL.md',
133
- '.claude/skills/output-error-try-catch/SKILL.md',
134
- '.claude/skills/output-error-missing-schemas/SKILL.md',
135
- '.claude/skills/output-error-direct-io/SKILL.md',
136
- '.claude/skills/output-error-http-client/SKILL.md'
137
- ],
138
- isComplete: false
50
+ isComplete: false,
51
+ needsInit: true
139
52
  });
140
53
  });
141
- it('should return complete when all files exist', async () => {
142
- // Mock directory exists
54
+ it('should return complete when all files exist with valid settings', async () => {
143
55
  vi.mocked(access).mockResolvedValue(undefined);
56
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
57
+ extraKnownMarketplaces: {
58
+ 'team-tools': {
59
+ source: {
60
+ source: 'github',
61
+ repo: 'growthxai/output-claude-plugins'
62
+ }
63
+ }
64
+ },
65
+ enabledPlugins: {
66
+ 'outputai@outputai': true
67
+ }
68
+ }));
144
69
  const result = await checkAgentStructure('/test/project');
145
70
  expect(result).toEqual({
146
- dirExists: true,
147
- missingFiles: [],
148
- isComplete: true
71
+ isComplete: true,
72
+ needsInit: false
149
73
  });
150
- expect(access).toHaveBeenCalledTimes(51); // dir + 26 outputai + 24 claude-code
151
74
  });
152
- it('should return missing files when some files do not exist', async () => {
153
- const missingFiles = new Set([
154
- '.outputai/agents/workflow_planner.md',
155
- '.claude/agents/workflow_planner.md'
156
- ]);
157
- vi.mocked(access).mockImplementation(async (path) => {
158
- const pathStr = path.toString();
159
- // Check if this path ends with any of the missing files
160
- for (const missing of missingFiles) {
161
- if (pathStr.endsWith(missing)) {
162
- throw { code: 'ENOENT' };
75
+ it('should return needsInit true when settings.json has wrong marketplace repo', async () => {
76
+ vi.mocked(access).mockResolvedValue(undefined);
77
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
78
+ extraKnownMarketplaces: {
79
+ 'team-tools': {
80
+ source: {
81
+ source: 'github',
82
+ repo: 'wrong/repo'
83
+ }
163
84
  }
85
+ },
86
+ enabledPlugins: {
87
+ 'outputai@outputai': true
164
88
  }
165
- return undefined;
166
- });
89
+ }));
167
90
  const result = await checkAgentStructure('/test/project');
168
- expect(result).toEqual({
169
- dirExists: true,
170
- missingFiles: [
171
- '.outputai/agents/workflow_planner.md',
172
- '.claude/agents/workflow_planner.md'
173
- ],
174
- isComplete: false
175
- });
91
+ expect(result.isComplete).toBe(false);
92
+ expect(result.needsInit).toBe(true);
93
+ });
94
+ it('should return needsInit true when plugin is not enabled', async () => {
95
+ vi.mocked(access).mockResolvedValue(undefined);
96
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
97
+ extraKnownMarketplaces: {
98
+ 'team-tools': {
99
+ source: {
100
+ source: 'github',
101
+ repo: 'growthxai/output-claude-plugins'
102
+ }
103
+ }
104
+ },
105
+ enabledPlugins: {
106
+ 'outputai@outputai': false
107
+ }
108
+ }));
109
+ const result = await checkAgentStructure('/test/project');
110
+ expect(result.isComplete).toBe(false);
111
+ expect(result.needsInit).toBe(true);
176
112
  });
177
- it('should check all required files even when directory exists', async () => {
178
- const missingFiles = new Set([
179
- '.outputai/agents/workflow_planner.md',
180
- '.outputai/commands/plan_workflow.md',
181
- '.claude/commands/plan_workflow.md'
182
- ]);
113
+ it('should return needsInit true when settings.json does not exist', async () => {
183
114
  vi.mocked(access).mockImplementation(async (path) => {
184
115
  const pathStr = path.toString();
185
- for (const missing of missingFiles) {
186
- if (pathStr.endsWith(missing)) {
187
- throw { code: 'ENOENT' };
188
- }
116
+ if (pathStr.endsWith('.claude/settings.json')) {
117
+ throw { code: 'ENOENT' };
189
118
  }
190
119
  return undefined;
191
120
  });
121
+ vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' });
192
122
  const result = await checkAgentStructure('/test/project');
193
- expect(result.dirExists).toBe(true);
194
- expect(result.missingFiles).toEqual([
195
- '.outputai/agents/workflow_planner.md',
196
- '.outputai/commands/plan_workflow.md',
197
- '.claude/commands/plan_workflow.md'
198
- ]);
199
123
  expect(result.isComplete).toBe(false);
124
+ expect(result.needsInit).toBe(true);
200
125
  });
201
126
  });
202
127
  describe('prepareTemplateVariables', () => {
@@ -204,97 +129,60 @@ describe('coding_agents service', () => {
204
129
  const variables = prepareTemplateVariables();
205
130
  expect(variables).toHaveProperty('date');
206
131
  expect(typeof variables.date).toBe('string');
207
- // Should be in format like "January 1, 2025"
208
132
  expect(variables.date).toMatch(/^[A-Z][a-z]+ \d{1,2}, \d{4}$/);
209
133
  });
210
134
  });
211
135
  describe('initializeAgentConfig', () => {
212
136
  beforeEach(() => {
213
- // Mock fs operations
214
137
  vi.mocked(fs.mkdir).mockResolvedValue(undefined);
215
- vi.mocked(access).mockRejectedValue({ code: 'ENOENT' }); // Files don't exist
138
+ vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
216
139
  vi.mocked(fs.readFile).mockResolvedValue('template content');
217
140
  vi.mocked(fs.writeFile).mockResolvedValue(undefined);
218
141
  vi.mocked(fs.symlink).mockResolvedValue(undefined);
219
142
  });
220
- it('should process both outputai and provider mappings', async () => {
143
+ it('should create exactly 3 outputs: AGENTS.md, settings.json, and CLAUDE.md symlink', async () => {
221
144
  await initializeAgentConfig({
222
145
  projectRoot: '/test/project',
223
- force: false,
224
- agentProvider: 'claude-code'
146
+ force: false
225
147
  });
226
- // Should create outputai files (26 templates)
227
- expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining('AGENTS.md'), expect.any(String), 'utf-8');
228
- // Should create symlinks (24 symlinks for claude-code)
229
- expect(fs.symlink).toHaveBeenCalledTimes(24);
148
+ expect(fs.mkdir).toHaveBeenCalledTimes(2);
149
+ expect(fs.mkdir).toHaveBeenCalledWith('/test/project/.outputai', expect.objectContaining({ recursive: true }));
150
+ expect(fs.mkdir).toHaveBeenCalledWith('/test/project/.claude', expect.objectContaining({ recursive: true }));
151
+ expect(fs.writeFile).toHaveBeenCalledWith('/test/project/.outputai/AGENTS.md', expect.any(String), 'utf-8');
152
+ expect(fs.writeFile).toHaveBeenCalledWith('/test/project/.claude/settings.json', expect.any(String), 'utf-8');
153
+ expect(fs.symlink).toHaveBeenCalledTimes(1);
230
154
  });
231
155
  it('should skip existing files when force is false', async () => {
232
- // Mock some files exist
233
- vi.mocked(access).mockImplementation(async (path, _mode) => {
234
- const pathStr = path.toString();
235
- if (pathStr.includes('AGENTS.md')) {
236
- return; // File exists
237
- }
238
- throw { code: 'ENOENT' }; // File doesn't exist
239
- });
156
+ vi.mocked(access).mockResolvedValue(undefined);
240
157
  await initializeAgentConfig({
241
158
  projectRoot: '/test/project',
242
- force: false,
243
- agentProvider: 'claude-code'
159
+ force: false
244
160
  });
245
- // Should not create files that already exist
246
- expect(fs.writeFile).toHaveBeenCalled();
247
- const writeFileCalls = vi.mocked(fs.writeFile).mock.calls;
248
- const agentsWriteCalls = writeFileCalls.filter(call => call[0].toString().includes('AGENTS.md'));
249
- expect(agentsWriteCalls).toHaveLength(0);
161
+ expect(fs.writeFile).not.toHaveBeenCalled();
162
+ expect(fs.symlink).not.toHaveBeenCalled();
250
163
  });
251
164
  it('should overwrite existing files when force is true', async () => {
252
- // Mock all files exist
253
165
  vi.mocked(access).mockResolvedValue(undefined);
166
+ vi.mocked(fs.unlink).mockResolvedValue(undefined);
254
167
  await initializeAgentConfig({
255
168
  projectRoot: '/test/project',
256
- force: true,
257
- agentProvider: 'claude-code'
169
+ force: true
258
170
  });
259
- // Should still write files
260
171
  expect(fs.writeFile).toHaveBeenCalled();
261
172
  });
262
- it('should create files and output to stdout', async () => {
263
- await initializeAgentConfig({
264
- projectRoot: '/test/project',
265
- force: false,
266
- agentProvider: 'claude-code'
267
- });
268
- // Should create files
269
- expect(fs.writeFile).toHaveBeenCalled();
270
- expect(fs.symlink).toHaveBeenCalled();
271
- });
272
- it('should create directories for nested files', async () => {
273
- await initializeAgentConfig({
274
- projectRoot: '/test/project',
275
- force: false,
276
- agentProvider: 'claude-code'
277
- });
278
- // Should create directories like .outputai/agents, .outputai/commands
279
- expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining('agents'), expect.objectContaining({ recursive: true }));
280
- });
281
173
  it('should handle symlink errors by falling back to copy', async () => {
282
174
  vi.mocked(fs.symlink).mockRejectedValue({ code: 'ENOTSUP' });
283
175
  await initializeAgentConfig({
284
176
  projectRoot: '/test/project',
285
- force: false,
286
- agentProvider: 'claude-code'
177
+ force: false
287
178
  });
288
- // Should fall back to copying via writeFile
289
- expect(fs.writeFile).toHaveBeenCalled();
290
179
  const writeFileCalls = vi.mocked(fs.writeFile).mock.calls;
291
- const symlinkFallbackCalls = writeFileCalls.filter(call => call[0].toString().includes('CLAUDE.md') || call[0].toString().includes('.claude/'));
292
- expect(symlinkFallbackCalls.length).toBeGreaterThan(0);
180
+ const claudeMdCalls = writeFileCalls.filter(call => call[0].toString().includes('CLAUDE.md'));
181
+ expect(claudeMdCalls.length).toBe(1);
293
182
  });
294
183
  });
295
- describe('ensureOutputAIStructure', () => {
184
+ describe('ensureOutputAISystem', () => {
296
185
  beforeEach(() => {
297
- // Mock fs operations
298
186
  vi.mocked(fs.mkdir).mockResolvedValue(undefined);
299
187
  vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
300
188
  vi.mocked(fs.readFile).mockResolvedValue('template content');
@@ -302,113 +190,36 @@ describe('coding_agents service', () => {
302
190
  vi.mocked(fs.symlink).mockResolvedValue(undefined);
303
191
  });
304
192
  it('should return immediately when agent structure is complete', async () => {
305
- // Mock complete structure
306
193
  vi.mocked(access).mockResolvedValue(undefined);
307
- await ensureOutputAIStructure('/test/project');
308
- // Should not call init functions
194
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
195
+ extraKnownMarketplaces: {
196
+ 'team-tools': {
197
+ source: { source: 'github', repo: 'growthxai/output-claude-plugins' }
198
+ }
199
+ },
200
+ enabledPlugins: { 'outputai@outputai': true }
201
+ }));
202
+ await ensureOutputAISystem('/test/project');
309
203
  expect(fs.mkdir).not.toHaveBeenCalled();
310
- expect(fs.writeFile).not.toHaveBeenCalled();
311
204
  });
312
205
  it('should auto-initialize when directory does not exist', async () => {
313
- // Mock directory doesn't exist
314
206
  vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
315
- await ensureOutputAIStructure('/test/project');
316
- // Should call init functions to create structure
207
+ await ensureOutputAISystem('/test/project');
317
208
  expect(fs.mkdir).toHaveBeenCalled();
318
209
  expect(fs.writeFile).toHaveBeenCalled();
319
210
  });
320
- it('should prompt user when some files are missing', async () => {
321
- const { confirm } = await import('@inquirer/prompts');
322
- const { ux } = await import('@oclif/core');
323
- // Mock directory exists but some files missing
324
- const calls = [];
325
- vi.mocked(access).mockImplementation(async () => {
326
- const callNum = calls.length + 1;
327
- calls.push(callNum);
328
- if (callNum === 1) {
329
- return undefined; // Directory exists
330
- }
331
- if (callNum === 2) {
332
- return undefined; // First file exists
333
- }
334
- throw { code: 'ENOENT' }; // Rest are missing
335
- });
336
- vi.mocked(confirm).mockResolvedValue(true);
337
- await ensureOutputAIStructure('/test/project');
338
- // Should warn about missing files
339
- expect(ux.warn).toHaveBeenCalledWith(expect.stringContaining('Agent configuration is incomplete'));
340
- // Should prompt user
341
- expect(confirm).toHaveBeenCalledWith({
342
- message: 'Would you like to run "agents init --force" to recreate missing files?',
343
- default: true
344
- });
345
- // Should reinitialize with force=true
346
- expect(fs.writeFile).toHaveBeenCalled();
347
- });
348
- it('should throw error when user declines to reinitialize', async () => {
349
- const { confirm } = await import('@inquirer/prompts');
350
- // Mock directory exists but files missing
351
- const calls = [];
352
- vi.mocked(access).mockImplementation(async () => {
353
- const callNum = calls.length + 1;
354
- calls.push(callNum);
355
- if (callNum === 1) {
356
- return undefined; // Directory exists
357
- }
358
- throw { code: 'ENOENT' }; // Files are missing
359
- });
360
- vi.mocked(confirm).mockResolvedValue(false);
361
- await expect(ensureOutputAIStructure('/test/project')).rejects.toThrow('Agent configuration incomplete');
362
- // Should not call init functions
363
- expect(fs.mkdir).not.toHaveBeenCalled();
364
- expect(fs.writeFile).not.toHaveBeenCalled();
365
- });
366
- it('should list all missing files in error message when user declines', async () => {
367
- const { confirm } = await import('@inquirer/prompts');
368
- // Mock directory exists but all files missing
369
- const calls = [];
370
- vi.mocked(access).mockImplementation(async () => {
371
- const callNum = calls.length + 1;
372
- calls.push(callNum);
373
- if (callNum === 1) {
374
- return undefined; // Directory exists
375
- }
376
- throw { code: 'ENOENT' }; // All files missing
377
- });
378
- vi.mocked(confirm).mockResolvedValue(false);
379
- try {
380
- await ensureOutputAIStructure('/test/project');
381
- expect.fail('Should have thrown an error');
382
- }
383
- catch (error) {
384
- const message = getErrorMessage(error);
385
- expect(message).toContain('.outputai/AGENTS.md');
386
- expect(message).toContain('.outputai/agents/workflow_planner.md');
387
- expect(message).toContain('Run "output-cli agents init --force"');
388
- }
389
- });
390
- it('should call initializeAgentConfig with force=false when dir does not exist', async () => {
391
- vi.mocked(access).mockRejectedValue({ code: 'ENOENT' });
392
- await ensureOutputAIStructure('/test/project');
393
- // Should have called mkdir (part of init with force=false)
211
+ it('should auto-initialize when settings.json is invalid', async () => {
212
+ vi.mocked(access).mockResolvedValue(undefined);
213
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
214
+ extraKnownMarketplaces: {
215
+ 'team-tools': {
216
+ source: { source: 'github', repo: 'wrong/repo' }
217
+ }
218
+ },
219
+ enabledPlugins: { 'outputai@outputai': true }
220
+ }));
221
+ await ensureOutputAISystem('/test/project');
394
222
  expect(fs.mkdir).toHaveBeenCalled();
395
223
  });
396
- it('should call initializeAgentConfig with force=true after user confirmation', async () => {
397
- const { confirm } = await import('@inquirer/prompts');
398
- // Mock directory exists but files missing
399
- const calls = [];
400
- vi.mocked(access).mockImplementation(async () => {
401
- const callNum = calls.length + 1;
402
- calls.push(callNum);
403
- if (callNum === 1) {
404
- return undefined; // Directory exists
405
- }
406
- throw { code: 'ENOENT' }; // Files missing
407
- });
408
- vi.mocked(confirm).mockResolvedValue(true);
409
- await ensureOutputAIStructure('/test/project');
410
- // Should have called writeFile (part of init with force=true)
411
- expect(fs.writeFile).toHaveBeenCalled();
412
- });
413
224
  });
414
225
  });
@@ -9,7 +9,7 @@ import { getSDKVersions } from '#utils/sdk_versions.js';
9
9
  import { getErrorMessage, getErrorCode } from '#utils/error_utils.js';
10
10
  import { configureEnvironmentVariables } from './env_configurator.js';
11
11
  import { getTemplateFiles, processTemplateFile } from './template_processor.js';
12
- import { ensureOutputAIStructure } from './coding_agents.js';
12
+ import { ensureOutputAISystem } from './coding_agents.js';
13
13
  import { getProjectSuccessMessage } from './messages.js';
14
14
  const getProjectConfig = async () => {
15
15
  try {
@@ -59,7 +59,7 @@ async function executeNpmInstall(projectPath) {
59
59
  await executeCommand('npm', ['install'], projectPath);
60
60
  }
61
61
  async function initializeAgents(projectPath) {
62
- await ensureOutputAIStructure(projectPath);
62
+ await ensureOutputAISystem(projectPath);
63
63
  }
64
64
  function extractProjectPath(error) {
65
65
  if (error instanceof FolderAlreadyExistsError) {
@@ -144,7 +144,7 @@ export async function runInit(log, _warn, skipEnv = false) {
144
144
  if (envConfigured) {
145
145
  log('🔐 Environment variables configured in .env');
146
146
  }
147
- await executeCommandWithMessages(() => initializeAgents(config.projectPath), '🤖 Initializing agent system...', '✅ Agent system initialized in .outputai/');
147
+ await executeCommandWithMessages(() => initializeAgents(config.projectPath), '🤖 Initializing agent system...', '✅ Agent system initialized');
148
148
  const installSuccess = await executeCommandWithMessages(() => executeNpmInstall(config.projectPath), '📦 Installing dependencies...', '✅ Dependencies installed');
149
149
  const nextSteps = getProjectSuccessMessage(config.folderName, installSuccess, envConfigured);
150
150
  log('✅ Project created successfully!');
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Build a workflow from a plan file using the /build_workflow slash command
2
+ * Build a workflow from a plan file using the /outputai:build_workflow slash command
3
3
  * @param planFilePath - Absolute path to the plan file
4
4
  * @param workflowDir - Absolute path to the workflow directory
5
5
  * @param workflowName - Name of the workflow
@@ -31,7 +31,7 @@ function isEmpty(modification) {
31
31
  return modification.trim() === '';
32
32
  }
33
33
  /**
34
- * Build a workflow from a plan file using the /build_workflow slash command
34
+ * Build a workflow from a plan file using the /outputai:build_workflow slash command
35
35
  * @param planFilePath - Absolute path to the plan file
36
36
  * @param workflowDir - Absolute path to the workflow directory
37
37
  * @param workflowName - Name of the workflow
@@ -4,7 +4,10 @@ import fs from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { AGENT_CONFIG_DIR } from '#config.js';
6
6
  export async function generatePlanName(description, date = new Date()) {
7
- const datePrefix = new Date(date).toISOString().split('T')[0].replaceAll('-', '_');
7
+ const year = date.getFullYear();
8
+ const month = String(date.getMonth() + 1).padStart(2, '0');
9
+ const day = String(date.getDate()).padStart(2, '0');
10
+ const datePrefix = `${year}_${month}_${day}`;
8
11
  const planNameSlug = await generateText({
9
12
  prompt: 'generate_plan_name@v1',
10
13
  variables: { description }
@@ -40,7 +43,6 @@ export async function writePlanFile(planName, content, projectRoot) {
40
43
  export async function updateAgentTemplates(projectRoot) {
41
44
  await initializeAgentConfig({
42
45
  projectRoot,
43
- force: true,
44
- agentProvider: 'claude-code'
46
+ force: true
45
47
  });
46
48
  }
@@ -75,7 +75,7 @@ describe('workflow-planner service', () => {
75
75
  it('should handle UTF-8 content with special characters', async () => {
76
76
  vi.mocked(fs.mkdir).mockResolvedValue(undefined);
77
77
  vi.mocked(fs.writeFile).mockResolvedValue(undefined);
78
- const content = '# Plan 🎯\n\n中文 Español • العربية\n\n"Smart quotes" anddashes';
78
+ const content = '# Plan\n\nSpecial characters: quotes and dashes';
79
79
  await writePlanFile('unicode_test', content, '/test');
80
80
  expect(fs.writeFile).toHaveBeenCalledWith(expect.any(String), content, 'utf-8');
81
81
  });
@@ -95,11 +95,10 @@ describe('workflow-planner service', () => {
95
95
  it('should invoke agents init with force flag', async () => {
96
96
  vi.mocked(initializeAgentConfig).mockResolvedValue();
97
97
  await updateAgentTemplates('/test/project');
98
- expect(initializeAgentConfig).toHaveBeenCalledWith(expect.objectContaining({
98
+ expect(initializeAgentConfig).toHaveBeenCalledWith({
99
99
  projectRoot: '/test/project',
100
- force: true,
101
- agentProvider: 'claude-code'
102
- }));
100
+ force: true
101
+ });
103
102
  });
104
103
  it('should propagate errors from agents init', async () => {
105
104
  vi.mocked(initializeAgentConfig).mockRejectedValue(new Error('Failed to write templates'));
@@ -0,0 +1,29 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch",
5
+ "Bash(npx output:*)",
6
+ "Bash(npm run dev)",
7
+ "Bash(npm run output:*)",
8
+ "Skills(output*)",
9
+ "Skills(flow*)"
10
+ ]
11
+ },
12
+ "enabledPlugins": {
13
+ "outputai@outputai": true
14
+ },
15
+ "extraKnownMarketplaces": {
16
+ "team-tools": {
17
+ "source": {
18
+ "source": "github",
19
+ "repo": "growthxai/output-claude-plugins"
20
+ }
21
+ }
22
+ },
23
+ "companyAnnouncements": [
24
+ "Output.ai: Stay up to date with the latest Output.ai announcements and updates at https://output.ai",
25
+ "Output.ai: Ensure the latest version of the Output.ai CLI is installed and up to date with `npm update @output.ai/cli`",
26
+ "Output.ai: Learn more about how to use the Output Framework at https://docs.output.ai/",
27
+ "Output.ai: Ensure your Claude Code Plugins are up to date with `/plugin marketplace update outputai` and `/plugin install @outputai@outputai`"
28
+ ]
29
+ }