codz-cli 0.1.2 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,7 +105,7 @@ cd shop_app && codz init # then api add, screen add, ... as
105
105
  - **Data only.** A starter is files plus a manifest. It never runs its own scripts; codz runs only `flutter create`, `flutter pub get`, `dart format` and `npm install --ignore-scripts`, with arguments it builds itself from typed answers.
106
106
  - **Strict reading.** The archive reader accepts regular files and folders only. Links, devices, `..`, absolute paths, Windows reserved names, case or Unicode clashes, and oversized or bomb archives stop it before anything is written.
107
107
  - **One step, and undoable.** The preview lists every file and step. The project is built in a temporary folder and moved into place in one rename. `codz undo` inside the new project removes it again while you have not changed it; files from `pub get`, `npm install` and `codz init` do not count as changes.
108
- - **Plans and agents.** `--plan` (or `--json` without `--yes`) saves a plan, and `codz apply <id>` creates exactly that. From MCP, `new` only plans, `login` is not a tool, and a paid starter has to be downloaded once by a person first.
108
+ - **Plans and agents.** `--plan` (or `--json` without `--yes`) saves a plan, and `codz apply <id>` creates exactly that. When an AI agent runs codz (`CODZ_MCP=1`), a paid starter has to be downloaded once by a person first.
109
109
  - **Offline.** `--cached` uses only what is already downloaded. In CI, set `CODZ_TOKEN` instead of logging in.
110
110
 
111
111
  The catalog is served by the separate codz store project (accounts, purchases, `codz login`); until a release points at it, `new` works with template folders or `CODZ_REGISTRY`. Starter sources live in `starters/`, and `scripts/starters.ts` checks, packs and signs them (see starters/README.md).
@@ -200,6 +200,42 @@ codz api remove deleteUser # remove it; lists callers that
200
200
 
201
201
  `api rename` and `api remove` find uses with your project's own TypeScript compiler when `typescript` is installed in it (it is in almost every Next.js project), so a local variable or parameter that happens to share the name, an object key, or another file's own function with that name is never touched. Without it, codz falls back to its text scanner (`CODZ_NO_AST=1` forces the fallback).
202
202
 
203
+ ### A model from JSON: codz model
204
+
205
+ ```bash
206
+ codz model Order --from-clipboard # copy a response or object, then run this
207
+ codz model Order --sample order.json
208
+ curl -s https://api.example.com/orders/1 | codz model Order
209
+ ```
210
+
211
+ Writes the model and a class for each nested object (`Order`, `Customer`, `OrderItem`), in the project's own style, with no endpoint. They all go in one file, the main model's (`order.dart`, `Order.kt`, `order.ts`), so a long response does not scatter a dozen files; `--split` gives each class its own file. A nested model the project already has is reused, not copied. A list gives the model of its items, `{"data": ...}` is unwrapped (`--no-unwrap` keeps it), and pasted text around the JSON (a `response:` label, code fences) is ignored. An existing model is never overwritten unless you pass `--force`; to add one field, use `codz field add`.
212
+
213
+ ### One file per model
214
+
215
+ `model`, `api add`, `api import`, `api sync` and `feature add` put nested models in the file of the model that uses them: an order response gives `order.dart` (or `Order.kt`, `order.ts`, `Order.swift`) holding `Order`, `Customer` and `OrderItem`, not three files. A nested model that several top-level models use (an `Address` used by both `Order` and `User` in a spec) keeps its own file, and models the project already has stay where they are. For one file per model, pass `--split`, or set it for good with `codz config set modelFiles split`.
216
+
217
+ ### Choosing the model folder: --model-dir
218
+
219
+ New models go into the project's models folder (`modelsFolder` in `codz.config.yaml`, which `codz init` detects). For a feature-first project where each feature keeps its own models, choose the folder for one command:
220
+
221
+ ```bash
222
+ codz model Order --from-clipboard --model-dir lib/features/orders/models
223
+ codz api add --url https://api.example.com/v1/carts/1 --sample cart.json --model-dir lib/features/cart/models
224
+ codz feature add orders --spec api.yaml --model-dir src/features/orders/types
225
+ ```
226
+
227
+ `--model-dir` works with `model`, `api add`, `api import`, `api sync` and `feature add`, and lasts for that one command. Imports point at the new folder, and Kotlin models get the matching `package` line. Existing models keep their place, so a nested model that already exists elsewhere is reused, not copied. To change the folder for good: `codz config set modelsFolder lib/features/orders/models`.
228
+
229
+ ### One field at a time: field add
230
+
231
+ ```bash
232
+ codz field add Product rating:double? tags:string[] # the backend added two fields
233
+ codz field add Order created_at:date category:Category
234
+ codz field remove Product legacy_code # and lists the lines that still use it
235
+ ```
236
+
237
+ The name is the JSON key; codz writes the property name your models use (`created_at` becomes `createdAt` in Dart, Kotlin and Swift). Types: `string`, `int`, `double`, `bool`, `date`, `any`, a model in the project, or a list (`string[]`, `list<Category>`); end with `?` for an optional field. Only the field's own lines change, in every place the model spells it: the TypeScript interface; the Dart constructor, `fromJson`, field and `toJson` (or the freezed factory, with `@JsonKey` for snake_case keys); the Kotlin data class parameter with your serializer's annotation; the Swift property and its `CodingKeys` case. Everything else in the file, including code you wrote, stays as it is. Then it lists the files that use the model, so you can show the field where it belongs, and `codz mock serve` fills it with sample data straight away.
238
+
203
239
  ### Other stacks
