@topolo/cli 0.1.10 → 0.1.11

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 +1799 -76
  3. package/package.json +1 -1
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
+ // ../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);
@@ -67,14 +67,20 @@ var TopoloHttpError = class extends TopoloSdkError {
67
67
  var SERVICES = {
68
68
  agent: "https://topolo-agent-worker.topolo.workers.dev",
69
69
  auth: "https://auth.topolo.app",
70
+ books: "https://books.topolo.app",
70
71
  bugfix: "https://bugfix.topolo.app",
71
72
  bytes: "https://r2-media-manager-worker.topolo.workers.dev",
72
73
  calendar: "https://calendar.topolo.app",
73
74
  chat: "https://chat.topolo.app",
74
75
  commerce: "https://topolo-commerce-api.topolo.workers.dev",
75
76
  crm: "https://crm-api.topolo.app",
77
+ customer_success: "https://customer-success.topolo.app",
76
78
  feed: "https://topolo-feed-api.topolo.app",
79
+ flow: "https://flow.topolo.app",
77
80
  forecast: "https://forecast-worker-production.topolo.workers.dev",
81
+ forms: "https://forms.topolo.app",
82
+ insights: "https://insights.topolo.app",
83
+ inventory: "https://inventory.topolo.app",
78
84
  learn: "https://topololearn-api.topolo.workers.dev",
79
85
  mail: "https://mail.topolo.app",
80
86
  mdm: "https://topolo-mdm-api.topolo.app",
@@ -82,16 +88,30 @@ var SERVICES = {
82
88
  nexus: "https://topolo-nexus-gateway.topolo.workers.dev",
83
89
  one: "https://topoloone-worker.topolo.workers.dev",
84
90
  pay: "https://topolo-pay.topolo.workers.dev",
91
+ people: "https://people.topolo.app",
85
92
  "quro.api": "https://api.ol0.me",
86
93
  "quro.redirect": "https://ol0.me",
87
94
  roadmapper: "https://roadmapper-api.topolo.app",
95
+ sign: "https://sign.topolo.app",
88
96
  social_studio: "https://topolo-social-studio-api.topolo.workers.dev",
89
97
  socialize: "https://socialize-prod-api.topolo.workers.dev",
90
98
  support: "https://support.topolo.app",
99
+ survey: "https://survey.topolo.app",
100
+ voice: "https://voice.topolo.app",
91
101
  web: "https://web-api.topolo.app"
92
102
  };
93
103
  var PLATFORM_SERVICE_IDS = {
94
- crm: "srv_iCwM4jGXcwlj"
104
+ books: "srv_topolo_books",
105
+ crm: "srv_iCwM4jGXcwlj",
106
+ customer_success: "srv_topolo_customer_success",
107
+ flow: "srv_topolo_flow",
108
+ forms: "srv_topolo_forms",
109
+ insights: "srv_topolo_insights",
110
+ inventory: "srv_topolo_inventory",
111
+ people: "srv_topolo_people",
112
+ sign: "srv_topolo_sign",
113
+ survey: "srv_topolo_survey",
114
+ voice: "srv_topolo_voice"
95
115
  };
96
116
  var APPLICATIONS = {
97
117
  "admin": {
@@ -164,6 +184,30 @@ var APPLICATIONS = {
164
184
  }
165
185
  ]
166
186
  },
187
+ "books": {
188
+ "id": "books",
189
+ "appDir": "TopoloBooks",
190
+ "name": "topolo-books",
191
+ "packageName": "topolo-books",
192
+ "productionUrl": "https://books.topolo.app",
193
+ "services": [
194
+ "books"
195
+ ],
196
+ "deployTargets": [
197
+ {
198
+ "name": "topolo-books",
199
+ "kind": "worker",
200
+ "apiUrl": "https://books.topolo.app",
201
+ "domains": [
202
+ "books.topolo.app"
203
+ ],
204
+ "routes": [
205
+ "books.topolo.app"
206
+ ],
207
+ "serviceId": "books"
208
+ }
209
+ ]
210
+ },
167
211
  "bugfix": {
168
212
  "id": "bugfix",
169
213
  "appDir": "TopoloBugFix",
@@ -365,6 +409,30 @@ var APPLICATIONS = {
365
409
  }
366
410
  ]
367
411
  },
412
+ "customer_success": {
413
+ "id": "customer_success",
414
+ "appDir": "TopoloCustomerSuccess",
415
+ "name": "topolo-customer-success",
416
+ "packageName": "topolo-customer-success",
417
+ "productionUrl": "https://customer-success.topolo.app",
418
+ "services": [
419
+ "customer_success"
420
+ ],
421
+ "deployTargets": [
422
+ {
423
+ "name": "topolo-customer-success",
424
+ "kind": "worker",
425
+ "apiUrl": "https://customer-success.topolo.app",
426
+ "domains": [
427
+ "customer-success.topolo.app"
428
+ ],
429
+ "routes": [
430
+ "customer-success.topolo.app"
431
+ ],
432
+ "serviceId": "customer_success"
433
+ }
434
+ ]
435
+ },
368
436
  "developers": {
369
437
  "id": "developers",
370
438
  "appDir": "TopoloDevelopers",
@@ -441,6 +509,30 @@ var APPLICATIONS = {
441
509
  }
442
510
  ]
443
511
  },
512
+ "flow": {
513
+ "id": "flow",
514
+ "appDir": "TopoloFlow",
515
+ "name": "topolo-flow",
516
+ "packageName": "topolo-flow",
517
+ "productionUrl": "https://flow.topolo.app",
518
+ "services": [
519
+ "flow"
520
+ ],
521
+ "deployTargets": [
522
+ {
523
+ "name": "topolo-flow",
524
+ "kind": "worker",
525
+ "apiUrl": "https://flow.topolo.app",
526
+ "domains": [
527
+ "flow.topolo.app"
528
+ ],
529
+ "routes": [
530
+ "flow.topolo.app"
531
+ ],
532
+ "serviceId": "flow"
533
+ }
534
+ ]
535
+ },
444
536
  "forecast": {
445
537
  "id": "forecast",
446
538
  "appDir": "TopoloForecast",
@@ -469,6 +561,78 @@ var APPLICATIONS = {
469
561
  }
470
562
  ]
471
563
  },
564
+ "forms": {
565
+ "id": "forms",
566
+ "appDir": "TopoloForms",
567
+ "name": "topolo-forms",
568
+ "packageName": "topolo-forms",
569
+ "productionUrl": "https://forms.topolo.app",
570
+ "services": [
571
+ "forms"
572
+ ],
573
+ "deployTargets": [
574
+ {
575
+ "name": "topolo-forms",
576
+ "kind": "worker",
577
+ "apiUrl": "https://forms.topolo.app",
578
+ "domains": [
579
+ "forms.topolo.app"
580
+ ],
581
+ "routes": [
582
+ "forms.topolo.app"
583
+ ],
584
+ "serviceId": "forms"
585
+ }
586
+ ]
587
+ },
588
+ "insights": {
589
+ "id": "insights",
590
+ "appDir": "TopoloInsights",
591
+ "name": "topolo-insights",
592
+ "packageName": "topolo-insights",
593
+ "productionUrl": "https://insights.topolo.app",
594
+ "services": [
595
+ "insights"
596
+ ],
597
+ "deployTargets": [
598
+ {
599
+ "name": "topolo-insights",
600
+ "kind": "worker",
601
+ "apiUrl": "https://insights.topolo.app",
602
+ "domains": [
603
+ "insights.topolo.app"
604
+ ],
605
+ "routes": [
606
+ "insights.topolo.app"
607
+ ],
608
+ "serviceId": "insights"
609
+ }
610
+ ]
611
+ },
612
+ "inventory": {
613
+ "id": "inventory",
614
+ "appDir": "TopoloInventory",
615
+ "name": "topolo-inventory",
616
+ "packageName": "topolo-inventory",
617
+ "productionUrl": "https://inventory.topolo.app",
618
+ "services": [
619
+ "inventory"
620
+ ],
621
+ "deployTargets": [
622
+ {
623
+ "name": "topolo-inventory",
624
+ "kind": "worker",
625
+ "apiUrl": "https://inventory.topolo.app",
626
+ "domains": [
627
+ "inventory.topolo.app"
628
+ ],
629
+ "routes": [
630
+ "inventory.topolo.app"
631
+ ],
632
+ "serviceId": "inventory"
633
+ }
634
+ ]
635
+ },
472
636
  "learn": {
473
637
  "id": "learn",
474
638
  "appDir": "TopoloLearn",
@@ -669,7 +833,9 @@ var APPLICATIONS = {
669
833
  "kind": "worker",
670
834
  "apiUrl": null,
671
835
  "domains": [
672
- "one.topolo.io"
836
+ "one.topolo.io",
837
+ "topolo.io",
838
+ "www.topolo.io"
673
839
  ],
674
840
  "routes": [],
675
841
  "serviceId": null
@@ -706,6 +872,30 @@ var APPLICATIONS = {
706
872
  }
707
873
  ]
708
874
  },
875
+ "people": {
876
+ "id": "people",
877
+ "appDir": "TopoloPeople",
878
+ "name": "topolo-people",
879
+ "packageName": "topolo-people",
880
+ "productionUrl": "https://people.topolo.app",
881
+ "services": [
882
+ "people"
883
+ ],
884
+ "deployTargets": [
885
+ {
886
+ "name": "topolo-people",
887
+ "kind": "worker",
888
+ "apiUrl": "https://people.topolo.app",
889
+ "domains": [
890
+ "people.topolo.app"
891
+ ],
892
+ "routes": [
893
+ "people.topolo.app"
894
+ ],
895
+ "serviceId": "people"
896
+ }
897
+ ]
898
+ },
709
899
  "quro": {
710
900
  "id": "quro",
711
901
  "appDir": "TopoloQuro",
@@ -777,6 +967,30 @@ var APPLICATIONS = {
777
967
  }
778
968
  ]
779
969
  },
970
+ "sign": {
971
+ "id": "sign",
972
+ "appDir": "TopoloSign",
973
+ "name": "topolo-sign",
974
+ "packageName": "topolo-sign",
975
+ "productionUrl": "https://sign.topolo.app",
976
+ "services": [
977
+ "sign"
978
+ ],
979
+ "deployTargets": [
980
+ {
981
+ "name": "topolo-sign",
982
+ "kind": "worker",
983
+ "apiUrl": "https://sign.topolo.app",
984
+ "domains": [
985
+ "sign.topolo.app"
986
+ ],
987
+ "routes": [
988
+ "sign.topolo.app"
989
+ ],
990
+ "serviceId": "sign"
991
+ }
992
+ ]
993
+ },
780
994
  "social_studio": {
781
995
  "id": "social_studio",
782
996
  "appDir": "TopoloSocialStudio",
@@ -873,6 +1087,54 @@ var APPLICATIONS = {
873
1087
  }
874
1088
  ]
875
1089
  },
