@textjs/app 0.0.0-stage → 1.0.2

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 (335) hide show
  1. package/README.md +34 -2
  2. package/bin/text.js +29 -0
  3. package/bin/text.ts +13 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/cli/build.d.ts +39 -0
  6. package/dist/cli/build.d.ts.map +1 -0
  7. package/dist/cli/build.js +157 -0
  8. package/dist/cli/build.js.map +1 -0
  9. package/dist/cli/config.d.ts +22 -0
  10. package/dist/cli/config.d.ts.map +1 -0
  11. package/dist/cli/config.js +73 -0
  12. package/dist/cli/config.js.map +1 -0
  13. package/dist/cli/dev.d.ts +12 -0
  14. package/dist/cli/dev.d.ts.map +1 -0
  15. package/dist/cli/dev.js +49 -0
  16. package/dist/cli/dev.js.map +1 -0
  17. package/dist/cli/index.d.ts +26 -0
  18. package/dist/cli/index.d.ts.map +1 -0
  19. package/dist/cli/index.js +192 -0
  20. package/dist/cli/index.js.map +1 -0
  21. package/dist/cli/run.d.ts +31 -0
  22. package/dist/cli/run.d.ts.map +1 -0
  23. package/dist/cli/run.js +111 -0
  24. package/dist/cli/run.js.map +1 -0
  25. package/dist/cli/start.d.ts +14 -0
  26. package/dist/cli/start.d.ts.map +1 -0
  27. package/dist/cli/start.js +183 -0
  28. package/dist/cli/start.js.map +1 -0
  29. package/dist/cli/ui.d.ts +35 -0
  30. package/dist/cli/ui.d.ts.map +1 -0
  31. package/dist/cli/ui.js +73 -0
  32. package/dist/cli/ui.js.map +1 -0
  33. package/dist/client.d.ts +34 -0
  34. package/dist/client.d.ts.map +1 -0
  35. package/dist/client.js +93 -0
  36. package/dist/client.js.map +1 -0
  37. package/dist/config.d.ts +143 -0
  38. package/dist/config.d.ts.map +1 -0
  39. package/dist/config.js +28 -0
  40. package/dist/config.js.map +1 -0
  41. package/dist/context.d.ts +44 -0
  42. package/dist/context.d.ts.map +1 -0
  43. package/dist/context.js +26 -0
  44. package/dist/context.js.map +1 -0
  45. package/dist/dev-errors.d.ts +68 -0
  46. package/dist/dev-errors.d.ts.map +1 -0
  47. package/dist/dev-errors.js +121 -0
  48. package/dist/dev-errors.js.map +1 -0
  49. package/dist/devtools.d.ts +64 -0
  50. package/dist/devtools.d.ts.map +1 -0
  51. package/dist/devtools.js +186 -0
  52. package/dist/devtools.js.map +1 -0
  53. package/dist/frame-mirror.d.ts +19 -0
  54. package/dist/frame-mirror.d.ts.map +1 -0
  55. package/dist/frame-mirror.js +141 -0
  56. package/dist/frame-mirror.js.map +1 -0
  57. package/dist/frame-protocol.d.ts +175 -0
  58. package/dist/frame-protocol.d.ts.map +1 -0
  59. package/dist/frame-protocol.js +369 -0
  60. package/dist/frame-protocol.js.map +1 -0
  61. package/dist/history.d.ts +54 -0
  62. package/dist/history.d.ts.map +1 -0
  63. package/dist/history.js +152 -0
  64. package/dist/history.js.map +1 -0
  65. package/dist/index.d.ts +9 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +8 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/link.d.ts +32 -0
  70. package/dist/link.d.ts.map +1 -0
  71. package/dist/link.js +54 -0
  72. package/dist/link.js.map +1 -0
  73. package/dist/live-player.d.ts +83 -0
  74. package/dist/live-player.d.ts.map +1 -0
  75. package/dist/live-player.js +413 -0
  76. package/dist/live-player.js.map +1 -0
  77. package/dist/live-protocol.d.ts +132 -0
  78. package/dist/live-protocol.d.ts.map +1 -0
  79. package/dist/live-protocol.js +278 -0
  80. package/dist/live-protocol.js.map +1 -0
  81. package/dist/live-stub.d.ts +10 -0
  82. package/dist/live-stub.d.ts.map +1 -0
  83. package/dist/live-stub.js +32 -0
  84. package/dist/live-stub.js.map +1 -0
  85. package/dist/mcp/cli.d.ts +22 -0
  86. package/dist/mcp/cli.d.ts.map +1 -0
  87. package/dist/mcp/cli.js +177 -0
  88. package/dist/mcp/cli.js.map +1 -0
  89. package/dist/mcp/frame.d.ts +58 -0
  90. package/dist/mcp/frame.d.ts.map +1 -0
  91. package/dist/mcp/frame.js +169 -0
  92. package/dist/mcp/frame.js.map +1 -0
  93. package/dist/mcp/index.d.ts +15 -0
  94. package/dist/mcp/index.d.ts.map +1 -0
  95. package/dist/mcp/index.js +15 -0
  96. package/dist/mcp/index.js.map +1 -0
  97. package/dist/mcp/inspect.d.ts +95 -0
  98. package/dist/mcp/inspect.d.ts.map +1 -0
  99. package/dist/mcp/inspect.js +386 -0
  100. package/dist/mcp/inspect.js.map +1 -0
  101. package/dist/mcp/keys.d.ts +20 -0
  102. package/dist/mcp/keys.d.ts.map +1 -0
  103. package/dist/mcp/keys.js +87 -0
  104. package/dist/mcp/keys.js.map +1 -0
  105. package/dist/mcp/logs.d.ts +63 -0
  106. package/dist/mcp/logs.d.ts.map +1 -0
  107. package/dist/mcp/logs.js +217 -0
  108. package/dist/mcp/logs.js.map +1 -0
  109. package/dist/mcp/server.d.ts +64 -0
  110. package/dist/mcp/server.d.ts.map +1 -0
  111. package/dist/mcp/server.js +485 -0
  112. package/dist/mcp/server.js.map +1 -0
  113. package/dist/mcp/session.d.ts +230 -0
  114. package/dist/mcp/session.d.ts.map +1 -0
  115. package/dist/mcp/session.js +742 -0
  116. package/dist/mcp/session.js.map +1 -0
  117. package/dist/navigation-errors.d.ts +28 -0
  118. package/dist/navigation-errors.d.ts.map +1 -0
  119. package/dist/navigation-errors.js +43 -0
  120. package/dist/navigation-errors.js.map +1 -0
  121. package/dist/navigation.d.ts +21 -0
  122. package/dist/navigation.d.ts.map +1 -0
  123. package/dist/navigation.js +33 -0
  124. package/dist/navigation.js.map +1 -0
  125. package/dist/router.d.ts +76 -0
  126. package/dist/router.d.ts.map +1 -0
  127. package/dist/router.js +296 -0
  128. package/dist/router.js.map +1 -0
  129. package/dist/routing.d.ts +101 -0
  130. package/dist/routing.d.ts.map +1 -0
  131. package/dist/routing.js +286 -0
  132. package/dist/routing.js.map +1 -0
  133. package/dist/server/adapters/build-step.d.ts +34 -0
  134. package/dist/server/adapters/build-step.d.ts.map +1 -0
  135. package/dist/server/adapters/build-step.js +112 -0
  136. package/dist/server/adapters/build-step.js.map +1 -0
  137. package/dist/server/adapters/bun.d.ts +8 -0
  138. package/dist/server/adapters/bun.d.ts.map +1 -0
  139. package/dist/server/adapters/bun.js +89 -0
  140. package/dist/server/adapters/bun.js.map +1 -0
  141. package/dist/server/adapters/bundle.d.ts +21 -0
  142. package/dist/server/adapters/bundle.d.ts.map +1 -0
  143. package/dist/server/adapters/bundle.js +86 -0
  144. package/dist/server/adapters/bundle.js.map +1 -0
  145. package/dist/server/adapters/cloudflare.d.ts +29 -0
  146. package/dist/server/adapters/cloudflare.d.ts.map +1 -0
  147. package/dist/server/adapters/cloudflare.js +105 -0
  148. package/dist/server/adapters/cloudflare.js.map +1 -0
  149. package/dist/server/adapters/index.d.ts +10 -0
  150. package/dist/server/adapters/index.d.ts.map +1 -0
  151. package/dist/server/adapters/index.js +24 -0
  152. package/dist/server/adapters/index.js.map +1 -0
  153. package/dist/server/adapters/runtime-node.d.ts +16 -0
  154. package/dist/server/adapters/runtime-node.d.ts.map +1 -0
  155. package/dist/server/adapters/runtime-node.js +29 -0
  156. package/dist/server/adapters/runtime-node.js.map +1 -0
  157. package/dist/server/adapters/runtime.d.ts +42 -0
  158. package/dist/server/adapters/runtime.d.ts.map +1 -0
  159. package/dist/server/adapters/runtime.js +115 -0
  160. package/dist/server/adapters/runtime.js.map +1 -0
  161. package/dist/server/adapters/shared.d.ts +66 -0
  162. package/dist/server/adapters/shared.d.ts.map +1 -0
  163. package/dist/server/adapters/shared.js +124 -0
  164. package/dist/server/adapters/shared.js.map +1 -0
  165. package/dist/server/adapters/static.d.ts +9 -0
  166. package/dist/server/adapters/static.d.ts.map +1 -0
  167. package/dist/server/adapters/static.js +70 -0
  168. package/dist/server/adapters/static.js.map +1 -0
  169. package/dist/server/adapters/vercel.d.ts +43 -0
  170. package/dist/server/adapters/vercel.d.ts.map +1 -0
  171. package/dist/server/adapters/vercel.js +119 -0
  172. package/dist/server/adapters/vercel.js.map +1 -0
  173. package/dist/server/bun-jsx.d.ts +18 -0
  174. package/dist/server/bun-jsx.d.ts.map +1 -0
  175. package/dist/server/bun-jsx.js +56 -0
  176. package/dist/server/bun-jsx.js.map +1 -0
  177. package/dist/server/cache-route.d.ts +38 -0
  178. package/dist/server/cache-route.d.ts.map +1 -0
  179. package/dist/server/cache-route.js +109 -0
  180. package/dist/server/cache-route.js.map +1 -0
  181. package/dist/server/cache.d.ts +131 -0
  182. package/dist/server/cache.d.ts.map +1 -0
  183. package/dist/server/cache.js +409 -0
  184. package/dist/server/cache.js.map +1 -0
  185. package/dist/server/frames.d.ts +45 -0
  186. package/dist/server/frames.d.ts.map +1 -0
  187. package/dist/server/frames.js +294 -0
  188. package/dist/server/frames.js.map +1 -0
  189. package/dist/server/http.d.ts +80 -0
  190. package/dist/server/http.d.ts.map +1 -0
  191. package/dist/server/http.js +453 -0
  192. package/dist/server/http.js.map +1 -0
  193. package/dist/server/jsx-dev-runtime.d.ts +5 -0
  194. package/dist/server/jsx-dev-runtime.d.ts.map +1 -0
  195. package/dist/server/jsx-dev-runtime.js +6 -0
  196. package/dist/server/jsx-dev-runtime.js.map +1 -0
  197. package/dist/server/jsx-runtime.d.ts +10 -0
  198. package/dist/server/jsx-runtime.d.ts.map +1 -0
  199. package/dist/server/jsx-runtime.js +11 -0
  200. package/dist/server/jsx-runtime.js.map +1 -0
  201. package/dist/server/live-app.d.ts +87 -0
  202. package/dist/server/live-app.d.ts.map +1 -0
  203. package/dist/server/live-app.js +307 -0
  204. package/dist/server/live-app.js.map +1 -0
  205. package/dist/server/live-build.d.ts +70 -0
  206. package/dist/server/live-build.d.ts.map +1 -0
  207. package/dist/server/live-build.js +222 -0
  208. package/dist/server/live-build.js.map +1 -0
  209. package/dist/server/live-dev.d.ts +62 -0
  210. package/dist/server/live-dev.d.ts.map +1 -0
  211. package/dist/server/live-dev.js +313 -0
  212. package/dist/server/live-dev.js.map +1 -0
  213. package/dist/server/live-manifest.d.ts +23 -0
  214. package/dist/server/live-manifest.d.ts.map +1 -0
  215. package/dist/server/live-manifest.js +7 -0
  216. package/dist/server/live-manifest.js.map +1 -0
  217. package/dist/server/live-prod.d.ts +30 -0
  218. package/dist/server/live-prod.d.ts.map +1 -0
  219. package/dist/server/live-prod.js +97 -0
  220. package/dist/server/live-prod.js.map +1 -0
  221. package/dist/server/live-routes.d.ts +72 -0
  222. package/dist/server/live-routes.d.ts.map +1 -0
  223. package/dist/server/live-routes.js +274 -0
  224. package/dist/server/live-routes.js.map +1 -0
  225. package/dist/server/live-web.d.ts +16 -0
  226. package/dist/server/live-web.d.ts.map +1 -0
  227. package/dist/server/live-web.js +159 -0
  228. package/dist/server/live-web.js.map +1 -0
  229. package/dist/server/live.d.ts +138 -0
  230. package/dist/server/live.d.ts.map +1 -0
  231. package/dist/server/live.js +542 -0
  232. package/dist/server/live.js.map +1 -0
  233. package/dist/server/middleware-node.d.ts +48 -0
  234. package/dist/server/middleware-node.d.ts.map +1 -0
  235. package/dist/server/middleware-node.js +167 -0
  236. package/dist/server/middleware-node.js.map +1 -0
  237. package/dist/server/middleware-runtime.d.ts +97 -0
  238. package/dist/server/middleware-runtime.d.ts.map +1 -0
  239. package/dist/server/middleware-runtime.js +447 -0
  240. package/dist/server/middleware-runtime.js.map +1 -0
  241. package/dist/server/middleware.d.ts +145 -0
  242. package/dist/server/middleware.d.ts.map +1 -0
  243. package/dist/server/middleware.js +365 -0
  244. package/dist/server/middleware.js.map +1 -0
  245. package/dist/server/ppr.d.ts +22 -0
  246. package/dist/server/ppr.d.ts.map +1 -0
  247. package/dist/server/ppr.js +58 -0
  248. package/dist/server/ppr.js.map +1 -0
  249. package/dist/server/scan.d.ts +48 -0
  250. package/dist/server/scan.d.ts.map +1 -0
  251. package/dist/server/scan.js +161 -0
  252. package/dist/server/scan.js.map +1 -0
  253. package/dist/server/server-components.d.ts +65 -0
  254. package/dist/server/server-components.d.ts.map +1 -0
  255. package/dist/server/server-components.js +215 -0
  256. package/dist/server/server-components.js.map +1 -0
  257. package/dist/terminal.d.ts +5 -0
  258. package/dist/terminal.d.ts.map +1 -0
  259. package/dist/terminal.js +5 -0
  260. package/dist/terminal.js.map +1 -0
  261. package/dist/testing.d.ts +5 -0
  262. package/dist/testing.d.ts.map +1 -0
  263. package/dist/testing.js +5 -0
  264. package/dist/testing.js.map +1 -0
  265. package/dist/vite-plugin.d.ts +51 -0
  266. package/dist/vite-plugin.d.ts.map +1 -0
  267. package/dist/vite-plugin.js +403 -0
  268. package/dist/vite-plugin.js.map +1 -0
  269. package/package.json +130 -4
  270. package/src/cli/build.ts +182 -0
  271. package/src/cli/config.ts +72 -0
  272. package/src/cli/dev.ts +54 -0
  273. package/src/cli/index.ts +185 -0
  274. package/src/cli/run.ts +124 -0
  275. package/src/cli/start.ts +194 -0
  276. package/src/cli/ui.ts +93 -0
  277. package/src/client.tsx +122 -0
  278. package/src/config.ts +155 -0
  279. package/src/context.ts +69 -0
  280. package/src/dev-errors.ts +167 -0
  281. package/src/devtools.tsx +419 -0
  282. package/src/frame-mirror.ts +145 -0
  283. package/src/frame-protocol.ts +435 -0
  284. package/src/history.ts +201 -0
  285. package/src/index.ts +9 -0
  286. package/src/link.tsx +108 -0
  287. package/src/live-player.ts +449 -0
  288. package/src/live-protocol.ts +317 -0
  289. package/src/live-stub.tsx +32 -0
  290. package/src/mcp/cli.ts +188 -0
  291. package/src/mcp/frame.ts +224 -0
  292. package/src/mcp/index.ts +15 -0
  293. package/src/mcp/inspect.ts +445 -0
  294. package/src/mcp/keys.ts +86 -0
  295. package/src/mcp/logs.ts +225 -0
  296. package/src/mcp/server.ts +595 -0
  297. package/src/mcp/session.ts +821 -0
  298. package/src/navigation-errors.ts +60 -0
  299. package/src/navigation.ts +44 -0
  300. package/src/router.tsx +497 -0
  301. package/src/routing.ts +344 -0
  302. package/src/server/adapters/build-step.ts +135 -0
  303. package/src/server/adapters/bun.ts +92 -0
  304. package/src/server/adapters/bundle.ts +97 -0
  305. package/src/server/adapters/cloudflare.ts +111 -0
  306. package/src/server/adapters/index.ts +29 -0
  307. package/src/server/adapters/runtime-node.ts +34 -0
  308. package/src/server/adapters/runtime.ts +145 -0
  309. package/src/server/adapters/shared.ts +159 -0
  310. package/src/server/adapters/static.ts +68 -0
  311. package/src/server/adapters/vercel.ts +149 -0
  312. package/src/server/bun-jsx.ts +65 -0
  313. package/src/server/cache-route.ts +139 -0
  314. package/src/server/cache.ts +498 -0
  315. package/src/server/frames.ts +326 -0
  316. package/src/server/http.ts +467 -0
  317. package/src/server/jsx-dev-runtime.ts +9 -0
  318. package/src/server/jsx-runtime.ts +13 -0
  319. package/src/server/live-app.tsx +374 -0
  320. package/src/server/live-build.ts +258 -0
  321. package/src/server/live-dev.ts +357 -0
  322. package/src/server/live-manifest.ts +25 -0
  323. package/src/server/live-prod.ts +120 -0
  324. package/src/server/live-routes.ts +298 -0
  325. package/src/server/live-web.ts +170 -0
  326. package/src/server/live.ts +551 -0
  327. package/src/server/middleware-node.ts +168 -0
  328. package/src/server/middleware-runtime.ts +478 -0
  329. package/src/server/middleware.ts +451 -0
  330. package/src/server/ppr.ts +62 -0
  331. package/src/server/scan.ts +190 -0
  332. package/src/server/server-components.ts +224 -0
  333. package/src/terminal.ts +5 -0
  334. package/src/testing.ts +5 -0
  335. package/src/vite-plugin.ts +413 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Key input for the `press` tool: `"ctrl+k"`, `"down down enter"`,
