@boopkit/cli 0.6.14 → 0.6.15
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/dist/capability-versions.json +5 -5
- package/dist/cli.js +0 -0
- package/dist/commands/init.js +1 -1
- package/dist/commands/update.js +30 -1
- package/dist/lib/execute.js +3 -12
- package/dist/lib/scaffold.d.ts +36 -6
- package/dist/lib/scaffold.js +79 -10
- package/dist/lib/script-merge.d.ts +55 -0
- package/dist/lib/script-merge.js +144 -0
- package/dist/lib/stale-scripts.d.ts +3 -2
- package/dist/lib/stale-scripts.js +3 -2
- package/dist/lib/update.d.ts +7 -0
- package/dist/lib/update.js +67 -47
- package/package.json +1 -1
- package/dist/commands/loop.js.orig-pre-maxbuffer +0 -1455
- package/dist/commands/status.test.d.ts +0 -1
- package/dist/commands/status.test.js +0 -118
- package/dist/lib/project.test.d.ts +0 -1
- package/dist/lib/project.test.js +0 -118
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
"@boopkit/cap-api": "0.4.5",
|
|
3
3
|
"@boopkit/cap-background-jobs": "0.4.3",
|
|
4
4
|
"@boopkit/cap-cloud-storage": "0.4.1",
|
|
5
|
-
"@boopkit/cap-coding": "0.4.
|
|
5
|
+
"@boopkit/cap-coding": "0.4.6",
|
|
6
6
|
"@boopkit/cap-database": "0.4.4",
|
|
7
7
|
"@boopkit/cap-email": "0.4.3",
|
|
8
8
|
"@boopkit/cap-hello": "0.4.1",
|
|
9
9
|
"@boopkit/cap-noticeboard": "0.4.1",
|
|
10
10
|
"@boopkit/cap-oauth-client": "0.1.1",
|
|
11
11
|
"@boopkit/cap-oauth-server": "0.1.4",
|
|
12
|
-
"@boopkit/cap-product-design": "0.6.
|
|
13
|
-
"@boopkit/cap-project-mgmt": "0.4.
|
|
12
|
+
"@boopkit/cap-product-design": "0.6.9",
|
|
13
|
+
"@boopkit/cap-project-mgmt": "0.4.11",
|
|
14
14
|
"@boopkit/cap-services": "0.4.3",
|
|
15
|
-
"@boopkit/cap-skills": "0.5.
|
|
15
|
+
"@boopkit/cap-skills": "0.5.6",
|
|
16
16
|
"@boopkit/cap-testing": "0.5.7",
|
|
17
17
|
"@boopkit/cap-users": "0.8.1",
|
|
18
18
|
"@boopkit/cap-ux": "0.2.3",
|
|
19
19
|
"@boopkit/cap-web": "0.5.5",
|
|
20
|
-
"@boopkit/cap-workflows": "0.2.
|
|
20
|
+
"@boopkit/cap-workflows": "0.2.2",
|
|
21
21
|
"@boopkit/logger": "0.4.0"
|
|
22
22
|
}
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/commands/init.js
CHANGED
|
@@ -369,7 +369,7 @@ function runInit(name, options, deps = {}) {
|
|
|
369
369
|
(0, scaffold_1.injectCssImports)(targetDir, cssCapMap, { skipBackup: true });
|
|
370
370
|
// --- Symlink skills/* into .claude/skills/ for Claude Code auto-discovery, and give each a PROJECT.md ---
|
|
371
371
|
(0, scaffold_1.linkSkillsToClaude)(targetDir);
|
|
372
|
-
(0, scaffold_1.createSkillProjectFiles)(targetDir);
|
|
372
|
+
(0, scaffold_1.createSkillProjectFiles)(targetDir, capSearchPaths);
|
|
373
373
|
// --- Merge capability scripts into package.json ---
|
|
374
374
|
if (Object.keys(allScripts).length > 0) {
|
|
375
375
|
const pkgPath = path.join(targetDir, 'package.json');
|
package/dist/commands/update.js
CHANGED
|
@@ -105,7 +105,7 @@ function runUpdate(capabilityName, options) {
|
|
|
105
105
|
console.log(`\n${plan.capability}:`);
|
|
106
106
|
// Recording a hash for a file that already holds what boop ships changes nothing the project can see
|
|
107
107
|
const visible = plan.actions.filter(a => a.action !== 'record-hash');
|
|
108
|
-
if (visible.length === 0) {
|
|
108
|
+
if (visible.length === 0 && plan.scriptActions.length === 0) {
|
|
109
109
|
console.log(' Already up to date');
|
|
110
110
|
continue;
|
|
111
111
|
}
|
|
@@ -122,6 +122,9 @@ function runUpdate(capabilityName, options) {
|
|
|
122
122
|
if (action.instruction)
|
|
123
123
|
console.log(instructionLine(action.instruction));
|
|
124
124
|
}
|
|
125
|
+
for (const line of scriptLines(plan.scriptActions)) {
|
|
126
|
+
console.log(line);
|
|
127
|
+
}
|
|
125
128
|
}
|
|
126
129
|
if (stale.length > 0) {
|
|
127
130
|
console.log(`\n${(0, stale_scripts_1.formatStaleScriptWarning)(stale)}`);
|
|
@@ -142,6 +145,8 @@ function runUpdate(capabilityName, options) {
|
|
|
142
145
|
skippedDeleted: [],
|
|
143
146
|
pruned: [],
|
|
144
147
|
migrated: [],
|
|
148
|
+
scriptsAdded: [],
|
|
149
|
+
scriptsKept: plan.scriptActions.filter(a => a.action === 'keep'),
|
|
145
150
|
unchangedCount: plan.unchangedCount,
|
|
146
151
|
instructions: [],
|
|
147
152
|
});
|
|
@@ -219,6 +224,9 @@ function runUpdate(capabilityName, options) {
|
|
|
219
224
|
console.log(` \x1b[33m⚠\x1b[0m ${f} (deprecated — removed from package, use --prune to delete)`);
|
|
220
225
|
}
|
|
221
226
|
}
|
|
227
|
+
for (const line of scriptLines([...(result.scriptsAdded || []), ...(result.scriptsKept || [])])) {
|
|
228
|
+
console.log(line);
|
|
229
|
+
}
|
|
222
230
|
});
|
|
223
231
|
for (const fixed of fixedScripts) {
|
|
224
232
|
console.log(`\n\x1b[32m✓\x1b[0m package.json script "${fixed.name}" rewritten: ${fixed.command} → ${fixed.rewritten}`);
|
|
@@ -227,6 +235,27 @@ function runUpdate(capabilityName, options) {
|
|
|
227
235
|
console.log(`\n${(0, stale_scripts_1.formatStaleScriptWarning)(remainingStale)}`);
|
|
228
236
|
}
|
|
229
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* How the report shows the npm scripts a capability declares: an added one reads
|
|
240
|
+
* like a new file, and a kept one shows the command the capability ships, so a
|
|
241
|
+
* project whose own command has fallen behind can see that it has. The label
|
|
242
|
+
* claims only that the commands differ: with no record of the command boop wrote,
|
|
243
|
+
* a command an older capability version shipped looks just like a customisation.
|
|
244
|
+
*/
|
|
245
|
+
function scriptLines(actions) {
|
|
246
|
+
const lines = [];
|
|
247
|
+
for (const action of actions) {
|
|
248
|
+
if (action.action === 'add') {
|
|
249
|
+
lines.push(` \x1b[32m+\x1b[0m npm run ${action.name} (new script)`);
|
|
250
|
+
lines.push(instructionLine(action.command));
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
lines.push(` \x1b[33m◐\x1b[0m npm run ${action.name} (your command differs; kept)`);
|
|
254
|
+
lines.push(instructionLine(`this capability ships: ${action.command}`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return lines;
|
|
258
|
+
}
|
|
230
259
|
/**
|
|
231
260
|
* A reference page or code file the project edited, and boop has not, is doing
|
|
232
261
|
* what those files are for, so the report does not list it.
|
package/dist/lib/execute.js
CHANGED
|
@@ -50,6 +50,7 @@ const project_1 = require("./project");
|
|
|
50
50
|
const env_1 = require("./env");
|
|
51
51
|
const rewire_1 = require("./rewire");
|
|
52
52
|
const safe_io_1 = require("./safe-io");
|
|
53
|
+
const script_merge_1 = require("./script-merge");
|
|
53
54
|
/**
|
|
54
55
|
* Execute all tasks in a manifest. Returns the result with per-task status.
|
|
55
56
|
*/
|
|
@@ -110,19 +111,9 @@ function executeTask(task, manifest) {
|
|
|
110
111
|
break;
|
|
111
112
|
}
|
|
112
113
|
case 'merge-scripts': {
|
|
114
|
+
// One answer to "does this script go in", shared with `boop update`
|
|
113
115
|
const data = task.data;
|
|
114
|
-
|
|
115
|
-
if (fs.existsSync(pkgPath)) {
|
|
116
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
117
|
-
if (!pkg.scripts)
|
|
118
|
-
pkg.scripts = {};
|
|
119
|
-
for (const [scriptName, scriptDef] of Object.entries(data.scripts)) {
|
|
120
|
-
if (!(scriptName in pkg.scripts)) {
|
|
121
|
-
pkg.scripts[scriptName] = scriptDef.cmd;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
125
|
-
}
|
|
116
|
+
(0, script_merge_1.applyScriptUpdates)(targetDir, (0, script_merge_1.planScriptUpdates)(data.scripts, (0, script_merge_1.readProjectScripts)(targetDir)));
|
|
126
117
|
break;
|
|
127
118
|
}
|
|
128
119
|
case 'link-package': {
|
package/dist/lib/scaffold.d.ts
CHANGED
|
@@ -70,17 +70,47 @@ export declare function copyCapabilityFiles(targetDir: string, capabilityName: s
|
|
|
70
70
|
*/
|
|
71
71
|
export declare function linkSkillsToClaude(targetDir: string, only?: Iterable<string>): string[];
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* `
|
|
76
|
-
*
|
|
73
|
+
* The names of the skill folders under `<targetDir>/skills/` that boop installed.
|
|
74
|
+
*
|
|
75
|
+
* A project's `skills/` is not all boop's: a project writes skills of its own
|
|
76
|
+
* beside the ones its capabilities ship, and those folders are already entirely
|
|
77
|
+
* the project's file. Two records say which are boop's, and the union is taken
|
|
78
|
+
* because neither is complete on its own:
|
|
79
|
+
*
|
|
80
|
+
* - the `fileHashes` a capability stored in `.boop/config.json`, which is the
|
|
81
|
+
* record of what was actually copied into this project;
|
|
82
|
+
* - the skill placements of an installed capability that has no stored hashes —
|
|
83
|
+
* a manifest written before `capabilityMeta` existed, or one whose hash
|
|
84
|
+
* storage was skipped.
|
|
85
|
+
*
|
|
86
|
+
* Returns null when the directory holds no `.boop/config.json`, i.e. nothing
|
|
87
|
+
* here is a boop project and no folder can be attributed either way.
|
|
88
|
+
*/
|
|
89
|
+
export declare function installedSkillNames(targetDir: string, searchPaths?: string[]): Set<string> | null;
|
|
90
|
+
/**
|
|
91
|
+
* Give every boop-installed skill folder under `<targetDir>/skills/` that lacks
|
|
92
|
+
* one a `PROJECT.md`, where the project records how it applies the skill. Each
|
|
93
|
+
* boop `SKILL.md` opens by pointing at it; the file being present is the
|
|
94
|
+
* backstop for an agent that skipped the pointer.
|
|
95
|
+
*
|
|
96
|
+
* Only a folder boop installed gets one. A skill the project wrote itself is
|
|
97
|
+
* already the project's file outright, so a `PROJECT.md` beside it separates
|
|
98
|
+
* nothing — and because this runs on every init, add and update, a project
|
|
99
|
+
* could never delete the stub it left there.
|
|
100
|
+
*
|
|
101
|
+
* For the same reason it takes back a stub an earlier boop wrote beside a
|
|
102
|
+
* project-owned skill, but only while the file's bytes are still exactly the
|
|
103
|
+
* template's: anything the project typed into a `PROJECT.md` keeps it.
|
|
77
104
|
*
|
|
78
105
|
* The file is the project's: boop never hashes it, so `boop update` can never
|
|
79
106
|
* report it, and an existing one is never opened for writing.
|
|
80
107
|
*
|
|
81
|
-
* Returns the relative paths of the files created.
|
|
108
|
+
* Returns the relative paths of the files created and of the stubs removed.
|
|
82
109
|
*/
|
|
83
|
-
export declare function createSkillProjectFiles(targetDir: string
|
|
110
|
+
export declare function createSkillProjectFiles(targetDir: string, searchPaths?: string[]): {
|
|
111
|
+
created: string[];
|
|
112
|
+
removed: string[];
|
|
113
|
+
};
|
|
84
114
|
/**
|
|
85
115
|
* Whether a skill's PROJECT.md has rules under "Offered to boop" — anything in
|
|
86
116
|
* that section beyond the template's own explanation of it.
|
package/dist/lib/scaffold.js
CHANGED
|
@@ -44,6 +44,7 @@ exports.hashFile = hashFile;
|
|
|
44
44
|
exports.hashContent = hashContent;
|
|
45
45
|
exports.copyCapabilityFiles = copyCapabilityFiles;
|
|
46
46
|
exports.linkSkillsToClaude = linkSkillsToClaude;
|
|
47
|
+
exports.installedSkillNames = installedSkillNames;
|
|
47
48
|
exports.createSkillProjectFiles = createSkillProjectFiles;
|
|
48
49
|
exports.hasOfferedRules = hasOfferedRules;
|
|
49
50
|
exports.readSkillProjectTemplate = readSkillProjectTemplate;
|
|
@@ -60,6 +61,7 @@ const config_1 = require("./config");
|
|
|
60
61
|
const capability_1 = require("./capability");
|
|
61
62
|
const safe_io_1 = require("./safe-io");
|
|
62
63
|
const install_layout_1 = require("./install-layout");
|
|
64
|
+
const project_1 = require("./project");
|
|
63
65
|
/**
|
|
64
66
|
* Render a single Handlebars template string with variables.
|
|
65
67
|
*
|
|
@@ -254,34 +256,101 @@ function linkSkillsToClaude(targetDir, only) {
|
|
|
254
256
|
return linked;
|
|
255
257
|
}
|
|
256
258
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* `
|
|
260
|
-
*
|
|
259
|
+
* The names of the skill folders under `<targetDir>/skills/` that boop installed.
|
|
260
|
+
*
|
|
261
|
+
* A project's `skills/` is not all boop's: a project writes skills of its own
|
|
262
|
+
* beside the ones its capabilities ship, and those folders are already entirely
|
|
263
|
+
* the project's file. Two records say which are boop's, and the union is taken
|
|
264
|
+
* because neither is complete on its own:
|
|
265
|
+
*
|
|
266
|
+
* - the `fileHashes` a capability stored in `.boop/config.json`, which is the
|
|
267
|
+
* record of what was actually copied into this project;
|
|
268
|
+
* - the skill placements of an installed capability that has no stored hashes —
|
|
269
|
+
* a manifest written before `capabilityMeta` existed, or one whose hash
|
|
270
|
+
* storage was skipped.
|
|
271
|
+
*
|
|
272
|
+
* Returns null when the directory holds no `.boop/config.json`, i.e. nothing
|
|
273
|
+
* here is a boop project and no folder can be attributed either way.
|
|
274
|
+
*/
|
|
275
|
+
function installedSkillNames(targetDir, searchPaths) {
|
|
276
|
+
const manifest = (0, project_1.readProjectManifest)(targetDir);
|
|
277
|
+
if (!manifest)
|
|
278
|
+
return null;
|
|
279
|
+
const names = new Set();
|
|
280
|
+
const addDest = (dest) => {
|
|
281
|
+
// Stored destinations are POSIX, but tolerate a manifest written on Windows.
|
|
282
|
+
const [top, skillName] = dest.split(/[\\/]/);
|
|
283
|
+
if (top === 'skills' && skillName)
|
|
284
|
+
names.add(skillName);
|
|
285
|
+
};
|
|
286
|
+
const meta = manifest.capabilityMeta ?? {};
|
|
287
|
+
for (const entry of Object.values(meta)) {
|
|
288
|
+
for (const dest of Object.keys(entry?.fileHashes ?? {}))
|
|
289
|
+
addDest(dest);
|
|
290
|
+
}
|
|
291
|
+
// Walking a package's files is the expensive half, so it runs only for a
|
|
292
|
+
// capability whose hashes are missing — where it is the only record left.
|
|
293
|
+
for (const capability of manifest.capabilities ?? []) {
|
|
294
|
+
if (Object.keys(meta[capability]?.fileHashes ?? {}).length > 0)
|
|
295
|
+
continue;
|
|
296
|
+
for (const placement of (0, install_layout_1.capabilityPlacements)(capability, searchPaths ?? [targetDir])) {
|
|
297
|
+
if (placement.kind === 'skill')
|
|
298
|
+
addDest(placement.dest);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return names;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Give every boop-installed skill folder under `<targetDir>/skills/` that lacks
|
|
305
|
+
* one a `PROJECT.md`, where the project records how it applies the skill. Each
|
|
306
|
+
* boop `SKILL.md` opens by pointing at it; the file being present is the
|
|
307
|
+
* backstop for an agent that skipped the pointer.
|
|
308
|
+
*
|
|
309
|
+
* Only a folder boop installed gets one. A skill the project wrote itself is
|
|
310
|
+
* already the project's file outright, so a `PROJECT.md` beside it separates
|
|
311
|
+
* nothing — and because this runs on every init, add and update, a project
|
|
312
|
+
* could never delete the stub it left there.
|
|
313
|
+
*
|
|
314
|
+
* For the same reason it takes back a stub an earlier boop wrote beside a
|
|
315
|
+
* project-owned skill, but only while the file's bytes are still exactly the
|
|
316
|
+
* template's: anything the project typed into a `PROJECT.md` keeps it.
|
|
261
317
|
*
|
|
262
318
|
* The file is the project's: boop never hashes it, so `boop update` can never
|
|
263
319
|
* report it, and an existing one is never opened for writing.
|
|
264
320
|
*
|
|
265
|
-
* Returns the relative paths of the files created.
|
|
321
|
+
* Returns the relative paths of the files created and of the stubs removed.
|
|
266
322
|
*/
|
|
267
|
-
function createSkillProjectFiles(targetDir) {
|
|
323
|
+
function createSkillProjectFiles(targetDir, searchPaths) {
|
|
268
324
|
const skillsDir = path.join(targetDir, 'skills');
|
|
269
325
|
if (!fs.existsSync(skillsDir))
|
|
270
|
-
return [];
|
|
326
|
+
return { created: [], removed: [] };
|
|
327
|
+
const installed = installedSkillNames(targetDir, searchPaths);
|
|
328
|
+
if (installed === null)
|
|
329
|
+
return { created: [], removed: [] };
|
|
271
330
|
const template = readSkillProjectTemplate();
|
|
272
331
|
const created = [];
|
|
332
|
+
const removed = [];
|
|
273
333
|
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
274
334
|
// A symlinked skill folder is someone else's. In the boop repository each one
|
|
275
335
|
// points into a capability package, from which a PROJECT.md would ship to every project.
|
|
276
336
|
if (!entry.isDirectory())
|
|
277
337
|
continue;
|
|
278
338
|
const filePath = path.join(skillsDir, entry.name, 'PROJECT.md');
|
|
279
|
-
|
|
339
|
+
const relPath = path.join('skills', entry.name, 'PROJECT.md');
|
|
340
|
+
const existing = fs.lstatSync(filePath, { throwIfNoEntry: false });
|
|
341
|
+
if (!installed.has(entry.name)) {
|
|
342
|
+
if (existing?.isFile() && fs.readFileSync(filePath, 'utf-8') === template) {
|
|
343
|
+
fs.unlinkSync(filePath);
|
|
344
|
+
removed.push(relPath);
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (existing)
|
|
280
349
|
continue;
|
|
281
350
|
fs.writeFileSync(filePath, template, { flag: 'wx' });
|
|
282
|
-
created.push(
|
|
351
|
+
created.push(relPath);
|
|
283
352
|
}
|
|
284
|
-
return created;
|
|
353
|
+
return { created, removed };
|
|
285
354
|
}
|
|
286
355
|
/**
|
|
287
356
|
* Whether a skill's PROJECT.md has rules under "Offered to boop" — anything in
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge the npm scripts a capability declares into a project's package.json
|
|
3
|
+
* during `boop update`.
|
|
4
|
+
*
|
|
5
|
+
* The case this exists for (boop-ldou): a capability ships a new command as two
|
|
6
|
+
* things — a file under `scaffold/`, and a `scripts` entry in `capability.json`.
|
|
7
|
+
* `boop add` merges those scripts at install time (see `merge-scripts` in
|
|
8
|
+
* lib/execute.ts), but `boop update` only ever copied files, so an upgraded
|
|
9
|
+
* project received `src/scripts/migrate-credentials.ts` and no way to run it by
|
|
10
|
+
* name. Every document, post-install note and status hint that said
|
|
11
|
+
* `npm run credentials:migrate` was right for a freshly scaffolded project and
|
|
12
|
+
* wrong for every upgraded one — precisely the projects a migration tool is for.
|
|
13
|
+
*
|
|
14
|
+
* The merge is additive, like `boop add`'s: a script is written only when the
|
|
15
|
+
* project has no script of that name. A name the project already uses is left
|
|
16
|
+
* exactly as it stands, because a command somebody customised is theirs, and
|
|
17
|
+
* clobbering it would be worse than leaving it alone — the same restraint
|
|
18
|
+
* `rewriteStaleRunnerScripts` shows when a command has changed underneath it.
|
|
19
|
+
* What boop does do is say so, so a kept command that no longer matches what the
|
|
20
|
+
* capability ships is visible rather than silent.
|
|
21
|
+
*/
|
|
22
|
+
import { CapabilityScript } from './config';
|
|
23
|
+
export interface ScriptUpdateAction {
|
|
24
|
+
/** The script's key in package.json, e.g. `credentials:migrate`. */
|
|
25
|
+
name: string;
|
|
26
|
+
/** The command the capability declares. */
|
|
27
|
+
command: string;
|
|
28
|
+
/** `add` writes it; `keep` leaves the project's own command in place. */
|
|
29
|
+
action: 'add' | 'keep';
|
|
30
|
+
/** Why this action was chosen. */
|
|
31
|
+
reason: string;
|
|
32
|
+
/** The command the project has today, when there is one. */
|
|
33
|
+
currentCommand?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pure: given what a capability declares and what the project's package.json
|
|
37
|
+
* holds, decide what happens to each script.
|
|
38
|
+
*
|
|
39
|
+
* A script whose command already matches what the capability ships is not
|
|
40
|
+
* returned at all — there is nothing to do and nothing worth reporting.
|
|
41
|
+
*/
|
|
42
|
+
export declare function planScriptUpdates(declared: Record<string, CapabilityScript> | undefined, projectScripts: Record<string, string> | undefined): ScriptUpdateAction[];
|
|
43
|
+
/**
|
|
44
|
+
* Write the `add` actions into the project's package.json, and return the ones
|
|
45
|
+
* actually written.
|
|
46
|
+
*
|
|
47
|
+
* Re-reads the file rather than trusting the plan: an action is applied only
|
|
48
|
+
* while the name is still free, so a script written between planning and here
|
|
49
|
+
* survives. A missing or unreadable package.json is not an error — scaffold
|
|
50
|
+
* files have already been delivered by the time this runs, and failing the whole
|
|
51
|
+
* update over the script merge would be worse than reporting nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare function applyScriptUpdates(targetDir: string, actions: ScriptUpdateAction[]): ScriptUpdateAction[];
|
|
54
|
+
/** The project's package.json scripts, or undefined when there is no readable one. */
|
|
55
|
+
export declare function readProjectScripts(targetDir: string): Record<string, string> | undefined;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Merge the npm scripts a capability declares into a project's package.json
|
|
4
|
+
* during `boop update`.
|
|
5
|
+
*
|
|
6
|
+
* The case this exists for (boop-ldou): a capability ships a new command as two
|
|
7
|
+
* things — a file under `scaffold/`, and a `scripts` entry in `capability.json`.
|
|
8
|
+
* `boop add` merges those scripts at install time (see `merge-scripts` in
|
|
9
|
+
* lib/execute.ts), but `boop update` only ever copied files, so an upgraded
|
|
10
|
+
* project received `src/scripts/migrate-credentials.ts` and no way to run it by
|
|
11
|
+
* name. Every document, post-install note and status hint that said
|
|
12
|
+
* `npm run credentials:migrate` was right for a freshly scaffolded project and
|
|
13
|
+
* wrong for every upgraded one — precisely the projects a migration tool is for.
|
|
14
|
+
*
|
|
15
|
+
* The merge is additive, like `boop add`'s: a script is written only when the
|
|
16
|
+
* project has no script of that name. A name the project already uses is left
|
|
17
|
+
* exactly as it stands, because a command somebody customised is theirs, and
|
|
18
|
+
* clobbering it would be worse than leaving it alone — the same restraint
|
|
19
|
+
* `rewriteStaleRunnerScripts` shows when a command has changed underneath it.
|
|
20
|
+
* What boop does do is say so, so a kept command that no longer matches what the
|
|
21
|
+
* capability ships is visible rather than silent.
|
|
22
|
+
*/
|
|
23
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
26
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
27
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(o, k2, desc);
|
|
30
|
+
}) : (function(o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
o[k2] = m[k];
|
|
33
|
+
}));
|
|
34
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
35
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
36
|
+
}) : function(o, v) {
|
|
37
|
+
o["default"] = v;
|
|
38
|
+
});
|
|
39
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
40
|
+
var ownKeys = function(o) {
|
|
41
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
42
|
+
var ar = [];
|
|
43
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
44
|
+
return ar;
|
|
45
|
+
};
|
|
46
|
+
return ownKeys(o);
|
|
47
|
+
};
|
|
48
|
+
return function (mod) {
|
|
49
|
+
if (mod && mod.__esModule) return mod;
|
|
50
|
+
var result = {};
|
|
51
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
52
|
+
__setModuleDefault(result, mod);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.planScriptUpdates = planScriptUpdates;
|
|
58
|
+
exports.applyScriptUpdates = applyScriptUpdates;
|
|
59
|
+
exports.readProjectScripts = readProjectScripts;
|
|
60
|
+
const fs = __importStar(require("fs"));
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
/**
|
|
63
|
+
* Pure: given what a capability declares and what the project's package.json
|
|
64
|
+
* holds, decide what happens to each script.
|
|
65
|
+
*
|
|
66
|
+
* A script whose command already matches what the capability ships is not
|
|
67
|
+
* returned at all — there is nothing to do and nothing worth reporting.
|
|
68
|
+
*/
|
|
69
|
+
function planScriptUpdates(declared, projectScripts) {
|
|
70
|
+
if (!declared)
|
|
71
|
+
return [];
|
|
72
|
+
const actions = [];
|
|
73
|
+
for (const [name, script] of Object.entries(declared)) {
|
|
74
|
+
const command = typeof script === 'string' ? script : script?.cmd;
|
|
75
|
+
if (!command)
|
|
76
|
+
continue;
|
|
77
|
+
const currentCommand = projectScripts?.[name];
|
|
78
|
+
if (currentCommand === undefined) {
|
|
79
|
+
actions.push({ name, command, action: 'add', reason: 'New script from capability' });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (currentCommand === command)
|
|
83
|
+
continue;
|
|
84
|
+
actions.push({
|
|
85
|
+
name,
|
|
86
|
+
command,
|
|
87
|
+
action: 'keep',
|
|
88
|
+
reason: 'Project has its own command for this script',
|
|
89
|
+
currentCommand,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return actions;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Write the `add` actions into the project's package.json, and return the ones
|
|
96
|
+
* actually written.
|
|
97
|
+
*
|
|
98
|
+
* Re-reads the file rather than trusting the plan: an action is applied only
|
|
99
|
+
* while the name is still free, so a script written between planning and here
|
|
100
|
+
* survives. A missing or unreadable package.json is not an error — scaffold
|
|
101
|
+
* files have already been delivered by the time this runs, and failing the whole
|
|
102
|
+
* update over the script merge would be worse than reporting nothing.
|
|
103
|
+
*/
|
|
104
|
+
function applyScriptUpdates(targetDir, actions) {
|
|
105
|
+
const additions = actions.filter(a => a.action === 'add');
|
|
106
|
+
if (additions.length === 0)
|
|
107
|
+
return [];
|
|
108
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
109
|
+
if (!fs.existsSync(pkgPath))
|
|
110
|
+
return [];
|
|
111
|
+
let pkg;
|
|
112
|
+
try {
|
|
113
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
if (!pkg.scripts)
|
|
119
|
+
pkg.scripts = {};
|
|
120
|
+
const applied = [];
|
|
121
|
+
for (const action of additions) {
|
|
122
|
+
if (action.name in pkg.scripts)
|
|
123
|
+
continue;
|
|
124
|
+
pkg.scripts[action.name] = action.command;
|
|
125
|
+
applied.push(action);
|
|
126
|
+
}
|
|
127
|
+
if (applied.length > 0) {
|
|
128
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
129
|
+
}
|
|
130
|
+
return applied;
|
|
131
|
+
}
|
|
132
|
+
/** The project's package.json scripts, or undefined when there is no readable one. */
|
|
133
|
+
function readProjectScripts(targetDir) {
|
|
134
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
135
|
+
if (!fs.existsSync(pkgPath))
|
|
136
|
+
return undefined;
|
|
137
|
+
try {
|
|
138
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
139
|
+
return pkg.scripts;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* Jest never recycles, and a long suite dies as that process's heap grows.
|
|
9
9
|
*
|
|
10
10
|
* `boop update` cannot simply fix those projects. Scripts are merged only when
|
|
11
|
-
* absent (see
|
|
12
|
-
* clobbering a script somebody customised would be
|
|
11
|
+
* absent (see lib/script-merge.ts, which both `boop add` and `boop update` use),
|
|
12
|
+
* which is the right default — clobbering a script somebody customised would be
|
|
13
|
+
* worse than leaving it stale.
|
|
13
14
|
* So the old command survives the update, and because a CLI `--runInBand` is the
|
|
14
15
|
* first and unconditional branch of Jest's `shouldRunInBand()`, it overrides the
|
|
15
16
|
* new config entirely. That is the state worth warning about: the project now has
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* Jest never recycles, and a long suite dies as that process's heap grows.
|
|
10
10
|
*
|
|
11
11
|
* `boop update` cannot simply fix those projects. Scripts are merged only when
|
|
12
|
-
* absent (see
|
|
13
|
-
* clobbering a script somebody customised would be
|
|
12
|
+
* absent (see lib/script-merge.ts, which both `boop add` and `boop update` use),
|
|
13
|
+
* which is the right default — clobbering a script somebody customised would be
|
|
14
|
+
* worse than leaving it stale.
|
|
14
15
|
* So the old command survives the update, and because a CLI `--runInBand` is the
|
|
15
16
|
* first and unconditional branch of Jest's `shouldRunInBand()`, it overrides the
|
|
16
17
|
* new config entirely. That is the state worth warning about: the project now has
|
package/dist/lib/update.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* local modifications, then selectively updates from the capability package.
|
|
6
6
|
*/
|
|
7
7
|
import { BoopManifest } from './project';
|
|
8
|
+
import { ScriptUpdateAction } from './script-merge';
|
|
8
9
|
export type FileState = 'match' | 'modified' | 'missing';
|
|
9
10
|
/**
|
|
10
11
|
* How a locally modified file stands against the base (what boop shipped last)
|
|
@@ -53,6 +54,8 @@ export interface UpdatePlan {
|
|
|
53
54
|
newVersion: string;
|
|
54
55
|
/** Per-file actions */
|
|
55
56
|
actions: FileUpdateAction[];
|
|
57
|
+
/** What happens to each npm script the capability declares */
|
|
58
|
+
scriptActions: ScriptUpdateAction[];
|
|
56
59
|
/** Number of files unchanged in both package and locally */
|
|
57
60
|
unchangedCount: number;
|
|
58
61
|
/** True if already up to date */
|
|
@@ -71,6 +74,10 @@ export interface UpdateResult {
|
|
|
71
74
|
pruned: string[];
|
|
72
75
|
/** Files migrated from old flat format to new folder format */
|
|
73
76
|
migrated: string[];
|
|
77
|
+
/** npm scripts the capability declares that this update wrote into package.json */
|
|
78
|
+
scriptsAdded: ScriptUpdateAction[];
|
|
79
|
+
/** npm scripts left as the project has them, because it has its own command */
|
|
80
|
+
scriptsKept: ScriptUpdateAction[];
|
|
74
81
|
/** Count of files unchanged in both package and locally */
|
|
75
82
|
unchangedCount: number;
|
|
76
83
|
/** What to do about the files this update skipped or replaced, where the report says anything */
|