@topolo/cli 0.1.25 → 0.1.27
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 +26 -17
- package/dist/index.js +222 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ via `stdin`/`stdout`.
|
|
|
10
10
|
- Access-token authentication via OAuth device flow when a CLI OAuth client id is configured.
|
|
11
11
|
- Stored access-token refresh when the config includes a refresh token and OAuth client id.
|
|
12
12
|
- First-class `production` and `staging` CLI environments with separate stored credentials, OAuth client ids, and service URL overrides.
|
|
13
|
-
- Commands: `auth login/logout/status`, `whoami`, `apps`, `apps scaffold`, `oauth clients`, `integrations prompt/check`, `services`,
|
|
13
|
+
- Commands: `auth login/logout/status`, `whoami`, `apps`, `apps scaffold`, `oauth clients`, `integrations prompt/check`, `services`, credential-scoped `actions`, and generic `api` passthrough for SDK-registered services.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -76,10 +76,12 @@ The platform logs these for traceability.
|
|
|
76
76
|
## Write-action gating
|
|
77
77
|
|
|
78
78
|
The SDK defaults to requiring an explicit `--confirm` flag on any mutating
|
|
79
|
-
request.
|
|
79
|
+
request. Dynamic action calls and the generic `topolo api` passthrough enforce
|
|
80
|
+
this too:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
82
|
-
topolo
|
|
83
|
+
topolo actions call topolo_mail_messages_send --data '{"mailboxId":"primary","subject":"Hello"}' --confirm
|
|
84
|
+
topolo api mail POST /api/mail/mailboxes/primary/messages --data '{"subject":"Hello"}' --confirm
|
|
83
85
|
```
|
|
84
86
|
|
|
85
87
|
## Commands
|
|
@@ -93,19 +95,6 @@ topolo whoami # pretty
|
|
|
93
95
|
topolo whoami --json # machine-readable
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
### `topolo crm contacts:list`
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
topolo crm contacts:list
|
|
100
|
-
topolo crm contacts:list -q "acme" -p 2 -s 50 --json
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### `topolo crm contacts:get <id>`
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
topolo crm contacts:get contact_abc123
|
|
107
|
-
```
|
|
108
|
-
|
|
109
98
|
### `topolo services`
|
|
110
99
|
|
|
111
100
|
Lists the services the current credential can use in its organization, with the
|
|
@@ -123,6 +112,26 @@ topolo services --env staging --json
|
|
|
123
112
|
catalog. New services become usable without a CLI release once Auth grants the
|
|
124
113
|
organization and credential the relevant permissions.
|
|
125
114
|
|
|
115
|
+
### `topolo actions`
|
|
116
|
+
|
|
117
|
+
Lists the callable actions available to the current credential. Applications
|
|
118
|
+
publish actions through Topolo Developers, Auth filters them by the same
|
|
119
|
+
service permissions already used by API keys, launchers, and backend services,
|
|
120
|
+
and the CLI calls them through the shared SDK.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
topolo actions --json
|
|
124
|
+
topolo actions --service mail --json
|
|
125
|
+
topolo actions get topolo_mail_messages_send --json
|
|
126
|
+
topolo actions call topolo_mail_messages_send \
|
|
127
|
+
--data '{"mailboxId":"primary","subject":"Hello","text":"..."}' \
|
|
128
|
+
--confirm --json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use `topolo actions` before `topolo api` when an app has published a machine
|
|
132
|
+
contract for the operation. `topolo api` remains the escape hatch for endpoints
|
|
133
|
+
that are not yet in the action catalog.
|
|
134
|
+
|
|
126
135
|
### `topolo apps`
|
|
127
136
|
|
|
128
137
|
Lists launchable Topolo applications available to the current credential. This
|
|
@@ -206,7 +215,7 @@ Escape hatch for endpoints without typed modules yet.
|
|
|
206
215
|
|
|
207
216
|
```bash
|
|
208
217
|
topolo api auth GET /api/me
|
|
209
|
-
topolo api
|
|
218
|
+
topolo api mail POST /api/mail/mailboxes/primary/messages --data '{"subject":"Hello"}' --confirm
|
|
210
219
|
```
|
|
211
220
|
|
|
212
221
|
Use `topolo services --json` to discover service ids permitted for the current
|
package/dist/index.js
CHANGED
|
@@ -2068,6 +2068,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2068
2068
|
fetchImpl;
|
|
2069
2069
|
debug;
|
|
2070
2070
|
serviceCatalogCache = null;
|
|
2071
|
+
actionCatalogCache = null;
|
|
2071
2072
|
/**
|
|
2072
2073
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
2073
2074
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2328,6 +2329,63 @@ var TopoloClient = class _TopoloClient {
|
|
|
2328
2329
|
};
|
|
2329
2330
|
return entries;
|
|
2330
2331
|
}
|
|
2332
|
+
/**
|
|
2333
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
2334
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
2335
|
+
* action list is used as an authorization source.
|
|
2336
|
+
*/
|
|
2337
|
+
async listActions(options = {}) {
|
|
2338
|
+
const now = Date.now();
|
|
2339
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
2340
|
+
return this.actionCatalogCache.entries;
|
|
2341
|
+
}
|
|
2342
|
+
const res = await this.request({
|
|
2343
|
+
service: "auth",
|
|
2344
|
+
method: "GET",
|
|
2345
|
+
path: "/api/actions/catalog",
|
|
2346
|
+
...options.service ? { query: { service: options.service } } : {}
|
|
2347
|
+
});
|
|
2348
|
+
const raw = res.data?.actions ?? res.actions;
|
|
2349
|
+
if (!Array.isArray(raw)) {
|
|
2350
|
+
throw new TopoloSdkError(
|
|
2351
|
+
"action_catalog_unavailable",
|
|
2352
|
+
"TopoloAuth did not return an action catalog for the current credential."
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2356
|
+
if (!options.service) {
|
|
2357
|
+
this.actionCatalogCache = {
|
|
2358
|
+
entries,
|
|
2359
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
return entries;
|
|
2363
|
+
}
|
|
2364
|
+
async getAction(action) {
|
|
2365
|
+
const needle = normalizeActionLookupKey(action);
|
|
2366
|
+
const actions = await this.listActions();
|
|
2367
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2368
|
+
if (!match) {
|
|
2369
|
+
throw new TopoloSdkError(
|
|
2370
|
+
"unknown_action",
|
|
2371
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
return match;
|
|
2375
|
+
}
|
|
2376
|
+
async callAction(action, input2 = {}, options = {}) {
|
|
2377
|
+
const entry = await this.getAction(action);
|
|
2378
|
+
const mapped = mapActionInput(entry, input2);
|
|
2379
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
2380
|
+
return this.request({
|
|
2381
|
+
service,
|
|
2382
|
+
method: entry.method,
|
|
2383
|
+
path: mapped.path,
|
|
2384
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2385
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2386
|
+
confirm: options.confirm === true
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2331
2389
|
/**
|
|
2332
2390
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
2333
2391
|
* for deciding what the current credential may use.
|
|
@@ -2437,6 +2495,98 @@ function isCallableCatalogEntry(entry) {
|
|
|
2437
2495
|
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2438
2496
|
);
|
|
2439
2497
|
}
|
|
2498
|
+
function normalizeActionCatalogEntry(item) {
|
|
2499
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2500
|
+
const a = item;
|
|
2501
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
2502
|
+
const name = pickActionString(a.name);
|
|
2503
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
2504
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
2505
|
+
const path4 = pickActionString(a.path);
|
|
2506
|
+
if (!actionId || !name || !serviceId || !method || !path4 || !isHttpMethod(method)) {
|
|
2507
|
+
return null;
|
|
2508
|
+
}
|
|
2509
|
+
return {
|
|
2510
|
+
actionId,
|
|
2511
|
+
name,
|
|
2512
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2513
|
+
title: pickActionString(a.title) ?? name,
|
|
2514
|
+
description: pickActionString(a.description) ?? "",
|
|
2515
|
+
serviceId,
|
|
2516
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
2517
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
2518
|
+
method,
|
|
2519
|
+
path: path4,
|
|
2520
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2521
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2522
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2523
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2524
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
2525
|
+
destructive: Boolean(a.destructive),
|
|
2526
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
2527
|
+
status: pickActionString(a.status),
|
|
2528
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision)
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
function isHttpMethod(value) {
|
|
2532
|
+
return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
|
|
2533
|
+
}
|
|
2534
|
+
function pickActionString(value) {
|
|
2535
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2536
|
+
}
|
|
2537
|
+
function plainObject(value) {
|
|
2538
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2539
|
+
}
|
|
2540
|
+
function normalizeActionLookupKey(value) {
|
|
2541
|
+
return String(value || "").trim().toLowerCase();
|
|
2542
|
+
}
|
|
2543
|
+
function actionEntryMatches(entry, needle) {
|
|
2544
|
+
if (!needle) return false;
|
|
2545
|
+
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2546
|
+
}
|
|
2547
|
+
function mapActionInput(action, input2) {
|
|
2548
|
+
const remaining = { ...input2 || {} };
|
|
2549
|
+
const path4 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
2550
|
+
const value = remaining[key];
|
|
2551
|
+
if (value === void 0 || value === null || value === "") {
|
|
2552
|
+
throw new TopoloSdkError(
|
|
2553
|
+
"invalid_action_input",
|
|
2554
|
+
`Action "${action.name}" requires path parameter "${key}".`
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
delete remaining[key];
|
|
2558
|
+
return encodeURIComponent(String(value));
|
|
2559
|
+
});
|
|
2560
|
+
const explicitQuery = plainObject(remaining.query);
|
|
2561
|
+
delete remaining.query;
|
|
2562
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
|
|
2563
|
+
delete remaining.body;
|
|
2564
|
+
if (action.method === "GET") {
|
|
2565
|
+
return {
|
|
2566
|
+
path: path4,
|
|
2567
|
+
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
const query = objectToQuery(explicitQuery);
|
|
2571
|
+
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
2572
|
+
return {
|
|
2573
|
+
path: path4,
|
|
2574
|
+
...query ? { query } : {},
|
|
2575
|
+
body
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
function objectToQuery(value) {
|
|
2579
|
+
const query = {};
|
|
2580
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
2581
|
+
if (raw === void 0 || raw === null) continue;
|
|
2582
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
2583
|
+
query[key] = raw;
|
|
2584
|
+
} else {
|
|
2585
|
+
query[key] = JSON.stringify(raw);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
return Object.keys(query).length > 0 ? query : void 0;
|
|
2589
|
+
}
|
|
2440
2590
|
function describeError(body, fallback) {
|
|
2441
2591
|
if (body && typeof body === "object") {
|
|
2442
2592
|
const maybe = body;
|
|
@@ -3198,78 +3348,90 @@ function formatPretty(info) {
|
|
|
3198
3348
|
return lines.join("\n");
|
|
3199
3349
|
}
|
|
3200
3350
|
|
|
3201
|
-
// src/commands/
|
|
3202
|
-
function
|
|
3203
|
-
const
|
|
3204
|
-
crm.command("contacts:list").description("List contacts in the authenticated organization").option("-q, --query <text>", "Full-text search").option("-p, --page <n>", "Page number (1-indexed)", (v) => parseInt(v, 10), 1).option("-s, --page-size <n>", "Page size", (v) => parseInt(v, 10), 25).option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
3351
|
+
// src/commands/actions.ts
|
|
3352
|
+
function registerActionsCommand(program2) {
|
|
3353
|
+
const actions = program2.command("actions").description("List and call permission-scoped Topolo actions from the live catalog").option("-s, --service <service>", "Filter by service id, slug, or name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
|
|
3205
3354
|
const mode = resolveOutputMode(opts.json);
|
|
3206
3355
|
try {
|
|
3207
|
-
await assertCrmRoutable();
|
|
3208
3356
|
const topolo = await buildClient();
|
|
3209
3357
|
if (!topolo) {
|
|
3210
3358
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3211
3359
|
}
|
|
3212
|
-
const
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3360
|
+
const rows = await topolo.client.listActions({ service: opts.service });
|
|
3361
|
+
emit({ actions: rows }, mode, () => formatActionRows(rows));
|
|
3362
|
+
} catch (err) {
|
|
3363
|
+
fail(err, mode);
|
|
3364
|
+
}
|
|
3365
|
+
});
|
|
3366
|
+
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) => {
|
|
3367
|
+
const mode = resolveOutputMode(opts.json);
|
|
3368
|
+
try {
|
|
3369
|
+
const topolo = await buildClient();
|
|
3370
|
+
if (!topolo) {
|
|
3371
|
+
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3372
|
+
}
|
|
3373
|
+
const entry = await topolo.client.getAction(action);
|
|
3374
|
+
emit(entry, mode, () => formatActionDetail(entry));
|
|
3218
3375
|
} catch (err) {
|
|
3219
3376
|
fail(err, mode);
|
|
3220
3377
|
}
|
|
3221
3378
|
});
|
|
3222
|
-
|
|
3379
|
+
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) => {
|
|
3223
3380
|
const mode = resolveOutputMode(opts.json);
|
|
3224
3381
|
try {
|
|
3225
|
-
await assertCrmRoutable();
|
|
3226
3382
|
const topolo = await buildClient();
|
|
3227
3383
|
if (!topolo) {
|
|
3228
3384
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
3229
3385
|
}
|
|
3230
|
-
const
|
|
3231
|
-
|
|
3232
|
-
emit(
|
|
3386
|
+
const input2 = parseJsonObject(opts.data || "{}");
|
|
3387
|
+
const result = await topolo.client.callAction(action, input2, { confirm: opts.confirm === true });
|
|
3388
|
+
emit(result, mode, () => JSON.stringify(result, null, 2));
|
|
3233
3389
|
} catch (err) {
|
|
3234
3390
|
fail(err, mode);
|
|
3235
3391
|
}
|
|
3236
3392
|
});
|
|
3237
3393
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
)
|
|
3394
|
+
function parseJsonObject(value) {
|
|
3395
|
+
let parsed;
|
|
3396
|
+
try {
|
|
3397
|
+
parsed = JSON.parse(value);
|
|
3398
|
+
} catch {
|
|
3399
|
+
throw new Error("--data must be valid JSON");
|
|
3400
|
+
}
|
|
3401
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3402
|
+
throw new Error("--data must be a JSON object");
|
|
3403
|
+
}
|
|
3404
|
+
return parsed;
|
|
3246
3405
|
}
|
|
3247
|
-
function
|
|
3248
|
-
if (rows.length === 0) return "No
|
|
3249
|
-
const
|
|
3250
|
-
|
|
3251
|
-
|
|
3406
|
+
function formatActionRows(rows) {
|
|
3407
|
+
if (rows.length === 0) return "No actions available for this credential.";
|
|
3408
|
+
const serviceWidth = Math.max(7, ...rows.map((row) => (row.serviceSlug || row.serviceId).length));
|
|
3409
|
+
const nameWidth = Math.max(6, ...rows.map((row) => row.name.length));
|
|
3410
|
+
const methodWidth = Math.max(6, ...rows.map((row) => row.method.length));
|
|
3411
|
+
const lines = [
|
|
3412
|
+
`${"SERVICE".padEnd(serviceWidth)} ${"ACTION".padEnd(nameWidth)} ${"METHOD".padEnd(methodWidth)} CONFIRM PERMISSION`,
|
|
3413
|
+
`${"-".repeat(serviceWidth)} ${"-".repeat(nameWidth)} ${"-".repeat(methodWidth)} ------- ----------`
|
|
3414
|
+
];
|
|
3252
3415
|
for (const row of rows) {
|
|
3253
|
-
const
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
if (row.leadStatus) parts.push(`[${row.leadStatus}]`);
|
|
3258
|
-
lines.push(` ${row.id} ${parts.join(" \xB7 ")}`);
|
|
3416
|
+
const confirmation = row.requiresConfirmation ? "yes" : "no";
|
|
3417
|
+
lines.push(
|
|
3418
|
+
`${(row.serviceSlug || row.serviceId).padEnd(serviceWidth)} ${row.name.padEnd(nameWidth)} ${row.method.padEnd(methodWidth)} ${confirmation.padEnd(7)} ${row.requiredPermission}`
|
|
3419
|
+
);
|
|
3259
3420
|
}
|
|
3260
3421
|
return lines.join("\n");
|
|
3261
3422
|
}
|
|
3262
|
-
function
|
|
3263
|
-
const name = [c.firstName, c.lastName].filter(Boolean).join(" ") || "(no name)";
|
|
3423
|
+
function formatActionDetail(action) {
|
|
3264
3424
|
return [
|
|
3265
|
-
`
|
|
3266
|
-
`
|
|
3267
|
-
`
|
|
3268
|
-
`
|
|
3269
|
-
`
|
|
3270
|
-
`
|
|
3271
|
-
`
|
|
3272
|
-
`
|
|
3425
|
+
`Action: ${action.name}`,
|
|
3426
|
+
`ID: ${action.actionId}`,
|
|
3427
|
+
`Tool: ${action.toolName || "-"}`,
|
|
3428
|
+
`Service: ${action.serviceSlug || action.serviceId}`,
|
|
3429
|
+
`Method: ${action.method}`,
|
|
3430
|
+
`Path: ${action.path}`,
|
|
3431
|
+
`Permission: ${action.requiredPermission}`,
|
|
3432
|
+
`Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
|
|
3433
|
+
"",
|
|
3434
|
+
action.description
|
|
3273
3435
|
].join("\n");
|
|
3274
3436
|
}
|
|
3275
3437
|
|
|
@@ -8593,14 +8755,15 @@ function buildRows(catalog, overrides, environment = selectedEnvironment()) {
|
|
|
8593
8755
|
return catalog.map((entry) => {
|
|
8594
8756
|
const id = displayServiceId(entry);
|
|
8595
8757
|
const defaultUrl = defaultServiceUrl(id, entry.baseUrl);
|
|
8596
|
-
const
|
|
8758
|
+
const configuredUrl = configuredServiceUrl(id, resolvedOverrides);
|
|
8759
|
+
const effectiveUrl = configuredUrl || defaultUrl;
|
|
8597
8760
|
return {
|
|
8598
8761
|
id,
|
|
8599
8762
|
serviceId: entry.serviceId,
|
|
8600
8763
|
slug: entry.slug,
|
|
8601
8764
|
defaultUrl,
|
|
8602
8765
|
effectiveUrl,
|
|
8603
|
-
overridden:
|
|
8766
|
+
overridden: Boolean(configuredUrl && configuredUrl !== defaultUrl),
|
|
8604
8767
|
environmentSafe: environment !== "staging" || isStagingUrl(effectiveUrl),
|
|
8605
8768
|
permissions: [...entry.permissions]
|
|
8606
8769
|
};
|
|
@@ -8637,7 +8800,17 @@ function displayServiceId(entry) {
|
|
|
8637
8800
|
return serviceIdCandidates(entry.slug || entry.serviceId)[0] || entry.serviceId;
|
|
8638
8801
|
}
|
|
8639
8802
|
function defaultServiceUrl(id, fallback) {
|
|
8640
|
-
return DEFAULT_SERVICE_URLS[id] ??
|
|
8803
|
+
return fallback ?? DEFAULT_SERVICE_URLS[id] ?? "";
|
|
8804
|
+
}
|
|
8805
|
+
function configuredServiceUrl(id, overrides) {
|
|
8806
|
+
if (!overrides) return null;
|
|
8807
|
+
for (const candidate of serviceIdCandidates(id)) {
|
|
8808
|
+
const direct = overrides[candidate];
|
|
8809
|
+
if (direct) return direct;
|
|
8810
|
+
const envStyle = overrides[candidate.replace(/\./g, "_")];
|
|
8811
|
+
if (envStyle) return envStyle;
|
|
8812
|
+
}
|
|
8813
|
+
return null;
|
|
8641
8814
|
}
|
|
8642
8815
|
function serviceIdCandidates(value) {
|
|
8643
8816
|
const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
@@ -9179,7 +9352,7 @@ program.hook("preAction", (thisCommand) => {
|
|
|
9179
9352
|
});
|
|
9180
9353
|
registerAuthCommands(program);
|
|
9181
9354
|
registerWhoamiCommand(program);
|
|
9182
|
-
|
|
9355
|
+
registerActionsCommand(program);
|
|
9183
9356
|
registerApiCommand(program);
|
|
9184
9357
|
registerAppsCommand(program);
|
|
9185
9358
|
registerOAuthCommands(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|