3
+ * `["tab", "shift+tab"]`, `"down*3"` (repeat). Every combo is validated before
4
+ * any key is sent, so a typo never leaves the app half-driven.
5
+ */
6
+
7
+ import { txError, type KeyInputEvent } from "@textjs/core";
8
+ import { keyEventFromCombo, parseKeys } from "@textjs/react";
9
+
10
+ /** Upper bound on keys per `press` call (guards against `down*100000`). */
11
+ export const MAX_KEYS_PER_CALL = 500;
12
+
13
+ export class KeySpecError extends Error {
14
+ readonly code: string | undefined;
15
+ constructor(message: string, code?: string) {
16
+ super(message);
17
+ this.name = "KeySpecError";
18
+ this.code = code;
19
+ }
20
+ }
21
+
22
+ function splitSteps(spec: string): string[] {
23
+ const trimmed = spec.trim();
24
+ // A bare " " means the space bar, matching `@textjs/react/testing`.
25
+ if (trimmed === "") return spec.length > 0 ? ["space"] : [];
26
+ return trimmed.split(/\s+/);
27
+ }
28
+
29
+ /** Expand `keys` into validated key events, in order. */
30
+ export function parseKeyInput(keys: string | readonly string[]): KeyInputEvent[] {
31
+ const steps = (typeof keys === "string" ? [keys] : keys).flatMap(splitSteps);
32
+ const out: KeyInputEvent[] = [];
33
+ for (const step of steps) {
34
+ const rep = /^(.+?)\*(\d+)$/.exec(step);
35
+ const combo = rep ? (rep[1] as string) : step;
36
+ const count = rep ? Number(rep[2]) : 1;
37
+ if (count < 1) throw new KeySpecError(`Repeat count must be at least 1 in "${step}".`);
38
+ let event: KeyInputEvent;
39
+ try {
40
+ // `"a,b"` would parse as alternatives; a press is always one concrete stroke.
41
+ if (combo !== "," && parseKeys(combo).length !== 1) throw txError("TX009", combo);
42
+ event = keyEventFromCombo(combo);
43
+ } catch (error) {
44
+ const message = error instanceof Error ? error.message : String(error);
45
+ const code = (error as { code?: unknown }).code;
46
+ throw new KeySpecError(message, typeof code === "string" ? code : undefined);
47
+ }
48
+ if (out.length + count > MAX_KEYS_PER_CALL) throw new KeySpecError(`Too many keys in one call (max ${MAX_KEYS_PER_CALL}).`);
49
+ for (let i = 0; i < count; i++) out.push({ ...event });
50
+ }
51
+ if (out.length === 0) throw new KeySpecError("No keys given. Pass combos like \"down\", \"ctrl+k\" or \"tab tab enter\".");
52
+ return out;
53
+ }
54
+
55
+ const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
56
+
57
+ /**
58
+ * Key events that type `text` one grapheme at a time (`\n` presses return,
59
+ * `\t` tab), so an emoji with modifiers or a ZWJ sequence is one keystroke.
60
+ */
61
+ export function textToKeyEvents(text: string): KeyInputEvent[] {
62
+ const out: KeyInputEvent[] = [];
63
+ for (const { segment: ch } of graphemes.segment(text.replace(/\r\n?/g, "\n"))) {
64
+ if (ch === "\n") {
65
+ out.push(keyEventFromCombo("return"));
66
+ continue;
67
+ }
68
+ if (ch === "\t") {
69
+ out.push(keyEventFromCombo("tab"));
70
+ continue;
71
+ }
72
+ const upper = ch.length === 1 && ch >= "A" && ch <= "Z";
73
+ out.push({
74
+ type: "key",
75
+ key: ch === " " ? "space" : ch.toLowerCase(),
76
+ input: ch,
77
+ code: "",
78
+ repeat: false,
79
+ ctrl: false,
80
+ meta: false,
81
+ shift: upper,
82
+ alt: false,
83
+ });
84
+ }
85
+ return out;
86
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Console capture for `text mcp`. On the stdio transport stdout carries
3
+ * JSON-RPC, so app output must never reach it: every `console.*` call is
4
+ * recorded into a bounded ring buffer that agents read with the `logs` tool.
5
+ */
6
+
7
+ import { formatWithOptions } from "node:util";
8
+
9
+ export type LogLevel = "log" | "info" | "warn" | "error" | "debug";
10
+
11
+ export interface LogEntry {
12
+ /** Monotonic sequence number (stable across `logs` calls; use it as a cursor). */
13
+ seq: number;
14
+ /** Milliseconds since the session started. */
15
+ t: number;
16
+ level: LogLevel;
17
+ /** Where the entry came from: app `console.*`, a React error callback, or the host process. */
18
+ source: "console" | "react" | "process" | "server";
19
+ message: string;
20
+ /** `TX###` code when the logged error carries one. */
21
+ code?: string;
22
+ }
23
+
24
+ const MAX_MESSAGE = 8_000;
25
+
26
+ /** `TX###` code of an error-like value (duck-typed so this works with any Text.js version). */
27
+ export function txCodeOf(value: unknown): string | undefined {
28
+ if (value === null || typeof value !== "object") return undefined;
29
+ const code = (value as { code?: unknown }).code;
30
+ if (typeof code === "string" && /^TX\d{3}$/.test(code)) return code;
31
+ const message = (value as { message?: unknown }).message;
32
+ if (typeof message === "string") return /\b(TX\d{3})\b/.exec(message)?.[1];
33
+ return undefined;
34
+ }
35
+
36
+ /**
37
+ * Format console arguments like Node's `util.format`, dropping `%c` styling
38
+ * (the browser console's styled-span syntax) together with the style argument
39
+ * it consumes. Directives are scanned left to right, so `%%c` stays a literal
40
+ * `%c` and `%s` / `%c` mixes consume arguments in order.
41
+ */
42
+ export function formatLogArgs(args: readonly unknown[]): string {
43
+ let list = args;
44
+ const fmt = list[0];
45
+ if (typeof fmt === "string" && fmt.includes("%c")) {
46
+ const rest = list.slice(1);
47
+ const kept: unknown[] = [];
48
+ let next = 0;
49
+ const out = fmt.replace(/%[csdifjoO%]/g, (token) => {
50
+ if (token === "%%") return token;
51
+ if (next >= rest.length) return token;
52
+ const arg = rest[next++];
53
+ if (token === "%c") return "";
54
+ kept.push(arg);
55
+ return token;
56
+ });
57
+ list = [out, ...kept, ...rest.slice(next)];
58
+ }
59
+ const text = formatWithOptions({ colors: false, depth: 4, breakLength: 120 }, ...list);
60
+ return text.length > MAX_MESSAGE ? `${text.slice(0, MAX_MESSAGE)}… (${text.length - MAX_MESSAGE} more chars)` : text;
61
+ }
62
+
63
+ export class LogBuffer {
64
+ private readonly entries: LogEntry[] = [];
65
+ private seq = 0;
66
+ private readonly started = Date.now();
67
+ private readonly listeners = new Set<(entry: LogEntry) => void>();
68
+
69
+ constructor(private readonly capacity = 1_000) {}
70
+
71
+ /** Record an entry; `indent` is the `console.group` depth (two spaces per level). */
72
+ push(level: LogLevel, source: LogEntry["source"], args: readonly unknown[], indent = 0): LogEntry {
73
+ const code = args.map(txCodeOf).find((c) => c !== undefined);
74
+ const text = formatLogArgs(args);
75
+ const message = indent > 0 ? text.replace(/^/gm, " ".repeat(indent)) : text;
76
+ const entry: LogEntry = { seq: ++this.seq, t: Date.now() - this.started, level, source, message, ...(code ? { code } : {}) };
77
+ this.entries.push(entry);
78
+ if (this.entries.length > this.capacity) this.entries.splice(0, this.entries.length - this.capacity);
79
+ for (const l of this.listeners) l(entry);
80
+ return entry;
81
+ }
82
+
83
+ /** Entries with `seq > since`, optionally filtered by level, newest last. */
84
+ read(options: { since?: number; levels?: readonly LogLevel[]; limit?: number } = {}): LogEntry[] {
85
+ const { since = 0, levels, limit = 200 } = options;
86
+ const out = this.entries.filter((e) => e.seq > since && (!levels || levels.includes(e.level)));
87
+ return limit > 0 && out.length > limit ? out.slice(out.length - limit) : out;
88
+ }
89
+
90
+ /** Highest sequence number issued so far. */
91
+ get cursor(): number {
92
+ return this.seq;
93
+ }
94
+
95
+ /** Errors logged after `since`. */
96
+ errorsSince(since: number): number {
97
+ let n = 0;
98
+ for (const e of this.entries) if (e.seq > since && e.level === "error") n++;
99
+ return n;
100
+ }
101
+
102
+ clear(): void {
103
+ this.entries.length = 0;
104
+ }
105
+
106
+ onEntry(listener: (entry: LogEntry) => void): () => void {
107
+ this.listeners.add(listener);
108
+ return () => this.listeners.delete(listener);
109
+ }
110
+ }
111
+
112
+ export interface ConsoleCapture {
113
+ restore(): void;
114
+ }
115
+
116
+ type ConsoleFn = (...args: unknown[]) => unknown;
117
+
118
+ /**
119
+ * Route every `console` method into `buffer`. `mirror` additionally receives
120
+ * each entry (e.g. stderr when `--verbose`); stdout is never written.
121
+ *
122
+ * All methods are replaced, not just `log`/`info`/`warn`/`error`/`debug`: Bun's
123
+ * native `console.count`, `group`, `table`, `dir`, `write` (and Node's) print
124
+ * straight to file descriptor 1, which would corrupt a stdio JSON-RPC stream,
125
+ * and `assert` / `timeLog` / `timeEnd` would bypass the buffer.
126
+ */
127
+ export function captureConsole(buffer: LogBuffer, mirror?: (entry: LogEntry) => void): ConsoleCapture {
128
+ const target = console as unknown as Record<string, unknown>;
129
+ let depth = 0;
130
+ const counts = new Map<string, number>();
131
+ const timers = new Map<string, number>();
132
+ const push = (level: LogLevel, args: readonly unknown[]): void => {
133
+ const entry = buffer.push(level, "console", args, depth);
134
+ mirror?.(entry);
135
+ };
136
+ const label = (v: unknown): string => (v === undefined ? "default" : String(v));
137
+ const elapsed = (name: string): string | null => {
138
+ const start = timers.get(name);
139
+ if (start === undefined) return null;
140
+ return `${name}: ${(performance.now() - start).toFixed(3)}ms`;
141
+ };
142
+
143
+ const handlers: Record<string, ConsoleFn> = {
144
+ log: (...a) => push("log", a),
145
+ info: (...a) => push("info", a),
146
+ warn: (...a) => push("warn", a),
147
+ error: (...a) => push("error", a),
148
+ debug: (...a) => push("debug", a),
149
+ dir: (obj) => push("log", [obj]),
150
+ dirxml: (...a) => push("log", a),
151
+ table: (data, columns) => push("log", columns === undefined ? [data] : [data, columns]),
152
+ trace: (...a) => {
153
+ const stack = new Error().stack?.split("\n").slice(2).join("\n") ?? "";
154
+ push("debug", [`Trace${a.length > 0 ? `: ${formatLogArgs(a)}` : ""}${stack ? `\n${stack}` : ""}`]);
155
+ },
156
+ assert: (condition, ...a) => {
157
+ if (condition) return;
158
+ push("error", a.length > 0 ? ["Assertion failed:", ...a] : ["Assertion failed"]);
159
+ },
160
+ count: (name) => {
161
+ const key = label(name);
162
+ const n = (counts.get(key) ?? 0) + 1;
163
+ counts.set(key, n);
164
+ push("log", [`${key}: ${n}`]);
165
+ },
166
+ countReset: (name) => void counts.set(label(name), 0),
167
+ group: (...a) => {
168
+ if (a.length > 0) push("log", a);
169
+ depth++;
170
+ },
171
+ groupCollapsed: (...a) => {
172
+ if (a.length > 0) push("log", a);
173
+ depth++;
174
+ },
175
+ groupEnd: () => {
176
+ depth = Math.max(0, depth - 1);
177
+ },
178
+ time: (name) => void timers.set(label(name), performance.now()),
179
+ timeLog: (name, ...a) => {
180
+ const line = elapsed(label(name));
181
+ if (line === null) push("warn", [`Timer "${label(name)}" does not exist`]);
182
+ else push("log", [line, ...a]);
183
+ },
184
+ timeEnd: (name) => {
185
+ const line = elapsed(label(name));
186
+ if (line === null) push("warn", [`Timer "${label(name)}" does not exist`]);
187
+ else push("log", [line]);
188
+ timers.delete(label(name));
189
+ },
190
+ clear: () => undefined,
191
+ // Bun: `console.write(...chunks)` writes raw bytes to stdout and returns the byte count.
192
+ write: (...chunks) => {
193
+ const text = chunks.map((c) => (typeof c === "string" ? c : c instanceof Uint8Array ? new TextDecoder().decode(c) : String(c))).join("");
194
+ push("log", [text.replace(/\n$/, "")]);
195
+ return new TextEncoder().encode(text).length;
196
+ },
197
+ };
198
+
199
+ const originals = new Map<string, PropertyDescriptor | undefined>();
200
+ const names = new Set([...Object.keys(handlers), ...Object.getOwnPropertyNames(console)]);
201
+ for (const name of names) {
202
+ if (name === "Console" || name.startsWith("_")) continue;
203
+ const current = target[name];
204
+ if (!(name in handlers) && typeof current !== "function") continue;
205
+ originals.set(name, Object.getOwnPropertyDescriptor(console, name));
206
+ // Anything else (`profile`, `timeStamp`, Bun's `record` / `screenshot`, …) becomes a no-op.
207
+ const fn = handlers[name] ?? (() => undefined);
208
+ try {
209
+ Object.defineProperty(console, name, { value: fn, writable: true, configurable: true, enumerable: true });
210
+ } catch {
211
+ target[name] = fn;
212
+ }
213
+ }
214
+ let restored = false;
215
+ return {
216
+ restore() {
217
+ if (restored) return;
218
+ restored = true;
219
+ for (const [name, desc] of originals) {
220
+ if (desc) Object.defineProperty(console, name, desc);
221
+ else delete target[name];
222
+ }
223
+ },
224
+ };
225
+ }