quoxcli 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +18 -3
  2. package/lib/api/endpoints/admin.js +21 -0
  3. package/lib/api/endpoints/calendarq.js +8 -0
  4. package/lib/api/endpoints/index.js +2 -0
  5. package/lib/api/endpoints/lens.js +34 -0
  6. package/lib/api/endpoints/lucid.js +2 -1
  7. package/lib/api/endpoints/mon.js +6 -7
  8. package/lib/api/endpoints/v1.js +57 -0
  9. package/lib/commands/admin.js +86 -0
  10. package/lib/commands/approvals.js +5 -1
  11. package/lib/commands/calendar.js +27 -0
  12. package/lib/commands/chat.js +1 -1
  13. package/lib/commands/fleet.js +164 -0
  14. package/lib/commands/fleetRecipe.js +174 -0
  15. package/lib/commands/lens.js +268 -0
  16. package/lib/commands/lucidGoals.js +36 -2
  17. package/lib/commands/mail.js +21 -2
  18. package/lib/commands/monitoring.js +5 -1
  19. package/lib/commands/plan.js +36 -1
  20. package/lib/commands/research.js +78 -0
  21. package/lib/commands/settings.js +7 -1
  22. package/lib/commands/wazuh.js +9 -0
  23. package/lib/commands/wazuhIntake.js +11 -3
  24. package/lib/core/commandRegistrars.js +1 -0
  25. package/lib/core/version.js +15 -2
  26. package/lib/dragon/select.js +2 -2
  27. package/lib/quoxon/app.js +1 -1
  28. package/lib/quoxon/commands.js +1 -1
  29. package/lib/quoxon/dragonCompanion.js +24 -12
  30. package/lib/quoxon/main.js +1 -1
  31. package/lib/quoxon/onboarding/view.js +1 -1
  32. package/lib/quoxon/slash/cliLeaves.json +19 -0
  33. package/lib/quoxon/whatsnew.jsonl +1 -0
  34. package/package.json +1 -1
  35. package/vendor/dragonlab/SOURCE +3 -3
  36. package/vendor/dragonlab/design/architect/v2/architect-master.svg +67 -0
  37. package/vendor/dragonlab/design/architect/v2/character.mjs +83 -0
  38. package/vendor/dragonlab/design/architect/v2/review.mjs +35 -0
  39. package/vendor/dragonlab/design/architect/v2/watch.mjs +56 -0
  40. package/vendor/dragonlab/design/archivist/v2/archivist-master.svg +77 -0
  41. package/vendor/dragonlab/design/archivist/v3/character.mjs +56 -0
  42. package/vendor/dragonlab/design/archivist/v4/character.mjs +46 -0
  43. package/vendor/dragonlab/design/archivist/v4/review.mjs +35 -0
  44. package/vendor/dragonlab/design/archivist/v4/watch.mjs +56 -0
  45. package/vendor/dragonlab/design/argus/v2/character.mjs +69 -0
  46. package/vendor/dragonlab/design/argus/v3/argus-master.svg +68 -0
  47. package/vendor/dragonlab/design/argus/v3/character.mjs +63 -0
  48. package/vendor/dragonlab/design/argus/v3/review.mjs +35 -0
  49. package/vendor/dragonlab/design/argus/v3/watch.mjs +56 -0
  50. package/vendor/dragonlab/design/daedalus/v2/character.mjs +49 -0
  51. package/vendor/dragonlab/design/daedalus/v3/daedalus-master.svg +65 -0
  52. package/vendor/dragonlab/design/daedalus/v4/character.mjs +47 -0
  53. package/vendor/dragonlab/design/daedalus/v5/character.mjs +66 -0
  54. package/vendor/dragonlab/design/daedalus/v6/character.mjs +32 -0
  55. package/vendor/dragonlab/design/daedalus/v6/review.mjs +35 -0
  56. package/vendor/dragonlab/design/daedalus/v6/watch.mjs +56 -0
  57. package/vendor/dragonlab/design/janus/v1/janus-master.svg +70 -0
  58. package/vendor/dragonlab/design/janus/v2/character.mjs +56 -0
  59. package/vendor/dragonlab/design/janus/v3/character.mjs +7 -0
  60. package/vendor/dragonlab/design/janus/v4/character.mjs +150 -0
  61. package/vendor/dragonlab/design/janus/v5/character.mjs +55 -0
  62. package/vendor/dragonlab/design/janus/v5/review.mjs +35 -0
  63. package/vendor/dragonlab/design/janus/v5/watch.mjs +56 -0
  64. package/vendor/dragonlab/design/roady/v1/roady-master.svg +67 -0
  65. package/vendor/dragonlab/design/roady/v2/character.mjs +69 -0
  66. package/vendor/dragonlab/design/roady/v3/character.mjs +91 -0
  67. package/vendor/dragonlab/design/roady/v3/review.mjs +35 -0
  68. package/vendor/dragonlab/design/roady/v3/watch.mjs +56 -0
  69. package/vendor/dragonlab/design/weaver/v3/weaver-master.svg +57 -0
  70. package/vendor/dragonlab/design/weaver/v4/character.mjs +56 -0
  71. package/vendor/dragonlab/design/weaver/v5/character.mjs +52 -0
  72. package/vendor/dragonlab/design/weaver/v6/character.mjs +88 -0
  73. package/vendor/dragonlab/design/weaver/v6/review.mjs +35 -0
  74. package/vendor/dragonlab/design/weaver/v6/watch.mjs +56 -0
  75. package/vendor/dragonlab/integration/companions-v1/architect.mjs +2 -0
  76. package/vendor/dragonlab/integration/companions-v1/archivist.mjs +2 -0
  77. package/vendor/dragonlab/integration/companions-v1/argus.mjs +2 -0
  78. package/vendor/dragonlab/integration/companions-v1/characters.mjs +86 -0
  79. package/vendor/dragonlab/integration/companions-v1/daedalus.mjs +2 -0
  80. package/vendor/dragonlab/integration/companions-v1/janus.mjs +2 -0
  81. package/vendor/dragonlab/integration/companions-v1/roady.mjs +2 -0
  82. package/vendor/dragonlab/integration/companions-v1/view.mjs +4 -1
  83. package/vendor/dragonlab/integration/companions-v1/weaver.mjs +2 -0
