@solidrt/cli 0.0.51 → 0.0.53

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 (100) hide show
  1. package/AGENTS.md +68 -38
  2. package/README.md +86 -15
  3. package/agents/assets.md +19 -5
  4. package/agents/debugging.md +173 -17
  5. package/dist/console.srtapp +73916 -51
  6. package/dist/demos/3d/assets/icon.svg +23 -0
  7. package/dist/demos/3d/package.json +9 -0
  8. package/dist/demos/3d/the-third-dimension/the-third-dimension.srt.js +7528 -0
  9. package/dist/demos/components/assets/icon.png +0 -0
  10. package/dist/demos/components/assets/icon.svg +23 -0
  11. package/dist/demos/components/gallery/gallery.srt.js +14087 -0
  12. package/dist/demos/components/package.json +9 -0
  13. package/dist/server.js +3936 -0
  14. package/package.json +10 -11
  15. package/src/android/docs.md +21 -0
  16. package/src/android/main.ts +286 -0
  17. package/src/{bundler.ts → bundle/bundler.ts} +70 -58
  18. package/src/bundle/docs.md +12 -0
  19. package/src/{commands/bundle.ts → bundle/main.ts} +67 -25
  20. package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +3 -3
  21. package/src/check/docs.md +10 -0
  22. package/src/check/main.ts +85 -0
  23. package/src/{commands/check.ts → check/typecheck.ts} +11 -41
  24. package/src/client/docs.md +9 -0
  25. package/src/client/main.ts +33 -0
  26. package/src/console/docs.md +14 -0
  27. package/src/console/main.ts +21 -0
  28. package/src/demo/docs.md +33 -0
  29. package/src/demo/main.ts +69 -0
  30. package/src/init/docs.md +11 -0
  31. package/src/{commands/init.ts → init/main.ts} +21 -15
  32. package/src/init/scaffold/AGENTS.md +100 -0
  33. package/src/init/scaffold/package.json +23 -0
  34. package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
  35. package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
  36. package/src/lib/args.ts +194 -0
  37. package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
  38. package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
  39. package/src/{fonts.ts → lib/fonts.ts} +12 -26
  40. package/src/lib/mode.ts +77 -0
  41. package/src/{project.ts → lib/project.ts} +109 -61
  42. package/src/lib/registry.ts +120 -0
  43. package/src/lib/server-bundle.ts +24 -0
  44. package/src/lib/usage.ts +117 -0
  45. package/src/lib/util.ts +36 -0
  46. package/src/main.ts +109 -31
  47. package/src/mcp/docs.md +22 -0
  48. package/src/mcp/main.ts +719 -0
  49. package/src/pack/docs.md +18 -0
  50. package/src/{pack-folder.ts → pack/layout.ts} +13 -22
  51. package/src/{commands/pack.ts → pack/main.ts} +28 -17
  52. package/src/{packer.ts → pack/trailer.ts} +32 -42
  53. package/src/render/docs.md +19 -0
  54. package/src/{commands/render.ts → render/main.ts} +17 -15
  55. package/src/server/args.ts +126 -0
  56. package/src/server/binaries.ts +47 -0
  57. package/src/server/config.ts +54 -0
  58. package/{server → src/server}/control.ts +239 -81
  59. package/src/server/docs.md +51 -0
  60. package/src/server/line-editor.ts +200 -0
  61. package/src/server/main.ts +476 -0
  62. package/src/server/mode.ts +92 -0
  63. package/src/server/rebuild.ts +90 -0
  64. package/src/server/registry.ts +138 -0
  65. package/src/server/repl.ts +223 -0
  66. package/src/server/state.ts +54 -0
  67. package/{server → src/server}/tsconfig.json +1 -1
  68. package/{server → src/server}/tunnel.ts +6 -6
  69. package/src/server/watcher.ts +121 -0
  70. package/src/tool/main.ts +70 -0
  71. package/src/types/bundle.d.ts +24 -0
  72. package/src/types/control.d.ts +90 -0
  73. package/src/types/registry.d.ts +16 -0
  74. package/scaffold/AGENTS.md +0 -185
  75. package/scaffold/package.json +0 -22
  76. package/scaffold/templates/components/icon.tsx +0 -48
  77. package/scaffold/templates/default/icon.tsx +0 -48
  78. package/server/main.ts +0 -308
  79. package/server/rebuild.ts +0 -76
  80. package/server/state.ts +0 -94
  81. package/src/args.ts +0 -210
  82. package/src/bundle-cli.ts +0 -13
  83. package/src/commands/client.ts +0 -34
  84. package/src/commands/mcp.ts +0 -617
  85. package/src/commands/server.ts +0 -73
  86. package/src/dev-android.ts +0 -176
  87. package/src/dev-client.ts +0 -29
  88. package/src/dev-server.ts +0 -302
  89. package/src/repl.ts +0 -249
  90. package/src/util.ts +0 -122
  91. package/src/watcher.ts +0 -73
  92. /package/src/{prompt.ts → init/prompt.ts} +0 -0
  93. /package/{scaffold → src/init/scaffold}/gitignore +0 -0
  94. /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
  95. /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
  96. /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
  97. /package/{server → src/server}/cache.ts +0 -0
  98. /package/{server → src/server}/proxy.ts +0 -0
  99. /package/{server → src/server}/qr.ts +0 -0
  100. /package/{server → src/server}/remap.ts +0 -0
