decantr 0.9.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.
- package/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- package/types/test.d.ts +48 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import {
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const srcRoot = join(__dirname, '..', '..', 'src');
|
|
13
|
+
const registryDir = join(srcRoot, 'registry');
|
|
14
|
+
|
|
15
|
+
// ─── Registry data (loaded once at startup) ────────────────────
|
|
16
|
+
|
|
17
|
+
let components, patternsIndex, archetypesIndex, tokens, icons, skeletons;
|
|
18
|
+
const patternCache = new Map();
|
|
19
|
+
const archetypeCache = new Map();
|
|
20
|
+
|
|
21
|
+
async function loadJSON(path) {
|
|
22
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function loadRegistry() {
|
|
26
|
+
[components, patternsIndex, archetypesIndex, tokens, icons, skeletons] = await Promise.all([
|
|
27
|
+
loadJSON(join(registryDir, 'components.json')),
|
|
28
|
+
loadJSON(join(registryDir, 'patterns', 'index.json')),
|
|
29
|
+
loadJSON(join(registryDir, 'archetypes', 'index.json')),
|
|
30
|
+
loadJSON(join(registryDir, 'tokens.json')),
|
|
31
|
+
loadJSON(join(registryDir, 'icons.json')),
|
|
32
|
+
loadJSON(join(registryDir, 'skeletons.json')),
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getPattern(id) {
|
|
37
|
+
if (patternCache.has(id)) return patternCache.get(id);
|
|
38
|
+
const entry = patternsIndex.patterns[id];
|
|
39
|
+
if (!entry) return null;
|
|
40
|
+
const data = await loadJSON(join(registryDir, 'patterns', entry.file));
|
|
41
|
+
patternCache.set(id, data);
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function getArchetype(id) {
|
|
46
|
+
if (archetypeCache.has(id)) return archetypeCache.get(id);
|
|
47
|
+
const entry = archetypesIndex.archetypes[id];
|
|
48
|
+
if (!entry) return null;
|
|
49
|
+
const data = await loadJSON(join(registryDir, 'archetypes', entry.file));
|
|
50
|
+
archetypeCache.set(id, data);
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Atom resolver (imported dynamically to stay ESM-clean) ────
|
|
55
|
+
|
|
56
|
+
let resolveAtomDecl;
|
|
57
|
+
async function loadAtomResolver() {
|
|
58
|
+
const mod = await import(pathToFileURL(join(srcRoot, 'css', 'atoms.js')).href);
|
|
59
|
+
resolveAtomDecl = mod.resolveAtomDecl;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Validation logic (extracted from validate.js) ─────────────
|
|
63
|
+
|
|
64
|
+
const KNOWN_ARCHETYPES = ['ecommerce', 'saas-dashboard', 'portfolio', 'content-site', 'docs-explorer', 'financial-dashboard', 'recipe-community'];
|
|
65
|
+
const KNOWN_STYLES = ['auradecantism', 'clean', 'retro', 'glassmorphism', 'command-center'];
|
|
66
|
+
|
|
67
|
+
function validateEssence(essence) {
|
|
68
|
+
const errors = [];
|
|
69
|
+
const warnings = [];
|
|
70
|
+
|
|
71
|
+
const isSectioned = Array.isArray(essence.sections);
|
|
72
|
+
const isSimple = typeof essence.terroir === 'string' || essence.terroir === null;
|
|
73
|
+
|
|
74
|
+
if (!isSectioned && !isSimple) {
|
|
75
|
+
errors.push('Essence must have either "terroir" (simple) or "sections" (sectioned)');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validateVintage(vintage, prefix) {
|
|
79
|
+
if (!vintage) return;
|
|
80
|
+
if (vintage.style && !KNOWN_STYLES.includes(vintage.style)) {
|
|
81
|
+
warnings.push(`${prefix}vintage.style "${vintage.style}" is not a built-in style. Built-in: ${KNOWN_STYLES.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
if (vintage.mode && !['light', 'dark', 'auto'].includes(vintage.mode)) {
|
|
84
|
+
errors.push(`${prefix}vintage.mode must be light|dark|auto, got "${vintage.mode}"`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateStructure(structure, prefix) {
|
|
89
|
+
if (!Array.isArray(structure)) return;
|
|
90
|
+
for (const page of structure) {
|
|
91
|
+
if (!page.id) errors.push(`${prefix}structure entry missing "id"`);
|
|
92
|
+
if (!page.skeleton) warnings.push(`${prefix}structure entry "${page.id || '?'}" missing "skeleton"`);
|
|
93
|
+
if (!page.blend && !page.patterns) warnings.push(`${prefix}structure entry "${page.id || '?'}" missing "blend"`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isSectioned) {
|
|
98
|
+
const sectionPaths = new Set();
|
|
99
|
+
for (const section of essence.sections) {
|
|
100
|
+
if (!section.id) errors.push('Section missing "id"');
|
|
101
|
+
if (!section.path) errors.push(`Section "${section.id || '?'}" missing "path"`);
|
|
102
|
+
if (section.path && sectionPaths.has(section.path)) {
|
|
103
|
+
errors.push(`Duplicate section path: "${section.path}"`);
|
|
104
|
+
}
|
|
105
|
+
if (section.path) sectionPaths.add(section.path);
|
|
106
|
+
if (section.terroir && !KNOWN_ARCHETYPES.includes(section.terroir)) {
|
|
107
|
+
errors.push(`Section "${section.id}": terroir "${section.terroir}" is not a known archetype`);
|
|
108
|
+
}
|
|
109
|
+
validateVintage(section.vintage, `Section "${section.id || '?'}": `);
|
|
110
|
+
validateStructure(section.structure, `Section "${section.id || '?'}": `);
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(essence.shared_tannins)) {
|
|
113
|
+
for (const section of essence.sections) {
|
|
114
|
+
if (Array.isArray(section.tannins)) {
|
|
115
|
+
for (const t of section.tannins) {
|
|
116
|
+
if (essence.shared_tannins.includes(t)) {
|
|
117
|
+
warnings.push(`Tannin "${t}" is in both shared_tannins and section "${section.id}"`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
if (essence.terroir && !KNOWN_ARCHETYPES.includes(essence.terroir)) {
|
|
125
|
+
errors.push(`terroir "${essence.terroir}" is not a known archetype. Known: ${KNOWN_ARCHETYPES.join(', ')}`);
|
|
126
|
+
}
|
|
127
|
+
validateVintage(essence.vintage, '');
|
|
128
|
+
validateStructure(essence.structure, '');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (essence.vessel) {
|
|
132
|
+
if (essence.vessel.type && !['spa', 'mpa'].includes(essence.vessel.type)) {
|
|
133
|
+
warnings.push(`vessel.type "${essence.vessel.type}" is unusual (expected spa|mpa)`);
|
|
134
|
+
}
|
|
135
|
+
if (essence.vessel.routing && !['hash', 'history'].includes(essence.vessel.routing)) {
|
|
136
|
+
errors.push(`vessel.routing must be hash|history, got "${essence.vessel.routing}"`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (essence.cork && typeof essence.cork !== 'object') {
|
|
141
|
+
errors.push('cork must be an object');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Fuzzy search ──────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
function fuzzyMatch(query, text) {
|
|
150
|
+
const q = query.toLowerCase();
|
|
151
|
+
const t = text.toLowerCase();
|
|
152
|
+
if (t.includes(q)) return true;
|
|
153
|
+
// Simple subsequence match
|
|
154
|
+
let qi = 0;
|
|
155
|
+
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
156
|
+
if (t[ti] === q[qi]) qi++;
|
|
157
|
+
}
|
|
158
|
+
return qi === q.length;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function fuzzyScore(query, text) {
|
|
162
|
+
const q = query.toLowerCase();
|
|
163
|
+
const t = text.toLowerCase();
|
|
164
|
+
if (t === q) return 100;
|
|
165
|
+
if (t.startsWith(q)) return 90;
|
|
166
|
+
if (t.includes(q)) return 80;
|
|
167
|
+
// Subsequence scoring
|
|
168
|
+
let qi = 0;
|
|
169
|
+
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
170
|
+
if (t[ti] === q[qi]) qi++;
|
|
171
|
+
}
|
|
172
|
+
return qi === q.length ? 60 : 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Tool definitions ──────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
const READ_ONLY_ANNOTATIONS = {
|
|
178
|
+
readOnlyHint: true,
|
|
179
|
+
destructiveHint: false,
|
|
180
|
+
idempotentHint: true,
|
|
181
|
+
openWorldHint: false,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const TOOLS = [
|
|
185
|
+
{
|
|
186
|
+
name: 'lookup_component',
|
|
187
|
+
title: 'Look Up Component',
|
|
188
|
+
description: 'Look up a decantr component by name. Returns props, types, reactive flags, examples, and showcase data.',
|
|
189
|
+
inputSchema: {
|
|
190
|
+
type: 'object',
|
|
191
|
+
properties: {
|
|
192
|
+
name: { type: 'string', description: 'Component name (e.g. "Button", "Card", "Modal")' },
|
|
193
|
+
},
|
|
194
|
+
required: ['name'],
|
|
195
|
+
},
|
|
196
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'lookup_pattern',
|
|
200
|
+
title: 'Look Up Pattern',
|
|
201
|
+
description: 'Look up an experience pattern by ID. Returns blend specs, components used, recipe overrides, and example code.',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
name: { type: 'string', description: 'Pattern ID (e.g. "hero", "data-table", "kpi-grid")' },
|
|
206
|
+
},
|
|
207
|
+
required: ['name'],
|
|
208
|
+
},
|
|
209
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'lookup_archetype',
|
|
213
|
+
title: 'Look Up Archetype',
|
|
214
|
+
description: 'Look up a domain archetype by ID. Returns pages, skeletons, tannins, suggested vintage, and default blends.',
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
properties: {
|
|
218
|
+
name: { type: 'string', description: 'Archetype ID (e.g. "saas-dashboard", "ecommerce", "portfolio")' },
|
|
219
|
+
},
|
|
220
|
+
required: ['name'],
|
|
221
|
+
},
|
|
222
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'resolve_atoms',
|
|
226
|
+
title: 'Resolve Atoms',
|
|
227
|
+
description: 'Resolve space-separated decantr atom class names to their CSS declarations. Returns validity and CSS for each atom.',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: {
|
|
231
|
+
atoms: { type: 'string', description: 'Space-separated atom names (e.g. "_flex _col _gap4 _p4 _bgprimary")' },
|
|
232
|
+
},
|
|
233
|
+
required: ['atoms'],
|
|
234
|
+
},
|
|
235
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'lookup_tokens',
|
|
239
|
+
title: 'Look Up Tokens',
|
|
240
|
+
description: 'Look up decantr design tokens. Returns token names, values, and organization by group.',
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
group: { type: 'string', description: 'Optional group filter (palette, neutral, surfaces, typography, spacing, elevation, motion, chart)' },
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'lookup_icon',
|
|
251
|
+
title: 'Look Up Icon',
|
|
252
|
+
description: 'Check if a decantr icon exists by name, or list all available icons.',
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: 'object',
|
|
255
|
+
properties: {
|
|
256
|
+
name: { type: 'string', description: 'Icon name to check (e.g. "arrow-left", "check"). Omit to list all icons.' },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'search_registry',
|
|
263
|
+
title: 'Search Registry',
|
|
264
|
+
description: 'Fuzzy search across all decantr registry entries — components, patterns, archetypes, and icons.',
|
|
265
|
+
inputSchema: {
|
|
266
|
+
type: 'object',
|
|
267
|
+
properties: {
|
|
268
|
+
query: { type: 'string', description: 'Search query (e.g. "table", "auth", "chart")' },
|
|
269
|
+
},
|
|
270
|
+
required: ['query'],
|
|
271
|
+
},
|
|
272
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'validate_essence',
|
|
276
|
+
title: 'Validate Essence',
|
|
277
|
+
description: 'Validate a decantr.essence.json file. Returns errors and warnings.',
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
properties: {
|
|
281
|
+
path: { type: 'string', description: 'Path to essence file. Defaults to ./decantr.essence.json in the current working directory.' },
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'lookup_skeleton',
|
|
288
|
+
title: 'Look Up Skeleton',
|
|
289
|
+
description: 'Look up a page skeleton layout by name, or list all available skeletons. Returns layout specs, atoms, and code examples.',
|
|
290
|
+
inputSchema: {
|
|
291
|
+
type: 'object',
|
|
292
|
+
properties: {
|
|
293
|
+
name: { type: 'string', description: 'Skeleton name (e.g. "sidebar-main", "top-nav-main"). Omit to list all skeletons.' },
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
// ─── Tool handlers ─────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
const MAX_INPUT_LENGTH = 1000;
|
|
303
|
+
|
|
304
|
+
function validateStringArg(args, field) {
|
|
305
|
+
const val = args[field];
|
|
306
|
+
if (!val || typeof val !== 'string') {
|
|
307
|
+
return `Required parameter "${field}" must be a non-empty string.`;
|
|
308
|
+
}
|
|
309
|
+
if (val.length > MAX_INPUT_LENGTH) {
|
|
310
|
+
return `Parameter "${field}" exceeds maximum length of ${MAX_INPUT_LENGTH} characters.`;
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function handleTool(name, args) {
|
|
316
|
+
switch (name) {
|
|
317
|
+
case 'lookup_component': {
|
|
318
|
+
const err = validateStringArg(args, 'name');
|
|
319
|
+
if (err) return { error: err };
|
|
320
|
+
const compName = args.name;
|
|
321
|
+
const comp = components.components[compName];
|
|
322
|
+
if (!comp) {
|
|
323
|
+
// Try case-insensitive search
|
|
324
|
+
const key = Object.keys(components.components).find(
|
|
325
|
+
k => k.toLowerCase() === compName.toLowerCase()
|
|
326
|
+
);
|
|
327
|
+
if (key) {
|
|
328
|
+
return { found: true, name: key, ...components.components[key] };
|
|
329
|
+
}
|
|
330
|
+
// Suggest similar names
|
|
331
|
+
const similar = Object.keys(components.components)
|
|
332
|
+
.filter(k => fuzzyMatch(compName, k))
|
|
333
|
+
.slice(0, 5);
|
|
334
|
+
return { found: false, message: `Component "${compName}" not found.`, suggestions: similar };
|
|
335
|
+
}
|
|
336
|
+
return { found: true, name: compName, ...comp };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
case 'lookup_pattern': {
|
|
340
|
+
const err = validateStringArg(args, 'name');
|
|
341
|
+
if (err) return { error: err };
|
|
342
|
+
const patternId = args.name;
|
|
343
|
+
const pattern = await getPattern(patternId);
|
|
344
|
+
if (!pattern) {
|
|
345
|
+
const similar = Object.keys(patternsIndex.patterns)
|
|
346
|
+
.filter(k => fuzzyMatch(patternId, k))
|
|
347
|
+
.slice(0, 5);
|
|
348
|
+
return { found: false, message: `Pattern "${patternId}" not found.`, suggestions: similar };
|
|
349
|
+
}
|
|
350
|
+
return { found: true, ...pattern };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
case 'lookup_archetype': {
|
|
354
|
+
const err = validateStringArg(args, 'name');
|
|
355
|
+
if (err) return { error: err };
|
|
356
|
+
const archId = args.name;
|
|
357
|
+
const arch = await getArchetype(archId);
|
|
358
|
+
if (!arch) {
|
|
359
|
+
const similar = Object.keys(archetypesIndex.archetypes)
|
|
360
|
+
.filter(k => fuzzyMatch(archId, k))
|
|
361
|
+
.slice(0, 5);
|
|
362
|
+
return { found: false, message: `Archetype "${archId}" not found.`, suggestions: similar };
|
|
363
|
+
}
|
|
364
|
+
return { found: true, ...arch };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
case 'resolve_atoms': {
|
|
368
|
+
const err = validateStringArg(args, 'atoms');
|
|
369
|
+
if (err) return { error: err };
|
|
370
|
+
const atomNames = args.atoms.trim().split(/\s+/);
|
|
371
|
+
const results = atomNames.map(atom => {
|
|
372
|
+
const css = resolveAtomDecl(atom);
|
|
373
|
+
return { atom, css: css || null, valid: css !== null };
|
|
374
|
+
});
|
|
375
|
+
const valid = results.filter(r => r.valid).length;
|
|
376
|
+
const invalid = results.filter(r => !r.valid).length;
|
|
377
|
+
return { total: results.length, valid, invalid, atoms: results };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
case 'lookup_tokens': {
|
|
381
|
+
const group = args?.group;
|
|
382
|
+
if (group) {
|
|
383
|
+
const g = tokens.groups[group];
|
|
384
|
+
if (!g) {
|
|
385
|
+
return {
|
|
386
|
+
found: false,
|
|
387
|
+
message: `Token group "${group}" not found.`,
|
|
388
|
+
available_groups: Object.keys(tokens.groups),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return { found: true, group, ...g };
|
|
392
|
+
}
|
|
393
|
+
// Return all groups with summary
|
|
394
|
+
const summary = {};
|
|
395
|
+
for (const [key, val] of Object.entries(tokens.groups)) {
|
|
396
|
+
summary[key] = { label: val.label };
|
|
397
|
+
if (val.tokens) summary[key].count = val.tokens.length;
|
|
398
|
+
if (val.roles) summary[key].roles = val.roles;
|
|
399
|
+
if (val.levels) summary[key].levels = val.levels;
|
|
400
|
+
if (val.count) summary[key].count = val.count;
|
|
401
|
+
}
|
|
402
|
+
return { groups: summary };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
case 'lookup_icon': {
|
|
406
|
+
const iconName = args?.name;
|
|
407
|
+
const allIcons = icons.essential || [];
|
|
408
|
+
if (!iconName) {
|
|
409
|
+
return { total: allIcons.length, icons: allIcons };
|
|
410
|
+
}
|
|
411
|
+
const exists = allIcons.includes(iconName);
|
|
412
|
+
if (exists) {
|
|
413
|
+
return { found: true, name: iconName, usage: `icon('${iconName}')` };
|
|
414
|
+
}
|
|
415
|
+
const similar = allIcons.filter(i => fuzzyMatch(iconName, i)).slice(0, 10);
|
|
416
|
+
return { found: false, message: `Icon "${iconName}" not found.`, suggestions: similar };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
case 'search_registry': {
|
|
420
|
+
const err = validateStringArg(args, 'query');
|
|
421
|
+
if (err) return { error: err };
|
|
422
|
+
const query = args.query;
|
|
423
|
+
const results = [];
|
|
424
|
+
|
|
425
|
+
// Search components
|
|
426
|
+
for (const name of Object.keys(components.components)) {
|
|
427
|
+
const score = fuzzyScore(query, name);
|
|
428
|
+
if (score > 0) results.push({ type: 'component', name, score });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Search patterns
|
|
432
|
+
for (const [id, entry] of Object.entries(patternsIndex.patterns)) {
|
|
433
|
+
const score = Math.max(fuzzyScore(query, id), fuzzyScore(query, entry.name));
|
|
434
|
+
if (score > 0) results.push({ type: 'pattern', id, name: entry.name, score });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Search archetypes
|
|
438
|
+
for (const [id, entry] of Object.entries(archetypesIndex.archetypes)) {
|
|
439
|
+
const score = Math.max(fuzzyScore(query, id), fuzzyScore(query, entry.name), fuzzyScore(query, entry.description));
|
|
440
|
+
if (score > 0) results.push({ type: 'archetype', id, name: entry.name, score });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Search icons
|
|
444
|
+
const allIcons = icons.essential || [];
|
|
445
|
+
for (const name of allIcons) {
|
|
446
|
+
const score = fuzzyScore(query, name);
|
|
447
|
+
if (score > 0) results.push({ type: 'icon', name, score });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Search skeletons
|
|
451
|
+
for (const [id, skel] of Object.entries(skeletons.skeletons)) {
|
|
452
|
+
const score = Math.max(fuzzyScore(query, id), fuzzyScore(query, skel.name));
|
|
453
|
+
if (score > 0) results.push({ type: 'skeleton', id, name: skel.name, score });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Sort by score descending, limit to 20
|
|
457
|
+
results.sort((a, b) => b.score - a.score);
|
|
458
|
+
return { query, total: results.length, results: results.slice(0, 20) };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
case 'validate_essence': {
|
|
462
|
+
const essencePath = args?.path || join(process.cwd(), 'decantr.essence.json');
|
|
463
|
+
let essence;
|
|
464
|
+
try {
|
|
465
|
+
essence = JSON.parse(await readFile(essencePath, 'utf-8'));
|
|
466
|
+
} catch (e) {
|
|
467
|
+
return { valid: false, errors: [`Could not read essence file at "${essencePath}": ${e.message}`], warnings: [] };
|
|
468
|
+
}
|
|
469
|
+
return validateEssence(essence);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
case 'lookup_skeleton': {
|
|
473
|
+
const skelName = args?.name;
|
|
474
|
+
if (!skelName) {
|
|
475
|
+
const summary = {};
|
|
476
|
+
for (const [id, skel] of Object.entries(skeletons.skeletons)) {
|
|
477
|
+
summary[id] = { name: skel.name, description: skel.description, layout: skel.layout, atoms: skel.atoms };
|
|
478
|
+
}
|
|
479
|
+
return { skeletons: summary };
|
|
480
|
+
}
|
|
481
|
+
const skel = skeletons.skeletons[skelName];
|
|
482
|
+
if (!skel) {
|
|
483
|
+
const similar = Object.keys(skeletons.skeletons).filter(k => fuzzyMatch(skelName, k));
|
|
484
|
+
return { found: false, message: `Skeleton "${skelName}" not found.`, suggestions: similar };
|
|
485
|
+
}
|
|
486
|
+
return { found: true, id: skelName, ...skel };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
default:
|
|
490
|
+
return { error: `Unknown tool: ${name}` };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ─── Server setup ──────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
export async function run() {
|
|
497
|
+
// Load all data before starting
|
|
498
|
+
await Promise.all([loadRegistry(), loadAtomResolver()]);
|
|
499
|
+
|
|
500
|
+
const server = new Server(
|
|
501
|
+
{ name: 'decantr', version: '0.4.0' },
|
|
502
|
+
{ capabilities: { tools: {} } }
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
506
|
+
return { tools: TOOLS };
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
510
|
+
const { name, arguments: args } = request.params;
|
|
511
|
+
try {
|
|
512
|
+
const result = await handleTool(name, args || {});
|
|
513
|
+
const response = {
|
|
514
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
515
|
+
};
|
|
516
|
+
if (result && result.error) {
|
|
517
|
+
response.isError = true;
|
|
518
|
+
}
|
|
519
|
+
return response;
|
|
520
|
+
} catch (err) {
|
|
521
|
+
return {
|
|
522
|
+
content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }],
|
|
523
|
+
isError: true,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const transport = new StdioServerTransport();
|
|
529
|
+
await server.connect(transport);
|
|
530
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decantr migrate — Automated migration for decantr.essence.json
|
|
3
|
+
*
|
|
4
|
+
* Detects the current essence version, applies migrations in semver order,
|
|
5
|
+
* and writes the updated essence back. Supports --dry-run for preview
|
|
6
|
+
* and --target=<version> for migrating to a specific version.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx decantr migrate
|
|
10
|
+
* npx decantr migrate --dry-run
|
|
11
|
+
* npx decantr migrate --target=0.5.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFile, writeFile, copyFile, readdir } from 'node:fs/promises';
|
|
15
|
+
import { join, dirname } from 'node:path';
|
|
16
|
+
import { parseArgs } from 'node:util';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { success, info, heading } from '../art.js';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const MIGRATIONS_DIR = join(__dirname, '..', '..', 'tools', 'migrations');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Compare two semver strings. Returns -1, 0, or 1.
|
|
25
|
+
*/
|
|
26
|
+
function compareSemver(a, b) {
|
|
27
|
+
const pa = a.split('.').map(Number);
|
|
28
|
+
const pb = b.split('.').map(Number);
|
|
29
|
+
for (let i = 0; i < 3; i++) {
|
|
30
|
+
if (pa[i] < pb[i]) return -1;
|
|
31
|
+
if (pa[i] > pb[i]) return 1;
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load all migration modules from tools/migrations/ in semver order.
|
|
38
|
+
* Each module exports { version: string, migrate: (essence) => essence }.
|
|
39
|
+
*/
|
|
40
|
+
async function loadMigrations() {
|
|
41
|
+
let files;
|
|
42
|
+
try {
|
|
43
|
+
files = await readdir(MIGRATIONS_DIR);
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const jsFiles = files
|
|
49
|
+
.filter(f => /^\d+\.\d+\.\d+\.js$/.test(f))
|
|
50
|
+
.sort((a, b) => compareSemver(a.replace('.js', ''), b.replace('.js', '')));
|
|
51
|
+
|
|
52
|
+
const migrations = [];
|
|
53
|
+
for (const file of jsFiles) {
|
|
54
|
+
const mod = await import(join(MIGRATIONS_DIR, file));
|
|
55
|
+
if (mod.version && typeof mod.migrate === 'function') {
|
|
56
|
+
migrations.push({ version: mod.version, migrate: mod.migrate, file });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return migrations;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function run() {
|
|
64
|
+
const { values } = parseArgs({
|
|
65
|
+
options: {
|
|
66
|
+
'dry-run': { type: 'boolean', default: false },
|
|
67
|
+
'target': { type: 'string' },
|
|
68
|
+
},
|
|
69
|
+
strict: false,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const dryRun = values['dry-run'];
|
|
73
|
+
const cwd = process.cwd();
|
|
74
|
+
const essencePath = join(cwd, 'decantr.essence.json');
|
|
75
|
+
|
|
76
|
+
console.log(heading('decantr migrate'));
|
|
77
|
+
|
|
78
|
+
// 1. Read essence
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
raw = await readFile(essencePath, 'utf-8');
|
|
82
|
+
} catch {
|
|
83
|
+
console.error(' \x1b[31m\u2717\x1b[0m decantr.essence.json not found in current directory.');
|
|
84
|
+
console.error(' Run the CLARIFY stage to create your project essence.');
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let essence;
|
|
90
|
+
try {
|
|
91
|
+
essence = JSON.parse(raw);
|
|
92
|
+
} catch {
|
|
93
|
+
console.error(' \x1b[31m\u2717\x1b[0m decantr.essence.json contains invalid JSON.');
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Detect current version (default to 0.4.0 if missing)
|
|
99
|
+
const currentVersion = essence.version || '0.4.0';
|
|
100
|
+
console.log(` ${info(`Current version: ${currentVersion}`)}`);
|
|
101
|
+
|
|
102
|
+
// 3. Load migrations
|
|
103
|
+
const allMigrations = await loadMigrations();
|
|
104
|
+
|
|
105
|
+
if (allMigrations.length === 0) {
|
|
106
|
+
console.log(` ${info('No migration files found.')}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Determine target version (default: latest migration)
|
|
111
|
+
const targetVersion = values.target || allMigrations[allMigrations.length - 1].version;
|
|
112
|
+
console.log(` ${info(`Target version: ${targetVersion}`)}`);
|
|
113
|
+
|
|
114
|
+
// 4. Filter applicable migrations: > current and <= target
|
|
115
|
+
const applicable = allMigrations.filter(m =>
|
|
116
|
+
compareSemver(m.version, currentVersion) > 0 &&
|
|
117
|
+
compareSemver(m.version, targetVersion) <= 0
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (applicable.length === 0) {
|
|
121
|
+
console.log(`\n ${success('Already up to date.')}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(`\n ${info(`${applicable.length} migration(s) to apply:`)}`);
|
|
126
|
+
for (const m of applicable) {
|
|
127
|
+
console.log(` \x1b[2m\u2192\x1b[0m ${m.version} (${m.file})`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 5. Apply migrations in order
|
|
131
|
+
let result = JSON.parse(JSON.stringify(essence)); // deep clone
|
|
132
|
+
for (const m of applicable) {
|
|
133
|
+
result = m.migrate(result);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 6. Dry run: show diff and exit
|
|
137
|
+
if (dryRun) {
|
|
138
|
+
console.log(`\n ${info('Dry run \u2014 no files modified.')}`);
|
|
139
|
+
console.log('\n Changes preview:\n');
|
|
140
|
+
|
|
141
|
+
const before = JSON.stringify(essence, null, 2);
|
|
142
|
+
const after = JSON.stringify(result, null, 2);
|
|
143
|
+
|
|
144
|
+
if (before === after) {
|
|
145
|
+
console.log(' No changes detected.');
|
|
146
|
+
} else {
|
|
147
|
+
// Show a simple key-level diff
|
|
148
|
+
const beforeLines = before.split('\n');
|
|
149
|
+
const afterLines = after.split('\n');
|
|
150
|
+
const maxLines = Math.max(beforeLines.length, afterLines.length);
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < maxLines; i++) {
|
|
153
|
+
const bl = beforeLines[i] ?? '';
|
|
154
|
+
const al = afterLines[i] ?? '';
|
|
155
|
+
if (bl !== al) {
|
|
156
|
+
if (bl) console.log(` \x1b[31m- ${bl.trim()}\x1b[0m`);
|
|
157
|
+
if (al) console.log(` \x1b[32m+ ${al.trim()}\x1b[0m`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 7. Back up and write
|
|
166
|
+
const backupPath = essencePath + '.backup';
|
|
167
|
+
await copyFile(essencePath, backupPath);
|
|
168
|
+
console.log(`\n ${info(`Backup saved to decantr.essence.json.backup`)}`);
|
|
169
|
+
|
|
170
|
+
const output = JSON.stringify(result, null, 2) + '\n';
|
|
171
|
+
await writeFile(essencePath, output, 'utf-8');
|
|
172
|
+
|
|
173
|
+
console.log(` ${success(`Migrated to ${result.version}.`)}`);
|
|
174
|
+
console.log(`\n ${applicable.length} migration(s) applied successfully.\n`);
|
|
175
|
+
}
|