codz-cli 0.0.0-stage → 0.1.2

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 (118) hide show
  1. package/BETA.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +525 -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 +638 -0
  77. package/dist/skills/createDemo.js +1026 -0
  78. package/dist/skills/doctor.js +304 -0
  79. package/dist/skills/doctorFix.js +214 -0
  80. package/dist/skills/explain.js +61 -0
  81. package/dist/skills/feature.js +418 -0
  82. package/dist/skills/featureEdit.js +225 -0
  83. package/dist/skills/featureLinks.js +179 -0
  84. package/dist/skills/init.js +66 -0
  85. package/dist/skills/iosUi.js +468 -0
  86. package/dist/skills/locale.js +335 -0
  87. package/dist/skills/mcp.js +175 -0
  88. package/dist/skills/mock.js +482 -0
  89. package/dist/skills/module.js +23 -0
  90. package/dist/skills/plans.js +66 -0
  91. package/dist/skills/prefill.js +84 -0
  92. package/dist/skills/reactNativeUi.js +388 -0
  93. package/dist/skills/reactUi.js +352 -0
  94. package/dist/skills/report.js +75 -0
  95. package/dist/skills/route.js +230 -0
  96. package/dist/skills/screen.js +597 -0
  97. package/dist/skills/screenForm.js +410 -0
  98. package/dist/skills/setup.js +28 -0
  99. package/dist/skills/setupDoctor.js +226 -0
  100. package/dist/skills/templatesCmd.js +32 -0
  101. package/dist/skills/testgen.js +331 -0
  102. package/dist/skills/theme.js +388 -0
  103. package/dist/starters/auth.js +144 -0
  104. package/dist/starters/commands.js +283 -0
  105. package/dist/starters/engine.js +315 -0
  106. package/dist/starters/home.js +42 -0
  107. package/dist/starters/http.js +93 -0
  108. package/dist/starters/manifest.js +285 -0
  109. package/dist/starters/paths.js +70 -0
  110. package/dist/starters/publish.js +150 -0
  111. package/dist/starters/registry.js +258 -0
  112. package/dist/starters/sign.js +72 -0
  113. package/dist/starters/source.js +98 -0
  114. package/dist/starters/strictJson.js +148 -0
  115. package/dist/starters/tar.js +180 -0
  116. package/docs/COMMANDS.md +527 -0
  117. package/examples/openapi.yaml +319 -0
  118. package/package.json +57 -4
