@topolo/cli 0.1.34 → 0.2.1
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 +60 -19
- package/dist/index.js +627 -202
- package/package.json +6 -2
- package/skills/codex/AGENTS.md +25 -11
- package/skills/topolo/SKILL.md +70 -27
package/dist/index.js
CHANGED
|
@@ -50,6 +50,12 @@ var TopoloPermissionError = class extends TopoloSdkError {
|
|
|
50
50
|
this.required = required;
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
|
+
var TopoloConfirmationRequiredError = class extends TopoloSdkError {
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super("confirmation_required", message);
|
|
56
|
+
this.name = "TopoloConfirmationRequiredError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
53
59
|
var TopoloHttpError = class extends TopoloSdkError {
|
|
54
60
|
status;
|
|
55
61
|
body;
|
|
@@ -1112,6 +1118,7 @@ var APPLICATIONS = {
|
|
|
1112
1118
|
"topolo.io"
|
|
1113
1119
|
],
|
|
1114
1120
|
"routes": [
|
|
1121
|
+
"one.topolo.io",
|
|
1115
1122
|
"topolo.io",
|
|
1116
1123
|
"www.topolo.io"
|
|
1117
1124
|
],
|
|
@@ -2065,7 +2072,6 @@ var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
|
|
|
2065
2072
|
var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
|
|
2066
2073
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2067
2074
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
2068
|
-
var CATALOG_CACHE_TTL_MS = 60 * 1e3;
|
|
2069
2075
|
var TopoloClient = class _TopoloClient {
|
|
2070
2076
|
credential;
|
|
2071
2077
|
agent;
|
|
@@ -2074,9 +2080,9 @@ var TopoloClient = class _TopoloClient {
|
|
|
2074
2080
|
requireConfirmForWrites;
|
|
2075
2081
|
timeoutMs;
|
|
2076
2082
|
fetchImpl;
|
|
2083
|
+
refreshCredential;
|
|
2084
|
+
refreshInFlight = null;
|
|
2077
2085
|
debug;
|
|
2078
|
-
serviceCatalogCache = null;
|
|
2079
|
-
actionCatalogCache = null;
|
|
2080
2086
|
/**
|
|
2081
2087
|
* Process-wide cache for slug → app-id lookups. Keyed by
|
|
2082
2088
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2101,6 +2107,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2101
2107
|
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
2102
2108
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
2103
2109
|
this.fetchImpl = options.fetch ?? fetch;
|
|
2110
|
+
this.refreshCredential = options.refreshCredential;
|
|
2104
2111
|
this.debug = options.debug;
|
|
2105
2112
|
}
|
|
2106
2113
|
emit(event) {
|
|
@@ -2118,11 +2125,11 @@ var TopoloClient = class _TopoloClient {
|
|
|
2118
2125
|
async request(opts) {
|
|
2119
2126
|
const method = opts.method ?? "GET";
|
|
2120
2127
|
if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
|
|
2121
|
-
throw new
|
|
2128
|
+
throw new TopoloConfirmationRequiredError(
|
|
2122
2129
|
`Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
|
|
2123
2130
|
);
|
|
2124
2131
|
}
|
|
2125
|
-
const resolvedService = await this.resolveRequestService(opts.service);
|
|
2132
|
+
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
|
|
2126
2133
|
const baseUrl = resolvedService.baseUrl;
|
|
2127
2134
|
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
2128
2135
|
if (opts.query) {
|
|
@@ -2153,20 +2160,36 @@ var TopoloClient = class _TopoloClient {
|
|
|
2153
2160
|
headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
|
|
2154
2161
|
headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
|
|
2155
2162
|
}
|
|
2156
|
-
const controller = new AbortController();
|
|
2157
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2158
|
-
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
2159
2163
|
const urlStr = url.toString();
|
|
2160
2164
|
const startedAt = Date.now();
|
|
2161
2165
|
this.emit({ phase: "request", method, service: resolvedService.serviceKey, path: opts.path, url: urlStr, requestId });
|
|
2166
|
+
const fetchOnce = async () => {
|
|
2167
|
+
const controller = new AbortController();
|
|
2168
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2169
|
+
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
2170
|
+
try {
|
|
2171
|
+
return await this.fetchImpl(urlStr, {
|
|
2172
|
+
method,
|
|
2173
|
+
headers: new Headers(headers),
|
|
2174
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
|
|
2175
|
+
signal
|
|
2176
|
+
});
|
|
2177
|
+
} finally {
|
|
2178
|
+
clearTimeout(timeoutId);
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2162
2181
|
let res;
|
|
2163
2182
|
try {
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2183
|
+
const attemptedCredential = this.credential;
|
|
2184
|
+
res = await fetchOnce();
|
|
2185
|
+
if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
|
|
2186
|
+
await res.body?.cancel().catch(() => void 0);
|
|
2187
|
+
const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
|
|
2188
|
+
if (refreshed) {
|
|
2189
|
+
applyAuthHeaders(headers, refreshed);
|
|
2190
|
+
res = await fetchOnce();
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2170
2193
|
} catch (err) {
|
|
2171
2194
|
this.emit({
|
|
2172
2195
|
phase: "error",
|
|
@@ -2179,8 +2202,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
2179
2202
|
error: err instanceof Error ? err.message : String(err)
|
|
2180
2203
|
});
|
|
2181
2204
|
throw err;
|
|
2182
|
-
} finally {
|
|
2183
|
-
clearTimeout(timeoutId);
|
|
2184
2205
|
}
|
|
2185
2206
|
const contentType = res.headers.get("Content-Type") ?? "";
|
|
2186
2207
|
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
@@ -2219,6 +2240,19 @@ var TopoloClient = class _TopoloClient {
|
|
|
2219
2240
|
});
|
|
2220
2241
|
return parsed;
|
|
2221
2242
|
}
|
|
2243
|
+
async refreshRejectedAccessToken(attemptedAccessToken) {
|
|
2244
|
+
if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
|
|
2245
|
+
return this.credential;
|
|
2246
|
+
}
|
|
2247
|
+
if (!this.refreshCredential) return null;
|
|
2248
|
+
this.refreshInFlight ??= this.refreshCredential().then((credential) => {
|
|
2249
|
+
if (credential) this.credential = credential;
|
|
2250
|
+
return credential;
|
|
2251
|
+
}).catch(() => null).finally(() => {
|
|
2252
|
+
this.refreshInFlight = null;
|
|
2253
|
+
});
|
|
2254
|
+
return this.refreshInFlight;
|
|
2255
|
+
}
|
|
2222
2256
|
async resolveRequestService(service) {
|
|
2223
2257
|
const serviceKey = normalizeLookupKey(service);
|
|
2224
2258
|
if (!serviceKey) {
|
|
@@ -2244,9 +2278,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2244
2278
|
);
|
|
2245
2279
|
}
|
|
2246
2280
|
async resolveLiveCallableService(serviceKey) {
|
|
2247
|
-
const
|
|
2248
|
-
|
|
2249
|
-
if (!match) return null;
|
|
2281
|
+
const match = await this.getService(serviceKey).catch(() => null);
|
|
2282
|
+
if (!match || !catalogEntryMatches(match, serviceKey)) return null;
|
|
2250
2283
|
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
2251
2284
|
serviceKey,
|
|
2252
2285
|
overrides: this.serviceUrls
|
|
@@ -2320,12 +2353,13 @@ var TopoloClient = class _TopoloClient {
|
|
|
2320
2353
|
* The live Auth catalog is the authority because it is scoped to the caller's
|
|
2321
2354
|
* organization and effective permissions.
|
|
2322
2355
|
*/
|
|
2323
|
-
async
|
|
2324
|
-
const
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2356
|
+
async searchServices(options = {}) {
|
|
2357
|
+
const res = await this.request({
|
|
2358
|
+
service: "developers",
|
|
2359
|
+
method: "GET",
|
|
2360
|
+
path: "/api/services/catalog",
|
|
2361
|
+
query: catalogSearchQuery(options)
|
|
2362
|
+
});
|
|
2329
2363
|
const raw = res.data?.services ?? res.services;
|
|
2330
2364
|
if (!Array.isArray(raw)) {
|
|
2331
2365
|
throw new TopoloSdkError(
|
|
@@ -2335,6 +2369,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2335
2369
|
}
|
|
2336
2370
|
const entries = [];
|
|
2337
2371
|
for (const item of raw) {
|
|
2372
|
+
if (!isRecord(item)) continue;
|
|
2338
2373
|
const s = item;
|
|
2339
2374
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
2340
2375
|
if (!appId) continue;
|
|
@@ -2359,11 +2394,30 @@ var TopoloClient = class _TopoloClient {
|
|
|
2359
2394
|
apiKeyResources
|
|
2360
2395
|
});
|
|
2361
2396
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
};
|
|
2366
|
-
|
|
2397
|
+
const page = normalizeCatalogPage(
|
|
2398
|
+
res.data?.page ?? res.page
|
|
2399
|
+
);
|
|
2400
|
+
return { services: entries, page };
|
|
2401
|
+
}
|
|
2402
|
+
/** @deprecated Prefer searchServices() so callers retain the continuation cursor. */
|
|
2403
|
+
async listServices(options = {}) {
|
|
2404
|
+
return (await this.searchServices(options)).services;
|
|
2405
|
+
}
|
|
2406
|
+
async getService(application) {
|
|
2407
|
+
const needle = application.trim();
|
|
2408
|
+
if (!needle) throw new TopoloSdkError("unknown_service", "application is required");
|
|
2409
|
+
const res = await this.request({
|
|
2410
|
+
service: "developers",
|
|
2411
|
+
method: "GET",
|
|
2412
|
+
path: "/api/services/catalog",
|
|
2413
|
+
query: { application: needle }
|
|
2414
|
+
});
|
|
2415
|
+
const raw = res.data?.application ?? res.application;
|
|
2416
|
+
const entry = normalizeServiceCatalogEntry(raw);
|
|
2417
|
+
if (!entry) {
|
|
2418
|
+
throw new TopoloSdkError("unknown_service", `Unknown Topolo application "${application}".`);
|
|
2419
|
+
}
|
|
2420
|
+
return entry;
|
|
2367
2421
|
}
|
|
2368
2422
|
/**
|
|
2369
2423
|
* List callable actions from the live registry-canonical catalog (definitions
|
|
@@ -2371,16 +2425,15 @@ var TopoloClient = class _TopoloClient {
|
|
|
2371
2425
|
* caller's organization and effective permissions; no bundled/static app
|
|
2372
2426
|
* action list is used as an authorization source.
|
|
2373
2427
|
*/
|
|
2374
|
-
async
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
return this.actionCatalogCache.entries;
|
|
2428
|
+
async searchActions(options) {
|
|
2429
|
+
if (!options.service?.trim()) {
|
|
2430
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2378
2431
|
}
|
|
2379
2432
|
const res = await this.request({
|
|
2380
2433
|
service: "developers",
|
|
2381
2434
|
method: "GET",
|
|
2382
2435
|
path: "/api/actions/catalog",
|
|
2383
|
-
|
|
2436
|
+
query: { service: options.service, ...catalogSearchQuery(options) }
|
|
2384
2437
|
});
|
|
2385
2438
|
const raw = res.data?.actions ?? res.actions;
|
|
2386
2439
|
if (!Array.isArray(raw)) {
|
|
@@ -2390,36 +2443,96 @@ var TopoloClient = class _TopoloClient {
|
|
|
2390
2443
|
);
|
|
2391
2444
|
}
|
|
2392
2445
|
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2446
|
+
return {
|
|
2447
|
+
actions: entries,
|
|
2448
|
+
page: normalizeCatalogPage(
|
|
2449
|
+
res.data?.page ?? res.page
|
|
2450
|
+
)
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
async getActionCatalogSummary(service) {
|
|
2454
|
+
const boundedService = service.trim();
|
|
2455
|
+
if (!boundedService) {
|
|
2456
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2398
2457
|
}
|
|
2399
|
-
|
|
2458
|
+
const res = await this.request({
|
|
2459
|
+
service: "developers",
|
|
2460
|
+
method: "GET",
|
|
2461
|
+
path: "/api/actions/catalog",
|
|
2462
|
+
query: { service: boundedService, summary: "true" }
|
|
2463
|
+
});
|
|
2464
|
+
const raw = res.data?.summary ?? res.summary;
|
|
2465
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2466
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
|
|
2467
|
+
}
|
|
2468
|
+
const record = raw;
|
|
2469
|
+
if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
|
|
2470
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
|
|
2471
|
+
}
|
|
2472
|
+
return {
|
|
2473
|
+
algorithm: "sha256-v1",
|
|
2474
|
+
digest: record.digest,
|
|
2475
|
+
actionCount: Number(record.actionCount)
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
/** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
|
|
2479
|
+
async listActions(options) {
|
|
2480
|
+
if (!options) {
|
|
2481
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2482
|
+
}
|
|
2483
|
+
return (await this.searchActions(options)).actions;
|
|
2400
2484
|
}
|
|
2401
2485
|
async getAction(action) {
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2404
|
-
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2405
|
-
if (!match) {
|
|
2486
|
+
const actionId = action.trim();
|
|
2487
|
+
if (!actionId) {
|
|
2406
2488
|
throw new TopoloSdkError(
|
|
2407
2489
|
"unknown_action",
|
|
2408
|
-
|
|
2490
|
+
"action id is required"
|
|
2409
2491
|
);
|
|
2410
2492
|
}
|
|
2493
|
+
let res;
|
|
2494
|
+
try {
|
|
2495
|
+
res = await this.request({
|
|
2496
|
+
service: "developers",
|
|
2497
|
+
method: "GET",
|
|
2498
|
+
path: "/api/actions/catalog",
|
|
2499
|
+
query: { actionId }
|
|
2500
|
+
});
|
|
2501
|
+
} catch (error) {
|
|
2502
|
+
if (error instanceof TopoloHttpError && error.status === 404) {
|
|
2503
|
+
throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2504
|
+
}
|
|
2505
|
+
throw error;
|
|
2506
|
+
}
|
|
2507
|
+
const raw = res.data?.action ?? res.action;
|
|
2508
|
+
const match = normalizeActionCatalogEntry(raw);
|
|
2509
|
+
if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2411
2510
|
return match;
|
|
2412
2511
|
}
|
|
2413
2512
|
async callAction(action, input2 = {}, options = {}) {
|
|
2414
2513
|
const entry = await this.getAction(action);
|
|
2514
|
+
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2515
|
+
if (agentAccess === "off") {
|
|
2516
|
+
throw new TopoloPermissionError(
|
|
2517
|
+
`Topolo action "${entry.actionId}" is not available for agent invocation.`,
|
|
2518
|
+
entry.requiredPermission ? [entry.requiredPermission] : []
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
|
|
2522
|
+
throw new TopoloConfirmationRequiredError(
|
|
2523
|
+
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2415
2526
|
const mapped = mapActionInput(entry, input2);
|
|
2416
2527
|
const service = entry.appSlug || entry.appId;
|
|
2417
2528
|
return this.request({
|
|
2418
2529
|
service,
|
|
2530
|
+
...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
|
|
2419
2531
|
method: entry.method,
|
|
2420
2532
|
path: mapped.path,
|
|
2421
2533
|
...mapped.query ? { query: mapped.query } : {},
|
|
2422
2534
|
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2535
|
+
...options.resource ? { resource: options.resource } : {},
|
|
2423
2536
|
headers: {
|
|
2424
2537
|
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2425
2538
|
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
@@ -2557,6 +2670,18 @@ function serviceKeyCandidates2(slug) {
|
|
|
2557
2670
|
function normalizeLookupKey(value) {
|
|
2558
2671
|
return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
|
|
2559
2672
|
}
|
|
2673
|
+
function requireHttpOrigin(value) {
|
|
2674
|
+
let url;
|
|
2675
|
+
try {
|
|
2676
|
+
url = new URL(value);
|
|
2677
|
+
} catch {
|
|
2678
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2679
|
+
}
|
|
2680
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
|
|
2681
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2682
|
+
}
|
|
2683
|
+
return url.origin;
|
|
2684
|
+
}
|
|
2560
2685
|
function deriveDevelopersFromAuth(authUrl) {
|
|
2561
2686
|
if (!authUrl) return null;
|
|
2562
2687
|
try {
|
|
@@ -2583,6 +2708,47 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2583
2708
|
}
|
|
2584
2709
|
return [...keys];
|
|
2585
2710
|
}
|
|
2711
|
+
function catalogSearchQuery(options) {
|
|
2712
|
+
const query = {};
|
|
2713
|
+
if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
|
|
2714
|
+
if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
|
|
2715
|
+
if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
|
|
2716
|
+
return query;
|
|
2717
|
+
}
|
|
2718
|
+
function normalizeCatalogPage(value) {
|
|
2719
|
+
if (!isRecord(value)) return { nextCursor: null, hasMore: false };
|
|
2720
|
+
return {
|
|
2721
|
+
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2722
|
+
hasMore: value.hasMore === true
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
function normalizeServiceCatalogEntry(item) {
|
|
2726
|
+
if (!isRecord(item)) return null;
|
|
2727
|
+
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2728
|
+
if (!appId) return null;
|
|
2729
|
+
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
2730
|
+
(permission) => typeof permission === "string" && permission.trim().length > 0
|
|
2731
|
+
) : [];
|
|
2732
|
+
const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
|
|
2733
|
+
item.api_key_resources ?? item.apiKeyResources
|
|
2734
|
+
);
|
|
2735
|
+
const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
|
|
2736
|
+
const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
|
|
2737
|
+
return {
|
|
2738
|
+
appId,
|
|
2739
|
+
slug: typeof item.app_slug === "string" ? item.app_slug : null,
|
|
2740
|
+
name: typeof item.name === "string" ? item.name : null,
|
|
2741
|
+
description: typeof item.description === "string" ? item.description : null,
|
|
2742
|
+
apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
|
|
2743
|
+
launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
|
|
2744
|
+
status: typeof item.status === "string" ? item.status : null,
|
|
2745
|
+
surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
|
|
2746
|
+
launchable: item.launchable === true,
|
|
2747
|
+
permissions,
|
|
2748
|
+
apiKeyResource,
|
|
2749
|
+
apiKeyResources
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2586
2752
|
function normalizeActionCatalogEntry(item) {
|
|
2587
2753
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2588
2754
|
const a = item;
|
|
@@ -2596,6 +2762,8 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2596
2762
|
}
|
|
2597
2763
|
return {
|
|
2598
2764
|
actionId,
|
|
2765
|
+
target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
|
|
2766
|
+
apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
|
|
2599
2767
|
name,
|
|
2600
2768
|
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2601
2769
|
title: pickActionString(a.title) ?? name,
|
|
@@ -2607,6 +2775,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2607
2775
|
path: path4,
|
|
2608
2776
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2609
2777
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2778
|
+
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
2610
2779
|
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2611
2780
|
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2612
2781
|
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
@@ -2626,16 +2795,12 @@ function pickActionString(value) {
|
|
|
2626
2795
|
function pickAgentAccess(value) {
|
|
2627
2796
|
return value === "auto" || value === "confirm" || value === "off" ? value : null;
|
|
2628
2797
|
}
|
|
2798
|
+
function pickAuthorizationMode(value) {
|
|
2799
|
+
return value === "runtime" ? "runtime" : "static";
|
|
2800
|
+
}
|
|
2629
2801
|
function plainObject(value) {
|
|
2630
2802
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2631
2803
|
}
|
|
2632
|
-
function normalizeActionLookupKey(value) {
|
|
2633
|
-
return String(value || "").trim().toLowerCase();
|
|
2634
|
-
}
|
|
2635
|
-
function actionEntryMatches(entry, needle) {
|
|
2636
|
-
if (!needle) return false;
|
|
2637
|
-
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2638
|
-
}
|
|
2639
2804
|
function mapActionInput(action, input2) {
|
|
2640
2805
|
const remaining = { ...input2 || {} };
|
|
2641
2806
|
const path4 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
@@ -3039,6 +3204,9 @@ var TopoloOAuth2 = class {
|
|
|
3039
3204
|
return parsed;
|
|
3040
3205
|
}
|
|
3041
3206
|
};
|
|
3207
|
+
function contextKey(contextType, organizationId) {
|
|
3208
|
+
return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
|
|
3209
|
+
}
|
|
3042
3210
|
var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
|
|
3043
3211
|
production: {
|
|
3044
3212
|
developers: "https://developers.topolo.app"
|
|
@@ -3132,6 +3300,28 @@ function writeProfileConfig(config, environment, profile) {
|
|
|
3132
3300
|
};
|
|
3133
3301
|
return { ...config, profiles };
|
|
3134
3302
|
}
|
|
3303
|
+
function writeContextCredential(config, environment, ctx, makeActive = true) {
|
|
3304
|
+
const key = contextKey(ctx.contextType, ctx.organizationId);
|
|
3305
|
+
const profile = profileForEnvironment(config, environment);
|
|
3306
|
+
const contexts = { ...profile.contexts ?? {}, [key]: ctx };
|
|
3307
|
+
const next = { contexts };
|
|
3308
|
+
if (makeActive) {
|
|
3309
|
+
next.activeContext = key;
|
|
3310
|
+
next.credential = ctx.credential;
|
|
3311
|
+
next.oauthClientId = ctx.oauthClientId ?? profile.oauthClientId;
|
|
3312
|
+
}
|
|
3313
|
+
return writeProfileConfig(config, environment, next);
|
|
3314
|
+
}
|
|
3315
|
+
function activateContext(config, environment, key) {
|
|
3316
|
+
const profile = profileForEnvironment(config, environment);
|
|
3317
|
+
const ctx = profile.contexts?.[key];
|
|
3318
|
+
if (!ctx) return config;
|
|
3319
|
+
return writeProfileConfig(config, environment, {
|
|
3320
|
+
activeContext: key,
|
|
3321
|
+
credential: ctx.credential,
|
|
3322
|
+
oauthClientId: ctx.oauthClientId ?? profile.oauthClientId
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3135
3325
|
function clearProfileCredential(config, environment) {
|
|
3136
3326
|
const next = { ...config };
|
|
3137
3327
|
if (environment === "production") {
|
|
@@ -3163,7 +3353,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
3163
3353
|
return profileForEnvironment(config, environment).credential ?? null;
|
|
3164
3354
|
}
|
|
3165
3355
|
var REFRESH_SKEW_SECONDS = 60;
|
|
3166
|
-
async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
3356
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
3167
3357
|
const prefix = envVarPrefix(environment);
|
|
3168
3358
|
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
3169
3359
|
return resolveCredential(environment);
|
|
@@ -3174,7 +3364,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
3174
3364
|
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
3175
3365
|
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
3176
3366
|
const now = Math.floor(Date.now() / 1e3);
|
|
3177
|
-
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
3367
|
+
if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
3178
3368
|
try {
|
|
3179
3369
|
const oauth = new TopoloOAuth2({
|
|
3180
3370
|
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
@@ -3189,9 +3379,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
3189
3379
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
3190
3380
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
3191
3381
|
};
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3382
|
+
const activeContext = profile.activeContext;
|
|
3383
|
+
const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
|
|
3384
|
+
const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
|
|
3385
|
+
credential: refreshed,
|
|
3386
|
+
contexts: {
|
|
3387
|
+
...profile.contexts ?? {},
|
|
3388
|
+
[activeContext]: {
|
|
3389
|
+
...activeEntry,
|
|
3390
|
+
credential: refreshed,
|
|
3391
|
+
oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
}) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
|
|
3395
|
+
await writeConfig(updated);
|
|
3195
3396
|
return refreshed;
|
|
3196
3397
|
} catch {
|
|
3197
3398
|
return cred;
|
|
@@ -3243,7 +3444,14 @@ function oauthClientRegistrationUrl(environment = selectedEnvironment()) {
|
|
|
3243
3444
|
function apiKeyIssuanceUrl(environment = selectedEnvironment()) {
|
|
3244
3445
|
return `${developerConsoleUrl(environment)}/credentials`;
|
|
3245
3446
|
}
|
|
3246
|
-
var DEFAULT_CLI_CLIENT_IDS = {
|
|
3447
|
+
var DEFAULT_CLI_CLIENT_IDS = {
|
|
3448
|
+
// First-party `topolo-cli` device-flow client, seeded per environment by Auth
|
|
3449
|
+
// (core/TopoloAuth/scripts/seed-first-party-oauth-clients.sql). Each env's
|
|
3450
|
+
// Auth must have this client before the CLI ships pointing at it.
|
|
3451
|
+
production: "topolo-cli",
|
|
3452
|
+
staging: "topolo-cli",
|
|
3453
|
+
development: "topolo-cli"
|
|
3454
|
+
};
|
|
3247
3455
|
function resolveOAuthClientId(config, environment = selectedEnvironment()) {
|
|
3248
3456
|
const envSpecific = process.env[`${envVarPrefix(environment)}_CLI_CLIENT_ID`];
|
|
3249
3457
|
if (envSpecific) return envSpecific;
|
|
@@ -3301,18 +3509,43 @@ function normalizeError(err) {
|
|
|
3301
3509
|
const code = err.code;
|
|
3302
3510
|
const hintMap = {
|
|
3303
3511
|
auth_error: "Run `topolo auth login` or set TOPOLO_API_KEY.",
|
|
3512
|
+
confirmation_required: "Review the action and rerun with --confirm to allow it.",
|
|
3304
3513
|
permission_denied: "Check your API key scopes or role assignments in TopoloDevelopers."
|
|
3305
3514
|
};
|
|
3306
3515
|
return {
|
|
3307
3516
|
message: err.message,
|
|
3308
3517
|
code,
|
|
3309
3518
|
hint: code ? hintMap[code] : void 0,
|
|
3310
|
-
exitCode: code === "auth_error" ? 2 : code === "permission_denied" ? 3 : 1
|
|
3519
|
+
exitCode: code === "auth_error" || code === "confirmation_required" ? 2 : code === "permission_denied" ? 3 : 1
|
|
3311
3520
|
};
|
|
3312
3521
|
}
|
|
3313
3522
|
return { message: String(err), exitCode: 1 };
|
|
3314
3523
|
}
|
|
3315
3524
|
|
|
3525
|
+
// src/version.ts
|
|
3526
|
+
import { readFileSync } from "fs";
|
|
3527
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
3528
|
+
import { fileURLToPath } from "url";
|
|
3529
|
+
function readCliVersion() {
|
|
3530
|
+
try {
|
|
3531
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
3532
|
+
const candidates = [
|
|
3533
|
+
join2(here, "package.json"),
|
|
3534
|
+
join2(here, "..", "package.json")
|
|
3535
|
+
];
|
|
3536
|
+
for (const path4 of candidates) {
|
|
3537
|
+
try {
|
|
3538
|
+
const pkg = JSON.parse(readFileSync(path4, "utf8"));
|
|
3539
|
+
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
3540
|
+
} catch {
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
} catch {
|
|
3544
|
+
}
|
|
3545
|
+
return "0.0.0";
|
|
3546
|
+
}
|
|
3547
|
+
var CLI_VERSION = readCliVersion();
|
|
3548
|
+
|
|
3316
3549
|
// src/commands/auth.ts
|
|
3317
3550
|
function registerAuthCommands(program2) {
|
|
3318
3551
|
const auth = program2.command("auth").description("Manage Topolo credentials");
|
|
@@ -3450,18 +3683,15 @@ async function runDeviceFlow(options) {
|
|
|
3450
3683
|
} finally {
|
|
3451
3684
|
spinner?.stop();
|
|
3452
3685
|
}
|
|
3686
|
+
const credential = {
|
|
3687
|
+
kind: "access_token",
|
|
3688
|
+
accessToken: token.access_token,
|
|
3689
|
+
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
3690
|
+
expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
|
|
3691
|
+
};
|
|
3692
|
+
const context = await resolveLoginContext(credential, resolved.serviceUrls, options.clientId);
|
|
3453
3693
|
const updated = await readConfig();
|
|
3454
|
-
await writeConfig(
|
|
3455
|
-
writeProfileConfig(updated, options.environment, {
|
|
3456
|
-
credential: {
|
|
3457
|
-
kind: "access_token",
|
|
3458
|
-
accessToken: token.access_token,
|
|
3459
|
-
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
3460
|
-
expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
|
|
3461
|
-
},
|
|
3462
|
-
oauthClientId: options.clientId
|
|
3463
|
-
})
|
|
3464
|
-
);
|
|
3694
|
+
await writeConfig(writeContextCredential(updated, options.environment, context));
|
|
3465
3695
|
emit(
|
|
3466
3696
|
{
|
|
3467
3697
|
ok: true,
|
|
@@ -3476,6 +3706,29 @@ async function runDeviceFlow(options) {
|
|
|
3476
3706
|
() => "\n \u2713 Signed in. Run `topolo whoami` to see your identity."
|
|
3477
3707
|
);
|
|
3478
3708
|
}
|
|
3709
|
+
async function resolveLoginContext(credential, serviceUrls, clientId) {
|
|
3710
|
+
try {
|
|
3711
|
+
const topolo = createTopolo({
|
|
3712
|
+
credential,
|
|
3713
|
+
agent: { clientName: "topolo-cli", clientVersion: CLI_VERSION },
|
|
3714
|
+
serviceUrls
|
|
3715
|
+
});
|
|
3716
|
+
const me = await topolo.identity.whoami();
|
|
3717
|
+
const org = me.organization;
|
|
3718
|
+
if (org?.id) {
|
|
3719
|
+
return {
|
|
3720
|
+
contextType: "organization",
|
|
3721
|
+
organizationId: org.id,
|
|
3722
|
+
organizationSlug: org.slug,
|
|
3723
|
+
organizationName: org.name,
|
|
3724
|
+
credential,
|
|
3725
|
+
oauthClientId: clientId
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
} catch {
|
|
3729
|
+
}
|
|
3730
|
+
return { contextType: "personal", credential, oauthClientId: clientId };
|
|
3731
|
+
}
|
|
3479
3732
|
function tryOpenBrowser(url) {
|
|
3480
3733
|
try {
|
|
3481
3734
|
const os = platform2();
|
|
@@ -3540,30 +3793,6 @@ function sleep(ms) {
|
|
|
3540
3793
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
3541
3794
|
}
|
|
3542
3795
|
|
|
3543
|
-
// src/version.ts
|
|
3544
|
-
import { readFileSync } from "fs";
|
|
3545
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
3546
|
-
import { fileURLToPath } from "url";
|
|
3547
|
-
function readCliVersion() {
|
|
3548
|
-
try {
|
|
3549
|
-
const here = dirname2(fileURLToPath(import.meta.url));
|
|
3550
|
-
const candidates = [
|
|
3551
|
-
join2(here, "package.json"),
|
|
3552
|
-
join2(here, "..", "package.json")
|
|
3553
|
-
];
|
|
3554
|
-
for (const path4 of candidates) {
|
|
3555
|
-
try {
|
|
3556
|
-
const pkg = JSON.parse(readFileSync(path4, "utf8"));
|
|
3557
|
-
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
3558
|
-
} catch {
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
} catch {
|
|
3562
|
-
}
|
|
3563
|
-
return "0.0.0";
|
|
3564
|
-
}
|
|
3565
|
-
var CLI_VERSION = readCliVersion();
|
|
3566
|
-
|
|
3567
3796
|
// src/client.ts
|
|
3568
3797
|
async function buildClient(options) {
|
|
3569
3798
|
const environment = selectedEnvironment();
|
|
@@ -3580,6 +3809,7 @@ async function buildClient(options) {
|
|
|
3580
3809
|
},
|
|
3581
3810
|
serviceUrls: profile.serviceUrls,
|
|
3582
3811
|
requireConfirmForWrites: options?.requireConfirmForWrites,
|
|
3812
|
+
...credential.kind === "access_token" && credential.refreshToken ? { refreshCredential: () => refreshCredentialIfNeeded(environment, { force: true }) } : {},
|
|
3583
3813
|
...isDebugEnabled() ? { debug: stderrDebugLogger } : {}
|
|
3584
3814
|
});
|
|
3585
3815
|
}
|
|
@@ -3642,93 +3872,6 @@ function formatPretty(info) {
|
|
|
3642
3872
|
return lines.join("\n");
|
|
3643
3873
|
}
|
|
3644
3874
|
|
|
3645
|
-
// src/commands/actions.ts
|
|
3646
|
-
function registerActionsCommand(program2) {
|
|
3647
|
-
const actions = program2.command("actions").description("List and call permission-scoped Topolo actions from the live catalog").option("-s, --service <service>", "Filter by app id, slug, or name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
|
|
3648
|
-
const mode = resolveOutputMode(opts.json);
|
|
3649
|
-
try {
|
|
3650
|
-
const topolo = await buildClient();
|
|
3651
|
-
if (!topolo) {
|
|
3652
|
-
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3653
|
-
}
|
|
3654
|
-
const rows = await topolo.client.listActions({ service: opts.service });
|
|
3655
|
-
emit({ actions: rows }, mode, () => formatActionRows(rows));
|
|
3656
|
-
} catch (err) {
|
|
3657
|
-
fail(err, mode);
|
|
3658
|
-
}
|
|
3659
|
-
});
|
|
3660
|
-
actions.command("get <action>").description("Describe one Topolo action by action id, name, or tool name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (action, opts) => {
|
|
3661
|
-
const mode = resolveOutputMode(opts.json);
|
|
3662
|
-
try {
|
|
3663
|
-
const topolo = await buildClient();
|
|
3664
|
-
if (!topolo) {
|
|
3665
|
-
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3666
|
-
}
|
|
3667
|
-
const entry = await topolo.client.getAction(action);
|
|
3668
|
-
emit(entry, mode, () => formatActionDetail(entry));
|
|
3669
|
-
} catch (err) {
|
|
3670
|
-
fail(err, mode);
|
|
3671
|
-
}
|
|
3672
|
-
});
|
|
3673
|
-
actions.command("call <action>").description("Call a permission-scoped Topolo action").option("-d, --data <json>", "JSON action input", "{}").option("--confirm", "Acknowledge a mutating action").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (action, opts) => {
|
|
3674
|
-
const mode = resolveOutputMode(opts.json);
|
|
3675
|
-
try {
|
|
3676
|
-
const topolo = await buildClient();
|
|
3677
|
-
if (!topolo) {
|
|
3678
|
-
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3679
|
-
}
|
|
3680
|
-
const input2 = parseJsonObject(opts.data || "{}");
|
|
3681
|
-
const result = await topolo.client.callAction(action, input2, { confirm: opts.confirm === true });
|
|
3682
|
-
emit(result, mode, () => JSON.stringify(result, null, 2));
|
|
3683
|
-
} catch (err) {
|
|
3684
|
-
fail(err, mode);
|
|
3685
|
-
}
|
|
3686
|
-
});
|
|
3687
|
-
}
|
|
3688
|
-
function parseJsonObject(value) {
|
|
3689
|
-
let parsed;
|
|
3690
|
-
try {
|
|
3691
|
-
parsed = JSON.parse(value);
|
|
3692
|
-
} catch {
|
|
3693
|
-
throw new Error("--data must be valid JSON");
|
|
3694
|
-
}
|
|
3695
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3696
|
-
throw new Error("--data must be a JSON object");
|
|
3697
|
-
}
|
|
3698
|
-
return parsed;
|
|
3699
|
-
}
|
|
3700
|
-
function formatActionRows(rows) {
|
|
3701
|
-
if (rows.length === 0) return "No actions available for this credential.";
|
|
3702
|
-
const serviceWidth = Math.max(7, ...rows.map((row) => (row.appSlug || row.appId).length));
|
|
3703
|
-
const nameWidth = Math.max(6, ...rows.map((row) => row.name.length));
|
|
3704
|
-
const methodWidth = Math.max(6, ...rows.map((row) => row.method.length));
|
|
3705
|
-
const lines = [
|
|
3706
|
-
`${"SERVICE".padEnd(serviceWidth)} ${"ACTION".padEnd(nameWidth)} ${"METHOD".padEnd(methodWidth)} CONFIRM PERMISSION`,
|
|
3707
|
-
`${"-".repeat(serviceWidth)} ${"-".repeat(nameWidth)} ${"-".repeat(methodWidth)} ------- ----------`
|
|
3708
|
-
];
|
|
3709
|
-
for (const row of rows) {
|
|
3710
|
-
const confirmation = row.requiresConfirmation ? "yes" : "no";
|
|
3711
|
-
lines.push(
|
|
3712
|
-
`${(row.appSlug || row.appId).padEnd(serviceWidth)} ${row.name.padEnd(nameWidth)} ${row.method.padEnd(methodWidth)} ${confirmation.padEnd(7)} ${row.requiredPermission}`
|
|
3713
|
-
);
|
|
3714
|
-
}
|
|
3715
|
-
return lines.join("\n");
|
|
3716
|
-
}
|
|
3717
|
-
function formatActionDetail(action) {
|
|
3718
|
-
return [
|
|
3719
|
-
`Action: ${action.name}`,
|
|
3720
|
-
`ID: ${action.actionId}`,
|
|
3721
|
-
`Tool: ${action.toolName || "-"}`,
|
|
3722
|
-
`Service: ${action.appSlug || action.appId}`,
|
|
3723
|
-
`Method: ${action.method}`,
|
|
3724
|
-
`Path: ${action.path}`,
|
|
3725
|
-
`Permission: ${action.requiredPermission}`,
|
|
3726
|
-
`Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
|
|
3727
|
-
"",
|
|
3728
|
-
action.description
|
|
3729
|
-
].join("\n");
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
3875
|
// src/commands/api.ts
|
|
3733
3876
|
var STATIC_APP_IDS = new Set(Object.keys(DEFAULT_APP_URLS));
|
|
3734
3877
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
@@ -3832,6 +3975,113 @@ function parseQueryPairs(pairs) {
|
|
|
3832
3975
|
return out;
|
|
3833
3976
|
}
|
|
3834
3977
|
|
|
3978
|
+
// src/commands/actions.ts
|
|
3979
|
+
function registerActionsCommand(program2) {
|
|
3980
|
+
const actions = program2.command("actions").description("Search permission-scoped Topolo actions for one application").option("-s, --service <service>", "App id, slug, or name (required for search)").option("-q, --query <query>", "Filter by action id, name, or title").option("--cursor <cursor>", "Continuation cursor from the previous page").option("--limit <limit>", "Page size (1-100)", "25").option("--json", "Output JSON").option("--no-json", "Output human-readable table").action(async (opts) => {
|
|
3981
|
+
const mode = resolveOutputMode(opts.json);
|
|
3982
|
+
try {
|
|
3983
|
+
const topolo = await buildClient();
|
|
3984
|
+
if (!topolo) {
|
|
3985
|
+
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3986
|
+
}
|
|
3987
|
+
if (!opts.service?.trim()) throw new Error("--service is required for action search");
|
|
3988
|
+
const result = await topolo.client.searchActions({
|
|
3989
|
+
service: opts.service.trim(),
|
|
3990
|
+
query: opts.query,
|
|
3991
|
+
cursor: opts.cursor,
|
|
3992
|
+
limit: parsePageSize(opts.limit)
|
|
3993
|
+
});
|
|
3994
|
+
emit(result, mode, () => formatActionRows(result.actions, result.page.nextCursor));
|
|
3995
|
+
} catch (err) {
|
|
3996
|
+
fail(err, mode);
|
|
3997
|
+
}
|
|
3998
|
+
});
|
|
3999
|
+
actions.command("get <action>").description("Describe one Topolo action by exact action id").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
4000
|
+
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
4001
|
+
try {
|
|
4002
|
+
const topolo = await buildClient();
|
|
4003
|
+
if (!topolo) {
|
|
4004
|
+
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
4005
|
+
}
|
|
4006
|
+
const entry = await topolo.client.getAction(action);
|
|
4007
|
+
emit(entry, mode, () => formatActionDetail(entry));
|
|
4008
|
+
} catch (err) {
|
|
4009
|
+
fail(err, mode);
|
|
4010
|
+
}
|
|
4011
|
+
});
|
|
4012
|
+
actions.command("call <action>").description("Call a permission-scoped Topolo action").option("-d, --data <json>", "JSON action input", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Acknowledge a mutating action").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
4013
|
+
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
4014
|
+
try {
|
|
4015
|
+
const topolo = await buildClient();
|
|
4016
|
+
if (!topolo) {
|
|
4017
|
+
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
4018
|
+
}
|
|
4019
|
+
const input2 = parseJsonObject(opts.data || "{}");
|
|
4020
|
+
const resource = parseResourceOption(opts.resourceType, opts.resourceId);
|
|
4021
|
+
const result = await topolo.client.callAction(action, input2, {
|
|
4022
|
+
confirm: opts.confirm === true,
|
|
4023
|
+
...resource ? { resource } : {}
|
|
4024
|
+
});
|
|
4025
|
+
emit(result, mode, () => JSON.stringify(result, null, 2));
|
|
4026
|
+
} catch (err) {
|
|
4027
|
+
fail(err, mode);
|
|
4028
|
+
}
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
function parseJsonObject(value) {
|
|
4032
|
+
let parsed;
|
|
4033
|
+
try {
|
|
4034
|
+
parsed = JSON.parse(value);
|
|
4035
|
+
} catch {
|
|
4036
|
+
throw new Error("--data must be valid JSON");
|
|
4037
|
+
}
|
|
4038
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4039
|
+
throw new Error("--data must be a JSON object");
|
|
4040
|
+
}
|
|
4041
|
+
return parsed;
|
|
4042
|
+
}
|
|
4043
|
+
function formatActionRows(rows, nextCursor = null) {
|
|
4044
|
+
if (rows.length === 0) return "No actions available for this credential.";
|
|
4045
|
+
const serviceWidth = Math.max(7, ...rows.map((row) => (row.appSlug || row.appId).length));
|
|
4046
|
+
const nameWidth = Math.max(6, ...rows.map((row) => row.name.length));
|
|
4047
|
+
const methodWidth = Math.max(6, ...rows.map((row) => row.method.length));
|
|
4048
|
+
const lines = [
|
|
4049
|
+
`${"SERVICE".padEnd(serviceWidth)} ${"ACTION".padEnd(nameWidth)} ${"METHOD".padEnd(methodWidth)} CONFIRM PERMISSION`,
|
|
4050
|
+
`${"-".repeat(serviceWidth)} ${"-".repeat(nameWidth)} ${"-".repeat(methodWidth)} ------- ----------`
|
|
4051
|
+
];
|
|
4052
|
+
for (const row of rows) {
|
|
4053
|
+
const confirmation = row.requiresConfirmation ? "yes" : "no";
|
|
4054
|
+
lines.push(
|
|
4055
|
+
`${(row.appSlug || row.appId).padEnd(serviceWidth)} ${row.name.padEnd(nameWidth)} ${row.method.padEnd(methodWidth)} ${confirmation.padEnd(7)} ${row.requiredPermission}`
|
|
4056
|
+
);
|
|
4057
|
+
}
|
|
4058
|
+
if (nextCursor) lines.push("", `Next cursor: ${nextCursor}`);
|
|
4059
|
+
return lines.join("\n");
|
|
4060
|
+
}
|
|
4061
|
+
function parsePageSize(value) {
|
|
4062
|
+
const parsed = Number.parseInt(value || "25", 10);
|
|
4063
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) {
|
|
4064
|
+
throw new Error("--limit must be an integer between 1 and 100");
|
|
4065
|
+
}
|
|
4066
|
+
return parsed;
|
|
4067
|
+
}
|
|
4068
|
+
function formatActionDetail(action) {
|
|
4069
|
+
return [
|
|
4070
|
+
`Action: ${action.name}`,
|
|
4071
|
+
`ID: ${action.actionId}`,
|
|
4072
|
+
`Tool: ${action.toolName || "-"}`,
|
|
4073
|
+
`Service: ${action.appSlug || action.appId}`,
|
|
4074
|
+
`Target: ${action.target || "-"}`,
|
|
4075
|
+
`API URL: ${action.apiBaseUrl || "-"}`,
|
|
4076
|
+
`Method: ${action.method}`,
|
|
4077
|
+
`Path: ${action.path}`,
|
|
4078
|
+
`Permission: ${action.requiredPermission}`,
|
|
4079
|
+
`Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
|
|
4080
|
+
"",
|
|
4081
|
+
action.description
|
|
4082
|
+
].join("\n");
|
|
4083
|
+
}
|
|
4084
|
+
|
|
3835
4085
|
// src/commands/apps.ts
|
|
3836
4086
|
import { execSync } from "child_process";
|
|
3837
4087
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
@@ -5120,7 +5370,7 @@ function packageJson(application, target, options) {
|
|
|
5120
5370
|
dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
|
|
5121
5371
|
scripts.deploy = "npm run build && wrangler deploy";
|
|
5122
5372
|
}
|
|
5123
|
-
devDependencies.wrangler = "^4.
|
|
5373
|
+
devDependencies.wrangler = "^4.91.0";
|
|
5124
5374
|
}
|
|
5125
5375
|
if (target === "platform" && hasScaffoldResources(options.resources)) {
|
|
5126
5376
|
scripts["check:deploy-config"] = "wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build && wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build";
|
|
@@ -5140,11 +5390,11 @@ function topoloPackageVersion(packageName, target) {
|
|
|
5140
5390
|
if (target === "developer") {
|
|
5141
5391
|
if (packageName === "@topolo/login") return "^0.1.0";
|
|
5142
5392
|
if (packageName === "@topolo-io/auth-client") return "^1.0.0";
|
|
5143
|
-
if (packageName === "@topolo/sdk") return "^0.
|
|
5393
|
+
if (packageName === "@topolo/sdk") return "^0.8.1";
|
|
5144
5394
|
return "^0.1.0";
|
|
5145
5395
|
}
|
|
5146
5396
|
const platformPackageVersions = {
|
|
5147
|
-
"@topolo/sdk": "^0.
|
|
5397
|
+
"@topolo/sdk": "^0.8.1",
|
|
5148
5398
|
"@topolo/login": "^0.1.0",
|
|
5149
5399
|
"@topolo-io/app-funnel": "^0.1.6",
|
|
5150
5400
|
"@topolo-io/app-launcher": "^0.1.15",
|
|
@@ -8372,15 +8622,20 @@ var CLOUDCONTROL_LOCAL_APPLICATION = {
|
|
|
8372
8622
|
deployTargets: []
|
|
8373
8623
|
};
|
|
8374
8624
|
function registerAppsCommand(program2) {
|
|
8375
|
-
const apps = program2.command("apps").description("
|
|
8625
|
+
const apps = program2.command("apps").description("Search Topolo platform applications available to the current credential").option("-q, --query <query>", "Filter by app id, slug, or name").option("--cursor <cursor>", "Continuation cursor from the previous page").option("--limit <limit>", "Page size (1-100)", "25").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
|
|
8376
8626
|
const mode = resolveOutputMode(opts.json);
|
|
8377
8627
|
try {
|
|
8378
8628
|
const topolo = await buildClient();
|
|
8379
8629
|
if (!topolo) {
|
|
8380
8630
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
8381
8631
|
}
|
|
8382
|
-
const
|
|
8383
|
-
|
|
8632
|
+
const result = await topolo.client.searchServices({
|
|
8633
|
+
query: opts.query,
|
|
8634
|
+
cursor: opts.cursor,
|
|
8635
|
+
limit: parsePageSize2(opts.limit)
|
|
8636
|
+
});
|
|
8637
|
+
const rows = applicationRowsFromCatalog(result.services);
|
|
8638
|
+
emit({ applications: rows, page: result.page }, mode, () => formatRows(rows));
|
|
8384
8639
|
} catch (err) {
|
|
8385
8640
|
fail(err, mode);
|
|
8386
8641
|
}
|
|
@@ -8392,7 +8647,8 @@ function registerAppsCommand(program2) {
|
|
|
8392
8647
|
if (!topolo) {
|
|
8393
8648
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
8394
8649
|
}
|
|
8395
|
-
const
|
|
8650
|
+
const entry = await topolo.client.getService(application);
|
|
8651
|
+
const app = applicationRowsFromCatalog([entry])[0];
|
|
8396
8652
|
emit({ application: app }, mode, () => formatApplicationRow(app));
|
|
8397
8653
|
} catch (err) {
|
|
8398
8654
|
fail(err, mode);
|
|
@@ -8512,16 +8768,12 @@ function applicationRowsFromCatalog(catalog) {
|
|
|
8512
8768
|
deployTargets: 0
|
|
8513
8769
|
})).sort((left, right) => left.id.localeCompare(right.id));
|
|
8514
8770
|
}
|
|
8515
|
-
function
|
|
8516
|
-
const
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
(row) => row.id.toLowerCase() === needle || row.name.toLowerCase() === needle || row.services.some((service) => service.toLowerCase() === needle)
|
|
8520
|
-
);
|
|
8521
|
-
if (!match) {
|
|
8522
|
-
throw new Error(`Unknown application "${application}". Run \`topolo apps --json\` to discover applications available to this credential.`);
|
|
8771
|
+
function parsePageSize2(value) {
|
|
8772
|
+
const parsed = Number.parseInt(value || "25", 10);
|
|
8773
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) {
|
|
8774
|
+
throw new Error("--limit must be an integer between 1 and 100");
|
|
8523
8775
|
}
|
|
8524
|
-
return
|
|
8776
|
+
return parsed;
|
|
8525
8777
|
}
|
|
8526
8778
|
function resolveApplication(application) {
|
|
8527
8779
|
const id = application.trim().toLowerCase();
|
|
@@ -9074,7 +9326,7 @@ async function requirePlatformAdminSession() {
|
|
|
9074
9326
|
|
|
9075
9327
|
// src/commands/services.ts
|
|
9076
9328
|
function registerServicesCommand(program2) {
|
|
9077
|
-
program2.command("services").description("
|
|
9329
|
+
program2.command("services").description("Search Topolo services the SDK can route to").option("-q, --query <query>", "Filter by app id, slug, or name").option("--cursor <cursor>", "Continuation cursor from the previous page").option("--limit <limit>", "Page size (1-100)", "25").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
|
|
9078
9330
|
const mode = resolveOutputMode(opts.json);
|
|
9079
9331
|
try {
|
|
9080
9332
|
const environment = selectedEnvironment();
|
|
@@ -9083,8 +9335,13 @@ function registerServicesCommand(program2) {
|
|
|
9083
9335
|
if (!topolo) {
|
|
9084
9336
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
9085
9337
|
}
|
|
9086
|
-
const
|
|
9087
|
-
|
|
9338
|
+
const result = await topolo.client.searchServices({
|
|
9339
|
+
query: opts.query,
|
|
9340
|
+
cursor: opts.cursor,
|
|
9341
|
+
limit: parsePageSize3(opts.limit)
|
|
9342
|
+
});
|
|
9343
|
+
const rows = buildRows(result.services, config.serviceUrls, environment);
|
|
9344
|
+
emit({ environment, services: rows, page: result.page }, mode, () => formatRows2(rows, environment, result.page.nextCursor));
|
|
9088
9345
|
} catch (err) {
|
|
9089
9346
|
fail(err, mode);
|
|
9090
9347
|
}
|
|
@@ -9111,7 +9368,7 @@ function buildRows(catalog, overrides, environment = selectedEnvironment()) {
|
|
|
9111
9368
|
};
|
|
9112
9369
|
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
9113
9370
|
}
|
|
9114
|
-
function formatRows2(rows, environment = selectedEnvironment()) {
|
|
9371
|
+
function formatRows2(rows, environment = selectedEnvironment(), nextCursor = null) {
|
|
9115
9372
|
const idWidth = Math.max(3, ...rows.map((r) => r.id.length));
|
|
9116
9373
|
const urlWidth = Math.max(7, ...rows.map((r) => (r.effectiveUrl ?? "-").length));
|
|
9117
9374
|
const lines = [];
|
|
@@ -9126,8 +9383,14 @@ function formatRows2(rows, environment = selectedEnvironment()) {
|
|
|
9126
9383
|
`${r.id.padEnd(idWidth)} ${effectiveUrl.padEnd(urlWidth)} ${r.overridden ? "yes" : "".padEnd(8)} ${r.environmentSafe ? "yes" : "no"} ${apiKeyResource}`
|
|
9127
9384
|
);
|
|
9128
9385
|
}
|
|
9386
|
+
if (nextCursor) lines.push("", `Next cursor: ${nextCursor}`);
|
|
9129
9387
|
return lines.join("\n");
|
|
9130
9388
|
}
|
|
9389
|
+
function parsePageSize3(value) {
|
|
9390
|
+
const parsed = Number.parseInt(value || "25", 10);
|
|
9391
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) throw new Error("--limit must be an integer between 1 and 100");
|
|
9392
|
+
return parsed;
|
|
9393
|
+
}
|
|
9131
9394
|
function isStagingUrl(value) {
|
|
9132
9395
|
const url = value.toLowerCase();
|
|
9133
9396
|
return url.includes(".stg.topolo.us") || url.includes("localhost") || url.includes("127.0.0.1") || url.includes("topolo-staging");
|
|
@@ -9900,6 +10163,167 @@ function formatStatus(rows) {
|
|
|
9900
10163
|
).join("\n");
|
|
9901
10164
|
}
|
|
9902
10165
|
|
|
10166
|
+
// src/commands/context.ts
|
|
10167
|
+
function makeClient(credential, serviceUrls) {
|
|
10168
|
+
return createTopolo({
|
|
10169
|
+
credential,
|
|
10170
|
+
agent: { clientName: "topolo-cli", clientVersion: CLI_VERSION },
|
|
10171
|
+
serviceUrls
|
|
10172
|
+
});
|
|
10173
|
+
}
|
|
10174
|
+
function unwrap(res) {
|
|
10175
|
+
const r = res ?? {};
|
|
10176
|
+
return r.data ?? r;
|
|
10177
|
+
}
|
|
10178
|
+
async function fetchMe(credential, serviceUrls) {
|
|
10179
|
+
const topolo = makeClient(credential, serviceUrls);
|
|
10180
|
+
const data = unwrap(await topolo.client.request({ service: "auth", method: "GET", path: "/api/auth/me" }));
|
|
10181
|
+
const user = data.user ?? {};
|
|
10182
|
+
const rawMemberships = user.organizationMemberships || data.organizations || [];
|
|
10183
|
+
const memberships = rawMemberships.filter((m) => m && m.id);
|
|
10184
|
+
const activeType = user.orgId ? "organization" : "personal";
|
|
10185
|
+
return { memberships, activeType, activeOrgId: user.orgId || void 0 };
|
|
10186
|
+
}
|
|
10187
|
+
function credentialFromTokens(tokens) {
|
|
10188
|
+
const accessToken = tokens.accessToken || tokens.access_token;
|
|
10189
|
+
const refreshToken = tokens.refreshToken || tokens.refresh_token;
|
|
10190
|
+
const expiresIn = tokens.expiresIn || tokens.expires_in;
|
|
10191
|
+
const expiresAtRaw = tokens.expiresAt || tokens.expires_at;
|
|
10192
|
+
return {
|
|
10193
|
+
kind: "access_token",
|
|
10194
|
+
accessToken,
|
|
10195
|
+
...refreshToken ? { refreshToken } : {},
|
|
10196
|
+
...expiresAtRaw ? { expiresAt: expiresAtRaw } : expiresIn ? { expiresAt: Math.floor(Date.now() / 1e3) + expiresIn } : {}
|
|
10197
|
+
};
|
|
10198
|
+
}
|
|
10199
|
+
async function exchangeForContext(credential, serviceUrls, target) {
|
|
10200
|
+
const topolo = makeClient(credential, serviceUrls);
|
|
10201
|
+
const data = unwrap(
|
|
10202
|
+
await topolo.client.request({
|
|
10203
|
+
service: "auth",
|
|
10204
|
+
method: "PUT",
|
|
10205
|
+
path: "/api/me/active-context",
|
|
10206
|
+
body: {
|
|
10207
|
+
contextType: target.type,
|
|
10208
|
+
contextId: target.type === "organization" ? target.id : null
|
|
10209
|
+
},
|
|
10210
|
+
confirm: true
|
|
10211
|
+
})
|
|
10212
|
+
);
|
|
10213
|
+
const tokens = data.tokens || {};
|
|
10214
|
+
return credentialFromTokens(tokens);
|
|
10215
|
+
}
|
|
10216
|
+
function labelFor(ctx) {
|
|
10217
|
+
if (ctx.contextType === "personal") return "Personal";
|
|
10218
|
+
return ctx.organizationName || ctx.organizationSlug || ctx.organizationId || "organization";
|
|
10219
|
+
}
|
|
10220
|
+
function registerContextCommand(program2) {
|
|
10221
|
+
const context = program2.command("context").description("Show and switch the active context (personal or an organization)");
|
|
10222
|
+
context.command("ls", { isDefault: true }).alias("list").description("List the contexts you can act in and which is active").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
10223
|
+
const mode = resolveOutputMode(opts.json);
|
|
10224
|
+
try {
|
|
10225
|
+
const environment = selectedEnvironment();
|
|
10226
|
+
const config = await readConfig();
|
|
10227
|
+
const profile = profileForEnvironment(config, environment);
|
|
10228
|
+
const stored = profile.contexts ?? {};
|
|
10229
|
+
const activeKey = profile.activeContext;
|
|
10230
|
+
const rows = /* @__PURE__ */ new Map();
|
|
10231
|
+
for (const [key, ctx] of Object.entries(stored)) {
|
|
10232
|
+
rows.set(key, { key, type: ctx.contextType, label: labelFor(ctx), signedIn: true, active: key === activeKey });
|
|
10233
|
+
}
|
|
10234
|
+
const cred = await refreshCredentialIfNeeded(environment);
|
|
10235
|
+
if (cred) {
|
|
10236
|
+
try {
|
|
10237
|
+
const me = await fetchMe(cred, resolvedProfileConfig(config, environment).serviceUrls);
|
|
10238
|
+
const personalKey = contextKey("personal");
|
|
10239
|
+
if (!rows.has(personalKey)) {
|
|
10240
|
+
rows.set(personalKey, { key: personalKey, type: "personal", label: "Personal", signedIn: false, active: personalKey === activeKey });
|
|
10241
|
+
}
|
|
10242
|
+
for (const m of me.memberships) {
|
|
10243
|
+
const key = contextKey("organization", m.id);
|
|
10244
|
+
if (!rows.has(key)) {
|
|
10245
|
+
rows.set(key, { key, type: "organization", label: m.name || m.slug || m.id, signedIn: false, active: key === activeKey });
|
|
10246
|
+
}
|
|
10247
|
+
}
|
|
10248
|
+
} catch {
|
|
10249
|
+
}
|
|
10250
|
+
}
|
|
10251
|
+
const list = [...rows.values()];
|
|
10252
|
+
emit({ environment, active: activeKey ?? null, contexts: list }, mode, () => {
|
|
10253
|
+
if (list.length === 0) return "No contexts yet. Run `topolo auth login` first.";
|
|
10254
|
+
const lines = [`Contexts (${environmentLabel(environment)}):`, ""];
|
|
10255
|
+
for (const r of list) {
|
|
10256
|
+
const marks = [r.active ? "active" : "", r.signedIn ? "signed in" : "available"].filter(Boolean).join(", ");
|
|
10257
|
+
lines.push(` ${r.active ? "*" : " "} ${r.label} (${r.type}) [${marks}]`);
|
|
10258
|
+
}
|
|
10259
|
+
lines.push("", "Switch with: topolo context use <organization|personal>");
|
|
10260
|
+
return lines.join("\n");
|
|
10261
|
+
});
|
|
10262
|
+
} catch (err) {
|
|
10263
|
+
fail(err, mode);
|
|
10264
|
+
}
|
|
10265
|
+
});
|
|
10266
|
+
context.command("use <target>").description('Switch the active context (an organization name/slug, or "personal")').option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (target, opts) => {
|
|
10267
|
+
const mode = resolveOutputMode(opts.json);
|
|
10268
|
+
try {
|
|
10269
|
+
const environment = selectedEnvironment();
|
|
10270
|
+
const config = await readConfig();
|
|
10271
|
+
const profile = profileForEnvironment(config, environment);
|
|
10272
|
+
const serviceUrls = resolvedProfileConfig(config, environment).serviceUrls;
|
|
10273
|
+
const cred = await refreshCredentialIfNeeded(environment);
|
|
10274
|
+
if (!cred) {
|
|
10275
|
+
throw new Error("Not signed in. Run `topolo auth login` first.");
|
|
10276
|
+
}
|
|
10277
|
+
const wantPersonal = /^(personal|me)$/i.test(target.trim());
|
|
10278
|
+
let resolved;
|
|
10279
|
+
if (wantPersonal) {
|
|
10280
|
+
resolved = { contextType: "personal" };
|
|
10281
|
+
} else {
|
|
10282
|
+
const me = await fetchMe(cred, serviceUrls);
|
|
10283
|
+
const needle = target.trim().toLowerCase();
|
|
10284
|
+
const match = me.memberships.find(
|
|
10285
|
+
(m) => m.id === target || m.slug?.toLowerCase() === needle || m.name?.toLowerCase() === needle
|
|
10286
|
+
);
|
|
10287
|
+
if (!match) {
|
|
10288
|
+
const names = me.memberships.map((m) => m.slug || m.name || m.id);
|
|
10289
|
+
throw new Error(
|
|
10290
|
+
`You are not a member of "${target}". Available: personal${names.length ? ", " + names.join(", ") : ""}.`
|
|
10291
|
+
);
|
|
10292
|
+
}
|
|
10293
|
+
resolved = {
|
|
10294
|
+
contextType: "organization",
|
|
10295
|
+
organizationId: match.id,
|
|
10296
|
+
organizationSlug: match.slug,
|
|
10297
|
+
organizationName: match.name
|
|
10298
|
+
};
|
|
10299
|
+
}
|
|
10300
|
+
const key = contextKey(resolved.contextType, resolved.organizationId);
|
|
10301
|
+
const existing = profile.contexts?.[key];
|
|
10302
|
+
let updated = config;
|
|
10303
|
+
if (existing?.credential) {
|
|
10304
|
+
updated = activateContext(config, environment, key);
|
|
10305
|
+
} else {
|
|
10306
|
+
const newCredential = await exchangeForContext(cred, serviceUrls, {
|
|
10307
|
+
type: resolved.contextType,
|
|
10308
|
+
id: resolved.organizationId
|
|
10309
|
+
});
|
|
10310
|
+
resolved.credential = newCredential;
|
|
10311
|
+
resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
|
|
10312
|
+
updated = writeContextCredential(config, environment, resolved, true);
|
|
10313
|
+
}
|
|
10314
|
+
await writeConfig(updated);
|
|
10315
|
+
emit(
|
|
10316
|
+
{ environment, active: key, context: { type: resolved.contextType, label: labelFor(resolved) } },
|
|
10317
|
+
mode,
|
|
10318
|
+
() => `
|
|
10319
|
+
\u2713 Now acting as ${labelFor(resolved)} (${resolved.contextType}).`
|
|
10320
|
+
);
|
|
10321
|
+
} catch (err) {
|
|
10322
|
+
fail(err, mode);
|
|
10323
|
+
}
|
|
10324
|
+
});
|
|
10325
|
+
}
|
|
10326
|
+
|
|
9903
10327
|
// src/index.ts
|
|
9904
10328
|
var program = new Command();
|
|
9905
10329
|
var argv;
|
|
@@ -9929,6 +10353,7 @@ registerPlatformCommand(program);
|
|
|
9929
10353
|
registerServicesCommand(program);
|
|
9930
10354
|
registerSeedCommands(program);
|
|
9931
10355
|
registerSkillsCommand(program);
|
|
10356
|
+
registerContextCommand(program);
|
|
9932
10357
|
program.parseAsync(argv).catch((err) => {
|
|
9933
10358
|
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
9934
10359
|
`);
|