@workos/oagen 0.22.7 → 0.23.0

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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.mjs +142 -33
  3. package/dist/cli/index.mjs.map +1 -1
  4. package/dist/compat/index.d.mts +2 -2
  5. package/dist/compat/index.mjs +7 -7
  6. package/dist/compat/index.mjs.map +1 -1
  7. package/dist/{diff-D4XaiPeA.mjs → diff-B7dMUCoJ.mjs} +128 -19
  8. package/dist/diff-B7dMUCoJ.mjs.map +1 -0
  9. package/dist/{differ-Q13KY9Rd.d.mts → differ-DMmw0tbA.d.mts} +5 -5
  10. package/dist/{differ-Q13KY9Rd.d.mts.map → differ-DMmw0tbA.d.mts.map} +1 -1
  11. package/dist/index.d.mts +50 -9
  12. package/dist/index.d.mts.map +1 -1
  13. package/dist/index.mjs +9 -11
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/{overlay-cb32jDBS.mjs → overlay-sQHQoN0I.mjs} +4 -4
  16. package/dist/overlay-sQHQoN0I.mjs.map +1 -0
  17. package/dist/scripts/smoke/baseline.mjs +8 -10
  18. package/dist/scripts/smoke/baseline.mjs.map +1 -1
  19. package/dist/scripts/smoke/sdk-test.mjs +3 -3
  20. package/dist/scripts/smoke/sdk-test.mjs.map +1 -1
  21. package/dist/smoke.d.mts +3 -4
  22. package/dist/smoke.d.mts.map +1 -1
  23. package/dist/smoke.mjs +8 -10
  24. package/dist/smoke.mjs.map +1 -1
  25. package/dist/{types-DZrKFHis.d.mts → types-nM5Kp7UP.d.mts} +7 -9
  26. package/dist/types-nM5Kp7UP.d.mts.map +1 -0
  27. package/dist/verify/index.d.mts +2 -2
  28. package/dist/verify/index.mjs +2 -2
  29. package/docs/architecture/extractor-contract.md +2 -2
  30. package/docs/architecture/pipeline.md +2 -2
  31. package/docs/cli.md +19 -19
  32. package/docs/core/compatibility-policy.md +1 -1
  33. package/docs/core/manifest-schema.md +2 -2
  34. package/package.json +1 -1
  35. package/skills/generate-emitter/SKILL.md +2 -2
  36. package/skills/generate-emitter/references/common-pitfalls.md +1 -1
  37. package/skills/generate-emitter/references/project-scaffold.md +3 -3
  38. package/skills/generate-extractor/SKILL.md +2 -2
  39. package/skills/generate-smoke-test/SKILL.md +1 -1
  40. package/skills/verify-smoke-test/SKILL.md +1 -1
  41. package/dist/diff-D4XaiPeA.mjs.map +0 -1
  42. package/dist/overlay-cb32jDBS.mjs.map +0 -1
  43. package/dist/types-DZrKFHis.d.mts.map +0 -1
@@ -715,12 +715,12 @@ function extractKotlinClassMembers(classBody, source) {
715
715
  }
716
716
  /**
717
717
  * KDoc signature that identifies a generator-emitted service accessor inside
718
- * the WorkOS class. The Kotlin client emitter writes every accessor with this
718
+ * a Kotlin client class. The Kotlin client emitter writes every accessor with this
719
719
  * exact doc preamble — when the existing file carries a member with this doc
720
720
  * but the regenerated content omits it, the member is stale (its mount target
721
721
  * was remapped or removed) and gets pruned.
722
722
  */
