@codefast/cli 0.3.12 → 0.3.13-canary.1

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 (159) hide show
  1. package/README.md +206 -51
  2. package/dist/{bin.js → bin.mjs} +4 -1
  3. package/dist/commands/arrange.mjs +107 -0
  4. package/dist/commands/mirror.mjs +66 -0
  5. package/dist/commands/tag.mjs +58 -0
  6. package/dist/lib/arrange/application/analyze.mjs +92 -0
  7. package/dist/lib/arrange/application/group-file.mjs +113 -0
  8. package/dist/lib/arrange/application/run-target.mjs +65 -0
  9. package/dist/lib/arrange/domain/ast/ast-helpers.mjs +115 -0
  10. package/dist/lib/arrange/domain/ast/collectors-cn.mjs +61 -0
  11. package/dist/lib/arrange/domain/ast/collectors-jsx.mjs +20 -0
  12. package/dist/lib/arrange/domain/ast/collectors-tv.mjs +170 -0
  13. package/dist/lib/arrange/domain/ast/targets.mjs +106 -0
  14. package/dist/lib/arrange/domain/constants.mjs +169 -0
  15. package/dist/lib/arrange/domain/errors.mjs +19 -0
  16. package/dist/lib/arrange/domain/grouping.mjs +235 -0
  17. package/dist/lib/arrange/domain/imports.mjs +54 -0
  18. package/dist/lib/arrange/domain/tokenizer.mjs +216 -0
  19. package/dist/lib/arrange/index.mjs +15 -0
  20. package/dist/lib/arrange/infra/walk.mjs +33 -0
  21. package/dist/lib/arrange/presentation/formatters.mjs +52 -0
  22. package/dist/lib/arrange/presentation/report.mjs +24 -0
  23. package/dist/lib/config/domain/schema.mjs +25 -0
  24. package/dist/lib/config/index.mjs +3 -0
  25. package/dist/lib/config/infra/loader.mjs +78 -0
  26. package/dist/lib/infra/caught-unknown-message.mjs +20 -0
  27. package/dist/lib/infra/config-reporter.mjs +10 -0
  28. package/dist/lib/infra/node-io.mjs +30 -0
  29. package/dist/lib/infra/workspace/repo-root.mjs +22 -0
  30. package/dist/lib/mirror/application/engine.mjs +248 -0
  31. package/dist/lib/mirror/application/sync.mjs +162 -0
  32. package/dist/lib/mirror/domain/constants.mjs +12 -0
  33. package/dist/lib/mirror/domain/errors.mjs +15 -0
  34. package/dist/lib/mirror/domain/types.mjs +1 -0
  35. package/dist/lib/mirror/index.mjs +2 -0
  36. package/dist/lib/mirror/infra/dirent-list.mjs +14 -0
  37. package/dist/lib/mirror/infra/package-filter.mjs +26 -0
  38. package/dist/lib/mirror/infra/path-normalizer.mjs +7 -0
  39. package/dist/lib/mirror/infra/update-pkg.mjs +105 -0
  40. package/dist/lib/mirror/infra/workspace-packages.mjs +159 -0
  41. package/dist/lib/mirror/presentation/reporter.mjs +111 -0
  42. package/dist/lib/tag/application/engine.mjs +287 -0
  43. package/dist/lib/tag/domain/types.mjs +1 -0
  44. package/dist/lib/tag/index.mjs +4 -0
  45. package/dist/lib/tag/infra/target-resolver.mjs +170 -0
  46. package/dist/lib/tag/presentation/colors.mjs +12 -0
  47. package/dist/lib/tag/presentation/tag-presenter.mjs +63 -0
  48. package/dist/program.mjs +34 -0
  49. package/package.json +11 -11
  50. package/dist/bin.d.ts +0 -3
  51. package/dist/bin.d.ts.map +0 -1
  52. package/dist/commands/arrange.d.ts +0 -3
  53. package/dist/commands/arrange.d.ts.map +0 -1
  54. package/dist/commands/arrange.js +0 -114
  55. package/dist/commands/mirror.d.ts +0 -4
  56. package/dist/commands/mirror.d.ts.map +0 -1
  57. package/dist/commands/mirror.js +0 -63
  58. package/dist/lib/arrange/analyze.d.ts +0 -4
  59. package/dist/lib/arrange/analyze.d.ts.map +0 -1
  60. package/dist/lib/arrange/analyze.js +0 -105
  61. package/dist/lib/arrange/ast/collectors-cn.d.ts +0 -10
  62. package/dist/lib/arrange/ast/collectors-cn.d.ts.map +0 -1
  63. package/dist/lib/arrange/ast/collectors-cn.js +0 -84
  64. package/dist/lib/arrange/ast/collectors-jsx.d.ts +0 -4
  65. package/dist/lib/arrange/ast/collectors-jsx.d.ts.map +0 -1
  66. package/dist/lib/arrange/ast/collectors-jsx.js +0 -18
  67. package/dist/lib/arrange/ast/collectors-tv.d.ts +0 -13
  68. package/dist/lib/arrange/ast/collectors-tv.d.ts.map +0 -1
  69. package/dist/lib/arrange/ast/collectors-tv.js +0 -273
  70. package/dist/lib/arrange/ast/targets.d.ts +0 -8
  71. package/dist/lib/arrange/ast/targets.d.ts.map +0 -1
  72. package/dist/lib/arrange/ast/targets.js +0 -143
  73. package/dist/lib/arrange/ast/utils.d.ts +0 -36
  74. package/dist/lib/arrange/ast/utils.d.ts.map +0 -1
  75. package/dist/lib/arrange/ast/utils.js +0 -138
  76. package/dist/lib/arrange/constants.d.ts +0 -68
  77. package/dist/lib/arrange/constants.d.ts.map +0 -1
  78. package/dist/lib/arrange/constants.js +0 -179
  79. package/dist/lib/arrange/errors.d.ts +0 -14
  80. package/dist/lib/arrange/errors.d.ts.map +0 -1
  81. package/dist/lib/arrange/errors.js +0 -16
  82. package/dist/lib/arrange/formatters.d.ts +0 -16
  83. package/dist/lib/arrange/formatters.d.ts.map +0 -1
  84. package/dist/lib/arrange/formatters.js +0 -57
  85. package/dist/lib/arrange/group-file.d.ts +0 -4
  86. package/dist/lib/arrange/group-file.d.ts.map +0 -1
  87. package/dist/lib/arrange/group-file.js +0 -115
  88. package/dist/lib/arrange/grouping.d.ts +0 -41
  89. package/dist/lib/arrange/grouping.d.ts.map +0 -1
  90. package/dist/lib/arrange/grouping.js +0 -280
  91. package/dist/lib/arrange/imports.d.ts +0 -6
  92. package/dist/lib/arrange/imports.d.ts.map +0 -1
  93. package/dist/lib/arrange/imports.js +0 -74
  94. package/dist/lib/arrange/report.d.ts +0 -4
  95. package/dist/lib/arrange/report.d.ts.map +0 -1
  96. package/dist/lib/arrange/report.js +0 -37
  97. package/dist/lib/arrange/run-target.d.ts +0 -4
  98. package/dist/lib/arrange/run-target.d.ts.map +0 -1
  99. package/dist/lib/arrange/run-target.js +0 -28
  100. package/dist/lib/arrange/tokenizer.d.ts +0 -37
  101. package/dist/lib/arrange/tokenizer.d.ts.map +0 -1
  102. package/dist/lib/arrange/tokenizer.js +0 -390
  103. package/dist/lib/arrange/types.d.ts +0 -104
  104. package/dist/lib/arrange/types.d.ts.map +0 -1
  105. package/dist/lib/arrange/types.js +0 -7
  106. package/dist/lib/arrange/walk.d.ts +0 -4
  107. package/dist/lib/arrange/walk.d.ts.map +0 -1
  108. package/dist/lib/arrange/walk.js +0 -34
  109. package/dist/lib/arrange.d.ts +0 -41
  110. package/dist/lib/arrange.d.ts.map +0 -1
  111. package/dist/lib/arrange.js +0 -38
  112. package/dist/lib/infra/fs-contract.d.ts +0 -28
  113. package/dist/lib/infra/fs-contract.d.ts.map +0 -1
  114. package/dist/lib/infra/node-io.d.ts +0 -4
  115. package/dist/lib/infra/node-io.d.ts.map +0 -1
  116. package/dist/lib/infra/node-io.js +0 -27
  117. package/dist/lib/mirror/config.d.ts +0 -7
  118. package/dist/lib/mirror/config.d.ts.map +0 -1
  119. package/dist/lib/mirror/config.js +0 -146
  120. package/dist/lib/mirror/constants.d.ts +0 -10
  121. package/dist/lib/mirror/constants.d.ts.map +0 -1
  122. package/dist/lib/mirror/constants.js +0 -13
  123. package/dist/lib/mirror/engine.d.ts +0 -12
  124. package/dist/lib/mirror/engine.d.ts.map +0 -1
  125. package/dist/lib/mirror/engine.js +0 -248
  126. package/dist/lib/mirror/errors.d.ts +0 -10
  127. package/dist/lib/mirror/errors.d.ts.map +0 -1
  128. package/dist/lib/mirror/errors.js +0 -12
  129. package/dist/lib/mirror/package-filter.d.ts +0 -8
  130. package/dist/lib/mirror/package-filter.d.ts.map +0 -1
  131. package/dist/lib/mirror/package-filter.js +0 -33
  132. package/dist/lib/mirror/reporter.d.ts +0 -24
  133. package/dist/lib/mirror/reporter.d.ts.map +0 -1
  134. package/dist/lib/mirror/reporter.js +0 -126
  135. package/dist/lib/mirror/sync.d.ts +0 -4
  136. package/dist/lib/mirror/sync.d.ts.map +0 -1
  137. package/dist/lib/mirror/sync.js +0 -135
  138. package/dist/lib/mirror/types.d.ts +0 -81
  139. package/dist/lib/mirror/types.d.ts.map +0 -1
  140. package/dist/lib/mirror/update-pkg.d.ts +0 -13
  141. package/dist/lib/mirror/update-pkg.d.ts.map +0 -1
  142. package/dist/lib/mirror/update-pkg.js +0 -39
  143. package/dist/lib/mirror/workspace-packages.d.ts +0 -26
  144. package/dist/lib/mirror/workspace-packages.d.ts.map +0 -1
  145. package/dist/lib/mirror/workspace-packages.js +0 -160
  146. package/dist/lib/mirror.d.ts +0 -7
  147. package/dist/lib/mirror.d.ts.map +0 -1
  148. package/dist/lib/mirror.js +0 -5
  149. package/dist/lib/repo-root.d.ts +0 -7
  150. package/dist/lib/repo-root.d.ts.map +0 -1
  151. package/dist/lib/repo-root.js +0 -23
  152. package/dist/lib/shared/utils.d.ts +0 -9
  153. package/dist/lib/shared/utils.d.ts.map +0 -1
  154. package/dist/lib/shared/utils.js +0 -12
  155. package/dist/program.d.ts +0 -4
  156. package/dist/program.d.ts.map +0 -1
  157. package/dist/program.js +0 -38
  158. /package/dist/lib/{infra/fs-contract.js → arrange/domain/types.mjs} +0 -0
  159. /package/dist/lib/{mirror/types.js → infra/fs-contract.mjs} +0 -0
