@topolo/cli 0.13.7 → 0.13.8

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.
Files changed (2) hide show
  1. package/dist/index.js +173 -24
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -158,7 +158,7 @@ import { platform as platform2 } from "os";
158
158
  import { createInterface } from "readline/promises";
159
159
  import { stdin as input } from "process";
160
160
 
161
- // node_modules/.pnpm/@topolo+sdk@0.23.19/node_modules/@topolo/sdk/dist/index.js
161
+ // node_modules/.pnpm/@topolo+sdk@0.23.20/node_modules/@topolo/sdk/dist/index.js
162
162
  function record(value) {
163
163
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
164
164
  }
@@ -3768,10 +3768,11 @@ async function pollUntilApproved(params) {
3768
3768
  throw new Error("Device code expired. Run `topolo auth login` again.");
3769
3769
  }
3770
3770
 
3771
- // node_modules/.pnpm/@topolo+sdk@0.23.19/node_modules/@topolo/sdk/dist/node/credential-store.js
3772
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2, rename as rename2, link } from "fs/promises";
3771
+ // node_modules/.pnpm/@topolo+sdk@0.23.20/node_modules/@topolo/sdk/dist/node/credential-store.js
3772
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2 } from "fs/promises";
3773
3773
  import { existsSync as existsSync2, mkdirSync, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync, linkSync } from "fs";
3774
3774
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
3775
+ import { constants, fstatSync, readSync } from "fs";
3775
3776
  import { homedir as homedir2, platform } from "os";
3776
3777
  import { dirname as dirname2, join as join3 } from "path";
3777
3778
  import { createHash, randomUUID } from "crypto";
@@ -4637,6 +4638,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
4637
4638
  var REFRESH_SKEW_SECONDS = 60;
4638
4639
  var CONFIG_LOCK_RETRY_MS = 25;
4639
4640
  var CONFIG_LOCK_TIMEOUT_MS = 5e3;
