@rubytech/create-maxy-lite 0.1.0

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/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @rubytech/create-maxy-lite
2
+
3
+ The maxy-lite installer: one command that puts the `claude` code binary on an Android phone, controlled from the phone browser. Run it in bare Termux; it orchestrates the whole on-device stack.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ # In Termux (from F-Droid):
9
+ pkg install -y nodejs
10
+ npx @rubytech/create-maxy-lite
11
+ ```
12
+
13
+ The single `npx` does everything, idempotently:
14
+
15
+ 1. `termux-deps` — Termux packages (`proot-distro`) for the glibc layer
16
+ 2. `proot` — verify the proot-distro framework is operational
17
+ 3. `ubuntu` — install the glibc Ubuntu rootfs under proot
18
+ 4. `node` — pinned glibc Node inside Ubuntu (the bionic Node cannot run `claude`)
19
+ 5. `toolchain` — `python3`/`make`/`g++` so `node-pty` compiles
20
+ 6. `vault-bind` — the shared-storage vault plus the proot bind so Obsidian and `claude` share one folder
21
+ 7. `npm-app` — pinned `@anthropic-ai/claude-code` and `ttyd`, the app (validator + web-chat relay), and the `maxy-lite` launcher
22
+
23
+ Re-running reconciles to the same state without duplicating anything. `--dry-run` prints the plan and pinned versions without touching the device.
24
+
25
+ ## Why a Node-only installer works in bare Termux
26
+
27
+ `claude` ships a glibc-only native binary, so the *agent* must run inside a proot-distro Ubuntu layer. But the *installer* is plain Node and runs fine on Termux's bionic Node, so there is no hosted shell script to maintain. A `curl … | bash` bootstrap exists only as a documented alternative (it just installs the Node prerequisite and calls this same package).
28
+
29
+ ## Layout
30
+
31
+ | Path | What |
32
+ |---|---|
33
+ | `index.mjs` | Entry: wires the real subprocess/fs/probe primitives and runs the orchestrator. |
34
+ | `lib/orchestrate.mjs` | The guarded, idempotent step sequence. |
35
+ | `lib/healthcheck.mjs` | The four post-install probes (claude, webchat, validator, vault). |
36
+ | `lib/pins.mjs`, `versions.json` | Pinned Node major, claude-code and ttyd versions. |
37
+ | `lib/paths.mjs` | On-device paths and the `maxy-lite` launcher. |
38
+ | `payload/` | Bundled app components (schema, validator, webchat), generated by `npm run bundle`. |
39
+
40
+ The `payload/` directory is gitignored and rebuilt at publish time from the sibling `maxy-lite` component tree; edit the sources there, never the payload.
41
+
42
+ ## Observability
43
+
44
+ Every step emits one structured line so a failed install is diagnosable from its log:
45
+
46
+ ```
47
+ [lite-install] op=step name=<…> ok=<bool> ms=<…>
48
+ [lite-install] op=versions node=<v> claude=<v> ttyd=<v>
49
+ [lite-install] op=healthcheck claude=<bool> webchat=<bool> validator=<bool> vault=<bool>
50
+ [lite-install] op=done ok=<bool> ranMs=<…>
51
+ ```
52
+
53
+ A healthy run ends `op=done ok=true`. The first `ok=false` step is the failure point. Full operator instructions: `maxy-code/.docs/maxy-lite-install.md`.
54
+
55
+ ## Tests
56
+
57
+ ```sh
58
+ npm test # node --test over lib (log, pins, paths, healthcheck, orchestrate, bundle)
59
+ ```
60
+
61
+ The validator probe runs the real bundled validator against an empty vault; the orchestrator tests assert the step order, the idempotency guards, and the structured-line output against a recording context.
package/index.mjs ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ // @rubytech/create-maxy-lite — the maxy-lite installer.
3
+ //
4
+ // Run in bare Termux (bionic Node) via `npx @rubytech/create-maxy-lite`. It
5
+ // orchestrates the whole on-device install: proot-distro Ubuntu -> glibc Node ->
6
+ // claude -> the app (validator + web-chat relay) -> the shared-storage vault and
7
+ // its proot bind-mount. Every step emits one `[lite-install]` line and the run
8
+ // exits non-zero on any failed step or health-check probe.
9
+ //
10
+ // This file is only the wiring: it builds the real subprocess/fs/probe
11
+ // primitives and hands them to orchestrate(). The sequence, guards, idempotency
12
+ // and probe rules live in ./lib and are unit-tested there.
13
+ //
14
+ // Subprocesses use spawnSync with an explicit (file, args[]) — never exec() and
15
+ // never shell:true. The command text is interpreted by an explicit `bash -lc` so
16
+ // pipes work; every command is a pinned constant from versions.json/paths.mjs,
17
+ // never user input, so there is no injection surface.
18
+
19
+ import fs from 'node:fs'
20
+ import path from 'node:path'
21
+ import { spawnSync } from 'node:child_process'
22
+ import { fileURLToPath } from 'node:url'
23
+
24
+ import { makeLog } from './lib/log.mjs'
25
+ import { readPins } from './lib/pins.mjs'
26
+ import { PATHS, WEBCHAT_DIR, VALIDATOR_CLI, launcherPath, launcherScript } from './lib/paths.mjs'
27
+ import { orchestrate, STEP_NAMES } from './lib/orchestrate.mjs'
28
+ import { claudeOk, validatorOk, vaultOk, webchatOk, healthcheck } from './lib/healthcheck.mjs'
29
+
30
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
31
+ const PAYLOAD = path.join(HERE, 'payload')
32
+ const PINS = readPins(path.join(HERE, 'versions.json'))
33
+ const PREFIX = process.env.PREFIX || '/data/data/com.termux/files/usr'
34
+
35
+ // ---- subprocess primitives --------------------------------------------------
36
+
37
+ const decode = (r) => ({ code: r.status === null ? 1 : r.status, stdout: r.stdout || '' })
38
+
39
+ /** Run a command in bare Termux. */
40
+ const run = (cmd) => decode(spawnSync('bash', ['-lc', cmd], { encoding: 'utf8' }))
41
+
42
+ /** Run a command inside the glibc Ubuntu layer. */
43
+ const runIn = (cmd) =>
44
+ decode(spawnSync('proot-distro', ['login', PATHS.distro, '--', 'bash', '-lc', cmd], { encoding: 'utf8' }))
45
+
46
+ // ---- filesystem primitives --------------------------------------------------
47
+ // The Ubuntu rootfs is a normal directory under the Termux prefix, so the app
48
+ // payload is copied into it directly from bionic Node (no proot round-trip).
49
+
50
+ const rootfs = `${PREFIX}/var/lib/proot-distro/installed-rootfs/${PATHS.distro}`
51
+ const appHostPath = `${rootfs}${PATHS.appDir}` // appDir is absolute inside the rootfs
52
+
53
+ const existsHost = (p) => fs.existsSync(p)
54
+ const mkdirHost = (p) => fs.mkdirSync(p, { recursive: true })
55
+
56
+ /** Copy the bundled app payload into the Ubuntu rootfs (idempotent overwrite). */
57
+ const layPayload = () => {
58
+ fs.mkdirSync(appHostPath, { recursive: true })
59
+ fs.cpSync(PAYLOAD, appHostPath, { recursive: true })
60
+ }
61
+
62
+ /** Install the app deps (validator's js-yaml) and the webchat deps (node-pty, ws). */
63
+ const installDeps = () => {
64
+ if (runIn(`cd ${PATHS.appDir} && npm install --omit=dev`).code !== 0) throw new Error('app deps install failed')
65
+ if (runIn(`cd ${WEBCHAT_DIR} && npm install --omit=dev`).code !== 0) throw new Error('webchat deps install failed')
66
+ }
67
+
68
+ /** Write the `maxy-lite` launcher into the Termux bin dir. */
69
+ const writeLauncher = () => {
70
+ fs.writeFileSync(launcherPath(PREFIX), launcherScript({ prefix: PREFIX }), { mode: 0o755 })
71
+ }
72
+
73
+ // ---- measured versions ------------------------------------------------------
74
+
75
+ const firstMatch = (s, re, fallback) => {
76
+ const m = re.exec(s || '')
77
+ return m ? m[1] : fallback
78
+ }
79
+
80
+ const readVersions = () => ({
81
+ node: firstMatch(runIn('node -v').stdout, /v(\d+\.\d+\.\d+)/, PINS.nodeMajor),
82
+ claude: firstMatch(runIn('claude --version').stdout, /(\d+\.\d+\.\d+)/, PINS.claudeCode),
83
+ ttyd: firstMatch(runIn('ttyd --version').stdout, /(\d+\.\d+\.\d+)/, PINS.ttyd),
84
+ })
85
+
86
+ // ---- health-check probes (the measured post-conditions) ---------------------
87
+
88
+ const runHealthcheck = () =>
89
+ healthcheck({
90
+ checkClaude: () => claudeOk(() => runIn('claude --version')),
91
+ // Start the relay, poll its port for up to 10s, record whether it bound, kill it.
92
+ checkWebchat: () =>
93
+ webchatOk(async () => {
94
+ const probe =
95
+ `cd ${WEBCHAT_DIR} && node server.mjs & SVPID=$!; ` +
96
+ `for i in $(seq 1 10); do sleep 1; ` +
97
+ `curl -sf -o /dev/null http://localhost:${PATHS.webchatPort} && break; done; ` +
98
+ `curl -sf -o /dev/null http://localhost:${PATHS.webchatPort}; RC=$?; ` +
99
+ `kill $SVPID 2>/dev/null; exit $RC`
100
+ return runIn(probe).code === 0
101
+ }),
102
+ // Validator must exit 0 on an empty vault.
103
+ checkValidator: () => {
104
+ const empty = `${PATHS.appDir}/.hc-empty-vault`
105
+ runIn(`mkdir -p ${empty}`)
106
+ return validatorOk((vault) => runIn(`node ${VALIDATOR_CLI} ${vault}`).code, empty)
107
+ },
108
+ checkVault: () => vaultOk(() => runIn(`test -d ${PATHS.vaultGuest}`).code === 0),
109
+ })
110
+
111
+ // ---- dry run ----------------------------------------------------------------
112
+
113
+ function printDryRun(log) {
114
+ log('plan', { steps: STEP_NAMES.join('>'), node: PINS.nodeMajor, claude: PINS.claudeCode, ttyd: PINS.ttyd })
115
+ log('plan', { vaultHost: PATHS.vaultHost, vaultGuest: PATHS.vaultGuest, appDir: PATHS.appDir })
116
+ log('plan', { launcher: launcherPath(PREFIX), webchatPort: PATHS.webchatPort })
117
+ }
118
+
119
+ // ---- entry ------------------------------------------------------------------
120
+
121
+ async function main() {
122
+ const log = makeLog()
123
+ if (process.argv.includes('--dry-run')) {
124
+ printDryRun(log)
125
+ return 0
126
+ }
127
+ const ctx = {
128
+ run,
129
+ runIn,
130
+ existsHost,
131
+ mkdirHost,
132
+ layPayload,
133
+ installDeps,
134
+ writeLauncher,
135
+ readVersions,
136
+ runHealthcheck,
137
+ pins: PINS,
138
+ now: () => Date.now(),
139
+ log,
140
+ }
141
+ const result = await orchestrate(ctx)
142
+ return result.ok ? 0 : 1
143
+ }
144
+
145
+ main()
146
+ .then((code) => process.exit(code))
147
+ .catch((err) => {
148
+ process.stderr.write(`[lite-install] op=done ok=false error=${err && err.message ? err.message : err}\n`)
149
+ process.exit(1)
150
+ })
@@ -0,0 +1,41 @@
1
+ // Post-install health check — the measured post-conditions, not "ran the step".
2
+ // Each probe answers a binary question about the running system:
3
+ // claude — does `claude --version` answer with a version?
4
+ // webchat — does the relay port listen?
5
+ // validator — does the validator exit 0 on an empty vault?
6
+ // vault — does the bind-mount path exist inside Ubuntu?
7
+ // The probes take injected primitives so the decision rules are unit-testable;
8
+ // index.mjs wires the real ones (run-in-Ubuntu, transient port probe, fs stat).
9
+
10
+ const VERSION_RE = /\d+\.\d+\.\d+/
11
+
12
+ /** True when `claude --version` exits 0 and prints a version. `runIn(cmd)` → {code, stdout}. */
13
+ export function claudeOk(runVersion) {
14
+ const r = runVersion()
15
+ return r.code === 0 && VERSION_RE.test(r.stdout || '')
16
+ }
17
+
18
+ /** True when the validator exits 0 on the given (empty) vault. `runValidator(vault)` → exit code. */
19
+ export function validatorOk(runValidator, vaultPath) {
20
+ return runValidator(vaultPath) === 0
21
+ }
22
+
23
+ /** True when the bind-mount path exists. `exists()` → bool. */
24
+ export function vaultOk(exists) {
25
+ return exists() === true
26
+ }
27
+
28
+ /** True when the relay port responded. `getStatus()` → Promise<bool>. */
29
+ export async function webchatOk(getStatus) {
30
+ return (await getStatus()) === true
31
+ }
32
+
33
+ /** Run all four probes and return `{ claude, webchat, validator, vault }`. */
34
+ export async function healthcheck({ checkClaude, checkWebchat, checkValidator, checkVault }) {
35
+ return {
36
+ claude: checkClaude() === true,
37
+ webchat: (await checkWebchat()) === true,
38
+ validator: checkValidator() === true,
39
+ vault: checkVault() === true,
40
+ }
41
+ }
package/lib/log.mjs ADDED
@@ -0,0 +1,16 @@
1
+ // Structured install log. Every install step emits exactly one `[lite-install]`
2
+ // line so a failed install is diagnosable from its log without re-running blind:
3
+ // bootstrap 2>&1 | tee install.log; grep '\[lite-install\]' install.log
4
+ // The first `ok=false` step is the failure point.
5
+
6
+ /** Format one structured line: `[lite-install] op=<op> k=v k=v …` (field order preserved). */
7
+ export function formatLine(op, fields = {}) {
8
+ const parts = [`[lite-install] op=${op}`]
9
+ for (const [k, v] of Object.entries(fields)) parts.push(`${k}=${v}`)
10
+ return parts.join(' ')
11
+ }
12
+
13
+ /** Wrap a writer (default: stderr) into `log(op, fields)` that emits one formatted line. */
14
+ export function makeLog(write = (s) => process.stderr.write(s + '\n')) {
15
+ return (op, fields = {}) => write(formatLine(op, fields))
16
+ }
@@ -0,0 +1,148 @@
1
+ // The install sequence. Run in bare Termux (bionic Node); each step provisions
2
+ // one layer of the runtime stack and emits exactly one `[lite-install] op=step`
3
+ // line, gated on exit code. Every step has a `done(ctx)` guard so a re-run on a
4
+ // converged device skips the mutation and still reports ok=true (idempotent).
5
+ //
6
+ // Steps, in order:
7
+ // termux-deps — Termux packages (proot-distro) for the glibc layer
8
+ // proot — verify the proot-distro framework is operational
9
+ // ubuntu — install the glibc Ubuntu rootfs under proot
10
+ // node — pinned glibc Node inside Ubuntu (the bionic Node can't run claude)
11
+ // toolchain — python3/make/g++ so node-pty compiles
12
+ // vault-bind — shared-storage vault + the proot bind so Obsidian and claude share it
13
+ // npm-app — pinned claude-code + ttyd, the app payload, webchat deps, the launcher
14
+ //
15
+ // All external effects go through injected ctx primitives so the sequence,
16
+ // guards and idempotency are unit-testable; index.mjs wires the real ones.
17
+
18
+ import { PATHS, WEBCHAT_DIR, launcherPath } from './paths.mjs'
19
+
20
+ export const STEP_NAMES = ['termux-deps', 'proot', 'ubuntu', 'node', 'toolchain', 'vault-bind', 'npm-app']
21
+
22
+ /** Throw unless the command result is a clean exit. */
23
+ function ensureOk(r) {
24
+ if (!r || r.code !== 0) throw new Error(`command failed (code ${r ? r.code : 'none'})`)
25
+ return r
26
+ }
27
+
28
+ /** The Ubuntu rootfs path under the Termux prefix. */
29
+ function rootfsPath() {
30
+ const prefix = process.env.PREFIX || '/data/data/com.termux/files/usr'
31
+ return `${prefix}/var/lib/proot-distro/installed-rootfs/${PATHS.distro}`
32
+ }
33
+
34
+ /** True when the glibc Node inside Ubuntu matches the pinned major. */
35
+ function nodeMajorOk(c) {
36
+ const r = c.runIn('node -v')
37
+ if (r.code !== 0) return false
38
+ const m = /^v(\d+)\./.exec((r.stdout || '').trim())
39
+ return !!m && m[1] === String(c.pins.nodeMajor)
40
+ }
41
+
42
+ const STEPS = [
43
+ {
44
+ name: 'termux-deps',
45
+ done: (c) => c.run('command -v proot-distro').code === 0,
46
+ run: (c) => ensureOk(c.run('pkg install -y proot-distro')),
47
+ },
48
+ {
49
+ name: 'proot',
50
+ done: () => false, // read-only verification gate; always runs, never mutates
51
+ run: (c) => ensureOk(c.run('proot-distro list')),
52
+ },
53
+ {
54
+ name: 'ubuntu',
55
+ done: (c) => c.existsHost(rootfsPath()),
56
+ run: (c) => ensureOk(c.run(`proot-distro install ${PATHS.distro}`)),
57
+ },
58
+ {
59
+ name: 'node',
60
+ done: (c) => nodeMajorOk(c),
61
+ run: (c) => {
62
+ // runIn already wraps in `bash -lc`; pass the pipeline directly (no inner bash).
63
+ ensureOk(c.runIn(`curl -fsSL https://deb.nodesource.com/setup_${c.pins.nodeMajor}.x | bash -`))
64
+ ensureOk(c.runIn('apt install -y nodejs'))
65
+ },
66
+ },
67
+ {
68
+ name: 'toolchain',
69
+ done: (c) => c.runIn('command -v g++').code === 0,
70
+ run: (c) => ensureOk(c.runIn('apt install -y python3 make g++')),
71
+ },
72
+ {
73
+ name: 'vault-bind',
74
+ done: (c) => c.existsHost(PATHS.vaultHost),
75
+ run: (c) => {
76
+ c.mkdirHost(PATHS.vaultHost)
77
+ ensureOk(c.run(`proot-distro login ${PATHS.distro} --bind ${PATHS.vaultHost}:${PATHS.vaultGuest} -- test -d ${PATHS.vaultGuest}`))
78
+ },
79
+ },
80
+ {
81
+ name: 'npm-app',
82
+ // Converged only when the deps are built, claude is present, AND the launcher
83
+ // is written — the launcher is the last mutation, so guarding on the first two
84
+ // alone would skip a half-finished install and report ok=true with no launcher.
85
+ done: (c) =>
86
+ c.runIn(`test -d ${WEBCHAT_DIR}/node_modules`).code === 0 &&
87
+ c.runIn('command -v claude').code === 0 &&
88
+ c.existsHost(launcherPath()),
89
+ run: (c) => {
90
+ ensureOk(c.runIn(`npm install -g @anthropic-ai/claude-code@${c.pins.claudeCode}`))
91
+ // Pin ttyd to the manifest version via its release binary; apt would float.
92
+ ensureOk(
93
+ c.runIn(
94
+ `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
+ ),
96
+ )
97
+ c.layPayload()
98
+ c.installDeps()
99
+ c.writeLauncher()
100
+ },
101
+ },
102
+ ]
103
+
104
+ /** Run one step: guard then action, timed, one structured line. Never throws. */
105
+ function runStep(step, ctx) {
106
+ const start = ctx.now()
107
+ let ok = false
108
+ let skipped = false
109
+ try {
110
+ if (step.done(ctx)) {
111
+ skipped = true
112
+ ok = true
113
+ } else {
114
+ step.run(ctx)
115
+ ok = true
116
+ }
117
+ } catch {
118
+ ok = false
119
+ }
120
+ const ms = ctx.now() - start
121
+ ctx.log('step', { name: step.name, ok, ms })
122
+ return { name: step.name, ok, skipped, ms }
123
+ }
124
+
125
+ /**
126
+ * Run the full install. Stops at the first failed step (logs `done ok=false`,
127
+ * no healthcheck). On success: emits the resolved versions, the four-probe
128
+ * healthcheck, and `done ok=<all probes true>`.
129
+ */
130
+ export async function orchestrate(ctx) {
131
+ const t0 = ctx.now()
132
+ const results = []
133
+ for (const step of STEPS) {
134
+ const r = runStep(step, ctx)
135
+ results.push(r)
136
+ if (!r.ok) {
137
+ ctx.log('done', { ok: false, ranMs: ctx.now() - t0 })
138
+ return { ok: false, results }
139
+ }
140
+ }
141
+ const v = ctx.readVersions()
142
+ ctx.log('versions', { node: v.node, claude: v.claude, ttyd: v.ttyd })
143
+ const hc = await ctx.runHealthcheck()
144
+ ctx.log('healthcheck', hc)
145
+ const ok = Object.values(hc).every(Boolean)
146
+ ctx.log('done', { ok, ranMs: ctx.now() - t0 })
147
+ return { ok, results, healthcheck: hc }
148
+ }
package/lib/paths.mjs ADDED
@@ -0,0 +1,50 @@
1
+ // On-device paths and the launcher.
2
+ //
3
+ // Two distinct surfaces, deliberately separated:
4
+ // - The VAULT (markdown the agent reads/writes) lives on Android shared
5
+ // storage so Obsidian — an Android app — can open it as a vault. It is
6
+ // bind-mounted into the Ubuntu rootfs so `claude` (in proot) sees the same
7
+ // folder. Obsidian and claude share one tree.
8
+ // - The APP code (validator, webchat, its node_modules with native node-pty)
9
+ // lives INSIDE the Ubuntu rootfs. Android shared storage is commonly mounted
10
+ // noexec / no-symlink, which breaks a native node_modules; the rootfs is a
11
+ // normal ext4-backed tree, so the app and its build live there.
12
+
13
+ export const PATHS = {
14
+ distro: 'ubuntu',
15
+ vaultHost: '/storage/emulated/0/maxy', // Android shared storage (Obsidian opens this)
16
+ vaultGuest: '/root/maxy', // bind target inside Ubuntu (claude's working dir)
17
+ appDir: '/root/.maxy-lite/app', // app code + node_modules, in the rootfs
18
+ webchatPort: 7682,
19
+ }
20
+
21
+ export const WEBCHAT_DIR = `${PATHS.appDir}/webchat`
22
+ export const VALIDATOR_CLI = `${PATHS.appDir}/validator/cli.mjs`
23
+
24
+ /** Absolute path of the `maxy-lite` launcher in the Termux bin dir. */
25
+ export function launcherPath(prefix = process.env.PREFIX || '/data/data/com.termux/files/usr') {
26
+ return `${prefix}/bin/maxy-lite`
27
+ }
28
+
29
+ /**
30
+ * The `maxy-lite` launcher script (written into the Termux bin dir). Run from the
31
+ * Termux shell, it enters Ubuntu with the vault bind-mount and starts the webchat
32
+ * relay on localhost. Documented Termux:Boot opt-in re-runs this on boot.
33
+ */
34
+ export function launcherScript({
35
+ prefix = process.env.PREFIX || '/data/data/com.termux/files/usr',
36
+ vaultHost = PATHS.vaultHost,
37
+ vaultGuest = PATHS.vaultGuest,
38
+ webchatDir = WEBCHAT_DIR,
39
+ port = PATHS.webchatPort,
40
+ distro = PATHS.distro,
41
+ } = {}) {
42
+ return `#!${prefix}/bin/bash
43
+ # maxy-lite launcher — starts the on-device web chat relay.
44
+ # Enters the glibc Ubuntu layer with the shared-storage vault bound in, then runs
45
+ # the relay on http://localhost:${port}. Open that URL in the phone browser.
46
+ set -e
47
+ exec proot-distro login ${distro} --bind ${vaultHost}:${vaultGuest} -- \\
48
+ bash -lc 'cd ${webchatDir} && LITE_AGENT_HOME=${vaultGuest} LITE_PORT=${port} exec node server.mjs'
49
+ `
50
+ }
package/lib/pins.mjs ADDED
@@ -0,0 +1,22 @@
1
+ // Version pins — one source of truth (versions.json). The installer pins Node,
2
+ // claude-code and ttyd so a fresh device gets reproducible versions, never a
3
+ // floating `latest`. Read once at startup; `op=versions` reports what landed.
4
+
5
+ import fs from 'node:fs'
6
+
7
+ const REQUIRED = ['nodeMajor', 'claudeCode', 'ttyd']
8
+
9
+ /** Read and validate the pinned versions. Throws on a missing, empty or floating pin. */
10
+ export function readPins(manifestPath) {
11
+ const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
12
+ for (const key of REQUIRED) {
13
+ const v = raw[key]
14
+ if (v === undefined || v === null || String(v).trim() === '') {
15
+ throw new Error(`pins: ${key} is required in ${manifestPath}`)
16
+ }
17
+ if (String(v).trim().toLowerCase() === 'latest') {
18
+ throw new Error(`pins: ${key} must be a fixed version, not "latest"`)
19
+ }
20
+ }
21
+ return { nodeMajor: String(raw.nodeMajor), claudeCode: String(raw.claudeCode), ttyd: String(raw.ttyd) }
22
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@rubytech/create-maxy-lite",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "bin": {
10
+ "create-maxy-lite": "index.mjs"
11
+ },
12
+ "files": [
13
+ "index.mjs",
14
+ "lib",
15
+ "versions.json",
16
+ "payload",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "bundle": "node scripts/bundle.mjs",
21
+ "test": "node --test 'test/*.test.mjs'",
22
+ "prepublishOnly": "node scripts/bundle.mjs"
23
+ },
24
+ "keywords": [
25
+ "maxy-lite",
26
+ "android",
27
+ "termux",
28
+ "claude",
29
+ "on-device"
30
+ ],
31
+ "author": "Rubytech LLC",
32
+ "license": "UNLICENSED"
33
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "maxy-lite-app",
3
+ "private": true,
4
+ "type": "module",
5
+ "dependencies": {
6
+ "js-yaml": "^4.1.0"
7
+ }
8
+ }