@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.
- package/dist/index.js +851 -615
- 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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
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"] = "^
|
|
6036
|
+
devDependencies["@cloudflare/workers-types"] = "^5.20260716.1";
|
|
5973
6037
|
if (target === "platform") {
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6036
|
-
|
|
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
|
-
"-
|
|
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`
|
|
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(
|
|
6663
|
-
}
|
|
6664
|
-
function platformAppTsx(
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
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
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
'
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
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
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
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
|
-
|
|
7088
|
-
|
|
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
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
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
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
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
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
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:
|
|
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 "::
|
|
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:
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
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\
|
|
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.");
|