@solidrt/cli 0.0.50 → 0.0.52

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 (98) hide show
  1. package/AGENTS.md +79 -36
  2. package/README.md +86 -15
  3. package/agents/assets.md +46 -0
  4. package/agents/debugging.md +294 -0
  5. package/dist/console.srtapp +94123 -73
  6. package/dist/server.js +3936 -0
  7. package/package.json +11 -10
  8. package/src/android/docs.md +21 -0
  9. package/src/android/main.ts +286 -0
  10. package/src/{bundler.ts → bundle/bundler.ts} +163 -76
  11. package/src/bundle/docs.md +12 -0
  12. package/src/bundle/main.ts +203 -0
  13. package/src/check/docs.md +10 -0
  14. package/src/check/main.ts +85 -0
  15. package/src/{commands/check.ts → check/typecheck.ts} +11 -34
  16. package/src/client/docs.md +9 -0
  17. package/src/client/main.ts +33 -0
  18. package/src/console/docs.md +14 -0
  19. package/src/console/main.ts +21 -0
  20. package/src/demo/docs.md +27 -0
  21. package/src/demo/main.ts +67 -0
  22. package/src/init/docs.md +11 -0
  23. package/src/{commands/init.ts → init/main.ts} +31 -25
  24. package/src/init/scaffold/AGENTS.md +98 -0
  25. package/src/init/scaffold/package.json +23 -0
  26. package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
  27. package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
  28. package/src/lib/args.ts +194 -0
  29. package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
  30. package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
  31. package/src/{fonts.ts → lib/fonts.ts} +12 -26
  32. package/src/lib/mode.ts +77 -0
  33. package/src/{project.ts → lib/project.ts} +109 -61
  34. package/src/lib/registry.ts +120 -0
  35. package/src/lib/server-bundle.ts +24 -0
  36. package/src/lib/usage.ts +117 -0
  37. package/src/lib/util.ts +36 -0
  38. package/src/main.ts +109 -31
  39. package/src/mcp/docs.md +22 -0
  40. package/src/mcp/main.ts +719 -0
  41. package/src/pack/docs.md +18 -0
  42. package/src/{pack-folder.ts → pack/layout.ts} +13 -22
  43. package/src/pack/main.ts +86 -0
  44. package/src/pack/trailer.ts +97 -0
  45. package/src/render/docs.md +19 -0
  46. package/src/render/main.ts +49 -0
  47. package/src/server/args.ts +126 -0
  48. package/src/server/binaries.ts +47 -0
  49. package/src/server/config.ts +54 -0
  50. package/{server → src/server}/control.ts +246 -78
  51. package/src/server/docs.md +51 -0
  52. package/src/server/line-editor.ts +200 -0
  53. package/src/server/main.ts +473 -0
  54. package/src/server/mode.ts +92 -0
  55. package/src/server/rebuild.ts +90 -0
  56. package/src/server/registry.ts +138 -0
  57. package/src/server/remap.ts +60 -0
  58. package/src/server/repl.ts +223 -0
  59. package/src/server/state.ts +54 -0
  60. package/{server → src/server}/tsconfig.json +1 -1
  61. package/{server → src/server}/tunnel.ts +6 -6
  62. package/src/server/watcher.ts +121 -0
  63. package/src/tool/main.ts +70 -0
  64. package/src/types/bundle.d.ts +24 -0
  65. package/src/types/control.d.ts +90 -0
  66. package/src/types/registry.d.ts +16 -0
  67. package/scaffold/AGENTS.md +0 -630
  68. package/scaffold/package.json +0 -22
  69. package/scaffold/templates/components/icon.tsx +0 -48
  70. package/scaffold/templates/default/icon.tsx +0 -48
  71. package/server/main.ts +0 -308
  72. package/server/rebuild.ts +0 -68
  73. package/server/remap.ts +0 -47
  74. package/server/state.ts +0 -93
  75. package/src/args.ts +0 -212
  76. package/src/bundle-cli.ts +0 -13
  77. package/src/commands/bundle.ts +0 -76
  78. package/src/commands/client.ts +0 -34
  79. package/src/commands/mcp.ts +0 -603
  80. package/src/commands/pack.ts +0 -65
  81. package/src/commands/render.ts +0 -24
  82. package/src/commands/server.ts +0 -73
  83. package/src/dev-android.ts +0 -176
  84. package/src/dev-client.ts +0 -29
  85. package/src/dev-server.ts +0 -302
  86. package/src/packer.ts +0 -103
  87. package/src/repl.ts +0 -233
  88. package/src/util.ts +0 -121
  89. package/src/watcher.ts +0 -69
  90. /package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +0 -0
  91. /package/src/{prompt.ts → init/prompt.ts} +0 -0
  92. /package/{scaffold → src/init/scaffold}/gitignore +0 -0
  93. /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
  94. /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
  95. /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
  96. /package/{server → src/server}/cache.ts +0 -0
  97. /package/{server → src/server}/proxy.ts +0 -0
  98. /package/{server → src/server}/qr.ts +0 -0
