@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
|
@@ -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
|
|
5
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
69
|
-
//
|
|
70
|
-
|
|
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[]
|
|
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(
|
|
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(
|
|
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 =
|
|
146
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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 =
|
|
197
|
-
let config = project?.
|
|
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?.
|
|
257
|
+
let fallbackName = (project?.name ?? basename(sourcePath).replace(/\.[jt]sx?$/, "")).split("/").pop()!
|
|
201
258
|
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}
|
package/src/lib/usage.ts
ADDED
|
@@ -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)`
|
package/src/lib/util.ts
ADDED
|
@@ -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
|
+
}
|