1090
+ "survey": {
1091
+ "id": "survey",
1092
+ "appDir": "TopoloSurvey",
1093
+ "name": "topolo-survey",
1094
+ "packageName": "topolo-survey",
1095
+ "productionUrl": "https://survey.topolo.app",
1096
+ "services": [
1097
+ "survey"
1098
+ ],
1099
+ "deployTargets": [
1100
+ {
1101
+ "name": "topolo-survey",
1102
+ "kind": "worker",
1103
+ "apiUrl": "https://survey.topolo.app",
1104
+ "domains": [
1105
+ "survey.topolo.app"
1106
+ ],
1107
+ "routes": [
1108
+ "survey.topolo.app"
1109
+ ],
1110
+ "serviceId": "survey"
1111
+ }
1112
+ ]
1113
+ },
1114
+ "voice": {
1115
+ "id": "voice",
1116
+ "appDir": "TopoloVoice",
1117
+ "name": "topolo-voice",
1118
+ "packageName": "topolo-voice",
1119
+ "productionUrl": "https://voice.topolo.app",
1120
+ "services": [
1121
+ "voice"
1122
+ ],
1123
+ "deployTargets": [
1124
+ {
1125
+ "name": "topolo-voice",
1126
+ "kind": "worker",
1127
+ "apiUrl": "https://voice.topolo.app",
1128
+ "domains": [
1129
+ "voice.topolo.app"
1130
+ ],
1131
+ "routes": [
1132
+ "voice.topolo.app"
1133
+ ],
1134
+ "serviceId": "voice"
1135
+ }
1136
+ ]
1137
+ },
876
1138
  "web": {
877
1139
  "id": "web",
878
1140
  "appDir": "TopoloWeb",
@@ -1938,11 +2200,11 @@ async function pollUntilApproved(params) {
1938
2200
  throw new Error("Device code expired. Run `topolo auth login` again.");
1939
2201
  }
1940
2202
  function sleep(ms) {
1941
- return new Promise((resolve) => setTimeout(resolve, ms));
2203
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1942
2204
  }
1943
2205
 
1944
2206
  // src/version.ts
1945
- var CLI_VERSION = "0.1.10";
2207
+ var CLI_VERSION = "0.1.11";
1946
2208
 
1947
2209
  // src/client.ts
1948
2210
  async function buildClient(options) {
@@ -2304,13 +2566,26 @@ function enrichFinding(application, finding2, evidence) {
2304
2566
  });
2305
2567
  }
2306
2568
  if (shellEvidence.length === 0) return finding2;
2569
+ if (shellEvidence.includes("local=account-menu")) {
2570
+ return mergeFinding(finding2, {
2571
+ status: "needs_review",
2572
+ 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.",
2573
+ evidence: shellEvidence,
2574
+ nextActions: [
2575
+ "Replace app-local UserMenu, user-menu, or accountMenuOpen dropdown code with TopoloAccountMenu",
2576
+ "Use TopoloAppShell accountMenuItems for app-specific account actions",
2577
+ "Keep sign-out, app launcher, command palette, theme, and support actions on the shared account-menu surface"
2578
+ ]
2579
+ });
2580
+ }
2307
2581
  if (!shellEvidence.includes("source=TopoloAppShell")) {
2308
2582
  return mergeFinding(finding2, {
2309
2583
  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.",
2584
+ 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
2585
  evidence: shellEvidence,
2312
2586
  nextActions: [
2313
2587
  "Use TopoloAppShell for authenticated first-party app layouts",
2588
+ "Use TopoloAccountMenu when a partial shell still needs account chrome",
2314
2589
  "Keep launcher, account menu, command palette, theme toggle, and bug reporter on the shared shell surface",
2315
2590
  "Keep mobile and desktop shell behavior aligned with the platform UI kit"
2316
2591
  ]
@@ -2322,6 +2597,7 @@ function enrichFinding(application, finding2, evidence) {
2322
2597
  evidence: shellEvidence,
2323
2598
  nextActions: [
2324
2599
  "Use TopoloAppShell for authenticated first-party app layouts",
2600
+ "Pass app-specific account actions through accountMenuItems instead of app-local dropdowns",
2325
2601
  "Keep launcher, account menu, command palette, theme toggle, and bug reporter on the shared shell surface",
2326
2602
  "Keep mobile and desktop shell behavior aligned with the platform UI kit"
2327
2603
  ]
@@ -2373,14 +2649,14 @@ function enrichFinding(application, finding2, evidence) {
2373
2649
  function collectEvidence(repoRoot, application) {
2374
2650
  const appRoot = path.join(repoRoot, "PlatformApplications", application.appDir);
2375
2651
  const packageJsonPath = path.join(appRoot, "package.json");
2376
- const packageJson = readPackageJson(packageJsonPath);
2652
+ const packageJson2 = readPackageJson(packageJsonPath);
2377
2653
  const cloudcontrolPath = path.join(appRoot, "topolo.cloudcontrol.json");
2378
2654
  const systems = matchingSystems(repoRoot, application);
2379
2655
  return {
2380
2656
  appRoot,
2381
2657
  appExists: isDirectory(appRoot),
2382
2658
  packageJsonPath,
2383
- packageJson,
2659
+ packageJson: packageJson2,
2384
2660
  packageJsons: collectPackageJsons(appRoot),
2385
2661
  cloudcontrolPath,
2386
2662
  hasCloudControl: isFile(cloudcontrolPath),
@@ -2435,8 +2711,12 @@ function sharedShellEvidence(evidence) {
2435
2711
  if (text.includes("@topolo/ui-kit")) items.push("source=@topolo/ui-kit");
2436
2712
  if (text.includes("TopoloShell")) items.push("source=TopoloShell");
2437
2713
  if (text.includes("TopoloAppShell")) items.push("source=TopoloAppShell");
2714
+ if (text.includes("TopoloAccountMenu")) items.push("source=TopoloAccountMenu");
2438
2715
  if (text.includes("TopoloLauncher")) items.push("source=TopoloLauncher");
2439
2716
  if (text.includes("AppSwitcher")) items.push("source=AppSwitcher");
2717
+ if (/\baccountMenuOpen\b|\bsetAccountMenuOpen\b|\buserMenuOpen\b|\bUserMenu\b|user-menu/.test(text)) {
2718
+ items.push("local=account-menu");
2719
+ }
2440
2720
  return unique(items);
2441
2721
  }
2442
2722
  function hasDocumentationSurface(application, evidence) {
@@ -2601,8 +2881,8 @@ function collectPackageJsons(root) {
2601
2881
  continue;
2602
2882
  }
2603
2883
  if (!entry.isFile() || entry.name !== "package.json") continue;
2604
- const packageJson = readPackageJson(entryPath);
2605
- if (packageJson) records.push({ path: entryPath, packageJson });
2884
+ const packageJson2 = readPackageJson(entryPath);
2885
+ if (packageJson2) records.push({ path: entryPath, packageJson: packageJson2 });
2606
2886
  }
2607
2887
  };
2608
2888
  visit(root);
@@ -2691,38 +2971,89 @@ function isFile(filePath) {
2691
2971
  }
2692
2972
 
2693
2973
  // src/commands/app-scaffold.ts
2974
+ import { constants } from "fs";
2975
+ import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
2976
+ import { dirname as dirname2, relative, resolve } from "path";
2977
+ var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
2978
+ var CURRENT_VERIFICATION_DATE = "2026-04-23";
2694
2979
  function applicationScaffoldPayload(application, options = {}) {
2980
+ const target = normalizeTarget(options.target);
2695
2981
  const id = normalizeApplicationId(application);
2982
+ const slug = toSlug(id);
2696
2983
  const catalogApplication = APPLICATIONS[id];
2697
2984
  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;
2985
+ const api = options.api ?? target === "platform";
2986
+ const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
2987
+ const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
2988
+ const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
2989
+ const defaultServiceId = target === "platform" ? `srv_topolo_${id}` : id;
2990
+ const serviceId = api ? options.service ?? (target === "platform" ? defaultServiceId : catalogApplication?.services[0] ?? defaultServiceId) : null;
2991
+ const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
2704
2992
  const scopes = scaffoldScopes({ browser, api });
2993
+ const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
2994
+ const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir }) : null;
2995
+ const applicationPayload = {
2996
+ id,
2997
+ name,
2998
+ appDir,
2999
+ packageName,
3000
+ productionUrl,
3001
+ serviceId
3002
+ };
2705
3003
  return {
2706
3004
  version: APPLICATION_REQUIREMENTS_VERSION,
2707
- application: {
2708
- id,
2709
- name,
2710
- appDir,
2711
- packageName,
2712
- productionUrl,
2713
- serviceId
2714
- },
3005
+ target,
3006
+ application: applicationPayload,
2715
3007
  scopes,
2716
3008
  requirements: APPLICATION_REQUIREMENTS.filter(
2717
3009
  (requirement) => requirement.appliesTo.some((scope) => scopes.includes(scope))
2718
3010
  ),
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 })
3011
+ packages: scaffoldPackages({ browser, api, target }),
3012
+ files: scaffoldFiles({
3013
+ application: applicationPayload,
3014
+ target,
3015
+ browser,
3016
+ api,
3017
+ ciWorkflow
3018
+ }),
3019
+ commands: scaffoldCommands(id, target),
3020
+ ciWorkflowPath,
3021
+ ciWorkflow
2724
3022
  };
2725
3023
  }
3024
+ async function writeApplicationScaffold(payload, options = {}) {
3025
+ const root = resolve(options.repoRoot ?? process.cwd());
3026
+ const plannedFiles = payload.files.map((file) => ({
3027
+ file,
3028
+ absolutePath: resolveInside(root, file.path)
3029
+ }));
3030
+ const existingFiles = [];
3031
+ for (const planned of plannedFiles) {
3032
+ if (await pathExists(planned.absolutePath)) {
3033
+ existingFiles.push(planned.file.path);
3034
+ }
3035
+ }
3036
+ if (existingFiles.length > 0 && !options.force) {
3037
+ throw new Error(
3038
+ [
3039
+ "Refusing to overwrite existing scaffold files:",
3040
+ ...existingFiles.map((filePath) => `- ${filePath}`),
3041
+ "Pass --force to overwrite these files."
3042
+ ].join("\n")
3043
+ );
3044
+ }
3045
+ const files = [];
3046
+ for (const planned of plannedFiles) {
3047
+ const existed = await pathExists(planned.absolutePath);
3048
+ await mkdir2(dirname2(planned.absolutePath), { recursive: true });
3049
+ await writeFile2(planned.absolutePath, planned.file.content, "utf8");
3050
+ files.push({
3051
+ path: planned.file.path,
3052
+ status: existed ? "overwritten" : "created"
3053
+ });
3054
+ }
3055
+ return { root, files };
3056
+ }
2726
3057
  function scaffoldScopes(options) {
2727
3058
  const scopes = ["all"];
2728
3059
  if (options.browser) scopes.push("browser");
@@ -2732,6 +3063,9 @@ function scaffoldScopes(options) {
2732
3063
  return scopes;
2733
3064
  }
2734
3065
  function scaffoldPackages(options) {
3066
+ if (options.target === "developer") {
3067
+ return [];
3068
+ }
2735
3069
  const packages = /* @__PURE__ */ new Set();
2736
3070
  if (options.browser) {
2737
3071
  packages.add("@topolo/ui-kit");
@@ -2744,52 +3078,177 @@ function scaffoldPackages(options) {
2744
3078
  return Array.from(packages);
2745
3079
  }
2746
3080
  function scaffoldFiles(options) {
2747
- const base = `PlatformApplications/${options.appDir}`;
3081
+ const { application, target } = options;
3082
+ const slug = toSlug(application.id);
3083
+ const base = target === "platform" ? `PlatformApplications/${application.appDir}` : application.appDir;
2748
3084
  const files = [
2749
3085
  {
2750
3086
  path: `${base}/package.json`,
2751
3087
  purpose: "Package metadata and verification commands.",
2752
3088
  requiredSignals: [
2753
3089
  "name matches the packageName in the SDK application catalog",
2754
- "scripts include build plus typecheck or test where the stack supports them"
2755
- ]
3090
+ "scripts include build plus typecheck where the stack supports them"
3091
+ ],
3092
+ content: packageJson(application, target, { browser: options.browser, api: options.api })
3093
+ },
3094
+ {
3095
+ path: `${base}/README.md`,
3096
+ purpose: "Starter README with install, run, and verification commands.",
3097
+ requiredSignals: [
3098
+ "documents the generated app target",
3099
+ "points maintainers to the shared Topolo packages used by the scaffold"
3100
+ ],
3101
+ content: readme(application, target, { api: options.api })
2756
3102
  },
2757
3103
  {
2758
- path: `${base}/topolo.cloudcontrol.json`,
2759
- purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
3104
+ path: `${base}/.gitignore`,
3105
+ purpose: "Generated-app ignore rules.",
2760
3106
  requiredSignals: [
2761
- "production URLs and Cloudflare targets are explicit",
2762
- "resource bindings and required secrets are declared without secret values"
2763
- ]
3107
+ "ignores dependency installs and build output",
3108
+ "keeps local environment files out of source control"
3109
+ ],
3110
+ content: gitignore()
2764
3111
  },
2765
3112
  {
2766
- path: `${base}/.github/workflows/topolo-conformance.yml`,
2767
- purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
3113
+ path: `${base}/tsconfig.json`,
3114
+ purpose: "TypeScript configuration for the generated app.",
2768
3115
  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
- ]
3116
+ "uses strict mode",
3117
+ "includes browser DOM types for React and Worker types for API-backed apps"
3118
+ ],
3119
+ content: tsconfigJson(options.api)
2774
3120
  },
2775
3121
  {
2776
- path: `PlatformApplications/TopoloDocs/src/content/systems/topolo-${options.id}.json`,
2777
- purpose: "Canonical system registry entry.",
3122
+ path: `${base}/vite.config.ts`,
3123
+ purpose: "Vite browser build configuration.",
2778
3124
  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
- ]
3125
+ "builds the authenticated browser surface",
3126
+ "keeps generated apps on the standard React/Vite baseline"
3127
+ ],
3128
+ content: viteConfig()
2783
3129
  },
2784
3130
  {
2785
- path: `PlatformApplications/TopoloDocs/src/content/internal/apps/${options.id}.mdx`,
2786
- purpose: "Internal operator handbook.",
3131
+ path: `${base}/index.html`,
3132
+ purpose: "Browser entry document.",
2787
3133
  requiredSignals: [
2788
- "documents ownership, routes, auth boundary, deployment, operations, and verification",
2789
- "last_verified is updated when behavior changes"
2790
- ]
3134
+ "mounts the React app at #root",
3135
+ "uses the generated application display name"
3136
+ ],
3137
+ content: indexHtml(application.name)
3138
+ },
3139
+ {
3140
+ path: `${base}/src/main.tsx`,
3141
+ purpose: "React app entrypoint.",
3142
+ requiredSignals: [
3143
+ "renders the generated app component",
3144
+ "imports the generated app stylesheet"
3145
+ ],
3146
+ content: mainTsx()
3147
+ },
3148
+ {
3149
+ path: `${base}/src/env.d.ts`,
3150
+ purpose: "Vite environment variable typing.",
3151
+ requiredSignals: [
3152
+ "loads vite/client import.meta.env types",
3153
+ "declares the Topolo environment variables consumed by the starter"
3154
+ ],
3155
+ content: envDts(options.api)
3156
+ },
3157
+ {
3158
+ path: `${base}/src/styles.css`,
3159
+ purpose: "Minimal app-local styles outside the shared Topolo shell.",
3160
+ requiredSignals: [
3161
+ "does not fork the shared shell chrome",
3162
+ "keeps app-specific layout scoped under generated classes"
3163
+ ],
3164
+ content: stylesCss()
2791
3165
  }