4641
+ var CONFIG_LOCK_DEAD_OWNER_MIN_AGE_MS = 2e3;
4640
4642
  var ConfigOwnershipError = class extends TopoloSdkError2 {
4641
4643
  constructor() {
4642
4644
  super("state_conflict", "Credential configuration ownership cannot be verified. Retain the unresolved operation instead of starting another renewal.");
@@ -4651,32 +4653,145 @@ function lockEntryExists(path) {
4651
4653
  throw new ConfigOwnershipError();
4652
4654
  }
4653
4655
  }
4656
+ function lockIdentity(entry) {
4657
+ return [entry.dev, entry.ino, entry.uid, entry.mode, entry.nlink, entry.size, entry.mtimeMs, entry.ctimeMs];
4658
+ }
4659
+ function privateLockDirectory(path) {
4660
+ const entry = lstatSync(path);
4661
+ if (typeof process.getuid !== "function" || !entry.isDirectory() || entry.isSymbolicLink() || entry.uid !== process.getuid() || (entry.mode & 511) !== 448) throw new ConfigOwnershipError();
4662
+ return entry;
4663
+ }
4664
+ function readPrivateLockMarker(path) {
4665
+ const before = lstatSync(path);
4666
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > 512 || before.uid !== process.getuid?.() || (before.mode & 511) !== 384) throw new ConfigOwnershipError();
4667
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
4668
+ try {
4669
+ if (JSON.stringify(lockIdentity(fstatSync(fd))) !== JSON.stringify(lockIdentity(before))) throw new ConfigOwnershipError();
4670
+ const bytes = Buffer.alloc(513);
4671
+ const count = readSync(fd, bytes, 0, bytes.length, 0);
4672
+ if (count !== before.size || count > 512 || JSON.stringify(lockIdentity(fstatSync(fd))) !== JSON.stringify(lockIdentity(before)) || JSON.stringify(lockIdentity(lstatSync(path))) !== JSON.stringify(lockIdentity(before))) throw new ConfigOwnershipError();
4673
+ return bytes.subarray(0, count).toString("utf8");
4674
+ } finally {
4675
+ closeSync(fd);
4676
+ }
4677
+ }
4678
+ var OWNER_MARKER_NAME = /^owner-[a-f0-9-]{36}\.json$/;
4679
+ function readOwnerMarker(lockPath, name) {
4680
+ const bytes = readPrivateLockMarker(join3(lockPath, name));
4681
+ const owner = JSON.parse(bytes);
4682
+ if (owner.version !== 1 || Object.keys(owner).sort().join(",") !== "identity,pid,version" || name !== "owner-" + owner.identity + ".json" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) throw new ConfigOwnershipError();
4683
+ return { bytes, pid: owner.pid };
4684
+ }
4685
+ function writePrivateLockRecord(path, bytes) {
4686
+ const fd = openSync(path, "wx", 384);
4687
+ try {
4688
+ writeFileSync(fd, bytes);
4689
+ fsyncSync(fd);
4690
+ } finally {
4691
+ closeSync(fd);
4692
+ }
4693
+ }
4694
+ function deadOwnerMarker(lockPath, ignoreOwnFence = false) {
4695
+ try {
4696
+ if (!ignoreOwnFence && lockEntryExists(lockPath + ".reconcile")) return null;
4697
+ const directory = privateLockDirectory(lockPath);
4698
+ const entries = readdirSync(lockPath);
4699
+ if (entries.length !== 1 || !OWNER_MARKER_NAME.test(entries[0])) return null;
4700
+ const name = entries[0];
4701
+ const { bytes, pid } = readOwnerMarker(lockPath, name);
4702
+ if (Date.now() - lstatSync(join3(lockPath, name)).mtimeMs < CONFIG_LOCK_DEAD_OWNER_MIN_AGE_MS) return null;
4703
+ try {
4704
+ process.kill(pid, 0);
4705
+ return null;
4706
+ } catch (error) {
4707
+ if (error.code !== "ESRCH") return null;
4708
+ }
4709
+ return { directory, name, bytes, pid };
4710
+ } catch {
4711
+ return null;
4712
+ }
4713
+ }
4714
+ function reclaimDeadOwnerLock(lockPath, markerName, marker) {
4715
+ const observed = deadOwnerMarker(lockPath);
4716
+ if (!observed) return false;
4717
+ const fence = lockPath + ".reconcile";
4718
+ try {
4719
+ mkdirSync(fence, { mode: 448 });
4720
+ } catch {
4721
+ return false;
4722
+ }
4723
+ const fenceMarker = join3(fence, markerName);
4724
+ const quarantine = join3(dirname2(lockPath), ".credential-reclaim-" + randomUUID2());
4725
+ let captured;
4726
+ let reclaimed = false;
4727
+ try {
4728
+ writePrivateLockRecord(fenceMarker, marker);
4729
+ const current = deadOwnerMarker(lockPath, true);
4730
+ if (!current || current.name !== observed.name || current.bytes !== observed.bytes || current.directory.dev !== observed.directory.dev || current.directory.ino !== observed.directory.ino) return false;
4731
+ mkdirSync(quarantine, { mode: 448 });
4732
+ writePrivateLockRecord(join3(quarantine, "receipt.json"), JSON.stringify({
4733
+ schemaVersion: 1,
4734
+ disposition: "DEAD_OWNER_RECLAIMED",
4735
+ lockPath,
4736
+ deadOwnerMarker: current.name,
4737
+ deadOwnerPid: current.pid,
4738
+ deadOwnerEvidence: "ESRCH",
4739
+ reclaimedByPid: process.pid,
4740
+ reclaimedAt: (/* @__PURE__ */ new Date()).toISOString(),
4741
+ configCredentialOrOAuthEffectsByThisReclaim: "none"
4742
+ }, null, 2));
4743
+ captured = join3(quarantine, current.name);
4744
+ renameSync(join3(lockPath, current.name), captured);
4745
+ const directory = privateLockDirectory(lockPath);
4746
+ if (directory.dev !== observed.directory.dev || directory.ino !== observed.directory.ino || readdirSync(lockPath).length) throw new ConfigOwnershipError();
4747
+ writePrivateLockRecord(join3(lockPath, markerName), marker);
4748
+ reclaimed = true;
4749
+ } catch {
4750
+ if (captured && !reclaimed) {
4751
+ try {
4752
+ linkSync(captured, join3(lockPath, observed.name));
4753
+ } catch {
4754
+ }
4755
+ }
4756
+ } finally {
4757
+ try {
4758
+ if (readPrivateLockMarker(fenceMarker) !== marker || readdirSync(fence).length !== 1) throw new ConfigOwnershipError();
4759
+ unlinkSync(fenceMarker);
4760
+ rmdirSync(fence);
4761
+ } catch {
4762
+ reclaimed = false;
4763
+ }
4764
+ }
4765
+ return reclaimed;
4766
+ }
4767
+ var RELEASE_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
4654
4768
  async function withConfigWriteLock(operation) {
4655
4769
  const lockPath = `${configPath()}.lock`;
4656
4770
  await mkdir2(dirname2(lockPath), { recursive: true });
4657
4771
  const startedAt = performance.now();
4772
+ const identity = randomUUID2();
4773
+ const markerName = "owner-" + identity + ".json";
4774
+ const markerPath = join3(lockPath, markerName);
4775
+ const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
4658
4776
  while (true) {
4659
4777
  try {
4660
4778
  if (lockEntryExists(lockPath + ".reconcile")) throw Object.assign(new Error(), { code: "EEXIST" });
4661
- await mkdir2(lockPath, { mode: 448 });
4779
+ mkdirSync(lockPath, { mode: 448 });
4662
4780
  if (lockEntryExists(lockPath + ".reconcile")) {
4663
4781
  throw new ConfigOwnershipError();
4664
4782
  }
4783
+ writeFileSync(markerPath, marker, { flag: "wx", mode: 384 });
4665
4784
  break;
4666
4785
  } catch (error) {
4667
4786
  if (error.code !== "EEXIST") throw error;
4787
+ if (reclaimDeadOwnerLock(lockPath, markerName, marker)) break;
4668
4788
  if (performance.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
4669
4789
  throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
4670
4790
  }
4671
4791
  await new Promise((resolve22) => setTimeout(resolve22, CONFIG_LOCK_RETRY_MS));
4672
4792
  }
4673
4793
  }
4674
- const directory = await lstat(lockPath);
4675
- const identity = randomUUID2();
4676
- const markerName = "owner-" + identity + ".json";
4677
- const markerPath = join3(lockPath, markerName);
4678
- const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
4679
- await writeFile2(markerPath, marker, { flag: "wx", mode: 384 });
4794
+ const directory = lstatSync(lockPath);
4680
4795
  const assertDirectory = async () => {
4681
4796
  const current = await lstat(lockPath);
4682
4797
  if (lockEntryExists(lockPath + ".reconcile") || !current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
@@ -4700,10 +4815,14 @@ async function withConfigWriteLock(operation) {
4700
4815
  throw new ConfigOwnershipError();
4701
4816
  };
4702
4817
  const assertOwnedSync = () => {
4703
- assertDirectorySync();
4704
- const entries = readdirSync(lockPath);
4705
- const entry = lstatSync(markerPath);
4706
- if (entries.length !== 1 || entries[0] !== markerName || !entry.isFile() || entry.isSymbolicLink() || readFileSync2(markerPath, "utf8") !== marker) throw new ConfigOwnershipError();
4818
+ try {
4819
+ assertDirectorySync();
4820
+ const entries = readdirSync(lockPath);
4821
+ const entry = lstatSync(markerPath);
4822
+ if (entries.length !== 1 || entries[0] !== markerName || !entry.isFile() || entry.isSymbolicLink() || readFileSync2(markerPath, "utf8") !== marker) throw new ConfigOwnershipError();
4823
+ } catch {
4824
+ throw new ConfigOwnershipError();
4825
+ }
4707
4826
  };
4708
4827
  const persist = async (update) => {
4709
4828
  const path = configPath();
@@ -4732,14 +4851,14 @@ async function withConfigWriteLock(operation) {
4732
4851
  if (temporaryOwned) unlinkSync(temporary);
4733
4852
  }
4734
4853
  };
4735
- const release = async () => {
4736
- await assertOwned();
4854
+ const release = () => {
4855
+ assertOwnedSync();
4737
4856
  const quarantine = join3(dirname2(lockPath), ".credential-release-" + randomUUID2());
4738
- await mkdir2(quarantine, { mode: 448 });
4857
+ mkdirSync(quarantine, { mode: 448 });
4739
4858
  const captured = join3(quarantine, markerName);
4740
4859
  try {
4741
- await assertOwned();
4742
- await rename2(markerPath, captured);
4860
+ assertOwnedSync();
4861
+ renameSync(markerPath, captured);
4743
4862
  assertDirectorySync();
4744
4863
  const entry = lstatSync(captured);
4745
4864
  if (!entry.isFile() || entry.isSymbolicLink() || readFileSync2(captured, "utf8") !== marker || readdirSync(lockPath).length) throw new ConfigOwnershipError();
@@ -4751,20 +4870,40 @@ async function withConfigWriteLock(operation) {
4751
4870
  } catch {
4752
4871
  if (existsSync2(captured)) {
4753
4872
  try {
4754
- await link(captured, markerPath);
4873
+ linkSync(captured, markerPath);
4755
4874
  } catch {
4756
4875
  }
4757
4876
  }
4758
4877
  throw new ConfigOwnershipError();
4759
4878
  }
4760
4879
  };
4880
+ let held = true;
4881
+ const releaseOnce = () => {
4882
+ if (!held) return;
4883
+ held = false;
4884
+ process.off("exit", onExit);
4885
+ for (const signal of RELEASE_SIGNALS) process.off(signal, onSignal);
4886
+ release();
4887
+ };
4888
+ const onExit = () => {
4889
+ try {
4890
+ releaseOnce();
4891
+ } catch {
4892
+ }
4893
+ };
4894
+ const onSignal = (signal) => {
4895
+ onExit();
4896
+ if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
4897
+ };
4898
+ process.on("exit", onExit);
4899
+ for (const signal of RELEASE_SIGNALS) process.on(signal, onSignal);
4761
4900
  try {
4762
4901
  await assertOwned();
4763
4902
  const result = await operation(assertOwned, persist);
4764
4903
  await assertOwned();
4765
4904
  return result;
4766
4905
  } finally {
4767
- await release();
4906
+ releaseOnce();
4768
4907
  }
4769
4908
  }
4770
4909
  function renewalFailure(error) {
@@ -6657,6 +6796,7 @@ async function readGooseRegistrations(path) {
6657
6796
  continue;
6658
6797
  }
6659
6798
  if (inExtensions && /^\S/.test(rawLine) && rawLine.trim()) {
6799
+ if (current?.command) registrations.push(current);
6660
6800
  inExtensions = false;
6661
6801
  inTopolo = false;
6662
6802
  current = void 0;
@@ -6664,6 +6804,7 @@ async function readGooseRegistrations(path) {
6664
6804
  if (!inExtensions) continue;
6665
6805
  const extension = rawLine.match(/^ ([A-Za-z0-9_-]+):\s*$/);
6666
6806
  if (extension) {
6807
+ if (current?.command) registrations.push(current);
6667
6808
  inTopolo = true;
6668
6809
  current = isPublicRegistrationName(extension[1]) ? { name: extension[1], command: "", args: [], env: {} } : void 0;
6669
6810
  continue;
@@ -6841,7 +6982,7 @@ function message(error) {
6841
6982
 
6842
6983
  // src/commands/agent-config.ts
6843
6984
  import { existsSync as existsSync5 } from "fs";
6844
- import { mkdir as mkdir3, readFile as readFile5, rename as rename3, writeFile as writeFile3 } from "fs/promises";
6985
+ import { mkdir as mkdir3, readFile as readFile5, rename as rename2, writeFile as writeFile3 } from "fs/promises";
6845
6986
  import { dirname as dirname5 } from "path";
6846
6987
  async function installAgentSkill(agent, source, homeDir, projectDir) {
6847
6988
  const path = agentSkillPath(agent, homeDir, projectDir);
@@ -6877,6 +7018,7 @@ async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
6877
7018
  function registrationEnvironment(agent, registration) {
6878
7019
  return {
6879
7020
  TOPOLO_AGENT_NAME: AGENT_NAME_FOR[agent],
7021
+ ...registration.configPath ? { TOPOLO_CONFIG_PATH: registration.configPath } : {},
6880
7022
  ...registration.installationId ? { TOPOLO_AGENT_INSTALLATION_ID: registration.installationId } : {},
6881
7023
  ...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
6882
7024
  };
@@ -7008,7 +7150,7 @@ async function atomicWrite(path, contents) {
7008
7150
  await mkdir3(dirname5(path), { recursive: true });
7009
7151
  const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
7010
7152
  await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
7011
- await rename3(temporaryPath, path);
7153
+ await rename2(temporaryPath, path);
7012
7154
  }
7013
7155
  function withoutMcpServerSections(text2, name) {
7014
7156
  const section = "[mcp_servers." + name + "]";
@@ -7072,7 +7214,14 @@ function resolveMcpRegistration(options) {
7072
7214
  if (installationId && /[\r\n]/.test(installationId)) {
7073
7215
  throw new Error("Installation id must be a single-line identifier.");
7074
7216
  }
7075
- return { name, ...context ? { context } : {}, environment, ...installationId ? { installationId } : {} };
7217
+ const configPath2 = process.env.TOPOLO_CONFIG_PATH;
7218
+ return {
7219
+ name,
7220
+ ...context ? { context } : {},
7221
+ environment,
7222
+ ...installationId ? { installationId } : {},
7223
+ ...configPath2 ? { configPath: configPath2 } : {}
7224
+ };
7076
7225
  }
7077
7226
  async function installForAgent(agent, sourceDir, mcp, opts) {
7078
7227
  const source = await readFile6(join7(sourceDir, "topolo", "SKILL.md"), "utf8");
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
3
  "packageManager": "pnpm@10.17.1",
4
- "version": "0.13.7",
5
- "topoloPublishedVersion": "0.13.7",
4
+ "version": "0.13.8",
5
+ "topoloPublishedVersion": "0.13.8",
6
6
  "description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
7
7
  "license": "UNLICENSED",
8
8
  "author": "Topolo",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "^1.0.0",
34
- "@topolo/mcp": "0.14.24",
35
- "@topolo/sdk": "0.23.19",
34
+ "@topolo/mcp": "0.14.25",
35
+ "@topolo/sdk": "0.23.20",
36
36
  "commander": "^12.1.0",
37
37
  "undici": "6.29.0"
38
38
  },