@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,317 @@
1
+ /**
2
+ * Wire protocol for server-rendered console routes (`export const runtime = "server"`).
3
+ *
4
+ * Server ──(SSE `GET /__textjs/live`)──▶ frame player: {@link LiveServerMessage}
5
+ * Player ──(`POST /__textjs/live`, JSON {@link LiveInput}[])──▶ server
6
+ *
7
+ * Frames are exact cell deltas: only rows whose hash changed since the last
8
+ * message are sent, as runs of `[styleIndex, text]` against an append-only
9
+ * per-connection style table of packed `[fg, bg, attrs]`. The player writes
10
+ * them into its own `CellBuffer` and encodes `console.log` frames with the
11
+ * same `CssConsoleDriver` the client renderer uses, so a server-rendered
12
+ * frame is byte-for-byte the frame the client would have logged.
13
+ *
14
+ * Browser-safe and dependency-light: the frame player bundles this module.
15
+ */
16
+
17
+ import { ATTR_WIDE_CONT, CHAR_CONTINUATION, GRAPHEME_POOL_FLAG, type CellBuffer } from "@textjs/core";
18
+
19
+ export const LIVE_PROTOCOL_VERSION = 1;
20
+ /** SSE stream (GET) and input endpoint (POST), relative to `basePath`. */
21
+ export const LIVE_PATH = "/__textjs/live";
22
+ /** Session id header required on input POSTs (a custom header forces a CORS preflight cross-origin). */
23
+ export const LIVE_SESSION_HEADER = "x-textjs-live";
24
+
25
+ /** `[fg, bg, attrs]` (packed `0xRRGGBBAA` colors, attribute bitmask without `ATTR_WIDE_CONT`). */
26
+ export type LiveStyle = [fg: number, bg: number, attrs: number];
27
+ /** `[y, runs, graphemes?]`: `runs` alternates style index and text. */
28
+ export type LiveRow = [y: number, runs: Array<number | string>, graphemes?: string[]];
29
+
30
+ export interface LiveFrame {
31
+ t: "f";
32
+ w: number;
33
+ h: number;
34
+ /** Keyframe: reset the style table and buffer before applying. */
35
+ k?: 1;
36
+ /** Styles appended to the table, in index order. */
37
+ s?: LiveStyle[];
38
+ r: LiveRow[];
39
+ }
40
+
41
+ export type LiveServerMessage =
42
+ | { t: "hello"; v: number; sid: string; resumed: boolean }
43
+ | LiveFrame
44
+ /** The app navigated: mirror it into the address bar. */
45
+ | { t: "url"; href: string; replace?: boolean }
46
+ /** The app went back / forward (`router.back()`): move the browser's history by `delta`. */
47
+ | { t: "go"; delta: number }
48
+ | { t: "title"; title: string }
49
+ /** The next route renders on the client: load it as a document. */
50
+ | { t: "load"; href: string }
51
+ | { t: "error"; message: string };
52
+
53
+ export interface LiveKeyInput {
54
+ type: "key";
55
+ key: string;
56
+ input: string;
57
+ code: string;
58
+ repeat: boolean;
59
+ ctrl: boolean;
60
+ meta: boolean;
61
+ shift: boolean;
62
+ alt: boolean;
63
+ }
64
+
65
+ export type LiveInput =
66
+ | LiveKeyInput
67
+ | { type: "paste"; text: string }
68
+ | { type: "wheel"; deltaX: number; deltaY: number; rows: number; cols: number; ctrl: boolean; meta: boolean; shift: boolean; alt: boolean }
69
+ | { type: "resize"; cols: number; rows: number }
70
+ | { type: "focus"; focused: boolean }
71
+ /** Browser Back / Forward (`delta` entries from the previous one, when known). */
72
+ | { type: "nav"; href: string; delta?: number };
73
+
74
+ export const MAX_COLS = 400;
75
+ export const MAX_ROWS = 200;
76
+ const MAX_KEY = 64;
77
+ const MAX_PASTE = 256 * 1024;
78
+
79
+ const clampInt = (v: unknown, lo: number, hi: number): number | null =>
80
+ typeof v === "number" && Number.isFinite(v) ? Math.max(lo, Math.min(hi, Math.floor(v))) : null;
81
+ const finite = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) ? Math.max(-1e6, Math.min(1e6, v)) : 0);
82
+ const str = (v: unknown, max: number): string | null => (typeof v === "string" && v.length <= max ? v : null);
83
+
84
+ /**
85
+ * `true` for a same-origin app path: one leading `/`, no `//` or `/\` prefix
86
+ * (which browsers resolve to another host), no backslashes or control characters.
87
+ */
88
+ export function isAppPath(href: string): boolean {
89
+ return href.startsWith("/") && !/^\/[/\\]/.test(href) && !/[\u0000-\u001f\u007f\\]/.test(href);
90
+ }
91
+
92
+ /** Validate one untrusted input event (returns `null` when malformed). */
93
+ export function parseLiveInput(raw: unknown): LiveInput | null {
94
+ if (typeof raw !== "object" || raw === null) return null;
95
+ const m = raw as Record<string, unknown>;
96
+ switch (m.type) {
97
+ case "key": {
98
+ const key = str(m.key, MAX_KEY);
99
+ if (key === null || key.length === 0) return null;
100
+ return {
101
+ type: "key",
102
+ key,
103
+ input: str(m.input, MAX_KEY) ?? "",
104
+ code: str(m.code, MAX_KEY) ?? "",
105
+ repeat: m.repeat === true,
106
+ ctrl: m.ctrl === true,
107
+ meta: m.meta === true,
108
+ shift: m.shift === true,
109
+ alt: m.alt === true,
110
+ };
111
+ }
112
+ case "paste": {
113
+ const text = str(m.text, MAX_PASTE);
114
+ return text === null ? null : { type: "paste", text: text.replace(/\r\n?/g, "\n") };
115
+ }
116
+ case "wheel":
117
+ return {
118
+ type: "wheel",
119
+ deltaX: finite(m.deltaX),
120
+ deltaY: finite(m.deltaY),
121
+ rows: finite(m.rows),
122
+ cols: finite(m.cols),
123
+ ctrl: m.ctrl === true,
124
+ meta: m.meta === true,
125
+ shift: m.shift === true,
126
+ alt: m.alt === true,
127
+ };
128
+ case "resize": {
129
+ const cols = clampInt(m.cols, 1, MAX_COLS);
130
+ const rows = clampInt(m.rows, 1, MAX_ROWS);
131
+ return cols === null || rows === null ? null : { type: "resize", cols, rows };
132
+ }
133
+ case "focus":
134
+ return typeof m.focused === "boolean" ? { type: "focus", focused: m.focused } : null;
135
+ case "nav": {
136
+ const href = str(m.href, 2048);
137
+ if (href === null || !isAppPath(href)) return null;
138
+ const delta = clampInt(m.delta, -1000, 1000);
139
+ return delta === null || delta === 0 ? { type: "nav", href } : { type: "nav", href, delta };
140
+ }
141
+ default:
142
+ return null;
143
+ }
144
+ }
145
+
146
+ /** Format one SSE event. */
147
+ export function sseEvent(message: LiveServerMessage, id?: string): string {
148
+ return `${id !== undefined ? `id: ${id}\n` : ""}data: ${JSON.stringify(message)}\n\n`;
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Cell codec
153
+ // ---------------------------------------------------------------------------
154
+
155
+ /** Run text markers: a wide-grapheme continuation cell, a pooled grapheme, an escaped code point. */
156
+ const M_CONT = "\u0000";
157
+ const M_POOL = "\u0001";
158
+ const M_ESC = "\u0002";
159
+ const STYLE_TABLE_CAP = 4096;
160
+
161
+ /** Server half: diffs a `CellBuffer` against what this connection has been sent. */
162
+ export class LiveFrameEncoder {
163
+ private styles = new Map<string, number>();
164
+ private prev = new Uint32Array(0);
165
+ private width = -1;
166
+ private height = -1;
167
+ private keyframe = true;
168
+
169
+ /** Make the next frame a keyframe (a new or resumed connection). */
170
+ reset(): void {
171
+ this.keyframe = true;
172
+ }
173
+
174
+ /** Encode rows changed since the last call, or `null` when nothing changed. */
175
+ encode(buf: CellBuffer): LiveFrame | null {
176
+ const W = buf.width;
177
+ const H = buf.height;
178
+ if (W !== this.width || H !== this.height || this.styles.size > STYLE_TABLE_CAP) this.keyframe = true;
179
+ const key = this.keyframe;
180
+ if (key) {
181
+ this.keyframe = false;
182
+ this.width = W;
183
+ this.height = H;
184
+ this.prev = new Uint32Array(H);
185
+ this.styles = new Map();
186
+ }
187
+ const hashes = buf.getRowHashes();
188
+ const newStyles: LiveStyle[] = [];
189
+ const rows: LiveRow[] = [];
190
+ for (let y = 0; y < H; y++) {
191
+ const h = hashes[y] as number;
192
+ if (!key && h === this.prev[y]) continue;
193
+ this.prev[y] = h;
194
+ rows.push(this.encodeRow(buf, y, newStyles));
195
+ }
196
+ if (rows.length === 0) return null;
197
+ const frame: LiveFrame = { t: "f", w: W, h: H, r: rows };
198
+ if (key) frame.k = 1;
199
+ if (newStyles.length > 0) frame.s = newStyles;
200
+ return frame;
201
+ }
202
+
203
+ private encodeRow(buf: CellBuffer, y: number, newStyles: LiveStyle[]): LiveRow {
204
+ const W = buf.width;
205
+ const C = buf.chars;
206
+ const F = buf.fg;
207
+ const B = buf.bg;
208
+ const A = buf.attrs;
209
+ const runs: Array<number | string> = [];
210
+ let graphemes: string[] | undefined;
211
+ let curF = -1;
212
+ let curB = -1;
213
+ let curA = -1;
214
+ let text = "";
215
+ for (let i = y * W, end = i + W; i < end; i++) {
216
+ const f = F[i] as number;
217
+ const b = B[i] as number;
218
+ const rawA = A[i] as number;
219
+ const a = rawA & ~ATTR_WIDE_CONT;
220
+ if (f !== curF || b !== curB || a !== curA) {
221
+ if (text.length > 0) runs.push(text);
222
+ text = "";
223
+ curF = f;
224
+ curB = b;
225
+ curA = a;
226
+ const k = `${f},${b},${a}`;
227
+ let idx = this.styles.get(k);
228
+ if (idx === undefined) {
229
+ idx = this.styles.size;
230
+ this.styles.set(k, idx);
231
+ newStyles.push([f, b, a]);
232
+ }
233
+ runs.push(idx);
234
+ }
235
+ const c = C[i] as number;
236
+ if (rawA & ATTR_WIDE_CONT) text += M_CONT;
237
+ else if (c >= GRAPHEME_POOL_FLAG) {
238
+ text += M_POOL;
239
+ (graphemes ??= []).push(buf.graphemePool[c & 0x7fffffff] ?? " ");
240
+ } else if (c < 0x20 || c === 0x7f || (c >= 0xd800 && c <= 0xdfff)) text += `${M_ESC}${c.toString(16)};`;
241
+ else text += c < 0x10000 ? String.fromCharCode(c) : String.fromCodePoint(c);
242
+ }
243
+ if (text.length > 0) runs.push(text);
244
+ return graphemes ? [y, runs, graphemes] : [y, runs];
245
+ }
246
+ }
247
+
248
+ /** Player half: applies {@link LiveFrame}s to a `CellBuffer`. */
249
+ export class LiveFrameDecoder {
250
+ private styles: LiveStyle[] = [];
251
+
252
+ /** Apply a frame. Returns `false` when it can't be applied (a delta before any keyframe). */
253
+ apply(buf: CellBuffer, frame: LiveFrame): boolean {
254
+ if (frame.k === 1) {
255
+ this.styles = [];
256
+ if (buf.width !== frame.w || buf.height !== frame.h) buf.resize(frame.w, frame.h);
257
+ buf.clear();
258
+ } else if (buf.width !== frame.w || buf.height !== frame.h) return false;
259
+ if (frame.s) for (const s of frame.s) this.styles.push(s);
260
+ const W = buf.width;
261
+ const C = buf.chars;
262
+ const F = buf.fg;
263
+ const B = buf.bg;
264
+ const A = buf.attrs;
265
+ for (const row of frame.r) {
266
+ const y = row[0];
267
+ if (y < 0 || y >= buf.height) continue;
268
+ const runs = row[1];
269
+ const graphemes = row[2];
270
+ let gi = 0;
271
+ let i = y * W;
272
+ const end = i + W;
273
+ let f = 0;
274
+ let b = 0;
275
+ let a = 0;
276
+ for (let r = 0; r < runs.length; r++) {
277
+ const item = runs[r] as number | string;
278
+ if (typeof item === "number") {
279
+ const s = this.styles[item];
280
+ if (s === undefined) return false;
281
+ f = s[0];
282
+ b = s[1];
283
+ a = s[2];
284
+ continue;
285
+ }
286
+ for (let k = 0; k < item.length && i < end; ) {
287
+ const cp = item.codePointAt(k) as number;
288
+ let code: number;
289
+ let attrs = a;
290
+ if (cp === 0) {
291
+ code = CHAR_CONTINUATION;
292
+ attrs = a | ATTR_WIDE_CONT;
293
+ k++;
294
+ } else if (cp === 1) {
295
+ code = buf.internGrapheme(graphemes?.[gi++] ?? " ");
296
+ k++;
297
+ } else if (cp === 2) {
298
+ const semi = item.indexOf(";", k);
299
+ if (semi < 0) return false;
300
+ code = Number.parseInt(item.slice(k + 1, semi), 16) >>> 0;
301
+ k = semi + 1;
302
+ } else {
303
+ code = cp;
304
+ k += cp > 0xffff ? 2 : 1;
305
+ }
306
+ C[i] = code;
307
+ F[i] = f;
308
+ B[i] = b;
309
+ A[i] = attrs;
310
+ i++;
311
+ }
312
+ }
313
+ buf.markRowsDirty(y);
314
+ }
315
+ return true;
316
+ }
317
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Stands in for server-rendered pages (`export const runtime = "server"`) in
3
+ * the browser bundle, so their modules (and whatever they import) never ship
4
+ * to the client. Navigating to one from a client route loads it as a document,
5
+ * which the server answers with the frame player.
6
+ */
7
+
8
+ import { useEffect, type ReactNode } from "react";
9
+
10
+ export const runtime = "server";
11
+
12
+ const GUARD_KEY = "textjs:server-route";
13
+
14
+ export default function ServerRoute(): ReactNode {
15
+ useEffect(() => {
16
+ if (typeof window === "undefined") return;
17
+ const href = window.location.href;
18
+ try {
19
+ // Never loop if the document for this URL is (stale) client HTML.
20
+ const last = window.sessionStorage.getItem(GUARD_KEY);
21
+ if (last !== null) {
22
+ const [at, url] = [Number(last.slice(0, last.indexOf(" "))), last.slice(last.indexOf(" ") + 1)];
23
+ if (url === href && Date.now() - at < 3000) return;
24
+ }
25
+ window.sessionStorage.setItem(GUARD_KEY, `${Date.now()} ${href}`);
26
+ } catch {
27
+ // Storage blocked: reload anyway.
28
+ }
29
+ window.location.reload();
30
+ }, []);
31
+ return null;
32
+ }
package/src/mcp/cli.ts ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * `text mcp [dir]` — serve a headless Text.js app to coding agents over MCP
3
+ * (stdio). stdout carries JSON-RPC only: app `console.*` output and stray
4
+ * `process.stdout.write` calls are captured into the `logs` tool instead.
5
+ */
6
+
7
+ import { resolve } from "node:path";
8
+ import type { Writable } from "node:stream";
9
+ import { txMessage } from "@textjs/core";
10
+ import type { ThemeName } from "@textjs/react";
11
+ import { captureConsole, LogBuffer, type LogEntry } from "./logs";
12
+
13
+ export interface McpCliArgs {
14
+ dir: string;
15
+ path?: string;
16
+ cols?: number;
17
+ rows?: number;
18
+ theme?: string;
19
+ skipConfig: boolean;
20
+ verbose: boolean;
21
+ help: boolean;
22
+ /** Unrecognized `-`/`--` flags (reported as a usage error). */
23
+ unknown: string[];
24
+ }
25
+
26
+ export const MCP_HELP = `Usage: text mcp [dir] [options]
27
+
28
+ Serve the app in [dir] to coding agents over the Model Context Protocol (stdio).
29
+ The app runs headless, in-process: same routes, layouts and route handlers.
30
+
31
+ Options:
32
+ --path <pathname> Initial route (default /)
33
+ --cols <n> Grid width (default: text.config.ts viewport, else 100)
34
+ --rows <n> Grid height (default: text.config.ts viewport, else 32)
35
+ --theme <name> Theme preset
36
+ --no-config Ignore text.config.ts
37
+ --verbose Mirror captured logs to stderr
38
+ -h, --help Show this help
39
+
40
+ Tools: list_routes navigate read_frame press type paste scroll resize
41
+ inspect_tree find wait_for logs fetch reload
42
+ `;
43
+
44
+ export function parseMcpArgs(argv: readonly string[]): McpCliArgs {
45
+ const out: McpCliArgs = { dir: ".", skipConfig: false, verbose: false, help: false, unknown: [] };
46
+ const positional: string[] = [];
47
+ const num = (v: string | undefined): number | undefined => {
48
+ const n = Number(v);
49
+ return v !== undefined && Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
50
+ };
51
+ for (let i = 0; i < argv.length; i++) {
52
+ const a = argv[i] as string;
53
+ const eq = a.indexOf("=");
54
+ const flag = a.startsWith("--") && eq > 0 ? a.slice(0, eq) : a;
55
+ const inline = a.startsWith("--") && eq > 0 ? a.slice(eq + 1) : undefined;
56
+ const value = (): string | undefined => inline ?? argv[++i];
57
+ if (flag === "-h" || flag === "--help") out.help = true;
58
+ else if (flag === "--path") out.path = value();
59
+ else if (flag === "--cols") out.cols = num(value());
60
+ else if (flag === "--rows") out.rows = num(value());
61
+ else if (flag === "--theme") out.theme = value();
62
+ else if (flag === "--no-config") out.skipConfig = true;
63
+ else if (flag === "--verbose") out.verbose = true;
64
+ else if (a.startsWith("-") && a !== "-") out.unknown.push(a);
65
+ else positional.push(a);
66
+ }
67
+ // argv includes the command itself ("mcp") when called from `text`.
68
+ if (positional[0] === "mcp") positional.shift();
69
+ if (positional[0]) out.dir = positional[0];
70
+ return out;
71
+ }
72
+
73
+ function formatEntry(e: LogEntry): string {
74
+ return `[text mcp] ${e.level}${e.source !== "console" ? ` (${e.source})` : ""}: ${e.message}\n`;
75
+ }
76
+
77
+ /** Run the stdio server until stdin closes or a signal arrives. Resolves with the exit code. */
78
+ export async function runMcpCli(argv: readonly string[]): Promise<number> {
79
+ const args = parseMcpArgs(argv);
80
+ const stderr = (s: string): void => void process.stderr.write(s);
81
+ if (args.help) {
82
+ // Only a human asks for help, so stdout is not carrying JSON-RPC yet.
83
+ process.stdout.write(MCP_HELP);
84
+ return 0;
85
+ }
86
+ if (args.unknown.length > 0) {
87
+ stderr(`text mcp: unknown option${args.unknown.length > 1 ? "s" : ""} ${args.unknown.join(", ")}. See \`text mcp --help\`.\n`);
88
+ return 2;
89
+ }
90
+ if (typeof process.versions.bun !== "string") {
91
+ stderr(`${txMessage("TX019", "mcp", "mcp")}\n`);
92
+ return 1;
93
+ }
94
+
95
+ // 1. Own stdout before anything can write to it.
96
+ const realWrite = process.stdout.write.bind(process.stdout) as (chunk: string | Uint8Array, cb?: (err?: Error | null) => void) => boolean;
97
+ const logs = new LogBuffer();
98
+ const mirror = args.verbose ? (e: LogEntry) => stderr(formatEntry(e)) : undefined;
99
+ const capture = captureConsole(logs, mirror);
100
+ const strayWrite = ((chunk: string | Uint8Array, encodingOrCb?: unknown, cb?: unknown): boolean => {
101
+ const text = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
102
+ const entry = logs.push("log", "process", [text.replace(/\n$/, "")]);
103
+ mirror?.(entry);
104
+ const done = typeof encodingOrCb === "function" ? encodingOrCb : cb;
105
+ if (typeof done === "function") queueMicrotask(() => (done as () => void)());
106
+ return true;
107
+ }) as typeof process.stdout.write;
108
+ process.stdout.write = strayWrite;
109
+ const transportOut = {
110
+ write: (chunk: string) => realWrite(chunk),
111
+ once: (event: string, listener: () => void) => {
112
+ process.stdout.once(event, listener);
113
+ return transportOut;
114
+ },
115
+ on: (event: string, listener: () => void) => {
116
+ process.stdout.on(event, listener);
117
+ return transportOut;
118
+ },
119
+ } as unknown as Writable;
120
+
121
+ const onRejection = (reason: unknown): void => {
122
+ const e = logs.push("error", "process", ["Unhandled promise rejection:", reason]);
123
+ mirror?.(e);
124
+ };
125
+ const onException = (error: unknown): void => {
126
+ const e = logs.push("error", "process", ["Uncaught exception:", error]);
127
+ mirror?.(e);
128
+ };
129
+ process.on("unhandledRejection", onRejection);
130
+ process.on("uncaughtException", onException);
131
+
132
+ const [{ StdioServerTransport }, { createTextMcpServer }] = await Promise.all([import("@modelcontextprotocol/sdk/server/stdio.js"), import("./server")]);
133
+ const root = resolve(args.dir);
134
+ const { server, controller } = createTextMcpServer({
135
+ root,
136
+ path: args.path,
137
+ cols: args.cols,
138
+ rows: args.rows,
139
+ theme: args.theme as ThemeName | undefined,
140
+ skipConfig: args.skipConfig,
141
+ logs,
142
+ captureConsole: false,
143
+ });
144
+ const transport = new StdioServerTransport(process.stdin, transportOut);
145
+
146
+ let resolveExit: (code: number) => void = () => undefined;
147
+ const exited = new Promise<number>((r) => {
148
+ resolveExit = r;
149
+ });
150
+ let closing = false;
151
+ const shutdown = async (code: number): Promise<void> => {
152
+ if (closing) return;
153
+ closing = true;
154
+ try {
155
+ await Promise.race([controller.dispose(), new Promise((r) => setTimeout(r, 2000))]);
156
+ await server.close().catch(() => undefined);
157
+ } finally {
158
+ process.off("unhandledRejection", onRejection);
159
+ process.off("uncaughtException", onException);
160
+ capture.restore();
161
+ process.stdout.write = realWrite as typeof process.stdout.write;
162
+ resolveExit(code);
163
+ // App timers or sockets must not keep a disconnected server alive.
164
+ setTimeout(() => process.exit(code), 500).unref();
165
+ }
166
+ };
167
+ process.stdin.once("end", () => void shutdown(0));
168
+ process.stdin.once("close", () => void shutdown(0));
169
+ process.once("SIGINT", () => void shutdown(130));
170
+ process.once("SIGTERM", () => void shutdown(143));
171
+
172
+ // The client went away mid-write (EPIPE): nothing can be delivered any more.
173
+ process.stdout.on("error", () => void shutdown(0));
174
+
175
+ await server.connect(transport);
176
+ // After connect: `Protocol.connect` installs its own transport `onclose`.
177
+ const onServerClose = server.server.onclose;
178
+ server.server.onclose = () => {
179
+ onServerClose?.();
180
+ void shutdown(0);
181
+ };
182
+ stderr(`▼ Text.js MCP server · ${root}\n`);
183
+ // Load the app in the background; tools wait for it and report load errors.
184
+ controller.start().catch((error: unknown) => {
185
+ stderr(`[text mcp] failed to start the app: ${error instanceof Error ? error.message : String(error)}\n`);
186
+ });
187
+ return exited;
188
+ }