@workos/oagen 0.24.0 → 0.24.1

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.
@@ -5779,6 +5779,58 @@ function resolveResponseModelName(op) {
5779
5779
  return null;
5780
5780
  }
5781
5781
  //#endregion
5782
+ //#region src/engine/scoped-services.ts
5783
+ /**
5784
+ * Validate a `--services` selection and return it as a set of POST-MOUNT service
5785
+ * names for emitters to scope on.
5786
+ *
5787
+ * Scoped generation does NOT filter the IR: model placement, dedup/aliasing, and
5788
+ * shared-schema decisions are global computations over the full service/model set
5789
+ * (filtering them churns shared `common/` files). Instead the full spec flows to
5790
+ * the emitters unchanged and this set is passed as `ctx.scopedServices`; each
5791
+ * emitter gates only its per-service resource/test emission on it.
5792
+ *
5793
+ * Matching is on the POST-MOUNT name. Mount resolution never mutates
5794
+ * `spec.services` — the post-mount target lives on `ResolvedOperation.mountOn`
5795
+ * (`mountOn = hint?.mountOn ?? mountRules[service.name] ?? service.name`) — so the
5796
+ * post-mount name of a source service is derived here as
5797
+ * `mountRules[service.name] ?? service.name`. Selecting a target (e.g.
5798
+ * `DirectorySync`) is valid because every source tag that mounts into it
5799
+ * (`DirectoryUsers`, `DirectoryGroups`, …) shares that post-mount name, and the
5800
+ * emitters group resources by mount, so all siblings emit together.
5801
+ *
5802
+ * @param spec - The parsed API spec (never mutated; never filtered).
5803
+ * @param selected - Post-mount service names from `--services` (normalized/trimmed).
5804
+ * @param mountRules - Source-service-name → post-mount-target mappings.
5805
+ * @returns The validated selection as a `Set` of post-mount names.
5806
+ * @throws A `ConfigError` listing valid post-mount names when a selection is
5807
+ * unknown — scoped generation never silently emits the wrong set.
5808
+ */
5809
+ /**
5810
+ * Canonicalize a service name to a casing/separator-insensitive key so that
5811
+ * `MultiFactorAuth` (post-mount IR name), `multi_factor_auth` (snake-cased
5812
+ * manifest record), and `multiFactorAuth` (camel-cased manifest record) all
5813
+ * collapse to `multifactorauth`. Used to match a spec service against the
5814
+ * services a prior manifest recorded as already on disk, where the casing of
5815
+ * the recorded `service` field varies per emitter.
5816
+ */
5817
+ function canonicalServiceKey(name) {
5818
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
5819
+ }
5820
+ function resolveScopedServices(spec, selected, mountRules) {
5821
+ const mounts = mountRules ?? {};
5822
+ const postMountName = (name) => mounts[name] ?? name;
5823
+ const validPostMount = new Set(spec.services.map((s) => postMountName(s.name)));
5824
+ const selectedSet = new Set(selected);
5825
+ const unknown = [...selectedSet].filter((s) => !validPostMount.has(s));
5826
+ if (unknown.length > 0) {
5827
+ const err = /* @__PURE__ */ new Error(`Unknown --services: ${unknown.sort().join(", ")}. Valid post-mount services: ${[...validPostMount].sort().join(", ")}`);
5828
+ err.name = "ConfigError";
5829
+ throw err;
5830
+ }
5831
+ return selectedSet;
5832
+ }
5833
+ //#endregion
5782
5834
  //#region src/engine/generate-files.ts
5783
5835
  function buildEmitterContext(spec, options) {
5784
5836
  const resolvedOperations = resolveOperations(spec, options.operationHints, options.mountRules);
@@ -5803,11 +5855,32 @@ function buildEmitterContext(spec, options) {
5803
5855
  targetDir: options.target,
5804
5856
  priorTargetManifestPaths: options.priorTargetManifestPaths,
5805
5857
  scopedServices: options.scopedServices,
5858
+ presentServiceKeys: options.presentServiceKeys,
5806
5859
  scopedModelNames,
5807
5860
  scopedEnumNames
5808
5861
  };
5809
5862
  }
