@allocator-one/harness-cli 1.2.1 → 1.3.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.
@@ -1477,12 +1477,12 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
1477
1477
  };
1478
1478
  }
1479
1479
  function resolveCacheDir(env = process.env) {
1480
- return absolute(env["HARNESS_CACHE_DIR"]) ?? join4(cacheHome(env), "harness", "attachments");
1480
+ return absolute(env["HARNESS_CACHE_DIR"]) ?? join4(resolveCacheHome(env), "harness", "attachments");
1481
1481
  }
1482
1482
  function absolute(value) {
1483
1483
  return value !== void 0 && value !== "" && isAbsolute3(value) ? value : null;
1484
1484
  }
1485
- function cacheHome(env) {
1485
+ function resolveCacheHome(env = process.env) {
1486
1486
  return absolute(env["XDG_CACHE_HOME"]) ?? join4(homedir3() || tmpdir(), ".cache");
1487
1487
  }
1488
1488
  function normalize(url) {
@@ -1595,6 +1595,7 @@ export {
1595
1595
  trustHost,
1596
1596
  DEFAULT_BASE_URL,
1597
1597
  resolveConfig,
1598
+ resolveCacheHome,
1598
1599
  validateBaseUrl,
1599
1600
  loginHelp
1600
1601
  };
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CLI_VERSION,
3
4
  Client,
4
5
  CommandError,
5
6
  DEFAULT_BASE_URL,
@@ -43,6 +44,7 @@ import {
43
44
  readCredentials,
44
45
  ready,
45
46
  requireIdentifiers,
47
+ resolveCacheHome,
46
48
  resolveConfig,
47
49
  search,
48
50
  show,
@@ -57,7 +59,7 @@ import {
57
59
  version,
58
60
  workflowStates,
59
61
  writeCredentials
60
- } from "./chunk-JMGBMBN6.js";
62
+ } from "./chunk-3WR6YFU6.js";
61
63
 
62
64
  // src/cli.ts
63
65
  import { createInterface as createInterface2 } from "readline";
@@ -370,6 +372,426 @@ function defaultOpenBrowser(url) {
370
372
  }
371
373
  }
372
374
 
375
+ // src/core/upgrade.ts
376
+ import { spawn, spawnSync } from "child_process";
377
+ import {
378
+ accessSync as accessSync2,
379
+ constants,
380
+ existsSync,
381
+ linkSync,
382
+ mkdirSync,
383
+ readFileSync,
384
+ realpathSync,
385
+ renameSync,
386
+ rmSync,
387
+ statSync,
388
+ writeFileSync
389
+ } from "fs";
390
+ import { basename, dirname, join as join2, parse, sep } from "path";
391
+ import { fileURLToPath } from "url";
392
+ var PACKAGE_NAME = "@allocator-one/harness-cli";
393
+ var UPDATE_CHECK_COMMAND = "__update-check";
394
+ var REEXEC_SENTINEL = "HARNESS_UPGRADED";
395
+ var DEFAULT_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
396
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
397
+ var REGISTRY_TIMEOUT_MS = 5e3;
398
+ var NPM_ROOT_TIMEOUT_MS = 15e3;
399
+ var INSTALL_TIMEOUT_MS = 12e4;
400
+ var LOCK_STALE_MS = NPM_ROOT_TIMEOUT_MS + INSTALL_TIMEOUT_MS + 6e4;
401
+ function detectInstall(from = fileURLToPath(import.meta.url)) {
402
+ const unknown = { kind: "unknown", packageRoot: null, nodeModules: null };
403
+ if (from.split(sep).includes("_npx")) return { ...unknown, kind: "npx" };
404
+ const packageRoot = findPackageRoot(from);
405
+ if (packageRoot === null) return unknown;
406
+ if (existsSync(join2(packageRoot, ".git"))) {
407
+ return { kind: "checkout", packageRoot, nodeModules: null };
408
+ }
409
+ const nodeModules = enclosingNodeModules(packageRoot);
410
+ if (nodeModules === null) return { ...unknown, packageRoot };
411
+ const kind = existsSync(join2(nodeModules, ".bin")) ? "local" : "global";
412
+ return { kind, packageRoot, nodeModules };
413
+ }
414
+ function findPackageRoot(from) {
415
+ let dir = dirname(from);
416
+ const { root } = parse(dir);
417
+ for (; ; ) {
418
+ if (packageNameAt(join2(dir, "package.json")) === PACKAGE_NAME) return dir;
419
+ if (dir === root) return null;
420
+ dir = dirname(dir);
421
+ }
422
+ }
423
+ function packageNameAt(path) {
424
+ try {
425
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
426
+ if (typeof parsed !== "object" || parsed === null) return null;
427
+ const name = parsed["name"];
428
+ return typeof name === "string" ? name : null;
429
+ } catch {
430
+ return null;
431
+ }
432
+ }
433
+ function versionAt(packageRoot) {
434
+ try {
435
+ const parsed = JSON.parse(readFileSync(join2(packageRoot, "package.json"), "utf8"));
436
+ if (typeof parsed !== "object" || parsed === null) return null;
437
+ const version2 = parsed["version"];
438
+ return typeof version2 === "string" ? version2 : null;
439
+ } catch {
440
+ return null;
441
+ }
442
+ }
443
+ function enclosingNodeModules(packageRoot) {
444
+ let dir = dirname(packageRoot);
445
+ const { root } = parse(dir);
446
+ for (; ; ) {
447
+ if (basename(dir) === "node_modules") return dir;
448
+ if (dir === root) return null;
449
+ dir = dirname(dir);
450
+ }
451
+ }
452
+ var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
453
+ function parseVersion(value) {
454
+ const match = SEMVER.exec(value.trim());
455
+ if (match === null) return null;
456
+ return {
457
+ parts: [Number(match[1]), Number(match[2]), Number(match[3])],
458
+ pre: match[4] ?? null
459
+ };
460
+ }
461
+ function isNewer(candidate, current) {
462
+ const a = parseVersion(candidate);
463
+ const b = parseVersion(current);
464
+ if (a === null || b === null) return false;
465
+ for (let index = 0; index < 3; index++) {
466
+ if (a.parts[index] !== b.parts[index]) return a.parts[index] > b.parts[index];
467
+ }
468
+ if (a.pre === b.pre) return false;
469
+ if (a.pre === null) return true;
470
+ if (b.pre === null) return false;
471
+ return comparePrerelease(a.pre, b.pre) > 0;
472
+ }
473
+ function comparePrerelease(left, right) {
474
+ const a = left.split(".");
475
+ const b = right.split(".");
476
+ for (let index = 0; index < Math.max(a.length, b.length); index++) {
477
+ const x = a[index];
478
+ const y = b[index];
479
+ if (x === void 0) return -1;
480
+ if (y === void 0) return 1;
481
+ const xNumeric = /^\d+$/.test(x);
482
+ const yNumeric = /^\d+$/.test(y);
483
+ if (xNumeric && yNumeric) {
484
+ if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1;
485
+ } else if (xNumeric !== yNumeric) {
486
+ return xNumeric ? -1 : 1;
487
+ } else if (x !== y) {
488
+ return x < y ? -1 : 1;
489
+ }
490
+ }
491
+ return 0;
492
+ }
493
+ function stampPath(env = process.env) {
494
+ return join2(resolveCacheHome(env), "harness", "update-check.json");
495
+ }
496
+ function readStamp(env = process.env) {
497
+ try {
498
+ const parsed = JSON.parse(readFileSync(stampPath(env), "utf8"));
499
+ if (typeof parsed !== "object" || parsed === null) return null;
500
+ const record = parsed;
501
+ const checkedAt = record["checkedAt"];
502
+ if (typeof checkedAt !== "number" || !Number.isFinite(checkedAt)) return null;
503
+ const latest = record["latest"];
504
+ return { checkedAt, latest: typeof latest === "string" ? latest : null };
505
+ } catch {
506
+ return null;
507
+ }
508
+ }
509
+ function writeStamp(stamp, env = process.env) {
510
+ const path = stampPath(env);
511
+ const temporary = `${path}.${process.pid}.tmp`;
512
+ const latest = stamp.latest ?? readStamp(env)?.latest ?? null;
513
+ try {
514
+ mkdirSync(dirname(path), { recursive: true });
515
+ writeFileSync(temporary, JSON.stringify({ ...stamp, latest }) + "\n");
516
+ renameSync(temporary, path);
517
+ } catch {
518
+ try {
519
+ rmSync(temporary, { force: true });
520
+ } catch {
521
+ }
522
+ }
523
+ }
524
+ function updateCheck(options = {}) {
525
+ const env = options.env ?? process.env;
526
+ const now = options.now ?? Date.now();
527
+ if (!checksEnabled(env)) return null;
528
+ const install = options.install ?? detectInstall();
529
+ if (install.kind !== "global" && install.kind !== "local") return null;
530
+ const stamp = readStamp(env);
531
+ if (stamp === null || now - stamp.checkedAt >= CHECK_INTERVAL_MS) {
532
+ writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env);
533
+ (options.refresh ?? spawnRefresh)();
534
+ }
535
+ if (stamp !== null && stamp.latest !== null && isNewer(stamp.latest, CLI_VERSION)) {
536
+ return `harness: ${stamp.latest} is available (you have ${CLI_VERSION}) \u2014 ${upgradeHint(install)}
537
+ `;
538
+ }
539
+ return null;
540
+ }
541
+ function upgradeHint(install) {
542
+ return install.kind === "local" ? `npm i ${PACKAGE_NAME}@latest` : `npm i -g ${PACKAGE_NAME}@latest`;
543
+ }
544
+ function spawnRefresh() {
545
+ const self = process.argv[1];
546
+ if (self === void 0 || self === "") return;
547
+ try {
548
+ const child = spawn(process.execPath, [self, UPDATE_CHECK_COMMAND], {
549
+ detached: true,
550
+ stdio: "ignore"
551
+ });
552
+ child.on("error", () => {
553
+ });
554
+ child.unref();
555
+ } catch {
556
+ }
557
+ }
558
+ async function performUpdateCheck(env = process.env, now = Date.now()) {
559
+ if (!checksEnabled(env)) return;
560
+ writeStamp({ checkedAt: now, latest: await fetchLatestVersion(env) }, env);
561
+ }
562
+ async function fetchLatestVersion(env) {
563
+ const configured = env["HARNESS_REGISTRY_URL"];
564
+ const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : DEFAULT_REGISTRY_URL;
565
+ try {
566
+ const response = await fetch(url, {
567
+ headers: { accept: "application/json" },
568
+ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS)
569
+ });
570
+ if (!response.ok) return null;
571
+ const parsed = await response.json();
572
+ if (typeof parsed !== "object" || parsed === null) return null;
573
+ const version2 = parsed["version"];
574
+ if (typeof version2 !== "string" || parseVersion(version2) === null) return null;
575
+ return version2;
576
+ } catch {
577
+ return null;
578
+ }
579
+ }
580
+ function selfHeal(options = {}) {
581
+ const env = options.env ?? process.env;
582
+ const argv2 = options.argv ?? process.argv;
583
+ if (truthy(env[REEXEC_SENTINEL])) {
584
+ return { kind: "skipped", reason: "already upgraded once for this command" };
585
+ }
586
+ if (!autoUpgradeEnabled(env)) return { kind: "skipped", reason: "automatic upgrades are off" };
587
+ if (argv2[2] === "mcp") return { kind: "skipped", reason: "an MCP session must not be replaced mid-stream" };
588
+ const install = options.install ?? detectInstall();
589
+ if (install.kind !== "global" || install.packageRoot === null) {
590
+ return { kind: "skipped", reason: describeInstall(install.kind) };
591
+ }
592
+ const packageRoot = install.packageRoot;
593
+ const installed = versionAt(packageRoot);
594
+ if (installed !== null && isNewer(installed, CLI_VERSION)) {
595
+ return { kind: "upgraded", code: reexec(options, env, argv2) };
596
+ }
597
+ const lock = takeLock(env);
598
+ if (lock === null) return { kind: "skipped", reason: "another harness process is already upgrading" };
599
+ try {
600
+ const stillHeld = () => readHolder(lock.path)?.pid === lock.pid;
601
+ const result = (options.runInstall ?? npmInstallGlobal)(install, stillHeld);
602
+ if (!result.ok) return { kind: "failed", reason: result.detail };
603
+ const after = versionAt(packageRoot);
604
+ if (after !== null && after === installed) {
605
+ return { kind: "failed", reason: `still ${after} after upgrading \u2014 no newer release is published` };
606
+ }
607
+ if (after !== null && installed !== null && !isNewer(after, installed)) {
608
+ return { kind: "failed", reason: `npm left ${after} installed, which is not newer than ${installed}` };
609
+ }
610
+ } finally {
611
+ releaseLock(lock);
612
+ }
613
+ return { kind: "upgraded", code: reexec(options, env, argv2) };
614
+ }
615
+ function reexec(options, env, argv2) {
616
+ return (options.reexec ?? reexecSelf)({ ...env, [REEXEC_SENTINEL]: "1" }, argv2);
617
+ }
618
+ function describeInstall(kind) {
619
+ switch (kind) {
620
+ case "checkout":
621
+ return "this is a git checkout, not an npm install";
622
+ case "local":
623
+ return "this copy is a project dependency, not a global install";
624
+ case "npx":
625
+ return "npx runs a throwaway copy";
626
+ default:
627
+ return "cannot tell how this CLI was installed";
628
+ }
629
+ }
630
+ function npmInstallGlobal(install, stillHeld) {
631
+ const globalRoot = npmGlobalRoot();
632
+ if (globalRoot === null) return { ok: false, detail: "npm did not say where its global root is" };
633
+ if (globalRoot.includes("***")) {
634
+ return { ok: false, detail: "npm redacted its own global root, so this copy's location cannot be confirmed" };
635
+ }
636
+ if (install.nodeModules === null || !samePath(globalRoot, install.nodeModules)) {
637
+ return {
638
+ ok: false,
639
+ detail: `this copy lives in ${install.nodeModules ?? "an unrecognized place"}, not npm's global root (${globalRoot})`
640
+ };
641
+ }
642
+ try {
643
+ accessSync2(globalRoot, constants.W_OK);
644
+ } catch {
645
+ return { ok: false, detail: `${globalRoot} is not writable \u2014 upgrade it yourself` };
646
+ }
647
+ if (!stillHeld()) {
648
+ return { ok: false, detail: "lost the upgrade lock while preparing \u2014 another process took over" };
649
+ }
650
+ process.stderr.write(`harness: upgrading ${PACKAGE_NAME} \u2026
651
+ `);
652
+ const args = [
653
+ "install",
654
+ "--global",
655
+ "--ignore-scripts",
656
+ "--no-audit",
657
+ "--no-fund",
658
+ "--no-update-notifier",
659
+ "--loglevel=error",
660
+ `${PACKAGE_NAME}@latest`
661
+ ];
662
+ const result = spawnSync(npmCommand(), args, {
663
+ // npm's diagnostics belong on stderr with ours; stdout stays the
664
+ // command's own, so its summary is dropped rather than piped into jq.
665
+ stdio: ["ignore", "ignore", "inherit"],
666
+ timeout: INSTALL_TIMEOUT_MS,
667
+ ...npmSpawnOptions()
668
+ });
669
+ if (result.error !== void 0) return { ok: false, detail: result.error.message };
670
+ if (result.status !== 0) {
671
+ return { ok: false, detail: `npm install exited ${result.status ?? `on ${String(result.signal)}`}` };
672
+ }
673
+ return { ok: true, detail: "" };
674
+ }
675
+ function npmGlobalRoot() {
676
+ try {
677
+ const result = spawnSync(npmCommand(), ["root", "--global"], {
678
+ encoding: "utf8",
679
+ timeout: NPM_ROOT_TIMEOUT_MS,
680
+ ...npmSpawnOptions()
681
+ });
682
+ if (result.status !== 0 || typeof result.stdout !== "string") return null;
683
+ const path = result.stdout.trim();
684
+ return path === "" ? null : path;
685
+ } catch {
686
+ return null;
687
+ }
688
+ }
689
+ function npmCommand() {
690
+ return process.platform === "win32" ? "npm.cmd" : "npm";
691
+ }
692
+ function npmSpawnOptions() {
693
+ return process.platform === "win32" ? { shell: true } : {};
694
+ }
695
+ function samePath(left, right) {
696
+ const resolve = (path) => {
697
+ try {
698
+ return realpathSync(path);
699
+ } catch {
700
+ return path;
701
+ }
702
+ };
703
+ return resolve(left) === resolve(right);
704
+ }
705
+ function reexecSelf(env, argv2) {
706
+ const self = argv2[1];
707
+ if (self === void 0) return 1;
708
+ const result = spawnSync(process.execPath, [self, ...argv2.slice(2)], {
709
+ stdio: "inherit",
710
+ env
711
+ });
712
+ if (result.error !== void 0) {
713
+ process.stderr.write(`harness: upgraded, but re-running failed: ${result.error.message}
714
+ `);
715
+ return 1;
716
+ }
717
+ return result.status ?? 1;
718
+ }
719
+ function takeLock(env) {
720
+ const path = join2(resolveCacheHome(env), "harness", "upgrade.lock");
721
+ const temporary = `${path}.${process.pid}.tmp`;
722
+ try {
723
+ mkdirSync(dirname(path), { recursive: true });
724
+ writeFileSync(temporary, `${process.pid}
725
+ `);
726
+ } catch {
727
+ return null;
728
+ }
729
+ try {
730
+ for (let attempt = 0; attempt < 2; attempt++) {
731
+ try {
732
+ linkSync(temporary, path);
733
+ return { path, pid: process.pid };
734
+ } catch {
735
+ const holder = readHolder(path);
736
+ if (holder === null) continue;
737
+ const aged = Date.now() - holder.mtimeMs > LOCK_STALE_MS;
738
+ const abandoned = holder.pid === null ? aged : !running(holder.pid) || aged;
739
+ if (!abandoned) return null;
740
+ try {
741
+ rmSync(path, { force: true });
742
+ } catch {
743
+ return null;
744
+ }
745
+ }
746
+ }
747
+ return null;
748
+ } finally {
749
+ try {
750
+ rmSync(temporary, { force: true });
751
+ } catch {
752
+ }
753
+ }
754
+ }
755
+ function releaseLock(lock) {
756
+ try {
757
+ if (readHolder(lock.path)?.pid !== lock.pid) return;
758
+ rmSync(lock.path, { force: true });
759
+ } catch {
760
+ }
761
+ }
762
+ function readHolder(path) {
763
+ try {
764
+ const mtimeMs = statSync(path).mtimeMs;
765
+ const pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
766
+ return { pid: Number.isInteger(pid) && pid > 0 ? pid : null, mtimeMs };
767
+ } catch {
768
+ return null;
769
+ }
770
+ }
771
+ function running(pid) {
772
+ if (pid === null) return false;
773
+ try {
774
+ process.kill(pid, 0);
775
+ return true;
776
+ } catch (error) {
777
+ return error.code === "EPERM";
778
+ }
779
+ }
780
+ function truthy(value) {
781
+ if (value === void 0) return false;
782
+ const normalized = value.trim().toLowerCase();
783
+ return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
784
+ }
785
+ function checksEnabled(env) {
786
+ return !truthy(env["CI"]) && !truthy(env["HARNESS_NO_UPDATE_CHECK"]);
787
+ }
788
+ function autoUpgradeEnabled(env) {
789
+ if (!checksEnabled(env)) return false;
790
+ const explicit = env["HARNESS_AUTO_UPGRADE"];
791
+ if (explicit !== void 0 && explicit.trim() !== "") return truthy(explicit);
792
+ return true;
793
+ }
794
+
373
795
  // src/cli.ts
