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.
- package/CHANGELOG.md +52 -0
- package/CONSUMER.md +67 -27
- package/INTEGRATION.md +1 -1
- package/README.md +17 -8
- package/package.json +4 -1
- package/packages/cli/README.md +13 -1
- package/packages/cli/dist/bin.js +50 -6
- package/packages/cli/dist/chunk-JMNRJCWF.js +301 -0
- package/packages/cli/dist/index.d.ts +31 -1
- package/packages/cli/dist/index.js +7 -3
- package/packages/cli/src/bin.ts +50 -4
- package/packages/cli/src/index.ts +2 -0
- package/packages/cli/src/init.ts +10 -4
- package/packages/cli/src/link.ts +61 -0
- package/packages/cli/src/migrate.ts +170 -0
- package/packages/core/dist/index.d.ts +35 -7
- package/packages/core/dist/index.js +102 -2
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/core/src/anatomy.ts +39 -3
- package/packages/core/src/identicon.test.ts +39 -0
- package/packages/core/src/identicon.ts +79 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/panda/panda.config.ts +4 -0
- package/packages/panda/src/recipes/composer.ts +58 -4
- package/packages/panda/src/recipes/file-upload.ts +100 -0
- package/packages/panda/src/recipes/index.ts +2 -0
- package/packages/panda/src/recipes/range-slider.ts +59 -0
- package/packages/panda/src/recipes/thread.ts +77 -2
- package/packages/panda/styled-system/recipes/aeon-composer.d.ts +1 -1
- package/packages/panda/styled-system/recipes/aeon-composer.mjs +12 -0
- package/packages/panda/styled-system/recipes/aeon-file-upload.d.ts +31 -0
- package/packages/panda/styled-system/recipes/aeon-file-upload.mjs +61 -0
- package/packages/panda/styled-system/recipes/aeon-range-slider.d.ts +31 -0
- package/packages/panda/styled-system/recipes/aeon-range-slider.mjs +49 -0
- package/packages/panda/styled-system/recipes/aeon-thread.d.ts +1 -1
- package/packages/panda/styled-system/recipes/aeon-thread.mjs +20 -0
- package/packages/panda/styled-system/recipes/index.d.ts +2 -0
- package/packages/panda/styled-system/recipes/index.mjs +2 -0
- package/packages/panda/styled-system/styles.css +292 -0
- package/packages/primitives/dist/chunk-CV4UXXZF.js +44 -0
- package/packages/primitives/dist/chunk-IZKQSCYU.js +164 -0
- package/packages/primitives/dist/{chunk-PB3LFHXU.js → chunk-VUPWYBGU.js} +14 -0
- package/packages/primitives/dist/index.d.ts +2 -0
- package/packages/primitives/dist/index.js +23 -7
- package/packages/primitives/dist/machines/file-upload.d.ts +39 -0
- package/packages/primitives/dist/machines/file-upload.js +8 -0
- package/packages/primitives/dist/machines/range-slider.d.ts +129 -0
- package/packages/primitives/dist/machines/range-slider.js +12 -0
- package/packages/primitives/dist/machines/slider.d.ts +22 -0
- package/packages/primitives/dist/machines/slider.js +1 -1
- package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/primitives/src/__tests__/file-upload.test.ts +25 -0
- package/packages/primitives/src/__tests__/range-slider.test.ts +39 -0
- package/packages/primitives/src/index.ts +2 -0
- package/packages/primitives/src/machines/file-upload.ts +55 -0
- package/packages/primitives/src/machines/range-slider.ts +209 -0
- package/packages/primitives/src/machines/slider.ts +17 -0
- package/packages/react/dist/index.d.ts +132 -26
- package/packages/react/dist/index.js +1822 -1029
- package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/react/src/button/index.tsx +50 -15
- package/packages/react/src/composer/index.tsx +58 -3
- package/packages/react/src/file-upload/index.tsx +322 -0
- package/packages/react/src/floating/FloatingPositioner.tsx +8 -0
- package/packages/react/src/hooks/use-anchor-position.ts +88 -17
- package/packages/react/src/index.ts +3 -0
- package/packages/react/src/menu/index.tsx +37 -3
- package/packages/react/src/popover/index.tsx +7 -1
- package/packages/react/src/prompt/index.tsx +45 -0
- package/packages/react/src/range-slider/index.tsx +322 -0
- package/packages/react/src/slider/index.tsx +54 -14
- package/packages/react/src/thread/index.tsx +74 -3
- package/packages/react/src/tooltip/index.tsx +7 -1
- package/packages/react/src/utils/slot.tsx +47 -0
- package/packages/schemas/dist/index.d.ts +4 -0
- package/packages/schemas/dist/index.js +67 -1
- package/packages/schemas/json/button.json +6 -0
- package/packages/schemas/json/catalog.json +333 -0
- package/packages/schemas/json/fileUpload.json +168 -0
- package/packages/schemas/json/rangeSlider.json +157 -0
- package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/schemas/prompts/cursorrules.md +3 -1
- package/packages/schemas/prompts/statecharts.md +1 -1
- package/packages/schemas/registry/catalog.json +18 -0
- package/packages/schemas/registry/components/button.json +6 -0
- package/packages/schemas/registry/components/fileUpload.json +168 -0
- package/packages/schemas/registry/components/rangeSlider.json +157 -0
- package/packages/schemas/registry/machines/fileUpload.json +182 -0
- package/packages/schemas/registry/machines/rangeSlider.json +201 -0
- package/packages/schemas/scripts/emit.ts +4 -0
- package/packages/schemas/src/components.ts +70 -0
- package/packages/schemas/src/prompts.ts +3 -1
- package/packages/schemas/tsconfig.json +1 -1
- package/packages/ui/dist/index.d.ts +100 -46
- package/packages/ui/dist/index.js +463 -341
- package/packages/ui/src/composer.tsx +37 -11
- package/packages/ui/src/file-upload.tsx +36 -0
- package/packages/ui/src/index.ts +2 -0
- package/packages/ui/src/prompt.tsx +3 -0
- package/packages/ui/src/range-slider.tsx +24 -0
- package/packages/ui/src/thread.tsx +40 -0
- package/scripts/link-scoped-packages.mjs +206 -0
- package/scripts/vite-aliases.mjs +6 -8
- 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
|
-
|
|
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 };
|
package/packages/cli/src/bin.ts
CHANGED
|
@@ -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
|
|
16
|
-
|
|
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') {
|
package/packages/cli/src/init.ts
CHANGED
|
@@ -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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
}
|