harnessed 3.9.7 → 3.9.9

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
@@ -789,11 +789,33 @@ async function isMcpServerRegistered(name) {
789
789
  return Object.hasOwn(servers, name);
790
790
  }
791
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);
792
+ try {
793
+ const path = join(homedir(), ".claude", "plugins", "installed_plugins.json");
794
+ const raw = await readFile(path, "utf8");
795
+ const parsed = JSON.parse(raw);
796
+ const plugins = parsed.plugins;
797
+ if (plugins && typeof plugins === "object") {
798
+ if (Object.hasOwn(plugins, pluginName)) return true;
799
+ if (Object.keys(plugins).some((k) => k.split("@")[0] === pluginName)) return true;
800
+ }
801
+ } catch {
802
+ }
803
+ for (const path of [
804
+ join(homedir(), ".claude", "settings.json"),
805
+ join(homedir(), ".claude.json")
806
+ ]) {
807
+ try {
808
+ const raw = await readFile(path, "utf8");
809
+ const parsed = JSON.parse(raw);
810
+ const plugins = parsed.enabledPlugins;
811
+ if (plugins && typeof plugins === "object") {
812
+ if (Object.hasOwn(plugins, pluginName)) return true;
813
+ if (Object.keys(plugins).some((k) => k.split("@")[0] === pluginName)) return true;
814
+ }
815
+ } catch {
816
+ }
817
+ }
818
+ return false;
797
819
  }
