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