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 +63 -0
- package/bin/autodev-app.js +170 -0
- package/package.json +35 -0
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
|
+
}
|