@topolo/cli 0.1.30 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +807 -766
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -92,7 +92,7 @@ var SERVICES = {
|
|
|
92
92
|
mail: "https://mail.topolo.app",
|
|
93
93
|
mdm: "https://mdm.topolo.app",
|
|
94
94
|
messages: "https://messages.topolo.app",
|
|
95
|
-
nexus: "https://nexus
|
|
95
|
+
nexus: "https://nexus.topolo.app",
|
|
96
96
|
notify: "https://notify.topolo.app",
|
|
97
97
|
observability_watch: "https://observability-watch.topolo.app",
|
|
98
98
|
one: "https://www.topolo.app",
|
|
@@ -641,8 +641,10 @@ var APPLICATIONS = {
|
|
|
641
641
|
{
|
|
642
642
|
"name": "topolo-feed-api",
|
|
643
643
|
"kind": "worker",
|
|
644
|
-
"apiUrl":
|
|
645
|
-
"domains": [
|
|
644
|
+
"apiUrl": "https://feed-api.topolo.app",
|
|
645
|
+
"domains": [
|
|
646
|
+
"feed-api.topolo.app"
|
|
647
|
+
],
|
|
646
648
|
"routes": [],
|
|
647
649
|
"serviceId": null
|
|
648
650
|
},
|
|
@@ -1013,9 +1015,9 @@ var APPLICATIONS = {
|
|
|
1013
1015
|
{
|
|
1014
1016
|
"name": "topolo-nexus-gateway",
|
|
1015
1017
|
"kind": "worker",
|
|
1016
|
-
"apiUrl": "https://nexus
|
|
1018
|
+
"apiUrl": "https://nexus.topolo.app",
|
|
1017
1019
|
"domains": [
|
|
1018
|
-
"nexus
|
|
1020
|
+
"nexus.topolo.app"
|
|
1019
1021
|
],
|
|
1020
1022
|
"routes": [],
|
|
1021
1023
|
"serviceId": "nexus"
|
|
@@ -1375,8 +1377,10 @@ var APPLICATIONS = {
|
|
|
1375
1377
|
{
|
|
1376
1378
|
"name": "topolo-socialize-api",
|
|
1377
1379
|
"kind": "worker",
|
|
1378
|
-
"apiUrl":
|
|
1379
|
-
"domains": [
|
|
1380
|
+
"apiUrl": "https://socialize-api.topolo.app",
|
|
1381
|
+
"domains": [
|
|
1382
|
+
"socialize-api.topolo.app"
|
|
1383
|
+
],
|
|
1380
1384
|
"routes": [],
|
|
1381
1385
|
"serviceId": null
|
|
1382
1386
|
},
|
|
@@ -2220,10 +2224,14 @@ var TopoloClient = class _TopoloClient {
|
|
|
2220
2224
|
if (!serviceKey) {
|
|
2221
2225
|
throw new TopoloSdkError("unknown_service", "service is required");
|
|
2222
2226
|
}
|
|
2223
|
-
if (serviceKey === "auth") {
|
|
2227
|
+
if (serviceKey === "auth" || serviceKey === "developers") {
|
|
2224
2228
|
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
2225
2229
|
if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
|
|
2226
|
-
|
|
2230
|
+
if (serviceKey === "developers") {
|
|
2231
|
+
const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
|
|
2232
|
+
if (derived) return { serviceKey, baseUrl: derived };
|
|
2233
|
+
}
|
|
2234
|
+
return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS[serviceKey] };
|
|
2227
2235
|
}
|
|
2228
2236
|
const live = await this.resolveLiveCallableService(serviceKey);
|
|
2229
2237
|
if (live) {
|
|
@@ -2263,7 +2271,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2263
2271
|
if (!normalized) return null;
|
|
2264
2272
|
const configuredServiceId = resolveConfiguredServiceId(normalized, this.serviceIds);
|
|
2265
2273
|
if (configuredServiceId) return configuredServiceId;
|
|
2266
|
-
const authBase = resolveServiceUrl("
|
|
2274
|
+
const authBase = resolveServiceUrl("developers", this.serviceUrls);
|
|
2267
2275
|
const cacheKey = `${authBase}|${normalized}`;
|
|
2268
2276
|
const now = Date.now();
|
|
2269
2277
|
const cached = _TopoloClient.serviceIdCache.get(cacheKey);
|
|
@@ -2317,7 +2325,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2317
2325
|
if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
|
|
2318
2326
|
return this.serviceCatalogCache.entries;
|
|
2319
2327
|
}
|
|
2320
|
-
const res = await this.request({ service: "
|
|
2328
|
+
const res = await this.request({ service: "developers", method: "GET", path: "/api/services/catalog" });
|
|
2321
2329
|
const raw = res.data?.services ?? res.services;
|
|
2322
2330
|
if (!Array.isArray(raw)) {
|
|
2323
2331
|
throw new TopoloSdkError(
|
|
@@ -2548,6 +2556,17 @@ function serviceKeyCandidates2(slug) {
|
|
|
2548
2556
|
function normalizeLookupKey(value) {
|
|
2549
2557
|
return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
|
|
2550
2558
|
}
|
|
2559
|
+
function deriveDevelopersFromAuth(authUrl) {
|
|
2560
|
+
if (!authUrl) return null;
|
|
2561
|
+
try {
|
|
2562
|
+
const url = new URL(authUrl);
|
|
2563
|
+
if (!/^auth\./i.test(url.hostname)) return null;
|
|
2564
|
+
url.hostname = url.hostname.replace(/^auth\./i, "developers.");
|
|
2565
|
+
return url.origin;
|
|
2566
|
+
} catch {
|
|
2567
|
+
return null;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2551
2570
|
function catalogEntryMatches(entry, needle) {
|
|
2552
2571
|
return catalogEntryLookupKeys(entry).includes(needle);
|
|
2553
2572
|
}
|
|
@@ -3311,7 +3330,7 @@ async function pollUntilApproved(params) {
|
|
|
3311
3330
|
throw new Error("Device code expired. Run `topolo auth login` again.");
|
|
3312
3331
|
}
|
|
3313
3332
|
function sleep(ms) {
|
|
3314
|
-
return new Promise((
|
|
3333
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
3315
3334
|
}
|
|
3316
3335
|
|
|
3317
3336
|
// src/version.ts
|
|
@@ -4694,640 +4713,150 @@ function isFile(filePath) {
|
|
|
4694
4713
|
|
|
4695
4714
|
// src/commands/app-scaffold.ts
|
|
4696
4715
|
import { randomBytes } from "crypto";
|
|
4716
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
4717
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
4718
|
+
|
|
4719
|
+
// src/commands/app-scaffold-normalizers.ts
|
|
4697
4720
|
import { constants } from "fs";
|
|
4698
|
-
import { access
|
|
4699
|
-
import {
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
function generateOpaqueServiceId() {
|
|
4705
|
-
return `srv_${randomBytes(9).toString("base64url")}`;
|
|
4721
|
+
import { access } from "fs/promises";
|
|
4722
|
+
import { relative, resolve } from "path";
|
|
4723
|
+
function normalizeTarget(value) {
|
|
4724
|
+
if (!value) return "platform";
|
|
4725
|
+
if (value === "platform" || value === "developer") return value;
|
|
4726
|
+
throw new Error("Invalid scaffold target. Use platform or developer.");
|
|
4706
4727
|
}
|
|
4707
|
-
function
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
const id = target === "platform" ? normalizeFirstPartyApplicationId(requestedId) : requestedId;
|
|
4712
|
-
const slug = toSlug(id);
|
|
4713
|
-
const catalogApplication = APPLICATIONS[id];
|
|
4714
|
-
const browser = options.browser !== false;
|
|
4715
|
-
const api = options.api ?? target === "platform";
|
|
4716
|
-
const audience = normalizeAudience(
|
|
4717
|
-
options.audience,
|
|
4718
|
-
target === "platform" ? portfolio : null
|
|
4719
|
-
);
|
|
4720
|
-
const tenancy = normalizeTenancy(options.tenancy, audience);
|
|
4721
|
-
const surfaceModel = normalizeSurfaceModel(options.surfaceModel);
|
|
4722
|
-
const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
|
|
4723
|
-
const profile = normalizeScaffoldProfile(options.profile);
|
|
4724
|
-
const resources = resolveScaffoldResources(profile, options);
|
|
4725
|
-
const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
|
|
4726
|
-
const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
|
|
4727
|
-
const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
|
|
4728
|
-
const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
|
|
4729
|
-
const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
|
|
4730
|
-
const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
|
|
4731
|
-
const defaultServiceId = target === "platform" ? generateOpaqueServiceId() : catalogApplication?.services[0] ?? id;
|
|
4732
|
-
const serviceId = api ? options.service ?? defaultServiceId : null;
|
|
4733
|
-
const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
|
|
4734
|
-
const scopes = scaffoldScopes({ browser, api });
|
|
4735
|
-
const capabilities = scaffoldCapabilities({ browser, api, distributionModel });
|
|
4736
|
-
const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
|
|
4737
|
-
const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir, audience }) : null;
|
|
4738
|
-
const applicationPayload = {
|
|
4739
|
-
id,
|
|
4740
|
-
name,
|
|
4741
|
-
appDir,
|
|
4742
|
-
packageName,
|
|
4743
|
-
productionUrl,
|
|
4744
|
-
serviceSlug,
|
|
4745
|
-
serviceId
|
|
4746
|
-
};
|
|
4747
|
-
return {
|
|
4748
|
-
version: APPLICATION_REQUIREMENTS_VERSION,
|
|
4749
|
-
target,
|
|
4750
|
-
ownerType,
|
|
4751
|
-
portfolio,
|
|
4752
|
-
audience,
|
|
4753
|
-
tenancy,
|
|
4754
|
-
surfaceModel,
|
|
4755
|
-
containerAppId,
|
|
4756
|
-
profile,
|
|
4757
|
-
resources,
|
|
4758
|
-
distributionModel,
|
|
4759
|
-
capabilities,
|
|
4760
|
-
application: applicationPayload,
|
|
4761
|
-
scopes,
|
|
4762
|
-
requirements: APPLICATION_REQUIREMENTS.filter(
|
|
4763
|
-
(requirement) => requirement.appliesTo.some((scope) => scopes.includes(scope))
|
|
4764
|
-
),
|
|
4765
|
-
packages: scaffoldPackages({ browser, api, target }),
|
|
4766
|
-
files: scaffoldFiles({
|
|
4767
|
-
payload: {
|
|
4768
|
-
ownerType,
|
|
4769
|
-
portfolio,
|
|
4770
|
-
audience,
|
|
4771
|
-
tenancy,
|
|
4772
|
-
surfaceModel,
|
|
4773
|
-
containerAppId,
|
|
4774
|
-
profile,
|
|
4775
|
-
resources,
|
|
4776
|
-
distributionModel,
|
|
4777
|
-
capabilities
|
|
4778
|
-
},
|
|
4779
|
-
application: applicationPayload,
|
|
4780
|
-
target,
|
|
4781
|
-
browser,
|
|
4782
|
-
api,
|
|
4783
|
-
ciWorkflow
|
|
4784
|
-
}),
|
|
4785
|
-
commands: scaffoldCommands({
|
|
4786
|
-
id,
|
|
4787
|
-
target,
|
|
4788
|
-
audience,
|
|
4789
|
-
tenancy,
|
|
4790
|
-
portfolio,
|
|
4791
|
-
profile
|
|
4792
|
-
}),
|
|
4793
|
-
ciWorkflowPath,
|
|
4794
|
-
ciWorkflow
|
|
4795
|
-
};
|
|
4728
|
+
function normalizePortfolio(value) {
|
|
4729
|
+
if (!value) return "platform";
|
|
4730
|
+
if (value === "platform" || value === "personal") return value;
|
|
4731
|
+
throw new Error("Invalid scaffold portfolio. Use platform or personal.");
|
|
4796
4732
|
}
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
file,
|
|
4801
|
-
absolutePath: resolveInside(root, file.path)
|
|
4802
|
-
}));
|
|
4803
|
-
const existingFiles = [];
|
|
4804
|
-
for (const planned of plannedFiles) {
|
|
4805
|
-
if (await pathExists(planned.absolutePath)) {
|
|
4806
|
-
existingFiles.push(planned.file.path);
|
|
4807
|
-
}
|
|
4733
|
+
function normalizeAudience(value, portfolio) {
|
|
4734
|
+
if (!value) {
|
|
4735
|
+
return portfolio === "personal" ? "personal" : "business";
|
|
4808
4736
|
}
|
|
4809
|
-
if (
|
|
4810
|
-
|
|
4811
|
-
[
|
|
4812
|
-
"Refusing to overwrite existing scaffold files:",
|
|
4813
|
-
...existingFiles.map((filePath) => `- ${filePath}`),
|
|
4814
|
-
"Pass --force to overwrite these files."
|
|
4815
|
-
].join("\n")
|
|
4816
|
-
);
|
|
4737
|
+
if (value === "business" || value === "personal") {
|
|
4738
|
+
return value;
|
|
4817
4739
|
}
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
path: planned.file.path,
|
|
4825
|
-
status: existed ? "overwritten" : "created"
|
|
4826
|
-
});
|
|
4740
|
+
throw new Error("Invalid scaffold audience. Use business or personal.");
|
|
4741
|
+
}
|
|
4742
|
+
function normalizeTenancy(value, audience) {
|
|
4743
|
+
const resolved = value ?? (audience === "personal" ? "household" : "organization");
|
|
4744
|
+
if (resolved !== "organization" && resolved !== "household") {
|
|
4745
|
+
throw new Error("Invalid scaffold tenancy. Use organization or household.");
|
|
4827
4746
|
}
|
|
4828
|
-
|
|
4747
|
+
if (audience === "business" && resolved !== "organization") {
|
|
4748
|
+
throw new Error("Business scaffolds must use organization tenancy.");
|
|
4749
|
+
}
|
|
4750
|
+
if (audience === "personal" && resolved !== "household") {
|
|
4751
|
+
throw new Error("Personal scaffolds must use household tenancy.");
|
|
4752
|
+
}
|
|
4753
|
+
return resolved;
|
|
4829
4754
|
}
|
|
4830
|
-
function
|
|
4831
|
-
|
|
4832
|
-
if (
|
|
4833
|
-
|
|
4834
|
-
scopes.push("api", "agent_surface");
|
|
4755
|
+
function normalizeSurfaceModel(value) {
|
|
4756
|
+
if (!value) return "web_app";
|
|
4757
|
+
if (value === "web_app" || value === "mobile_container" || value === "mobile_module" || value === "messaging_surface") {
|
|
4758
|
+
return value;
|
|
4835
4759
|
}
|
|
4836
|
-
|
|
4760
|
+
throw new Error(
|
|
4761
|
+
"Invalid scaffold surface model. Use web_app, mobile_container, mobile_module, or messaging_surface."
|
|
4762
|
+
);
|
|
4837
4763
|
}
|
|
4838
|
-
function
|
|
4839
|
-
if (
|
|
4840
|
-
|
|
4764
|
+
function normalizeScaffoldProfile(value) {
|
|
4765
|
+
if (!value) return null;
|
|
4766
|
+
if (value === "spatial-assets") return value;
|
|
4767
|
+
throw new Error("Invalid scaffold profile. Use spatial-assets.");
|
|
4768
|
+
}
|
|
4769
|
+
function resolveScaffoldResources(profile, options) {
|
|
4770
|
+
const profileResources = profile === "spatial-assets" ? { d1: true, r2: true, queue: true, workflow: true, containerProcessing: true } : { d1: false, r2: false, queue: false, workflow: false, containerProcessing: false };
|
|
4771
|
+
return {
|
|
4772
|
+
d1: Boolean(profileResources.d1 || options.withD1),
|
|
4773
|
+
r2: Boolean(profileResources.r2 || options.withR2),
|
|
4774
|
+
queue: Boolean(profileResources.queue || options.withQueue),
|
|
4775
|
+
workflow: Boolean(profileResources.workflow || options.withWorkflow),
|
|
4776
|
+
containerProcessing: Boolean(profileResources.containerProcessing || options.withContainerProcessing)
|
|
4777
|
+
};
|
|
4778
|
+
}
|
|
4779
|
+
function normalizeContainerAppId(value, surfaceModel) {
|
|
4780
|
+
const trimmed = String(value || "").trim();
|
|
4781
|
+
if (surfaceModel === "mobile_module") {
|
|
4782
|
+
if (!trimmed) {
|
|
4783
|
+
throw new Error("Mobile-module scaffolds require --container-app-id.");
|
|
4784
|
+
}
|
|
4785
|
+
return trimmed;
|
|
4841
4786
|
}
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4787
|
+
return trimmed || null;
|
|
4788
|
+
}
|
|
4789
|
+
function normalizeApplicationId(application) {
|
|
4790
|
+
const raw = application.trim().toLowerCase();
|
|
4791
|
+
const normalized = raw.replace(/-/g, "_");
|
|
4792
|
+
if (Object.prototype.hasOwnProperty.call(APPLICATIONS, raw)) {
|
|
4793
|
+
return raw;
|
|
4849
4794
|
}
|
|
4850
|
-
if (
|
|
4851
|
-
|
|
4852
|
-
packages.add("@topolo/sdk");
|
|
4795
|
+
if (Object.prototype.hasOwnProperty.call(APPLICATIONS, normalized)) {
|
|
4796
|
+
return normalized;
|
|
4853
4797
|
}
|
|
4854
|
-
|
|
4798
|
+
if (/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(raw) || /^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/.test(raw)) {
|
|
4799
|
+
return normalized;
|
|
4800
|
+
}
|
|
4801
|
+
throw new Error(
|
|
4802
|
+
"Application id must be kebab-case or snake_case using lowercase letters, numbers, hyphens, and underscores."
|
|
4803
|
+
);
|
|
4855
4804
|
}
|
|
4856
|
-
function
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
capabilities.push("login", "launcher");
|
|
4805
|
+
function normalizeFirstPartyApplicationId(applicationId) {
|
|
4806
|
+
if (applicationId.startsWith("topolo_") && applicationId.length > "topolo_".length) {
|
|
4807
|
+
return applicationId.slice("topolo_".length);
|
|
4860
4808
|
}
|
|
4861
|
-
|
|
4862
|
-
if (options.api) capabilities.push("api", "widget");
|
|
4863
|
-
return capabilities;
|
|
4809
|
+
return applicationId;
|
|
4864
4810
|
}
|
|
4865
|
-
function
|
|
4866
|
-
|
|
4867
|
-
if (appDir === "TopoloDocs") return "Apps/system/TopoloDocs";
|
|
4868
|
-
if (appDir === "TopoloAuth") return "Apps/system/TopoloAuth";
|
|
4869
|
-
return `${audience === "personal" ? "Apps/personal" : "Apps/business"}/${appDir}`;
|
|
4811
|
+
function toSlug(value) {
|
|
4812
|
+
return value.replace(/_/g, "-");
|
|
4870
4813
|
}
|
|
4871
|
-
function
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
const
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
},
|
|
4891
|
-
{
|
|
4892
|
-
path: `${base}/README.md`,
|
|
4893
|
-
purpose: "Starter README with install, run, and verification commands.",
|
|
4894
|
-
requiredSignals: [
|
|
4895
|
-
"documents the generated app target",
|
|
4896
|
-
"points maintainers to the shared Topolo packages used by the scaffold"
|
|
4897
|
-
],
|
|
4898
|
-
content: readme(application, target, {
|
|
4899
|
-
api: options.api,
|
|
4900
|
-
audience: options.payload.audience,
|
|
4901
|
-
tenancy: options.payload.tenancy,
|
|
4902
|
-
portfolio: options.payload.portfolio,
|
|
4903
|
-
profile: options.payload.profile,
|
|
4904
|
-
resources: options.payload.resources
|
|
4905
|
-
})
|
|
4906
|
-
},
|
|
4907
|
-
{
|
|
4908
|
-
path: `${base}/.gitignore`,
|
|
4909
|
-
purpose: "Generated-app ignore rules.",
|
|
4910
|
-
requiredSignals: [
|
|
4911
|
-
"ignores dependency installs and build output",
|
|
4912
|
-
"keeps local environment files out of source control"
|
|
4913
|
-
],
|
|
4914
|
-
content: gitignore()
|
|
4915
|
-
},
|
|
4916
|
-
{
|
|
4917
|
-
path: `${base}/tsconfig.json`,
|
|
4918
|
-
purpose: "TypeScript configuration for the generated app.",
|
|
4919
|
-
requiredSignals: [
|
|
4920
|
-
"uses strict mode",
|
|
4921
|
-
"includes browser DOM types for React and Worker types for API-backed apps"
|
|
4922
|
-
],
|
|
4923
|
-
content: tsconfigJson(options.api, target)
|
|
4924
|
-
},
|
|
4925
|
-
{
|
|
4926
|
-
path: `${browserBase}/vite.config.ts`,
|
|
4927
|
-
purpose: "Vite browser build configuration.",
|
|
4928
|
-
requiredSignals: [
|
|
4929
|
-
"builds the authenticated browser surface",
|
|
4930
|
-
"keeps generated apps on the standard React/Vite baseline"
|
|
4931
|
-
],
|
|
4932
|
-
content: viteConfig(target)
|
|
4933
|
-
},
|
|
4934
|
-
{
|
|
4935
|
-
path: `${browserBase}/index.html`,
|
|
4936
|
-
purpose: "Browser entry document.",
|
|
4937
|
-
requiredSignals: [
|
|
4938
|
-
"mounts the React app at #root",
|
|
4939
|
-
"uses the generated application display name"
|
|
4940
|
-
],
|
|
4941
|
-
content: indexHtml(application.name)
|
|
4942
|
-
},
|
|
4943
|
-
{
|
|
4944
|
-
path: `${browserBase}/src/main.tsx`,
|
|
4945
|
-
purpose: "React app entrypoint.",
|
|
4946
|
-
requiredSignals: [
|
|
4947
|
-
"renders the generated app component",
|
|
4948
|
-
"imports the generated app stylesheet"
|
|
4949
|
-
],
|
|
4950
|
-
content: mainTsx()
|
|
4951
|
-
},
|
|
4952
|
-
{
|
|
4953
|
-
path: `${browserBase}/src/env.d.ts`,
|
|
4954
|
-
purpose: "Vite environment variable typing.",
|
|
4955
|
-
requiredSignals: [
|
|
4956
|
-
"loads vite/client import.meta.env types",
|
|
4957
|
-
"declares the Topolo environment variables consumed by the starter"
|
|
4958
|
-
],
|
|
4959
|
-
content: envDts(options.api)
|
|
4960
|
-
},
|
|
4961
|
-
...options.payload.resources.workflow ? [
|
|
4962
|
-
{
|
|
4963
|
-
path: `${base}/src/cloudflare-workers.d.ts`,
|
|
4964
|
-
purpose: "Minimal Cloudflare Workflow entrypoint typing for scaffolded Workers.",
|
|
4965
|
-
requiredSignals: [
|
|
4966
|
-
"declares cloudflare:workers WorkflowEntrypoint for TypeScript",
|
|
4967
|
-
"keeps Workflow imports deployable without adding a new dependency"
|
|
4968
|
-
],
|
|
4969
|
-
content: cloudflareWorkersDts()
|
|
4970
|
-
}
|
|
4971
|
-
] : [],
|
|
4972
|
-
{
|
|
4973
|
-
path: `${browserBase}/src/styles.css`,
|
|
4974
|
-
purpose: "Minimal app-local styles outside the shared Topolo shell.",
|
|
4975
|
-
requiredSignals: [
|
|
4976
|
-
"does not fork the shared shell chrome",
|
|
4977
|
-
"keeps app-specific layout scoped under generated classes"
|
|
4978
|
-
],
|
|
4979
|
-
content: stylesCss()
|
|
4980
|
-
}
|
|
4981
|
-
];
|
|
4982
|
-
if (target === "platform") {
|
|
4983
|
-
files.push(
|
|
4984
|
-
{
|
|
4985
|
-
path: `${base}/topolo.app.json`,
|
|
4986
|
-
purpose: "Canonical app manifest that drives generated Wrangler and CloudControl metadata.",
|
|
4987
|
-
requiredSignals: [
|
|
4988
|
-
"declares web/api targets with canonical names and app-local target directories",
|
|
4989
|
-
"keeps Cloudflare account, domain, service id, and bindings in one manifest source of truth"
|
|
4990
|
-
],
|
|
4991
|
-
content: topoloAppManifestJson(application, {
|
|
4992
|
-
api: options.api,
|
|
4993
|
-
resources: options.payload.resources,
|
|
4994
|
-
profile: options.payload.profile
|
|
4995
|
-
})
|
|
4996
|
-
},
|
|
4997
|
-
{
|
|
4998
|
-
path: `${base}/topolo.cloudcontrol.json`,
|
|
4999
|
-
purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
|
|
5000
|
-
requiredSignals: [
|
|
5001
|
-
"production URLs and Cloudflare targets are explicit",
|
|
5002
|
-
"resource bindings and required secrets are declared without secret values"
|
|
5003
|
-
],
|
|
5004
|
-
content: cloudcontrolJson(application, {
|
|
5005
|
-
api: options.api,
|
|
5006
|
-
audience: options.payload.audience,
|
|
5007
|
-
profile: options.payload.profile,
|
|
5008
|
-
resources: options.payload.resources
|
|
5009
|
-
})
|
|
5010
|
-
},
|
|
5011
|
-
{
|
|
5012
|
-
path: `${base}/.github/workflows/topolo-conformance.yml`,
|
|
5013
|
-
purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
|
|
5014
|
-
requiredSignals: [
|
|
5015
|
-
"checks out this repo under its Apps lane directory",
|
|
5016
|
-
"checks out TopoloDocs under Apps/system/TopoloDocs",
|
|
5017
|
-
"checks out topolo-sdk and TopoloCli from main so the gate does not wait for an npm publish",
|
|
5018
|
-
"runs topolo apps audit with --local and --fail-on needs_review"
|
|
5019
|
-
],
|
|
5020
|
-
content: options.ciWorkflow ?? ""
|
|
5021
|
-
},
|
|
5022
|
-
{
|
|
5023
|
-
path: `Apps/system/TopoloDocs/src/content/systems/topolo-${slug}.json`,
|
|
5024
|
-
purpose: "Canonical system registry entry.",
|
|
5025
|
-
requiredSignals: [
|
|
5026
|
-
`repo_paths includes ${base}`,
|
|
5027
|
-
"doc_paths includes internal handbook coverage",
|
|
5028
|
-
"security_assurance records the auth, tenancy, deployment, and verification boundary"
|
|
5029
|
-
],
|
|
5030
|
-
content: systemJson(application, {
|
|
5031
|
-
api: options.api,
|
|
5032
|
-
audience: options.payload.audience,
|
|
5033
|
-
tenancy: options.payload.tenancy,
|
|
5034
|
-
portfolio: options.payload.portfolio,
|
|
5035
|
-
profile: options.payload.profile,
|
|
5036
|
-
resources: options.payload.resources
|
|
5037
|
-
})
|
|
5038
|
-
},
|
|
5039
|
-
{
|
|
5040
|
-
path: `Apps/system/TopoloDocs/src/content/internal/apps/${slug}.mdx`,
|
|
5041
|
-
purpose: "Internal operator handbook.",
|
|
5042
|
-
requiredSignals: [
|
|
5043
|
-
"documents ownership, routes, auth boundary, deployment, operations, and verification",
|
|
5044
|
-
"last_verified is updated when behavior changes"
|
|
5045
|
-
],
|
|
5046
|
-
content: internalDocs(application, {
|
|
5047
|
-
api: options.api,
|
|
5048
|
-
audience: options.payload.audience,
|
|
5049
|
-
tenancy: options.payload.tenancy,
|
|
5050
|
-
portfolio: options.payload.portfolio,
|
|
5051
|
-
profile: options.payload.profile,
|
|
5052
|
-
resources: options.payload.resources
|
|
5053
|
-
})
|
|
5054
|
-
},
|
|
5055
|
-
{
|
|
5056
|
-
path: `Apps/system/TopoloDocs/src/content/public/applications/${slug}.mdx`,
|
|
5057
|
-
purpose: "Public application overview.",
|
|
5058
|
-
requiredSignals: [
|
|
5059
|
-
"documents the product surface without internal-only details",
|
|
5060
|
-
"links back to the canonical system registry entry"
|
|
5061
|
-
],
|
|
5062
|
-
content: publicDocs(application, {
|
|
5063
|
-
api: options.api,
|
|
5064
|
-
audience: options.payload.audience,
|
|
5065
|
-
tenancy: options.payload.tenancy,
|
|
5066
|
-
portfolio: options.payload.portfolio,
|
|
5067
|
-
profile: options.payload.profile,
|
|
5068
|
-
resources: options.payload.resources
|
|
5069
|
-
})
|
|
5070
|
-
}
|
|
5071
|
-
);
|
|
5072
|
-
if (options.api && application.serviceId) {
|
|
5073
|
-
files.push(
|
|
5074
|
-
{
|
|
5075
|
-
path: `Apps/system/TopoloAuth/catalog/service-manifests/${application.serviceId}.json`,
|
|
5076
|
-
purpose: "Auth service manifest for first-party permissions, role bundles, and API-key scopes.",
|
|
5077
|
-
requiredSignals: [
|
|
5078
|
-
"serviceId matches the generated Worker service id",
|
|
5079
|
-
"permissions, role bundles, and API-key scopes exist before the app is catalog-visible",
|
|
5080
|
-
"systems metadata maps the service back to the generated TopoloDocs entries"
|
|
5081
|
-
],
|
|
5082
|
-
content: serviceManifestJson(application, {
|
|
5083
|
-
audience: options.payload.audience,
|
|
5084
|
-
tenancy: options.payload.tenancy
|
|
5085
|
-
})
|
|
5086
|
-
}
|
|
5087
|
-
);
|
|
5088
|
-
}
|
|
5089
|
-
} else {
|
|
5090
|
-
files.push(
|
|
5091
|
-
{
|
|
5092
|
-
path: `${base}/topolo.app.json`,
|
|
5093
|
-
purpose: "Third-party Topolo application manifest.",
|
|
5094
|
-
requiredSignals: [
|
|
5095
|
-
"declares the external app id, callback URLs, and requested platform scopes",
|
|
5096
|
-
"does not include internal Topolo CloudControl or canonical-docs ownership"
|
|
5097
|
-
],
|
|
5098
|
-
content: developerManifest(application, {
|
|
5099
|
-
api: options.api,
|
|
5100
|
-
audience: options.payload.audience,
|
|
5101
|
-
tenancy: options.payload.tenancy,
|
|
5102
|
-
surfaceModel: options.payload.surfaceModel,
|
|
5103
|
-
containerAppId: options.payload.containerAppId,
|
|
5104
|
-
distributionModel: options.payload.distributionModel,
|
|
5105
|
-
capabilities: options.payload.capabilities
|
|
5106
|
-
})
|
|
5107
|
-
},
|
|
5108
|
-
{
|
|
5109
|
-
path: `${base}/.env.example`,
|
|
5110
|
-
purpose: "Developer environment template for local auth configuration.",
|
|
5111
|
-
requiredSignals: [
|
|
5112
|
-
"names the Topolo Auth URL",
|
|
5113
|
-
"keeps OAuth client ids and service ids out of source control by default"
|
|
5114
|
-
],
|
|
5115
|
-
content: envExample(application)
|
|
5116
|
-
}
|
|
5117
|
-
);
|
|
4814
|
+
function toConstantCase(value) {
|
|
4815
|
+
return value.split(/[-_]/).filter(Boolean).join("_").toUpperCase();
|
|
4816
|
+
}
|
|
4817
|
+
function toPascalCase(value) {
|
|
4818
|
+
const acronyms = /* @__PURE__ */ new Set(["api", "cli", "crm", "mcp", "mdm", "p2p"]);
|
|
4819
|
+
return value.split(/[-_]/).filter(Boolean).map((part) => acronyms.has(part.toLowerCase()) ? part.toUpperCase() : part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
|
|
4820
|
+
}
|
|
4821
|
+
function toTitle(value) {
|
|
4822
|
+
return value.replace(/^topolo[-_\s]*/i, "Topolo ").split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
4823
|
+
}
|
|
4824
|
+
function json(value) {
|
|
4825
|
+
return JSON.stringify(value, null, 2);
|
|
4826
|
+
}
|
|
4827
|
+
async function pathExists(path4) {
|
|
4828
|
+
try {
|
|
4829
|
+
await access(path4, constants.F_OK);
|
|
4830
|
+
return true;
|
|
4831
|
+
} catch {
|
|
4832
|
+
return false;
|
|
5118
4833
|
}
|
|
5119
|
-
if (options.browser) {
|
|
5120
|
-
if (target === "platform") {
|
|
5121
|
-
files.push({
|
|
5122
|
-
path: `${browserBase}/src/service-identity.ts`,
|
|
5123
|
-
purpose: "Central service identity resolver shared by browser and Worker runtime surfaces.",
|
|
5124
|
-
requiredSignals: [
|
|
5125
|
-
"exports the generated app id, display name, and stable Auth service slug",
|
|
5126
|
-
"requires the provisioned Auth service id from the Worker deployment config"
|
|
5127
|
-
],
|
|
5128
|
-
content: serviceIdentityTs(application)
|
|
5129
|
-
});
|
|
5130
|
-
}
|
|
5131
|
-
files.push({
|
|
5132
|
-
path: `${browserBase}/src/App.tsx`,
|
|
5133
|
-
purpose: "Authenticated browser surface.",
|
|
5134
|
-
requiredSignals: [
|
|
5135
|
-
"bootstraps routes, auth provider, and shared shell through @topolo-io/app-shell",
|
|
5136
|
-
"uses @topolo-io/auth-client through the app-shell wrapper for app-origin auth flows",
|
|
5137
|
-
"keeps launcher and account-menu behavior on the shared platform surface",
|
|
5138
|
-
"passes app-specific account actions through accountMenuItems or TopoloAccountMenu"
|
|
5139
|
-
],
|
|
5140
|
-
content: appTsx(application, target, {
|
|
5141
|
-
audience: options.payload.audience,
|
|
5142
|
-
portfolio: options.payload.portfolio
|
|
5143
|
-
})
|
|
5144
|
-
});
|
|
5145
|
-
}
|
|
5146
|
-
if (options.api) {
|
|
5147
|
-
files.push(
|
|
5148
|
-
{
|
|
5149
|
-
path: `${apiBase}/wrangler.toml`,
|
|
5150
|
-
purpose: "Cloudflare Worker runtime configuration.",
|
|
5151
|
-
requiredSignals: [
|
|
5152
|
-
"bindings match topolo.cloudcontrol.json or topolo.app.json",
|
|
5153
|
-
"secrets are referenced by name only"
|
|
5154
|
-
],
|
|
5155
|
-
content: target === "platform" ? platformApiWranglerToml(application, options.payload.resources, options.payload.profile) : wranglerToml(application, target, options.payload.resources, options.payload.profile)
|
|
5156
|
-
},
|
|
5157
|
-
{
|
|
5158
|
-
path: `${apiBase}/src/worker.ts`,
|
|
5159
|
-
purpose: "Protected service boundary.",
|
|
5160
|
-
requiredSignals: [
|
|
5161
|
-
"uses @topolo-io/worker-runtime for Worker auth, gatewaying, headers, and request helpers",
|
|
5162
|
-
"derives organization from the credential rather than client-supplied orgId",
|
|
5163
|
-
"emits or propagates X-Topolo-Request-Id and X-Topolo-Client"
|
|
5164
|
-
],
|
|
5165
|
-
content: workerTs(application, target, {
|
|
5166
|
-
audience: options.payload.audience,
|
|
5167
|
-
tenancy: options.payload.tenancy,
|
|
5168
|
-
profile: options.payload.profile,
|
|
5169
|
-
resources: options.payload.resources
|
|
5170
|
-
})
|
|
5171
|
-
}
|
|
5172
|
-
);
|
|
5173
|
-
if (target === "platform") {
|
|
5174
|
-
files.push(
|
|
5175
|
-
{
|
|
5176
|
-
path: `${apiBase}/src/service-identity.ts`,
|
|
5177
|
-
purpose: "Runtime service identity resolver for the API Worker.",
|
|
5178
|
-
requiredSignals: [
|
|
5179
|
-
"resolves the concrete Auth service id from Worker vars",
|
|
5180
|
-
"keeps the API Worker independent from browser-only import.meta usage"
|
|
5181
|
-
],
|
|
5182
|
-
content: apiServiceIdentityTs(application)
|
|
5183
|
-
},
|
|
5184
|
-
{
|
|
5185
|
-
path: `${browserBase}/src/worker.ts`,
|
|
5186
|
-
purpose: "Browser Worker that serves assets and forwards API paths to the API Worker binding.",
|
|
5187
|
-
requiredSignals: [
|
|
5188
|
-
"keeps the frontend Worker thin",
|
|
5189
|
-
"routes /api/* to the API service binding instead of owning backend routes locally"
|
|
5190
|
-
],
|
|
5191
|
-
content: webWorkerTs(application)
|
|
5192
|
-
},
|
|
5193
|
-
{
|
|
5194
|
-
path: `${browserBase}/wrangler.toml`,
|
|
5195
|
-
purpose: "Cloudflare Worker static-assets configuration for the browser surface.",
|
|
5196
|
-
requiredSignals: [
|
|
5197
|
-
"serves Vite assets through a Worker with assets",
|
|
5198
|
-
"binds the frontend Worker to the API Worker for /api/* requests"
|
|
5199
|
-
],
|
|
5200
|
-
content: platformWebWranglerToml(application)
|
|
5201
|
-
}
|
|
5202
|
-
);
|
|
5203
|
-
}
|
|
5204
|
-
}
|
|
5205
|
-
return files;
|
|
5206
4834
|
}
|
|
5207
|
-
function
|
|
5208
|
-
const
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
`topolo apps scaffold ${scaffoldId}${audienceFlags} --write --repo-root .`,
|
|
5213
|
-
`cd topolo-${scaffoldId}`,
|
|
5214
|
-
"npm install",
|
|
5215
|
-
"npm run typecheck",
|
|
5216
|
-
"npm run build"
|
|
5217
|
-
];
|
|
4835
|
+
function resolveInside(root, filePath) {
|
|
4836
|
+
const resolved = resolve(root, filePath);
|
|
4837
|
+
const relativePath = relative(root, resolved);
|
|
4838
|
+
if (relativePath.startsWith("..") || relativePath === "" || relativePath.startsWith("/")) {
|
|
4839
|
+
throw new Error(`Scaffold file path escapes the target root: ${filePath}`);
|
|
5218
4840
|
}
|
|
5219
|
-
|
|
5220
|
-
const profileFlags = options.profile ? ` --profile ${options.profile}` : "";
|
|
5221
|
-
return [
|
|
5222
|
-
`topolo platform apps scaffold ${scaffoldId}${portfolioFlags}${profileFlags} --write --provision --repo-root /Users/ashes/Projects/Topolo/Platform --json`,
|
|
5223
|
-
"topolo apps requirements --json",
|
|
5224
|
-
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on needs_review --json`,
|
|
5225
|
-
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on partial --json`
|
|
5226
|
-
];
|
|
4841
|
+
return resolved;
|
|
5227
4842
|
}
|
|
5228
|
-
function
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
" - name: Checkout Topolo CLI",
|
|
5243
|
-
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
5244
|
-
" uses: actions/checkout@v4",
|
|
5245
|
-
" with:",
|
|
5246
|
-
" repository: Topolo-io/TopoloCli",
|
|
5247
|
-
" path: Apps/system/TopoloCLI",
|
|
5248
|
-
" token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}"
|
|
5249
|
-
];
|
|
5250
|
-
return [
|
|
5251
|
-
"name: Topolo App Conformance",
|
|
5252
|
-
"",
|
|
5253
|
-
"on:",
|
|
5254
|
-
" pull_request:",
|
|
5255
|
-
" push:",
|
|
5256
|
-
" branches:",
|
|
5257
|
-
" - main",
|
|
5258
|
-
" workflow_dispatch:",
|
|
5259
|
-
"",
|
|
5260
|
-
"permissions:",
|
|
5261
|
-
" contents: read",
|
|
5262
|
-
"",
|
|
5263
|
-
"jobs:",
|
|
5264
|
-
" conformance:",
|
|
5265
|
-
" runs-on: ubuntu-latest",
|
|
5266
|
-
" env:",
|
|
5267
|
-
` HAS_TOPOLO_PACKAGES_APP: \${{ secrets.TOPOLO_PACKAGES_APP_ID != '' && secrets.TOPOLO_PACKAGES_APP_PRIVATE_KEY != '' }}`,
|
|
5268
|
-
` HAS_TOPOLO_CI_REPO_TOKEN: \${{ secrets.TOPOLO_CI_REPO_TOKEN != '' }}`,
|
|
5269
|
-
" steps:",
|
|
5270
|
-
" - name: Checkout application",
|
|
5271
|
-
" uses: actions/checkout@v4",
|
|
5272
|
-
" with:",
|
|
5273
|
-
` path: ${appPath}`,
|
|
5274
|
-
"",
|
|
5275
|
-
" - name: Mint Topolo cross-repo token",
|
|
5276
|
-
" id: app-token",
|
|
5277
|
-
` if: env.HAS_TOPOLO_PACKAGES_APP == 'true'`,
|
|
5278
|
-
" uses: actions/create-github-app-token@v1",
|
|
5279
|
-
" with:",
|
|
5280
|
-
` app-id: \${{ secrets.TOPOLO_PACKAGES_APP_ID }}`,
|
|
5281
|
-
` private-key: \${{ secrets.TOPOLO_PACKAGES_APP_PRIVATE_KEY }}`,
|
|
5282
|
-
" owner: Topolo-io",
|
|
5283
|
-
...docsCheckoutStep,
|
|
5284
|
-
"",
|
|
5285
|
-
" - name: Checkout Topolo SDK",
|
|
5286
|
-
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
5287
|
-
" uses: actions/checkout@v4",
|
|
5288
|
-
" with:",
|
|
5289
|
-
" repository: Topolo-io/topolo-sdk",
|
|
5290
|
-
" path: Apps/packages/topolo-sdk",
|
|
5291
|
-
" token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}",
|
|
5292
|
-
...cliCheckoutStep,
|
|
5293
|
-
"",
|
|
5294
|
-
" - name: Skip conformance without cross-repo token",
|
|
5295
|
-
` if: steps.app-token.outputs.token == '' && env.HAS_TOPOLO_CI_REPO_TOKEN != 'true'`,
|
|
5296
|
-
" run: |",
|
|
5297
|
-
' echo "::warning::Skipping Topolo app conformance because no Topolo cross-repo token is available to this workflow context."',
|
|
5298
|
-
"",
|
|
5299
|
-
" - name: Setup Node.js",
|
|
5300
|
-
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
5301
|
-
" uses: actions/setup-node@v4",
|
|
5302
|
-
" with:",
|
|
5303
|
-
" node-version: 20",
|
|
5304
|
-
"",
|
|
5305
|
-
" - name: Build Topolo SDK",
|
|
5306
|
-
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
5307
|
-
" working-directory: Apps/packages/topolo-sdk",
|
|
5308
|
-
" run: |",
|
|
5309
|
-
" npm install --no-audit --no-fund",
|
|
5310
|
-
" npm run build",
|
|
5311
|
-
"",
|
|
5312
|
-
" - name: Build Topolo CLI",
|
|
5313
|
-
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
5314
|
-
" working-directory: Apps/system/TopoloCLI",
|
|
5315
|
-
" run: |",
|
|
5316
|
-
" npm install --no-audit --no-fund",
|
|
5317
|
-
" npm run build",
|
|
5318
|
-
"",
|
|
5319
|
-
" - name: Run Topolo app conformance audit",
|
|
5320
|
-
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
5321
|
-
" run: |",
|
|
5322
|
-
` node Apps/system/TopoloCLI/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`,
|
|
5323
|
-
"",
|
|
5324
|
-
" - name: Record green CI evidence",
|
|
5325
|
-
` if: \${{ success() && (steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true') }}`,
|
|
5326
|
-
` working-directory: ${appPath}`,
|
|
5327
|
-
" run: |",
|
|
5328
|
-
` node "$GITHUB_WORKSPACE/Apps/system/TopoloCLI/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`
|
|
5329
|
-
].join("\n");
|
|
4843
|
+
function escapeHtml(value) {
|
|
4844
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4845
|
+
}
|
|
4846
|
+
function escapeSingleQuoted(value) {
|
|
4847
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
4848
|
+
}
|
|
4849
|
+
function escapeFrontmatter(value) {
|
|
4850
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
4851
|
+
}
|
|
4852
|
+
function firstPartyAppPath(appDir, audience) {
|
|
4853
|
+
if (appDir === "TopoloCli" || appDir === "TopoloCLI") return "Apps/system/TopoloCLI";
|
|
4854
|
+
if (appDir === "TopoloDocs") return "Apps/system/TopoloDocs";
|
|
4855
|
+
if (appDir === "TopoloAuth") return "Apps/system/TopoloAuth";
|
|
4856
|
+
return `${audience === "personal" ? "Apps/personal" : "Apps/business"}/${appDir}`;
|
|
5330
4857
|
}
|
|
4858
|
+
|
|
4859
|
+
// src/commands/app-scaffold-package-template.ts
|
|
5331
4860
|
function packageJson(application, target, options) {
|
|
5332
4861
|
const dependencies = {};
|
|
5333
4862
|
const devDependencies = {
|
|
@@ -5423,11 +4952,12 @@ function hasScaffoldResources(resources) {
|
|
|
5423
4952
|
function resourceConfig(application, profile) {
|
|
5424
4953
|
const slug = toSlug(application.id);
|
|
5425
4954
|
const pascal = toPascalCase(application.id);
|
|
4955
|
+
const constant = toConstantCase(application.id);
|
|
5426
4956
|
if (profile === "spatial-assets") {
|
|
5427
4957
|
return {
|
|
5428
|
-
d1Binding:
|
|
5429
|
-
d1ProductionName: `topolo-${slug}-
|
|
5430
|
-
d1StagingName: `topolo-${slug}-staging
|
|
4958
|
+
d1Binding: `${constant}_CORE_DB`,
|
|
4959
|
+
d1ProductionName: `topolo-${slug}-core-prod`,
|
|
4960
|
+
d1StagingName: `topolo-${slug}-core-staging`,
|
|
5431
4961
|
r2Binding: "SPACES_ASSETS",
|
|
5432
4962
|
r2ProductionName: `topolo-${slug}-assets`,
|
|
5433
4963
|
r2StagingName: `topolo-${slug}-assets-staging`,
|
|
@@ -5443,9 +4973,9 @@ function resourceConfig(application, profile) {
|
|
|
5443
4973
|
};
|
|
5444
4974
|
}
|
|
5445
4975
|
return {
|
|
5446
|
-
d1Binding:
|
|
5447
|
-
d1ProductionName: `topolo-${slug}-prod`,
|
|
5448
|
-
d1StagingName: `topolo-${slug}-staging`,
|
|
4976
|
+
d1Binding: `${constant}_CORE_DB`,
|
|
4977
|
+
d1ProductionName: `topolo-${slug}-core-prod`,
|
|
4978
|
+
d1StagingName: `topolo-${slug}-core-staging`,
|
|
5449
4979
|
r2Binding: "APP_ASSETS",
|
|
5450
4980
|
r2ProductionName: `topolo-${slug}-assets`,
|
|
5451
4981
|
r2StagingName: `topolo-${slug}-assets-staging`,
|
|
@@ -5460,6 +4990,8 @@ function resourceConfig(application, profile) {
|
|
|
5460
4990
|
containerStagingName: `topolo-${slug}-processor-staging`
|
|
5461
4991
|
};
|
|
5462
4992
|
}
|
|
4993
|
+
|
|
4994
|
+
// src/commands/app-scaffold-browser-template.ts
|
|
5463
4995
|
function readme(application, target, options) {
|
|
5464
4996
|
const displayName = toTitle(application.name);
|
|
5465
4997
|
const slug = toSlug(application.id);
|
|
@@ -5904,6 +5436,8 @@ function stylesCss() {
|
|
|
5904
5436
|
""
|
|
5905
5437
|
].join("\n");
|
|
5906
5438
|
}
|
|
5439
|
+
|
|
5440
|
+
// src/commands/app-scaffold-app-template.ts
|
|
5907
5441
|
function appTsx(application, target, options) {
|
|
5908
5442
|
if (target === "developer") return developerAppTsx(application);
|
|
5909
5443
|
return platformAppTsx(application, options);
|
|
@@ -6331,6 +5865,13 @@ function developerAppTsx(application) {
|
|
|
6331
5865
|
""
|
|
6332
5866
|
].join("\n");
|
|
6333
5867
|
}
|
|
5868
|
+
|
|
5869
|
+
// src/commands/app-scaffold-cloudflare-constants.ts
|
|
5870
|
+
var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
|
|
5871
|
+
var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
|
|
5872
|
+
var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
|
|
5873
|
+
|
|
5874
|
+
// src/commands/app-scaffold-worker-template.ts
|
|
6334
5875
|
function webWorkerTs(application) {
|
|
6335
5876
|
const serviceName = toTitle(application.name);
|
|
6336
5877
|
return [
|
|
@@ -6448,7 +5989,7 @@ function platformApiWranglerToml(application, resources, profile) {
|
|
|
6448
5989
|
"",
|
|
6449
5990
|
"[vars]",
|
|
6450
5991
|
'AUTH_BASE_URL = "https://auth.topolo.app"',
|
|
6451
|
-
'NEXUS_GATEWAY_URL = "https://nexus
|
|
5992
|
+
'NEXUS_GATEWAY_URL = "https://nexus.topolo.app"',
|
|
6452
5993
|
`NEXUS_APP_ID = "${slug}"`,
|
|
6453
5994
|
`SERVICE_ID = "${application.serviceId ?? ""}"`,
|
|
6454
5995
|
`VITE_TOPOLO_SERVICE_ID = "${application.serviceId ?? ""}"`,
|
|
@@ -6463,7 +6004,7 @@ function platformApiWranglerToml(application, resources, profile) {
|
|
|
6463
6004
|
"",
|
|
6464
6005
|
"[env.staging.vars]",
|
|
6465
6006
|
'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
|
|
6466
|
-
'NEXUS_GATEWAY_URL = "https://nexus
|
|
6007
|
+
'NEXUS_GATEWAY_URL = "https://nexus.stg.topolo.us"',
|
|
6467
6008
|
`NEXUS_APP_ID = "${slug}"`,
|
|
6468
6009
|
`SERVICE_ID = "${application.serviceId ?? ""}"`,
|
|
6469
6010
|
`VITE_TOPOLO_SERVICE_ID = "${application.serviceId ?? ""}"`,
|
|
@@ -6478,7 +6019,7 @@ function platformApiWranglerToml(application, resources, profile) {
|
|
|
6478
6019
|
"",
|
|
6479
6020
|
"[env.production.vars]",
|
|
6480
6021
|
'AUTH_BASE_URL = "https://auth.topolo.app"',
|
|
6481
|
-
'NEXUS_GATEWAY_URL = "https://nexus
|
|
6022
|
+
'NEXUS_GATEWAY_URL = "https://nexus.topolo.app"',
|
|
6482
6023
|
`NEXUS_APP_ID = "${slug}"`,
|
|
6483
6024
|
`SERVICE_ID = "${application.serviceId ?? ""}"`,
|
|
6484
6025
|
`VITE_TOPOLO_SERVICE_ID = "${application.serviceId ?? ""}"`,
|
|
@@ -6574,7 +6115,7 @@ function wranglerToml(application, target, resources, profile) {
|
|
|
6574
6115
|
"",
|
|
6575
6116
|
"[vars]",
|
|
6576
6117
|
'AUTH_BASE_URL = "https://auth.topolo.app"',
|
|
6577
|
-
'NEXUS_GATEWAY_URL = "https://nexus
|
|
6118
|
+
'NEXUS_GATEWAY_URL = "https://nexus.topolo.app"',
|
|
6578
6119
|
runtimeIdentityVar,
|
|
6579
6120
|
""
|
|
6580
6121
|
);
|
|
@@ -6629,7 +6170,7 @@ function wranglerToml(application, target, resources, profile) {
|
|
|
6629
6170
|
"",
|
|
6630
6171
|
"[env.staging.vars]",
|
|
6631
6172
|
'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
|
|
6632
|
-
'NEXUS_GATEWAY_URL = "https://nexus
|
|
6173
|
+
'NEXUS_GATEWAY_URL = "https://nexus.stg.topolo.us"',
|
|
6633
6174
|
runtimeIdentityVar,
|
|
6634
6175
|
""
|
|
6635
6176
|
);
|
|
@@ -6929,6 +6470,632 @@ function developerWorkerTs(application) {
|
|
|
6929
6470
|
""
|
|
6930
6471
|
].join("\n");
|
|
6931
6472
|
}
|
|
6473
|
+
|
|
6474
|
+
// src/commands/app-scaffold-workflows.ts
|
|
6475
|
+
function scaffoldCommands(options) {
|
|
6476
|
+
const scaffoldId = toSlug(options.id);
|
|
6477
|
+
if (options.target === "developer") {
|
|
6478
|
+
const audienceFlags = options.audience === "personal" ? " --audience personal --tenancy household" : "";
|
|
6479
|
+
return [
|
|
6480
|
+
`topolo apps scaffold ${scaffoldId}${audienceFlags} --write --repo-root .`,
|
|
6481
|
+
`cd topolo-${scaffoldId}`,
|
|
6482
|
+
"npm install",
|
|
6483
|
+
"npm run typecheck",
|
|
6484
|
+
"npm run build"
|
|
6485
|
+
];
|
|
6486
|
+
}
|
|
6487
|
+
const portfolioFlags = options.portfolio === "personal" ? " --portfolio personal" : "";
|
|
6488
|
+
const profileFlags = options.profile ? ` --profile ${options.profile}` : "";
|
|
6489
|
+
return [
|
|
6490
|
+
`topolo platform apps scaffold ${scaffoldId}${portfolioFlags}${profileFlags} --write --provision --repo-root /Users/ashes/Projects/Topolo/Platform --json`,
|
|
6491
|
+
"topolo apps requirements --json",
|
|
6492
|
+
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on needs_review --json`,
|
|
6493
|
+
`topolo apps audit ${options.id} --local --repo-root /Users/ashes/Projects/Topolo/Platform --fail-on partial --json`
|
|
6494
|
+
];
|
|
6495
|
+
}
|
|
6496
|
+
function topoloConformanceWorkflow(options) {
|
|
6497
|
+
const appPath = firstPartyAppPath(options.appDir, options.audience);
|
|
6498
|
+
const docsCheckoutStep = options.appDir === "TopoloDocs" ? [] : [
|
|
6499
|
+
"",
|
|
6500
|
+
" - name: Checkout TopoloDocs",
|
|
6501
|
+
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
6502
|
+
" uses: actions/checkout@v4",
|
|
6503
|
+
" with:",
|
|
6504
|
+
" repository: Topolo-io/TopoloDocs",
|
|
6505
|
+
" path: Apps/system/TopoloDocs",
|
|
6506
|
+
" token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}"
|
|
6507
|
+
];
|
|
6508
|
+
const cliCheckoutStep = options.appDir === "TopoloCLI" || options.appDir === "TopoloCli" ? [] : [
|
|
6509
|
+
"",
|
|
6510
|
+
" - name: Checkout Topolo CLI",
|
|
6511
|
+
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
6512
|
+
" uses: actions/checkout@v4",
|
|
6513
|
+
" with:",
|
|
6514
|
+
" repository: Topolo-io/TopoloCli",
|
|
6515
|
+
" path: Apps/system/TopoloCLI",
|
|
6516
|
+
" token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}"
|
|
6517
|
+
];
|
|
6518
|
+
return [
|
|
6519
|
+
"name: Topolo App Conformance",
|
|
6520
|
+
"",
|
|
6521
|
+
"on:",
|
|
6522
|
+
" pull_request:",
|
|
6523
|
+
" push:",
|
|
6524
|
+
" branches:",
|
|
6525
|
+
" - main",
|
|
6526
|
+
" workflow_dispatch:",
|
|
6527
|
+
"",
|
|
6528
|
+
"permissions:",
|
|
6529
|
+
" contents: read",
|
|
6530
|
+
"",
|
|
6531
|
+
"jobs:",
|
|
6532
|
+
" conformance:",
|
|
6533
|
+
" runs-on: ubuntu-latest",
|
|
6534
|
+
" env:",
|
|
6535
|
+
` HAS_TOPOLO_PACKAGES_APP: \${{ secrets.TOPOLO_PACKAGES_APP_ID != '' && secrets.TOPOLO_PACKAGES_APP_PRIVATE_KEY != '' }}`,
|
|
6536
|
+
` HAS_TOPOLO_CI_REPO_TOKEN: \${{ secrets.TOPOLO_CI_REPO_TOKEN != '' }}`,
|
|
6537
|
+
" steps:",
|
|
6538
|
+
" - name: Checkout application",
|
|
6539
|
+
" uses: actions/checkout@v4",
|
|
6540
|
+
" with:",
|
|
6541
|
+
` path: ${appPath}`,
|
|
6542
|
+
"",
|
|
6543
|
+
" - name: Mint Topolo cross-repo token",
|
|
6544
|
+
" id: app-token",
|
|
6545
|
+
` if: env.HAS_TOPOLO_PACKAGES_APP == 'true'`,
|
|
6546
|
+
" uses: actions/create-github-app-token@v1",
|
|
6547
|
+
" with:",
|
|
6548
|
+
` app-id: \${{ secrets.TOPOLO_PACKAGES_APP_ID }}`,
|
|
6549
|
+
` private-key: \${{ secrets.TOPOLO_PACKAGES_APP_PRIVATE_KEY }}`,
|
|
6550
|
+
" owner: Topolo-io",
|
|
6551
|
+
...docsCheckoutStep,
|
|
6552
|
+
"",
|
|
6553
|
+
" - name: Checkout Topolo SDK",
|
|
6554
|
+
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
6555
|
+
" uses: actions/checkout@v4",
|
|
6556
|
+
" with:",
|
|
6557
|
+
" repository: Topolo-io/topolo-sdk",
|
|
6558
|
+
" path: Apps/packages/topolo-sdk",
|
|
6559
|
+
" token: ${{ steps.app-token.outputs.token || secrets.TOPOLO_CI_REPO_TOKEN }}",
|
|
6560
|
+
...cliCheckoutStep,
|
|
6561
|
+
"",
|
|
6562
|
+
" - name: Skip conformance without cross-repo token",
|
|
6563
|
+
` if: steps.app-token.outputs.token == '' && env.HAS_TOPOLO_CI_REPO_TOKEN != 'true'`,
|
|
6564
|
+
" run: |",
|
|
6565
|
+
' echo "::warning::Skipping Topolo app conformance because no Topolo cross-repo token is available to this workflow context."',
|
|
6566
|
+
"",
|
|
6567
|
+
" - name: Setup Node.js",
|
|
6568
|
+
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
6569
|
+
" uses: actions/setup-node@v4",
|
|
6570
|
+
" with:",
|
|
6571
|
+
" node-version: 20",
|
|
6572
|
+
"",
|
|
6573
|
+
" - name: Build Topolo SDK",
|
|
6574
|
+
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
6575
|
+
" working-directory: Apps/packages/topolo-sdk",
|
|
6576
|
+
" run: |",
|
|
6577
|
+
" npm install --no-audit --no-fund",
|
|
6578
|
+
" npm run build",
|
|
6579
|
+
"",
|
|
6580
|
+
" - name: Build Topolo CLI",
|
|
6581
|
+
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
6582
|
+
" working-directory: Apps/system/TopoloCLI",
|
|
6583
|
+
" run: |",
|
|
6584
|
+
" npm install --no-audit --no-fund",
|
|
6585
|
+
" npm run build",
|
|
6586
|
+
"",
|
|
6587
|
+
" - name: Run Topolo app conformance audit",
|
|
6588
|
+
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
6589
|
+
" run: |",
|
|
6590
|
+
` node Apps/system/TopoloCLI/dist/index.js apps audit ${options.id} --local --repo-root "$GITHUB_WORKSPACE" --fail-on needs_review --json`,
|
|
6591
|
+
"",
|
|
6592
|
+
" - name: Record green CI evidence",
|
|
6593
|
+
` if: \${{ success() && (steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true') }}`,
|
|
6594
|
+
` working-directory: ${appPath}`,
|
|
6595
|
+
" run: |",
|
|
6596
|
+
` node "$GITHUB_WORKSPACE/Apps/system/TopoloCLI/dist/index.js" apps record-evidence ${options.id} --build --typecheck --test --commit "$GITHUB_SHA"`
|
|
6597
|
+
].join("\n");
|
|
6598
|
+
}
|
|
6599
|
+
|
|
6600
|
+
// src/commands/app-scaffold.ts
|
|
6601
|
+
var CURRENT_VERIFICATION_DATE = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6602
|
+
function generateOpaqueServiceId() {
|
|
6603
|
+
return `srv_${randomBytes(9).toString("base64url")}`;
|
|
6604
|
+
}
|
|
6605
|
+
function applicationScaffoldPayload(application, options = {}) {
|
|
6606
|
+
const target = normalizeTarget(options.target);
|
|
6607
|
+
const portfolio = target === "platform" ? normalizePortfolio(options.portfolio) : null;
|
|
6608
|
+
const requestedId = normalizeApplicationId(application);
|
|
6609
|
+
const id = target === "platform" ? normalizeFirstPartyApplicationId(requestedId) : requestedId;
|
|
6610
|
+
const slug = toSlug(id);
|
|
6611
|
+
const catalogApplication = APPLICATIONS[id];
|
|
6612
|
+
const browser = options.browser !== false;
|
|
6613
|
+
const api = options.api ?? target === "platform";
|
|
6614
|
+
const audience = normalizeAudience(
|
|
6615
|
+
options.audience,
|
|
6616
|
+
target === "platform" ? portfolio : null
|
|
6617
|
+
);
|
|
6618
|
+
const tenancy = normalizeTenancy(options.tenancy, audience);
|
|
6619
|
+
const surfaceModel = normalizeSurfaceModel(options.surfaceModel);
|
|
6620
|
+
const containerAppId = normalizeContainerAppId(options.containerAppId, surfaceModel);
|
|
6621
|
+
const profile = normalizeScaffoldProfile(options.profile);
|
|
6622
|
+
const resources = resolveScaffoldResources(profile, options);
|
|
6623
|
+
const ownerType = target === "platform" ? "topolo_first_party" : "third_party";
|
|
6624
|
+
const distributionModel = target === "platform" ? "marketplace" : "organization_internal";
|
|
6625
|
+
const appDir = options.appDir ?? catalogApplication?.appDir ?? (target === "platform" ? `Topolo${toPascalCase(id)}` : `topolo-${slug}`);
|
|
6626
|
+
const name = options.name ?? catalogApplication?.name ?? `topolo-${slug}`;
|
|
6627
|
+
const packageName = options.packageName ?? catalogApplication?.packageName ?? `topolo-${slug}`;
|
|
6628
|
+
const serviceSlug = api ? target === "platform" ? `topolo-${slug}` : id : null;
|
|
6629
|
+
const defaultServiceId = target === "platform" ? generateOpaqueServiceId() : catalogApplication?.services[0] ?? id;
|
|
6630
|
+
const serviceId = api ? options.service ?? defaultServiceId : null;
|
|
6631
|
+
const productionUrl = browser ? options.productionUrl ?? catalogApplication?.productionUrl ?? `https://${slug}.topolo.app` : null;
|
|
6632
|
+
const scopes = scaffoldScopes({ browser, api });
|
|
6633
|
+
const capabilities = scaffoldCapabilities({ browser, api, distributionModel });
|
|
6634
|
+
const ciWorkflowPath = target === "platform" ? ".github/workflows/topolo-conformance.yml" : null;
|
|
6635
|
+
const ciWorkflow = target === "platform" ? topoloConformanceWorkflow({ id, appDir, audience }) : null;
|
|
6636
|
+
const applicationPayload = {
|
|
6637
|
+
id,
|
|
6638
|
+
name,
|
|
6639
|
+
appDir,
|
|
6640
|
+
packageName,
|
|
6641
|
+
productionUrl,
|
|
6642
|
+
serviceSlug,
|
|
6643
|
+
serviceId
|
|
6644
|
+
};
|
|
6645
|
+
return {
|
|
6646
|
+
version: APPLICATION_REQUIREMENTS_VERSION,
|
|
6647
|
+
target,
|
|
6648
|
+
ownerType,
|
|
6649
|
+
portfolio,
|
|
6650
|
+
audience,
|
|
6651
|
+
tenancy,
|
|
6652
|
+
surfaceModel,
|
|
6653
|
+
containerAppId,
|
|
6654
|
+
profile,
|
|
6655
|
+
resources,
|
|
6656
|
+
distributionModel,
|
|
6657
|
+
capabilities,
|
|
6658
|
+
application: applicationPayload,
|
|
6659
|
+
scopes,
|
|
6660
|
+
requirements: APPLICATION_REQUIREMENTS.filter(
|
|
6661
|
+
(requirement) => requirement.appliesTo.some((scope) => scopes.includes(scope))
|
|
6662
|
+
),
|
|
6663
|
+
packages: scaffoldPackages({ browser, api, target }),
|
|
6664
|
+
files: scaffoldFiles({
|
|
6665
|
+
payload: {
|
|
6666
|
+
ownerType,
|
|
6667
|
+
portfolio,
|
|
6668
|
+
audience,
|
|
6669
|
+
tenancy,
|
|
6670
|
+
surfaceModel,
|
|
6671
|
+
containerAppId,
|
|
6672
|
+
profile,
|
|
6673
|
+
resources,
|
|
6674
|
+
distributionModel,
|
|
6675
|
+
capabilities
|
|
6676
|
+
},
|
|
6677
|
+
application: applicationPayload,
|
|
6678
|
+
target,
|
|
6679
|
+
browser,
|
|
6680
|
+
api,
|
|
6681
|
+
ciWorkflow
|
|
6682
|
+
}),
|
|
6683
|
+
commands: scaffoldCommands({
|
|
6684
|
+
id,
|
|
6685
|
+
target,
|
|
6686
|
+
audience,
|
|
6687
|
+
tenancy,
|
|
6688
|
+
portfolio,
|
|
6689
|
+
profile
|
|
6690
|
+
}),
|
|
6691
|
+
ciWorkflowPath,
|
|
6692
|
+
ciWorkflow
|
|
6693
|
+
};
|
|
6694
|
+
}
|
|
6695
|
+
async function writeApplicationScaffold(payload, options = {}) {
|
|
6696
|
+
const root = resolve2(options.repoRoot ?? process.cwd());
|
|
6697
|
+
const plannedFiles = payload.files.map((file) => ({
|
|
6698
|
+
file,
|
|
6699
|
+
absolutePath: resolveInside(root, file.path)
|
|
6700
|
+
}));
|
|
6701
|
+
const existingFiles = [];
|
|
6702
|
+
for (const planned of plannedFiles) {
|
|
6703
|
+
if (await pathExists(planned.absolutePath)) {
|
|
6704
|
+
existingFiles.push(planned.file.path);
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
if (existingFiles.length > 0 && !options.force) {
|
|
6708
|
+
throw new Error(
|
|
6709
|
+
[
|
|
6710
|
+
"Refusing to overwrite existing scaffold files:",
|
|
6711
|
+
...existingFiles.map((filePath) => `- ${filePath}`),
|
|
6712
|
+
"Pass --force to overwrite these files."
|
|
6713
|
+
].join("\n")
|
|
6714
|
+
);
|
|
6715
|
+
}
|
|
6716
|
+
const files = [];
|
|
6717
|
+
for (const planned of plannedFiles) {
|
|
6718
|
+
const existed = await pathExists(planned.absolutePath);
|
|
6719
|
+
await mkdir2(dirname3(planned.absolutePath), { recursive: true });
|
|
6720
|
+
await writeFile2(planned.absolutePath, planned.file.content, "utf8");
|
|
6721
|
+
files.push({
|
|
6722
|
+
path: planned.file.path,
|
|
6723
|
+
status: existed ? "overwritten" : "created"
|
|
6724
|
+
});
|
|
6725
|
+
}
|
|
6726
|
+
return { root, files };
|
|
6727
|
+
}
|
|
6728
|
+
function scaffoldScopes(options) {
|
|
6729
|
+
const scopes = ["all"];
|
|
6730
|
+
if (options.browser) scopes.push("browser");
|
|
6731
|
+
if (options.api) {
|
|
6732
|
+
scopes.push("api", "agent_surface");
|
|
6733
|
+
}
|
|
6734
|
+
return scopes;
|
|
6735
|
+
}
|
|
6736
|
+
function scaffoldPackages(options) {
|
|
6737
|
+
if (options.target === "developer") {
|
|
6738
|
+
return options.browser ? ["@topolo/login"] : [];
|
|
6739
|
+
}
|
|
6740
|
+
const packages = /* @__PURE__ */ new Set();
|
|
6741
|
+
if (options.browser) {
|
|
6742
|
+
packages.add("@topolo-io/app-funnel");
|
|
6743
|
+
packages.add("@topolo-io/app-launcher");
|
|
6744
|
+
packages.add("@topolo-io/app-shell");
|
|
6745
|
+
packages.add("@topolo-io/auth-client");
|
|
6746
|
+
packages.add("@topolo-io/ui-kit");
|
|
6747
|
+
}
|
|
6748
|
+
if (options.api) {
|
|
6749
|
+
packages.add("@topolo-io/worker-runtime");
|
|
6750
|
+
packages.add("@topolo/sdk");
|
|
6751
|
+
}
|
|
6752
|
+
return Array.from(packages);
|
|
6753
|
+
}
|
|
6754
|
+
function scaffoldCapabilities(options) {
|
|
6755
|
+
const capabilities = [];
|
|
6756
|
+
if (options.browser) {
|
|
6757
|
+
capabilities.push("login", "launcher");
|
|
6758
|
+
}
|
|
6759
|
+
if (options.distributionModel === "marketplace") capabilities.push("marketplace");
|
|
6760
|
+
if (options.api) capabilities.push("api", "widget");
|
|
6761
|
+
return capabilities;
|
|
6762
|
+
}
|
|
6763
|
+
function scaffoldFiles(options) {
|
|
6764
|
+
const { application, target } = options;
|
|
6765
|
+
const slug = toSlug(application.id);
|
|
6766
|
+
const base = target === "platform" ? firstPartyAppPath(application.appDir, options.payload.audience) : application.appDir;
|
|
6767
|
+
const browserBase = target === "platform" ? `${base}/apps/web` : base;
|
|
6768
|
+
const apiBase = target === "platform" ? `${base}/apps/api` : base;
|
|
6769
|
+
const files = [
|
|
6770
|
+
{
|
|
6771
|
+
path: `${base}/package.json`,
|
|
6772
|
+
purpose: "Package metadata and verification commands.",
|
|
6773
|
+
requiredSignals: [
|
|
6774
|
+
"name matches the packageName in the SDK application catalog",
|
|
6775
|
+
"scripts include build plus typecheck where the stack supports them"
|
|
6776
|
+
],
|
|
6777
|
+
content: packageJson(application, target, {
|
|
6778
|
+
browser: options.browser,
|
|
6779
|
+
api: options.api,
|
|
6780
|
+
resources: options.payload.resources
|
|
6781
|
+
})
|
|
6782
|
+
},
|
|
6783
|
+
{
|
|
6784
|
+
path: `${base}/README.md`,
|
|
6785
|
+
purpose: "Starter README with install, run, and verification commands.",
|
|
6786
|
+
requiredSignals: [
|
|
6787
|
+
"documents the generated app target",
|
|
6788
|
+
"points maintainers to the shared Topolo packages used by the scaffold"
|
|
6789
|
+
],
|
|
6790
|
+
content: readme(application, target, {
|
|
6791
|
+
api: options.api,
|
|
6792
|
+
audience: options.payload.audience,
|
|
6793
|
+
tenancy: options.payload.tenancy,
|
|
6794
|
+
portfolio: options.payload.portfolio,
|
|
6795
|
+
profile: options.payload.profile,
|
|
6796
|
+
resources: options.payload.resources
|
|
6797
|
+
})
|
|
6798
|
+
},
|
|
6799
|
+
{
|
|
6800
|
+
path: `${base}/.gitignore`,
|
|
6801
|
+
purpose: "Generated-app ignore rules.",
|
|
6802
|
+
requiredSignals: [
|
|
6803
|
+
"ignores dependency installs and build output",
|
|
6804
|
+
"keeps local environment files out of source control"
|
|
6805
|
+
],
|
|
6806
|
+
content: gitignore()
|
|
6807
|
+
},
|
|
6808
|
+
{
|
|
6809
|
+
path: `${base}/tsconfig.json`,
|
|
6810
|
+
purpose: "TypeScript configuration for the generated app.",
|
|
6811
|
+
requiredSignals: [
|
|
6812
|
+
"uses strict mode",
|
|
6813
|
+
"includes browser DOM types for React and Worker types for API-backed apps"
|
|
6814
|
+
],
|
|
6815
|
+
content: tsconfigJson(options.api, target)
|
|
6816
|
+
},
|
|
6817
|
+
{
|
|
6818
|
+
path: `${browserBase}/vite.config.ts`,
|
|
6819
|
+
purpose: "Vite browser build configuration.",
|
|
6820
|
+
requiredSignals: [
|
|
6821
|
+
"builds the authenticated browser surface",
|
|
6822
|
+
"keeps generated apps on the standard React/Vite baseline"
|
|
6823
|
+
],
|
|
6824
|
+
content: viteConfig(target)
|
|
6825
|
+
},
|
|
6826
|
+
{
|
|
6827
|
+
path: `${browserBase}/index.html`,
|
|
6828
|
+
purpose: "Browser entry document.",
|
|
6829
|
+
requiredSignals: [
|
|
6830
|
+
"mounts the React app at #root",
|
|
6831
|
+
"uses the generated application display name"
|
|
6832
|
+
],
|
|
6833
|
+
content: indexHtml(application.name)
|
|
6834
|
+
},
|
|
6835
|
+
{
|
|
6836
|
+
path: `${browserBase}/src/main.tsx`,
|
|
6837
|
+
purpose: "React app entrypoint.",
|
|
6838
|
+
requiredSignals: [
|
|
6839
|
+
"renders the generated app component",
|
|
6840
|
+
"imports the generated app stylesheet"
|
|
6841
|
+
],
|
|
6842
|
+
content: mainTsx()
|
|
6843
|
+
},
|
|
6844
|
+
{
|
|
6845
|
+
path: `${browserBase}/src/env.d.ts`,
|
|
6846
|
+
purpose: "Vite environment variable typing.",
|
|
6847
|
+
requiredSignals: [
|
|
6848
|
+
"loads vite/client import.meta.env types",
|
|
6849
|
+
"declares the Topolo environment variables consumed by the starter"
|
|
6850
|
+
],
|
|
6851
|
+
content: envDts(options.api)
|
|
6852
|
+
},
|
|
6853
|
+
...options.payload.resources.workflow ? [
|
|
6854
|
+
{
|
|
6855
|
+
path: `${base}/src/cloudflare-workers.d.ts`,
|
|
6856
|
+
purpose: "Minimal Cloudflare Workflow entrypoint typing for scaffolded Workers.",
|
|
6857
|
+
requiredSignals: [
|
|
6858
|
+
"declares cloudflare:workers WorkflowEntrypoint for TypeScript",
|
|
6859
|
+
"keeps Workflow imports deployable without adding a new dependency"
|
|
6860
|
+
],
|
|
6861
|
+
content: cloudflareWorkersDts()
|
|
6862
|
+
}
|
|
6863
|
+
] : [],
|
|
6864
|
+
{
|
|
6865
|
+
path: `${browserBase}/src/styles.css`,
|
|
6866
|
+
purpose: "Minimal app-local styles outside the shared Topolo shell.",
|
|
6867
|
+
requiredSignals: [
|
|
6868
|
+
"does not fork the shared shell chrome",
|
|
6869
|
+
"keeps app-specific layout scoped under generated classes"
|
|
6870
|
+
],
|
|
6871
|
+
content: stylesCss()
|
|
6872
|
+
}
|
|
6873
|
+
];
|
|
6874
|
+
if (target === "platform") {
|
|
6875
|
+
files.push(
|
|
6876
|
+
{
|
|
6877
|
+
path: `${base}/topolo.app.json`,
|
|
6878
|
+
purpose: "Canonical app manifest that drives generated Wrangler and CloudControl metadata.",
|
|
6879
|
+
requiredSignals: [
|
|
6880
|
+
"declares web/api targets with canonical names and app-local target directories",
|
|
6881
|
+
"keeps Cloudflare account, domain, service id, and bindings in one manifest source of truth"
|
|
6882
|
+
],
|
|
6883
|
+
content: topoloAppManifestJson(application, {
|
|
6884
|
+
api: options.api,
|
|
6885
|
+
resources: options.payload.resources,
|
|
6886
|
+
profile: options.payload.profile
|
|
6887
|
+
})
|
|
6888
|
+
},
|
|
6889
|
+
{
|
|
6890
|
+
path: `${base}/topolo.cloudcontrol.json`,
|
|
6891
|
+
purpose: "CloudControl source of truth for deploy targets, routes, bindings, and CI metadata.",
|
|
6892
|
+
requiredSignals: [
|
|
6893
|
+
"production URLs and Cloudflare targets are explicit",
|
|
6894
|
+
"resource bindings and required secrets are declared without secret values"
|
|
6895
|
+
],
|
|
6896
|
+
content: cloudcontrolJson(application, {
|
|
6897
|
+
api: options.api,
|
|
6898
|
+
audience: options.payload.audience,
|
|
6899
|
+
profile: options.payload.profile,
|
|
6900
|
+
resources: options.payload.resources
|
|
6901
|
+
})
|
|
6902
|
+
},
|
|
6903
|
+
{
|
|
6904
|
+
path: `${base}/.github/workflows/topolo-conformance.yml`,
|
|
6905
|
+
purpose: "PR/push gate that runs the local Topolo app audit with docs evidence.",
|
|
6906
|
+
requiredSignals: [
|
|
6907
|
+
"checks out this repo under its Apps lane directory",
|
|
6908
|
+
"checks out TopoloDocs under Apps/system/TopoloDocs",
|
|
6909
|
+
"checks out topolo-sdk and TopoloCli from main so the gate does not wait for an npm publish",
|
|
6910
|
+
"runs topolo apps audit with --local and --fail-on needs_review"
|
|
6911
|
+
],
|
|
6912
|
+
content: options.ciWorkflow ?? ""
|
|
6913
|
+
},
|
|
6914
|
+
{
|
|
6915
|
+
path: `Apps/system/TopoloDocs/src/content/systems/topolo-${slug}.json`,
|
|
6916
|
+
purpose: "Canonical system registry entry.",
|
|
6917
|
+
requiredSignals: [
|
|
6918
|
+
`repo_paths includes ${base}`,
|
|
6919
|
+
"doc_paths includes internal handbook coverage",
|
|
6920
|
+
"security_assurance records the auth, tenancy, deployment, and verification boundary"
|
|
6921
|
+
],
|
|
6922
|
+
content: systemJson(application, {
|
|
6923
|
+
api: options.api,
|
|
6924
|
+
audience: options.payload.audience,
|
|
6925
|
+
tenancy: options.payload.tenancy,
|
|
6926
|
+
portfolio: options.payload.portfolio,
|
|
6927
|
+
profile: options.payload.profile,
|
|
6928
|
+
resources: options.payload.resources
|
|
6929
|
+
})
|
|
6930
|
+
},
|
|
6931
|
+
{
|
|
6932
|
+
path: `Apps/system/TopoloDocs/src/content/internal/apps/${slug}.mdx`,
|
|
6933
|
+
purpose: "Internal operator handbook.",
|
|
6934
|
+
requiredSignals: [
|
|
6935
|
+
"documents ownership, routes, auth boundary, deployment, operations, and verification",
|
|
6936
|
+
"last_verified is updated when behavior changes"
|
|
6937
|
+
],
|
|
6938
|
+
content: internalDocs(application, {
|
|
6939
|
+
api: options.api,
|
|
6940
|
+
audience: options.payload.audience,
|
|
6941
|
+
tenancy: options.payload.tenancy,
|
|
6942
|
+
portfolio: options.payload.portfolio,
|
|
6943
|
+
profile: options.payload.profile,
|
|
6944
|
+
resources: options.payload.resources
|
|
6945
|
+
})
|
|
6946
|
+
},
|
|
6947
|
+
{
|
|
6948
|
+
path: `Apps/system/TopoloDocs/src/content/public/applications/${slug}.mdx`,
|
|
6949
|
+
purpose: "Public application overview.",
|
|
6950
|
+
requiredSignals: [
|
|
6951
|
+
"documents the product surface without internal-only details",
|
|
6952
|
+
"links back to the canonical system registry entry"
|
|
6953
|
+
],
|
|
6954
|
+
content: publicDocs(application, {
|
|
6955
|
+
api: options.api,
|
|
6956
|
+
audience: options.payload.audience,
|
|
6957
|
+
tenancy: options.payload.tenancy,
|
|
6958
|
+
portfolio: options.payload.portfolio,
|
|
6959
|
+
profile: options.payload.profile,
|
|
6960
|
+
resources: options.payload.resources
|
|
6961
|
+
})
|
|
6962
|
+
}
|
|
6963
|
+
);
|
|
6964
|
+
if (options.api && application.serviceId) {
|
|
6965
|
+
files.push(
|
|
6966
|
+
{
|
|
6967
|
+
path: `Apps/system/TopoloAuth/catalog/service-manifests/${application.serviceId}.json`,
|
|
6968
|
+
purpose: "Auth service manifest for first-party permissions, role bundles, and API-key scopes.",
|
|
6969
|
+
requiredSignals: [
|
|
6970
|
+
"serviceId matches the generated Worker service id",
|
|
6971
|
+
"permissions, role bundles, and API-key scopes exist before the app is catalog-visible",
|
|
6972
|
+
"systems metadata maps the service back to the generated TopoloDocs entries"
|
|
6973
|
+
],
|
|
6974
|
+
content: serviceManifestJson(application, {
|
|
6975
|
+
audience: options.payload.audience,
|
|
6976
|
+
tenancy: options.payload.tenancy
|
|
6977
|
+
})
|
|
6978
|
+
}
|
|
6979
|
+
);
|
|
6980
|
+
}
|
|
6981
|
+
} else {
|
|
6982
|
+
files.push(
|
|
6983
|
+
{
|
|
6984
|
+
path: `${base}/topolo.app.json`,
|
|
6985
|
+
purpose: "Third-party Topolo application manifest.",
|
|
6986
|
+
requiredSignals: [
|
|
6987
|
+
"declares the external app id, callback URLs, and requested platform scopes",
|
|
6988
|
+
"does not include internal Topolo CloudControl or canonical-docs ownership"
|
|
6989
|
+
],
|
|
6990
|
+
content: developerManifest(application, {
|
|
6991
|
+
api: options.api,
|
|
6992
|
+
audience: options.payload.audience,
|
|
6993
|
+
tenancy: options.payload.tenancy,
|
|
6994
|
+
surfaceModel: options.payload.surfaceModel,
|
|
6995
|
+
containerAppId: options.payload.containerAppId,
|
|
6996
|
+
distributionModel: options.payload.distributionModel,
|
|
6997
|
+
capabilities: options.payload.capabilities
|
|
6998
|
+
})
|
|
6999
|
+
},
|
|
7000
|
+
{
|
|
7001
|
+
path: `${base}/.env.example`,
|
|
7002
|
+
purpose: "Developer environment template for local auth configuration.",
|
|
7003
|
+
requiredSignals: [
|
|
7004
|
+
"names the Topolo Auth URL",
|
|
7005
|
+
"keeps OAuth client ids and service ids out of source control by default"
|
|
7006
|
+
],
|
|
7007
|
+
content: envExample(application)
|
|
7008
|
+
}
|
|
7009
|
+
);
|
|
7010
|
+
}
|
|
7011
|
+
if (options.browser) {
|
|
7012
|
+
if (target === "platform") {
|
|
7013
|
+
files.push({
|
|
7014
|
+
path: `${browserBase}/src/service-identity.ts`,
|
|
7015
|
+
purpose: "Central service identity resolver shared by browser and Worker runtime surfaces.",
|
|
7016
|
+
requiredSignals: [
|
|
7017
|
+
"exports the generated app id, display name, and stable Auth service slug",
|
|
7018
|
+
"requires the provisioned Auth service id from the Worker deployment config"
|
|
7019
|
+
],
|
|
7020
|
+
content: serviceIdentityTs(application)
|
|
7021
|
+
});
|
|
7022
|
+
}
|
|
7023
|
+
files.push({
|
|
7024
|
+
path: `${browserBase}/src/App.tsx`,
|
|
7025
|
+
purpose: "Authenticated browser surface.",
|
|
7026
|
+
requiredSignals: [
|
|
7027
|
+
"bootstraps routes, auth provider, and shared shell through @topolo-io/app-shell",
|
|
7028
|
+
"uses @topolo-io/auth-client through the app-shell wrapper for app-origin auth flows",
|
|
7029
|
+
"keeps launcher and account-menu behavior on the shared platform surface",
|
|
7030
|
+
"passes app-specific account actions through accountMenuItems or TopoloAccountMenu"
|
|
7031
|
+
],
|
|
7032
|
+
content: appTsx(application, target, {
|
|
7033
|
+
audience: options.payload.audience,
|
|
7034
|
+
portfolio: options.payload.portfolio
|
|
7035
|
+
})
|
|
7036
|
+
});
|
|
7037
|
+
}
|
|
7038
|
+
if (options.api) {
|
|
7039
|
+
files.push(
|
|
7040
|
+
{
|
|
7041
|
+
path: `${apiBase}/wrangler.toml`,
|
|
7042
|
+
purpose: "Cloudflare Worker runtime configuration.",
|
|
7043
|
+
requiredSignals: [
|
|
7044
|
+
"bindings match topolo.cloudcontrol.json or topolo.app.json",
|
|
7045
|
+
"secrets are referenced by name only"
|
|
7046
|
+
],
|
|
7047
|
+
content: target === "platform" ? platformApiWranglerToml(application, options.payload.resources, options.payload.profile) : wranglerToml(application, target, options.payload.resources, options.payload.profile)
|
|
7048
|
+
},
|
|
7049
|
+
{
|
|
7050
|
+
path: `${apiBase}/src/worker.ts`,
|
|
7051
|
+
purpose: "Protected service boundary.",
|
|
7052
|
+
requiredSignals: [
|
|
7053
|
+
"uses @topolo-io/worker-runtime for Worker auth, gatewaying, headers, and request helpers",
|
|
7054
|
+
"derives organization from the credential rather than client-supplied orgId",
|
|
7055
|
+
"emits or propagates X-Topolo-Request-Id and X-Topolo-Client"
|
|
7056
|
+
],
|
|
7057
|
+
content: workerTs(application, target, {
|
|
7058
|
+
audience: options.payload.audience,
|
|
7059
|
+
tenancy: options.payload.tenancy,
|
|
7060
|
+
profile: options.payload.profile,
|
|
7061
|
+
resources: options.payload.resources
|
|
7062
|
+
})
|
|
7063
|
+
}
|
|
7064
|
+
);
|
|
7065
|
+
if (target === "platform") {
|
|
7066
|
+
files.push(
|
|
7067
|
+
{
|
|
7068
|
+
path: `${apiBase}/src/service-identity.ts`,
|
|
7069
|
+
purpose: "Runtime service identity resolver for the API Worker.",
|
|
7070
|
+
requiredSignals: [
|
|
7071
|
+
"resolves the concrete Auth service id from Worker vars",
|
|
7072
|
+
"keeps the API Worker independent from browser-only import.meta usage"
|
|
7073
|
+
],
|
|
7074
|
+
content: apiServiceIdentityTs(application)
|
|
7075
|
+
},
|
|
7076
|
+
{
|
|
7077
|
+
path: `${browserBase}/src/worker.ts`,
|
|
7078
|
+
purpose: "Browser Worker that serves assets and forwards API paths to the API Worker binding.",
|
|
7079
|
+
requiredSignals: [
|
|
7080
|
+
"keeps the frontend Worker thin",
|
|
7081
|
+
"routes /api/* to the API service binding instead of owning backend routes locally"
|
|
7082
|
+
],
|
|
7083
|
+
content: webWorkerTs(application)
|
|
7084
|
+
},
|
|
7085
|
+
{
|
|
7086
|
+
path: `${browserBase}/wrangler.toml`,
|
|
7087
|
+
purpose: "Cloudflare Worker static-assets configuration for the browser surface.",
|
|
7088
|
+
requiredSignals: [
|
|
7089
|
+
"serves Vite assets through a Worker with assets",
|
|
7090
|
+
"binds the frontend Worker to the API Worker for /api/* requests"
|
|
7091
|
+
],
|
|
7092
|
+
content: platformWebWranglerToml(application)
|
|
7093
|
+
}
|
|
7094
|
+
);
|
|
7095
|
+
}
|
|
7096
|
+
}
|
|
7097
|
+
return files;
|
|
7098
|
+
}
|
|
6932
7099
|
function topoloAppManifestJson(application, options) {
|
|
6933
7100
|
const slug = toSlug(application.id);
|
|
6934
7101
|
const resource = resourceConfig(application, options.profile);
|
|
@@ -6993,8 +7160,8 @@ function topoloAppManifestJson(application, options) {
|
|
|
6993
7160
|
production: "https://auth.topolo.app"
|
|
6994
7161
|
},
|
|
6995
7162
|
NEXUS_GATEWAY_URL: {
|
|
6996
|
-
staging: "https://nexus
|
|
6997
|
-
production: "https://nexus
|
|
7163
|
+
staging: "https://nexus.stg.topolo.us",
|
|
7164
|
+
production: "https://nexus.topolo.app"
|
|
6998
7165
|
},
|
|
6999
7166
|
NEXUS_APP_ID: slug
|
|
7000
7167
|
},
|
|
@@ -7697,132 +7864,6 @@ function envExample(application) {
|
|
|
7697
7864
|
""
|
|
7698
7865
|
].join("\n");
|
|
7699
7866
|
}
|
|
7700
|
-
function normalizeTarget(value) {
|
|
7701
|
-
if (!value) return "platform";
|
|
7702
|
-
if (value === "platform" || value === "developer") return value;
|
|
7703
|
-
throw new Error("Invalid scaffold target. Use platform or developer.");
|
|
7704
|
-
}
|
|
7705
|
-
function normalizePortfolio(value) {
|
|
7706
|
-
if (!value) return "platform";
|
|
7707
|
-
if (value === "platform" || value === "personal") return value;
|
|
7708
|
-
throw new Error("Invalid scaffold portfolio. Use platform or personal.");
|
|
7709
|
-
}
|
|
7710
|
-
function normalizeAudience(value, portfolio) {
|
|
7711
|
-
if (!value) {
|
|
7712
|
-
return portfolio === "personal" ? "personal" : "business";
|
|
7713
|
-
}
|
|
7714
|
-
if (value === "business" || value === "personal") {
|
|
7715
|
-
return value;
|
|
7716
|
-
}
|
|
7717
|
-
throw new Error("Invalid scaffold audience. Use business or personal.");
|
|
7718
|
-
}
|
|
7719
|
-
function normalizeTenancy(value, audience) {
|
|
7720
|
-
const resolved = value ?? (audience === "personal" ? "household" : "organization");
|
|
7721
|
-
if (resolved !== "organization" && resolved !== "household") {
|
|
7722
|
-
throw new Error("Invalid scaffold tenancy. Use organization or household.");
|
|
7723
|
-
}
|
|
7724
|
-
if (audience === "business" && resolved !== "organization") {
|
|
7725
|
-
throw new Error("Business scaffolds must use organization tenancy.");
|
|
7726
|
-
}
|
|
7727
|
-
if (audience === "personal" && resolved !== "household") {
|
|
7728
|
-
throw new Error("Personal scaffolds must use household tenancy.");
|
|
7729
|
-
}
|
|
7730
|
-
return resolved;
|
|
7731
|
-
}
|
|
7732
|
-
function normalizeSurfaceModel(value) {
|
|
7733
|
-
if (!value) return "web_app";
|
|
7734
|
-
if (value === "web_app" || value === "mobile_container" || value === "mobile_module" || value === "messaging_surface") {
|
|
7735
|
-
return value;
|
|
7736
|
-
}
|
|
7737
|
-
throw new Error(
|
|
7738
|
-
"Invalid scaffold surface model. Use web_app, mobile_container, mobile_module, or messaging_surface."
|
|
7739
|
-
);
|
|
7740
|
-
}
|
|
7741
|
-
function normalizeScaffoldProfile(value) {
|
|
7742
|
-
if (!value) return null;
|
|
7743
|
-
if (value === "spatial-assets") return value;
|
|
7744
|
-
throw new Error("Invalid scaffold profile. Use spatial-assets.");
|
|
7745
|
-
}
|
|
7746
|
-
function resolveScaffoldResources(profile, options) {
|
|
7747
|
-
const profileResources = profile === "spatial-assets" ? { d1: true, r2: true, queue: true, workflow: true, containerProcessing: true } : { d1: false, r2: false, queue: false, workflow: false, containerProcessing: false };
|
|
7748
|
-
return {
|
|
7749
|
-
d1: Boolean(profileResources.d1 || options.withD1),
|
|
7750
|
-
r2: Boolean(profileResources.r2 || options.withR2),
|
|
7751
|
-
queue: Boolean(profileResources.queue || options.withQueue),
|
|
7752
|
-
workflow: Boolean(profileResources.workflow || options.withWorkflow),
|
|
7753
|
-
containerProcessing: Boolean(profileResources.containerProcessing || options.withContainerProcessing)
|
|
7754
|
-
};
|
|
7755
|
-
}
|
|
7756
|
-
function normalizeContainerAppId(value, surfaceModel) {
|
|
7757
|
-
const trimmed = String(value || "").trim();
|
|
7758
|
-
if (surfaceModel === "mobile_module") {
|
|
7759
|
-
if (!trimmed) {
|
|
7760
|
-
throw new Error("Mobile-module scaffolds require --container-app-id.");
|
|
7761
|
-
}
|
|
7762
|
-
return trimmed;
|
|
7763
|
-
}
|
|
7764
|
-
return trimmed || null;
|
|
7765
|
-
}
|
|
7766
|
-
function normalizeApplicationId(application) {
|
|
7767
|
-
const raw = application.trim().toLowerCase();
|
|
7768
|
-
const normalized = raw.replace(/-/g, "_");
|
|
7769
|
-
if (Object.prototype.hasOwnProperty.call(APPLICATIONS, raw)) {
|
|
7770
|
-
return raw;
|
|
7771
|
-
}
|
|
7772
|
-
if (Object.prototype.hasOwnProperty.call(APPLICATIONS, normalized)) {
|
|
7773
|
-
return normalized;
|
|
7774
|
-
}
|
|
7775
|
-
if (/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(raw) || /^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/.test(raw)) {
|
|
7776
|
-
return normalized;
|
|
7777
|
-
}
|
|
7778
|
-
throw new Error(
|
|
7779
|
-
"Application id must be kebab-case or snake_case using lowercase letters, numbers, hyphens, and underscores."
|
|
7780
|
-
);
|
|
7781
|
-
}
|
|
7782
|
-
function normalizeFirstPartyApplicationId(applicationId) {
|
|
7783
|
-
if (applicationId.startsWith("topolo_") && applicationId.length > "topolo_".length) {
|
|
7784
|
-
return applicationId.slice("topolo_".length);
|
|
7785
|
-
}
|
|
7786
|
-
return applicationId;
|
|
7787
|
-
}
|
|
7788
|
-
function toSlug(value) {
|
|
7789
|
-
return value.replace(/_/g, "-");
|
|
7790
|
-
}
|
|
7791
|
-
function toPascalCase(value) {
|
|
7792
|
-
const acronyms = /* @__PURE__ */ new Set(["api", "cli", "crm", "mcp", "mdm", "p2p"]);
|
|
7793
|
-
return value.split(/[-_]/).filter(Boolean).map((part) => acronyms.has(part.toLowerCase()) ? part.toUpperCase() : part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
|
|
7794
|
-
}
|
|
7795
|
-
function toTitle(value) {
|
|
7796
|
-
return value.replace(/^topolo[-_\s]*/i, "Topolo ").split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
7797
|
-
}
|
|
7798
|
-
function json(value) {
|
|
7799
|
-
return JSON.stringify(value, null, 2);
|
|
7800
|
-
}
|
|
7801
|
-
async function pathExists(path4) {
|
|
7802
|
-
try {
|
|
7803
|
-
await access(path4, constants.F_OK);
|
|
7804
|
-
return true;
|
|
7805
|
-
} catch {
|
|
7806
|
-
return false;
|
|
7807
|
-
}
|
|
7808
|
-
}
|
|
7809
|
-
function resolveInside(root, filePath) {
|
|
7810
|
-
const resolved = resolve(root, filePath);
|
|
7811
|
-
const relativePath = relative(root, resolved);
|
|
7812
|
-
if (relativePath.startsWith("..") || relativePath === "" || relativePath.startsWith("/")) {
|
|
7813
|
-
throw new Error(`Scaffold file path escapes the target root: ${filePath}`);
|
|
7814
|
-
}
|
|
7815
|
-
return resolved;
|
|
7816
|
-
}
|
|
7817
|
-
function escapeHtml(value) {
|
|
7818
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7819
|
-
}
|
|
7820
|
-
function escapeSingleQuoted(value) {
|
|
7821
|
-
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
7822
|
-
}
|
|
7823
|
-
function escapeFrontmatter(value) {
|
|
7824
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
7825
|
-
}
|
|
7826
7867
|
|
|
7827
7868
|
// src/commands/app-scaffold-provision.ts
|
|
7828
7869
|
import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
@@ -9182,7 +9223,7 @@ function normalizeList(values, fallback = []) {
|
|
|
9182
9223
|
|
|
9183
9224
|
// src/commands/integrations.ts
|
|
9184
9225
|
import { readdir, readFile as readFile3, stat } from "fs/promises";
|
|
9185
|
-
import { join as join4, relative as
|
|
9226
|
+
import { join as join4, relative as relative3 } from "path";
|
|
9186
9227
|
function registerIntegrationsCommand(program2) {
|
|
9187
9228
|
const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
|
|
9188
9229
|
integrations.command("prompt").description("Print an agent prompt for adding a Topolo integration.").argument("<integration>", "Integration prompt id, currently login-with-topolo").option("--framework <framework>", "auto, authjs, express, or generic", "auto").option("--json", "Emit JSON output").action((integration, options) => {
|
|
@@ -9347,7 +9388,7 @@ async function collectFiles(repoRoot) {
|
|
|
9347
9388
|
const info = await stat(fullPath);
|
|
9348
9389
|
if (info.size > 25e4) continue;
|
|
9349
9390
|
result.push({
|
|
9350
|
-
path:
|
|
9391
|
+
path: relative3(root, fullPath),
|
|
9351
9392
|
content: await readFile3(fullPath, "utf8")
|
|
9352
9393
|
});
|
|
9353
9394
|
}
|