package/dist/cli.js ADDED
@@ -0,0 +1,1175 @@
1
+ // codz command line. Every command supports --json for scripts and AI agents.
2
+ import { baseUrlProblem } from "./core/safe.js";
3
+ import { existsSync, openSync, writeFileSync } from 'node:fs';
4
+ import { ReadStream } from 'node:tty';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { createInterface } from 'node:readline/promises';
7
+ import { adapterFor } from "./adapters/index.js";
8
+ import { loadMap, setCacheWrites } from "./core/cache.js";
9
+ import { applyChangeSet, changeSetJson, historyShow, loadPlan, readHistory, renderDiff, savePlan, undoLast, undoTo } from "./core/changeset.js";
10
+ import { changeSummaryLine, commitChange, openDiffInEditor, readClipboard } from "./core/workflow.js";
11
+ import { CONFIG_FILE, loadConfig } from "./core/config.js";
12
+ import { ToolError } from "./core/types.js";
13
+ import { planApiAdd } from "./skills/apiAdd.js";
14
+ import { renderExplain } from "./skills/explain.js";
15
+ import { detectProject, initFiles, writeInitFiles } from "./skills/init.js";
16
+ import { planSetup } from "./skills/setup.js";
17
+ import { planApiWire, planModuleAdd } from "./skills/module.js";
18
+ import { listApiImport, planApiImport } from "./skills/apiImport.js";
19
+ import { planApiSync } from "./skills/apiSync.js";
20
+ import { planTestAdd } from "./skills/testgen.js";
21
+ import { listTemplates, planTemplatesInit } from "./skills/templatesCmd.js";
22
+ import { serveMcp } from "./skills/mcp.js";
23
+ import { createProject, describeCreate, PROJECT_TYPES, projectType } from "./skills/create.js";
24
+ import { planApiRemove, planApiRename, planApiUpdate } from "./skills/apiEdit.js";
25
+ import { planMockAdd } from "./skills/mock.js";
26
+ import { planLocaleAdd, planLocaleKey } from "./skills/locale.js";
27
+ import { planThemeSet } from "./skills/theme.js";
28
+ import { planRouteAdd } from "./skills/route.js";
29
+ import { planScreenAdd } from "./skills/screen.js";
30
+ import { planFeatureAdd } from "./skills/feature.js";
31
+ import { planAuthAdd } from "./skills/auth.js";
32
+ import { diffFails, planApiDiff, renderDiffMarkdown, renderDiffSarif, renderDiffText } from "./skills/apiDiff.js";
33
+ import { renderDoctor, runDoctor } from "./skills/doctor.js";
34
+ import { renderSetupDoctor, runSetupDoctor } from "./skills/setupDoctor.js";
35
+ import { planDoctorFix } from "./skills/doctorFix.js";
36
+ import { configCheck, configView, planConfigSet } from "./skills/config.js";
37
+ import { apiList, apiShow } from "./skills/apiList.js";
38
+ import { cleanPlans, listPlans, showPlan } from "./skills/plans.js";
39
+ import { complete, completionScript, SHELLS } from "./skills/completion.js";
40
+ import { genericAdapter } from "./adapters/generic.js";
41
+ import { allowedFlags, BOOLEAN_FLAGS, findCommand, FLAGS, renderCommandHelp, renderHelp, REPEATABLE_FLAGS } from "./core/commands.js";
42
+ import { closest } from "./core/validateConfig.js";
43
+ import { recordEvent } from "./core/events.js";
44
+ import { buildReport, renderReport } from "./skills/report.js";
45
+ import { homedir } from 'node:os';
46
+ import { runTool } from "./core/proc.js";
47
+ /** Operating-system errors that are not codz bugs, with what to do about them. */
48
+ const SYSTEM_ERRORS = {
49
+ ENOSPC: { code: 'disk_full', say: 'The disk is full, so codz could not write its files.', hint: 'Free some disk space and run the command again.' },
50
+ EACCES: { code: 'permission_denied', say: 'codz is not allowed to write here.', hint: 'Check the folder permissions (or that the files are not read-only) and run the command again.' },
51
+ EPERM: { code: 'permission_denied', say: 'The operating system did not allow the change.', hint: 'Check the folder permissions, and that no other app (an editor or antivirus) has the files locked.' },
52
+ EROFS: { code: 'read_only', say: 'This folder is on a read-only disk.', hint: 'Run codz in a writable copy of the project.' },
53
+ EMFILE: { code: 'too_many_files', say: 'Too many files are open at once.', hint: 'Close other programs or raise the limit (ulimit -n 4096), then run the command again.' },
54
+ };
55
+ export const VERSION = '0.1.0';
56
+ export function parseArgs(argv) {
57
+ const args = { positional: [], flags: {}, multi: {} };
58
+ for (let i = 0; i < argv.length; i++) {
59
+ const a = argv[i];
60
+ if (a === '--') {
61
+ args.positional.push(...argv.slice(i + 1));
62
+ break;
63
+ }
64
+ if (a.startsWith('-') && a.length > 1) {
65
+ const body = a.replace(/^--?/, '');
66
+ const eq = body.indexOf('=');
67
+ const name = eq >= 0 ? body.slice(0, eq) : body;
68
+ let value;
69
+ if (eq >= 0)
70
+ value = body.slice(eq + 1);
71
+ else if (BOOLEAN_FLAGS.has(name) || !(name in FLAGS))
72
+ value = true;
73
+ else {
74
+ const next = argv[i + 1];
75
+ if (next === undefined)
76
+ throw new ToolError('bad_args', `--${name} needs a value.`);
77
+ value = next;
78
+ i++;
79
+ }
80
+ if (REPEATABLE_FLAGS.has(name))
81
+ (args.multi[name] ??= []).push(String(value));
82
+ else
83
+ args.flags[name] = value;
84
+ }
85
+ else {
86
+ args.positional.push(a);
87
+ }
88
+ }
89
+ return args;
90
+ }
91
+ const HELP = renderHelp(VERSION, CONFIG_FILE);
92
+ function findRoot(start) {
93
+ let dir = resolve(start);
94
+ for (;;) {
95
+ if (existsSync(join(dir, CONFIG_FILE)))
96
+ return dir;
97
+ const parent = dirname(dir);
98
+ if (parent === dir)
99
+ return resolve(start);
100
+ dir = parent;
101
+ }
102
+ }
103
+ async function readStdin() {
104
+ if (process.stdin.isTTY)
105
+ return '';
106
+ const chunks = [];
107
+ for await (const chunk of process.stdin)
108
+ chunks.push(chunk);
109
+ return Buffer.concat(chunks).toString('utf8');
110
+ }
111
+ async function readPasteInteractive() {
112
+ process.stderr.write('Paste a cURL command or URL. You can add "response:" and a JSON sample after it.\nFinish with Ctrl-D (Ctrl-Z then Enter on Windows).\n');
113
+ const chunks = [];
114
+ for await (const chunk of process.stdin)
115
+ chunks.push(chunk);
116
+ // Ctrl-D leaves the cursor at the end of the pasted text.
117
+ process.stderr.write('\n');
118
+ return Buffer.concat(chunks).toString('utf8');
119
+ }
120
+ /**
121
+ * Opens the terminal directly. Standard input may already be used up (a paste
122
+ * ended with Ctrl-D, or input piped from a file), so questions are asked on
123
+ * the terminal itself.
124
+ */
125
+ function openTerminal() {
126
+ try {
127
+ const fd = openSync(process.platform === 'win32' ? 'CONIN$' : '/dev/tty', 'r');
128
+ return new ReadStream(fd);
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ /** Asks a yes/no question on the terminal. Returns null when there is no terminal to ask on. */
135
+ async function confirm(question) {
136
+ const input = openTerminal();
137
+ if (!input)
138
+ return null;
139
+ const rl = createInterface({ input, output: process.stderr });
140
+ try {
141
+ const answer = await new Promise((resolve) => {
142
+ rl.on('close', () => resolve(null));
143
+ rl.question(`${question} (y/N) `).then(resolve, () => resolve(null));
144
+ });
145
+ if (answer === null) {
146
+ process.stderr.write('\n');
147
+ return false;
148
+ }
149
+ const a = answer.trim().toLowerCase();
150
+ return a === 'y' || a === 'yes';
151
+ }
152
+ finally {
153
+ rl.close();
154
+ input.destroy();
155
+ }
156
+ }
157
+ /** Asks the user to pick one option on the terminal. Returns the index, or null without a terminal. */
158
+ async function choose(question, options, defaultIndex) {
159
+ const input = openTerminal();
160
+ if (!input)
161
+ return null;
162
+ const rl = createInterface({ input, output: process.stderr });
163
+ try {
164
+ process.stderr.write(`${question}\n`);
165
+ options.forEach((o, i) => process.stderr.write(` ${i + 1}) ${o}\n`));
166
+ for (let attempt = 0; attempt < 3; attempt++) {
167
+ const answer = await new Promise((resolve) => {
168
+ rl.on('close', () => resolve(null));
169
+ rl.question(`Choose 1-${options.length} [${defaultIndex + 1}]: `).then(resolve, () => resolve(null));
170
+ });
171
+ if (answer === null)
172
+ return null;
173
+ const t = answer.trim();
174
+ if (t === '')
175
+ return defaultIndex;
176
+ const n = Number(t);
177
+ if (Number.isInteger(n) && n >= 1 && n <= options.length)
178
+ return n - 1;
179
+ process.stderr.write(`Please type a number from 1 to ${options.length}.\n`);
180
+ }
181
+ return null;
182
+ }
183
+ finally {
184
+ rl.close();
185
+ input.destroy();
186
+ }
187
+ }
188
+ /** Asks for a line of text on the terminal. Returns null without a terminal. */
189
+ async function ask(question, validate) {
190
+ const input = openTerminal();
191
+ if (!input)
192
+ return null;
193
+ const rl = createInterface({ input, output: process.stderr });
194
+ try {
195
+ for (let attempt = 0; attempt < 3; attempt++) {
196
+ const answer = await new Promise((resolve) => {
197
+ rl.on('close', () => resolve(null));
198
+ rl.question(question).then(resolve, () => resolve(null));
199
+ });
200
+ if (answer === null)
201
+ return null;
202
+ const problem = validate(answer.trim());
203
+ if (!problem)
204
+ return answer.trim();
205
+ process.stderr.write(`${problem}\n`);
206
+ }
207
+ return null;
208
+ }
209
+ finally {
210
+ rl.close();
211
+ input.destroy();
212
+ }
213
+ }
214
+ const interactive = (out) => !out.json && !!process.stderr.isTTY;
215
+ function makeOut(flags) {
216
+ const json = flags.json === true;
217
+ const color = !json && !flags['no-color'] && !!process.stdout.isTTY && !process.env.NO_COLOR;
218
+ return {
219
+ json,
220
+ color,
221
+ print: (t) => {
222
+ if (!json)
223
+ process.stdout.write(t.endsWith('\n') ? t : t + '\n');
224
+ },
225
+ warn: (t) => {
226
+ if (!json)
227
+ process.stderr.write((color ? `\x1b[33m! ${t}\x1b[0m` : `! ${t}`) + '\n');
228
+ },
229
+ result: (obj) => {
230
+ if (json)
231
+ process.stdout.write(JSON.stringify({ ok: true, ...obj }, null, 2) + '\n');
232
+ },
233
+ };
234
+ }
235
+ function printChangeSet(out, cs) {
236
+ for (const s of cs.summary)
237
+ out.print(`• ${s}`);
238
+ for (const w of cs.warnings)
239
+ out.warn(w);
240
+ if (cs.changes.length)
241
+ out.print(`\n${changeSummaryLine(cs)}\n\n${renderDiff(cs, out.color)}`);
242
+ }
243
+ function printApplied(out, r) {
244
+ out.print(`Applied: ${r.files.join(', ')}`);
245
+ if (r.formatted)
246
+ out.print('Formatted with the project formatter.');
247
+ if (r.formatNote)
248
+ out.warn(r.formatNote);
249
+ if (r.verified)
250
+ out.print('Checked with the project analyzer: no new errors.');
251
+ if (r.verifyNote)
252
+ out.warn(r.verifyNote);
253
+ out.print('Undo with: codz undo');
254
+ }
255
+ async function handleChange(root, out, flags, cs, adapter, opts = {}) {
256
+ if (cs.changes.length === 0) {
257
+ out.print(cs.summary.length ? cs.summary.join('\n') : 'Nothing to change.');
258
+ for (const w of cs.warnings)
259
+ out.warn(w);
260
+ out.result({ applied: false, changeSet: changeSetJson(cs) });
261
+ return 0;
262
+ }
263
+ if (flags['dry-run']) {
264
+ printChangeSet(out, cs);
265
+ out.result({ applied: false, dryRun: true, changeSet: changeSetJson(cs) });
266
+ return 0;
267
+ }
268
+ if (flags.plan) {
269
+ savePlan(root, cs);
270
+ printChangeSet(out, cs);
271
+ out.print(`\nSaved plan ${cs.id}. Apply exactly this change with: codz apply ${cs.id}`);
272
+ out.result({ applied: false, planId: cs.id, changeSet: changeSetJson(cs) });
273
+ return 0;
274
+ }
275
+ printChangeSet(out, cs);
276
+ if (flags.open === true && !out.json && !openDiffInEditor(root, cs))
277
+ out.warn('Set $EDITOR (or $VISUAL) to open the diff in your editor.');
278
+ let go = flags.yes === true || flags.y === true;
279
+ if (!go && !out.json && process.stderr.isTTY)
280
+ go = (await confirm('\nApply these changes?')) ?? false;
281
+ if (!go) {
282
+ savePlan(root, cs);
283
+ out.print(`\nNot applied. To apply exactly this diff later: codz apply ${cs.id} (or re-run with --yes)`);
284
+ out.result({ applied: false, planId: cs.id, changeSet: changeSetJson(cs) });
285
+ return 0;
286
+ }
287
+ const r = applyChangeSet(root, cs, { allowDirty: opts.allowDirty || flags['allow-dirty'] === true, adapter, format: !flags['no-format'], verify: opts.verify ?? !flags['no-verify'] });
288
+ printApplied(out, r);
289
+ let commit;
290
+ if (flags.commit === true) {
291
+ commit = commitChange(root, cs, typeof flags['commit-message'] === 'string' ? flags['commit-message'] : undefined);
292
+ out.print(`Committed ${commit}. Undo with: codz undo (then git reset --soft HEAD~1 if you want the commit gone too)`);
293
+ }
294
+ const extra = opts.afterApply?.() ?? {};
295
+ out.result({ applied: true, id: cs.id, ...r, ...(commit ? { commit } : {}), ...extra, changeSet: changeSetJson(cs, false) });
296
+ return 0;
297
+ }
298
+ const URL_CHECK = (v) => baseUrlProblem(v);
299
+ /** Installs dependencies a change added (flutter pub get, npm install), or says how. Returns the command that ran. */
300
+ function installDependencies(root, adapter, out) {
301
+ const cmd = adapter.afterSetup?.(root);
302
+ if (cmd) {
303
+ out.print(`Running ${cmd.label}...`);
304
+ const r = runTool(cmd.cmd, cmd.args, { cwd: root });
305
+ if (r.status === 0) {
306
+ out.print(`${cmd.label}: done.`);
307
+ return cmd.label;
308
+ }
309
+ out.warn(`${cmd.label} failed. Run it yourself: ${(r.stderr || r.stdout || '').trim().split('\n').slice(-1)[0] ?? ''}`);
310
+ return undefined;
311
+ }
312
+ out.warn(adapter.id === 'android' ? 'Gradle dependencies and plugins were added. Sync the project in Android Studio (or run ./gradlew build).' : adapter.id === 'flutter' ? 'Dependencies were added. Install them with `flutter pub get`.' : 'Dependencies were added. Install them with `npm install` (or your package manager).');
313
+ return undefined;
314
+ }
315
+ /** Runs setup as one change set, then installs dependencies when they changed. */
316
+ async function runSetup(d, out, flags, architecture, baseUrl) {
317
+ const { changeSet, plan } = planSetup(d.root, d.adapter, d.map, { architecture, baseUrl });
318
+ return handleChange(d.root, out, flags, changeSet, d.adapter, {
319
+ // A fresh project is often not committed yet; undo still works from codz's backups.
320
+ allowDirty: true,
321
+ // New packages are not installed until after apply, so the analyzer would fail on their imports.
322
+ verify: false,
323
+ afterApply: () => {
324
+ const installed = plan.dependenciesChanged ? installDependencies(d.root, d.adapter, out) : undefined;
325
+ out.print('\nReady. Add your first endpoint with: codz api add');
326
+ return { installed };
327
+ },
328
+ });
329
+ }
330
+ /** codz create: asks what it needs (or takes flags), then creates and sets up the project. */
331
+ /** What the starter commands need from the CLI: flags, output and (in a terminal) questions. */
332
+ function starterCtx(args, out, cwd) {
333
+ const canAsk = interactive(out) && args.flags.yes !== true && args.flags.y !== true;
334
+ return {
335
+ sub: args.positional[0] === 'apply' ? undefined : args.positional[1],
336
+ positional: args.positional,
337
+ flags: args.flags,
338
+ multi: args.multi,
339
+ cwd,
340
+ version: VERSION,
341
+ io: { json: out.json, print: out.print, warn: out.warn, result: out.result, ask: canAsk ? ask : null, choose: canAsk ? choose : null, confirm: canAsk ? confirm : null },
342
+ };
343
+ }
344
+ async function runCreate(nameArg, flags, out, parent) {
345
+ const str = (k) => (typeof flags[k] === 'string' ? flags[k] : '');
346
+ const canAsk = interactive(out) && flags.yes !== true && flags.y !== true;
347
+ const need = (what, flag) => new ToolError('bad_args', `Which ${what}? Pass --${flag}, or run codz create in a terminal to be asked.`);
348
+ let typeId = str('type');
349
+ if (!typeId) {
350
+ if (!canAsk)
351
+ throw need('project type', 'type');
352
+ const i = await choose('Which project do you want to create?', PROJECT_TYPES.map((t) => `${t.label}: ${t.description}`), 0);
353
+ if (i === null)
354
+ return 1;
355
+ typeId = PROJECT_TYPES[i].id;
356
+ }
357
+ const type = projectType(typeId);
358
+ let architecture = str('architecture');
359
+ if (!architecture) {
360
+ if (!canAsk)
361
+ throw need('architecture', 'architecture');
362
+ const i = await choose(`Which architecture for ${type.label}?`, type.architectures.map((a) => `${a.label}: ${a.description}`), 0);
363
+ if (i === null)
364
+ return 1;
365
+ architecture = type.architectures[i].id;
366
+ }
367
+ let name = nameArg ?? '';
368
+ if (!name) {
369
+ if (!canAsk)
370
+ throw need('project name', 'name (codz create <name>)');
371
+ name = (await ask(`Project name: `, (v) => type.checkName(v))) ?? '';
372
+ if (!name)
373
+ return 1;
374
+ }
375
+ let org = str('org') || 'com.example';
376
+ if (type.needsOrg && !str('org') && canAsk)
377
+ org = (await ask(`Organization (reverse domain) [com.example]: `, (v) => (!v || /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(v) ? null : 'Use reverse-domain form, for example com.mycompany'))) || 'com.example';
378
+ let baseUrl = str('base-url');
379
+ if (!baseUrl && architecture !== 'none' && canAsk)
380
+ baseUrl = (await ask('API base URL (Enter to skip): ', URL_CHECK)) ?? '';
381
+ let tailwind = flags.tailwind === true;
382
+ if (type.options.includes('tailwind') && flags.tailwind === undefined && canAsk)
383
+ tailwind = (await confirm('Use Tailwind CSS?')) ?? false;
384
+ let flavor = str('flavor') === 'bare' ? 'bare' : 'expo';
385
+ if (type.options.includes('flavor') && !str('flavor') && canAsk)
386
+ flavor = (await choose('Expo or the React Native CLI?', ['Expo (recommended): runs in Expo Go, native folders when you need them', 'React Native CLI: android/ and ios/ folders from the start'], 0)) === 1 ? 'bare' : 'expo';
387
+ let primary = str('primary');
388
+ if (!primary && canAsk)
389
+ primary = (await ask('Primary color, #rrggbb (Enter to skip): ', (v) => (!v || /^#?[0-9a-fA-F]{6}$/.test(v) ? null : 'Use a color like #6750A4'))) ?? '';
390
+ const opts = { type: type.id, name, parent, architecture, baseUrl, org, tailwind, flavor, primary: primary || undefined, offline: flags.offline === true, install: flags['no-install'] !== true, git: flags['no-git'] !== true };
391
+ const plan = describeCreate(opts);
392
+ if (flags['dry-run'] === true) {
393
+ out.print(plan.map((s) => `• ${s}`).join('\n'));
394
+ out.result({ dryRun: true, steps: plan, options: opts });
395
+ return 0;
396
+ }
397
+ if (canAsk) {
398
+ out.print(`\n${plan.map((s) => `• ${s}`).join('\n')}`);
399
+ if (!(await confirm('\nCreate it?'))) {
400
+ out.print('Not created.');
401
+ return 0;
402
+ }
403
+ }
404
+ const r = await createProject(opts, (l) => out.print(l));
405
+ out.print(r.steps.map((s) => `• ${s}`).join('\n'));
406
+ for (const w of r.warnings)
407
+ out.warn(w);
408
+ out.print(`\nNext: cd ${name} && codz api add`);
409
+ out.result({ ...r });
410
+ return 0;
411
+ }
412
+ /** Picks an architecture and base URL from flags or by asking. Returns null when the user skips. */
413
+ async function chooseSetup(options, out, flags, defaultBaseUrl, onlyOne = false) {
414
+ let architecture = typeof flags.architecture === 'string' ? flags.architecture : '';
415
+ let baseUrl = typeof flags['base-url'] === 'string' ? flags['base-url'] : '';
416
+ // With only one architecture there is nothing to choose.
417
+ if (onlyOne && !architecture && options.length === 1 && !interactive(out))
418
+ architecture = options[0].id;
419
+ if (!architecture) {
420
+ if (!interactive(out))
421
+ return null;
422
+ const labels = [...options.map((o) => `${o.label}${o.recommended ? ' (recommended)' : ''}: ${o.description}`), 'Skip: set it up myself'];
423
+ const picked = await choose('\nNo API layer was found in this project. Which architecture should codz set up?', labels, 0);
424
+ if (picked === null || picked === options.length)
425
+ return null;
426
+ architecture = options[picked].id;
427
+ if (!baseUrl) {
428
+ const hint = defaultBaseUrl ? ` [${defaultBaseUrl}]` : ' (Enter to skip)';
429
+ const answer = await ask(`API base URL${hint}: `, URL_CHECK);
430
+ baseUrl = answer || defaultBaseUrl;
431
+ }
432
+ }
433
+ return { architecture, baseUrl };
434
+ }
435
+ export async function main(argv) {
436
+ // Shell completion: the words may be half-typed flags, so they skip normal parsing.
437
+ if (argv[0] === '__complete') {
438
+ try {
439
+ const words = argv.slice(1);
440
+ const cwdAt = words.indexOf('--cwd');
441
+ const root = findRoot(resolve(cwdAt !== -1 && words[cwdAt + 1] ? words[cwdAt + 1] : process.cwd()));
442
+ let adapter = genericAdapter;
443
+ try {
444
+ adapter = adapterFor(root).adapter;
445
+ }
446
+ catch {
447
+ // Broken config: commands and flags still complete.
448
+ }
449
+ const out = await complete(words.length ? words : [''], root, adapter);
450
+ if (out.length)
451
+ process.stdout.write(out.join('\n') + '\n');
452
+ }
453
+ catch {
454
+ // Completion never prints errors.
455
+ }
456
+ return 0;
457
+ }
458
+ let args;
459
+ try {
460
+ args = parseArgs(argv);
461
+ }
462
+ catch (e) {
463
+ process.stderr.write(`${e.message}\n`);
464
+ return 2;
465
+ }
466
+ const out = makeOut(args.flags);
467
+ const started = Date.now();
468
+ const event = (ok, code) => {
469
+ const [cmd, sub] = args.positional;
470
+ if (!cmd || ['report', 'completion', 'help', 'mcp'].includes(cmd) || args.flags.help || args.flags.version)
471
+ return;
472
+ const spec = findCommand(cmd, sub);
473
+ const name = spec ? (spec.sub ? `${spec.cmd} ${spec.sub}` : spec.cmd) : 'unknown';
474
+ const start = resolve(typeof args.flags.cwd === 'string' ? args.flags.cwd : process.cwd());
475
+ recordEvent(typeof args.flags.root === 'string' ? resolve(args.flags.root) : findRoot(start), { t: new Date().toISOString(), cmd: name, ok, ...(code ? { code } : {}), ms: Date.now() - started, v: VERSION });
476
+ };
477
+ try {
478
+ const code = await run(args, out);
479
+ event(code === 0, code === 0 ? undefined : `exit_${code}`);
480
+ return code;
481
+ }
482
+ catch (e) {
483
+ event(false, e instanceof ToolError ? e.code : 'internal');
484
+ if (e instanceof ToolError) {
485
+ // Every refusal says what to do next; errors written without their own hint get one by kind.
486
+ const [c0, c1] = args.positional;
487
+ const spec = c0 ? findCommand(c0, c1) : undefined;
488
+ const usage = spec ? `codz ${spec.cmd}${spec.sub ? ` ${spec.sub}` : ''} --help` : 'codz --help';
489
+ const hint = e.hint ?? (e.code === 'not_supported' || e.code === 'unsupported_setup' ? `codz explain shows what codz found in this project; README "What it supports" lists the setups each command handles.` : `See the options and examples with: ${usage}`);
490
+ if (out.json)
491
+ process.stdout.write(JSON.stringify({ ok: false, error: { code: e.code, message: e.message, hint } }, null, 2) + '\n');
492
+ else {
493
+ process.stderr.write((out.color ? `\x1b[31m✗ ${e.message}\x1b[0m` : `✗ ${e.message}`) + '\n');
494
+ process.stderr.write(` ${hint}\n`);
495
+ }
496
+ return 1;
497
+ }
498
+ // Problems with the machine, not codz: say what to do instead of calling it a bug.
499
+ const sys = SYSTEM_ERRORS[e?.code ?? ''];
500
+ if (sys) {
501
+ const message = e instanceof Error ? e.message : String(e);
502
+ if (out.json)
503
+ process.stdout.write(JSON.stringify({ ok: false, error: { code: sys.code, message, hint: sys.hint } }, null, 2) + '\n');
504
+ else {
505
+ process.stderr.write((out.color ? `\x1b[31m✗ ${sys.say}\x1b[0m` : `✗ ${sys.say}`) + '\n');
506
+ process.stderr.write(` ${sys.hint}\n (${message})\n`);
507
+ }
508
+ return 1;
509
+ }
510
+ // An unexpected error still answers in the format the caller asked for.
511
+ const message = e instanceof Error ? e.message : String(e);
512
+ if (out.json)
513
+ process.stdout.write(JSON.stringify({ ok: false, error: { code: 'internal', message, hint: 'This is a codz bug. Run again with CODZ_DEBUG=1 for the stack trace and report it.' } }, null, 2) + '\n');
514
+ else {
515
+ process.stderr.write((out.color ? `\x1b[31m✗ ${message}\x1b[0m` : `✗ ${message}`) + '\n');
516
+ process.stderr.write(process.env.CODZ_DEBUG && e instanceof Error && e.stack ? `${e.stack}\n` : ' This is a codz bug. Run again with CODZ_DEBUG=1 for the stack trace and report it.\n');
517
+ }
518
+ return 1;
519
+ }
520
+ }
521
+ async function run(args, out) {
522
+ const { flags, positional } = args;
523
+ if (flags.version || flags.v) {
524
+ out.print(VERSION);
525
+ out.result({ version: VERSION });
526
+ return 0;
527
+ }
528
+ const [cmd, sub] = positional;
529
+ if (cmd === 'help' && positional[1]) {
530
+ const spec = findCommand(positional[1], positional[2]);
531
+ if (!spec)
532
+ throw new ToolError('bad_args', `Unknown command "${positional.slice(1).join(' ')}".`, 'See every command with: codz --help');
533
+ out.print(renderCommandHelp(spec));
534
+ return 0;
535
+ }
536
+ if (cmd && (flags.help || flags.h)) {
537
+ const spec = findCommand(cmd, sub);
538
+ if (spec) {
539
+ out.print(renderCommandHelp(spec));
540
+ return 0;
541
+ }
542
+ }
543
+ if (!cmd || flags.help || flags.h || cmd === 'help') {
544
+ out.print(HELP);
545
+ return cmd || flags.help || flags.h ? 0 : 1;
546
+ }
547
+ // A mistyped option (--dryrun) must not be ignored: it could apply a change the user meant to preview.
548
+ const spec = findCommand(cmd, sub);
549
+ if (spec) {
550
+ const allowed = allowedFlags(spec);
551
+ for (const name of [...Object.keys(flags), ...Object.keys(args.multi)]) {
552
+ if (allowed.includes(name))
553
+ continue;
554
+ const near = closest(name, allowed);
555
+ throw new ToolError('bad_args', `codz ${spec.cmd}${spec.sub ? ` ${spec.sub}` : ''} has no option --${name}.`, near ? `Did you mean --${near}? (codz ${spec.cmd}${spec.sub ? ` ${spec.sub}` : ''} --help lists the options)` : `See its options with: codz ${spec.cmd}${spec.sub ? ` ${spec.sub}` : ''} --help`);
556
+ }
557
+ }
558
+ const start = resolve(typeof flags.cwd === 'string' ? flags.cwd : process.cwd());
559
+ // A preview or a health check must not leave files behind, not even the scan cache.
560
+ setCacheWrites(!(flags['dry-run'] === true || cmd === 'check' || (cmd === 'doctor' && flags.fix !== true)));
561
+ if (cmd === 'create')
562
+ return runCreate(positional[1], flags, out, start);
563
+ // Starter commands: the only ones with network code, loaded only when used.
564
+ if (cmd === 'new' || cmd === 'starters' || cmd === 'login' || cmd === 'logout' || cmd === 'whoami') {
565
+ const starters = await import("./starters/commands.js");
566
+ return starters.runStarterCommand(cmd, starterCtx(args, out, start));
567
+ }
568
+ if (cmd === 'apply' && sub && /^[\w-]+$/.test(sub) && existsSync(join(process.env.CODZ_HOME || join(homedir(), '.codz'), 'starters', 'plans', `${sub}.json`))) {
569
+ const starters = await import("./starters/commands.js");
570
+ return (await starters.applyStarterPlan(sub, starterCtx(args, out, start))) ?? 1;
571
+ }
572
+ if (cmd === 'init' || cmd === 'setup') {
573
+ const d = await detectProject(start, { root: typeof flags.root === 'string' ? flags.root : undefined, force: flags.force === true });
574
+ const m = d.map;
575
+ const options = d.adapter.architectures?.(m) ?? [];
576
+ const defaultBaseUrl = m.baseUrls[0]?.value ?? '';
577
+ if (cmd === 'setup') {
578
+ if (!options.length) {
579
+ throw new ToolError('already_set_up', d.adapter.architectures ? 'This project already has an API layer, so setup is not needed.' : `setup is not available for the ${d.adapter.id} adapter.`, 'Run codz explain to see it, then codz api add.');
580
+ }
581
+ const choice = await chooseSetup(options, out, flags, defaultBaseUrl, true);
582
+ if (!choice) {
583
+ throw new ToolError('bad_args', 'Which architecture?', `Pass --architecture with one of: ${options.map((o) => o.id).join(', ')} (and --base-url <url>).`);
584
+ }
585
+ return runSetup(d, out, flags, choice.architecture, choice.baseUrl);
586
+ }
587
+ out.print(`Detected ${d.label} → ${d.adapter.id} adapter${d.adapter.id === 'generic' ? ' (structure, locales and config only)' : ''}`);
588
+ out.print(`Project root: ${d.root}`);
589
+ out.print(`Found ${m.routes.length} routes, ${m.endpoints.length} endpoints, ${m.models.length} models, ${m.locales.languages.length} languages.`);
590
+ for (const n of m.notes)
591
+ out.warn(n);
592
+ // A project with no API layer: offer to create one, in the same change as the config file.
593
+ if (options.length && (!d.configExists || flags.force || typeof flags.architecture === 'string')) {
594
+ const choice = await chooseSetup(options, out, flags, defaultBaseUrl);
595
+ if (choice)
596
+ return runSetup(d, out, flags, choice.architecture, choice.baseUrl);
597
+ }
598
+ const files = initFiles(d.root, d.adapter, m.conventions);
599
+ const keepConfig = d.configExists && !flags.force;
600
+ const w = writeInitFiles(d.root, files, keepConfig);
601
+ if (w.configWritten)
602
+ out.print(`Wrote ${CONFIG_FILE}. Review the detected conventions once and correct anything wrong, then commit it.`);
603
+ else
604
+ out.print(`${CONFIG_FILE} already exists; kept your values (use --force to detect again).`);
605
+ if (w.gitignoreUpdated)
606
+ out.print('Added .codz/ to .gitignore.');
607
+ if (options.length) {
608
+ out.print(`\nNo API layer was found. Create one with: codz setup --architecture ${options[0].id} --base-url https://api.example.com`);
609
+ out.print(`Architectures: ${options.map((o) => o.id).join(', ')}`);
610
+ }
611
+ else {
612
+ out.print('Next: codz explain');
613
+ }
614
+ out.result({
615
+ root: d.root,
616
+ framework: d.adapter.id,
617
+ detected: d.label,
618
+ configWritten: w.configWritten,
619
+ gitignoreUpdated: w.gitignoreUpdated,
620
+ counts: { routes: m.routes.length, endpoints: m.endpoints.length, models: m.models.length, languages: m.locales.languages.length },
621
+ conventions: m.conventions,
622
+ architectures: options,
623
+ notes: m.notes,
624
+ });
625
+ return 0;
626
+ }
627
+ const root = typeof flags.root === 'string' ? resolve(flags.root) : findRoot(start);
628
+ // Commands that must work even when the config file is broken.
629
+ if (cmd === 'completion') {
630
+ if (!sub || !SHELLS.includes(sub))
631
+ throw new ToolError('bad_args', 'Which shell? Usage: codz completion bash|zsh|fish', 'bash: eval "$(codz completion bash)" in ~/.bashrc; zsh: source <(codz completion zsh) in ~/.zshrc; fish: codz completion fish > ~/.config/fish/completions/codz.fish');
632
+ process.stdout.write(completionScript(sub));
633
+ return 0;
634
+ }
635
+ if (cmd === 'mcp')
636
+ return serveMcp(root, VERSION);
637
+ if (cmd === 'doctor' && flags.setup === true) {
638
+ let a = genericAdapter;
639
+ try {
640
+ a = adapterFor(root).adapter;
641
+ }
642
+ catch {
643
+ // A broken config is reported by the config_valid check.
644
+ }
645
+ const r = runSetupDoctor(root, a);
646
+ out.print(renderSetupDoctor(r, `codz doctor --setup (${a.id}@${a.version}, ${root})`, out.color));
647
+ out.result({ checks: r.checks, summary: r.summary });
648
+ return r.summary.fail ? 1 : 0;
649
+ }
650
+ if (cmd === 'config' && sub === 'check') {
651
+ const r = configCheck(root);
652
+ if (!r.issues.length)
653
+ out.print(`${r.file}: no problems found.`);
654
+ for (const i of r.issues)
655
+ out.print(`${i.level === 'error' ? '✗' : '!'} ${i.key ? `${i.key}: ` : ''}${i.message}${i.hint ? `\n → ${i.hint}` : ''}`);
656
+ const errors = r.issues.filter((i) => i.level === 'error').length;
657
+ out.result({ file: r.file, issues: r.issues, valid: errors === 0 });
658
+ return errors ? 1 : 0;
659
+ }
660
+ // api diff compares two files; it works anywhere and uses the project only for the impact lines.
661
+ if (cmd === 'api' && sub === 'diff') {
662
+ const [oldFile, newFile] = [positional[2], positional[3]];
663
+ if (!oldFile || !newFile)
664
+ throw new ToolError('bad_args', 'Usage: codz api diff <old spec> <new spec> [--format text|md|json|sarif] [--report <file>] [--fail-on breaking|warning|any|none]');
665
+ const format = typeof flags.format === 'string' ? flags.format : 'text';
666
+ if (!['text', 'md', 'markdown', 'json', 'sarif'].includes(format))
667
+ throw new ToolError('bad_args', `--format must be text, md, json or sarif, got "${format}".`);
668
+ const failOn = typeof flags['fail-on'] === 'string' ? flags['fail-on'] : 'breaking';
669
+ if (!['breaking', 'warning', 'any', 'none'].includes(failOn))
670
+ throw new ToolError('bad_args', `--fail-on must be breaking, warning, any or none, got "${failOn}".`);
671
+ let projectAdapter = null;
672
+ if (existsSync(join(root, CONFIG_FILE))) {
673
+ try {
674
+ projectAdapter = adapterFor(root).adapter;
675
+ }
676
+ catch {
677
+ projectAdapter = null;
678
+ }
679
+ }
680
+ const split = (xs) => xs?.flatMap((x) => (typeof x === 'string' ? x.split(',') : [])).map((x) => x.trim()).filter(Boolean) ?? [];
681
+ const report = await planApiDiff(projectAdapter ? root : null, projectAdapter, resolve(oldFile), resolve(newFile), { tags: split(args.multi.tag), paths: split(args.multi.path) });
682
+ const text = format === 'json' ? `${JSON.stringify(report, null, 2)}\n` : format === 'sarif' ? renderDiffSarif(report, projectAdapter ? root : null) : format === 'text' ? `${renderDiffText(report)}\n` : renderDiffMarkdown(report);
683
+ if (typeof flags.report === 'string') {
684
+ writeFileSync(resolve(flags.report), text);
685
+ out.print(renderDiffText(report));
686
+ out.print(`Report written to ${flags.report}`);
687
+ }
688
+ else if (format === 'text')
689
+ out.print(text.trimEnd());
690
+ else
691
+ process.stdout.write(text);
692
+ out.result({ ...report, failed: diffFails(report, failOn) });
693
+ return diffFails(report, failOn) ? 1 : 0;
694
+ }
695
+ const { adapter } = adapterFor(root);
696
+ switch (cmd) {
697
+ case 'report': {
698
+ const { map } = await loadMap(root, adapter, loadConfig(root).conventions);
699
+ const r = buildReport(root, adapter, map, VERSION);
700
+ out.print(renderReport(r));
701
+ out.result({ ...r });
702
+ return 0;
703
+ }
704
+ case 'explain': {
705
+ const cfg = loadConfig(root);
706
+ if (!cfg.exists)
707
+ out.warn(`No ${CONFIG_FILE} yet; showing detected values. Run codz init to save them.`);
708
+ const { map } = await loadMap(root, adapter, cfg.conventions, { force: flags.rescan === true });
709
+ out.print(renderExplain(map, adapter, out.color));
710
+ out.result({ map, supportedSetup: adapter.supportedSetup });
711
+ return 0;
712
+ }
713
+ case 'mock': {
714
+ if (sub !== 'add')
715
+ throw new ToolError('bad_args', `Unknown command "mock ${sub ?? ''}".`, 'Usage: codz mock add <endpoint> [--sample file.json] | codz mock add --all');
716
+ const cs = await planMockAdd(root, adapter, { endpoints: positional.slice(2), all: flags.all === true, sample: typeof flags.sample === 'string' ? flags.sample : args.multi.sample?.[0], keepValues: flags['keep-values'] === true, force: flags.force === true });
717
+ return handleChange(root, out, flags, cs, adapter);
718
+ }
719
+ case 'locale': {
720
+ if (sub === 'add') {
721
+ const lang = positional[2];
722
+ if (!lang)
723
+ throw new ToolError('bad_args', 'Usage: codz locale add <lang> [--from <lang>]');
724
+ const cs = await planLocaleAdd(root, adapter, lang, { from: typeof flags.from === 'string' ? flags.from : undefined });
725
+ return handleChange(root, out, flags, cs, adapter);
726
+ }
727
+ if (sub === 'key') {
728
+ const key = positional[2];
729
+ const text = positional.slice(3).join(' ');
730
+ if (!key || !text)
731
+ throw new ToolError('bad_args', 'Usage: codz locale key <key> "<text>" [--value hi="..."]');
732
+ const values = new Map();
733
+ for (const v of args.multi.value ?? []) {
734
+ const i = v.indexOf('=');
735
+ if (i < 1)
736
+ throw new ToolError('bad_args', `--value needs lang=text, got "${v}".`);
737
+ values.set(v.slice(0, i), v.slice(i + 1));
738
+ }
739
+ const cs = await planLocaleKey(root, adapter, key, text, { values, force: flags.force === true });
740
+ return handleChange(root, out, flags, cs, adapter);
741
+ }
742
+ throw new ToolError('bad_args', `Unknown command "locale ${sub ?? ''}".`, 'Usage: codz locale add <lang> | codz locale key <key> "<text>"');
743
+ }
744
+ case 'config': {
745
+ if (!sub || sub === 'show' || sub === 'list') {
746
+ const v = await configView(root, adapter);
747
+ out.print(`${v.file ?? `(no ${CONFIG_FILE} yet: showing detected values)`} framework: ${v.framework}`);
748
+ const w = Math.max(0, ...v.rows.map((r) => r.key.length));
749
+ for (const r of v.rows)
750
+ out.print(` ${r.key.padEnd(w)} ${r.value === '' ? "''" : r.value}${r.source === 'detected' ? ' (detected)' : ''}`);
751
+ out.result(v);
752
+ return 0;
753
+ }
754
+ if (sub === 'get') {
755
+ const key = positional[2];
756
+ if (!key)
757
+ throw new ToolError('bad_args', 'Usage: codz config get <key>');
758
+ const v = await configView(root, adapter);
759
+ const row = v.rows.find((r) => r.key === key);
760
+ if (!row)
761
+ throw new ToolError('unknown_key', `"${key}" is not a ${adapter.id} convention.`, `Known keys: ${v.rows.map((r) => r.key).join(', ')}`);
762
+ out.print(row.value);
763
+ out.result({ ...row });
764
+ return 0;
765
+ }
766
+ if (sub === 'set') {
767
+ const key = positional[2];
768
+ const value = positional.length > 3 ? positional.slice(3).join(' ') : undefined;
769
+ if (!key || value === undefined)
770
+ throw new ToolError('bad_args', 'Usage: codz config set <key> <value>', 'Use \'\' as the value to go back to detecting it.');
771
+ const cs = planConfigSet(root, adapter, key, value === "''" || value === '""' ? '' : value, { force: flags.force === true });
772
+ return handleChange(root, out, flags, cs, adapter, { allowDirty: true, verify: false });
773
+ }
774
+ throw new ToolError('bad_args', `Unknown command "config ${sub}".`, 'Usage: codz config [show] | get <key> | set <key> <value> | check');
775
+ }
776
+ case 'plans': {
777
+ if (!sub || sub === 'list') {
778
+ const rows = listPlans(root);
779
+ if (!rows.length)
780
+ out.print('No saved plans. Make one with --plan on any command that changes code.');
781
+ for (const p of rows)
782
+ out.print(`${p.id} ${p.status.padEnd(7)} ${p.skill.padEnd(12)} ${p.summary}`);
783
+ out.result({ plans: rows });
784
+ return 0;
785
+ }
786
+ if (sub === 'show') {
787
+ const id = positional[2];
788
+ if (!id)
789
+ throw new ToolError('bad_args', 'Which plan? Usage: codz plans show <plan-id>', 'List them with: codz plans list');
790
+ const { plan, status } = showPlan(root, id);
791
+ out.print(`${plan.id} ${plan.skill} (${status}${status === 'stale' ? ': a file changed since; run the command again' : status === 'ready' ? `: apply with codz apply ${plan.id}` : ''})`);
792
+ printChangeSet(out, plan);
793
+ out.result({ status, ...changeSetJson(plan) });
794
+ return 0;
795
+ }
796
+ if (sub === 'clean') {
797
+ const removed = cleanPlans(root, { all: flags.all === true });
798
+ out.print(removed.length ? `Removed ${removed.length} plan${removed.length === 1 ? '' : 's'}: ${removed.join(', ')}` : 'Nothing to remove (only applied or stale plans are removed; --all removes every plan).');
799
+ out.result({ removed });
800
+ return 0;
801
+ }
802
+ throw new ToolError('bad_args', `Unknown command "plans ${sub}".`, 'Usage: codz plans [list] | show <plan-id> | clean [--all]');
803
+ }
804
+ case 'doctor': {
805
+ const only = (args.multi.only ?? []).flatMap((x) => x.split(',')).map((x) => x.trim()).filter(Boolean);
806
+ const bad = only.find((o) => !['locales', 'api', 'urls', 'models'].includes(o));
807
+ if (bad)
808
+ throw new ToolError('bad_args', `Unknown check "${bad}".`, 'Checks: locales, api, urls, models');
809
+ if (flags.fix === true) {
810
+ const fx = await planDoctorFix(root, adapter, { only });
811
+ return handleChange(root, out, flags, fx.changeSet, adapter);
812
+ }
813
+ const r = await runDoctor(root, adapter, { only });
814
+ out.print(renderDoctor(r, out.color));
815
+ out.result({ findings: r.findings, counts: r.counts });
816
+ return r.counts.error || (flags.strict === true && r.counts.warning) ? 1 : 0;
817
+ }
818
+ case 'check': {
819
+ // CI gate: the config file is valid and doctor finds no errors or warnings (fresh scan).
820
+ let configErrors = 0;
821
+ let configIssues = [];
822
+ if (loadConfig(root).exists) {
823
+ const c = configCheck(root);
824
+ configIssues = c.issues;
825
+ configErrors = c.issues.filter((i) => i.level === 'error').length;
826
+ for (const i of c.issues)
827
+ out.print(`${i.level === 'error' ? '✗' : '!'} ${c.file}: ${i.key ? `${i.key}: ` : ''}${i.message}`);
828
+ }
829
+ const r = await runDoctor(root, adapter, {});
830
+ out.print(renderDoctor(r, out.color));
831
+ const failed = configErrors > 0 || r.counts.error > 0 || r.counts.warning > 0;
832
+ out.print(failed ? 'check failed' : 'check passed');
833
+ out.result({ passed: !failed, config: configIssues, findings: r.findings, counts: r.counts });
834
+ return failed ? 1 : 0;
835
+ }
836
+ case 'templates': {
837
+ if (sub === 'init')
838
+ return handleChange(root, out, flags, planTemplatesInit(root, positional.slice(2)), adapter);
839
+ if (sub && sub !== 'list')
840
+ throw new ToolError('bad_args', `Unknown command "templates ${sub}".`, 'Usage: codz templates list | templates init [id...]');
841
+ const l = listTemplates(root);
842
+ out.print(`Templates in ${l.folder}:`);
843
+ for (const t of l.templates)
844
+ out.print(` ${t.custom ? '✓' : ' '} ${t.id.padEnd(18)} ${t.about}`);
845
+ out.print(`\nStart one with: codz templates init <id> (placeholders are listed in the file's summary)`);
846
+ out.result({ ...l });
847
+ return 0;
848
+ }
849
+ case 'test': {
850
+ if (sub !== 'add')
851
+ throw new ToolError('bad_args', `Unknown command "test ${sub ?? ''}".`, 'Usage: codz test add <endpoint>... | --all');
852
+ const cs = await planTestAdd(root, adapter, { endpoints: positional.slice(2), all: flags.all === true });
853
+ return handleChange(root, out, flags, cs, adapter);
854
+ }
855
+ case 'screen': {
856
+ if (sub !== 'add')
857
+ throw new ToolError('bad_args', `Unknown command "screen ${sub ?? ''}".`, 'Usage: codz screen add <name> --api <endpoint> [--path /orders/:id]');
858
+ const name = positional[2];
859
+ if (!name)
860
+ throw new ToolError('bad_args', 'Which screen? Usage: codz screen add <name> --api <endpoint> [--path /orders/:id]');
861
+ const cs = await planScreenAdd(root, adapter, { name, api: args.multi.api?.[0], path: typeof flags.path === 'string' ? flags.path : undefined, initial: flags.initial === true, form: flags.form === true });
862
+ return handleChange(root, out, flags, cs, adapter);
863
+ }
864
+ case 'route': {
865
+ if (sub !== 'add')
866
+ throw new ToolError('bad_args', `Unknown command "route ${sub ?? ''}".`, 'Usage: codz route add <name> [--path /orders/:id]');
867
+ const name = positional[2];
868
+ if (!name)
869
+ throw new ToolError('bad_args', 'Which route? Usage: codz route add <name> [--path /orders/:id]');
870
+ const cs = await planRouteAdd(root, adapter, { name, path: typeof flags.path === 'string' ? flags.path : undefined, initial: flags.initial === true });
871
+ return handleChange(root, out, flags, cs, adapter);
872
+ }
873
+ case 'theme': {
874
+ if (sub !== 'set')
875
+ throw new ToolError('bad_args', `Unknown command "theme ${sub ?? ''}".`, 'Usage: codz theme set primary "#6750A4" radius 12 font Inter');
876
+ const rest = positional.slice(2);
877
+ const tokens = [];
878
+ for (const r of rest) {
879
+ const eq = r.indexOf('=');
880
+ if (eq > 0)
881
+ tokens.push({ name: r.slice(0, eq), value: r.slice(eq + 1) });
882
+ else if (tokens.length && tokens[tokens.length - 1].value === '')
883
+ tokens[tokens.length - 1].value = r;
884
+ else
885
+ tokens.push({ name: r, value: '' });
886
+ }
887
+ const missing = tokens.find((t) => !t.value);
888
+ if (!tokens.length || missing)
889
+ throw new ToolError('bad_args', missing ? `${missing.name} needs a value.` : 'Usage: codz theme set <name> <value>', 'Example: codz theme set primary "#6750A4" radius 12 font Inter');
890
+ const cs = await planThemeSet(root, adapter, tokens);
891
+ return handleChange(root, out, flags, cs, adapter);
892
+ }
893
+ case 'feature': {
894
+ if (sub !== 'add')
895
+ throw new ToolError('bad_args', `Unknown command "feature ${sub ?? ''}".`, 'Usage: codz feature add <name> --spec <file> | --api <endpoint>...');
896
+ const name = positional[2];
897
+ if (!name)
898
+ throw new ToolError('bad_args', 'Which feature? Usage: codz feature add <name> --spec <openapi.yaml>', 'Example: codz feature add orders --spec api.yaml');
899
+ const split = (xs) => xs?.flatMap((x) => (typeof x === 'string' ? x.split(',') : [])).map((x) => x.trim()).filter(Boolean) ?? [];
900
+ const r = await planFeatureAdd(root, adapter, {
901
+ name,
902
+ spec: typeof flags.spec === 'string' ? flags.spec : undefined,
903
+ tags: split(args.multi.tag),
904
+ paths: split([flags.path]),
905
+ apis: args.multi.api,
906
+ baseUrl: typeof flags['base-url'] === 'string' ? flags['base-url'] : undefined,
907
+ initial: flags.initial === true,
908
+ forms: flags['no-forms'] !== true,
909
+ tests: flags['no-tests'] !== true,
910
+ skipConflicts: flags['skip-conflicts'] === true,
911
+ architecture: typeof flags.architecture === 'string' ? flags.architecture : undefined,
912
+ });
913
+ if (!r.setup)
914
+ return handleChange(root, out, flags, r.changeSet, adapter);
915
+ const setupDone = r.setup;
916
+ return handleChange(root, out, flags, r.changeSet, adapter, {
917
+ // The new packages are installed after apply, so the analyzer would fail on their imports.
918
+ verify: false,
919
+ afterApply: () => {
920
+ if (setupDone.dependenciesChanged && flags['no-install'] === true)
921
+ out.warn('Dependencies were added; install them yourself (npm install, flutter pub get or a Gradle sync).');
922
+ return { installed: setupDone.dependenciesChanged && flags['no-install'] !== true ? installDependencies(root, adapter, out) : undefined };
923
+ },
924
+ });
925
+ }
926
+ case 'auth': {
927
+ if (sub !== 'add')
928
+ throw new ToolError('bad_args', `Unknown command "auth ${sub ?? ''}".`, 'Usage: codz auth add [--login-path /auth/login] [--me-path /auth/me]');
929
+ const str = (k) => (typeof flags[k] === 'string' ? flags[k] : undefined);
930
+ const r = await planAuthAdd(root, adapter, {
931
+ loginPath: str('login-path'),
932
+ mePath: str('me-path'),
933
+ logoutPath: str('logout-path'),
934
+ refreshPath: str('refresh-path'),
935
+ tokenField: str('token-field'),
936
+ refreshField: str('refresh-field'),
937
+ publicPaths: args.multi.public,
938
+ });
939
+ return handleChange(root, out, flags, r.changeSet, adapter);
940
+ }
941
+ case 'module': {
942
+ if (sub !== 'add')
943
+ throw new ToolError('bad_args', `Unknown command "module ${sub ?? ''}".`, 'Did you mean: codz module add <name>');
944
+ const name = positional[2];
945
+ if (!name)
946
+ throw new ToolError('bad_args', 'Which module? Usage: codz module add <name>');
947
+ const cs = await planModuleAdd(root, adapter, { name, initial: flags.initial === true, apis: args.multi.api, noLoad: flags['no-load'] === true });
948
+ return handleChange(root, out, flags, cs, adapter);
949
+ }
950
+ case 'api': {
951
+ if (sub === 'list') {
952
+ const rows = await apiList(root, adapter, { filter: positional[2] });
953
+ if (!rows.length)
954
+ out.print(positional[2] ? `No endpoints match "${positional[2]}".` : 'No endpoints found yet. Add one with: codz api add');
955
+ const w = Math.max(0, ...rows.map((r) => r.name.length));
956
+ const pw = Math.max(0, ...rows.map((r) => r.path.length));
957
+ for (const r of rows)
958
+ out.print(`${r.method.padEnd(6)} ${r.path.padEnd(pw)} ${r.name.padEnd(w)} ${(r.response ?? '?').padEnd(14)} ${r.file}${r.uses ? '' : ' (not called yet)'}`);
959
+ out.result({ endpoints: rows });
960
+ return 0;
961
+ }
962
+ if (sub === 'show') {
963
+ const name = positional[2];
964
+ if (!name)
965
+ throw new ToolError('bad_args', 'Which endpoint? Usage: codz api show <name>', 'List them with: codz api list');
966
+ const d = await apiShow(root, adapter, name);
967
+ const e = d.endpoint;
968
+ out.print(`${e.name} ${e.method} ${e.path}`);
969
+ out.print(` declared ${e.file}${d.declLine ? `:${d.declLine}` : ''}`);
970
+ if (d.signature)
971
+ out.print(` signature ${d.signature}`);
972
+ out.print(` returns ${e.response ?? 'unknown'}`);
973
+ for (const m of d.models)
974
+ out.print(` model ${m.name} (${m.file}): ${m.fields.map((f) => `${f.name}${f.nullable ? '?' : ''}: ${f.type}`).join(', ')}`);
975
+ if (!d.uses.length)
976
+ out.print(' used by nothing yet');
977
+ for (const u of d.uses)
978
+ out.print(` used by ${u.file}:${u.line} ${u.text}`);
979
+ out.result({ ...d });
980
+ return 0;
981
+ }
982
+ if (sub === 'wire') {
983
+ const endpoint = positional[2];
984
+ const into = typeof flags.into === 'string' ? flags.into : positional[3];
985
+ if (!endpoint || !into)
986
+ throw new ToolError('bad_args', 'Usage: codz api wire <endpoint> --into <module>');
987
+ const cs = await planApiWire(root, adapter, { endpoint, module: into, noLoad: flags['no-load'] === true });
988
+ return handleChange(root, out, flags, cs, adapter);
989
+ }
990
+ if (sub === 'import') {
991
+ const file = positional[2];
992
+ if (!file)
993
+ throw new ToolError('bad_args', 'Usage: codz api import <openapi.yaml|swagger.json|postman.json|file.har> [--tag t] [--only name] [--path /users/**] [--method GET] [--max n] [--list]');
994
+ const split = (xs) => xs?.flatMap((x) => (typeof x === 'string' ? x.split(',') : [])).map((x) => x.trim()).filter(Boolean) ?? [];
995
+ const max = typeof flags.max === 'string' ? Number(flags.max) : undefined;
996
+ if (max !== undefined && (!Number.isInteger(max) || max < 1))
997
+ throw new ToolError('bad_args', `--max needs a whole number, got "${flags.max}".`);
998
+ const importOpts = {
999
+ file,
1000
+ tags: split(args.multi.tag),
1001
+ only: split(args.multi.only),
1002
+ paths: split([flags.path]),
1003
+ methods: split([flags.method]),
1004
+ max,
1005
+ yes: flags.yes === true || flags.y === true,
1006
+ baseUrl: typeof flags['base-url'] === 'string' ? flags['base-url'] : undefined,
1007
+ includeErrors: flags['include-errors'] === true,
1008
+ skipConflicts: flags['skip-conflicts'] === true,
1009
+ };
1010
+ const bad = importOpts.methods.find((m) => !['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(m.toUpperCase()));
1011
+ if (bad)
1012
+ throw new ToolError('bad_method', `"${bad}" is not an HTTP method.`, 'Use GET, POST, PUT, PATCH or DELETE.');
1013
+ if (flags.list === true) {
1014
+ const l = listApiImport({ ...importOpts, file: existsSync(join(root, file)) ? join(root, file) : file });
1015
+ out.print(`${l.title}: ${l.operations.length} of ${l.total} operations`);
1016
+ const w = Math.max(0, ...l.operations.map((o) => o.path.length));
1017
+ for (const o of l.operations)
1018
+ out.print(` ${o.method.padEnd(6)} ${o.path.padEnd(w)} ${o.name}${o.response ? ` → ${o.response}` : ''}${o.tags.length ? ` [${o.tags.join(', ')}]` : ''}`);
1019
+ for (const wn of l.warnings)
1020
+ out.warn(wn);
1021
+ out.result({ ...l });
1022
+ return 0;
1023
+ }
1024
+ const showProgress = !out.json && process.stderr.isTTY === true;
1025
+ const r = await planApiImport(root, adapter, {
1026
+ ...importOpts,
1027
+ onProgress: showProgress
1028
+ ? (done, total, name) => {
1029
+ if (total < 10)
1030
+ return;
1031
+ process.stderr.write(done >= total ? '\r\x1b[K' : `\r\x1b[K Planning ${done + 1}/${total} ${name}`);
1032
+ }
1033
+ : undefined,
1034
+ });
1035
+ return handleChange(root, out, flags, r.changeSet, adapter);
1036
+ }
1037
+ if (sub === 'sync') {
1038
+ const file = positional[2];
1039
+ if (!file)
1040
+ throw new ToolError('bad_args', 'Usage: codz api sync <openapi.yaml|swagger.json|postman.json|file.har> [--add] [--remove] [--check] [--tag t] [--path /users/**]');
1041
+ const split = (xs) => xs?.flatMap((x) => (typeof x === 'string' ? x.split(',') : [])).map((x) => x.trim()).filter(Boolean) ?? [];
1042
+ if (flags.check === true)
1043
+ setCacheWrites(false);
1044
+ const r = await planApiSync(root, adapter, {
1045
+ file,
1046
+ tags: split(args.multi.tag),
1047
+ paths: split([flags.path]),
1048
+ add: flags.add === true,
1049
+ remove: flags.remove === true,
1050
+ baseUrl: typeof flags['base-url'] === 'string' ? flags['base-url'] : undefined,
1051
+ });
1052
+ if (flags.check === true) {
1053
+ // CI: report drift without writing anything.
1054
+ // Endpoints only in the project may belong to another API, so they are reported but are not drift unless --remove was asked for.
1055
+ const drift = r.changeSet.changes.length > 0 || r.newInSpec.length > 0 || (flags.remove === true && r.missingFromSpec.length > 0);
1056
+ out.print(r.changeSet.summary.join('\n'));
1057
+ for (const w of r.changeSet.warnings)
1058
+ out.warn(w);
1059
+ if (drift)
1060
+ out.print(`\nOut of sync with ${file}. Run codz api sync ${file}${r.newInSpec.length ? ' --add' : ''} to update.`);
1061
+ out.result({ inSync: !drift, updated: r.updated, unchanged: r.unchanged, newInSpec: r.newInSpec, missingFromSpec: r.missingFromSpec, breaking: r.breaking, files: r.changeSet.changes.map((c) => c.path) });
1062
+ return drift ? 1 : 0;
1063
+ }
1064
+ return handleChange(root, out, flags, r.changeSet, adapter);
1065
+ }
1066
+ if (sub === 'update') {
1067
+ const name = positional[2];
1068
+ if (!name)
1069
+ throw new ToolError('bad_args', 'Usage: codz api update <endpoint> --sample <file.json>');
1070
+ const r = await planApiUpdate(root, adapter, name, args.multi.sample ?? [], { noUnwrap: flags['no-unwrap'] === true });
1071
+ return handleChange(root, out, flags, r.changeSet, adapter);
1072
+ }
1073
+ if (sub === 'remove') {
1074
+ const name = positional[2];
1075
+ if (!name)
1076
+ throw new ToolError('bad_args', 'Usage: codz api remove <endpoint>');
1077
+ return handleChange(root, out, flags, await planApiRemove(root, adapter, name), adapter);
1078
+ }
1079
+ if (sub === 'rename') {
1080
+ const [from, to] = [positional[2], positional[3]];
1081
+ if (!from || !to)
1082
+ throw new ToolError('bad_args', 'Usage: codz api rename <endpoint> <newName>');
1083
+ return handleChange(root, out, flags, await planApiRename(root, adapter, from, to), adapter);
1084
+ }
1085
+ if (sub !== 'add')
1086
+ throw new ToolError('bad_args', `Unknown command "api ${sub ?? ''}".`, 'Commands: api add, import, sync, update, remove, rename, wire, list, show');
1087
+ const opts = {
1088
+ curl: typeof flags.curl === 'string' ? flags.curl : undefined,
1089
+ url: typeof flags.url === 'string' ? flags.url : positional[2],
1090
+ method: typeof flags.method === 'string' ? flags.method : undefined,
1091
+ samples: args.multi.sample,
1092
+ requestSample: typeof flags['request-sample'] === 'string' ? flags['request-sample'] : undefined,
1093
+ name: typeof flags.name === 'string' ? flags.name : undefined,
1094
+ path: typeof flags.path === 'string' ? flags.path : undefined,
1095
+ modelName: typeof flags['model-name'] === 'string' ? flags['model-name'] : undefined,
1096
+ noUnwrap: flags['no-unwrap'] === true,
1097
+ into: typeof flags.into === 'string' ? flags.into : undefined,
1098
+ noLoad: flags['no-load'] === true,
1099
+ };
1100
+ if (flags.clipboard === true && !opts.curl && !opts.url)
1101
+ opts.paste = readClipboard();
1102
+ if (!opts.curl && !opts.url && !opts.paste) {
1103
+ const piped = await readStdin();
1104
+ if (piped.trim())
1105
+ opts.paste = piped;
1106
+ else if (process.stdin.isTTY && !out.json)
1107
+ opts.paste = await readPasteInteractive();
1108
+ }
1109
+ if (!loadConfig(root).exists)
1110
+ out.warn(`No ${CONFIG_FILE} yet; using detected conventions. Run codz init to review them.`);
1111
+ const { changeSet } = await planApiAdd(root, adapter, opts);
1112
+ return handleChange(root, out, flags, changeSet, adapter);
1113
+ }
1114
+ case 'apply': {
1115
+ if (!sub)
1116
+ throw new ToolError('bad_args', 'Which plan? Usage: codz apply <plan-id>');
1117
+ const cs = loadPlan(root, sub);
1118
+ const r = applyChangeSet(root, cs, { allowDirty: flags['allow-dirty'] === true, adapter, format: !flags['no-format'], verify: !flags['no-verify'] });
1119
+ printApplied(out, r);
1120
+ out.result({ applied: true, id: cs.id, ...r });
1121
+ return 0;
1122
+ }
1123
+ case 'undo': {
1124
+ if (typeof flags.to === 'string') {
1125
+ const rs = undoTo(root, flags.to, { force: flags.force === true });
1126
+ for (const r of rs)
1127
+ out.print(`Undid ${r.skill} (${r.id}): ${r.files.join(', ')}`);
1128
+ out.result({ undone: rs });
1129
+ return 0;
1130
+ }
1131
+ const last = [...readHistory(root)].reverse().find((e) => !e.undone);
1132
+ if (last?.skill === 'new') {
1133
+ // The project itself was made by codz new: undo removes the whole folder.
1134
+ const { undoNew } = await import("./starters/engine.js");
1135
+ const r = undoNew(root, { force: flags.force === true });
1136
+ out.print(`Undid new (${r.id}): removed ${r.removed}`);
1137
+ out.result({ undone: { id: r.id, skill: 'new', removed: r.removed } });
1138
+ return 0;
1139
+ }
1140
+ const r = undoLast(root, { force: flags.force === true });
1141
+ out.print(`Undid ${r.skill} (${r.id}): ${r.files.join(', ')}`);
1142
+ out.result({ undone: r });
1143
+ return 0;
1144
+ }
1145
+ case 'history': {
1146
+ if (sub === 'show') {
1147
+ const id = positional[2];
1148
+ if (!id)
1149
+ throw new ToolError('bad_args', 'Which change? Usage: codz history show <id>', 'List them with: codz history');
1150
+ const d = historyShow(root, id);
1151
+ out.print(`${d.entry.id} ${d.entry.skill}${d.entry.undone ? ' (undone)' : ''}`);
1152
+ for (const l of d.entry.summary)
1153
+ out.print(` • ${l}`);
1154
+ for (const f of d.files)
1155
+ out.print(` ${f.action.padEnd(8)} ${f.path}${f.state === 'as applied' ? '' : ` (${f.state})`}`);
1156
+ const diffs = d.files.map((f) => f.diff).filter(Boolean);
1157
+ if (diffs.length && !d.entry.undone)
1158
+ out.print('\n' + diffs.join('\n'));
1159
+ out.result({ ...d });
1160
+ return 0;
1161
+ }
1162
+ const h = readHistory(root);
1163
+ if (!h.length)
1164
+ out.print('No changes applied yet.');
1165
+ for (const e of h)
1166
+ out.print(`${e.id} ${e.skill.padEnd(12)}${e.undone ? ' (undone)' : ''} ${e.summary[0] ?? ''}`);
1167
+ if (h.length)
1168
+ out.print('\nDetails: codz history show <id> Undo back to one: codz undo --to <id>');
1169
+ out.result({ history: h });
1170
+ return 0;
1171
+ }
1172
+ default:
1173
+ throw new ToolError('bad_args', `Unknown command "${cmd}".`, 'Run codz --help.');
1174
+ }
1175
+ }