@rubytech/create-maxy-lite 0.1.1 → 0.1.2

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 CHANGED
@@ -82,6 +82,25 @@ const appHostPath = `${rootfs}${PATHS.appDir}` // appDir is absolute inside the
82
82
  const existsHost = (p) => fs.existsSync(p)
83
83
  const mkdirHost = (p) => fs.mkdirSync(p, { recursive: true })
84
84
 
85
+ // ---- shared-storage permission ----------------------------------------------
86
+ // The vault lives on Android shared storage, whose parent (`/storage/emulated/0`)
87
+ // is unwritable until Termux holds the shared-storage permission. `termux-setup-
88
+ // storage` pops the Android grant dialog; the parent becomes writable once the
89
+ // user taps Allow. Detection is the writability of that parent, not an existence
90
+ // check — the directory can be present but inaccessible before the grant.
91
+
92
+ const storageParent = path.dirname(PATHS.vaultHost)
93
+ const storageAccessible = () => {
94
+ try {
95
+ fs.accessSync(storageParent, fs.constants.W_OK)
96
+ return true
97
+ } catch {
98
+ return false
99
+ }
100
+ }
101
+ const interactive = Boolean(process.stdin.isTTY)
102
+ const STORAGE_POLL_MS = 3000
103
+
85
104
  /** Copy the bundled app payload into the Ubuntu rootfs (idempotent overwrite). */
86
105
  const layPayload = () => {
87
106
  fs.mkdirSync(appHostPath, { recursive: true })
@@ -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,
@@ -29,10 +29,34 @@ function ensureOk(r) {
29
29
  return r
30
30
  }
31
31
 
32
- /** The Ubuntu rootfs path under the Termux prefix. */
33
- function rootfsPath() {
34
- const prefix = process.env.PREFIX || '/data/data/com.termux/files/usr'
35
- return `${prefix}/var/lib/proot-distro/installed-rootfs/${PATHS.distro}`
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.existsHost(rootfsPath()),
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-lite",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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": {