@outputai/cli 0.1.13-next.f537949.0 → 0.2.1-dev.82acd68.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.
- package/bin/run.js +4 -2
- package/dist/api/generated/api.d.ts +160 -7
- package/dist/api/generated/api.js +33 -1
- package/dist/api/http_client.js +24 -19
- package/dist/assets/docker/docker-compose-dev.yml +6 -10
- package/dist/commands/credentials/set.d.ts +16 -0
- package/dist/commands/credentials/set.js +125 -0
- package/dist/commands/credentials/set.spec.d.ts +1 -0
- package/dist/commands/credentials/set.spec.js +160 -0
- package/dist/commands/dev/index.js +12 -1
- package/dist/commands/fix.js +1 -1
- package/dist/commands/fix.spec.js +2 -2
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +5 -1
- package/dist/commands/init.spec.js +10 -5
- package/dist/commands/migrate.d.ts +11 -0
- package/dist/commands/migrate.js +40 -0
- package/dist/commands/update.js +1 -1
- package/dist/commands/update.spec.js +2 -2
- package/dist/commands/workflow/plan.js +6 -2
- package/dist/commands/workflow/plan.spec.js +3 -2
- package/dist/commands/workflow/run.d.ts +1 -1
- package/dist/commands/workflow/run.js +8 -5
- package/dist/commands/workflow/run.spec.js +3 -3
- package/dist/commands/workflow/runs/list.d.ts +1 -0
- package/dist/commands/workflow/runs/list.js +7 -0
- package/dist/commands/workflow/start.d.ts +1 -1
- package/dist/commands/workflow/start.js +8 -5
- package/dist/commands/workflow/start.spec.js +1 -1
- package/dist/config.d.ts +11 -38
- package/dist/config.js +34 -42
- package/dist/config.spec.d.ts +1 -0
- package/dist/config.spec.js +129 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/hooks/init.d.ts +4 -0
- package/dist/hooks/init.js +17 -1
- package/dist/hooks/init.spec.js +79 -5
- package/dist/services/claude_client.d.ts +14 -2
- package/dist/services/claude_client.integration.test.js +2 -2
- package/dist/services/claude_client.js +38 -5
- package/dist/services/claude_client.spec.js +3 -3
- package/dist/services/coding_agents.js +5 -1
- package/dist/services/coding_agents.spec.js +19 -6
- package/dist/services/credentials_configurator.js +1 -1
- package/dist/services/docker.js +5 -2
- package/dist/services/docker.spec.js +74 -3
- package/dist/services/env_configurator.js +1 -1
- package/dist/services/env_configurator.spec.js +12 -12
- package/dist/services/messages.js +2 -1
- package/dist/services/project_scaffold.d.ts +1 -1
- package/dist/services/project_scaffold.js +17 -2
- package/dist/services/project_scaffold.spec.js +6 -6
- package/dist/services/workflow_builder.d.ts +1 -1
- package/dist/services/workflow_builder.js +6 -2
- package/dist/services/workflow_builder.spec.js +3 -2
- package/dist/services/workflow_runs.d.ts +1 -0
- package/dist/services/workflow_runs.js +3 -0
- package/dist/templates/project/.env.example.template +17 -0
- package/dist/utils/credentials_loader.d.ts +1 -0
- package/dist/utils/credentials_loader.js +18 -0
- package/dist/utils/credentials_loader.spec.d.ts +1 -0
- package/dist/utils/credentials_loader.spec.js +84 -0
- package/dist/utils/env_loader.js +1 -2
- package/dist/utils/error_handler.js +10 -8
- package/dist/utils/interactive.d.ts +2 -0
- package/dist/utils/interactive.js +5 -0
- package/dist/utils/interactive.spec.d.ts +1 -0
- package/dist/utils/interactive.spec.js +40 -0
- package/dist/utils/prompt.d.ts +17 -0
- package/dist/utils/prompt.js +20 -0
- package/dist/utils/prompt.spec.d.ts +1 -0
- package/dist/utils/prompt.spec.js +70 -0
- package/dist/utils/proxy.d.ts +9 -0
- package/dist/utils/proxy.js +24 -0
- package/dist/utils/proxy.spec.d.ts +1 -0
- package/dist/utils/proxy.spec.js +48 -0
- package/dist/utils/validation.d.ts +13 -0
- package/dist/utils/validation.js +31 -0
- package/dist/utils/validation.spec.js +47 -1
- package/dist/views/dev.js +3 -3
- package/dist/views/workflow/list.js +10 -8
- package/package.json +10 -9
package/dist/config.js
CHANGED
|
@@ -1,47 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { parsePort } from '#utils/validation.js';
|
|
2
|
+
const DEFAULT_API_PORT = 3001;
|
|
3
|
+
const DEFAULT_TEMPORAL_UI_PORT = 8080;
|
|
4
4
|
export const config = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
get apiUrl() {
|
|
6
|
+
return process.env.OUTPUT_API_URL || `http://localhost:${this.ports.api}`;
|
|
7
|
+
},
|
|
8
|
+
get ports() {
|
|
9
|
+
return {
|
|
10
|
+
temporalUi: parsePort(process.env.OUTPUT_TEMPORAL_UI_HOST_PORT, DEFAULT_TEMPORAL_UI_PORT, 'OUTPUT_TEMPORAL_UI_HOST_PORT'),
|
|
11
|
+
api: parsePort(process.env.OUTPUT_API_HOST_PORT, DEFAULT_API_PORT, 'OUTPUT_API_HOST_PORT')
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
get temporalUiUrl() {
|
|
15
|
+
return `http://localhost:${this.ports.temporalUi}`;
|
|
16
|
+
},
|
|
17
|
+
get apiToken() {
|
|
18
|
+
return process.env.OUTPUT_API_AUTH_TOKEN;
|
|
19
|
+
},
|
|
18
20
|
requestTimeout: 30000,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Where the env vars are stored, defaults to `.env`
|
|
30
|
-
*/
|
|
31
|
-
envFile: process.env.OUTPUT_CLI_ENV || '.env',
|
|
32
|
-
/**
|
|
33
|
-
* Agent configuration directory name
|
|
34
|
-
*/
|
|
21
|
+
get dockerServiceName() {
|
|
22
|
+
return process.env.DOCKER_SERVICE_NAME || 'output-sdk';
|
|
23
|
+
},
|
|
24
|
+
get debugMode() {
|
|
25
|
+
return process.env.OUTPUT_DEBUG === 'true';
|
|
26
|
+
},
|
|
27
|
+
get envFile() {
|
|
28
|
+
return process.env.OUTPUT_CLI_ENV || '.env';
|
|
29
|
+
},
|
|
35
30
|
agentConfigDir: '.outputai',
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
region: process.env.OUTPUT_AWS_REGION,
|
|
44
|
-
accessKeyId: process.env.OUTPUT_AWS_ACCESS_KEY_ID,
|
|
45
|
-
secretAccessKey: process.env.OUTPUT_AWS_SECRET_ACCESS_KEY
|
|
31
|
+
get s3() {
|
|
32
|
+
return {
|
|
33
|
+
bucket: process.env.OUTPUT_TRACE_REMOTE_S3_BUCKET,
|
|
34
|
+
region: process.env.OUTPUT_AWS_REGION,
|
|
35
|
+
accessKeyId: process.env.OUTPUT_AWS_ACCESS_KEY_ID,
|
|
36
|
+
secretAccessKey: process.env.OUTPUT_AWS_SECRET_ACCESS_KEY
|
|
37
|
+
};
|
|
46
38
|
}
|
|
47
39
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { config } from '#config.js';
|
|
3
|
+
import { InvalidPortError } from '#utils/validation.js';
|
|
4
|
+
describe('config', () => {
|
|
5
|
+
const envVars = [
|
|
6
|
+
'OUTPUT_API_URL',
|
|
7
|
+
'OUTPUT_API_HOST_PORT',
|
|
8
|
+
'OUTPUT_TEMPORAL_UI_HOST_PORT',
|
|
9
|
+
'OUTPUT_API_AUTH_TOKEN',
|
|
10
|
+
'DOCKER_SERVICE_NAME',
|
|
11
|
+
'OUTPUT_DEBUG',
|
|
12
|
+
'OUTPUT_CLI_ENV',
|
|
13
|
+
'OUTPUT_TRACE_REMOTE_S3_BUCKET',
|
|
14
|
+
'OUTPUT_AWS_REGION',
|
|
15
|
+
'OUTPUT_AWS_ACCESS_KEY_ID',
|
|
16
|
+
'OUTPUT_AWS_SECRET_ACCESS_KEY'
|
|
17
|
+
];
|
|
18
|
+
const saved = {};
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
for (const key of envVars) {
|
|
21
|
+
saved[key] = process.env[key];
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
for (const key of envVars) {
|
|
26
|
+
if (saved[key] === undefined) {
|
|
27
|
+
delete process.env[key];
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
process.env[key] = saved[key];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
it('reads env vars lazily, not at module evaluation time', () => {
|
|
35
|
+
process.env.OUTPUT_API_URL = 'https://lazy-test.example.com';
|
|
36
|
+
expect(config.apiUrl).toBe('https://lazy-test.example.com');
|
|
37
|
+
process.env.OUTPUT_API_URL = 'https://changed.example.com';
|
|
38
|
+
expect(config.apiUrl).toBe('https://changed.example.com');
|
|
39
|
+
});
|
|
40
|
+
it('falls back to defaults when env vars are unset', () => {
|
|
41
|
+
delete process.env.OUTPUT_API_URL;
|
|
42
|
+
delete process.env.OUTPUT_API_HOST_PORT;
|
|
43
|
+
delete process.env.OUTPUT_TEMPORAL_UI_HOST_PORT;
|
|
44
|
+
delete process.env.DOCKER_SERVICE_NAME;
|
|
45
|
+
delete process.env.OUTPUT_DEBUG;
|
|
46
|
+
delete process.env.OUTPUT_CLI_ENV;
|
|
47
|
+
expect(config.apiUrl).toBe('http://localhost:3001');
|
|
48
|
+
expect(config.ports).toEqual({ temporalUi: 8080, api: 3001 });
|
|
49
|
+
expect(config.temporalUiUrl).toBe('http://localhost:8080');
|
|
50
|
+
expect(config.dockerServiceName).toBe('output-sdk');
|
|
51
|
+
expect(config.debugMode).toBe(false);
|
|
52
|
+
expect(config.envFile).toBe('.env');
|
|
53
|
+
});
|
|
54
|
+
it('derives apiUrl from OUTPUT_API_HOST_PORT when OUTPUT_API_URL is unset', () => {
|
|
55
|
+
delete process.env.OUTPUT_API_URL;
|
|
56
|
+
process.env.OUTPUT_API_HOST_PORT = '3002';
|
|
57
|
+
expect(config.apiUrl).toBe('http://localhost:3002');
|
|
58
|
+
});
|
|
59
|
+
it('OUTPUT_API_URL takes precedence over OUTPUT_API_HOST_PORT', () => {
|
|
60
|
+
process.env.OUTPUT_API_URL = 'https://api.example.com';
|
|
61
|
+
process.env.OUTPUT_API_HOST_PORT = '3002';
|
|
62
|
+
expect(config.apiUrl).toBe('https://api.example.com');
|
|
63
|
+
});
|
|
64
|
+
it('empty-string OUTPUT_API_URL falls through to OUTPUT_API_HOST_PORT (|| not ??)', () => {
|
|
65
|
+
process.env.OUTPUT_API_URL = '';
|
|
66
|
+
process.env.OUTPUT_API_HOST_PORT = '3002';
|
|
67
|
+
expect(config.apiUrl).toBe('http://localhost:3002');
|
|
68
|
+
});
|
|
69
|
+
it('reads port overrides from env vars', () => {
|
|
70
|
+
process.env.OUTPUT_TEMPORAL_UI_HOST_PORT = '8081';
|
|
71
|
+
process.env.OUTPUT_API_HOST_PORT = '3002';
|
|
72
|
+
expect(config.ports).toEqual({ temporalUi: 8081, api: 3002 });
|
|
73
|
+
expect(config.temporalUiUrl).toBe('http://localhost:8081');
|
|
74
|
+
});
|
|
75
|
+
it('treats empty-string port env vars as unset (matches Compose semantics)', () => {
|
|
76
|
+
delete process.env.OUTPUT_API_URL;
|
|
77
|
+
process.env.OUTPUT_API_HOST_PORT = '';
|
|
78
|
+
process.env.OUTPUT_TEMPORAL_UI_HOST_PORT = '';
|
|
79
|
+
expect(config.apiUrl).toBe('http://localhost:3001');
|
|
80
|
+
expect(config.ports).toEqual({ temporalUi: 8080, api: 3001 });
|
|
81
|
+
expect(config.temporalUiUrl).toBe('http://localhost:8080');
|
|
82
|
+
});
|
|
83
|
+
it('throws InvalidPortError on non-numeric port values', () => {
|
|
84
|
+
delete process.env.OUTPUT_API_URL;
|
|
85
|
+
process.env.OUTPUT_API_HOST_PORT = 'abc';
|
|
86
|
+
expect(() => config.ports).toThrow(InvalidPortError);
|
|
87
|
+
expect(() => config.apiUrl).toThrow(InvalidPortError);
|
|
88
|
+
});
|
|
89
|
+
it('throws InvalidPortError on out-of-range port values', () => {
|
|
90
|
+
process.env.OUTPUT_API_HOST_PORT = '99999';
|
|
91
|
+
expect(() => config.ports).toThrow(InvalidPortError);
|
|
92
|
+
});
|
|
93
|
+
it('throws InvalidPortError on trailing-junk port values', () => {
|
|
94
|
+
process.env.OUTPUT_TEMPORAL_UI_HOST_PORT = '8080abc';
|
|
95
|
+
expect(() => config.ports).toThrow(InvalidPortError);
|
|
96
|
+
});
|
|
97
|
+
it('throws InvalidPortError on port 0 (Compose treats 0 as ephemeral - prevents CLI/Docker desync)', () => {
|
|
98
|
+
process.env.OUTPUT_API_HOST_PORT = '0';
|
|
99
|
+
expect(() => config.ports).toThrow(InvalidPortError);
|
|
100
|
+
});
|
|
101
|
+
it('reads apiToken from env', () => {
|
|
102
|
+
process.env.OUTPUT_API_AUTH_TOKEN = 'test-token-123';
|
|
103
|
+
expect(config.apiToken).toBe('test-token-123');
|
|
104
|
+
delete process.env.OUTPUT_API_AUTH_TOKEN;
|
|
105
|
+
expect(config.apiToken).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
it('reads debugMode as boolean', () => {
|
|
108
|
+
process.env.OUTPUT_DEBUG = 'true';
|
|
109
|
+
expect(config.debugMode).toBe(true);
|
|
110
|
+
process.env.OUTPUT_DEBUG = 'false';
|
|
111
|
+
expect(config.debugMode).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
it('reads s3 config lazily', () => {
|
|
114
|
+
process.env.OUTPUT_TRACE_REMOTE_S3_BUCKET = 'my-bucket';
|
|
115
|
+
process.env.OUTPUT_AWS_REGION = 'us-west-2';
|
|
116
|
+
process.env.OUTPUT_AWS_ACCESS_KEY_ID = 'AKIA123';
|
|
117
|
+
process.env.OUTPUT_AWS_SECRET_ACCESS_KEY = 'secret123';
|
|
118
|
+
expect(config.s3).toEqual({
|
|
119
|
+
bucket: 'my-bucket',
|
|
120
|
+
region: 'us-west-2',
|
|
121
|
+
accessKeyId: 'AKIA123',
|
|
122
|
+
secretAccessKey: 'secret123'
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
it('has static properties that are not env-derived', () => {
|
|
126
|
+
expect(config.requestTimeout).toBe(30000);
|
|
127
|
+
expect(config.agentConfigDir).toBe('.outputai');
|
|
128
|
+
});
|
|
129
|
+
});
|
package/dist/hooks/init.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import { Hook } from '@oclif/core';
|
|
2
|
+
export declare const INTERACTIVE_FLAGS: string[];
|
|
3
|
+
export declare const GLOBAL_FLAGS: Set<string>;
|
|
4
|
+
export declare const hasInteractiveFlag: (argv: string[]) => boolean;
|
|
5
|
+
export declare const stripGlobalFlags: (argv: string[]) => void;
|
|
2
6
|
declare const hook: Hook<'init'>;
|
|
3
7
|
export default hook;
|
package/dist/hooks/init.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { ux } from '@oclif/core';
|
|
2
2
|
import { checkForUpdate } from '#services/version_check.js';
|
|
3
|
-
|
|
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) {
|
package/dist/hooks/init.spec.js
CHANGED
|
@@ -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
|
});
|
|
@@ -5,6 +5,7 @@ import { Options } from '@anthropic-ai/claude-agent-sdk';
|
|
|
5
5
|
export declare const ADDITIONAL_INSTRUCTIONS: {
|
|
6
6
|
readonly PLAN: "\n! IMPORTANT !\n1. Use TodoWrite to track your progress through plan creation.\n\n2. Please respond with only the final version of the plan content.\n\n3. Respond in a markdown format with these metadata headers:\n\n---\ntitle: <plan-title>\ndescription: <plan-description>\ndate: <plan-date>\n---\n\n<plan-content>\n\n4. After you mark all todos as complete, you must respond with the final version of the plan.\n\n5. DO NOT write the plan to disk — the CLI will handle saving the file to the plans directory.\n\n6. DO NOT suggest any next steps, follow-up commands, or instructions for the user — the CLI will inform the user of next steps after saving.\n";
|
|
7
7
|
readonly BUILD: "\n! IMPORTANT !\n1. Use TodoWrite to track your progress through workflow implementation.\n\n2. Follow the implementation plan exactly as specified in the plan file.\n\n3. Implement all workflow files following Output.ai patterns and best practices.\n\n4. After you mark all todos as complete, provide a summary of what was implemented.\n";
|
|
8
|
+
readonly MIGRATE: "\n! IMPORTANT !\n1. Use TodoWrite to track your progress through the migration.\n\n2. Fetch the migration guide from https://docs.output.ai/migrations — do not invent migration steps from memory.\n\n3. If the specific guide URL 404s, fall back to the /migrations index and chain the guides that cover the version range.\n\n4. Confirm the planned changes with the user before editing files.\n\n5. After you mark all todos as complete, provide a summary of which files changed and whether the type check passed.\n";
|
|
8
9
|
};
|
|
9
10
|
export declare const PLAN_COMMAND_OPTIONS: Options;
|
|
10
11
|
interface ReplyToClaudeOptions {
|
|
@@ -12,13 +13,14 @@ interface ReplyToClaudeOptions {
|
|
|
12
13
|
applyAdditionalInstructions?: string;
|
|
13
14
|
}
|
|
14
15
|
export declare const BUILD_COMMAND_OPTIONS: Options;
|
|
16
|
+
export declare const MIGRATE_COMMAND_OPTIONS: Options;
|
|
15
17
|
export declare class ClaudeInvocationError extends Error {
|
|
16
18
|
cause?: Error | undefined;
|
|
17
19
|
constructor(message: string, cause?: Error | undefined);
|
|
18
20
|
}
|
|
19
21
|
export declare function replyToClaude(message: string, { anthropicOpts, applyAdditionalInstructions }?: ReplyToClaudeOptions): Promise<string>;
|
|
20
22
|
/**
|
|
21
|
-
* Invoke claude-code with /
|
|
23
|
+
* Invoke claude-code with /output-plan-workflow slash command
|
|
22
24
|
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
23
25
|
* ensureOutputAISystem() scaffolds the command files to that location.
|
|
24
26
|
* @param description - Workflow description
|
|
@@ -26,7 +28,7 @@ export declare function replyToClaude(message: string, { anthropicOpts, applyAdd
|
|
|
26
28
|
*/
|
|
27
29
|
export declare function invokePlanWorkflow(description: string): Promise<string>;
|
|
28
30
|
/**
|
|
29
|
-
* Invoke claude-code with /
|
|
31
|
+
* Invoke claude-code with /output-build-workflow slash command
|
|
30
32
|
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
31
33
|
* ensureOutputAISystem() scaffolds the command files to that location.
|
|
32
34
|
* @param planFilePath - Absolute path to the plan file
|
|
@@ -36,4 +38,14 @@ export declare function invokePlanWorkflow(description: string): Promise<string>
|
|
|
36
38
|
* @returns Implementation output from claude-code
|
|
37
39
|
*/
|
|
38
40
|
export declare function invokeBuildWorkflow(planFilePath: string, workflowDir: string, workflowName: string, additionalInstructions?: string): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Invoke claude-code with /output-migrate slash command (registered via the output-migrate skill).
|
|
43
|
+
* The slash command fetches migration instructions from docs.output.ai —
|
|
44
|
+
* this CLI wrapper just passes through the version arguments.
|
|
45
|
+
* @param fromVersion - Current framework version (empty string = auto-detect)
|
|
46
|
+
* @param toVersion - Target version (empty string = use npm "latest")
|
|
47
|
+
* @param additionalInstructions - Optional user-supplied guidance
|
|
48
|
+
* @returns Migration summary from claude-code
|
|
49
|
+
*/
|
|
50
|
+
export declare function invokeMigrate(fromVersion: string, toVersion: string, additionalInstructions?: string): Promise<string>;
|
|
39
51
|
export {};
|
|
@@ -15,7 +15,7 @@ describe('invokePlanWorkflow - Integration Tests', () => {
|
|
|
15
15
|
const messages = [];
|
|
16
16
|
try {
|
|
17
17
|
for await (const message of query({
|
|
18
|
-
prompt: `/
|
|
18
|
+
prompt: `/output-plan-workflow ${description}`,
|
|
19
19
|
options: { maxTurns: 1 }
|
|
20
20
|
})) {
|
|
21
21
|
console.log('\nReceived message:', JSON.stringify(message, null, 2));
|
|
@@ -31,7 +31,7 @@ describe('invokePlanWorkflow - Integration Tests', () => {
|
|
|
31
31
|
// This test is just for debugging - we expect messages
|
|
32
32
|
expect(messages.length).toBeGreaterThan(0);
|
|
33
33
|
}, 60000); // 60 second timeout
|
|
34
|
-
it('should successfully invoke /
|
|
34
|
+
it('should successfully invoke /output-plan-workflow slash command and return content', async () => {
|
|
35
35
|
const description = 'Simple workflow that takes a number and doubles it';
|
|
36
36
|
const result = await invokePlanWorkflow(description);
|
|
37
37
|
console.log('\n===== PLAN RESULT =====');
|
|
@@ -38,10 +38,25 @@ date: <plan-date>
|
|
|
38
38
|
3. Implement all workflow files following Output.ai patterns and best practices.
|
|
39
39
|
|
|
40
40
|
4. After you mark all todos as complete, provide a summary of what was implemented.
|
|
41
|
+
`,
|
|
42
|
+
MIGRATE: `
|
|
43
|
+
! IMPORTANT !
|
|
44
|
+
1. Use TodoWrite to track your progress through the migration.
|
|
45
|
+
|
|
46
|
+
2. Fetch the migration guide from https://docs.output.ai/migrations — do not invent migration steps from memory.
|
|
47
|
+
|
|
48
|
+
3. If the specific guide URL 404s, fall back to the /migrations index and chain the guides that cover the version range.
|
|
49
|
+
|
|
50
|
+
4. Confirm the planned changes with the user before editing files.
|
|
51
|
+
|
|
52
|
+
5. After you mark all todos as complete, provide a summary of which files changed and whether the type check passed.
|
|
41
53
|
`
|
|
42
54
|
};
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
// Slash-command naming convention used by the outputai plugin:
|
|
56
|
+
// `outputai:<kebab-name>` — skills under coding_assistants/.../skills/, which surface as top-level slash commands without the plugin prefix.
|
|
57
|
+
const PLAN_COMMAND = 'outputai:output-plan-workflow';
|
|
58
|
+
const BUILD_COMMAND = 'outputai:output-build-workflow';
|
|
59
|
+
const MIGRATE_COMMAND = 'outputai:output-migrate';
|
|
45
60
|
const GLOBAL_CLAUDE_OPTIONS = {
|
|
46
61
|
settingSources: ['user', 'project', 'local']
|
|
47
62
|
};
|
|
@@ -51,6 +66,9 @@ export const PLAN_COMMAND_OPTIONS = {
|
|
|
51
66
|
export const BUILD_COMMAND_OPTIONS = {
|
|
52
67
|
permissionMode: 'bypassPermissions'
|
|
53
68
|
};
|
|
69
|
+
export const MIGRATE_COMMAND_OPTIONS = {
|
|
70
|
+
permissionMode: 'bypassPermissions'
|
|
71
|
+
};
|
|
54
72
|
export class ClaudeInvocationError extends Error {
|
|
55
73
|
cause;
|
|
56
74
|
constructor(message, cause) {
|
|
@@ -77,7 +95,7 @@ function validateEnvironment() {
|
|
|
77
95
|
}
|
|
78
96
|
}
|
|
79
97
|
function validateSystem(systemMessage) {
|
|
80
|
-
const requiredCommands = [PLAN_COMMAND, BUILD_COMMAND];
|
|
98
|
+
const requiredCommands = [PLAN_COMMAND, BUILD_COMMAND, MIGRATE_COMMAND];
|
|
81
99
|
const availableCommands = systemMessage.slash_commands;
|
|
82
100
|
const missingCommands = requiredCommands.filter(command => !availableCommands.includes(command));
|
|
83
101
|
return {
|
|
@@ -193,7 +211,7 @@ export async function replyToClaude(message, { anthropicOpts, applyAdditionalIns
|
|
|
193
211
|
return singleQuery(applyInstructions(message, applyAdditionalInstructions), { continue: true, ...anthropicOpts });
|
|
194
212
|
}
|
|
195
213
|
/**
|
|
196
|
-
* Invoke claude-code with /
|
|
214
|
+
* Invoke claude-code with /output-plan-workflow slash command
|
|
197
215
|
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
198
216
|
* ensureOutputAISystem() scaffolds the command files to that location.
|
|
199
217
|
* @param description - Workflow description
|
|
@@ -203,7 +221,7 @@ export async function invokePlanWorkflow(description) {
|
|
|
203
221
|
return singleQuery(applyInstructions(`/${PLAN_COMMAND} ${description}`, ADDITIONAL_INSTRUCTIONS.PLAN), PLAN_COMMAND_OPTIONS);
|
|
204
222
|
}
|
|
205
223
|
/**
|
|
206
|
-
* Invoke claude-code with /
|
|
224
|
+
* Invoke claude-code with /output-build-workflow slash command
|
|
207
225
|
* The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
|
|
208
226
|
* ensureOutputAISystem() scaffolds the command files to that location.
|
|
209
227
|
* @param planFilePath - Absolute path to the plan file
|
|
@@ -219,3 +237,18 @@ export async function invokeBuildWorkflow(planFilePath, workflowDir, workflowNam
|
|
|
219
237
|
`/${BUILD_COMMAND} ${commandArgs}`;
|
|
220
238
|
return singleQuery(applyInstructions(fullCommand, ADDITIONAL_INSTRUCTIONS.BUILD), BUILD_COMMAND_OPTIONS);
|
|
221
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Invoke claude-code with /output-migrate slash command (registered via the output-migrate skill).
|
|
242
|
+
* The slash command fetches migration instructions from docs.output.ai —
|
|
243
|
+
* this CLI wrapper just passes through the version arguments.
|
|
244
|
+
* @param fromVersion - Current framework version (empty string = auto-detect)
|
|
245
|
+
* @param toVersion - Target version (empty string = use npm "latest")
|
|
246
|
+
* @param additionalInstructions - Optional user-supplied guidance
|
|
247
|
+
* @returns Migration summary from claude-code
|
|
248
|
+
*/
|
|
249
|
+
export async function invokeMigrate(fromVersion, toVersion, additionalInstructions) {
|
|
250
|
+
const from = fromVersion || 'auto';
|
|
251
|
+
const to = toVersion || 'latest';
|
|
252
|
+
const commandArgs = [from, to, additionalInstructions].filter(Boolean).join(' ');
|
|
253
|
+
return singleQuery(applyInstructions(`/${MIGRATE_COMMAND} ${commandArgs}`, ADDITIONAL_INSTRUCTIONS.MIGRATE), MIGRATE_COMMAND_OPTIONS);
|
|
254
|
+
}
|
|
@@ -13,7 +13,7 @@ describe('invokePlanWorkflow', () => {
|
|
|
13
13
|
// Clean up environment variables
|
|
14
14
|
delete process.env.ANTHROPIC_API_KEY;
|
|
15
15
|
});
|
|
16
|
-
it('should invoke /outputai:
|
|
16
|
+
it('should invoke /outputai:output-plan-workflow slash command with settingSources', async () => {
|
|
17
17
|
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
18
18
|
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
19
19
|
async function* mockIterator() {
|
|
@@ -22,7 +22,7 @@ describe('invokePlanWorkflow', () => {
|
|
|
22
22
|
vi.mocked(query).mockReturnValue(mockIterator());
|
|
23
23
|
await invokePlanWorkflow('Test workflow');
|
|
24
24
|
const calls = vi.mocked(query).mock.calls;
|
|
25
|
-
expect(calls[0]?.[0]?.prompt).toContain('/outputai:
|
|
25
|
+
expect(calls[0]?.[0]?.prompt).toContain('/outputai:output-plan-workflow Test workflow');
|
|
26
26
|
expect(calls[0]?.[0]?.options?.settingSources).toEqual(['user', 'project', 'local']);
|
|
27
27
|
expect(calls[0]?.[0]?.options?.allowedTools).toEqual(['Read', 'Grep', 'WebSearch', 'WebFetch', 'TodoWrite']);
|
|
28
28
|
});
|
|
@@ -36,7 +36,7 @@ describe('invokePlanWorkflow', () => {
|
|
|
36
36
|
const description = 'Build a user authentication system';
|
|
37
37
|
await invokePlanWorkflow(description);
|
|
38
38
|
const calls = vi.mocked(query).mock.calls;
|
|
39
|
-
expect(calls[0]?.[0]?.prompt).toContain(`/outputai:
|
|
39
|
+
expect(calls[0]?.[0]?.prompt).toContain(`/outputai:output-plan-workflow ${description}`);
|
|
40
40
|
expect(calls[0]?.[0]?.options?.settingSources).toEqual(['user', 'project', 'local']);
|
|
41
41
|
});
|
|
42
42
|
it('should return plan output from claude-code', async () => {
|
|
@@ -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 '
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
});
|