@@ -0,0 +1,100 @@
1
+ # SolidRT app - agent notes
2
+
3
+ This project uses SolidRT: a custom SolidJS renderer that paints through a Rust
4
+ runtime. No DOM, no HTML, no CSS cascade. If you are an AI assistant, read this
5
+ whole file before writing or editing code here - it is short on purpose. The
6
+ depth lives in the topic files listed under "Read before you", one of which you
7
+ should open whenever the work matches its trigger.
8
+
9
+ ## Levels: core, and extensions on top
10
+
11
+ - @solidrt/core is the low-level foundation: host intrinsics (`<window>`,
12
+ `<view>`, `<text>`, the detached `d-*` drawing primitives) with flat props
13
+ that feed the layout and paint engine directly. An app can be written
14
+ entirely at this level. d-* elements go under anything; laid-out elements
15
+ only under laid-out parents (a `<view>` inside a `<d-view>` throws), so a
16
+ component's doc comment says which kind it renders.
17
+ - Extensions build on core. The first-party ones are @solidrt/components
18
+ (UI components), @solidrt/2d (2D graphics) and @solidrt/3d (3D graphics).
19
+ None is privileged - an extension is just functions returning core JSX,
20
+ and an app can use a third-party one or grow its own.
21
+
22
+ Match the level the code you are editing already uses. package.json shows
23
+ the choice this app made: only the extensions listed there are installed -
24
+ do not add one for a change core covers.
25
+
26
+ Convention: every @solidrt package describes itself in
27
+ node_modules/@solidrt/<name>/AGENTS.md, carries its prose in docs/, and
28
+ ships working code in examples/. AGENTS.md is the reference for the package;
29
+ open it before using anything from the package, and read the extensions'
30
+ files only when they are installed.
31
+
32
+ ## Read before you
33
+
34
+ The authoritative references ship inside the installed packages. Open the one
35
+ that matches the work; do not work from memory of what a web framework does.
36
+
37
+ - write any reactive code (signals, effects, control flow) ->
38
+ node_modules/solid-js/CHEATSHEET.md - the SolidJS 2.0 model
39
+ - touch elements, props, events, gestures or text ->
40
+ node_modules/@solidrt/core/AGENTS.md, and
41
+ node_modules/@solidrt/core/src/types.d.ts + jsx-runtime.d.ts (source of truth)
42
+ - style a screen: a background, a gradient, a shadow, an effect, vector art,
43
+ a chart -> node_modules/@solidrt/core/agents/painting.md
44
+ - write per-frame code, an animation, or anything writing properties in a
45
+ loop -> node_modules/@solidrt/core/agents/performance.md
46
+ - use an installed extension (UI components, 2D, 3D) ->
47
+ node_modules/@solidrt/<name>/AGENTS.md and its examples/
48
+ - debug a running app, or drive it over MCP to verify a change ->
49
+ node_modules/@solidrt/cli/agents/debugging.md
50
+ - add an asset or font, set the app's identity, or build for distribution ->
51
+ node_modules/@solidrt/cli/agents/assets.md
52
+ - run, bundle, typecheck or render headlessly ->
53
+ node_modules/@solidrt/cli/AGENTS.md
54
+ - copy a working pattern -> node_modules/@solidrt/core/examples/ (see its
55
+ README.md index)
56
+
57
+ <!-- Claude Code auto-imports these; other tools read the paths above. -->
58
+ @./node_modules/solid-js/CHEATSHEET.md
59
+ @./node_modules/@solidrt/core/AGENTS.md
60
+ <!-- components:begin -->
61
+ @./node_modules/@solidrt/components/AGENTS.md
62
+ <!-- components:end -->
63
+ <!-- 2d:begin -->
64
+ @./node_modules/@solidrt/2d/AGENTS.md
65
+ <!-- 2d:end -->
66
+ <!-- 3d:begin -->
67
+ @./node_modules/@solidrt/3d/AGENTS.md
68
+ <!-- 3d:end -->
69
+ @./node_modules/@solidrt/cli/AGENTS.md
70
+
71
+ ## The three traps that cost the most (this is not React/DOM)
72
+
73
+ Each package's AGENTS.md carries its own trap list; these three are
74
+ platform-wide and bite in every app:
75
+
76
+ 1. Reading a signal/prop/store at the top level of a component body (not
77
+ inside JSX, a `createMemo`, or an effect's compute phase) reads it
78
+ untracked - it silently freezes at the initial value. `createEffect` is
79
+ two-argument here: `(compute, apply)`; the Solid 1.x single-arg form does
80
+ not track.
81
+ 2. Writing a signal or store from inside an owned scope - a component body,
82
+ a `createMemo`, an effect's compute phase - throws
83
+ `REACTIVE_WRITE_IN_OWNED_SCOPE` in dev. Move the write into an event
84
+ handler, an effect's apply phase, `onSettled`, or an `untrack` block; opt
85
+ in with `createSignal(v, { ownedWrite: true })` for internal state.
86
+ 3. An element-valued prop (children, a content/icon slot) builds a fresh
87
+ native subtree on EVERY read, and an uninserted subtree is never freed -
88
+ a permanent memory leak, not wasted work. Read such props exactly once,
89
+ where they mount; inspect them through the `children()` helper.
90
+
91
+ ## Run / verify
92
+
93
+ - FIRST check whether a dev server and a client are already running and
94
+ build against those; do not start a second `srt run` when one is up.
95
+ - The dev loop (reload, logs, snapshots, the holds on reload-on-save and on
96
+ the user's input), typechecking, headless rendering and the MCP tools:
97
+ node_modules/@solidrt/cli/AGENTS.md and its agents/debugging.md. Read it
98
+ before the first reload. The `.mcp.json` here is Claude Code's convention;
99
+ if your client lists no `solidrt` tools, debugging.md has the entry to add
100
+ to its own config.
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "solidrt-app",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "srt run",
7
+ "pack": "srt pack -o out",
8
+ "server": "srt server",
9
+ "client": "srt client",
10
+ "android": "srt android"
11
+ },
12
+ "dependencies": {
13
+ "@solidrt/core": "0.0.53",
14
+ "@solidrt/components": "0.0.53",
15
+ "@solidrt/2d": "0.0.53",
16
+ "@solidrt/3d": "0.0.53"
17
+ },
18
+ "devDependencies": {
19
+ "@solidrt/cli": "0.0.53",
20
+ "@solidrt/flux-types": "0.0.53",
21
+ "typescript": "^7"
22
+ }
23
+ }
@@ -1,7 +1,6 @@
1
1
  // Components template: built with the @solidrt/components framework.
