@addai/node 0.16.0 → 0.18.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 +29 -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 +9 -1
- package/dist/desktop/install-engine.js +33 -4
- package/dist/desktop/provider.d.ts +5 -0
- package/package.json +1 -1
package/dist/command-runner.js
CHANGED
|
@@ -692,7 +692,35 @@ async function runInstallEngine(cmd) {
|
|
|
692
692
|
};
|
|
693
693
|
await update(cmd.id, 'running', { log });
|
|
694
694
|
try {
|
|
695
|
-
await (0, install_engine_1.installEngine)(onLog)
|
|
695
|
+
await (0, install_engine_1.installEngine)(onLog, async () => {
|
|
696
|
+
// Park the command and let Studio collect it, exactly as a harness
|
|
697
|
+
// login collects a paste-back code.
|
|
698
|
+
await update(cmd.id, 'awaiting_input', { needs: 'sudo_password', log });
|
|
699
|
+
const deadline = Date.now() + INPUT_WAIT_MS;
|
|
700
|
+
while (Date.now() < deadline) {
|
|
701
|
+
const row = await pollRow(cmd.id);
|
|
702
|
+
if (!row)
|
|
703
|
+
break;
|
|
704
|
+
if (row.status === 'canceled')
|
|
705
|
+
return null;
|
|
706
|
+
const pw = row.input?.sudo_password;
|
|
707
|
+
if (pw) {
|
|
708
|
+
// Wipe it from the row the moment we hold it. It exists in one
|
|
709
|
+
// process's memory from here, and nowhere else.
|
|
710
|
+
const t = token();
|
|
711
|
+
if (t) {
|
|
712
|
+
try {
|
|
713
|
+
await (0, supabase_client_1.rpc)('runtime_command_clear_input', { p_token: t, p_id: cmd.id });
|
|
714
|
+
}
|
|
715
|
+
catch { /* best effort — the install still must not print it */ }
|
|
716
|
+
}
|
|
717
|
+
await update(cmd.id, 'running', { needs: null, log });
|
|
718
|
+
return pw;
|
|
719
|
+
}
|
|
720
|
+
await new Promise(r => setTimeout(r, INPUT_POLL_MS));
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
});
|
|
696
724
|
(0, docker_1.resetProviderCache)();
|
|
697
725
|
const engine = await (0, docker_1.getProvider)(true);
|
|
698
726
|
if (!engine) {
|
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
|
}
|
|
@@ -9,4 +9,12 @@ export interface InstallPlan {
|
|
|
9
9
|
/** Pure, so the choice is testable on any machine. */
|
|
10
10
|
export declare function installPlan(platform: NodeJS.Platform, isRoot: boolean): InstallPlan;
|
|
11
11
|
export declare function runningAsRoot(): boolean;
|
|
12
|
-
|
|
12
|
+
/** Build the argv for a privileged install. The password NEVER goes in argv —
|
|
13
|
+
* argv is world-readable through /proc on Linux, so anyone with a shell on
|
|
14
|
+
* the box could read it. `-S` makes sudo take it on stdin instead, and
|
|
15
|
+
* `-p ''` stops it printing a prompt into the log we stream back. */
|
|
16
|
+
export declare function sudoArgv(inner: string): {
|
|
17
|
+
file: string;
|
|
18
|
+
args: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare function installEngine(onLog: (s: string) => void, askPassword?: () => Promise<string | null>): Promise<void>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.installPlan = installPlan;
|
|
4
4
|
exports.runningAsRoot = runningAsRoot;
|
|
5
|
+
exports.sudoArgv = sudoArgv;
|
|
5
6
|
exports.installEngine = installEngine;
|
|
6
7
|
// Install a container engine on this machine, on request.
|
|
7
8
|
//
|
|
@@ -39,22 +40,50 @@ function installPlan(platform, isRoot) {
|
|
|
39
40
|
function runningAsRoot() {
|
|
40
41
|
return typeof process.getuid === 'function' && process.getuid() === 0;
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
/** Build the argv for a privileged install. The password NEVER goes in argv —
|
|
44
|
+
* argv is world-readable through /proc on Linux, so anyone with a shell on
|
|
45
|
+
* the box could read it. `-S` makes sudo take it on stdin instead, and
|
|
46
|
+
* `-p ''` stops it printing a prompt into the log we stream back. */
|
|
47
|
+
function sudoArgv(inner) {
|
|
48
|
+
return { file: 'sudo', args: ['-S', '-p', '', 'sh', '-c', inner] };
|
|
49
|
+
}
|
|
50
|
+
async function installEngine(onLog, askPassword) {
|
|
43
51
|
const plan = installPlan(process.platform, runningAsRoot());
|
|
44
|
-
|
|
52
|
+
let file = plan.file;
|
|
53
|
+
let args = plan.args;
|
|
54
|
+
let password = null;
|
|
55
|
+
if (!file && plan.needsRoot && askPassword) {
|
|
56
|
+
onLog('This machine needs a sudo password to install Docker.\n');
|
|
57
|
+
password = await askPassword();
|
|
58
|
+
if (!password) {
|
|
59
|
+
throw new Error(`No password given. Run it on the machine instead:\n ${plan.manual}`);
|
|
60
|
+
}
|
|
61
|
+
const sudo = sudoArgv(plan.args[1] ?? '');
|
|
62
|
+
file = sudo.file;
|
|
63
|
+
args = sudo.args;
|
|
64
|
+
}
|
|
65
|
+
if (!file) {
|
|
45
66
|
throw new Error(`This needs root, and the node is not running as root. Run this on ${require('os').hostname()}:\n ${plan.manual}`);
|
|
46
67
|
}
|
|
47
68
|
onLog(`installing a container engine\n ${plan.manual}\n\n`);
|
|
48
|
-
const inv = (0, win_1.resolveCliInvocation)(
|
|
69
|
+
const inv = (0, win_1.resolveCliInvocation)(file, args);
|
|
49
70
|
await new Promise((resolve, reject) => {
|
|
50
71
|
const child = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
51
72
|
env: process.env, windowsHide: true, timeout: 25 * 60_000,
|
|
73
|
+
stdio: password ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
|
52
74
|
});
|
|
75
|
+
if (password) {
|
|
76
|
+
// Straight down stdin and gone. It is never echoed, never logged, and
|
|
77
|
+
// the caller wipes it from the row as soon as it reaches here.
|
|
78
|
+
child.stdin?.write(`${password}\n`);
|
|
79
|
+
child.stdin?.end();
|
|
80
|
+
password = null;
|
|
81
|
+
}
|
|
53
82
|
let tail = '';
|
|
54
83
|
const cap = (b) => { const s = b.toString('utf8'); tail = (tail + s).slice(-2000); onLog(s); };
|
|
55
84
|
child.stdout?.on('data', cap);
|
|
56
85
|
child.stderr?.on('data', cap);
|
|
57
|
-
child.on('error', err => reject(new Error(`${
|
|
86
|
+
child.on('error', err => reject(new Error(`${file} is not available here. Install it by hand:\n ${plan.manual}\n(${err.message})`)));
|
|
58
87
|
child.on('exit', code => code === 0
|
|
59
88
|
? resolve()
|
|
60
89
|
: reject(new Error(`install failed (exit ${code}). Try by hand:\n ${plan.manual}\n${tail.slice(-400)}`)));
|
|
@@ -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.18.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": [
|