2792
3166
  ];
3167
+ if (target === "platform") {
3168
+ files.push(
3169
+ {
3170
+ path: `${base}/topolo.cloudcontrol.json`,
3171
+ purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
3172
+ requiredSignals: [
3173
+ "production URLs and Cloudflare targets are explicit",
3174
+ "resource bindings and required secrets are declared without secret values"
3175
+ ],
3176
+ content: cloudcontrolJson(application, { api: options.api })
3177
+ },
3178
+ {
3179
+ path: `${base}/.github/workflows/topolo-conformance.yml`,
3180
+ purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
3181
+ requiredSignals: [
3182
+ "checks out this repo under its PlatformApplications app directory",
3183
+ "checks out TopoloDocs under PlatformApplications/TopoloDocs",
3184
+ "checks out topolo-sdk and TopoloCli from main so the gate does not wait for an npm publish",
3185
+ "runs topolo apps audit with --local and --fail-on needs_review"
3186
+ ],
3187
+ content: options.ciWorkflow ?? ""
3188
+ },
3189
+ {
3190
+ path: `PlatformApplications/TopoloDocs/src/content/systems/topolo-${slug}.json`,
3191
+ purpose: "Canonical system registry entry.",
3192
+ requiredSignals: [
3193
+ `repo_paths includes ${base}`,
3194
+ "doc_paths includes internal handbook coverage",
3195
+ "security_assurance records the auth, tenancy, deployment, and verification boundary"
3196
+ ],
3197
+ content: systemJson(application, { api: options.api })
3198
+ },
3199
+ {
3200
+ path: `PlatformApplications/TopoloDocs/src/content/internal/apps/${slug}.mdx`,
3201
+ purpose: "Internal operator handbook.",
3202
+ requiredSignals: [
3203
+ "documents ownership, routes, auth boundary, deployment, operations, and verification",
3204
+ "last_verified is updated when behavior changes"
3205
+ ],
3206
+ content: internalDocs(application, { api: options.api })
3207
+ },
3208
+ {
3209
+ path: `PlatformApplications/TopoloDocs/src/content/public/applications/${slug}.mdx`,
3210
+ purpose: "Public application overview.",
3211
+ requiredSignals: [
3212
+ "documents the product surface without internal-only details",
3213
+ "links back to the canonical system registry entry"
3214
+ ],
3215
+ content: publicDocs(application, { api: options.api })
3216
+ }
3217
+ );
3218
+ if (options.api && application.serviceId) {
3219
+ files.push({
3220
+ path: `PlatformApplications/TopoloAuth/catalog/service-manifests/${application.serviceId}.json`,
3221
+ purpose: "Auth service manifest for first-party permissions, role bundles, and API-key scopes.",
3222
+ requiredSignals: [
3223
+ "serviceId matches the generated Worker service id",
3224
+ "permissions, role bundles, and API-key scopes exist before the app is catalog-visible",
3225
+ "systems metadata maps the service back to the generated TopoloDocs entries"
3226
+ ],
3227
+ content: serviceManifestJson(application)
3228
+ });
3229
+ }
3230
+ } else {
3231
+ files.push(
3232
+ {
3233
+ path: `${base}/topolo.app.json`,
3234
+ purpose: "Third-party Topolo application manifest.",
3235
+ requiredSignals: [
3236
+ "declares the external app id, callback URLs, and requested platform scopes",
3237
+ "does not include internal Topolo CloudControl or canonical-docs ownership"
3238
+ ],
3239
+ content: developerManifest(application, { api: options.api })
3240
+ },
3241
+ {
3242
+ path: `${base}/.env.example`,
3243
+ purpose: "Developer environment template for local auth configuration.",
3244
+ requiredSignals: [
3245
+ "names the Topolo Auth URL",
3246
+ "keeps OAuth client ids and service ids out of source control by default"
3247
+ ],
3248
+ content: envExample(application)
3249
+ }
3250
+ );
3251
+ }
2793
3252
  if (options.browser) {
2794
3253
  files.push({
2795
3254
  path: `${base}/src/App.tsx`,
@@ -2797,8 +3256,10 @@ function scaffoldFiles(options) {
2797
3256
  requiredSignals: [
2798
3257
  "imports shared TopoloAppShell from @topolo/ui-kit",
2799
3258
  "uses @topolo/auth-client for app-origin auth flows",
2800
- "keeps launcher and account-menu behavior on the shared platform surface"
2801
- ]
3259
+ "keeps launcher and account-menu behavior on the shared platform surface",
3260
+ "passes app-specific account actions through accountMenuItems or TopoloAccountMenu"
3261
+ ],
3262
+ content: appTsx(application, target)
2802
3263
  });
2803
3264
  }
2804
3265
  if (options.api) {
@@ -2807,9 +3268,10 @@ function scaffoldFiles(options) {
2807
3268
  path: `${base}/wrangler.toml`,
2808
3269
  purpose: "Cloudflare Worker runtime configuration.",
2809
3270
  requiredSignals: [
2810
- "bindings match topolo.cloudcontrol.json",
3271
+ "bindings match topolo.cloudcontrol.json or topolo.app.json",
2811
3272
  "secrets are referenced by name only"
2812
- ]
3273
+ ],
3274
+ content: wranglerToml(application, target)
2813
3275
  },
2814
3276
  {
2815
3277
  path: `${base}/src/worker.ts`,
@@ -2818,15 +3280,26 @@ function scaffoldFiles(options) {
2818
3280
  "uses @topolo/auth-middleware or Auth validation for protected routes",
2819
3281
  "derives organization from the credential rather than client-supplied orgId",
2820
3282
  "emits or propagates X-Topolo-Request-Id and X-Topolo-Client"
2821
- ]
3283
+ ],
3284
+ content: workerTs(application, target)
2822
3285
  }
2823
3286
  );
2824
3287
  }
2825
3288
  return files;
2826
3289
  }
