@outputai/cli 0.1.12-next.ecd4dda.0 → 0.1.13-dev.01b8dea.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 +2 -0
- package/dist/api/generated/api.d.ts +4 -0
- package/dist/assets/config/costs.yml +6 -0
- package/dist/assets/docker/docker-compose-dev.yml +2 -8
- package/dist/commands/fix.d.ts +10 -0
- package/dist/commands/fix.js +58 -0
- package/dist/commands/fix.spec.d.ts +1 -0
- package/dist/commands/fix.spec.js +77 -0
- package/dist/commands/update.js +1 -1
- package/dist/commands/update.spec.js +2 -2
- package/dist/commands/workflow/generate.js +3 -1
- package/dist/commands/workflow/generate.spec.js +12 -0
- package/dist/commands/workflow/list.d.ts +1 -0
- package/dist/commands/workflow/list.js +12 -6
- package/dist/commands/workflow/list.spec.js +21 -0
- package/dist/commands/workflow/plan.js +5 -1
- package/dist/commands/workflow/plan.spec.js +3 -2
- package/dist/commands/workflow/run.js +2 -1
- package/dist/commands/workflow/run.spec.js +1 -0
- package/dist/commands/workflow/start.js +2 -1
- package/dist/commands/workflow/start.spec.js +1 -0
- package/dist/components/command_footer.d.ts +8 -0
- package/dist/components/command_footer.js +4 -0
- package/dist/components/status_icon.d.ts +11 -0
- package/dist/components/status_icon.js +25 -0
- package/dist/components/workflow_summary.d.ts +10 -0
- package/dist/components/workflow_summary.js +4 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/hooks/init.js +4 -0
- package/dist/services/claude_client.js +4 -1
- package/dist/services/coding_agents.js +1 -1
- package/dist/services/coding_agents.spec.js +6 -6
- package/dist/services/credentials_configurator.js +1 -1
- package/dist/services/docker.d.ts +1 -3
- package/dist/services/docker.js +38 -13
- package/dist/services/env_configurator.js +1 -1
- package/dist/services/env_configurator.spec.js +12 -12
- package/dist/services/fix_package.d.ts +34 -0
- package/dist/services/fix_package.js +105 -0
- package/dist/services/fix_package.spec.d.ts +1 -0
- package/dist/services/fix_package.spec.js +63 -0
- package/dist/services/messages.d.ts +1 -1
- package/dist/services/messages.js +2 -2
- package/dist/services/project_scaffold.js +2 -2
- package/dist/services/project_scaffold.spec.js +6 -6
- package/dist/services/workflow_builder.js +5 -1
- package/dist/services/workflow_builder.spec.js +3 -2
- package/dist/templates/agent_instructions/CLAUDE.md.template +36 -2
- package/dist/templates/agent_instructions/dotclaude/settings.json.template +2 -3
- package/dist/templates/project/README.md.template +2 -2
- package/dist/templates/project/package.json.template +8 -7
- package/dist/utils/date_formatter.d.ts +11 -1
- package/dist/utils/date_formatter.js +26 -1
- 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/open_url.d.ts +1 -0
- package/dist/utils/open_url.js +12 -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 +74 -0
- package/dist/utils/proxy.d.ts +1 -0
- package/dist/utils/proxy.js +9 -0
- package/dist/utils/proxy.spec.d.ts +1 -0
- package/dist/utils/proxy.spec.js +39 -0
- package/dist/utils/workflow_dir_parser.d.ts +5 -0
- package/dist/utils/workflow_dir_parser.js +39 -0
- package/dist/utils/workflow_dir_parser.spec.d.ts +1 -0
- package/dist/utils/workflow_dir_parser.spec.js +74 -0
- package/dist/views/dev.js +70 -27
- package/dist/views/workflow/list.d.ts +6 -0
- package/dist/views/workflow/list.js +127 -0
- package/package.json +12 -11
|
@@ -19,7 +19,7 @@ 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
25
|
describe('coding_agents service', () => {
|
|
@@ -157,7 +157,7 @@ describe('coding_agents service', () => {
|
|
|
157
157
|
});
|
|
158
158
|
it('should show error and prompt user when plugin commands fail', async () => {
|
|
159
159
|
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
160
|
-
const { confirm } = await import('
|
|
160
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
161
161
|
vi.mocked(executeClaudeCommand)
|
|
162
162
|
.mockResolvedValueOnce(undefined) // marketplace add
|
|
163
163
|
.mockRejectedValueOnce(new Error('Plugin update failed')); // marketplace update
|
|
@@ -169,7 +169,7 @@ describe('coding_agents service', () => {
|
|
|
169
169
|
});
|
|
170
170
|
it('should allow user to proceed without plugin setup if they confirm', async () => {
|
|
171
171
|
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
172
|
-
const { confirm } = await import('
|
|
172
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
173
173
|
vi.mocked(executeClaudeCommand)
|
|
174
174
|
.mockRejectedValue(new Error('All plugin commands fail'));
|
|
175
175
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
@@ -219,7 +219,7 @@ describe('coding_agents service', () => {
|
|
|
219
219
|
});
|
|
220
220
|
it('should show error and prompt user when registerPluginMarketplace fails', async () => {
|
|
221
221
|
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
222
|
-
const { confirm } = await import('
|
|
222
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
223
223
|
vi.mocked(executeClaudeCommand)
|
|
224
224
|
.mockResolvedValueOnce(undefined) // marketplace add
|
|
225
225
|
.mockRejectedValueOnce(new Error('Plugin update failed')); // marketplace update
|
|
@@ -231,7 +231,7 @@ describe('coding_agents service', () => {
|
|
|
231
231
|
});
|
|
232
232
|
it('should show error and prompt user when installOutputAIPlugin fails', async () => {
|
|
233
233
|
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
234
|
-
const { confirm } = await import('
|
|
234
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
235
235
|
vi.mocked(executeClaudeCommand)
|
|
236
236
|
.mockResolvedValueOnce(undefined) // marketplace add
|
|
237
237
|
.mockResolvedValueOnce(undefined) // marketplace update
|
|
@@ -244,7 +244,7 @@ describe('coding_agents service', () => {
|
|
|
244
244
|
});
|
|
245
245
|
it('should allow user to proceed without plugin setup if they confirm', async () => {
|
|
246
246
|
const { executeClaudeCommand } = await import('../utils/claude.js');
|
|
247
|
-
const { confirm } = await import('
|
|
247
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
248
248
|
vi.mocked(executeClaudeCommand)
|
|
249
249
|
.mockRejectedValue(new Error('All plugin commands fail'));
|
|
250
250
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
@@ -21,8 +21,6 @@ export declare class DockerComposeConfigNotFoundError extends Error {
|
|
|
21
21
|
constructor(dockerComposePath: string);
|
|
22
22
|
}
|
|
23
23
|
declare const isDockerInstalled: () => boolean;
|
|
24
|
-
declare const isDockerComposeAvailable: () => boolean;
|
|
25
|
-
declare const isDockerDaemonRunning: () => boolean;
|
|
26
24
|
export declare function validateDockerEnvironment(): void;
|
|
27
25
|
export declare function getDefaultDockerComposePath(): string;
|
|
28
26
|
export declare function parseServiceStatus(jsonOutput: string): ServiceStatus[];
|
|
@@ -37,4 +35,4 @@ export type PullPolicy = 'always' | 'missing' | 'never';
|
|
|
37
35
|
export declare function startDockerCompose(dockerComposePath: string, pullPolicy?: PullPolicy): Promise<DockerComposeProcess>;
|
|
38
36
|
export declare function startDockerComposeDetached(dockerComposePath: string, pullPolicy?: PullPolicy): void;
|
|
39
37
|
export declare function stopDockerCompose(dockerComposePath: string): Promise<void>;
|
|
40
|
-
export { isDockerInstalled,
|
|
38
|
+
export { isDockerInstalled, DockerValidationError };
|
package/dist/services/docker.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFileSync, execSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { ux } from '@oclif/core';
|
|
5
|
+
import semver from 'semver';
|
|
5
6
|
const DEFAULT_COMPOSE_PATH = '../assets/docker/docker-compose-dev.yml';
|
|
6
7
|
export const SERVICE_HEALTH = {
|
|
7
8
|
HEALTHY: 'healthy',
|
|
@@ -30,27 +31,51 @@ const checkDockerCommand = (command) => {
|
|
|
30
31
|
return false;
|
|
31
32
|
}
|
|
32
33
|
};
|
|
34
|
+
const getCommandVersion = (command, pattern = /(\d+\.\d+\.\d+)/) => {
|
|
35
|
+
try {
|
|
36
|
+
const output = execSync(command, { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
37
|
+
const match = output.match(pattern);
|
|
38
|
+
return match ? match[1] : null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
33
44
|
const isDockerInstalled = () => checkDockerCommand('docker --version');
|
|
34
|
-
const
|
|
35
|
-
const isDockerDaemonRunning = () => checkDockerCommand('docker ps');
|
|
36
|
-
const DOCKER_VALIDATIONS = [
|
|
45
|
+
const PREREQUISITES = [
|
|
37
46
|
{
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
name: 'Docker',
|
|
48
|
+
semverRange: '>=20.0.0',
|
|
49
|
+
getVersion: () => getCommandVersion('docker --version'),
|
|
50
|
+
errorMessage: (current, required) => current === null ?
|
|
51
|
+
'Docker is not installed. Please install Docker to use the dev command.\nVisit: https://docs.docker.com/get-docker/' :
|
|
52
|
+
`Docker version ${required} is required (found v${current}).\nVisit: https://docs.docker.com/get-docker/`
|
|
40
53
|
},
|
|
41
54
|
{
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
name: 'Docker Compose',
|
|
56
|
+
semverRange: '>=2.24.0',
|
|
57
|
+
getVersion: () => getCommandVersion('docker compose version --short'),
|
|
58
|
+
errorMessage: (current, required) => current === null ?
|
|
59
|
+
'Docker Compose is not installed. Please install Docker Compose to use the dev command.\nVisit: https://docs.docker.com/compose/install/' :
|
|
60
|
+
`Docker Compose ${required} is required (found v${current}).\nPlease update Docker Compose: https://docs.docker.com/compose/install/`
|
|
44
61
|
},
|
|
45
62
|
{
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
name: 'Docker Daemon',
|
|
64
|
+
semverRange: '*',
|
|
65
|
+
getVersion: () => checkDockerCommand('docker ps') ? '0.0.0' : null,
|
|
66
|
+
errorMessage: () => 'Docker daemon is not running. Please start Docker and try again.'
|
|
48
67
|
}
|
|
49
68
|
];
|
|
50
69
|
export function validateDockerEnvironment() {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
for (const prereq of PREREQUISITES) {
|
|
71
|
+
const raw = prereq.getVersion();
|
|
72
|
+
const version = raw ? semver.valid(semver.coerce(raw)) : null;
|
|
73
|
+
if (!version) {
|
|
74
|
+
throw new DockerValidationError(prereq.errorMessage(null, prereq.semverRange));
|
|
75
|
+
}
|
|
76
|
+
if (!semver.satisfies(version, prereq.semverRange)) {
|
|
77
|
+
throw new DockerValidationError(prereq.errorMessage(version, prereq.semverRange));
|
|
78
|
+
}
|
|
54
79
|
}
|
|
55
80
|
}
|
|
56
81
|
export function getDefaultDockerComposePath() {
|
|
@@ -129,4 +154,4 @@ export async function stopDockerCompose(dockerComposePath) {
|
|
|
129
154
|
ux.stdout('⏹️ Stopping services...\n');
|
|
130
155
|
execFileSync('docker', ['compose', '-f', dockerComposePath, 'down'], { stdio: 'inherit', cwd: process.cwd() });
|
|
131
156
|
}
|
|
132
|
-
export { isDockerInstalled,
|
|
157
|
+
export { isDockerInstalled, DockerValidationError };
|
|
@@ -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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Legacy script names from older versions of the project */
|
|
2
|
+
export declare const legacyScripts: readonly ["dev"];
|
|
3
|
+
export interface ScriptToRemove {
|
|
4
|
+
key: string;
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ScriptToAdd {
|
|
8
|
+
key: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ScriptToReplace {
|
|
12
|
+
key: string;
|
|
13
|
+
before: string;
|
|
14
|
+
after: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Plan for aligning `scripts` with the scaffold template (reads only; no write until apply).
|
|
18
|
+
*/
|
|
19
|
+
export interface FixPlan {
|
|
20
|
+
packageJsonPath: string;
|
|
21
|
+
packageJsonUpdatedContent: string;
|
|
22
|
+
hasChanges: boolean;
|
|
23
|
+
scriptsToRemove: ScriptToRemove[];
|
|
24
|
+
scriptsToReplace: ScriptToReplace[];
|
|
25
|
+
scriptsToAdd: ScriptToAdd[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Computes the package.json rewrite without writing. Use with {@link applyFix}.
|
|
29
|
+
*/
|
|
30
|
+
export declare function planFix(projectRoot: string): FixPlan;
|
|
31
|
+
/**
|
|
32
|
+
* Writes the planned package.json. No-op when {@link FixPlan.hasChanges} is false.
|
|
33
|
+
*/
|
|
34
|
+
export declare function applyFix(plan: FixPlan): void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { processTemplate } from '#utils/template.js';
|
|
5
|
+
const packageJson = 'package.json';
|
|
6
|
+
const templateRelativePath = path.join('templates', 'project', 'package.json.template');
|
|
7
|
+
/** Legacy script names from older versions of the project */
|
|
8
|
+
export const legacyScripts = ['dev'];
|
|
9
|
+
function getTemplatesPackageJsonPath() {
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
return path.join(__dirname, '..', templateRelativePath);
|
|
13
|
+
}
|
|
14
|
+
function readPackageJsonText(packagePath) {
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(packagePath, 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (typeof error !== 'object' || error === null) {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const code = error.code;
|
|
23
|
+
if (typeof code === 'string' && code === 'ENOENT') {
|
|
24
|
+
throw new Error(`No ${packageJson} found at ${packagePath}. Run this command from your Output project root.`);
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function parsePackageJsonObject(raw, packagePath) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new Error(`File ${packagePath} is not a valid JSON.`, { cause: error });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function parseTemplatePackageJson(processed) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(processed);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error(`Internal error: failed to parse processed ${templateRelativePath}.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Computes the package.json rewrite without writing. Use with {@link applyFix}.
|
|
47
|
+
*/
|
|
48
|
+
export function planFix(projectRoot) {
|
|
49
|
+
const packageJsonPath = path.join(projectRoot, packageJson);
|
|
50
|
+
const raw = readPackageJsonText(packageJsonPath);
|
|
51
|
+
const pkg = parsePackageJsonObject(raw, packageJsonPath);
|
|
52
|
+
const originalScripts = typeof pkg.scripts === 'object' && pkg.scripts !== null && !Array.isArray(pkg.scripts) ?
|
|
53
|
+
{ ...pkg.scripts } :
|
|
54
|
+
{};
|
|
55
|
+
const scripts = { ...originalScripts };
|
|
56
|
+
const scriptsToRemove = [];
|
|
57
|
+
for (const key of legacyScripts) {
|
|
58
|
+
if (Object.hasOwn(scripts, key)) {
|
|
59
|
+
scriptsToRemove.push({ key, value: scripts[key] });
|
|
60
|
+
delete scripts[key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const templateRaw = readFileSync(getTemplatesPackageJsonPath(), 'utf-8');
|
|
64
|
+
const templateVars = { projectName: '', description: '', frameworkVersion: '' };
|
|
65
|
+
const processed = processTemplate(templateRaw, templateVars);
|
|
66
|
+
const templatePkg = parseTemplatePackageJson(processed);
|
|
67
|
+
const templateScripts = templatePkg.scripts;
|
|
68
|
+
if (!templateScripts || typeof templateScripts !== 'object') {
|
|
69
|
+
throw new Error(`Internal error: ${templateRelativePath} has no scripts object.`);
|
|
70
|
+
}
|
|
71
|
+
const scriptsToAdd = [];
|
|
72
|
+
const scriptsToReplace = [];
|
|
73
|
+
for (const [key, after] of Object.entries(templateScripts)) {
|
|
74
|
+
const before = originalScripts[key];
|
|
75
|
+
if (before !== after) {
|
|
76
|
+
if (Object.hasOwn(originalScripts, key)) {
|
|
77
|
+
scriptsToReplace.push({ key, before: before, after });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
scriptsToAdd.push({ key, value: after });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
scripts[key] = after;
|
|
84
|
+
}
|
|
85
|
+
pkg.scripts = scripts;
|
|
86
|
+
const packageJsonUpdatedContent = `${JSON.stringify(pkg, null, 2)}\n`;
|
|
87
|
+
const hasChanges = packageJsonUpdatedContent !== raw;
|
|
88
|
+
return {
|
|
89
|
+
packageJsonPath,
|
|
90
|
+
packageJsonUpdatedContent,
|
|
91
|
+
hasChanges,
|
|
92
|
+
scriptsToRemove,
|
|
93
|
+
scriptsToReplace,
|
|
94
|
+
scriptsToAdd
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Writes the planned package.json. No-op when {@link FixPlan.hasChanges} is false.
|
|
99
|
+
*/
|
|
100
|
+
export function applyFix(plan) {
|
|
101
|
+
if (!plan.hasChanges) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
writeFileSync(plan.packageJsonPath, plan.packageJsonUpdatedContent, 'utf-8');
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { applyFix, planFix, legacyScripts } from './fix_package.js';
|
|
6
|
+
describe('fix package', () => {
|
|
7
|
+
it('should remove legacy keys and apply template scripts while preserving other scripts', async () => {
|
|
8
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
|
|
9
|
+
const pkg = {
|
|
10
|
+
name: 'my-proj',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
scripts: {
|
|
13
|
+
dev: 'old dev',
|
|
14
|
+
'custom:test': 'vitest'
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
await fs.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2), 'utf-8');
|
|
18
|
+
const plan = planFix(tmpDir);
|
|
19
|
+
expect(plan.scriptsToRemove.map(r => r.key).sort()).toEqual([...legacyScripts].sort());
|
|
20
|
+
expect(plan.hasChanges).toBe(true);
|
|
21
|
+
expect(plan.scriptsToReplace).toEqual([]);
|
|
22
|
+
expect(plan.scriptsToAdd).toHaveLength(6);
|
|
23
|
+
applyFix(plan);
|
|
24
|
+
const next = JSON.parse(await fs.readFile(path.join(tmpDir, 'package.json'), 'utf-8'));
|
|
25
|
+
expect(next.scripts['dev']).toBeUndefined();
|
|
26
|
+
expect(next.scripts['custom:test']).toBe('vitest');
|
|
27
|
+
expect(next.scripts['output:dev']).toBe('output dev');
|
|
28
|
+
expect(next.scripts['output:worker:start']).toBe('output-worker');
|
|
29
|
+
});
|
|
30
|
+
it('should classify an existing key with a different value as replace, and new keys as add', async () => {
|
|
31
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
|
|
32
|
+
const pkg = {
|
|
33
|
+
name: 'my-proj',
|
|
34
|
+
version: '1.0.0',
|
|
35
|
+
scripts: {
|
|
36
|
+
'output:dev': 'bad-dev-command'
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
await fs.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2), 'utf-8');
|
|
40
|
+
const plan = planFix(tmpDir);
|
|
41
|
+
expect(plan.scriptsToRemove).toEqual([]);
|
|
42
|
+
expect(plan.scriptsToReplace).toEqual([
|
|
43
|
+
{ key: 'output:dev', before: 'bad-dev-command', after: 'output dev' }
|
|
44
|
+
]);
|
|
45
|
+
expect(plan.scriptsToAdd).toHaveLength(5);
|
|
46
|
+
expect(plan.scriptsToAdd.map(a => a.key).sort()).toEqual([
|
|
47
|
+
'output:worker',
|
|
48
|
+
'output:worker:build',
|
|
49
|
+
'output:worker:install',
|
|
50
|
+
'output:worker:start',
|
|
51
|
+
'output:worker:watch'
|
|
52
|
+
].sort());
|
|
53
|
+
});
|
|
54
|
+
it('should throw when package.json is missing', async () => {
|
|
55
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
|
|
56
|
+
expect(() => planFix(tmpDir)).toThrow(/No package\.json found/);
|
|
57
|
+
});
|
|
58
|
+
it('should throw when package.json is not valid JSON', async () => {
|
|
59
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
|
|
60
|
+
await fs.writeFile(path.join(tmpDir, 'package.json'), '{ not json', 'utf-8');
|
|
61
|
+
expect(() => planFix(tmpDir)).toThrow(/not a valid JSON/);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const getEjectSuccessMessage: (destPath: string, outputFile: string, binName: string) => string;
|
|
5
5
|
export declare const getProjectSuccessMessage: (folderName: string, installSuccess: boolean, credentialsConfigured?: boolean) => string;
|
|
6
|
-
export declare const getWorkflowGenerateSuccessMessage: (workflowName: string, targetDir: string, filesCreated: string[]) => string;
|
|
6
|
+
export declare const getWorkflowGenerateSuccessMessage: (workflowName: string, workflowId: string, scenarioName: string | undefined, targetDir: string, filesCreated: string[]) => string;
|
|
@@ -234,7 +234,7 @@ ${ux.colorize('dim', ' to manage your project secrets.')}
|
|
|
234
234
|
${ux.colorize('green', ux.colorize('bold', 'Happy building with Output! 🚀'))}
|
|
235
235
|
`;
|
|
236
236
|
};
|
|
237
|
-
export const getWorkflowGenerateSuccessMessage = (workflowName, targetDir, filesCreated) => {
|
|
237
|
+
export const getWorkflowGenerateSuccessMessage = (workflowName, workflowId, scenarioName, targetDir, filesCreated) => {
|
|
238
238
|
const divider = ux.colorize('dim', '─'.repeat(80));
|
|
239
239
|
const bulletPoint = ux.colorize('green', '▸');
|
|
240
240
|
const formattedFiles = filesCreated.map(file => {
|
|
@@ -256,7 +256,7 @@ export const getWorkflowGenerateSuccessMessage = (workflowName, targetDir, files
|
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
258
|
step: 'Test your workflow',
|
|
259
|
-
command: `npx output workflow run ${
|
|
259
|
+
command: `npx output workflow run ${workflowId}${scenarioName ? ` ${scenarioName}` : ''}`,
|
|
260
260
|
note: 'Run after starting services with "npx output dev"'
|
|
261
261
|
}
|
|
262
262
|
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { input, confirm } from '
|
|
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';
|
|
@@ -41,7 +41,7 @@ export async function checkDependencies() {
|
|
|
41
41
|
try {
|
|
42
42
|
const shouldProceed = await confirm({
|
|
43
43
|
message: 'Would you like to proceed anyway?',
|
|
44
|
-
default:
|
|
44
|
+
default: true
|
|
45
45
|
});
|
|
46
46
|
if (!shouldProceed) {
|
|
47
47
|
throw new UserCancelledError();
|
|
@@ -8,7 +8,7 @@ vi.mock('#utils/framework_version.js', () => ({
|
|
|
8
8
|
})
|
|
9
9
|
}));
|
|
10
10
|
// Mock other dependencies
|
|
11
|
-
vi.mock('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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 '
|
|
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 '
|
|
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('
|
|
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(),
|