@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,8 +1,9 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
2
2
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
3
+ import { fail } from "./util"
3
4
 
4
- // Project configuration lives in the `solidrt` key of the nearest package.json
5
- // above the entry file (okf/plans/client-storage-updates.md):
5
+ // Project configuration lives in the `solidrt` key of the project's
6
+ // package.json (okf/plans/client-storage-updates.md):
6
7
  //
7
8
  // "solidrt": {
8
9
  // "appId": "com.example.app", // stable identity: storage dir, Android package id
@@ -15,16 +16,68 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
15
16
  // }
16
17
  //
17
18
  // Everything defaults from the package name (or the entry filename when there
18
- // is no package.json) so a dev project needs zero config; `srt pack` warns
19
+ // is no project) so a dev project needs zero config; `srt pack` warns
19
20
  // when appId is defaulted, since a distributed app should pin its identity.
21
+ //
22
+ // The key is read in one place, loadProject, which checks every field's
23
+ // shape (throw-in-dev policy: a bad value fails the command); the readers
24
+ // (mode.ts, fonts.ts, and the identity and asset collectors below) take
25
+ // their fields from the result and only check what is specific to them.
26
+ //
27
+ // Which project an entry belongs to is the caller's decision (mode.ts: the
28
+ // cwd, never a search); the one exception is `srt check`, which verifies
29
+ // trees of entries from one cwd and walks up from each (findProject).
30
+
31
+ /** The `solidrt` key: every field optional, every present field shape-checked. */
32
+ export type ProjectConfig = {
33
+ entry?: string
34
+ appId?: string
35
+ org?: string
36
+ displayName?: string
37
+ /** alias -> font file path, or false to drop a role default (fonts.ts). */
38
+ fonts?: Record<string, string | false>
39
+ icon?: string
40
+ }
41
+
42
+ export type Project = { dir: string; name: string | undefined; config: ProjectConfig }
43
+
44
+ function parseProjectConfig(raw: unknown): ProjectConfig {
45
+ if (raw === undefined) return {}
46
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) fail('"solidrt" in package.json must be an object')
47
+ let config = raw as Record<string, unknown>
48
+ for (let key of ["entry", "appId", "org", "displayName", "icon"]) {
49
+ if (key in config && typeof config[key] !== "string") fail(`"solidrt": "${key}" must be a string`)
50
+ }
51
+ if ("fonts" in config) {
52
+ let fonts = config.fonts
53
+ if (typeof fonts !== "object" || fonts === null || Array.isArray(fonts)) {
54
+ fail('The "solidrt.fonts" key in package.json must be a map of alias to font file path (or false)')
55
+ }
56
+ for (let [alias, value] of Object.entries(fonts)) {
57
+ if (value !== false && typeof value !== "string") {
58
+ fail(`"solidrt.fonts": "${alias}" must be a font file path or false, got ${JSON.stringify(value)}`)
59
+ }
60
+ if (Buffer.byteLength(alias, "utf8") > 255) fail(`"solidrt.fonts": alias "${alias}" is too long (max 255 bytes)`)
61
+ }
62
+ }
63
+ return config as ProjectConfig
64
+ }
65
+
66
+ // The project at `dir`: its package name and validated `solidrt` config. A
67
+ // dir without a package.json is an empty project; null (file mode: the entry
68
+ // stands alone) stays null.
69
+ export function loadProject(dir: string | null): Project | null {
70
+ if (dir === null) return null
71
+ let pkgPath = resolve(dir, "package.json")
72
+ let pkg = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")) : {}
73
+ return { dir, name: typeof pkg.name === "string" ? pkg.name : undefined, config: parseProjectConfig(pkg.solidrt) }
74
+ }
20
75
 
