@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.
Files changed (53) hide show
  1. package/README.md +65 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +85 -0
  4. package/dist/commands/add.d.ts +6 -0
  5. package/dist/commands/add.js +173 -0
  6. package/dist/commands/adopt.d.ts +5 -0
  7. package/dist/commands/adopt.js +286 -0
  8. package/dist/commands/init.d.ts +5 -0
  9. package/dist/commands/init.js +295 -0
  10. package/dist/commands/list.d.ts +6 -0
  11. package/dist/commands/list.js +105 -0
  12. package/dist/commands/loop.d.ts +63 -0
  13. package/dist/commands/loop.js +765 -0
  14. package/dist/commands/status.d.ts +40 -0
  15. package/dist/commands/status.js +303 -0
  16. package/dist/commands/status.test.d.ts +1 -0
  17. package/dist/commands/status.test.js +118 -0
  18. package/dist/commands/update.d.ts +8 -0
  19. package/dist/commands/update.js +204 -0
  20. package/dist/lib/capability.d.ts +17 -0
  21. package/dist/lib/capability.js +172 -0
  22. package/dist/lib/config.d.ts +70 -0
  23. package/dist/lib/config.js +59 -0
  24. package/dist/lib/db-status.d.ts +64 -0
  25. package/dist/lib/db-status.js +257 -0
  26. package/dist/lib/env.d.ts +25 -0
  27. package/dist/lib/env.js +116 -0
  28. package/dist/lib/execute.d.ts +21 -0
  29. package/dist/lib/execute.js +228 -0
  30. package/dist/lib/interpolate.d.ts +4 -0
  31. package/dist/lib/interpolate.js +13 -0
  32. package/dist/lib/manifest.d.ts +54 -0
  33. package/dist/lib/manifest.js +11 -0
  34. package/dist/lib/preflight.d.ts +31 -0
  35. package/dist/lib/preflight.js +402 -0
  36. package/dist/lib/profile.d.ts +11 -0
  37. package/dist/lib/profile.js +73 -0
  38. package/dist/lib/project.d.ts +36 -0
  39. package/dist/lib/project.js +71 -0
  40. package/dist/lib/project.test.d.ts +1 -0
  41. package/dist/lib/project.test.js +118 -0
  42. package/dist/lib/rewire.d.ts +19 -0
  43. package/dist/lib/rewire.js +222 -0
  44. package/dist/lib/safe-io.d.ts +66 -0
  45. package/dist/lib/safe-io.js +167 -0
  46. package/dist/lib/scaffold.d.ts +87 -0
  47. package/dist/lib/scaffold.js +548 -0
  48. package/dist/lib/update.d.ts +73 -0
  49. package/dist/lib/update.js +379 -0
  50. package/package.json +46 -0
  51. package/profiles/web-app/profile.md +113 -0
  52. package/profiles/web-app/reference/architecture.ref.md +65 -0
  53. package/profiles/web-app/reference/message-queue-patterns.ref.md +247 -0