package/README.md CHANGED
@@ -1,100 +1,255 @@
1
1
  # @codefast/cli
2
2
 
3
- Two tools bundled in one CLI:
3
+ A focused CLI for two recurring maintenance tasks in monorepos:
4
4
 
5
- | Command | Purpose |
6
- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
- | `mirror sync` | Regenerate `package.json` `exports` from each package's built `dist/` tree (pnpm workspace packages) |
8
- | `arrange` | Analyze, dry-run, or apply suggested grouping for long Tailwind class strings inside `cn()` / `tv()` call sites (Tailwind v4–oriented heuristics), or `group` a pasted class string |
5
+ - **`arrange`** — analyze and regroup Tailwind class strings inside `cn()` / `tv()` calls according to a consistent render-pipeline order.
6
+ - **`mirror`** — regenerate `package.json` `exports` fields from built `dist/` trees across a pnpm workspace.
7
+ - **`tag`** (alias: **`annotate`**) — auto-add `@since <version>` to exported TypeScript declarations that are still missing version metadata.
8
+
9
+ ```mermaid
10
+ flowchart LR
11
+ R[codefast]
12
+ R --> A[arrange]
13
+ R --> M[mirror]
14
+ R --> T[tag]
15
+
16
+ A --> A0[analyze]
17
+ A --> A1[preview]
18
+ A --> A2[apply]
19
+ A --> A3[group]
20
+
21
+ M --> M0[sync]
22
+ ```
9
23
 
