@outputai/cli 0.1.12 → 0.1.13-dev.98dfd72.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/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/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/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 -2
- 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 +62 -26
- package/dist/views/workflow/list.d.ts +6 -0
- package/dist/views/workflow/list.js +127 -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 } );
|
|
@@ -63,6 +63,8 @@ export interface Workflow {
|
|
|
63
63
|
path?: string;
|
|
64
64
|
inputSchema?: JSONSchema;
|
|
65
65
|
outputSchema?: JSONSchema;
|
|
66
|
+
/** Alternative names that resolve to this workflow */
|
|
67
|
+
aliases?: string[];
|
|
66
68
|
}
|
|
67
69
|
/**
|
|
68
70
|
* File destinations for trace data
|
|
@@ -304,6 +306,8 @@ export declare const GetWorkflowIdResult200Status: {
|
|
|
304
306
|
export type GetWorkflowIdResult200 = {
|
|
305
307
|
/** The workflow execution id */
|
|
306
308
|
workflowId?: string;
|
|
309
|
+
/** The original input passed to the workflow, null if unavailable */
|
|
310
|
+
input?: unknown;
|
|
307
311
|
/** The result of workflow, null if workflow failed */
|
|
308
312
|
output?: unknown;
|
|
309
313
|
trace?: TraceInfo;
|
|
@@ -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', () => {
|
|
@@ -4,6 +4,7 @@ import { buildWorkflow, buildWorkflowInteractiveLoop } from '#services/workflow_
|
|
|
4
4
|
import { ensureOutputAISystem } from '#services/coding_agents.js';
|
|
5
5
|
import { getWorkflowGenerateSuccessMessage } from '#services/messages.js';
|
|
6
6
|
import { DEFAULT_OUTPUT_DIRS } from '#utils/paths.js';
|
|
7
|
+
import { parseWorkflowDir } from '#utils/workflow_dir_parser.js';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import * as fsSync from 'node:fs';
|
|
9
10
|
import { getErrorMessage } from '#utils/error_utils.js';
|
|
@@ -83,7 +84,8 @@ export default class Generate extends Command {
|
|
|
83
84
|
this.displaySuccess(result);
|
|
84
85
|
}
|
|
85
86
|
displaySuccess(result) {
|
|
86
|
-
const
|
|
87
|
+
const dirInfo = parseWorkflowDir(result.targetDir);
|
|
88
|
+
const message = getWorkflowGenerateSuccessMessage(result.workflowName, dirInfo.workflowId ?? result.workflowName, dirInfo.scenarioNames[0], result.targetDir, result.filesCreated);
|
|
87
89
|
this.log(message);
|
|
88
90
|
}
|
|
89
91
|
}
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
3
3
|
import Generate from './generate.js';
|
|
4
4
|
import { generateWorkflow } from '#services/workflow_generator.js';
|
|
5
|
+
import { parseWorkflowDir } from '#utils/workflow_dir_parser.js';
|
|
5
6
|
import { InvalidNameError, WorkflowExistsError } from '#types/errors.js';
|
|
6
7
|
vi.mock('../../services/workflow_generator.js');
|
|
8
|
+
vi.mock('../../utils/workflow_dir_parser.js');
|
|
7
9
|
describe('Generate Command', () => {
|
|
8
10
|
let mockGenerateWorkflow;
|
|
11
|
+
let mockParseWorkflowDir;
|
|
9
12
|
let logSpy;
|
|
10
13
|
const createCommand = () => {
|
|
11
14
|
const cmd = new Generate([], {});
|
|
@@ -20,6 +23,7 @@ describe('Generate Command', () => {
|
|
|
20
23
|
beforeEach(() => {
|
|
21
24
|
vi.clearAllMocks();
|
|
22
25
|
mockGenerateWorkflow = vi.mocked(generateWorkflow);
|
|
26
|
+
mockParseWorkflowDir = vi.mocked(parseWorkflowDir);
|
|
23
27
|
});
|
|
24
28
|
describe('successful workflow generation', () => {
|
|
25
29
|
it('should generate workflow with skeleton flag', async () => {
|
|
@@ -38,6 +42,10 @@ describe('Generate Command', () => {
|
|
|
38
42
|
targetDir: '/tmp/test-workflow',
|
|
39
43
|
filesCreated: ['index.ts', 'steps.ts', 'types.ts']
|
|
40
44
|
});
|
|
45
|
+
mockParseWorkflowDir.mockReturnValue({
|
|
46
|
+
workflowId: 'testWorkflow',
|
|
47
|
+
scenarioNames: ['test_input']
|
|
48
|
+
});
|
|
41
49
|
await cmd.run();
|
|
42
50
|
expect(mockGenerateWorkflow).toHaveBeenCalledWith({
|
|
43
51
|
name: 'test-workflow',
|
|
@@ -105,6 +113,10 @@ describe('Generate Command', () => {
|
|
|
105
113
|
targetDir: '/custom/path/my-workflow',
|
|
106
114
|
filesCreated: ['index.ts', 'steps.ts', 'types.ts']
|
|
107
115
|
});
|
|
116
|
+
mockParseWorkflowDir.mockReturnValue({
|
|
117
|
+
workflowId: 'myWorkflow',
|
|
118
|
+
scenarioNames: ['test_input']
|
|
119
|
+
});
|
|
108
120
|
await cmd.run();
|
|
109
121
|
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('SUCCESS!'));
|
|
110
122
|
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('my-workflow'));
|
|
@@ -17,7 +17,8 @@ export function parseWorkflowForDisplay(workflow) {
|
|
|
17
17
|
description: parsed.description || 'No description',
|
|
18
18
|
inputs: formatParameters(parsed.inputs),
|
|
19
19
|
outputs: formatParameters(parsed.outputs),
|
|
20
|
-
scenarios: scenarioNames.length > 0 ? scenarioNames.join(', ') : 'none'
|
|
20
|
+
scenarios: scenarioNames.length > 0 ? scenarioNames.join(', ') : 'none',
|
|
21
|
+
aliases: workflow.aliases?.length ? workflow.aliases.join(', ') : 'none'
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
function caseInsensitiveIncludes(str, filter) {
|
|
@@ -26,7 +27,10 @@ function caseInsensitiveIncludes(str, filter) {
|
|
|
26
27
|
function matchName(filterString) {
|
|
27
28
|
return workflow => {
|
|
28
29
|
const name = workflow.name || '';
|
|
29
|
-
|
|
30
|
+
if (caseInsensitiveIncludes(name, filterString)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return (workflow.aliases ?? []).some(alias => caseInsensitiveIncludes(alias, filterString));
|
|
30
34
|
};
|
|
31
35
|
}
|
|
32
36
|
function sortWorkflowsByName(workflows) {
|
|
@@ -38,8 +42,8 @@ function sortWorkflowsByName(workflows) {
|
|
|
38
42
|
}
|
|
39
43
|
function createWorkflowTable(workflows, detailed) {
|
|
40
44
|
const table = new Table({
|
|
41
|
-
head: ['Name', 'Description', 'Inputs', 'Outputs', 'Scenarios'],
|
|
42
|
-
colWidths: detailed ? [
|
|
45
|
+
head: ['Name', 'Description', 'Aliases', 'Inputs', 'Outputs', 'Scenarios'],
|
|
46
|
+
colWidths: detailed ? [28, 36, 36, 36, 36, 48] : [22, 26, 26, 22, 22, 36],
|
|
43
47
|
wordWrap: true,
|
|
44
48
|
style: {
|
|
45
49
|
head: ['cyan']
|
|
@@ -49,13 +53,14 @@ function createWorkflowTable(workflows, detailed) {
|
|
|
49
53
|
sortedWorkflows.forEach(workflow => {
|
|
50
54
|
const display = parseWorkflowForDisplay(workflow);
|
|
51
55
|
if (detailed) {
|
|
56
|
+
const aliases = workflow.aliases?.length ? workflow.aliases.join('\n') : 'none';
|
|
52
57
|
const inputs = display.inputs.split(', ').join('\n');
|
|
53
58
|
const outputs = display.outputs.split(', ').join('\n');
|
|
54
59
|
const scenarios = display.scenarios.split(', ').join('\n');
|
|
55
|
-
table.push([display.name, display.description, inputs, outputs, scenarios]);
|
|
60
|
+
table.push([display.name, display.description, aliases, inputs, outputs, scenarios]);
|
|
56
61
|
}
|
|
57
62
|
else {
|
|
58
|
-
table.push([display.name, display.description, display.inputs, display.outputs, display.scenarios]);
|
|
63
|
+
table.push([display.name, display.description, display.aliases, display.inputs, display.outputs, display.scenarios]);
|
|
59
64
|
}
|
|
60
65
|
});
|
|
61
66
|
return table.toString();
|
|
@@ -72,6 +77,7 @@ function formatWorkflowsAsJson(workflows) {
|
|
|
72
77
|
return {
|
|
73
78
|
name: display.name,
|
|
74
79
|
description: display.description,
|
|
80
|
+
aliases: w.aliases ?? [],
|
|
75
81
|
inputs: display.inputs.split(', '),
|
|
76
82
|
outputs: display.outputs.split(', '),
|
|
77
83
|
scenarios: display.scenarios === 'none' ? [] : display.scenarios.split(', '),
|
|
@@ -64,6 +64,27 @@ describe('workflow list parsing', () => {
|
|
|
64
64
|
expect(parsed.inputs).toBe('none');
|
|
65
65
|
expect(parsed.outputs).toBe('none');
|
|
66
66
|
expect(parsed.scenarios).toBe('none');
|
|
67
|
+
expect(parsed.aliases).toBe('none');
|
|
68
|
+
});
|
|
69
|
+
it('should include aliases when present', async () => {
|
|
70
|
+
const { parseWorkflowForDisplay } = await import('./list.js');
|
|
71
|
+
const mockWorkflow = {
|
|
72
|
+
name: 'aliased-workflow',
|
|
73
|
+
description: 'Has aliases',
|
|
74
|
+
aliases: ['old_name', 'legacy_name']
|
|
75
|
+
};
|
|
76
|
+
const parsed = parseWorkflowForDisplay(mockWorkflow);
|
|
77
|
+
expect(parsed.aliases).toBe('old_name, legacy_name');
|
|
78
|
+
});
|
|
79
|
+
it('should show none when aliases array is empty', async () => {
|
|
80
|
+
const { parseWorkflowForDisplay } = await import('./list.js');
|
|
81
|
+
const mockWorkflow = {
|
|
82
|
+
name: 'no-aliases',
|
|
83
|
+
description: 'Empty aliases',
|
|
84
|
+
aliases: []
|
|
85
|
+
};
|
|
86
|
+
const parsed = parseWorkflowForDisplay(mockWorkflow);
|
|
87
|
+
expect(parsed.aliases).toBe('none');
|
|
67
88
|
});
|
|
68
89
|
it('should include scenario names when scenarios exist', async () => {
|
|
69
90
|
mockListScenarios.mockReturnValueOnce(['basic', 'advanced', 'stress_test']);
|
|
@@ -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([], {});
|
|
@@ -55,7 +55,8 @@ export default class WorkflowRun extends Command {
|
|
|
55
55
|
}),
|
|
56
56
|
'task-queue': Flags.string({
|
|
57
57
|
char: 'q',
|
|
58
|
-
description: 'Task queue name for workflow execution'
|
|
58
|
+
description: 'Task queue name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
|
|
59
|
+
env: 'OUTPUT_CATALOG_ID'
|
|
59
60
|
}),
|
|
60
61
|
format: Flags.string({
|
|
61
62
|
char: 'f',
|
|
@@ -13,6 +13,7 @@ vi.mock('#utils/sleep.js', () => ({
|
|
|
13
13
|
describe('workflow run command', () => {
|
|
14
14
|
beforeEach(async () => {
|
|
15
15
|
vi.clearAllMocks();
|
|
16
|
+
delete process.env.OUTPUT_CATALOG_ID;
|
|
16
17
|
const { resolveInput } = await import('#utils/resolve_input.js');
|
|
17
18
|
const { sleep } = await import('#utils/sleep.js');
|
|
18
19
|
vi.mocked(resolveInput).mockResolvedValue({});
|
|
@@ -28,7 +28,8 @@ export default class WorkflowStart extends Command {
|
|
|
28
28
|
}),
|
|
29
29
|
'task-queue': Flags.string({
|
|
30
30
|
char: 'q',
|
|
31
|
-
description: 'Task queue name for workflow execution'
|
|
31
|
+
description: 'Task queue name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
|
|
32
|
+
env: 'OUTPUT_CATALOG_ID'
|
|
32
33
|
})
|
|
33
34
|
};
|
|
34
35
|
async run() {
|
|
@@ -5,6 +5,7 @@ vi.mock('../../api/generated/api.js', () => ({
|
|
|
5
5
|
describe('workflow start command', () => {
|
|
6
6
|
beforeEach(() => {
|
|
7
7
|
vi.clearAllMocks();
|
|
8
|
+
delete process.env.OUTPUT_CATALOG_ID;
|
|
8
9
|
});
|
|
9
10
|
describe('command definition', () => {
|
|
10
11
|
it('should export a valid OCLIF command', async () => {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
export const CommandFooter = ({ hints }) => (_jsx(Box, { marginTop: 1, children: hints.map((hint, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { dimColor: true, children: ' | ' }), _jsx(Text, { dimColor: true, children: '(' }), _jsx(Text, { dimColor: true, bold: true, children: hint.key }), _jsx(Text, { dimColor: true, children: ')' }), _jsx(Text, { dimColor: true, children: ` ${hint.label}` })] }, hint.key))) }));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface StatusDisplay {
|
|
3
|
+
icon: string;
|
|
4
|
+
color: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const resolveStatus: (status: string) => StatusDisplay;
|
|
7
|
+
export declare const statusColor: (status: string) => string;
|
|
8
|
+
export declare const StatusIcon: React.FC<{
|
|
9
|
+
status: string;
|
|
10
|
+
}>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
const STATUS_MAP = {
|
|
4
|
+
// Docker service health
|
|
5
|
+
healthy: { icon: '●', color: 'green' },
|
|
6
|
+
unhealthy: { icon: '○', color: 'red' },
|
|
7
|
+
starting: { icon: '◐', color: 'yellow' },
|
|
8
|
+
none: { icon: '●', color: 'blue' },
|
|
9
|
+
exited: { icon: '✗', color: 'red' },
|
|
10
|
+
// Workflow run status
|
|
11
|
+
running: { icon: '●', color: 'blue' },
|
|
12
|
+
completed: { icon: '●', color: 'green' },
|
|
13
|
+
failed: { icon: '✗', color: 'red' },
|
|
14
|
+
canceled: { icon: '○', color: 'gray' },
|
|
15
|
+
terminated: { icon: '✗', color: 'red' },
|
|
16
|
+
timed_out: { icon: '✗', color: 'red' },
|
|
17
|
+
continued: { icon: '↻', color: 'blue' }
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_DISPLAY = { icon: '?', color: 'white' };
|
|
20
|
+
export const resolveStatus = (status) => STATUS_MAP[status] ?? DEFAULT_DISPLAY;
|
|
21
|
+
export const statusColor = (status) => resolveStatus(status).color;
|
|
22
|
+
export const StatusIcon = ({ status }) => {
|
|
23
|
+
const { icon, color } = resolveStatus(status);
|
|
24
|
+
return _jsx(Text, { color: color, children: icon });
|
|
25
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { statusColor } from '#components/status_icon.js';
|
|
4
|
+
export const WorkflowSummarySection = ({ summary }) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "\uD83D\uDCCB Workflows" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: statusColor('running'), children: [summary.running, " running"] }), _jsx(Text, { children: ", " }), _jsxs(Text, { color: statusColor('failed'), children: [summary.failed, " failed"] }), _jsx(Text, { children: ", " }), _jsxs(Text, { color: statusColor('completed'), children: [summary.completed, " complete"] })] })] }));
|
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) {
|
|
@@ -105,7 +105,10 @@ function getTodoWriteMessage(message) {
|
|
|
105
105
|
if (message.type !== 'assistant') {
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
|
-
const todoWriteMessage = message.message.content.find((c) =>
|
|
108
|
+
const todoWriteMessage = message.message.content.find((c) => {
|
|
109
|
+
const block = c;
|
|
110
|
+
return block.type === 'tool_use' && block.name === 'TodoWrite';
|
|
111
|
+
});
|
|
109
112
|
return todoWriteMessage ?? null;
|
|
110
113
|
}
|
|
111
114
|
function applyInstructions(message, instructions) {
|
|
@@ -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';
|