aeon-ui-engine 1.3.5 → 1.5.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 (104) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/CONSUMER.md +67 -27
  3. package/INTEGRATION.md +1 -1
  4. package/README.md +17 -8
  5. package/package.json +4 -1
  6. package/packages/cli/README.md +13 -1
  7. package/packages/cli/dist/bin.js +50 -6
  8. package/packages/cli/dist/chunk-JMNRJCWF.js +301 -0
  9. package/packages/cli/dist/index.d.ts +31 -1
  10. package/packages/cli/dist/index.js +7 -3
  11. package/packages/cli/src/bin.ts +50 -4
  12. package/packages/cli/src/index.ts +2 -0
  13. package/packages/cli/src/init.ts +10 -4
  14. package/packages/cli/src/link.ts +61 -0
  15. package/packages/cli/src/migrate.ts +170 -0
  16. package/packages/core/dist/index.d.ts +35 -7
  17. package/packages/core/dist/index.js +102 -2
  18. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  19. package/packages/core/src/anatomy.ts +39 -3
  20. package/packages/core/src/identicon.test.ts +39 -0
  21. package/packages/core/src/identicon.ts +79 -0
  22. package/packages/core/src/index.ts +1 -0
  23. package/packages/panda/panda.config.ts +4 -0
  24. package/packages/panda/src/recipes/composer.ts +58 -4
  25. package/packages/panda/src/recipes/file-upload.ts +100 -0
  26. package/packages/panda/src/recipes/index.ts +2 -0
  27. package/packages/panda/src/recipes/range-slider.ts +59 -0
  28. package/packages/panda/src/recipes/thread.ts +77 -2
  29. package/packages/panda/styled-system/recipes/aeon-composer.d.ts +1 -1
  30. package/packages/panda/styled-system/recipes/aeon-composer.mjs +12 -0
  31. package/packages/panda/styled-system/recipes/aeon-file-upload.d.ts +31 -0
  32. package/packages/panda/styled-system/recipes/aeon-file-upload.mjs +61 -0
  33. package/packages/panda/styled-system/recipes/aeon-range-slider.d.ts +31 -0
  34. package/packages/panda/styled-system/recipes/aeon-range-slider.mjs +49 -0
  35. package/packages/panda/styled-system/recipes/aeon-thread.d.ts +1 -1
  36. package/packages/panda/styled-system/recipes/aeon-thread.mjs +20 -0
  37. package/packages/panda/styled-system/recipes/index.d.ts +2 -0
  38. package/packages/panda/styled-system/recipes/index.mjs +2 -0
  39. package/packages/panda/styled-system/styles.css +292 -0
  40. package/packages/primitives/dist/chunk-CV4UXXZF.js +44 -0
  41. package/packages/primitives/dist/chunk-IZKQSCYU.js +164 -0
  42. package/packages/primitives/dist/{chunk-PB3LFHXU.js → chunk-VUPWYBGU.js} +14 -0
  43. package/packages/primitives/dist/index.d.ts +2 -0
  44. package/packages/primitives/dist/index.js +23 -7
  45. package/packages/primitives/dist/machines/file-upload.d.ts +39 -0
  46. package/packages/primitives/dist/machines/file-upload.js +8 -0
  47. package/packages/primitives/dist/machines/range-slider.d.ts +129 -0
  48. package/packages/primitives/dist/machines/range-slider.js +12 -0
  49. package/packages/primitives/dist/machines/slider.d.ts +22 -0
  50. package/packages/primitives/dist/machines/slider.js +1 -1
  51. package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  52. package/packages/primitives/src/__tests__/file-upload.test.ts +25 -0
  53. package/packages/primitives/src/__tests__/range-slider.test.ts +39 -0
  54. package/packages/primitives/src/index.ts +2 -0
  55. package/packages/primitives/src/machines/file-upload.ts +55 -0
  56. package/packages/primitives/src/machines/range-slider.ts +209 -0
  57. package/packages/primitives/src/machines/slider.ts +17 -0
  58. package/packages/react/dist/index.d.ts +132 -26
  59. package/packages/react/dist/index.js +1822 -1029
  60. package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  61. package/packages/react/src/button/index.tsx +50 -15
  62. package/packages/react/src/composer/index.tsx +58 -3
  63. package/packages/react/src/file-upload/index.tsx +322 -0
  64. package/packages/react/src/floating/FloatingPositioner.tsx +8 -0
  65. package/packages/react/src/hooks/use-anchor-position.ts +88 -17
  66. package/packages/react/src/index.ts +3 -0
  67. package/packages/react/src/menu/index.tsx +37 -3
  68. package/packages/react/src/popover/index.tsx +7 -1
  69. package/packages/react/src/prompt/index.tsx +45 -0
  70. package/packages/react/src/range-slider/index.tsx +322 -0
  71. package/packages/react/src/slider/index.tsx +54 -14
  72. package/packages/react/src/thread/index.tsx +74 -3
  73. package/packages/react/src/tooltip/index.tsx +7 -1
  74. package/packages/react/src/utils/slot.tsx +47 -0
  75. package/packages/schemas/dist/index.d.ts +4 -0
  76. package/packages/schemas/dist/index.js +67 -1
  77. package/packages/schemas/json/button.json +6 -0
  78. package/packages/schemas/json/catalog.json +333 -0
  79. package/packages/schemas/json/fileUpload.json +168 -0
  80. package/packages/schemas/json/rangeSlider.json +157 -0
  81. package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  82. package/packages/schemas/prompts/cursorrules.md +3 -1
  83. package/packages/schemas/prompts/statecharts.md +1 -1
  84. package/packages/schemas/registry/catalog.json +18 -0
  85. package/packages/schemas/registry/components/button.json +6 -0
  86. package/packages/schemas/registry/components/fileUpload.json +168 -0
  87. package/packages/schemas/registry/components/rangeSlider.json +157 -0
  88. package/packages/schemas/registry/machines/fileUpload.json +182 -0
  89. package/packages/schemas/registry/machines/rangeSlider.json +201 -0
  90. package/packages/schemas/scripts/emit.ts +4 -0
  91. package/packages/schemas/src/components.ts +70 -0
  92. package/packages/schemas/src/prompts.ts +3 -1
  93. package/packages/schemas/tsconfig.json +1 -1
  94. package/packages/ui/dist/index.d.ts +100 -46
  95. package/packages/ui/dist/index.js +463 -341
  96. package/packages/ui/src/composer.tsx +37 -11
  97. package/packages/ui/src/file-upload.tsx +36 -0
  98. package/packages/ui/src/index.ts +2 -0
  99. package/packages/ui/src/prompt.tsx +3 -0
  100. package/packages/ui/src/range-slider.tsx +24 -0
  101. package/packages/ui/src/thread.tsx +40 -0
  102. package/scripts/link-scoped-packages.mjs +206 -0
  103. package/scripts/vite-aliases.mjs +6 -8
  104. package/packages/cli/dist/chunk-IW5NT2Z2.js +0 -103