@@ -1,48 +0,0 @@
1
- // The animated SolidRT logo: each segment fades in, then out, on its own delay.
2
- import { For, onFrame, createSignal, createLinearGradient } from "@solidrt/core"
3
-
4
- const FADE = 360
5
-
6
- let SEGMENTS = [
7
- { base: 0, light: "#3f5494", dark: "#162b6c", d: "M50.000 50.000 L28.330 50.000 C28.330 48.810 27.695 47.711 26.665 47.116 C25.635 46.521 24.365 46.521 23.335 47.116 C22.305 47.711 21.670 48.810 21.670 50.000 L0.000 50.000 L50.000 0.000 L50.000 9.170 C48.810 9.170 47.711 9.805 47.116 10.835 C46.521 11.865 46.521 13.135 47.116 14.165 C47.711 15.195 48.810 15.830 50.000 15.830 L50.000 25.000 L50.000 34.170 C48.810 34.170 47.711 34.805 47.116 35.835 C46.521 36.865 46.521 38.135 47.116 39.165 C47.711 40.195 48.810 40.830 50.000 40.830 L50.000 50.000 Z" },
8
- { base: 90, light: "#547ebf", dark: "#2b5696", d: "M50.000 50.000 L50.000 59.170 C48.810 59.170 47.711 59.805 47.116 60.835 C46.521 61.865 46.521 63.135 47.116 64.165 C47.711 65.195 48.810 65.830 50.000 65.830 L50.000 75.000 L50.000 84.170 C48.810 84.170 47.711 84.805 47.116 85.835 C46.521 86.865 46.521 88.135 47.116 89.165 C47.711 90.195 48.810 90.830 50.000 90.830 L50.000 100.000 L0.000 50.000 L21.670 50.000 C21.670 48.810 22.305 47.711 23.335 47.116 C24.365 46.521 25.635 46.521 26.665 47.116 C27.695 47.711 28.330 48.810 28.330 50.000 L50.000 50.000 Z" },
9
- { base: 180, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 25.000 L50.000 15.830 C48.810 15.830 47.711 15.195 47.116 14.165 C46.521 13.135 46.521 11.865 47.116 10.835 C47.711 9.805 48.810 9.170 50.000 9.170 L50.000 0.000 L75.000 25.000 L65.830 25.000 C65.830 26.190 65.195 27.289 64.165 27.884 C63.135 28.479 61.865 28.479 60.835 27.884 C59.805 27.289 59.170 26.190 59.170 25.000 L50.000 25.000 Z" },
10
- { base: 270, light: "#547ebf", dark: "#2b5696", d: "M50.000 25.000 L59.170 25.000 C59.170 26.190 59.805 27.289 60.835 27.884 C61.865 28.479 63.135 28.479 64.165 27.884 C65.195 27.289 65.830 26.190 65.830 25.000 L75.000 25.000 L75.000 34.170 C73.810 34.170 72.711 34.805 72.116 35.835 C71.521 36.865 71.521 38.135 72.116 39.165 C72.711 40.195 73.810 40.830 75.000 40.830 L75.000 50.000 L65.830 50.000 C65.830 48.810 65.195 47.711 64.165 47.116 C63.135 46.521 61.865 46.521 60.835 47.116 C59.805 47.711 59.170 48.810 59.170 50.000 L50.000 50.000 L50.000 40.830 C48.810 40.830 47.711 40.195 47.116 39.165 C46.521 38.135 46.521 36.865 47.116 35.835 C47.711 34.805 48.810 34.170 50.000 34.170 L50.000 25.000 Z" },
11
- { base: 360, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 50.000 L59.170 50.000 C59.170 48.810 59.805 47.711 60.835 47.116 C61.865 46.521 63.135 46.521 64.165 47.116 C65.195 47.711 65.830 48.810 65.830 50.000 L75.000 50.000 L64.855 60.145 C64.013 59.304 62.787 58.976 61.638 59.283 C60.489 59.591 59.591 60.489 59.283 61.638 C58.976 62.787 59.304 64.013 60.145 64.855 L50.000 75.000 L50.000 65.830 C48.810 65.830 47.711 65.195 47.116 64.165 C46.521 63.135 46.521 61.865 47.116 60.835 C47.711 59.805 48.810 59.170 50.000 59.170 L50.000 50.000 Z" },
12
- { base: 450, light: "#3f5494", dark: "#162b6c", d: "M75.000 50.000 L75.000 59.170 C73.810 59.170 72.711 59.805 72.116 60.835 C71.521 61.865 71.521 63.135 72.116 64.165 C72.711 65.195 73.810 65.830 75.000 65.830 L75.000 75.000 L50.000 100.000 L50.000 90.830 C48.810 90.830 47.711 90.195 47.116 89.165 C46.521 88.135 46.521 86.865 47.116 85.835 C47.711 84.805 48.810 84.170 50.000 84.170 L50.000 75.000 L60.145 64.855 C59.304 64.013 58.976 62.787 59.283 61.638 C59.591 60.489 60.489 59.591 61.638 59.283 C62.787 58.976 64.013 59.304 64.855 60.145 L75.000 50.000 Z" },
13
- { base: 540, light: "#7ea9ea", dark: "#5681c1", d: "M100.000 50.000 L75.000 75.000 L75.000 65.830 C73.810 65.830 72.711 65.195 72.116 64.165 C71.521 63.135 71.521 61.865 72.116 60.835 C72.711 59.805 73.810 59.170 75.000 59.170 L75.000 50.000 L75.000 40.830 C73.810 40.830 72.711 40.195 72.116 39.165 C71.521 38.135 71.521 36.865 72.116 35.835 C72.711 34.805 73.810 34.170 75.000 34.170 L75.000 25.000 L100.000 50.000 Z" },
14
- ]
15
-
16
- let LAST = SEGMENTS[SEGMENTS.length - 1]!.base
17
- let IN_DONE = LAST + FADE
18
- let CYCLE = IN_DONE + LAST + FADE
19
-
20
- let clamp = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x)
21
- let ease = (t: number) => 1 - (1 - t) * (1 - t)
22
- let byte = (x: number) => Math.round(Math.max(0, Math.min(255, x))).toString(16).padStart(2, "0")
23
-
24
- let fill = (seg: (typeof SEGMENTS)[number], a: number) =>
25
- createLinearGradient(0, 0, 1, 1, [
26
- { offset: 0, color: seg.light + byte(a * 255) },
27
- { offset: 1, color: seg.dark + byte(a * 255) },
28
- ])
29
-
30
- export function Icon() {
31
- let [clock, setClock] = createSignal(0)
32
- let start = -1
33
- onFrame((t) => (start < 0 && (start = t), setClock((t - start) % CYCLE)))
34
- return (
35
- <view width={300} height={300} viewBox={[100, 100]}>
36
- <For each={SEGMENTS}>
37
- {(seg) => {
38
- let a = () => {
39
- let t = clock(), end = IN_DONE + seg.base + FADE
40
- if (t < seg.base || t >= end) return 0
41
- return ease(Math.min(clamp((t - seg.base) / FADE), clamp((end - t) / FADE)))
42
- }
43
- return <d-path d={seg.d} color={fill(seg, a())} />
44
- }}
45
- </For>
46
- </view>
47
- )
48
- }
@@ -1,48 +0,0 @@
1
- // The animated SolidRT logo: each segment fades in, then out, on its own delay.
2
- import { For, onFrame, createSignal, createLinearGradient } from "@solidrt/core"
3
-
4
- const FADE = 360
5
-
6
- let SEGMENTS = [
7
- { base: 0, light: "#3f5494", dark: "#162b6c", d: "M50.000 50.000 L28.330 50.000 C28.330 48.810 27.695 47.711 26.665 47.116 C25.635 46.521 24.365 46.521 23.335 47.116 C22.305 47.711 21.670 48.810 21.670 50.000 L0.000 50.000 L50.000 0.000 L50.000 9.170 C48.810 9.170 47.711 9.805 47.116 10.835 C46.521 11.865 46.521 13.135 47.116 14.165 C47.711 15.195 48.810 15.830 50.000 15.830 L50.000 25.000 L50.000 34.170 C48.810 34.170 47.711 34.805 47.116 35.835 C46.521 36.865 46.521 38.135 47.116 39.165 C47.711 40.195 48.810 40.830 50.000 40.830 L50.000 50.000 Z" },
8
- { base: 90, light: "#547ebf", dark: "#2b5696", d: "M50.000 50.000 L50.000 59.170 C48.810 59.170 47.711 59.805 47.116 60.835 C46.521 61.865 46.521 63.135 47.116 64.165 C47.711 65.195 48.810 65.830 50.000 65.830 L50.000 75.000 L50.000 84.170 C48.810 84.170 47.711 84.805 47.116 85.835 C46.521 86.865 46.521 88.135 47.116 89.165 C47.711 90.195 48.810 90.830 50.000 90.830 L50.000 100.000 L0.000 50.000 L21.670 50.000 C21.670 48.810 22.305 47.711 23.335 47.116 C24.365 46.521 25.635 46.521 26.665 47.116 C27.695 47.711 28.330 48.810 28.330 50.000 L50.000 50.000 Z" },
9
- { base: 180, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 25.000 L50.000 15.830 C48.810 15.830 47.711 15.195 47.116 14.165 C46.521 13.135 46.521 11.865 47.116 10.835 C47.711 9.805 48.810 9.170 50.000 9.170 L50.000 0.000 L75.000 25.000 L65.830 25.000 C65.830 26.190 65.195 27.289 64.165 27.884 C63.135 28.479 61.865 28.479 60.835 27.884 C59.805 27.289 59.170 26.190 59.170 25.000 L50.000 25.000 Z" },
10
- { base: 270, light: "#547ebf", dark: "#2b5696", d: "M50.000 25.000 L59.170 25.000 C59.170 26.190 59.805 27.289 60.835 27.884 C61.865 28.479 63.135 28.479 64.165 27.884 C65.195 27.289 65.830 26.190 65.830 25.000 L75.000 25.000 L75.000 34.170 C73.810 34.170 72.711 34.805 72.116 35.835 C71.521 36.865 71.521 38.135 72.116 39.165 C72.711 40.195 73.810 40.830 75.000 40.830 L75.000 50.000 L65.830 50.000 C65.830 48.810 65.195 47.711 64.165 47.116 C63.135 46.521 61.865 46.521 60.835 47.116 C59.805 47.711 59.170 48.810 59.170 50.000 L50.000 50.000 L50.000 40.830 C48.810 40.830 47.711 40.195 47.116 39.165 C46.521 38.135 46.521 36.865 47.116 35.835 C47.711 34.805 48.810 34.170 50.000 34.170 L50.000 25.000 Z" },
11
- { base: 360, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 50.000 L59.170 50.000 C59.170 48.810 59.805 47.711 60.835 47.116 C61.865 46.521 63.135 46.521 64.165 47.116 C65.195 47.711 65.830 48.810 65.830 50.000 L75.000 50.000 L64.855 60.145 C64.013 59.304 62.787 58.976 61.638 59.283 C60.489 59.591 59.591 60.489 59.283 61.638 C58.976 62.787 59.304 64.013 60.145 64.855 L50.000 75.000 L50.000 65.830 C48.810 65.830 47.711 65.195 47.116 64.165 C46.521 63.135 46.521 61.865 47.116 60.835 C47.711 59.805 48.810 59.170 50.000 59.170 L50.000 50.000 Z" },
12
- { base: 450, light: "#3f5494", dark: "#162b6c", d: "M75.000 50.000 L75.000 59.170 C73.810 59.170 72.711 59.805 72.116 60.835 C71.521 61.865 71.521 63.135 72.116 64.165 C72.711 65.195 73.810 65.830 75.000 65.830 L75.000 75.000 L50.000 100.000 L50.000 90.830 C48.810 90.830 47.711 90.195 47.116 89.165 C46.521 88.135 46.521 86.865 47.116 85.835 C47.711 84.805 48.810 84.170 50.000 84.170 L50.000 75.000 L60.145 64.855 C59.304 64.013 58.976 62.787 59.283 61.638 C59.591 60.489 60.489 59.591 61.638 59.283 C62.787 58.976 64.013 59.304 64.855 60.145 L75.000 50.000 Z" },
13
- { base: 540, light: "#7ea9ea", dark: "#5681c1", d: "M100.000 50.000 L75.000 75.000 L75.000 65.830 C73.810 65.830 72.711 65.195 72.116 64.165 C71.521 63.135 71.521 61.865 72.116 60.835 C72.711 59.805 73.810 59.170 75.000 59.170 L75.000 50.000 L75.000 40.830 C73.810 40.830 72.711 40.195 72.116 39.165 C71.521 38.135 71.521 36.865 72.116 35.835 C72.711 34.805 73.810 34.170 75.000 34.170 L75.000 25.000 L100.000 50.000 Z" },
14
- ]
15
-
16
- let LAST = SEGMENTS[SEGMENTS.length - 1]!.base
17
- let IN_DONE = LAST + FADE
18
- let CYCLE = IN_DONE + LAST + FADE
19
-
20
- let clamp = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x)
21
- let ease = (t: number) => 1 - (1 - t) * (1 - t)
22
- let byte = (x: number) => Math.round(Math.max(0, Math.min(255, x))).toString(16).padStart(2, "0")
23
-
24
- let fill = (seg: (typeof SEGMENTS)[number], a: number) =>
25
- createLinearGradient(0, 0, 1, 1, [
26
- { offset: 0, color: seg.light + byte(a * 255) },
27
- { offset: 1, color: seg.dark + byte(a * 255) },
28
- ])
29
-
30
- export function Icon() {
31
- let [clock, setClock] = createSignal(0)
32
- let start = -1
33
- onFrame((t) => (start < 0 && (start = t), setClock((t - start) % CYCLE)))
34
- return (
35
- <view width={300} height={300} viewBox={[100, 100]}>
36
- <For each={SEGMENTS}>
37
- {(seg) => {
38
- let a = () => {
39
- let t = clock(), end = IN_DONE + seg.base + FADE
40
- if (t < seg.base || t >= end) return 0
41
- return ease(Math.min(clamp((t - seg.base) / FADE), clamp((end - t) / FADE)))
42
- }
43
- return <d-path d={seg.d} color={fill(seg, a())} />
44
- }}
45
- </For>
46
- </view>
47
- )
48
- }
package/server/main.ts DELETED
@@ -1,308 +0,0 @@
1
- // The srt dev server as a flux script. srt (Bun) spawns this with one JSON
2
- // config argument; bundling, file watching, and the repl stay in srt, which
3
- // drives this process over the loopback-only /__internal__/ routes. The
4
- // shutdown-when-empty policy also lives in srt (it polls /__internal__/clients);
5
- // this process runs until srt kills it. See docs/flux-dev-server-plan.md.
6
-
7
- import { serve } from "flux:http"
8
- import type { FluxRequest, Server } from "flux:http"
9
- import { file } from "flux:fs"
10
- import { argv } from "flux:process"
11
- import { resolveWithin } from "flux:path"
12
- import { state, type Config } from "./state"
13
- import * as cache from "./cache"
14
- import { handleProxy } from "./proxy"
15
- import { appendLog, clientList, handleControl, resolveQuery } from "./control"
16
- import { printQr } from "./qr"
17
- import { createTunnelEndpoint, TUNNEL_PROTOCOL } from "./tunnel"
18
-
19
- let config: Config = JSON.parse(argv[0]!)
20
- state.config = config
21
- state.sourceDir = config.sourceDir
22
- state.projectDir = config.projectDir
23
- state.stats = config.stats
24
- state.serverUrl = `${config.address}:${config.port}`
25
-
26
- if (config.cache) {
27
- await cache.initCache({ dir: config.cacheDir })
28
- console.log("[cli] HTTP cache enabled")
29
- }
30
-
31
- if (config.capture) {
32
- // Start each capture from an empty file: appends would otherwise tack onto
33
- // whatever a previous run left behind.
34
- await file(config.capture).write("")
35
- state.captureStartMs = Date.now()
36
- }
37
-
38
- // Split an origin-form request URL ("/path?a=1&b=2") into its decoded path and
39
- // query parameters. flux has no URL global; this covers what the routes need.
40
- function splitQuery(url: string): { path: string; query: Map<string, string> } {
41
- let i = url.indexOf("?")
42
- let path = i < 0 ? url : url.slice(0, i)
43
- let query = new Map<string, string>()
44
- if (i >= 0) {
45
- for (let pair of url.slice(i + 1).split("&")) {
46
- if (!pair) continue
47
- let j = pair.indexOf("=")
48
- let k = j < 0 ? pair : pair.slice(0, j)
49
- let v = j < 0 ? "" : pair.slice(j + 1)
50
- query.set(decodeURIComponent(k), decodeURIComponent(v.replace(/\+/g, " ")))
51
- }
52
- }
53
- return { path: decodeURIComponent(path), query }
54
- }
55
-
56
- // Send `text` to the clients with the given ids, or to every client when
57
- // `ids` is omitted.
58
- function sendTo(ids: number[] | undefined, text: string) {
59
- for (let [ws, info] of state.clients) {
60
- if (!ids || ids.includes(info.id)) ws.send(text)
61
- }
62
- }
63
-
64
- // The srt -> server IPC under /__internal__/: only the srt process on this
65
- // machine may drive it, so reject any peer that is not loopback.
66
- async function handleInternal(req: FluxRequest, server: Server, path: string): Promise<Response> {
67
- let ip = server.requestIP(req)
68
- let loopback = ip && (ip.address === "127.0.0.1" || ip.address === "::1" || ip.address === "::ffff:127.0.0.1")
69
- if (!loopback) return new Response("Forbidden", { status: 403 })
70
-
71
- if (path === "/__internal__/clients") return Response.json(clientList(true))
72
- if (path === "/__internal__/watch" && req.method === "GET") return Response.json({ enabled: state.watch })
73
- if (req.method !== "POST") return new Response("Method not allowed", { status: 405 })
74
-
75
- switch (path) {
76
- case "/__internal__/reload": {
77
- // { message, clients?, latch?, sourceDir?, map? }: send `message` (a
78
- // full client-protocol message, built by srt) to the listed client ids,
79
- // or to all when omitted. `latch` keeps it for late-joining clients
80
- // (code reloads latch, one-shot bytecode loads do not); `sourceDir`
81
- // moves the file-serving root (repl `load`; the project root - and with
82
- // it the /assets/ root - is fixed for the life of the run); `map` is
83
- // the bundle's sourcemap for log remapping, replaced on every reload
84
- // (absent means none).
85
- let body = await req.json()
86
- if (typeof body.sourceDir === "string") state.sourceDir = body.sourceDir
87
- // Keep the rebuild entry in sync when `load` moves it, so a later MCP
88
- // reload bundles the newly loaded file, not the launch-time one.
89
- if (typeof body.entry === "string") state.config.entry = body.entry
90
- state.currentMap = typeof body.map === "string" ? body.map : null
91
- let text = JSON.stringify(body.message)
92
- if (body.latch) state.currentReload = text
93
- sendTo(body.clients, text)
94
- return new Response("", { status: 204 })
95
- }
96
- case "/__internal__/stop": {
97
- let body = await req.json()
98
- // A broadcast stop also forgets the latched reload, so a client that
99
- // connects afterwards starts clean.
100
- if (!body.clients) {
101
- state.currentReload = null
102
- state.currentMap = null
103
- }
104
- sendTo(body.clients, JSON.stringify({ type: "stop" }))
105
- return new Response("", { status: 204 })
106
- }
107
- case "/__internal__/watch": {
108
- // The repl's `watch on|off`; agents use /__control__/watch instead.
109
- let body = await req.json()
110
- state.watch = !!body.enabled
111
- return new Response("", { status: 204 })
112
- }
113
- case "/__internal__/stats": {
114
- let body = await req.json()
115
- state.stats = !!body.stats
116
- sendTo(undefined, JSON.stringify({ type: "stats", stats: state.stats }))
117
- return new Response("", { status: 204 })
118
- }
119
- default:
120
- return Response.json({ error: "Unknown internal endpoint" }, { status: 404 })
121
- }
122
- }
123
-
124
- // The file routes: GET file with single-range 206 support. All paths are
125
- // contained in `root` (the source directory, or the project dir for the
126
- // /assets/ convention route).
127
- async function handleFiles(req: FluxRequest, path: string, root: string): Promise<Response> {
128
- let filePath = resolveWithin(root, "." + path)
129
- if (!filePath) {
130
- return new Response("Forbidden", { status: 403 })
131
- }
132
-
133
- console.log("[cli] get " + path)
134
-
135
- let stat
136
- try {
137
- stat = await file(filePath).stat()
138
- } catch {
139
- console.log(`[cli] file not found ${path}`)
140
- return new Response("Not found", { status: 404 })
141
- }
142
- if (stat.type === "directory") {
143
- return new Response("Not found", { status: 404 })
144
- }
145
-
146
- let baseHeaders: Record<string, string> = { "Accept-Ranges": "bytes" }
147
-
148
- // Honor a single byte-range request (e.g. streaming audio decoding on the
149
- // client, which seeks and reads on demand). Only the common "bytes=a-b" /
150
- // "bytes=a-" / "bytes=-n" forms; anything else falls through to the whole
151
- // file. Range makes proxied streaming viable without pulling the whole
152
- // track over the wire.
153
- let range = req.headers.get("range")
154
- let match = range ? /^bytes=(\d*)-(\d*)$/.exec(range.trim()) : null
155
- if (match) {
156
- let size = stat.size
157
- let start: number
158
- let end: number
159
- if (match[1] === "") {
160
- // Suffix range: the last N bytes.
161
- let n = parseInt(match[2]!, 10)
162
- start = isNaN(n) ? 0 : Math.max(0, size - n)
163
- end = size - 1
164
- } else {
165
- start = parseInt(match[1]!, 10)
166
- end = match[2] === "" ? size - 1 : Math.min(parseInt(match[2]!, 10), size - 1)
167
- }
168
- if (start > end || start >= size) {
169
- return new Response("Range not satisfiable", {
170
- status: 416,
171
- headers: { ...baseHeaders, "Content-Range": `bytes */${size}` },
172
- })
173
- }
174
- return new Response(await file(filePath).read(start, end - start + 1), {
175
- status: 206,
176
- headers: {
177
- ...baseHeaders,
178
- "Content-Range": `bytes ${start}-${end}/${size}`,
179
- "Content-Length": String(end - start + 1),
180
- },
181
- })
182
- }
183
-
184
- return new Response(await file(filePath).bytes(), { headers: baseHeaders })
185
- }
186
-
187
- // Ticket-paired clients connect through this endpoint; serve() accepts its
188
- // connections directly alongside the TCP listener.
189
- let tunnel = config.tunnel ? await createTunnelEndpoint(config.port, config.keyDir) : null
190
-
191
- serve({
192
- port: config.port,
193
- p2p: tunnel ? { endpoint: tunnel, protocol: TUNNEL_PROTOCOL } : undefined,
194
- async fetch(req, server) {
195
- if (server.upgrade(req)) return
196
-
197
- let { path, query } = splitQuery(req.url)
198
-
199
- if (path === "/__proxy__") {
200
- return handleProxy(req)
201
- }
202
- if (path.startsWith("/__control__/")) {
203
- return handleControl(req, path, query)
204
- }
205
- if (path.startsWith("/__internal__/")) {
206
- return handleInternal(req, server, path)
207
- }
208
- // The assets/ convention roots at the project dir (package.json), which is
209
- // not necessarily the entry's dir the file routes serve; clients fetch
210
- // manifest asset paths here (live proxy reads and store installs alike).
211
- if (path === "/assets" || path.startsWith("/assets/")) {
212
- return handleFiles(req, path, state.projectDir)
213
- }
214
- // Isolate bundles are build outputs, not project files: srt (and the
215
- // rebuild here) write them under .srt-data/isolates/, and the manifest
216
- // lists them as isolates/<id>.js.
217
- if (path.startsWith("/isolates/")) {
218
- return handleFiles(req, path, config.cacheDir)
219
- }
220
- return handleFiles(req, path, state.sourceDir)
221
- },
222
- websocket: {
223
- open(ws) {
224
- let id = state.nextClientId++
225
- state.clients.set(ws, {
226
- platform: "unknown",
227
- version: "unknown",
228
- profile: "unknown",
229
- id,
230
- capabilities: [],
231
- queries: [],
232
- })
233
- console.log(`[cli] Client connected ${ws.remoteAddr ?? "unknown"}`)
234
- // Advertise our real LAN address so clients dialed over a loopback hop
235
- // can show/remember the directly reachable address (see connection.rs).
236
- ws.send(
237
- JSON.stringify({ type: "welcome", address: state.serverUrl, stats: state.stats, capture: !!config.capture }),
238
- )
239
- if (state.currentReload) {
240
- ws.send(state.currentReload)
241
- }
242
- },
243
- close(ws) {
244
- let info = state.clients.get(ws)
245
- state.clients.delete(ws)
246
- console.log(`[cli] Client disconnected: ${info?.platform ?? "unknown"}`)
247
- },
248
- message(ws, msg) {
249
- try {
250
- let data = JSON.parse(typeof msg === "string" ? msg : new TextDecoder().decode(msg))
251
- if (data.type === "info") {
252
- let existing = state.clients.get(ws)
253
- state.clients.set(ws, {
254
- platform: data.platform ?? "unknown",
255
- version: data.version ?? "unknown",
256
- profile: data.profile ?? "unknown",
257
- id: existing?.id ?? state.nextClientId++,
258
- capabilities: Array.isArray(data.capabilities) ? data.capabilities.map(String) : [],
259
- queries: Array.isArray(data.queries) ? data.queries.map(String) : [],
260
- })
261
- console.log(`[cli] Client info ${ws.remoteAddr ?? "unknown"} ${data.platform} (${data.version})`)
262
- } else if (data.type === "log") {
263
- // Forwarded console output / runtime errors from the client's
264
- // engine logger, buffered for the control API (see control.ts).
265
- // Not printed here: the local client already writes to this
266
- // terminal, so echoing would duplicate every line.
267
- let device = state.clients.get(ws)?.id ?? -1
268
- appendLog(device, String(data.level ?? "log"), String(data.text ?? ""))
269
- } else if (data.type === "result") {
270
- // Reply to a query the control API forwarded to this client.
271
- resolveQuery(data)
272
- } else if (data.type === "capture" && config.capture) {
273
- let device = state.clients.get(ws)?.id ?? -1
274
- // Milliseconds, integer: Date.now() is already integer ms, so the
275
- // delta needs no rounding.
276
- let at = Date.now() - state.captureStartMs
277
- let after = at - state.captureLastAt
278
- state.captureLastAt = at
279
- // JSON Lines: one event object per line, streamed to disk as it
280
- // arrives rather than buffered - no in-memory growth for a long
281
- // capture, and the file is always complete on disk mid-session.
282
- // Appends are chained so events land in arrival order.
283
- let line = JSON.stringify({ after, type: data.kind, key: data.key, device }) + "\n"
284
- state.captureChain = state.captureChain.then(() => file(config.capture!).append(line))
285
- }
286
- } catch {}
287
- },
288
- },
289
- })
290
-
291
- // One QR on screen: with the tunnel on, the ticket QR (printed by
292
- // createTunnelEndpoint) is the pairing story and the address stays text-only;
293
- // without it, the address QR is the scan target as before.
294
- if (!config.tunnel) {
295
- console.log("")
296
- printQr(state.serverUrl)
297
- console.log("")
298
- }
299
- console.log(`[cli] WebSocket server on ws://${state.serverUrl}`)
300
- // mDNS advertise is intentionally not implemented here: the p2p ticket is the
301
- // cross-device connect story (see docs/flux-dev-server-plan.md).
302
-
303
- // Keepalive
304
- setInterval(() => {
305
- for (let ws of state.clients.keys()) {
306
- ws.ping()
307
- }
308
- }, 5000)
package/server/rebuild.ts DELETED
@@ -1,68 +0,0 @@
1
- import { command } from "flux:subprocess"
2
- import { dir, file } from "flux:fs"
3
- import { state } from "./state"
4
-
5
- // Server-owned "rebuild and push": the single place the running app is rebuilt
6
- // from source on demand (an MCP reload). The srt repl still bundles in-process
7
- // for its own keystroke reloads, but both routes call the same bundle-cli, so
8
- // the bundling logic cannot drift. Making the server the rebuild authority is
9
- // the interim step toward folding the whole CLI into flux (see
10
- // okf/backlog/cli-flux-migration.md).
11
-
12
- // Build the reload message the same way srt's buildReload does, so a
13
- // server-triggered reload is indistinguishable from a repl-triggered one to
14
- // clients. proxyHttp is a message flag, not a build input.
15
- function buildReload(code: string, manifest?: string) {
16
- let config = state.config
17
- return {
18
- type: "reload",
19
- proxyHttp: config.proxyHttp,
20
- args: config.args,
21
- ...(manifest ? { manifest } : {}),
22
- code,
23
- }
24
- }
25
-
26
- // Rebuild from state.config.entry via the external bundle-cli subprocess, then
27
- // latch (for late-joining clients) and broadcast the reload to every connected
28
- // client. Resolves with an error message on failure (no entry configured, or a
29
- // build error), or null on success.
30
- export async function rebuildAndBroadcast(): Promise<string | null> {
31
- let config = state.config
32
- if (!config.entry) {
33
- return "No app entry to rebuild. Start srt with a source file (srt run src/index.tsx)."
34
- }
35
-
36
- let params = JSON.stringify({
37
- entry: config.entry,
38
- devBase: state.serverUrl,
39
- dev: true,
40
- minify: config.minify,
41
- })
42
-
43
- let result = await command(config.bundlerCmd[0]!, [...config.bundlerCmd.slice(1), params]).output()
44
- if (!result.success) {
45
- let stderr = typeof result.stderr === "string" ? result.stderr : ""
46
- return `Rebuild failed:\n${stderr.trim()}`
47
- }
48
-
49
- // bundle-cli writes one JSON object { code, map, manifest, isolates } to stdout.
50
- let bundle: { code?: string; map?: string | null; manifest?: string; isolates?: { id: string; code: string }[] }
51
- try {
52
- bundle = JSON.parse(typeof result.stdout === "string" ? result.stdout : "")
53
- } catch {
54
- return "Rebuild failed: unreadable bundler output"
55
- }
56
- // Isolate bundles are manifest assets clients fetch from our /isolates/
57
- // route (served from cacheDir), so they must be on disk before the push.
58
- for (let isolate of bundle.isolates ?? []) {
59
- let path = `${config.cacheDir}/isolates/${isolate.id}.js`
60
- await dir(path.slice(0, path.lastIndexOf("/"))).create()
61
- await file(path).write(isolate.code)
62
- }
63
- state.currentMap = bundle.map ?? null
64
- let text = JSON.stringify(buildReload(bundle.code ?? "", bundle.manifest))
65
- state.currentReload = text
66
- for (let ws of state.clients.keys()) ws.send(text)
67
- return null
68
- }
package/server/remap.ts DELETED
@@ -1,47 +0,0 @@
1
- import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping"
2
-
3
- // Stack-trace remapping for forwarded client logs. The runtime evaluates the
4
- // bundle as module "main", so QuickJS frames cite bundle positions like
5
- // "at boom (main:212:9)". With the current reload's sourcemap latched on the
6
- // server (state.currentMap), those positions are rewritten to the original
7
- // .tsx sources before a log entry is buffered.
8
-
9
- // The parsed map is cached per map text; a reload swaps the text and the next
10
- // lookup rebuilds the tracer.
11
- let cachedText: string | null = null
12
- let tracer: TraceMap | null = null
13
-
14
- function tracerFor(map: string | null): TraceMap | null {
15
- if (map !== cachedText) {
16
- cachedText = map
17
- tracer = null
18
- if (map) {
19
- try {
20
- tracer = new TraceMap(JSON.parse(map))
21
- } catch {
22
- // A malformed map disables remapping until the next reload.
23
- }
24
- }
25
- }
26
- return tracer
27
- }
28
-
29
- /**
30
- * Rewrite every "main:LINE:COL" (or "main:LINE") position in `text` to its
31
- * original source position, e.g. "src/app.tsx:42:7". Positions the map has no
32
- * entry for, and all text when `map` is null, pass through unchanged.
33
- * QuickJS lines and columns are 1-based; sourcemap columns are 0-based.
34
- */
35
- export function remapPositions(text: string, map: string | null): string {
36
- if (!map || !text.includes("main:")) return text
37
- let t = tracerFor(map)
38
- if (!t) return text
39
- return text.replace(/\bmain:(\d+)(?::(\d+))?\b/g, (frame, line, column) => {
40
- let pos = originalPositionFor(t, {
41
- line: parseInt(line, 10),
42
- column: column ? Math.max(parseInt(column, 10) - 1, 0) : 0,
43
- })
44
- if (pos.source == null || pos.line == null) return frame
45
- return `${pos.source}:${pos.line}:${(pos.column ?? 0) + 1}`
46
- })
47
- }
package/server/state.ts DELETED
@@ -1,93 +0,0 @@
1
- // Server-side state shared by the route handlers. The srt process keeps the
2
- // bundler, watcher, and repl; this state is only what the protocol needs.
3
-
4
- import type { ServerWebSocket } from "flux:http"
5
-
6
- export type Config = {
7
- port: number
8
- /** Directory served by the file routes (updatable via /__internal__/reload). */
9
- sourceDir: string
10
- /** Project root whose assets/ folder the /assets/ route serves (updatable
11
- * via /__internal__/reload, moves with the repl `load`). */
12
- projectDir: string
13
- /** The address clients can reach this machine on (LAN IP or 127.0.0.1). */
14
- address: string
15
- proxyHttp: boolean
16
- /** The app entry (absolute .tsx/.jsx path) the server rebuilds on an
17
- * MCP-triggered reload, or undefined when srt was started without a source.
18
- * Moved by the repl `load` command via /__internal__/reload. */
19
- entry?: string
20
- /** The session's app arguments (the srt command-line tail after a bare
21
- * "--"), included in every reload push as flux:process argv. */
22
- args: string[]
23
- /** Minify the rebuild output, mirroring the srt --minify flag. */
24
- minify: boolean
25
- /** How the server invokes the external bundler: [bunPath, bundleCliPath],
26
- * spawned with a JSON params argument appended (see rebuild.ts). */
27
- bundlerCmd: string[]
28
- /** Enable the sqlite-backed proxy cache. */
29
- cache: boolean
30
- /** Directory holding the proxy cache db (the project-local .srt-data). */
31
- cacheDir: string
32
- /** Directory holding tunnel.key (the server's ~/.solidrt/servers/<port>/ folder). */
33
- keyDir: string
34
- /** Destination for captured key events, or unset when off. */
35
- capture?: string
36
- stats: boolean
37
- /** Accept ticket-paired clients through the p2p tunnel. */
38
- tunnel: boolean
39
- }
40
-
41
- export type ClientInfo = {
42
- platform: string
43
- version: string
44
- profile: string
45
- id: number
46
- capabilities: string[]
47
- /** Query kinds this client's runtime answers (empty on runtimes that predate
48
- * the advertisement); dev tools plan their verification surface from it. */
49
- queries: string[]
50
- }
51
-
52
- export let state = {
53
- config: undefined as unknown as Config,
54
- clients: new Map<ServerWebSocket, ClientInfo>(),
55
- nextClientId: 0,
56
- /**
57
- * Identity of this server run, included in control responses that carry
58
- * cross-call state (client ids, log seq cursors). Both reset on restart, so
59
- * a consumer that sees the generation change knows its ids and cursors are
60
- * from a dead server and must be re-fetched.
61
- */
62
- generation: Date.now(),
63
- /**
64
- * The latched reload message (JSON text), replayed to late-joining clients.
65
- * Set by /__internal__/reload posts with `latch`, cleared by a broadcast stop.
66
- */
67
- currentReload: null as string | null,
68
- /**
69
- * The running bundle's sourcemap (JSON text, bundle -> .tsx sources), used
70
- * to remap stack traces in forwarded client logs (see control.ts). Replaced
71
- * on every reload; a reload without a map clears it so frames are never
72
- * remapped against a stale map.
73
- */
74
- currentMap: null as string | null,
75
- sourceDir: "",
76
- projectDir: "",
77
- serverUrl: "",
78
- stats: false,
79
- /**
80
- * Whether srt's file watcher may auto-reload on source changes. Agents
81
- * pause it (MCP watch tool -> /__control__/watch) while creating or
82
- * editing files; a successful /reload or /load re-enables it. srt reads
83
- * it via /__internal__/watch before acting on a change event.
84
- */
85
- watch: true,
86
- // Capture events from all connected clients share one clock (captureStartMs,
87
- // integer milliseconds) so they merge into one coherent timeline, tagged by
88
- // `device`. Streamed to disk as JSON Lines - see main.ts's "capture" handling.
89
- captureStartMs: 0,
90
- captureLastAt: 0, // ms, same clock as captureStartMs
91
- /** Serializes capture appends so events land on disk in arrival order. */
92
- captureChain: Promise.resolve(),
93
- }