harnessed 3.9.3 → 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
 
@@ -1118,15 +1144,14 @@ async function runAutoInstall(opts) {
1118
1144
  out.skipped.push(check.name);
1119
1145
  continue;
1120
1146
  }
1121
- let chainOk = true;
1147
+ const failureReasons = [];
1122
1148
  for (const cmd of commands) {
1123
1149
  const tokens = cmd.split(/\s+/).filter((t2) => t2.length > 0);
1124
1150
  const exe = tokens[0];
1125
1151
  const args = tokens.slice(1);
1126
1152
  if (exe === void 0) {
1127
- out.failed.push({ name: check.name, reason: `empty command in install_commands` });
1128
- chainOk = false;
1129
- break;
1153
+ failureReasons.push(`empty command in install_commands`);
1154
+ continue;
1130
1155
  }
1131
1156
  const r = spawnSync(exe, args, {
1132
1157
  encoding: "utf8",
@@ -1137,16 +1162,28 @@ async function runAutoInstall(opts) {
1137
1162
  shell: true
1138
1163
  });
1139
1164
  if (r.status !== 0) {
1140
- const reason = r.error !== void 0 ? `spawn error: ${r.error.message}` : `exit code ${r.status ?? "<unknown>"} on \`${cmd}\``;
1141
- out.failed.push({ name: check.name, reason });
1142
- console.error(` \u2717 failed ${check.name} \u2014 ${reason}`);
1143
- chainOk = false;
1144
- break;
1165
+ const reason = r.error !== void 0 ? `spawn error on \`${cmd}\`: ${r.error.message}` : `exit ${r.status ?? "<unknown>"} on \`${cmd}\``;
1166
+ failureReasons.push(reason);
1167
+ console.error(` \u2717 ${cmd} \u2014 ${reason}`);
1145
1168
  }
1146
1169
  }
1147
- if (chainOk) {
1170
+ if (failureReasons.length === 0) {
1148
1171
  out.installed.push(check.name);
1149
1172
  console.log(` \u2713 installed ${check.name}`);
1173
+ } else if (failureReasons.length === commands.length) {
1174
+ out.failed.push({
1175
+ name: check.name,
1176
+ reason: `all commands failed (${failureReasons.length})`
1177
+ });
1178
+ console.error(` \u2717 failed ${check.name} \u2014 all ${commands.length} commands failed`);
1179
+ } else {
1180
+ out.failed.push({
1181
+ name: check.name,
1182
+ reason: `partial: ${failureReasons.length}/${commands.length} commands failed`
1183
+ });
1184
+ console.error(
1185
+ ` \u26A0 ${check.name} \u2014 partial: ${failureReasons.length}/${commands.length} commands failed (others succeeded)`
1186
+ );
1150
1187
  }
1151
1188
  }
1152
1189
  console.log(
@@ -1163,7 +1200,7 @@ var init_auto_install = __esm({
1163
1200
 
1164
1201
  // package.json
1165
1202
  var package_default = {
1166
- version: "3.9.3"};
1203
+ version: "3.9.6"};
1167
1204
 
1168
1205
  // src/manifest/errors.ts
1169
1206
  function instancePathToKeyPath(instancePath) {
@@ -4320,39 +4357,98 @@ var installCcHookAdd = async (ctx) => {
4320
4357
  await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, "", "");
4321
4358
  return { ok: true, backupId: bk.backupId, appliedFiles: [settingsPath3] };
4322
4359
  };
4323
- function getUserClaudeJsonPath() {
4324
- return join(homedir(), ".claude.json");
4325
- }
4326
- async function readUserClaudeJson() {
4327
- const path = getUserClaudeJsonPath();
4328
- let raw;
4329
- try {
4330
- raw = await readFile(path, "utf8");
4331
- } catch (err2) {
4332
- if (err2.code === "ENOENT") return {};
4333
- 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
+ };
4334
4377
  }
4335
- try {
4336
- const parsed = JSON.parse(raw);
4337
- if (parsed === null || typeof parsed !== "object") return {};
4338
- return parsed;
4339
- } catch {
4340
- 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 });
4341
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
+ });
4342
4433
  }
