codz-cli 0.1.3 → 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 +140 -17
- package/bin/codz.js +0 -0
- package/dist/adapters/common.js +3 -1
- package/dist/adapters/flutterScan.js +1 -1
- package/dist/cli.js +162 -10
- package/dist/core/cache.js +8 -2
- package/dist/core/commands.js +25 -6
- package/dist/core/modelGroup.js +180 -0
- package/dist/core/validateConfig.js +6 -0
- package/dist/skills/androidUi.js +167 -1
- package/dist/skills/androidViews.js +181 -1
- package/dist/skills/apiAdd.js +3 -0
- package/dist/skills/apiImport.js +3 -1
- package/dist/skills/apiSync.js +3 -1
- package/dist/skills/assets.js +289 -0
- package/dist/skills/env.js +509 -0
- package/dist/skills/feature.js +1 -1
- package/dist/skills/field.js +419 -0
- package/dist/skills/iosUi.js +154 -0
- package/dist/skills/locale.js +13 -4
- package/dist/skills/localeExtract.js +540 -0
- package/dist/skills/mockServer.js +512 -0
- package/dist/skills/model.js +100 -0
- package/dist/skills/paging.js +185 -0
- package/dist/skills/reactNativeUi.js +72 -0
- package/dist/skills/reactUi.js +55 -0
- package/dist/skills/screen.js +553 -23
- package/docs/COMMANDS.md +109 -8
- package/package.json +2 -3
- package/dist/skills/mcp.js +0 -175
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.
|
|
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
|
|
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
|
package/dist/adapters/common.js
CHANGED
|
@@ -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
|
-
|
|
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 --
|
|
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
|
-
|
|
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'
|
|
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();
|
package/dist/core/cache.js
CHANGED
|
@@ -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');
|