@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
@@ -0,0 +1,18 @@
1
+ # srt pack
2
+
3
+ {{ usage pack }}
4
+
5
+ Bundles, compiles to bytecode and appends the result to the runner as one
6
+ standalone executable; `--folder` writes the flat runner + manifest +
7
+ bundle + assets folder to `dist/pack/` instead. `--flux` packs a script for
8
+ the bare [Flux runtime](/runtime/). Experimental.
9
+
10
+ `--app` writes the app alone, without a runner: one `<entry>.srtapp` holding
11
+ the manifest, the bytecode and the assets, which any `solidrt` runner of the
12
+ same version runs as `solidrt <file>.srtapp`. The runner is used in place,
13
+ so a signed runner stays signed; the file is platform-independent. This is
14
+ how the CLI ships the [console](../console/docs.md).
15
+
16
+ Set a stable `appId` in the `solidrt` key of package.json before
17
+ distributing (it keys the app's storage folder); `pack` warns while it is
18
+ defaulted from the package name.
@@ -1,17 +1,11 @@
1
1
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"
2
2
  import { basename, dirname, join, resolve } from "node:path"
3
- import {
4
- assetPathFor,
5
- collectAssets,
6
- loadAppIdentity,
7
- projectDirFor,
8
- RUNTIME_VERSION,
9
- SOLIDRT_VERSION,
10
- type ManifestFont,
11
- } from "./project"
12
- import { resolvePackFonts } from "./fonts"
13
- import { runnerGlLibs } from "./packer"
14
- import { isolateAssetPath } from "./bundler"
3
+ import { assetPathFor, collectAssets, loadAppIdentity, RUNTIME_VERSION, SOLIDRT_VERSION, type ManifestFont } from "../lib/project"
4
+ import { resolvePackFonts } from "../lib/fonts"
5
+ import type { Mode } from "../lib/mode"
6
+ import { runnerGlLibs } from "../lib/artifacts"
7
+ import { fail } from "../lib/util"
8
+ import { isolateAssetPath } from "../bundle/bundler"
15
9
 
16
10
  // The canonical flat pack folder (okf/plans/client-storage-updates.md, Pack
17
11
  // output): runner + manifest.json + bundle.bin + assets/. The manifest
@@ -20,11 +14,6 @@ import { isolateAssetPath } from "./bundler"
20
14
  // identity (org, displayName) and the complete font set with the default
21
15
  // fonts materialized under assets/fonts/.
22
16
 
23
- function fail(message: string): never {
24
- console.error(message)
25
- process.exit(1)
26
- }
27
-
28
17
  function hashHex(bytes: Uint8Array): string {
29
18
  return new Bun.CryptoHasher("sha256").update(bytes).digest("hex")
30
19
  }
@@ -41,10 +30,12 @@ export type PackFolder = {
41
30
  // `isolates` are the app's isolate bundles compiled to bytecode; they ship as
42
31
  // the manifest assets isolates/<id>.bin (the production runtime has no
43
32
  // compiler, so pack never ships isolate source).
44
- export function buildPackFolder(entry: string, bytecode: Buffer, isolates: { id: string; bytecode: Buffer }[]): PackFolder {
45
- let identity = loadAppIdentity(entry)
46
- let projectDir = projectDirFor(resolve(entry))
47
- let { assets, icon } = collectAssets(entry)
33
+ export function buildPackFolder(mode: Mode, bytecode: Buffer, isolates: { id: string; bytecode: Buffer }[]): PackFolder {
34
+ let identity = loadAppIdentity(mode.entry, mode.projectDir)
35
+ // File mode collects no assets, so the dir is only ever joined in project
36
+ // mode; the entry's dir stands in to keep the paths absolute.
37
+ let projectDir = mode.projectDir ?? dirname(mode.entry)
38
+ let { assets, icon } = collectAssets(mode.projectDir)
48
39
  let copies = assets.map((a) => ({ from: join(projectDir, a.path), to: a.path }))
49
40
  let files = isolates.map((i) => ({ to: isolateAssetPath(i.id, "bin"), bytes: i.bytecode }))
50
41
  for (let f of files) assets.push({ path: f.to, sha256: hashHex(f.bytes), size: f.bytes.length })
@@ -53,7 +44,7 @@ export function buildPackFolder(entry: string, bytecode: Buffer, isolates: { id:
53
44
  // defaults materialize under assets/fonts/ (a user file already at that
54
45
  // path must be the same bytes, otherwise the layout is ambiguous).
55
46
  let fonts: ManifestFont[] = []
56
- for (let font of resolvePackFonts(entry)) {
47
+ for (let font of resolvePackFonts(mode.projectDir)) {
57
48
  if (font.isDefault) {
58
49
  let path = "assets/fonts/" + basename(font.path)
59
50
  let bytes = readFileSync(font.path)
@@ -0,0 +1,86 @@
1
+ import { values, source } from "../lib/args"
2
+ import { bundleFlux, bundleSolid, compileToBytecode, findFluxIsolates } from "../bundle/bundler"
3
+ import { resolvePackFonts } from "../lib/fonts"
4
+ import { loadAppIdentity } from "../lib/project"
5
+ import { resolveMode } from "../lib/mode"
6
+ import { packApp, packFlux, packSolid } from "./trailer"
7
+ import { buildPackFolder, writePackFolder } from "./layout"
8
+ import { requireBinary } from "../lib/util"
9
+ import { dirname, join, resolve } from "node:path"
10
+
11
+ // Windows executables need the suffix; a user-given --output may already
12
+ // carry it.
13
+ function exeName(outfile: string): string {
14
+ return process.platform === "win32" && !outfile.toLowerCase().endsWith(".exe") ? outfile + ".exe" : outfile
15
+ }
16
+
17
+ // Write the packed executable, mark it runnable, and report its size.
18
+ async function writeExecutable(packed: Buffer, outfile: string) {
19
+ await Bun.write(outfile, packed)
20
+ if (process.platform !== "win32") {
21
+ Bun.spawnSync(["chmod", "+x", outfile])
22
+ }
23
+ console.log(`>> wrote ${packed.length} bytes to ${outfile}`)
24
+ }
25
+
26
+ export async function main() {
27
+ if (values.flux) {
28
+ if (values.folder || values.app) {
29
+ console.error("--folder and --app are for app packs; flux scripts have no folder or .srtapp output")
30
+ process.exit(1)
31
+ }
32
+ let outfile = exeName(values.output ?? source!.replace(/\.[jt]s$/, ""))
33
+ // The entry's isolate modules ride along as isolates/<id>.bin sections
34
+ // (module name = id, for stack attribution).
35
+ let isolates = []
36
+ for (let module of findFluxIsolates(dirname(resolve(source!)))) {
37
+ isolates.push({ id: module.id, bytecode: await compileToBytecode(await bundleFlux(module.path), module.id) })
38
+ }
39
+ if (isolates.length) console.log(`>> isolates: ${isolates.map((i) => i.id).join(", ")}`)
40
+ await writeExecutable(packFlux(await compileToBytecode(await bundleFlux(source!)), isolates), outfile)
41
+ process.exit()
42
+ }
43
+
44
+ // All solidrt outputs are the same canonical pack: manifest + bundle.bin +
45
+ // assets (fonts included). --folder writes it as a flat folder next to a
46
+ // bare runner; --app writes it alone as one .srtapp for a runner to load;
47
+ // the default single-file exe carries it as trailer sections.
48
+ let mode = resolveMode()
49
+ let identity = loadAppIdentity(mode.entry, mode.projectDir)
50
+ console.log(`>> app: ${identity.appId} (${identity.org} / ${identity.displayName})`)
51
+ if (identity.defaulted) {
52
+ console.warn('>> warning: no "solidrt.appId" in package.json; set a stable reverse-DNS id before distributing')
53
+ }
54
+ let fonts = resolvePackFonts(mode.projectDir)
55
+ console.log(`>> fonts: ${fonts.length ? fonts.map((f) => f.alias).join(", ") : "none"}`)
56
+
57
+ let bundled = await bundleSolid(mode)
58
+ let bytecode = await compileToBytecode(bundled.code)
59
+ let isolates = []
60
+ for (let i of bundled.isolates) isolates.push({ id: i.id, bytecode: await compileToBytecode(i.code, i.id) })
61
+ if (isolates.length) console.log(`>> isolates: ${isolates.map((i) => i.id).join(", ")}`)
62
+ let folder = buildPackFolder(mode, bytecode, isolates)
63
+
64
+ if (values.folder) {
65
+ let outDir = values.output ?? join("dist", "pack")
66
+ writePackFolder(outDir, requireBinary("solidrt"), bytecode, folder)
67
+ console.log(`>> wrote pack folder to ${resolve(outDir)}`)
68
+ process.exit()
69
+ }
70
+
71
+ if (values.app) {
72
+ let outfile = values.output ?? mode.entry.replace(/\.[jt]sx?$/, ".srtapp")
73
+ let packed = packApp(folder, bytecode)
74
+ await Bun.write(outfile, packed)
75
+ console.log(`>> wrote ${packed.length} bytes to ${outfile}`)
76
+ process.exit()
77
+ }
78
+
79
+ let outfile = values.output ?? mode.entry.replace(/\.[jt]sx?$/, "")
80
+ // On Windows the packed image is a PE executable; it needs a .exe name to run.
81
+ if (process.platform === "win32" && !outfile.toLowerCase().endsWith(".exe")) {
82
+ outfile += ".exe"
83
+ }
84
+ await writeExecutable(packSolid(folder, bytecode), outfile)
85
+ process.exit()
86
+ }
@@ -0,0 +1,97 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { runnerGlLibs } from "../lib/artifacts"
3
+ import { requireBinary } from "../lib/util"
4
+ import type { PackFolder } from "./layout"
5
+
6
+ // Trailer magic identifying the runner an embedded payload belongs to. Must
7
+ // match the runner-side checks: both runners parse the trailer through
8
+ // forge/src/trailer.rs (readers: lattice/src/main.rs, flux/src/bin/fluxrt.rs).
9
+ const MAGIC = {
10
+ fluxrt: Buffer.from([0x46, 0x4c, 0x55, 0x58, 0x52, 0x54, 0x88, 0x44]), // "FLUXRT\x88\x44"
11
+ solidrt: Buffer.from([0x53, 0x4f, 0x4c, 0x49, 0x44, 0x52, 0x54, 0x88, 0x44]), // "SOLIDRT\x88\x44"
12
+ }
13
+
14
+ // Section kinds in the trailer. Must match forge/src/trailer.rs (fluxrt only
15
+ // consumes kind-2 file sections; solidrt consumes all three).
16
+ const SECTION_MANIFEST = 1
17
+ const SECTION_FILE = 2
18
+ const SECTION_GL_LIB = 3
19
+
20
+ type Section = { kind: number; bytes: Buffer; name?: string }
21
+
22
+ // Append sections to the runner image: each section's bytes, then a table of
23
+ // section entries, then [table offset u64 LE][entry count u32 LE][magic].
24
+ // Table entry: [kind u32 LE][offset u64 LE][len u64 LE][name len u16 LE][name].
25
+ // Offsets are absolute file offsets. The runner reads its own image at
26
+ // startup, validates the magic, and slices the sections back out.
27
+ function packSections(runnerBytes: Buffer, sections: Section[], magic: Buffer): Buffer {
28
+ let parts: Buffer[] = [runnerBytes]
29
+ let entries: Buffer[] = []
30
+ let offset = runnerBytes.length
31
+ for (let section of sections) {
32
+ parts.push(section.bytes)
33
+ let name = Buffer.from(section.name ?? "", "utf8")
34
+ let entry = Buffer.allocUnsafe(22 + name.length)
35
+ entry.writeUInt32LE(section.kind, 0)
36
+ entry.writeBigUInt64LE(BigInt(offset), 4)
37
+ entry.writeBigUInt64LE(BigInt(section.bytes.length), 12)
38
+ entry.writeUInt16LE(name.length, 20)
39
+ name.copy(entry, 22)
40
+ entries.push(entry)
41
+ offset += section.bytes.length
42
+ }
43
+ let tail = Buffer.allocUnsafe(12)
44
+ tail.writeBigUInt64LE(BigInt(offset), 0) // the table starts where the sections end
45
+ tail.writeUInt32LE(sections.length, 8)
46
+ return Buffer.concat([...parts, ...entries, tail, magic])
47
+ }
48
+
49
+ // The pack folder in section form - the canonical manifest verbatim, then
50
+ // every manifest-listed file named by its manifest path. Bundle, fonts, and
51
+ // identity all come from the manifest; assets are read in place via ranged
52
+ // reads at their section offsets, so nothing is unpacked at runtime.
53
+ function appSections(folder: PackFolder, bytecode: Buffer): Section[] {
54
+ return [
55
+ { kind: SECTION_MANIFEST, bytes: Buffer.from(folder.manifest, "utf8") },
56
+ { kind: SECTION_FILE, bytes: bytecode, name: "bundle.bin" },
57
+ ...folder.copies.map((c) => ({ kind: SECTION_FILE, bytes: readFileSync(c.from), name: c.to })),
58
+ ...folder.files.map((f) => ({ kind: SECTION_FILE, bytes: f.bytes, name: f.to })),
59
+ ]
60
+ }
61
+
62
+ // The single-file solidrt executable: the runner image plus the app sections.
63
+ // GL libraries ride along as kind-3 sections (runtime freight, deliberately
64
+ // outside the manifest); the runner extracts those to its cache and preloads
65
+ // them before window setup.
66
+ export function packSolid(folder: PackFolder, bytecode: Buffer): Buffer {
67
+ let runnerPath = requireBinary("solidrt")
68
+ let sections: Section[] = [
69
+ ...appSections(folder, bytecode),
70
+ ...runnerGlLibs(runnerPath).map((lib) => ({ kind: SECTION_GL_LIB, bytes: readFileSync(lib.path), name: lib.name })),
71
+ ]
72
+ return packSections(readFileSync(runnerPath), sections, MAGIC.solidrt)
73
+ }
74
+
75
+ // A standalone .srtapp: the app sections alone, no runner in front and no GL
76
+ // libraries (the runner that loads it brings its own). The runner parses it
77
+ // exactly as it parses its own image (lattice/src/main.rs, load_payload), so
78
+ // `solidrt <file>.srtapp` runs it with the runner used in place - nothing is
79
+ // copied or appended to, and a signed runner stays signed. The extension is
80
+ // a convention; the magic is the contract.
81
+ export function packApp(folder: PackFolder, bytecode: Buffer): Buffer {
82
+ return packSections(Buffer.alloc(0), appSections(folder, bytecode), MAGIC.solidrt)
83
+ }
84
+
85
+ // The single-file flux executable: the fluxrt runner plus the program in the
86
+ // same section trailer packSolid uses, kind-2 file sections only -
87
+ // "bundle.bin" is the program, each isolate module "isolates/<id>.bin".
88
+ // Like packSolid, this assembles precompiled bytecode; the pack command
89
+ // compiles.
90
+ export function packFlux(bytecode: Buffer, isolates: { id: string; bytecode: Buffer }[] = []): Buffer {
91
+ let runnerBytes = readFileSync(requireBinary("fluxrt"))
92
+ let sections: Section[] = [
93
+ { kind: SECTION_FILE, bytes: bytecode, name: "bundle.bin" },
94
+ ...isolates.map((i) => ({ kind: SECTION_FILE, bytes: i.bytecode, name: `isolates/${i.id}.bin` })),
95
+ ]
96
+ return packSections(runnerBytes, sections, MAGIC.fluxrt)
97
+ }
@@ -0,0 +1,19 @@
1
+ # srt render
2
+
3
+ {{ usage render }}
4
+
5
+ Replays an optional recorded script and writes frames, which is how an app
6
+ produces video or deterministic screenshots with no display attached:
7
+ rendering uses SDL's offscreen driver, or alloy's own EGL pbuffer where that
8
+ driver cannot go headless.
9
+
10
+ ```sh
11
+ srt run --capture session.json
12
+ srt render --script session.json --fps 60 --duration 5
13
+ ```
14
+
15
+ `--capture` records key events from connected clients to a script (pointer
16
+ input is not captured yet). `--size` is physical output pixels: layout runs
17
+ at exactly that size, so frames are identical on every machine. Frames land
18
+ in the directory the command runs from; `-o <path>` picks another (or a
19
+ path prefix for the `-NNNNNN.png` names).
@@ -0,0 +1,49 @@
1
+ import { appArgs, values } from "../lib/args"
2
+ import { requireBinary, run } from "../lib/util"
3
+ import { bundleSolid, writeIsolates } from "../bundle/bundler"
4
+ import { collectAssets } from "../lib/project"
5
+ import { resolveMode } from "../lib/mode"
6
+ import { cpSync, mkdirSync, rmSync } from "node:fs"
7
+ import { basename, dirname, join, resolve } from "path"
8
+
9
+ export async function main() {
10
+ let mode = resolveMode()
11
+ let entry = mode.entry
12
+ let result = await bundleSolid(mode)
13
+ // The staged run dir (okf/backlog/build-output-dirs.md): bundle +
14
+ // isolates/ + assets/ under one root - the shape of an installed version
15
+ // dir, so the runtime's assets mount resolves both trees. Under the cwd's
16
+ // dist/ (the build root, see the bundle command). Wiped first so removed
17
+ // isolates and deleted assets cannot go stale; render owns this subdir
18
+ // and nothing else under dist/.
19
+ let outDir = resolve("dist", "render")
20
+ rmSync(outDir, { recursive: true, force: true })
21
+ let jsOutfile = join(outDir, basename(entry).replace(/\.[jt]sx?$/, "") + ".srt.js")
22
+ await Bun.write(jsOutfile, result.code)
23
+ writeIsolates(join(outDir, "isolates"), result.isolates)
24
+ // The project's assets/ tree, copied in (dotfiles filtered, like a pack)
25
+ // so `assets/...` resolves like it does under the dev server and in a
26
+ // packed app (the runtime's cwd is the data sandbox, which holds no
27
+ // assets). File mode collects none.
28
+ for (let asset of collectAssets(mode.projectDir).assets) {
29
+ let dest = join(outDir, asset.path)
30
+ mkdirSync(dirname(dest), { recursive: true })
31
+ cpSync(join(mode.projectDir!, asset.path), dest)
32
+ }
33
+ let runner = requireBinary("solidrt-go")
34
+ let playbackArgs = ["--playback"]
35
+ if (values.fps) playbackArgs.push("--fps", values.fps)
36
+ if (values.duration) playbackArgs.push("--duration", values.duration)
37
+ if (values.size) playbackArgs.push("--size", values.size)
38
+ if (values.script) playbackArgs.push("--script", resolve(values.script))
39
+ // Always absolute: the runtime chdirs into the app's data sandbox before
40
+ // frames are written, so a bare prefix would land the PNGs there.
41
+ playbackArgs.push("--out", resolve(values.output ?? "."))
42
+ playbackArgs.push("--assets", outDir)
43
+ playbackArgs.push(jsOutfile)
44
+ // The runner takes everything after the source path verbatim as the app's
45
+ // argument vector (flux:process argv).
46
+ playbackArgs.push(...appArgs)
47
+ let exit = await run(runner, playbackArgs)
48
+ process.exit(exit)
49
+ }
@@ -0,0 +1,126 @@
1
+ // The server's command line: the interface `srt run` / `srt server` (a bun
2
+ // launcher that spawns this process) and the console (a flux app that spawns
3
+ // it or imports it) share, so it takes flags and never a config blob
4
+ // (okf/done/srt-command-folders.md):
5
+ //
6
+ // flux server.js [file] [--project|--file] [--port N] [--lan] [--proxy-http]
7
+ // [--capture f] [--tunnel] [--stats] [--minify]
8
+ // [--client N [--data-root d] [--size WxH]] [-- args]
9
+ //
10
+ // --client N spawns the local solidrt-go client with data slot N (`srt run`);
11
+ // without it the server runs alone (`srt server`). Everything after a bare
12
+ // "--" is the app's argument vector (flux:process argv on every client).
13
+ // flux:process argv is the tail after the script, so a small parser covers
14
+ // it: boolean flags, valued flags, one positional, the "--" tail.
15
+
16
+ import { argv } from "flux:process"
17
+
18
+ export type ServerArgs = {
19
+ /** The entry file argument, or none (the project at the cwd). */
20
+ entry: string | undefined
21
+ project: boolean
22
+ file: boolean
23
+ lan: boolean
24
+ proxyHttp: boolean
25
+ tunnel: boolean
26
+ stats: boolean
27
+ minify: boolean
28
+ port: number | undefined
29
+ capture: string | undefined
30
+ /** The local client's data slot, or null for no local client. */
31
+ client: number | null
32
+ dataRoot: string | undefined
33
+ size: string | undefined
34
+ appArgs: string[]
35
+ }
36
+
37
+ // A fatal usage or configuration error. flux has no exit call: an uncaught
38
+ // error at startup ends the process with a nonzero status and the message
39
+ // on stderr, which is what a failed launch needs.
40
+ export function fail(message: string): never {
41
+ throw new Error(message)
42
+ }
43
+
44
+ function integer(name: string, raw: string, min: number, max: number): number {
45
+ let value = Number(raw)
46
+ if (!/^\d+$/.test(raw) || value < min || value > max) {
47
+ fail(`Invalid --${name} value "${raw}": expected an integer between ${min} and ${max}`)
48
+ }
49
+ return value
50
+ }
51
+
52
+ export function parseArgs(raw: string[] = argv): ServerArgs {
53
+ let sep = raw.indexOf("--")
54
+ let own = sep === -1 ? raw : raw.slice(0, sep)
55
+ let args: ServerArgs = {
56
+ entry: undefined,
57
+ project: false,
58
+ file: false,
59
+ lan: false,
60
+ proxyHttp: false,
61
+ tunnel: false,
62
+ stats: false,
63
+ minify: false,
64
+ port: undefined,
65
+ capture: undefined,
66
+ client: null,
67
+ dataRoot: undefined,
68
+ size: undefined,
69
+ appArgs: sep === -1 ? [] : raw.slice(sep + 1),
70
+ }
71
+ let value = (name: string, i: number): string => {
72
+ let v = own[i]
73
+ if (v === undefined || v.startsWith("--")) fail(`--${name} needs a value`)
74
+ return v
75
+ }
76
+ for (let i = 0; i < own.length; i++) {
77
+ let arg = own[i]!
78
+ if (!arg.startsWith("--")) {
79
+ if (args.entry !== undefined) fail(`Unexpected argument: ${arg}`)
80
+ args.entry = arg
81
+ continue
82
+ }
83
+ let name = arg.slice(2)
84
+ switch (name) {
85
+ case "project":
86
+ args.project = true
87
+ break
88
+ case "file":
89
+ args.file = true
90
+ break
91
+ case "lan":
92
+ args.lan = true
93
+ break
94
+ case "proxy-http":
95
+ args.proxyHttp = true
96
+ break
97
+ case "tunnel":
98
+ args.tunnel = true
99
+ break
100
+ case "stats":
101
+ args.stats = true
102
+ break
103
+ case "minify":
104
+ args.minify = true
105
+ break
106
+ case "port":
107
+ args.port = integer(name, value(name, ++i), 1, 65535)
108
+ break
109
+ case "capture":
110
+ args.capture = value(name, ++i)
111
+ break
112
+ case "client":
113
+ args.client = integer(name, value(name, ++i), 0, Number.MAX_SAFE_INTEGER)
114
+ break
115
+ case "data-root":
116
+ args.dataRoot = value(name, ++i)
117
+ break
118
+ case "size":
119
+ args.size = value(name, ++i)
120
+ break
121
+ default:
122
+ fail(`Unknown flag: ${arg}`)
123
+ }
124
+ }
125
+ return args
126
+ }
@@ -0,0 +1,47 @@
1
+ // Where the server finds what it spawns (okf/done/srt-command-folders.md):
2
+ //
3
+ // - the platform binaries (solidrt-go) in SRT_PLATFORM_DIR, which srt sets to
4
+ // the platform package it resolved, else a checkout's SRT_HOME/dist/<triple>;
5
+ // - srt itself, run as `bun <SRT_CLI>/bin/srt`, so the bundle and the startup
6
+ // typecheck run by command name (`srt bundle --json`, `srt check`): SRT_CLI
7
+ // is the @solidrt/cli package root (srt sets it; a checkout's is
8
+ // SRT_HOME/packages/cli) and SRT_BUN the bun to use (srt's own; else PATH).
9
+ //
10
+ // So `flux server.js` started by hand needs SRT_HOME (a built checkout) or
11
+ // the three variables, and started by srt needs nothing.
12
+
13
+ import { file } from "flux:fs"
14
+ import { join } from "flux:path"
15
+ import { arch, env, platform } from "flux:process"
16
+ import { fail } from "./args"
17
+
18
+ // Host platform -> dist triple, as src/artifacts.ts maps it for bun.
19
+ const TRIPLES: Record<string, string> = {
20
+ "linux-x64": "linux-x64-gnu",
21
+ "linux-arm64": "linux-arm64-gnu",
22
+ "darwin-arm64": "darwin-arm64",
23
+ "win32-x64": "win32-x64-msvc",
24
+ }
25
+
26
+ function platformDir(): string | null {
27
+ if (env.SRT_PLATFORM_DIR) return env.SRT_PLATFORM_DIR
28
+ let triple = TRIPLES[`${platform}-${arch}`]
29
+ return env.SRT_HOME && triple ? join(env.SRT_HOME, "dist", triple) : null
30
+ }
31
+
32
+ /** The absolute path of a platform binary, or a failed launch. */
33
+ export async function requireBinary(name: string): Promise<string> {
34
+ let dir = platformDir()
35
+ let path = dir ? join(dir, name + (platform === "win32" ? ".exe" : "")) : null
36
+ if (path && (await file(path).exists())) return path
37
+ fail(
38
+ `Could not find the ${name} binary${dir ? ` in ${dir}` : ""}. Run through srt, or set SRT_HOME to a SolidRT checkout built with make client.`,
39
+ )
40
+ }
41
+
42
+ /** The srt command prefix, `[bun, <cli>/bin/srt]`, for the bun-side commands. */
43
+ export function srtCommand(): string[] {
44
+ let cli = env.SRT_CLI ?? (env.SRT_HOME ? join(env.SRT_HOME, "packages", "cli") : undefined)
45
+ if (!cli) fail("Could not find srt: set SRT_CLI to the @solidrt/cli package root, or SRT_HOME to a SolidRT checkout.")
46
+ return [env.SRT_BUN ?? "bun", join(cli, "bin", "srt")]
47
+ }
@@ -0,0 +1,54 @@
1
+ // What the server resolved at startup from its flags (args.ts), its mode
2
+ // (mode.ts) and its environment (binaries.ts), shared with the route
3
+ // handlers through state.ts.
4
+
5
+ export type ServerConfig = {
6
+ /** Project mode serves the project at its root; file mode serves one file. */
7
+ mode: "project" | "file"
8
+ /** The canonical project root (project mode) or file path (file mode): the
9
+ * registry key, and what every control response names. */
10
+ key: string
11
+ /** This server's folder under ~/.solidrt/servers/: the registry record,
12
+ * the remembered port, the tunnel key. */
13
+ serverDir: string
14
+ /** The app entry (absolute path) rebuilt on a reload. */
15
+ entry: string
16
+ /** Directory served by the file routes (the entry's directory). */
17
+ sourceDir: string
18
+ /** Project root whose assets/ folder the /assets/ route serves; null in
19
+ * file mode (no assets). */
20
+ projectDir: string | null
21
+ /** Where the bun-side commands run and the entry arguments they take
22
+ * (`<entry> --project` in the project root, `<entry> --file` in the
23
+ * entry's directory), so they resolve the same mode this server did. */
24
+ cwd: string
25
+ entryArgs: string[]
26
+ /** The srt command prefix (binaries.ts) the bundle and the startup
27
+ * typecheck run through. */
28
+ srt: string[]
29
+ /** An explicit --port; otherwise the remembered port is tried, then the
30
+ * first free one. */
31
+ port?: number
32
+ /** Bind every interface (and announce the LAN address); default loopback. */
33
+ lan: boolean
34
+ /** The address clients are told to reach this server on. */
35
+ address: string
36
+ proxyHttp: boolean
37
+ /** The session's app arguments (the tail after a bare "--"), included in
38
+ * every reload push as flux:process argv. */
39
+ args: string[]
40
+ /** Minify the rebuild output. */
41
+ minify: boolean
42
+ /** Enable the sqlite-backed proxy cache. */
43
+ cache: boolean
44
+ /** Build outputs and the proxy cache: the project's .srt-data, or the
45
+ * server folder in file mode. */
46
+ cacheDir: string
47
+ /** Destination for captured key events, or unset when off. */
48
+ capture?: string
49
+ stats: boolean
50
+ /** Accept ticket-paired clients through the p2p tunnel. */
51
+ tunnel: boolean
52
+ /** The local client to spawn once the port is bound, or null. */
53
+ client: { cmd: string; args: string[] } | null
54
+ }