@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.
- package/README.md +19 -6
- package/dist/index.js +1799 -76
- 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
|
-
//
|
|
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
|
-
|
|
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((
|
|
2203
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1942
2204
|
}
|
|
1943
2205
|
|
|
1944
2206
|
// src/version.ts
|
|
1945
|
-
var CLI_VERSION = "0.1.
|
|
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,
|
|
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
|
|
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
|
|
2605
|
-
if (
|
|
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
|
|
2699
|
-
const appDir = options.appDir ?? catalogApplication?.appDir ?? `Topolo${toPascalCase(id)}
|
|
2700
|
-
const name = options.name ?? catalogApplication?.name ?? `topolo-${
|
|
2701
|
-
const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${
|
|
2702
|
-
const
|
|
2703
|
-
const
|
|
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
|
-
|
|
2708
|
-
|
|
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({
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
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
|
|
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
|
|
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}
|
|
2759
|
-
purpose: "
|
|
3104
|
+
path: `${base}/.gitignore`,
|
|
3105
|
+
purpose: "Generated-app ignore rules.",
|
|
2760
3106
|
requiredSignals: [
|
|
2761
|
-
"
|
|
2762
|
-
"
|
|
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}
|
|
2767
|
-
purpose: "
|
|
3113
|
+
path: `${base}/tsconfig.json`,
|
|
3114
|
+
purpose: "TypeScript configuration for the generated app.",
|
|
2768
3115
|
requiredSignals: [
|
|
2769
|
-
"
|
|
2770
|
-
"
|
|
2771
|
-
|
|
2772
|
-
|
|
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:
|
|
2777
|
-
purpose: "
|
|
3122
|
+
path: `${base}/vite.config.ts`,
|
|
3123
|
+
purpose: "Vite browser build configuration.",
|
|
2778
3124
|
requiredSignals: [
|
|
2779
|
-
|
|
2780
|
-
"
|
|
2781
|
-
|
|
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:
|
|
2786
|
-
purpose: "
|
|
3131
|
+
path: `${base}/index.html`,
|
|
3132
|
+
purpose: "Browser entry document.",
|
|
2787
3133
|
requiredSignals: [
|
|
2788
|
-
"
|
|
2789
|
-
"
|
|
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 ${
|
|
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
|
|
2909
|
-
|
|
2910
|
-
|
|
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 (
|
|
2913
|
-
|
|
4563
|
+
if (Object.prototype.hasOwnProperty.call(APPLICATIONS, normalized)) {
|
|
4564
|
+
return normalized;
|
|
2914
4565
|
}
|
|
2915
|
-
|
|
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(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
2993
|
-
browser: opts.browser
|
|
4687
|
+
api,
|
|
4688
|
+
browser: opts.browser,
|
|
4689
|
+
target
|
|
2994
4690
|
});
|
|
2995
|
-
|
|
2996
|
-
|
|
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") {
|