@outputai/cli 0.2.1-next.af8a069.0 → 0.2.1-next.bc8ccee.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 (48) hide show
  1. package/bin/run.js +2 -0
  2. package/dist/api/generated/api.d.ts +141 -2
  3. package/dist/api/generated/api.js +32 -0
  4. package/dist/api/http_client.js +24 -19
  5. package/dist/assets/docker/docker-compose-dev.yml +1 -1
  6. package/dist/commands/fix.js +1 -1
  7. package/dist/commands/fix.spec.js +2 -2
  8. package/dist/commands/init.d.ts +1 -0
  9. package/dist/commands/init.js +5 -1
  10. package/dist/commands/init.spec.js +10 -5
  11. package/dist/commands/update.js +1 -1
  12. package/dist/commands/update.spec.js +2 -2
  13. package/dist/commands/workflow/plan.js +5 -1
  14. package/dist/commands/workflow/plan.spec.js +3 -2
  15. package/dist/config.d.ts +6 -38
  16. package/dist/config.js +22 -42
  17. package/dist/config.spec.d.ts +1 -0
  18. package/dist/config.spec.js +75 -0
  19. package/dist/generated/framework_version.json +1 -1
  20. package/dist/hooks/init.d.ts +4 -0
  21. package/dist/hooks/init.js +17 -1
  22. package/dist/hooks/init.spec.js +79 -5
  23. package/dist/services/coding_agents.js +5 -1
  24. package/dist/services/coding_agents.spec.js +19 -6
  25. package/dist/services/credentials_configurator.js +1 -1
  26. package/dist/services/env_configurator.js +1 -1
  27. package/dist/services/env_configurator.spec.js +12 -12
  28. package/dist/services/project_scaffold.d.ts +1 -1
  29. package/dist/services/project_scaffold.js +17 -2
  30. package/dist/services/project_scaffold.spec.js +6 -6
  31. package/dist/services/workflow_builder.js +5 -1
  32. package/dist/services/workflow_builder.spec.js +3 -2
  33. package/dist/utils/env_loader.js +1 -2
  34. package/dist/utils/error_handler.js +10 -8
  35. package/dist/utils/interactive.d.ts +2 -0
  36. package/dist/utils/interactive.js +5 -0
  37. package/dist/utils/interactive.spec.d.ts +1 -0
  38. package/dist/utils/interactive.spec.js +40 -0
  39. package/dist/utils/prompt.d.ts +17 -0
  40. package/dist/utils/prompt.js +20 -0
  41. package/dist/utils/prompt.spec.d.ts +1 -0
  42. package/dist/utils/prompt.spec.js +70 -0
  43. package/dist/utils/proxy.d.ts +9 -0
  44. package/dist/utils/proxy.js +24 -0
  45. package/dist/utils/proxy.spec.d.ts +1 -0
  46. package/dist/utils/proxy.spec.js +48 -0
  47. package/dist/views/workflow/list.js +8 -6
  48. package/package.json +5 -4
@@ -1,6 +1,22 @@
1
1
  import { ux } from '@oclif/core';
2
2
  import { checkForUpdate } from '#services/version_check.js';
