@mingfuyun/star-allinone 0.2.0 → 0.3.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.
Files changed (31) hide show
  1. package/README.md +17 -10
  2. package/examples/AGENTS.md +26 -0
  3. package/examples/projects/starlink-frontend.star-allinone.json +7 -0
  4. package/examples/projects/starscope-frontend.star-allinone.json +7 -0
  5. package/examples/skills/custom-domain-invariants/SKILL.md +16 -0
  6. package/package.json +5 -3
  7. package/scripts/cli.js +52 -13
  8. package/scripts/gen/fixtures/run.js +104 -0
  9. package/scripts/gen/fixtures/widgets.openapi.json +194 -0
  10. package/scripts/gen/names.js +117 -0
  11. package/scripts/gen/page.js +483 -0
  12. package/scripts/gen/project.js +154 -0
  13. package/scripts/gen/service.js +264 -0
  14. package/scripts/gen/spec.js +220 -0
  15. package/scripts/gen/ts.js +121 -0
  16. package/skills/README.md +45 -188
  17. package/skills/shared/api-to-service/SKILL.md +18 -10
  18. package/skills/shared/{api-mock/SKILL.md → api-to-service/references/mock.md} +3 -10
  19. package/skills/shared/design-to-page/SKILL.md +45 -6
  20. package/skills/shared/design-to-page/references/list-page-template.md +44 -0
  21. package/skills/shared/design-to-page/references/page-conventions.md +1 -1
  22. package/skills/shared/i-have-adhd/{SKILL.md → i-have-adhd.md} +0 -10
  23. package/skills/shared/project-conventions/references/architecture.md +3 -0
  24. package/skills/shared/publish-test/SKILL.md +4 -2
  25. package/skills/shared/review-and-submit/SKILL.md +8 -16
  26. package/skills/shared/{ai-review/SKILL.md → review-and-submit/references/ai-review.md} +4 -11
  27. package/skills/shared/{commit-current-changes/SKILL.md → review-and-submit/references/commit.md} +4 -12
  28. package/skills/shared/{impact-analysis/SKILL.md → review-and-submit/references/impact-analysis.md} +1 -10
  29. package/skills/shared/{verify/SKILL.md → review-and-submit/references/verify.md} +4 -11
  30. package/skills/shared/project-conventions/SKILL.md +0 -34
  31. /package/skills/shared/{api-mock → api-to-service}/references/mock-conventions.md +0 -0