374
796
  function promptLine(question) {
375
797
  const readline = createInterface2({ input: process.stdin, output: process.stderr });
@@ -474,7 +896,10 @@ async function fail(error, hostNotice) {
474
896
  return exitWith(3);
475
897
  }
476
898
  if (error instanceof UpgradeRequiredError) {
477
- process.stderr.write(hostNotice + `harness: ${clean(error.message)} (HTTP 426)
899
+ const healed = selfHeal();
900
+ if (healed.kind === "upgraded") return exitWith(healed.code);
901
+ const why = healed.kind === "failed" ? ` (tried to upgrade: ${clean(healed.reason)})` : "";
902
+ process.stderr.write(hostNotice + `harness: ${clean(error.message)} (HTTP 426)${why}
478
903
  `);
479
904
  return exitWith(1);
480
905
  }
@@ -698,7 +1123,7 @@ program.command("upload").description("upload a file; prints markdown to referen
698
1123
  });
699
1124
  });
700
1125
  program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
701
- const { startMcpServer } = await import("./server-H3U2Y6R7.js");
1126
+ const { startMcpServer } = await import("./server-TK2KFAOQ.js");
702
1127
  await startMcpServer();
703
1128
  });
704
1129
  program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option("--timeout <ms>", "how long to wait for the browser callback", integer).option("--json", "emit JSON").action(async (opts) => {
@@ -749,6 +1174,15 @@ async function finishAuth(result, json2) {
749
1174
  }
750
1175
  var argv = process.argv.slice();
751
1176
  if (argv[2] === "--version" || argv[2] === "-V") argv[2] = "version";
1177
+ if (argv[2] === UPDATE_CHECK_COMMAND) {
1178
+ await performUpdateCheck();
1179
+ await exitWith(0);
1180
+ }
1181
+ try {
1182
+ const notice = updateCheck();
1183
+ if (notice !== null) process.stderr.write(notice);
1184
+ } catch {
1185
+ }
752
1186
  if (argv.length <= 2) {
753
1187
  program.outputHelp({ error: true });
754
1188
  await exitWith(2);
@@ -38,7 +38,7 @@ import {
38
38
  upload,
39
39
  userList,
40
40
  workflowStates
41
- } from "./chunk-JMGBMBN6.js";
41
+ } from "./chunk-3WR6YFU6.js";
42
42
 
43
43
  // src/mcp/server.ts
44
44
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allocator-one/harness-cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "CLI and MCP server for Harness, allocator.one's issue tracker",
5
5
  "type": "module",
6
6
  "license": "MIT",