723
- const KOTLIN_MANAGED_ACCESSOR_DOC = /\/\*\*[\s\S]*?Lazily-constructed \[[^\]]+\] accessor for this \[WorkOS\] client\.[\s\S]*?\*\//;
723
+ const KOTLIN_MANAGED_ACCESSOR_DOC = /\/\*\*[\s\S]*?Lazily-constructed \[[^\]]+\] accessor for this \[[^\]]+\] client\.[\s\S]*?\*\//;
724
724
  const kotlinMergeAdapter = {
725
725
  language: "kotlin",
726
726
  grammarModule: "tree-sitter-kotlin",
@@ -973,7 +973,6 @@ const nodeMergeAdapter = {
973
973
  const newText = newMembers.map((m) => m.text).join("\n");
974
974
  for (const match of newText.matchAll(/this\.(\w+)/g)) {
975
975
  const propName = match[1];
976
- if (propName === "workos") continue;
977
976
  if (propName && !existingMemberKeys.has(propName)) return true;
978
977
  }
979
978
  return false;
@@ -2957,6 +2956,15 @@ function spliceExtraImports(existingLines, resultLines) {
2957
2956
  //#endregion
2958
2957
  //#region src/engine/generate-files.ts
2959
2958
  function buildEmitterContext(spec, options) {
2959
+ const resolvedOperations = resolveOperations(spec, options.operationHints, options.mountRules);
2960
+ let scopedModelNames;
2961
+ let scopedEnumNames;
2962
+ if (options.scopedServices && options.scopedServices.size > 0) {
2963
+ const selected = options.scopedServices;
2964
+ const reachable = collectReferencedNames(spec.services.filter((s) => resolvedOperations.some((r) => r.service.name === s.name && selected.has(r.mountOn))), spec.models, { preserveAllDiscriminated: false });
2965
+ scopedModelNames = reachable.models;
2966
+ scopedEnumNames = reachable.enums;
2967
+ }
2960
2968
  return {
2961
2969
  namespace: toSnakeCase(options.namespace),
2962
2970
  namespacePascal: options.namespace,
@@ -2964,11 +2972,14 @@ function buildEmitterContext(spec, options) {
2964
2972
  outputDir: options.outputDir,
2965
2973
  apiSurface: options.apiSurface,
2966
2974
  overlayLookup: options.overlayLookup,
2967
- resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules),
2975
+ resolvedOperations,
2968
2976
  modelHints: options.modelHints,
2969
2977
  emitterOptions: options.emitterOptions,
2970
2978
  targetDir: options.target,
2971
- priorTargetManifestPaths: options.priorTargetManifestPaths
2979
+ priorTargetManifestPaths: options.priorTargetManifestPaths,
2980
+ scopedServices: options.scopedServices,
2981
+ scopedModelNames,
2982
+ scopedEnumNames
2972
2983
  };
2973
2984
  }
2974
2985
  /**
@@ -2976,7 +2987,8 @@ function buildEmitterContext(spec, options) {
2976
2987
  * Walks operation parameters, request bodies, responses, and pagination item
2977
2988
  * types, then chases model field references until the set stabilizes.
2978
2989
  */
2979
- function collectReferencedNames(services, models) {
2990
+ function collectReferencedNames(services, models, opts = {}) {
2991
+ const preserveAllDiscriminated = opts.preserveAllDiscriminated !== false;
2980
2992
  const referencedModels = /* @__PURE__ */ new Set();
2981
2993
  const referencedEnums = /* @__PURE__ */ new Set();
2982
2994
  const collectFromTypeRef = (ref) => {
@@ -2998,7 +3010,7 @@ function collectReferencedNames(services, models) {
2998
3010
  for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
2999
3011
  if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
3000
3012
  }
3001
- for (const model of models) {
3013
+ if (preserveAllDiscriminated) for (const model of models) {
3002
3014
  if (isDiscriminatedModel(model)) referencedModels.add(model.name);
3003
3015
  const disc = model.discriminator;
3004
3016
  if (disc?.mapping) for (const variantName of Object.values(disc.mapping)) referencedModels.add(variantName);
@@ -3012,6 +3024,13 @@ function collectReferencedNames(services, models) {
3012
3024
  visited.add(name);
3013
3025
  const model = modelsByName.get(name);
3014
3026
  if (!model) continue;
3027
+ const disc = model.discriminator;
3028
+ if (disc?.mapping) {
3029
+ for (const variantName of Object.values(disc.mapping)) if (!referencedModels.has(variantName)) {
3030
+ referencedModels.add(variantName);
3031
+ queue.push(variantName);
3032
+ }
3033
+ }
3015
3034
  for (const field of model.fields) {
3016
3035
  collectFromTypeRef(field.type);
3017
3036
  for (const m of referencedModels) if (!visited.has(m)) queue.push(m);
@@ -3025,7 +3044,16 @@ function collectReferencedNames(services, models) {
3025
3044
  function isDiscriminatedModel(model) {
3026
3045
  return model.fields.some((field) => (field.name === "event" || field.name === "type" || field.name === "object") && field.type.kind === "literal");
3027
3046
  }
3028
- /** Collect all generated files from an emitter (no headers, no path prefixes). */
3047
+ /**
3048
+ * Collect all generated files from an emitter (no headers, no path prefixes).
3049
+ *
3050
+ * In a scoped (`--services`) run the spec is NOT filtered: every generator runs
3051
+ * over the full spec so model placement, dedup, the root client, and all
3052
+ * aggregate/barrel files are byte-identical to a full run (and a brand-new
3053
+ * selected service is wired into the client automatically). The emitters
3054
+ * themselves consult `ctx.scopedServices` to emit only the selected services'
3055
+ * per-service resource/test files.
3056
+ */
3029
3057
  function generateAllFiles(spec, emitter, ctx) {
3030
3058
  const referenced = collectReferencedNames(spec.services, spec.models);
3031
3059
  const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
@@ -3066,6 +3094,47 @@ function generateFiles(spec, emitter, options) {
3066
3094
  };
3067
3095
  }
3068
3096
  //#endregion
3097
+ //#region src/engine/scoped-services.ts
3098
+ /**
3099
+ * Validate a `--services` selection and return it as a set of POST-MOUNT service
3100
+ * names for emitters to scope on.
3101
+ *
3102
+ * Scoped generation does NOT filter the IR: model placement, dedup/aliasing, and
3103
+ * shared-schema decisions are global computations over the full service/model set
3104
+ * (filtering them churns shared `common/` files). Instead the full spec flows to
3105
+ * the emitters unchanged and this set is passed as `ctx.scopedServices`; each
3106
+ * emitter gates only its per-service resource/test emission on it.
3107
+ *
3108
+ * Matching is on the POST-MOUNT name. Mount resolution never mutates
3109
+ * `spec.services` — the post-mount target lives on `ResolvedOperation.mountOn`
3110
+ * (`mountOn = hint?.mountOn ?? mountRules[service.name] ?? service.name`) — so the
3111
+ * post-mount name of a source service is derived here as
3112
+ * `mountRules[service.name] ?? service.name`. Selecting a target (e.g.
3113
+ * `DirectorySync`) is valid because every source tag that mounts into it
3114
+ * (`DirectoryUsers`, `DirectoryGroups`, …) shares that post-mount name, and the
3115
+ * emitters group resources by mount, so all siblings emit together.
3116
+ *
3117
+ * @param spec - The parsed API spec (never mutated; never filtered).
3118
+ * @param selected - Post-mount service names from `--services` (normalized/trimmed).
3119
+ * @param mountRules - Source-service-name → post-mount-target mappings.
3120
+ * @returns The validated selection as a `Set` of post-mount names.
3121
+ * @throws A `ConfigError` listing valid post-mount names when a selection is
3122
+ * unknown — scoped generation never silently emits the wrong set.
3123
+ */
3124
+ function resolveScopedServices(spec, selected, mountRules) {
3125
+ const mounts = mountRules ?? {};
3126
+ const postMountName = (name) => mounts[name] ?? name;
3127
+ const validPostMount = new Set(spec.services.map((s) => postMountName(s.name)));
3128
+ const selectedSet = new Set(selected);
3129
+ const unknown = [...selectedSet].filter((s) => !validPostMount.has(s));
3130
+ if (unknown.length > 0) {
3131
+ const err = /* @__PURE__ */ new Error(`Unknown --services: ${unknown.sort().join(", ")}. Valid post-mount services: ${[...validPostMount].sort().join(", ")}`);
3132
+ err.name = "ConfigError";
3133
+ throw err;
3134
+ }
3135
+ return selectedSet;
3136
+ }
3137
+ //#endregion
3069
3138
  //#region src/engine/integrate.ts
3070
3139
  /**
3071
3140
  * Resolve a relative import path against a source file path to get the
@@ -3182,7 +3251,7 @@ function mapFilesForTargetIntegration(files, language) {
3182
3251
  * that would shadow an existing module file (e.g., `api_keys.py`), move the
3183
3252
  * module file's content into `_compat.py` inside the package and add a
3184
3253
  * re-export from `__init__.py`. This preserves backward-compatible imports
3185
- * (e.g., `from workos.api_keys import ApiKeysModule`) while the package directory
3254
+ * (e.g., `from acme.api_keys import ApiKeysModule`) while the package directory
3186
3255
  * provides the generated models and resource classes.
3187
3256
  */
3188
3257
  async function resolveFileToDirectoryConflicts(files, targetDir) {
@@ -3348,6 +3417,35 @@ function computeStalePaths(prev, currentPaths) {
3348
3417
  return prev.files.filter((p) => !current.has(p)).sort();
3349
3418
  }
3350
3419
  /**
3420
+ * Merge a scoped run's emitted records into the prior manifest's records.
3421
+ *
3422
+ * A scoped (`--services`) run only emits files for the selected services, but it
3423
+ * must NOT discard the prior manifest's record of unselected services' files
3424
+ * records. The result unions prior + scoped file paths and shallow-merges the
3425
+ * operations maps, with scoped operations overriding prior entries for the same
3426
+ * "METHOD /path" key (a re-emission updates that operation's record).
3427
+ *
3428
+ * When `prev` is null (the first-ever run is scoped) there is nothing to
3429
+ * preserve, so the scoped subset is returned unchanged — matching the
3430
+ * first-adoption behavior of a full run.
3431
+ */
3432
+ function mergeScopedManifestRecords(prev, scopedFiles, scopedOperations) {
3433
+ const scopedFileList = [...scopedFiles];
3434
+ if (!prev) return {
3435
+ files: scopedFileList,
3436
+ operations: scopedOperations
3437
+ };
3438
+ const files = [...new Set([...prev.files, ...scopedFileList])].sort();
3439
+ const priorOps = prev.operations;
3440
+ return {
3441
+ files,
3442
+ operations: priorOps || scopedOperations ? {
3443
+ ...priorOps,
3444
+ ...scopedOperations
3445
+ } : void 0
3446
+ };
3447
+ }
3448
+ /**
3351
3449
  * Delete stale files from `dir`.
3352
3450
  *
3353
3451
  * When `header` is provided, a file is only deleted if its contents start with
@@ -3416,12 +3514,17 @@ async function removeEmptyParents(startDir, stopDir) {
3416
3514
  //#endregion
3417
3515
  //#region src/engine/orchestrator.ts
3418
3516
  async function generate(spec, emitter, options) {
3419
- const outputPrevManifestForCtx = options.noPrune ? null : await readManifest(options.outputDir);
3420
- const targetManifestForCtx = options.target && !options.noPrune ? await readManifest(options.target) : null;
3517
+ const scopedServices = options.services && options.services.length > 0 ? resolveScopedServices(spec, options.services, options.mountRules) : void 0;
3518
+ const scoped = scopedServices !== void 0;
3519
+ const noPrune = options.noPrune === true || scoped;
3520
+ const skipManifestRead = options.noPrune === true && !scoped;
3521
+ const outputPrevManifestForCtx = skipManifestRead ? null : await readManifest(options.outputDir);
3522
+ const targetManifestForCtx = options.target && !skipManifestRead ? await readManifest(options.target) : null;
3421
3523
  const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : outputPrevManifestForCtx ? new Set(outputPrevManifestForCtx.files) : void 0;
3422
3524
  const { files: withHeaders, header, operations } = generateFiles(spec, emitter, {
3423
3525
  ...options,
3424
- priorTargetManifestPaths
3526
+ priorTargetManifestPaths,
3527
+ scopedServices
3425
3528
  });
3426
3529
  if (options.dryRun) {
3427
3530
  if (options.target) {
@@ -3445,8 +3548,9 @@ async function generate(spec, emitter, options) {
3445
3548
  currentPaths: outputEmittedPaths,
3446
3549
  language: emitter.language,
3447
3550
  header,
3448
- noPrune: options.noPrune,
3449
- operations
3551
+ noPrune,
3552
+ operations,
3553
+ scoped
3450
3554
  });
3451
3555
  const allOutputFiles = [...writeResult.written, ...writeResult.merged];
3452
3556
  if (allOutputFiles.length > 0) await formatTargetFiles(emitter, options.outputDir, allOutputFiles);
@@ -3468,8 +3572,9 @@ async function generate(spec, emitter, options) {
3468
3572
  currentPaths: targetResult.emittedPaths,
3469
3573
  language: emitter.language,
3470
3574
  header,
3471
- noPrune: options.noPrune,
3472
- operations
3575
+ noPrune,
3576
+ operations,
3577
+ scoped
3473
3578
  });
3474
3579
  const allTargetFiles = [
3475
3580
  ...targetResult.written,
@@ -3485,11 +3590,15 @@ async function generate(spec, emitter, options) {
3485
3590
  * then write the fresh manifest. Safe on first adoption (no previous manifest).
3486
3591
  */
3487
3592
  async function applyManifestPrune(opts) {
3488
- const manifestOpts = {
3489
- language: opts.language,
3593
+ const records = opts.scoped ? mergeScopedManifestRecords(opts.prevManifest, opts.currentPaths, opts.operations) : {
3490
3594
  files: opts.currentPaths,
3491
3595
  operations: opts.operations
3492
3596
  };
3597
+ const manifestOpts = {
3598
+ language: opts.language,
3599
+ files: records.files,
3600
+ operations: records.operations
3601
+ };
3493
3602
  if (opts.noPrune) {
3494
3603
  await writeManifest(opts.dir, manifestOpts);
3495
3604
  return;
@@ -3960,4 +4069,4 @@ function summarize(changes) {
3960
4069
  //#endregion
3961
4070
  export { MANIFEST_FILENAME as a, treeShakeFiles as c, collectReferencedNames as d, generateAllFiles as f, resolveOperations as h, generate as i, applyFileHeaders as l, deriveMethodName as m, getEmitter as n, integrateGeneratedFiles as o, generateFiles as p, registerEmitter as r, mapFilesForTargetIntegration as s, diffSpecs as t, buildEmitterContext as u };
3962
4071
 
3963
- //# sourceMappingURL=diff-D4XaiPeA.mjs.map
4072
+ //# sourceMappingURL=diff-B7dMUCoJ.mjs.map