204
240
 
205
241
  codz writes in the style the project already uses:
@@ -233,6 +269,68 @@ codz api diff old.yaml new.yaml --fail-on warning # exit 1 on
233
269
 
234
270
  Breaking: an operation removed, a response field removed or retyped, a new path parameter, a new required request field. Warning: a response field that can now be null, a removed query parameter, a retyped request field. Added operations, response fields and optional request fields are listed as additions. Inside a project each change names the endpoint function and the files and lines that use it (its callers, the screens, the model); outside a project it compares the two files only. Nothing is written except the `--report` file.
235
271
 
272
+ ### Typed asset names: codz assets
273
+
274
+ ```bash
275
+ codz assets # run again after adding or removing files
276
+ ```
277
+
278
+ | Project | What it writes | Use |
279
+ | --- | --- | --- |
280
+ | Flutter | `lib/gen/assets.dart` with a class per folder under `assets/` (`AppImages`, `AppIcons`, `AppFonts`), and every asset folder and font family in `pubspec.yaml` | `Image.asset(AppImages.logo)` |
281
+ | React Native | `src/assets/index.ts` with a `require()` for each file | `<Image source={images.logo} />` |
282
+ | Next.js, Vite | `lib/assets.ts` / `src/assets.ts`: paths for `public/`, imports for Vite's `src/assets` | `<img src={publicFiles.logo} />` |
283
+ | Android, iOS | Nothing: `R.drawable.logo` and `Image(.logo)` are already typed. codz checks resource names that would break the build | |
284
+
285
+ A typo in an asset path becomes a compile error instead of a blank image. Flutter resolution folders (`2.0x/`) are left to Flutter, font weights and italics come from the file names (`Inter-BoldItalic.ttf`), and assets that no code uses are listed so you can delete them.
286
+
287
+ ### API environments: env add
288
+
289
+ ```bash
290
+ codz env add staging --base-url https://staging.example.com
291
+ codz env add qa --base-url https://qa.example.com
292
+ codz env list # every environment and its base URL
293
+ codz env use staging # React Native CLI, Android, iOS: switch in the code
294
+ ```
295
+
296
+ The base URL you have now becomes `default`. Each platform gets its usual mechanism:
297
+
298
+ | Project | What env add writes | Run against staging |
299
+ | --- | --- | --- |
300
+ | Next.js | `.env.staging` with `NEXT_PUBLIC_API_URL`, `dev:staging` / `build:staging` scripts (with dotenv-cli) | `npm run dev:staging` |
301
+ | React (Vite) | `.env.staging` with `VITE_API_URL`, scripts with `--mode staging` | `npm run dev:staging` |
302
+ | Expo | `.env.staging` with `EXPO_PUBLIC_API_URL`, a `start:staging` script | `npm run start:staging` |
303
+ | React Native CLI | An `API_URLS` table in `src/config/env.ts`; `APP_ENV` picks one | `codz env use staging` |
304
+ | Flutter | `env/staging.json`; Dio reads `String.fromEnvironment('API_BASE_URL')`; a VS Code run configuration | `flutter run --dart-define-from-file=env/staging.json` |
305
+ | Android | An `apiUrls` table in `app/build.gradle(.kts)` into `BuildConfig.BASE_URL` (build variants stay the same) | `./gradlew installDebug -PapiEnv=staging`, or `codz env use staging` |
306
+ | iOS | `AppEnvironment.swift`, an enum the API client reads | `APP_ENV=staging` in the scheme, or `codz env use staging` |
307
+
308
+ `development`, `production` and `test` need no script in Next.js (and `development`, `production` in Vite): the framework reads those files by itself. codz never writes `.env.local`. Base URLs are not secrets, so these files are meant to be committed; keep keys and tokens out of them.
309
+
310
+ ### Mock API server
311
+
312
+ ```bash
313
+ codz mock serve # the project's endpoints, on http://localhost:4010
314
+ codz mock serve --spec api.yaml # every operation in an OpenAPI, Swagger, Postman or HAR file
315
+ codz mock serve --lan --port 8080 # reachable from a phone on the same Wi-Fi
316
+ codz mock serve --scenario empty # start with empty lists (or error, unauthorized, slow, offline)
317
+ ```
318
+
319
+ A local fake API, so new screens show data before the backend exists. Every endpoint answers with sample data built from its response model (5 rows per list, each a little different; `--items`, and a 300 ms delay so loading states show; `--delay 0` to skip it). A list and its items share in-memory data: a create form adds a row the list then shows, edit forms change it and delete removes it, and restarting resets everything. Paths match by their last segments, so `/v1/products` reaches `GET /products`; anything unknown gets a 404 that lists the routes. It sends CORS headers for web apps, and prints how to point your app at it (environment variable for Next.js and Vite, emulator and simulator addresses for mobile). It listens on this computer only unless you pass `--lan`, and it never makes requests of its own.
320
+
321
+ **Every screen state, without touching the backend.** Open `http://localhost:4010/__codz` while the app runs:
322
+
323
+ | Scenario | What the app gets | Checks |
324
+ | --- | --- | --- |
325
+ | `normal` | sample data | the happy path |
326
+ | `empty` | empty lists; their items 404 | empty states |
327
+ | `error` | 500 for every request | error messages and retry buttons |
328
+ | `unauthorized` | 401 for every request | sign-out and token refresh |
329
+ | `slow` | answers after 3 seconds | loading spinners, double taps |
330
+ | `offline` | the connection drops | no-network handling |
331
+
332
+ Switch for every endpoint, or set one endpoint differently (only `GET /v1/products` empty, say). The same page lists the last requests with their request and response bodies, and **Reset** brings back the starting data. Scripts and tests can switch too: `curl -X POST localhost:4010/__codz/scenario -d '{"scenario":"error","route":"GET /v1/products"}'`, `GET /__codz/state`, `POST /__codz/reset`.
333
+
236
334
  ### Mock responses