@@ -0,0 +1,301 @@
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
+ - Postinstall links \`@aeon-ui/*\` into \`node_modules\` (re-run with \`npx aeon-ui link\` if needed).
89
+ 2. CSS: \`import 'aeon-ui-engine/aeon.css'\` (e.g. root layout).
90
+ 3. **Next.js:** add \`transpilePackages\` for \`@aeon-ui/ui\`, \`@aeon-ui/react\`, \`@aeon-ui/core\`, \`@aeon-ui/primitives\`, \`@aeon-ui/panda\`. Use \`"use client"\` for interactive components. No webpack aliases required.
91
+ 4. **Vite (optional):** \`aeonUiVitePlugin()\` from \`aeon-ui-engine/vite\` still helps with optimizeDeps; scoped imports resolve via the link step alone.
92
+ 5. Paste \`aeon.cursorrules.md\` into Cursor rules / system prompt (already mirrored to \`.cursor/rules/aeon-ui.mdc\`)
93
+ 6. Constrain generation with JSON under \`aeon.schemas/\`
94
+
95
+ ## Frameworks
96
+
97
+ Aeon is **React-first** (full catalog + styled layer). Next.js is supported. Vue / Solid / Svelte adapters exist but are thin \u2014 not the main product.
98
+
99
+ ## Principle
100
+
101
+ **UI = f(state)** \u2014 never invent boolean combinations for states the schema does not list.
102
+ `
103
+ );
104
+ return { cwd, written };
105
+ }
106
+
107
+ // src/link.ts
108
+ import { spawnSync } from "child_process";
109
+ import { createRequire } from "module";
110
+ import { existsSync as existsSync2 } from "fs";
111
+ import { dirname, join as join2 } from "path";
112
+ import { fileURLToPath } from "url";
113
+ function resolveLinkScript() {
114
+ const here = dirname(fileURLToPath(import.meta.url));
115
+ const fromMonorepo = join2(here, "../../../scripts/link-scoped-packages.mjs");
116
+ if (existsSync2(fromMonorepo)) return fromMonorepo;
117
+ try {
118
+ const require2 = createRequire(import.meta.url);
119
+ const engineRoot = dirname(require2.resolve("aeon-ui-engine/package.json"));
120
+ return join2(engineRoot, "scripts/link-scoped-packages.mjs");
121
+ } catch {
122
+ throw new Error(
123
+ "Could not find scripts/link-scoped-packages.mjs (aeon-ui-engine not installed?)"
124
+ );
125
+ }
126
+ }
127
+ function linkAeon(options = {}) {
128
+ const script = resolveLinkScript();
129
+ const args = [script];
130
+ if (options.cwd) {
131
+ args.push("--cwd", options.cwd);
132
+ }
133
+ if (options.quiet) {
134
+ args.push("--quiet");
135
+ }
136
+ if (options.force) {
137
+ args.push("--force");
138
+ }
139
+ const result = spawnSync(process.execPath, args, {
140
+ stdio: options.quiet ? "pipe" : "inherit",
141
+ env: process.env,
142
+ cwd: options.cwd ?? process.cwd()
143
+ });
144
+ if (result.status !== 0 && result.status !== null) {
145
+ const err = result.stderr?.toString() || `link exited with code ${result.status}`;
146
+ throw new Error(err);
147
+ }
148
+ return {
149
+ cwd: options.cwd ?? process.cwd(),
150
+ status: result.status ?? 0
151
+ };
152
+ }
153
+
154
+ // src/migrate.ts
155
+ import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
156
+ import { join as join3 } from "path";
157
+ var FINDINGS = [
158
+ {
159
+ id: "preview-window-machine",
160
+ severity: "replace",
161
+ patterns: ["preview-window-machine", "previewWindowMachine"],
162
+ advice: "Replace with RangeSlider from @aeon-ui/react or @aeon-ui/ui (rangeSliderMachine)."
163
+ },
164
+ {
165
+ id: "file-dropzone",
166
+ severity: "replace",
167
+ patterns: ["file-dropzone-machine", "fileDropzoneMachine", "FileDropzone"],
168
+ advice: "Replace with FileUpload compound (fileUploadMachine) \u2014 idle | dragging | rejected."
169
+ },
170
+ {
171
+ id: "sonner-bridge",
172
+ severity: "replace",
173
+ patterns: ["from 'sonner'", 'from "sonner"', "components/ui/sonner"],
174
+ advice: "Use Toast.Provider + useToast().publish / dismiss (aeon-ui-engine \u2265 1.2)."
175
+ },
176
+ {
177
+ id: "manual-link-button",
178
+ severity: "optional",
179
+ patterns: ['data-aeon-scope="button"', "buttonVariants("],
180
+ advice: "Prefer <Button.Root asChild><Link \u2026 /></Button.Root> instead of hand-rolled data-aeon-* on anchors."
181
+ },
182
+ {
183
+ id: "chakra",
184
+ severity: "info",
185
+ patterns: ["@chakra-ui/react", "@ark-ui/react"],
186
+ advice: "Migrate leaf \u2192 forms \u2192 overlays per docs/MIGRATION_FROM_ARK.md; see docs/MIGRATION.md for greenfield checklist."
187
+ },
188
+ {
189
+ id: "alert-dialog-local",
190
+ severity: "optional",
191
+ patterns: ["alertDialogMachine", 'role="alertdialog"'],
192
+ advice: "Prefer Dialog/Prompt with dismissible=false for destructive confirms until AlertDialog ships."
193
+ }
194
+ ];
195
+ var CANDIDATES = [
196
+ "package.json",
197
+ "package-lock.json",
198
+ "pnpm-lock.yaml",
199
+ "lib/preview-window-machine.ts",
200
+ "lib/file-dropzone-machine.ts",
201
+ "components/file-dropzone.tsx",
202
+ "components/ui/sonner.tsx",
203
+ "components/ui/alert-dialog.tsx",
204
+ "components/ui/slider.tsx",
205
+ "components/ui/button.tsx",
206
+ "components/dual-login-buttons.tsx"
207
+ ];
208
+ function walkHints(cwd) {
209
+ const hits = [];
210
+ let corpus = "";
211
+ for (const rel of CANDIDATES) {
212
+ const abs = join3(cwd, rel);
213
+ if (!existsSync3(abs)) continue;
214
+ try {
215
+ corpus += `
216
+ ${rel}
217
+ ${readFileSync(abs, "utf8")}`;
218
+ } catch {
219
+ }
220
+ }
221
+ for (const rule of FINDINGS) {
222
+ const matched = rule.patterns.find((p) => corpus.includes(p));
223
+ if (matched) {
224
+ hits.push({
225
+ id: rule.id,
226
+ severity: rule.severity,
227
+ pattern: matched,
228
+ advice: rule.advice
229
+ });
230
+ }
231
+ }
232
+ return hits;
233
+ }
234
+ var CHECKLIST = `# Aeon migration checklist
235
+
236
+ Generated by \`npx aeon-ui migrate\`.
237
+
238
+ ## Install
239
+
240
+ \`\`\`bash
241
+ npm install aeon-ui-engine@^1.4.0 react react-dom xstate @xstate/react
242
+ \`\`\`
243
+
244
+ \`\`\`ts
245
+ import 'aeon-ui-engine/aeon.css'
246
+ \`\`\`
247
+
248
+ ## Agent setup
249
+
250
+ \`\`\`bash
251
+ npx aeon-ui init
252
+ \`\`\`
253
+
254
+ Paste \`aeon.cursorrules.md\` into Cursor rules. Keep domain machines in the app; use Aeon for chrome + interactive primitives.
255
+
256
+ ## Statechart-first rule
257
+
258
+ 1. Model interaction with an XState machine (or reuse Aeon machines).
259
+ 2. Project \`snapshot\` \u2192 \`data-aeon-state\` / compound parts.
260
+ 3. Do not invent parallel booleans for chart-owned concerns.
261
+
262
+ ## New primitives (1.4+)
263
+
264
+ | Need | Use |
265
+ |------|-----|
266
+ | Two-thumb range | \`RangeSlider\` + \`rangeSliderMachine\` |
267
+ | Link styled as button | \`<Button.Root asChild><Link/></Button.Root>\` |
268
+ | Drag-drop file surface | \`FileUpload\` + \`fileUploadMachine\` |
269
+ | Toast | \`Toast.Provider\` + \`useToast()\` |
270
+ | Confirm sheet | \`Prompt\` / \`Dialog\` |
271
+
272
+ ## Docs
273
+
274
+ - docs/MIGRATION.md (in aeon-ui-engine / AeonUI repo)
275
+ - docs/MIGRATION_FROM_ARK.md
276
+ - AGENTS.md
277
+ `;
278
+ function migrateAeon(options = {}) {
279
+ const cwd = options.cwd ?? process.cwd();
280
+ const write = options.write ?? true;
281
+ const findings = walkHints(cwd);
282
+ const written = [];
283
+ if (write) {
284
+ const path = join3(cwd, "AEON_MIGRATION_CHECKLIST.md");
285
+ const findingsBlock = findings.length === 0 ? "\n## Scan results\n\nNo known local workarounds detected in the quick scan.\n" : `
286
+ ## Scan results
287
+
288
+ ${findings.map((f) => `- **${f.severity}** \`${f.id}\` (matched \`${f.pattern}\`): ${f.advice}`).join("\n")}
289
+ `;
290
+ writeFileSync2(path, `${CHECKLIST.trim()}
291
+ ${findingsBlock}`);
292
+ written.push("write AEON_MIGRATION_CHECKLIST.md");
293
+ }
294
+ return { cwd, written, findings };
295
+ }
296
+
297
+ export {
298
+ initAeon,
299
+ linkAeon,
300
+ migrateAeon
301
+ };
@@ -7,4 +7,34 @@ declare function initAeon(options?: InitOptions): {
7
7
  written: string[];
8
8
  };
9
9
 
10
- export { type InitOptions, initAeon };
10
+ type LinkOptions = {
11
+ cwd?: string;
12
+ quiet?: boolean;
13
+ force?: boolean;
14
+ };
15
+ /**
16
+ * Symlink `node_modules/@aeon-ui/*` → `aeon-ui-engine/packages/*`.
17
+ * Prefer the postinstall hook; this command re-runs linking manually.
18
+ */
19
+ declare function linkAeon(options?: LinkOptions): {
20
+ cwd: string;
21
+ status: number;
22
+ };
23
+
24
+ type MigrateOptions = {
25
+ cwd?: string;
26
+ write?: boolean;
27
+ };
28
+ type Finding = {
29
+ id: string;
30
+ severity: 'replace' | 'optional' | 'info';
31
+ pattern: string;
32
+ advice: string;
33
+ };
34
+ declare function migrateAeon(options?: MigrateOptions): {
35
+ cwd: string;
36
+ written: string[];
37
+ findings: Finding[];
38
+ };
39
+
40
+ export { type InitOptions, type LinkOptions, type MigrateOptions, initAeon, linkAeon, migrateAeon };
@@ -1,6 +1,10 @@
1
1
  import {
2
- initAeon
3
- } from "./chunk-IW5NT2Z2.js";
2
+ initAeon,
3
+ linkAeon,
4
+ migrateAeon
5
+ } from "./chunk-JMNRJCWF.js";
4
6
  export {
5
- initAeon
7
+ initAeon,
8
+ linkAeon,
9
+ migrateAeon
6
10
  };
