@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 +627 -30
- package/package.json +1 -1
- package/skills/codex/AGENTS.md +12 -3
- package/skills/topolo/SKILL.md +132 -167
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
3903
|
-
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
`);
|