autodev-app 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,63 @@
1
+ # autodev-app
2
+
3
+ The **AutoDev desktop app** — run autonomous AI agents from a GUI instead of a terminal.
4
+
5
+ ```bash
6
+ npx autodev-app
7
+ ```
8
+
9
+ That's it. No account needed to install, no sudo, no `.deb`. The first run downloads
10
+ the app (~126 MB) and unpacks it into `~/.cache/autodev-app/`; every run after that
11
+ starts straight away.
12
+
13
+ To keep it on your PATH:
14
+
15
+ ```bash
16
+ npm install -g autodev-app
17
+ autodev-app
18
+ ```
19
+
20
+ ## What you get
21
+
22
+ A window that runs AutoDev agents for you: connect a character from your
23
+ [office](https://autodev.code.aioffice.works), point it at a folder, and watch it
24
+ work — live output, sessions, and a file browser in one place. The `autodev` CLI is
25
+ bundled, so you don't install it separately.
26
+
27
+ ## Requirements
28
+
29
+ - **Linux x64** — other platforms aren't built yet; the launcher tells you so
30
+ instead of failing strangely.
31
+ - **Node.js 18+** (only to run this launcher).
32
+ - No libfuse2 needed. The AppImage is unpacked rather than FUSE-mounted, so this
33
+ works on Ubuntu 22.04+ where AppImages otherwise fail out of the box.
34
+
35
+ ## Prefer a real system install?
36
+
37
+ The `.deb` integrates with your desktop (menu entry, icon) and ships a properly
38
+ `setuid` Chromium sandbox, which an unpacked AppImage cannot:
39
+
40
+ ```bash
41
+ wget https://autodev.code.aioffice.works/download/AutoDev-0.1.0-amd64.deb
42
+ sudo apt install ./AutoDev-0.1.0-amd64.deb
43
+ ```
44
+
45
+ ## Notes
46
+
47
+ - **Integrity**: the download is checked against a SHA-256 pinned inside this
48
+ package before anything is executed. A mismatch aborts.
49
+ - **Disk**: the unpacked app is ~345 MB under `~/.cache/autodev-app/<version>/`.
50
+ Delete that folder to reclaim it or to force a clean re-download.
51
+ - **Self-hosting**: set `AUTODEV_APP_DOWNLOAD_BASE` to serve the artifact from your
52
+ own office (the pinned checksum still applies).
53
+
54
+ ## Why a launcher and not the app itself
55
+
56
+ The app links `node-pty`, a native module built against **Electron's** ABI. Shipping
57
+ the sources to npm would rebuild it against your local **Node** ABI, and the app
58
+ would install perfectly and then refuse to start. The release artifact already has
59
+ that solved, so this package fetches it rather than trying to recreate it.
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * `npx autodev-app` — fetch and run the AutoDev desktop app.
6
+ *
7
+ * Why this package ships a launcher instead of the app itself:
8
+ *
9
+ * The app depends on node-pty, a NATIVE module. electron-builder rebuilds it
10
+ * against Electron's ABI when it produces the release artifact. An npm-installed
11
+ * copy would be compiled for the host Node's ABI instead and abort on load, so
12
+ * "just publish the sources" produces a package that installs cleanly and then
13
+ * never runs. The release artifact already has that solved — so we fetch it.
14
+ *
15
+ * The AppImage is extracted once rather than run directly: AppImages need libfuse2,
16
+ * which Ubuntu has not shipped by default since 22.04. Extracting sidesteps FUSE
17
+ * entirely and makes every launch after the first one instant.
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+ const crypto = require('crypto');
24
+ const { spawn, spawnSync } = require('child_process');
25
+
26
+ const APP_VERSION = require('../package.json').version;
27
+ const ASSET = `AutoDev-${APP_VERSION}-x86_64.AppImage`;
28
+
29
+ // sha256 of ASSET, pinned at publish time. The download is verified against this
30
+ // before anything is executed — a truncated or tampered file must never run.
31
+ const SHA256 = '8c8c5506c9a01b556fed85362a5333e4e73cad671adaacd24a89d59cc2524f0f';
32
+
33
+ // Overridable so a self-hosted office can serve its own build.
34
+ const BASE = (process.env.AUTODEV_APP_DOWNLOAD_BASE || 'https://autodev.code.aioffice.works/download').replace(/\/+$/, '');
35
+
36
+ const CACHE = path.join(
37
+ process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'),
38
+ 'autodev-app',
39
+ APP_VERSION
40
+ );
41
+ const ROOT = path.join(CACHE, 'squashfs-root');
42
+ const APPRUN = path.join(ROOT, 'AppRun');
43
+
44
+ const say = (m) => process.stderr.write(`${m}\n`);
45
+ const die = (m) => { say(`\nautodev-app: ${m}`); process.exit(1); };
46
+
47
+ /**
48
+ * VERIFIED THE HARD WAY: with ELECTRON_RUN_AS_NODE set, Electron runs as plain
49
+ * Node — the app exits 0 immediately with no window and no error. Some
50
+ * Electron-hosted terminals and agent runtimes export it, so a user inside one
51
+ * would just see "nothing happened". Strip it from anything we launch.
52
+ *
53
+ * (Assigning `undefined` into an env object is NOT reliable — delete the key.)
54
+ */
55
+ function cleanEnv() {
56
+ const env = { ...process.env };
57
+ delete env.ELECTRON_RUN_AS_NODE;
58
+ return env;
59
+ }
60
+
61
+ function checkPlatform() {
62
+ if (process.platform !== 'linux' || process.arch !== 'x64') {
63
+ die(
64
+ `no build for ${process.platform}-${process.arch} yet — only linux-x64.\n` +
65
+ `See ${BASE.replace(/\/download$/, '')} for other options.`
66
+ );
67
+ }
68
+ }
69
+
70
+ async function download(dest) {
71
+ const url = `${BASE}/${ASSET}`;
72
+ say(`autodev-app ${APP_VERSION} — first run, fetching the app (~126 MB)`);
73
+ say(` from ${url}`);
74
+
75
+ let res;
76
+ try {
77
+ res = await fetch(url, { redirect: 'follow' });
78
+ } catch (err) {
79
+ // Node's fetch collapses DNS/TLS/connection failures into "fetch failed".
80
+ // Surface the cause, or the user gets two useless words.
81
+ const cause = err && err.cause ? ` (${err.cause.code || err.cause.message})` : '';
82
+ die(`could not reach ${BASE}${cause}\n ${err.message}\n Check your connection, then retry.`);
83
+ }
84
+ if (!res.ok) die(`download failed — HTTP ${res.status} ${res.statusText}\n ${url}`);
85
+
86
+ const total = Number(res.headers.get('content-length')) || 0;
87
+ const tmp = `${dest}.part`;
88
+ const out = fs.createWriteStream(tmp);
89
+ const hash = crypto.createHash('sha256');
90
+ let got = 0;
91
+ let lastPct = -1;
92
+
93
+ for await (const chunk of res.body) {
94
+ hash.update(chunk);
95
+ got += chunk.length;
96
+ if (!out.write(chunk)) await new Promise((r) => out.once('drain', r));
97
+ if (total && process.stderr.isTTY) {
98
+ const pct = Math.floor((got / total) * 100);
99
+ if (pct !== lastPct) {
100
+ lastPct = pct;
101
+ process.stderr.write(`\r ${pct}% ${(got / 1e6).toFixed(0)}/${(total / 1e6).toFixed(0)} MB`);
102
+ }
103
+ }
104
+ }
105
+ await new Promise((r, j) => { out.end(); out.on('finish', r); out.on('error', j); });
106
+ if (process.stderr.isTTY && total) process.stderr.write('\n');
107
+
108
+ const got256 = hash.digest('hex');
109
+ if (got256 !== SHA256) {
110
+ fs.unlinkSync(tmp);
111
+ die(`checksum mismatch — refusing to run this file.\n expected ${SHA256}\n got ${got256}`);
112
+ }
113
+ fs.renameSync(tmp, dest);
114
+ say(' checksum ok');
115
+ }
116
+
117
+ function extract(appimage) {
118
+ say(' unpacking…');
119
+ fs.chmodSync(appimage, 0o755);
120
+ const r = spawnSync(appimage, ['--appimage-extract'], {
121
+ cwd: CACHE,
122
+ stdio: ['ignore', 'ignore', 'pipe'],
123
+ env: cleanEnv(),
124
+ });
125
+ if (r.status !== 0 || !fs.existsSync(APPRUN)) {
126
+ die(`could not unpack the app.\n ${String(r.stderr || '').trim().split('\n').slice(0, 3).join('\n ')}`);
127
+ }
128
+ // The 126 MB archive is dead weight once unpacked.
129
+ try { fs.unlinkSync(appimage); } catch { /* not fatal */ }
130
+ }
131
+
132
+ /**
133
+ * Electron's SUID sandbox helper is not setuid inside an extracted AppImage (only
134
+ * the .deb's postinst can chmod 4755 it). Electron then falls back to the
135
+ * unprivileged-userns sandbox — which Ubuntu 24.04+ restricts via AppArmor. Where
136
+ * that restriction is active, the app would abort with a Chromium "Operation not
137
+ * permitted" crash, so drop the sandbox there rather than fail to start.
138
+ */
139
+ function sandboxArgs() {
140
+ let restricted = false;
141
+ try {
142
+ restricted = fs.readFileSync('/proc/sys/kernel/apparmor_restrict_unprivileged_userns', 'utf8').trim() === '1';
143
+ } catch { /* file absent → no restriction */ }
144
+ if (!restricted) return [];
145
+ let suid = false;
146
+ try { suid = (fs.statSync(path.join(ROOT, 'chrome-sandbox')).mode & 0o4000) !== 0; } catch { /* ignore */ }
147
+ if (suid) return [];
148
+ say(' note: this kernel restricts unprivileged user namespaces — starting with --no-sandbox.');
149
+ say(' For the sandboxed build, install the .deb instead: ' + `${BASE}/AutoDev-${APP_VERSION}-amd64.deb`);
150
+ return ['--no-sandbox'];
151
+ }
152
+
153
+ async function main() {
154
+ checkPlatform();
155
+
156
+ if (!fs.existsSync(APPRUN)) {
157
+ fs.mkdirSync(CACHE, { recursive: true });
158
+ const appimage = path.join(CACHE, ASSET);
159
+ if (!fs.existsSync(appimage)) await download(appimage);
160
+ extract(appimage);
161
+ say(` installed to ${ROOT}`);
162
+ }
163
+
164
+ const args = process.argv.slice(2).concat(sandboxArgs());
165
+ const child = spawn(APPRUN, args, { stdio: 'inherit', env: cleanEnv() });
166
+ child.on('error', (err) => die(`could not start the app: ${err.message}`));
167
+ child.on('exit', (code, signal) => process.exit(signal ? 1 : code ?? 0));
168
+ }
169
+
170
+ main().catch((err) => die(err && err.stack ? err.stack : String(err)));
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "autodev-app",
3
+ "version": "0.1.0",
4
+ "description": "AutoDev desktop app — run autonomous AI agents from a GUI. Installs and launches the AutoDev desktop app.",
5
+ "keywords": [
6
+ "autodev",
7
+ "ai",
8
+ "agents",
9
+ "desktop",
10
+ "electron",
11
+ "autonomous"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "autoaidev",
15
+ "homepage": "https://autodev.code.aioffice.works",
16
+ "bin": {
17
+ "autodev-app": "bin/autodev-app.js"
18
+ },
19
+ "files": [
20
+ "bin/autodev-app.js",
21
+ "README.md"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "os": [
27
+ "linux"
28
+ ],
29
+ "cpu": [
30
+ "x64"
31
+ ],
32
+ "scripts": {
33
+ "test": "node test/launcher.test.mjs"
34
+ }
35
+ }