@topolo/cli 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -78,6 +78,7 @@ var APP_API_URLS = {
78
78
  bugfix: "https://bugfix.topolo.app",
79
79
  bytes: "https://bytes.topolo.app",
80
80
  calendar: "https://calendar.topolo.app",
81
+ campaigns: "https://campaigns.topolo.app",
81
82
  capacity: "https://capacity.topolo.app",
82
83
  chat: "https://chat.topolo.app",
83
84
  commerce: "https://commerce.topolo.app",
@@ -336,6 +337,38 @@ var APPLICATIONS = {
336
337
  }
337
338
  ]
338
339
  },
340
+ "campaigns": {
341
+ "id": "campaigns",
342
+ "appDir": "TopoloCampaigns",
343
+ "name": "topolo-campaigns",
344
+ "packageName": "topolo-campaigns",
345
+ "productionUrl": "https://campaigns.topolo.app",
346
+ "services": [
347
+ "campaigns"
348
+ ],
349
+ "deployTargets": [
350
+ {
351
+ "name": "topolo-campaigns",
352
+ "kind": "worker",
353
+ "apiUrl": "https://campaigns.topolo.app",
354
+ "domains": [
355
+ "campaigns.topolo.app"
356
+ ],
357
+ "routes": [
358
+ "campaigns.topolo.app"
359
+ ],
360
+ "appId": "campaigns"
361
+ },
362
+ {
363
+ "name": "topolo-campaigns-api",
364
+ "kind": "worker",
365
+ "apiUrl": null,
366
+ "domains": [],
367
+ "routes": [],
368
+ "appId": null
369
+ }
370
+ ]
371
+ },
339
372
  "capacity": {
340
373
  "id": "capacity",
341
374
  "appDir": "TopoloCapacity",
@@ -2065,6 +2098,262 @@ function firstString(...values) {
2065
2098
  }
2066
2099
  return null;
2067
2100
  }