237
335
 
238
336
  ```bash
@@ -254,6 +352,26 @@ codz locale key cart.empty "Your cart is empty" --value hi="आपकी का
254
352
 
255
353
  Works with JSON message files (next-intl, i18next; `cart.empty` becomes a nested key), Flutter ARB files (keys become identifiers like `cartEmpty`, `{name}` placeholders get metadata) and GetX `Translations`. A GetX project without translations gets `lib/app/translations/app_translations.dart`, wired into `GetMaterialApp` with `translations:`, `locale:` and `fallbackLocale:`. Languages without a `--value` get the default text and are listed by `codz doctor` as untranslated. Only the changed keys are touched; the rest of each file keeps its formatting.
256
354
 
355
+
356
+ #### Moving screen text into the locale files: locale extract
357
+
358
+ ```bash
359
+ codz locale extract # every screen
360
+ codz locale extract lib/features/cart/cart_page.dart # one file
361
+ ```
362
+
363
+ Finds the text users see and replaces it with the project's lookup, in one previewed change:
364
+
365
+ | Project | Before | After |
366
+ | --- | --- | --- |
367
+ | Flutter (ARB) | `Text('Nothing here yet')` | `Text(AppLocalizations.of(context)!.productsNothingHereYet)` |
368
+ | Flutter (GetX) | `Text('Nothing here yet')` | `Text('products.nothingHereYet'.tr)` |
369
+ | next-intl | `<p>Nothing here yet</p>` | `<p>{t('products.nothingHereYet')}</p>` with `useTranslations()` or `await getTranslations()` |
370
+ | react-i18next | `placeholder="Search"` | `placeholder={t('products.search')}` with `useTranslation()` |
371
+ | Compose | `Text("Nothing here yet")` | `Text(stringResource(R.string.products_nothing_here_yet))` and `strings.xml` |
372
+
373
+ It looks at `Text(...)` and hint, label and tooltip text in Flutter, JSX text and `placeholder`, `title`, `alt`, `aria-label` in React, and `Text("...")` in Compose. Values in the text become placeholders (`'Hello $name'` → `greetingHello(name)`, `trParams`, or `%1$s`). Keys are named after the screen; text used on several screens goes under `common`, and text that already has a key reuses it. Text in conditions or built in code is left alone. In Flutter it removes `const` where the text is no longer constant, and the first time it wires `AppLocalizations` into `MaterialApp` (with `l10n.yaml` and `flutter_localizations`). SwiftUI needs none of this: Xcode collects `Text("...")` into a String Catalog by itself.
374
+
257
375
  ### Theme
258
376
 
259
377
  ```bash
@@ -298,6 +416,26 @@ One change with the route, the page, the call to the endpoint, and the loading,
298
416
  - React Native: a screen that loads the endpoint (through its TanStack Query hook when there is one) with `ActivityIndicator`, the error with Retry, and a `FlatList` or the fields; registered in the navigator, or a file under `app/` with Expo Router.
299
417
  - Android: a Compose screen and a ViewModel next to it. The ViewModel gets the Retrofit interface from Hilt (`@HiltViewModel`, `hiltViewModel()`), Koin (`viewModelOf(::X)` added to the module, `koinViewModel()`) or the service locator (a `ViewModelProvider.Factory`), reads route parameters from `SavedStateHandle` (converted to `Int` when the method wants one), and exposes `StateFlow<UiState<T>>` (`UiState.kt` with Loading, Success and Error is written once). The screen shows a spinner, the error with Retry, then a `LazyColumn` or the model's fields, and gets a `composable("orders/{id}")` entry in your `NavHost`.
300
418
 
