@khanhcan148/mk 0.1.24 → 0.1.26
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/bin/mk.js +1 -0
- package/package.json +1 -1
- package/scripts/convert-agents-to-codex.js +12 -4
- package/scripts/convert-hooks-to-codex.js +6 -1
- package/scripts/convert-skills-to-codex.js +14 -5
- package/src/commands/codex.js +30 -10
- package/src/commands/update.js +41 -21
- package/src/lib/copy.js +21 -13
package/bin/mk.js
CHANGED
|
@@ -27,6 +27,7 @@ program.command('update')
|
|
|
27
27
|
.description('Update kit files to the latest version, preserving user modifications')
|
|
28
28
|
.option('--force', 'Overwrite user-modified files without warning')
|
|
29
29
|
.option('--global', 'Update global installation in ~/.claude/')
|
|
30
|
+
.option('-v, --verbose', 'Print per-file changes and routine warnings')
|
|
30
31
|
.action((options) => updateAction(options));
|
|
31
32
|
|
|
32
33
|
program.command('remove')
|
package/package.json
CHANGED
|
@@ -110,6 +110,11 @@ try {
|
|
|
110
110
|
process.exit(1);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// Verbose flag: MK_VERBOSE=1 enables per-agent progress lines and IAC-1 warnings.
|
|
114
|
+
// When spawned by runCodexConversion, the parent passes MK_VERBOSE based on its
|
|
115
|
+
// verbose option so the child inherits the same quietness setting.
|
|
116
|
+
const VERBOSE = process.env.MK_VERBOSE === '1';
|
|
117
|
+
|
|
113
118
|
// ---------------------------------------------------------------------------
|
|
114
119
|
// IAC-1 patterns: Claude-only constructs to flag in agent bodies
|
|
115
120
|
// ---------------------------------------------------------------------------
|
|
@@ -127,6 +132,7 @@ const IAC1_PATTERNS = [
|
|
|
127
132
|
* @param {string} body Raw Markdown body (frontmatter stripped)
|
|
128
133
|
*/
|
|
129
134
|
function checkIac1(agentName, body) {
|
|
135
|
+
if (!VERBOSE) return;
|
|
130
136
|
const lines = body.split('\n');
|
|
131
137
|
for (const { pattern, label } of IAC1_PATTERNS) {
|
|
132
138
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
@@ -341,10 +347,12 @@ for (const mdPath of mdFiles) {
|
|
|
341
347
|
}
|
|
342
348
|
}
|
|
343
349
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
350
|
+
if (VERBOSE) {
|
|
351
|
+
process.stdout.write(
|
|
352
|
+
`[convert-agents-to-codex] Done. ${successCount} converted, ${errorCount} errors.\n`
|
|
353
|
+
);
|
|
354
|
+
process.stdout.write(`[convert-agents-to-codex] Output: ${outputDir}\n`);
|
|
355
|
+
}
|
|
348
356
|
|
|
349
357
|
if (errors.length > 0) {
|
|
350
358
|
process.stderr.write(`[convert-agents-to-codex] Errors:\n${errors.join('\n')}\n`);
|
|
@@ -154,6 +154,9 @@ function buildToml() {
|
|
|
154
154
|
export async function convertHooksToCodex(opts = {}) {
|
|
155
155
|
const projectRoot = opts.projectRoot ? resolve(opts.projectRoot) : PACKAGE_ROOT;
|
|
156
156
|
const outputDir = opts.outputDir ? resolve(opts.outputDir) : join(projectRoot, '.codex');
|
|
157
|
+
// verbose: caller passes explicit boolean; fallback to MK_VERBOSE env var for
|
|
158
|
+
// the case where this module is imported by a spawned child process.
|
|
159
|
+
const verbose = opts.verbose ?? process.env.MK_VERBOSE === '1';
|
|
157
160
|
|
|
158
161
|
const warnings = [];
|
|
159
162
|
const dropped = [];
|
|
@@ -163,7 +166,9 @@ export async function convertHooksToCodex(opts = {}) {
|
|
|
163
166
|
const msg = `[convert-hooks-to-codex] WARNING: Dropping ${name} (${source} event has no Codex equivalent)`;
|
|
164
167
|
warnings.push(msg);
|
|
165
168
|
dropped.push(name);
|
|
166
|
-
|
|
169
|
+
if (verbose) {
|
|
170
|
+
process.stderr.write(msg + '\n');
|
|
171
|
+
}
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
// Count emitted hooks
|
|
@@ -88,6 +88,11 @@ for (let i = 0; i < args.length; i++) {
|
|
|
88
88
|
|
|
89
89
|
const modelMap = loadModelMap(modelMapPath);
|
|
90
90
|
|
|
91
|
+
// Verbose flag: MK_VERBOSE=1 enables per-skill progress lines.
|
|
92
|
+
// When spawned by runCodexConversion, the parent passes MK_VERBOSE based on its
|
|
93
|
+
// verbose option so the child inherits the same quietness setting.
|
|
94
|
+
const VERBOSE = process.env.MK_VERBOSE === '1';
|
|
95
|
+
|
|
91
96
|
const kitRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
92
97
|
const inputDir = inputDirArg || join(kitRoot, '.claude', 'skills');
|
|
93
98
|
const outputDir = outputDirArg || join(kitRoot, '.codex', 'skills');
|
|
@@ -219,7 +224,9 @@ let errorCount = 0;
|
|
|
219
224
|
const errors = [];
|
|
220
225
|
|
|
221
226
|
function logInfo(msg) {
|
|
222
|
-
|
|
227
|
+
if (VERBOSE) {
|
|
228
|
+
process.stderr.write(`${LOG_PREFIX} ${msg}\n`);
|
|
229
|
+
}
|
|
223
230
|
}
|
|
224
231
|
|
|
225
232
|
function logError(msg) {
|
|
@@ -336,10 +343,12 @@ for (const dirent of skillDirs) {
|
|
|
336
343
|
}
|
|
337
344
|
}
|
|
338
345
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
346
|
+
if (VERBOSE) {
|
|
347
|
+
process.stdout.write(
|
|
348
|
+
`${LOG_PREFIX} Done. ${successCount} converted, ${errorCount} errors.\n`
|
|
349
|
+
);
|
|
350
|
+
process.stdout.write(`${LOG_PREFIX} Output: ${outputDir}\n`);
|
|
351
|
+
}
|
|
343
352
|
|
|
344
353
|
if (errors.length > 0) {
|
|
345
354
|
process.stderr.write(`${LOG_PREFIX} Errors:\n${errors.join('\n')}\n`);
|
package/src/commands/codex.js
CHANGED
|
@@ -158,9 +158,18 @@ function isDirectory(p) {
|
|
|
158
158
|
* path can never traverse outside its parent.
|
|
159
159
|
* @param {string} [options.output] - Override for agents output dir
|
|
160
160
|
* @param {string} [options.modelMap] - Path to custom model-map JSON
|
|
161
|
+
* @param {boolean} [options.verbose=true] - When false, suppresses the per-directory
|
|
162
|
+
* progress lines (`Converting`, `Mirroring`,
|
|
163
|
+
* `✓ codex conversion complete`) and the
|
|
164
|
+
* hook-drop warning. `console.error` calls
|
|
165
|
+
* stay loud unconditionally. Defaults to `true`
|
|
166
|
+
* to preserve direct `mk codex` ergonomics;
|
|
167
|
+
* `maybeSyncCodex` passes `false` so the codex
|
|
168
|
+
* sync inherits the host run's quietness.
|
|
161
169
|
* @returns {Promise<{ exitCode: number, hooksResult?: object, errors: string[] }>}
|
|
162
170
|
*/
|
|
163
171
|
export async function runCodexConversion(options = {}) {
|
|
172
|
+
const { verbose = true } = options;
|
|
164
173
|
// S1: Reject `..` segments AFTER resolve to catch encoded traversal attempts
|
|
165
174
|
// (e.g. "foo/../../../etc"). A full homedir-bound would break legitimate
|
|
166
175
|
// `mk codex --cwd /tmp/…` invocations, so we use the lighter ..‑segment check.
|
|
@@ -235,7 +244,7 @@ export async function runCodexConversion(options = {}) {
|
|
|
235
244
|
}
|
|
236
245
|
|
|
237
246
|
// Emit progress lines upfront (before awaiting) so the user sees intent immediately.
|
|
238
|
-
if (isDirectory(skillsDir)) {
|
|
247
|
+
if (verbose && isDirectory(skillsDir)) {
|
|
239
248
|
console.log(chalk.cyan(`Converting ${skillsDir}`));
|
|
240
249
|
console.log(chalk.cyan(` → ${skillsOut}`));
|
|
241
250
|
}
|
|
@@ -244,8 +253,15 @@ export async function runCodexConversion(options = {}) {
|
|
|
244
253
|
if (options.modelMap) {
|
|
245
254
|
args.push('--model-map', resolve(options.modelMap));
|
|
246
255
|
}
|
|
247
|
-
|
|
248
|
-
|
|
256
|
+
if (verbose) {
|
|
257
|
+
console.log(chalk.cyan(`Converting ${agentsDir}`));
|
|
258
|
+
console.log(chalk.cyan(` → ${agentsOut}`));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Propagate verbose flag to child converter processes via MK_VERBOSE env var.
|
|
262
|
+
// Child processes use { stdio: 'inherit' } so their stdout/stderr bypass the
|
|
263
|
+
// parent-level verbose gate — the env var is the only way to suppress them.
|
|
264
|
+
const childEnv = { ...process.env, MK_VERBOSE: verbose ? '1' : '0' };
|
|
249
265
|
|
|
250
266
|
// Start both spawns concurrently.
|
|
251
267
|
const skillsPromise = isDirectory(skillsDir)
|
|
@@ -253,7 +269,7 @@ export async function runCodexConversion(options = {}) {
|
|
|
253
269
|
const child = spawn(
|
|
254
270
|
process.execPath,
|
|
255
271
|
[SKILLS_CONVERTER_SCRIPT, '--input', skillsDir, '--output', skillsOut],
|
|
256
|
-
{ stdio: 'inherit' }
|
|
272
|
+
{ stdio: 'inherit', env: childEnv }
|
|
257
273
|
);
|
|
258
274
|
child.on('close', (exitCode) => resolveExit(exitCode ?? 1));
|
|
259
275
|
child.on('error', (err) => {
|
|
@@ -264,7 +280,7 @@ export async function runCodexConversion(options = {}) {
|
|
|
264
280
|
: Promise.resolve(0);
|
|
265
281
|
|
|
266
282
|
const agentsPromise = new Promise((resolveExit) => {
|
|
267
|
-
const child = spawn(process.execPath, args, { stdio: 'inherit' });
|
|
283
|
+
const child = spawn(process.execPath, args, { stdio: 'inherit', env: childEnv });
|
|
268
284
|
child.on('close', (exitCode) => resolveExit(exitCode ?? 1));
|
|
269
285
|
child.on('error', (err) => {
|
|
270
286
|
console.error(chalk.red(`error: failed to spawn converter: ${err.message}`));
|
|
@@ -288,20 +304,24 @@ export async function runCodexConversion(options = {}) {
|
|
|
288
304
|
// Uses mirrorDirWithRewrite so .claude/<subdir>/ refs in .md/.txt/.toml files
|
|
289
305
|
// are rewritten to .codex/<subdir>/ for Codex-runtime resolution.
|
|
290
306
|
if (isDirectory(workflowsDir)) {
|
|
291
|
-
|
|
292
|
-
|
|
307
|
+
if (verbose) {
|
|
308
|
+
console.log(chalk.cyan(`Mirroring ${workflowsDir}`));
|
|
309
|
+
console.log(chalk.cyan(` → ${workflowsOut}`));
|
|
310
|
+
}
|
|
293
311
|
mirrorDirWithRewrite(workflowsDir, workflowsOut);
|
|
294
312
|
}
|
|
295
313
|
|
|
296
|
-
const hooksResult = await convertHooksToCodex({ projectRoot, outputDir: codexRoot });
|
|
297
|
-
if (hooksResult.dropped.length > 0) {
|
|
314
|
+
const hooksResult = await convertHooksToCodex({ projectRoot, outputDir: codexRoot, verbose });
|
|
315
|
+
if (verbose && hooksResult.dropped.length > 0) {
|
|
298
316
|
console.warn(
|
|
299
317
|
chalk.yellow(
|
|
300
318
|
`Warning: dropped ${hooksResult.dropped.length} hooks (SubagentStart/TeammateIdle/TaskCompleted have no Codex equivalent)`
|
|
301
319
|
)
|
|
302
320
|
);
|
|
303
321
|
}
|
|
304
|
-
|
|
322
|
+
if (verbose) {
|
|
323
|
+
console.log(chalk.green('✓ codex conversion complete'));
|
|
324
|
+
}
|
|
305
325
|
return { exitCode: 0, hooksResult, errors };
|
|
306
326
|
}
|
|
307
327
|
|
package/src/commands/update.js
CHANGED
|
@@ -116,15 +116,18 @@ export function hasCodexInstall(codexDir) {
|
|
|
116
116
|
* Throws a tagged error (`.codexSyncFailure = true`) on conversion failure so
|
|
117
117
|
* callers can distinguish a codex-only problem from a core update failure.
|
|
118
118
|
*
|
|
119
|
-
* @param {{ projectRoot: string, codexRunner: Function }} opts
|
|
119
|
+
* @param {{ projectRoot: string, codexRunner: Function, verbose?: boolean }} opts
|
|
120
120
|
* @param {string} opts.projectRoot - Absolute path to the project root
|
|
121
121
|
* @param {Function} opts.codexRunner - DI-injectable; defaults to runCodexConversion
|
|
122
|
+
* @param {boolean} [opts.verbose=false] - When true, emits the "Syncing .codex/ mirror…" heartbeat
|
|
122
123
|
*/
|
|
123
|
-
export async function maybeSyncCodex({ projectRoot, codexRunner }) {
|
|
124
|
+
export async function maybeSyncCodex({ projectRoot, codexRunner, verbose = false }) {
|
|
124
125
|
const codexDir = join(projectRoot, '.codex');
|
|
125
126
|
if (!hasCodexInstall(codexDir)) return; // silent skip — no codex install detected
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
if (verbose) {
|
|
128
|
+
process.stdout.write(chalk.cyan('Syncing .codex/ mirror…\n'));
|
|
129
|
+
}
|
|
130
|
+
const result = await codexRunner({ cwd: projectRoot, verbose });
|
|
128
131
|
if (result.exitCode !== 0) {
|
|
129
132
|
const err = new Error(
|
|
130
133
|
'Codex sync failed after .claude/ update completed. Retry manually: `mk codex`'
|
|
@@ -163,7 +166,8 @@ async function defaultPromptUser(question) {
|
|
|
163
166
|
* targetDir?: string,
|
|
164
167
|
* manifestPath?: string,
|
|
165
168
|
* force?: boolean,
|
|
166
|
-
* version?: string - Kit version to store in manifest (defaults to pkg.version when omitted)
|
|
169
|
+
* version?: string, - Kit version to store in manifest (defaults to pkg.version when omitted)
|
|
170
|
+
* verbose?: boolean - When true, threads verbose=true to copyKitFiles and mergeSettingsJson
|
|
167
171
|
* }} params
|
|
168
172
|
* @returns {Promise<{ updated: string[], added: string[], removed: string[], conflicts: string[], unchanged: string[], upToDate: boolean }>}
|
|
169
173
|
*/
|
|
@@ -173,7 +177,8 @@ export async function runUpdate(params = {}) {
|
|
|
173
177
|
targetDir = resolveTargetDir({ global: false }),
|
|
174
178
|
manifestPath = resolveManifestPath({ global: false }),
|
|
175
179
|
force = false,
|
|
176
|
-
version: explicitVersion
|
|
180
|
+
version: explicitVersion,
|
|
181
|
+
verbose = false
|
|
177
182
|
} = params;
|
|
178
183
|
|
|
179
184
|
// Read existing manifest
|
|
@@ -187,7 +192,7 @@ export async function runUpdate(params = {}) {
|
|
|
187
192
|
// Derive project root from manifest scope (global: homedir, project: dirname(manifestPath))
|
|
188
193
|
const projectRoot = deriveProjectRoot(manifest, manifestPath);
|
|
189
194
|
const claudeRoot = resolve(join(projectRoot, '.claude'));
|
|
190
|
-
const sourceFileList = copyKitFiles(sourceDir, targetDir, { dryRun: true });
|
|
195
|
+
const sourceFileList = copyKitFiles(sourceDir, targetDir, { dryRun: true, verbose });
|
|
191
196
|
// Fix 11-12 (performance): Build a Map for O(1) lookups in applyCopy.
|
|
192
197
|
// Previously sourceFileList.find() in applyCopy was O(n) per call, causing O(n²)
|
|
193
198
|
// behaviour when many files need updating. The Map is built once in O(n).
|
|
@@ -241,7 +246,7 @@ export async function runUpdate(params = {}) {
|
|
|
241
246
|
// Always merge settings.json hooks even when kit files are unchanged.
|
|
242
247
|
// A user who installed before hooks were added (or whose settings.json was
|
|
243
248
|
// edited/reset) would otherwise never receive hook updates via `mk update`.
|
|
244
|
-
mergeSettingsJson(sourceDir, targetDir);
|
|
249
|
+
mergeSettingsJson(sourceDir, targetDir, { verbose });
|
|
245
250
|
// Files are unchanged but we may still need to record the release version.
|
|
246
251
|
// Without this, manifest.version stays at the old value and the next `mk update`
|
|
247
252
|
// will always report "Update available" even though nothing changed on disk.
|
|
@@ -369,7 +374,7 @@ export async function runUpdate(params = {}) {
|
|
|
369
374
|
}
|
|
370
375
|
|
|
371
376
|
// Merge settings.json hooks — additive merge, never overwrites user keys
|
|
372
|
-
mergeSettingsJson(sourceDir, targetDir);
|
|
377
|
+
mergeSettingsJson(sourceDir, targetDir, { verbose });
|
|
373
378
|
|
|
374
379
|
// Update manifest with new file map.
|
|
375
380
|
// Use explicitVersion when provided (e.g. release.version from updateAction);
|
|
@@ -393,7 +398,7 @@ export async function runUpdate(params = {}) {
|
|
|
393
398
|
* and applies a three-way diff. Falls back to the main-branch tarball if no
|
|
394
399
|
* release information is available.
|
|
395
400
|
*
|
|
396
|
-
* @param {{ force: boolean, global: boolean }} options
|
|
401
|
+
* @param {{ force: boolean, global: boolean, verbose?: boolean }} options
|
|
397
402
|
* @param {object} [deps] - Injected dependencies (for testing)
|
|
398
403
|
*/
|
|
399
404
|
export async function updateAction(options = {}, deps = {}) {
|
|
@@ -409,7 +414,8 @@ export async function updateAction(options = {}, deps = {}) {
|
|
|
409
414
|
// Injectable for tests — allows overriding resolved paths without touching CWD
|
|
410
415
|
manifestPath: injectedManifestPath,
|
|
411
416
|
targetDir: injectedTargetDir,
|
|
412
|
-
runCodexConversion: codexRunner = runCodexConversion
|
|
417
|
+
runCodexConversion: codexRunner = runCodexConversion,
|
|
418
|
+
runUpdate: runUpdateImpl = runUpdate
|
|
413
419
|
} = deps;
|
|
414
420
|
|
|
415
421
|
// Read local package version (used as fallback when manifest has no version)
|
|
@@ -425,6 +431,10 @@ export async function updateAction(options = {}, deps = {}) {
|
|
|
425
431
|
|
|
426
432
|
let tempDir = null;
|
|
427
433
|
try {
|
|
434
|
+
// MK_VERBOSE only honours the literal '1' to avoid CI surprises (e.g. 'true', 'false', '0').
|
|
435
|
+
// SECURITY: strict literal only — do not relax to truthy; coercion silently enables CI-break behaviour.
|
|
436
|
+
const verbose = options.verbose === true || process.env.MK_VERBOSE === '1';
|
|
437
|
+
|
|
428
438
|
process.stdout.write('Authenticating with GitHub...\n');
|
|
429
439
|
const token = await resolveAndLogin({
|
|
430
440
|
readStored: readToken,
|
|
@@ -471,8 +481,9 @@ export async function updateAction(options = {}, deps = {}) {
|
|
|
471
481
|
// The installed settings.json may be missing hooks if it was created
|
|
472
482
|
// before hooks were added to the kit or if it was manually edited.
|
|
473
483
|
// resolveSourceDir() points to the CLI package's own .claude/ — no download needed.
|
|
474
|
-
|
|
475
|
-
|
|
484
|
+
// Note: maybeSyncCodex is intentionally NOT called here — running codex conversion
|
|
485
|
+
// on every no-op invocation wastes wall-clock time with no benefit (PR #132 reversal).
|
|
486
|
+
mergeSettingsJson(resolveSourceDir(), targetDir, { verbose });
|
|
476
487
|
return;
|
|
477
488
|
}
|
|
478
489
|
|
|
@@ -511,7 +522,7 @@ export async function updateAction(options = {}, deps = {}) {
|
|
|
511
522
|
// Pass releaseVersion so runUpdate stores it in the manifest.
|
|
512
523
|
// When falling back to main branch (no release), releaseVersion is undefined and
|
|
513
524
|
// runUpdate falls back to pkg.version — which is the correct behaviour for that path.
|
|
514
|
-
const result = await
|
|
525
|
+
const result = await runUpdateImpl({ sourceDir, targetDir, manifestPath, force: options.force, version: releaseVersion, verbose });
|
|
515
526
|
|
|
516
527
|
if (result.upToDate) {
|
|
517
528
|
process.stdout.write(chalk.green('Already up to date.\n'));
|
|
@@ -524,26 +535,35 @@ export async function updateAction(options = {}, deps = {}) {
|
|
|
524
535
|
}
|
|
525
536
|
if (result.removed.length > 0) {
|
|
526
537
|
process.stdout.write(chalk.yellow(`Removed: ${result.removed.length} files\n`));
|
|
527
|
-
|
|
528
|
-
|
|
538
|
+
if (verbose) {
|
|
539
|
+
for (const f of result.removed) {
|
|
540
|
+
process.stdout.write(` Removed: ${f}\n`);
|
|
541
|
+
}
|
|
529
542
|
}
|
|
530
543
|
}
|
|
531
544
|
if (result.orphans && result.orphans.length > 0) {
|
|
532
545
|
process.stdout.write(chalk.yellow(`Cleaned: ${result.orphans.length} orphan files\n`));
|
|
533
|
-
|
|
534
|
-
|
|
546
|
+
if (verbose) {
|
|
547
|
+
for (const f of result.orphans) {
|
|
548
|
+
process.stdout.write(` Cleaned: ${f}\n`);
|
|
549
|
+
}
|
|
535
550
|
}
|
|
536
551
|
}
|
|
537
552
|
if (result.conflicts.length > 0) {
|
|
538
553
|
process.stdout.write(
|
|
539
554
|
chalk.yellow(`Skipped: ${result.conflicts.length} user-modified files (use --force to overwrite)\n`)
|
|
540
555
|
);
|
|
541
|
-
|
|
542
|
-
|
|
556
|
+
if (verbose) {
|
|
557
|
+
for (const f of result.conflicts) {
|
|
558
|
+
process.stdout.write(` ${f}\n`);
|
|
559
|
+
}
|
|
543
560
|
}
|
|
544
561
|
}
|
|
562
|
+
if (!verbose && (result.removed.length > 0 || (result.orphans?.length > 0) || result.conflicts.length > 0)) {
|
|
563
|
+
process.stdout.write(chalk.dim('(re-run with --verbose to see per-file changes)\n'));
|
|
564
|
+
}
|
|
545
565
|
}
|
|
546
|
-
await maybeSyncCodex({ projectRoot, codexRunner });
|
|
566
|
+
await maybeSyncCodex({ projectRoot, codexRunner, verbose });
|
|
547
567
|
} catch (err) {
|
|
548
568
|
// S3+S4: scrub tokens and absolute paths from error messages before surfacing
|
|
549
569
|
let storedToken = null;
|
package/src/lib/copy.js
CHANGED
|
@@ -95,14 +95,14 @@ export function collectDiskFiles(targetDir) {
|
|
|
95
95
|
*
|
|
96
96
|
* @param {string} sourceDir - Absolute path to source .claude/
|
|
97
97
|
* @param {string} targetDir - Absolute path to target .claude/
|
|
98
|
-
* @param {{ dryRun: boolean }} options
|
|
98
|
+
* @param {{ dryRun: boolean, verbose: boolean }} options
|
|
99
99
|
* @returns {FileEntry[]}
|
|
100
100
|
* @remarks Naming convention: `absolutePath` is the destination (under targetDir),
|
|
101
101
|
* `sourceAbsPath` is the source (under sourceDir). The asymmetry is intentional —
|
|
102
102
|
* renaming would break consumers (update.js). See DEBT-016.
|
|
103
103
|
*/
|
|
104
104
|
export function copyKitFiles(sourceDir, targetDir, options = {}) {
|
|
105
|
-
const { dryRun = false } = options;
|
|
105
|
+
const { dryRun = false, verbose = false } = options;
|
|
106
106
|
const fileList = [];
|
|
107
107
|
const warnings = [];
|
|
108
108
|
|
|
@@ -136,9 +136,12 @@ export function copyKitFiles(sourceDir, targetDir, options = {}) {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Print warnings
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
// Print warnings: always loud on real installs; suppressed only when doing a dry-run probe
|
|
140
|
+
// without verbose (the probe is internal and operators never see it).
|
|
141
|
+
if (!dryRun || verbose) {
|
|
142
|
+
for (const w of warnings) {
|
|
143
|
+
process.stderr.write(w + '\n');
|
|
144
|
+
}
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
if (dryRun) {
|
|
@@ -199,18 +202,21 @@ function isInSibling(siblingHooks, event, matcher) {
|
|
|
199
202
|
|
|
200
203
|
/**
|
|
201
204
|
* Filter kitEntries removing any whose event+matcher already exists in siblingHooks.
|
|
202
|
-
* Emits a stderr info message per skipped entry.
|
|
205
|
+
* Emits a stderr info message per skipped entry (suppressed when verbose=false).
|
|
203
206
|
* @param {object[]} kitEntries - Hook entries from kit source
|
|
204
207
|
* @param {object|null} siblingHooks - Parsed hooks from settings.local.json, or null
|
|
205
208
|
* @param {string} event - Hook event name
|
|
209
|
+
* @param {boolean} [verbose] - When false, suppresses the skipped-hook stderr message
|
|
206
210
|
* @returns {object[]} Entries not present in sibling
|
|
207
211
|
*/
|
|
208
|
-
function dedupeAgainstSibling(kitEntries, siblingHooks, event) {
|
|
212
|
+
function dedupeAgainstSibling(kitEntries, siblingHooks, event, verbose = false) {
|
|
209
213
|
if (!siblingHooks?.[event]) return kitEntries;
|
|
210
214
|
return kitEntries.filter(ke => {
|
|
211
215
|
const m = ke.matcher || '*';
|
|
212
216
|
if (isInSibling(siblingHooks, event, m)) {
|
|
213
|
-
|
|
217
|
+
if (verbose) {
|
|
218
|
+
process.stderr.write(`mk: hook ${event}[${m}] skipped (exists in settings.local.json)\n`);
|
|
219
|
+
}
|
|
214
220
|
return false;
|
|
215
221
|
}
|
|
216
222
|
return true;
|
|
@@ -229,11 +235,11 @@ function dedupeAgainstSibling(kitEntries, siblingHooks, event) {
|
|
|
229
235
|
*
|
|
230
236
|
* @param {string} sourceDir - Absolute path to source .claude/
|
|
231
237
|
* @param {string} targetDir - Absolute path to target .claude/
|
|
232
|
-
* @param {{ dryRun: boolean }} options
|
|
238
|
+
* @param {{ dryRun: boolean, verbose: boolean }} options
|
|
233
239
|
* @returns {{ action: 'created'|'merged'|'skipped', merged?: string[], backup?: string }}
|
|
234
240
|
*/
|
|
235
241
|
export function mergeSettingsJson(sourceDir, targetDir, options = {}) {
|
|
236
|
-
const { dryRun = false } = options;
|
|
242
|
+
const { dryRun = false, verbose = false } = options;
|
|
237
243
|
const srcPath = join(sourceDir, 'settings.json');
|
|
238
244
|
const destPath = join(targetDir, 'settings.json');
|
|
239
245
|
|
|
@@ -266,7 +272,7 @@ export function mergeSettingsJson(sourceDir, targetDir, options = {}) {
|
|
|
266
272
|
const hooksOnly = {};
|
|
267
273
|
if (kitSettings.hooks) {
|
|
268
274
|
for (const [event, kitEntries] of Object.entries(kitSettings.hooks)) {
|
|
269
|
-
const deduped = dedupeAgainstSibling(kitEntries, siblingHooks, event);
|
|
275
|
+
const deduped = dedupeAgainstSibling(kitEntries, siblingHooks, event, verbose);
|
|
270
276
|
if (deduped.length > 0) hooksOnly[event] = deduped;
|
|
271
277
|
}
|
|
272
278
|
}
|
|
@@ -295,7 +301,7 @@ export function mergeSettingsJson(sourceDir, targetDir, options = {}) {
|
|
|
295
301
|
if (!userSettings.hooks) userSettings.hooks = {};
|
|
296
302
|
for (const [event, kitEntries] of Object.entries(kitSettings.hooks)) {
|
|
297
303
|
if (!userSettings.hooks[event]) {
|
|
298
|
-
const deduped = dedupeAgainstSibling(kitEntries, siblingHooks, event);
|
|
304
|
+
const deduped = dedupeAgainstSibling(kitEntries, siblingHooks, event, verbose);
|
|
299
305
|
if (deduped.length > 0) {
|
|
300
306
|
userSettings.hooks[event] = deduped;
|
|
301
307
|
merged.push(event);
|
|
@@ -305,7 +311,9 @@ export function mergeSettingsJson(sourceDir, targetDir, options = {}) {
|
|
|
305
311
|
const kitMatcher = kitEntry.matcher || '*';
|
|
306
312
|
// Dedup guard: skip if sibling already has this event+matcher
|
|
307
313
|
if (isInSibling(siblingHooks, event, kitMatcher)) {
|
|
308
|
-
|
|
314
|
+
if (verbose) {
|
|
315
|
+
process.stderr.write(`mk: hook ${event}[${kitMatcher}] skipped (exists in settings.local.json)\n`);
|
|
316
|
+
}
|
|
309
317
|
continue;
|
|
310
318
|
}
|
|
311
319
|
const idx = userSettings.hooks[event].findIndex(
|