aeon-ui-engine 1.3.5 → 1.4.0

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 (84) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +1 -1
  3. package/packages/cli/README.md +13 -1
  4. package/packages/cli/dist/bin.js +23 -5
  5. package/packages/cli/dist/chunk-5L47SLNV.js +247 -0
  6. package/packages/cli/dist/index.d.ts +17 -1
  7. package/packages/cli/dist/index.js +5 -3
  8. package/packages/cli/src/bin.ts +21 -3
  9. package/packages/cli/src/index.ts +1 -0
  10. package/packages/cli/src/migrate.ts +170 -0
  11. package/packages/core/dist/index.d.ts +35 -7
  12. package/packages/core/dist/index.js +102 -2
  13. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  14. package/packages/core/src/anatomy.ts +39 -3
  15. package/packages/core/src/identicon.test.ts +39 -0
  16. package/packages/core/src/identicon.ts +79 -0
  17. package/packages/core/src/index.ts +1 -0
  18. package/packages/panda/panda.config.ts +4 -0
  19. package/packages/panda/src/recipes/composer.ts +58 -4
  20. package/packages/panda/src/recipes/file-upload.ts +100 -0
  21. package/packages/panda/src/recipes/index.ts +2 -0
  22. package/packages/panda/src/recipes/range-slider.ts +59 -0
  23. package/packages/panda/src/recipes/thread.ts +77 -2
  24. package/packages/panda/styled-system/recipes/aeon-composer.d.ts +1 -1
  25. package/packages/panda/styled-system/recipes/aeon-composer.mjs +12 -0
  26. package/packages/panda/styled-system/recipes/aeon-file-upload.d.ts +31 -0
  27. package/packages/panda/styled-system/recipes/aeon-file-upload.mjs +61 -0
  28. package/packages/panda/styled-system/recipes/aeon-range-slider.d.ts +31 -0
  29. package/packages/panda/styled-system/recipes/aeon-range-slider.mjs +49 -0
  30. package/packages/panda/styled-system/recipes/aeon-thread.d.ts +1 -1
  31. package/packages/panda/styled-system/recipes/aeon-thread.mjs +20 -0
  32. package/packages/panda/styled-system/recipes/index.d.ts +2 -0
  33. package/packages/panda/styled-system/recipes/index.mjs +2 -0
  34. package/packages/panda/styled-system/styles.css +292 -0
  35. package/packages/primitives/dist/chunk-CV4UXXZF.js +44 -0
  36. package/packages/primitives/dist/chunk-IZKQSCYU.js +164 -0
  37. package/packages/primitives/dist/index.d.ts +2 -0
  38. package/packages/primitives/dist/index.js +22 -6
  39. package/packages/primitives/dist/machines/file-upload.d.ts +39 -0
  40. package/packages/primitives/dist/machines/file-upload.js +8 -0
  41. package/packages/primitives/dist/machines/range-slider.d.ts +129 -0
  42. package/packages/primitives/dist/machines/range-slider.js +12 -0
  43. package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  44. package/packages/primitives/src/__tests__/file-upload.test.ts +25 -0
  45. package/packages/primitives/src/__tests__/range-slider.test.ts +39 -0
  46. package/packages/primitives/src/index.ts +2 -0
  47. package/packages/primitives/src/machines/file-upload.ts +55 -0
  48. package/packages/primitives/src/machines/range-slider.ts +209 -0
  49. package/packages/react/dist/index.d.ts +117 -25
  50. package/packages/react/dist/index.js +1731 -1006
  51. package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  52. package/packages/react/src/button/index.tsx +50 -15
  53. package/packages/react/src/composer/index.tsx +58 -3
  54. package/packages/react/src/file-upload/index.tsx +322 -0
  55. package/packages/react/src/index.ts +3 -0
  56. package/packages/react/src/prompt/index.tsx +45 -0
  57. package/packages/react/src/range-slider/index.tsx +322 -0
  58. package/packages/react/src/thread/index.tsx +74 -3
  59. package/packages/react/src/utils/slot.tsx +47 -0
  60. package/packages/schemas/dist/index.d.ts +4 -0
  61. package/packages/schemas/dist/index.js +67 -1
  62. package/packages/schemas/json/button.json +6 -0
  63. package/packages/schemas/json/catalog.json +333 -0
  64. package/packages/schemas/json/fileUpload.json +168 -0
  65. package/packages/schemas/json/rangeSlider.json +157 -0
  66. package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  67. package/packages/schemas/registry/catalog.json +18 -0
  68. package/packages/schemas/registry/components/button.json +6 -0
  69. package/packages/schemas/registry/components/fileUpload.json +168 -0
  70. package/packages/schemas/registry/components/rangeSlider.json +157 -0
  71. package/packages/schemas/registry/machines/fileUpload.json +182 -0
  72. package/packages/schemas/registry/machines/rangeSlider.json +201 -0
  73. package/packages/schemas/scripts/emit.ts +4 -0
  74. package/packages/schemas/src/components.ts +70 -0
  75. package/packages/schemas/tsconfig.json +1 -1
  76. package/packages/ui/dist/index.d.ts +100 -46
  77. package/packages/ui/dist/index.js +463 -341
  78. package/packages/ui/src/composer.tsx +37 -11
  79. package/packages/ui/src/file-upload.tsx +36 -0
  80. package/packages/ui/src/index.ts +2 -0
  81. package/packages/ui/src/prompt.tsx +3 -0
  82. package/packages/ui/src/range-slider.tsx +24 -0
  83. package/packages/ui/src/thread.tsx +40 -0
  84. package/packages/cli/dist/chunk-IW5NT2Z2.js +0 -103
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0] - 2026-08-17
4
+
5
+ ### Added
6
+
7
+ - **RangeSlider** — `rangeSliderMachine` + headless/styled compound (two thumbs, `minStepsBetween`).
8
+ - **FileUpload** — `fileUploadMachine` + dropzone compound (`idle | dragging | rejected`; file bytes in React).
9
+ - **Button `asChild`** + exported `Slot` — merge button attrs onto `Link` / `<a>`.
10
+ - **`npx aeon-ui migrate`** — scan known workarounds + write `AEON_MIGRATION_CHECKLIST.md`.
11
+ - **docs/MIGRATION.md** — greenfield + upgrade playbook for future app migrations.
12
+
13
+ ### Changed
14
+
15
+ - COMPONENTS / MACHINES / PRIMITIVE_COVERAGE / API_PATTERNS updated for 1.4 primitives.
16
+
17
+ ## [1.3.6] - 2026-07-31
18
+
19
+ ### Changed
20
+
21
+ - **Landing hero** — “A better way to build with AI” (title, OG, tagline).
22
+ - **Builder statecharts** — live chart follows the active UX scope/machine and highlights more reliably as you interact.
23
+
24
+ ### Earlier in 1.3.6
25
+
26
+ - **Landing why-first** — philosophy, pillars, FAQ, and charts/totality support that outcome instead of leading with mechanics.
27
+
3
28
  ## [1.3.5] - 2026-07-31
