@addai/node 0.16.0 → 0.19.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/dist/command-runner.js +45 -1
- package/dist/desktop/docker.js +5 -2
- package/dist/desktop/engine.d.ts +12 -0
- package/dist/desktop/engine.js +39 -10
- package/dist/desktop/install-engine.d.ts +24 -1
- package/dist/desktop/install-engine.js +76 -5
- package/dist/desktop/provider.d.ts +5 -0
- package/package.json +1 -1
package/dist/command-runner.js
CHANGED
|
@@ -64,6 +64,7 @@ const docker_1 = require("./desktop/docker");
|
|
|
64
64
|
const manager_1 = require("./desktop/manager");
|
|
65
65
|
const creds_1 = require("./desktop/creds");
|
|
66
66
|
const install_engine_1 = require("./desktop/install-engine");
|
|
67
|
+
const engine_1 = require("./desktop/engine");
|
|
67
68
|
const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
68
69
|
/** Own version, read the same way index.ts does. Resolved here rather than
|
|
69
70
|
* imported from index.ts, which already imports this module. */
|
|
@@ -692,8 +693,51 @@ async function runInstallEngine(cmd) {
|
|
|
692
693
|
};
|
|
693
694
|
await update(cmd.id, 'running', { log });
|
|
694
695
|
try {
|
|
695
|
-
|
|
696
|
+
// Installed but unreachable is a permissions problem, not a missing one -
|
|
697
|
+
// the same button must repair rather than reinstall.
|
|
698
|
+
const before = await (0, engine_1.detectEngine)();
|
|
699
|
+
const blocked = !!before && !before.running;
|
|
700
|
+
const outcome = await (0, install_engine_1.installEngine)(onLog, async () => {
|
|
701
|
+
// Park the command and let Studio collect it, exactly as a harness
|
|
702
|
+
// login collects a paste-back code.
|
|
703
|
+
await update(cmd.id, 'awaiting_input', { needs: 'sudo_password', log });
|
|
704
|
+
const deadline = Date.now() + INPUT_WAIT_MS;
|
|
705
|
+
while (Date.now() < deadline) {
|
|
706
|
+
const row = await pollRow(cmd.id);
|
|
707
|
+
if (!row)
|
|
708
|
+
break;
|
|
709
|
+
if (row.status === 'canceled')
|
|
710
|
+
return null;
|
|
711
|
+
const pw = row.input?.sudo_password;
|
|
712
|
+
if (pw) {
|
|
713
|
+
// Wipe it from the row the moment we hold it. It exists in one
|
|
714
|
+
// process's memory from here, and nowhere else.
|
|
715
|
+
const t = token();
|
|
716
|
+
if (t) {
|
|
717
|
+
try {
|
|
718
|
+
await (0, supabase_client_1.rpc)('runtime_command_clear_input', { p_token: t, p_id: cmd.id });
|
|
719
|
+
}
|
|
720
|
+
catch { /* best effort — the install still must not print it */ }
|
|
721
|
+
}
|
|
722
|
+
await update(cmd.id, 'running', { needs: null, log });
|
|
723
|
+
return pw;
|
|
724
|
+
}
|
|
725
|
+
await new Promise(r => setTimeout(r, INPUT_POLL_MS));
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}, blocked);
|
|
696
729
|
(0, docker_1.resetProviderCache)();
|
|
730
|
+
if (outcome.needsRestart) {
|
|
731
|
+
// Group membership only applies to new processes, so this daemon still
|
|
732
|
+
// cannot reach the socket. Say so plainly and bounce it.
|
|
733
|
+
onLog('\nPermissions fixed. Restarting this machine\'s node so it picks up the new group…\n');
|
|
734
|
+
await update(cmd.id, 'completed', { log, restarting: true });
|
|
735
|
+
await (0, heartbeat_1.beat)();
|
|
736
|
+
if (restartHook) {
|
|
737
|
+
await restartHook({ commandId: cmd.id, version: VERSION, restartOnly: true });
|
|
738
|
+
}
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
697
741
|
const engine = await (0, docker_1.getProvider)(true);
|
|
698
742
|
if (!engine) {
|
|
699
743
|
await update(cmd.id, 'failed', { log }, 'Installed, but no engine is answering yet. On macOS, Docker Desktop has to be opened once before its daemon runs.');
|
package/dist/desktop/docker.js
CHANGED
|
@@ -35,7 +35,7 @@ class CliProvider {
|
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
async probe() {
|
|
38
|
-
return { id: this.id, version: this.version };
|
|
38
|
+
return { id: this.id, version: this.version, running: true };
|
|
39
39
|
}
|
|
40
40
|
async create(row, opts, onLog) {
|
|
41
41
|
// Pull first so the slow, chatty image download streams into the command
|
|
@@ -93,7 +93,10 @@ async function getProvider(force = false) {
|
|
|
93
93
|
if (!force && cached !== undefined)
|
|
94
94
|
return cached;
|
|
95
95
|
const info = await (0, engine_1.detectEngine)();
|
|
96
|
-
|
|
96
|
+
// An engine whose daemon is not answering is not a provider: every call
|
|
97
|
+
// through it would fail at the socket. Studio reports the reason from the
|
|
98
|
+
// capability probe instead.
|
|
99
|
+
cached = info?.running ? new CliProvider(info.id, info.version) : null;
|
|
97
100
|
return cached;
|
|
98
101
|
}
|
|
99
102
|
function resetProviderCache() { cached = undefined; }
|
package/dist/desktop/engine.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
import { EngineInfo } from './provider';
|
|
2
2
|
export declare function parseEngineVersion(stdout: string): string | null;
|
|
3
|
+
/** Why an installed engine still cannot run anything. The three causes are
|
|
4
|
+
* worth telling apart, because the fix for each is different and none of
|
|
5
|
+
* them is "install Docker". */
|
|
6
|
+
export declare function explainUnreachable(output: string, platform: NodeJS.Platform): string;
|
|
7
|
+
/**
|
|
8
|
+
* Is there an engine here that can actually RUN something?
|
|
9
|
+
*
|
|
10
|
+
* `--version` is not enough: the CLI answers it with no daemon at all, so a
|
|
11
|
+
* machine where Docker Desktop is installed-but-closed, or where the user is
|
|
12
|
+
* not in the docker group, would report itself ready and then fail on every
|
|
13
|
+
* create. `info` is the question that needs the daemon, so it is the one asked.
|
|
14
|
+
*/
|
|
3
15
|
export declare function detectEngine(): Promise<EngineInfo | null>;
|
package/dist/desktop/engine.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseEngineVersion = parseEngineVersion;
|
|
4
|
+
exports.explainUnreachable = explainUnreachable;
|
|
4
5
|
exports.detectEngine = detectEngine;
|
|
5
6
|
// Which container engine is on this machine? Docker first (the verified
|
|
6
7
|
// path), Podman second. Never installs anything: installing Docker Desktop
|
|
@@ -12,33 +13,61 @@ function parseEngineVersion(stdout) {
|
|
|
12
13
|
const m = /version\s+v?(\d+\.\d+\.\d+)/i.exec(stdout);
|
|
13
14
|
return m ? m[1] : null;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
/** Run a command and hand back exit code + output. */
|
|
17
|
+
function run(binary, args) {
|
|
16
18
|
return new Promise(resolve => {
|
|
17
19
|
try {
|
|
18
|
-
const inv = (0, win_1.resolveCliInvocation)(binary,
|
|
20
|
+
const inv = (0, win_1.resolveCliInvocation)(binary, args);
|
|
19
21
|
const child = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
20
22
|
env: process.env, windowsHide: true, timeout: 8_000,
|
|
21
23
|
});
|
|
22
24
|
let out = '';
|
|
23
25
|
child.stdout?.on('data', b => { out += b.toString('utf8'); });
|
|
24
|
-
child.on('
|
|
25
|
-
child.on('
|
|
26
|
+
child.stderr?.on('data', b => { out += b.toString('utf8'); });
|
|
27
|
+
child.on('error', () => resolve({ ok: false, out: '' }));
|
|
28
|
+
child.on('exit', code => resolve({ ok: code === 0, out }));
|
|
26
29
|
}
|
|
27
30
|
catch {
|
|
28
31
|
// resolveCliInvocation throws when a Windows shim can't be resolved -
|
|
29
32
|
// that is "not installed" as far as this probe is concerned.
|
|
30
|
-
resolve(
|
|
33
|
+
resolve({ ok: false, out: '' });
|
|
31
34
|
}
|
|
32
35
|
});
|
|
33
36
|
}
|
|
37
|
+
/** Why an installed engine still cannot run anything. The three causes are
|
|
38
|
+
* worth telling apart, because the fix for each is different and none of
|
|
39
|
+
* them is "install Docker". */
|
|
40
|
+
function explainUnreachable(output, platform) {
|
|
41
|
+
const t = output.toLowerCase();
|
|
42
|
+
if (t.includes('permission denied') || t.includes('dial unix')) {
|
|
43
|
+
return 'Docker is installed but this user cannot reach it. Add the user to the docker group:\n sudo usermod -aG docker $USER (then log out and back in)';
|
|
44
|
+
}
|
|
45
|
+
if (platform === 'darwin')
|
|
46
|
+
return 'Docker is installed but not running. Open Docker Desktop once and leave it running.';
|
|
47
|
+
if (platform === 'win32')
|
|
48
|
+
return 'Docker is installed but not running. Start Docker Desktop and let it finish starting.';
|
|
49
|
+
return 'Docker is installed but its daemon is not answering. Start it:\n sudo systemctl start docker';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Is there an engine here that can actually RUN something?
|
|
53
|
+
*
|
|
54
|
+
* `--version` is not enough: the CLI answers it with no daemon at all, so a
|
|
55
|
+
* machine where Docker Desktop is installed-but-closed, or where the user is
|
|
56
|
+
* not in the docker group, would report itself ready and then fail on every
|
|
57
|
+
* create. `info` is the question that needs the daemon, so it is the one asked.
|
|
58
|
+
*/
|
|
34
59
|
async function detectEngine() {
|
|
35
60
|
for (const id of ['docker', 'podman']) {
|
|
36
|
-
const
|
|
37
|
-
if (!
|
|
61
|
+
const v = await run(id, ['--version']);
|
|
62
|
+
if (!v.ok)
|
|
63
|
+
continue;
|
|
64
|
+
const version = parseEngineVersion(v.out);
|
|
65
|
+
if (!version)
|
|
38
66
|
continue;
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
return { id, version };
|
|
67
|
+
const info = await run(id, ['info', '--format', '{{.ServerVersion}}']);
|
|
68
|
+
if (info.ok)
|
|
69
|
+
return { id, version, running: true };
|
|
70
|
+
return { id, version, running: false, reason: explainUnreachable(info.out, process.platform) };
|
|
42
71
|
}
|
|
43
72
|
return null;
|
|
44
73
|
}
|
|
@@ -8,5 +8,28 @@ export interface InstallPlan {
|
|
|
8
8
|
}
|
|
9
9
|
/** Pure, so the choice is testable on any machine. */
|
|
10
10
|
export declare function installPlan(platform: NodeJS.Platform, isRoot: boolean): InstallPlan;
|
|
11
|
+
/** Docker is there but this user cannot reach its socket. The fix is group
|
|
12
|
+
* membership, not another install — and it needs root, so it takes the same
|
|
13
|
+
* password round trip. */
|
|
14
|
+
export declare function repairPlan(user: string): {
|
|
15
|
+
inner: string;
|
|
16
|
+
manual: string;
|
|
17
|
+
};
|
|
18
|
+
/** Group membership is fixed for FUTURE processes only, so the daemon that
|
|
19
|
+
* asked for the fix still cannot reach the socket until it restarts. */
|
|
20
|
+
export declare const REPAIR_NEEDS_RESTART = true;
|
|
11
21
|
export declare function runningAsRoot(): boolean;
|
|
12
|
-
|
|
22
|
+
/** Build the argv for a privileged install. The password NEVER goes in argv —
|
|
23
|
+
* argv is world-readable through /proc on Linux, so anyone with a shell on
|
|
24
|
+
* the box could read it. `-S` makes sudo take it on stdin instead, and
|
|
25
|
+
* `-p ''` stops it printing a prompt into the log we stream back. */
|
|
26
|
+
export declare function sudoArgv(inner: string): {
|
|
27
|
+
file: string;
|
|
28
|
+
args: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Install if it is missing; fix permissions if it is merely unreachable.
|
|
31
|
+
* One button, because from the outside they are the same complaint: this
|
|
32
|
+
* machine cannot host desktops. Returns whether a restart is now needed. */
|
|
33
|
+
export declare function installEngine(onLog: (s: string) => void, askPassword?: () => Promise<string | null>, alreadyInstalledButBlocked?: boolean): Promise<{
|
|
34
|
+
needsRestart: boolean;
|
|
35
|
+
}>;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REPAIR_NEEDS_RESTART = void 0;
|
|
3
4
|
exports.installPlan = installPlan;
|
|
5
|
+
exports.repairPlan = repairPlan;
|
|
4
6
|
exports.runningAsRoot = runningAsRoot;
|
|
7
|
+
exports.sudoArgv = sudoArgv;
|
|
5
8
|
exports.installEngine = installEngine;
|
|
6
9
|
// Install a container engine on this machine, on request.
|
|
7
10
|
//
|
|
@@ -36,27 +39,95 @@ function installPlan(platform, isRoot) {
|
|
|
36
39
|
manual: 'curl -fsSL https://get.docker.com | sudo sh', needsRoot: true,
|
|
37
40
|
};
|
|
38
41
|
}
|
|
42
|
+
/** Docker is there but this user cannot reach its socket. The fix is group
|
|
43
|
+
* membership, not another install — and it needs root, so it takes the same
|
|
44
|
+
* password round trip. */
|
|
45
|
+
function repairPlan(user) {
|
|
46
|
+
const u = user.replace(/[^A-Za-z0-9._-]/g, '');
|
|
47
|
+
return {
|
|
48
|
+
inner: `usermod -aG docker ${u}`,
|
|
49
|
+
manual: `sudo usermod -aG docker ${u}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Group membership is fixed for FUTURE processes only, so the daemon that
|
|
53
|
+
* asked for the fix still cannot reach the socket until it restarts. */
|
|
54
|
+
exports.REPAIR_NEEDS_RESTART = true;
|
|
39
55
|
function runningAsRoot() {
|
|
40
56
|
return typeof process.getuid === 'function' && process.getuid() === 0;
|
|
41
57
|
}
|
|
42
|
-
|
|
58
|
+
/** Build the argv for a privileged install. The password NEVER goes in argv —
|
|
59
|
+
* argv is world-readable through /proc on Linux, so anyone with a shell on
|
|
60
|
+
* the box could read it. `-S` makes sudo take it on stdin instead, and
|
|
61
|
+
* `-p ''` stops it printing a prompt into the log we stream back. */
|
|
62
|
+
function sudoArgv(inner) {
|
|
63
|
+
return { file: 'sudo', args: ['-S', '-p', '', 'sh', '-c', inner] };
|
|
64
|
+
}
|
|
65
|
+
/** Install if it is missing; fix permissions if it is merely unreachable.
|
|
66
|
+
* One button, because from the outside they are the same complaint: this
|
|
67
|
+
* machine cannot host desktops. Returns whether a restart is now needed. */
|
|
68
|
+
async function installEngine(onLog, askPassword, alreadyInstalledButBlocked) {
|
|
43
69
|
const plan = installPlan(process.platform, runningAsRoot());
|
|
44
|
-
|
|
70
|
+
let file = plan.file;
|
|
71
|
+
let args = plan.args;
|
|
72
|
+
let password = null;
|
|
73
|
+
// Docker present but the socket is refused: repair, do not reinstall.
|
|
74
|
+
const repair = alreadyInstalledButBlocked
|
|
75
|
+
? repairPlan(process.env.USER || process.env.LOGNAME || 'root')
|
|
76
|
+
: null;
|
|
77
|
+
if (repair) {
|
|
78
|
+
if (runningAsRoot()) {
|
|
79
|
+
onLog(`fixing permissions\n ${repair.inner}\n\n`);
|
|
80
|
+
file = 'sh';
|
|
81
|
+
args = ['-c', repair.inner];
|
|
82
|
+
}
|
|
83
|
+
else if (askPassword) {
|
|
84
|
+
onLog(`This machine needs a sudo password to fix Docker permissions.\n ${repair.manual}\n\n`);
|
|
85
|
+
password = await askPassword();
|
|
86
|
+
if (!password)
|
|
87
|
+
throw new Error(`No password given. Run it on the machine instead:\n ${repair.manual}`);
|
|
88
|
+
const sudo = sudoArgv(repair.inner);
|
|
89
|
+
file = sudo.file;
|
|
90
|
+
args = sudo.args;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
throw new Error(`Needs root. Run this on the machine:\n ${repair.manual}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!file && plan.needsRoot && askPassword) {
|
|
97
|
+
onLog('This machine needs a sudo password to install Docker.\n');
|
|
98
|
+
password = await askPassword();
|
|
99
|
+
if (!password) {
|
|
100
|
+
throw new Error(`No password given. Run it on the machine instead:\n ${plan.manual}`);
|
|
101
|
+
}
|
|
102
|
+
const sudo = sudoArgv(plan.args[1] ?? '');
|
|
103
|
+
file = sudo.file;
|
|
104
|
+
args = sudo.args;
|
|
105
|
+
}
|
|
106
|
+
if (!file) {
|
|
45
107
|
throw new Error(`This needs root, and the node is not running as root. Run this on ${require('os').hostname()}:\n ${plan.manual}`);
|
|
46
108
|
}
|
|
47
109
|
onLog(`installing a container engine\n ${plan.manual}\n\n`);
|
|
48
|
-
const inv = (0, win_1.resolveCliInvocation)(
|
|
110
|
+
const inv = (0, win_1.resolveCliInvocation)(file, args);
|
|
49
111
|
await new Promise((resolve, reject) => {
|
|
50
112
|
const child = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
51
113
|
env: process.env, windowsHide: true, timeout: 25 * 60_000,
|
|
114
|
+
stdio: password ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
|
52
115
|
});
|
|
116
|
+
if (password) {
|
|
117
|
+
// Straight down stdin and gone. It is never echoed, never logged, and
|
|
118
|
+
// the caller wipes it from the row as soon as it reaches here.
|
|
119
|
+
child.stdin?.write(`${password}\n`);
|
|
120
|
+
child.stdin?.end();
|
|
121
|
+
password = null;
|
|
122
|
+
}
|
|
53
123
|
let tail = '';
|
|
54
124
|
const cap = (b) => { const s = b.toString('utf8'); tail = (tail + s).slice(-2000); onLog(s); };
|
|
55
125
|
child.stdout?.on('data', cap);
|
|
56
126
|
child.stderr?.on('data', cap);
|
|
57
|
-
child.on('error', err => reject(new Error(`${
|
|
127
|
+
child.on('error', err => reject(new Error(`${file} is not available here. Install it by hand:\n ${plan.manual}\n(${err.message})`)));
|
|
58
128
|
child.on('exit', code => code === 0
|
|
59
129
|
? resolve()
|
|
60
|
-
: reject(new Error(
|
|
130
|
+
: reject(new Error(`${repair ? 'permission fix' : 'install'} failed (exit ${code}). Try by hand:\n ${repair ? repair.manual : plan.manual}\n${tail.slice(-400)}`)));
|
|
61
131
|
});
|
|
132
|
+
return { needsRestart: !!repair && exports.REPAIR_NEEDS_RESTART };
|
|
62
133
|
}
|
|
@@ -2,6 +2,11 @@ import { DesktopRow } from './spec';
|
|
|
2
2
|
export interface EngineInfo {
|
|
3
3
|
id: 'docker' | 'podman';
|
|
4
4
|
version: string;
|
|
5
|
+
/** The CLI exists AND its daemon answers. False means installed but
|
|
6
|
+
* unusable — a different problem, with a different fix, than missing. */
|
|
7
|
+
running: boolean;
|
|
8
|
+
/** Set when running is false: what to do about it, in words. */
|
|
9
|
+
reason?: string;
|
|
5
10
|
}
|
|
6
11
|
export interface ExecOpts {
|
|
7
12
|
cwd: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|