@outputai/cli 0.1.13-dev.2f0a972.0 → 0.1.13-dev.59a1b6d.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 +5 -1
- package/dist/api/generated/api.js +1 -1
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/credentials/set.d.ts +14 -0
- package/dist/commands/credentials/set.js +57 -0
- package/dist/commands/credentials/set.spec.d.ts +1 -0
- package/dist/commands/credentials/set.spec.js +95 -0
- package/dist/commands/fix.js +1 -1
- package/dist/commands/fix.spec.js +2 -2
- package/dist/commands/update.js +1 -1
- package/dist/commands/update.spec.js +2 -2
- package/dist/commands/workflow/plan.js +5 -1
- package/dist/commands/workflow/plan.spec.js +3 -2
- package/dist/commands/workflow/runs/list.d.ts +1 -0
- package/dist/commands/workflow/runs/list.js +6 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/hooks/init.js +4 -0
- 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/env_configurator.js +1 -1
- package/dist/services/env_configurator.spec.js +12 -12
- 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/services/workflow_runs.d.ts +1 -0
- package/dist/services/workflow_runs.js +3 -0
- 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 +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/package.json +12 -11
package/bin/run.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { execute } from '@oclif/core';
|
|
4
4
|
import { loadEnvironment } from '../dist/utils/env_loader.js';
|
|
5
|
+
import { bootstrapProxy } from '../dist/utils/proxy.js';
|
|
5
6
|
import { resolveCredentialRefs } from '@outputai/credentials';
|
|
6
7
|
|
|
7
8
|
// Load environment variables from .env files before executing CLI
|
|
8
9
|
loadEnvironment();
|
|
10
|
+
bootstrapProxy();
|
|
9
11
|
resolveCredentialRefs();
|
|
10
12
|
|
|
11
13
|
await execute( { dir: import.meta.url } );
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generated by orval v8.
|
|
2
|
+
* Generated by orval v8.6.2 🍺
|
|
3
3
|
* Do not edit manually.
|
|
4
4
|
* Output.ai API
|
|
5
5
|
* API for managing and executing Output.ai workflows
|
|
@@ -357,6 +357,10 @@ export type GetWorkflowRunsParams = {
|
|
|
357
357
|
* Filter by workflow type/name
|
|
358
358
|
*/
|
|
359
359
|
workflowType?: string;
|
|
360
|
+
/**
|
|
361
|
+
* Filter by task queue name (e.g. catalog/session ID)
|
|
362
|
+
*/
|
|
363
|
+
taskQueue?: string;
|
|
360
364
|
/**
|
|
361
365
|
* Maximum number of runs to return
|
|
362
366
|
* @minimum 1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class CredentialsSet extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
path: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
value: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
8
|
+
};
|
|
9
|
+
static flags: {
|
|
10
|
+
environment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
workflow: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { load as parseYaml, dump as stringifyYaml } from 'js-yaml';
|
|
3
|
+
import { decryptCredentials, credentialsExist, writeEncrypted, resolveCredentialsPath } from '#services/credentials_service.js';
|
|
4
|
+
const setNestedValue = (obj, dotPath, value) => {
|
|
5
|
+
const parts = dotPath.split('.');
|
|
6
|
+
const parent = parts.slice(0, -1).reduce((current, key) => {
|
|
7
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
8
|
+
current[key] = {};
|
|
9
|
+
}
|
|
10
|
+
return current[key];
|
|
11
|
+
}, obj);
|
|
12
|
+
parent[parts[parts.length - 1]] = value;
|
|
13
|
+
};
|
|
14
|
+
export default class CredentialsSet extends Command {
|
|
15
|
+
static description = 'Set a credential value by dot-notation path';
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> <%= command.id %> anthropic.api_key sk-ant-...',
|
|
18
|
+
'<%= config.bin %> <%= command.id %> openai.api_key sk-... --environment production',
|
|
19
|
+
'<%= config.bin %> <%= command.id %> stripe.key sk_live_... --workflow my_workflow'
|
|
20
|
+
];
|
|
21
|
+
static args = {
|
|
22
|
+
path: Args.string({
|
|
23
|
+
description: 'Dot-notation path to the credential (e.g. anthropic.api_key)',
|
|
24
|
+
required: true
|
|
25
|
+
}),
|
|
26
|
+
value: Args.string({
|
|
27
|
+
description: 'Value to set',
|
|
28
|
+
required: true
|
|
29
|
+
})
|
|
30
|
+
};
|
|
31
|
+
static flags = {
|
|
32
|
+
environment: Flags.string({
|
|
33
|
+
char: 'e',
|
|
34
|
+
description: 'Target environment (e.g. production, development)'
|
|
35
|
+
}),
|
|
36
|
+
workflow: Flags.string({
|
|
37
|
+
char: 'w',
|
|
38
|
+
description: 'Target a specific workflow directory'
|
|
39
|
+
})
|
|
40
|
+
};
|
|
41
|
+
async run() {
|
|
42
|
+
const { args, flags } = await this.parse(CredentialsSet);
|
|
43
|
+
const environment = flags.environment;
|
|
44
|
+
const workflow = flags.workflow;
|
|
45
|
+
if (environment && workflow) {
|
|
46
|
+
this.error('Cannot specify both --environment and --workflow.');
|
|
47
|
+
}
|
|
48
|
+
if (!credentialsExist(environment, workflow)) {
|
|
49
|
+
this.error(`No credentials file found at ${resolveCredentialsPath(environment, workflow)}. Run "output credentials init" first.`);
|
|
50
|
+
}
|
|
51
|
+
const plaintext = decryptCredentials(environment, workflow);
|
|
52
|
+
const data = (parseYaml(plaintext) || {});
|
|
53
|
+
setNestedValue(data, args.path, args.value);
|
|
54
|
+
writeEncrypted(environment, stringifyYaml(data), workflow);
|
|
55
|
+
this.log(`Set ${args.path}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
3
|
+
import * as credentialsService from '#services/credentials_service.js';
|
|
4
|
+
import CredentialsSet from './set.js';
|
|
5
|
+
vi.mock('#services/credentials_service.js');
|
|
6
|
+
vi.mock('js-yaml', () => ({
|
|
7
|
+
load: vi.fn((yaml) => {
|
|
8
|
+
if (yaml.includes('sk-existing')) {
|
|
9
|
+
return { anthropic: { api_key: 'sk-existing' } };
|
|
10
|
+
}
|
|
11
|
+
return {};
|
|
12
|
+
}),
|
|
13
|
+
dump: vi.fn((obj) => JSON.stringify(obj))
|
|
14
|
+
}));
|
|
15
|
+
describe('credentials set command', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
vi.mocked(credentialsService.credentialsExist).mockReturnValue(true);
|
|
19
|
+
vi.mocked(credentialsService.decryptCredentials).mockReturnValue('anthropic:\n api_key: sk-existing\n');
|
|
20
|
+
vi.mocked(credentialsService.writeEncrypted).mockImplementation(() => { });
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
const createTestCommand = (parsedArgs = {}, flags = {}) => {
|
|
26
|
+
const cmd = new CredentialsSet([], {});
|
|
27
|
+
cmd.log = vi.fn();
|
|
28
|
+
cmd.error = vi.fn((msg) => {
|
|
29
|
+
throw new Error(msg);
|
|
30
|
+
});
|
|
31
|
+
Object.defineProperty(cmd, 'parse', {
|
|
32
|
+
value: vi.fn().mockResolvedValue({
|
|
33
|
+
args: { path: 'anthropic.api_key', value: 'sk-new-key', ...parsedArgs },
|
|
34
|
+
flags: { environment: undefined, workflow: undefined, ...flags }
|
|
35
|
+
}),
|
|
36
|
+
configurable: true
|
|
37
|
+
});
|
|
38
|
+
return cmd;
|
|
39
|
+
};
|
|
40
|
+
describe('command structure', () => {
|
|
41
|
+
it('should have correct description', () => {
|
|
42
|
+
expect(CredentialsSet.description).toContain('credential value');
|
|
43
|
+
});
|
|
44
|
+
it('should have required path and value arguments', () => {
|
|
45
|
+
expect(CredentialsSet.args.path).toBeDefined();
|
|
46
|
+
expect(CredentialsSet.args.path.required).toBe(true);
|
|
47
|
+
expect(CredentialsSet.args.value).toBeDefined();
|
|
48
|
+
expect(CredentialsSet.args.value.required).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('should have environment and workflow flags', () => {
|
|
51
|
+
expect(CredentialsSet.flags.environment).toBeDefined();
|
|
52
|
+
expect(CredentialsSet.flags.workflow).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('command execution', () => {
|
|
56
|
+
it('should decrypt, update, and re-encrypt credentials', async () => {
|
|
57
|
+
const cmd = createTestCommand();
|
|
58
|
+
await cmd.run();
|
|
59
|
+
expect(credentialsService.decryptCredentials).toHaveBeenCalledWith(undefined, undefined);
|
|
60
|
+
expect(credentialsService.writeEncrypted).toHaveBeenCalledWith(undefined, expect.any(String), undefined);
|
|
61
|
+
expect(cmd.log).toHaveBeenCalledWith('Set anthropic.api_key');
|
|
62
|
+
});
|
|
63
|
+
it('should create nested keys that do not exist', async () => {
|
|
64
|
+
vi.mocked(credentialsService.decryptCredentials).mockReturnValue('');
|
|
65
|
+
const cmd = createTestCommand({ path: 'new.nested.key', value: 'my-value' });
|
|
66
|
+
await cmd.run();
|
|
67
|
+
expect(credentialsService.writeEncrypted).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(cmd.log).toHaveBeenCalledWith('Set new.nested.key');
|
|
69
|
+
});
|
|
70
|
+
it('should pass environment flag to service functions', async () => {
|
|
71
|
+
const cmd = createTestCommand({}, { environment: 'production' });
|
|
72
|
+
await cmd.run();
|
|
73
|
+
expect(credentialsService.credentialsExist).toHaveBeenCalledWith('production', undefined);
|
|
74
|
+
expect(credentialsService.decryptCredentials).toHaveBeenCalledWith('production', undefined);
|
|
75
|
+
expect(credentialsService.writeEncrypted).toHaveBeenCalledWith('production', expect.any(String), undefined);
|
|
76
|
+
});
|
|
77
|
+
it('should pass workflow flag to service functions', async () => {
|
|
78
|
+
const cmd = createTestCommand({}, { workflow: 'my_workflow' });
|
|
79
|
+
await cmd.run();
|
|
80
|
+
expect(credentialsService.credentialsExist).toHaveBeenCalledWith(undefined, 'my_workflow');
|
|
81
|
+
expect(credentialsService.decryptCredentials).toHaveBeenCalledWith(undefined, 'my_workflow');
|
|
82
|
+
expect(credentialsService.writeEncrypted).toHaveBeenCalledWith(undefined, expect.any(String), 'my_workflow');
|
|
83
|
+
});
|
|
84
|
+
it('should error when both environment and workflow are specified', async () => {
|
|
85
|
+
const cmd = createTestCommand({}, { environment: 'production', workflow: 'my_workflow' });
|
|
86
|
+
await expect(cmd.run()).rejects.toThrow('Cannot specify both');
|
|
87
|
+
});
|
|
88
|
+
it('should error when credentials file does not exist', async () => {
|
|
89
|
+
vi.mocked(credentialsService.credentialsExist).mockReturnValue(false);
|
|
90
|
+
vi.mocked(credentialsService.resolveCredentialsPath).mockReturnValue('/project/config/credentials.yml.enc');
|
|
91
|
+
const cmd = createTestCommand();
|
|
92
|
+
await expect(cmd.run()).rejects.toThrow('No credentials file found');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
package/dist/commands/fix.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { confirm } from '
|
|
2
|
+
import { confirm } from '#utils/prompt.js';
|
|
3
3
|
import { applyFix, planFix } from '#services/fix_package.js';
|
|
4
4
|
import { getErrorMessage } from '#utils/error_utils.js';
|
|
5
5
|
const Ansi = {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
3
3
|
import Fix from './fix.js';
|
|
4
4
|
import * as fixService from '#services/fix_package.js';
|
|
5
|
-
import { confirm } from '
|
|
5
|
+
import { confirm } from '#utils/prompt.js';
|
|
6
6
|
vi.mock('#services/fix_package.js', () => ({
|
|
7
7
|
planFix: vi.fn(),
|
|
8
8
|
applyFix: vi.fn()
|
|
9
9
|
}));
|
|
10
|
-
vi.mock('
|
|
10
|
+
vi.mock('#utils/prompt.js', () => ({
|
|
11
11
|
confirm: vi.fn()
|
|
12
12
|
}));
|
|
13
13
|
const basePlan = () => ({
|
package/dist/commands/update.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
|
-
import { confirm } from '
|
|
2
|
+
import { confirm } from '#utils/prompt.js';
|
|
3
3
|
import { fetchLatestVersion, getGlobalInstalledVersion, getLocalInstalledVersion, updateGlobal, updateLocal, isOutdated } from '#services/npm_update_service.js';
|
|
4
4
|
import { ensureClaudePlugin } from '#services/coding_agents.js';
|
|
5
5
|
import { getErrorMessage } from '#utils/error_utils.js';
|
|
@@ -3,7 +3,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
3
3
|
import Update from './update.js';
|
|
4
4
|
import { fetchLatestVersion, getGlobalInstalledVersion, getLocalInstalledVersion, updateGlobal, updateLocal, isOutdated } from '#services/npm_update_service.js';
|
|
5
5
|
import { ensureClaudePlugin } from '#services/coding_agents.js';
|
|
6
|
-
import { confirm } from '
|
|
6
|
+
import { confirm } from '#utils/prompt.js';
|
|
7
7
|
vi.mock('#services/npm_update_service.js', () => ({
|
|
8
8
|
fetchLatestVersion: vi.fn(),
|
|
9
9
|
getGlobalInstalledVersion: vi.fn(),
|
|
@@ -15,7 +15,7 @@ vi.mock('#services/npm_update_service.js', () => ({
|
|
|
15
15
|
vi.mock('#services/coding_agents.js', () => ({
|
|
16
16
|
ensureClaudePlugin: vi.fn()
|
|
17
17
|
}));
|
|
18
|
-
vi.mock('
|
|
18
|
+
vi.mock('#utils/prompt.js', () => ({
|
|
19
19
|
confirm: vi.fn()
|
|
20
20
|
}));
|
|
21
21
|
describe('update command', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command, Flags, ux } from '@oclif/core';
|
|
2
|
-
import { input } from '
|
|
2
|
+
import { input } from '#utils/prompt.js';
|
|
3
|
+
import { isInteractive } from '#utils/interactive.js';
|
|
3
4
|
import { generatePlanName, updateAgentTemplates, writePlanFile } from '#services/workflow_planner.js';
|
|
4
5
|
import { ensureOutputAISystem } from '#services/coding_agents.js';
|
|
5
6
|
import { invokePlanWorkflow, PLAN_COMMAND_OPTIONS, replyToClaude } from '#services/claude_client.js';
|
|
@@ -45,6 +46,9 @@ export default class WorkflowPlan extends Command {
|
|
|
45
46
|
this.log('=========');
|
|
46
47
|
this.log(originalPlanContent);
|
|
47
48
|
this.log('=========');
|
|
49
|
+
if (!isInteractive()) {
|
|
50
|
+
return originalPlanContent;
|
|
51
|
+
}
|
|
48
52
|
const modifications = await input({
|
|
49
53
|
message: ux.colorize('gray', `Reply or type ${acceptKey} to accept the plan as is: `),
|
|
50
54
|
validate: (value) => value.length >= 10 || value === acceptKey
|
|
@@ -3,11 +3,12 @@ import WorkflowPlan from './plan.js';
|
|
|
3
3
|
import { generatePlanName, writePlanFile, updateAgentTemplates } from '#services/workflow_planner.js';
|
|
4
4
|
import { ensureOutputAISystem } from '#services/coding_agents.js';
|
|
5
5
|
import { invokePlanWorkflow, replyToClaude, ClaudeInvocationError } from '#services/claude_client.js';
|
|
6
|
-
import { input } from '
|
|
6
|
+
import { input } from '#utils/prompt.js';
|
|
7
7
|
vi.mock('#services/workflow_planner.js');
|
|
8
8
|
vi.mock('#services/coding_agents.js');
|
|
9
9
|
vi.mock('#services/claude_client.js');
|
|
10
|
-
vi.mock('
|
|
10
|
+
vi.mock('#utils/prompt.js');
|
|
11
|
+
vi.mock('#utils/interactive.js', () => ({ isInteractive: () => true }));
|
|
11
12
|
describe('WorkflowPlan Command', () => {
|
|
12
13
|
const createCommand = () => {
|
|
13
14
|
const cmd = new WorkflowPlan([], {});
|
|
@@ -6,6 +6,7 @@ export default class WorkflowRunsList extends Command {
|
|
|
6
6
|
workflowName: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
7
7
|
};
|
|
8
8
|
static flags: {
|
|
9
|
+
'task-queue': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
};
|
|
@@ -65,6 +65,11 @@ export default class WorkflowRunsList extends Command {
|
|
|
65
65
|
})
|
|
66
66
|
};
|
|
67
67
|
static flags = {
|
|
68
|
+
'task-queue': Flags.string({
|
|
69
|
+
char: 'q',
|
|
70
|
+
description: 'Filter runs by task queue (defaults to OUTPUT_CATALOG_ID)',
|
|
71
|
+
env: 'OUTPUT_CATALOG_ID'
|
|
72
|
+
}),
|
|
68
73
|
limit: Flags.integer({
|
|
69
74
|
char: 'l',
|
|
70
75
|
description: 'Maximum number of runs to return',
|
|
@@ -81,6 +86,7 @@ export default class WorkflowRunsList extends Command {
|
|
|
81
86
|
const { args, flags } = await this.parse(WorkflowRunsList);
|
|
82
87
|
const { runs, count } = await fetchWorkflowRuns({
|
|
83
88
|
workflowType: args.workflowName,
|
|
89
|
+
taskQueue: flags['task-queue'],
|
|
84
90
|
limit: flags.limit
|
|
85
91
|
});
|
|
86
92
|
if (runs.length === 0) {
|
package/dist/hooks/init.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ux } from '@oclif/core';
|
|
2
2
|
import { checkForUpdate } from '#services/version_check.js';
|
|
3
|
+
import { setNonInteractive } from '#utils/interactive.js';
|
|
3
4
|
const hook = async function () {
|
|
5
|
+
if (process.argv.includes('--yes') || process.argv.includes('--non-interactive') || !process.stdin.isTTY) {
|
|
6
|
+
setNonInteractive(true);
|
|
7
|
+
}
|
|
4
8
|
try {
|
|
5
9
|
const result = await checkForUpdate(this.config.version, this.config.cacheDir);
|
|
6
10
|
if (!result.updateAvailable) {
|
|
@@ -7,7 +7,7 @@ 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
11
|
import debugFactory from 'debug';
|
|
12
12
|
import { getTemplateDir } from '#utils/paths.js';
|
|
13
13
|
import { executeClaudeCommand } from '#utils/claude.js';
|
|
@@ -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);
|
|
@@ -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
|
|
@@ -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(),
|
|
@@ -9,6 +9,7 @@ export interface WorkflowRunsResult {
|
|
|
9
9
|
}
|
|
10
10
|
export interface FetchWorkflowRunsOptions {
|
|
11
11
|
workflowType?: string;
|
|
12
|
+
taskQueue?: string;
|
|
12
13
|
limit?: number;
|
|
13
14
|
}
|
|
14
15
|
export declare function fetchWorkflowRuns(options?: FetchWorkflowRunsOptions): Promise<WorkflowRunsResult>;
|
|
@@ -10,6 +10,9 @@ export async function fetchWorkflowRuns(options = {}) {
|
|
|
10
10
|
if (options.workflowType) {
|
|
11
11
|
params.workflowType = options.workflowType;
|
|
12
12
|
}
|
|
13
|
+
if (options.taskQueue) {
|
|
14
|
+
params.taskQueue = options.taskQueue;
|
|
15
|
+
}
|
|
13
16
|
const response = await getWorkflowRuns(params);
|
|
14
17
|
if (!response) {
|
|
15
18
|
throw new Error('Failed to connect to API server. Is it running?');
|
|
@@ -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 options.default ?? 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 {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { confirm as inquirerConfirm, input as inquirerInput, password as inquirerPassword } from '@inquirer/prompts';
|
|
3
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
4
|
+
confirm: vi.fn(),
|
|
5
|
+
input: vi.fn(),
|
|
6
|
+
password: vi.fn()
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('./interactive.js', () => ({
|
|
9
|
+
isInteractive: vi.fn()
|
|
10
|
+
}));
|
|
11
|
+
describe('prompt wrapper', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
describe('when interactive', () => {
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
const { isInteractive } = await import('./interactive.js');
|
|
18
|
+
vi.mocked(isInteractive).mockReturnValue(true);
|
|
19
|
+
});
|
|
20
|
+
it('confirm delegates to inquirer', async () => {
|
|
21
|
+
vi.mocked(inquirerConfirm).mockResolvedValue(false);
|
|
22
|
+
const { confirm } = await import('./prompt.js');
|
|
23
|
+
const result = await confirm({ message: 'Continue?', default: true });
|
|
24
|
+
expect(inquirerConfirm).toHaveBeenCalledWith({ message: 'Continue?', default: true });
|
|
25
|
+
expect(result).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it('input delegates to inquirer', async () => {
|
|
28
|
+
vi.mocked(inquirerInput).mockResolvedValue('user input');
|
|
29
|
+
const { input } = await import('./prompt.js');
|
|
30
|
+
const result = await input({ message: 'Name?', default: 'default' });
|
|
31
|
+
expect(inquirerInput).toHaveBeenCalledWith({ message: 'Name?', default: 'default' });
|
|
32
|
+
expect(result).toBe('user input');
|
|
33
|
+
});
|
|
34
|
+
it('password delegates to inquirer', async () => {
|
|
35
|
+
vi.mocked(inquirerPassword).mockResolvedValue('secret');
|
|
36
|
+
const { password } = await import('./prompt.js');
|
|
37
|
+
const result = await password({ message: 'Token?' });
|
|
38
|
+
expect(inquirerPassword).toHaveBeenCalledWith({ message: 'Token?' });
|
|
39
|
+
expect(result).toBe('secret');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('when non-interactive', () => {
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
const { isInteractive } = await import('./interactive.js');
|
|
45
|
+
vi.mocked(isInteractive).mockReturnValue(false);
|
|
46
|
+
});
|
|
47
|
+
it('confirm returns default value', async () => {
|
|
48
|
+
const { confirm } = await import('./prompt.js');
|
|
49
|
+
expect(await confirm({ message: 'Continue?', default: false })).toBe(false);
|
|
50
|
+
expect(await confirm({ message: 'Continue?', default: true })).toBe(true);
|
|
51
|
+
expect(inquirerConfirm).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
it('confirm defaults to true when no default specified', async () => {
|
|
54
|
+
const { confirm } = await import('./prompt.js');
|
|
55
|
+
expect(await confirm({ message: 'Continue?' })).toBe(true);
|
|
56
|
+
expect(inquirerConfirm).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
it('input returns default value', async () => {
|
|
59
|
+
const { input } = await import('./prompt.js');
|
|
60
|
+
expect(await input({ message: 'Name?', default: 'fallback' })).toBe('fallback');
|
|
61
|
+
expect(inquirerInput).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
it('input returns empty string when no default', async () => {
|
|
64
|
+
const { input } = await import('./prompt.js');
|
|
65
|
+
expect(await input({ message: 'Name?' })).toBe('');
|
|
66
|
+
expect(inquirerInput).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
it('password returns empty string', async () => {
|
|
69
|
+
const { password } = await import('./prompt.js');
|
|
70
|
+
expect(await password({ message: 'Token?' })).toBe('');
|
|
71
|
+
expect(inquirerPassword).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const bootstrapProxy: () => void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
|
|
2
|
+
export const bootstrapProxy = () => {
|
|
3
|
+
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy ||
|
|
4
|
+
process.env.HTTP_PROXY || process.env.http_proxy;
|
|
5
|
+
if (!proxyUrl) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
9
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
const mockSetGlobalDispatcher = vi.fn();
|
|
3
|
+
const MockEnvHttpProxyAgent = vi.fn();
|
|
4
|
+
vi.mock('undici', () => ({
|
|
5
|
+
EnvHttpProxyAgent: MockEnvHttpProxyAgent,
|
|
6
|
+
setGlobalDispatcher: mockSetGlobalDispatcher
|
|
7
|
+
}));
|
|
8
|
+
describe('proxy bootstrap', () => {
|
|
9
|
+
const originalEnv = { ...process.env };
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
delete process.env.HTTPS_PROXY;
|
|
13
|
+
delete process.env.https_proxy;
|
|
14
|
+
delete process.env.HTTP_PROXY;
|
|
15
|
+
delete process.env.http_proxy;
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
process.env = { ...originalEnv };
|
|
19
|
+
});
|
|
20
|
+
it('does nothing when no proxy env vars are set', async () => {
|
|
21
|
+
const { bootstrapProxy } = await import('./proxy.js');
|
|
22
|
+
bootstrapProxy();
|
|
23
|
+
expect(mockSetGlobalDispatcher).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
it('sets global dispatcher when HTTPS_PROXY is set', async () => {
|
|
26
|
+
process.env.HTTPS_PROXY = 'http://proxy:8080';
|
|
27
|
+
const { bootstrapProxy } = await import('./proxy.js');
|
|
28
|
+
bootstrapProxy();
|
|
29
|
+
expect(MockEnvHttpProxyAgent).toHaveBeenCalled();
|
|
30
|
+
expect(mockSetGlobalDispatcher).toHaveBeenCalledTimes(1);
|
|
31
|
+
});
|
|
32
|
+
it('sets global dispatcher when HTTP_PROXY is set', async () => {
|
|
33
|
+
process.env.HTTP_PROXY = 'http://proxy:8080';
|
|
34
|
+
const { bootstrapProxy } = await import('./proxy.js');
|
|
35
|
+
bootstrapProxy();
|
|
36
|
+
expect(MockEnvHttpProxyAgent).toHaveBeenCalled();
|
|
37
|
+
expect(mockSetGlobalDispatcher).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.1.13-dev.
|
|
3
|
+
"version": "0.1.13-dev.59a1b6d.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,29 +15,30 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@anthropic-ai/claude-agent-sdk": "0.2.92",
|
|
18
|
-
"@aws-sdk/client-s3": "3.
|
|
18
|
+
"@aws-sdk/client-s3": "3.1024.0",
|
|
19
19
|
"@hackylabs/deep-redact": "3.0.5",
|
|
20
|
-
"@inquirer/prompts": "8.
|
|
20
|
+
"@inquirer/prompts": "8.3.2",
|
|
21
21
|
"@oclif/core": "4.10.5",
|
|
22
|
-
"@oclif/plugin-help": "6.2.
|
|
22
|
+
"@oclif/plugin-help": "6.2.42",
|
|
23
23
|
"change-case": "5.4.4",
|
|
24
24
|
"cli-progress": "3.12.0",
|
|
25
25
|
"cli-table3": "0.6.5",
|
|
26
26
|
"date-fns": "4.1.0",
|
|
27
27
|
"debug": "4.4.3",
|
|
28
|
-
"dotenv": "17.4.
|
|
28
|
+
"dotenv": "17.4.0",
|
|
29
29
|
"handlebars": "4.7.9",
|
|
30
30
|
"ink": "6.8.0",
|
|
31
31
|
"ink-spinner": "5.0.0",
|
|
32
32
|
"js-yaml": "4.1.1",
|
|
33
|
-
"json-schema-library": "11.
|
|
33
|
+
"json-schema-library": "11.1.0",
|
|
34
34
|
"ky": "1.14.3",
|
|
35
|
-
"react": "19.2.
|
|
35
|
+
"react": "19.2.4",
|
|
36
36
|
"semver": "7.7.4",
|
|
37
|
+
"undici": "8.0.2",
|
|
37
38
|
"yaml": "^2.8.3",
|
|
38
|
-
"@outputai/credentials": "0.1.13-dev.
|
|
39
|
-
"@outputai/
|
|
40
|
-
"@outputai/
|
|
39
|
+
"@outputai/credentials": "0.1.13-dev.59a1b6d.0",
|
|
40
|
+
"@outputai/evals": "0.1.13-dev.59a1b6d.0",
|
|
41
|
+
"@outputai/llm": "0.1.13-dev.59a1b6d.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@types/cli-progress": "3.11.6",
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"@types/js-yaml": "4.0.9",
|
|
46
47
|
"@types/react": "19.2.14",
|
|
47
48
|
"@types/semver": "7.7.1",
|
|
48
|
-
"orval": "8.
|
|
49
|
+
"orval": "8.6.2"
|
|
49
50
|
},
|
|
50
51
|
"license": "Apache-2.0",
|
|
51
52
|
"publishConfig": {
|