@rubytech/create-maxy-lite 0.1.2 → 0.1.4
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/index.mjs +37 -14
- package/lib/orchestrate.mjs +6 -4
- package/lib/paths.mjs +49 -3
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
23
23
|
|
|
24
24
|
import { makeLog } from './lib/log.mjs'
|
|
25
25
|
import { readPins } from './lib/pins.mjs'
|
|
26
|
-
import { PATHS, WEBCHAT_DIR, VALIDATOR_CLI, launcherPath, launcherScript } from './lib/paths.mjs'
|
|
26
|
+
import { PATHS, WEBCHAT_DIR, VALIDATOR_CLI, launcherPath, launcherScript, payloadLayCommand, vaultBindLogin, webchatRunEnv } from './lib/paths.mjs'
|
|
27
27
|
import { orchestrate, STEP_NAMES } from './lib/orchestrate.mjs'
|
|
28
28
|
import { claudeOk, validatorOk, vaultOk, webchatOk, healthcheck } from './lib/healthcheck.mjs'
|
|
29
29
|
|
|
@@ -73,11 +73,9 @@ const runStream = (cmd) => spawnStreaming('bash', ['-lc', cmd])
|
|
|
73
73
|
const runInStream = (cmd) => spawnStreaming('proot-distro', ['login', PATHS.distro, '--', 'bash', '-lc', cmd])
|
|
74
74
|
|
|
75
75
|
// ---- filesystem primitives --------------------------------------------------
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
const rootfs = `${PREFIX}/var/lib/proot-distro/installed-rootfs/${PATHS.distro}`
|
|
80
|
-
const appHostPath = `${rootfs}${PATHS.appDir}` // appDir is absolute inside the rootfs
|
|
76
|
+
// Host-side fs is used only for genuine host paths (the Termux-prefix launcher
|
|
77
|
+
// and the shared-storage vault). The app payload lands inside the rootfs through
|
|
78
|
+
// proot (see layPayload), never via a reconstructed host rootfs path.
|
|
81
79
|
|
|
82
80
|
const existsHost = (p) => fs.existsSync(p)
|
|
83
81
|
const mkdirHost = (p) => fs.mkdirSync(p, { recursive: true })
|
|
@@ -101,20 +99,38 @@ const storageAccessible = () => {
|
|
|
101
99
|
const interactive = Boolean(process.stdin.isTTY)
|
|
102
100
|
const STORAGE_POLL_MS = 3000
|
|
103
101
|
|
|
104
|
-
/**
|
|
102
|
+
/** Lay the bundled app payload into the guest's appDir through proot (idempotent overwrite). */
|
|
103
|
+
// Delivered via a proot bind + guest-side copy so the payload lands in whatever
|
|
104
|
+
// rootfs proot-distro actually uses — no host-side guess at the rootfs location.
|
|
105
105
|
const layPayload = () => {
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const r = run(payloadLayCommand({ payloadHost: PAYLOAD }))
|
|
107
|
+
if (r.code !== 0) throw new Error(`payload lay failed: ${r.stderr.replace(/\s+/g, ' ').trim()}`)
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/** Install the app deps (validator's js-yaml) and the webchat deps (node-pty, ws). */
|
|
111
111
|
// Streamed: the webchat deps build node-pty natively, which is slow and verbose;
|
|
112
112
|
// a failure carries the child's stderr so the cause is in the log.
|
|
113
|
+
const PTY_NODE = `${WEBCHAT_DIR}/node_modules/node-pty/build/Release/pty.node`
|
|
113
114
|
const installDeps = async () => {
|
|
114
115
|
const app = await runInStream(`cd ${PATHS.appDir} && npm install --omit=dev`)
|
|
115
116
|
if (app.code !== 0) throw new Error(`app deps install failed: ${app.stderr.replace(/\s+/g, ' ').trim()}`)
|
|
116
117
|
const wc = await runInStream(`cd ${WEBCHAT_DIR} && npm install --omit=dev`)
|
|
117
118
|
if (wc.code !== 0) throw new Error(`webchat deps install failed: ${wc.stderr.replace(/\s+/g, ' ').trim()}`)
|
|
119
|
+
// node-pty ships no linux-arm64 + Node 20 prebuild, so the install above can
|
|
120
|
+
// leave pty.node unbuilt when npm has ignore-scripts set. `npm rebuild` re-runs
|
|
121
|
+
// node-pty's node-gyp build; the env var forces scripts on regardless of ambient
|
|
122
|
+
// config, and --build-from-source keeps it a source compile should a future
|
|
123
|
+
// node-pty gain a prebuild fetcher. The toolchain step guarantees python3/make/g++.
|
|
124
|
+
const rb = await runInStream(
|
|
125
|
+
`cd ${WEBCHAT_DIR} && npm_config_ignore_scripts=false npm rebuild node-pty --build-from-source --foreground-scripts`,
|
|
126
|
+
)
|
|
127
|
+
if (rb.code !== 0) throw new Error(`node-pty native build failed: ${rb.stderr.replace(/\s+/g, ' ').trim()}`)
|
|
128
|
+
// Prove the built module actually loads under the guest's Node — this is exactly
|
|
129
|
+
// what the relay does at launch, so a pass here means no `Failed to load native
|
|
130
|
+
// module: pty.node` crash. A bare file-presence test would miss an unloadable
|
|
131
|
+
// binary. Fail at install time, not at relay launch.
|
|
132
|
+
if (runIn(`cd ${WEBCHAT_DIR} && node -e 'require("node-pty")'`).code !== 0)
|
|
133
|
+
throw new Error(`node-pty native module did not load after build: ${PTY_NODE}`)
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
/** Write the `maxy-lite` launcher into the Termux bin dir. */
|
|
@@ -140,16 +156,20 @@ const readVersions = () => ({
|
|
|
140
156
|
const runHealthcheck = () =>
|
|
141
157
|
healthcheck({
|
|
142
158
|
checkClaude: () => claudeOk(() => runIn('claude --version')),
|
|
143
|
-
// Start the relay
|
|
159
|
+
// Start the relay under the launcher's exact runtime condition — the vault
|
|
160
|
+
// bound in and the launcher env set — then poll its port for up to 10s, record
|
|
161
|
+
// whether it bound, and kill it. Run from bare Termux (`run`) because
|
|
162
|
+
// vaultBindLogin already enters the distro; `runIn` would nest a second login
|
|
163
|
+
// with no bind and test the wrong process.
|
|
144
164
|
checkWebchat: () =>
|
|
145
165
|
webchatOk(async () => {
|
|
146
|
-
const
|
|
147
|
-
`cd ${WEBCHAT_DIR} && node server.mjs & SVPID=$!; ` +
|
|
166
|
+
const inner =
|
|
167
|
+
`cd ${WEBCHAT_DIR} && ${webchatRunEnv()} node server.mjs & SVPID=$!; ` +
|
|
148
168
|
`for i in $(seq 1 10); do sleep 1; ` +
|
|
149
169
|
`curl -sf -o /dev/null http://localhost:${PATHS.webchatPort} && break; done; ` +
|
|
150
170
|
`curl -sf -o /dev/null http://localhost:${PATHS.webchatPort}; RC=$?; ` +
|
|
151
171
|
`kill $SVPID 2>/dev/null; exit $RC`
|
|
152
|
-
return
|
|
172
|
+
return run(vaultBindLogin(inner)).code === 0
|
|
153
173
|
}),
|
|
154
174
|
// Validator must exit 0 on an empty vault.
|
|
155
175
|
checkValidator: () => {
|
|
@@ -157,7 +177,10 @@ const runHealthcheck = () =>
|
|
|
157
177
|
runIn(`mkdir -p ${empty}`)
|
|
158
178
|
return validatorOk((vault) => runIn(`node ${VALIDATOR_CLI} ${vault}`).code, empty)
|
|
159
179
|
},
|
|
160
|
-
|
|
180
|
+
// The vault path only exists inside the distro when the login carries the
|
|
181
|
+
// launcher's `--bind`. Reproduce that bind, then check the guest path — exactly
|
|
182
|
+
// the launcher condition, not a session where the vault was never mounted.
|
|
183
|
+
checkVault: () => vaultOk(() => run(vaultBindLogin(`test -d ${PATHS.vaultGuest}`)).code === 0),
|
|
161
184
|
})
|
|
162
185
|
|
|
163
186
|
// ---- dry run ----------------------------------------------------------------
|
package/lib/orchestrate.mjs
CHANGED
|
@@ -109,11 +109,13 @@ const STEPS = [
|
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
name: 'npm-app',
|
|
112
|
-
// Converged only when
|
|
113
|
-
// is written
|
|
114
|
-
//
|
|
112
|
+
// Converged only when node-pty's native module is built, claude is present,
|
|
113
|
+
// AND the launcher is written. The deps probe is the built `pty.node`, not the
|
|
114
|
+
// node_modules dir: node-pty ships no linux-arm64 prebuild, so a dir-present
|
|
115
|
+
// install can still lack the native module and crash the relay at launch.
|
|
116
|
+
// Guarding on pty.node makes a re-run rebuild it instead of skipping ok=true.
|
|
115
117
|
done: (c) =>
|
|
116
|
-
c.runIn(`test -
|
|
118
|
+
c.runIn(`test -f ${WEBCHAT_DIR}/node_modules/node-pty/build/Release/pty.node`).code === 0 &&
|
|
117
119
|
c.runIn('command -v claude').code === 0 &&
|
|
118
120
|
c.existsHost(launcherPath()),
|
|
119
121
|
run: async (c) => {
|
package/lib/paths.mjs
CHANGED
|
@@ -21,15 +21,61 @@ export const PATHS = {
|
|
|
21
21
|
export const WEBCHAT_DIR = `${PATHS.appDir}/webchat`
|
|
22
22
|
export const VALIDATOR_CLI = `${PATHS.appDir}/validator/cli.mjs`
|
|
23
23
|
|
|
24
|
+
// Where the host payload is bind-mounted inside the guest while it is copied into
|
|
25
|
+
// place. A throwaway path under the rootfs's own /tmp, distinct from the app dir.
|
|
26
|
+
export const PAYLOAD_STAGE = '/tmp/maxy-lite-payload'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The bare-Termux command that lays the bundled app payload into the guest's
|
|
30
|
+
* `appDir`. It binds the host payload directory into the guest under `stage`,
|
|
31
|
+
* then copies it guest-side — so the payload always lands in whatever rootfs
|
|
32
|
+
* proot-distro actually uses, with no host-side guess at the rootfs location.
|
|
33
|
+
* `payloadHost` is the only environment-dependent path (the npm package's
|
|
34
|
+
* `payload/`), so it is single-quoted; `stage` and `appDir` are pinned constants.
|
|
35
|
+
*/
|
|
36
|
+
export function payloadLayCommand({ payloadHost, distro = PATHS.distro, stage = PAYLOAD_STAGE, appDir = PATHS.appDir }) {
|
|
37
|
+
return (
|
|
38
|
+
`proot-distro login ${distro} --bind '${payloadHost}':${stage} -- ` +
|
|
39
|
+
`bash -lc 'mkdir -p ${appDir} && cp -a ${stage}/. ${appDir}/'`
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
24
43
|
/** Absolute path of the `maxy-lite` launcher in the Termux bin dir. */
|
|
25
44
|
export function launcherPath(prefix = process.env.PREFIX || '/data/data/com.termux/files/usr') {
|
|
26
45
|
return `${prefix}/bin/maxy-lite`
|
|
27
46
|
}
|
|
28
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The env prefix the webchat relay needs to serve the bound vault on the pinned
|
|
50
|
+
* port. Shared by the launcher and the webchat health-check probe so the two
|
|
51
|
+
* cannot drift: the probe must start the relay with exactly the env production
|
|
52
|
+
* uses, or it tests a different process than the one the launcher runs.
|
|
53
|
+
*/
|
|
54
|
+
export function webchatRunEnv({ vaultGuest = PATHS.vaultGuest, port = PATHS.webchatPort } = {}) {
|
|
55
|
+
return `LITE_AGENT_HOME=${vaultGuest} LITE_PORT=${port}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A proot-distro login into the distro with the shared-storage vault bound in,
|
|
60
|
+
* running `inner` under `bash -lc`. This is the single construction of the
|
|
61
|
+
* launcher's runtime condition (the `--bind` that makes `${vaultGuest}` exist);
|
|
62
|
+
* the launcher and the vault/webchat probes all build on it so a probe can never
|
|
63
|
+
* assert a post-condition under different conditions than production.
|
|
64
|
+
* Run from bare Termux (not nested inside another `proot-distro login`).
|
|
65
|
+
*/
|
|
66
|
+
export function vaultBindLogin(
|
|
67
|
+
inner,
|
|
68
|
+
{ distro = PATHS.distro, vaultHost = PATHS.vaultHost, vaultGuest = PATHS.vaultGuest } = {},
|
|
69
|
+
) {
|
|
70
|
+
return `proot-distro login ${distro} --bind ${vaultHost}:${vaultGuest} -- bash -lc '${inner}'`
|
|
71
|
+
}
|
|
72
|
+
|
|
29
73
|
/**
|
|
30
74
|
* The `maxy-lite` launcher script (written into the Termux bin dir). Run from the
|
|
31
75
|
* Termux shell, it enters Ubuntu with the vault bind-mount and starts the webchat
|
|
32
|
-
* relay on localhost.
|
|
76
|
+
* relay on localhost. Built from the shared bind + env helpers so the launcher
|
|
77
|
+
* and the health-check probes stay in lockstep. Documented Termux:Boot opt-in
|
|
78
|
+
* re-runs this on boot.
|
|
33
79
|
*/
|
|
34
80
|
export function launcherScript({
|
|
35
81
|
prefix = process.env.PREFIX || '/data/data/com.termux/files/usr',
|
|
@@ -39,12 +85,12 @@ export function launcherScript({
|
|
|
39
85
|
port = PATHS.webchatPort,
|
|
40
86
|
distro = PATHS.distro,
|
|
41
87
|
} = {}) {
|
|
88
|
+
const inner = `cd ${webchatDir} && ${webchatRunEnv({ vaultGuest, port })} exec node server.mjs`
|
|
42
89
|
return `#!${prefix}/bin/bash
|
|
43
90
|
# maxy-lite launcher — starts the on-device web chat relay.
|
|
44
91
|
# Enters the glibc Ubuntu layer with the shared-storage vault bound in, then runs
|
|
45
92
|
# the relay on http://localhost:${port}. Open that URL in the phone browser.
|
|
46
93
|
set -e
|
|
47
|
-
exec
|
|
48
|
-
bash -lc 'cd ${webchatDir} && LITE_AGENT_HOME=${vaultGuest} LITE_PORT=${port} exec node server.mjs'
|
|
94
|
+
exec ${vaultBindLogin(inner, { distro, vaultHost, vaultGuest })}
|
|
49
95
|
`
|
|
50
96
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rubytech/create-maxy-lite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Install maxy-lite on an Android phone: orchestrates proot-distro Ubuntu, glibc Node, claude, the web-chat relay, the vault and its bind-mount — run via npx in bare Termux.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|