@topolo/cli 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +19 -6
  2. package/dist/index.js +1977 -78
  3. package/package.json +2 -2
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
- // ../../PlatformApplications/packages/topolo-sdk/dist/index.js
12
+ // ../../../../../Users/ashes/Projects/topolo/PlatformApplications/packages/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);
@@ -1938,11 +1938,11 @@ async function pollUntilApproved(params) {
1938
1938
  throw new Error("Device code expired. Run `topolo auth login` again.");
1939
1939
  }
1940
1940
  function sleep(ms) {
1941
- return new Promise((resolve) => setTimeout(resolve, ms));
1941
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1942
1942
  }
1943
1943
 
1944
1944
  // src/version.ts
1945
- var CLI_VERSION = "0.1.10";
1945
+ var CLI_VERSION = "0.1.12";
1946
1946
 
1947
1947
  // src/client.ts
1948
1948
  async function buildClient(options) {
@@ -2304,13 +2304,26 @@ function enrichFinding(application, finding2, evidence) {
2304
2304
  });
2305
2305
  }
2306
2306
  if (shellEvidence.length === 0) return finding2;
2307
+ if (shellEvidence.includes("local=account-menu")) {
2308
+ return mergeFinding(finding2, {
2309
+ status: "needs_review",
2310
+ reason: "An app-local account menu implementation is visible locally. Account dropdown chrome must come from TopoloAppShell or TopoloAccountMenu, with app-specific actions passed as additive menu items.",
2311
+ evidence: shellEvidence,
2312
+ nextActions: [
2313
+ "Replace app-local UserMenu, user-menu, or accountMenuOpen dropdown code with TopoloAccountMenu",
2314
+ "Use TopoloAppShell accountMenuItems for app-specific account actions",
2315
+ "Keep sign-out, app launcher, command palette, theme, and support actions on the shared account-menu surface"
2316
+ ]
2317
+ });
2318
+ }
2307
2319
  if (!shellEvidence.includes("source=TopoloAppShell")) {
2308
2320
  return mergeFinding(finding2, {
2309
2321
  status: "needs_review",
2310
- reason: "Shared UI-kit or primitive shell usage is visible locally, but authenticated browser apps must use TopoloAppShell so launcher, account-menu, command, theme, and bug-reporting chrome stay on the shared platform surface.",
2322
+ reason: "Shared UI-kit or primitive shell usage is visible locally, but authenticated browser apps must use TopoloAppShell so launcher, TopoloAccountMenu, command, theme, and bug-reporting chrome stay on the shared platform surface.",
2311
2323
  evidence: shellEvidence,
2312
2324
  nextActions: [
2313
2325
  "Use TopoloAppShell for authenticated first-party app layouts",
2326
+ "Use TopoloAccountMenu when a partial shell still needs account chrome",
2314
2327
  "Keep launcher, account menu, command palette, theme toggle, and bug reporter on the shared shell surface",
2315
2328
  "Keep mobile and desktop shell behavior aligned with the platform UI kit"
2316
2329
  ]
@@ -2322,6 +2335,7 @@ function enrichFinding(application, finding2, evidence) {
2322
2335
  evidence: shellEvidence,
2323
2336
  nextActions: [
2324
2337
  "Use TopoloAppShell for authenticated first-party app layouts",
2338
+ "Pass app-specific account actions through accountMenuItems instead of app-local dropdowns",
2325
2339
  "Keep launcher, account menu, command palette, theme toggle, and bug reporter on the shared shell surface",
2326
2340
  "Keep mobile and desktop shell behavior aligned with the platform UI kit"
2327
2341
  ]
@@ -2373,14 +2387,14 @@ function enrichFinding(application, finding2, evidence) {
2373
2387
  function collectEvidence(repoRoot, application) {
2374
2388
  const appRoot = path.join(repoRoot, "PlatformApplications", application.appDir);
2375
2389
  const packageJsonPath = path.join(appRoot, "package.json");
2376
- const packageJson = readPackageJson(packageJsonPath);
2390
+ const packageJson2 = readPackageJson(packageJsonPath);
2377
2391
  const cloudcontrolPath = path.join(appRoot, "topolo.cloudcontrol.json");
2378
2392
  const systems = matchingSystems(repoRoot, application);
2379
2393
  return {
2380
2394
  appRoot,
2381
2395
  appExists: isDirectory(appRoot),
2382
2396
  packageJsonPath,
2383
- packageJson,
2397
+ packageJson: packageJson2,
2384
2398
  packageJsons: collectPackageJsons(appRoot),
2385
2399
  cloudcontrolPath,
2386
2400
  hasCloudControl: isFile(cloudcontrolPath),
@@ -2435,8 +2449,12 @@ function sharedShellEvidence(evidence) {
2435
2449
  if (text.includes("@topolo/ui-kit")) items.push("source=@topolo/ui-kit");
2436
2450
  if (text.includes("TopoloShell")) items.push("source=TopoloShell");
2437
2451
  if (text.includes("TopoloAppShell")) items.push("source=TopoloAppShell");
2452
+ if (text.includes("TopoloAccountMenu")) items.push("source=TopoloAccountMenu");
2438
2453
  if (text.includes("TopoloLauncher")) items.push("source=TopoloLauncher");
2439
2454
  if (text.includes("AppSwitcher")) items.push("source=AppSwitcher");
2455
+ if (/\baccountMenuOpen\b|\bsetAccountMenuOpen\b|\buserMenuOpen\b|\bUserMenu\b|user-menu/.test(text)) {
2456
+ items.push("local=account-menu");
2457
+ }
2440
2458
  return unique(items);
2441
2459
  }
2442
2460
  function hasDocumentationSurface(application, evidence) {
@@ -2601,8 +2619,8 @@ function collectPackageJsons(root) {
2601
2619
  continue;
2602
2620
  }
2603
2621
  if (!entry.isFile() || entry.name !== "package.json") continue;
2604
- const packageJson = readPackageJson(entryPath);
2605
- if (packageJson) records.push({ path: entryPath, packageJson });
2622
+ const packageJson2 = readPackageJson(entryPath);
2623
+ if (packageJson2) records.push({ path: entryPath, packageJson: packageJson2 });
2606
2624
  }
2607
2625
  };
2608
2626
  visit(root);
@@ -2691,38 +2709,121 @@ function isFile(filePath) {
2691
2709
  }
2692
2710
 
2693
2711
  // src/commands/app-scaffold.ts
2712
+ import { constants } from "fs";
2713
+ import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
2714
+ import { dirname as dirname2, relative, resolve } from "path";
2715
+ var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
2716
+ var CURRENT_VERIFICATION_DATE = "2026-04-23";
2694
2717
  function applicationScaffoldPayload(application, options = {}) {
2718
+ const target = normalizeTarget(options.target);
2719
+ const portfolio = target === "platform" ? normalizePortfolio(options.portfolio) : null;
2695
2720
  const id = normalizeApplicationId(application);
2721
+ const slug = toSlug(id);
2696
2722
  const catalogApplication = APPLICATIONS[id];
2697
2723
  const browser = options.browser !== false;
2698
- const api = options.api !== false;
2699
- const appDir = options.appDir ?? catalogApplication?.appDir ?? `Topolo${toPascalCase(id)}`;
2700
- const name = options.name ?? catalogApplication?.name ?? `topolo-${id}`;
2701
- const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${id}`;
2702
- const serviceId = api ? options.service ?? catalogApplication?.services[0] ?? id : null;
2703
- const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${id}.topolo.app` : null;
2724
+ const api = options.api ?? target === "platform";
2725
+ const audience = normalizeAudience(
2726
+ options.audience,
2727
+ target === "platform" ? portfolio : null
2728
+ );
2729
+ const tenancy = normalizeTenancy(options.tenancy, audience);
2730
+ const surfaceModel = normalizeSurfaceModel(options.surfaceModel);
2731
+ const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
2732
+ const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
2733
+ const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
2734
+ const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
2735
+ const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
2736
+ const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
2737
+ const defaultServiceId = target === "platform" ? `srv_topolo_${id}` : id;
2738
+ const serviceId = api ? options.service ?? (target === "platform" ? defaultServiceId : catalogApplication?.services[0] ?? defaultServiceId) : null;
2739
+ const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
2704
2740
  const scopes = scaffoldScopes({ browser, api });
2741
+ const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
2742
+ const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir }) : null;
2743
+ const applicationPayload = {
2744
+ id,
2745
+ name,
2746
+ appDir,
2747
+ packageName,
2748
+ productionUrl,
2749
+ serviceId
2750
+ };
2705
2751
  return {
2706
2752
  version: APPLICATION_REQUIREMENTS_VERSION,
2707
- application: {
2708
- id,
2709
- name,
2710
- appDir,
2711
- packageName,
2712
- productionUrl,
2713
- serviceId
2714
- },
2753
+ target,
2754
+ ownerType,
2755
+ portfolio,
2756
+ audience,
2757
+ tenancy,
2758
+ surfaceModel,
2759
+ containerAppId,
2760
+ distributionModel,
2761
+ application: applicationPayload,
2715
2762
  scopes,
2716
2763
  requirements: APPLICATION_REQUIREMENTS.filter(
2717
2764
  (requirement) => requirement.appliesTo.some((scope) => scopes.includes(scope))
2718
2765
  ),
2719
- packages: scaffoldPackages({ browser, api }),
2720
- files: scaffoldFiles({ id, appDir, browser, api }),
2721
- commands: scaffoldCommands(id),
2722
- ciWorkflowPath: ".github/workflows/topolo-conformance.yml",
2723
- ciWorkflow: topoloConformanceWorkflow({ id, appDir })
2766
+ packages: scaffoldPackages({ browser, api, target }),
2767
+ files: scaffoldFiles({
2768
+ payload: {
2769
+ ownerType,
2770
+ portfolio,
2771
+ audience,
2772
+ tenancy,
2773
+ surfaceModel,
2774
+ containerAppId,
2775
+ distributionModel
2776
+ },
2777
+ application: applicationPayload,
2778
+ target,
2779
+ browser,
2780
+ api,
2781
+ ciWorkflow
2782
+ }),
2783
+ commands: scaffoldCommands({
2784
+ id,
2785
+ target,
2786
+ audience,
2787
+ tenancy,
2788
+ portfolio
2789
+ }),
2790
+ ciWorkflowPath,
2791
+ ciWorkflow
2724
2792
  };
2725
2793
  }
2794
+ async function writeApplicationScaffold(payload, options = {}) {
2795
+ const root = resolve(options.repoRoot ?? process.cwd());
2796
+ const plannedFiles = payload.files.map((file) => ({
2797
+ file,
2798
+ absolutePath: resolveInside(root, file.path)
2799
+ }));
2800
+ const existingFiles = [];
2801
+ for (const planned of plannedFiles) {
2802
+ if (await pathExists(planned.absolutePath)) {
2803
+ existingFiles.push(planned.file.path);
2804
+ }
2805
+ }
2806
+ if (existingFiles.length > 0 && !options.force) {
2807
+ throw new Error(
2808
+ [
2809
+ "Refusing to overwrite existing scaffold files:",
2810
+ ...existingFiles.map((filePath) => `- ${filePath}`),
2811
+ "Pass --force to overwrite these files."
2812
+ ].join("\n")
2813
+ );
2814
+ }
2815
+ const files = [];
2816
+ for (const planned of plannedFiles) {
2817
+ const existed = await pathExists(planned.absolutePath);
2818
+ await mkdir2(dirname2(planned.absolutePath), { recursive: true });
2819
+ await writeFile2(planned.absolutePath, planned.file.content, "utf8");
2820
+ files.push({
2821
+ path: planned.file.path,
2822
+ status: existed ? "overwritten" : "created"
2823
+ });
2824
+ }
2825
+ return { root, files };
2826
+ }
2726
2827
  function scaffoldScopes(options) {
2727
2828
  const scopes = ["all"];
2728
2829
  if (options.browser) scopes.push("browser");
@@ -2732,6 +2833,9 @@ function scaffoldScopes(options) {
2732
2833
  return scopes;
2733
2834
  }
2734
2835
  function scaffoldPackages(options) {
2836
+ if (options.target === "developer") {
2837
+ return [];
2838
+ }
2735
2839
  const packages = /* @__PURE__ */ new Set();
2736
2840
  if (options.browser) {
2737
2841
  packages.add("@topolo/ui-kit");
@@ -2744,52 +2848,206 @@ function scaffoldPackages(options) {
2744
2848
  return Array.from(packages);
2745
2849
  }
2746
2850
  function scaffoldFiles(options) {
2747
- const base = `PlatformApplications/${options.appDir}`;
2851
+ const { application, target } = options;
2852
+ const slug = toSlug(application.id);
2853
+ const base = target === "platform" ? `PlatformApplications/${application.appDir}` : application.appDir;
2748
2854
  const files = [
2749
2855
  {
2750
2856
  path: `${base}/package.json`,
2751
2857
  purpose: "Package metadata and verification commands.",
2752
2858
  requiredSignals: [
2753
2859
  "name matches the packageName in the SDK application catalog",
2754
- "scripts include build plus typecheck or test where the stack supports them"
2755
- ]
2860
+ "scripts include build plus typecheck where the stack supports them"
2861
+ ],
2862
+ content: packageJson(application, target, { browser: options.browser, api: options.api })
2863
+ },
2864
+ {
2865
+ path: `${base}/README.md`,
2866
+ purpose: "Starter README with install, run, and verification commands.",
2867
+ requiredSignals: [
2868
+ "documents the generated app target",
2869
+ "points maintainers to the shared Topolo packages used by the scaffold"
2870
+ ],
2871
+ content: readme(application, target, {
2872
+ api: options.api,
2873
+ audience: options.payload.audience,
2874
+ tenancy: options.payload.tenancy,
2875
+ portfolio: options.payload.portfolio
2876
+ })
2877
+ },
2878
+ {
2879
+ path: `${base}/.gitignore`,
2880
+ purpose: "Generated-app ignore rules.",
2881
+ requiredSignals: [
2882
+ "ignores dependency installs and build output",
2883
+ "keeps local environment files out of source control"
2884
+ ],
2885
+ content: gitignore()
2886
+ },
2887
+ {
2888
+ path: `${base}/tsconfig.json`,
2889
+ purpose: "TypeScript configuration for the generated app.",
2890
+ requiredSignals: [
2891
+ "uses strict mode",
2892
+ "includes browser DOM types for React and Worker types for API-backed apps"
2893
+ ],
2894
+ content: tsconfigJson(options.api)
2895
+ },
2896
+ {
2897
+ path: `${base}/vite.config.ts`,
2898
+ purpose: "Vite browser build configuration.",
2899
+ requiredSignals: [
2900
+ "builds the authenticated browser surface",
2901
+ "keeps generated apps on the standard React/Vite baseline"
2902
+ ],
2903
+ content: viteConfig()
2756
2904
  },
2757
2905
  {
2758
- path: `${base}/topolo.cloudcontrol.json`,
2759
- purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
2906
+ path: `${base}/index.html`,
2907
+ purpose: "Browser entry document.",
2760
2908
  requiredSignals: [
2761
- "production URLs and Cloudflare targets are explicit",
2762
- "resource bindings and required secrets are declared without secret values"
2763
- ]
2909
+ "mounts the React app at #root",
2910
+ "uses the generated application display name"
2911
+ ],
2912
+ content: indexHtml(application.name)
2764
2913
  },
2765
2914
  {
2766
- path: `${base}/.github/workflows/topolo-conformance.yml`,
2767
- purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
2915
+ path: `${base}/src/main.tsx`,
2916
+ purpose: "React app entrypoint.",
2768
2917
  requiredSignals: [
2769
- "checks out this repo under its PlatformApplications app directory",
2770
- "checks out TopoloDocs under PlatformApplications/TopoloDocs",
2771
- "checks out topolo-sdk and TopoloCli from main so the gate does not wait for an npm publish",
2772
- "runs topolo apps audit with --local and --fail-on needs_review"
2773
- ]
2918
+ "renders the generated app component",
2919
+ "imports the generated app stylesheet"
2920
+ ],
2921
+ content: mainTsx()
2774
2922
  },
2775
2923
  {
2776
- path: `PlatformApplications/TopoloDocs/src/content/systems/topolo-${options.id}.json`,
2777
- purpose: "Canonical system registry entry.",
2924
+ path: `${base}/src/env.d.ts`,
2925
+ purpose: "Vite environment variable typing.",
2778
2926
  requiredSignals: [
2779
- `repo_paths includes ${base}`,
2780
- "doc_paths includes internal handbook coverage",
2781
- "security_assurance records the auth, tenancy, deployment, and verification boundary"
2782
- ]
2927
+ "loads vite/client import.meta.env types",
2928
+ "declares the Topolo environment variables consumed by the starter"
2929
+ ],
2930
+ content: envDts(options.api)
2783
2931
  },
2784
2932
  {
2785
- path: `PlatformApplications/TopoloDocs/src/content/internal/apps/${options.id}.mdx`,
2786
- purpose: "Internal operator handbook.",
2933
+ path: `${base}/src/styles.css`,
2934
+ purpose: "Minimal app-local styles outside the shared Topolo shell.",
2787
2935
  requiredSignals: [
2788
- "documents ownership, routes, auth boundary, deployment, operations, and verification",
2789
- "last_verified is updated when behavior changes"
2790
- ]
2936
+ "does not fork the shared shell chrome",
2937
+ "keeps app-specific layout scoped under generated classes"
2938
+ ],
2939
+ content: stylesCss()
2791
2940
  }
2792
2941
  ];
2942
+ if (target === "platform") {
2943
+ files.push(
2944
+ {
2945
+ path: `${base}/topolo.cloudcontrol.json`,
2946
+ purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
2947
+ requiredSignals: [
2948
+ "production URLs and Cloudflare targets are explicit",
2949
+ "resource bindings and required secrets are declared without secret values"
2950
+ ],
2951
+ content: cloudcontrolJson(application, { api: options.api })
2952
+ },
2953
+ {
2954
+ path: `${base}/.github/workflows/topolo-conformance.yml`,
2955
+ purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
2956
+ requiredSignals: [
2957
+ "checks out this repo under its PlatformApplications app directory",
2958
+ "checks out TopoloDocs under PlatformApplications/TopoloDocs",
2959
+ "checks out topolo-sdk and TopoloCli from main so the gate does not wait for an npm publish",
2960
+ "runs topolo apps audit with --local and --fail-on needs_review"
2961
+ ],
2962
+ content: options.ciWorkflow ?? ""
2963
+ },
2964
+ {
2965
+ path: `PlatformApplications/TopoloDocs/src/content/systems/topolo-${slug}.json`,
2966
+ purpose: "Canonical system registry entry.",
2967
+ requiredSignals: [
2968
+ `repo_paths includes ${base}`,
2969
+ "doc_paths includes internal handbook coverage",
2970
+ "security_assurance records the auth, tenancy, deployment, and verification boundary"
2971
+ ],
2972
+ content: systemJson(application, {
2973
+ api: options.api,
2974
+ audience: options.payload.audience,
2975
+ tenancy: options.payload.tenancy,
2976
+ portfolio: options.payload.portfolio
2977
+ })
2978
+ },
2979
+ {
2980
+ path: `PlatformApplications/TopoloDocs/src/content/internal/apps/${slug}.mdx`,
2981
+ purpose: "Internal operator handbook.",
2982
+ requiredSignals: [
2983
+ "documents ownership, routes, auth boundary, deployment, operations, and verification",
2984
+ "last_verified is updated when behavior changes"
2985
+ ],
2986
+ content: internalDocs(application, {
2987
+ api: options.api,
2988
+ audience: options.payload.audience,
2989
+ tenancy: options.payload.tenancy,
2990
+ portfolio: options.payload.portfolio
2991
+ })
2992
+ },
2993
+ {
2994
+ path: `PlatformApplications/TopoloDocs/src/content/public/applications/${slug}.mdx`,
2995
+ purpose: "Public application overview.",
2996
+ requiredSignals: [
2997
+ "documents the product surface without internal-only details",
2998
+ "links back to the canonical system registry entry"
2999
+ ],
3000
+ content: publicDocs(application, {
3001
+ api: options.api,
3002
+ audience: options.payload.audience,
3003
+ tenancy: options.payload.tenancy,
3004
+ portfolio: options.payload.portfolio
3005
+ })
3006
+ }
3007
+ );
3008
+ if (options.api && application.serviceId) {
3009
+ files.push({
3010
+ path: `PlatformApplications/TopoloAuth/catalog/service-manifests/${application.serviceId}.json`,
3011
+ purpose: "Auth service manifest for first-party permissions, role bundles, and API-key scopes.",
3012
+ requiredSignals: [
3013
+ "serviceId matches the generated Worker service id",
3014
+ "permissions, role bundles, and API-key scopes exist before the app is catalog-visible",
3015
+ "systems metadata maps the service back to the generated TopoloDocs entries"
3016
+ ],
3017
+ content: serviceManifestJson(application, {
3018
+ tenancy: options.payload.tenancy
3019
+ })
3020
+ });
3021
+ }
3022
+ } else {
3023
+ files.push(
3024
+ {
3025
+ path: `${base}/topolo.app.json`,
3026
+ purpose: "Third-party Topolo application manifest.",
3027
+ requiredSignals: [
3028
+ "declares the external app id, callback URLs, and requested platform scopes",
3029
+ "does not include internal Topolo CloudControl or canonical-docs ownership"
3030
+ ],
3031
+ content: developerManifest(application, {
3032
+ api: options.api,
3033
+ audience: options.payload.audience,
3034
+ tenancy: options.payload.tenancy,
3035
+ surfaceModel: options.payload.surfaceModel,
3036
+ containerAppId: options.payload.containerAppId,
3037
+ distributionModel: options.payload.distributionModel
3038
+ })
3039
+ },
3040
+ {
3041
+ path: `${base}/.env.example`,
3042
+ purpose: "Developer environment template for local auth configuration.",
3043
+ requiredSignals: [
3044
+ "names the Topolo Auth URL",
3045
+ "keeps OAuth client ids and service ids out of source control by default"
3046
+ ],
3047
+ content: envExample(application)
3048
+ }
3049
+ );
3050
+ }
2793
3051
  if (options.browser) {
2794
3052
  files.push({
2795
3053
  path: `${base}/src/App.tsx`,
@@ -2797,8 +3055,13 @@ function scaffoldFiles(options) {
2797
3055
  requiredSignals: [
2798
3056
  "imports shared TopoloAppShell from @topolo/ui-kit",
2799
3057
  "uses @topolo/auth-client for app-origin auth flows",
2800
- "keeps launcher and account-menu behavior on the shared platform surface"
2801
- ]
3058
+ "keeps launcher and account-menu behavior on the shared platform surface",
3059
+ "passes app-specific account actions through accountMenuItems or TopoloAccountMenu"
3060
+ ],
3061
+ content: appTsx(application, target, {
3062
+ audience: options.payload.audience,
3063
+ portfolio: options.payload.portfolio
3064
+ })
2802
3065
  });
2803
3066
  }
2804
3067
  if (options.api) {
@@ -2807,9 +3070,10 @@ function scaffoldFiles(options) {
2807
3070
  path: `${base}/wrangler.toml`,
2808
3071
  purpose: "Cloudflare Worker runtime configuration.",
2809
3072
  requiredSignals: [
2810
- "bindings match topolo.cloudcontrol.json",
3073
+ "bindings match topolo.cloudcontrol.json or topolo.app.json",
2811
3074
  "secrets are referenced by name only"
2812
- ]
3075
+ ],
3076
+ content: wranglerToml(application, target)
2813
3077
  },
2814
3078
  {
2815
3079
  path: `${base}/src/worker.ts`,
@@ -2818,18 +3082,34 @@ function scaffoldFiles(options) {
2818
3082
  "uses @topolo/auth-middleware or Auth validation for protected routes",
2819
3083
  "derives organization from the credential rather than client-supplied orgId",
2820
3084
  "emits or propagates X-Topolo-Request-Id and X-Topolo-Client"
2821
- ]
3085
+ ],
3086
+ content: workerTs(application, target, {
3087
+ audience: options.payload.audience,
3088
+ tenancy: options.payload.tenancy
3089
+ })
2822
3090
  }
2823
3091
  );
2824
3092
  }
