@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/index.js CHANGED
@@ -61,13 +61,13 @@ var TopoloHttpError = class extends TopoloSdkError {
61
61
  body;
62
62
  service;
63
63
  path;
64
- constructor(service, path4, status, body, message) {
65
- super("http_error", message ?? `HTTP ${status} from ${service}${path4}`);
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 = path4;
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/login",
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": null,
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, input2) {
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, input2, "$", schema, errors, 0);
2176
+ validateNode(schema, input3, "$", schema, errors, 0);
2179
2177
  return { valid: errors.length === 0, errors };
2180
2178
  }
2181
- function inspectSchema(schema, path4, root, depth) {
2182
- if (depth > 128) return { path: path4, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
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: path4, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
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: path4, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
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: path4, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
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, `${path4}.${key}`, root, depth + 1);
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, path4, root, errors, depth) {
2213
+ function validateNode(schema, value, path6, root, errors, depth) {
2216
2214
  if (depth > 128) {
2217
- errors.push({ path: path4, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
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: path4, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
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, path4, root, errors, depth + 1);
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, path4, root, errors, depth + 1);
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: path4, code: "any_of", message: "Value does not match any allowed schema." });
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: path4, code: "one_of", message: "Value must match exactly one allowed schema." });
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: path4, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
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: path4, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
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: path4, code: "type", message: `Expected ${String(schema.type)}.` });
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, path4, errors);
2258
- if (typeof value === "number") validateNumber(schema, value, path4, errors);
2259
- if (Array.isArray(value)) validateArray(schema, value, path4, root, errors, depth);
2260
- if (isRecord(value)) validateObject(schema, value, path4, root, errors, depth);
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, path4, root, errors, depth) {
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: path4, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
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: path4, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
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: `${path4}.${key}`, code: "required", message: "Required property is missing." });
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, `${path4}.${key}`, root, errors, depth + 1);
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, `${path4}.${key}`, root, errors, depth + 1);
2282
+ validateNode(childSchema, child, `${path6}.${key}`, root, errors, depth + 1);
2285
2283
  } else if (schema.additionalProperties === false) {
2286
- errors.push({ path: `${path4}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
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, `${path4}.${key}`, root, errors, depth + 1);
2286
+ validateNode(schema.additionalProperties, child, `${path6}.${key}`, root, errors, depth + 1);
2289
2287
  }
2290
2288
  }
2291
2289
  }
2292
- function validateArray(schema, value, path4, root, errors, depth) {
2290
+ function validateArray(schema, value, path6, root, errors, depth) {
2293
2291
  if (typeof schema.minItems === "number" && value.length < schema.minItems) {
2294
- errors.push({ path: path4, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
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: path4, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
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: path4, code: "unique_items", message: "Expected every item to be unique." });
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, `${path4}[${index}]`, root, errors, depth + 1));
2302
+ value.forEach((item, index) => validateNode(schema.items, item, `${path6}[${index}]`, root, errors, depth + 1));
2305
2303
  }
2306
2304
  }
2307
- function validateString(schema, value, path4, errors) {
2305
+ function validateString(schema, value, path6, errors) {
2308
2306
  if (typeof schema.minLength === "number" && value.length < schema.minLength) {
2309
- errors.push({ path: path4, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
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: path4, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
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: path4, code: "pattern", message: "Value does not match the required pattern." });
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: path4, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
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: path4, code: "format_uri", message: "Expected an absolute URL." });
2322
- if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path: path4, code: "format_email", message: "Expected an email address." });
2323
- if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path: path4, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
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, path4, errors) {
2326
- if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path: path4, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
2327
- if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path: path4, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
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: path4, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
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: path4, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
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: path4, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
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 record = raw;
2849
- if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
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: record.digest,
2855
- actionCount: Number(record.actionCount)
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, input2 = {}, options = {}) {
2890
+ async callAction(action, input3 = {}, options = {}) {
2893
2891
  const entry = await this.getAction(action);
2894
- return this.executeResolvedAction(entry, input2, options);
2892
+ return this.executeResolvedAction(entry, input3, options);
2895
2893
  }
2896
- async validateActionInput(action, input2 = {}) {
2894
+ async validateActionInput(action, input3 = {}) {
2897
2895
  const entry = await this.getAction(action);
2898
- return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input2) };
2896
+ return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input3) };
2899
2897
  }
2900
- async planAction(action, input2 = {}, options = {}) {
2898
+ async planAction(action, input3 = {}, options = {}) {
2901
2899
  const entry = await this.getAction(action);
2902
- const validation = validateTopoloActionInput(entry.inputSchema, input2);
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, input2);
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, input2 = {}, options = {}) {
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, input2, options);
2937
+ return this.executeResolvedAction(entry, input3, options);
2940
2938
  }
2941
- async executeResolvedAction(entry, input2, options) {
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, input2);
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, input2);
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(input2) {
3054
- if (!isRecord2(input2)) return null;
3055
- const resourceType = readString(input2.resourceType ?? input2.resource_type);
3056
- const label = readString(input2.label);
3057
- const emptyLabel = readString(input2.emptyLabel ?? input2.empty_label);
3058
- const resourceConcept = readString(input2.resourceConcept ?? input2.resource_concept);
3059
- const canonicalResourceType = readString(input2.canonicalResourceType ?? input2.canonical_resource_type);
3060
- const defaultEntitlement = readString(input2.defaultEntitlement ?? input2.default_entitlement);
3061
- const exceptionPolicy = readString(input2.exceptionPolicy ?? input2.exception_policy);
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(input2) {
3076
- if (!Array.isArray(input2)) return [];
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 input2) {
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(input2) {
3088
- if (!isRecord2(input2)) return null;
3089
- const resourceType = readString(input2.resourceType ?? input2.resource_type);
3090
- const resourceId = readString(input2.resourceId ?? input2.resource_id);
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(input2.label),
3096
- slug: readOptionalString(input2.slug),
3097
- status: readOptionalString(input2.status),
3098
- organizationId: readOptionalString(input2.organizationId ?? input2.organization_id),
3099
- metadata: isRecord2(input2.metadata) ? input2.metadata : {}
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(input2) {
3103
- if (!Array.isArray(input2)) return [];
3104
- return input2.flatMap((binding) => {
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 path4 = pickActionString(a.path);
3231
- if (!actionId || !name || !appId || !method || !path4 || !isHttpMethod(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: path4,
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, input2) {
3281
- const remaining = { ...input2 || {} };
3282
- const path4 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
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: path4,
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: path4,
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(path4, body) {
3477
- const url = new URL(path4, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
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", path4, res.status, parsed);
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, path4, status, body, message) {
3532
- super("http_error", message ?? `HTTP ${status} from ${service}${path4}`);
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 = path4;
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(path4, body) {
3669
- const url = new URL(path4, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
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", path4, res.status, parsed);
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 path4 = configPath();
3716
- if (!existsSync(path4)) return {};
3713
+ const path6 = configPath();
3714
+ if (!existsSync(path6)) return {};
3717
3715
  try {
3718
- const raw = await readFile(path4, "utf8");
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 path4 = configPath();
3726
- await mkdir(dirname(path4), { recursive: true });
3727
- await writeFile(path4, JSON.stringify(config, null, 2) + "\n", "utf8");
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(path4, 384);
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 path4 of candidates) {
4048
+ for (const path6 of candidates) {
4050
4049
  try {
4051
- const pkg = JSON.parse(readFileSync(path4, "utf8"));
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, path4, opts) => {
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: path4,
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 input2 = parseJsonObject(opts.data || "{}");
4541
+ const input3 = parseJsonObject(opts.data || "{}");
4543
4542
  const resource = parseResourceOption(opts.resourceType, opts.resourceId);
4544
- const result = await topolo.client.callAction(action, input2, {
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 path4 = value.slice(1);
4614
- if (!path4) throw new Error("--data @file path must not be empty");
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(path4, "utf8");
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 ${path4}: ${detail}`);
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 existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
4694
- import path2 from "path";
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((record) => {
5286
- const scripts = record.packageJson.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 record of evidence.packageJsons) {
5553
- const scripts = record.packageJson.scripts ?? {};
5554
- const isRootPackage = record.path === evidence.packageJsonPath;
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(record)}:${canonical}`);
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((record) => [
5568
- ...Object.keys(record.packageJson.dependencies ?? {}),
5569
- ...Object.keys(record.packageJson.devDependencies ?? {}),
5570
- ...Object.keys(record.packageJson.peerDependencies ?? {})
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((record) => {
5575
+ evidence.packageJsons.flatMap((record2) => {
5577
5576
  const specs = [
5578
- record.packageJson.dependencies?.[packageName],
5579
- record.packageJson.devDependencies?.[packageName],
5580
- record.packageJson.peerDependencies?.[packageName]
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(record) {
5707
- return record.packageJson.name ?? relativeTopoloPath(path.dirname(record.path));
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 path4 = String(value || "/dashboard").trim();
5797
- if (!path4.startsWith("/") || path4.startsWith("//") || /[?#]/.test(path4)) {
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(path4)) {
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 path4.replace(/\/$/, "") || "/dashboard";
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(path4) {
5943
+ async function pathExists(path6) {
5945
5944
  try {
5946
- await access(path4, constants.F_OK);
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
- " const defaultAuthUrl =",
6483
- " typeof window !== 'undefined' && window.location.hostname.endsWith('.stg.topolo.us')",
6484
- " ? 'https://auth.stg.topolo.us'",
6485
- " : 'https://auth.topolo.app';",
6486
- " return (import.meta.env.VITE_TOPOLO_AUTH_URL || defaultAuthUrl).replace(/\\/+$/, '');",
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
- const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? import.meta.env.VITE_TOPOLO_AUTH_BASE_URL ?? 'https://auth.topolo.app';
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
- " authGateway: { enabled: false },",
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 || 'https://auth.topolo.app';",
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
- bindings.queues = [
8138
- {
8139
- binding: resource.queueBinding,
8140
- staging: { queueName: resource.queueStagingName },
8141
- production: { queueName: resource.queueProductionName },
8142
- consumer: true
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: slug
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
- targets.web = {
8179
- kind: "worker",
8180
- dir: "apps/web",
8181
- wranglerName: `topolo-${slug}`,
8182
- subdomain: slug,
8183
- main: "src/worker.ts",
8184
- workersDev: false,
8185
- assets: {
8186
- directory: "./dist",
8187
- binding: "ASSETS",
8188
- notFoundHandling: "single-page-application",
8189
- runWorkerFirst: ["/api/*"]
8190
- },
8191
- compatibilityFlags: ["nodejs_compat"],
8192
- buildCommand: {
8193
- staging: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us pnpm run build",
8194
- production: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app pnpm run build"
8195
- },
8196
- healthcheckPath: options.api ? "/api/health" : "/",
8197
- ...options.api ? {
8198
- bindings: {
8199
- services: [
8200
- {
8201
- binding: "API",
8202
- service: {
8203
- staging: `topolo-${slug}-api-staging`,
8204
- production: `topolo-${slug}-api-production`
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: options.api ? application.appId : null,
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 = api ? target === "platform" ? `topolo-${slug}` : id : null;
9247
- const defaultAppId = target === "platform" ? `app_topolo_${id}` : catalogApplication?.services[0] ?? id;
9248
- const appId = api ? options.appId ?? defaultAppId : null;
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 topolo.cloudcontrol.json or topolo.app.json",
9555
+ "bindings match the declared resources",
9892
9556
  "secrets are referenced by name only"
9893
9557
  ],
9894
- content: target === "platform" ? platformApiWranglerToml(application, options.payload.resources, options.payload.profile) : wranglerToml(application, target, options.payload.resources, options.payload.profile)
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/first-party-app-scaffolds" : "/api/developer-console/apps/scaffold";
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 service = pickRecord(envelope.data.service);
10057
- const authConfig = pickRecord(envelope.data.authConfig);
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
- target: payload.target,
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
- marketplaceIntent: payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
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 output = gate ? { ...payload, conformanceGate: gate } : payload;
10258
- emit(output, mode, () => formatAudit(output));
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 output = {
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
- output,
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 = path2.resolve(options.appRoot ?? process.cwd());
10392
- if (!existsSync3(root)) {
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 = path2.join(root, ".topolo");
10404
- const evidencePath = path2.join(evidenceDir, "evidence.json");
10413
+ const evidenceDir = path4.join(root, ".topolo");
10414
+ const evidencePath = path4.join(evidenceDir, "evidence.json");
10405
10415
  let before = null;
10406
- if (existsSync3(evidencePath)) {
10416
+ if (existsSync4(evidencePath)) {
10407
10417
  try {
10408
- const raw = JSON.parse(readFileSync4(evidencePath, "utf8"));
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
- mkdirSync(evidenceDir, { recursive: true });
10447
- writeFileSync(evidencePath, JSON.stringify(after, null, 2) + "\n");
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 readFileSync5, statSync as statSync2 } from "fs";
10704
- import path3 from "path";
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 output = opts.workflowOnly ? { application: payload.application, ciWorkflowPath: payload.ciWorkflowPath, ciWorkflow: payload.ciWorkflow } : {
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
- output,
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 = path3.resolve(rootInput);
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 = path3.basename(filePath);
10826
- const relativePath = path3.relative(root, filePath);
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 === path3.join("src", "environment.ts") || relativePath === path3.join("src", "environmentUrls.ts") || relativePath.endsWith(path3.join("src", "environment.ts")) || relativePath.endsWith(path3.join("src", "lib", "environment.ts")) || relativePath === path3.join("packages", "topolo-ui-kit", "src", "environmentUrls.ts")) return true;
10836
- if (relativePath.includes(`${path3.sep}test${path3.sep}`) || relativePath.includes(`${path3.sep}tests${path3.sep}`) || relativePath.includes(`${path3.sep}e2e${path3.sep}`)) return true;
10837
- if (relativePath.startsWith(`scripts${path3.sep}`) || relativePath.includes(`${path3.sep}scripts${path3.sep}`)) return true;
10838
- return !RUNTIME_EXTENSIONS.has(path3.extname(filePath));
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 = path3.relative(root, filePath);
10843
- const content = readFileSync5(filePath, "utf8");
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 = path3.join(dir, entry.name);
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(path4, init = {}) {
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()}${path4}`, { ...init, headers });
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(path4, init = {}) {
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}${path4}`, { ...init, headers });
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 readFile3, stat } from "fs/promises";
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 readFile3(join4(repoRoot, "package.json"), "utf8"));
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 readFile3(fullPath, "utf8")
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 existsSync4 } from "fs";
11535
- import { mkdir as mkdir3, copyFile, readFile as readFile4, writeFile as writeFile4, rm } from "fs/promises";
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 (existsSync4(join5(candidate, "topolo", "SKILL.md"))) return candidate;
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 = spawnSync(process.platform === "win32" ? "where" : "which", ["topolo-mcp"], {
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 = spawnSync(process.platform === "win32" ? "where" : "which", [bin], {
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
- spawnSync(cli, ["mcp", "remove", "topolo", ...scopeArgs], { stdio: "ignore" });
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 = spawnSync(
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 path4 = join5(homedir2(), ".claude.json");
11600
+ const path6 = join5(homedir2(), ".claude.json");
11590
11601
  let config = {};
11591
- if (existsSync4(path4)) {
11602
+ if (existsSync5(path6)) {
11592
11603
  try {
11593
- config = JSON.parse(await readFile4(path4, "utf8"));
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 writeFile4(path4, `${JSON.stringify(config, null, 2)}
11617
+ await writeFile5(path6, `${JSON.stringify(config, null, 2)}
11607
11618
  `, "utf8");
11608
11619
  }
11609
11620
  async function registerCodexViaConfig(mcp) {
11610
- const path4 = join5(homedir2(), ".codex", "config.toml");
11621
+ const path6 = join5(homedir2(), ".codex", "config.toml");
11611
11622
  let text = "";
11612
- if (existsSync4(path4)) text = await readFile4(path4, "utf8");
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(path4), { recursive: true });
11629
- await writeFile4(path4, `${text}${block}`, "utf8");
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 (!existsSync4(join5(sourceDir, "topolo", "SKILL.md"))) {
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: existsSync4(join5(skillDir, "SKILL.md")),
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 = existsSync4(skillFile);
11724
+ const skillRemoved = existsSync5(skillFile);
11714
11725
  if (skillRemoved) await rm(skillFile, { force: true });
11715
11726
  if (agentBinaryExists(agent)) {
11716
- spawnSync(agent, ["mcp", "remove", "topolo"], { stdio: "ignore" });
11727
+ spawnSync2(agent, ["mcp", "remove", "topolo"], { stdio: "ignore" });
11717
11728
  }
11718
11729
  removed.push({ agent, skillRemoved });
11719
11730
  }