10
24
  ---
11
25
 
12
- ## Requirements & Install
26
+ ## Requirements
27
+
28
+ - Node.js `>=22.0.0`
29
+ - pnpm (recommended)
30
+
31
+ ---
13
32
 
14
- **Node.js ≥ 24** is required.
33
+ ## Installation
15
34
 
16
35
  ```bash
17
- # Global
36
+ # Install globally
18
37
  pnpm add -g @codefast/cli
19
38
 
20
- # Without a global install
21
- pnpm dlx @codefast/cli -- --help
39
+ # Or run without installing
40
+ pnpm dlx @codefast/cli --help
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Quick start
46
+
47
+ ```bash
48
+ # 1. Preview proposed changes — no files written
49
+ codefast arrange preview packages/ui/src/components
50
+
51
+ # 2. Apply after reviewing the diff
52
+ codefast arrange apply packages/ui/src/components
53
+
54
+ # 3. Regenerate package exports from built dist/
55
+ codefast mirror sync
56
+
57
+ # 4. Add @since tags to exported APIs under src/
58
+ codefast tag
22
59
  ```
23
60
 
24
61
  ---
25
62
 
26
63
  ## `arrange`
27
64
 
28
- ### Recommended workflow
65
+ Reads `cn()` and `tv()` call sites, classifies each Tailwind utility, and rewrites the class strings in render-pipeline order (see [Grouping philosophy](#grouping-philosophy--render-pipeline-order) below).
66
+
67
+ ### Workflow
29
68
 
30
- 1. **`analyze [target]`** — Prints a report (long strings, nested `cn` in `tv`, related notes). No files changed.
31
- 2. **`preview [target]`** — Same transforms as `apply`, but writes nothing. Inspect stdout before touching the tree.
32
- 3. **`apply [target]`** — Writes edits. Run `preview` first.
69
+ Run the three subcommands in order:
33
70
 
34
- **Default target** (when path is omitted): `packages/ui/src/components` resolved from `process.cwd()`.
71
+ | Step | Command | Effect |
72
+ | ---- | ----------------------------------- | ------------------------------------- |
73
+ | 1 | `codefast arrange analyze [target]` | Report only — no files changed |
74
+ | 2 | `codefast arrange preview [target]` | Show exactly what `apply` would write |
75
+ | 3 | `codefast arrange apply [target]` | Write the changes |
35
76
 
36
- ### Useful options
77
+ The default `target` when omitted is `packages/ui/src/components`, resolved from `process.cwd()`.
37
78
 
38
- - **`--with-class-name`** — Append `className` as the last argument to the suggested `cn(...)`.
39
- - **`--cn-import <spec>`** — Override the module specifier when the tool adds a `cn` import.
79
+ ### Flags
40
80
 
41
- ### `group [tokens...]`
81
+ | Flag | Description |
82
+ | -------------------- | ----------------------------------------------------------------------- |
83
+ | `--with-class-name` | Append `className` as the last argument when rewriting a `cn(...)` call |
84
+ | `--cn-import <spec>` | Override the module specifier used when adding a missing `cn` import |
42
85
 
43
- No filesystem involved — paste a class string, get back a suggested `cn(...)` (or a `tv()`-style array with `--tv`) plus a short buckets summary. Use this to tune your mental model before running `analyze` on a large tree.
86
+ ### `arrange group` — one-shot string grouping
87
+
88
+ Groups a single class string without touching the filesystem. Useful for checking how a string would be classified before running `apply`:
89
+
90
+ ```bash
91
+ codefast arrange group "relative flex items-center h-10 w-full rounded-md bg-primary text-white hover:bg-primary/90"
92
+ ```
44
93
 
45
94
  ---
46
95
 
47
- ## Grouping philosophy — Render Pipeline Order
96
+ ## `mirror sync`
48
97
 
49
- `arrange` does **not** sort alphabetically. It groups utilities in roughly the same order the browser reasons about them:
98
+ Scans built `dist/` trees and regenerates the `exports` field in each `package.json`. Run from anywhere inside the monorepo — the workspace root is discovered automatically via `pnpm-workspace.yaml`.
50
99
 
51
- **Existence → Position → Layout → Sizing → Spacing → Shape → Background → Shadow → Typography → Composite → Motion → Starting → Behavior → Conditions (State)**
100
+ ```bash
101
+ codefast mirror sync # all packages in the workspace
102
+ codefast mirror sync packages/ui # a single package path
103
+ codefast mirror sync -v # verbose output
104
+ ```
52
105
 
53
- Bucket breakdown:
106
+ > **Note:** Packages must be built first so `dist/` exists. Run your build step before `mirror sync`.
107
+
108
+ ### Configuration
109
+
110
+ Create a `codefast.config.js` (or `.mjs`, `.cjs`, `.json`) at the repo root with a `mirror` key:
111
+
112
+ ```js
113
+ // codefast.config.mjs
114
+ export default {
115
+ mirror: {
116
+ skipPackages: ["@acme/internal"],
117
+ pathTransformations: {
118
+ "@acme/ui": {
119
+ removePrefix: "./components/",
120
+ },
121
+ },
122
+ customExports: {
123
+ "@acme/ui": {
124
+ "./css/*": "./src/styles/*",
125
+ },
126
+ },
127
+ cssExports: {
128
+ "@acme/ui": {
129
+ enabled: true,
130
+ customExports: {
131
+ "./tokens.css": "./dist/tokens.css",
132
+ },
133
+ },
134
+ },
135
+ },
136
+ };
137
+ ```
54
138
 
55
- | Bucket | What it covers | Examples |
56
- | -------------- | ------------------------------------------------- | ------------------------------------------------- |
57
- | **Existence** | Display / containment context | `hidden`, `block`, `@container`, `group`, `peer` |
58
- | **Position** | Where the box sits | `absolute`, `inset-*`, `top-*`, `z-*` |
59
- | **Layout** | How children flow | `flex`, `grid`, `gap-*`, `items-*` |
60
- | **Sizing** | Box dimensions and overflow | `w-*`, `h-*`, `aspect-*`, `overflow-*` |
61
- | **Spacing** | Padding and margin only (gaps stay with Layout) | `p-*`, `m-*` |
62
- | **Shape** | Corners and strokes | `rounded-*`, `border-*`, `ring-*` |
63
- | **Background** | Surfaces and masks | `bg-*`, `from-*`, `via-*`, `to-*`, `mask-*` |
64
- | **Shadow** | Depth | `shadow-*`, `inset-shadow-*`, `text-shadow-*` |
65
- | **Typography** | Text appearance | `font-*`, `text-*`, `leading-*` |
66
- | **Composite** | Layers and transforms — 3D context → 3D → 2D | `opacity-*`, `rotate-x-*`, `translate-*` |
67
- | **Motion** | Time-based change | `transition-*`, `animate-*` |
68
- | **Starting** | Tailwind's `starting:` layer, kept next to Motion | `starting:*` |
69
- | **Behavior** | Input / scrolling / chrome | `cursor-*`, `scroll-*`, `field-sizing-*`, `inert` |
70
- | **State** | Everything with a variant stack | `hover:`, `md:`, `@md/sidebar:`, `data-[…]:` |
139
+ Use your real package names from `package.json#name` (for example `@acme/ui`) and adjust entries to match your workspace.
71
140
 
