@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.
- package/AGENTS.md +79 -36
- package/README.md +86 -15
- package/agents/assets.md +46 -0
- package/agents/debugging.md +294 -0
- package/dist/console.srtapp +94123 -73
- package/dist/server.js +3936 -0
- package/package.json +11 -10
- package/src/android/docs.md +21 -0
- package/src/android/main.ts +286 -0
- package/src/{bundler.ts → bundle/bundler.ts} +163 -76
- package/src/bundle/docs.md +12 -0
- package/src/bundle/main.ts +203 -0
- package/src/check/docs.md +10 -0
- package/src/check/main.ts +85 -0
- package/src/{commands/check.ts → check/typecheck.ts} +11 -34
- package/src/client/docs.md +9 -0
- package/src/client/main.ts +33 -0
- package/src/console/docs.md +14 -0
- package/src/console/main.ts +21 -0
- package/src/demo/docs.md +27 -0
- package/src/demo/main.ts +67 -0
- package/src/init/docs.md +11 -0
- package/src/{commands/init.ts → init/main.ts} +31 -25
- package/src/init/scaffold/AGENTS.md +98 -0
- package/src/init/scaffold/package.json +23 -0
- package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
- package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
- package/src/lib/args.ts +194 -0
- package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
- package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
- package/src/{fonts.ts → lib/fonts.ts} +12 -26
- package/src/lib/mode.ts +77 -0
- package/src/{project.ts → lib/project.ts} +109 -61
- package/src/lib/registry.ts +120 -0
- package/src/lib/server-bundle.ts +24 -0
- package/src/lib/usage.ts +117 -0
- package/src/lib/util.ts +36 -0
- package/src/main.ts +109 -31
- package/src/mcp/docs.md +22 -0
- package/src/mcp/main.ts +719 -0
- package/src/pack/docs.md +18 -0
- package/src/{pack-folder.ts → pack/layout.ts} +13 -22
- package/src/pack/main.ts +86 -0
- package/src/pack/trailer.ts +97 -0
- package/src/render/docs.md +19 -0
- package/src/render/main.ts +49 -0
- package/src/server/args.ts +126 -0
- package/src/server/binaries.ts +47 -0
- package/src/server/config.ts +54 -0
- package/{server → src/server}/control.ts +246 -78
- package/src/server/docs.md +51 -0
- package/src/server/line-editor.ts +200 -0
- package/src/server/main.ts +473 -0
- package/src/server/mode.ts +92 -0
- package/src/server/rebuild.ts +90 -0
- package/src/server/registry.ts +138 -0
- package/src/server/remap.ts +60 -0
- package/src/server/repl.ts +223 -0
- package/src/server/state.ts +54 -0
- package/{server → src/server}/tsconfig.json +1 -1
- package/{server → src/server}/tunnel.ts +6 -6
- package/src/server/watcher.ts +121 -0
- package/src/tool/main.ts +70 -0
- package/src/types/bundle.d.ts +24 -0
- package/src/types/control.d.ts +90 -0
- package/src/types/registry.d.ts +16 -0
- package/scaffold/AGENTS.md +0 -630
- package/scaffold/package.json +0 -22
- package/scaffold/templates/components/icon.tsx +0 -48
- package/scaffold/templates/default/icon.tsx +0 -48
- package/server/main.ts +0 -308
- package/server/rebuild.ts +0 -68
- package/server/remap.ts +0 -47
- package/server/state.ts +0 -93
- package/src/args.ts +0 -212
- package/src/bundle-cli.ts +0 -13
- package/src/commands/bundle.ts +0 -76
- package/src/commands/client.ts +0 -34
- package/src/commands/mcp.ts +0 -603
- package/src/commands/pack.ts +0 -65
- package/src/commands/render.ts +0 -24
- package/src/commands/server.ts +0 -73
- package/src/dev-android.ts +0 -176
- package/src/dev-client.ts +0 -29
- package/src/dev-server.ts +0 -302
- package/src/packer.ts +0 -103
- package/src/repl.ts +0 -233
- package/src/util.ts +0 -121
- package/src/watcher.ts +0 -69
- /package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +0 -0
- /package/src/{prompt.ts → init/prompt.ts} +0 -0
- /package/{scaffold → src/init/scaffold}/gitignore +0 -0
- /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
- /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
- /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
- /package/{server → src/server}/cache.ts +0 -0
- /package/{server → src/server}/proxy.ts +0 -0
- /package/{server → src/server}/qr.ts +0 -0
package/src/pack/docs.md
ADDED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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(
|
|
45
|
-
let identity = loadAppIdentity(entry)
|
|
46
|
-
|
|
47
|
-
|
|
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(
|
|
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)
|
package/src/pack/main.ts
ADDED
|
@@ -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
|
+
}
|