opencastle 0.26.1 → 0.27.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/README.md +7 -1
- package/bin/cli.mjs +4 -0
- package/dist/cli/destroy.d.ts +3 -0
- package/dist/cli/destroy.d.ts.map +1 -0
- package/dist/cli/destroy.js +69 -0
- package/dist/cli/destroy.js.map +1 -0
- package/dist/cli/destroy.test.d.ts +2 -0
- package/dist/cli/destroy.test.d.ts.map +1 -0
- package/dist/cli/destroy.test.js +116 -0
- package/dist/cli/destroy.test.js.map +1 -0
- package/dist/cli/gitignore.d.ts +9 -0
- package/dist/cli/gitignore.d.ts.map +1 -1
- package/dist/cli/gitignore.js +29 -0
- package/dist/cli/gitignore.js.map +1 -1
- package/dist/cli/plan.d.ts +3 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +288 -0
- package/dist/cli/plan.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/destroy.test.ts +141 -0
- package/src/cli/destroy.ts +88 -0
- package/src/cli/gitignore.ts +36 -0
- package/src/cli/plan.ts +316 -0
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
package/README.md
CHANGED
|
@@ -70,6 +70,8 @@ MCP servers are auto-configured for your stack in each IDE's native format.
|
|
|
70
70
|
| `opencastle init` | Set up agents in your project |
|
|
71
71
|
| `opencastle update` | Update framework files (keeps your customizations) |
|
|
72
72
|
| `opencastle eject` | Remove the dependency, keep all files |
|
|
73
|
+
| `opencastle destroy` | Remove ALL OpenCastle files (reverse of init) |
|
|
74
|
+
| `opencastle plan` | Generate a convoy spec from a task description |
|
|
73
75
|
| `opencastle run` | Run the Convoy Engine (deterministic, crash-recoverable orchestrator) |
|
|
74
76
|
| `opencastle dashboard` | Open the observability dashboard |
|
|
75
77
|
| `opencastle doctor` | Validate your setup and surface issues |
|
|
@@ -165,7 +167,11 @@ gates:
|
|
|
165
167
|
- **Observable** — real-time dashboard auto-starts during execution.
|
|
166
168
|
- **Multi-runtime** — mix Copilot, Claude Code, Cursor, and OpenCode in the same convoy.
|
|
167
169
|
|
|
168
|
-
|
|
170
|
+
Generate a convoy spec from a plain text description — no YAML by hand:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npx opencastle plan --file task.txt
|
|
174
|
+
```
|
|
169
175
|
|
|
170
176
|
📖 [Full Convoy Engine documentation →](https://www.opencastle.dev/docs/cli#run)
|
|
171
177
|
|
package/bin/cli.mjs
CHANGED
|
@@ -21,7 +21,9 @@ const HELP = `
|
|
|
21
21
|
init Set up OpenCastle in your project
|
|
22
22
|
update Update framework files (preserves customizations)
|
|
23
23
|
eject Remove dependency, keep all files standalone
|
|
24
|
+
destroy Remove ALL OpenCastle files (reverse of init)
|
|
24
25
|
run Process a task queue from a spec file autonomously
|
|
26
|
+
plan Generate a convoy spec from a task description file
|
|
25
27
|
dashboard View agent observability dashboard in your browser
|
|
26
28
|
doctor Validate your OpenCastle setup
|
|
27
29
|
log Append a structured event to the observability log
|
|
@@ -50,7 +52,9 @@ const commands = {
|
|
|
50
52
|
init: () => import('../dist/cli/init.js'),
|
|
51
53
|
update: () => import('../dist/cli/update.js'),
|
|
52
54
|
eject: () => import('../dist/cli/eject.js'),
|
|
55
|
+
destroy: () => import('../dist/cli/destroy.js'),
|
|
53
56
|
run: () => import('../dist/cli/run.js'),
|
|
57
|
+
plan: () => import('../dist/cli/plan.js'),
|
|
54
58
|
dashboard: () => import('../dist/cli/dashboard.js'),
|
|
55
59
|
doctor: () => import('../dist/cli/doctor.js'),
|
|
56
60
|
log: () => import('../dist/cli/log.js'),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destroy.d.ts","sourceRoot":"","sources":["../../src/cli/destroy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,wBAA8B,OAAO,CAAC,EACpC,OAAO,EAAE,QAAQ,EACjB,IAAI,GACL,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA2E5B"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { unlink } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { readManifest } from './manifest.js';
|
|
5
|
+
import { removeDirIfExists } from './copy.js';
|
|
6
|
+
import { removeGitignoreBlock } from './gitignore.js';
|
|
7
|
+
import { confirm, closePrompts, c } from './prompt.js';
|
|
8
|
+
export default async function destroy({ pkgRoot: _pkgRoot, args, }) {
|
|
9
|
+
const projectRoot = process.cwd();
|
|
10
|
+
const dryRun = args.includes('--dry-run') || args.includes('--dryRun');
|
|
11
|
+
const manifest = await readManifest(projectRoot);
|
|
12
|
+
if (!manifest) {
|
|
13
|
+
console.error(' ✗ No OpenCastle installation found.');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const frameworkPaths = manifest.managedPaths?.framework ?? [];
|
|
17
|
+
const customizablePaths = manifest.managedPaths?.customizable ?? [];
|
|
18
|
+
const legacyManifestPath = resolve(projectRoot, '.opencastle.json');
|
|
19
|
+
const hasLegacy = existsSync(legacyManifestPath);
|
|
20
|
+
console.log(`\n 🏰 OpenCastle destroy\n`);
|
|
21
|
+
console.log(' This will permanently remove:\n');
|
|
22
|
+
for (const p of frameworkPaths) {
|
|
23
|
+
console.log(` ${c.dim(p)}`);
|
|
24
|
+
}
|
|
25
|
+
for (const p of customizablePaths) {
|
|
26
|
+
console.log(` ${c.dim(p)}`);
|
|
27
|
+
}
|
|
28
|
+
console.log(` ${c.dim('.opencastle/')}`);
|
|
29
|
+
if (hasLegacy) {
|
|
30
|
+
console.log(` ${c.dim('.opencastle.json')}`);
|
|
31
|
+
}
|
|
32
|
+
console.log(` ${c.dim('.gitignore block')}\n`);
|
|
33
|
+
if (dryRun) {
|
|
34
|
+
console.log(' [dry-run] No files were changed.\n');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const proceed = await confirm('This will permanently delete all OpenCastle files. Continue?', false);
|
|
38
|
+
if (!proceed) {
|
|
39
|
+
console.log(' Aborted.');
|
|
40
|
+
closePrompts();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let removed = 0;
|
|
44
|
+
for (const p of [...frameworkPaths, ...customizablePaths]) {
|
|
45
|
+
if (p.endsWith('/')) {
|
|
46
|
+
const dir = resolve(projectRoot, p);
|
|
47
|
+
await removeDirIfExists(dir);
|
|
48
|
+
removed++;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const file = resolve(projectRoot, p);
|
|
52
|
+
if (existsSync(file)) {
|
|
53
|
+
await unlink(file);
|
|
54
|
+
removed++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
await removeDirIfExists(resolve(projectRoot, '.opencastle'));
|
|
59
|
+
removed++;
|
|
60
|
+
if (hasLegacy) {
|
|
61
|
+
await unlink(legacyManifestPath);
|
|
62
|
+
removed++;
|
|
63
|
+
}
|
|
64
|
+
const gitignoreResult = await removeGitignoreBlock(projectRoot);
|
|
65
|
+
console.log(`\n ${c.green('✓')} Removed ${removed} path(s)${gitignoreResult === 'removed' ? ' + .gitignore block' : ''}.`);
|
|
66
|
+
console.log(` You can uninstall: ${c.bold('npm uninstall opencastle')}\n`);
|
|
67
|
+
closePrompts();
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=destroy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destroy.js","sourceRoot":"","sources":["../../src/cli/destroy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destroy.test.d.ts","sourceRoot":"","sources":["../../src/cli/destroy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
vi.mock('./prompt.js', () => ({
|
|
7
|
+
confirm: vi.fn().mockResolvedValue(true),
|
|
8
|
+
closePrompts: vi.fn(),
|
|
9
|
+
c: {
|
|
10
|
+
green: (s) => s,
|
|
11
|
+
dim: (s) => s,
|
|
12
|
+
bold: (s) => s,
|
|
13
|
+
red: (s) => s,
|
|
14
|
+
cyan: (s) => s,
|
|
15
|
+
yellow: (s) => s,
|
|
16
|
+
magenta: (s) => s,
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
import destroy from './destroy.js';
|
|
20
|
+
import { confirm } from './prompt.js';
|
|
21
|
+
const START_MARKER = '# >>> OpenCastle managed (do not edit) >>>';
|
|
22
|
+
const END_MARKER = '# <<< OpenCastle managed <<<';
|
|
23
|
+
async function writeManifestFile(dir, manifest = {}) {
|
|
24
|
+
await mkdir(join(dir, '.opencastle'), { recursive: true });
|
|
25
|
+
const full = {
|
|
26
|
+
version: '1.0.0',
|
|
27
|
+
ide: 'vscode',
|
|
28
|
+
ides: ['vscode'],
|
|
29
|
+
installedAt: new Date().toISOString(),
|
|
30
|
+
updatedAt: new Date().toISOString(),
|
|
31
|
+
managedPaths: { framework: [], customizable: [] },
|
|
32
|
+
...manifest,
|
|
33
|
+
};
|
|
34
|
+
await writeFile(join(dir, '.opencastle', 'manifest.json'), JSON.stringify(full, null, 2));
|
|
35
|
+
}
|
|
36
|
+
async function writeGitignoreWithBlock(dir, userEntries = 'node_modules\n') {
|
|
37
|
+
const block = [userEntries, '', START_MARKER, '.github/', '!.github/customizations/', END_MARKER, ''].join('\n');
|
|
38
|
+
await writeFile(join(dir, '.gitignore'), block);
|
|
39
|
+
}
|
|
40
|
+
// ── Tests ──────────────────────────────────────────────────────
|
|
41
|
+
describe('destroy', () => {
|
|
42
|
+
let tmpDir;
|
|
43
|
+
let cwdSpy;
|
|
44
|
+
let exitSpy;
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'oc-destroy-'));
|
|
47
|
+
cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tmpDir);
|
|
48
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
49
|
+
throw new Error('process.exit called');
|
|
50
|
+
});
|
|
51
|
+
vi.mocked(confirm).mockResolvedValue(true);
|
|
52
|
+
});
|
|
53
|
+
afterEach(async () => {
|
|
54
|
+
cwdSpy.mockRestore();
|
|
55
|
+
exitSpy.mockRestore();
|
|
56
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
it('removes all managed framework files', async () => {
|
|
59
|
+
await writeManifestFile(tmpDir, {
|
|
60
|
+
managedPaths: {
|
|
61
|
+
framework: ['.github/instructions/general.instructions.md', '.github/copilot-instructions.md'],
|
|
62
|
+
customizable: [],
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
await mkdir(join(tmpDir, '.github', 'instructions'), { recursive: true });
|
|
66
|
+
await writeFile(join(tmpDir, '.github', 'instructions', 'general.instructions.md'), 'content');
|
|
67
|
+
await writeFile(join(tmpDir, '.github', 'copilot-instructions.md'), 'content');
|
|
68
|
+
await destroy({ pkgRoot: tmpDir, args: [] });
|
|
69
|
+
expect(existsSync(join(tmpDir, '.github', 'instructions', 'general.instructions.md'))).toBe(false);
|
|
70
|
+
expect(existsSync(join(tmpDir, '.github', 'copilot-instructions.md'))).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it('removes .opencastle/ directory', async () => {
|
|
73
|
+
await writeManifestFile(tmpDir);
|
|
74
|
+
expect(existsSync(join(tmpDir, '.opencastle'))).toBe(true);
|
|
75
|
+
await destroy({ pkgRoot: tmpDir, args: [] });
|
|
76
|
+
expect(existsSync(join(tmpDir, '.opencastle'))).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it('removes legacy .opencastle.json manifest', async () => {
|
|
79
|
+
await writeManifestFile(tmpDir);
|
|
80
|
+
const legacyPath = join(tmpDir, '.opencastle.json');
|
|
81
|
+
await writeFile(legacyPath, JSON.stringify({ version: '0.1.0', ide: 'vscode', installedAt: '', updatedAt: '' }));
|
|
82
|
+
await destroy({ pkgRoot: tmpDir, args: [] });
|
|
83
|
+
expect(existsSync(legacyPath)).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
it('cleans the gitignore block but keeps user entries', async () => {
|
|
86
|
+
await writeManifestFile(tmpDir);
|
|
87
|
+
await writeGitignoreWithBlock(tmpDir, 'node_modules\ndist\n');
|
|
88
|
+
await destroy({ pkgRoot: tmpDir, args: [] });
|
|
89
|
+
const gitignorePath = join(tmpDir, '.gitignore');
|
|
90
|
+
expect(existsSync(gitignorePath)).toBe(true);
|
|
91
|
+
const { readFile } = await import('node:fs/promises');
|
|
92
|
+
const content = await readFile(gitignorePath, 'utf8');
|
|
93
|
+
expect(content).not.toContain(START_MARKER);
|
|
94
|
+
expect(content).not.toContain(END_MARKER);
|
|
95
|
+
expect(content).toContain('node_modules');
|
|
96
|
+
expect(content).toContain('dist');
|
|
97
|
+
});
|
|
98
|
+
it('dry-run makes no changes', async () => {
|
|
99
|
+
await writeManifestFile(tmpDir, {
|
|
100
|
+
managedPaths: { framework: ['some-file.md'], customizable: [] },
|
|
101
|
+
});
|
|
102
|
+
await writeFile(join(tmpDir, 'some-file.md'), 'content');
|
|
103
|
+
await writeGitignoreWithBlock(tmpDir);
|
|
104
|
+
await destroy({ pkgRoot: tmpDir, args: ['--dry-run'] });
|
|
105
|
+
expect(existsSync(join(tmpDir, 'some-file.md'))).toBe(true);
|
|
106
|
+
expect(existsSync(join(tmpDir, '.opencastle'))).toBe(true);
|
|
107
|
+
const { readFile } = await import('node:fs/promises');
|
|
108
|
+
const gitignore = await readFile(join(tmpDir, '.gitignore'), 'utf8');
|
|
109
|
+
expect(gitignore).toContain(START_MARKER);
|
|
110
|
+
});
|
|
111
|
+
it('exits with error when no manifest found', async () => {
|
|
112
|
+
await expect(destroy({ pkgRoot: tmpDir, args: [] })).rejects.toThrow('process.exit called');
|
|
113
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=destroy.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destroy.test.js","sourceRoot":"","sources":["../../src/cli/destroy.test.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/gitignore.d.ts
CHANGED
|
@@ -8,4 +8,13 @@ import type { ManagedPaths } from './types.js';
|
|
|
8
8
|
* (handles re-init or IDE switch cleanly).
|
|
9
9
|
*/
|
|
10
10
|
export declare function updateGitignore(projectRoot: string, managed: ManagedPaths): Promise<'created' | 'updated' | 'unchanged'>;
|
|
11
|
+
/**
|
|
12
|
+
* Remove the OpenCastle managed block from `.gitignore`.
|
|
13
|
+
*
|
|
14
|
+
* - No-op if no `.gitignore` exists or no block is present.
|
|
15
|
+
* - Cleans up resulting double blank lines.
|
|
16
|
+
* - Deletes `.gitignore` if the file becomes empty after removal.
|
|
17
|
+
* - Returns 'removed' or 'unchanged'.
|
|
18
|
+
*/
|
|
19
|
+
export declare function removeGitignoreBlock(projectRoot: string): Promise<'removed' | 'unchanged'>;
|
|
11
20
|
//# sourceMappingURL=gitignore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitignore.d.ts","sourceRoot":"","sources":["../../src/cli/gitignore.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AA6B9C;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC,CA8B9C"}
|
|
1
|
+
{"version":3,"file":"gitignore.d.ts","sourceRoot":"","sources":["../../src/cli/gitignore.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AA6B9C;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC,CA8B9C;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,SAAS,GAAG,WAAW,CAAC,CAwBlC"}
|
package/dist/cli/gitignore.js
CHANGED
|
@@ -56,4 +56,33 @@ export async function updateGitignore(projectRoot, managed) {
|
|
|
56
56
|
await writeFile(gitignorePath, existing + separator + block + '\n', 'utf8');
|
|
57
57
|
return 'updated';
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Remove the OpenCastle managed block from `.gitignore`.
|
|
61
|
+
*
|
|
62
|
+
* - No-op if no `.gitignore` exists or no block is present.
|
|
63
|
+
* - Cleans up resulting double blank lines.
|
|
64
|
+
* - Deletes `.gitignore` if the file becomes empty after removal.
|
|
65
|
+
* - Returns 'removed' or 'unchanged'.
|
|
66
|
+
*/
|
|
67
|
+
export async function removeGitignoreBlock(projectRoot) {
|
|
68
|
+
const gitignorePath = resolve(projectRoot, '.gitignore');
|
|
69
|
+
if (!existsSync(gitignorePath))
|
|
70
|
+
return 'unchanged';
|
|
71
|
+
const existing = await readFile(gitignorePath, 'utf8');
|
|
72
|
+
const startIdx = existing.indexOf(START_MARKER);
|
|
73
|
+
const endIdx = existing.indexOf(END_MARKER);
|
|
74
|
+
if (startIdx === -1 || endIdx === -1)
|
|
75
|
+
return 'unchanged';
|
|
76
|
+
const before = existing.slice(0, startIdx);
|
|
77
|
+
const after = existing.slice(endIdx + END_MARKER.length);
|
|
78
|
+
// Collapse consecutive blank lines left by removal
|
|
79
|
+
const updated = (before + after).replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
80
|
+
if (!updated) {
|
|
81
|
+
const { unlink } = await import('node:fs/promises');
|
|
82
|
+
await unlink(gitignorePath);
|
|
83
|
+
return 'removed';
|
|
84
|
+
}
|
|
85
|
+
await writeFile(gitignorePath, updated + '\n', 'utf8');
|
|
86
|
+
return 'removed';
|
|
87
|
+
}
|
|
59
88
|
//# sourceMappingURL=gitignore.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitignore.js","sourceRoot":"","sources":["../../src/cli/gitignore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGpC,MAAM,YAAY,GAAG,4CAA4C,CAAA;AACjE,MAAM,UAAU,GAAG,8BAA8B,CAAA;AAEjD;;;;;;GAMG;AACH,SAAS,UAAU,CAAC,OAAqB;IACvC,MAAM,KAAK,GAAa,CAAC,YAAY,CAAC,CAAA;IAEtC,+DAA+D;IAC/D,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACf,CAAC;IAED,oDAAoD;IACpD,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACrB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACtB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,WAAmB,EACnB,OAAqB;IAErB,MAAM,aAAa,GAAG,OAAO,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;IACxD,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;IAEjC,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/B,MAAM,SAAS,CAAC,aAAa,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,CAAC,CAAA;QACpD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,CAAA;IAEtD,yBAAyB;IACzB,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IAE3C,IAAI,QAAQ,KAAK,CAAC,CAAC,IAAI,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAA;QAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;QACxD,MAAM,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAA;QAEtC,IAAI,OAAO,KAAK,QAAQ;YAAE,OAAO,WAAW,CAAA;QAE5C,MAAM,SAAS,CAAC,aAAa,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;QAC/C,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,gCAAgC;IAChC,MAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAA;IACzD,MAAM,SAAS,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,GAAG,IAAI,EAAE,MAAM,CAAC,CAAA;IAC3E,OAAO,SAAS,CAAA;AAClB,CAAC"}
|
|
1
|
+
{"version":3,"file":"gitignore.js","sourceRoot":"","sources":["../../src/cli/gitignore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/cli/plan.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAQ,MAAM,YAAY,CAAA;AAmKlD,wBAA8B,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAiJ/E"}
|
package/dist/cli/plan.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve, join, basename } from 'node:path';
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { getAdapter, detectAdapter } from './run/adapters/index.js';
|
|
6
|
+
import { parseTaskSpecText } from './run/schema.js';
|
|
7
|
+
import { c } from './prompt.js';
|
|
8
|
+
const HELP = `
|
|
9
|
+
opencastle plan [options]
|
|
10
|
+
|
|
11
|
+
Generate a convoy spec from a task description file by running it through the
|
|
12
|
+
generate-convoy prompt via an AI adapter.
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--file, -f <path> Path to a text file with the task description (required)
|
|
16
|
+
--context <path> Optional path to an additional context file
|
|
17
|
+
--output, -o <path> Output path for the generated convoy spec
|
|
18
|
+
--adapter, -a <name> Override agent runtime adapter
|
|
19
|
+
--verbose Show full agent output
|
|
20
|
+
--dry-run Print the prompt that would be sent without executing
|
|
21
|
+
--help, -h Show this help
|
|
22
|
+
`;
|
|
23
|
+
function parseArgs(args) {
|
|
24
|
+
const opts = {
|
|
25
|
+
file: null,
|
|
26
|
+
context: null,
|
|
27
|
+
output: null,
|
|
28
|
+
adapter: null,
|
|
29
|
+
verbose: false,
|
|
30
|
+
dryRun: false,
|
|
31
|
+
help: false,
|
|
32
|
+
};
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
switch (arg) {
|
|
36
|
+
case '--help':
|
|
37
|
+
case '-h':
|
|
38
|
+
opts.help = true;
|
|
39
|
+
break;
|
|
40
|
+
case '--file':
|
|
41
|
+
case '-f':
|
|
42
|
+
if (i + 1 >= args.length) {
|
|
43
|
+
console.error(' ✗ --file requires a path');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
opts.file = args[++i];
|
|
47
|
+
break;
|
|
48
|
+
case '--context':
|
|
49
|
+
if (i + 1 >= args.length) {
|
|
50
|
+
console.error(' ✗ --context requires a path');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
opts.context = args[++i];
|
|
54
|
+
break;
|
|
55
|
+
case '--output':
|
|
56
|
+
case '-o':
|
|
57
|
+
if (i + 1 >= args.length) {
|
|
58
|
+
console.error(' ✗ --output requires a path');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
opts.output = args[++i];
|
|
62
|
+
break;
|
|
63
|
+
case '--adapter':
|
|
64
|
+
case '-a':
|
|
65
|
+
if (i + 1 >= args.length) {
|
|
66
|
+
console.error(' ✗ --adapter requires a name');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
opts.adapter = args[++i];
|
|
70
|
+
break;
|
|
71
|
+
case '--verbose':
|
|
72
|
+
opts.verbose = true;
|
|
73
|
+
break;
|
|
74
|
+
case '--dry-run':
|
|
75
|
+
case '--dryRun':
|
|
76
|
+
opts.dryRun = true;
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
console.error(` ✗ Unknown option: ${arg}`);
|
|
80
|
+
console.log(HELP);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return opts;
|
|
85
|
+
}
|
|
86
|
+
function printAdapterError(detectionFailed, adapterName) {
|
|
87
|
+
if (detectionFailed) {
|
|
88
|
+
console.error(` ✗ No agent CLI found on your PATH.\n` +
|
|
89
|
+
` Install one of the following adapters:\n` +
|
|
90
|
+
` • copilot — https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli\n` +
|
|
91
|
+
` • claude — npm install -g @anthropic-ai/claude-code\n` +
|
|
92
|
+
` • cursor — https://cursor.com (Cursor > Install CLI)\n` +
|
|
93
|
+
` • opencode — https://opencode.ai\n` +
|
|
94
|
+
`\n` +
|
|
95
|
+
` Or specify an adapter explicitly: opencastle plan --adapter <name>`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const hints = {
|
|
99
|
+
'claude': ' Install: npm install -g @anthropic-ai/claude-code\n' +
|
|
100
|
+
' Docs: https://docs.anthropic.com/en/docs/claude-code',
|
|
101
|
+
copilot: ' Requires the Copilot CLI installed and authenticated:\n' +
|
|
102
|
+
' https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli\n' +
|
|
103
|
+
' Docs: https://docs.github.com/en/copilot',
|
|
104
|
+
cursor: ' The Cursor agent CLI ships with the Cursor editor.\n' +
|
|
105
|
+
' Install Cursor from https://cursor.com and ensure the\n' +
|
|
106
|
+
' "agent" command is on your PATH (Cursor > Install CLI).',
|
|
107
|
+
opencode: ' Install OpenCode from https://opencode.ai\n' +
|
|
108
|
+
' Ensure the "opencode" command is on your PATH.',
|
|
109
|
+
};
|
|
110
|
+
const cliName = adapterName === 'cursor' ? 'agent' : adapterName;
|
|
111
|
+
const hint = hints[adapterName] ?? '';
|
|
112
|
+
console.error(` ✗ Adapter "${adapterName}" is not available.\n` +
|
|
113
|
+
` Make sure the "${cliName}" CLI is installed and on your PATH.\n` +
|
|
114
|
+
hint);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Strip YAML frontmatter (everything between first and second --- lines).
|
|
119
|
+
*/
|
|
120
|
+
function stripFrontmatter(text) {
|
|
121
|
+
const lines = text.split('\n');
|
|
122
|
+
if (lines[0]?.trim() !== '---')
|
|
123
|
+
return text;
|
|
124
|
+
const closingIdx = lines.findIndex((line, i) => i > 0 && line.trim() === '---');
|
|
125
|
+
if (closingIdx === -1)
|
|
126
|
+
return text;
|
|
127
|
+
return lines.slice(closingIdx + 1).join('\n').trimStart();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Extract YAML content from a fenced code block (```yaml or ```yml).
|
|
131
|
+
*/
|
|
132
|
+
function extractYamlBlock(text) {
|
|
133
|
+
const match = text.match(/```ya?ml\s*\n([\s\S]*?)```/);
|
|
134
|
+
if (!match)
|
|
135
|
+
return null;
|
|
136
|
+
return match[1].trim();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Derive an output filename from YAML content.
|
|
140
|
+
* Checks for a comment on the first line, then falls back to the `name` field.
|
|
141
|
+
*/
|
|
142
|
+
function deriveOutputFilename(yaml) {
|
|
143
|
+
// First line comment: # .opencastle/convoys/some-name.convoy.yml
|
|
144
|
+
const firstLine = yaml.split('\n')[0] ?? '';
|
|
145
|
+
const commentMatch = firstLine.match(/^#\s*(.+\.convoy\.ya?ml)\s*$/);
|
|
146
|
+
if (commentMatch) {
|
|
147
|
+
return basename(commentMatch[1]);
|
|
148
|
+
}
|
|
149
|
+
// Fall back to `name:` field
|
|
150
|
+
const nameMatch = yaml.match(/^name:\s*['"]?([^'"\n]+)['"]?\s*$/m);
|
|
151
|
+
if (nameMatch) {
|
|
152
|
+
const kebab = nameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
153
|
+
if (kebab)
|
|
154
|
+
return `${kebab}.convoy.yml`;
|
|
155
|
+
}
|
|
156
|
+
return 'convoy-plan.convoy.yml';
|
|
157
|
+
}
|
|
158
|
+
export default async function plan({ args, pkgRoot }) {
|
|
159
|
+
const opts = parseArgs(args);
|
|
160
|
+
if (opts.help) {
|
|
161
|
+
console.log(HELP);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// ── Validate required --file arg ──────────────────────────────
|
|
165
|
+
if (!opts.file) {
|
|
166
|
+
console.error(` ✗ --file is required. Specify a text file with the task description.`);
|
|
167
|
+
console.log(HELP);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
const filePath = resolve(process.cwd(), opts.file);
|
|
171
|
+
if (!existsSync(filePath)) {
|
|
172
|
+
console.error(` ✗ File not found: ${opts.file}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
// ── Read task description ──────────────────────────────────────
|
|
176
|
+
const taskDescription = await readFile(filePath, 'utf8');
|
|
177
|
+
// ── Read optional context file ─────────────────────────────────
|
|
178
|
+
let contextContent = '';
|
|
179
|
+
if (opts.context) {
|
|
180
|
+
const contextPath = resolve(process.cwd(), opts.context);
|
|
181
|
+
if (!existsSync(contextPath)) {
|
|
182
|
+
console.error(` ✗ Context file not found: ${opts.context}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
contextContent = await readFile(contextPath, 'utf8');
|
|
186
|
+
}
|
|
187
|
+
// ── Load and assemble the prompt template ─────────────────────
|
|
188
|
+
const promptTemplatePath = join(pkgRoot, 'src', 'orchestrator', 'prompts', 'generate-convoy.prompt.md');
|
|
189
|
+
if (!existsSync(promptTemplatePath)) {
|
|
190
|
+
console.error(` ✗ Prompt template not found: ${promptTemplatePath}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
const rawTemplate = await readFile(promptTemplatePath, 'utf8');
|
|
194
|
+
const template = stripFrontmatter(rawTemplate);
|
|
195
|
+
const assembledPrompt = template
|
|
196
|
+
.replace('{{goal}}', taskDescription.trim())
|
|
197
|
+
.replace('{{context}}', contextContent.trim());
|
|
198
|
+
// ── Dry-run: print prompt and exit ────────────────────────────
|
|
199
|
+
if (opts.dryRun) {
|
|
200
|
+
console.log(c.bold(c.cyan(' Assembled prompt (dry-run):\n')));
|
|
201
|
+
console.log(assembledPrompt);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// ── Resolve adapter ───────────────────────────────────────────
|
|
205
|
+
let adapterName;
|
|
206
|
+
if (opts.adapter) {
|
|
207
|
+
adapterName = opts.adapter;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const detected = await detectAdapter();
|
|
211
|
+
if (!detected) {
|
|
212
|
+
printAdapterError(true, '');
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
adapterName = detected;
|
|
216
|
+
}
|
|
217
|
+
let adapter;
|
|
218
|
+
try {
|
|
219
|
+
adapter = await getAdapter(adapterName);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
printAdapterError(false, adapterName);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const available = await adapter.isAvailable();
|
|
226
|
+
if (!available) {
|
|
227
|
+
printAdapterError(false, adapterName);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
console.log(c.dim(` Using adapter: ${adapterName}`));
|
|
231
|
+
console.log(c.dim(` Generating convoy spec from: ${opts.file}\n`));
|
|
232
|
+
// ── Execute the prompt through the adapter ────────────────────
|
|
233
|
+
const task = {
|
|
234
|
+
id: 'generate-convoy',
|
|
235
|
+
prompt: assembledPrompt,
|
|
236
|
+
agent: 'team-lead',
|
|
237
|
+
timeout: '10m',
|
|
238
|
+
depends_on: [],
|
|
239
|
+
files: [],
|
|
240
|
+
description: 'Generate convoy spec from task description',
|
|
241
|
+
max_retries: 1,
|
|
242
|
+
};
|
|
243
|
+
const result = await adapter.execute(task, { verbose: opts.verbose });
|
|
244
|
+
// ── Extract YAML from the response ────────────────────────────
|
|
245
|
+
const yamlContent = extractYamlBlock(result.output);
|
|
246
|
+
if (!yamlContent) {
|
|
247
|
+
const preview = result.output.slice(0, 500);
|
|
248
|
+
console.error(` ✗ No YAML code block found in the agent response.\n`);
|
|
249
|
+
console.error(c.dim(` Raw output (truncated):\n${preview}`));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
// ── Validate YAML ─────────────────────────────────────────────
|
|
253
|
+
let validationWarning = false;
|
|
254
|
+
try {
|
|
255
|
+
parseTaskSpecText(yamlContent);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
validationWarning = true;
|
|
259
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
260
|
+
console.warn(c.yellow(` ⚠ YAML validation warning: ${msg}`));
|
|
261
|
+
console.warn(c.dim(` The file will still be written — you may need to edit it before running.\n`));
|
|
262
|
+
}
|
|
263
|
+
// ── Determine output path ─────────────────────────────────────
|
|
264
|
+
let outputPath;
|
|
265
|
+
if (opts.output) {
|
|
266
|
+
outputPath = resolve(process.cwd(), opts.output);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys');
|
|
270
|
+
await mkdir(convoyDir, { recursive: true });
|
|
271
|
+
const filename = deriveOutputFilename(yamlContent);
|
|
272
|
+
outputPath = join(convoyDir, filename);
|
|
273
|
+
}
|
|
274
|
+
await mkdir(resolve(outputPath, '..'), { recursive: true });
|
|
275
|
+
await writeFile(outputPath, yamlContent + '\n', 'utf8');
|
|
276
|
+
const relPath = outputPath.startsWith(process.cwd())
|
|
277
|
+
? outputPath.slice(process.cwd().length + 1)
|
|
278
|
+
: outputPath;
|
|
279
|
+
console.log(c.green(` ✓ Convoy spec written to ${relPath}`));
|
|
280
|
+
if (validationWarning) {
|
|
281
|
+
console.log(c.yellow(` (contains validation warnings — review before running)`));
|
|
282
|
+
}
|
|
283
|
+
console.log(`
|
|
284
|
+
${c.dim('Preview:')} npx opencastle run -f ${relPath} --dry-run
|
|
285
|
+
${c.dim('Execute:')} npx opencastle run -f ${relPath}
|
|
286
|
+
`);
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/cli/plan.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { existsSync } from 'node:fs'
|
|
6
|
+
|
|
7
|
+
vi.mock('./prompt.js', () => ({
|
|
8
|
+
confirm: vi.fn().mockResolvedValue(true),
|
|
9
|
+
closePrompts: vi.fn(),
|
|
10
|
+
c: {
|
|
11
|
+
green: (s: string) => s,
|
|
12
|
+
dim: (s: string) => s,
|
|
13
|
+
bold: (s: string) => s,
|
|
14
|
+
red: (s: string) => s,
|
|
15
|
+
cyan: (s: string) => s,
|
|
16
|
+
yellow: (s: string) => s,
|
|
17
|
+
magenta: (s: string) => s,
|
|
18
|
+
},
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
import destroy from './destroy.js'
|
|
22
|
+
import { confirm } from './prompt.js'
|
|
23
|
+
import type { Manifest } from './types.js'
|
|
24
|
+
|
|
25
|
+
const START_MARKER = '# >>> OpenCastle managed (do not edit) >>>'
|
|
26
|
+
const END_MARKER = '# <<< OpenCastle managed <<<'
|
|
27
|
+
|
|
28
|
+
async function writeManifestFile(dir: string, manifest: Partial<Manifest> = {}): Promise<void> {
|
|
29
|
+
await mkdir(join(dir, '.opencastle'), { recursive: true })
|
|
30
|
+
const full: Manifest = {
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
ide: 'vscode',
|
|
33
|
+
ides: ['vscode'],
|
|
34
|
+
installedAt: new Date().toISOString(),
|
|
35
|
+
updatedAt: new Date().toISOString(),
|
|
36
|
+
managedPaths: { framework: [], customizable: [] },
|
|
37
|
+
...manifest,
|
|
38
|
+
}
|
|
39
|
+
await writeFile(join(dir, '.opencastle', 'manifest.json'), JSON.stringify(full, null, 2))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function writeGitignoreWithBlock(dir: string, userEntries = 'node_modules\n'): Promise<void> {
|
|
43
|
+
const block = [userEntries, '', START_MARKER, '.github/', '!.github/customizations/', END_MARKER, ''].join('\n')
|
|
44
|
+
await writeFile(join(dir, '.gitignore'), block)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Tests ──────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
describe('destroy', () => {
|
|
50
|
+
let tmpDir: string
|
|
51
|
+
let cwdSpy: ReturnType<typeof vi.spyOn>
|
|
52
|
+
let exitSpy: ReturnType<typeof vi.spyOn>
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'oc-destroy-'))
|
|
56
|
+
cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tmpDir)
|
|
57
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
58
|
+
throw new Error('process.exit called')
|
|
59
|
+
})
|
|
60
|
+
vi.mocked(confirm).mockResolvedValue(true)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
afterEach(async () => {
|
|
64
|
+
cwdSpy.mockRestore()
|
|
65
|
+
exitSpy.mockRestore()
|
|
66
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('removes all managed framework files', async () => {
|
|
70
|
+
await writeManifestFile(tmpDir, {
|
|
71
|
+
managedPaths: {
|
|
72
|
+
framework: ['.github/instructions/general.instructions.md', '.github/copilot-instructions.md'],
|
|
73
|
+
customizable: [],
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
await mkdir(join(tmpDir, '.github', 'instructions'), { recursive: true })
|
|
77
|
+
await writeFile(join(tmpDir, '.github', 'instructions', 'general.instructions.md'), 'content')
|
|
78
|
+
await writeFile(join(tmpDir, '.github', 'copilot-instructions.md'), 'content')
|
|
79
|
+
|
|
80
|
+
await destroy({ pkgRoot: tmpDir, args: [] })
|
|
81
|
+
|
|
82
|
+
expect(existsSync(join(tmpDir, '.github', 'instructions', 'general.instructions.md'))).toBe(false)
|
|
83
|
+
expect(existsSync(join(tmpDir, '.github', 'copilot-instructions.md'))).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('removes .opencastle/ directory', async () => {
|
|
87
|
+
await writeManifestFile(tmpDir)
|
|
88
|
+
expect(existsSync(join(tmpDir, '.opencastle'))).toBe(true)
|
|
89
|
+
|
|
90
|
+
await destroy({ pkgRoot: tmpDir, args: [] })
|
|
91
|
+
|
|
92
|
+
expect(existsSync(join(tmpDir, '.opencastle'))).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('removes legacy .opencastle.json manifest', async () => {
|
|
96
|
+
await writeManifestFile(tmpDir)
|
|
97
|
+
const legacyPath = join(tmpDir, '.opencastle.json')
|
|
98
|
+
await writeFile(legacyPath, JSON.stringify({ version: '0.1.0', ide: 'vscode', installedAt: '', updatedAt: '' }))
|
|
99
|
+
|
|
100
|
+
await destroy({ pkgRoot: tmpDir, args: [] })
|
|
101
|
+
|
|
102
|
+
expect(existsSync(legacyPath)).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('cleans the gitignore block but keeps user entries', async () => {
|
|
106
|
+
await writeManifestFile(tmpDir)
|
|
107
|
+
await writeGitignoreWithBlock(tmpDir, 'node_modules\ndist\n')
|
|
108
|
+
|
|
109
|
+
await destroy({ pkgRoot: tmpDir, args: [] })
|
|
110
|
+
|
|
111
|
+
const gitignorePath = join(tmpDir, '.gitignore')
|
|
112
|
+
expect(existsSync(gitignorePath)).toBe(true)
|
|
113
|
+
const { readFile } = await import('node:fs/promises')
|
|
114
|
+
const content = await readFile(gitignorePath, 'utf8')
|
|
115
|
+
expect(content).not.toContain(START_MARKER)
|
|
116
|
+
expect(content).not.toContain(END_MARKER)
|
|
117
|
+
expect(content).toContain('node_modules')
|
|
118
|
+
expect(content).toContain('dist')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('dry-run makes no changes', async () => {
|
|
122
|
+
await writeManifestFile(tmpDir, {
|
|
123
|
+
managedPaths: { framework: ['some-file.md'], customizable: [] },
|
|
124
|
+
})
|
|
125
|
+
await writeFile(join(tmpDir, 'some-file.md'), 'content')
|
|
126
|
+
await writeGitignoreWithBlock(tmpDir)
|
|
127
|
+
|
|
128
|
+
await destroy({ pkgRoot: tmpDir, args: ['--dry-run'] })
|
|
129
|
+
|
|
130
|
+
expect(existsSync(join(tmpDir, 'some-file.md'))).toBe(true)
|
|
131
|
+
expect(existsSync(join(tmpDir, '.opencastle'))).toBe(true)
|
|
132
|
+
const { readFile } = await import('node:fs/promises')
|
|
133
|
+
const gitignore = await readFile(join(tmpDir, '.gitignore'), 'utf8')
|
|
134
|
+
expect(gitignore).toContain(START_MARKER)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('exits with error when no manifest found', async () => {
|
|
138
|
+
await expect(destroy({ pkgRoot: tmpDir, args: [] })).rejects.toThrow('process.exit called')
|
|
139
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { unlink } from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { readManifest } from './manifest.js'
|
|
5
|
+
import { removeDirIfExists } from './copy.js'
|
|
6
|
+
import { removeGitignoreBlock } from './gitignore.js'
|
|
7
|
+
import { confirm, closePrompts, c } from './prompt.js'
|
|
8
|
+
import type { CliContext } from './types.js'
|
|
9
|
+
|
|
10
|
+
export default async function destroy({
|
|
11
|
+
pkgRoot: _pkgRoot,
|
|
12
|
+
args,
|
|
13
|
+
}: CliContext): Promise<void> {
|
|
14
|
+
const projectRoot = process.cwd()
|
|
15
|
+
const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
|
|
16
|
+
|
|
17
|
+
const manifest = await readManifest(projectRoot)
|
|
18
|
+
if (!manifest) {
|
|
19
|
+
console.error(' ✗ No OpenCastle installation found.')
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const frameworkPaths = manifest.managedPaths?.framework ?? []
|
|
24
|
+
const customizablePaths = manifest.managedPaths?.customizable ?? []
|
|
25
|
+
const legacyManifestPath = resolve(projectRoot, '.opencastle.json')
|
|
26
|
+
const hasLegacy = existsSync(legacyManifestPath)
|
|
27
|
+
|
|
28
|
+
console.log(`\n 🏰 OpenCastle destroy\n`)
|
|
29
|
+
console.log(' This will permanently remove:\n')
|
|
30
|
+
|
|
31
|
+
for (const p of frameworkPaths) {
|
|
32
|
+
console.log(` ${c.dim(p)}`)
|
|
33
|
+
}
|
|
34
|
+
for (const p of customizablePaths) {
|
|
35
|
+
console.log(` ${c.dim(p)}`)
|
|
36
|
+
}
|
|
37
|
+
console.log(` ${c.dim('.opencastle/')}`)
|
|
38
|
+
if (hasLegacy) {
|
|
39
|
+
console.log(` ${c.dim('.opencastle.json')}`)
|
|
40
|
+
}
|
|
41
|
+
console.log(` ${c.dim('.gitignore block')}\n`)
|
|
42
|
+
|
|
43
|
+
if (dryRun) {
|
|
44
|
+
console.log(' [dry-run] No files were changed.\n')
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const proceed = await confirm(
|
|
49
|
+
'This will permanently delete all OpenCastle files. Continue?',
|
|
50
|
+
false
|
|
51
|
+
)
|
|
52
|
+
if (!proceed) {
|
|
53
|
+
console.log(' Aborted.')
|
|
54
|
+
closePrompts()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let removed = 0
|
|
59
|
+
|
|
60
|
+
for (const p of [...frameworkPaths, ...customizablePaths]) {
|
|
61
|
+
if (p.endsWith('/')) {
|
|
62
|
+
const dir = resolve(projectRoot, p)
|
|
63
|
+
await removeDirIfExists(dir)
|
|
64
|
+
removed++
|
|
65
|
+
} else {
|
|
66
|
+
const file = resolve(projectRoot, p)
|
|
67
|
+
if (existsSync(file)) {
|
|
68
|
+
await unlink(file)
|
|
69
|
+
removed++
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await removeDirIfExists(resolve(projectRoot, '.opencastle'))
|
|
75
|
+
removed++
|
|
76
|
+
|
|
77
|
+
if (hasLegacy) {
|
|
78
|
+
await unlink(legacyManifestPath)
|
|
79
|
+
removed++
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const gitignoreResult = await removeGitignoreBlock(projectRoot)
|
|
83
|
+
|
|
84
|
+
console.log(`\n ${c.green('✓')} Removed ${removed} path(s)${gitignoreResult === 'removed' ? ' + .gitignore block' : ''}.`)
|
|
85
|
+
console.log(` You can uninstall: ${c.bold('npm uninstall opencastle')}\n`)
|
|
86
|
+
|
|
87
|
+
closePrompts()
|
|
88
|
+
}
|
package/src/cli/gitignore.ts
CHANGED
|
@@ -72,3 +72,39 @@ export async function updateGitignore(
|
|
|
72
72
|
await writeFile(gitignorePath, existing + separator + block + '\n', 'utf8')
|
|
73
73
|
return 'updated'
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Remove the OpenCastle managed block from `.gitignore`.
|
|
78
|
+
*
|
|
79
|
+
* - No-op if no `.gitignore` exists or no block is present.
|
|
80
|
+
* - Cleans up resulting double blank lines.
|
|
81
|
+
* - Deletes `.gitignore` if the file becomes empty after removal.
|
|
82
|
+
* - Returns 'removed' or 'unchanged'.
|
|
83
|
+
*/
|
|
84
|
+
export async function removeGitignoreBlock(
|
|
85
|
+
projectRoot: string
|
|
86
|
+
): Promise<'removed' | 'unchanged'> {
|
|
87
|
+
const gitignorePath = resolve(projectRoot, '.gitignore')
|
|
88
|
+
if (!existsSync(gitignorePath)) return 'unchanged'
|
|
89
|
+
|
|
90
|
+
const existing = await readFile(gitignorePath, 'utf8')
|
|
91
|
+
const startIdx = existing.indexOf(START_MARKER)
|
|
92
|
+
const endIdx = existing.indexOf(END_MARKER)
|
|
93
|
+
|
|
94
|
+
if (startIdx === -1 || endIdx === -1) return 'unchanged'
|
|
95
|
+
|
|
96
|
+
const before = existing.slice(0, startIdx)
|
|
97
|
+
const after = existing.slice(endIdx + END_MARKER.length)
|
|
98
|
+
|
|
99
|
+
// Collapse consecutive blank lines left by removal
|
|
100
|
+
const updated = (before + after).replace(/\n{3,}/g, '\n\n').trimEnd()
|
|
101
|
+
|
|
102
|
+
if (!updated) {
|
|
103
|
+
const { unlink } = await import('node:fs/promises')
|
|
104
|
+
await unlink(gitignorePath)
|
|
105
|
+
return 'removed'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await writeFile(gitignorePath, updated + '\n', 'utf8')
|
|
109
|
+
return 'removed'
|
|
110
|
+
}
|
package/src/cli/plan.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { resolve, join, basename } from 'node:path'
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { getAdapter, detectAdapter } from './run/adapters/index.js'
|
|
6
|
+
import { parseTaskSpecText } from './run/schema.js'
|
|
7
|
+
import { c } from './prompt.js'
|
|
8
|
+
import type { CliContext, Task } from './types.js'
|
|
9
|
+
|
|
10
|
+
const HELP = `
|
|
11
|
+
opencastle plan [options]
|
|
12
|
+
|
|
13
|
+
Generate a convoy spec from a task description file by running it through the
|
|
14
|
+
generate-convoy prompt via an AI adapter.
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--file, -f <path> Path to a text file with the task description (required)
|
|
18
|
+
--context <path> Optional path to an additional context file
|
|
19
|
+
--output, -o <path> Output path for the generated convoy spec
|
|
20
|
+
--adapter, -a <name> Override agent runtime adapter
|
|
21
|
+
--verbose Show full agent output
|
|
22
|
+
--dry-run Print the prompt that would be sent without executing
|
|
23
|
+
--help, -h Show this help
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
interface PlanOptions {
|
|
27
|
+
file: string | null
|
|
28
|
+
context: string | null
|
|
29
|
+
output: string | null
|
|
30
|
+
adapter: string | null
|
|
31
|
+
verbose: boolean
|
|
32
|
+
dryRun: boolean
|
|
33
|
+
help: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseArgs(args: string[]): PlanOptions {
|
|
37
|
+
const opts: PlanOptions = {
|
|
38
|
+
file: null,
|
|
39
|
+
context: null,
|
|
40
|
+
output: null,
|
|
41
|
+
adapter: null,
|
|
42
|
+
verbose: false,
|
|
43
|
+
dryRun: false,
|
|
44
|
+
help: false,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
const arg = args[i]
|
|
49
|
+
switch (arg) {
|
|
50
|
+
case '--help':
|
|
51
|
+
case '-h':
|
|
52
|
+
opts.help = true
|
|
53
|
+
break
|
|
54
|
+
case '--file':
|
|
55
|
+
case '-f':
|
|
56
|
+
if (i + 1 >= args.length) { console.error(' ✗ --file requires a path'); process.exit(1) }
|
|
57
|
+
opts.file = args[++i]
|
|
58
|
+
break
|
|
59
|
+
case '--context':
|
|
60
|
+
if (i + 1 >= args.length) { console.error(' ✗ --context requires a path'); process.exit(1) }
|
|
61
|
+
opts.context = args[++i]
|
|
62
|
+
break
|
|
63
|
+
case '--output':
|
|
64
|
+
case '-o':
|
|
65
|
+
if (i + 1 >= args.length) { console.error(' ✗ --output requires a path'); process.exit(1) }
|
|
66
|
+
opts.output = args[++i]
|
|
67
|
+
break
|
|
68
|
+
case '--adapter':
|
|
69
|
+
case '-a':
|
|
70
|
+
if (i + 1 >= args.length) { console.error(' ✗ --adapter requires a name'); process.exit(1) }
|
|
71
|
+
opts.adapter = args[++i]
|
|
72
|
+
break
|
|
73
|
+
case '--verbose':
|
|
74
|
+
opts.verbose = true
|
|
75
|
+
break
|
|
76
|
+
case '--dry-run':
|
|
77
|
+
case '--dryRun':
|
|
78
|
+
opts.dryRun = true
|
|
79
|
+
break
|
|
80
|
+
default:
|
|
81
|
+
console.error(` ✗ Unknown option: ${arg}`)
|
|
82
|
+
console.log(HELP)
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return opts
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printAdapterError(detectionFailed: boolean, adapterName: string): void {
|
|
91
|
+
if (detectionFailed) {
|
|
92
|
+
console.error(
|
|
93
|
+
` ✗ No agent CLI found on your PATH.\n` +
|
|
94
|
+
` Install one of the following adapters:\n` +
|
|
95
|
+
` • copilot — https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli\n` +
|
|
96
|
+
` • claude — npm install -g @anthropic-ai/claude-code\n` +
|
|
97
|
+
` • cursor — https://cursor.com (Cursor > Install CLI)\n` +
|
|
98
|
+
` • opencode — https://opencode.ai\n` +
|
|
99
|
+
`\n` +
|
|
100
|
+
` Or specify an adapter explicitly: opencastle plan --adapter <name>`
|
|
101
|
+
)
|
|
102
|
+
} else {
|
|
103
|
+
const hints: Record<string, string> = {
|
|
104
|
+
'claude':
|
|
105
|
+
' Install: npm install -g @anthropic-ai/claude-code\n' +
|
|
106
|
+
' Docs: https://docs.anthropic.com/en/docs/claude-code',
|
|
107
|
+
copilot:
|
|
108
|
+
' Requires the Copilot CLI installed and authenticated:\n' +
|
|
109
|
+
' https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli\n' +
|
|
110
|
+
' Docs: https://docs.github.com/en/copilot',
|
|
111
|
+
cursor:
|
|
112
|
+
' The Cursor agent CLI ships with the Cursor editor.\n' +
|
|
113
|
+
' Install Cursor from https://cursor.com and ensure the\n' +
|
|
114
|
+
' "agent" command is on your PATH (Cursor > Install CLI).',
|
|
115
|
+
opencode:
|
|
116
|
+
' Install OpenCode from https://opencode.ai\n' +
|
|
117
|
+
' Ensure the "opencode" command is on your PATH.',
|
|
118
|
+
}
|
|
119
|
+
const cliName = adapterName === 'cursor' ? 'agent' : adapterName
|
|
120
|
+
const hint = hints[adapterName] ?? ''
|
|
121
|
+
console.error(
|
|
122
|
+
` ✗ Adapter "${adapterName}" is not available.\n` +
|
|
123
|
+
` Make sure the "${cliName}" CLI is installed and on your PATH.\n` +
|
|
124
|
+
hint
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Strip YAML frontmatter (everything between first and second --- lines).
|
|
131
|
+
*/
|
|
132
|
+
function stripFrontmatter(text: string): string {
|
|
133
|
+
const lines = text.split('\n')
|
|
134
|
+
if (lines[0]?.trim() !== '---') return text
|
|
135
|
+
const closingIdx = lines.findIndex((line, i) => i > 0 && line.trim() === '---')
|
|
136
|
+
if (closingIdx === -1) return text
|
|
137
|
+
return lines.slice(closingIdx + 1).join('\n').trimStart()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extract YAML content from a fenced code block (```yaml or ```yml).
|
|
142
|
+
*/
|
|
143
|
+
function extractYamlBlock(text: string): string | null {
|
|
144
|
+
const match = text.match(/```ya?ml\s*\n([\s\S]*?)```/)
|
|
145
|
+
if (!match) return null
|
|
146
|
+
return match[1].trim()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Derive an output filename from YAML content.
|
|
151
|
+
* Checks for a comment on the first line, then falls back to the `name` field.
|
|
152
|
+
*/
|
|
153
|
+
function deriveOutputFilename(yaml: string): string {
|
|
154
|
+
// First line comment: # .opencastle/convoys/some-name.convoy.yml
|
|
155
|
+
const firstLine = yaml.split('\n')[0] ?? ''
|
|
156
|
+
const commentMatch = firstLine.match(/^#\s*(.+\.convoy\.ya?ml)\s*$/)
|
|
157
|
+
if (commentMatch) {
|
|
158
|
+
return basename(commentMatch[1])
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Fall back to `name:` field
|
|
162
|
+
const nameMatch = yaml.match(/^name:\s*['"]?([^'"\n]+)['"]?\s*$/m)
|
|
163
|
+
if (nameMatch) {
|
|
164
|
+
const kebab = nameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
|
165
|
+
if (kebab) return `${kebab}.convoy.yml`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return 'convoy-plan.convoy.yml'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default async function plan({ args, pkgRoot }: CliContext): Promise<void> {
|
|
172
|
+
const opts = parseArgs(args)
|
|
173
|
+
|
|
174
|
+
if (opts.help) {
|
|
175
|
+
console.log(HELP)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Validate required --file arg ──────────────────────────────
|
|
180
|
+
if (!opts.file) {
|
|
181
|
+
console.error(` ✗ --file is required. Specify a text file with the task description.`)
|
|
182
|
+
console.log(HELP)
|
|
183
|
+
process.exit(1)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const filePath = resolve(process.cwd(), opts.file)
|
|
187
|
+
if (!existsSync(filePath)) {
|
|
188
|
+
console.error(` ✗ File not found: ${opts.file}`)
|
|
189
|
+
process.exit(1)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Read task description ──────────────────────────────────────
|
|
193
|
+
const taskDescription = await readFile(filePath, 'utf8')
|
|
194
|
+
|
|
195
|
+
// ── Read optional context file ─────────────────────────────────
|
|
196
|
+
let contextContent = ''
|
|
197
|
+
if (opts.context) {
|
|
198
|
+
const contextPath = resolve(process.cwd(), opts.context)
|
|
199
|
+
if (!existsSync(contextPath)) {
|
|
200
|
+
console.error(` ✗ Context file not found: ${opts.context}`)
|
|
201
|
+
process.exit(1)
|
|
202
|
+
}
|
|
203
|
+
contextContent = await readFile(contextPath, 'utf8')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Load and assemble the prompt template ─────────────────────
|
|
207
|
+
const promptTemplatePath = join(pkgRoot, 'src', 'orchestrator', 'prompts', 'generate-convoy.prompt.md')
|
|
208
|
+
if (!existsSync(promptTemplatePath)) {
|
|
209
|
+
console.error(` ✗ Prompt template not found: ${promptTemplatePath}`)
|
|
210
|
+
process.exit(1)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const rawTemplate = await readFile(promptTemplatePath, 'utf8')
|
|
214
|
+
const template = stripFrontmatter(rawTemplate)
|
|
215
|
+
const assembledPrompt = template
|
|
216
|
+
.replace('{{goal}}', taskDescription.trim())
|
|
217
|
+
.replace('{{context}}', contextContent.trim())
|
|
218
|
+
|
|
219
|
+
// ── Dry-run: print prompt and exit ────────────────────────────
|
|
220
|
+
if (opts.dryRun) {
|
|
221
|
+
console.log(c.bold(c.cyan(' Assembled prompt (dry-run):\n')))
|
|
222
|
+
console.log(assembledPrompt)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Resolve adapter ───────────────────────────────────────────
|
|
227
|
+
let adapterName: string
|
|
228
|
+
if (opts.adapter) {
|
|
229
|
+
adapterName = opts.adapter
|
|
230
|
+
} else {
|
|
231
|
+
const detected = await detectAdapter()
|
|
232
|
+
if (!detected) {
|
|
233
|
+
printAdapterError(true, '')
|
|
234
|
+
process.exit(1)
|
|
235
|
+
}
|
|
236
|
+
adapterName = detected
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let adapter
|
|
240
|
+
try {
|
|
241
|
+
adapter = await getAdapter(adapterName)
|
|
242
|
+
} catch {
|
|
243
|
+
printAdapterError(false, adapterName)
|
|
244
|
+
process.exit(1)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const available = await adapter.isAvailable()
|
|
248
|
+
if (!available) {
|
|
249
|
+
printAdapterError(false, adapterName)
|
|
250
|
+
process.exit(1)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(c.dim(` Using adapter: ${adapterName}`))
|
|
254
|
+
console.log(c.dim(` Generating convoy spec from: ${opts.file}\n`))
|
|
255
|
+
|
|
256
|
+
// ── Execute the prompt through the adapter ────────────────────
|
|
257
|
+
const task: Task = {
|
|
258
|
+
id: 'generate-convoy',
|
|
259
|
+
prompt: assembledPrompt,
|
|
260
|
+
agent: 'team-lead',
|
|
261
|
+
timeout: '10m',
|
|
262
|
+
depends_on: [],
|
|
263
|
+
files: [],
|
|
264
|
+
description: 'Generate convoy spec from task description',
|
|
265
|
+
max_retries: 1,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const result = await adapter.execute(task, { verbose: opts.verbose })
|
|
269
|
+
|
|
270
|
+
// ── Extract YAML from the response ────────────────────────────
|
|
271
|
+
const yamlContent = extractYamlBlock(result.output)
|
|
272
|
+
if (!yamlContent) {
|
|
273
|
+
const preview = result.output.slice(0, 500)
|
|
274
|
+
console.error(` ✗ No YAML code block found in the agent response.\n`)
|
|
275
|
+
console.error(c.dim(` Raw output (truncated):\n${preview}`))
|
|
276
|
+
process.exit(1)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Validate YAML ─────────────────────────────────────────────
|
|
280
|
+
let validationWarning = false
|
|
281
|
+
try {
|
|
282
|
+
parseTaskSpecText(yamlContent)
|
|
283
|
+
} catch (err) {
|
|
284
|
+
validationWarning = true
|
|
285
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
286
|
+
console.warn(c.yellow(` ⚠ YAML validation warning: ${msg}`))
|
|
287
|
+
console.warn(c.dim(` The file will still be written — you may need to edit it before running.\n`))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Determine output path ─────────────────────────────────────
|
|
291
|
+
let outputPath: string
|
|
292
|
+
if (opts.output) {
|
|
293
|
+
outputPath = resolve(process.cwd(), opts.output)
|
|
294
|
+
} else {
|
|
295
|
+
const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys')
|
|
296
|
+
await mkdir(convoyDir, { recursive: true })
|
|
297
|
+
const filename = deriveOutputFilename(yamlContent)
|
|
298
|
+
outputPath = join(convoyDir, filename)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await mkdir(resolve(outputPath, '..'), { recursive: true })
|
|
302
|
+
await writeFile(outputPath, yamlContent + '\n', 'utf8')
|
|
303
|
+
|
|
304
|
+
const relPath = outputPath.startsWith(process.cwd())
|
|
305
|
+
? outputPath.slice(process.cwd().length + 1)
|
|
306
|
+
: outputPath
|
|
307
|
+
|
|
308
|
+
console.log(c.green(` ✓ Convoy spec written to ${relPath}`))
|
|
309
|
+
if (validationWarning) {
|
|
310
|
+
console.log(c.yellow(` (contains validation warnings — review before running)`))
|
|
311
|
+
}
|
|
312
|
+
console.log(`
|
|
313
|
+
${c.dim('Preview:')} npx opencastle run -f ${relPath} --dry-run
|
|
314
|
+
${c.dim('Execute:')} npx opencastle run -f ${relPath}
|
|
315
|
+
`)
|
|
316
|
+
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hash": "
|
|
2
|
+
"hash": "46302718",
|
|
3
3
|
"configHash": "30f8ea04",
|
|
4
|
-
"lockfileHash": "
|
|
5
|
-
"browserHash": "
|
|
4
|
+
"lockfileHash": "0bec59b6",
|
|
5
|
+
"browserHash": "455152ca",
|
|
6
6
|
"optimized": {
|
|
7
7
|
"astro > cssesc": {
|
|
8
8
|
"src": "../../../../../node_modules/cssesc/cssesc.js",
|
|
9
9
|
"file": "astro___cssesc.js",
|
|
10
|
-
"fileHash": "
|
|
10
|
+
"fileHash": "5409385e",
|
|
11
11
|
"needsInterop": true
|
|
12
12
|
},
|
|
13
13
|
"astro > aria-query": {
|
|
14
14
|
"src": "../../../../../node_modules/aria-query/lib/index.js",
|
|
15
15
|
"file": "astro___aria-query.js",
|
|
16
|
-
"fileHash": "
|
|
16
|
+
"fileHash": "5fd7c72e",
|
|
17
17
|
"needsInterop": true
|
|
18
18
|
},
|
|
19
19
|
"astro > axobject-query": {
|
|
20
20
|
"src": "../../../../../node_modules/axobject-query/lib/index.js",
|
|
21
21
|
"file": "astro___axobject-query.js",
|
|
22
|
-
"fileHash": "
|
|
22
|
+
"fileHash": "530306fc",
|
|
23
23
|
"needsInterop": true
|
|
24
24
|
}
|
|
25
25
|
},
|