@bagelink/workspace 1.15.558 → 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 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
- const configPath = node_path.resolve(root, "bgl.config.ts");
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
- const configPath = node_path.resolve(root, "bgl.config.ts");
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, mkdirSync, rmSync } from 'node:fs';
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
- const configPath = resolve(root, "bgl.config.ts");
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
- const configPath = resolve(root, "bgl.config.ts");
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/workspace",
3
3
  "type": "module",
4
- "version": "1.15.558",
4
+ "version": "1.15.562",
5
5
  "description": "Monorepo workspace tooling for Bagel projects with proxy and config management",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
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
- const configPath = resolve(root, 'bgl.config.ts')
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
- const configPath = resolve(root, 'bgl.config.ts')
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')