motion-cart 0.1.0 → 0.1.5
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/README.md +33 -2
- package/dist/index.js +553 -257
- package/package.json +2 -2
- package/previews/advanced-interactions__add-to-cart.jpg +0 -0
- package/previews/advanced-interactions__add-to-cart.webm +0 -0
- package/previews/advanced-interactions__context-menu.jpg +0 -0
- package/previews/advanced-interactions__context-menu.webm +0 -0
- package/previews/advanced-interactions__cursor-image-hover.jpg +0 -0
- package/previews/advanced-interactions__cursor-image-hover.webm +0 -0
- package/previews/advanced-interactions__dots-morph-button.jpg +0 -0
- package/previews/advanced-interactions__dots-morph-button.webm +0 -0
- package/previews/advanced-interactions__ios-exposure-slider.jpg +0 -0
- package/previews/advanced-interactions__ios-exposure-slider.webm +0 -0
- package/previews/advanced-interactions__ios-pointer.jpg +0 -0
- package/previews/advanced-interactions__ios-pointer.webm +0 -0
- package/previews/advanced-interactions__magnetic-filings.jpg +0 -0
- package/previews/advanced-interactions__magnetic-filings.webm +0 -0
- package/previews/advanced-interactions__parallax-hover-card.jpg +0 -0
- package/previews/advanced-interactions__parallax-hover-card.webm +0 -0
- package/previews/advanced-interactions__pointer-collision.jpg +0 -0
- package/previews/advanced-interactions__pointer-collision.webm +0 -0
- package/previews/advanced-interactions__scroll-velocity-3d-planes.jpg +0 -0
- package/previews/advanced-interactions__scroll-velocity-3d-planes.webm +0 -0
- package/previews/advanced-interactions__smooth-tabs.jpg +0 -0
- package/previews/advanced-interactions__smooth-tabs.webm +0 -0
- package/previews/advanced-interactions__swipe-actions.jpg +0 -0
- package/previews/advanced-interactions__swipe-actions.webm +0 -0
- package/previews/cursor-effects__click-ripples.jpg +0 -0
- package/previews/cursor-effects__click-ripples.webm +0 -0
- package/previews/cursor-effects__cursor-distortion.jpg +0 -0
- package/previews/cursor-effects__cursor-distortion.webm +0 -0
- package/previews/cursor-effects__cursor-follower.jpg +0 -0
- package/previews/cursor-effects__cursor-follower.webm +0 -0
- package/previews/cursor-effects__cursor-spotlight.jpg +0 -0
- package/previews/cursor-effects__cursor-spotlight.webm +0 -0
- package/previews/cursor-effects__magnetic-elements.jpg +0 -0
- package/previews/cursor-effects__magnetic-elements.webm +0 -0
- package/previews/cursor-effects__particle-trail.jpg +0 -0
- package/previews/cursor-effects__particle-trail.webm +0 -0
- package/previews/data-numbers__activity-feed.jpg +0 -0
- package/previews/data-numbers__activity-feed.webm +0 -0
- package/previews/data-numbers__animated-counter.jpg +0 -0
- package/previews/data-numbers__animated-counter.webm +0 -0
- package/previews/data-numbers__counting-stats-row.jpg +0 -0
- package/previews/data-numbers__counting-stats-row.webm +0 -0
- package/previews/data-numbers__mini-bar-chart.jpg +0 -0
- package/previews/data-numbers__mini-bar-chart.webm +0 -0
- package/previews/data-numbers__revenue-card.jpg +0 -0
- package/previews/data-numbers__revenue-card.webm +0 -0
- package/previews/data-numbers__stats-grid.jpg +0 -0
- package/previews/data-numbers__stats-grid.webm +0 -0
- package/previews/gestures-interactions__hover-cards.jpg +0 -0
- package/previews/gestures-interactions__hover-cards.webm +0 -0
- package/previews/gestures-interactions__magnetic-button.jpg +0 -0
- package/previews/gestures-interactions__magnetic-button.webm +0 -0
- package/previews/gestures-interactions__swipe-to-delete.jpg +0 -0
- package/previews/gestures-interactions__swipe-to-delete.webm +0 -0
- package/previews/layout-navigation__animated-sidebar.jpg +0 -0
- package/previews/layout-navigation__animated-sidebar.webm +0 -0
- package/previews/layout-navigation__animated-tabs.jpg +0 -0
- package/previews/layout-navigation__animated-tabs.webm +0 -0
- package/previews/layout-navigation__expandable-cards.jpg +0 -0
- package/previews/layout-navigation__expandable-cards.webm +0 -0
- package/previews/layout-navigation__page-transitions.jpg +0 -0
- package/previews/layout-navigation__page-transitions.webm +0 -0
- package/previews/layout-navigation__shared-layout-modal.jpg +0 -0
- package/previews/layout-navigation__shared-layout-modal.webm +0 -0
- package/previews/lists-tables__animated-table.jpg +0 -0
- package/previews/lists-tables__animated-table.webm +0 -0
- package/previews/lists-tables__drag-to-reorder.jpg +0 -0
- package/previews/lists-tables__drag-to-reorder.webm +0 -0
- package/previews/lists-tables__staggered-list.jpg +0 -0
- package/previews/lists-tables__staggered-list.webm +0 -0
- package/previews/loading-states__circular-progress.jpg +0 -0
- package/previews/loading-states__circular-progress.webm +0 -0
- package/previews/loading-states__progress-bar.jpg +0 -0
- package/previews/loading-states__progress-bar.webm +0 -0
- package/previews/loading-states__pulsing-dots.jpg +0 -0
- package/previews/loading-states__pulsing-dots.webm +0 -0
- package/previews/loading-states__skeleton-shimmer.jpg +0 -0
- package/previews/loading-states__skeleton-shimmer.webm +0 -0
- package/previews/micro-interactions__animated-accordion.jpg +0 -0
- package/previews/micro-interactions__animated-accordion.webm +0 -0
- package/previews/micro-interactions__animated-badges.jpg +0 -0
- package/previews/micro-interactions__animated-badges.webm +0 -0
- package/previews/micro-interactions__animated-checkboxes.jpg +0 -0
- package/previews/micro-interactions__animated-checkboxes.webm +0 -0
- package/previews/micro-interactions__animated-search.jpg +0 -0
- package/previews/micro-interactions__animated-search.webm +0 -0
- package/previews/micro-interactions__command-palette.jpg +0 -0
- package/previews/micro-interactions__command-palette.webm +0 -0
- package/previews/micro-interactions__confetti-burst.jpg +0 -0
- package/previews/micro-interactions__confetti-burst.webm +0 -0
- package/previews/micro-interactions__floating-action-button.jpg +0 -0
- package/previews/micro-interactions__floating-action-button.webm +0 -0
- package/previews/micro-interactions__otp-input.jpg +0 -0
- package/previews/micro-interactions__otp-input.webm +0 -0
- package/previews/micro-interactions__segmented-control.jpg +0 -0
- package/previews/micro-interactions__segmented-control.webm +0 -0
- package/previews/micro-interactions__toggle-switch.jpg +0 -0
- package/previews/micro-interactions__toggle-switch.webm +0 -0
- package/previews/motion-premium__animate-number.jpg +0 -0
- package/previews/motion-premium__animate-number.webm +0 -0
- package/previews/motion-premium__animate-text.jpg +0 -0
- package/previews/motion-premium__animate-text.webm +0 -0
- package/previews/motion-premium__carousel.jpg +0 -0
- package/previews/motion-premium__carousel.webm +0 -0
- package/previews/motion-premium__coverflow-carousel.jpg +0 -0
- package/previews/motion-premium__coverflow-carousel.webm +0 -0
- package/previews/motion-premium__curtain-transitions.jpg +0 -0
- package/previews/motion-premium__curtain-transitions.webm +0 -0
- package/previews/motion-premium__magnetic-cursor-reticule.jpg +0 -0
- package/previews/motion-premium__magnetic-cursor-reticule.webm +0 -0
- package/previews/motion-premium__magnetic-pull.jpg +0 -0
- package/previews/motion-premium__magnetic-pull.webm +0 -0
- package/previews/motion-premium__pointer-compass.jpg +0 -0
- package/previews/motion-premium__pointer-compass.webm +0 -0
- package/previews/motion-premium__price-toggle.jpg +0 -0
- package/previews/motion-premium__price-toggle.webm +0 -0
- package/previews/motion-premium__scramble-headline.jpg +0 -0
- package/previews/motion-premium__scramble-headline.webm +0 -0
- package/previews/motion-premium__scramble-text.jpg +0 -0
- package/previews/motion-premium__scramble-text.webm +0 -0
- package/previews/motion-premium__square-cursor-reticule.jpg +0 -0
- package/previews/motion-premium__square-cursor-reticule.webm +0 -0
- package/previews/motion-premium__ticker-marquee.jpg +0 -0
- package/previews/motion-premium__ticker-marquee.webm +0 -0
- package/previews/motion-premium__typewriter.jpg +0 -0
- package/previews/motion-premium__typewriter.webm +0 -0
- package/previews/notifications__notification-bell.jpg +0 -0
- package/previews/notifications__notification-bell.webm +0 -0
- package/previews/notifications__notifications-panel.jpg +0 -0
- package/previews/notifications__notifications-panel.webm +0 -0
- package/previews/notifications__toast-stack.jpg +0 -0
- package/previews/notifications__toast-stack.webm +0 -0
- package/previews/preview-manifest.json +1 -0
- package/previews/scroll-animations__scroll-reveal.jpg +0 -0
- package/previews/scroll-animations__scroll-reveal.webm +0 -0
- package/previews/special-unique__3d-card-stack.jpg +0 -0
- package/previews/special-unique__3d-card-stack.webm +0 -0
- package/previews/special-unique__3d-coin-flip.jpg +0 -0
- package/previews/special-unique__3d-coin-flip.webm +0 -0
- package/previews/special-unique__3d-tilt-card.jpg +0 -0
- package/previews/special-unique__3d-tilt-card.webm +0 -0
- package/previews/special-unique__aurora-background.jpg +0 -0
- package/previews/special-unique__aurora-background.webm +0 -0
- package/previews/special-unique__elastic-grid.jpg +0 -0
- package/previews/special-unique__elastic-grid.webm +0 -0
- package/previews/special-unique__elastic-slider.jpg +0 -0
- package/previews/special-unique__elastic-slider.webm +0 -0
- package/previews/special-unique__flip-clock.jpg +0 -0
- package/previews/special-unique__flip-clock.webm +0 -0
- package/previews/special-unique__glitch-text.jpg +0 -0
- package/previews/special-unique__glitch-text.webm +0 -0
- package/previews/special-unique__glowing-orbs.jpg +0 -0
- package/previews/special-unique__glowing-orbs.webm +0 -0
- package/previews/special-unique__gravity-balls.jpg +0 -0
- package/previews/special-unique__gravity-balls.webm +0 -0
- package/previews/special-unique__heartbeat-monitor.jpg +0 -0
- package/previews/special-unique__heartbeat-monitor.webm +0 -0
- package/previews/special-unique__liquid-button.jpg +0 -0
- package/previews/special-unique__liquid-button.webm +0 -0
- package/previews/special-unique__magnetic-dock.jpg +0 -0
- package/previews/special-unique__magnetic-dock.webm +0 -0
- package/previews/special-unique__matrix-rain.jpg +0 -0
- package/previews/special-unique__matrix-rain.webm +0 -0
- package/previews/special-unique__morphing-blob.jpg +0 -0
- package/previews/special-unique__morphing-blob.webm +0 -0
- package/previews/special-unique__morphing-shapes.jpg +0 -0
- package/previews/special-unique__morphing-shapes.webm +0 -0
- package/previews/special-unique__particle-background.jpg +0 -0
- package/previews/special-unique__particle-background.webm +0 -0
- package/previews/special-unique__perspective-grid.jpg +0 -0
- package/previews/special-unique__perspective-grid.webm +0 -0
- package/previews/special-unique__radial-menu.jpg +0 -0
- package/previews/special-unique__radial-menu.webm +0 -0
- package/previews/special-unique__ripple-grid.jpg +0 -0
- package/previews/special-unique__ripple-grid.webm +0 -0
- package/previews/special-unique__signature-draw.jpg +0 -0
- package/previews/special-unique__signature-draw.webm +0 -0
- package/previews/special-unique__split-flap-display.jpg +0 -0
- package/previews/special-unique__split-flap-display.webm +0 -0
- package/previews/special-unique__spotlight-card.jpg +0 -0
- package/previews/special-unique__spotlight-card.webm +0 -0
- package/previews/special-unique__springy-text.jpg +0 -0
- package/previews/special-unique__springy-text.webm +0 -0
- package/previews/special-unique__tech-stack-orbit.jpg +0 -0
- package/previews/special-unique__tech-stack-orbit.webm +0 -0
- package/previews/special-unique__voice-waveform.jpg +0 -0
- package/previews/special-unique__voice-waveform.webm +0 -0
- package/previews/text-animations__gradient-text.jpg +0 -0
- package/previews/text-animations__gradient-text.webm +0 -0
- package/previews/text-animations__typewriter-effect.jpg +0 -0
- package/previews/text-animations__typewriter-effect.webm +0 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import path6 from "path";
|
|
5
5
|
|
|
6
6
|
// ../core/dist/chunk-YCZ2J2MA.js
|
|
7
7
|
var MENU_GROUPS = [
|
|
@@ -649,13 +649,216 @@ function overlayScript() {
|
|
|
649
649
|
}
|
|
650
650
|
|
|
651
651
|
// ../core/dist/index.js
|
|
652
|
-
import {
|
|
653
|
-
import
|
|
652
|
+
import { randomUUID } from "crypto";
|
|
653
|
+
import { promises as fs2 } from "fs";
|
|
654
|
+
import path3 from "path";
|
|
654
655
|
import path2 from "path";
|
|
655
656
|
import { realpathSync } from "fs";
|
|
656
657
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
658
|
+
import { promises as fs } from "fs";
|
|
659
|
+
import path from "path";
|
|
657
660
|
import { execFile } from "child_process";
|
|
658
661
|
import { promisify } from "util";
|
|
662
|
+
function panelHtml(opts) {
|
|
663
|
+
const base = opts.base ?? "";
|
|
664
|
+
return `<!doctype html>
|
|
665
|
+
<html><head><meta charset="utf-8"><title>motion-cart</title>
|
|
666
|
+
<style>
|
|
667
|
+
:root{color-scheme:dark}
|
|
668
|
+
*{box-sizing:border-box}
|
|
669
|
+
body{margin:0;height:100vh;display:flex;flex-direction:column;background:#0a0b0e;color:#e7e7ea;font:13px system-ui}
|
|
670
|
+
header{display:flex;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid #ffffff1a}
|
|
671
|
+
header b{font-size:14px}header small{color:#ffffff80}
|
|
672
|
+
.body{flex:1;display:flex;min-height:0}
|
|
673
|
+
iframe{flex:1;border:0;background:#fff}
|
|
674
|
+
aside{width:360px;border-left:1px solid #ffffff1a;display:flex;flex-direction:column;min-height:0}
|
|
675
|
+
input{width:100%;background:#ffffff0d;border:1px solid #ffffff26;border-radius:8px;color:#fff;padding:9px 11px;outline:none;font-size:13px}
|
|
676
|
+
input:focus{border-color:#22d3ee80}
|
|
677
|
+
.hint{color:#ffffff73;font-size:12px;margin:8px 2px 0}
|
|
678
|
+
.cat{flex:1;overflow:auto;padding:8px 10px}
|
|
679
|
+
.grp{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:#ffffff8c;margin:12px 2px 5px;font-weight:600}
|
|
680
|
+
.row{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-radius:6px;cursor:pointer;color:#ffffffcc;font-size:13px}
|
|
681
|
+
.row:hover{background:#ffffff14;color:#fff}
|
|
682
|
+
.row.in{background:#22d3ee30;color:#a5f3fc;box-shadow:inset 0 0 0 1px #22d3ee66}
|
|
683
|
+
footer{border-top:1px solid #ffffff1a;padding:11px}
|
|
684
|
+
.item{border:1px solid #ffffff1a;border-radius:6px;padding:7px 9px;margin-bottom:6px;background:#ffffff0a}
|
|
685
|
+
.item.pick{border-color:#22d3eecc;background:#22d3ee26}
|
|
686
|
+
.item .top{display:flex;justify-content:space-between;align-items:center;gap:6px}
|
|
687
|
+
.btn{border:1px solid #ffffff33;border-radius:5px;background:#ffffff0d;color:#fff;padding:4px 9px;font-size:12px;cursor:pointer}
|
|
688
|
+
.btn.on{background:#22d3ee;color:#06202a;border-color:#22d3ee}
|
|
689
|
+
.chip{display:inline-flex;gap:5px;align-items:center;background:#00000080;border-radius:4px;padding:2px 7px;font-size:11px;color:#ffffffb3;margin:4px 4px 0 0}
|
|
690
|
+
.empty{border:1px dashed #ffffff26;border-radius:6px;padding:12px;text-align:center;color:#ffffff73;font-size:12px;margin-bottom:8px}
|
|
691
|
+
.row2{display:flex;gap:8px}
|
|
692
|
+
.apply{flex:1;padding:11px;border:0;border-radius:8px;font-weight:600;color:#fff;cursor:pointer;background:linear-gradient(90deg,#06b6d4,#6366f1);font-size:13px}
|
|
693
|
+
.apply:disabled{opacity:.35;cursor:not-allowed}
|
|
694
|
+
.stop{padding:11px 14px;border:1px solid #ffffff33;border-radius:8px;background:#ffffff0d;color:#fff;cursor:pointer}
|
|
695
|
+
.note{text-align:center;color:#ffffff66;font-size:11px;margin-top:7px}
|
|
696
|
+
#savebar{display:flex;gap:6px;margin-top:8px}
|
|
697
|
+
.save{flex:1;padding:8px;border:1px solid #ffffff26;border-radius:7px;background:#ffffff0d;color:#fff;cursor:pointer;font-size:12px}
|
|
698
|
+
.save:hover{background:#ffffff1a}
|
|
699
|
+
.save.danger{border-color:#f8717155;color:#fca5a5}
|
|
700
|
+
.save:disabled{opacity:.4;cursor:not-allowed}
|
|
701
|
+
pre{margin:7px 0 0;max-height:160px;overflow:auto;background:#000000a6;border:1px solid #ffffff26;border-radius:6px;padding:9px;font:12px ui-monospace;color:#ffffffd9;white-space:pre-wrap}
|
|
702
|
+
.x{color:#ffffff73;cursor:pointer}.x:hover{color:#fff}
|
|
703
|
+
#cart{max-height:34vh;overflow-y:auto;margin-bottom:8px}
|
|
704
|
+
.cat,#cart,pre{scrollbar-width:thin;scrollbar-color:#ffffff33 transparent}
|
|
705
|
+
.cat::-webkit-scrollbar,#cart::-webkit-scrollbar,pre::-webkit-scrollbar{width:8px;height:8px}
|
|
706
|
+
.cat::-webkit-scrollbar-thumb,#cart::-webkit-scrollbar-thumb,pre::-webkit-scrollbar-thumb{background:#ffffff26;border-radius:8px;border:2px solid transparent;background-clip:content-box}
|
|
707
|
+
.cat::-webkit-scrollbar-thumb:hover,#cart::-webkit-scrollbar-thumb:hover,pre::-webkit-scrollbar-thumb:hover{background:#ffffff45;background-clip:content-box}
|
|
708
|
+
.cat::-webkit-scrollbar-track,#cart::-webkit-scrollbar-track,pre::-webkit-scrollbar-track{background:transparent}
|
|
709
|
+
#hovercard{position:fixed;z-index:9999;pointer-events:none;width:300px;border:1px solid #ffffff2e;border-radius:10px;overflow:hidden;background:#0c0d11;box-shadow:0 12px 44px #000a;display:none}
|
|
710
|
+
#hovercard .cap{font:10px ui-monospace;text-transform:uppercase;letter-spacing:.06em;color:#ffffff59;padding:5px 9px;border-bottom:1px solid #ffffff14}
|
|
711
|
+
#hovercard video{display:block;width:100%;height:198px;object-fit:contain;background:#0c0d11}
|
|
712
|
+
</style></head>
|
|
713
|
+
<body>
|
|
714
|
+
<header><b>motion-cart</b><small>${opts.framework} · ${opts.target}</small></header>
|
|
715
|
+
<div class="body">
|
|
716
|
+
<iframe id="site" src="${opts.iframeSrc}"></iframe>
|
|
717
|
+
<aside>
|
|
718
|
+
<div style="padding:11px;border-bottom:1px solid #ffffff1a">
|
|
719
|
+
<input id="q" placeholder="Search animations\u2026">
|
|
720
|
+
<div class="hint">Click an animation to add it · then optionally pick the elements to target.</div>
|
|
721
|
+
</div>
|
|
722
|
+
<div class="cat" id="cat"></div>
|
|
723
|
+
<footer><div id="cart"></div>
|
|
724
|
+
<div class="row2">
|
|
725
|
+
<button class="apply" id="apply" disabled>Apply with AI</button>
|
|
726
|
+
<button class="stop" id="stop" style="display:none">Stop</button>
|
|
727
|
+
</div>
|
|
728
|
+
<div class="note">Runs a real Claude agent on your project · ~30s · uses AI credits<span id="sess"></span></div>
|
|
729
|
+
<div id="savebar" style="display:none">
|
|
730
|
+
<button class="save" id="undo">Undo last</button>
|
|
731
|
+
<button class="save" id="merge">Merge</button>
|
|
732
|
+
<button class="save" id="push" style="display:none">Push</button>
|
|
733
|
+
<button class="save danger" id="discard">Discard</button>
|
|
734
|
+
</div>
|
|
735
|
+
<pre id="log" style="display:none"></pre>
|
|
736
|
+
</footer>
|
|
737
|
+
</aside>
|
|
738
|
+
</div>
|
|
739
|
+
<div id="hovercard"><div class="cap">Live preview</div><video id="hovervid" muted loop playsinline preload="none"></video></div>
|
|
740
|
+
<script>
|
|
741
|
+
const MC="motion-cart", BASE=${JSON.stringify(base)}, IFRAME=${JSON.stringify(opts.iframeSrc)}, TOKEN=${JSON.stringify(opts.token)};
|
|
742
|
+
const FRAME_ORIGIN=new URL(IFRAME,location.href).origin;
|
|
743
|
+
let MENU=[],GROUPS=[],cart=[],pickingFor=null,running=false,q="",sessionCost=0,abort=null;
|
|
744
|
+
const site=document.getElementById('site');
|
|
745
|
+
function send(kind){ try{ site.contentWindow.postMessage({source:MC,kind},FRAME_ORIGIN);}catch(e){} }
|
|
746
|
+
window.addEventListener('message',e=>{
|
|
747
|
+
if(e.origin!==FRAME_ORIGIN) return;
|
|
748
|
+
const d=e.data; if(!d||d.source!==MC) return;
|
|
749
|
+
if(d.kind==='picker:ready' && pickingFor) send('picker:enable');
|
|
750
|
+
if(d.kind==='picker:selected' && pickingFor){
|
|
751
|
+
const it=cart.find(c=>c.id===pickingFor); if(!it) return;
|
|
752
|
+
if(!it.targets.some(t=>t.animId===d.target.animId)) it.targets.push(d.target);
|
|
753
|
+
renderCart();
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
const H={'x-mc-token':TOKEN};
|
|
757
|
+
fetch(BASE+'/api/catalogue',{headers:H}).then(r=>r.json()).then(d=>{MENU=d.items;GROUPS=d.groups;renderCat();});
|
|
758
|
+
let PREVIEWS={}, hoverTimer=null, mx=0, my=0;
|
|
759
|
+
fetch(BASE+'/api/previews',{headers:H}).then(r=>r.json()).then(d=>PREVIEWS=d||{});
|
|
760
|
+
const hc=document.getElementById('hovercard'), hv=document.getElementById('hovervid');
|
|
761
|
+
function placeHover(){ if(hc.style.display==='none')return; const w=300,h=224; let l=mx-w-18; if(l<8)l=mx+18; let t=my-h/2; if(t<8)t=8; if(t+h>innerHeight)t=innerHeight-h-8; hc.style.transform='translate('+l+'px,'+t+'px)'; }
|
|
762
|
+
window.addEventListener('pointermove',e=>{mx=e.clientX;my=e.clientY;placeHover();});
|
|
763
|
+
function showPreview(id){ const p=PREVIEWS[id]; if(!p){hidePreview();return;} if(hoverTimer){clearTimeout(hoverTimer);hoverTimer=null;} hv.poster=BASE+'/previews/'+p.poster; hv.src=BASE+'/previews/'+p.clip; hc.style.display='block'; placeHover(); hv.play&&hv.play().catch(()=>{}); }
|
|
764
|
+
function hidePreview(){ if(hoverTimer)clearTimeout(hoverTimer); hoverTimer=setTimeout(()=>{hc.style.display='none'; hv.pause&&hv.pause(); hv.removeAttribute('src');},120); }
|
|
765
|
+
function inCart(id){return cart.some(c=>c.id===id)}
|
|
766
|
+
function toggle(id){ cart=inCart(id)?cart.filter(c=>c.id!==id):cart.concat([{id,targets:[]}]); if(pickingFor===id)setPicking(null); renderCat();renderCart(); }
|
|
767
|
+
function setPicking(id){ pickingFor=id; send(id?'picker:enable':'picker:disable'); renderCart(); }
|
|
768
|
+
function pickFor(id){ if(!inCart(id))cart.push({id,targets:[]}); setPicking(pickingFor===id?null:id); }
|
|
769
|
+
function title(id){const m=MENU.find(x=>x.id===id);return m?m.title:id}
|
|
770
|
+
function renderCat(){
|
|
771
|
+
const cat=document.getElementById('cat');cat.innerHTML='';
|
|
772
|
+
const matched=GROUPS.map(g=>({g,items:MENU.filter(m=>m.group===g && (!q || (m.title+' '+g).toLowerCase().includes(q)))})).filter(x=>x.items.length);
|
|
773
|
+
if(!matched.length){ cat.innerHTML='<p class="empty">No animations match "'+q+'".</p>'; return; }
|
|
774
|
+
matched.forEach(({g,items})=>{
|
|
775
|
+
const h=document.createElement('div');h.className='grp';h.textContent=g;cat.appendChild(h);
|
|
776
|
+
items.forEach(m=>{const r=document.createElement('div');r.className='row'+(inCart(m.id)?' in':'');
|
|
777
|
+
r.innerHTML='<span>'+m.title+'</span><span>'+(inCart(m.id)?'\\u2713':'+')+'</span>';
|
|
778
|
+
r.onclick=()=>toggle(m.id);
|
|
779
|
+
r.onmouseenter=()=>showPreview(m.id); r.onmouseleave=()=>hidePreview();
|
|
780
|
+
cat.appendChild(r);});
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
function renderCart(){
|
|
784
|
+
const c=document.getElementById('cart');c.innerHTML='';
|
|
785
|
+
if(!cart.length){ c.innerHTML='<div class="empty">Your cart is empty. Add animations from the catalogue above.</div>'; }
|
|
786
|
+
cart.forEach(it=>{const d=document.createElement('div');d.className='item'+(pickingFor===it.id?' pick':'');
|
|
787
|
+
const picking=pickingFor===it.id;
|
|
788
|
+
d.innerHTML='<div class="top"><span>'+title(it.id)+'</span><span>'+
|
|
789
|
+
'<button class="btn" data-retry="'+it.id+'">Retry</button> '+
|
|
790
|
+
'<button class="btn '+(picking?'on':'')+'" data-pick="'+it.id+'">'+(picking?'Done picking':('Target'+(it.targets.length?' ('+it.targets.length+')':'')))+'</button> '+
|
|
791
|
+
'<span class="x" data-rm="'+it.id+'">\\u2715</span></span></div>'+
|
|
792
|
+
(it.targets.length?'<div>'+it.targets.map(t=>'<span class="chip">'+t.tag+(t.text?'\\u00b7'+t.text.slice(0,14):'')+'</span>').join('')+'</div>':'');
|
|
793
|
+
c.appendChild(d);});
|
|
794
|
+
c.querySelectorAll('[data-pick]').forEach(b=>b.onclick=()=>pickFor(b.getAttribute('data-pick')));
|
|
795
|
+
c.querySelectorAll('[data-rm]').forEach(b=>b.onclick=()=>toggle(b.getAttribute('data-rm')));
|
|
796
|
+
c.querySelectorAll('[data-retry]').forEach(b=>b.onclick=()=>retryItem(b.getAttribute('data-retry')));
|
|
797
|
+
document.getElementById('apply').disabled=!cart.length||running;
|
|
798
|
+
}
|
|
799
|
+
document.getElementById('q').oninput=e=>{q=e.target.value.trim().toLowerCase();renderCat();};
|
|
800
|
+
document.getElementById('stop').onclick=()=>{ if(abort) abort.abort(); };
|
|
801
|
+
async function runStream(endpoint, body, intro){
|
|
802
|
+
if(running) return; running=true; setPicking(null);
|
|
803
|
+
const log=document.getElementById('log');log.style.display='block';log.textContent=intro+'\\n';
|
|
804
|
+
document.getElementById('apply').disabled=true; document.getElementById('stop').style.display='inline-block';
|
|
805
|
+
const started=Date.now(); abort=new AbortController();
|
|
806
|
+
try{
|
|
807
|
+
const res=await fetch(BASE+endpoint,{method:'POST',headers:{'content-type':'application/json','x-mc-token':TOKEN},body:JSON.stringify(body),signal:abort.signal});
|
|
808
|
+
if(!res.ok||!res.body){ log.textContent+='Error: server returned '+res.status+'\\n'; return; }
|
|
809
|
+
const rd=res.body.getReader();const dec=new TextDecoder();let buf='';
|
|
810
|
+
for(;;){const{value,done}=await rd.read();
|
|
811
|
+
buf+=done?dec.decode():dec.decode(value,{stream:true});
|
|
812
|
+
const fr=buf.split('\\n\\n');buf=done?'':fr.pop();
|
|
813
|
+
for(const f of fr){const l=f.replace(/^data: /,'').trim();if(!l||l[0]===':')continue;let ev;try{ev=JSON.parse(l)}catch{continue}
|
|
814
|
+
if(ev.type==='status')log.textContent+=ev.message+'\\n';
|
|
815
|
+
else if(ev.type==='tool')log.textContent+=(ev.tool==='Read'?'read ':(/Edit|Write/.test(ev.tool)?'edit ':ev.tool.toLowerCase()+' '))+(ev.detail||'')+'\\n';
|
|
816
|
+
else if(ev.type==='assistant')log.textContent+='AI: '+ev.text.slice(0,200)+'\\n';
|
|
817
|
+
else if(ev.type==='done'){log.textContent+='Done'+(ev.turns?' \\u00b7 '+ev.turns+' turns':'')+(ev.cost?' \\u00b7 $'+ev.cost.toFixed(3):'')+'\\n'; if(ev.cost){sessionCost+=ev.cost; document.getElementById('sess').textContent=' \\u00b7 session: $'+sessionCost.toFixed(3);}}
|
|
818
|
+
else if(ev.type==='error')log.textContent+='Error: '+ev.message+'\\n';
|
|
819
|
+
log.scrollTop=log.scrollHeight;}
|
|
820
|
+
if(done)break;
|
|
821
|
+
}
|
|
822
|
+
}catch(err){ log.textContent+=(err&&err.name==='AbortError'?'Stopped':'Error: '+err)+'\\n'; }
|
|
823
|
+
finally{
|
|
824
|
+
running=false;abort=null;
|
|
825
|
+
document.getElementById('apply').disabled=false; document.getElementById('stop').style.display='none';
|
|
826
|
+
const secs=Math.round((Date.now()-started)/1000); log.textContent+='('+secs+'s)\\n';
|
|
827
|
+
setTimeout(()=>{site.src=site.src; refreshStatus();},400);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
document.getElementById('apply').onclick=()=>{ if(cart.length) runStream('/api/apply',{items:cart},'Sending cart to the agent\u2026'); };
|
|
831
|
+
function retryItem(id){
|
|
832
|
+
const it=cart.find(c=>c.id===id); if(!it) return;
|
|
833
|
+
const note=window.prompt('Retry "'+title(id)+'" \u2014 how should it be different? (optional, e.g. "subtler", "slower", "different effect")');
|
|
834
|
+
if(note===null) return;
|
|
835
|
+
runStream('/api/retry',{items:[it],note},'Retrying "'+title(id)+'"'+(note?' \u2014 '+note:'')+'\u2026');
|
|
836
|
+
}
|
|
837
|
+
async function refreshStatus(){
|
|
838
|
+
try{
|
|
839
|
+
const s=await (await fetch(BASE+'/api/status',{headers:H})).json();
|
|
840
|
+
document.getElementById('savebar').style.display=(s.active && s.checkpoints>0)?'flex':'none';
|
|
841
|
+
document.getElementById('merge').textContent='Merge \u2192 '+s.base;
|
|
842
|
+
document.getElementById('push').style.display=s.remote?'block':'none';
|
|
843
|
+
document.getElementById('undo').disabled=s.checkpoints<1;
|
|
844
|
+
}catch(e){}
|
|
845
|
+
}
|
|
846
|
+
async function gitAction(endpoint, confirmMsg){
|
|
847
|
+
if(confirmMsg && !window.confirm(confirmMsg)) return;
|
|
848
|
+
const log=document.getElementById('log');log.style.display='block';
|
|
849
|
+
try{ const r=await fetch(BASE+endpoint,{method:'POST',headers:H}); const d=await r.json();
|
|
850
|
+
log.textContent+=(d.ok?'\\u2713 '+endpoint.replace('/api/','')+(d.merged?' '+d.merged:'')+(d.branch?' '+d.branch:''):'Error: '+(d.error||r.status))+'\\n';
|
|
851
|
+
}catch(e){ log.textContent+='Error: '+e+'\\n'; }
|
|
852
|
+
setTimeout(()=>{site.src=site.src; refreshStatus();},400);
|
|
853
|
+
}
|
|
854
|
+
document.getElementById('undo').onclick=()=>gitAction('/api/undo');
|
|
855
|
+
document.getElementById('merge').onclick=()=>gitAction('/api/merge','Merge this session into your base branch?');
|
|
856
|
+
document.getElementById('push').onclick=()=>gitAction('/api/push');
|
|
857
|
+
document.getElementById('discard').onclick=()=>gitAction('/api/discard','Discard the whole session (delete the branch)?');
|
|
858
|
+
refreshStatus();
|
|
859
|
+
</script>
|
|
860
|
+
</body></html>`;
|
|
861
|
+
}
|
|
659
862
|
async function detectFramework(targetDir) {
|
|
660
863
|
let pkg = {};
|
|
661
864
|
try {
|
|
@@ -681,7 +884,7 @@ function frameworkGuidance(fw) {
|
|
|
681
884
|
return "Vanilla JS/DOM. Use the `motion` package's `animate()` / `scroll()` / `inView()` functions directly on elements; no JSX. Adapt React reference techniques to imperative DOM calls.";
|
|
682
885
|
}
|
|
683
886
|
}
|
|
684
|
-
function composeInstruction(framework, items) {
|
|
887
|
+
function composeInstruction(framework, items, note) {
|
|
685
888
|
const lines = items.flatMap((ci) => {
|
|
686
889
|
const it = MENU_BY_ID[ci.id];
|
|
687
890
|
if (!it) return [];
|
|
@@ -706,6 +909,7 @@ function composeInstruction(framework, items) {
|
|
|
706
909
|
`- Keep it performant (animate transform/opacity/filter; avoid layout thrash; respect prefers-reduced-motion).`,
|
|
707
910
|
`- If the Motion library isn't installed yet, note it clearly in your summary (do not run package installs).`,
|
|
708
911
|
`- The project must keep compiling.`,
|
|
912
|
+
...note ? [``, `Adjustment requested for this pass: ${note}`] : [],
|
|
709
913
|
``,
|
|
710
914
|
`Explain briefly what you changed.`
|
|
711
915
|
].join("\n");
|
|
@@ -735,8 +939,15 @@ function inside(root, abs) {
|
|
|
735
939
|
}
|
|
736
940
|
return abs === realRoot || abs.startsWith(realRoot + path2.sep);
|
|
737
941
|
}
|
|
942
|
+
function authHint(message) {
|
|
943
|
+
if (/401|authenticat|bearer|api key|credential|unauthor/i.test(message)) {
|
|
944
|
+
return `${message}
|
|
945
|
+
\u2192 Auth: pass --api-key/--base-url (e.g. iO Bonzai), log into Claude Code, or set ANTHROPIC_API_KEY. Note: an invalid env key overrides your Claude login.`;
|
|
946
|
+
}
|
|
947
|
+
return message;
|
|
948
|
+
}
|
|
738
949
|
async function* runAnimationAgent(opts) {
|
|
739
|
-
const { root, framework, items, motionToken } = opts;
|
|
950
|
+
const { root, framework, items, motionToken, baseUrl, apiKey, model, note } = opts;
|
|
740
951
|
const guard = async (tool, input) => {
|
|
741
952
|
if (BLOCKED_TOOLS.has(tool)) return { behavior: "deny", message: `${tool} is disabled.` };
|
|
742
953
|
if (WRITE_TOOLS.has(tool)) {
|
|
@@ -764,12 +975,18 @@ async function* runAnimationAgent(opts) {
|
|
|
764
975
|
env: { TOKEN: motionToken }
|
|
765
976
|
}
|
|
766
977
|
} : void 0;
|
|
978
|
+
const agentEnv = baseUrl || apiKey ? {
|
|
979
|
+
...process.env,
|
|
980
|
+
...baseUrl ? { ANTHROPIC_BASE_URL: baseUrl } : {},
|
|
981
|
+
...apiKey ? { ANTHROPIC_AUTH_TOKEN: apiKey, ANTHROPIC_API_KEY: apiKey } : {}
|
|
982
|
+
} : void 0;
|
|
983
|
+
const providerNote = baseUrl ? ` via ${new URL(baseUrl).host}` : "";
|
|
767
984
|
yield {
|
|
768
985
|
type: "status",
|
|
769
|
-
message: `Agent starting on a ${framework} project with ${items.length} animation(s)\u2026 ${mcpServers ? "(Motion AI Kit MCP enabled)" : "(bundled Motion guidance)"}`
|
|
986
|
+
message: `Agent starting on a ${framework} project with ${items.length} animation(s)${providerNote}\u2026 ${mcpServers ? "(Motion AI Kit MCP enabled)" : "(bundled Motion guidance)"}`
|
|
770
987
|
};
|
|
771
988
|
const response = query({
|
|
772
|
-
prompt: composeInstruction(framework, items),
|
|
989
|
+
prompt: composeInstruction(framework, items, note),
|
|
773
990
|
options: {
|
|
774
991
|
cwd: root,
|
|
775
992
|
permissionMode: "default",
|
|
@@ -777,6 +994,8 @@ async function* runAnimationAgent(opts) {
|
|
|
777
994
|
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep"],
|
|
778
995
|
disallowedTools: [...BLOCKED_TOOLS],
|
|
779
996
|
...mcpServers ? { mcpServers } : {},
|
|
997
|
+
...agentEnv ? { env: agentEnv } : {},
|
|
998
|
+
...model ? { model } : {},
|
|
780
999
|
settingSources: [],
|
|
781
1000
|
maxTurns: 80
|
|
782
1001
|
}
|
|
@@ -799,12 +1018,12 @@ async function* runAnimationAgent(opts) {
|
|
|
799
1018
|
if (message.subtype === "success") {
|
|
800
1019
|
yield { type: "done", summary: message.result, cost: message.total_cost_usd, turns: message.num_turns, ms: message.duration_ms };
|
|
801
1020
|
} else {
|
|
802
|
-
yield { type: "error", message: `Agent ended: ${message.subtype}` };
|
|
1021
|
+
yield { type: "error", message: authHint(`Agent ended: ${message.subtype}`) };
|
|
803
1022
|
}
|
|
804
1023
|
}
|
|
805
1024
|
}
|
|
806
1025
|
} catch (err) {
|
|
807
|
-
yield { type: "error", message: err instanceof Error ? err.message : String(err) };
|
|
1026
|
+
yield { type: "error", message: authHint(err instanceof Error ? err.message : String(err)) };
|
|
808
1027
|
}
|
|
809
1028
|
}
|
|
810
1029
|
var run = promisify(execFile);
|
|
@@ -849,6 +1068,247 @@ async function diffStat(dir) {
|
|
|
849
1068
|
return "";
|
|
850
1069
|
}
|
|
851
1070
|
}
|
|
1071
|
+
async function headSha(dir) {
|
|
1072
|
+
return git(dir, ["rev-parse", "--short", "HEAD"]);
|
|
1073
|
+
}
|
|
1074
|
+
async function getRemote(dir) {
|
|
1075
|
+
try {
|
|
1076
|
+
const out = await git(dir, ["remote"]);
|
|
1077
|
+
const first = out.split("\n").map((s) => s.trim()).filter(Boolean)[0];
|
|
1078
|
+
return first ?? null;
|
|
1079
|
+
} catch {
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async function pushBranch(dir, branch, remote = "origin") {
|
|
1084
|
+
await git(dir, ["push", "-u", remote, branch]);
|
|
1085
|
+
}
|
|
1086
|
+
async function resetHard(dir, ref) {
|
|
1087
|
+
await git(dir, ["reset", "--hard", ref]);
|
|
1088
|
+
}
|
|
1089
|
+
async function checkoutBranch(dir, branch) {
|
|
1090
|
+
await git(dir, ["checkout", branch]);
|
|
1091
|
+
}
|
|
1092
|
+
async function deleteBranch(dir, branch) {
|
|
1093
|
+
await git(dir, ["branch", "-D", branch]);
|
|
1094
|
+
}
|
|
1095
|
+
async function mergeToBase(dir, base, session) {
|
|
1096
|
+
await git(dir, ["checkout", base]);
|
|
1097
|
+
await git(dir, ["merge", "--no-ff", "--no-edit", session]);
|
|
1098
|
+
}
|
|
1099
|
+
var MAX_BODY = 256 * 1024;
|
|
1100
|
+
function readBody(req) {
|
|
1101
|
+
return new Promise((resolve, reject) => {
|
|
1102
|
+
let b = "";
|
|
1103
|
+
let size = 0;
|
|
1104
|
+
req.on("data", (c) => {
|
|
1105
|
+
size += c.length;
|
|
1106
|
+
if (size > MAX_BODY) {
|
|
1107
|
+
reject(new Error("body too large"));
|
|
1108
|
+
req.destroy();
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
b += c;
|
|
1112
|
+
});
|
|
1113
|
+
req.on("end", () => resolve(b));
|
|
1114
|
+
req.on("error", reject);
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
function createNodeDevApi(cfg) {
|
|
1118
|
+
const base = cfg.base ?? "";
|
|
1119
|
+
const token = randomUUID();
|
|
1120
|
+
let sessionBranch = null;
|
|
1121
|
+
let baseBranch = "HEAD";
|
|
1122
|
+
let baseSha = "";
|
|
1123
|
+
const checkpoints = [];
|
|
1124
|
+
async function ensureSession() {
|
|
1125
|
+
if (sessionBranch) return;
|
|
1126
|
+
if (!await isGitRepo(cfg.targetDir)) throw new Error("target is not a git repository");
|
|
1127
|
+
if (!await isClean(cfg.targetDir))
|
|
1128
|
+
throw new Error("target git tree has uncommitted changes \u2014 commit or stash first");
|
|
1129
|
+
baseBranch = await currentBranch(cfg.targetDir);
|
|
1130
|
+
sessionBranch = `motion-cart/session-${Date.now()}`;
|
|
1131
|
+
await checkoutNewBranch(cfg.targetDir, sessionBranch);
|
|
1132
|
+
baseSha = await headSha(cfg.targetDir);
|
|
1133
|
+
checkpoints.length = 0;
|
|
1134
|
+
}
|
|
1135
|
+
const json = (res, code, body) => {
|
|
1136
|
+
res.writeHead(code, { "content-type": "application/json" });
|
|
1137
|
+
res.end(JSON.stringify(body));
|
|
1138
|
+
};
|
|
1139
|
+
const authed = (req) => req.headers["x-mc-token"] === token;
|
|
1140
|
+
async function runAgentSSE(res, items, note) {
|
|
1141
|
+
res.writeHead(200, {
|
|
1142
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
1143
|
+
"cache-control": "no-cache, no-transform",
|
|
1144
|
+
connection: "keep-alive"
|
|
1145
|
+
});
|
|
1146
|
+
const send = (e) => res.write(`data: ${JSON.stringify(e)}
|
|
1147
|
+
|
|
1148
|
+
`);
|
|
1149
|
+
const heartbeat = setInterval(() => res.write(`: ping
|
|
1150
|
+
|
|
1151
|
+
`), 15e3);
|
|
1152
|
+
try {
|
|
1153
|
+
if (!items.length) throw new Error("cart is empty");
|
|
1154
|
+
const fresh = !sessionBranch;
|
|
1155
|
+
await ensureSession();
|
|
1156
|
+
if (fresh)
|
|
1157
|
+
send({
|
|
1158
|
+
type: "status",
|
|
1159
|
+
message: `Editing on branch ${sessionBranch} (branched from ${baseBranch}; your ${baseBranch} branch is untouched)`
|
|
1160
|
+
});
|
|
1161
|
+
for await (const ev of runAnimationAgent({
|
|
1162
|
+
root: cfg.targetDir,
|
|
1163
|
+
framework: cfg.framework,
|
|
1164
|
+
items,
|
|
1165
|
+
note,
|
|
1166
|
+
motionToken: cfg.token,
|
|
1167
|
+
baseUrl: cfg.baseUrl,
|
|
1168
|
+
apiKey: cfg.apiKey,
|
|
1169
|
+
model: cfg.model
|
|
1170
|
+
}))
|
|
1171
|
+
send(ev);
|
|
1172
|
+
const stat = await diffStat(cfg.targetDir);
|
|
1173
|
+
const committed = await commitAll(
|
|
1174
|
+
cfg.targetDir,
|
|
1175
|
+
note ? `motion-cart: retry \u2014 ${note.slice(0, 60)}` : `motion-cart: applied ${items.length} animation(s)`
|
|
1176
|
+
);
|
|
1177
|
+
if (committed) {
|
|
1178
|
+
checkpoints.push(await headSha(cfg.targetDir));
|
|
1179
|
+
if (stat) send({ type: "status", message: `Committed (checkpoint ${checkpoints.length}):
|
|
1180
|
+
${stat}` });
|
|
1181
|
+
}
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
send({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
1184
|
+
} finally {
|
|
1185
|
+
clearInterval(heartbeat);
|
|
1186
|
+
send({ type: "end" });
|
|
1187
|
+
res.end();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async function servedPreview(res, file0) {
|
|
1191
|
+
if (!cfg.previewsDir) return false;
|
|
1192
|
+
const file = path3.basename(decodeURIComponent(file0));
|
|
1193
|
+
if (file === "preview-manifest.json") {
|
|
1194
|
+
try {
|
|
1195
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1196
|
+
res.end(await fs2.readFile(path3.join(cfg.previewsDir, file)));
|
|
1197
|
+
} catch {
|
|
1198
|
+
json(res, 200, {});
|
|
1199
|
+
}
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
if (!/^[\w.-]+\.(webm|jpg)$/.test(file)) {
|
|
1203
|
+
res.writeHead(404).end();
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
try {
|
|
1207
|
+
const buf = await fs2.readFile(path3.join(cfg.previewsDir, file));
|
|
1208
|
+
res.writeHead(200, {
|
|
1209
|
+
"content-type": file.endsWith(".webm") ? "video/webm" : "image/jpeg",
|
|
1210
|
+
"cache-control": "public, max-age=3600"
|
|
1211
|
+
});
|
|
1212
|
+
res.end(buf);
|
|
1213
|
+
} catch {
|
|
1214
|
+
res.writeHead(404).end();
|
|
1215
|
+
}
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
async function handle(req, res) {
|
|
1219
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
1220
|
+
let p = url.pathname;
|
|
1221
|
+
if (base && p.startsWith(base)) p = p.slice(base.length) || "/";
|
|
1222
|
+
else if (base) return false;
|
|
1223
|
+
if (req.method === "GET" && p === "/") {
|
|
1224
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1225
|
+
res.end(panelHtml({ base, iframeSrc: cfg.iframeSrc, target: cfg.targetDir, framework: cfg.framework, token }));
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1228
|
+
if (p.startsWith("/previews/")) return servedPreview(res, p.slice("/previews/".length));
|
|
1229
|
+
if (!p.startsWith("/api/")) return false;
|
|
1230
|
+
if (!authed(req)) {
|
|
1231
|
+
json(res, 403, { error: "forbidden" });
|
|
1232
|
+
return true;
|
|
1233
|
+
}
|
|
1234
|
+
if (req.method === "GET" && p === "/api/catalogue") {
|
|
1235
|
+
json(res, 200, { groups: MENU_GROUPS, items: MENU });
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
if (req.method === "GET" && p === "/api/previews") return servedPreview(res, "preview-manifest.json");
|
|
1239
|
+
if (req.method === "GET" && p === "/api/status") {
|
|
1240
|
+
const remote = sessionBranch ? await getRemote(cfg.targetDir) : null;
|
|
1241
|
+
json(res, 200, { base: baseBranch, session: sessionBranch, checkpoints: checkpoints.length, remote, active: !!sessionBranch });
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
if (req.method === "POST" && (p === "/api/apply" || p === "/api/retry")) {
|
|
1245
|
+
let items = [];
|
|
1246
|
+
let note;
|
|
1247
|
+
try {
|
|
1248
|
+
const body = JSON.parse(await readBody(req));
|
|
1249
|
+
items = body.items ?? [];
|
|
1250
|
+
if (p === "/api/retry" && typeof body.note === "string" && body.note.trim()) note = body.note.trim();
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1253
|
+
await runAgentSSE(res, items, note);
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
if (req.method === "POST" && p === "/api/undo") {
|
|
1257
|
+
if (!sessionBranch || checkpoints.length === 0) return json(res, 400, { error: "nothing to undo" }), true;
|
|
1258
|
+
const target = checkpoints.length >= 2 ? checkpoints[checkpoints.length - 2] : baseSha;
|
|
1259
|
+
try {
|
|
1260
|
+
await resetHard(cfg.targetDir, target);
|
|
1261
|
+
checkpoints.pop();
|
|
1262
|
+
json(res, 200, { ok: true, checkpoints: checkpoints.length });
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
1265
|
+
}
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
if (req.method === "POST" && p === "/api/push") {
|
|
1269
|
+
if (!sessionBranch) return json(res, 400, { error: "no active session" }), true;
|
|
1270
|
+
const remote = await getRemote(cfg.targetDir);
|
|
1271
|
+
if (!remote) return json(res, 400, { error: "no git remote configured" }), true;
|
|
1272
|
+
try {
|
|
1273
|
+
await pushBranch(cfg.targetDir, sessionBranch, remote);
|
|
1274
|
+
json(res, 200, { ok: true, remote, branch: sessionBranch });
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
1277
|
+
}
|
|
1278
|
+
return true;
|
|
1279
|
+
}
|
|
1280
|
+
if (req.method === "POST" && p === "/api/merge") {
|
|
1281
|
+
if (!sessionBranch) return json(res, 400, { error: "no active session" }), true;
|
|
1282
|
+
const merged = sessionBranch;
|
|
1283
|
+
try {
|
|
1284
|
+
await mergeToBase(cfg.targetDir, baseBranch, sessionBranch);
|
|
1285
|
+
sessionBranch = null;
|
|
1286
|
+
checkpoints.length = 0;
|
|
1287
|
+
json(res, 200, { ok: true, merged, base: baseBranch });
|
|
1288
|
+
} catch (err) {
|
|
1289
|
+
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
1290
|
+
}
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1293
|
+
if (req.method === "POST" && p === "/api/discard") {
|
|
1294
|
+
if (!sessionBranch) return json(res, 400, { error: "no active session" }), true;
|
|
1295
|
+
const branch = sessionBranch;
|
|
1296
|
+
try {
|
|
1297
|
+
await checkoutBranch(cfg.targetDir, baseBranch);
|
|
1298
|
+
await deleteBranch(cfg.targetDir, branch);
|
|
1299
|
+
sessionBranch = null;
|
|
1300
|
+
checkpoints.length = 0;
|
|
1301
|
+
json(res, 200, { ok: true, discarded: branch });
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
1304
|
+
}
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
json(res, 404, { error: "not found" });
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
return { token, handle };
|
|
1311
|
+
}
|
|
852
1312
|
|
|
853
1313
|
// src/proxy.ts
|
|
854
1314
|
import http from "http";
|
|
@@ -950,261 +1410,71 @@ function startProxy(devUrl, port2, panelOrigin) {
|
|
|
950
1410
|
|
|
951
1411
|
// src/server.ts
|
|
952
1412
|
import http2 from "http";
|
|
953
|
-
import
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
function panelHtml(opts) {
|
|
957
|
-
return `<!doctype html>
|
|
958
|
-
<html><head><meta charset="utf-8"><title>motion-cart</title>
|
|
959
|
-
<style>
|
|
960
|
-
:root{color-scheme:dark}
|
|
961
|
-
*{box-sizing:border-box}
|
|
962
|
-
body{margin:0;height:100vh;display:flex;flex-direction:column;background:#0a0b0e;color:#e7e7ea;font:13px system-ui}
|
|
963
|
-
header{display:flex;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid #ffffff1a}
|
|
964
|
-
header b{font-size:14px}header small{color:#ffffff80}
|
|
965
|
-
.body{flex:1;display:flex;min-height:0}
|
|
966
|
-
iframe{flex:1;border:0;background:#fff}
|
|
967
|
-
aside{width:360px;border-left:1px solid #ffffff1a;display:flex;flex-direction:column;min-height:0}
|
|
968
|
-
input{width:100%;background:#ffffff0d;border:1px solid #ffffff26;border-radius:8px;color:#fff;padding:9px 11px;outline:none;font-size:13px}
|
|
969
|
-
input:focus{border-color:#22d3ee80}
|
|
970
|
-
.hint{color:#ffffff73;font-size:12px;margin:8px 2px 0}
|
|
971
|
-
.cat{flex:1;overflow:auto;padding:8px 10px}
|
|
972
|
-
.grp{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:#ffffff8c;margin:12px 2px 5px;font-weight:600}
|
|
973
|
-
.row{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-radius:6px;cursor:pointer;color:#ffffffcc;font-size:13px}
|
|
974
|
-
.row:hover{background:#ffffff14;color:#fff}
|
|
975
|
-
.row.in{background:#22d3ee30;color:#a5f3fc;box-shadow:inset 0 0 0 1px #22d3ee66}
|
|
976
|
-
footer{border-top:1px solid #ffffff1a;padding:11px}
|
|
977
|
-
.item{border:1px solid #ffffff1a;border-radius:6px;padding:7px 9px;margin-bottom:6px;background:#ffffff0a}
|
|
978
|
-
.item.pick{border-color:#22d3eecc;background:#22d3ee26}
|
|
979
|
-
.item .top{display:flex;justify-content:space-between;align-items:center;gap:6px}
|
|
980
|
-
.btn{border:1px solid #ffffff33;border-radius:5px;background:#ffffff0d;color:#fff;padding:4px 9px;font-size:12px;cursor:pointer}
|
|
981
|
-
.btn.on{background:#22d3ee;color:#06202a;border-color:#22d3ee}
|
|
982
|
-
.chip{display:inline-flex;gap:5px;align-items:center;background:#00000080;border-radius:4px;padding:2px 7px;font-size:11px;color:#ffffffb3;margin:4px 4px 0 0}
|
|
983
|
-
.empty{border:1px dashed #ffffff26;border-radius:6px;padding:12px;text-align:center;color:#ffffff73;font-size:12px;margin-bottom:8px}
|
|
984
|
-
.row2{display:flex;gap:8px}
|
|
985
|
-
.apply{flex:1;padding:11px;border:0;border-radius:8px;font-weight:600;color:#fff;cursor:pointer;background:linear-gradient(90deg,#06b6d4,#6366f1);font-size:13px}
|
|
986
|
-
.apply:disabled{opacity:.35;cursor:not-allowed}
|
|
987
|
-
.stop{padding:11px 14px;border:1px solid #ffffff33;border-radius:8px;background:#ffffff0d;color:#fff;cursor:pointer}
|
|
988
|
-
.note{text-align:center;color:#ffffff66;font-size:11px;margin-top:7px}
|
|
989
|
-
pre{margin:7px 0 0;max-height:200px;overflow:auto;background:#000000a6;border:1px solid #ffffff26;border-radius:6px;padding:9px;font:12px ui-monospace;color:#ffffffd9;white-space:pre-wrap}
|
|
990
|
-
.x{color:#ffffff73;cursor:pointer}.x:hover{color:#fff}
|
|
991
|
-
</style></head>
|
|
992
|
-
<body>
|
|
993
|
-
<header><b>motion-cart</b><small>${opts.framework} · ${opts.target}</small></header>
|
|
994
|
-
<div class="body">
|
|
995
|
-
<iframe id="site" src="${opts.proxyUrl}"></iframe>
|
|
996
|
-
<aside>
|
|
997
|
-
<div style="padding:11px;border-bottom:1px solid #ffffff1a">
|
|
998
|
-
<input id="q" placeholder="Search animations\u2026">
|
|
999
|
-
<div class="hint">Click an animation to add it · then optionally pick the elements to target.</div>
|
|
1000
|
-
</div>
|
|
1001
|
-
<div class="cat" id="cat"></div>
|
|
1002
|
-
<footer><div id="cart"></div>
|
|
1003
|
-
<div class="row2">
|
|
1004
|
-
<button class="apply" id="apply" disabled>Apply with AI</button>
|
|
1005
|
-
<button class="stop" id="stop" style="display:none">Stop</button>
|
|
1006
|
-
</div>
|
|
1007
|
-
<div class="note">Runs a real Claude agent on your project · ~30s · uses AI credits<span id="sess"></span></div>
|
|
1008
|
-
<pre id="log" style="display:none"></pre>
|
|
1009
|
-
</footer>
|
|
1010
|
-
</aside>
|
|
1011
|
-
</div>
|
|
1012
|
-
<script>
|
|
1013
|
-
const MC="motion-cart", PROXY=${JSON.stringify(opts.proxyUrl)}, TOKEN=${JSON.stringify(opts.token)};
|
|
1014
|
-
let MENU=[],GROUPS=[],cart=[],pickingFor=null,running=false,q="",sessionCost=0,abort=null;
|
|
1015
|
-
const site=document.getElementById('site');
|
|
1016
|
-
function send(kind){ try{ site.contentWindow.postMessage({source:MC,kind},PROXY);}catch(e){} }
|
|
1017
|
-
window.addEventListener('message',e=>{
|
|
1018
|
-
if(e.origin!==PROXY) return;
|
|
1019
|
-
const d=e.data; if(!d||d.source!==MC) return;
|
|
1020
|
-
if(d.kind==='picker:ready' && pickingFor) send('picker:enable');
|
|
1021
|
-
if(d.kind==='picker:selected' && pickingFor){
|
|
1022
|
-
const it=cart.find(c=>c.id===pickingFor); if(!it) return;
|
|
1023
|
-
if(!it.targets.some(t=>t.animId===d.target.animId)) it.targets.push(d.target);
|
|
1024
|
-
renderCart();
|
|
1025
|
-
}
|
|
1026
|
-
});
|
|
1027
|
-
fetch('/api/catalogue',{headers:{'x-mc-token':TOKEN}}).then(r=>r.json()).then(d=>{MENU=d.items;GROUPS=d.groups;renderCat();});
|
|
1028
|
-
function inCart(id){return cart.some(c=>c.id===id)}
|
|
1029
|
-
function toggle(id){ cart=inCart(id)?cart.filter(c=>c.id!==id):cart.concat([{id,targets:[]}]); if(pickingFor===id)setPicking(null); renderCat();renderCart(); }
|
|
1030
|
-
function setPicking(id){ pickingFor=id; send(id?'picker:enable':'picker:disable'); renderCart(); }
|
|
1031
|
-
function pickFor(id){ if(!inCart(id))cart.push({id,targets:[]}); setPicking(pickingFor===id?null:id); }
|
|
1032
|
-
function title(id){const m=MENU.find(x=>x.id===id);return m?m.title:id}
|
|
1033
|
-
function renderCat(){
|
|
1034
|
-
const cat=document.getElementById('cat');cat.innerHTML='';
|
|
1035
|
-
const matched=GROUPS.map(g=>({g,items:MENU.filter(m=>m.group===g && (!q || (m.title+' '+g).toLowerCase().includes(q)))})).filter(x=>x.items.length);
|
|
1036
|
-
if(!matched.length){ cat.innerHTML='<p class="empty">No animations match "'+q+'".</p>'; return; }
|
|
1037
|
-
matched.forEach(({g,items})=>{
|
|
1038
|
-
const h=document.createElement('div');h.className='grp';h.textContent=g;cat.appendChild(h);
|
|
1039
|
-
items.forEach(m=>{const r=document.createElement('div');r.className='row'+(inCart(m.id)?' in':'');
|
|
1040
|
-
r.innerHTML='<span>'+m.title+'</span><span>'+(inCart(m.id)?'\\u2713':'+')+'</span>';
|
|
1041
|
-
r.onclick=()=>toggle(m.id);cat.appendChild(r);});
|
|
1042
|
-
});
|
|
1043
|
-
}
|
|
1044
|
-
function renderCart(){
|
|
1045
|
-
const c=document.getElementById('cart');c.innerHTML='';
|
|
1046
|
-
if(!cart.length){ c.innerHTML='<div class="empty">Your cart is empty. Add animations from the catalogue above.</div>'; }
|
|
1047
|
-
cart.forEach(it=>{const d=document.createElement('div');d.className='item'+(pickingFor===it.id?' pick':'');
|
|
1048
|
-
const picking=pickingFor===it.id;
|
|
1049
|
-
d.innerHTML='<div class="top"><span>'+title(it.id)+'</span><span>'+
|
|
1050
|
-
'<button class="btn '+(picking?'on':'')+'" data-pick="'+it.id+'">'+(picking?'Done picking':('Target'+(it.targets.length?' ('+it.targets.length+')':'')))+'</button> '+
|
|
1051
|
-
'<span class="x" data-rm="'+it.id+'">\\u2715</span></span></div>'+
|
|
1052
|
-
(it.targets.length?'<div>'+it.targets.map(t=>'<span class="chip">'+t.tag+(t.text?'\\u00b7'+t.text.slice(0,14):'')+'</span>').join('')+'</div>':'');
|
|
1053
|
-
c.appendChild(d);});
|
|
1054
|
-
c.querySelectorAll('[data-pick]').forEach(b=>b.onclick=()=>pickFor(b.getAttribute('data-pick')));
|
|
1055
|
-
c.querySelectorAll('[data-rm]').forEach(b=>b.onclick=()=>toggle(b.getAttribute('data-rm')));
|
|
1056
|
-
document.getElementById('apply').disabled=!cart.length||running;
|
|
1057
|
-
}
|
|
1058
|
-
document.getElementById('q').oninput=e=>{q=e.target.value.trim().toLowerCase();renderCat();};
|
|
1059
|
-
document.getElementById('stop').onclick=()=>{ if(abort) abort.abort(); };
|
|
1060
|
-
document.getElementById('apply').onclick=async()=>{
|
|
1061
|
-
if(!cart.length||running)return; running=true; setPicking(null);
|
|
1062
|
-
const log=document.getElementById('log');log.style.display='block';log.textContent='Sending cart to the agent\u2026\\n';
|
|
1063
|
-
document.getElementById('apply').disabled=true; document.getElementById('stop').style.display='inline-block';
|
|
1064
|
-
const started=Date.now();
|
|
1065
|
-
abort=new AbortController();
|
|
1066
|
-
try{
|
|
1067
|
-
const res=await fetch('/api/apply',{method:'POST',headers:{'content-type':'application/json','x-mc-token':TOKEN},body:JSON.stringify({items:cart}),signal:abort.signal});
|
|
1068
|
-
if(!res.ok||!res.body){ log.textContent+='Error: server returned '+res.status+'\\n'; return; }
|
|
1069
|
-
const rd=res.body.getReader();const dec=new TextDecoder();let buf='';
|
|
1070
|
-
for(;;){const{value,done}=await rd.read();
|
|
1071
|
-
buf+=done?dec.decode():dec.decode(value,{stream:true});
|
|
1072
|
-
const fr=buf.split('\\n\\n');buf=done?'':fr.pop();
|
|
1073
|
-
for(const f of fr){const l=f.replace(/^data: /,'').trim();if(!l||l[0]===':')continue;let ev;try{ev=JSON.parse(l)}catch{continue}
|
|
1074
|
-
if(ev.type==='status')log.textContent+=ev.message+'\\n';
|
|
1075
|
-
else if(ev.type==='tool')log.textContent+=(ev.tool==='Read'?'read ':(/Edit|Write/.test(ev.tool)?'edit ':ev.tool.toLowerCase()+' '))+(ev.detail||'')+'\\n';
|
|
1076
|
-
else if(ev.type==='assistant')log.textContent+='AI: '+ev.text.slice(0,200)+'\\n';
|
|
1077
|
-
else if(ev.type==='done'){log.textContent+='Done'+(ev.turns?' \\u00b7 '+ev.turns+' turns':'')+(ev.cost?' \\u00b7 $'+ev.cost.toFixed(3):'')+'\\n'; if(ev.cost){sessionCost+=ev.cost; document.getElementById('sess').textContent=' \\u00b7 session: $'+sessionCost.toFixed(3);}}
|
|
1078
|
-
else if(ev.type==='error')log.textContent+='Error: '+ev.message+'\\n';
|
|
1079
|
-
log.scrollTop=log.scrollHeight;}
|
|
1080
|
-
if(done)break;
|
|
1081
|
-
}
|
|
1082
|
-
}catch(err){ log.textContent+=(err&&err.name==='AbortError'?'Stopped':'Error: '+err)+'\\n'; }
|
|
1083
|
-
finally{
|
|
1084
|
-
running=false;abort=null;
|
|
1085
|
-
document.getElementById('apply').disabled=false; document.getElementById('stop').style.display='none';
|
|
1086
|
-
const secs=Math.round((Date.now()-started)/1000); log.textContent+='('+secs+'s)\\n';
|
|
1087
|
-
setTimeout(()=>{site.src=site.src;},400);
|
|
1088
|
-
}
|
|
1089
|
-
};
|
|
1090
|
-
</script>
|
|
1091
|
-
</body></html>`;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// src/server.ts
|
|
1095
|
-
var MAX_BODY = 256 * 1024;
|
|
1096
|
-
function readBody(req) {
|
|
1097
|
-
return new Promise((resolve, reject) => {
|
|
1098
|
-
let b = "";
|
|
1099
|
-
let size = 0;
|
|
1100
|
-
req.on("data", (c) => {
|
|
1101
|
-
size += c.length;
|
|
1102
|
-
if (size > MAX_BODY) {
|
|
1103
|
-
reject(new Error("body too large"));
|
|
1104
|
-
req.destroy();
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
b += c;
|
|
1108
|
-
});
|
|
1109
|
-
req.on("end", () => resolve(b));
|
|
1110
|
-
req.on("error", reject);
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1413
|
+
import path4 from "path";
|
|
1414
|
+
import { fileURLToPath } from "url";
|
|
1415
|
+
var PREVIEWS_DIR = path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "../previews");
|
|
1113
1416
|
function startControlServer(cfg, port2) {
|
|
1114
1417
|
const panelOrigin = `http://localhost:${port2}`;
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}
|
|
1127
|
-
function authed(req) {
|
|
1128
|
-
const origin = req.headers["origin"];
|
|
1129
|
-
if (origin && origin !== panelOrigin) return false;
|
|
1130
|
-
return req.headers["x-mc-token"] === sessionToken;
|
|
1131
|
-
}
|
|
1418
|
+
const api = createNodeDevApi({
|
|
1419
|
+
targetDir: cfg.targetDir,
|
|
1420
|
+
framework: cfg.framework,
|
|
1421
|
+
iframeSrc: cfg.proxyUrl,
|
|
1422
|
+
// the panel embeds the proxied target site
|
|
1423
|
+
base: "",
|
|
1424
|
+
previewsDir: PREVIEWS_DIR,
|
|
1425
|
+
token: cfg.token,
|
|
1426
|
+
baseUrl: cfg.baseUrl,
|
|
1427
|
+
apiKey: cfg.apiKey,
|
|
1428
|
+
model: cfg.model
|
|
1429
|
+
});
|
|
1132
1430
|
const server = http2.createServer(async (req, res) => {
|
|
1133
|
-
|
|
1134
|
-
if (
|
|
1135
|
-
res.writeHead(
|
|
1136
|
-
|
|
1137
|
-
panelHtml({
|
|
1138
|
-
proxyUrl: cfg.proxyUrl,
|
|
1139
|
-
target: cfg.targetDir,
|
|
1140
|
-
framework: cfg.framework,
|
|
1141
|
-
token: sessionToken
|
|
1142
|
-
})
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
if (url.pathname.startsWith("/api/")) {
|
|
1146
|
-
if (!authed(req)) {
|
|
1147
|
-
res.writeHead(403, { "content-type": "application/json" });
|
|
1148
|
-
return res.end(JSON.stringify({ error: "forbidden" }));
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
if (req.method === "GET" && url.pathname === "/api/catalogue") {
|
|
1152
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
1153
|
-
return res.end(JSON.stringify({ groups: MENU_GROUPS, items: MENU }));
|
|
1154
|
-
}
|
|
1155
|
-
if (req.method === "POST" && url.pathname === "/api/apply") {
|
|
1156
|
-
let items = [];
|
|
1157
|
-
try {
|
|
1158
|
-
items = JSON.parse(await readBody(req)).items ?? [];
|
|
1159
|
-
} catch {
|
|
1160
|
-
}
|
|
1161
|
-
res.writeHead(200, {
|
|
1162
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
1163
|
-
"cache-control": "no-cache, no-transform",
|
|
1164
|
-
connection: "keep-alive"
|
|
1165
|
-
});
|
|
1166
|
-
const send = (e) => res.write(`data: ${JSON.stringify(e)}
|
|
1167
|
-
|
|
1168
|
-
`);
|
|
1169
|
-
const heartbeat = setInterval(() => res.write(`: ping
|
|
1170
|
-
|
|
1171
|
-
`), 15e3);
|
|
1172
|
-
try {
|
|
1173
|
-
if (!items.length) throw new Error("cart is empty");
|
|
1174
|
-
await ensureSession();
|
|
1175
|
-
send({
|
|
1176
|
-
type: "status",
|
|
1177
|
-
message: `Editing on branch ${sessionBranch} (branched from ${baseBranch}; your ${baseBranch} branch is untouched)`
|
|
1178
|
-
});
|
|
1179
|
-
for await (const ev of runAnimationAgent({
|
|
1180
|
-
root: cfg.targetDir,
|
|
1181
|
-
framework: cfg.framework,
|
|
1182
|
-
items,
|
|
1183
|
-
motionToken: cfg.token
|
|
1184
|
-
})) {
|
|
1185
|
-
send(ev);
|
|
1186
|
-
}
|
|
1187
|
-
const stat = await diffStat(cfg.targetDir);
|
|
1188
|
-
const committed = await commitAll(cfg.targetDir, `motion-cart: applied ${items.length} animation(s)`);
|
|
1189
|
-
if (committed && stat) send({ type: "status", message: `Committed:
|
|
1190
|
-
${stat}` });
|
|
1191
|
-
send({ type: "status", message: `Review: git diff ${baseBranch}..${sessionBranch}` });
|
|
1192
|
-
} catch (err) {
|
|
1193
|
-
send({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
1194
|
-
} finally {
|
|
1195
|
-
clearInterval(heartbeat);
|
|
1196
|
-
send({ type: "end" });
|
|
1197
|
-
res.end();
|
|
1198
|
-
}
|
|
1199
|
-
return;
|
|
1431
|
+
if (req.url?.startsWith("/__mc/previews/")) req.url = req.url.replace("/__mc/previews/", "/previews/");
|
|
1432
|
+
if (!await api.handle(req, res)) {
|
|
1433
|
+
res.writeHead(404);
|
|
1434
|
+
res.end();
|
|
1200
1435
|
}
|
|
1201
|
-
res.writeHead(404);
|
|
1202
|
-
res.end();
|
|
1203
1436
|
});
|
|
1204
1437
|
server.listen(port2, "127.0.0.1");
|
|
1205
1438
|
return { server, url: panelOrigin };
|
|
1206
1439
|
}
|
|
1207
1440
|
|
|
1441
|
+
// src/config.ts
|
|
1442
|
+
import { promises as fs3 } from "fs";
|
|
1443
|
+
import os from "os";
|
|
1444
|
+
import path5 from "path";
|
|
1445
|
+
var DIR = path5.join(os.homedir(), ".motion-cart");
|
|
1446
|
+
var FILE = path5.join(DIR, "config.json");
|
|
1447
|
+
function configPath() {
|
|
1448
|
+
return FILE;
|
|
1449
|
+
}
|
|
1450
|
+
async function loadConfig() {
|
|
1451
|
+
try {
|
|
1452
|
+
return JSON.parse(await fs3.readFile(FILE, "utf8"));
|
|
1453
|
+
} catch {
|
|
1454
|
+
return {};
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
async function saveConfig(patch) {
|
|
1458
|
+
const current = await loadConfig();
|
|
1459
|
+
const merged = { ...current };
|
|
1460
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
1461
|
+
if (v != null && v !== "") merged[k] = v;
|
|
1462
|
+
}
|
|
1463
|
+
await fs3.mkdir(DIR, { recursive: true });
|
|
1464
|
+
await fs3.writeFile(FILE, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
1465
|
+
try {
|
|
1466
|
+
await fs3.chmod(FILE, 384);
|
|
1467
|
+
} catch {
|
|
1468
|
+
}
|
|
1469
|
+
return merged;
|
|
1470
|
+
}
|
|
1471
|
+
async function clearConfig() {
|
|
1472
|
+
try {
|
|
1473
|
+
await fs3.rm(FILE);
|
|
1474
|
+
} catch {
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1208
1478
|
// src/index.ts
|
|
1209
1479
|
function arg(name, fallback) {
|
|
1210
1480
|
const i = process.argv.indexOf(`--${name}`);
|
|
@@ -1214,17 +1484,38 @@ function arg(name, fallback) {
|
|
|
1214
1484
|
}
|
|
1215
1485
|
return fallback;
|
|
1216
1486
|
}
|
|
1487
|
+
async function runConfig() {
|
|
1488
|
+
if (process.argv.includes("--clear")) {
|
|
1489
|
+
await clearConfig();
|
|
1490
|
+
console.log("motion-cart: provider config cleared.");
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const patch = { baseUrl: arg("base-url"), apiKey: arg("api-key"), model: arg("model") };
|
|
1494
|
+
const cfg = Object.values(patch).some((v) => v) ? await saveConfig(patch) : await loadConfig();
|
|
1495
|
+
const key = cfg.apiKey ? `${cfg.apiKey.slice(0, 6)}\u2026 (set)` : "(unset)";
|
|
1496
|
+
console.log(`motion-cart provider config \u2014 ${configPath()}`);
|
|
1497
|
+
console.log(` base-url ${cfg.baseUrl || "(unset \u2192 Claude Code login)"}`);
|
|
1498
|
+
console.log(` api-key ${key}`);
|
|
1499
|
+
console.log(` model ${cfg.model || "(default)"}`);
|
|
1500
|
+
console.log(`
|
|
1501
|
+
Flags and MOTION_CART_* env vars override these per run.`);
|
|
1502
|
+
}
|
|
1217
1503
|
function port(name, def) {
|
|
1218
1504
|
const v = Number(arg(name, String(def)));
|
|
1219
1505
|
return Number.isInteger(v) && v > 0 && v < 65536 ? v : def;
|
|
1220
1506
|
}
|
|
1221
1507
|
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "::1"]);
|
|
1222
1508
|
async function main() {
|
|
1223
|
-
|
|
1509
|
+
if (process.argv[2] === "config") return runConfig();
|
|
1510
|
+
const saved = await loadConfig();
|
|
1511
|
+
const target = path6.resolve(arg("target", process.cwd()));
|
|
1224
1512
|
const devUrl = arg("dev-url");
|
|
1225
1513
|
const panelPort = port("port", 7100);
|
|
1226
1514
|
const proxyPort = port("proxy-port", 7101);
|
|
1227
1515
|
const token = arg("token", process.env.MOTION_PLUS_TOKEN);
|
|
1516
|
+
const baseUrl = arg("base-url", process.env.MOTION_CART_BASE_URL ?? saved.baseUrl);
|
|
1517
|
+
const apiKey = arg("api-key", process.env.MOTION_CART_API_KEY ?? saved.apiKey);
|
|
1518
|
+
const model = arg("model", process.env.MOTION_CART_MODEL ?? saved.model);
|
|
1228
1519
|
if (!devUrl) {
|
|
1229
1520
|
console.error(
|
|
1230
1521
|
"Usage: motion-cart --dev-url <url> [--target <dir>] [--port 7100] [--proxy-port 7101] [--token <motion+ token>]\n --dev-url URL of your already-running LOCAL dev server (e.g. http://localhost:5173)\n --target project directory the agent will edit (default: cwd)"
|
|
@@ -1247,11 +1538,16 @@ async function main() {
|
|
|
1247
1538
|
const framework = await detectFramework(target);
|
|
1248
1539
|
const proxyUrl = `http://localhost:${proxyPort}`;
|
|
1249
1540
|
startProxy(devUrl, proxyPort, `http://localhost:${panelPort}`);
|
|
1250
|
-
const { url } = startControlServer(
|
|
1541
|
+
const { url } = startControlServer(
|
|
1542
|
+
{ targetDir: target, devUrl, proxyUrl, framework, token, baseUrl, apiKey, model },
|
|
1543
|
+
panelPort
|
|
1544
|
+
);
|
|
1545
|
+
const provider = baseUrl ? `${new URL(baseUrl).host}${model ? ` (${model})` : ""}` : apiKey ? "custom api key" : "Claude Code login";
|
|
1251
1546
|
console.log(`
|
|
1252
1547
|
motion-cart`);
|
|
1253
1548
|
console.log(` target ${target} (${framework})`);
|
|
1254
1549
|
console.log(` dev-url ${devUrl} -> proxied at ${proxyUrl}`);
|
|
1550
|
+
console.log(` provider ${provider}`);
|
|
1255
1551
|
console.log(` MCP ${token ? "Motion AI Kit enabled" : "bundled guidance (set --token to enable)"}`);
|
|
1256
1552
|
console.log(`
|
|
1257
1553
|
Open the control panel: ${url}
|