@jmoyers/harness 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/native/ptyd/Cargo.lock +16 -0
  4. package/native/ptyd/Cargo.toml +7 -0
  5. package/native/ptyd/src/main.rs +257 -0
  6. package/package.json +90 -0
  7. package/scripts/build-ptyd.sh +73 -0
  8. package/scripts/control-plane-daemon.ts +277 -0
  9. package/scripts/cursor-hook-relay.ts +82 -0
  10. package/scripts/harness-animate.ts +469 -0
  11. package/scripts/harness-bin.js +77 -0
  12. package/scripts/harness-core.ts +1 -0
  13. package/scripts/harness-inspector.ts +439 -0
  14. package/scripts/harness.ts +2493 -0
  15. package/src/adapters/agent-session-state.ts +390 -0
  16. package/src/cli/gateway-record.ts +173 -0
  17. package/src/codex/live-session.ts +872 -0
  18. package/src/config/config-core.ts +1359 -0
  19. package/src/config/secrets-core.ts +170 -0
  20. package/src/control-plane/agent-realtime-api.ts +2441 -0
  21. package/src/control-plane/codex-session-stream.ts +392 -0
  22. package/src/control-plane/codex-telemetry.ts +1325 -0
  23. package/src/control-plane/lifecycle-hooks.ts +706 -0
  24. package/src/control-plane/session-summary.ts +380 -0
  25. package/src/control-plane/status/agent-status-reducer.ts +21 -0
  26. package/src/control-plane/status/reducer-base.ts +170 -0
  27. package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
  28. package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
  29. package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
  30. package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
  31. package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
  32. package/src/control-plane/status/session-status-engine.ts +76 -0
  33. package/src/control-plane/stream-client.ts +396 -0
  34. package/src/control-plane/stream-command-parser.ts +1673 -0
  35. package/src/control-plane/stream-protocol.ts +1808 -0
  36. package/src/control-plane/stream-server-background.ts +486 -0
  37. package/src/control-plane/stream-server-command.ts +2557 -0
  38. package/src/control-plane/stream-server-connection.ts +234 -0
  39. package/src/control-plane/stream-server-observed-filter.ts +112 -0
  40. package/src/control-plane/stream-server-session-runtime.ts +566 -0
  41. package/src/control-plane/stream-server-state-store.ts +15 -0
  42. package/src/control-plane/stream-server.ts +3192 -0
  43. package/src/cursor/managed-hooks.ts +282 -0
  44. package/src/domain/conversations.ts +414 -0
  45. package/src/domain/directories.ts +78 -0
  46. package/src/domain/repositories.ts +123 -0
  47. package/src/domain/tasks.ts +148 -0
  48. package/src/domain/workspace.ts +156 -0
  49. package/src/events/normalized-events.ts +124 -0
  50. package/src/mux/ansi-integrity.ts +103 -0
  51. package/src/mux/control-plane-op-queue.ts +212 -0
  52. package/src/mux/conversation-rail.ts +339 -0
  53. package/src/mux/double-click.ts +78 -0
  54. package/src/mux/dual-pane-core.ts +435 -0
  55. package/src/mux/harness-core-ui.ts +817 -0
  56. package/src/mux/input-shortcuts.ts +667 -0
  57. package/src/mux/live-mux/actions-conversation.ts +344 -0
  58. package/src/mux/live-mux/actions-repository.ts +246 -0
  59. package/src/mux/live-mux/actions-task.ts +115 -0
  60. package/src/mux/live-mux/args.ts +142 -0
  61. package/src/mux/live-mux/command-menu.ts +298 -0
  62. package/src/mux/live-mux/control-plane-records.ts +546 -0
  63. package/src/mux/live-mux/conversation-state.ts +188 -0
  64. package/src/mux/live-mux/directory-resolution.ts +34 -0
  65. package/src/mux/live-mux/event-mapping.ts +96 -0
  66. package/src/mux/live-mux/gateway-profiler.ts +152 -0
  67. package/src/mux/live-mux/gateway-render-trace.ts +177 -0
  68. package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
  69. package/src/mux/live-mux/git-parsing.ts +131 -0
  70. package/src/mux/live-mux/git-snapshot.ts +263 -0
  71. package/src/mux/live-mux/git-state.ts +136 -0
  72. package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
  73. package/src/mux/live-mux/home-pane-actions.ts +58 -0
  74. package/src/mux/live-mux/home-pane-drop.ts +44 -0
  75. package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
  76. package/src/mux/live-mux/home-pane-pointer.ts +96 -0
  77. package/src/mux/live-mux/input-forwarding.ts +112 -0
  78. package/src/mux/live-mux/layout.ts +30 -0
  79. package/src/mux/live-mux/left-nav-activation.ts +103 -0
  80. package/src/mux/live-mux/left-nav.ts +85 -0
  81. package/src/mux/live-mux/left-rail-actions.ts +118 -0
  82. package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
  83. package/src/mux/live-mux/left-rail-pointer.ts +74 -0
  84. package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
  85. package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
  86. package/src/mux/live-mux/modal-input-reducers.ts +94 -0
  87. package/src/mux/live-mux/modal-overlays.ts +287 -0
  88. package/src/mux/live-mux/modal-pointer.ts +70 -0
  89. package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
  90. package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
  91. package/src/mux/live-mux/observed-stream.ts +87 -0
  92. package/src/mux/live-mux/palette-parsing.ts +128 -0
  93. package/src/mux/live-mux/pointer-routing.ts +108 -0
  94. package/src/mux/live-mux/process-usage.ts +53 -0
  95. package/src/mux/live-mux/project-pane-pointer.ts +44 -0
  96. package/src/mux/live-mux/rail-layout.ts +244 -0
  97. package/src/mux/live-mux/render-trace-analysis.ts +213 -0
  98. package/src/mux/live-mux/render-trace-state.ts +84 -0
  99. package/src/mux/live-mux/repository-folding.ts +207 -0
  100. package/src/mux/live-mux/runtime-shutdown.ts +51 -0
  101. package/src/mux/live-mux/selection.ts +411 -0
  102. package/src/mux/live-mux/startup-utils.ts +187 -0
  103. package/src/mux/live-mux/status-timeline-state.ts +82 -0
  104. package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
  105. package/src/mux/live-mux/terminal-palette.ts +79 -0
  106. package/src/mux/new-thread-prompt.ts +165 -0
  107. package/src/mux/project-tree.ts +295 -0
  108. package/src/mux/render-frame.ts +113 -0
  109. package/src/mux/runtime-wiring.ts +185 -0
  110. package/src/mux/selector-index.ts +160 -0
  111. package/src/mux/startup-sequencer.ts +238 -0
  112. package/src/mux/task-composer.ts +289 -0
  113. package/src/mux/task-focused-pane.ts +417 -0
  114. package/src/mux/task-screen-keybindings.ts +539 -0
  115. package/src/mux/terminal-input-modes.ts +35 -0
  116. package/src/mux/workspace-path.ts +55 -0
  117. package/src/mux/workspace-rail-model.ts +701 -0
  118. package/src/mux/workspace-rail.ts +247 -0
  119. package/src/perf/perf-core.ts +307 -0
  120. package/src/pty/pty_host.ts +217 -0
  121. package/src/pty/session-broker.ts +158 -0
  122. package/src/recording/terminal-recording.ts +383 -0
  123. package/src/services/control-plane.ts +567 -0
  124. package/src/services/conversation-lifecycle.ts +176 -0
  125. package/src/services/conversation-startup-hydration.ts +47 -0
  126. package/src/services/directory-hydration.ts +49 -0
  127. package/src/services/event-persistence.ts +104 -0
  128. package/src/services/mux-ui-state-persistence.ts +82 -0
  129. package/src/services/output-load-sampler.ts +231 -0
  130. package/src/services/process-usage-refresh.ts +88 -0
  131. package/src/services/recording.ts +75 -0
  132. package/src/services/render-trace-recorder.ts +177 -0
  133. package/src/services/runtime-control-actions.ts +123 -0
  134. package/src/services/runtime-control-plane-ops.ts +131 -0
  135. package/src/services/runtime-conversation-actions.ts +113 -0
  136. package/src/services/runtime-conversation-activation.ts +78 -0
  137. package/src/services/runtime-conversation-starter.ts +171 -0
  138. package/src/services/runtime-conversation-title-edit.ts +149 -0
  139. package/src/services/runtime-directory-actions.ts +164 -0
  140. package/src/services/runtime-envelope-handler.ts +198 -0
  141. package/src/services/runtime-git-state.ts +92 -0
  142. package/src/services/runtime-input-pipeline.ts +50 -0
  143. package/src/services/runtime-input-router.ts +202 -0
  144. package/src/services/runtime-layout-resize.ts +236 -0
  145. package/src/services/runtime-left-rail-render.ts +159 -0
  146. package/src/services/runtime-main-pane-input.ts +230 -0
  147. package/src/services/runtime-modal-input.ts +119 -0
  148. package/src/services/runtime-navigation-input.ts +207 -0
  149. package/src/services/runtime-process-wiring.ts +68 -0
  150. package/src/services/runtime-rail-input.ts +287 -0
  151. package/src/services/runtime-render-flush.ts +146 -0
  152. package/src/services/runtime-render-lifecycle.ts +104 -0
  153. package/src/services/runtime-render-orchestrator.ts +108 -0
  154. package/src/services/runtime-render-pipeline.ts +167 -0
  155. package/src/services/runtime-render-state.ts +72 -0
  156. package/src/services/runtime-repository-actions.ts +197 -0
  157. package/src/services/runtime-right-pane-render.ts +132 -0
  158. package/src/services/runtime-shutdown.ts +79 -0
  159. package/src/services/runtime-stream-subscriptions.ts +56 -0
  160. package/src/services/runtime-task-composer-persistence.ts +139 -0
  161. package/src/services/runtime-task-editor-actions.ts +83 -0
  162. package/src/services/runtime-task-pane-actions.ts +198 -0
  163. package/src/services/runtime-task-pane-shortcuts.ts +189 -0
  164. package/src/services/runtime-task-pane.ts +62 -0
  165. package/src/services/runtime-workspace-actions.ts +153 -0
  166. package/src/services/runtime-workspace-observed-events.ts +190 -0
  167. package/src/services/session-projection-instrumentation.ts +190 -0
  168. package/src/services/startup-background-probe.ts +91 -0
  169. package/src/services/startup-background-resume.ts +65 -0
  170. package/src/services/startup-orchestrator.ts +166 -0
  171. package/src/services/startup-output-tracker.ts +54 -0
  172. package/src/services/startup-paint-tracker.ts +115 -0
  173. package/src/services/startup-persisted-conversation-queue.ts +45 -0
  174. package/src/services/startup-settled-gate.ts +67 -0
  175. package/src/services/startup-shutdown.ts +53 -0
  176. package/src/services/startup-span-tracker.ts +77 -0
  177. package/src/services/startup-state-hydration.ts +94 -0
  178. package/src/services/startup-visibility.ts +35 -0
  179. package/src/services/status-timeline-recorder.ts +144 -0
  180. package/src/services/task-pane-selection-actions.ts +153 -0
  181. package/src/services/task-planning-hydration.ts +58 -0
  182. package/src/services/task-planning-observed-events.ts +89 -0
  183. package/src/services/workspace-observed-events.ts +113 -0
  184. package/src/store/control-plane-store-normalize.ts +760 -0
  185. package/src/store/control-plane-store-types.ts +224 -0
  186. package/src/store/control-plane-store.ts +2951 -0
  187. package/src/store/event-store.ts +253 -0
  188. package/src/store/sqlite.ts +81 -0
  189. package/src/terminal/compat-matrix.ts +345 -0
  190. package/src/terminal/differential-checkpoints.ts +132 -0
  191. package/src/terminal/parity-suite.ts +441 -0
  192. package/src/terminal/snapshot-oracle.ts +1840 -0
  193. package/src/ui/conversation-input-forwarder.ts +114 -0
  194. package/src/ui/conversation-selection-input.ts +103 -0
  195. package/src/ui/debug-footer-notice.ts +39 -0
  196. package/src/ui/global-shortcut-input.ts +126 -0
  197. package/src/ui/input-preflight.ts +68 -0
  198. package/src/ui/input-token-router.ts +312 -0
  199. package/src/ui/input.ts +238 -0
  200. package/src/ui/kit.ts +509 -0
  201. package/src/ui/left-nav-input.ts +80 -0
  202. package/src/ui/left-rail-pointer-input.ts +148 -0
  203. package/src/ui/main-pane-pointer-input.ts +150 -0
  204. package/src/ui/modals/manager.ts +192 -0
  205. package/src/ui/mux-theme.ts +529 -0
  206. package/src/ui/panes/conversation.ts +19 -0
  207. package/src/ui/panes/home-gridfire.ts +302 -0
  208. package/src/ui/panes/home.ts +109 -0
  209. package/src/ui/panes/left-rail.ts +12 -0
  210. package/src/ui/panes/project.ts +44 -0
  211. package/src/ui/pointer-routing-input.ts +158 -0
  212. package/src/ui/repository-fold-input.ts +91 -0
  213. package/src/ui/screen.ts +210 -0
  214. package/src/ui/surface.ts +224 -0
