@bagelink/workspace 1.15.556 → 1.15.562
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/bin/bgl.ts +19 -1
- package/dist/bin/bgl.cjs +86 -25
- package/dist/bin/bgl.mjs +88 -27
- package/package.json +1 -1
- package/src/openapi.ts +64 -0
- package/src/sdk.ts +23 -25
package/bin/bgl.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { runDev } from '../src/dev.js'
|
|
|
7
7
|
import { generateWorkspaceConfig } from '../src/init.js'
|
|
8
8
|
import { setupLint } from '../src/lint.js'
|
|
9
9
|
import { generateSDK, generateSDKForWorkspace } from '../src/sdk.js'
|
|
10
|
+
import { resolveOpenApiUrl } from '../src/sdk.js'
|
|
11
|
+
import { validateOpenApiConformance } from '../src/openapi.js'
|
|
10
12
|
import { addProject, initWorkspace, listProjects } from '../src/workspace.js'
|
|
11
13
|
|
|
12
14
|
const [,, command, subcommand, ...args] = process.argv
|
|
@@ -88,12 +90,14 @@ Lint Commands:
|
|
|
88
90
|
const splitMode = args.includes('--split') || args.includes('-s')
|
|
89
91
|
const modeIndex = args.indexOf('--mode')
|
|
90
92
|
const mode = modeIndex >= 0 ? args[modeIndex + 1] : undefined
|
|
93
|
+
const manifestIndex = args.indexOf('--manifest')
|
|
94
|
+
const manifest = manifestIndex >= 0 ? args[manifestIndex + 1] : undefined
|
|
91
95
|
|
|
92
96
|
let workspaceMode = isWorkspace(process.cwd())
|
|
93
97
|
if (forceWorkspace) workspaceMode = true
|
|
94
98
|
if (forceProject) workspaceMode = false
|
|
95
99
|
|
|
96
|
-
const sdkOptions = { split: splitMode || undefined, mode }
|
|
100
|
+
const sdkOptions = { split: splitMode || undefined, mode, manifest }
|
|
97
101
|
|
|
98
102
|
if (workspaceMode) {
|
|
99
103
|
console.log('✓ Detected workspace mode - will generate for multiple projects')
|
|
@@ -103,6 +107,16 @@ Lint Commands:
|
|
|
103
107
|
await generateSDK(process.cwd(), sdkOptions)
|
|
104
108
|
}
|
|
105
109
|
}
|
|
110
|
+
else if (subcommand === 'validate') {
|
|
111
|
+
const modeIndex = args.indexOf('--mode')
|
|
112
|
+
const mode = modeIndex >= 0 ? args[modeIndex + 1] : undefined
|
|
113
|
+
const manifestIndex = args.indexOf('--manifest')
|
|
114
|
+
const manifest = manifestIndex >= 0 ? args[manifestIndex + 1] : undefined
|
|
115
|
+
const openApiUrl = await resolveOpenApiUrl(process.cwd(), mode)
|
|
116
|
+
if (!openApiUrl) throw new Error('No openapi_url found in bgl.config.ts')
|
|
117
|
+
await validateOpenApiConformance(process.cwd(), openApiUrl, manifest)
|
|
118
|
+
console.log('✅ OpenAPI conforms to explicit module ownership manifest')
|
|
119
|
+
}
|
|
106
120
|
else {
|
|
107
121
|
console.log(`
|
|
108
122
|
SDK Commands:
|
|
@@ -111,6 +125,8 @@ SDK Commands:
|
|
|
111
125
|
bgl sdk generate --workspace Force workspace mode
|
|
112
126
|
bgl sdk generate --project Force single project mode
|
|
113
127
|
bgl sdk generate --mode <env> Use openapi_url from that bgl.config.ts env
|
|
128
|
+
bgl sdk generate --manifest <path> Generate explicit per-module SDK files
|
|
129
|
+
bgl sdk validate [--manifest <path>] Validate endpoint ownership and schemas
|
|
114
130
|
`)
|
|
115
131
|
process.exit(1)
|
|
116
132
|
}
|
|
@@ -153,12 +169,14 @@ Usage:
|
|
|
153
169
|
bgl lint init Set up linting (auto-detects workspace)
|
|
154
170
|
bgl sdk generate Generate SDK (auto-detects workspace)
|
|
155
171
|
bgl sdk generate --split Generate per-module API files
|
|
172
|
+
bgl sdk validate Validate OpenAPI module ownership
|
|
156
173
|
|
|
157
174
|
Options:
|
|
158
175
|
--workspace, -w Force workspace mode
|
|
159
176
|
--project, -p Force single project mode
|
|
160
177
|
--split, -s Split SDK into per-module files
|
|
161
178
|
--mode <env> Set environment (matches bgl.config.ts keys)
|
|
179
|
+
--manifest <path> Ownership manifest (default: openapi.modules.json)
|
|
162
180
|
--help, -h Show this help message
|
|
163
181
|
|
|
164
182
|
Note: Commands auto-detect workspace mode based on directory structure
|
package/dist/bin/bgl.cjs
CHANGED
|
@@ -659,22 +659,69 @@ function updatePackageJsonLint(root, configs) {
|
|
|
659
659
|
}
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
+
const DEFAULT_OWNERSHIP_MANIFEST = "openapi.modules.json";
|
|
663
|
+
function resolveWithinRoot(root, file) {
|
|
664
|
+
const absoluteRoot = node_path.resolve(root);
|
|
665
|
+
const path = node_path.resolve(absoluteRoot, file);
|
|
666
|
+
const relativePath = node_path.relative(absoluteRoot, path);
|
|
667
|
+
if (relativePath.startsWith("..") || relativePath.includes("/../") || relativePath === "") {
|
|
668
|
+
throw new Error(`Path must be inside the project root: ${file}`);
|
|
669
|
+
}
|
|
670
|
+
return path;
|
|
671
|
+
}
|
|
672
|
+
function readOwnershipManifest(root, file = DEFAULT_OWNERSHIP_MANIFEST) {
|
|
673
|
+
const path = resolveWithinRoot(root, file);
|
|
674
|
+
if (!node_fs.existsSync(path)) throw new Error(`Module ownership manifest not found: ${path}`);
|
|
675
|
+
return JSON.parse(node_fs.readFileSync(path, "utf8"));
|
|
676
|
+
}
|
|
677
|
+
async function fetchOpenApi(url) {
|
|
678
|
+
const response = await fetch(url, {
|
|
679
|
+
headers: { Authorization: "Basic YmFnZWxfdXNlcm5hbWU6Tm90U2VjdXJlQGJhZ2Vs" }
|
|
680
|
+
});
|
|
681
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: failed to fetch ${url}`);
|
|
682
|
+
return await response.json();
|
|
683
|
+
}
|
|
684
|
+
async function validateOpenApiConformance(root, openApiUrl, manifestFile = DEFAULT_OWNERSHIP_MANIFEST) {
|
|
685
|
+
const { validateOwnershipManifest } = await import('@bagelink/sdk');
|
|
686
|
+
const issues = validateOwnershipManifest(await fetchOpenApi(openApiUrl), readOwnershipManifest(root, manifestFile));
|
|
687
|
+
if (issues.length) throw new Error(`OpenAPI ownership conformance failed:
|
|
688
|
+
${issues.map((issue) => `- ${issue.message}`).join("\n")}`);
|
|
689
|
+
}
|
|
690
|
+
async function generateOwnedModules(root, openApiUrl, outputDir, manifestFile = DEFAULT_OWNERSHIP_MANIFEST) {
|
|
691
|
+
const { generateModules } = await import('@bagelink/sdk');
|
|
692
|
+
const outputPath = resolveWithinRoot(root, outputDir);
|
|
693
|
+
const modules = generateModules(await fetchOpenApi(openApiUrl), readOwnershipManifest(root, manifestFile), "/api");
|
|
694
|
+
node_fs.rmSync(outputPath, { recursive: true, force: true });
|
|
695
|
+
node_fs.mkdirSync(outputPath, { recursive: true });
|
|
696
|
+
for (const module of modules) {
|
|
697
|
+
const moduleDir = resolveWithinRoot(outputPath, module.name);
|
|
698
|
+
node_fs.mkdirSync(moduleDir, { recursive: true });
|
|
699
|
+
node_fs.writeFileSync(node_path.resolve(moduleDir, "api.ts"), module.code);
|
|
700
|
+
node_fs.writeFileSync(node_path.resolve(moduleDir, "types.d.ts"), module.types);
|
|
701
|
+
node_fs.writeFileSync(node_path.resolve(moduleDir, "index.ts"), `export * from './api'
|
|
702
|
+
export * from './types.d'
|
|
703
|
+
`);
|
|
704
|
+
}
|
|
705
|
+
node_fs.writeFileSync(node_path.resolve(outputPath, "index.ts"), modules.map((module) => `export * as ${module.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())} from './${module.name}'`).join("\n") + "\n");
|
|
706
|
+
node_fs.writeFileSync(node_path.resolve(outputPath, "manifest.json"), `${JSON.stringify({
|
|
707
|
+
version: 1,
|
|
708
|
+
modules: modules.map(({ name, paths, schemas }) => ({ name, paths, schemas }))
|
|
709
|
+
}, null, 2)}
|
|
710
|
+
`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function resolveOpenApiUrl(root, mode = "development") {
|
|
714
|
+
const configPath = node_path.resolve(root, "bgl.config.ts");
|
|
715
|
+
if (!node_fs.existsSync(configPath)) return void 0;
|
|
716
|
+
const module = await import(`file://${configPath}`);
|
|
717
|
+
const workspace = module.default;
|
|
718
|
+
return typeof workspace === "function" ? workspace(mode)?.openapi_url : void 0;
|
|
719
|
+
}
|
|
662
720
|
async function generateSDK(root = process__default.cwd(), options = {}) {
|
|
663
721
|
console.log("\n\u{1F527} Generating SDK from OpenAPI...\n");
|
|
664
|
-
let config = null;
|
|
665
722
|
let openApiUrl;
|
|
666
723
|
try {
|
|
667
|
-
|
|
668
|
-
if (node_fs.existsSync(configPath)) {
|
|
669
|
-
const module = await import(`file://${configPath}`);
|
|
670
|
-
const workspace = module.default;
|
|
671
|
-
if (typeof workspace === "function") {
|
|
672
|
-
config = workspace(options.mode ?? "development");
|
|
673
|
-
if (config?.openapi_url) {
|
|
674
|
-
openApiUrl = config.openapi_url;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
724
|
+
openApiUrl = await resolveOpenApiUrl(root, options.mode);
|
|
678
725
|
} catch {
|
|
679
726
|
}
|
|
680
727
|
const response = await prompts__default([
|
|
@@ -710,6 +757,11 @@ async function generateSDK(root = process__default.cwd(), options = {}) {
|
|
|
710
757
|
console.log(`\u{1F500} Split mode: ${splitFiles ? "per-module" : "monolithic"}
|
|
711
758
|
`);
|
|
712
759
|
try {
|
|
760
|
+
if (options.manifest) {
|
|
761
|
+
await generateOwnedModules(root, finalUrl, outputDir, options.manifest);
|
|
762
|
+
console.log("\n\u2705 Generated deterministic per-module SDK!");
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
713
765
|
const { openAPI } = await import('@bagelink/sdk');
|
|
714
766
|
const { types, code } = await openAPI(finalUrl, "/api");
|
|
715
767
|
const outputPath = node_path.resolve(root, outputDir);
|
|
@@ -769,20 +821,9 @@ Import directly in your code:`);
|
|
|
769
821
|
}
|
|
770
822
|
async function generateSDKForWorkspace(root = process__default.cwd(), options = {}) {
|
|
771
823
|
console.log("\n\u{1F3E2} Generating SDK for workspace...\n");
|
|
772
|
-
let config = null;
|
|
773
824
|
let openApiUrl;
|
|
774
825
|
try {
|
|
775
|
-
|
|
776
|
-
if (node_fs.existsSync(configPath)) {
|
|
777
|
-
const module = await import(`file://${configPath}`);
|
|
778
|
-
const workspace = module.default;
|
|
779
|
-
if (typeof workspace === "function") {
|
|
780
|
-
config = workspace(options.mode ?? "development");
|
|
781
|
-
if (config?.openapi_url) {
|
|
782
|
-
openApiUrl = config.openapi_url;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
}
|
|
826
|
+
openApiUrl = await resolveOpenApiUrl(root, options.mode);
|
|
786
827
|
} catch {
|
|
787
828
|
}
|
|
788
829
|
const response = await prompts__default({
|
|
@@ -804,6 +845,11 @@ async function generateSDKForWorkspace(root = process__default.cwd(), options =
|
|
|
804
845
|
console.log(`\u{1F500} Split mode: ${splitFiles ? "per-module" : "monolithic"}
|
|
805
846
|
`);
|
|
806
847
|
try {
|
|
848
|
+
if (options.manifest) {
|
|
849
|
+
await generateOwnedModules(root, finalUrl, `${outputDir}/modules`, options.manifest);
|
|
850
|
+
console.log("\n\u2705 Generated deterministic per-module SDK in shared/modules!");
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
807
853
|
const { openAPI } = await import('@bagelink/sdk');
|
|
808
854
|
const { types, code } = await openAPI(finalUrl, "/api");
|
|
809
855
|
const outputPath = node_path.resolve(root, outputDir);
|
|
@@ -919,16 +965,27 @@ Lint Commands:
|
|
|
919
965
|
const splitMode = args.includes("--split") || args.includes("-s");
|
|
920
966
|
const modeIndex = args.indexOf("--mode");
|
|
921
967
|
const mode = modeIndex >= 0 ? args[modeIndex + 1] : void 0;
|
|
968
|
+
const manifestIndex = args.indexOf("--manifest");
|
|
969
|
+
const manifest = manifestIndex >= 0 ? args[manifestIndex + 1] : void 0;
|
|
922
970
|
let workspaceMode = isWorkspace(process__default.cwd());
|
|
923
971
|
if (forceWorkspace) workspaceMode = true;
|
|
924
972
|
if (forceProject) workspaceMode = false;
|
|
925
|
-
const sdkOptions = { split: splitMode || void 0, mode };
|
|
973
|
+
const sdkOptions = { split: splitMode || void 0, mode, manifest };
|
|
926
974
|
if (workspaceMode) {
|
|
927
975
|
console.log("\u2713 Detected workspace mode - will generate for multiple projects");
|
|
928
976
|
await generateSDKForWorkspace(process__default.cwd(), sdkOptions);
|
|
929
977
|
} else {
|
|
930
978
|
await generateSDK(process__default.cwd(), sdkOptions);
|
|
931
979
|
}
|
|
980
|
+
} else if (subcommand === "validate") {
|
|
981
|
+
const modeIndex = args.indexOf("--mode");
|
|
982
|
+
const mode = modeIndex >= 0 ? args[modeIndex + 1] : void 0;
|
|
983
|
+
const manifestIndex = args.indexOf("--manifest");
|
|
984
|
+
const manifest = manifestIndex >= 0 ? args[manifestIndex + 1] : void 0;
|
|
985
|
+
const openApiUrl = await resolveOpenApiUrl(process__default.cwd(), mode);
|
|
986
|
+
if (!openApiUrl) throw new Error("No openapi_url found in bgl.config.ts");
|
|
987
|
+
await validateOpenApiConformance(process__default.cwd(), openApiUrl, manifest);
|
|
988
|
+
console.log("\u2705 OpenAPI conforms to explicit module ownership manifest");
|
|
932
989
|
} else {
|
|
933
990
|
console.log(`
|
|
934
991
|
SDK Commands:
|
|
@@ -937,6 +994,8 @@ SDK Commands:
|
|
|
937
994
|
bgl sdk generate --workspace Force workspace mode
|
|
938
995
|
bgl sdk generate --project Force single project mode
|
|
939
996
|
bgl sdk generate --mode <env> Use openapi_url from that bgl.config.ts env
|
|
997
|
+
bgl sdk generate --manifest <path> Generate explicit per-module SDK files
|
|
998
|
+
bgl sdk validate [--manifest <path>] Validate endpoint ownership and schemas
|
|
940
999
|
`);
|
|
941
1000
|
process__default.exit(1);
|
|
942
1001
|
}
|
|
@@ -976,12 +1035,14 @@ Usage:
|
|
|
976
1035
|
bgl lint init Set up linting (auto-detects workspace)
|
|
977
1036
|
bgl sdk generate Generate SDK (auto-detects workspace)
|
|
978
1037
|
bgl sdk generate --split Generate per-module API files
|
|
1038
|
+
bgl sdk validate Validate OpenAPI module ownership
|
|
979
1039
|
|
|
980
1040
|
Options:
|
|
981
1041
|
--workspace, -w Force workspace mode
|
|
982
1042
|
--project, -p Force single project mode
|
|
983
1043
|
--split, -s Split SDK into per-module files
|
|
984
1044
|
--mode <env> Set environment (matches bgl.config.ts keys)
|
|
1045
|
+
--manifest <path> Ownership manifest (default: openapi.modules.json)
|
|
985
1046
|
--help, -h Show this help message
|
|
986
1047
|
|
|
987
1048
|
Note: Commands auto-detect workspace mode based on directory structure
|
package/dist/bin/bgl.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolve } from 'node:path';
|
|
2
|
+
import { resolve, relative } from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { l as listProjects, w as writeNetlifyConfig, i as initWorkspace, a as addProject } from '../shared/workspace.Dhg5QE-j.mjs';
|
|
6
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync,
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, rmSync, mkdirSync } from 'node:fs';
|
|
7
7
|
import prompts from 'prompts';
|
|
8
8
|
|
|
9
9
|
async function runBuild(filter, additionalArgs = []) {
|
|
@@ -652,22 +652,69 @@ function updatePackageJsonLint(root, configs) {
|
|
|
652
652
|
}
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
+
const DEFAULT_OWNERSHIP_MANIFEST = "openapi.modules.json";
|
|
656
|
+
function resolveWithinRoot(root, file) {
|
|
657
|
+
const absoluteRoot = resolve(root);
|
|
658
|
+
const path = resolve(absoluteRoot, file);
|
|
659
|
+
const relativePath = relative(absoluteRoot, path);
|
|
660
|
+
if (relativePath.startsWith("..") || relativePath.includes("/../") || relativePath === "") {
|
|
661
|
+
throw new Error(`Path must be inside the project root: ${file}`);
|
|
662
|
+
}
|
|
663
|
+
return path;
|
|
664
|
+
}
|
|
665
|
+
function readOwnershipManifest(root, file = DEFAULT_OWNERSHIP_MANIFEST) {
|
|
666
|
+
const path = resolveWithinRoot(root, file);
|
|
667
|
+
if (!existsSync(path)) throw new Error(`Module ownership manifest not found: ${path}`);
|
|
668
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
669
|
+
}
|
|
670
|
+
async function fetchOpenApi(url) {
|
|
671
|
+
const response = await fetch(url, {
|
|
672
|
+
headers: { Authorization: "Basic YmFnZWxfdXNlcm5hbWU6Tm90U2VjdXJlQGJhZ2Vs" }
|
|
673
|
+
});
|
|
674
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: failed to fetch ${url}`);
|
|
675
|
+
return await response.json();
|
|
676
|
+
}
|
|
677
|
+
async function validateOpenApiConformance(root, openApiUrl, manifestFile = DEFAULT_OWNERSHIP_MANIFEST) {
|
|
678
|
+
const { validateOwnershipManifest } = await import('@bagelink/sdk');
|
|
679
|
+
const issues = validateOwnershipManifest(await fetchOpenApi(openApiUrl), readOwnershipManifest(root, manifestFile));
|
|
680
|
+
if (issues.length) throw new Error(`OpenAPI ownership conformance failed:
|
|
681
|
+
${issues.map((issue) => `- ${issue.message}`).join("\n")}`);
|
|
682
|
+
}
|
|
683
|
+
async function generateOwnedModules(root, openApiUrl, outputDir, manifestFile = DEFAULT_OWNERSHIP_MANIFEST) {
|
|
684
|
+
const { generateModules } = await import('@bagelink/sdk');
|
|
685
|
+
const outputPath = resolveWithinRoot(root, outputDir);
|
|
686
|
+
const modules = generateModules(await fetchOpenApi(openApiUrl), readOwnershipManifest(root, manifestFile), "/api");
|
|
687
|
+
rmSync(outputPath, { recursive: true, force: true });
|
|
688
|
+
mkdirSync(outputPath, { recursive: true });
|
|
689
|
+
for (const module of modules) {
|
|
690
|
+
const moduleDir = resolveWithinRoot(outputPath, module.name);
|
|
691
|
+
mkdirSync(moduleDir, { recursive: true });
|
|
692
|
+
writeFileSync(resolve(moduleDir, "api.ts"), module.code);
|
|
693
|
+
writeFileSync(resolve(moduleDir, "types.d.ts"), module.types);
|
|
694
|
+
writeFileSync(resolve(moduleDir, "index.ts"), `export * from './api'
|
|
695
|
+
export * from './types.d'
|
|
696
|
+
`);
|
|
697
|
+
}
|
|
698
|
+
writeFileSync(resolve(outputPath, "index.ts"), modules.map((module) => `export * as ${module.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())} from './${module.name}'`).join("\n") + "\n");
|
|
699
|
+
writeFileSync(resolve(outputPath, "manifest.json"), `${JSON.stringify({
|
|
700
|
+
version: 1,
|
|
701
|
+
modules: modules.map(({ name, paths, schemas }) => ({ name, paths, schemas }))
|
|
702
|
+
}, null, 2)}
|
|
703
|
+
`);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function resolveOpenApiUrl(root, mode = "development") {
|
|
707
|
+
const configPath = resolve(root, "bgl.config.ts");
|
|
708
|
+
if (!existsSync(configPath)) return void 0;
|
|
709
|
+
const module = await import(`file://${configPath}`);
|
|
710
|
+
const workspace = module.default;
|
|
711
|
+
return typeof workspace === "function" ? workspace(mode)?.openapi_url : void 0;
|
|
712
|
+
}
|
|
655
713
|
async function generateSDK(root = process.cwd(), options = {}) {
|
|
656
714
|
console.log("\n\u{1F527} Generating SDK from OpenAPI...\n");
|
|
657
|
-
let config = null;
|
|
658
715
|
let openApiUrl;
|
|
659
716
|
try {
|
|
660
|
-
|
|
661
|
-
if (existsSync(configPath)) {
|
|
662
|
-
const module = await import(`file://${configPath}`);
|
|
663
|
-
const workspace = module.default;
|
|
664
|
-
if (typeof workspace === "function") {
|
|
665
|
-
config = workspace(options.mode ?? "development");
|
|
666
|
-
if (config?.openapi_url) {
|
|
667
|
-
openApiUrl = config.openapi_url;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
717
|
+
openApiUrl = await resolveOpenApiUrl(root, options.mode);
|
|
671
718
|
} catch {
|
|
672
719
|
}
|
|
673
720
|
const response = await prompts([
|
|
@@ -703,6 +750,11 @@ async function generateSDK(root = process.cwd(), options = {}) {
|
|
|
703
750
|
console.log(`\u{1F500} Split mode: ${splitFiles ? "per-module" : "monolithic"}
|
|
704
751
|
`);
|
|
705
752
|
try {
|
|
753
|
+
if (options.manifest) {
|
|
754
|
+
await generateOwnedModules(root, finalUrl, outputDir, options.manifest);
|
|
755
|
+
console.log("\n\u2705 Generated deterministic per-module SDK!");
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
706
758
|
const { openAPI } = await import('@bagelink/sdk');
|
|
707
759
|
const { types, code } = await openAPI(finalUrl, "/api");
|
|
708
760
|
const outputPath = resolve(root, outputDir);
|
|
@@ -762,20 +814,9 @@ Import directly in your code:`);
|
|
|
762
814
|
}
|
|
763
815
|
async function generateSDKForWorkspace(root = process.cwd(), options = {}) {
|
|
764
816
|
console.log("\n\u{1F3E2} Generating SDK for workspace...\n");
|
|
765
|
-
let config = null;
|
|
766
817
|
let openApiUrl;
|
|
767
818
|
try {
|
|
768
|
-
|
|
769
|
-
if (existsSync(configPath)) {
|
|
770
|
-
const module = await import(`file://${configPath}`);
|
|
771
|
-
const workspace = module.default;
|
|
772
|
-
if (typeof workspace === "function") {
|
|
773
|
-
config = workspace(options.mode ?? "development");
|
|
774
|
-
if (config?.openapi_url) {
|
|
775
|
-
openApiUrl = config.openapi_url;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
}
|
|
819
|
+
openApiUrl = await resolveOpenApiUrl(root, options.mode);
|
|
779
820
|
} catch {
|
|
780
821
|
}
|
|
781
822
|
const response = await prompts({
|
|
@@ -797,6 +838,11 @@ async function generateSDKForWorkspace(root = process.cwd(), options = {}) {
|
|
|
797
838
|
console.log(`\u{1F500} Split mode: ${splitFiles ? "per-module" : "monolithic"}
|
|
798
839
|
`);
|
|
799
840
|
try {
|
|
841
|
+
if (options.manifest) {
|
|
842
|
+
await generateOwnedModules(root, finalUrl, `${outputDir}/modules`, options.manifest);
|
|
843
|
+
console.log("\n\u2705 Generated deterministic per-module SDK in shared/modules!");
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
800
846
|
const { openAPI } = await import('@bagelink/sdk');
|
|
801
847
|
const { types, code } = await openAPI(finalUrl, "/api");
|
|
802
848
|
const outputPath = resolve(root, outputDir);
|
|
@@ -912,16 +958,27 @@ Lint Commands:
|
|
|
912
958
|
const splitMode = args.includes("--split") || args.includes("-s");
|
|
913
959
|
const modeIndex = args.indexOf("--mode");
|
|
914
960
|
const mode = modeIndex >= 0 ? args[modeIndex + 1] : void 0;
|
|
961
|
+
const manifestIndex = args.indexOf("--manifest");
|
|
962
|
+
const manifest = manifestIndex >= 0 ? args[manifestIndex + 1] : void 0;
|
|
915
963
|
let workspaceMode = isWorkspace(process.cwd());
|
|
916
964
|
if (forceWorkspace) workspaceMode = true;
|
|
917
965
|
if (forceProject) workspaceMode = false;
|
|
918
|
-
const sdkOptions = { split: splitMode || void 0, mode };
|
|
966
|
+
const sdkOptions = { split: splitMode || void 0, mode, manifest };
|
|
919
967
|
if (workspaceMode) {
|
|
920
968
|
console.log("\u2713 Detected workspace mode - will generate for multiple projects");
|
|
921
969
|
await generateSDKForWorkspace(process.cwd(), sdkOptions);
|
|
922
970
|
} else {
|
|
923
971
|
await generateSDK(process.cwd(), sdkOptions);
|
|
924
972
|
}
|
|
973
|
+
} else if (subcommand === "validate") {
|
|
974
|
+
const modeIndex = args.indexOf("--mode");
|
|
975
|
+
const mode = modeIndex >= 0 ? args[modeIndex + 1] : void 0;
|
|
976
|
+
const manifestIndex = args.indexOf("--manifest");
|
|
977
|
+
const manifest = manifestIndex >= 0 ? args[manifestIndex + 1] : void 0;
|
|
978
|
+
const openApiUrl = await resolveOpenApiUrl(process.cwd(), mode);
|
|
979
|
+
if (!openApiUrl) throw new Error("No openapi_url found in bgl.config.ts");
|
|
980
|
+
await validateOpenApiConformance(process.cwd(), openApiUrl, manifest);
|
|
981
|
+
console.log("\u2705 OpenAPI conforms to explicit module ownership manifest");
|
|
925
982
|
} else {
|
|
926
983
|
console.log(`
|
|
927
984
|
SDK Commands:
|
|
@@ -930,6 +987,8 @@ SDK Commands:
|
|
|
930
987
|
bgl sdk generate --workspace Force workspace mode
|
|
931
988
|
bgl sdk generate --project Force single project mode
|
|
932
989
|
bgl sdk generate --mode <env> Use openapi_url from that bgl.config.ts env
|
|
990
|
+
bgl sdk generate --manifest <path> Generate explicit per-module SDK files
|
|
991
|
+
bgl sdk validate [--manifest <path>] Validate endpoint ownership and schemas
|
|
933
992
|
`);
|
|
934
993
|
process.exit(1);
|
|
935
994
|
}
|
|
@@ -969,12 +1028,14 @@ Usage:
|
|
|
969
1028
|
bgl lint init Set up linting (auto-detects workspace)
|
|
970
1029
|
bgl sdk generate Generate SDK (auto-detects workspace)
|
|
971
1030
|
bgl sdk generate --split Generate per-module API files
|
|
1031
|
+
bgl sdk validate Validate OpenAPI module ownership
|
|
972
1032
|
|
|
973
1033
|
Options:
|
|
974
1034
|
--workspace, -w Force workspace mode
|
|
975
1035
|
--project, -p Force single project mode
|
|
976
1036
|
--split, -s Split SDK into per-module files
|
|
977
1037
|
--mode <env> Set environment (matches bgl.config.ts keys)
|
|
1038
|
+
--manifest <path> Ownership manifest (default: openapi.modules.json)
|
|
978
1039
|
--help, -h Show this help message
|
|
979
1040
|
|
|
980
1041
|
Note: Commands auto-detect workspace mode based on directory structure
|
package/package.json
CHANGED
package/src/openapi.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ModuleOwnershipManifest, OpenAPIObject } from '@bagelink/sdk'
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { relative, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_OWNERSHIP_MANIFEST = 'openapi.modules.json'
|
|
6
|
+
|
|
7
|
+
function resolveWithinRoot(root: string, file: string): string {
|
|
8
|
+
const absoluteRoot = resolve(root)
|
|
9
|
+
const path = resolve(absoluteRoot, file)
|
|
10
|
+
const relativePath = relative(absoluteRoot, path)
|
|
11
|
+
if (relativePath.startsWith('..') || relativePath.includes('/../') || relativePath === '') {
|
|
12
|
+
throw new Error(`Path must be inside the project root: ${file}`)
|
|
13
|
+
}
|
|
14
|
+
return path
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readOwnershipManifest(root: string, file = DEFAULT_OWNERSHIP_MANIFEST): ModuleOwnershipManifest {
|
|
18
|
+
const path = resolveWithinRoot(root, file)
|
|
19
|
+
if (!existsSync(path)) throw new Error(`Module ownership manifest not found: ${path}`)
|
|
20
|
+
return JSON.parse(readFileSync(path, 'utf8')) as ModuleOwnershipManifest
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function fetchOpenApi(url: string): Promise<OpenAPIObject> {
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
headers: { Authorization: 'Basic YmFnZWxfdXNlcm5hbWU6Tm90U2VjdXJlQGJhZ2Vs' },
|
|
26
|
+
})
|
|
27
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: failed to fetch ${url}`)
|
|
28
|
+
return await response.json() as OpenAPIObject
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function validateOpenApiConformance(
|
|
32
|
+
root: string,
|
|
33
|
+
openApiUrl: string,
|
|
34
|
+
manifestFile = DEFAULT_OWNERSHIP_MANIFEST,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
const { validateOwnershipManifest } = await import('@bagelink/sdk')
|
|
37
|
+
const issues = validateOwnershipManifest(await fetchOpenApi(openApiUrl), readOwnershipManifest(root, manifestFile))
|
|
38
|
+
if (issues.length) throw new Error(`OpenAPI ownership conformance failed:\n${issues.map(issue => `- ${issue.message}`).join('\n')}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function generateOwnedModules(
|
|
42
|
+
root: string,
|
|
43
|
+
openApiUrl: string,
|
|
44
|
+
outputDir: string,
|
|
45
|
+
manifestFile = DEFAULT_OWNERSHIP_MANIFEST,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
const { generateModules } = await import('@bagelink/sdk')
|
|
48
|
+
const outputPath = resolveWithinRoot(root, outputDir)
|
|
49
|
+
const modules = generateModules(await fetchOpenApi(openApiUrl), readOwnershipManifest(root, manifestFile), '/api')
|
|
50
|
+
rmSync(outputPath, { recursive: true, force: true })
|
|
51
|
+
mkdirSync(outputPath, { recursive: true })
|
|
52
|
+
for (const module of modules) {
|
|
53
|
+
const moduleDir = resolveWithinRoot(outputPath, module.name)
|
|
54
|
+
mkdirSync(moduleDir, { recursive: true })
|
|
55
|
+
writeFileSync(resolve(moduleDir, 'api.ts'), module.code)
|
|
56
|
+
writeFileSync(resolve(moduleDir, 'types.d.ts'), module.types)
|
|
57
|
+
writeFileSync(resolve(moduleDir, 'index.ts'), `export * from './api'\nexport * from './types.d'\n`)
|
|
58
|
+
}
|
|
59
|
+
writeFileSync(resolve(outputPath, 'index.ts'), modules.map(module => `export * as ${module.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())} from './${module.name}'`).join('\n') + '\n')
|
|
60
|
+
writeFileSync(resolve(outputPath, 'manifest.json'), `${JSON.stringify({
|
|
61
|
+
version: 1,
|
|
62
|
+
modules: modules.map(({ name, paths, schemas }) => ({ name, paths, schemas })),
|
|
63
|
+
}, null, 2)}\n`)
|
|
64
|
+
}
|
package/src/sdk.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import type { WorkspaceConfig } from './types'
|
|
2
1
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
2
|
import { resolve } from 'node:path'
|
|
4
3
|
import process from 'node:process'
|
|
5
4
|
import prompts from 'prompts'
|
|
5
|
+
import { generateOwnedModules } from './openapi'
|
|
6
6
|
|
|
7
7
|
export interface GenerateSDKOptions {
|
|
8
8
|
/** Force split mode (per-module files) */
|
|
9
9
|
split?: boolean
|
|
10
10
|
/** Which bgl.config.ts environment's openapi_url to use (default: 'development') */
|
|
11
11
|
mode?: string
|
|
12
|
+
/** Explicit module ownership manifest path, relative to the project root */
|
|
13
|
+
manifest?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function resolveOpenApiUrl(root: string, mode = 'development'): Promise<string | undefined> {
|
|
17
|
+
const configPath = resolve(root, 'bgl.config.ts')
|
|
18
|
+
if (!existsSync(configPath)) return undefined
|
|
19
|
+
const module = await import(`file://${configPath}`)
|
|
20
|
+
const workspace = module.default
|
|
21
|
+
return typeof workspace === 'function' ? workspace(mode)?.openapi_url : undefined
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
/**
|
|
@@ -21,21 +31,10 @@ export async function generateSDK(
|
|
|
21
31
|
console.log('\n🔧 Generating SDK from OpenAPI...\n')
|
|
22
32
|
|
|
23
33
|
// Try to load config
|
|
24
|
-
let config: WorkspaceConfig | null = null
|
|
25
34
|
let openApiUrl: string | undefined
|
|
26
35
|
|
|
27
36
|
try {
|
|
28
|
-
|
|
29
|
-
if (existsSync(configPath)) {
|
|
30
|
-
const module = await import(`file://${configPath}`)
|
|
31
|
-
const workspace = module.default
|
|
32
|
-
if (typeof workspace === 'function') {
|
|
33
|
-
config = workspace(options.mode ?? 'development')
|
|
34
|
-
if (config?.openapi_url) {
|
|
35
|
-
openApiUrl = config.openapi_url
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
37
|
+
openApiUrl = await resolveOpenApiUrl(root, options.mode)
|
|
39
38
|
}
|
|
40
39
|
catch {
|
|
41
40
|
// Ignore config load errors
|
|
@@ -77,6 +76,11 @@ export async function generateSDK(
|
|
|
77
76
|
console.log(`🔀 Split mode: ${splitFiles ? 'per-module' : 'monolithic'}\n`)
|
|
78
77
|
|
|
79
78
|
try {
|
|
79
|
+
if (options.manifest) {
|
|
80
|
+
await generateOwnedModules(root, finalUrl, outputDir, options.manifest)
|
|
81
|
+
console.log('\n✅ Generated deterministic per-module SDK!')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
80
84
|
const { openAPI } = await import('@bagelink/sdk')
|
|
81
85
|
|
|
82
86
|
const { types, code } = await openAPI(finalUrl, '/api')
|
|
@@ -157,21 +161,10 @@ export async function generateSDKForWorkspace(
|
|
|
157
161
|
console.log('\n🏢 Generating SDK for workspace...\n')
|
|
158
162
|
|
|
159
163
|
// Try to load config
|
|
160
|
-
let config: WorkspaceConfig | null = null
|
|
161
164
|
let openApiUrl: string | undefined
|
|
162
165
|
|
|
163
166
|
try {
|
|
164
|
-
|
|
165
|
-
if (existsSync(configPath)) {
|
|
166
|
-
const module = await import(`file://${configPath}`)
|
|
167
|
-
const workspace = module.default
|
|
168
|
-
if (typeof workspace === 'function') {
|
|
169
|
-
config = workspace(options.mode ?? 'development')
|
|
170
|
-
if (config?.openapi_url) {
|
|
171
|
-
openApiUrl = config.openapi_url
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
167
|
+
openApiUrl = await resolveOpenApiUrl(root, options.mode)
|
|
175
168
|
}
|
|
176
169
|
catch {
|
|
177
170
|
// Ignore config load errors
|
|
@@ -199,6 +192,11 @@ export async function generateSDKForWorkspace(
|
|
|
199
192
|
console.log(`🔀 Split mode: ${splitFiles ? 'per-module' : 'monolithic'}\n`)
|
|
200
193
|
|
|
201
194
|
try {
|
|
195
|
+
if (options.manifest) {
|
|
196
|
+
await generateOwnedModules(root, finalUrl, `${outputDir}/modules`, options.manifest)
|
|
197
|
+
console.log('\n✅ Generated deterministic per-module SDK in shared/modules!')
|
|
198
|
+
return
|
|
199
|
+
}
|
|
202
200
|
const { openAPI } = await import('@bagelink/sdk')
|
|
203
201
|
|
|
204
202
|
const { types, code } = await openAPI(finalUrl, '/api')
|