@outputai/cli 0.1.4 → 0.1.5
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/dist/api/generated/api.d.ts +820 -0
- package/dist/api/generated/api.js +226 -0
- package/dist/api/http_client.d.ts +27 -0
- package/dist/api/http_client.js +71 -0
- package/dist/api/orval_post_process.d.ts +11 -0
- package/dist/api/orval_post_process.js +46 -0
- package/dist/api/parser.d.ts +17 -0
- package/dist/api/parser.js +68 -0
- package/dist/assets/config/costs.yml +309 -0
- package/dist/assets/docker/docker-compose-dev.yml +146 -0
- package/dist/commands/credentials/edit.d.ts +10 -0
- package/dist/commands/credentials/edit.js +67 -0
- package/dist/commands/credentials/edit.spec.d.ts +1 -0
- package/dist/commands/credentials/edit.spec.js +73 -0
- package/dist/commands/credentials/get.d.ts +13 -0
- package/dist/commands/credentials/get.js +46 -0
- package/dist/commands/credentials/get.spec.d.ts +1 -0
- package/dist/commands/credentials/get.spec.js +74 -0
- package/dist/commands/credentials/init.d.ts +11 -0
- package/dist/commands/credentials/init.js +45 -0
- package/dist/commands/credentials/init.spec.d.ts +1 -0
- package/dist/commands/credentials/init.spec.js +68 -0
- package/dist/commands/credentials/show.d.ts +10 -0
- package/dist/commands/credentials/show.js +33 -0
- package/dist/commands/credentials/show.spec.d.ts +1 -0
- package/dist/commands/credentials/show.spec.js +57 -0
- package/dist/commands/dev/eject.d.ts +11 -0
- package/dist/commands/dev/eject.js +58 -0
- package/dist/commands/dev/eject.spec.d.ts +1 -0
- package/dist/commands/dev/eject.spec.js +109 -0
- package/dist/commands/dev/index.d.ts +14 -0
- package/dist/commands/dev/index.js +173 -0
- package/dist/commands/dev/index.spec.d.ts +1 -0
- package/dist/commands/dev/index.spec.js +239 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.js +37 -0
- package/dist/commands/init.spec.d.ts +1 -0
- package/dist/commands/init.spec.js +100 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +120 -0
- package/dist/commands/update.spec.d.ts +1 -0
- package/dist/commands/update.spec.js +178 -0
- package/dist/commands/workflow/cost.d.ts +16 -0
- package/dist/commands/workflow/cost.js +71 -0
- package/dist/commands/workflow/cost.spec.d.ts +1 -0
- package/dist/commands/workflow/cost.spec.js +47 -0
- package/dist/commands/workflow/dataset/generate.d.ts +22 -0
- package/dist/commands/workflow/dataset/generate.js +143 -0
- package/dist/commands/workflow/dataset/list.d.ts +12 -0
- package/dist/commands/workflow/dataset/list.js +87 -0
- package/dist/commands/workflow/debug.d.ts +16 -0
- package/dist/commands/workflow/debug.js +60 -0
- package/dist/commands/workflow/debug.spec.d.ts +1 -0
- package/dist/commands/workflow/debug.spec.js +34 -0
- package/dist/commands/workflow/generate.d.ts +17 -0
- package/dist/commands/workflow/generate.js +85 -0
- package/dist/commands/workflow/generate.spec.d.ts +1 -0
- package/dist/commands/workflow/generate.spec.js +115 -0
- package/dist/commands/workflow/list.d.ts +22 -0
- package/dist/commands/workflow/list.js +152 -0
- package/dist/commands/workflow/list.spec.d.ts +1 -0
- package/dist/commands/workflow/list.spec.js +99 -0
- package/dist/commands/workflow/plan.d.ts +12 -0
- package/dist/commands/workflow/plan.js +66 -0
- package/dist/commands/workflow/plan.spec.d.ts +1 -0
- package/dist/commands/workflow/plan.spec.js +341 -0
- package/dist/commands/workflow/reset.d.ts +14 -0
- package/dist/commands/workflow/reset.js +51 -0
- package/dist/commands/workflow/result.d.ts +13 -0
- package/dist/commands/workflow/result.js +46 -0
- package/dist/commands/workflow/result.spec.d.ts +1 -0
- package/dist/commands/workflow/result.spec.js +23 -0
- package/dist/commands/workflow/run.d.ts +16 -0
- package/dist/commands/workflow/run.js +97 -0
- package/dist/commands/workflow/run.spec.d.ts +1 -0
- package/dist/commands/workflow/run.spec.js +110 -0
- package/dist/commands/workflow/runs/list.d.ts +14 -0
- package/dist/commands/workflow/runs/list.js +104 -0
- package/dist/commands/workflow/start.d.ts +15 -0
- package/dist/commands/workflow/start.js +62 -0
- package/dist/commands/workflow/start.spec.d.ts +1 -0
- package/dist/commands/workflow/start.spec.js +28 -0
- package/dist/commands/workflow/status.d.ts +13 -0
- package/dist/commands/workflow/status.js +57 -0
- package/dist/commands/workflow/status.spec.d.ts +1 -0
- package/dist/commands/workflow/status.spec.js +33 -0
- package/dist/commands/workflow/stop.d.ts +10 -0
- package/dist/commands/workflow/stop.js +31 -0
- package/dist/commands/workflow/stop.spec.d.ts +1 -0
- package/dist/commands/workflow/stop.spec.js +17 -0
- package/dist/commands/workflow/terminate.d.ts +13 -0
- package/dist/commands/workflow/terminate.js +39 -0
- package/dist/commands/workflow/test_eval.d.ts +20 -0
- package/dist/commands/workflow/test_eval.js +151 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.js +47 -0
- package/dist/generated/framework_version.json +3 -0
- package/dist/hooks/init.d.ts +3 -0
- package/dist/hooks/init.js +30 -0
- package/dist/hooks/init.spec.d.ts +1 -0
- package/dist/hooks/init.spec.js +54 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.spec.d.ts +1 -0
- package/dist/index.spec.js +6 -0
- package/dist/services/claude_client.d.ts +30 -0
- package/dist/services/claude_client.integration.test.d.ts +1 -0
- package/dist/services/claude_client.integration.test.js +43 -0
- package/dist/services/claude_client.js +215 -0
- package/dist/services/claude_client.spec.d.ts +1 -0
- package/dist/services/claude_client.spec.js +145 -0
- package/dist/services/coding_agents.d.ts +36 -0
- package/dist/services/coding_agents.js +236 -0
- package/dist/services/coding_agents.spec.d.ts +1 -0
- package/dist/services/coding_agents.spec.js +256 -0
- package/dist/services/copy_assets.spec.d.ts +1 -0
- package/dist/services/copy_assets.spec.js +22 -0
- package/dist/services/cost_calculator.d.ts +18 -0
- package/dist/services/cost_calculator.js +359 -0
- package/dist/services/cost_calculator.spec.d.ts +1 -0
- package/dist/services/cost_calculator.spec.js +540 -0
- package/dist/services/credentials_service.d.ts +12 -0
- package/dist/services/credentials_service.integration.test.d.ts +1 -0
- package/dist/services/credentials_service.integration.test.js +66 -0
- package/dist/services/credentials_service.js +64 -0
- package/dist/services/credentials_service.spec.d.ts +1 -0
- package/dist/services/credentials_service.spec.js +106 -0
- package/dist/services/datasets.d.ts +20 -0
- package/dist/services/datasets.js +132 -0
- package/dist/services/docker.d.ts +39 -0
- package/dist/services/docker.js +160 -0
- package/dist/services/docker.spec.d.ts +1 -0
- package/dist/services/docker.spec.js +124 -0
- package/dist/services/env_configurator.d.ts +15 -0
- package/dist/services/env_configurator.js +163 -0
- package/dist/services/env_configurator.spec.d.ts +1 -0
- package/dist/services/env_configurator.spec.js +192 -0
- package/dist/services/generate_plan_name@v1.prompt +24 -0
- package/dist/services/messages.d.ts +9 -0
- package/dist/services/messages.js +338 -0
- package/dist/services/messages.spec.d.ts +1 -0
- package/dist/services/messages.spec.js +55 -0
- package/dist/services/npm_update_service.d.ts +6 -0
- package/dist/services/npm_update_service.js +87 -0
- package/dist/services/npm_update_service.spec.d.ts +1 -0
- package/dist/services/npm_update_service.spec.js +104 -0
- package/dist/services/project_scaffold.d.ts +31 -0
- package/dist/services/project_scaffold.js +212 -0
- package/dist/services/project_scaffold.spec.d.ts +1 -0
- package/dist/services/project_scaffold.spec.js +122 -0
- package/dist/services/s3_trace_downloader.d.ts +12 -0
- package/dist/services/s3_trace_downloader.js +57 -0
- package/dist/services/template_processor.d.ts +14 -0
- package/dist/services/template_processor.js +57 -0
- package/dist/services/trace_reader.d.ts +16 -0
- package/dist/services/trace_reader.js +57 -0
- package/dist/services/trace_reader.spec.d.ts +1 -0
- package/dist/services/trace_reader.spec.js +78 -0
- package/dist/services/version_check.d.ts +6 -0
- package/dist/services/version_check.js +52 -0
- package/dist/services/version_check.spec.d.ts +1 -0
- package/dist/services/version_check.spec.js +106 -0
- package/dist/services/workflow_builder.d.ts +16 -0
- package/dist/services/workflow_builder.js +86 -0
- package/dist/services/workflow_builder.spec.d.ts +1 -0
- package/dist/services/workflow_builder.spec.js +165 -0
- package/dist/services/workflow_generator.d.ts +5 -0
- package/dist/services/workflow_generator.js +40 -0
- package/dist/services/workflow_generator.spec.d.ts +1 -0
- package/dist/services/workflow_generator.spec.js +77 -0
- package/dist/services/workflow_planner.d.ts +15 -0
- package/dist/services/workflow_planner.js +48 -0
- package/dist/services/workflow_planner.spec.d.ts +1 -0
- package/dist/services/workflow_planner.spec.js +122 -0
- package/dist/services/workflow_runs.d.ts +14 -0
- package/dist/services/workflow_runs.js +25 -0
- package/dist/templates/agent_instructions/CLAUDE.md.template +19 -0
- package/dist/templates/agent_instructions/dotclaude/settings.json.template +29 -0
- package/dist/templates/project/.env.example.template +9 -0
- package/dist/templates/project/.gitignore.template +35 -0
- package/dist/templates/project/README.md.template +100 -0
- package/dist/templates/project/config/costs.yml.template +29 -0
- package/dist/templates/project/package.json.template +25 -0
- package/dist/templates/project/src/clients/jina.ts.template +30 -0
- package/dist/templates/project/src/shared/utils/string.ts.template +3 -0
- package/dist/templates/project/src/shared/utils/url.ts.template +15 -0
- package/dist/templates/project/src/workflows/blog_evaluator/evaluators.ts.template +23 -0
- package/dist/templates/project/src/workflows/blog_evaluator/prompts/signal_noise@v1.prompt.template +26 -0
- package/dist/templates/project/src/workflows/blog_evaluator/scenarios/paulgraham_hwh.json.template +3 -0
- package/dist/templates/project/src/workflows/blog_evaluator/steps.ts.template +27 -0
- package/dist/templates/project/src/workflows/blog_evaluator/types.ts.template +30 -0
- package/dist/templates/project/src/workflows/blog_evaluator/utils.ts.template +15 -0
- package/dist/templates/project/src/workflows/blog_evaluator/workflow.ts.template +27 -0
- package/dist/templates/project/tsconfig.json.template +20 -0
- package/dist/templates/workflow/README.md.template +216 -0
- package/dist/templates/workflow/evaluators.ts.template +21 -0
- package/dist/templates/workflow/prompts/example@v1.prompt.template +15 -0
- package/dist/templates/workflow/scenarios/test_input.json.template +3 -0
- package/dist/templates/workflow/steps.ts.template +20 -0
- package/dist/templates/workflow/types.ts.template +13 -0
- package/dist/templates/workflow/workflow.ts.template +23 -0
- package/dist/test_helpers/mocks.d.ts +38 -0
- package/dist/test_helpers/mocks.js +77 -0
- package/dist/types/cost.d.ts +149 -0
- package/dist/types/cost.js +6 -0
- package/dist/types/domain.d.ts +20 -0
- package/dist/types/domain.js +4 -0
- package/dist/types/errors.d.ts +68 -0
- package/dist/types/errors.js +100 -0
- package/dist/types/errors.spec.d.ts +1 -0
- package/dist/types/errors.spec.js +18 -0
- package/dist/types/generator.d.ts +26 -0
- package/dist/types/generator.js +1 -0
- package/dist/types/trace.d.ts +161 -0
- package/dist/types/trace.js +18 -0
- package/dist/utils/claude.d.ts +5 -0
- package/dist/utils/claude.js +19 -0
- package/dist/utils/claude.spec.d.ts +1 -0
- package/dist/utils/claude.spec.js +119 -0
- package/dist/utils/constants.d.ts +5 -0
- package/dist/utils/constants.js +4 -0
- package/dist/utils/cost_formatter.d.ts +5 -0
- package/dist/utils/cost_formatter.js +218 -0
- package/dist/utils/date_formatter.d.ts +23 -0
- package/dist/utils/date_formatter.js +49 -0
- package/dist/utils/env_loader.d.ts +1 -0
- package/dist/utils/env_loader.js +22 -0
- package/dist/utils/env_loader.spec.d.ts +1 -0
- package/dist/utils/env_loader.spec.js +43 -0
- package/dist/utils/error_handler.d.ts +8 -0
- package/dist/utils/error_handler.js +71 -0
- package/dist/utils/error_utils.d.ts +24 -0
- package/dist/utils/error_utils.js +87 -0
- package/dist/utils/file_system.d.ts +3 -0
- package/dist/utils/file_system.js +33 -0
- package/dist/utils/format_workflow_result.d.ts +5 -0
- package/dist/utils/format_workflow_result.js +18 -0
- package/dist/utils/format_workflow_result.spec.d.ts +1 -0
- package/dist/utils/format_workflow_result.spec.js +81 -0
- package/dist/utils/framework_version.d.ts +4 -0
- package/dist/utils/framework_version.js +4 -0
- package/dist/utils/framework_version.spec.d.ts +1 -0
- package/dist/utils/framework_version.spec.js +13 -0
- package/dist/utils/header_utils.d.ts +12 -0
- package/dist/utils/header_utils.js +29 -0
- package/dist/utils/header_utils.spec.d.ts +1 -0
- package/dist/utils/header_utils.spec.js +52 -0
- package/dist/utils/input_parser.d.ts +1 -0
- package/dist/utils/input_parser.js +19 -0
- package/dist/utils/output_formatter.d.ts +2 -0
- package/dist/utils/output_formatter.js +11 -0
- package/dist/utils/paths.d.ts +25 -0
- package/dist/utils/paths.js +36 -0
- package/dist/utils/process.d.ts +4 -0
- package/dist/utils/process.js +50 -0
- package/dist/utils/resolve_input.d.ts +1 -0
- package/dist/utils/resolve_input.js +22 -0
- package/dist/utils/scenario_resolver.d.ts +9 -0
- package/dist/utils/scenario_resolver.js +93 -0
- package/dist/utils/scenario_resolver.spec.d.ts +1 -0
- package/dist/utils/scenario_resolver.spec.js +214 -0
- package/dist/utils/secret_sanitizer.d.ts +1 -0
- package/dist/utils/secret_sanitizer.js +29 -0
- package/dist/utils/sleep.d.ts +5 -0
- package/dist/utils/sleep.js +5 -0
- package/dist/utils/template.d.ts +9 -0
- package/dist/utils/template.js +30 -0
- package/dist/utils/template.spec.d.ts +1 -0
- package/dist/utils/template.spec.js +77 -0
- package/dist/utils/trace_extractor.d.ts +27 -0
- package/dist/utils/trace_extractor.js +53 -0
- package/dist/utils/trace_formatter.d.ts +11 -0
- package/dist/utils/trace_formatter.js +402 -0
- package/dist/utils/validation.d.ts +13 -0
- package/dist/utils/validation.js +25 -0
- package/dist/utils/validation.spec.d.ts +1 -0
- package/dist/utils/validation.spec.js +140 -0
- package/package.json +4 -4
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fetchLatestVersion, isOutdated } from '#services/npm_update_service.js';
|
|
4
|
+
const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
5
|
+
const CACHE_FILENAME = 'version_check.json';
|
|
6
|
+
async function readCache(cacheDir, currentVersion) {
|
|
7
|
+
try {
|
|
8
|
+
const raw = await readFile(join(cacheDir, CACHE_FILENAME), 'utf-8');
|
|
9
|
+
const cache = JSON.parse(raw);
|
|
10
|
+
if (Date.now() - cache.timestamp > CACHE_TTL_MS) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (cache.result.currentVersion !== currentVersion) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return cache.result;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function writeCache(cacheDir, result) {
|
|
23
|
+
try {
|
|
24
|
+
await mkdir(cacheDir, { recursive: true });
|
|
25
|
+
const cache = { timestamp: Date.now(), result };
|
|
26
|
+
await writeFile(join(cacheDir, CACHE_FILENAME), JSON.stringify(cache));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Silently ignore cache write failures
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function checkForUpdate(currentVersion, cacheDir) {
|
|
33
|
+
if (cacheDir) {
|
|
34
|
+
const cached = await readCache(cacheDir, currentVersion);
|
|
35
|
+
if (cached) {
|
|
36
|
+
return cached;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const latestVersion = await fetchLatestVersion();
|
|
40
|
+
if (!latestVersion) {
|
|
41
|
+
return { updateAvailable: false, currentVersion, latestVersion: currentVersion };
|
|
42
|
+
}
|
|
43
|
+
const result = {
|
|
44
|
+
updateAvailable: isOutdated(currentVersion, latestVersion),
|
|
45
|
+
currentVersion,
|
|
46
|
+
latestVersion
|
|
47
|
+
};
|
|
48
|
+
if (cacheDir) {
|
|
49
|
+
await writeCache(cacheDir, result);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { checkForUpdate } from './version_check.js';
|
|
3
|
+
import { fetchLatestVersion, isOutdated } from '#services/npm_update_service.js';
|
|
4
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
5
|
+
vi.mock('#services/npm_update_service.js', () => ({
|
|
6
|
+
fetchLatestVersion: vi.fn(),
|
|
7
|
+
isOutdated: vi.fn()
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('node:fs/promises', () => ({
|
|
10
|
+
readFile: vi.fn(),
|
|
11
|
+
writeFile: vi.fn(),
|
|
12
|
+
mkdir: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
describe('version_check', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
describe('checkForUpdate', () => {
|
|
19
|
+
it('should return updateAvailable true when outdated', async () => {
|
|
20
|
+
vi.mocked(fetchLatestVersion).mockResolvedValue('1.0.0');
|
|
21
|
+
vi.mocked(isOutdated).mockReturnValue(true);
|
|
22
|
+
const result = await checkForUpdate('0.8.4');
|
|
23
|
+
expect(result).toEqual({
|
|
24
|
+
updateAvailable: true,
|
|
25
|
+
currentVersion: '0.8.4',
|
|
26
|
+
latestVersion: '1.0.0'
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it('should return updateAvailable false when up to date', async () => {
|
|
30
|
+
vi.mocked(fetchLatestVersion).mockResolvedValue('0.8.4');
|
|
31
|
+
vi.mocked(isOutdated).mockReturnValue(false);
|
|
32
|
+
const result = await checkForUpdate('0.8.4');
|
|
33
|
+
expect(result).toEqual({
|
|
34
|
+
updateAvailable: false,
|
|
35
|
+
currentVersion: '0.8.4',
|
|
36
|
+
latestVersion: '0.8.4'
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('should return updateAvailable false when fetch fails', async () => {
|
|
40
|
+
vi.mocked(fetchLatestVersion).mockResolvedValue(null);
|
|
41
|
+
const result = await checkForUpdate('0.8.4');
|
|
42
|
+
expect(result.updateAvailable).toBe(false);
|
|
43
|
+
expect(isOutdated).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('caching', () => {
|
|
47
|
+
const cacheDir = '/tmp/test-cache';
|
|
48
|
+
it('should return cached result when cache is fresh', async () => {
|
|
49
|
+
const cached = {
|
|
50
|
+
timestamp: Date.now() - 1000,
|
|
51
|
+
result: { updateAvailable: true, currentVersion: '0.8.4', latestVersion: '1.0.0' }
|
|
52
|
+
};
|
|
53
|
+
vi.mocked(readFile).mockResolvedValue(JSON.stringify(cached));
|
|
54
|
+
const result = await checkForUpdate('0.8.4', cacheDir);
|
|
55
|
+
expect(result).toEqual(cached.result);
|
|
56
|
+
expect(fetchLatestVersion).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
it('should fetch fresh result when cache is expired', async () => {
|
|
59
|
+
const cached = {
|
|
60
|
+
timestamp: Date.now() - (5 * 60 * 60 * 1000), // 5 hours ago
|
|
61
|
+
result: { updateAvailable: true, currentVersion: '0.8.4', latestVersion: '0.9.0' }
|
|
62
|
+
};
|
|
63
|
+
vi.mocked(readFile).mockResolvedValue(JSON.stringify(cached));
|
|
64
|
+
vi.mocked(fetchLatestVersion).mockResolvedValue('1.0.0');
|
|
65
|
+
vi.mocked(isOutdated).mockReturnValue(true);
|
|
66
|
+
const result = await checkForUpdate('0.8.4', cacheDir);
|
|
67
|
+
expect(fetchLatestVersion).toHaveBeenCalled();
|
|
68
|
+
expect(result.latestVersion).toBe('1.0.0');
|
|
69
|
+
});
|
|
70
|
+
it('should fetch fresh result when cached version differs from current', async () => {
|
|
71
|
+
const cached = {
|
|
72
|
+
timestamp: Date.now() - 1000,
|
|
73
|
+
result: { updateAvailable: true, currentVersion: '0.8.4', latestVersion: '1.0.0' }
|
|
74
|
+
};
|
|
75
|
+
vi.mocked(readFile).mockResolvedValue(JSON.stringify(cached));
|
|
76
|
+
vi.mocked(fetchLatestVersion).mockResolvedValue('1.0.0');
|
|
77
|
+
vi.mocked(isOutdated).mockReturnValue(true);
|
|
78
|
+
const result = await checkForUpdate('0.9.0', cacheDir);
|
|
79
|
+
expect(fetchLatestVersion).toHaveBeenCalled();
|
|
80
|
+
expect(result.currentVersion).toBe('0.9.0');
|
|
81
|
+
});
|
|
82
|
+
it('should fetch fresh result when cache file is missing', async () => {
|
|
83
|
+
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
84
|
+
vi.mocked(fetchLatestVersion).mockResolvedValue('1.0.0');
|
|
85
|
+
vi.mocked(isOutdated).mockReturnValue(true);
|
|
86
|
+
const result = await checkForUpdate('0.8.4', cacheDir);
|
|
87
|
+
expect(fetchLatestVersion).toHaveBeenCalled();
|
|
88
|
+
expect(result.latestVersion).toBe('1.0.0');
|
|
89
|
+
});
|
|
90
|
+
it('should write cache after fresh fetch', async () => {
|
|
91
|
+
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
92
|
+
vi.mocked(fetchLatestVersion).mockResolvedValue('1.0.0');
|
|
93
|
+
vi.mocked(isOutdated).mockReturnValue(true);
|
|
94
|
+
await checkForUpdate('0.8.4', cacheDir);
|
|
95
|
+
expect(mkdir).toHaveBeenCalledWith(cacheDir, { recursive: true });
|
|
96
|
+
expect(writeFile).toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
it('should skip caching when no cacheDir provided', async () => {
|
|
99
|
+
vi.mocked(fetchLatestVersion).mockResolvedValue('1.0.0');
|
|
100
|
+
vi.mocked(isOutdated).mockReturnValue(true);
|
|
101
|
+
await checkForUpdate('0.8.4');
|
|
102
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
103
|
+
expect(writeFile).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a workflow from a plan file using the /outputai:build_workflow slash command
|
|
3
|
+
* @param planFilePath - Absolute path to the plan file
|
|
4
|
+
* @param workflowDir - Absolute path to the workflow directory
|
|
5
|
+
* @param workflowName - Name of the workflow
|
|
6
|
+
* @param additionalInstructions - Optional additional instructions
|
|
7
|
+
* @returns Implementation output from claude-code
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildWorkflow(planFilePath: string, workflowDir: string, workflowName: string, additionalInstructions?: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Interactive loop for refining workflow implementation
|
|
12
|
+
* Similar to the plan modification loop pattern
|
|
13
|
+
* @param originalOutput - Initial implementation output from claude-code
|
|
14
|
+
* @returns Final accepted output
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildWorkflowInteractiveLoop(originalOutput: string): Promise<string>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow builder service for implementing workflows from plan files
|
|
3
|
+
*/
|
|
4
|
+
import { BUILD_COMMAND_OPTIONS, invokeBuildWorkflow as invokeBuildWorkflowFromClient, replyToClaude } from './claude_client.js';
|
|
5
|
+
import { input } from '@inquirer/prompts';
|
|
6
|
+
import { ux } from '@oclif/core';
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { getErrorMessage } from '#utils/error_utils.js';
|
|
10
|
+
const ACCEPT_KEY = 'ACCEPT';
|
|
11
|
+
const SEPARATOR_LINE = '─'.repeat(80);
|
|
12
|
+
function displayImplementationOutput(output, message) {
|
|
13
|
+
ux.stdout('\n');
|
|
14
|
+
ux.stdout(ux.colorize('green', message));
|
|
15
|
+
ux.stdout('\n');
|
|
16
|
+
ux.stdout(ux.colorize('dim', SEPARATOR_LINE));
|
|
17
|
+
ux.stdout(output);
|
|
18
|
+
ux.stdout(ux.colorize('dim', SEPARATOR_LINE));
|
|
19
|
+
ux.stdout('\n');
|
|
20
|
+
}
|
|
21
|
+
async function promptForModification() {
|
|
22
|
+
return input({
|
|
23
|
+
message: `Review the implementation. Type "${ACCEPT_KEY}" to accept, or describe modifications:`,
|
|
24
|
+
default: ACCEPT_KEY
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function isAcceptCommand(modification) {
|
|
28
|
+
return modification.trim().toUpperCase() === ACCEPT_KEY;
|
|
29
|
+
}
|
|
30
|
+
function isEmpty(modification) {
|
|
31
|
+
return modification.trim() === '';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build a workflow from a plan file using the /outputai:build_workflow slash command
|
|
35
|
+
* @param planFilePath - Absolute path to the plan file
|
|
36
|
+
* @param workflowDir - Absolute path to the workflow directory
|
|
37
|
+
* @param workflowName - Name of the workflow
|
|
38
|
+
* @param additionalInstructions - Optional additional instructions
|
|
39
|
+
* @returns Implementation output from claude-code
|
|
40
|
+
*/
|
|
41
|
+
export async function buildWorkflow(planFilePath, workflowDir, workflowName, additionalInstructions) {
|
|
42
|
+
try {
|
|
43
|
+
await fs.access(planFilePath);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error(`Plan file not found: ${planFilePath}`);
|
|
47
|
+
}
|
|
48
|
+
await fs.mkdir(workflowDir, { recursive: true });
|
|
49
|
+
const absolutePlanPath = path.resolve(planFilePath);
|
|
50
|
+
const absoluteWorkflowDir = path.resolve(workflowDir);
|
|
51
|
+
return invokeBuildWorkflowFromClient(absolutePlanPath, absoluteWorkflowDir, workflowName, additionalInstructions);
|
|
52
|
+
}
|
|
53
|
+
async function processModification(modification, currentOutput) {
|
|
54
|
+
if (isEmpty(modification)) {
|
|
55
|
+
ux.stdout(ux.colorize('yellow', 'Please provide modification instructions or type ACCEPT to continue.'));
|
|
56
|
+
return currentOutput;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const updatedOutput = await replyToClaude(modification, BUILD_COMMAND_OPTIONS);
|
|
60
|
+
displayImplementationOutput(updatedOutput, '✓ Implementation updated!');
|
|
61
|
+
return updatedOutput;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
ux.error(`Failed to apply modifications: ${getErrorMessage(error)}`);
|
|
65
|
+
ux.stdout('Continuing with previous version...\n');
|
|
66
|
+
return currentOutput;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function interactiveRefinementLoop(currentOutput) {
|
|
70
|
+
const modification = await promptForModification();
|
|
71
|
+
if (isAcceptCommand(modification)) {
|
|
72
|
+
return currentOutput;
|
|
73
|
+
}
|
|
74
|
+
const updatedOutput = await processModification(modification, currentOutput);
|
|
75
|
+
return interactiveRefinementLoop(updatedOutput);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Interactive loop for refining workflow implementation
|
|
79
|
+
* Similar to the plan modification loop pattern
|
|
80
|
+
* @param originalOutput - Initial implementation output from claude-code
|
|
81
|
+
* @returns Final accepted output
|
|
82
|
+
*/
|
|
83
|
+
export async function buildWorkflowInteractiveLoop(originalOutput) {
|
|
84
|
+
displayImplementationOutput(originalOutput, '✓ Workflow implementation complete!');
|
|
85
|
+
return interactiveRefinementLoop(originalOutput);
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { buildWorkflow, buildWorkflowInteractiveLoop } from './workflow_builder.js';
|
|
3
|
+
import { BUILD_COMMAND_OPTIONS, invokeBuildWorkflow, replyToClaude } from './claude_client.js';
|
|
4
|
+
import { input } from '@inquirer/prompts';
|
|
5
|
+
import { ux } from '@oclif/core';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
vi.mock('./claude_client.js');
|
|
8
|
+
vi.mock('@inquirer/prompts');
|
|
9
|
+
vi.mock('@oclif/core', () => ({
|
|
10
|
+
ux: {
|
|
11
|
+
stdout: vi.fn(),
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
colorize: vi.fn((_color, text) => text)
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('node:fs/promises');
|
|
17
|
+
describe('workflow-builder service', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
describe('buildWorkflow', () => {
|
|
22
|
+
it('should build workflow from plan file', async () => {
|
|
23
|
+
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
24
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
25
|
+
vi.mocked(invokeBuildWorkflow).mockResolvedValue('Implementation complete!');
|
|
26
|
+
const result = await buildWorkflow('/path/to/plan.md', '/path/to/workflows/test_workflow', 'test_workflow');
|
|
27
|
+
expect(fs.access).toHaveBeenCalledWith('/path/to/plan.md');
|
|
28
|
+
expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining('test_workflow'), { recursive: true });
|
|
29
|
+
expect(invokeBuildWorkflow).toHaveBeenCalledWith(expect.stringContaining('plan.md'), expect.stringContaining('test_workflow'), 'test_workflow', undefined);
|
|
30
|
+
expect(result).toBe('Implementation complete!');
|
|
31
|
+
});
|
|
32
|
+
it('should pass additional instructions to claude-code', async () => {
|
|
33
|
+
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
34
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
35
|
+
vi.mocked(invokeBuildWorkflow).mockResolvedValue('Done!');
|
|
36
|
+
await buildWorkflow('/plan.md', '/workflows', 'test', 'Use TypeScript only');
|
|
37
|
+
expect(invokeBuildWorkflow).toHaveBeenCalledWith(expect.any(String), expect.any(String), 'test', 'Use TypeScript only');
|
|
38
|
+
});
|
|
39
|
+
it('should throw error if plan file does not exist', async () => {
|
|
40
|
+
vi.mocked(fs.access).mockRejectedValue(new Error('ENOENT'));
|
|
41
|
+
await expect(buildWorkflow('/nonexistent/plan.md', '/workflows', 'test')).rejects.toThrow('Plan file not found: /nonexistent/plan.md');
|
|
42
|
+
expect(fs.mkdir).not.toHaveBeenCalled();
|
|
43
|
+
expect(invokeBuildWorkflow).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
it('should create workflow directory if it does not exist', async () => {
|
|
46
|
+
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
47
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
48
|
+
vi.mocked(invokeBuildWorkflow).mockResolvedValue('Done');
|
|
49
|
+
await buildWorkflow('/plan.md', '/new/workflows/test', 'test');
|
|
50
|
+
expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining('test'), { recursive: true });
|
|
51
|
+
});
|
|
52
|
+
it('should resolve paths to absolute paths', async () => {
|
|
53
|
+
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
54
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
55
|
+
vi.mocked(invokeBuildWorkflow).mockResolvedValue('Done');
|
|
56
|
+
await buildWorkflow('relative/plan.md', 'relative/workflows', 'test');
|
|
57
|
+
// Should call with absolute paths
|
|
58
|
+
const calls = vi.mocked(invokeBuildWorkflow).mock.calls[0];
|
|
59
|
+
expect(calls[0]).toMatch(/^[/\\]/); // Absolute path starts with / or \
|
|
60
|
+
expect(calls[1]).toMatch(/^[/\\]/); // Absolute path starts with / or \
|
|
61
|
+
});
|
|
62
|
+
it('should propagate errors from claude-code invocation', async () => {
|
|
63
|
+
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
64
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
65
|
+
vi.mocked(invokeBuildWorkflow).mockRejectedValue(new Error('Claude API timeout'));
|
|
66
|
+
await expect(buildWorkflow('/plan.md', '/workflows', 'test')).rejects.toThrow('Claude API timeout');
|
|
67
|
+
});
|
|
68
|
+
it('should handle directory creation failures', async () => {
|
|
69
|
+
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
70
|
+
vi.mocked(fs.mkdir).mockRejectedValue(new Error('Permission denied'));
|
|
71
|
+
await expect(buildWorkflow('/plan.md', '/workflows', 'test')).rejects.toThrow('Permission denied');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('buildWorkflowInteractiveLoop', () => {
|
|
75
|
+
it('should accept implementation immediately when user types ACCEPT', async () => {
|
|
76
|
+
vi.mocked(input).mockResolvedValue('ACCEPT');
|
|
77
|
+
const result = await buildWorkflowInteractiveLoop('Initial implementation');
|
|
78
|
+
expect(result).toBe('Initial implementation');
|
|
79
|
+
expect(input).toHaveBeenCalledOnce();
|
|
80
|
+
expect(replyToClaude).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
it('should accept implementation when user types lowercase accept', async () => {
|
|
83
|
+
vi.mocked(input).mockResolvedValue('accept');
|
|
84
|
+
const result = await buildWorkflowInteractiveLoop('Initial implementation');
|
|
85
|
+
expect(result).toBe('Initial implementation');
|
|
86
|
+
expect(replyToClaude).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
it('should accept implementation with extra whitespace', async () => {
|
|
89
|
+
vi.mocked(input).mockResolvedValue(' ACCEPT ');
|
|
90
|
+
const result = await buildWorkflowInteractiveLoop('Initial implementation');
|
|
91
|
+
expect(result).toBe('Initial implementation');
|
|
92
|
+
expect(replyToClaude).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
it('should apply modifications and return updated implementation', async () => {
|
|
95
|
+
vi.mocked(input)
|
|
96
|
+
.mockResolvedValueOnce('Add error handling')
|
|
97
|
+
.mockResolvedValueOnce('ACCEPT');
|
|
98
|
+
vi.mocked(replyToClaude).mockResolvedValue('Updated implementation with error handling');
|
|
99
|
+
const result = await buildWorkflowInteractiveLoop('Initial implementation');
|
|
100
|
+
expect(replyToClaude).toHaveBeenCalledWith('Add error handling', BUILD_COMMAND_OPTIONS);
|
|
101
|
+
expect(result).toBe('Updated implementation with error handling');
|
|
102
|
+
expect(input).toHaveBeenCalledTimes(2);
|
|
103
|
+
});
|
|
104
|
+
it('should handle multiple modification rounds', async () => {
|
|
105
|
+
vi.mocked(input)
|
|
106
|
+
.mockResolvedValueOnce('Add logging')
|
|
107
|
+
.mockResolvedValueOnce('Add validation')
|
|
108
|
+
.mockResolvedValueOnce('ACCEPT');
|
|
109
|
+
vi.mocked(replyToClaude)
|
|
110
|
+
.mockResolvedValueOnce('Implementation with logging')
|
|
111
|
+
.mockResolvedValueOnce('Implementation with logging and validation');
|
|
112
|
+
const result = await buildWorkflowInteractiveLoop('Initial implementation');
|
|
113
|
+
expect(replyToClaude).toHaveBeenCalledTimes(2);
|
|
114
|
+
expect(replyToClaude).toHaveBeenNthCalledWith(1, 'Add logging', BUILD_COMMAND_OPTIONS);
|
|
115
|
+
expect(replyToClaude).toHaveBeenNthCalledWith(2, 'Add validation', BUILD_COMMAND_OPTIONS);
|
|
116
|
+
expect(result).toBe('Implementation with logging and validation');
|
|
117
|
+
});
|
|
118
|
+
it('should prompt again when user provides empty input', async () => {
|
|
119
|
+
vi.mocked(input)
|
|
120
|
+
.mockResolvedValueOnce('')
|
|
121
|
+
.mockResolvedValueOnce(' ')
|
|
122
|
+
.mockResolvedValueOnce('ACCEPT');
|
|
123
|
+
const result = await buildWorkflowInteractiveLoop('Initial implementation');
|
|
124
|
+
expect(result).toBe('Initial implementation');
|
|
125
|
+
expect(input).toHaveBeenCalledTimes(3);
|
|
126
|
+
expect(ux.stdout).toHaveBeenCalledWith(expect.stringContaining('provide modification instructions'));
|
|
127
|
+
});
|
|
128
|
+
it('should display implementation output to user', async () => {
|
|
129
|
+
vi.mocked(input).mockResolvedValue('ACCEPT');
|
|
130
|
+
await buildWorkflowInteractiveLoop('Implementation summary');
|
|
131
|
+
expect(ux.stdout).toHaveBeenCalledWith(expect.stringContaining('Implementation summary'));
|
|
132
|
+
});
|
|
133
|
+
it('should display updated implementation after modifications', async () => {
|
|
134
|
+
vi.mocked(input)
|
|
135
|
+
.mockResolvedValueOnce('Improve performance')
|
|
136
|
+
.mockResolvedValueOnce('ACCEPT');
|
|
137
|
+
vi.mocked(replyToClaude).mockResolvedValue('Optimized implementation');
|
|
138
|
+
await buildWorkflowInteractiveLoop('Initial');
|
|
139
|
+
expect(ux.stdout).toHaveBeenCalledWith(expect.stringContaining('Optimized implementation'));
|
|
140
|
+
});
|
|
141
|
+
it('should handle errors from replyToClaude gracefully', async () => {
|
|
142
|
+
vi.mocked(input)
|
|
143
|
+
.mockResolvedValueOnce('Invalid modification')
|
|
144
|
+
.mockResolvedValueOnce('ACCEPT');
|
|
145
|
+
vi.mocked(replyToClaude).mockRejectedValue(new Error('API error'));
|
|
146
|
+
const result = await buildWorkflowInteractiveLoop('Original implementation');
|
|
147
|
+
// Should return original implementation after error
|
|
148
|
+
expect(result).toBe('Original implementation');
|
|
149
|
+
expect(ux.error).toHaveBeenCalledWith(expect.stringContaining('Failed to apply modifications'));
|
|
150
|
+
expect(ux.stdout).toHaveBeenCalledWith(expect.stringContaining('Continuing with previous version'));
|
|
151
|
+
});
|
|
152
|
+
it('should continue looping after handling error', async () => {
|
|
153
|
+
vi.mocked(input)
|
|
154
|
+
.mockResolvedValueOnce('Bad request')
|
|
155
|
+
.mockResolvedValueOnce('Good request')
|
|
156
|
+
.mockResolvedValueOnce('ACCEPT');
|
|
157
|
+
vi.mocked(replyToClaude)
|
|
158
|
+
.mockRejectedValueOnce(new Error('API error'))
|
|
159
|
+
.mockResolvedValueOnce('Fixed implementation');
|
|
160
|
+
const result = await buildWorkflowInteractiveLoop('Initial');
|
|
161
|
+
expect(result).toBe('Fixed implementation');
|
|
162
|
+
expect(replyToClaude).toHaveBeenCalledTimes(2);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { WorkflowExistsError } from '#types/errors.js';
|
|
3
|
+
import { createTargetDir, getTemplateDir } from '#utils/paths.js';
|
|
4
|
+
import { validateWorkflowName, validateOutputDirectory } from '#utils/validation.js';
|
|
5
|
+
import { prepareTemplateVariables } from '#utils/template.js';
|
|
6
|
+
import { getTemplateFiles, processAllTemplates } from './template_processor.js';
|
|
7
|
+
/**
|
|
8
|
+
* Validate the generation configuration
|
|
9
|
+
*/
|
|
10
|
+
function validateConfig(config) {
|
|
11
|
+
validateWorkflowName(config.name);
|
|
12
|
+
validateOutputDirectory(config.outputDir);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check if target directory exists and handle accordingly
|
|
16
|
+
*/
|
|
17
|
+
import * as fsSync from 'node:fs';
|
|
18
|
+
async function checkTargetDirectory(targetDir, force) {
|
|
19
|
+
if (fsSync.existsSync(targetDir) && !force) {
|
|
20
|
+
throw new WorkflowExistsError('', targetDir);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate a new workflow
|
|
25
|
+
*/
|
|
26
|
+
export async function generateWorkflow(config) {
|
|
27
|
+
validateConfig(config);
|
|
28
|
+
const targetDir = createTargetDir(config.outputDir, config.name);
|
|
29
|
+
const templatesDir = getTemplateDir('workflow');
|
|
30
|
+
await checkTargetDirectory(targetDir, config.force);
|
|
31
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
32
|
+
const variables = prepareTemplateVariables(config.name, config.description || '');
|
|
33
|
+
const templateFiles = await getTemplateFiles(templatesDir);
|
|
34
|
+
const filesCreated = await processAllTemplates(templateFiles, targetDir, variables);
|
|
35
|
+
return {
|
|
36
|
+
workflowName: config.name,
|
|
37
|
+
targetDir,
|
|
38
|
+
filesCreated
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-syntax, init-declarations */
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import { generateWorkflow } from './workflow_generator.js';
|
|
7
|
+
describe('Workflow Generator', () => {
|
|
8
|
+
let tempDir;
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'workflow-gen-test-'));
|
|
11
|
+
});
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
describe('skeleton generation', () => {
|
|
16
|
+
it('should create prompts folder with prompt template', async () => {
|
|
17
|
+
const result = await generateWorkflow({
|
|
18
|
+
name: 'testWorkflow',
|
|
19
|
+
description: 'Test workflow',
|
|
20
|
+
outputDir: tempDir,
|
|
21
|
+
skeleton: true,
|
|
22
|
+
force: false
|
|
23
|
+
});
|
|
24
|
+
const promptPath = path.join(result.targetDir, 'prompts', 'example@v1.prompt');
|
|
25
|
+
const promptExists = await fs.access(promptPath).then(() => true).catch(() => false);
|
|
26
|
+
expect(promptExists).toBe(true);
|
|
27
|
+
expect(result.filesCreated).toContain('prompts/example@v1.prompt');
|
|
28
|
+
});
|
|
29
|
+
it('should create scenarios folder with test_input.json', async () => {
|
|
30
|
+
const result = await generateWorkflow({
|
|
31
|
+
name: 'testWorkflow',
|
|
32
|
+
description: 'Test workflow',
|
|
33
|
+
outputDir: tempDir,
|
|
34
|
+
skeleton: true,
|
|
35
|
+
force: false
|
|
36
|
+
});
|
|
37
|
+
const scenarioPath = path.join(result.targetDir, 'scenarios', 'test_input.json');
|
|
38
|
+
const scenarioExists = await fs.access(scenarioPath).then(() => true).catch(() => false);
|
|
39
|
+
expect(scenarioExists).toBe(true);
|
|
40
|
+
expect(result.filesCreated).toContain('scenarios/test_input.json');
|
|
41
|
+
});
|
|
42
|
+
it('should create valid JSON in scenario file', async () => {
|
|
43
|
+
const result = await generateWorkflow({
|
|
44
|
+
name: 'testWorkflow',
|
|
45
|
+
description: 'Test workflow',
|
|
46
|
+
outputDir: tempDir,
|
|
47
|
+
skeleton: true,
|
|
48
|
+
force: false
|
|
49
|
+
});
|
|
50
|
+
const scenarioPath = path.join(result.targetDir, 'scenarios', 'test_input.json');
|
|
51
|
+
const content = await fs.readFile(scenarioPath, 'utf-8');
|
|
52
|
+
const parsed = JSON.parse(content);
|
|
53
|
+
expect(parsed).toHaveProperty('text');
|
|
54
|
+
});
|
|
55
|
+
it('should create all expected skeleton files', async () => {
|
|
56
|
+
const result = await generateWorkflow({
|
|
57
|
+
name: 'testWorkflow',
|
|
58
|
+
description: 'Test workflow',
|
|
59
|
+
outputDir: tempDir,
|
|
60
|
+
skeleton: true,
|
|
61
|
+
force: false
|
|
62
|
+
});
|
|
63
|
+
const expectedFiles = [
|
|
64
|
+
'workflow.ts',
|
|
65
|
+
'steps.ts',
|
|
66
|
+
'evaluators.ts',
|
|
67
|
+
'types.ts',
|
|
68
|
+
'README.md',
|
|
69
|
+
'prompts/example@v1.prompt',
|
|
70
|
+
'scenarios/test_input.json'
|
|
71
|
+
];
|
|
72
|
+
for (const file of expectedFiles) {
|
|
73
|
+
expect(result.filesCreated).toContain(file);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function generatePlanName(description: string, date?: Date): Promise<string>;
|
|
2
|
+
/**
|
|
3
|
+
* Write plan content to PLAN.md file in the plans directory
|
|
4
|
+
* @param planName - Name of the plan (e.g., "2025_10_06_customer_order_processing")
|
|
5
|
+
* @param content - Plan content to write
|
|
6
|
+
* @param projectRoot - Root directory of the project
|
|
7
|
+
* @returns Full path to the created PLAN.md file
|
|
8
|
+
*/
|
|
9
|
+
export declare function writePlanFile(planName: string, content: string, projectRoot: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Update agent templates by reinitializing with force flag
|
|
12
|
+
* This recreates all agent configuration files, overwriting existing ones
|
|
13
|
+
* @param projectRoot - Root directory of the project
|
|
14
|
+
*/
|
|
15
|
+
export declare function updateAgentTemplates(projectRoot: string): Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { initializeAgentConfig } from './coding_agents.js';
|
|
2
|
+
import { generateText } from '@outputai/llm';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { config } from '#config.js';
|
|
6
|
+
export async function generatePlanName(description, date = new Date()) {
|
|
7
|
+
const year = date.getFullYear();
|
|
8
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
9
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
10
|
+
const datePrefix = `${year}_${month}_${day}`;
|
|
11
|
+
const { text: planNameSlug } = await generateText({
|
|
12
|
+
prompt: 'generate_plan_name@v1',
|
|
13
|
+
variables: { description }
|
|
14
|
+
});
|
|
15
|
+
const cleanedName = planNameSlug
|
|
16
|
+
.trim()
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
19
|
+
.replace(/_+/g, '_')
|
|
20
|
+
.replace(/^_|_$/g, '')
|
|
21
|
+
.slice(0, 50);
|
|
22
|
+
return `${datePrefix}_${cleanedName}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Write plan content to PLAN.md file in the plans directory
|
|
26
|
+
* @param planName - Name of the plan (e.g., "2025_10_06_customer_order_processing")
|
|
27
|
+
* @param content - Plan content to write
|
|
28
|
+
* @param projectRoot - Root directory of the project
|
|
29
|
+
* @returns Full path to the created PLAN.md file
|
|
30
|
+
*/
|
|
31
|
+
export async function writePlanFile(planName, content, projectRoot) {
|
|
32
|
+
const planDir = path.join(projectRoot, config.agentConfigDir, 'plans', planName);
|
|
33
|
+
const planFilePath = path.join(planDir, 'PLAN.md');
|
|
34
|
+
await fs.mkdir(planDir, { recursive: true });
|
|
35
|
+
await fs.writeFile(planFilePath, content, 'utf-8');
|
|
36
|
+
return planFilePath;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Update agent templates by reinitializing with force flag
|
|
40
|
+
* This recreates all agent configuration files, overwriting existing ones
|
|
41
|
+
* @param projectRoot - Root directory of the project
|
|
42
|
+
*/
|
|
43
|
+
export async function updateAgentTemplates(projectRoot) {
|
|
44
|
+
await initializeAgentConfig({
|
|
45
|
+
projectRoot,
|
|
46
|
+
force: true
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|