@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
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pre-flight planning for `boop add`.
|
|
4
|
+
*
|
|
5
|
+
* Reads all inputs, validates everything, and produces a manifest of tasks
|
|
6
|
+
* to execute — without writing a single file.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.PreflightError = void 0;
|
|
43
|
+
exports.planAdd = planAdd;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const config_1 = require("./config");
|
|
47
|
+
const capability_1 = require("./capability");
|
|
48
|
+
const project_1 = require("./project");
|
|
49
|
+
class PreflightError extends Error {
|
|
50
|
+
constructor(message, checks) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = 'PreflightError';
|
|
53
|
+
this.checks = checks;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.PreflightError = PreflightError;
|
|
57
|
+
/**
|
|
58
|
+
* Plan a `boop add` operation. Reads filesystem, validates everything,
|
|
59
|
+
* and produces a manifest. Writes nothing.
|
|
60
|
+
*
|
|
61
|
+
* Throws PreflightError if any check fails.
|
|
62
|
+
*/
|
|
63
|
+
function planAdd(targetDir, capabilityName) {
|
|
64
|
+
const checks = [];
|
|
65
|
+
const tasks = [];
|
|
66
|
+
const warnings = [];
|
|
67
|
+
const boopRoot = (0, config_1.getBoopRoot)();
|
|
68
|
+
// --- Pre-flight checks ---
|
|
69
|
+
// 1. Validate project
|
|
70
|
+
const projectManifest = (0, project_1.readProjectManifest)(targetDir);
|
|
71
|
+
if (!projectManifest) {
|
|
72
|
+
checks.push({ name: 'project-exists', passed: false, message: 'Not a boop project (no .boop/config.json)' });
|
|
73
|
+
throw new PreflightError('Pre-flight failed', checks);
|
|
74
|
+
}
|
|
75
|
+
checks.push({ name: 'project-exists', passed: true });
|
|
76
|
+
// 2. Resolve capability
|
|
77
|
+
const cap = (0, capability_1.getCapability)(capabilityName);
|
|
78
|
+
if (!cap) {
|
|
79
|
+
checks.push({ name: 'capability-exists', passed: false, message: `Capability "${capabilityName}" not found` });
|
|
80
|
+
throw new PreflightError('Pre-flight failed', checks);
|
|
81
|
+
}
|
|
82
|
+
checks.push({ name: 'capability-exists', passed: true });
|
|
83
|
+
// 3. Not already installed
|
|
84
|
+
if (projectManifest.capabilities.includes(capabilityName)) {
|
|
85
|
+
checks.push({ name: 'not-already-installed', passed: false, message: `Capability "${capabilityName}" is already installed` });
|
|
86
|
+
throw new PreflightError('Pre-flight failed', checks);
|
|
87
|
+
}
|
|
88
|
+
checks.push({ name: 'not-already-installed', passed: true });
|
|
89
|
+
// 4. Verify required capabilities are installed
|
|
90
|
+
if (cap.requires && cap.requires.length > 0) {
|
|
91
|
+
const missing = cap.requires.filter(r => !projectManifest.capabilities.includes(r));
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
checks.push({
|
|
94
|
+
name: 'requires-satisfied',
|
|
95
|
+
passed: false,
|
|
96
|
+
message: `Missing required capabilities: ${missing.join(', ')}. Install them first with: ${missing.map(m => `boop add ${m}`).join(', then ')}`,
|
|
97
|
+
});
|
|
98
|
+
throw new PreflightError('Pre-flight failed', checks);
|
|
99
|
+
}
|
|
100
|
+
checks.push({ name: 'requires-satisfied', passed: true });
|
|
101
|
+
}
|
|
102
|
+
// 5. Enumerate files that would be copied — check for conflicts
|
|
103
|
+
// Resolve from package (scaffold/ and skills/) or source tree (files/ and skills/)
|
|
104
|
+
const pkgDir = (0, capability_1.getPackageDir)(capabilityName);
|
|
105
|
+
const pkgJsonPath = pkgDir ? path.join(pkgDir, 'package.json') : null;
|
|
106
|
+
const pkgJson = pkgJsonPath && fs.existsSync(pkgJsonPath)
|
|
107
|
+
? JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'))
|
|
108
|
+
: null;
|
|
109
|
+
const boopkit = pkgJson?.boopkit;
|
|
110
|
+
const capDir = boopkit
|
|
111
|
+
? pkgDir
|
|
112
|
+
: path.join(boopRoot, 'capabilities', capabilityName);
|
|
113
|
+
const filesToCopy = [];
|
|
114
|
+
const fileConflicts = [];
|
|
115
|
+
// Skills files
|
|
116
|
+
const skillsDir = boopkit?.skillsDir
|
|
117
|
+
? path.join(capDir, boopkit.skillsDir)
|
|
118
|
+
: path.join(capDir, 'skills');
|
|
119
|
+
if (fs.existsSync(skillsDir)) {
|
|
120
|
+
for (const entry of fs.readdirSync(skillsDir)) {
|
|
121
|
+
const destPath = path.join(targetDir, 'skills', entry);
|
|
122
|
+
const relPath = path.join('skills', entry);
|
|
123
|
+
filesToCopy.push(relPath);
|
|
124
|
+
if (fs.existsSync(destPath)) {
|
|
125
|
+
fileConflicts.push(relPath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Capability files from scaffold/ (package) or files/ (source tree)
|
|
130
|
+
if (cap.files) {
|
|
131
|
+
const filesDir = boopkit?.scaffoldDir
|
|
132
|
+
? path.join(capDir, boopkit.scaffoldDir)
|
|
133
|
+
: path.join(capDir, 'files');
|
|
134
|
+
if (fs.existsSync(filesDir)) {
|
|
135
|
+
enumerateDir(filesDir, targetDir, filesToCopy, fileConflicts);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// File conflicts are warnings (skip behavior), not failures
|
|
139
|
+
if (fileConflicts.length > 0) {
|
|
140
|
+
checks.push({
|
|
141
|
+
name: 'no-file-conflicts',
|
|
142
|
+
passed: true,
|
|
143
|
+
message: `${fileConflicts.length} existing file(s) will be skipped: ${fileConflicts.join(', ')}`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
checks.push({ name: 'no-file-conflicts', passed: true });
|
|
148
|
+
}
|
|
149
|
+
// 5. Validate hook markers exist in app.ts or server.ts (if capability has hooks)
|
|
150
|
+
if (cap.hooks && cap.hooks.length > 0) {
|
|
151
|
+
const appPath = path.join(targetDir, 'src', 'app.ts');
|
|
152
|
+
const serverPath = path.join(targetDir, 'src', 'server.ts');
|
|
153
|
+
// New structure uses app.ts; legacy uses server.ts
|
|
154
|
+
const hookFile = fs.existsSync(appPath) ? appPath : serverPath;
|
|
155
|
+
const hookFileName = fs.existsSync(appPath) ? 'app.ts' : 'server.ts';
|
|
156
|
+
if (fs.existsSync(hookFile)) {
|
|
157
|
+
const content = fs.readFileSync(hookFile, 'utf-8');
|
|
158
|
+
const phases = new Set(cap.hooks.map(h => h.phase));
|
|
159
|
+
const missingMarkers = [];
|
|
160
|
+
for (const phase of phases) {
|
|
161
|
+
const marker = `// @boopkit:${phase}`;
|
|
162
|
+
if (!content.includes(marker)) {
|
|
163
|
+
missingMarkers.push(phase);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (missingMarkers.length > 0) {
|
|
167
|
+
checks.push({
|
|
168
|
+
name: 'has-hook-markers',
|
|
169
|
+
passed: false,
|
|
170
|
+
message: `Missing markers in ${hookFileName}: ${missingMarkers.join(', ')}`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
checks.push({ name: 'has-hook-markers', passed: true });
|
|
175
|
+
}
|
|
176
|
+
// Check if already wired (idempotency)
|
|
177
|
+
const capAnnotation = `@boopkit/cap-${capabilityName}`;
|
|
178
|
+
if (content.includes(capAnnotation)) {
|
|
179
|
+
checks.push({
|
|
180
|
+
name: 'hooks-not-duplicate',
|
|
181
|
+
passed: true,
|
|
182
|
+
message: `Hooks for ${capabilityName} already present in ${hookFileName} — will skip injection`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
checks.push({
|
|
188
|
+
name: 'has-hook-markers',
|
|
189
|
+
passed: false,
|
|
190
|
+
message: 'Neither app.ts nor server.ts found — hooks cannot be injected',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// 6. Check for script conflicts
|
|
195
|
+
if (cap.scripts) {
|
|
196
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
197
|
+
if (fs.existsSync(pkgPath)) {
|
|
198
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
199
|
+
const existingScripts = pkg.scripts || {};
|
|
200
|
+
const conflicts = [];
|
|
201
|
+
for (const scriptName of Object.keys(cap.scripts)) {
|
|
202
|
+
if (scriptName in existingScripts) {
|
|
203
|
+
conflicts.push(scriptName);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (conflicts.length > 0) {
|
|
207
|
+
checks.push({
|
|
208
|
+
name: 'script-conflicts',
|
|
209
|
+
passed: true,
|
|
210
|
+
message: `Existing scripts will be preserved: ${conflicts.join(', ')}`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
checks.push({ name: 'script-conflicts', passed: true });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// --- Abort if any check failed ---
|
|
219
|
+
const failedChecks = checks.filter(c => !c.passed);
|
|
220
|
+
if (failedChecks.length > 0) {
|
|
221
|
+
throw new PreflightError(`Pre-flight failed: ${failedChecks.map(c => c.message).join('; ')}`, checks);
|
|
222
|
+
}
|
|
223
|
+
// --- Build task manifest ---
|
|
224
|
+
// Task: copy files
|
|
225
|
+
if (filesToCopy.length > 0) {
|
|
226
|
+
tasks.push({
|
|
227
|
+
op: 'copy-files',
|
|
228
|
+
source: capDir,
|
|
229
|
+
target: targetDir,
|
|
230
|
+
detail: `Copy ${filesToCopy.length} file(s) from ${capabilityName}`,
|
|
231
|
+
status: 'pending',
|
|
232
|
+
data: { files: filesToCopy, conflicts: fileConflicts },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// Task: merge dependencies
|
|
236
|
+
if (cap.dependencies?.length || cap.devDependencies?.length) {
|
|
237
|
+
tasks.push({
|
|
238
|
+
op: 'merge-deps',
|
|
239
|
+
source: capabilityName,
|
|
240
|
+
target: path.join(targetDir, 'package.json'),
|
|
241
|
+
detail: `Merge ${[...(cap.dependencies || []), ...(cap.devDependencies || [])].join(', ')}`,
|
|
242
|
+
status: 'pending',
|
|
243
|
+
data: { dependencies: cap.dependencies, devDependencies: cap.devDependencies },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// Task: merge scripts
|
|
247
|
+
if (cap.scripts && Object.keys(cap.scripts).length > 0) {
|
|
248
|
+
tasks.push({
|
|
249
|
+
op: 'merge-scripts',
|
|
250
|
+
source: capabilityName,
|
|
251
|
+
target: path.join(targetDir, 'package.json'),
|
|
252
|
+
detail: `Add scripts: ${Object.keys(cap.scripts).join(', ')}`,
|
|
253
|
+
status: 'pending',
|
|
254
|
+
data: { scripts: cap.scripts },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// Task: link package
|
|
258
|
+
if (cap.package) {
|
|
259
|
+
const pkgName = cap.package.replace(/^@boopkit\//, '');
|
|
260
|
+
const isAdopted = projectManifest.adopted?.includes(pkgName);
|
|
261
|
+
if (isAdopted) {
|
|
262
|
+
warnings.push(`Skipping package link for ${cap.package} — package is adopted (source already in project)`);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
tasks.push({
|
|
266
|
+
op: 'link-package',
|
|
267
|
+
source: path.join(boopRoot, 'packages', pkgName),
|
|
268
|
+
target: path.join(targetDir, 'package.json'),
|
|
269
|
+
detail: `Add ${cap.package} as semver dependency`,
|
|
270
|
+
status: 'pending',
|
|
271
|
+
data: { packageName: cap.package },
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Task: set env vars
|
|
276
|
+
const hasEnv = cap.env && cap.env.length > 0;
|
|
277
|
+
if (hasEnv) {
|
|
278
|
+
tasks.push({
|
|
279
|
+
op: 'set-env',
|
|
280
|
+
source: capabilityName,
|
|
281
|
+
target: path.join(targetDir, '.env'),
|
|
282
|
+
detail: `Set env vars: ${cap.env.map(e => e.name).join(', ')}`,
|
|
283
|
+
status: 'pending',
|
|
284
|
+
data: {
|
|
285
|
+
entries: cap.env.map(e => ({
|
|
286
|
+
name: e.name,
|
|
287
|
+
description: e.description,
|
|
288
|
+
default: e.default,
|
|
289
|
+
capability: capabilityName,
|
|
290
|
+
})),
|
|
291
|
+
projectName: projectManifest.name,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
// Also need dotenv dependency
|
|
295
|
+
tasks.push({
|
|
296
|
+
op: 'merge-deps',
|
|
297
|
+
source: 'dotenv',
|
|
298
|
+
target: path.join(targetDir, 'package.json'),
|
|
299
|
+
detail: 'Add dotenv dependency (required for env vars)',
|
|
300
|
+
status: 'pending',
|
|
301
|
+
data: { dependencies: ['dotenv@^16'] },
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// Task: inject hooks
|
|
305
|
+
if (cap.hooks && cap.hooks.length > 0) {
|
|
306
|
+
const hasAppTs = fs.existsSync(path.join(targetDir, 'src', 'app.ts'));
|
|
307
|
+
tasks.push({
|
|
308
|
+
op: 'inject-hook',
|
|
309
|
+
source: capabilityName,
|
|
310
|
+
target: path.join(targetDir, 'src', hasAppTs ? 'app.ts' : 'server.ts'),
|
|
311
|
+
detail: `Inject ${cap.hooks.length} hook(s) into ${hasAppTs ? 'app.ts/server.ts' : 'server.ts'}`,
|
|
312
|
+
status: 'pending',
|
|
313
|
+
data: { hooks: cap.hooks, needsEnv: !!hasEnv },
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// Task: inject CSS
|
|
317
|
+
const cssFiles = filesToCopy.filter(f => f.startsWith('styles/') && f.endsWith('.css') && f !== 'styles/styles.css');
|
|
318
|
+
if (cssFiles.length > 0) {
|
|
319
|
+
const cssCapMap = {};
|
|
320
|
+
for (const f of cssFiles) {
|
|
321
|
+
cssCapMap[path.basename(f)] = capabilityName;
|
|
322
|
+
}
|
|
323
|
+
tasks.push({
|
|
324
|
+
op: 'inject-css',
|
|
325
|
+
source: capabilityName,
|
|
326
|
+
target: path.join(targetDir, 'styles', 'styles.css'),
|
|
327
|
+
detail: `Inject CSS imports for ${cssFiles.join(', ')}`,
|
|
328
|
+
status: 'pending',
|
|
329
|
+
data: { cssCapMap },
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// Task: regenerate skills index
|
|
333
|
+
tasks.push({
|
|
334
|
+
op: 'generate-skills-index',
|
|
335
|
+
source: capabilityName,
|
|
336
|
+
target: path.join(targetDir, 'skills', 'index.md'),
|
|
337
|
+
detail: 'Regenerate skills/index.md',
|
|
338
|
+
status: 'pending',
|
|
339
|
+
});
|
|
340
|
+
// Task: update manifest
|
|
341
|
+
tasks.push({
|
|
342
|
+
op: 'update-manifest',
|
|
343
|
+
source: capabilityName,
|
|
344
|
+
target: path.join(targetDir, '.boop', 'config.json'),
|
|
345
|
+
detail: `Add ${capabilityName} to project manifest`,
|
|
346
|
+
status: 'pending',
|
|
347
|
+
data: { hasEnv: !!hasEnv, envVars: cap.env },
|
|
348
|
+
});
|
|
349
|
+
// Task: npm install
|
|
350
|
+
tasks.push({
|
|
351
|
+
op: 'run-command',
|
|
352
|
+
target: targetDir,
|
|
353
|
+
detail: 'npm install',
|
|
354
|
+
status: 'pending',
|
|
355
|
+
data: { command: 'npm install' },
|
|
356
|
+
});
|
|
357
|
+
// Task: rebuild CSS if we injected styles and the project has tailwind:build
|
|
358
|
+
const hasInjectCss = tasks.some(t => t.op === 'inject-css');
|
|
359
|
+
if (hasInjectCss) {
|
|
360
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
361
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
362
|
+
if (pkgJson.scripts?.['tailwind:build']) {
|
|
363
|
+
tasks.push({
|
|
364
|
+
op: 'run-command',
|
|
365
|
+
target: targetDir,
|
|
366
|
+
detail: 'Rebuild CSS',
|
|
367
|
+
status: 'pending',
|
|
368
|
+
data: { command: 'npm run tailwind:build' },
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const manifest = {
|
|
373
|
+
capability: capabilityName,
|
|
374
|
+
targetDir,
|
|
375
|
+
timestamp: new Date().toISOString(),
|
|
376
|
+
preflight: { passed: true, checks },
|
|
377
|
+
tasks,
|
|
378
|
+
};
|
|
379
|
+
return { manifest, cap, projectManifest, filesToCopy, warnings };
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Recursively enumerate files in srcDir and check for conflicts in destDir.
|
|
383
|
+
* Pure read — does not write.
|
|
384
|
+
*/
|
|
385
|
+
function enumerateDir(srcDir, destBase, filesToCopy, fileConflicts, baseDir) {
|
|
386
|
+
const root = baseDir ?? srcDir;
|
|
387
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
388
|
+
for (const entry of entries) {
|
|
389
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
390
|
+
if (entry.isDirectory()) {
|
|
391
|
+
enumerateDir(srcPath, destBase, filesToCopy, fileConflicts, root);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
const rel = path.relative(root, srcPath);
|
|
395
|
+
const destPath = path.join(destBase, rel);
|
|
396
|
+
filesToCopy.push(rel);
|
|
397
|
+
if (fs.existsSync(destPath)) {
|
|
398
|
+
fileConflicts.push(rel);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ProfileMetadata } from './config';
|
|
2
|
+
/**
|
|
3
|
+
* List all profiles by scanning the profiles/ directory.
|
|
4
|
+
* Returns metadata for each profile that has a valid profile.md.
|
|
5
|
+
*/
|
|
6
|
+
export declare function listProfiles(): ProfileMetadata[];
|
|
7
|
+
/**
|
|
8
|
+
* Get a single profile's metadata by name.
|
|
9
|
+
* Returns null if the profile directory or profile.md doesn't exist.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getProfile(name: string): ProfileMetadata | null;
|
|
@@ -0,0 +1,73 @@
|
|
|
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.listProfiles = listProfiles;
|
|
37
|
+
exports.getProfile = getProfile;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const config_1 = require("./config");
|
|
41
|
+
/**
|
|
42
|
+
* List all profiles by scanning the profiles/ directory.
|
|
43
|
+
* Returns metadata for each profile that has a valid profile.md.
|
|
44
|
+
*/
|
|
45
|
+
function listProfiles() {
|
|
46
|
+
const profilesDir = path.join((0, config_1.getBoopRoot)(), 'profiles');
|
|
47
|
+
if (!fs.existsSync(profilesDir)) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const entries = fs.readdirSync(profilesDir, { withFileTypes: true });
|
|
51
|
+
const profiles = [];
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (!entry.isDirectory())
|
|
54
|
+
continue;
|
|
55
|
+
const profile = getProfile(entry.name);
|
|
56
|
+
if (profile) {
|
|
57
|
+
profiles.push(profile);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return profiles;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get a single profile's metadata by name.
|
|
64
|
+
* Returns null if the profile directory or profile.md doesn't exist.
|
|
65
|
+
*/
|
|
66
|
+
function getProfile(name) {
|
|
67
|
+
const profileMd = path.join((0, config_1.getBoopRoot)(), 'profiles', name, 'profile.md');
|
|
68
|
+
const parsed = (0, config_1.parseFrontmatter)(profileMd);
|
|
69
|
+
if (!parsed) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return parsed.data;
|
|
73
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface CapabilityConfig {
|
|
2
|
+
env?: Record<string, {
|
|
3
|
+
required?: string;
|
|
4
|
+
set: boolean;
|
|
5
|
+
}>;
|
|
6
|
+
}
|
|
7
|
+
export interface CapabilityMeta {
|
|
8
|
+
installedAt: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
fileHashes: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export interface BoopManifest {
|
|
13
|
+
name: string;
|
|
14
|
+
profile: string;
|
|
15
|
+
boopVersion: string;
|
|
16
|
+
capabilities: string[];
|
|
17
|
+
capabilityConfig?: Record<string, CapabilityConfig>;
|
|
18
|
+
capabilityMeta?: Record<string, CapabilityMeta>;
|
|
19
|
+
adopted?: string[];
|
|
20
|
+
createdAt: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read and parse the .boop/config.json manifest from a directory.
|
|
24
|
+
* Returns null if the file does not exist.
|
|
25
|
+
* Throws if the file exists but contains invalid JSON.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readProjectManifest(dir: string): BoopManifest | null;
|
|
28
|
+
/**
|
|
29
|
+
* Write a .boop/config.json manifest to a directory.
|
|
30
|
+
* Creates the .boop/ directory if it does not exist.
|
|
31
|
+
*/
|
|
32
|
+
export declare function writeProjectManifest(dir: string, manifest: BoopManifest): void;
|
|
33
|
+
/**
|
|
34
|
+
* Check if a directory contains a boop project (.boop/config.json exists).
|
|
35
|
+
*/
|
|
36
|
+
export declare function isBoopProject(dir: string): boolean;
|
|
@@ -0,0 +1,71 @@
|
|
|
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.readProjectManifest = readProjectManifest;
|
|
37
|
+
exports.writeProjectManifest = writeProjectManifest;
|
|
38
|
+
exports.isBoopProject = isBoopProject;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* Read and parse the .boop/config.json manifest from a directory.
|
|
43
|
+
* Returns null if the file does not exist.
|
|
44
|
+
* Throws if the file exists but contains invalid JSON.
|
|
45
|
+
*/
|
|
46
|
+
function readProjectManifest(dir) {
|
|
47
|
+
const manifestPath = path.join(dir, '.boop', 'config.json');
|
|
48
|
+
if (!fs.existsSync(manifestPath)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
52
|
+
return JSON.parse(content);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Write a .boop/config.json manifest to a directory.
|
|
56
|
+
* Creates the .boop/ directory if it does not exist.
|
|
57
|
+
*/
|
|
58
|
+
function writeProjectManifest(dir, manifest) {
|
|
59
|
+
const boopDir = path.join(dir, '.boop');
|
|
60
|
+
if (!fs.existsSync(boopDir)) {
|
|
61
|
+
fs.mkdirSync(boopDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
const manifestPath = path.join(boopDir, 'config.json');
|
|
64
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check if a directory contains a boop project (.boop/config.json exists).
|
|
68
|
+
*/
|
|
69
|
+
function isBoopProject(dir) {
|
|
70
|
+
return fs.existsSync(path.join(dir, '.boop', 'config.json'));
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|