@topolo/cli 0.4.9 → 0.5.1

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.
Files changed (2) hide show
  1. package/dist/index.js +918 -218
  2. package/package.json +8 -2
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
9
9
  import { createInterface } from "readline/promises";
10
10
  import { stdin as input } from "process";
11
11
 
12
- // node_modules/@topolo/sdk/dist/index.js
12
+ // ../topolo-sdk/dist/index.js
13
13
  function applyAuthHeaders(headers, credential) {
14
14
  if (credential.kind === "api_key") {
15
15
  headers.set("X-Api-Key", credential.apiKey);
@@ -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, path5, status, body, message) {
65
+ super("http_error", message ?? `HTTP ${status} from ${service}${path5}`);
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 = path5;
71
71
  }
72
72
  };
73
73
  var APP_API_URLS = {
@@ -2171,24 +2171,24 @@ var SUPPORTED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
2171
2171
  "exclusiveMaximum",
2172
2172
  "multipleOf"
2173
2173
  ]);
2174
- function validateTopoloActionInput(schema, input2) {
2174
+ function validateTopoloActionInput(schema, input3) {
2175
2175
  const schemaIssue = inspectSchema(schema, "$", schema, 0);
2176
2176
  if (schemaIssue) return { valid: false, errors: [schemaIssue] };
2177
2177
  const errors = [];
2178
- validateNode(schema, input2, "$", schema, errors, 0);
2178
+ validateNode(schema, input3, "$", schema, errors, 0);
2179
2179
  return { valid: errors.length === 0, errors };
2180
2180
  }
2181
- function inspectSchema(schema, path4, root, depth) {
2182
- if (depth > 128) return { path: path4, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
2181
+ function inspectSchema(schema, path5, root, depth) {
2182
+ if (depth > 128) return { path: path5, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
2183
2183
  const unsupportedKeyword = Object.keys(schema).find((keyword) => !SUPPORTED_SCHEMA_KEYWORDS.has(keyword));
2184
2184
  if (unsupportedKeyword) {
2185
- return { path: path4, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
2185
+ return { path: path5, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
2186
2186
  }
2187
2187
  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}.` };
2188
+ return { path: path5, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
2189
2189
  }
2190
2190
  if (typeof schema.$ref === "string" && !resolveLocalReference(root, schema.$ref)) {
2191
- return { path: path4, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
2191
+ return { path: path5, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
2192
2192
  }
2193
2193
  const children = [];
2194
2194
  if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
@@ -2204,7 +2204,7 @@ function inspectSchema(schema, path4, root, depth) {
2204
2204
  );
2205
2205
  for (const [key, child] of children) {
2206
2206
  if (!isRecord(child)) continue;
2207
- const issue = inspectSchema(child, `${path4}.${key}`, root, depth + 1);
2207
+ const issue = inspectSchema(child, `${path5}.${key}`, root, depth + 1);
2208
2208
  if (issue) return issue;
2209
2209
  }
2210
2210
  return null;
@@ -2212,129 +2212,129 @@ function inspectSchema(schema, path4, root, depth) {
2212
2212
  function arraySchemaEntries(keyword, value) {
2213
2213
  return Array.isArray(value) ? value.map((candidate, index) => [`${keyword}[${index}]`, candidate]) : [];
2214
2214
  }
2215
- function validateNode(schema, value, path4, root, errors, depth) {
2215
+ function validateNode(schema, value, path5, root, errors, depth) {
2216
2216
  if (depth > 128) {
2217
- errors.push({ path: path4, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
2217
+ errors.push({ path: path5, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
2218
2218
  return;
2219
2219
  }
2220
2220
  if (typeof schema.$ref === "string") {
2221
2221
  const resolved = resolveLocalReference(root, schema.$ref);
2222
2222
  if (!resolved) {
2223
- errors.push({ path: path4, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
2223
+ errors.push({ path: path5, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
2224
2224
  return;
2225
2225
  }
2226
- validateNode(resolved, value, path4, root, errors, depth + 1);
2226
+ validateNode(resolved, value, path5, root, errors, depth + 1);
2227
2227
  }
2228
2228
  if (Array.isArray(schema.allOf)) {
2229
2229
  for (const candidate of schema.allOf) {
2230
- if (isRecord(candidate)) validateNode(candidate, value, path4, root, errors, depth + 1);
2230
+ if (isRecord(candidate)) validateNode(candidate, value, path5, root, errors, depth + 1);
2231
2231
  }
2232
2232
  }
2233
2233
  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." });
2234
+ errors.push({ path: path5, code: "any_of", message: "Value does not match any allowed schema." });
2235
2235
  return;
2236
2236
  }
2237
2237
  if (Array.isArray(schema.oneOf)) {
2238
2238
  const matches = schema.oneOf.filter((candidate) => candidateMatches(candidate, value, root, depth)).length;
2239
2239
  if (matches !== 1) {
2240
- errors.push({ path: path4, code: "one_of", message: "Value must match exactly one allowed schema." });
2240
+ errors.push({ path: path5, code: "one_of", message: "Value must match exactly one allowed schema." });
2241
2241
  return;
2242
2242
  }
2243
2243
  }
2244
2244
  if (value === null && schema.nullable === true) return;
2245
2245
  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)}.` });
2246
+ errors.push({ path: path5, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
2247
2247
  return;
2248
2248
  }
2249
2249
  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(", ")}.` });
2250
+ errors.push({ path: path5, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
2251
2251
  return;
2252
2252
  }
2253
2253
  if (!matchesType(schema.type, value)) {
2254
- errors.push({ path: path4, code: "type", message: `Expected ${String(schema.type)}.` });
2254
+ errors.push({ path: path5, code: "type", message: `Expected ${String(schema.type)}.` });
2255
2255
  return;
2256
2256
  }
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);
2257
+ if (typeof value === "string") validateString(schema, value, path5, errors);
2258
+ if (typeof value === "number") validateNumber(schema, value, path5, errors);
2259
+ if (Array.isArray(value)) validateArray(schema, value, path5, root, errors, depth);
2260
+ if (isRecord(value)) validateObject(schema, value, path5, root, errors, depth);
2261
2261
  }
2262
- function validateObject(schema, value, path4, root, errors, depth) {
2262
+ function validateObject(schema, value, path5, root, errors, depth) {
2263
2263
  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.` });
2264
+ errors.push({ path: path5, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
2265
2265
  }
2266
2266
  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.` });
2267
+ errors.push({ path: path5, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
2268
2268
  }
2269
2269
  const properties = isRecord(schema.properties) ? schema.properties : {};
2270
2270
  const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
2271
2271
  for (const key of required) {
2272
2272
  if (!Object.prototype.hasOwnProperty.call(value, key)) {
2273
- errors.push({ path: `${path4}.${key}`, code: "required", message: "Required property is missing." });
2273
+ errors.push({ path: `${path5}.${key}`, code: "required", message: "Required property is missing." });
2274
2274
  }
2275
2275
  }
2276
2276
  if (isRecord(schema.propertyNames)) {
2277
2277
  for (const key of Object.keys(value)) {
2278
- validateNode(schema.propertyNames, key, `${path4}.${key}`, root, errors, depth + 1);
2278
+ validateNode(schema.propertyNames, key, `${path5}.${key}`, root, errors, depth + 1);
2279
2279
  }
2280
2280
  }
2281
2281
  for (const [key, child] of Object.entries(value)) {
2282
2282
  const childSchema = properties[key];
2283
2283
  if (isRecord(childSchema)) {
2284
- validateNode(childSchema, child, `${path4}.${key}`, root, errors, depth + 1);
2284
+ validateNode(childSchema, child, `${path5}.${key}`, root, errors, depth + 1);
2285
2285
  } else if (schema.additionalProperties === false) {
2286
- errors.push({ path: `${path4}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
2286
+ errors.push({ path: `${path5}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
2287
2287
  } else if (isRecord(schema.additionalProperties)) {
2288
- validateNode(schema.additionalProperties, child, `${path4}.${key}`, root, errors, depth + 1);
2288
+ validateNode(schema.additionalProperties, child, `${path5}.${key}`, root, errors, depth + 1);
2289
2289
  }
2290
2290
  }
2291
2291
  }
2292
- function validateArray(schema, value, path4, root, errors, depth) {
2292
+ function validateArray(schema, value, path5, root, errors, depth) {
2293
2293
  if (typeof schema.minItems === "number" && value.length < schema.minItems) {
2294
- errors.push({ path: path4, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
2294
+ errors.push({ path: path5, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
2295
2295
  }
2296
2296
  if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
2297
- errors.push({ path: path4, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
2297
+ errors.push({ path: path5, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
2298
2298
  }
2299
2299
  if (schema.uniqueItems === true) {
2300
2300
  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." });
2301
+ if (unique2.size !== value.length) errors.push({ path: path5, code: "unique_items", message: "Expected every item to be unique." });
2302
2302
  }
2303
2303
  if (isRecord(schema.items)) {
2304
- value.forEach((item, index) => validateNode(schema.items, item, `${path4}[${index}]`, root, errors, depth + 1));
2304
+ value.forEach((item, index) => validateNode(schema.items, item, `${path5}[${index}]`, root, errors, depth + 1));
2305
2305
  }
2306
2306
  }
2307
- function validateString(schema, value, path4, errors) {
2307
+ function validateString(schema, value, path5, errors) {
2308
2308
  if (typeof schema.minLength === "number" && value.length < schema.minLength) {
2309
- errors.push({ path: path4, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
2309
+ errors.push({ path: path5, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
2310
2310
  }
2311
2311
  if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
2312
- errors.push({ path: path4, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
2312
+ errors.push({ path: path5, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
2313
2313
  }
2314
2314
  if (typeof schema.pattern === "string") {
2315
2315
  try {
2316
- if (!new RegExp(schema.pattern).test(value)) errors.push({ path: path4, code: "pattern", message: "Value does not match the required pattern." });
2316
+ if (!new RegExp(schema.pattern).test(value)) errors.push({ path: path5, code: "pattern", message: "Value does not match the required pattern." });
2317
2317
  } catch {
2318
- errors.push({ path: path4, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
2318
+ errors.push({ path: path5, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
2319
2319
  }
2320
2320
  }
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." });
2321
+ if (schema.format === "uri" && !isUrl(value)) errors.push({ path: path5, code: "format_uri", message: "Expected an absolute URL." });
2322
+ if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path: path5, code: "format_email", message: "Expected an email address." });
2323
+ if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path: path5, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
2324
2324
  }
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}.` });
2325
+ function validateNumber(schema, value, path5, errors) {
2326
+ if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path: path5, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
2327
+ if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path: path5, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
2328
2328
  const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum === true && typeof schema.minimum === "number" ? schema.minimum : null;
2329
2329
  const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum === true && typeof schema.maximum === "number" ? schema.maximum : null;
2330
2330
  if (exclusiveMinimum !== null && value <= exclusiveMinimum) {
2331
- errors.push({ path: path4, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
2331
+ errors.push({ path: path5, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
2332
2332
  }
2333
2333
  if (exclusiveMaximum !== null && value >= exclusiveMaximum) {
2334
- errors.push({ path: path4, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
2334
+ errors.push({ path: path5, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
2335
2335
  }
2336
2336
  if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
2337
- errors.push({ path: path4, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
2337
+ errors.push({ path: path5, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
2338
2338
  }
2339
2339
  }
2340
2340
  function candidateMatches(candidate, value, root, depth) {
@@ -2845,14 +2845,14 @@ var TopoloClient = class _TopoloClient {
2845
2845
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2846
2846
  throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
2847
2847
  }
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) {
2848
+ const record2 = raw;
2849
+ 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
2850
  throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
2851
2851
  }
2852
2852
  return {
2853
2853
  algorithm: "sha256-v1",
2854
- digest: record.digest,
2855
- actionCount: Number(record.actionCount)
2854
+ digest: record2.digest,
2855
+ actionCount: Number(record2.actionCount)
2856
2856
  };
2857
2857
  }
2858
2858
  /** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
@@ -2889,22 +2889,22 @@ var TopoloClient = class _TopoloClient {
2889
2889
  if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
2890
2890
  return match;
2891
2891
  }
2892
- async callAction(action, input2 = {}, options = {}) {
2892
+ async callAction(action, input3 = {}, options = {}) {
2893
2893
  const entry = await this.getAction(action);
2894
- return this.executeResolvedAction(entry, input2, options);
2894
+ return this.executeResolvedAction(entry, input3, options);
2895
2895
  }
2896
- async validateActionInput(action, input2 = {}) {
2896
+ async validateActionInput(action, input3 = {}) {
2897
2897
  const entry = await this.getAction(action);
2898
- return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input2) };
2898
+ return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input3) };
2899
2899
  }
2900
- async planAction(action, input2 = {}, options = {}) {
2900
+ async planAction(action, input3 = {}, options = {}) {
2901
2901
  const entry = await this.getAction(action);
2902
- const validation = validateTopoloActionInput(entry.inputSchema, input2);
2902
+ const validation = validateTopoloActionInput(entry.inputSchema, input3);
2903
2903
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2904
2904
  const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
2905
2905
  let request = null;
2906
2906
  if (validation.valid) {
2907
- const mapped = mapActionInput(entry, input2);
2907
+ const mapped = mapActionInput(entry, input3);
2908
2908
  request = {
2909
2909
  method: entry.method,
2910
2910
  service: entry.appSlug || entry.appId,
@@ -2927,7 +2927,7 @@ var TopoloClient = class _TopoloClient {
2927
2927
  nextActionIds: stringArray(entry.agentContract?.nextActionIds)
2928
2928
  };
2929
2929
  }
2930
- async readAction(action, input2 = {}, options = {}) {
2930
+ async readAction(action, input3 = {}, options = {}) {
2931
2931
  const entry = await this.getAction(action);
2932
2932
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2933
2933
  if (!entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
@@ -2936,9 +2936,9 @@ var TopoloClient = class _TopoloClient {
2936
2936
  entry.requiredPermission ? [entry.requiredPermission] : []
2937
2937
  );
2938
2938
  }
2939
- return this.executeResolvedAction(entry, input2, options);
2939
+ return this.executeResolvedAction(entry, input3, options);
2940
2940
  }
2941
- async executeResolvedAction(entry, input2, options) {
2941
+ async executeResolvedAction(entry, input3, options) {
2942
2942
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2943
2943
  if (agentAccess === "off") {
2944
2944
  throw new TopoloPermissionError(
@@ -2951,14 +2951,14 @@ var TopoloClient = class _TopoloClient {
2951
2951
  `Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
2952
2952
  );
2953
2953
  }
2954
- const validation = validateTopoloActionInput(entry.inputSchema, input2);
2954
+ const validation = validateTopoloActionInput(entry.inputSchema, input3);
2955
2955
  if (!validation.valid) {
2956
2956
  throw new TopoloSdkError(
2957
2957
  "invalid_action_input",
2958
2958
  `Topolo action "${entry.actionId}" input is invalid: ${validation.errors.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`
2959
2959
  );
2960
2960
  }
2961
- const mapped = mapActionInput(entry, input2);
2961
+ const mapped = mapActionInput(entry, input3);
2962
2962
  const service = entry.appSlug || entry.appId;
2963
2963
  return this.request({
2964
2964
  service,
@@ -3050,15 +3050,15 @@ function resolveConfiguredAppId(slug, appIds) {
3050
3050
  }
3051
3051
  return null;
3052
3052
  }
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);
3053
+ function normalizeApiKeyResourceMetadata(input3) {
3054
+ if (!isRecord2(input3)) return null;
3055
+ const resourceType = readString(input3.resourceType ?? input3.resource_type);
3056
+ const label = readString(input3.label);
3057
+ const emptyLabel = readString(input3.emptyLabel ?? input3.empty_label);
3058
+ const resourceConcept = readString(input3.resourceConcept ?? input3.resource_concept);
3059
+ const canonicalResourceType = readString(input3.canonicalResourceType ?? input3.canonical_resource_type);
3060
+ const defaultEntitlement = readString(input3.defaultEntitlement ?? input3.default_entitlement);
3061
+ const exceptionPolicy = readString(input3.exceptionPolicy ?? input3.exception_policy);
3062
3062
  if (!resourceType || !label || !emptyLabel || resourceConcept !== "workspace" || canonicalResourceType !== "workspace" || defaultEntitlement !== "enterprise" || exceptionPolicy !== "app_or_organization") {
3063
3063
  return null;
3064
3064
  }
@@ -3072,11 +3072,11 @@ function normalizeApiKeyResourceMetadata(input2) {
3072
3072
  exceptionPolicy
3073
3073
  };
3074
3074
  }
3075
- function normalizeApiKeyResourceMetadataList(input2) {
3076
- if (!Array.isArray(input2)) return [];
3075
+ function normalizeApiKeyResourceMetadataList(input3) {
3076
+ if (!Array.isArray(input3)) return [];
3077
3077
  const resources = [];
3078
3078
  const seenTypes = /* @__PURE__ */ new Set();
3079
- for (const item of input2) {
3079
+ for (const item of input3) {
3080
3080
  const resource = normalizeApiKeyResourceMetadata(item);
3081
3081
  if (!resource || seenTypes.has(resource.resourceType)) continue;
3082
3082
  seenTypes.add(resource.resourceType);
@@ -3084,24 +3084,24 @@ function normalizeApiKeyResourceMetadataList(input2) {
3084
3084
  }
3085
3085
  return resources;
3086
3086
  }
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);
3087
+ function normalizeBindableResource(input3) {
3088
+ if (!isRecord2(input3)) return null;
3089
+ const resourceType = readString(input3.resourceType ?? input3.resource_type);
3090
+ const resourceId = readString(input3.resourceId ?? input3.resource_id);
3091
3091
  if (!resourceType || !resourceId) return null;
3092
3092
  return {
3093
3093
  resourceType,
3094
3094
  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 : {}
3095
+ label: readOptionalString(input3.label),
3096
+ slug: readOptionalString(input3.slug),
3097
+ status: readOptionalString(input3.status),
3098
+ organizationId: readOptionalString(input3.organizationId ?? input3.organization_id),
3099
+ metadata: isRecord2(input3.metadata) ? input3.metadata : {}
3100
3100
  };
3101
3101
  }
3102
- function normalizeResourceBindings(input2) {
3103
- if (!Array.isArray(input2)) return [];
3104
- return input2.flatMap((binding) => {
3102
+ function normalizeResourceBindings(input3) {
3103
+ if (!Array.isArray(input3)) return [];
3104
+ return input3.flatMap((binding) => {
3105
3105
  if (!isRecord2(binding)) return [];
3106
3106
  const resourceType = readString(binding.resourceType ?? binding.resource_type);
3107
3107
  const resourceId = readString(binding.resourceId ?? binding.resource_id);
@@ -3227,8 +3227,8 @@ function normalizeActionCatalogEntry(item) {
3227
3227
  const name = pickActionString(a.name);
3228
3228
  const appId = pickActionString(a.app_id) ?? pickActionString(a.appId);
3229
3229
  const method = pickActionString(a.method)?.toUpperCase();
3230
- const path4 = pickActionString(a.path);
3231
- if (!actionId || !name || !appId || !method || !path4 || !isHttpMethod(method)) {
3230
+ const path5 = pickActionString(a.path);
3231
+ if (!actionId || !name || !appId || !method || !path5 || !isHttpMethod(method)) {
3232
3232
  return null;
3233
3233
  }
3234
3234
  return {
@@ -3243,7 +3243,7 @@ function normalizeActionCatalogEntry(item) {
3243
3243
  appSlug: pickActionString(a.app_slug) ?? pickActionString(a.appSlug) ?? null,
3244
3244
  appName: pickActionString(a.app_name) ?? pickActionString(a.appName) ?? null,
3245
3245
  method,
3246
- path: path4,
3246
+ path: path5,
3247
3247
  permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
3248
3248
  requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
3249
3249
  authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
@@ -3277,9 +3277,9 @@ function plainObject(value) {
3277
3277
  function stringArray(value) {
3278
3278
  return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
3279
3279
  }
3280
- function mapActionInput(action, input2) {
3281
- const remaining = { ...input2 || {} };
3282
- const path4 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
3280
+ function mapActionInput(action, input3) {
3281
+ const remaining = { ...input3 || {} };
3282
+ const path5 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
3283
3283
  const value = remaining[key];
3284
3284
  if (value === void 0 || value === null || value === "") {
3285
3285
  throw new TopoloSdkError(
@@ -3296,14 +3296,14 @@ function mapActionInput(action, input2) {
3296
3296
  delete remaining.body;
3297
3297
  if (action.method === "GET") {
3298
3298
  return {
3299
- path: path4,
3299
+ path: path5,
3300
3300
  query: objectToQuery({ ...remaining, ...explicitQuery })
3301
3301
  };
3302
3302
  }
3303
3303
  const query = objectToQuery(explicitQuery);
3304
3304
  const body = explicitBody !== void 0 ? explicitBody : remaining;
3305
3305
  return {
3306
- path: path4,
3306
+ path: path5,
3307
3307
  ...query ? { query } : {},
3308
3308
  body
3309
3309
  };
@@ -3473,8 +3473,8 @@ var TopoloOAuth = class {
3473
3473
  if (params.clientSecret) body.set("client_secret", params.clientSecret);
3474
3474
  return this.#postForm("/api/developer-oauth/token", body);
3475
3475
  }
3476
- async #postForm(path4, body) {
3477
- const url = new URL(path4, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
3476
+ async #postForm(path5, body) {
3477
+ const url = new URL(path5, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
3478
3478
  const res = await this.fetchImpl(url.toString(), {
3479
3479
  method: "POST",
3480
3480
  headers: {
@@ -3490,7 +3490,7 @@ var TopoloOAuth = class {
3490
3490
  const err = parsed;
3491
3491
  throw new TopoloAuthError(err.error_description || err.error, err.error);
3492
3492
  }
3493
- throw new TopoloHttpError("auth", path4, res.status, parsed);
3493
+ throw new TopoloHttpError("auth", path5, res.status, parsed);
3494
3494
  }
3495
3495
  return parsed;
3496
3496
  }
@@ -3504,7 +3504,7 @@ function createTopolo(options) {
3504
3504
  };
3505
3505
  }
3506
3506
 
3507
- // node_modules/@topolo/sdk/dist/node/credential-store.js
3507
+ // ../topolo-sdk/dist/node/credential-store.js
3508
3508
  import { mkdir, readFile, writeFile, chmod } from "fs/promises";
3509
3509
  import { existsSync } from "fs";
3510
3510
  import { homedir, platform } from "os";
@@ -3528,13 +3528,13 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
3528
3528
  body;
3529
3529
  service;
3530
3530
  path;
3531
- constructor(service, path4, status, body, message) {
3532
- super("http_error", message ?? `HTTP ${status} from ${service}${path4}`);
3531
+ constructor(service, path5, status, body, message) {
3532
+ super("http_error", message ?? `HTTP ${status} from ${service}${path5}`);
3533
3533
  this.name = "TopoloHttpError";
3534
3534
  this.status = status;
3535
3535
  this.body = body;
3536
3536
  this.service = service;
3537
- this.path = path4;
3537
+ this.path = path5;
3538
3538
  }
3539
3539
  };
3540
3540
  var APP_API_URLS2 = {
@@ -3665,8 +3665,8 @@ var TopoloOAuth2 = class {
3665
3665
  if (params.clientSecret) body.set("client_secret", params.clientSecret);
3666
3666
  return this.#postForm("/api/developer-oauth/token", body);
3667
3667
  }
3668
- async #postForm(path4, body) {
3669
- const url = new URL(path4, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
3668
+ async #postForm(path5, body) {
3669
+ const url = new URL(path5, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
3670
3670
  const res = await this.fetchImpl(url.toString(), {
3671
3671
  method: "POST",
3672
3672
  headers: {
@@ -3682,7 +3682,7 @@ var TopoloOAuth2 = class {
3682
3682
  const err = parsed;
3683
3683
  throw new TopoloAuthError2(err.error_description || err.error, err.error);
3684
3684
  }
3685
- throw new TopoloHttpError2("auth", path4, res.status, parsed);
3685
+ throw new TopoloHttpError2("auth", path5, res.status, parsed);
3686
3686
  }
3687
3687
  return parsed;
3688
3688
  }
@@ -3712,22 +3712,22 @@ function configPath() {
3712
3712
  return join(base, "topolo", "config.json");
3713
3713
  }
3714
3714
  async function readConfig() {
3715
- const path4 = configPath();
3716
- if (!existsSync(path4)) return {};
3715
+ const path5 = configPath();
3716
+ if (!existsSync(path5)) return {};
3717
3717
  try {
3718
- const raw = await readFile(path4, "utf8");
3718
+ const raw = await readFile(path5, "utf8");
3719
3719
  return JSON.parse(raw);
3720
3720
  } catch {
3721
3721
  return {};
3722
3722
  }
3723
3723
  }
3724
3724
  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");
3725
+ const path5 = configPath();
3726
+ await mkdir(dirname(path5), { recursive: true });
3727
+ await writeFile(path5, JSON.stringify(config, null, 2) + "\n", "utf8");
3728
3728
  if (platform() !== "win32") {
3729
3729
  try {
3730
- await chmod(path4, 384);
3730
+ await chmod(path5, 384);
3731
3731
  } catch {
3732
3732
  }
3733
3733
  }
@@ -4023,13 +4023,14 @@ function normalizeError(err) {
4023
4023
  const hintMap = {
4024
4024
  auth_error: "Run `topolo auth login` or set TOPOLO_API_KEY.",
4025
4025
  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."
4026
+ permission_denied: "Check your API key scopes or role assignments in TopoloDevelopers.",
4027
+ immutable_drift: "Restore the manifest identity fields to the registered values; immutable ids cannot be updated."
4027
4028
  };
4028
4029
  return {
4029
4030
  message: err.message,
4030
4031
  code,
4031
4032
  hint: code ? hintMap[code] : void 0,
4032
- exitCode: code === "auth_error" || code === "confirmation_required" ? 2 : code === "permission_denied" ? 3 : 1
4033
+ exitCode: code === "auth_error" || code === "confirmation_required" ? 2 : code === "permission_denied" ? 3 : code === "immutable_drift" ? 4 : 1
4033
4034
  };
4034
4035
  }
4035
4036
  return { message: String(err), exitCode: 1 };
@@ -4046,9 +4047,9 @@ function readCliVersion() {
4046
4047
  join2(here, "package.json"),
4047
4048
  join2(here, "..", "package.json")
4048
4049
  ];
4049
- for (const path4 of candidates) {
4050
+ for (const path5 of candidates) {
4050
4051
  try {
4051
- const pkg = JSON.parse(readFileSync(path4, "utf8"));
4052
+ const pkg = JSON.parse(readFileSync(path5, "utf8"));
4052
4053
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
4053
4054
  } catch {
4054
4055
  }
@@ -4407,7 +4408,7 @@ function registerApiCommand(program2) {
4407
4408
  "-q, --query <kv...>",
4408
4409
  "Query params as key=value (repeatable), e.g. --query source=cli page=2"
4409
4410
  ).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) => {
4411
+ async (service, method, path5, opts) => {
4411
4412
  const mode = resolveOutputMode(opts.json);
4412
4413
  try {
4413
4414
  const upperMethod = method.toUpperCase();
@@ -4446,7 +4447,7 @@ function registerApiCommand(program2) {
4446
4447
  const result = await topolo.client.request({
4447
4448
  service,
4448
4449
  method: httpMethod,
4449
- path: path4,
4450
+ path: path5,
4450
4451
  body,
4451
4452
  ...query ? { query } : {},
4452
4453
  ...resource ? { resource } : {},
@@ -4539,9 +4540,9 @@ function registerActionsCommand(program2) {
4539
4540
  if (!topolo) {
4540
4541
  throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
4541
4542
  }
4542
- const input2 = parseJsonObject(opts.data || "{}");
4543
+ const input3 = parseJsonObject(opts.data || "{}");
4543
4544
  const resource = parseResourceOption(opts.resourceType, opts.resourceId);
4544
- const result = await topolo.client.callAction(action, input2, {
4545
+ const result = await topolo.client.callAction(action, input3, {
4545
4546
  confirm: opts.confirm === true,
4546
4547
  ...resource ? { resource } : {}
4547
4548
  });
@@ -4610,13 +4611,13 @@ async function requiredClient() {
4610
4611
  function parseJsonObject(value) {
4611
4612
  let source = value;
4612
4613
  if (value.startsWith("@")) {
4613
- const path4 = value.slice(1);
4614
- if (!path4) throw new Error("--data @file path must not be empty");
4614
+ const path5 = value.slice(1);
4615
+ if (!path5) throw new Error("--data @file path must not be empty");
4615
4616
  try {
4616
- source = readFileSync2(path4, "utf8");
4617
+ source = readFileSync2(path5, "utf8");
4617
4618
  } catch (err) {
4618
4619
  const detail = err instanceof Error ? err.message : String(err);
4619
- throw new Error(`Could not read --data file ${path4}: ${detail}`);
4620
+ throw new Error(`Could not read --data file ${path5}: ${detail}`);
4620
4621
  }
4621
4622
  }
4622
4623
  let parsed;
@@ -4691,7 +4692,7 @@ function formatActionPlan(plan) {
4691
4692
  // src/commands/apps.ts
4692
4693
  import { execSync } from "child_process";
4693
4694
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
4694
- import path2 from "path";
4695
+ import path3 from "path";
4695
4696
 
4696
4697
  // src/commands/app-audit-local.ts
4697
4698
  import {
@@ -5282,8 +5283,8 @@ function enrichFinding(application, finding2, evidence) {
5282
5283
  evidence: verificationEvidence
5283
5284
  });
5284
5285
  }
5285
- const usesNodeCheck = evidence.packageJsons.some((record) => {
5286
- const scripts = record.packageJson.scripts ?? {};
5286
+ const usesNodeCheck = evidence.packageJsons.some((record2) => {
5287
+ const scripts = record2.packageJson.scripts ?? {};
5287
5288
  return Object.values(scripts).some((cmd) => typeof cmd === "string" && /node\s+--check/.test(cmd));
5288
5289
  });
5289
5290
  if (hasBuild && usesNodeCheck) {
@@ -5549,35 +5550,35 @@ function verificationEvidenceFor(evidence) {
5549
5550
  "test:auth-contract": "test",
5550
5551
  validate: "validate"
5551
5552
  };
5552
- for (const record of evidence.packageJsons) {
5553
- const scripts = record.packageJson.scripts ?? {};
5554
- const isRootPackage = record.path === evidence.packageJsonPath;
5553
+ for (const record2 of evidence.packageJsons) {
5554
+ const scripts = record2.packageJson.scripts ?? {};
5555
+ const isRootPackage = record2.path === evidence.packageJsonPath;
5555
5556
  const seenCanonical = /* @__PURE__ */ new Set();
5556
5557
  for (const [scriptName, canonical] of Object.entries(scriptAliases)) {
5557
5558
  if (!scripts[scriptName]) continue;
5558
5559
  if (seenCanonical.has(canonical)) continue;
5559
5560
  seenCanonical.add(canonical);
5560
- items.push(isRootPackage ? `script=${canonical}` : `script=${packageLabel(record)}:${canonical}`);
5561
+ items.push(isRootPackage ? `script=${canonical}` : `script=${packageLabel(record2)}:${canonical}`);
5561
5562
  }
5562
5563
  }
5563
5564
  return unique(items);
5564
5565
  }
5565
5566
  function dependencyNames(evidence) {
5566
5567
  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 ?? {})
5568
+ evidence.packageJsons.flatMap((record2) => [
5569
+ ...Object.keys(record2.packageJson.dependencies ?? {}),
5570
+ ...Object.keys(record2.packageJson.devDependencies ?? {}),
5571
+ ...Object.keys(record2.packageJson.peerDependencies ?? {})
5571
5572
  ])
5572
5573
  );
5573
5574
  }
5574
5575
  function packageDependencyVersions(evidence, packageName) {
5575
5576
  return unique(
5576
- evidence.packageJsons.flatMap((record) => {
5577
+ evidence.packageJsons.flatMap((record2) => {
5577
5578
  const specs = [
5578
- record.packageJson.dependencies?.[packageName],
5579
- record.packageJson.devDependencies?.[packageName],
5580
- record.packageJson.peerDependencies?.[packageName]
5579
+ record2.packageJson.dependencies?.[packageName],
5580
+ record2.packageJson.devDependencies?.[packageName],
5581
+ record2.packageJson.peerDependencies?.[packageName]
5581
5582
  ];
5582
5583
  return specs.filter((spec) => typeof spec === "string");
5583
5584
  })
@@ -5703,8 +5704,8 @@ function collectPackageJsons(root) {
5703
5704
  function readPackageJson(filePath) {
5704
5705
  return readJson(filePath);
5705
5706
  }
5706
- function packageLabel(record) {
5707
- return record.packageJson.name ?? relativeTopoloPath(path.dirname(record.path));
5707
+ function packageLabel(record2) {
5708
+ return record2.packageJson.name ?? relativeTopoloPath(path.dirname(record2.path));
5708
5709
  }
5709
5710
  function sortedDirEntries(dir) {
5710
5711
  return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
@@ -5793,14 +5794,14 @@ import { constants } from "fs";
5793
5794
  import { access } from "fs/promises";
5794
5795
  import { relative, resolve } from "path";
5795
5796
  function normalizeHomePath(value) {
5796
- const path4 = String(value || "/dashboard").trim();
5797
- if (!path4.startsWith("/") || path4.startsWith("//") || /[?#]/.test(path4)) {
5797
+ const path5 = String(value || "/dashboard").trim();
5798
+ if (!path5.startsWith("/") || path5.startsWith("//") || /[?#]/.test(path5)) {
5798
5799
  throw new Error("Invalid home path. Use an absolute application path such as /dashboard.");
5799
5800
  }
5800
- if (["/", "/login", "/auth/callback", "/getting-started"].includes(path4)) {
5801
+ if (["/", "/login", "/auth/callback", "/getting-started"].includes(path5)) {
5801
5802
  throw new Error("Home path must be a protected product route, not a reserved entry route.");
5802
5803
  }
5803
- return path4.replace(/\/$/, "") || "/dashboard";
5804
+ return path5.replace(/\/$/, "") || "/dashboard";
5804
5805
  }
5805
5806
  function normalizeHomeLabel(value) {
5806
5807
  const label = String(value || "Dashboard").trim();
@@ -5819,6 +5820,13 @@ function normalizeWorkspaceSwitcherMode(value) {
5819
5820
  if (mode === "required" || mode === "optional" || mode === "off") return mode;
5820
5821
  throw new Error("Invalid workspace switcher mode. Use required, optional, or off.");
5821
5822
  }
5823
+ function normalizeUiTemplate(value) {
5824
+ const template = String(value || "operations").trim();
5825
+ if (template === "operations" || template === "index" || template === "workbench" || template === "minimal") {
5826
+ return template;
5827
+ }
5828
+ throw new Error("Invalid UI template. Use operations, index, workbench, or minimal.");
5829
+ }
5822
5830
  function normalizeFirstPartyAppDir(value) {
5823
5831
  const requested = String(value).trim().replace(/\\/g, "/").replace(/^\.\//, "");
5824
5832
  const appDir = requested.startsWith("apps/") ? requested.slice("apps/".length) : requested;
@@ -5927,14 +5935,16 @@ function toPascalCase(value) {
5927
5935
  return value.split(/[-_]/).filter(Boolean).map((part) => acronyms.has(part.toLowerCase()) ? part.toUpperCase() : part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
5928
5936
  }
5929
5937
  function toTitle(value) {
5930
- return value.replace(/^topolo[-_\s]*/i, "Topolo ").split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
5938
+ const withoutVendorPrefix = value.replace(/^topolo[-_\s]+/i, "");
5939
+ const source = withoutVendorPrefix.trim() ? withoutVendorPrefix : value;
5940
+ return source.split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
5931
5941
  }
5932
5942
  function json(value) {
5933
5943
  return JSON.stringify(value, null, 2);
5934
5944
  }
5935
- async function pathExists(path4) {
5945
+ async function pathExists(path5) {
5936
5946
  try {
5937
- await access(path4, constants.F_OK);
5947
+ await access(path5, constants.F_OK);
5938
5948
  return true;
5939
5949
  } catch {
5940
5950
  return false;
@@ -6033,6 +6043,9 @@ function packageJson(application, target, options) {
6033
6043
  devDependencies["@types/react-dom"] = "^19.2.2";
6034
6044
  devDependencies["@vitejs/plugin-react"] = "^4.7.0";
6035
6045
  if (target === "platform") {
6046
+ devDependencies.autoprefixer = "10.5.0";
6047
+ devDependencies.postcss = "8.5.6";
6048
+ devDependencies.tailwindcss = "3.4.19";
6036
6049
  for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) {
6037
6050
  dependencies[packageName] = topoloPackageVersion(packageName, target);
6038
6051
  }
@@ -6196,13 +6209,16 @@ function readme(application, target, options) {
6196
6209
  "- `@topolo-io/app-funnel` provides the generated public application funnel/landing surface.",
6197
6210
  "- `@topolo-io/app-shell` provides the generated route/auth bootstrap, product shell, login, callback, and loading surfaces.",
6198
6211
  "- `@topolo-io/app-launcher` provides shared app switching and handoff helpers.",
6199
- "- `@topolo-io/ui-kit` provides primitive account, command palette, icon, transition, and bug reporter UI.",
6212
+ "- `@topolo-io/ui-kit` provides every control, table, panel, workbench, inspector, icon, transition, and bug reporter primitive used by the generated product surface.",
6200
6213
  "- `@topolo-io/auth-client` owns browser auth, callback exchange, token refresh, and logout.",
6201
6214
  "- `@topolo-io/api-key-management`, `@topolo-io/chat-widget`, `@topolo-io/notifications`, and `@topolo-io/resource-context` complete the AppShell capability baseline.",
6202
6215
  "- Every Topolo-owned browser dependency is emitted at `latest`; product-domain packages are added only when the app actually adopts that domain.",
6203
6216
  "- `.npmrc` routes private `@topolo-io` packages to GitHub Packages without storing credentials; public `@topolo` packages remain on npm. Local and CI environments must provide a GitHub Packages token.",
6204
6217
  "- Generated first-party browser apps start on the shared landing page at `/`, use the shared login page at `/login`, and complete sign-in on `/auth/callback` before entering the configured authenticated home.",
6205
6218
  "- The workspace selector renders in the top utility bar; Getting Started is an account-menu action and is not part of the left product rail.",
6219
+ "- The default `operations` UI template separates dashboard, CRUD index, detail workbench, and inspector concerns. Use `--ui-template index`, `--ui-template workbench`, or `--ui-template minimal` only when that product shape is intentional.",
6220
+ "- Every visible product navigation item includes a canonical icon so the shared sidebar remains usable when collapsed.",
6221
+ "- `styles.css` contains only Tailwind directives; app-local controls, semantic tables, or replacement design-system CSS fail the generated UI contract test.",
6206
6222
  "- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
6207
6223
  );
6208
6224
  if (options.api) {
@@ -6299,6 +6315,8 @@ function agentRules(application, target, options) {
6299
6315
  "- Start from `staging`, preserve other agents' work, and do not revert unrelated local changes.",
6300
6316
  "- Make contained changes at the real ownership boundary; do not patch around shared runtime, auth, launcher, shell, or resource-context issues locally.",
6301
6317
  "- Browser code should stay on the shared landing/login/callback/AppShell baseline generated by the scaffold.",
6318
+ "- Keep every visible product navigation item on a canonical `@topolo-io/ui-kit/icons` icon so collapsed navigation remains complete.",
6319
+ "- Use shared UI-kit controls, tables, panels, workbenches, and inspectors; the generated UI contract intentionally rejects app-local native controls and tables.",
6302
6320
  ...options.api ? ["- API code should stay on `@topolo-io/worker-runtime`, derive auth context from verified credentials, and preserve Nexus usage monitoring."] : [],
6303
6321
  ...hasScaffoldResources(options.resources) ? ["- Keep Cloudflare resource bindings in Wrangler, CloudControl, Env typing, docs, and README verification output in sync."] : [],
6304
6322
  "",
@@ -6649,7 +6667,36 @@ function mainTsx() {
6649
6667
  ""
6650
6668
  ].join("\n");
6651
6669
  }
6652
- function stylesCss() {
6670
+ function tailwindConfigCjs() {
6671
+ return [
6672
+ "const { createTopoloTailwindConfig } = require('@topolo-io/design-tokens/tailwind.config.cjs');",
6673
+ "",
6674
+ "module.exports = createTopoloTailwindConfig({",
6675
+ " content: [",
6676
+ " './apps/web/index.html',",
6677
+ " './apps/web/src/**/*.{js,jsx,ts,tsx}',",
6678
+ " ],",
6679
+ "});",
6680
+ ""
6681
+ ].join("\n");
6682
+ }
6683
+ function postcssConfigCjs() {
6684
+ return [
6685
+ "const path = require('node:path');",
6686
+ "",
6687
+ "module.exports = {",
6688
+ " plugins: {",
6689
+ " tailwindcss: { config: path.join(__dirname, 'tailwind.config.cjs') },",
6690
+ " autoprefixer: {},",
6691
+ " },",
6692
+ "};",
6693
+ ""
6694
+ ].join("\n");
6695
+ }
6696
+ function stylesCss(target) {
6697
+ if (target === "platform") {
6698
+ return ["@tailwind base;", "@tailwind components;", "@tailwind utilities;", ""].join("\n");
6699
+ }
6653
6700
  return [
6654
6701
  "/* Local app styles. Design tokens come from @topolo-io/design-tokens (imported in main.tsx). */",
6655
6702
  "/* Use hsl(var(--token)) for colors so light/dark themes flip from the shared shell. */",
@@ -6855,20 +6902,41 @@ export const APP_DISPLAY_NAME = '${escapeSingleQuoted(appName)}';
6855
6902
  `;
6856
6903
  }
6857
6904
  function platformAppRoutesTsx(experience) {
6858
- const nav = experience.navigationMode === "none" ? "" : " nav: { id: 'home', label: HOME_LABEL },\n";
6905
+ const nav = experience.navigationMode === "none" ? "" : " nav: { id: 'home', label: HOME_LABEL, icon: LayoutDashboard },\n";
6906
+ const recordsNav = experience.navigationMode === "none" ? "" : " nav: { id: 'records', label: 'Records', icon: ListTree },\n";
6907
+ const starterImports = experience.uiTemplate === "minimal" ? "" : "import RecordDetailRoute from './routes/RecordDetailRoute';\nimport RecordsRoute from './routes/RecordsRoute';\n";
6908
+ const homeElement = experience.uiTemplate === "index" ? "<RecordsRoute />" : experience.uiTemplate === "workbench" ? "<RecordDetailRoute />" : "<HomeRoute />";
6909
+ const starterRoutes = experience.uiTemplate === "minimal" ? "" : experience.uiTemplate === "operations" ? ` {
6910
+ path: '/records',
6911
+ ${recordsNav} loading: { shell: NAVIGATION_MODE, content: 'table', title: 'Records' },
6912
+ element: <RecordsRoute />,
6913
+ },
6914
+ {
6915
+ path: '/records/:recordId/:section',
6916
+ loading: { shell: NAVIGATION_MODE, content: 'split', title: 'Record' },
6917
+ element: <RecordDetailRoute />,
6918
+ },
6919
+ ` : ` {
6920
+ path: '/records/:recordId/:section',
6921
+ loading: { shell: NAVIGATION_MODE, content: 'split', title: 'Record' },
6922
+ element: <RecordDetailRoute />,
6923
+ },
6924
+ `;
6859
6925
  return `import type { AppRoute } from '@topolo-io/app-shell';
6926
+ import { LayoutDashboard, ListTree } from '@topolo-io/ui-kit/icons';
6860
6927
  import { HOME_LABEL, HOME_PATH, NAVIGATION_MODE } from './app-config';
6861
6928
  import GettingStartedRoute from './routes/GettingStartedRoute';
6862
6929
  import HomeRoute from './routes/HomeRoute';
6930
+ ${starterImports}
6863
6931
 
6864
6932
  export function createAppRoutes(appId: string): AppRoute[] {
6865
6933
  return [
6866
6934
  {
6867
6935
  path: HOME_PATH,
6868
6936
  ${nav} loading: { shell: NAVIGATION_MODE, content: 'workspace', title: HOME_LABEL },
6869
- element: <HomeRoute />,
6937
+ element: ${homeElement},
6870
6938
  },
6871
- {
6939
+ ${starterRoutes} {
6872
6940
  path: '/getting-started',
6873
6941
  loading: { shell: NAVIGATION_MODE, content: 'card-grid', title: 'Getting started' },
6874
6942
  element: <GettingStartedRoute appId={appId} />,
@@ -6877,7 +6945,8 @@ ${nav} loading: { shell: NAVIGATION_MODE, content: 'workspace', title: HOME
6877
6945
  }
6878
6946
  `;
6879
6947
  }
6880
- function platformHomeRouteTsx() {
6948
+ function platformHomeRouteTsx(uiTemplate) {
6949
+ if (uiTemplate === "operations") return platformOperationsHomeRouteTsx();
6881
6950
  return `import { TopoloAppPage, TopoloAppPageHeader, useAuth } from '@topolo-io/app-shell';
6882
6951
  import { IntroTour } from '@topolo-io/tour';
6883
6952
  import { useTranslation } from '@topolo-io/ui-kit';
@@ -6913,6 +6982,148 @@ export default function HomeRoute() {
6913
6982
  }
6914
6983
  `;
6915
6984
  }
6985
+ function platformOperationsHomeRouteTsx() {
6986
+ return `import { TopoloAppPage, TopoloAppPageHeader, useAuth } from '@topolo-io/app-shell';
6987
+ import { Button, ListRow, TopoloMetricStrip, TopoloOperationalPanel } from '@topolo-io/ui-kit';
6988
+ import { IntroTour } from '@topolo-io/tour';
6989
+ import { useNavigate } from 'react-router-dom';
6990
+ import { APP_NAME, HOME_LABEL, HOME_PATH, PRODUCT_TAGLINE } from '../app-config';
6991
+ import { STARTER_RECORDS } from './starter-records';
6992
+
6993
+ // STARTER TEMPLATE: replace the sample metrics and activity with product data.
6994
+ export default function HomeRoute() {
6995
+ const { user } = useAuth();
6996
+ const navigate = useNavigate();
6997
+ const active = STARTER_RECORDS.filter((record) => record.status === 'active').length;
6998
+ return (
6999
+ <TopoloAppPage width="none" data-tour="home-page">
7000
+ <TopoloAppPageHeader eyebrow={APP_NAME} title={HOME_LABEL} description={PRODUCT_TAGLINE} actions={<Button onClick={() => navigate('/records')}>Open records</Button>} />
7001
+ <TopoloMetricStrip metrics={[
7002
+ { id: 'total', label: 'Total records', value: STARTER_RECORDS.length },
7003
+ { id: 'active', label: 'Active', value: active, tone: 'success' },
7004
+ { id: 'attention', label: 'Needs attention', value: STARTER_RECORDS.length - active, tone: 'warning' },
7005
+ ]} />
7006
+ <TopoloOperationalPanel title="Recent records" description="Keep operational summaries focused; detailed CRUD belongs in the index and workbench routes." action={<Button variant="secondary" onClick={() => navigate('/records')}>View index</Button>}>
7007
+ <div className="grid gap-1">{STARTER_RECORDS.slice(0, 3).map((record) => <ListRow key={record.id} title={record.name} subtitle={record.owner} meta={record.updatedAt} onSelect={() => navigate(\`/records/\${record.id}/overview\`)} />)}</div>
7008
+ </TopoloOperationalPanel>
7009
+ <IntroTour appName={APP_NAME} role={user?.role} onOpenGettingStarted={() => navigate('/getting-started')} extraSteps={[{
7010
+ id: 'home-page', target: '[data-tour="home-page"]', title: HOME_LABEL, content: PRODUCT_TAGLINE,
7011
+ placement: 'bottom' as const, beforeStep: () => navigate(HOME_PATH),
7012
+ }]} />
7013
+ </TopoloAppPage>
7014
+ );
7015
+ }
7016
+ `;
7017
+ }
7018
+ function platformStarterDataTs() {
7019
+ return `// STARTER TEMPLATE: replace this file with the app's real API-backed domain model.
7020
+ export type StarterRecordStatus = 'active' | 'draft' | 'paused';
7021
+
7022
+ export interface StarterRecord {
7023
+ id: string;
7024
+ name: string;
7025
+ status: StarterRecordStatus;
7026
+ owner: string;
7027
+ summary: string;
7028
+ updatedAt: string;
7029
+ }
7030
+
7031
+ export const STARTER_RECORDS: StarterRecord[] = [
7032
+ { id: 'starter-record-1', name: 'First starter record', status: 'active', owner: 'Product team', summary: 'Replace this sample with one app-specific concept.', updatedAt: 'Today' },
7033
+ { id: 'starter-record-2', name: 'Second starter record', status: 'draft', owner: 'Operations', summary: 'Use the index for discovery and the workbench for focused editing.', updatedAt: 'Yesterday' },
7034
+ { id: 'starter-record-3', name: 'Third starter record', status: 'paused', owner: 'Workspace admin', summary: 'Keep unrelated workflows on separate routes.', updatedAt: '3 days ago' },
7035
+ ];
7036
+ `;
7037
+ }
7038
+ function platformRecordsRouteTsx() {
7039
+ return `import { useMemo, useState, type FormEvent } from 'react';
7040
+ import { TopoloAppDataTable, TopoloAppEmptyState, TopoloAppFilterBar, TopoloAppPage, TopoloAppPageHeader, TopoloAppToolbar } from '@topolo-io/app-shell';
7041
+ import { Badge, Button, FormField, Input, SearchInput, Select, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TopoloRightPanel } from '@topolo-io/ui-kit';
7042
+ import { Plus } from '@topolo-io/ui-kit/icons';
7043
+ import { useNavigate } from 'react-router-dom';
7044
+ import { STARTER_RECORDS, type StarterRecord, type StarterRecordStatus } from './starter-records';
7045
+
7046
+ // STARTER TEMPLATE: keep the index focused on discovery, filtering, and create entry.
7047
+ export default function RecordsRoute() {
7048
+ const navigate = useNavigate();
7049
+ const [records, setRecords] = useState<StarterRecord[]>(STARTER_RECORDS);
7050
+ const [query, setQuery] = useState('');
7051
+ const [status, setStatus] = useState('current');
7052
+ const [createOpen, setCreateOpen] = useState(false);
7053
+ const visible = useMemo(() => records.filter((record) => {
7054
+ if (status !== 'current' && status !== 'all' && record.status !== status) return false;
7055
+ return !query || record.name.toLowerCase().includes(query.toLowerCase());
7056
+ }), [query, records, status]);
7057
+
7058
+ function createRecord(event: FormEvent<HTMLFormElement>) {
7059
+ event.preventDefault();
7060
+ const data = new FormData(event.currentTarget);
7061
+ const name = String(data.get('name') || '').trim();
7062
+ if (!name) return;
7063
+ const record: StarterRecord = {
7064
+ id: \`starter-record-\${Date.now()}\`, name, status: 'draft', owner: String(data.get('owner') || 'Workspace team'),
7065
+ summary: String(data.get('summary') || 'Replace this starter form with domain fields.'), updatedAt: 'Just now',
7066
+ };
7067
+ setRecords((current) => [record, ...current]);
7068
+ setCreateOpen(false);
7069
+ }
7070
+
7071
+ return (
7072
+ <TopoloAppPage width="none">
7073
+ <TopoloAppPageHeader eyebrow="Starter CRUD" title="Records" description="A canonical index route with shared controls, shared tables, and a separate creation panel." actions={<Button leftIcon={<Plus size={16} />} onClick={() => setCreateOpen(true)}>New record</Button>} />
7074
+ <TopoloAppToolbar><TopoloAppFilterBar>
7075
+ <SearchInput className="w-64" value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search records" aria-label="Search records" />
7076
+ <Select className="w-44" value={status} onChange={(event) => setStatus(event.target.value)} aria-label="Record status"><option value="current">Current</option><option value="active">Active</option><option value="draft">Draft</option><option value="paused">Paused</option><option value="all">All</option></Select>
7077
+ </TopoloAppFilterBar><span className="text-sm text-muted-foreground">{visible.length} result{visible.length === 1 ? '' : 's'}</span></TopoloAppToolbar>
7078
+ {visible.length ? <TopoloAppDataTable aria-label="Records"><Table>
7079
+ <TableHeader><TableRow><TableHead>Name</TableHead><TableHead>Status</TableHead><TableHead>Owner</TableHead><TableHead>Updated</TableHead></TableRow></TableHeader>
7080
+ <TableBody>{visible.map((record) => <TableRow className="cursor-pointer" key={record.id} tabIndex={0} onClick={() => navigate(\`/records/\${record.id}/overview\`)} onKeyDown={(event) => { if (event.key === 'Enter') navigate(\`/records/\${record.id}/overview\`); }}>
7081
+ <TableCell><span className="grid gap-0.5"><strong>{record.name}</strong><small className="text-xs text-muted-foreground">{record.id}</small></span></TableCell>
7082
+ <TableCell><RecordStatus status={record.status} /></TableCell><TableCell>{record.owner}</TableCell><TableCell>{record.updatedAt}</TableCell>
7083
+ </TableRow>)}</TableBody>
7084
+ </Table></TopoloAppDataTable> : <TopoloAppEmptyState title="No records match" description="Adjust the filters or create a record." action={<Button onClick={() => setCreateOpen(true)}>Create record</Button>} />}
7085
+ <TopoloRightPanel open={createOpen} onOpenChange={setCreateOpen} title="Create record" description="Replace these starter fields with the smallest valid domain create flow." footer={<Button type="submit" form="starter-create-record">Create record</Button>}>
7086
+ <form id="starter-create-record" className="grid gap-5" onSubmit={createRecord}><FormField label="Name"><Input name="name" required autoFocus /></FormField><FormField label="Owner"><Input name="owner" placeholder="Workspace team" /></FormField><FormField label="Summary"><Input name="summary" placeholder="What this record represents" /></FormField></form>
7087
+ </TopoloRightPanel>
7088
+ </TopoloAppPage>
7089
+ );
7090
+ }
7091
+
7092
+ export function RecordStatus({ status }: { status: StarterRecordStatus }) {
7093
+ const tone = status === 'active' ? 'success' : status === 'paused' ? 'warning' : 'info';
7094
+ return <Badge tone={tone}>{status}</Badge>;
7095
+ }
7096
+ `;
7097
+ }
7098
+ function platformRecordDetailRouteTsx() {
7099
+ return `import { TopoloAppEmptyState } from '@topolo-io/app-shell';
7100
+ import { Button, CopyField, Tabs, TabsList, TabsTrigger, TopoloCommandBar, TopoloInspectorPanel, TopoloKeyValueRow, TopoloOperationalPanel, TopoloWorkbench } from '@topolo-io/ui-kit';
7101
+ import { useNavigate, useParams } from 'react-router-dom';
7102
+ import { RecordStatus } from './RecordsRoute';
7103
+ import { STARTER_RECORDS } from './starter-records';
7104
+
7105
+ const sections = ['overview', 'activity', 'settings'] as const;
7106
+
7107
+ // STARTER TEMPLATE: one selected record, one focused workflow, one inspector. The product sidebar remains the only left rail.
7108
+ export default function RecordDetailRoute() {
7109
+ const navigate = useNavigate();
7110
+ const { recordId, section: rawSection } = useParams();
7111
+ const record = STARTER_RECORDS.find((item) => item.id === recordId) ?? STARTER_RECORDS[0];
7112
+ const section = sections.includes(rawSection as (typeof sections)[number]) ? rawSection! : 'overview';
7113
+ if (!record) return <TopoloAppEmptyState title="Record not found" description="Return to the index and choose another record." action={<Button onClick={() => navigate('/records')}>Back to records</Button>} />;
7114
+ return (
7115
+ <div className="-m-6 min-w-0 max-md:-m-4"><TopoloWorkbench layout="split" className="min-h-[calc(100dvh-4rem)] gap-0 overflow-hidden bg-background">
7116
+ <main className="min-w-0 overflow-y-auto p-6 max-md:p-4">
7117
+ <TopoloCommandBar eyebrow="Starter record" title={record.name} description={record.summary} meta={<RecordStatus status={record.status} />} actions={<Button>Edit record</Button>} />
7118
+ <Tabs className="my-5" value={section} onValueChange={(next) => navigate(\`/records/\${record.id}/\${next}\`)}><TabsList>{sections.map((item) => <TabsTrigger key={item} value={item}>{item}</TabsTrigger>)}</TabsList></Tabs>
7119
+ <TopoloOperationalPanel title={section === 'overview' ? 'Record overview' : section === 'activity' ? 'Record activity' : 'Record settings'} description="Replace this panel with one focused domain workflow. Do not combine unrelated concepts on this route."><TopoloKeyValueRow label="Owner" value={record.owner} /><TopoloKeyValueRow label="Updated" value={record.updatedAt} /></TopoloOperationalPanel>
7120
+ </main>
7121
+ <TopoloInspectorPanel className="m-4 ml-0 max-lg:m-4" title="Record context" description="Keep selected-record metadata and secondary actions here." sticky><TopoloKeyValueRow label="Status" value={<RecordStatus status={record.status} />} /><TopoloKeyValueRow label="Owner" value={record.owner} /><CopyField label="Record ID" value={record.id} /></TopoloInspectorPanel>
7122
+ </TopoloWorkbench></div>
7123
+ );
7124
+ }
7125
+ `;
7126
+ }
6916
7127
  function platformGettingStartedRouteTsx() {
6917
7128
  return `import { TopoloAppPage, useAuth } from '@topolo-io/app-shell';
6918
7129
  import { GettingStarted, type OnboardingGuideStep } from '@topolo-io/onboarding';
@@ -7866,7 +8077,7 @@ function topoloConformanceWorkflow(options) {
7866
8077
  " - name: Install generated application",
7867
8078
  ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7868
8079
  ` working-directory: ${appPath}`,
7869
- " run: pnpm install --frozen-lockfile",
8080
+ " run: pnpm install --no-frozen-lockfile",
7870
8081
  "",
7871
8082
  " - name: Run generated application quality gates",
7872
8083
  ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
@@ -8019,6 +8230,37 @@ function topoloAppManifestJson(application, options) {
8019
8230
  }
8020
8231
  },
8021
8232
  targets,
8233
+ registration: {
8234
+ ownerType: options.payload.ownerType,
8235
+ portfolio: options.payload.portfolio,
8236
+ applicationId: application.id,
8237
+ serviceSlug: application.serviceSlug,
8238
+ displayName: toTitle(application.name),
8239
+ description: options.payload.experience.tagline,
8240
+ category: "topolo-platform",
8241
+ audience: options.payload.audience,
8242
+ tenancy: options.payload.tenancy,
8243
+ surfaceModel: options.payload.surfaceModel,
8244
+ containerAppId: options.payload.containerAppId,
8245
+ distributionModel: options.payload.distributionModel,
8246
+ version: "1.0.0",
8247
+ pricing: { type: "free", monthlyPrice: null },
8248
+ supportEmail: "support@topolo.io",
8249
+ auth: {
8250
+ callbackPath: "/auth/callback",
8251
+ requestedScopes: options.api ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${application.id}:read`] : ["openid", "profile", "email", "organization:read"]
8252
+ },
8253
+ capabilities: options.payload.capabilities,
8254
+ marketplace: {
8255
+ intent: options.payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
8256
+ status: "preview",
8257
+ shortBlurb: options.payload.experience.tagline,
8258
+ tagline: options.payload.experience.tagline,
8259
+ description: options.payload.experience.tagline,
8260
+ docsUrl: `https://docs.topolo.app/applications/${slug}`
8261
+ }
8262
+ },
8263
+ notifications: { role: "consumer_only" },
8022
8264
  ...actions.length ? { actions } : {}
8023
8265
  })}
8024
8266
  `;
@@ -8703,7 +8945,7 @@ function developerManifest(application, options) {
8703
8945
  surface_model: options.surfaceModel,
8704
8946
  container_app_id: options.containerAppId,
8705
8947
  package_name: application.packageName,
8706
- app_id: options.api ? application.appId : null,
8948
+ app_id: application.appId,
8707
8949
  capabilities: {
8708
8950
  selected: options.capabilities,
8709
8951
  supported_v1: [
@@ -8747,7 +8989,38 @@ function developerManifest(application, options) {
8747
8989
  },
8748
8990
  marketplace: {
8749
8991
  intent: options.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested"
8750
- }
8992
+ },
8993
+ registration: {
8994
+ ownerType: "third_party",
8995
+ applicationId: application.id,
8996
+ serviceSlug: application.serviceSlug,
8997
+ displayName: toTitle(application.name),
8998
+ description: `${toTitle(application.name)} application for Topolo.`,
8999
+ category: "productivity",
9000
+ audience: options.audience,
9001
+ tenancy: options.tenancy,
9002
+ surfaceModel: options.surfaceModel,
9003
+ containerAppId: options.containerAppId,
9004
+ distributionModel: options.distributionModel,
9005
+ version: "1.0.0",
9006
+ pricing: { type: "free", monthlyPrice: null },
9007
+ supportEmail: "",
9008
+ productionUrl: application.productionUrl,
9009
+ auth: {
9010
+ callbackPath,
9011
+ requestedScopes
9012
+ },
9013
+ capabilities: options.capabilities,
9014
+ marketplace: {
9015
+ intent: options.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
9016
+ status: "preview",
9017
+ shortBlurb: `${toTitle(application.name)} application for Topolo.`,
9018
+ tagline: `${toTitle(application.name)} application for Topolo.`,
9019
+ description: `${toTitle(application.name)} application for Topolo.`,
9020
+ docsUrl: application.productionUrl || "https://developers.topolo.app"
9021
+ }
9022
+ },
9023
+ notifications: { role: "consumer_only" }
8751
9024
  })}
8752
9025
  `;
8753
9026
  }
@@ -8804,7 +9077,8 @@ ${coverageThresholds}
8804
9077
  function platformBrowserContractTest(application, experience) {
8805
9078
  const required = experience.workspaceSwitcher === "required";
8806
9079
  const enabled = experience.workspaceSwitcher !== "off";
8807
- const navExpectation = experience.navigationMode === "none" ? " expect(home.nav).toBeUndefined();" : ` expect(home.nav).toEqual({ id: 'home', label: '${escapeSingleQuoted(experience.homeLabel)}' });`;
9080
+ const navExpectation = experience.navigationMode === "none" ? " expect(home.nav).toBeUndefined();" : ` expect(home.nav).toMatchObject({ id: 'home', label: '${escapeSingleQuoted(experience.homeLabel)}' });
9081
+ expect(home.nav?.icon).toBeDefined();`;
8808
9082
  return `import { describe, expect, it, vi } from 'vitest';
8809
9083
 
8810
9084
  vi.mock('../src/app-identity', () => ({
@@ -8825,12 +9099,52 @@ describe('generated application contract', () => {
8825
9099
  });
8826
9100
 
8827
9101
  it('keeps Getting Started out of product navigation', () => {
8828
- const [home, gettingStarted] = createAppRoutes('app_test');
9102
+ const routes = createAppRoutes('app_test');
9103
+ const home = routes.find((route) => route.path === '${escapeSingleQuoted(experience.homePath)}')!;
9104
+ const gettingStarted = routes.find((route) => route.path === '/getting-started')!;
8829
9105
  expect(home.path).toBe('${escapeSingleQuoted(experience.homePath)}');
8830
9106
  ${navExpectation}
8831
9107
  expect(gettingStarted.path).toBe('/getting-started');
8832
9108
  expect(gettingStarted.nav).toBeUndefined();
8833
9109
  });
9110
+
9111
+ it('keeps every visible route reachable when a sidebar is collapsed', () => {
9112
+ for (const route of createAppRoutes('app_test').filter((candidate) => candidate.nav)) {
9113
+ expect(route.nav?.icon).toBeDefined();
9114
+ }
9115
+ });
9116
+ });
9117
+ `;
9118
+ }
9119
+ function platformUiKitContractTest() {
9120
+ return `import { readFileSync, readdirSync, statSync } from 'node:fs';
9121
+ import { join } from 'node:path';
9122
+ import { describe, expect, it } from 'vitest';
9123
+
9124
+ function sourceFiles(root: string): string[] {
9125
+ return readdirSync(root).flatMap((entry) => {
9126
+ const path = join(root, entry);
9127
+ return statSync(path).isDirectory() ? sourceFiles(path) : path.endsWith('.tsx') ? [path] : [];
9128
+ });
9129
+ }
9130
+
9131
+ describe('generated shared UI contract', () => {
9132
+ const sourceRoot = join(process.cwd(), 'src');
9133
+ const source = sourceFiles(sourceRoot).map((path) => readFileSync(path, 'utf8')).join('\\n');
9134
+
9135
+ it('does not hand-roll controls or semantic tables in first-party routes', () => {
9136
+ expect(source).not.toMatch(/<(button|input|select|textarea|table|thead|tbody|tr|th|td)(\\s|>)/);
9137
+ });
9138
+
9139
+ it('keeps app-local CSS limited to the shared Tailwind pipeline', () => {
9140
+ expect(readFileSync(join(sourceRoot, 'styles.css'), 'utf8').trim()).toBe('@tailwind base;\\n@tailwind components;\\n@tailwind utilities;');
9141
+ });
9142
+
9143
+ it('scans app source and shared packages through the canonical token config', () => {
9144
+ const config = readFileSync(join(process.cwd(), 'tailwind.config.cjs'), 'utf8');
9145
+ expect(config).toContain('createTopoloTailwindConfig');
9146
+ expect(config).toContain("./apps/web/src/**/*.{js,jsx,ts,tsx}");
9147
+ });
8834
9148
  });
8835
9149
  `;
8836
9150
  }
@@ -8988,12 +9302,14 @@ function applicationScaffoldPayload(application, options = {}) {
8988
9302
  homeLabel: normalizeHomeLabel(options.homeLabel),
8989
9303
  navigationMode: normalizeNavigationMode(options.navigationMode),
8990
9304
  workspaceSwitcher: normalizeWorkspaceSwitcherMode(options.workspaceSwitcher),
9305
+ uiTemplate: normalizeUiTemplate(options.uiTemplate),
8991
9306
  workspaceLabel: tenancy === "household" ? "Households" : "Workspaces",
8992
9307
  tagline: String(options.tagline || "").trim() || `Manage your work in ${toTitle(name)}.`
8993
9308
  };
8994
- const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
8995
- const defaultAppId = target === "platform" ? `app_topolo_${id}` : catalogApplication?.services[0] ?? id;
8996
- const appId = api ? options.appId ?? defaultAppId : null;
9309
+ const serviceSlug = target === "platform" ? `topolo-${slug}` : id;
9310
+ const catalogAppId = catalogApplication?.services.find((service) => service.startsWith("app_"));
9311
+ const defaultAppId = target === "platform" ? `app_topolo_${id}` : catalogAppId ?? `app_${id.replace(/-/g, "_")}`;
9312
+ const appId = options.appId ?? defaultAppId;
8997
9313
  const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
8998
9314
  const scopes = scaffoldScopes({ browser, api });
8999
9315
  const capabilities = scaffoldCapabilities({ browser, api, distributionModel });
@@ -9267,9 +9583,21 @@ function scaffoldFiles(options) {
9267
9583
  "does not fork the shared shell chrome",
9268
9584
  "keeps app-specific layout scoped under generated classes"
9269
9585
  ],
9270
- content: stylesCss()
9586
+ content: stylesCss(target)
9271
9587
  }] : [],
9272
9588
  ...target === "platform" && options.browser ? [
9589
+ {
9590
+ path: `${browserBase}/tailwind.config.cjs`,
9591
+ purpose: "Canonical shared-token Tailwind content configuration.",
9592
+ requiredSignals: ["extends the shared Topolo token config", "scans app source correctly when Vite starts from the repository root"],
9593
+ content: tailwindConfigCjs()
9594
+ },
9595
+ {
9596
+ path: `${browserBase}/postcss.config.cjs`,
9597
+ purpose: "PostCSS pipeline for shared Topolo utility classes.",
9598
+ requiredSignals: ["loads the generated Tailwind config", "runs autoprefixer"],
9599
+ content: postcssConfigCjs()
9600
+ },
9273
9601
  {
9274
9602
  path: `${browserBase}/src/app-config.ts`,
9275
9603
  purpose: "Product route, navigation, context-switcher, and entry-surface configuration.",
@@ -9286,8 +9614,28 @@ function scaffoldFiles(options) {
9286
9614
  path: `${browserBase}/src/routes/HomeRoute.tsx`,
9287
9615
  purpose: "Focused authenticated home product surface.",
9288
9616
  requiredSignals: ["uses shared page primitives", "uses translation-safe product copy"],
9289
- content: platformHomeRouteTsx()
9617
+ content: platformHomeRouteTsx(options.payload.experience.uiTemplate)
9290
9618
  },
9619
+ ...options.payload.experience.uiTemplate === "minimal" ? [] : [
9620
+ {
9621
+ path: `${browserBase}/src/routes/starter-records.ts`,
9622
+ purpose: "Clearly marked replaceable starter records for the generated UI template.",
9623
+ requiredSignals: ["keeps sample data outside route components", "is explicitly marked for replacement"],
9624
+ content: platformStarterDataTs()
9625
+ },
9626
+ {
9627
+ path: `${browserBase}/src/routes/RecordsRoute.tsx`,
9628
+ purpose: "Focused CRUD index template built exclusively from shared UI primitives.",
9629
+ requiredSignals: ["uses shared table, search, select, button, and right-panel primitives", "keeps creation separate from the index"],
9630
+ content: platformRecordsRouteTsx()
9631
+ },
9632
+ {
9633
+ path: `${browserBase}/src/routes/RecordDetailRoute.tsx`,
9634
+ purpose: "Focused record workbench template with collection rail and inspector.",
9635
+ requiredSignals: ["uses the shared workbench and inspector primitives", "keeps detail sections URL-addressable"],
9636
+ content: platformRecordDetailRouteTsx()
9637
+ }
9638
+ ],
9291
9639
  {
9292
9640
  path: `${browserBase}/src/routes/GettingStartedRoute.tsx`,
9293
9641
  purpose: "Focused role-aware onboarding route reached from the account menu.",
@@ -9339,7 +9687,8 @@ function scaffoldFiles(options) {
9339
9687
  content: topoloAppManifestJson(application, {
9340
9688
  api: options.api,
9341
9689
  resources: options.payload.resources,
9342
- profile: options.payload.profile
9690
+ profile: options.payload.profile,
9691
+ payload: options.payload
9343
9692
  })
9344
9693
  },
9345
9694
  ...options.api ? [
@@ -9570,6 +9919,12 @@ export default defineConfig({
9570
9919
  requiredSignals: ["proves API proxying", "proves non-API browser fallback"],
9571
9920
  content: platformWebWorkerTest(application, options.api)
9572
9921
  },
9922
+ {
9923
+ path: `${browserBase}/test/ui-kit-contract.test.ts`,
9924
+ purpose: "Executable guard against hand-rolled first-party controls and broken shared styling.",
9925
+ requiredSignals: ["rejects native controls and semantic tables in generated routes", "proves canonical Tailwind scanning and zero app-local design CSS"],
9926
+ content: platformUiKitContractTest()
9927
+ },
9573
9928
  {
9574
9929
  path: `${browserBase}/src/worker.ts`,
9575
9930
  purpose: "Browser Worker that serves assets and forwards API paths when an API target exists.",
@@ -9736,7 +10091,7 @@ async function provisionApplicationScaffold(payload) {
9736
10091
  `Could not resolve Developers app id from Auth slug ${DEVELOPERS_SERVICE_SLUG}.`
9737
10092
  );
9738
10093
  }
9739
- const endpoint = payload.target === "platform" ? "/api/developer-console/admin/first-party-app-scaffolds" : "/api/developer-console/apps/scaffold";
10094
+ const endpoint = payload.target === "platform" ? "/api/developer-console/admin/apps/reconcile" : "/api/developer-console/apps/reconcile";
9740
10095
  const headers = new Headers({
9741
10096
  "Content-Type": "application/json",
9742
10097
  "X-App-ID": developersAppId,
@@ -9754,7 +10109,7 @@ async function provisionApplicationScaffold(payload) {
9754
10109
  const response = await fetch(`${developersOrigin}${endpoint}`, {
9755
10110
  method: "POST",
9756
10111
  headers,
9757
- body: JSON.stringify(buildProvisioningRequest(payload))
10112
+ body: JSON.stringify(buildProvisioningRequest(payload, identity.user.email))
9758
10113
  });
9759
10114
  const envelope = await response.json().catch(() => null);
9760
10115
  if (!response.ok || !envelope?.success || !envelope.data) {
@@ -9763,8 +10118,10 @@ async function provisionApplicationScaffold(payload) {
9763
10118
  );
9764
10119
  }
9765
10120
  const app = pickRecord(envelope.data.app);
9766
- const service = pickRecord(envelope.data.service);
9767
- const authConfig = pickRecord(envelope.data.authConfig);
10121
+ const projections = pickRecord(envelope.data.projections);
10122
+ const authProjection = pickRecord(projections?.auth);
10123
+ const service = pickRecord(authProjection?.service);
10124
+ const authConfig = pickRecord(authProjection?.authConfig);
9768
10125
  if (!app) {
9769
10126
  throw new Error("Developers provisioning did not return an app record.");
9770
10127
  }
@@ -9801,9 +10158,9 @@ async function provisionApplicationScaffold(payload) {
9801
10158
  } : null
9802
10159
  };
9803
10160
  }
9804
- function buildProvisioningRequest(payload) {
10161
+ function buildProvisioningRequest(payload, supportEmail) {
9805
10162
  return {
9806
- target: payload.target,
10163
+ mode: "apply",
9807
10164
  ownerType: payload.ownerType,
9808
10165
  portfolio: payload.portfolio,
9809
10166
  audience: payload.audience,
@@ -9816,14 +10173,28 @@ function buildProvisioningRequest(payload) {
9816
10173
  "http://localhost:5173/auth/callback",
9817
10174
  payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/auth/callback` : null
9818
10175
  ].filter(Boolean),
9819
- healthEndpoint: payload.application.productionUrl ? `${payload.application.productionUrl.replace(/\/$/, "")}/api/health` : null,
9820
- runtimeStrategy: "external",
9821
10176
  requestedScopes: payload.application.appId ? ["openid", "profile", "email", "organization:read", "entitlements:read", `${payload.application.id}:read`] : ["openid", "profile", "email", "organization:read"],
9822
- marketplaceIntent: payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
10177
+ marketplace: {
10178
+ intent: payload.distributionModel === "marketplace" ? "submit_when_ready" : "not_requested",
10179
+ status: "preview",
10180
+ shortBlurb: payload.experience.tagline,
10181
+ tagline: payload.experience.tagline,
10182
+ description: payload.experience.tagline,
10183
+ docsUrl: payload.application.productionUrl || "https://developers.topolo.app"
10184
+ },
10185
+ notifications: { role: "consumer_only" },
9823
10186
  applicationId: payload.application.id,
9824
- appDir: payload.application.appDir,
9825
- packageName: payload.application.packageName,
9826
10187
  name: payload.application.name,
10188
+ description: payload.experience.tagline,
10189
+ category: payload.target === "platform" ? "topolo-platform" : "productivity",
10190
+ version: "1.0.0",
10191
+ pricingType: "free",
10192
+ monthlyPrice: null,
10193
+ demoUrl: payload.application.productionUrl,
10194
+ repoUrl: null,
10195
+ supportEmail,
10196
+ tags: [],
10197
+ screenshots: [],
9827
10198
  productionUrl: payload.application.productionUrl,
9828
10199
  serviceSlug: payload.application.serviceSlug,
9829
10200
  appId: payload.application.appId
@@ -9897,6 +10268,332 @@ async function applyProvisionedAppId(manifestPath, appId) {
9897
10268
 
9898
10269
  // src/commands/apps.ts
9899
10270
  import { join as join3 } from "path";
10271
+
10272
+ // src/commands/app-reconcile.ts
10273
+ import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
10274
+ import path2 from "path";
10275
+ import { createInterface as createInterface2 } from "readline/promises";
10276
+ import { stdin as input2, stdout as output } from "process";
10277
+ function record(value) {
10278
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
10279
+ }
10280
+ function environmentValue(value, environment) {
10281
+ if (typeof value === "string") return value;
10282
+ if (!value) return null;
10283
+ return typeof value[environment] === "string" ? value[environment] : null;
10284
+ }
10285
+ function resolveManifestApplicationUrl(manifest, environment) {
10286
+ const targets = manifest.targets || {};
10287
+ const targetName = manifest.primaryTarget || (targets.web ? "web" : Object.keys(targets)[0]);
10288
+ const target = targetName ? targets[targetName] : null;
10289
+ if (!target) {
10290
+ if (manifest.registration?.productionUrl) return manifest.registration.productionUrl.replace(/\/+$/, "");
10291
+ throw new Error("topolo.app.json has no application target or registration.productionUrl.");
10292
+ }
10293
+ const publicUrl = environmentValue(target.publicUrl, environment);
10294
+ if (publicUrl) return publicUrl.replace(/\/+$/, "");
10295
+ const hostOverride = target.hostOverride?.[environment];
10296
+ if (hostOverride) return `https://${hostOverride.replace(/^https?:\/\//, "").replace(/\/+$/, "")}`;
10297
+ const domain = manifest.environments?.[environment]?.domain;
10298
+ if (!target.subdomain || !domain) {
10299
+ throw new Error(`Cannot derive the ${environment} application URL from target ${targetName}.`);
10300
+ }
10301
+ return `https://${target.subdomain}.${domain}`;
10302
+ }
10303
+ function assertRegistration(value) {
10304
+ const registration = record(value);
10305
+ const required = [
10306
+ "ownerType",
10307
+ "applicationId",
10308
+ "serviceSlug",
10309
+ "displayName",
10310
+ "description",
10311
+ "audience",
10312
+ "tenancy",
10313
+ "surfaceModel",
10314
+ "distributionModel",
10315
+ "version",
10316
+ "pricing",
10317
+ "supportEmail",
10318
+ "auth",
10319
+ "capabilities",
10320
+ "marketplace"
10321
+ ];
10322
+ const missing = required.filter((field) => registration?.[field] == null || registration[field] === "");
10323
+ if (missing.length) {
10324
+ throw new Error(
10325
+ `topolo.app.json registration is incomplete: ${missing.join(", ")}. Run in an interactive terminal to complete the manifest, or edit the declarative registration block.`
10326
+ );
10327
+ }
10328
+ }
10329
+ function titleFromSlug(value) {
10330
+ return value.replace(/^topolo[-_]/, "").split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
10331
+ }
10332
+ async function completeApplicationRegistrationInteractively(options) {
10333
+ const manifestPath = path2.resolve(options.manifestPath || "topolo.app.json");
10334
+ const manifest = JSON.parse(await readFile3(manifestPath, "utf8"));
10335
+ const existing = record(manifest.registration) || {};
10336
+ const appId = String(manifest.appId || manifest.app_id || "").trim();
10337
+ const slug = String(existing.serviceSlug || manifest.name || "").trim();
10338
+ const defaultName = titleFromSlug(slug);
10339
+ const readline = options.prompt ? null : createInterface2({ input: input2, output });
10340
+ const ask = options.prompt || ((question) => readline.question(question));
10341
+ let changed = !manifest.registration;
10342
+ const requiredText = async (field, label, fallback = "") => {
10343
+ const current = typeof existing[field] === "string" ? String(existing[field]).trim() : "";
10344
+ if (current) return current;
10345
+ const answer = (await ask(`${label}${fallback ? ` [${fallback}]` : ""}: `)).trim() || fallback;
10346
+ if (!answer) throw new Error(`${field} is required.`);
10347
+ changed = true;
10348
+ return answer;
10349
+ };
10350
+ try {
10351
+ const productionUrl = String(existing.productionUrl || resolveManifestApplicationUrl(manifest, options.environment || selectedEnvironment()));
10352
+ const displayName = await requiredText("displayName", "Application display name", defaultName);
10353
+ const description = await requiredText("description", "Application description", `${displayName} application for Topolo.`);
10354
+ const supportEmail = await requiredText("supportEmail", "Support email");
10355
+ manifest.registration = {
10356
+ ownerType: options.expectedOwnerType,
10357
+ ...options.expectedOwnerType === "topolo_first_party" ? { portfolio: existing.portfolio || "platform" } : {},
10358
+ applicationId: String(existing.applicationId || slug).replace(/^topolo-/, "topolo-"),
10359
+ serviceSlug: slug,
10360
+ displayName,
10361
+ description,
10362
+ category: String(existing.category || (options.expectedOwnerType === "topolo_first_party" ? "topolo-platform" : "productivity")),
10363
+ audience: existing.audience || "business",
10364
+ tenancy: existing.tenancy || "organization",
10365
+ surfaceModel: existing.surfaceModel || "web_app",
10366
+ containerAppId: existing.containerAppId,
10367
+ distributionModel: existing.distributionModel || (options.expectedOwnerType === "topolo_first_party" ? "marketplace" : "organization_internal"),
10368
+ version: String(existing.version || "1.0.0"),
10369
+ pricing: record(existing.pricing) || { type: "free", monthlyPrice: null },
10370
+ supportEmail,
10371
+ productionUrl,
10372
+ demoUrl: typeof existing.demoUrl === "string" ? existing.demoUrl : void 0,
10373
+ repoUrl: typeof existing.repoUrl === "string" ? existing.repoUrl : void 0,
10374
+ tags: Array.isArray(existing.tags) ? existing.tags : [],
10375
+ screenshots: Array.isArray(existing.screenshots) ? existing.screenshots : [],
10376
+ auth: record(existing.auth) || {
10377
+ callbackPath: "/auth/callback",
10378
+ requestedScopes: ["openid", "profile", "email", "organization:read"]
10379
+ },
10380
+ capabilities: Array.isArray(existing.capabilities) ? existing.capabilities : ["login", "launcher"],
10381
+ marketplace: record(existing.marketplace) || {
10382
+ intent: options.expectedOwnerType === "topolo_first_party" ? "submit_when_ready" : "not_requested",
10383
+ status: "preview",
10384
+ shortBlurb: description,
10385
+ tagline: description,
10386
+ description,
10387
+ docsUrl: productionUrl
10388
+ }
10389
+ };
10390
+ if (!manifest.notifications) {
10391
+ const role = (await ask("Notification role [consumer_only]: ")).trim() || "consumer_only";
10392
+ if (!["producer", "consumer_only", "internal_non_producer"].includes(role)) {
10393
+ throw new Error("Notification role must be producer, consumer_only, or internal_non_producer.");
10394
+ }
10395
+ if (role === "producer") {
10396
+ const contracts = (await ask("Notification contracts path: ")).trim();
10397
+ if (!contracts) throw new Error("Producer applications must declare a notification contracts path.");
10398
+ manifest.notifications = { role, contracts };
10399
+ } else {
10400
+ manifest.notifications = { role };
10401
+ }
10402
+ changed = true;
10403
+ }
10404
+ if (!appId) throw new Error("topolo.app.json must declare the immutable appId before reconciliation.");
10405
+ if (changed) await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
10406
+ `, "utf8");
10407
+ return { changed, manifestPath };
10408
+ } finally {
10409
+ if (readline) readline.close();
10410
+ }
10411
+ }
10412
+ async function loadApplicationReconcileRequest(options) {
10413
+ const manifestPath = path2.resolve(options.manifestPath || "topolo.app.json");
10414
+ const raw = await readFile3(manifestPath, "utf8");
10415
+ const manifest = JSON.parse(raw);
10416
+ assertRegistration(manifest.registration);
10417
+ const registration = manifest.registration;
10418
+ if (registration.ownerType !== options.expectedOwnerType) {
10419
+ throw new Error(
10420
+ `${registration.ownerType} registration cannot be reconciled through the ${options.expectedOwnerType} command surface.`
10421
+ );
10422
+ }
10423
+ const environment = options.environment || selectedEnvironment();
10424
+ const productionUrl = resolveManifestApplicationUrl(manifest, environment);
10425
+ const callbackPath = registration.auth.callbackPath.startsWith("/") ? registration.auth.callbackPath : `/${registration.auth.callbackPath}`;
10426
+ const appId = String(manifest.appId || manifest.app_id || "").trim();
10427
+ if (!appId) throw new Error("topolo.app.json must declare the immutable appId.");
10428
+ return {
10429
+ manifestPath,
10430
+ request: {
10431
+ mode: options.mode,
10432
+ appId,
10433
+ ownerType: registration.ownerType,
10434
+ portfolio: registration.portfolio ?? null,
10435
+ applicationId: registration.applicationId,
10436
+ serviceSlug: registration.serviceSlug,
10437
+ audience: registration.audience,
10438
+ tenancy: registration.tenancy,
10439
+ surfaceModel: registration.surfaceModel,
10440
+ containerAppId: registration.containerAppId ?? null,
10441
+ distributionModel: registration.distributionModel,
10442
+ name: registration.displayName,
10443
+ description: registration.description,
10444
+ category: registration.category ?? null,
10445
+ version: registration.version,
10446
+ pricingType: registration.pricing.type,
10447
+ monthlyPrice: registration.pricing.monthlyPrice ?? null,
10448
+ productionUrl,
10449
+ demoUrl: registration.ownerType === "topolo_first_party" ? productionUrl : registration.demoUrl ?? productionUrl,
10450
+ repoUrl: registration.repoUrl ?? null,
10451
+ supportEmail: registration.supportEmail,
10452
+ tags: registration.tags ?? [],
10453
+ screenshots: registration.screenshots ?? [],
10454
+ callbackUrls: [`${productionUrl}${callbackPath}`],
10455
+ requestedScopes: registration.auth.requestedScopes,
10456
+ capabilities: registration.capabilities,
10457
+ marketplace: { ...registration.marketplace, appUrl: productionUrl },
10458
+ notifications: manifest.notifications ?? null
10459
+ }
10460
+ };
10461
+ }
10462
+ function defaultAuthOrigin2(environment) {
10463
+ if (environment === "development") return "https://auth.topolo.dev";
10464
+ if (environment === "staging") return "https://auth.stg.topolo.us";
10465
+ return "https://auth.topolo.app";
10466
+ }
10467
+ async function resolveDevelopersAppId(authOrigin, fetchImpl) {
10468
+ const response = await fetchImpl(`${authOrigin}/api/services/by-slug/topolo-developers`, {
10469
+ headers: { Accept: "application/json" }
10470
+ });
10471
+ const payload = await response.json().catch(() => null);
10472
+ const appId = typeof payload?.data?.app_id === "string" ? payload.data.app_id.trim() : "";
10473
+ if (!response.ok || !appId) throw new Error("Could not resolve the Developers app id from Auth.");
10474
+ return appId;
10475
+ }
10476
+ async function reconcileApplicationRequest(request, dependencies = {}) {
10477
+ const selectEnvironment = dependencies.selectedEnvironment || selectedEnvironment;
10478
+ const refreshCredential = dependencies.refreshCredentialIfNeeded || refreshCredentialIfNeeded;
10479
+ const makeClient2 = dependencies.buildClient || buildClient;
10480
+ const loadConfig = dependencies.readConfig || readConfig;
10481
+ const resolveProfile = dependencies.resolvedProfileConfig || resolvedProfileConfig;
10482
+ const assertServiceUrl = dependencies.assertServiceUrlForEnvironment || assertServiceUrlForEnvironment;
10483
+ const fetchImpl = dependencies.fetch || fetch;
10484
+ const environment = selectEnvironment();
10485
+ const credential = await refreshCredential(environment);
10486
+ if (!credential) throw new Error("Application reconciliation requires an authenticated Topolo session. Run `topolo auth login`.");
10487
+ const client = await makeClient2();
10488
+ if (!client) throw new Error("Could not build the authenticated Topolo client.");
10489
+ await client.client.introspect();
10490
+ const config = await loadConfig();
10491
+ const profile = resolveProfile(config, environment);
10492
+ const authOrigin = (profile.serviceUrls?.auth || defaultAuthOrigin2(environment)).replace(/\/+$/, "");
10493
+ const developersOrigin = (profile.serviceUrls?.developers || "https://developers.topolo.app").replace(/\/+$/, "");
10494
+ assertServiceUrl("auth", authOrigin, environment);
10495
+ assertServiceUrl("developers", developersOrigin, environment);
10496
+ const headers = new Headers({
10497
+ "Content-Type": "application/json",
10498
+ "X-App-ID": await resolveDevelopersAppId(authOrigin, fetchImpl),
10499
+ "X-Topolo-Client": `topolo-cli/${CLI_VERSION}`
10500
+ });
10501
+ const agentName = resolveAgentName(profile.agentName);
10502
+ if (agentName) headers.set("X-Topolo-Agent", agentName);
10503
+ if (credential.kind === "api_key") headers.set("X-Api-Key", credential.apiKey);
10504
+ else headers.set("Authorization", `Bearer ${credential.accessToken}`);
10505
+ const endpoint = request.ownerType === "topolo_first_party" ? "/api/developer-console/admin/apps/reconcile" : "/api/developer-console/apps/reconcile";
10506
+ const response = await fetchImpl(`${developersOrigin}${endpoint}`, {
10507
+ method: "POST",
10508
+ headers,
10509
+ body: JSON.stringify(request)
10510
+ });
10511
+ const envelope = await response.json().catch(() => null);
10512
+ if (!response.ok || !envelope?.success || !envelope.data) {
10513
+ const message = typeof envelope?.error === "string" ? envelope.error : envelope?.error?.message || envelope?.message || `Application reconciliation failed with status ${response.status}.`;
10514
+ const error = new Error(message);
10515
+ error.code = response.status === 409 ? "immutable_drift" : response.status === 403 ? "permission_denied" : "reconcile_failed";
10516
+ error.details = typeof envelope?.error === "object" ? envelope.error.details : void 0;
10517
+ throw error;
10518
+ }
10519
+ return envelope.data;
10520
+ }
10521
+ function resolveReconcileMode(options) {
10522
+ const explicit = [options.check, options.dryRun, options.apply].filter(Boolean).length;
10523
+ if (explicit > 1) throw new Error("Choose exactly one of --check, --dry-run, or --apply.");
10524
+ if (options.apply) return { mode: "apply", interactiveApply: false };
10525
+ if (options.check || options.dryRun) return { mode: "check", interactiveApply: false };
10526
+ return {
10527
+ mode: "check",
10528
+ interactiveApply: !options.nonInteractive && Boolean(options.interactive || process.stdin.isTTY && process.stdout.isTTY)
10529
+ };
10530
+ }
10531
+ async function confirmApplicationReconcile(result) {
10532
+ if (result.plan.status === "in_sync") return false;
10533
+ const readline = createInterface2({ input: input2, output });
10534
+ try {
10535
+ const answer = await readline.question(`Apply ${result.plan.changes.length} application registration change(s)? [y/N] `);
10536
+ return /^(y|yes)$/i.test(answer.trim());
10537
+ } finally {
10538
+ readline.close();
10539
+ }
10540
+ }
10541
+ function formatApplicationReconcile(result, manifestPath) {
10542
+ const lines = [
10543
+ `Application registration: ${result.plan.status}`,
10544
+ `Manifest: ${manifestPath}`,
10545
+ `Mode: ${result.mode}`
10546
+ ];
10547
+ for (const change of result.changes || result.plan.changes) {
10548
+ lines.push(`- ${change.path}: ${JSON.stringify(change.before)} -> ${JSON.stringify(change.after)}`);
10549
+ }
10550
+ if (result.projections) lines.push("Catalog/Auth projections: reconciled");
10551
+ return lines.join("\n");
10552
+ }
10553
+ function registerApplicationReconcileCommand(parent, ownerType, dependencies = {}) {
10554
+ const completeRegistration = dependencies.completeRegistration || completeApplicationRegistrationInteractively;
10555
+ const loadRequest = dependencies.loadRequest || loadApplicationReconcileRequest;
10556
+ const reconcileRequest = dependencies.reconcileRequest || reconcileApplicationRequest;
10557
+ const confirm = dependencies.confirm || confirmApplicationReconcile;
10558
+ 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) => {
10559
+ const outputMode = resolveOutputMode(options.json);
10560
+ try {
10561
+ const selection = resolveReconcileMode(options);
10562
+ if (selection.interactiveApply) {
10563
+ await completeRegistration({
10564
+ manifestPath: options.manifest,
10565
+ expectedOwnerType: ownerType
10566
+ });
10567
+ }
10568
+ const loaded = await loadRequest({
10569
+ manifestPath: options.manifest,
10570
+ expectedOwnerType: ownerType,
10571
+ mode: selection.mode
10572
+ });
10573
+ let result = await reconcileRequest(loaded.request);
10574
+ if (selection.interactiveApply && result.plan.status !== "in_sync") {
10575
+ if (outputMode === "pretty") {
10576
+ process.stdout.write(`${formatApplicationReconcile(result, loaded.manifestPath)}
10577
+ `);
10578
+ }
10579
+ const confirmed = options.yes || await confirm(result);
10580
+ if (confirmed) {
10581
+ result = await reconcileRequest({ ...loaded.request, mode: "apply" });
10582
+ }
10583
+ }
10584
+ emit(
10585
+ { manifest: loaded.manifestPath, reconciliation: result },
10586
+ outputMode,
10587
+ () => formatApplicationReconcile(result, loaded.manifestPath)
10588
+ );
10589
+ if (result.mode === "check" && result.plan.status !== "in_sync") process.exitCode = 2;
10590
+ } catch (error) {
10591
+ fail(error, outputMode);
10592
+ }
10593
+ });
10594
+ }
10595
+
10596
+ // src/commands/apps.ts
9900
10597
  var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
9901
10598
  var CLOUDCONTROL_LOCAL_APPLICATION = {
9902
10599
  id: "cloudcontrol",
@@ -9926,6 +10623,7 @@ function registerAppsCommand(program2) {
9926
10623
  fail(err, mode);
9927
10624
  }
9928
10625
  });
10626
+ registerApplicationReconcileCommand(apps, "third_party");
9929
10627
  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) => {
9930
10628
  const mode = resolveOutputMode(opts.json);
9931
10629
  try {
@@ -9964,8 +10662,8 @@ function registerAppsCommand(program2) {
9964
10662
  repoRoot: opts.repoRoot
9965
10663
  });
9966
10664
  const gate = opts.failOn ? evaluateApplicationAuditGate(payload, opts.failOn) : null;
9967
- const output = gate ? { ...payload, conformanceGate: gate } : payload;
9968
- emit(output, mode, () => formatAudit(output));
10665
+ const output2 = gate ? { ...payload, conformanceGate: gate } : payload;
10666
+ emit(output2, mode, () => formatAudit(output2));
9969
10667
  if (gate && !gate.passed) {
9970
10668
  if (mode === "json") {
9971
10669
  process.stderr.write(JSON.stringify({ error: gate.message, conformanceGate: gate }, null, 2) + "\n");
@@ -10027,14 +10725,14 @@ function registerAppsCommand(program2) {
10027
10725
  appIdWriteBack = { ...result, path: manifestFile.path };
10028
10726
  }
10029
10727
  }
10030
- const output = {
10728
+ const output2 = {
10031
10729
  ...payload,
10032
10730
  ...writeResult ? { write: writeResult } : {},
10033
10731
  ...provisionResult ? { provision: provisionResult } : {},
10034
10732
  ...appIdWriteBack ? { appIdWriteBack } : {}
10035
10733
  };
10036
10734
  emit(
10037
- output,
10735
+ output2,
10038
10736
  mode,
10039
10737
  () => formatScaffold(payload, writeResult, provisionResult, appIdWriteBack)
10040
10738
  );
@@ -10098,7 +10796,7 @@ function applicationAuditPayload(application, options = {}) {
10098
10796
  }
10099
10797
  function recordApplicationEvidence(application, options = {}) {
10100
10798
  const appId = resolveApplication(application).id;
10101
- const root = path2.resolve(options.appRoot ?? process.cwd());
10799
+ const root = path3.resolve(options.appRoot ?? process.cwd());
10102
10800
  if (!existsSync3(root)) {
10103
10801
  throw new Error(`Application root does not exist: ${root}`);
10104
10802
  }
@@ -10110,8 +10808,8 @@ function recordApplicationEvidence(application, options = {}) {
10110
10808
  commit = void 0;
10111
10809
  }
10112
10810
  }
10113
- const evidenceDir = path2.join(root, ".topolo");
10114
- const evidencePath = path2.join(evidenceDir, "evidence.json");
10811
+ const evidenceDir = path3.join(root, ".topolo");
10812
+ const evidencePath = path3.join(evidenceDir, "evidence.json");
10115
10813
  let before = null;
10116
10814
  if (existsSync3(evidencePath)) {
10117
10815
  try {
@@ -10411,16 +11109,17 @@ function statusesAtOrWorse(threshold) {
10411
11109
 
10412
11110
  // src/commands/platform.ts
10413
11111
  import { readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
10414
- import path3 from "path";
11112
+ import path4 from "path";
10415
11113
  function registerPlatformCommand(program2) {
10416
11114
  const platform3 = program2.command("platform").description("Internal Topolo first-party platform and personal app commands");
10417
11115
  const platformApps = platform3.command("apps").description("Scaffold and operate first-party Topolo platform and personal applications");
11116
+ registerApplicationReconcileCommand(platformApps, "topolo_first_party");
10418
11117
  platformApps.command("scaffold <application>").description(
10419
11118
  "Generate a first-party Topolo scaffold for the platform or personal portfolio."
10420
11119
  ).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(
10421
11120
  "--surface-model <surfaceModel>",
10422
11121
  "First-party surface model: web_app, mobile_container, mobile_module, or messaging_surface"
10423
- ).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--profile <profile>", "First-party scaffold profile such as spatial-assets").option("--with-d1", "Declare a D1 binding in the first-party scaffold").option("--with-r2", "Declare an R2 binding in the first-party scaffold").option("--with-queue", "Declare a Queue binding in the first-party scaffold").option("--with-workflow", "Declare a Workflow binding in the first-party scaffold").option("--with-container-processing", "Declare container-processing metadata in CloudControl").option("--home-path <path>", "Authenticated home route (default: /dashboard)").option("--home-label <label>", "Home navigation label (default: Dashboard)").option("--navigation-mode <mode>", "Product navigation: sidebar, topbar, or none").option("--workspace-switcher <mode>", "Workspace context selector: required, optional, or off").option("--tagline <text>", "Product-facing landing and home description").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include the API/service baseline").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
11122
+ ).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--profile <profile>", "First-party scaffold profile such as spatial-assets").option("--with-d1", "Declare a D1 binding in the first-party scaffold").option("--with-r2", "Declare an R2 binding in the first-party scaffold").option("--with-queue", "Declare a Queue binding in the first-party scaffold").option("--with-workflow", "Declare a Workflow binding in the first-party scaffold").option("--with-container-processing", "Declare container-processing metadata in CloudControl").option("--home-path <path>", "Authenticated home route (default: /dashboard)").option("--home-label <label>", "Home navigation label (default: Dashboard)").option("--navigation-mode <mode>", "Product navigation: sidebar, topbar, or none").option("--workspace-switcher <mode>", "Workspace context selector: required, optional, or off").option("--ui-template <template>", "Shared UI starter: operations, index, workbench, or minimal").option("--tagline <text>", "Product-facing landing and home description").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include the API/service baseline").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
10424
11123
  "--provision",
10425
11124
  "Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
10426
11125
  ).option(
@@ -10460,6 +11159,7 @@ function registerPlatformCommand(program2) {
10460
11159
  homeLabel: opts.homeLabel,
10461
11160
  navigationMode: opts.navigationMode,
10462
11161
  workspaceSwitcher: opts.workspaceSwitcher,
11162
+ uiTemplate: opts.uiTemplate,
10463
11163
  tagline: opts.tagline
10464
11164
  });
10465
11165
  if (opts.workflowOnly && !payload.ciWorkflow) {
@@ -10470,13 +11170,13 @@ function registerPlatformCommand(program2) {
10470
11170
  }
10471
11171
  const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
10472
11172
  const provisionResult = shouldProvision ? await provisionApplicationScaffold(payload) : null;
10473
- const output = opts.workflowOnly ? { application: payload.application, ciWorkflowPath: payload.ciWorkflowPath, ciWorkflow: payload.ciWorkflow } : {
11173
+ const output2 = opts.workflowOnly ? { application: payload.application, ciWorkflowPath: payload.ciWorkflowPath, ciWorkflow: payload.ciWorkflow } : {
10474
11174
  ...payload,
10475
11175
  ...writeResult ? { write: writeResult } : {},
10476
11176
  ...provisionResult ? { provision: provisionResult } : {}
10477
11177
  };
10478
11178
  emit(
10479
- output,
11179
+ output2,
10480
11180
  mode,
10481
11181
  () => opts.workflowOnly ? payload.ciWorkflow ?? "" : formatScaffold(payload, writeResult, provisionResult)
10482
11182
  );
@@ -10527,12 +11227,12 @@ var SKIPPED_SCAN_DIRS = /* @__PURE__ */ new Set([
10527
11227
  "output"
10528
11228
  ]);
10529
11229
  function scanStagingProductionUrls(rootInput, allowPatterns) {
10530
- const root = path3.resolve(rootInput);
11230
+ const root = path4.resolve(rootInput);
10531
11231
  const allowlist = allowPatterns.map((pattern) => new RegExp(pattern));
10532
11232
  const violations = [];
10533
11233
  function shouldSkipFile(filePath) {
10534
- const basename = path3.basename(filePath);
10535
- const relativePath = path3.relative(root, filePath);
11234
+ const basename = path4.basename(filePath);
11235
+ const relativePath = path4.relative(root, filePath);
10536
11236
  if (basename === "package-lock.json" || basename === "pnpm-lock.yaml" || basename === "yarn.lock") return true;
10537
11237
  if (basename === "topolo.cloudcontrol.json") return true;
10538
11238
  if (basename === "wrangler.toml" || basename === "wrangler.jsonc") return true;
@@ -10541,14 +11241,14 @@ function scanStagingProductionUrls(rootInput, allowPatterns) {
10541
11241
  if (basename.endsWith(".generated.ts")) return true;
10542
11242
  if (basename.endsWith(".test.ts") || basename.endsWith(".test.tsx") || basename.endsWith(".spec.ts")) return true;
10543
11243
  if (basename === ".env.production") return true;
10544
- 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;
10545
- if (relativePath.includes(`${path3.sep}test${path3.sep}`) || relativePath.includes(`${path3.sep}tests${path3.sep}`) || relativePath.includes(`${path3.sep}e2e${path3.sep}`)) return true;
10546
- if (relativePath.startsWith(`scripts${path3.sep}`) || relativePath.includes(`${path3.sep}scripts${path3.sep}`)) return true;
10547
- return !RUNTIME_EXTENSIONS.has(path3.extname(filePath));
11244
+ if (relativePath === path4.join("src", "environment.ts") || relativePath === path4.join("src", "environmentUrls.ts") || relativePath.endsWith(path4.join("src", "environment.ts")) || relativePath.endsWith(path4.join("src", "lib", "environment.ts")) || relativePath === path4.join("packages", "topolo-ui-kit", "src", "environmentUrls.ts")) return true;
11245
+ if (relativePath.includes(`${path4.sep}test${path4.sep}`) || relativePath.includes(`${path4.sep}tests${path4.sep}`) || relativePath.includes(`${path4.sep}e2e${path4.sep}`)) return true;
11246
+ if (relativePath.startsWith(`scripts${path4.sep}`) || relativePath.includes(`${path4.sep}scripts${path4.sep}`)) return true;
11247
+ return !RUNTIME_EXTENSIONS.has(path4.extname(filePath));
10548
11248
  }
10549
11249
  function scanFile(filePath) {
10550
11250
  if (shouldSkipFile(filePath)) return;
10551
- const relativePath = path3.relative(root, filePath);
11251
+ const relativePath = path4.relative(root, filePath);
10552
11252
  const content = readFileSync5(filePath, "utf8");
10553
11253
  const lines = content.split(/\r?\n/);
10554
11254
  lines.forEach((line, index) => {
@@ -10563,7 +11263,7 @@ function scanStagingProductionUrls(rootInput, allowPatterns) {
10563
11263
  }
10564
11264
  function walk(dir) {
10565
11265
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
10566
- const fullPath = path3.join(dir, entry.name);
11266
+ const fullPath = path4.join(dir, entry.name);
10567
11267
  if (entry.isDirectory()) {
10568
11268
  if (!SKIPPED_SCAN_DIRS.has(entry.name)) walk(fullPath);
10569
11269
  continue;
@@ -10731,7 +11431,7 @@ var DEFAULT_SEED_BASE_URL = "https://seed.stg.topolo.us";
10731
11431
  function seedBaseUrl() {
10732
11432
  return process.env.TOPOLO_SEED_URL || DEFAULT_SEED_BASE_URL;
10733
11433
  }
10734
- async function authedFetch(path4, init = {}) {
11434
+ async function authedFetch(path5, init = {}) {
10735
11435
  const credential = await refreshCredentialIfNeeded("staging");
10736
11436
  if (!credential) {
10737
11437
  throw new Error("No staging credential configured. Run `topolo auth login --env staging` or set TOPOLO_STAGING_API_KEY.");
@@ -10741,7 +11441,7 @@ async function authedFetch(path4, init = {}) {
10741
11441
  else headers.set("Authorization", `Bearer ${credential.accessToken}`);
10742
11442
  headers.set("X-Topolo-Client", `topolo-cli/${CLI_VERSION}`);
10743
11443
  if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
10744
- const response = await fetch(`${seedBaseUrl()}${path4}`, { ...init, headers });
11444
+ const response = await fetch(`${seedBaseUrl()}${path5}`, { ...init, headers });
10745
11445
  const text = await response.text();
10746
11446
  let body;
10747
11447
  try {
@@ -11018,7 +11718,7 @@ function createOAuthClientPayload(options) {
11018
11718
  logo_url: options.logoUrl
11019
11719
  };
11020
11720
  }
11021
- async function authFetchJson(path4, init = {}) {
11721
+ async function authFetchJson(path5, init = {}) {
11022
11722
  const environment = selectedEnvironment();
11023
11723
  const credential = await refreshCredentialIfNeeded(environment);
11024
11724
  if (!credential) {
@@ -11040,7 +11740,7 @@ async function authFetchJson(path4, init = {}) {
11040
11740
  throw err;
11041
11741
  }
11042
11742
  headers.set("Authorization", `Bearer ${credential.accessToken}`);
11043
- const response = await fetch(`${authBaseUrl}${path4}`, { ...init, headers });
11743
+ const response = await fetch(`${authBaseUrl}${path5}`, { ...init, headers });
11044
11744
  const text = await response.text();
11045
11745
  const payload = text ? JSON.parse(text) : {};
11046
11746
  if (!response.ok || payload.success === false) {
@@ -11056,7 +11756,7 @@ function normalizeList(values, fallback = []) {
11056
11756
  }
11057
11757
 
11058
11758
  // src/commands/integrations.ts
11059
- import { readdir, readFile as readFile3, stat } from "fs/promises";
11759
+ import { readdir, readFile as readFile4, stat } from "fs/promises";
11060
11760
  import { join as join4, relative as relative3 } from "path";
11061
11761
  function registerIntegrationsCommand(program2) {
11062
11762
  const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
@@ -11200,7 +11900,7 @@ function detectFramework(packageJson2, haystack) {
11200
11900
  }
11201
11901
  async function readPackageJson2(repoRoot) {
11202
11902
  try {
11203
- return JSON.parse(await readFile3(join4(repoRoot, "package.json"), "utf8"));
11903
+ return JSON.parse(await readFile4(join4(repoRoot, "package.json"), "utf8"));
11204
11904
  } catch {
11205
11905
  return null;
11206
11906
  }
@@ -11223,7 +11923,7 @@ async function collectFiles(repoRoot) {
11223
11923
  if (info.size > 25e4) continue;
11224
11924
  result.push({
11225
11925
  path: relative3(root, fullPath),
11226
- content: await readFile3(fullPath, "utf8")
11926
+ content: await readFile4(fullPath, "utf8")
11227
11927
  });
11228
11928
  }
11229
11929
  }
@@ -11241,7 +11941,7 @@ function formatCheckResult(result) {
11241
11941
  // src/commands/skills.ts
11242
11942
  import { spawnSync } from "child_process";
11243
11943
  import { existsSync as existsSync4 } from "fs";
11244
- import { mkdir as mkdir3, copyFile, readFile as readFile4, writeFile as writeFile4, rm } from "fs/promises";
11944
+ import { mkdir as mkdir3, copyFile, readFile as readFile5, writeFile as writeFile5, rm } from "fs/promises";
11245
11945
  import { homedir as homedir2 } from "os";
11246
11946
  import { dirname as dirname4, join as join5, resolve as resolve3 } from "path";
11247
11947
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -11295,11 +11995,11 @@ function registerViaOfficialCli(cli, mcp, agentName) {
11295
11995
  return result.status === 0;
11296
11996
  }
11297
11997
  async function registerClaudeViaConfig(mcp) {
11298
- const path4 = join5(homedir2(), ".claude.json");
11998
+ const path5 = join5(homedir2(), ".claude.json");
11299
11999
  let config = {};
11300
- if (existsSync4(path4)) {
12000
+ if (existsSync4(path5)) {
11301
12001
  try {
11302
- config = JSON.parse(await readFile4(path4, "utf8"));
12002
+ config = JSON.parse(await readFile5(path5, "utf8"));
11303
12003
  } catch {
11304
12004
  config = {};
11305
12005
  }
@@ -11312,13 +12012,13 @@ async function registerClaudeViaConfig(mcp) {
11312
12012
  env: { TOPOLO_AGENT_NAME: AGENT_NAME_FOR.claude }
11313
12013
  };
11314
12014
  config.mcpServers = servers;
11315
- await writeFile4(path4, `${JSON.stringify(config, null, 2)}
12015
+ await writeFile5(path5, `${JSON.stringify(config, null, 2)}
11316
12016
  `, "utf8");
11317
12017
  }
11318
12018
  async function registerCodexViaConfig(mcp) {
11319
- const path4 = join5(homedir2(), ".codex", "config.toml");
12019
+ const path5 = join5(homedir2(), ".codex", "config.toml");
11320
12020
  let text = "";
11321
- if (existsSync4(path4)) text = await readFile4(path4, "utf8");
12021
+ if (existsSync4(path5)) text = await readFile5(path5, "utf8");
11322
12022
  text = text.replace(
11323
12023
  /(^|\n)\[mcp_servers\.topolo(?:\.[a-z]+)?\][^\[]*/g,
11324
12024
  (match) => match.startsWith("\n") ? "\n" : ""
@@ -11334,8 +12034,8 @@ command = "${mcp.command}"${argsToml}
11334
12034
  [mcp_servers.topolo.env]
11335
12035
  TOPOLO_AGENT_NAME = "${AGENT_NAME_FOR.codex}"
11336
12036
  `;
11337
- await mkdir3(dirname4(path4), { recursive: true });
11338
- await writeFile4(path4, `${text}${block}`, "utf8");
12037
+ await mkdir3(dirname4(path5), { recursive: true });
12038
+ await writeFile5(path5, `${text}${block}`, "utf8");
11339
12039
  }
11340
12040
  async function installForAgent(agent, sourceDir, mcp, opts) {
11341
12041
  const skillDir = agent === "claude" ? claudeSkillDir(opts.projectDir) : codexSkillDir();