@codefast/cli 0.3.16-canary.2 → 0.4.0-canary.4

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 (41) hide show
  1. package/README.md +254 -86
  2. package/dist/arrange/analyze.mjs +2 -2
  3. package/dist/arrange/command.mjs +51 -27
  4. package/dist/arrange/domain/analyze-service.mjs +2 -2
  5. package/dist/arrange/domain/ast/collectors-cn.mjs +1 -1
  6. package/dist/arrange/domain/ast/collectors-tv.mjs +2 -2
  7. package/dist/arrange/domain/ast/helpers.mjs +2 -2
  8. package/dist/arrange/domain/ast/simplify-targets.mjs +109 -0
  9. package/dist/arrange/domain/ast/targets.mjs +25 -10
  10. package/dist/arrange/domain/grouping-service.mjs +1 -1
  11. package/dist/arrange/domain/grouping.mjs +5 -5
  12. package/dist/arrange/domain/imports.mjs +50 -2
  13. package/dist/arrange/domain/token-classifier.mjs +12 -12
  14. package/dist/arrange/output.mjs +11 -3
  15. package/dist/arrange/process-file.mjs +1 -1
  16. package/dist/arrange/scan-target.mjs +10 -1
  17. package/dist/arrange/simplify-process-file.mjs +35 -0
  18. package/dist/arrange/simplify-sync.mjs +32 -0
  19. package/dist/arrange/suggest.mjs +1 -1
  20. package/dist/arrange/sync.mjs +1 -1
  21. package/dist/arrange/workspace.mjs +1 -1
  22. package/dist/cli.mjs +1 -1
  23. package/dist/core/cli/result-handle.mjs +1 -1
  24. package/dist/core/config/schema.mjs +31 -10
  25. package/dist/core/source-text-edit.mjs +1 -1
  26. package/dist/core/workspace/resolver.mjs +3 -3
  27. package/dist/mirror/cli-result.mjs +2 -1
  28. package/dist/mirror/cli-schema.mjs +2 -1
  29. package/dist/mirror/command.mjs +12 -11
  30. package/dist/mirror/domain/exports.mjs +16 -19
  31. package/dist/mirror/output.mjs +3 -0
  32. package/dist/mirror/supplement-exports.mjs +114 -0
  33. package/dist/mirror/sync-reporter.mjs +5 -2
  34. package/dist/mirror/sync-workspace-package.mjs +35 -29
  35. package/dist/mirror/sync.mjs +2 -1
  36. package/dist/mirror/write-exports.mjs +3 -2
  37. package/dist/tag/command.mjs +6 -6
  38. package/dist/tag/output.mjs +1 -1
  39. package/dist/tag/sync.mjs +1 -1
  40. package/dist/tag/target-runner.mjs +1 -1
  41. package/package.json +10 -10
