@topolo/cli 0.13.22 → 0.13.23
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/README.md +8 -0
- package/dist/index.js +392 -151
- package/package.json +2 -2
- package/postinstall.mjs +6 -6
package/README.md
CHANGED
|
@@ -58,8 +58,16 @@ topolo auth login # OAuth device flow — no client id needed; appro
|
|
|
58
58
|
topolo auth login --api-key topo_live_... # or store an API key directly (headless/service)
|
|
59
59
|
topolo auth status # shows which source is active
|
|
60
60
|
topolo auth logout # clears the stored credential
|
|
61
|
+
topolo auth lock # shows who holds the sign-in renewal lock and how to recover
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
If every command fails with `state_conflict` after about 15 seconds, the lock
|
|
65
|
+
that serializes sign-in renewal is held or was left behind. `topolo auth lock`
|
|
66
|
+
shows its state and the next step; an empty lock left by an interrupted writer
|
|
67
|
+
is recovered with `topolo auth lock --reconcile <generation>`, which keeps the
|
|
68
|
+
old lock in a private quarantine as evidence. Credentials are not read or
|
|
69
|
+
changed.
|
|
70
|
+
|
|
63
71
|
`topolo auth login` uses a **built-in first-party OAuth client per environment**,
|
|
64
72
|
so any Topolo user can sign in without registering a client — what they can do
|
|
65
73
|
is decided by their permissions. `--client-id doac_...` (or
|
package/dist/index.js
CHANGED
|
@@ -308,7 +308,7 @@ import { Command, Help } from "commander";
|
|
|
308
308
|
import { createInterface } from "readline/promises";
|
|
309
309
|
import { stdin as input } from "process";
|
|
310
310
|
|
|
311
|
-
// node_modules/@topolo/sdk/dist/index.js
|
|
311
|
+
// node_modules/.pnpm/@topolo+sdk@0.23.25/node_modules/@topolo/sdk/dist/index.js
|
|
312
312
|
function record(value) {
|
|
313
313
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
314
314
|
}
|
|
@@ -1968,7 +1968,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1968
1968
|
} catch (error) {
|
|
1969
1969
|
const transportFailure = !(error instanceof TopoloSdkError) && !opts.signal?.aborted;
|
|
1970
1970
|
if (!transportFailure || attempt === maxAttempts) throw error;
|
|
1971
|
-
await new Promise((
|
|
1971
|
+
await new Promise((resolve6) => setTimeout(resolve6, 250));
|
|
1972
1972
|
}
|
|
1973
1973
|
}
|
|
1974
1974
|
throw new TopoloSdkError("network_error", "Topolo read retry loop exhausted.");
|
|
@@ -1991,7 +1991,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1991
1991
|
const delayMs = header === void 0 ? RATE_LIMIT_READ_DEFAULT_DELAY_MS * (retry + 1) : (seconds ?? Infinity) * 1e3;
|
|
1992
1992
|
if (delayMs > RATE_LIMIT_READ_RETRY_MAX_DELAY_MS || opts.signal?.aborted) break;
|
|
1993
1993
|
await res.body?.cancel().catch(() => void 0);
|
|
1994
|
-
await new Promise((
|
|
1994
|
+
await new Promise((resolve6) => setTimeout(resolve6, delayMs));
|
|
1995
1995
|
res = await fetchWithReadRetry();
|
|
1996
1996
|
}
|
|
1997
1997
|
} catch (err) {
|
|
@@ -3989,7 +3989,7 @@ function createTopolo(options) {
|
|
|
3989
3989
|
async function pollUntilApproved(params) {
|
|
3990
3990
|
let interval = Math.max(params.intervalSeconds ?? 5, 1);
|
|
3991
3991
|
while (Date.now() < params.expiresAtMs) {
|
|
3992
|
-
await new Promise((
|
|
3992
|
+
await new Promise((resolve6) => setTimeout(resolve6, Math.min(interval * 1e3, params.expiresAtMs - Date.now())));
|
|
3993
3993
|
if (Date.now() >= params.expiresAtMs) break;
|
|
3994
3994
|
try {
|
|
3995
3995
|
return await params.oauth.pollDeviceToken({
|
|
@@ -4017,32 +4017,16 @@ async function pollUntilApproved(params) {
|
|
|
4017
4017
|
throw new Error("Device code expired. Run `topolo auth login` again.");
|
|
4018
4018
|
}
|
|
4019
4019
|
|
|
4020
|
-
// src/
|
|
4021
|
-
import {
|
|
4022
|
-
import { platform } from "os";
|
|
4023
|
-
function tryOpenBrowser(url) {
|
|
4024
|
-
try {
|
|
4025
|
-
const os = platform();
|
|
4026
|
-
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
4027
|
-
const args = os === "win32" ? ["", url] : [url];
|
|
4028
|
-
const child = spawn(cmd, args, {
|
|
4029
|
-
detached: true,
|
|
4030
|
-
stdio: "ignore",
|
|
4031
|
-
...os === "win32" ? { shell: true } : {}
|
|
4032
|
-
});
|
|
4033
|
-
child.unref();
|
|
4034
|
-
return true;
|
|
4035
|
-
} catch {
|
|
4036
|
-
return false;
|
|
4037
|
-
}
|
|
4038
|
-
}
|
|
4020
|
+
// src/commands/auth-lock.ts
|
|
4021
|
+
import { createHash as createHash3 } from "crypto";
|
|
4039
4022
|
|
|
4040
|
-
// node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
4023
|
+
// node_modules/.pnpm/@topolo+sdk@0.23.25/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
4041
4024
|
import { mkdir as mkdir2, readFile as readFile2, chmod as chmod2, lstat, readdir as readdir2 } from "fs/promises";
|
|
4042
4025
|
import { existsSync as existsSync2, mkdirSync, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync, linkSync } from "fs";
|
|
4043
4026
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
4044
4027
|
import { constants, fstatSync, readSync } from "fs";
|
|
4045
|
-
import {
|
|
4028
|
+
import { resolve as resolve2 } from "path";
|
|
4029
|
+
import { homedir as homedir2, platform } from "os";
|
|
4046
4030
|
import { dirname as dirname2, join as join3 } from "path";
|
|
4047
4031
|
import { createHash, randomUUID } from "crypto";
|
|
4048
4032
|
import { join as join2 } from "path";
|
|
@@ -4810,7 +4794,7 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
|
|
|
4810
4794
|
function configPath() {
|
|
4811
4795
|
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
4812
4796
|
if (override) return override;
|
|
4813
|
-
const base = process.env.XDG_CONFIG_HOME || (
|
|
4797
|
+
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join3(homedir2(), "AppData", "Roaming") : join3(homedir2(), ".config"));
|
|
4814
4798
|
return join3(base, "topolo", "config.json");
|
|
4815
4799
|
}
|
|
4816
4800
|
async function readConfig() {
|
|
@@ -4829,7 +4813,7 @@ async function writeConfig(config) {
|
|
|
4829
4813
|
await withConfigWriteLock(async (_assertOwned, persist) => {
|
|
4830
4814
|
await persist((current) => protectStoredGrants(config, current), { allowCreate: true });
|
|
4831
4815
|
});
|
|
4832
|
-
if (
|
|
4816
|
+
if (platform() !== "win32") {
|
|
4833
4817
|
try {
|
|
4834
4818
|
await chmod2(path, 384);
|
|
4835
4819
|
} catch {
|
|
@@ -5126,6 +5110,61 @@ function readOwnerMarker(lockPath, name) {
|
|
|
5126
5110
|
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();
|
|
5127
5111
|
return { bytes, pid: owner.pid };
|
|
5128
5112
|
}
|
|
5113
|
+
function inspectConfigLockAt(path, ignoreOwnFence = false) {
|
|
5114
|
+
const lockPath = path + ".lock";
|
|
5115
|
+
let state = "UNKNOWN";
|
|
5116
|
+
let reason = "UNVERIFIED_CUSTODY";
|
|
5117
|
+
let witness = "unverified";
|
|
5118
|
+
try {
|
|
5119
|
+
const parent = lstatSync(dirname2(lockPath));
|
|
5120
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== process.getuid?.() || parent.mode & 18) throw new ConfigOwnershipError();
|
|
5121
|
+
const parentIdentity = [parent.dev, parent.ino, parent.uid, parent.mode];
|
|
5122
|
+
if (!existsSync2(lockPath)) {
|
|
5123
|
+
try {
|
|
5124
|
+
lstatSync(lockPath);
|
|
5125
|
+
throw new ConfigOwnershipError();
|
|
5126
|
+
} catch (error) {
|
|
5127
|
+
if (error.code !== "ENOENT") throw error;
|
|
5128
|
+
}
|
|
5129
|
+
state = "FREE";
|
|
5130
|
+
reason = "ABSENT";
|
|
5131
|
+
witness = [parentIdentity, "absent"];
|
|
5132
|
+
} else {
|
|
5133
|
+
const directory = privateLockDirectory(lockPath);
|
|
5134
|
+
const entries = readdirSync(lockPath).sort();
|
|
5135
|
+
witness = [parentIdentity, lockIdentity(directory), entries];
|
|
5136
|
+
if (entries.length === 0) reason = "EMPTY_OWNER_UNKNOWN";
|
|
5137
|
+
else if (entries.length === 1 && OWNER_MARKER_NAME.test(entries[0])) {
|
|
5138
|
+
const { bytes, pid } = readOwnerMarker(lockPath, entries[0]);
|
|
5139
|
+
witness = [witness, createHash2("sha256").update(bytes).digest("hex")];
|
|
5140
|
+
try {
|
|
5141
|
+
process.kill(pid, 0);
|
|
5142
|
+
state = "LIVE";
|
|
5143
|
+
reason = "OWNER_PID_EXISTS";
|
|
5144
|
+
} catch {
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
if (JSON.stringify(lockIdentity(lstatSync(lockPath))) !== JSON.stringify(lockIdentity(directory)) || JSON.stringify(readdirSync(lockPath).sort()) !== JSON.stringify(entries)) throw new ConfigOwnershipError();
|
|
5148
|
+
}
|
|
5149
|
+
if (!ignoreOwnFence && lockEntryExists(lockPath + ".reconcile")) {
|
|
5150
|
+
state = "UNKNOWN";
|
|
5151
|
+
reason = "RECONCILIATION_IN_PROGRESS";
|
|
5152
|
+
}
|
|
5153
|
+
} catch {
|
|
5154
|
+
state = "UNKNOWN";
|
|
5155
|
+
reason = "UNVERIFIED_CUSTODY";
|
|
5156
|
+
}
|
|
5157
|
+
return {
|
|
5158
|
+
configPath: path,
|
|
5159
|
+
lockPath,
|
|
5160
|
+
state,
|
|
5161
|
+
reason,
|
|
5162
|
+
generation: createHash2("sha256").update(JSON.stringify([path, witness])).digest("hex")
|
|
5163
|
+
};
|
|
5164
|
+
}
|
|
5165
|
+
async function inspectConfigLock() {
|
|
5166
|
+
return inspectConfigLockAt(resolve2(configPath()));
|
|
5167
|
+
}
|
|
5129
5168
|
function writePrivateLockRecord(path, bytes) {
|
|
5130
5169
|
const fd = openSync(path, "wx", 384);
|
|
5131
5170
|
try {
|
|
@@ -5135,6 +5174,107 @@ function writePrivateLockRecord(path, bytes) {
|
|
|
5135
5174
|
closeSync(fd);
|
|
5136
5175
|
}
|
|
5137
5176
|
}
|
|
5177
|
+
async function reconcileConfigLock(input2) {
|
|
5178
|
+
const path = resolve2(configPath());
|
|
5179
|
+
const before = inspectConfigLockAt(path);
|
|
5180
|
+
if (!input2 || typeof input2 !== "object") throw new ConfigOwnershipError();
|
|
5181
|
+
const inputFields = Object.getOwnPropertyDescriptors(input2);
|
|
5182
|
+
if (Reflect.ownKeys(inputFields).length !== 2 || typeof inputFields.expectedGeneration?.value !== "string" || !inputFields.review || !Object.prototype.hasOwnProperty.call(inputFields.review, "value")) throw new ConfigOwnershipError();
|
|
5183
|
+
const supplied = inputFields.review.value;
|
|
5184
|
+
if (!supplied || typeof supplied !== "object") throw new ConfigOwnershipError();
|
|
5185
|
+
const fields = Object.getOwnPropertyDescriptors(supplied);
|
|
5186
|
+
const names = ["kind", "configPath", "generation", "quiescenceEvidenceSha256", "reviewedBy", "expiresAt"];
|
|
5187
|
+
if (Reflect.ownKeys(fields).length !== names.length || names.some((name) => !fields[name] || !Object.prototype.hasOwnProperty.call(fields[name], "value") || typeof fields[name].value !== "string")) throw new ConfigOwnershipError();
|
|
5188
|
+
const expectedGeneration = inputFields.expectedGeneration.value;
|
|
5189
|
+
const review = Object.freeze({
|
|
5190
|
+
kind: fields.kind.value,
|
|
5191
|
+
configPath: fields.configPath.value,
|
|
5192
|
+
generation: fields.generation.value,
|
|
5193
|
+
quiescenceEvidenceSha256: fields.quiescenceEvidenceSha256.value,
|
|
5194
|
+
reviewedBy: fields.reviewedBy.value,
|
|
5195
|
+
expiresAt: fields.expiresAt.value
|
|
5196
|
+
});
|
|
5197
|
+
const expiry = Date.parse(review.expiresAt);
|
|
5198
|
+
if (review.kind !== "quiescent-empty-quarantine" || review.configPath !== path || review.generation !== expectedGeneration || before.generation !== expectedGeneration || !/^[a-f0-9]{64}$/.test(review.quiescenceEvidenceSha256) || typeof review.reviewedBy !== "string" || !/^[a-zA-Z0-9_.:-]{1,80}$/.test(review.reviewedBy) || !Number.isFinite(expiry) || expiry <= Date.now() || expiry > Date.now() + 15 * 6e4 || before.state !== "UNKNOWN" || before.reason !== "EMPTY_OWNER_UNKNOWN") throw new ConfigOwnershipError();
|
|
5199
|
+
const lockPath = before.lockPath;
|
|
5200
|
+
const fence = lockPath + ".reconcile";
|
|
5201
|
+
const identity = randomUUID2();
|
|
5202
|
+
const markerName = "owner-" + identity + ".json";
|
|
5203
|
+
const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
|
|
5204
|
+
const quarantine = join3(dirname2(lockPath), ".credential-reconcile-" + identity);
|
|
5205
|
+
const captured = join3(quarantine, "lock");
|
|
5206
|
+
const receiptPath = join3(quarantine, "receipt.json");
|
|
5207
|
+
let capturedLock = false;
|
|
5208
|
+
let preserveFence = false;
|
|
5209
|
+
try {
|
|
5210
|
+
mkdirSync(fence, { mode: 448 });
|
|
5211
|
+
} catch {
|
|
5212
|
+
throw new ConfigOwnershipError();
|
|
5213
|
+
}
|
|
5214
|
+
const fenceDirectory = privateLockDirectory(fence);
|
|
5215
|
+
const fenceMarker = join3(fence, markerName);
|
|
5216
|
+
const assertFence = () => {
|
|
5217
|
+
const entry = privateLockDirectory(fence);
|
|
5218
|
+
if (entry.dev !== fenceDirectory.dev || entry.ino !== fenceDirectory.ino || readdirSync(fence).length !== 1 || readdirSync(fence)[0] !== markerName || readPrivateLockMarker(fenceMarker) !== marker) throw new ConfigOwnershipError();
|
|
5219
|
+
};
|
|
5220
|
+
try {
|
|
5221
|
+
writePrivateLockRecord(fenceMarker, marker);
|
|
5222
|
+
assertFence();
|
|
5223
|
+
const current = inspectConfigLockAt(path, true);
|
|
5224
|
+
if (current.state !== "UNKNOWN" || current.reason !== "EMPTY_OWNER_UNKNOWN" || current.generation !== before.generation || Date.now() >= expiry) throw new ConfigOwnershipError();
|
|
5225
|
+
const directory = privateLockDirectory(lockPath);
|
|
5226
|
+
mkdirSync(quarantine, { mode: 448 });
|
|
5227
|
+
writePrivateLockRecord(receiptPath, JSON.stringify({
|
|
5228
|
+
schemaVersion: 1,
|
|
5229
|
+
disposition: "PENDING_CAPTURE",
|
|
5230
|
+
classificationBefore: "UNKNOWN",
|
|
5231
|
+
before,
|
|
5232
|
+
review,
|
|
5233
|
+
directoryIdentity: [directory.dev, directory.ino, directory.uid, directory.mode],
|
|
5234
|
+
quiescence: "caller-reviewed; legacy and bare writers not globally enforced by SDK",
|
|
5235
|
+
configCredentialOrOAuthEffectsByThisAPI: "none"
|
|
5236
|
+
}, null, 2));
|
|
5237
|
+
assertFence();
|
|
5238
|
+
if (inspectConfigLockAt(path, true).generation !== before.generation || Date.now() >= expiry) throw new ConfigOwnershipError();
|
|
5239
|
+
renameSync(lockPath, captured);
|
|
5240
|
+
capturedLock = true;
|
|
5241
|
+
const entry = privateLockDirectory(captured);
|
|
5242
|
+
if (entry.dev !== directory.dev || entry.ino !== directory.ino || readdirSync(captured).length || lockEntryExists(lockPath) || Date.now() >= expiry) throw new ConfigOwnershipError();
|
|
5243
|
+
assertFence();
|
|
5244
|
+
writePrivateLockRecord(join3(quarantine, "outcome.json"), JSON.stringify({
|
|
5245
|
+
disposition: "CAPTURE_VERIFIED_PENDING_FENCE_CLOSURE",
|
|
5246
|
+
classificationBefore: "UNKNOWN",
|
|
5247
|
+
fenceClosureVerified: false,
|
|
5248
|
+
noDeathOrPriorRemoteEffectResolutionInferred: true
|
|
5249
|
+
}));
|
|
5250
|
+
} catch (error) {
|
|
5251
|
+
if (!capturedLock) throw error instanceof ConfigOwnershipError ? error : new ConfigOwnershipError();
|
|
5252
|
+
preserveFence = true;
|
|
5253
|
+
return { outcome: "UNRESOLVED_CAPTURE", before, quarantinePath: quarantine, receiptPath, fenceRetained: true };
|
|
5254
|
+
} finally {
|
|
5255
|
+
if (!preserveFence) {
|
|
5256
|
+
try {
|
|
5257
|
+
assertFence();
|
|
5258
|
+
const capturedMarker = join3(quarantine, markerName);
|
|
5259
|
+
if (!lockEntryExists(quarantine)) mkdirSync(quarantine, { mode: 448 });
|
|
5260
|
+
renameSync(fenceMarker, capturedMarker);
|
|
5261
|
+
const entry = privateLockDirectory(fence);
|
|
5262
|
+
if (entry.dev !== fenceDirectory.dev || entry.ino !== fenceDirectory.ino || readdirSync(fence).length || readPrivateLockMarker(capturedMarker) !== marker) {
|
|
5263
|
+
try {
|
|
5264
|
+
linkSync(capturedMarker, fenceMarker);
|
|
5265
|
+
} catch {
|
|
5266
|
+
}
|
|
5267
|
+
throw new ConfigOwnershipError();
|
|
5268
|
+
}
|
|
5269
|
+
rmdirSync(fence);
|
|
5270
|
+
} catch {
|
|
5271
|
+
if (capturedLock) return { outcome: "UNRESOLVED_CAPTURE", before, quarantinePath: quarantine, receiptPath, fenceRetained: true };
|
|
5272
|
+
throw new ConfigOwnershipError();
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
return { outcome: "QUARANTINED_UNKNOWN_EMPTY", before, quarantinePath: quarantine, receiptPath, fenceRetained: false };
|
|
5277
|
+
}
|
|
5138
5278
|
function deadOwnerMarker(lockPath, ignoreOwnFence = false) {
|
|
5139
5279
|
try {
|
|
5140
5280
|
if (!ignoreOwnFence && lockEntryExists(lockPath + ".reconcile")) return null;
|
|
@@ -5732,110 +5872,6 @@ function resolvedProfileConfig(config, environment = selectedEnvironment()) {
|
|
|
5732
5872
|
};
|
|
5733
5873
|
}
|
|
5734
5874
|
|
|
5735
|
-
// src/config.ts
|
|
5736
|
-
async function authenticatedEnvironments(configOrPromise = readConfig()) {
|
|
5737
|
-
const config = await configOrPromise;
|
|
5738
|
-
const found = /* @__PURE__ */ new Set();
|
|
5739
|
-
if (config.credential) found.add("production");
|
|
5740
|
-
for (const environment of TOPOLO_ENVIRONMENTS) {
|
|
5741
|
-
const profile = config.profiles?.[environment];
|
|
5742
|
-
if (!profile) continue;
|
|
5743
|
-
if (profile.credential || Object.keys(profile.contexts ?? {}).length > 0) {
|
|
5744
|
-
found.add(environment);
|
|
5745
|
-
}
|
|
5746
|
-
}
|
|
5747
|
-
return TOPOLO_ENVIRONMENTS.filter((environment) => found.has(environment));
|
|
5748
|
-
}
|
|
5749
|
-
function environmentLabel(environment = selectedEnvironment()) {
|
|
5750
|
-
return environment;
|
|
5751
|
-
}
|
|
5752
|
-
function developerConsoleUrl(environment = selectedEnvironment()) {
|
|
5753
|
-
if (environment === "production") return "https://developers.topolo.app";
|
|
5754
|
-
if (environment === "development") return "https://developers.topolo.dev";
|
|
5755
|
-
return "https://developers.stg.topolo.us";
|
|
5756
|
-
}
|
|
5757
|
-
function oauthClientRegistrationUrl(environment = selectedEnvironment()) {
|
|
5758
|
-
return `${developerConsoleUrl(environment)}/developers/oauth-clients`;
|
|
5759
|
-
}
|
|
5760
|
-
function apiKeyIssuanceUrl(environment = selectedEnvironment()) {
|
|
5761
|
-
return `${developerConsoleUrl(environment)}/credentials`;
|
|
5762
|
-
}
|
|
5763
|
-
var DEFAULT_CLI_CLIENT_IDS = {
|
|
5764
|
-
production: "topolo-cli",
|
|
5765
|
-
staging: "topolo-cli",
|
|
5766
|
-
development: "topolo-cli"
|
|
5767
|
-
};
|
|
5768
|
-
function resolveOAuthClientId(config, environment = selectedEnvironment()) {
|
|
5769
|
-
const envSpecific = process.env[`${envVarPrefix(environment)}_CLI_CLIENT_ID`];
|
|
5770
|
-
if (envSpecific) return envSpecific;
|
|
5771
|
-
if (process.env.TOPOLO_CLI_CLIENT_ID) return process.env.TOPOLO_CLI_CLIENT_ID;
|
|
5772
|
-
return DEFAULT_CLI_CLIENT_IDS[environment] ?? profileForEnvironment(config, environment).oauthClientId;
|
|
5773
|
-
}
|
|
5774
|
-
function signedInContextKeys(config, environment) {
|
|
5775
|
-
return Object.keys(profileForEnvironment(config, environment).contexts ?? {});
|
|
5776
|
-
}
|
|
5777
|
-
function matchContextSelector(config, environment, selector) {
|
|
5778
|
-
const normalized = selector.trim().toLowerCase();
|
|
5779
|
-
if (!normalized || /[\r\n]/.test(selector)) {
|
|
5780
|
-
throw new Error("Context must be a non-empty single-line identifier.");
|
|
5781
|
-
}
|
|
5782
|
-
const contexts = Object.entries(profileForEnvironment(config, environment).contexts ?? {});
|
|
5783
|
-
const exact = contexts.find(([key]) => key.toLowerCase() === normalized);
|
|
5784
|
-
if (exact) return exact[0];
|
|
5785
|
-
const matches = contexts.filter(([, context]) => [
|
|
5786
|
-
context.organizationId,
|
|
5787
|
-
context.organizationSlug,
|
|
5788
|
-
context.organizationName
|
|
5789
|
-
].some((value) => String(value || "").trim().toLowerCase() === normalized));
|
|
5790
|
-
if (matches.length > 1) {
|
|
5791
|
-
throw new Error(`Context "${selector.trim()}" is ambiguous in ${environment}; use an exact key from \`topolo --env ${environment} context ls\`.`);
|
|
5792
|
-
}
|
|
5793
|
-
return matches[0]?.[0] ?? null;
|
|
5794
|
-
}
|
|
5795
|
-
function commandContextKey(config, environment, explicit = process.env.TOPOLO_CONTEXT) {
|
|
5796
|
-
const selector = explicit?.trim();
|
|
5797
|
-
if (selector) return matchContextSelector(config, environment, selector) ?? selector;
|
|
5798
|
-
return implicitContextKey(config, environment) ?? void 0;
|
|
5799
|
-
}
|
|
5800
|
-
function implicitContextKey(config, environment) {
|
|
5801
|
-
if (credentialSourceFromConfig(config, environment).startsWith("env:")) return null;
|
|
5802
|
-
const keys = signedInContextKeys(config, environment);
|
|
5803
|
-
if (keys.length > 1) throw new ContextNotSafeToAssumeError(environment, keys);
|
|
5804
|
-
return keys[0] ?? null;
|
|
5805
|
-
}
|
|
5806
|
-
var ContextNotSafeToAssumeError = class extends Error {
|
|
5807
|
-
constructor(environment, contexts) {
|
|
5808
|
-
super(
|
|
5809
|
-
`This machine is signed in to ${contexts.length} Topolo contexts in ${environment} (${contexts.join(", ")}), so the context is no longer safe to assume.
|
|
5810
|
-
|
|
5811
|
-
State it explicitly:
|
|
5812
|
-
topolo --env ${environment} --context <${contexts.join("|")}> ...
|
|
5813
|
-
|
|
5814
|
-
Or set it once for the whole session:
|
|
5815
|
-
export TOPOLO_CONTEXT=<${contexts.join("|")}>`
|
|
5816
|
-
);
|
|
5817
|
-
this.environment = environment;
|
|
5818
|
-
this.contexts = contexts;
|
|
5819
|
-
this.name = "ContextNotSafeToAssumeError";
|
|
5820
|
-
}
|
|
5821
|
-
environment;
|
|
5822
|
-
contexts;
|
|
5823
|
-
};
|
|
5824
|
-
async function withPinnedContext(options, read = () => readConfig()) {
|
|
5825
|
-
if (options.context?.trim() || options.installationId?.trim()) return options;
|
|
5826
|
-
const environment = normalizeEnvironment(options.environment?.trim() || process.env.TOPOLO_ENV || "production");
|
|
5827
|
-
const keys = signedInContextKeys(await read(), environment);
|
|
5828
|
-
if (keys.length === 1) return { ...options, context: keys[0] };
|
|
5829
|
-
if (!keys.length) {
|
|
5830
|
-
throw new Error(
|
|
5831
|
-
`No Topolo context is signed in for ${environment}, so there is nothing to pin. Run \`topolo --env ${environment} auth login\` first, or pass --context <context>.`
|
|
5832
|
-
);
|
|
5833
|
-
}
|
|
5834
|
-
throw new Error(
|
|
5835
|
-
`${keys.length} Topolo contexts are signed in for ${environment} (${keys.join(", ")}). Pass --context <context> to choose the one this MCP registration pins.`
|
|
5836
|
-
);
|
|
5837
|
-
}
|
|
5838
|
-
|
|
5839
5875
|
// src/output.ts
|
|
5840
5876
|
function resolveOutputMode(override) {
|
|
5841
5877
|
if (override === true) return "json";
|
|
@@ -5895,6 +5931,7 @@ function normalizeError(err) {
|
|
|
5895
5931
|
};
|
|
5896
5932
|
const body = err.body;
|
|
5897
5933
|
const status = err.status;
|
|
5934
|
+
const localLockConflict = code === "state_conflict" && status === void 0 && body === void 0 && err.service === void 0;
|
|
5898
5935
|
const suppliedDetails = err.details;
|
|
5899
5936
|
return {
|
|
5900
5937
|
message: status !== void 0 || body !== void 0 ? publicTopoloErrorMessage({ code, body, status, message: err.message }) : err.message,
|
|
@@ -5907,7 +5944,7 @@ function normalizeError(err) {
|
|
|
5907
5944
|
// SDK HTTP errors already carry sanitized details; projecting them again
|
|
5908
5945
|
// would drop form-level issues that have no field path.
|
|
5909
5946
|
details: reauthorizationDetails(err) ?? (err instanceof TopoloHttpError || err instanceof TopoloAuthError || err instanceof TopoloPermissionError ? err.details : extractTopoloServiceErrorDetails(suppliedDetails === void 0 ? body : { details: suppliedDetails })),
|
|
5910
|
-
hint: reauthorizationHint(err) ?? topoloErrorHint({ code, body }) ?? (code ? fallbackHintMap[code] : void 0),
|
|
5947
|
+
hint: reauthorizationHint(err) ?? topoloErrorHint({ code, body }) ?? (localLockConflict ? "The saved sign-in is locked by another or an interrupted Topolo process. Run `topolo auth lock` to see who holds it and how to recover." : code ? fallbackHintMap[code] : void 0),
|
|
5911
5948
|
exitCode: status === 401 || code === "auth_error" || code === "confirmation_required" ? 2 : status === 403 || code === "permission_denied" ? 3 : code === "immutable_drift" ? 4 : 1
|
|
5912
5949
|
};
|
|
5913
5950
|
}
|
|
@@ -5968,9 +6005,213 @@ function readCliVersion() {
|
|
|
5968
6005
|
}
|
|
5969
6006
|
var CLI_VERSION = readCliVersion();
|
|
5970
6007
|
|
|
6008
|
+
// src/commands/auth-lock.ts
|
|
6009
|
+
var QUIESCENCE_WAIT_MS = 2e3;
|
|
6010
|
+
var REVIEW_TTL_MS = 5 * 6e4;
|
|
6011
|
+
function lockGuidance(diagnostic) {
|
|
6012
|
+
const { state, reason, lockPath, generation } = diagnostic;
|
|
6013
|
+
if (state === "FREE") return "No lock is held. Sign-in renewal is not blocked.";
|
|
6014
|
+
if (state === "LIVE") {
|
|
6015
|
+
return `A running process holds the lock; a renewal normally finishes within 10 seconds. If it stays held after every Topolo CLI and MCP process has exited (after a restart the recorded process number can belong to another program), quit them all and move ${lockPath} aside.`;
|
|
6016
|
+
}
|
|
6017
|
+
if (reason === "EMPTY_OWNER_UNKNOWN") {
|
|
6018
|
+
return `An empty lock was left by an interrupted writer. Close other Topolo CLI and MCP processes, then run:
|
|
6019
|
+
topolo auth lock --reconcile ${generation}`;
|
|
6020
|
+
}
|
|
6021
|
+
if (reason === "RECONCILIATION_IN_PROGRESS") {
|
|
6022
|
+
return `A lock recovery is running or was interrupted (${lockPath}.reconcile). Wait for it, or if no Topolo process is running, keep the fence for review and move both paths aside.`;
|
|
6023
|
+
}
|
|
6024
|
+
return `The lock's owner cannot be verified on this system (unexpected owner, permissions or platform). If no Topolo CLI or MCP process is running, move ${lockPath} aside.`;
|
|
6025
|
+
}
|
|
6026
|
+
function registerAuthLockCommand(auth) {
|
|
6027
|
+
auth.command("lock").description("Inspect, and recover, the lock that serializes sign-in renewal").option("--reconcile <generation>", "Quarantine an empty lock left by an interrupted writer (generation from `topolo auth lock`)").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
6028
|
+
const mode = resolveOutputMode(opts.json);
|
|
6029
|
+
try {
|
|
6030
|
+
const diagnostic = await inspectConfigLock();
|
|
6031
|
+
if (opts.reconcile === void 0) {
|
|
6032
|
+
emit(
|
|
6033
|
+
{ lock: diagnostic, guidance: lockGuidance(diagnostic) },
|
|
6034
|
+
mode,
|
|
6035
|
+
() => [
|
|
6036
|
+
`Lock: ${diagnostic.lockPath}`,
|
|
6037
|
+
`State: ${diagnostic.state} (${diagnostic.reason})`,
|
|
6038
|
+
`Generation: ${diagnostic.generation}`,
|
|
6039
|
+
"",
|
|
6040
|
+
lockGuidance(diagnostic)
|
|
6041
|
+
].join("\n")
|
|
6042
|
+
);
|
|
6043
|
+
return;
|
|
6044
|
+
}
|
|
6045
|
+
const generation = opts.reconcile.trim();
|
|
6046
|
+
if (diagnostic.state !== "UNKNOWN" || diagnostic.reason !== "EMPTY_OWNER_UNKNOWN") {
|
|
6047
|
+
throw new Error(`Only an empty lock left by an interrupted writer can be reconciled; this lock is ${diagnostic.state} (${diagnostic.reason}). ${lockGuidance(diagnostic)}`);
|
|
6048
|
+
}
|
|
6049
|
+
if (diagnostic.generation !== generation) {
|
|
6050
|
+
throw new Error("The lock changed since it was inspected. Run `topolo auth lock` again and use the generation it prints.");
|
|
6051
|
+
}
|
|
6052
|
+
await new Promise((resolve6) => setTimeout(resolve6, QUIESCENCE_WAIT_MS));
|
|
6053
|
+
const settled = await inspectConfigLock();
|
|
6054
|
+
if (settled.generation !== generation) {
|
|
6055
|
+
throw new Error("The lock changed while waiting, so another process is using it. Nothing was changed.");
|
|
6056
|
+
}
|
|
6057
|
+
const evidence = JSON.stringify({
|
|
6058
|
+
observer: `topolo-cli/${CLI_VERSION}`,
|
|
6059
|
+
before: diagnostic,
|
|
6060
|
+
after: settled,
|
|
6061
|
+
waitedMs: QUIESCENCE_WAIT_MS
|
|
6062
|
+
});
|
|
6063
|
+
const result = await reconcileConfigLock({
|
|
6064
|
+
expectedGeneration: generation,
|
|
6065
|
+
review: {
|
|
6066
|
+
kind: "quiescent-empty-quarantine",
|
|
6067
|
+
configPath: diagnostic.configPath,
|
|
6068
|
+
generation,
|
|
6069
|
+
quiescenceEvidenceSha256: createHash3("sha256").update(evidence).digest("hex"),
|
|
6070
|
+
reviewedBy: "topolo-cli:auth-lock",
|
|
6071
|
+
expiresAt: new Date(Date.now() + REVIEW_TTL_MS).toISOString()
|
|
6072
|
+
}
|
|
6073
|
+
});
|
|
6074
|
+
const recovered = result.outcome === "QUARANTINED_UNKNOWN_EMPTY";
|
|
6075
|
+
if (!recovered) process.exitCode = 1;
|
|
6076
|
+
emit(
|
|
6077
|
+
{ ok: recovered, outcome: result.outcome, quarantinePath: result.quarantinePath, fenceRetained: result.fenceRetained },
|
|
6078
|
+
mode,
|
|
6079
|
+
() => recovered ? `Recovered: the empty lock was moved to ${result.quarantinePath}. Sign-in renewal can proceed.` : `Not recovered: the lock changed during capture. Evidence and the recovery fence were kept in ${result.quarantinePath} for review.`
|
|
6080
|
+
);
|
|
6081
|
+
} catch (error) {
|
|
6082
|
+
fail2(error, mode);
|
|
6083
|
+
}
|
|
6084
|
+
});
|
|
6085
|
+
}
|
|
6086
|
+
|
|
6087
|
+
// src/lib/open-browser.ts
|
|
6088
|
+
import { spawn } from "child_process";
|
|
6089
|
+
import { platform as platform2 } from "os";
|
|
6090
|
+
function tryOpenBrowser(url) {
|
|
6091
|
+
try {
|
|
6092
|
+
const os = platform2();
|
|
6093
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
6094
|
+
const args = os === "win32" ? ["", url] : [url];
|
|
6095
|
+
const child = spawn(cmd, args, {
|
|
6096
|
+
detached: true,
|
|
6097
|
+
stdio: "ignore",
|
|
6098
|
+
...os === "win32" ? { shell: true } : {}
|
|
6099
|
+
});
|
|
6100
|
+
child.unref();
|
|
6101
|
+
return true;
|
|
6102
|
+
} catch {
|
|
6103
|
+
return false;
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
6106
|
+
|
|
6107
|
+
// src/config.ts
|
|
6108
|
+
async function authenticatedEnvironments(configOrPromise = readConfig()) {
|
|
6109
|
+
const config = await configOrPromise;
|
|
6110
|
+
const found = /* @__PURE__ */ new Set();
|
|
6111
|
+
if (config.credential) found.add("production");
|
|
6112
|
+
for (const environment of TOPOLO_ENVIRONMENTS) {
|
|
6113
|
+
const profile = config.profiles?.[environment];
|
|
6114
|
+
if (!profile) continue;
|
|
6115
|
+
if (profile.credential || Object.keys(profile.contexts ?? {}).length > 0) {
|
|
6116
|
+
found.add(environment);
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
return TOPOLO_ENVIRONMENTS.filter((environment) => found.has(environment));
|
|
6120
|
+
}
|
|
6121
|
+
function environmentLabel(environment = selectedEnvironment()) {
|
|
6122
|
+
return environment;
|
|
6123
|
+
}
|
|
6124
|
+
function developerConsoleUrl(environment = selectedEnvironment()) {
|
|
6125
|
+
if (environment === "production") return "https://developers.topolo.app";
|
|
6126
|
+
if (environment === "development") return "https://developers.topolo.dev";
|
|
6127
|
+
return "https://developers.stg.topolo.us";
|
|
6128
|
+
}
|
|
6129
|
+
function oauthClientRegistrationUrl(environment = selectedEnvironment()) {
|
|
6130
|
+
return `${developerConsoleUrl(environment)}/developers/oauth-clients`;
|
|
6131
|
+
}
|
|
6132
|
+
function apiKeyIssuanceUrl(environment = selectedEnvironment()) {
|
|
6133
|
+
return `${developerConsoleUrl(environment)}/credentials`;
|
|
6134
|
+
}
|
|
6135
|
+
var DEFAULT_CLI_CLIENT_IDS = {
|
|
6136
|
+
production: "topolo-cli",
|
|
6137
|
+
staging: "topolo-cli",
|
|
6138
|
+
development: "topolo-cli"
|
|
6139
|
+
};
|
|
6140
|
+
function resolveOAuthClientId(config, environment = selectedEnvironment()) {
|
|
6141
|
+
const envSpecific = process.env[`${envVarPrefix(environment)}_CLI_CLIENT_ID`];
|
|
6142
|
+
if (envSpecific) return envSpecific;
|
|
6143
|
+
if (process.env.TOPOLO_CLI_CLIENT_ID) return process.env.TOPOLO_CLI_CLIENT_ID;
|
|
6144
|
+
return DEFAULT_CLI_CLIENT_IDS[environment] ?? profileForEnvironment(config, environment).oauthClientId;
|
|
6145
|
+
}
|
|
6146
|
+
function signedInContextKeys(config, environment) {
|
|
6147
|
+
return Object.keys(profileForEnvironment(config, environment).contexts ?? {});
|
|
6148
|
+
}
|
|
6149
|
+
function matchContextSelector(config, environment, selector) {
|
|
6150
|
+
const normalized = selector.trim().toLowerCase();
|
|
6151
|
+
if (!normalized || /[\r\n]/.test(selector)) {
|
|
6152
|
+
throw new Error("Context must be a non-empty single-line identifier.");
|
|
6153
|
+
}
|
|
6154
|
+
const contexts = Object.entries(profileForEnvironment(config, environment).contexts ?? {});
|
|
6155
|
+
const exact = contexts.find(([key]) => key.toLowerCase() === normalized);
|
|
6156
|
+
if (exact) return exact[0];
|
|
6157
|
+
const matches = contexts.filter(([, context]) => [
|
|
6158
|
+
context.organizationId,
|
|
6159
|
+
context.organizationSlug,
|
|
6160
|
+
context.organizationName
|
|
6161
|
+
].some((value) => String(value || "").trim().toLowerCase() === normalized));
|
|
6162
|
+
if (matches.length > 1) {
|
|
6163
|
+
throw new Error(`Context "${selector.trim()}" is ambiguous in ${environment}; use an exact key from \`topolo --env ${environment} context ls\`.`);
|
|
6164
|
+
}
|
|
6165
|
+
return matches[0]?.[0] ?? null;
|
|
6166
|
+
}
|
|
6167
|
+
function commandContextKey(config, environment, explicit = process.env.TOPOLO_CONTEXT) {
|
|
6168
|
+
const selector = explicit?.trim();
|
|
6169
|
+
if (selector) return matchContextSelector(config, environment, selector) ?? selector;
|
|
6170
|
+
return implicitContextKey(config, environment) ?? void 0;
|
|
6171
|
+
}
|
|
6172
|
+
function implicitContextKey(config, environment) {
|
|
6173
|
+
if (credentialSourceFromConfig(config, environment).startsWith("env:")) return null;
|
|
6174
|
+
const keys = signedInContextKeys(config, environment);
|
|
6175
|
+
if (keys.length > 1) throw new ContextNotSafeToAssumeError(environment, keys);
|
|
6176
|
+
return keys[0] ?? null;
|
|
6177
|
+
}
|
|
6178
|
+
var ContextNotSafeToAssumeError = class extends Error {
|
|
6179
|
+
constructor(environment, contexts) {
|
|
6180
|
+
super(
|
|
6181
|
+
`This machine is signed in to ${contexts.length} Topolo contexts in ${environment} (${contexts.join(", ")}), so the context is no longer safe to assume.
|
|
6182
|
+
|
|
6183
|
+
State it explicitly:
|
|
6184
|
+
topolo --env ${environment} --context <${contexts.join("|")}> ...
|
|
6185
|
+
|
|
6186
|
+
Or set it once for the whole session:
|
|
6187
|
+
export TOPOLO_CONTEXT=<${contexts.join("|")}>`
|
|
6188
|
+
);
|
|
6189
|
+
this.environment = environment;
|
|
6190
|
+
this.contexts = contexts;
|
|
6191
|
+
this.name = "ContextNotSafeToAssumeError";
|
|
6192
|
+
}
|
|
6193
|
+
environment;
|
|
6194
|
+
contexts;
|
|
6195
|
+
};
|
|
6196
|
+
async function withPinnedContext(options, read = () => readConfig()) {
|
|
6197
|
+
if (options.context?.trim() || options.installationId?.trim()) return options;
|
|
6198
|
+
const environment = normalizeEnvironment(options.environment?.trim() || process.env.TOPOLO_ENV || "production");
|
|
6199
|
+
const keys = signedInContextKeys(await read(), environment);
|
|
6200
|
+
if (keys.length === 1) return { ...options, context: keys[0] };
|
|
6201
|
+
if (!keys.length) {
|
|
6202
|
+
throw new Error(
|
|
6203
|
+
`No Topolo context is signed in for ${environment}, so there is nothing to pin. Run \`topolo --env ${environment} auth login\` first, or pass --context <context>.`
|
|
6204
|
+
);
|
|
6205
|
+
}
|
|
6206
|
+
throw new Error(
|
|
6207
|
+
`${keys.length} Topolo contexts are signed in for ${environment} (${keys.join(", ")}). Pass --context <context> to choose the one this MCP registration pins.`
|
|
6208
|
+
);
|
|
6209
|
+
}
|
|
6210
|
+
|
|
5971
6211
|
// src/commands/auth.ts
|
|
5972
6212
|
function registerAuthCommands(program2) {
|
|
5973
6213
|
const auth = program2.command("auth").description("Manage Topolo credentials");
|
|
6214
|
+
registerAuthLockCommand(auth);
|
|
5974
6215
|
auth.command("login").description("Authenticate with Topolo via OAuth device flow (default) or store an API key").option("--api-key <key>", "Store an API key directly, skipping the OAuth flow").option("--client-id <id>", "OAuth client_id (or set TOPOLO_CLI_CLIENT_ID)").option("--scope <scope>", "Scopes to request (space-separated)").option("--no-browser", "Print the approval URL without opening a browser").option("--use", "Also make the approved context this environment's stored default (as `topolo context use`)").option("--json", "Output JSON").option("--no-json", "Output human-readable text").addHelpText("after", [
|
|
5975
6216
|
"",
|
|
5976
6217
|
"The approved sign-in is stored under its own context key (personal or org:<id>).",
|
|
@@ -6890,7 +7131,7 @@ function formatActionPlan(plan) {
|
|
|
6890
7131
|
}
|
|
6891
7132
|
|
|
6892
7133
|
// src/lib/credential-custody.ts
|
|
6893
|
-
import { createHash as
|
|
7134
|
+
import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
|
|
6894
7135
|
import { constants as constants2 } from "fs";
|
|
6895
7136
|
import { chmod as chmod3, lstat as lstat2, mkdir as mkdir3, open, rename as rename2, rm as rm2 } from "fs/promises";
|
|
6896
7137
|
import { basename as basename2, isAbsolute, join as join5, normalize, sep } from "path";
|
|
@@ -6966,7 +7207,7 @@ async function writeCredentialCustody(request, deps) {
|
|
|
6966
7207
|
environment: deps.environment,
|
|
6967
7208
|
apiKeyId: key.id,
|
|
6968
7209
|
expiresAt: key.expiresAt,
|
|
6969
|
-
keyDigest: `sha256:${
|
|
7210
|
+
keyDigest: `sha256:${createHash4("sha256").update(secret, "utf8").digest("hex")}`,
|
|
6970
7211
|
createdAt: key.createdAt
|
|
6971
7212
|
};
|
|
6972
7213
|
await writeCustodyFiles(directory, [
|
|
@@ -7722,7 +7963,7 @@ import { spawnSync as spawnSync5 } from "child_process";
|
|
|
7722
7963
|
import { existsSync as existsSync6 } from "fs";
|
|
7723
7964
|
import { readFile as readFile9, rm as rm3 } from "fs/promises";
|
|
7724
7965
|
import { homedir as homedir5 } from "os";
|
|
7725
|
-
import { dirname as dirname7, join as join11, resolve as
|
|
7966
|
+
import { dirname as dirname7, join as join11, resolve as resolve4 } from "path";
|
|
7726
7967
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7727
7968
|
import { createRequire } from "module";
|
|
7728
7969
|
|
|
@@ -7737,7 +7978,7 @@ import { spawnSync as spawnSync3 } from "child_process";
|
|
|
7737
7978
|
import { existsSync as existsSync4 } from "fs";
|
|
7738
7979
|
import { readFile as readFile4 } from "fs/promises";
|
|
7739
7980
|
import { homedir as homedir4 } from "os";
|
|
7740
|
-
import { isAbsolute as isAbsolute2, join as join7, resolve as
|
|
7981
|
+
import { isAbsolute as isAbsolute2, join as join7, resolve as resolve3 } from "path";
|
|
7741
7982
|
var SUPPORTED_AGENT_IDS = [
|
|
7742
7983
|
"claude",
|
|
7743
7984
|
"claude-desktop",
|
|
@@ -7778,14 +8019,14 @@ var AGENT_NAME_FOR = {
|
|
|
7778
8019
|
"amazon-q": "amazon-q"
|
|
7779
8020
|
};
|
|
7780
8021
|
function resolveAgentHostTarget(options) {
|
|
7781
|
-
const homeDir =
|
|
8022
|
+
const homeDir = resolve3(options.homeDir ?? homedir4());
|
|
7782
8023
|
const codexHome = options.codexHome ?? (options.homeDir === void 0 ? process.env.CODEX_HOME || void 0 : void 0);
|
|
7783
8024
|
if (codexHome !== void 0 && (!codexHome.trim() || !isAbsolute2(codexHome))) {
|
|
7784
8025
|
throw new Error("Codex home must be an absolute nonempty directory.");
|
|
7785
8026
|
}
|
|
7786
8027
|
return Object.freeze({
|
|
7787
8028
|
homeDir,
|
|
7788
|
-
...options.projectDir ? { projectDir:
|
|
8029
|
+
...options.projectDir ? { projectDir: resolve3(options.projectDir) } : {},
|
|
7789
8030
|
codexHome: codexHome ?? join7(homeDir, ".codex")
|
|
7790
8031
|
});
|
|
7791
8032
|
}
|
|
@@ -7947,7 +8188,7 @@ async function assertClaudeProjectScope(homeDir, projectDir, name) {
|
|
|
7947
8188
|
}
|
|
7948
8189
|
return value;
|
|
7949
8190
|
};
|
|
7950
|
-
const local = object(object(config.projects)?.[
|
|
8191
|
+
const local = object(object(config.projects)?.[resolve3(projectDir)]);
|
|
7951
8192
|
if (Object.hasOwn(object(config.mcpServers) ?? {}, name) || Object.hasOwn(object(local?.mcpServers) ?? {}, name)) {
|
|
7952
8193
|
throw new Error("Claude MCP scope conflict for " + name + ": a user or local definition can override the project registration. Resolve that scope before setup.");
|
|
7953
8194
|
}
|
|
@@ -9040,8 +9281,8 @@ function formatSharedSetup(result) {
|
|
|
9040
9281
|
var AGENT_LABELS2 = AGENT_LABELS;
|
|
9041
9282
|
function skillsSourceDir() {
|
|
9042
9283
|
const here = dirname7(fileURLToPath3(import.meta.url));
|
|
9043
|
-
const distCandidate =
|
|
9044
|
-
const srcCandidate =
|
|
9284
|
+
const distCandidate = resolve4(here, "../skills");
|
|
9285
|
+
const srcCandidate = resolve4(here, "../../skills");
|
|
9045
9286
|
for (const candidate of [distCandidate, srcCandidate]) {
|
|
9046
9287
|
if (existsSync6(join11(candidate, "topolo", "SKILL.md"))) return candidate;
|
|
9047
9288
|
}
|
|
@@ -9168,7 +9409,7 @@ function registerSkillsCommand(program2) {
|
|
|
9168
9409
|
const mode = resolveOutputMode(opts.json);
|
|
9169
9410
|
try {
|
|
9170
9411
|
const rows = await agentSurfaceStatus({
|
|
9171
|
-
homeDir: opts.agentHome ?
|
|
9412
|
+
homeDir: opts.agentHome ? resolve4(opts.agentHome) : void 0,
|
|
9172
9413
|
codexHome: opts.codexHome,
|
|
9173
9414
|
name: opts.name,
|
|
9174
9415
|
agent: opts.agent,
|
|
@@ -9185,7 +9426,7 @@ function registerSkillsCommand(program2) {
|
|
|
9185
9426
|
try {
|
|
9186
9427
|
const removed = await uninstallAgentSurfaces({
|
|
9187
9428
|
...opts,
|
|
9188
|
-
homeDir: opts.agentHome ?
|
|
9429
|
+
homeDir: opts.agentHome ? resolve4(opts.agentHome) : void 0
|
|
9189
9430
|
});
|
|
9190
9431
|
emit(
|
|
9191
9432
|
{ removed },
|
|
@@ -9328,7 +9569,7 @@ function formatStatus(rows) {
|
|
|
9328
9569
|
// src/commands/doctor.ts
|
|
9329
9570
|
import { access as access2 } from "fs/promises";
|
|
9330
9571
|
import { homedir as homedir6 } from "os";
|
|
9331
|
-
import { resolve as
|
|
9572
|
+
import { resolve as resolve5 } from "path";
|
|
9332
9573
|
import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
|
|
9333
9574
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
9334
9575
|
|
|
@@ -9504,7 +9745,7 @@ async function verifyMcpProtocol(command, args, env = definedEnvironment(process
|
|
|
9504
9745
|
}
|
|
9505
9746
|
async function resolveInstalledDoctorTarget(options) {
|
|
9506
9747
|
const name = options.name?.trim() || "topolo";
|
|
9507
|
-
const homeDir = options.agentHome ?
|
|
9748
|
+
const homeDir = options.agentHome ? resolve5(options.agentHome) : homedir6();
|
|
9508
9749
|
const expected = resolveMcpCommand();
|
|
9509
9750
|
const requestedAgents = resolveTargets({
|
|
9510
9751
|
...options,
|
|
@@ -10155,7 +10396,7 @@ function registerInstallationsCommand(program2) {
|
|
|
10155
10396
|
import { createTopoloMcpLocalHandler } from "@topolo/mcp/local";
|
|
10156
10397
|
|
|
10157
10398
|
// src/lib/mcp-hub-server.ts
|
|
10158
|
-
import { createHash as
|
|
10399
|
+
import { createHash as createHash5, timingSafeEqual } from "crypto";
|
|
10159
10400
|
import { stat as stat2 } from "fs/promises";
|
|
10160
10401
|
import { createServer } from "http";
|
|
10161
10402
|
var MCP_HUB_HOST = "127.0.0.1";
|
|
@@ -10170,7 +10411,7 @@ function refusal(status, message3, headers = {}) {
|
|
|
10170
10411
|
return json(status, { jsonrpc: "2.0", error: { code: -32e3, message: message3 }, id: null }, headers);
|
|
10171
10412
|
}
|
|
10172
10413
|
function digest(value) {
|
|
10173
|
-
return
|
|
10414
|
+
return createHash5("sha256").update(value, "utf8").digest();
|
|
10174
10415
|
}
|
|
10175
10416
|
function bearerTokenMatches(header, token) {
|
|
10176
10417
|
const match = /^Bearer[ \t]+(\S+)[ \t]*$/i.exec(header ?? "");
|
|
@@ -10301,11 +10542,11 @@ async function writeWebResponse(response, target) {
|
|
|
10301
10542
|
}
|
|
10302
10543
|
async function startMcpHub(options) {
|
|
10303
10544
|
const server = createServer();
|
|
10304
|
-
await new Promise((
|
|
10545
|
+
await new Promise((resolve6, reject) => {
|
|
10305
10546
|
server.once("error", reject);
|
|
10306
10547
|
server.listen(options.port, MCP_HUB_HOST, () => {
|
|
10307
10548
|
server.off("error", reject);
|
|
10308
|
-
|
|
10549
|
+
resolve6();
|
|
10309
10550
|
});
|
|
10310
10551
|
});
|
|
10311
10552
|
const port = server.address().port;
|
|
@@ -10336,8 +10577,8 @@ async function startMcpHub(options) {
|
|
|
10336
10577
|
return {
|
|
10337
10578
|
port,
|
|
10338
10579
|
server,
|
|
10339
|
-
close: () => new Promise((
|
|
10340
|
-
server.close(() =>
|
|
10580
|
+
close: () => new Promise((resolve6) => {
|
|
10581
|
+
server.close(() => resolve6());
|
|
10341
10582
|
server.closeAllConnections();
|
|
10342
10583
|
})
|
|
10343
10584
|
};
|
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.
|
|
5
|
-
"topoloPublishedVersion": "0.13.
|
|
4
|
+
"version": "0.13.23",
|
|
5
|
+
"topoloPublishedVersion": "0.13.23",
|
|
6
6
|
"description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
8
8
|
"author": "Topolo",
|
package/postinstall.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
+
// Plain guidance for the person installing. Install output must never tell an
|
|
2
|
+
// AI assistant to act on its own: `topolo setup` edits other tools' config files.
|
|
1
3
|
const message = [
|
|
2
4
|
'Topolo CLI installed successfully.',
|
|
3
5
|
'',
|
|
4
|
-
'
|
|
5
|
-
' 1.
|
|
6
|
+
'Next steps, when you choose to run them:',
|
|
7
|
+
' 1. topolo setup connects your local AI tools to Topolo (edits their config files)',
|
|
6
8
|
' (Use --all only when every supported local host should be configured.)',
|
|
7
|
-
' 2.
|
|
8
|
-
' 3.
|
|
9
|
+
' 2. topolo auth login signs in through your browser',
|
|
10
|
+
' 3. topolo doctor checks the result',
|
|
9
11
|
'',
|
|
10
12
|
'The setup command detects native config files for Claude Code/Desktop, Codex, Cursor, Grok Build, Gemini CLI, GitHub Copilot/VS Code, Windsurf, Cline, Goose, and Amazon Q Developer.',
|
|
11
|
-
'If multiple contexts are listed, use the exact intended context returned by the CLI; do not guess organization IDs.',
|
|
12
13
|
'JetBrains AI Assistant and web-only connectors use their own UI or remote connector setup and are not edited by this local installer.',
|
|
13
|
-
'Only pause for the browser sign-in or an operating-system permission prompt.',
|
|
14
14
|
].join('\n');
|
|
15
15
|
|
|
16
16
|
process.stdout.write(`${message}\n`);
|