72
- Some adjacent buckets may be merged into one string literal when declared _compatible_ (e.g. `layout` + `sizing`) — keeps `cn()` readable without flattening unrelated concerns.
141
+ > **Migration:** Path-based keys (for example `packages/ui`) are deprecated for `pathTransformations`, `customExports`, `cssExports`, and `skipPackages`. Migrate to package-name keys.
73
142
 
74
- To change a placement, edit `classifyBareUtility` in `src/lib/arrange/tokenizer.ts` and add a `classifyToken` test in `src/lib/arrange.test.ts`.
143
+ > **Security note:** `.js`, `.mjs`, and `.cjs` config files are loaded via `import()`. Only run `mirror sync` in repositories you trust.
75
144
 
76
145
  ---
77
146
 
78
- ## `mirror sync`
147
+ ## Lifecycle hooks (`codefast.config.mjs`)
148
+
149
+ `codefast` supports lifecycle hooks so teams can plug in their own post-write workflow (formatter, lint-fix, codemods) without hardcoding any formatter inside CLI core.
150
+
151
+ ```javascript
152
+ import { execSync } from "node:child_process";
153
+
154
+ export default {
155
+ tag: {
156
+ onAfterWrite: ({ files }) => {
157
+ console.log(`Formatting ${files.length} files with Oxc...`);
158
+ execSync(`npx oxc format ${files.join(" ")}`, { stdio: "inherit" });
159
+ },
160
+ },
161
+ arrange: {
162
+ onAfterWrite: ({ files }) => {
163
+ execSync(`npx oxc format ${files.join(" ")}`, { stdio: "inherit" });
164
+ },
165
+ },
166
+ };
167
+ ```
168
+
169
+ Hook contract:
170
+
171
+ - `tag.onAfterWrite?.({ files })` runs after `codefast tag` writes files.
172
+ - `arrange.onAfterWrite?.({ files })` runs after `codefast arrange apply` writes files.
173
+ - Hooks support both sync and async functions (`void | Promise<void>`).
174
+ - Hook errors are logged but do not crash the CLI process.
175
+
176
+ ---
177
+
178
+ ## `tag` / `annotate`
79
179
 
80
- Run from anywhere under the monorepo — the CLI finds the root via `pnpm-workspace.yaml`.
180
+ Scans `.ts` / `.tsx` source files and annotates exported declarations with `@since <current-package-version>`. This keeps API evolution visible and reduces documentation drift in long-lived codebases.
81
181
 
82
182
  ```bash
83
- codefast mirror sync # All workspace packages
84
- codefast mirror sync packages/ui # One package only
85
- codefast mirror sync -v # Verbose
183
+ codefast tag # annotate exports in ./src
184
+ codefast tag packages/ui/src # annotate a custom target
185
+ codefast annotate --dry-run # preview only, do not write files
86
186
  ```
87
187
 
88
- **Config:** Place `codefast.config.js` (or `.mjs` / `.cjs` / `.json`) at repo root with a `mirror` object (`skipPackages`, `pathTransformations`, `customExports`, …).
188
+ What it updates:
89
189
 
