@boopkit/cli 0.1.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 +65 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +85 -0
- package/dist/commands/add.d.ts +6 -0
- package/dist/commands/add.js +173 -0
- package/dist/commands/adopt.d.ts +5 -0
- package/dist/commands/adopt.js +286 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +295 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +105 -0
- package/dist/commands/loop.d.ts +63 -0
- package/dist/commands/loop.js +765 -0
- package/dist/commands/status.d.ts +40 -0
- package/dist/commands/status.js +303 -0
- package/dist/commands/status.test.d.ts +1 -0
- package/dist/commands/status.test.js +118 -0
- package/dist/commands/update.d.ts +8 -0
- package/dist/commands/update.js +204 -0
- package/dist/lib/capability.d.ts +17 -0
- package/dist/lib/capability.js +172 -0
- package/dist/lib/config.d.ts +70 -0
- package/dist/lib/config.js +59 -0
- package/dist/lib/db-status.d.ts +64 -0
- package/dist/lib/db-status.js +257 -0
- package/dist/lib/env.d.ts +25 -0
- package/dist/lib/env.js +116 -0
- package/dist/lib/execute.d.ts +21 -0
- package/dist/lib/execute.js +228 -0
- package/dist/lib/interpolate.d.ts +4 -0
- package/dist/lib/interpolate.js +13 -0
- package/dist/lib/manifest.d.ts +54 -0
- package/dist/lib/manifest.js +11 -0
- package/dist/lib/preflight.d.ts +31 -0
- package/dist/lib/preflight.js +402 -0
- package/dist/lib/profile.d.ts +11 -0
- package/dist/lib/profile.js +73 -0
- package/dist/lib/project.d.ts +36 -0
- package/dist/lib/project.js +71 -0
- package/dist/lib/project.test.d.ts +1 -0
- package/dist/lib/project.test.js +118 -0
- package/dist/lib/rewire.d.ts +19 -0
- package/dist/lib/rewire.js +222 -0
- package/dist/lib/safe-io.d.ts +66 -0
- package/dist/lib/safe-io.js +167 -0
- package/dist/lib/scaffold.d.ts +87 -0
- package/dist/lib/scaffold.js +548 -0
- package/dist/lib/update.d.ts +73 -0
- package/dist/lib/update.js +379 -0
- package/package.json +46 -0
- package/profiles/web-app/profile.md +113 -0
- package/profiles/web-app/reference/architecture.ref.md +65 -0
- package/profiles/web-app/reference/message-queue-patterns.ref.md +247 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# boop
|
|
2
|
+
|
|
3
|
+
CLI tool to scaffold new projects from reusable profiles and capabilities. Get a running web app with proven architecture, conventions, and AI agent skills — in one command.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @boopkit/cli@beta
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use directly:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @boopkit/cli@beta init myapp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
boop init my-app
|
|
21
|
+
cd my-app
|
|
22
|
+
npm run dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
| Command | Description |
|
|
28
|
+
|---------|-------------|
|
|
29
|
+
| `boop list profiles` | List available profiles |
|
|
30
|
+
| `boop init <name>` | Create a new project from a profile (default profile is `web-app`) |
|
|
31
|
+
| `boop status` | Show current project status |
|
|
32
|
+
| `boop caps` | List available capabilities (alias for `boop list capabilities`) |
|
|
33
|
+
| `boop add <capability>` | Add a capability to an existing project |
|
|
34
|
+
|
|
35
|
+
## What You Get
|
|
36
|
+
|
|
37
|
+
`boop init` scaffolds a full-stack web application with:
|
|
38
|
+
|
|
39
|
+
- **Express** server with Handlebars views and Tailwind CSS
|
|
40
|
+
- **REST API** with controllers, routes, and middleware
|
|
41
|
+
- **TypeScript** with strict mode and path aliases
|
|
42
|
+
- **Jest** testing setup with TDD workflow
|
|
43
|
+
- **Multi-process architecture** (web, worker, scheduler)
|
|
44
|
+
- **Heroku deployment** via Procfile
|
|
45
|
+
- **AI agent skills** for development, testing, design, and project management
|
|
46
|
+
|
|
47
|
+
Capabilities are additive. Start simple, add complexity when you need it:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
boop add users-and-accounts # Adds user authentication and account management features
|
|
51
|
+
boop add email # Adds email sending capabilities with templates
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install
|
|
58
|
+
npm run build
|
|
59
|
+
npm run test
|
|
60
|
+
npm run dev -- init test-project # Run from source
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
Private
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const status_1 = require("./commands/status");
|
|
6
|
+
const init_1 = require("./commands/init");
|
|
7
|
+
const list_1 = require("./commands/list");
|
|
8
|
+
const add_1 = require("./commands/add");
|
|
9
|
+
const adopt_1 = require("./commands/adopt");
|
|
10
|
+
const update_1 = require("./commands/update");
|
|
11
|
+
const loop_1 = require("./commands/loop");
|
|
12
|
+
const program = new commander_1.Command();
|
|
13
|
+
program
|
|
14
|
+
.name('boop')
|
|
15
|
+
.description('Scaffold new projects from boop profiles and capabilities')
|
|
16
|
+
.version('0.1.0');
|
|
17
|
+
program
|
|
18
|
+
.command('init')
|
|
19
|
+
.description('Create a new project from a boop profile')
|
|
20
|
+
.argument('<name>', 'Project name')
|
|
21
|
+
.option('-p, --profile <profile>', 'Profile to use', 'web-app')
|
|
22
|
+
.option('--no-install', 'Skip npm install')
|
|
23
|
+
.action(init_1.runInit);
|
|
24
|
+
program
|
|
25
|
+
.command('list')
|
|
26
|
+
.description('List available profiles or capabilities')
|
|
27
|
+
.argument('<type>', 'What to list: profiles or capabilities')
|
|
28
|
+
.action(list_1.runList);
|
|
29
|
+
program
|
|
30
|
+
.command('status')
|
|
31
|
+
.description('Show current boop project status')
|
|
32
|
+
.option('--json', 'Output status as JSON')
|
|
33
|
+
.action(status_1.runStatus);
|
|
34
|
+
program
|
|
35
|
+
.command('caps')
|
|
36
|
+
.description('List available capabilities (shortcut for "list capabilities")')
|
|
37
|
+
.action(() => (0, list_1.runList)('capabilities'));
|
|
38
|
+
program
|
|
39
|
+
.command('add')
|
|
40
|
+
.description('Add a capability to an existing boop project')
|
|
41
|
+
.argument('[capability]', 'Capability to add')
|
|
42
|
+
.option('--no-install', 'Skip npm install')
|
|
43
|
+
.option('--json', 'Output structured JSON')
|
|
44
|
+
.option('--dry-run', 'Show what would be done without making changes')
|
|
45
|
+
.addHelpText('after', '\nRun `boop list capabilities` to see available options.')
|
|
46
|
+
.action(add_1.runAdd);
|
|
47
|
+
program
|
|
48
|
+
.command('update')
|
|
49
|
+
.description('Update scaffold files from capability packages')
|
|
50
|
+
.argument('[capability]', 'Capability to update (all if omitted)')
|
|
51
|
+
.option('--dry-run', 'Show what would change without writing')
|
|
52
|
+
.option('--force', 'Overwrite locally modified files (creates .bak backup)')
|
|
53
|
+
.option('--restore', 'Restore locally deleted scaffold files')
|
|
54
|
+
.option('--prune', 'Remove deprecated scaffold files from disk (creates .bak backup)')
|
|
55
|
+
.option('--json', 'Output structured JSON')
|
|
56
|
+
.action(update_1.runUpdate);
|
|
57
|
+
program
|
|
58
|
+
.command('adopt')
|
|
59
|
+
.description('Copy package source into the project for full ownership')
|
|
60
|
+
.argument('[capability]', 'Capability whose package to adopt')
|
|
61
|
+
.option('--no-install', 'Skip npm install')
|
|
62
|
+
.option('--json', 'Output structured JSON')
|
|
63
|
+
.action(adopt_1.runAdopt);
|
|
64
|
+
program
|
|
65
|
+
.command('loop')
|
|
66
|
+
.description('Continuously process ready beads issues via Claude Code sessions')
|
|
67
|
+
.option('--prefix <prefix>', 'Issue ID prefix to filter by')
|
|
68
|
+
.option('--work <path>', 'Spec file for planning phase')
|
|
69
|
+
.option('--flow <path>', 'Workflow file for planning phase')
|
|
70
|
+
.option('--run', 'Actually execute (dry-run by default)')
|
|
71
|
+
.option('--max <n>', 'Stop after N issues')
|
|
72
|
+
.option('--budget <amount>', 'Budget cap per issue in USD (default: 5.00)')
|
|
73
|
+
.option('--resume-limit <n>', 'Max resume attempts per issue (default: 3)')
|
|
74
|
+
.option('--poll', 'Keep polling when no ready issues found')
|
|
75
|
+
.option('--poll-interval <seconds>', 'Seconds between polls (default: 60)')
|
|
76
|
+
.option('--model <model>', 'Claude model to use (default: claude-opus-4-6)')
|
|
77
|
+
.option('--per-issue-branch', 'Create a separate branch per issue')
|
|
78
|
+
.option('--no-branch', 'Skip branch creation entirely')
|
|
79
|
+
.option('--features', 'Include feature-type issues (excluded by default)')
|
|
80
|
+
.option('--no-skip-perms', 'Do not skip permission prompts')
|
|
81
|
+
.option('--bare', 'Force API key auth (--bare mode); skips OAuth/keychain, uses ANTHROPIC_API_KEY strictly')
|
|
82
|
+
.option('--json', 'Output summary as JSON')
|
|
83
|
+
.addHelpText('after', '\nBy default runs in dry-run mode. Add --run to execute.')
|
|
84
|
+
.action(loop_1.runLoop);
|
|
85
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runAdd = runAdd;
|
|
4
|
+
const preflight_1 = require("../lib/preflight");
|
|
5
|
+
const execute_1 = require("../lib/execute");
|
|
6
|
+
const interpolate_1 = require("../lib/interpolate");
|
|
7
|
+
const list_1 = require("./list");
|
|
8
|
+
function runAdd(capabilityName, options) {
|
|
9
|
+
// 0. Validate capability argument is provided
|
|
10
|
+
if (!capabilityName) {
|
|
11
|
+
console.error(`\x1b[31mERR\x1b[0m Missing capability argument.\n`);
|
|
12
|
+
(0, list_1.printGroupedCapabilities)();
|
|
13
|
+
console.error(`Usage: boop add <capability>`);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const targetDir = process.cwd();
|
|
18
|
+
// --- Phase 1: Pre-flight (read-only) ---
|
|
19
|
+
let plan;
|
|
20
|
+
try {
|
|
21
|
+
plan = (0, preflight_1.planAdd)(targetDir, capabilityName);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err instanceof SyntaxError) {
|
|
25
|
+
console.error(`\x1b[31mERR\x1b[0m .boop/config.json is corrupt (invalid JSON). Delete it and run \`boop init\` to recreate.`);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (err instanceof preflight_1.PreflightError) {
|
|
30
|
+
console.error(`\x1b[31mERR\x1b[0m Pre-flight failed:\n`);
|
|
31
|
+
for (const check of err.checks) {
|
|
32
|
+
const icon = check.passed ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
33
|
+
const msg = check.message ? ` — ${check.message}` : '';
|
|
34
|
+
console.error(` ${icon} ${check.name}${msg}`);
|
|
35
|
+
}
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
const { manifest, cap, projectManifest, filesToCopy, warnings } = plan;
|
|
42
|
+
// Emit any warnings from pre-flight
|
|
43
|
+
for (const w of warnings) {
|
|
44
|
+
console.error(w);
|
|
45
|
+
}
|
|
46
|
+
// --- Dry run: print manifest and exit ---
|
|
47
|
+
if (options.dryRun) {
|
|
48
|
+
if (options.json) {
|
|
49
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log((0, execute_1.formatManifest)(manifest));
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// --- Phase 2: Execute from manifest ---
|
|
57
|
+
const result = (0, execute_1.executeManifest)(manifest, { install: options.install });
|
|
58
|
+
// Save audit log
|
|
59
|
+
try {
|
|
60
|
+
(0, execute_1.saveManifestLog)(manifest);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Non-fatal — log dir might not be writable
|
|
64
|
+
}
|
|
65
|
+
if (!result.success) {
|
|
66
|
+
console.error(`\nWarning: ${result.failed.length} task(s) failed during execution.`);
|
|
67
|
+
}
|
|
68
|
+
// --- Output ---
|
|
69
|
+
const fileEntries = filesToCopy.map(f => ({ path: f }));
|
|
70
|
+
const skillEntries = [];
|
|
71
|
+
if (cap.skills) {
|
|
72
|
+
for (const s of cap.skills) {
|
|
73
|
+
skillEntries.push({ path: `skills/${s}` });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const hasEnv = cap.env && cap.env.length > 0;
|
|
77
|
+
const nextSteps = [];
|
|
78
|
+
if (hasEnv) {
|
|
79
|
+
for (const e of cap.env) {
|
|
80
|
+
const example = e.default ? ` (e.g. ${(0, interpolate_1.interpolate)(e.default, { projectName: projectManifest.name })})` : '';
|
|
81
|
+
nextSteps.push({ text: `Set ${e.name} in .env${example}` });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (cap.scripts) {
|
|
85
|
+
const scriptNames = Object.keys(cap.scripts);
|
|
86
|
+
if (scriptNames.length > 0) {
|
|
87
|
+
const first = scriptNames[0];
|
|
88
|
+
const desc = cap.scripts[first].description;
|
|
89
|
+
nextSteps.push({ text: `Run \`npm run ${first}\` — ${desc}` });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
nextSteps.push({ text: 'Run `npm run dev` to start developing' });
|
|
93
|
+
if (options.json) {
|
|
94
|
+
console.log(JSON.stringify({
|
|
95
|
+
capability: capabilityName,
|
|
96
|
+
project: projectManifest.name,
|
|
97
|
+
files: fileEntries,
|
|
98
|
+
skills: skillEntries,
|
|
99
|
+
nextSteps,
|
|
100
|
+
manifest: {
|
|
101
|
+
tasks: manifest.tasks.length,
|
|
102
|
+
completed: result.completed.length,
|
|
103
|
+
failed: result.failed.length,
|
|
104
|
+
skipped: result.skipped.length,
|
|
105
|
+
},
|
|
106
|
+
}, null, 2));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Determine which files were skipped due to conflicts
|
|
110
|
+
const conflictCheck = manifest.preflight.checks.find(c => c.name === 'no-file-conflicts');
|
|
111
|
+
const skippedFiles = new Set();
|
|
112
|
+
const copyTask = manifest.tasks.find(t => t.op === 'copy-files');
|
|
113
|
+
if (copyTask?.data) {
|
|
114
|
+
const conflicts = copyTask.data.conflicts || [];
|
|
115
|
+
for (const f of conflicts)
|
|
116
|
+
skippedFiles.add(f);
|
|
117
|
+
}
|
|
118
|
+
// Human-readable output
|
|
119
|
+
console.log(`\n${'─'.repeat(40)}`);
|
|
120
|
+
console.log(`Added capability "${capabilityName}" to ${projectManifest.name}\n`);
|
|
121
|
+
if (fileEntries.length > 0) {
|
|
122
|
+
console.log('Files:');
|
|
123
|
+
for (const f of fileEntries) {
|
|
124
|
+
if (skippedFiles.has(f.path)) {
|
|
125
|
+
console.log(` ◐ ${f.path} (skipped — already exists)`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(` ✓ ${f.path}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (skillEntries.length > 0) {
|
|
133
|
+
console.log('\nSkills available:');
|
|
134
|
+
for (const s of skillEntries) {
|
|
135
|
+
console.log(` ${s.path}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Show capability-specific post-install guidance if available
|
|
139
|
+
if (cap.postInstall) {
|
|
140
|
+
const pi = cap.postInstall;
|
|
141
|
+
const vars = { projectName: projectManifest.name, capabilityName };
|
|
142
|
+
if (pi.prerequisites && pi.prerequisites.length > 0) {
|
|
143
|
+
console.log('\nPrerequisites:');
|
|
144
|
+
for (const prereq of pi.prerequisites) {
|
|
145
|
+
console.log(` - ${(0, interpolate_1.interpolate)(prereq, vars)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (pi.steps && pi.steps.length > 0) {
|
|
149
|
+
console.log('\nSetup steps:');
|
|
150
|
+
for (let i = 0; i < pi.steps.length; i++) {
|
|
151
|
+
console.log(` ${i + 1}. ${(0, interpolate_1.interpolate)(pi.steps[i], vars)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (pi.verify) {
|
|
155
|
+
console.log('\nVerify with:');
|
|
156
|
+
console.log(` ${(0, interpolate_1.interpolate)(pi.verify, vars)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.log('\nNext steps:');
|
|
161
|
+
for (let i = 0; i < nextSteps.length; i++) {
|
|
162
|
+
console.log(` ${i + 1}. ${nextSteps[i].text}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (hasEnv) {
|
|
166
|
+
console.log(`\nEnvironment variables:`);
|
|
167
|
+
for (const e of cap.env) {
|
|
168
|
+
const required = e.required ? ` (required at ${e.required})` : '';
|
|
169
|
+
console.log(` ${e.name}${required}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
console.log(`\nRun \`boop status\` to check configuration.`);
|
|
173
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runAdopt = runAdopt;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const config_1 = require("../lib/config");
|
|
41
|
+
const capability_1 = require("../lib/capability");
|
|
42
|
+
const project_1 = require("../lib/project");
|
|
43
|
+
const safe_io_1 = require("../lib/safe-io");
|
|
44
|
+
function runAdopt(capabilityName, options) {
|
|
45
|
+
// 0. Validate capability argument
|
|
46
|
+
if (!capabilityName) {
|
|
47
|
+
console.error(`\x1b[31mERR\x1b[0m Missing capability argument.\n`);
|
|
48
|
+
console.error('Usage: boop adopt <capability>');
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const targetDir = process.cwd();
|
|
53
|
+
const shouldInstall = options.install !== false;
|
|
54
|
+
// 1. Validate we're in a boop project
|
|
55
|
+
let manifest;
|
|
56
|
+
try {
|
|
57
|
+
manifest = (0, project_1.readProjectManifest)(targetDir);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err instanceof SyntaxError) {
|
|
61
|
+
console.error('Error: .boop/config.json is corrupt (invalid JSON). Delete it and run `boop init` to recreate.');
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
if (!manifest) {
|
|
68
|
+
console.error('Not a boop project (no .boop/config.json found). Run `boop init` to create one.');
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// 2. Validate capability is installed
|
|
73
|
+
if (!manifest.capabilities.includes(capabilityName)) {
|
|
74
|
+
console.error(`Error: Capability "${capabilityName}" is not installed. Run \`boop add ${capabilityName}\` first.`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// 3. Resolve capability and verify it has a package
|
|
79
|
+
const cap = (0, capability_1.getCapability)(capabilityName);
|
|
80
|
+
if (!cap) {
|
|
81
|
+
console.error(`Error: Capability "${capabilityName}" not found.`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!cap.package) {
|
|
86
|
+
console.error(`Error: Capability "${capabilityName}" has no package to adopt.`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const pkgName = cap.package.replace(/^@boopkit\//, '');
|
|
91
|
+
const pkgDir = path.join((0, config_1.getBoopRoot)(), 'packages', pkgName);
|
|
92
|
+
// Check already adopted
|
|
93
|
+
if (manifest.adopted && manifest.adopted.includes(pkgName)) {
|
|
94
|
+
console.error(`Error: Package "${cap.package}" is already adopted.`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// 4. Warn if other installed packages depend on the package being adopted
|
|
99
|
+
warnDependentPackages(targetDir, cap.package, pkgName);
|
|
100
|
+
// 5. Copy package src/ → src/adopted/<pkgName>/
|
|
101
|
+
const srcDir = path.join(pkgDir, 'src');
|
|
102
|
+
const adoptedSrcDir = path.join(targetDir, 'src', 'adopted', pkgName);
|
|
103
|
+
const copiedSrc = [];
|
|
104
|
+
if (fs.existsSync(srcDir)) {
|
|
105
|
+
(0, safe_io_1.safeCopyDir)(srcDir, adoptedSrcDir, { onConflict: 'skip' });
|
|
106
|
+
copiedSrc.push(...listFilesRecursive(adoptedSrcDir).map(f => path.relative(targetDir, f)));
|
|
107
|
+
}
|
|
108
|
+
// 5. Copy package styles/ → styles/adopted/<pkgName>/
|
|
109
|
+
const stylesDir = path.join(pkgDir, 'styles');
|
|
110
|
+
const adoptedStylesDir = path.join(targetDir, 'styles', 'adopted', pkgName);
|
|
111
|
+
const copiedStyles = [];
|
|
112
|
+
if (fs.existsSync(stylesDir)) {
|
|
113
|
+
(0, safe_io_1.safeCopyDir)(stylesDir, adoptedStylesDir, { onConflict: 'skip' });
|
|
114
|
+
copiedStyles.push(...listFilesRecursive(adoptedStylesDir).map(f => path.relative(targetDir, f)));
|
|
115
|
+
}
|
|
116
|
+
// 6. Rewrite .ts imports
|
|
117
|
+
rewriteTsImports(targetDir, cap.package, pkgName);
|
|
118
|
+
// 7. Rewrite .css imports
|
|
119
|
+
rewriteCssImports(targetDir, cap.package, pkgName);
|
|
120
|
+
// 8. Remove package dependency from package.json
|
|
121
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
122
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
123
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
124
|
+
if (pkg.dependencies && pkg.dependencies[cap.package]) {
|
|
125
|
+
delete pkg.dependencies[cap.package];
|
|
126
|
+
(0, safe_io_1.createBackup)(pkgJsonPath);
|
|
127
|
+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// 9. Update manifest
|
|
131
|
+
if (!manifest.adopted)
|
|
132
|
+
manifest.adopted = [];
|
|
133
|
+
manifest.adopted.push(pkgName);
|
|
134
|
+
(0, project_1.writeProjectManifest)(targetDir, manifest);
|
|
135
|
+
// 10. Run npm install
|
|
136
|
+
if (shouldInstall) {
|
|
137
|
+
try {
|
|
138
|
+
console.log('\nInstalling dependencies...');
|
|
139
|
+
(0, child_process_1.execSync)('npm install', { cwd: targetDir, stdio: 'inherit' });
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
console.error('Warning: npm install failed. You can run it manually.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 11. Output
|
|
146
|
+
if (options.json) {
|
|
147
|
+
console.log(JSON.stringify({
|
|
148
|
+
capability: capabilityName,
|
|
149
|
+
package: cap.package,
|
|
150
|
+
adopted: {
|
|
151
|
+
src: copiedSrc,
|
|
152
|
+
styles: copiedStyles,
|
|
153
|
+
},
|
|
154
|
+
}, null, 2));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
console.log(`\n${'─'.repeat(40)}`);
|
|
158
|
+
console.log(`Adopted "${cap.package}" into ${manifest.name}\n`);
|
|
159
|
+
if (copiedSrc.length > 0) {
|
|
160
|
+
console.log('Source files copied:');
|
|
161
|
+
for (const f of copiedSrc) {
|
|
162
|
+
console.log(` ${f}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (copiedStyles.length > 0) {
|
|
166
|
+
console.log('\nStyle files copied:');
|
|
167
|
+
for (const f of copiedStyles) {
|
|
168
|
+
console.log(` ${f}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
console.log('\nImports rewritten from package to local paths.');
|
|
172
|
+
console.log(`Package "${cap.package}" removed from dependencies.`);
|
|
173
|
+
console.log(`\nYou now own this code — edit freely in src/adopted/${pkgName}/`);
|
|
174
|
+
}
|
|
175
|
+
function listFilesRecursive(dir) {
|
|
176
|
+
const results = [];
|
|
177
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
178
|
+
const fullPath = path.join(dir, entry.name);
|
|
179
|
+
if (entry.isDirectory()) {
|
|
180
|
+
results.push(...listFilesRecursive(fullPath));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
results.push(fullPath);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
188
|
+
function rewriteTsImports(targetDir, packageName, pkgName) {
|
|
189
|
+
const srcDir = path.join(targetDir, 'src');
|
|
190
|
+
if (!fs.existsSync(srcDir))
|
|
191
|
+
return;
|
|
192
|
+
const tsFiles = findFilesWithExt(srcDir, ['.ts', '.tsx']);
|
|
193
|
+
for (const file of tsFiles) {
|
|
194
|
+
let content = fs.readFileSync(file, 'utf-8');
|
|
195
|
+
if (!content.includes(packageName))
|
|
196
|
+
continue;
|
|
197
|
+
const fileDir = path.dirname(file);
|
|
198
|
+
const adoptedDir = path.join(srcDir, 'adopted', pkgName);
|
|
199
|
+
let relativePath = path.relative(fileDir, adoptedDir);
|
|
200
|
+
if (!relativePath.startsWith('.')) {
|
|
201
|
+
relativePath = './' + relativePath;
|
|
202
|
+
}
|
|
203
|
+
// Rewrite bare package imports: from '@boopkit/cap-foo' → from './adopted/cap-foo'
|
|
204
|
+
content = content.replace(new RegExp(`from\\s+['"]${escapeRegex(packageName)}['"]`, 'g'), `from '${relativePath}'`);
|
|
205
|
+
// Rewrite deep imports: from '@boopkit/cap-foo/something' → from './adopted/cap-foo/something'
|
|
206
|
+
content = content.replace(new RegExp(`from\\s+['"]${escapeRegex(packageName)}/([^'"]+)['"]`, 'g'), `from '${relativePath}/$1'`);
|
|
207
|
+
(0, safe_io_1.createBackup)(file);
|
|
208
|
+
fs.writeFileSync(file, content, 'utf-8');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function rewriteCssImports(targetDir, packageName, pkgName) {
|
|
212
|
+
const stylesDir = path.join(targetDir, 'styles');
|
|
213
|
+
if (!fs.existsSync(stylesDir))
|
|
214
|
+
return;
|
|
215
|
+
const cssFiles = findFilesWithExt(stylesDir, ['.css']);
|
|
216
|
+
for (const file of cssFiles) {
|
|
217
|
+
let content = fs.readFileSync(file, 'utf-8');
|
|
218
|
+
if (!content.includes(packageName))
|
|
219
|
+
continue;
|
|
220
|
+
const fileDir = path.dirname(file);
|
|
221
|
+
const adoptedDir = path.join(stylesDir, 'adopted', pkgName);
|
|
222
|
+
let relativePath = path.relative(fileDir, adoptedDir);
|
|
223
|
+
if (!relativePath.startsWith('.')) {
|
|
224
|
+
relativePath = './' + relativePath;
|
|
225
|
+
}
|
|
226
|
+
// Rewrite @import "@boopkit/cap-foo/styles/x.css" → @import "./adopted/cap-foo/x.css"
|
|
227
|
+
content = content.replace(new RegExp(`@import\\s+["']${escapeRegex(packageName)}/styles/([^"']+)["']`, 'g'), `@import "${relativePath}/$1"`);
|
|
228
|
+
// Rewrite any remaining package references in @import
|
|
229
|
+
content = content.replace(new RegExp(`@import\\s+["']${escapeRegex(packageName)}/([^"']+)["']`, 'g'), `@import "${relativePath}/$1"`);
|
|
230
|
+
(0, safe_io_1.createBackup)(file);
|
|
231
|
+
fs.writeFileSync(file, content, 'utf-8');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function findFilesWithExt(dir, extensions) {
|
|
235
|
+
const results = [];
|
|
236
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
237
|
+
const fullPath = path.join(dir, entry.name);
|
|
238
|
+
if (entry.isDirectory()) {
|
|
239
|
+
results.push(...findFilesWithExt(fullPath, extensions));
|
|
240
|
+
}
|
|
241
|
+
else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
242
|
+
results.push(fullPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
function escapeRegex(str) {
|
|
248
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
249
|
+
}
|
|
250
|
+
function warnDependentPackages(targetDir, adoptedPackage, adoptedPkgName) {
|
|
251
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
252
|
+
if (!fs.existsSync(pkgJsonPath))
|
|
253
|
+
return;
|
|
254
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
255
|
+
const deps = pkg.dependencies || {};
|
|
256
|
+
const dependents = [];
|
|
257
|
+
for (const [depName, depValue] of Object.entries(deps)) {
|
|
258
|
+
if (!depName.startsWith('@boopkit/') || depName === adoptedPackage)
|
|
259
|
+
continue;
|
|
260
|
+
// Resolve the package dir from the file: link
|
|
261
|
+
const depStr = depValue;
|
|
262
|
+
let pkgDir;
|
|
263
|
+
if (depStr.startsWith('file:')) {
|
|
264
|
+
pkgDir = path.resolve(targetDir, depStr.slice(5));
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const shortName = depName.replace(/^@boopkit\//, '');
|
|
268
|
+
pkgDir = path.join((0, config_1.getBoopRoot)(), 'packages', shortName);
|
|
269
|
+
}
|
|
270
|
+
const depPkgJsonPath = path.join(pkgDir, 'package.json');
|
|
271
|
+
if (!fs.existsSync(depPkgJsonPath))
|
|
272
|
+
continue;
|
|
273
|
+
const depPkg = JSON.parse(fs.readFileSync(depPkgJsonPath, 'utf-8'));
|
|
274
|
+
const allDeps = {
|
|
275
|
+
...depPkg.dependencies,
|
|
276
|
+
...depPkg.peerDependencies,
|
|
277
|
+
};
|
|
278
|
+
if (allDeps[adoptedPackage]) {
|
|
279
|
+
dependents.push(depName);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (dependents.length > 0) {
|
|
283
|
+
console.error(`Warning: The following installed packages depend on ${adoptedPackage}: ${dependents.join(', ')}. ` +
|
|
284
|
+
`They will continue using the node_modules copy if available.`);
|
|
285
|
+
}
|
|
286
|
+
}
|