@rubytech/create-maxy-lite 0.1.0 → 0.1.1
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 +54 -7
- package/lib/orchestrate.mjs +39 -20
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import fs from 'node:fs'
|
|
20
20
|
import path from 'node:path'
|
|
21
|
-
import { spawnSync } from 'node:child_process'
|
|
21
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
22
22
|
import { fileURLToPath } from 'node:url'
|
|
23
23
|
|
|
24
24
|
import { makeLog } from './lib/log.mjs'
|
|
@@ -34,15 +34,44 @@ const PREFIX = process.env.PREFIX || '/data/data/com.termux/files/usr'
|
|
|
34
34
|
|
|
35
35
|
// ---- subprocess primitives --------------------------------------------------
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// stderr is kept (not just stdout) so a failed command's cause is recoverable
|
|
38
|
+
// from the install log without re-running by hand.
|
|
39
|
+
const decode = (r) => ({ code: r.status === null ? 1 : r.status, stdout: r.stdout || '', stderr: r.stderr || '' })
|
|
38
40
|
|
|
39
|
-
/** Run a command in bare Termux. */
|
|
41
|
+
/** Run a command in bare Termux (captured, for guards and quick steps). */
|
|
40
42
|
const run = (cmd) => decode(spawnSync('bash', ['-lc', cmd], { encoding: 'utf8' }))
|
|
41
43
|
|
|
42
|
-
/** Run a command inside the glibc Ubuntu layer. */
|
|
44
|
+
/** Run a command inside the glibc Ubuntu layer (captured). */
|
|
43
45
|
const runIn = (cmd) =>
|
|
44
46
|
decode(spawnSync('proot-distro', ['login', PATHS.distro, '--', 'bash', '-lc', cmd], { encoding: 'utf8' }))
|
|
45
47
|
|
|
48
|
+
// Streaming variants for the long, network-bound steps: tee the child's stdout
|
|
49
|
+
// and stderr to the terminal as they arrive (so the operator sees apt/proot/npm
|
|
50
|
+
// progress live), while capturing a bounded slice of stderr for the failure
|
|
51
|
+
// message. Returns the same { code, stdout, stderr } shape, stdout left empty
|
|
52
|
+
// (these commands are run for effect, never parsed).
|
|
53
|
+
const STDERR_CAP = 8192
|
|
54
|
+
|
|
55
|
+
function spawnStreaming(file, args) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const child = spawn(file, args, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
58
|
+
let stderr = ''
|
|
59
|
+
child.stdout.on('data', (d) => process.stdout.write(d))
|
|
60
|
+
child.stderr.on('data', (d) => {
|
|
61
|
+
process.stderr.write(d)
|
|
62
|
+
if (stderr.length < STDERR_CAP) stderr += d.toString()
|
|
63
|
+
})
|
|
64
|
+
child.on('error', (e) => resolve({ code: 1, stdout: '', stderr: String(e && e.message ? e.message : e) }))
|
|
65
|
+
child.on('close', (code) => resolve({ code: code === null ? 1 : code, stdout: '', stderr: stderr.slice(0, STDERR_CAP) }))
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Stream a command in bare Termux. */
|
|
70
|
+
const runStream = (cmd) => spawnStreaming('bash', ['-lc', cmd])
|
|
71
|
+
|
|
72
|
+
/** Stream a command inside the glibc Ubuntu layer. */
|
|
73
|
+
const runInStream = (cmd) => spawnStreaming('proot-distro', ['login', PATHS.distro, '--', 'bash', '-lc', cmd])
|
|
74
|
+
|
|
46
75
|
// ---- filesystem primitives --------------------------------------------------
|
|
47
76
|
// The Ubuntu rootfs is a normal directory under the Termux prefix, so the app
|
|
48
77
|
// payload is copied into it directly from bionic Node (no proot round-trip).
|
|
@@ -60,9 +89,13 @@ const layPayload = () => {
|
|
|
60
89
|
}
|
|
61
90
|
|
|
62
91
|
/** Install the app deps (validator's js-yaml) and the webchat deps (node-pty, ws). */
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
// Streamed: the webchat deps build node-pty natively, which is slow and verbose;
|
|
93
|
+
// a failure carries the child's stderr so the cause is in the log.
|
|
94
|
+
const installDeps = async () => {
|
|
95
|
+
const app = await runInStream(`cd ${PATHS.appDir} && npm install --omit=dev`)
|
|
96
|
+
if (app.code !== 0) throw new Error(`app deps install failed: ${app.stderr.replace(/\s+/g, ' ').trim()}`)
|
|
97
|
+
const wc = await runInStream(`cd ${WEBCHAT_DIR} && npm install --omit=dev`)
|
|
98
|
+
if (wc.code !== 0) throw new Error(`webchat deps install failed: ${wc.stderr.replace(/\s+/g, ' ').trim()}`)
|
|
66
99
|
}
|
|
67
100
|
|
|
68
101
|
/** Write the `maxy-lite` launcher into the Termux bin dir. */
|
|
@@ -118,15 +151,28 @@ function printDryRun(log) {
|
|
|
118
151
|
|
|
119
152
|
// ---- entry ------------------------------------------------------------------
|
|
120
153
|
|
|
154
|
+
// A long step emits a liveness heartbeat at least this often, so no silent gap
|
|
155
|
+
// between log lines reads as a hang.
|
|
156
|
+
const HEARTBEAT_MS = 10000
|
|
157
|
+
|
|
121
158
|
async function main() {
|
|
122
159
|
const log = makeLog()
|
|
123
160
|
if (process.argv.includes('--dry-run')) {
|
|
124
161
|
printDryRun(log)
|
|
125
162
|
return 0
|
|
126
163
|
}
|
|
164
|
+
// Periodic liveness for a running step: emit `op=step-progress` every
|
|
165
|
+
// HEARTBEAT_MS until stopped. unref so a stray timer never holds the process open.
|
|
166
|
+
const heartbeat = (name, startMs) => {
|
|
167
|
+
const timer = setInterval(() => log('step-progress', { name, elapsedMs: Date.now() - startMs }), HEARTBEAT_MS)
|
|
168
|
+
if (timer.unref) timer.unref()
|
|
169
|
+
return () => clearInterval(timer)
|
|
170
|
+
}
|
|
127
171
|
const ctx = {
|
|
128
172
|
run,
|
|
129
173
|
runIn,
|
|
174
|
+
runStream,
|
|
175
|
+
runInStream,
|
|
130
176
|
existsHost,
|
|
131
177
|
mkdirHost,
|
|
132
178
|
layPayload,
|
|
@@ -137,6 +183,7 @@ async function main() {
|
|
|
137
183
|
pins: PINS,
|
|
138
184
|
now: () => Date.now(),
|
|
139
185
|
log,
|
|
186
|
+
heartbeat,
|
|
140
187
|
}
|
|
141
188
|
const result = await orchestrate(ctx)
|
|
142
189
|
return result.ok ? 0 : 1
|
package/lib/orchestrate.mjs
CHANGED
|
@@ -19,9 +19,13 @@ import { PATHS, WEBCHAT_DIR, launcherPath } from './paths.mjs'
|
|
|
19
19
|
|
|
20
20
|
export const STEP_NAMES = ['termux-deps', 'proot', 'ubuntu', 'node', 'toolchain', 'vault-bind', 'npm-app']
|
|
21
21
|
|
|
22
|
-
/** Throw unless the command result is a clean exit. */
|
|
22
|
+
/** Throw unless the command result is a clean exit; the message carries the command's stderr. */
|
|
23
23
|
function ensureOk(r) {
|
|
24
|
-
if (!r || r.code !== 0)
|
|
24
|
+
if (!r || r.code !== 0) {
|
|
25
|
+
const stderr = r && r.stderr ? String(r.stderr).replace(/\s+/g, ' ').trim() : ''
|
|
26
|
+
const detail = stderr ? `: ${stderr}` : ''
|
|
27
|
+
throw new Error(`command failed (code ${r ? r.code : 'none'})${detail}`)
|
|
28
|
+
}
|
|
25
29
|
return r
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -43,7 +47,8 @@ const STEPS = [
|
|
|
43
47
|
{
|
|
44
48
|
name: 'termux-deps',
|
|
45
49
|
done: (c) => c.run('command -v proot-distro').code === 0,
|
|
46
|
-
|
|
50
|
+
// Long and network-bound (repo sync + download); stream so the operator sees apt progress.
|
|
51
|
+
run: async (c) => ensureOk(await c.runStream('pkg install -y proot-distro')),
|
|
47
52
|
},
|
|
48
53
|
{
|
|
49
54
|
name: 'proot',
|
|
@@ -53,21 +58,21 @@ const STEPS = [
|
|
|
53
58
|
{
|
|
54
59
|
name: 'ubuntu',
|
|
55
60
|
done: (c) => c.existsHost(rootfsPath()),
|
|
56
|
-
run: (c) => ensureOk(c.
|
|
61
|
+
run: async (c) => ensureOk(await c.runStream(`proot-distro install ${PATHS.distro}`)),
|
|
57
62
|
},
|
|
58
63
|
{
|
|
59
64
|
name: 'node',
|
|
60
65
|
done: (c) => nodeMajorOk(c),
|
|
61
|
-
run: (c) => {
|
|
62
|
-
//
|
|
63
|
-
ensureOk(c.
|
|
64
|
-
ensureOk(c.
|
|
66
|
+
run: async (c) => {
|
|
67
|
+
// runInStream already wraps in `bash -lc`; pass the pipeline directly (no inner bash).
|
|
68
|
+
ensureOk(await c.runInStream(`curl -fsSL https://deb.nodesource.com/setup_${c.pins.nodeMajor}.x | bash -`))
|
|
69
|
+
ensureOk(await c.runInStream('apt install -y nodejs'))
|
|
65
70
|
},
|
|
66
71
|
},
|
|
67
72
|
{
|
|
68
73
|
name: 'toolchain',
|
|
69
74
|
done: (c) => c.runIn('command -v g++').code === 0,
|
|
70
|
-
run: (c) => ensureOk(c.
|
|
75
|
+
run: async (c) => ensureOk(await c.runInStream('apt install -y python3 make g++')),
|
|
71
76
|
},
|
|
72
77
|
{
|
|
73
78
|
name: 'vault-bind',
|
|
@@ -86,39 +91,53 @@ const STEPS = [
|
|
|
86
91
|
c.runIn(`test -d ${WEBCHAT_DIR}/node_modules`).code === 0 &&
|
|
87
92
|
c.runIn('command -v claude').code === 0 &&
|
|
88
93
|
c.existsHost(launcherPath()),
|
|
89
|
-
run: (c) => {
|
|
90
|
-
ensureOk(c.
|
|
94
|
+
run: async (c) => {
|
|
95
|
+
ensureOk(await c.runInStream(`npm install -g @anthropic-ai/claude-code@${c.pins.claudeCode}`))
|
|
91
96
|
// Pin ttyd to the manifest version via its release binary; apt would float.
|
|
92
97
|
ensureOk(
|
|
93
|
-
c.
|
|
98
|
+
await c.runInStream(
|
|
94
99
|
`curl -fsSL -o /usr/local/bin/ttyd https://github.com/tsl0922/ttyd/releases/download/${c.pins.ttyd}/ttyd.aarch64 && chmod +x /usr/local/bin/ttyd`,
|
|
95
100
|
),
|
|
96
101
|
)
|
|
97
102
|
c.layPayload()
|
|
98
|
-
c.installDeps()
|
|
103
|
+
await c.installDeps()
|
|
99
104
|
c.writeLauncher()
|
|
100
105
|
},
|
|
101
106
|
},
|
|
102
107
|
]
|
|
103
108
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Run one step: announce, guard, then action — timed, one structured line. Never
|
|
111
|
+
* throws. Emits `op=step-start` before any work so the operator always sees what
|
|
112
|
+
* is running; a failed step's `op=step` line carries `err=<message>` (the command's
|
|
113
|
+
* stderr, via ensureOk) so the cause is diagnosable from the log alone. While the
|
|
114
|
+
* action runs, a heartbeat (injected) keeps a long step from looking hung.
|
|
115
|
+
*/
|
|
116
|
+
async function runStep(step, ctx) {
|
|
106
117
|
const start = ctx.now()
|
|
118
|
+
ctx.log('step-start', { name: step.name })
|
|
107
119
|
let ok = false
|
|
108
120
|
let skipped = false
|
|
121
|
+
let errMsg = null
|
|
122
|
+
let stopHeartbeat = null
|
|
109
123
|
try {
|
|
110
124
|
if (step.done(ctx)) {
|
|
111
125
|
skipped = true
|
|
112
126
|
ok = true
|
|
113
127
|
} else {
|
|
114
|
-
|
|
128
|
+
if (ctx.heartbeat) stopHeartbeat = ctx.heartbeat(step.name, start)
|
|
129
|
+
await step.run(ctx)
|
|
115
130
|
ok = true
|
|
116
131
|
}
|
|
117
|
-
} catch {
|
|
118
|
-
|
|
132
|
+
} catch (err) {
|
|
133
|
+
errMsg = err && err.message ? err.message : String(err)
|
|
134
|
+
} finally {
|
|
135
|
+
if (stopHeartbeat) stopHeartbeat()
|
|
119
136
|
}
|
|
120
137
|
const ms = ctx.now() - start
|
|
121
|
-
|
|
138
|
+
const fields = { name: step.name, ok, ms }
|
|
139
|
+
if (!ok && errMsg) fields.err = errMsg.replace(/\s+/g, ' ').trim().slice(0, 300)
|
|
140
|
+
ctx.log('step', fields)
|
|
122
141
|
return { name: step.name, ok, skipped, ms }
|
|
123
142
|
}
|
|
124
143
|
|
|
@@ -131,7 +150,7 @@ export async function orchestrate(ctx) {
|
|
|
131
150
|
const t0 = ctx.now()
|
|
132
151
|
const results = []
|
|
133
152
|
for (const step of STEPS) {
|
|
134
|
-
const r = runStep(step, ctx)
|
|
153
|
+
const r = await runStep(step, ctx)
|
|
135
154
|
results.push(r)
|
|
136
155
|
if (!r.ok) {
|
|
137
156
|
ctx.log('done', { ok: false, ranMs: ctx.now() - t0 })
|
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.1",
|
|
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": {
|