@workos/oagen 0.22.7 → 0.24.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.
- package/README.md +1 -1
- package/dist/cli/index.mjs +207 -33
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +2 -2
- package/dist/compat/index.mjs +7 -7
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-D4XaiPeA.mjs → diff-B7dMUCoJ.mjs} +128 -19
- package/dist/diff-B7dMUCoJ.mjs.map +1 -0
- package/dist/{differ-Q13KY9Rd.d.mts → differ-DMmw0tbA.d.mts} +5 -5
- package/dist/{differ-Q13KY9Rd.d.mts.map → differ-DMmw0tbA.d.mts.map} +1 -1
- package/dist/index.d.mts +50 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +74 -11
- package/dist/index.mjs.map +1 -1
- package/dist/{overlay-cb32jDBS.mjs → overlay-sQHQoN0I.mjs} +4 -4
- package/dist/overlay-sQHQoN0I.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.mjs +73 -10
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/scripts/smoke/sdk-test.mjs +3 -3
- package/dist/scripts/smoke/sdk-test.mjs.map +1 -1
- package/dist/smoke.d.mts +3 -4
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +73 -10
- package/dist/smoke.mjs.map +1 -1
- package/dist/{types-DZrKFHis.d.mts → types-nM5Kp7UP.d.mts} +7 -9
- package/dist/types-nM5Kp7UP.d.mts.map +1 -0
- package/dist/verify/index.d.mts +2 -2
- package/dist/verify/index.mjs +2 -2
- package/docs/architecture/extractor-contract.md +2 -2
- package/docs/architecture/pipeline.md +2 -2
- package/docs/cli.md +19 -19
- package/docs/core/compatibility-policy.md +1 -1
- package/docs/core/manifest-schema.md +2 -2
- package/package.json +1 -1
- package/skills/generate-emitter/SKILL.md +2 -2
- package/skills/generate-emitter/references/common-pitfalls.md +1 -1
- package/skills/generate-emitter/references/project-scaffold.md +3 -3
- package/skills/generate-extractor/SKILL.md +2 -2
- package/skills/generate-smoke-test/SKILL.md +1 -1
- package/skills/verify-smoke-test/SKILL.md +1 -1
- package/dist/diff-D4XaiPeA.mjs.map +0 -1
- package/dist/overlay-cb32jDBS.mjs.map +0 -1
- package/dist/types-DZrKFHis.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -165,7 +165,7 @@ export default {
|
|
|
165
165
|
retry: { backoff: { initialDelay: 0.5, maxDelay: 8.0 } },
|
|
166
166
|
timeout: {
|
|
167
167
|
defaultTimeoutSeconds: 30,
|
|
168
|
-
timeoutEnvVar: "
|
|
168
|
+
timeoutEnvVar: "SDK_REQUEST_TIMEOUT",
|
|
169
169
|
},
|
|
170
170
|
pagination: { autoPageDelayMs: 0 },
|
|
171
171
|
},
|
package/dist/cli/index.mjs
CHANGED
|
@@ -14,9 +14,8 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rea
|
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
//#region src/ir/sdk-behavior.ts
|
|
16
16
|
/**
|
|
17
|
-
* Canonical SDK behavior defaults.
|
|
18
|
-
*
|
|
19
|
-
* via `mergeSdkBehavior()` in each SDK's `oagen.config.ts`.
|
|
17
|
+
* Canonical SDK behavior defaults. Per-language or per-project overrides can
|
|
18
|
+
* be applied via `mergeSdkBehavior()` in each SDK's `oagen.config.ts`.
|
|
20
19
|
*
|
|
21
20
|
* - Retry: exponential backoff with jitter, retries on 429/5xx
|
|
22
21
|
* - Errors: standard HTTP status → exception kind mapping
|
|
@@ -59,12 +58,11 @@ function defaultSdkBehavior() {
|
|
|
59
58
|
429: "RateLimitExceeded"
|
|
60
59
|
},
|
|
61
60
|
serverErrorKind: "Server",
|
|
62
|
-
clientErrorKind: "Api"
|
|
63
|
-
errorDocUrlTemplate: "https://workos.com/docs/errors/{code}"
|
|
61
|
+
clientErrorKind: "Api"
|
|
64
62
|
},
|
|
65
63
|
telemetry: {
|
|
66
64
|
enabledByDefault: true,
|
|
67
|
-
headerName: "X-
|
|
65
|
+
headerName: "X-Client-Telemetry",
|
|
68
66
|
requestIdHeader: "X-Request-ID"
|
|
69
67
|
},
|
|
70
68
|
pagination: { autoPageDelayMs: 0 },
|
|
@@ -2044,11 +2042,11 @@ function inferVerb(method, path) {
|
|
|
2044
2042
|
* Uses the last non-param segment (the resource being operated on).
|
|
2045
2043
|
*
|
|
2046
2044
|
* Examples:
|
|
2047
|
-
* /
|
|
2048
|
-
* /
|
|
2049
|
-
* /
|
|
2045
|
+
* /accounts/users → "Users"
|
|
2046
|
+
* /accounts/users/{id} → "User" (singular for item operations)
|
|
2047
|
+
* /accounts/users/{id}/auth_factors → "AuthFactors"
|
|
2050
2048
|
* /organizations/{id}/api_keys → "ApiKeys"
|
|
2051
|
-
* /
|
|
2049
|
+
* /auth/authorize → "Authorize" (action endpoint)
|
|
2052
2050
|
*/
|
|
2053
2051
|
function inferResourceFromPath(path) {
|
|
2054
2052
|
const segments = path.split("/").filter(Boolean);
|
|
@@ -2355,6 +2353,10 @@ function extractRequestBody(body, op, inlineModels) {
|
|
|
2355
2353
|
return {};
|
|
2356
2354
|
}
|
|
2357
2355
|
const contextName = cleanSchemaName(op?.operationId ? toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Request" : "RequestBody");
|
|
2356
|
+
if (schema.oneOf && schema.discriminator) {
|
|
2357
|
+
const merged = flattenDiscriminatedUnionBody(schema);
|
|
2358
|
+
if (merged) schema = merged;
|
|
2359
|
+
}
|
|
2358
2360
|
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
2359
2361
|
const requiredSet = new Set(schema.required ?? []);
|
|
2360
2362
|
const fields = [];
|
|
@@ -2420,6 +2422,67 @@ function extractRequestBody(body, op, inlineModels) {
|
|
|
2420
2422
|
encoding
|
|
2421
2423
|
};
|
|
2422
2424
|
}
|
|
2425
|
+
/**
|
|
2426
|
+
* Fold a discriminated-union request body into the equivalent single object so
|
|
2427
|
+
* its fields can be spread into method arguments instead of forcing callers to
|
|
2428
|
+
* construct an opaque union.
|
|
2429
|
+
*
|
|
2430
|
+
* The result is the union of every variant's properties: the discriminator is
|
|
2431
|
+
* collapsed to an enum of the pinned const values, and any field that isn't
|
|
2432
|
+
* present-and-required in *every* variant becomes optional (it varies by
|
|
2433
|
+
* branch, so it can't be unconditionally required). The discriminator itself is
|
|
2434
|
+
* always required. This mirrors what a spec author would write by hand to model
|
|
2435
|
+
* the same shape as a flat object.
|
|
2436
|
+
*
|
|
2437
|
+
* Returns null — leaving the union intact — unless the body is a clean
|
|
2438
|
+
* inline-object discriminated union: a variant that is a `$ref`, a non-object,
|
|
2439
|
+
* or that doesn't pin the discriminator to a string const can't be safely
|
|
2440
|
+
* merged. Gating on an *explicit* `discriminator` keeps undiscriminated `oneOf`
|
|
2441
|
+
* bodies untouched.
|
|
2442
|
+
*/
|
|
2443
|
+
function flattenDiscriminatedUnionBody(schema) {
|
|
2444
|
+
const discProp = schema.discriminator?.propertyName;
|
|
2445
|
+
if (!discProp || !Array.isArray(schema.oneOf) || schema.oneOf.length === 0) return null;
|
|
2446
|
+
const variants = schema.oneOf;
|
|
2447
|
+
const discValues = [];
|
|
2448
|
+
let discPropSchema;
|
|
2449
|
+
for (const variant of variants) {
|
|
2450
|
+
if (variant.$ref || !variant.properties || !(variant.type === "object" || variant.type === void 0)) return null;
|
|
2451
|
+
const dp = variant.properties[discProp];
|
|
2452
|
+
const constVal = typeof dp?.const === "string" ? dp.const : Array.isArray(dp?.enum) && dp.enum.length === 1 && typeof dp.enum[0] === "string" ? dp.enum[0] : void 0;
|
|
2453
|
+
if (constVal === void 0) return null;
|
|
2454
|
+
if (!discValues.includes(constVal)) discValues.push(constVal);
|
|
2455
|
+
if (!discPropSchema) discPropSchema = dp;
|
|
2456
|
+
}
|
|
2457
|
+
const variantCount = variants.length;
|
|
2458
|
+
const mergedProps = {};
|
|
2459
|
+
const requiredCounts = {};
|
|
2460
|
+
const order = [discProp];
|
|
2461
|
+
for (const variant of variants) {
|
|
2462
|
+
const required = new Set(variant.required ?? []);
|
|
2463
|
+
for (const [name, propSchema] of Object.entries(variant.properties)) {
|
|
2464
|
+
if (name === discProp || !propSchema) continue;
|
|
2465
|
+
if (!(name in mergedProps)) {
|
|
2466
|
+
mergedProps[name] = propSchema;
|
|
2467
|
+
order.push(name);
|
|
2468
|
+
}
|
|
2469
|
+
if (required.has(name)) requiredCounts[name] = (requiredCounts[name] ?? 0) + 1;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
mergedProps[discProp] = {
|
|
2473
|
+
type: "string",
|
|
2474
|
+
enum: discValues,
|
|
2475
|
+
...discPropSchema?.description ? { description: discPropSchema.description } : {}
|
|
2476
|
+
};
|
|
2477
|
+
const properties = {};
|
|
2478
|
+
for (const name of order) properties[name] = mergedProps[name];
|
|
2479
|
+
return {
|
|
2480
|
+
type: "object",
|
|
2481
|
+
properties,
|
|
2482
|
+
required: order.filter((name) => name === discProp || requiredCounts[name] === variantCount),
|
|
2483
|
+
...schema.description ? { description: schema.description } : {}
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2423
2486
|
/** Derive a unique variant name for a oneOf inline model. */
|
|
2424
2487
|
function deriveOneOfVariantName(variant, baseName, existingModels) {
|
|
2425
2488
|
if (variant.properties) {
|
|
@@ -3330,12 +3393,12 @@ function extractKotlinClassMembers(classBody, source) {
|
|
|
3330
3393
|
}
|
|
3331
3394
|
/**
|
|
3332
3395
|
* KDoc signature that identifies a generator-emitted service accessor inside
|
|
3333
|
-
*
|
|
3396
|
+
* a Kotlin client class. The Kotlin client emitter writes every accessor with this
|
|
3334
3397
|
* exact doc preamble — when the existing file carries a member with this doc
|
|
3335
3398
|
* but the regenerated content omits it, the member is stale (its mount target
|
|
3336
3399
|
* was remapped or removed) and gets pruned.
|
|
3337
3400
|
*/
|
|
3338
|
-
const KOTLIN_MANAGED_ACCESSOR_DOC = /\/\*\*[\s\S]*?Lazily-constructed \[[^\]]+\] accessor for this \[
|
|
3401
|
+
const KOTLIN_MANAGED_ACCESSOR_DOC = /\/\*\*[\s\S]*?Lazily-constructed \[[^\]]+\] accessor for this \[[^\]]+\] client\.[\s\S]*?\*\//;
|
|
3339
3402
|
const kotlinMergeAdapter = {
|
|
3340
3403
|
language: "kotlin",
|
|
3341
3404
|
grammarModule: "tree-sitter-kotlin",
|
|
@@ -3588,7 +3651,6 @@ const nodeMergeAdapter = {
|
|
|
3588
3651
|
const newText = newMembers.map((m) => m.text).join("\n");
|
|
3589
3652
|
for (const match of newText.matchAll(/this\.(\w+)/g)) {
|
|
3590
3653
|
const propName = match[1];
|
|
3591
|
-
if (propName === "workos") continue;
|
|
3592
3654
|
if (propName && !existingMemberKeys.has(propName)) return true;
|
|
3593
3655
|
}
|
|
3594
3656
|
return false;
|
|
@@ -5719,6 +5781,15 @@ function resolveResponseModelName(op) {
|
|
|
5719
5781
|
//#endregion
|
|
5720
5782
|
//#region src/engine/generate-files.ts
|
|
5721
5783
|
function buildEmitterContext(spec, options) {
|
|
5784
|
+
const resolvedOperations = resolveOperations(spec, options.operationHints, options.mountRules);
|
|
5785
|
+
let scopedModelNames;
|
|
5786
|
+
let scopedEnumNames;
|
|
5787
|
+
if (options.scopedServices && options.scopedServices.size > 0) {
|
|
5788
|
+
const selected = options.scopedServices;
|
|
5789
|
+
const reachable = collectReferencedNames(spec.services.filter((s) => resolvedOperations.some((r) => r.service.name === s.name && selected.has(r.mountOn))), spec.models, { preserveAllDiscriminated: false });
|
|
5790
|
+
scopedModelNames = reachable.models;
|
|
5791
|
+
scopedEnumNames = reachable.enums;
|
|
5792
|
+
}
|
|
5722
5793
|
return {
|
|
5723
5794
|
namespace: toSnakeCase(options.namespace),
|
|
5724
5795
|
namespacePascal: options.namespace,
|
|
@@ -5726,11 +5797,14 @@ function buildEmitterContext(spec, options) {
|
|
|
5726
5797
|
outputDir: options.outputDir,
|
|
5727
5798
|
apiSurface: options.apiSurface,
|
|
5728
5799
|
overlayLookup: options.overlayLookup,
|
|
5729
|
-
resolvedOperations
|
|
5800
|
+
resolvedOperations,
|
|
5730
5801
|
modelHints: options.modelHints,
|
|
5731
5802
|
emitterOptions: options.emitterOptions,
|
|
5732
5803
|
targetDir: options.target,
|
|
5733
|
-
priorTargetManifestPaths: options.priorTargetManifestPaths
|
|
5804
|
+
priorTargetManifestPaths: options.priorTargetManifestPaths,
|
|
5805
|
+
scopedServices: options.scopedServices,
|
|
5806
|
+
scopedModelNames,
|
|
5807
|
+
scopedEnumNames
|
|
5734
5808
|
};
|
|
5735
5809
|
}
|
|
5736
5810
|
/**
|
|
@@ -5738,7 +5812,8 @@ function buildEmitterContext(spec, options) {
|
|
|
5738
5812
|
* Walks operation parameters, request bodies, responses, and pagination item
|
|
5739
5813
|
* types, then chases model field references until the set stabilizes.
|
|
5740
5814
|
*/
|
|
5741
|
-
function collectReferencedNames(services, models) {
|
|
5815
|
+
function collectReferencedNames(services, models, opts = {}) {
|
|
5816
|
+
const preserveAllDiscriminated = opts.preserveAllDiscriminated !== false;
|
|
5742
5817
|
const referencedModels = /* @__PURE__ */ new Set();
|
|
5743
5818
|
const referencedEnums = /* @__PURE__ */ new Set();
|
|
5744
5819
|
const collectFromTypeRef = (ref) => {
|
|
@@ -5760,7 +5835,7 @@ function collectReferencedNames(services, models) {
|
|
|
5760
5835
|
for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
|
|
5761
5836
|
if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
|
|
5762
5837
|
}
|
|
5763
|
-
for (const model of models) {
|
|
5838
|
+
if (preserveAllDiscriminated) for (const model of models) {
|
|
5764
5839
|
if (isDiscriminatedModel(model)) referencedModels.add(model.name);
|
|
5765
5840
|
const disc = model.discriminator;
|
|
5766
5841
|
if (disc?.mapping) for (const variantName of Object.values(disc.mapping)) referencedModels.add(variantName);
|
|
@@ -5774,6 +5849,13 @@ function collectReferencedNames(services, models) {
|
|
|
5774
5849
|
visited.add(name);
|
|
5775
5850
|
const model = modelsByName.get(name);
|
|
5776
5851
|
if (!model) continue;
|
|
5852
|
+
const disc = model.discriminator;
|
|
5853
|
+
if (disc?.mapping) {
|
|
5854
|
+
for (const variantName of Object.values(disc.mapping)) if (!referencedModels.has(variantName)) {
|
|
5855
|
+
referencedModels.add(variantName);
|
|
5856
|
+
queue.push(variantName);
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5777
5859
|
for (const field of model.fields) {
|
|
5778
5860
|
collectFromTypeRef(field.type);
|
|
5779
5861
|
for (const m of referencedModels) if (!visited.has(m)) queue.push(m);
|
|
@@ -5787,7 +5869,16 @@ function collectReferencedNames(services, models) {
|
|
|
5787
5869
|
function isDiscriminatedModel(model) {
|
|
5788
5870
|
return model.fields.some((field) => (field.name === "event" || field.name === "type" || field.name === "object") && field.type.kind === "literal");
|
|
5789
5871
|
}
|
|
5790
|
-
/**
|
|
5872
|
+
/**
|
|
5873
|
+
* Collect all generated files from an emitter (no headers, no path prefixes).
|
|
5874
|
+
*
|
|
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.
|
|
5881
|
+
*/
|
|
5791
5882
|
function generateAllFiles(spec, emitter, ctx) {
|
|
5792
5883
|
const referenced = collectReferencedNames(spec.services, spec.models);
|
|
5793
5884
|
const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
|
|
@@ -5828,6 +5919,47 @@ function generateFiles(spec, emitter, options) {
|
|
|
5828
5919
|
};
|
|
5829
5920
|
}
|
|
5830
5921
|
//#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
|
|
5831
5963
|
//#region src/engine/integrate.ts
|
|
5832
5964
|
/**
|
|
5833
5965
|
* Resolve a relative import path against a source file path to get the
|
|
@@ -5944,7 +6076,7 @@ function mapFilesForTargetIntegration(files, language) {
|
|
|
5944
6076
|
* that would shadow an existing module file (e.g., `api_keys.py`), move the
|
|
5945
6077
|
* module file's content into `_compat.py` inside the package and add a
|
|
5946
6078
|
* re-export from `__init__.py`. This preserves backward-compatible imports
|
|
5947
|
-
* (e.g., `from
|
|
6079
|
+
* (e.g., `from acme.api_keys import ApiKeysModule`) while the package directory
|
|
5948
6080
|
* provides the generated models and resource classes.
|
|
5949
6081
|
*/
|
|
5950
6082
|
async function resolveFileToDirectoryConflicts(files, targetDir) {
|
|
@@ -6110,6 +6242,35 @@ function computeStalePaths(prev, currentPaths) {
|
|
|
6110
6242
|
return prev.files.filter((p) => !current.has(p)).sort();
|
|
6111
6243
|
}
|
|
6112
6244
|
/**
|
|
6245
|
+
* Merge a scoped run's emitted records into the prior manifest's records.
|
|
6246
|
+
*
|
|
6247
|
+
* A scoped (`--services`) run only emits files for the selected services, but it
|
|
6248
|
+
* must NOT discard the prior manifest's record of unselected services' files
|
|
6249
|
+
* records. The result unions prior + scoped file paths and shallow-merges the
|
|
6250
|
+
* operations maps, with scoped operations overriding prior entries for the same
|
|
6251
|
+
* "METHOD /path" key (a re-emission updates that operation's record).
|
|
6252
|
+
*
|
|
6253
|
+
* When `prev` is null (the first-ever run is scoped) there is nothing to
|
|
6254
|
+
* preserve, so the scoped subset is returned unchanged — matching the
|
|
6255
|
+
* first-adoption behavior of a full run.
|
|
6256
|
+
*/
|
|
6257
|
+
function mergeScopedManifestRecords(prev, scopedFiles, scopedOperations) {
|
|
6258
|
+
const scopedFileList = [...scopedFiles];
|
|
6259
|
+
if (!prev) return {
|
|
6260
|
+
files: scopedFileList,
|
|
6261
|
+
operations: scopedOperations
|
|
6262
|
+
};
|
|
6263
|
+
const files = [...new Set([...prev.files, ...scopedFileList])].sort();
|
|
6264
|
+
const priorOps = prev.operations;
|
|
6265
|
+
return {
|
|
6266
|
+
files,
|
|
6267
|
+
operations: priorOps || scopedOperations ? {
|
|
6268
|
+
...priorOps,
|
|
6269
|
+
...scopedOperations
|
|
6270
|
+
} : void 0
|
|
6271
|
+
};
|
|
6272
|
+
}
|
|
6273
|
+
/**
|
|
6113
6274
|
* Delete stale files from `dir`.
|
|
6114
6275
|
*
|
|
6115
6276
|
* When `header` is provided, a file is only deleted if its contents start with
|
|
@@ -6178,12 +6339,17 @@ async function removeEmptyParents(startDir, stopDir) {
|
|
|
6178
6339
|
//#endregion
|
|
6179
6340
|
//#region src/engine/orchestrator.ts
|
|
6180
6341
|
async function generate(spec, emitter, options) {
|
|
6181
|
-
const
|
|
6182
|
-
const
|
|
6342
|
+
const scopedServices = options.services && options.services.length > 0 ? resolveScopedServices(spec, options.services, options.mountRules) : void 0;
|
|
6343
|
+
const scoped = scopedServices !== void 0;
|
|
6344
|
+
const noPrune = options.noPrune === true || scoped;
|
|
6345
|
+
const skipManifestRead = options.noPrune === true && !scoped;
|
|
6346
|
+
const outputPrevManifestForCtx = skipManifestRead ? null : await readManifest(options.outputDir);
|
|
6347
|
+
const targetManifestForCtx = options.target && !skipManifestRead ? await readManifest(options.target) : null;
|
|
6183
6348
|
const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : outputPrevManifestForCtx ? new Set(outputPrevManifestForCtx.files) : void 0;
|
|
6184
6349
|
const { files: withHeaders, header, operations } = generateFiles(spec, emitter, {
|
|
6185
6350
|
...options,
|
|
6186
|
-
priorTargetManifestPaths
|
|
6351
|
+
priorTargetManifestPaths,
|
|
6352
|
+
scopedServices
|
|
6187
6353
|
});
|
|
6188
6354
|
if (options.dryRun) {
|
|
6189
6355
|
if (options.target) {
|
|
@@ -6207,8 +6373,9 @@ async function generate(spec, emitter, options) {
|
|
|
6207
6373
|
currentPaths: outputEmittedPaths,
|
|
6208
6374
|
language: emitter.language,
|
|
6209
6375
|
header,
|
|
6210
|
-
noPrune
|
|
6211
|
-
operations
|
|
6376
|
+
noPrune,
|
|
6377
|
+
operations,
|
|
6378
|
+
scoped
|
|
6212
6379
|
});
|
|
6213
6380
|
const allOutputFiles = [...writeResult.written, ...writeResult.merged];
|
|
6214
6381
|
if (allOutputFiles.length > 0) await formatTargetFiles(emitter, options.outputDir, allOutputFiles);
|
|
@@ -6230,8 +6397,9 @@ async function generate(spec, emitter, options) {
|
|
|
6230
6397
|
currentPaths: targetResult.emittedPaths,
|
|
6231
6398
|
language: emitter.language,
|
|
6232
6399
|
header,
|
|
6233
|
-
noPrune
|
|
6234
|
-
operations
|
|
6400
|
+
noPrune,
|
|
6401
|
+
operations,
|
|
6402
|
+
scoped
|
|
6235
6403
|
});
|
|
6236
6404
|
const allTargetFiles = [
|
|
6237
6405
|
...targetResult.written,
|
|
@@ -6247,11 +6415,15 @@ async function generate(spec, emitter, options) {
|
|
|
6247
6415
|
* then write the fresh manifest. Safe on first adoption (no previous manifest).
|
|
6248
6416
|
*/
|
|
6249
6417
|
async function applyManifestPrune(opts) {
|
|
6250
|
-
const
|
|
6251
|
-
language: opts.language,
|
|
6418
|
+
const records = opts.scoped ? mergeScopedManifestRecords(opts.prevManifest, opts.currentPaths, opts.operations) : {
|
|
6252
6419
|
files: opts.currentPaths,
|
|
6253
6420
|
operations: opts.operations
|
|
6254
6421
|
};
|
|
6422
|
+
const manifestOpts = {
|
|
6423
|
+
language: opts.language,
|
|
6424
|
+
files: records.files,
|
|
6425
|
+
operations: records.operations
|
|
6426
|
+
};
|
|
6255
6427
|
if (opts.noPrune) {
|
|
6256
6428
|
await writeManifest(opts.dir, manifestOpts);
|
|
6257
6429
|
return;
|
|
@@ -6891,7 +7063,7 @@ function loadOverlayContext(opts) {
|
|
|
6891
7063
|
//#region src/utils/expand-doc-urls.ts
|
|
6892
7064
|
/**
|
|
6893
7065
|
* Walk all description fields in the IR and expand relative URL paths
|
|
6894
|
-
* (e.g. `/reference/
|
|
7066
|
+
* (e.g. `/reference/users/user`) into full URLs using the given base.
|
|
6895
7067
|
*
|
|
6896
7068
|
* Matches markdown-style links: `[text](/path)` → `[text](https://example.com/path)`
|
|
6897
7069
|
* and bare parenthetical refs: `(/path)` → `(https://example.com/path)`
|
|
@@ -6954,6 +7126,7 @@ async function generateCommand(opts) {
|
|
|
6954
7126
|
if (opts.docUrl) ir = expandDocUrls(ir, opts.docUrl);
|
|
6955
7127
|
const emitter = getEmitter(opts.lang);
|
|
6956
7128
|
const namespace = opts.namespace ?? ir.name;
|
|
7129
|
+
const services = (Array.isArray(opts.services) ? opts.services : opts.services?.split(","))?.map((s) => s.trim()).filter(Boolean);
|
|
6957
7130
|
let apiSurface;
|
|
6958
7131
|
let overlayLookup;
|
|
6959
7132
|
if (opts.apiSurface && opts.compatCheck !== false) {
|
|
@@ -6977,7 +7150,8 @@ async function generateCommand(opts) {
|
|
|
6977
7150
|
mountRules: opts.mountRules,
|
|
6978
7151
|
modelHints: opts.modelHints,
|
|
6979
7152
|
emitterOptions: opts.emitterOptions,
|
|
6980
|
-
noPrune: opts.prune === false
|
|
7153
|
+
noPrune: opts.prune === false,
|
|
7154
|
+
services: services && services.length > 0 ? services : void 0
|
|
6981
7155
|
});
|
|
6982
7156
|
if (opts.dryRun) for (const f of files) console.log(f.path);
|
|
6983
7157
|
else {
|
|
@@ -8240,7 +8414,7 @@ function specDerivedFieldPaths(spec, hints) {
|
|
|
8240
8414
|
return paths;
|
|
8241
8415
|
}
|
|
8242
8416
|
/**
|
|
8243
|
-
* Compute the set of method paths (e.g., "
|
|
8417
|
+
* Compute the set of method paths (e.g., "Auth.authorize") that are defined
|
|
8244
8418
|
* as operations in the OpenAPI spec's services. Used by filterSurface to
|
|
8245
8419
|
* exclude hand-written SDK methods that don't correspond to spec operations.
|
|
8246
8420
|
*/
|
|
@@ -8276,7 +8450,7 @@ function filterRecord(record, allowed) {
|
|
|
8276
8450
|
*
|
|
8277
8451
|
* For classes: when methodPaths is provided, only keeps methods whose names
|
|
8278
8452
|
* appear in the spec-derived method paths. This prevents false positives from
|
|
8279
|
-
* hand-written SDK methods (e.g.,
|
|
8453
|
+
* hand-written SDK methods (e.g., Auth.authorization_url, Webhooks.construct_event)
|
|
8280
8454
|
* that don't correspond to spec operations. Properties on service classes are
|
|
8281
8455
|
* also filtered: only UPPER_CASE constants that are spec-derivable are kept.
|
|
8282
8456
|
*/
|
|
@@ -9955,7 +10129,7 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
|
|
|
9955
10129
|
transformSpec: configTransformSpec
|
|
9956
10130
|
});
|
|
9957
10131
|
});
|
|
9958
|
-
program.command("generate").description("Run a registered emitter against an OpenAPI spec").option("--spec <path>", "Path to OpenAPI spec file (or set OPENAPI_SPEC_PATH)").requiredOption("--lang <language>", "Target language").requiredOption("--output <dir>", "Output directory").option("--target <dir>", "Target directory for live SDK integration (merged output)").option("--namespace <name>", "SDK namespace/package name").option("--dry-run", "Preview files without writing").option("--api-surface <path>", "Path to baseline API surface JSON for compat overlay").option("--no-compat-check", "Skip compat overlay even if --api-surface is provided").option("--no-prune", "Skip deletion of stale files recorded in the previous .oagen-manifest.json").action((opts) => {
|
|
10132
|
+
program.command("generate").description("Run a registered emitter against an OpenAPI spec").option("--spec <path>", "Path to OpenAPI spec file (or set OPENAPI_SPEC_PATH)").requiredOption("--lang <language>", "Target language").requiredOption("--output <dir>", "Output directory").option("--target <dir>", "Target directory for live SDK integration (merged output)").option("--namespace <name>", "SDK namespace/package name").option("--dry-run", "Preview files without writing").option("--api-surface <path>", "Path to baseline API surface JSON for compat overlay").option("--no-compat-check", "Skip compat overlay even if --api-surface is provided").option("--no-prune", "Skip deletion of stale files recorded in the previous .oagen-manifest.json").option("--services <list>", "Comma-separated post-mount service names to generate (default: all)").action((opts) => {
|
|
9959
10133
|
opts.spec ??= process.env.OPENAPI_SPEC_PATH;
|
|
9960
10134
|
if (!opts.spec) {
|
|
9961
10135
|
console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");
|