2101
+ var SUPPORTED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
2102
+ "$schema",
2103
+ "$id",
2104
+ "$ref",
2105
+ "$defs",
2106
+ "definitions",
2107
+ "title",
2108
+ "description",
2109
+ "default",
2110
+ "examples",
2111
+ "deprecated",
2112
+ "readOnly",
2113
+ "writeOnly",
2114
+ "type",
2115
+ "nullable",
2116
+ "const",
2117
+ "enum",
2118
+ "allOf",
2119
+ "anyOf",
2120
+ "oneOf",
2121
+ "properties",
2122
+ "required",
2123
+ "additionalProperties",
2124
+ "propertyNames",
2125
+ "minProperties",
2126
+ "maxProperties",
2127
+ "items",
2128
+ "minItems",
2129
+ "maxItems",
2130
+ "uniqueItems",
2131
+ "minLength",
2132
+ "maxLength",
2133
+ "pattern",
2134
+ "format",
2135
+ "minimum",
2136
+ "maximum",
2137
+ "exclusiveMinimum",
2138
+ "exclusiveMaximum",
2139
+ "multipleOf"
2140
+ ]);
2141
+ function validateTopoloActionInput(schema, input2) {
2142
+ const schemaIssue = inspectSchema(schema, "$", schema, 0);
2143
+ if (schemaIssue) return { valid: false, errors: [schemaIssue] };
2144
+ const errors = [];
2145
+ validateNode(schema, input2, "$", schema, errors, 0);
2146
+ return { valid: errors.length === 0, errors };
2147
+ }
2148
+ function inspectSchema(schema, path4, root, depth) {
2149
+ if (depth > 128) return { path: path4, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
2150
+ const unsupportedKeyword = Object.keys(schema).find((keyword) => !SUPPORTED_SCHEMA_KEYWORDS.has(keyword));
2151
+ if (unsupportedKeyword) {
2152
+ return { path: path4, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
2153
+ }
2154
+ if (typeof schema.format === "string" && !["uri", "email", "date-time"].includes(schema.format)) {
2155
+ return { path: path4, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
2156
+ }
2157
+ if (typeof schema.$ref === "string" && !resolveLocalReference(root, schema.$ref)) {
2158
+ return { path: path4, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
2159
+ }
2160
+ const children = [];
2161
+ if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
2162
+ if (isRecord(schema.$defs)) children.push(...Object.entries(schema.$defs));
2163
+ if (isRecord(schema.definitions)) children.push(...Object.entries(schema.definitions));
2164
+ children.push(
2165
+ ["additionalProperties", schema.additionalProperties],
2166
+ ["propertyNames", schema.propertyNames],
2167
+ ["items", schema.items],
2168
+ ...arraySchemaEntries("allOf", schema.allOf),
2169
+ ...arraySchemaEntries("anyOf", schema.anyOf),
2170
+ ...arraySchemaEntries("oneOf", schema.oneOf)
2171
+ );
2172
+ for (const [key, child] of children) {
2173
+ if (!isRecord(child)) continue;
2174
+ const issue = inspectSchema(child, `${path4}.${key}`, root, depth + 1);
2175
+ if (issue) return issue;
2176
+ }
2177
+ return null;
2178
+ }
2179
+ function arraySchemaEntries(keyword, value) {
2180
+ return Array.isArray(value) ? value.map((candidate, index) => [`${keyword}[${index}]`, candidate]) : [];
2181
+ }
2182
+ function validateNode(schema, value, path4, root, errors, depth) {
2183
+ if (depth > 128) {
2184
+ errors.push({ path: path4, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
2185
+ return;
2186
+ }
2187
+ if (typeof schema.$ref === "string") {
2188
+ const resolved = resolveLocalReference(root, schema.$ref);
2189
+ if (!resolved) {
2190
+ errors.push({ path: path4, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
2191
+ return;
2192
+ }
2193
+ validateNode(resolved, value, path4, root, errors, depth + 1);
2194
+ }
2195
+ if (Array.isArray(schema.allOf)) {
2196
+ for (const candidate of schema.allOf) {
2197
+ if (isRecord(candidate)) validateNode(candidate, value, path4, root, errors, depth + 1);
2198
+ }
2199
+ }
2200
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
2201
+ errors.push({ path: path4, code: "any_of", message: "Value does not match any allowed schema." });
2202
+ return;
2203
+ }
2204
+ if (Array.isArray(schema.oneOf)) {
2205
+ const matches = schema.oneOf.filter((candidate) => candidateMatches(candidate, value, root, depth)).length;
2206
+ if (matches !== 1) {
2207
+ errors.push({ path: path4, code: "one_of", message: "Value must match exactly one allowed schema." });
2208
+ return;
2209
+ }
2210
+ }
2211
+ if (value === null && schema.nullable === true) return;
2212
+ if (Object.prototype.hasOwnProperty.call(schema, "const") && !sameJsonValue(value, schema.const)) {
2213
+ errors.push({ path: path4, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
2214
+ return;
2215
+ }
2216
+ if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => sameJsonValue(value, candidate))) {
2217
+ errors.push({ path: path4, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
2218
+ return;
2219
+ }
2220
+ if (!matchesType(schema.type, value)) {
2221
+ errors.push({ path: path4, code: "type", message: `Expected ${String(schema.type)}.` });
2222
+ return;
2223
+ }
2224
+ if (typeof value === "string") validateString(schema, value, path4, errors);
2225
+ if (typeof value === "number") validateNumber(schema, value, path4, errors);
2226
+ if (Array.isArray(value)) validateArray(schema, value, path4, root, errors, depth);
2227
+ if (isRecord(value)) validateObject(schema, value, path4, root, errors, depth);
2228
+ }
2229
+ function validateObject(schema, value, path4, root, errors, depth) {
2230
+ if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
2231
+ errors.push({ path: path4, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
2232
+ }
2233
+ if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
2234
+ errors.push({ path: path4, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
2235
+ }
2236
+ const properties = isRecord(schema.properties) ? schema.properties : {};
2237
+ const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
2238
+ for (const key of required) {
2239
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
2240
+ errors.push({ path: `${path4}.${key}`, code: "required", message: "Required property is missing." });
2241
+ }
2242
+ }
2243
+ if (isRecord(schema.propertyNames)) {
2244
+ for (const key of Object.keys(value)) {
2245
+ validateNode(schema.propertyNames, key, `${path4}.${key}`, root, errors, depth + 1);
2246
+ }
2247
+ }
2248
+ for (const [key, child] of Object.entries(value)) {
2249
+ const childSchema = properties[key];
2250
+ if (isRecord(childSchema)) {
2251
+ validateNode(childSchema, child, `${path4}.${key}`, root, errors, depth + 1);
2252
+ } else if (schema.additionalProperties === false) {
2253
+ errors.push({ path: `${path4}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
2254
+ } else if (isRecord(schema.additionalProperties)) {
2255
+ validateNode(schema.additionalProperties, child, `${path4}.${key}`, root, errors, depth + 1);
2256
+ }
2257
+ }
2258
+ }
2259
+ function validateArray(schema, value, path4, root, errors, depth) {
2260
+ if (typeof schema.minItems === "number" && value.length < schema.minItems) {
2261
+ errors.push({ path: path4, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
2262
+ }
2263
+ if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
2264
+ errors.push({ path: path4, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
2265
+ }
2266
+ if (schema.uniqueItems === true) {
2267
+ const unique2 = new Set(value.map((item) => JSON.stringify(item)));
2268
+ if (unique2.size !== value.length) errors.push({ path: path4, code: "unique_items", message: "Expected every item to be unique." });
2269
+ }
2270
+ if (isRecord(schema.items)) {
2271
+ value.forEach((item, index) => validateNode(schema.items, item, `${path4}[${index}]`, root, errors, depth + 1));
2272
+ }
2273
+ }
2274
+ function validateString(schema, value, path4, errors) {
2275
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) {
2276
+ errors.push({ path: path4, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
2277
+ }
2278
+ if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
2279
+ errors.push({ path: path4, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
2280
+ }
2281
+ if (typeof schema.pattern === "string") {
2282
+ try {
2283
+ if (!new RegExp(schema.pattern).test(value)) errors.push({ path: path4, code: "pattern", message: "Value does not match the required pattern." });
2284
+ } catch {
2285
+ errors.push({ path: path4, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
2286
+ }
2287
+ }
2288
+ if (schema.format === "uri" && !isUrl(value)) errors.push({ path: path4, code: "format_uri", message: "Expected an absolute URL." });
2289
+ if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path: path4, code: "format_email", message: "Expected an email address." });
2290
+ if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path: path4, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
2291
+ }
2292
+ function validateNumber(schema, value, path4, errors) {
2293
+ if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path: path4, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
2294
+ if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path: path4, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
2295
+ const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum === true && typeof schema.minimum === "number" ? schema.minimum : null;
2296
+ const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum === true && typeof schema.maximum === "number" ? schema.maximum : null;
2297
+ if (exclusiveMinimum !== null && value <= exclusiveMinimum) {
2298
+ errors.push({ path: path4, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
2299
+ }
2300
+ if (exclusiveMaximum !== null && value >= exclusiveMaximum) {
2301
+ errors.push({ path: path4, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
2302
+ }
2303
+ if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
2304
+ errors.push({ path: path4, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
2305
+ }
2306
+ }
2307
+ function candidateMatches(candidate, value, root, depth) {
2308
+ if (!isRecord(candidate)) return false;
2309
+ const issues = [];
2310
+ validateNode(candidate, value, "$", root, issues, depth + 1);
2311
+ return issues.length === 0;
2312
+ }
2313
+ function resolveLocalReference(root, reference) {
2314
+ if (!reference.startsWith("#/")) return null;
2315
+ let current = root;
2316
+ for (const token of reference.slice(2).split("/").map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))) {
2317
+ if (Array.isArray(current)) {
2318
+ if (!/^\d+$/.test(token) || Number(token) >= current.length) return null;
2319
+ current = current[Number(token)];
2320
+ continue;
2321
+ }
2322
+ if (!isRecord(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
2323
+ current = current[token];
2324
+ }
2325
+ return isRecord(current) ? current : null;
2326
+ }
2327
+ function matchesType(type, value) {
2328
+ if (type === void 0) return true;
2329
+ if (Array.isArray(type)) return type.some((candidate) => matchesType(candidate, value));
2330
+ if (type === "null") return value === null;
2331
+ if (type === "array") return Array.isArray(value);
2332
+ if (type === "object") return isRecord(value);
2333
+ if (type === "integer") return typeof value === "number" && Number.isInteger(value);
2334
+ return typeof value === type;
2335
+ }
2336
+ function sameJsonValue(left, right) {
2337
+ return JSON.stringify(left) === JSON.stringify(right);
2338
+ }
2339
+ function isRecord(value) {
2340
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2341
+ }
2342
+ function isUrl(value) {
2343
+ try {
2344
+ return Boolean(new URL(value).protocol);
2345
+ } catch {
2346
+ return false;
2347
+ }
2348
+ }
2349
+ function isDateTime(value) {
2350
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value));
2351
+ }
2352
+ function isMultipleOf(value, divisor) {
2353
+ if (!(divisor > 0)) return false;
2354
+ const quotient = value / divisor;
2355
+ return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
2356
+ }
2068
2357
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2069
2358
  var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
2070
2359
  var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
@@ -2284,7 +2573,10 @@ var TopoloClient = class _TopoloClient {
2284
2573
  serviceKey,
2285
2574
  overrides: this.serviceUrls
2286
2575
  });
2287
- return baseUrl ? { serviceKey, baseUrl } : null;
2576
+ if (baseUrl) return { serviceKey, baseUrl };
2577
+ const actionPage = await this.searchActions({ service: match.appId, limit: 1 }).catch(() => null);
2578
+ const actionBaseUrl = actionPage?.actions.find((action) => action.apiBaseUrl)?.apiBaseUrl ?? null;
2579
+ return actionBaseUrl ? { serviceKey, baseUrl: requireHttpOrigin(actionBaseUrl) } : null;
2288
2580
  }
2289
2581
  /**
2290
2582
  * Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
@@ -2369,7 +2661,7 @@ var TopoloClient = class _TopoloClient {
2369
2661
  }
2370
2662
  const entries = [];
2371
2663
  for (const item of raw) {
2372
- if (!isRecord(item)) continue;
2664
+ if (!isRecord2(item)) continue;
2373
2665
  const s = item;
2374
2666
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2375
2667
  if (!appId) continue;
@@ -2419,6 +2711,44 @@ var TopoloClient = class _TopoloClient {
2419
2711
  }
2420
2712
  return entry;
2421
2713
  }
2714
+ async listBindableResources(application) {
2715
+ const service = await this.getService(application);
2716
+ const identity = await this.introspect();
2717
+ if (identity.apiKey) {
2718
+ return {
2719
+ application: { appId: service.appId, slug: service.slug, name: service.name },
2720
+ organizationId: identity.organization?.id ?? null,
2721
+ resources: identity.apiKey.resourceBindings.map((binding) => ({
2722
+ resourceType: binding.resourceType,
2723
+ resourceId: binding.resourceId,
2724
+ label: null,
2725
+ slug: null,
2726
+ status: "active",
2727
+ organizationId: identity.organization?.id ?? null,
2728
+ metadata: {}
2729
+ })),
2730
+ resourceTypes: service.apiKeyResources,
2731
+ syncError: null,
2732
+ source: "api_key_binding"
2733
+ };
2734
+ }
2735
+ const response = await this.readAction(
2736
+ "app_topolo_auth.service_api_key_resources.list",
2737
+ { appId: service.appId }
2738
+ );
2739
+ const payload = isRecord2(response) && isRecord2(response.data) ? response.data : response;
2740
+ if (!isRecord2(payload) || !Array.isArray(payload.resources)) {
2741
+ throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
2742
+ }
2743
+ return {
2744
+ application: { appId: service.appId, slug: service.slug, name: service.name },
2745
+ organizationId: readOptionalString(payload.organizationId ?? payload.organization_id),
2746
+ resources: payload.resources.map(normalizeBindableResource).filter((resource) => Boolean(resource)),
2747
+ resourceTypes: normalizeApiKeyResourceMetadataList(payload.resourceTypes ?? payload.resource_types),
2748
+ syncError: readOptionalString(payload.syncError ?? payload.sync_error),
2749
+ source: "auth_catalog"
2750
+ };
2751
+ }
2422
2752
  /**
2423
2753
  * List callable actions from the live registry-canonical catalog (definitions
2424
2754
  * from the Catalog registry, scoping from Auth). The response is scoped to the
@@ -2511,6 +2841,54 @@ var TopoloClient = class _TopoloClient {
2511
2841
  }
2512
2842
  async callAction(action, input2 = {}, options = {}) {
2513
2843
  const entry = await this.getAction(action);
2844
+ return this.executeResolvedAction(entry, input2, options);
2845
+ }
2846
+ async validateActionInput(action, input2 = {}) {
2847
+ const entry = await this.getAction(action);
2848
+ return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input2) };
2849
+ }
2850
+ async planAction(action, input2 = {}, options = {}) {
2851
+ const entry = await this.getAction(action);
2852
+ const validation = validateTopoloActionInput(entry.inputSchema, input2);
2853
+ const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2854
+ const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
2855
+ let request = null;
2856
+ if (validation.valid) {
2857
+ const mapped = mapActionInput(entry, input2);
2858
+ request = {
2859
+ method: entry.method,
2860
+ service: entry.appSlug || entry.appId,
2861
+ apiBaseUrl: entry.apiBaseUrl,
2862
+ path: mapped.path,
2863
+ ...mapped.query ? { query: mapped.query } : {},
2864
+ ...mapped.body !== void 0 ? { body: mapped.body } : {},
2865
+ ...options.resource ? { resource: options.resource } : {}
2866
+ };
2867
+ }
2868
+ return {
2869
+ action: entry,
2870
+ validation,
2871
+ confirmationRequired,
2872
+ confirmationProvided: options.confirm === true,
2873
+ executable: validation.valid && agentAccess !== "off" && (!confirmationRequired || options.confirm === true),
2874
+ request,
2875
+ verification: stringArray(entry.agentContract?.verify),
2876
+ rollbackActionId: pickActionString(entry.agentContract?.rollbackActionId),
2877
+ nextActionIds: stringArray(entry.agentContract?.nextActionIds)
2878
+ };
2879
+ }
2880
+ async readAction(action, input2 = {}, options = {}) {
2881
+ const entry = await this.getAction(action);
2882
+ const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2883
+ if (!entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
2884
+ throw new TopoloPermissionError(
2885
+ `Topolo action "${entry.actionId}" is not an automatic read-only action.`,
2886
+ entry.requiredPermission ? [entry.requiredPermission] : []
2887
+ );
2888
+ }
2889
+ return this.executeResolvedAction(entry, input2, options);
2890
+ }
2891
+ async executeResolvedAction(entry, input2, options) {
2514
2892
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2515
2893
  if (agentAccess === "off") {
2516
2894
  throw new TopoloPermissionError(
@@ -2523,6 +2901,13 @@ var TopoloClient = class _TopoloClient {
2523
2901
  `Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
2524
2902
  );
2525
2903
  }
2904
+ const validation = validateTopoloActionInput(entry.inputSchema, input2);
2905
+ if (!validation.valid) {
2906
+ throw new TopoloSdkError(
2907
+ "invalid_action_input",
2908
+ `Topolo action "${entry.actionId}" input is invalid: ${validation.errors.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`
2909
+ );
2910
+ }
2526
2911
  const mapped = mapActionInput(entry, input2);
2527
2912
  const service = entry.appSlug || entry.appId;
2528
2913
  return this.request({
@@ -2537,7 +2922,7 @@ var TopoloClient = class _TopoloClient {
2537
2922
  [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2538
2923
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
2539
2924
  },
2540
- confirm: options.confirm === true
2925
+ confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
2541
2926
  });
2542
2927
  }
2543
2928
  /**
@@ -2594,7 +2979,14 @@ var TopoloClient = class _TopoloClient {
2594
2979
  id: organization.id,
2595
2980
  slug: organization.slug,
2596
2981
  name: organization.name ?? organization.slug
2597
- } : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null
2982
+ } : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null,
2983
+ apiKey: kind === "api_key" ? {
2984
+ id: readOptionalString(payload.apiKey?.id),
2985
+ name: readOptionalString(payload.apiKey?.name),
2986
+ appId: readOptionalString(payload.apiKey?.appId),
2987
+ scopes: stringArray(payload.apiKey?.scopes),
2988
+ resourceBindings: normalizeResourceBindings(payload.apiKey?.resourceBindings)
2989
+ } : null
2598
2990
  };
2599
2991
  }
2600
2992
  };
@@ -2609,7 +3001,7 @@ function resolveConfiguredAppId(slug, appIds) {
2609
3001
  return null;
2610
3002
  }
2611
3003
  function normalizeApiKeyResourceMetadata(input2) {
2612
- if (!isRecord(input2)) return null;
3004
+ if (!isRecord2(input2)) return null;
2613
3005
  const resourceType = readString(input2.resourceType ?? input2.resource_type);
2614
3006
  const label = readString(input2.label);
2615
3007
  const emptyLabel = readString(input2.emptyLabel ?? input2.empty_label);
@@ -2642,10 +3034,38 @@ function normalizeApiKeyResourceMetadataList(input2) {
2642
3034
  }
2643
3035
  return resources;
2644
3036
  }
3037
+ function normalizeBindableResource(input2) {
3038
+ if (!isRecord2(input2)) return null;
3039
+ const resourceType = readString(input2.resourceType ?? input2.resource_type);
3040
+ const resourceId = readString(input2.resourceId ?? input2.resource_id);
3041
+ if (!resourceType || !resourceId) return null;
3042
+ return {
3043
+ resourceType,
3044
+ resourceId,
3045
+ label: readOptionalString(input2.label),
3046
+ slug: readOptionalString(input2.slug),
3047
+ status: readOptionalString(input2.status),
3048
+ organizationId: readOptionalString(input2.organizationId ?? input2.organization_id),
3049
+ metadata: isRecord2(input2.metadata) ? input2.metadata : {}
3050
+ };
3051
+ }
3052
+ function normalizeResourceBindings(input2) {
3053
+ if (!Array.isArray(input2)) return [];
3054
+ return input2.flatMap((binding) => {
3055
+ if (!isRecord2(binding)) return [];
3056
+ const resourceType = readString(binding.resourceType ?? binding.resource_type);
3057
+ const resourceId = readString(binding.resourceId ?? binding.resource_id);
3058
+ return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
3059
+ });
3060
+ }
2645
3061
  function readString(value) {
2646
3062
  return typeof value === "string" ? value.trim() : "";
2647
3063
  }
2648
- function isRecord(value) {
3064
+ function readOptionalString(value) {
3065
+ const normalized = readString(value);
3066
+ return normalized || null;
3067
+ }
3068
+ function isRecord2(value) {
2649
3069
  return typeof value === "object" && value !== null && !Array.isArray(value);
2650
3070
  }
2651
3071
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
@@ -2717,14 +3137,14 @@ function catalogSearchQuery(options) {
2717
3137
  return query;
2718
3138
  }
2719
3139
  function normalizeCatalogPage(value) {
2720
- if (!isRecord(value)) return { nextCursor: null, hasMore: false };
3140
+ if (!isRecord2(value)) return { nextCursor: null, hasMore: false };
2721
3141
  return {
2722
3142
  nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
2723
3143
  hasMore: value.hasMore === true
2724
3144
  };
2725
3145
  }
2726
3146
  function normalizeServiceCatalogEntry(item) {
2727
- if (!isRecord(item)) return null;
3147
+ if (!isRecord2(item)) return null;
2728
3148
  const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
2729
3149
  if (!appId) return null;
2730
3150
  const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
@@ -2779,6 +3199,7 @@ function normalizeActionCatalogEntry(item) {
2779
3199
  authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
2780
3200
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
2781
3201
  outputSchema: plainObject(a.output_schema ?? a.outputSchema),
3202
+ agentContract: plainObject(a.agent_contract ?? a.agentContract),
2782
3203
  readOnly: Boolean(a.read_only ?? a.readOnly),
2783
3204
  destructive: Boolean(a.destructive),
2784
3205
  requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
@@ -2802,6 +3223,9 @@ function pickAuthorizationMode(value) {
2802
3223
  function plainObject(value) {
2803
3224
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2804
3225
  }
3226
+ function stringArray(value) {
3227
+ return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
3228
+ }
2805
3229
  function mapActionInput(action, input2) {
2806
3230
  const remaining = { ...input2 || {} };
2807
3231
  const path4 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
@@ -3070,6 +3494,7 @@ var APP_API_URLS2 = {
3070
3494
  bugfix: "https://bugfix.topolo.app",
3071
3495
  bytes: "https://bytes.topolo.app",
3072
3496
  calendar: "https://calendar.topolo.app",
3497
+ campaigns: "https://campaigns.topolo.app",
3073
3498
  capacity: "https://capacity.topolo.app",
3074
3499
  chat: "https://chat.topolo.app",
3075
3500
  commerce: "https://commerce.topolo.app",
@@ -3394,6 +3819,46 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
3394
3819
  return cred;
3395
3820
  }
3396
3821
  }
3822
+ async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
3823
+ const config = await readConfig();
3824
+ const profile = profileForEnvironment(config, environment);
3825
+ const context = profile.contexts?.[key];
3826
+ const credential = context?.credential;
3827
+ if (!credential || credential.kind !== "access_token") return credential ?? null;
3828
+ const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
3829
+ if (!credential.refreshToken || !credential.expiresAt || !oauthClientId) return credential;
3830
+ const now = Math.floor(Date.now() / 1e3);
3831
+ if (!options.force && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
3832
+ try {
3833
+ const oauth = new TopoloOAuth2({
3834
+ serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
3835
+ });
3836
+ const tokens = await oauth.refreshToken({
3837
+ clientId: oauthClientId,
3838
+ refreshToken: credential.refreshToken
3839
+ });
3840
+ const refreshed = {
3841
+ kind: "access_token",
3842
+ accessToken: tokens.access_token,
3843
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
3844
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
3845
+ };
3846
+ await writeConfig(writeProfileConfig(config, environment, {
3847
+ ...profile.activeContext === key ? { credential: refreshed } : {},
3848
+ contexts: {
3849
+ ...profile.contexts ?? {},
3850
+ [key]: {
3851
+ ...context,
3852
+ credential: refreshed,
3853
+ oauthClientId
3854
+ }
3855
+ }
3856
+ }));
3857
+ return refreshed;
3858
+ } catch {
3859
+ return credential;
3860
+ }
3861
+ }
3397
3862
  function resolveAgentName(configAgentName) {
3398
3863
  return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
3399
3864
  }
@@ -3871,8 +4336,10 @@ function formatPretty(info) {
3871
4336
  return lines.join("\n");
3872
4337
  }
3873
4338
 
4339
+ // src/commands/actions.ts
4340
+ import { readFileSync as readFileSync2 } from "fs";
4341
+
3874
4342
  // src/commands/api.ts
3875
- var STATIC_APP_IDS = new Set(Object.keys(DEFAULT_APP_URLS));
3876
4343
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
3877
4344
  var WRITE_METHODS2 = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
3878
4345
  function registerApiCommand(program2) {
@@ -3899,9 +4366,8 @@ function registerApiCommand(program2) {
3899
4366
  const config = await readConfig();
3900
4367
  const profile = resolvedProfileConfig(config, environment);
3901
4368
  const configuredUrl = resolveConfiguredServiceUrl2(service, profile.serviceUrls);
3902
- const staticUrl = STATIC_APP_IDS.has(service) ? DEFAULT_APP_URLS[service] : void 0;
3903
- if (configuredUrl || staticUrl) {
3904
- assertServiceUrlForEnvironment(service, configuredUrl ?? staticUrl, environment);
4369
+ if (configuredUrl) {
4370
+ assertServiceUrlForEnvironment(service, configuredUrl, environment);
3905
4371
  }
3906
4372
  let body = void 0;
3907
4373
  if (opts.data) {
@@ -4008,7 +4474,7 @@ function registerActionsCommand(program2) {
4008
4474
  fail(err, mode);
4009
4475
  }
4010
4476
  });
4011
- 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) => {
4477
+ actions.command("call <action>").description("Call a permission-scoped Topolo action").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").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) => {
4012
4478
  const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
4013
4479
  try {
4014
4480
  const topolo = await buildClient();
@@ -4026,11 +4492,78 @@ function registerActionsCommand(program2) {
4026
4492
  fail(err, mode);
4027
4493
  }
4028
4494
  });
4495
+ actions.command("validate <action>").description("Validate action input locally against the published JSON Schema").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
4496
+ const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
4497
+ try {
4498
+ const topolo = await requiredClient();
4499
+ const result = await topolo.client.validateActionInput(action, parseJsonObject(opts.data || "{}"));
4500
+ emit(result, mode, () => result.validation.valid ? `Valid input for ${result.action.actionId}.` : result.validation.errors.map((issue) => `${issue.path}: ${issue.message}`).join("\n"));
4501
+ } catch (err) {
4502
+ fail(err, mode);
4503
+ }
4504
+ });
4505
+ actions.command("plan <action>").description("Validate and preview the exact request, confirmation, verification, and rollback plan").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Include confirmation in the plan without executing it").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
4506
+ const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
4507
+ try {
4508
+ const topolo = await requiredClient();
4509
+ const resource = parseResourceOption(opts.resourceType, opts.resourceId);
4510
+ const plan = await topolo.client.planAction(action, parseJsonObject(opts.data || "{}"), {
4511
+ confirm: opts.confirm === true,
4512
+ ...resource ? { resource } : {}
4513
+ });
4514
+ emit(plan, mode, () => formatActionPlan(plan));
4515
+ } catch (err) {
4516
+ fail(err, mode);
4517
+ }
4518
+ });
4519
+ actions.command("examples <action>").description("Show published examples and public docs for one action").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
4520
+ const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
4521
+ try {
4522
+ const topolo = await requiredClient();
4523
+ const entry = await topolo.client.getAction(action);
4524
+ const result = {
4525
+ actionId: entry.actionId,
4526
+ docsUrl: entry.agentContract?.docsUrl ?? null,
4527
+ examples: entry.agentContract?.examples ?? []
4528
+ };
4529
+ emit(result, mode, () => result.examples.length ? JSON.stringify(result, null, 2) : `No inline examples published. Docs: ${result.docsUrl || "not published"}`);
4530
+ } catch (err) {
4531
+ fail(err, mode);
4532
+ }
4533
+ });
4534
+ actions.command("capabilities").description("Discover capability and schema actions for one application").option("-s, --service <service>", "App id, slug, or name").option("--json", "Output JSON").option("--no-json", "Output human-readable table").action(async (opts, command) => {
4535
+ const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
4536
+ try {
4537
+ const topolo = await requiredClient();
4538
+ const service = opts.service || command.parent?.opts().service;
4539
+ if (!service?.trim()) throw new Error("--service is required for capability discovery");
4540
+ const result = await topolo.client.searchActions({ service: service.trim(), query: "capabilities", limit: 100 });
4541
+ emit(result, mode, () => formatActionRows(result.actions, result.page.nextCursor));
4542
+ } catch (err) {
4543
+ fail(err, mode);
4544
+ }
4545
+ });
4546
+ }
4547
+ async function requiredClient() {
4548
+ const topolo = await buildClient();
4549
+ if (!topolo) throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
4550
+ return topolo;
4029
4551
  }
4030
4552
  function parseJsonObject(value) {
4553
+ let source = value;
4554
+ if (value.startsWith("@")) {
4555
+ const path4 = value.slice(1);
4556
+ if (!path4) throw new Error("--data @file path must not be empty");
4557
+ try {
4558
+ source = readFileSync2(path4, "utf8");
4559
+ } catch (err) {
4560
+ const detail = err instanceof Error ? err.message : String(err);
4561
+ throw new Error(`Could not read --data file ${path4}: ${detail}`);
4562
+ }
4563
+ }
4031
4564
  let parsed;
4032
4565
  try {
4033
- parsed = JSON.parse(value);
4566
+ parsed = JSON.parse(source);
4034
4567
  } catch {
4035
4568
  throw new Error("--data must be valid JSON");
4036
4569
  }
@@ -4076,21 +4609,37 @@ function formatActionDetail(action) {
4076
4609
  `Path: ${action.path}`,
4077
4610
  `Permission: ${action.requiredPermission}`,
4078
4611
  `Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
4612
+ `Docs: ${action.agentContract?.docsUrl || "-"}`,
4613
+ `Rollback: ${action.agentContract?.rollbackActionId || "-"}`,
4079
4614
  "",
4080
4615
  action.description
4081
4616
  ].join("\n");
4082
4617
  }
4618
+ function formatActionPlan(plan) {
4619
+ const lines = [
4620
+ `Action: ${plan.action.actionId}`,
4621
+ `Valid: ${plan.validation.valid ? "yes" : "no"}`,
4622
+ `Executable: ${plan.executable ? "yes" : "no"}`,
4623
+ `Confirmation: ${plan.confirmationRequired ? plan.confirmationProvided ? "provided" : "required" : "not required"}`
4624
+ ];
4625
+ if (plan.request) lines.push(`Request: ${plan.request.method} ${plan.request.apiBaseUrl || plan.request.service}${plan.request.path}`);
4626
+ if (plan.request?.resource) lines.push(`Resource: ${plan.request.resource.resourceType}:${plan.request.resource.resourceId}`);
4627
+ if (plan.validation.errors.length) lines.push("", ...plan.validation.errors.map((issue) => `${issue.path}: ${issue.message}`));
4628
+ if (plan.verification.length) lines.push("", "Verify:", ...plan.verification.map((step) => `- ${step}`));
4629
+ if (plan.rollbackActionId) lines.push("", `Rollback: ${plan.rollbackActionId}`);
4630
+ return lines.join("\n");
4631
+ }
4083
4632
 
4084
4633
  // src/commands/apps.ts
4085
4634
  import { execSync } from "child_process";
4086
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
4635
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
4087
4636
  import path2 from "path";
4088
4637
 
4089
4638
  // src/commands/app-audit-local.ts
4090
4639
  import {
4091
4640
  existsSync as existsSync2,
4092
4641
  readdirSync,
4093
- readFileSync as readFileSync2,
4642
+ readFileSync as readFileSync3,
4094
4643
  statSync
4095
4644
  } from "fs";
4096
4645
  import path from "path";
@@ -4101,7 +4650,7 @@ function readEvidenceFile(appRoot) {
4101
4650
  const candidate = path.join(appRoot, ".topolo", "evidence.json");
4102
4651
  if (!isFile(candidate)) return null;
4103
4652
  try {
4104
- const raw = JSON.parse(readFileSync2(candidate, "utf8"));
4653
+ const raw = JSON.parse(readFileSync3(candidate, "utf8"));
4105
4654
  if (raw && typeof raw === "object" && raw.version === 1) {
4106
4655
  return raw;
4107
4656
  }
@@ -5045,7 +5594,7 @@ function readSourceIndex(root) {
5045
5594
  const files = collectSourceFiles(root, 4e3);
5046
5595
  const chunks = files.map((file) => {
5047
5596
  try {
5048
- return readFileSync2(file, "utf8");
5597
+ return readFileSync3(file, "utf8");
5049
5598
  } catch {
5050
5599
  return "";
5051
5600
  }
@@ -5105,7 +5654,7 @@ function sortedDirEntries(dir) {
5105
5654
  function readJson(filePath) {
5106
5655
  if (!isFile(filePath)) return null;
5107
5656
  try {
5108
- return JSON.parse(readFileSync2(filePath, "utf8"));
5657
+ return JSON.parse(readFileSync3(filePath, "utf8"));
5109
5658
  } catch {
5110
5659
  return null;
5111
5660
  }
@@ -9201,7 +9750,8 @@ function applicationRowsFromCatalog(catalog) {
9201
9750
  name: entry.name ?? entry.slug ?? entry.appId,
9202
9751
  appDir: "-",
9203
9752
  packageName: null,
9204
- productionUrl: entry.launchUrl ?? null,
9753
+ apiBaseUrl: entry.apiBaseUrl ?? null,
9754
+ launchUrl: entry.launchUrl ?? null,
9205
9755
  services: [displayCatalogId(entry)],
9206
9756
  deployTargets: 0
9207
9757
  })).sort((left, right) => left.id.localeCompare(right.id));
@@ -9266,7 +9816,7 @@ function recordApplicationEvidence(application, options = {}) {
9266
9816
  let before = null;
9267
9817
  if (existsSync3(evidencePath)) {
9268
9818
  try {
9269
- const raw = JSON.parse(readFileSync3(evidencePath, "utf8"));
9819
+ const raw = JSON.parse(readFileSync4(evidencePath, "utf8"));
9270
9820
  if (raw && typeof raw === "object" && raw.version === 1) {
9271
9821
  before = raw;
9272
9822
  }
@@ -9346,7 +9896,7 @@ function evaluateApplicationAuditGate(payload, failOn) {
9346
9896
  function formatRows(rows) {
9347
9897
  const idWidth = Math.max(2, ...rows.map((r) => r.id.length));
9348
9898
  const nameWidth = Math.max(4, ...rows.map((r) => r.name.length));
9349
- const urlWidth = Math.max(3, ...rows.map((r) => (r.productionUrl ?? "-").length));
9899
+ const urlWidth = Math.max(3, ...rows.map((r) => (applicationUrl(r) ?? "-").length));
9350
9900
  const servicesWidth = Math.max(
9351
9901
  8,
9352
9902
  ...rows.map((r) => r.services.length ? r.services.join(",").length : 1)
@@ -9361,7 +9911,7 @@ function formatRows(rows) {
9361
9911
  for (const r of rows) {
9362
9912
  const services = r.services.length ? r.services.join(",") : "-";
9363
9913
  lines.push(
9364
- `${r.id.padEnd(idWidth)} ${r.name.padEnd(nameWidth)} ${(r.productionUrl ?? "-").padEnd(urlWidth)} ${services.padEnd(servicesWidth)} ${r.deployTargets}`
9914
+ `${r.id.padEnd(idWidth)} ${r.name.padEnd(nameWidth)} ${(applicationUrl(r) ?? "-").padEnd(urlWidth)} ${services.padEnd(servicesWidth)} ${r.deployTargets}`
9365
9915
  );
9366
9916
  }
9367
9917
  return lines.join("\n");
@@ -9370,10 +9920,14 @@ function formatApplicationRow(application) {
9370
9920
  return [
9371
9921
  `ID: ${application.id}`,
9372
9922
  `Name: ${application.name}`,
9373
- `URL: ${application.productionUrl ?? "-"}`,
9923
+ `API URL: ${application.apiBaseUrl ?? "-"}`,
9924
+ `Launch URL: ${application.launchUrl ?? application.productionUrl ?? "-"}`,
9374
9925
  `Services: ${application.services.length ? application.services.join(", ") : "-"}`
9375
9926
  ].join("\n");
9376
9927
  }
9928
+ function applicationUrl(application) {
9929
+ return application.apiBaseUrl ?? application.launchUrl ?? application.productionUrl ?? null;
9930
+ }
9377
9931
  function displayCatalogId(entry) {
9378
9932
  for (const value of [entry.slug, entry.name, entry.appId]) {
9379
9933
  if (typeof value !== "string" || !value.trim()) continue;
@@ -9557,7 +10111,7 @@ function statusesAtOrWorse(threshold) {
9557
10111
  }
9558
10112
 
9559
10113
  // src/commands/platform.ts
9560
- import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
10114
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
9561
10115
  import path3 from "path";
9562
10116
  function registerPlatformCommand(program2) {
9563
10117
  const platform3 = program2.command("platform").description("Internal Topolo first-party platform and personal app commands");
@@ -9691,7 +10245,7 @@ function scanStagingProductionUrls(rootInput, allowPatterns) {
9691
10245
  function scanFile(filePath) {
9692
10246
  if (shouldSkipFile(filePath)) return;
9693
10247
  const relativePath = path3.relative(root, filePath);
9694
- const content = readFileSync4(filePath, "utf8");
10248
+ const content = readFileSync5(filePath, "utf8");
9695
10249
  const lines = content.split(/\r?\n/);
9696
10250
  lines.forEach((line, index) => {
9697
10251
  const matches = [...line.matchAll(PRODUCTION_TOPOLO_URL_PATTERN)].map((match) => match[0]);
@@ -10714,11 +11268,25 @@ function registerContextCommand(program2) {
10714
11268
  }
10715
11269
  const wantPersonal = /^(personal|me)$/i.test(target.trim());
10716
11270
  let resolved;
10717
- if (wantPersonal) {
11271
+ let exchangeCredential = cred;
11272
+ const needle = target.trim().toLowerCase();
11273
+ const storedEntry = Object.entries(profile.contexts ?? {}).find(([, candidate]) => {
11274
+ if (wantPersonal) return candidate.contextType === "personal";
11275
+ return candidate.contextType === "organization" && (candidate.organizationId === target || candidate.organizationSlug?.toLowerCase() === needle || candidate.organizationName?.toLowerCase() === needle);
11276
+ });
11277
+ const storedContext = storedEntry?.[1];
11278
+ if (storedEntry && storedContext?.credential) {
11279
+ exchangeCredential = await refreshContextCredentialIfNeeded(environment, storedEntry[0]) ?? storedContext.credential;
11280
+ resolved = {
11281
+ contextType: storedContext.contextType,
11282
+ organizationId: storedContext.organizationId,
11283
+ organizationSlug: storedContext.organizationSlug,
11284
+ organizationName: storedContext.organizationName
11285
+ };
11286
+ } else if (wantPersonal) {
10718
11287
  resolved = { contextType: "personal" };
10719
11288
  } else {
10720
11289
  const me = await fetchMe(cred, serviceUrls);
10721
- const needle = target.trim().toLowerCase();
10722
11290
  const match = me.memberships.find(
10723
11291
  (m) => m.id === target || m.slug?.toLowerCase() === needle || m.name?.toLowerCase() === needle
10724
11292
  );
@@ -10737,13 +11305,13 @@ function registerContextCommand(program2) {
10737
11305
  }
10738
11306
  const key = contextKey(resolved.contextType, resolved.organizationId);
10739
11307
  const existing = profile.contexts?.[key];
10740
- const newCredential = await exchangeForContext(cred, serviceUrls, {
11308
+ const newCredential = await exchangeForContext(exchangeCredential, serviceUrls, {
10741
11309
  type: resolved.contextType,
10742
11310
  id: resolved.organizationId
10743
11311
  });
10744
11312
  resolved.credential = newCredential;
10745
11313
  resolved.oauthClientId = existing?.oauthClientId ?? profile.oauthClientId;
10746
- const updated = writeContextCredential(config, environment, resolved, true);
11314
+ const updated = writeContextCredential(await readConfig(), environment, resolved, true);
10747
11315
  await writeConfig(updated);
10748
11316
  emit(
10749
11317
  { environment, active: key, context: { type: resolved.contextType, label: labelFor(resolved) } },
@@ -10757,6 +11325,34 @@ function registerContextCommand(program2) {
10757
11325
  });
10758
11326
  }
10759
11327
 
11328
+ // src/commands/resources.ts
11329
+ function registerResourcesCommand(program2) {
11330
+ program2.command("resources <application>").description("List bindable resource ids for one credential-scoped Topolo application").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (application, opts) => {
11331
+ const mode = resolveOutputMode(opts.json);
11332
+ try {
11333
+ const topolo = await buildClient();
11334
+ if (!topolo) throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
11335
+ const result = await topolo.client.listBindableResources(application);
11336
+ emit(result, mode, () => formatResources(result));
11337
+ } catch (err) {
11338
+ fail(err, mode);
11339
+ }
11340
+ });
11341
+ }
11342
+ function formatResources(result) {
11343
+ const lines = [
11344
+ `Application: ${result.application.name || result.application.slug || result.application.appId}`,
11345
+ `Source: ${result.source}`
11346
+ ];
11347
+ if (result.resources.length === 0) return [...lines, "", "No bindable resources available."].join("\n");
11348
+ lines.push("", "TYPE ID LABEL");
11349
+ for (const resource of result.resources) {
11350
+ lines.push(`${resource.resourceType} ${resource.resourceId} ${resource.label || resource.slug || "-"}`);
11351
+ }
11352
+ if (result.syncError) lines.push("", `Sync warning: ${result.syncError}`);
11353
+ return lines.join("\n");
11354
+ }
11355
+
10760
11356
  // src/index.ts
10761
11357
  var program = new Command();
10762
11358
  var argv;
@@ -10787,6 +11383,7 @@ registerServicesCommand(program);
10787
11383
  registerSeedCommands(program);
10788
11384
  registerSkillsCommand(program);
10789
11385
  registerContextCommand(program);
11386
+ registerResourcesCommand(program);
10790
11387
  program.parseAsync(argv).catch((err) => {
10791
11388
  process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
10792
11389
  `);