@topolo/cli 0.4.6 → 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 +851 -615
  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,33 @@ 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
+ }
5795
5822
  function normalizeTarget(value) {
5796
5823
  if (!value) return "platform";
5797
5824
  if (value === "platform" || value === "developer") return value;
@@ -5934,18 +5961,60 @@ var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
5934
5961
  var DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID = "0ebdeec262a245481a721fc4f9d6d798";
5935
5962
  var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
5936
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
+
5937
6006
  // src/commands/app-scaffold-package-template.ts
5938
6007
  function packageJson(application, target, options) {
5939
6008
  const dependencies = {};
5940
6009
  const devDependencies = {
5941
6010
  "@types/node": "^20.12.0",
5942
6011
  typescript: "^5.4.0",
5943
- vite: "^5.4.0"
6012
+ ...options.browser ? { vite: "^5.4.0" } : {}
5944
6013
  };
5945
6014
  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",
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",
5949
6018
  typecheck: "tsc --noEmit"
5950
6019
  };
5951
6020
  if (options.browser) {
@@ -5955,29 +6024,20 @@ function packageJson(application, target, options) {
5955
6024
  devDependencies["@types/react-dom"] = "^19.2.2";
5956
6025
  devDependencies["@vitejs/plugin-react"] = "^4.7.0";
5957
6026
  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);
6027
+ for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) {
6028
+ dependencies[packageName] = topoloPackageVersion(packageName, target);
6029
+ }
5966
6030
  dependencies["react-router-dom"] = "^7.6.2";
5967
6031
  } else {
5968
6032
  dependencies["@topolo/login"] = topoloPackageVersion("@topolo/login", target);
5969
6033
  }
5970
6034
  }
5971
6035
  if (options.api) {
5972
- devDependencies["@cloudflare/workers-types"] = "^4.20260413.1";
6036
+ devDependencies["@cloudflare/workers-types"] = "^5.20260716.1";
5973
6037
  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);
6038
+ for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) {
6039
+ dependencies[packageName] = topoloPackageVersion(packageName, target);
6040
+ }
5981
6041
  dependencies.hono = "^4";
5982
6042
  dependencies.zod = "^4.4.3";
5983
6043
  scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
@@ -5996,7 +6056,7 @@ function packageJson(application, target, options) {
5996
6056
  if (testDirs.length > 0) {
5997
6057
  const chain = (withCoverage) => testDirs.map((dir, index) => {
5998
6058
  const enter = index === 0 ? `cd ${dir}` : `cd ../${dir.split("/").pop()}`;
5999
- return `${enter} && vitest run --config vitest.config.ts --passWithNoTests${withCoverage ? " --coverage" : ""}`;
6059
+ return `${enter} && vitest run --config vitest.config.ts${withCoverage ? " --coverage" : ""}`;
6000
6060
  }).join(" && ");
6001
6061
  scripts.test = chain(false);
6002
6062
  scripts.coverage = chain(true);
@@ -6011,7 +6071,11 @@ function packageJson(application, target, options) {
6011
6071
  }
6012
6072
  }
6013
6073
  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";
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(" && ");
6015
6079
  }
6016
6080
  return `${json({
6017
6081
  name: application.packageName,
@@ -6032,29 +6096,8 @@ function topoloPackageVersion(packageName, target) {
6032
6096
  if (packageName === "@topolo/sdk") return "^0.8.1";
6033
6097
  return "^0.1.0";
6034
6098
  }
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;
6099
+ if (packageName.startsWith("@topolo/") || packageName.startsWith("@topolo-io/")) return "latest";
6100
+ throw new Error(`Unknown Topolo platform package: ${packageName}`);
6058
6101
  }
6059
6102
  function hasScaffoldResources(resources) {
6060
6103
  return resources.d1 || resources.r2 || resources.queue || resources.workflow || resources.containerProcessing;
@@ -6129,10 +6172,11 @@ function readme(application, target, options) {
6129
6172
  "## Development",
6130
6173
  "",
6131
6174
  "```bash",