2
- import { render, createLinearGradient } from "@solidrt/core"
2
+ import { render, createLinearGradient, Logo } from "@solidrt/core"
3
3
  import { Window, SafeArea, View, Text } from "@solidrt/components"
4
- import { Icon } from "./icon"
5
4
 
6
5
  function App() {
7
6
  let backgroundColor = createLinearGradient(0, 0, 1, 1, [
@@ -13,7 +12,7 @@ function App() {
13
12
  <Window title="The Solid Runtime" style={{ backgroundColor }}>
14
13
  <SafeArea>
15
14
  <View layout={{ flex: 1, gap: 20, alignItems: "center", justifyContent: "center" }}>
16
- <Icon />
15
+ <Logo size={300} animation="loop" />
17
16
  <Text layout={{ fontSize: 40 }} style={{ color: "#ccc" }}>
18
17
  The Solid Runtime
19
18
  </Text>
@@ -1,5 +1,4 @@
1
- import { render, createLinearGradient, safeArea } from "@solidrt/core"
2
- import { Icon } from "./icon"
1
+ import { render, createLinearGradient, safeArea, Logo } from "@solidrt/core"
3
2
 
4
3
  function App() {
5
4
  let backgroundColor = createLinearGradient(0, 0, 1, 1, [
@@ -18,7 +17,7 @@ function App() {
18
17
  paddingTop={safeArea().top}
19
18
  paddingBottom={safeArea().bottom}
20
19
  >
21
- <Icon />
20
+ <Logo size={300} animation="loop" />
22
21
  <text fontSize={40} color="#ccc">The Solid Runtime</text>
23
22
  </view>
24
23
  </window>
@@ -0,0 +1,194 @@
1
+ import { existsSync, statSync } from "node:fs"
2
+ import { parseArgs } from "node:util"
3
+ import { resolve } from "node:path"
4
+ import { clientsRoot } from "./dev-dir"
5
+ import { hint } from "./usage"
6
+
7
+ // Everything after a bare "--" is the app's argument vector, kept out of
8
+ // parseArgs (which would fold it into positionals) and forwarded verbatim to
9
+ // the runner, where it becomes flux:process argv. srt's own flags may follow
10
+ // the source file, so the separator is required.
11
+ let rawArgs = process.argv.slice(2)
12
+ let appArgsSep = rawArgs.indexOf("--")
13
+ export let appArgs = appArgsSep === -1 ? [] : rawArgs.slice(appArgsSep + 1)
14
+
15
+ // `srt tool <package>/<name> ...`: everything after the tool name belongs to
16
+ // the tool (its own flags, its own usage), so only the command and the tool
17
+ // name go through parseArgs and the rest is forwarded verbatim (tool/main.ts).
18
+ let isTool = rawArgs[0] === "tool"
19
+ export let toolArgs = isTool ? rawArgs.slice(2) : []
20
+
21
+ // A parse failure (unknown flag, missing value) is a usage error, not a
22
+ // crash: the parser's first sentence names it, the hint says where to look.
23
+ function parse() {
24
+ try {
25
+ return parseArgs({
26
+ args: isTool ? rawArgs.slice(0, 2) : appArgsSep === -1 ? rawArgs : rawArgs.slice(0, appArgsSep),
27
+ options: OPTIONS,
28
+ allowPositionals: true,
29
+ })
30
+ } catch (e: any) {
31
+ hint(String(e?.message ?? e).split(". ")[0])
32
+ }
33
+ }
34
+
35
+ const OPTIONS = {
36
+ help: { type: "boolean", default: false },
37
+ version: { type: "boolean", default: false },
38
+ dev: { type: "boolean", short: "d", default: false },
39
+ minify: { type: "boolean", short: "m", default: false },
40
+ compile: { type: "boolean", default: false },
41
+ flux: { type: "boolean", short: "f", default: false },
42
+ file: { type: "boolean", default: false },
43
+ project: { type: "boolean", default: false },
44
+ lan: { type: "boolean", default: false },
45
+ folder: { type: "boolean", default: false },
46
+ app: { type: "boolean", default: false },
47
+ stdout: { type: "boolean", default: false },
48
+ json: { type: "boolean", default: false },
49
+ output: { type: "string", short: "o" },
50
+ "proxy-http": { type: "boolean", default: false },
51
+ fps: { type: "string" },
52
+ duration: { type: "string" },
53
+ size: { type: "string" },
54
+ script: { type: "string" },
55
+ capture: { type: "string" },
56
+ tunnel: { type: "boolean", default: false },
57
+ stats: { type: "boolean", default: false },
58
+ "data-root": { type: "string" },
59
+ client: { type: "string", short: "c" },
60
+ server: { type: "string" },
61
+ port: { type: "string" },
62
+ device: { type: "string" },
63
+ install: { type: "boolean" },
64
+ } as const
65
+
66
+ export let { values, positionals } = parse()
67
+
68
+ // An explicit --port: the dev server binds it (run, server), or the caller
69
+ // picks the server on it (client, mcp). Without it the server binds its
70
+ // remembered port or a free one, and callers resolve by project (mode.ts,
71
+ // registry.ts).
72
+ function resolvePort(): number | undefined {
73
+ let raw = values.port
74
+ if (raw === undefined) return undefined
75
+ let port = Number(raw)
76
+ if (!/^\d+$/.test(raw) || port < 1 || port > 65535) {
77
+ console.error(`Invalid --port value "${raw}": expected a port number between 1 and 65535`)
78
+ process.exit(1)
79
+ }
80
+ return port
81
+ }
82
+ export let port = resolvePort()
83
+
84
+ // Storage flags for a locally spawned client. Dev client trees live in
85
+ // ~/.solidrt/clients/client<M>/, reached through --data-root so the client
86
+ // runtime keeps its single pref-path default rule (see lattice/src/storage.rs
87
+ // and okf/backlog/cli-flux-migration.md); an explicit --data-root wins.
88
+ // Storage is per app under a tree, so two projects share client 0 without
89
+ // colliding; only two clients of the same app need distinct slots. Roots are
90
+ // passed absolute because the client chdirs into its app sandbox at startup.
91
+ export function clientStorageArgs(): string[] {
92
+ let root = values["data-root"]
93
+ let args = ["--data-root", root ? resolve(root) : clientsRoot()]
94
+ let client = values.client ?? "0"
95
+ if (!/^\d+$/.test(client)) {
96
+ console.error(`Invalid --client value "${client}": expected a non-negative integer`)
97
+ process.exit(1)
98
+ }
99
+ args.push("--client", client)
100
+ return args
101
+ }
102
+
103
+ export let command = positionals[0]
104
+ export let source = positionals[1]
105
+ export let isTsx = source?.endsWith(".tsx") || source?.endsWith(".jsx")
106
+ export let isPrebuilt = source?.endsWith(".srt.js") || source?.endsWith(".srt.bin")
107
+ // A .srt.js also ends with .js: prebuilt wins, or the server would re-bundle
108
+ // a prebuilt bundle as source and skip the prebuilt load path.
109
+ export let isTs = (source?.endsWith(".ts") || source?.endsWith(".js")) && !isPrebuilt
110
+ export let isSource = isTsx || isTs
111
+
112
+ function usage(line: string): never {
113
+ console.error("Usage: " + line)
114
+ process.exit(1)
115
+ }
116
+
117
+ // Per-command argument requirements. Called once before dispatch. The app
118
+ // commands take an optional entry: none means the project at the cwd
119
+ // (mode.ts decides, and reports a missing or wrong entry itself).
120
+ export function validateArgs() {
121
+ switch (command) {
122
+ case "bundle":
123
+ // A prebuilt .srt.js compiles to bytecode (bundle.ts); a .srt.bin
124
+ // already is bytecode, so there is nothing to do with it.
125
+ if (values.flux) {
126
+ if (!source || !isTs) usage("srt bundle --flux [options] <entry.[ts|js]>")
127
+ } else if (source && !isSource && !source.endsWith(".srt.js")) {
128
+ usage("srt bundle [options] [entry.[tsx|jsx|ts|js|srt.js]]")
129
+ }
130
+ break
131
+ case "check":
132
+ // An entry file, or a folder whose entries are discovered (check.ts).
133
+ if (source && !isSource && !(existsSync(source) && statSync(source).isDirectory())) {
134
+ usage("srt check [entry.[tsx|jsx|ts|js] | folder]")
135
+ }
136
+ break
137
+ case "demo":
138
+ // A number from the printed list, or the qualified name beside it.
139
+ if (source && !/^\d+$/.test(source) && !/^[\w.-]+\/[\w.-]+$/.test(source)) {
140
+ usage("srt demo [<number> | <package>/<demo>]")
141
+ }
142
+ break
143
+ case "render":
144
+ if (source && !isTsx) usage("srt render [entry.[tsx|jsx]]")
145
+ break
146
+ case "pack":
147
+ if (values.flux) {
148
+ if (!source || !isTs) usage("srt pack --flux [options] <entry.[ts|js]>")
149
+ } else if (source && !isSource) {
150
+ usage("srt pack [options] [entry.[tsx|jsx|ts|js]]")
151
+ }
152
+ break
153
+ }
154
+
155
+ // The commands that start a dev server; `demo` resolves its own project.
156
+ let serves = command === "run" || command === "server" || command === "demo"
157
+ // The commands that work on a project or a file (mode.ts).
158
+ let onApp = command === "run" || command === "server" || command === "bundle" || command === "pack" || command === "render"
159
+ // --folder and --app pick a pack output shape.
160
+ if ((values.folder || values.app) && command !== "pack") {
161
+ usage("srt pack --folder|--app (--folder and --app are only valid with the pack command)")
162
+ }
163
+ if (values.folder && values.app) usage("srt pack --folder|--app (--folder and --app exclude each other)")
164
+ // --install is the android command's APK install step.
165
+ if (values.install && command !== "android") {
166
+ usage("srt android --install (--install is only valid with the android command)")
167
+ }
168
+ // --device picks the adb device `android` installs on.
169
+ if (values.device && command !== "android") {
170
+ usage("srt android --device <serial> (--device is only valid with the android command)")
171
+ }
172
+ // --server points a standalone client at a dev server; `run` and `server`
173
+ // own their server side, so it is valid nowhere else.
174
+ if (values.server && command !== "client") {
175
+ usage("srt client --server <host:port> (--server is only valid with the client command)")
176
+ }
177
+ // --json is the dev server's rebuild contract (bundle.ts).
178
+ if (values.json && command !== "bundle") {
179
+ usage("srt bundle --json (--json is only valid with the bundle command)")
180
+ }
181
+ // --port binds the dev server (`run`, `server`) or picks one (`client`,
182
+ // `android`, `mcp`).
183
+ if (port !== undefined && !serves && command !== "client" && command !== "android" && command !== "mcp") {
184
+ usage("srt <run|server|demo|client|android|mcp> --port <N> (--port is only valid with the run, server, demo, client, android and mcp commands)")
185
+ }
186
+ // --file/--project resolve a file argument in a project directory.
187
+ if ((values.file || values.project) && !onApp) {
188
+ usage("srt <run|server|bundle|pack|render> <file> [--file|--project] (only valid with the run, server, bundle, pack and render commands)")
189
+ }
190
+ // --lan binds every interface of a server being started.
191
+ if (values.lan && !serves) {
192
+ usage("srt <run|server|demo> --lan (--lan is only valid with the run, server and demo commands)")
193
+ }
194
+ }
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module"
2
2
  import { existsSync } from "node:fs"
3
- import { resolve, dirname } from "node:path"
3
+ import { resolve, dirname, join } from "node:path"
4
4
  import process from "node:process"
5
5
 
6
6
  let require = createRequire(import.meta.url)
@@ -46,6 +46,32 @@ export function resolveBinary(name: string) {
46
46
  return null
47
47
  }
48
48
 
49
+ // The GL libraries the runner needs next to it (or, single-file, embedded as
50
+ // kind-3 trailer sections it extracts at boot): ANGLE's libraries on Windows
51
+ // and macOS, nothing on platforms with a system GL. Order matters and the
52
+ // runner preloads in section order: libGLESv2 must load before libEGL so
53
+ // libEGL's import of it resolves against the already-loaded module instead
54
+ // of a directory search.
55
+ const GL_LIB_NAMES: Partial<Record<NodeJS.Platform, string[]>> = {
56
+ win32: ["libGLESv2.dll", "libEGL.dll"],
57
+ darwin: ["libGLESv2.dylib", "libEGL.dylib"],
58
+ }
59
+
60
+ // The GL libraries shipped next to the runner binary, resolved to their paths.
61
+ // Missing files are fatal: a pack without them cannot create a window.
62
+ export function runnerGlLibs(runnerPath: string): Array<{ name: string; path: string }> {
63
+ let names = GL_LIB_NAMES[process.platform] ?? []
64
+ let dir = dirname(runnerPath)
65
+ return names.map((name) => {
66
+ let path = join(dir, name)
67
+ if (!existsSync(path)) {
68
+ console.error(`Could not find ${name} next to the runner (${dir}); the packed app needs it to create a GL context.`)
69
+ process.exit(1)
70
+ }
71
+ return { name, path }
72
+ })
73
+ }
74
+
49
75
  // The Android client APK is host-independent (it bundles native .so for the
50
76
  // device, not the host), so it lives under dist/android/<abi>/ rather than the
51
77
  // host triple map. arm64-v8a (a fat APK: arm64-v8a + the x86_64 emulator) and
@@ -57,6 +83,20 @@ export let ANDROID_PKG_MAP: Record<string, string> = {
57
83
  "armeabi-v7a": "@solidrt/android-armeabi-v7a",
58
84
  }
59
85
 
86
+ // The client version the project expects on an `abi` device: the version of
87
+ // its @solidrt/android-<abi> dev dependency (the release action pins it to the
88
+ // runtime version). Null when the package is not installed.
89
+ export function androidPackageVersion(abi: string): string | null {
90
+ let pkg = ANDROID_PKG_MAP[abi]
91
+ if (!pkg) return null
92
+ try {
93
+ let version = require(`${pkg}/package.json`).version
94
+ return typeof version === "string" ? version : null
95
+ } catch {
96
+ return null
97
+ }
98
+ }
99
+
60
100
  export function resolveApk(abi: string = DEFAULT_ANDROID_ABI) {
61
101
  // 1. SRT_HOME: contributor checkout, where `make dist-android` stages the APK
62
102
  // under dist/android/<abi>/.
@@ -6,20 +6,22 @@ import { join } from "node:path"
6
6
  const DEV_DIR_NAME = ".solidrt"
7
7
 
8
8
  // All dev-tooling state lives in one home dotdir, one rule on every platform
9
- // (okf/backlog/parallel-dev-servers.md): servers/<port>/ holds a dev server's
10
- // identity (tunnel.key) and its registry record (live.json), clients/ is the
11
- // data root srt passes for every locally spawned client, so dev client trees
12
- // land in clients/client<M>/. Deleting the dir resets every bit of dev state.
9
+ // (okf/backlog/cli-flux-migration.md): servers/<key hash>/ holds a dev
10
+ // server's registry record (live.json, written by the server itself, which
11
+ // also names the folder: server/registry.ts), its remembered port and its
12
+ // tunnel key; clients/ is the data root for every locally spawned client,
13
+ // so dev client trees land in clients/client<M>/. Deleting the dir resets
14
+ // every bit of dev state.
13
15
  export function devDir(...parts: string[]): string {
14
16
  return join(homedir(), DEV_DIR_NAME, ...parts)
15
17
  }
16
18
 
17
- /** `servers/<port>/` under the dev dir - a dev server's identity and registry record, keyed by port. */
18
- export function serverDir(port: number): string {
19
- return devDir("servers", String(port))
19
+ /** `servers/` under the dev dir: one folder per server key. */
20
+ export function serversRoot(): string {
21
+ return devDir("servers")
20
22
  }
21
23
 
22
24
  /** `clients/` under the dev dir - the --data-root for locally spawned dev clients. */
23
25
  export function clientsRoot(): string {
24
26
  return devDir("clients")
25
- }
27
+ }
@@ -1,6 +1,7 @@
1
- import { existsSync, readFileSync } from "node:fs"
1
+ import { existsSync } from "node:fs"
2
2
  import { resolve } from "node:path"
3
- import { findProjectPackage } from "./project"
3
+ import { loadProject } from "./project"
4
+ import { fail } from "./util"
4
5
 
5
6
  // The fonts `srt pack` appends to a solidrt binary (see
6
7
  // okf/plans/packaged-fonts.md). By default the three Noto role defaults;
@@ -30,46 +31,31 @@ let DEFAULT_FONTS: Record<string, string> = {
30
31
  function defaultFontsDir(): string | null {
31
32
  let candidates: string[] = []
32
33
  if (process.env.SRT_HOME) candidates.push(resolve(process.env.SRT_HOME, "alloy/assets/fonts"))
33
- candidates.push(resolve(import.meta.dir, "../fonts"))
34
- candidates.push(resolve(import.meta.dir, "../../../alloy/assets/fonts"))
34
+ candidates.push(resolve(import.meta.dir, "../../fonts"))
35
+ candidates.push(resolve(import.meta.dir, "../../../../alloy/assets/fonts"))
35
36
  for (let dir of candidates) {
36
37
  if (existsSync(resolve(dir, "NotoSans.ttf"))) return dir
37
38
  }
38
39
  return null
39
40
  }
40
41
 
41
- function findProjectConfig(sourcePath: string): { dir: string; fonts: unknown } | null {
42
- let project = findProjectPackage(sourcePath)
43
- return project && { dir: project.dir, fonts: project.pkg.solidrt?.fonts }
44
- }
45
-
46
- function fail(message: string): never {
47
- console.error(message)
48
- process.exit(1)
49
- }
50
-
51
42
  // Resolve the font set for a pack as file paths: role defaults merged with the
52
- // project's `solidrt.fonts` map. Order is roles first (sans, serif, mono),
43
+ // project's `solidrt.fonts` map (shape-checked by loadProject; file mode has
44
+ // no project, so defaults only). Order is roles first (sans, serif, mono),
53
45
  // then added aliases in config order.
54
- export function resolvePackFonts(sourcePath: string): ResolvedFont[] {
55
- let config = findProjectConfig(sourcePath)
56
- let overrides = config?.fonts ?? {}
57
- if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides)) {
58
- fail('The "solidrt.fonts" key in package.json must be a map of alias to font file path (or false)')
59
- }
46
+ export function resolvePackFonts(projectDir: string | null): ResolvedFont[] {
47
+ let project = loadProject(projectDir)
48
+ let overrides = project?.config.fonts ?? {}
60
49
 
61
- // alias -> path relative to the config dir, or null for a role default.
50
+ // alias -> path relative to the project dir, or null for a role default.
62
51
  let selected = new Map<string, string | null>()
63
52
  for (let role of Object.keys(DEFAULT_FONTS)) selected.set(role, null)
64
53
  for (let [alias, value] of Object.entries(overrides)) {
65
54
  if (value === false) {
66
55
  if (!(alias in DEFAULT_FONTS)) fail(`"solidrt.fonts": "${alias}": false drops a default, but "${alias}" is not one of ${Object.keys(DEFAULT_FONTS).join("/")}`)
67
56
  selected.delete(alias)
68
- } else if (typeof value === "string") {
69
- if (Buffer.byteLength(alias, "utf8") > 255) fail(`"solidrt.fonts": alias "${alias}" is too long (max 255 bytes)`)
70
- selected.set(alias, resolve(config!.dir, value))
71
57
  } else {
72
- fail(`"solidrt.fonts": "${alias}" must be a font file path or false, got ${JSON.stringify(value)}`)
58
+ selected.set(alias, resolve(project!.dir, value))
73
59
  }
74
60
  }
75
61
 
@@ -0,0 +1,77 @@
1
+ // What a command works on (run, server, bundle, pack, render), decided by
2
+ // the cwd and the argument and never by searching upward
3
+ // (okf/backlog/cli-flux-migration.md):
4
+ //
5
+ // cwd has package.json argument mode
6
+ // yes none project (entry = solidrt.entry, default src/index.tsx)
7
+ // no none error
8
+ // no file file
9
+ // yes file error, unless --project (project at cwd,
10
+ // entry overridden) or --file (ignore the project)
11
+ //
12
+ // Project mode hangs assets, fonts, identity, isolates and build outputs off
13
+ // the project root; file mode has none of those, the file stands alone. The
14
+ // key (the canonical project root, or the canonical file path) is what the
15
+ // dev server registry and every control response name.
16
+
17
+ import { existsSync, realpathSync } from "node:fs"
18
+ import { dirname, resolve } from "node:path"
19
+ import { values, source, isSource, isPrebuilt } from "./args"
20
+ import { loadProject } from "./project"
21
+ import { fail } from "./util"
22
+
23
+ export type Mode =
24
+ | { mode: "project"; key: string; projectDir: string; entry: string }
25
+ | { mode: "file"; key: string; projectDir: null; entry: string }
26
+
27
+ const DEFAULT_ENTRY = "src/index.tsx"
28
+
29
+ function canonical(path: string): string {
30
+ try {
31
+ return realpathSync.native(path)
32
+ } catch {
33
+ return resolve(path)
34
+ }
35
+ }
36
+
37
+ export function resolveMode(): Mode {
38
+ let cwd = process.cwd()
39
+ let pkgPath = resolve(cwd, "package.json")
40
+ let hasPkg = existsSync(pkgPath)
41
+
42
+ if (source !== undefined && !isSource && !(isPrebuilt && source.endsWith(".srt.js"))) {
43
+ fail(`Not an app entry: ${source} (expected .tsx, .jsx, .ts, .js or .srt.js)`)
44
+ }
45
+ if (source !== undefined && !existsSync(source)) fail(`Entry not found: ${source}`)
46
+ if (values.file && values.project) fail("--file and --project exclude each other")
47
+
48
+ if (source === undefined) {
49
+ if (values.file || values.project) fail("--file and --project need an entry file")
50
+ if (!hasPkg) fail(`No package.json in ${cwd}. Run from the project root, or pass a file to use on its own.`)
51
+ let declared = loadProject(cwd)!.config.entry
52
+ let entry = resolve(cwd, declared ?? DEFAULT_ENTRY)
53
+ if (!existsSync(entry)) {
54
+ fail(`Entry not found: ${entry}${declared ? "" : ' (set "solidrt": { "entry": ... } in package.json)'}`)
55
+ }
56
+ let key = canonical(cwd)
57
+ return { mode: "project", key, projectDir: key, entry }
58
+ }
59
+
60
+ let entry = canonical(source)
61
+ if (hasPkg && !values.file && !values.project) {
62
+ fail(
63
+ `${cwd} is a project (it has a package.json) and ${source} is a file: pass --project to use the project with this entry, or --file to use the file on its own.`,
64
+ )
65
+ }
66
+ if (hasPkg && values.project) {
67
+ let key = canonical(cwd)
68
+ return { mode: "project", key, projectDir: key, entry }
69
+ }
70
+ if (!hasPkg && values.project) fail(`--project needs a package.json in ${cwd}`)
71
+ return { mode: "file", key: entry, projectDir: null, entry }
72
+ }
73
+
74
+ /** The directory the file routes serve: the entry's directory. */
75
+ export function sourceDirOf(mode: Mode): string {
76
+ return dirname(mode.entry)
77
+ }