798
820
  var init_readClaudeConfig = __esm({
799
821
  "src/installers/lib/readClaudeConfig.ts"() {
@@ -1200,7 +1222,7 @@ var init_auto_install = __esm({
1200
1222
 
1201
1223
  // package.json
1202
1224
  var package_default = {
1203
- version: "3.9.7"};
1225
+ version: "3.9.9"};
1204
1226
 
1205
1227
  // src/manifest/errors.ts
1206
1228
  function instancePathToKeyPath(instancePath) {
@@ -4357,6 +4379,13 @@ var installCcHookAdd = async (ctx) => {
4357
4379
  await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, "", "");
4358
4380
  return { ok: true, backupId: bk.backupId, appliedFiles: [settingsPath3] };
4359
4381
  };
4382
+
4383
+ // src/installers/lib/idempotent.ts
4384
+ init_readClaudeConfig();
4385
+ function expandTildeForWindows(cmd) {
4386
+ const home = homedir().replace(/\\/g, "/");
4387
+ return cmd.replace(/(^|[\s"'`(])~\//g, `$1${home}/`);
4388
+ }
4360
4389
  var DEFAULT_VERIFY_TIMEOUT_MS = 15e3;
4361
4390
  var DEFAULT_INSTALL_TIMEOUT_MS = 6e4;
4362
4391
  async function spawnCmd(ctx, cmd, args, timeoutMs) {
@@ -4381,7 +4410,13 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
4381
4410
  const cwd = installCfg.cwd ?? ctx.cwd;
4382
4411
  let child;
4383
4412
  if (process.platform === "win32") {
4384
- child = spawn("cmd.exe", ["/c", cmd, ...args], { cwd, env, windowsHide: true });
4413
+ const expandedCmd = expandTildeForWindows(cmd);
4414
+ const expandedArgs = args.map(expandTildeForWindows);
4415
+ child = spawn("cmd.exe", ["/c", expandedCmd, ...expandedArgs], {
4416
+ cwd,
4417
+ env,
4418
+ windowsHide: true
4419
+ });
4385
4420
  } else {
4386
4421
  const joined = args.length > 0 ? `${cmd} ${args.join(" ")}` : cmd;
4387
4422
  child = spawn("/bin/sh", ["-c", joined], { cwd, env });
@@ -4434,16 +4469,83 @@ async function spawnCmd(ctx, cmd, args, timeoutMs) {
4434
4469
 
4435
4470
  // src/installers/lib/idempotent.ts
4436
4471
  var IDEMPOTENT_CHECK_TIMEOUT_MS = 1e4;
4437
- async function isAlreadyInstalled(ctx) {
4438
- if (ctx.opts.updateInstalled === true) return false;
4472
+ function extractSkillName(cmd, fallback) {
4473
+ const m = cmd.match(/\bskills(?:@\S+)?\s+add\s+(\S+)/i);
4474
+ if (!m?.[1]) return fallback;
4475
+ const seg = m[1].split("/");
4476
+ return seg[seg.length - 1] ?? fallback;
4477
+ }
4478
+ function extractGitCloneTarget(cmd) {
4479
+ const idx = cmd.indexOf("git clone");
4480
+ if (idx < 0) return null;
4481
+ const tail = cmd.slice(idx + "git clone".length).trim();
4482
+ const tokens = tail.split(/\s+/);
4483
+ let i = 0;
4484
+ while (i < tokens.length && tokens[i]?.startsWith("-")) {
4485
+ i += tokens[i]?.includes("=") ? 1 : 2;
4486
+ }
4487
+ const dest = tokens[i + 1];
4488
+ if (!dest || dest === "&&" || dest === ";" || dest === "|") return null;
4489
+ if (dest.startsWith("~/")) return join(homedir(), dest.slice(2));
4490
+ if (dest.startsWith("/") || /^[A-Z]:[\\/]/i.test(dest)) return dest;
4491
+ return null;
4492
+ }
4493
+ async function detectNative(ctx) {
4494
+ const method = ctx.manifest.spec.install.method;
4495
+ const cmd = ctx.manifest.spec.install.cmd;
4496
+ const name = ctx.manifest.metadata.name;
4497
+ if (method === "cc-plugin-marketplace") {
4498
+ const m = cmd.match(/(?:claude\s+)?plugin\s+install\s+(\S+)/i);
4499
+ const pluginName = m?.[1]?.split("@")[0] ?? name;
4500
+ try {
4501
+ return await isPluginRegistered(pluginName);
4502
+ } catch {
4503
+ return false;
4504
+ }
4505
+ }
4506
+ if (method === "npx-skill-installer") {
4507
+ const skillName = extractSkillName(cmd, name);
4508
+ const skillMd = join(homedir(), ".claude", "skills", skillName, "SKILL.md");
4509
+ try {
4510
+ await access(skillMd);
4511
+ return true;
4512
+ } catch {
4513
+ return false;
4514
+ }
4515
+ }
4516
+ if (method === "git-clone-with-setup") {
4517
+ const target = extractGitCloneTarget(cmd);
4518
+ if (!target) return false;
4519
+ try {
4520
+ await access(target);
4521
+ return true;
4522
+ } catch {
4523
+ return false;
4524
+ }
4525
+ }
4526
+ if (method === "npm-cli") {
4527
+ const skillDir = join(homedir(), ".claude", "skills", name);
4528
+ try {
4529
+ await access(skillDir);
4530
+ return true;
4531
+ } catch {
4532
+ return false;
4533
+ }
4534
+ }
4535
+ return false;
4536
+ }
4537
+ async function isAlreadyInstalled(ctx, opts = {}) {
4538
+ const honorUpdateFlag = opts.honorUpdateFlag !== false;
4539
+ if (honorUpdateFlag && ctx.opts.updateInstalled === true) return false;
4540
+ if (ctx.opts.dryRun) return false;
4541
+ const native = await detectNative(ctx);
4542
+ if (native) return true;
4439
4543
  const idempotentCmd = ctx.manifest.spec.install.idempotent_check;
4440
4544
  if (typeof idempotentCmd !== "string" || idempotentCmd.length === 0) {
4441
4545
  return false;
4442
4546
  }
4443
4547
  const r = await spawnCmd(ctx, idempotentCmd, [], IDEMPOTENT_CHECK_TIMEOUT_MS);
4444
- if (!("exitCode" in r)) {
4445
- return false;
4446
- }
4548
+ if (!("exitCode" in r)) return false;
4447
4549
  return r.exitCode === 0;
4448
4550
  }
4449
4551
 
@@ -4581,7 +4683,7 @@ ${newEntry}
4581
4683
  const r1 = await runArgs(["plugin", "marketplace", "add", parsed.marketplaceRef], spawnCwd);
4582
4684
  stepOneStderr = r1.stderr;
4583
4685
  }
4584
- const r2 = await runArgs(installArgs, spawnCwd);
4686
+ const r2 = await runArgs(installArgs, spawnCwd, 6e4);
4585
4687
  if (r2.exitCode !== 0) {
4586
4688
  return {
4587
4689
  ok: false,
@@ -4856,6 +4958,9 @@ var installMcpHttpAdd = async (ctx) => {
4856
4958
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed (no detail)", "preflight");
4857
4959
  return { ok: false, phase: "preflight", error: e };
4858
4960
  }
4961
+ if (await isAlreadyInstalled(ctx, { honorUpdateFlag: false })) {
4962
+ return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
4963
+ }
4859
4964
  const name = ctx.manifest.metadata.name;
4860
4965
  const url = extractUrl(install.cmd);
4861
4966
  if (!url) {
@@ -4992,6 +5097,9 @@ var installMcpStdioAdd = async (ctx) => {
4992
5097
  const e = pre.errors[0] ?? err(ctx, "/", "preflight failed (no detail)", "preflight");
4993
5098
  return { ok: false, phase: "preflight", error: e };
4994
5099
  }
5100
+ if (await isAlreadyInstalled(ctx, { honorUpdateFlag: false })) {
5101
+ return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
5102
+ }
4995
5103
  const name = ctx.manifest.metadata.name;
4996
5104
  const pkg = ctx.manifest.metadata.upstream.source;
4997
5105
  const ver = install.npm_version;
@@ -5181,7 +5289,7 @@ var installNpmCli = async (ctx) => {
5181
5289
  await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, install.npm_version, "");
5182
5290
  return { ok: true, backupId: bk.backupId, appliedFiles: [] };
5183
5291
  };
5184
- function extractSkillName(cmd, fallback) {
5292
+ function extractSkillName2(cmd, fallback) {
5185
5293
  const m = cmd.match(/\bskills(?:@\S+)?\s+add\s+(\S+)/i);
5186
5294
  if (!m || m[1] === void 0) return fallback;
5187
5295
  const ref = m[1];
@@ -5212,21 +5320,6 @@ var installNpxSkillInstaller = async (ctx) => {
5212
5320
  if (await isAlreadyInstalled(ctx)) {
5213
5321
  return { ok: true, alreadyInstalled: true, backupId: "noop-idempotent" };
5214
5322
  }
5215
- if (!/\bskills@(?!latest\b)\S+/.test(install.cmd)) {
5216
- return {
5217
- ok: false,
5218
- phase: "preflight",
5219
- error: {
5220
- ...err(
5221
- ctx,
5222
- "/spec/install/cmd",
5223
- `npx-skill-installer cmd must reference a pinned skills@<version> (got: '${install.cmd.slice(0, 100)}'); @latest is forbidden for reproducibility (ADR 0001)`,
5224
- "skills-pin-required"
5225
- ),
5226
- suggest: "change `skills@latest` \u2192 `skills@1.5.7` (current research-pinned stable)"
5227
- }
5228
- };
5229
- }
5230
5323
  if (!/\B--copy\b/.test(install.cmd) || !/\B--global\b/.test(install.cmd)) {
5231
5324
  return {
5232
5325
  ok: false,
@@ -5243,7 +5336,7 @@ var installNpxSkillInstaller = async (ctx) => {
5243
5336
  };
5244
5337
  }
5245
5338
  const name = ctx.manifest.metadata.name;
5246
- const skillSegment = extractSkillName(install.cmd, name);
5339
+ const skillSegment = extractSkillName2(install.cmd, name);
5247
5340
  const skillDir = join(homedir(), ".claude", "skills", skillSegment);
5248
5341
  const skillMdPath = join(skillDir, "SKILL.md");
5249
5342
  const plan = {
@@ -6110,7 +6203,7 @@ async function runStepBInstall(manifestPaths, runOpts = {}) {
6110
6203
  }
6111
6204
  const name = v.manifest.metadata.name;
6112
6205
  const r = await runInstall(v.manifest, opts);
6113
- if ("aborted" in r) return { status: "skipped", name };
6206
+ if ("aborted" in r) return { status: "skipped", name, reason: r.reason };
6114
6207
  if (r.ok && "alreadyInstalled" in r && r.alreadyInstalled)
6115
6208
  return { status: "already-installed", name };
6116
6209
  if (r.ok) return { status: "installed", name };
@@ -6129,8 +6222,10 @@ async function runStepBInstall(manifestPaths, runOpts = {}) {
6129
6222
  };
6130
6223
  if (v.status === "installed") installed.push(v.name);
6131
6224
  else if (v.status === "already-installed") alreadyInstalled.push(v.name);
6132
- else if (v.status === "skipped") skipped.push(v.name);
6133
- else
6225
+ else if (v.status === "skipped") {
6226
+ const skipReason = v.reason ?? "unknown";
6227
+ skipped.push({ name: v.name, reason: skipReason });
6228
+ } else
6134
6229
  failed.push(`${v.name}: ${v.reason}`);
6135
6230
  }
6136
6231
  return { installed, alreadyInstalled, skipped, failed, elapsedMs: Date.now() - start };
@@ -6303,7 +6398,7 @@ function registerSetup(program2) {
6303
6398
  console.log(
6304
6399
  ` [B] already-installed ${n} \u2014 run \`/mcp\` in Claude Code to verify connection`
6305
6400
  );
6306
- for (const n of b.skipped) console.log(` [B] skipped ${n}`);
6401
+ for (const s of b.skipped) console.log(` [B] skipped ${s.name} \u2014 ${s.reason}`);
6307
6402
  for (const n of b.failed) console.error(` [B] failed ${n}`);
6308
6403
  if (!forceFirstPass && !dryRun && raw.nonInteractive !== true && b.alreadyInstalled.length > 0) {
6309
6404
  const isTty = process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -6323,7 +6418,8 @@ Force-update pass complete: ${b2.installed.length} installed / ${b2.alreadyInsta
6323
6418
  for (const n of b2.installed) console.log(` [B*] installed ${n}`);
6324
6419
  for (const n of b2.alreadyInstalled)
6325
6420
  console.log(` [B*] already-installed ${n} (MCP / no force-update)`);
6326
- for (const n of b2.skipped) console.log(` [B*] skipped ${n}`);
6421
+ for (const s of b2.skipped)
6422
+ console.log(` [B*] skipped ${s.name} \u2014 ${s.reason}`);
6327
6423
  for (const n of b2.failed) console.error(` [B*] failed ${n}`);
6328
6424
  }
6329
6425
  }
@@ -6604,7 +6700,7 @@ var uninstallNpmCli = async (ctx) => {
6604
6700
  }
6605
6701
  return { ok: true, removedPaths: [pkg] };
6606
6702
  };
6607
- function extractSkillName2(cmd, fallback) {
6703
+ function extractSkillName3(cmd, fallback) {
6608
6704
  const m = cmd.match(/\bskills(?:@\S+)?\s+add\s+(\S+)/i);
6609
6705
  if (!m || m[1] === void 0) return fallback;
6610
6706
  const ref = m[1];
@@ -6619,7 +6715,7 @@ var uninstallNpxSkillInstaller = async (ctx) => {
6619
6715
  const abort = dryRunGate(ctx);
6620
6716
  if (abort) return abort;
6621
6717
  const name = ctx.manifest.metadata.name;
6622
- const skillName = extractSkillName2(install.cmd, name);
6718
+ const skillName = extractSkillName3(install.cmd, name);
6623
6719
  const skillDir = join(homedir(), ".claude", "skills", skillName);
6624
6720
  await rm(skillDir, { recursive: true, force: true, maxRetries: 3 });
6625
6721
  return { ok: true, removedPaths: [skillDir] };