5810
5863
  /**
5864
+ * The services an aggregate (barrel/client/test) generator may reference in a
5865
+ * scoped run: the selected services plus every service the prior manifest
5866
+ * recorded as already on disk. A service the spec just added that is neither
5867
+ * selected nor already generated is excluded — otherwise the client/barrels
5868
+ * would import a resource/model whose file this run does not emit (the orphaned
5869
+ * `agents/__init__.py → ._resource` class of build break).
5870
+ *
5871
+ * Outside a scoped run (full generation), every service is in surface.
5872
+ */
5873
+ function aggregateSurfaceServices(spec, ctx) {
5874
+ const scope = ctx.scopedServices;
5875
+ if (!scope || scope.size === 0) return spec.services;
5876
+ const present = ctx.presentServiceKeys ?? /* @__PURE__ */ new Set();
5877
+ const postMountOf = (service) => ctx.resolvedOperations?.find((r) => r.service.name === service.name)?.mountOn ?? service.name;
5878
+ return spec.services.filter((s) => {
5879
+ const postMount = postMountOf(s);
5880
+ return scope.has(postMount) || present.has(canonicalServiceKey(postMount));
5881
+ });
5882
+ }
5883
+ /**
5811
5884
  * Collect model and enum names transitively referenced by service operations.
5812
5885
  * Walks operation parameters, request bodies, responses, and pagination item
5813
5886
  * types, then chases model field references until the set stabilizes.
@@ -5872,27 +5945,38 @@ function isDiscriminatedModel(model) {
5872
5945
  /**
5873
5946
  * Collect all generated files from an emitter (no headers, no path prefixes).
5874
5947
  *
5875
- * In a scoped (`--services`) run the spec is NOT filtered: every generator runs
5876
- * over the full spec so model placement, dedup, the root client, and all
5877
- * aggregate/barrel files are byte-identical to a full run (and a brand-new
5878
- * selected service is wired into the client automatically). The emitters
5879
- * themselves consult `ctx.scopedServices` to emit only the selected services'
5880
- * per-service resource/test files.
5948
+ * A full run emits over the whole spec. A scoped (`--services`) run still emits
5949
+ * model placement, dedup, the root client, and aggregate/barrel files
5950
+ * byte-identically for every service ALREADY on disk (plus the brand-new
5951
+ * selected service, which is wired into the client automatically) — but it
5952
+ * narrows that surface to `selected ∪ already-on-disk` services
5953
+ * (`aggregateSurfaceServices`). A service the spec just added that is neither
5954
+ * selected nor already generated is dropped from the surface entirely, so its
5955
+ * models/discriminators are not pulled into reachability and barrels/the client
5956
+ * never import a resource this run does not emit. The emitters additionally
5957
+ * consult `ctx.scopedServices` to emit only the selected services' per-service
5958
+ * resource/test files.
5881
5959
  */