6132
- "npm install",
6133
- "npm run typecheck",
6134
- "npm run build",
6135
- "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"] : [],
6136
6180
  "```",
6137
6181
  ""
6138
6182
  ];
@@ -6145,11 +6189,15 @@ function readme(application, target, options) {
6145
6189
  "- `@topolo-io/app-launcher` provides shared app switching and handoff helpers.",
6146
6190
  "- `@topolo-io/ui-kit` provides primitive account, command palette, icon, transition, and bug reporter UI.",
6147
6191
  "- `@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.",
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.",
6149
6197
  "- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
6150
6198
  );
6151
6199
  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.");
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.");
6153
6201
  }
6154
6202
  if (hasScaffoldResources(options.resources)) {
6155
6203
  lines.push(
@@ -6352,6 +6400,15 @@ function gitignore() {
6352
6400
  ""
6353
6401
  ].join("\n");
6354
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
+ }
6355
6412
  function envDts(api) {
6356
6413
  const lines = [
6357
6414
  '/// <reference types="vite/client" />',
@@ -6612,46 +6669,6 @@ function stylesCss() {
6612
6669
  "textarea {",
6613
6670
  " font: inherit;",
6614
6671
  "}",
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
6672
  ""
6656
6673
  ].join("\n");
6657
6674
  }
@@ -6659,450 +6676,429 @@ function stylesCss() {
6659
6676
  // src/commands/app-scaffold-app-template.ts
6660
6677
  function appTsx(application, target, options) {
6661
6678
  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.";
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[] {
6671
6856
  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");
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
+ `;
6973
6993
  }
6974
6994
  function developerAppTsx(application) {
6975
6995
  const appName = toTitle(application.name);
6976
6996
  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");
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
+ `;
7081
7073
  }
7082
7074
 
7083
7075
  // src/commands/app-scaffold-worker-template.ts
7084
- function webWorkerTs(application) {
7076
+ function webWorkerTs(application, api) {
7085
7077
  const serviceName = toTitle(application.name);
7086
7078
  return [
7087
- "interface Env {",
7088
- " API: { fetch(request: Request): Promise<Response> };",
7089
- "}",
7079
+ ...api ? [
7080
+ "interface Env {",
7081
+ " API: { fetch(request: Request): Promise<Response> };",
7082
+ "}"
7083
+ ] : ["type Env = Record<string, never>;"],
7090
7084
  "",
7091
7085
  `const SERVICE_NAME = '${escapeSingleQuoted(serviceName)}';`,
7092
7086
  "",
7093
7087
  "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
- " }",
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
+ ] : [],
7099
7095
  " return Response.json({ ok: true, service: SERVICE_NAME }, { status: 200 });",
7100
7096
  " },",
7101
7097
  "};",
7102
7098
  ""
7103
7099
  ].join("\n");
7104
7100
  }
7105
- function platformWebWranglerToml(application) {
7101
+ function platformWebWranglerToml(application, api) {
7106
7102
  const slug = toSlug(application.id);
7107
7103
  const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
7108
7104
  return [
@@ -7117,7 +7113,7 @@ function platformWebWranglerToml(application) {
7117
7113
  'directory = "./dist"',
7118
7114
  'binding = "ASSETS"',
7119
7115
  'not_found_handling = "single-page-application"',
7120
- 'run_worker_first = ["/api/*"]',
7116
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7121
7117
  "",
7122
7118
  "[observability]",
7123
7119
  "enabled = true",
@@ -7130,10 +7126,12 @@ function platformWebWranglerToml(application) {
7130
7126
  `APP_ID = "${application.appId ?? ""}"`,
7131
7127
  `VITE_TOPOLO_APP_ID = "${application.appId ?? ""}"`,
7132
7128
  "",
7133
- "[[services]]",
7134
- 'binding = "API"',
7135
- `service = "topolo-${slug}-api"`,
7136
- "",
7129
+ ...api ? [
7130
+ "[[services]]",
7131
+ 'binding = "API"',
7132
+ `service = "topolo-${slug}-api"`,
7133
+ ""
7134
+ ] : [],
7137
7135
  "[env.development]",
7138
7136
  `name = "topolo-${slug}-development"`,
7139
7137
  `account_id = "${DEVELOPMENT_CLOUDFLARE_ACCOUNT_ID}"`,
@@ -7151,12 +7149,14 @@ function platformWebWranglerToml(application) {
7151
7149
  'directory = "./dist"',
7152
7150
  'binding = "ASSETS"',
7153
7151
  '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"`,
7152
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7159
7153
  "",
7154
+ ...api ? [
7155
+ "[[env.development.services]]",
7156
+ 'binding = "API"',
7157
+ `service = "topolo-${slug}-api-development"`,
7158
+ ""
7159
+ ] : [],
7160
7160
  "[env.staging]",
7161
7161
  `name = "topolo-${slug}-staging"`,
7162
7162
  `account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
@@ -7174,12 +7174,14 @@ function platformWebWranglerToml(application) {
7174
7174
  'directory = "./dist"',
7175
7175
  'binding = "ASSETS"',
7176
7176
  '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"`,
7177
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7182
7178
  "",
7179
+ ...api ? [
7180
+ "[[env.staging.services]]",
7181
+ 'binding = "API"',
7182
+ `service = "topolo-${slug}-api-staging"`,
7183
+ ""
7184
+ ] : [],
7183
7185
  "[env.production]",
7184
7186
  `name = "topolo-${slug}-production"`,
7185
7187
  `account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
@@ -7197,12 +7199,14 @@ function platformWebWranglerToml(application) {
7197
7199
  'directory = "./dist"',
7198
7200
  'binding = "ASSETS"',
7199
7201
  'not_found_handling = "single-page-application"',
7200
- 'run_worker_first = ["/api/*"]',
7202
+ ...api ? ['run_worker_first = ["/api/*"]'] : [],
7201
7203
  "",
7202
- "[[env.production.services]]",
7203
- 'binding = "API"',
7204
- `service = "topolo-${slug}-api-production"`,
7205
- ""
7204
+ ...api ? [
7205
+ "[[env.production.services]]",
7206
+ 'binding = "API"',
7207
+ `service = "topolo-${slug}-api-production"`,
7208
+ ""
7209
+ ] : []
7206
7210
  ].join("\n");
7207
7211
  }
7208
7212
  function platformApiWranglerToml(application, resources, profile) {
@@ -7777,6 +7781,8 @@ function topoloConformanceWorkflow(options) {
7777
7781
  " pull_request:",
7778
7782
  " push:",
7779
7783
  " branches:",
7784
+ " - staging",
7785
+ " - development",
7780
7786
  " - main",
7781
7787
  " workflow_dispatch:",
7782
7788
  "",
@@ -7814,10 +7820,11 @@ function topoloConformanceWorkflow(options) {
7814
7820
  " token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}",
7815
7821
  ...cliCheckoutStep,
7816
7822
  "",
7817
- " - name: Skip conformance without cross-repo token",
7823
+ " - name: Require Topolo package and repository credentials",
7818
7824
  ` if: steps.app-token.outputs.token == '' && env.HAS_TOPOLO_CI_REPO_TOKEN != 'true'`,
7819
7825
  " run: |",
7820
- ' 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",
7821
7828
  "",
7822
7829
  " - name: Setup Node.js",
7823
7830
  ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
@@ -7825,6 +7832,14 @@ function topoloConformanceWorkflow(options) {
7825
7832
  " with:",
7826
7833
  " node-version: 20",
7827
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
+ "",
7828
7843
  " - name: Build Topolo SDK",
7829
7844
  ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7830
7845
  " working-directory: topolo-platform/packages/topolo-sdk",
@@ -7839,16 +7854,30 @@ function topoloConformanceWorkflow(options) {
7839
7854
  " npm install --no-audit --no-fund",
7840
7855
  " npm run build",
7841
7856
  "",
7842
- " - name: Run Topolo app conformance audit",
7857
+ " - name: Install generated application",
7843
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",
7863
+ ` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
7864
+ ` working-directory: ${appPath}`,
7844
7865
  " 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`,
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',
7846
7870
  "",
7847
- " - name: Record green CI evidence",
7871
+ " - name: Record verified CI evidence",
7848
7872
  ` if: \${{ success() && (steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true') }}`,
7849
7873
  ` working-directory: ${appPath}`,
7850
7874
  " run: |",
7851
- ` 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`
7852
7881
  ].join("\n");
7853
7882
  }
7854
7883
 
@@ -8022,6 +8051,15 @@ function topoloActionContractsMjs() {
8022
8051
  ""
8023
8052
  ].join("\n");
8024
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
+ }
8025
8063
  function baselineApiActions() {
8026
8064
  const emptyInputSchema = {
8027
8065
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -8715,6 +8753,131 @@ function envExample(application) {
8715
8753
  ].join("\n");
8716
8754
  }
8717
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
+
8718
8881
  // src/commands/app-scaffold.ts
8719
8882
  function standardScriptStub() {
8720
8883
  return `#!/usr/bin/env python3
@@ -8810,6 +8973,14 @@ function applicationScaffoldPayload(application, options = {}) {
8810
8973
  const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
8811
8974
  const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
8812
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
+ };
8813
8984
  const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
8814
8985
  const defaultAppId = target === "platform" ? `app_topolo_${id}` : catalogApplication?.services[0] ?? id;
8815
8986
  const appId = api ? options.appId ?? defaultAppId : null;
@@ -8838,6 +9009,7 @@ function applicationScaffoldPayload(application, options = {}) {
8838
9009
  containerAppId,
8839
9010
  profile,
8840
9011
  resources,
9012
+ experience,
8841
9013
  distributionModel,
8842
9014
  capabilities,
8843
9015
  application: applicationPayload,
@@ -8856,6 +9028,7 @@ function applicationScaffoldPayload(application, options = {}) {
8856
9028
  containerAppId,
8857
9029
  profile,
8858
9030
  resources,
9031
+ experience,
8859
9032
  distributionModel,
8860
9033
  capabilities
8861
9034
  },
@@ -8924,18 +9097,10 @@ function scaffoldPackages(options) {
8924
9097
  }
8925
9098
  const packages = /* @__PURE__ */ new Set();
8926
9099
  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");
9100
+ for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) packages.add(packageName);
8935
9101
  }
8936
9102
  if (options.api) {
8937
- packages.add("@topolo-io/worker-runtime");
8938
- packages.add("@topolo/sdk");
9103
+ for (const packageName of PLATFORM_API_TOPOLO_PACKAGES) packages.add(packageName);
8939
9104
  }
8940
9105
  return Array.from(packages);
8941
9106
  }
@@ -9015,6 +9180,12 @@ function scaffoldFiles(options) {
9015
9180
  ],
9016
9181
  content: gitignore()
9017
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
+ }] : [],
9018
9189
  {
9019
9190
  path: `${base}/AGENTS.md`,
9020
9191
  purpose: "Local agent rules for safe work inside the generated project.",
@@ -9030,42 +9201,44 @@ function scaffoldFiles(options) {
9030
9201
  resources: options.payload.resources
9031
9202
  })
9032
9203
  },
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
- },
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
+ ] : [],
9069
9242
  ...options.payload.resources.workflow ? [
9070
9243
  {
9071
9244
  path: `${base}/src/cloudflare-workers.d.ts`,
@@ -9077,7 +9250,7 @@ function scaffoldFiles(options) {
9077
9250
  content: cloudflareWorkersDts()
9078
9251
  }
9079
9252
  ] : [],
9080
- {
9253
+ ...options.browser ? [{
9081
9254
  path: `${browserBase}/src/styles.css`,
9082
9255
  purpose: "Minimal app-local styles outside the shared Topolo shell.",
9083
9256
  requiredSignals: [
@@ -9085,7 +9258,39 @@ function scaffoldFiles(options) {
9085
9258
  "keeps app-specific layout scoped under generated classes"
9086
9259
  ],
9087
9260
  content: stylesCss()
9088
- }
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
+ ] : []
9089
9294
  ];
9090
9295
  if (target === "platform") {
9091
9296
  if (options.browser) {
@@ -9136,6 +9341,15 @@ function scaffoldFiles(options) {
9136
9341
  "each contract has a zod input validator, keeping the manifest actions[] honest"
9137
9342
  ],
9138
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()
9139
9353
  }
9140
9354
  ] : [],
9141
9355
  {
@@ -9310,7 +9524,8 @@ function scaffoldFiles(options) {
9310
9524
  ],
9311
9525
  content: appTsx(application, target, {
9312
9526
  audience: options.payload.audience,
9313
- portfolio: options.payload.portfolio
9527
+ portfolio: options.payload.portfolio,
9528
+ experience: options.payload.experience
9314
9529
  })
9315
9530
  });
9316
9531
  if (target === "platform") {
@@ -9321,7 +9536,7 @@ function scaffoldFiles(options) {
9321
9536
  "runs component/unit tests under jsdom",
9322
9537
  "keeps the generated browser surface testable from day one"
9323
9538
  ],
9324
- content: `import { defineConfig } from 'vitest/config';
9539
+ content: target === "platform" ? platformBrowserVitestConfig() : `import { defineConfig } from 'vitest/config';
9325
9540
 
9326
9541
  export default defineConfig({
9327
9542
  test: {
@@ -9331,6 +9546,40 @@ export default defineConfig({
9331
9546
  });
9332
9547
  `
9333
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
+ }
9334
9583
  }
9335
9584
  }
9336
9585
  if (options.api) {
@@ -9371,24 +9620,6 @@ export default defineConfig({
9371
9620
  ],
9372
9621
  content: apiAppIdentityTs(application)
9373
9622
  },
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
9623
  {
9393
9624
  path: `${apiBase}/vitest.config.ts`,
9394
9625
  purpose: "Vitest config for the API worker surface.",
@@ -9396,7 +9627,7 @@ export default defineConfig({
9396
9627
  "runs worker/unit tests under the node environment",
9397
9628
  "keeps the generated service testable from day one"
9398
9629
  ],
9399
- content: `import { defineConfig } from 'vitest/config';
9630
+ content: target === "platform" ? platformApiVitestConfig() : `import { defineConfig } from 'vitest/config';
9400
9631
 
9401
9632
  export default defineConfig({
9402
9633
  test: {
@@ -9413,7 +9644,7 @@ export default defineConfig({
9413
9644
  "asserts the scaffold boots",
9414
9645
  "is replaced by real route/repository tests as the surface grows"
9415
9646
  ],
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"
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"
9417
9648
  }
9418
9649
  );
9419
9650
  if (options.payload.resources.d1) {
@@ -10179,7 +10410,7 @@ function registerPlatformCommand(program2) {
10179
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(
10180
10411
  "--surface-model <surfaceModel>",
10181
10412
  "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(
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(
10183
10414
  "--provision",
10184
10415
  "Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
10185
10416
  ).option(
@@ -10214,7 +10445,12 @@ function registerPlatformCommand(program2) {
10214
10445
  withR2: opts.withR2,
10215
10446
  withQueue: opts.withQueue,
10216
10447
  withWorkflow: opts.withWorkflow,
10217
- 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
10218
10454
  });
10219
10455
  if (opts.workflowOnly && !payload.ciWorkflow) {
10220
10456
  throw new Error("--workflow-only is only available for first-party scaffolds.");