harnessed 3.9.4 → 3.9.6

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/cli.mjs CHANGED
@@ -762,6 +762,43 @@ var init_check_mattpocock_skills = __esm({
762
762
  INSTALL_COMMANDS2 = ["npx skills@latest add mattpocock/skills"];
763
763
  }
764
764
  });
765
+ function getUserClaudeJsonPath() {
766
+ return join(homedir(), ".claude.json");
767
+ }
768
+ async function readUserClaudeJson() {
769
+ const path = getUserClaudeJsonPath();
770
+ let raw;
771
+ try {
772
+ raw = await readFile(path, "utf8");
773
+ } catch (err2) {
774
+ if (err2.code === "ENOENT") return {};
775
+ throw err2;
776
+ }
777
+ try {
778
+ const parsed = JSON.parse(raw);
779
+ if (parsed === null || typeof parsed !== "object") return {};
780
+ return parsed;
781
+ } catch {
782
+ return {};
783
+ }
784
+ }
785
+ async function isMcpServerRegistered(name) {
786
+ const config = await readUserClaudeJson();
787
+ const servers = config.mcpServers;
788
+ if (!servers || typeof servers !== "object") return false;
789
+ return Object.hasOwn(servers, name);
790
+ }
791
+ async function isPluginRegistered(pluginName) {
792
+ const config = await readUserClaudeJson();
793
+ const plugins = config.enabledPlugins;
794
+ if (!plugins || typeof plugins !== "object") return false;
795
+ if (Object.hasOwn(plugins, pluginName)) return true;
796
+ return Object.keys(plugins).some((k) => k.split("@")[0] === pluginName);
797
+ }
798
+ var init_readClaudeConfig = __esm({
799
+ "src/installers/lib/readClaudeConfig.ts"() {
800
+ }
801
+ });
765
802
 
766
803
  // src/cli/lib/check-mcp-availability.ts
767
804
  var check_mcp_availability_exports = {};
@@ -769,17 +806,15 @@ __export(check_mcp_availability_exports, {
769
806
  checkMcpAvailability: () => checkMcpAvailability
770
807
  });