5882
5960
  function generateAllFiles(spec, emitter, ctx) {
5883
- const referenced = collectReferencedNames(spec.services, spec.models);
5961
+ const scoped = !!ctx.scopedServices && ctx.scopedServices.size > 0;
5962
+ const surfaceServices = aggregateSurfaceServices(spec, ctx);
5963
+ const referenced = collectReferencedNames(surfaceServices, spec.models, { preserveAllDiscriminated: !scoped });
5884
5964
  const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
5885
5965
  const reachableEnums = spec.enums.filter((e) => referenced.enums.has(e.name));
5886
- const reachableSpec = {
5966
+ const surfaceSpec = {
5887
5967
  ...spec,
5968
+ services: surfaceServices
5969
+ };
5970
+ const reachableSpec = {
5971
+ ...surfaceSpec,
5888
5972
  models: reachableModels,
5889
5973
  enums: reachableEnums
5890
5974
  };
5891
5975
  return [
5892
5976
  ...emitter.generateModels(reachableModels, ctx),
5893
5977
  ...emitter.generateEnums(reachableEnums, ctx),
5894
- ...emitter.generateResources(spec.services, ctx),
5895
- ...emitter.generateClient(spec, ctx),
5978
+ ...emitter.generateResources(surfaceServices, ctx),
5979
+ ...emitter.generateClient(surfaceSpec, ctx),
5896
5980
  ...emitter.generateErrors(ctx),
5897
5981
  ...emitter.generateTypeSignatures?.(reachableSpec, ctx) ?? [],
5898
5982
  ...emitter.generateTests(reachableSpec, ctx)
@@ -5909,7 +5993,11 @@ function applyFileHeaders(files, header) {
5909
5993
  function generateFiles(spec, emitter, options) {
5910
5994
  const ctx = buildEmitterContext(spec, options);
5911
5995
  const files = generateAllFiles(spec, emitter, ctx);
5912
- const operations = emitter.buildOperationsMap?.(spec, ctx);
5996
+ const surfaceSpec = {
5997
+ ...spec,
5998
+ services: aggregateSurfaceServices(spec, ctx)
5999
+ };
6000
+ const operations = emitter.buildOperationsMap?.(surfaceSpec, ctx);
5913
6001
  const header = emitter.fileHeader();
5914
6002
  return {
5915
6003
  files: applyFileHeaders(files, header),
@@ -5919,47 +6007,6 @@ function generateFiles(spec, emitter, options) {
5919
6007
  };
5920
6008
  }
5921
6009
  //#endregion
5922
- //#region src/engine/scoped-services.ts
5923
- /**
5924
- * Validate a `--services` selection and return it as a set of POST-MOUNT service
5925
- * names for emitters to scope on.
5926
- *
5927
- * Scoped generation does NOT filter the IR: model placement, dedup/aliasing, and
5928
- * shared-schema decisions are global computations over the full service/model set
5929
- * (filtering them churns shared `common/` files). Instead the full spec flows to
5930
- * the emitters unchanged and this set is passed as `ctx.scopedServices`; each
5931
- * emitter gates only its per-service resource/test emission on it.
5932
- *
5933
- * Matching is on the POST-MOUNT name. Mount resolution never mutates
5934
- * `spec.services` — the post-mount target lives on `ResolvedOperation.mountOn`
5935
- * (`mountOn = hint?.mountOn ?? mountRules[service.name] ?? service.name`) — so the
5936
- * post-mount name of a source service is derived here as
5937
- * `mountRules[service.name] ?? service.name`. Selecting a target (e.g.
5938
- * `DirectorySync`) is valid because every source tag that mounts into it
5939
- * (`DirectoryUsers`, `DirectoryGroups`, …) shares that post-mount name, and the
5940
- * emitters group resources by mount, so all siblings emit together.
5941
- *
5942
- * @param spec - The parsed API spec (never mutated; never filtered).
5943
- * @param selected - Post-mount service names from `--services` (normalized/trimmed).
5944
- * @param mountRules - Source-service-name → post-mount-target mappings.
5945
- * @returns The validated selection as a `Set` of post-mount names.
5946
- * @throws A `ConfigError` listing valid post-mount names when a selection is
5947
- * unknown — scoped generation never silently emits the wrong set.
5948
- */
5949
- function resolveScopedServices(spec, selected, mountRules) {
5950
- const mounts = mountRules ?? {};
5951
- const postMountName = (name) => mounts[name] ?? name;
5952
- const validPostMount = new Set(spec.services.map((s) => postMountName(s.name)));
5953
- const selectedSet = new Set(selected);
5954
- const unknown = [...selectedSet].filter((s) => !validPostMount.has(s));
5955
- if (unknown.length > 0) {
5956
- const err = /* @__PURE__ */ new Error(`Unknown --services: ${unknown.sort().join(", ")}. Valid post-mount services: ${[...validPostMount].sort().join(", ")}`);
5957
- err.name = "ConfigError";
5958
- throw err;
5959
- }
5960
- return selectedSet;
5961
- }
5962
- //#endregion
5963
6010
  //#region src/engine/integrate.ts
5964
6011
  /**
5965
6012
  * Resolve a relative import path against a source file path to get the
@@ -6271,6 +6318,35 @@ function mergeScopedManifestRecords(prev, scopedFiles, scopedOperations) {
6271
6318
  };
6272
6319
  }
6273
6320
  /**
6321
+ * Extract the set of services a prior manifest records as already generated on
6322
+ * disk, as canonical (casing/separator-insensitive) keys.
6323
+ *
6324
+ * The manifest's `operations` map carries each operation's owning `service`
6325
+ * (snake_case for most emitters, camelCase for Kotlin). A scoped (`--services`)
6326
+ * run uses this to tell "service in the spec that is already on disk" apart from
6327
+ * "service the spec just added that this SDK has never generated" — only the
6328
+ * former may be wired into aggregate/barrel/client files alongside the
6329
+ * selection, so a brand-new out-of-scope service is never referenced by files
6330
+ * whose implementation this run does not emit.
6331
+ *
6332
+ * Returns an empty set when the manifest is null or carries no operations
6333
+ * (a legacy manifest with no operations data); callers treat that as "present
6334
+ * set unknown" and fall back to not narrowing the surface.
6335
+ */
6336
+ function manifestServiceKeys(manifest) {
6337
+ const keys = /* @__PURE__ */ new Set();
6338
+ const ops = manifest?.operations;
6339
+ if (!ops) return keys;
6340
+ for (const value of Object.values(ops)) {
6341
+ const entries = Array.isArray(value) ? value : [value];
6342
+ for (const entry of entries) {
6343
+ const service = entry?.service;
6344
+ if (typeof service === "string" && service.length > 0) keys.add(canonicalServiceKey(service));
6345
+ }
6346
+ }
6347
+ return keys;
6348
+ }
6349
+ /**
6274
6350
  * Delete stale files from `dir`.
6275
6351
  *
6276
6352
  * When `header` is provided, a file is only deleted if its contents start with
@@ -6345,11 +6421,14 @@ async function generate(spec, emitter, options) {
6345
6421
  const skipManifestRead = options.noPrune === true && !scoped;
6346
6422
  const outputPrevManifestForCtx = skipManifestRead ? null : await readManifest(options.outputDir);
6347
6423
  const targetManifestForCtx = options.target && !skipManifestRead ? await readManifest(options.target) : null;
6348
- const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : outputPrevManifestForCtx ? new Set(outputPrevManifestForCtx.files) : void 0;
6424
+ const priorManifestForCtx = targetManifestForCtx ?? outputPrevManifestForCtx;
6425
+ const priorTargetManifestPaths = priorManifestForCtx ? new Set(priorManifestForCtx.files) : void 0;
6426
+ const presentServiceKeys = scoped ? manifestServiceKeys(priorManifestForCtx) : void 0;
6349
6427
  const { files: withHeaders, header, operations } = generateFiles(spec, emitter, {
6350
6428
  ...options,
6351
6429
  priorTargetManifestPaths,
6352
- scopedServices
6430
+ scopedServices,
6431
+ presentServiceKeys
6353
6432
  });
6354
6433
  if (options.dryRun) {
6355
6434
  if (options.target) {