3
- const hook = async function () {
3
+ import { setNonInteractive } from '#utils/interactive.js';
4
+ export const INTERACTIVE_FLAGS = ['--yes', '--non-interactive'];
5
+ export const GLOBAL_FLAGS = new Set(INTERACTIVE_FLAGS);
6
+ export const hasInteractiveFlag = (argv) => argv.some(arg => INTERACTIVE_FLAGS.includes(arg));
7
+ export const stripGlobalFlags = (argv) => {
8
+ const kept = argv.filter(arg => !GLOBAL_FLAGS.has(arg));
9
+ if (kept.length !== argv.length) {
10
+ argv.splice(0, argv.length, ...kept);
11
+ }
12
+ };
13
+ const hook = async function (opts) {
14
+ const interactive = hasInteractiveFlag(opts.argv) || hasInteractiveFlag(process.argv);
15
+ stripGlobalFlags(opts.argv);
16
+ stripGlobalFlags(process.argv);
17
+ if (interactive) {
18
+ setNonInteractive(true);
19
+ }
4
20
  try {
5
21
  const result = await checkForUpdate(this.config.version, this.config.cacheDir);
6
22
  if (!result.updateAvailable) {
@@ -1,9 +1,13 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
3
  import { checkForUpdate } from '#services/version_check.js';
4
+ import { setNonInteractive } from '#utils/interactive.js';
4
5
  vi.mock('#services/version_check.js', () => ({
5
6
  checkForUpdate: vi.fn()
6
7
  }));
8
+ vi.mock('#utils/interactive.js', () => ({
9
+ setNonInteractive: vi.fn()
10
+ }));
7
11
  vi.mock('@oclif/core', () => ({
8
12
  ux: {
9
13
  stdout: vi.fn(),
@@ -11,7 +15,7 @@ vi.mock('@oclif/core', () => ({
11
15
  }
12
16
  }));
13
17
  import { ux } from '@oclif/core';
14
- import hook from './init.js';
18
+ import hook, { hasInteractiveFlag, stripGlobalFlags } from './init.js';
15
19
  describe('init hook', () => {
16
20
  beforeEach(() => {
17
21
  vi.clearAllMocks();
@@ -26,7 +30,7 @@ describe('init hook', () => {
26
30
  latestVersion: '1.0.0'
27
31
  });
28
32
  const ctx = createHookContext();
29
- await hook.call(ctx, {});
33
+ await hook.call(ctx, { argv: [], id: undefined });
30
34
  expect(checkForUpdate).toHaveBeenCalledWith('0.8.4', '/tmp/test-cache');
31
35
  expect(ux.stdout).toHaveBeenCalled();
32
36
  const output = vi.mocked(ux.stdout).mock.calls.map(c => c[0]).join('\n');
@@ -42,13 +46,83 @@ describe('init hook', () => {
42
46
  latestVersion: '0.8.4'
43
47
  });
44
48
  const ctx = createHookContext();
45
- await hook.call(ctx, {});
49
+ await hook.call(ctx, { argv: [], id: undefined });
46
50
  expect(ux.stdout).not.toHaveBeenCalled();
47
51
  });
48
52
  it('should silently handle errors', async () => {
49
53
  vi.mocked(checkForUpdate).mockRejectedValue(new Error('network failure'));
50
54
  const ctx = createHookContext();
51
- await hook.call(ctx, {});
55
+ await hook.call(ctx, { argv: [], id: undefined });
52
56
  expect(ux.stdout).not.toHaveBeenCalled();
53
57
  });
58
+ describe('global interactive flags', () => {
59
+ const originalArgv = process.argv;
60
+ beforeEach(() => {
61
+ vi.mocked(checkForUpdate).mockResolvedValue({
62
+ updateAvailable: false,
63
+ currentVersion: '0.8.4',
64
+ latestVersion: '0.8.4'
65
+ });
66
+ });
67
+ afterEach(() => {
68
+ process.argv = originalArgv;
69
+ });
70
+ it('should mutate opts.argv in place to strip --yes', async () => {
71
+ process.argv = ['node', 'run.js', 'init', '--yes', 'my-project'];
72
+ const optsArgv = ['--yes', 'my-project'];
73
+ const argvRef = optsArgv;
74
+ const ctx = createHookContext();
75
+ await hook.call(ctx, { argv: optsArgv, id: 'init' });
76
+ expect(setNonInteractive).toHaveBeenCalledWith(true);
77
+ expect(optsArgv).toBe(argvRef);
78
+ expect(optsArgv).toEqual(['my-project']);
79
+ expect(process.argv).toEqual(['node', 'run.js', 'init', 'my-project']);
80
+ });
81
+ it('should mutate opts.argv in place to strip --non-interactive', async () => {
82
+ process.argv = ['node', 'run.js', 'init', '--non-interactive'];
83
+ const optsArgv = ['--non-interactive'];
84
+ const ctx = createHookContext();
85
+ await hook.call(ctx, { argv: optsArgv, id: 'init' });
86
+ expect(setNonInteractive).toHaveBeenCalledWith(true);
87
+ expect(optsArgv).toEqual([]);
88
+ expect(process.argv).toEqual(['node', 'run.js', 'init']);
89
+ });
90
+ it('should leave argv untouched when no global flag is present', async () => {
91
+ process.argv = ['node', 'run.js', 'init', '--skip-env'];
92
+ const optsArgv = ['--skip-env'];
93
+ const ctx = createHookContext();
94
+ await hook.call(ctx, { argv: optsArgv, id: 'init' });
95
+ expect(setNonInteractive).not.toHaveBeenCalled();
96
+ expect(optsArgv).toEqual(['--skip-env']);
97
+ expect(process.argv).toEqual(['node', 'run.js', 'init', '--skip-env']);
98
+ });
99
+ });
100
+ describe('hasInteractiveFlag', () => {
101
+ it('returns true when --yes is present', () => {
102
+ expect(hasInteractiveFlag(['init', '--yes', 'foo'])).toBe(true);
103
+ });
104
+ it('returns true when --non-interactive is present', () => {
105
+ expect(hasInteractiveFlag(['--non-interactive'])).toBe(true);
106
+ });
107
+ it('returns false for unrelated flags', () => {
108
+ expect(hasInteractiveFlag(['init', '--skip-env', '--skip-git'])).toBe(false);
109
+ });
110
+ it('returns false for an empty argv', () => {
111
+ expect(hasInteractiveFlag([])).toBe(false);
112
+ });
113
+ });
114
+ describe('stripGlobalFlags', () => {
115
+ it('mutates argv in place to remove global flags', () => {
116
+ const argv = ['init', '--yes', 'foo', '--non-interactive'];
117
+ const ref = argv;
118
+ stripGlobalFlags(argv);
119
+ expect(argv).toBe(ref);
120
+ expect(argv).toEqual(['init', 'foo']);
121
+ });
122
+ it('leaves argv untouched when no global flag is present', () => {
123
+ const argv = ['init', '--skip-env'];
124
+ stripGlobalFlags(argv);
125
+ expect(argv).toEqual(['init', '--skip-env']);
126
+ });
127
+ });
54
128
  });
@@ -7,7 +7,8 @@ import { access } from 'node:fs/promises';
7
7
  import path from 'node:path';
8
8
  import { join } from 'node:path';
9
9
  import { ux } from '@oclif/core';
10
- import { confirm } from '@inquirer/prompts';
10
+ import { confirm } from '#utils/prompt.js';
11
+ import { isInteractive } from '#utils/interactive.js';
11
12
  import debugFactory from 'debug';
12
13
  import { getTemplateDir } from '#utils/paths.js';
13
14
  import { executeClaudeCommand } from '#utils/claude.js';
@@ -142,6 +143,9 @@ async function handlePluginError(error, commandName, silent = false) {
142
143
  debug('Plugin error: %s', pluginError.message);
143
144
  throw error;
144
145
  }
146
+ if (!isInteractive()) {
147
+ throw pluginError;
148
+ }
145
149
  ux.warn(pluginError.message);
146
150
  try {
147
151
  const shouldProceed = await confirm({
@@ -19,9 +19,12 @@ vi.mock('@oclif/core', () => ({
19
19
  colorize: vi.fn().mockImplementation((_color, text) => text)
20
20
  }
21
21
  }));
22
- vi.mock('@inquirer/prompts', () => ({
22
+ vi.mock('#utils/prompt.js', () => ({
23
23
  confirm: vi.fn()
24
24
  }));
25
+ vi.mock('#utils/interactive.js', () => ({
26
+ isInteractive: vi.fn(() => true)
27
+ }));
25
28
  describe('coding_agents service', () => {
26
29
  beforeEach(() => {
27
30
  vi.clearAllMocks();
@@ -157,7 +160,7 @@ describe('coding_agents service', () => {
157
160
  });
158
161
  it('should show error and prompt user when plugin commands fail', async () => {
159
162
  const { executeClaudeCommand } = await import('../utils/claude.js');
160
- const { confirm } = await import('@inquirer/prompts');
163
+ const { confirm } = await import('#utils/prompt.js');
161
164
  vi.mocked(executeClaudeCommand)
162
165
  .mockResolvedValueOnce(undefined) // marketplace add
163
166
  .mockRejectedValueOnce(new Error('Plugin update failed')); // marketplace update
@@ -169,7 +172,7 @@ describe('coding_agents service', () => {
169
172
  });
170
173
  it('should allow user to proceed without plugin setup if they confirm', async () => {
171
174
  const { executeClaudeCommand } = await import('../utils/claude.js');
172
- const { confirm } = await import('@inquirer/prompts');
175
+ const { confirm } = await import('#utils/prompt.js');
173
176
  vi.mocked(executeClaudeCommand)
174
177
  .mockRejectedValue(new Error('All plugin commands fail'));
175
178
  vi.mocked(confirm).mockResolvedValue(true);
@@ -219,7 +222,7 @@ describe('coding_agents service', () => {
219
222
  });
220
223
  it('should show error and prompt user when registerPluginMarketplace fails', async () => {
221
224
  const { executeClaudeCommand } = await import('../utils/claude.js');
222
- const { confirm } = await import('@inquirer/prompts');
225
+ const { confirm } = await import('#utils/prompt.js');
223
226
  vi.mocked(executeClaudeCommand)
224
227
  .mockResolvedValueOnce(undefined) // marketplace add
225
228
  .mockRejectedValueOnce(new Error('Plugin update failed')); // marketplace update
@@ -231,7 +234,7 @@ describe('coding_agents service', () => {
231
234
  });
232
235
  it('should show error and prompt user when installOutputAIPlugin fails', async () => {
233
236
  const { executeClaudeCommand } = await import('../utils/claude.js');
234
- const { confirm } = await import('@inquirer/prompts');
237
+ const { confirm } = await import('#utils/prompt.js');
235
238
  vi.mocked(executeClaudeCommand)
236
239
  .mockResolvedValueOnce(undefined) // marketplace add
237
240
  .mockResolvedValueOnce(undefined) // marketplace update
@@ -244,7 +247,7 @@ describe('coding_agents service', () => {
244
247
  });
245
248
  it('should allow user to proceed without plugin setup if they confirm', async () => {
246
249
  const { executeClaudeCommand } = await import('../utils/claude.js');
247
- const { confirm } = await import('@inquirer/prompts');
250
+ const { confirm } = await import('#utils/prompt.js');
248
251
  vi.mocked(executeClaudeCommand)
249
252
  .mockRejectedValue(new Error('All plugin commands fail'));
250
253
  vi.mocked(confirm).mockResolvedValue(true);
@@ -252,5 +255,15 @@ describe('coding_agents service', () => {
252
255
  // File operations should still complete
253
256
  expect(fs.mkdir).toHaveBeenCalled();
254
257
  });
258
+ it('should rethrow plugin error in non-interactive mode without prompting', async () => {
259
+ const { executeClaudeCommand } = await import('../utils/claude.js');
260
+ const { confirm } = await import('#utils/prompt.js');
261
+ const { isInteractive } = await import('#utils/interactive.js');
262
+ vi.mocked(isInteractive).mockReturnValueOnce(false);
263
+ vi.mocked(executeClaudeCommand)
264
+ .mockRejectedValueOnce(new Error('Plugin marketplace add failed'));
265
+ await expect(initializeAgentConfig({ projectRoot: '/test/project', force: true })).rejects.toThrow(/plugin marketplace add/i);
266
+ expect(confirm).not.toHaveBeenCalled();
267
+ });
255
268
  });
256
269
  });
@@ -1,4 +1,4 @@
1
- import { password, confirm } from '@inquirer/prompts';
1
+ import { password, confirm } from '#utils/prompt.js';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { ux } from '@oclif/core';
@@ -1,4 +1,4 @@
1
- import { input, confirm, password } from '@inquirer/prompts';
1
+ import { input, confirm, password } from '#utils/prompt.js';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { ux } from '@oclif/core';
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { configureEnvironmentVariables } from './env_configurator.js';
5
5
  // Mock inquirer prompts
6
- vi.mock('@inquirer/prompts', () => ({
6
+ vi.mock('#utils/prompt.js', () => ({
7
7
  input: vi.fn(),
8
8
  confirm: vi.fn(),
9
9
  password: vi.fn()
@@ -45,7 +45,7 @@ describe('configureEnvironmentVariables', () => {
45
45
  expect(result).toBe(false);
46
46
  });
47
47
  it('should return false if user declines configuration', async () => {
48
- const { confirm } = await import('@inquirer/prompts');
48
+ const { confirm } = await import('#utils/prompt.js');
49
49
  vi.mocked(confirm).mockResolvedValue(false);
50
50
  await fs.writeFile(testState.envExamplePath, '# API key\nAPIKEY=');
51
51
  const result = await configureEnvironmentVariables(testState.tempDir, false);
@@ -53,14 +53,14 @@ describe('configureEnvironmentVariables', () => {
53
53
  expect(vi.mocked(confirm)).toHaveBeenCalled();
54
54
  });
55
55
  it('should return false if no empty variables exist', async () => {
56
- const { confirm } = await import('@inquirer/prompts');
56
+ const { confirm } = await import('#utils/prompt.js');
57
57
  vi.mocked(confirm).mockResolvedValue(true);
58
58
  await fs.writeFile(testState.envExamplePath, 'APIKEY=my-secret-key');
59
59
  const result = await configureEnvironmentVariables(testState.tempDir, false);
60
60
  expect(result).toBe(false);
61
61
  });
62
62
  it('should copy .env.example to .env when user confirms configuration', async () => {
63
- const { input, confirm } = await import('@inquirer/prompts');
63
+ const { input, confirm } = await import('#utils/prompt.js');
64
64
  vi.mocked(confirm).mockResolvedValue(true);
65
65
  vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
66
66
  const originalContent = `# API key
@@ -72,7 +72,7 @@ APIKEY=`;
72
72
  await expect(fs.access(testState.envPath)).resolves.toBeUndefined();
73
73
  });
74
74
  it('should write configured values to .env while leaving .env.example unchanged', async () => {
75
- const { input, confirm } = await import('@inquirer/prompts');
75
+ const { input, confirm } = await import('#utils/prompt.js');
76
76
  vi.mocked(confirm).mockResolvedValue(true);
77
77
  vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
78
78
  const originalContent = `# API key
@@ -88,7 +88,7 @@ APIKEY=`;
88
88
  expect(envExampleContent).toBe(originalContent);
89
89
  });
90
90
  it('should prompt for empty variables and update .env', async () => {
91
- const { input, confirm } = await import('@inquirer/prompts');
91
+ const { input, confirm } = await import('#utils/prompt.js');
92
92
  vi.mocked(confirm).mockResolvedValue(true);
93
93
  vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
94
94
  vi.mocked(input).mockResolvedValueOnce('');
@@ -105,7 +105,7 @@ OPENAI_API_KEY=`);
105
105
  expect(content).toContain('OPENAI_API_KEY=');
106
106
  });
107
107
  it('should preserve comments in .env file', async () => {
108
- const { input, confirm } = await import('@inquirer/prompts');
108
+ const { input, confirm } = await import('#utils/prompt.js');
109
109
  vi.mocked(confirm).mockResolvedValue(true);
110
110
  vi.mocked(input).mockResolvedValueOnce('test-key');
111
111
  const originalContent = `# This is a comment
@@ -123,7 +123,7 @@ OTHER=value`;
123
123
  expect(content).toContain('OTHER=value');
124
124
  });
125
125
  it('should skip placeholder values and only prompt for truly empty variables', async () => {
126
- const { input, confirm } = await import('@inquirer/prompts');
126
+ const { input, confirm } = await import('#utils/prompt.js');
127
127
  vi.mocked(confirm).mockResolvedValue(true);
128
128
  vi.mocked(input).mockResolvedValueOnce('new-key');
129
129
  await fs.writeFile(testState.envExamplePath, `APIKEY=your_api_key_here
@@ -136,7 +136,7 @@ EMPTY_KEY=`);
136
136
  }));
137
137
  });
138
138
  it('should skip variables with existing values', async () => {
139
- const { input, confirm } = await import('@inquirer/prompts');
139
+ const { input, confirm } = await import('#utils/prompt.js');
140
140
  vi.mocked(confirm).mockResolvedValue(true);
141
141
  vi.mocked(input).mockResolvedValueOnce('new-key');
142
142
  await fs.writeFile(testState.envExamplePath, `EXISTING_KEY=existing-value
@@ -147,7 +147,7 @@ EMPTY_KEY=`);
147
147
  expect(vi.mocked(input)).toHaveBeenCalledTimes(1);
148
148
  });
149
149
  it('should handle case where .env already exists (overwrite with copy)', async () => {
150
- const { input, confirm } = await import('@inquirer/prompts');
150
+ const { input, confirm } = await import('#utils/prompt.js');
151
151
  vi.mocked(confirm).mockResolvedValue(true);
152
152
  vi.mocked(input).mockResolvedValueOnce('new-configured-value');
153
153
  // Create existing .env with old content
@@ -162,7 +162,7 @@ EMPTY_KEY=`);
162
162
  expect(envContent).not.toContain('OLD_KEY');
163
163
  });
164
164
  it('should return false if an error occurs during parsing', async () => {
165
- const { confirm } = await import('@inquirer/prompts');
165
+ const { confirm } = await import('#utils/prompt.js');
166
166
  vi.mocked(confirm).mockResolvedValue(true);
167
167
  await fs.writeFile(testState.envExamplePath, 'KEY=');
168
168
  // Delete the .env.example file after access check but before parsing would happen
@@ -178,7 +178,7 @@ EMPTY_KEY=`);
178
178
  vi.mocked(fs.copyFile).mockImplementation(originalCopyFile);
179
179
  });
180
180
  it('should prompt for SECRET marker values with password input', async () => {
181
- const { password, confirm } = await import('@inquirer/prompts');
181
+ const { password, confirm } = await import('#utils/prompt.js');
182
182
  vi.mocked(confirm).mockResolvedValue(true);
183
183
  vi.mocked(password).mockResolvedValueOnce('my-secret-api-key');
184
184
  await fs.writeFile(testState.envExamplePath, `# API Key
@@ -27,5 +27,5 @@ export declare function createSigintHandler(projectPath: string, folderCreated:
27
27
  * @param skipEnv - Whether to skip environment configuration prompts
28
28
  * @param folderName - Optional folder name to skip folder name prompt
29
29
  */
30
- export declare function runInit(skipEnv?: boolean, folderName?: string): Promise<void>;
30
+ export declare function runInit(skipEnv?: boolean, skipGit?: boolean, folderName?: string): Promise<void>;
31
31
  export {};
@@ -1,4 +1,4 @@
1
- import { input, confirm } from '@inquirer/prompts';
1
+ import { input, confirm } from '#utils/prompt.js';
2
2
  import { ux } from '@oclif/core';
3
3
  import { kebabCase, pascalCase } from 'change-case';
4
4
  import fs from 'node:fs/promises';
@@ -119,6 +119,18 @@ async function executeNpmInstall(projectPath) {
119
119
  async function initializeAgents(projectPath) {
120
120
  await initializeAgentConfig({ projectRoot: projectPath, force: false });
121
121
  }
122
+ async function maybeInitializeGit(projectPath) {
123
+ const shouldInit = await confirm({
124
+ message: 'Initialize a git repository?',
125
+ default: true
126
+ });
127
+ if (!shouldInit) {
128
+ return false;
129
+ }
130
+ return executeCommandWithMessages(async () => {
131
+ await executeCommand('git', ['init'], projectPath);
132
+ }, 'Initializing git repository...', 'Git repository initialized');
133
+ }
122
134
  /**
123
135
  * Format error message for init errors
124
136
  * Single responsibility: only format error messages, no cleanup logic
@@ -172,7 +184,7 @@ function handleRunInitError(error, projectPath, projectFolderCreated) {
172
184
  * @param skipEnv - Whether to skip environment configuration prompts
173
185
  * @param folderName - Optional folder name to skip folder name prompt
174
186
  */
175
- export async function runInit(skipEnv = false, folderName) {
187
+ export async function runInit(skipEnv = false, skipGit = false, folderName) {
176
188
  // Track state for SIGINT cleanup using an object to avoid let
177
189
  const state = {
178
190
  projectFolderCreated: false,
@@ -210,6 +222,9 @@ export async function runInit(skipEnv = false, folderName) {
210
222
  await fs.copyFile(path.join(config.projectPath, '.env.example'), path.join(config.projectPath, '.env'));
211
223
  await executeCommandWithMessages(() => initializeAgents(config.projectPath), 'Initializing agent system...', 'Agent system initialized');
212
224
  const installSuccess = await executeCommandWithMessages(() => executeNpmInstall(config.projectPath), 'Installing dependencies...', 'Dependencies installed');
225
+ if (!skipGit) {
226
+ await maybeInitializeGit(config.projectPath);
227
+ }
213
228
  const nextSteps = getProjectSuccessMessage(config.folderName, installSuccess, credentialsConfigured);
214
229
  ux.stdout('Project created successfully!');
215
230
  ux.stdout(nextSteps);
@@ -8,7 +8,7 @@ vi.mock('#utils/framework_version.js', () => ({
8
8
  })
9
9
  }));
10
10
  // Mock other dependencies
11
- vi.mock('@inquirer/prompts', () => ({
11
+ vi.mock('#utils/prompt.js', () => ({
12
12
  input: vi.fn(),
13
13
  confirm: vi.fn()
14
14
  }));
@@ -47,7 +47,7 @@ describe('project_scaffold', () => {
47
47
  });
48
48
  describe('getProjectConfig', () => {
49
49
  it('should skip all prompts when folderName is provided', async () => {
50
- const { input } = await import('@inquirer/prompts');
50
+ const { input } = await import('#utils/prompt.js');
51
51
  const config = await getProjectConfig('my-project');
52
52
  expect(config.folderName).toBe('my-project');
53
53
  expect(config.projectName).toBe('my-project');
@@ -58,7 +58,7 @@ describe('project_scaffold', () => {
58
58
  expect(config.description).toBe('AI Agents & Workflows built with Output.ai for test-folder');
59
59
  });
60
60
  it('should prompt for project name and folder name when not provided', async () => {
61
- const { input } = await import('@inquirer/prompts');
61
+ const { input } = await import('#utils/prompt.js');
62
62
  vi.mocked(input)
63
63
  .mockResolvedValueOnce('Test Project')
64
64
  .mockResolvedValueOnce('test-project');
@@ -73,7 +73,7 @@ describe('project_scaffold', () => {
73
73
  it('should not prompt when all dependencies are available', async () => {
74
74
  const { isDockerInstalled } = await import('#services/docker.js');
75
75
  const { isClaudeCliAvailable } = await import('#utils/claude.js');
76
- const { confirm } = await import('@inquirer/prompts');
76
+ const { confirm } = await import('#utils/prompt.js');
77
77
  vi.mocked(isDockerInstalled).mockReturnValue(true);
78
78
  vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
79
79
  await checkDependencies();
@@ -82,7 +82,7 @@ describe('project_scaffold', () => {
82
82
  it('should prompt user when docker is missing', async () => {
83
83
  const { isDockerInstalled } = await import('#services/docker.js');
84
84
  const { isClaudeCliAvailable } = await import('#utils/claude.js');
85
- const { confirm } = await import('@inquirer/prompts');
85
+ const { confirm } = await import('#utils/prompt.js');
86
86
  vi.mocked(isDockerInstalled).mockReturnValue(false);
87
87
  vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
88
88
  vi.mocked(confirm).mockResolvedValue(true);
@@ -94,7 +94,7 @@ describe('project_scaffold', () => {
94
94
  it('should throw UserCancelledError when user declines to proceed', async () => {
95
95
  const { isDockerInstalled } = await import('#services/docker.js');
96
96
  const { isClaudeCliAvailable } = await import('#utils/claude.js');
97
- const { confirm } = await import('@inquirer/prompts');
97
+ const { confirm } = await import('#utils/prompt.js');
98
98
  vi.mocked(isDockerInstalled).mockReturnValue(false);
99
99
  vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
100
100
  vi.mocked(confirm).mockResolvedValue(false);
@@ -2,7 +2,8 @@
2
2
  * Workflow builder service for implementing workflows from plan files
3
3
  */
4
4
  import { ADDITIONAL_INSTRUCTIONS, BUILD_COMMAND_OPTIONS, invokeBuildWorkflow as invokeBuildWorkflowFromClient, replyToClaude } from './claude_client.js';
5
- import { input } from '@inquirer/prompts';
5
+ import { input } from '#utils/prompt.js';
6
+ import { isInteractive } from '#utils/interactive.js';
6
7
  import { ux } from '@oclif/core';
7
8
  import fs from 'node:fs/promises';
8
9
  import path from 'node:path';
@@ -70,6 +71,9 @@ async function processModification(modification, currentOutput) {
70
71
  }
71
72
  }
72
73
  async function interactiveRefinementLoop(currentOutput) {
74
+ if (!isInteractive()) {
75
+ return currentOutput;
76
+ }
73
77
  const modification = await promptForModification();
74
78
  if (isAcceptCommand(modification)) {
75
79
  return currentOutput;
@@ -1,11 +1,12 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { buildWorkflow, buildWorkflowInteractiveLoop } from './workflow_builder.js';
3
3
  import { ADDITIONAL_INSTRUCTIONS, BUILD_COMMAND_OPTIONS, invokeBuildWorkflow, replyToClaude } from './claude_client.js';
4
- import { input } from '@inquirer/prompts';
4
+ import { input } from '#utils/prompt.js';
5
5
  import { ux } from '@oclif/core';
6
6
  import fs from 'node:fs/promises';
7
7
  vi.mock('./claude_client.js');
8
- vi.mock('@inquirer/prompts');
8
+ vi.mock('#utils/prompt.js');
9
+ vi.mock('#utils/interactive.js', () => ({ isInteractive: () => true }));
9
10
  vi.mock('@oclif/core', () => ({
10
11
  ux: {
11
12
  stdout: vi.fn(),
@@ -7,11 +7,10 @@ import { existsSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
8
  import * as dotenv from 'dotenv';
9
9
  import debugFactory from 'debug';
10
- import { config } from '#config.js';
11
10
  const debug = debugFactory('output-cli:env-loader');
12
11
  export function loadEnvironment() {
13
12
  const cwd = process.cwd();
14
- const envFile = config.envFile;
13
+ const envFile = process.env.OUTPUT_CLI_ENV || '.env';
15
14
  const envPath = resolve(cwd, envFile);
16
15
  if (!existsSync(envPath)) {
17
16
  debug(`Warning: Env file not found: ${envPath}`);
@@ -1,11 +1,13 @@
1
1
  import { config } from '#config.js';
2
- const DEFAULT_MESSAGES = {
3
- ECONNREFUSED: `Connection refused to ${config.apiUrl}. Is the API server running?`,
4
- 401: 'Authentication failed. Check your OUTPUT_API_AUTH_TOKEN.',
5
- 404: 'Resource not found.',
6
- 500: 'Server error.',
7
- UNKNOWN: 'An unknown error occurred.'
8
- };
2
+ function getDefaultMessages() {
3
+ return {
4
+ ECONNREFUSED: `Connection refused to ${config.apiUrl}. Is the API server running?`,
5
+ 401: 'Authentication failed. Check your OUTPUT_API_AUTH_TOKEN.',
6
+ 404: 'Resource not found.',
7
+ 500: 'Server error.',
8
+ UNKNOWN: 'An unknown error occurred.'
9
+ };
10
+ }
9
11
  /**
10
12
  * Extract error type and message from API response data
11
13
  */
@@ -49,7 +51,7 @@ function getDetailedErrorMessage(error) {
49
51
  }
50
52
  export function handleApiError(error, errorFn, overrides = {}) {
51
53
  const apiError = error;
52
- const errorMessages = { ...DEFAULT_MESSAGES, ...overrides };
54
+ const errorMessages = { ...getDefaultMessages(), ...overrides };
53
55
  if (apiError.code === 'ECONNREFUSED' || apiError.cause?.code === 'ECONNREFUSED') {
54
56
  errorFn(errorMessages.ECONNREFUSED, { exit: 1 });
55
57
  }
@@ -0,0 +1,2 @@
1
+ export declare const setNonInteractive: (value: boolean) => void;
2
+ export declare const isInteractive: () => boolean;
@@ -0,0 +1,5 @@
1
+ const state = { nonInteractive: false };
2
+ export const setNonInteractive = (value) => {
3
+ state.nonInteractive = value;
4
+ };
5
+ export const isInteractive = () => !state.nonInteractive && !!process.stdin.isTTY;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ describe('interactive', () => {
3
+ beforeEach(async () => {
4
+ // Re-import to reset singleton state
5
+ const mod = await import('./interactive.js');
6
+ mod.setNonInteractive(false);
7
+ });
8
+ it('isInteractive returns true by default when TTY is available', async () => {
9
+ const originalIsTTY = process.stdin.isTTY;
10
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
11
+ const { isInteractive } = await import('./interactive.js');
12
+ expect(isInteractive()).toBe(true);
13
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
14
+ });
15
+ it('isInteractive returns false when no TTY', async () => {
16
+ const originalIsTTY = process.stdin.isTTY;
17
+ Object.defineProperty(process.stdin, 'isTTY', { value: undefined, configurable: true });
18
+ const { isInteractive } = await import('./interactive.js');
19
+ expect(isInteractive()).toBe(false);
20
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
21
+ });
22
+ it('isInteractive returns false after setNonInteractive(true)', async () => {
23
+ const originalIsTTY = process.stdin.isTTY;
24
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
25
+ const { isInteractive, setNonInteractive } = await import('./interactive.js');
26
+ setNonInteractive(true);
27
+ expect(isInteractive()).toBe(false);
28
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
29
+ });
30
+ it('setNonInteractive(false) restores interactive mode', async () => {
31
+ const originalIsTTY = process.stdin.isTTY;
32
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
33
+ const { isInteractive, setNonInteractive } = await import('./interactive.js');
34
+ setNonInteractive(true);
35
+ expect(isInteractive()).toBe(false);
36
+ setNonInteractive(false);
37
+ expect(isInteractive()).toBe(true);
38
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
39
+ });
40
+ });
@@ -0,0 +1,17 @@
1
+ type ConfirmOptions = {
2
+ message: string;
3
+ default?: boolean;
4
+ };
5
+ type InputOptions = {
6
+ message: string;
7
+ default?: string;
8
+ validate?: (value: string) => boolean | string;
9
+ };
10
+ type PasswordOptions = {
11
+ message: string;
12
+ mask?: boolean;
13
+ };
14
+ export declare const confirm: (options: ConfirmOptions) => Promise<boolean>;
15
+ export declare const input: (options: InputOptions) => Promise<string>;
16
+ export declare const password: (options: PasswordOptions) => Promise<string>;
17
+ export {};
@@ -0,0 +1,20 @@
1
+ import { confirm as inquirerConfirm, input as inquirerInput, password as inquirerPassword } from '@inquirer/prompts';
2
+ import { isInteractive } from './interactive.js';
3
+ export const confirm = async (options) => {
4
+ if (!isInteractive()) {
5
+ return true;
6
+ }
7
+ return inquirerConfirm(options);
8
+ };
9
+ export const input = async (options) => {
10
+ if (!isInteractive()) {
11
+ return options.default ?? '';
12
+ }
13
+ return inquirerInput(options);
14
+ };
15
+ export const password = async (options) => {
16
+ if (!isInteractive()) {
17
+ return '';
18
+ }
19
+ return inquirerPassword(options);
20
+ };
@@ -0,0 +1 @@
1
+ export {};