419
+ #### Long lists: --paginate and --search
420
+
421
+ ```bash
422
+ codz screen add products --api listProducts --paginate --search
423
+ codz screen add orders --api listOrders --search
424
+ ```
425
+
426
+ `--paginate` loads 20 items at a time and the next page as you reach the end of the list. It needs the endpoint to take a page number (`page`) or an offset (`offset`, `skip`); with a page-size parameter (`limit`, `pageSize`, `per_page`) codz sends 20 and stops at a short page, otherwise it stops at an empty one. `--search` adds a search box: when the endpoint has a search parameter (`q`, `query`, `search`, `term`), the screen asks the server after a short pause in typing and starts again from the first page; otherwise it filters the items already loaded by their text fields, and says so. A new search or refresh replaces a request still loading, so an older answer never overwrites a newer one. Each screen keeps the project's own way of doing things:
427
+
428
+ | Project | Paged list |
429
+ | --- | --- |
430
+ | Next.js | Server page: `?page=2&q=lamp` in the URL, a search form, Previous / Next links |
431
+ | React | `useState` / `useEffect`, or `useInfiniteQuery` with TanStack Query 5; a Load more button |
432
+ | React Native | `FlatList` with `onEndReached` and pull to refresh; `useInfiniteQuery` with TanStack Query |
433
+ | Flutter (Riverpod, get_it) | A `StatefulWidget` that loads the next page when the end of the list is on screen |
434
+ | Flutter (Bloc) | A Cubit with one state (items, loading, error, query, hasMore) and a `BlocBuilder` page |
435
+ | Flutter (GetX) | The module's controller (`items`, `isLoading`, `load()`, `search()`) and an `Obx` view |
436
+ | Android | A ViewModel with one `StateFlow` state; a `LazyColumn` (Compose) or `RecyclerView` (Views) that loads more near the end |
437
+ | iOS | An `ObservableObject` view model; `List` with `.searchable` and a progress row that loads the next page |
438
+
301
439
  Only GET endpoints make a data screen. For POST, PUT and PATCH endpoints, `--form` makes a form instead:
302
440
 
303
441
  ```bash
@@ -454,20 +592,6 @@ codz api add --curl "..." --plan --json # get the diff and a plan id
454
592
  codz apply <plan-id> --json # apply exactly what was shown
455
593
  ```
456
594
 
457
- ### MCP server
458
-
459
- ```bash
460
- codz mcp # in the project folder (or: codz mcp --cwd /path/to/project)
461
- ```
462
-
463
- `codz mcp` serves codz over the Model Context Protocol on stdio, for AI agents and editors that support MCP. Every command becomes a tool (`api add` → `api_add`, `api sync` → `api_sync`, `doctor`, ...), with a JSON schema built from its options. Tools that edit code never write: they return the diff and a `planId`, and the `apply` tool applies that plan exactly as shown. Two resources: `codz://project` (what codz knows about the project) and `codz://commands` (every command and option).
464
-
465
- To add it to an MCP client, register a stdio server whose command is `codz` with the arguments `["mcp", "--cwd", "/path/to/project"]`. Many clients take a JSON entry like:
466
-
467
- ```json
468
- { "mcpServers": { "codz": { "command": "codz", "args": ["mcp", "--cwd", "/path/to/project"] } } }
469
- ```
470
-
471
595
  ### VS Code extension
472
596
 
473
597
  `extensions/vscode` runs codz from the command palette: add an endpoint from a cURL command or URL, sync with a spec, show an endpoint and jump to its uses, rename, mock and test every endpoint, doctor findings in the Problems panel, undo. Every change opens as a diff first and is applied only when you click Apply. Package it with `npx @vscode/vsce package` in that folder and install the `.vsix`; see its README.
@@ -503,8 +627,7 @@ bin/codz.js entry point
503
627
  src/cli.ts commands and flags
504
628
  src/core/ types, cache, config and YAML reader, JSON editing, diff, change sets, git
505
629
  src/input/ cURL, paste, OpenAPI/Swagger/Postman parsing, naming, model inference
506
- src/core/commands.ts the command and option table (help, completion, parser, docs, MCP tools)
507
- src/skills/mcp.ts codz mcp: the Model Context Protocol server
630
+ src/core/commands.ts the command and option table (help, completion, parser, docs)
508
631
  src/starters/ codz new and the starter catalog (the only network code; loaded on demand)
509
632
  starters/ the internal starter templates; scripts/starters.ts publishes them
510
633
  extensions/vscode/ the VS Code extension
package/bin/codz.js CHANGED
File without changes
@@ -65,7 +65,9 @@ export function scanLocales(root, files) {
65
65
  const d = l.file.split('/').slice(0, -1).join('/');
66
66
  byDir.set(d, [...(byDir.get(d) ?? []), l]);
67
67
  }
68
- const [, chosen] = [...byDir.entries()].sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]))[0];
68
+ // Ties go to the shallower folder (lib/l10n over example/lib/l10n).
69
+ const depth = (d) => (d ? d.split('/').length : 0);
70
+ const [, chosen] = [...byDir.entries()].sort((a, b) => b[1].length - a[1].length || depth(a[0]) - depth(b[0]) || a[0].localeCompare(b[0]))[0];
69
71
  const languages = chosen.map((l) => l.lang).sort();
70
72
  let keyCount = 0;