@@ -0,0 +1,150 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ import {poseRig as v3Pose,renderJanus as v3Render} from '../v3/character.mjs';
3
+
4
+ /**
5
+ * Janus V4: gate acting on the approved V1 body and the V3 horns (review pending).
6
+ * Idle and Threshold are V3 exactly. V4 adds a compact attach entrance (notice, acknowledge,
7
+ * settle), Hold (gate shut and barred while the host reports a pending approval) and Deny
8
+ * (an offline performance: a traveller is turned back from a barred gate).
9
+ * Pure functions of elapsed time. Renderers draw; the host supplies approval state.
10
+ */
11
+ export const JANUS_CLIPS=Object.freeze([
12
+ {id:'idle',title:'Watch both sides',duration:12000},
13
+ {id:'attach',title:'Notice, acknowledge, settle',duration:3600},
14
+ {id:'threshold',title:'Open the threshold',duration:12000},
15
+ {id:'hold',title:'Hold the gate',duration:9200},
16
+ {id:'grant',title:'Unbar and open',duration:1400},
17
+ {id:'deny',title:'Turn back at the gate',duration:6400},
18
+ ]);
19
+ export const ATTACH_MS=3600,HOLD_ENTER_MS=1200,HOLD_LOOP_MS=8000,GRANT_MS=1400,DENY_MS=6400;
20
+ export const STORY_DURATION=ATTACH_MS+HOLD_ENTER_MS+HOLD_LOOP_MS+GRANT_MS+12000+DENY_MS;
21
+ const STILL=Object.freeze({idle:0,attach:1500,threshold:6500,hold:3000,grant:700,deny:2600});
22
+ // V3 camera, held fixed for every V4 clip so nothing in the frame breathes.
23
+ const framed=rig=>({...rig,bounds:{left:110.7777777777778,top:64.82150205761317,right:607,bottom:445}});
24
+ const clamp=x=>Math.max(0,Math.min(1,x));
25
+ const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
26
+ const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
27
+ const mix=(a,b,k)=>a+(b-a)*k;
28
+
29
+ /** One posed parameter set; every V4 clip is a timeline of these. Rest = V3 idle at t=0. */
30
+ const REST=Object.freeze({lx:0,ly:0,rx:0,ry:0,lg:0,rg:0,cloak:0,door:0,drop:0,glow:0,key:0,flash:0,traveller:null});
31
+ export function janusParams(scene,t){
32
+ if(scene==='attach'){
33
+ // Notice: the outer (cyan, left) face lifts first, the inner (amber, right) one follows.
34
+ const lift=-4*pulse(t,0,1700),liftR=-4*pulse(t,250,1950);
35
+ // Acknowledge: both faces dip toward the gate together and glance back at whoever arrived.
36
+ const nod=pulse(t,900,2700),look=ease((t-350)/550)*(1-ease((t-2300)/900));
37
+ return {...REST,lx:7*nod,ly:lift+7*nod,rx:-7*nod,ry:liftR+7*nod,lg:7*look,rg:-7*look,
38
+ // Cloak tips follow the nod late and settle last; the keystone answers the nod.
39
+ cloak:-5*pulse(t,1250,3500),glow:pulse(t,900,2900),key:pulse(t,1000,3100)};
40
+ }
41
+ if(scene==='hold'){
42
+ // The doorway darkens, then the portcullis drops from the lintel: the gate visibly closes.
43
+ const shut=ease(t/HOLD_ENTER_MS),drop=ease((t-200)/(HOLD_ENTER_MS-200));
44
+ const u=Math.max(0,t-HOLD_ENTER_MS)%HOLD_LOOP_MS;
45
+ // Patient watch while shut: one face keeps the gate, the other checks the fleet, then they swap.
46
+ const outL=pulse(u,600,3400),outR=pulse(u,4600,7400),breathe=Math.sin(Math.PI*u/HOLD_LOOP_MS)**2;
47
+ return {...REST,lx:10*shut-6*outL,ly:7*shut+breathe,rx:-10*shut+6*outR,ry:7*shut+breathe,
48
+ lg:7*shut*(1-outL)-3*outL,rg:-7*shut*(1-outR)+3*outR,door:shut,drop,glow:.5*shut,key:shut};
49
+ }
50
+ if(scene==='grant'){
51
+ // Starts from the shut Hold pose (loop phase zero): portcullis rises first, then the doorway and faces return to rest.
52
+ const k=1-ease(t/GRANT_MS),up=1-ease(t/(GRANT_MS*.6));
53
+ return {...REST,lx:10*k,ly:7*k,rx:-10*k,ry:7*k,lg:7*k,rg:-7*k,door:k,drop:up,glow:.5*k+.5*pulse(t,0,GRANT_MS),key:k};
54
+ }
55
+ if(scene==='deny'){
56
+ const turn=ease(t/700)*(1-ease((t-4600)/1400));
57
+ // The portcullis slams, the bars flash when the traveller meets them, and it is turned back.
58
+ const drop=ease((t-500)/350)*(1-ease((t-4400)/1100)),flash=pulse(t,1850,2750),rear=-4*pulse(t,1850,3000);
59
+ let traveller=null;
60
+ if(t>300&&t<4400){
61
+ const stop=375-(6+12*turn)-19,x=t<1900?mix(232,stop,ease((t-300)/1600)):t<2500?stop:mix(stop,232,ease((t-2500)/1600));
62
+ const size=11*ease((t-300)/300)*(1-ease((t-3800)/600));
63
+ traveller={x,size,rgb:t<2500?[243,194,109]:[226,120,96]};
64
+ }
65
+ return {...REST,lx:10*turn,ly:7*turn+rear,rx:-10*turn,ry:7*turn+rear,lg:7*turn,rg:-7*turn,door:turn,drop,glow:turn,key:turn,flash,traveller};
66
+ }
67
+ return REST;
68
+ }
69
+ export const BAR_Y=Object.freeze([308,330,352]);
70
+ function poseFromParams(rig,p,{deck=false}={}){
71
+ const moveHead=(x,y,side)=>side==='left'?[x+p.lx,y+p.ly]:[x+p.rx,y+p.ry];
72
+ const paths=rig.paths.filter(q=>q.group!=='threshold').map(q=>({...q,polygons:q.polygons.map(poly=>poly.map(([x,y])=>{
73
+ if(q.group==='left-head'||q.group==='right-head')return moveHead(x,y,q.group.split('-')[0]);
74
+ if(q.group==='eyes'){
75
+ const side=q.id.split('-')[0],[hx,hy]=moveHead(x,y,side);
76
+ // Gaze: only the pupil slides, staying inside the white. Positive = toward the gate.
77
+ return q.id.endsWith('-pupil')?[hx+(side==='left'?p.lg:p.rg),hy]:[hx,hy];
78
+ }
79
+ // Blend the neck into the chest so the faces never detach from the body.
80
+ if(q.group==='necks'){const w=clamp((280-y)/85),side=x<375?'left':'right',[hx,hy]=moveHead(x,y,side);return [mix(x,hx,w),mix(y,hy,w)];}
81
+ // Cloak roots pinned at the shoulders; distal tips lift with distance.
82
+ if(q.group==='wings'){const w=clamp(Math.abs(x-375)/230);return [x,y+p.cloak*w];}
83
+ return [x,y];
84
+ }))}));
85
+ const add=(id,rgb,polygons,stroke=3,erase=false)=>paths.push({id,group:'effect',rgb,polygons,stroke,erase});
86
+ const lerp=(a,b,k)=>a.map((v,i)=>Math.round(mix(v,b[i],k)));
87
+ // At rest these three paths are exactly V3's closed seam.
88
+ const half=6+12*p.door,top=294-8*p.door,bottom=375+3*p.door;
89
+ add('gateway-void',[8,13,25],[[[375-half,top],[375+half,top],[375+half,bottom],[375-half,bottom]]],0,true);
90
+ add('cyan-jamb',lerp([111,233,235],[190,255,252],p.glow),[[[375-half,top],[375-half,bottom]]],4);
91
+ add('amber-jamb',lerp([246,188,106],[255,226,160],p.glow),[[[375+half,top],[375+half,bottom]]],4);
92
+ if(p.drop>.02){
93
+ // Portcullis: the lattice slides down from the lintel; only bars already inside the doorway are drawn.
94
+ const fall=(1-p.drop)*(BAR_Y[2]-top+6),rgb=lerp([232,238,240],[255,138,100],p.flash);
95
+ BAR_Y.forEach((y0,i)=>{const y=y0-fall;if(y<top+4)return;
96
+ add('gate-bar-'+i+'-gap',[8,13,25],[[[375-half+3,y-5],[375+half-3,y-5],[375+half-3,y+5],[375-half+3,y+5]]],0,true);
97
+ add('gate-bar-'+i,rgb,[[[375-half+3,y],[375+half-3,y]]],6);});
98
+ }
99
+ if(p.key>.05){
100
+ // Keystone: the gate's own mark, a wedge on an erased halo just above the doorway.
101
+ const k=p.key,w=6+6*k,h=16*k,y=top-3;
102
+ add('keystone-halo',[8,13,25],[[[375-w-4,y-h-4],[375+w+4,y-h-4],[375+w*.55+3,y+2],[375-w*.55-3,y+2]]],0,true);
103
+ add('keystone',lerp([150,160,170],[246,226,170],k),[[[375-w,y-h],[375+w,y-h],[375+w*.55,y],[375-w*.55,y]]],0);
104
+ }
105
+ if(p.traveller&&p.traveller.size>.5){
106
+ const {x,size,rgb}=p.traveller,y=335,d=size+5;
107
+ add('traveller-halo',[8,13,25],[[[x-d,y],[x,y-d],[x+d,y],[x,y+d]]],0,true);
108
+ add('traveller',rgb,[[[x-size,y],[x,y-size],[x+size,y],[x,y+size]]],0);
109
+ }
110
+ const map=new Map(paths.map(q=>[q.id,q]));
111
+ return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:map.get(e.socket.id),white:map.get(e.white.id),pupil:map.get(e.pupil.id)}))};
112
+ }
113
+
114
+ const V4_SCENES=new Set(['attach','hold','grant','deny']);
115
+ export function poseRig(rig,scene,ms,options={}){
116
+ if(!V4_SCENES.has(scene))return v3Pose(rig,scene,ms,options);
117
+ if(scene==='attach'&&ms>=ATTACH_MS)return v3Pose(rig,'idle',ms-ATTACH_MS,options);
118
+ if(scene==='grant'&&ms>=GRANT_MS)return v3Pose(rig,'idle',ms-GRANT_MS,options);
119
+ if(scene==='deny'&&ms>=DENY_MS)return v3Pose(rig,'idle',ms-DENY_MS,options);
120
+ return poseFromParams(framed(rig),janusParams(scene,ms),options);
121
+ }
122
+ function caption(scene,t){
123
+ if(scene==='attach')return t<900?'Someone at the gate.':t<2300?'Both faces acknowledge you.':'Settle. Watch both ways.';
124
+ if(scene==='hold')return t<HOLD_ENTER_MS?'Shut the gate.':'Gate held until you decide.';
125
+ if(scene==='grant')return 'Approved. Unbar the gate.';
126
+ if(scene==='deny')return t<700?'Face the gate.':t<2600?'Bar the way.':t<4400?'Turned back.':'Resume watch.';
127
+ return '';
128
+ }
129
+ export function renderJanus(rig,scene='idle',ms=0,options={}){
130
+ if(!JANUS_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
131
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
132
+ if(!V4_SCENES.has(scene))return v3Render(rig,scene,ms,options);
133
+ // Still freezes every channel (heads, pupils, cloak, keystone, bars, traveller) at one pose.
134
+ const t=options.still?STILL[scene]:ms;
135
+ if(scene==='attach'&&t>=ATTACH_MS)return v3Render(rig,'idle',t-ATTACH_MS,options);
136
+ if(scene==='grant'&&t>=GRANT_MS)return v3Render(rig,'idle',t-GRANT_MS,options);
137
+ if(scene==='deny'&&t>=DENY_MS)return v3Render(rig,'idle',t-DENY_MS,options);
138
+ return {...renderSvgRig(poseRig(rig,scene,t,options),options),scene,clip:scene,caption:caption(scene,t)};
139
+ }
140
+ /** Offline story: attach, a pending approval holds the gate, approval unbars it, a traveller passes, then a denial. Simulated; no host state. */
141
+ export function renderStory(rig,ms,options={}){
142
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
143
+ if(options.still)return renderJanus(rig,'hold',0,options);
144
+ const holdEnd=ATTACH_MS+HOLD_ENTER_MS+HOLD_LOOP_MS,grantEnd=holdEnd+GRANT_MS,openEnd=grantEnd+12000;
145
+ if(ms<ATTACH_MS)return renderJanus(rig,'attach',ms,options);
146
+ if(ms<holdEnd)return renderJanus(rig,'hold',ms-ATTACH_MS,options);
147
+ if(ms<grantEnd)return renderJanus(rig,'grant',ms-holdEnd,options);
148
+ if(ms<openEnd)return renderJanus(rig,'threshold',ms-grantEnd,options);
149
+ return renderJanus(rig,'deny',Math.min(ms-openEnd,DENY_MS-1),options);
150
+ }
@@ -0,0 +1,55 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ import {poseRig as v4Pose,JANUS_CLIPS,ATTACH_MS,GRANT_MS,DENY_MS,HOLD_ENTER_MS,HOLD_LOOP_MS,STORY_DURATION,janusParams,BAR_Y} from '../v4/character.mjs';
3
+ export {JANUS_CLIPS,ATTACH_MS,GRANT_MS,DENY_MS,HOLD_ENTER_MS,HOLD_LOOP_MS,STORY_DURATION,janusParams,BAR_Y};
4
+
5
+ /**
6
+ * Janus V5: the APPROVED V1 look (design/janus/v1/janus-master.svg: anatomy, curled horns, palette,
7
+ * face) driven by V4's acting (attach, idle, hold, grant, threshold, deny). Load it with the V1 master.
8
+ * V4 acted on V3's lower ram horns, which the owner never approved; V5 drops them.
9
+ * V4's camera bounds already equal the V1 master's ink bounds, so framing is V1's.
10
+ * One visual repair: V2 to V4 replaced V1's authored closed seam (an 18-wide dark stroke between the
11
+ * two jambs) with a narrower erased doorway. V5 puts V1's seam back, following the doorway's top and
12
+ * bottom, so the rest pose is cell-for-cell V1 and the opened doorway still erases over it.
13
+ * Pure functions of elapsed time. Renderers draw; the host supplies approval state.
14
+ */
15
+ const STILL=Object.freeze({idle:0,attach:1500,threshold:6500,hold:3000,grant:700,deny:2600});
16
+ const V1_SEAM={id:'closed-seam-dark',group:'threshold',rgb:[8,13,25],stroke:18,erase:true};
17
+
18
+ function restoreV1Seam(posed){
19
+ const at=posed.paths.findIndex(p=>p.id==='gateway-void');
20
+ if(at<0)return posed;
21
+ const ys=posed.paths[at].polygons[0].map(([,y])=>y),top=Math.min(...ys),bottom=Math.max(...ys);
22
+ const paths=[...posed.paths.slice(0,at),{...V1_SEAM,polygons:[[[375,top],[375,bottom]]]},...posed.paths.slice(at)];
23
+ return {...posed,paths};
24
+ }
25
+ export function poseRig(rig,scene,ms,options={}){return restoreV1Seam(v4Pose(rig,scene,ms,options));}
26
+
27
+ function caption(scene,t){
28
+ if(scene==='attach')return t<900?'Someone at the gate.':t<2300?'Both faces acknowledge you.':'Settle. Watch both ways.';
29
+ if(scene==='hold')return t<HOLD_ENTER_MS?'Shut the gate.':'Gate held until you decide.';
30
+ if(scene==='grant')return 'Approved. Unbar the gate.';
31
+ if(scene==='deny')return t<700?'Face the gate.':t<2600?'Bar the way.':t<4400?'Turned back.':'Resume watch.';
32
+ if(scene==='threshold')return t<2200?'An arrival at the gate.':t<3000?'Wait at the threshold.':t<5500?'Both guardians attend. Open the way.':t<8300?'Pass through the gateway.':'Close the passage. Resume watch.';
33
+ return 'One watches each side.';
34
+ }
35
+ const ENDS={attach:ATTACH_MS,grant:GRANT_MS,deny:DENY_MS,threshold:12000};
36
+ export function renderJanus(rig,scene='idle',ms=0,options={}){
37
+ if(!JANUS_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
38
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
39
+ // Still freezes every channel (heads, pupils, cloak, keystone, bars, traveller) at one pose.
40
+ let t=options.still?STILL[scene]:ms;
41
+ // Clips that hand off to idle continue on idle's clock.
42
+ if(ENDS[scene]&&t>=ENDS[scene]){t-=ENDS[scene];scene='idle';}
43
+ return {...renderSvgRig(poseRig(rig,scene,t,options),options),scene,clip:scene,caption:caption(scene,t)};
44
+ }
45
+ /** Offline story: attach, a pending approval holds the gate, approval unbars it, a traveller passes, then a denial. Simulated; no host state. */
46
+ export function renderStory(rig,ms,options={}){
47
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
48
+ if(options.still)return renderJanus(rig,'hold',0,options);
49
+ const holdEnd=ATTACH_MS+HOLD_ENTER_MS+HOLD_LOOP_MS,grantEnd=holdEnd+GRANT_MS,openEnd=grantEnd+12000;
50
+ if(ms<ATTACH_MS)return renderJanus(rig,'attach',ms,options);
51
+ if(ms<holdEnd)return renderJanus(rig,'hold',ms-ATTACH_MS,options);
52
+ if(ms<grantEnd)return renderJanus(rig,'grant',ms-holdEnd,options);
53
+ if(ms<openEnd)return renderJanus(rig,'threshold',ms-grantEnd,options);
54
+ return renderJanus(rig,'deny',Math.min(ms-openEnd,DENY_MS-1),options);
55
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import {parseSvgRig} from '../../../factory/svg-rig-contour.mjs';
4
+ import {renderJanus,JANUS_CLIPS} from './character.mjs';
5
+ import {artSlot,composeMessage} from '../../lucid/recovery/layout.mjs';
6
+ import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
7
+ const args=process.argv.slice(2),values=new Set(['--cols','--rows','--terminal-cols','--terminal-rows','--message','--scene','--at']);
8
+ for(let i=0;i<args.length;i++){
9
+ if(values.has(args[i])){if(!args[i+1]||args[i+1].startsWith('--'))throw Error('Missing option value');i++;}
10
+ else if(!['--large','--mono','--help','--watch','--attach','--intro','--still','--list'].includes(args[i]))throw Error('Unknown option: '+args[i]);
11
+ }
12
+ const value=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback;
13
+ if(args.includes('--help')){console.log('Janus V5 gate studio (approved V1 look, V4 acting). --watch --attach --intro --still --scene ID --at MS --list --large --mono --message TEXT --cols N --rows N --terminal-cols N --terminal-rows N');process.exit(0);}
14
+ if(args.includes('--list')){console.log(JANUS_CLIPS.map(c=>c.id+' · '+c.title).join('\n'));process.exit(0);}
15
+ const w=Number(value('--terminal-cols',process.stdout.columns||80)),h=Number(value('--terminal-rows',process.stdout.rows||24));
16
+ if(!Number.isSafeInteger(w)||!Number.isSafeInteger(h)||w<1||h<1)throw Error('Invalid terminal size');
17
+ const slot=artSlot(w,h),large=args.includes('--large')||args.includes('--intro'),explicit=large||args.includes('--cols')||args.includes('--rows');
18
+ const cols=Number(value('--cols',large?Math.min(120,w-4):Math.min(39,slot.cols))),rows=Number(value('--rows',large?Math.min(36,h-4):Math.min(11,slot.rows)));
19
+ const message=value('--message','JANUS\nWatch. Judge. Open.');
20
+ if(!slot.showArt&&!explicit){console.log(composeMessage(null,message,{terminalCols:w}).join('\n'));}
21
+ else{
22
+ if(cols>w-4||rows>h-2)throw Error('Requested art exceeds terminal margins');
23
+ const rig=parseSvgRig(await fs.readFile(new URL('../v1/janus-master.svg',import.meta.url),'utf8'));
24
+ const opts={cols,rows,mono:args.includes('--mono'),still:args.includes('--still'),deck:large};
25
+ const scene=value('--scene',args.includes('--attach')?'attach':'idle');
26
+ const r=renderJanus(rig,scene,Number(value('--at',0)),opts);
27
+ if(!r.ok)throw Error(r.code);
28
+ if(args.includes('--watch')||args.includes('--attach')||args.includes('--intro')){
29
+ const {watch}=await import('./watch.mjs');
30
+ await watch(rig,opts,{terminalCols:w,terminalRows:h,large,scene:args.includes('--scene')||args.includes('--attach')?scene:null,arrival:args.includes('--attach'),message:args.includes('--message')?message:null});
31
+ }else{
32
+ const shownMessage=args.includes('--message')?message:'JANUS\n'+r.caption;
33
+ console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,shownMessage,{terminalCols:w})]:composeMessage(r.frame,shownMessage,{terminalCols:w})).join('\n'));
34
+ }
35
+ }
@@ -0,0 +1,56 @@
1
+ import {performance} from 'node:perf_hooks';
2
+ import {JANUS_CLIPS,renderJanus,renderStory,STORY_DURATION} from './character.mjs';
3
+ import {composeMessage} from '../../lucid/recovery/layout.mjs';
4
+ import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
5
+
6
+ /** One bounded performance. No alternate screen or full-screen clearing. */
7
+ export async function watch(paths,options,config){
8
+ if(!process.stdout.isTTY||!process.stdin.isTTY)throw new Error('--watch/--intro needs an interactive terminal. Use --scene and --at for snapshots.');
9
+ const {terminalCols,terminalRows,scene,large,message}=config;
10
+ const clip=scene?JANUS_CLIPS.find(c=>c.id===scene):null;
11
+ const duration=(clip?.duration??STORY_DURATION)+(config.arrival?6000:0);
12
+ const realCols=process.stdout.columns,realRows=process.stdout.rows;
13
+ const start=performance.now();
14
+ const render=t=>{
15
+ const r=scene?renderJanus(paths,scene,t,options):renderStory(paths,t,options);
16
+ if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
17
+ const caption=message??'JANUS · '+r.clip+'\n'+r.caption;
18
+ return large?[...cellsToAnsi(r.frame),...caption.split('\n')]:composeMessage(r.frame,caption,{terminalCols});
19
+ };
20
+ // Reserve enough lines for captions too; keep within the real terminal.
21
+ const height=options.rows+3;
22
+ if(height>=terminalRows)throw new Error('Animation needs room for its caption; reduce --rows.');
23
+ let ended=false,drawn=false,timer,resolveDone,rejectDone;
24
+ const wasRaw=Boolean(process.stdin.isRaw);
25
+ const done=new Promise((resolve,reject)=>{resolveDone=resolve;rejectDone=reject;});
26
+ const cleanup=(error)=>{
27
+ if(ended)return;ended=true;clearInterval(timer);
28
+ process.stdin.off('data',key);process.stdout.off('resize',resized);
29
+ process.off('SIGINT',sigint);process.off('SIGTERM',sigterm);
30
+ process.stdin.setRawMode(wasRaw);process.stdin.pause();
31
+ if(drawn)process.stdout.write(`\x1b[${height}B\r`);
32
+ process.stdout.write('\x1b[0m\x1b[?25h');
33
+ error?rejectDone(error):resolveDone();
34
+ };
35
+ const key=data=>{if(data.includes(3)||data.toString().includes('q'))cleanup();};
36
+ const sigint=()=>cleanup(),sigterm=()=>cleanup();
37
+ const resized=()=>cleanup(new Error('Terminal resized; rerun for the new art slot.'));
38
+ try{
39
+ process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on('data',key);
40
+ process.stdout.on('resize',resized);process.on('SIGINT',sigint);process.on('SIGTERM',sigterm);
41
+ process.stdout.write('\x1b[?25l');
42
+ // Allocate the block once; each frame replaces only these lines.
43
+ process.stdout.write('\n'.repeat(height)+`\x1b[${height}A`);drawn=true;
44
+ const tick=()=>{
45
+ try{
46
+ if(process.stdout.columns!==realCols||process.stdout.rows!==realRows)return resized();
47
+ const t=Math.min(performance.now()-start,duration-1),lines=render(t);
48
+ if(lines.length>height)throw new Error('Message exceeds the animation block; use a shorter message or a static preview.');
49
+ process.stdout.write(Array.from({length:height},(_,i)=>'\r\x1b[2K'+(lines[i]??'')+'\n').join('')+`\x1b[${height}A`);
50
+ if(performance.now()-start>=duration)cleanup();
51
+ }catch(e){cleanup(e);}
52
+ };
53
+ tick();if(!ended)timer=setInterval(tick,1000/6);
54
+ await done;
55
+ }catch(e){cleanup(e);await done;}
56
+ }
@@ -0,0 +1,67 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480"><title>Roady — the way forward</title>
2
+ <g id="far-wing">
3
+ <path id="far-sail" d="M 391 282 Q 439 202 524 154 L 500 243 L 467 229 L 443 290 L 418 278 L 399 322 Z" fill="#b7572c"/>
4
+ <path id="far-ribs" d="M 391 282 Q 439 202 524 154 M 391 282 L 500 243 M 391 282 L 443 290" fill="none" stroke="#3b9b9b" stroke-width="7"/>
5
+ </g>
6
+ <g id="tail">
7
+ <path id="long-tail" d="M 300 352 C 234 356 217 418 156 416 C 105 415 99 376 129 346 Q 89 356 88 395 C 86 443 154 457 201 434 Q 263 398 319 396 Z" fill="#217582"/>
8
+ <path id="tail-highlight" d="M 296 376 Q 231 393 192 426 Q 133 455 103 410 Q 136 436 184 416 Q 236 380 296 376 Z" fill="#63afa5"/>
9
+ <path id="compass-tip" d="M 128 322 L 148 355 L 126 349 L 108 365 Z" fill="#ffb957"/>
10
+ </g>
11
+ <g id="body">
12
+ <path id="body-mass" d="M 310 234 Q 357 217 395 255 L 409 331 Q 405 381 363 399 L 293 389 Q 266 349 282 303 Z" fill="#236f7c"/>
13
+ <path id="belly" d="M 355 255 Q 383 248 393 279 L 397 334 Q 387 373 360 383 L 335 366 Q 359 314 345 279 Z" fill="#ead8aa"/>
14
+ <path id="belly-lines" d="M 352 295 Q 372 307 396 292 M 350 324 Q 373 338 394 323 M 343 352 Q 366 363 383 352" fill="none" stroke="#b9976c" stroke-width="4"/>
15
+ <path id="rear-leg" d="M 304 346 Q 269 353 270 389 L 250 427 L 293 435 L 324 382 Z" fill="#328b95"/>
16
+ <path id="forward-leg" d="M 364 360 Q 389 362 390 391 L 427 417 L 459 419 L 469 435 L 407 437 L 357 403 Z" fill="#3a9ba0"/>
17
+ <path id="claws" d="M 255 424 L 247 439 L 262 434 L 272 424 L 269 440 L 288 434 M 441 422 L 436 438 L 449 433 L 457 425 L 456 439 L 473 434" fill="#fff0d0"/>
18
+ </g>
19
+ <g id="near-wing">
20
+ <path id="near-sail" d="M 318 267 Q 259 179 161 148 Q 190 193 161 239 L 204 232 L 227 287 L 262 271 L 298 321 Z" fill="#ec7938"/>
21
+ <path id="near-web" d="M 318 267 L 161 148 Q 239 208 227 287 L 262 271 L 298 321 Z" fill="#ffa24a"/>
22
+ <path id="near-ribs" d="M 318 267 Q 259 179 161 148 M 318 267 L 161 239 M 318 267 L 227 287" fill="none" stroke="#277d8a" stroke-width="8"/>
23
+ <path id="shoulder" d="M 294 279 C 294 265.745166 302.50658975 255 313 255 C 323.49341025 255 332 265.745166 332 279 C 332 292.254834 323.49341025 303 313 303 C 302.50658975 303 294 292.254834 294 279 Z" fill="#3a96a0"/>
24
+ </g>
25
+ <g id="neck">
26
+ <path id="neck-mass" d="M 331 176 L 386 185 Q 372 231 397 270 L 364 298 L 323 268 Q 344 224 331 176 Z" fill="#328f9a"/>
27
+ </g>
28
+ <g id="scarf">
29
+ <path id="streamer" d="M 346 244 Q 289 227 252 247 L 217 229 L 232 265 L 211 282 Q 279 286 351 260 Z" fill="#ed7233"/>
30
+ <path id="collar" d="M 320 229 Q 354 247 389 224 L 394 249 Q 357 272 321 250 Z" fill="#ff9c43"/>
31
+ </g>
32
+ <g id="satchel">
33
+ <path id="strap" d="M 324 267 L 335 265 L 376 350 L 366 355 Z" fill="#e1b56b"/>
34
+ <path id="bag" d="M 285 322 L 328 331 L 328 374 L 284 365 Z" fill="#b57743"/>
35
+ <path id="flap" d="M 285 322 L 329 331 L 313 350 L 283 338 Z" fill="#e2af6b"/>
36
+ </g>
37
+ <g id="head">
38
+ <path id="far-horn" d="M 344 142 Q 291 117 272 66 Q 313 102 360 113 Z" fill="#e5d9bc"/>
39
+ <path id="near-horn" d="M 382 130 Q 345 91 348 53 Q 369 90 409 105 L 405 138 Z" fill="#fff0d0"/>
40
+ <path id="ear" d="M 328 155 L 291 131 L 300 174 L 331 187 Z" fill="#4ea9a6"/>
41
+ <path id="face" d="M 331 133 Q 373 108 405 139 Q 426 166 406 203 L 370 223 L 331 207 Q 314 172 331 133 Z" fill="#3c9aa5"/>
42
+ <path id="muzzle" d="M 381 178 Q 426 164 460 183 L 466 204 Q 444 226 398 222 L 371 204 Z" fill="#6bb9b2"/>
43
+ <path id="jaw" d="M 387 214 Q 433 224 458 207 Q 450 238 410 232 Z" fill="#dfd6ae"/>
44
+ <path id="smile" d="M 386 204 Q 424 220 458 201" fill="none" stroke="#080d19" stroke-width="6"/>
45
+ <path id="nostril" d="M 442 182 Q 456 182 451 192 L 441 190 Z" fill="#080d19"/>
46
+ </g>
47
+ <g id="map">
48
+ <path id="scroll" d="M 360 332 L 446 300 L 460 328 L 373 362 Z" fill="#eddcb0"/>
49
+ <path id="scroll-edge" d="M 360 332 L 373 362 M 446 300 L 460 328" fill="none" stroke="#fff3ce" stroke-width="10"/>
50
+ <path id="map-tie" d="M 402 316 L 415 346" fill="none" stroke="#da6c37" stroke-width="8"/>
51
+ <path id="scroll-end" d="M 443 314 C 443 305.71572875 447.4771525 299 453 299 C 458.5228475 299 463 305.71572875 463 314 C 463 322.28427125 458.5228475 329 453 329 C 447.4771525 329 443 322.28427125 443 314 Z" fill="#fff0cb"/>
52
+ <path id="scroll-hole" d="M 449 314 C 449 310.13400675 450.790861 307 453 307 C 455.209139 307 457 310.13400675 457 314 C 457 317.86599325 455.209139 321 453 321 C 450.790861 321 449 317.86599325 449 314 Z" fill="#080d19"/>
53
+ </g>
54
+ <g id="arms">
55
+ <path id="map-arm" d="M 350 278 Q 370 274 375 297 L 403 311 L 419 301 L 433 313 Q 417 341 395 336 L 357 317 Z" fill="#439eaa"/>
56
+ <path id="point-arm" d="M 391 259 Q 402 247 419 263 L 442 276 L 487 250 L 493 234 L 520 223 L 526 232 L 508 245 L 526 245 L 526 255 L 499 264 L 454 305 Q 439 312 423 298 Z" fill="#59acb1"/>
57
+ <path id="grip-claws" d="M 407 305 L 414 297 L 418 313 M 418 309 L 427 302 L 430 317" fill="#fff0cf"/>
58
+ </g>
59
+ <g id="eyes">
60
+ <path id="far-eye-socket" d="M 324 167 C 324 153.19288125 332.50658975 142 343 142 C 353.49341025 142 362 153.19288125 362 167 C 362 180.80711875 353.49341025 192 343 192 C 332.50658975 192 324 180.80711875 324 167 Z" fill="#080d19"/>
61
+ <path id="far-eye-white" d="M 331 167 C 331 157.0588745 336.372583 149 343 149 C 349.627417 149 355 157.0588745 355 167 C 355 176.9411255 349.627417 185 343 185 C 336.372583 185 331 176.9411255 331 167 Z" fill="#ffffff"/>
62
+ <path id="far-pupil" d="M 342 167 C 342 163.13400675 344.23857625 160 347 160 C 349.76142375 160 352 163.13400675 352 167 C 352 170.86599325 349.76142375 174 347 174 C 344.23857625 174 342 170.86599325 342 167 Z" fill="#080d19"/>
63
+ <path id="near-eye-socket" d="M 363 163 C 363 146.98374225 373.745166 134 387 134 C 400.254834 134 411 146.98374225 411 163 C 411 179.01625775 400.254834 192 387 192 C 373.745166 192 363 179.01625775 363 163 Z" fill="#080d19"/>
64
+ <path id="near-eye-white" d="M 370 163 C 370 150.8497355 377.61115925 141 387 141 C 396.38884075 141 404 150.8497355 404 163 C 404 175.1502645 396.38884075 185 387 185 C 377.61115925 185 370 175.1502645 370 163 Z" fill="#ffffff"/>
65
+ <path id="near-pupil" d="M 386 163 C 386 159.13400675 388.23857625 156 391 156 C 393.76142375 156 396 159.13400675 396 163 C 396 166.86599325 393.76142375 170 391 170 C 388.23857625 170 386 166.86599325 386 163 Z" fill="#080d19"/>
66
+ </g>
67
+ </svg>
@@ -0,0 +1,69 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ export const STORY_DURATION=20000;
3
+ export const ROADY_CLIPS=Object.freeze([{id:'idle',title:'Ready for the next step',duration:8000},{id:'journey',title:'Chart · Blocked · Reroute · Arrive',duration:STORY_DURATION}]);
4
+ const clamp=x=>Math.max(0,Math.min(1,x));
5
+ const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
6
+ const rot=(x,y,cx,cy,a)=>[cx+(x-cx)*Math.cos(a)-(y-cy)*Math.sin(a),cy+(x-cx)*Math.sin(a)+(y-cy)*Math.cos(a)];
7
+ export function poseRig(rig,scene,ms){
8
+ if(scene==='journey'&&ms>=STORY_DURATION)return poseRig(rig,'idle',ms-STORY_DURATION);
9
+ const t=ms,active=scene==='journey',env=active?ease(t/1400)*(1-ease((t-17500)/2500)):0;
10
+ const open=active?ease((t-1300)/2000)*(1-ease((t-17000)/2300)):0;
11
+ const blocked=active?ease((t-6300)/600)*(1-ease((t-10500)/1000)):0;
12
+ const success=active?ease((t-13500)/1100)*(1-ease((t-17000)/1800)):0;
13
+ const quiet=Math.sin(t/8000*Math.PI*2);
14
+ const headAngle=active?(.09*blocked-.08*success+.025*Math.sin(t/700)*env):.018*quiet;
15
+ const head=(x,y)=>rot(x,y,355,235,headAngle);
16
+ const paths=rig.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
17
+ if(p.group==='head'||p.group==='eyes'){
18
+ const q=head(x,y);if(p.id.endsWith('pupil'))q[0]+=active?(-5*success+2*blocked):1.5*quiet;return q;
19
+ }
20
+ if(p.group==='neck'){const w=clamp((285-y)/100),q=head(x,y);return [x+(q[0]-x)*w,y+(q[1]-y)*w];}
21
+ if(p.group==='near-wing')return rot(x,y,318,267,.035*quiet+.14*success);
22
+ if(p.group==='far-wing')return rot(x,y,391,282,-.025*quiet-.11*success);
23
+ if(p.group==='tail'){const w=clamp((300-x)/210);return [x+8*w*quiet,y-12*w*quiet-10*w*success];}
24
+ if(p.id==='streamer'){const w=clamp((346-x)/135);return [x,y+w*(4*quiet+5*env*Math.sin(t/450))];}
25
+ if(p.id==='point-arm'){const w=clamp((x-405)/95);return [x+5*w*env,y+14*w*blocked-10*w*success];}
26
+ return [x,y];
27
+ }))}));
28
+ const add=(id,rgb,poly,stroke=0,erase=false)=>paths.push({id,group:'roadmap',rgb,polygons:[poly],stroke,erase});
29
+ const line=(id,rgb,a,b,w=5)=>add(id,rgb,[a,b],w);
30
+ if(open>.001){
31
+ const X=u=>453+u*open,Y=(u,v)=>320+u*.18+v;
32
+ const pt=(u,v)=>[X(u),Y(u,v)];
33
+ add('map-shadow',[8,13,25],[pt(0,-4),pt(256,-4),pt(256,111),pt(0,111)],0,true);
34
+ add('unfurled-map',[229,213,164],[pt(0,0),pt(250,0),pt(250,104),pt(0,104)]);
35
+ line('map-edge',[255,242,205],pt(250,0),pt(250,104),7);
36
+ const route=[[15,75],[75,51],[140,67],[222,26]];
37
+ const draw=(id,points,progress,rgb,w)=>{
38
+ let count=clamp(progress)*(points.length-1);
39
+ for(let i=0;i<points.length-1;i++){
40
+ const f=clamp(count-i);if(f>0)line(id+i,rgb,pt(...points[i]),pt(points[i][0]+(points[i+1][0]-points[i][0])*f,points[i][1]+(points[i+1][1]-points[i][1])*f),w);
41
+ }
42
+ };
43
+ draw('planned-route',route,(t-2800)/2700,[50,105,117],6);
44
+ for(let i=0;i<route.length;i++)if(t>3200+i*700){const [u,v]=route[i];add('milestone'+i,[37,104,118],[pt(u-6,v),pt(u,v-7),pt(u+6,v),pt(u,v+7)]);}
45
+ if(blocked>.01){const u=140,v=67;line('blocked-a',[199,58,40],pt(u-12*blocked,v-12*blocked),pt(u+12*blocked,v+12*blocked),8);line('blocked-b',[199,58,40],pt(u-12*blocked,v+12*blocked),pt(u+12*blocked,v-12*blocked),8);}
46
+ const detour=[[75,51],[114,20],[168,13],[222,26]];
47
+ if(t>9000)draw('detour',detour,(t-9000)/3500,[220,102,27],7);
48
+ if(t>12300){
49
+ const progress=clamp((t-12300)/2100)*(detour.length-1),i=Math.min(2,Math.floor(progress)),f=progress-i;
50
+ const [u,v]=[detour[i][0]+(detour[i+1][0]-detour[i][0])*f,detour[i][1]+(detour[i+1][1]-detour[i][1])*f];
51
+ add('traveller',[255,255,255],[pt(u-7,v),pt(u,v-8),pt(u+7,v),pt(u,v+8)]);
52
+ }
53
+ if(success>.001){
54
+ const h=48*success;line('flagpole',[37,90,103],pt(222,26),pt(222,26-h),5);
55
+ add('destination-flag',[238,104,35],[pt(222,26-h),pt(247,34-h+3*Math.sin(t/180)),pt(222,43-h)]);
56
+ }
57
+ }
58
+ const byId=new Map(paths.map(p=>[p.id,p]));
59
+ return {...rig,bounds:{left:65,top:35,right:730,bottom:510},paths,eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))};
60
+ }
61
+ export function renderRoady(rig,scene='idle',ms=0,options={}){
62
+ if(!ROADY_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
63
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
64
+ if(!options.still&&scene==='journey'&&ms>=STORY_DURATION)return renderRoady(rig,'idle',ms-STORY_DURATION,options);
65
+ const t=options.still?(scene==='journey'?15000:0):ms;
66
+ const caption=scene==='idle'?'Every goal needs a way forward.':t<2800?'First, let’s see where we’re going.':t<6300?'A destination. A few clear steps.':t<9000?'Ah. That route is blocked.':t<12300?'There’s another way around.':t<14500?'One step at a time.':t<17500?'Told you we’d find a way.':'Ready for the next adventure.';
67
+ return {...renderSvgRig(poseRig(rig,scene,t),options),scene,clip:scene,caption};
68
+ }
69
+ export const renderStory=(rig,ms,options={})=>renderRoady(rig,'journey',ms,options);
@@ -0,0 +1,91 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ import {poseRig as v2Pose,renderRoady as v2Render,ROADY_CLIPS as v2Clips} from '../v2/character.mjs';
3
+
4
+ /**
5
+ * Roady V3: companion pass on the approved V1 SVG. Idle and Journey are V2 exactly.
6
+ * Adds 'attach' (notice, a raised-claw wave, settle; exact handoff to idle) and 'chart' (unroll the map, trace one
7
+ * planned route with its milestones, point the way, roll it up). Chart has no blockage, detour or arrival flag:
8
+ * those would claim plan facts the host never states. Pure functions of elapsed time.
9
+ */
10
+ export const ATTACH_MS=3400,CHART_MS=7000;
11
+ export const ROADY_CLIPS=Object.freeze([...v2Clips,{id:'attach',title:'Notice · wave · settle',duration:ATTACH_MS},{id:'chart',title:'Chart the route',duration:CHART_MS}]);
12
+ export const STORY_DURATION=ATTACH_MS+CHART_MS;
13
+ const STILL=Object.freeze({attach:1500,chart:3600});
14
+ const clamp=x=>Math.max(0,Math.min(1,x));
15
+ const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
16
+ const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
17
+ const rot=(x,y,cx,cy,a)=>[cx+(x-cx)*Math.cos(a)-(y-cy)*Math.sin(a),cy+(x-cx)*Math.sin(a)+(y-cy)*Math.cos(a)];
18
+ const REST=Object.freeze({head:0,pupil:0,near:0,far:0,tailX:0,tailY:0,streamer:0,armX:0,armY:0,open:0,route:0,marks:0});
19
+
20
+ /** One parameter set per instant. All zero is V2 idle at t=0, the rest pose. */
21
+ export function roadyParams(scene,t){
22
+ if(scene==='attach'){
23
+ // Notice: the head lifts and the eyes glance back at whoever arrived. Acknowledge: a nod and a raised-claw wave.
24
+ // Settle: wings flex, scarf and tail land last.
25
+ return {...REST,head:-.06*pulse(t,0,1400)+.06*pulse(t,1100,2600),pupil:-3*pulse(t,200,2400),
26
+ armX:4*pulse(t,900,2600),armY:-12*pulse(t,900,2600),near:.10*pulse(t,1500,3100),far:-.08*pulse(t,1700,3300),
27
+ tailX:4*pulse(t,1900,3400),tailY:-8*pulse(t,1900,3400),streamer:4*pulse(t,1600,3400)*Math.sin(t/300)};
28
+ }
29
+ if(scene==='chart'){
30
+ const env=ease(t/900)*(1-ease((t-6000)/1000));
31
+ return {...REST,head:.03*pulse(t,1800,4400)-.05*pulse(t,4300,5800),pupil:2*pulse(t,1500,4600),
32
+ armX:5*env,armY:-6*pulse(t,3800,5400),near:.05*pulse(t,4200,6000),streamer:5*env*Math.sin(t/450),
33
+ open:ease((t-500)/1600)*(1-ease((t-5200)/1500)),route:clamp((t-1800)/2400),marks:t<2000?0:Math.min(4,1+Math.floor((t-2000)/600))};
34
+ }
35
+ return REST;
36
+ }
37
+ const BOUNDS=Object.freeze({left:65,top:35,right:730,bottom:510});
38
+ function poseFromParams(rig,P){
39
+ const head=(x,y)=>rot(x,y,355,235,P.head);
40
+ // Same anatomy transforms as V2: shared head/eye joint, neck blended into the chest, rooted wings, root-weighted tail.
41
+ const paths=rig.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
42
+ if(p.group==='head'||p.group==='eyes'){const q=head(x,y);if(p.id.endsWith('pupil'))q[0]+=P.pupil;return q;}
43
+ if(p.group==='neck'){const w=clamp((285-y)/100),q=head(x,y);return [x+(q[0]-x)*w,y+(q[1]-y)*w];}
44
+ if(p.group==='near-wing')return rot(x,y,318,267,P.near);
45
+ if(p.group==='far-wing')return rot(x,y,391,282,P.far);
46
+ if(p.group==='tail'){const w=clamp((300-x)/210);return [x+P.tailX*w,y+P.tailY*w];}
47
+ if(p.id==='streamer'){const w=clamp((346-x)/135);return [x,y+w*P.streamer];}
48
+ if(p.id==='point-arm'){const w=clamp((x-405)/95);return [x+P.armX*w,y+P.armY*w];}
49
+ return [x,y];
50
+ }))}));
51
+ if(P.open>.001){
52
+ // V2's map, planned route and milestones only. No blockage, detour, traveller or flag.
53
+ const add=(id,rgb,poly,stroke=0,erase=false)=>paths.push({id,group:'roadmap',rgb,polygons:[poly],stroke,erase});
54
+ const pt=(u,v)=>[453+u*P.open,320+u*.18+v];
55
+ add('map-shadow',[8,13,25],[pt(0,-4),pt(256,-4),pt(256,111),pt(0,111)],0,true);
56
+ add('unfurled-map',[229,213,164],[pt(0,0),pt(250,0),pt(250,104),pt(0,104)]);
57
+ add('map-edge',[255,242,205],[pt(250,0),pt(250,104)],7);
58
+ const route=[[15,75],[75,51],[140,67],[222,26]],count=P.route*(route.length-1);
59
+ for(let i=0;i<route.length-1;i++){const f=clamp(count-i);if(f>0)add('planned-route'+i,[50,105,117],[pt(...route[i]),pt(route[i][0]+(route[i+1][0]-route[i][0])*f,route[i][1]+(route[i+1][1]-route[i][1])*f)],6);}
60
+ for(let i=0;i<P.marks;i++){const [u,v]=route[i];add('milestone'+i,[37,104,118],[pt(u-6,v),pt(u,v-7),pt(u+6,v),pt(u,v+7)]);}
61
+ }
62
+ const byId=new Map(paths.map(p=>[p.id,p]));
63
+ return {...rig,bounds:BOUNDS,paths,eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))};
64
+ }
65
+ const NEW=new Set(['attach','chart']);
66
+ export function poseRig(rig,scene,ms){
67
+ if(!NEW.has(scene))return v2Pose(rig,scene,ms);
68
+ const d=scene==='attach'?ATTACH_MS:CHART_MS;
69
+ if(ms>=d)return v2Pose(rig,'idle',ms-d);
70
+ return poseFromParams(rig,roadyParams(scene,ms));
71
+ }
72
+ function caption(scene,t){
73
+ if(scene==='attach')return t<1100?'Oh, hello there.':t<2600?'Roady waves you aboard.':'Ready for the next step.';
74
+ return t<1800?'Unroll the map.':t<4400?'Trace the route. Mark the milestones.':t<5200?'That way.':'Roll it up. Onward.';
75
+ }
76
+ export function renderRoady(rig,scene='idle',ms=0,options={}){
77
+ if(!ROADY_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
78
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
79
+ if(!NEW.has(scene))return v2Render(rig,scene,ms,options);
80
+ const d=scene==='attach'?ATTACH_MS:CHART_MS;
81
+ if(!options.still&&ms>=d)return v2Render(rig,'idle',ms-d,options);
82
+ // Still freezes every channel (head, pupils, wave, wings, tail, scarf, map, route) at one pose.
83
+ const t=options.still?STILL[scene]:ms;
84
+ return {...renderSvgRig(poseRig(rig,scene,t),options),scene,clip:scene,caption:caption(scene,t)};
85
+ }
86
+ /** Offline companion story: attach, then chart, then idle. Simulated; no QuoxPlan state. */
87
+ export function renderStory(rig,ms,options={}){
88
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
89
+ if(options.still)return renderRoady(rig,'chart',0,options);
90
+ return ms<ATTACH_MS?renderRoady(rig,'attach',ms,options):renderRoady(rig,'chart',ms-ATTACH_MS,options);
91
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import {parseSvgRig} from '../../../factory/svg-rig-contour.mjs';
4
+ import {renderRoady,ROADY_CLIPS} from './character.mjs';
5
+ import {artSlot,composeMessage} from '../../lucid/recovery/layout.mjs';
6
+ import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
7
+ const args=process.argv.slice(2),values=new Set(['--cols','--rows','--terminal-cols','--terminal-rows','--message','--scene','--at']);
8
+ for(let i=0;i<args.length;i++){
9
+ if(values.has(args[i])){if(!args[i+1]||args[i+1].startsWith('--'))throw Error('Missing option value');i++;}
10
+ else if(!['--large','--mono','--help','--watch','--attach','--intro','--still','--list'].includes(args[i]))throw Error('Unknown option: '+args[i]);
11
+ }
12
+ const value=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback;
13
+ if(args.includes('--help')){console.log('Roady V3 roadmap studio (companion attach and Chart). --watch --attach --intro --still --scene ID --at MS --list --large --mono --message TEXT --cols N --rows N --terminal-cols N --terminal-rows N');process.exit(0);}
14
+ if(args.includes('--list')){console.log(ROADY_CLIPS.map(c=>c.id+' · '+c.title).join('\n'));process.exit(0);}
15
+ const w=Number(value('--terminal-cols',process.stdout.columns||80)),h=Number(value('--terminal-rows',process.stdout.rows||24));
16
+ if(!Number.isSafeInteger(w)||!Number.isSafeInteger(h)||w<1||h<1)throw Error('Invalid terminal size');
17
+ const slot=artSlot(w,h),large=args.includes('--large')||args.includes('--intro'),explicit=large||args.includes('--cols')||args.includes('--rows');
18
+ const cols=Number(value('--cols',large?Math.min(120,w-4):Math.min(39,slot.cols))),rows=Number(value('--rows',large?Math.min(36,h-4):Math.min(11,slot.rows)));
19
+ const message=value('--message','ROADY\nEvery goal needs a way forward.');
20
+ if(!slot.showArt&&!explicit){console.log(composeMessage(null,message,{terminalCols:w}).join('\n'));}
21
+ else{
22
+ if(cols>w-4||rows>h-2)throw Error('Requested art exceeds terminal margins');
23
+ const rig=parseSvgRig(await fs.readFile(new URL('../v1/roady-master.svg',import.meta.url),'utf8'));
24
+ const opts={cols,rows,mono:args.includes('--mono'),still:args.includes('--still'),deck:large};
25
+ const scene=value('--scene',args.includes('--attach')?'attach':'idle');
26
+ const r=renderRoady(rig,scene,Number(value('--at',0)),opts);
27
+ if(!r.ok)throw Error(r.code);
28
+ if(args.includes('--watch')||args.includes('--attach')||args.includes('--intro')){
29
+ const {watch}=await import('./watch.mjs');
30
+ await watch(rig,opts,{terminalCols:w,terminalRows:h,large,scene:args.includes('--scene')||args.includes('--attach')?scene:null,arrival:args.includes('--attach'),message:args.includes('--message')?message:null});
31
+ }else{
32
+ const shownMessage=args.includes('--message')?message:'ROADY\n'+r.caption;
33
+ console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,shownMessage,{terminalCols:w})]:composeMessage(r.frame,shownMessage,{terminalCols:w})).join('\n'));
34
+ }
35
+ }
@@ -0,0 +1,56 @@
1
+ import {performance} from 'node:perf_hooks';
2
+ import {ROADY_CLIPS,renderRoady,renderStory,STORY_DURATION} from './character.mjs';
3
+ import {composeMessage} from '../../lucid/recovery/layout.mjs';
4
+ import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
5
+
6
+ /** One bounded performance. No alternate screen or full-screen clearing. */
7
+ export async function watch(paths,options,config){
8
+ if(!process.stdout.isTTY||!process.stdin.isTTY)throw new Error('--watch/--intro needs an interactive terminal. Use --scene and --at for snapshots.');
9
+ const {terminalCols,terminalRows,scene,large,message}=config;
10
+ const clip=scene?ROADY_CLIPS.find(c=>c.id===scene):null;
11
+ const duration=(clip?.duration??STORY_DURATION)+(config.arrival?6000:0);
12
+ const realCols=process.stdout.columns,realRows=process.stdout.rows;
13
+ const start=performance.now();
14
+ const render=t=>{
15
+ const r=scene?renderRoady(paths,scene,t,options):renderStory(paths,t,options);
16
+ if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
17
+ const caption=message??'ROADY · '+r.clip+'\n'+r.caption;
18
+ return large?[...cellsToAnsi(r.frame),...caption.split('\n')]:composeMessage(r.frame,caption,{terminalCols});
19
+ };
20
+ // Reserve enough lines for captions too; keep within the real terminal.
21
+ const height=options.rows+3;
22
+ if(height>=terminalRows)throw new Error('Animation needs room for its caption; reduce --rows.');
23
+ let ended=false,drawn=false,timer,resolveDone,rejectDone;
24
+ const wasRaw=Boolean(process.stdin.isRaw);
25
+ const done=new Promise((resolve,reject)=>{resolveDone=resolve;rejectDone=reject;});
26
+ const cleanup=(error)=>{
27
+ if(ended)return;ended=true;clearInterval(timer);
28
+ process.stdin.off('data',key);process.stdout.off('resize',resized);
29
+ process.off('SIGINT',sigint);process.off('SIGTERM',sigterm);
30
+ process.stdin.setRawMode(wasRaw);process.stdin.pause();
31
+ if(drawn)process.stdout.write(`\x1b[${height}B\r`);
32
+ process.stdout.write('\x1b[0m\x1b[?25h');
33
+ error?rejectDone(error):resolveDone();
34
+ };
35
+ const key=data=>{if(data.includes(3)||data.toString().includes('q'))cleanup();};
36
+ const sigint=()=>cleanup(),sigterm=()=>cleanup();
37
+ const resized=()=>cleanup(new Error('Terminal resized; rerun for the new art slot.'));
38
+ try{
39
+ process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on('data',key);
40
+ process.stdout.on('resize',resized);process.on('SIGINT',sigint);process.on('SIGTERM',sigterm);
41
+ process.stdout.write('\x1b[?25l');
42
+ // Allocate the block once; each frame replaces only these lines.
43
+ process.stdout.write('\n'.repeat(height)+`\x1b[${height}A`);drawn=true;
44
+ const tick=()=>{
45
+ try{
46
+ if(process.stdout.columns!==realCols||process.stdout.rows!==realRows)return resized();
47
+ const t=Math.min(performance.now()-start,duration-1),lines=render(t);
48
+ if(lines.length>height)throw new Error('Message exceeds the animation block; use a shorter message or a static preview.');
49
+ process.stdout.write(Array.from({length:height},(_,i)=>'\r\x1b[2K'+(lines[i]??'')+'\n').join('')+`\x1b[${height}A`);
50
+ if(performance.now()-start>=duration)cleanup();
51
+ }catch(e){cleanup(e);}
52
+ };
53
+ tick();if(!ended)timer=setInterval(tick,1000/6);
54
+ await done;
55
+ }catch(e){cleanup(e);await done;}
56
+ }