@@ -1,31 +1,77 @@
1
1
  #!/usr/bin/env node
2
2
  import { initAeon } from './init.js'
3
+ import { linkAeon } from './link.js'
4
+ import { migrateAeon } from './migrate.js'
3
5
 
4
6
  const args = process.argv.slice(2)
5
7
  const cmd = args[0] ?? 'help'
6
8
  const force = args.includes('--force') || args.includes('-f')
9
+ const checkOnly = args.includes('--check')
10
+ const quiet = args.includes('--quiet') || args.includes('-q')
11
+
12
+ function argValue(flag: string): string | undefined {
13
+ const i = args.indexOf(flag)
14
+ if (i >= 0 && args[i + 1] && !args[i + 1].startsWith('-')) return args[i + 1]
15
+ return undefined
16
+ }
17
+
18
+ const cwdArg = argValue('--cwd')
7
19
 
8
20
  function help() {
9
21
  console.log(`aeon-ui — AI-optimized CLI for Aeon UI
10
22
 
11
23
  Usage:
12
24
  npx aeon-ui init [--force]
25
+ npx aeon-ui link [--quiet] [--force] [--cwd <dir>]
26
+ npx aeon-ui migrate [--check]
13
27
 
14
28
  Commands:
15
- init Inject Cursor rules, JSON schemas, system prompt, and a starter component
16
- help Show this message
29
+ init Inject Cursor rules, JSON schemas, system prompt, and a starter component
30
+ link Symlink node_modules/@aeon-ui/* → aeon-ui-engine/packages/* (also runs on postinstall)
31
+ migrate Scan for known workarounds and write AEON_MIGRATION_CHECKLIST.md
32
+ help Show this message
17
33
 
18
34
  Options:
19
- --force, -f Overwrite existing generated files
35
+ --force, -f Overwrite existing generated files (init); replace existing @aeon-ui links (link)
36
+ --check Report findings only; do not write checklist (migrate)
37
+ --quiet, -q Suppress link output (link)
38
+ --cwd <dir> Project root for link (defaults to process.cwd())
20
39
  `)
21
40
  }