2827
- function scaffoldCommands(id) {
3290
+ function scaffoldCommands(id, target) {
3291
+ const scaffoldId = toSlug(id);
3292
+ if (target === "developer") {
3293
+ return [
3294
+ `topolo apps scaffold ${scaffoldId} --target developer --write --repo-root .`,
3295
+ `cd topolo-${scaffoldId}`,
3296
+ "npm install",
3297
+ "npm run typecheck",
3298
+ "npm run build"
3299
+ ];
3300
+ }
2828
3301
  return [
2829
- `topolo apps scaffold ${id} --json`,
3302
+ `topolo apps scaffold ${scaffoldId} --write --repo-root /Users/ashes/Projects/topolo --json`,
2830
3303
  "topolo apps requirements --json",
2831
3304
  `topolo apps audit ${id} --local --repo-root /Users/ashes/Projects/topolo --fail-on needs_review --json`,
2832
3305
  `topolo apps audit ${id} --local --repo-root /Users/ashes/Projects/topolo --fail-on partial --json`
@@ -2904,18 +3377,1235 @@ function topoloConformanceWorkflow(options) {
2904
3377
  ` node PlatformApplications/TopoloCli/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`
2905
3378
  ].join("\n");
2906
3379
  }
3380
+ function packageJson(application, target, options) {
3381
+ const dependencies = {};
3382
+ const devDependencies = {
3383
+ "@types/node": "^20.12.0",
3384
+ typescript: "^5.4.0",
3385
+ vite: "^5.4.0"
3386
+ };
3387
+ const scripts = {
3388
+ dev: "vite --host 0.0.0.0",
3389
+ build: "vite build",
3390
+ preview: "vite preview --host 0.0.0.0",
3391
+ typecheck: "tsc --noEmit"
3392
+ };
3393
+ if (options.browser) {
3394
+ dependencies.react = "^18.3.1";
3395
+ dependencies["react-dom"] = "^18.3.1";
3396
+ devDependencies["@types/react"] = "^18.3.3";
3397
+ devDependencies["@types/react-dom"] = "^18.3.0";
3398
+ devDependencies["@vitejs/plugin-react"] = "^4.7.0";
3399
+ if (target === "platform") {
3400
+ dependencies["@topolo/auth-client"] = topoloPackageVersion("@topolo/auth-client", target);
3401
+ dependencies["@topolo/ui-kit"] = topoloPackageVersion("@topolo/ui-kit", target);
3402
+ }
3403
+ }
3404
+ if (options.api) {
3405
+ if (target === "platform") {
3406
+ dependencies["@topolo/auth-middleware"] = topoloPackageVersion("@topolo/auth-middleware", target);
3407
+ dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
3408
+ }
3409
+ devDependencies.wrangler = "^4.40.2";
3410
+ scripts.deploy = "npm run build && wrangler deploy";
3411
+ }
3412
+ return `${json({
3413
+ name: application.packageName,
3414
+ version: "0.1.0",
3415
+ private: true,
3416
+ type: "module",
3417
+ scripts,
3418
+ dependencies,
3419
+ devDependencies
3420
+ })}
3421
+ `;
3422
+ }
3423
+ function topoloPackageVersion(packageName, target) {
3424
+ if (target === "developer") {
3425
+ if (packageName === "@topolo/auth-client") return "^1.0.0";
3426
+ return "^0.1.0";
3427
+ }
3428
+ const folder = packageName === "@topolo/sdk" ? "topolo-sdk" : packageName.replace("@topolo/", "topolo-");
3429
+ return `file:../packages/${folder}`;
3430
+ }
3431
+ function readme(application, target, options) {
3432
+ const displayName = toTitle(application.name);
3433
+ const targetLabel = target === "platform" ? "first-party platform application" : "third-party Topolo application";
3434
+ const lines = [
3435
+ `# ${displayName}`,
3436
+ "",
3437
+ `Generated ${targetLabel} scaffold for Topolo.`,
3438
+ "",
3439
+ "## Development",
3440
+ "",
3441
+ "```bash",
3442
+ "npm install",
3443
+ "npm run typecheck",
3444
+ "npm run build",
3445
+ "npm run dev",
3446
+ "```",
3447
+ ""
3448
+ ];
3449
+ if (target === "platform") {
3450
+ lines.push(
3451
+ "## Shared Topolo Surface",
3452
+ "",
3453
+ "- `@topolo/ui-kit` provides the shared shell, launcher, account menu, command palette, and bug reporter.",
3454
+ "- `@topolo/auth-client` owns browser auth, callback exchange, token refresh, and logout.",
3455
+ "- 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.",
3456
+ "- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
3457
+ );
3458
+ if (options.api) {
3459
+ lines.push("- `@topolo/auth-middleware` validates Worker requests through Topolo Auth.");
3460
+ }
3461
+ } else {
3462
+ lines.push(
3463
+ "## Topolo Developer Surface",
3464
+ "",
3465
+ "- `topolo.app.json` declares callback URLs, requested scopes, and runtime shape for Topolo Developers.",
3466
+ "- Browser sign-in uses the public Topolo OAuth authorization-code + PKCE flow.",
3467
+ "- The starter intentionally avoids private first-party packages so it can install outside the Topolo monorepo."
3468
+ );
3469
+ }
3470
+ if (target === "platform") {
3471
+ lines.push(
3472
+ "",
3473
+ "## Platform Registration",
3474
+ "",
3475
+ "This scaffold includes CloudControl metadata, TopoloDocs system coverage, and a Topolo app conformance workflow."
3476
+ );
3477
+ } else {
3478
+ lines.push(
3479
+ "",
3480
+ "## Developer Registration",
3481
+ "",
3482
+ "`topolo.app.json` is the app manifest used to register callback URLs, requested scopes, and local development metadata with Topolo."
3483
+ );
3484
+ }
3485
+ return `${lines.join("\n")}
3486
+ `;
3487
+ }
3488
+ function tsconfigJson(api) {
3489
+ return `${json({
3490
+ compilerOptions: {
3491
+ target: "ES2022",
3492
+ useDefineForClassFields: true,
3493
+ lib: api ? ["ES2022", "DOM", "DOM.Iterable", "WebWorker"] : ["ES2022", "DOM", "DOM.Iterable"],
3494
+ allowJs: false,
3495
+ skipLibCheck: true,
3496
+ esModuleInterop: true,
3497
+ allowSyntheticDefaultImports: true,
3498
+ strict: true,
3499
+ forceConsistentCasingInFileNames: true,
3500
+ module: "ESNext",
3501
+ moduleResolution: "Bundler",
3502
+ resolveJsonModule: true,
3503
+ isolatedModules: true,
3504
+ noEmit: true,
3505
+ jsx: "react-jsx",
3506
+ types: ["vite/client"]
3507
+ },
3508
+ include: ["src"],
3509
+ references: []
3510
+ })}
3511
+ `;
3512
+ }
3513
+ function gitignore() {
3514
+ return [
3515
+ "node_modules/",
3516
+ "dist/",
3517
+ ".wrangler/",
3518
+ ".env",
3519
+ ".env.*",
3520
+ "!.env.example",
3521
+ ""
3522
+ ].join("\n");
3523
+ }
3524
+ function envDts(api) {
3525
+ const lines = [
3526
+ '/// <reference types="vite/client" />',
3527
+ "",
3528
+ "interface ImportMetaEnv {",
3529
+ " readonly VITE_TOPOLO_AUTH_URL?: string;",
3530
+ " readonly VITE_TOPOLO_CLIENT_ID?: string;",
3531
+ " readonly VITE_TOPOLO_REDIRECT_URI?: string;",
3532
+ " readonly VITE_TOPOLO_SCOPES?: string;",
3533
+ " readonly VITE_TOPOLO_SERVICE_ID?: string;"
3534
+ ];
3535
+ if (api) {
3536
+ lines.push(" readonly VITE_TOPOLO_API_URL?: string;");
3537
+ }
3538
+ lines.push(
3539
+ "}",
3540
+ "",
3541
+ "interface ImportMeta {",
3542
+ " readonly env: ImportMetaEnv;",
3543
+ "}",
3544
+ ""
3545
+ );
3546
+ return lines.join("\n");
3547
+ }
3548
+ function viteConfig() {
3549
+ return [
3550
+ "import { defineConfig } from 'vite';",
3551
+ "import react from '@vitejs/plugin-react';",
3552
+ "",
3553
+ "export default defineConfig({",
3554
+ " plugins: [react()],",
3555
+ " resolve: {",
3556
+ " dedupe: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],",
3557
+ " },",
3558
+ " build: { outDir: 'dist' },",
3559
+ "});",
3560
+ ""
3561
+ ].join("\n");
3562
+ }
3563
+ function indexHtml(name) {
3564
+ return [
3565
+ "<!doctype html>",
3566
+ '<html lang="en">',
3567
+ " <head>",
3568
+ ' <meta charset="UTF-8" />',
3569
+ ' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
3570
+ ` <title>${escapeHtml(toTitle(name))}</title>`,
3571
+ " </head>",
3572
+ " <body>",
3573
+ ' <div id="root"></div>',
3574
+ ' <script type="module" src="/src/main.tsx"></script>',
3575
+ " </body>",
3576
+ "</html>",
3577
+ ""
3578
+ ].join("\n");
3579
+ }
3580
+ function mainTsx() {
3581
+ return [
3582
+ "import React from 'react';",
3583
+ "import { createRoot } from 'react-dom/client';",
3584
+ "import App from './App';",
3585
+ "import './styles.css';",
3586
+ "",
3587
+ "const root = document.getElementById('root');",
3588
+ "",
3589
+ "if (!root) {",
3590
+ " throw new Error('Missing #root element.');",
3591
+ "}",
3592
+ "",
3593
+ "createRoot(root).render(",
3594
+ " <React.StrictMode>",
3595
+ " <App />",
3596
+ " </React.StrictMode>,",
3597
+ ");",
3598
+ ""
3599
+ ].join("\n");
3600
+ }
3601
+ function stylesCss() {
3602
+ return [
3603
+ ":root {",
3604
+ " color-scheme: light dark;",
3605
+ " font-family:",
3606
+ ' Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;',
3607
+ " background: #f7f8fb;",
3608
+ " color: #182032;",
3609
+ "}",
3610
+ "",
3611
+ "* {",
3612
+ " box-sizing: border-box;",
3613
+ "}",
3614
+ "",
3615
+ "body {",
3616
+ " margin: 0;",
3617
+ " background: #f7f8fb;",
3618
+ "}",
3619
+ "",
3620
+ "button,",
3621
+ "input,",
3622
+ "select,",
3623
+ "textarea {",
3624
+ " font: inherit;",
3625
+ "}",
3626
+ "",
3627
+ ".topolo-starter-workspace,",
3628
+ ".topolo-starter-card-stack {",
3629
+ " display: grid;",
3630
+ " gap: 18px;",
3631
+ "}",
3632
+ "",
3633
+ ".topolo-starter-card h2 {",
3634
+ " margin: 0 0 12px;",
3635
+ " font-size: 24px;",
3636
+ " line-height: 1.2;",
3637
+ "}",
3638
+ "",
3639
+ ".topolo-starter-card p {",
3640
+ " margin: 0;",
3641
+ " color: #526071;",
3642
+ " line-height: 1.55;",
3643
+ "}",
3644
+ "",
3645
+ ".topolo-starter-card {",
3646
+ " border: 1px solid rgba(24, 32, 50, 0.12);",
3647
+ " border-radius: 8px;",
3648
+ " padding: 20px;",
3649
+ " background: #ffffff;",
3650
+ "}",
3651
+ ""
3652
+ ].join("\n");
3653
+ }
3654
+ function appTsx(application, target) {
3655
+ if (target === "developer") return developerAppTsx(application);
3656
+ return platformAppTsx(application);
3657
+ }
3658
+ function platformAppTsx(application) {
3659
+ const appName = toTitle(application.name);
3660
+ const serviceId = application.serviceId ?? application.id;
3661
+ return [
3662
+ "import { useEffect, useMemo, useState } from 'react';",
3663
+ "import { createBrowserAuthModule, type AuthUser } from '@topolo/auth-client';",
3664
+ "import { LandingPage, LoginPage, TopoloAppShell, TopoloAuthFlowScreen, TopoloLoadingScreen, type TopoloAppShellUser } from '@topolo/ui-kit';",
3665
+ "",
3666
+ "type NavItem = 'home';",
3667
+ "type AppRoute = { kind: 'root' } | { kind: 'login' } | { kind: 'callback' };",
3668
+ "",
3669
+ `const APP_ID = '${application.id}';`,
3670
+ `const APP_NAME = '${escapeSingleQuoted(appName)}';`,
3671
+ `const DEFAULT_SERVICE_ID = '${serviceId}';`,
3672
+ "const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? 'https://auth.topolo.app';",
3673
+ "const SERVICE_ID = import.meta.env.VITE_TOPOLO_SERVICE_ID ?? DEFAULT_SERVICE_ID;",
3674
+ "const LANDING_CONFIG = {",
3675
+ " app_name: APP_NAME,",
3676
+ " app_tagline: 'Topolo workspace',",
3677
+ " hero_title: `Open ${APP_NAME} in Topolo`,",
3678
+ " hero_subtitle: 'Shared landing, login, callback, auth, and workspace shell wiring are already included in the generated baseline.',",
3679
+ " hero_cta_text: 'Sign in',",
3680
+ " hero_cta_url: '/login',",
3681
+ " cta_title: 'Start from the shared platform entry flow',",
3682
+ " cta_subtitle: 'Unauthenticated routes stay on the shared Topolo surfaces before users enter the signed workspace shell.',",
3683
+ "};",
3684
+ "const LOGIN_CONFIG = {",
3685
+ " app_name: APP_NAME,",
3686
+ " app_tagline: 'Topolo workspace',",
3687
+ " hero_title: `Sign in to ${APP_NAME}`,",
3688
+ " hero_subtitle: 'Use Topolo Auth to open the shared workspace shell for this application.',",
3689
+ " authenticated_home_path: '/',",
3690
+ "};",
3691
+ "",
3692
+ "const auth = createBrowserAuthModule({",
3693
+ " authServiceUrl: AUTH_URL,",
3694
+ " serviceId: SERVICE_ID,",
3695
+ " devSessionKey: `${SERVICE_ID}_session`,",
3696
+ " devSessionStorageEnabled: import.meta.env.DEV,",
3697
+ " callbackPath: '/auth/callback',",
3698
+ " debug: import.meta.env.DEV,",
3699
+ "});",
3700
+ "",
3701
+ "function getAccessToken(): string | null {",
3702
+ " return auth.accessTokenMemory.get() ?? auth.accessTokenSession.get();",
3703
+ "}",
3704
+ "",
3705
+ "function toShellUser(user: AuthUser | null): TopoloAppShellUser | null {",
3706
+ " if (!user) return null;",
3707
+ " return {",
3708
+ " name: user.name ?? user.email,",
3709
+ " email: user.email,",
3710
+ " role: user.role,",
3711
+ " organizationName: user.organization?.name ?? user.orgSlug ?? null,",
3712
+ " organizationId: user.organization?.id ?? user.orgId ?? null,",
3713
+ " };",
3714
+ "}",
3715
+ "",
3716
+ "export default function App() {",
3717
+ " const route = getRoute();",
3718
+ " const [user, setUser] = useState<AuthUser | null>(null);",
3719
+ " const [loading, setLoading] = useState(true);",
3720
+ " const [authError, setAuthError] = useState('');",
3721
+ " const [loginAttempted, setLoginAttempted] = useState(false);",
3722
+ "",
3723
+ " useEffect(() => {",
3724
+ " let active = true;",
3725
+ " const syncAuth = async () => {",
3726
+ " setLoading(true);",
3727
+ " setAuthError('');",
3728
+ "",
3729
+ " if (route.kind === 'callback') {",
3730
+ " const result = await auth.authClient.handleAuthCallbackAsync(window.location.href);",
3731
+ " if (!active) return;",
3732
+ " if (!result.authenticated) {",
3733
+ " setAuthError(result.error || 'Authentication failed.');",
3734
+ " setLoading(false);",
3735
+ " return;",
3736
+ " }",
3737
+ " setUser(result.user ?? null);",
3738
+ " window.location.replace('/');",
3739
+ " return;",
3740
+ " }",
3741
+ "",
3742
+ " const nextUser = await auth.authClient.initialize();",
3743
+ " if (!active) return;",
3744
+ " setUser(nextUser);",
3745
+ " setLoading(false);",
3746
+ " };",
3747
+ "",
3748
+ " void syncAuth().catch((error) => {",
3749
+ " if (!active) return;",
3750
+ " setAuthError(errorMessage(error));",
3751
+ " setLoading(false);",
3752
+ " });",
3753
+ " return () => {",
3754
+ " active = false;",
3755
+ " };",
3756
+ " }, [route.kind]);",
3757
+ "",
3758
+ " useEffect(() => {",
3759
+ " if (!loading && user && route.kind === 'login') {",
3760
+ " window.location.replace('/');",
3761
+ " }",
3762
+ " }, [loading, route.kind, user]);",
3763
+ "",
3764
+ " const shellUser = useMemo(() => toShellUser(user), [user]);",
3765
+ "",
3766
+ " if (route.kind === 'callback') {",
3767
+ " if (loading) {",
3768
+ " return (",
3769
+ " <TopoloLoadingScreen",
3770
+ " appId={APP_ID}",
3771
+ " serviceId={SERVICE_ID}",
3772
+ " appName={APP_NAME}",
3773
+ ' subtitle="Completing secure sign-in..."',
3774
+ " />",
3775
+ " );",
3776
+ " }",
3777
+ "",
3778
+ " return (",
3779
+ " <TopoloAuthFlowScreen",
3780
+ " appName={APP_NAME}",
3781
+ " appId={APP_ID}",
3782
+ " serviceId={SERVICE_ID}",
3783
+ ' status="error"',
3784
+ ' title="Authentication failed"',
3785
+ " message={authError || `We could not complete sign-in for ${APP_NAME}.`}",
3786
+ ' actionLabel="Back to sign in"',
3787
+ " onAction={() => window.location.replace('/login')}",
3788
+ " />",
3789
+ " );",
3790
+ " }",
3791
+ "",
3792
+ " if (loading) {",
3793
+ " return (",
3794
+ " <TopoloLoadingScreen",
3795
+ " appId={APP_ID}",
3796
+ " serviceId={SERVICE_ID}",
3797
+ " appName={APP_NAME}",
3798
+ " subtitle={`Loading ${APP_NAME}...`}",
3799
+ " />",
3800
+ " );",
3801
+ " }",
3802
+ "",
3803
+ " if (!user) {",
3804
+ " if (route.kind === 'login') {",
3805
+ " return (",
3806
+ " <LoginPage",
3807
+ " serviceId={SERVICE_ID}",
3808
+ " authApiUrl={AUTH_URL}",
3809
+ " config={LOGIN_CONFIG}",
3810
+ " isLoading={loginAttempted}",
3811
+ " onLogin={async (provider) => {",
3812
+ " setLoginAttempted(true);",
3813
+ " try {",
3814
+ " await auth.authClient.login(provider || 'topolo');",
3815
+ " } catch {",
3816
+ " setLoginAttempted(false);",
3817
+ " }",
3818
+ " }}",
3819
+ " onPasswordLogin={async (request) => {",
3820
+ " const result = await auth.authClient.loginWithPassword(request);",
3821
+ " if (!result.authenticated) {",
3822
+ " return result;",
3823
+ " }",
3824
+ " setUser(result.user ?? null);",
3825
+ " window.location.replace('/');",
3826
+ " return undefined;",
3827
+ " }}",
3828
+ " />",
3829
+ " );",
3830
+ " }",
3831
+ "",
3832
+ " return (",
3833
+ " <LandingPage",
3834
+ " serviceId={SERVICE_ID}",
3835
+ " authApiUrl={AUTH_URL}",
3836
+ " config={LANDING_CONFIG}",
3837
+ " onCTAClick={() => window.location.assign('/login')}",
3838
+ " />",
3839
+ " );",
3840
+ " }",
3841
+ "",
3842
+ " if (route.kind === 'login') {",
3843
+ " return (",
3844
+ " <TopoloLoadingScreen",
3845
+ " appId={APP_ID}",
3846
+ " serviceId={SERVICE_ID}",
3847
+ " appName={APP_NAME}",
3848
+ ' subtitle="Opening your workspace..."',
3849
+ " />",
3850
+ " );",
3851
+ " }",
3852
+ "",
3853
+ " return (",
3854
+ " <TopoloAppShell<NavItem>",
3855
+ " appId={APP_ID}",
3856
+ " serviceId={SERVICE_ID}",
3857
+ " appName={APP_NAME}",
3858
+ " user={shellUser}",
3859
+ ' activeNavItemId="home"',
3860
+ " navItems={[{ id: 'home', label: 'Workspace' }]}",
3861
+ " onNavItemSelect={() => undefined}",
3862
+ " eyebrow={APP_NAME}",
3863
+ ' title="Workspace"',
3864
+ " authUrl={AUTH_URL}",
3865
+ " getAccessToken={getAccessToken}",
3866
+ " onSignOut={async () => {",
3867
+ " await auth.authClient.logout();",
3868
+ " window.location.assign('/login');",
3869
+ " }}",
3870
+ " commandPlaceholder={`Search ${APP_NAME}`}",
3871
+ " bugReporterAppName={APP_NAME}",
3872
+ " enableCommandPalette",
3873
+ " enableBugReporter",
3874
+ " showThemeToggle",
3875
+ " >",
3876
+ ' <div className="topolo-starter-workspace">',
3877
+ ' <section className="topolo-starter-card">',
3878
+ " <h2>Shared workspace baseline</h2>",
3879
+ " <p>",
3880
+ " 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.",
3881
+ " </p>",
3882
+ " </section>",
3883
+ " </div>",
3884
+ " </TopoloAppShell>",
3885
+ " );",
3886
+ "}",
3887
+ "",
3888
+ "function getRoute(): AppRoute {",
3889
+ " const pathname = window.location.pathname.replace(/\\/+$/, '') || '/';",
3890
+ " if (pathname === '/login') return { kind: 'login' };",
3891
+ " if (pathname === '/auth/callback') return { kind: 'callback' };",
3892
+ " return { kind: 'root' };",
3893
+ "}",
3894
+ "",
3895
+ "function errorMessage(error: unknown): string {",
3896
+ " if (error instanceof Error && error.message) return error.message;",
3897
+ " return 'Unexpected authentication error.';",
3898
+ "}",
3899
+ ""
3900
+ ].join("\n");
3901
+ }
3902
+ function developerAppTsx(application) {
3903
+ const appName = toTitle(application.name);
3904
+ const defaultClientId = `${toSlug(application.id)}_local_client`;
3905
+ return [
3906
+ "import { useEffect, useMemo, useState } from 'react';",
3907
+ "",
3908
+ "type AuthState = 'ready' | 'missing_client' | 'authorizing' | 'authenticated' | 'error';",
3909
+ "",
3910
+ `const APP_NAME = '${escapeSingleQuoted(appName)}';`,
3911
+ "const AUTH_URL = import.meta.env.VITE_TOPOLO_AUTH_URL ?? 'https://auth.topolo.app';",
3912
+ `const CLIENT_ID = import.meta.env.VITE_TOPOLO_CLIENT_ID || '${defaultClientId}';`,
3913
+ "const REDIRECT_URI = import.meta.env.VITE_TOPOLO_REDIRECT_URI || `${window.location.origin}/auth/callback`;",
3914
+ "const REQUESTED_SCOPES = (import.meta.env.VITE_TOPOLO_SCOPES || 'identity:read organization:read').trim();",
3915
+ "",
3916
+ "export default function App() {",
3917
+ " const [authState, setAuthState] = useState<AuthState>(() => CLIENT_ID ? 'ready' : 'missing_client');",
3918
+ " const [message, setMessage] = useState<string | null>(null);",
3919
+ " const [tokenPreview, setTokenPreview] = useState<string | null>(null);",
3920
+ "",
3921
+ " const callbackCode = useMemo(() => new URLSearchParams(window.location.search).get('code'), []);",
3922
+ " const callbackError = useMemo(() => new URLSearchParams(window.location.search).get('error'), []);",
3923
+ "",
3924
+ " useEffect(() => {",
3925
+ " if (callbackError) {",
3926
+ " setAuthState('error');",
3927
+ " setMessage(callbackError);",
3928
+ " return;",
3929
+ " }",
3930
+ " if (!callbackCode) return;",
3931
+ "",
3932
+ " setAuthState('authorizing');",
3933
+ " void exchangeAuthorizationCode(callbackCode)",
3934
+ " .then((tokens) => {",
3935
+ " sessionStorage.setItem(`${CLIENT_ID}:topolo_access_token`, tokens.access_token);",
3936
+ " if (tokens.refresh_token) {",
3937
+ " sessionStorage.setItem(`${CLIENT_ID}:topolo_refresh_token`, tokens.refresh_token);",
3938
+ " }",
3939
+ " setTokenPreview(`${tokens.access_token.slice(0, 18)}...`);",
3940
+ " setAuthState('authenticated');",
3941
+ " window.history.replaceState({}, document.title, '/');",
3942
+ " })",
3943
+ " .catch((error: Error) => {",
3944
+ " setAuthState('error');",
3945
+ " setMessage(error.message);",
3946
+ " });",
3947
+ " }, [callbackCode, callbackError]);",
3948
+ "",
3949
+ " return (",
3950
+ ' <main className="topolo-starter-login">',
3951
+ ' <section className="topolo-starter-login__panel">',
3952
+ " <h1>{APP_NAME}</h1>",
3953
+ " <p>Public Topolo OAuth starter using authorization-code + PKCE.</p>",
3954
+ " {authState === 'missing_client' && (",
3955
+ " <p>Set VITE_TOPOLO_CLIENT_ID after registering this app in Topolo Developers.</p>",
3956
+ " )}",
3957
+ " {authState === 'authenticated' && (",
3958
+ " <p>Topolo sign-in complete. Access token: {tokenPreview}</p>",
3959
+ " )}",
3960
+ " {authState === 'error' && <p>{message}</p>}",
3961
+ ` <button type="button" disabled={!CLIENT_ID || authState === 'authorizing'} onClick={() => void startTopoloLogin()}>`,
3962
+ " {authState === 'authorizing' ? 'Completing sign-in' : 'Sign in with Topolo'}",
3963
+ " </button>",
3964
+ " </section>",
3965
+ " </main>",
3966
+ " );",
3967
+ "}",
3968
+ "",
3969
+ "async function startTopoloLogin(): Promise<void> {",
3970
+ " const verifier = randomString(64);",
3971
+ " const challenge = await pkceChallenge(verifier);",
3972
+ " const state = randomString(32);",
3973
+ " sessionStorage.setItem(`${CLIENT_ID}:topolo_pkce_verifier`, verifier);",
3974
+ " sessionStorage.setItem(`${CLIENT_ID}:topolo_oauth_state`, state);",
3975
+ "",
3976
+ " const authorizeUrl = new URL('/api/developer-oauth/authorize', AUTH_URL);",
3977
+ " authorizeUrl.searchParams.set('response_type', 'code');",
3978
+ " authorizeUrl.searchParams.set('client_id', CLIENT_ID);",
3979
+ " authorizeUrl.searchParams.set('redirect_uri', REDIRECT_URI);",
3980
+ " authorizeUrl.searchParams.set('scope', REQUESTED_SCOPES);",
3981
+ " authorizeUrl.searchParams.set('state', state);",
3982
+ " authorizeUrl.searchParams.set('code_challenge', challenge);",
3983
+ " authorizeUrl.searchParams.set('code_challenge_method', 'S256');",
3984
+ " window.location.assign(authorizeUrl.toString());",
3985
+ "}",
3986
+ "",
3987
+ "async function exchangeAuthorizationCode(code: string): Promise<{ access_token: string; refresh_token?: string }> {",
3988
+ " const params = new URLSearchParams(window.location.search);",
3989
+ " const expectedState = sessionStorage.getItem(`${CLIENT_ID}:topolo_oauth_state`);",
3990
+ " if (expectedState && params.get('state') !== expectedState) {",
3991
+ " throw new Error('OAuth state mismatch. Restart sign-in.');",
3992
+ " }",
3993
+ " const verifier = sessionStorage.getItem(`${CLIENT_ID}:topolo_pkce_verifier`);",
3994
+ " if (!verifier) throw new Error('Missing PKCE verifier. Restart sign-in.');",
3995
+ "",
3996
+ " const tokenUrl = new URL('/api/developer-oauth/token', AUTH_URL);",
3997
+ " const response = await fetch(tokenUrl, {",
3998
+ " method: 'POST',",
3999
+ " headers: { 'Content-Type': 'application/json' },",
4000
+ " body: JSON.stringify({",
4001
+ " grant_type: 'authorization_code',",
4002
+ " client_id: CLIENT_ID,",
4003
+ " code,",
4004
+ " redirect_uri: REDIRECT_URI,",
4005
+ " code_verifier: verifier,",
4006
+ " }),",
4007
+ " });",
4008
+ " const payload = await response.json().catch(() => null);",
4009
+ " if (!response.ok || !payload?.access_token) {",
4010
+ " throw new Error(payload?.error_description || payload?.error || 'Token exchange failed.');",
4011
+ " }",
4012
+ " return payload;",
4013
+ "}",
4014
+ "",
4015
+ "function randomString(length: number): string {",
4016
+ " const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';",
4017
+ " const bytes = crypto.getRandomValues(new Uint8Array(length));",
4018
+ " return Array.from(bytes, (byte) => alphabet[byte % alphabet.length]).join('');",
4019
+ "}",
4020
+ "",
4021
+ "async function pkceChallenge(verifier: string): Promise<string> {",
4022
+ " const data = new TextEncoder().encode(verifier);",
4023
+ " const digest = await crypto.subtle.digest('SHA-256', data);",
4024
+ " return base64Url(new Uint8Array(digest));",
4025
+ "}",
4026
+ "",
4027
+ "function base64Url(bytes: Uint8Array): string {",
4028
+ " let binary = '';",
4029
+ " for (const byte of bytes) binary += String.fromCharCode(byte);",
4030
+ " return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');",
4031
+ "}",
4032
+ ""
4033
+ ].join("\n");
4034
+ }
4035
+ function wranglerToml(application, target) {
4036
+ const slug = toSlug(application.id);
4037
+ const workerName = target === "platform" ? `topolo-${slug}` : application.packageName;
4038
+ const routeHost = application.productionUrl ? new URL(application.productionUrl).host : null;
4039
+ const lines = [
4040
+ `name = "${workerName}"`,
4041
+ 'main = "src/worker.ts"',
4042
+ `compatibility_date = "${CURRENT_VERIFICATION_DATE}"`,
4043
+ 'compatibility_flags = ["nodejs_compat"]'
4044
+ ];
4045
+ if (target === "platform") {
4046
+ lines.push(
4047
+ `account_id = "${CLOUDFLARE_ACCOUNT_ID}"`,
4048
+ "workers_dev = false"
4049
+ );
4050
+ }
4051
+ lines.push(
4052
+ "",
4053
+ "[observability]",
4054
+ "enabled = true",
4055
+ "",
4056
+ "[assets]",
4057
+ 'directory = "dist"',
4058
+ 'binding = "ASSETS"',
4059
+ 'not_found_handling = "single-page-application"',
4060
+ 'run_worker_first = ["/api/*"]'
4061
+ );
4062
+ if (target === "platform" && routeHost) {
4063
+ lines.push(
4064
+ "",
4065
+ "[[routes]]",
4066
+ `pattern = "${routeHost}"`,
4067
+ "custom_domain = true"
4068
+ );
4069
+ }
4070
+ lines.push(
4071
+ "",
4072
+ "[vars]",
4073
+ 'AUTH_BASE_URL = "https://auth.topolo.app"',
4074
+ `SERVICE_ID = "${application.serviceId ?? application.id}"`,
4075
+ ""
4076
+ );
4077
+ return lines.join("\n");
4078
+ }
4079
+ function workerTs(application, target) {
4080
+ if (target === "developer") return developerWorkerTs(application);
4081
+ return platformWorkerTs(application);
4082
+ }
4083
+ function platformWorkerTs(application) {
4084
+ const serviceId = application.serviceId ?? application.id;
4085
+ return [
4086
+ "import { verifyRequest } from '@topolo/auth-middleware';",
4087
+ "",
4088
+ "export interface Env {",
4089
+ " AUTH_BASE_URL?: string;",
4090
+ " SERVICE_ID?: string;",
4091
+ " TOPOLO_SERVICE_API_KEY?: string;",
4092
+ "}",
4093
+ "",
4094
+ "const CLIENT_HEADER = 'topolo-worker-starter';",
4095
+ "",
4096
+ "export default {",
4097
+ " async fetch(request: Request, env: Env): Promise<Response> {",
4098
+ " const url = new URL(request.url);",
4099
+ "",
4100
+ " if (url.pathname === '/api/health') {",
4101
+ " return json({ ok: true, service: env.SERVICE_ID ?? DEFAULT_SERVICE_ID }, { status: 200 }, request);",
4102
+ " }",
4103
+ "",
4104
+ " const auth = await verifyRequest(request, {",
4105
+ " authBaseUrl: env.AUTH_BASE_URL,",
4106
+ " serviceId: env.SERVICE_ID ?? DEFAULT_SERVICE_ID,",
4107
+ " serviceApiKey: env.TOPOLO_SERVICE_API_KEY,",
4108
+ " });",
4109
+ "",
4110
+ " if (!auth.ok) {",
4111
+ " return json({ error: auth.error }, { status: auth.status }, request);",
4112
+ " }",
4113
+ "",
4114
+ " if (url.pathname === '/api/bootstrap') {",
4115
+ " return json({",
4116
+ " user: auth.user,",
4117
+ " organization: auth.organization,",
4118
+ " }, { status: 200 }, request);",
4119
+ " }",
4120
+ "",
4121
+ " return json({ error: 'not_found' }, { status: 404 }, request);",
4122
+ " },",
4123
+ "};",
4124
+ "",
4125
+ `const DEFAULT_SERVICE_ID = '${serviceId}';`,
4126
+ "",
4127
+ "function json(body: unknown, init: ResponseInit, request: Request): Response {",
4128
+ " const headers = new Headers(init.headers);",
4129
+ " headers.set('Content-Type', 'application/json');",
4130
+ " headers.set('X-Topolo-Client', CLIENT_HEADER);",
4131
+ " headers.set('X-Topolo-Request-Id', request.headers.get('X-Topolo-Request-Id') ?? crypto.randomUUID());",
4132
+ " return new Response(JSON.stringify(body), { ...init, headers });",
4133
+ "}",
4134
+ ""
4135
+ ].join("\n");
4136
+ }
4137
+ function developerWorkerTs(application) {
4138
+ const serviceId = application.serviceId ?? application.id;
4139
+ return [
4140
+ "export interface Env {",
4141
+ " AUTH_BASE_URL?: string;",
4142
+ " SERVICE_ID?: string;",
4143
+ "}",
4144
+ "",
4145
+ "const CLIENT_HEADER = 'topolo-developer-starter';",
4146
+ `const DEFAULT_SERVICE_ID = '${serviceId}';`,
4147
+ "",
4148
+ "export default {",
4149
+ " async fetch(request: Request, env: Env): Promise<Response> {",
4150
+ " const url = new URL(request.url);",
4151
+ "",
4152
+ " if (url.pathname === '/api/health') {",
4153
+ " return json({ ok: true, service: env.SERVICE_ID ?? DEFAULT_SERVICE_ID }, { status: 200 }, request);",
4154
+ " }",
4155
+ "",
4156
+ " const auth = await verifyTopoloRequest(request, env);",
4157
+ " if (!auth.ok) {",
4158
+ " return json({ error: auth.error }, { status: auth.status }, request);",
4159
+ " }",
4160
+ "",
4161
+ " if (url.pathname === '/api/bootstrap') {",
4162
+ " return json({",
4163
+ " user: auth.user,",
4164
+ " organization: auth.organization,",
4165
+ " }, { status: 200 }, request);",
4166
+ " }",
4167
+ "",
4168
+ " return json({ error: 'not_found' }, { status: 404 }, request);",
4169
+ " },",
4170
+ "};",
4171
+ "",
4172
+ "async function verifyTopoloRequest(request: Request, env: Env): Promise<",
4173
+ " | { ok: true; user: unknown; organization: unknown }",
4174
+ " | { ok: false; status: number; error: string }",
4175
+ "> {",
4176
+ " const authHeader = request.headers.get('Authorization') || '';",
4177
+ " const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;",
4178
+ " if (!token) return { ok: false, status: 401, error: 'Bearer token required' };",
4179
+ "",
4180
+ " try {",
4181
+ " const authBaseUrl = env.AUTH_BASE_URL || 'https://auth.topolo.app';",
4182
+ " const response = await fetch(`${authBaseUrl}/validate`, {",
4183
+ " method: 'POST',",
4184
+ " headers: {",
4185
+ " Authorization: `Bearer ${token}`,",
4186
+ " 'X-Service-ID': env.SERVICE_ID ?? DEFAULT_SERVICE_ID,",
4187
+ " },",
4188
+ " });",
4189
+ "",
4190
+ " const payload = await response.json().catch(() => null);",
4191
+ " if (!response.ok) return { ok: false, status: 401, error: 'Token validation failed' };",
4192
+ " const user = payload?.data?.user;",
4193
+ " if (!user) return { ok: false, status: 401, error: 'Invalid auth response' };",
4194
+ " return { ok: true, user, organization: payload?.data?.organization ?? null };",
4195
+ " } catch {",
4196
+ " return { ok: false, status: 503, error: 'Topolo Auth unavailable' };",
4197
+ " }",
4198
+ "}",
4199
+ "",
4200
+ "function json(body: unknown, init: ResponseInit, request: Request): Response {",
4201
+ " const headers = new Headers(init.headers);",
4202
+ " headers.set('Content-Type', 'application/json');",
4203
+ " headers.set('X-Topolo-Client', CLIENT_HEADER);",
4204
+ " headers.set('X-Topolo-Request-Id', request.headers.get('X-Topolo-Request-Id') ?? crypto.randomUUID());",
4205
+ " return new Response(JSON.stringify(body), { ...init, headers });",
4206
+ "}",
4207
+ ""
4208
+ ].join("\n");
4209
+ }
4210
+ function cloudcontrolJson(application, options) {
4211
+ const slug = toSlug(application.id);
4212
+ const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
4213
+ const deployTargets = options.api ? [
4214
+ {
4215
+ kind: "worker",
4216
+ environment: "production",
4217
+ target_name: `topolo-${slug}`,
4218
+ cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
4219
+ config_path: "wrangler.toml",
4220
+ build_command: "npm run build",
4221
+ output_dir: "dist",
4222
+ production_branch: "main",
4223
+ healthcheck_path: "/api/health",
4224
+ domains: [host],
4225
+ routes: [host],
4226
+ api_url: application.productionUrl,
4227
+ service_id: application.serviceId
4228
+ }
4229
+ ] : [
4230
+ {
4231
+ kind: "pages",
4232
+ environment: "production",
4233
+ target_name: `topolo-${slug}`,
4234
+ cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID,
4235
+ config_path: null,
4236
+ build_command: "npm run build",
4237
+ output_dir: "dist",
4238
+ production_branch: "main",
4239
+ healthcheck_path: null,
4240
+ domains: [host],
4241
+ routes: [host],
4242
+ api_url: null
4243
+ }
4244
+ ];
4245
+ return `${json({
4246
+ name: `topolo-${slug}`,
4247
+ description: `Canonical documentation for ${toTitle(application.name)} lives in PlatformApplications/TopoloDocs.`,
4248
+ deploy_method: options.api ? "wrangler" : "cloudflare_pages",
4249
+ ci_provider: "github_actions",
4250
+ repo_identity: {
4251
+ provider: "github",
4252
+ owner: "Topolo-io",
4253
+ repo: `topolo-${slug}`,
4254
+ remote_url: `git@github.com:Topolo-io/topolo-${slug}.git`,
4255
+ default_branch: "main"
4256
+ },
4257
+ environments: application.productionUrl ? [
4258
+ {
4259
+ name: "production",
4260
+ url: application.productionUrl,
4261
+ branch: "main",
4262
+ status: "unknown"
4263
+ }
4264
+ ] : [],
4265
+ deploy_targets: deployTargets,
4266
+ resource_bindings: [],
4267
+ ci_workflows: [
4268
+ {
4269
+ provider: "github_actions",
4270
+ workflow_name: "topolo-conformance.yml",
4271
+ path: ".github/workflows/topolo-conformance.yml",
4272
+ branch: null,
4273
+ required: true
4274
+ }
4275
+ ],
4276
+ ci_targets: [],
4277
+ tags: ["application", "cloudflare", "typescript"],
4278
+ cloudflare_account_id: CLOUDFLARE_ACCOUNT_ID
4279
+ })}
4280
+ `;
4281
+ }
4282
+ function systemJson(application, options) {
4283
+ const slug = toSlug(application.id);
4284
+ return `${json({
4285
+ id: `topolo-${slug}`,
4286
+ name: toTitle(application.name),
4287
+ slug: `topolo-${slug}`,
4288
+ kind: "application",
4289
+ summary: `${toTitle(application.name)} is a first-party Topolo application scaffolded on shared Auth, UI Kit, CloudControl, and conformance contracts.`,
4290
+ lifecycle: "planned",
4291
+ last_verified: CURRENT_VERIFICATION_DATE,
4292
+ owners: ["platform-engineering"],
4293
+ repo_paths: [`PlatformApplications/${application.appDir}`],
4294
+ service_ids: options.api && application.serviceId ? [application.serviceId] : [],
4295
+ visibility: "internal",
4296
+ api_contract: {
4297
+ type: options.api ? "worker" : "none",
4298
+ source: options.api ? `PlatformApplications/${application.appDir}/src/worker.ts` : `PlatformApplications/${application.appDir}/src/App.tsx`,
4299
+ 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."
4300
+ },
4301
+ primary_hosts: application.productionUrl ? [application.productionUrl] : [],
4302
+ doc_paths: [`internal/apps/${slug}`, `public/applications/${slug}`],
4303
+ security_assurance: {
4304
+ risk_tier: "internal",
4305
+ auth_boundary: "Topolo Auth through @topolo/auth-client for browser sessions and @topolo/auth-middleware for Worker requests.",
4306
+ tenant_isolation: "organization_scoped",
4307
+ external_inputs: ["browser", ...options.api ? ["api"] : []],
4308
+ sensitive_data: ["identity", "org_data"],
4309
+ security_review_status: "not_started",
4310
+ last_security_review: CURRENT_VERIFICATION_DATE,
4311
+ pentest_status: "not_started",
4312
+ evidence_doc: `internal/apps/${slug}`
4313
+ },
4314
+ dependencies: ["topolo-auth", "topolo-platform"]
4315
+ })}
4316
+ `;
4317
+ }
4318
+ function internalDocs(application, options) {
4319
+ const slug = toSlug(application.id);
4320
+ const title = toTitle(application.name);
4321
+ return [
4322
+ "---",
4323
+ `title: "${escapeFrontmatter(title)} Handbook"`,
4324
+ `summary: "Internal handbook for ${escapeFrontmatter(title)}."`,
4325
+ 'owners: ["platform-engineering"]',
4326
+ 'status: "planned"',
4327
+ `last_verified: "${CURRENT_VERIFICATION_DATE}"`,
4328
+ "source_repos:",
4329
+ ` - "PlatformApplications/${application.appDir}"`,
4330
+ ' - "PlatformApplications/TopoloDocs"',
4331
+ 'audience: "internal"',
4332
+ "tags:",
4333
+ ` - "${slug}"`,
4334
+ ' - "application"',
4335
+ "system_ids:",
4336
+ ` - "topolo-${slug}"`,
4337
+ ' - "topolo-auth"',
4338
+ "related_docs:",
4339
+ ` - "/systems/topolo-${slug}"`,
4340
+ ` - "/applications/${slug}"`,
4341
+ "---",
4342
+ "",
4343
+ "## What It Is",
4344
+ "",
4345
+ `${title} is a first-party Topolo application scaffolded from the shared platform application contract.`,
4346
+ "",
4347
+ "## Architecture",
4348
+ "",
4349
+ "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.",
4350
+ "",
4351
+ "## Runtime Surfaces",
4352
+ "",
4353
+ `- \`PlatformApplications/${application.appDir}/src/App.tsx\``,
4354
+ "- public landing route: `/`",
4355
+ "- public sign-in route: `/login`",
4356
+ "- auth callback route: `/auth/callback`",
4357
+ ...options.api ? [`- \`PlatformApplications/${application.appDir}/src/worker.ts\``] : [],
4358
+ `- \`PlatformApplications/${application.appDir}/topolo.cloudcontrol.json\``,
4359
+ ...options.api && application.serviceId ? [`- \`PlatformApplications/TopoloAuth/catalog/service-manifests/${application.serviceId}.json\``] : [],
4360
+ "",
4361
+ "## API Reference",
4362
+ "",
4363
+ 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.",
4364
+ "",
4365
+ "## Auth and Permissions",
4366
+ "",
4367
+ 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.",
4368
+ "",
4369
+ "## Data Ownership",
4370
+ "",
4371
+ "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 organization at the backend boundary.",
4372
+ "",
4373
+ "## Deployments",
4374
+ "",
4375
+ 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.",
4376
+ "",
4377
+ "## Failure Modes",
4378
+ "",
4379
+ "- Auth service registration drifts from the generated Worker `SERVICE_ID`.",
4380
+ "- CloudControl metadata does not match the eventual Cloudflare project or route.",
4381
+ "- linked Topolo packages resolve a second React runtime and the browser shell renders blank instead of mounting the shared surfaces.",
4382
+ "- Domain data is added without an organization-scoped backend boundary.",
4383
+ "",
4384
+ "## Debugging",
4385
+ "",
4386
+ "- run `npm run typecheck` and `npm run build` in the application directory",
4387
+ "- run `topolo apps audit <app> --local --repo-root /Users/ashes/Projects/topolo --fail-on needs_review --json` after SDK catalog registration",
4388
+ "- inspect `topolo.cloudcontrol.json` before deployment changes",
4389
+ ...options.api && application.serviceId ? [`- inspect \`PlatformApplications/TopoloAuth/catalog/service-manifests/${application.serviceId}.json\` when permission grants fail`] : [],
4390
+ "",
4391
+ "## Change Log / Verification",
4392
+ "",
4393
+ `- Scaffold generated on ${CURRENT_VERIFICATION_DATE} from \`topolo apps scaffold\`.`,
4394
+ "- `npm run typecheck`",
4395
+ "- `npm run build`",
4396
+ "- `npm run validate` in TopoloDocs",
4397
+ ""
4398
+ ].join("\n");
4399
+ }
4400
+ function publicDocs(application, options) {
4401
+ const slug = toSlug(application.id);
4402
+ const title = toTitle(application.name);
4403
+ return [
4404
+ "---",
4405
+ `title: "${escapeFrontmatter(title)}"`,
4406
+ `summary: "Public overview of ${escapeFrontmatter(title)} in the Topolo application suite."`,
4407
+ 'owners: ["platform-engineering"]',
4408
+ 'status: "planned"',
4409
+ `last_verified: "${CURRENT_VERIFICATION_DATE}"`,
4410
+ "source_repos:",
4411
+ ` - "PlatformApplications/${application.appDir}"`,
4412
+ 'audience: "public"',
4413
+ "tags:",
4414
+ ` - "${slug}"`,
4415
+ ' - "application"',
4416
+ "system_ids:",
4417
+ ` - "topolo-${slug}"`,
4418
+ ' - "topolo-auth"',
4419
+ "related_docs:",
4420
+ ` - "/systems/topolo-${slug}"`,
4421
+ ` - "/internal/apps/${slug}"`,
4422
+ "---",
4423
+ "",
4424
+ "## What It Is",
4425
+ "",
4426
+ `${title} is part of the Topolo business application suite.`,
4427
+ "",
4428
+ "## Architecture",
4429
+ "",
4430
+ "The application uses the shared Topolo landing page, shared login page, auth callback handling, and shared Topolo shell from the outset.",
4431
+ "",
4432
+ "## Runtime Surfaces",
4433
+ "",
4434
+ 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.",
4435
+ "",
4436
+ "## API Reference",
4437
+ "",
4438
+ 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.",
4439
+ "",
4440
+ "## Auth and Permissions",
4441
+ "",
4442
+ 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.",
4443
+ "",
4444
+ "## Data Ownership",
4445
+ "",
4446
+ "The generated baseline has no durable domain data yet. Future data must be organization-scoped at the backend boundary.",
4447
+ "",
4448
+ "## Deployments",
4449
+ "",
4450
+ application.productionUrl ? `The planned production host is \`${application.productionUrl}\`.` : "The production host has not been assigned yet.",
4451
+ "",
4452
+ "## Failure Modes",
4453
+ "",
4454
+ "- service registration and app routing can drift while the product is still planned",
4455
+ "- organization-scoped data models are not implemented until domain work begins",
4456
+ "",
4457
+ "## Debugging",
4458
+ "",
4459
+ "- use the matching internal handbook for operational details",
4460
+ "- verify the system registry entry before deployment or service-registration changes",
4461
+ "",
4462
+ "## Change Log / Verification",
4463
+ "",
4464
+ `- Scaffold generated on ${CURRENT_VERIFICATION_DATE} from \`topolo apps scaffold\`.`,
4465
+ ""
4466
+ ].join("\n");
4467
+ }
4468
+ function serviceManifestJson(application) {
4469
+ const slug = toSlug(application.id);
4470
+ const title = toTitle(application.name);
4471
+ const permissionPrefix = application.id.replace(/[^a-z0-9_]/g, "_");
4472
+ return `${json({
4473
+ serviceId: application.serviceId,
4474
+ systems: [
4475
+ {
4476
+ systemId: `topolo-${slug}`,
4477
+ name: title,
4478
+ repoPaths: [`PlatformApplications/${application.appDir}`],
4479
+ docPaths: [`applications/${slug}`, `internal/apps/${slug}`]
4480
+ }
4481
+ ],
4482
+ permissions: [
4483
+ {
4484
+ id: `perm_${permissionPrefix}_workspace_read`,
4485
+ name: "workspace:read",
4486
+ description: `View ${title} workspace data`,
4487
+ resourcePattern: null
4488
+ },
4489
+ {
4490
+ id: `perm_${permissionPrefix}_workspace_write`,
4491
+ name: "workspace:write",
4492
+ description: `Manage ${title} workspace data`,
4493
+ resourcePattern: null
4494
+ }
4495
+ ],
4496
+ apiKeyScopes: [
4497
+ {
4498
+ id: `aks_${permissionPrefix}_workspace_read`,
4499
+ name: "workspace.read",
4500
+ description: `View ${title} workspace data`,
4501
+ resourcePattern: null
4502
+ },
4503
+ {
4504
+ id: `aks_${permissionPrefix}_workspace_write`,
4505
+ name: "workspace.write",
4506
+ description: `Manage ${title} workspace data`,
4507
+ resourcePattern: null
4508
+ }
4509
+ ],
4510
+ roleBundles: {
4511
+ admin: ["workspace:read", "workspace:write"],
4512
+ member: ["workspace:read", "workspace:write"],
4513
+ guest: ["workspace:read"],
4514
+ super_admin: ["workspace:read", "workspace:write"]
4515
+ }
4516
+ })}
4517
+ `;
4518
+ }
4519
+ function developerManifest(application, options) {
4520
+ return `${json({
4521
+ schema: "https://topolo.app/schemas/topolo-app-manifest.v1.json",
4522
+ id: application.id,
4523
+ name: toTitle(application.name),
4524
+ type: "third_party",
4525
+ package_name: application.packageName,
4526
+ service_id: options.api ? application.serviceId : null,
4527
+ auth: {
4528
+ provider: "topolo",
4529
+ flow: "authorization_code_pkce",
4530
+ callback_path: "/auth/callback",
4531
+ local_callback_url: "http://localhost:5173/auth/callback",
4532
+ production_callback_url: application.productionUrl ? `${application.productionUrl.replace(/\/$/, "")}/auth/callback` : null,
4533
+ requested_scopes: options.api ? ["identity:read", "organization:read", `${application.id}:read`] : ["identity:read", "organization:read"]
4534
+ },
4535
+ runtime: {
4536
+ browser: true,
4537
+ api: options.api
4538
+ }
4539
+ })}
4540
+ `;
4541
+ }
4542
+ function envExample(application) {
4543
+ return [
4544
+ "VITE_TOPOLO_AUTH_URL=https://auth.topolo.app",
4545
+ "VITE_TOPOLO_CLIENT_ID=",
4546
+ "VITE_TOPOLO_REDIRECT_URI=http://localhost:5173/auth/callback",
4547
+ "VITE_TOPOLO_SCOPES=identity:read organization:read",
4548
+ `VITE_TOPOLO_SERVICE_ID=${application.serviceId ?? application.id}`,
4549
+ ""
4550
+ ].join("\n");
4551
+ }
4552
+ function normalizeTarget(value) {
4553
+ if (!value) return "platform";
4554
+ if (value === "platform" || value === "developer") return value;
4555
+ throw new Error("Invalid scaffold target. Use platform or developer.");
4556
+ }
2907
4557
  function normalizeApplicationId(application) {
2908
- const id = application.trim().toLowerCase();
2909
- if (Object.prototype.hasOwnProperty.call(APPLICATIONS, id)) {
2910
- return id;
4558
+ const raw = application.trim().toLowerCase();
4559
+ const normalized = raw.replace(/-/g, "_");
4560
+ if (Object.prototype.hasOwnProperty.call(APPLICATIONS, raw)) {
4561
+ return raw;
2911
4562
  }
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.");
4563
+ if (Object.prototype.hasOwnProperty.call(APPLICATIONS, normalized)) {
4564
+ return normalized;
2914
4565
  }
2915
- return id;
4566
+ if (/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(raw) || /^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/.test(raw)) {
4567
+ return normalized;
4568
+ }
4569
+ throw new Error(
4570
+ "Application id must be kebab-case or snake_case using lowercase letters, numbers, hyphens, and underscores."
4571
+ );
4572
+ }
4573
+ function toSlug(value) {
4574
+ return value.replace(/_/g, "-");
2916
4575
  }
2917
4576
  function toPascalCase(value) {
2918
- return value.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
4577
+ return value.split(/[-_]/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
4578
+ }
4579
+ function toTitle(value) {
4580
+ return value.replace(/^topolo[-_\s]*/i, "Topolo ").split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
4581
+ }
4582
+ function json(value) {
4583
+ return JSON.stringify(value, null, 2);
4584
+ }
4585
+ async function pathExists(path2) {
4586
+ try {
4587
+ await access(path2, constants.F_OK);
4588
+ return true;
4589
+ } catch {
4590
+ return false;
4591
+ }
4592
+ }
4593
+ function resolveInside(root, filePath) {
4594
+ const resolved = resolve(root, filePath);
4595
+ const relativePath = relative(root, resolved);
4596
+ if (relativePath.startsWith("..") || relativePath === "" || relativePath.startsWith("/")) {
4597
+ throw new Error(`Scaffold file path escapes the target root: ${filePath}`);
4598
+ }
4599
+ return resolved;
4600
+ }
4601
+ function escapeHtml(value) {
4602
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4603
+ }
4604
+ function escapeSingleQuoted(value) {
4605
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
4606
+ }
4607
+ function escapeFrontmatter(value) {
4608
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2919
4609
  }
2920
4610
 
2921
4611
  // src/commands/apps.ts
@@ -2980,20 +4670,30 @@ function registerAppsCommand(program2) {
2980
4670
  });
2981
4671
  apps.command("scaffold <application>").description(
2982
4672
  "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) => {
4673
+ ).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("--target <target>", "Scaffold target: platform or developer").option("--developer", "Shortcut for --target developer").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("--workflow-only", "Output only the generated GitHub Actions workflow in human mode").action(async (application, opts) => {
2984
4674
  const mode = opts.json === false || process.argv.includes("--no-json") ? "pretty" : resolveOutputMode(opts.json);
2985
4675
  try {
4676
+ const target = resolveScaffoldTarget(opts.target, opts.developer);
4677
+ if (opts.workflowOnly && opts.write) {
4678
+ throw new Error("--workflow-only cannot be used with --write.");
4679
+ }
4680
+ const api = resolveScaffoldApiOption(opts.api, opts.withApi);
2986
4681
  const payload = applicationScaffoldPayload(application, {
2987
4682
  appDir: opts.appDir,
2988
4683
  name: opts.name,
2989
4684
  packageName: opts.packageName,
2990
4685
  productionUrl: opts.productionUrl,
2991
4686
  service: opts.service,
2992
- api: opts.api,
2993
- browser: opts.browser
4687
+ api,
4688
+ browser: opts.browser,
4689
+ target
2994
4690
  });
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));
4691
+ if (opts.workflowOnly && !payload.ciWorkflow) {
4692
+ throw new Error("--workflow-only is only available for platform scaffolds.");
4693
+ }
4694
+ const writeResult = opts.write ? await writeApplicationScaffold(payload, { repoRoot: opts.repoRoot, force: opts.force }) : null;
4695
+ const output = opts.workflowOnly ? { application: payload.application, ciWorkflowPath: payload.ciWorkflowPath, ciWorkflow: payload.ciWorkflow } : writeResult ? { ...payload, write: writeResult } : payload;
4696
+ emit(output, mode, () => opts.workflowOnly ? payload.ciWorkflow ?? "" : formatScaffold(payload, writeResult));
2997
4697
  } catch (err) {
2998
4698
  fail(err, mode);
2999
4699
  }
@@ -3154,9 +4854,10 @@ function formatAudit(payload) {
3154
4854
  }
3155
4855
  return lines.join("\n");
3156
4856
  }
3157
- function formatScaffold(payload) {
4857
+ function formatScaffold(payload, writeResult = null) {
3158
4858
  const lines = [
3159
4859
  `Version: ${payload.version}`,
4860
+ `Target: ${payload.target}`,
3160
4861
  `Application: ${payload.application.id}`,
3161
4862
  `Directory: ${payload.application.appDir}`,
3162
4863
  `Package: ${payload.application.packageName}`,
@@ -3177,9 +4878,31 @@ function formatScaffold(payload) {
3177
4878
  for (const command of payload.commands) {
3178
4879
  lines.push(` - ${command}`);
3179
4880
  }
3180
- lines.push(`CI workflow: ${payload.ciWorkflowPath}`);
4881
+ lines.push(`CI workflow: ${payload.ciWorkflowPath ?? "-"}`);
4882
+ if (writeResult) {
4883
+ lines.push(`Written root: ${writeResult.root}`);
4884
+ lines.push("Written files:");
4885
+ for (const file of writeResult.files) {
4886
+ lines.push(` - ${file.status}: ${file.path}`);
4887
+ }
4888
+ }
3181
4889
  return lines.join("\n");
3182
4890
  }
4891
+ function resolveScaffoldTarget(target, developer) {
4892
+ const normalized = developer ? "developer" : target?.trim().toLowerCase();
4893
+ if (!normalized) return "platform";
4894
+ if (normalized === "platform" || normalized === "developer") return normalized;
4895
+ throw new Error("Invalid --target value. Use platform or developer.");
4896
+ }
4897
+ function resolveScaffoldApiOption(api, withApi) {
4898
+ const noApi = process.argv.includes("--no-api");
4899
+ if (withApi && noApi) {
4900
+ throw new Error("--with-api cannot be combined with --no-api.");
4901
+ }
4902
+ if (withApi) return true;
4903
+ if (noApi || api === false) return false;
4904
+ return void 0;
4905
+ }
3183
4906
  function normalizeFailOn(value) {
3184
4907
  const normalized = value.trim().toLowerCase().replace(/-/g, "_");
3185
4908
  if (normalized === "missing" || normalized === "needs_review" || normalized === "partial") {