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,271 @@
1
+ // api diff <old> <new>: what changed between two versions of an API spec,
2
+ // which changes break clients, and where this project uses each one.
3
+ //
4
+ // breaking an operation removed; a response field removed or changed type;
5
+ // a new path parameter; a new required request field
6
+ // warning a response field that can now be null; a query parameter removed;
7
+ // a request field changed type
8
+ // info operations, response fields and optional request fields added
9
+ //
10
+ // Inside a codz project each change is matched to the project's endpoint
11
+ // and the lines that use it (the endpoint's callers, and the field in those
12
+ // files and the model). Output: text, Markdown (for a pull request comment),
13
+ // JSON, or SARIF (GitHub code scanning). Exit 1 at --fail-on (default breaking).
14
+ import { readFileSync } from 'node:fs';
15
+ import { join, relative } from 'node:path';
16
+ import { loadMap } from "../core/cache.js";
17
+ import { loadConfig } from "../core/config.js";
18
+ import { readText } from "../core/fsutil.js";
19
+ import { ToolError } from "../core/types.js";
20
+ import { readApiFile } from "../input/apiSpec.js";
21
+ import { camel } from "../input/naming.js";
22
+ import { pathMatches } from "./apiImport.js";
23
+ /** Path with parameters blanked, without scheme and host (some adapters keep the full URL). */
24
+ const shape = (p) => p.replace(/^\/?[a-z]+:\/\/[^/]+/i, '').replace(/\{[^/}]+\}|:[A-Za-z_]\w*|\$\{[^}]+\}|\$[A-Za-z_]\w*/g, '{}').replace(/\/+$/, '') || '/';
25
+ const key = (o) => `${o.spec.method} ${shape(o.spec.path)}`;
26
+ const label = (o) => `${o.spec.method} ${o.spec.path}`;
27
+ function typeName(t) {
28
+ if (!t)
29
+ return 'nothing';
30
+ if (t.kind === 'array')
31
+ return `${typeName(t.item)}[]`;
32
+ if (t.kind === 'object')
33
+ return 'object';
34
+ return t.kind;
35
+ }
36
+ /** Response side: what a client reads. Removing or retyping breaks it; null where there was none may crash it. */
37
+ function compareResponse(o, n, at, ctx) {
38
+ if (!o || !n) {
39
+ if (o && !n)
40
+ ctx.out.push({ severity: 'breaking', kind: 'response-removed', at, message: `${at} is no longer returned` });
41
+ return;
42
+ }
43
+ if (o.kind !== n.kind && !(o.kind === 'int' && n.kind === 'double') && o.kind !== 'unknown' && n.kind !== 'unknown') {
44
+ ctx.out.push({ severity: 'breaking', kind: 'type-changed', at, message: `${at} changed type: ${typeName(o)} → ${typeName(n)}` });
45
+ return;
46
+ }
47
+ if (o.kind === 'array' && n.kind === 'array')
48
+ return compareResponse(o.item, n.item, `${at}[]`, ctx);
49
+ if (o.kind === 'object' && n.kind === 'object') {
50
+ const visit = `${at}:${o.model}:${n.model}`;
51
+ if (ctx.seen.has(visit))
52
+ return;
53
+ ctx.seen.add(visit);
54
+ const om = ctx.oldModels.get(o.model);
55
+ const nm = ctx.newModels.get(n.model);
56
+ if (!om || !nm)
57
+ return;
58
+ for (const f of om.fields) {
59
+ const g = nm.fields.find((x) => x.jsonName === f.jsonName);
60
+ const p = `${at}.${f.jsonName}`;
61
+ if (!g) {
62
+ ctx.out.push({ severity: 'breaking', kind: 'field-removed', at: p, message: `${p} was removed` });
63
+ continue;
64
+ }
65
+ if (!f.nullable && g.nullable)
66
+ ctx.out.push({ severity: 'warning', kind: 'became-nullable', at: p, message: `${p} can now be null` });
67
+ compareResponse(f.type, g.type, p, ctx);
68
+ }
69
+ for (const g of nm.fields)
70
+ if (!om.fields.some((f) => f.jsonName === g.jsonName))
71
+ ctx.out.push({ severity: 'info', kind: 'field-added', at: `${at}.${g.jsonName}`, message: `${at}.${g.jsonName} was added (${typeName(g.type)}${g.nullable ? ', optional' : ''})` });
72
+ }
73
+ }
74
+ /** Request side: what a client sends. A new required field breaks it; a dropped field is usually ignored by servers. */
75
+ function compareRequest(o, n, at, ctx) {
76
+ if (!n)
77
+ return;
78
+ if (!o) {
79
+ ctx.out.push({ severity: 'breaking', kind: 'request-added', at, message: `${at} body is now required` });
80
+ return;
81
+ }
82
+ if (o.kind !== n.kind && o.kind !== 'unknown' && n.kind !== 'unknown') {
83
+ ctx.out.push({ severity: 'warning', kind: 'request-type-changed', at, message: `${at} changed type: ${typeName(o)} → ${typeName(n)}` });
84
+ return;
85
+ }
86
+ if (o.kind === 'array' && n.kind === 'array')
87
+ return compareRequest(o.item, n.item, `${at}[]`, ctx);
88
+ if (o.kind === 'object' && n.kind === 'object') {
89
+ const visit = `req:${at}:${o.model}:${n.model}`;
90
+ if (ctx.seen.has(visit))
91
+ return;
92
+ ctx.seen.add(visit);
93
+ const om = ctx.oldModels.get(o.model);
94
+ const nm = ctx.newModels.get(n.model);
95
+ if (!om || !nm)
96
+ return;
97
+ for (const g of nm.fields) {
98
+ const f = om.fields.find((x) => x.jsonName === g.jsonName);
99
+ const p = `${at}.${g.jsonName}`;
100
+ if (!f) {
101
+ ctx.out.push(g.nullable ? { severity: 'info', kind: 'request-field-added', at: p, message: `${p} was added (optional)` } : { severity: 'breaking', kind: 'request-field-required', at: p, message: `${p} is new and required` });
102
+ continue;
103
+ }
104
+ if (f.nullable && !g.nullable)
105
+ ctx.out.push({ severity: 'breaking', kind: 'request-field-required', at: p, message: `${p} is now required` });
106
+ compareRequest(f.type, g.type, p, ctx);
107
+ }
108
+ for (const f of om.fields)
109
+ if (!nm.fields.some((g) => g.jsonName === f.jsonName))
110
+ ctx.out.push({ severity: 'info', kind: 'request-field-removed', at: `${at}.${f.jsonName}`, message: `${at}.${f.jsonName} is no longer accepted (servers usually ignore it)` });
111
+ }
112
+ }
113
+ export function diffSpecs(oldText, newText, opts = {}) {
114
+ const filter = (ops) => {
115
+ const wantTags = new Set((opts.tags ?? []).map((t) => t.toLowerCase()));
116
+ return ops.filter((o) => (!wantTags.size || o.tags.some((t) => wantTags.has(t.toLowerCase()))) && (!opts.paths?.length || opts.paths.some((g) => pathMatches(g, o.spec.path))));
117
+ };
118
+ const a = readApiFile(oldText);
119
+ const b = readApiFile(newText);
120
+ const oldOps = new Map(filter(a.operations).map((o) => [key(o), o]));
121
+ const newOps = new Map(filter(b.operations).map((o) => [key(o), o]));
122
+ const pairs = [];
123
+ let compared = 0;
124
+ for (const [k, o] of oldOps) {
125
+ const n = newOps.get(k);
126
+ if (!n) {
127
+ pairs.push({ op: o, change: { severity: 'breaking', kind: 'operation-removed', operation: label(o), name: o.spec.name, message: `${label(o)} was removed` } });
128
+ continue;
129
+ }
130
+ compared++;
131
+ const ctx = { oldModels: new Map(o.spec.models.map((m) => [m.name, m])), newModels: new Map(n.spec.models.map((m) => [m.name, m])), out: [], seen: new Set() };
132
+ if (n.spec.pathParams.length !== o.spec.pathParams.length)
133
+ ctx.out.push({ severity: 'breaking', kind: 'path-params-changed', message: `path parameters changed: ${o.spec.pathParams.length} → ${n.spec.pathParams.length}` });
134
+ for (const q of o.spec.queryParams)
135
+ if (!n.spec.queryParams.some((x) => x.name === q.name))
136
+ ctx.out.push({ severity: 'warning', kind: 'query-removed', at: `query.${q.name}`, message: `query parameter ${q.name} was removed` });
137
+ for (const q of n.spec.queryParams)
138
+ if (!o.spec.queryParams.some((x) => x.name === q.name))
139
+ ctx.out.push({ severity: 'info', kind: 'query-added', at: `query.${q.name}`, message: `query parameter ${q.name} was added` });
140
+ compareResponse(o.spec.response, n.spec.response, 'response', ctx);
141
+ compareRequest(o.spec.request, n.spec.request, 'request', ctx);
142
+ for (const c of ctx.out)
143
+ pairs.push({ op: n, change: { ...c, operation: label(n), name: n.spec.name, message: `${label(n)}: ${c.message}` } });
144
+ }
145
+ for (const [k, n] of newOps)
146
+ if (!oldOps.has(k))
147
+ pairs.push({ op: n, change: { severity: 'info', kind: 'operation-added', operation: label(n), name: n.spec.name, message: `${label(n)} was added` } });
148
+ const order = { breaking: 0, warning: 1, info: 2 };
149
+ pairs.sort((x, y) => order[x.change.severity] - order[y.change.severity] || x.change.operation.localeCompare(y.change.operation));
150
+ const counts = { breaking: 0, warning: 0, info: 0 };
151
+ for (const p of pairs)
152
+ counts[p.change.severity]++;
153
+ return { report: { oldTitle: a.title, newTitle: b.title, changes: pairs.map((p) => ({ ...p.change, usages: [] })), counts, compared }, pairs };
154
+ }
155
+ /** The project's endpoint for a spec operation: same method, same path or the same path under a prefix (/v1). */
156
+ function matchEndpoint(op, endpoints) {
157
+ const same = endpoints.filter((e) => e.method === op.spec.method);
158
+ const p = shape(op.spec.path);
159
+ return same.find((e) => shape(e.path) === p) ?? same.find((e) => p.endsWith(shape(e.path)) && shape(e.path) !== '/');
160
+ }
161
+ export async function planApiDiff(root, adapter, oldFile, newFile, opts = {}) {
162
+ const read = (f) => {
163
+ const p = root && readText(join(root, f)) !== null ? join(root, f) : f;
164
+ const t = readText(p);
165
+ if (t === null)
166
+ throw new ToolError('no_file', `Cannot read ${f}.`);
167
+ return t;
168
+ };
169
+ const { report, pairs } = diffSpecs(read(oldFile), read(newFile), opts);
170
+ if (!root || !adapter || adapter.id === 'generic')
171
+ return { ...report, inProject: false };
172
+ // Where the project uses each change.
173
+ const { map } = await loadMap(root, adapter, loadConfig(root).conventions);
174
+ const fileCache = new Map();
175
+ const lines = (f) => {
176
+ if (!fileCache.has(f)) {
177
+ try {
178
+ fileCache.set(f, readFileSync(join(root, f), 'utf8').split('\n'));
179
+ }
180
+ catch {
181
+ fileCache.set(f, []);
182
+ }
183
+ }
184
+ return fileCache.get(f);
185
+ };
186
+ const changes = pairs.map(({ change, op }) => {
187
+ const ep = matchEndpoint(op, map.endpoints);
188
+ const usages = [];
189
+ if (ep) {
190
+ const field = change.at?.split('.').pop()?.replace(/\[\]$/, '');
191
+ const files = [...new Set([...ep.calledFrom, ...(field ? map.models.filter((m) => m.fields.some((f) => f.name === field || f.name === camel(field))).map((m) => m.file) : [])])];
192
+ if (change.kind === 'operation-removed' || change.kind === 'path-params-changed' || change.kind === 'request-added') {
193
+ // Every call of the function.
194
+ for (const f of ep.calledFrom)
195
+ lines(f).forEach((l, i) => new RegExp(`\\b${ep.name}\\b`).test(l) && usages.push({ file: f, line: i + 1, text: l.trim() }));
196
+ }
197
+ else if (field && /^[A-Za-z_$][\w$]*$/.test(field)) {
198
+ const names = [...new Set([field, camel(field)])].map((n) => n.replace(/\$/g, '\\$'));
199
+ const re = new RegExp(`(\\.(${names.join('|')})\\b|['"](${names.join('|')})['"]|\\b(?:val|var)\\s+(${names.join('|')})\\b)`);
200
+ for (const f of files)
201
+ lines(f).forEach((l, i) => re.test(l) && usages.push({ file: f, line: i + 1, text: l.trim() }));
202
+ }
203
+ }
204
+ return { ...change, endpoint: ep?.name, usages: usages.slice(0, 50) };
205
+ });
206
+ return { ...report, changes, inProject: true };
207
+ }
208
+ // ---------- Output ----------
209
+ const ICON = { breaking: '✗', warning: '!', info: '+' };
210
+ export function renderDiffText(r) {
211
+ const out = [`${r.oldTitle || 'old'} → ${r.newTitle || 'new'}: ${r.counts.breaking} breaking, ${r.counts.warning} warning${r.counts.warning === 1 ? '' : 's'}, ${r.counts.info} added (${r.compared} operations in both)`];
212
+ for (const c of r.changes) {
213
+ out.push(`${ICON[c.severity]} ${c.message}${c.endpoint ? ` [${c.endpoint}()]` : ''}`);
214
+ for (const u of c.usages.slice(0, 8))
215
+ out.push(` ${u.file}:${u.line} ${u.text.slice(0, 100)}`);
216
+ if (c.usages.length > 8)
217
+ out.push(` … ${c.usages.length - 8} more`);
218
+ }
219
+ if (!r.changes.length)
220
+ out.push('No differences.');
221
+ return out.join('\n');
222
+ }
223
+ export function renderDiffMarkdown(r) {
224
+ const head = r.counts.breaking ? `### API change: ${r.counts.breaking} breaking` : '### API change: nothing breaking';
225
+ const out = [head, '', `${r.oldTitle || 'old'} → ${r.newTitle || 'new'}: **${r.counts.breaking}** breaking, **${r.counts.warning}** warnings, **${r.counts.info}** additions.`, ''];
226
+ const section = (sev, title) => {
227
+ const list = r.changes.filter((c) => c.severity === sev);
228
+ if (!list.length)
229
+ return;
230
+ out.push(`#### ${title}`, '');
231
+ for (const c of list) {
232
+ out.push(`- ${c.message.replace(/`/g, "'")}${c.endpoint ? ` (\`${c.endpoint}()\`)` : ''}`);
233
+ for (const u of c.usages.slice(0, 5))
234
+ out.push(` - \`${u.file}:${u.line}\``);
235
+ if (c.usages.length > 5)
236
+ out.push(` - … ${c.usages.length - 5} more`);
237
+ }
238
+ out.push('');
239
+ };
240
+ section('breaking', 'Breaking');
241
+ section('warning', 'Check');
242
+ section('info', 'Added');
243
+ if (r.inProject && r.counts.breaking)
244
+ out.push('Update the app with `codz api sync <new spec>`, then fix the lines listed above.');
245
+ return `${out.join('\n').trim()}\n`;
246
+ }
247
+ export function renderDiffSarif(r, root) {
248
+ const level = (s) => (s === 'breaking' ? 'error' : s === 'warning' ? 'warning' : 'note');
249
+ const results = r.changes.flatMap((c) => {
250
+ const base = { ruleId: `api/${c.kind}`, level: level(c.severity), message: { text: c.message } };
251
+ if (!c.usages.length)
252
+ return c.severity === 'info' ? [] : [base];
253
+ return c.usages.map((u) => ({ ...base, locations: [{ physicalLocation: { artifactLocation: { uri: root ? relative(root, join(root, u.file)).split('\\').join('/') : u.file }, region: { startLine: u.line } } }] }));
254
+ });
255
+ const kinds = [...new Set(r.changes.map((c) => c.kind))];
256
+ return `${JSON.stringify({
257
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
258
+ version: '2.1.0',
259
+ runs: [{ tool: { driver: { name: 'codz api diff', informationUri: 'https://github.com/', rules: kinds.map((k) => ({ id: `api/${k}`, shortDescription: { text: k.replace(/-/g, ' ') } })) } }, results }],
260
+ }, null, 2)}\n`;
261
+ }
262
+ /** Exit code for --fail-on. */
263
+ export function diffFails(r, failOn) {
264
+ if (failOn === 'none')
265
+ return false;
266
+ if (failOn === 'any')
267
+ return r.changes.length > 0;
268
+ if (failOn === 'warning')
269
+ return r.counts.breaking + r.counts.warning > 0;
270
+ return r.counts.breaking > 0;
271
+ }