90
- > ⚠️ `.js`/`.mjs`/`.cjs` config files are loaded via `import()` — only run `mirror sync` in repositories you trust.
190
+ - Adds `/** @since <version> */` when an exported declaration has no JSDoc.
191
+ - Injects `@since <version>` into an existing JSDoc block when missing.
192
+ - Leaves declarations unchanged when `@since` is already present.
193
+
194
+ The `<version>` value is read from the nearest `package.json` found by walking up from the target path.
91
195
 
92
196
  ---
93
197
 
94
- ## Developing inside this monorepo
198
+ ## Grouping philosophy — Render Pipeline Order
199
+
200
+ `arrange` does **not** sort classes alphabetically. Instead, it groups utilities in roughly the order the browser applies them — from the box's existence, through its shape and surface, to interactive behavior. This makes class strings easier to scan and reason about at a glance.
201
+
202
+ **Existence → Position → Layout → Sizing → Spacing → Shape → Background → Shadow → Typography → Composite → Motion → Starting → Behavior → State → Selector**
203
+
204
+ | Bucket | What it covers | Examples |
205
+ | -------------- | --------------------------------------------------- | ------------------------------------------------- |
206
+ | **Existence** | Display and containment context | `hidden`, `block`, `@container`, `group`, `peer` |
207
+ | **Position** | Where the box sits | `absolute`, `inset-*`, `top-*`, `z-*` |
208
+ | **Layout** | How children flow | `flex`, `grid`, `gap-*`, `items-*` |
209
+ | **Sizing** | Box dimensions and overflow | `w-*`, `h-*`, `aspect-*`, `overflow-*` |
210
+ | **Spacing** | Padding and margin only (gaps stay with Layout) | `p-*`, `m-*` |
211
+ | **Shape** | Corners and strokes | `rounded-*`, `border-*`, `ring-*` |
212
+ | **Background** | Surfaces and masks | `bg-*`, `from-*`, `via-*`, `to-*`, `mask-*` |
213
+ | **Shadow** | Depth | `shadow-*`, `inset-shadow-*`, `text-shadow-*` |
214
+ | **Typography** | Text appearance | `font-*`, `text-*`, `leading-*` |
215
+ | **Composite** | Layers and transforms (3D context → 3D → 2D) | `opacity-*`, `rotate-x-*`, `translate-*` |
216
+ | **Motion** | Time-based change | `transition-*`, `animate-*` |
217
+ | **Starting** | Tailwind's `starting:` layer — kept next to Motion | `starting:*` |
218
+ | **Behavior** | Input, scrolling, and browser chrome | `cursor-*`, `scroll-*`, `field-sizing-*`, `inert` |
219
+ | **State** | Interactive and conditional variants (non-selector) | `hover:`, `md:`, `@md/sidebar:`, `data-[…]:` |
220
+ | **Selector** | Selector-driven variants | `[&…]:`, `*:`, `**:`, `has-*`, `group-[…]:` |
221
+
222
+ Adjacent buckets may be merged into one string literal when declared _compatible_ (e.g. `layout` + `sizing`). This keeps `cn()` calls readable without flattening unrelated concerns into a single undifferentiated blob.
223
+
224
+ To change a placement, edit `classifyBareUtility` in `src/lib/arrange/tokenizer.ts` and add a corresponding `classifyToken` test in `src/lib/arrange.test.ts`.
225
+
226
+ ---
227
+
228
+ ## Troubleshooting
229
+
230
+ **`codefast: command not found`**
231
+ Install globally with `pnpm add -g @codefast/cli`, or run via `pnpm dlx @codefast/cli <command>`.
232
+
233
+ **`mirror sync` writes little or no output**
234
+ Packages must be built before syncing. Ensure `dist/` exists by running your build step first, then re-run `codefast mirror sync`.
235
+
236
+ **Unexpected class reorder after `arrange apply`**
237
+ Run `arrange preview` before applying and smoke-test the UI. Some components rely on cascade-sensitive ordering that `arrange` cannot detect automatically.
238
+
239
+ ---
240
+
241
+ ## Contributing (monorepo setup)
95
242
 
96
243
  ```bash
244
+ # Build the local CLI (produces dist/bin.js)
245
+ pnpm --filter @codefast/cli build
246
+
247
+ # Run the local entrypoint
97
248
  pnpm exec codefast --help
98
249
  ```
99
250
 
100
- Root `package.json` defines optional `cli:*` scripts (e.g. `cli:mirror-sync`, `cli:arrange-analyze`) as thin wrappers around `pnpm exec codefast …`.
251
+ A few naming conventions to keep in mind:
252
+
253
+ - **`codefast <command>`** refers to CLI commands exposed via the `@codefast/cli` `bin` entry.
254
+ - **Scripts in `packages/cli/package.json`** (`build`, `test`, …) are package-local dev scripts, not CLI commands.
255
+ - The root `package.json` includes optional convenience wrappers such as `cli:mirror-sync` and `cli:arrange-analyze` for common dev workflows.
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { runCli } from "./program.mjs";
2
3
  import process from "node:process";
3
- import { runCli } from "#program";
4
+ //#region src/bin.ts
4
5
  const code = await runCli(process.argv);
5
6
  process.exit(code);
