@rubytech/create-maxy-lite 0.1.1 → 0.1.3
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 +47 -9
- package/lib/orchestrate.mjs +31 -6
- package/lib/paths.mjs +19 -0
- 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 } 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,19 +73,38 @@ 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 })
|
|
84
82
|
|
|
85
|
-
|
|
83
|
+
// ---- shared-storage permission ----------------------------------------------
|
|
84
|
+
// The vault lives on Android shared storage, whose parent (`/storage/emulated/0`)
|
|
85
|
+
// is unwritable until Termux holds the shared-storage permission. `termux-setup-
|
|
86
|
+
// storage` pops the Android grant dialog; the parent becomes writable once the
|
|
87
|
+
// user taps Allow. Detection is the writability of that parent, not an existence
|
|
88
|
+
// check — the directory can be present but inaccessible before the grant.
|
|
89
|
+
|
|
90
|
+
const storageParent = path.dirname(PATHS.vaultHost)
|
|
91
|
+
const storageAccessible = () => {
|
|
92
|
+
try {
|
|
93
|
+
fs.accessSync(storageParent, fs.constants.W_OK)
|
|
94
|
+
return true
|
|
95
|
+
} catch {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const interactive = Boolean(process.stdin.isTTY)
|
|
100
|
+
const STORAGE_POLL_MS = 3000
|
|
101
|
+
|
|
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.
|
|
86
105
|
const layPayload = () => {
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
const r = run(payloadLayCommand({ payloadHost: PAYLOAD }))
|
|
107
|
+
if (r.code !== 0) throw new Error(`payload lay failed: ${r.stderr.replace(/\s+/g, ' ').trim()}`)
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
/** Install the app deps (validator's js-yaml) and the webchat deps (node-pty, ws). */
|
|
@@ -168,6 +187,22 @@ async function main() {
|
|
|
168
187
|
if (timer.unref) timer.unref()
|
|
169
188
|
return () => clearInterval(timer)
|
|
170
189
|
}
|
|
190
|
+
// Drive the Android shared-storage grant: surface the dialog, then poll the vault
|
|
191
|
+
// parent's writability until the user taps Allow. No timeout — the step heartbeat
|
|
192
|
+
// keeps the wait visibly alive — so a slow grant never fails the install. Returns
|
|
193
|
+
// only once storage is writable; the caller re-runs the vault-bind mutation after.
|
|
194
|
+
const ensureStorageGrant = async () => {
|
|
195
|
+
log('storage-grant', { state: 'requesting' })
|
|
196
|
+
process.stdout.write(
|
|
197
|
+
`\n[maxy-lite] Shared storage is needed for the vault at ${PATHS.vaultHost}.\n` +
|
|
198
|
+
`[maxy-lite] An Android dialog will appear — tap "Allow", then leave this running.\n`,
|
|
199
|
+
)
|
|
200
|
+
run('termux-setup-storage')
|
|
201
|
+
while (!storageAccessible()) {
|
|
202
|
+
await new Promise((resolve) => setTimeout(resolve, STORAGE_POLL_MS))
|
|
203
|
+
}
|
|
204
|
+
log('storage-grant', { state: 'granted' })
|
|
205
|
+
}
|
|
171
206
|
const ctx = {
|
|
172
207
|
run,
|
|
173
208
|
runIn,
|
|
@@ -175,6 +210,9 @@ async function main() {
|
|
|
175
210
|
runInStream,
|
|
176
211
|
existsHost,
|
|
177
212
|
mkdirHost,
|
|
213
|
+
storageAccessible,
|
|
214
|
+
interactive,
|
|
215
|
+
ensureStorageGrant,
|
|
178
216
|
layPayload,
|
|
179
217
|
installDeps,
|
|
180
218
|
writeLauncher,
|
package/lib/orchestrate.mjs
CHANGED
|
@@ -29,10 +29,34 @@ function ensureOk(r) {
|
|
|
29
29
|
return r
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** The
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
/** The actionable error when the vault's shared-storage parent is not writable. */
|
|
33
|
+
const STORAGE_PERMISSION_ERROR = 'storage-permission-required: run termux-setup-storage and grant access'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gate the vault-bind mutation on shared-storage access. The vault parent
|
|
37
|
+
* (`/storage/emulated/0`) is unwritable until Termux has been granted Android
|
|
38
|
+
* shared-storage permission, so `c.mkdirHost(vaultHost)` would otherwise fail with
|
|
39
|
+
* an opaque `Permission denied`. When access is missing we either drive the
|
|
40
|
+
* interactive grant (TTY present), which blocks until the permission lands, or
|
|
41
|
+
* fail fast with a clear, actionable message when no TTY can surface the dialog.
|
|
42
|
+
*/
|
|
43
|
+
async function ensureStorageAccessible(c) {
|
|
44
|
+
if (c.storageAccessible()) return
|
|
45
|
+
if (!c.interactive) throw new Error(STORAGE_PERMISSION_ERROR)
|
|
46
|
+
await c.ensureStorageGrant()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* True when proot-distro can enter the distro — its own proof the rootfs is
|
|
51
|
+
* installed. A login that exits 0 means proot-distro resolved and entered the
|
|
52
|
+
* rootfs. The earlier guard reconstructed the rootfs path and `fs.existsSync`'d
|
|
53
|
+
* it, which disagreed with proot-distro on-device (present to proot-distro,
|
|
54
|
+
* absent to the path guess) and re-ran `install` into an "already exists"
|
|
55
|
+
* failure. Delegating to proot-distro's own exit code makes the guard agree
|
|
56
|
+
* with the install action by construction, so an installed distro skips.
|
|
57
|
+
*/
|
|
58
|
+
function distroInstalled(c) {
|
|
59
|
+
return c.runIn('true').code === 0
|
|
36
60
|
}
|
|
37
61
|
|
|
38
62
|
/** True when the glibc Node inside Ubuntu matches the pinned major. */
|
|
@@ -57,7 +81,7 @@ const STEPS = [
|
|
|
57
81
|
},
|
|
58
82
|
{
|
|
59
83
|
name: 'ubuntu',
|
|
60
|
-
done: (c) => c
|
|
84
|
+
done: (c) => distroInstalled(c),
|
|
61
85
|
run: async (c) => ensureOk(await c.runStream(`proot-distro install ${PATHS.distro}`)),
|
|
62
86
|
},
|
|
63
87
|
{
|
|
@@ -77,7 +101,8 @@ const STEPS = [
|
|
|
77
101
|
{
|
|
78
102
|
name: 'vault-bind',
|
|
79
103
|
done: (c) => c.existsHost(PATHS.vaultHost),
|
|
80
|
-
run: (c) => {
|
|
104
|
+
run: async (c) => {
|
|
105
|
+
await ensureStorageAccessible(c)
|
|
81
106
|
c.mkdirHost(PATHS.vaultHost)
|
|
82
107
|
ensureOk(c.run(`proot-distro login ${PATHS.distro} --bind ${PATHS.vaultHost}:${PATHS.vaultGuest} -- test -d ${PATHS.vaultGuest}`))
|
|
83
108
|
},
|
package/lib/paths.mjs
CHANGED
|
@@ -21,6 +21,25 @@ 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`
|
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.3",
|
|
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": {
|