@addai/node 0.5.1 → 0.7.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/autostart-mac.d.ts +62 -0
- package/dist/autostart-mac.js +306 -0
- package/dist/autostart-win.d.ts +46 -0
- package/dist/autostart-win.js +290 -0
- package/dist/autostart.d.ts +81 -0
- package/dist/autostart.js +224 -0
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +87 -1
- package/dist/command-runner.d.ts +3 -0
- package/dist/command-runner.js +63 -7
- package/dist/index.js +34 -12
- package/dist/self-update.d.ts +25 -0
- package/dist/self-update.js +21 -0
- package/dist/tui/dashboard.d.ts +14 -0
- package/dist/tui/dashboard.js +67 -1
- package/dist/tui/data.d.ts +9 -0
- package/dist/tui/request-row.d.ts +1 -1
- package/dist/tui/request-row.js +22 -8
- package/dist/tui/run.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Windows backend: a Task Scheduler logon task.
|
|
3
|
+
//
|
|
4
|
+
// Three shapes were possible and only this one is both hidden and durable:
|
|
5
|
+
//
|
|
6
|
+
// Startup-folder shortcut — trivial, but pops a console window at every
|
|
7
|
+
// logon and nothing brings the node back if it dies.
|
|
8
|
+
// Windows Service — survives with nobody logged in, but runs outside the
|
|
9
|
+
// user's session: no keychain, no user PATH, no ~/.claude. It would boot
|
|
10
|
+
// a node that fails every run.
|
|
11
|
+
// Logon task (this) — runs as the user with an InteractiveToken, so no
|
|
12
|
+
// stored password and no admin rights.
|
|
13
|
+
//
|
|
14
|
+
// The task is registered from generated XML rather than `schtasks` flags,
|
|
15
|
+
// because the settings that decide whether it ever runs are not reachable
|
|
16
|
+
// from the flag form — DisallowStartIfOnBatteries above all, which defaults
|
|
17
|
+
// to TRUE and would leave a laptop node silently never starting.
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.backend = exports.XML_PATH = exports.VBS_PATH = exports.CMD_PATH = exports.STARTUP_DIR = void 0;
|
|
53
|
+
exports.currentUserId = currentUserId;
|
|
54
|
+
exports.buildCmd = buildCmd;
|
|
55
|
+
exports.buildVbs = buildVbs;
|
|
56
|
+
exports.buildTaskXml = buildTaskXml;
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const os = __importStar(require("os"));
|
|
59
|
+
const path = __importStar(require("path"));
|
|
60
|
+
const child_process_1 = require("child_process");
|
|
61
|
+
const autostart_1 = require("./autostart");
|
|
62
|
+
const paths_1 = require("./paths");
|
|
63
|
+
exports.STARTUP_DIR = path.join(paths_1.RUNTIME_HOME, 'startup');
|
|
64
|
+
exports.CMD_PATH = path.join(exports.STARTUP_DIR, 'ainode-startup.cmd');
|
|
65
|
+
exports.VBS_PATH = path.join(exports.STARTUP_DIR, 'ainode-startup.vbs');
|
|
66
|
+
exports.XML_PATH = path.join(exports.STARTUP_DIR, 'ainode-task.xml');
|
|
67
|
+
function xmlEsc(s) {
|
|
68
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
69
|
+
}
|
|
70
|
+
/** The account the task runs as. `DOMAIN\User` when we have a domain — a bare
|
|
71
|
+
* username is ambiguous on a machine joined to one. */
|
|
72
|
+
function currentUserId(env = process.env) {
|
|
73
|
+
const user = env.USERNAME || os.userInfo().username;
|
|
74
|
+
const dom = env.USERDOMAIN;
|
|
75
|
+
return dom && dom.toLowerCase() !== user.toLowerCase() ? `${dom}\\${user}` : user;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The batch file that actually starts the node.
|
|
79
|
+
*
|
|
80
|
+
* Environment and output redirection live here, in a file where batch quoting
|
|
81
|
+
* behaves, instead of being wedged into a VBS string or a `schtasks` argument.
|
|
82
|
+
* The redirect matters: headless stdout on Windows has nowhere else to go, and
|
|
83
|
+
* a node whose only account of itself is missing is a node nobody can debug.
|
|
84
|
+
*/
|
|
85
|
+
function buildCmd(entry, env, logPath) {
|
|
86
|
+
const sets = Object.entries(env)
|
|
87
|
+
// `set "K=V"` quotes the whole assignment, which is the form that survives
|
|
88
|
+
// a value containing spaces, `&` or `(` — all of which appear in a real
|
|
89
|
+
// Windows PATH (`C:\Program Files (x86)\…`).
|
|
90
|
+
.map(([k, v]) => `set "${k}=${v}"`)
|
|
91
|
+
.join('\r\n');
|
|
92
|
+
const cmd = [entry.file, ...entry.args].map(a => `"${a}"`).join(' ');
|
|
93
|
+
return [
|
|
94
|
+
'@echo off',
|
|
95
|
+
'rem Generated by `ainode startup enable` — edits here are overwritten.',
|
|
96
|
+
sets,
|
|
97
|
+
`${cmd} >> "${logPath}" 2>&1`,
|
|
98
|
+
'',
|
|
99
|
+
].join('\r\n');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The window-hiding shim.
|
|
103
|
+
*
|
|
104
|
+
* A task running under an InteractiveToken shows a console window for every
|
|
105
|
+
* launch; with the logon trigger repeating, that would be a window popping up
|
|
106
|
+
* on the user's desktop every five minutes. `WScript.Shell.Run(…, 0, False)`
|
|
107
|
+
* starts the batch file with no window and does not wait for it.
|
|
108
|
+
*/
|
|
109
|
+
function buildVbs(cmdPath) {
|
|
110
|
+
const quoted = cmdPath.replace(/"/g, '""');
|
|
111
|
+
return [
|
|
112
|
+
"' Generated by `ainode startup enable` — edits here are overwritten.",
|
|
113
|
+
'Dim sh',
|
|
114
|
+
'Set sh = CreateObject("WScript.Shell")',
|
|
115
|
+
`sh.Run """${quoted}""", 0, False`,
|
|
116
|
+
'',
|
|
117
|
+
].join('\r\n');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The task definition.
|
|
121
|
+
*
|
|
122
|
+
* The repetition is what stands in for launchd's KeepAlive. Task Scheduler
|
|
123
|
+
* cannot supervise this process — the shim returns instantly, so the task is
|
|
124
|
+
* "finished" a second after it starts and RestartOnFailure never applies. So
|
|
125
|
+
* the logon trigger repeats every five minutes and the action is
|
|
126
|
+
* `run --ensure`, which exits immediately when a live daemon already owns the
|
|
127
|
+
* node. Crash recovery therefore costs at most five minutes and never stacks
|
|
128
|
+
* a second daemon.
|
|
129
|
+
*/
|
|
130
|
+
function buildTaskXml(spec) {
|
|
131
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
132
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
133
|
+
<RegistrationInfo>
|
|
134
|
+
<Date>${xmlEsc(spec.createdAt)}</Date>
|
|
135
|
+
<Author>@addai/node</Author>
|
|
136
|
+
<Description>Runs this +Ai Node so entities can reach it. Created by \`ainode startup enable\`.</Description>
|
|
137
|
+
</RegistrationInfo>
|
|
138
|
+
<Triggers>
|
|
139
|
+
<LogonTrigger>
|
|
140
|
+
<Enabled>true</Enabled>
|
|
141
|
+
<UserId>${xmlEsc(spec.userId)}</UserId>
|
|
142
|
+
<Repetition>
|
|
143
|
+
<Interval>PT5M</Interval>
|
|
144
|
+
<StopAtDurationEnd>false</StopAtDurationEnd>
|
|
145
|
+
</Repetition>
|
|
146
|
+
</LogonTrigger>
|
|
147
|
+
</Triggers>
|
|
148
|
+
<Principals>
|
|
149
|
+
<Principal id="Author">
|
|
150
|
+
<UserId>${xmlEsc(spec.userId)}</UserId>
|
|
151
|
+
<LogonType>InteractiveToken</LogonType>
|
|
152
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
153
|
+
</Principal>
|
|
154
|
+
</Principals>
|
|
155
|
+
<Settings>
|
|
156
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
157
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
158
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
159
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
160
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
161
|
+
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
162
|
+
<IdleSettings>
|
|
163
|
+
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
164
|
+
<RestartOnIdle>false</RestartOnIdle>
|
|
165
|
+
</IdleSettings>
|
|
166
|
+
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
167
|
+
<Enabled>true</Enabled>
|
|
168
|
+
<Hidden>false</Hidden>
|
|
169
|
+
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
170
|
+
<WakeToRun>false</WakeToRun>
|
|
171
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
172
|
+
<Priority>7</Priority>
|
|
173
|
+
</Settings>
|
|
174
|
+
<Actions Context="Author">
|
|
175
|
+
<Exec>
|
|
176
|
+
<Command>${xmlEsc(spec.wscriptPath)}</Command>
|
|
177
|
+
<Arguments>"${xmlEsc(spec.vbsPath)}"</Arguments>
|
|
178
|
+
</Exec>
|
|
179
|
+
</Actions>
|
|
180
|
+
</Task>
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
function schtasks(args) {
|
|
184
|
+
try {
|
|
185
|
+
const out = (0, child_process_1.execFileSync)('schtasks', args, { encoding: 'utf8', timeout: 30_000, windowsHide: true });
|
|
186
|
+
return { ok: true, output: out.toString().trim() };
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
const e = err;
|
|
190
|
+
const text = [e.stderr?.toString(), e.stdout?.toString()].filter(Boolean).join(' ').trim();
|
|
191
|
+
return { ok: false, output: text || e.message || 'schtasks failed' };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/* `schtasks /Query` costs a process spawn, and status() rides every heartbeat.
|
|
195
|
+
* Cache the answer briefly; enable/disable clear it so the CLI and the TUI
|
|
196
|
+
* never show a stale yes/no straight after a change. */
|
|
197
|
+
let queryCache = null;
|
|
198
|
+
const QUERY_TTL_MS = 5 * 60_000;
|
|
199
|
+
function taskExists() {
|
|
200
|
+
if (queryCache && Date.now() - queryCache.at < QUERY_TTL_MS)
|
|
201
|
+
return queryCache.exists;
|
|
202
|
+
const r = schtasks(['/Query', '/TN', autostart_1.TASK_NAME]);
|
|
203
|
+
queryCache = { at: Date.now(), exists: r.ok };
|
|
204
|
+
return r.ok;
|
|
205
|
+
}
|
|
206
|
+
function wscriptPath(env = process.env) {
|
|
207
|
+
const root = env.SystemRoot || 'C:\\Windows';
|
|
208
|
+
return path.join(root, 'System32', 'wscript.exe');
|
|
209
|
+
}
|
|
210
|
+
// Windows has no equivalent of macOS's protected folders, so `force` has
|
|
211
|
+
// nothing to override here — the parameter exists to satisfy one interface.
|
|
212
|
+
function enable(entry, env, logPath) {
|
|
213
|
+
const warnings = [];
|
|
214
|
+
fs.mkdirSync(exports.STARTUP_DIR, { recursive: true });
|
|
215
|
+
// `--ensure` is what makes the five-minute repeat safe: it stands down
|
|
216
|
+
// instantly when a daemon already owns the lockfile.
|
|
217
|
+
const runEntry = { file: entry.file, args: [...entry.args, 'run', '--ensure'] };
|
|
218
|
+
fs.writeFileSync(exports.CMD_PATH, buildCmd(runEntry, env, logPath), 'utf8');
|
|
219
|
+
fs.writeFileSync(exports.VBS_PATH, buildVbs(exports.CMD_PATH), 'utf8');
|
|
220
|
+
const xml = buildTaskXml({
|
|
221
|
+
userId: currentUserId(),
|
|
222
|
+
vbsPath: exports.VBS_PATH,
|
|
223
|
+
wscriptPath: wscriptPath(),
|
|
224
|
+
createdAt: new Date().toISOString().replace(/\.\d+Z$/, ''),
|
|
225
|
+
});
|
|
226
|
+
// schtasks reads task XML as Unicode. Handed a UTF-8 file it fails with
|
|
227
|
+
// "The task XML contains a value which is incorrectly formatted or out of
|
|
228
|
+
// range" — an error that says nothing about encoding and has cost people
|
|
229
|
+
// hours. UTF-16LE with a BOM is what it wants.
|
|
230
|
+
fs.writeFileSync(exports.XML_PATH, Buffer.from('\uFEFF' + xml, 'utf16le'));
|
|
231
|
+
const created = schtasks(['/Create', '/TN', autostart_1.TASK_NAME, '/XML', exports.XML_PATH, '/F']);
|
|
232
|
+
queryCache = null;
|
|
233
|
+
if (!created.ok) {
|
|
234
|
+
return { ok: false, path: exports.XML_PATH, warnings, error: `schtasks could not register the task: ${created.output}` };
|
|
235
|
+
}
|
|
236
|
+
// Registering does not run it — the logon trigger has already passed for
|
|
237
|
+
// this session. Start it now so enabling has a visible effect today; the
|
|
238
|
+
// action is `--ensure`, so this is a no-op when a node is already up.
|
|
239
|
+
const ran = schtasks(['/Run', '/TN', autostart_1.TASK_NAME]);
|
|
240
|
+
if (!ran.ok)
|
|
241
|
+
warnings.push(`task registered but would not start now (${ran.output}); it will start at your next logon`);
|
|
242
|
+
return { ok: true, path: exports.XML_PATH, warnings };
|
|
243
|
+
}
|
|
244
|
+
function disable() {
|
|
245
|
+
const r = schtasks(['/Delete', '/TN', autostart_1.TASK_NAME, '/F']);
|
|
246
|
+
queryCache = null;
|
|
247
|
+
for (const f of [exports.CMD_PATH, exports.VBS_PATH, exports.XML_PATH]) {
|
|
248
|
+
try {
|
|
249
|
+
fs.unlinkSync(f);
|
|
250
|
+
}
|
|
251
|
+
catch { /* already gone */ }
|
|
252
|
+
}
|
|
253
|
+
// A missing task is the desired end state, not a failure.
|
|
254
|
+
if (!r.ok && !/cannot find|does not exist/i.test(r.output)) {
|
|
255
|
+
return { ok: false, error: r.output };
|
|
256
|
+
}
|
|
257
|
+
return { ok: true };
|
|
258
|
+
}
|
|
259
|
+
function status() {
|
|
260
|
+
const exists = taskExists();
|
|
261
|
+
if (!exists)
|
|
262
|
+
return { supported: true, enabled: false, supervisor: 'schtasks' };
|
|
263
|
+
const issues = [];
|
|
264
|
+
let entry;
|
|
265
|
+
try {
|
|
266
|
+
const body = fs.readFileSync(exports.CMD_PATH, 'utf8');
|
|
267
|
+
const line = body.split(/\r?\n/).find(l => l.trim().startsWith('"'));
|
|
268
|
+
if (line) {
|
|
269
|
+
const parts = [...line.matchAll(/"([^"]+)"/g)].map(m => m[1]);
|
|
270
|
+
// Last quoted token is the log path from the redirect, not an argument.
|
|
271
|
+
if (parts.length >= 2)
|
|
272
|
+
entry = { file: parts[0], args: parts.slice(1, -1) };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
issues.push('the task exists but its launcher script is missing — re-run `ainode startup enable`');
|
|
277
|
+
}
|
|
278
|
+
if (entry && !fs.existsSync(entry.args[0] ?? entry.file)) {
|
|
279
|
+
issues.push(`the pinned program is gone (${entry.args[0] ?? entry.file}) — re-run \`ainode startup enable\``);
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
supported: true,
|
|
283
|
+
enabled: true,
|
|
284
|
+
supervisor: 'schtasks',
|
|
285
|
+
entry,
|
|
286
|
+
path: exports.XML_PATH,
|
|
287
|
+
...(issues.length ? { issues } : {}),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
exports.backend = { supervisor: 'schtasks', status, enable, disable };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type Supervisor = 'launchd' | 'schtasks';
|
|
2
|
+
/** The exact command a supervisor is told to run. */
|
|
3
|
+
export interface AutostartEntry {
|
|
4
|
+
file: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface AutostartState {
|
|
8
|
+
/** False on platforms we don't manage (Linux, anything exotic). */
|
|
9
|
+
supported: boolean;
|
|
10
|
+
/** An entry exists AND its pinned program is still on disk. */
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
supervisor: Supervisor | null;
|
|
13
|
+
entry?: AutostartEntry;
|
|
14
|
+
/** Where the plist / task definition lives, for the user to go look. */
|
|
15
|
+
path?: string;
|
|
16
|
+
/** Non-fatal problems worth surfacing — chiefly a pinned path that rotted. */
|
|
17
|
+
issues?: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface EnableResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
path?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
warnings: string[];
|
|
24
|
+
}
|
|
25
|
+
export interface DisableResult {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface EnableOptions {
|
|
30
|
+
/** Arm it anyway when a platform check says it will not work. Exists for
|
|
31
|
+
* one case: a Mac whose `node` has been granted Full Disk Access, where
|
|
32
|
+
* our ~/Documents refusal is the wrong answer. */
|
|
33
|
+
force?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** What each platform backend must provide. */
|
|
36
|
+
export interface AutostartBackend {
|
|
37
|
+
supervisor: Supervisor;
|
|
38
|
+
status(): AutostartState;
|
|
39
|
+
enable(entry: AutostartEntry, env: Record<string, string>, logPath: string, opts: EnableOptions): EnableResult;
|
|
40
|
+
disable(): DisableResult;
|
|
41
|
+
}
|
|
42
|
+
export declare const LABEL = "ai.add.node";
|
|
43
|
+
/** Task Scheduler's name for the same thing. Space-free: `schtasks` quoting
|
|
44
|
+
* through cmd.exe is a known source of Windows breakage in this codebase. */
|
|
45
|
+
export declare const TASK_NAME = "AiNode";
|
|
46
|
+
export interface ResolvedEntry {
|
|
47
|
+
entry?: AutostartEntry;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The command to write into the supervisor's definition.
|
|
52
|
+
*
|
|
53
|
+
* Absolute node + absolute script, never a shim name. launchd hands a job
|
|
54
|
+
* `PATH=/usr/bin:/bin:/usr/sbin:/sbin`, where neither `node` nor `ainode`
|
|
55
|
+
* exists on any machine of ours — the job would fail to launch at every login
|
|
56
|
+
* and the only evidence would be a launchd error nobody reads.
|
|
57
|
+
*
|
|
58
|
+
* npx is refused outright. An `_npx/<hash>/` directory is a throwaway cache
|
|
59
|
+
* pinned to ONE version; writing it into a plist relaunches that version at
|
|
60
|
+
* every login forever, which is the pin trap we have already been bitten by.
|
|
61
|
+
* `global` and `source` both pin to a stable path: npm overwrites a global
|
|
62
|
+
* install in place, and a checkout is a checkout.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveEntry(scriptPath?: string, execPath?: string, exists?: (p: string) => boolean): ResolvedEntry;
|
|
65
|
+
/**
|
|
66
|
+
* Environment the supervised daemon starts with.
|
|
67
|
+
*
|
|
68
|
+
* PATH is captured from the live shell at enable time and frozen into the
|
|
69
|
+
* definition. This is the single most important line in the feature: without
|
|
70
|
+
* it the node boots under launchd's minimal PATH and cannot find `claude`,
|
|
71
|
+
* `codex`, `git` or any harness — an node that is "online" and fails
|
|
72
|
+
* everything is worse than one that is plainly offline.
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildEnv(supervisor: Supervisor, env?: NodeJS.ProcessEnv, home?: string): Record<string, string>;
|
|
75
|
+
export declare function status(): AutostartState;
|
|
76
|
+
export declare function enable(opts?: EnableOptions): EnableResult;
|
|
77
|
+
export declare function disable(): DisableResult;
|
|
78
|
+
/** Is a supervisor going to restart us if we exit? Decides whether a remote
|
|
79
|
+
* roll self-respawns or simply stands down and lets the supervisor act. */
|
|
80
|
+
export declare function isSupervised(state?: AutostartState, env?: NodeJS.ProcessEnv): boolean;
|
|
81
|
+
export declare function describeStatus(s: AutostartState): string;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Start this node when the machine starts.
|
|
3
|
+
//
|
|
4
|
+
// Until now a node only ran while somebody kept a terminal open. Reboot the
|
|
5
|
+
// machine, close the window, or miss a crash, and every entity pinned to that
|
|
6
|
+
// node sat at "Getting ready…" until a human walked over and typed `ainode`.
|
|
7
|
+
// self-update.ts says it outright: "The daemon is NOT supervised on any of our
|
|
8
|
+
// nodes — nothing restarts it if it merely exits."
|
|
9
|
+
//
|
|
10
|
+
// This hands supervision to the OS: launchd on macOS, Task Scheduler on
|
|
11
|
+
// Windows. Both run the daemon AS THE USER at login — never as a system
|
|
12
|
+
// service. The daemon needs the login keychain (Claude/Codex auth), the user's
|
|
13
|
+
// PATH (Homebrew, nvm, ~/.local/bin) and ~/.claude / ~/.codex. A root
|
|
14
|
+
// LaunchDaemon or a Windows Service has none of those, so it would faithfully
|
|
15
|
+
// boot a node that then fails every single run.
|
|
16
|
+
//
|
|
17
|
+
// The cost of that choice, stated plainly: a machine that reboots unattended
|
|
18
|
+
// only comes back once somebody logs in, unless auto-login is on.
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.TASK_NAME = exports.LABEL = void 0;
|
|
54
|
+
exports.resolveEntry = resolveEntry;
|
|
55
|
+
exports.buildEnv = buildEnv;
|
|
56
|
+
exports.status = status;
|
|
57
|
+
exports.enable = enable;
|
|
58
|
+
exports.disable = disable;
|
|
59
|
+
exports.isSupervised = isSupervised;
|
|
60
|
+
exports.describeStatus = describeStatus;
|
|
61
|
+
const os = __importStar(require("os"));
|
|
62
|
+
const fs = __importStar(require("fs"));
|
|
63
|
+
const self_update_1 = require("./self-update");
|
|
64
|
+
const lockfile_1 = require("./lockfile");
|
|
65
|
+
const paths_1 = require("./paths");
|
|
66
|
+
exports.LABEL = 'ai.add.node';
|
|
67
|
+
/** Task Scheduler's name for the same thing. Space-free: `schtasks` quoting
|
|
68
|
+
* through cmd.exe is a known source of Windows breakage in this codebase. */
|
|
69
|
+
exports.TASK_NAME = 'AiNode';
|
|
70
|
+
/**
|
|
71
|
+
* The command to write into the supervisor's definition.
|
|
72
|
+
*
|
|
73
|
+
* Absolute node + absolute script, never a shim name. launchd hands a job
|
|
74
|
+
* `PATH=/usr/bin:/bin:/usr/sbin:/sbin`, where neither `node` nor `ainode`
|
|
75
|
+
* exists on any machine of ours — the job would fail to launch at every login
|
|
76
|
+
* and the only evidence would be a launchd error nobody reads.
|
|
77
|
+
*
|
|
78
|
+
* npx is refused outright. An `_npx/<hash>/` directory is a throwaway cache
|
|
79
|
+
* pinned to ONE version; writing it into a plist relaunches that version at
|
|
80
|
+
* every login forever, which is the pin trap we have already been bitten by.
|
|
81
|
+
* `global` and `source` both pin to a stable path: npm overwrites a global
|
|
82
|
+
* install in place, and a checkout is a checkout.
|
|
83
|
+
*/
|
|
84
|
+
function resolveEntry(scriptPath = (0, lockfile_1.entryScript)(), execPath = process.execPath, exists = p => fs.existsSync(p)) {
|
|
85
|
+
if (!scriptPath) {
|
|
86
|
+
return { error: 'cannot tell which script this daemon is running' };
|
|
87
|
+
}
|
|
88
|
+
const mode = (0, self_update_1.detectLaunchMode)(scriptPath);
|
|
89
|
+
if (mode === 'npx') {
|
|
90
|
+
return {
|
|
91
|
+
error: `this node runs from an npx cache, which is thrown away and pinned to one version.\n`
|
|
92
|
+
+ `Install it properly first, then re-run:\n\n npm i -g ${self_update_1.PACKAGE_NAME}\n`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// realpath so a symlinked global bin resolves to the file npm actually
|
|
96
|
+
// overwrites on upgrade — pinning the symlink would be fine too, but the
|
|
97
|
+
// real path is what the lockfile records, so the two agree.
|
|
98
|
+
const real = (() => {
|
|
99
|
+
try {
|
|
100
|
+
return fs.realpathSync(scriptPath);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return scriptPath;
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
if (!exists(real)) {
|
|
107
|
+
return { error: `entry script does not exist: ${real}` };
|
|
108
|
+
}
|
|
109
|
+
return { entry: { file: execPath, args: [real] } };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Environment the supervised daemon starts with.
|
|
113
|
+
*
|
|
114
|
+
* PATH is captured from the live shell at enable time and frozen into the
|
|
115
|
+
* definition. This is the single most important line in the feature: without
|
|
116
|
+
* it the node boots under launchd's minimal PATH and cannot find `claude`,
|
|
117
|
+
* `codex`, `git` or any harness — an node that is "online" and fails
|
|
118
|
+
* everything is worse than one that is plainly offline.
|
|
119
|
+
*/
|
|
120
|
+
function buildEnv(supervisor, env = process.env, home = os.homedir()) {
|
|
121
|
+
const out = {
|
|
122
|
+
PATH: env.PATH || '',
|
|
123
|
+
HOME: env.HOME || home,
|
|
124
|
+
// Supervised output goes to a log file, so escape sequences would be
|
|
125
|
+
// noise at best. cli.ts already falls back to plain lines when not a TTY;
|
|
126
|
+
// this makes it explicit and survives a supervisor that fakes a TTY.
|
|
127
|
+
AINODE_NO_TUI: '1',
|
|
128
|
+
AINODE_SUPERVISOR: supervisor,
|
|
129
|
+
};
|
|
130
|
+
if (env.LANG)
|
|
131
|
+
out.LANG = env.LANG;
|
|
132
|
+
if (process.platform === 'win32') {
|
|
133
|
+
if (env.USERPROFILE)
|
|
134
|
+
out.USERPROFILE = env.USERPROFILE;
|
|
135
|
+
if (env.APPDATA)
|
|
136
|
+
out.APPDATA = env.APPDATA;
|
|
137
|
+
if (env.LOCALAPPDATA)
|
|
138
|
+
out.LOCALAPPDATA = env.LOCALAPPDATA;
|
|
139
|
+
if (env.SystemRoot)
|
|
140
|
+
out.SystemRoot = env.SystemRoot;
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/* ── platform dispatch ────────────────────────────────────────────────── */
|
|
145
|
+
function backend(platform = process.platform) {
|
|
146
|
+
// Required lazily: each backend reaches for platform tools at module scope
|
|
147
|
+
// in tests, and loading the Windows one on a Mac (or vice versa) has no
|
|
148
|
+
// reason to happen at all.
|
|
149
|
+
if (platform === 'darwin') {
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
151
|
+
return require('./autostart-mac').backend;
|
|
152
|
+
}
|
|
153
|
+
if (platform === 'win32') {
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
155
|
+
return require('./autostart-win').backend;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const UNSUPPORTED = { supported: false, enabled: false, supervisor: null };
|
|
160
|
+
function status() {
|
|
161
|
+
const b = backend();
|
|
162
|
+
if (!b)
|
|
163
|
+
return UNSUPPORTED;
|
|
164
|
+
try {
|
|
165
|
+
return b.status();
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
// Reading state must never throw into the heartbeat — capabilities go out
|
|
169
|
+
// every few seconds and one bad plist would take the node offline.
|
|
170
|
+
return { supported: true, enabled: false, supervisor: b.supervisor, issues: [err.message] };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function enable(opts = {}) {
|
|
174
|
+
const b = backend();
|
|
175
|
+
if (!b) {
|
|
176
|
+
return { ok: false, warnings: [], error: `starting at login is not supported on ${process.platform} yet` };
|
|
177
|
+
}
|
|
178
|
+
const resolved = resolveEntry();
|
|
179
|
+
if (!resolved.entry)
|
|
180
|
+
return { ok: false, warnings: [], error: resolved.error };
|
|
181
|
+
try {
|
|
182
|
+
return b.enable(resolved.entry, buildEnv(b.supervisor), paths_1.RUNTIME_LOG_FILE, opts);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return { ok: false, warnings: [], error: err.message };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function disable() {
|
|
189
|
+
const b = backend();
|
|
190
|
+
if (!b)
|
|
191
|
+
return { ok: true }; // nothing was ever installed; nothing to remove
|
|
192
|
+
try {
|
|
193
|
+
return b.disable();
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
return { ok: false, error: err.message };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Is a supervisor going to restart us if we exit? Decides whether a remote
|
|
200
|
+
* roll self-respawns or simply stands down and lets the supervisor act. */
|
|
201
|
+
function isSupervised(state = status(), env = process.env) {
|
|
202
|
+
// launchd is a live supervisor: KeepAlive brings the job straight back.
|
|
203
|
+
// Task Scheduler is not — its logon trigger has already fired, and the
|
|
204
|
+
// 5-minute repeat is a backstop measured in minutes, not a restart path.
|
|
205
|
+
// So only launchd earns exit-and-be-restarted.
|
|
206
|
+
if (env.AINODE_SUPERVISOR === 'launchd')
|
|
207
|
+
return true;
|
|
208
|
+
return state.supervisor === 'launchd' && state.enabled;
|
|
209
|
+
}
|
|
210
|
+
/* ── human-readable state, shared by the CLI and the TUI ──────────────── */
|
|
211
|
+
function describeStatus(s) {
|
|
212
|
+
if (!s.supported)
|
|
213
|
+
return `Starts at login: not supported on ${process.platform}`;
|
|
214
|
+
const lines = [
|
|
215
|
+
`Starts at login: ${s.enabled ? 'yes' : 'no'}${s.supervisor ? ` (${s.supervisor})` : ''}`,
|
|
216
|
+
];
|
|
217
|
+
if (s.path)
|
|
218
|
+
lines.push(` definition: ${s.path}`);
|
|
219
|
+
if (s.entry)
|
|
220
|
+
lines.push(` runs: ${[s.entry.file, ...s.entry.args].join(' ')}`);
|
|
221
|
+
for (const issue of s.issues ?? [])
|
|
222
|
+
lines.push(` ! ${issue}`);
|
|
223
|
+
return lines.join('\n');
|
|
224
|
+
}
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AutostartState } from './autostart';
|
|
1
2
|
interface AuthShape {
|
|
2
3
|
authed: boolean;
|
|
3
4
|
account?: string;
|
|
@@ -37,6 +38,10 @@ interface CapabilitiesShape {
|
|
|
37
38
|
models?: string[];
|
|
38
39
|
};
|
|
39
40
|
git: BinaryProbe;
|
|
41
|
+
/** Does this node come back on its own after a reboot? Rides the heartbeat
|
|
42
|
+
* rather than a column of its own — same channel Studio already reads the
|
|
43
|
+
* harness grid from. */
|
|
44
|
+
autostart?: AutostartState;
|
|
40
45
|
}
|
|
41
46
|
/** Codex's credential store: $CODEX_HOME (or ~/.codex) + /auth.json. */
|
|
42
47
|
export declare function codexHome(): string;
|
package/dist/capabilities.js
CHANGED
|
@@ -52,6 +52,7 @@ const grok_binary_1 = require("./grok-binary");
|
|
|
52
52
|
const codex_binary_1 = require("./codex-binary");
|
|
53
53
|
const win_1 = require("./win");
|
|
54
54
|
const harness_registry_1 = require("./harness-registry");
|
|
55
|
+
const autostart_1 = require("./autostart");
|
|
55
56
|
function readDaemonVersion() {
|
|
56
57
|
try {
|
|
57
58
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -396,6 +397,7 @@ async function probeCapabilities() {
|
|
|
396
397
|
});
|
|
397
398
|
return {
|
|
398
399
|
daemon_version: readDaemonVersion(),
|
|
400
|
+
autostart: (0, autostart_1.status)(),
|
|
399
401
|
claude: deco('claude', claude),
|
|
400
402
|
codex: deco('codex', codex),
|
|
401
403
|
kimi: deco('kimi', kimi),
|