codz-cli 0.0.0-stage → 0.1.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 (117) hide show
  1. package/BETA.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +523 -2
  4. package/assets/gradle-wrapper/README.txt +2 -0
  5. package/assets/gradle-wrapper/gradle-wrapper.jar +0 -0
  6. package/assets/gradle-wrapper/gradlew +251 -0
  7. package/assets/gradle-wrapper/gradlew.bat +94 -0
  8. package/bin/codz.js +33 -0
  9. package/dist/adapters/android.js +321 -0
  10. package/dist/adapters/androidSetup.js +277 -0
  11. package/dist/adapters/common.js +114 -0
  12. package/dist/adapters/dartGen.js +83 -0
  13. package/dist/adapters/flutter.js +291 -0
  14. package/dist/adapters/flutterGen.js +204 -0
  15. package/dist/adapters/flutterScan.js +312 -0
  16. package/dist/adapters/flutterSetup.js +252 -0
  17. package/dist/adapters/flutterStyles.js +130 -0
  18. package/dist/adapters/generic.js +20 -0
  19. package/dist/adapters/getx.js +423 -0
  20. package/dist/adapters/index.js +28 -0
  21. package/dist/adapters/ios.js +440 -0
  22. package/dist/adapters/kotlinGen.js +110 -0
  23. package/dist/adapters/models.js +116 -0
  24. package/dist/adapters/nextjs.js +243 -0
  25. package/dist/adapters/nextjsGen.js +285 -0
  26. package/dist/adapters/nextjsScan.js +364 -0
  27. package/dist/adapters/nextjsSetup.js +119 -0
  28. package/dist/adapters/react.js +89 -0
  29. package/dist/adapters/reactNative.js +109 -0
  30. package/dist/adapters/reactNativeSetup.js +236 -0
  31. package/dist/adapters/reactQuery.js +89 -0
  32. package/dist/adapters/reactSetup.js +150 -0
  33. package/dist/adapters/source.js +165 -0
  34. package/dist/cli.js +1175 -0
  35. package/dist/core/cache.js +76 -0
  36. package/dist/core/changeset.js +404 -0
  37. package/dist/core/commands.js +239 -0
  38. package/dist/core/config.js +45 -0
  39. package/dist/core/diff.js +143 -0
  40. package/dist/core/events.js +41 -0
  41. package/dist/core/fsutil.js +122 -0
  42. package/dist/core/git.js +36 -0
  43. package/dist/core/jsonEdit.js +113 -0
  44. package/dist/core/merge.js +159 -0
  45. package/dist/core/proc.js +23 -0
  46. package/dist/core/safe.js +75 -0
  47. package/dist/core/templates.js +100 -0
  48. package/dist/core/tsAst.js +112 -0
  49. package/dist/core/types.js +12 -0
  50. package/dist/core/validateConfig.js +172 -0
  51. package/dist/core/workflow.js +72 -0
  52. package/dist/core/workspace.js +80 -0
  53. package/dist/core/yaml.js +126 -0
  54. package/dist/core/yamlDoc.js +367 -0
  55. package/dist/detector.js +95 -0
  56. package/dist/input/apiSpec.js +395 -0
  57. package/dist/input/curl.js +259 -0
  58. package/dist/input/endpoint.js +220 -0
  59. package/dist/input/har.js +120 -0
  60. package/dist/input/infer.js +174 -0
  61. package/dist/input/naming.js +145 -0
  62. package/dist/input/paste.js +130 -0
  63. package/dist/skills/androidSkeleton.js +292 -0
  64. package/dist/skills/androidTests.js +221 -0
  65. package/dist/skills/androidUi.js +561 -0
  66. package/dist/skills/androidViews.js +536 -0
  67. package/dist/skills/apiAdd.js +99 -0
  68. package/dist/skills/apiDiff.js +271 -0
  69. package/dist/skills/apiEdit.js +547 -0
  70. package/dist/skills/apiImport.js +127 -0
  71. package/dist/skills/apiList.js +35 -0
  72. package/dist/skills/apiSync.js +221 -0
  73. package/dist/skills/auth.js +1304 -0
  74. package/dist/skills/completion.js +142 -0
  75. package/dist/skills/config.js +93 -0
  76. package/dist/skills/create.js +358 -0
  77. package/dist/skills/doctor.js +304 -0
  78. package/dist/skills/doctorFix.js +214 -0
  79. package/dist/skills/explain.js +61 -0
  80. package/dist/skills/feature.js +418 -0
  81. package/dist/skills/featureEdit.js +225 -0
  82. package/dist/skills/featureLinks.js +179 -0
  83. package/dist/skills/init.js +66 -0
  84. package/dist/skills/iosUi.js +468 -0
  85. package/dist/skills/locale.js +335 -0
  86. package/dist/skills/mcp.js +175 -0
  87. package/dist/skills/mock.js +482 -0
  88. package/dist/skills/module.js +23 -0
  89. package/dist/skills/plans.js +66 -0
  90. package/dist/skills/prefill.js +84 -0
  91. package/dist/skills/reactNativeUi.js +388 -0
  92. package/dist/skills/reactUi.js +352 -0
  93. package/dist/skills/report.js +75 -0
  94. package/dist/skills/route.js +230 -0
  95. package/dist/skills/screen.js +597 -0
  96. package/dist/skills/screenForm.js +410 -0
  97. package/dist/skills/setup.js +28 -0
  98. package/dist/skills/setupDoctor.js +226 -0
  99. package/dist/skills/templatesCmd.js +32 -0
  100. package/dist/skills/testgen.js +331 -0
  101. package/dist/skills/theme.js +388 -0
  102. package/dist/starters/auth.js +144 -0
  103. package/dist/starters/commands.js +283 -0
  104. package/dist/starters/engine.js +315 -0
  105. package/dist/starters/home.js +42 -0
  106. package/dist/starters/http.js +93 -0
  107. package/dist/starters/manifest.js +285 -0
  108. package/dist/starters/paths.js +70 -0
  109. package/dist/starters/publish.js +150 -0
  110. package/dist/starters/registry.js +258 -0
  111. package/dist/starters/sign.js +72 -0
  112. package/dist/starters/source.js +98 -0
  113. package/dist/starters/strictJson.js +148 -0
  114. package/dist/starters/tar.js +180 -0
  115. package/docs/COMMANDS.md +527 -0
  116. package/examples/openapi.yaml +319 -0
  117. package/package.json +57 -4
