claudekit-cli 3.41.4-dev.47 → 3.41.4-dev.49

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/index.js CHANGED
@@ -12416,6 +12416,50 @@ function normalizeChecksum(checksum) {
12416
12416
  function isUnknownChecksum(checksum) {
12417
12417
  return normalizeChecksum(checksum) === UNKNOWN_CHECKSUM;
12418
12418
  }
12419
+ function getReasonCopy(code, _ctx) {
12420
+ switch (code) {
12421
+ case "new-item":
12422
+ return "New — not previously installed";
12423
+ case "new-provider-for-item":
12424
+ return "New provider for existing item";
12425
+ case "target-deleted-source-changed":
12426
+ return "You deleted this, CK has updates — reinstalling";
12427
+ case "target-dir-empty-reinstall":
12428
+ return "Provider directory is empty — reinstalling";
12429
+ case "force-reinstall":
12430
+ return "Force reinstall (target was deleted)";
12431
+ case "force-overwrite":
12432
+ return "Force overwrite (you edited this, --force active)";
12433
+ case "registry-upgrade-reinstall":
12434
+ return "Target deleted — reinstalling after registry upgrade";
12435
+ case "source-changed":
12436
+ return "CK updated, you didn't edit — safe to overwrite";
12437
+ case "registry-upgrade-heal":
12438
+ return "Healing stale target after registry upgrade";
12439
+ case "no-changes":
12440
+ return "Already up to date";
12441
+ case "user-edits-preserved":
12442
+ return "You edited this, CK unchanged — keeping your edits";
12443
+ case "user-deleted-respected":
12444
+ return "You deleted this, CK unchanged — respecting your choice";
12445
+ case "target-up-to-date-backfill":
12446
+ return "Already up to date — registry checksums will be backfilled";
12447
+ case "provider-checksum-unavailable":
12448
+ return "Provider checksum unavailable — cannot verify safely";
12449
+ case "target-state-unknown":
12450
+ return "Target state unavailable, CK unchanged — preserving target";
12451
+ case "source-removed-orphan":
12452
+ return "No longer shipped by CK — will be removed";
12453
+ case "renamed-cleanup":
12454
+ return "Renamed — cleaning up old path";
12455
+ case "path-migrated-cleanup":
12456
+ return "Path migrated — cleaning up old location";
12457
+ case "both-changed":
12458
+ return "Both you and CK changed this — pick one";
12459
+ case "target-state-unknown-source-changed":
12460
+ return "Target state unavailable while CK changed — manual review required";
12461
+ }
12462
+ }
12419
12463
  var UNKNOWN_CHECKSUM = "unknown";
12420
12464
 
12421
12465
  // src/commands/portable/portable-registry.ts
@@ -53250,24 +53294,37 @@ async function _ensureFeatureFlagLocked(configTomlPath) {
53250
53294
  };
53251
53295
  }
53252
53296
  }