@@ -0,0 +1,154 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function readJson(file) {
5
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
6
+ }
7
+
8
+ function findProjectRoot(start) {
9
+ let current = path.resolve(start);
10
+ while (true) {
11
+ if (fs.existsSync(path.join(current, '.star-allinone.json'))) return current;
12
+ const parent = path.dirname(current);
13
+ if (parent === current) {
14
+ throw new Error('Missing .star-allinone.json. Run this command from a business project root.');
15
+ }
16
+ current = parent;
17
+ }
18
+ }
19
+
20
+ function loadProject(cwd, options = {}) {
21
+ const root = findProjectRoot(cwd);
22
+ const config = readJson(path.join(root, '.star-allinone.json'));
23
+ const specPath = options.spec || config.openapi
24
+ ? path.resolve(root, options.spec || config.openapi)
25
+ : '';
26
+ if (options.requireSpec !== false && !specPath) {
27
+ throw new Error('Set openapi in .star-allinone.json or pass --spec <file>.');
28
+ }
29
+ if (specPath && !fs.existsSync(specPath)) {
30
+ throw new Error(`OpenAPI file not found: ${specPath}`);
31
+ }
32
+ return {
33
+ root,
34
+ config,
35
+ specPath,
36
+ spec: specPath ? readJson(specPath) : null,
37
+ serviceDir: path.resolve(root, config.serviceDir || 'src/services'),
38
+ dryRun: Boolean(options.dryRun),
39
+ };
40
+ }
41
+
42
+ function listDomainDirs(serviceDir) {
43
+ if (!fs.existsSync(serviceDir)) return [];
44
+ return fs.readdirSync(serviceDir, { withFileTypes: true })
45
+ .filter((entry) => entry.isDirectory())
46
+ .map((entry) => entry.name);
47
+ }
48
+
49
+ function existingRequestFile(serviceDir) {
50
+ for (const name of ['request.tsx', 'request.ts']) {
51
+ const file = path.join(serviceDir, name);
52
+ if (fs.existsSync(file)) return name.replace(/\.(tsx|ts)$/, '');
53
+ }
54
+ return 'request';
55
+ }
56
+
57
+ function writeFile(file, content, dryRun) {
58
+ if (dryRun) {
59
+ console.log(`[dry-run] write ${file}`);
60
+ return;
61
+ }
62
+ fs.mkdirSync(path.dirname(file), { recursive: true });
63
+ fs.writeFileSync(file, content.endsWith('\n') ? content : `${content}\n`);
64
+ }
65
+
66
+ function typeExists(content, typeName) {
67
+ return new RegExp(`\\btype\\s+${typeName}\\b`).test(content);
68
+ }
69
+
70
+ function insertTypes(content, namespace, blocks) {
71
+ if (!blocks.length) return content;
72
+ const nsRe = new RegExp(`declare namespace ${namespace}\\s*\\{`);
73
+ if (!nsRe.test(content)) {
74
+ const extra = `declare namespace ${namespace} {\n${blocks.join('\n\n')}\n}\n`;
75
+ return content.trim() ? `${content.trim()}\n\n${extra}` : extra;
76
+ }
77
+ const lastBrace = content.lastIndexOf('}');
78
+ if (lastBrace === -1) throw new Error(`Cannot insert types into ${namespace}`);
79
+ return `${content.slice(0, lastBrace).replace(/\s*$/, '\n')}${blocks.join('\n\n')}\n${content.slice(lastBrace)}`;
80
+ }
81
+
82
+ function insertMethods(content, methods, exportNames, requestImport) {
83
+ let next = content;
84
+ if (!next.includes("from '../request'") && !next.includes('from "../request"')) {
85
+ next = `import MFRequest from '../${requestImport}';\n${next}`;
86
+ }
87
+ const exportMatch = next.match(/export default \{[\s\S]*?\};?/);
88
+ if (!exportMatch) {
89
+ return `${next.trim()}\n\n${methods.join('\n\n')}\n\nexport default {\n${exportNames.map((name) => ` ${name},`).join('\n')}\n};\n`;
90
+ }
91
+ const insertAt = exportMatch.index;
92
+ const before = next.slice(0, insertAt).replace(/\s*$/, '\n\n');
93
+ const exportBlock = exportMatch[0];
94
+ const namesToAdd = exportNames.filter((name) => !new RegExp(`\\b${name}\\b`).test(exportBlock));
95
+ let updatedExport = exportBlock;
96
+ if (namesToAdd.length) {
97
+ updatedExport = exportBlock.replace(
98
+ /export default \{/,
99
+ `export default {\n${namesToAdd.map((name) => ` ${name},`).join('\n')}`,
100
+ );
101
+ }
102
+ return `${before}${methods.join('\n\n')}\n\n${updatedExport}${next.slice(insertAt + exportBlock.length)}`;
103
+ }
104
+
105
+ function patchServiceIndex(indexFile, domain, serviceFile, dryRun) {
106
+ const importName = `${toCamelSafe(domain)}API`;
107
+ const importLine = `import ${importName} from './${domain}/${serviceFile}';`;
108
+ const spread = `...${importName},`;
109
+ if (!fs.existsSync(indexFile)) {
110
+ writeFile(
111
+ indexFile,
112
+ `${importLine}\n\nexport default {\n ${spread}\n};\n`,
113
+ dryRun,
114
+ );
115
+ return;
116
+ }
117
+ const current = fs.readFileSync(indexFile, 'utf8');
118
+ if (current.includes(`'./${domain}/${serviceFile}'`) || current.includes(`"./${domain}/${serviceFile}"`)) {
119
+ return;
120
+ }
121
+ let next = `${importLine}\n${current}`;
122
+ if (/export default \{/.test(next) && !next.includes(spread)) {
123
+ next = next.replace(/export default \{/, `export default {\n ${spread}`);
124
+ }
125
+ writeFile(indexFile, next, dryRun);
126
+ }
127
+
128
+ function toCamelSafe(domain) {
129
+ return domain.charAt(0).toLowerCase() + domain.slice(1);
130
+ }
131
+
132
+ function patchRootTypings(root, domain, dryRun) {
133
+ const file = path.join(root, 'typings.d.ts');
134
+ if (!fs.existsSync(file)) return;
135
+ const ref = `/// <reference path="./src/services/${domain}/typings.d.ts" />`;
136
+ const current = fs.readFileSync(file, 'utf8');
137
+ if (current.includes(`src/services/${domain}/typings.d.ts`)) return;
138
+ if (!current.includes('/// <reference path=')) return;
139
+ writeFile(file, `${ref}\n${current}`, dryRun);
140
+ }
141
+
142
+ module.exports = {
143
+ readJson,
144
+ findProjectRoot,
145
+ loadProject,
146
+ listDomainDirs,
147
+ existingRequestFile,
148
+ writeFile,
149
+ typeExists,
150
+ insertTypes,
151
+ insertMethods,
152
+ patchServiceIndex,
153
+ patchRootTypings,
154
+ };
@@ -0,0 +1,264 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const {
4
+ toPascal,
5
+ toNamespace,
6
+ toServiceFile,
7
+ pathToTemplate,
8
+ pathParamsFromPath,
9
+ parseOperationSelector,
10
+ functionName,
11
+ requestHelper,
12
+ domainFromPath,
13
+ typeName,
14
+ } = require('./names');
15
+ const { findOperation, deref, refName, exampleFromSchema } = require('./spec');
16
+ const { collectRefs, emitNamedType, emitQueryType, paramType } = require('./ts');
17
+ const {
18
+ loadProject,
19
+ listDomainDirs,
20
+ existingRequestFile,
21
+ writeFile,
22
+ typeExists,
23
+ insertTypes,
24
+ insertMethods,
25
+ patchServiceIndex,
26
+ patchRootTypings,
27
+ } = require('./project');
28
+
29
+ function resolveDomain(operation, project, explicit) {
30
+ if (explicit) return toPascal(explicit);
31
+ const dirs = listDomainDirs(project.serviceDir);
32
+ for (const dir of dirs) {
33
+ const files = fs.readdirSync(path.join(project.serviceDir, dir));
34
+ for (const file of files.filter((name) => name.endsWith('.ts') && !name.endsWith('.d.ts'))) {
35
+ const content = fs.readFileSync(path.join(project.serviceDir, dir, file), 'utf8');
36
+ if (hasOperation(content, operation.path, operation.method)) {
37
+ return dir;
38
+ }
39
+ }
40
+ }
41
+ if (operation.tags?.[0]) return toPascal(operation.tags[0]);
42
+ return domainFromPath(operation.path);
43
+ }
44
+
45
+ function useIPrefix(typingsContent) {
46
+ if (!typingsContent) return false;
47
+ const prefixed = (typingsContent.match(/\btype I[A-Z]/g) || []).length;
48
+ const plain = (typingsContent.match(/\btype [A-Z]/g) || []).length;
49
+ return prefixed > 0 && prefixed >= plain / 2;
50
+ }
51
+
52
+ function existingNamespace(typingsContent, domain) {
53
+ const match = typingsContent?.match(/declare namespace (\w+)/);
54
+ return match?.[1] || toNamespace(domain);
55
+ }
56
+
57
+ function namedRefMap(spec, schemas, usePrefix, fallback) {
58
+ const map = new Map();
59
+ for (const ref of schemas) {
60
+ const base = refName(ref);
61
+ map.set(ref, usePrefix ? typeName(base, '', true).replace(/^II/, 'I') : toPascal(base) || fallback);
62
+ }
63
+ return map;
64
+ }
65
+
66
+ function buildArtifacts(project, operation, options) {
67
+ const fn = functionName(operation);
68
+ const domain = resolveDomain(operation, project, options.domain);
69
+ const serviceFile = toServiceFile(domain);
70
+ const domainDir = path.join(project.serviceDir, domain);
71
+ const servicePath = path.join(domainDir, `${serviceFile}.ts`);
72
+ const typingsPath = path.join(domainDir, 'typings.d.ts');
73
+ const existingService = fs.existsSync(servicePath) ? fs.readFileSync(servicePath, 'utf8') : '';
74
+ const existingTypings = fs.existsSync(typingsPath) ? fs.readFileSync(typingsPath, 'utf8') : '';
75
+ const namespace = existingNamespace(existingTypings, domain);
76
+ const prefix = useIPrefix(existingTypings);
77
+ const spec = operation.spec;
78
+ const alreadyHasPath = hasOperation(existingService, operation.path, operation.method);
79
+ if (!alreadyHasPath && new RegExp(`\\bconst ${fn}\\b`).test(existingService)) {
80
+ throw new Error(`Conflict: ${fn} already exists in ${path.relative(project.root, servicePath)} but path differs`);
81
+ }
82
+
83
+ const refSet = new Set();
84
+ [operation.dataSchema, operation.bodySchema, operation.itemSchema, ...operation.queryParams.map((item) => item.schema)]
85
+ .forEach((schema) => collectRefs(spec, schema, refSet));
86
+ const namedRefs = namedRefMap(spec, refSet, prefix, 'Entity');
87
+
88
+ const typeBlocks = [];
89
+ for (const [ref, name] of namedRefs.entries()) {
90
+ if (typeExists(existingTypings, name)) continue;
91
+ typeBlocks.push(emitNamedType(spec, name, { $ref: ref }, namedRefs));
92
+ }
93
+
94
+ const queryType = typeName(fn, 'Query', prefix);
95
+ const bodyType = typeName(fn, 'Body', prefix);
96
+ const dataType = typeName(fn, 'Data', prefix);
97
+ let responseTypeName;
98
+
99
+ const itemType = typeName(fn, 'Item', prefix);
100
+ if (operation.itemSchema && !operation.itemSchema.$ref && !typeExists(existingTypings, itemType)) {
101
+ typeBlocks.push(emitNamedType(spec, itemType, deref(spec, operation.itemSchema), namedRefs));
102
+ namedRefs.set('__item__', itemType);
103
+ } else if (operation.itemSchema?.$ref) {
104
+ namedRefs.set('__item__', namedRefs.get(operation.itemSchema.$ref));
105
+ }
106
+
107
+ if (operation.dataSchema?.$ref && namedRefs.get(operation.dataSchema.$ref)) {
108
+ responseTypeName = namedRefs.get(operation.dataSchema.$ref);
109
+ } else if (operation.dataSchema) {
110
+ if (!typeExists(existingTypings, dataType) && !typeBlocks.some((block) => block.includes(`type ${dataType} `))) {
111
+ typeBlocks.push(emitNamedType(spec, dataType, deref(spec, operation.dataSchema), namedRefs));
112
+ }
113
+ responseTypeName = dataType;
114
+ }
115
+
116
+ if (operation.queryParams.length && !typeExists(existingTypings, queryType)) {
117
+ const emitted = emitQueryType(spec, queryType, operation.queryParams, namedRefs);
118
+ if (emitted) typeBlocks.push(emitted);
119
+ }
120
+ if (operation.bodySchema && !operation.bodySchema.$ref && !typeExists(existingTypings, bodyType)) {
121
+ typeBlocks.push(emitNamedType(spec, bodyType, deref(spec, operation.bodySchema), namedRefs));
122
+ } else if (operation.bodySchema?.$ref) {
123
+ // named ref already emitted
124
+ }
125
+
126
+ const helper = requestHelper(operation);
127
+ const pathParams = operation.pathParams.length
128
+ ? operation.pathParams
129
+ : pathParamsFromPath(operation.path).map((name) => ({ name, required: true, schema: { type: 'string' } }));
130
+ const args = [];
131
+ pathParams.forEach((param) => {
132
+ args.push(`${param.name}: ${paramType(spec, param, namedRefs)}`);
133
+ });
134
+ if (operation.queryParams.length) args.push(`params: ${namespace}.${queryType}`);
135
+ else if (['get', 'delete', 'export'].includes(helper) && !pathParams.length) {
136
+ args.push(`params?: Record<string, unknown>`);
137
+ }
138
+ if (operation.bodySchema) {
139
+ const bodyName = operation.bodySchema.$ref
140
+ ? namedRefs.get(operation.bodySchema.$ref) || bodyType
141
+ : bodyType;
142
+ args.push(`params: ${namespace}.${bodyName}`);
143
+ }
144
+
145
+ const url = pathToTemplate(operation.path);
146
+ const generic = responseTypeName ? `<${namespace}.${responseTypeName}>` : '';
147
+ const callArgs = [];
148
+ callArgs.push(url);
149
+ if (operation.bodySchema || operation.queryParams.length) callArgs.push('params');
150
+ else if (['get', 'delete', 'export'].includes(helper)) callArgs.push(pathParams.length ? '{}' : 'params');
151
+
152
+ const summary = (operation.summary || fn).split('\n')[0].trim();
153
+ const methodSource = `/**\n * ${summary}\n */\nconst ${fn} = (${args.join(', ')}) => {\n return MFRequest.${helper}${generic}(${callArgs.join(', ')});\n};`;
154
+
155
+ const requestImport = existingRequestFile(project.serviceDir);
156
+ const nextTypings = insertTypes(existingTypings || `declare namespace ${namespace} {\n}\n`, namespace, typeBlocks);
157
+ const nextService = existingService
158
+ ? insertMethods(existingService, [methodSource], [fn], requestImport)
159
+ : `import MFRequest from '../${requestImport}';\n\n${methodSource}\n\nexport default {\n ${fn},\n};\n`;
160
+
161
+ const mock = {
162
+ code: 0,
163
+ message: 'success',
164
+ data: exampleFromSchema(spec, operation.dataSchema) ?? {},
165
+ };
166
+
167
+ return {
168
+ status: alreadyHasPath ? 'reuse' : 'create',
169
+ domain,
170
+ namespace,
171
+ fn,
172
+ servicePath,
173
+ typingsPath,
174
+ nextService,
175
+ nextTypings,
176
+ mock,
177
+ operation,
178
+ queryType: operation.queryParams.length ? `${namespace}.${queryType}` : undefined,
179
+ itemType: itemTypeName(operation, namedRefs, prefix, namespace, itemType),
180
+ pagination: operation.pagination,
181
+ message: alreadyHasPath
182
+ ? `Reused existing ${operation.method.toUpperCase()} ${operation.path}`
183
+ : undefined,
184
+ };
185
+ }
186
+
187
+ function pathVariantsForSearch(apiPath) {
188
+ return [
189
+ apiPath,
190
+ apiPath.replace(/\/$/, ''),
191
+ apiPath.replace(/\{([^}]+)\}/g, '${$1}'),
192
+ apiPath.replace(/\{([^}]+)\}/g, '${$1}').replace(/\/$/, ''),
193
+ ].filter(Boolean);
194
+ }
195
+
196
+ function escapeRegExp(value) {
197
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
198
+ }
199
+
200
+ function hasOperation(content, apiPath, method) {
201
+ if (!content) return false;
202
+ const helper = requestHelper({ method, path: apiPath, contentType: '' });
203
+ return pathVariantsForSearch(apiPath).some((item) => {
204
+ const pattern = new RegExp(
205
+ `MFRequest\\.${helper}(?:<[^>]+>)?\\(\\s*[\`'"]${escapeRegExp(item)}`,
206
+ );
207
+ return pattern.test(content);
208
+ });
209
+ }
210
+
211
+ function itemTypeName(operation, namedRefs, prefix, namespace, fallbackItem) {
212
+ if (namedRefs.get('__item__')) return `${namespace}.${namedRefs.get('__item__')}`;
213
+ if (!operation.itemSchema) return undefined;
214
+ return `${namespace}.${fallbackItem}`;
215
+ }
216
+
217
+ function writeMock(project, operation, mock, options) {
218
+ if (!options.mockFile) {
219
+ return { printed: true, mock };
220
+ }
221
+ const relative = `mock${operation.path.replace(/\{[^}]+\}/g, 'id')}${operation.method}.json`
222
+ .replace(/\/{2,}/g, '/');
223
+ const file = path.join(project.root, relative);
224
+ if (fs.existsSync(file) && !options.force) {
225
+ return { file, status: 'reuse' };
226
+ }
227
+ writeFile(file, `${JSON.stringify(mock, null, 2)}\n`, project.dryRun);
228
+ return { file, status: 'create' };
229
+ }
230
+
231
+ function generateService(cwd, options) {
232
+ const project = loadProject(cwd, options);
233
+ if (!project.spec) throw new Error('OpenAPI spec is required for gen service');
234
+ const selector = parseOperationSelector(options.operation);
235
+ const operation = findOperation(project.spec, selector);
236
+ const artifact = buildArtifacts(project, operation, options);
237
+ if (artifact.status === 'reuse') {
238
+ console.log(artifact.message);
239
+ return artifact;
240
+ }
241
+ writeFile(artifact.typingsPath, artifact.nextTypings, project.dryRun);
242
+ writeFile(artifact.servicePath, artifact.nextService, project.dryRun);
243
+ patchServiceIndex(
244
+ path.join(project.serviceDir, 'index.ts'),
245
+ artifact.domain,
246
+ toServiceFile(artifact.domain),
247
+ project.dryRun,
248
+ );
249
+ patchRootTypings(project.root, artifact.domain, project.dryRun);
250
+ const mockResult = writeMock(project, operation, artifact.mock, options);
251
+ console.log(`${project.dryRun ? '[dry-run] ' : ''}generated ${operation.method.toUpperCase()} ${operation.path} -> ${path.relative(project.root, artifact.servicePath)}#${artifact.fn}`);
252
+ if (mockResult.file) console.log(`${project.dryRun ? '[dry-run] ' : ''}mock ${path.relative(project.root, mockResult.file)}`);
253
+ else {
254
+ console.log(`${operation.method.toUpperCase()} ${operation.path} — success`);
255
+ console.log(JSON.stringify(artifact.mock, null, 2));
256
+ }
257
+ return { ...artifact, mockResult, operation };
258
+ }
259
+
260
+ module.exports = {
261
+ generateService,
262
+ buildArtifacts,
263
+ resolveDomain,
264
+ };
@@ -0,0 +1,220 @@
1
+ const { normalizePath, pathVariants } = require('./names');
2
+
3
+ function isOAS3(spec) {
4
+ return Boolean(spec.openapi && String(spec.openapi).startsWith('3'));
5
+ }
6
+
7
+ function pointerValue(spec, ref) {
8
+ if (!ref || !ref.startsWith('#/')) return undefined;
9
+ return ref
10
+ .slice(2)
11
+ .split('/')
12
+ .map((part) => decodeURIComponent(part.replace(/~1/g, '/').replace(/~0/g, '~')))
13
+ .reduce((value, key) => value?.[key], spec);
14
+ }
15
+
16
+ function deref(spec, schema, seen = new Set()) {
17
+ if (!schema) return schema;
18
+ if (schema.$ref) {
19
+ if (seen.has(schema.$ref)) return { type: 'object' };
20
+ const next = new Set(seen);
21
+ next.add(schema.$ref);
22
+ const resolved = pointerValue(spec, schema.$ref);
23
+ if (!resolved) return { type: 'object', description: `unresolved ${schema.$ref}` };
24
+ return deref(spec, { ...resolved, title: resolved.title || refName(schema.$ref) }, next);
25
+ }
26
+ if (Array.isArray(schema.allOf) && schema.allOf.length) {
27
+ return schema.allOf.reduce(
28
+ (merged, part) => mergeSchemas(merged, deref(spec, part, seen)),
29
+ { type: 'object', properties: {}, required: [] },
30
+ );
31
+ }
32
+ return schema;
33
+ }
34
+
35
+ function refName(ref) {
36
+ if (!ref) return 'Anonymous';
37
+ const raw = decodeURIComponent(ref.split('/').pop() || 'Anonymous');
38
+ return raw.replace(/^transport\./, '').replace(/[^\w]/g, '');
39
+ }
40
+
41
+ function mergeSchemas(left = {}, right = {}) {
42
+ return {
43
+ type: 'object',
44
+ properties: { ...(left.properties || {}), ...(right.properties || {}) },
45
+ required: [...new Set([...(left.required || []), ...(right.required || [])])],
46
+ nullable: left.nullable || right.nullable,
47
+ description: right.description || left.description,
48
+ };
49
+ }
50
+
51
+ function jsonContent(object) {
52
+ const content = object?.content || {};
53
+ return (
54
+ content['application/json'] ||
55
+ content['application/json; charset=utf-8'] ||
56
+ content['*/*'] ||
57
+ Object.values(content)[0]
58
+ );
59
+ }
60
+
61
+ function successResponse(operation) {
62
+ const responses = operation.responses || {};
63
+ return responses['200'] || responses['201'] || responses['204'] || responses.default;
64
+ }
65
+
66
+ function collectParameters(spec, pathItem, operation) {
67
+ const list = [...(pathItem.parameters || []), ...(operation.parameters || [])]
68
+ .map((item) => (item.$ref ? pointerValue(spec, item.$ref) : item))
69
+ .filter(Boolean);
70
+ const pathParams = [];
71
+ const queryParams = [];
72
+ let bodySchema;
73
+ for (const param of list) {
74
+ const schema = param.schema || param;
75
+ if (param.in === 'path') pathParams.push({ ...param, schema });
76
+ else if (param.in === 'query') queryParams.push({ ...param, schema });
77
+ else if (param.in === 'body') bodySchema = schema;
78
+ }
79
+ return { pathParams, queryParams, bodySchema };
80
+ }
81
+
82
+ function bodyFromOperation(spec, operation, swaggerBody) {
83
+ if (operation.requestBody) {
84
+ const media = jsonContent(operation.requestBody);
85
+ return {
86
+ schema: media?.schema,
87
+ contentType: Object.keys(operation.requestBody.content || {})[0] || 'application/json',
88
+ };
89
+ }
90
+ if (swaggerBody) {
91
+ return { schema: swaggerBody, contentType: 'application/json' };
92
+ }
93
+ return { schema: undefined, contentType: undefined };
94
+ }
95
+
96
+ function successSchema(spec, operation) {
97
+ const response = successResponse(operation);
98
+ if (!response) return undefined;
99
+ if (response.content) return jsonContent(response)?.schema;
100
+ return response.schema;
101
+ }
102
+
103
+ function unwrapData(spec, schema) {
104
+ const resolved = deref(spec, schema);
105
+ const properties = resolved?.properties || {};
106
+ if (properties.data && (properties.code || properties.message || properties.msg)) {
107
+ return properties.data;
108
+ }
109
+ return schema;
110
+ }
111
+
112
+ function listItemSchema(spec, dataSchema) {
113
+ const resolved = deref(spec, dataSchema);
114
+ const properties = resolved?.properties || {};
115
+ if (properties.results?.items) return properties.results.items;
116
+ if (properties.list?.items) return properties.list.items;
117
+ if (resolved?.type === 'array' && resolved.items) return resolved.items;
118
+ return undefined;
119
+ }
120
+
121
+ function paginationKind(spec, dataSchema) {
122
+ const resolved = deref(spec, dataSchema);
123
+ const properties = resolved?.properties || {};
124
+ if (properties.results && (properties.count || properties.total !== undefined)) {
125
+ return 'results-count';
126
+ }
127
+ if (properties.list && properties.total !== undefined) return 'list-total';
128
+ return 'unknown';
129
+ }
130
+
131
+ function findPathKey(spec, apiPath) {
132
+ const paths = spec.paths || {};
133
+ const variants = pathVariants(apiPath);
134
+ return Object.keys(paths).find((key) => variants.includes(normalizePath(key)));
135
+ }
136
+
137
+ function findOperation(spec, selector) {
138
+ const paths = spec.paths || {};
139
+ if (selector.operationId) {
140
+ for (const [path, pathItem] of Object.entries(paths)) {
141
+ for (const [method, operation] of Object.entries(pathItem)) {
142
+ if (['get', 'post', 'put', 'patch', 'delete'].includes(method) && operation.operationId === selector.operationId) {
143
+ return buildOperation(spec, path, method, pathItem, operation);
144
+ }
145
+ }
146
+ }
147
+ throw new Error(`operationId not found: ${selector.operationId}`);
148
+ }
149
+ if (!selector.path) throw new Error('Operation selector needs a path or operationId');
150
+ const pathKey = findPathKey(spec, selector.path);
151
+ if (!pathKey) throw new Error(`Path not found: ${selector.path}`);
152
+ const pathItem = paths[pathKey];
153
+ const methods = ['get', 'post', 'put', 'patch', 'delete'].filter((method) => pathItem[method]);
154
+ const method = (selector.method || (methods.length === 1 ? methods[0] : '')).toLowerCase();
155
+ if (!method) {
156
+ throw new Error(`Path ${pathKey} has multiple methods (${methods.join(', ')}). Pass METHOD /path.`);
157
+ }
158
+ if (!pathItem[method]) throw new Error(`${method.toUpperCase()} ${pathKey} not found`);
159
+ return buildOperation(spec, pathKey, method, pathItem, pathItem[method]);
160
+ }
161
+
162
+ function buildOperation(spec, path, method, pathItem, operation) {
163
+ const collected = collectParameters(spec, pathItem, operation);
164
+ const body = bodyFromOperation(spec, operation, collected.bodySchema);
165
+ const rawSuccess = successSchema(spec, operation);
166
+ const dataSchema = unwrapData(spec, rawSuccess);
167
+ return {
168
+ spec,
169
+ isOAS3: isOAS3(spec),
170
+ method,
171
+ path: normalizePath(path),
172
+ operationId: operation.operationId,
173
+ summary: operation.summary || operation.description || '',
174
+ tags: operation.tags || pathItem.tags || [],
175
+ pathParams: collected.pathParams,
176
+ queryParams: collected.queryParams,
177
+ bodySchema: body.schema,
178
+ contentType: body.contentType,
179
+ successSchema: rawSuccess,
180
+ dataSchema,
181
+ itemSchema: listItemSchema(spec, dataSchema),
182
+ pagination: paginationKind(spec, dataSchema),
183
+ };
184
+ }
185
+
186
+ function exampleFromSchema(spec, schema, depth = 0) {
187
+ const resolved = deref(spec, schema);
188
+ if (!resolved || depth > 6) return null;
189
+ if (resolved.example !== undefined) return resolved.example;
190
+ if (resolved.enum?.length) return resolved.enum[0];
191
+ if (resolved.type === 'array' || resolved.items) {
192
+ return [exampleFromSchema(spec, resolved.items, depth + 1)];
193
+ }
194
+ if (resolved.oneOf?.length) return exampleFromSchema(spec, resolved.oneOf[0], depth + 1);
195
+ if (resolved.anyOf?.length) return exampleFromSchema(spec, resolved.anyOf[0], depth + 1);
196
+ const type = resolved.type;
197
+ if (type === 'integer' || type === 'number') return 1;
198
+ if (type === 'boolean') return true;
199
+ if (type === 'string') {
200
+ if (resolved.format === 'date-time') return '2026-01-01T00:00:00Z';
201
+ if (resolved.format === 'date') return '2026-01-01';
202
+ return resolved.title || 'example';
203
+ }
204
+ const properties = resolved.properties || {};
205
+ const object = {};
206
+ for (const [key, value] of Object.entries(properties)) {
207
+ object[key] = exampleFromSchema(spec, value, depth + 1);
208
+ }
209
+ return object;
210
+ }
211
+
212
+ module.exports = {
213
+ isOAS3,
214
+ pointerValue,
215
+ deref,
216
+ refName,
217
+ findOperation,
218
+ exampleFromSchema,
219
+ unwrapData,
220
+ };