@@ -0,0 +1,220 @@
1
+ // Builds the universal EndpointSpec from parsed input. Every rule here is
2
+ // framework-neutral; adapters turn the spec into code.
3
+ import { ToolError } from "../core/types.js";
4
+ import { inferModels, signature, unwrapEnvelope } from "./infer.js";
5
+ import { safeDecode } from "./curl.js";
6
+ import { checkHost, encodePathSegment } from "../core/safe.js";
7
+ import { camel, functionName, pascal, singular, typeName } from "./naming.js";
8
+ const VERSION_SEGMENT = /^(api|v\d+(\.\d+)?|rest)$/i;
9
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+ const OBJECT_ID = /^[0-9a-f]{24}$/i;
11
+ function isIdLike(seg) {
12
+ if (/^\d+$/.test(seg))
13
+ return 'int';
14
+ if (UUID.test(seg) || OBJECT_ID.test(seg))
15
+ return 'string';
16
+ return null;
17
+ }
18
+ export function analyzePath(rawPath, override) {
19
+ const segs = rawPath.split('/').filter(Boolean).map((s) => {
20
+ try {
21
+ return decodeURIComponent(s);
22
+ }
23
+ catch {
24
+ return s;
25
+ }
26
+ });
27
+ const overrideSegs = override ? override.split('/').filter(Boolean) : null;
28
+ if (overrideSegs && overrideSegs.length !== segs.length) {
29
+ throw new ToolError('bad_path', `--path "${override}" has ${overrideSegs.length} segments but the URL has ${segs.length}.`);
30
+ }
31
+ const params = [];
32
+ const nouns = [];
33
+ const out = [];
34
+ const paramIndexes = [];
35
+ segs.forEach((seg, i) => {
36
+ const o = overrideSegs?.[i];
37
+ const explicit = (o ?? seg).match(/^(?:\{([^/{}]+)\}|:([\w.-]+))$/);
38
+ if (explicit) {
39
+ paramIndexes.push(i);
40
+ params.push({ name: camel(explicit[1] ?? explicit[2]), example: seg, type: isIdLike(seg) ?? 'string' });
41
+ out.push(`{${camel(explicit[1] ?? explicit[2])}}`);
42
+ return;
43
+ }
44
+ if (o && o !== seg) {
45
+ throw new ToolError('bad_path', `--path segment "${o}" does not match the URL segment "${seg}".`);
46
+ }
47
+ const kind = overrideSegs ? null : isIdLike(seg);
48
+ if (kind) {
49
+ paramIndexes.push(i);
50
+ params.push({ name: '', example: seg, type: kind });
51
+ out.push('');
52
+ }
53
+ else {
54
+ // Kept percent-encoded: a literal segment never carries quotes, backticks, $ or comment ends into code.
55
+ out.push(encodePathSegment(seg));
56
+ // Names come from the words: /config.json is "config", /user-settings is "user settings".
57
+ const noun = seg.replace(/\.(json|xml|php|aspx?|html?|jsp|cgi|do)$/i, '');
58
+ if (noun && (!VERSION_SEGMENT.test(seg) || nouns.length > 0))
59
+ nouns.push(noun);
60
+ }
61
+ });
62
+ // Name inferred params: a parameter at the very end is "id"; any other is "<parent>Id".
63
+ paramIndexes.forEach((segIndex, pi) => {
64
+ const p = params[pi];
65
+ if (p.name !== '')
66
+ return;
67
+ const prev = segIndex > 0 ? segs[segIndex - 1] : 'item';
68
+ p.name = segIndex === segs.length - 1 ? 'id' : `${camel(singular(prev))}Id`;
69
+ out[segIndex] = `{${p.name}}`;
70
+ });
71
+ const names = new Set();
72
+ for (const p of params) {
73
+ if (names.has(p.name))
74
+ throw new ToolError('bad_path', `Two path parameters are both called "${p.name}". Use --path to name them.`);
75
+ names.add(p.name);
76
+ }
77
+ return { path: '/' + out.join('/'), params, nouns, endsWithParam: paramIndexes.length > 0 && paramIndexes[paramIndexes.length - 1] === segs.length - 1 };
78
+ }
79
+ export function deriveName(method, a) {
80
+ const nouns = a.nouns.length ? a.nouns : ['root'];
81
+ const last = nouns[nouns.length - 1];
82
+ const parents = nouns.slice(0, -1).map((n) => pascal(singular(n)));
83
+ const prefix = parents.join('');
84
+ const isPluralNoun = /s$/i.test(last) && !/(ss|us|is)$/i.test(last);
85
+ // Action endpoints: POST /users/{id}/activate → activateUser
86
+ if (method === 'POST' && !a.endsWithParam && !isPluralNoun && parents.length > 0) {
87
+ return { name: camel(`${last} ${prefix}`), target: singular(nouns[nouns.length - 2]) };
88
+ }
89
+ const single = pascal(singular(last));
90
+ switch (method) {
91
+ case 'GET':
92
+ return { name: camel(`get ${prefix} ${a.endsWithParam || !isPluralNoun ? single : pascal(last)}`), target: last };
93
+ case 'POST':
94
+ return { name: camel(`create ${prefix} ${single}`), target: last };
95
+ case 'PUT':
96
+ case 'PATCH':
97
+ return { name: camel(`update ${prefix} ${single}`), target: last };
98
+ case 'DELETE':
99
+ return { name: camel(`delete ${prefix} ${single}`), target: last };
100
+ default:
101
+ return { name: camel(`${method.toLowerCase()} ${prefix} ${single}`), target: last };
102
+ }
103
+ }
104
+ function queryType(value) {
105
+ if (/^-?\d+$/.test(value))
106
+ return { kind: 'int' };
107
+ if (/^-?\d*\.\d+$/.test(value))
108
+ return { kind: 'double' };
109
+ if (/^(true|false)$/i.test(value))
110
+ return { kind: 'bool' };
111
+ return { kind: 'string' };
112
+ }
113
+ function parseBody(body) {
114
+ if (body === undefined || body === '')
115
+ return undefined;
116
+ const t = body.trim();
117
+ if (t.startsWith('{') || t.startsWith('[')) {
118
+ try {
119
+ return JSON.parse(t);
120
+ }
121
+ catch {
122
+ throw new ToolError('bad_body', 'The request body in the cURL looks like JSON but does not parse.');
123
+ }
124
+ }
125
+ // Form-encoded: a=1&b=two
126
+ const obj = {};
127
+ for (const pair of t.split('&')) {
128
+ const eqAt = pair.indexOf('=');
129
+ const k = eqAt === -1 ? pair : pair.slice(0, eqAt);
130
+ const v = eqAt === -1 ? '' : pair.slice(eqAt + 1);
131
+ if (k)
132
+ obj[safeDecode(k)] = safeDecode(v);
133
+ }
134
+ return obj;
135
+ }
136
+ function renameIn(t, from, to) {
137
+ if (t.kind === 'object' && t.model === from)
138
+ return { kind: 'object', model: to };
139
+ if (t.kind === 'array')
140
+ return { kind: 'array', item: renameIn(t.item, from, to) };
141
+ return t;
142
+ }
143
+ export function buildEndpoint(input) {
144
+ const warnings = [...(input.warnings ?? [])];
145
+ if (input.url.hostname !== 'api.invalid')
146
+ checkHost(input.url);
147
+ const analysis = analyzePath(input.url.pathname, input.path);
148
+ const derived = deriveName(input.method, analysis);
149
+ const name = input.name ? functionName(input.name, input.method, derived.name) : derived.name;
150
+ const resource = analysis.nouns[0] ?? 'root';
151
+ const queryParams = [];
152
+ for (const [k, v] of input.url.searchParams.entries()) {
153
+ if (!queryParams.some((q) => q.name === k))
154
+ queryParams.push({ name: k, type: queryType(v) });
155
+ }
156
+ // Response.
157
+ let samples = input.responseSamples;
158
+ let unwrapData = false;
159
+ if (input.unwrap !== 'never') {
160
+ const inner = unwrapEnvelope(samples);
161
+ if (inner) {
162
+ samples = inner;
163
+ unwrapData = true;
164
+ warnings.push('The response is wrapped in {"data": ...}; the generated code returns the inner "data" value. Use --no-unwrap to keep the wrapper.');
165
+ }
166
+ }
167
+ const rootModelName = input.modelName ? typeName(input.modelName) : typeName(singular(derived.target));
168
+ let response;
169
+ const models = [];
170
+ if (samples.length) {
171
+ const r = inferModels(samples, rootModelName);
172
+ response = r.root;
173
+ models.push(...r.models);
174
+ warnings.push(...r.warnings.map((w) => `Response: ${w.replace(/^\$/, 'root')}`));
175
+ }
176
+ else if (input.method !== 'DELETE' && input.method !== 'HEAD') {
177
+ warnings.push('No response sample was given, so the response type is unknown. Pass --sample <file.json> or paste the response after the cURL.');
178
+ }
179
+ // Request.
180
+ let request;
181
+ const bodySample = input.requestSample ?? parseBody(input.body);
182
+ if (bodySample !== undefined) {
183
+ const r = inferModels([bodySample], `${pascal(name)} Request`);
184
+ let reqRoot = r.root;
185
+ const reqModels = r.models;
186
+ // Merge with response models: reuse identical shapes, rename clashes.
187
+ for (const m of [...reqModels]) {
188
+ const same = models.find((x) => x.name === m.name);
189
+ if (!same)
190
+ continue;
191
+ if (signature(same.fields) === signature(m.fields)) {
192
+ reqModels.splice(reqModels.indexOf(m), 1);
193
+ }
194
+ else {
195
+ const to = `${m.name}Input`;
196
+ for (const other of reqModels)
197
+ other.fields = other.fields.map((f) => ({ ...f, type: renameIn(f.type, m.name, to) }));
198
+ reqRoot = renameIn(reqRoot, m.name, to);
199
+ m.name = to;
200
+ }
201
+ }
202
+ request = reqRoot;
203
+ models.push(...reqModels);
204
+ warnings.push(...r.warnings.map((w) => `Request: ${w.replace(/^\$/, 'body')}`));
205
+ }
206
+ return {
207
+ name,
208
+ method: input.method,
209
+ origin: input.url.hostname === 'api.invalid' ? '' : input.url.origin,
210
+ path: analysis.path,
211
+ resource,
212
+ pathParams: analysis.params,
213
+ queryParams,
214
+ response,
215
+ unwrapData,
216
+ request,
217
+ models,
218
+ warnings,
219
+ };
220
+ }
@@ -0,0 +1,120 @@
1
+ // HAR import: the requests a browser recorded (DevTools → Network → Save all as
2
+ // HAR). JSON API calls are grouped by method and normalized path, and every
3
+ // response of a group becomes a sample for the model.
4
+ //
5
+ // A HAR file holds cookies, auth headers and personal data. Headers are never
6
+ // read; secret query parameters are dropped; bodies are only used to infer
7
+ // types and are never written, cached or kept in history.
8
+ import { ToolError } from "../core/types.js";
9
+ import { stripSecretQuery } from "./curl.js";
10
+ import { analyzePath, buildEndpoint } from "./endpoint.js";
11
+ const ANALYTICS = /(google-analytics|googletagmanager|doubleclick|segment\.(io|com)|sentry\.io|hotjar|mixpanel|amplitude|datadoghq|newrelic|nr-data|intercom|facebook\.(com|net)|clarity\.ms|fullstory|launchdarkly|stripe\.network|cloudflareinsights|vercel-insights|posthog)/i;
12
+ const METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
13
+ export const isHar = (doc) => !!doc && typeof doc === 'object' && !!doc.log && Array.isArray(doc.log.entries);
14
+ function bodyText(content) {
15
+ if (!content?.text)
16
+ return undefined;
17
+ return content.encoding === 'base64' ? Buffer.from(content.text, 'base64').toString('utf8') : content.text;
18
+ }
19
+ function json(text) {
20
+ if (text === undefined || text.trim() === '')
21
+ return undefined;
22
+ try {
23
+ return JSON.parse(text);
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ export function readHar(doc, opts = {}) {
30
+ const warnings = [];
31
+ const groups = new Map();
32
+ let skipped = { notJson: 0, errors: 0, analytics: 0, other: 0, outside: 0 };
33
+ const removed = [];
34
+ for (const e of doc.log.entries) {
35
+ const method = String(e.request?.method ?? '').toUpperCase();
36
+ const raw = e.request?.url ?? '';
37
+ if (!METHODS.has(method) || !/^https?:\/\//.test(raw)) {
38
+ skipped.other++;
39
+ continue;
40
+ }
41
+ let url;
42
+ try {
43
+ url = new URL(raw);
44
+ }
45
+ catch {
46
+ skipped.other++;
47
+ continue;
48
+ }
49
+ if (ANALYTICS.test(url.hostname)) {
50
+ skipped.analytics++;
51
+ continue;
52
+ }
53
+ if (opts.baseUrl && !raw.startsWith(opts.baseUrl.replace(/\/$/, ''))) {
54
+ skipped.outside++;
55
+ continue;
56
+ }
57
+ const status = e.response?.status ?? 0;
58
+ if ((status < 200 || status >= 300) && !opts.includeErrors) {
59
+ skipped.errors++;
60
+ continue;
61
+ }
62
+ const mime = e.response?.content?.mimeType ?? '';
63
+ const body = json(bodyText(e.response?.content));
64
+ const isDelete = method === 'DELETE' || status === 204;
65
+ if (!/json/i.test(mime) && !(isDelete && !mime)) {
66
+ skipped.notJson++;
67
+ continue;
68
+ }
69
+ stripSecretQuery(url, removed);
70
+ let path;
71
+ try {
72
+ path = analyzePath(url.pathname).path;
73
+ }
74
+ catch {
75
+ skipped.other++;
76
+ continue;
77
+ }
78
+ const key = `${method} ${url.host}${path}`;
79
+ const g = groups.get(key) ?? { method: method, url, samples: [], count: 0, host: url.host };
80
+ g.count++;
81
+ if (body !== undefined && g.samples.length < 10)
82
+ g.samples.push(body);
83
+ for (const [k, v] of url.searchParams)
84
+ if (!g.url.searchParams.has(k))
85
+ g.url.searchParams.set(k, v);
86
+ if (g.request === undefined && /json/i.test(e.request?.postData?.mimeType ?? 'json'))
87
+ g.request = json(e.request?.postData?.text);
88
+ groups.set(key, g);
89
+ }
90
+ if (!groups.size) {
91
+ throw new ToolError('no_endpoints', 'The HAR file has no JSON API calls to import.', `Skipped: ${skipped.notJson} non-JSON, ${skipped.errors} failed (use --include-errors), ${skipped.analytics} analytics, ${skipped.outside} outside --base-url, ${skipped.other} other.`);
92
+ }
93
+ const hosts = [...new Set([...groups.values()].map((g) => g.host))];
94
+ if (hosts.length > 1 && !opts.baseUrl)
95
+ warnings.push(`Requests go to ${hosts.length} hosts (${hosts.join(', ')}). Pass --base-url https://<host> to import only your API.`);
96
+ if (removed.length)
97
+ warnings.push(`Removed from the URLs and never written anywhere: ${[...new Set(removed)].join(', ')}.`);
98
+ const skippedTotal = skipped.notJson + skipped.errors + skipped.analytics + skipped.other + skipped.outside;
99
+ if (skippedTotal)
100
+ warnings.push(`Skipped ${skippedTotal} request${skippedTotal === 1 ? '' : 's'}: ${skipped.notJson} not JSON, ${skipped.errors} failed${skipped.errors ? ' (--include-errors keeps them)' : ''}, ${skipped.analytics} analytics, ${skipped.outside} outside --base-url, ${skipped.other} other.`);
101
+ const operations = [];
102
+ const used = new Set();
103
+ for (const g of groups.values()) {
104
+ try {
105
+ const spec = buildEndpoint({ method: g.method, url: g.url, responseSamples: g.samples, requestSample: g.request });
106
+ if (used.has(spec.name)) {
107
+ let n = 2;
108
+ while (used.has(`${spec.name}${n}`))
109
+ n++;
110
+ spec.name = `${spec.name}${n}`;
111
+ }
112
+ used.add(spec.name);
113
+ operations.push({ spec, tags: [g.host], source: `${g.method} ${g.host}${spec.path} (${g.count} request${g.count === 1 ? '' : 's'})` });
114
+ }
115
+ catch (e) {
116
+ warnings.push(`${g.method} ${g.url.pathname}: ${e.message}`);
117
+ }
118
+ }
119
+ return { format: 'har', title: `HAR from ${doc.log.creator?.name ?? 'a browser'} (${doc.log.entries.length} requests)`, operations, warnings };
120
+ }
@@ -0,0 +1,174 @@
1
+ // Infers models from one or more JSON samples. Rules are fixed and
2
+ // documented, so the same samples always give the same models.
3
+ //
4
+ // - Several samples are merged: a field missing or null in any sample is nullable.
5
+ // - Integers become double when any sample has a fraction, or when the field
6
+ // name suggests money, rates or coordinates (price, amount, lat, ...).
7
+ // - ISO 8601 strings become dates. Enums are never inferred.
8
+ // - A field that is only ever null, or an array that is only ever empty, gets
9
+ // an "unknown" type and a warning instead of a guess.
10
+ // - Identical object shapes are reused; same name with a different shape gets
11
+ // the parent name as a prefix.
12
+ import { pascal, singular } from "./naming.js";
13
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
14
+ const GENERIC_NAMES = new Set(['Item', 'Data', 'Detail', 'Details', 'Info', 'Meta', 'Value', 'Result', 'Attribute', 'Attributes', 'Entry', 'Node', 'Element', 'Record', 'Object', 'Option', 'Setting', 'Config']);
15
+ const DOUBLE_HINT = /(price|amount|total|subtotal|rate|ratio|lat|lng|lon|latitude|longitude|score|percent|percentage|balance|cost|fee|weight|height|width|discount|tax|rating)$/i;
16
+ function emptyShape() {
17
+ return { seen: 0, nulls: 0, strings: 0, dateStrings: 0, ints: 0, doubles: 0, bools: 0, objects: 0, arrays: 0, emptyArrays: 0, fields: new Map(), keyOrder: [], items: null };
18
+ }
19
+ function add(shape, value) {
20
+ shape.seen++;
21
+ if (value === null || value === undefined)
22
+ shape.nulls++;
23
+ else if (typeof value === 'string') {
24
+ shape.strings++;
25
+ if (ISO_DATE.test(value))
26
+ shape.dateStrings++;
27
+ }
28
+ else if (typeof value === 'number') {
29
+ if (Number.isInteger(value))
30
+ shape.ints++;
31
+ else
32
+ shape.doubles++;
33
+ }
34
+ else if (typeof value === 'boolean')
35
+ shape.bools++;
36
+ else if (Array.isArray(value)) {
37
+ shape.arrays++;
38
+ if (value.length === 0)
39
+ shape.emptyArrays++;
40
+ shape.items ??= emptyShape();
41
+ for (const item of value)
42
+ add(shape.items, item);
43
+ }
44
+ else if (typeof value === 'object') {
45
+ shape.objects++;
46
+ for (const [k, v] of Object.entries(value)) {
47
+ if (!shape.fields.has(k)) {
48
+ shape.fields.set(k, emptyShape());
49
+ shape.keyOrder.push(k);
50
+ }
51
+ add(shape.fields.get(k), v);
52
+ }
53
+ }
54
+ }
55
+ export function signature(fields) {
56
+ return fields
57
+ .map((f) => `${f.jsonName}:${typeKey(f.type)}${f.nullable ? '?' : ''}`)
58
+ .sort()
59
+ .join(',');
60
+ }
61
+ function typeKey(t) {
62
+ if (t.kind === 'array')
63
+ return `[${typeKey(t.item)}]`;
64
+ if (t.kind === 'object')
65
+ return `{${t.model}}`;
66
+ return t.kind;
67
+ }
68
+ export function inferModels(samples, rootName) {
69
+ const shape = emptyShape();
70
+ for (const s of samples)
71
+ add(shape, s);
72
+ const models = [];
73
+ const bySignature = new Map();
74
+ const byName = new Map();
75
+ const warnings = [];
76
+ function toType(s, path, nameHint, parentName, fieldName) {
77
+ const nullable = s.nulls > 0;
78
+ const nonNull = s.seen - s.nulls;
79
+ if (nonNull === 0) {
80
+ warnings.push(`${path} is null in every sample, so its type is unknown. Pass a sample where it has a value.`);
81
+ return { type: { kind: 'unknown' }, nullable: true };
82
+ }
83
+ const kinds = [s.strings > 0, s.ints + s.doubles > 0, s.bools > 0, s.objects > 0, s.arrays > 0].filter(Boolean).length;
84
+ if (kinds > 1) {
85
+ warnings.push(`${path} has different types in the samples (for example text and number), so its type is unknown.`);
86
+ return { type: { kind: 'unknown' }, nullable };
87
+ }
88
+ if (s.strings > 0)
89
+ return { type: { kind: s.dateStrings === s.strings ? 'date' : 'string' }, nullable };
90
+ if (s.ints + s.doubles > 0) {
91
+ const isDouble = s.doubles > 0 || DOUBLE_HINT.test(fieldName);
92
+ return { type: { kind: isDouble ? 'double' : 'int' }, nullable };
93
+ }
94
+ if (s.bools > 0)
95
+ return { type: { kind: 'bool' }, nullable };
96
+ if (s.arrays > 0) {
97
+ const items = s.items;
98
+ if (items.seen === 0) {
99
+ warnings.push(`${path} is an empty list in every sample, so its item type is unknown. Pass a sample with at least one item.`);
100
+ return { type: { kind: 'array', item: { kind: 'unknown' } }, nullable };
101
+ }
102
+ // A list under a singular key ("checklist") holds "ChecklistItem"s.
103
+ const itemHint = parentName !== '' && singular(nameHint) === nameHint && s.items.objects > 0 ? `${nameHint} Item` : singular(nameHint);
104
+ const item = toType(items, `${path}[]`, itemHint, parentName, singular(fieldName));
105
+ if (item.nullable)
106
+ warnings.push(`${path} contains null items; they are typed as non-null.`);
107
+ return { type: { kind: 'array', item: item.type }, nullable };
108
+ }
109
+ // Object.
110
+ const name = objectModel(s, path, nameHint, parentName);
111
+ return { type: { kind: 'object', model: name }, nullable };
112
+ }
113
+ function objectModel(s, path, nameHint, parentName) {
114
+ let base = pascal(nameHint) || 'Item';
115
+ if (GENERIC_NAMES.has(base) && parentName)
116
+ base = pascal(`${parentName} ${base}`);
117
+ const fields = [];
118
+ for (const key of s.keyOrder) {
119
+ const f = s.fields.get(key);
120
+ const childName = pascal(key);
121
+ const { type, nullable } = toType(f, `${path}.${key}`, childName, base, key);
122
+ fields.push({ jsonName: key, type, nullable: nullable || f.seen < s.objects });
123
+ }
124
+ const sig = signature(fields);
125
+ const reuse = bySignature.get(sig);
126
+ if (reuse)
127
+ return reuse;
128
+ let name = base;
129
+ if (byName.has(name) && !parentName) {
130
+ // The root model takes a "Response" suffix rather than a number.
131
+ name = `${base}Response`;
132
+ let n = 2;
133
+ while (byName.has(name))
134
+ name = `${base}Response${n++}`;
135
+ }
136
+ else if (byName.has(name)) {
137
+ name = pascal(`${parentName} ${base}`);
138
+ let n = 2;
139
+ while (byName.has(name))
140
+ name = `${pascal(`${parentName} ${base}`)}${n++}`;
141
+ }
142
+ const model = { name, fields };
143
+ byName.set(name, model);
144
+ bySignature.set(sig, name);
145
+ models.push(model);
146
+ return name;
147
+ }
148
+ const { type } = samples.length ? toType(shape, '$', rootName, '', rootName) : { type: { kind: 'unknown' } };
149
+ return { root: type, models, warnings };
150
+ }
151
+ const META_KEYS = new Set(['meta', 'status', 'message', 'success', 'code', 'errors', 'error', 'pagination', 'links', 'total', 'page', 'per_page', 'perPage', 'count', 'timestamp']);
152
+ /**
153
+ * Detects a response envelope such as {"data": {...}, "meta": {...}}. Returns
154
+ * the inner samples when every sample is an envelope, or null.
155
+ */
156
+ export function unwrapEnvelope(samples) {
157
+ if (!samples.length)
158
+ return null;
159
+ const inner = [];
160
+ for (const s of samples) {
161
+ if (!s || typeof s !== 'object' || Array.isArray(s))
162
+ return null;
163
+ const obj = s;
164
+ if (!('data' in obj))
165
+ return null;
166
+ if (!Object.keys(obj).every((k) => k === 'data' || META_KEYS.has(k)))
167
+ return null;
168
+ const d = obj.data;
169
+ if (d === null || typeof d !== 'object')
170
+ return null;
171
+ inner.push(d);
172
+ }
173
+ return inner;
174
+ }
@@ -0,0 +1,145 @@
1
+ // Deterministic naming helpers shared by skills and adapters.
2
+ export function words(input) {
3
+ return input
4
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
5
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
6
+ .split(/[^A-Za-z0-9]+/)
7
+ .filter(Boolean)
8
+ .map((w) => w.toLowerCase());
9
+ }
10
+ const cap = (w) => (w ? w[0].toUpperCase() + w.slice(1) : w);
11
+ export const pascal = (s) => words(s).map(cap).join('');
12
+ export const camel = (s) => {
13
+ const p = pascal(s);
14
+ return p ? p[0].toLowerCase() + p.slice(1) : p;
15
+ };
16
+ export const snake = (s) => words(s).join('_');
17
+ export const kebab = (s) => words(s).join('-');
18
+ export function fileName(base, style) {
19
+ switch (style) {
20
+ case 'camel':
21
+ return camel(base);
22
+ case 'pascal':
23
+ return pascal(base);
24
+ case 'snake':
25
+ return snake(base);
26
+ default:
27
+ return kebab(base);
28
+ }
29
+ }
30
+ /** Guesses the naming style of a set of file base names (without extension). */
31
+ export function detectFileNaming(names, fallback) {
32
+ const counts = { kebab: 0, camel: 0, pascal: 0, snake: 0 };
33
+ for (const n of names) {
34
+ if (n.includes('-'))
35
+ counts.kebab++;
36
+ else if (n.includes('_'))
37
+ counts.snake++;
38
+ else if (/^[A-Z]/.test(n) && /[a-z]/.test(n))
39
+ counts.pascal++;
40
+ else if (/^[a-z]+[A-Z]/.test(n))
41
+ counts.camel++;
42
+ }
43
+ let best = fallback;
44
+ let bestCount = 0;
45
+ for (const style of ['kebab', 'camel', 'pascal', 'snake']) {
46
+ if (counts[style] > bestCount) {
47
+ best = style;
48
+ bestCount = counts[style];
49
+ }
50
+ }
51
+ return best;
52
+ }
53
+ const IRREGULAR = {
54
+ people: 'person',
55
+ children: 'child',
56
+ men: 'man',
57
+ women: 'woman',
58
+ data: 'data',
59
+ news: 'news',
60
+ status: 'status',
61
+ series: 'series',
62
+ species: 'species',
63
+ analytics: 'analytics',
64
+ settings: 'settings',
65
+ me: 'me',
66
+ };
67
+ /** Singularizes the last word of an identifier, keeping its case style. */
68
+ export function singular(word) {
69
+ const lower = word.toLowerCase();
70
+ if (IRREGULAR[lower])
71
+ return matchCase(word, IRREGULAR[lower]);
72
+ if (/ies$/i.test(word) && word.length > 4)
73
+ return word.slice(0, -3) + matchCase(word.slice(-3), 'y');
74
+ if (/(ss|us|is)$/i.test(word))
75
+ return word;
76
+ if (/(ss|x|ch|sh|z|us)es$/i.test(word))
77
+ return word.slice(0, -2);
78
+ if (/s$/i.test(word) && word.length > 2)
79
+ return word.slice(0, -1);
80
+ return word;
81
+ }
82
+ export function plural(word) {
83
+ if (/[^aeiou]y$/i.test(word))
84
+ return word.slice(0, -1) + 'ies';
85
+ if (/(s|x|ch|sh)$/i.test(word))
86
+ return word + 'es';
87
+ return word + 's';
88
+ }
89
+ function matchCase(source, target) {
90
+ if (source === source.toUpperCase())
91
+ return target.toUpperCase();
92
+ if (source[0] === source[0].toUpperCase())
93
+ return cap(target);
94
+ return target;
95
+ }
96
+ /** Makes a safe identifier from any string (used for JSON keys that are not valid identifiers). */
97
+ export function identifier(jsonKey, reserved) {
98
+ let id = camel(jsonKey);
99
+ if (!id)
100
+ id = 'field';
101
+ if (/^[0-9]/.test(id))
102
+ id = `n${id}`;
103
+ if (reserved.has(id))
104
+ id = `${id}Value`;
105
+ return id;
106
+ }
107
+ export const TS_RESERVED = new Set([
108
+ 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export',
109
+ 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super',
110
+ 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with',
111
+ ]);
112
+ export const DART_RESERVED = new Set([
113
+ 'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'covariant',
114
+ 'default', 'deferred', 'do', 'dynamic', 'else', 'enum', 'export', 'extends', 'extension', 'external', 'factory', 'false',
115
+ 'final', 'finally', 'for', 'Function', 'get', 'hide', 'if', 'implements', 'import', 'in', 'interface', 'is', 'late',
116
+ 'library', 'mixin', 'new', 'null', 'on', 'operator', 'part', 'required', 'rethrow', 'return', 'set', 'show', 'static',
117
+ 'super', 'switch', 'sync', 'this', 'throw', 'true', 'try', 'typedef', 'var', 'void', 'while', 'with', 'yield', 'hashCode',
118
+ 'runtimeType', 'toString', 'noSuchMethod',
119
+ ]);
120
+ /**
121
+ * A function name that compiles in both TypeScript and Dart. Names from an
122
+ * operationId, a Postman item or --name can be reserved words ("delete") or
123
+ * start with a digit ("2fa.verify"). A reserved word falls back to the name
124
+ * derived from the path (deleteUser); a leading digit gets the method in front.
125
+ */
126
+ export function functionName(raw, method, fallback) {
127
+ let name = camel(raw);
128
+ if (!name)
129
+ name = fallback ?? camel(`${method} request`);
130
+ if (/^[0-9]/.test(name))
131
+ name = `${method.toLowerCase()}${name}`;
132
+ if (TS_RESERVED.has(name) || DART_RESERVED.has(name)) {
133
+ name = fallback && fallback !== name && !TS_RESERVED.has(fallback) && !DART_RESERVED.has(fallback) ? fallback : `${name}Request`;
134
+ }
135
+ return name;
136
+ }
137
+ /** A class or interface name that compiles in both languages. */
138
+ export function typeName(raw, fallback = 'Item') {
139
+ let name = pascal(raw) || fallback;
140
+ if (/^[0-9]/.test(name))
141
+ name = `${fallback === 'Item' ? 'Model' : fallback}${name}`;
142
+ if (TS_RESERVED.has(name) || DART_RESERVED.has(name))
143
+ name = `${name}Model`;
144
+ return name;
145
+ }