2825
3093
  return files;
2826
3094
  }
2827
- function scaffoldCommands(id) {
3095
+ function scaffoldCommands(options) {
3096
+ const scaffoldId = toSlug(options.id);
3097
+ if (options.target === "developer") {
3098
+ const audienceFlags = options.audience === "personal" ? " --audience personal --tenancy household" : "";
3099
+ return [
3100
+ `topolo apps scaffold ${scaffoldId}${audienceFlags} --write --repo-root .`,
3101
+ `cd topolo-${scaffoldId}`,
3102
+ "npm install",
3103
+ "npm run typecheck",
3104
+ "npm run build"
3105
+ ];
3106
+ }
3107
+ const portfolioFlags = options.portfolio === "personal" ? " --portfolio personal" : "";
2828
3108
  return [
2829
- `topolo apps scaffold ${id} --json`,
3109
+ `topolo platform apps scaffold ${scaffoldId}${portfolioFlags} --write --repo-root /Users/ashes/Projects/topolo --json`,
2830
3110
  "topolo apps requirements --json",
2831
- `topolo apps audit ${id} --local --repo-root /Users/ashes/Projects/topolo --fail-on needs_review --json`,
2832
- `topolo apps audit ${id} --local --repo-root /Users/ashes/Projects/topolo --fail-on partial --json`
3111
+ `topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/topolo --fail-on needs_review --json`,
3112
+ `topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/topolo --fail-on partial --json`
2833
3113
  ];
2834
3114
  }
2835
3115
  function topoloConformanceWorkflow(options) {
@@ -2904,18 +3184,1448 @@ function topoloConformanceWorkflow(options) {
2904
3184
  ` node PlatformApplications/TopoloCli/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`
2905
3185
  ].join("\n");
2906
3186
  }
3187
+ function packageJson(application, target, options) {
3188
+ const dependencies = {};
3189
+ const devDependencies = {
3190
+ "@types/node": "^20.12.0",
3191
+ typescript: "^5.4.0",
3192
+ vite: "^5.4.0"
3193
+ };
3194
+ const scripts = {
3195
+ dev: "vite --host 0.0.0.0",
3196
+ build: "vite build",
3197
+ preview: "vite preview --host 0.0.0.0",
3198
+ typecheck: "tsc --noEmit"
3199
+ };
3200
+ if (options.browser) {
3201
+ dependencies.react = "^18.3.1";
3202
+ dependencies["react-dom"] = "^18.3.1";
3203
+ devDependencies["@types/react"] = "^18.3.3";
3204
+ devDependencies["@types/react-dom"] = "^18.3.0";
3205
+ devDependencies["@vitejs/plugin-react"] = "^4.7.0";
3206
+ if (target === "platform") {
3207
+ dependencies["@topolo/auth-client"] = topoloPackageVersion("@topolo/auth-client", target);
3208
+ dependencies["@topolo/ui-kit"] = topoloPackageVersion("@topolo/ui-kit", target);
3209
+ }
3210
+ }
3211
+ if (options.api) {
3212
+ if (target === "platform") {
3213
+ dependencies["@topolo/auth-middleware"] = topoloPackageVersion("@topolo/auth-middleware", target);
3214
+ dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
3215
+ }
3216
+ devDependencies.wrangler = "^4.40.2";
3217
+ scripts.deploy = "npm run build && wrangler deploy";
3218
+ }
3219
+ return `${json({
3220
+ name: application.packageName,
3221
+ version: "0.1.0",
3222
+ private: true,
3223
+ type: "module",
3224
+ scripts,
3225
+ dependencies,
3226
+ devDependencies
3227
+ })}
3228
+ `;
3229
+ }
3230
+ function topoloPackageVersion(packageName, target) {
3231
+ if (target === "developer") {
3232
+ if (packageName === "@topolo/auth-client") return "^1.0.0";
3233
+ return "^0.1.0";
3234
+ }
3235
+ const folder = packageName === "@topolo/sdk" ? "topolo-sdk" : packageName.replace("@topolo/", "topolo-");
3236
+ return `file:../packages/${folder}`;
3237
+ }
3238
+ function readme(application, target, options) {
3239
+ const displayName = toTitle(application.name);
3240
+ const targetLabel = target === "platform" ? options.portfolio === "personal" ? "first-party Topolo personal application" : "first-party Topolo platform application" : options.audience === "personal" ? "third-party Topolo personal application" : "third-party Topolo business application";
3241
+ const lines = [
3242
+ `# ${displayName}`,
3243
+ "",
3244
+ `Generated ${targetLabel} scaffold for Topolo.`,
3245
+ "",
3246
+ `Audience: ${options.audience}`,
3247
+ `Tenancy: ${options.tenancy}`,
3248
+ ...target === "platform" && options.portfolio ? [`Portfolio: ${options.portfolio}`] : [],
3249
+ "",
3250
+ "## Development",
3251
+ "",
3252
+ "```bash",
3253
+ "npm install",
3254
+ "npm run typecheck",
3255
+ "npm run build",
3256
+ "npm run dev",
3257
+ "```",
3258
+ ""
3259
+ ];
3260
+ if (target === "platform") {
3261
+ lines.push(
3262
+ "## Shared Topolo Surface",
3263
+ "",
3264
+ "- `@topolo/ui-kit` provides the shared shell, launcher, account menu, command palette, and bug reporter.",
3265
+ "- `@topolo/auth-client` owns browser auth, callback exchange, token refresh, and logout.",
3266
+ "- Generated first-party browser apps start on the shared landing page at `/`, use the shared login page at `/login`, and complete sign-in on `/auth/callback` before entering the workspace shell.",
3267
+ "- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
3268
+ );
3269
+ if (options.api) {
3270
+ lines.push("- `@topolo/auth-middleware` validates Worker requests through Topolo Auth.");
3271
+ }
3272
+ } else {
3273
+ lines.push(
3274
+ "## Topolo Developer Surface",
3275
+ "",
3276
+ "- `topolo.app.json` declares callback URLs, requested scopes, and runtime shape for Topolo Developers.",
3277
+ "- Browser sign-in uses the public Topolo OAuth authorization-code + PKCE flow.",
3278
+ `- The generated app is seeded as a ${options.audience}/${options.tenancy} developer application.`,
3279
+ "- The starter intentionally avoids private first-party packages so it can install outside the Topolo monorepo."
3280
+ );
3281
+ }
3282
+ if (target === "platform") {
3283
+ lines.push(
3284
+ "",
3285
+ "## Platform Registration",
3286
+ "",
3287
+ "This scaffold includes CloudControl metadata, TopoloDocs system coverage, and a Topolo app conformance workflow."
3288
+ );
3289
+ } else {
3290
+ lines.push(
3291
+ "",
3292
+ "## Developer Registration",
3293
+ "",
3294
+ "`topolo.app.json` is the app manifest used to register callback URLs, requested scopes, and local development metadata with Topolo."
3295
+ );
3296
+ }
3297
+ return `${lines.join("\n")}
3298
+ `;
3299
+ }
3300
+ function tsconfigJson(api) {
3301
+ return `${json({
3302
+ compilerOptions: {
3303
+ target: "ES2022",
3304
+ useDefineForClassFields: true,
3305
+ lib: api ? ["ES2022", "DOM", "DOM.Iterable", "WebWorker"] : ["ES2022", "DOM", "DOM.Iterable"],
3306
+ allowJs: false,
3307
+ skipLibCheck: true,
3308
+ esModuleInterop: true,
3309
+ allowSyntheticDefaultImports: true,
3310
+ strict: true,
3311
+ forceConsistentCasingInFileNames: true,
3312
+ module: "ESNext",
3313
+ moduleResolution: "Bundler",
3314
+ resolveJsonModule: true,
3315
+ isolatedModules: true,
3316
+ noEmit: true,
3317
+ jsx: "react-jsx",
3318
+ types: ["vite/client"]
3319
+ },
3320
+ include: ["src"],
3321
+ references: []
3322
+ })}
3323
+ `;
3324
+ }
3325
+ function gitignore() {
3326
+ return [
3327
+ "node_modules/",
3328
+ "dist/",
3329
+ ".wrangler/",
3330
+ ".env",
3331
+ ".env.*",
3332
+ "!.env.example",
3333
+ ""
3334
+ ].join("\n");
3335
+ }
3336
+ function envDts(api) {
3337
+ const lines = [
3338
+ '/// <reference types="vite/client" />',
3339
+ "",
3340
+ "interface ImportMetaEnv {",
3341
+ " readonly VITE_TOPOLO_AUTH_URL?: string;",
3342
+ " readonly VITE_TOPOLO_CLIENT_ID?: string;",
3343
+ " readonly VITE_TOPOLO_REDIRECT_URI?: string;",
3344
+ " readonly VITE_TOPOLO_SCOPES?: string;",
3345
+ " readonly VITE_TOPOLO_SERVICE_ID?: string;"
3346
+ ];
3347
+ if (api) {
3348
+ lines.push(" readonly VITE_TOPOLO_API_URL?: string;");
3349
+ }
3350
+ lines.push(
3351
+ "}",
3352
+ "",
3353
+ "interface ImportMeta {",
3354
+ " readonly env: ImportMetaEnv;",
3355
+ "}",
3356
+ ""
3357
+ );
3358
+ return lines.join("\n");
3359
+ }
3360
+ function viteConfig() {
3361
+ return [
3362
+ "import { defineConfig } from 'vite';",
3363
+ "import react from '@vitejs/plugin-react';",
3364
+ "",
3365
+ "export default defineConfig({",
3366
+ " plugins: [react()],",
3367
+ " resolve: {",
3368
+ " dedupe: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],",
3369
+ " },",
3370
+ " build: { outDir: 'dist' },",
3371
+ "});",
3372
+ ""
3373
+ ].join("\n");
3374
+ }
3375
+ function indexHtml(name) {
3376
+ return [
3377
+ "<!doctype html>",
3378
+ '<html lang="en">',
3379
+ " <head>",
3380
+ ' <meta charset="UTF-8" />',
3381
+ ' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
3382
+ ` <title>${escapeHtml(toTitle(name))}</title>`,
3383
+ " </head>",
3384
+ " <body>",
3385
+ ' <div id="root"></div>',
3386
+ ' <script type="module" src="/src/main.tsx"></script>',
3387
+ " </body>",
3388
+ "</html>",
3389
+ ""
3390
+ ].join("\n");
3391
+ }
3392
+ function mainTsx() {
3393
+ return [
3394
+ "import React from 'react';",
3395
+ "import { createRoot } from 'react-dom/client';",
3396
+ "import App from './App';",
3397
+ "import './styles.css';",
3398
+ "",
3399
+ "const root = document.getElementById('root');",
3400
+ "",
3401
+ "if (!root) {",
3402
+ " throw new Error('Missing #root element.');",
3403
+ "}",
3404
+ "",
3405
+ "createRoot(root).render(",
3406
+ " <React.StrictMode>",
3407
+ " <App />",
3408
+ " </React.StrictMode>,",
3409
+ ");",
3410
+ ""
3411
+ ].join("\n");
3412
+ }
3413
+ function stylesCss() {
3414
+ return [
3415
+ ":root {",
3416
+ " color-scheme: light dark;",
3417
+ " font-family:",
3418
+ ' Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;',
3419
+ " background: #f7f8fb;",
3420
+ " color: #182032;",
3421
+ "}",
3422
+ "",
3423
+ "* {",
3424
+ " box-sizing: border-box;",
3425
+ "}",
3426
+ "",
3427
+ "body {",
3428
+ " margin: 0;",
3429
+ " background: #f7f8fb;",
3430
+ "}",
3431
+ "",
3432
+ "button,",
3433
+ "input,",
3434
+ "select,",
3435
+ "textarea {",
3436
+ " font: inherit;",
3437
+ "}",
3438
+ "",
3439
+ ".topolo-starter-workspace,",
3440
+ ".topolo-starter-card-stack {",
3441
+ " display: grid;",
3442
+ " gap: 18px;",
3443
+ "}",
3444
+ "",
3445
+ ".topolo-starter-card h2 {",
3446
+ " margin: 0 0 12px;",
3447
+ " font-size: 24px;",
3448
+ " line-height: 1.2;",
3449
+ "}",
3450
+ "",
3451
+ ".topolo-starter-card p {",
3452
+ " margin: 0;",
3453
+ " color: #526071;",
3454
+ " line-height: 1.55;",
3455
+ "}",
3456
+ "",
3457
+ ".topolo-starter-card {",
3458
+ " border: 1px solid rgba(24, 32, 50, 0.12);",
3459
+ " border-radius: 8px;",
3460
+ " padding: 20px;",
3461
+ " background: #ffffff;",
3462
+ "}",
3463
+ ""
3464
+ ].join("\n");
3465
+ }
3466
+ function appTsx(application, target, options) {
3467
+ if (target === "developer") return developerAppTsx(application);
3468
+ return platformAppTsx(application, options);
3469
+ }
3470
+ function platformAppTsx(application, options) {
3471
+ const appName = toTitle(application.name);
3472
+ const serviceId = application.serviceId ?? application.id;
3473
+ const tagline = options.audience === "personal" ? "Topolo personal app" : "Topolo workspace";
3474
+ const workspaceTitle = options.audience === "personal" ? "Personal app" : "Workspace";
3475
+ return [
3476
+ "import { useEffect, useMemo, useState } from 'react';",
3477
+ "import { createBrowserAuthModule, type AuthUser } from '@topolo/auth-client';",
3478
+ "import { LandingPage, LoginPage, TopoloAppShell, TopoloAuthFlowScreen, TopoloLoadingScreen, type TopoloAppShellUser } from '@topolo/ui-kit';",
3479
+ "",
3480
+ "type NavItem = 'home';",
3481
+ "type AppRoute = { kind: 'root' } | { kind: 'login' } | { kind: 'callback' };",
3482
+ "",
3483
+ `const APP_ID = '${application.id}';`,
3484
+ `const APP_NAME = '${escapeSingleQuoted(appName)}';`,
3485
+ `const DEFAULT_SERVICE_ID = '${serviceId}';`,
3486
+ "const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? 'https://auth.topolo.app';",
3487
+ "const SERVICE_ID = import.meta.env.VITE_TOPOLO_SERVICE_ID ?? DEFAULT_SERVICE_ID;",
3488
+ "const LANDING_CONFIG = {",
3489
+ " app_name: APP_NAME,",
3490
+ ` app_tagline: '${escapeSingleQuoted(tagline)}',`,
3491
+ " hero_title: `Open ${APP_NAME} in Topolo`,",
3492
+ " hero_subtitle: 'Shared landing, login, callback, auth, and workspace shell wiring are already included in the generated baseline.',",
3493
+ " hero_cta_text: 'Sign in',",
3494
+ " hero_cta_url: '/login',",
3495
+ " cta_title: 'Start from the shared platform entry flow',",
3496
+ " cta_subtitle: 'Unauthenticated routes stay on the shared Topolo surfaces before users enter the signed workspace shell.',",
3497
+ "};",
3498
+ "const LOGIN_CONFIG = {",
3499
+ " app_name: APP_NAME,",
3500
+ ` app_tagline: '${escapeSingleQuoted(tagline)}',`,
3501
+ " hero_title: `Sign in to ${APP_NAME}`,",
3502
+ " hero_subtitle: 'Use Topolo Auth to open the shared workspace shell for this application.',",
3503
+ " authenticated_home_path: '/',",
3504
+ "};",
3505
+ "",
3506
+ "const auth = createBrowserAuthModule({",
3507
+ " authServiceUrl: AUTH_URL,",
3508
+ " serviceId: SERVICE_ID,",
3509
+ " devSessionKey: `${SERVICE_ID}_session`,",
3510
+ " devSessionStorageEnabled: import.meta.env.DEV,",
3511
+ " callbackPath: '/auth/callback',",
3512
+ " debug: import.meta.env.DEV,",
3513
+ "});",
3514
+ "",
3515
+ "function getAccessToken(): string | null {",
3516
+ " return auth.accessTokenMemory.get() ?? auth.accessTokenSession.get();",
3517
+ "}",
3518
+ "",
3519
+ "function toShellUser(user: AuthUser | null): TopoloAppShellUser | null {",
3520
+ " if (!user) return null;",
3521
+ " return {",
3522
+ " name: user.name ?? user.email,",
3523
+ " email: user.email,",
3524
+ " role: user.role,",
3525
+ " organizationName: user.organization?.name ?? user.orgSlug ?? null,",
3526
+ " organizationId: user.organization?.id ?? user.orgId ?? null,",
3527
+ " };",
3528
+ "}",
3529
+ "",
3530
+ "export default function App() {",
3531
+ " const route = getRoute();",
3532
+ " const [user, setUser] = useState<AuthUser | null>(null);",
3533
+ " const [loading, setLoading] = useState(true);",
3534
+ " const [authError, setAuthError] = useState('');",
3535
+ " const [loginAttempted, setLoginAttempted] = useState(false);",
3536
+ "",
3537
+ " useEffect(() => {",
3538
+ " let active = true;",
3539
+ " const syncAuth = async () => {",
3540
+ " setLoading(true);",
3541
+ " setAuthError('');",
3542
+ "",
3543
+ " if (route.kind === 'callback') {",
3544
+ " const result = await auth.authClient.handleAuthCallbackAsync(window.location.href);",
3545
+ " if (!active) return;",
3546
+ " if (!result.authenticated) {",
3547
+ " setAuthError(result.error || 'Authentication failed.');",
3548
+ " setLoading(false);",
3549
+ " return;",
3550
+ " }",
3551
+ " setUser(result.user ?? null);",
3552
+ " window.location.replace('/');",
3553
+ " return;",
3554
+ " }",
3555
+ "",
3556
+ " const nextUser = await auth.authClient.initialize();",
3557
+ " if (!active) return;",
3558
+ " setUser(nextUser);",
3559
+ " setLoading(false);",
3560
+ " };",
3561
+ "",
3562
+ " void syncAuth().catch((error) => {",
3563
+ " if (!active) return;",
3564
+ " setAuthError(errorMessage(error));",
3565
+ " setLoading(false);",
3566
+ " });",
3567
+ " return () => {",
3568
+ " active = false;",
3569
+ " };",
3570
+ " }, [route.kind]);",
3571
+ "",
3572
+ " useEffect(() => {",
3573
+ " if (!loading && user && route.kind === 'login') {",
3574
+ " window.location.replace('/');",
3575
+ " }",
3576
+ " }, [loading, route.kind, user]);",
3577
+ "",
3578
+ " const shellUser = useMemo(() => toShellUser(user), [user]);",
3579
+ "",
3580
+ " if (route.kind === 'callback') {",
3581
+ " if (loading) {",
3582
+ " return (",
3583
+ " <TopoloLoadingScreen",
3584
+ " appId={APP_ID}",
3585
+ " serviceId={SERVICE_ID}",
3586
+ " appName={APP_NAME}",
3587
+ ' subtitle="Completing secure sign-in..."',
3588
+ " />",
3589
+ " );",
3590
+ " }",
3591
+ "",
3592
+ " return (",
3593
+ " <TopoloAuthFlowScreen",
3594
+ " appName={APP_NAME}",
3595
+ " appId={APP_ID}",
3596
+ " serviceId={SERVICE_ID}",
3597
+ ' status="error"',
3598
+ ' title="Authentication failed"',
3599
+ " message={authError || `We could not complete sign-in for ${APP_NAME}.`}",
3600
+ ' actionLabel="Back to sign in"',
3601
+ " onAction={() => window.location.replace('/login')}",
3602
+ " />",
3603
+ " );",
3604
+ " }",
3605
+ "",
3606
+ " if (loading) {",
3607
+ " return (",
3608
+ " <TopoloLoadingScreen",
3609
+ " appId={APP_ID}",
3610
+ " serviceId={SERVICE_ID}",
3611
+ " appName={APP_NAME}",
3612
+ " subtitle={`Loading ${APP_NAME}...`}",
3613
+ " />",
3614
+ " );",
3615
+ " }",
3616
+ "",
3617
+ " if (!user) {",
3618
+ " if (route.kind === 'login') {",
3619
+ " return (",
3620
+ " <LoginPage",
3621
+ " serviceId={SERVICE_ID}",
3622
+ " authApiUrl={AUTH_URL}",
3623
+ " config={LOGIN_CONFIG}",
3624
+ " isLoading={loginAttempted}",
3625
+ " onLogin={async (provider) => {",
3626
+ " setLoginAttempted(true);",
3627
+ " try {",
3628
+ " await auth.authClient.login(provider || 'topolo');",
3629
+ " } catch {",
3630
+ " setLoginAttempted(false);",
3631
+ " }",
3632
+ " }}",
3633
+ " onPasswordLogin={async (request) => {",
3634
+ " const result = await auth.authClient.loginWithPassword(request);",
3635
+ " if (!result.authenticated) {",
3636
+ " return result;",
3637
+ " }",
3638
+ " setUser(result.user ?? null);",
3639
+ " window.location.replace('/');",
3640
+ " return undefined;",
3641
+ " }}",
3642
+ " />",
3643
+ " );",
3644
+ " }",
3645
+ "",
3646
+ " return (",
3647
+ " <LandingPage",
3648
+ " serviceId={SERVICE_ID}",
3649
+ " authApiUrl={AUTH_URL}",
3650
+ " config={LANDING_CONFIG}",
3651
+ " onCTAClick={() => window.location.assign('/login')}",
3652
+ " />",
3653
+ " );",
3654
+ " }",
3655
+ "",
3656
+ " if (route.kind === 'login') {",
3657
+ " return (",
3658
+ " <TopoloLoadingScreen",
3659
+ " appId={APP_ID}",
3660
+ " serviceId={SERVICE_ID}",
3661
+ " appName={APP_NAME}",
3662
+ ' subtitle="Opening your workspace..."',
3663
+ " />",
3664
+ " );",
3665
+ " }",
3666
+ "",
3667
+ " return (",
3668
+ " <TopoloAppShell<NavItem>",
3669
+ " appId={APP_ID}",
3670
+ " serviceId={SERVICE_ID}",
3671
+ " appName={APP_NAME}",
3672
+ " user={shellUser}",
3673
+ ' activeNavItemId="home"',
3674
+ ` navItems={[{ id: 'home', label: '${escapeSingleQuoted(workspaceTitle)}' }]}`,
3675
+ " onNavItemSelect={() => undefined}",
3676
+ " eyebrow={APP_NAME}",
3677
+ ` title="${escapeDoubleQuoted(workspaceTitle)}"`,
3678
+ " authUrl={AUTH_URL}",
3679
+ " getAccessToken={getAccessToken}",
3680
+ " onSignOut={async () => {",
3681
+ " await auth.authClient.logout();",
3682
+ " window.location.assign('/login');",
3683
+ " }}",
3684
+ " commandPlaceholder={`Search ${APP_NAME}`}",
3685
+ " bugReporterAppName={APP_NAME}",
3686
+ " enableCommandPalette",
3687
+ " enableBugReporter",
3688
+ " showThemeToggle",
3689
+ " >",
3690
+ ' <div className="topolo-starter-workspace">',
3691
+ ' <section className="topolo-starter-card">',
3692
+ " <h2>Shared workspace baseline</h2>",
3693
+ " <p>",
3694
+ " This scaffold starts on the shared landing and login surfaces, completes sign-in on the shared callback path, and renders the signed workspace inside the shared Topolo shell.",
3695
+ " </p>",
3696
+ " </section>",
3697
+ " </div>",
3698
+ " </TopoloAppShell>",
3699
+ " );",
3700
+ "}",
3701
+ "",
3702
+ "function getRoute(): AppRoute {",
3703
+ " const pathname = window.location.pathname.replace(/\\/+$/, '') || '/';",
3704
+ " if (pathname === '/login') return { kind: 'login' };",
3705
+ " if (pathname === '/auth/callback') return { kind: 'callback' };",
3706
+ " return { kind: 'root' };",
3707
+ "}",
3708
+ "",
3709
+ "function errorMessage(error: unknown): string {",
3710
+ " if (error instanceof Error && error.message) return error.message;",
3711
+ " return 'Unexpected authentication error.';",
3712
+ "}",
3713
+ ""
3714
+ ].join("\n");
3715
+ }
3716
+ function developerAppTsx(application) {
3717
+ const appName = toTitle(application.name);
3718
+ const defaultClientId = `${toSlug(application.id)}_local_client`;
3719
+ return [
3720
+ "import { useEffect, useMemo, useState } from 'react';",
3721
+ "",
3722
+ "type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';",
3723
+ "",
3724
+ `const APP_NAME = '${escapeSingleQuoted(appName)}';`,
3725
+ "const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? 'https://auth.topolo.app';",
3726
+ `const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';`,
3727
+ "const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || `${window.location.origin}/auth/callback`;",
3728
+ "const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'identity:read organization:read').trim();",
3729
+ "",
3730
+ "export default function App() {",
3731
+ " const [authState, setAuthState] = useState<AuthState>(() => CLIENT_ID ? 'ready' : 'missing_client');",
3732
+ " const [message, setMessage] = useState<string | null>(null);",
3733
+ " const [tokenPreview, setTokenPreview] = useState<string | null>(null);",
3734
+ "",
3735
+ " const callbackCode = useMemo(() => new URLSearchParams(window.location.search).get('code'), []);",
3736
+ " const callbackError = useMemo(() => new URLSearchParams(window.location.search).get('error'), []);",
3737
+ "",
3738
+ " useEffect(() => {",
3739
+ " if (callbackError) {",
3740
+ " setAuthState('error');",
3741
+ " setMessage(callbackError);",
3742
+ " return;",
3743
+ " }",
3744
+ " if (!callbackCode) return;",
3745
+ "",
3746
+ " setAuthState('authorizing');",
3747
+ " void exchangeAuthorizationCode(callbackCode)",
3748
+ " .then((tokens) => {",
3749
+ " sessionStorage.setItem(`${CLIENT_ID}:topolo_access_token`, tokens.access_token);",
3750
+ " if (tokens.refresh_token) {",
3751
+ " sessionStorage.setItem(`${CLIENT_ID}:topolo_refresh_token`, tokens.refresh_token);",
3752
+ " }",
3753
+ " setTokenPreview(`${tokens.access_token.slice(0, 18)}...`);",
3754
+ " setAuthState('authenticated');",
3755
+ " window.history.replaceState({}, document.title, '/');",
3756
+ " })",
3757
+ " .catch((error: Error) => {",
3758
+ " setAuthState('error');",
3759
+ " setMessage(error.message);",
3760
+ " });",
3761
+ " }, [callbackCode, callbackError]);",
3762
+ "",
3763
+ " return (",
3764
+ ' <main className="topolo-starter-login">',
3765
+ ' <section className="topolo-starter-login__panel">',
3766
+ " <h1>{APP_NAME}</h1>",
3767
+ " <p>Public Topolo OAuth starter using authorization-code + PKCE.</p>",
3768
+ " {authState === 'missing_client' && (",
3769
+ " <p>Set VITE_TOPOLO_CLIENT_ID after registering this app in Topolo Developers.</p>",
3770
+ " )}",
3771
+ " {authState === 'authenticated' && (",
3772
+ " <p>Topolo sign-in complete. Access token: {tokenPreview}</p>",
3773
+ " )}",
3774
+ " {authState === 'error' && <p>{message}</p>}",
3775
+ ` <button type="button" disabled={!CLIENT_ID || authState === 'authorizing'} onClick={() => void startTopoloLogin()}>`,
3776
+ " {authState === 'authorizing' ? 'Completing sign-in' : 'Sign in with Topolo'}",
3777
+ " </button>",
3778
+ " </section>",
3779
+ " </main>",
3780
+ " );",
3781
+ "}",
3782
+ "",
3783
+ "async function startTopoloLogin(): Promise<void> {",
3784
+ " const verifier = randomString(64);",
3785
+ " const challenge = await pkceChallenge(verifier);",
3786
+ " const state = randomString(32);",
3787
+ " sessionStorage.setItem(`${CLIENT_ID}:topolo_pkce_verifier`, verifier);",
3788
+ " sessionStorage.setItem(`${CLIENT_ID}:topolo_oauth_state`, state);",
3789
+ "",
3790
+ " const authorizeUrl = new URL('/api/developer-oauth/authorize', AUTH_URL);",
3791
+ " authorizeUrl.searchParams.set('response_type', 'code');",
3792
+ " authorizeUrl.searchParams.set('client_id', CLIENT_ID);",
3793
+ " authorizeUrl.searchParams.set('redirect_uri', REDIRECT_URI);",
3794
+ " authorizeUrl.searchParams.set('scope', REQUESTED_SCOPES);",
3795
+ " authorizeUrl.searchParams.set('state', state);",
3796
+ " authorizeUrl.searchParams.set('code_challenge', challenge);",
3797
+ " authorizeUrl.searchParams.set('code_challenge_method', 'S256');",
3798
+ " window.location.assign(authorizeUrl.toString());",
3799
+ "}",
3800
+ "",
3801
+ "async function exchangeAuthorizationCode(code: string): Promise<{ access_token: string; refresh_token?: string }> {",
3802
+ " const params = new URLSearchParams(window.location.search);",
3803
+ " const expectedState = sessionStorage.getItem(`${CLIENT_ID}:topolo_oauth_state`);",
3804
+ " if (expectedState && params.get('state') !== expectedState) {",
3805
+ " throw new Error('OAuth state mismatch. Restart sign-in.');",
3806
+ " }",
3807
+ " const verifier = sessionStorage.getItem(`${CLIENT_ID}:topolo_pkce_verifier`);",
3808
+ " if (!verifier) throw new Error('Missing PKCE verifier. Restart sign-in.');",
3809
+ "",
3810
+ " const tokenUrl = new URL('/api/developer-oauth/token', AUTH_URL);",
3811
+ " const response = await fetch(tokenUrl, {",
3812
+ " method: 'POST',",
3813
+ " headers: { 'Content-Type': 'application/json' },",
3814
+ " body: JSON.stringify({",
3815
+ " grant_type: 'authorization_code',",
3816
+ " client_id: CLIENT_ID,",
3817
+ " code,",
3818
+ " redirect_uri: REDIRECT_URI,",
3819
+ " code_verifier: verifier,",
3820
+ " }),",
3821
+ " });",
3822
+ " const payload = await response.json().catch(() => null);",
3823
+ " if (!response.ok || !payload?.access_token) {",
3824
+ " throw new Error(payload?.error_description || payload?.error || 'Token exchange failed.');",
3825
+ " }",
3826
+ " return payload;",
3827
+ "}",
3828
+ "",
3829
+ "function randomString(length: number): string {",
3830
+ " const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';",
3831
+ " const bytes = crypto.getRandomValues(new Uint8Array(length));",
3832
+ " return Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('');",
3833
+ "}",
3834
+ "",
3835
+ "async function pkceChallenge(verifier: string): Promise<string> {",
3836
+ " const data = new TextEncoder().encode(verifier);",
3837
+ " const digest = await crypto.subtle.digest('SHA-256', data);",
3838
+ " return base64Url(new Uint8Array(digest));",
3839
+ "}",
3840
+ "",
3841
+ "function base64Url(bytes: Uint8Array): string {",
3842
+ " let binary = '';",
3843
+ " for (const byte of bytes) binary += String.fromCharCode(byte);",
3844
+ " return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');",
3845
+ "}",
3846
+ ""
3847
+ ].join("\n");
3848
+ }
3849
+ function wranglerToml(application, target) {
3850
+ const slug = toSlug(application.id);
3851
+ const workerName = target === "platform" ? `topolo-${slug}` : application.packageName;
3852
+ const routeHost = application.productionUrl ? new URL(application.productionUrl).host : null;
3853
+ const lines = [
3854
+ `name = "${workerName}"`,
3855
+ 'main = "src/worker.ts"',
3856
+ `compatibility_date = "${CURRENT_VERIFICATION_DATE}"`,
3857
+ 'compatibility_flags = ["nodejs_compat"]'
3858
+ ];
3859
+ if (target === "platform") {
3860
+ lines.push(
3861
+ `account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
3862
+ "workers_dev = false"
3863
+ );
3864
+ }
3865
+ lines.push(
3866
+ "",
3867
+ "[observability]",
3868
+ "enabled = true",
3869
+ "",
3870
+ "[assets]",
3871
+ 'directory = "dist"',
3872
+ 'binding = "ASSETS"',
3873
+ 'not_found_handling = "single-page-application"',
3874
+ 'run_worker_first = ["/api/*"]'
3875
+ );
3876
+ if (target === "platform" && routeHost) {
3877
+ lines.push(
3878
+ "",
3879
+ "[[routes]]",
3880
+ `pattern = "${routeHost}"`,
3881
+ "custom_domain = true"
3882
+ );
3883
+ }
3884
+ lines.push(
3885
+ "",
3886
+ "[vars]",
3887
+ 'AUTH_BASE_URL = "https://auth.topolo.app"',
3888
+ `SERVICE_ID = "${application.serviceId ?? application.id}"`,
3889
+ ""
3890
+ );
3891
+ return lines.join("\n");
3892
+ }
3893
+ function workerTs(application, target, _options) {
3894
+ if (target === "developer") return developerWorkerTs(application);
3895
+ return platformWorkerTs(application);
3896
+ }
3897
+ function platformWorkerTs(application) {
3898
+ const serviceId = application.serviceId ?? application.id;
3899
+ return [
3900
+ "import { verifyRequest } from '@topolo/auth-middleware';",
3901
+ "",
3902
+ "export interface Env {",
3903
+ " AUTH_BASE_URL?: string;",
3904
+ " SERVICE_ID?: string;",
3905
+ " TOPOLO_SERVICE_API_KEY?: string;",
3906
+ "}",
3907
+ "",
3908
+ "const CLIENT_HEADER = 'topolo-worker-starter';",
3909
+ "",
3910
+ "export default {",
3911
+ " async fetch(request: Request, env: Env): Promise<Response> {",
3912
+ " const url = new URL(request.url);",
3913
+ "",
3914
+ " if (url.pathname === '/api/health') {",
3915
+ " return json({ ok: true, service: env.SERVICE_ID ?? DEFAULT_SERVICE_ID }, { status: 200 }, request);",
3916
+ " }",
3917
+ "",
3918
+ " const auth = await verifyRequest(request, {",
3919
+ " authBaseUrl: env.AUTH_BASE_URL,",
3920
+ " serviceId: env.SERVICE_ID ?? DEFAULT_SERVICE_ID,",
3921
+ " serviceApiKey: env.TOPOLO_SERVICE_API_KEY,",
3922
+ " });",
3923
+ "",
3924
+ " if (!auth.ok) {",
3925
+ " return json({ error: auth.error }, { status: auth.status }, request);",
3926
+ " }",
3927
+ "",
3928
+ " if (url.pathname === '/api/bootstrap') {",
3929
+ " return json({",
3930
+ " user: auth.user,",
3931
+ " organization: auth.organization,",
3932
+ " }, { status: 200 }, request);",
3933
+ " }",
3934
+ "",
3935
+ " return json({ error: 'not_found' }, { status: 404 }, request);",
3936
+ " },",
3937
+ "};",
3938
+ "",
3939
+ `const DEFAULT_SERVICE_ID = '${serviceId}';`,
3940
+ "",
3941
+ "function json(body: unknown, init: ResponseInit, request: Request): Response {",
3942
+ " const headers = new Headers(init.headers);",
3943
+ " headers.set('Content-Type', 'application/json');",
3944
+ " headers.set('X-Topolo-Client', CLIENT_HEADER);",
3945
+ " headers.set('X-Topolo-Request-Id', request.headers.get('X-Topolo-Request-Id') ?? crypto.randomUUID());",
3946
+ " return new Response(JSON.stringify(body), { ...init, headers });",
3947
+ "}",
3948
+ ""
3949
+ ].join("\n");
3950
+ }
3951
+ function developerWorkerTs(application) {
3952
+ const serviceId = application.serviceId ?? application.id;
3953
+ return [
3954
+ "export interface Env {",
3955
+ " AUTH_BASE_URL?: string;",
3956
+ " SERVICE_ID?: string;",
3957
+ "}",
3958
+ "",
3959
+ "const CLIENT_HEADER = 'topolo-developer-starter';",
3960
+ `const DEFAULT_SERVICE_ID = '${serviceId}';`,
3961
+ "",
3962
+ "export default {",
3963
+ " async fetch(request: Request, env: Env): Promise<Response> {",
3964
+ " const url = new URL(request.url);",
3965
+ "",
3966
+ " if (url.pathname === '/api/health') {",
3967
+ " return json({ ok: true, service: env.SERVICE_ID ?? DEFAULT_SERVICE_ID }, { status: 200 }, request);",
3968
+ " }",
3969
+ "",
3970
+ " const auth = await verifyTopoloRequest(request, env);",
3971
+ " if (!auth.ok) {",
3972
+ " return json({ error: auth.error }, { status: auth.status }, request);",
3973
+ " }",
3974
+ "",
3975
+ " if (url.pathname === '/api/bootstrap') {",
3976
+ " return json({",
3977
+ " user: auth.user,",
3978
+ " organization: auth.organization,",
3979
+ " }, { status: 200 }, request);",
3980
+ " }",
3981
+ "",
3982
+ " return json({ error: 'not_found' }, { status: 404 }, request);",
3983
+ " },",
3984
+ "};",
3985
+ "",
3986
+ "async function verifyTopoloRequest(request: Request, env: Env): Promise<",
3987
+ " | { ok: true; user: unknown; organization: unknown }",
3988
+ " | { ok: false; status: number; error: string }",
3989
+ "> {",
3990
+ " const authHeader = request.headers.get('Authorization') || '';",
3991
+ " const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;",
3992
+ " if (!token) return { ok: false, status: 401, error: 'Bearer token required' };",
3993
+ "",
3994
+ " try {",
3995
+ " const authBaseUrl = env.AUTH_BASE_URL || 'https://auth.topolo.app';",
3996
+ " const response = await fetch(`${authBaseUrl}/validate`, {",
3997
+ " method: 'POST',",
3998
+ " headers: {",
3999
+ " Authorization: `Bearer ${token}`,",
4000
+ " 'X-Service-ID': env.SERVICE_ID ?? DEFAULT_SERVICE_ID,",
4001
+ " },",
4002
+ " });",
4003
+ "",
4004
+ " const payload = await response.json().catch(() => null);",
4005
+ " if (!response.ok) return { ok: false, status: 401, error: 'Token validation failed' };",
4006
+ " const user = payload?.data?.user;",
4007
+ " if (!user) return { ok: false, status: 401, error: 'Invalid auth response' };",
4008
+ " return { ok: true, user, organization: payload?.data?.organization ?? null };",
4009
+ " } catch {",
4010
+ " return { ok: false, status: 503, error: 'Topolo Auth unavailable' };",
4011
+ " }",
4012
+ "}",
4013
+ "",
4014
+ "function json(body: unknown, init: ResponseInit, request: Request): Response {",
4015
+ " const headers = new Headers(init.headers);",
4016
+ " headers.set('Content-Type', 'application/json');",
4017
+ " headers.set('X-Topolo-Client', CLIENT_HEADER);",
4018
+ " headers.set('X-Topolo-Request-Id', request.headers.get('X-Topolo-Request-Id') ?? crypto.randomUUID());",
4019
+ " return new Response(JSON.stringify(body), { ...init, headers });",
4020
+ "}",
4021
+ ""
4022
+ ].join("\n");
4023
+ }
4024
+ function cloudcontrolJson(application, options) {
4025
+ const slug = toSlug(application.id);
4026
+ const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
4027
+ const deployTargets = options.api ? [
4028
+ {
4029
+ kind: "worker",
4030
+ environment: "production",
4031
+ target_name: `topolo-${slug}`,
4032
+ cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
4033
+ config_path: "wrangler.toml",
4034
+ build_command: "npm run build",
4035
+ output_dir: "dist",
4036
+ production_branch: "main",
4037
+ healthcheck_path: "/api/health",
4038
+ domains: [host],
4039
+ routes: [host],
4040
+ api_url: application.productionUrl,
4041
+ service_id: application.serviceId
4042
+ }
4043
+ ] : [
4044
+ {
4045
+ kind: "pages",
4046
+ environment: "production",
4047
+ target_name: `topolo-${slug}`,
4048
+ cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
4049
+ config_path: null,
4050
+ build_command: "npm run build",
4051
+ output_dir: "dist",
4052
+ production_branch: "main",
4053
+ healthcheck_path: null,
4054
+ domains: [host],
4055
+ routes: [host],
4056
+ api_url: null
4057
+ }
4058
+ ];
4059
+ return `${json({
4060
+ name: `topolo-${slug}`,
4061
+ description: `Canonical documentation for ${toTitle(application.name)} lives in PlatformApplications/TopoloDocs.`,
4062
+ deploy_method: options.api ? "wrangler" : "cloudflare_pages",
4063
+ ci_provider: "github_actions",
4064
+ repo_identity: {
4065
+ provider: "github",
4066
+ owner: "Topolo-io",
4067
+ repo: `topolo-${slug}`,
4068
+ remote_url: `git@github.com:Topolo-io/topolo-${slug}.git`,
4069
+ default_branch: "main"
4070
+ },
4071
+ environments: application.productionUrl ? [
4072
+ {
4073
+ name: "production",
4074
+ url: application.productionUrl,
4075
+ branch: "main",
4076
+ status: "unknown"
4077
+ }
4078
+ ] : [],
4079
+ deploy_targets: deployTargets,
4080
+ resource_bindings: [],
4081
+ ci_workflows: [
4082
+ {
4083
+ provider: "github_actions",
4084
+ workflow_name: "topolo-conformance.yml",
4085
+ path: ".github/workflows/topolo-conformance.yml",
4086
+ branch: null,
4087
+ required: true
4088
+ }
4089
+ ],
4090
+ ci_targets: [],
4091
+ tags: ["application", "cloudflare", "typescript"],
4092
+ cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID
4093
+ })}
4094
+ `;
4095
+ }
4096
+ function systemJson(application, options) {
4097
+ const slug = toSlug(application.id);
4098
+ const suiteLabel = options.audience === "personal" ? "personal" : "platform";
4099
+ const summaryLabel = options.audience === "personal" ? "first-party Topolo personal application" : "first-party Topolo platform application";
4100
+ return `${json({
4101
+ id: `topolo-${slug}`,
4102
+ name: toTitle(application.name),
4103
+ slug: `topolo-${slug}`,
4104
+ kind: "application",
4105
+ summary: `${toTitle(application.name)} is a ${summaryLabel} scaffolded on shared Auth, UI Kit, CloudControl, and conformance contracts.`,
4106
+ lifecycle: "planned",
4107
+ last_verified: CURRENT_VERIFICATION_DATE,
4108
+ owners: ["platform-engineering"],
4109
+ repo_paths: [`PlatformApplications/${application.appDir}`],
4110
+ service_ids: options.api && application.serviceId ? [application.serviceId] : [],
4111
+ visibility: "internal",
4112
+ api_contract: {
4113
+ type: options.api ? "worker" : "none",
4114
+ source: options.api ? `PlatformApplications/${application.appDir}/src/worker.ts` : `PlatformApplications/${application.appDir}/src/App.tsx`,
4115
+ notes: options.api ? "Protected Worker routes validate Topolo Auth credentials and derive organization context from the credential. The browser baseline starts on shared landing and login routes, completes auth on /auth/callback, and dedupes React when linked Topolo packages are installed locally." : "Browser-only application surface using shared Topolo Auth landing/login/callback routes, shared shell packages, and React runtime dedupe."
4116
+ },
4117
+ primary_hosts: application.productionUrl ? [application.productionUrl] : [],
4118
+ doc_paths: [`internal/apps/${slug}`, `public/applications/${slug}`],
4119
+ security_assurance: {
4120
+ risk_tier: "internal",
4121
+ auth_boundary: "Topolo Auth through @topolo/auth-client for browser sessions and @topolo/auth-middleware for Worker requests.",
4122
+ tenant_isolation: options.tenancy === "household" ? "household_scoped" : "organization_scoped",
4123
+ external_inputs: ["browser", ...options.api ? ["api"] : []],
4124
+ sensitive_data: ["identity", "org_data"],
4125
+ security_review_status: "not_started",
4126
+ last_security_review: CURRENT_VERIFICATION_DATE,
4127
+ pentest_status: "not_started",
4128
+ evidence_doc: `internal/apps/${slug}`
4129
+ },
4130
+ dependencies: options.audience === "personal" ? ["topolo-auth"] : ["topolo-auth", "topolo-platform"],
4131
+ tags: ["application", "cloudflare", "typescript", suiteLabel]
4132
+ })}
4133
+ `;
4134
+ }
4135
+ function internalDocs(application, options) {
4136
+ const slug = toSlug(application.id);
4137
+ const title = toTitle(application.name);
4138
+ const suiteLabel = options.audience === "personal" ? "personal" : "platform";
4139
+ const tenantLabel = options.tenancy === "household" ? "household" : "organization";
4140
+ return [
4141
+ "---",
4142
+ `title: "${escapeFrontmatter(title)} Handbook"`,
4143
+ `summary: "Internal handbook for ${escapeFrontmatter(title)}."`,
4144
+ 'owners: ["platform-engineering"]',
4145
+ 'status: "planned"',
4146
+ `last_verified: "${CURRENT_VERIFICATION_DATE}"`,
4147
+ "source_repos:",
4148
+ ` - "PlatformApplications/${application.appDir}"`,
4149
+ ' - "PlatformApplications/TopoloDocs"',
4150
+ 'audience: "internal"',
4151
+ "tags:",
4152
+ ` - "${slug}"`,
4153
+ ' - "application"',
4154
+ "system_ids:",
4155
+ ` - "topolo-${slug}"`,
4156
+ ' - "topolo-auth"',
4157
+ "related_docs:",
4158
+ ` - "/systems/topolo-${slug}"`,
4159
+ ` - "/applications/${slug}"`,
4160
+ "---",
4161
+ "",
4162
+ "## What It Is",
4163
+ "",
4164
+ `${title} is a first-party Topolo ${suiteLabel} application scaffolded from the shared first-party application contract.`,
4165
+ "",
4166
+ "## Architecture",
4167
+ "",
4168
+ "The generated browser surface uses `@topolo/ui-kit` for the shared landing page, shared login page, auth callback state, and shared signed shell. `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve a single browser React runtime.",
4169
+ "",
4170
+ "## Runtime Surfaces",
4171
+ "",
4172
+ `- \`PlatformApplications/${application.appDir}/src/App.tsx\``,
4173
+ "- public landing route: `/`",
4174
+ "- public sign-in route: `/login`",
4175
+ "- auth callback route: `/auth/callback`",
4176
+ ...options.api ? [`- \`PlatformApplications/${application.appDir}/src/worker.ts\``] : [],
4177
+ `- \`PlatformApplications/${application.appDir}/topolo.cloudcontrol.json\``,
4178
+ ...options.api && application.serviceId ? [`- \`PlatformApplications/TopoloAuth/catalog/service-manifests/${application.serviceId}.json\``] : [],
4179
+ "",
4180
+ "## API Reference",
4181
+ "",
4182
+ options.api ? "The generated API baseline exposes `/api/health` and `/api/bootstrap`. `/api/bootstrap` requires Topolo Auth validation and returns the verified user plus organization context." : "This generated baseline is browser-only and does not expose a callable API service.",
4183
+ "",
4184
+ "## Auth and Permissions",
4185
+ "",
4186
+ options.api ? `Browser sessions and Worker requests must be validated through Topolo Auth under service id \`${application.serviceId}\`. Worker routes derive organization context from the verified credential.` : "Browser sessions must be established through Topolo Auth and rendered inside the shared Topolo shell.",
4187
+ "",
4188
+ "## Data Ownership",
4189
+ "",
4190
+ `The generated scaffold stores no durable application data yet. Future domain tables, Durable Objects, queues, KV namespaces, R2 buckets, and external provider records must be scoped to the authenticated ${tenantLabel} context at the backend boundary.`,
4191
+ "",
4192
+ "## Deployments",
4193
+ "",
4194
+ application.productionUrl ? `The planned production host is \`${application.productionUrl}\`. Deployment metadata starts in \`topolo.cloudcontrol.json\` and must stay aligned with CloudControl before live release.` : "No production host has been assigned yet. Deployment metadata must be completed before live release.",
4195
+ "",
4196
+ "## Failure Modes",
4197
+ "",
4198
+ "- Auth service registration drifts from the generated Worker `SERVICE_ID`.",
4199
+ "- CloudControl metadata does not match the eventual Cloudflare project or route.",
4200
+ "- linked Topolo packages resolve a second React runtime and the browser shell renders blank instead of mounting the shared surfaces.",
4201
+ "- Domain data is added without an organization-scoped backend boundary.",
4202
+ "",
4203
+ "## Debugging",
4204
+ "",
4205
+ "- run `npm run typecheck` and `npm run build` in the application directory",
4206
+ "- run `topolo apps audit <app> --local --repo-root /Users/ashes/Projects/topolo --fail-on needs_review --json` after SDK catalog registration",
4207
+ "- inspect `topolo.cloudcontrol.json` before deployment changes",
4208
+ ...options.api && application.serviceId ? [`- inspect \`PlatformApplications/TopoloAuth/catalog/service-manifests/${application.serviceId}.json\` when permission grants fail`] : [],
4209
+ "",
4210
+ "## Change Log / Verification",
4211
+ "",
4212
+ `- Scaffold generated on ${CURRENT_VERIFICATION_DATE} from \`${options.portfolio ? "topolo platform apps scaffold" : "topolo apps scaffold"}\`.`,
4213
+ "- `npm run typecheck`",
4214
+ "- `npm run build`",
4215
+ "- `npm run validate` in TopoloDocs",
4216
+ ""
4217
+ ].join("\n");
4218
+ }
4219
+ function publicDocs(application, options) {
4220
+ const slug = toSlug(application.id);
4221
+ const title = toTitle(application.name);
4222
+ const suiteLabel = options.audience === "personal" ? "personal" : "business";
4223
+ const tenantLabel = options.tenancy === "household" ? "household" : "organization";
4224
+ return [
4225
+ "---",
4226
+ `title: "${escapeFrontmatter(title)}"`,
4227
+ `summary: "Public overview of ${escapeFrontmatter(title)} in the Topolo application suite."`,
4228
+ 'owners: ["platform-engineering"]',
4229
+ 'status: "planned"',
4230
+ `last_verified: "${CURRENT_VERIFICATION_DATE}"`,
4231
+ "source_repos:",
4232
+ ` - "PlatformApplications/${application.appDir}"`,
4233
+ 'audience: "public"',
4234
+ "tags:",
4235
+ ` - "${slug}"`,
4236
+ ' - "application"',
4237
+ "system_ids:",
4238
+ ` - "topolo-${slug}"`,
4239
+ ' - "topolo-auth"',
4240
+ "related_docs:",
4241
+ ` - "/systems/topolo-${slug}"`,
4242
+ ` - "/internal/apps/${slug}"`,
4243
+ "---",
4244
+ "",
4245
+ "## What It Is",
4246
+ "",
4247
+ `${title} is part of the Topolo ${suiteLabel} application suite.`,
4248
+ "",
4249
+ "## Architecture",
4250
+ "",
4251
+ "The application uses the shared Topolo landing page, shared login page, auth callback handling, and shared Topolo shell from the outset.",
4252
+ "",
4253
+ "## Runtime Surfaces",
4254
+ "",
4255
+ application.productionUrl ? `The planned production host is \`${application.productionUrl}\`, with public entry at \`/\`, sign-in at \`/login\`, and callback completion at \`/auth/callback\`.` : "The production host has not been assigned yet.",
4256
+ "",
4257
+ "## API Reference",
4258
+ "",
4259
+ options.api ? "The generated API baseline exposes authenticated workspace routes under `/api/*` and validates requests through Topolo Auth." : "This generated baseline is browser-only and does not expose a callable API service.",
4260
+ "",
4261
+ "## Auth and Permissions",
4262
+ "",
4263
+ options.api && application.serviceId ? `Signed browser and API requests use Topolo Auth through service id \`${application.serviceId}\`.` : "Signed browser requests use Topolo Auth and the shared Topolo shell.",
4264
+ "",
4265
+ "## Data Ownership",
4266
+ "",
4267
+ `The generated baseline has no durable domain data yet. Future data must be ${tenantLabel}-scoped at the backend boundary.`,
4268
+ "",
4269
+ "## Deployments",
4270
+ "",
4271
+ application.productionUrl ? `The planned production host is \`${application.productionUrl}\`.` : "The production host has not been assigned yet.",
4272
+ "",
4273
+ "## Failure Modes",
4274
+ "",
4275
+ "- service registration and app routing can drift while the product is still planned",
4276
+ "- organization-scoped data models are not implemented until domain work begins",
4277
+ "",
4278
+ "## Debugging",
4279
+ "",
4280
+ "- use the matching internal handbook for operational details",
4281
+ "- verify the system registry entry before deployment or service-registration changes",
4282
+ "",
4283
+ "## Change Log / Verification",
4284
+ "",
4285
+ `- Scaffold generated on ${CURRENT_VERIFICATION_DATE} from \`topolo apps scaffold\`.`,
4286
+ ""
4287
+ ].join("\n");
4288
+ }
4289
+ function serviceManifestJson(application, options) {
4290
+ const slug = toSlug(application.id);
4291
+ const title = toTitle(application.name);
4292
+ const permissionPrefix = application.id.replace(/[^a-z0-9_]/g, "_");
4293
+ const resourceLabel = options.tenancy === "household" ? "household" : "workspace";
4294
+ return `${json({
4295
+ serviceId: application.serviceId,
4296
+ systems: [
4297
+ {
4298
+ systemId: `topolo-${slug}`,
4299
+ name: title,
4300
+ repoPaths: [`PlatformApplications/${application.appDir}`],
4301
+ docPaths: [`applications/${slug}`, `internal/apps/${slug}`]
4302
+ }
4303
+ ],
4304
+ permissions: [
4305
+ {
4306
+ id: `perm_${permissionPrefix}_workspace_read`,
4307
+ name: "workspace:read",
4308
+ description: `View ${title} ${resourceLabel} data`,
4309
+ resourcePattern: null
4310
+ },
4311
+ {
4312
+ id: `perm_${permissionPrefix}_workspace_write`,
4313
+ name: "workspace:write",
4314
+ description: `Manage ${title} ${resourceLabel} data`,
4315
+ resourcePattern: null
4316
+ }
4317
+ ],
4318
+ apiKeyScopes: [
4319
+ {
4320
+ id: `aks_${permissionPrefix}_workspace_read`,
4321
+ name: "workspace.read",
4322
+ description: `View ${title} ${resourceLabel} data`,
4323
+ resourcePattern: null
4324
+ },
4325
+ {
4326
+ id: `aks_${permissionPrefix}_workspace_write`,
4327
+ name: "workspace.write",
4328
+ description: `Manage ${title} ${resourceLabel} data`,
4329
+ resourcePattern: null
4330
+ }
4331
+ ],
4332
+ roleBundles: {
4333
+ admin: ["workspace:read", "workspace:write"],
4334
+ member: ["workspace:read", "workspace:write"],
4335
+ guest: ["workspace:read"],
4336
+ super_admin: ["workspace:read", "workspace:write"]
4337
+ }
4338
+ })}
4339
+ `;
4340
+ }
4341
+ function developerManifest(application, options) {
4342
+ return `${json({
4343
+ schema: "https://topolo.app/schemas/topolo-app-manifest.v1.json",
4344
+ id: application.id,
4345
+ name: toTitle(application.name),
4346
+ type: "third_party",
4347
+ audience: options.audience,
4348
+ tenancy: options.tenancy,
4349
+ distribution_model: options.distributionModel,
4350
+ surface_model: options.surfaceModel,
4351
+ container_app_id: options.containerAppId,
4352
+ package_name: application.packageName,
4353
+ service_id: options.api ? application.serviceId : null,
4354
+ auth: {
4355
+ provider: "topolo",
4356
+ flow: "authorization_code_pkce",
4357
+ callback_path: "/auth/callback",
4358
+ local_callback_url: "http://localhost:5173/auth/callback",
4359
+ production_callback_url: application.productionUrl ? `${application.productionUrl.replace(/\/$/, "")}/auth/callback` : null,
4360
+ requested_scopes: options.api ? ["identity:read", "organization:read", `${application.id}:read`] : ["identity:read", "organization:read"]
4361
+ },
4362
+ runtime: {
4363
+ browser: true,
4364
+ api: options.api
4365
+ }
4366
+ })}
4367
+ `;
4368
+ }
4369
+ function envExample(application) {
4370
+ return [
4371
+ "VITE_TOPOLO_AUTH_URL=https://auth.topolo.app",
4372
+ "VITE_TOPOLO_CLIENT_ID=",
4373
+ "VITE_TOPOLO_REDIRECT_URI=http://localhost:5173/auth/callback",
4374
+ "VITE_TOPOLO_SCOPES=identity:read organization:read",
4375
+ `VITE_TOPOLO_SERVICE_ID=${application.serviceId ?? application.id}`,
4376
+ ""
4377
+ ].join("\n");
4378
+ }
4379
+ function normalizeTarget(value) {
4380
+ if (!value) return "platform";
4381
+ if (value === "platform" || value === "developer") return value;
4382
+ throw new Error("Invalid scaffold target. Use platform or developer.");
4383
+ }
4384
+ function normalizePortfolio(value) {
4385
+ if (!value) return "platform";
4386
+ if (value === "platform" || value === "personal") return value;
4387
+ throw new Error("Invalid scaffold portfolio. Use platform or personal.");
4388
+ }
4389
+ function normalizeAudience(value, portfolio) {
4390
+ if (!value) {
4391
+ return portfolio === "personal" ? "personal" : "business";
4392
+ }
4393
+ if (value === "business" || value === "personal") {
4394
+ return value;
4395
+ }
4396
+ throw new Error("Invalid scaffold audience. Use business or personal.");
4397
+ }
4398
+ function normalizeTenancy(value, audience) {
4399
+ const resolved = value ?? (audience === "personal" ? "household" : "organization");
4400
+ if (resolved !== "organization" && resolved !== "household") {
4401
+ throw new Error("Invalid scaffold tenancy. Use organization or household.");
4402
+ }
4403
+ if (audience === "business" && resolved !== "organization") {
4404
+ throw new Error("Business scaffolds must use organization tenancy.");
4405
+ }
4406
+ if (audience === "personal" && resolved !== "household") {
4407
+ throw new Error("Personal scaffolds must use household tenancy.");
4408
+ }
4409
+ return resolved;
4410
+ }
4411
+ function normalizeSurfaceModel(value) {
4412
+ if (!value) return "web_app";
4413
+ if (value === "web_app" || value === "mobile_container" || value === "mobile_module" || value === "messaging_surface") {
4414
+ return value;
4415
+ }
4416
+ throw new Error(
4417
+ "Invalid scaffold surface model. Use web_app, mobile_container, mobile_module, or messaging_surface."
4418
+ );
4419
+ }
4420
+ function normalizeContainerAppId(value, surfaceModel) {
4421
+ const trimmed = String(value || "").trim();
4422
+ if (surfaceModel === "mobile_module") {
4423
+ if (!trimmed) {
4424
+ throw new Error("Mobile-module scaffolds require --container-app-id.");
4425
+ }
4426
+ return trimmed;
4427
+ }
4428
+ return trimmed || null;
4429
+ }
2907
4430
  function normalizeApplicationId(application) {
2908
- const id = application.trim().toLowerCase();
2909
- if (Object.prototype.hasOwnProperty.call(APPLICATIONS, id)) {
2910
- return id;
4431
+ const raw = application.trim().toLowerCase();
4432
+ const normalized = raw.replace(/-/g, "_");
4433
+ if (Object.prototype.hasOwnProperty.call(APPLICATIONS, raw)) {
4434
+ return raw;
2911
4435
  }
2912
- if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(id)) {
2913
- throw new Error("Application id must be kebab-case using lowercase letters, numbers, and hyphens.");
4436
+ if (Object.prototype.hasOwnProperty.call(APPLICATIONS, normalized)) {
4437
+ return normalized;
2914
4438
  }
2915
- return id;
4439
+ if (/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(raw) || /^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/.test(raw)) {
4440
+ return normalized;
4441
+ }
4442
+ throw new Error(
4443
+ "Application id must be kebab-case or snake_case using lowercase letters, numbers, hyphens, and underscores."
4444
+ );
4445
+ }
4446
+ function toSlug(value) {
4447
+ return value.replace(/_/g, "-");
2916
4448
  }
2917
4449
  function toPascalCase(value) {
2918
- return value.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
4450
+ return value.split(/[-_]/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
4451
+ }
4452
+ function toTitle(value) {
4453
+ return value.replace(/^topolo[-_\s]*/i, "Topolo ").split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
4454
+ }
4455
+ function json(value) {
4456
+ return JSON.stringify(value, null, 2);
4457
+ }
4458
+ async function pathExists(path2) {
4459
+ try {
4460
+ await access(path2, constants.F_OK);
4461
+ return true;
4462
+ } catch {
4463
+ return false;
4464
+ }
4465
+ }
4466
+ function resolveInside(root, filePath) {
4467
+ const resolved = resolve(root, filePath);
4468
+ const relativePath = relative(root, resolved);
4469
+ if (relativePath.startsWith("..") || relativePath === "" || relativePath.startsWith("/")) {
4470
+ throw new Error(`Scaffold file path escapes the target root: ${filePath}`);
4471
+ }
4472
+ return resolved;
4473
+ }
4474
+ function escapeHtml(value) {
4475
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4476
+ }
4477
+ function escapeSingleQuoted(value) {
4478
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
4479
+ }
4480
+ function escapeDoubleQuoted(value) {
4481
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4482
+ }
4483
+ function escapeFrontmatter(value) {
4484
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4485
+ }
4486
+
4487
+ // src/commands/app-scaffold-provision.ts
4488
+ var DEFAULT_DEVELOPERS_ORIGIN = "https://developers.topolo.app";
4489
+ var DEVELOPERS_SERVICE_ID = "svc_topolo_developers";
4490
+ async function provisionApplicationScaffold(payload) {
4491
+ if (!payload.application.productionUrl) {
4492
+ throw new Error(
4493
+ "Scaffold provisioning requires a browser-capable application with a production URL."
4494
+ );
4495
+ }
4496
+ if (payload.target === "platform" && !payload.application.serviceId) {
4497
+ throw new Error(
4498
+ "Platform scaffold provisioning requires a service id. Re-run without --no-api or pass --service."
4499
+ );
4500
+ }
4501
+ const credential = await refreshCredentialIfNeeded();
4502
+ if (!credential) {
4503
+ throw new Error(
4504
+ "topolo apps scaffold --provision requires an authenticated Topolo session. Run `topolo auth login` first."
4505
+ );
4506
+ }
4507
+ const client = await buildClient();
4508
+ if (!client) {
4509
+ throw new Error(
4510
+ "Could not build a Topolo client for scaffold provisioning. Run `topolo auth login` first."
4511
+ );
4512
+ }
4513
+ const identity = await client.client.introspect();
4514
+ const config = await readConfig();
4515
+ const serviceUrls = resolveServiceUrls(config.serviceUrls) ?? {};
4516
+ const developersOrigin = sanitizeOrigin(serviceUrls.developers ?? DEFAULT_DEVELOPERS_ORIGIN);
4517
+ const endpoint = payload.target === "platform" ? "/api/developer-console/admin/first-party-app-scaffolds" : "/api/developer-console/apps/scaffold";
4518
+ const headers = new Headers({
4519
+ "Content-Type": "application/json",
4520
+ "X-Service-ID": DEVELOPERS_SERVICE_ID,
4521
+ "X-Topolo-Client": `topolo-cli/${CLI_VERSION}`
4522
+ });
4523
+ const agentName = resolveAgentName(config.agentName);
4524
+ if (agentName) {
4525
+ headers.set("X-Topolo-Agent", agentName);
4526
+ }
4527
+ if (credential.kind === "api_key") {
4528
+ headers.set("X-Api-Key", credential.apiKey);
4529
+ } else {
4530
+ headers.set("Authorization", `Bearer ${credential.accessToken}`);
4531
+ }
4532
+ const response = await fetch(`${developersOrigin}${endpoint}`, {
4533
+ method: "POST",
4534
+ headers,
4535
+ body: JSON.stringify(buildProvisioningRequest(payload))
4536
+ });
4537
+ const envelope = await response.json().catch(() => null);
4538
+ if (!response.ok || !envelope?.success || !envelope.data) {
4539
+ throw new Error(
4540
+ resolveProvisioningError(envelope, response.status, payload.target)
4541
+ );
4542
+ }
4543
+ const app = pickRecord(envelope.data.app);
4544
+ const service = pickRecord(envelope.data.service);
4545
+ const authConfig = pickRecord(envelope.data.authConfig);
4546
+ if (!app) {
4547
+ throw new Error("Developers provisioning did not return an app record.");
4548
+ }
4549
+ const appId = readRequiredString(app.id, "Developers provisioning did not return an app id.");
4550
+ return {
4551
+ target: payload.target,
4552
+ actor: {
4553
+ kind: identity.kind,
4554
+ userId: identity.user.id,
4555
+ email: identity.user.email,
4556
+ role: identity.user.role,
4557
+ organizationId: identity.organization?.id ?? null,
4558
+ organizationSlug: identity.organization?.slug ?? null
4559
+ },
4560
+ developersOrigin,
4561
+ developersApp: {
4562
+ id: appId,
4563
+ slug: readString(app.slug),
4564
+ listingStatus: readString(app.listingStatus),
4565
+ organizationId: readString(app.organizationId)
4566
+ },
4567
+ authService: service ? {
4568
+ id: readRequiredString(
4569
+ service.id,
4570
+ "Auth registration did not return a service id."
4571
+ ),
4572
+ status: readString(service.status),
4573
+ baseUrl: readString(service.base_url) ?? readString(service.baseUrl)
4574
+ } : null,
4575
+ authConfig: authConfig ? {
4576
+ login: authConfig.login === true,
4577
+ landing: authConfig.landing === true,
4578
+ appUi: authConfig.appUi === true
4579
+ } : null
4580
+ };
4581
+ }
4582
+ function buildProvisioningRequest(payload) {
4583
+ return {
4584
+ target: payload.target,
4585
+ ownerType: payload.ownerType,
4586
+ portfolio: payload.portfolio,
4587
+ audience: payload.audience,
4588
+ tenancy: payload.tenancy,
4589
+ surfaceModel: payload.surfaceModel,
4590
+ containerAppId: payload.containerAppId,
4591
+ distributionModel: payload.distributionModel,
4592
+ applicationId: payload.application.id,
4593
+ appDir: payload.application.appDir,
4594
+ packageName: payload.application.packageName,
4595
+ name: payload.application.name,
4596
+ productionUrl: payload.application.productionUrl,
4597
+ serviceId: payload.application.serviceId
4598
+ };
4599
+ }
4600
+ function resolveProvisioningError(envelope, status, target) {
4601
+ const message = (typeof envelope?.error === "string" ? envelope.error : envelope?.error?.message) || envelope?.message || `Scaffold provisioning failed with status ${status}.`;
4602
+ if (status === 401 || status === 403) {
4603
+ if (target === "platform") {
4604
+ return `${message} Platform scaffold provisioning requires a Topolo platform-admin session with Developers commerce access.`;
4605
+ }
4606
+ return `${message} Developer scaffold provisioning requires a signed-in Topolo developer workspace session.`;
4607
+ }
4608
+ return message;
4609
+ }
4610
+ function sanitizeOrigin(value) {
4611
+ return value.replace(/\/+$/, "");
4612
+ }
4613
+ function pickRecord(value) {
4614
+ return value && typeof value === "object" ? value : null;
4615
+ }
4616
+ function readString(value) {
4617
+ if (typeof value !== "string") {
4618
+ return null;
4619
+ }
4620
+ const trimmed = value.trim();
4621
+ return trimmed ? trimmed : null;
4622
+ }
4623
+ function readRequiredString(value, message) {
4624
+ const resolved = readString(value);
4625
+ if (!resolved) {
4626
+ throw new Error(message);
4627
+ }
4628
+ return resolved;
2919
4629
  }
2920
4630
 
2921
4631
  // src/commands/apps.ts
@@ -2979,21 +4689,46 @@ function registerAppsCommand(program2) {
2979
4689
  }
2980
4690
  });
2981
4691
  apps.command("scaffold <application>").description(
2982
- "Generate the required Topolo new-application scaffold, including a conformance workflow."
2983
- ).option("--json", "Output JSON").option("--no-json", "Output human-readable checklist").option("--app-dir <path>", "Application directory under PlatformApplications").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 service id for API-backed apps").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option("--workflow-only", "Output only the generated GitHub Actions workflow in human mode").action((application, opts) => {
4692
+ "Generate a third-party Topolo application scaffold for business or personal/household use."
4693
+ ).option("--json", "Output JSON").option("--no-json", "Output human-readable checklist").option("--app-dir <path>", "Application directory under PlatformApplications").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 service id for API-backed apps").option("--audience <audience>", "Developer app audience: business or personal").option("--tenancy <tenancy>", "Developer app tenancy: organization or household").option(
4694
+ "--surface-model <surfaceModel>",
4695
+ "Developer app surface model: web_app, mobile_container, mobile_module, or messaging_surface"
4696
+ ).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").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 API/service files for developer scaffolds").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
4697
+ "--provision",
4698
+ "Provision the generated app into the current Topolo developer workspace using the authenticated session"
4699
+ ).action(async (application, opts) => {
2984
4700
  const mode = opts.json === false || process.argv.includes("--no-json") ? "pretty" : resolveOutputMode(opts.json);
2985
4701
  try {
4702
+ const api = resolveScaffoldApiOption(opts.api, opts.withApi);
2986
4703
  const payload = applicationScaffoldPayload(application, {
2987
4704
  appDir: opts.appDir,
2988
4705
  name: opts.name,
2989
4706
  packageName: opts.packageName,
2990
4707
  productionUrl: opts.productionUrl,
2991
4708
  service: opts.service,
2992
- api: opts.api,
2993
- browser: opts.browser
4709
+ api,
4710
+ browser: opts.browser,
4711
+ target: "developer",
4712
+ audience: resolveScaffoldAudience(opts.audience),
4713
+ tenancy: resolveScaffoldTenancy(opts.tenancy),
4714
+ surfaceModel: resolveScaffoldSurfaceModel(opts.surfaceModel),
4715
+ containerAppId: opts.containerAppId
2994
4716
  });
2995
- const output = opts.workflowOnly ? { application: payload.application, ciWorkflowPath: payload.ciWorkflowPath, ciWorkflow: payload.ciWorkflow } : payload;
2996
- emit(output, mode, () => opts.workflowOnly ? payload.ciWorkflow : formatScaffold(payload));
4717
+ if (opts.provision && !opts.write) {
4718
+ throw new Error("--provision requires --write so remote developer records match written scaffold files.");
4719
+ }
4720
+ const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
4721
+ const provisionResult = opts.provision ? await provisionApplicationScaffold(payload) : null;
4722
+ const output = {
4723
+ ...payload,
4724
+ ...writeResult ? { write: writeResult } : {},
4725
+ ...provisionResult ? { provision: provisionResult } : {}
4726
+ };
4727
+ emit(
4728
+ output,
4729
+ mode,
4730
+ () => formatScaffold(payload, writeResult, provisionResult)
4731
+ );
2997
4732
  } catch (err) {
2998
4733
  fail(err, mode);
2999
4734
  }
@@ -3154,9 +4889,17 @@ function formatAudit(payload) {
3154
4889
  }
3155
4890
  return lines.join("\n");
3156
4891
  }
3157
- function formatScaffold(payload) {
4892
+ function formatScaffold(payload, writeResult = null, provisionResult = null) {
3158
4893
  const lines = [
3159
4894
  `Version: ${payload.version}`,
4895
+ `Target: ${payload.target}`,
4896
+ `Owner: ${payload.ownerType}`,
4897
+ `Audience: ${payload.audience}`,
4898
+ `Tenancy: ${payload.tenancy}`,
4899
+ `Portfolio: ${payload.portfolio ?? "-"}`,
4900
+ `Surface: ${payload.surfaceModel}`,
4901
+ `Container: ${payload.containerAppId ?? "-"}`,
4902
+ `Distribution: ${payload.distributionModel}`,
3160
4903
  `Application: ${payload.application.id}`,
3161
4904
  `Directory: ${payload.application.appDir}`,
3162
4905
  `Package: ${payload.application.packageName}`,
@@ -3177,9 +4920,72 @@ function formatScaffold(payload) {
3177
4920
  for (const command of payload.commands) {
3178
4921
  lines.push(` - ${command}`);
3179
4922
  }
3180
- lines.push(`CI workflow: ${payload.ciWorkflowPath}`);
4923
+ lines.push(`CI workflow: ${payload.ciWorkflowPath ?? "-"}`);
4924
+ if (writeResult) {
4925
+ lines.push(`Written root: ${writeResult.root}`);
4926
+ lines.push("Written files:");
4927
+ for (const file of writeResult.files) {
4928
+ lines.push(` - ${file.status}: ${file.path}`);
4929
+ }
4930
+ }
4931
+ if (provisionResult) {
4932
+ lines.push("Provisioning:");
4933
+ lines.push(` - target: ${provisionResult.target}`);
4934
+ lines.push(
4935
+ ` - actor: ${provisionResult.actor.email} (${provisionResult.actor.role}, org=${provisionResult.actor.organizationSlug ?? provisionResult.actor.organizationId ?? "-"})`
4936
+ );
4937
+ lines.push(` - developers origin: ${provisionResult.developersOrigin}`);
4938
+ lines.push(
4939
+ ` - developers app: ${provisionResult.developersApp.id} (listing=${provisionResult.developersApp.listingStatus ?? "-"}, org=${provisionResult.developersApp.organizationId ?? "-"})`
4940
+ );
4941
+ if (provisionResult.authService) {
4942
+ lines.push(
4943
+ ` - auth service: ${provisionResult.authService.id} (status=${provisionResult.authService.status ?? "-"}, url=${provisionResult.authService.baseUrl ?? "-"})`
4944
+ );
4945
+ }
4946
+ if (provisionResult.authConfig) {
4947
+ lines.push(
4948
+ ` - auth config: login=${provisionResult.authConfig.login} landing=${provisionResult.authConfig.landing} app_ui=${provisionResult.authConfig.appUi}`
4949
+ );
4950
+ }
4951
+ }
3181
4952
  return lines.join("\n");
3182
4953
  }
4954
+ function resolveScaffoldAudience(value) {
4955
+ if (!value) return void 0;
4956
+ const normalized = value.trim().toLowerCase();
4957
+ if (normalized === "business" || normalized === "personal") {
4958
+ return normalized;
4959
+ }
4960
+ throw new Error("Invalid --audience value. Use business or personal.");
4961
+ }
4962
+ function resolveScaffoldTenancy(value) {
4963
+ if (!value) return void 0;
4964
+ const normalized = value.trim().toLowerCase();
4965
+ if (normalized === "organization" || normalized === "household") {
4966
+ return normalized;
4967
+ }
4968
+ throw new Error("Invalid --tenancy value. Use organization or household.");
4969
+ }
4970
+ function resolveScaffoldSurfaceModel(value) {
4971
+ if (!value) return void 0;
4972
+ const normalized = value.trim().toLowerCase();
4973
+ if (normalized === "web_app" || normalized === "mobile_container" || normalized === "mobile_module" || normalized === "messaging_surface") {
4974
+ return normalized;
4975
+ }
4976
+ throw new Error(
4977
+ "Invalid --surface-model value. Use web_app, mobile_container, mobile_module, or messaging_surface."
4978
+ );
4979
+ }
4980
+ function resolveScaffoldApiOption(api, withApi) {
4981
+ const noApi = process.argv.includes("--no-api");
4982
+ if (withApi && noApi) {
4983
+ throw new Error("--with-api cannot be combined with --no-api.");
4984
+ }
4985
+ if (withApi) return true;
4986
+ if (noApi || api === false) return false;
4987
+ return void 0;
4988
+ }
3183
4989
  function normalizeFailOn(value) {
3184
4990
  const normalized = value.trim().toLowerCase().replace(/-/g, "_");
3185
4991
  if (normalized === "missing" || normalized === "needs_review" || normalized === "partial") {
@@ -3193,6 +4999,98 @@ function statusesAtOrWorse(threshold) {
3193
4999
  return ["missing", "needs_review", "partial"];
3194
5000
  }
3195
5001
 
5002
+ // src/commands/platform.ts
5003
+ function registerPlatformCommand(program2) {
5004
+ const platform3 = program2.command("platform").description("Internal Topolo first-party platform and personal app commands");
5005
+ const platformApps = platform3.command("apps").description("Scaffold and operate first-party Topolo platform and personal applications");
5006
+ platformApps.command("scaffold <application>").description(
5007
+ "Generate a first-party Topolo scaffold for the platform or personal portfolio."
5008
+ ).option("--json", "Output JSON").option("--no-json", "Output human-readable checklist").option("--app-dir <path>", "Application directory under PlatformApplications").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 service id for API-backed apps").option("--portfolio <portfolio>", "First-party portfolio: platform or personal").option(
5009
+ "--surface-model <surfaceModel>",
5010
+ "First-party surface model: web_app, mobile_container, mobile_module, or messaging_surface"
5011
+ ).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").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(
5012
+ "--provision",
5013
+ "Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
5014
+ ).option("--workflow-only", "Output only the generated GitHub Actions workflow in human mode").action(async (application, opts) => {
5015
+ const mode = opts.json === false || process.argv.includes("--no-json") ? "pretty" : resolveOutputMode(opts.json);
5016
+ try {
5017
+ await requirePlatformAdminSession();
5018
+ if (opts.workflowOnly && opts.write) {
5019
+ throw new Error("--workflow-only cannot be used with --write.");
5020
+ }
5021
+ const api = resolveScaffoldApiOption(opts.api, opts.withApi);
5022
+ const payload = applicationScaffoldPayload(application, {
5023
+ appDir: opts.appDir,
5024
+ name: opts.name,
5025
+ packageName: opts.packageName,
5026
+ productionUrl: opts.productionUrl,
5027
+ service: opts.service,
5028
+ api,
5029
+ browser: opts.browser,
5030
+ target: "platform",
5031
+ portfolio: resolvePlatformPortfolio(opts.portfolio),
5032
+ surfaceModel: resolvePlatformSurfaceModel(opts.surfaceModel),
5033
+ containerAppId: opts.containerAppId
5034
+ });
5035
+ if (opts.workflowOnly && !payload.ciWorkflow) {
5036
+ throw new Error("--workflow-only is only available for first-party scaffolds.");
5037
+ }
5038
+ if (opts.provision && !opts.write) {
5039
+ throw new Error("--provision requires --write so remote first-party records match written scaffold files.");
5040
+ }
5041
+ const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
5042
+ const provisionResult = opts.provision ? await provisionApplicationScaffold(payload) : null;
5043
+ const output = opts.workflowOnly ? { application: payload.application, ciWorkflowPath: payload.ciWorkflowPath, ciWorkflow: payload.ciWorkflow } : {
5044
+ ...payload,
5045
+ ...writeResult ? { write: writeResult } : {},
5046
+ ...provisionResult ? { provision: provisionResult } : {}
5047
+ };
5048
+ emit(
5049
+ output,
5050
+ mode,
5051
+ () => opts.workflowOnly ? payload.ciWorkflow ?? "" : formatScaffold(payload, writeResult, provisionResult)
5052
+ );
5053
+ } catch (err) {
5054
+ fail(err, mode);
5055
+ }
5056
+ });
5057
+ }
5058
+ function resolvePlatformPortfolio(value) {
5059
+ if (!value) return void 0;
5060
+ const normalized = value.trim().toLowerCase();
5061
+ if (normalized === "platform" || normalized === "personal") {
5062
+ return normalized;
5063
+ }
5064
+ throw new Error("Invalid --portfolio value. Use platform or personal.");
5065
+ }
5066
+ function resolvePlatformSurfaceModel(value) {
5067
+ if (!value) return void 0;
5068
+ const normalized = value.trim().toLowerCase();
5069
+ if (normalized === "web_app" || normalized === "mobile_container" || normalized === "mobile_module" || normalized === "messaging_surface") {
5070
+ return normalized;
5071
+ }
5072
+ throw new Error(
5073
+ "Invalid --surface-model value. Use web_app, mobile_container, mobile_module, or messaging_surface."
5074
+ );
5075
+ }
5076
+ function isPlatformAdminIdentity(identity) {
5077
+ return String(identity?.user?.role || "").trim().toLowerCase() === "super_admin" && String(identity?.organization?.slug || "").trim().toLowerCase() === "admin";
5078
+ }
5079
+ async function requirePlatformAdminSession() {
5080
+ const client = await buildClient();
5081
+ if (!client) {
5082
+ throw new Error(
5083
+ "`topolo platform ...` requires an authenticated Topolo platform-admin session. Run `topolo auth login` first."
5084
+ );
5085
+ }
5086
+ const identity = await client.client.introspect();
5087
+ if (!isPlatformAdminIdentity(identity)) {
5088
+ throw new Error(
5089
+ "`topolo platform ...` is restricted to Topolo platform admins in the Auth `admin` organization."
5090
+ );
5091
+ }
5092
+ }
5093
+
3196
5094
  // src/commands/services.ts
3197
5095
  function registerServicesCommand(program2) {
3198
5096
  program2.command("services").description("List every Topolo service the SDK can route to").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action((opts) => {
@@ -3231,13 +5129,14 @@ function formatRows2(rows) {
3231
5129
  // src/index.ts
3232
5130
  var program = new Command();
3233
5131
  program.name("topolo").description(
3234
- "Topolo platform CLI. Every command is scoped to the authenticated organization \u2014 there is no way to target a different org."
5132
+ "Topolo CLI. Public commands are scoped to the authenticated organization or developer workspace, and first-party Topolo scaffolding lives under the internal `topolo platform` namespace."
3235
5133
  ).version(CLI_VERSION);
3236
5134
  registerAuthCommands(program);
3237
5135
  registerWhoamiCommand(program);
3238
5136
  registerCrmCommands(program);
3239
5137
  registerApiCommand(program);
3240
5138
  registerAppsCommand(program);
5139
+ registerPlatformCommand(program);
3241
5140
  registerServicesCommand(program);
3242
5141
  program.parseAsync(process.argv).catch((err) => {
3243
5142
  process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}