@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,40 @@
|
|
|
1
|
+
import { BoopManifest } from '../lib/project';
|
|
2
|
+
export interface CapabilityStatusEnvVar {
|
|
3
|
+
required?: string;
|
|
4
|
+
set: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface CapabilityStatus {
|
|
7
|
+
status: 'configured' | 'setup' | 'pending';
|
|
8
|
+
type?: string;
|
|
9
|
+
env?: Record<string, CapabilityStatusEnvVar>;
|
|
10
|
+
completedSteps?: string[];
|
|
11
|
+
nextSteps?: string[];
|
|
12
|
+
optionalSteps?: string[];
|
|
13
|
+
reachable?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface StatusData {
|
|
16
|
+
name: string;
|
|
17
|
+
profile: string;
|
|
18
|
+
capabilities: Record<string, CapabilityStatus>;
|
|
19
|
+
}
|
|
20
|
+
export declare function formatStatus(manifest: BoopManifest): string;
|
|
21
|
+
/**
|
|
22
|
+
* Compute live status for each capability by cross-referencing
|
|
23
|
+
* .boop/config.json env declarations with actual .env values.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getStatusData(manifest: BoopManifest, dir: string): StatusData;
|
|
26
|
+
export { parseDbHostPort, checkTcpReachable } from '../lib/db-status';
|
|
27
|
+
/**
|
|
28
|
+
* Enrich capabilities that have setupChecks with live-detected status.
|
|
29
|
+
* Mutates data.capabilities in place.
|
|
30
|
+
*/
|
|
31
|
+
export declare function enrichWithSetupChecks(data: StatusData, envValues: Map<string, string>, projectDir: string, projectName: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Format StatusData into a human-readable display string.
|
|
34
|
+
* Capabilities are grouped by type (e.g. application, development).
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatEnhancedStatus(data: StatusData): string;
|
|
37
|
+
export interface StatusOptions {
|
|
38
|
+
json?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function runStatus(options?: StatusOptions): Promise<void>;
|
|
@@ -0,0 +1,303 @@
|
|
|
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.checkTcpReachable = exports.parseDbHostPort = void 0;
|
|
37
|
+
exports.formatStatus = formatStatus;
|
|
38
|
+
exports.getStatusData = getStatusData;
|
|
39
|
+
exports.enrichWithSetupChecks = enrichWithSetupChecks;
|
|
40
|
+
exports.formatEnhancedStatus = formatEnhancedStatus;
|
|
41
|
+
exports.runStatus = runStatus;
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const project_1 = require("../lib/project");
|
|
44
|
+
const env_1 = require("../lib/env");
|
|
45
|
+
const capability_1 = require("../lib/capability");
|
|
46
|
+
const interpolate_1 = require("../lib/interpolate");
|
|
47
|
+
const db_status_1 = require("../lib/db-status");
|
|
48
|
+
function formatStatus(manifest) {
|
|
49
|
+
const caps = manifest.capabilities.length > 0
|
|
50
|
+
? manifest.capabilities.join(', ')
|
|
51
|
+
: 'none';
|
|
52
|
+
const created = manifest.createdAt.split('T')[0];
|
|
53
|
+
return [
|
|
54
|
+
`Project: ${manifest.name}`,
|
|
55
|
+
`Profile: ${manifest.profile}`,
|
|
56
|
+
`Created: ${created}`,
|
|
57
|
+
`Boop Version: ${manifest.boopVersion}`,
|
|
58
|
+
`Capabilities: ${caps}`,
|
|
59
|
+
].join('\n');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Compute live status for each capability by cross-referencing
|
|
63
|
+
* .boop/config.json env declarations with actual .env values.
|
|
64
|
+
*/
|
|
65
|
+
function getStatusData(manifest, dir) {
|
|
66
|
+
const envPath = path.join(dir, '.env');
|
|
67
|
+
const setKeys = (0, env_1.readEnvKeys)(envPath);
|
|
68
|
+
const capabilities = {};
|
|
69
|
+
for (const capName of manifest.capabilities) {
|
|
70
|
+
const capConfig = manifest.capabilityConfig?.[capName];
|
|
71
|
+
const capMeta = (0, capability_1.getCapability)(capName);
|
|
72
|
+
const capType = capMeta?.type;
|
|
73
|
+
// Resolve env declarations: prefer manifest capabilityConfig, fall back to capability definition
|
|
74
|
+
let envEntries;
|
|
75
|
+
if (capConfig?.env && Object.keys(capConfig.env).length > 0) {
|
|
76
|
+
envEntries = capConfig.env;
|
|
77
|
+
// Merge defaults from capability metadata — manifest doesn't persist them
|
|
78
|
+
if (capMeta?.env) {
|
|
79
|
+
for (const e of capMeta.env) {
|
|
80
|
+
if (envEntries[e.name] && e.default != null && envEntries[e.name].default == null) {
|
|
81
|
+
envEntries[e.name].default = e.default;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (capMeta?.env && capMeta.env.length > 0) {
|
|
87
|
+
// capabilityConfig missing (e.g. from boop init) — synthesize from capability metadata
|
|
88
|
+
envEntries = {};
|
|
89
|
+
for (const e of capMeta.env) {
|
|
90
|
+
envEntries[e.name] = { required: e.required, default: e.default };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!envEntries) {
|
|
94
|
+
capabilities[capName] = { status: 'configured', type: capType };
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const envStatus = {};
|
|
98
|
+
const nextSteps = [];
|
|
99
|
+
const optionalSteps = [];
|
|
100
|
+
let allRequiredSet = true;
|
|
101
|
+
for (const [varName, decl] of Object.entries(envEntries)) {
|
|
102
|
+
const hasLiteralDefault = decl.default != null && !decl.default.includes('${');
|
|
103
|
+
const isSet = setKeys.has(varName) || hasLiteralDefault;
|
|
104
|
+
envStatus[varName] = { required: decl.required, set: isSet };
|
|
105
|
+
if (!isSet) {
|
|
106
|
+
const isRequired = decl.required && decl.required !== 'false';
|
|
107
|
+
if (isRequired) {
|
|
108
|
+
allRequiredSet = false;
|
|
109
|
+
// Look for a setupCheck hint for this env var
|
|
110
|
+
const envCheck = capMeta?.postInstall?.setupChecks?.find(c => c.type === 'env-set');
|
|
111
|
+
const hint = envCheck?.hint ? ` (${envCheck.hint})` : '';
|
|
112
|
+
nextSteps.push(`Set ${varName} in .env${hint}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
optionalSteps.push(`${varName} not set (optional)`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (allRequiredSet) {
|
|
120
|
+
// Env vars are set — check if capability has setup checks or postInstall steps
|
|
121
|
+
const capDef = (0, capability_1.getCapability)(capName);
|
|
122
|
+
const hasSetupChecks = capDef?.postInstall?.setupChecks?.length;
|
|
123
|
+
const hasPostInstall = capDef?.postInstall && (capDef.postInstall.steps?.length || capDef.postInstall.verify);
|
|
124
|
+
if (hasSetupChecks || hasPostInstall) {
|
|
125
|
+
// Mark as 'setup' — dynamic checks will populate steps during enrichment
|
|
126
|
+
capabilities[capName] = { status: 'setup', type: capType, env: envStatus, optionalSteps: optionalSteps.length > 0 ? optionalSteps : undefined };
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
capabilities[capName] = { status: 'configured', type: capType, optionalSteps: optionalSteps.length > 0 ? optionalSteps : undefined };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Enrich next steps with postInstall guidance from capability definition
|
|
134
|
+
const capDef = (0, capability_1.getCapability)(capName);
|
|
135
|
+
if (capDef?.postInstall) {
|
|
136
|
+
const vars = { projectName: manifest.name, capabilityName: capName };
|
|
137
|
+
const pi = capDef.postInstall;
|
|
138
|
+
if (pi.prerequisites) {
|
|
139
|
+
for (const prereq of pi.prerequisites) {
|
|
140
|
+
nextSteps.push((0, interpolate_1.interpolate)(prereq, vars));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (pi.steps) {
|
|
144
|
+
for (const step of pi.steps) {
|
|
145
|
+
// Skip steps that duplicate the basic "Set X in .env" entries
|
|
146
|
+
const interpolated = (0, interpolate_1.interpolate)(step, vars);
|
|
147
|
+
const isDuplicate = nextSteps.some((existing) => existing.startsWith('Set ') && interpolated.includes(existing.split(' ')[1]));
|
|
148
|
+
if (!isDuplicate) {
|
|
149
|
+
nextSteps.push(interpolated);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (pi.verify) {
|
|
154
|
+
nextSteps.push(`Verify: ${(0, interpolate_1.interpolate)(pi.verify, vars)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
capabilities[capName] = { status: 'pending', type: capType, env: envStatus, nextSteps };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { name: manifest.name, profile: manifest.profile, capabilities };
|
|
161
|
+
}
|
|
162
|
+
// Re-export for backward compatibility in tests
|
|
163
|
+
var db_status_2 = require("../lib/db-status");
|
|
164
|
+
Object.defineProperty(exports, "parseDbHostPort", { enumerable: true, get: function () { return db_status_2.parseDbHostPort; } });
|
|
165
|
+
Object.defineProperty(exports, "checkTcpReachable", { enumerable: true, get: function () { return db_status_2.checkTcpReachable; } });
|
|
166
|
+
/**
|
|
167
|
+
* Enrich capabilities that have setupChecks with live-detected status.
|
|
168
|
+
* Mutates data.capabilities in place.
|
|
169
|
+
*/
|
|
170
|
+
async function enrichWithSetupChecks(data, envValues, projectDir, projectName) {
|
|
171
|
+
for (const [capName, cap] of Object.entries(data.capabilities)) {
|
|
172
|
+
if (cap.status !== 'setup')
|
|
173
|
+
continue;
|
|
174
|
+
const capDef = (0, capability_1.getCapability)(capName);
|
|
175
|
+
const checks = capDef?.postInstall?.setupChecks;
|
|
176
|
+
if (!checks?.length)
|
|
177
|
+
continue;
|
|
178
|
+
// Interpolate labels and hints
|
|
179
|
+
const vars = { projectName, capabilityName: capName };
|
|
180
|
+
const interpolatedChecks = checks.map(c => ({
|
|
181
|
+
type: c.type,
|
|
182
|
+
label: (0, interpolate_1.interpolate)(c.label, vars),
|
|
183
|
+
hint: c.hint ? (0, interpolate_1.interpolate)(c.hint, vars) : undefined,
|
|
184
|
+
}));
|
|
185
|
+
const steps = await (0, db_status_1.runSetupChecks)(interpolatedChecks, envValues, projectDir, projectName);
|
|
186
|
+
const completedSteps = steps.filter(s => s.done).map(s => s.label);
|
|
187
|
+
const nextSteps = steps.filter(s => !s.done).map(s => s.hint ? `${s.label}: ${s.hint}` : s.label);
|
|
188
|
+
if (nextSteps.length === 0) {
|
|
189
|
+
cap.status = 'configured';
|
|
190
|
+
cap.reachable = true;
|
|
191
|
+
cap.completedSteps = undefined;
|
|
192
|
+
cap.nextSteps = undefined;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
cap.completedSteps = completedSteps.length > 0 ? completedSteps : undefined;
|
|
196
|
+
cap.nextSteps = nextSteps;
|
|
197
|
+
const serviceStep = steps.find(s => s.label.includes('running') || s.label.includes('service'));
|
|
198
|
+
if (serviceStep && !serviceStep.done) {
|
|
199
|
+
cap.reachable = false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function formatCapabilityLine(capName, cap) {
|
|
205
|
+
const lines = [];
|
|
206
|
+
if (cap.status === 'configured' && cap.reachable !== false) {
|
|
207
|
+
const reachLabel = cap.reachable === true ? ' (reachable)' : '';
|
|
208
|
+
lines.push(` \x1b[32m\u2713\x1b[0m \x1b[32m${capName.padEnd(12)}\x1b[0m configured${reachLabel}`);
|
|
209
|
+
if (cap.optionalSteps) {
|
|
210
|
+
for (const step of cap.optionalSteps) {
|
|
211
|
+
lines.push(` \x1b[2m· ${step}\x1b[0m`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (cap.status === 'configured' && cap.reachable === false) {
|
|
216
|
+
lines.push(` \x1b[33m\u26a0\x1b[0m \x1b[33m${capName.padEnd(12)}\x1b[0m configured but unreachable`);
|
|
217
|
+
if (cap.nextSteps) {
|
|
218
|
+
for (const step of cap.nextSteps) {
|
|
219
|
+
lines.push(` → ${step}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (cap.status === 'setup') {
|
|
224
|
+
lines.push(` \x1b[36m◐\x1b[0m \x1b[36m${capName.padEnd(12)}\x1b[0m setup incomplete`);
|
|
225
|
+
if (cap.completedSteps) {
|
|
226
|
+
for (const step of cap.completedSteps) {
|
|
227
|
+
lines.push(` \x1b[2m✓ ${step}\x1b[0m`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (cap.nextSteps) {
|
|
231
|
+
for (const step of cap.nextSteps) {
|
|
232
|
+
lines.push(` → ${step}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
const unsetVars = cap.env
|
|
238
|
+
? Object.entries(cap.env).filter(([, v]) => !v.set).map(([k]) => k)
|
|
239
|
+
: [];
|
|
240
|
+
const summary = unsetVars.length > 0 ? `${unsetVars[0]} not set` : 'pending';
|
|
241
|
+
lines.push(` \x1b[33m\u26a0\x1b[0m \x1b[33m${capName.padEnd(12)}\x1b[0m ${summary}`);
|
|
242
|
+
if (cap.nextSteps) {
|
|
243
|
+
for (const step of cap.nextSteps) {
|
|
244
|
+
lines.push(` → ${step}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return lines;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Format StatusData into a human-readable display string.
|
|
252
|
+
* Capabilities are grouped by type (e.g. application, development).
|
|
253
|
+
*/
|
|
254
|
+
function formatEnhancedStatus(data) {
|
|
255
|
+
const lines = [`${data.name} status:`];
|
|
256
|
+
// Group capabilities by type
|
|
257
|
+
const groups = new Map();
|
|
258
|
+
for (const [capName, cap] of Object.entries(data.capabilities)) {
|
|
259
|
+
const type = cap.type || 'other';
|
|
260
|
+
if (!groups.has(type)) {
|
|
261
|
+
groups.set(type, []);
|
|
262
|
+
}
|
|
263
|
+
groups.get(type).push([capName, cap]);
|
|
264
|
+
}
|
|
265
|
+
for (const [type, caps] of groups) {
|
|
266
|
+
lines.push(` ${type}:`);
|
|
267
|
+
for (const [capName, cap] of caps) {
|
|
268
|
+
lines.push(...formatCapabilityLine(capName, cap));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return lines.join('\n');
|
|
272
|
+
}
|
|
273
|
+
async function runStatus(options) {
|
|
274
|
+
const dir = process.cwd();
|
|
275
|
+
let manifest;
|
|
276
|
+
try {
|
|
277
|
+
manifest = (0, project_1.readProjectManifest)(dir);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
if (err instanceof SyntaxError) {
|
|
281
|
+
console.error('Error: .boop/config.json is corrupt (invalid JSON). Delete it and run `boop init` to recreate.');
|
|
282
|
+
process.exitCode = 1;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
if (!manifest) {
|
|
288
|
+
console.error('Not a boop project (no .boop/config.json found). Run `boop init` to create one.');
|
|
289
|
+
process.exitCode = 1;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const data = getStatusData(manifest, dir);
|
|
293
|
+
// Enrich capabilities that have setupChecks with live status
|
|
294
|
+
const envPath = path.join(dir, '.env');
|
|
295
|
+
const envValues = (0, env_1.readEnvValues)(envPath);
|
|
296
|
+
await enrichWithSetupChecks(data, envValues, dir, manifest.name);
|
|
297
|
+
if (options?.json) {
|
|
298
|
+
console.log(JSON.stringify(data, null, 2));
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
console.log(formatEnhancedStatus(data));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const status_1 = require("../commands/status");
|
|
40
|
+
describe('formatStatus', () => {
|
|
41
|
+
it('formats manifest fields into a readable status string', () => {
|
|
42
|
+
const manifest = {
|
|
43
|
+
name: 'my-saas',
|
|
44
|
+
profile: 'web-app',
|
|
45
|
+
boopVersion: '0.1.0',
|
|
46
|
+
capabilities: ['database', 'api', 'web'],
|
|
47
|
+
createdAt: '2026-03-19T00:00:00Z',
|
|
48
|
+
};
|
|
49
|
+
const output = (0, status_1.formatStatus)(manifest);
|
|
50
|
+
expect(output).toContain('my-saas');
|
|
51
|
+
expect(output).toContain('web-app');
|
|
52
|
+
expect(output).toContain('0.1.0');
|
|
53
|
+
expect(output).toContain('database');
|
|
54
|
+
expect(output).toContain('api');
|
|
55
|
+
expect(output).toContain('web');
|
|
56
|
+
expect(output).toContain('2026-03-19');
|
|
57
|
+
});
|
|
58
|
+
it('handles empty capabilities list', () => {
|
|
59
|
+
const manifest = {
|
|
60
|
+
name: 'bare-project',
|
|
61
|
+
profile: 'web-app',
|
|
62
|
+
boopVersion: '0.1.0',
|
|
63
|
+
capabilities: [],
|
|
64
|
+
createdAt: '2026-03-20T00:00:00Z',
|
|
65
|
+
};
|
|
66
|
+
const output = (0, status_1.formatStatus)(manifest);
|
|
67
|
+
expect(output).toContain('bare-project');
|
|
68
|
+
expect(output).toContain('none');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('status command integration', () => {
|
|
72
|
+
let tmpDir;
|
|
73
|
+
let originalCwd;
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'boop-status-'));
|
|
76
|
+
originalCwd = process.cwd();
|
|
77
|
+
process.chdir(tmpDir);
|
|
78
|
+
});
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
process.chdir(originalCwd);
|
|
81
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
it('prints status when .boop.json exists', () => {
|
|
84
|
+
const manifest = {
|
|
85
|
+
name: 'test-project',
|
|
86
|
+
profile: 'web-app',
|
|
87
|
+
boopVersion: '0.1.0',
|
|
88
|
+
capabilities: ['api', 'database'],
|
|
89
|
+
createdAt: '2026-03-19T12:00:00Z',
|
|
90
|
+
};
|
|
91
|
+
fs.writeFileSync(path.join(tmpDir, '.boop.json'), JSON.stringify(manifest));
|
|
92
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
93
|
+
const { runStatus } = require('../commands/status');
|
|
94
|
+
runStatus();
|
|
95
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
96
|
+
const output = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
97
|
+
expect(output).toContain('test-project');
|
|
98
|
+
expect(output).toContain('web-app');
|
|
99
|
+
consoleSpy.mockRestore();
|
|
100
|
+
});
|
|
101
|
+
it('prints error when not in a boop project', () => {
|
|
102
|
+
const consoleErrorSpy = jest
|
|
103
|
+
.spyOn(console, 'error')
|
|
104
|
+
.mockImplementation();
|
|
105
|
+
const processExitSpy = jest
|
|
106
|
+
.spyOn(process, 'exit')
|
|
107
|
+
.mockImplementation(() => undefined);
|
|
108
|
+
const { runStatus } = require('../commands/status');
|
|
109
|
+
runStatus();
|
|
110
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
111
|
+
const output = consoleErrorSpy.mock.calls
|
|
112
|
+
.map((c) => c.join(' '))
|
|
113
|
+
.join('\n');
|
|
114
|
+
expect(output).toMatch(/not.*boop project/i);
|
|
115
|
+
consoleErrorSpy.mockRestore();
|
|
116
|
+
processExitSpy.mockRestore();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
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.runUpdate = runUpdate;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const project_1 = require("../lib/project");
|
|
40
|
+
const capability_1 = require("../lib/capability");
|
|
41
|
+
const update_1 = require("../lib/update");
|
|
42
|
+
function runUpdate(capabilityName, options) {
|
|
43
|
+
const targetDir = process.cwd();
|
|
44
|
+
// Must be in a boop project
|
|
45
|
+
const manifest = (0, project_1.readProjectManifest)(targetDir);
|
|
46
|
+
if (!manifest) {
|
|
47
|
+
console.error(`\x1b[31mERR\x1b[0m Not a boop project (no .boop/config.json). Run \`boop init\` first.`);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Determine which capabilities to update
|
|
52
|
+
const capsToUpdate = capabilityName
|
|
53
|
+
? [capabilityName]
|
|
54
|
+
: manifest.capabilities;
|
|
55
|
+
// Validate specified capability is installed
|
|
56
|
+
if (capabilityName && !manifest.capabilities.includes(capabilityName)) {
|
|
57
|
+
console.error(`\x1b[31mERR\x1b[0m Capability "${capabilityName}" is not installed. Installed: ${manifest.capabilities.join(', ')}`);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Plan all updates
|
|
62
|
+
const plans = [];
|
|
63
|
+
for (const cap of capsToUpdate) {
|
|
64
|
+
const plan = (0, update_1.planCapabilityUpdate)(targetDir, cap, manifest);
|
|
65
|
+
if (plan) {
|
|
66
|
+
plans.push(plan);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (plans.length === 0) {
|
|
70
|
+
const diagnosticMessage = diagnoseMissingPlans(capsToUpdate, capabilityName);
|
|
71
|
+
if (options.json) {
|
|
72
|
+
console.log(JSON.stringify({ capabilities: [], message: diagnosticMessage }, null, 2));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log(diagnosticMessage);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Dry run: show plans and exit
|
|
80
|
+
if (options.dryRun) {
|
|
81
|
+
if (options.json) {
|
|
82
|
+
console.log(JSON.stringify({ capabilities: plans }, null, 2));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const plan of plans) {
|
|
86
|
+
console.log(`\n${plan.capability}:`);
|
|
87
|
+
if (plan.upToDate) {
|
|
88
|
+
console.log(' Already up to date');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
for (const action of plan.actions) {
|
|
92
|
+
const icon = action.action === 'overwrite' ? '\x1b[32m↑\x1b[0m'
|
|
93
|
+
: action.action === 'skip' ? '\x1b[33m◐\x1b[0m'
|
|
94
|
+
: action.action === 'add' ? '\x1b[32m+\x1b[0m'
|
|
95
|
+
: '\x1b[90m⚠\x1b[0m';
|
|
96
|
+
console.log(` ${icon} ${action.relativePath} — ${action.reason}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Execute updates
|
|
102
|
+
const results = [];
|
|
103
|
+
for (const plan of plans) {
|
|
104
|
+
if (plan.upToDate) {
|
|
105
|
+
results.push({
|
|
106
|
+
capability: plan.capability,
|
|
107
|
+
overwritten: [],
|
|
108
|
+
skipped: [],
|
|
109
|
+
added: [],
|
|
110
|
+
deprecated: [],
|
|
111
|
+
forcedOverwrite: [],
|
|
112
|
+
skippedDeleted: [],
|
|
113
|
+
pruned: [],
|
|
114
|
+
unchangedCount: plan.unchangedCount,
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const result = (0, update_1.executeUpdatePlan)(plan, { force: options.force, restore: options.restore, prune: options.prune });
|
|
119
|
+
results.push(result);
|
|
120
|
+
}
|
|
121
|
+
// Output
|
|
122
|
+
if (options.json) {
|
|
123
|
+
console.log(JSON.stringify({ capabilities: results }, null, 2));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
console.log(`\n${'─'.repeat(40)}`);
|
|
127
|
+
for (const result of results) {
|
|
128
|
+
const summary = (0, update_1.formatUpdateSummary)(result);
|
|
129
|
+
console.log(`${result.capability}: ${summary}`);
|
|
130
|
+
// Show details for non-trivial updates
|
|
131
|
+
for (const f of result.overwritten) {
|
|
132
|
+
if (result.forcedOverwrite.includes(f)) {
|
|
133
|
+
console.log(` \x1b[33m⚠\x1b[0m ${f} (force-overwritten, backup saved)`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.log(` \x1b[32m↑\x1b[0m ${f}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const f of result.skipped) {
|
|
140
|
+
if (result.skippedDeleted?.includes(f)) {
|
|
141
|
+
console.log(` \x1b[36mℹ\x1b[0m ${f} (locally deleted, skipped — use --restore to re-add)`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(` \x1b[33m◐\x1b[0m ${f} (locally modified, skipped)`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const f of result.added) {
|
|
148
|
+
console.log(` \x1b[32m+\x1b[0m ${f} (new)`);
|
|
149
|
+
}
|
|
150
|
+
for (const f of result.deprecated) {
|
|
151
|
+
if (result.pruned?.includes(f)) {
|
|
152
|
+
console.log(` \x1b[31m✕\x1b[0m ${f} (pruned, backup saved)`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
console.log(` \x1b[33m⚠\x1b[0m ${f} (deprecated — removed from package, use --prune to delete)`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Diagnose why no update plans were generated and return a specific message.
|
|
162
|
+
*/
|
|
163
|
+
function diagnoseMissingPlans(capsToUpdate, requestedCap) {
|
|
164
|
+
if (capsToUpdate.length === 0) {
|
|
165
|
+
return 'No capabilities installed. Run `boop add <capability>` to install one.';
|
|
166
|
+
}
|
|
167
|
+
// Check each capability and collect reasons
|
|
168
|
+
const noPackage = [];
|
|
169
|
+
const noBoopkit = [];
|
|
170
|
+
for (const cap of capsToUpdate) {
|
|
171
|
+
const pkgDir = (0, capability_1.getPackageDir)(cap);
|
|
172
|
+
if (!pkgDir) {
|
|
173
|
+
noPackage.push(cap);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const pkgJsonPath = path.join(pkgDir, 'package.json');
|
|
177
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
178
|
+
noPackage.push(cap);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
182
|
+
if (!pkgJson.boopkit?.capability) {
|
|
183
|
+
noBoopkit.push(cap);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (requestedCap) {
|
|
187
|
+
// Single capability was requested
|
|
188
|
+
if (noPackage.includes(requestedCap)) {
|
|
189
|
+
return `Capability "${requestedCap}" is installed but has no package. It may be a local-only capability without updatable scaffold files.`;
|
|
190
|
+
}
|
|
191
|
+
if (noBoopkit.includes(requestedCap)) {
|
|
192
|
+
return `Capability "${requestedCap}" has a package but no boopkit scaffold configuration. Nothing to update.`;
|
|
193
|
+
}
|
|
194
|
+
return `No updates available for "${requestedCap}".`;
|
|
195
|
+
}
|
|
196
|
+
// All capabilities requested
|
|
197
|
+
if (noPackage.length === capsToUpdate.length) {
|
|
198
|
+
return 'All installed capabilities are local-only (no packages found). Nothing to update.';
|
|
199
|
+
}
|
|
200
|
+
if (noBoopkit.length + noPackage.length === capsToUpdate.length) {
|
|
201
|
+
return 'No installed capabilities have updatable scaffold files.';
|
|
202
|
+
}
|
|
203
|
+
return 'No capabilities to update.';
|
|
204
|
+
}
|