22
41
 
23
42
  if (cmd === 'init') {
24
- const result = initAeon({ force })
43
+ const result = initAeon({ force, cwd: cwdArg })
25
44
  console.log(`Aeon UI init → ${result.cwd}`)
26
45
  for (const line of result.written) console.log(` ${line}`)
46
+ try {
47
+ linkAeon({ cwd: result.cwd, quiet: true })
48
+ console.log(' link @aeon-ui/* (scoped packages)')
49
+ } catch {
50
+ console.log(' link skipped (install aeon-ui-engine first, then: npx aeon-ui link)')
51
+ }
27
52
  console.log('\nNext: npm install aeon-ui-engine react react-dom xstate @xstate/react')
28
53
  console.log('Docs: AEON_AI.md · paste aeon.cursorrules.md into your agent system prompt')
54
+ } else if (cmd === 'link') {
55
+ try {
56
+ const result = linkAeon({ quiet, force, cwd: cwdArg })
57
+ if (quiet) console.log(`Aeon UI link → ${result.cwd}`)
58
+ } catch (err) {
59
+ console.error(err instanceof Error ? err.message : err)
60
+ process.exitCode = 1
61
+ }
62
+ } else if (cmd === 'migrate') {
63
+ const result = migrateAeon({ write: !checkOnly })
64
+ console.log(`Aeon UI migrate → ${result.cwd}`)
65
+ for (const line of result.written) console.log(` ${line}`)
66
+ if (result.findings.length === 0) {
67
+ console.log(' (no known workarounds matched in quick scan)')
68
+ } else {
69
+ for (const f of result.findings) {
70
+ console.log(` [${f.severity}] ${f.id}: ${f.advice}`)
71
+ }
72
+ }
73
+ console.log('\nInstall: npm install aeon-ui-engine@^1.4.0 xstate @xstate/react')
74
+ console.log('Docs: https://aeon-ui.com · repo docs/MIGRATION.md')
29
75
  } else {
30
76
  help()
31
77
  if (cmd !== 'help' && cmd !== '--help' && cmd !== '-h') {
@@ -1 +1,3 @@
1
1
  export { initAeon, type InitOptions } from './init.js'
2
+ export { linkAeon, type LinkOptions } from './link.js'
3
+ export { migrateAeon, type MigrateOptions } from './migrate.js'
@@ -92,10 +92,16 @@ Generated by \`npx aeon-ui init\`.
92
92
  ## Next steps
93
93
 
94
94
  1. Install: \`npm install aeon-ui-engine react react-dom xstate @xstate/react\`
95
- 2. CSS: \`import 'aeon-ui-engine/aeon.css'\`
96
- 3. Vite: add \`aeonUiVitePlugin()\` from \`aeon-ui-engine/vite\`
97
- 4. Paste \`aeon.cursorrules.md\` into Cursor rules / system prompt (already mirrored to \`.cursor/rules/aeon-ui.mdc\`)
98
- 5. Constrain generation with JSON under \`aeon.schemas/\`
95
+ - Postinstall links \`@aeon-ui/*\` into \`node_modules\` (re-run with \`npx aeon-ui link\` if needed).
96
+ 2. CSS: \`import 'aeon-ui-engine/aeon.css'\` (e.g. root layout).
97
+ 3. **Next.js:** add \`transpilePackages\` for \`@aeon-ui/ui\`, \`@aeon-ui/react\`, \`@aeon-ui/core\`, \`@aeon-ui/primitives\`, \`@aeon-ui/panda\`. Use \`"use client"\` for interactive components. No webpack aliases required.
98
+ 4. **Vite (optional):** \`aeonUiVitePlugin()\` from \`aeon-ui-engine/vite\` still helps with optimizeDeps; scoped imports resolve via the link step alone.
99
+ 5. Paste \`aeon.cursorrules.md\` into Cursor rules / system prompt (already mirrored to \`.cursor/rules/aeon-ui.mdc\`)
100
+ 6. Constrain generation with JSON under \`aeon.schemas/\`
101
+
102
+ ## Frameworks
103
+
104
+ Aeon is **React-first** (full catalog + styled layer). Next.js is supported. Vue / Solid / Svelte adapters exist but are thin — not the main product.
99
105
 
100
106
  ## Principle
101
107
 
@@ -0,0 +1,61 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { createRequire } from 'node:module'
3
+ import { existsSync } from 'node:fs'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ export type LinkOptions = {
8
+ cwd?: string
9
+ quiet?: boolean
10
+ force?: boolean
11
+ }
12
+
13
+ function resolveLinkScript(): string {
14
+ const here = dirname(fileURLToPath(import.meta.url))
15
+ const fromMonorepo = join(here, '../../../scripts/link-scoped-packages.mjs')
16
+ if (existsSync(fromMonorepo)) return fromMonorepo
17
+
18
+ try {
19
+ const require = createRequire(import.meta.url)
20
+ const engineRoot = dirname(require.resolve('aeon-ui-engine/package.json'))
21
+ return join(engineRoot, 'scripts/link-scoped-packages.mjs')
22
+ } catch {
23
+ throw new Error(
24
+ 'Could not find scripts/link-scoped-packages.mjs (aeon-ui-engine not installed?)',
25
+ )
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Symlink `node_modules/@aeon-ui/*` → `aeon-ui-engine/packages/*`.
31
+ * Prefer the postinstall hook; this command re-runs linking manually.
32
+ */
33
+ export function linkAeon(options: LinkOptions = {}) {
34
+ const script = resolveLinkScript()
35
+ const args = [script]
36
+ if (options.cwd) {
37
+ args.push('--cwd', options.cwd)
38
+ }
39
+ if (options.quiet) {
40
+ args.push('--quiet')
41
+ }
42
+ if (options.force) {
43
+ args.push('--force')
44
+ }
45
+
46
+ const result = spawnSync(process.execPath, args, {
47
+ stdio: options.quiet ? 'pipe' : 'inherit',
48
+ env: process.env,
49
+ cwd: options.cwd ?? process.cwd(),
50
+ })
51
+
52
+ if (result.status !== 0 && result.status !== null) {
53
+ const err = result.stderr?.toString() || `link exited with code ${result.status}`
54
+ throw new Error(err)
55
+ }
56
+
57
+ return {
58
+ cwd: options.cwd ?? process.cwd(),
59
+ status: result.status ?? 0,
60
+ }
61
+ }
@@ -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
+ }