@solidrt/cli 0.0.11 → 0.0.14
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 +4 -0
- package/package.json +8 -7
- package/scaffold/AGENTS.md +71 -0
- package/scaffold/gitignore +4 -0
- package/scaffold/package.json +17 -0
- package/scaffold/src/index.tsx +25 -0
- package/scaffold/tsconfig.json +10 -0
- package/src/args.ts +4 -0
- package/src/artifacts.ts +20 -12
- package/src/commands/init.ts +67 -0
- package/src/commands/server.ts +1 -0
- package/src/dev-android.ts +77 -27
- package/src/dev-server.ts +7 -6
- package/src/main.ts +4 -1
- package/src/repl.ts +21 -4
- package/src/util.ts +1 -0
package/AGENTS.md
CHANGED
|
@@ -9,6 +9,10 @@ bundled `flux` runtime, not on Bun. Invoke via `bunx srt <command>`.
|
|
|
9
9
|
|
|
10
10
|
## Commands
|
|
11
11
|
|
|
12
|
+
- `bunx srt init <dir>` - scaffold a new SolidRT project into a new (empty)
|
|
13
|
+
folder: package.json, tsconfig.json, AGENTS.md, a starter src/index.tsx, then
|
|
14
|
+
installs deps. Greenfield shortcut (no install needed first):
|
|
15
|
+
`bun create solidrt <dir>`.
|
|
12
16
|
- `bunx srt run src/index.tsx` - dev server + a local client window, watches and
|
|
13
17
|
hot-reloads. NEEDS A DISPLAY (opens a GUI window). Not usable headless.
|
|
14
18
|
- `bunx srt bundle src/index.tsx` - transpile to `<file>.srt.js`. With
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -10,23 +10,24 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
12
12
|
"src/",
|
|
13
|
+
"scaffold/",
|
|
13
14
|
"AGENTS.md"
|
|
14
15
|
],
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"@babel/core": "^7.28.5",
|
|
17
18
|
"@babel/preset-typescript": "^7.28.5",
|
|
18
|
-
"babel-preset-solid": "2.0.0-beta.
|
|
19
|
+
"babel-preset-solid": "2.0.0-beta.15",
|
|
19
20
|
"bonjour-service": "^1.4.0",
|
|
20
21
|
"qrcode-generator": "^2.0.4"
|
|
21
22
|
},
|
|
22
23
|
"optionalDependencies": {
|
|
23
|
-
"@solidrt/darwin-arm64": "0.0.
|
|
24
|
-
"@solidrt/linux-x64-gnu": "0.0.
|
|
25
|
-
"@solidrt/win32-x64-msvc": "0.0.
|
|
24
|
+
"@solidrt/darwin-arm64": "0.0.0",
|
|
25
|
+
"@solidrt/linux-x64-gnu": "0.0.0",
|
|
26
|
+
"@solidrt/win32-x64-msvc": "0.0.0"
|
|
26
27
|
},
|
|
27
28
|
"peerDependencies": {
|
|
28
|
-
"@solidrt/core": "0.0.
|
|
29
|
-
"typescript": "^
|
|
29
|
+
"@solidrt/core": "0.0.0",
|
|
30
|
+
"typescript": "^6"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/bun": "latest"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# SolidRT app - agent notes
|
|
2
|
+
|
|
3
|
+
This project uses SolidRT: a custom SolidJS renderer that paints through a Rust
|
|
4
|
+
runtime. No DOM, no HTML, no CSS cascade. If you are an AI assistant, read this
|
|
5
|
+
before writing or editing code here.
|
|
6
|
+
|
|
7
|
+
Authoritative references ship inside the installed packages - read them:
|
|
8
|
+
- node_modules/@solidrt/core/AGENTS.md - element/prop/reactivity model
|
|
9
|
+
- node_modules/@solidrt/cli/AGENTS.md - running, bundling, headless verify
|
|
10
|
+
- node_modules/@solidrt/core/src/types.d.ts and jsx-runtime.d.ts - source of truth
|
|
11
|
+
|
|
12
|
+
<!-- Claude Code auto-imports these; other tools read the paths above. -->
|
|
13
|
+
@./node_modules/@solidrt/core/AGENTS.md
|
|
14
|
+
@./node_modules/@solidrt/cli/AGENTS.md
|
|
15
|
+
|
|
16
|
+
## The things assistants get wrong (this is not React/DOM)
|
|
17
|
+
|
|
18
|
+
1. SolidJS 2.0, not React (and not Solid 1.x). Import the whole authoring
|
|
19
|
+
surface from @solidrt/core - it re-exports the substrate so you do not have to
|
|
20
|
+
know which package a symbol lives in: render plus the window/paint/event APIs,
|
|
21
|
+
the reactive primitives (createSignal, createMemo, createEffect, createStore,
|
|
22
|
+
reconcile, mapArray, untrack, onCleanup), and the control-flow components (For,
|
|
23
|
+
Show, Switch/Match, Repeat, Loading, Errored). No hooks, no virtual DOM, the
|
|
24
|
+
component body runs once. Render lists with <For each={...}>{item => ...}</For>,
|
|
25
|
+
never array.map. createEffect has the 2.0 two-function shape - a TRACKED
|
|
26
|
+
compute that reads signals and returns a value, then an UNTRACKED effect that
|
|
27
|
+
receives it:
|
|
28
|
+
createEffect(() => count(), (c) => console.log(c))
|
|
29
|
+
The Solid 1.x single-callback form, createEffect(() => console.log(count())),
|
|
30
|
+
does NOT track in 2.0.
|
|
31
|
+
2. Host elements are lowercase intrinsics: window, view, text, rect, oval, path,
|
|
32
|
+
texture, audio (+ d- variants). No div/span/img/button.
|
|
33
|
+
3. render(() => <App/>) once, top level. The root MUST be <window> or it throws.
|
|
34
|
+
4. Containers (window, view) DO NOT PAINT. Background = a draw primitive child
|
|
35
|
+
placed behind the content, e.g. <d-rect color="..." />.
|
|
36
|
+
5. Paint color is the color prop (a CSS color string). No fill/stroke/background
|
|
37
|
+
prop. Outline: drawStyle="stroke" + strokeWidth.
|
|
38
|
+
6. No onClick/onPress. A button is a <view>/<rect> with onPointerDown.
|
|
39
|
+
7. d- prefix = detached from layout: a plain element is laid out by Taffy, a
|
|
40
|
+
d-element you position with x/y (omit to fill the parent = how backgrounds
|
|
41
|
+
work). A detached node cannot have attached (regular, Taffy-laid-out)
|
|
42
|
+
children - everything under a d-element must itself be detached. Nesting a
|
|
43
|
+
plain <view>/<text> inside a d-element is an error.
|
|
44
|
+
8. Per-frame work: onFrame((tick, frame) => {}), or the standard
|
|
45
|
+
requestAnimationFrame(t => {}) for animations. Also onResize, onLayout.
|
|
46
|
+
9. Device/GPU via subpath imports: @solidrt/core/camera, /microphone, /gpu.
|
|
47
|
+
10. tsconfig needs jsx:"preserve" + jsxImportSource:"@solidrt/core". Solid peer
|
|
48
|
+
deps are pinned betas - do not bump them casually.
|
|
49
|
+
11. Use ASCII characters whenever possible in code and text - for example, no
|
|
50
|
+
em-dashes (use a hyphen), no smart/curly quotes, no unicode symbols.
|
|
51
|
+
12. Mind the safe area. safeArea() from @solidrt/core is a reactive accessor
|
|
52
|
+
returning { top, left, right, bottom } insets (like CSS
|
|
53
|
+
env(safe-area-inset-*)). It is fine to place content outside the safe-area
|
|
54
|
+
zone (e.g. full-bleed backgrounds), just be aware it may not be visible and
|
|
55
|
+
cannot be interacted with (touch gestures). Keep interactive or essential
|
|
56
|
+
content inside the safe area by padding the edges.
|
|
57
|
+
13. Prefer let over const. Use const only for real constants - a single fixed
|
|
58
|
+
string or number value - and name those in ALL_CAPS.
|
|
59
|
+
14. SolidJS/SolidRT is a reactive framework: strongly prefer reactive primitives
|
|
60
|
+
(signals, memos, effects, the control-flow components) over imperative code.
|
|
61
|
+
Derive state with createMemo, react with createEffect, render conditionally
|
|
62
|
+
with <Show>/<Switch> and lists with <For> - do not hand-roll imperative
|
|
63
|
+
updates, manual subscriptions, or DOM-style mutation.
|
|
64
|
+
|
|
65
|
+
## Run / verify
|
|
66
|
+
|
|
67
|
+
- bunx srt run src/index.tsx - dev server + window (needs a display)
|
|
68
|
+
- bunx srt bundle src/index.tsx - exit 0 means it compiles
|
|
69
|
+
- bunx srt record src/index.tsx --size 480x640 --duration 1 --fps 2 - headless
|
|
70
|
+
render to PNG frames (proves it renders; see the cli AGENTS.md for where the
|
|
71
|
+
frames land)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "solidrt-app",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "srt run src/index.tsx",
|
|
7
|
+
"bundle": "srt bundle src/index.tsx"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@solidrt/core": "latest"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@solidrt/cli": "latest",
|
|
14
|
+
"@solidrt/flux-types": "latest",
|
|
15
|
+
"typescript": "^6"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { render } from "@solidrt/core"
|
|
2
|
+
|
|
3
|
+
function Icon() {
|
|
4
|
+
return (
|
|
5
|
+
<view width={100} height={100} scale={3}>
|
|
6
|
+
<d-path d="M50 50L0 50L50 0Z" color="#1a3380" />
|
|
7
|
+
<d-path d="M50 50L50 100L0 50Z" color="#3366b3" />
|
|
8
|
+
<d-path d="M50 25L50 0L75 25Z" color="#6699e6" />
|
|
9
|
+
<d-path d="M50 25L75 25L75 50L50 50Z" color="#3366b3" />
|
|
10
|
+
<d-path d="M50 50L75 50L50 75Z" color="#6699e6" />
|
|
11
|
+
<d-path d="M75 50L75 75L50 100L50 75Z" color="#1a3380" />
|
|
12
|
+
<d-path d="M100 50L75 75L75 25Z" color="#6699e6" />
|
|
13
|
+
</view>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function App() {
|
|
18
|
+
return (
|
|
19
|
+
<window alignItems="center" justifyContent="center">
|
|
20
|
+
<Icon />
|
|
21
|
+
</window>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render(() => <App />)
|
package/src/args.ts
CHANGED
|
@@ -35,6 +35,9 @@ function usage(line: string): never {
|
|
|
35
35
|
// Per-command argument requirements. Called once before dispatch.
|
|
36
36
|
export function validateArgs() {
|
|
37
37
|
switch (command) {
|
|
38
|
+
case "init":
|
|
39
|
+
if (!source) usage("srt init <dir> (the target folder is required)")
|
|
40
|
+
break
|
|
38
41
|
case "bundle":
|
|
39
42
|
if (values.flux) {
|
|
40
43
|
if (!source || !isTs) usage("srt bundle --flux [options] <entry.[ts|js]>")
|
|
@@ -65,6 +68,7 @@ export function printUsage() {
|
|
|
65
68
|
console.error(`Usage: srt <command> [options] [file]
|
|
66
69
|
|
|
67
70
|
Commands:
|
|
71
|
+
init <dir> Scaffold a new SolidRT project into a new (empty) folder
|
|
68
72
|
run [file] Start dev server + local solidrt-go client
|
|
69
73
|
server [file] Start dev server only
|
|
70
74
|
client Start solidrt-go client only
|
package/src/artifacts.ts
CHANGED
|
@@ -44,27 +44,35 @@ export function resolveBinary(name: string) {
|
|
|
44
44
|
return null
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// The Android client APK is
|
|
48
|
-
//
|
|
49
|
-
// host triple map.
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
// The Android client APK is host-independent (it bundles native .so for the
|
|
48
|
+
// device, not the host), so it lives under dist/android/<abi>/ rather than the
|
|
49
|
+
// host triple map. arm64-v8a (a fat APK: arm64-v8a + the x86_64 emulator) and
|
|
50
|
+
// armeabi-v7a (32-bit) ship published npm packages; other ABIs (e.g. x86) only
|
|
51
|
+
// resolve via the SRT_HOME contributor path.
|
|
52
|
+
let DEFAULT_ANDROID_ABI = "arm64-v8a"
|
|
53
|
+
export let ANDROID_PKG_MAP: Record<string, string> = {
|
|
54
|
+
"arm64-v8a": "@solidrt/android-arm64-v8a",
|
|
55
|
+
"armeabi-v7a": "@solidrt/android-armeabi-v7a",
|
|
56
|
+
}
|
|
52
57
|
|
|
53
|
-
export function resolveApk() {
|
|
58
|
+
export function resolveApk(abi: string = DEFAULT_ANDROID_ABI) {
|
|
54
59
|
// 1. SRT_HOME: contributor checkout, where `make dist-android` stages the APK
|
|
55
60
|
// under dist/android/<abi>/.
|
|
56
61
|
let srtRoot = process.env.SRT_HOME
|
|
57
62
|
if (srtRoot) {
|
|
58
|
-
let apk = resolve(srtRoot, "dist/android",
|
|
63
|
+
let apk = resolve(srtRoot, "dist/android", abi, "solidrt-go.apk")
|
|
59
64
|
if (existsSync(apk)) return apk
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
// 2. Platform npm package
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
let pkg = ANDROID_PKG_MAP[abi]
|
|
69
|
+
if (pkg) {
|
|
70
|
+
try {
|
|
71
|
+
let pkgDir = dirname(require.resolve(`${pkg}/package.json`))
|
|
72
|
+
let apk = resolve(pkgDir, "solidrt-go.apk")
|
|
73
|
+
if (existsSync(apk)) return apk
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
68
76
|
|
|
69
77
|
return null
|
|
70
78
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path"
|
|
3
|
+
import { source } from "../args"
|
|
4
|
+
|
|
5
|
+
const SCAFFOLD_DIR = join(import.meta.dir, "../../scaffold")
|
|
6
|
+
|
|
7
|
+
// What gets written into a new project. Sources live in cli/scaffold/. The
|
|
8
|
+
// .gitignore is stored there as `gitignore` because npm strips files literally
|
|
9
|
+
// named `.gitignore` from published packages, so it is renamed on the way out.
|
|
10
|
+
const TEMPLATE_FILES: Array<{ from: string; to: string }> = [
|
|
11
|
+
{ from: "package.json", to: "package.json" },
|
|
12
|
+
{ from: "tsconfig.json", to: "tsconfig.json" },
|
|
13
|
+
{ from: "gitignore", to: ".gitignore" },
|
|
14
|
+
{ from: "AGENTS.md", to: "AGENTS.md" },
|
|
15
|
+
{ from: "src/index.tsx", to: "src/index.tsx" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
// A valid npm package name derived from the target directory.
|
|
19
|
+
function packageName(dir: string): string {
|
|
20
|
+
let name = basename(resolve(dir))
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9-_.]/g, "-")
|
|
23
|
+
return name || "solidrt-app"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runInitCommand() {
|
|
27
|
+
// The target folder is required (validateArgs enforces it) and must be empty
|
|
28
|
+
// or absent, so init can never overwrite files in an existing project.
|
|
29
|
+
let dir = source!
|
|
30
|
+
let existing = await readdir(dir).catch(() => null)
|
|
31
|
+
if (existing && existing.length > 0) {
|
|
32
|
+
console.error(`!! ${resolve(dir)} already exists and is not empty; choose a new folder name`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`>> scaffolding SolidRT project in ${resolve(dir)}`)
|
|
37
|
+
for (let { from, to } of TEMPLATE_FILES) {
|
|
38
|
+
let dest = join(dir, to)
|
|
39
|
+
await mkdir(dirname(dest), { recursive: true })
|
|
40
|
+
await writeFile(dest, await readFile(join(SCAFFOLD_DIR, from)))
|
|
41
|
+
console.log(` write ${to}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The scaffold package.json carries a placeholder name; set it from the
|
|
45
|
+
// target folder.
|
|
46
|
+
let pkgPath = join(dir, "package.json")
|
|
47
|
+
let pkg = JSON.parse(await readFile(pkgPath, "utf8"))
|
|
48
|
+
pkg.name = packageName(dir)
|
|
49
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n")
|
|
50
|
+
|
|
51
|
+
// Deps are declared in scaffold/package.json (Solid peers resolve via
|
|
52
|
+
// @solidrt/core's peerDependencies), so a plain install is enough.
|
|
53
|
+
console.log("\n>> installing dependencies")
|
|
54
|
+
let install = Bun.spawnSync(["bun", "install"], {
|
|
55
|
+
cwd: dir,
|
|
56
|
+
stdout: "inherit",
|
|
57
|
+
stderr: "inherit",
|
|
58
|
+
})
|
|
59
|
+
if (install.exitCode !== 0) {
|
|
60
|
+
console.error("\n!! dependency install failed; retry with `bun install` in the project")
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let prefix = dir === "." ? "" : `cd ${dir} && `
|
|
65
|
+
console.log(`\n>> done. next:\n ${prefix}bunx srt run src/index.tsx\n`)
|
|
66
|
+
process.exit()
|
|
67
|
+
}
|
package/src/commands/server.ts
CHANGED
|
@@ -14,6 +14,7 @@ export async function runServerCommand() {
|
|
|
14
14
|
// Initialize state from args
|
|
15
15
|
state.source = source
|
|
16
16
|
state.sourceDir = source ? dirname(resolve(source)) : process.cwd()
|
|
17
|
+
state.stats = values.stats
|
|
17
18
|
|
|
18
19
|
if (values["proxy-http"]) {
|
|
19
20
|
cache.initCache({ dir: process.cwd() })
|
package/src/dev-android.ts
CHANGED
|
@@ -1,26 +1,54 @@
|
|
|
1
|
+
import { networkInterfaces } from "node:os"
|
|
1
2
|
import { print, requireAdb } from "./util"
|
|
2
|
-
import { resolveApk } from "./artifacts"
|
|
3
|
+
import { resolveApk, ANDROID_PKG_MAP } from "./artifacts"
|
|
3
4
|
import { values } from "./args"
|
|
4
5
|
import { DEV_PORT } from "./dev-server"
|
|
5
6
|
|
|
6
7
|
// Launch component of the "go" dev-client flavor (see lattice/Makefile.android).
|
|
7
8
|
let PACKAGE_ACTIVITY = "com.solidrt.go/com.solidrt.app.MainActivity"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
let ipToInt = (ip: string) => ip.split(".").reduce((acc, o) => (acc << 8) + (parseInt(o, 10) & 255), 0) >>> 0
|
|
11
|
+
|
|
12
|
+
// This host's IPv4 on the same subnet as `deviceIp`: the interface whose
|
|
13
|
+
// (address & netmask) matches the device's. Pure computation over what the OS
|
|
14
|
+
// reports (cross-platform, no routing socket, no hardcoded ranges).
|
|
15
|
+
function hostIpFor(deviceIp: string): string | null {
|
|
16
|
+
let d = ipToInt(deviceIp)
|
|
17
|
+
for (let addrs of Object.values(networkInterfaces())) {
|
|
18
|
+
for (let a of addrs ?? []) {
|
|
19
|
+
if (a.family === "IPv4" && !a.internal && (ipToInt(a.address) & ipToInt(a.netmask)) === (d & ipToInt(a.netmask))) {
|
|
20
|
+
return a.address
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The device's own IPv4 on its primary network: from the serial for wireless adb
|
|
28
|
+
// (ip:port), else queried over adb. `ip route get` reports the source IP of the
|
|
29
|
+
// route to a public address, i.e. the device's main interface IP.
|
|
30
|
+
function deviceIp(adb: string, target: string): string | null {
|
|
31
|
+
let m = target.match(/^(\d+\.\d+\.\d+\.\d+):\d+$/)
|
|
32
|
+
if (m) return m[1] ?? null
|
|
33
|
+
let res = Bun.spawnSync([adb, "-s", target, "shell", "ip", "route", "get", "1.1.1.1"], {
|
|
18
34
|
stdout: "pipe",
|
|
19
35
|
stderr: "pipe",
|
|
20
36
|
})
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
37
|
+
return res.stdout.toString().match(/src (\d+\.\d+\.\d+\.\d+)/)?.[1] ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The host:port the client on `target` should dial to reach this machine's dev
|
|
41
|
+
// server, by adb transport: the emulator reaches the host through its NAT alias
|
|
42
|
+
// 10.0.2.2; every other transport shares a LAN with the host, so match the
|
|
43
|
+
// device's IP to the host interface on its subnet. Replaces the old adb-reverse
|
|
44
|
+
// loopback tunnel, which never worked over wireless adb. Returns null when the
|
|
45
|
+
// address cannot be resolved (the client then falls back to QR/recents).
|
|
46
|
+
function devServerAddress(adb: string, target: string): string | null {
|
|
47
|
+
if (target.startsWith("emulator-")) return `10.0.2.2:${DEV_PORT}`
|
|
48
|
+
let dip = deviceIp(adb, target)
|
|
49
|
+
if (!dip) return null
|
|
50
|
+
let host = hostIpFor(dip)
|
|
51
|
+
return host ? `${host}:${DEV_PORT}` : null
|
|
24
52
|
}
|
|
25
53
|
|
|
26
54
|
// Serials of connected, authorized devices (excludes offline/unauthorized).
|
|
@@ -70,22 +98,37 @@ function resolveTarget(adb: string): string {
|
|
|
70
98
|
return only
|
|
71
99
|
}
|
|
72
100
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
101
|
+
// Primary ABI of the connected device (e.g. "arm64-v8a", "armeabi-v7a"), used
|
|
102
|
+
// to pick the matching APK -- a fat (multi-ABI) APK reports its 64-bit ABI
|
|
103
|
+
// here and still installs fine, so this only matters for single-ABI builds.
|
|
104
|
+
function deviceAbi(adb: string, target: string): string {
|
|
105
|
+
let res = Bun.spawnSync([adb, "-s", target, "shell", "getprop", "ro.product.cpu.abi"], {
|
|
106
|
+
stdout: "pipe",
|
|
107
|
+
stderr: "pipe",
|
|
108
|
+
})
|
|
109
|
+
return res.stdout.toString().trim()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Install + launch the Android client on a connected device over adb, passing it
|
|
113
|
+
// the dev-server address to dial as a launch-intent extra (see devServerAddress).
|
|
114
|
+
// Fire-and-forget: the client's lifecycle is tracked via WS connect/disconnect in
|
|
115
|
+
// dev-server.ts, not as a child process here.
|
|
77
116
|
export async function spawnAndroidClient() {
|
|
78
117
|
let adb = requireAdb()
|
|
79
118
|
|
|
80
|
-
let
|
|
119
|
+
let target = resolveTarget(adb)
|
|
120
|
+
let abi = deviceAbi(adb, target)
|
|
121
|
+
|
|
122
|
+
let apk = resolveApk(abi)
|
|
81
123
|
if (!apk) {
|
|
82
|
-
console.error(
|
|
83
|
-
|
|
124
|
+
console.error(`Could not find a SolidRT-Go APK for ABI "${abi}".`)
|
|
125
|
+
let pkg = ANDROID_PKG_MAP[abi]
|
|
126
|
+
if (pkg) console.error(`Add it with: bun add -d ${pkg}`)
|
|
127
|
+
let target = abi === "armeabi-v7a" ? "make dist-android-armeabi-v7a" : "make dist-android"
|
|
128
|
+
console.error(`Or build one locally (from lattice/): ${target}`)
|
|
84
129
|
process.exit(1)
|
|
85
130
|
}
|
|
86
131
|
|
|
87
|
-
let target = resolveTarget(adb)
|
|
88
|
-
|
|
89
132
|
print(`[cli] Installing SolidRT-Go on ${target}`)
|
|
90
133
|
let install = Bun.spawn([adb, "-s", target, "install", "-r", apk], { stdout: "pipe", stderr: "pipe" })
|
|
91
134
|
if ((await install.exited) !== 0) {
|
|
@@ -93,12 +136,19 @@ export async function spawnAndroidClient() {
|
|
|
93
136
|
process.exit(1)
|
|
94
137
|
}
|
|
95
138
|
|
|
96
|
-
|
|
139
|
+
// Hand the client the dev-server address to dial, as a launch-intent extra that
|
|
140
|
+
// MainActivity forwards to native argv (--dev-server); the client auto-connects
|
|
141
|
+
// to it. Replaces adb reverse, which never worked over wireless adb.
|
|
142
|
+
let devServer = devServerAddress(adb, target)
|
|
143
|
+
let launchArgs = [adb, "-s", target, "shell", "am", "start", "-n", PACKAGE_ACTIVITY]
|
|
144
|
+
if (devServer) {
|
|
145
|
+
print(`[cli] Client will dial dev server at ${devServer}`)
|
|
146
|
+
launchArgs.push("--es", "srt_dev_server", devServer)
|
|
147
|
+
} else {
|
|
148
|
+
print("[cli] Could not resolve a host address for the device; client will need a manual/QR connect")
|
|
149
|
+
}
|
|
97
150
|
|
|
98
|
-
let start = Bun.spawn(
|
|
99
|
-
stdout: "pipe",
|
|
100
|
-
stderr: "pipe",
|
|
101
|
-
})
|
|
151
|
+
let start = Bun.spawn(launchArgs, { stdout: "pipe", stderr: "pipe" })
|
|
102
152
|
if ((await start.exited) !== 0) {
|
|
103
153
|
console.error("adb start failed:\n" + (await new Response(start.stderr).text()))
|
|
104
154
|
process.exit(1)
|
package/src/dev-server.ts
CHANGED
|
@@ -8,16 +8,17 @@ import { values } from "./args"
|
|
|
8
8
|
import * as cache from "./cache"
|
|
9
9
|
|
|
10
10
|
export const DEV_HOST = "127.0.0.1"
|
|
11
|
-
export const DEV_PORT =
|
|
11
|
+
export const DEV_PORT = 0x8844
|
|
12
12
|
|
|
13
|
-
// Dev-server WS protocol helpers: the reload message shape and
|
|
13
|
+
// Dev-server WS protocol helpers: the reload message shape and a broadcast to all clients.
|
|
14
14
|
export function buildReload(payload: { code?: string | null; bytecode?: string }) {
|
|
15
15
|
return JSON.stringify({ type: "reload", proxyFiles: values["proxy-files"], proxyHttp: values["proxy-http"], ...payload })
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function
|
|
18
|
+
export function broadcast(msg: object) {
|
|
19
|
+
let text = JSON.stringify(msg)
|
|
19
20
|
for (let ws of state.clients.keys()) {
|
|
20
|
-
ws.send(
|
|
21
|
+
ws.send(text)
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -157,7 +158,7 @@ export function startServer() {
|
|
|
157
158
|
print(`[cli] Client connected ${ws.remoteAddress}`)
|
|
158
159
|
// Advertise our real LAN address so clients dialed over the adb loopback
|
|
159
160
|
// can show/remember the directly reachable address (see connection.rs).
|
|
160
|
-
ws.send(JSON.stringify({ type: "welcome", address: state.serverUrl, stats:
|
|
161
|
+
ws.send(JSON.stringify({ type: "welcome", address: state.serverUrl, stats: state.stats }))
|
|
161
162
|
if (state.currentCode) {
|
|
162
163
|
ws.send(buildReload({ code: state.currentCode }))
|
|
163
164
|
}
|
|
@@ -245,4 +246,4 @@ export function startServer() {
|
|
|
245
246
|
ws.ping()
|
|
246
247
|
}
|
|
247
248
|
}, 5000)
|
|
248
|
-
}
|
|
249
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { values, command, validateArgs, printUsage } from "./args"
|
|
4
|
+
import { runInitCommand } from "./commands/init"
|
|
4
5
|
import { runBundleCommand } from "./commands/bundle"
|
|
5
6
|
import { runPackCommand } from "./commands/pack"
|
|
6
7
|
import { runRecordCommand } from "./commands/record"
|
|
@@ -35,7 +36,9 @@ if (isProdBuild && process.env.NODE_ENV !== "production") {
|
|
|
35
36
|
|
|
36
37
|
// -- Dispatch --
|
|
37
38
|
|
|
38
|
-
if (command === "
|
|
39
|
+
if (command === "init") {
|
|
40
|
+
await runInitCommand()
|
|
41
|
+
} else if (command === "bundle") {
|
|
39
42
|
await runBundleCommand()
|
|
40
43
|
} else if (command === "pack") {
|
|
41
44
|
await runPackCommand()
|
package/src/repl.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createInterface } from "node:readline"
|
|
|
2
2
|
import { resolve, dirname } from "path"
|
|
3
3
|
import { readdirSync } from "node:fs"
|
|
4
4
|
import { state, print, printErr, shutdown } from "./util"
|
|
5
|
-
import { buildReload,
|
|
5
|
+
import { buildReload, broadcast } from "./dev-server"
|
|
6
6
|
import { bundle } from "./bundler"
|
|
7
7
|
import { startWatcher, stopWatcher } from "./watcher"
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ function cmdStop(args: string) {
|
|
|
11
11
|
stopWatcher()
|
|
12
12
|
state.currentCode = null
|
|
13
13
|
state.source = undefined
|
|
14
|
-
|
|
14
|
+
broadcast({ type: "stop" })
|
|
15
15
|
print("[cli] Sent stop to all clients")
|
|
16
16
|
return
|
|
17
17
|
}
|
|
@@ -56,6 +56,21 @@ async function cmdReload(args: string) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function cmdStats(args: string) {
|
|
60
|
+
if (args === "on") {
|
|
61
|
+
state.stats = true
|
|
62
|
+
} else if (args === "off") {
|
|
63
|
+
state.stats = false
|
|
64
|
+
} else if (!args) {
|
|
65
|
+
state.stats = !state.stats
|
|
66
|
+
} else {
|
|
67
|
+
print("Usage: stats [on|off]")
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
broadcast({ type: "stats", stats: state.stats })
|
|
71
|
+
print(`[cli] Stats overlay ${state.stats ? "on" : "off"}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
59
74
|
function cmdList() {
|
|
60
75
|
if (state.clients.size === 0) {
|
|
61
76
|
print("No connected clients")
|
|
@@ -105,7 +120,7 @@ async function cmdLoad(file: string) {
|
|
|
105
120
|
print(`[cli] Loaded ${file}`)
|
|
106
121
|
}
|
|
107
122
|
|
|
108
|
-
let COMMANDS = ["load ", "stop", "reload", "list", "quit", "exit", "help"]
|
|
123
|
+
let COMMANDS = ["load ", "stop", "reload", "list", "stats", "quit", "exit", "help"]
|
|
109
124
|
let LOAD_EXTENSIONS = [".tsx", ".srt.js", ".srt.bin"]
|
|
110
125
|
|
|
111
126
|
function completer(line: string): [string[], string] {
|
|
@@ -150,6 +165,8 @@ export function startRepl() {
|
|
|
150
165
|
cmdLoad(cmd.slice(5).trim())
|
|
151
166
|
} else if (cmd === "list") {
|
|
152
167
|
cmdList()
|
|
168
|
+
} else if (cmd === "stats" || cmd.startsWith("stats ")) {
|
|
169
|
+
cmdStats(cmd.slice(6).trim())
|
|
153
170
|
} else if (cmd === "quit" || cmd === "exit") {
|
|
154
171
|
shutdown()
|
|
155
172
|
} else if (cmd.startsWith("!")) {
|
|
@@ -165,7 +182,7 @@ export function startRepl() {
|
|
|
165
182
|
)
|
|
166
183
|
}
|
|
167
184
|
} else if (cmd === "help") {
|
|
168
|
-
print("Commands: load, stop, reload, list, !<cmd>, quit, help")
|
|
185
|
+
print("Commands: load, stop, reload, list, stats, !<cmd>, quit, help")
|
|
169
186
|
} else if (cmd) {
|
|
170
187
|
print(`Unknown command: ${cmd}`)
|
|
171
188
|
}
|
package/src/util.ts
CHANGED