21
- export function findProjectPackage(sourcePath: string): { dir: string; pkg: any } | null {
76
+ // The nearest project above a source file (see the note on searching above).
77
+ export function findProject(sourcePath: string): Project | null {
22
78
  let dir = resolve(dirname(sourcePath))
23
79
  while (true) {
24
- let pkgPath = resolve(dir, "package.json")
25
- if (existsSync(pkgPath)) {
26
- return { dir, pkg: JSON.parse(readFileSync(pkgPath, "utf8")) }
27
- }
80
+ if (existsSync(resolve(dir, "package.json"))) return loadProject(dir)
28
81
  let parent = dirname(dir)
29
82
  if (parent === dir) return null
30
83
  dir = parent
@@ -33,11 +86,6 @@ export function findProjectPackage(sourcePath: string): { dir: string; pkg: any
33
86
 
34
87
  export type AppIdentity = { appId: string; org: string; displayName: string; defaulted: boolean }
35
88
 
36
- function fail(message: string): never {
37
- console.error(message)
38
- process.exit(1)
39
- }
40
-
41
89
  // Storage directory component (matches the runtime's safe_component check).
42
90
  let APP_ID_PATTERN = /^[A-Za-z0-9._-]+$/
43
91
 
@@ -64,19 +112,36 @@ function checkField(value: string, what: string) {
64
112
  // derivation question is settled (see plan).
65
113
  export const RUNTIME_VERSION = 1
66
114
 
115
+ // This CLI's version, the one value every "which srt is this" answer comes
116
+ // from (--version, the manifest stamp, the MCP server). A published CLI
117
+ // carries the real version in its package.json; in-repo that is the 0.0.0
118
+ // placeholder (see CLAUDE.md, "Versioning"), so a checkout reports the same
119
+ // git describe the runtime builds stamp themselves with (lattice/Makefile,
120
+ // flux/build.rs). Falls back to the placeholder when git cannot answer.
121
+ function cliVersion(): string {
122
+ let pkgVersion = JSON.parse(readFileSync(join(import.meta.dir, "..", "..", "package.json"), "utf8")).version
123
+ if (pkgVersion !== "0.0.0") return pkgVersion
124
+ let git = Bun.spawnSync({
125
+ cmd: ["git", "describe", "--tags", "--always", "--dirty"],
126
+ cwd: import.meta.dir,
127
+ stdout: "pipe",
128
+ stderr: "ignore",
129
+ })
130
+ return git.success ? git.stdout.toString().trim().replace(/^v/, "") : pkgVersion
131
+ }
132
+ export const CLI_VERSION: string = cliVersion()
133
+
67
134
  // The manifest's solidrtVersion: provenance, not a compat gate like
68
- // runtimeVersion - the CLI release that built the version. Published CLIs
69
- // carry the real version in their package.json; the in-repo 0.0.0
70
- // placeholder stamps "unknown".
71
- let pkgVersion = JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf8")).version
72
- export const SOLIDRT_VERSION: string = pkgVersion === "0.0.0" ? "unknown" : pkgVersion
135
+ // runtimeVersion - the CLI release (or checkout) that built the version. The
136
+ // packed runner warns when it differs from its own (lattice/src/main.rs).
137
+ export const SOLIDRT_VERSION: string = CLI_VERSION
73
138
 
74
139
  // `extra` are build outputs that ship as assets too (isolate bundles); they
75
140
  // follow the assets/ tree in the list, in the order given.
76
- export function buildManifest(code: string, entry: string, extra: ManifestAsset[] = []): string {
77
- let identity = loadAppIdentity(entry)
141
+ export function buildManifest(code: string, entry: string, extra: ManifestAsset[], projectDir: string | null): string {
142
+ let identity = loadAppIdentity(entry, projectDir)
78
143
  let sha256 = new Bun.CryptoHasher("sha256").update(code).digest("hex")
79
- let { assets, fonts, icon } = collectAssets(entry)
144
+ let { assets, fonts, icon } = collectAssets(projectDir)
80
145
  assets.push(...extra)
81
146
  return JSON.stringify({
82
147
  appId: identity.appId,
@@ -98,12 +163,6 @@ export function manifestAssetFor(path: string, bytes: Uint8Array): ManifestAsset
98
163
  return { path, sha256: new Bun.CryptoHasher("sha256").update(bytes).digest("hex"), size: bytes.length }
99
164
  }
100
165
 
101
- // The project root the assets/ convention hangs off: the nearest package.json
102
- // dir, or the entry's own dir when there is none.
103
- export function projectDirFor(sourcePath: string): string {
104
- return findProjectPackage(sourcePath)?.dir ?? resolve(dirname(sourcePath))
105
- }
106
-
107
166
  // The manifest asset path for an absolute file inside the project's assets/
108
167
  // dir, or null when it lies outside it.
109
168
  export function assetPathFor(projectDir: string, abs: string): string | null {
@@ -137,13 +196,15 @@ function walkAssets(assetsDir: string, dir: string, out: ManifestAsset[]) {
137
196
  // and the raster surfaces (window icon, OS embedding) come with later stages
138
197
  // (okf/backlog/app-icons.md). An undeclared assets/icon.svg is picked up by
139
198
  // convention.
140
- export function collectAssets(entry: string): {
199
+ export function collectAssets(dir: string | null): {
141
200
  assets: ManifestAsset[]
142
201
  fonts: ManifestFont[]
143
202
  icon: string | null
144
203
  } {
145
- let project = findProjectPackage(entry)
146
- let projectDir = projectDirFor(entry)
204
+ let project = loadProject(dir)
205
+ // No project (file mode): the entry stands alone, so no assets at all.
206
+ if (!project) return { assets: [], fonts: [], icon: null }
207
+ let projectDir = project.dir
147
208
  let assetsDir = resolve(projectDir, "assets")
148
209
 
149
210
  let assets: ManifestAsset[] = []
@@ -153,25 +214,21 @@ export function collectAssets(entry: string): {
153
214
  }
154
215
 
155
216
  let fonts: ManifestFont[] = []
156
- let map = project?.pkg.solidrt?.fonts
157
- if (map && typeof map === "object" && !Array.isArray(map)) {
158
- for (let [alias, value] of Object.entries(map)) {
159
- if (typeof value !== "string") continue
160
- let path = assetPathFor(projectDir, resolve(projectDir, value))
161
- if (!path) {
162
- fail(`"solidrt.fonts": "${alias}": ${value} must live under assets/ (fonts ship as version assets)`)
163
- }
164
- if (!assets.some((a) => a.path === path)) {
165
- fail(`"solidrt.fonts": "${alias}": no such file: ${resolve(projectDir, value)}`)
166
- }
167
- fonts.push({ path, alias })
217
+ for (let [alias, value] of Object.entries(project.config.fonts ?? {})) {
218
+ if (value === false) continue
219
+ let path = assetPathFor(projectDir, resolve(projectDir, value))
220
+ if (!path) {
221
+ fail(`"solidrt.fonts": "${alias}": ${value} must live under assets/ (fonts ship as version assets)`)
222
+ }
223
+ if (!assets.some((a) => a.path === path)) {
224
+ fail(`"solidrt.fonts": "${alias}": no such file: ${resolve(projectDir, value)}`)
168
225
  }
226
+ fonts.push({ path, alias })
169
227
  }
170
228
 
171
229
  let icon: string | null = null
172
- let declared = project?.pkg.solidrt?.icon
230
+ let declared = project.config.icon
173
231
  if (declared !== undefined) {
174
- if (typeof declared !== "string") fail('"solidrt": "icon" must be a string path')
175
232
  let path = assetPathFor(projectDir, resolve(projectDir, declared))
176
233
  if (!path) {
177
234
  fail(`"solidrt.icon": ${declared} must live under assets/ (the icon ships as a version asset)`)
@@ -192,26 +249,17 @@ export function collectAssets(entry: string): {
192
249
 
193
250
  // Resolve the app identity for a pack. All three fields are guaranteed
194
251
  // non-empty and 255 bytes max (the trailer encoding's length prefix).
195
- export function loadAppIdentity(sourcePath: string): AppIdentity {
196
- let project = findProjectPackage(sourcePath)
197
- let config = project?.pkg.solidrt ?? {}
252
+ export function loadAppIdentity(sourcePath: string, projectDir: string | null): AppIdentity {
253
+ let project = loadProject(projectDir)
254
+ let config = project?.config ?? {}
198
255
  // A scoped package name (@org/name) defaults to its last segment: identity
199
256
  // fields reject path separators, and derived defaults must never fail that.
200
- let fallbackName = (project?.pkg.name ?? basename(sourcePath).replace(/\.[jt]sx?$/, "")).split("/").pop()!
257
+ let fallbackName = (project?.name ?? basename(sourcePath).replace(/\.[jt]sx?$/, "")).split("/").pop()!
201
258
 
202
- for (let key of ["appId", "org", "displayName"]) {
203
- if (key in config && typeof config[key] !== "string") fail(`"solidrt": "${key}" must be a string`)
204
- }
205
-
206
- let appId: string
207
- let defaulted = typeof config.appId !== "string"
208
- if (defaulted) {
209
- appId = sanitizeAppId(fallbackName)
210
- } else {
211
- appId = config.appId
212
- if (!APP_ID_PATTERN.test(appId) || appId === "." || appId === "..") {
213
- fail(`"solidrt": "appId" must match ${APP_ID_PATTERN} (reverse-DNS recommended, e.g. "com.example.app")`)
214
- }
259
+ let defaulted = config.appId === undefined
260
+ let appId = config.appId ?? sanitizeAppId(fallbackName)
261
+ if (!defaulted && (!APP_ID_PATTERN.test(appId) || appId === "." || appId === "..")) {
262
+ fail(`"solidrt": "appId" must match ${APP_ID_PATTERN} (reverse-DNS recommended, e.g. "com.example.app")`)
215
263
  }
216
264
  let displayName = config.displayName ?? fallbackName
217
265
  let org = config.org ?? displayName
@@ -0,0 +1,120 @@
1
+ // Reading the server registry: every running dev server keeps a live.json in
2
+ // ~/.solidrt/servers/<key hash>/ (written by the server itself, see
3
+ // packages/cli/src/server/registry.ts). `srt client`, `srt mcp` and the launcher
4
+ // resolve a server from it by key: the canonical project root or file path
5
+ // (see mode.ts). The record is a hint; the server is authoritative, so a
6
+ // caller confirms with a control call (the x-solidrt-project header names
7
+ // the key it serves).
8
+
9
+ import { readdirSync, readFileSync, realpathSync } from "node:fs"
10
+ import { join } from "node:path"
11
+ import { serversRoot } from "./dev-dir"
12
+ import type { LiveRecord } from "../types/registry"
13
+
14
+ // Only ESRCH means the process is gone. EPERM is a live process this bridge
15
+ // may not signal (Windows reports it for other users' processes), and a
16
+ // bare try/catch would drop that healthy server from the registry.
17
+ export function pidAlive(pid: number): boolean {
18
+ try {
19
+ process.kill(pid, 0)
20
+ return true
21
+ } catch (e: any) {
22
+ return e?.code === "EPERM"
23
+ }
24
+ }
25
+
26
+ // Two keys from different processes agree by construction on the path, not
27
+ // the spelling (drive-letter case, 8.3 names, symlinks); compare canonical.
28
+ export function sameKey(a: string, b: string): boolean {
29
+ if (a === b) return true
30
+ try {
31
+ return realpathSync.native(a) === realpathSync.native(b)
32
+ } catch {
33
+ return false
34
+ }
35
+ }
36
+
37
+ /** Every readable record whose process is alive. Malformed records are
38
+ * skipped, not fatal. */
39
+ export function liveRecords(): LiveRecord[] {
40
+ let root = serversRoot()
41
+ let names: string[]
42
+ try {
43
+ names = readdirSync(root)
44
+ } catch {
45
+ return []
46
+ }
47
+ let records: LiveRecord[] = []
48
+ for (let name of names) {
49
+ try {
50
+ let record = JSON.parse(readFileSync(join(root, name, "live.json"), "utf8"))
51
+ if (
52
+ typeof record?.pid === "number" &&
53
+ typeof record?.port === "number" &&
54
+ typeof record?.key === "string" &&
55
+ pidAlive(record.pid)
56
+ ) {
57
+ records.push(record)
58
+ }
59
+ } catch {}
60
+ }
61
+ return records
62
+ }
63
+
64
+ export type Resolution = { ok: true; record: LiveRecord } | { ok: false; message: string }
65
+
66
+ /** Whether the server a record names is really there: its control API
67
+ * answers on the port and names the record's key. A pid reused by an
68
+ * unrelated process, or a port taken over by another server, fails here. */
69
+ export async function confirmed(record: LiveRecord): Promise<boolean> {
70
+ try {
71
+ let resp = await fetch(`http://127.0.0.1:${record.port}/__control__/clients`, {
72
+ signal: AbortSignal.timeout(1000),
73
+ })
74
+ let served = resp.headers.get("x-solidrt-project")
75
+ return served !== null && sameKey(served, record.key)
76
+ } catch {
77
+ return false
78
+ }
79
+ }
80
+
81
+ // The one record the registry offered was a fossil (its pid lives on in an
82
+ // unrelated process); the next server start prunes it.
83
+ function stale(record: LiveRecord): string {
84
+ return (
85
+ `The registry names port ${record.port} (pid ${record.pid}) for ${record.key}, ` +
86
+ `but no dev server answers there: the record is stale. Start one with srt run.`
87
+ )
88
+ }
89
+
90
+ async function confirm(record: LiveRecord): Promise<Resolution> {
91
+ return (await confirmed(record)) ? { ok: true, record } : { ok: false, message: stale(record) }
92
+ }
93
+
94
+ /** The server on `port`, if the registry names one there. */
95
+ export async function resolveByPort(port: number): Promise<Resolution> {
96
+ let record = liveRecords().find((r) => r.port === port)
97
+ return record ? confirm(record) : { ok: false, message: `No dev server on port ${port}.` }
98
+ }
99
+
100
+ /**
101
+ * The server for `cwd`: the project server keyed by cwd itself; otherwise
102
+ * the one file server whose file lies under cwd; otherwise an error that
103
+ * lists the candidates. The record is a hint and the server is
104
+ * authoritative, so the chosen record is confirmed before it is returned.
105
+ */
106
+ export async function resolveFromCwd(cwd: string): Promise<Resolution> {
107
+ let records = liveRecords()
108
+ let project = records.find((r) => r.mode === "project" && sameKey(r.key, cwd))
109
+ if (project) return confirm(project)
110
+ let prefix = cwd.replace(/[\\/]+$/, "") + "/"
111
+ let files = records.filter((r) => r.mode === "file" && r.key.replace(/\\/g, "/").startsWith(prefix.replace(/\\/g, "/")))
112
+ if (files.length === 1) return confirm(files[0]!)
113
+ let listing =
114
+ records.length === 0
115
+ ? `Registry ${serversRoot()}: no running servers.`
116
+ : `Registry ${serversRoot()}: ${records.length} running server(s).\n` +
117
+ records.map((r) => ` port ${r.port} ${r.mode} ${r.key}`).join("\n")
118
+ let hint = files.length > 1 ? `${files.length} file servers run under ${cwd}; pass --port <N> to pick one.` : "Start one with srt run."
119
+ return { ok: false, message: `No dev server for ${cwd}.\n${listing}\n${hint}` }
120
+ }
@@ -0,0 +1,24 @@
1
+ import { fileURLToPath } from "node:url"
2
+
3
+ // The dev server (src/server/) is a flux script; the flux binary
4
+ // runs one plain-JS file, so the server's TypeScript modules are bundled
5
+ // into one. Bun is already the bundler; the browser target keeps node
6
+ // builtins out, and the flux: capability modules stay external (the runtime
7
+ // provides them). A published CLI ships this bundle prebuilt as
8
+ // dist/server.js (the Makefile target, run at release time); a checkout
9
+ // builds it per launch (main.ts).
10
+ export async function buildServerBundle(outfile: string): Promise<void> {
11
+ let entry = fileURLToPath(new URL("../server/main.ts", import.meta.url))
12
+ let result = await Bun.build({
13
+ entrypoints: [entry],
14
+ target: "browser",
15
+ format: "esm",
16
+ external: ["flux:*"],
17
+ })
18
+ if (!result.success) {
19
+ console.error("[cli] Failed to bundle the dev server:")
20
+ for (let log of result.logs) console.error(String(log))
21
+ process.exit(1)
22
+ }
23
+ await Bun.write(outfile, result.outputs[0]!)
24
+ }
@@ -0,0 +1,117 @@
1
+ import { CLI_VERSION } from "./project"
2
+
3
+ // What srt says about itself: the version, the usage, and the short hint a
4
+ // bare `srt`, an unknown command or a bad flag gets instead of the full usage
5
+ // (so an error line stays on screen).
6
+
7
+ export function printVersion() {
8
+ console.log(CLI_VERSION)
9
+ }
10
+
11
+ // The banner is information, not the error: only the error line goes to
12
+ // stderr, and only an error fails the run.
13
+ export function hint(error?: string): never {
14
+ if (error) console.error(error)
15
+ console.log(`srt ${CLI_VERSION}\nRun srt --help for usage.`)
16
+ process.exit(error ? 1 : 0)
17
+ }
18
+
19
+ export function printUsage() {
20
+ console.log(USAGE)
21
+ }
22
+
23
+ const USAGE = `Usage: srt <command> [options] [file]
24
+
25
+ Commands:
26
+ init <dir> Scaffold a new SolidRT project into a new (empty) folder
27
+ run [file] Start dev server + local solidrt-go client
28
+ server [file] Start dev server only
29
+ client Start solidrt-go client only
30
+ demo [<number>] List the demos the installed packages ship, or run one
31
+ tool [<pkg>/<name>] List the tools the installed packages ship, or run one
32
+ (everything after the tool name is the tool's own arguments)
33
+ console Start the dev console: the dev servers on this machine and their clients
34
+ android Launch the client on a connected Android device (--install to install it first)
35
+ bundle [file] Transpile TS/JS/TSX/JSX to JS or bytecode
36
+ (a prebuilt <name>.srt.js: compile it to bytecode)
37
+ check [file] Verify the app builds and typechecks, without writing anything
38
+ (no file: every examples/*/src/index.tsx, packages/*/examples/*.tsx
39
+ and packages/*/demos/src/*.tsx)
40
+ render [file] Replay a script (optional) and render frames for video generation
41
+ pack [file] Bundle + compile to a standalone executable (experimental)
42
+ mcp MCP server (stdio) exposing the running dev server to coding agents
43
+
44
+ Global options:
45
+ --help Print this usage and exit
46
+ --version Print the srt version and exit
47
+
48
+ run/server/bundle/pack/render: what the command works on
49
+ srt run In a project root (package.json): the project, entry from
50
+ "solidrt": { "entry" } (default src/index.tsx)
51
+ srt run <file> Outside a project: the file on its own (no assets, no isolates)
52
+ srt run <file> --project In a project root: the project, with this entry
53
+ srt run <file> --file In a project root: the file on its own, ignoring the project
54
+ Build outputs land under dist/ in the current directory.
55
+ One server per project or file; each keeps the port it had last time,
56
+ else the first free one from 34884 up (see the startup line). Loopback only unless --lan.
57
+
58
+ run/server/demo options:
59
+ --port <N> Bind this port instead of the remembered/next free one
60
+ --lan Bind every interface and announce the LAN address (QR)
61
+ --proxy-http Route fetch calls through the dev server (HTTP cache enabled)
62
+ --capture <file> Record connected clients' key events to a script file
63
+ --tunnel Accept ticket-paired clients through the p2p tunnel
64
+ -- <args...> Everything after -- reaches the app on every client (flux:process argv)
65
+
66
+ run/client options:
67
+ --size <WxH> Window size (default: 1280x720)
68
+ --stats Show the debug stats overlay (FPS, memory, frame timings)
69
+ --data-root <dir> Client data root (default: ~/.solidrt/clients)
70
+ -c, --client <N> Client number: its own data tree under the data root (default: 0)
71
+
72
+ client options:
73
+ (no flags) Connect to the dev server of the project (or file) in the current directory
74
+ --port <N> Connect to the local dev server on this port
75
+ --server <host:port> Connect to a dev server at this address
76
+
77
+ android options:
78
+ (no flags) Launch the installed client, pointed at the dev server of the project (or file)
79
+ in the current directory (the server must run with --lan, or be reached from an emulator)
80
+ --install Install or update the client first, from the project's @solidrt/android-<abi> package
81
+ --port <N> Point it at the local dev server on this port
82
+ --device <serial> Target a specific adb device by serial or unique prefix
83
+
84
+ mcp options:
85
+ --port <N> Attach to the dev server on this port (default: resolve by project)
86
+
87
+ bundle options:
88
+ -f, --flux Bundle for the bare Flux runtime, without SolidJS (entry must be .ts|.js)
89
+ -d, --dev Use development build of SolidJS (default: production)
90
+ -m, --minify Minify the output
91
+ --compile Compile to bytecode
92
+ -o, --output <dir> Output directory (default: dist/bundle; for a prebuilt
93
+ .srt.js, its own directory)
94
+ --stdout Write bundle to stdout
95
+ --json Write the bundle, its manifest and its isolates as one JSON object
96
+ to stdout (the dev server's rebuild; --server <host:port> names it)
97
+
98
+ console options:
99
+ -- <args...> Everything after -- reaches the console (flux:process argv)
100
+
101
+ pack options:
102
+ --folder Write the flat app folder (runner + manifest + bundle + assets)
103
+ instead of the single-file executable
104
+ --app Write the app alone as one <entry>.srtapp (manifest + bundle + assets,
105
+ no runner), for a runner to load: solidrt <file>.srtapp
106
+ -f, --flux Pack for the bare Flux runtime instead of SolidRT (entry must be .ts|.js)
107
+ -m, --minify Minify the output
108
+ -o, --output <name> Output filename
109
+
110
+ render options:
111
+ --script <file> Script file to replay (default: no scripted input)
112
+ --fps <N> Frames per second (default: 60)
113
+ --duration <N> Duration in seconds, fractions allowed (default: 1)
114
+ --size <WxH> Frame size in physical pixels (default: 1280x720)
115
+ -o, --output <path> Where frames land: a directory (frame-NNNNNN.png inside it)
116
+ or a path prefix (default: the current directory)
117
+ -- <args...> Everything after -- is passed to the app (flux:process argv)`
@@ -0,0 +1,36 @@
1
+ import { resolveBinary } from "./artifacts"
2
+
3
+ // A fatal usage or configuration error: the message, then exit. The
4
+ // throw-in-dev policy (CLAUDE.md, "API design"): a bad value fails the
5
+ // command instead of being papered over.
6
+ export function fail(message: string): never {
7
+ console.error(message)
8
+ process.exit(1)
9
+ }
10
+
11
+ // Build target per binary, for the "not found" hint. Run from the repo root.
12
+ let BUILD_HINTS: Record<string, string> = {
13
+ "solidrt-go": "make client",
14
+ solidrt: "make runtime",
15
+ flux: "make -C flux flux",
16
+ fluxc: "make -C flux fluxc",
17
+ fluxrt: "make -C flux fluxrt PROFILE=release-opt",
18
+ }
19
+
20
+ export function requireBinary(name: string) {
21
+ let path = resolveBinary(name)
22
+ if (path) return path
23
+ let hint = BUILD_HINTS[name]
24
+ console.error(`Could not find ${name} binary.`)
25
+ if (hint) {
26
+ console.error(`Build it from source: run ${hint}, with SRT_HOME pointing at your SolidRT checkout.`)
27
+ } else {
28
+ console.error("Build it from source, with SRT_HOME pointing at your SolidRT checkout.")
29
+ }
30
+ process.exit(1)
31
+ }
32
+
33
+ export async function run(binary: string, args: string[]) {
34
+ let proc = Bun.spawn([binary, ...args], { stdio: ["inherit", "inherit", "inherit"] })
35
+ return proc.exited
36
+ }