7
+ //#endregion
8
+ export {};
@@ -0,0 +1,107 @@
1
+ import { messageFromCaughtUnknown } from "../lib/infra/caught-unknown-message.mjs";
2
+ import { loadConfig } from "../lib/config/infra/loader.mjs";
3
+ import "../lib/config/index.mjs";
4
+ import { printConfigSchemaWarnings } from "../lib/infra/config-reporter.mjs";
5
+ import { DEFAULT_ARRANGE_TARGET } from "../lib/arrange/domain/constants.mjs";
6
+ import { analyzeDirectory } from "../lib/arrange/application/analyze.mjs";
7
+ import { printAnalyzeReport } from "../lib/arrange/presentation/report.mjs";
8
+ import { suggestCnGroups, summarizeGroupBucketLabels } from "../lib/arrange/domain/grouping.mjs";
9
+ import { formatArray, formatCnCall } from "../lib/arrange/presentation/formatters.mjs";
10
+ import { runArrangeSync } from "../lib/arrange/application/run-target.mjs";
11
+ import { createNodeCliFs, createNodeCliLogger } from "../lib/infra/node-io.mjs";
12
+ import "../lib/arrange/index.mjs";
13
+ import { findRepoRoot } from "../lib/infra/workspace/repo-root.mjs";
14
+ import process from "node:process";
15
+ import path from "node:path";
16
+ import { Option } from "commander";
17
+ //#region src/commands/arrange.ts
18
+ /** Commander attribute `withClassName` (second long flag `--with-class-name`). */
19
+ function createWithClassNameOption() {
20
+ return new Option("--with-classname, --with-class-name", "Append className as final cn() argument").default(false);
21
+ }
22
+ function defaultTargetPath() {
23
+ return path.resolve(process.cwd(), DEFAULT_ARRANGE_TARGET);
24
+ }
25
+ function checkTargetExists(resolved, fs, logger) {
26
+ if (!fs.existsSync(resolved)) {
27
+ logger.err(`Not found: ${resolved}`);
28
+ process.exitCode = 1;
29
+ return false;
30
+ }
31
+ return true;
32
+ }
33
+ async function loadArrangeCommandConfig(fs, logger, rootDir) {
34
+ try {
35
+ const { config, warnings } = await loadConfig(fs, rootDir);
36
+ printConfigSchemaWarnings(logger, warnings);
37
+ return { arrangeConfig: config.arrange ?? {} };
38
+ } catch (caughtConfigError) {
39
+ logger.err(messageFromCaughtUnknown(caughtConfigError));
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ }
44
+ function registerArrangeCommand(program) {
45
+ const arrange = program.command("arrange").description("Analyze and regroup Tailwind classes in cn() / tv() calls (Tailwind v4)");
46
+ arrange.command("analyze").description("Report long strings, nested cn in tv(), and related findings").argument("[target]", "Directory or file (default: packages/ui/src/components)").action(async (target) => {
47
+ const fs = createNodeCliFs();
48
+ const logger = createNodeCliLogger();
49
+ const resolved = target ? path.resolve(target) : defaultTargetPath();
50
+ if (!checkTargetExists(resolved, fs, logger)) return;
51
+ if (!await loadArrangeCommandConfig(fs, logger, findRepoRoot(fs))) return;
52
+ printAnalyzeReport(resolved, analyzeDirectory(resolved, fs), logger);
53
+ });
54
+ arrange.command("preview").description("Dry-run: print suggested replacements without writing files").argument("[target]", "Directory or file (default: packages/ui/src/components)").addOption(createWithClassNameOption()).option("--cn-import <spec>", "Override module specifier when adding cn import").action(async (target, opts) => {
55
+ const fs = createNodeCliFs();
56
+ const logger = createNodeCliLogger();
57
+ const resolved = target ? path.resolve(target) : defaultTargetPath();
58
+ if (!checkTargetExists(resolved, fs, logger)) return;
59
+ const rootDir = findRepoRoot(fs);
60
+ const loaded = await loadArrangeCommandConfig(fs, logger, rootDir);
61
+ if (!loaded) return;
62
+ process.exitCode = await runArrangeSync({
63
+ rootDir,
64
+ config: loaded.arrangeConfig,
65
+ targetPath: resolved,
66
+ write: false,
67
+ withClassName: opts.withClassName,
68
+ cnImport: opts.cnImport,
69
+ fs,
70
+ logger
71
+ });
72
+ });
73
+ arrange.command("apply").description("Apply grouping and cn-in-tv unwrap edits to files").argument("[target]", "Directory or file (default: packages/ui/src/components)").addOption(createWithClassNameOption()).option("--cn-import <spec>", "Override module specifier when adding cn import").action(async (target, opts) => {
74
+ const fs = createNodeCliFs();
75
+ const logger = createNodeCliLogger();
76
+ const resolved = target ? path.resolve(target) : defaultTargetPath();
77
+ if (!checkTargetExists(resolved, fs, logger)) return;
78
+ const rootDir = findRepoRoot(fs);
79
+ const loaded = await loadArrangeCommandConfig(fs, logger, rootDir);
80
+ if (!loaded) return;
81
+ process.exitCode = await runArrangeSync({
82
+ rootDir,
83
+ config: loaded.arrangeConfig,
84
+ targetPath: resolved,
85
+ write: true,
86
+ withClassName: opts.withClassName,
87
+ cnImport: opts.cnImport,
88
+ fs,
89
+ logger
90
+ });
91
+ });
92
+ arrange.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).addOption(createWithClassNameOption()).action((tokens, opts) => {
93
+ const inlineClasses = tokens.join(" ").trim();
94
+ if (!inlineClasses) {
95
+ process.stderr.write("Pass a class string. Example: codefast arrange group \"flex gap-2 text-sm rounded-md\"\n");
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+ const groups = suggestCnGroups(inlineClasses);
100
+ const result = opts.tv ? formatArray(groups) : formatCnCall(groups, { trailingClassName: !!opts.withClassName });
101
+ process.stdout.write(`${result}\n`);
102
+ const bucketSummary = summarizeGroupBucketLabels(groups);
103
+ process.stdout.write(`\n// Buckets: ${JSON.stringify(bucketSummary)}\n`);
104
+ });
105
+ }
106
+ //#endregion
107
+ export { registerArrangeCommand };
@@ -0,0 +1,66 @@
1
+ import { messageFromCaughtUnknown } from "../lib/infra/caught-unknown-message.mjs";
2
+ import { loadConfig } from "../lib/config/infra/loader.mjs";
3
+ import "../lib/config/index.mjs";
4
+ import { printConfigSchemaWarnings } from "../lib/infra/config-reporter.mjs";
5
+ import { createNodeCliFs, createNodeCliLogger } from "../lib/infra/node-io.mjs";
6
+ import { findRepoRoot } from "../lib/infra/workspace/repo-root.mjs";
7
+ import { runMirrorSync } from "../lib/mirror/application/sync.mjs";
8
+ import "../lib/mirror/index.mjs";
9
+ import process from "node:process";
10
+ import { realpathSync } from "node:fs";
11
+ import path from "node:path";
12
+ //#region src/commands/mirror.ts
13
+ function tryRealpath(entryPath) {
14
+ try {
15
+ return realpathSync.native(entryPath);
16
+ } catch {
17
+ return path.resolve(entryPath);
18
+ }
19
+ }
20
+ function normalizePath(relPath) {
21
+ return relPath.split(path.sep).join("/").replace(/\\/g, "/");
22
+ }
23
+ function packageArgToRelative(rootDir, arg) {
24
+ if (!arg) return;
25
+ const rootReal = tryRealpath(path.resolve(rootDir));
26
+ const cwdReal = tryRealpath(process.cwd());
27
+ const targetReal = tryRealpath(path.isAbsolute(arg) ? path.resolve(arg) : path.resolve(cwdReal, arg));
28
+ const normalized = normalizePath(path.relative(rootReal, targetReal));
29
+ if (normalized.startsWith("..") || path.isAbsolute(normalized) || normalized === "" || normalized === ".") throw new Error(`Package path must be a subdirectory under monorepo root: ${rootDir}`);
30
+ return normalized;
31
+ }
32
+ function registerMirrorCommand(program) {
33
+ program.command("mirror").description("Keep package manifests aligned with what you ship").command("sync").description("Write package.json exports from dist/ for workspace packages").argument("[package]", "Optional package path relative to repo root (e.g. packages/ui)").option("-v, --verbose", "Print extra diagnostics", false).action(async function(pkg, options) {
34
+ const globals = this.optsWithGlobals();
35
+ const fs = createNodeCliFs();
36
+ const logger = createNodeCliLogger();
37
+ const rootDir = findRepoRoot(fs);
38
+ let packageFilter;
39
+ try {
40
+ packageFilter = packageArgToRelative(rootDir, pkg);
41
+ } catch (caughtPathError) {
42
+ this.error(messageFromCaughtUnknown(caughtPathError));
43
+ return;
44
+ }
45
+ let mirrorConfig = {};
46
+ try {
47
+ const { config, warnings } = await loadConfig(fs, rootDir);
48
+ printConfigSchemaWarnings(logger, warnings);
49
+ mirrorConfig = config.mirror ?? {};
50
+ } catch (caughtConfigError) {
51
+ this.error(messageFromCaughtUnknown(caughtConfigError));
52
+ return;
53
+ }
54
+ process.exitCode = await runMirrorSync({
55
+ rootDir,
56
+ config: mirrorConfig,
57
+ verbose: options.verbose,
58
+ noColor: globals.color === false,
59
+ packageFilter,
60
+ fs,
61
+ logger
62
+ });
63
+ });
64
+ }
65
+ //#endregion
66
+ export { packageArgToRelative, registerMirrorCommand };
@@ -0,0 +1,58 @@
1
+ import { messageFromCaughtUnknown } from "../lib/infra/caught-unknown-message.mjs";
2
+ import { loadConfig } from "../lib/config/infra/loader.mjs";
3
+ import "../lib/config/index.mjs";
4
+ import { printConfigSchemaWarnings } from "../lib/infra/config-reporter.mjs";
5
+ import { createNodeCliFs, createNodeCliLogger } from "../lib/infra/node-io.mjs";
6
+ import { findRepoRoot } from "../lib/infra/workspace/repo-root.mjs";
7
+ import { runTagSync } from "../lib/tag/application/engine.mjs";
8
+ import { createTagProgressListener, formatSummary, formatTargetTable, formatWarningsAndErrors } from "../lib/tag/presentation/tag-presenter.mjs";
9
+ import "../lib/tag/index.mjs";
10
+ import process from "node:process";
11
+ import path from "node:path";
12
+ //#region src/commands/tag.ts
13
+ function resolveTagRootDir(fs, logger) {
14
+ try {
15
+ return findRepoRoot(fs);
16
+ } catch (caughtRepoRootError) {
17
+ logger.out(`[tag] workspace root auto-detection failed (${messageFromCaughtUnknown(caughtRepoRootError)}), using cwd=${process.cwd()}`);
18
+ return process.cwd();
19
+ }
20
+ }
21
+ function registerTagCommand(program) {
22
+ program.command("tag").alias("annotate").description("Add @since <version> JSDoc tags to exported declarations").argument("[target]", "Directory or file to annotate (default: auto-discover workspace packages)").option("--dry-run", "Show summary without writing files", false).action(async (target, options) => {
23
+ const fs = createNodeCliFs();
24
+ const logger = createNodeCliLogger();
25
+ const rootDir = resolveTagRootDir(fs, logger);
26
+ let tagConfig = {};
27
+ try {
28
+ const { config, warnings } = await loadConfig(fs, rootDir);
29
+ printConfigSchemaWarnings(logger, warnings);
30
+ tagConfig = config.tag ?? {};
31
+ } catch (caughtConfigError) {
32
+ logger.err(messageFromCaughtUnknown(caughtConfigError));
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ const tagResult = await runTagSync({
37
+ rootDir,
38
+ config: tagConfig,
39
+ skipPackages: tagConfig.skipPackages,
40
+ targetPath: target ? path.resolve(target) : void 0,
41
+ write: !options.dryRun,
42
+ fs,
43
+ listener: createTagProgressListener((line) => logger.out(line))
44
+ });
45
+ logger.out(formatTargetTable(tagResult.selectedTargets, rootDir));
46
+ if (tagResult.selectedTargets.length === 0) {
47
+ logger.err("No packages found in workspace. Check your pnpm-workspace.yaml or provide an explicit target path.");
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ const warningsAndErrorsSection = formatWarningsAndErrors(tagResult);
52
+ if (warningsAndErrorsSection) logger.err(warningsAndErrorsSection);
53
+ logger.out(formatSummary(tagResult));
54
+ process.exitCode = tagResult.targetResults.some((targetResult) => targetResult.runError !== null) || tagResult.hookError ? 1 : 0;
55
+ });
56
+ }
57
+ //#endregion
58
+ export { registerTagCommand };
@@ -0,0 +1,92 @@
1
+ import "../domain/constants.mjs";
2
+ import { tokenizeClassString } from "../domain/tokenizer.mjs";
3
+ import { forEachStringLiteralInClassExpression } from "../domain/ast/collectors-cn.mjs";
4
+ import { jsxClassNameStaticLiteral } from "../domain/ast/collectors-jsx.mjs";
5
+ import { buildKnownCnTvBindings, isCnOrTvIdentifier, lineOf } from "../domain/ast/ast-helpers.mjs";
6
+ import { collectCnCallsInsideTv, traverseTvObject } from "../domain/ast/collectors-tv.mjs";
7
+ import { walkTsxFiles } from "../infra/walk.mjs";
8
+ import ts from "typescript";
9
+ //#region src/lib/arrange/application/analyze.ts
10
+ function analyzeCnCall(sf, call, report) {
11
+ for (const arg of call.arguments) forEachStringLiteralInClassExpression(arg, (lit) => {
12
+ const text = lit.text;
13
+ const tokenCount = tokenizeClassString(text).length;
14
+ if (tokenCount >= 18) report.longCnStringLiterals.push({
15
+ file: sf.fileName,
16
+ line: lineOf(sf, lit),
17
+ tokenCount,
18
+ preview: text.length > 72 ? `${text.slice(0, 72)}…` : text
19
+ });
20
+ });
21
+ }
22
+ function visitCallExpressionForArrangeAnalyze(callExpression, sf, sourceText, knownBindings, report) {
23
+ if (isCnOrTvIdentifier(callExpression.expression, "cn", knownBindings)) {
24
+ report.cnCallExpressions++;
25
+ analyzeCnCall(sf, callExpression, report);
26
+ return;
27
+ }
28
+ if (!isCnOrTvIdentifier(callExpression.expression, "tv", knownBindings)) return;
29
+ report.tvCallExpressions++;
30
+ const arg0 = callExpression.arguments[0];
31
+ if (!arg0 || !ts.isObjectLiteralExpression(arg0)) return;
32
+ for (const nestedCn of collectCnCallsInsideTv(sf, arg0, knownBindings, 0)) {
33
+ const src = sourceText.slice(nestedCn.getStart(sf), nestedCn.getEnd());
34
+ const preview = src.length > 72 ? `${src.slice(0, 72)}…` : src;
35
+ report.cnInsideTvCalls.push({
36
+ file: sf.fileName,
37
+ line: lineOf(sf, nestedCn),
38
+ argCount: nestedCn.arguments.length,
39
+ preview
40
+ });
41
+ }
42
+ traverseTvObject(sf, arg0, (classLiteral) => {
43
+ const text = classLiteral.text;
44
+ const tokenCount = tokenizeClassString(text).length;
45
+ if (tokenCount >= 18) report.longTvStringLiterals.push({
46
+ file: sf.fileName,
47
+ line: lineOf(sf, classLiteral),
48
+ tokenCount,
49
+ preview: text.length > 72 ? `${text.slice(0, 72)}…` : text
50
+ });
51
+ }, 0, knownBindings);
52
+ }
53
+ function visitJsxAttributeForArrangeAnalyze(jsxClassAttribute, sf, filePath, report) {
54
+ if (!filePath.endsWith(".tsx")) return;
55
+ const parsed = jsxClassNameStaticLiteral(jsxClassAttribute);
56
+ if (!parsed) return;
57
+ const text = parsed.lit.text;
58
+ const tokenCount = tokenizeClassString(text).length;
59
+ if (tokenCount >= 18) report.longJsxClassNameLiterals.push({
60
+ file: sf.fileName,
61
+ line: lineOf(sf, parsed.lit),
62
+ tokenCount,
63
+ preview: text.length > 72 ? `${text.slice(0, 72)}…` : text
64
+ });
65
+ }
66
+ function analyzeDirectory(analyzeRootPath, fs) {
67
+ const report = {
68
+ files: 0,
69
+ cnCallExpressions: 0,
70
+ tvCallExpressions: 0,
71
+ cnInsideTvCalls: [],
72
+ longCnStringLiterals: [],
73
+ longTvStringLiterals: [],
74
+ longJsxClassNameLiterals: []
75
+ };
76
+ const files = fs.statSync(analyzeRootPath).isDirectory() ? walkTsxFiles(analyzeRootPath, fs) : [analyzeRootPath];
77
+ for (const filePath of files) {
78
+ const sourceText = fs.readFileSync(filePath, "utf8");
79
+ const sf = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
80
+ report.files++;
81
+ const knownBindings = buildKnownCnTvBindings(sf);
82
+ const visitTypeScriptSubtree = (tsNode) => {
83
+ if (ts.isCallExpression(tsNode)) visitCallExpressionForArrangeAnalyze(tsNode, sf, sourceText, knownBindings, report);
84
+ if (ts.isJsxAttribute(tsNode)) visitJsxAttributeForArrangeAnalyze(tsNode, sf, filePath, report);
85
+ ts.forEachChild(tsNode, visitTypeScriptSubtree);
86
+ };
87
+ visitTypeScriptSubtree(sf);
88
+ }
89
+ return report;
90
+ }
91
+ //#endregion
92
+ export { analyzeDirectory };