4343
- async function isMcpServerRegistered(name) {
4344
- const config = await readUserClaudeJson();
4345
- const servers = config.mcpServers;
4346
- if (!servers || typeof servers !== "object") return false;
4347
- return Object.hasOwn(servers, name);
4348
- }
4349
- async function isPluginRegistered(pluginName) {
4350
- const config = await readUserClaudeJson();
4351
- const plugins = config.enabledPlugins;
4352
- if (!plugins || typeof plugins !== "object") return false;
4353
- if (Object.hasOwn(plugins, pluginName)) return true;
4354
- 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;
4355
4448
  }
4449
+
4450
+ // src/installers/ccPluginMarketplace.ts
4451
+ init_readClaudeConfig();
4356
4452
  function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
4357
4453
  return new Promise((resolve15) => {
4358
4454
  const isWin = process.platform === "win32";
@@ -4413,6 +4509,9 @@ var installCcPluginMarketplace = async (ctx) => {
4413
4509
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed (no detail)", "preflight");
4414
4510
  return { ok: false, phase: "preflight", error: e };
4415
4511
  }
4512
+ if (await isAlreadyInstalled(ctx)) {
4513
+ return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
4514
+ }
4416
4515
  const parsed = parseCmd(install.cmd);
4417
4516
  if (!parsed) {
4418
4517
  return {
@@ -4513,82 +4612,6 @@ ${newEntry}
4513
4612
  await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, install.git_ref, "");
4514
4613
  return { ok: true, backupId: bk.backupId, appliedFiles: [settingsFile] };
4515
4614
  };
4516
- var DEFAULT_VERIFY_TIMEOUT_MS = 15e3;
4517
- var DEFAULT_INSTALL_TIMEOUT_MS = 6e4;
4518
- async function spawnCmd(ctx, cmd, args, timeoutMs) {
4519
- const violation = checkCmdString(cmd);
4520
- if (violation) {
4521
- return {
4522
- ok: false,
4523
- phase: "preflight",
4524
- error: {
4525
- file: ctx.manifest.metadata.name,
4526
- path: "/spec/install/cmd",
4527
- 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.`,
4528
- line: null,
4529
- column: null,
4530
- keyword: "security-gate-bypass"
4531
- }
4532
- };
4533
- }
4534
- const installCfg = ctx.manifest.spec.install;
4535
- const effectiveTimeoutMs = timeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
4536
- const env = { ...process.env, ...installCfg.env ?? {} };
4537
- const cwd = installCfg.cwd ?? ctx.cwd;
4538
- let child;
4539
- if (process.platform === "win32") {
4540
- child = spawn("cmd.exe", ["/c", cmd, ...args], { cwd, env, windowsHide: true });
4541
- } else {
4542
- const joined = args.length > 0 ? `${cmd} ${args.join(" ")}` : cmd;
4543
- child = spawn("/bin/sh", ["-c", joined], { cwd, env });
4544
- }
4545
- let stdout2 = "";
4546
- let stderr = "";
4547
- child.stdout?.setEncoding("utf8").on("data", (chunk) => {
4548
- stdout2 += chunk;
4549
- });
4550
- child.stderr?.setEncoding("utf8").on("data", (chunk) => {
4551
- stderr += chunk;
4552
- });
4553
- return await new Promise((resolve15) => {
4554
- const timer = setTimeout(() => {
4555
- child.kill("SIGKILL");
4556
- resolve15({
4557
- ok: false,
4558
- phase: "spawn",
4559
- error: {
4560
- file: ctx.manifest.metadata.name,
4561
- path: "/spec/install/cmd",
4562
- message: `spawn timed out after ${effectiveTimeoutMs}ms (cmd: ${cmd}); partial stderr: ${stderr.slice(0, 200)}`,
4563
- line: null,
4564
- column: null,
4565
- keyword: "spawn-timeout"
4566
- }
4567
- });
4568
- }, effectiveTimeoutMs);
4569
- child.on("error", (err2) => {
4570
- clearTimeout(timer);
4571
- resolve15({
4572
- ok: false,
4573
- phase: "spawn",
4574
- error: {
4575
- file: ctx.manifest.metadata.name,
4576
- path: "/spec/install/cmd",
4577
- message: `spawn failed: ${err2.message}`,
4578
- line: null,
4579
- column: null,
4580
- keyword: "spawn-error"
4581
- }
4582
- });
4583
- });
4584
- child.on("close", (code) => {
4585
- clearTimeout(timer);
4586
- resolve15({ ok: true, exitCode: code ?? -1, stdout: stdout2, stderr });
4587
- });
4588
- });
4589
- }
4590
-
4591
- // src/installers/gitCloneWithSetup.ts
4592
4615
  function gitRevParseHead(cwd, timeoutMs = 1e4) {
4593
4616
  return new Promise((resolve15) => {
4594
4617
  const isWin = process.platform === "win32";
@@ -4658,6 +4681,9 @@ var installGitCloneWithSetup = async (ctx) => {
4658
4681
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed (no detail)", "preflight");
4659
4682
  return { ok: false, phase: "preflight", error: e };
4660
4683
  }
4684
+ if (await isAlreadyInstalled(ctx)) {
4685
+ return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
4686
+ }
4661
4687
  if (!/^[a-f0-9]{7,40}$/.test(install.git_ref)) {
4662
4688
  return {
4663
4689
  ok: false,
@@ -4775,6 +4801,7 @@ var installGitCloneWithSetup = async (ctx) => {
4775
4801
  };
4776
4802
 
4777
4803
  // src/installers/mcpHttpAdd.ts
4804
+ init_readClaudeConfig();
4778
4805
  function resolveEnvVars(value) {
4779
4806
  const pattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
4780
4807
  let resolved = value;
@@ -4943,6 +4970,7 @@ ${newEntry}
4943
4970
  };
4944
4971
 
4945
4972
  // src/installers/mcpStdioAdd.ts
4973
+ init_readClaudeConfig();
4946
4974
  var installMcpStdioAdd = async (ctx) => {
4947
4975
  const install = ctx.manifest.spec.install;
4948
4976
  if (install.method !== "mcp-stdio-add") {
@@ -5083,6 +5111,9 @@ var installNpmCli = async (ctx) => {
5083
5111
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed (no detail)", "preflight");
5084
5112
  return { ok: false, phase: "preflight", error: e };
5085
5113
  }
5114
+ if (await isAlreadyInstalled(ctx)) {
5115
+ return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
5116
+ }
5086
5117
  let level = detectLevel(install.cmd);
5087
5118
  let cmd = install.cmd;
5088
5119
  const plan = { files: [] };
@@ -5178,6 +5209,9 @@ var installNpxSkillInstaller = async (ctx) => {
5178
5209
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed (no detail)", "preflight");
5179
5210
  return { ok: false, phase: "preflight", error: e };
5180
5211
  }
5212
+ if (await isAlreadyInstalled(ctx)) {
5213
+ return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
5214
+ }
5181
5215
  if (!/\bskills@(?!latest\b)\S+/.test(install.cmd)) {
5182
5216
  return {
5183
5217
  ok: false,
@@ -5392,12 +5426,6 @@ ${t("install.manifest_not_found.fix", { name: resolvedName })}`
5392
5426
  process.exit(1);
5393
5427
  });
5394
5428
  }
5395
- var PHASE_21 = /* @__PURE__ */ new Set([
5396
- "cc-plugin-marketplace",
5397
- "git-clone-with-setup",
5398
- "npx-skill-installer",
5399
- "mcp-http-add"
5400
- ]);
5401
5429
  async function listBaseManifests(cwd) {
5402
5430
  const out = [];
5403
5431
  for (const d of ["manifests/tools", "manifests/skill-packs"]) {
@@ -5440,11 +5468,6 @@ function registerInstallBase(program2) {
5440
5468
  continue;
5441
5469
  }
5442
5470
  const name = v.manifest.metadata.name;
5443
- const method = v.manifest.spec.install.method;
5444
- if (PHASE_21.has(method)) {
5445
- skipped.push({ name, reason: `deferred phase 2.1 (${method})` });
5446
- continue;
5447
- }
5448
5471
  const r = await runInstall(v.manifest, opts);
5449
5472
  if ("aborted" in r) skipped.push({ name, reason: `aborted: ${r.reason}` });
5450
5473
  else if (r.ok && "alreadyInstalled" in r && r.alreadyInstalled) alreadyInstalled.push(name);
@@ -5453,7 +5476,7 @@ function registerInstallBase(program2) {
5453
5476
  }
5454
5477
  console.log(
5455
5478
  `
5456
- 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}`
5457
5480
  );
5458
5481
  for (const i of installed) console.log(` installed ${i}`);
5459
5482
  for (const a of alreadyInstalled)
@@ -6043,12 +6066,6 @@ async function scanWorkflowsNested(workflowsDir, entries) {
6043
6066
  }
6044
6067
 
6045
6068
  // src/cli/lib/setup-helpers.ts
6046
- var PHASE_212 = /* @__PURE__ */ new Set([
6047
- "cc-plugin-marketplace",
6048
- "git-clone-with-setup",
6049
- "npx-skill-installer",
6050
- "mcp-http-add"
6051
- ]);
6052
6069
  async function warnIfAgentTeamsMissing() {
6053
6070
  const r = await checkAgentTeams();
6054
6071
  if (r.status !== "missing") return;
@@ -6064,14 +6081,15 @@ async function warnIfAgentTeamsMissing() {
6064
6081
  async function scanWorkflowsWithSkill(workflowsDir, entries) {
6065
6082
  return scanWorkflowsNested(workflowsDir, entries);
6066
6083
  }
6067
- async function runStepBInstall(manifestPaths) {
6084
+ async function runStepBInstall(manifestPaths, runOpts = {}) {
6068
6085
  const opts = {
6069
6086
  apply: true,
6070
6087
  dryRun: false,
6071
6088
  system: false,
6072
6089
  nonInteractive: true,
6073
6090
  fullDiff: false,
6074
- color: "auto"
6091
+ color: "auto",
6092
+ updateInstalled: runOpts.updateInstalled === true
6075
6093
  };
6076
6094
  const start = Date.now();
6077
6095
  const settled = await Promise.allSettled(
@@ -6091,8 +6109,6 @@ async function runStepBInstall(manifestPaths) {
6091
6109
  };
6092
6110
  }
6093
6111
  const name = v.manifest.metadata.name;
6094
- const method = v.manifest.spec.install.method;
6095
- if (PHASE_212.has(method)) return { status: "skipped", name };
6096
6112
  const r = await runInstall(v.manifest, opts);
6097
6113
  if ("aborted" in r) return { status: "skipped", name };
6098
6114
  if (r.ok && "alreadyInstalled" in r && r.alreadyInstalled)
@@ -6138,7 +6154,10 @@ function registerSetup(program2) {
6138
6154
  ).option("--dry-run", "preview only \u2014 do not write to disk (opt-in for advanced users)").option(
6139
6155
  "--user-lang <code>",
6140
6156
  "override detected OS locale for env.HARNESSED_USER_LANG (en | zh-Hans / zh-CN / zh-TW)"
6141
- ).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) => {
6142
6161
  const dryRun = raw.dryRun === true;
6143
6162
  const pkgRoot = getPackageRoot();
6144
6163
  const workflowsDir = resolve(pkgRoot, "workflows");
@@ -6266,8 +6285,20 @@ function registerSetup(program2) {
6266
6285
  } else {
6267
6286
  console.warn(t("setup.step_d.skipped", { message: dResult.message }));
6268
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
+ }
6269
6300
  const manifestPaths = await listBaseManifests2(pkgRoot);
6270
- const b = await runStepBInstall(manifestPaths);
6301
+ const b = await runStepBInstall(manifestPaths, { updateInstalled: updateInstalled2 });
6271
6302
  const stepBMs = (b.elapsedMs / 1e3).toFixed(1);
6272
6303
  console.log(
6273
6304
  t("setup.step_b_complete", {