771
808
  async function checkMcpAvailability() {
772
- const settingsPath3 = join(homedir(), ".claude", "settings.json");
773
- let installed = [];
774
- let missing = [...TARGET_SERVERS];
775
- try {
776
- const raw = await readFile(settingsPath3, "utf8");
777
- const parsed = JSON.parse(raw);
778
- const servers = parsed.mcpServers ?? {};
779
- const serverNames = Object.keys(servers);
780
- installed = TARGET_SERVERS.filter((s) => serverNames.includes(s));
781
- missing = TARGET_SERVERS.filter((s) => !installed.includes(s));
782
- } catch {
809
+ const installed = [];
810
+ const missing = [];
811
+ for (const s of TARGET_SERVERS) {
812
+ const present = await isMcpServerRegistered(s);
813
+ if (present) {
814
+ installed.push(s);
815
+ } else {
816
+ missing.push(s);
817
+ }
783
818
  }
784
819
  if (missing.length === 0) {
785
820
  return {
@@ -788,35 +823,26 @@ async function checkMcpAvailability() {
788
823
  message: `all 3 installed: ${installed.join(", ")}`
789
824
  };
790
825
  }
791
- const installCommands = missing.map((s) => SERVER_INSTALL_COMMANDS[s]);
792
826
  if (installed.length === 0) {
793
827
  return {
794
828
  name: "MCP servers (tavily/exa/chrome-devtools)",
795
829
  status: "warn",
796
- message: "none of 3 target MCP servers installed in ~/.claude/settings.json",
797
- fix: "install via per-server transport-specific command (see install_commands); harnessed routes web-search to tavily/exa per workflows/judgments/web-search-routing.yaml \u2014 without them, falls back to WebFetch/WebSearch built-in (degraded but functional)",
798
- install_commands: installCommands
830
+ message: "none of 3 target MCP servers registered in ~/.claude.json",
831
+ fix: "run `harnessed setup` to install via Step B (manifests/tools/{tavily,exa,chrome-devtools}-mcp.yaml)"
799
832
  };
800
833
  }
801
834
  return {
802
835
  name: "MCP servers (tavily/exa/chrome-devtools)",
803
836
  status: "warn",
804
837
  message: `${installed.length}/3 installed: ${installed.join(", ")}; missing: ${missing.join(", ")}`,
805
- fix: `install missing via per-server command (see install_commands): ${missing.join(", ")}`,
806
- install_commands: installCommands
838
+ fix: "run `harnessed setup` to install missing MCPs via Step B"
807
839
  };
808
840
  }
809
- var TARGET_SERVERS, SERVER_INSTALL_COMMANDS;
841
+ var TARGET_SERVERS;
810
842
  var init_check_mcp_availability = __esm({
811
843
  "src/cli/lib/check-mcp-availability.ts"() {
812
- TARGET_SERVERS = ["tavily-remote-mcp", "exa", "chrome-devtools"];
813
- SERVER_INSTALL_COMMANDS = {
814
- "tavily-remote-mcp": "claude mcp add tavily-remote-mcp --transport http https://mcp.tavily.com/mcp/",
815
- exa: "claude mcp add --transport http exa https://mcp.exa.ai/mcp",
816
- // chrome-devtools: official Claude marketplace direct install (v3.9.2 dogfood
817
- // confirmed — was assumed npx in v3.9.1 SPEC, corrected to official marketplace).
818
- "chrome-devtools": "claude plugin install chrome-devtools-mcp"
819
- };
844
+ init_readClaudeConfig();
845
+ TARGET_SERVERS = ["tavily-mcp", "exa-mcp", "chrome-devtools-mcp"];
820
846
  }
821
847
  });
822
848
 
@@ -1174,7 +1200,7 @@ var init_auto_install = __esm({
1174
1200
 
1175
1201
  // package.json
1176
1202
  var package_default = {
1177
- version: "3.9.4"};
1203
+ version: "3.9.6"};
1178
1204
 
1179
1205
  // src/manifest/errors.ts
1180
1206
  function instancePathToKeyPath(instancePath) {
@@ -4331,39 +4357,98 @@ var installCcHookAdd = async (ctx) => {
4331
4357
  await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, "", "");
4332
4358
  return { ok: true, backupId: bk.backupId, appliedFiles: [settingsPath3] };
4333
4359
  };
4334
- function getUserClaudeJsonPath() {
4335
- return join(homedir(), ".claude.json");
4336
- }
4337
- async function readUserClaudeJson() {
4338
- const path = getUserClaudeJsonPath();
4339
- let raw;
4340
- try {
4341
- raw = await readFile(path, "utf8");
4342
- } catch (err2) {
4343
- if (err2.code === "ENOENT") return {};
4344
- throw err2;
4360
+ var DEFAULT_VERIFY_TIMEOUT_MS = 15e3;
4361
+ var DEFAULT_INSTALL_TIMEOUT_MS = 6e4;
4362
+ async function spawnCmd(ctx, cmd, args, timeoutMs) {
4363
+ const violation = checkCmdString(cmd);
4364
+ if (violation) {
4365
+ return {
4366
+ ok: false,
4367
+ phase: "preflight",
4368
+ error: {
4369
+ file: ctx.manifest.metadata.name,
4370
+ path: "/spec/install/cmd",
4371
+ message: `shell escape detected at spawn boundary: '${violation.label}' (${violation.hint}) \u2014 refusing to execute. v0.1 forbids dynamic shell evaluation; this is a defense-in-depth gate after schema validation.`,
4372
+ line: null,
4373
+ column: null,
4374
+ keyword: "security-gate-bypass"
4375
+ }
4376
+ };
4345
4377
  }
4346
- try {
4347
- const parsed = JSON.parse(raw);
4348
- if (parsed === null || typeof parsed !== "object") return {};
4349
- return parsed;
4350
- } catch {
4351
- return {};
4378
+ const installCfg = ctx.manifest.spec.install;
4379
+ const effectiveTimeoutMs = timeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
4380
+ const env = { ...process.env, ...installCfg.env ?? {} };
4381
+ const cwd = installCfg.cwd ?? ctx.cwd;
4382
+ let child;
4383
+ if (process.platform === "win32") {
4384
+ child = spawn("cmd.exe", ["/c", cmd, ...args], { cwd, env, windowsHide: true });
4385
+ } else {
4386
+ const joined = args.length > 0 ? `${cmd} ${args.join(" ")}` : cmd;
4387
+ child = spawn("/bin/sh", ["-c", joined], { cwd, env });
4352
4388
  }
4389
+ let stdout2 = "";
4390
+ let stderr = "";
4391
+ child.stdout?.setEncoding("utf8").on("data", (chunk) => {
4392
+ stdout2 += chunk;
4393
+ });
4394
+ child.stderr?.setEncoding("utf8").on("data", (chunk) => {
4395
+ stderr += chunk;
4396
+ });
4397
+ return await new Promise((resolve15) => {
4398
+ const timer = setTimeout(() => {
4399
+ child.kill("SIGKILL");
4400
+ resolve15({
4401
+ ok: false,
4402
+ phase: "spawn",
4403
+ error: {
4404
+ file: ctx.manifest.metadata.name,
4405
+ path: "/spec/install/cmd",
4406
+ message: `spawn timed out after ${effectiveTimeoutMs}ms (cmd: ${cmd}); partial stderr: ${stderr.slice(0, 200)}`,
4407
+ line: null,
4408
+ column: null,
4409
+ keyword: "spawn-timeout"
4410
+ }
4411
+ });
4412
+ }, effectiveTimeoutMs);
4413
+ child.on("error", (err2) => {
4414
+ clearTimeout(timer);
4415
+ resolve15({
4416
+ ok: false,
4417
+ phase: "spawn",
4418
+ error: {
4419
+ file: ctx.manifest.metadata.name,
4420
+ path: "/spec/install/cmd",
4421
+ message: `spawn failed: ${err2.message}`,
4422
+ line: null,
4423
+ column: null,
4424
+ keyword: "spawn-error"
4425
+ }
4426
+ });
4427
+ });
4428
+ child.on("close", (code) => {
4429
+ clearTimeout(timer);
4430
+ resolve15({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
4431
+ });
4432
+ });
4353
4433
  }
4354
- async function isMcpServerRegistered(name) {
4355
- const config = await readUserClaudeJson();
4356
- const servers = config.mcpServers;
4357
- if (!servers || typeof servers !== "object") return false;
4358
- return Object.hasOwn(servers, name);
4359
- }
4360
- async function isPluginRegistered(pluginName) {
4361
- const config = await readUserClaudeJson();
4362
- const plugins = config.enabledPlugins;
4363
- if (!plugins || typeof plugins !== "object") return false;
4364
- if (Object.hasOwn(plugins, pluginName)) return true;
4365
- return Object.keys(plugins).some((k) => k.split("@")[0] === pluginName);
4434
+
4435
+ // src/installers/lib/idempotent.ts
4436
+ var IDEMPOTENT_CHECK_TIMEOUT_MS = 1e4;
4437
+ async function isAlreadyInstalled(ctx) {
4438
+ if (ctx.opts.updateInstalled === true) return false;
4439
+ const idempotentCmd = ctx.manifest.spec.install.idempotent_check;
4440
+ if (typeof idempotentCmd !== "string" || idempotentCmd.length === 0) {
4441
+ return false;
4442
+ }
4443
+ const r = await spawnCmd(ctx, idempotentCmd, [], IDEMPOTENT_CHECK_TIMEOUT_MS);
4444
+ if (!("exitCode" in r)) {
4445
+ return false;
4446
+ }
4447
+ return r.exitCode === 0;
4366
4448
  }
4449
+
4450
+ // src/installers/ccPluginMarketplace.ts
4451
+ init_readClaudeConfig();
4367
4452
  function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
4368
4453
  return new Promise((resolve15) => {
4369
4454
  const isWin = process.platform === "win32";
@@ -4424,6 +4509,9 @@ var installCcPluginMarketplace = async (ctx) => {
4424
4509
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed (no detail)", "preflight");
4425
4510
  return { ok: false, phase: "preflight", error: e };
4426
4511
  }
4512
+ if (await isAlreadyInstalled(ctx)) {
4513
+ return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
4514
+ }
4427
4515
  const parsed = parseCmd(install.cmd);
4428
4516
  if (!parsed) {
4429
4517
  return {
@@ -4524,82 +4612,6 @@ ${newEntry}
4524
4612
  await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, install.git_ref, "");
4525
4613
  return { ok: true, backupId: bk.backupId, appliedFiles: [settingsFile] };
4526
4614
  };
4527
- var DEFAULT_VERIFY_TIMEOUT_MS = 15e3;
4528
- var DEFAULT_INSTALL_TIMEOUT_MS = 6e4;
4529
- async function spawnCmd(ctx, cmd, args, timeoutMs) {
4530
- const violation = checkCmdString(cmd);
4531
- if (violation) {
4532
- return {
4533
- ok: false,
4534
- phase: "preflight",
4535
- error: {
4536
- file: ctx.manifest.metadata.name,
4537
- path: "/spec/install/cmd",
4538
- message: `shell escape detected at spawn boundary: '${violation.label}' (${violation.hint}) \u2014 refusing to execute. v0.1 forbids dynamic shell evaluation; this is a defense-in-depth gate after schema validation.`,
4539
- line: null,
4540
- column: null,
4541
- keyword: "security-gate-bypass"
4542
- }
4543
- };
4544
- }
4545
- const installCfg = ctx.manifest.spec.install;
4546
- const effectiveTimeoutMs = timeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
4547
- const env = { ...process.env, ...installCfg.env ?? {} };
4548
- const cwd = installCfg.cwd ?? ctx.cwd;
4549
- let child;
4550
- if (process.platform === "win32") {
4551
- child = spawn("cmd.exe", ["/c", cmd, ...args], { cwd, env, windowsHide: true });
4552
- } else {
4553
- const joined = args.length > 0 ? `${cmd} ${args.join(" ")}` : cmd;
4554
- child = spawn("/bin/sh", ["-c", joined], { cwd, env });
4555
- }
4556
- let stdout2 = "";
4557
- let stderr = "";
4558
- child.stdout?.setEncoding("utf8").on("data", (chunk) => {
4559
- stdout2 += chunk;
4560
- });
4561
- child.stderr?.setEncoding("utf8").on("data", (chunk) => {
4562
- stderr += chunk;
4563
- });
4564
- return await new Promise((resolve15) => {
4565
- const timer = setTimeout(() => {
4566
- child.kill("SIGKILL");
4567
- resolve15({
4568
- ok: false,
4569
- phase: "spawn",
4570
- error: {
4571
- file: ctx.manifest.metadata.name,
4572
- path: "/spec/install/cmd",
4573
- message: `spawn timed out after ${effectiveTimeoutMs}ms (cmd: ${cmd}); partial stderr: ${stderr.slice(0, 200)}`,
4574
- line: null,
4575
- column: null,
4576
- keyword: "spawn-timeout"
4577
- }
4578
- });
4579
- }, effectiveTimeoutMs);
4580
- child.on("error", (err2) => {
4581
- clearTimeout(timer);
4582
- resolve15({
4583
- ok: false,
4584
- phase: "spawn",
4585
- error: {
4586
- file: ctx.manifest.metadata.name,
4587
- path: "/spec/install/cmd",
4588
- message: `spawn failed: ${err2.message}`,
4589
- line: null,
4590
- column: null,
4591
- keyword: "spawn-error"
4592
- }
4593
- });
4594
- });
4595
- child.on("close", (code) => {
4596
- clearTimeout(timer);
4597
- resolve15({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
4598
- });
4599
- });
4600
- }
4601
-
4602
- // src/installers/gitCloneWithSetup.ts
4603
4615
  function gitRevParseHead(cwd, timeoutMs = 1e4) {
4604
4616
  return new Promise((resolve15) => {
4605
4617
  const isWin = process.platform === "win32";
@@ -4669,6 +4681,9 @@ var installGitCloneWithSetup = async (ctx) => {
4669
4681
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed (no detail)", "preflight");
4670
4682
  return { ok: false, phase: "preflight", error: e };
4671
4683
  }
4684
+ if (await isAlreadyInstalled(ctx)) {
4685
+ return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
4686
+ }
4672
4687
  if (!/^[a-f0-9]{7,40}$/.test(install.git_ref)) {
4673
4688
  return {
4674
4689
  ok: false,
@@ -4786,6 +4801,7 @@ var installGitCloneWithSetup = async (ctx) => {
4786
4801
  };
4787
4802
 
4788
4803
  // src/installers/mcpHttpAdd.ts
4804
+ init_readClaudeConfig();
4789
4805
  function resolveEnvVars(value) {
4790
4806
  const pattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
4791
4807
  let resolved = value;
@@ -4954,6 +4970,7 @@ ${newEntry}
4954
4970
  };
4955
4971
 
4956
4972
  // src/installers/mcpStdioAdd.ts
4973
+ init_readClaudeConfig();
4957
4974
  var installMcpStdioAdd = async (ctx) => {
4958
4975
  const install = ctx.manifest.spec.install;
4959
4976
  if (install.method !== "mcp-stdio-add") {
@@ -5094,6 +5111,9 @@ var installNpmCli = async (ctx) => {
5094
5111
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed (no detail)", "preflight");
5095
5112
  return { ok: false, phase: "preflight", error: e };
5096
5113
  }
5114
+ if (await isAlreadyInstalled(ctx)) {
5115
+ return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
5116
+ }
5097
5117
  let level = detectLevel(install.cmd);
5098
5118
  let cmd = install.cmd;
5099
5119
  const plan = { files: [] };
@@ -5189,6 +5209,9 @@ var installNpxSkillInstaller = async (ctx) => {
5189
5209
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed (no detail)", "preflight");
5190
5210
  return { ok: false, phase: "preflight", error: e };
5191
5211
  }
5212
+ if (await isAlreadyInstalled(ctx)) {
5213
+ return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
5214
+ }
5192
5215
  if (!/\bskills@(?!latest\b)\S+/.test(install.cmd)) {
5193
5216
  return {
5194
5217
  ok: false,
@@ -5403,12 +5426,6 @@ ${t("install.manifest_not_found.fix", { name: resolvedName })}`
5403
5426
  process.exit(1);
5404
5427
  });
5405
5428
  }
5406
- var PHASE_21 = /* @__PURE__ */ new Set([
5407
- "cc-plugin-marketplace",
5408
- "git-clone-with-setup",
5409
- "npx-skill-installer",
5410
- "mcp-http-add"
5411
- ]);
5412
5429
  async function listBaseManifests(cwd) {
5413
5430
  const out = [];
5414
5431
  for (const d of ["manifests/tools", "manifests/skill-packs"]) {
@@ -5451,11 +5468,6 @@ function registerInstallBase(program2) {
5451
5468
  continue;
5452
5469
  }
5453
5470
  const name = v.manifest.metadata.name;
5454
- const method = v.manifest.spec.install.method;
5455
- if (PHASE_21.has(method)) {
5456
- skipped.push({ name, reason: `deferred phase 2.1 (${method})` });
5457
- continue;
5458
- }
5459
5471
  const r = await runInstall(v.manifest, opts);
5460
5472
  if ("aborted" in r) skipped.push({ name, reason: `aborted: ${r.reason}` });
5461
5473
  else if (r.ok && "alreadyInstalled" in r && r.alreadyInstalled) alreadyInstalled.push(name);
@@ -5464,7 +5476,7 @@ function registerInstallBase(program2) {
5464
5476
  }
5465
5477
  console.log(
5466
5478
  `
5467
- installed: ${installed.length} / already-installed: ${alreadyInstalled.length} / skipped (deferred installer methods awaiting phase 2.1): ${skipped.length} / failed: ${failed.length}`
5479
+ installed: ${installed.length} / already-installed: ${alreadyInstalled.length} / skipped (user-aborted): ${skipped.length} / failed: ${failed.length}`
5468
5480
  );
5469
5481
  for (const i of installed) console.log(` installed ${i}`);
5470
5482
  for (const a of alreadyInstalled)
@@ -6054,12 +6066,6 @@ async function scanWorkflowsNested(workflowsDir, entries) {
6054
6066
  }
6055
6067
 
6056
6068
  // src/cli/lib/setup-helpers.ts
6057
- var PHASE_212 = /* @__PURE__ */ new Set([
6058
- "cc-plugin-marketplace",
6059
- "git-clone-with-setup",
6060
- "npx-skill-installer",
6061
- "mcp-http-add"
6062
- ]);
6063
6069
  async function warnIfAgentTeamsMissing() {
6064
6070
  const r = await checkAgentTeams();
6065
6071
  if (r.status !== "missing") return;
@@ -6075,14 +6081,15 @@ async function warnIfAgentTeamsMissing() {
6075
6081
  async function scanWorkflowsWithSkill(workflowsDir, entries) {
6076
6082
  return scanWorkflowsNested(workflowsDir, entries);
6077
6083
  }
6078
- async function runStepBInstall(manifestPaths) {
6084
+ async function runStepBInstall(manifestPaths, runOpts = {}) {
6079
6085
  const opts = {
6080
6086
  apply: true,
6081
6087
  dryRun: false,
6082
6088
  system: false,
6083
6089
  nonInteractive: true,
6084
6090
  fullDiff: false,
6085
- color: "auto"
6091
+ color: "auto",
6092
+ updateInstalled: runOpts.updateInstalled === true
6086
6093
  };
6087
6094
  const start = Date.now();
6088
6095
  const settled = await Promise.allSettled(
@@ -6102,8 +6109,6 @@ async function runStepBInstall(manifestPaths) {
6102
6109
  };
6103
6110
  }
6104
6111
  const name = v.manifest.metadata.name;
6105
- const method = v.manifest.spec.install.method;
6106
- if (PHASE_212.has(method)) return { status: "skipped", name };
6107
6112
  const r = await runInstall(v.manifest, opts);
6108
6113
  if ("aborted" in r) return { status: "skipped", name };
6109
6114
  if (r.ok && "alreadyInstalled" in r && r.alreadyInstalled)
@@ -6149,7 +6154,10 @@ function registerSetup(program2) {
6149
6154
  ).option("--dry-run", "preview only \u2014 do not write to disk (opt-in for advanced users)").option(
6150
6155
  "--user-lang <code>",
6151
6156
  "override detected OS locale for env.HARNESSED_USER_LANG (en | zh-Hans / zh-CN / zh-TW)"
6152
- ).option("--non-interactive", "skip all confirm prompts (CI / scripted setup)").option("--no-auto-install", "do not prompt to auto-install missing plugins (advisory only)").action(async (raw) => {
6157
+ ).option("--non-interactive", "skip all confirm prompts (CI / scripted setup)").option("--no-auto-install", "do not prompt to auto-install missing plugins (advisory only)").option(
6158
+ "--update-installed",
6159
+ "force re-install already-installed plugins (excludes MCP servers); default: skip if installed"
6160
+ ).action(async (raw) => {
6153
6161
  const dryRun = raw.dryRun === true;
6154
6162
  const pkgRoot = getPackageRoot();
6155
6163
  const workflowsDir = resolve(pkgRoot, "workflows");
@@ -6277,8 +6285,20 @@ function registerSetup(program2) {
6277
6285
  } else {
6278
6286
  console.warn(t("setup.step_d.skipped", { message: dResult.message }));
6279
6287
  }
6288
+ let updateInstalled2 = raw.updateInstalled === true;
6289
+ if (!updateInstalled2 && !dryRun && raw.nonInteractive !== true) {
6290
+ const isTty = process.stdin.isTTY === true && process.stdout.isTTY === true;
6291
+ if (isTty) {
6292
+ const { confirm: confirm4, isCancel: isCancel5 } = await import('@clack/prompts');
6293
+ const ans = await confirm4({
6294
+ message: "Update already-installed third-party plugins? (excludes MCP servers)",
6295
+ initialValue: false
6296
+ });
6297
+ if (!isCancel5(ans) && ans === true) updateInstalled2 = true;
6298
+ }
6299
+ }
6280
6300
  const manifestPaths = await listBaseManifests2(pkgRoot);
6281
- const b = await runStepBInstall(manifestPaths);
6301
+ const b = await runStepBInstall(manifestPaths, { updateInstalled: updateInstalled2 });
6282
6302
  const stepBMs = (b.elapsedMs / 1e3).toFixed(1);
6283
6303
  console.log(
6284
6304
  t("setup.step_b_complete", {