@topolo/cli 0.1.9 → 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 +1823 -87
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
|
|
|
9
9
|
import { createInterface } from "readline/promises";
|
|
10
10
|
import { stdin as input } from "process";
|
|
11
11
|
|
|
12
|
-
//
|
|
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,13 +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",
|
|
78
|
+
feed: "https://topolo-feed-api.topolo.app",
|
|
79
|
+
flow: "https://flow.topolo.app",
|
|
76
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",
|
|
77
84
|
learn: "https://topololearn-api.topolo.workers.dev",
|
|
78
85
|
mail: "https://mail.topolo.app",
|
|
79
86
|
mdm: "https://topolo-mdm-api.topolo.app",
|
|
@@ -81,16 +88,30 @@ var SERVICES = {
|
|
|
81
88
|
nexus: "https://topolo-nexus-gateway.topolo.workers.dev",
|
|
82
89
|
one: "https://topoloone-worker.topolo.workers.dev",
|
|
83
90
|
pay: "https://topolo-pay.topolo.workers.dev",
|
|
91
|
+
people: "https://people.topolo.app",
|
|
84
92
|
"quro.api": "https://api.ol0.me",
|
|
85
93
|
"quro.redirect": "https://ol0.me",
|
|
86
94
|
roadmapper: "https://roadmapper-api.topolo.app",
|
|
95
|
+
sign: "https://sign.topolo.app",
|
|
87
96
|
social_studio: "https://topolo-social-studio-api.topolo.workers.dev",
|
|
88
97
|
socialize: "https://socialize-prod-api.topolo.workers.dev",
|
|
89
98
|
support: "https://support.topolo.app",
|
|
99
|
+
survey: "https://survey.topolo.app",
|
|
100
|
+
voice: "https://voice.topolo.app",
|
|
90
101
|
web: "https://web-api.topolo.app"
|
|
91
102
|
};
|
|
92
103
|
var PLATFORM_SERVICE_IDS = {
|
|
93
|
-
|
|
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"
|
|
94
115
|
};
|
|
95
116
|
var APPLICATIONS = {
|
|
96
117
|
"admin": {
|
|
@@ -163,6 +184,30 @@ var APPLICATIONS = {
|
|
|
163
184
|
}
|
|
164
185
|
]
|
|
165
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
|
+
},
|
|
166
211
|
"bugfix": {
|
|
167
212
|
"id": "bugfix",
|
|
168
213
|
"appDir": "TopoloBugFix",
|
|
@@ -364,6 +409,30 @@ var APPLICATIONS = {
|
|
|
364
409
|
}
|
|
365
410
|
]
|
|
366
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
|
+
},
|
|
367
436
|
"developers": {
|
|
368
437
|
"id": "developers",
|
|
369
438
|
"appDir": "TopoloDevelopers",
|
|
@@ -409,24 +478,58 @@ var APPLICATIONS = {
|
|
|
409
478
|
"appDir": "TopoloFeed",
|
|
410
479
|
"name": "TopoloFeed",
|
|
411
480
|
"packageName": null,
|
|
412
|
-
"productionUrl":
|
|
413
|
-
"services": [
|
|
481
|
+
"productionUrl": "https://topolo-feed-api.topolo.app",
|
|
482
|
+
"services": [
|
|
483
|
+
"feed"
|
|
484
|
+
],
|
|
414
485
|
"deployTargets": [
|
|
415
486
|
{
|
|
416
487
|
"name": "topolo-feed-analytics-api",
|
|
417
488
|
"kind": "worker",
|
|
418
|
-
"apiUrl":
|
|
419
|
-
"domains": [
|
|
420
|
-
|
|
489
|
+
"apiUrl": "https://topolo-feed-analytics-api.topolo.app",
|
|
490
|
+
"domains": [
|
|
491
|
+
"topolo-feed-analytics-api.topolo.app"
|
|
492
|
+
],
|
|
493
|
+
"routes": [
|
|
494
|
+
"topolo-feed-analytics-api.topolo.app"
|
|
495
|
+
],
|
|
421
496
|
"serviceId": null
|
|
422
497
|
},
|
|
423
498
|
{
|
|
424
499
|
"name": "topolo-feed-api",
|
|
425
500
|
"kind": "worker",
|
|
426
|
-
"apiUrl":
|
|
427
|
-
"domains": [
|
|
428
|
-
|
|
429
|
-
|
|
501
|
+
"apiUrl": "https://topolo-feed-api.topolo.app",
|
|
502
|
+
"domains": [
|
|
503
|
+
"topolo-feed-api.topolo.app"
|
|
504
|
+
],
|
|
505
|
+
"routes": [
|
|
506
|
+
"topolo-feed-api.topolo.app"
|
|
507
|
+
],
|
|
508
|
+
"serviceId": "feed"
|
|
509
|
+
}
|
|
510
|
+
]
|
|
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"
|
|
430
533
|
}
|
|
431
534
|
]
|
|
432
535
|
},
|
|
@@ -458,6 +561,78 @@ var APPLICATIONS = {
|
|
|
458
561
|
}
|
|
459
562
|
]
|
|
460
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
|
+
},
|
|
461
636
|
"learn": {
|
|
462
637
|
"id": "learn",
|
|
463
638
|
"appDir": "TopoloLearn",
|
|
@@ -658,7 +833,9 @@ var APPLICATIONS = {
|
|
|
658
833
|
"kind": "worker",
|
|
659
834
|
"apiUrl": null,
|
|
660
835
|
"domains": [
|
|
661
|
-
"one.topolo.io"
|
|
836
|
+
"one.topolo.io",
|
|
837
|
+
"topolo.io",
|
|
838
|
+
"www.topolo.io"
|
|
662
839
|
],
|
|
663
840
|
"routes": [],
|
|
664
841
|
"serviceId": null
|
|
@@ -695,6 +872,30 @@ var APPLICATIONS = {
|
|
|
695
872
|
}
|
|
696
873
|
]
|
|
697
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
|
+
},
|
|
698
899
|
"quro": {
|
|
699
900
|
"id": "quro",
|
|
700
901
|
"appDir": "TopoloQuro",
|
|
@@ -766,6 +967,30 @@ var APPLICATIONS = {
|
|
|
766
967
|
}
|
|
767
968
|
]
|
|
768
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
|
+
},
|
|
769
994
|
"social_studio": {
|
|
770
995
|
"id": "social_studio",
|
|
771
996
|
"appDir": "TopoloSocialStudio",
|
|
@@ -862,6 +1087,54 @@ var APPLICATIONS = {
|
|
|
862
1087
|
}
|
|
863
1088
|
]
|
|
864
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
|
+
},
|
|
865
1138
|
"web": {
|
|
866
1139
|
"id": "web",
|
|
867
1140
|
"appDir": "TopoloWeb",
|
|
@@ -963,10 +1236,12 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
963
1236
|
evidence: [
|
|
964
1237
|
"@topolo/ui-kit is used for the platform shell where the app has authenticated UI",
|
|
965
1238
|
"The shared launcher/app switcher is reachable from authenticated app chrome",
|
|
966
|
-
"Account menu and sign-out behavior use
|
|
1239
|
+
"Account menu and sign-out behavior use TopoloAppShell or TopoloAccountMenu"
|
|
967
1240
|
],
|
|
968
1241
|
implementation: [
|
|
969
|
-
"Use
|
|
1242
|
+
"Use TopoloAppShell for authenticated app layouts",
|
|
1243
|
+
"Use TopoloAccountMenu when a partial-shell surface still needs account chrome",
|
|
1244
|
+
"Pass app-specific account actions as additive menu items instead of forking dropdowns",
|
|
970
1245
|
"Use shared design tokens and platform UI primitives before creating local equivalents",
|
|
971
1246
|
"Keep mobile and desktop shell behavior aligned with the platform UI kit"
|
|
972
1247
|
]
|
|
@@ -1925,11 +2200,11 @@ async function pollUntilApproved(params) {
|
|
|
1925
2200
|
throw new Error("Device code expired. Run `topolo auth login` again.");
|
|
1926
2201
|
}
|
|
1927
2202
|
function sleep(ms) {
|
|
1928
|
-
return new Promise((
|
|
2203
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1929
2204
|
}
|
|
1930
2205
|
|
|
1931
2206
|
// src/version.ts
|
|
1932
|
-
var CLI_VERSION = "0.1.
|
|
2207
|
+
var CLI_VERSION = "0.1.11";
|
|
1933
2208
|
|
|
1934
2209
|
// src/client.ts
|
|
1935
2210
|
async function buildClient(options) {
|
|
@@ -2291,13 +2566,26 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
2291
2566
|
});
|
|
2292
2567
|
}
|
|
2293
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
|
+
}
|
|
2294
2581
|
if (!shellEvidence.includes("source=TopoloAppShell")) {
|
|
2295
2582
|
return mergeFinding(finding2, {
|
|
2296
2583
|
status: "needs_review",
|
|
2297
|
-
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.",
|
|
2298
2585
|
evidence: shellEvidence,
|
|
2299
2586
|
nextActions: [
|
|
2300
2587
|
"Use TopoloAppShell for authenticated first-party app layouts",
|
|
2588
|
+
"Use TopoloAccountMenu when a partial shell still needs account chrome",
|
|
2301
2589
|
"Keep launcher, account menu, command palette, theme toggle, and bug reporter on the shared shell surface",
|
|
2302
2590
|
"Keep mobile and desktop shell behavior aligned with the platform UI kit"
|
|
2303
2591
|
]
|
|
@@ -2309,6 +2597,7 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
2309
2597
|
evidence: shellEvidence,
|
|
2310
2598
|
nextActions: [
|
|
2311
2599
|
"Use TopoloAppShell for authenticated first-party app layouts",
|
|
2600
|
+
"Pass app-specific account actions through accountMenuItems instead of app-local dropdowns",
|
|
2312
2601
|
"Keep launcher, account menu, command palette, theme toggle, and bug reporter on the shared shell surface",
|
|
2313
2602
|
"Keep mobile and desktop shell behavior aligned with the platform UI kit"
|
|
2314
2603
|
]
|
|
@@ -2360,14 +2649,14 @@ function enrichFinding(application, finding2, evidence) {
|
|
|
2360
2649
|
function collectEvidence(repoRoot, application) {
|
|
2361
2650
|
const appRoot = path.join(repoRoot, "PlatformApplications", application.appDir);
|
|
2362
2651
|
const packageJsonPath = path.join(appRoot, "package.json");
|
|
2363
|
-
const
|
|
2652
|
+
const packageJson2 = readPackageJson(packageJsonPath);
|
|
2364
2653
|
const cloudcontrolPath = path.join(appRoot, "topolo.cloudcontrol.json");
|
|
2365
2654
|
const systems = matchingSystems(repoRoot, application);
|
|
2366
2655
|
return {
|
|
2367
2656
|
appRoot,
|
|
2368
2657
|
appExists: isDirectory(appRoot),
|
|
2369
2658
|
packageJsonPath,
|
|
2370
|
-
packageJson,
|
|
2659
|
+
packageJson: packageJson2,
|
|
2371
2660
|
packageJsons: collectPackageJsons(appRoot),
|
|
2372
2661
|
cloudcontrolPath,
|
|
2373
2662
|
hasCloudControl: isFile(cloudcontrolPath),
|
|
@@ -2422,8 +2711,12 @@ function sharedShellEvidence(evidence) {
|
|
|
2422
2711
|
if (text.includes("@topolo/ui-kit")) items.push("source=@topolo/ui-kit");
|
|
2423
2712
|
if (text.includes("TopoloShell")) items.push("source=TopoloShell");
|
|
2424
2713
|
if (text.includes("TopoloAppShell")) items.push("source=TopoloAppShell");
|
|
2714
|
+
if (text.includes("TopoloAccountMenu")) items.push("source=TopoloAccountMenu");
|
|
2425
2715
|
if (text.includes("TopoloLauncher")) items.push("source=TopoloLauncher");
|
|
2426
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
|
+
}
|
|
2427
2720
|
return unique(items);
|
|
2428
2721
|
}
|
|
2429
2722
|
function hasDocumentationSurface(application, evidence) {
|
|
@@ -2588,8 +2881,8 @@ function collectPackageJsons(root) {
|
|
|
2588
2881
|
continue;
|
|
2589
2882
|
}
|
|
2590
2883
|
if (!entry.isFile() || entry.name !== "package.json") continue;
|
|
2591
|
-
const
|
|
2592
|
-
if (
|
|
2884
|
+
const packageJson2 = readPackageJson(entryPath);
|
|
2885
|
+
if (packageJson2) records.push({ path: entryPath, packageJson: packageJson2 });
|
|
2593
2886
|
}
|
|
2594
2887
|
};
|
|
2595
2888
|
visit(root);
|
|
@@ -2678,38 +2971,89 @@ function isFile(filePath) {
|
|
|
2678
2971
|
}
|
|
2679
2972
|
|
|
2680
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";
|
|
2681
2979
|
function applicationScaffoldPayload(application, options = {}) {
|
|
2980
|
+
const target = normalizeTarget(options.target);
|
|
2682
2981
|
const id = normalizeApplicationId(application);
|
|
2982
|
+
const slug = toSlug(id);
|
|
2683
2983
|
const catalogApplication = APPLICATIONS[id];
|
|
2684
2984
|
const browser = options.browser !== false;
|
|
2685
|
-
const api = options.api
|
|
2686
|
-
const appDir = options.appDir ?? catalogApplication?.appDir ?? `Topolo${toPascalCase(id)}
|
|
2687
|
-
const name = options.name ?? catalogApplication?.name ?? `topolo-${
|
|
2688
|
-
const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${
|
|
2689
|
-
const
|
|
2690
|
-
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;
|
|
2691
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
|
+
};
|
|
2692
3003
|
return {
|
|
2693
3004
|
version: APPLICATION_REQUIREMENTS_VERSION,
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
name,
|
|
2697
|
-
appDir,
|
|
2698
|
-
packageName,
|
|
2699
|
-
productionUrl,
|
|
2700
|
-
serviceId
|
|
2701
|
-
},
|
|
3005
|
+
target,
|
|
3006
|
+
application: applicationPayload,
|
|
2702
3007
|
scopes,
|
|
2703
3008
|
requirements: APPLICATION_REQUIREMENTS.filter(
|
|
2704
3009
|
(requirement) => requirement.appliesTo.some((scope) => scopes.includes(scope))
|
|
2705
3010
|
),
|
|
2706
|
-
packages: scaffoldPackages({ browser, api }),
|
|
2707
|
-
files: scaffoldFiles({
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
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
|
|
2711
3022
|
};
|
|
2712
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
|
+
}
|
|
2713
3057
|
function scaffoldScopes(options) {
|
|
2714
3058
|
const scopes = ["all"];
|
|
2715
3059
|
if (options.browser) scopes.push("browser");
|
|
@@ -2719,6 +3063,9 @@ function scaffoldScopes(options) {
|
|
|
2719
3063
|
return scopes;
|
|
2720
3064
|
}
|
|
2721
3065
|
function scaffoldPackages(options) {
|
|
3066
|
+
if (options.target === "developer") {
|
|
3067
|
+
return [];
|
|
3068
|
+
}
|
|
2722
3069
|
const packages = /* @__PURE__ */ new Set();
|
|
2723
3070
|
if (options.browser) {
|
|
2724
3071
|
packages.add("@topolo/ui-kit");
|
|
@@ -2731,52 +3078,177 @@ function scaffoldPackages(options) {
|
|
|
2731
3078
|
return Array.from(packages);
|
|
2732
3079
|
}
|
|
2733
3080
|
function scaffoldFiles(options) {
|
|
2734
|
-
const
|
|
3081
|
+
const { application, target } = options;
|
|
3082
|
+
const slug = toSlug(application.id);
|
|
3083
|
+
const base = target === "platform" ? `PlatformApplications/${application.appDir}` : application.appDir;
|
|
2735
3084
|
const files = [
|
|
2736
3085
|
{
|
|
2737
3086
|
path: `${base}/package.json`,
|
|
2738
3087
|
purpose: "Package metadata and verification commands.",
|
|
2739
3088
|
requiredSignals: [
|
|
2740
3089
|
"name matches the packageName in the SDK application catalog",
|
|
2741
|
-
"scripts include build plus typecheck
|
|
2742
|
-
]
|
|
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 })
|
|
3102
|
+
},
|
|
3103
|
+
{
|
|
3104
|
+
path: `${base}/.gitignore`,
|
|
3105
|
+
purpose: "Generated-app ignore rules.",
|
|
3106
|
+
requiredSignals: [
|
|
3107
|
+
"ignores dependency installs and build output",
|
|
3108
|
+
"keeps local environment files out of source control"
|
|
3109
|
+
],
|
|
3110
|
+
content: gitignore()
|
|
3111
|
+
},
|
|
3112
|
+
{
|
|
3113
|
+
path: `${base}/tsconfig.json`,
|
|
3114
|
+
purpose: "TypeScript configuration for the generated app.",
|
|
3115
|
+
requiredSignals: [
|
|
3116
|
+
"uses strict mode",
|
|
3117
|
+
"includes browser DOM types for React and Worker types for API-backed apps"
|
|
3118
|
+
],
|
|
3119
|
+
content: tsconfigJson(options.api)
|
|
2743
3120
|
},
|
|
2744
3121
|
{
|
|
2745
|
-
path: `${base}/
|
|
2746
|
-
purpose: "
|
|
3122
|
+
path: `${base}/vite.config.ts`,
|
|
3123
|
+
purpose: "Vite browser build configuration.",
|
|
2747
3124
|
requiredSignals: [
|
|
2748
|
-
"
|
|
2749
|
-
"
|
|
2750
|
-
]
|
|
3125
|
+
"builds the authenticated browser surface",
|
|
3126
|
+
"keeps generated apps on the standard React/Vite baseline"
|
|
3127
|
+
],
|
|
3128
|
+
content: viteConfig()
|
|
2751
3129
|
},
|
|
2752
3130
|
{
|
|
2753
|
-
path: `${base}
|
|
2754
|
-
purpose: "
|
|
3131
|
+
path: `${base}/index.html`,
|
|
3132
|
+
purpose: "Browser entry document.",
|
|
2755
3133
|
requiredSignals: [
|
|
2756
|
-
"
|
|
2757
|
-
"
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
]
|
|
3134
|
+
"mounts the React app at #root",
|
|
3135
|
+
"uses the generated application display name"
|
|
3136
|
+
],
|
|
3137
|
+
content: indexHtml(application.name)
|
|
2761
3138
|
},
|
|
2762
3139
|
{
|
|
2763
|
-
path:
|
|
2764
|
-
purpose: "
|
|
3140
|
+
path: `${base}/src/main.tsx`,
|
|
3141
|
+
purpose: "React app entrypoint.",
|
|
2765
3142
|
requiredSignals: [
|
|
2766
|
-
|
|
2767
|
-
"
|
|
2768
|
-
|
|
2769
|
-
|
|
3143
|
+
"renders the generated app component",
|
|
3144
|
+
"imports the generated app stylesheet"
|
|
3145
|
+
],
|
|
3146
|
+
content: mainTsx()
|
|
2770
3147
|
},
|
|
2771
3148
|
{
|
|
2772
|
-
path:
|
|
2773
|
-
purpose: "
|
|
3149
|
+
path: `${base}/src/env.d.ts`,
|
|
3150
|
+
purpose: "Vite environment variable typing.",
|
|
2774
3151
|
requiredSignals: [
|
|
2775
|
-
"
|
|
2776
|
-
"
|
|
2777
|
-
]
|
|
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()
|
|
2778
3165
|
}
|
|
2779
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
|
+
}
|
|
2780
3252
|
if (options.browser) {
|
|
2781
3253
|
files.push({
|
|
2782
3254
|
path: `${base}/src/App.tsx`,
|
|
@@ -2784,8 +3256,10 @@ function scaffoldFiles(options) {
|
|
|
2784
3256
|
requiredSignals: [
|
|
2785
3257
|
"imports shared TopoloAppShell from @topolo/ui-kit",
|
|
2786
3258
|
"uses @topolo/auth-client for app-origin auth flows",
|
|
2787
|
-
"keeps launcher and account-menu behavior on the shared platform surface"
|
|
2788
|
-
|
|
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)
|
|
2789
3263
|
});
|
|
2790
3264
|
}
|
|
2791
3265
|
if (options.api) {
|
|
@@ -2794,9 +3268,10 @@ function scaffoldFiles(options) {
|
|
|
2794
3268
|
path: `${base}/wrangler.toml`,
|
|
2795
3269
|
purpose: "Cloudflare Worker runtime configuration.",
|
|
2796
3270
|
requiredSignals: [
|
|
2797
|
-
"bindings match topolo.cloudcontrol.json",
|
|
3271
|
+
"bindings match topolo.cloudcontrol.json or topolo.app.json",
|
|
2798
3272
|
"secrets are referenced by name only"
|
|
2799
|
-
]
|
|
3273
|
+
],
|
|
3274
|
+
content: wranglerToml(application, target)
|
|
2800
3275
|
},
|
|
2801
3276
|
{
|
|
2802
3277
|
path: `${base}/src/worker.ts`,
|
|
@@ -2805,15 +3280,26 @@ function scaffoldFiles(options) {
|
|
|
2805
3280
|
"uses @topolo/auth-middleware or Auth validation for protected routes",
|
|
2806
3281
|
"derives organization from the credential rather than client-supplied orgId",
|
|
2807
3282
|
"emits or propagates X-Topolo-Request-Id and X-Topolo-Client"
|
|
2808
|
-
]
|
|
3283
|
+
],
|
|
3284
|
+
content: workerTs(application, target)
|
|
2809
3285
|
}
|
|
2810
3286
|
);
|
|
2811
3287
|
}
|
|
2812
3288
|
return files;
|
|
2813
3289
|
}
|
|
2814
|
-
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
|
+
}
|
|
2815
3301
|
return [
|
|
2816
|
-
`topolo apps scaffold ${
|
|
3302
|
+
`topolo apps scaffold ${scaffoldId} --write --repo-root /Users/ashes/Projects/topolo --json`,
|
|
2817
3303
|
"topolo apps requirements --json",
|
|
2818
3304
|
`topolo apps audit ${id} --local --repo-root /Users/ashes/Projects/topolo --fail-on needs_review --json`,
|
|
2819
3305
|
`topolo apps audit ${id} --local --repo-root /Users/ashes/Projects/topolo --fail-on partial --json`
|
|
@@ -2891,18 +3377,1235 @@ function topoloConformanceWorkflow(options) {
|
|
|
2891
3377
|
` node PlatformApplications/TopoloCli/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`
|
|
2892
3378
|
].join("\n");
|
|
2893
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
|
+
}
|
|
2894
4557
|
function normalizeApplicationId(application) {
|
|
2895
|
-
const
|
|
2896
|
-
|
|
2897
|
-
|
|
4558
|
+
const raw = application.trim().toLowerCase();
|
|
4559
|
+
const normalized = raw.replace(/-/g, "_");
|
|
4560
|
+
if (Object.prototype.hasOwnProperty.call(APPLICATIONS, raw)) {
|
|
4561
|
+
return raw;
|
|
4562
|
+
}
|
|
4563
|
+
if (Object.prototype.hasOwnProperty.call(APPLICATIONS, normalized)) {
|
|
4564
|
+
return normalized;
|
|
2898
4565
|
}
|
|
2899
|
-
if (
|
|
2900
|
-
|
|
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;
|
|
2901
4568
|
}
|
|
2902
|
-
|
|
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, "-");
|
|
2903
4575
|
}
|
|
2904
4576
|
function toPascalCase(value) {
|
|
2905
|
-
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, '\\"');
|
|
2906
4609
|
}
|
|
2907
4610
|
|
|
2908
4611
|
// src/commands/apps.ts
|
|
@@ -2967,20 +4670,30 @@ function registerAppsCommand(program2) {
|
|
|
2967
4670
|
});
|
|
2968
4671
|
apps.command("scaffold <application>").description(
|
|
2969
4672
|
"Generate the required Topolo new-application scaffold, including a conformance workflow."
|
|
2970
|
-
).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) => {
|
|
2971
4674
|
const mode = opts.json === false || process.argv.includes("--no-json") ? "pretty" : resolveOutputMode(opts.json);
|
|
2972
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);
|
|
2973
4681
|
const payload = applicationScaffoldPayload(application, {
|
|
2974
4682
|
appDir: opts.appDir,
|
|
2975
4683
|
name: opts.name,
|
|
2976
4684
|
packageName: opts.packageName,
|
|
2977
4685
|
productionUrl: opts.productionUrl,
|
|
2978
4686
|
service: opts.service,
|
|
2979
|
-
api
|
|
2980
|
-
browser: opts.browser
|
|
4687
|
+
api,
|
|
4688
|
+
browser: opts.browser,
|
|
4689
|
+
target
|
|
2981
4690
|
});
|
|
2982
|
-
|
|
2983
|
-
|
|
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));
|
|
2984
4697
|
} catch (err) {
|
|
2985
4698
|
fail(err, mode);
|
|
2986
4699
|
}
|
|
@@ -3141,9 +4854,10 @@ function formatAudit(payload) {
|
|
|
3141
4854
|
}
|
|
3142
4855
|
return lines.join("\n");
|
|
3143
4856
|
}
|
|
3144
|
-
function formatScaffold(payload) {
|
|
4857
|
+
function formatScaffold(payload, writeResult = null) {
|
|
3145
4858
|
const lines = [
|
|
3146
4859
|
`Version: ${payload.version}`,
|
|
4860
|
+
`Target: ${payload.target}`,
|
|
3147
4861
|
`Application: ${payload.application.id}`,
|
|
3148
4862
|
`Directory: ${payload.application.appDir}`,
|
|
3149
4863
|
`Package: ${payload.application.packageName}`,
|
|
@@ -3164,9 +4878,31 @@ function formatScaffold(payload) {
|
|
|
3164
4878
|
for (const command of payload.commands) {
|
|
3165
4879
|
lines.push(` - ${command}`);
|
|
3166
4880
|
}
|
|
3167
|
-
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
|
+
}
|
|
3168
4889
|
return lines.join("\n");
|
|
3169
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
|
+
}
|
|
3170
4906
|
function normalizeFailOn(value) {
|
|
3171
4907
|
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
3172
4908
|
if (normalized === "missing" || normalized === "needs_review" || normalized === "partial") {
|