@topolo/cli 0.4.5 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +921 -631
  2. package/package.json +2 -6
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
- // ../topolo-sdk/dist/index.js
12
+ // node_modules/@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);
@@ -75,6 +75,7 @@ var APP_API_URLS = {
75
75
  auth: "https://auth.topolo.app",
76
76
  backup: "https://backup.topolo.app",
77
77
  books: "https://books.topolo.app",
78
+ brand: "https://brand.topolo.app",
78
79
  bugfix: "https://bugfix.topolo.app",
79
80
  bytes: "https://bytes.topolo.app",
80
81
  calendar: "https://calendar.topolo.app",
@@ -255,6 +256,38 @@ var APPLICATIONS = {
255
256
  }
256
257
  ]
257
258
  },
259
+ "brand": {
260
+ "id": "brand",
261
+ "appDir": "TopoloBrand",
262
+ "name": "topolo-brand",
263
+ "packageName": "@topolo/topolo-brand",
264
+ "productionUrl": "https://brand.topolo.app",
265
+ "services": [
266
+ "brand"
267
+ ],
268
+ "deployTargets": [
269
+ {
270
+ "name": "topolo-brand",
271
+ "kind": "worker",
272
+ "apiUrl": "https://brand.topolo.app",
273
+ "domains": [
274
+ "brand.topolo.app"
275
+ ],
276
+ "routes": [
277
+ "brand.topolo.app"
278
+ ],
279
+ "appId": "brand"
280
+ },
281
+ {
282
+ "name": "topolo-brand-api",
283
+ "kind": "worker",
284
+ "apiUrl": null,
285
+ "domains": [],
286
+ "routes": [],
287
+ "appId": null
288
+ }
289
+ ]
290
+ },
258
291
  "bugfix": {
259
292
  "id": "bugfix",
260
293
  "appDir": "TopoloBugFix",
@@ -2369,6 +2402,8 @@ var TopoloClient = class _TopoloClient {
2369
2402
  requireConfirmForWrites;
2370
2403
  timeoutMs;
2371
2404
  fetchImpl;
2405
+ credentialProvider;
2406
+ credentialLoadInFlight = null;
2372
2407
  refreshCredential;
2373
2408
  refreshInFlight = null;
2374
2409
  debug;
@@ -2396,6 +2431,7 @@ var TopoloClient = class _TopoloClient {
2396
2431
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
2397
2432
  this.timeoutMs = options.timeoutMs ?? 3e4;
2398
2433
  this.fetchImpl = options.fetch ?? fetch;
2434
+ this.credentialProvider = options.credentialProvider;
2399
2435
  this.refreshCredential = options.refreshCredential;
2400
2436
  this.debug = options.debug;
2401
2437
  }
@@ -2418,6 +2454,7 @@ var TopoloClient = class _TopoloClient {
2418
2454
  `Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
2419
2455
  );
2420
2456
  }
2457
+ await this.loadCurrentCredential();
2421
2458
  const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
2422
2459
  const baseUrl = resolvedService.baseUrl;
2423
2460
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
@@ -2529,6 +2566,19 @@ var TopoloClient = class _TopoloClient {
2529
2566
  });
2530
2567
  return parsed;
2531
2568
  }
2569
+ async loadCurrentCredential() {
2570
+ if (!this.credentialProvider) return;
2571
+ this.credentialLoadInFlight ??= this.credentialProvider().then((credential) => {
2572
+ if (!credential) {
2573
+ throw new TopoloAuthError("The configured Topolo credential is no longer available.");
2574
+ }
2575
+ this.credential = credential;
2576
+ return credential;
2577
+ }).finally(() => {
2578
+ this.credentialLoadInFlight = null;
2579
+ });
2580
+ await this.credentialLoadInFlight;
2581
+ }
2532
2582
  async refreshRejectedAccessToken(attemptedAccessToken) {
2533
2583
  if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
2534
2584
  return this.credential;
@@ -3197,6 +3247,7 @@ function normalizeActionCatalogEntry(item) {
3197
3247
  permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
3198
3248
  requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
3199
3249
  authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
3250
+ allowedServices: stringArray(a.allowed_services ?? a.allowedServices),
3200
3251
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
3201
3252
  outputSchema: plainObject(a.output_schema ?? a.outputSchema),
3202
3253
  agentContract: plainObject(a.agent_contract ?? a.agentContract),
@@ -3218,7 +3269,7 @@ function pickAgentAccess(value) {
3218
3269
  return value === "auto" || value === "confirm" || value === "off" ? value : null;
3219
3270
  }
3220
3271
  function pickAuthorizationMode(value) {
3221
- return value === "runtime" ? "runtime" : "static";
3272
+ return value === "runtime" || value === "public" || value === "service" ? value : "static";
3222
3273
  }
3223
3274
  function plainObject(value) {
3224
3275
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -3312,6 +3363,7 @@ var CrmModule = class {
3312
3363
  constructor(client) {
3313
3364
  this.client = client;
3314
3365
  }
3366
+ client;
3315
3367
  async listContacts(options = {}) {
3316
3368
  const res = await this.client.request({
3317
3369
  service: "crm",
@@ -3357,6 +3409,7 @@ var IdentityModule = class {
3357
3409
  constructor(client) {
3358
3410
  this.client = client;
3359
3411
  }
3412
+ client;
3360
3413
  /** Returns the resolved user, organization, and permission set for the
3361
3414
  * current credential. Equivalent to `TopoloClient.introspect()` — exposed
3362
3415
  * on this module for symmetry with the other domain modules. */
@@ -3451,7 +3504,7 @@ function createTopolo(options) {
3451
3504
  };
3452
3505
  }
3453
3506
 
3454
- // ../topolo-sdk/dist/node/credential-store.js
3507
+ // node_modules/@topolo/sdk/dist/node/credential-store.js
3455
3508
  import { mkdir, readFile, writeFile, chmod } from "fs/promises";
3456
3509
  import { existsSync } from "fs";
3457
3510
  import { homedir, platform } from "os";
@@ -3489,6 +3542,7 @@ var APP_API_URLS2 = {
3489
3542
  auth: "https://auth.topolo.app",
3490
3543
  backup: "https://backup.topolo.app",
3491
3544
  books: "https://books.topolo.app",
3545
+ brand: "https://brand.topolo.app",
3492
3546
  bugfix: "https://bugfix.topolo.app",
3493
3547
  bytes: "https://bytes.topolo.app",
3494
3548
  calendar: "https://calendar.topolo.app",
@@ -5738,6 +5792,33 @@ import { dirname as dirname3, resolve as resolve2 } from "path";
5738
5792
  import { constants } from "fs";
5739
5793
  import { access } from "fs/promises";
5740
5794
  import { relative, resolve } from "path";
5795
+ function normalizeHomePath(value) {
5796
+ const path4 = String(value || "/dashboard").trim();
5797
+ if (!path4.startsWith("/") || path4.startsWith("//") || /[?#]/.test(path4)) {
5798
+ throw new Error("Invalid home path. Use an absolute application path such as /dashboard.");
5799
+ }
5800
+ if (["/", "/login", "/auth/callback", "/getting-started"].includes(path4)) {
5801
+ throw new Error("Home path must be a protected product route, not a reserved entry route.");
5802
+ }
5803
+ return path4.replace(/\/$/, "") || "/dashboard";
5804
+ }
5805
+ function normalizeHomeLabel(value) {
5806
+ const label = String(value || "Dashboard").trim();
5807
+ if (!label || label.length > 80) {
5808
+ throw new Error("Home label must contain between 1 and 80 characters.");
5809
+ }
5810
+ return label;
5811
+ }
5812
+ function normalizeNavigationMode(value) {
5813
+ const mode = String(value || "sidebar").trim();
5814
+ if (mode === "sidebar" || mode === "topbar" || mode === "none") return mode;
5815
+ throw new Error("Invalid navigation mode. Use sidebar, topbar, or none.");
5816
+ }
5817
+ function normalizeWorkspaceSwitcherMode(value) {
5818
+ const mode = String(value || "required").trim();
5819
+ if (mode === "required" || mode === "optional" || mode === "off") return mode;
5820
+ throw new Error("Invalid workspace switcher mode. Use required, optional, or off.");
5821
+ }
5741
5822
  function normalizeTarget(value) {
5742
5823
  if (!value) return "platform";
5743
5824
  if (value === "platform" || value === "developer") return value;
@@ -5880,18 +5961,60 @@ var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
5880
5961
  var DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID = "0ebdeec262a245481a721fc4f9d6d798";
5881
5962
  var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
5882
5963
 
5964
+ // src/commands/app-scaffold-package-policy.ts
5965
+ var TOPOLO_PACKAGE_SCAFFOLD_POLICY = {
5966
+ "@topolo-io/api-key-management": "browser-baseline",
5967
+ "@topolo-io/app-funnel": "browser-baseline",
5968
+ "@topolo-io/app-launcher": "browser-baseline",
5969
+ "@topolo-io/app-manifest": "domain-opt-in",
5970
+ "@topolo-io/app-shell": "browser-baseline",
5971
+ "@topolo-io/auth-client": "browser-baseline",
5972
+ "@topolo-io/auth-middleware": "api-baseline",
5973
+ "@topolo-io/billing-manifest": "domain-opt-in",
5974
+ "@topolo-io/billing-ui": "domain-opt-in",
5975
+ "@topolo-io/bridge": "domain-opt-in",
5976
+ "@topolo-io/calendar": "domain-opt-in",
5977
+ "@topolo-io/chat-widget": "browser-baseline",
5978
+ "@topolo-io/data": "domain-opt-in",
5979
+ "@topolo-io/design-tokens": "browser-baseline",
5980
+ "@topolo-io/error-middleware": "api-baseline",
5981
+ "@topolo-io/mailbox": "domain-opt-in",
5982
+ "@topolo-io/mobile-error-middleware": "domain-opt-in",
5983
+ "@topolo-io/mobile-experience-contracts": "domain-opt-in",
5984
+ "@topolo-io/nexus-client": "api-baseline",
5985
+ "@topolo-io/node-canvas": "domain-opt-in",
5986
+ "@topolo-io/notifications": "browser-baseline",
5987
+ "@topolo-io/onboarding": "browser-baseline",
5988
+ "@topolo-io/platform-mcp": "tooling",
5989
+ "@topolo-io/resource-context": "browser-baseline",
5990
+ "@topolo-io/security-headers": "api-baseline",
5991
+ "@topolo-io/tour": "browser-baseline",
5992
+ "@topolo-io/ui-kit": "browser-baseline",
5993
+ "@topolo-io/widget-server": "api-baseline",
5994
+ "@topolo-io/worker-runtime": "api-baseline",
5995
+ "@topolo/cli": "tooling",
5996
+ "@topolo/login": "developer-baseline",
5997
+ "@topolo/mcp": "tooling",
5998
+ "@topolo/sdk": "api-baseline"
5999
+ };
6000
+ function packagesForScaffoldClass(scaffoldClass) {
6001
+ return Object.entries(TOPOLO_PACKAGE_SCAFFOLD_POLICY).filter(([, value]) => value === scaffoldClass).map(([name]) => name).sort();
6002
+ }
6003
+ var PLATFORM_BROWSER_TOPOLO_PACKAGES = packagesForScaffoldClass("browser-baseline");
6004
+ var PLATFORM_API_TOPOLO_PACKAGES = packagesForScaffoldClass("api-baseline");
6005
+
5883
6006
  // src/commands/app-scaffold-package-template.ts
5884
6007
  function packageJson(application, target, options) {
5885
6008
  const dependencies = {};
5886
6009
  const devDependencies = {
5887
6010
  "@types/node": "^20.12.0",
5888
6011
  typescript: "^5.4.0",
5889
- vite: "^5.4.0"
6012
+ ...options.browser ? { vite: "^5.4.0" } : {}
5890
6013
  };
5891
6014
  const scripts = {
5892
- dev: target === "platform" ? "vite --config apps/web/vite.config.ts --host 0.0.0.0" : "vite --host 0.0.0.0",
5893
- build: target === "platform" ? "vite build --config apps/web/vite.config.ts" : "vite build",
5894
- preview: target === "platform" ? "vite preview --config apps/web/vite.config.ts --host 0.0.0.0" : "vite preview --host 0.0.0.0",
6015
+ dev: target === "platform" ? options.browser ? "vite --config apps/web/vite.config.ts --host 0.0.0.0" : "wrangler dev --config apps/api/wrangler.toml" : "vite --host 0.0.0.0",
6016
+ build: target === "platform" ? options.browser ? "vite build --config apps/web/vite.config.ts" : 'wrangler deploy --config apps/api/wrangler.toml --env="" --dry-run --outdir dist' : "vite build",
6017
+ preview: target === "platform" ? options.browser ? "vite preview --config apps/web/vite.config.ts --host 0.0.0.0" : "wrangler dev --config apps/api/wrangler.toml" : "vite preview --host 0.0.0.0",
5895
6018
  typecheck: "tsc --noEmit"
5896
6019
  };
5897
6020
  if (options.browser) {
@@ -5901,29 +6024,20 @@ function packageJson(application, target, options) {
5901
6024
  devDependencies["@types/react-dom"] = "^19.2.2";
5902
6025
  devDependencies["@vitejs/plugin-react"] = "^4.7.0";
5903
6026
  if (target === "platform") {
5904
- dependencies["@topolo-io/auth-client"] = topoloPackageVersion("@topolo-io/auth-client", target);
5905
- dependencies["@topolo-io/app-funnel"] = topoloPackageVersion("@topolo-io/app-funnel", target);
5906
- dependencies["@topolo-io/app-launcher"] = topoloPackageVersion("@topolo-io/app-launcher", target);
5907
- dependencies["@topolo-io/app-shell"] = topoloPackageVersion("@topolo-io/app-shell", target);
5908
- dependencies["@topolo-io/design-tokens"] = topoloPackageVersion("@topolo-io/design-tokens", target);
5909
- dependencies["@topolo-io/onboarding"] = topoloPackageVersion("@topolo-io/onboarding", target);
5910
- dependencies["@topolo-io/tour"] = topoloPackageVersion("@topolo-io/tour", target);
5911
- dependencies["@topolo-io/ui-kit"] = topoloPackageVersion("@topolo-io/ui-kit", target);
6027
+ for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) {
6028
+ dependencies[packageName] = topoloPackageVersion(packageName, target);
6029
+ }
5912
6030
  dependencies["react-router-dom"] = "^7.6.2";
5913
6031
  } else {
5914
6032
  dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
5915
6033
  }
5916
6034
  }
5917
6035
  if (options.api) {
5918
- devDependencies["@cloudflare/workers-types"] = "^4.20260413.1";
6036
+ devDependencies["@cloudflare/workers-types"] = "^5.20260716.1";
5919
6037
  if (target === "platform") {
5920
- dependencies["@topolo-io/worker-runtime"] = topoloPackageVersion("@topolo-io/worker-runtime", target);
5921
- dependencies["@topolo-io/auth-middleware"] = topoloPackageVersion("@topolo-io/auth-middleware", target);
5922
- dependencies["@topolo-io/error-middleware"] = topoloPackageVersion("@topolo-io/error-middleware", target);
5923
- dependencies["@topolo-io/security-headers"] = topoloPackageVersion("@topolo-io/security-headers", target);
5924
- dependencies["@topolo-io/resource-context"] = topoloPackageVersion("@topolo-io/resource-context", target);
5925
- dependencies["@topolo-io/data"] = topoloPackageVersion("@topolo-io/data", target);
5926
- dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
6038
+ for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) {
6039
+ dependencies[packageName] = topoloPackageVersion(packageName, target);
6040
+ }
5927
6041
  dependencies.hono = "^4";
5928
6042
  dependencies.zod = "^4.4.3";
5929
6043
  scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
@@ -5942,7 +6056,7 @@ function packageJson(application, target, options) {
5942
6056
  if (testDirs.length > 0) {
5943
6057
  const chain = (withCoverage) => testDirs.map((dir, index) => {
5944
6058
  const enter = index === 0 ? `cd ${dir}` : `cd ../${dir.split("/").pop()}`;
5945
- return `${enter} && vitest run --config vitest.config.ts --passWithNoTests${withCoverage ? " --coverage" : ""}`;
6059
+ return `${enter} && vitest run --config vitest.config.ts${withCoverage ? " --coverage" : ""}`;
5946
6060
  }).join(" && ");
5947
6061
  scripts.test = chain(false);
5948
6062
  scripts.coverage = chain(true);
@@ -5957,13 +6071,18 @@ function packageJson(application, target, options) {
5957
6071
  }
5958
6072
  }
5959
6073
  if (target === "platform" && hasScaffoldResources(options.resources)) {
5960
- scripts["check:deploy-config"] = "wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build && wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build";
6074
+ const deployChecks = [
6075
+ ...options.api ? ["wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build"] : [],
6076
+ ...options.browser ? ["wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build"] : []
6077
+ ];
6078
+ scripts["check:deploy-config"] = deployChecks.join(" && ");
5961
6079
  }
5962
6080
  return `${json({
5963
6081
  name: application.packageName,
5964
6082
  version: "0.1.0",
5965
6083
  private: true,
5966
6084
  type: "module",
6085
+ ...target === "platform" ? { packageManager: "pnpm@10.17.1" } : {},
5967
6086
  scripts,
5968
6087
  dependencies,
5969
6088
  devDependencies
@@ -5977,29 +6096,8 @@ function topoloPackageVersion(packageName, target) {
5977
6096
  if (packageName === "@topolo/sdk") return "^0.8.1";
5978
6097
  return "^0.1.0";
5979
6098
  }
5980
- const platformPackageVersions = {
5981
- "@topolo/sdk": "^0.8.1",
5982
- "@topolo/login": "^0.1.0",
5983
- "@topolo-io/app-funnel": "^0.1.6",
5984
- "@topolo-io/app-launcher": "^0.1.15",
5985
- "@topolo-io/app-shell": "^0.5.54",
5986
- "@topolo-io/auth-client": "^1.2.1",
5987
- "@topolo-io/auth-middleware": "^0.4.0",
5988
- "@topolo-io/error-middleware": "^0.6.0",
5989
- "@topolo-io/security-headers": "^0.2.2",
5990
- "@topolo-io/resource-context": "^0.3.0",
5991
- "@topolo-io/data": "^0.1.2",
5992
- "@topolo-io/worker-runtime": "^0.1.11",
5993
- "@topolo-io/design-tokens": "^0.1.8",
5994
- "@topolo-io/onboarding": "^0.9.5",
5995
- "@topolo-io/tour": "^0.3.4",
5996
- "@topolo-io/ui-kit": "^0.4.233"
5997
- };
5998
- const version = platformPackageVersions[packageName];
5999
- if (!version) {
6000
- throw new Error(`Unknown Topolo platform package: ${packageName}`);
6001
- }
6002
- return version;
6099
+ if (packageName.startsWith("@topolo/") || packageName.startsWith("@topolo-io/")) return "latest";
6100
+ throw new Error(`Unknown Topolo platform package: ${packageName}`);
6003
6101
  }
6004
6102
  function hasScaffoldResources(resources) {
6005
6103
  return resources.d1 || resources.r2 || resources.queue || resources.workflow || resources.containerProcessing;
@@ -6074,10 +6172,11 @@ function readme(application, target, options) {
6074
6172
  "## Development",
6075
6173
  "",
6076
6174
  "```bash",
6077
- "npm install",
6078
- "npm run typecheck",
6079
- "npm run build",
6080
- "npm run dev",
6175
+ target === "platform" ? "pnpm install" : "npm install",
6176
+ target === "platform" ? "pnpm run typecheck" : "npm run typecheck",
6177
+ target === "platform" ? "pnpm run coverage" : "npm run build",
6178
+ target === "platform" ? "pnpm run build" : "npm run dev",
6179
+ ...target === "platform" ? ["pnpm run dev"] : [],
6081
6180
  "```",
6082
6181
  ""
6083
6182
  ];
@@ -6090,11 +6189,15 @@ function readme(application, target, options) {
6090
6189
  "- `@topolo-io/app-launcher` provides shared app switching and handoff helpers.",
6091
6190
  "- `@topolo-io/ui-kit` provides primitive account, command palette, icon, transition, and bug reporter UI.",
6092
6191
  "- `@topolo-io/auth-client` owns browser auth, callback exchange, token refresh, and logout.",
6093
- "- 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 workspace shell.",
6192
+ "- `@topolo-io/api-key-management`, `@topolo-io/chat-widget`, `@topolo-io/notifications`, and `@topolo-io/resource-context` complete the AppShell capability baseline.",
6193
+ "- Every Topolo-owned browser dependency is emitted at `latest`; product-domain packages are added only when the app actually adopts that domain.",
6194
+ "- `.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.",
6195
+ "- 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.",
6196
+ "- 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.",
6094
6197
  "- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
6095
6198
  );
6096
6199
  if (options.api) {
6097
- lines.push("- `@topolo-io/worker-runtime` owns Worker auth validation, `/api/auth/*` gatewaying, security headers, request ids, Nexus API-call usage monitoring, and BugFix reporting.");
6200
+ lines.push("- `@topolo-io/worker-runtime` plus its auth, error, Nexus, security-header, and widget-server suite own Worker validation, request ids, usage monitoring, widget responses, and BugFix reporting.");
6098
6201
  }
6099
6202
  if (hasScaffoldResources(options.resources)) {
6100
6203
  lines.push(
@@ -6297,6 +6400,15 @@ function gitignore() {
6297
6400
  ""
6298
6401
  ].join("\n");
6299
6402
  }
6403
+ function npmrc() {
6404
+ return [
6405
+ "@topolo-io:registry=https://npm.pkg.github.com",
6406
+ "strict-peer-dependencies=true",
6407
+ "minimum-release-age-exclude[]=@topolo-io/*",
6408
+ "minimum-release-age-exclude[]=@topolo/*",
6409
+ ""
6410
+ ].join("\n");
6411
+ }
6300
6412
  function envDts(api) {
6301
6413
  const lines = [
6302
6414
  '/// <reference types="vite/client" />',
@@ -6557,46 +6669,6 @@ function stylesCss() {
6557
6669
  "textarea {",
6558
6670
  " font: inherit;",
6559
6671
  "}",
6560
- "",
6561
- ".topolo-starter-workspace {",
6562
- " display: grid;",
6563
- " gap: 18px;",
6564
- " min-width: 0;",
6565
- " width: auto;",
6566
- " max-width: min(100%, calc(100vw - 56px));",
6567
- " overflow-x: clip;",
6568
- "}",
6569
- "",
6570
- ".topolo-starter-card-stack {",
6571
- " display: grid;",
6572
- " gap: 18px;",
6573
- " min-width: 0;",
6574
- "}",
6575
- "",
6576
- ".topolo-starter-workspace > *,",
6577
- ".topolo-starter-card-stack > * {",
6578
- " min-width: 0;",
6579
- "}",
6580
- "",
6581
- ".topolo-starter-card h2 {",
6582
- " margin: 0 0 12px;",
6583
- " font-size: 24px;",
6584
- " line-height: 1.2;",
6585
- "}",
6586
- "",
6587
- ".topolo-starter-card p {",
6588
- " margin: 0;",
6589
- " color: hsl(var(--muted-foreground));",
6590
- " line-height: 1.55;",
6591
- "}",
6592
- "",
6593
- ".topolo-starter-card {",
6594
- " border: 1px solid hsl(var(--border));",
6595
- " border-radius: var(--radius);",
6596
- " padding: 20px;",
6597
- " background: hsl(var(--card));",
6598
- " color: hsl(var(--card-foreground));",
6599
- "}",
6600
6672
  ""
6601
6673
  ].join("\n");
6602
6674
  }
@@ -6604,450 +6676,429 @@ function stylesCss() {
6604
6676
  // src/commands/app-scaffold-app-template.ts
6605
6677
  function appTsx(application, target, options) {
6606
6678
  if (target === "developer") return developerAppTsx(application);
6607
- return platformAppTsx(application, options);
6608
- }
6609
- function platformAppTsx(application, options) {
6610
- const isPersonal = options.audience === "personal";
6611
- const tagline = isPersonal ? "Topolo personal app" : "Topolo workspace";
6612
- const workspaceTitle = isPersonal ? "Personal app" : "Workspace";
6613
- const shellLabel = isPersonal ? "personal app shell" : "workspace shell";
6614
- const baselineHeading = isPersonal ? "Shared personal baseline" : "Shared workspace baseline";
6615
- const baselineDescription = isPersonal ? "This scaffold starts on the shared landing and login surfaces, completes sign-in on the shared callback path, and renders the signed personal app inside the shared Topolo shell." : "This scaffold starts on the shared landing and login surfaces, completes sign-in on the shared callback path, and renders the signed workspace inside the shared Topolo shell.";
6679
+ return platformAppTsx();
6680
+ }
6681
+ function platformAppTsx() {
6682
+ return `import { useEffect, useMemo, useState } from 'react';
6683
+ import { LandingPage } from '@topolo-io/app-funnel';
6684
+ import { LoginPage, TopoloAppLoadingScaffold, TopoloAppRoot, TopoloAuthFlowScreen, createTopoloAuth } from '@topolo-io/app-shell';
6685
+ import { setupBugReporterErrorCapture } from '@topolo-io/ui-kit';
6686
+ import AppErrorBoundary from './AppErrorBoundary';
6687
+ import { APP_NAME, AUTH_URL, HOME_PATH, LANDING_CONFIG, LOGIN_CONFIG, NAVIGATION_MODE, WORKSPACE_RESOURCE } from './app-config';
6688
+ import { createAppRoutes } from './app-routes';
6689
+ import { resolveAppId } from './app-identity';
6690
+
6691
+ setupBugReporterErrorCapture();
6692
+
6693
+ export default function App() {
6694
+ return (
6695
+ <AppErrorBoundary>
6696
+ <CanonicalAppRoot />
6697
+ </AppErrorBoundary>
6698
+ );
6699
+ }
6700
+
6701
+ export function CanonicalAppRoot() {
6702
+ const [appId, setAppId] = useState('');
6703
+ const [appIdentityError, setAppIdentityError] = useState('');
6704
+
6705
+ useEffect(() => {
6706
+ let active = true;
6707
+ setAppIdentityError('');
6708
+ void resolveAppId()
6709
+ .then((nextAppId) => {
6710
+ if (active) setAppId(nextAppId);
6711
+ })
6712
+ .catch((error) => {
6713
+ if (active) setAppIdentityError(errorMessage(error));
6714
+ });
6715
+ return () => {
6716
+ active = false;
6717
+ };
6718
+ }, []);
6719
+
6720
+ const auth = useMemo(() => appId ? createTopoloAuth({
6721
+ authServiceUrl: AUTH_URL,
6722
+ appId,
6723
+ devSessionKey: \`\${appId}_session\`,
6724
+ callbackPath: '/auth/callback',
6725
+ sessionStorageRestoreEnabled: import.meta.env.DEV,
6726
+ debug: import.meta.env.DEV,
6727
+ }) : null, [appId]);
6728
+ const routes = useMemo(() => appId ? createAppRoutes(appId) : [], [appId]);
6729
+
6730
+ if (appIdentityError) {
6731
+ return (
6732
+ <TopoloAuthFlowScreen
6733
+ appName={APP_NAME}
6734
+ status="error"
6735
+ title="App identity unavailable"
6736
+ message={appIdentityError}
6737
+ actionLabel="Retry"
6738
+ onAction={() => window.location.reload()}
6739
+ />
6740
+ );
6741
+ }
6742
+
6743
+ if (!auth) {
6744
+ return (
6745
+ <TopoloAppLoadingScaffold
6746
+ appId={appId}
6747
+ appName={APP_NAME}
6748
+ layout={{ shell: NAVIGATION_MODE, content: 'workspace' }}
6749
+ />
6750
+ );
6751
+ }
6752
+
6753
+ return (
6754
+ <TopoloAppRoot
6755
+ appName={APP_NAME}
6756
+ appId={appId}
6757
+ auth={auth}
6758
+ routes={routes}
6759
+ home={HOME_PATH}
6760
+ workspaceResource={WORKSPACE_RESOURCE}
6761
+ gettingStarted={{ path: '/getting-started' }}
6762
+ publicLanding={
6763
+ <LandingPage
6764
+ appId={appId}
6765
+ authApiUrl={AUTH_URL}
6766
+ config={LANDING_CONFIG}
6767
+ onCTAClick={() => window.location.assign('/login')}
6768
+ />
6769
+ }
6770
+ loginElement={
6771
+ <LoginPage
6772
+ appId={appId}
6773
+ authApiUrl={AUTH_URL}
6774
+ config={LOGIN_CONFIG}
6775
+ onLogin={async (provider) => {
6776
+ await auth.login(provider || 'topolo');
6777
+ }}
6778
+ onPasswordLogin={async (request) => {
6779
+ const result = await auth.loginWithPassword(request);
6780
+ if (!result.authenticated) return result;
6781
+ window.dispatchEvent(new CustomEvent('auth:session-updated', { detail: { tokens: result.tokens, user: result.user } }));
6782
+ return undefined;
6783
+ }}
6784
+ />
6785
+ }
6786
+ loadingFallback={({ loading }) => (
6787
+ <TopoloAppLoadingScaffold
6788
+ appId={appId}
6789
+ appName={APP_NAME}
6790
+ layout={{ shell: loading?.shell || NAVIGATION_MODE, content: loading?.content || 'workspace' }}
6791
+ />
6792
+ )}
6793
+ shell={{
6794
+ appId,
6795
+ authUrl: AUTH_URL,
6796
+ navigationMode: NAVIGATION_MODE,
6797
+ title: APP_NAME,
6798
+ commandPlaceholder: \`Search \${APP_NAME}\`,
6799
+ bugReporterAppName: APP_NAME,
6800
+ enableCommandPalette: true,
6801
+ enableBugReporter: true,
6802
+ }}
6803
+ />
6804
+ );
6805
+ }
6806
+
6807
+ export function errorMessage(error: unknown): string {
6808
+ if (error instanceof Error && error.message) return error.message;
6809
+ return 'Unexpected authentication error.';
6810
+ }
6811
+ `;
6812
+ }
6813
+ function platformAppConfigTs(application, experience) {
6814
+ const appName = toTitle(application.name);
6815
+ const workspaceResource = experience.workspaceSwitcher === "off" ? "{ enabled: false }" : `{ enabled: true, required: ${experience.workspaceSwitcher === "required"}, label: '${escapeSingleQuoted(experience.workspaceLabel)}' }`;
6816
+ return `import { SCAFFOLD_APP_NAME, getBrowserAuthBaseUrl } from './app-identity';
6817
+
6818
+ export const APP_NAME = SCAFFOLD_APP_NAME;
6819
+ export const AUTH_URL = getBrowserAuthBaseUrl();
6820
+ export const HOME_PATH = '${escapeSingleQuoted(experience.homePath)}';
6821
+ export const HOME_LABEL = '${escapeSingleQuoted(experience.homeLabel)}';
6822
+ export const NAVIGATION_MODE = '${experience.navigationMode}' as const;
6823
+ export const WORKSPACE_RESOURCE = ${workspaceResource} as const;
6824
+ export const PRODUCT_TAGLINE = '${escapeSingleQuoted(experience.tagline)}';
6825
+
6826
+ export const LANDING_CONFIG = {
6827
+ app_name: APP_NAME,
6828
+ app_tagline: PRODUCT_TAGLINE,
6829
+ hero_title: APP_NAME,
6830
+ hero_subtitle: PRODUCT_TAGLINE,
6831
+ hero_cta_text: 'Sign in',
6832
+ hero_cta_url: '/login',
6833
+ cta_title: 'Ready to continue?',
6834
+ cta_subtitle: 'Sign in with your Topolo account.',
6835
+ };
6836
+
6837
+ export const LOGIN_CONFIG = {
6838
+ app_name: APP_NAME,
6839
+ app_tagline: PRODUCT_TAGLINE,
6840
+ hero_title: \`Sign in to \${APP_NAME}\`,
6841
+ hero_subtitle: PRODUCT_TAGLINE,
6842
+ authenticated_home_path: HOME_PATH,
6843
+ };
6844
+
6845
+ export const APP_DISPLAY_NAME = '${escapeSingleQuoted(appName)}';
6846
+ `;
6847
+ }
6848
+ function platformAppRoutesTsx(experience) {
6849
+ const nav = experience.navigationMode === "none" ? "" : " nav: { id: 'home', label: HOME_LABEL },\n";
6850
+ return `import type { AppRoute } from '@topolo-io/app-shell';
6851
+ import { HOME_LABEL, HOME_PATH, NAVIGATION_MODE } from './app-config';
6852
+ import GettingStartedRoute from './routes/GettingStartedRoute';
6853
+ import HomeRoute from './routes/HomeRoute';
6854
+
6855
+ export function createAppRoutes(appId: string): AppRoute[] {
6616
6856
  return [
6617
- "import { Component, useCallback, useEffect, useMemo, useState, type ErrorInfo, type ReactNode } from 'react';",
6618
- "import { LandingPage } from '@topolo-io/app-funnel';",
6619
- "import { LoginPage, TopoloAppLoadingScaffold, TopoloAppRoot, TopoloAuthFlowScreen, createTopoloAuth, useAuth, type AppRoute as TopoloAppRoute } from '@topolo-io/app-shell';",
6620
- "import { setupBugReporterErrorCapture } from '@topolo-io/ui-kit';",
6621
- "import { GettingStarted, type OnboardingGuideStep } from '@topolo-io/onboarding';",
6622
- "import { IntroTour } from '@topolo-io/tour';",
6623
- "import { SCAFFOLD_APP_NAME, getBrowserAuthBaseUrl, resolveAppId } from './app-identity';",
6624
- "",
6625
- "// Wire global error/console capture so BugReporter (enabled in TopoloAppShell below)",
6626
- "// can attach the last few errors to any submitted bug report.",
6627
- "setupBugReporterErrorCapture();",
6628
- "",
6629
- "const APP_NAME = SCAFFOLD_APP_NAME;",
6630
- "const AUTH_URL = getBrowserAuthBaseUrl();",
6631
- "const LANDING_CONFIG = {",
6632
- " app_name: APP_NAME,",
6633
- ` app_tagline: '${escapeSingleQuoted(tagline)}',`,
6634
- " hero_title: `Open ${APP_NAME} in Topolo`,",
6635
- ` hero_subtitle: 'Shared landing, login, callback, auth, and ${escapeSingleQuoted(shellLabel)} wiring are already included in the generated baseline.',`,
6636
- " hero_cta_text: 'Sign in',",
6637
- " hero_cta_url: '/login',",
6638
- " cta_title: 'Start from the shared platform entry flow',",
6639
- ` cta_subtitle: 'Unauthenticated routes stay on the shared Topolo surfaces before users enter the signed ${escapeSingleQuoted(shellLabel)}.',`,
6640
- "};",
6641
- "const LOGIN_CONFIG = {",
6642
- " app_name: APP_NAME,",
6643
- ` app_tagline: '${escapeSingleQuoted(tagline)}',`,
6644
- " hero_title: `Sign in to ${APP_NAME}`,",
6645
- ` hero_subtitle: 'Use Topolo Auth to open the shared ${escapeSingleQuoted(shellLabel)} for this application.',`,
6646
- " authenticated_home_path: '/dashboard',",
6647
- "};",
6648
- "",
6649
- "export default function App() {",
6650
- " return (",
6651
- " <AppErrorBoundary>",
6652
- " <CanonicalAppRoot />",
6653
- " </AppErrorBoundary>",
6654
- " );",
6655
- "}",
6656
- "",
6657
- "function CanonicalAppRoot() {",
6658
- " const [appId, setAppId] = useState('');",
6659
- " const [appIdentityError, setAppIdentityError] = useState('');",
6660
- "",
6661
- " useEffect(() => {",
6662
- " let active = true;",
6663
- " setAppIdentityError('');",
6664
- " void resolveAppId()",
6665
- " .then((nextAppId) => {",
6666
- " if (active) setAppId(nextAppId);",
6667
- " })",
6668
- " .catch((error) => {",
6669
- " if (!active) return;",
6670
- " setAppIdentityError(errorMessage(error));",
6671
- " });",
6672
- " return () => {",
6673
- " active = false;",
6674
- " };",
6675
- " }, []);",
6676
- "",
6677
- " const auth = useMemo(() => appId ? createTopoloAuth({",
6678
- " authServiceUrl: AUTH_URL,",
6679
- " appId,",
6680
- " devSessionKey: `${appId}_session`,",
6681
- " callbackPath: '/auth/callback',",
6682
- " sessionStorageRestoreEnabled: import.meta.env.DEV,",
6683
- " debug: import.meta.env.DEV,",
6684
- " }) : null, [appId]);",
6685
- "",
6686
- " const routes = useMemo<TopoloAppRoute[]>(() => appId ? [",
6687
- " {",
6688
- " path: '/workspace',",
6689
- " nav: { id: 'home', label: '" + escapeSingleQuoted(workspaceTitle) + "' },",
6690
- " loading: { shell: 'topbar', content: 'workspace', title: '" + escapeSingleQuoted(workspaceTitle) + "' },",
6691
- " element: <ScaffoldWorkspace />,",
6692
- " },",
6693
- " {",
6694
- " path: '/getting-started',",
6695
- " nav: { id: 'getting-started', label: 'Getting started' },",
6696
- " loading: { shell: 'topbar', content: 'card-grid', title: 'Getting started' },",
6697
- " element: <ScaffoldGettingStartedRoute appId={appId} />,",
6698
- " },",
6699
- " ] : [], [appId]);",
6700
- "",
6701
- " if (appIdentityError) {",
6702
- " return (",
6703
- " <TopoloAuthFlowScreen",
6704
- " appName={APP_NAME}",
6705
- " appId={appId}",
6706
- ' status="error"',
6707
- ' title="App identity unavailable"',
6708
- " message={appIdentityError}",
6709
- ' actionLabel="Retry"',
6710
- " onAction={() => window.location.reload()}",
6711
- " />",
6712
- " );",
6713
- " }",
6714
- "",
6715
- " if (!auth) {",
6716
- " // Pre-AppRoot: appId is still resolving, so auth can't be built yet.",
6717
- " // Render the SAME scaffold layout AppRoot uses below so this phase and",
6718
- " // the auth-gate fallback read as one continuous loading state (the",
6719
- " // scaffold resolves theme + rail width on its own to match the shell).",
6720
- " return (",
6721
- " <TopoloAppLoadingScaffold",
6722
- " appId={appId}",
6723
- " appName={APP_NAME}",
6724
- " layout={{ shell: 'topbar', content: 'workspace' }}",
6725
- " />",
6726
- " );",
6727
- " }",
6728
- "",
6729
- " return (",
6730
- " <TopoloAppRoot",
6731
- " appName={APP_NAME}",
6732
- " appId={appId}",
6733
- " auth={auth}",
6734
- " routes={routes}",
6735
- ' home="/workspace"',
6736
- " publicLanding={",
6737
- " <LandingPage",
6738
- " appId={appId}",
6739
- " authApiUrl={AUTH_URL}",
6740
- " config={LANDING_CONFIG}",
6741
- " onCTAClick={() => window.location.assign('/login')}",
6742
- " />",
6743
- " }",
6744
- " loginElement={",
6745
- " <LoginPage",
6746
- " appId={appId}",
6747
- " authApiUrl={AUTH_URL}",
6748
- " config={LOGIN_CONFIG}",
6749
- " onLogin={async (provider) => {",
6750
- " await auth.login(provider || 'topolo');",
6751
- " }}",
6752
- " onPasswordLogin={async (request) => {",
6753
- " const result = await auth.loginWithPassword(request);",
6754
- " if (!result.authenticated) return result;",
6755
- " window.dispatchEvent(new CustomEvent('auth:session-updated', { detail: { tokens: result.tokens, user: result.user } }));",
6756
- " return undefined;",
6757
- " }}",
6758
- " />",
6759
- " }",
6760
- " loadingFallback={({ loading }) => (",
6761
- " <TopoloAppLoadingScaffold",
6762
- " appId={appId}",
6763
- " appName={APP_NAME}",
6764
- " layout={{ shell: loading?.shell || 'topbar', content: loading?.content || 'workspace' }}",
6765
- " />",
6766
- " )}",
6767
- " shell={{",
6768
- " appId: appId,",
6769
- " authUrl: AUTH_URL,",
6770
- " navigationMode: 'topbar',",
6771
- " title: APP_NAME,",
6772
- " commandPlaceholder: `Search ${APP_NAME}`,",
6773
- " bugReporterAppName: APP_NAME,",
6774
- " enableCommandPalette: true,",
6775
- " enableBugReporter: true,",
6776
- " }}",
6777
- " />",
6778
- " );",
6779
- "}",
6780
- "",
6781
- "function ScaffoldWorkspace() {",
6782
- " const { user } = useAuth();",
6783
- " const openWorkspace = useCallback(() => {",
6784
- " window.history.pushState({}, '', '/workspace');",
6785
- " window.dispatchEvent(new PopStateEvent('popstate'));",
6786
- " }, []);",
6787
- " const openGettingStarted = useCallback(() => {",
6788
- " window.history.pushState({}, '', '/getting-started');",
6789
- " window.dispatchEvent(new PopStateEvent('popstate'));",
6790
- " }, []);",
6791
- " return (",
6792
- ' <div className="topolo-starter-workspace">',
6793
- ' <section className="topolo-starter-card" data-tour="workspace-card">',
6794
- ` <h2>${baselineHeading}</h2>`,
6795
- " <p>",
6796
- ` ${baselineDescription}`,
6797
- " </p>",
6798
- " </section>",
6799
- " <IntroTour",
6800
- " appName={APP_NAME}",
6801
- " role={user?.role}",
6802
- " onOpenGettingStarted={openGettingStarted}",
6803
- " extraSteps={[",
6804
- " {",
6805
- " id: 'workspace-card',",
6806
- ` target: '[data-tour="workspace-card"]',`,
6807
- " title: 'Your workspace',",
6808
- " content: 'Everything you build renders here, inside the shared Topolo shell.',",
6809
- " placement: 'bottom' as const,",
6810
- " beforeStep: () => openWorkspace(),",
6811
- " },",
6812
- " ]}",
6813
- " />",
6814
- " </div>",
6815
- " );",
6816
- "}",
6817
- "",
6818
- "function ScaffoldGettingStartedRoute({ appId }: { appId: string }) {",
6819
- " const { user, tokens } = useAuth();",
6820
- " return (",
6821
- " <ScaffoldGettingStarted",
6822
- " appId={appId}",
6823
- " organizationId={String((user as any)?.organization?.id ?? user?.orgId ?? '')}",
6824
- " role={user?.role}",
6825
- " getAccessToken={() => tokens?.accessToken ?? null}",
6826
- " onOpenWorkspace={() => window.location.assign('/workspace')}",
6827
- " />",
6828
- " );",
6829
- "}",
6830
- "",
6831
- "function ScaffoldGettingStarted(props: {",
6832
- " appId: string;",
6833
- " organizationId: string;",
6834
- " role?: string;",
6835
- " getAccessToken: () => string | null;",
6836
- " onOpenWorkspace: () => void;",
6837
- "}) {",
6838
- " const { appId, organizationId, role, getAccessToken, onOpenWorkspace } = props;",
6839
- " const steps: OnboardingGuideStep[] = [",
6840
- " {",
6841
- " id: 'explore-workspace',",
6842
- " title: 'Explore your workspace',",
6843
- " description:",
6844
- " 'Your workspace lives in the Topolo shell \u2014 the launcher, command palette, and account menu are all one click away.',",
6845
- " actionLabel: 'Open workspace',",
6846
- " onAction: onOpenWorkspace,",
6847
- " },",
6848
- " {",
6849
- " id: 'discover-platform',",
6850
- " title: 'Move between your Topolo apps',",
6851
- " description:",
6852
- " 'Use the launcher to switch between your Topolo apps, and the command palette (Cmd/Ctrl-K) to jump anywhere fast.',",
6853
- " optional: true,",
6854
- " },",
6855
- " {",
6856
- " id: 'build-feature',",
6857
- " title: 'Make it your own',",
6858
- " description:",
6859
- " 'Open the account menu to adjust your settings and theme \u2014 your preferences stay in sync across every Topolo app.',",
6860
- " },",
6861
- " ];",
6862
- " return (",
6863
- ' <div className="topolo-starter-workspace">',
6864
- " <GettingStarted",
6865
- " authApiBaseUrl={`${AUTH_URL}/api`}",
6866
- " organizationId={organizationId}",
6867
- " appId={appId}",
6868
- " getAccessToken={getAccessToken}",
6869
- " role={role || undefined}",
6870
- " steps={steps}",
6871
- " eyebrow={`Welcome to ${APP_NAME}`}",
6872
- " title={`Get started with ${APP_NAME}`}",
6873
- ' subtitle="A short checklist to orient you in your new Topolo workspace."',
6874
- ' completeLabel="Mark done"',
6875
- ' dismissLabel="Skip for now"',
6876
- " completedContent={`You're all set \u2014 ${APP_NAME} is ready to build on.`}",
6877
- " completeAction={{ label: 'Go to workspace', onClick: onOpenWorkspace }}",
6878
- " onDismiss={onOpenWorkspace}",
6879
- " />",
6880
- " </div>",
6881
- " );",
6882
- "}",
6883
- "",
6884
- "function errorMessage(error: unknown): string {",
6885
- " if (error instanceof Error && error.message) return error.message;",
6886
- " return 'Unexpected authentication error.';",
6887
- "}",
6888
- "",
6889
- "// Minimal error boundary so a render-time crash falls back to a recoverable surface",
6890
- "// instead of a blank page. The shared TopoloAppShell BugReporter still has access to",
6891
- "// the captured stack via setupBugReporterErrorCapture().",
6892
- "class AppErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {",
6893
- " state = { error: null as Error | null };",
6894
- " static getDerivedStateFromError(error: Error) {",
6895
- " return { error };",
6896
- " }",
6897
- " componentDidCatch(error: Error, info: ErrorInfo) {",
6898
- " // Surface to the platform error pipeline. setupBugReporterErrorCapture() also",
6899
- " // records this via the console.error hook for BugReporter attachments.",
6900
- " console.error('[' + APP_NAME + '] render error:', error, info.componentStack);",
6901
- " }",
6902
- " render() {",
6903
- " if (!this.state.error) return this.props.children;",
6904
- " return (",
6905
- " <TopoloAuthFlowScreen",
6906
- " appName={APP_NAME}",
6907
- ' status="error"',
6908
- " title={APP_NAME + ' hit an unexpected error'}",
6909
- " message={this.state.error.message}",
6910
- ' actionLabel="Reload"',
6911
- " onAction={() => window.location.reload()}",
6912
- " />",
6913
- " );",
6914
- " }",
6915
- "}",
6916
- ""
6917
- ].join("\n");
6857
+ {
6858
+ path: HOME_PATH,
6859
+ ${nav} loading: { shell: NAVIGATION_MODE, content: 'workspace', title: HOME_LABEL },
6860
+ element: <HomeRoute />,
6861
+ },
6862
+ {
6863
+ path: '/getting-started',
6864
+ loading: { shell: NAVIGATION_MODE, content: 'card-grid', title: 'Getting started' },
6865
+ element: <GettingStartedRoute appId={appId} />,
6866
+ },
6867
+ ];
6868
+ }
6869
+ `;
6870
+ }
6871
+ function platformHomeRouteTsx() {
6872
+ return `import { TopoloAppPage, TopoloAppPageHeader, useAuth } from '@topolo-io/app-shell';
6873
+ import { IntroTour } from '@topolo-io/tour';
6874
+ import { useTranslation } from '@topolo-io/ui-kit';
6875
+ import { useNavigate } from 'react-router-dom';
6876
+ import { APP_NAME, HOME_LABEL, HOME_PATH, PRODUCT_TAGLINE } from '../app-config';
6877
+
6878
+ export default function HomeRoute() {
6879
+ const { user } = useAuth();
6880
+ const { t } = useTranslation();
6881
+ const navigate = useNavigate();
6882
+ return (
6883
+ <TopoloAppPage data-tour="home-page">
6884
+ <TopoloAppPageHeader
6885
+ eyebrow={APP_NAME}
6886
+ title={t('app.home.title', { defaultValue: HOME_LABEL })}
6887
+ description={t('app.home.description', { defaultValue: PRODUCT_TAGLINE })}
6888
+ />
6889
+ <IntroTour
6890
+ appName={APP_NAME}
6891
+ role={user?.role}
6892
+ onOpenGettingStarted={() => navigate('/getting-started')}
6893
+ extraSteps={[{
6894
+ id: 'home-page',
6895
+ target: '[data-tour="home-page"]',
6896
+ title: HOME_LABEL,
6897
+ content: PRODUCT_TAGLINE,
6898
+ placement: 'bottom' as const,
6899
+ beforeStep: () => navigate(HOME_PATH),
6900
+ }]}
6901
+ />
6902
+ </TopoloAppPage>
6903
+ );
6904
+ }
6905
+ `;
6906
+ }
6907
+ function platformGettingStartedRouteTsx() {
6908
+ return `import { TopoloAppPage, useAuth } from '@topolo-io/app-shell';
6909
+ import { GettingStarted, type OnboardingGuideStep } from '@topolo-io/onboarding';
6910
+ import { useTranslation } from '@topolo-io/ui-kit';
6911
+ import { useNavigate } from 'react-router-dom';
6912
+ import { APP_NAME, AUTH_URL, HOME_LABEL, HOME_PATH } from '../app-config';
6913
+
6914
+ export default function GettingStartedRoute({ appId }: { appId: string }) {
6915
+ const { user, tokens } = useAuth();
6916
+ const { t } = useTranslation();
6917
+ const navigate = useNavigate();
6918
+ const openHome = () => navigate(HOME_PATH);
6919
+ const steps: OnboardingGuideStep[] = [
6920
+ {
6921
+ id: 'explore-home',
6922
+ title: t('app.gettingStarted.explore.title', { defaultValue: \`Explore \${HOME_LABEL.toLowerCase()}\` }),
6923
+ description: t('app.gettingStarted.explore.description', { defaultValue: 'Start with the main product surface and the actions available to you.' }),
6924
+ actionLabel: t('app.gettingStarted.explore.action', { defaultValue: \`Open \${HOME_LABEL.toLowerCase()}\` }),
6925
+ onAction: openHome,
6926
+ },
6927
+ {
6928
+ id: 'discover-platform',
6929
+ title: t('app.gettingStarted.platform.title', { defaultValue: 'Move between Topolo apps' }),
6930
+ description: t('app.gettingStarted.platform.description', { defaultValue: 'Use the app launcher and command palette to move through your work.' }),
6931
+ optional: true,
6932
+ },
6933
+ {
6934
+ id: 'personalize',
6935
+ title: t('app.gettingStarted.personalize.title', { defaultValue: 'Set your preferences' }),
6936
+ description: t('app.gettingStarted.personalize.description', { defaultValue: 'Open the account menu to manage your settings and theme.' }),
6937
+ },
6938
+ ];
6939
+ return (
6940
+ <TopoloAppPage>
6941
+ <GettingStarted
6942
+ authApiBaseUrl={\`\${AUTH_URL}/api\`}
6943
+ organizationId={String((user as { organization?: { id?: string } } | null)?.organization?.id ?? user?.orgId ?? '')}
6944
+ appId={appId}
6945
+ getAccessToken={() => tokens?.accessToken ?? null}
6946
+ role={user?.role}
6947
+ steps={steps}
6948
+ eyebrow={t('app.gettingStarted.eyebrow', { defaultValue: \`Welcome to \${APP_NAME}\` })}
6949
+ title={t('app.gettingStarted.title', { defaultValue: \`Get started with \${APP_NAME}\` })}
6950
+ subtitle={t('app.gettingStarted.subtitle', { defaultValue: 'A short checklist to help you find your way around.' })}
6951
+ completeLabel={t('app.gettingStarted.complete', { defaultValue: 'Mark done' })}
6952
+ dismissLabel={t('app.gettingStarted.dismiss', { defaultValue: 'Skip for now' })}
6953
+ completedContent={t('app.gettingStarted.completed', { defaultValue: \`You're all set \u2014 \${APP_NAME} is ready.\` })}
6954
+ completeAction={{ label: t('app.gettingStarted.openHome', { defaultValue: \`Go to \${HOME_LABEL.toLowerCase()}\` }), onClick: openHome }}
6955
+ onDismiss={openHome}
6956
+ />
6957
+ </TopoloAppPage>
6958
+ );
6959
+ }
6960
+ `;
6961
+ }
6962
+ function platformAppErrorBoundaryTsx() {
6963
+ return `import { Component, type ErrorInfo, type ReactNode } from 'react';
6964
+ import { TopoloAuthFlowScreen } from '@topolo-io/app-shell';
6965
+ import { APP_NAME } from './app-config';
6966
+
6967
+ export default class AppErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
6968
+ state = { error: null as Error | null };
6969
+
6970
+ static getDerivedStateFromError(error: Error) {
6971
+ return { error };
6972
+ }
6973
+
6974
+ componentDidCatch(error: Error, info: ErrorInfo) {
6975
+ console.error('[' + APP_NAME + '] render error:', error, info.componentStack);
6976
+ }
6977
+
6978
+ render() {
6979
+ if (!this.state.error) return this.props.children;
6980
+ return (
6981
+ <TopoloAuthFlowScreen
6982
+ appName={APP_NAME}
6983
+ status="error"
6984
+ title={APP_NAME + ' hit an unexpected error'}
6985
+ message={this.state.error.message}
6986
+ actionLabel="Reload"
6987
+ onAction={() => window.location.reload()}
6988
+ />
6989
+ );
6990
+ }
6991
+ }
6992
+ `;
6918
6993
  }
6919
6994
  function developerAppTsx(application) {
6920
6995
  const appName = toTitle(application.name);
6921
6996
  const defaultClientId = `${toSlug(application.id)}_local_client`;
6922
- return [
6923
- "import { useEffect, useMemo, useState } from 'react';",
6924
- "import { createAccountLinkPayload, createTopoloAuthorizationUrl, exchangeTopoloCode, fetchTopoloUserInfo, type TopoloUserInfo } from '@topolo/login';",
6925
- "",
6926
- "type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';",
6927
- "",
6928
- `const APP_NAME = '${escapeSingleQuoted(appName)}';`,
6929
- "const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? import.meta.env.VITE_TOPOLO_AUTH_BASE_URL ?? 'https://auth.topolo.app';",
6930
- `const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';`,
6931
- "const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || `${window.location.origin}/auth/callback`;",
6932
- "const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'openid profile email organization:read entitlements:read').split(/[\\s,]+/).filter(Boolean);",
6933
- "",
6934
- "export default function App() {",
6935
- " const [authState, setAuthState] = useState<AuthState>(() => CLIENT_ID ? 'ready' : 'missing_client');",
6936
- " const [message, setMessage] = useState<string | null>(null);",
6937
- " const [tokenPreview, setTokenPreview] = useState<string | null>(null);",
6938
- " const [userInfo, setUserInfo] = useState<TopoloUserInfo | null>(null);",
6939
- "",
6940
- " const callbackCode = useMemo(() => new URLSearchParams(window.location.search).get('code'), []);",
6941
- " const callbackError = useMemo(() => new URLSearchParams(window.location.search).get('error'), []);",
6942
- "",
6943
- " useEffect(() => {",
6944
- " if (callbackError) {",
6945
- " setAuthState('error');",
6946
- " setMessage(callbackError);",
6947
- " return;",
6948
- " }",
6949
- " if (!callbackCode) return;",
6950
- "",
6951
- " setAuthState('authorizing');",
6952
- " void exchangeAuthorizationCode(callbackCode)",
6953
- " .then(async (tokens) => {",
6954
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_access_token`, tokens.access_token);",
6955
- " if (tokens.refresh_token) {",
6956
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_refresh_token`, tokens.refresh_token);",
6957
- " }",
6958
- " const profile = await fetchTopoloUserInfo({ authBaseUrl: AUTH_URL, accessToken: tokens.access_token });",
6959
- " const link = createAccountLinkPayload({",
6960
- " localUserId: profile.email ? `local:${profile.email}` : `local:${profile.sub}`,",
6961
- " userInfo: profile,",
6962
- " });",
6963
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_account_link`, JSON.stringify(link));",
6964
- " setUserInfo(profile);",
6965
- " setTokenPreview(`${tokens.access_token.slice(0, 18)}...`);",
6966
- " setAuthState('authenticated');",
6967
- " window.history.replaceState({}, document.title, '/');",
6968
- " })",
6969
- " .catch((error: Error) => {",
6970
- " setAuthState('error');",
6971
- " setMessage(error.message);",
6972
- " });",
6973
- " }, [callbackCode, callbackError]);",
6974
- "",
6975
- " return (",
6976
- ' <main className="topolo-starter-login">',
6977
- ' <section className="topolo-starter-login__panel">',
6978
- " <h1>{APP_NAME}</h1>",
6979
- " <p>Public Topolo OAuth starter using authorization-code + PKCE.</p>",
6980
- " {authState === 'missing_client' && (",
6981
- " <p>Set VITE_TOPOLO_CLIENT_ID after registering this app in Topolo Developers.</p>",
6982
- " )}",
6983
- " {authState === 'authenticated' && (",
6984
- " <p>Topolo sign-in complete for {userInfo?.email ?? userInfo?.sub}. Access token: {tokenPreview}</p>",
6985
- " )}",
6986
- " {authState === 'error' && <p>{message}</p>}",
6987
- ` <button type="button" disabled={!CLIENT_ID || authState === 'authorizing'} onClick={() => void startTopoloLogin()}>`,
6988
- " {authState === 'authorizing' ? 'Completing sign-in' : 'Sign in with Topolo'}",
6989
- " </button>",
6990
- " </section>",
6991
- " </main>",
6992
- " );",
6993
- "}",
6994
- "",
6995
- "async function startTopoloLogin(): Promise<void> {",
6996
- " const authorization = await createTopoloAuthorizationUrl({",
6997
- " authBaseUrl: AUTH_URL,",
6998
- " clientId: CLIENT_ID,",
6999
- " redirectUri: REDIRECT_URI,",
7000
- " scopes: REQUESTED_SCOPES,",
7001
- " });",
7002
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_pkce_verifier`, authorization.codeVerifier);",
7003
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_oauth_state`, authorization.state);",
7004
- " window.location.assign(authorization.url);",
7005
- "}",
7006
- "",
7007
- "async function exchangeAuthorizationCode(code: string): Promise<{ access_token: string; refresh_token?: string }> {",
7008
- " const params = new URLSearchParams(window.location.search);",
7009
- " const expectedState = sessionStorage.getItem(`${CLIENT_ID}:topolo_oauth_state`);",
7010
- " if (expectedState && params.get('state') !== expectedState) {",
7011
- " throw new Error('OAuth state mismatch. Restart sign-in.');",
7012
- " }",
7013
- " const verifier = sessionStorage.getItem(`${CLIENT_ID}:topolo_pkce_verifier`);",
7014
- " if (!verifier) throw new Error('Missing PKCE verifier. Restart sign-in.');",
7015
- "",
7016
- " return exchangeTopoloCode({",
7017
- " authBaseUrl: AUTH_URL,",
7018
- " clientId: CLIENT_ID,",
7019
- " code,",
7020
- " redirectUri: REDIRECT_URI,",
7021
- " codeVerifier: verifier,",
7022
- " });",
7023
- "}",
7024
- ""
7025
- ].join("\n");
6997
+ return `import { useEffect, useMemo, useState } from 'react';
6998
+ import { createAccountLinkPayload, createTopoloAuthorizationUrl, exchangeTopoloCode, fetchTopoloUserInfo, type TopoloUserInfo } from '@topolo/login';
6999
+
7000
+ type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';
7001
+ const APP_NAME = '${escapeSingleQuoted(appName)}';
7002
+ const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? import.meta.env.VITE_TOPOLO_AUTH_BASE_URL ?? 'https://auth.topolo.app';
7003
+ const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';
7004
+ const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || \`\${window.location.origin}/auth/callback\`;
7005
+ const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'openid profile email organization:read entitlements:read').split(/[\\s,]+/).filter(Boolean);
7006
+
7007
+ export default function App() {
7008
+ const [authState, setAuthState] = useState<AuthState>(() => CLIENT_ID ? 'ready' : 'missing_client');
7009
+ const [message, setMessage] = useState<string | null>(null);
7010
+ const [tokenPreview, setTokenPreview] = useState<string | null>(null);
7011
+ const [userInfo, setUserInfo] = useState<TopoloUserInfo | null>(null);
7012
+ const callbackCode = useMemo(() => new URLSearchParams(window.location.search).get('code'), []);
7013
+ const callbackError = useMemo(() => new URLSearchParams(window.location.search).get('error'), []);
7014
+
7015
+ useEffect(() => {
7016
+ if (callbackError) {
7017
+ setAuthState('error');
7018
+ setMessage(callbackError);
7019
+ return;
7020
+ }
7021
+ if (!callbackCode) return;
7022
+ setAuthState('authorizing');
7023
+ void exchangeAuthorizationCode(callbackCode)
7024
+ .then(async (tokens) => {
7025
+ sessionStorage.setItem(\`\${CLIENT_ID}:topolo_access_token\`, tokens.access_token);
7026
+ if (tokens.refresh_token) sessionStorage.setItem(\`\${CLIENT_ID}:topolo_refresh_token\`, tokens.refresh_token);
7027
+ const profile = await fetchTopoloUserInfo({ authBaseUrl: AUTH_URL, accessToken: tokens.access_token });
7028
+ const link = createAccountLinkPayload({ localUserId: profile.email ? \`local:\${profile.email}\` : \`local:\${profile.sub}\`, userInfo: profile });
7029
+ sessionStorage.setItem(\`\${CLIENT_ID}:topolo_account_link\`, JSON.stringify(link));
7030
+ setUserInfo(profile);
7031
+ setTokenPreview(\`\${tokens.access_token.slice(0, 18)}...\`);
7032
+ setAuthState('authenticated');
7033
+ window.history.replaceState({}, document.title, '/');
7034
+ })
7035
+ .catch((error: Error) => {
7036
+ setAuthState('error');
7037
+ setMessage(error.message);
7038
+ });
7039
+ }, [callbackCode, callbackError]);
7040
+
7041
+ return (
7042
+ <main className="topolo-starter-login">
7043
+ <section className="topolo-starter-login__panel">
7044
+ <h1>{APP_NAME}</h1>
7045
+ <p>Public Topolo OAuth starter using authorization-code + PKCE.</p>
7046
+ {authState === 'missing_client' && <p>Set VITE_TOPOLO_CLIENT_ID after registering this app in Topolo Developers.</p>}
7047
+ {authState === 'authenticated' && <p>Topolo sign-in complete for {userInfo?.email ?? userInfo?.sub}. Access token: {tokenPreview}</p>}
7048
+ {authState === 'error' && <p>{message}</p>}
7049
+ <button type="button" disabled={!CLIENT_ID || authState === 'authorizing'} onClick={() => void startTopoloLogin()}>
7050
+ {authState === 'authorizing' ? 'Completing sign-in' : 'Sign in with Topolo'}
7051
+ </button>
7052
+ </section>
7053
+ </main>
7054
+ );
7055
+ }
7056
+
7057
+ async function startTopoloLogin(): Promise<void> {
7058
+ const authorization = await createTopoloAuthorizationUrl({ authBaseUrl: AUTH_URL, clientId: CLIENT_ID, redirectUri: REDIRECT_URI, scopes: REQUESTED_SCOPES });
7059
+ sessionStorage.setItem(\`\${CLIENT_ID}:topolo_pkce_verifier\`, authorization.codeVerifier);
7060
+ sessionStorage.setItem(\`\${CLIENT_ID}:topolo_oauth_state\`, authorization.state);
7061
+ window.location.assign(authorization.url);
7062
+ }
7063
+
7064
+ async function exchangeAuthorizationCode(code: string): Promise<{ access_token: string; refresh_token?: string }> {
7065
+ const params = new URLSearchParams(window.location.search);
7066
+ const expectedState = sessionStorage.getItem(\`\${CLIENT_ID}:topolo_oauth_state\`);
7067
+ if (expectedState && params.get('state') !== expectedState) throw new Error('OAuth state mismatch. Restart sign-in.');
7068
+ const verifier = sessionStorage.getItem(\`\${CLIENT_ID}:topolo_pkce_verifier\`);
7069
+ if (!verifier) throw new Error('Missing PKCE verifier. Restart sign-in.');
7070
+ return exchangeTopoloCode({ authBaseUrl: AUTH_URL, clientId: CLIENT_ID, code, redirectUri: REDIRECT_URI, codeVerifier: verifier });
7071
+ }
7072
+ `;
7026
7073
  }
7027
7074
 
7028
7075
  // src/commands/app-scaffold-worker-template.ts
7029
- function webWorkerTs(application) {
7076
+ function webWorkerTs(application, api) {
7030
7077
  const serviceName = toTitle(application.name);
7031
7078
  return [
7032
- "interface Env {",
7033
- " API: { fetch(request: Request): Promise<Response> };",
7034
- "}",
7079
+ ...api ? [
7080
+ "interface Env {",
7081
+ " API: { fetch(request: Request): Promise<Response> };",
7082
+ "}"
7083
+ ] : ["type Env = Record<string, never>;"],
7035
7084
  "",
7036
7085
  `const SERVICE_NAME = '${escapeSingleQuoted(serviceName)}';`,
7037
7086
  "",
7038
7087
  "export default {",
7039
- " async fetch(request: Request, env: Env): Promise<Response> {",
7040
- " const url = new URL(request.url);",
7041
- " if (url.pathname.startsWith('/api/')) {",
7042
- " return env.API.fetch(request);",
7043
- " }",
7088
+ api ? " async fetch(request: Request, env: Env): Promise<Response> {" : " async fetch(): Promise<Response> {",
7089
+ ...api ? [
7090
+ " const url = new URL(request.url);",
7091
+ " if (url.pathname.startsWith('/api/')) {",
7092
+ " return env.API.fetch(request);",
7093
+ " }"
7094
+ ] : [],
7044
7095
  " return Response.json({ ok: true, service: SERVICE_NAME }, { status: 200 });",
7045
7096
  " },",
7046
7097
  "};",
7047
7098
  ""
7048
7099
  ].join("\n");
7049
7100
  }
7050
- function platformWebWranglerToml(application) {
7101
+ function platformWebWranglerToml(application, api) {
7051
7102
  const slug = toSlug(application.id);
7052
7103
  const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
7053
7104
  return [
@@ -7062,7 +7113,7 @@ function platformWebWranglerToml(application) {
7062
7113
  'directory = "./dist"',
7063
7114
  'binding = "ASSETS"',
7064
7115
  'not_found_handling = "single-page-application"',
7065
- 'run_worker_first = ["/api/*"]',
7116
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7066
7117
  "",
7067
7118
  "[observability]",
7068
7119
  "enabled = true",
@@ -7075,10 +7126,12 @@ function platformWebWranglerToml(application) {
7075
7126
  `APP_ID = "${application.appId ?? ""}"`,
7076
7127
  `VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
7077
7128
  "",
7078
- "[[services]]",
7079
- 'binding = "API"',
7080
- `service = "topolo-${slug}-api"`,
7081
- "",
7129
+ ...api ? [
7130
+ "[[services]]",
7131
+ 'binding = "API"',
7132
+ `service = "topolo-${slug}-api"`,
7133
+ ""
7134
+ ] : [],
7082
7135
  "[env.development]",
7083
7136
  `name = "topolo-${slug}-development"`,
7084
7137
  `account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
@@ -7096,12 +7149,14 @@ function platformWebWranglerToml(application) {
7096
7149
  'directory = "./dist"',
7097
7150
  'binding = "ASSETS"',
7098
7151
  'not_found_handling = "single-page-application"',
7099
- 'run_worker_first = ["/api/*"]',
7100
- "",
7101
- "[[env.development.services]]",
7102
- 'binding = "API"',
7103
- `service = "topolo-${slug}-api-development"`,
7152
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7104
7153
  "",
7154
+ ...api ? [
7155
+ "[[env.development.services]]",
7156
+ 'binding = "API"',
7157
+ `service = "topolo-${slug}-api-development"`,
7158
+ ""
7159
+ ] : [],
7105
7160
  "[env.staging]",
7106
7161
  `name = "topolo-${slug}-staging"`,
7107
7162
  `account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
@@ -7119,12 +7174,14 @@ function platformWebWranglerToml(application) {
7119
7174
  'directory = "./dist"',
7120
7175
  'binding = "ASSETS"',
7121
7176
  'not_found_handling = "single-page-application"',
7122
- 'run_worker_first = ["/api/*"]',
7123
- "",
7124
- "[[env.staging.services]]",
7125
- 'binding = "API"',
7126
- `service = "topolo-${slug}-api-staging"`,
7177
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7127
7178
  "",
7179
+ ...api ? [
7180
+ "[[env.staging.services]]",
7181
+ 'binding = "API"',
7182
+ `service = "topolo-${slug}-api-staging"`,
7183
+ ""
7184
+ ] : [],
7128
7185
  "[env.production]",
7129
7186
  `name = "topolo-${slug}-production"`,
7130
7187
  `account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
@@ -7142,12 +7199,14 @@ function platformWebWranglerToml(application) {
7142
7199
  'directory = "./dist"',
7143
7200
  'binding = "ASSETS"',
7144
7201
  'not_found_handling = "single-page-application"',
7145
- 'run_worker_first = ["/api/*"]',
7202
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7146
7203
  "",
7147
- "[[env.production.services]]",
7148
- 'binding = "API"',
7149
- `service = "topolo-${slug}-api-production"`,
7150
- ""
7204
+ ...api ? [
7205
+ "[[env.production.services]]",
7206
+ 'binding = "API"',
7207
+ `service = "topolo-${slug}-api-production"`,
7208
+ ""
7209
+ ] : []
7151
7210
  ].join("\n");
7152
7211
  }
7153
7212
  function platformApiWranglerToml(application, resources, profile) {
@@ -7459,6 +7518,7 @@ function platformWorkerTs(application, options) {
7459
7518
  "import { withSecurityHeaders } from '@topolo-io/security-headers';",
7460
7519
  "import { withErrorReporting } from '@topolo-io/error-middleware';",
7461
7520
  "import { SCAFFOLD_APP_NAME, SCAFFOLD_SERVICE_SLUG, resolveRuntimeAppId } from './app-identity';",
7521
+ "import { topoloActionContracts } from '../../../topolo.action-contracts.mjs';",
7462
7522
  ...options.resources.workflow ? ["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';"] : [],
7463
7523
  "",
7464
7524
  "export interface Env {",
@@ -7487,6 +7547,7 @@ function platformWorkerTs(application, options) {
7487
7547
  " // so rt.verifyAuth() targets the correct Auth service per deploy.",
7488
7548
  " authBaseUrl: (env) => env.AUTH_BASE_URL,",
7489
7549
  " authGateway: { enabled: false },",
7550
+ " actionContracts: topoloActionContracts,",
7490
7551
  "});",
7491
7552
  "",
7492
7553
  "async function handleRequest(",
@@ -7516,9 +7577,10 @@ function platformWorkerTs(application, options) {
7516
7577
  " }, { status: 200 });",
7517
7578
  " }",
7518
7579
  "",
7580
+ " const auth = await rt.verifyAuth({ appId });",
7581
+ " if (!auth.ok) return rt.authError(auth);",
7582
+ "",
7519
7583
  " if (url.pathname === '/api/widget') {",
7520
- " const auth = await rt.verifyAuth({ appId });",
7521
- " if (!auth.ok) return rt.authError(auth);",
7522
7584
  " return rt.json(createTopoloWidgetResponse({",
7523
7585
  " appId,",
7524
7586
  " serviceName: SCAFFOLD_APP_NAME,",
@@ -7526,12 +7588,6 @@ function platformWorkerTs(application, options) {
7526
7588
  " }), { status: 200, headers: { 'Cache-Control': 'public, max-age=15' } });",
7527
7589
  " }",
7528
7590
  "",
7529
- " const auth = await rt.verifyAuth({ appId });",
7530
- "",
7531
- " if (!auth.ok) {",
7532
- " return rt.authError(auth);",
7533
- " }",
7534
- "",
7535
7591
  " if (url.pathname === '/api/bootstrap') {",
7536
7592
  ...bootstrapBody,
7537
7593
  " }",
@@ -7725,6 +7781,8 @@ function topoloConformanceWorkflow(options) {
7725
7781
  " pull_request:",
7726
7782
  " push:",
7727
7783
  " branches:",
7784
+ " - staging",
7785
+ " - development",
7728
7786
  " - main",
7729
7787
  " workflow_dispatch:",
7730
7788
  "",
@@ -7762,10 +7820,11 @@ function topoloConformanceWorkflow(options) {
7762
7820
  " token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}",
7763
7821
  ...cliCheckoutStep,
7764
7822
  "",
7765
- " - name: Skip conformance without cross-repo token",
7823
+ " - name: Require Topolo package and repository credentials",
7766
7824
  ` if: steps.app-token.outputs.token == '' && env.HAS_TOPOLO_CI_REPO_TOKEN != 'true'`,
7767
7825
  " run: |",
7768
- ' echo "::warning::Skipping Topolo app conformance because no Topolo cross-repo token is available to this workflow context."',
7826
+ ' echo "::error::Topolo conformance requires TOPOLO_PACKAGES_APP credentials or TOPOLO_CI_REPO_TOKEN."',
7827
+ " exit 1",
7769
7828
  "",
7770
7829
  " - name: Setup Node.js",
7771
7830
  ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
@@ -7773,6 +7832,14 @@ function topoloConformanceWorkflow(options) {
7773
7832
  " with:",
7774
7833
  " node-version: 20",
7775
7834
  "",
7835
+ " - name: Enable pnpm and authenticate Topolo packages",
7836
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7837
+ " env:",
7838
+ ` NODE_AUTH_TOKEN: \${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}`,
7839
+ " run: |",
7840
+ " corepack enable",
7841
+ ' printf "//npm.pkg.github.com/:_authToken=%s\\n" "$NODE_AUTH_TOKEN" >> "$HOME/.npmrc"',
7842
+ "",
7776
7843
  " - name: Build Topolo SDK",
7777
7844
  ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7778
7845
  " working-directory: topolo-platform/packages/topolo-sdk",
@@ -7787,16 +7854,30 @@ function topoloConformanceWorkflow(options) {
7787
7854
  " npm install --no-audit --no-fund",
7788
7855
  " npm run build",
7789
7856
  "",
7790
- " - name: Run Topolo app conformance audit",
7857
+ " - name: Install generated application",
7858
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7859
+ ` working-directory: ${appPath}`,
7860
+ " run: pnpm install --frozen-lockfile",
7861
+ "",
7862
+ " - name: Run generated application quality gates",
7791
7863
  ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7864
+ ` working-directory: ${appPath}`,
7792
7865
  " run: |",
7793
- ` node topolo-platform/packages/topolo-cli/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`,
7866
+ " pnpm run typecheck",
7867
+ " pnpm run coverage",
7868
+ " pnpm run build",
7869
+ ' if pnpm run | grep -q "check:deploy-config"; then pnpm run check:deploy-config; fi',
7794
7870
  "",
7795
- " - name: Record green CI evidence",
7871
+ " - name: Record verified CI evidence",
7796
7872
  ` if: \${{ success() && (steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true') }}`,
7797
7873
  ` working-directory: ${appPath}`,
7798
7874
  " run: |",
7799
- ` node "$GITHUB_WORKSPACE/topolo-platform/packages/topolo-cli/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`
7875
+ ` node "$GITHUB_WORKSPACE/topolo-platform/packages/topolo-cli/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`,
7876
+ "",
7877
+ " - name: Run Topolo app conformance audit",
7878
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7879
+ " run: |",
7880
+ ` node topolo-platform/packages/topolo-cli/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`
7800
7881
  ].join("\n");
7801
7882
  }
7802
7883
 
@@ -7890,8 +7971,8 @@ function topoloAppManifestJson(application, options) {
7890
7971
  },
7891
7972
  compatibilityFlags: ["nodejs_compat"],
7892
7973
  buildCommand: {
7893
- staging: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build",
7894
- production: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app npm run build"
7974
+ staging: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us pnpm run build",
7975
+ production: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app pnpm run build"
7895
7976
  },
7896
7977
  healthcheckPath: options.api ? "/api/health" : "/",
7897
7978
  ...options.api ? {
@@ -7949,12 +8030,14 @@ function topoloActionContractsMjs() {
7949
8030
  " name: 'widget.get',",
7950
8031
  " method: 'GET',",
7951
8032
  " path: '/api/widget',",
8033
+ " permission: 'workspace:read',",
7952
8034
  " input: emptyInput,",
7953
8035
  " },",
7954
8036
  " 'workspace.bootstrap': {",
7955
8037
  " name: 'workspace.bootstrap',",
7956
8038
  " method: 'GET',",
7957
8039
  " path: '/api/bootstrap',",
8040
+ " permission: 'workspace:read',",
7958
8041
  " input: emptyInput,",
7959
8042
  " },",
7960
8043
  "};",
@@ -7968,6 +8051,15 @@ function topoloActionContractsMjs() {
7968
8051
  ""
7969
8052
  ].join("\n");
7970
8053
  }
8054
+ function topoloActionContractsDmts() {
8055
+ return [
8056
+ "import type { TopoloActionContracts } from '@topolo-io/worker-runtime/actions';",
8057
+ "",
8058
+ "export const topoloActionContracts: TopoloActionContracts;",
8059
+ "export const topoloActionInputSchemas: Record<string, unknown>;",
8060
+ ""
8061
+ ].join("\n");
8062
+ }
7971
8063
  function baselineApiActions() {
7972
8064
  const emptyInputSchema = {
7973
8065
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -8145,7 +8237,7 @@ function cloudcontrolJson(application, options) {
8145
8237
  cloudflare_account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
8146
8238
  config_path: "apps/web/wrangler.toml",
8147
8239
  workdir: "apps/web",
8148
- build_command: "DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build",
8240
+ build_command: "cd ../.. && DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us pnpm run build",
8149
8241
  output_dir: null,
8150
8242
  production_branch: "staging",
8151
8243
  healthcheck_path: "/api/health",
@@ -8178,7 +8270,7 @@ function cloudcontrolJson(application, options) {
8178
8270
  cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
8179
8271
  config_path: "apps/web/wrangler.toml",
8180
8272
  workdir: "apps/web",
8181
- build_command: "DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app npm run build",
8273
+ build_command: "cd ../.. && DEPLOY_ENVIRONMENT=production VITE_TOPOLO_AUTH_URL=https://auth.topolo.app pnpm run build",
8182
8274
  output_dir: null,
8183
8275
  production_branch: "main",
8184
8276
  healthcheck_path: "/api/health",
@@ -8246,17 +8338,17 @@ function cloudcontrolJson(application, options) {
8246
8338
  ci_targets: [
8247
8339
  {
8248
8340
  name: "typecheck",
8249
- command: "npm run typecheck",
8341
+ command: "pnpm run typecheck",
8250
8342
  required: true
8251
8343
  },
8252
8344
  {
8253
8345
  name: "test",
8254
- command: "npm run test",
8346
+ command: "pnpm run test",
8255
8347
  required: true
8256
8348
  },
8257
8349
  {
8258
8350
  name: "build",
8259
- command: "npm run build",
8351
+ command: "pnpm run build",
8260
8352
  required: true
8261
8353
  }
8262
8354
  ],
@@ -8661,6 +8753,131 @@ function envExample(application) {
8661
8753
  ].join("\n");
8662
8754
  }
8663
8755
 
8756
+ // src/commands/app-scaffold-test-template.ts
8757
+ var coverageThresholds = ` thresholds: {
8758
+ statements: 100,
8759
+ branches: 100,
8760
+ functions: 100,
8761
+ lines: 100,
8762
+ },`;
8763
+ function platformBrowserVitestConfig() {
8764
+ return `import { defineConfig } from 'vitest/config';
8765
+
8766
+ export default defineConfig({
8767
+ test: {
8768
+ include: ['test/**/*.test.{ts,tsx}'],
8769
+ environment: 'jsdom',
8770
+ coverage: {
8771
+ provider: 'v8',
8772
+ include: ['src/app-config.ts', 'src/app-routes.tsx', 'src/worker.ts'],
8773
+ ${coverageThresholds}
8774
+ },
8775
+ },
8776
+ });
8777
+ `;
8778
+ }
8779
+ function platformApiVitestConfig() {
8780
+ return `import { defineConfig } from 'vitest/config';
8781
+
8782
+ export default defineConfig({
8783
+ test: {
8784
+ include: ['test/**/*.test.{js,mjs,mts,ts,tsx}'],
8785
+ environment: 'node',
8786
+ coverage: {
8787
+ provider: 'v8',
8788
+ include: ['src/app-identity.ts'],
8789
+ ${coverageThresholds}
8790
+ },
8791
+ },
8792
+ });
8793
+ `;
8794
+ }
8795
+ function platformBrowserContractTest(application, experience) {
8796
+ const required = experience.workspaceSwitcher === "required";
8797
+ const enabled = experience.workspaceSwitcher !== "off";
8798
+ const navExpectation = experience.navigationMode === "none" ? " expect(home.nav).toBeUndefined();" : ` expect(home.nav).toEqual({ id: 'home', label: '${escapeSingleQuoted(experience.homeLabel)}' });`;
8799
+ return `import { describe, expect, it, vi } from 'vitest';
8800
+
8801
+ vi.mock('../src/app-identity', () => ({
8802
+ SCAFFOLD_APP_NAME: '${escapeSingleQuoted(application.name)}',
8803
+ getBrowserAuthBaseUrl: () => 'https://auth.stg.topolo.us',
8804
+ }));
8805
+
8806
+ import { HOME_LABEL, HOME_PATH, NAVIGATION_MODE, WORKSPACE_RESOURCE } from '../src/app-config';
8807
+ import { createAppRoutes } from '../src/app-routes';
8808
+
8809
+ describe('generated application contract', () => {
8810
+ it('uses the selected home, navigation, and context-switcher shape', () => {
8811
+ expect(HOME_PATH).toBe('${escapeSingleQuoted(experience.homePath)}');
8812
+ expect(HOME_LABEL).toBe('${escapeSingleQuoted(experience.homeLabel)}');
8813
+ expect(NAVIGATION_MODE).toBe('${experience.navigationMode}');
8814
+ expect(WORKSPACE_RESOURCE.enabled).toBe(${enabled});
8815
+ expect('required' in WORKSPACE_RESOURCE ? WORKSPACE_RESOURCE.required : false).toBe(${required});
8816
+ });
8817
+
8818
+ it('keeps Getting Started out of product navigation', () => {
8819
+ const [home, gettingStarted] = createAppRoutes('app_test');
8820
+ expect(home.path).toBe('${escapeSingleQuoted(experience.homePath)}');
8821
+ ${navExpectation}
8822
+ expect(gettingStarted.path).toBe('/getting-started');
8823
+ expect(gettingStarted.nav).toBeUndefined();
8824
+ });
8825
+ });
8826
+ `;
8827
+ }
8828
+ function platformWebWorkerTest(application, api) {
8829
+ const apiTest = api ? ` it('proxies API paths to the API service binding', async () => {
8830
+ const response = new Response('proxied', { status: 202 });
8831
+ const fetch = vi.fn().mockResolvedValue(response);
8832
+ await expect(worker.fetch(new Request('https://app.test/api/health'), { API: { fetch } })).resolves.toBe(response);
8833
+ expect(fetch).toHaveBeenCalledOnce();
8834
+ });
8835
+
8836
+ ` : "";
8837
+ const env = api ? "{ API: { fetch: vi.fn() } }" : "{}";
8838
+ return `import { describe, expect, it, vi } from 'vitest';
8839
+ import worker from '../src/worker';
8840
+
8841
+ describe('browser worker routing', () => {
8842
+ ${apiTest}
8843
+ it('returns the browser service descriptor for asset paths', async () => {
8844
+ const response = await worker.fetch(new Request('https://app.test/dashboard'), ${env});
8845
+ expect(response.status).toBe(200);
8846
+ await expect(response.json()).resolves.toEqual({ ok: true, service: '${escapeSingleQuoted(toTitle(application.name))}' });
8847
+ });
8848
+ });
8849
+ `;
8850
+ }
8851
+ function platformApiHealthTest(application) {
8852
+ return `import { describe, expect, it } from 'vitest';
8853
+ import worker from '../src/worker';
8854
+ import { resolveRuntimeAppId } from '../src/app-identity';
8855
+
8856
+ const executionContext = {
8857
+ waitUntil() {},
8858
+ passThroughOnException() {},
8859
+ props: {},
8860
+ } as unknown as ExecutionContext;
8861
+
8862
+ describe('API runtime baseline', () => {
8863
+ it('serves a real health response through the generated worker stack', async () => {
8864
+ const response = await worker.fetch(
8865
+ new Request('https://api.test/api/health'),
8866
+ { APP_ID: '${escapeSingleQuoted(application.appId ?? application.id)}' },
8867
+ executionContext,
8868
+ );
8869
+ expect(response.status).toBe(200);
8870
+ await expect(response.json()).resolves.toMatchObject({ ok: true, app: '${escapeSingleQuoted(application.appId ?? application.id)}' });
8871
+ });
8872
+
8873
+ it('requires runtime app identity', async () => {
8874
+ await expect(resolveRuntimeAppId({ APP_ID: ' app_runtime ' })).resolves.toBe('app_runtime');
8875
+ expect(() => resolveRuntimeAppId({})).toThrow('APP_ID is required');
8876
+ });
8877
+ });
8878
+ `;
8879
+ }
8880
+
8664
8881
  // src/commands/app-scaffold.ts
8665
8882
  function standardScriptStub() {
8666
8883
  return `#!/usr/bin/env python3
@@ -8756,6 +8973,14 @@ function applicationScaffoldPayload(application, options = {}) {
8756
8973
  const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
8757
8974
  const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
8758
8975
  const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
8976
+ const experience = {
8977
+ homePath: normalizeHomePath(options.homePath),
8978
+ homeLabel: normalizeHomeLabel(options.homeLabel),
8979
+ navigationMode: normalizeNavigationMode(options.navigationMode),
8980
+ workspaceSwitcher: normalizeWorkspaceSwitcherMode(options.workspaceSwitcher),
8981
+ workspaceLabel: tenancy === "household" ? "Households" : "Workspaces",
8982
+ tagline: String(options.tagline || "").trim() || `Manage your work in ${toTitle(name)}.`
8983
+ };
8759
8984
  const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
8760
8985
  const defaultAppId = target === "platform" ? `app_topolo_${id}` : catalogApplication?.services[0] ?? id;
8761
8986
  const appId = api ? options.appId ?? defaultAppId : null;
@@ -8784,6 +9009,7 @@ function applicationScaffoldPayload(application, options = {}) {
8784
9009
  containerAppId,
8785
9010
  profile,
8786
9011
  resources,
9012
+ experience,
8787
9013
  distributionModel,
8788
9014
  capabilities,
8789
9015
  application: applicationPayload,
@@ -8802,6 +9028,7 @@ function applicationScaffoldPayload(application, options = {}) {
8802
9028
  containerAppId,
8803
9029
  profile,
8804
9030
  resources,
9031
+ experience,
8805
9032
  distributionModel,
8806
9033
  capabilities
8807
9034
  },
@@ -8870,18 +9097,10 @@ function scaffoldPackages(options) {
8870
9097
  }
8871
9098
  const packages = /* @__PURE__ */ new Set();
8872
9099
  if (options.browser) {
8873
- packages.add("@topolo-io/app-funnel");
8874
- packages.add("@topolo-io/app-launcher");
8875
- packages.add("@topolo-io/app-shell");
8876
- packages.add("@topolo-io/auth-client");
8877
- packages.add("@topolo-io/design-tokens");
8878
- packages.add("@topolo-io/onboarding");
8879
- packages.add("@topolo-io/tour");
8880
- packages.add("@topolo-io/ui-kit");
9100
+ for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) packages.add(packageName);
8881
9101
  }
8882
9102
  if (options.api) {
8883
- packages.add("@topolo-io/worker-runtime");
8884
- packages.add("@topolo/sdk");
9103
+ for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) packages.add(packageName);
8885
9104
  }
8886
9105
  return Array.from(packages);
8887
9106
  }
@@ -8961,6 +9180,12 @@ function scaffoldFiles(options) {
8961
9180
  ],
8962
9181
  content: gitignore()
8963
9182
  },
9183
+ ...target === "platform" ? [{
9184
+ path: `${base}/.npmrc`,
9185
+ purpose: "Scoped package registry routing without persisted credentials.",
9186
+ requiredSignals: ["routes private @topolo-io packages to GitHub Packages", "keeps public @topolo packages on npm", "does not contain an authentication token"],
9187
+ content: npmrc()
9188
+ }] : [],
8964
9189
  {
8965
9190
  path: `${base}/AGENTS.md`,
8966
9191
  purpose: "Local agent rules for safe work inside the generated project.",
@@ -8976,42 +9201,44 @@ function scaffoldFiles(options) {
8976
9201
  resources: options.payload.resources
8977
9202
  })
8978
9203
  },
8979
- {
8980
- path: `${browserBase}/vite.config.ts`,
8981
- purpose: "Vite browser build configuration.",
8982
- requiredSignals: [
8983
- "builds the authenticated browser surface",
8984
- "keeps generated apps on the standard React/Vite baseline"
8985
- ],
8986
- content: viteConfig(target)
8987
- },
8988
- {
8989
- path: `${browserBase}/index.html`,
8990
- purpose: "Browser entry document.",
8991
- requiredSignals: [
8992
- "mounts the React app at #root",
8993
- "uses the generated application display name"
8994
- ],
8995
- content: indexHtml(application.name)
8996
- },
8997
- {
8998
- path: `${browserBase}/src/main.tsx`,
8999
- purpose: "React app entrypoint.",
9000
- requiredSignals: [
9001
- "renders the generated app component",
9002
- "imports the generated app stylesheet"
9003
- ],
9004
- content: mainTsx()
9005
- },
9006
- {
9007
- path: `${browserBase}/src/env.d.ts`,
9008
- purpose: "Vite environment variable typing.",
9009
- requiredSignals: [
9010
- "loads vite/client import.meta.env types",
9011
- "declares the Topolo environment variables consumed by the starter"
9012
- ],
9013
- content: envDts(options.api)
9014
- },
9204
+ ...options.browser ? [
9205
+ {
9206
+ path: `${browserBase}/vite.config.ts`,
9207
+ purpose: "Vite browser build configuration.",
9208
+ requiredSignals: [
9209
+ "builds the authenticated browser surface",
9210
+ "keeps generated apps on the standard React/Vite baseline"
9211
+ ],
9212
+ content: viteConfig(target)
9213
+ },
9214
+ {
9215
+ path: `${browserBase}/index.html`,
9216
+ purpose: "Browser entry document.",
9217
+ requiredSignals: [
9218
+ "mounts the React app at #root",
9219
+ "uses the generated application display name"
9220
+ ],
9221
+ content: indexHtml(application.name)
9222
+ },
9223
+ {
9224
+ path: `${browserBase}/src/main.tsx`,
9225
+ purpose: "React app entrypoint.",
9226
+ requiredSignals: [
9227
+ "renders the generated app component",
9228
+ "imports the generated app stylesheet"
9229
+ ],
9230
+ content: mainTsx()
9231
+ },
9232
+ {
9233
+ path: `${browserBase}/src/env.d.ts`,
9234
+ purpose: "Vite environment variable typing.",
9235
+ requiredSignals: [
9236
+ "loads vite/client import.meta.env types",
9237
+ "declares the Topolo environment variables consumed by the starter"
9238
+ ],
9239
+ content: envDts(options.api)
9240
+ }
9241
+ ] : [],
9015
9242
  ...options.payload.resources.workflow ? [
9016
9243
  {
9017
9244
  path: `${base}/src/cloudflare-workers.d.ts`,
@@ -9023,7 +9250,7 @@ function scaffoldFiles(options) {
9023
9250
  content: cloudflareWorkersDts()
9024
9251
  }
9025
9252
  ] : [],
9026
- {
9253
+ ...options.browser ? [{
9027
9254
  path: `${browserBase}/src/styles.css`,
9028
9255
  purpose: "Minimal app-local styles outside the shared Topolo shell.",
9029
9256
  requiredSignals: [
@@ -9031,7 +9258,39 @@ function scaffoldFiles(options) {
9031
9258
  "keeps app-specific layout scoped under generated classes"
9032
9259
  ],
9033
9260
  content: stylesCss()
9034
- }
9261
+ }] : [],
9262
+ ...target === "platform" && options.browser ? [
9263
+ {
9264
+ path: `${browserBase}/src/app-config.ts`,
9265
+ purpose: "Product route, navigation, context-switcher, and entry-surface configuration.",
9266
+ requiredSignals: ["defaults the authenticated home to /dashboard", "keeps product copy separate from shell wiring"],
9267
+ content: platformAppConfigTs(application, options.payload.experience)
9268
+ },
9269
+ {
9270
+ path: `${browserBase}/src/app-routes.tsx`,
9271
+ purpose: "Canonical protected route manifest and product navigation metadata.",
9272
+ requiredSignals: ["uses the configured home route", "keeps Getting Started out of product navigation"],
9273
+ content: platformAppRoutesTsx(options.payload.experience)
9274
+ },
9275
+ {
9276
+ path: `${browserBase}/src/routes/HomeRoute.tsx`,
9277
+ purpose: "Focused authenticated home product surface.",
9278
+ requiredSignals: ["uses shared page primitives", "uses translation-safe product copy"],
9279
+ content: platformHomeRouteTsx()
9280
+ },
9281
+ {
9282
+ path: `${browserBase}/src/routes/GettingStartedRoute.tsx`,
9283
+ purpose: "Focused role-aware onboarding route reached from the account menu.",
9284
+ requiredSignals: ["uses shared onboarding", "returns to the configured home route"],
9285
+ content: platformGettingStartedRouteTsx()
9286
+ },
9287
+ {
9288
+ path: `${browserBase}/src/AppErrorBoundary.tsx`,
9289
+ purpose: "Recoverable render-error boundary for the product root.",
9290
+ requiredSignals: ["uses the shared auth flow error surface", "forwards render errors to shared capture"],
9291
+ content: platformAppErrorBoundaryTsx()
9292
+ }
9293
+ ] : []
9035
9294
  ];
9036
9295
  if (target === "platform") {
9037
9296
  if (options.browser) {
@@ -9082,6 +9341,15 @@ function scaffoldFiles(options) {
9082
9341
  "each contract has a zod input validator, keeping the manifest actions[] honest"
9083
9342
  ],
9084
9343
  content: topoloActionContractsMjs()
9344
+ },
9345
+ {
9346
+ path: `${base}/topolo.action-contracts.d.mts`,
9347
+ purpose: "TypeScript declaration for the generated JavaScript action-contract module.",
9348
+ requiredSignals: [
9349
+ "types topoloActionContracts against the canonical worker-runtime action contract",
9350
+ "keeps generated API typecheck strict without enabling allowJs"
9351
+ ],
9352
+ content: topoloActionContractsDmts()
9085
9353
  }
9086
9354
  ] : [],
9087
9355
  {
@@ -9256,7 +9524,8 @@ function scaffoldFiles(options) {
9256
9524
  ],
9257
9525
  content: appTsx(application, target, {
9258
9526
  audience: options.payload.audience,
9259
- portfolio: options.payload.portfolio
9527
+ portfolio: options.payload.portfolio,
9528
+ experience: options.payload.experience
9260
9529
  })
9261
9530
  });
9262
9531
  if (target === "platform") {
@@ -9267,7 +9536,7 @@ function scaffoldFiles(options) {
9267
9536
  "runs component/unit tests under jsdom",
9268
9537
  "keeps the generated browser surface testable from day one"
9269
9538
  ],
9270
- content: `import { defineConfig } from 'vitest/config';
9539
+ content: target === "platform" ? platformBrowserVitestConfig() : `import { defineConfig } from 'vitest/config';
9271
9540
 
9272
9541
  export default defineConfig({
9273
9542
  test: {
@@ -9277,6 +9546,40 @@ export default defineConfig({
9277
9546
  });
9278
9547
  `
9279
9548
  });
9549
+ if (target === "platform") {
9550
+ files.push(
9551
+ {
9552
+ path: `${browserBase}/test/application-contract.test.tsx`,
9553
+ purpose: "Executable route, navigation, and resource-switcher contract.",
9554
+ requiredSignals: ["asserts the configured product shape", "proves Getting Started is absent from product navigation"],
9555
+ content: platformBrowserContractTest(application, options.payload.experience)
9556
+ },
9557
+ {
9558
+ path: `${browserBase}/test/worker.test.ts`,
9559
+ purpose: "Executable browser Worker routing tests.",
9560
+ requiredSignals: ["proves API proxying", "proves non-API browser fallback"],
9561
+ content: platformWebWorkerTest(application, options.api)
9562
+ },
9563
+ {
9564
+ path: `${browserBase}/src/worker.ts`,
9565
+ purpose: "Browser Worker that serves assets and forwards API paths when an API target exists.",
9566
+ requiredSignals: [
9567
+ "keeps the frontend Worker thin",
9568
+ "only declares an API service binding for API-backed scaffolds"
9569
+ ],
9570
+ content: webWorkerTs(application, options.api)
9571
+ },
9572
+ {
9573
+ path: `${browserBase}/wrangler.toml`,
9574
+ purpose: "Cloudflare Worker static-assets configuration for the browser surface.",
9575
+ requiredSignals: [
9576
+ "serves Vite assets through a Worker with assets",
9577
+ "only binds an API Worker when the scaffold includes one"
9578
+ ],
9579
+ content: platformWebWranglerToml(application, options.api)
9580
+ }
9581
+ );
9582
+ }
9280
9583
  }
9281
9584
  }
9282
9585
  if (options.api) {
@@ -9317,24 +9620,6 @@ export default defineConfig({
9317
9620
  ],
9318
9621
  content: apiAppIdentityTs(application)
9319
9622
  },
9320
- {
9321
- path: `${browserBase}/src/worker.ts`,
9322
- purpose: "Browser Worker that serves assets and forwards API paths to the API Worker binding.",
9323
- requiredSignals: [
9324
- "keeps the frontend Worker thin",
9325
- "routes /api/* to the API service binding instead of owning backend routes locally"
9326
- ],
9327
- content: webWorkerTs(application)
9328
- },
9329
- {
9330
- path: `${browserBase}/wrangler.toml`,
9331
- purpose: "Cloudflare Worker static-assets configuration for the browser surface.",
9332
- requiredSignals: [
9333
- "serves Vite assets through a Worker with assets",
9334
- "binds the frontend Worker to the API Worker for /api/* requests"
9335
- ],
9336
- content: platformWebWranglerToml(application)
9337
- },
9338
9623
  {
9339
9624
  path: `${apiBase}/vitest.config.ts`,
9340
9625
  purpose: "Vitest config for the API worker surface.",
@@ -9342,7 +9627,7 @@ export default defineConfig({
9342
9627
  "runs worker/unit tests under the node environment",
9343
9628
  "keeps the generated service testable from day one"
9344
9629
  ],
9345
- content: `import { defineConfig } from 'vitest/config';
9630
+ content: target === "platform" ? platformApiVitestConfig() : `import { defineConfig } from 'vitest/config';
9346
9631
 
9347
9632
  export default defineConfig({
9348
9633
  test: {
@@ -9359,7 +9644,7 @@ export default defineConfig({
9359
9644
  "asserts the scaffold boots",
9360
9645
  "is replaced by real route/repository tests as the surface grows"
9361
9646
  ],
9362
- content: "import { describe, expect, it } from 'vitest';\n\n// Smoke test so the API test suite is non-empty from day one. Replace with\n// real route/repository tests as the surface grows.\ndescribe('api scaffold', () => {\n it('boots', () => {\n expect(true).toBe(true);\n });\n});\n"
9647
+ content: target === "platform" ? platformApiHealthTest(application) : "import { describe, expect, it } from 'vitest';\n\ndescribe('api scaffold', () => {\n it('boots', () => {\n expect(true).toBe(true);\n });\n});\n"
9363
9648
  }
9364
9649
  );
9365
9650
  if (options.payload.resources.d1) {
@@ -10125,7 +10410,7 @@ function registerPlatformCommand(program2) {
10125
10410
  ).option("--json", "Output JSON").option("--no-json", "Output human-readable checklist").option("--app-dir <path>", "Application directory under apps/").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(
10126
10411
  "--surface-model <surfaceModel>",
10127
10412
  "First-party surface model: web_app, mobile_container, mobile_module, or messaging_surface"
10128
- ).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("--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(
10413
+ ).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(
10129
10414
  "--provision",
10130
10415
  "Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
10131
10416
  ).option(
@@ -10160,7 +10445,12 @@ function registerPlatformCommand(program2) {
10160
10445
  withR2: opts.withR2,
10161
10446
  withQueue: opts.withQueue,
10162
10447
  withWorkflow: opts.withWorkflow,
10163
- withContainerProcessing: opts.withContainerProcessing
10448
+ withContainerProcessing: opts.withContainerProcessing,
10449
+ homePath: opts.homePath,
10450
+ homeLabel: opts.homeLabel,
10451
+ navigationMode: opts.navigationMode,
10452
+ workspaceSwitcher: opts.workspaceSwitcher,
10453
+ tagline: opts.tagline
10164
10454
  });
10165
10455
  if (opts.workflowOnly && !payload.ciWorkflow) {
10166
10456
  throw new Error("--workflow-only is only available for first-party scaffolds.");