package/README.md CHANGED
@@ -18,9 +18,13 @@ A small developer CLI for maintenance tasks in a TypeScript monorepo — Tailwin
18
18
  - [Global options](#global-options)
19
19
  - [Exit codes](#exit-codes)
20
20
  - [`arrange`](#arrange)
21
- - [`mirror sync`](#mirror-sync)
22
- - [`tag` / `annotate`](#tag--annotate)
21
+ - [`mirror`](#mirror)
22
+ - [`tag`](#tag)
23
23
  - [Configuration (`codefast.config.*`)](#configuration-codefastconfig)
24
+ - [Full skeleton](#full-skeleton)
25
+ - [`mirror` configuration](#mirror-configuration)
26
+ - [`tag` configuration](#tag-configuration)
27
+ - [`arrange` configuration](#arrange-configuration)
24
28
  - [Lifecycle hooks](#lifecycle-hooks)
25
29
  - [Grouping philosophy — Render Pipeline Order](#grouping-philosophy--render-pipeline-order)
26
30
  - [Troubleshooting](#troubleshooting)
@@ -36,7 +40,9 @@ Three recurring maintenance chores you don't want to script by hand:
36
40
 
37
41
  - **`arrange`** — regroup Tailwind class strings inside `cn()` / `tv()` calls in render-pipeline order.
38
42
  - **`mirror`** — regenerate `package.json` `exports` fields from built `dist/` trees across a pnpm workspace.
39
- - **`tag`** (alias **`annotate`**) — add `@since <version>` JSDoc tags to exported declarations that are missing version metadata.
43
+ - **`tag`** — add `@since <version>` JSDoc tags to exported declarations that are missing version metadata.
44
+
45
+ Each top-level command **performs its action by default** (writing files). Pass `--dry-run` to preview without writing. `arrange` additionally exposes a read-only `inspect` report.
40
46
 
41
47
  ```mermaid
42
48
  flowchart LR
@@ -45,19 +51,16 @@ flowchart LR
45
51
  R --> M[mirror]
46
52
  R --> T[tag]
47
53
 
48
- A --> A0[analyze]
49
- A --> A1[preview]
50
- A --> A2[apply]
51
- A --> A3[group]
52
-
53
- M --> M0[sync]
54
+ A --> A0[inspect]
55
+ A --> A1[simplify]
56
+ A --> A2[group]
54
57
  ```
55
58
 
56
59
  ---
57
60
 
58
61
  ## Requirements
59
62
 
60
- - Node.js `>= 22.0.0`
63
+ - Node.js `>= 24.0.0`
61
64
  - pnpm (recommended — the CLI discovers workspaces via `pnpm-workspace.yaml`)
62
65
 
63
66
  ---
@@ -83,17 +86,17 @@ npx @codefast/cli --help
83
86
  ## Quick Start
84
87
 
85
88
  ```bash
86
- # Analyze Tailwind classes in the nearest package
87
- codefast arrange analyze
89
+ # Inspect Tailwind classes in the nearest package (read-only report)
90
+ codefast arrange inspect
88
91
 
89
92
  # Preview proposed rewrites — no files written
90
- codefast arrange preview packages/ui/src/components
93
+ codefast arrange --dry-run packages/ui/src/components
91
94
 
92
95
  # Apply after reviewing
93
- codefast arrange apply packages/ui/src/components
96
+ codefast arrange packages/ui/src/components
94
97
 
95
98
  # Regenerate every package's `exports` from built dist/
96
- codefast mirror sync
99
+ codefast mirror
97
100
 
98
101
  # Add @since <version> to exported APIs under ./src
99
102
  codefast tag
@@ -109,15 +112,17 @@ codefast tag
109
112
  | `-V`, `--version` | Print the CLI version and exit. |
110
113
  | `-h`, `--help` | Show contextual help for the invoked command. |
111
114
 
115
+ > **Placement.** Global flags must come **before** the command name (git-style), e.g. `codefast --no-color mirror`. A flag after the command name binds to that command.
116
+
112
117
  ---
113
118
 
114
119
  ## Exit codes
115
120
 
116
- | Code | Meaning |
117
- | ---- | -------------------------------------------------------------------------------------------------------- |
118
- | `0` | Success. |
119
- | `1` | General failure (missing paths, infrastructure errors, partial failures in `mirror sync`, failed hooks). |
120
- | `2` | Invalid invocation or input (Zod schema validation on CLI requests — `CLI_EXIT_USAGE`). |
121
+ | Code | Meaning |
122
+ | ---- | --------------------------------------------------------------------------------------------------- |
123
+ | `0` | Success. |
124
+ | `1` | General failure (missing paths, infrastructure errors, partial failures in `mirror`, failed hooks). |
125
+ | `2` | Invalid invocation or input (Zod schema validation on CLI requests — `CLI_EXIT_USAGE`). |
121
126
 
122
127
  Diagnostics go to **stderr**; primary command output goes to **stdout**. When a subcommand accepts `--json`, only the JSON object is written to stdout and all human progress is suppressed, so the stream stays pipeline-safe.
123
128
 
@@ -133,25 +138,28 @@ When `[target]` is omitted, `arrange` auto-detects the **nearest package directo
133
138
 
134
139
  ### Workflow
135
140
 
136
- | Step | Command | Effect |
137
- | ---- | ----------------------------------- | ------------------------------------- |
138
- | 1 | `codefast arrange analyze [target]` | Report only — no files changed |
139
- | 2 | `codefast arrange preview [target]` | Show exactly what `apply` would write |
140
- | 3 | `codefast arrange apply [target]` | Write the changes |
141
+ | Step | Command | Effect |
142
+ | ---- | ------------------------------------- | ---------------------------------------------- |
143
+ | 1 | `codefast arrange inspect [target]` | Report only — no files changed |
144
+ | 2 | `codefast arrange --dry-run [target]` | Show exactly what a bare `arrange` would write |
145
+ | 3 | `codefast arrange [target]` | Write the changes |
141
146
 
142
- ### Flags (preview / apply)
147
+ ### Flags (`arrange`)
143
148
 
144
149
  | Flag | Description |
145
150
  | -------------------- | --------------------------------------------------------------------------- |
151
+ | `--dry-run` | Preview the rewrite without writing files. |
146
152
  | `--with-class-name` | Append `className` as the last argument when rewriting a `cn(...)` call. |
147
153
  | `--cn-import <spec>` | Override the module specifier used when a missing `cn` import is added. |
148
154
  | `--json` | Print a single JSON object on stdout; suppresses human progress and colors. |
149
155
 
150
- `analyze` also supports `--json`.
156
+ `inspect` also supports `--json`. `simplify` accepts `--dry-run` and `--json`.
157
+
158
+ > **Test files are skipped.** Directory scans exclude `*.test.*` / `*.spec.*` files — a `cn(...)` inside an assertion is test data, not styling to reformat. Pass such a file explicitly to override.
151
159
 
152
160
  ### `arrange group` — one-shot classification
153
161
 
154
- Groups a class string without touching the filesystem. Useful for checking how classes would be grouped before running `apply`:
162
+ Groups a class string without touching the filesystem. Useful for checking how classes would be grouped before running `arrange`:
155
163
 
156
164
  ```bash
157
165
  # Quoted string
@@ -172,44 +180,47 @@ codefast arrange group --tv "flex items-center gap-2"
172
180
 
173
181
  ### `--json` payloads
174
182
 
175
- | Subcommand | Payload highlights |
176
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
177
- | `analyze` | `schemaVersion`, `analyzeRootPath`, full `report` (same data as the human report). |
178
- | `preview` / `apply` | `schemaVersion`, `write`, `ok` (`false` if the `onAfterWrite` hook failed), full `result` (`filePaths`, `modifiedFiles`, `totalFound`, …). |
179
- | `group` | `schemaVersion`, `primaryLine`, `bucketsCommentLine`. |
183
+ | Subcommand | Payload highlights |
184
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
185
+ | `arrange` | `schemaVersion`, `write`, `ok` (`false` if the `onAfterWrite` hook failed), full `result` (`filePaths`, `modifiedFiles`, `totalFound`, …). |
186
+ | `inspect` | `schemaVersion`, `analyzeRootPath`, full `report` (same data as the human report). |
187
+ | `simplify` | `schemaVersion`, `write`, `ok`, full `result`. |
188
+ | `group` | `schemaVersion`, `primaryLine`, `bucketsCommentLine`. |
180
189
 
181
190
  ---
182
191
 
183
- ## `mirror sync`
192
+ ## `mirror`
184
193
 
185
194
  Scans built `dist/` trees and regenerates the `exports` field of every workspace package. Run it from anywhere inside the monorepo — the workspace root is discovered via `pnpm-workspace.yaml`.
186
195
 
187
196
  ```bash
188
- codefast mirror sync # all workspace packages
189
- codefast mirror sync packages/ui # one package (path relative to repo root)
190
- codefast mirror sync -v # verbose diagnostics
191
- codefast mirror sync --json # JSON summary for scripts / CI
197
+ codefast mirror # all workspace packages
198
+ codefast mirror packages/ui # one package (path relative to repo root)
199
+ codefast mirror --dry-run # preview — report changes without writing
200
+ codefast mirror -v # verbose diagnostics
201
+ codefast mirror --json # JSON summary for scripts / CI
192
202
  ```
193
203
 
194
- | Flag | Description |
195
- | ----------------- | --------------------------------------------------------------------- |
196
- | `-v`, `--verbose` | Print extra diagnostics. |
197
- | `--json` | Print a single `{ schemaVersion, ok, elapsedSeconds, stats }` object. |
204
+ | Flag | Description |
205
+ | ----------------- | ---------------------------------------------------------------------------- |
206
+ | `--dry-run` | Report what would change without writing any `package.json`. |
207
+ | `-v`, `--verbose` | Print extra diagnostics. |
208
+ | `--json` | Print a single `{ schemaVersion, ok, write, elapsedSeconds, stats }` object. |
198
209
 
199
- > **Build first.** `mirror sync` reads from `dist/`. Run your build before syncing or exports will reflect stale output.
210
+ > **Build first.** `mirror` reads from `dist/`. Run your build before syncing or exports will reflect stale output.
200
211
 
201
212
  Exit code is `1` when any package fails (`stats.packagesErrored > 0`), `0` otherwise.
202
213
 
203
214
  ---
204
215
 
205
- ## `tag` / `annotate`
216
+ ## `tag`
206
217
 
207
218
  Scans `.ts` / `.tsx` sources and adds `@since <current-package-version>` to exported declarations that don't already carry one. The version is read from the nearest `package.json` walking up from the target path.
208
219
 
209
220
  ```bash
210
221
  codefast tag # auto-discover workspace packages from cwd
211
- codefast tag packages/ui/src # annotate a custom target
212
- codefast annotate --dry-run # preview only, do not write
222
+ codefast tag packages/ui/src # tag a custom target
223
+ codefast tag --dry-run # preview only, do not write
213
224
  codefast tag --json # JSON summary for scripts / CI
214
225
  ```
215
226
 
@@ -228,37 +239,46 @@ What it updates:
228
239
 
229
240
  ## Configuration (`codefast.config.*`)
230
241
 
231
- Create `codefast.config.js`, `.mjs`, `.cjs`, or `.json` at the repo root. Each command reads its own slice.
242
+ Create a config file at the **monorepo root** (next to `pnpm-workspace.yaml`). Supported names, in priority order:
243
+
244
+ | File name | Format |
245
+ | ---------------------- | --------------------------------------- |
246
+ | `codefast.config.mjs` | ES module — `export default { … }` |
247
+ | `codefast.config.js` | ES module (if `"type":"module"`) or CJS |
248
+ | `codefast.config.cjs` | CommonJS — `module.exports = { … }` |
249
+ | `codefast.config.json` | Plain JSON (no functions — no hooks) |
250
+
251
+ The file is found by walking up from the working directory, so running the CLI from any sub-directory still picks up the root config.
252
+
253
+ > **Security.** `.js`, `.mjs`, and `.cjs` files are executed via `import()`. Only run `codefast` inside repositories you trust.
254
+
255
+ ---
256
+
257
+ ### Full skeleton
232
258
 
233
259
  ```javascript
234
260
  // codefast.config.mjs
235
261
  import { execSync } from "node:child_process";
236
262
 
237
263
  export default {
264
+ // ─── mirror ────────────────────────────────────────────────────────────────
265
+ // Keys are package names (from package.json#name).
266
+ // Set a package to `false` to skip it entirely.
267
+ // Omit a package to process it with default settings.
238
268
  mirror: {
239
- skipPackages: ["@acme/internal"],
240
- pathTransformations: {
241
- "@acme/ui": { removePrefix: "./components/" },
242
- },
243
- customExports: {
244
- "@acme/ui": {
245
- "./css/*": "./src/styles/*",
246
- },
247
- },
248
- cssExports: {
249
- // Shorthand: `true` enables default CSS export detection
250
- "@acme/theme": true,
251
- // Or configure explicitly:
252
- "@acme/ui": {
253
- enabled: true,
254
- forceExportFiles: false,
255
- customExports: {
256
- "./tokens.css": "./dist/tokens.css",
257
- },
258
- },
269
+ "@acme/ui": {
270
+ strip: "./components/",
271
+ exports: { "./css/*": "./src/css/*" },
272
+ source: true, // default: true
273
+ types: true, // default: true
274
+ import: true, // default: true
275
+ css: true,
259
276
  },
277
+ "@acme/internal": false,
278
+ "@acme/docs": false,
260
279
  },
261
280
 
281
+ // ─── tag ───────────────────────────────────────────────────────────────────
262
282
  tag: {
263
283
  skipPackages: ["@acme/internal"],
264
284
  onAfterWrite: ({ files }) => {
@@ -266,6 +286,7 @@ export default {
266
286
  },
267
287
  },
268
288
 
289
+ // ─── arrange ───────────────────────────────────────────────────────────────
269
290
  arrange: {
270
291
  onAfterWrite: ({ files }) => {
271
292
  execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
@@ -274,31 +295,177 @@ export default {
274
295
  };
275
296
  ```
276
297
 
277
- Notes:
298
+ ---
299
+
300
+ ### `mirror` configuration
301
+
302
+ `mirror` is a record keyed by **package name** (the `name` field in the package's `package.json`, e.g. `"@acme/ui"`).
303
+
304
+ #### Skipping a package
305
+
306
+ Set a package to `false` to exclude it from `codefast mirror` entirely:
307
+
308
+ ```javascript
309
+ mirror: {
310
+ "@acme/internal": false,
311
+ "@acme/docs": false,
312
+ }
313
+ ```
314
+
315
+ Packages not mentioned in the config are processed with default settings.
316
+
317
+ #### Per-package options
318
+
319
+ Each package entry is an object with the following fields:
320
+
321
+ | Field | Type | Default | Description |
322
+ | ---------- | ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
323
+ | `source` | `boolean \| string` | `true` | Add a `source` condition to each export entry pointing to the original `.ts` file. `true` auto-derives the path (`./src/<module>.ts`). Pass a string to set the root-export path explicitly (`"./src/index.tsx"`). Set to `false` to omit. |
324
+ | `types` | `boolean` | `true` | Include the `types` condition when a `.d.ts` file is present. Set to `false` to omit. |
325
+ | `import` | `boolean` | `true` | Include the `import` condition. Set to `false` to omit (useful for CJS-only packages). |
326
+ | `strip` | `string` | — | Strip a leading path segment from generated export specifiers. See below. |
327
+ | `exports` | `Record<string, string>` | — | Add or override specific export specifiers. See below. |
328
+ | `preserve` | `boolean` | — | Keep the existing `package.json#exports` and only fill in missing conditions. |
329
+ | `css` | `boolean \| CssConfig` | — | Enable CSS export detection. See below. |
330
+
331
+ #### `strip`
332
+
333
+ Removes a fixed prefix from every generated export specifier. Use this when a package's `dist/` mirrors deep directory structure that you want to flatten in the public API.
334
+
335
+ ```javascript
336
+ // Without strip, dist/components/button.mjs → "./components/button"
337
+ // With strip: "./components/", it becomes → "./button"
338
+ "@acme/ui": {
339
+ strip: "./components/",
340
+ }
341
+ ```
342
+
343
+ The original file path is preserved for sorting — only the public specifier changes.
344
+
345
+ #### `exports`
346
+
347
+ Adds or overrides specific specifiers in the final export map. Keys and values are the exact strings written into `package.json#exports`.
348
+
349
+ ```javascript
350
+ "@acme/ui": {
351
+ exports: {
352
+ "./css/*": "./src/css/*", // wildcard passthrough to sources
353
+ "./tokens": "./dist/tokens.js", // explicit extra entry
354
+ },
355
+ }
356
+ ```
278
357
 
279
- - Keys in `mirror.skipPackages` / `pathTransformations` / `customExports` / `cssExports` are **package names** from `package.json#name` (e.g. `@acme/ui`). Path-based keys such as `packages/ui` are deprecated and will be removed.
280
- - `cssExports[pkg]` accepts a boolean shorthand or the full `{ enabled, customExports, forceExportFiles }` object.
281
- - `tag.skipPackages` lists package names to skip entirely when `codefast tag` is run without an explicit target.
358
+ Extra entries are merged after auto-generation. They win over anything the scanner would produce for the same specifier. `./package.json` cannot be overridden.
282
359
 
283
- > **Security.** `.js`, `.mjs`, and `.cjs` config files are loaded via `import()` — only run `codefast` inside repositories you trust.
360
+ #### `preserve`
361
+
362
+ Keeps the existing `package.json#exports` map exactly as-is and only fills in missing conditions (`source`, `types`, `import`) for each entry — no `dist/` scan is performed. Use this when you maintain the exports map by hand and only want the CLI to supplement missing conditions.
363
+
364
+ ```javascript
365
+ "@acme/tailwind-variants": {
366
+ preserve: true,
367
+ }
368
+ ```
369
+
370
+ #### `css`
371
+
372
+ Controls CSS file export generation. `mirror` scans `dist/` for `.css` files and writes wildcard or per-file export entries.
373
+
374
+ ```javascript
375
+ // Shorthand: auto-detect all CSS files in dist/
376
+ "@acme/theme": { css: true }
377
+
378
+ // Full config:
379
+ "@acme/ui": {
380
+ css: {
381
+ enabled: true,
382
+ // Force individual file entries instead of directory wildcards:
383
+ forceExportFiles: false,
384
+ // Manually add or override individual CSS specifiers:
385
+ customExports: {
386
+ "./tokens.css": "./dist/tokens.css",
387
+ },
388
+ },
389
+ }
390
+
391
+ // Explicitly disable CSS exports for this package:
392
+ "@acme/legacy": { css: false }
393
+ ```
394
+
395
+ When `css` is omitted, CSS files found in `dist/` are still exported by default.
396
+
397
+ #### What `mirror` writes
398
+
399
+ For a package with `dist/button.mjs`, `dist/button.d.ts`, and `source: true`, the generated export entry looks like:
400
+
401
+ ```json
402
+ {
403
+ "./button": {
404
+ "source": "./src/button.ts",
405
+ "types": "./dist/button.d.ts",
406
+ "import": "./dist/button.mjs"
407
+ }
408
+ }
409
+ ```
410
+
411
+ It also updates the top-level `main`, `module`, and `types` fields from the root (`.`) export, and ensures `"dist"` is listed in `files`.
412
+
413
+ ---
414
+
415
+ ### `tag` configuration
416
+
417
+ ```javascript
418
+ tag: {
419
+ // Package names to skip when running without an explicit target
420
+ skipPackages: ["@acme/internal", "@acme/docs"],
421
+
422
+ // Called after files are written — use it to format or lint-fix
423
+ onAfterWrite: ({ files }) => {
424
+ execSync(`prettier --write ${files.join(" ")}`, { stdio: "inherit" });
425
+ },
426
+ }
427
+ ```
428
+
429
+ | Field | Type | Description |
430
+ | -------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
431
+ | `skipPackages` | `string[]` | Package names to skip when `codefast tag` is run without an explicit target. Has no effect when a target path is provided directly. |
432
+ | `onAfterWrite` | `(ctx: { files: string[] }) => void \| Promise<void>` | Lifecycle hook — runs after files are written. |
433
+
434
+ ---
435
+
436
+ ### `arrange` configuration
437
+
438
+ ```javascript
439
+ arrange: {
440
+ // Called after files are written by `codefast arrange`
441
+ onAfterWrite: ({ files }) => {
442
+ execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
443
+ },
444
+ }
445
+ ```
446
+
447
+ | Field | Type | Description |
448
+ | -------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------- |
449
+ | `onAfterWrite` | `(ctx: { files: string[] }) => void \| Promise<void>` | Lifecycle hook — runs after `arrange` writes files. Not called with `--dry-run`. |
284
450
 
285
451
  ---
286
452
 
287
453
  ## Lifecycle hooks
288
454
 
289
- Both `tag` and `arrange apply` expose an `onAfterWrite` hook so teams can plug in their own formatter, lint-fix step, or codemod without embedding one in the CLI core.
455
+ Both `tag` and `arrange` call `onAfterWrite` immediately after writing files to disk. The hook receives the list of written file paths and can run any synchronous or asynchronous work — formatters, linters, codegen, notifications.
290
456
 
291
457
  ```javascript
292
458
  export default {
293
459
  tag: {
294
460
  onAfterWrite: async ({ files }) => {
295
- console.log(`Formatting ${files.length} files…`);
296
- // sync or async work
461
+ // async is supported
462
+ await runFormatter(files);
297
463
  },
298
464
  },
299
465
  arrange: {
300
466
  onAfterWrite: ({ files }) => {
301
- /* … */
467
+ // sync is fine too
468
+ execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
302
469
  },
303
470
  },
304
471
  };
@@ -306,10 +473,11 @@ export default {
306
473
 
307
474
  Contract:
308
475
 
309
- - `tag.onAfterWrite?.({ files })` runs after `codefast tag` writes files.
310
- - `arrange.onAfterWrite?.({ files })` runs after `codefast arrange apply` writes files.
311
- - Hooks may be synchronous or asynchronous (`void | Promise<void>`).
312
- - Hook failures are reported on stderr; both commands exit with code `1` when the hook rejects.
476
+ - `tag.onAfterWrite` fires after `codefast tag` writes JSDoc annotations.
477
+ - `arrange.onAfterWrite` fires after `codefast arrange` rewrites class strings.
478
+ - Hook is **not** called on `--dry-run`.
479
+ - Hooks may be synchronous or `async` (`void | Promise<void>`).
480
+ - If the hook throws or rejects, the command reports the error on stderr and exits with code `1`.
313
481
 
314
482
  ---
315
483
 
@@ -348,14 +516,14 @@ To change placement, extend `classifyBareUtility` in `packages/cli/src/arrange/d
348
516
  **`codefast: command not found`**
349
517
  Install globally with `pnpm add -g @codefast/cli`, or run via `pnpm dlx @codefast/cli <command>`.
350
518
 
351
- **`mirror sync` writes little or no output**
352
- Packages must be built first. Ensure `dist/` exists by running your build step, then re-run `codefast mirror sync`.
519
+ **`mirror` writes little or no output**
520
+ Packages must be built first. Ensure `dist/` exists by running your build step, then re-run `codefast mirror`.
353
521
 
354
- **Unexpected class reorder after `arrange apply`**
355
- Run `arrange preview` first and smoke-test the UI. Some components rely on cascade-sensitive ordering that `arrange` cannot detect automatically.
522
+ **Unexpected class reorder after `arrange`**
523
+ Run `arrange --dry-run` first and smoke-test the UI. Some components rely on cascade-sensitive ordering that `arrange` cannot detect automatically.
356
524
 
357
525
  **`--json` output mixed with progress lines**
358
- Some shells buffer progress writes on stderr into stdout when piping — redirect stderr explicitly: `codefast mirror sync --json 2>/dev/null | jq`.
526
+ Some shells buffer progress writes on stderr into stdout when piping — redirect stderr explicitly: `codefast mirror --json 2>/dev/null | jq`.
359
527
 
360
528
  ---
361
529
 
@@ -377,7 +545,7 @@ A few naming conventions:
377
545
 
378
546
  - **`codefast <command>`** refers to CLI commands exposed via the `bin` entry in `@codefast/cli`.
379
547
  - **Scripts in `packages/cli/package.json`** (`build`, `test`, …) are package-local dev scripts, not CLI commands.
380
- - The root `package.json` includes optional convenience wrappers (e.g. `cli:mirror-sync`, `cli:arrange-analyze`) for common dev workflows.
548
+ - The root `package.json` includes optional convenience wrappers (e.g. `cli:mirror`, `cli:arrange-inspect`) for common dev workflows.
381
549
 
382
550
  ---
383
551
 
@@ -1,8 +1,8 @@
1
- import { AppError, messageFrom } from "../core/errors.mjs";
2
- import { err, ok } from "../core/result.mjs";
3
1
  import { accumulateAnalyzeReportForSourceFile, createEmptyAnalyzeReport } from "./domain/analyze-service.mjs";
2
+ import { AppError, messageFrom } from "../core/errors.mjs";
4
3
  import { scanArrangeTargets } from "./scan-target.mjs";
5
4
  import { parseDomainSourceFile } from "./source-parse.mjs";
5
+ import { err, ok } from "../core/result.mjs";
6
6
  //#region src/arrange/analyze.ts
7
7
  /**
8
8
  * @since 0.3.16-canary.0
@@ -1,15 +1,16 @@
1
- import { parseWithSchema } from "../core/schema-parse.mjs";
2
- import "../core/exit-codes.mjs";
3
1
  import { logger } from "../core/logger.mjs";
4
- import { consumeCliAppError, runCliResultAsync } from "../core/cli/result-handle.mjs";
5
- import { nodeFilesystem } from "../core/filesystem/node.mjs";
6
- import { arrangeAnalyzeDirectoryRequestSchema, arrangeSuggestGroupsRequestSchema, arrangeSyncRunRequestSchema } from "./cli-schema.mjs";
7
- import { prepareArrangeWorkspace } from "./workspace.mjs";
8
2
  import { analyzeDirectory } from "./analyze.mjs";
9
- import { runArrangeSync } from "./sync.mjs";
3
+ import { arrangeAnalyzeDirectoryRequestSchema, arrangeSuggestGroupsRequestSchema, arrangeSyncRunRequestSchema } from "./cli-schema.mjs";
4
+ import { printAnalyzeReport, printGroupFilePreviewFromWork, printSimplifyResult, printSyncResult } from "./output.mjs";
5
+ import { runArrangeSimplify } from "./simplify-sync.mjs";
10
6
  import { suggestCnGroupsFromCli } from "./suggest.mjs";
11
- import { printAnalyzeReport, printGroupFilePreviewFromWork, printSyncResult } from "./output.mjs";
7
+ import { runArrangeSync } from "./sync.mjs";
8
+ import { prepareArrangeWorkspace } from "./workspace.mjs";
12
9
  import { readOptionalPositionalArg } from "../core/cli/positional.mjs";
10
+ import "../core/exit-codes.mjs";
11
+ import { consumeCliAppError, runCliResultAsync } from "../core/cli/result-handle.mjs";
12
+ import { nodeFilesystem } from "../core/filesystem/node.mjs";
13
+ import { parseWithSchema } from "../core/schema-parse.mjs";
13
14
  import process from "node:process";
14
15
  import { Command } from "commander";
15
16
  //#region src/arrange/command.ts
@@ -17,22 +18,8 @@ import { Command } from "commander";
17
18
  * @since 0.3.16-canary.0
18
19
  */
19
20
  function createArrangeCommand() {
20
- const cmd = new Command("arrange").description("Analyze and regroup Tailwind classes in cn() / tv() calls (Tailwind v4)");
21
- cmd.command("analyze").description("Report long strings, nested cn in tv(), and related findings").argument("[target]", "Directory or file (default: nearest package directory from cwd)").option("--json", "Print one JSON object on stdout instead of a human report", false).action(async (target, opts) => {
22
- const prelude = await prepareArrangeWorkspace(nodeFilesystem, {
23
- currentWorkingDirectory: process.cwd(),
24
- rawTarget: readOptionalPositionalArg(target)
25
- });
26
- if (!consumeCliAppError(prelude)) return;
27
- const { resolvedTarget } = prelude.value;
28
- const parsed = parseWithSchema(arrangeAnalyzeDirectoryRequestSchema, { analyzeRootPath: resolvedTarget });
29
- if (!consumeCliAppError(parsed)) return;
30
- const outcome = analyzeDirectory(nodeFilesystem, parsed.value.analyzeRootPath);
31
- if (!consumeCliAppError(outcome)) return;
32
- if (opts.json) logger.out(formatArrangeAnalyzeJsonOutput(resolvedTarget, outcome.value));
33
- else printAnalyzeReport(resolvedTarget, outcome.value);
34
- });
35
- const previewOrApply = (write) => async (target, opts) => {
21
+ const cmd = new Command("arrange").description("Regroup Tailwind classes in cn() / tv() calls in render-pipeline order").enablePositionalOptions().argument("[target]", "Directory or file (default: nearest package directory from cwd)").option("--dry-run", "Preview suggested replacements without writing files", false).option("--with-classname, --with-class-name", "Append className as final cn() argument", false).option("--cn-import <spec>", "Override module specifier when adding cn import").option("--json", "Print one JSON object on stdout (suppresses human progress)", false).action(async (target, opts) => {
22
+ const write = !opts.dryRun;
36
23
  const prelude = await prepareArrangeWorkspace(nodeFilesystem, {
37
24
  currentWorkingDirectory: process.cwd(),
38
25
  rawTarget: readOptionalPositionalArg(target)
@@ -57,9 +44,46 @@ function createArrangeCommand() {
57
44
  printSyncResult(value, write);
58
45
  return exitCodeForArrangeSyncResult(value);
59
46
  });
60
- };
61
- cmd.command("preview").description("Dry-run: print suggested replacements without writing files").argument("[target]", "Directory or file (default: nearest package directory from cwd)").option("--with-classname, --with-class-name", "Append className as final cn() argument", false).option("--cn-import <spec>", "Override module specifier when adding cn import").option("--json", "Print one JSON object on stdout (suppresses human progress)", false).action(previewOrApply(false));
62
- cmd.command("apply").description("Apply grouping and cn-in-tv unwrap edits to files").argument("[target]", "Directory or file (default: nearest package directory from cwd)").option("--with-classname, --with-class-name", "Append className as final cn() argument", false).option("--cn-import <spec>", "Override module specifier when adding cn import").option("--json", "Print one JSON object on stdout (suppresses human progress)", false).action(previewOrApply(true));
47
+ });
48
+ cmd.command("inspect").description("Report long strings, nested cn in tv(), and related findings (read-only)").argument("[target]", "Directory or file (default: nearest package directory from cwd)").option("--json", "Print one JSON object on stdout instead of a human report", false).action(async (target, opts) => {
49
+ const prelude = await prepareArrangeWorkspace(nodeFilesystem, {
50
+ currentWorkingDirectory: process.cwd(),
51
+ rawTarget: readOptionalPositionalArg(target)
52
+ });
53
+ if (!consumeCliAppError(prelude)) return;
54
+ const { resolvedTarget } = prelude.value;
55
+ const parsed = parseWithSchema(arrangeAnalyzeDirectoryRequestSchema, { analyzeRootPath: resolvedTarget });
56
+ if (!consumeCliAppError(parsed)) return;
57
+ const outcome = analyzeDirectory(nodeFilesystem, parsed.value.analyzeRootPath);
58
+ if (!consumeCliAppError(outcome)) return;
59
+ if (opts.json) logger.out(formatArrangeAnalyzeJsonOutput(resolvedTarget, outcome.value));
60
+ else printAnalyzeReport(resolvedTarget, outcome.value);
61
+ });
62
+ cmd.command("simplify").description("Flatten grouped arrays and static-only cn() calls back to plain strings in tv() slots").argument("[target]", "Directory or file (default: nearest package directory from cwd)").option("--dry-run", "Show what simplify would change without writing files", false).option("--json", "Print one JSON object on stdout (suppresses human progress)", false).action(async (target, opts) => {
63
+ const write = !opts.dryRun;
64
+ const prelude = await prepareArrangeWorkspace(nodeFilesystem, {
65
+ currentWorkingDirectory: process.cwd(),
66
+ rawTarget: readOptionalPositionalArg(target)
67
+ });
68
+ if (!consumeCliAppError(prelude)) return;
69
+ const { resolvedTarget } = prelude.value;
70
+ await runCliResultAsync(runArrangeSimplify(nodeFilesystem, {
71
+ targetPath: resolvedTarget,
72
+ write
73
+ }), (value) => {
74
+ if (opts.json) {
75
+ logger.out(JSON.stringify({
76
+ schemaVersion: 1,
77
+ ok: true,
78
+ write,
79
+ result: value
80
+ }));
81
+ return 0;
82
+ }
83
+ printSimplifyResult(value, write);
84
+ return 0;
85
+ });
86
+ });
63
87
  cmd.command("group").description("Try grouping on a pasted class string (stdout: cn(...) or tv array with --tv)").argument("<tokens...>", "Class tokens (quote a single string if it contains spaces)").option("--tv", "Emit tv()-style array instead of cn() call", false).option("--with-classname, --with-class-name", "Append className as final cn() argument", false).option("--json", "Print one JSON object on stdout instead of plain lines", false).action(async (classTokenSeries, opts) => {
64
88
  const parsed = parseWithSchema(arrangeSuggestGroupsRequestSchema, {
65
89
  inlineClasses: classTokenSeries.join(" ").trim(),
@@ -1,9 +1,9 @@
1
- import "./constants.mjs";
2
- import { tokenizeClassString } from "./tailwind-token.mjs";
3
1
  import { forEachDomainChild, isDomainCallExpression, isDomainJsxAttribute, isDomainObjectLiteralExpression } from "./ast/ast-node.mjs";
2
+ import "./constants.mjs";
4
3
  import { forEachStringLiteralInClassExpression } from "./ast/collectors-cn.mjs";
5
4
  import { jsxClassNameStaticLiteral } from "./ast/collectors-jsx.mjs";
6
5
  import { buildKnownCnTvBindings, isCnOrTvIdentifier, lineOf } from "./ast/helpers.mjs";
6
+ import { tokenizeClassString } from "./tailwind-token.mjs";
7
7
  import { collectCnCallsInsideTv, traverseTvObject } from "./ast/collectors-tv.mjs";
8
8
  //#region src/arrange/domain/analyze-service.ts
9
9
  /**
@@ -1,5 +1,5 @@
1
- import "../constants.mjs";
2
1
  import { isDomainArrayLiteralExpression, isDomainTailwindClassLiteral } from "./ast-node.mjs";
2
+ import "../constants.mjs";
3
3
  //#region src/arrange/domain/ast/collectors-cn.ts
4
4
  /**
5
5
  * @since 0.3.16-canary.0