@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.
- package/dist/index.js +918 -218
- 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
|
-
//
|
|
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,
|
|
65
|
-
super("http_error", message ?? `HTTP ${status} from ${service}${
|
|
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 =
|
|
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,
|
|
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,
|
|
2178
|
+
validateNode(schema, input3, "$", schema, errors, 0);
|
|
2179
2179
|
return { valid: errors.length === 0, errors };
|
|
2180
2180
|
}
|
|
2181
|
-
function inspectSchema(schema,
|
|
2182
|
-
if (depth > 128) return { path:
|
|
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:
|
|
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:
|
|
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:
|
|
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, `${
|
|
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,
|
|
2215
|
+
function validateNode(schema, value, path5, root, errors, depth) {
|
|
2216
2216
|
if (depth > 128) {
|
|
2217
|
-
errors.push({ path:
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
2258
|
-
if (typeof value === "number") validateNumber(schema, value,
|
|
2259
|
-
if (Array.isArray(value)) validateArray(schema, value,
|
|
2260
|
-
if (isRecord(value)) validateObject(schema, value,
|
|
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,
|
|
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:
|
|
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:
|
|
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: `${
|
|
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, `${
|
|
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, `${
|
|
2284
|
+
validateNode(childSchema, child, `${path5}.${key}`, root, errors, depth + 1);
|
|
2285
2285
|
} else if (schema.additionalProperties === false) {
|
|
2286
|
-
errors.push({ path: `${
|
|
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, `${
|
|
2288
|
+
validateNode(schema.additionalProperties, child, `${path5}.${key}`, root, errors, depth + 1);
|
|
2289
2289
|
}
|
|
2290
2290
|
}
|
|
2291
2291
|
}
|
|
2292
|
-
function validateArray(schema, value,
|
|
2292
|
+
function validateArray(schema, value, path5, root, errors, depth) {
|
|
2293
2293
|
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
2294
|
-
errors.push({ path:
|
|
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:
|
|
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:
|
|
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, `${
|
|
2304
|
+
value.forEach((item, index) => validateNode(schema.items, item, `${path5}[${index}]`, root, errors, depth + 1));
|
|
2305
2305
|
}
|
|
2306
2306
|
}
|
|
2307
|
-
function validateString(schema, value,
|
|
2307
|
+
function validateString(schema, value, path5, errors) {
|
|
2308
2308
|
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
2309
|
-
errors.push({ path:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
2322
|
-
if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path:
|
|
2323
|
-
if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path:
|
|
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,
|
|
2326
|
-
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path:
|
|
2327
|
-
if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
2849
|
-
if (
|
|
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:
|
|
2855
|
-
actionCount: Number(
|
|
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,
|
|
2892
|
+
async callAction(action, input3 = {}, options = {}) {
|
|
2893
2893
|
const entry = await this.getAction(action);
|
|
2894
|
-
return this.executeResolvedAction(entry,
|
|
2894
|
+
return this.executeResolvedAction(entry, input3, options);
|
|
2895
2895
|
}
|
|
2896
|
-
async validateActionInput(action,
|
|
2896
|
+
async validateActionInput(action, input3 = {}) {
|
|
2897
2897
|
const entry = await this.getAction(action);
|
|
2898
|
-
return { action: entry, validation: validateTopoloActionInput(entry.inputSchema,
|
|
2898
|
+
return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input3) };
|
|
2899
2899
|
}
|
|
2900
|
-
async planAction(action,
|
|
2900
|
+
async planAction(action, input3 = {}, options = {}) {
|
|
2901
2901
|
const entry = await this.getAction(action);
|
|
2902
|
-
const validation = validateTopoloActionInput(entry.inputSchema,
|
|
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,
|
|
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,
|
|
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,
|
|
2939
|
+
return this.executeResolvedAction(entry, input3, options);
|
|
2940
2940
|
}
|
|
2941
|
-
async executeResolvedAction(entry,
|
|
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,
|
|
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,
|
|
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(
|
|
3054
|
-
if (!isRecord2(
|
|
3055
|
-
const resourceType = readString(
|
|
3056
|
-
const label = readString(
|
|
3057
|
-
const emptyLabel = readString(
|
|
3058
|
-
const resourceConcept = readString(
|
|
3059
|
-
const canonicalResourceType = readString(
|
|
3060
|
-
const defaultEntitlement = readString(
|
|
3061
|
-
const exceptionPolicy = readString(
|
|
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(
|
|
3076
|
-
if (!Array.isArray(
|
|
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
|
|
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(
|
|
3088
|
-
if (!isRecord2(
|
|
3089
|
-
const resourceType = readString(
|
|
3090
|
-
const resourceId = readString(
|
|
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(
|
|
3096
|
-
slug: readOptionalString(
|
|
3097
|
-
status: readOptionalString(
|
|
3098
|
-
organizationId: readOptionalString(
|
|
3099
|
-
metadata: isRecord2(
|
|
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(
|
|
3103
|
-
if (!Array.isArray(
|
|
3104
|
-
return
|
|
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
|
|
3231
|
-
if (!actionId || !name || !appId || !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:
|
|
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,
|
|
3281
|
-
const remaining = { ...
|
|
3282
|
-
const
|
|
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:
|
|
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:
|
|
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(
|
|
3477
|
-
const url = new URL(
|
|
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",
|
|
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
|
-
//
|
|
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,
|
|
3532
|
-
super("http_error", message ?? `HTTP ${status} from ${service}${
|
|
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 =
|
|
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(
|
|
3669
|
-
const url = new URL(
|
|
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",
|
|
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
|
|
3716
|
-
if (!existsSync(
|
|
3715
|
+
const path5 = configPath();
|
|
3716
|
+
if (!existsSync(path5)) return {};
|
|
3717
3717
|
try {
|
|
3718
|
-
const raw = await readFile(
|
|
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
|
|
3726
|
-
await mkdir(dirname(
|
|
3727
|
-
await writeFile(
|
|
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(
|
|
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
|
|
4050
|
+
for (const path5 of candidates) {
|
|
4050
4051
|
try {
|
|
4051
|
-
const pkg = JSON.parse(readFileSync(
|
|
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,
|
|
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:
|
|
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
|
|
4543
|
+
const input3 = parseJsonObject(opts.data || "{}");
|
|
4543
4544
|
const resource = parseResourceOption(opts.resourceType, opts.resourceId);
|
|
4544
|
-
const result = await topolo.client.callAction(action,
|
|
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
|
|
4614
|
-
if (!
|
|
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(
|
|
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 ${
|
|
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
|
|
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((
|
|
5286
|
-
const 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
|
|
5553
|
-
const scripts =
|
|
5554
|
-
const isRootPackage =
|
|
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(
|
|
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((
|
|
5568
|
-
...Object.keys(
|
|
5569
|
-
...Object.keys(
|
|
5570
|
-
...Object.keys(
|
|
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((
|
|
5577
|
+
evidence.packageJsons.flatMap((record2) => {
|
|
5577
5578
|
const specs = [
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
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(
|
|
5707
|
-
return
|
|
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
|
|
5797
|
-
if (!
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
5945
|
+
async function pathExists(path5) {
|
|
5936
5946
|
try {
|
|
5937
|
-
await access(
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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).
|
|
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
|
|
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 =
|
|
8995
|
-
const
|
|
8996
|
-
const
|
|
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/
|
|
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
|
|
9767
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9968
|
-
emit(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
10114
|
-
const evidencePath =
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
10535
|
-
const relativePath =
|
|
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 ===
|
|
10545
|
-
if (relativePath.includes(`${
|
|
10546
|
-
if (relativePath.startsWith(`scripts${
|
|
10547
|
-
return !RUNTIME_EXTENSIONS.has(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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()}${
|
|
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(
|
|
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}${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
11998
|
+
const path5 = join5(homedir2(), ".claude.json");
|
|
11299
11999
|
let config = {};
|
|
11300
|
-
if (existsSync4(
|
|
12000
|
+
if (existsSync4(path5)) {
|
|
11301
12001
|
try {
|
|
11302
|
-
config = JSON.parse(await
|
|
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
|
|
12015
|
+
await writeFile5(path5, `${JSON.stringify(config, null, 2)}
|
|
11316
12016
|
`, "utf8");
|
|
11317
12017
|
}
|
|
11318
12018
|
async function registerCodexViaConfig(mcp) {
|
|
11319
|
-
const
|
|
12019
|
+
const path5 = join5(homedir2(), ".codex", "config.toml");
|
|
11320
12020
|
let text = "";
|
|
11321
|
-
if (existsSync4(
|
|
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(
|
|
11338
|
-
await
|
|
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();
|