4
29
 
5
30
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aeon-ui-engine",
3
- "version": "1.3.5",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,14 +4,26 @@ AI-optimized CLI for Aeon UI.
4
4
 
5
5
  ```bash
6
6
  npx aeon-ui init
7
+ npx aeon-ui migrate
7
8
  ```
8
9
 
10
+ ## `init`
11
+
9
12
  Injects:
10
13
 
11
14
  - `.cursor/rules/aeon-ui.mdc` — always-on Cursor rule
12
15
  - `aeon.cursorrules.md` / `aeon.system-prompt.md` — paste into any agent
13
16
  - `aeon.schemas/` — deterministic JSON schemas per component
14
- - `src/components/AeonStarter.tsx` — minimal Field + Button + Async scaffold
17
+ - `src/components/AeonStarter.tsx` — minimal Field + Button scaffold
15
18
  - `AEON_AI.md` — next steps
16
19
 
17
20
  Use `--force` to overwrite.
21
+
22
+ ## `migrate`
23
+
24
+ Scans for known local workarounds (preview-window machine, file dropzone, sonner bridge, etc.) and writes `AEON_MIGRATION_CHECKLIST.md`.
25
+
26
+ ```bash
27
+ npx aeon-ui migrate # write checklist
28
+ npx aeon-ui migrate --check # print findings only
29
+ ```
@@ -1,24 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- initAeon
4
- } from "./chunk-IW5NT2Z2.js";
3
+ initAeon,
4
+ migrateAeon
5
+ } from "./chunk-5L47SLNV.js";
5
6
 