@@ -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 project_1 = require("../lib/project");
40
+ describe('readProjectManifest', () => {
41
+ let tmpDir;
42
+ beforeEach(() => {
43
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'boop-test-'));
44
+ });
45
+ afterEach(() => {
46
+ fs.rmSync(tmpDir, { recursive: true, force: true });
47
+ });
48
+ it('reads a valid .boop.json manifest', () => {
49
+ const manifest = {
50
+ name: 'my-saas',
51
+ profile: 'web-app',
52
+ boopVersion: '0.1.0',
53
+ capabilities: ['database', 'api', 'web'],
54
+ createdAt: '2026-03-19T00:00:00Z',
55
+ };
56
+ fs.writeFileSync(path.join(tmpDir, '.boop.json'), JSON.stringify(manifest));
57
+ const result = (0, project_1.readProjectManifest)(tmpDir);
58
+ expect(result).toEqual(manifest);
59
+ });
60
+ it('returns null when .boop.json does not exist', () => {
61
+ const result = (0, project_1.readProjectManifest)(tmpDir);
62
+ expect(result).toBeNull();
63
+ });
64
+ it('throws on invalid JSON', () => {
65
+ fs.writeFileSync(path.join(tmpDir, '.boop.json'), '{ not valid json }');
66
+ expect(() => (0, project_1.readProjectManifest)(tmpDir)).toThrow();
67
+ });
68
+ });
69
+ describe('writeProjectManifest', () => {
70
+ let tmpDir;
71
+ beforeEach(() => {
72
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'boop-test-'));
73
+ });
74
+ afterEach(() => {
75
+ fs.rmSync(tmpDir, { recursive: true, force: true });
76
+ });
77
+ it('writes a manifest that can be read back', () => {
78
+ const manifest = {
79
+ name: 'round-trip',
80
+ profile: 'web-app',
81
+ boopVersion: '0.1.0',
82
+ capabilities: ['api', 'database'],
83
+ createdAt: '2026-03-20T00:00:00Z',
84
+ };
85
+ (0, project_1.writeProjectManifest)(tmpDir, manifest);
86
+ const result = (0, project_1.readProjectManifest)(tmpDir);
87
+ expect(result).toEqual(manifest);
88
+ });
89
+ it('writes formatted JSON with trailing newline', () => {
90
+ const manifest = {
91
+ name: 'test',
92
+ profile: 'web-app',
93
+ boopVersion: '0.1.0',
94
+ capabilities: [],
95
+ createdAt: '2026-03-20T00:00:00Z',
96
+ };
97
+ (0, project_1.writeProjectManifest)(tmpDir, manifest);
98
+ const raw = fs.readFileSync(path.join(tmpDir, '.boop.json'), 'utf-8');
99
+ expect(raw).toContain('\n'); // formatted, not single-line
100
+ expect(raw.endsWith('\n')).toBe(true);
101
+ });
102
+ });
103
+ describe('isBoopProject', () => {
104
+ let tmpDir;
105
+ beforeEach(() => {
106
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'boop-test-'));
107
+ });
108
+ afterEach(() => {
109
+ fs.rmSync(tmpDir, { recursive: true, force: true });
110
+ });
111
+ it('returns true when .boop.json exists', () => {
112
+ fs.writeFileSync(path.join(tmpDir, '.boop.json'), '{}');
113
+ expect((0, project_1.isBoopProject)(tmpDir)).toBe(true);
114
+ });
115
+ it('returns false when .boop.json does not exist', () => {
116
+ expect((0, project_1.isBoopProject)(tmpDir)).toBe(false);
117
+ });
118
+ });
@@ -0,0 +1,19 @@
1
+ import { CapabilityHook } from './config';
2
+ export interface RewireOptions {
3
+ needsEnv?: boolean;
4
+ }
5
+ export interface RewireResult {
6
+ missingMarkers: string[];
7
+ }
8
+ /**
9
+ * Rewire an existing server.ts by finding `// @boopkit:<phase>` markers
10
+ * and inserting capability hook code above them.
11
+ *
12
+ * - Inserted code gets `@boopkit/cap-<name>` annotations
13
+ * - Markers are preserved after insertion for future `boop add` calls
14
+ * - Missing markers produce a warning to stderr with manual insertion guidance
15
+ * - Already-inserted code (same capability annotation) is skipped to prevent duplicates
16
+ */
17
+ export declare function rewireServerFile(serverPath: string, capabilityName: string, hooks: CapabilityHook[], options?: RewireOptions & {
18
+ skipBackup?: boolean;
19
+ }): RewireResult;
@@ -0,0 +1,222 @@
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.rewireServerFile = rewireServerFile;
37
+ const fs = __importStar(require("fs"));
38
+ const safe_io_1 = require("./safe-io");
39
+ /**
40
+ * Rewire an existing server.ts by finding `// @boopkit:<phase>` markers
41
+ * and inserting capability hook code above them.
42
+ *
43
+ * - Inserted code gets `@boopkit/cap-<name>` annotations
44
+ * - Markers are preserved after insertion for future `boop add` calls
45
+ * - Missing markers produce a warning to stderr with manual insertion guidance
46
+ * - Already-inserted code (same capability annotation) is skipped to prevent duplicates
47
+ */
48
+ function rewireServerFile(serverPath, capabilityName, hooks, options = {}) {
49
+ const content = fs.readFileSync(serverPath, 'utf-8');
50
+ const capAnnotation = `@boopkit/cap-${capabilityName}`;
51
+ const alreadyPresent = content.includes(capAnnotation);
52
+ // Skip if this capability's hooks are already wired
53
+ if (alreadyPresent) {
54
+ return { missingMarkers: [] };
55
+ }
56
+ const missingMarkers = [];
57
+ let result = content;
58
+ // Group hooks by phase
59
+ const byPhase = new Map();
60
+ for (const hook of hooks) {
61
+ if (!byPhase.has(hook.phase)) {
62
+ byPhase.set(hook.phase, []);
63
+ }
64
+ byPhase.get(hook.phase).push(hook);
65
+ }
66
+ // Handle env import
67
+ if (options.needsEnv && !result.includes("dotenv/config")) {
68
+ const envMarker = '// @boopkit:env';
69
+ if (result.includes(envMarker)) {
70
+ result = insertAboveMarker(result, envMarker, "import 'dotenv/config';");
71
+ }
72
+ else {
73
+ missingMarkers.push('env');
74
+ reportMissing('env', "import 'dotenv/config';");
75
+ }
76
+ }
77
+ // Process each phase
78
+ for (const [phase, phaseHooks] of byPhase) {
79
+ const marker = `// @boopkit:${phase}`;
80
+ if (!result.includes(marker)) {
81
+ missingMarkers.push(phase);
82
+ const code = renderHooksForPhase(phase, capabilityName, phaseHooks);
83
+ reportMissing(phase, code);
84
+ continue;
85
+ }
86
+ if (phase === 'imports') {
87
+ const code = renderHooksForPhase(phase, capabilityName, phaseHooks);
88
+ result = insertAboveMarker(result, marker, code);
89
+ }
90
+ else {
91
+ result = insertOrderedHooks(result, marker, capabilityName, phaseHooks);
92
+ }
93
+ }
94
+ if (!options.skipBackup)
95
+ (0, safe_io_1.createBackup)(serverPath);
96
+ fs.writeFileSync(serverPath, result, 'utf-8');
97
+ return { missingMarkers };
98
+ }
99
+ function renderHooksForPhase(phase, capName, hooks) {
100
+ if (phase === 'imports') {
101
+ return renderImportHooks(capName, hooks);
102
+ }
103
+ return renderOrderedHooks(capName, hooks);
104
+ }
105
+ function renderImportHooks(capName, hooks) {
106
+ // Group all imports under a single annotation comment
107
+ const lines = [];
108
+ const descriptions = hooks.map(h => h.description).filter(Boolean);
109
+ const desc = descriptions.length > 0 ? ` ${descriptions[0]}` : '';
110
+ lines.push(`/** @boopkit/cap-${capName}${desc} */`);
111
+ for (const hook of hooks) {
112
+ lines.push(hook.code);
113
+ }
114
+ return lines.join('\n');
115
+ }
116
+ function renderOrderedHooks(capName, hooks) {
117
+ return hooks.map(hook => {
118
+ const order = hook.order ?? 50;
119
+ const desc = hook.description ? ` ${hook.description}` : '';
120
+ const comment = `/** @boopkit/cap-${capName} [${order}]${desc} */`;
121
+ return `${comment}\n${hook.code}`;
122
+ }).join('\n\n');
123
+ }
124
+ /**
125
+ * Insert code above a marker line, preserving the marker's indentation.
126
+ */
127
+ function insertAboveMarker(content, marker, code) {
128
+ const lines = content.split('\n');
129
+ const markerIdx = lines.findIndex(l => l.trim() === marker);
130
+ if (markerIdx === -1)
131
+ return content;
132
+ const markerLine = lines[markerIdx];
133
+ const indent = markerLine.match(/^(\s*)/)?.[1] ?? '';
134
+ // Indent each line of the inserted code to match marker indentation
135
+ const indentedCode = code.split('\n').map(l => l.length > 0 ? indent + l : l).join('\n');
136
+ lines.splice(markerIdx, 0, indentedCode, '');
137
+ return lines.join('\n');
138
+ }
139
+ /**
140
+ * Parse the order number from a hook annotation comment like:
141
+ * /** @boopkit/cap-name [42] description *​/
142
+ */
143
+ function parseOrderFromAnnotation(line) {
144
+ const match = line.match(/\[(\d+)\]/);
145
+ return match ? parseInt(match[1], 10) : 50;
146
+ }
147
+ /**
148
+ * Parse existing hook blocks above a marker line.
149
+ * Returns the blocks with their order numbers and the line index where the hook zone starts.
150
+ */
151
+ function parseHookZone(lines, markerIdx) {
152
+ const blocks = [];
153
+ let i = markerIdx - 1;
154
+ let zoneStart = markerIdx;
155
+ // Skip blank lines above the marker
156
+ while (i >= 0 && lines[i].trim() === '')
157
+ i--;
158
+ while (i >= 0) {
159
+ const blockEnd = i;
160
+ let annotationIdx = i;
161
+ let foundAnnotation = false;
162
+ // Scan backwards from current line to find the annotation comment
163
+ while (annotationIdx >= 0) {
164
+ if (lines[annotationIdx].trim().startsWith('/**') && lines[annotationIdx].includes('@boopkit/cap-')) {
165
+ foundAnnotation = true;
166
+ break;
167
+ }
168
+ if (lines[annotationIdx].trim() === '')
169
+ break;
170
+ annotationIdx--;
171
+ }
172
+ if (!foundAnnotation)
173
+ break;
174
+ const blockLines = lines.slice(annotationIdx, blockEnd + 1);
175
+ const order = parseOrderFromAnnotation(lines[annotationIdx]);
176
+ blocks.unshift({ order, text: blockLines.join('\n') });
177
+ zoneStart = annotationIdx;
178
+ i = annotationIdx - 1;
179
+ // Skip blank lines between blocks
180
+ while (i >= 0 && lines[i].trim() === '')
181
+ i--;
182
+ }
183
+ return { blocks, zoneStart };
184
+ }
185
+ /**
186
+ * Insert hooks into a phase zone in order-sorted position.
187
+ * Parses existing hook blocks above the marker, merges with new hooks,
188
+ * sorts by order, and rewrites the zone.
189
+ */
190
+ function insertOrderedHooks(content, marker, capName, hooks) {
191
+ const lines = content.split('\n');
192
+ const markerIdx = lines.findIndex(l => l.trim() === marker);
193
+ if (markerIdx === -1)
194
+ return content;
195
+ const markerLine = lines[markerIdx];
196
+ const indent = markerLine.match(/^(\s*)/)?.[1] ?? '';
197
+ // Parse existing hook blocks above the marker
198
+ const { blocks: existingBlocks, zoneStart } = parseHookZone(lines, markerIdx);
199
+ // Create new hook blocks with proper indentation
200
+ const newBlocks = hooks.map(hook => {
201
+ const order = hook.order ?? 50;
202
+ const desc = hook.description ? ` ${hook.description}` : '';
203
+ const comment = `${indent}/** @boopkit/cap-${capName} [${order}]${desc} */`;
204
+ const code = hook.code.split('\n').map(l => l.length > 0 ? indent + l : l).join('\n');
205
+ return { order, text: `${comment}\n${code}` };
206
+ });
207
+ // Merge and sort by order
208
+ const allBlocks = [...existingBlocks, ...newBlocks];
209
+ allBlocks.sort((a, b) => a.order - b.order);
210
+ // Build the replacement for the zone
211
+ const zoneContent = allBlocks.map(b => b.text).join('\n\n');
212
+ // Reconstruct: everything before zone + sorted blocks + blank line + marker + rest
213
+ const before = lines.slice(0, zoneStart);
214
+ const after = lines.slice(markerIdx);
215
+ return [...before, zoneContent, '', ...after].join('\n');
216
+ }
217
+ function reportMissing(phase, code) {
218
+ const marker = `// @boopkit:${phase}`;
219
+ process.stderr.write(`Warning: marker "${marker}" not found in server.ts.\n` +
220
+ `Add the marker and insert the following code above it:\n` +
221
+ `${code}\n\n`);
222
+ }
@@ -0,0 +1,66 @@
1
+ export type ConflictStrategy = 'skip' | 'backup' | 'overwrite' | 'abort';
2
+ export interface SafeWriteOptions {
3
+ /** What to do when the target file already exists. Default: 'abort'. */
4
+ onConflict?: ConflictStrategy;
5
+ }
6
+ export interface SafeCopyOptions {
7
+ /** What to do when the target file already exists. Default: 'skip'. */
8
+ onConflict?: ConflictStrategy;
9
+ }
10
+ export interface SafeWriteResult {
11
+ /** Whether the write actually happened. */
12
+ written: boolean;
13
+ /** Path to backup file if one was created. */
14
+ backupPath?: string;
15
+ /** If skipped, the reason why. */
16
+ skipped?: string;
17
+ }
18
+ export interface SafeCopyResult {
19
+ /** Whether the copy actually happened. */
20
+ copied: boolean;
21
+ /** Path to backup file if one was created. */
22
+ backupPath?: string;
23
+ /** If skipped, the reason why. */
24
+ skipped?: string;
25
+ }
26
+ /**
27
+ * Write a file with conflict-awareness.
28
+ *
29
+ * - 'skip': don't write if file exists; return skipped result
30
+ * - 'backup': create .bak before overwriting
31
+ * - 'overwrite': write unconditionally (current behavior)
32
+ * - 'abort': throw if file exists
33
+ *
34
+ * If the file does not exist, writes unconditionally regardless of strategy.
35
+ */
36
+ export declare function safeWriteFile(filePath: string, content: string, options?: SafeWriteOptions): SafeWriteResult;
37
+ /**
38
+ * Copy a file with conflict-awareness.
39
+ *
40
+ * - 'skip': don't copy if dest exists
41
+ * - 'backup': create .bak of dest before overwriting
42
+ * - 'overwrite': copy unconditionally
43
+ * - 'abort': throw if dest exists
44
+ */
45
+ export declare function safeCopyFile(srcPath: string, destPath: string, options?: SafeCopyOptions): SafeCopyResult;
46
+ export interface SafeCopyDirResult {
47
+ copied: string[];
48
+ skipped: string[];
49
+ backups: string[];
50
+ }
51
+ /**
52
+ * Recursively copy a directory with per-file conflict handling.
53
+ */
54
+ export declare function safeCopyDir(srcDir: string, destDir: string, options?: SafeCopyOptions): SafeCopyDirResult;
55
+ /**
56
+ * Create a .bak backup of a file. Returns the backup path.
57
+ * Overwrites any existing .bak for the same file.
58
+ */
59
+ export declare function createBackup(filePath: string): string;
60
+ /**
61
+ * Error thrown when a file operation encounters an unexpected existing file.
62
+ */
63
+ export declare class FileConflictError extends Error {
64
+ readonly filePath: string;
65
+ constructor(filePath: string, message: string);
66
+ }
@@ -0,0 +1,167 @@
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.FileConflictError = void 0;
37
+ exports.safeWriteFile = safeWriteFile;
38
+ exports.safeCopyFile = safeCopyFile;
39
+ exports.safeCopyDir = safeCopyDir;
40
+ exports.createBackup = createBackup;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ /**
44
+ * Write a file with conflict-awareness.
45
+ *
46
+ * - 'skip': don't write if file exists; return skipped result
47
+ * - 'backup': create .bak before overwriting
48
+ * - 'overwrite': write unconditionally (current behavior)
49
+ * - 'abort': throw if file exists
50
+ *
51
+ * If the file does not exist, writes unconditionally regardless of strategy.
52
+ */
53
+ function safeWriteFile(filePath, content, options = {}) {
54
+ const strategy = options.onConflict ?? 'abort';
55
+ const exists = fs.existsSync(filePath);
56
+ if (exists) {
57
+ switch (strategy) {
58
+ case 'abort':
59
+ throw new FileConflictError(filePath, 'File already exists');
60
+ case 'skip':
61
+ return { written: false, skipped: `File already exists: ${filePath}` };
62
+ case 'backup': {
63
+ const backupPath = createBackup(filePath);
64
+ fs.writeFileSync(filePath, content, 'utf-8');
65
+ return { written: true, backupPath };
66
+ }
67
+ case 'overwrite':
68
+ fs.writeFileSync(filePath, content, 'utf-8');
69
+ return { written: true };
70
+ }
71
+ }
72
+ // File doesn't exist — ensure directory exists and write
73
+ const dir = path.dirname(filePath);
74
+ if (!fs.existsSync(dir)) {
75
+ fs.mkdirSync(dir, { recursive: true });
76
+ }
77
+ fs.writeFileSync(filePath, content, 'utf-8');
78
+ return { written: true };
79
+ }
80
+ /**
81
+ * Copy a file with conflict-awareness.
82
+ *
83
+ * - 'skip': don't copy if dest exists
84
+ * - 'backup': create .bak of dest before overwriting
85
+ * - 'overwrite': copy unconditionally
86
+ * - 'abort': throw if dest exists
87
+ */
88
+ function safeCopyFile(srcPath, destPath, options = {}) {
89
+ const strategy = options.onConflict ?? 'skip';
90
+ const exists = fs.existsSync(destPath);
91
+ if (exists) {
92
+ switch (strategy) {
93
+ case 'abort':
94
+ throw new FileConflictError(destPath, 'File already exists');
95
+ case 'skip':
96
+ return { copied: false, skipped: `File already exists: ${destPath}` };
97
+ case 'backup': {
98
+ const backupPath = createBackup(destPath);
99
+ fs.copyFileSync(srcPath, destPath);
100
+ return { copied: true, backupPath };
101
+ }
102
+ case 'overwrite':
103
+ fs.copyFileSync(srcPath, destPath);
104
+ return { copied: true };
105
+ }
106
+ }
107
+ // Dest doesn't exist — ensure directory exists and copy
108
+ const dir = path.dirname(destPath);
109
+ if (!fs.existsSync(dir)) {
110
+ fs.mkdirSync(dir, { recursive: true });
111
+ }
112
+ fs.copyFileSync(srcPath, destPath);
113
+ return { copied: true };
114
+ }
115
+ /**
116
+ * Recursively copy a directory with per-file conflict handling.
117
+ */
118
+ function safeCopyDir(srcDir, destDir, options = {}) {
119
+ const result = { copied: [], skipped: [], backups: [] };
120
+ function processDir(src, dest) {
121
+ if (!fs.existsSync(dest)) {
122
+ fs.mkdirSync(dest, { recursive: true });
123
+ }
124
+ const entries = fs.readdirSync(src, { withFileTypes: true });
125
+ for (const entry of entries) {
126
+ const srcPath = path.join(src, entry.name);
127
+ const destPath = path.join(dest, entry.name);
128
+ if (entry.isDirectory()) {
129
+ processDir(srcPath, destPath);
130
+ }
131
+ else {
132
+ const copyResult = safeCopyFile(srcPath, destPath, options);
133
+ if (copyResult.copied) {
134
+ result.copied.push(destPath);
135
+ if (copyResult.backupPath) {
136
+ result.backups.push(copyResult.backupPath);
137
+ }
138
+ }
139
+ else if (copyResult.skipped) {
140
+ result.skipped.push(destPath);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ processDir(srcDir, destDir);
146
+ return result;
147
+ }
148
+ /**
149
+ * Create a .bak backup of a file. Returns the backup path.
150
+ * Overwrites any existing .bak for the same file.
151
+ */
152
+ function createBackup(filePath) {
153
+ const backupPath = filePath + '.bak';
154
+ fs.copyFileSync(filePath, backupPath);
155
+ return backupPath;
156
+ }
157
+ /**
158
+ * Error thrown when a file operation encounters an unexpected existing file.
159
+ */
160
+ class FileConflictError extends Error {
161
+ constructor(filePath, message) {
162
+ super(`${message}: ${filePath}`);
163
+ this.name = 'FileConflictError';
164
+ this.filePath = filePath;
165
+ }
166
+ }
167
+ exports.FileConflictError = FileConflictError;
@@ -0,0 +1,87 @@
1
+ import { CapabilityHook } from './config';
2
+ export interface TemplateVariables {
3
+ projectName: string;
4
+ port: number;
5
+ scripts: Record<string, string>;
6
+ nodeVersion: string;
7
+ hookImports?: string;
8
+ hookInit?: string;
9
+ hookPreRoutes?: string;
10
+ hookPostRoutes?: string;
11
+ }
12
+ /**
13
+ * Render a single Handlebars template string with variables.
14
+ *
15
+ * TRUST BOUNDARY: noEscape is intentional — template variables (hook code,
16
+ * import statements, etc.) contain raw code that must not be HTML-escaped.
17
+ * This is safe as long as all capability manifests are first-party/trusted.
18
+ * If third-party capabilities are ever supported, hook code from manifests
19
+ * must be sanitized or sandboxed before reaching this function. See also
20
+ * collectHooks(), which passes manifest hook code through as-is.
21
+ */
22
+ export declare function renderTemplate(templateContent: string, variables: TemplateVariables): string;
23
+ /**
24
+ * Copy and render all kernel templates into a target directory.
25
+ * Creates directories as needed. Does not overwrite existing files
26
+ * unless overwrite is true.
27
+ */
28
+ export declare function scaffoldKernel(targetDir: string, variables: TemplateVariables, overwrite?: boolean): string[];
29
+ /**
30
+ * Compute SHA-256 hash of a file's content.
31
+ */
32
+ export declare function hashFile(filePath: string): string;
33
+ /**
34
+ * Copy capability-specific files into a target project directory.
35
+ * Tries package resolution first (scaffold/ and skills/ dirs), falls back to source tree.
36
+ * Returns list of copied file paths relative to targetDir, and file hashes.
37
+ */
38
+ export declare function copyCapabilityFiles(targetDir: string, capabilityName: string): string[];
39
+ /**
40
+ * Merge dependencies into an existing package.json.
41
+ * Does not overwrite existing dependency versions.
42
+ */
43
+ /**
44
+ * Parse a dependency string like "express" or "@prisma/client@^6" into name + version.
45
+ */
46
+ /**
47
+ * Read the version from a @boopkit/ package's package.json and return ^version.
48
+ * Falls back to '*' if the package.json cannot be read.
49
+ */
50
+ export declare function getBoopkitVersion(packageName: string): string;
51
+ export declare function mergePackageJson(targetDir: string, dependencies?: string[], devDependencies?: string[], packages?: Record<string, string>, options?: {
52
+ skipBackup?: boolean;
53
+ }): void;
54
+ export interface HookVariables {
55
+ hookImports: string;
56
+ hookInit: string;
57
+ hookPreRoutes: string;
58
+ hookPostRoutes: string;
59
+ }
60
+ /**
61
+ * Scan the styles/ directory for CSS partials and inject @import lines
62
+ * into styles/styles.css after the last existing @import line.
63
+ */
64
+ export declare function injectCssImports(targetDir: string, cssCapMap?: Record<string, string>, options?: {
65
+ skipBackup?: boolean;
66
+ }): void;
67
+ interface CapabilityHookEntry {
68
+ name: string;
69
+ hooks: CapabilityHook[];
70
+ }
71
+ /**
72
+ * Collect hooks from installed capabilities, group by phase, sort by order,
73
+ * and render into strings suitable for injection into server.ts.hbs.
74
+ *
75
+ * TRUST BOUNDARY: Hook code from capability manifests is injected verbatim
76
+ * into the generated server file — no escaping, validation, or sandboxing.
77
+ * This is safe while all capabilities are first-party. Before supporting
78
+ * third-party capabilities, hook code must be validated or sandboxed to
79
+ * prevent arbitrary code injection into scaffolded projects.
80
+ */
81
+ export declare function collectHooks(capabilities: CapabilityHookEntry[]): HookVariables;
82
+ /**
83
+ * Generate skills/index.md from the .skill.md files present in the project.
84
+ * Overwrites any existing index.md.
85
+ */
86
+ export declare function generateSkillsIndex(targetDir: string): void;
87
+ export {};