@aksp/opencrew 1.3.3 → 1.4.1
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/CHANGELOG.md +231 -139
- package/README.md +286 -150
- package/package.json +63 -63
- package/src/cli.js +137 -136
- package/src/commands/init.js +159 -125
- package/src/commands/update.js +95 -87
- package/src/lib/fsx.js +127 -127
- package/src/lib/ides.js +61 -4
- package/templates/.mcp.json +9 -9
- package/templates/AGENTS.md +139 -133
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/_memory/preferences.md +11 -11
- package/templates/_opencrew/core/prompts/build.prompt.md +633 -614
- package/templates/_opencrew/core/prompts/repair.prompt.md +119 -119
- package/templates/_opencrew/core/runner.pipeline.md +829 -729
- package/templates/_opencrew/core/skills.engine.md +490 -490
- package/templates/gitignore +1 -0
- package/templates/skills/README.md +22 -22
- package/templates/skills/catalog.json +61 -61
- package/templates/skills/instagram-publisher/SKILL.md +119 -119
package/src/cli.js
CHANGED
|
@@ -1,136 +1,137 @@
|
|
|
1
|
-
import { readJson } from './lib/fsx.js';
|
|
2
|
-
import { packageJsonPath } from './lib/paths.js';
|
|
3
|
-
import { allIdeIds } from './lib/ides.js';
|
|
4
|
-
import { init } from './commands/init.js';
|
|
5
|
-
import { update } from './commands/update.js';
|
|
6
|
-
import { c, log, err, warn, info } from './lib/ui.js';
|
|
7
|
-
|
|
8
|
-
// Extract the minimum required Node version from an engines.node range string.
|
|
9
|
-
// Handles: ">=20.0.0", "^20.5", ">=18.0.0 || >=20.0.0", plain "20.0.0".
|
|
10
|
-
function minNodeVersion(range) {
|
|
11
|
-
// Split on || and take the lowest version (user is expected to meet at least one).
|
|
12
|
-
const parts = range.split(/\s*\|\|\s*/);
|
|
13
|
-
let lowest = null;
|
|
14
|
-
for (const part of parts) {
|
|
15
|
-
const v = part.replace(/[^0-9.]/g, '');
|
|
16
|
-
if (!v) continue;
|
|
17
|
-
if (!lowest || lt(v, lowest)) lowest = v;
|
|
18
|
-
}
|
|
19
|
-
return lowest;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Simple semver comparison (no prerelease tags). Returns true if a < b.
|
|
23
|
-
function lt(a, b) {
|
|
24
|
-
const pa = a.split('.').map(Number);
|
|
25
|
-
const pb = b.split('.').map(Number);
|
|
26
|
-
for (let i = 0; i < 3; i++) {
|
|
27
|
-
const na = pa[i] || 0;
|
|
28
|
-
const nb = pb[i] || 0;
|
|
29
|
-
if (na !== nb) return na < nb;
|
|
30
|
-
}
|
|
31
|
-
return false; // equal
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function parseArgs(argv) {
|
|
35
|
-
const opts = { _: [] };
|
|
36
|
-
for (const a of argv) {
|
|
37
|
-
if (a.startsWith('--')) {
|
|
38
|
-
const eq = a.indexOf('=');
|
|
39
|
-
const k = eq > -1 ? a.slice(2, eq) : a.slice(2);
|
|
40
|
-
const v = eq > -1 ? a.slice(eq + 1) : true;
|
|
41
|
-
opts[k] = v;
|
|
42
|
-
} else if (a.startsWith('-') && a.length === 2) {
|
|
43
|
-
// Short flags: -y → yes, -v → version, -h → help
|
|
44
|
-
const short = a[1];
|
|
45
|
-
opts[short] = true;
|
|
46
|
-
} else {
|
|
47
|
-
opts._.push(a);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
// Normalize short flags to their long-form equivalents.
|
|
51
|
-
if (opts.y) opts.yes = true;
|
|
52
|
-
if (opts.v) opts.version = true;
|
|
53
|
-
if (opts.h) opts.help = true;
|
|
54
|
-
return opts;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function help(version) {
|
|
58
|
-
log(`
|
|
59
|
-
${c.bold(c.cyan('opencrew'))} ${c.dim('v' + version)} — create AI agent crews that work together
|
|
60
|
-
|
|
61
|
-
${c.bold('Usage')}
|
|
62
|
-
npx @aksp/opencrew <command> [options]
|
|
63
|
-
|
|
64
|
-
${c.bold('Commands')}
|
|
65
|
-
init Scaffold an opencrew workspace in the current folder
|
|
66
|
-
update Refresh the framework (keeps your crews, memory and .env)
|
|
67
|
-
upgrade Alias for update
|
|
68
|
-
help Show this help
|
|
69
|
-
version Print the version
|
|
70
|
-
|
|
71
|
-
${c.bold('Options for init')}
|
|
72
|
-
--ide=a,b Preselect IDEs (skip the prompt). Valid: ${allIdeIds().join(', ')}
|
|
73
|
-
--all Configure every supported IDE
|
|
74
|
-
--yes, -y Non-interactive; accept defaults
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
npx @aksp/opencrew init
|
|
82
|
-
npx @aksp/opencrew init --
|
|
83
|
-
npx @aksp/opencrew init
|
|
84
|
-
npx @aksp/opencrew
|
|
85
|
-
npx @aksp/opencrew update
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
case '
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
case '
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
case '
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
1
|
+
import { readJson } from './lib/fsx.js';
|
|
2
|
+
import { packageJsonPath } from './lib/paths.js';
|
|
3
|
+
import { allIdeIds } from './lib/ides.js';
|
|
4
|
+
import { init } from './commands/init.js';
|
|
5
|
+
import { update } from './commands/update.js';
|
|
6
|
+
import { c, log, err, warn, info } from './lib/ui.js';
|
|
7
|
+
|
|
8
|
+
// Extract the minimum required Node version from an engines.node range string.
|
|
9
|
+
// Handles: ">=20.0.0", "^20.5", ">=18.0.0 || >=20.0.0", plain "20.0.0".
|
|
10
|
+
function minNodeVersion(range) {
|
|
11
|
+
// Split on || and take the lowest version (user is expected to meet at least one).
|
|
12
|
+
const parts = range.split(/\s*\|\|\s*/);
|
|
13
|
+
let lowest = null;
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
const v = part.replace(/[^0-9.]/g, '');
|
|
16
|
+
if (!v) continue;
|
|
17
|
+
if (!lowest || lt(v, lowest)) lowest = v;
|
|
18
|
+
}
|
|
19
|
+
return lowest;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Simple semver comparison (no prerelease tags). Returns true if a < b.
|
|
23
|
+
function lt(a, b) {
|
|
24
|
+
const pa = a.split('.').map(Number);
|
|
25
|
+
const pb = b.split('.').map(Number);
|
|
26
|
+
for (let i = 0; i < 3; i++) {
|
|
27
|
+
const na = pa[i] || 0;
|
|
28
|
+
const nb = pb[i] || 0;
|
|
29
|
+
if (na !== nb) return na < nb;
|
|
30
|
+
}
|
|
31
|
+
return false; // equal
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const opts = { _: [] };
|
|
36
|
+
for (const a of argv) {
|
|
37
|
+
if (a.startsWith('--')) {
|
|
38
|
+
const eq = a.indexOf('=');
|
|
39
|
+
const k = eq > -1 ? a.slice(2, eq) : a.slice(2);
|
|
40
|
+
const v = eq > -1 ? a.slice(eq + 1) : true;
|
|
41
|
+
opts[k] = v;
|
|
42
|
+
} else if (a.startsWith('-') && a.length === 2) {
|
|
43
|
+
// Short flags: -y → yes, -v → version, -h → help
|
|
44
|
+
const short = a[1];
|
|
45
|
+
opts[short] = true;
|
|
46
|
+
} else {
|
|
47
|
+
opts._.push(a);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Normalize short flags to their long-form equivalents.
|
|
51
|
+
if (opts.y) opts.yes = true;
|
|
52
|
+
if (opts.v) opts.version = true;
|
|
53
|
+
if (opts.h) opts.help = true;
|
|
54
|
+
return opts;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function help(version) {
|
|
58
|
+
log(`
|
|
59
|
+
${c.bold(c.cyan('opencrew'))} ${c.dim('v' + version)} — create AI agent crews that work together
|
|
60
|
+
|
|
61
|
+
${c.bold('Usage')}
|
|
62
|
+
npx @aksp/opencrew <command> [options]
|
|
63
|
+
|
|
64
|
+
${c.bold('Commands')}
|
|
65
|
+
init Scaffold an opencrew workspace in the current folder
|
|
66
|
+
update Refresh the framework (keeps your crews, memory and .env)
|
|
67
|
+
upgrade Alias for update
|
|
68
|
+
help Show this help
|
|
69
|
+
version Print the version
|
|
70
|
+
|
|
71
|
+
${c.bold('Options for init')}
|
|
72
|
+
--ide=a,b Preselect IDEs (skip the prompt). Valid: ${allIdeIds().join(', ')}
|
|
73
|
+
--all Configure every supported IDE
|
|
74
|
+
--yes, -y Non-interactive; accept defaults
|
|
75
|
+
--repair-bridges Regenerate IDE bridge files in an existing workspace
|
|
76
|
+
|
|
77
|
+
${c.bold('Options for update')}
|
|
78
|
+
--check Dry-run: report whether an update is available without making changes
|
|
79
|
+
|
|
80
|
+
${c.bold('Examples')}
|
|
81
|
+
npx @aksp/opencrew init
|
|
82
|
+
npx @aksp/opencrew init --ide=claude-code,codex
|
|
83
|
+
npx @aksp/opencrew init --all
|
|
84
|
+
npx @aksp/opencrew init -y
|
|
85
|
+
npx @aksp/opencrew update
|
|
86
|
+
npx @aksp/opencrew update --check
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function run(argv) {
|
|
91
|
+
const opts = parseArgs(argv);
|
|
92
|
+
|
|
93
|
+
let version = 'unknown';
|
|
94
|
+
let engines = {};
|
|
95
|
+
try {
|
|
96
|
+
const pkg = await readJson(packageJsonPath);
|
|
97
|
+
version = pkg.version;
|
|
98
|
+
engines = pkg.engines || {};
|
|
99
|
+
} catch {
|
|
100
|
+
err('Could not read package.json. The installation may be corrupted.');
|
|
101
|
+
info('Try reinstalling: npm install @aksp/opencrew');
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Validate Node version against engines.node requirement.
|
|
107
|
+
if (engines.node) {
|
|
108
|
+
const required = minNodeVersion(engines.node);
|
|
109
|
+
const current = process.versions.node;
|
|
110
|
+
if (required && lt(current, required)) {
|
|
111
|
+
warn(`opencrew requires Node.js ${engines.node}. You have v${current}.`);
|
|
112
|
+
info(`Upgrade Node or use a compatible version.`);
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cmd = opts._[0] || (opts.version ? 'version' : opts.help ? 'help' : 'init');
|
|
119
|
+
|
|
120
|
+
switch (cmd) {
|
|
121
|
+
case 'init':
|
|
122
|
+
return init(opts);
|
|
123
|
+
case 'update':
|
|
124
|
+
case 'upgrade':
|
|
125
|
+
return update(opts);
|
|
126
|
+
case 'version':
|
|
127
|
+
case '--version':
|
|
128
|
+
return log(version);
|
|
129
|
+
case 'help':
|
|
130
|
+
case '--help':
|
|
131
|
+
return help(version);
|
|
132
|
+
default:
|
|
133
|
+
err(`Unknown command: ${cmd}`);
|
|
134
|
+
help(version);
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -1,125 +1,159 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { promises as fs } from 'node:fs';
|
|
3
|
-
import { templatesDir, packageJsonPath } from '../lib/paths.js';
|
|
4
|
-
import { copyDir, exists, writeFileSafe, readJson, writeBridgeFile } from '../lib/fsx.js';
|
|
5
|
-
import { ideById, allIdeIds } from '../lib/ides.js';
|
|
6
|
-
import { pickIdes } from '../lib/prompts.js';
|
|
7
|
-
import { c, log, info, ok, warn, step } from '../lib/ui.js';
|
|
8
|
-
|
|
9
|
-
export async function init(opts = {}) {
|
|
10
|
-
const target = process.cwd();
|
|
11
|
-
const pkg = await readJson(packageJsonPath);
|
|
12
|
-
const version = pkg.version;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (alreadyInstalled) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await writeFileSafe(path.join(target, '.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
warn(`
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
log(
|
|
111
|
-
log(
|
|
112
|
-
log(`
|
|
113
|
-
log(`
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { templatesDir, packageJsonPath } from '../lib/paths.js';
|
|
4
|
+
import { copyDir, exists, writeFileSafe, readJson, writeBridgeFile } from '../lib/fsx.js';
|
|
5
|
+
import { ideById, allIdeIds } from '../lib/ides.js';
|
|
6
|
+
import { pickIdes } from '../lib/prompts.js';
|
|
7
|
+
import { c, log, info, ok, warn, step } from '../lib/ui.js';
|
|
8
|
+
|
|
9
|
+
export async function init(opts = {}) {
|
|
10
|
+
const target = process.cwd();
|
|
11
|
+
const pkg = await readJson(packageJsonPath);
|
|
12
|
+
const version = pkg.version;
|
|
13
|
+
const repairBridges = opts['repair-bridges'];
|
|
14
|
+
|
|
15
|
+
const alreadyInstalled = await exists(path.join(target, '_opencrew', 'core'));
|
|
16
|
+
|
|
17
|
+
// --repair-bridges mode: regenerate IDE bridge files in an existing workspace.
|
|
18
|
+
if (repairBridges && alreadyInstalled) {
|
|
19
|
+
log(`\n${c.bold(c.cyan('opencrew'))} ${c.dim('v' + version)} — repairing IDE bridges`);
|
|
20
|
+
log(c.dim(`Target: ${target}\n`));
|
|
21
|
+
|
|
22
|
+
let ids = normalizeIdes(opts.ide);
|
|
23
|
+
if (opts.all) ids = allIdeIds();
|
|
24
|
+
if (opts.yes || !ids) ids = allIdeIds();
|
|
25
|
+
await writeBridges(target, ids, { overwrite: true });
|
|
26
|
+
|
|
27
|
+
log(`\n${c.green(c.bold('Done!'))} IDE bridges regenerated.\n`);
|
|
28
|
+
log(`${c.bold('Next step:')} Restart your IDE, then type ${c.cyan('/opencrew')} to verify.\n`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (alreadyInstalled) {
|
|
33
|
+
warn('An opencrew workspace already exists here.');
|
|
34
|
+
info(`To update only the framework, use: ${c.cyan('npx @aksp/opencrew update')}`);
|
|
35
|
+
info(`To repair IDE bridges, use: ${c.cyan('npx @aksp/opencrew init --repair-bridges')}`);
|
|
36
|
+
info(`To reinstall from scratch, delete _opencrew/ first, then run init again.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
log(`\n${c.bold(c.cyan('opencrew'))} ${c.dim('v' + version)} — scaffolding a crew workspace`);
|
|
41
|
+
log(c.dim(`Target: ${target}\n`));
|
|
42
|
+
|
|
43
|
+
// 1. Copy the framework payload (never clobber user work).
|
|
44
|
+
step('Installing framework files');
|
|
45
|
+
const copied = { count: 0 };
|
|
46
|
+
await copyDir(path.join(templatesDir, '_opencrew'), path.join(target, '_opencrew'), {
|
|
47
|
+
overwrite: false,
|
|
48
|
+
// Never ship stray logs or browser sessions; keep the empty dir via .gitkeep only.
|
|
49
|
+
skip: (rel) =>
|
|
50
|
+
(rel.startsWith('logs/') && rel !== 'logs/.gitkeep') ||
|
|
51
|
+
rel.startsWith('_browser_profile/'),
|
|
52
|
+
onCopy: () => (copied.count += 1),
|
|
53
|
+
});
|
|
54
|
+
await copyDir(path.join(templatesDir, 'skills'), path.join(target, 'skills'), {
|
|
55
|
+
overwrite: false,
|
|
56
|
+
onCopy: () => (copied.count += 1),
|
|
57
|
+
});
|
|
58
|
+
await copyDir(path.join(templatesDir, 'crews'), path.join(target, 'crews'), {
|
|
59
|
+
overwrite: false,
|
|
60
|
+
});
|
|
61
|
+
ok(`Framework files ready (${copied.count} written, existing files preserved)`);
|
|
62
|
+
|
|
63
|
+
// 2. System doc + root configs.
|
|
64
|
+
step('Writing configuration');
|
|
65
|
+
|
|
66
|
+
// Full system definition lives in _opencrew/core/ — never at project root.
|
|
67
|
+
// The root AGENTS.md is just a thin bridge (like CLAUDE.md, GEMINI.md, etc.).
|
|
68
|
+
await writeFileSafe(path.join(target, '_opencrew', 'core', 'system.md'), await tpl('AGENTS.md'));
|
|
69
|
+
ok('_opencrew/core/system.md (full system definition)');
|
|
70
|
+
|
|
71
|
+
const agentsBridge = '# opencrew\n\n'
|
|
72
|
+
+ 'The opencrew system definition lives at `_opencrew/core/system.md`.\n'
|
|
73
|
+
+ 'Read that file and adopt the opencrew system role — follow all initialization,\n'
|
|
74
|
+
+ 'command routing, and workflow instructions defined there.\n\n'
|
|
75
|
+
+ 'Type `/opencrew` to open the main menu.\n';
|
|
76
|
+
|
|
77
|
+
const agentsResult = await writeBridgeFile(path.join(target, 'AGENTS.md'), agentsBridge);
|
|
78
|
+
if (agentsResult.merged) info('AGENTS.md (merged — existing content preserved)');
|
|
79
|
+
else ok('AGENTS.md (bridge to system.md)');
|
|
80
|
+
|
|
81
|
+
const mcpWritten = await writeFileSafe(path.join(target, '.mcp.json'), await tpl('.mcp.json'), {
|
|
82
|
+
overwrite: false,
|
|
83
|
+
});
|
|
84
|
+
info(mcpWritten ? '.mcp.json' : '.mcp.json (kept existing)');
|
|
85
|
+
|
|
86
|
+
await writeFileSafe(path.join(target, '.env.example'), await tpl('.env.example'));
|
|
87
|
+
const giWritten = await writeFileSafe(path.join(target, '.gitignore'), await tpl('gitignore'), {
|
|
88
|
+
overwrite: false,
|
|
89
|
+
});
|
|
90
|
+
info(giWritten ? '.gitignore' : '.gitignore (kept existing)');
|
|
91
|
+
|
|
92
|
+
// 3. IDE bridge files.
|
|
93
|
+
step('Configuring AI IDEs');
|
|
94
|
+
let ids = normalizeIdes(opts.ide);
|
|
95
|
+
if (opts.all) ids = allIdeIds();
|
|
96
|
+
if (opts.yes) ids = allIdeIds();
|
|
97
|
+
if (!ids) ids = await pickIdes();
|
|
98
|
+
await writeBridges(target, ids, { overwrite: false });
|
|
99
|
+
|
|
100
|
+
if (ids.includes('claude-code')) {
|
|
101
|
+
warn(`opencrew ships its own Playwright MCP server (.mcp.json) — disable Claude Code's`);
|
|
102
|
+
warn(`native Playwright plugin/extension to avoid the two conflicting.`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. Version stamp.
|
|
106
|
+
await fs.writeFile(path.join(target, '_opencrew', '.opencrew-version'), version + '\n');
|
|
107
|
+
|
|
108
|
+
// 5. Done.
|
|
109
|
+
log(`\n${c.green(c.bold('Done!'))} opencrew is installed.\n`);
|
|
110
|
+
log(`${c.bold('Next steps:')}`);
|
|
111
|
+
log(` 1. Open this folder in your AI IDE.`);
|
|
112
|
+
log(` 2. Type ${c.cyan('/opencrew')} to start (first run sets up your company profile).`);
|
|
113
|
+
log(` No API keys needed up front — opencrew asks for them in chat only if a skill you use requires one.\n`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Write IDE bridge files to the target directory.
|
|
118
|
+
* @param {string} target — project root
|
|
119
|
+
* @param {string[]} ids — IDE ids to configure
|
|
120
|
+
* @param {{ overwrite: boolean }} opts
|
|
121
|
+
*/
|
|
122
|
+
async function writeBridges(target, ids, { overwrite }) {
|
|
123
|
+
const writtenPaths = new Set();
|
|
124
|
+
|
|
125
|
+
for (const id of ids) {
|
|
126
|
+
const ide = ideById(id);
|
|
127
|
+
if (!ide) {
|
|
128
|
+
warn(`Unknown IDE "${id}" — skipped. Valid: ${allIdeIds().join(', ')}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
for (const f of ide.files) {
|
|
132
|
+
if (writtenPaths.has(f.path)) {
|
|
133
|
+
info(`${f.path} (shared path — written once)`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
writtenPaths.add(f.path);
|
|
137
|
+
const fp = path.join(target, f.path);
|
|
138
|
+
const hasFrontmatter = f.content.startsWith('---');
|
|
139
|
+
if (hasFrontmatter) {
|
|
140
|
+
await writeFileSafe(fp, f.content, { overwrite });
|
|
141
|
+
} else {
|
|
142
|
+
const result = await writeBridgeFile(fp, f.content);
|
|
143
|
+
if (result.merged) info(`${f.path} (merged — existing content preserved)`);
|
|
144
|
+
else if (result.written && overwrite) info(`${f.path} (regenerated)`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
ok(`${ide.label} → ${ide.files.map((f) => f.path).join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function tpl(name) {
|
|
152
|
+
return fs.readFile(path.join(templatesDir, name), 'utf8');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeIdes(val) {
|
|
156
|
+
if (!val || val === true) return null;
|
|
157
|
+
const list = Array.isArray(val) ? val : String(val).split(',');
|
|
158
|
+
return list.map((s) => s.trim()).filter(Boolean);
|
|
159
|
+
}
|