@uzysjung/agent-harness 26.173.0 → 26.174.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -96,12 +96,13 @@ import {
96
96
  resolveScope,
97
97
  scaffoldBanner,
98
98
  sharedPathOfKeyId,
99
+ shouldInstallAsset,
99
100
  stripHarnessImport,
100
101
  summarizeContextCost,
101
102
  targetsInclude,
102
103
  upsertHarnessImport,
103
104
  writeInstallLog
104
- } from "./chunk-JABFWW6X.js";
105
+ } from "./chunk-LAWKDFKZ.js";
105
106
 
106
107
  // node_modules/sisteransi/src/index.js
107
108
  var require_src = __commonJS({
@@ -5056,7 +5057,7 @@ function allowSkillCreation(writer, projectDir, ids) {
5056
5057
  }
5057
5058
  function agentsSkillSlot(params) {
5058
5059
  const { projectDir, selectedInternalSkills, refreshOnly, writer } = params;
5059
- const creatable = refreshOnly ? bundledSkillTargets(projectDir) : [];
5060
+ const creatable = refreshOnly ? bundledSkillTargets(projectDir, params.log) : [];
5060
5061
  return {
5061
5062
  skillIds: creatable.length === 0 ? selectedInternalSkills : [.../* @__PURE__ */ new Set([...selectedInternalSkills, ...creatable])],
5062
5063
  skillWriter: allowSkillCreation(writer, projectDir, creatable)
@@ -5486,7 +5487,8 @@ function runAgentsMdTransform(params) {
5486
5487
  projectDir,
5487
5488
  selectedInternalSkills: params.selectedInternalSkills,
5488
5489
  refreshOnly,
5489
- writer
5490
+ writer,
5491
+ ...params.log === void 0 ? {} : { log: params.log }
5490
5492
  });
5491
5493
  const creates = !exists2 && (!refreshOnly || params.installedByRecord === true);
5492
5494
  const legacy = params.legacyContext ?? null;
@@ -8119,7 +8121,7 @@ function excludedSkillOf(log) {
8119
8121
  const excluded = excludedIds(log);
8120
8122
  return (id) => excluded.has(id) || isBaselineExcluded(`.claude/skills/${id}`, excluded);
8121
8123
  }
8122
- function recordedRenderInputs(projectDir, log) {
8124
+ function recordedRenderInputs(projectDir, log, tracks = installedTracks(projectDir)) {
8123
8125
  const excluded = excludedIds(log);
8124
8126
  return {
8125
8127
  // 스킬은 **기록 + 이번 릴리즈 기본**만 넘긴다(#761 — `recordedBundledSkills`). 스킬 *파일*은 전체 목록을 넘겨도
@@ -8132,7 +8134,7 @@ function recordedRenderInputs(projectDir, log) {
8132
8134
  ),
8133
8135
  // #601 — install 과 같은 SSOT(resolveRules)를 설치된 트랙으로. 렌더 경로(AGENTS.md 절 · antigravity 룰 생성)는
8134
8136
  // 디스크 게이트가 없어, data 트랙 설치에서 update 만으로 cli-development(제6룰)가 생겼다.
8135
- rules: resolveRules({ tracks: installedTracks(projectDir) }).filter(
8137
+ rules: resolveRules({ tracks }).filter(
8136
8138
  (r) => !isBaselineExcluded(`.claude/rules/${r}.md`, excluded)
8137
8139
  )
8138
8140
  };
@@ -11065,6 +11067,55 @@ import {
11065
11067
  } from "fs";
11066
11068
  import { dirname as dirname8, join as join35, relative as relative8, resolve as resolve4, sep as sep8 } from "path";
11067
11069
 
11070
+ // src/track-removal.ts
11071
+ init_esm_shims();
11072
+ function trackRemoval(log, target) {
11073
+ const known = log.spec.tracks.filter(isTrack);
11074
+ const remaining = known.filter((t) => t !== target);
11075
+ const stays = claudeTargets(remaining);
11076
+ const only = [...claudeTargets(known)].filter((p2) => !stays.has(p2));
11077
+ const has = (path) => only.some((t) => claudeSpot(path) === t || claudeSpot(path).startsWith(`${t}/`));
11078
+ return { target, known, remaining, has };
11079
+ }
11080
+ function claudeSpot(path) {
11081
+ return bundlePath(path).replace(/^\.agents\/rules\//, ".claude/rules/");
11082
+ }
11083
+ function trackRemovalNames(removed, target) {
11084
+ const groups = /* @__PURE__ */ new Map();
11085
+ for (const p2 of removed) {
11086
+ const m = /^\.claude\/([^/]+)\/([^/]+?)(?:\.md)?(?:\/|$)/.exec(claudeSpot(p2));
11087
+ const [kind, name] = m ? [m[1] ?? "", m[2] ?? ""] : ["files", p2];
11088
+ groups.set(kind, (groups.get(kind) ?? /* @__PURE__ */ new Set()).add(name));
11089
+ }
11090
+ const names = [...groups].sort(([a], [b2]) => a.localeCompare(b2)).map(([kind, names2]) => `${kind} ${[...names2].sort().join(", ")}`);
11091
+ return `removes: ${names.join(" \xB7 ") || `nothing only ${target} installs`}`;
11092
+ }
11093
+ function trackLeftLines(log, r, harnessRoot) {
11094
+ const recorded2 = new Set(log.assets.map((a) => a.id));
11095
+ const def = (a, tracks) => shouldInstallAsset(a, { tracks, options: DEFAULT_OPTIONS });
11096
+ const mine = (a) => def(a, r.known) && !def(a, r.remaining);
11097
+ const lines2 = EXTERNAL_ASSETS.filter(
11098
+ (a) => a.method.kind !== "internal" && recorded2.has(a.id) && mine(a)
11099
+ ).map(
11100
+ (a) => `asset ${a.id} (a default of ${r.target}) \u2014 remove with: agent-harness uninstall --only ${a.id}`
11101
+ );
11102
+ const keys = (log.portions ?? []).filter((p2) => p2.path === ".mcp.json").map((p2) => p2.key);
11103
+ const staysMcp = new Set(trackDefaultMcpAssetIds(harnessRoot, r.remaining));
11104
+ const mcp = trackDefaultMcpAssetIds(harnessRoot, r.known).filter(
11105
+ (id) => !staysMcp.has(id) && keys.includes(`mcpServers.${id}`)
11106
+ );
11107
+ if (mcp.length === 0) return lines2;
11108
+ const flags = [
11109
+ r.remaining.map((t) => `--track ${t}`),
11110
+ installedClis(log).map((c3) => `--cli ${c3}`)
11111
+ ];
11112
+ const command = `agent-harness install ${[...flags.flat(), ...mcp.map((id) => `--without ${id}`)].join(" ")}`;
11113
+ return [
11114
+ ...lines2,
11115
+ `MCP servers ${mcp.join(" \xB7 ")} (defaults of ${r.target}, kept because your install record has them) \u2014 drop: ${command} (this re-runs the whole install: harness files refreshed, edited ones backed up; --without stays on record, so add --with <id> to get one back later)`
11116
+ ];
11117
+ }
11118
+
11068
11119
  // src/uninstall-interactive.ts
11069
11120
  init_esm_shims();
11070
11121
 
@@ -12109,7 +12160,7 @@ function buildRouterChoices(_state, record) {
12109
12160
  {
12110
12161
  value: "uninstall",
12111
12162
  label: "Uninstall",
12112
- hint: "remove everything, one CLI, or single assets",
12163
+ hint: "remove everything, one CLI, one track, or single assets",
12113
12164
  enabled: true
12114
12165
  },
12115
12166
  {
@@ -12472,7 +12523,7 @@ function hintFor(asset) {
12472
12523
  return 'no automatic reverse path \u2014 only "Remove everything" clears it';
12473
12524
  }
12474
12525
  }
12475
- function buildUninstallModeChoices(clis, assetCount) {
12526
+ function buildUninstallModeChoices(clis, assetCount, tracks) {
12476
12527
  return [
12477
12528
  {
12478
12529
  value: "cli",
@@ -12480,6 +12531,7 @@ function buildUninstallModeChoices(clis, assetCount) {
12480
12531
  hint: clis.length > 1 ? "that CLI's own files go; files shared with a remaining CLI stay; your text stays" : 'only one CLI here \u2014 use "Remove everything"',
12481
12532
  enabled: clis.length > 1
12482
12533
  },
12534
+ ...tracks === void 0 ? [] : [trackMode(tracks.length > 1)],
12483
12535
  {
12484
12536
  value: "selected",
12485
12537
  label: "Remove selected assets",
@@ -12494,6 +12546,12 @@ function buildUninstallModeChoices(clis, assetCount) {
12494
12546
  }
12495
12547
  ];
12496
12548
  }
12549
+ var trackMode = (many) => ({
12550
+ value: "track",
12551
+ label: "Remove one track",
12552
+ hint: many ? "only that track's own files go" : 'only one track here \u2014 use "Remove everything"',
12553
+ enabled: many
12554
+ });
12497
12555
  var CLI_FOLDERS = [".claude/", ".codex/", ".opencode/"];
12498
12556
  var FOLDER_CLI = {
12499
12557
  ".claude/": "claude",
@@ -12574,7 +12632,7 @@ async function runInteractiveUninstall(projectDir, deps = {}) {
12574
12632
  const rows2 = buildRemovableRows(log.assets);
12575
12633
  const clis = installedClis(log);
12576
12634
  const mode = await prompts.selectMode(
12577
- buildUninstallModeChoices(clis, rows2.length),
12635
+ buildUninstallModeChoices(clis, rows2.length, log.spec.tracks.filter(isTrack)),
12578
12636
  `uzys-agent-harness \xB7 uninstall installed: ${clis.join(", ") || "(none)"} \xB7 assets ${rows2.length}`
12579
12637
  );
12580
12638
  if (mode === null) {
@@ -12582,6 +12640,7 @@ async function runInteractiveUninstall(projectDir, deps = {}) {
12582
12640
  return { ok: false, reason: "cancelled" };
12583
12641
  }
12584
12642
  if (mode === "cli") return pickCli(projectDir, clis, prompts);
12643
+ if (mode === "track") return pickTrack(projectDir, log, prompts);
12585
12644
  if (mode === "all") {
12586
12645
  const ok2 = await prompts.confirm(
12587
12646
  [
@@ -12644,6 +12703,26 @@ async function pickCli(projectDir, clis, prompts) {
12644
12703
  }
12645
12704
  return { ok: true, options: { projectDir, cli } };
12646
12705
  }
12706
+ async function pickTrack(projectDir, log, prompts) {
12707
+ const rows2 = log.spec.tracks.filter(isTrack).map((t) => ({ value: t, label: TRACK_LABELS[t], hint: t }));
12708
+ const track = await prompts.selectTrack(rows2);
12709
+ const left = rows2.map((r) => r.value).filter((t) => t !== track);
12710
+ const ok = track !== null && await prompts.confirm(
12711
+ [
12712
+ `Remove ${track} (remaining: ${left.join(", ")})? Same as: agent-harness uninstall --track ${track}`,
12713
+ ...previewTrackRemoval(
12714
+ { installLog: log, projectDir, harnessRoot: resolveBundleRoot() },
12715
+ track
12716
+ ).map((l) => ` \xB7 ${l}`),
12717
+ ` \xB7 file counts: agent-harness uninstall --track ${track} --dry-run`
12718
+ ].join("\n")
12719
+ );
12720
+ if (!ok) {
12721
+ prompts.cancel("Cancelled.");
12722
+ return { ok: false, reason: "cancelled" };
12723
+ }
12724
+ return { ok: true, options: { projectDir, track } };
12725
+ }
12647
12726
  function defaultUninstallPrompts() {
12648
12727
  return {
12649
12728
  intro: (m) => ge(m),
@@ -12668,6 +12747,13 @@ function defaultUninstallPrompts() {
12668
12747
  });
12669
12748
  return q(r) ? null : r;
12670
12749
  },
12750
+ selectTrack: async (rows2) => {
12751
+ const r = await Ee({
12752
+ message: 'Which track? (the last remaining track cannot be removed this way \u2014 use "Remove everything")',
12753
+ options: rows2.map((x) => ({ value: x.value, label: x.label, hint: x.hint }))
12754
+ });
12755
+ return q(r) ? null : r;
12756
+ },
12671
12757
  selectAssets: async (rows2) => {
12672
12758
  if (rows2.length === 0) return [];
12673
12759
  const r = await ve({
@@ -12740,6 +12826,13 @@ function uninstallAction(options, deps = {}) {
12740
12826
  exit(1);
12741
12827
  return;
12742
12828
  }
12829
+ if (options.track !== void 0) {
12830
+ removeTrackAction(
12831
+ { options, installLog, projectDir, harnessRoot },
12832
+ { log, err, exit, rm, writeLog }
12833
+ );
12834
+ return;
12835
+ }
12743
12836
  if (options.cli !== void 0) {
12744
12837
  removeCliAction(
12745
12838
  { options, installLog, projectDir, harnessRoot },
@@ -13141,37 +13234,37 @@ var TEMPLATE_DIR_FIELD = {
13141
13234
  codex: "codexDir",
13142
13235
  opencode: "opencodeDir"
13143
13236
  };
13144
- function resolveCliTarget(ctx, io) {
13145
- const { options, installLog } = ctx;
13146
- if (options.only !== void 0) {
13147
- io.err(status.failure(c.red("ERROR: --cli \uB294 --only \uC640 \uD568\uAED8 \uC4F8 \uC218 \uC5C6\uB2E4")));
13148
- io.err(
13149
- c.dim(
13150
- " CLI \uD558\uB098 \uC81C\uAC70: uninstall --cli <name> / \uC790\uC0B0 \uD558\uB098 \uC81C\uAC70: uninstall --only <id>"
13151
- )
13152
- );
13237
+ function resolveScopeTarget(io, s) {
13238
+ const reject = (msg, hint) => {
13239
+ io.err(status.failure(c.red(`ERROR: ${msg}`)));
13240
+ io.err(c.dim(` ${hint}`));
13153
13241
  io.exit(1);
13154
13242
  return null;
13243
+ };
13244
+ if (s.conflict) {
13245
+ return reject(
13246
+ `--${s.flag} \uB294 --only \xB7 --cli \xB7 --track \uC911 \uD558\uB098\uB9CC \uC4F8 \uC218 \uC788\uB2E4`,
13247
+ "CLI \uD558\uB098: uninstall --cli <name> / \uD2B8\uB799 \uD558\uB098: uninstall --track <name> / \uC790\uC0B0: uninstall --only <id>"
13248
+ );
13155
13249
  }
13156
- const target = (options.cli ?? "").trim();
13157
- if (!isCliBase(target)) {
13158
- io.err(status.failure(c.red(`ERROR: Invalid --cli value: ${target || "(empty)"}`)));
13159
- io.err(c.dim(` Must be one of: ${CLI_BASES.join(" | ")}`));
13160
- io.exit(1);
13161
- return null;
13250
+ const target = String(s.value ?? "").trim();
13251
+ if (!s.vocab.includes(target)) {
13252
+ return reject(
13253
+ `Invalid --${s.flag} value: ${target || "(empty)"}`,
13254
+ `Must be one of: ${s.vocab.join(" | ")}`
13255
+ );
13162
13256
  }
13163
- const installed = installedClis(installLog);
13164
- if (!installed.includes(target)) {
13165
- io.err(status.failure(c.red(`ERROR: ${target} is not installed in this project`)));
13166
- io.err(c.dim(` installed: ${installed.join(", ") || "(none)"}`));
13167
- io.exit(1);
13168
- return null;
13257
+ if (!s.installed.includes(target)) {
13258
+ return reject(
13259
+ `${target} is not installed in this project`,
13260
+ `installed: ${s.installed.join(", ") || "(none)"}`
13261
+ );
13169
13262
  }
13170
- if (installed.length <= 1) {
13171
- io.err(status.failure(c.red(`ERROR: ${target} \uB294 \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uB9C8\uC9C0\uB9C9 CLI \uB2E4`)));
13172
- io.err(c.dim(" \uC804\uB7C9 \uC81C\uAC70\uB294 `agent-harness uninstall` \uC744 \uC4F4\uB2E4 (\uC790\uC0B0\xB7\uC124\uCE58 \uAE30\uB85D\uAE4C\uC9C0 \uD568\uAED8)"));
13173
- io.exit(1);
13174
- return null;
13263
+ if (s.installed.length <= 1) {
13264
+ return reject(
13265
+ `${target} \uB294 \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uB9C8\uC9C0\uB9C9 ${s.flag === "cli" ? "CLI \uB2E4" : "\uD2B8\uB799\uC774\uB2E4"}`,
13266
+ "\uC804\uB7C9 \uC81C\uAC70\uB294 `agent-harness uninstall` \uC744 \uC4F4\uB2E4 (\uC790\uC0B0\xB7\uC124\uCE58 \uAE30\uB85D\uAE4C\uC9C0 \uD568\uAED8)"
13267
+ );
13175
13268
  }
13176
13269
  return target;
13177
13270
  }
@@ -13206,41 +13299,11 @@ function keptForRemainingLines(plan, preview) {
13206
13299
  const what = `${plan.kept.join(" \xB7 ")} \u2014 still used by Claude Code through its .claude/skills/ link(s)`;
13207
13300
  return [preview ? ` \u25CB leave ${what}` : ` ${c.yellow("\u2298")} left ${what}`];
13208
13301
  }
13209
- function removeCliHeader(target, installed, remaining) {
13210
- return [
13211
- "",
13212
- c.bold(`uzys-agent-harness \xB7 uninstall --cli ${target}`),
13213
- "",
13214
- c.dim(` installed: ${installed.join(", ")}`),
13215
- c.dim(` removing: ${target}`),
13216
- c.dim(` remaining: ${remaining.join(", ")}`),
13217
- ""
13218
- ];
13219
- }
13220
- function removeCliDryRunLines(plan, ctx, remaining) {
13221
- const { installLog, projectDir, harnessRoot } = ctx;
13222
- const lines2 = [c.yellow("[DRY RUN] CLI \uC81C\uAC70 \uBBF8\uB9AC\uBCF4\uAE30 (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
13223
- const preview = removeHarnessFiles(installLog, projectDir, plan.scope, {
13224
- rm: () => {
13225
- },
13226
- write: false,
13227
- harnessRoot
13228
- });
13229
- lines2.push(...harnessRemovalLines(preview, installLog, projectDir, plan.scope, true));
13230
- lines2.push(...keptForRemainingLines(plan, true));
13231
- lines2.push(...assetsLeftLines(installLog, projectDir, plan.scope));
13232
- const rerender = agentsMdRerenderSet(plan.target, remaining, installLog, projectDir);
13233
- if (rerender !== null) {
13234
- lines2.push(` \u25CB would re-render AGENTS.md for ${displayNames(rerender)} (your sections kept)`);
13235
- }
13236
- lines2.push(` \u25CB install log updated (clis: ${remaining.join(", ")})`, "");
13237
- return lines2;
13238
- }
13239
13302
  function displayNames(clis) {
13240
13303
  return clis.map((c3) => CLI_DISPLAY_NAMES[c3]).join(" \xB7 ");
13241
13304
  }
13242
13305
  function agentsMdRerenderSet(target, remaining, log, projectDir) {
13243
- if (!isAgentsMdCli(target)) return null;
13306
+ if (target !== null && !isAgentsMdCli(target)) return null;
13244
13307
  const set = agentsMdClis(remaining);
13245
13308
  if (set.length === 0) return null;
13246
13309
  const recorded2 = (log.externalFiles ?? []).some((f) => f.path === AGENTS_MD2) || (log.portions ?? []).some((p2) => p2.path === AGENTS_MD2);
@@ -13257,17 +13320,18 @@ function readTextOrEmpty(abs) {
13257
13320
  return "";
13258
13321
  }
13259
13322
  }
13260
- function rerenderAgentsMd(ctx, set) {
13261
- const { installLog, projectDir, harnessRoot } = ctx;
13323
+ function rerenderAgentsMd(ctx, set, log) {
13324
+ const { projectDir, harnessRoot } = ctx;
13262
13325
  return runAgentsMdTransform({
13263
13326
  harnessRoot,
13264
13327
  projectDir,
13265
13328
  clis: agentsMdClis(set),
13266
- ...recordedRenderInputs(projectDir, installLog),
13267
- baseline: new Map((installLog.externalFiles ?? []).map((f) => [f.path, f.sha256])),
13329
+ ...recordedRenderInputs(projectDir, log, log.spec.tracks.filter(isTrack)),
13330
+ baseline: new Map((log.externalFiles ?? []).map((f) => [f.path, f.sha256])),
13268
13331
  refreshOnly: true,
13269
13332
  installedByRecord: true,
13270
- shared: { portions: installLog.portions ?? [], excluded: [...excludedIds(installLog)] }
13333
+ shared: { portions: log.portions ?? [], excluded: [...excludedIds(log)] },
13334
+ log
13271
13335
  });
13272
13336
  }
13273
13337
  function withRerender(next, r) {
@@ -13316,66 +13380,197 @@ function settleCliLog(ctx, target, remaining, done, anchorRemoved) {
13316
13380
  delete next.policyFiles;
13317
13381
  delete next.skillFiles;
13318
13382
  }
13319
- const survivors = (installLog.externalFiles ?? []).filter((f) => !done.forgotten.has(f.path));
13320
- if (survivors.length > 0) next.externalFiles = survivors;
13321
- else delete next.externalFiles;
13322
- if (installLog.rootFiles !== void 0) {
13323
- next.rootFiles = withoutRestored(installLog.rootFiles, done.displaced);
13324
- }
13383
+ return forgetRemoved(next, done);
13384
+ }
13385
+ function forgetRemoved(next, done) {
13386
+ const out = { ...next };
13387
+ const kept = (prefix) => (f) => !done.forgotten.has(`${prefix}${f.path}`);
13388
+ if (out.policyFiles !== void 0) out.policyFiles = out.policyFiles.filter(kept(CLAUDE_DIR3));
13389
+ if (out.skillFiles !== void 0) out.skillFiles = out.skillFiles.filter(kept(SKILLS_DIR3));
13390
+ const survivors = (out.externalFiles ?? []).filter(kept(""));
13391
+ if (survivors.length > 0) out.externalFiles = survivors;
13392
+ else delete out.externalFiles;
13393
+ if (out.rootFiles !== void 0) out.rootFiles = withoutRestored(out.rootFiles, done.displaced);
13325
13394
  const touched = new Set(done.shared.map((r) => r.path));
13326
13395
  const portions = [
13327
- ...(installLog.portions ?? []).filter((p2) => !touched.has(p2.path)),
13396
+ ...(out.portions ?? []).filter((p2) => !touched.has(p2.path)),
13328
13397
  ...done.shared.flatMap((r) => r.portions)
13329
13398
  ];
13330
- if (portions.length > 0) next.portions = portions;
13331
- else delete next.portions;
13332
- return next;
13399
+ if (portions.length > 0) out.portions = portions;
13400
+ else delete out.portions;
13401
+ return out;
13333
13402
  }
13334
- function removeCliAction(ctx, io) {
13335
- const target = resolveCliTarget(ctx, io);
13336
- if (target === null) return;
13403
+ function runScopedRemoval(ctx, io, plan) {
13337
13404
  const { options, installLog, projectDir, harnessRoot } = ctx;
13338
- const installed = installedClis(installLog);
13339
- const remaining = installed.filter((cli) => cli !== target);
13340
- const plan = planCliRemoval(target, remaining, installLog, projectDir);
13341
- for (const line of removeCliHeader(target, installed, remaining)) io.log(line);
13342
- if (options.dryRun) {
13343
- for (const line of removeCliDryRunLines(plan, ctx, remaining)) io.log(line);
13344
- io.exit(0);
13345
- return;
13346
- }
13405
+ const preview = options.dryRun === true;
13406
+ const remaining = plan.remaining.join(", ");
13407
+ const recordNote = `${plan.flag === "cli" ? "clis" : "tracks"}: ${remaining}`;
13408
+ const header = [
13409
+ "",
13410
+ c.bold(`uzys-agent-harness \xB7 uninstall --${plan.flag} ${plan.target}`),
13411
+ "",
13412
+ c.dim(` installed: ${plan.installed.join(", ")}`),
13413
+ c.dim(` removing: ${plan.target}`),
13414
+ c.dim(` remaining: ${remaining}`),
13415
+ ""
13416
+ ];
13417
+ for (const line of header) io.log(line);
13347
13418
  const done = removeHarnessFiles(installLog, projectDir, plan.scope, {
13348
- rm: io.rm,
13349
- write: true,
13419
+ rm: preview ? () => {
13420
+ } : io.rm,
13421
+ write: !preview,
13350
13422
  harnessRoot
13351
13423
  });
13352
- for (const line of harnessRemovalLines(done, installLog, projectDir, plan.scope, false)) {
13353
- io.log(line);
13354
- }
13355
- for (const line of keptForRemainingLines(plan, false)) io.log(line);
13356
- for (const line of assetsLeftLines(installLog, projectDir, plan.scope)) io.log(line);
13357
- const rerenderSet = agentsMdRerenderSet(target, remaining, installLog, projectDir);
13358
- const rerender = rerenderSet === null ? null : rerenderAgentsMd(ctx, rerenderSet);
13359
- if (rerender !== null) for (const line of rerenderLines(rerender)) io.log(line);
13360
- const settled = settleCliLog(ctx, target, remaining, done, done.anchor.removed);
13424
+ const what = plan.flag === "cli" ? "CLI" : "\uD2B8\uB799";
13425
+ const lines2 = [
13426
+ ...preview ? [c.yellow(`[DRY RUN] ${what} \uC81C\uAC70 \uBBF8\uB9AC\uBCF4\uAE30 (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):`), ""] : [],
13427
+ ...harnessRemovalLines(done, installLog, projectDir, plan.scope, preview),
13428
+ ...plan.leftLines(preview, done)
13429
+ ];
13430
+ if (preview) {
13431
+ if (plan.rerenderSet !== null) {
13432
+ lines2.push(
13433
+ ` \u25CB would re-render AGENTS.md for ${displayNames(plan.rerenderSet)} (your sections kept)`
13434
+ );
13435
+ }
13436
+ for (const line of [...lines2, ` \u25CB install log updated (${recordNote})`, ""]) io.log(line);
13437
+ io.exit(0);
13438
+ return;
13439
+ }
13440
+ for (const line of lines2) io.log(line);
13441
+ const settled = plan.settle(done);
13442
+ const agentsBefore = readTextOrEmpty(join35(projectDir, AGENTS_MD2));
13443
+ const rerender = plan.rerenderSet === null ? null : rerenderAgentsMd(ctx, plan.rerenderSet, settled);
13444
+ const rewrote = rerender !== null && readTextOrEmpty(join35(projectDir, AGENTS_MD2)) !== agentsBefore;
13445
+ if (rewrote) for (const line of rerenderLines(rerender)) io.log(line);
13361
13446
  const next = rerender === null ? settled : withRerender(settled, rerender);
13362
13447
  try {
13363
13448
  io.writeLog(projectDir, next);
13364
- io.log(` ${status.success(`install log updated (clis: ${remaining.join(", ")})`)}`);
13449
+ io.log(` ${status.success(`install log updated (${recordNote})`)}`);
13365
13450
  } catch (e2) {
13366
13451
  io.err(status.failure(c.red(`ERROR: install log \uAC31\uC2E0 \uC2E4\uD328 \u2014 ${installLogPath(projectDir)}`)));
13367
13452
  io.err(c.dim(` ${e2 instanceof Error ? e2.message : String(e2)}`));
13368
13453
  io.log("");
13369
13454
  io.log(
13370
- c.yellow(`${target} \uB294 \uC81C\uAC70\uB410\uC73C\uB098 install log \uB97C \uAC31\uC2E0\uD558\uC9C0 \uBABB\uD588\uB2E4 (\uAE30\uB85D\uC774 \uC2E4\uC81C\uC640 \uB2E4\uB974\uB2E4)`)
13455
+ c.yellow(
13456
+ `${plan.target} \uB294 \uC81C\uAC70\uB410\uC73C\uB098 install log \uB97C \uAC31\uC2E0\uD558\uC9C0 \uBABB\uD588\uB2E4 (\uAE30\uB85D\uC774 \uC2E4\uC81C\uC640 \uB2E4\uB974\uB2E4)`
13457
+ )
13371
13458
  );
13372
13459
  io.exit(1);
13373
13460
  return;
13374
13461
  }
13375
13462
  io.log("");
13376
- io.log(status.success(c.green(`${target} removed (remaining: ${remaining.join(", ")})`)));
13463
+ io.log(status.success(c.green(`${plan.target} removed (remaining: ${remaining})`)));
13377
13464
  io.exit(0);
13378
13465
  }
13466
+ function removeCliAction(ctx, io) {
13467
+ const { options, installLog, projectDir } = ctx;
13468
+ const installed = installedClis(installLog);
13469
+ const target = resolveScopeTarget(io, {
13470
+ flag: "cli",
13471
+ value: options.cli,
13472
+ conflict: options.only !== void 0,
13473
+ vocab: CLI_BASES,
13474
+ installed
13475
+ });
13476
+ if (target === null) return;
13477
+ const remaining = installed.filter((cli) => cli !== target);
13478
+ const plan = planCliRemoval(target, remaining, installLog, projectDir);
13479
+ runScopedRemoval(ctx, io, {
13480
+ flag: "cli",
13481
+ target,
13482
+ installed,
13483
+ remaining,
13484
+ scope: plan.scope,
13485
+ leftLines: (preview) => [
13486
+ ...keptForRemainingLines(plan, preview),
13487
+ ...assetsLeftLines(installLog, projectDir, plan.scope)
13488
+ ],
13489
+ rerenderSet: agentsMdRerenderSet(target, remaining, installLog, projectDir),
13490
+ settle: (done) => settleCliLog(ctx, target, remaining, done, done.anchor.removed)
13491
+ });
13492
+ }
13493
+ function removeTrackAction(ctx, io) {
13494
+ const { options, installLog, projectDir } = ctx;
13495
+ const target = resolveScopeTarget(io, {
13496
+ flag: "track",
13497
+ value: options.track,
13498
+ conflict: options.only !== void 0 || options.cli !== void 0,
13499
+ vocab: TRACKS,
13500
+ installed: installLog.spec.tracks.filter(isTrack)
13501
+ });
13502
+ if (target === null) return;
13503
+ const removal = trackRemoval(installLog, target);
13504
+ runScopedRemoval(ctx, io, {
13505
+ flag: "track",
13506
+ target,
13507
+ installed: removal.known,
13508
+ remaining: removal.remaining,
13509
+ scope: { ...TRACK_SCOPE, has: removal.has },
13510
+ leftLines: (preview, done) => preview ? trackNotes(ctx, removal, done, false).map((l) => ` \u25CB ${l}`) : trackNotes(ctx, removal, done, true),
13511
+ rerenderSet: agentsMdRerenderSet(null, installedClis(installLog), installLog, projectDir),
13512
+ settle: (done) => settleTrackLog(ctx, target, removal.remaining, done)
13513
+ });
13514
+ }
13515
+ var TRACK_SCOPE = { shared: [], skeleton: [], folders: [], importBlock: false, anchor: false };
13516
+ function trackNotes(ctx, r, d2, write) {
13517
+ const md = join35(ctx.projectDir, "CLAUDE.md");
13518
+ const block = rootBlockNext(ctx, new Set(d2.removed));
13519
+ if (write && block !== null) {
13520
+ backupIfLossyUtf8(md);
13521
+ writeFileSync9(md, block);
13522
+ }
13523
+ const left = trackLeftLines(ctx.installLog, r, ctx.harnessRoot);
13524
+ if (write) {
13525
+ const refreshed = block === null ? [] : ["CLAUDE.md \u2014 harness block refreshed (\uBCF8\uBB38 \uBCF4\uC874)"];
13526
+ return [
13527
+ ...refreshed.map((l) => ` ${status.success(l)}`),
13528
+ ...left.map((l) => ` \u2298 left ${l}`)
13529
+ ];
13530
+ }
13531
+ const would = block === null ? [] : ["would refresh the harness block in CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)"];
13532
+ return [trackRemovalNames(d2.removed, r.target), ...would, ...left.map((l) => `leave ${l}`)];
13533
+ }
13534
+ function previewTrackRemoval(ctx, target) {
13535
+ const r = trackRemoval(ctx.installLog, target);
13536
+ const io = { rm: () => {
13537
+ }, write: false, harnessRoot: ctx.harnessRoot };
13538
+ const scope = { ...TRACK_SCOPE, has: r.has };
13539
+ const done = removeHarnessFiles(ctx.installLog, ctx.projectDir, scope, io);
13540
+ const counts = harnessRemovalLines(done, ctx.installLog, ctx.projectDir, scope, true);
13541
+ return [...counts.map((l) => l.replace(/^\s*○ /, "")), ...trackNotes(ctx, r, done, false)];
13542
+ }
13543
+ function rootBlockNext({ installLog, projectDir }, gone) {
13544
+ const anchor = installLog.templates?.rootClaudeMd?.path;
13545
+ const md = join35(projectDir, "CLAUDE.md");
13546
+ const text = safeExists(md) && outsideProjectTarget(projectDir, md) === null ? readTextOrEmpty(md) : "";
13547
+ if (anchor === void 0 || !safeExists(join35(projectDir, anchor))) return null;
13548
+ if (stripHarnessImport(text) === null) return null;
13549
+ const stays = (id) => listFilesRecursive(join35(projectDir, SKILLS_DIR3, id)).some(
13550
+ (f) => !gone.has(`${SKILLS_DIR3}${id}/${f}`)
13551
+ );
13552
+ const continuousSkills = CONTINUOUS_SKILLS.map((s) => s.id).filter(stays);
13553
+ const next = upsertHarnessImport(text, { projectName: "", tracks: [], continuousSkills });
13554
+ return next === text ? null : next;
13555
+ }
13556
+ function settleTrackLog(ctx, target, remaining, done) {
13557
+ const { installLog, projectDir } = ctx;
13558
+ const tracks = installLog.spec.tracks.filter((t) => t !== target);
13559
+ const next = forgetRemoved({ ...installLog, spec: { ...installLog.spec, tracks } }, done);
13560
+ const abs = join35(projectDir, INSTALLED_TRACKS);
13561
+ const key = INSTALLED_TRACKS.slice(CLAUDE_DIR3.length);
13562
+ const prior = installLog.policyFiles?.find((f) => f.path === key)?.sha256;
13563
+ const edited = prior !== void 0 && safeExists(abs) && hashContent(readTextOrEmpty(abs)) !== prior;
13564
+ if (!installedClis(installLog).includes("claude") || edited) return next;
13565
+ if (outsideProjectTarget(projectDir, abs) !== null) return next;
13566
+ const content = installedTracksText(remaining);
13567
+ writeFileSync9(abs, content);
13568
+ if (next.policyFiles !== void 0) {
13569
+ const sha256 = hashContent(content);
13570
+ next.policyFiles = [...next.policyFiles.filter((f) => f.path !== key), { path: key, sha256 }];
13571
+ }
13572
+ return next;
13573
+ }
13379
13574
  var UNINSTALL_SHARED = Object.keys(SHARED_FILES).filter(
13380
13575
  (p2) => p2 !== "CLAUDE.md"
13381
13576
  );
@@ -14367,6 +14562,9 @@ function registerUninstallCommand(cli) {
14367
14562
  ).option(
14368
14563
  "--cli <name>",
14369
14564
  `[Scope] Remove one CLI only (${CLI_BASES.join(" | ")}). Shared files stay until the last user leaves`
14565
+ ).option(
14566
+ "--track <name>",
14567
+ `[Scope] Remove one track only (${TRACKS.join(" | ")}). Files another recorded track also installs stay`
14370
14568
  ).option("--yes", "[Mode] Skip the interactive picker and remove everything (non-interactive)").action(async (options) => {
14371
14569
  await dispatchUninstall(options);
14372
14570
  });
@@ -14375,7 +14573,7 @@ function shouldRunInteractive(options, isTty) {
14375
14573
  if (!isTty) return false;
14376
14574
  if (options.yes || options.dryRun) return false;
14377
14575
  if (options.keepTemplates) return false;
14378
- return options.only === void 0 && options.cli === void 0;
14576
+ return options.only === void 0 && options.cli === void 0 && options.track === void 0;
14379
14577
  }
14380
14578
  async function dispatchUninstall(options) {
14381
14579
  const projectDir = resolve4(options.projectDir ?? process.cwd());
@@ -14388,7 +14586,7 @@ async function dispatchUninstall(options) {
14388
14586
  console.error(
14389
14587
  status.failure(
14390
14588
  c.red(
14391
- "ERROR: no terminal and no flag saying what to remove \u2014 nothing was done. Pass --yes (remove everything), --only <ids>, --cli <name>, or --dry-run."
14589
+ "ERROR: no terminal and no flag saying what to remove \u2014 nothing was done. Pass --yes (remove everything), --only <ids>, --cli <name>, --track <name>, or --dry-run."
14392
14590
  )
14393
14591
  )
14394
14592
  );
@@ -14401,7 +14599,7 @@ async function dispatchUninstall(options) {
14401
14599
  await runUninstallScreen(projectDir);
14402
14600
  }
14403
14601
  function lacksRemovalIntent(options) {
14404
- return options.yes === void 0 && options.dryRun === void 0 && options.only === void 0 && options.cli === void 0;
14602
+ return options.yes === void 0 && options.dryRun === void 0 && options.only === void 0 && options.cli === void 0 && options.track === void 0;
14405
14603
  }
14406
14604
  async function runUninstallScreen(projectDir, opts = {}) {
14407
14605
  const picked = await runInteractiveUninstall(projectDir, opts);