@@ -0,0 +1,469 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+
3
+ const TAU = Math.PI * 2;
4
+ const DEFAULT_FPS = 60;
5
+ const DEFAULT_SEED = 1337;
6
+ const MIN_WIDTH = 40;
7
+ const MIN_HEIGHT = 16;
8
+ const ANIMATE_COLOR_INDEX = 109;
9
+ const SHADING_CHARS = ' .,:;irsXA253hMHGS#9B&@';
10
+ const HARNESS_LOGO_LINES = [
11
+ ' _ _ _ ____ _ _ _____ ____ ____ ',
12
+ '| | | | / \\ | _ \\| \\ | | ____/ ___/ ___|',
13
+ '| |_| | / _ \\ | |_) | \\| | _| \\___ \\___ \\',
14
+ '| _ |/ ___ \\| _ <| |\\ | |___ ___) |__) |',
15
+ '|_| |_/_/ \\_\\_| \\_\\_| \\_|_____|____/____/',
16
+ ' HARNESS',
17
+ ];
18
+
19
+ interface AnimateOptions {
20
+ fps: number;
21
+ frames: number | null;
22
+ durationMs: number | null;
23
+ seed: number;
24
+ color: boolean;
25
+ }
26
+
27
+ interface TunnelState {
28
+ width: number;
29
+ height: number;
30
+ intensity: Float32Array;
31
+ chars: string[];
32
+ stars: StarField;
33
+ }
34
+
35
+ interface StarField {
36
+ x: Float32Array;
37
+ y: Float32Array;
38
+ z: Float32Array;
39
+ speed: Float32Array;
40
+ }
41
+
42
+ function readCliValue(argv: readonly string[], index: number, flag: string): string {
43
+ const value = argv[index + 1];
44
+ if (value === undefined) {
45
+ throw new Error(`missing value for ${flag}`);
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function parsePositiveIntFlag(value: string, flag: string): number {
51
+ const parsed = Number.parseInt(value, 10);
52
+ if (!Number.isInteger(parsed) || parsed <= 0) {
53
+ throw new Error(`invalid ${flag} value: ${value}`);
54
+ }
55
+ return parsed;
56
+ }
57
+
58
+ function parseAnimateOptions(argv: readonly string[]): AnimateOptions {
59
+ const options: AnimateOptions = {
60
+ fps: DEFAULT_FPS,
61
+ frames: null,
62
+ durationMs: null,
63
+ seed: DEFAULT_SEED,
64
+ color: true,
65
+ };
66
+ for (let index = 0; index < argv.length; index += 1) {
67
+ const arg = argv[index]!;
68
+ if (arg === '--fps') {
69
+ options.fps = parsePositiveIntFlag(readCliValue(argv, index, '--fps'), '--fps');
70
+ index += 1;
71
+ continue;
72
+ }
73
+ if (arg === '--frames') {
74
+ options.frames = parsePositiveIntFlag(readCliValue(argv, index, '--frames'), '--frames');
75
+ index += 1;
76
+ continue;
77
+ }
78
+ if (arg === '--duration-ms') {
79
+ options.durationMs = parsePositiveIntFlag(
80
+ readCliValue(argv, index, '--duration-ms'),
81
+ '--duration-ms',
82
+ );
83
+ index += 1;
84
+ continue;
85
+ }
86
+ if (arg === '--seed') {
87
+ options.seed = parsePositiveIntFlag(readCliValue(argv, index, '--seed'), '--seed');
88
+ index += 1;
89
+ continue;
90
+ }
91
+ if (arg === '--no-color') {
92
+ options.color = false;
93
+ continue;
94
+ }
95
+ throw new Error(`unknown animate option: ${arg}`);
96
+ }
97
+ return options;
98
+ }
99
+
100
+ function printUsage(): void {
101
+ process.stdout.write(
102
+ [
103
+ 'usage:',
104
+ ' harness animate [--fps <fps>] [--frames <count>] [--duration-ms <ms>] [--seed <seed>] [--no-color]',
105
+ '',
106
+ 'notes:',
107
+ ' - `harness animate` runs forever until Ctrl+C in a TTY.',
108
+ ' - In non-TTY mode, provide --frames or --duration-ms to avoid unbounded output.',
109
+ ].join('\n') + '\n',
110
+ );
111
+ }
112
+
113
+ function createMulberry32(seed: number): () => number {
114
+ let state = seed >>> 0;
115
+ return () => {
116
+ state = (state + 0x6d2b79f5) >>> 0;
117
+ let value = state;
118
+ value = Math.imul(value ^ (value >>> 15), value | 1);
119
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
120
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
121
+ };
122
+ }
123
+
124
+ function createStarField(count: number, random: () => number): StarField {
125
+ const stars: StarField = {
126
+ x: new Float32Array(count),
127
+ y: new Float32Array(count),
128
+ z: new Float32Array(count),
129
+ speed: new Float32Array(count),
130
+ };
131
+ for (let index = 0; index < count; index += 1) {
132
+ resetStar(stars, index, random);
133
+ }
134
+ return stars;
135
+ }
136
+
137
+ function resetStar(stars: StarField, index: number, random: () => number): void {
138
+ const angle = random() * TAU;
139
+ const radius = Math.pow(random(), 0.65);
140
+ stars.x[index] = Math.cos(angle) * radius;
141
+ stars.y[index] = Math.sin(angle) * radius * 0.6;
142
+ stars.z[index] = 0.2 + random() * 0.8;
143
+ stars.speed[index] = 0.24 + random() * 0.8;
144
+ }
145
+
146
+ function buildState(width: number, height: number, random: () => number): TunnelState {
147
+ const starCount = Math.max(120, Math.floor((width * height) / 18));
148
+ return {
149
+ width,
150
+ height,
151
+ intensity: new Float32Array(width * height),
152
+ chars: new Array<string>(width * height).fill(' '),
153
+ stars: createStarField(starCount, random),
154
+ };
155
+ }
156
+
157
+ function writePixel(
158
+ intensity: Float32Array,
159
+ width: number,
160
+ height: number,
161
+ x: number,
162
+ y: number,
163
+ value: number,
164
+ ): void {
165
+ if (x < 0 || y < 0 || x >= width || y >= height) {
166
+ return;
167
+ }
168
+ const index = y * width + x;
169
+ if (value > intensity[index]!) {
170
+ intensity[index] = value;
171
+ }
172
+ }
173
+
174
+ function drawTunnelLayer(state: TunnelState, elapsedSeconds: number): void {
175
+ const { width, height, intensity } = state;
176
+ const centerX = (width - 1) / 2;
177
+ const centerY = (height - 1) / 2;
178
+ const maxRadius = Math.min(width * 0.46, height * 0.95);
179
+ const layers = Math.max(34, Math.floor(Math.min(width, height) * 1.4));
180
+ const verticalScale = 0.62 + Math.sin(elapsedSeconds * 0.7) * 0.11;
181
+
182
+ for (let layer = 0; layer < layers; layer += 1) {
183
+ const depth = (elapsedSeconds * 0.48 + layer / layers) % 1;
184
+ const radius = Math.pow(depth, 1.45) * maxRadius;
185
+ const twist =
186
+ elapsedSeconds * 1.1 + layer * 0.27 + Math.sin(elapsedSeconds + layer * 0.08) * 0.33;
187
+ const samples = Math.max(22, Math.floor(radius * 5.3));
188
+ const brightness = 0.2 + (1 - depth) * 0.85;
189
+
190
+ for (let sample = 0; sample < samples; sample += 1) {
191
+ const angle = (sample / samples) * TAU + twist;
192
+ const x = Math.round(centerX + Math.cos(angle) * radius * 1.36);
193
+ const y = Math.round(centerY + Math.sin(angle) * radius * verticalScale);
194
+ writePixel(intensity, width, height, x, y, brightness);
195
+ if ((sample & 7) === 0) {
196
+ writePixel(intensity, width, height, x + 1, y, brightness * 0.72);
197
+ }
198
+ }
199
+ }
200
+
201
+ const spokes = 14;
202
+ for (let spoke = 0; spoke < spokes; spoke += 1) {
203
+ const baseAngle = (spoke / spokes) * TAU + elapsedSeconds * 0.94;
204
+ for (let depth = 0.18; depth < 1; depth += 0.14) {
205
+ const radius = Math.pow(depth, 1.3) * maxRadius;
206
+ const x = Math.round(centerX + Math.cos(baseAngle) * radius * 1.3);
207
+ const y = Math.round(centerY + Math.sin(baseAngle) * radius * verticalScale);
208
+ writePixel(intensity, width, height, x, y, 0.12 + (1 - depth) * 0.25);
209
+ }
210
+ }
211
+ }
212
+
213
+ function drawStars(state: TunnelState, elapsedSeconds: number, random: () => number): void {
214
+ const { width, height, intensity, stars } = state;
215
+ const centerX = (width - 1) / 2;
216
+ const centerY = (height - 1) / 2;
217
+ const depthScaleX = width * 0.62;
218
+ const depthScaleY = height * 0.58;
219
+ const pulse = 0.8 + Math.sin(elapsedSeconds * 3.1) * 0.2;
220
+ const count = stars.x.length;
221
+
222
+ for (let index = 0; index < count; index += 1) {
223
+ const depth = (stars.z[index] ?? 1) - (stars.speed[index] ?? 0.24) * 0.011;
224
+ stars.z[index] = depth;
225
+ if (depth <= 0.015) {
226
+ resetStar(stars, index, random);
227
+ continue;
228
+ }
229
+ const inverse = 1 / depth;
230
+ const x = Math.round(centerX + (stars.x[index] ?? 0) * inverse * depthScaleX);
231
+ const y = Math.round(centerY + (stars.y[index] ?? 0) * inverse * depthScaleY);
232
+ const value = (1 - depth) * 0.95 * pulse;
233
+ writePixel(intensity, width, height, x, y, value);
234
+ if (value > 0.6) {
235
+ writePixel(intensity, width, height, x + 1, y, value * 0.65);
236
+ }
237
+ }
238
+ }
239
+
240
+ function writeCenteredText(
241
+ chars: string[],
242
+ width: number,
243
+ height: number,
244
+ row: number,
245
+ text: string,
246
+ ): void {
247
+ if (row < 0 || row >= height || text.length === 0) {
248
+ return;
249
+ }
250
+ const startX = Math.floor((width - text.length) / 2);
251
+ for (let index = 0; index < text.length; index += 1) {
252
+ const x = startX + index;
253
+ if (x < 0 || x >= width) {
254
+ continue;
255
+ }
256
+ chars[row * width + x] = text[index]!;
257
+ }
258
+ }
259
+
260
+ function drawHarnessLogo(
261
+ chars: string[],
262
+ width: number,
263
+ height: number,
264
+ elapsedSeconds: number,
265
+ ): { top: number; bottom: number } {
266
+ const logoWidth = HARNESS_LOGO_LINES.reduce((max, line) => Math.max(max, line.length), 0);
267
+ const panelPaddingX = 2;
268
+ const panelPaddingY = 1;
269
+ const panelWidth = logoWidth + panelPaddingX * 2;
270
+ const panelHeight = HARNESS_LOGO_LINES.length + panelPaddingY * 2;
271
+ const wobble = Math.round(Math.sin(elapsedSeconds * 1.15) * 1);
272
+ const panelTop = Math.max(1, Math.floor(height * 0.56) - Math.floor(panelHeight / 2) + wobble);
273
+ const panelLeft = Math.floor((width - panelWidth) / 2);
274
+ const panelBottom = panelTop + panelHeight - 1;
275
+ const panelRight = panelLeft + panelWidth - 1;
276
+ const sweepCenter = Math.floor(((elapsedSeconds * 20) % (panelWidth + 20)) + panelLeft - 10);
277
+
278
+ for (let y = panelTop; y <= panelBottom; y += 1) {
279
+ if (y < 0 || y >= height) {
280
+ continue;
281
+ }
282
+ for (let x = panelLeft; x <= panelRight; x += 1) {
283
+ if (x < 0 || x >= width) {
284
+ continue;
285
+ }
286
+ chars[y * width + x] = ' ';
287
+ }
288
+ }
289
+
290
+ for (let y = panelTop; y <= panelBottom; y += 1) {
291
+ if (y < 0 || y >= height) {
292
+ continue;
293
+ }
294
+ for (let x = panelLeft; x <= panelRight; x += 1) {
295
+ if (x < 0 || x >= width) {
296
+ continue;
297
+ }
298
+ const isTop = y === panelTop;
299
+ const isBottom = y === panelBottom;
300
+ const isLeft = x === panelLeft;
301
+ const isRight = x === panelRight;
302
+ if (!(isTop || isBottom || isLeft || isRight)) {
303
+ continue;
304
+ }
305
+ if ((isTop || isBottom) && !isLeft && !isRight) {
306
+ chars[y * width + x] = Math.abs(x - sweepCenter) <= 1 ? '=' : '-';
307
+ continue;
308
+ }
309
+ chars[y * width + x] = isLeft || isRight ? '|' : '+';
310
+ }
311
+ }
312
+
313
+ const logoLeft = panelLeft + panelPaddingX;
314
+ const logoTop = panelTop + panelPaddingY;
315
+ for (let lineIndex = 0; lineIndex < HARNESS_LOGO_LINES.length; lineIndex += 1) {
316
+ const y = logoTop + lineIndex;
317
+ if (y < 0 || y >= height) {
318
+ continue;
319
+ }
320
+ const line = HARNESS_LOGO_LINES[lineIndex]!;
321
+ for (let index = 0; index < line.length; index += 1) {
322
+ const glyph = line[index]!;
323
+ if (glyph === ' ') {
324
+ continue;
325
+ }
326
+ const x = logoLeft + index;
327
+ if (x < 0 || x >= width) {
328
+ continue;
329
+ }
330
+ chars[y * width + x] = glyph;
331
+ }
332
+ }
333
+
334
+ return {
335
+ top: panelTop,
336
+ bottom: panelBottom,
337
+ };
338
+ }
339
+
340
+ function renderFrame(
341
+ state: TunnelState,
342
+ elapsedSeconds: number,
343
+ color: boolean,
344
+ random: () => number,
345
+ ): string {
346
+ const { width, height, intensity, chars } = state;
347
+ intensity.fill(0);
348
+ chars.fill(' ');
349
+
350
+ drawTunnelLayer(state, elapsedSeconds);
351
+ drawStars(state, elapsedSeconds, random);
352
+
353
+ const scanlineOffset = elapsedSeconds * 8.2;
354
+ const maxShadeIndex = SHADING_CHARS.length - 1;
355
+ for (let y = 0; y < height; y += 1) {
356
+ const scanline = 0.83 + Math.sin(scanlineOffset + y * 0.7) * 0.17;
357
+ for (let x = 0; x < width; x += 1) {
358
+ const index = y * width + x;
359
+ const value = Math.max(0, Math.min(1, intensity[index]! * scanline));
360
+ if (value < 0.02) {
361
+ continue;
362
+ }
363
+ const shade = Math.floor(value * maxShadeIndex);
364
+ chars[index] = SHADING_CHARS[shade] ?? SHADING_CHARS[maxShadeIndex]!;
365
+ }
366
+ }
367
+
368
+ const logo = drawHarnessLogo(chars, width, height, elapsedSeconds);
369
+ writeCenteredText(chars, width, height, logo.bottom + 1, 'HIGH FPS TERMINAL BENCH');
370
+ writeCenteredText(chars, width, height, logo.bottom + 2, 'CTRL+C TO EXIT');
371
+
372
+ const lines: string[] = [];
373
+ for (let y = 0; y < height; y += 1) {
374
+ const start = y * width;
375
+ lines.push(chars.slice(start, start + width).join(''));
376
+ }
377
+ const prefix = color ? `\u001b[H\u001b[38;5;${String(ANIMATE_COLOR_INDEX)}m` : '\u001b[H';
378
+ return `${prefix}${lines.join('\n')}${color ? '\u001b[0m' : ''}`;
379
+ }
380
+
381
+ export async function runHarnessAnimate(argv: readonly string[]): Promise<number> {
382
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
383
+ printUsage();
384
+ return 0;
385
+ }
386
+
387
+ const options = parseAnimateOptions(argv);
388
+ const hasBoundedRun = options.frames !== null || options.durationMs !== null;
389
+ const isTty = process.stdout.isTTY === true;
390
+ if (!isTty && !hasBoundedRun) {
391
+ throw new Error('harness animate requires a TTY or explicit --frames/--duration-ms bounds');
392
+ }
393
+
394
+ const random = createMulberry32(options.seed);
395
+ const targetIntervalMs = 1000 / options.fps;
396
+ let frameIndex = 0;
397
+ let stopSignal: NodeJS.Signals | null = null;
398
+ let stopRequested = false;
399
+
400
+ const onSigInt = (): void => {
401
+ stopSignal = 'SIGINT';
402
+ stopRequested = true;
403
+ };
404
+ const onSigTerm = (): void => {
405
+ stopSignal = 'SIGTERM';
406
+ stopRequested = true;
407
+ };
408
+ process.on('SIGINT', onSigInt);
409
+ process.on('SIGTERM', onSigTerm);
410
+
411
+ let usingAltScreen = false;
412
+ try {
413
+ if (isTty) {
414
+ process.stdout.write('\u001b[?1049h\u001b[2J\u001b[H\u001b[?25l');
415
+ usingAltScreen = true;
416
+ }
417
+
418
+ const startedAt = process.hrtime.bigint();
419
+ let state = buildState(
420
+ Math.max(MIN_WIDTH, process.stdout.columns ?? 0),
421
+ Math.max(MIN_HEIGHT, process.stdout.rows ?? 0),
422
+ random,
423
+ );
424
+ let nextFrameAt = Date.now();
425
+
426
+ while (!stopRequested) {
427
+ const frameStart = process.hrtime.bigint();
428
+ const elapsedSeconds = Number(frameStart - startedAt) / 1_000_000_000;
429
+ const width = Math.max(MIN_WIDTH, process.stdout.columns ?? state.width);
430
+ const height = Math.max(MIN_HEIGHT, process.stdout.rows ?? state.height);
431
+ if (width !== state.width || height !== state.height) {
432
+ state = buildState(width, height, random);
433
+ }
434
+ process.stdout.write(renderFrame(state, elapsedSeconds, options.color, random));
435
+ frameIndex += 1;
436
+
437
+ if (options.frames !== null && frameIndex >= options.frames) {
438
+ break;
439
+ }
440
+ if (options.durationMs !== null && elapsedSeconds * 1000 >= options.durationMs) {
441
+ break;
442
+ }
443
+
444
+ nextFrameAt += targetIntervalMs;
445
+ const waitMs = nextFrameAt - Date.now();
446
+ if (waitMs > 0) {
447
+ await delay(waitMs);
448
+ } else if (waitMs < -targetIntervalMs * 2) {
449
+ nextFrameAt = Date.now();
450
+ }
451
+ }
452
+ } finally {
453
+ process.off('SIGINT', onSigInt);
454
+ process.off('SIGTERM', onSigTerm);
455
+ if (usingAltScreen) {
456
+ process.stdout.write('\u001b[0m\u001b[?25h\u001b[?1049l');
457
+ } else {
458
+ process.stdout.write('\u001b[0m\n');
459
+ }
460
+ }
461
+
462
+ if (stopSignal === 'SIGINT') {
463
+ return 130;
464
+ }
465
+ if (stopSignal === 'SIGTERM') {
466
+ return 143;
467
+ }
468
+ return 0;
469
+ }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { spawn } from 'node:child_process';
5
+ import { dirname, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const LEGACY_LOCKFILES = [
9
+ 'package-lock.json',
10
+ 'pnpm-lock.yaml',
11
+ 'yarn.lock',
12
+ 'npm-shrinkwrap.json',
13
+ ];
14
+
15
+ function readPackageManager(packageJsonPath) {
16
+ try {
17
+ const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
18
+ if (typeof parsed.packageManager === 'string') {
19
+ return parsed.packageManager;
20
+ }
21
+ } catch {
22
+ return null;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ function findLegacyLockfiles(cwd) {
28
+ return LEGACY_LOCKFILES.filter((file) => existsSync(resolve(cwd, file)));
29
+ }
30
+
31
+ function maybePrintBunMigrationHint() {
32
+ if (process.env.HARNESS_SUPPRESS_BUN_MIGRATION_HINT === '1') {
33
+ return;
34
+ }
35
+ const cwd = process.cwd();
36
+ const packageJsonPath = resolve(cwd, 'package.json');
37
+ if (!existsSync(packageJsonPath)) {
38
+ return;
39
+ }
40
+ const packageManager = readPackageManager(packageJsonPath);
41
+ if (packageManager === null || packageManager.startsWith('bun@') === false) {
42
+ return;
43
+ }
44
+ const legacyLockfiles = findLegacyLockfiles(cwd);
45
+ if (legacyLockfiles.length === 0) {
46
+ return;
47
+ }
48
+ const lockfileList = legacyLockfiles.map((entry) => ` - ${entry}`).join('\n');
49
+ process.stderr.write(
50
+ `[harness] legacy package-manager lockfiles detected in ${cwd}:\n${lockfileList}\n[harness] run: bun run migrate:bun\n`,
51
+ );
52
+ }
53
+
54
+ maybePrintBunMigrationHint();
55
+
56
+ const here = dirname(fileURLToPath(import.meta.url));
57
+ const scriptPath = resolve(here, './harness.ts');
58
+ const runtimeArgs = [scriptPath, ...process.argv.slice(2)];
59
+ const child = spawn(process.execPath, runtimeArgs, {
60
+ stdio: 'inherit',
61
+ });
62
+
63
+ child.once('exit', (code, signal) => {
64
+ if (code !== null) {
65
+ process.exit(code);
66
+ return;
67
+ }
68
+ if (signal === 'SIGINT') {
69
+ process.exit(130);
70
+ return;
71
+ }
72
+ if (signal === 'SIGTERM') {
73
+ process.exit(143);
74
+ return;
75
+ }
76
+ process.exit(1);
77
+ });
@@ -0,0 +1 @@
1
+ await import('./codex-live-mux.ts');