6
7
  // src/bin.ts
7
8
  var args = process.argv.slice(2);
8
9
  var cmd = args[0] ?? "help";
9
10
  var force = args.includes("--force") || args.includes("-f");
11
+ var checkOnly = args.includes("--check");
10
12
  function help() {
11
13
  console.log(`aeon-ui \u2014 AI-optimized CLI for Aeon UI
12
14
 
13
15
  Usage:
14
16
  npx aeon-ui init [--force]
17
+ npx aeon-ui migrate [--check]
15
18
 
16
19
  Commands:
17
- init Inject Cursor rules, JSON schemas, system prompt, and a starter component
18
- help Show this message
20
+ init Inject Cursor rules, JSON schemas, system prompt, and a starter component
21
+ migrate Scan for known workarounds and write AEON_MIGRATION_CHECKLIST.md
22
+ help Show this message
19
23
 
20
24
  Options:
21
- --force, -f Overwrite existing generated files
25
+ --force, -f Overwrite existing generated files (init)
26
+ --check Report findings only; do not write checklist (migrate)
22
27
  `);
23
28
  }
24
29
  if (cmd === "init") {
@@ -27,6 +32,19 @@ if (cmd === "init") {
27
32
  for (const line of result.written) console.log(` ${line}`);
28
33
  console.log("\nNext: npm install aeon-ui-engine react react-dom xstate @xstate/react");
29
34
  console.log("Docs: AEON_AI.md \xB7 paste aeon.cursorrules.md into your agent system prompt");
35
+ } else if (cmd === "migrate") {
36
+ const result = migrateAeon({ write: !checkOnly });
37
+ console.log(`Aeon UI migrate \u2192 ${result.cwd}`);
38
+ for (const line of result.written) console.log(` ${line}`);
39
+ if (result.findings.length === 0) {
40
+ console.log(" (no known workarounds matched in quick scan)");
41
+ } else {
42
+ for (const f of result.findings) {
43
+ console.log(` [${f.severity}] ${f.id}: ${f.advice}`);
44
+ }
45
+ }
46
+ console.log("\nInstall: npm install aeon-ui-engine@^1.4.0 xstate @xstate/react");
47
+ console.log("Docs: https://aeon-ui.com \xB7 repo docs/MIGRATION.md");
30
48
  } else {
31
49
  help();
32
50
  if (cmd !== "help" && cmd !== "--help" && cmd !== "-h") {
@@ -0,0 +1,247 @@
1
+ // src/init.ts
2
+ import { mkdirSync, writeFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import {
5
+ CURSOR_RULES_SNIPPET,
6
+ AEON_SYSTEM_PROMPT,
7
+ catalog,
8
+ componentSchemas
9
+ } from "@aeon-ui/schemas";
10
+ var STARTER = `/**
11
+ * Aeon UI starter \u2014 generated by \`npx aeon-ui init\`
12
+ * Headless: @aeon-ui/react \xB7 Styled: @aeon-ui/ui \xB7 Schemas: @aeon-ui/schemas
13
+ */
14
+ import { Button, Field } from '@aeon-ui/ui'
15
+
16
+ export function AeonStarter() {
17
+ return (
18
+ <div>
19
+ <Field.Root>
20
+ <Field.Label>Email</Field.Label>
21
+ <Field.Control type="email" placeholder="you@example.com" />
22
+ <Field.Message />
23
+ </Field.Root>
24
+ <Button.Root status="idle">Continue</Button.Root>
25
+ </div>
26
+ )
27
+ }
28
+ `;
29
+ function initAeon(options = {}) {
30
+ const cwd = options.cwd ?? process.cwd();
31
+ const force = options.force ?? false;
32
+ const written = [];
33
+ const write = (rel, body) => {
34
+ const path = join(cwd, rel);
35
+ if (existsSync(path) && !force) {
36
+ written.push(`skip ${rel} (exists)`);
37
+ return;
38
+ }
39
+ mkdirSync(join(path, ".."), { recursive: true });
40
+ writeFileSync(path, body.endsWith("\n") ? body : `${body}
41
+ `);
42
+ written.push(`write ${rel}`);
43
+ };
44
+ write(".cursor/rules/aeon-ui.mdc", `---
45
+ description: Aeon UI \u2014 statechart-driven components for AI agents
46
+ globs: **/*.{tsx,ts,jsx,js}
47
+ alwaysApply: true
48
+ ---
49
+
50
+ ${CURSOR_RULES_SNIPPET.trim()}
51
+ `);
52
+ write("aeon.cursorrules.md", `${CURSOR_RULES_SNIPPET.trim()}
53
+ `);
54
+ write("aeon.system-prompt.md", `${AEON_SYSTEM_PROMPT.trim()}
55
+ `);
56
+ write(
57
+ "aeon.schemas/catalog.json",
58
+ `${JSON.stringify(
59
+ {
60
+ $id: catalog.$id,
61
+ title: catalog.title,
62
+ description: catalog.description,
63
+ version: catalog.version,
64
+ principle: catalog.principle,
65
+ attributes: catalog.attributes,
66
+ components: catalog.components,
67
+ note: "Full schemas live under aeon.schemas/components/*.json"
68
+ },
69
+ null,
70
+ 2
71
+ )}
72
+ `
73
+ );
74
+ for (const [id, schema] of Object.entries(componentSchemas)) {
75
+ write(`aeon.schemas/components/${id}.json`, `${JSON.stringify(schema, null, 2)}
76
+ `);
77
+ }
78
+ write("src/components/AeonStarter.tsx", STARTER);
79
+ write(
80
+ "AEON_AI.md",
81
+ `# Aeon UI \u2014 AI agent setup
82
+
83
+ Generated by \`npx aeon-ui init\`.
84
+
85
+ ## Next steps
86
+
87
+ 1. Install: \`npm install aeon-ui-engine react react-dom xstate @xstate/react\`
88
+ 2. CSS: \`import 'aeon-ui-engine/aeon.css'\`
89
+ 3. Vite: add \`aeonUiVitePlugin()\` from \`aeon-ui-engine/vite\`
90
+ 4. Paste \`aeon.cursorrules.md\` into Cursor rules / system prompt (already mirrored to \`.cursor/rules/aeon-ui.mdc\`)
91
+ 5. Constrain generation with JSON under \`aeon.schemas/\`
92
+
93
+ ## Principle
94
+
95
+ **UI = f(state)** \u2014 never invent boolean combinations for states the schema does not list.
96
+ `
97
+ );
98
+ return { cwd, written };
99
+ }
100
+
101
+ // src/migrate.ts
102
+ import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
103
+ import { join as join2 } from "path";
104
+ var FINDINGS = [
105
+ {
106
+ id: "preview-window-machine",
107
+ severity: "replace",
108
+ patterns: ["preview-window-machine", "previewWindowMachine"],
109
+ advice: "Replace with RangeSlider from @aeon-ui/react or @aeon-ui/ui (rangeSliderMachine)."
110
+ },
111
+ {
112
+ id: "file-dropzone",
113
+ severity: "replace",
114
+ patterns: ["file-dropzone-machine", "fileDropzoneMachine", "FileDropzone"],
115
+ advice: "Replace with FileUpload compound (fileUploadMachine) \u2014 idle | dragging | rejected."
116
+ },
117
+ {
118
+ id: "sonner-bridge",
119
+ severity: "replace",
120
+ patterns: ["from 'sonner'", 'from "sonner"', "components/ui/sonner"],
121
+ advice: "Use Toast.Provider + useToast().publish / dismiss (aeon-ui-engine \u2265 1.2)."
122
+ },
123
+ {
124
+ id: "manual-link-button",
125
+ severity: "optional",
126
+ patterns: ['data-aeon-scope="button"', "buttonVariants("],
127
+ advice: "Prefer <Button.Root asChild><Link \u2026 /></Button.Root> instead of hand-rolled data-aeon-* on anchors."
128
+ },
129
+ {
130
+ id: "chakra",
131
+ severity: "info",
132
+ patterns: ["@chakra-ui/react", "@ark-ui/react"],
133
+ advice: "Migrate leaf \u2192 forms \u2192 overlays per docs/MIGRATION_FROM_ARK.md; see docs/MIGRATION.md for greenfield checklist."
134
+ },
135
+ {
136
+ id: "alert-dialog-local",
137
+ severity: "optional",
138
+ patterns: ["alertDialogMachine", 'role="alertdialog"'],
139
+ advice: "Prefer Dialog/Prompt with dismissible=false for destructive confirms until AlertDialog ships."
140
+ }
141
+ ];
142
+ var CANDIDATES = [
143
+ "package.json",
144
+ "package-lock.json",
145
+ "pnpm-lock.yaml",
146
+ "lib/preview-window-machine.ts",
147
+ "lib/file-dropzone-machine.ts",
148
+ "components/file-dropzone.tsx",
149
+ "components/ui/sonner.tsx",
150
+ "components/ui/alert-dialog.tsx",
151
+ "components/ui/slider.tsx",
152
+ "components/ui/button.tsx",
153
+ "components/dual-login-buttons.tsx"
154
+ ];
155
+ function walkHints(cwd) {
156
+ const hits = [];
157
+ let corpus = "";
158
+ for (const rel of CANDIDATES) {
159
+ const abs = join2(cwd, rel);
160
+ if (!existsSync2(abs)) continue;
161
+ try {
162
+ corpus += `
163
+ ${rel}
164
+ ${readFileSync(abs, "utf8")}`;
165
+ } catch {
166
+ }
167
+ }
168
+ for (const rule of FINDINGS) {
169
+ const matched = rule.patterns.find((p) => corpus.includes(p));
170
+ if (matched) {
171
+ hits.push({
172
+ id: rule.id,
173
+ severity: rule.severity,
174
+ pattern: matched,
175
+ advice: rule.advice
176
+ });
177
+ }
178
+ }
179
+ return hits;
180
+ }
181
+ var CHECKLIST = `# Aeon migration checklist
182
+
183
+ Generated by \`npx aeon-ui migrate\`.
184
+
185
+ ## Install
186
+
187
+ \`\`\`bash
188
+ npm install aeon-ui-engine@^1.4.0 react react-dom xstate @xstate/react
189
+ \`\`\`
190
+
191
+ \`\`\`ts
192
+ import 'aeon-ui-engine/aeon.css'
193
+ \`\`\`
194
+
195
+ ## Agent setup
196
+
197
+ \`\`\`bash
198
+ npx aeon-ui init
199
+ \`\`\`
200
+
201
+ Paste \`aeon.cursorrules.md\` into Cursor rules. Keep domain machines in the app; use Aeon for chrome + interactive primitives.
202
+
203
+ ## Statechart-first rule
204
+
205
+ 1. Model interaction with an XState machine (or reuse Aeon machines).
206
+ 2. Project \`snapshot\` \u2192 \`data-aeon-state\` / compound parts.
207
+ 3. Do not invent parallel booleans for chart-owned concerns.
208
+
209
+ ## New primitives (1.4+)
210
+
211
+ | Need | Use |
212
+ |------|-----|
213
+ | Two-thumb range | \`RangeSlider\` + \`rangeSliderMachine\` |
214
+ | Link styled as button | \`<Button.Root asChild><Link/></Button.Root>\` |
215
+ | Drag-drop file surface | \`FileUpload\` + \`fileUploadMachine\` |
216
+ | Toast | \`Toast.Provider\` + \`useToast()\` |
217
+ | Confirm sheet | \`Prompt\` / \`Dialog\` |
218
+
219
+ ## Docs
220
+
221
+ - docs/MIGRATION.md (in aeon-ui-engine / AeonUI repo)
222
+ - docs/MIGRATION_FROM_ARK.md
223
+ - AGENTS.md
224
+ `;
225
+ function migrateAeon(options = {}) {
226
+ const cwd = options.cwd ?? process.cwd();
227
+ const write = options.write ?? true;
228
+ const findings = walkHints(cwd);
229
+ const written = [];
230
+ if (write) {
231
+ const path = join2(cwd, "AEON_MIGRATION_CHECKLIST.md");
232
+ const findingsBlock = findings.length === 0 ? "\n## Scan results\n\nNo known local workarounds detected in the quick scan.\n" : `
233
+ ## Scan results
234
+
235
+ ${findings.map((f) => `- **${f.severity}** \`${f.id}\` (matched \`${f.pattern}\`): ${f.advice}`).join("\n")}
236
+ `;
237
+ writeFileSync2(path, `${CHECKLIST.trim()}
238
+ ${findingsBlock}`);
239
+ written.push("write AEON_MIGRATION_CHECKLIST.md");
240
+ }
241
+ return { cwd, written, findings };
242
+ }
243
+
244
+ export {
245
+ initAeon,
246
+ migrateAeon
247
+ };
@@ -7,4 +7,20 @@ declare function initAeon(options?: InitOptions): {
7
7
  written: string[];
8
8
  };
9
9
 
10
- export { type InitOptions, initAeon };
10
+ type MigrateOptions = {
11
+ cwd?: string;
12
+ write?: boolean;
13
+ };
14
+ type Finding = {
15
+ id: string;
16
+ severity: 'replace' | 'optional' | 'info';
17
+ pattern: string;
18
+ advice: string;
19
+ };
20
+ declare function migrateAeon(options?: MigrateOptions): {
21
+ cwd: string;
22
+ written: string[];
23
+ findings: Finding[];
24
+ };
25
+
26
+ export { type InitOptions, type MigrateOptions, initAeon, migrateAeon };
@@ -1,6 +1,8 @@
1
1
  import {
2
- initAeon
3
- } from "./chunk-IW5NT2Z2.js";
2
+ initAeon,
3
+ migrateAeon
4
+ } from "./chunk-5L47SLNV.js";
4
5
  export {
5
- initAeon
6
+ initAeon,
7
+ migrateAeon
6
8
  };
@@ -1,22 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import { initAeon } from './init.js'
3
+ import { migrateAeon } from './migrate.js'
3
4
 
4
5
  const args = process.argv.slice(2)
5
6
  const cmd = args[0] ?? 'help'
6
7
  const force = args.includes('--force') || args.includes('-f')
8
+ const checkOnly = args.includes('--check')
7
9
 
8
10
  function help() {
9
11
  console.log(`aeon-ui — AI-optimized CLI for Aeon UI
10
12
 
11
13
  Usage:
12
14
  npx aeon-ui init [--force]
15
+ npx aeon-ui migrate [--check]
13
16
 
14
17
  Commands:
15
- init Inject Cursor rules, JSON schemas, system prompt, and a starter component
16
- help Show this message
18
+ init Inject Cursor rules, JSON schemas, system prompt, and a starter component
19
+ migrate Scan for known workarounds and write AEON_MIGRATION_CHECKLIST.md
20
+ help Show this message
17
21
 
18
22
  Options:
19
- --force, -f Overwrite existing generated files
23
+ --force, -f Overwrite existing generated files (init)
24
+ --check Report findings only; do not write checklist (migrate)
20
25
  `)
21
26
  }
22
27
 
@@ -26,6 +31,19 @@ if (cmd === 'init') {
26
31
  for (const line of result.written) console.log(` ${line}`)
27
32
  console.log('\nNext: npm install aeon-ui-engine react react-dom xstate @xstate/react')
28
33
  console.log('Docs: AEON_AI.md · paste aeon.cursorrules.md into your agent system prompt')
34
+ } else if (cmd === 'migrate') {
35
+ const result = migrateAeon({ write: !checkOnly })
36
+ console.log(`Aeon UI migrate → ${result.cwd}`)
37
+ for (const line of result.written) console.log(` ${line}`)
38
+ if (result.findings.length === 0) {
39
+ console.log(' (no known workarounds matched in quick scan)')
40
+ } else {
41
+ for (const f of result.findings) {
42
+ console.log(` [${f.severity}] ${f.id}: ${f.advice}`)
43
+ }
44
+ }
45
+ console.log('\nInstall: npm install aeon-ui-engine@^1.4.0 xstate @xstate/react')
46
+ console.log('Docs: https://aeon-ui.com · repo docs/MIGRATION.md')
29
47
  } else {
30
48
  help()
31
49
  if (cmd !== 'help' && cmd !== '--help' && cmd !== '-h') {
@@ -1 +1,2 @@
1
1
  export { initAeon, type InitOptions } from './init.js'
2
+ export { migrateAeon, type MigrateOptions } from './migrate.js'
@@ -0,0 +1,170 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export type MigrateOptions = {
5
+ cwd?: string
6
+ write?: boolean
7
+ }
8
+
9
+ type Finding = {
10
+ id: string
11
+ severity: 'replace' | 'optional' | 'info'
12
+ pattern: string
13
+ advice: string
14
+ }
15
+
16
+ const FINDINGS: Array<{
17
+ id: string
18
+ severity: Finding['severity']
19
+ patterns: string[]
20
+ advice: string
21
+ }> = [
22
+ {
23
+ id: 'preview-window-machine',
24
+ severity: 'replace',
25
+ patterns: ['preview-window-machine', 'previewWindowMachine'],
26
+ advice: 'Replace with RangeSlider from @aeon-ui/react or @aeon-ui/ui (rangeSliderMachine).',
27
+ },
28
+ {
29
+ id: 'file-dropzone',
30
+ severity: 'replace',
31
+ patterns: ['file-dropzone-machine', 'fileDropzoneMachine', 'FileDropzone'],
32
+ advice: 'Replace with FileUpload compound (fileUploadMachine) — idle | dragging | rejected.',
33
+ },
34
+ {
35
+ id: 'sonner-bridge',
36
+ severity: 'replace',
37
+ patterns: ["from 'sonner'", 'from "sonner"', 'components/ui/sonner'],
38
+ advice: 'Use Toast.Provider + useToast().publish / dismiss (aeon-ui-engine ≥ 1.2).',
39
+ },
40
+ {
41
+ id: 'manual-link-button',
42
+ severity: 'optional',
43
+ patterns: ['data-aeon-scope="button"', 'buttonVariants('],
44
+ advice:
45
+ 'Prefer <Button.Root asChild><Link … /></Button.Root> instead of hand-rolled data-aeon-* on anchors.',
46
+ },
47
+ {
48
+ id: 'chakra',
49
+ severity: 'info',
50
+ patterns: ['@chakra-ui/react', '@ark-ui/react'],
51
+ advice:
52
+ 'Migrate leaf → forms → overlays per docs/MIGRATION_FROM_ARK.md; see docs/MIGRATION.md for greenfield checklist.',
53
+ },
54
+ {
55
+ id: 'alert-dialog-local',
56
+ severity: 'optional',
57
+ patterns: ['alertDialogMachine', 'role="alertdialog"'],
58
+ advice:
59
+ 'Prefer Dialog/Prompt with dismissible=false for destructive confirms until AlertDialog ships.',
60
+ },
61
+ ]
62
+
63
+ const CANDIDATES = [
64
+ 'package.json',
65
+ 'package-lock.json',
66
+ 'pnpm-lock.yaml',
67
+ 'lib/preview-window-machine.ts',
68
+ 'lib/file-dropzone-machine.ts',
69
+ 'components/file-dropzone.tsx',
70
+ 'components/ui/sonner.tsx',
71
+ 'components/ui/alert-dialog.tsx',
72
+ 'components/ui/slider.tsx',
73
+ 'components/ui/button.tsx',
74
+ 'components/dual-login-buttons.tsx',
75
+ ]
76
+
77
+ function walkHints(cwd: string): Finding[] {
78
+ const hits: Finding[] = []
79
+ let corpus = ''
80
+
81
+ for (const rel of CANDIDATES) {
82
+ const abs = join(cwd, rel)
83
+ if (!existsSync(abs)) continue
84
+ try {
85
+ corpus += `\n${rel}\n${readFileSync(abs, 'utf8')}`
86
+ } catch {
87
+ /* ignore unreadable */
88
+ }
89
+ }
90
+
91
+ for (const rule of FINDINGS) {
92
+ const matched = rule.patterns.find((p) => corpus.includes(p))
93
+ if (matched) {
94
+ hits.push({
95
+ id: rule.id,
96
+ severity: rule.severity,
97
+ pattern: matched,
98
+ advice: rule.advice,
99
+ })
100
+ }
101
+ }
102
+
103
+ return hits
104
+ }
105
+
106
+ const CHECKLIST = `# Aeon migration checklist
107
+
108
+ Generated by \`npx aeon-ui migrate\`.
109
+
110
+ ## Install
111
+
112
+ \`\`\`bash
113
+ npm install aeon-ui-engine@^1.4.0 react react-dom xstate @xstate/react
114
+ \`\`\`
115
+
116
+ \`\`\`ts
117
+ import 'aeon-ui-engine/aeon.css'
118
+ \`\`\`
119
+
120
+ ## Agent setup
121
+
122
+ \`\`\`bash
123
+ npx aeon-ui init
124
+ \`\`\`
125
+
126
+ Paste \`aeon.cursorrules.md\` into Cursor rules. Keep domain machines in the app; use Aeon for chrome + interactive primitives.
127
+
128
+ ## Statechart-first rule
129
+
130
+ 1. Model interaction with an XState machine (or reuse Aeon machines).
131
+ 2. Project \`snapshot\` → \`data-aeon-state\` / compound parts.
132
+ 3. Do not invent parallel booleans for chart-owned concerns.
133
+
134
+ ## New primitives (1.4+)
135
+
136
+ | Need | Use |
137
+ |------|-----|
138
+ | Two-thumb range | \`RangeSlider\` + \`rangeSliderMachine\` |
139
+ | Link styled as button | \`<Button.Root asChild><Link/></Button.Root>\` |
140
+ | Drag-drop file surface | \`FileUpload\` + \`fileUploadMachine\` |
141
+ | Toast | \`Toast.Provider\` + \`useToast()\` |
142
+ | Confirm sheet | \`Prompt\` / \`Dialog\` |
143
+
144
+ ## Docs
145
+
146
+ - docs/MIGRATION.md (in aeon-ui-engine / AeonUI repo)
147
+ - docs/MIGRATION_FROM_ARK.md
148
+ - AGENTS.md
149
+ `
150
+
151
+ export function migrateAeon(options: MigrateOptions = {}) {
152
+ const cwd = options.cwd ?? process.cwd()
153
+ const write = options.write ?? true
154
+ const findings = walkHints(cwd)
155
+ const written: string[] = []
156
+
157
+ if (write) {
158
+ const path = join(cwd, 'AEON_MIGRATION_CHECKLIST.md')
159
+ const findingsBlock =
160
+ findings.length === 0
161
+ ? '\n## Scan results\n\nNo known local workarounds detected in the quick scan.\n'
162
+ : `\n## Scan results\n\n${findings
163
+ .map((f) => `- **${f.severity}** \`${f.id}\` (matched \`${f.pattern}\`): ${f.advice}`)
164
+ .join('\n')}\n`
165
+ writeFileSync(path, `${CHECKLIST.trim()}\n${findingsBlock}`)
166
+ written.push('write AEON_MIGRATION_CHECKLIST.md')
167
+ }
168
+
169
+ return { cwd, written, findings }
170
+ }