71
73
  for (const l of chosen)
@@ -135,7 +135,7 @@ export async function scan(root, configConventions) {
135
135
  const dirs = new Set(modelFiles.map(relDir));
136
136
  const inBest = modelFiles.filter((f) => relDir(f) === c.modelsFolder).length;
137
137
  if (dirs.size >= 3 && inBest * 2 < modelFiles.length) {
138
- map.notes.push(`Models live in ${dirs.size} different folders, so new ones go to lib/models. Use --into <folder> or set modelsFolder in codz.config.yaml to choose.`);
138
+ map.notes.push(`Models live in ${dirs.size} different folders, so new ones go to lib/models. Use --model-dir <folder> or set modelsFolder in codz.config.yaml to choose.`);
139
139
  c.modelsFolder = 'lib/models';
140
140
  }
141
141
  }
package/dist/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  // codz command line. Every command supports --json for scripts and AI agents.
2
2
  import { baseUrlProblem } from "./core/safe.js";
3
- import { existsSync, openSync, writeFileSync } from 'node:fs';
3
+ import { existsSync, openSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import { ReadStream } from 'node:tty';
5
6
  import { dirname, join, resolve } from 'node:path';
6
7
  import { createInterface } from 'node:readline/promises';
7
8
  import { adapterFor } from "./adapters/index.js";
8
- import { loadMap, setCacheWrites } from "./core/cache.js";
9
+ import { loadMap, setCacheWrites, setRunConventions } from "./core/cache.js";
10
+ import { readText } from "./core/fsutil.js";
9
11
  import { applyChangeSet, changeSetJson, historyShow, loadPlan, readHistory, renderDiff, savePlan, undoLast, undoTo } from "./core/changeset.js";
10
12
  import { changeSummaryLine, commitChange, openDiffInEditor, readClipboard } from "./core/workflow.js";
11
13
  import { CONFIG_FILE, loadConfig } from "./core/config.js";
@@ -19,9 +21,13 @@ import { listApiImport, planApiImport } from "./skills/apiImport.js";
19
21
  import { planApiSync } from "./skills/apiSync.js";
20
22
  import { planTestAdd } from "./skills/testgen.js";
21
23
  import { listTemplates, planTemplatesInit } from "./skills/templatesCmd.js";
22
- import { serveMcp } from "./skills/mcp.js";
23
24
  import { createProject, describeCreate, PROJECT_TYPES, projectType } from "./skills/create.js";
24
25
  import { planApiRemove, planApiRename, planApiUpdate } from "./skills/apiEdit.js";
26
+ import { planFieldAdd, planFieldRemove } from "./skills/field.js";
27
+ import { normalizeModelDir, parseJsonText, planModel } from "./skills/model.js";
28
+ import { listEnvs, planEnvAdd, planEnvUse } from "./skills/env.js";
29
+ import { planLocaleExtract } from "./skills/localeExtract.js";
30
+ import { planAssets } from "./skills/assets.js";
25
31
  import { planMockAdd } from "./skills/mock.js";
26
32
  import { planLocaleAdd, planLocaleKey } from "./skills/locale.js";
27
33
  import { planThemeSet } from "./skills/theme.js";
@@ -38,12 +44,13 @@ import { apiList, apiShow } from "./skills/apiList.js";
38
44
  import { cleanPlans, listPlans, showPlan } from "./skills/plans.js";
39
45
  import { complete, completionScript, SHELLS } from "./skills/completion.js";
40
46
  import { genericAdapter } from "./adapters/generic.js";
41
- import { allowedFlags, BOOLEAN_FLAGS, findCommand, FLAGS, renderCommandHelp, renderHelp, REPEATABLE_FLAGS } from "./core/commands.js";
47
+ import { allowedFlags, BOOLEAN_FLAGS, CHANGE_FLAGS, COMMAND_LIST, findCommand, FLAGS, GLOBAL_FLAGS, renderCommandHelp, renderHelp, renderMarkdown, REPEATABLE_FLAGS } from "./core/commands.js";
42
48
  import { closest } from "./core/validateConfig.js";
43
49
  import { recordEvent } from "./core/events.js";
44
50
  import { buildReport, renderReport } from "./skills/report.js";
45
51
  import { homedir } from 'node:os';
46
52
  import { runTool } from "./core/proc.js";
53
+ import { connectHints, MockServer, mockApiFromProject, mockApiFromSpec, SCENARIOS } from "./skills/mockServer.js";
47
54
  /** Operating-system errors that are not codz bugs, with what to do about them. */
48
55
  const SYSTEM_ERRORS = {
49
56
  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.' },
@@ -52,7 +59,15 @@ const SYSTEM_ERRORS = {
52
59
  EROFS: { code: 'read_only', say: 'This folder is on a read-only disk.', hint: 'Run codz in a writable copy of the project.' },
53
60
  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
61
  };
55
- export const VERSION = '0.1.0';
62
+ /** From package.json (one folder up from src/ and dist/), so --version matches what npm installed. */
63
+ export const VERSION = (() => {
64
+ try {
65
+ return JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8')).version;
66
+ }
67
+ catch {
68
+ return '0.0.0';
69
+ }
70
+ })();
56
71
  export function parseArgs(argv) {
57
72
  const args = { positional: [], flags: {}, multi: {} };
58
73
  for (let i = 0; i < argv.length; i++) {
@@ -467,7 +482,7 @@ export async function main(argv) {
467
482
  const started = Date.now();
468
483
  const event = (ok, code) => {
469
484
  const [cmd, sub] = args.positional;
470
- if (!cmd || ['report', 'completion', 'help', 'mcp'].includes(cmd) || args.flags.help || args.flags.version)
485
+ if (!cmd || ['report', 'completion', 'help'].includes(cmd) || args.flags.help || args.flags.version)
471
486
  return;
472
487
  const spec = findCommand(cmd, sub);
473
488
  const name = spec ? (spec.sub ? `${spec.cmd} ${spec.sub}` : spec.cmd) : 'unknown';
@@ -540,6 +555,18 @@ async function run(args, out) {
540
555
  return 0;
541
556
  }
542
557
  }
558
+ // The command table as data, for editors, completion and add-ons that call codz.
559
+ const optionsOf = (names) => names.map((name) => ({ name, ...FLAGS[name] }));
560
+ if (cmd === 'help' && flags.json) {
561
+ process.stdout.write(JSON.stringify({
562
+ version: VERSION,
563
+ commands: COMMAND_LIST.map((c) => ({ ...c, options: optionsOf(c.flags) })),
564
+ changeFlags: CHANGE_FLAGS,
565
+ globalFlags: GLOBAL_FLAGS,
566
+ markdown: renderMarkdown(VERSION),
567
+ }, null, 2) + '\n');
568
+ return 0;
569
+ }
543
570
  if (!cmd || flags.help || flags.h || cmd === 'help') {
544
571
  out.print(HELP);
545
572
  return cmd || flags.help || flags.h ? 0 : 1;
@@ -632,8 +659,6 @@ async function run(args, out) {
632
659
  process.stdout.write(completionScript(sub));
633
660
  return 0;
634
661
  }
635
- if (cmd === 'mcp')
636
- return serveMcp(root, VERSION);
637
662
  if (cmd === 'doctor' && flags.setup === true) {
638
663
  let a = genericAdapter;
639
664
  try {
@@ -692,7 +717,69 @@ async function run(args, out) {
692
717
  out.result({ ...report, failed: diffFails(report, failOn) });
693
718
  return diffFails(report, failOn) ? 1 : 0;
694
719
  }
720
+ // mock serve: a local fake API; with --spec it works anywhere, without a project.
721
+ if (cmd === 'mock' && sub === 'serve') {
722
+ const num = (k, def) => {
723
+ const v = flags[k];
724
+ if (v === undefined)
725
+ return def;
726
+ const n = Number(v);
727
+ if (!Number.isInteger(n) || n < 0)
728
+ throw new ToolError('bad_args', `--${k} must be a whole number, got "${v}".`);
729
+ return n;
730
+ };
731
+ const port = num('port', 4010);
732
+ let api;
733
+ let framework = null;
734
+ let map = null;
735
+ const hasProject = existsSync(join(root, CONFIG_FILE));
736
+ if (hasProject) {
737
+ try {
738
+ const a = adapterFor(root).adapter;
739
+ framework = a.id;
740
+ map = (await loadMap(root, a, loadConfig(root).conventions)).map;
741
+ }
742
+ catch {
743
+ framework = null;
744
+ }
745
+ }
746
+ if (typeof flags.spec === 'string')
747
+ api = mockApiFromSpec(resolve(flags.spec));
748
+ else if (map && framework)
749
+ api = mockApiFromProject(root, map, framework === 'flutter' ? 'dart' : framework === 'android' ? 'kt' : framework === 'ios' ? 'swift' : 'ts');
750
+ else
751
+ throw new ToolError('bad_args', 'No codz project here, so there are no endpoints to mock.', 'Serve an API file instead: codz mock serve --spec api.yaml (or run codz init in your app first).');
752
+ const lan = flags.lan === true;
753
+ const scenario = (typeof flags.scenario === 'string' ? flags.scenario : 'normal');
754
+ if (!SCENARIOS.includes(scenario))
755
+ throw new ToolError('bad_args', `Unknown scenario "${scenario}".`, `Choose: ${SCENARIOS.join(', ')}.`);
756
+ const server = new MockServer(api, { scenario, port, host: lan ? '0.0.0.0' : '127.0.0.1', delay: num('delay', 300), items: num('items', 5), log: (line) => out.print(` ${line}`) });
757
+ const actual = await server.start();
758
+ out.print(`Mock API on http://localhost:${actual}, from ${api.source}:`);
759
+ for (const r of api.routes.slice(0, 40))
760
+ out.print(` ${r.method.padEnd(6)} ${r.path}`);
761
+ if (api.routes.length > 40)
762
+ out.print(` … and ${api.routes.length - 40} more`);
763
+ out.print('');
764
+ for (const h of connectHints(map, framework, actual, lan))
765
+ out.print(h);
766
+ out.print(`Switch scenarios (empty, error, unauthorized, slow, offline) and watch requests: http://localhost:${actual}/__codz`);
767
+ if (scenario !== 'normal')
768
+ out.print(`Starting in scenario: ${scenario}.`);
769
+ out.print('Lists, create, edit and delete share in-memory data (reset it on that page). Press Ctrl+C to stop.');
770
+ out.print('');
771
+ await new Promise((done) => {
772
+ process.once('SIGINT', () => done());
773
+ process.once('SIGTERM', () => done());
774
+ });
775
+ await server.stop();
776
+ return 0;
777
+ }
695
778
  const { adapter } = adapterFor(root);
779
+ if (typeof flags['model-dir'] === 'string') {
780
+ const dir = normalizeModelDir(flags['model-dir'], adapter.id);
781
+ setRunConventions({ modelsFolder: dir, ...(adapter.id === 'nextjs' || adapter.id === 'react' || adapter.id === 'react-native' ? { modelsLayout: 'per-file' } : {}) });
782
+ }
696
783
  switch (cmd) {
697
784
  case 'report': {
698
785
  const { map } = await loadMap(root, adapter, loadConfig(root).conventions);
@@ -710,9 +797,66 @@ async function run(args, out) {
710
797
  out.result({ map, supportedSetup: adapter.supportedSetup });
711
798
  return 0;
712
799
  }
800
+ case 'assets':
801
+ return handleChange(root, out, flags, await planAssets(root, adapter), adapter);
802
+ case 'env': {
803
+ if (sub === 'list') {
804
+ const envs = await listEnvs(root, adapter);
805
+ if (!envs.length)
806
+ out.print('No API environments yet. Add one with: codz env add staging --base-url https://staging.example.com');
807
+ for (const e of envs)
808
+ out.print(` ${e.name.padEnd(12)} ${e.baseUrl} (${e.where})`);
809
+ out.result({ environments: envs });
810
+ return 0;
811
+ }
812
+ if (sub === 'add')
813
+ return handleChange(root, out, flags, await planEnvAdd(root, adapter, positional[2], typeof flags['base-url'] === 'string' ? flags['base-url'] : undefined, { force: flags.force === true }), adapter);
814
+ if (sub === 'use') {
815
+ if (!positional[2])
816
+ throw new ToolError('bad_args', 'Which environment? codz env use <name>');
817
+ return handleChange(root, out, flags, await planEnvUse(root, adapter, positional[2]), adapter);
818
+ }
819
+ throw new ToolError('bad_args', `Unknown command "env ${sub ?? ''}".`, 'Commands: env add <name> --base-url <url>, env list, env use <name>');
820
+ }
821
+ case 'model': {
822
+ const name = positional.slice(1).join(' ').trim();
823
+ if (!name)
824
+ throw new ToolError('bad_args', 'Which model? codz model <Name> --from-clipboard', 'Or: codz model Order --sample order.json');
825
+ let text;
826
+ let from;
827
+ if (flags['from-clipboard'] === true || flags.clipboard === true)
828
+ [text, from] = [readClipboard(), 'The clipboard'];
829
+ else if (typeof flags.sample === 'string' || args.multi.sample?.length) {
830
+ const file = typeof flags.sample === 'string' ? flags.sample : args.multi.sample[0];
831
+ const t = readText(resolve(root, file)) ?? readText(resolve(file));
832
+ if (t === null)
833
+ throw new ToolError('no_sample', `Cannot read ${file}.`);
834
+ [text, from] = [t, file];
835
+ }
836
+ else {
837
+ text = await readStdin();
838
+ if (!text.trim() && process.stdin.isTTY && !out.json) {
839
+ process.stderr.write('Paste the JSON, then press Ctrl-D (Ctrl-Z then Enter on Windows).\n');
840
+ const chunks = [];
841
+ for await (const chunk of process.stdin)
842
+ chunks.push(chunk);
843
+ text = Buffer.concat(chunks).toString('utf8');
844
+ }
845
+ from = 'The pasted text';
846
+ }
847
+ const cs = await planModel(root, adapter, name, parseJsonText(text, from), { noUnwrap: flags['no-unwrap'] === true, force: flags.force === true, split: flags.split === true });
848
+ return handleChange(root, out, flags, cs, adapter);
849
+ }
850
+ case 'field': {
851
+ const [model, ...rest] = positional.slice(2);
852
+ if ((sub !== 'add' && sub !== 'remove') || !model)
853
+ throw new ToolError('bad_args', `Unknown command "field ${sub ?? ''}".`, 'Usage: codz field add <Model> <name:type> ... | codz field remove <Model> <field> ...');
854
+ const cs = sub === 'add' ? await planFieldAdd(root, adapter, model, rest) : await planFieldRemove(root, adapter, model, rest);
855
+ return handleChange(root, out, flags, cs, adapter);
856
+ }
713
857
  case 'mock': {
714
858
  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');
859
+ throw new ToolError('bad_args', `Unknown command "mock ${sub ?? ''}".`, 'Usage: codz mock add <endpoint> [--sample file.json] | codz mock add --all | codz mock serve [--spec api.yaml] [--port 4010]');
716
860
  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
861
  return handleChange(root, out, flags, cs, adapter);
718
862
  }
@@ -724,6 +868,8 @@ async function run(args, out) {
724
868
  const cs = await planLocaleAdd(root, adapter, lang, { from: typeof flags.from === 'string' ? flags.from : undefined });
725
869
  return handleChange(root, out, flags, cs, adapter);
726
870
  }
871
+ if (sub === 'extract')
872
+ return handleChange(root, out, flags, await planLocaleExtract(root, adapter, positional.slice(2)), adapter);
727
873
  if (sub === 'key') {
728
874
  const key = positional[2];
729
875
  const text = positional.slice(3).join(' ');
@@ -858,7 +1004,9 @@ async function run(args, out) {
858
1004
  const name = positional[2];
859
1005
  if (!name)
860
1006
  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 });
1007
+ 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, paginate: flags.paginate === true, search: flags.search === true });
1008
+ if ((flags.paginate === true || flags.search === true) && flags.form === true)
1009
+ throw new ToolError('bad_args', '--paginate and --search are for list screens, not --form.');
862
1010
  return handleChange(root, out, flags, cs, adapter);
863
1011
  }
864
1012
  case 'route': {
@@ -898,6 +1046,7 @@ async function run(args, out) {
898
1046
  throw new ToolError('bad_args', 'Which feature? Usage: codz feature add <name> --spec <openapi.yaml>', 'Example: codz feature add orders --spec api.yaml');
899
1047
  const split = (xs) => xs?.flatMap((x) => (typeof x === 'string' ? x.split(',') : [])).map((x) => x.trim()).filter(Boolean) ?? [];
900
1048
  const r = await planFeatureAdd(root, adapter, {
1049
+ split: flags.split === true,
901
1050
  name,
902
1051
  spec: typeof flags.spec === 'string' ? flags.spec : undefined,
903
1052
  tags: split(args.multi.tag),
@@ -1023,6 +1172,7 @@ async function run(args, out) {
1023
1172
  }
1024
1173
  const showProgress = !out.json && process.stderr.isTTY === true;
1025
1174
  const r = await planApiImport(root, adapter, {
1175
+ split: flags.split === true,
1026
1176
  ...importOpts,
1027
1177
  onProgress: showProgress
1028
1178
  ? (done, total, name) => {
@@ -1042,6 +1192,7 @@ async function run(args, out) {
1042
1192
  if (flags.check === true)
1043
1193
  setCacheWrites(false);
1044
1194
  const r = await planApiSync(root, adapter, {
1195
+ split: flags.split === true,
1045
1196
  file,
1046
1197
  tags: split(args.multi.tag),
1047
1198
  paths: split([flags.path]),
@@ -1096,6 +1247,7 @@ async function run(args, out) {
1096
1247
  noUnwrap: flags['no-unwrap'] === true,
1097
1248
  into: typeof flags.into === 'string' ? flags.into : undefined,
1098
1249
  noLoad: flags['no-load'] === true,
1250
+ split: flags.split === true,
1099
1251
  };
1100
1252
  if (flags.clipboard === true && !opts.curl && !opts.url)
1101
1253
  opts.paste = readClipboard();
@@ -48,13 +48,19 @@ export function fingerprint(root, configText) {
48
48
  }
49
49
  return hash(parts.join('\n'));
50
50
  }
51
+ /** Conventions set for one run from the command line (--model-dir); never cached or saved. */
52
+ let runOverrides = {};
53
+ export function setRunConventions(c) {
54
+ runOverrides = { ...c };
55
+ }
56
+ const withRun = (map) => (Object.keys(runOverrides).length ? { ...map, conventions: { ...map.conventions, ...runOverrides } } : map);
51
57
  export async function loadMap(root, adapter, conventions, opts = {}) {
52
58
  const cachePath = join(root, TOOL_DIR, 'cache', 'map.json');
53
59
  const configText = readText(join(root, 'codz.config.yaml')) ?? '';
54
60
  const fp = fingerprint(root, configText);
55
61
  const cached = readJson(cachePath);
56
62
  if (!opts.force && cached && cached.schema === MAP_SCHEMA_VERSION && cached.adapter === `${adapter.id}@${adapter.version}` && cached.tool === toolFingerprint() && cached.fingerprint === fp) {
57
- return { map: cached.map, fromCache: true };
63
+ return { map: withRun(cached.map), fromCache: true };
58
64
  }
59
65
  // An old schema or adapter version is simply rebuilt, never migrated.
60
66
  const map = await adapter.scan(root, conventions);
@@ -63,7 +69,7 @@ export async function loadMap(root, adapter, conventions, opts = {}) {
63
69
  toolDir(root, 'cache');
64
70
  writeFileSync(cachePath, JSON.stringify(file, null, 2));
65
71
  }
66
- return { map, fromCache: false };
72
+ return { map: withRun(map), fromCache: false };
67
73
  }
68
74
  export function invalidateMap(root) {
69
75
  const cachePath = join(root, TOOL_DIR, 'cache', 'map.json');