@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
@@ -23,13 +23,13 @@ const KEY_FILE = "tunnel.key"
23
23
  /**
24
24
  * Bind the tunnel endpoint and print its ticket (text + QR). The endpoint is
25
25
  * kept stable across restarts so a paired client can re-dial the old ticket
26
- * without re-scanning: the UDP port is pinned to the dev server's port, and the
27
- * secret key is persisted in <keyDir>/tunnel.key (generated on first
28
- * run). Both are needed - a moving port or a fresh key each start would change
29
- * the ticket. Stable across restarts on the same network only; a new machine IP
26
+ * without re-scanning: the UDP port follows the dev server's remembered port
27
+ * (ephemeral on a first run), and the secret key is persisted in
28
+ * <keyDir>/tunnel.key (generated on first run). Both are needed - a moving
29
+ * port or a fresh key each start would change the ticket. Stable across restarts on the same network only; a new machine IP
30
30
  * still stales the ticket's addresses (that is the discovery/off-LAN story).
31
31
  */
32
- export async function createTunnelEndpoint(port: number, keyDir: string): Promise<Endpoint> {
32
+ export async function createTunnelEndpoint(port: number | null, keyDir: string): Promise<Endpoint> {
33
33
  let keyPath = join(keyDir, KEY_FILE)
34
34
 
35
35
  let secretKey: string | undefined
@@ -39,7 +39,7 @@ export async function createTunnelEndpoint(port: number, keyDir: string): Promis
39
39
  if (saved.length === 64) secretKey = saved
40
40
  }
41
41
 
42
- let endpoint = await Endpoint.create({ local: true, protocols: [TUNNEL_PROTOCOL], port, secretKey })
42
+ let endpoint = await Endpoint.create({ local: true, protocols: [TUNNEL_PROTOCOL], ...(port !== null ? { port } : {}), secretKey })
43
43
 
44
44
  // First run (no saved key): persist the freshly generated one so the next run
45
45
  // reuses it and the ticket stays the same.
@@ -0,0 +1,121 @@
1
+ import { dir } from "flux:fs"
2
+ import { state } from "./state"
3
+ import { dirname } from "./mode"
4
+ import { rebuildAndBroadcast, showBuildFailure } from "./rebuild"
5
+
6
+ // Reload-on-save. Not a directory watch: the watch set is the bundle's own
7
+ // input list (BundleOutput.inputs: the app's modules, the dependency modules
8
+ // bundled in, inlined files, package.json, tsconfig.json), so a source file
9
+ // the running app does not import, or a dependency it does not use, never
10
+ // triggers anything, while an edit to a workspace package it does import
11
+ // does. Each input's parent directory is watched (never the file itself: an
12
+ // editor's atomic save replaces the inode, which a file watch would lose)
13
+ // and events are matched by name within that directory. On top of that, in
14
+ // project mode, the whole assets/ tree: the manifest lists the tree, so any
15
+ // change to it is a new version. While the last build failed the input list
16
+ // is gone, so the source directory is watched as a whole until a build
17
+ // succeeds again (the missing-import case: the file the build waits for is
18
+ // not in any list yet). Every hit debounces into the one
19
+ // rebuildAndBroadcast() path, which re-arms the watch from the new inputs.
20
+ // Paused by the MCP pause_watch tool while an agent edits (state.watchPaused):
21
+ // changes made meanwhile are not pushed; the agent's reload is.
22
+
23
+ const DEBOUNCE_MS = 100
24
+ const SOURCE_EXT = /\.(tsx?|jsx?)$/
25
+
26
+ let offs: (() => void)[] = []
27
+ let timer: ReturnType<typeof setTimeout> | null = null
28
+ let changed = new Set<string>()
29
+ let building = false
30
+ let dirty = false
31
+
32
+ function basename(path: string): string {
33
+ return path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1)
34
+ }
35
+
36
+ // A path with a dot component (below `root` when given) is never a source:
37
+ // .srt-data in particular is this server's own output, and rebuilding on it
38
+ // would rebuild forever. node_modules is the same in file mode, where the
39
+ // source directory may be a project root.
40
+ function isToolingPath(path: string, root: string): boolean {
41
+ let rel = path.startsWith(root) ? path.slice(root.length) : path
42
+ return rel.split(/[\\/]/).some((part) => part.startsWith(".") || part === "node_modules")
43
+ }
44
+
45
+ /** Arm the watch from a build's inputs; null (a failed build) watches the source tree as a whole. */
46
+ export function armWatcher(inputs: string[] | null) {
47
+ stopWatcher()
48
+ let config = state.config
49
+ let hit = (path: string) => onChange(path)
50
+ try {
51
+ if (inputs) {
52
+ let byDir = new Map<string, Set<string>>()
53
+ for (let input of inputs) {
54
+ let parent = dirname(input)
55
+ let names = byDir.get(parent)
56
+ if (!names) byDir.set(parent, (names = new Set()))
57
+ names.add(basename(input))
58
+ }
59
+ for (let [parent, names] of byDir) {
60
+ offs.push(dir(parent).watch((e) => names.has(basename(e.path)) && hit(e.path)))
61
+ }
62
+ } else {
63
+ let root = config.sourceDir
64
+ offs.push(
65
+ dir(root).watch((e) => SOURCE_EXT.test(e.path) && !isToolingPath(e.path, root) && hit(e.path), { recursive: true }),
66
+ )
67
+ }
68
+ if (config.projectDir) {
69
+ let assets = `${config.projectDir}/assets`
70
+ dir(assets)
71
+ .exists()
72
+ .then((exists) => {
73
+ if (exists && offs.length) offs.push(dir(assets).watch((e) => !isToolingPath(e.path, assets) && hit(e.path), { recursive: true }))
74
+ })
75
+ }
76
+ } catch (e) {
77
+ console.error(`[cli] Watch failed: ${e instanceof Error ? e.message : e}`)
78
+ stopWatcher()
79
+ }
80
+ }
81
+
82
+ /** Drop every watch; the engine loop can go idle. */
83
+ export function stopWatcher() {
84
+ for (let off of offs) off()
85
+ offs = []
86
+ if (timer !== null) clearTimeout(timer)
87
+ timer = null
88
+ changed.clear()
89
+ }
90
+
91
+ function onChange(path: string) {
92
+ if (state.watchPaused) return
93
+ changed.add(path)
94
+ if (building) {
95
+ dirty = true
96
+ return
97
+ }
98
+ if (timer !== null) clearTimeout(timer)
99
+ timer = setTimeout(() => void rebuild(), DEBOUNCE_MS)
100
+ }
101
+
102
+ // One rebuild per burst; a change that lands while the bundle runs queues
103
+ // exactly one more, since the running build cannot have seen it.
104
+ async function rebuild() {
105
+ timer = null
106
+ building = true
107
+ let root = state.config.projectDir ?? state.config.sourceDir
108
+ let names = [...changed].map((p) => (p.startsWith(root) ? p.slice(root.length + 1) : p))
109
+ changed.clear()
110
+ console.log(`[cli] Change detected: ${names.slice(0, 3).join(", ")}${names.length > 3 ? ` (+${names.length - 3})` : ""}`)
111
+ let error = await rebuildAndBroadcast()
112
+ if (error) {
113
+ console.error(error)
114
+ showBuildFailure()
115
+ }
116
+ building = false
117
+ if (dirty) {
118
+ dirty = false
119
+ onChange(names[0] ?? root)
120
+ }
121
+ }
@@ -0,0 +1,70 @@
1
+ import { readdirSync } from "node:fs"
2
+ import { join, resolve } from "node:path"
3
+ import { source, toolArgs } from "../lib/args"
4
+
5
+ // srt tool: the tools the installed @solidrt packages ship - build-time
6
+ // helpers that belong to an extension, not to core (a model converter in
7
+ // @solidrt/3d, say). Discovery is by convention, like demos: every
8
+ // `<package>/tools/<name>.ts` is a tool named `<package>/<name>`, run with
9
+ // bun in the caller's cwd with the arguments after the tool name passed
10
+ // through untouched. srt knows nothing about what a tool does; a tool
11
+ // prints its own usage.
12
+
13
+ type Tool = { name: string; script: string }
14
+
15
+ const SCOPE = join("node_modules", "@solidrt")
16
+
17
+ /** Every tool installed here, sorted so the listing is stable. The cwd and
18
+ * nothing above it - the same rule demos follow, so this lists what THIS
19
+ * project installed. */
20
+ function discover(): Tool[] {
21
+ let tools: Tool[] = []
22
+ for (let pkg of names(SCOPE)) {
23
+ let dir = join(SCOPE, pkg, "tools")
24
+ for (let file of names(dir)) {
25
+ if (!file.endsWith(".ts")) continue
26
+ tools.push({ name: `${pkg}/${file.slice(0, -".ts".length)}`, script: resolve(dir, file) })
27
+ }
28
+ }
29
+ return tools
30
+ }
31
+
32
+ // A missing folder is the normal case (most packages ship no tools), so it
33
+ // reads as an empty one rather than an error.
34
+ function names(dir: string): string[] {
35
+ try {
36
+ return readdirSync(dir).sort()
37
+ } catch {
38
+ return []
39
+ }
40
+ }
41
+
42
+ function list(tools: Tool[]) {
43
+ for (let tool of tools) console.log(` ${tool.name}`)
44
+ }
45
+
46
+ export async function main(): Promise<void> {
47
+ let tools = discover()
48
+ if (tools.length === 0) {
49
+ console.error(`No tools installed in ${process.cwd()} (looked in ${SCOPE}/*/tools/)`)
50
+ process.exit(1)
51
+ }
52
+
53
+ if (source === undefined) {
54
+ list(tools)
55
+ console.log("\nRun one with: srt tool <pkg>/<name> [arguments]")
56
+ return
57
+ }
58
+
59
+ let picked = tools.find((t) => t.name === source)
60
+ if (!picked) {
61
+ console.error(`No such tool: ${source}`)
62
+ list(tools)
63
+ process.exit(1)
64
+ }
65
+
66
+ let proc = Bun.spawn([process.execPath, picked.script, ...toolArgs], {
67
+ stdio: ["inherit", "inherit", "inherit"],
68
+ })
69
+ process.exit(await proc.exited)
70
+ }
@@ -0,0 +1,24 @@
1
+ // What `srt bundle --json` (src/bundle/main.ts, a bun subprocess of the
2
+ // dev server) writes to stdout as one JSON object and the dev server
3
+ // (src/server/rebuild.ts) reads back. src/types/ holds the type-only contracts
4
+ // between the two programs: each has its own tsconfig (bun types on one
5
+ // side, flux types on the other), both include this folder, so nothing here
6
+ // may reference either runtime.
7
+
8
+ export type BundleOutput = {
9
+ code: string
10
+ /** Composed sourcemap JSON (bundle -> original .tsx sources), dev builds only. */
11
+ map: string | null
12
+ /** Version manifest JSON for this bundle; clients install pushes under its hash. */
13
+ manifest: string
14
+ /** The app's isolate bundles, one per "use isolate" module, in id order; maps dev builds only. */
15
+ isolates: { id: string; code: string; map: string | null }[]
16
+ /**
17
+ * Every file this bundle was built from, absolute and sorted: the app's
18
+ * modules, the dependency modules bundled in, files inlined by import
19
+ * attribute, and the project's package.json / tsconfig.json. The assets/
20
+ * tree is not listed (the manifest carries it). What the dev server
21
+ * watches for reload-on-save.
22
+ */
23
+ inputs: string[]
24
+ }
@@ -0,0 +1,90 @@
1
+ // The control API (/__control__/, server/control.ts) response shapes the
2
+ // MCP bridge (src/mcp/main.ts) reads fields from. Only the server-held
3
+ // answers are shaped here; queries forwarded to a client return whatever the
4
+ // client runtime answers. See bundle.d.ts for the folder.
5
+
6
+ export type ClientEntry = {
7
+ id: number
8
+ platform: string
9
+ /** The client runtime's version (git describe). */
10
+ version: string
11
+ /** Build profile: debug or release. */
12
+ profile: string
13
+ capabilities: string[]
14
+ /** Query kinds this client's runtime answers (empty on runtimes that predate
15
+ * the advertisement); dev tools plan their verification surface from it. */
16
+ queries: string[]
17
+ /** The client's storage tree on its own machine (`<data-root>/client<N>`
18
+ * for a dev client, the install folder for the launcher or a packed app),
19
+ * or null when it runs without writable storage or predates the field. */
20
+ clientDir: string | null
21
+ /** Whether its stats overlay is drawn (see POST /stats). */
22
+ stats: boolean
23
+ /** Its time scale as the client last reported it to POST /clock: 0 paused,
24
+ * 1 real time. Back to 1 on every push (a reload restarts the clock). */
25
+ timeScale: number
26
+ /** The client's process id on its own machine. */
27
+ pid: number | null
28
+ /** The runtime binary it runs. */
29
+ execPath: string | null
30
+ /** Its machine's hostname. */
31
+ host: string | null
32
+ /** The OS as a person names it ("Linux (Ubuntu 24.04)", "Android 15 on Pixel 9 Pro"). */
33
+ os: string | null
34
+ /** The kernel version. */
35
+ kernel: string | null
36
+ /** The SDL video driver ("wayland", "x11", "android", "offscreen", ...). */
37
+ videoDriver: string | null
38
+ /** The GPU strings as GL reports them; null on a client that connected
39
+ * before its GL context existed (a reconnect fills it in). */
40
+ gpu: GpuInfo | null
41
+ }
42
+
43
+ export type GpuInfo = { vendor: string; renderer: string; version: string }
44
+
45
+ /** GET /clients */
46
+ export type ClientsResponse = {
47
+ /** Identity of this server run: client ids and log cursors are only valid within one. */
48
+ generation: number
49
+ key: string
50
+ mode: "project" | "file"
51
+ entry: string
52
+ projectDir: string | null
53
+ /** Whether the user's own input is muted on every client (see /mute). */
54
+ userInputMuted: boolean
55
+ /** Whether reload-on-save is paused (see /watch). */
56
+ watchPaused: boolean
57
+ clients: ClientEntry[]
58
+ }
59
+
60
+ export type LogEntry = { seq: number; at: number; client: number; level: string; text: string }
61
+
62
+ /** GET /logs */
63
+ export type LogsResponse = {
64
+ /** Consecutive identical entries come back as one, with `repeats`. */
65
+ entries: (LogEntry & { repeats?: number })[]
66
+ /** The next `since` cursor. */
67
+ latest: number
68
+ generation: number
69
+ }
70
+
71
+ /** POST /reload */
72
+ export type ReloadResponse = { ok: true; clients: number }
73
+
74
+ /** POST /load: the canonical entry now served. */
75
+ export type LoadResponse = { ok: true; entry: string; clients: number }
76
+
77
+ /** POST /mute: the mute state now in force and the clients told. */
78
+ export type MuteResponse = { ok: true; active: boolean; clients: number }
79
+
80
+ /** POST /stats: whether the overlay is now on, and on how many clients. */
81
+ export type StatsResponse = { ok: true; active: boolean; clients: number }
82
+
83
+ /** POST /watch: whether reload-on-save is now active. */
84
+ export type WatchResponse = { ok: true; active: boolean }
85
+
86
+ /** GET /snapshot and /texture: png by default, RGBA8 bytes with format=raw. */
87
+ export type ImageResponse = { width: number; height: number; pngBase64?: string; rgbaBase64?: string }
88
+
89
+ /** Every non-2xx answer. */
90
+ export type ControlError = { error: string }
@@ -0,0 +1,16 @@
1
+ // The dev server registry record: ~/.solidrt/servers/<key hash>/live.json,
2
+ // written by the server (server/registry.ts) and read by srt client, srt mcp
3
+ // and the launcher (src/lib/registry.ts). See bundle.d.ts for the folder.
4
+
5
+ export type LiveRecord = {
6
+ pid: number
7
+ port: number
8
+ address: string
9
+ /** The canonical project root or file path (see src/lib/mode.ts). */
10
+ key: string
11
+ mode: "project" | "file"
12
+ entry: string
13
+ projectDir: string | null
14
+ /** ISO timestamp of the bind. */
15
+ started: string
16
+ }