@topolo/cli 0.5.0 → 0.5.2
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/generate-wrangler.mjs +908 -0
- package/dist/index.js +805 -794
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -61,13 +61,13 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
61
61
|
body;
|
|
62
62
|
service;
|
|
63
63
|
path;
|
|
64
|
-
constructor(service,
|
|
65
|
-
super("http_error", message ?? `HTTP ${status} from ${service}${
|
|
64
|
+
constructor(service, path6, status, body, message) {
|
|
65
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path6}`);
|
|
66
66
|
this.name = "TopoloHttpError";
|
|
67
67
|
this.status = status;
|
|
68
68
|
this.body = body;
|
|
69
69
|
this.service = service;
|
|
70
|
-
this.path =
|
|
70
|
+
this.path = path6;
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
73
|
var APP_API_URLS = {
|
|
@@ -185,7 +185,7 @@ var APPLICATIONS = {
|
|
|
185
185
|
"appDir": "TopoloAuth",
|
|
186
186
|
"name": "topolo-auth",
|
|
187
187
|
"packageName": "topolo-auth",
|
|
188
|
-
"productionUrl": "https://auth.topolo.app
|
|
188
|
+
"productionUrl": "https://auth.topolo.app",
|
|
189
189
|
"services": [
|
|
190
190
|
"auth"
|
|
191
191
|
],
|
|
@@ -273,9 +273,7 @@ var APPLICATIONS = {
|
|
|
273
273
|
"domains": [
|
|
274
274
|
"brand.topolo.app"
|
|
275
275
|
],
|
|
276
|
-
"routes": [
|
|
277
|
-
"brand.topolo.app"
|
|
278
|
-
],
|
|
276
|
+
"routes": [],
|
|
279
277
|
"appId": "brand"
|
|
280
278
|
},
|
|
281
279
|
{
|
|
@@ -387,9 +385,7 @@ var APPLICATIONS = {
|
|
|
387
385
|
"domains": [
|
|
388
386
|
"campaigns.topolo.app"
|
|
389
387
|
],
|
|
390
|
-
"routes": [
|
|
391
|
-
"campaigns.topolo.app"
|
|
392
|
-
],
|
|
388
|
+
"routes": [],
|
|
393
389
|
"appId": "campaigns"
|
|
394
390
|
},
|
|
395
391
|
{
|
|
@@ -1077,11 +1073,13 @@ var APPLICATIONS = {
|
|
|
1077
1073
|
{
|
|
1078
1074
|
"name": "topolo-nexus-dashboard",
|
|
1079
1075
|
"kind": "worker",
|
|
1080
|
-
"apiUrl":
|
|
1076
|
+
"apiUrl": "https://nexus.topolo.app",
|
|
1081
1077
|
"domains": [
|
|
1082
1078
|
"nexus.topolo.app"
|
|
1083
1079
|
],
|
|
1084
|
-
"routes": [
|
|
1080
|
+
"routes": [
|
|
1081
|
+
"nexus.topolo.app"
|
|
1082
|
+
],
|
|
1085
1083
|
"appId": null
|
|
1086
1084
|
},
|
|
1087
1085
|
{
|
|
@@ -2171,24 +2169,24 @@ var SUPPORTED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
2171
2169
|
"exclusiveMaximum",
|
|
2172
2170
|
"multipleOf"
|
|
2173
2171
|
]);
|
|
2174
|
-
function validateTopoloActionInput(schema,
|
|
2172
|
+
function validateTopoloActionInput(schema, input3) {
|
|
2175
2173
|
const schemaIssue = inspectSchema(schema, "$", schema, 0);
|
|
2176
2174
|
if (schemaIssue) return { valid: false, errors: [schemaIssue] };
|
|
2177
2175
|
const errors = [];
|
|
2178
|
-
validateNode(schema,
|
|
2176
|
+
validateNode(schema, input3, "$", schema, errors, 0);
|
|
2179
2177
|
return { valid: errors.length === 0, errors };
|
|
2180
2178
|
}
|
|
2181
|
-
function inspectSchema(schema,
|
|
2182
|
-
if (depth > 128) return { path:
|
|
2179
|
+
function inspectSchema(schema, path6, root, depth) {
|
|
2180
|
+
if (depth > 128) return { path: path6, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
|
|
2183
2181
|
const unsupportedKeyword = Object.keys(schema).find((keyword) => !SUPPORTED_SCHEMA_KEYWORDS.has(keyword));
|
|
2184
2182
|
if (unsupportedKeyword) {
|
|
2185
|
-
return { path:
|
|
2183
|
+
return { path: path6, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
|
|
2186
2184
|
}
|
|
2187
2185
|
if (typeof schema.format === "string" && !["uri", "email", "date-time"].includes(schema.format)) {
|
|
2188
|
-
return { path:
|
|
2186
|
+
return { path: path6, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
|
|
2189
2187
|
}
|
|
2190
2188
|
if (typeof schema.$ref === "string" && !resolveLocalReference(root, schema.$ref)) {
|
|
2191
|
-
return { path:
|
|
2189
|
+
return { path: path6, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
|
|
2192
2190
|
}
|
|
2193
2191
|
const children = [];
|
|
2194
2192
|
if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
|
|
@@ -2204,7 +2202,7 @@ function inspectSchema(schema, path4, root, depth) {
|
|
|
2204
2202
|
);
|
|
2205
2203
|
for (const [key, child] of children) {
|
|
2206
2204
|
if (!isRecord(child)) continue;
|
|
2207
|
-
const issue = inspectSchema(child, `${
|
|
2205
|
+
const issue = inspectSchema(child, `${path6}.${key}`, root, depth + 1);
|
|
2208
2206
|
if (issue) return issue;
|
|
2209
2207
|
}
|
|
2210
2208
|
return null;
|
|
@@ -2212,129 +2210,129 @@ function inspectSchema(schema, path4, root, depth) {
|
|
|
2212
2210
|
function arraySchemaEntries(keyword, value) {
|
|
2213
2211
|
return Array.isArray(value) ? value.map((candidate, index) => [`${keyword}[${index}]`, candidate]) : [];
|
|
2214
2212
|
}
|
|
2215
|
-
function validateNode(schema, value,
|
|
2213
|
+
function validateNode(schema, value, path6, root, errors, depth) {
|
|
2216
2214
|
if (depth > 128) {
|
|
2217
|
-
errors.push({ path:
|
|
2215
|
+
errors.push({ path: path6, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
|
|
2218
2216
|
return;
|
|
2219
2217
|
}
|
|
2220
2218
|
if (typeof schema.$ref === "string") {
|
|
2221
2219
|
const resolved = resolveLocalReference(root, schema.$ref);
|
|
2222
2220
|
if (!resolved) {
|
|
2223
|
-
errors.push({ path:
|
|
2221
|
+
errors.push({ path: path6, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
|
|
2224
2222
|
return;
|
|
2225
2223
|
}
|
|
2226
|
-
validateNode(resolved, value,
|
|
2224
|
+
validateNode(resolved, value, path6, root, errors, depth + 1);
|
|
2227
2225
|
}
|
|
2228
2226
|
if (Array.isArray(schema.allOf)) {
|
|
2229
2227
|
for (const candidate of schema.allOf) {
|
|
2230
|
-
if (isRecord(candidate)) validateNode(candidate, value,
|
|
2228
|
+
if (isRecord(candidate)) validateNode(candidate, value, path6, root, errors, depth + 1);
|
|
2231
2229
|
}
|
|
2232
2230
|
}
|
|
2233
2231
|
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
|
|
2234
|
-
errors.push({ path:
|
|
2232
|
+
errors.push({ path: path6, code: "any_of", message: "Value does not match any allowed schema." });
|
|
2235
2233
|
return;
|
|
2236
2234
|
}
|
|
2237
2235
|
if (Array.isArray(schema.oneOf)) {
|
|
2238
2236
|
const matches = schema.oneOf.filter((candidate) => candidateMatches(candidate, value, root, depth)).length;
|
|
2239
2237
|
if (matches !== 1) {
|
|
2240
|
-
errors.push({ path:
|
|
2238
|
+
errors.push({ path: path6, code: "one_of", message: "Value must match exactly one allowed schema." });
|
|
2241
2239
|
return;
|
|
2242
2240
|
}
|
|
2243
2241
|
}
|
|
2244
2242
|
if (value === null && schema.nullable === true) return;
|
|
2245
2243
|
if (Object.prototype.hasOwnProperty.call(schema, "const") && !sameJsonValue(value, schema.const)) {
|
|
2246
|
-
errors.push({ path:
|
|
2244
|
+
errors.push({ path: path6, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
|
|
2247
2245
|
return;
|
|
2248
2246
|
}
|
|
2249
2247
|
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => sameJsonValue(value, candidate))) {
|
|
2250
|
-
errors.push({ path:
|
|
2248
|
+
errors.push({ path: path6, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
|
|
2251
2249
|
return;
|
|
2252
2250
|
}
|
|
2253
2251
|
if (!matchesType(schema.type, value)) {
|
|
2254
|
-
errors.push({ path:
|
|
2252
|
+
errors.push({ path: path6, code: "type", message: `Expected ${String(schema.type)}.` });
|
|
2255
2253
|
return;
|
|
2256
2254
|
}
|
|
2257
|
-
if (typeof value === "string") validateString(schema, value,
|
|
2258
|
-
if (typeof value === "number") validateNumber(schema, value,
|
|
2259
|
-
if (Array.isArray(value)) validateArray(schema, value,
|
|
2260
|
-
if (isRecord(value)) validateObject(schema, value,
|
|
2255
|
+
if (typeof value === "string") validateString(schema, value, path6, errors);
|
|
2256
|
+
if (typeof value === "number") validateNumber(schema, value, path6, errors);
|
|
2257
|
+
if (Array.isArray(value)) validateArray(schema, value, path6, root, errors, depth);
|
|
2258
|
+
if (isRecord(value)) validateObject(schema, value, path6, root, errors, depth);
|
|
2261
2259
|
}
|
|
2262
|
-
function validateObject(schema, value,
|
|
2260
|
+
function validateObject(schema, value, path6, root, errors, depth) {
|
|
2263
2261
|
if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
|
|
2264
|
-
errors.push({ path:
|
|
2262
|
+
errors.push({ path: path6, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
|
|
2265
2263
|
}
|
|
2266
2264
|
if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
|
|
2267
|
-
errors.push({ path:
|
|
2265
|
+
errors.push({ path: path6, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
|
|
2268
2266
|
}
|
|
2269
2267
|
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
2270
2268
|
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
2271
2269
|
for (const key of required) {
|
|
2272
2270
|
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
2273
|
-
errors.push({ path: `${
|
|
2271
|
+
errors.push({ path: `${path6}.${key}`, code: "required", message: "Required property is missing." });
|
|
2274
2272
|
}
|
|
2275
2273
|
}
|
|
2276
2274
|
if (isRecord(schema.propertyNames)) {
|
|
2277
2275
|
for (const key of Object.keys(value)) {
|
|
2278
|
-
validateNode(schema.propertyNames, key, `${
|
|
2276
|
+
validateNode(schema.propertyNames, key, `${path6}.${key}`, root, errors, depth + 1);
|
|
2279
2277
|
}
|
|
2280
2278
|
}
|
|
2281
2279
|
for (const [key, child] of Object.entries(value)) {
|
|
2282
2280
|
const childSchema = properties[key];
|
|
2283
2281
|
if (isRecord(childSchema)) {
|
|
2284
|
-
validateNode(childSchema, child, `${
|
|
2282
|
+
validateNode(childSchema, child, `${path6}.${key}`, root, errors, depth + 1);
|
|
2285
2283
|
} else if (schema.additionalProperties === false) {
|
|
2286
|
-
errors.push({ path: `${
|
|
2284
|
+
errors.push({ path: `${path6}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
|
|
2287
2285
|
} else if (isRecord(schema.additionalProperties)) {
|
|
2288
|
-
validateNode(schema.additionalProperties, child, `${
|
|
2286
|
+
validateNode(schema.additionalProperties, child, `${path6}.${key}`, root, errors, depth + 1);
|
|
2289
2287
|
}
|
|
2290
2288
|
}
|
|
2291
2289
|
}
|
|
2292
|
-
function validateArray(schema, value,
|
|
2290
|
+
function validateArray(schema, value, path6, root, errors, depth) {
|
|
2293
2291
|
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
2294
|
-
errors.push({ path:
|
|
2292
|
+
errors.push({ path: path6, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
|
|
2295
2293
|
}
|
|
2296
2294
|
if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
|
|
2297
|
-
errors.push({ path:
|
|
2295
|
+
errors.push({ path: path6, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
|
|
2298
2296
|
}
|
|
2299
2297
|
if (schema.uniqueItems === true) {
|
|
2300
2298
|
const unique2 = new Set(value.map((item) => JSON.stringify(item)));
|
|
2301
|
-
if (unique2.size !== value.length) errors.push({ path:
|
|
2299
|
+
if (unique2.size !== value.length) errors.push({ path: path6, code: "unique_items", message: "Expected every item to be unique." });
|
|
2302
2300
|
}
|
|
2303
2301
|
if (isRecord(schema.items)) {
|
|
2304
|
-
value.forEach((item, index) => validateNode(schema.items, item, `${
|
|
2302
|
+
value.forEach((item, index) => validateNode(schema.items, item, `${path6}[${index}]`, root, errors, depth + 1));
|
|
2305
2303
|
}
|
|
2306
2304
|
}
|
|
2307
|
-
function validateString(schema, value,
|
|
2305
|
+
function validateString(schema, value, path6, errors) {
|
|
2308
2306
|
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
2309
|
-
errors.push({ path:
|
|
2307
|
+
errors.push({ path: path6, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
|
|
2310
2308
|
}
|
|
2311
2309
|
if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
|
|
2312
|
-
errors.push({ path:
|
|
2310
|
+
errors.push({ path: path6, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
|
|
2313
2311
|
}
|
|
2314
2312
|
if (typeof schema.pattern === "string") {
|
|
2315
2313
|
try {
|
|
2316
|
-
if (!new RegExp(schema.pattern).test(value)) errors.push({ path:
|
|
2314
|
+
if (!new RegExp(schema.pattern).test(value)) errors.push({ path: path6, code: "pattern", message: "Value does not match the required pattern." });
|
|
2317
2315
|
} catch {
|
|
2318
|
-
errors.push({ path:
|
|
2316
|
+
errors.push({ path: path6, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
|
|
2319
2317
|
}
|
|
2320
2318
|
}
|
|
2321
|
-
if (schema.format === "uri" && !isUrl(value)) errors.push({ path:
|
|
2322
|
-
if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path:
|
|
2323
|
-
if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path:
|
|
2319
|
+
if (schema.format === "uri" && !isUrl(value)) errors.push({ path: path6, code: "format_uri", message: "Expected an absolute URL." });
|
|
2320
|
+
if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path: path6, code: "format_email", message: "Expected an email address." });
|
|
2321
|
+
if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path: path6, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
|
|
2324
2322
|
}
|
|
2325
|
-
function validateNumber(schema, value,
|
|
2326
|
-
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path:
|
|
2327
|
-
if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path:
|
|
2323
|
+
function validateNumber(schema, value, path6, errors) {
|
|
2324
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path: path6, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
|
|
2325
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path: path6, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
|
|
2328
2326
|
const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum === true && typeof schema.minimum === "number" ? schema.minimum : null;
|
|
2329
2327
|
const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum === true && typeof schema.maximum === "number" ? schema.maximum : null;
|
|
2330
2328
|
if (exclusiveMinimum !== null && value <= exclusiveMinimum) {
|
|
2331
|
-
errors.push({ path:
|
|
2329
|
+
errors.push({ path: path6, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
|
|
2332
2330
|
}
|
|
2333
2331
|
if (exclusiveMaximum !== null && value >= exclusiveMaximum) {
|
|
2334
|
-
errors.push({ path:
|
|
2332
|
+
errors.push({ path: path6, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
|
|
2335
2333
|
}
|
|
2336
2334
|
if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
|
|
2337
|
-
errors.push({ path:
|
|
2335
|
+
errors.push({ path: path6, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
|
|
2338
2336
|
}
|
|
2339
2337
|
}
|
|
2340
2338
|
function candidateMatches(candidate, value, root, depth) {
|
|
@@ -2845,14 +2843,14 @@ var TopoloClient = class _TopoloClient {
|
|
|
2845
2843
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2846
2844
|
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
|
|
2847
2845
|
}
|
|
2848
|
-
const
|
|
2849
|
-
if (
|
|
2846
|
+
const record2 = raw;
|
|
2847
|
+
if (record2.algorithm !== "sha256-v1" || typeof record2.digest !== "string" || !/^[a-f0-9]{64}$/.test(record2.digest) || !Number.isSafeInteger(record2.actionCount) || Number(record2.actionCount) < 0) {
|
|
2850
2848
|
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
|
|
2851
2849
|
}
|
|
2852
2850
|
return {
|
|
2853
2851
|
algorithm: "sha256-v1",
|
|
2854
|
-
digest:
|
|
2855
|
-
actionCount: Number(
|
|
2852
|
+
digest: record2.digest,
|
|
2853
|
+
actionCount: Number(record2.actionCount)
|
|
2856
2854
|
};
|
|
2857
2855
|
}
|
|
2858
2856
|
/** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
|
|
@@ -2889,22 +2887,22 @@ var TopoloClient = class _TopoloClient {
|
|
|
2889
2887
|
if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2890
2888
|
return match;
|
|
2891
2889
|
}
|
|
2892
|
-
async callAction(action,
|
|
2890
|
+
async callAction(action, input3 = {}, options = {}) {
|
|
2893
2891
|
const entry = await this.getAction(action);
|
|
2894
|
-
return this.executeResolvedAction(entry,
|
|
2892
|
+
return this.executeResolvedAction(entry, input3, options);
|
|
2895
2893
|
}
|
|
2896
|
-
async validateActionInput(action,
|
|
2894
|
+
async validateActionInput(action, input3 = {}) {
|
|
2897
2895
|
const entry = await this.getAction(action);
|
|
2898
|
-
return { action: entry, validation: validateTopoloActionInput(entry.inputSchema,
|
|
2896
|
+
return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input3) };
|
|
2899
2897
|
}
|
|
2900
|
-
async planAction(action,
|
|
2898
|
+
async planAction(action, input3 = {}, options = {}) {
|
|
2901
2899
|
const entry = await this.getAction(action);
|
|
2902
|
-
const validation = validateTopoloActionInput(entry.inputSchema,
|
|
2900
|
+
const validation = validateTopoloActionInput(entry.inputSchema, input3);
|
|
2903
2901
|
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2904
2902
|
const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
|
|
2905
2903
|
let request = null;
|
|
2906
2904
|
if (validation.valid) {
|
|
2907
|
-
const mapped = mapActionInput(entry,
|
|
2905
|
+
const mapped = mapActionInput(entry, input3);
|
|
2908
2906
|
request = {
|
|
2909
2907
|
method: entry.method,
|
|
2910
2908
|
service: entry.appSlug || entry.appId,
|
|
@@ -2927,7 +2925,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2927
2925
|
nextActionIds: stringArray(entry.agentContract?.nextActionIds)
|
|
2928
2926
|
};
|
|
2929
2927
|
}
|
|
2930
|
-
async readAction(action,
|
|
2928
|
+
async readAction(action, input3 = {}, options = {}) {
|
|
2931
2929
|
const entry = await this.getAction(action);
|
|
2932
2930
|
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2933
2931
|
if (!entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
|
|
@@ -2936,9 +2934,9 @@ var TopoloClient = class _TopoloClient {
|
|
|
2936
2934
|
entry.requiredPermission ? [entry.requiredPermission] : []
|
|
2937
2935
|
);
|
|
2938
2936
|
}
|
|
2939
|
-
return this.executeResolvedAction(entry,
|
|
2937
|
+
return this.executeResolvedAction(entry, input3, options);
|
|
2940
2938
|
}
|
|
2941
|
-
async executeResolvedAction(entry,
|
|
2939
|
+
async executeResolvedAction(entry, input3, options) {
|
|
2942
2940
|
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2943
2941
|
if (agentAccess === "off") {
|
|
2944
2942
|
throw new TopoloPermissionError(
|
|
@@ -2951,14 +2949,14 @@ var TopoloClient = class _TopoloClient {
|
|
|
2951
2949
|
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
2952
2950
|
);
|
|
2953
2951
|
}
|
|
2954
|
-
const validation = validateTopoloActionInput(entry.inputSchema,
|
|
2952
|
+
const validation = validateTopoloActionInput(entry.inputSchema, input3);
|
|
2955
2953
|
if (!validation.valid) {
|
|
2956
2954
|
throw new TopoloSdkError(
|
|
2957
2955
|
"invalid_action_input",
|
|
2958
2956
|
`Topolo action "${entry.actionId}" input is invalid: ${validation.errors.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`
|
|
2959
2957
|
);
|
|
2960
2958
|
}
|
|
2961
|
-
const mapped = mapActionInput(entry,
|
|
2959
|
+
const mapped = mapActionInput(entry, input3);
|
|
2962
2960
|
const service = entry.appSlug || entry.appId;
|
|
2963
2961
|
return this.request({
|
|
2964
2962
|
service,
|
|
@@ -3050,15 +3048,15 @@ function resolveConfiguredAppId(slug, appIds) {
|
|
|
3050
3048
|
}
|
|
3051
3049
|
return null;
|
|
3052
3050
|
}
|
|
3053
|
-
function normalizeApiKeyResourceMetadata(
|
|
3054
|
-
if (!isRecord2(
|
|
3055
|
-
const resourceType = readString(
|
|
3056
|
-
const label = readString(
|
|
3057
|
-
const emptyLabel = readString(
|
|
3058
|
-
const resourceConcept = readString(
|
|
3059
|
-
const canonicalResourceType = readString(
|
|
3060
|
-
const defaultEntitlement = readString(
|
|
3061
|
-
const exceptionPolicy = readString(
|
|
3051
|
+
function normalizeApiKeyResourceMetadata(input3) {
|
|
3052
|
+
if (!isRecord2(input3)) return null;
|
|
3053
|
+
const resourceType = readString(input3.resourceType ?? input3.resource_type);
|
|
3054
|
+
const label = readString(input3.label);
|
|
3055
|
+
const emptyLabel = readString(input3.emptyLabel ?? input3.empty_label);
|
|
3056
|
+
const resourceConcept = readString(input3.resourceConcept ?? input3.resource_concept);
|
|
3057
|
+
const canonicalResourceType = readString(input3.canonicalResourceType ?? input3.canonical_resource_type);
|
|
3058
|
+
const defaultEntitlement = readString(input3.defaultEntitlement ?? input3.default_entitlement);
|
|
3059
|
+
const exceptionPolicy = readString(input3.exceptionPolicy ?? input3.exception_policy);
|
|
3062
3060
|
if (!resourceType || !label || !emptyLabel || resourceConcept !== "workspace" || canonicalResourceType !== "workspace" || defaultEntitlement !== "enterprise" || exceptionPolicy !== "app_or_organization") {
|
|
3063
3061
|
return null;
|
|
3064
3062
|
}
|
|
@@ -3072,11 +3070,11 @@ function normalizeApiKeyResourceMetadata(input2) {
|
|
|
3072
3070
|
exceptionPolicy
|
|
3073
3071
|
};
|
|
3074
3072
|
}
|
|
3075
|
-
function normalizeApiKeyResourceMetadataList(
|
|
3076
|
-
if (!Array.isArray(
|
|
3073
|
+
function normalizeApiKeyResourceMetadataList(input3) {
|
|
3074
|
+
if (!Array.isArray(input3)) return [];
|
|
3077
3075
|
const resources = [];
|
|
3078
3076
|
const seenTypes = /* @__PURE__ */ new Set();
|
|
3079
|
-
for (const item of
|
|
3077
|
+
for (const item of input3) {
|
|
3080
3078
|
const resource = normalizeApiKeyResourceMetadata(item);
|
|
3081
3079
|
if (!resource || seenTypes.has(resource.resourceType)) continue;
|
|
3082
3080
|
seenTypes.add(resource.resourceType);
|
|
@@ -3084,24 +3082,24 @@ function normalizeApiKeyResourceMetadataList(input2) {
|
|
|
3084
3082
|
}
|
|
3085
3083
|
return resources;
|
|
3086
3084
|
}
|
|
3087
|
-
function normalizeBindableResource(
|
|
3088
|
-
if (!isRecord2(
|
|
3089
|
-
const resourceType = readString(
|
|
3090
|
-
const resourceId = readString(
|
|
3085
|
+
function normalizeBindableResource(input3) {
|
|
3086
|
+
if (!isRecord2(input3)) return null;
|
|
3087
|
+
const resourceType = readString(input3.resourceType ?? input3.resource_type);
|
|
3088
|
+
const resourceId = readString(input3.resourceId ?? input3.resource_id);
|
|
3091
3089
|
if (!resourceType || !resourceId) return null;
|
|
3092
3090
|
return {
|
|
3093
3091
|
resourceType,
|
|
3094
3092
|
resourceId,
|
|
3095
|
-
label: readOptionalString(
|
|
3096
|
-
slug: readOptionalString(
|
|
3097
|
-
status: readOptionalString(
|
|
3098
|
-
organizationId: readOptionalString(
|
|
3099
|
-
metadata: isRecord2(
|
|
3093
|
+
label: readOptionalString(input3.label),
|
|
3094
|
+
slug: readOptionalString(input3.slug),
|
|
3095
|
+
status: readOptionalString(input3.status),
|
|
3096
|
+
organizationId: readOptionalString(input3.organizationId ?? input3.organization_id),
|
|
3097
|
+
metadata: isRecord2(input3.metadata) ? input3.metadata : {}
|
|
3100
3098
|
};
|
|
3101
3099
|
}
|
|
3102
|
-
function normalizeResourceBindings(
|
|
3103
|
-
if (!Array.isArray(
|
|
3104
|
-
return
|
|
3100
|
+
function normalizeResourceBindings(input3) {
|
|
3101
|
+
if (!Array.isArray(input3)) return [];
|
|
3102
|
+
return input3.flatMap((binding) => {
|
|
3105
3103
|
if (!isRecord2(binding)) return [];
|
|
3106
3104
|
const resourceType = readString(binding.resourceType ?? binding.resource_type);
|
|
3107
3105
|
const resourceId = readString(binding.resourceId ?? binding.resource_id);
|
|
@@ -3227,8 +3225,8 @@ function normalizeActionCatalogEntry(item) {
|
|
|
3227
3225
|
const name = pickActionString(a.name);
|
|
3228
3226
|
const appId = pickActionString(a.app_id) ?? pickActionString(a.appId);
|
|
3229
3227
|
const method = pickActionString(a.method)?.toUpperCase();
|
|
3230
|
-
const
|
|
3231
|
-
if (!actionId || !name || !appId || !method || !
|
|
3228
|
+
const path6 = pickActionString(a.path);
|
|
3229
|
+
if (!actionId || !name || !appId || !method || !path6 || !isHttpMethod(method)) {
|
|
3232
3230
|
return null;
|
|
3233
3231
|
}
|
|
3234
3232
|
return {
|
|
@@ -3243,7 +3241,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
3243
3241
|
appSlug: pickActionString(a.app_slug) ?? pickActionString(a.appSlug) ?? null,
|
|
3244
3242
|
appName: pickActionString(a.app_name) ?? pickActionString(a.appName) ?? null,
|
|
3245
3243
|
method,
|
|
3246
|
-
path:
|
|
3244
|
+
path: path6,
|
|
3247
3245
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
3248
3246
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
3249
3247
|
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
@@ -3277,9 +3275,9 @@ function plainObject(value) {
|
|
|
3277
3275
|
function stringArray(value) {
|
|
3278
3276
|
return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
|
|
3279
3277
|
}
|
|
3280
|
-
function mapActionInput(action,
|
|
3281
|
-
const remaining = { ...
|
|
3282
|
-
const
|
|
3278
|
+
function mapActionInput(action, input3) {
|
|
3279
|
+
const remaining = { ...input3 || {} };
|
|
3280
|
+
const path6 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
3283
3281
|
const value = remaining[key];
|
|
3284
3282
|
if (value === void 0 || value === null || value === "") {
|
|
3285
3283
|
throw new TopoloSdkError(
|
|
@@ -3296,14 +3294,14 @@ function mapActionInput(action, input2) {
|
|
|
3296
3294
|
delete remaining.body;
|
|
3297
3295
|
if (action.method === "GET") {
|
|
3298
3296
|
return {
|
|
3299
|
-
path:
|
|
3297
|
+
path: path6,
|
|
3300
3298
|
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
3301
3299
|
};
|
|
3302
3300
|
}
|
|
3303
3301
|
const query = objectToQuery(explicitQuery);
|
|
3304
3302
|
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
3305
3303
|
return {
|
|
3306
|
-
path:
|
|
3304
|
+
path: path6,
|
|
3307
3305
|
...query ? { query } : {},
|
|
3308
3306
|
body
|
|
3309
3307
|
};
|
|
@@ -3473,8 +3471,8 @@ var TopoloOAuth = class {
|
|
|
3473
3471
|
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3474
3472
|
return this.#postForm("/api/developer-oauth/token", body);
|
|
3475
3473
|
}
|
|
3476
|
-
async #postForm(
|
|
3477
|
-
const url = new URL(
|
|
3474
|
+
async #postForm(path6, body) {
|
|
3475
|
+
const url = new URL(path6, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
3478
3476
|
const res = await this.fetchImpl(url.toString(), {
|
|
3479
3477
|
method: "POST",
|
|
3480
3478
|
headers: {
|
|
@@ -3490,7 +3488,7 @@ var TopoloOAuth = class {
|
|
|
3490
3488
|
const err = parsed;
|
|
3491
3489
|
throw new TopoloAuthError(err.error_description || err.error, err.error);
|
|
3492
3490
|
}
|
|
3493
|
-
throw new TopoloHttpError("auth",
|
|
3491
|
+
throw new TopoloHttpError("auth", path6, res.status, parsed);
|
|
3494
3492
|
}
|
|
3495
3493
|
return parsed;
|
|
3496
3494
|
}
|
|
@@ -3528,13 +3526,13 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
|
3528
3526
|
body;
|
|
3529
3527
|
service;
|
|
3530
3528
|
path;
|
|
3531
|
-
constructor(service,
|
|
3532
|
-
super("http_error", message ?? `HTTP ${status} from ${service}${
|
|
3529
|
+
constructor(service, path6, status, body, message) {
|
|
3530
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path6}`);
|
|
3533
3531
|
this.name = "TopoloHttpError";
|
|
3534
3532
|
this.status = status;
|
|
3535
3533
|
this.body = body;
|
|
3536
3534
|
this.service = service;
|
|
3537
|
-
this.path =
|
|
3535
|
+
this.path = path6;
|
|
3538
3536
|
}
|
|
3539
3537
|
};
|
|
3540
3538
|
var APP_API_URLS2 = {
|
|
@@ -3665,8 +3663,8 @@ var TopoloOAuth2 = class {
|
|
|
3665
3663
|
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
3666
3664
|
return this.#postForm("/api/developer-oauth/token", body);
|
|
3667
3665
|
}
|
|
3668
|
-
async #postForm(
|
|
3669
|
-
const url = new URL(
|
|
3666
|
+
async #postForm(path6, body) {
|
|
3667
|
+
const url = new URL(path6, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
3670
3668
|
const res = await this.fetchImpl(url.toString(), {
|
|
3671
3669
|
method: "POST",
|
|
3672
3670
|
headers: {
|
|
@@ -3682,7 +3680,7 @@ var TopoloOAuth2 = class {
|
|
|
3682
3680
|
const err = parsed;
|
|
3683
3681
|
throw new TopoloAuthError2(err.error_description || err.error, err.error);
|
|
3684
3682
|
}
|
|
3685
|
-
throw new TopoloHttpError2("auth",
|
|
3683
|
+
throw new TopoloHttpError2("auth", path6, res.status, parsed);
|
|
3686
3684
|
}
|
|
3687
3685
|
return parsed;
|
|
3688
3686
|
}
|
|
@@ -3712,22 +3710,22 @@ function configPath() {
|
|
|
3712
3710
|
return join(base, "topolo", "config.json");
|
|
3713
3711
|
}
|
|
3714
3712
|
async function readConfig() {
|
|
3715
|
-
const
|
|
3716
|
-
if (!existsSync(
|
|
3713
|
+
const path6 = configPath();
|
|
3714
|
+
if (!existsSync(path6)) return {};
|
|
3717
3715
|
try {
|
|
3718
|
-
const raw = await readFile(
|
|
3716
|
+
const raw = await readFile(path6, "utf8");
|
|
3719
3717
|
return JSON.parse(raw);
|
|
3720
3718
|
} catch {
|
|
3721
3719
|
return {};
|
|
3722
3720
|
}
|
|
3723
3721
|
}
|
|
3724
3722
|
async function writeConfig(config) {
|
|
3725
|
-
const
|
|
3726
|
-
await mkdir(dirname(
|
|
3727
|
-
await writeFile(
|
|
3723
|
+
const path6 = configPath();
|
|
3724
|
+
await mkdir(dirname(path6), { recursive: true });
|
|
3725
|
+
await writeFile(path6, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3728
3726
|
if (platform() !== "win32") {
|
|
3729
3727
|
try {
|
|
3730
|
-
await chmod(
|
|
3728
|
+
await chmod(path6, 384);
|
|
3731
3729
|
} catch {
|
|
3732
3730
|
}
|
|
3733
3731
|
}
|
|
@@ -4023,13 +4021,14 @@ function normalizeError(err) {
|
|
|
4023
4021
|
const hintMap = {
|
|
4024
4022
|
auth_error: "Run `topolo auth login` or set TOPOLO_API_KEY.",
|
|
4025
4023
|
confirmation_required: "Review the action and rerun with --confirm to allow it.",
|
|
4026
|
-
permission_denied: "Check your API key scopes or role assignments in TopoloDevelopers."
|
|
4024
|
+
permission_denied: "Check your API key scopes or role assignments in TopoloDevelopers.",
|
|
4025
|
+
immutable_drift: "Restore the manifest identity fields to the registered values; immutable ids cannot be updated."
|
|
4027
4026
|
};
|
|
4028
4027
|
return {
|
|
4029
4028
|
message: err.message,
|
|
4030
4029
|
code,
|
|
4031
4030
|
hint: code ? hintMap[code] : void 0,
|
|
4032
|
-
exitCode: code === "auth_error" || code === "confirmation_required" ? 2 : code === "permission_denied" ? 3 : 1
|
|
4031
|
+
exitCode: code === "auth_error" || code === "confirmation_required" ? 2 : code === "permission_denied" ? 3 : code === "immutable_drift" ? 4 : 1
|
|
4033
4032
|
};
|
|
4034
4033
|
}
|
|
4035
4034
|
return { message: String(err), exitCode: 1 };
|
|
@@ -4046,9 +4045,9 @@ function readCliVersion() {
|
|
|
4046
4045
|
join2(here, "package.json"),
|
|
4047
4046
|
join2(here, "..", "package.json")
|
|
4048
4047
|
];
|
|
4049
|
-
for (const
|
|
4048
|
+
for (const path6 of candidates) {
|
|
4050
4049
|
try {
|
|
4051
|
-
const pkg = JSON.parse(readFileSync(
|
|
4050
|
+
const pkg = JSON.parse(readFileSync(path6, "utf8"));
|
|
4052
4051
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
4053
4052
|
} catch {
|
|
4054
4053
|
}
|
|
@@ -4407,7 +4406,7 @@ function registerApiCommand(program2) {
|
|
|
4407
4406
|
"-q, --query <kv...>",
|
|
4408
4407
|
"Query params as key=value (repeatable), e.g. --query source=cli page=2"
|
|
4409
4408
|
).option("--resource-type <type>", "Cataloged app-local workspace resource alias for resource-scoped API keys").option("--resource-id <id>", "Workspace resource id for resource-scoped API keys").option("--confirm", "Acknowledge a mutating request").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
|
|
4410
|
-
async (service, method,
|
|
4409
|
+
async (service, method, path6, opts) => {
|
|
4411
4410
|
const mode = resolveOutputMode(opts.json);
|
|
4412
4411
|
try {
|
|
4413
4412
|
const upperMethod = method.toUpperCase();
|
|
@@ -4446,7 +4445,7 @@ function registerApiCommand(program2) {
|
|
|
4446
4445
|
const result = await topolo.client.request({
|
|
4447
4446
|
service,
|
|
4448
4447
|
method: httpMethod,
|
|
4449
|
-
path:
|
|
4448
|
+
path: path6,
|
|
4450
4449
|
body,
|
|
4451
4450
|
...query ? { query } : {},
|
|
4452
4451
|
...resource ? { resource } : {},
|
|
@@ -4539,9 +4538,9 @@ function registerActionsCommand(program2) {
|
|
|
4539
4538
|
if (!topolo) {
|
|
4540
4539
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
4541
4540
|
}
|
|
4542
|
-
const
|
|
4541
|
+
const input3 = parseJsonObject(opts.data || "{}");
|
|
4543
4542
|
const resource = parseResourceOption(opts.resourceType, opts.resourceId);
|
|
4544
|
-
const result = await topolo.client.callAction(action,
|
|
4543
|
+
const result = await topolo.client.callAction(action, input3, {
|
|
4545
4544
|
confirm: opts.confirm === true,
|
|
4546
4545
|
...resource ? { resource } : {}
|
|
4547
4546
|
});
|
|
@@ -4610,13 +4609,13 @@ async function requiredClient() {
|
|
|
4610
4609
|
function parseJsonObject(value) {
|
|
4611
4610
|
let source = value;
|
|
4612
4611
|
if (value.startsWith("@")) {
|
|
4613
|
-
const
|
|
4614
|
-
if (!
|
|
4612
|
+
const path6 = value.slice(1);
|
|
4613
|
+
if (!path6) throw new Error("--data @file path must not be empty");
|
|
4615
4614
|
try {
|
|
4616
|
-
source = readFileSync2(
|
|
4615
|
+
source = readFileSync2(path6, "utf8");
|
|
4617
4616
|
} catch (err) {
|
|
4618
4617
|
const detail = err instanceof Error ? err.message : String(err);
|
|
4619
|
-
throw new Error(`Could not read --data file ${
|
|
4618
|
+
throw new Error(`Could not read --data file ${path6}: ${detail}`);
|
|
4620
4619
|
}
|
|
4621
4620
|
}
|
|
4622
4621
|
let parsed;
|
|
@@ -4690,8 +4689,8 @@ function formatActionPlan(plan) {
|
|
|
4690
4689
|
|
|
4691
4690
|
// src/commands/apps.ts
|
|
4692
4691
|
import { execSync } from "child_process";
|
|
4693
|
-
import { existsSync as
|
|
4694
|
-
import
|
|
4692
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
4693
|
+
import path4 from "path";
|
|
4695
4694
|
|
|
4696
4695
|
// src/commands/app-audit-local.ts
|
|
4697
4696
|
import {
|
|
@@ -5282,8 +5281,8 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
5282
5281
|
evidence: verificationEvidence
|
|
5283
5282
|
});
|
|
5284
5283
|
}
|
|
5285
|
-
const usesNodeCheck = evidence.packageJsons.some((
|
|
5286
|
-
const scripts =
|
|
5284
|
+
const usesNodeCheck = evidence.packageJsons.some((record2) => {
|
|
5285
|
+
const scripts = record2.packageJson.scripts ?? {};
|
|
5287
5286
|
return Object.values(scripts).some((cmd) => typeof cmd === "string" && /node\s+--check/.test(cmd));
|
|
5288
5287
|
});
|
|
5289
5288
|
if (hasBuild && usesNodeCheck) {
|
|
@@ -5549,35 +5548,35 @@ function verificationEvidenceFor(evidence) {
|
|
|
5549
5548
|
"test:auth-contract": "test",
|
|
5550
5549
|
validate: "validate"
|
|
5551
5550
|
};
|
|
5552
|
-
for (const
|
|
5553
|
-
const scripts =
|
|
5554
|
-
const isRootPackage =
|
|
5551
|
+
for (const record2 of evidence.packageJsons) {
|
|
5552
|
+
const scripts = record2.packageJson.scripts ?? {};
|
|
5553
|
+
const isRootPackage = record2.path === evidence.packageJsonPath;
|
|
5555
5554
|
const seenCanonical = /* @__PURE__ */ new Set();
|
|
5556
5555
|
for (const [scriptName, canonical] of Object.entries(scriptAliases)) {
|
|
5557
5556
|
if (!scripts[scriptName]) continue;
|
|
5558
5557
|
if (seenCanonical.has(canonical)) continue;
|
|
5559
5558
|
seenCanonical.add(canonical);
|
|
5560
|
-
items.push(isRootPackage ? `script=${canonical}` : `script=${packageLabel(
|
|
5559
|
+
items.push(isRootPackage ? `script=${canonical}` : `script=${packageLabel(record2)}:${canonical}`);
|
|
5561
5560
|
}
|
|
5562
5561
|
}
|
|
5563
5562
|
return unique(items);
|
|
5564
5563
|
}
|
|
5565
5564
|
function dependencyNames(evidence) {
|
|
5566
5565
|
return new Set(
|
|
5567
|
-
evidence.packageJsons.flatMap((
|
|
5568
|
-
...Object.keys(
|
|
5569
|
-
...Object.keys(
|
|
5570
|
-
...Object.keys(
|
|
5566
|
+
evidence.packageJsons.flatMap((record2) => [
|
|
5567
|
+
...Object.keys(record2.packageJson.dependencies ?? {}),
|
|
5568
|
+
...Object.keys(record2.packageJson.devDependencies ?? {}),
|
|
5569
|
+
...Object.keys(record2.packageJson.peerDependencies ?? {})
|
|
5571
5570
|
])
|
|
5572
5571
|
);
|
|
5573
5572
|
}
|
|
5574
5573
|
function packageDependencyVersions(evidence, packageName) {
|
|
5575
5574
|
return unique(
|
|
5576
|
-
evidence.packageJsons.flatMap((
|
|
5575
|
+
evidence.packageJsons.flatMap((record2) => {
|
|
5577
5576
|
const specs = [
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5577
|
+
record2.packageJson.dependencies?.[packageName],
|
|
5578
|
+
record2.packageJson.devDependencies?.[packageName],
|
|
5579
|
+
record2.packageJson.peerDependencies?.[packageName]
|
|
5581
5580
|
];
|
|
5582
5581
|
return specs.filter((spec) => typeof spec === "string");
|
|
5583
5582
|
})
|
|
@@ -5703,8 +5702,8 @@ function collectPackageJsons(root) {
|
|
|
5703
5702
|
function readPackageJson(filePath) {
|
|
5704
5703
|
return readJson(filePath);
|
|
5705
5704
|
}
|
|
5706
|
-
function packageLabel(
|
|
5707
|
-
return
|
|
5705
|
+
function packageLabel(record2) {
|
|
5706
|
+
return record2.packageJson.name ?? relativeTopoloPath(path.dirname(record2.path));
|
|
5708
5707
|
}
|
|
5709
5708
|
function sortedDirEntries(dir) {
|
|
5710
5709
|
return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -5793,14 +5792,14 @@ import { constants } from "fs";
|
|
|
5793
5792
|
import { access } from "fs/promises";
|
|
5794
5793
|
import { relative, resolve } from "path";
|
|
5795
5794
|
function normalizeHomePath(value) {
|
|
5796
|
-
const
|
|
5797
|
-
if (!
|
|
5795
|
+
const path6 = String(value || "/dashboard").trim();
|
|
5796
|
+
if (!path6.startsWith("/") || path6.startsWith("//") || /[?#]/.test(path6)) {
|
|
5798
5797
|
throw new Error("Invalid home path. Use an absolute application path such as /dashboard.");
|
|
5799
5798
|
}
|
|
5800
|
-
if (["/", "/login", "/auth/callback", "/getting-started"].includes(
|
|
5799
|
+
if (["/", "/login", "/auth/callback", "/getting-started"].includes(path6)) {
|
|
5801
5800
|
throw new Error("Home path must be a protected product route, not a reserved entry route.");
|
|
5802
5801
|
}
|
|
5803
|
-
return
|
|
5802
|
+
return path6.replace(/\/$/, "") || "/dashboard";
|
|
5804
5803
|
}
|
|
5805
5804
|
function normalizeHomeLabel(value) {
|
|
5806
5805
|
const label = String(value || "Dashboard").trim();
|
|
@@ -5941,9 +5940,9 @@ function toTitle(value) {
|
|
|
5941
5940
|
function json(value) {
|
|
5942
5941
|
return JSON.stringify(value, null, 2);
|
|
5943
5942
|
}
|
|
5944
|
-
async function pathExists(
|
|
5943
|
+
async function pathExists(path6) {
|
|
5945
5944
|
try {
|
|
5946
|
-
await access(
|
|
5945
|
+
await access(path6, constants.F_OK);
|
|
5947
5946
|
return true;
|
|
5948
5947
|
} catch {
|
|
5949
5948
|
return false;
|
|
@@ -6479,11 +6478,20 @@ function appIdentityTs(application) {
|
|
|
6479
6478
|
"}",
|
|
6480
6479
|
"",
|
|
6481
6480
|
"export function getBrowserAuthBaseUrl(): string {",
|
|
6482
|
-
"
|
|
6483
|
-
"
|
|
6484
|
-
"
|
|
6485
|
-
"
|
|
6486
|
-
"
|
|
6481
|
+
" // A declared URL always wins; deployed builds set VITE_TOPOLO_AUTH_URL in",
|
|
6482
|
+
" // their per-environment build commands, so the host map below only covers",
|
|
6483
|
+
" // platform domains. Anything else fails loudly \u2014 an unknown host",
|
|
6484
|
+
" // (localhost, workers.dev previews) must never silently authenticate",
|
|
6485
|
+
" // against production Auth.",
|
|
6486
|
+
" const declared = import.meta.env.VITE_TOPOLO_AUTH_URL;",
|
|
6487
|
+
" if (declared) return declared.replace(/\\/+$/, '');",
|
|
6488
|
+
" const hostname = typeof window !== 'undefined' ? window.location.hostname : '';",
|
|
6489
|
+
" if (hostname.endsWith('.stg.topolo.us')) return 'https://auth.stg.topolo.us';",
|
|
6490
|
+
" if (hostname.endsWith('.topolo.dev')) return 'https://auth.topolo.dev';",
|
|
6491
|
+
" if (hostname.endsWith('.topolo.app')) return 'https://auth.topolo.app';",
|
|
6492
|
+
" throw new Error(",
|
|
6493
|
+
" `VITE_TOPOLO_AUTH_URL must be set for host \"${hostname || 'non-browser context'}\" \u2014 refusing to default to production Auth.`,",
|
|
6494
|
+
" );",
|
|
6487
6495
|
"}",
|
|
6488
6496
|
"",
|
|
6489
6497
|
"// Canonical browser app id. index.html bakes %VITE_TOPOLO_APP_ID% into",
|
|
@@ -7218,7 +7226,12 @@ import { createAccountLinkPayload, createTopoloAuthorizationUrl, exchangeTopoloC
|
|
|
7218
7226
|
|
|
7219
7227
|
type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';
|
|
7220
7228
|
const APP_NAME = '${escapeSingleQuoted(appName)}';
|
|
7221
|
-
|
|
7229
|
+
// Environment-aware default: hardcoding the production host would point a
|
|
7230
|
+
// staging build at prod Auth whenever the VITE_ vars were dropped.
|
|
7231
|
+
const DEFAULT_AUTH_URL = window.location.hostname.endsWith('.stg.topolo.us')
|
|
7232
|
+
? 'https://auth.stg.topolo.us'
|
|
7233
|
+
: 'https://auth.topolo.app';
|
|
7234
|
+
const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? import.meta.env.VITE_TOPOLO_AUTH_BASE_URL ?? DEFAULT_AUTH_URL;
|
|
7222
7235
|
const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';
|
|
7223
7236
|
const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || \`\${window.location.origin}/auth/callback\`;
|
|
7224
7237
|
const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'openid profile email organization:read entitlements:read').split(/[\\s,]+/).filter(Boolean);
|
|
@@ -7317,235 +7330,6 @@ function webWorkerTs(application, api) {
|
|
|
7317
7330
|
""
|
|
7318
7331
|
].join("\n");
|
|
7319
7332
|
}
|
|
7320
|
-
function platformWebWranglerToml(application, api) {
|
|
7321
|
-
const slug = toSlug(application.id);
|
|
7322
|
-
const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
|
|
7323
|
-
return [
|
|
7324
|
-
`name = "topolo-${slug}"`,
|
|
7325
|
-
'main = "src/worker.ts"',
|
|
7326
|
-
`compatibility_date = "${CLOUDFLARE_COMPATIBILITY_DATE}"`,
|
|
7327
|
-
'compatibility_flags = ["nodejs_compat"]',
|
|
7328
|
-
`account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7329
|
-
"workers_dev = false",
|
|
7330
|
-
"",
|
|
7331
|
-
"[assets]",
|
|
7332
|
-
'directory = "./dist"',
|
|
7333
|
-
'binding = "ASSETS"',
|
|
7334
|
-
'not_found_handling = "single-page-application"',
|
|
7335
|
-
...api ? ['run_worker_first = ["/api/*"]'] : [],
|
|
7336
|
-
"",
|
|
7337
|
-
"[observability]",
|
|
7338
|
-
"enabled = true",
|
|
7339
|
-
"",
|
|
7340
|
-
"[[routes]]",
|
|
7341
|
-
`pattern = "${host}"`,
|
|
7342
|
-
"custom_domain = true",
|
|
7343
|
-
"",
|
|
7344
|
-
"[vars]",
|
|
7345
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7346
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7347
|
-
"",
|
|
7348
|
-
...api ? [
|
|
7349
|
-
"[[services]]",
|
|
7350
|
-
'binding = "API"',
|
|
7351
|
-
`service = "topolo-${slug}-api"`,
|
|
7352
|
-
""
|
|
7353
|
-
] : [],
|
|
7354
|
-
"[env.development]",
|
|
7355
|
-
`name = "topolo-${slug}-development"`,
|
|
7356
|
-
`account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7357
|
-
"workers_dev = true",
|
|
7358
|
-
"",
|
|
7359
|
-
"[env.development.vars]",
|
|
7360
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7361
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7362
|
-
"",
|
|
7363
|
-
"[[env.development.routes]]",
|
|
7364
|
-
`pattern = "${slug}.topolo.dev"`,
|
|
7365
|
-
"custom_domain = true",
|
|
7366
|
-
"",
|
|
7367
|
-
"[env.development.assets]",
|
|
7368
|
-
'directory = "./dist"',
|
|
7369
|
-
'binding = "ASSETS"',
|
|
7370
|
-
'not_found_handling = "single-page-application"',
|
|
7371
|
-
...api ? ['run_worker_first = ["/api/*"]'] : [],
|
|
7372
|
-
"",
|
|
7373
|
-
...api ? [
|
|
7374
|
-
"[[env.development.services]]",
|
|
7375
|
-
'binding = "API"',
|
|
7376
|
-
`service = "topolo-${slug}-api-development"`,
|
|
7377
|
-
""
|
|
7378
|
-
] : [],
|
|
7379
|
-
"[env.staging]",
|
|
7380
|
-
`name = "topolo-${slug}-staging"`,
|
|
7381
|
-
`account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7382
|
-
"workers_dev = true",
|
|
7383
|
-
"",
|
|
7384
|
-
"[env.staging.vars]",
|
|
7385
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7386
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7387
|
-
"",
|
|
7388
|
-
"[[env.staging.routes]]",
|
|
7389
|
-
`pattern = "${slug}.stg.topolo.us"`,
|
|
7390
|
-
"custom_domain = true",
|
|
7391
|
-
"",
|
|
7392
|
-
"[env.staging.assets]",
|
|
7393
|
-
'directory = "./dist"',
|
|
7394
|
-
'binding = "ASSETS"',
|
|
7395
|
-
'not_found_handling = "single-page-application"',
|
|
7396
|
-
...api ? ['run_worker_first = ["/api/*"]'] : [],
|
|
7397
|
-
"",
|
|
7398
|
-
...api ? [
|
|
7399
|
-
"[[env.staging.services]]",
|
|
7400
|
-
'binding = "API"',
|
|
7401
|
-
`service = "topolo-${slug}-api-staging"`,
|
|
7402
|
-
""
|
|
7403
|
-
] : [],
|
|
7404
|
-
"[env.production]",
|
|
7405
|
-
`name = "topolo-${slug}-production"`,
|
|
7406
|
-
`account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7407
|
-
"workers_dev = false",
|
|
7408
|
-
"",
|
|
7409
|
-
"[env.production.vars]",
|
|
7410
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7411
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7412
|
-
"",
|
|
7413
|
-
"[[env.production.routes]]",
|
|
7414
|
-
`pattern = "${host}"`,
|
|
7415
|
-
"custom_domain = true",
|
|
7416
|
-
"",
|
|
7417
|
-
"[env.production.assets]",
|
|
7418
|
-
'directory = "./dist"',
|
|
7419
|
-
'binding = "ASSETS"',
|
|
7420
|
-
'not_found_handling = "single-page-application"',
|
|
7421
|
-
...api ? ['run_worker_first = ["/api/*"]'] : [],
|
|
7422
|
-
"",
|
|
7423
|
-
...api ? [
|
|
7424
|
-
"[[env.production.services]]",
|
|
7425
|
-
'binding = "API"',
|
|
7426
|
-
`service = "topolo-${slug}-api-production"`,
|
|
7427
|
-
""
|
|
7428
|
-
] : []
|
|
7429
|
-
].join("\n");
|
|
7430
|
-
}
|
|
7431
|
-
function platformApiWranglerToml(application, resources, profile) {
|
|
7432
|
-
const slug = toSlug(application.id);
|
|
7433
|
-
const resource = resourceConfig(application, profile);
|
|
7434
|
-
const lines = [
|
|
7435
|
-
`name = "topolo-${slug}-api"`,
|
|
7436
|
-
'main = "src/worker.ts"',
|
|
7437
|
-
`compatibility_date = "${CLOUDFLARE_COMPATIBILITY_DATE}"`,
|
|
7438
|
-
'compatibility_flags = ["nodejs_compat"]',
|
|
7439
|
-
`account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7440
|
-
"workers_dev = true",
|
|
7441
|
-
"",
|
|
7442
|
-
"[observability]",
|
|
7443
|
-
"enabled = true",
|
|
7444
|
-
"",
|
|
7445
|
-
"[vars]",
|
|
7446
|
-
'AUTH_BASE_URL = "https://auth.topolo.app"',
|
|
7447
|
-
'NEXUS_GATEWAY_URL = "https://nexus.topolo.app"',
|
|
7448
|
-
`NEXUS_APP_ID = "${slug}"`,
|
|
7449
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7450
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7451
|
-
""
|
|
7452
|
-
];
|
|
7453
|
-
appendResourceWranglerBlocks(lines, resources, resource, "production");
|
|
7454
|
-
lines.push(
|
|
7455
|
-
"[env.development]",
|
|
7456
|
-
`name = "topolo-${slug}-api-development"`,
|
|
7457
|
-
`account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7458
|
-
"workers_dev = true",
|
|
7459
|
-
"",
|
|
7460
|
-
"[env.development.vars]",
|
|
7461
|
-
'AUTH_BASE_URL = "https://auth.topolo.dev"',
|
|
7462
|
-
'NEXUS_GATEWAY_URL = "https://nexus.topolo.dev"',
|
|
7463
|
-
`NEXUS_APP_ID = "${slug}"`,
|
|
7464
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7465
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7466
|
-
'ENVIRONMENT = "development"',
|
|
7467
|
-
""
|
|
7468
|
-
);
|
|
7469
|
-
appendResourceWranglerBlocks(lines, resources, resource, "development");
|
|
7470
|
-
lines.push(
|
|
7471
|
-
"[env.staging]",
|
|
7472
|
-
`name = "topolo-${slug}-api-staging"`,
|
|
7473
|
-
`account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7474
|
-
"workers_dev = true",
|
|
7475
|
-
"",
|
|
7476
|
-
"[env.staging.vars]",
|
|
7477
|
-
'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
|
|
7478
|
-
'NEXUS_GATEWAY_URL = "https://nexus.stg.topolo.us"',
|
|
7479
|
-
`NEXUS_APP_ID = "${slug}"`,
|
|
7480
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7481
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7482
|
-
""
|
|
7483
|
-
);
|
|
7484
|
-
appendResourceWranglerBlocks(lines, resources, resource, "staging");
|
|
7485
|
-
lines.push(
|
|
7486
|
-
"[env.production]",
|
|
7487
|
-
`name = "topolo-${slug}-api-production"`,
|
|
7488
|
-
`account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
|
|
7489
|
-
"workers_dev = false",
|
|
7490
|
-
"",
|
|
7491
|
-
"[env.production.vars]",
|
|
7492
|
-
'AUTH_BASE_URL = "https://auth.topolo.app"',
|
|
7493
|
-
'NEXUS_GATEWAY_URL = "https://nexus.topolo.app"',
|
|
7494
|
-
`NEXUS_APP_ID = "${slug}"`,
|
|
7495
|
-
`APP_ID = "${application.appId ?? ""}"`,
|
|
7496
|
-
`VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
|
|
7497
|
-
""
|
|
7498
|
-
);
|
|
7499
|
-
appendResourceWranglerBlocks(lines, resources, resource, "production-env");
|
|
7500
|
-
return lines.join("\n");
|
|
7501
|
-
}
|
|
7502
|
-
function appendResourceWranglerBlocks(lines, resources, resource, environment) {
|
|
7503
|
-
const prefix = environment === "staging" ? "env.staging." : environment === "development" ? "env.development." : environment === "production-env" ? "env.production." : "";
|
|
7504
|
-
const envName = environment === "staging" ? "Staging" : environment === "development" ? "Development" : "Production";
|
|
7505
|
-
const pick = (dev, staging, prod) => environment === "development" ? dev : environment === "staging" ? staging : prod;
|
|
7506
|
-
if (resources.d1) {
|
|
7507
|
-
lines.push(
|
|
7508
|
-
`[[${prefix}d1_databases]]`,
|
|
7509
|
-
`binding = "${resource.d1Binding}"`,
|
|
7510
|
-
`database_name = "${pick(resource.d1DevelopmentName, resource.d1StagingName, resource.d1ProductionName)}"`,
|
|
7511
|
-
'database_id = "provision-after-scaffold"',
|
|
7512
|
-
'migrations_dir = "migrations"',
|
|
7513
|
-
""
|
|
7514
|
-
);
|
|
7515
|
-
}
|
|
7516
|
-
if (resources.r2) {
|
|
7517
|
-
lines.push(
|
|
7518
|
-
`[[${prefix}r2_buckets]]`,
|
|
7519
|
-
`binding = "${resource.r2Binding}"`,
|
|
7520
|
-
`bucket_name = "${pick(resource.r2DevelopmentName, resource.r2StagingName, resource.r2ProductionName)}"`,
|
|
7521
|
-
""
|
|
7522
|
-
);
|
|
7523
|
-
}
|
|
7524
|
-
if (resources.queue) {
|
|
7525
|
-
lines.push(
|
|
7526
|
-
`[[${prefix}queues.producers]]`,
|
|
7527
|
-
`binding = "${resource.queueBinding}"`,
|
|
7528
|
-
`queue = "${pick(resource.queueDevelopmentName, resource.queueStagingName, resource.queueProductionName)}"`,
|
|
7529
|
-
"",
|
|
7530
|
-
`[[${prefix}queues.consumers]]`,
|
|
7531
|
-
`queue = "${pick(resource.queueDevelopmentName, resource.queueStagingName, resource.queueProductionName)}"`,
|
|
7532
|
-
""
|
|
7533
|
-
);
|
|
7534
|
-
}
|
|
7535
|
-
if (resources.workflow) {
|
|
7536
|
-
lines.push(
|
|
7537
|
-
`[[${prefix}workflows]]`,
|
|
7538
|
-
`binding = "${resource.workflowBinding}"`,
|
|
7539
|
-
`name = "${pick(resource.workflowDevelopmentName, resource.workflowStagingName, resource.workflowProductionName)}"`,
|
|
7540
|
-
`class_name = "${resource.workflowClassName}"`,
|
|
7541
|
-
""
|
|
7542
|
-
);
|
|
7543
|
-
}
|
|
7544
|
-
if (resources.containerProcessing) {
|
|
7545
|
-
lines.push(`# ${envName} container metadata is declared in topolo.app.json and topolo.cloudcontrol.json.`);
|
|
7546
|
-
lines.push("");
|
|
7547
|
-
}
|
|
7548
|
-
}
|
|
7549
7333
|
function wranglerToml(application, target, resources, profile) {
|
|
7550
7334
|
const slug = toSlug(application.id);
|
|
7551
7335
|
const workerName = target === "platform" ? `topolo-${slug}` : application.packageName;
|
|
@@ -7765,7 +7549,10 @@ function platformWorkerTs(application, options) {
|
|
|
7765
7549
|
" // Auth verification base URL is environment-driven (staging/prod/dev),",
|
|
7766
7550
|
" // so rt.verifyAuth() targets the correct Auth service per deploy.",
|
|
7767
7551
|
" authBaseUrl: (env) => env.AUTH_BASE_URL,",
|
|
7768
|
-
"
|
|
7552
|
+
" // The runtime's /api/auth/* gateway serves the app-switcher catalog,",
|
|
7553
|
+
" // preferences, and theme for the shared app shell \u2014 it must run before",
|
|
7554
|
+
" // the app's own auth so unentitled users still get the switcher.",
|
|
7555
|
+
" authGateway: { enabled: true },",
|
|
7769
7556
|
" actionContracts: topoloActionContracts,",
|
|
7770
7557
|
"});",
|
|
7771
7558
|
"",
|
|
@@ -7918,8 +7705,13 @@ function developerWorkerTs(application) {
|
|
|
7918
7705
|
" const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;",
|
|
7919
7706
|
" if (!token) return { ok: false, status: 401, error: 'Bearer token required' };",
|
|
7920
7707
|
"",
|
|
7708
|
+
" // Fail loudly: defaulting to the production Auth host here would let a",
|
|
7709
|
+
" // misconfigured staging/dev worker validate tokens against prod. The",
|
|
7710
|
+
" // manifest declares AUTH_BASE_URL for every environment.",
|
|
7711
|
+
" if (!env.AUTH_BASE_URL) return { ok: false, status: 500, error: 'AUTH_BASE_URL not configured' };",
|
|
7712
|
+
"",
|
|
7921
7713
|
" try {",
|
|
7922
|
-
" const authBaseUrl = env.AUTH_BASE_URL
|
|
7714
|
+
" const authBaseUrl = env.AUTH_BASE_URL;",
|
|
7923
7715
|
" const response = await fetch(`${authBaseUrl}/validate`, {",
|
|
7924
7716
|
" method: 'POST',",
|
|
7925
7717
|
" headers: {",
|
|
@@ -7949,6 +7741,72 @@ function developerWorkerTs(application) {
|
|
|
7949
7741
|
].join("\n");
|
|
7950
7742
|
}
|
|
7951
7743
|
|
|
7744
|
+
// src/commands/app-scaffold-generate.ts
|
|
7745
|
+
import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
|
|
7746
|
+
import { spawnSync } from "child_process";
|
|
7747
|
+
import { tmpdir } from "os";
|
|
7748
|
+
import path2 from "path";
|
|
7749
|
+
function resolveGeneratorPath() {
|
|
7750
|
+
const candidates = [
|
|
7751
|
+
// Published CLI: vendored alongside dist/index.js at build time.
|
|
7752
|
+
new URL("./generate-wrangler.mjs", import.meta.url),
|
|
7753
|
+
// In-repo (tests, `pnpm dev`): the platform's canonical copy, which is the
|
|
7754
|
+
// very file the vendor step copies.
|
|
7755
|
+
new URL("../../../../tooling/generate-wrangler.mjs", import.meta.url)
|
|
7756
|
+
];
|
|
7757
|
+
for (const candidate of candidates) {
|
|
7758
|
+
const p = decodeURIComponent(candidate.pathname);
|
|
7759
|
+
if (existsSync3(p)) return p;
|
|
7760
|
+
}
|
|
7761
|
+
throw new Error(
|
|
7762
|
+
`topolo: could not locate generate-wrangler.mjs. A published CLI vendors it into dist/ at build time; in-repo it is read from topolo-platform/tooling/. Looked in:
|
|
7763
|
+
${candidates.map((c) => ` ${decodeURIComponent(c.pathname)}`).join("\n")}`
|
|
7764
|
+
);
|
|
7765
|
+
}
|
|
7766
|
+
var GENERATED = {
|
|
7767
|
+
purpose: "GENERATED from topolo.app.json by topolo-platform/tooling/generate-wrangler.mjs \u2014 never hand-edit.",
|
|
7768
|
+
requiredSignals: [
|
|
7769
|
+
"byte-identical to what generate-wrangler emits for this manifest",
|
|
7770
|
+
"every change is made in topolo.app.json, never here"
|
|
7771
|
+
]
|
|
7772
|
+
};
|
|
7773
|
+
function deriveGeneratedConfigs(files, base) {
|
|
7774
|
+
const generator = resolveGeneratorPath();
|
|
7775
|
+
const root = mkdtempSync(path2.join(tmpdir(), "topolo-scaffold-generate-"));
|
|
7776
|
+
try {
|
|
7777
|
+
const prefix = `${base}/`;
|
|
7778
|
+
for (const file of files) {
|
|
7779
|
+
if (!file.path.startsWith(prefix)) continue;
|
|
7780
|
+
const abs = path2.join(root, file.path.slice(prefix.length));
|
|
7781
|
+
mkdirSync(path2.dirname(abs), { recursive: true });
|
|
7782
|
+
writeFileSync(abs, file.content, "utf8");
|
|
7783
|
+
}
|
|
7784
|
+
const result = spawnSync(
|
|
7785
|
+
process.execPath,
|
|
7786
|
+
[generator, `--manifest=${path2.join(root, "topolo.app.json")}`],
|
|
7787
|
+
{ cwd: root, encoding: "utf8" }
|
|
7788
|
+
);
|
|
7789
|
+
if (result.status !== 0) {
|
|
7790
|
+
throw new Error(
|
|
7791
|
+
`topolo: generate-wrangler failed for the scaffolded manifest (exit ${result.status}).
|
|
7792
|
+
${result.stdout ?? ""}${result.stderr ?? ""}`
|
|
7793
|
+
);
|
|
7794
|
+
}
|
|
7795
|
+
const written = [...(result.stdout ?? "").matchAll(/^\[generate\] wrote (.+)$/gm)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
7796
|
+
if (!written.length) {
|
|
7797
|
+
throw new Error(`topolo: generate-wrangler wrote no files.
|
|
7798
|
+
${result.stdout ?? ""}`);
|
|
7799
|
+
}
|
|
7800
|
+
return written.map((rel) => ({
|
|
7801
|
+
path: `${base}/${rel}`,
|
|
7802
|
+
...GENERATED,
|
|
7803
|
+
content: readFileSync4(path2.join(root, rel), "utf8")
|
|
7804
|
+
}));
|
|
7805
|
+
} finally {
|
|
7806
|
+
rmSync(root, { recursive: true, force: true });
|
|
7807
|
+
}
|
|
7808
|
+
}
|
|
7809
|
+
|
|
7952
7810
|
// src/commands/app-scaffold-workflows.ts
|
|
7953
7811
|
function scaffoldCommands(options) {
|
|
7954
7812
|
const scaffoldId = toSlug(options.id);
|
|
@@ -8112,6 +7970,10 @@ function topoloAppManifestJson(application, options) {
|
|
|
8112
7970
|
bindings.d1 = [
|
|
8113
7971
|
{
|
|
8114
7972
|
binding: resource.d1Binding,
|
|
7973
|
+
development: {
|
|
7974
|
+
databaseName: resource.d1DevelopmentName,
|
|
7975
|
+
databaseId: "provision-after-scaffold"
|
|
7976
|
+
},
|
|
8115
7977
|
staging: {
|
|
8116
7978
|
databaseName: resource.d1StagingName,
|
|
8117
7979
|
databaseId: "provision-after-scaffold"
|
|
@@ -8128,26 +7990,29 @@ function topoloAppManifestJson(application, options) {
|
|
|
8128
7990
|
bindings.r2 = [
|
|
8129
7991
|
{
|
|
8130
7992
|
binding: resource.r2Binding,
|
|
7993
|
+
development: { bucketName: resource.r2DevelopmentName },
|
|
8131
7994
|
staging: { bucketName: resource.r2StagingName },
|
|
8132
7995
|
production: { bucketName: resource.r2ProductionName }
|
|
8133
7996
|
}
|
|
8134
7997
|
];
|
|
8135
7998
|
}
|
|
8136
7999
|
if (options.resources.queue) {
|
|
8137
|
-
|
|
8138
|
-
{
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
}
|
|
8144
|
-
|
|
8000
|
+
const queue = {
|
|
8001
|
+
development: { queue: resource.queueDevelopmentName },
|
|
8002
|
+
staging: { queue: resource.queueStagingName },
|
|
8003
|
+
production: { queue: resource.queueProductionName }
|
|
8004
|
+
};
|
|
8005
|
+
bindings.queues = {
|
|
8006
|
+
producers: [{ binding: resource.queueBinding, ...queue }],
|
|
8007
|
+
consumers: [{ ...queue }]
|
|
8008
|
+
};
|
|
8145
8009
|
}
|
|
8146
8010
|
if (options.resources.workflow) {
|
|
8147
8011
|
bindings.workflows = [
|
|
8148
8012
|
{
|
|
8149
8013
|
binding: resource.workflowBinding,
|
|
8150
8014
|
className: resource.workflowClassName,
|
|
8015
|
+
development: { name: resource.workflowDevelopmentName },
|
|
8151
8016
|
staging: { name: resource.workflowStagingName },
|
|
8152
8017
|
production: { name: resource.workflowProductionName }
|
|
8153
8018
|
}
|
|
@@ -8161,53 +8026,67 @@ function topoloAppManifestJson(application, options) {
|
|
|
8161
8026
|
compatibilityFlags: ["nodejs_compat"],
|
|
8162
8027
|
healthcheckPath: "/api/health",
|
|
8163
8028
|
vars: {
|
|
8029
|
+
// Every environment the manifest declares must be listed. A per-env map
|
|
8030
|
+
// that omits one leaves that env with no value, and both of these fall
|
|
8031
|
+
// back to PRODUCTION when unset (@topolo-io/nexus-client defaults
|
|
8032
|
+
// gatewayUrl to https://nexus.topolo.app) — a staging or dev worker
|
|
8033
|
+
// silently talking to prod.
|
|
8164
8034
|
AUTH_BASE_URL: {
|
|
8035
|
+
development: "https://auth.topolo.dev",
|
|
8165
8036
|
staging: "https://auth.stg.topolo.us",
|
|
8166
8037
|
production: "https://auth.topolo.app"
|
|
8167
8038
|
},
|
|
8168
8039
|
NEXUS_GATEWAY_URL: {
|
|
8040
|
+
development: "https://nexus.topolo.dev",
|
|
8169
8041
|
staging: "https://nexus.stg.topolo.us",
|
|
8170
8042
|
production: "https://nexus.topolo.app"
|
|
8171
|
-
}
|
|
8172
|
-
NEXUS_APP_ID:
|
|
8043
|
+
}
|
|
8044
|
+
// No NEXUS_APP_ID: generate-wrangler binds APP_ID (the app_id) into every
|
|
8045
|
+
// env, and worker-runtime reads env.APP_ID for Nexus usage attribution,
|
|
8046
|
+
// so a second copy of the same identity would only be a chance to drift.
|
|
8047
|
+
// It previously carried the SLUG, which reads as a working identity while
|
|
8048
|
+
// Nexus attributes by app_id.
|
|
8173
8049
|
},
|
|
8174
8050
|
secrets: ["TOPOLO_SERVICE_CLIENT_SECRET"],
|
|
8175
8051
|
...Object.keys(bindings).length ? { bindings } : {}
|
|
8176
8052
|
};
|
|
8177
8053
|
}
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8054
|
+
if (options.browser) {
|
|
8055
|
+
targets.web = {
|
|
8056
|
+
kind: "worker",
|
|
8057
|
+
dir: "apps/web",
|
|
8058
|
+
wranglerName: `topolo-${slug}`,
|
|
8059
|
+
subdomain: slug,
|
|
8060
|
+
main: "src/worker.ts",
|
|
8061
|
+
workersDev: false,
|
|
8062
|
+
assets: {
|
|
8063
|
+
directory: "./dist",
|
|
8064
|
+
binding: "ASSETS",
|
|
8065
|
+
notFoundHandling: "single-page-application",
|
|
8066
|
+
// Only meaningful when there is an API Worker to run first.
|
|
8067
|
+
...options.api ? { runWorkerFirst: ["/api/*"] } : {}
|
|
8068
|
+
},
|
|
8069
|
+
compatibilityFlags: ["nodejs_compat"],
|
|
8070
|
+
buildCommand: {
|
|
8071
|
+
staging: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us pnpm run build",
|
|
8072
|
+
production: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app pnpm run build"
|
|
8073
|
+
},
|
|
8074
|
+
healthcheckPath: options.api ? "/api/health" : "/",
|
|
8075
|
+
...options.api ? {
|
|
8076
|
+
bindings: {
|
|
8077
|
+
services: [
|
|
8078
|
+
{
|
|
8079
|
+
binding: "API",
|
|
8080
|
+
service: {
|
|
8081
|
+
staging: `topolo-${slug}-api-staging`,
|
|
8082
|
+
production: `topolo-${slug}-api-production`
|
|
8083
|
+
}
|
|
8205
8084
|
}
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
}
|
|
8209
|
-
}
|
|
8210
|
-
}
|
|
8085
|
+
]
|
|
8086
|
+
}
|
|
8087
|
+
} : {}
|
|
8088
|
+
};
|
|
8089
|
+
}
|
|
8211
8090
|
const actions = options.api ? baselineApiActions() : [];
|
|
8212
8091
|
return `${json({
|
|
8213
8092
|
$schema: "./../../topolo-platform/tooling/app-manifest-schema.json",
|
|
@@ -8229,6 +8108,37 @@ function topoloAppManifestJson(application, options) {
|
|
|
8229
8108
|
}
|
|
8230
8109
|
},
|
|
8231
8110
|
targets,
|
|
8111
|
+
registration: {
|
|
8112
|
+
ownerType: options.payload.ownerType,
|
|
8113
|
+
portfolio: options.payload.portfolio,
|
|
8114
|
+
applicationId: application.id,
|
|
8115
|
+
serviceSlug: application.serviceSlug,
|
|
8116
|
+
displayName: toTitle(application.name),
|
|
8117
|
+
description: options.payload.experience.tagline,
|
|
8118
|
+
category: "topolo-platform",
|
|
8119
|
+
audience: options.payload.audience,
|
|
8120
|
+
tenancy: options.payload.tenancy,
|
|
8121
|
+
surfaceModel: options.payload.surfaceModel,
|
|
8122
|
+
containerAppId: options.payload.containerAppId,
|
|
8123
|
+
distributionModel: options.payload.distributionModel,
|
|
8124
|
+
version: "1.0.0",
|
|
8125
|
+
pricing: { type: "free", monthlyPrice: null },
|
|
8126
|
+
supportEmail: "support@topolo.io",
|
|
8127
|
+
auth: {
|
|
8128
|
+
callbackPath: "/auth/callback",
|
|
8129
|
+
requestedScopes: options.api ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${application.id}:read`] : ["openid", "profile", "email", "organization:read"]
|
|
8130
|
+
},
|
|
8131
|
+
capabilities: options.payload.capabilities,
|
|
8132
|
+
marketplace: {
|
|
8133
|
+
intent: options.payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
|
|
8134
|
+
status: "preview",
|
|
8135
|
+
shortBlurb: options.payload.experience.tagline,
|
|
8136
|
+
tagline: options.payload.experience.tagline,
|
|
8137
|
+
description: options.payload.experience.tagline,
|
|
8138
|
+
docsUrl: `https://docs.topolo.app/applications/${slug}`
|
|
8139
|
+
}
|
|
8140
|
+
},
|
|
8141
|
+
notifications: { role: "consumer_only" },
|
|
8232
8142
|
...actions.length ? { actions } : {}
|
|
8233
8143
|
})}
|
|
8234
8144
|
`;
|
|
@@ -8320,263 +8230,6 @@ function baselineApiActions() {
|
|
|
8320
8230
|
}
|
|
8321
8231
|
];
|
|
8322
8232
|
}
|
|
8323
|
-
function cloudcontrolJson(application, options) {
|
|
8324
|
-
const slug = toSlug(application.id);
|
|
8325
|
-
const appPath = firstPartyAppPath(application.appDir, options.audience);
|
|
8326
|
-
const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
|
|
8327
|
-
const stagingHost = `${slug}.stg.topolo.us`;
|
|
8328
|
-
const serviceSlug = application.serviceSlug ?? `topolo-${slug}`;
|
|
8329
|
-
const resources = resourceConfig(application, options.profile);
|
|
8330
|
-
const resourceBindings = [];
|
|
8331
|
-
if (options.resources.d1) {
|
|
8332
|
-
resourceBindings.push(
|
|
8333
|
-
{
|
|
8334
|
-
resource_type: "d1",
|
|
8335
|
-
binding: resources.d1Binding,
|
|
8336
|
-
environment: "production",
|
|
8337
|
-
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8338
|
-
resource_name: resources.d1ProductionName,
|
|
8339
|
-
resource_id: "provision-after-scaffold",
|
|
8340
|
-
migrations_dir: "migrations"
|
|
8341
|
-
},
|
|
8342
|
-
{
|
|
8343
|
-
resource_type: "d1",
|
|
8344
|
-
binding: resources.d1Binding,
|
|
8345
|
-
environment: "staging",
|
|
8346
|
-
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8347
|
-
resource_name: resources.d1StagingName,
|
|
8348
|
-
resource_id: "provision-after-scaffold",
|
|
8349
|
-
migrations_dir: "migrations"
|
|
8350
|
-
},
|
|
8351
|
-
{
|
|
8352
|
-
resource_type: "d1",
|
|
8353
|
-
binding: resources.d1Binding,
|
|
8354
|
-
environment: "development",
|
|
8355
|
-
account_id: DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID,
|
|
8356
|
-
resource_name: resources.d1DevelopmentName,
|
|
8357
|
-
resource_id: "provision-after-scaffold",
|
|
8358
|
-
migrations_dir: "migrations"
|
|
8359
|
-
}
|
|
8360
|
-
);
|
|
8361
|
-
}
|
|
8362
|
-
if (options.resources.r2) {
|
|
8363
|
-
resourceBindings.push(
|
|
8364
|
-
{
|
|
8365
|
-
resource_type: "r2",
|
|
8366
|
-
binding: resources.r2Binding,
|
|
8367
|
-
environment: "production",
|
|
8368
|
-
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8369
|
-
resource_name: resources.r2ProductionName
|
|
8370
|
-
},
|
|
8371
|
-
{
|
|
8372
|
-
resource_type: "r2",
|
|
8373
|
-
binding: resources.r2Binding,
|
|
8374
|
-
environment: "staging",
|
|
8375
|
-
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8376
|
-
resource_name: resources.r2StagingName
|
|
8377
|
-
}
|
|
8378
|
-
);
|
|
8379
|
-
}
|
|
8380
|
-
if (options.resources.queue) {
|
|
8381
|
-
resourceBindings.push(
|
|
8382
|
-
{
|
|
8383
|
-
resource_type: "queue",
|
|
8384
|
-
binding: resources.queueBinding,
|
|
8385
|
-
environment: "production",
|
|
8386
|
-
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8387
|
-
resource_name: resources.queueProductionName
|
|
8388
|
-
},
|
|
8389
|
-
{
|
|
8390
|
-
resource_type: "queue",
|
|
8391
|
-
binding: resources.queueBinding,
|
|
8392
|
-
environment: "staging",
|
|
8393
|
-
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8394
|
-
resource_name: resources.queueStagingName
|
|
8395
|
-
}
|
|
8396
|
-
);
|
|
8397
|
-
}
|
|
8398
|
-
if (options.resources.workflow) {
|
|
8399
|
-
resourceBindings.push(
|
|
8400
|
-
{
|
|
8401
|
-
resource_type: "workflow",
|
|
8402
|
-
binding: resources.workflowBinding,
|
|
8403
|
-
environment: "production",
|
|
8404
|
-
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8405
|
-
resource_name: resources.workflowProductionName,
|
|
8406
|
-
class_name: resources.workflowClassName
|
|
8407
|
-
},
|
|
8408
|
-
{
|
|
8409
|
-
resource_type: "workflow",
|
|
8410
|
-
binding: resources.workflowBinding,
|
|
8411
|
-
environment: "staging",
|
|
8412
|
-
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8413
|
-
resource_name: resources.workflowStagingName,
|
|
8414
|
-
class_name: resources.workflowClassName
|
|
8415
|
-
}
|
|
8416
|
-
);
|
|
8417
|
-
}
|
|
8418
|
-
if (options.resources.containerProcessing) {
|
|
8419
|
-
resourceBindings.push(
|
|
8420
|
-
{
|
|
8421
|
-
resource_type: "container",
|
|
8422
|
-
environment: "production",
|
|
8423
|
-
account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8424
|
-
resource_name: resources.containerProductionName,
|
|
8425
|
-
purpose: "processing"
|
|
8426
|
-
},
|
|
8427
|
-
{
|
|
8428
|
-
resource_type: "container",
|
|
8429
|
-
environment: "staging",
|
|
8430
|
-
account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8431
|
-
resource_name: resources.containerStagingName,
|
|
8432
|
-
purpose: "processing"
|
|
8433
|
-
}
|
|
8434
|
-
);
|
|
8435
|
-
}
|
|
8436
|
-
const deployTargets = options.api ? [
|
|
8437
|
-
{
|
|
8438
|
-
kind: "worker",
|
|
8439
|
-
environment: "production",
|
|
8440
|
-
target_name: `topolo-${slug}-api-production`,
|
|
8441
|
-
cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8442
|
-
config_path: "apps/api/wrangler.toml",
|
|
8443
|
-
workdir: "apps/api",
|
|
8444
|
-
build_command: null,
|
|
8445
|
-
output_dir: null,
|
|
8446
|
-
production_branch: "main",
|
|
8447
|
-
healthcheck_path: "/api/health",
|
|
8448
|
-
domains: [],
|
|
8449
|
-
routes: [],
|
|
8450
|
-
service_slug: serviceSlug
|
|
8451
|
-
},
|
|
8452
|
-
{
|
|
8453
|
-
kind: "worker",
|
|
8454
|
-
environment: "staging",
|
|
8455
|
-
target_name: `topolo-${slug}-staging`,
|
|
8456
|
-
cloudflare_account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8457
|
-
config_path: "apps/web/wrangler.toml",
|
|
8458
|
-
workdir: "apps/web",
|
|
8459
|
-
build_command: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us pnpm run build",
|
|
8460
|
-
output_dir: null,
|
|
8461
|
-
production_branch: "staging",
|
|
8462
|
-
healthcheck_path: "/api/health",
|
|
8463
|
-
domains: [stagingHost],
|
|
8464
|
-
routes: [stagingHost],
|
|
8465
|
-
api_url: `https://${stagingHost}`,
|
|
8466
|
-
service_slug: serviceSlug,
|
|
8467
|
-
sdk_app_id: application.id,
|
|
8468
|
-
agent_callable: true
|
|
8469
|
-
},
|
|
8470
|
-
{
|
|
8471
|
-
kind: "worker",
|
|
8472
|
-
environment: "staging",
|
|
8473
|
-
target_name: `topolo-${slug}-api-staging`,
|
|
8474
|
-
cloudflare_account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
8475
|
-
config_path: "apps/api/wrangler.toml",
|
|
8476
|
-
workdir: "apps/api",
|
|
8477
|
-
build_command: null,
|
|
8478
|
-
output_dir: null,
|
|
8479
|
-
production_branch: "staging",
|
|
8480
|
-
healthcheck_path: "/api/health",
|
|
8481
|
-
domains: [],
|
|
8482
|
-
routes: [],
|
|
8483
|
-
service_slug: serviceSlug
|
|
8484
|
-
},
|
|
8485
|
-
{
|
|
8486
|
-
kind: "worker",
|
|
8487
|
-
environment: "production",
|
|
8488
|
-
target_name: `topolo-${slug}-production`,
|
|
8489
|
-
cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8490
|
-
config_path: "apps/web/wrangler.toml",
|
|
8491
|
-
workdir: "apps/web",
|
|
8492
|
-
build_command: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app pnpm run build",
|
|
8493
|
-
output_dir: null,
|
|
8494
|
-
production_branch: "main",
|
|
8495
|
-
healthcheck_path: "/api/health",
|
|
8496
|
-
domains: [host],
|
|
8497
|
-
routes: [host],
|
|
8498
|
-
api_url: application.productionUrl,
|
|
8499
|
-
service_slug: serviceSlug,
|
|
8500
|
-
sdk_app_id: application.id,
|
|
8501
|
-
agent_callable: true
|
|
8502
|
-
}
|
|
8503
|
-
] : [
|
|
8504
|
-
{
|
|
8505
|
-
kind: "worker",
|
|
8506
|
-
environment: "production",
|
|
8507
|
-
target_name: `topolo-${slug}`,
|
|
8508
|
-
cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
|
|
8509
|
-
config_path: "wrangler.toml",
|
|
8510
|
-
build_command: "npm run build",
|
|
8511
|
-
output_dir: "dist",
|
|
8512
|
-
production_branch: "main",
|
|
8513
|
-
healthcheck_path: "/",
|
|
8514
|
-
domains: [host],
|
|
8515
|
-
routes: [host],
|
|
8516
|
-
api_url: application.productionUrl,
|
|
8517
|
-
agent_callable: false
|
|
8518
|
-
}
|
|
8519
|
-
];
|
|
8520
|
-
return `${json({
|
|
8521
|
-
name: `topolo-${slug}`,
|
|
8522
|
-
description: `Canonical documentation for ${toTitle(application.name)} lives in system-apps/TopoloDocs.`,
|
|
8523
|
-
deploy_method: "wrangler",
|
|
8524
|
-
ci_provider: "github_actions",
|
|
8525
|
-
repo_identity: {
|
|
8526
|
-
provider: "github",
|
|
8527
|
-
owner: "Topolo-io",
|
|
8528
|
-
repo: application.appDir,
|
|
8529
|
-
remote_url: `git@github.com:Topolo-io/${application.appDir}.git`,
|
|
8530
|
-
default_branch: "main"
|
|
8531
|
-
},
|
|
8532
|
-
environments: application.productionUrl ? [
|
|
8533
|
-
{
|
|
8534
|
-
name: "production",
|
|
8535
|
-
url: application.productionUrl,
|
|
8536
|
-
branch: "main",
|
|
8537
|
-
status: "unknown"
|
|
8538
|
-
},
|
|
8539
|
-
{
|
|
8540
|
-
name: "staging",
|
|
8541
|
-
url: `https://${stagingHost}`,
|
|
8542
|
-
branch: "main",
|
|
8543
|
-
status: "unknown"
|
|
8544
|
-
}
|
|
8545
|
-
] : [],
|
|
8546
|
-
deploy_targets: deployTargets,
|
|
8547
|
-
resource_bindings: resourceBindings,
|
|
8548
|
-
ci_workflows: [
|
|
8549
|
-
{
|
|
8550
|
-
provider: "github_actions",
|
|
8551
|
-
workflow_name: "topolo-conformance.yml",
|
|
8552
|
-
path: ".github/workflows/topolo-conformance.yml",
|
|
8553
|
-
branch: null,
|
|
8554
|
-
required: true
|
|
8555
|
-
}
|
|
8556
|
-
],
|
|
8557
|
-
ci_targets: [
|
|
8558
|
-
{
|
|
8559
|
-
name: "typecheck",
|
|
8560
|
-
command: "pnpm run typecheck",
|
|
8561
|
-
required: true
|
|
8562
|
-
},
|
|
8563
|
-
{
|
|
8564
|
-
name: "test",
|
|
8565
|
-
command: "pnpm run test",
|
|
8566
|
-
required: true
|
|
8567
|
-
},
|
|
8568
|
-
{
|
|
8569
|
-
name: "build",
|
|
8570
|
-
command: "pnpm run build",
|
|
8571
|
-
required: true
|
|
8572
|
-
}
|
|
8573
|
-
],
|
|
8574
|
-
tags: ["application", "cloudflare", "typescript", ...hasScaffoldResources(options.resources) ? ["resource-backed"] : []],
|
|
8575
|
-
source_paths: [appPath],
|
|
8576
|
-
cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID
|
|
8577
|
-
})}
|
|
8578
|
-
`;
|
|
8579
|
-
}
|
|
8580
8233
|
function systemJson(application, options) {
|
|
8581
8234
|
const slug = toSlug(application.id);
|
|
8582
8235
|
const suiteLabel = options.audience === "personal" ? "personal" : "platform";
|
|
@@ -8913,7 +8566,7 @@ function developerManifest(application, options) {
|
|
|
8913
8566
|
surface_model: options.surfaceModel,
|
|
8914
8567
|
container_app_id: options.containerAppId,
|
|
8915
8568
|
package_name: application.packageName,
|
|
8916
|
-
app_id:
|
|
8569
|
+
app_id: application.appId,
|
|
8917
8570
|
capabilities: {
|
|
8918
8571
|
selected: options.capabilities,
|
|
8919
8572
|
supported_v1: [
|
|
@@ -8957,7 +8610,38 @@ function developerManifest(application, options) {
|
|
|
8957
8610
|
},
|
|
8958
8611
|
marketplace: {
|
|
8959
8612
|
intent: options.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested"
|
|
8960
|
-
}
|
|
8613
|
+
},
|
|
8614
|
+
registration: {
|
|
8615
|
+
ownerType: "third_party",
|
|
8616
|
+
applicationId: application.id,
|
|
8617
|
+
serviceSlug: application.serviceSlug,
|
|
8618
|
+
displayName: toTitle(application.name),
|
|
8619
|
+
description: `${toTitle(application.name)} application for Topolo.`,
|
|
8620
|
+
category: "productivity",
|
|
8621
|
+
audience: options.audience,
|
|
8622
|
+
tenancy: options.tenancy,
|
|
8623
|
+
surfaceModel: options.surfaceModel,
|
|
8624
|
+
containerAppId: options.containerAppId,
|
|
8625
|
+
distributionModel: options.distributionModel,
|
|
8626
|
+
version: "1.0.0",
|
|
8627
|
+
pricing: { type: "free", monthlyPrice: null },
|
|
8628
|
+
supportEmail: "",
|
|
8629
|
+
productionUrl: application.productionUrl,
|
|
8630
|
+
auth: {
|
|
8631
|
+
callbackPath,
|
|
8632
|
+
requestedScopes
|
|
8633
|
+
},
|
|
8634
|
+
capabilities: options.capabilities,
|
|
8635
|
+
marketplace: {
|
|
8636
|
+
intent: options.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
|
|
8637
|
+
status: "preview",
|
|
8638
|
+
shortBlurb: `${toTitle(application.name)} application for Topolo.`,
|
|
8639
|
+
tagline: `${toTitle(application.name)} application for Topolo.`,
|
|
8640
|
+
description: `${toTitle(application.name)} application for Topolo.`,
|
|
8641
|
+
docsUrl: application.productionUrl || "https://developers.topolo.app"
|
|
8642
|
+
}
|
|
8643
|
+
},
|
|
8644
|
+
notifications: { role: "consumer_only" }
|
|
8961
8645
|
})}
|
|
8962
8646
|
`;
|
|
8963
8647
|
}
|
|
@@ -9243,9 +8927,10 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
9243
8927
|
workspaceLabel: tenancy === "household" ? "Households" : "Workspaces",
|
|
9244
8928
|
tagline: String(options.tagline || "").trim() || `Manage your work in ${toTitle(name)}.`
|
|
9245
8929
|
};
|
|
9246
|
-
const serviceSlug =
|
|
9247
|
-
const
|
|
9248
|
-
const
|
|
8930
|
+
const serviceSlug = target === "platform" ? `topolo-${slug}` : id;
|
|
8931
|
+
const catalogAppId = catalogApplication?.services.find((service) => service.startsWith("app_"));
|
|
8932
|
+
const defaultAppId = target === "platform" ? `app_topolo_${id}` : catalogAppId ?? `app_${id.replace(/-/g, "_")}`;
|
|
8933
|
+
const appId = options.appId ?? defaultAppId;
|
|
9249
8934
|
const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
|
|
9250
8935
|
const scopes = scaffoldScopes({ browser, api });
|
|
9251
8936
|
const capabilities = scaffoldCapabilities({ browser, api, distributionModel });
|
|
@@ -9622,8 +9307,10 @@ function scaffoldFiles(options) {
|
|
|
9622
9307
|
],
|
|
9623
9308
|
content: topoloAppManifestJson(application, {
|
|
9624
9309
|
api: options.api,
|
|
9310
|
+
browser: options.browser,
|
|
9625
9311
|
resources: options.payload.resources,
|
|
9626
|
-
profile: options.payload.profile
|
|
9312
|
+
profile: options.payload.profile,
|
|
9313
|
+
payload: options.payload
|
|
9627
9314
|
})
|
|
9628
9315
|
},
|
|
9629
9316
|
...options.api ? [
|
|
@@ -9646,20 +9333,6 @@ function scaffoldFiles(options) {
|
|
|
9646
9333
|
content: topoloActionContractsDmts()
|
|
9647
9334
|
}
|
|
9648
9335
|
] : [],
|
|
9649
|
-
{
|
|
9650
|
-
path: `${base}/topolo.cloudcontrol.json`,
|
|
9651
|
-
purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
|
|
9652
|
-
requiredSignals: [
|
|
9653
|
-
"production URLs and Cloudflare targets are explicit",
|
|
9654
|
-
"resource bindings and required secrets are declared without secret values"
|
|
9655
|
-
],
|
|
9656
|
-
content: cloudcontrolJson(application, {
|
|
9657
|
-
api: options.api,
|
|
9658
|
-
audience: options.payload.audience,
|
|
9659
|
-
profile: options.payload.profile,
|
|
9660
|
-
resources: options.payload.resources
|
|
9661
|
-
})
|
|
9662
|
-
},
|
|
9663
9336
|
{
|
|
9664
9337
|
path: `${base}/.github/workflows/topolo-conformance.yml`,
|
|
9665
9338
|
purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
|
|
@@ -9868,15 +9541,6 @@ export default defineConfig({
|
|
|
9868
9541
|
"only declares an API service binding for API-backed scaffolds"
|
|
9869
9542
|
],
|
|
9870
9543
|
content: webWorkerTs(application, options.api)
|
|
9871
|
-
},
|
|
9872
|
-
{
|
|
9873
|
-
path: `${browserBase}/wrangler.toml`,
|
|
9874
|
-
purpose: "Cloudflare Worker static-assets configuration for the browser surface.",
|
|
9875
|
-
requiredSignals: [
|
|
9876
|
-
"serves Vite assets through a Worker with assets",
|
|
9877
|
-
"only binds an API Worker when the scaffold includes one"
|
|
9878
|
-
],
|
|
9879
|
-
content: platformWebWranglerToml(application, options.api)
|
|
9880
9544
|
}
|
|
9881
9545
|
);
|
|
9882
9546
|
}
|
|
@@ -9884,15 +9548,15 @@ export default defineConfig({
|
|
|
9884
9548
|
}
|
|
9885
9549
|
if (options.api) {
|
|
9886
9550
|
files.push(
|
|
9887
|
-
{
|
|
9551
|
+
...target === "platform" ? [] : [{
|
|
9888
9552
|
path: `${apiBase}/wrangler.toml`,
|
|
9889
9553
|
purpose: "Cloudflare Worker runtime configuration.",
|
|
9890
9554
|
requiredSignals: [
|
|
9891
|
-
"bindings match
|
|
9555
|
+
"bindings match the declared resources",
|
|
9892
9556
|
"secrets are referenced by name only"
|
|
9893
9557
|
],
|
|
9894
|
-
content:
|
|
9895
|
-
},
|
|
9558
|
+
content: wranglerToml(application, target, options.payload.resources, options.payload.profile)
|
|
9559
|
+
}],
|
|
9896
9560
|
{
|
|
9897
9561
|
path: `${apiBase}/src/worker.ts`,
|
|
9898
9562
|
purpose: "Protected service boundary.",
|
|
@@ -9960,6 +9624,9 @@ export default defineConfig({
|
|
|
9960
9624
|
}
|
|
9961
9625
|
}
|
|
9962
9626
|
}
|
|
9627
|
+
if (target === "platform") {
|
|
9628
|
+
files.push(...deriveGeneratedConfigs(files, base));
|
|
9629
|
+
}
|
|
9963
9630
|
return files;
|
|
9964
9631
|
}
|
|
9965
9632
|
|
|
@@ -10026,7 +9693,7 @@ async function provisionApplicationScaffold(payload) {
|
|
|
10026
9693
|
`Could not resolve Developers app id from Auth slug ${DEVELOPERS_SERVICE_SLUG}.`
|
|
10027
9694
|
);
|
|
10028
9695
|
}
|
|
10029
|
-
const endpoint = payload.target === "platform" ? "/api/developer-console/admin/
|
|
9696
|
+
const endpoint = payload.target === "platform" ? "/api/developer-console/admin/apps/reconcile" : "/api/developer-console/apps/reconcile";
|
|
10030
9697
|
const headers = new Headers({
|
|
10031
9698
|
"Content-Type": "application/json",
|
|
10032
9699
|
"X-App-ID": developersAppId,
|
|
@@ -10044,7 +9711,7 @@ async function provisionApplicationScaffold(payload) {
|
|
|
10044
9711
|
const response = await fetch(`${developersOrigin}${endpoint}`, {
|
|
10045
9712
|
method: "POST",
|
|
10046
9713
|
headers,
|
|
10047
|
-
body: JSON.stringify(buildProvisioningRequest(payload))
|
|
9714
|
+
body: JSON.stringify(buildProvisioningRequest(payload, identity.user.email))
|
|
10048
9715
|
});
|
|
10049
9716
|
const envelope = await response.json().catch(() => null);
|
|
10050
9717
|
if (!response.ok || !envelope?.success || !envelope.data) {
|
|
@@ -10053,8 +9720,10 @@ async function provisionApplicationScaffold(payload) {
|
|
|
10053
9720
|
);
|
|
10054
9721
|
}
|
|
10055
9722
|
const app = pickRecord(envelope.data.app);
|
|
10056
|
-
const
|
|
10057
|
-
const
|
|
9723
|
+
const projections = pickRecord(envelope.data.projections);
|
|
9724
|
+
const authProjection = pickRecord(projections?.auth);
|
|
9725
|
+
const service = pickRecord(authProjection?.service);
|
|
9726
|
+
const authConfig = pickRecord(authProjection?.authConfig);
|
|
10058
9727
|
if (!app) {
|
|
10059
9728
|
throw new Error("Developers provisioning did not return an app record.");
|
|
10060
9729
|
}
|
|
@@ -10091,9 +9760,9 @@ async function provisionApplicationScaffold(payload) {
|
|
|
10091
9760
|
} : null
|
|
10092
9761
|
};
|
|
10093
9762
|
}
|
|
10094
|
-
function buildProvisioningRequest(payload) {
|
|
9763
|
+
function buildProvisioningRequest(payload, supportEmail) {
|
|
10095
9764
|
return {
|
|
10096
|
-
|
|
9765
|
+
mode: "apply",
|
|
10097
9766
|
ownerType: payload.ownerType,
|
|
10098
9767
|
portfolio: payload.portfolio,
|
|
10099
9768
|
audience: payload.audience,
|
|
@@ -10106,14 +9775,28 @@ function buildProvisioningRequest(payload) {
|
|
|
10106
9775
|
"http://localhost:5173/auth/callback",
|
|
10107
9776
|
payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/auth/callback` : null
|
|
10108
9777
|
].filter(Boolean),
|
|
10109
|
-
healthEndpoint: payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/api/health` : null,
|
|
10110
|
-
runtimeStrategy: "external",
|
|
10111
9778
|
requestedScopes: payload.application.appId ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${payload.application.id}:read`] : ["openid", "profile", "email", "organization:read"],
|
|
10112
|
-
|
|
9779
|
+
marketplace: {
|
|
9780
|
+
intent: payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
|
|
9781
|
+
status: "preview",
|
|
9782
|
+
shortBlurb: payload.experience.tagline,
|
|
9783
|
+
tagline: payload.experience.tagline,
|
|
9784
|
+
description: payload.experience.tagline,
|
|
9785
|
+
docsUrl: payload.application.productionUrl || "https://developers.topolo.app"
|
|
9786
|
+
},
|
|
9787
|
+
notifications: { role: "consumer_only" },
|
|
10113
9788
|
applicationId: payload.application.id,
|
|
10114
|
-
appDir: payload.application.appDir,
|
|
10115
|
-
packageName: payload.application.packageName,
|
|
10116
9789
|
name: payload.application.name,
|
|
9790
|
+
description: payload.experience.tagline,
|
|
9791
|
+
category: payload.target === "platform" ? "topolo-platform" : "productivity",
|
|
9792
|
+
version: "1.0.0",
|
|
9793
|
+
pricingType: "free",
|
|
9794
|
+
monthlyPrice: null,
|
|
9795
|
+
demoUrl: payload.application.productionUrl,
|
|
9796
|
+
repoUrl: null,
|
|
9797
|
+
supportEmail,
|
|
9798
|
+
tags: [],
|
|
9799
|
+
screenshots: [],
|
|
10117
9800
|
productionUrl: payload.application.productionUrl,
|
|
10118
9801
|
serviceSlug: payload.application.serviceSlug,
|
|
10119
9802
|
appId: payload.application.appId
|
|
@@ -10187,6 +9870,332 @@ async function applyProvisionedAppId(manifestPath, appId) {
|
|
|
10187
9870
|
|
|
10188
9871
|
// src/commands/apps.ts
|
|
10189
9872
|
import { join as join3 } from "path";
|
|
9873
|
+
|
|
9874
|
+
// src/commands/app-reconcile.ts
|
|
9875
|
+
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
9876
|
+
import path3 from "path";
|
|
9877
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
9878
|
+
import { stdin as input2, stdout as output } from "process";
|
|
9879
|
+
function record(value) {
|
|
9880
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
9881
|
+
}
|
|
9882
|
+
function environmentValue(value, environment) {
|
|
9883
|
+
if (typeof value === "string") return value;
|
|
9884
|
+
if (!value) return null;
|
|
9885
|
+
return typeof value[environment] === "string" ? value[environment] : null;
|
|
9886
|
+
}
|
|
9887
|
+
function resolveManifestApplicationUrl(manifest, environment) {
|
|
9888
|
+
const targets = manifest.targets || {};
|
|
9889
|
+
const targetName = manifest.primaryTarget || (targets.web ? "web" : Object.keys(targets)[0]);
|
|
9890
|
+
const target = targetName ? targets[targetName] : null;
|
|
9891
|
+
if (!target) {
|
|
9892
|
+
if (manifest.registration?.productionUrl) return manifest.registration.productionUrl.replace(/\/+$/, "");
|
|
9893
|
+
throw new Error("topolo.app.json has no application target or registration.productionUrl.");
|
|
9894
|
+
}
|
|
9895
|
+
const publicUrl = environmentValue(target.publicUrl, environment);
|
|
9896
|
+
if (publicUrl) return publicUrl.replace(/\/+$/, "");
|
|
9897
|
+
const hostOverride = target.hostOverride?.[environment];
|
|
9898
|
+
if (hostOverride) return `https://${hostOverride.replace(/^https?:\/\//, "").replace(/\/+$/, "")}`;
|
|
9899
|
+
const domain = manifest.environments?.[environment]?.domain;
|
|
9900
|
+
if (!target.subdomain || !domain) {
|
|
9901
|
+
throw new Error(`Cannot derive the ${environment} application URL from target ${targetName}.`);
|
|
9902
|
+
}
|
|
9903
|
+
return `https://${target.subdomain}.${domain}`;
|
|
9904
|
+
}
|
|
9905
|
+
function assertRegistration(value) {
|
|
9906
|
+
const registration = record(value);
|
|
9907
|
+
const required = [
|
|
9908
|
+
"ownerType",
|
|
9909
|
+
"applicationId",
|
|
9910
|
+
"serviceSlug",
|
|
9911
|
+
"displayName",
|
|
9912
|
+
"description",
|
|
9913
|
+
"audience",
|
|
9914
|
+
"tenancy",
|
|
9915
|
+
"surfaceModel",
|
|
9916
|
+
"distributionModel",
|
|
9917
|
+
"version",
|
|
9918
|
+
"pricing",
|
|
9919
|
+
"supportEmail",
|
|
9920
|
+
"auth",
|
|
9921
|
+
"capabilities",
|
|
9922
|
+
"marketplace"
|
|
9923
|
+
];
|
|
9924
|
+
const missing = required.filter((field) => registration?.[field] == null || registration[field] === "");
|
|
9925
|
+
if (missing.length) {
|
|
9926
|
+
throw new Error(
|
|
9927
|
+
`topolo.app.json registration is incomplete: ${missing.join(", ")}. Run in an interactive terminal to complete the manifest, or edit the declarative registration block.`
|
|
9928
|
+
);
|
|
9929
|
+
}
|
|
9930
|
+
}
|
|
9931
|
+
function titleFromSlug(value) {
|
|
9932
|
+
return value.replace(/^topolo[-_]/, "").split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
9933
|
+
}
|
|
9934
|
+
async function completeApplicationRegistrationInteractively(options) {
|
|
9935
|
+
const manifestPath = path3.resolve(options.manifestPath || "topolo.app.json");
|
|
9936
|
+
const manifest = JSON.parse(await readFile3(manifestPath, "utf8"));
|
|
9937
|
+
const existing = record(manifest.registration) || {};
|
|
9938
|
+
const appId = String(manifest.appId || manifest.app_id || "").trim();
|
|
9939
|
+
const slug = String(existing.serviceSlug || manifest.name || "").trim();
|
|
9940
|
+
const defaultName = titleFromSlug(slug);
|
|
9941
|
+
const readline = options.prompt ? null : createInterface2({ input: input2, output });
|
|
9942
|
+
const ask = options.prompt || ((question) => readline.question(question));
|
|
9943
|
+
let changed = !manifest.registration;
|
|
9944
|
+
const requiredText = async (field, label, fallback = "") => {
|
|
9945
|
+
const current = typeof existing[field] === "string" ? String(existing[field]).trim() : "";
|
|
9946
|
+
if (current) return current;
|
|
9947
|
+
const answer = (await ask(`${label}${fallback ? ` [${fallback}]` : ""}: `)).trim() || fallback;
|
|
9948
|
+
if (!answer) throw new Error(`${field} is required.`);
|
|
9949
|
+
changed = true;
|
|
9950
|
+
return answer;
|
|
9951
|
+
};
|
|
9952
|
+
try {
|
|
9953
|
+
const productionUrl = String(existing.productionUrl || resolveManifestApplicationUrl(manifest, options.environment || selectedEnvironment()));
|
|
9954
|
+
const displayName = await requiredText("displayName", "Application display name", defaultName);
|
|
9955
|
+
const description = await requiredText("description", "Application description", `${displayName} application for Topolo.`);
|
|
9956
|
+
const supportEmail = await requiredText("supportEmail", "Support email");
|
|
9957
|
+
manifest.registration = {
|
|
9958
|
+
ownerType: options.expectedOwnerType,
|
|
9959
|
+
...options.expectedOwnerType === "topolo_first_party" ? { portfolio: existing.portfolio || "platform" } : {},
|
|
9960
|
+
applicationId: String(existing.applicationId || slug).replace(/^topolo-/, "topolo-"),
|
|
9961
|
+
serviceSlug: slug,
|
|
9962
|
+
displayName,
|
|
9963
|
+
description,
|
|
9964
|
+
category: String(existing.category || (options.expectedOwnerType === "topolo_first_party" ? "topolo-platform" : "productivity")),
|
|
9965
|
+
audience: existing.audience || "business",
|
|
9966
|
+
tenancy: existing.tenancy || "organization",
|
|
9967
|
+
surfaceModel: existing.surfaceModel || "web_app",
|
|
9968
|
+
containerAppId: existing.containerAppId,
|
|
9969
|
+
distributionModel: existing.distributionModel || (options.expectedOwnerType === "topolo_first_party" ? "marketplace" : "organization_internal"),
|
|
9970
|
+
version: String(existing.version || "1.0.0"),
|
|
9971
|
+
pricing: record(existing.pricing) || { type: "free", monthlyPrice: null },
|
|
9972
|
+
supportEmail,
|
|
9973
|
+
productionUrl,
|
|
9974
|
+
demoUrl: typeof existing.demoUrl === "string" ? existing.demoUrl : void 0,
|
|
9975
|
+
repoUrl: typeof existing.repoUrl === "string" ? existing.repoUrl : void 0,
|
|
9976
|
+
tags: Array.isArray(existing.tags) ? existing.tags : [],
|
|
9977
|
+
screenshots: Array.isArray(existing.screenshots) ? existing.screenshots : [],
|
|
9978
|
+
auth: record(existing.auth) || {
|
|
9979
|
+
callbackPath: "/auth/callback",
|
|
9980
|
+
requestedScopes: ["openid", "profile", "email", "organization:read"]
|
|
9981
|
+
},
|
|
9982
|
+
capabilities: Array.isArray(existing.capabilities) ? existing.capabilities : ["login", "launcher"],
|
|
9983
|
+
marketplace: record(existing.marketplace) || {
|
|
9984
|
+
intent: options.expectedOwnerType === "topolo_first_party" ? "submit_when_ready" : "not_requested",
|
|
9985
|
+
status: "preview",
|
|
9986
|
+
shortBlurb: description,
|
|
9987
|
+
tagline: description,
|
|
9988
|
+
description,
|
|
9989
|
+
docsUrl: productionUrl
|
|
9990
|
+
}
|
|
9991
|
+
};
|
|
9992
|
+
if (!manifest.notifications) {
|
|
9993
|
+
const role = (await ask("Notification role [consumer_only]: ")).trim() || "consumer_only";
|
|
9994
|
+
if (!["producer", "consumer_only", "internal_non_producer"].includes(role)) {
|
|
9995
|
+
throw new Error("Notification role must be producer, consumer_only, or internal_non_producer.");
|
|
9996
|
+
}
|
|
9997
|
+
if (role === "producer") {
|
|
9998
|
+
const contracts = (await ask("Notification contracts path: ")).trim();
|
|
9999
|
+
if (!contracts) throw new Error("Producer applications must declare a notification contracts path.");
|
|
10000
|
+
manifest.notifications = { role, contracts };
|
|
10001
|
+
} else {
|
|
10002
|
+
manifest.notifications = { role };
|
|
10003
|
+
}
|
|
10004
|
+
changed = true;
|
|
10005
|
+
}
|
|
10006
|
+
if (!appId) throw new Error("topolo.app.json must declare the immutable appId before reconciliation.");
|
|
10007
|
+
if (changed) await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
10008
|
+
`, "utf8");
|
|
10009
|
+
return { changed, manifestPath };
|
|
10010
|
+
} finally {
|
|
10011
|
+
if (readline) readline.close();
|
|
10012
|
+
}
|
|
10013
|
+
}
|
|
10014
|
+
async function loadApplicationReconcileRequest(options) {
|
|
10015
|
+
const manifestPath = path3.resolve(options.manifestPath || "topolo.app.json");
|
|
10016
|
+
const raw = await readFile3(manifestPath, "utf8");
|
|
10017
|
+
const manifest = JSON.parse(raw);
|
|
10018
|
+
assertRegistration(manifest.registration);
|
|
10019
|
+
const registration = manifest.registration;
|
|
10020
|
+
if (registration.ownerType !== options.expectedOwnerType) {
|
|
10021
|
+
throw new Error(
|
|
10022
|
+
`${registration.ownerType} registration cannot be reconciled through the ${options.expectedOwnerType} command surface.`
|
|
10023
|
+
);
|
|
10024
|
+
}
|
|
10025
|
+
const environment = options.environment || selectedEnvironment();
|
|
10026
|
+
const productionUrl = resolveManifestApplicationUrl(manifest, environment);
|
|
10027
|
+
const callbackPath = registration.auth.callbackPath.startsWith("/") ? registration.auth.callbackPath : `/${registration.auth.callbackPath}`;
|
|
10028
|
+
const appId = String(manifest.appId || manifest.app_id || "").trim();
|
|
10029
|
+
if (!appId) throw new Error("topolo.app.json must declare the immutable appId.");
|
|
10030
|
+
return {
|
|
10031
|
+
manifestPath,
|
|
10032
|
+
request: {
|
|
10033
|
+
mode: options.mode,
|
|
10034
|
+
appId,
|
|
10035
|
+
ownerType: registration.ownerType,
|
|
10036
|
+
portfolio: registration.portfolio ?? null,
|
|
10037
|
+
applicationId: registration.applicationId,
|
|
10038
|
+
serviceSlug: registration.serviceSlug,
|
|
10039
|
+
audience: registration.audience,
|
|
10040
|
+
tenancy: registration.tenancy,
|
|
10041
|
+
surfaceModel: registration.surfaceModel,
|
|
10042
|
+
containerAppId: registration.containerAppId ?? null,
|
|
10043
|
+
distributionModel: registration.distributionModel,
|
|
10044
|
+
name: registration.displayName,
|
|
10045
|
+
description: registration.description,
|
|
10046
|
+
category: registration.category ?? null,
|
|
10047
|
+
version: registration.version,
|
|
10048
|
+
pricingType: registration.pricing.type,
|
|
10049
|
+
monthlyPrice: registration.pricing.monthlyPrice ?? null,
|
|
10050
|
+
productionUrl,
|
|
10051
|
+
demoUrl: registration.ownerType === "topolo_first_party" ? productionUrl : registration.demoUrl ?? productionUrl,
|
|
10052
|
+
repoUrl: registration.repoUrl ?? null,
|
|
10053
|
+
supportEmail: registration.supportEmail,
|
|
10054
|
+
tags: registration.tags ?? [],
|
|
10055
|
+
screenshots: registration.screenshots ?? [],
|
|
10056
|
+
callbackUrls: [`${productionUrl}${callbackPath}`],
|
|
10057
|
+
requestedScopes: registration.auth.requestedScopes,
|
|
10058
|
+
capabilities: registration.capabilities,
|
|
10059
|
+
marketplace: { ...registration.marketplace, appUrl: productionUrl },
|
|
10060
|
+
notifications: manifest.notifications ?? null
|
|
10061
|
+
}
|
|
10062
|
+
};
|
|
10063
|
+
}
|
|
10064
|
+
function defaultAuthOrigin2(environment) {
|
|
10065
|
+
if (environment === "development") return "https://auth.topolo.dev";
|
|
10066
|
+
if (environment === "staging") return "https://auth.stg.topolo.us";
|
|
10067
|
+
return "https://auth.topolo.app";
|
|
10068
|
+
}
|
|
10069
|
+
async function resolveDevelopersAppId(authOrigin, fetchImpl) {
|
|
10070
|
+
const response = await fetchImpl(`${authOrigin}/api/services/by-slug/topolo-developers`, {
|
|
10071
|
+
headers: { Accept: "application/json" }
|
|
10072
|
+
});
|
|
10073
|
+
const payload = await response.json().catch(() => null);
|
|
10074
|
+
const appId = typeof payload?.data?.app_id === "string" ? payload.data.app_id.trim() : "";
|
|
10075
|
+
if (!response.ok || !appId) throw new Error("Could not resolve the Developers app id from Auth.");
|
|
10076
|
+
return appId;
|
|
10077
|
+
}
|
|
10078
|
+
async function reconcileApplicationRequest(request, dependencies = {}) {
|
|
10079
|
+
const selectEnvironment = dependencies.selectedEnvironment || selectedEnvironment;
|
|
10080
|
+
const refreshCredential = dependencies.refreshCredentialIfNeeded || refreshCredentialIfNeeded;
|
|
10081
|
+
const makeClient2 = dependencies.buildClient || buildClient;
|
|
10082
|
+
const loadConfig = dependencies.readConfig || readConfig;
|
|
10083
|
+
const resolveProfile = dependencies.resolvedProfileConfig || resolvedProfileConfig;
|
|
10084
|
+
const assertServiceUrl = dependencies.assertServiceUrlForEnvironment || assertServiceUrlForEnvironment;
|
|
10085
|
+
const fetchImpl = dependencies.fetch || fetch;
|
|
10086
|
+
const environment = selectEnvironment();
|
|
10087
|
+
const credential = await refreshCredential(environment);
|
|
10088
|
+
if (!credential) throw new Error("Application reconciliation requires an authenticated Topolo session. Run `topolo auth login`.");
|
|
10089
|
+
const client = await makeClient2();
|
|
10090
|
+
if (!client) throw new Error("Could not build the authenticated Topolo client.");
|
|
10091
|
+
await client.client.introspect();
|
|
10092
|
+
const config = await loadConfig();
|
|
10093
|
+
const profile = resolveProfile(config, environment);
|
|
10094
|
+
const authOrigin = (profile.serviceUrls?.auth || defaultAuthOrigin2(environment)).replace(/\/+$/, "");
|
|
10095
|
+
const developersOrigin = (profile.serviceUrls?.developers || "https://developers.topolo.app").replace(/\/+$/, "");
|
|
10096
|
+
assertServiceUrl("auth", authOrigin, environment);
|
|
10097
|
+
assertServiceUrl("developers", developersOrigin, environment);
|
|
10098
|
+
const headers = new Headers({
|
|
10099
|
+
"Content-Type": "application/json",
|
|
10100
|
+
"X-App-ID": await resolveDevelopersAppId(authOrigin, fetchImpl),
|
|
10101
|
+
"X-Topolo-Client": `topolo-cli/${CLI_VERSION}`
|
|
10102
|
+
});
|
|
10103
|
+
const agentName = resolveAgentName(profile.agentName);
|
|
10104
|
+
if (agentName) headers.set("X-Topolo-Agent", agentName);
|
|
10105
|
+
if (credential.kind === "api_key") headers.set("X-Api-Key", credential.apiKey);
|
|
10106
|
+
else headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
10107
|
+
const endpoint = request.ownerType === "topolo_first_party" ? "/api/developer-console/admin/apps/reconcile" : "/api/developer-console/apps/reconcile";
|
|
10108
|
+
const response = await fetchImpl(`${developersOrigin}${endpoint}`, {
|
|
10109
|
+
method: "POST",
|
|
10110
|
+
headers,
|
|
10111
|
+
body: JSON.stringify(request)
|
|
10112
|
+
});
|
|
10113
|
+
const envelope = await response.json().catch(() => null);
|
|
10114
|
+
if (!response.ok || !envelope?.success || !envelope.data) {
|
|
10115
|
+
const message = typeof envelope?.error === "string" ? envelope.error : envelope?.error?.message || envelope?.message || `Application reconciliation failed with status ${response.status}.`;
|
|
10116
|
+
const error = new Error(message);
|
|
10117
|
+
error.code = response.status === 409 ? "immutable_drift" : response.status === 403 ? "permission_denied" : "reconcile_failed";
|
|
10118
|
+
error.details = typeof envelope?.error === "object" ? envelope.error.details : void 0;
|
|
10119
|
+
throw error;
|
|
10120
|
+
}
|
|
10121
|
+
return envelope.data;
|
|
10122
|
+
}
|
|
10123
|
+
function resolveReconcileMode(options) {
|
|
10124
|
+
const explicit = [options.check, options.dryRun, options.apply].filter(Boolean).length;
|
|
10125
|
+
if (explicit > 1) throw new Error("Choose exactly one of --check, --dry-run, or --apply.");
|
|
10126
|
+
if (options.apply) return { mode: "apply", interactiveApply: false };
|
|
10127
|
+
if (options.check || options.dryRun) return { mode: "check", interactiveApply: false };
|
|
10128
|
+
return {
|
|
10129
|
+
mode: "check",
|
|
10130
|
+
interactiveApply: !options.nonInteractive && Boolean(options.interactive || process.stdin.isTTY && process.stdout.isTTY)
|
|
10131
|
+
};
|
|
10132
|
+
}
|
|
10133
|
+
async function confirmApplicationReconcile(result) {
|
|
10134
|
+
if (result.plan.status === "in_sync") return false;
|
|
10135
|
+
const readline = createInterface2({ input: input2, output });
|
|
10136
|
+
try {
|
|
10137
|
+
const answer = await readline.question(`Apply ${result.plan.changes.length} application registration change(s)? [y/N] `);
|
|
10138
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
10139
|
+
} finally {
|
|
10140
|
+
readline.close();
|
|
10141
|
+
}
|
|
10142
|
+
}
|
|
10143
|
+
function formatApplicationReconcile(result, manifestPath) {
|
|
10144
|
+
const lines = [
|
|
10145
|
+
`Application registration: ${result.plan.status}`,
|
|
10146
|
+
`Manifest: ${manifestPath}`,
|
|
10147
|
+
`Mode: ${result.mode}`
|
|
10148
|
+
];
|
|
10149
|
+
for (const change of result.changes || result.plan.changes) {
|
|
10150
|
+
lines.push(`- ${change.path}: ${JSON.stringify(change.before)} -> ${JSON.stringify(change.after)}`);
|
|
10151
|
+
}
|
|
10152
|
+
if (result.projections) lines.push("Catalog/Auth projections: reconciled");
|
|
10153
|
+
return lines.join("\n");
|
|
10154
|
+
}
|
|
10155
|
+
function registerApplicationReconcileCommand(parent, ownerType, dependencies = {}) {
|
|
10156
|
+
const completeRegistration = dependencies.completeRegistration || completeApplicationRegistrationInteractively;
|
|
10157
|
+
const loadRequest = dependencies.loadRequest || loadApplicationReconcileRequest;
|
|
10158
|
+
const reconcileRequest = dependencies.reconcileRequest || reconcileApplicationRequest;
|
|
10159
|
+
const confirm = dependencies.confirm || confirmApplicationReconcile;
|
|
10160
|
+
parent.command("reconcile").description("Check or apply the declarative application registration in topolo.app.json.").option("--manifest <path>", "Path to topolo.app.json", "topolo.app.json").option("--check", "Report drift without mutating remote data").option("--dry-run", "Alias for --check").option("--apply", "Apply mutable drift and required projections").option("--interactive", "Use the human confirmation flow").option("--non-interactive", "Disable all prompts").option("--yes", "Accept the interactive apply confirmation").option("--json", "Output JSON").option("--no-json", "Output human-readable details").action(async (options) => {
|
|
10161
|
+
const outputMode = resolveOutputMode(options.json);
|
|
10162
|
+
try {
|
|
10163
|
+
const selection = resolveReconcileMode(options);
|
|
10164
|
+
if (selection.interactiveApply) {
|
|
10165
|
+
await completeRegistration({
|
|
10166
|
+
manifestPath: options.manifest,
|
|
10167
|
+
expectedOwnerType: ownerType
|
|
10168
|
+
});
|
|
10169
|
+
}
|
|
10170
|
+
const loaded = await loadRequest({
|
|
10171
|
+
manifestPath: options.manifest,
|
|
10172
|
+
expectedOwnerType: ownerType,
|
|
10173
|
+
mode: selection.mode
|
|
10174
|
+
});
|
|
10175
|
+
let result = await reconcileRequest(loaded.request);
|
|
10176
|
+
if (selection.interactiveApply && result.plan.status !== "in_sync") {
|
|
10177
|
+
if (outputMode === "pretty") {
|
|
10178
|
+
process.stdout.write(`${formatApplicationReconcile(result, loaded.manifestPath)}
|
|
10179
|
+
`);
|
|
10180
|
+
}
|
|
10181
|
+
const confirmed = options.yes || await confirm(result);
|
|
10182
|
+
if (confirmed) {
|
|
10183
|
+
result = await reconcileRequest({ ...loaded.request, mode: "apply" });
|
|
10184
|
+
}
|
|
10185
|
+
}
|
|
10186
|
+
emit(
|
|
10187
|
+
{ manifest: loaded.manifestPath, reconciliation: result },
|
|
10188
|
+
outputMode,
|
|
10189
|
+
() => formatApplicationReconcile(result, loaded.manifestPath)
|
|
10190
|
+
);
|
|
10191
|
+
if (result.mode === "check" && result.plan.status !== "in_sync") process.exitCode = 2;
|
|
10192
|
+
} catch (error) {
|
|
10193
|
+
fail(error, outputMode);
|
|
10194
|
+
}
|
|
10195
|
+
});
|
|
10196
|
+
}
|
|
10197
|
+
|
|
10198
|
+
// src/commands/apps.ts
|
|
10190
10199
|
var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
|
|
10191
10200
|
var CLOUDCONTROL_LOCAL_APPLICATION = {
|
|
10192
10201
|
id: "cloudcontrol",
|
|
@@ -10216,6 +10225,7 @@ function registerAppsCommand(program2) {
|
|
|
10216
10225
|
fail(err, mode);
|
|
10217
10226
|
}
|
|
10218
10227
|
});
|
|
10228
|
+
registerApplicationReconcileCommand(apps, "third_party");
|
|
10219
10229
|
apps.command("get <application>").description("Show one Topolo application available to the current credential.").option("--json", "Output JSON", true).option("--no-json", "Output human-readable details").action(async (application, opts) => {
|
|
10220
10230
|
const mode = resolveOutputMode(opts.json);
|
|
10221
10231
|
try {
|
|
@@ -10254,8 +10264,8 @@ function registerAppsCommand(program2) {
|
|
|
10254
10264
|
repoRoot: opts.repoRoot
|
|
10255
10265
|
});
|
|
10256
10266
|
const gate = opts.failOn ? evaluateApplicationAuditGate(payload, opts.failOn) : null;
|
|
10257
|
-
const
|
|
10258
|
-
emit(
|
|
10267
|
+
const output2 = gate ? { ...payload, conformanceGate: gate } : payload;
|
|
10268
|
+
emit(output2, mode, () => formatAudit(output2));
|
|
10259
10269
|
if (gate && !gate.passed) {
|
|
10260
10270
|
if (mode === "json") {
|
|
10261
10271
|
process.stderr.write(JSON.stringify({ error: gate.message, conformanceGate: gate }, null, 2) + "\n");
|
|
@@ -10317,14 +10327,14 @@ function registerAppsCommand(program2) {
|
|
|
10317
10327
|
appIdWriteBack = { ...result, path: manifestFile.path };
|
|
10318
10328
|
}
|
|
10319
10329
|
}
|
|
10320
|
-
const
|
|
10330
|
+
const output2 = {
|
|
10321
10331
|
...payload,
|
|
10322
10332
|
...writeResult ? { write: writeResult } : {},
|
|
10323
10333
|
...provisionResult ? { provision: provisionResult } : {},
|
|
10324
10334
|
...appIdWriteBack ? { appIdWriteBack } : {}
|
|
10325
10335
|
};
|
|
10326
10336
|
emit(
|
|
10327
|
-
|
|
10337
|
+
output2,
|
|
10328
10338
|
mode,
|
|
10329
10339
|
() => formatScaffold(payload, writeResult, provisionResult, appIdWriteBack)
|
|
10330
10340
|
);
|
|
@@ -10388,8 +10398,8 @@ function applicationAuditPayload(application, options = {}) {
|
|
|
10388
10398
|
}
|
|
10389
10399
|
function recordApplicationEvidence(application, options = {}) {
|
|
10390
10400
|
const appId = resolveApplication(application).id;
|
|
10391
|
-
const root =
|
|
10392
|
-
if (!
|
|
10401
|
+
const root = path4.resolve(options.appRoot ?? process.cwd());
|
|
10402
|
+
if (!existsSync4(root)) {
|
|
10393
10403
|
throw new Error(`Application root does not exist: ${root}`);
|
|
10394
10404
|
}
|
|
10395
10405
|
let commit = options.commit;
|
|
@@ -10400,12 +10410,12 @@ function recordApplicationEvidence(application, options = {}) {
|
|
|
10400
10410
|
commit = void 0;
|
|
10401
10411
|
}
|
|
10402
10412
|
}
|
|
10403
|
-
const evidenceDir =
|
|
10404
|
-
const evidencePath =
|
|
10413
|
+
const evidenceDir = path4.join(root, ".topolo");
|
|
10414
|
+
const evidencePath = path4.join(evidenceDir, "evidence.json");
|
|
10405
10415
|
let before = null;
|
|
10406
|
-
if (
|
|
10416
|
+
if (existsSync4(evidencePath)) {
|
|
10407
10417
|
try {
|
|
10408
|
-
const raw = JSON.parse(
|
|
10418
|
+
const raw = JSON.parse(readFileSync5(evidencePath, "utf8"));
|
|
10409
10419
|
if (raw && typeof raw === "object" && raw.version === 1) {
|
|
10410
10420
|
before = raw;
|
|
10411
10421
|
}
|
|
@@ -10443,8 +10453,8 @@ function recordApplicationEvidence(application, options = {}) {
|
|
|
10443
10453
|
"record-evidence: pass at least one of --build, --typecheck, --test, --docs-validate, --smoke <url>"
|
|
10444
10454
|
);
|
|
10445
10455
|
}
|
|
10446
|
-
|
|
10447
|
-
|
|
10456
|
+
mkdirSync2(evidenceDir, { recursive: true });
|
|
10457
|
+
writeFileSync2(evidencePath, JSON.stringify(after, null, 2) + "\n");
|
|
10448
10458
|
return { appId, evidencePath, before, after, updatedFields };
|
|
10449
10459
|
}
|
|
10450
10460
|
function formatEvidence(result) {
|
|
@@ -10700,11 +10710,12 @@ function statusesAtOrWorse(threshold) {
|
|
|
10700
10710
|
}
|
|
10701
10711
|
|
|
10702
10712
|
// src/commands/platform.ts
|
|
10703
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
10704
|
-
import
|
|
10713
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
10714
|
+
import path5 from "path";
|
|
10705
10715
|
function registerPlatformCommand(program2) {
|
|
10706
10716
|
const platform3 = program2.command("platform").description("Internal Topolo first-party platform and personal app commands");
|
|
10707
10717
|
const platformApps = platform3.command("apps").description("Scaffold and operate first-party Topolo platform and personal applications");
|
|
10718
|
+
registerApplicationReconcileCommand(platformApps, "topolo_first_party");
|
|
10708
10719
|
platformApps.command("scaffold <application>").description(
|
|
10709
10720
|
"Generate a first-party Topolo scaffold for the platform or personal portfolio."
|
|
10710
10721
|
).option("--json", "Output JSON").option("--no-json", "Output human-readable checklist").option("--app-dir <path>", "Application repo name or apps/<repo> path").option("--name <name>", "Application display/package source name").option("--package-name <name>", "package.json name").option("--production-url <url>", "Production browser URL").option("--service <id>", "Stable SDK/Auth app id for API-backed apps").option("--portfolio <portfolio>", "First-party portfolio: platform or personal").option(
|
|
@@ -10761,13 +10772,13 @@ function registerPlatformCommand(program2) {
|
|
|
10761
10772
|
}
|
|
10762
10773
|
const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
|
|
10763
10774
|
const provisionResult = shouldProvision ? await provisionApplicationScaffold(payload) : null;
|
|
10764
|
-
const
|
|
10775
|
+
const output2 = opts.workflowOnly ? { application: payload.application, ciWorkflowPath: payload.ciWorkflowPath, ciWorkflow: payload.ciWorkflow } : {
|
|
10765
10776
|
...payload,
|
|
10766
10777
|
...writeResult ? { write: writeResult } : {},
|
|
10767
10778
|
...provisionResult ? { provision: provisionResult } : {}
|
|
10768
10779
|
};
|
|
10769
10780
|
emit(
|
|
10770
|
-
|
|
10781
|
+
output2,
|
|
10771
10782
|
mode,
|
|
10772
10783
|
() => opts.workflowOnly ? payload.ciWorkflow ?? "" : formatScaffold(payload, writeResult, provisionResult)
|
|
10773
10784
|
);
|
|
@@ -10818,12 +10829,12 @@ var SKIPPED_SCAN_DIRS = /* @__PURE__ */ new Set([
|
|
|
10818
10829
|
"output"
|
|
10819
10830
|
]);
|
|
10820
10831
|
function scanStagingProductionUrls(rootInput, allowPatterns) {
|
|
10821
|
-
const root =
|
|
10832
|
+
const root = path5.resolve(rootInput);
|
|
10822
10833
|
const allowlist = allowPatterns.map((pattern) => new RegExp(pattern));
|
|
10823
10834
|
const violations = [];
|
|
10824
10835
|
function shouldSkipFile(filePath) {
|
|
10825
|
-
const basename =
|
|
10826
|
-
const relativePath =
|
|
10836
|
+
const basename = path5.basename(filePath);
|
|
10837
|
+
const relativePath = path5.relative(root, filePath);
|
|
10827
10838
|
if (basename === "package-lock.json" || basename === "pnpm-lock.yaml" || basename === "yarn.lock") return true;
|
|
10828
10839
|
if (basename === "topolo.cloudcontrol.json") return true;
|
|
10829
10840
|
if (basename === "wrangler.toml" || basename === "wrangler.jsonc") return true;
|
|
@@ -10832,15 +10843,15 @@ function scanStagingProductionUrls(rootInput, allowPatterns) {
|
|
|
10832
10843
|
if (basename.endsWith(".generated.ts")) return true;
|
|
10833
10844
|
if (basename.endsWith(".test.ts") || basename.endsWith(".test.tsx") || basename.endsWith(".spec.ts")) return true;
|
|
10834
10845
|
if (basename === ".env.production") return true;
|
|
10835
|
-
if (relativePath ===
|
|
10836
|
-
if (relativePath.includes(`${
|
|
10837
|
-
if (relativePath.startsWith(`scripts${
|
|
10838
|
-
return !RUNTIME_EXTENSIONS.has(
|
|
10846
|
+
if (relativePath === path5.join("src", "environment.ts") || relativePath === path5.join("src", "environmentUrls.ts") || relativePath.endsWith(path5.join("src", "environment.ts")) || relativePath.endsWith(path5.join("src", "lib", "environment.ts")) || relativePath === path5.join("packages", "topolo-ui-kit", "src", "environmentUrls.ts")) return true;
|
|
10847
|
+
if (relativePath.includes(`${path5.sep}test${path5.sep}`) || relativePath.includes(`${path5.sep}tests${path5.sep}`) || relativePath.includes(`${path5.sep}e2e${path5.sep}`)) return true;
|
|
10848
|
+
if (relativePath.startsWith(`scripts${path5.sep}`) || relativePath.includes(`${path5.sep}scripts${path5.sep}`)) return true;
|
|
10849
|
+
return !RUNTIME_EXTENSIONS.has(path5.extname(filePath));
|
|
10839
10850
|
}
|
|
10840
10851
|
function scanFile(filePath) {
|
|
10841
10852
|
if (shouldSkipFile(filePath)) return;
|
|
10842
|
-
const relativePath =
|
|
10843
|
-
const content =
|
|
10853
|
+
const relativePath = path5.relative(root, filePath);
|
|
10854
|
+
const content = readFileSync6(filePath, "utf8");
|
|
10844
10855
|
const lines = content.split(/\r?\n/);
|
|
10845
10856
|
lines.forEach((line, index) => {
|
|
10846
10857
|
const matches = [...line.matchAll(PRODUCTION_TOPOLO_URL_PATTERN)].map((match) => match[0]);
|
|
@@ -10854,7 +10865,7 @@ function scanStagingProductionUrls(rootInput, allowPatterns) {
|
|
|
10854
10865
|
}
|
|
10855
10866
|
function walk(dir) {
|
|
10856
10867
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
10857
|
-
const fullPath =
|
|
10868
|
+
const fullPath = path5.join(dir, entry.name);
|
|
10858
10869
|
if (entry.isDirectory()) {
|
|
10859
10870
|
if (!SKIPPED_SCAN_DIRS.has(entry.name)) walk(fullPath);
|
|
10860
10871
|
continue;
|
|
@@ -11022,7 +11033,7 @@ var DEFAULT_SEED_BASE_URL = "https://seed.stg.topolo.us";
|
|
|
11022
11033
|
function seedBaseUrl() {
|
|
11023
11034
|
return process.env.TOPOLO_SEED_URL || DEFAULT_SEED_BASE_URL;
|
|
11024
11035
|
}
|
|
11025
|
-
async function authedFetch(
|
|
11036
|
+
async function authedFetch(path6, init = {}) {
|
|
11026
11037
|
const credential = await refreshCredentialIfNeeded("staging");
|
|
11027
11038
|
if (!credential) {
|
|
11028
11039
|
throw new Error("No staging credential configured. Run `topolo auth login --env staging` or set TOPOLO_STAGING_API_KEY.");
|
|
@@ -11032,7 +11043,7 @@ async function authedFetch(path4, init = {}) {
|
|
|
11032
11043
|
else headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
11033
11044
|
headers.set("X-Topolo-Client", `topolo-cli/${CLI_VERSION}`);
|
|
11034
11045
|
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
11035
|
-
const response = await fetch(`${seedBaseUrl()}${
|
|
11046
|
+
const response = await fetch(`${seedBaseUrl()}${path6}`, { ...init, headers });
|
|
11036
11047
|
const text = await response.text();
|
|
11037
11048
|
let body;
|
|
11038
11049
|
try {
|
|
@@ -11309,7 +11320,7 @@ function createOAuthClientPayload(options) {
|
|
|
11309
11320
|
logo_url: options.logoUrl
|
|
11310
11321
|
};
|
|
11311
11322
|
}
|
|
11312
|
-
async function authFetchJson(
|
|
11323
|
+
async function authFetchJson(path6, init = {}) {
|
|
11313
11324
|
const environment = selectedEnvironment();
|
|
11314
11325
|
const credential = await refreshCredentialIfNeeded(environment);
|
|
11315
11326
|
if (!credential) {
|
|
@@ -11331,7 +11342,7 @@ async function authFetchJson(path4, init = {}) {
|
|
|
11331
11342
|
throw err;
|
|
11332
11343
|
}
|
|
11333
11344
|
headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
11334
|
-
const response = await fetch(`${authBaseUrl}${
|
|
11345
|
+
const response = await fetch(`${authBaseUrl}${path6}`, { ...init, headers });
|
|
11335
11346
|
const text = await response.text();
|
|
11336
11347
|
const payload = text ? JSON.parse(text) : {};
|
|
11337
11348
|
if (!response.ok || payload.success === false) {
|
|
@@ -11347,7 +11358,7 @@ function normalizeList(values, fallback = []) {
|
|
|
11347
11358
|
}
|
|
11348
11359
|
|
|
11349
11360
|
// src/commands/integrations.ts
|
|
11350
|
-
import { readdir, readFile as
|
|
11361
|
+
import { readdir, readFile as readFile4, stat } from "fs/promises";
|
|
11351
11362
|
import { join as join4, relative as relative3 } from "path";
|
|
11352
11363
|
function registerIntegrationsCommand(program2) {
|
|
11353
11364
|
const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
|
|
@@ -11491,7 +11502,7 @@ function detectFramework(packageJson2, haystack) {
|
|
|
11491
11502
|
}
|
|
11492
11503
|
async function readPackageJson2(repoRoot) {
|
|
11493
11504
|
try {
|
|
11494
|
-
return JSON.parse(await
|
|
11505
|
+
return JSON.parse(await readFile4(join4(repoRoot, "package.json"), "utf8"));
|
|
11495
11506
|
} catch {
|
|
11496
11507
|
return null;
|
|
11497
11508
|
}
|
|
@@ -11514,7 +11525,7 @@ async function collectFiles(repoRoot) {
|
|
|
11514
11525
|
if (info.size > 25e4) continue;
|
|
11515
11526
|
result.push({
|
|
11516
11527
|
path: relative3(root, fullPath),
|
|
11517
|
-
content: await
|
|
11528
|
+
content: await readFile4(fullPath, "utf8")
|
|
11518
11529
|
});
|
|
11519
11530
|
}
|
|
11520
11531
|
}
|
|
@@ -11530,9 +11541,9 @@ function formatCheckResult(result) {
|
|
|
11530
11541
|
}
|
|
11531
11542
|
|
|
11532
11543
|
// src/commands/skills.ts
|
|
11533
|
-
import { spawnSync } from "child_process";
|
|
11534
|
-
import { existsSync as
|
|
11535
|
-
import { mkdir as mkdir3, copyFile, readFile as
|
|
11544
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
11545
|
+
import { existsSync as existsSync5 } from "fs";
|
|
11546
|
+
import { mkdir as mkdir3, copyFile, readFile as readFile5, writeFile as writeFile5, rm } from "fs/promises";
|
|
11536
11547
|
import { homedir as homedir2 } from "os";
|
|
11537
11548
|
import { dirname as dirname4, join as join5, resolve as resolve3 } from "path";
|
|
11538
11549
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -11543,19 +11554,19 @@ function skillsSourceDir() {
|
|
|
11543
11554
|
const distCandidate = resolve3(here, "../skills");
|
|
11544
11555
|
const srcCandidate = resolve3(here, "../../skills");
|
|
11545
11556
|
for (const candidate of [distCandidate, srcCandidate]) {
|
|
11546
|
-
if (
|
|
11557
|
+
if (existsSync5(join5(candidate, "topolo", "SKILL.md"))) return candidate;
|
|
11547
11558
|
}
|
|
11548
11559
|
return distCandidate;
|
|
11549
11560
|
}
|
|
11550
11561
|
function resolveMcpCommand() {
|
|
11551
|
-
const probe =
|
|
11562
|
+
const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["topolo-mcp"], {
|
|
11552
11563
|
stdio: "ignore"
|
|
11553
11564
|
});
|
|
11554
11565
|
if (probe.status === 0) return { command: "topolo-mcp", args: [] };
|
|
11555
11566
|
return { command: "npx", args: ["-y", "@topolo/mcp"] };
|
|
11556
11567
|
}
|
|
11557
11568
|
function agentBinaryExists(bin) {
|
|
11558
|
-
const probe =
|
|
11569
|
+
const probe = spawnSync2(process.platform === "win32" ? "where" : "which", [bin], {
|
|
11559
11570
|
stdio: "ignore"
|
|
11560
11571
|
});
|
|
11561
11572
|
return probe.status === 0;
|
|
@@ -11576,9 +11587,9 @@ async function installSkillFile(sourceDir, targetDir) {
|
|
|
11576
11587
|
function registerViaOfficialCli(cli, mcp, agentName) {
|
|
11577
11588
|
if (!agentBinaryExists(cli)) return false;
|
|
11578
11589
|
const scopeArgs = cli === "claude" ? ["-s", "user"] : [];
|
|
11579
|
-
|
|
11590
|
+
spawnSync2(cli, ["mcp", "remove", "topolo", ...scopeArgs], { stdio: "ignore" });
|
|
11580
11591
|
const envArgs = cli === "claude" ? ["-e", `TOPOLO_AGENT_NAME=${agentName}`] : ["--env", `TOPOLO_AGENT_NAME=${agentName}`];
|
|
11581
|
-
const result =
|
|
11592
|
+
const result = spawnSync2(
|
|
11582
11593
|
cli,
|
|
11583
11594
|
["mcp", "add", "topolo", ...scopeArgs, ...envArgs, "--", mcp.command, ...mcp.args],
|
|
11584
11595
|
{ stdio: "ignore" }
|
|
@@ -11586,11 +11597,11 @@ function registerViaOfficialCli(cli, mcp, agentName) {
|
|
|
11586
11597
|
return result.status === 0;
|
|
11587
11598
|
}
|
|
11588
11599
|
async function registerClaudeViaConfig(mcp) {
|
|
11589
|
-
const
|
|
11600
|
+
const path6 = join5(homedir2(), ".claude.json");
|
|
11590
11601
|
let config = {};
|
|
11591
|
-
if (
|
|
11602
|
+
if (existsSync5(path6)) {
|
|
11592
11603
|
try {
|
|
11593
|
-
config = JSON.parse(await
|
|
11604
|
+
config = JSON.parse(await readFile5(path6, "utf8"));
|
|
11594
11605
|
} catch {
|
|
11595
11606
|
config = {};
|
|
11596
11607
|
}
|
|
@@ -11603,13 +11614,13 @@ async function registerClaudeViaConfig(mcp) {
|
|
|
11603
11614
|
env: { TOPOLO_AGENT_NAME: AGENT_NAME_FOR.claude }
|
|
11604
11615
|
};
|
|
11605
11616
|
config.mcpServers = servers;
|
|
11606
|
-
await
|
|
11617
|
+
await writeFile5(path6, `${JSON.stringify(config, null, 2)}
|
|
11607
11618
|
`, "utf8");
|
|
11608
11619
|
}
|
|
11609
11620
|
async function registerCodexViaConfig(mcp) {
|
|
11610
|
-
const
|
|
11621
|
+
const path6 = join5(homedir2(), ".codex", "config.toml");
|
|
11611
11622
|
let text = "";
|
|
11612
|
-
if (
|
|
11623
|
+
if (existsSync5(path6)) text = await readFile5(path6, "utf8");
|
|
11613
11624
|
text = text.replace(
|
|
11614
11625
|
/(^|\n)\[mcp_servers\.topolo(?:\.[a-z]+)?\][^\[]*/g,
|
|
11615
11626
|
(match) => match.startsWith("\n") ? "\n" : ""
|
|
@@ -11625,8 +11636,8 @@ command = "${mcp.command}"${argsToml}
|
|
|
11625
11636
|
[mcp_servers.topolo.env]
|
|
11626
11637
|
TOPOLO_AGENT_NAME = "${AGENT_NAME_FOR.codex}"
|
|
11627
11638
|
`;
|
|
11628
|
-
await mkdir3(dirname4(
|
|
11629
|
-
await
|
|
11639
|
+
await mkdir3(dirname4(path6), { recursive: true });
|
|
11640
|
+
await writeFile5(path6, `${text}${block}`, "utf8");
|
|
11630
11641
|
}
|
|
11631
11642
|
async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
11632
11643
|
const skillDir = agent === "claude" ? claudeSkillDir(opts.projectDir) : codexSkillDir();
|
|
@@ -11664,7 +11675,7 @@ function registerSkillsCommand(program2) {
|
|
|
11664
11675
|
const mode = resolveOutputMode(opts.json);
|
|
11665
11676
|
try {
|
|
11666
11677
|
const sourceDir = skillsSourceDir();
|
|
11667
|
-
if (!
|
|
11678
|
+
if (!existsSync5(join5(sourceDir, "topolo", "SKILL.md"))) {
|
|
11668
11679
|
throw new Error(
|
|
11669
11680
|
"Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli"
|
|
11670
11681
|
);
|
|
@@ -11693,7 +11704,7 @@ function registerSkillsCommand(program2) {
|
|
|
11693
11704
|
const skillDir = agent === "claude" ? claudeSkillDir() : codexSkillDir();
|
|
11694
11705
|
return {
|
|
11695
11706
|
agent,
|
|
11696
|
-
skillInstalled:
|
|
11707
|
+
skillInstalled: existsSync5(join5(skillDir, "SKILL.md")),
|
|
11697
11708
|
skillPath: join5(skillDir, "SKILL.md"),
|
|
11698
11709
|
cliAvailable: agentBinaryExists(agent)
|
|
11699
11710
|
};
|
|
@@ -11710,10 +11721,10 @@ function registerSkillsCommand(program2) {
|
|
|
11710
11721
|
for (const agent of resolveTargets(opts)) {
|
|
11711
11722
|
const skillDir = agent === "claude" ? claudeSkillDir() : codexSkillDir();
|
|
11712
11723
|
const skillFile = join5(skillDir, "SKILL.md");
|
|
11713
|
-
const skillRemoved =
|
|
11724
|
+
const skillRemoved = existsSync5(skillFile);
|
|
11714
11725
|
if (skillRemoved) await rm(skillFile, { force: true });
|
|
11715
11726
|
if (agentBinaryExists(agent)) {
|
|
11716
|
-
|
|
11727
|
+
spawnSync2(agent, ["mcp", "remove", "topolo"], { stdio: "ignore" });
|
|
11717
11728
|
}
|
|
11718
11729
|
removed.push({ agent, skillRemoved });
|
|
11719
11730
|
}
|