@topolo/cli 0.4.6 → 0.4.9

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 +863 -617
  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);
@@ -3504,7 +3504,7 @@ function createTopolo(options) {
3504
3504
  };
3505
3505
  }
3506
3506
 
3507
- // ../topolo-sdk/dist/node/credential-store.js
3507
+ // node_modules/@topolo/sdk/dist/node/credential-store.js
3508
3508
  import { mkdir, readFile, writeFile, chmod } from "fs/promises";
3509
3509
  import { existsSync } from "fs";
3510
3510
  import { homedir, platform } from "os";
@@ -5792,6 +5792,42 @@ import { dirname as dirname3, resolve as resolve2 } from "path";
5792
5792
  import { constants } from "fs";
5793
5793
  import { access } from "fs/promises";
5794
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
+ }
5822
+ function normalizeFirstPartyAppDir(value) {
5823
+ const requested = String(value).trim().replace(/\\/g, "/").replace(/^\.\//, "");
5824
+ const appDir = requested.startsWith("apps/") ? requested.slice("apps/".length) : requested;
5825
+ const segments = appDir.split("/");
5826
+ if (!appDir || appDir.startsWith("/") || appDir.startsWith("apps/") || segments.some((segment) => !segment || segment === "." || segment === "..")) {
5827
+ throw new Error("Invalid first-party app directory. Use a repository name such as TopoloForms or apps/TopoloForms.");
5828
+ }
5829
+ return appDir;
5830
+ }
5795
5831
  function normalizeTarget(value) {
5796
5832
  if (!value) return "platform";
5797
5833
  if (value === "platform" || value === "developer") return value;
@@ -5934,18 +5970,60 @@ var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
5934
5970
  var DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID = "0ebdeec262a245481a721fc4f9d6d798";
5935
5971
  var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
5936
5972
 
5973
+ // src/commands/app-scaffold-package-policy.ts
5974
+ var TOPOLO_PACKAGE_SCAFFOLD_POLICY = {
5975
+ "@topolo-io/api-key-management": "browser-baseline",
5976
+ "@topolo-io/app-funnel": "browser-baseline",
5977
+ "@topolo-io/app-launcher": "browser-baseline",
5978
+ "@topolo-io/app-manifest": "domain-opt-in",
5979
+ "@topolo-io/app-shell": "browser-baseline",
5980
+ "@topolo-io/auth-client": "browser-baseline",
5981
+ "@topolo-io/auth-middleware": "api-baseline",
5982
+ "@topolo-io/billing-manifest": "domain-opt-in",
5983
+ "@topolo-io/billing-ui": "domain-opt-in",
5984
+ "@topolo-io/bridge": "domain-opt-in",
5985
+ "@topolo-io/calendar": "domain-opt-in",
5986
+ "@topolo-io/chat-widget": "browser-baseline",
5987
+ "@topolo-io/data": "domain-opt-in",
5988
+ "@topolo-io/design-tokens": "browser-baseline",
5989
+ "@topolo-io/error-middleware": "api-baseline",
5990
+ "@topolo-io/mailbox": "domain-opt-in",
5991
+ "@topolo-io/mobile-error-middleware": "domain-opt-in",
5992
+ "@topolo-io/mobile-experience-contracts": "domain-opt-in",
5993
+ "@topolo-io/nexus-client": "api-baseline",
5994
+ "@topolo-io/node-canvas": "domain-opt-in",
5995
+ "@topolo-io/notifications": "browser-baseline",
5996
+ "@topolo-io/onboarding": "browser-baseline",
5997
+ "@topolo-io/platform-mcp": "tooling",
5998
+ "@topolo-io/resource-context": "browser-baseline",
5999
+ "@topolo-io/security-headers": "api-baseline",
6000
+ "@topolo-io/tour": "browser-baseline",
6001
+ "@topolo-io/ui-kit": "browser-baseline",
6002
+ "@topolo-io/widget-server": "api-baseline",
6003
+ "@topolo-io/worker-runtime": "api-baseline",
6004
+ "@topolo/cli": "tooling",
6005
+ "@topolo/login": "developer-baseline",
6006
+ "@topolo/mcp": "tooling",
6007
+ "@topolo/sdk": "api-baseline"
6008
+ };
6009
+ function packagesForScaffoldClass(scaffoldClass) {
6010
+ return Object.entries(TOPOLO_PACKAGE_SCAFFOLD_POLICY).filter(([, value]) => value === scaffoldClass).map(([name]) => name).sort();
6011
+ }
6012
+ var PLATFORM_BROWSER_TOPOLO_PACKAGES = packagesForScaffoldClass("browser-baseline");
6013
+ var PLATFORM_API_TOPOLO_PACKAGES = packagesForScaffoldClass("api-baseline");
6014
+
5937
6015
  // src/commands/app-scaffold-package-template.ts
5938
6016
  function packageJson(application, target, options) {
5939
6017
  const dependencies = {};
5940
6018
  const devDependencies = {
5941
6019
  "@types/node": "^20.12.0",
5942
6020
  typescript: "^5.4.0",
5943
- vite: "^5.4.0"
6021
+ ...options.browser ? { vite: "^5.4.0" } : {}
5944
6022
  };
5945
6023
  const scripts = {
5946
- dev: target === "platform" ? "vite --config apps/web/vite.config.ts --host 0.0.0.0" : "vite --host 0.0.0.0",
5947
- build: target === "platform" ? "vite build --config apps/web/vite.config.ts" : "vite build",
5948
- preview: target === "platform" ? "vite preview --config apps/web/vite.config.ts --host 0.0.0.0" : "vite preview --host 0.0.0.0",
6024
+ 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",
6025
+ 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",
6026
+ 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",
5949
6027
  typecheck: "tsc --noEmit"
5950
6028
  };
5951
6029
  if (options.browser) {
@@ -5955,29 +6033,20 @@ function packageJson(application, target, options) {
5955
6033
  devDependencies["@types/react-dom"] = "^19.2.2";
5956
6034
  devDependencies["@vitejs/plugin-react"] = "^4.7.0";
5957
6035
  if (target === "platform") {
5958
- dependencies["@topolo-io/auth-client"] = topoloPackageVersion("@topolo-io/auth-client", target);
5959
- dependencies["@topolo-io/app-funnel"] = topoloPackageVersion("@topolo-io/app-funnel", target);
5960
- dependencies["@topolo-io/app-launcher"] = topoloPackageVersion("@topolo-io/app-launcher", target);
5961
- dependencies["@topolo-io/app-shell"] = topoloPackageVersion("@topolo-io/app-shell", target);
5962
- dependencies["@topolo-io/design-tokens"] = topoloPackageVersion("@topolo-io/design-tokens", target);
5963
- dependencies["@topolo-io/onboarding"] = topoloPackageVersion("@topolo-io/onboarding", target);
5964
- dependencies["@topolo-io/tour"] = topoloPackageVersion("@topolo-io/tour", target);
5965
- dependencies["@topolo-io/ui-kit"] = topoloPackageVersion("@topolo-io/ui-kit", target);
6036
+ for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) {
6037
+ dependencies[packageName] = topoloPackageVersion(packageName, target);
6038
+ }
5966
6039
  dependencies["react-router-dom"] = "^7.6.2";
5967
6040
  } else {
5968
6041
  dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
5969
6042
  }
5970
6043
  }
5971
6044
  if (options.api) {
5972
- devDependencies["@cloudflare/workers-types"] = "^4.20260413.1";
6045
+ devDependencies["@cloudflare/workers-types"] = "^5.20260716.1";
5973
6046
  if (target === "platform") {
5974
- dependencies["@topolo-io/worker-runtime"] = topoloPackageVersion("@topolo-io/worker-runtime", target);
5975
- dependencies["@topolo-io/auth-middleware"] = topoloPackageVersion("@topolo-io/auth-middleware", target);
5976
- dependencies["@topolo-io/error-middleware"] = topoloPackageVersion("@topolo-io/error-middleware", target);
5977
- dependencies["@topolo-io/security-headers"] = topoloPackageVersion("@topolo-io/security-headers", target);
5978
- dependencies["@topolo-io/resource-context"] = topoloPackageVersion("@topolo-io/resource-context", target);
5979
- dependencies["@topolo-io/data"] = topoloPackageVersion("@topolo-io/data", target);
5980
- dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
6047
+ for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) {
6048
+ dependencies[packageName] = topoloPackageVersion(packageName, target);
6049
+ }
5981
6050
  dependencies.hono = "^4";
5982
6051
  dependencies.zod = "^4.4.3";
5983
6052
  scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
@@ -5996,7 +6065,7 @@ function packageJson(application, target, options) {
5996
6065
  if (testDirs.length > 0) {
5997
6066
  const chain = (withCoverage) => testDirs.map((dir, index) => {
5998
6067
  const enter = index === 0 ? `cd ${dir}` : `cd ../${dir.split("/").pop()}`;
5999
- return `${enter} && vitest run --config vitest.config.ts --passWithNoTests${withCoverage ? " --coverage" : ""}`;
6068
+ return `${enter} && vitest run --config vitest.config.ts${withCoverage ? " --coverage" : ""}`;
6000
6069
  }).join(" && ");
6001
6070
  scripts.test = chain(false);
6002
6071
  scripts.coverage = chain(true);
@@ -6011,7 +6080,11 @@ function packageJson(application, target, options) {
6011
6080
  }
6012
6081
  }
6013
6082
  if (target === "platform" && hasScaffoldResources(options.resources)) {
6014
- 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";
6083
+ const deployChecks = [
6084
+ ...options.api ? ["wrangler deploy --config apps/api/wrangler.toml --env staging --dry-run --outdir .wrangler/api-build"] : [],
6085
+ ...options.browser ? ["wrangler deploy --config apps/web/wrangler.toml --env staging --dry-run --outdir .wrangler/web-build"] : []
6086
+ ];
6087
+ scripts["check:deploy-config"] = deployChecks.join(" && ");
6015
6088
  }
6016
6089
  return `${json({
6017
6090
  name: application.packageName,
@@ -6032,29 +6105,8 @@ function topoloPackageVersion(packageName, target) {
6032
6105
  if (packageName === "@topolo/sdk") return "^0.8.1";
6033
6106
  return "^0.1.0";
6034
6107
  }
6035
- const platformPackageVersions = {
6036
- "@topolo/sdk": "^0.8.1",
6037
- "@topolo/login": "^0.1.0",
6038
- "@topolo-io/app-funnel": "^0.1.6",
6039
- "@topolo-io/app-launcher": "^0.1.15",
6040
- "@topolo-io/app-shell": "^0.8.4",
6041
- "@topolo-io/auth-client": "^1.2.1",
6042
- "@topolo-io/auth-middleware": "^0.4.0",
6043
- "@topolo-io/error-middleware": "^0.6.0",
6044
- "@topolo-io/security-headers": "^0.2.2",
6045
- "@topolo-io/resource-context": "^0.3.0",
6046
- "@topolo-io/data": "^0.1.2",
6047
- "@topolo-io/worker-runtime": "^0.1.11",
6048
- "@topolo-io/design-tokens": "^0.1.8",
6049
- "@topolo-io/onboarding": "^0.9.5",
6050
- "@topolo-io/tour": "^0.3.4",
6051
- "@topolo-io/ui-kit": "^0.4.233"
6052
- };
6053
- const version = platformPackageVersions[packageName];
6054
- if (!version) {
6055
- throw new Error(`Unknown Topolo platform package: ${packageName}`);
6056
- }
6057
- return version;
6108
+ if (packageName.startsWith("@topolo/") || packageName.startsWith("@topolo-io/")) return "latest";
6109
+ throw new Error(`Unknown Topolo platform package: ${packageName}`);
6058
6110
  }
6059
6111
  function hasScaffoldResources(resources) {
6060
6112
  return resources.d1 || resources.r2 || resources.queue || resources.workflow || resources.containerProcessing;
@@ -6129,10 +6181,11 @@ function readme(application, target, options) {
6129
6181
  "## Development",
6130
6182
  "",
6131
6183
  "```bash",
6132
- "npm install",
6133
- "npm run typecheck",
6134
- "npm run build",
6135
- "npm run dev",
6184
+ target === "platform" ? "pnpm install" : "npm install",
6185
+ target === "platform" ? "pnpm run typecheck" : "npm run typecheck",
6186
+ target === "platform" ? "pnpm run coverage" : "npm run build",
6187
+ target === "platform" ? "pnpm run build" : "npm run dev",
6188
+ ...target === "platform" ? ["pnpm run dev"] : [],
6136
6189
  "```",
6137
6190
  ""
6138
6191
  ];
@@ -6145,11 +6198,15 @@ function readme(application, target, options) {
6145
6198
  "- `@topolo-io/app-launcher` provides shared app switching and handoff helpers.",
6146
6199
  "- `@topolo-io/ui-kit` provides primitive account, command palette, icon, transition, and bug reporter UI.",
6147
6200
  "- `@topolo-io/auth-client` owns browser auth, callback exchange, token refresh, and logout.",
6148
- "- 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.",
6201
+ "- `@topolo-io/api-key-management`, `@topolo-io/chat-widget`, `@topolo-io/notifications`, and `@topolo-io/resource-context` complete the AppShell capability baseline.",
6202
+ "- Every Topolo-owned browser dependency is emitted at `latest`; product-domain packages are added only when the app actually adopts that domain.",
6203
+ "- `.npmrc` routes private `@topolo-io` packages to GitHub Packages without storing credentials; public `@topolo` packages remain on npm. Local and CI environments must provide a GitHub Packages token.",
6204
+ "- Generated first-party browser apps start on the shared landing page at `/`, use the shared login page at `/login`, and complete sign-in on `/auth/callback` before entering the configured authenticated home.",
6205
+ "- 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.",
6149
6206
  "- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
6150
6207
  );
6151
6208
  if (options.api) {
6152
- 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.");
6209
+ 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.");
6153
6210
  }
6154
6211
  if (hasScaffoldResources(options.resources)) {
6155
6212
  lines.push(
@@ -6352,6 +6409,15 @@ function gitignore() {
6352
6409
  ""
6353
6410
  ].join("\n");
6354
6411
  }
6412
+ function npmrc() {
6413
+ return [
6414
+ "@topolo-io:registry=https://npm.pkg.github.com",
6415
+ "strict-peer-dependencies=true",
6416
+ "minimum-release-age-exclude[]=@topolo-io/*",
6417
+ "minimum-release-age-exclude[]=@topolo/*",
6418
+ ""
6419
+ ].join("\n");
6420
+ }
6355
6421
  function envDts(api) {
6356
6422
  const lines = [
6357
6423
  '/// <reference types="vite/client" />',
@@ -6612,46 +6678,6 @@ function stylesCss() {
6612
6678
  "textarea {",
6613
6679
  " font: inherit;",
6614
6680
  "}",
6615
- "",
6616
- ".topolo-starter-workspace {",
6617
- " display: grid;",
6618
- " gap: 18px;",
6619
- " min-width: 0;",
6620
- " width: auto;",
6621
- " max-width: min(100%, calc(100vw - 56px));",
6622
- " overflow-x: clip;",
6623
- "}",
6624
- "",
6625
- ".topolo-starter-card-stack {",
6626
- " display: grid;",
6627
- " gap: 18px;",
6628
- " min-width: 0;",
6629
- "}",
6630
- "",
6631
- ".topolo-starter-workspace > *,",
6632
- ".topolo-starter-card-stack > * {",
6633
- " min-width: 0;",
6634
- "}",
6635
- "",
6636
- ".topolo-starter-card h2 {",
6637
- " margin: 0 0 12px;",
6638
- " font-size: 24px;",
6639
- " line-height: 1.2;",
6640
- "}",
6641
- "",
6642
- ".topolo-starter-card p {",
6643
- " margin: 0;",
6644
- " color: hsl(var(--muted-foreground));",
6645
- " line-height: 1.55;",
6646
- "}",
6647
- "",
6648
- ".topolo-starter-card {",
6649
- " border: 1px solid hsl(var(--border));",
6650
- " border-radius: var(--radius);",
6651
- " padding: 20px;",
6652
- " background: hsl(var(--card));",
6653
- " color: hsl(var(--card-foreground));",
6654
- "}",
6655
6681
  ""
6656
6682
  ].join("\n");
6657
6683
  }
@@ -6659,450 +6685,429 @@ function stylesCss() {
6659
6685
  // src/commands/app-scaffold-app-template.ts
6660
6686
  function appTsx(application, target, options) {
6661
6687
  if (target === "developer") return developerAppTsx(application);
6662
- return platformAppTsx(application, options);
6663
- }
6664
- function platformAppTsx(application, options) {
6665
- const isPersonal = options.audience === "personal";
6666
- const tagline = isPersonal ? "Topolo personal app" : "Topolo workspace";
6667
- const workspaceTitle = isPersonal ? "Personal app" : "Workspace";
6668
- const shellLabel = isPersonal ? "personal app shell" : "workspace shell";
6669
- const baselineHeading = isPersonal ? "Shared personal baseline" : "Shared workspace baseline";
6670
- 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.";
6688
+ return platformAppTsx();
6689
+ }
6690
+ function platformAppTsx() {
6691
+ return `import { useEffect, useMemo, useState } from 'react';
6692
+ import { LandingPage } from '@topolo-io/app-funnel';
6693
+ import { LoginPage, TopoloAppLoadingScaffold, TopoloAppRoot, TopoloAuthFlowScreen, createTopoloAuth } from '@topolo-io/app-shell';
6694
+ import { setupBugReporterErrorCapture } from '@topolo-io/ui-kit';
6695
+ import AppErrorBoundary from './AppErrorBoundary';
6696
+ import { APP_NAME, AUTH_URL, HOME_PATH, LANDING_CONFIG, LOGIN_CONFIG, NAVIGATION_MODE, WORKSPACE_RESOURCE } from './app-config';
6697
+ import { createAppRoutes } from './app-routes';
6698
+ import { resolveAppId } from './app-identity';
6699
+
6700
+ setupBugReporterErrorCapture();
6701
+
6702
+ export default function App() {
6703
+ return (
6704
+ <AppErrorBoundary>
6705
+ <CanonicalAppRoot />
6706
+ </AppErrorBoundary>
6707
+ );
6708
+ }
6709
+
6710
+ export function CanonicalAppRoot() {
6711
+ const [appId, setAppId] = useState('');
6712
+ const [appIdentityError, setAppIdentityError] = useState('');
6713
+
6714
+ useEffect(() => {
6715
+ let active = true;
6716
+ setAppIdentityError('');
6717
+ void resolveAppId()
6718
+ .then((nextAppId) => {
6719
+ if (active) setAppId(nextAppId);
6720
+ })
6721
+ .catch((error) => {
6722
+ if (active) setAppIdentityError(errorMessage(error));
6723
+ });
6724
+ return () => {
6725
+ active = false;
6726
+ };
6727
+ }, []);
6728
+
6729
+ const auth = useMemo(() => appId ? createTopoloAuth({
6730
+ authServiceUrl: AUTH_URL,
6731
+ appId,
6732
+ devSessionKey: \`\${appId}_session\`,
6733
+ callbackPath: '/auth/callback',
6734
+ sessionStorageRestoreEnabled: import.meta.env.DEV,
6735
+ debug: import.meta.env.DEV,
6736
+ }) : null, [appId]);
6737
+ const routes = useMemo(() => appId ? createAppRoutes(appId) : [], [appId]);
6738
+
6739
+ if (appIdentityError) {
6740
+ return (
6741
+ <TopoloAuthFlowScreen
6742
+ appName={APP_NAME}
6743
+ status="error"
6744
+ title="App identity unavailable"
6745
+ message={appIdentityError}
6746
+ actionLabel="Retry"
6747
+ onAction={() => window.location.reload()}
6748
+ />
6749
+ );
6750
+ }
6751
+
6752
+ if (!auth) {
6753
+ return (
6754
+ <TopoloAppLoadingScaffold
6755
+ appId={appId}
6756
+ appName={APP_NAME}
6757
+ layout={{ shell: NAVIGATION_MODE, content: 'workspace' }}
6758
+ />
6759
+ );
6760
+ }
6761
+
6762
+ return (
6763
+ <TopoloAppRoot
6764
+ appName={APP_NAME}
6765
+ appId={appId}
6766
+ auth={auth}
6767
+ routes={routes}
6768
+ home={HOME_PATH}
6769
+ workspaceResource={WORKSPACE_RESOURCE}
6770
+ gettingStarted={{ path: '/getting-started' }}
6771
+ publicLanding={
6772
+ <LandingPage
6773
+ appId={appId}
6774
+ authApiUrl={AUTH_URL}
6775
+ config={LANDING_CONFIG}
6776
+ onCTAClick={() => window.location.assign('/login')}
6777
+ />
6778
+ }
6779
+ loginElement={
6780
+ <LoginPage
6781
+ appId={appId}
6782
+ authApiUrl={AUTH_URL}
6783
+ config={LOGIN_CONFIG}
6784
+ onLogin={async (provider) => {
6785
+ await auth.login(provider || 'topolo');
6786
+ }}
6787
+ onPasswordLogin={async (request) => {
6788
+ const result = await auth.loginWithPassword(request);
6789
+ if (!result.authenticated) return result;
6790
+ window.dispatchEvent(new CustomEvent('auth:session-updated', { detail: { tokens: result.tokens, user: result.user } }));
6791
+ return undefined;
6792
+ }}
6793
+ />
6794
+ }
6795
+ loadingFallback={({ loading }) => (
6796
+ <TopoloAppLoadingScaffold
6797
+ appId={appId}
6798
+ appName={APP_NAME}
6799
+ layout={{ shell: loading?.shell || NAVIGATION_MODE, content: loading?.content || 'workspace' }}
6800
+ />
6801
+ )}
6802
+ shell={{
6803
+ appId,
6804
+ authUrl: AUTH_URL,
6805
+ navigationMode: NAVIGATION_MODE,
6806
+ title: APP_NAME,
6807
+ commandPlaceholder: \`Search \${APP_NAME}\`,
6808
+ bugReporterAppName: APP_NAME,
6809
+ enableCommandPalette: true,
6810
+ enableBugReporter: true,
6811
+ }}
6812
+ />
6813
+ );
6814
+ }
6815
+
6816
+ export function errorMessage(error: unknown): string {
6817
+ if (error instanceof Error && error.message) return error.message;
6818
+ return 'Unexpected authentication error.';
6819
+ }
6820
+ `;
6821
+ }
6822
+ function platformAppConfigTs(application, experience) {
6823
+ const appName = toTitle(application.name);
6824
+ const workspaceResource = experience.workspaceSwitcher === "off" ? "{ enabled: false }" : `{ enabled: true, required: ${experience.workspaceSwitcher === "required"}, label: '${escapeSingleQuoted(experience.workspaceLabel)}' }`;
6825
+ return `import { SCAFFOLD_APP_NAME, getBrowserAuthBaseUrl } from './app-identity';
6826
+
6827
+ export const APP_NAME = SCAFFOLD_APP_NAME;
6828
+ export const AUTH_URL = getBrowserAuthBaseUrl();
6829
+ export const HOME_PATH = '${escapeSingleQuoted(experience.homePath)}';
6830
+ export const HOME_LABEL = '${escapeSingleQuoted(experience.homeLabel)}';
6831
+ export const NAVIGATION_MODE = '${experience.navigationMode}' as const;
6832
+ export const WORKSPACE_RESOURCE = ${workspaceResource} as const;
6833
+ export const PRODUCT_TAGLINE = '${escapeSingleQuoted(experience.tagline)}';
6834
+
6835
+ export const LANDING_CONFIG = {
6836
+ app_name: APP_NAME,
6837
+ app_tagline: PRODUCT_TAGLINE,
6838
+ hero_title: APP_NAME,
6839
+ hero_subtitle: PRODUCT_TAGLINE,
6840
+ hero_cta_text: 'Sign in',
6841
+ hero_cta_url: '/login',
6842
+ cta_title: 'Ready to continue?',
6843
+ cta_subtitle: 'Sign in with your Topolo account.',
6844
+ };
6845
+
6846
+ export const LOGIN_CONFIG = {
6847
+ app_name: APP_NAME,
6848
+ app_tagline: PRODUCT_TAGLINE,
6849
+ hero_title: \`Sign in to \${APP_NAME}\`,
6850
+ hero_subtitle: PRODUCT_TAGLINE,
6851
+ authenticated_home_path: HOME_PATH,
6852
+ };
6853
+
6854
+ export const APP_DISPLAY_NAME = '${escapeSingleQuoted(appName)}';
6855
+ `;
6856
+ }
6857
+ function platformAppRoutesTsx(experience) {
6858
+ const nav = experience.navigationMode === "none" ? "" : " nav: { id: 'home', label: HOME_LABEL },\n";
6859
+ return `import type { AppRoute } from '@topolo-io/app-shell';
6860
+ import { HOME_LABEL, HOME_PATH, NAVIGATION_MODE } from './app-config';
6861
+ import GettingStartedRoute from './routes/GettingStartedRoute';
6862
+ import HomeRoute from './routes/HomeRoute';
6863
+
6864
+ export function createAppRoutes(appId: string): AppRoute[] {
6671
6865
  return [
6672
- "import { Component, useCallback, useEffect, useMemo, useState, type ErrorInfo, type ReactNode } from 'react';",
6673
- "import { LandingPage } from '@topolo-io/app-funnel';",
6674
- "import { LoginPage, TopoloAppLoadingScaffold, TopoloAppRoot, TopoloAuthFlowScreen, createTopoloAuth, useAuth, type AppRoute as TopoloAppRoute } from '@topolo-io/app-shell';",
6675
- "import { setupBugReporterErrorCapture } from '@topolo-io/ui-kit';",
6676
- "import { GettingStarted, type OnboardingGuideStep } from '@topolo-io/onboarding';",
6677
- "import { IntroTour } from '@topolo-io/tour';",
6678
- "import { SCAFFOLD_APP_NAME, getBrowserAuthBaseUrl, resolveAppId } from './app-identity';",
6679
- "",
6680
- "// Wire global error/console capture so BugReporter (enabled in TopoloAppShell below)",
6681
- "// can attach the last few errors to any submitted bug report.",
6682
- "setupBugReporterErrorCapture();",
6683
- "",
6684
- "const APP_NAME = SCAFFOLD_APP_NAME;",
6685
- "const AUTH_URL = getBrowserAuthBaseUrl();",
6686
- "const LANDING_CONFIG = {",
6687
- " app_name: APP_NAME,",
6688
- ` app_tagline: '${escapeSingleQuoted(tagline)}',`,
6689
- " hero_title: `Open ${APP_NAME} in Topolo`,",
6690
- ` hero_subtitle: 'Shared landing, login, callback, auth, and ${escapeSingleQuoted(shellLabel)} wiring are already included in the generated baseline.',`,
6691
- " hero_cta_text: 'Sign in',",
6692
- " hero_cta_url: '/login',",
6693
- " cta_title: 'Start from the shared platform entry flow',",
6694
- ` cta_subtitle: 'Unauthenticated routes stay on the shared Topolo surfaces before users enter the signed ${escapeSingleQuoted(shellLabel)}.',`,
6695
- "};",
6696
- "const LOGIN_CONFIG = {",
6697
- " app_name: APP_NAME,",
6698
- ` app_tagline: '${escapeSingleQuoted(tagline)}',`,
6699
- " hero_title: `Sign in to ${APP_NAME}`,",
6700
- ` hero_subtitle: 'Use Topolo Auth to open the shared ${escapeSingleQuoted(shellLabel)} for this application.',`,
6701
- " authenticated_home_path: '/dashboard',",
6702
- "};",
6703
- "",
6704
- "export default function App() {",
6705
- " return (",
6706
- " <AppErrorBoundary>",
6707
- " <CanonicalAppRoot />",
6708
- " </AppErrorBoundary>",
6709
- " );",
6710
- "}",
6711
- "",
6712
- "function CanonicalAppRoot() {",
6713
- " const [appId, setAppId] = useState('');",
6714
- " const [appIdentityError, setAppIdentityError] = useState('');",
6715
- "",
6716
- " useEffect(() => {",
6717
- " let active = true;",
6718
- " setAppIdentityError('');",
6719
- " void resolveAppId()",
6720
- " .then((nextAppId) => {",
6721
- " if (active) setAppId(nextAppId);",
6722
- " })",
6723
- " .catch((error) => {",
6724
- " if (!active) return;",
6725
- " setAppIdentityError(errorMessage(error));",
6726
- " });",
6727
- " return () => {",
6728
- " active = false;",
6729
- " };",
6730
- " }, []);",
6731
- "",
6732
- " const auth = useMemo(() => appId ? createTopoloAuth({",
6733
- " authServiceUrl: AUTH_URL,",
6734
- " appId,",
6735
- " devSessionKey: `${appId}_session`,",
6736
- " callbackPath: '/auth/callback',",
6737
- " sessionStorageRestoreEnabled: import.meta.env.DEV,",
6738
- " debug: import.meta.env.DEV,",
6739
- " }) : null, [appId]);",
6740
- "",
6741
- " const routes = useMemo<TopoloAppRoute[]>(() => appId ? [",
6742
- " {",
6743
- " path: '/workspace',",
6744
- " nav: { id: 'home', label: '" + escapeSingleQuoted(workspaceTitle) + "' },",
6745
- " loading: { shell: 'topbar', content: 'workspace', title: '" + escapeSingleQuoted(workspaceTitle) + "' },",
6746
- " element: <ScaffoldWorkspace />,",
6747
- " },",
6748
- " {",
6749
- " path: '/getting-started',",
6750
- " nav: { id: 'getting-started', label: 'Getting started' },",
6751
- " loading: { shell: 'topbar', content: 'card-grid', title: 'Getting started' },",
6752
- " element: <ScaffoldGettingStartedRoute appId={appId} />,",
6753
- " },",
6754
- " ] : [], [appId]);",
6755
- "",
6756
- " if (appIdentityError) {",
6757
- " return (",
6758
- " <TopoloAuthFlowScreen",
6759
- " appName={APP_NAME}",
6760
- " appId={appId}",
6761
- ' status="error"',
6762
- ' title="App identity unavailable"',
6763
- " message={appIdentityError}",
6764
- ' actionLabel="Retry"',
6765
- " onAction={() => window.location.reload()}",
6766
- " />",
6767
- " );",
6768
- " }",
6769
- "",
6770
- " if (!auth) {",
6771
- " // Pre-AppRoot: appId is still resolving, so auth can't be built yet.",
6772
- " // Render the SAME scaffold layout AppRoot uses below so this phase and",
6773
- " // the auth-gate fallback read as one continuous loading state (the",
6774
- " // scaffold resolves theme + rail width on its own to match the shell).",
6775
- " return (",
6776
- " <TopoloAppLoadingScaffold",
6777
- " appId={appId}",
6778
- " appName={APP_NAME}",
6779
- " layout={{ shell: 'topbar', content: 'workspace' }}",
6780
- " />",
6781
- " );",
6782
- " }",
6783
- "",
6784
- " return (",
6785
- " <TopoloAppRoot",
6786
- " appName={APP_NAME}",
6787
- " appId={appId}",
6788
- " auth={auth}",
6789
- " routes={routes}",
6790
- ' home="/workspace"',
6791
- " publicLanding={",
6792
- " <LandingPage",
6793
- " appId={appId}",
6794
- " authApiUrl={AUTH_URL}",
6795
- " config={LANDING_CONFIG}",
6796
- " onCTAClick={() => window.location.assign('/login')}",
6797
- " />",
6798
- " }",
6799
- " loginElement={",
6800
- " <LoginPage",
6801
- " appId={appId}",
6802
- " authApiUrl={AUTH_URL}",
6803
- " config={LOGIN_CONFIG}",
6804
- " onLogin={async (provider) => {",
6805
- " await auth.login(provider || 'topolo');",
6806
- " }}",
6807
- " onPasswordLogin={async (request) => {",
6808
- " const result = await auth.loginWithPassword(request);",
6809
- " if (!result.authenticated) return result;",
6810
- " window.dispatchEvent(new CustomEvent('auth:session-updated', { detail: { tokens: result.tokens, user: result.user } }));",
6811
- " return undefined;",
6812
- " }}",
6813
- " />",
6814
- " }",
6815
- " loadingFallback={({ loading }) => (",
6816
- " <TopoloAppLoadingScaffold",
6817
- " appId={appId}",
6818
- " appName={APP_NAME}",
6819
- " layout={{ shell: loading?.shell || 'topbar', content: loading?.content || 'workspace' }}",
6820
- " />",
6821
- " )}",
6822
- " shell={{",
6823
- " appId: appId,",
6824
- " authUrl: AUTH_URL,",
6825
- " navigationMode: 'topbar',",
6826
- " title: APP_NAME,",
6827
- " commandPlaceholder: `Search ${APP_NAME}`,",
6828
- " bugReporterAppName: APP_NAME,",
6829
- " enableCommandPalette: true,",
6830
- " enableBugReporter: true,",
6831
- " }}",
6832
- " />",
6833
- " );",
6834
- "}",
6835
- "",
6836
- "function ScaffoldWorkspace() {",
6837
- " const { user } = useAuth();",
6838
- " const openWorkspace = useCallback(() => {",
6839
- " window.history.pushState({}, '', '/workspace');",
6840
- " window.dispatchEvent(new PopStateEvent('popstate'));",
6841
- " }, []);",
6842
- " const openGettingStarted = useCallback(() => {",
6843
- " window.history.pushState({}, '', '/getting-started');",
6844
- " window.dispatchEvent(new PopStateEvent('popstate'));",
6845
- " }, []);",
6846
- " return (",
6847
- ' <div className="topolo-starter-workspace">',
6848
- ' <section className="topolo-starter-card" data-tour="workspace-card">',
6849
- ` <h2>${baselineHeading}</h2>`,
6850
- " <p>",
6851
- ` ${baselineDescription}`,
6852
- " </p>",
6853
- " </section>",
6854
- " <IntroTour",
6855
- " appName={APP_NAME}",
6856
- " role={user?.role}",
6857
- " onOpenGettingStarted={openGettingStarted}",
6858
- " extraSteps={[",
6859
- " {",
6860
- " id: 'workspace-card',",
6861
- ` target: '[data-tour="workspace-card"]',`,
6862
- " title: 'Your workspace',",
6863
- " content: 'Everything you build renders here, inside the shared Topolo shell.',",
6864
- " placement: 'bottom' as const,",
6865
- " beforeStep: () => openWorkspace(),",
6866
- " },",
6867
- " ]}",
6868
- " />",
6869
- " </div>",
6870
- " );",
6871
- "}",
6872
- "",
6873
- "function ScaffoldGettingStartedRoute({ appId }: { appId: string }) {",
6874
- " const { user, tokens } = useAuth();",
6875
- " return (",
6876
- " <ScaffoldGettingStarted",
6877
- " appId={appId}",
6878
- " organizationId={String((user as any)?.organization?.id ?? user?.orgId ?? '')}",
6879
- " role={user?.role}",
6880
- " getAccessToken={() => tokens?.accessToken ?? null}",
6881
- " onOpenWorkspace={() => window.location.assign('/workspace')}",
6882
- " />",
6883
- " );",
6884
- "}",
6885
- "",
6886
- "function ScaffoldGettingStarted(props: {",
6887
- " appId: string;",
6888
- " organizationId: string;",
6889
- " role?: string;",
6890
- " getAccessToken: () => string | null;",
6891
- " onOpenWorkspace: () => void;",
6892
- "}) {",
6893
- " const { appId, organizationId, role, getAccessToken, onOpenWorkspace } = props;",
6894
- " const steps: OnboardingGuideStep[] = [",
6895
- " {",
6896
- " id: 'explore-workspace',",
6897
- " title: 'Explore your workspace',",
6898
- " description:",
6899
- " 'Your workspace lives in the Topolo shell \u2014 the launcher, command palette, and account menu are all one click away.',",
6900
- " actionLabel: 'Open workspace',",
6901
- " onAction: onOpenWorkspace,",
6902
- " },",
6903
- " {",
6904
- " id: 'discover-platform',",
6905
- " title: 'Move between your Topolo apps',",
6906
- " description:",
6907
- " 'Use the launcher to switch between your Topolo apps, and the command palette (Cmd/Ctrl-K) to jump anywhere fast.',",
6908
- " optional: true,",
6909
- " },",
6910
- " {",
6911
- " id: 'build-feature',",
6912
- " title: 'Make it your own',",
6913
- " description:",
6914
- " 'Open the account menu to adjust your settings and theme \u2014 your preferences stay in sync across every Topolo app.',",
6915
- " },",
6916
- " ];",
6917
- " return (",
6918
- ' <div className="topolo-starter-workspace">',
6919
- " <GettingStarted",
6920
- " authApiBaseUrl={`${AUTH_URL}/api`}",
6921
- " organizationId={organizationId}",
6922
- " appId={appId}",
6923
- " getAccessToken={getAccessToken}",
6924
- " role={role || undefined}",
6925
- " steps={steps}",
6926
- " eyebrow={`Welcome to ${APP_NAME}`}",
6927
- " title={`Get started with ${APP_NAME}`}",
6928
- ' subtitle="A short checklist to orient you in your new Topolo workspace."',
6929
- ' completeLabel="Mark done"',
6930
- ' dismissLabel="Skip for now"',
6931
- " completedContent={`You're all set \u2014 ${APP_NAME} is ready to build on.`}",
6932
- " completeAction={{ label: 'Go to workspace', onClick: onOpenWorkspace }}",
6933
- " onDismiss={onOpenWorkspace}",
6934
- " />",
6935
- " </div>",
6936
- " );",
6937
- "}",
6938
- "",
6939
- "function errorMessage(error: unknown): string {",
6940
- " if (error instanceof Error && error.message) return error.message;",
6941
- " return 'Unexpected authentication error.';",
6942
- "}",
6943
- "",
6944
- "// Minimal error boundary so a render-time crash falls back to a recoverable surface",
6945
- "// instead of a blank page. The shared TopoloAppShell BugReporter still has access to",
6946
- "// the captured stack via setupBugReporterErrorCapture().",
6947
- "class AppErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {",
6948
- " state = { error: null as Error | null };",
6949
- " static getDerivedStateFromError(error: Error) {",
6950
- " return { error };",
6951
- " }",
6952
- " componentDidCatch(error: Error, info: ErrorInfo) {",
6953
- " // Surface to the platform error pipeline. setupBugReporterErrorCapture() also",
6954
- " // records this via the console.error hook for BugReporter attachments.",
6955
- " console.error('[' + APP_NAME + '] render error:', error, info.componentStack);",
6956
- " }",
6957
- " render() {",
6958
- " if (!this.state.error) return this.props.children;",
6959
- " return (",
6960
- " <TopoloAuthFlowScreen",
6961
- " appName={APP_NAME}",
6962
- ' status="error"',
6963
- " title={APP_NAME + ' hit an unexpected error'}",
6964
- " message={this.state.error.message}",
6965
- ' actionLabel="Reload"',
6966
- " onAction={() => window.location.reload()}",
6967
- " />",
6968
- " );",
6969
- " }",
6970
- "}",
6971
- ""
6972
- ].join("\n");
6866
+ {
6867
+ path: HOME_PATH,
6868
+ ${nav} loading: { shell: NAVIGATION_MODE, content: 'workspace', title: HOME_LABEL },
6869
+ element: <HomeRoute />,
6870
+ },
6871
+ {
6872
+ path: '/getting-started',
6873
+ loading: { shell: NAVIGATION_MODE, content: 'card-grid', title: 'Getting started' },
6874
+ element: <GettingStartedRoute appId={appId} />,
6875
+ },
6876
+ ];
6877
+ }
6878
+ `;
6879
+ }
6880
+ function platformHomeRouteTsx() {
6881
+ return `import { TopoloAppPage, TopoloAppPageHeader, useAuth } from '@topolo-io/app-shell';
6882
+ import { IntroTour } from '@topolo-io/tour';
6883
+ import { useTranslation } from '@topolo-io/ui-kit';
6884
+ import { useNavigate } from 'react-router-dom';
6885
+ import { APP_NAME, HOME_LABEL, HOME_PATH, PRODUCT_TAGLINE } from '../app-config';
6886
+
6887
+ export default function HomeRoute() {
6888
+ const { user } = useAuth();
6889
+ const { t } = useTranslation();
6890
+ const navigate = useNavigate();
6891
+ return (
6892
+ <TopoloAppPage data-tour="home-page">
6893
+ <TopoloAppPageHeader
6894
+ eyebrow={APP_NAME}
6895
+ title={t('app.home.title', { defaultValue: HOME_LABEL })}
6896
+ description={t('app.home.description', { defaultValue: PRODUCT_TAGLINE })}
6897
+ />
6898
+ <IntroTour
6899
+ appName={APP_NAME}
6900
+ role={user?.role}
6901
+ onOpenGettingStarted={() => navigate('/getting-started')}
6902
+ extraSteps={[{
6903
+ id: 'home-page',
6904
+ target: '[data-tour="home-page"]',
6905
+ title: HOME_LABEL,
6906
+ content: PRODUCT_TAGLINE,
6907
+ placement: 'bottom' as const,
6908
+ beforeStep: () => navigate(HOME_PATH),
6909
+ }]}
6910
+ />
6911
+ </TopoloAppPage>
6912
+ );
6913
+ }
6914
+ `;
6915
+ }
6916
+ function platformGettingStartedRouteTsx() {
6917
+ return `import { TopoloAppPage, useAuth } from '@topolo-io/app-shell';
6918
+ import { GettingStarted, type OnboardingGuideStep } from '@topolo-io/onboarding';
6919
+ import { useTranslation } from '@topolo-io/ui-kit';
6920
+ import { useNavigate } from 'react-router-dom';
6921
+ import { APP_NAME, AUTH_URL, HOME_LABEL, HOME_PATH } from '../app-config';
6922
+
6923
+ export default function GettingStartedRoute({ appId }: { appId: string }) {
6924
+ const { user, tokens } = useAuth();
6925
+ const { t } = useTranslation();
6926
+ const navigate = useNavigate();
6927
+ const openHome = () => navigate(HOME_PATH);
6928
+ const steps: OnboardingGuideStep[] = [
6929
+ {
6930
+ id: 'explore-home',
6931
+ title: t('app.gettingStarted.explore.title', { defaultValue: \`Explore \${HOME_LABEL.toLowerCase()}\` }),
6932
+ description: t('app.gettingStarted.explore.description', { defaultValue: 'Start with the main product surface and the actions available to you.' }),
6933
+ actionLabel: t('app.gettingStarted.explore.action', { defaultValue: \`Open \${HOME_LABEL.toLowerCase()}\` }),
6934
+ onAction: openHome,
6935
+ },
6936
+ {
6937
+ id: 'discover-platform',
6938
+ title: t('app.gettingStarted.platform.title', { defaultValue: 'Move between Topolo apps' }),
6939
+ description: t('app.gettingStarted.platform.description', { defaultValue: 'Use the app launcher and command palette to move through your work.' }),
6940
+ optional: true,
6941
+ },
6942
+ {
6943
+ id: 'personalize',
6944
+ title: t('app.gettingStarted.personalize.title', { defaultValue: 'Set your preferences' }),
6945
+ description: t('app.gettingStarted.personalize.description', { defaultValue: 'Open the account menu to manage your settings and theme.' }),
6946
+ },
6947
+ ];
6948
+ return (
6949
+ <TopoloAppPage>
6950
+ <GettingStarted
6951
+ authApiBaseUrl={\`\${AUTH_URL}/api\`}
6952
+ organizationId={String((user as { organization?: { id?: string } } | null)?.organization?.id ?? user?.orgId ?? '')}
6953
+ appId={appId}
6954
+ getAccessToken={() => tokens?.accessToken ?? null}
6955
+ role={user?.role}
6956
+ steps={steps}
6957
+ eyebrow={t('app.gettingStarted.eyebrow', { defaultValue: \`Welcome to \${APP_NAME}\` })}
6958
+ title={t('app.gettingStarted.title', { defaultValue: \`Get started with \${APP_NAME}\` })}
6959
+ subtitle={t('app.gettingStarted.subtitle', { defaultValue: 'A short checklist to help you find your way around.' })}
6960
+ completeLabel={t('app.gettingStarted.complete', { defaultValue: 'Mark done' })}
6961
+ dismissLabel={t('app.gettingStarted.dismiss', { defaultValue: 'Skip for now' })}
6962
+ completedContent={t('app.gettingStarted.completed', { defaultValue: \`You're all set \u2014 \${APP_NAME} is ready.\` })}
6963
+ completeAction={{ label: t('app.gettingStarted.openHome', { defaultValue: \`Go to \${HOME_LABEL.toLowerCase()}\` }), onClick: openHome }}
6964
+ onDismiss={openHome}
6965
+ />
6966
+ </TopoloAppPage>
6967
+ );
6968
+ }
6969
+ `;
6970
+ }
6971
+ function platformAppErrorBoundaryTsx() {
6972
+ return `import { Component, type ErrorInfo, type ReactNode } from 'react';
6973
+ import { TopoloAuthFlowScreen } from '@topolo-io/app-shell';
6974
+ import { APP_NAME } from './app-config';
6975
+
6976
+ export default class AppErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
6977
+ state = { error: null as Error | null };
6978
+
6979
+ static getDerivedStateFromError(error: Error) {
6980
+ return { error };
6981
+ }
6982
+
6983
+ componentDidCatch(error: Error, info: ErrorInfo) {
6984
+ console.error('[' + APP_NAME + '] render error:', error, info.componentStack);
6985
+ }
6986
+
6987
+ render() {
6988
+ if (!this.state.error) return this.props.children;
6989
+ return (
6990
+ <TopoloAuthFlowScreen
6991
+ appName={APP_NAME}
6992
+ status="error"
6993
+ title={APP_NAME + ' hit an unexpected error'}
6994
+ message={this.state.error.message}
6995
+ actionLabel="Reload"
6996
+ onAction={() => window.location.reload()}
6997
+ />
6998
+ );
6999
+ }
7000
+ }
7001
+ `;
6973
7002
  }
6974
7003
  function developerAppTsx(application) {
6975
7004
  const appName = toTitle(application.name);
6976
7005
  const defaultClientId = `${toSlug(application.id)}_local_client`;
6977
- return [
6978
- "import { useEffect, useMemo, useState } from 'react';",
6979
- "import { createAccountLinkPayload, createTopoloAuthorizationUrl, exchangeTopoloCode, fetchTopoloUserInfo, type TopoloUserInfo } from '@topolo/login';",
6980
- "",
6981
- "type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';",
6982
- "",
6983
- `const APP_NAME = '${escapeSingleQuoted(appName)}';`,
6984
- "const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? import.meta.env.VITE_TOPOLO_AUTH_BASE_URL ?? 'https://auth.topolo.app';",
6985
- `const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';`,
6986
- "const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || `${window.location.origin}/auth/callback`;",
6987
- "const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'openid profile email organization:read entitlements:read').split(/[\\s,]+/).filter(Boolean);",
6988
- "",
6989
- "export default function App() {",
6990
- " const [authState, setAuthState] = useState<AuthState>(() => CLIENT_ID ? 'ready' : 'missing_client');",
6991
- " const [message, setMessage] = useState<string | null>(null);",
6992
- " const [tokenPreview, setTokenPreview] = useState<string | null>(null);",
6993
- " const [userInfo, setUserInfo] = useState<TopoloUserInfo | null>(null);",
6994
- "",
6995
- " const callbackCode = useMemo(() => new URLSearchParams(window.location.search).get('code'), []);",
6996
- " const callbackError = useMemo(() => new URLSearchParams(window.location.search).get('error'), []);",
6997
- "",
6998
- " useEffect(() => {",
6999
- " if (callbackError) {",
7000
- " setAuthState('error');",
7001
- " setMessage(callbackError);",
7002
- " return;",
7003
- " }",
7004
- " if (!callbackCode) return;",
7005
- "",
7006
- " setAuthState('authorizing');",
7007
- " void exchangeAuthorizationCode(callbackCode)",
7008
- " .then(async (tokens) => {",
7009
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_access_token`, tokens.access_token);",
7010
- " if (tokens.refresh_token) {",
7011
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_refresh_token`, tokens.refresh_token);",
7012
- " }",
7013
- " const profile = await fetchTopoloUserInfo({ authBaseUrl: AUTH_URL, accessToken: tokens.access_token });",
7014
- " const link = createAccountLinkPayload({",
7015
- " localUserId: profile.email ? `local:${profile.email}` : `local:${profile.sub}`,",
7016
- " userInfo: profile,",
7017
- " });",
7018
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_account_link`, JSON.stringify(link));",
7019
- " setUserInfo(profile);",
7020
- " setTokenPreview(`${tokens.access_token.slice(0, 18)}...`);",
7021
- " setAuthState('authenticated');",
7022
- " window.history.replaceState({}, document.title, '/');",
7023
- " })",
7024
- " .catch((error: Error) => {",
7025
- " setAuthState('error');",
7026
- " setMessage(error.message);",
7027
- " });",
7028
- " }, [callbackCode, callbackError]);",
7029
- "",
7030
- " return (",
7031
- ' <main className="topolo-starter-login">',
7032
- ' <section className="topolo-starter-login__panel">',
7033
- " <h1>{APP_NAME}</h1>",
7034
- " <p>Public Topolo OAuth starter using authorization-code + PKCE.</p>",
7035
- " {authState === 'missing_client' && (",
7036
- " <p>Set VITE_TOPOLO_CLIENT_ID after registering this app in Topolo Developers.</p>",
7037
- " )}",
7038
- " {authState === 'authenticated' && (",
7039
- " <p>Topolo sign-in complete for {userInfo?.email ?? userInfo?.sub}. Access token: {tokenPreview}</p>",
7040
- " )}",
7041
- " {authState === 'error' && <p>{message}</p>}",
7042
- ` <button type="button" disabled={!CLIENT_ID || authState === 'authorizing'} onClick={() => void startTopoloLogin()}>`,
7043
- " {authState === 'authorizing' ? 'Completing sign-in' : 'Sign in with Topolo'}",
7044
- " </button>",
7045
- " </section>",
7046
- " </main>",
7047
- " );",
7048
- "}",
7049
- "",
7050
- "async function startTopoloLogin(): Promise<void> {",
7051
- " const authorization = await createTopoloAuthorizationUrl({",
7052
- " authBaseUrl: AUTH_URL,",
7053
- " clientId: CLIENT_ID,",
7054
- " redirectUri: REDIRECT_URI,",
7055
- " scopes: REQUESTED_SCOPES,",
7056
- " });",
7057
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_pkce_verifier`, authorization.codeVerifier);",
7058
- " sessionStorage.setItem(`${CLIENT_ID}:topolo_oauth_state`, authorization.state);",
7059
- " window.location.assign(authorization.url);",
7060
- "}",
7061
- "",
7062
- "async function exchangeAuthorizationCode(code: string): Promise<{ access_token: string; refresh_token?: string }> {",
7063
- " const params = new URLSearchParams(window.location.search);",
7064
- " const expectedState = sessionStorage.getItem(`${CLIENT_ID}:topolo_oauth_state`);",
7065
- " if (expectedState && params.get('state') !== expectedState) {",
7066
- " throw new Error('OAuth state mismatch. Restart sign-in.');",
7067
- " }",
7068
- " const verifier = sessionStorage.getItem(`${CLIENT_ID}:topolo_pkce_verifier`);",
7069
- " if (!verifier) throw new Error('Missing PKCE verifier. Restart sign-in.');",
7070
- "",
7071
- " return exchangeTopoloCode({",
7072
- " authBaseUrl: AUTH_URL,",
7073
- " clientId: CLIENT_ID,",
7074
- " code,",
7075
- " redirectUri: REDIRECT_URI,",
7076
- " codeVerifier: verifier,",
7077
- " });",
7078
- "}",
7079
- ""
7080
- ].join("\n");
7006
+ return `import { useEffect, useMemo, useState } from 'react';
7007
+ import { createAccountLinkPayload, createTopoloAuthorizationUrl, exchangeTopoloCode, fetchTopoloUserInfo, type TopoloUserInfo } from '@topolo/login';
7008
+
7009
+ type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';
7010
+ const APP_NAME = '${escapeSingleQuoted(appName)}';
7011
+ const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? import.meta.env.VITE_TOPOLO_AUTH_BASE_URL ?? 'https://auth.topolo.app';
7012
+ const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';
7013
+ const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || \`\${window.location.origin}/auth/callback\`;
7014
+ const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'openid profile email organization:read entitlements:read').split(/[\\s,]+/).filter(Boolean);
7015
+
7016
+ export default function App() {
7017
+ const [authState, setAuthState] = useState<AuthState>(() => CLIENT_ID ? 'ready' : 'missing_client');
7018
+ const [message, setMessage] = useState<string | null>(null);
7019
+ const [tokenPreview, setTokenPreview] = useState<string | null>(null);
7020
+ const [userInfo, setUserInfo] = useState<TopoloUserInfo | null>(null);
7021
+ const callbackCode = useMemo(() => new URLSearchParams(window.location.search).get('code'), []);
7022
+ const callbackError = useMemo(() => new URLSearchParams(window.location.search).get('error'), []);
7023
+
7024
+ useEffect(() => {
7025
+ if (callbackError) {
7026
+ setAuthState('error');
7027
+ setMessage(callbackError);
7028
+ return;
7029
+ }
7030
+ if (!callbackCode) return;
7031
+ setAuthState('authorizing');
7032
+ void exchangeAuthorizationCode(callbackCode)
7033
+ .then(async (tokens) => {
7034
+ sessionStorage.setItem(\`\${CLIENT_ID}:topolo_access_token\`, tokens.access_token);
7035
+ if (tokens.refresh_token) sessionStorage.setItem(\`\${CLIENT_ID}:topolo_refresh_token\`, tokens.refresh_token);
7036
+ const profile = await fetchTopoloUserInfo({ authBaseUrl: AUTH_URL, accessToken: tokens.access_token });
7037
+ const link = createAccountLinkPayload({ localUserId: profile.email ? \`local:\${profile.email}\` : \`local:\${profile.sub}\`, userInfo: profile });
7038
+ sessionStorage.setItem(\`\${CLIENT_ID}:topolo_account_link\`, JSON.stringify(link));
7039
+ setUserInfo(profile);
7040
+ setTokenPreview(\`\${tokens.access_token.slice(0, 18)}...\`);
7041
+ setAuthState('authenticated');
7042
+ window.history.replaceState({}, document.title, '/');
7043
+ })
7044
+ .catch((error: Error) => {
7045
+ setAuthState('error');
7046
+ setMessage(error.message);
7047
+ });
7048
+ }, [callbackCode, callbackError]);
7049
+
7050
+ return (
7051
+ <main className="topolo-starter-login">
7052
+ <section className="topolo-starter-login__panel">
7053
+ <h1>{APP_NAME}</h1>
7054
+ <p>Public Topolo OAuth starter using authorization-code + PKCE.</p>
7055
+ {authState === 'missing_client' && <p>Set VITE_TOPOLO_CLIENT_ID after registering this app in Topolo Developers.</p>}
7056
+ {authState === 'authenticated' && <p>Topolo sign-in complete for {userInfo?.email ?? userInfo?.sub}. Access token: {tokenPreview}</p>}
7057
+ {authState === 'error' && <p>{message}</p>}
7058
+ <button type="button" disabled={!CLIENT_ID || authState === 'authorizing'} onClick={() => void startTopoloLogin()}>
7059
+ {authState === 'authorizing' ? 'Completing sign-in' : 'Sign in with Topolo'}
7060
+ </button>
7061
+ </section>
7062
+ </main>
7063
+ );
7064
+ }
7065
+
7066
+ async function startTopoloLogin(): Promise<void> {
7067
+ const authorization = await createTopoloAuthorizationUrl({ authBaseUrl: AUTH_URL, clientId: CLIENT_ID, redirectUri: REDIRECT_URI, scopes: REQUESTED_SCOPES });
7068
+ sessionStorage.setItem(\`\${CLIENT_ID}:topolo_pkce_verifier\`, authorization.codeVerifier);
7069
+ sessionStorage.setItem(\`\${CLIENT_ID}:topolo_oauth_state\`, authorization.state);
7070
+ window.location.assign(authorization.url);
7071
+ }
7072
+
7073
+ async function exchangeAuthorizationCode(code: string): Promise<{ access_token: string; refresh_token?: string }> {
7074
+ const params = new URLSearchParams(window.location.search);
7075
+ const expectedState = sessionStorage.getItem(\`\${CLIENT_ID}:topolo_oauth_state\`);
7076
+ if (expectedState && params.get('state') !== expectedState) throw new Error('OAuth state mismatch. Restart sign-in.');
7077
+ const verifier = sessionStorage.getItem(\`\${CLIENT_ID}:topolo_pkce_verifier\`);
7078
+ if (!verifier) throw new Error('Missing PKCE verifier. Restart sign-in.');
7079
+ return exchangeTopoloCode({ authBaseUrl: AUTH_URL, clientId: CLIENT_ID, code, redirectUri: REDIRECT_URI, codeVerifier: verifier });
7080
+ }
7081
+ `;
7081
7082
  }
7082
7083
 
7083
7084
  // src/commands/app-scaffold-worker-template.ts
7084
- function webWorkerTs(application) {
7085
+ function webWorkerTs(application, api) {
7085
7086
  const serviceName = toTitle(application.name);
7086
7087
  return [
7087
- "interface Env {",
7088
- " API: { fetch(request: Request): Promise<Response> };",
7089
- "}",
7088
+ ...api ? [
7089
+ "interface Env {",
7090
+ " API: { fetch(request: Request): Promise<Response> };",
7091
+ "}"
7092
+ ] : ["type Env = Record<string, never>;"],
7090
7093
  "",
7091
7094
  `const SERVICE_NAME = '${escapeSingleQuoted(serviceName)}';`,
7092
7095
  "",
7093
7096
  "export default {",
7094
- " async fetch(request: Request, env: Env): Promise<Response> {",
7095
- " const url = new URL(request.url);",
7096
- " if (url.pathname.startsWith('/api/')) {",
7097
- " return env.API.fetch(request);",
7098
- " }",
7097
+ api ? " async fetch(request: Request, env: Env): Promise<Response> {" : " async fetch(): Promise<Response> {",
7098
+ ...api ? [
7099
+ " const url = new URL(request.url);",
7100
+ " if (url.pathname.startsWith('/api/')) {",
7101
+ " return env.API.fetch(request);",
7102
+ " }"
7103
+ ] : [],
7099
7104
  " return Response.json({ ok: true, service: SERVICE_NAME }, { status: 200 });",
7100
7105
  " },",
7101
7106
  "};",
7102
7107
  ""
7103
7108
  ].join("\n");
7104
7109
  }
7105
- function platformWebWranglerToml(application) {
7110
+ function platformWebWranglerToml(application, api) {
7106
7111
  const slug = toSlug(application.id);
7107
7112
  const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
7108
7113
  return [
@@ -7117,7 +7122,7 @@ function platformWebWranglerToml(application) {
7117
7122
  'directory = "./dist"',
7118
7123
  'binding = "ASSETS"',
7119
7124
  'not_found_handling = "single-page-application"',
7120
- 'run_worker_first = ["/api/*"]',
7125
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7121
7126
  "",
7122
7127
  "[observability]",
7123
7128
  "enabled = true",
@@ -7130,10 +7135,12 @@ function platformWebWranglerToml(application) {
7130
7135
  `APP_ID = "${application.appId ?? ""}"`,
7131
7136
  `VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
7132
7137
  "",
7133
- "[[services]]",
7134
- 'binding = "API"',
7135
- `service = "topolo-${slug}-api"`,
7136
- "",
7138
+ ...api ? [
7139
+ "[[services]]",
7140
+ 'binding = "API"',
7141
+ `service = "topolo-${slug}-api"`,
7142
+ ""
7143
+ ] : [],
7137
7144
  "[env.development]",
7138
7145
  `name = "topolo-${slug}-development"`,
7139
7146
  `account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
@@ -7151,12 +7158,14 @@ function platformWebWranglerToml(application) {
7151
7158
  'directory = "./dist"',
7152
7159
  'binding = "ASSETS"',
7153
7160
  'not_found_handling = "single-page-application"',
7154
- 'run_worker_first = ["/api/*"]',
7155
- "",
7156
- "[[env.development.services]]",
7157
- 'binding = "API"',
7158
- `service = "topolo-${slug}-api-development"`,
7161
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7159
7162
  "",
7163
+ ...api ? [
7164
+ "[[env.development.services]]",
7165
+ 'binding = "API"',
7166
+ `service = "topolo-${slug}-api-development"`,
7167
+ ""
7168
+ ] : [],
7160
7169
  "[env.staging]",
7161
7170
  `name = "topolo-${slug}-staging"`,
7162
7171
  `account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
@@ -7174,12 +7183,14 @@ function platformWebWranglerToml(application) {
7174
7183
  'directory = "./dist"',
7175
7184
  'binding = "ASSETS"',
7176
7185
  'not_found_handling = "single-page-application"',
7177
- 'run_worker_first = ["/api/*"]',
7178
- "",
7179
- "[[env.staging.services]]",
7180
- 'binding = "API"',
7181
- `service = "topolo-${slug}-api-staging"`,
7186
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7182
7187
  "",
7188
+ ...api ? [
7189
+ "[[env.staging.services]]",
7190
+ 'binding = "API"',
7191
+ `service = "topolo-${slug}-api-staging"`,
7192
+ ""
7193
+ ] : [],
7183
7194
  "[env.production]",
7184
7195
  `name = "topolo-${slug}-production"`,
7185
7196
  `account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
@@ -7197,12 +7208,14 @@ function platformWebWranglerToml(application) {
7197
7208
  'directory = "./dist"',
7198
7209
  'binding = "ASSETS"',
7199
7210
  'not_found_handling = "single-page-application"',
7200
- 'run_worker_first = ["/api/*"]',
7211
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7201
7212
  "",
7202
- "[[env.production.services]]",
7203
- 'binding = "API"',
7204
- `service = "topolo-${slug}-api-production"`,
7205
- ""
7213
+ ...api ? [
7214
+ "[[env.production.services]]",
7215
+ 'binding = "API"',
7216
+ `service = "topolo-${slug}-api-production"`,
7217
+ ""
7218
+ ] : []
7206
7219
  ].join("\n");
7207
7220
  }
7208
7221
  function platformApiWranglerToml(application, resources, profile) {
@@ -7777,6 +7790,8 @@ function topoloConformanceWorkflow(options) {
7777
7790
  " pull_request:",
7778
7791
  " push:",
7779
7792
  " branches:",
7793
+ " - staging",
7794
+ " - development",
7780
7795
  " - main",
7781
7796
  " workflow_dispatch:",
7782
7797
  "",
@@ -7814,10 +7829,11 @@ function topoloConformanceWorkflow(options) {
7814
7829
  " token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}",
7815
7830
  ...cliCheckoutStep,
7816
7831
  "",
7817
- " - name: Skip conformance without cross-repo token",
7832
+ " - name: Require Topolo package and repository credentials",
7818
7833
  ` if: steps.app-token.outputs.token == '' && env.HAS_TOPOLO_CI_REPO_TOKEN != 'true'`,
7819
7834
  " run: |",
7820
- ' echo "::warning::Skipping Topolo app conformance because no Topolo cross-repo token is available to this workflow context."',
7835
+ ' echo "::error::Topolo conformance requires TOPOLO_PACKAGES_APP credentials or TOPOLO_CI_REPO_TOKEN."',
7836
+ " exit 1",
7821
7837
  "",
7822
7838
  " - name: Setup Node.js",
7823
7839
  ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
@@ -7825,6 +7841,14 @@ function topoloConformanceWorkflow(options) {
7825
7841
  " with:",
7826
7842
  " node-version: 20",
7827
7843
  "",
7844
+ " - name: Enable pnpm and authenticate Topolo packages",
7845
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7846
+ " env:",
7847
+ ` NODE_AUTH_TOKEN: \${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}`,
7848
+ " run: |",
7849
+ " corepack enable",
7850
+ ' printf "//npm.pkg.github.com/:_authToken=%s\\n" "$NODE_AUTH_TOKEN" >> "$HOME/.npmrc"',
7851
+ "",
7828
7852
  " - name: Build Topolo SDK",
7829
7853
  ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7830
7854
  " working-directory: topolo-platform/packages/topolo-sdk",
@@ -7839,16 +7863,30 @@ function topoloConformanceWorkflow(options) {
7839
7863
  " npm install --no-audit --no-fund",
7840
7864
  " npm run build",
7841
7865
  "",
7842
- " - name: Run Topolo app conformance audit",
7866
+ " - name: Install generated application",
7843
7867
  ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7868
+ ` working-directory: ${appPath}`,
7869
+ " run: pnpm install --frozen-lockfile",
7870
+ "",
7871
+ " - name: Run generated application quality gates",
7872
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7873
+ ` working-directory: ${appPath}`,
7844
7874
  " run: |",
7845
- ` node topolo-platform/packages/topolo-cli/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`,
7875
+ " pnpm run typecheck",
7876
+ " pnpm run coverage",
7877
+ " pnpm run build",
7878
+ ' if pnpm run | grep -q "check:deploy-config"; then pnpm run check:deploy-config; fi',
7846
7879
  "",
7847
- " - name: Record green CI evidence",
7880
+ " - name: Record verified CI evidence",
7848
7881
  ` if: \${{ success() && (steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true') }}`,
7849
7882
  ` working-directory: ${appPath}`,
7850
7883
  " run: |",
7851
- ` node "$GITHUB_WORKSPACE/topolo-platform/packages/topolo-cli/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`
7884
+ ` node "$GITHUB_WORKSPACE/topolo-platform/packages/topolo-cli/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`,
7885
+ "",
7886
+ " - name: Run Topolo app conformance audit",
7887
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7888
+ " run: |",
7889
+ ` node topolo-platform/packages/topolo-cli/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`
7852
7890
  ].join("\n");
7853
7891
  }
7854
7892
 
@@ -8022,6 +8060,15 @@ function topoloActionContractsMjs() {
8022
8060
  ""
8023
8061
  ].join("\n");
8024
8062
  }
8063
+ function topoloActionContractsDmts() {
8064
+ return [
8065
+ "import type { TopoloActionContracts } from '@topolo-io/worker-runtime/actions';",
8066
+ "",
8067
+ "export const topoloActionContracts: TopoloActionContracts;",
8068
+ "export const topoloActionInputSchemas: Record<string, unknown>;",
8069
+ ""
8070
+ ].join("\n");
8071
+ }
8025
8072
  function baselineApiActions() {
8026
8073
  const emptyInputSchema = {
8027
8074
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -8715,6 +8762,131 @@ function envExample(application) {
8715
8762
  ].join("\n");
8716
8763
  }
8717
8764
 
8765
+ // src/commands/app-scaffold-test-template.ts
8766
+ var coverageThresholds = ` thresholds: {
8767
+ statements: 100,
8768
+ branches: 100,
8769
+ functions: 100,
8770
+ lines: 100,
8771
+ },`;
8772
+ function platformBrowserVitestConfig() {
8773
+ return `import { defineConfig } from 'vitest/config';
8774
+
8775
+ export default defineConfig({
8776
+ test: {
8777
+ include: ['test/**/*.test.{ts,tsx}'],
8778
+ environment: 'jsdom',
8779
+ coverage: {
8780
+ provider: 'v8',
8781
+ include: ['src/app-config.ts', 'src/app-routes.tsx', 'src/worker.ts'],
8782
+ ${coverageThresholds}
8783
+ },
8784
+ },
8785
+ });
8786
+ `;
8787
+ }
8788
+ function platformApiVitestConfig() {
8789
+ return `import { defineConfig } from 'vitest/config';
8790
+
8791
+ export default defineConfig({
8792
+ test: {
8793
+ include: ['test/**/*.test.{js,mjs,mts,ts,tsx}'],
8794
+ environment: 'node',
8795
+ coverage: {
8796
+ provider: 'v8',
8797
+ include: ['src/app-identity.ts'],
8798
+ ${coverageThresholds}
8799
+ },
8800
+ },
8801
+ });
8802
+ `;
8803
+ }
8804
+ function platformBrowserContractTest(application, experience) {
8805
+ const required = experience.workspaceSwitcher === "required";
8806
+ const enabled = experience.workspaceSwitcher !== "off";
8807
+ const navExpectation = experience.navigationMode === "none" ? " expect(home.nav).toBeUndefined();" : ` expect(home.nav).toEqual({ id: 'home', label: '${escapeSingleQuoted(experience.homeLabel)}' });`;
8808
+ return `import { describe, expect, it, vi } from 'vitest';
8809
+
8810
+ vi.mock('../src/app-identity', () => ({
8811
+ SCAFFOLD_APP_NAME: '${escapeSingleQuoted(application.name)}',
8812
+ getBrowserAuthBaseUrl: () => 'https://auth.stg.topolo.us',
8813
+ }));
8814
+
8815
+ import { HOME_LABEL, HOME_PATH, NAVIGATION_MODE, WORKSPACE_RESOURCE } from '../src/app-config';
8816
+ import { createAppRoutes } from '../src/app-routes';
8817
+
8818
+ describe('generated application contract', () => {
8819
+ it('uses the selected home, navigation, and context-switcher shape', () => {
8820
+ expect(HOME_PATH).toBe('${escapeSingleQuoted(experience.homePath)}');
8821
+ expect(HOME_LABEL).toBe('${escapeSingleQuoted(experience.homeLabel)}');
8822
+ expect(NAVIGATION_MODE).toBe('${experience.navigationMode}');
8823
+ expect(WORKSPACE_RESOURCE.enabled).toBe(${enabled});
8824
+ expect('required' in WORKSPACE_RESOURCE ? WORKSPACE_RESOURCE.required : false).toBe(${required});
8825
+ });
8826
+
8827
+ it('keeps Getting Started out of product navigation', () => {
8828
+ const [home, gettingStarted] = createAppRoutes('app_test');
8829
+ expect(home.path).toBe('${escapeSingleQuoted(experience.homePath)}');
8830
+ ${navExpectation}
8831
+ expect(gettingStarted.path).toBe('/getting-started');
8832
+ expect(gettingStarted.nav).toBeUndefined();
8833
+ });
8834
+ });
8835
+ `;
8836
+ }
8837
+ function platformWebWorkerTest(application, api) {
8838
+ const apiTest = api ? ` it('proxies API paths to the API service binding', async () => {
8839
+ const response = new Response('proxied', { status: 202 });
8840
+ const fetch = vi.fn().mockResolvedValue(response);
8841
+ await expect(worker.fetch(new Request('https://app.test/api/health'), { API: { fetch } })).resolves.toBe(response);
8842
+ expect(fetch).toHaveBeenCalledOnce();
8843
+ });
8844
+
8845
+ ` : "";
8846
+ const env = api ? "{ API: { fetch: vi.fn() } }" : "{}";
8847
+ return `import { describe, expect, it, vi } from 'vitest';
8848
+ import worker from '../src/worker';
8849
+
8850
+ describe('browser worker routing', () => {
8851
+ ${apiTest}
8852
+ it('returns the browser service descriptor for asset paths', async () => {
8853
+ const response = await worker.fetch(new Request('https://app.test/dashboard'), ${env});
8854
+ expect(response.status).toBe(200);
8855
+ await expect(response.json()).resolves.toEqual({ ok: true, service: '${escapeSingleQuoted(toTitle(application.name))}' });
8856
+ });
8857
+ });
8858
+ `;
8859
+ }
8860
+ function platformApiHealthTest(application) {
8861
+ return `import { describe, expect, it } from 'vitest';
8862
+ import worker from '../src/worker';
8863
+ import { resolveRuntimeAppId } from '../src/app-identity';
8864
+
8865
+ const executionContext = {
8866
+ waitUntil() {},
8867
+ passThroughOnException() {},
8868
+ props: {},
8869
+ } as unknown as ExecutionContext;
8870
+
8871
+ describe('API runtime baseline', () => {
8872
+ it('serves a real health response through the generated worker stack', async () => {
8873
+ const response = await worker.fetch(
8874
+ new Request('https://api.test/api/health'),
8875
+ { APP_ID: '${escapeSingleQuoted(application.appId ?? application.id)}' },
8876
+ executionContext,
8877
+ );
8878
+ expect(response.status).toBe(200);
8879
+ await expect(response.json()).resolves.toMatchObject({ ok: true, app: '${escapeSingleQuoted(application.appId ?? application.id)}' });
8880
+ });
8881
+
8882
+ it('requires runtime app identity', async () => {
8883
+ await expect(resolveRuntimeAppId({ APP_ID: ' app_runtime ' })).resolves.toBe('app_runtime');
8884
+ expect(() => resolveRuntimeAppId({})).toThrow('APP_ID is required');
8885
+ });
8886
+ });
8887
+ `;
8888
+ }
8889
+
8718
8890
  // src/commands/app-scaffold.ts
8719
8891
  function standardScriptStub() {
8720
8892
  return `#!/usr/bin/env python3
@@ -8807,9 +8979,18 @@ function applicationScaffoldPayload(application, options = {}) {
8807
8979
  }
8808
8980
  const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
8809
8981
  const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
8810
- const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
8982
+ const requestedAppDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
8983
+ const appDir = target === "platform" ? normalizeFirstPartyAppDir(requestedAppDir) : requestedAppDir;
8811
8984
  const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
8812
8985
  const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
8986
+ const experience = {
8987
+ homePath: normalizeHomePath(options.homePath),
8988
+ homeLabel: normalizeHomeLabel(options.homeLabel),
8989
+ navigationMode: normalizeNavigationMode(options.navigationMode),
8990
+ workspaceSwitcher: normalizeWorkspaceSwitcherMode(options.workspaceSwitcher),
8991
+ workspaceLabel: tenancy === "household" ? "Households" : "Workspaces",
8992
+ tagline: String(options.tagline || "").trim() || `Manage your work in ${toTitle(name)}.`
8993
+ };
8813
8994
  const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
8814
8995
  const defaultAppId = target === "platform" ? `app_topolo_${id}` : catalogApplication?.services[0] ?? id;
8815
8996
  const appId = api ? options.appId ?? defaultAppId : null;
@@ -8838,6 +9019,7 @@ function applicationScaffoldPayload(application, options = {}) {
8838
9019
  containerAppId,
8839
9020
  profile,
8840
9021
  resources,
9022
+ experience,
8841
9023
  distributionModel,
8842
9024
  capabilities,
8843
9025
  application: applicationPayload,
@@ -8856,6 +9038,7 @@ function applicationScaffoldPayload(application, options = {}) {
8856
9038
  containerAppId,
8857
9039
  profile,
8858
9040
  resources,
9041
+ experience,
8859
9042
  distributionModel,
8860
9043
  capabilities
8861
9044
  },
@@ -8924,18 +9107,10 @@ function scaffoldPackages(options) {
8924
9107
  }
8925
9108
  const packages = /* @__PURE__ */ new Set();
8926
9109
  if (options.browser) {
8927
- packages.add("@topolo-io/app-funnel");
8928
- packages.add("@topolo-io/app-launcher");
8929
- packages.add("@topolo-io/app-shell");
8930
- packages.add("@topolo-io/auth-client");
8931
- packages.add("@topolo-io/design-tokens");
8932
- packages.add("@topolo-io/onboarding");
8933
- packages.add("@topolo-io/tour");
8934
- packages.add("@topolo-io/ui-kit");
9110
+ for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) packages.add(packageName);
8935
9111
  }
8936
9112
  if (options.api) {
8937
- packages.add("@topolo-io/worker-runtime");
8938
- packages.add("@topolo/sdk");
9113
+ for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) packages.add(packageName);
8939
9114
  }
8940
9115
  return Array.from(packages);
8941
9116
  }
@@ -9015,6 +9190,12 @@ function scaffoldFiles(options) {
9015
9190
  ],
9016
9191
  content: gitignore()
9017
9192
  },
9193
+ ...target === "platform" ? [{
9194
+ path: `${base}/.npmrc`,
9195
+ purpose: "Scoped package registry routing without persisted credentials.",
9196
+ requiredSignals: ["routes private @topolo-io packages to GitHub Packages", "keeps public @topolo packages on npm", "does not contain an authentication token"],
9197
+ content: npmrc()
9198
+ }] : [],
9018
9199
  {
9019
9200
  path: `${base}/AGENTS.md`,
9020
9201
  purpose: "Local agent rules for safe work inside the generated project.",
@@ -9030,42 +9211,44 @@ function scaffoldFiles(options) {
9030
9211
  resources: options.payload.resources
9031
9212
  })
9032
9213
  },
9033
- {
9034
- path: `${browserBase}/vite.config.ts`,
9035
- purpose: "Vite browser build configuration.",
9036
- requiredSignals: [
9037
- "builds the authenticated browser surface",
9038
- "keeps generated apps on the standard React/Vite baseline"
9039
- ],
9040
- content: viteConfig(target)
9041
- },
9042
- {
9043
- path: `${browserBase}/index.html`,
9044
- purpose: "Browser entry document.",
9045
- requiredSignals: [
9046
- "mounts the React app at #root",
9047
- "uses the generated application display name"
9048
- ],
9049
- content: indexHtml(application.name)
9050
- },
9051
- {
9052
- path: `${browserBase}/src/main.tsx`,
9053
- purpose: "React app entrypoint.",
9054
- requiredSignals: [
9055
- "renders the generated app component",
9056
- "imports the generated app stylesheet"
9057
- ],
9058
- content: mainTsx()
9059
- },
9060
- {
9061
- path: `${browserBase}/src/env.d.ts`,
9062
- purpose: "Vite environment variable typing.",
9063
- requiredSignals: [
9064
- "loads vite/client import.meta.env types",
9065
- "declares the Topolo environment variables consumed by the starter"
9066
- ],
9067
- content: envDts(options.api)
9068
- },
9214
+ ...options.browser ? [
9215
+ {
9216
+ path: `${browserBase}/vite.config.ts`,
9217
+ purpose: "Vite browser build configuration.",
9218
+ requiredSignals: [
9219
+ "builds the authenticated browser surface",
9220
+ "keeps generated apps on the standard React/Vite baseline"
9221
+ ],
9222
+ content: viteConfig(target)
9223
+ },
9224
+ {
9225
+ path: `${browserBase}/index.html`,
9226
+ purpose: "Browser entry document.",
9227
+ requiredSignals: [
9228
+ "mounts the React app at #root",
9229
+ "uses the generated application display name"
9230
+ ],
9231
+ content: indexHtml(application.name)
9232
+ },
9233
+ {
9234
+ path: `${browserBase}/src/main.tsx`,
9235
+ purpose: "React app entrypoint.",
9236
+ requiredSignals: [
9237
+ "renders the generated app component",
9238
+ "imports the generated app stylesheet"
9239
+ ],
9240
+ content: mainTsx()
9241
+ },
9242
+ {
9243
+ path: `${browserBase}/src/env.d.ts`,
9244
+ purpose: "Vite environment variable typing.",
9245
+ requiredSignals: [
9246
+ "loads vite/client import.meta.env types",
9247
+ "declares the Topolo environment variables consumed by the starter"
9248
+ ],
9249
+ content: envDts(options.api)
9250
+ }
9251
+ ] : [],
9069
9252
  ...options.payload.resources.workflow ? [
9070
9253
  {
9071
9254
  path: `${base}/src/cloudflare-workers.d.ts`,
@@ -9077,7 +9260,7 @@ function scaffoldFiles(options) {
9077
9260
  content: cloudflareWorkersDts()
9078
9261
  }
9079
9262
  ] : [],
9080
- {
9263
+ ...options.browser ? [{
9081
9264
  path: `${browserBase}/src/styles.css`,
9082
9265
  purpose: "Minimal app-local styles outside the shared Topolo shell.",
9083
9266
  requiredSignals: [
@@ -9085,7 +9268,39 @@ function scaffoldFiles(options) {
9085
9268
  "keeps app-specific layout scoped under generated classes"
9086
9269
  ],
9087
9270
  content: stylesCss()
9088
- }
9271
+ }] : [],
9272
+ ...target === "platform" && options.browser ? [
9273
+ {
9274
+ path: `${browserBase}/src/app-config.ts`,
9275
+ purpose: "Product route, navigation, context-switcher, and entry-surface configuration.",
9276
+ requiredSignals: ["defaults the authenticated home to /dashboard", "keeps product copy separate from shell wiring"],
9277
+ content: platformAppConfigTs(application, options.payload.experience)
9278
+ },
9279
+ {
9280
+ path: `${browserBase}/src/app-routes.tsx`,
9281
+ purpose: "Canonical protected route manifest and product navigation metadata.",
9282
+ requiredSignals: ["uses the configured home route", "keeps Getting Started out of product navigation"],
9283
+ content: platformAppRoutesTsx(options.payload.experience)
9284
+ },
9285
+ {
9286
+ path: `${browserBase}/src/routes/HomeRoute.tsx`,
9287
+ purpose: "Focused authenticated home product surface.",
9288
+ requiredSignals: ["uses shared page primitives", "uses translation-safe product copy"],
9289
+ content: platformHomeRouteTsx()
9290
+ },
9291
+ {
9292
+ path: `${browserBase}/src/routes/GettingStartedRoute.tsx`,
9293
+ purpose: "Focused role-aware onboarding route reached from the account menu.",
9294
+ requiredSignals: ["uses shared onboarding", "returns to the configured home route"],
9295
+ content: platformGettingStartedRouteTsx()
9296
+ },
9297
+ {
9298
+ path: `${browserBase}/src/AppErrorBoundary.tsx`,
9299
+ purpose: "Recoverable render-error boundary for the product root.",
9300
+ requiredSignals: ["uses the shared auth flow error surface", "forwards render errors to shared capture"],
9301
+ content: platformAppErrorBoundaryTsx()
9302
+ }
9303
+ ] : []
9089
9304
  ];
9090
9305
  if (target === "platform") {
9091
9306
  if (options.browser) {
@@ -9136,6 +9351,15 @@ function scaffoldFiles(options) {
9136
9351
  "each contract has a zod input validator, keeping the manifest actions[] honest"
9137
9352
  ],
9138
9353
  content: topoloActionContractsMjs()
9354
+ },
9355
+ {
9356
+ path: `${base}/topolo.action-contracts.d.mts`,
9357
+ purpose: "TypeScript declaration for the generated JavaScript action-contract module.",
9358
+ requiredSignals: [
9359
+ "types topoloActionContracts against the canonical worker-runtime action contract",
9360
+ "keeps generated API typecheck strict without enabling allowJs"
9361
+ ],
9362
+ content: topoloActionContractsDmts()
9139
9363
  }
9140
9364
  ] : [],
9141
9365
  {
@@ -9310,7 +9534,8 @@ function scaffoldFiles(options) {
9310
9534
  ],
9311
9535
  content: appTsx(application, target, {
9312
9536
  audience: options.payload.audience,
9313
- portfolio: options.payload.portfolio
9537
+ portfolio: options.payload.portfolio,
9538
+ experience: options.payload.experience
9314
9539
  })
9315
9540
  });
9316
9541
  if (target === "platform") {
@@ -9321,7 +9546,7 @@ function scaffoldFiles(options) {
9321
9546
  "runs component/unit tests under jsdom",
9322
9547
  "keeps the generated browser surface testable from day one"
9323
9548
  ],
9324
- content: `import { defineConfig } from 'vitest/config';
9549
+ content: target === "platform" ? platformBrowserVitestConfig() : `import { defineConfig } from 'vitest/config';
9325
9550
 
9326
9551
  export default defineConfig({
9327
9552
  test: {
@@ -9331,6 +9556,40 @@ export default defineConfig({
9331
9556
  });
9332
9557
  `
9333
9558
  });
9559
+ if (target === "platform") {
9560
+ files.push(
9561
+ {
9562
+ path: `${browserBase}/test/application-contract.test.tsx`,
9563
+ purpose: "Executable route, navigation, and resource-switcher contract.",
9564
+ requiredSignals: ["asserts the configured product shape", "proves Getting Started is absent from product navigation"],
9565
+ content: platformBrowserContractTest(application, options.payload.experience)
9566
+ },
9567
+ {
9568
+ path: `${browserBase}/test/worker.test.ts`,
9569
+ purpose: "Executable browser Worker routing tests.",
9570
+ requiredSignals: ["proves API proxying", "proves non-API browser fallback"],
9571
+ content: platformWebWorkerTest(application, options.api)
9572
+ },
9573
+ {
9574
+ path: `${browserBase}/src/worker.ts`,
9575
+ purpose: "Browser Worker that serves assets and forwards API paths when an API target exists.",
9576
+ requiredSignals: [
9577
+ "keeps the frontend Worker thin",
9578
+ "only declares an API service binding for API-backed scaffolds"
9579
+ ],
9580
+ content: webWorkerTs(application, options.api)
9581
+ },
9582
+ {
9583
+ path: `${browserBase}/wrangler.toml`,
9584
+ purpose: "Cloudflare Worker static-assets configuration for the browser surface.",
9585
+ requiredSignals: [
9586
+ "serves Vite assets through a Worker with assets",
9587
+ "only binds an API Worker when the scaffold includes one"
9588
+ ],
9589
+ content: platformWebWranglerToml(application, options.api)
9590
+ }
9591
+ );
9592
+ }
9334
9593
  }
9335
9594
  }
9336
9595
  if (options.api) {
@@ -9371,24 +9630,6 @@ export default defineConfig({
9371
9630
  ],
9372
9631
  content: apiAppIdentityTs(application)
9373
9632
  },
9374
- {
9375
- path: `${browserBase}/src/worker.ts`,
9376
- purpose: "Browser Worker that serves assets and forwards API paths to the API Worker binding.",
9377
- requiredSignals: [
9378
- "keeps the frontend Worker thin",
9379
- "routes /api/* to the API service binding instead of owning backend routes locally"
9380
- ],
9381
- content: webWorkerTs(application)
9382
- },
9383
- {
9384
- path: `${browserBase}/wrangler.toml`,
9385
- purpose: "Cloudflare Worker static-assets configuration for the browser surface.",
9386
- requiredSignals: [
9387
- "serves Vite assets through a Worker with assets",
9388
- "binds the frontend Worker to the API Worker for /api/* requests"
9389
- ],
9390
- content: platformWebWranglerToml(application)
9391
- },
9392
9633
  {
9393
9634
  path: `${apiBase}/vitest.config.ts`,
9394
9635
  purpose: "Vitest config for the API worker surface.",
@@ -9396,7 +9637,7 @@ export default defineConfig({
9396
9637
  "runs worker/unit tests under the node environment",
9397
9638
  "keeps the generated service testable from day one"
9398
9639
  ],
9399
- content: `import { defineConfig } from 'vitest/config';
9640
+ content: target === "platform" ? platformApiVitestConfig() : `import { defineConfig } from 'vitest/config';
9400
9641
 
9401
9642
  export default defineConfig({
9402
9643
  test: {
@@ -9413,7 +9654,7 @@ export default defineConfig({
9413
9654
  "asserts the scaffold boots",
9414
9655
  "is replaced by real route/repository tests as the surface grows"
9415
9656
  ],
9416
- 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"
9657
+ 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"
9417
9658
  }
9418
9659
  );
9419
9660
  if (options.payload.resources.d1) {
@@ -10176,10 +10417,10 @@ function registerPlatformCommand(program2) {
10176
10417
  const platformApps = platform3.command("apps").description("Scaffold and operate first-party Topolo platform and personal applications");
10177
10418
  platformApps.command("scaffold <application>").description(
10178
10419
  "Generate a first-party Topolo scaffold for the platform or personal portfolio."
10179
- ).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(
10420
+ ).option("--json", "Output JSON").option("--no-json", "Output human-readable checklist").option("--app-dir <path>", "Application repo name or apps/<repo> path").option("--name <name>", "Application display/package source name").option("--package-name <name>", "package.json name").option("--production-url <url>", "Production browser URL").option("--service <id>", "Stable SDK/Auth app id for API-backed apps").option("--portfolio <portfolio>", "First-party portfolio: platform or personal").option(
10180
10421
  "--surface-model <surfaceModel>",
10181
10422
  "First-party surface model: web_app, mobile_container, mobile_module, or messaging_surface"
10182
- ).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(
10423
+ ).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--profile <profile>", "First-party scaffold profile such as spatial-assets").option("--with-d1", "Declare a D1 binding in the first-party scaffold").option("--with-r2", "Declare an R2 binding in the first-party scaffold").option("--with-queue", "Declare a Queue binding in the first-party scaffold").option("--with-workflow", "Declare a Workflow binding in the first-party scaffold").option("--with-container-processing", "Declare container-processing metadata in CloudControl").option("--home-path <path>", "Authenticated home route (default: /dashboard)").option("--home-label <label>", "Home navigation label (default: Dashboard)").option("--navigation-mode <mode>", "Product navigation: sidebar, topbar, or none").option("--workspace-switcher <mode>", "Workspace context selector: required, optional, or off").option("--tagline <text>", "Product-facing landing and home description").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include the API/service baseline").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
10183
10424
  "--provision",
10184
10425
  "Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
10185
10426
  ).option(
@@ -10214,7 +10455,12 @@ function registerPlatformCommand(program2) {
10214
10455
  withR2: opts.withR2,
10215
10456
  withQueue: opts.withQueue,
10216
10457
  withWorkflow: opts.withWorkflow,
10217
- withContainerProcessing: opts.withContainerProcessing
10458
+ withContainerProcessing: opts.withContainerProcessing,
10459
+ homePath: opts.homePath,
10460
+ homeLabel: opts.homeLabel,
10461
+ navigationMode: opts.navigationMode,
10462
+ workspaceSwitcher: opts.workspaceSwitcher,
10463
+ tagline: opts.tagline
10218
10464
  });
10219
10465
  if (opts.workflowOnly && !payload.ciWorkflow) {
10220
10466
  throw new Error("--workflow-only is only available for first-party scaffolds.");