53253
- const hasManagedBlock = existing.includes(SENTINEL_START2) && existing.includes(SENTINEL_END2);
53254
- if (hasManagedBlock) {
53255
- const replaced = replaceManagedBlock(existing);
53256
- await atomicWrite(configTomlPath, replaced);
53297
+ const { content: stripped, removed: hadManagedBlock } = stripAllManagedBlocks(existing);
53298
+ let content = stripped;
53299
+ let mutated = hadManagedBlock;
53300
+ const featuresHeaderIdx = findFeaturesSectionStart(content);
53301
+ if (featuresHeaderIdx !== -1) {
53302
+ const { updated, changed } = ensureFlagInFeaturesSection(content, featuresHeaderIdx);
53303
+ content = updated;
53304
+ mutated = mutated || changed;
53305
+ if (!mutated) {
53306
+ return { status: "already-set", configPath: configTomlPath };
53307
+ }
53308
+ try {
53309
+ await atomicWrite(configTomlPath, content);
53310
+ } catch (err) {
53311
+ return {
53312
+ status: "failed",
53313
+ configPath: configTomlPath,
53314
+ error: `Failed to write ${configTomlPath}: ${err instanceof Error ? err.message : String(err)}`
53315
+ };
53316
+ }
53257
53317
  return { status: "updated", configPath: configTomlPath };
53258
53318
  }
53259
- if (hasRawFeatureFlag(existing)) {
53260
- return { status: "already-set", configPath: configTomlPath };
53261
- }
53262
- const separator = existing.length > 0 && !existing.endsWith(`
53319
+ const separator = content.length === 0 ? "" : content.endsWith(`
53263
53320
  `) ? `
53264
-
53265
53321
  ` : `
53322
+
53266
53323
  `;
53267
- const updated = `${existing}${separator}${MANAGED_BLOCK}
53324
+ const withBlock = `${content}${separator}${MANAGED_BLOCK}
53268
53325
  `;
53269
53326
  try {
53270
- await atomicWrite(configTomlPath, updated);
53327
+ await atomicWrite(configTomlPath, withBlock);
53271
53328
  } catch (err) {
53272
53329
  return {
53273
53330
  status: "failed",
@@ -53275,34 +53332,80 @@ async function _ensureFeatureFlagLocked(configTomlPath) {
53275
53332
  error: `Failed to write ${configTomlPath}: ${err instanceof Error ? err.message : String(err)}`
53276
53333
  };
53277
53334
  }
53278
- return { status: "written", configPath: configTomlPath };
53335
+ return { status: hadManagedBlock ? "updated" : "written", configPath: configTomlPath };
53279
53336
  }
53280
- function hasRawFeatureFlag(content) {
53281
- const withoutManaged = removeManagedBlock(content);
53282
- return /^\s*codex_hooks\s*=\s*true(\s*#[^\r\n]*)?\s*$/m.test(withoutManaged);
53337
+ function findFeaturesSectionStart(content) {
53338
+ const match = /^[ \t]*\[features\][ \t]*(?:#[^\r\n]*)?$/m.exec(content);
53339
+ return match ? match.index : -1;
53283
53340
  }
53284
- function replaceManagedBlock(content) {
53285
- const startIdx = content.indexOf(SENTINEL_START2);
53286
- const endIdx = content.lastIndexOf(SENTINEL_END2);
53287
- if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
53288
- return content;
53341
+ function ensureFlagInFeaturesSection(content, headerStartIdx) {
53342
+ const headerLineEnd = content.indexOf(`
53343
+ `, headerStartIdx);
53344
+ const bodyStart = headerLineEnd === -1 ? content.length : headerLineEnd + 1;
53345
+ const rest = content.slice(bodyStart);
53346
+ const nextHeaderMatch = /\n\[[^\]]+\]/.exec(rest);
53347
+ const bodyEnd = nextHeaderMatch ? bodyStart + nextHeaderMatch.index + 1 : content.length;
53348
+ const body = content.slice(bodyStart, bodyEnd);
53349
+ const flagRegex = /^([ \t]*codex_hooks[ \t]*=[ \t]*)(true|false)([ \t]*#[^\r\n]*)?[ \t]*$/m;
53350
+ const flagMatch = flagRegex.exec(body);
53351
+ if (flagMatch) {
53352
+ if (flagMatch[2] === "true") {
53353
+ return { updated: content, changed: false };
53354
+ }
53355
+ const newBody = body.replace(flagRegex, (_m, prefix, _v, trailing) => `${prefix}true${trailing ?? ""}`);
53356
+ return {
53357
+ updated: content.slice(0, bodyStart) + newBody + content.slice(bodyEnd),
53358
+ changed: true
53359
+ };
53289
53360
  }
53290
- const endOfBlock = endIdx + SENTINEL_END2.length;
53291
- const afterBlock = content[endOfBlock] === `
53292
- ` ? content.slice(endOfBlock + 1) : content.slice(endOfBlock);
53293
- const before = content.slice(0, startIdx);
53294
- return `${before}${MANAGED_BLOCK}
53295
- ${afterBlock}`;
53296
- }
53297
- function removeManagedBlock(content) {
53298
- const startIdx = content.indexOf(SENTINEL_START2);
53299
- const endIdx = content.indexOf(SENTINEL_END2);
53300
- if (startIdx === -1 || endIdx === -1 || endIdx < startIdx)
53301
- return content;
53302
- const endOfBlock = endIdx + SENTINEL_END2.length;
53303
- const afterBlock = content[endOfBlock] === `
53304
- ` ? content.slice(endOfBlock + 1) : content.slice(endOfBlock);
53305
- return content.slice(0, startIdx) + afterBlock;
53361
+ if (headerLineEnd === -1) {
53362
+ return { updated: `${content}
53363
+ codex_hooks = true
53364
+ `, changed: true };
53365
+ }
53366
+ let insertAt = bodyEnd;
53367
+ while (insertAt > bodyStart && content[insertAt - 1] === `
53368
+ ` && content[insertAt - 2] === `
53369
+ `) {
53370
+ insertAt -= 1;
53371
+ }
53372
+ const needsLeadingNewline = insertAt > bodyStart && content[insertAt - 1] !== `
53373
+ `;
53374
+ const insertion = `${needsLeadingNewline ? `
53375
+ ` : ""}codex_hooks = true
53376
+ `;
53377
+ return {
53378
+ updated: content.slice(0, insertAt) + insertion + content.slice(insertAt),
53379
+ changed: true
53380
+ };
53381
+ }
53382
+ function stripAllManagedBlocks(content) {
53383
+ let result = content;
53384
+ let removed = false;
53385
+ while (true) {
53386
+ const startIdx = result.indexOf(SENTINEL_START2);
53387
+ if (startIdx === -1)
53388
+ break;
53389
+ const endIdx = result.indexOf(SENTINEL_END2, startIdx);
53390
+ if (endIdx === -1)
53391
+ break;
53392
+ const endOfBlock = endIdx + SENTINEL_END2.length;
53393
+ const afterBlockStart = result[endOfBlock] === `
53394
+ ` ? endOfBlock + 1 : endOfBlock;
53395
+ let beforeBlockEnd = startIdx;
53396
+ if (beforeBlockEnd >= 1 && result[beforeBlockEnd - 1] === `
53397
+ `) {
53398
+ beforeBlockEnd -= 1;
53399
+ if (beforeBlockEnd >= 1 && result[beforeBlockEnd - 1] === `
53400
+ `) {
53401
+ beforeBlockEnd -= 1;
53402
+ }
53403
+ beforeBlockEnd += 1;
53404
+ }
53405
+ result = result.slice(0, beforeBlockEnd) + result.slice(afterBlockStart);
53406
+ removed = true;
53407
+ }
53408
+ return { content: result, removed };
53306
53409
  }
53307
53410
  async function atomicWrite(filePath, content) {
53308
53411
  const tempPath = `${filePath}.ck-tmp`;
@@ -54298,7 +54401,7 @@ var init_reconcile_registry_backfill = __esm(() => {
54298
54401
  });
54299
54402
 
54300
54403
  // src/commands/portable/reconcile-state-builders.ts
54301
- import { existsSync as existsSync27 } from "node:fs";
54404
+ import { existsSync as existsSync27, readdirSync as readdirSync3, statSync as statSync5 } from "node:fs";
54302
54405
  import { readFile as readFile23 } from "node:fs/promises";
54303
54406
  function getProviderPathKeyForPortableType2(type) {
54304
54407
  switch (type) {
@@ -54394,6 +54497,95 @@ function buildSourceItemState(item, type, selectedProviders, options2) {
54394
54497
  targetChecksums
54395
54498
  };
54396
54499
  }
54500
+ function buildTypeDirectoryStates(providerConfigs, types4) {
54501
+ const results = [];
54502
+ for (const { provider, global: isGlobal } of providerConfigs) {
54503
+ const providerConfig = providers[provider];
54504
+ if (!providerConfig)
54505
+ continue;
54506
+ for (const type of types4) {
54507
+ const pathKey = portableTypeToProviderPathKey(type);
54508
+ const pathConfig = providerConfig[pathKey];
54509
+ if (!pathConfig)
54510
+ continue;
54511
+ if (pathConfig.writeStrategy === "merge-single" || pathConfig.writeStrategy === "single-file") {
54512
+ continue;
54513
+ }
54514
+ const dirPath = isGlobal ? pathConfig.globalPath : pathConfig.projectPath;
54515
+ if (!dirPath)
54516
+ continue;
54517
+ const exists = existsSync27(dirPath);
54518
+ if (!exists) {
54519
+ results.push({
54520
+ provider,
54521
+ type,
54522
+ global: isGlobal,
54523
+ path: dirPath,
54524
+ exists: false,
54525
+ isEmpty: true,
54526
+ fileCount: 0
54527
+ });
54528
+ continue;
54529
+ }
54530
+ let stat7 = null;
54531
+ try {
54532
+ stat7 = statSync5(dirPath);
54533
+ } catch {
54534
+ results.push({
54535
+ provider,
54536
+ type,
54537
+ global: isGlobal,
54538
+ path: dirPath,
54539
+ exists: false,
54540
+ isEmpty: true,
54541
+ fileCount: 0
54542
+ });
54543
+ continue;
54544
+ }
54545
+ if (!stat7.isDirectory()) {
54546
+ results.push({
54547
+ provider,
54548
+ type,
54549
+ global: isGlobal,
54550
+ path: dirPath,
54551
+ exists: true,
54552
+ isEmpty: false,
54553
+ fileCount: 1
54554
+ });
54555
+ continue;
54556
+ }
54557
+ const ext = pathConfig.fileExtension ?? "";
54558
+ let entries = [];
54559
+ try {
54560
+ entries = readdirSync3(dirPath);
54561
+ } catch {
54562
+ results.push({
54563
+ provider,
54564
+ type,
54565
+ global: isGlobal,
54566
+ path: dirPath,
54567
+ exists: true,
54568
+ isEmpty: true,
54569
+ fileCount: 0
54570
+ });
54571
+ continue;
54572
+ }
54573
+ const managedFiles = ext === "" ? entries.filter((f3) => {
54574
+ return f3.endsWith(".json") || f3.endsWith(".sh") || f3.endsWith(".js");
54575
+ }) : entries.filter((f3) => f3.endsWith(ext));
54576
+ results.push({
54577
+ provider,
54578
+ type,
54579
+ global: isGlobal,
54580
+ path: dirPath,
54581
+ exists: true,
54582
+ isEmpty: managedFiles.length === 0,
54583
+ fileCount: managedFiles.length
54584
+ });
54585
+ }
54586
+ }
54587
+ return results;
54588
+ }
54397
54589
  async function buildTargetStates(entries, options2) {
54398
54590
  const targetStates = new Map;
54399
54591
  const entriesByPath = new Map;
@@ -54422,11 +54614,13 @@ async function buildTargetStates(entries, options2) {
54422
54614
  }
54423
54615
  return targetStates;
54424
54616
  }
54617
+ var portableTypeToProviderPathKey;
54425
54618
  var init_reconcile_state_builders = __esm(() => {
54426
54619
  init_checksum_utils();
54427
54620
  init_converters();
54428
54621
  init_merge_single_sections();
54429
54622
  init_provider_registry();
54623
+ portableTypeToProviderPathKey = getProviderPathKeyForPortableType2;
54430
54624
  });
54431
54625
 
54432
54626
  // src/commands/portable/reconciler.ts
@@ -54479,6 +54673,9 @@ function makeItemTypeKey(item, type) {
54479
54673
  function makeRegistryIdentityKey(entry) {
54480
54674
  return JSON.stringify([entry.item, entry.type, entry.provider, entry.global]);
54481
54675
  }
54676
+ function makeDirStateKey(provider, type, global3) {
54677
+ return JSON.stringify([provider, type, global3]);
54678
+ }
54482
54679
  function dedupeProviderConfigs(providerConfigs) {
54483
54680
  const seen = new Set;
54484
54681
  const unique = [];
@@ -54505,6 +54702,14 @@ function buildTargetStateIndex(targetStates) {
54505
54702
  }
54506
54703
  return index;
54507
54704
  }
54705
+ function buildDirStateIndex(dirStates) {
54706
+ const index = new Map;
54707
+ for (const ds of dirStates) {
54708
+ const key = makeDirStateKey(ds.provider, ds.type, ds.global);
54709
+ index.set(key, ds);
54710
+ }
54711
+ return index;
54712
+ }
54508
54713
  function lookupTargetState(targetStateIndex, pathValue) {
54509
54714
  return targetStateIndex.get(normalizePortablePath(pathValue));
54510
54715
  }
@@ -54588,6 +54793,66 @@ function suppressOverlappingActions(actions) {
54588
54793
  }
54589
54794
  return filtered;
54590
54795
  }
54796
+ function applyEmptyDirOverride(actions, dirStates, respectDeletions) {
54797
+ if (dirStates.length === 0) {
54798
+ return { actions, banners: [] };
54799
+ }
54800
+ const dirIndex = buildDirStateIndex(dirStates);
54801
+ const banners = [];
54802
+ const flippedGroups = new Map;
54803
+ for (const action of actions) {
54804
+ if (action.action !== "skip" || action.reasonCode !== "user-deleted-respected") {
54805
+ continue;
54806
+ }
54807
+ const key = makeDirStateKey(action.provider, action.type, action.global);
54808
+ const dirState = dirIndex.get(key);
54809
+ if (!dirState?.isEmpty)
54810
+ continue;
54811
+ if (respectDeletions) {
54812
+ const existing2 = flippedGroups.get(key);
54813
+ if (existing2) {
54814
+ existing2.count++;
54815
+ } else {
54816
+ flippedGroups.set(key, { dirState, count: 1 });
54817
+ }
54818
+ continue;
54819
+ }
54820
+ action.action = "install";
54821
+ action.reasonCode = "target-dir-empty-reinstall";
54822
+ action.reasonCopy = getReasonCopy("target-dir-empty-reinstall");
54823
+ action.reason = action.reasonCopy;
54824
+ const existing = flippedGroups.get(key);
54825
+ if (existing) {
54826
+ existing.count++;
54827
+ } else {
54828
+ flippedGroups.set(key, { dirState, count: 1 });
54829
+ }
54830
+ }
54831
+ for (const [, { dirState, count }] of flippedGroups) {
54832
+ if (respectDeletions) {
54833
+ banners.push({
54834
+ kind: "empty-dir-respected",
54835
+ provider: dirState.provider,
54836
+ type: dirState.type,
54837
+ global: dirState.global,
54838
+ path: dirState.path,
54839
+ itemCount: count,
54840
+ message: `Detected empty ${dirState.path} — respecting your deletions (${count} items skipped).`
54841
+ });
54842
+ } else {
54843
+ banners.push({
54844
+ kind: "empty-dir",
54845
+ provider: dirState.provider,
54846
+ type: dirState.type,
54847
+ global: dirState.global,
54848
+ path: dirState.path,
54849
+ itemCount: count,
54850
+ message: `Detected empty ${dirState.path} — ${count} item${count === 1 ? "" : "s"} will be reinstalled. Uncheck any to skip.`
54851
+ });
54852
+ }
54853
+ }
54854
+ return { actions, banners };
54855
+ }
54591
54856
  function reconcile(input) {
54592
54857
  const actions = [];
54593
54858
  const targetStateIndex = buildTargetStateIndex(input.targetStates);
@@ -54617,7 +54882,10 @@ function reconcile(input) {
54617
54882
  const orphanActions = detectOrphans(input, renamedFromKeys);
54618
54883
  actions.push(...orphanActions);
54619
54884
  const normalizedActions = suppressOverlappingActions(dedupeActions(actions));
54620
- return buildPlan(normalizedActions);
54885
+ const dirStates = input.typeDirectoryStates ?? [];
54886
+ const respectDeletions = input.respectDeletions ?? false;
54887
+ const { actions: finalActions, banners } = applyEmptyDirOverride(normalizedActions, dirStates, respectDeletions);
54888
+ return buildPlan(finalActions, banners);
54621
54889
  }
54622
54890
  function determineAction(source, providerConfig, input, targetStateIndex, deletedIdentityKeys) {
54623
54891
  let registryEntry = findRegistryEntry(source, providerConfig, input.registry);
@@ -54630,12 +54898,14 @@ function determineAction(source, providerConfig, input, targetStateIndex, delete
54630
54898
  if (registryEntry && deletedIdentityKeys.has(identityKey)) {
54631
54899
  registryEntry = null;
54632
54900
  }
54901
+ const isDirectoryItem = source.type === "skill";
54633
54902
  const common = {
54634
54903
  item: source.item,
54635
54904
  type: source.type,
54636
54905
  provider: providerConfig.provider,
54637
54906
  global: providerConfig.global,
54638
- targetPath: ""
54907
+ targetPath: "",
54908
+ isDirectoryItem: isDirectoryItem || undefined
54639
54909
  };
54640
54910
  const convertedChecksumRaw = source.convertedChecksums[providerConfig.provider];
54641
54911
  const convertedChecksum = normalizeChecksum(convertedChecksumRaw);
@@ -54643,30 +54913,39 @@ function determineAction(source, providerConfig, input, targetStateIndex, delete
54643
54913
  if (!convertedChecksumRaw || isUnknownChecksum(convertedChecksumRaw)) {
54644
54914
  if (registryEntry) {
54645
54915
  common.targetPath = registryEntry.path;
54916
+ const code3 = "provider-checksum-unavailable";
54646
54917
  return {
54647
54918
  ...common,
54648
54919
  action: "skip",
54649
54920
  reason: "Provider checksum unavailable — cannot verify safely",
54921
+ reasonCode: code3,
54922
+ reasonCopy: getReasonCopy(code3),
54650
54923
  sourceChecksum: UNKNOWN_CHECKSUM,
54651
54924
  registeredSourceChecksum: normalizeChecksum(registryEntry.sourceChecksum),
54652
54925
  registeredTargetChecksum: normalizeChecksum(registryEntry.targetChecksum)
54653
54926
  };
54654
54927
  }
54655
54928
  const itemExistsElsewhere = input.registry.installations.some((i) => i.item === source.item && i.type === source.type);
54929
+ const code2 = itemExistsElsewhere ? "new-provider-for-item" : "new-item";
54656
54930
  return {
54657
54931
  ...common,
54658
54932
  action: "install",
54659
54933
  reason: itemExistsElsewhere ? "New provider for existing item" : "New item, not previously installed",
54934
+ reasonCode: code2,
54935
+ reasonCopy: getReasonCopy(code2),
54660
54936
  sourceChecksum: UNKNOWN_CHECKSUM
54661
54937
  };
54662
54938
  }
54663
54939
  if (!registryEntry) {
54664
54940
  const itemExistsElsewhere = input.registry.installations.some((i) => i.item === source.item && i.type === source.type);
54941
+ const code2 = itemExistsElsewhere ? "new-provider-for-item" : "new-item";
54665
54942
  const reason = itemExistsElsewhere ? "New provider for existing item" : "New item, not previously installed";
54666
54943
  return {
54667
54944
  ...common,
54668
54945
  action: "install",
54669
54946
  reason,
54947
+ reasonCode: code2,
54948
+ reasonCopy: getReasonCopy(code2),
54670
54949
  sourceChecksum: convertedChecksum
54671
54950
  };
54672
54951
  }
@@ -54678,36 +54957,48 @@ function determineAction(source, providerConfig, input, targetStateIndex, delete
54678
54957
  const targetMatchesExpectedOutput = targetState?.exists === true && !isUnknownChecksum(expectedTargetChecksum) && currentTargetChecksum === expectedTargetChecksum;
54679
54958
  if (isUnknownChecksum(registeredSourceChecksum)) {
54680
54959
  if (targetMatchesExpectedOutput) {
54960
+ const code3 = "target-up-to-date-backfill";
54681
54961
  return {
54682
54962
  ...common,
54683
54963
  action: "skip",
54684
54964
  reason: "Target up-to-date after registry upgrade — checksums will be backfilled",
54965
+ reasonCode: code3,
54966
+ reasonCopy: getReasonCopy(code3),
54685
54967
  sourceChecksum: convertedChecksum,
54686
54968
  currentTargetChecksum,
54687
54969
  backfillRegistry: true
54688
54970
  };
54689
54971
  }
54690
54972
  if (!targetState || !targetState.exists) {
54973
+ const code3 = "registry-upgrade-reinstall";
54691
54974
  return {
54692
54975
  ...common,
54693
54976
  action: "install",
54694
54977
  reason: "Target deleted — reinstalling after registry upgrade",
54978
+ reasonCode: code3,
54979
+ reasonCopy: getReasonCopy(code3),
54695
54980
  sourceChecksum: convertedChecksum
54696
54981
  };
54697
54982
  }
54983
+ const code2 = "registry-upgrade-heal";
54698
54984
  return {
54699
54985
  ...common,
54700
54986
  action: "update",
54701
54987
  reason: "Healing stale target after registry upgrade",
54988
+ reasonCode: code2,
54989
+ reasonCopy: getReasonCopy(code2),
54702
54990
  sourceChecksum: convertedChecksum,
54703
54991
  currentTargetChecksum
54704
54992
  };
54705
54993
  }
54706
54994
  if (targetMatchesExpectedOutput && (convertedChecksum !== registeredSourceChecksum || currentTargetChecksum !== registeredTargetChecksum)) {
54995
+ const code2 = "target-up-to-date-backfill";
54707
54996
  return {
54708
54997
  ...common,
54709
54998
  action: "skip",
54710
54999
  reason: "Target up-to-date — registry checksums will be backfilled",
55000
+ reasonCode: code2,
55001
+ reasonCopy: getReasonCopy(code2),
54711
55002
  sourceChecksum: convertedChecksum,
54712
55003
  registeredSourceChecksum,
54713
55004
  currentTargetChecksum,
@@ -54719,19 +55010,49 @@ function determineAction(source, providerConfig, input, targetStateIndex, delete
54719
55010
  const targetChangeState = getTargetChangeState(targetState, registryEntry, registeredTargetChecksum);
54720
55011
  if (targetChangeState === "deleted") {
54721
55012
  const forceReinstall = input.force && !sourceChanged;
55013
+ if (sourceChanged) {
55014
+ const code3 = "target-deleted-source-changed";
55015
+ return {
55016
+ ...common,
55017
+ action: "install",
55018
+ reason: "Target was deleted, CK has updates — reinstalling",
55019
+ reasonCode: code3,
55020
+ reasonCopy: getReasonCopy(code3),
55021
+ sourceChecksum: convertedChecksum,
55022
+ registeredSourceChecksum
55023
+ };
55024
+ }
55025
+ if (forceReinstall) {
55026
+ const code3 = "force-reinstall";
55027
+ return {
55028
+ ...common,
55029
+ action: "install",
55030
+ reason: "Force reinstall (target was deleted)",
55031
+ reasonCode: code3,
55032
+ reasonCopy: getReasonCopy(code3),
55033
+ sourceChecksum: convertedChecksum,
55034
+ registeredSourceChecksum
55035
+ };
55036
+ }
55037
+ const code2 = "user-deleted-respected";
54722
55038
  return {
54723
55039
  ...common,
54724
- action: sourceChanged || forceReinstall ? "install" : "skip",
54725
- reason: sourceChanged ? "Target was deleted, CK has updates — reinstalling" : forceReinstall ? "Force reinstall (target was deleted)" : "Target was deleted by user, CK unchanged — respecting deletion",
55040
+ action: "skip",
55041
+ reason: "Target was deleted by user, CK unchanged — respecting deletion",
55042
+ reasonCode: code2,
55043
+ reasonCopy: getReasonCopy(code2),
54726
55044
  sourceChecksum: convertedChecksum,
54727
55045
  registeredSourceChecksum
54728
55046
  };
54729
55047
  }
54730
55048
  if (targetChangeState === "unknown") {
55049
+ const code2 = sourceChanged ? "target-state-unknown-source-changed" : "target-state-unknown";
54731
55050
  return {
54732
55051
  ...common,
54733
55052
  action: sourceChanged ? "conflict" : "skip",
54734
55053
  reason: sourceChanged ? "Target state unavailable while CK changed — manual review required" : "Target state unavailable, CK unchanged — preserving target",
55054
+ reasonCode: code2,
55055
+ reasonCopy: getReasonCopy(code2),
54735
55056
  sourceChecksum: convertedChecksum,
54736
55057
  registeredSourceChecksum,
54737
55058
  currentTargetChecksum,
@@ -54740,19 +55061,38 @@ function determineAction(source, providerConfig, input, targetStateIndex, delete
54740
55061
  }
54741
55062
  const targetChanged = targetChangeState === "changed";
54742
55063
  if (!sourceChanged && !targetChanged) {
55064
+ const code2 = "no-changes";
54743
55065
  return {
54744
55066
  ...common,
54745
55067
  action: "skip",
54746
55068
  reason: "No changes",
55069
+ reasonCode: code2,
55070
+ reasonCopy: getReasonCopy(code2),
54747
55071
  sourceChecksum: convertedChecksum,
54748
55072
  currentTargetChecksum
54749
55073
  };
54750
55074
  }
54751
55075
  if (!sourceChanged && targetChanged) {
55076
+ if (input.force) {
55077
+ return {
55078
+ ...common,
55079
+ action: "install",
55080
+ reason: "Force overwrite (user edits)",
55081
+ reasonCode: "force-overwrite",
55082
+ reasonCopy: getReasonCopy("force-overwrite"),
55083
+ sourceChecksum: convertedChecksum,
55084
+ registeredSourceChecksum,
55085
+ currentTargetChecksum,
55086
+ registeredTargetChecksum
55087
+ };
55088
+ }
55089
+ const code2 = "user-edits-preserved";
54752
55090
  return {
54753
55091
  ...common,
54754
- action: input.force ? "install" : "skip",
54755
- reason: input.force ? "Force overwrite (user edits)" : "User edited, CK unchanged — preserving edits",
55092
+ action: "skip",
55093
+ reason: "User edited, CK unchanged — preserving edits",
55094
+ reasonCode: code2,
55095
+ reasonCopy: getReasonCopy(code2),
54756
55096
  sourceChecksum: convertedChecksum,
54757
55097
  registeredSourceChecksum,
54758
55098
  currentTargetChecksum,
@@ -54760,20 +55100,26 @@ function determineAction(source, providerConfig, input, targetStateIndex, delete
54760
55100
  };
54761
55101
  }
54762
55102
  if (sourceChanged && !targetChanged) {
55103
+ const code2 = "source-changed";
54763
55104
  return {
54764
55105
  ...common,
54765
55106
  action: "update",
54766
55107
  reason: "CK updated, no user edits — safe overwrite",
55108
+ reasonCode: code2,
55109
+ reasonCopy: getReasonCopy(code2),
54767
55110
  sourceChecksum: convertedChecksum,
54768
55111
  registeredSourceChecksum,
54769
55112
  currentTargetChecksum,
54770
55113
  registeredTargetChecksum
54771
55114
  };
54772
55115
  }
55116
+ const code = "both-changed";
54773
55117
  return {
54774
55118
  ...common,
54775
55119
  action: "conflict",
54776
55120
  reason: "Both CK and user modified this item",
55121
+ reasonCode: code,
55122
+ reasonCopy: getReasonCopy(code),
54777
55123
  sourceChecksum: convertedChecksum,
54778
55124
  registeredSourceChecksum,
54779
55125
  currentTargetChecksum,
@@ -54809,6 +55155,7 @@ function detectOrphans(input, renamedFromKeys) {
54809
55155
  if (entry.type === "config" && hasConfigSource)
54810
55156
  continue;
54811
55157
  if (!sourceItemKeys.has(sourceItemKey)) {
55158
+ const code = "source-removed-orphan";
54812
55159
  actions.push({
54813
55160
  action: "delete",
54814
55161
  item: entry.item,
@@ -54816,7 +55163,9 @@ function detectOrphans(input, renamedFromKeys) {
54816
55163
  provider: entry.provider,
54817
55164
  global: entry.global,
54818
55165
  targetPath: entry.path,
54819
- reason: "Item no longer in CK source — orphaned"
55166
+ reason: "Item no longer in CK source — orphaned",
55167
+ reasonCode: code,
55168
+ reasonCopy: getReasonCopy(code)
54820
55169
  });
54821
55170
  }
54822
55171
  }
@@ -54835,6 +55184,7 @@ function detectRenames(input) {
54835
55184
  const normalizedFrom = normalizePortablePath(rename8.from);
54836
55185
  const oldEntries = input.registry.installations.filter((e2) => normalizePortablePath(e2.sourcePath) === normalizedFrom);
54837
55186
  for (const oldEntry of oldEntries) {
55187
+ const code = "renamed-cleanup";
54838
55188
  actions.push({
54839
55189
  deleteAction: {
54840
55190
  action: "delete",
@@ -54844,6 +55194,8 @@ function detectRenames(input) {
54844
55194
  global: oldEntry.global,
54845
55195
  targetPath: oldEntry.path,
54846
55196
  reason: `Renamed: ${rename8.from} -> ${rename8.to}`,
55197
+ reasonCode: code,
55198
+ reasonCopy: getReasonCopy(code),
54847
55199
  previousItem: oldEntry.item
54848
55200
  },
54849
55201
  newItem: oldEntry.item
@@ -54860,6 +55212,7 @@ function detectPathMigrations(input) {
54860
55212
  for (const migration of applicable) {
54861
55213
  const affectedEntries = input.registry.installations.filter((e2) => e2.provider === migration.provider && e2.type === migration.type && pathContainsSegments(e2.path, migration.from));
54862
55214
  for (const entry of affectedEntries) {
55215
+ const code = "path-migrated-cleanup";
54863
55216
  actions.push({
54864
55217
  deleteAction: {
54865
55218
  action: "delete",
@@ -54869,6 +55222,8 @@ function detectPathMigrations(input) {
54869
55222
  global: entry.global,
54870
55223
  targetPath: entry.path,
54871
55224
  reason: `Provider path migrated: ${migration.from} -> ${migration.to}`,
55225
+ reasonCode: code,
55226
+ reasonCopy: getReasonCopy(code),
54872
55227
  previousPath: entry.path
54873
55228
  }
54874
55229
  });
@@ -54879,7 +55234,7 @@ function detectPathMigrations(input) {
54879
55234
  function detectSectionRenames(_input) {
54880
55235
  return [];
54881
55236
  }
54882
- function buildPlan(actions) {
55237
+ function buildPlan(actions, banners) {
54883
55238
  const summary = { install: 0, update: 0, skip: 0, conflict: 0, delete: 0 };
54884
55239
  for (const action of actions) {
54885
55240
  summary[action.action]++;
@@ -54887,7 +55242,8 @@ function buildPlan(actions) {
54887
55242
  return {
54888
55243
  actions,
54889
55244
  summary,
54890
- hasConflicts: summary.conflict > 0
55245
+ hasConflicts: summary.conflict > 0,
55246
+ banners
54891
55247
  };
54892
55248
  }
54893
55249
  var init_reconciler = __esm(() => {
@@ -55910,6 +56266,28 @@ function registerMigrationRoutes(app) {
55910
56266
  return;
55911
56267
  }
55912
56268
  const configSource = sourceParsed.value;
56269
+ const reinstallEmptyDirsParsed = parseBooleanLike(req.query.reinstallEmptyDirs);
56270
+ if (!reinstallEmptyDirsParsed.ok) {
56271
+ res.status(400).json({ error: `reinstallEmptyDirs ${reinstallEmptyDirsParsed.error}` });
56272
+ return;
56273
+ }
56274
+ const reinstallEmptyDirs = reinstallEmptyDirsParsed.value !== false;
56275
+ const respectDeletionsParsed = parseBooleanLike(req.query.respectDeletions);
56276
+ if (!respectDeletionsParsed.ok) {
56277
+ res.status(400).json({ error: `respectDeletions ${respectDeletionsParsed.error}` });
56278
+ return;
56279
+ }
56280
+ const respectDeletions = respectDeletionsParsed.value === true;
56281
+ const modeRaw = req.query.mode;
56282
+ let reconcileMode = "reconcile";
56283
+ if (modeRaw !== undefined) {
56284
+ const modeStr = String(modeRaw).trim().toLowerCase();
56285
+ if (modeStr !== "reconcile" && modeStr !== "install") {
56286
+ res.status(400).json({ error: "mode must be 'reconcile' or 'install'" });
56287
+ return;
56288
+ }
56289
+ reconcileMode = modeStr;
56290
+ }
55913
56291
  const discovered = await discoverMigrationItems(include, configSource);
55914
56292
  const sourceItems = [];
55915
56293
  for (const agent of discovered.agents) {
@@ -55995,20 +56373,33 @@ function registerMigrationRoutes(app) {
55995
56373
  provider,
55996
56374
  global: globalParam
55997
56375
  }));
56376
+ const enabledTypes = ["agent", "command", "skill", "config", "rules", "hooks"].filter((type) => {
56377
+ const key = type === "agent" ? "agents" : type === "command" ? "commands" : type === "skill" ? "skills" : type === "config" ? "config" : type === "rules" ? "rules" : "hooks";
56378
+ return include[key];
56379
+ });
56380
+ const typeDirectoryStates = reinstallEmptyDirs ? buildTypeDirectoryStates(providerConfigs.map((p) => ({
56381
+ provider: p.provider,
56382
+ global: p.global
56383
+ })), enabledTypes) : undefined;
55998
56384
  const input = {
55999
56385
  sourceItems,
56000
56386
  registry,
56001
56387
  targetStates,
56002
56388
  manifest,
56003
- providerConfigs
56389
+ providerConfigs,
56390
+ typeDirectoryStates,
56391
+ respectDeletions
56004
56392
  };
56005
56393
  const plan = reconcile(input);
56394
+ const hasUnknownChecksum = registry.installations.some((inst) => isUnknownChecksum(inst.sourceChecksum) || isUnknownChecksum(inst.targetChecksum));
56395
+ const suggestedMode = hasUnknownChecksum ? "install" : "reconcile";
56006
56396
  const planWithMeta = {
56007
56397
  ...plan,
56008
56398
  meta: {
56009
56399
  include,
56010
56400
  providers: selectedProviders,
56011
56401
  source: configSource,
56402
+ mode: reconcileMode,
56012
56403
  items: {
56013
56404
  agents: discovered.agents.map((item) => item.name),
56014
56405
  commands: discovered.commands.map((item) => item.name),
@@ -56019,7 +56410,10 @@ function registerMigrationRoutes(app) {
56019
56410
  }
56020
56411
  }
56021
56412
  };
56022
- res.status(200).json({ plan: planWithMeta });
56413
+ res.status(200).json({
56414
+ plan: planWithMeta,
56415
+ suggestedMode
56416
+ });
56023
56417
  } catch (error) {
56024
56418
  res.status(500).json({
56025
56419
  error: "Failed to compute reconcile plan",
@@ -56027,6 +56421,123 @@ function registerMigrationRoutes(app) {
56027
56421
  });
56028
56422
  }
56029
56423
  });
56424
+ app.get("/api/migrate/install-discovery", async (req, res) => {
56425
+ try {
56426
+ let addCandidates = function(items, type, isDirectoryItem) {
56427
+ for (const item of items) {
56428
+ const sourcePath = item.sourcePath ?? item.path ?? "";
56429
+ for (const provider of selectedProviders) {
56430
+ const registryEntry = registry.installations.find((inst) => inst.item === item.name && inst.type === type && inst.provider === provider && inst.global === globalParam);
56431
+ const alreadyInstalled = registryEntry !== undefined;
56432
+ candidates.push({
56433
+ item: item.name,
56434
+ type,
56435
+ provider,
56436
+ global: globalParam,
56437
+ isDirectoryItem,
56438
+ description: item.description,
56439
+ sourcePath,
56440
+ alreadyInstalled,
56441
+ registryPath: alreadyInstalled ? registryEntry?.path : undefined
56442
+ });
56443
+ }
56444
+ }
56445
+ };
56446
+ const providersParsed = parseProvidersFromQuery(req.query.providers);
56447
+ if (!providersParsed.ok || !providersParsed.value) {
56448
+ res.status(400).json({ error: providersParsed.error || "Invalid providers parameter" });
56449
+ return;
56450
+ }
56451
+ const selectedProviders = providersParsed.value;
56452
+ const includeParsed = parseIncludeOptionsStrict({
56453
+ agents: req.query.agents,
56454
+ commands: req.query.commands,
56455
+ skills: req.query.skills,
56456
+ config: req.query.config,
56457
+ rules: req.query.rules,
56458
+ hooks: req.query.hooks
56459
+ }, "");
56460
+ if (!includeParsed.ok || !includeParsed.value) {
56461
+ res.status(400).json({ error: includeParsed.error || "Invalid include options" });
56462
+ return;
56463
+ }
56464
+ const include = includeParsed.value;
56465
+ const globalParsed = parseBooleanLike(req.query.global);
56466
+ if (!globalParsed.ok) {
56467
+ res.status(400).json({ error: `global ${globalParsed.error}` });
56468
+ return;
56469
+ }
56470
+ const globalParam = globalParsed.value === true;
56471
+ const sourceParsed = parseConfigSource(req.query.source);
56472
+ if (!sourceParsed.ok) {
56473
+ res.status(400).json({ error: sourceParsed.error || "Invalid source value" });
56474
+ return;
56475
+ }
56476
+ const configSource = sourceParsed.value;
56477
+ const discovered = await discoverMigrationItems(include, configSource);
56478
+ const registry = await readPortableRegistry();
56479
+ const candidates = [];
56480
+ if (include.agents) {
56481
+ addCandidates(discovered.agents.map((a3) => ({
56482
+ name: a3.name,
56483
+ description: a3.description,
56484
+ sourcePath: a3.sourcePath ?? ""
56485
+ })), "agent", false);
56486
+ }
56487
+ if (include.commands) {
56488
+ addCandidates(discovered.commands.map((c2) => ({
56489
+ name: c2.name,
56490
+ description: c2.description,
56491
+ sourcePath: c2.sourcePath ?? ""
56492
+ })), "command", false);
56493
+ }
56494
+ if (include.skills) {
56495
+ addCandidates(discovered.skills.map((s) => ({
56496
+ name: s.name,
56497
+ description: s.description,
56498
+ path: s.path
56499
+ })), "skill", true);
56500
+ }
56501
+ if (include.config && discovered.configItem) {
56502
+ addCandidates([
56503
+ {
56504
+ name: discovered.configItem.name,
56505
+ description: undefined,
56506
+ sourcePath: discovered.configItem.sourcePath ?? ""
56507
+ }
56508
+ ], "config", false);
56509
+ }
56510
+ if (include.rules) {
56511
+ addCandidates(discovered.ruleItems.map((r2) => ({
56512
+ name: r2.name,
56513
+ description: undefined,
56514
+ sourcePath: r2.sourcePath ?? ""
56515
+ })), "rules", false);
56516
+ }
56517
+ if (include.hooks) {
56518
+ addCandidates(discovered.hookItems.map((h2) => ({
56519
+ name: h2.name,
56520
+ description: undefined,
56521
+ sourcePath: h2.sourcePath ?? ""
56522
+ })), "hooks", false);
56523
+ }
56524
+ const providerConfigs = selectedProviders.map((provider) => ({
56525
+ provider,
56526
+ global: globalParam
56527
+ }));
56528
+ const enabledTypes = ["agent", "command", "skill", "config", "rules", "hooks"].filter((type) => {
56529
+ const key = type === "agent" ? "agents" : type === "command" ? "commands" : type === "skill" ? "skills" : type === "config" ? "config" : type === "rules" ? "rules" : "hooks";
56530
+ return include[key];
56531
+ });
56532
+ const typeDirectoryStates = buildTypeDirectoryStates(providerConfigs, enabledTypes);
56533
+ res.status(200).json({ candidates, typeDirectoryStates });
56534
+ } catch (error) {
56535
+ res.status(500).json({
56536
+ error: "Failed to discover install candidates",
56537
+ message: sanitizeUntrusted(error, 260)
56538
+ });
56539
+ }
56540
+ });
56030
56541
  app.post("/api/migrate/execute", async (req, res) => {
56031
56542
  try {
56032
56543
  const planBased = req.body?.plan !== undefined;
@@ -56429,7 +56940,7 @@ function registerMigrationRoutes(app) {
56429
56940
  }
56430
56941
  });
56431
56942
  }
56432
- var MIGRATION_TYPES, MAX_PROVIDER_COUNT = 20, MAX_PLAN_ACTIONS = 5000, ALLOWED_CONFIG_SOURCE_KEYS, CONFLICT_RESOLUTION_SCHEMA, RECONCILE_ACTION_SCHEMA, RECONCILE_PLAN_SCHEMA, PLAN_EXECUTE_PAYLOAD_SCHEMA, PLURAL_TO_SINGULAR, shellHookWarningShown = false;
56943
+ var MIGRATION_TYPES, MAX_PROVIDER_COUNT = 20, MAX_PLAN_ACTIONS = 5000, ALLOWED_CONFIG_SOURCE_KEYS, CONFLICT_RESOLUTION_SCHEMA, RECONCILE_ACTION_SCHEMA, RECONCILE_BANNER_SCHEMA, RECONCILE_PLAN_SCHEMA, PLAN_EXECUTE_PAYLOAD_SCHEMA, PLURAL_TO_SINGULAR, shellHookWarningShown = false;
56433
56944
  var init_migration_routes = __esm(() => {
56434
56945
  init_agents_discovery();
56435
56946
  init_commands_discovery();
@@ -56481,8 +56992,20 @@ var init_migration_routes = __esm(() => {
56481
56992
  ownedSections: exports_external.array(exports_external.string()).optional(),
56482
56993
  affectedSections: exports_external.array(exports_external.string()).optional(),
56483
56994
  diff: exports_external.string().optional(),
56484
- resolution: CONFLICT_RESOLUTION_SCHEMA.optional()
56995
+ resolution: CONFLICT_RESOLUTION_SCHEMA.optional(),
56996
+ reasonCode: exports_external.string().optional(),
56997
+ reasonCopy: exports_external.string().optional(),
56998
+ isDirectoryItem: exports_external.boolean().optional()
56485
56999
  }).passthrough();
57000
+ RECONCILE_BANNER_SCHEMA = exports_external.object({
57001
+ kind: exports_external.enum(["empty-dir", "empty-dir-respected"]),
57002
+ provider: exports_external.string(),
57003
+ type: exports_external.string(),
57004
+ global: exports_external.boolean(),
57005
+ path: exports_external.string(),
57006
+ itemCount: exports_external.number().int().nonnegative(),
57007
+ message: exports_external.string()
57008
+ });
56486
57009
  RECONCILE_PLAN_SCHEMA = exports_external.object({
56487
57010
  actions: exports_external.array(RECONCILE_ACTION_SCHEMA).max(MAX_PLAN_ACTIONS),
56488
57011
  summary: exports_external.object({
@@ -56492,11 +57015,13 @@ var init_migration_routes = __esm(() => {
56492
57015
  conflict: exports_external.number().int().nonnegative(),
56493
57016
  delete: exports_external.number().int().nonnegative()
56494
57017
  }),
56495
- hasConflicts: exports_external.boolean()
57018
+ hasConflicts: exports_external.boolean(),
57019
+ banners: exports_external.array(RECONCILE_BANNER_SCHEMA).optional().default([])
56496
57020
  }).passthrough();
56497
57021
  PLAN_EXECUTE_PAYLOAD_SCHEMA = exports_external.object({
56498
57022
  plan: RECONCILE_PLAN_SCHEMA,
56499
- resolutions: exports_external.record(CONFLICT_RESOLUTION_SCHEMA).optional().default({})
57023
+ resolutions: exports_external.record(CONFLICT_RESOLUTION_SCHEMA).optional().default({}),
57024
+ mode: exports_external.enum(["reconcile", "install"]).optional()
56500
57025
  }).passthrough();
56501
57026
  PLURAL_TO_SINGULAR = {
56502
57027
  agents: "agent",
@@ -56509,7 +57034,7 @@ var init_migration_routes = __esm(() => {
56509
57034
  });
56510
57035
 
56511
57036
  // src/domains/plan-parser/plan-metadata.ts
56512
- import { readFileSync as readFileSync6, statSync as statSync5 } from "node:fs";
57037
+ import { readFileSync as readFileSync6, statSync as statSync6 } from "node:fs";
56513
57038
  function readMatter(filePath) {
56514
57039
  try {
56515
57040
  return import_gray_matter6.default(readFileSync6(filePath, "utf8")).data;
@@ -56586,7 +57111,7 @@ function parseEffortHours(value) {
56586
57111
  }
56587
57112
  function readPlanMetadata(planFile, counts) {
56588
57113
  const frontmatter = readMatter(planFile);
56589
- const stats = statSync5(planFile);
57114
+ const stats = statSync6(planFile);
56590
57115
  return {
56591
57116
  title: typeof frontmatter.title === "string" ? frontmatter.title : undefined,
56592
57117
  description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
@@ -56603,7 +57128,7 @@ function readPlanMetadata(planFile, counts) {
56603
57128
  }
56604
57129
  function readPhaseMetadata(phaseFile) {
56605
57130
  const frontmatter = readMatter(phaseFile);
56606
- const stats = statSync5(phaseFile);
57131
+ const stats = statSync6(phaseFile);
56607
57132
  return {
56608
57133
  title: typeof frontmatter.title === "string" ? frontmatter.title : undefined,
56609
57134
  status: normalizePhaseStatus(frontmatter.status),
@@ -57000,7 +57525,7 @@ var init_plan_table_parser = __esm(() => {
57000
57525
 
57001
57526
  // src/domains/plan-parser/activity-tracker.ts
57002
57527
  import { spawnSync as spawnSync2 } from "node:child_process";
57003
- import { readdirSync as readdirSync3, statSync as statSync6 } from "node:fs";
57528
+ import { readdirSync as readdirSync4, statSync as statSync7 } from "node:fs";
57004
57529
  import { join as join42, relative as relative8 } from "node:path";
57005
57530
  function startOfDay(date) {
57006
57531
  const copy = new Date(date);
@@ -57010,7 +57535,7 @@ function startOfDay(date) {
57010
57535
  function enumerateMarkdownFiles(dir, depth = 0) {
57011
57536
  if (depth >= MAX_DEPTH)
57012
57537
  return [];
57013
- const entries = readdirSync3(dir, { withFileTypes: true });
57538
+ const entries = readdirSync4(dir, { withFileTypes: true });
57014
57539
  return entries.flatMap((entry) => {
57015
57540
  const entryPath = join42(dir, entry.name);
57016
57541
  if (entry.isDirectory())
@@ -57054,7 +57579,7 @@ function getGitSamples(dir, samples) {
57054
57579
  }
57055
57580
  function getMtimeSamples(dir, samples) {
57056
57581
  for (const file of enumerateMarkdownFiles(dir)) {
57057
- const mtimeKey = startOfDay(statSync6(file).mtime).toISOString();
57582
+ const mtimeKey = startOfDay(statSync7(file).mtime).toISOString();
57058
57583
  const sample = samples.get(mtimeKey);
57059
57584
  if (!sample)
57060
57585
  continue;
@@ -57117,13 +57642,13 @@ var DAY_MS = 86400000, CELL_COUNT = 84, MAX_DEPTH = 10;
57117
57642
  var init_activity_tracker = () => {};
57118
57643
 
57119
57644
  // src/domains/plan-parser/plan-scanner.ts
57120
- import { existsSync as existsSync29, readdirSync as readdirSync4 } from "node:fs";
57645
+ import { existsSync as existsSync29, readdirSync as readdirSync5 } from "node:fs";
57121
57646
  import { join as join43 } from "node:path";
57122
57647
  function scanPlanDir(dir) {
57123
57648
  if (!existsSync29(dir))
57124
57649
  return [];
57125
57650
  try {
57126
- return readdirSync4(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join43(dir, entry.name, "plan.md")).filter(existsSync29);
57651
+ return readdirSync5(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join43(dir, entry.name, "plan.md")).filter(existsSync29);
57127
57652
  } catch {
57128
57653
  return [];
57129
57654
  }
@@ -60169,7 +60694,7 @@ var init_skill_browser_routes = __esm(() => {
60169
60694
  });
60170
60695
 
60171
60696
  // src/commands/skills/agents.ts
60172
- import { existsSync as existsSync37, readdirSync as readdirSync5, statSync as statSync7 } from "node:fs";
60697
+ import { existsSync as existsSync37, readdirSync as readdirSync6, statSync as statSync8 } from "node:fs";
60173
60698
  import { homedir as homedir37, platform as platform5 } from "node:os";
60174
60699
  import { join as join54 } from "node:path";
60175
60700
  function hasInstallSignal2(path6) {
@@ -60177,9 +60702,9 @@ function hasInstallSignal2(path6) {
60177
60702
  return false;
60178
60703
  }
60179
60704
  try {
60180
- const stat10 = statSync7(path6);
60705
+ const stat10 = statSync8(path6);
60181
60706
  if (stat10.isDirectory()) {
60182
- return readdirSync5(path6).length > 0;
60707
+ return readdirSync6(path6).length > 0;
60183
60708
  }
60184
60709
  if (stat10.isFile()) {
60185
60710
  return true;
@@ -61558,7 +62083,7 @@ var package_default;
61558
62083
  var init_package = __esm(() => {
61559
62084
  package_default = {
61560
62085
  name: "claudekit-cli",
61561
- version: "3.41.4-dev.47",
62086
+ version: "3.41.4-dev.49",
61562
62087
  description: "CLI tool for bootstrapping and updating ClaudeKit projects",
61563
62088
  type: "module",
61564
62089
  repository: {
@@ -61601,6 +62126,8 @@ var init_package = __esm(() => {
61601
62126
  "test:integration": "CK_RUN_CLI_INTEGRATION=1 bun test tests/integration/cli.test.ts",
61602
62127
  "test:watch": "bun test --watch",
61603
62128
  "test:quick": "./scripts/dev-quick-start.sh test",
62129
+ "test:e2e": "playwright test --config=playwright.config.ts",
62130
+ "test:e2e:ui": "playwright test --ui --config=playwright.config.ts",
61604
62131
  lint: "biome check .",
61605
62132
  "lint:fix": "biome check --fix .",
61606
62133
  "lint:fix-unsafe": "biome check --fix --unsafe .",
@@ -61659,6 +62186,7 @@ var init_package = __esm(() => {
61659
62186
  },
61660
62187
  devDependencies: {
61661
62188
  "@biomejs/biome": "^1.9.4",
62189
+ "@playwright/test": "^1.59.1",
61662
62190
  "@semantic-release/changelog": "^6.0.3",
61663
62191
  "@semantic-release/git": "^10.0.1",
61664
62192
  "@tauri-apps/cli": "^2",
@@ -72856,7 +73384,7 @@ var init_content_validator = __esm(() => {
72856
73384
 
72857
73385
  // src/commands/content/phases/context-cache-manager.ts
72858
73386
  import { createHash as createHash9 } from "node:crypto";
72859
- import { existsSync as existsSync75, mkdirSync as mkdirSync5, readFileSync as readFileSync18, readdirSync as readdirSync10, statSync as statSync13 } from "node:fs";
73387
+ import { existsSync as existsSync75, mkdirSync as mkdirSync5, readFileSync as readFileSync18, readdirSync as readdirSync11, statSync as statSync14 } from "node:fs";
72860
73388
  import { rename as rename14, writeFile as writeFile38 } from "node:fs/promises";
72861
73389
  import { homedir as homedir52 } from "node:os";
72862
73390
  import { basename as basename30, join as join158 } from "node:path";
@@ -72892,7 +73420,7 @@ function computeSourceHash(repoPath) {
72892
73420
  const paths = getDocSourcePaths(repoPath);
72893
73421
  for (const filePath of paths) {
72894
73422
  try {
72895
- const stat25 = statSync13(filePath);
73423
+ const stat25 = statSync14(filePath);
72896
73424
  hash.update(`${filePath}:${stat25.mtimeMs}`);
72897
73425
  } catch {
72898
73426
  hash.update(`${filePath}:0`);
@@ -72905,7 +73433,7 @@ function getDocSourcePaths(repoPath) {
72905
73433
  const docsDir = join158(repoPath, "docs");
72906
73434
  if (existsSync75(docsDir)) {
72907
73435
  try {
72908
- const files = readdirSync10(docsDir);
73436
+ const files = readdirSync11(docsDir);
72909
73437
  for (const f3 of files) {
72910
73438
  if (f3.endsWith(".md"))
72911
73439
  paths.push(join158(docsDir, f3));
@@ -72918,7 +73446,7 @@ function getDocSourcePaths(repoPath) {
72918
73446
  const stylesDir = join158(repoPath, "assets", "writing-styles");
72919
73447
  if (existsSync75(stylesDir)) {
72920
73448
  try {
72921
- const files = readdirSync10(stylesDir);
73449
+ const files = readdirSync11(stylesDir);
72922
73450
  for (const f3 of files) {
72923
73451
  paths.push(join158(stylesDir, f3));
72924
73452
  }
@@ -73113,7 +73641,7 @@ function extractContentFromResponse(response) {
73113
73641
 
73114
73642
  // src/commands/content/phases/docs-summarizer.ts
73115
73643
  import { execSync as execSync7 } from "node:child_process";
73116
- import { existsSync as existsSync76, readFileSync as readFileSync19, readdirSync as readdirSync11 } from "node:fs";
73644
+ import { existsSync as existsSync76, readFileSync as readFileSync19, readdirSync as readdirSync12 } from "node:fs";
73117
73645
  import { join as join159 } from "node:path";
73118
73646
  async function summarizeProjectDocs(repoPath, contentLogger) {
73119
73647
  const rawContent = collectRawDocs(repoPath);
@@ -73171,7 +73699,7 @@ function collectRawDocs(repoPath) {
73171
73699
  const docsDir = join159(repoPath, "docs");
73172
73700
  if (existsSync76(docsDir)) {
73173
73701
  try {
73174
- const files = readdirSync11(docsDir).filter((f3) => f3.endsWith(".md")).sort();
73702
+ const files = readdirSync12(docsDir).filter((f3) => f3.endsWith(".md")).sort();
73175
73703
  for (const f3 of files) {
73176
73704
  const content = readCapped(join159(docsDir, f3), 5000);
73177
73705
  if (content) {
@@ -73195,7 +73723,7 @@ ${content}`);
73195
73723
  const stylesDir = join159(repoPath, "assets", "writing-styles");
73196
73724
  if (existsSync76(stylesDir)) {
73197
73725
  try {
73198
- const files = readdirSync11(stylesDir).slice(0, 3);
73726
+ const files = readdirSync12(stylesDir).slice(0, 3);
73199
73727
  styles3 = files.map((f3) => readCapped(join159(stylesDir, f3), 1000)).filter(Boolean).join(`
73200
73728
 
73201
73729
  `);
@@ -73386,7 +73914,7 @@ IMPORTANT: Generate the image and output the path as JSON: {"imagePath": "/path/
73386
73914
 
73387
73915
  // src/commands/content/phases/photo-generator.ts
73388
73916
  import { execSync as execSync8 } from "node:child_process";
73389
- import { existsSync as existsSync77, mkdirSync as mkdirSync6, readdirSync as readdirSync12 } from "node:fs";
73917
+ import { existsSync as existsSync77, mkdirSync as mkdirSync6, readdirSync as readdirSync13 } from "node:fs";
73390
73918
  import { homedir as homedir53 } from "node:os";
73391
73919
  import { join as join160 } from "node:path";
73392
73920
  async function generatePhoto(_content, context, config, platform17, contentId, contentLogger) {
@@ -73411,7 +73939,7 @@ async function generatePhoto(_content, context, config, platform17, contentId, c
73411
73939
  return { path: imagePath, ...dimensions, format: "png" };
73412
73940
  }
73413
73941
  }
73414
- const files = readdirSync12(mediaDir);
73942
+ const files = readdirSync13(mediaDir);
73415
73943
  const imageFile = files.find((f3) => /\.(png|jpg|jpeg|webp)$/i.test(f3));
73416
73944
  if (imageFile) {
73417
73945
  const ext2 = imageFile.split(".").pop() ?? "png";
@@ -73503,7 +74031,7 @@ var init_content_creator = __esm(() => {
73503
74031
  });
73504
74032
 
73505
74033
  // src/commands/content/phases/content-logger.ts
73506
- import { createWriteStream as createWriteStream4, existsSync as existsSync78, mkdirSync as mkdirSync7, statSync as statSync14 } from "node:fs";
74034
+ import { createWriteStream as createWriteStream4, existsSync as existsSync78, mkdirSync as mkdirSync7, statSync as statSync15 } from "node:fs";
73507
74035
  import { homedir as homedir54 } from "node:os";
73508
74036
  import { join as join161 } from "node:path";
73509
74037
 
@@ -73569,7 +74097,7 @@ class ContentLogger {
73569
74097
  if (this.maxBytes > 0 && this.stream) {
73570
74098
  const logPath = join161(this.logDir, `content-${this.currentDate}.log`);
73571
74099
  try {
73572
- const stat25 = statSync14(logPath);
74100
+ const stat25 = statSync15(logPath);
73573
74101
  if (stat25.size >= this.maxBytes) {
73574
74102
  this.close();
73575
74103
  const suffix = Date.now();
@@ -73798,7 +74326,7 @@ function isNoiseCommit(title, author) {
73798
74326
 
73799
74327
  // src/commands/content/phases/change-detector.ts
73800
74328
  import { execSync as execSync10, spawnSync as spawnSync9 } from "node:child_process";
73801
- import { existsSync as existsSync80, readFileSync as readFileSync20, readdirSync as readdirSync13, statSync as statSync15 } from "node:fs";
74329
+ import { existsSync as existsSync80, readFileSync as readFileSync20, readdirSync as readdirSync14, statSync as statSync16 } from "node:fs";
73802
74330
  import { join as join162 } from "node:path";
73803
74331
  function detectCommits(repo, since) {
73804
74332
  try {
@@ -73914,7 +74442,7 @@ function detectCompletedPlans(repo, since) {
73914
74442
  const sinceMs = new Date(since).getTime();
73915
74443
  const events = [];
73916
74444
  try {
73917
- const entries = readdirSync13(plansDir, { withFileTypes: true });
74445
+ const entries = readdirSync14(plansDir, { withFileTypes: true });
73918
74446
  for (const entry of entries) {
73919
74447
  if (!entry.isDirectory())
73920
74448
  continue;
@@ -73922,7 +74450,7 @@ function detectCompletedPlans(repo, since) {
73922
74450
  if (!existsSync80(planFile))
73923
74451
  continue;
73924
74452
  try {
73925
- const stat25 = statSync15(planFile);
74453
+ const stat25 = statSync16(planFile);
73926
74454
  if (stat25.mtimeMs < sinceMs)
73927
74455
  continue;
73928
74456
  const content = readFileSync20(planFile, "utf-8");
@@ -73995,7 +74523,7 @@ function classifyCommit(event) {
73995
74523
 
73996
74524
  // src/commands/content/phases/repo-discoverer.ts
73997
74525
  import { execSync as execSync11 } from "node:child_process";
73998
- import { readdirSync as readdirSync14 } from "node:fs";
74526
+ import { readdirSync as readdirSync15 } from "node:fs";
73999
74527
  import { join as join163 } from "node:path";
74000
74528
  function discoverRepos2(cwd2) {
74001
74529
  const repos = [];
@@ -74005,7 +74533,7 @@ function discoverRepos2(cwd2) {
74005
74533
  repos.push(info);
74006
74534
  }
74007
74535
  try {
74008
- const entries = readdirSync14(cwd2, { withFileTypes: true });
74536
+ const entries = readdirSync15(cwd2, { withFileTypes: true });
74009
74537
  for (const entry of entries) {
74010
74538
  if (!entry.isDirectory() || entry.name.startsWith("."))
74011
74539
  continue;
@@ -76715,19 +77243,40 @@ var init_migrate_command_help = __esm(() => {
76715
77243
  usage: "ck migrate [options]",
76716
77244
  examples: [
76717
77245
  {
76718
- command: "ck migrate --agent codex --dry-run",
76719
- description: "Preview the destination-aware migration plan before writing files"
77246
+ command: "ck migrate --install",
77247
+ description: "Pick items to install interactively (install picker mode)"
76720
77248
  },
76721
77249
  {
76722
- command: "ck migrate --agent codex -g",
76723
- description: "Write to Codex global paths such as ~/.codex/ and ~/.agents/skills"
77250
+ command: "ck migrate --agent codex --dry-run",
77251
+ description: "Preview the destination-aware reconcile plan before writing files"
76724
77252
  },
76725
77253
  {
76726
- command: "CK_FORCE_ASCII=1 ck migrate --agent codex",
76727
- description: "Force ASCII borders on legacy Windows terminals (cmd.exe, older PowerShell)"
77254
+ command: "ck migrate --respect-deletions",
77255
+ description: "Preserve empty directories do not auto-reinstall deleted items"
76728
77256
  }
76729
77257
  ],
76730
77258
  optionGroups: [
77259
+ {
77260
+ title: "Mode Options",
77261
+ options: [
77262
+ {
77263
+ flags: "--install",
77264
+ description: "Opt-in install picker mode — interactively select which items to install (default when registry is empty or has unknown checksums)"
77265
+ },
77266
+ {
77267
+ flags: "--reconcile",
77268
+ description: "Force reconcile mode — compute diff vs registry and apply only changes (default when registry is valid)"
77269
+ },
77270
+ {
77271
+ flags: "--reinstall-empty-dirs",
77272
+ description: "Reinstall all items when their type directory is empty or missing (default: true). Use --respect-deletions to disable."
77273
+ },
77274
+ {
77275
+ flags: "--respect-deletions",
77276
+ description: "Preserve deletion even when a type directory is empty — skip reinstall heuristic. Mutually exclusive with --reinstall-empty-dirs."
77277
+ }
77278
+ ]
77279
+ },
76731
77280
  {
76732
77281
  title: "Target Options",
76733
77282
  options: [
@@ -76790,6 +77339,19 @@ var init_migrate_command_help = __esm(() => {
76790
77339
  }
76791
77340
  ]
76792
77341
  }
77342
+ ],
77343
+ sections: [
77344
+ {
77345
+ title: "Gotchas",
77346
+ content: [
77347
+ " --install and --reconcile are mutually exclusive — pass only one",
77348
+ " --reinstall-empty-dirs and --respect-deletions are mutually exclusive — pass only one",
77349
+ " Default mode is smart-detected: no/stale registry → install, valid registry → reconcile",
77350
+ " --respect-deletions disables the auto-reinstall heuristic for empty directories",
77351
+ " --force overrides skip decisions per item; --reinstall-empty-dirs is a per-directory heuristic"
77352
+ ].join(`
77353
+ `)
77354
+ }
76793
77355
  ]
76794
77356
  };
76795
77357
  });
@@ -84267,7 +84829,7 @@ async function checkCliInstallMethod() {
84267
84829
  };
84268
84830
  }
84269
84831
  // src/domains/health-checks/checkers/claude-md-checker.ts
84270
- import { existsSync as existsSync48, statSync as statSync8 } from "node:fs";
84832
+ import { existsSync as existsSync48, statSync as statSync9 } from "node:fs";
84271
84833
  import { join as join76 } from "node:path";
84272
84834
  function checkClaudeMd(setup, projectDir) {
84273
84835
  const results = [];
@@ -84293,7 +84855,7 @@ function checkClaudeMdFile(path6, name, id) {
84293
84855
  };
84294
84856
  }
84295
84857
  try {
84296
- const stat13 = statSync8(path6);
84858
+ const stat13 = statSync9(path6);
84297
84859
  const sizeKB = (stat13.size / 1024).toFixed(1);
84298
84860
  if (stat13.size === 0) {
84299
84861
  return {
@@ -85304,7 +85866,7 @@ init_command_normalizer();
85304
85866
  init_logger();
85305
85867
  init_path_resolver();
85306
85868
  import { spawnSync as spawnSync3 } from "node:child_process";
85307
- import { existsSync as existsSync55, readFileSync as readFileSync15, statSync as statSync9, writeFileSync as writeFileSync5 } from "node:fs";
85869
+ import { existsSync as existsSync55, readFileSync as readFileSync15, statSync as statSync10, writeFileSync as writeFileSync5 } from "node:fs";
85308
85870
  import { readdir as readdir21 } from "node:fs/promises";
85309
85871
  import { homedir as homedir43, tmpdir as tmpdir2 } from "node:os";
85310
85872
  import { join as join86, resolve as resolve30 } from "node:path";
@@ -86152,7 +86714,7 @@ async function checkHookLogs(projectDir) {
86152
86714
  };
86153
86715
  }
86154
86716
  try {
86155
- const logStats = statSync9(logPath);
86717
+ const logStats = statSync10(logPath);
86156
86718
  if (logStats.size > MAX_LOG_FILE_SIZE_BYTES) {
86157
86719
  return {
86158
86720
  id: "hook-logs",
@@ -98194,7 +98756,7 @@ async function handleDownload(ctx) {
98194
98756
  import { join as join122 } from "node:path";
98195
98757
 
98196
98758
  // src/domains/installation/deletion-handler.ts
98197
- import { existsSync as existsSync61, lstatSync as lstatSync3, readdirSync as readdirSync6, rmSync as rmSync2, rmdirSync, unlinkSync as unlinkSync4 } from "node:fs";
98759
+ import { existsSync as existsSync61, lstatSync as lstatSync3, readdirSync as readdirSync7, rmSync as rmSync2, rmdirSync, unlinkSync as unlinkSync4 } from "node:fs";
98198
98760
  import { dirname as dirname35, join as join108, relative as relative17, resolve as resolve34, sep as sep10 } from "node:path";
98199
98761
 
98200
98762
  // src/services/file-operations/manifest/manifest-reader.ts
@@ -98388,7 +98950,7 @@ function collectFilesRecursively(dir, baseDir) {
98388
98950
  if (!existsSync61(dir))
98389
98951
  return results;
98390
98952
  try {
98391
- const entries = readdirSync6(dir, { withFileTypes: true });
98953
+ const entries = readdirSync7(dir, { withFileTypes: true });
98392
98954
  for (const entry of entries) {
98393
98955
  const fullPath = join108(dir, entry.name);
98394
98956
  const relativePath = relative17(baseDir, fullPath);
@@ -98426,7 +98988,7 @@ function cleanupEmptyDirectories(filePath, claudeDir3) {
98426
98988
  while (currentDir !== normalizedClaudeDir && currentDir.startsWith(normalizedClaudeDir) && iterations < MAX_CLEANUP_ITERATIONS) {
98427
98989
  iterations++;
98428
98990
  try {
98429
- const entries = readdirSync6(currentDir);
98991
+ const entries = readdirSync7(currentDir);
98430
98992
  if (entries.length === 0) {
98431
98993
  rmdirSync(currentDir);
98432
98994
  logger.debug(`Removed empty directory: ${currentDir}`);
@@ -104104,7 +104666,7 @@ async function runPreflightChecks() {
104104
104666
 
104105
104667
  // src/domains/installation/fresh-installer.ts
104106
104668
  init_metadata_migration();
104107
- import { existsSync as existsSync63, readdirSync as readdirSync7, rmSync as rmSync3, rmdirSync as rmdirSync2, unlinkSync as unlinkSync5 } from "node:fs";
104669
+ import { existsSync as existsSync63, readdirSync as readdirSync8, rmSync as rmSync3, rmdirSync as rmdirSync2, unlinkSync as unlinkSync5 } from "node:fs";
104108
104670
  import { basename as basename24, dirname as dirname39, join as join134, resolve as resolve36 } from "node:path";
104109
104671
  init_logger();
104110
104672
  init_safe_spinner();
@@ -104157,7 +104719,7 @@ function cleanupEmptyDirectories2(filePath, claudeDir3) {
104157
104719
  let currentDir = resolve36(dirname39(filePath));
104158
104720
  while (currentDir !== normalizedClaudeDir && currentDir.startsWith(normalizedClaudeDir)) {
104159
104721
  try {
104160
- const entries = readdirSync7(currentDir);
104722
+ const entries = readdirSync8(currentDir);
104161
104723
  if (entries.length === 0) {
104162
104724
  rmdirSync2(currentDir);
104163
104725
  logger.debug(`Removed empty directory: ${currentDir}`);
@@ -106857,6 +107419,30 @@ function buildProviderScopeSubtitle(selectedProviders, global3) {
106857
107419
  }
106858
107420
  return `${selectedProviders.length} providers -> ${scope}`;
106859
107421
  }
107422
+ function renderBannerLines(banner) {
107423
+ const width = 64;
107424
+ const bar = `+${"=".repeat(width)}+`;
107425
+ const homePath = process.env.HOME ?? "";
107426
+ const displayPath = banner.path.replace(homePath, "~");
107427
+ if (banner.kind === "empty-dir") {
107428
+ return [
107429
+ bar,
107430
+ `| [i] Detected empty ${displayPath}`,
107431
+ `| ${banner.itemCount} item(s) below will be reinstalled.`,
107432
+ "| Use --respect-deletions to preserve deletion.",
107433
+ bar
107434
+ ];
107435
+ }
107436
+ if (banner.kind === "empty-dir-respected") {
107437
+ return [
107438
+ bar,
107439
+ `| [i] Detected empty ${displayPath}`,
107440
+ `| ${banner.itemCount} item(s) skipped (--respect-deletions active).`,
107441
+ bar
107442
+ ];
107443
+ }
107444
+ return [];
107445
+ }
106860
107446
  function buildSourceSummaryLines(counts, origins) {
106861
107447
  const parts = [];
106862
107448
  if (counts.agents > 0)
@@ -106880,6 +107466,148 @@ function buildSourceSummaryLines(counts, origins) {
106880
107466
 
106881
107467
  // src/commands/migrate/migrate-command.ts
106882
107468
  init_skill_directory_installer();
107469
+ function validateMutualExclusion(options2) {
107470
+ if (options2.install && options2.reconcile) {
107471
+ return "Pass either --install or --reconcile, not both.";
107472
+ }
107473
+ if (options2.reinstallEmptyDirs && options2.respectDeletions) {
107474
+ return "Pass either --reinstall-empty-dirs or --respect-deletions, not both.";
107475
+ }
107476
+ return null;
107477
+ }
107478
+ function resolveMigrationMode(options2, hasUnknownChecksums) {
107479
+ if (options2.install)
107480
+ return "install";
107481
+ if (options2.reconcile)
107482
+ return "reconcile";
107483
+ if (hasUnknownChecksums)
107484
+ return "install";
107485
+ return "reconcile";
107486
+ }
107487
+ function renderBanners(banners) {
107488
+ if (banners.length === 0)
107489
+ return;
107490
+ for (const banner of banners) {
107491
+ const lines = renderBannerLines(banner);
107492
+ if (lines.length > 0) {
107493
+ console.log();
107494
+ for (const line of lines) {
107495
+ console.log(line);
107496
+ }
107497
+ }
107498
+ }
107499
+ }
107500
+ async function runInstallMode(options2, discoveredItems, _selectedProviders, _installGlobally) {
107501
+ const interactive = process.stdout.isTTY && !options2.yes;
107502
+ if (!interactive) {
107503
+ return discoveredItems;
107504
+ }
107505
+ const toOption = (item) => ({
107506
+ value: item.name,
107507
+ label: item.name,
107508
+ hint: item.recommended ? "Recommended" : undefined
107509
+ });
107510
+ let selectedAgents = discoveredItems.agents;
107511
+ let selectedCommands = discoveredItems.commands;
107512
+ let selectedSkills = discoveredItems.skills;
107513
+ let selectedConfig = discoveredItems.configItem;
107514
+ let selectedRules = discoveredItems.ruleItems;
107515
+ let selectedHooks = discoveredItems.hookItems;
107516
+ if (discoveredItems.agents.length > 0) {
107517
+ const picked = await ae({
107518
+ message: `Select agents to install (${discoveredItems.agents.length} available)`,
107519
+ options: discoveredItems.agents.map(toOption),
107520
+ initialValues: discoveredItems.agents.map((a3) => a3.name),
107521
+ required: false
107522
+ });
107523
+ if (lD(picked)) {
107524
+ ue("Migrate cancelled");
107525
+ process.exit(0);
107526
+ }
107527
+ const pickedSet = new Set(picked);
107528
+ selectedAgents = discoveredItems.agents.filter((a3) => pickedSet.has(a3.name));
107529
+ }
107530
+ if (discoveredItems.commands.length > 0) {
107531
+ const picked = await ae({
107532
+ message: `Select commands to install (${discoveredItems.commands.length} available)`,
107533
+ options: discoveredItems.commands.map(toOption),
107534
+ initialValues: discoveredItems.commands.map((c2) => c2.name),
107535
+ required: false
107536
+ });
107537
+ if (lD(picked)) {
107538
+ ue("Migrate cancelled");
107539
+ process.exit(0);
107540
+ }
107541
+ const pickedSet = new Set(picked);
107542
+ selectedCommands = discoveredItems.commands.filter((c2) => pickedSet.has(c2.name));
107543
+ }
107544
+ if (discoveredItems.skills.length > 0) {
107545
+ const picked = await ae({
107546
+ message: `Select skills to install (${discoveredItems.skills.length} available, directory-level)`,
107547
+ options: discoveredItems.skills.map((s) => ({
107548
+ value: s.name,
107549
+ label: s.name,
107550
+ hint: "skill directory"
107551
+ })),
107552
+ initialValues: discoveredItems.skills.map((s) => s.name),
107553
+ required: false
107554
+ });
107555
+ if (lD(picked)) {
107556
+ ue("Migrate cancelled");
107557
+ process.exit(0);
107558
+ }
107559
+ const pickedSet = new Set(picked);
107560
+ selectedSkills = discoveredItems.skills.filter((s) => pickedSet.has(s.name));
107561
+ }
107562
+ if (discoveredItems.configItem) {
107563
+ const include = await se({
107564
+ message: "Include CLAUDE.md config?",
107565
+ initialValue: true
107566
+ });
107567
+ if (lD(include)) {
107568
+ ue("Migrate cancelled");
107569
+ process.exit(0);
107570
+ }
107571
+ if (!include)
107572
+ selectedConfig = null;
107573
+ }
107574
+ if (discoveredItems.ruleItems.length > 0) {
107575
+ const picked = await ae({
107576
+ message: `Select rules to install (${discoveredItems.ruleItems.length} available)`,
107577
+ options: discoveredItems.ruleItems.map(toOption),
107578
+ initialValues: discoveredItems.ruleItems.map((r2) => r2.name),
107579
+ required: false
107580
+ });
107581
+ if (lD(picked)) {
107582
+ ue("Migrate cancelled");
107583
+ process.exit(0);
107584
+ }
107585
+ const pickedSet = new Set(picked);
107586
+ selectedRules = discoveredItems.ruleItems.filter((r2) => pickedSet.has(r2.name));
107587
+ }
107588
+ if (discoveredItems.hookItems.length > 0) {
107589
+ const picked = await ae({
107590
+ message: `Select hooks to install (${discoveredItems.hookItems.length} available)`,
107591
+ options: discoveredItems.hookItems.map(toOption),
107592
+ initialValues: discoveredItems.hookItems.map((h2) => h2.name),
107593
+ required: false
107594
+ });
107595
+ if (lD(picked)) {
107596
+ ue("Migrate cancelled");
107597
+ process.exit(0);
107598
+ }
107599
+ const pickedSet = new Set(picked);
107600
+ selectedHooks = discoveredItems.hookItems.filter((h2) => pickedSet.has(h2.name));
107601
+ }
107602
+ return {
107603
+ agents: selectedAgents,
107604
+ commands: selectedCommands,
107605
+ skills: selectedSkills,
107606
+ configItem: selectedConfig,
107607
+ ruleItems: selectedRules,
107608
+ hookItems: selectedHooks
107609
+ };
107610
+ }
106883
107611
  function getProviderPathKey(type) {
106884
107612
  switch (type) {
106885
107613
  case "agent":
@@ -106894,8 +107622,6 @@ function getProviderPathKey(type) {
106894
107622
  return "hooks";
106895
107623
  case "skill":
106896
107624
  return "skills";
106897
- default:
106898
- return type;
106899
107625
  }
106900
107626
  }
106901
107627
  function shouldExecuteAction2(action) {
@@ -106977,6 +107703,11 @@ function inferKitTypeFromSourceMetadata(sourceMetadata) {
106977
107703
  }
106978
107704
  async function migrateCommand(options2) {
106979
107705
  console.log();
107706
+ const mutexError = validateMutualExclusion(options2);
107707
+ if (mutexError) {
107708
+ f2.error(mutexError);
107709
+ process.exit(1);
107710
+ }
106980
107711
  try {
106981
107712
  const scope = resolveMigrationScope(process.argv.slice(2), options2);
106982
107713
  const spinner = de();
@@ -107170,31 +107901,66 @@ async function migrateCommand(options2) {
107170
107901
  setTaxonomyOverrides(ckConfigResult.config.modelTaxonomy);
107171
107902
  const reconcileSpinner = de();
107172
107903
  reconcileSpinner.start("Computing migration plan...");
107904
+ const registry = await readPortableRegistry();
107905
+ const hasUnknownChecksums = registry.installations.some((entry) => !entry.sourceChecksum || entry.sourceChecksum === "unknown" || !entry.targetChecksum || entry.targetChecksum === "unknown");
107906
+ const migrationMode = resolveMigrationMode(options2, hasUnknownChecksums);
107907
+ let effectiveAgents = agents2;
107908
+ let effectiveCommands = commands;
107909
+ let effectiveSkills = skills;
107910
+ let effectiveConfigItem = configItem;
107911
+ let effectiveRuleItems = ruleItems;
107912
+ let effectiveHookItems = hookItems;
107913
+ if (migrationMode === "install") {
107914
+ reconcileSpinner.stop("Discovery complete");
107915
+ if (!options2.yes && process.stdout.isTTY) {
107916
+ f2.info(`[i] Smart default: ${hasUnknownChecksums ? "unknown checksums detected" : "--install flag"} — entering install picker mode.`);
107917
+ }
107918
+ const picked = await runInstallMode(options2, {
107919
+ agents: agents2,
107920
+ commands,
107921
+ skills,
107922
+ configItem,
107923
+ ruleItems,
107924
+ hookItems
107925
+ }, selectedProviders, installGlobally);
107926
+ effectiveAgents = picked.agents;
107927
+ effectiveCommands = picked.commands;
107928
+ effectiveSkills = picked.skills;
107929
+ effectiveConfigItem = picked.configItem;
107930
+ effectiveRuleItems = picked.ruleItems;
107931
+ effectiveHookItems = picked.hookItems;
107932
+ reconcileSpinner.start("Computing migration plan...");
107933
+ }
107173
107934
  const sourceStates = await computeSourceStates({
107174
- agents: agents2,
107175
- commands,
107176
- config: configItem,
107177
- rules: ruleItems,
107178
- hooks: hookItems
107935
+ agents: effectiveAgents,
107936
+ commands: effectiveCommands,
107937
+ config: effectiveConfigItem,
107938
+ rules: effectiveRuleItems,
107939
+ hooks: effectiveHookItems
107179
107940
  }, selectedProviders);
107180
107941
  const targetStates = await computeTargetStates(selectedProviders, installGlobally);
107181
- const registry = await readPortableRegistry();
107182
107942
  const providerConfigs = selectedProviders.map((provider) => ({
107183
107943
  provider,
107184
107944
  global: installGlobally
107185
107945
  }));
107946
+ const portableTypes = ["agent", "command", "config", "rules", "hooks"];
107947
+ const typeDirectoryStates = buildTypeDirectoryStates(selectedProviders.map((provider) => ({ provider, global: installGlobally })), [...portableTypes]);
107948
+ const reinstallEmptyDirs = options2.respectDeletions ? false : options2.reinstallEmptyDirs ?? true;
107186
107949
  const plan = reconcile({
107187
107950
  sourceItems: sourceStates,
107188
107951
  registry,
107189
107952
  targetStates,
107190
107953
  providerConfigs,
107191
- force: options2.force
107954
+ force: options2.force,
107955
+ typeDirectoryStates,
107956
+ respectDeletions: !reinstallEmptyDirs
107192
107957
  });
107193
107958
  reconcileSpinner.stop("Plan computed");
107194
107959
  const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
107960
+ renderBanners(plan.banners);
107195
107961
  displayReconcilePlan(plan, { color: useColor });
107196
107962
  if (options2.dryRun) {
107197
- displayMigrationSummary(plan, buildDryRunFallbackResults(skills, selectedProviders, installGlobally, plan.actions), { color: useColor, dryRun: true });
107963
+ displayMigrationSummary(plan, buildDryRunFallbackResults(effectiveSkills, selectedProviders, installGlobally, plan.actions), { color: useColor, dryRun: true });
107198
107964
  return;
107199
107965
  }
107200
107966
  if (plan.hasConflicts) {
@@ -107204,7 +107970,7 @@ async function migrateCommand(options2) {
107204
107970
  if (!action.diff && action.targetPath && existsSync64(action.targetPath)) {
107205
107971
  try {
107206
107972
  const targetContent = await readFile61(action.targetPath, "utf-8");
107207
- const sourceItem = agents2.find((a3) => a3.name === action.item) || commands.find((c2) => c2.name === action.item) || (configItem?.name === action.item ? configItem : null) || ruleItems.find((r2) => r2.name === action.item) || hookItems.find((h2) => h2.name === action.item);
107973
+ const sourceItem = effectiveAgents.find((a3) => a3.name === action.item) || effectiveCommands.find((c2) => c2.name === action.item) || (effectiveConfigItem?.name === action.item ? effectiveConfigItem : null) || effectiveRuleItems.find((r2) => r2.name === action.item) || effectiveHookItems.find((h2) => h2.name === action.item);
107208
107974
  if (sourceItem) {
107209
107975
  const providerConfig = providers[action.provider];
107210
107976
  const pathConfigKey = getProviderPathKey(action.type);
@@ -107242,12 +108008,12 @@ async function migrateCommand(options2) {
107242
108008
  }
107243
108009
  let allResults = [];
107244
108010
  const installOpts = { global: installGlobally };
107245
- const agentByName = new Map(agents2.map((item) => [item.name, item]));
107246
- const commandByName = new Map(commands.map((item) => [item.name, item]));
107247
- const skillByName = new Map(skills.map((item) => [item.name, item]));
107248
- const configByName = new Map(configItem ? [[configItem.name, configItem]] : []);
107249
- const ruleByName = new Map(ruleItems.map((item) => [item.name, item]));
107250
- const hookByName = new Map(hookItems.map((item) => [item.name, item]));
108011
+ const agentByName = new Map(effectiveAgents.map((item) => [item.name, item]));
108012
+ const commandByName = new Map(effectiveCommands.map((item) => [item.name, item]));
108013
+ const skillByName = new Map(effectiveSkills.map((item) => [item.name, item]));
108014
+ const configByName = new Map(effectiveConfigItem ? [[effectiveConfigItem.name, effectiveConfigItem]] : []);
108015
+ const ruleByName = new Map(effectiveRuleItems.map((item) => [item.name, item]));
108016
+ const hookByName = new Map(effectiveHookItems.map((item) => [item.name, item]));
107251
108017
  const successfulHookFiles = new Map;
107252
108018
  const successfulHookAbsPaths = new Map;
107253
108019
  const postProgressWarnings = [];
@@ -107299,10 +108065,10 @@ async function migrateCommand(options2) {
107299
108065
  }
107300
108066
  }
107301
108067
  const plannedSkillActions = plannedExecActions.filter((action) => action.type === "skill").length;
107302
- if (skills.length > 0 && plannedSkillActions === 0) {
108068
+ if (effectiveSkills.length > 0 && plannedSkillActions === 0) {
107303
108069
  const skillProviders = selectedProviders.filter((pv) => getProvidersSupporting("skills").includes(pv));
107304
108070
  for (const provider of skillProviders) {
107305
- for (const skill of skills) {
108071
+ for (const skill of effectiveSkills) {
107306
108072
  writeTasks.push({ item: skill, provider, type: "skill" });
107307
108073
  }
107308
108074
  }
@@ -108010,7 +108776,7 @@ Please use only one download method.`);
108010
108776
  }
108011
108777
  // src/commands/plan/plan-command.ts
108012
108778
  init_output_manager();
108013
- import { existsSync as existsSync67, statSync as statSync11 } from "node:fs";
108779
+ import { existsSync as existsSync67, statSync as statSync12 } from "node:fs";
108014
108780
  import { dirname as dirname46, isAbsolute as isAbsolute11, join as join147, parse as parse7, resolve as resolve45 } from "node:path";
108015
108781
 
108016
108782
  // src/commands/plan/plan-read-handlers.ts
@@ -108020,7 +108786,7 @@ init_plans_registry();
108020
108786
  init_logger();
108021
108787
  init_output_manager();
108022
108788
  var import_picocolors32 = __toESM(require_picocolors(), 1);
108023
- import { existsSync as existsSync66, statSync as statSync10 } from "node:fs";
108789
+ import { existsSync as existsSync66, statSync as statSync11 } from "node:fs";
108024
108790
  import { basename as basename27, dirname as dirname44, join as join146, relative as relative27, resolve as resolve43 } from "node:path";
108025
108791
 
108026
108792
  // src/commands/plan/plan-dependencies.ts
@@ -108190,7 +108956,7 @@ async function handleStatus(target, options2) {
108190
108956
  }
108191
108957
  const effectiveTarget = !resolvedTarget && globalBaseDir ? globalBaseDir : resolvedTarget;
108192
108958
  const t = effectiveTarget ? resolve43(effectiveTarget) : null;
108193
- const plansDir = t && existsSync66(t) && statSync10(t).isDirectory() && !existsSync66(join146(t, "plan.md")) ? t : null;
108959
+ const plansDir = t && existsSync66(t) && statSync11(t).isDirectory() && !existsSync66(join146(t, "plan.md")) ? t : null;
108194
108960
  if (plansDir) {
108195
108961
  const planFiles = scanPlanDir(plansDir);
108196
108962
  if (planFiles.length === 0) {
@@ -108602,7 +109368,7 @@ function resolveTargetPath(target, baseDir) {
108602
109368
  function resolvePlanFile(target, baseDir) {
108603
109369
  const t = target ? resolveTargetPath(target, baseDir) : baseDir ? resolve45(baseDir) : process.cwd();
108604
109370
  if (existsSync67(t)) {
108605
- const stat23 = statSync11(t);
109371
+ const stat23 = statSync12(t);
108606
109372
  if (stat23.isFile())
108607
109373
  return t;
108608
109374
  const candidate = join147(t, "plan.md");
@@ -109815,7 +110581,7 @@ async function detectInstallations() {
109815
110581
  }
109816
110582
 
109817
110583
  // src/commands/uninstall/removal-handler.ts
109818
- import { readdirSync as readdirSync9, rmSync as rmSync5 } from "node:fs";
110584
+ import { readdirSync as readdirSync10, rmSync as rmSync5 } from "node:fs";
109819
110585
  import { basename as basename29, join as join150, resolve as resolve47, sep as sep12 } from "node:path";
109820
110586
  init_logger();
109821
110587
  init_safe_prompts();
@@ -109824,7 +110590,7 @@ var import_fs_extra44 = __toESM(require_lib3(), 1);
109824
110590
 
109825
110591
  // src/commands/uninstall/analysis-handler.ts
109826
110592
  init_metadata_migration();
109827
- import { readdirSync as readdirSync8, rmSync as rmSync4 } from "node:fs";
110593
+ import { readdirSync as readdirSync9, rmSync as rmSync4 } from "node:fs";
109828
110594
  import { dirname as dirname47, join as join149 } from "node:path";
109829
110595
  init_logger();
109830
110596
  init_safe_prompts();
@@ -109850,7 +110616,7 @@ async function cleanupEmptyDirectories3(filePath, installationRoot) {
109850
110616
  let currentDir = dirname47(filePath);
109851
110617
  while (currentDir !== installationRoot && currentDir.startsWith(installationRoot)) {
109852
110618
  try {
109853
- const entries = readdirSync8(currentDir);
110619
+ const entries = readdirSync9(currentDir);
109854
110620
  if (entries.length === 0) {
109855
110621
  rmSync4(currentDir, { recursive: true });
109856
110622
  cleaned++;
@@ -110091,7 +110857,7 @@ async function removeInstallations(installations, options2) {
110091
110857
  }
110092
110858
  }
110093
110859
  try {
110094
- const remaining = readdirSync9(installation.path);
110860
+ const remaining = readdirSync10(installation.path);
110095
110861
  if (remaining.length === 0) {
110096
110862
  rmSync5(installation.path, { recursive: true });
110097
110863
  logger.debug(`Removed empty installation directory: ${installation.path}`);
@@ -111944,7 +112710,7 @@ Run this command from a directory with a GitHub remote.`);
111944
112710
  // src/commands/watch/phases/watch-logger.ts
111945
112711
  init_logger();
111946
112712
  init_path_resolver();
111947
- import { createWriteStream as createWriteStream3, statSync as statSync12 } from "node:fs";
112713
+ import { createWriteStream as createWriteStream3, statSync as statSync13 } from "node:fs";
111948
112714
  import { existsSync as existsSync73 } from "node:fs";
111949
112715
  import { mkdir as mkdir38, rename as rename13 } from "node:fs/promises";
111950
112716
  import { join as join156 } from "node:path";
@@ -112012,7 +112778,7 @@ class WatchLogger {
112012
112778
  return;
112013
112779
  if (this.maxBytes > 0 && this.logPath) {
112014
112780
  try {
112015
- const stats = statSync12(this.logPath);
112781
+ const stats = statSync13(this.logPath);
112016
112782
  if (stats.size >= this.maxBytes) {
112017
112783
  this.rotateLog();
112018
112784
  }
@@ -112390,7 +113156,7 @@ function registerCommands(cli) {
112390
113156
  cli.command("api [action] [service] [path]", "Interact with ClaudeKit API and proxy services").option("--method <method>", "HTTP method for proxy requests (default: GET)").option("--body <json>", "Request body as JSON string (proxy only)").option("--query <json>", "Query params as JSON string (proxy only)").option("--key <key>", "API key to use (setup only)").option("--force", "Force re-setup even if key exists (setup only)").option("--json", "Output raw JSON instead of formatted display").option("--locale <locale>", "Locale for vidcap summary/caption (default: en)").option("--max-results <n>", "Max results for vidcap search").option("--second <s>", "Timestamp in seconds for vidcap screenshot").option("--order <order>", "Sort order for vidcap comments (time/relevance)").option("--format <fmt>", "Summary format for reviewweb (bullet/paragraph)").option("--max-length <n>", "Max summary length for reviewweb").option("--instructions <text>", "Extraction instructions for reviewweb extract").option("--template <json>", "JSON template for reviewweb extract").option("--type <type>", "Link type filter for reviewweb links (web/image/file/all)").option("--country <code>", "Country code for reviewweb SEO commands").action(async (action, service, path16, options2) => {
112391
113157
  await apiCommand(action, service, path16, options2);
112392
113158
  });
112393
- cli.command("migrate", "Migrate agents, commands, skills, config, rules, and hooks to other providers").option("-a, --agent <agents...>", "Target providers (cursor, codex, droid, opencode, etc.)").option("-g, --global", "Install globally instead of project-level").option("--all", "Migrate to all supported providers").option("-y, --yes", "Skip confirmation prompts").option("--config", "Migrate CLAUDE.md config only").option("--rules", "Migrate .claude/rules/ only").option("--hooks", "Migrate .claude/hooks/ only").option("--skip-config", "Skip config migration").option("--skip-rules", "Skip rules migration").option("--skip-hooks", "Skip hooks migration").option("--source <path>", "Custom CLAUDE.md source path (config only, not agents/commands/skills/hooks)").option("--dry-run", "Preview migration targets without writing files").option("-f, --force", "Force reinstall deleted/edited items").action(async (options2) => {
113159
+ cli.command("migrate", "Migrate agents, commands, skills, config, rules, and hooks to other providers").option("-a, --agent <agents...>", "Target providers (cursor, codex, droid, opencode, etc.)").option("-g, --global", "Install globally instead of project-level").option("--all", "Migrate to all supported providers").option("-y, --yes", "Skip confirmation prompts").option("--config", "Migrate CLAUDE.md config only").option("--rules", "Migrate .claude/rules/ only").option("--hooks", "Migrate .claude/hooks/ only").option("--skip-config", "Skip config migration").option("--skip-rules", "Skip rules migration").option("--skip-hooks", "Skip hooks migration").option("--source <path>", "Custom CLAUDE.md source path (config only, not agents/commands/skills/hooks)").option("--dry-run", "Preview migration targets without writing files").option("-f, --force", "Force reinstall deleted/edited items").option("--install", "Opt-in install picker mode (select specific items to install)").option("--reconcile", "Force reconcile mode (current default when registry is valid)").option("--reinstall-empty-dirs", "Reinstall all items when their type directory is empty (default: true)").option("--respect-deletions", "Preserve deletion even when type directory is empty (disables reinstall-empty-dirs)").action(async (options2) => {
112394
113160
  if (options2.agent && !Array.isArray(options2.agent)) {
112395
113161
  options2.agent = [options2.agent];
112396
113162
  }