aiden-runtime 4.1.5 → 4.6.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 +265 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +536 -152
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +245 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
- package/dist/cli/v4/commands/browserDepth.js +45 -0
- package/dist/cli/v4/commands/cron.js +264 -0
- package/dist/cli/v4/commands/daemon.js +541 -0
- package/dist/cli/v4/commands/daemonStatus.js +253 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +13 -0
- package/dist/cli/v4/commands/index.js +35 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +223 -0
- package/dist/cli/v4/commands/sandbox.js +48 -0
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/commands/suggestions.js +68 -0
- package/dist/cli/v4/commands/tce.js +41 -0
- package/dist/cli/v4/commands/trigger.js +378 -0
- package/dist/cli/v4/commands/update.js +95 -3
- package/dist/cli/v4/daemonAgentBuilder.js +145 -0
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/display/capabilityCard.js +26 -0
- package/dist/cli/v4/display.js +18 -8
- package/dist/cli/v4/replyRenderer.js +31 -23
- package/dist/cli/v4/updateBootPrompt.js +170 -0
- package/dist/core/playwrightBridge.js +129 -0
- package/dist/core/v4/aidenAgent.js +527 -5
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +651 -0
- package/dist/core/v4/daemon/cleanShutdown.js +154 -0
- package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
- package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
- package/dist/core/v4/daemon/cron/migration.js +199 -0
- package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
- package/dist/core/v4/daemon/daemonConfig.js +90 -0
- package/dist/core/v4/daemon/db/connection.js +106 -0
- package/dist/core/v4/daemon/db/migrations.js +362 -0
- package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
- package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
- package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
- package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
- package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
- package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
- package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
- package/dist/core/v4/daemon/dispatcher/index.js +53 -0
- package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
- package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
- package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
- package/dist/core/v4/daemon/drain.js +156 -0
- package/dist/core/v4/daemon/eventLoopLag.js +73 -0
- package/dist/core/v4/daemon/health.js +159 -0
- package/dist/core/v4/daemon/idempotencyStore.js +204 -0
- package/dist/core/v4/daemon/index.js +179 -0
- package/dist/core/v4/daemon/instanceTracker.js +99 -0
- package/dist/core/v4/daemon/resourceRegistry.js +150 -0
- package/dist/core/v4/daemon/restartCode.js +32 -0
- package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
- package/dist/core/v4/daemon/runStore.js +144 -0
- package/dist/core/v4/daemon/runtimeLock.js +167 -0
- package/dist/core/v4/daemon/signals.js +50 -0
- package/dist/core/v4/daemon/supervisor.js +272 -0
- package/dist/core/v4/daemon/triggerBus.js +279 -0
- package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
- package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
- package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
- package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
- package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
- package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
- package/dist/core/v4/daemon/triggers/email/index.js +332 -0
- package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
- package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
- package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
- package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
- package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
- package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
- package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
- package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
- package/dist/core/v4/daemon/triggers/webhook.js +376 -0
- package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
- package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
- package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
- package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
- package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
- package/dist/core/v4/daemon/types.js +15 -0
- package/dist/core/v4/dockerSession.js +461 -0
- package/dist/core/v4/dryRun.js +117 -0
- package/dist/core/v4/failureClassifier.js +779 -0
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +214 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +37 -3
- package/dist/core/v4/turnState.js +587 -0
- package/dist/core/v4/update/checkUpdate.js +63 -3
- package/dist/core/v4/update/installMethodDetect.js +115 -0
- package/dist/core/v4/update/registryClient.js +121 -0
- package/dist/core/v4/update/skipState.js +75 -0
- package/dist/core/v4/verifier.js +448 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/browser/_observer.js +224 -0
- package/dist/tools/v4/browser/browserBlocker.js +396 -0
- package/dist/tools/v4/browser/browserClick.js +18 -1
- package/dist/tools/v4/browser/browserClose.js +18 -1
- package/dist/tools/v4/browser/browserExtract.js +5 -1
- package/dist/tools/v4/browser/browserFill.js +17 -1
- package/dist/tools/v4/browser/browserGetUrl.js +5 -1
- package/dist/tools/v4/browser/browserNavigate.js +16 -1
- package/dist/tools/v4/browser/browserScreenshot.js +5 -1
- package/dist/tools/v4/browser/browserScroll.js +18 -1
- package/dist/tools/v4/browser/browserType.js +17 -1
- package/dist/tools/v4/browser/captchaCheck.js +5 -1
- package/dist/tools/v4/executeCode.js +1 -0
- package/dist/tools/v4/files/fileCopy.js +56 -2
- package/dist/tools/v4/files/fileDelete.js +38 -1
- package/dist/tools/v4/files/fileList.js +12 -1
- package/dist/tools/v4/files/fileMove.js +59 -2
- package/dist/tools/v4/files/filePatch.js +43 -1
- package/dist/tools/v4/files/fileRead.js +12 -1
- package/dist/tools/v4/files/fileWrite.js +41 -1
- package/dist/tools/v4/index.js +88 -61
- package/dist/tools/v4/memory/memoryAdd.js +14 -0
- package/dist/tools/v4/memory/memoryRemove.js +14 -0
- package/dist/tools/v4/memory/memoryReplace.js +15 -0
- package/dist/tools/v4/memory/sessionSummary.js +12 -0
- package/dist/tools/v4/process/processKill.js +19 -0
- package/dist/tools/v4/process/processList.js +1 -0
- package/dist/tools/v4/process/processLogRead.js +1 -0
- package/dist/tools/v4/process/processSpawn.js +13 -0
- package/dist/tools/v4/process/processWait.js +1 -0
- package/dist/tools/v4/sessions/recallSession.js +1 -0
- package/dist/tools/v4/sessions/sessionList.js +1 -0
- package/dist/tools/v4/sessions/sessionSearch.js +1 -0
- package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
- package/dist/tools/v4/skills/skillManage.js +13 -0
- package/dist/tools/v4/skills/skillView.js +1 -0
- package/dist/tools/v4/skills/skillsList.js +1 -0
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +54 -1
- package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
- package/dist/tools/v4/system/appClose.js +13 -0
- package/dist/tools/v4/system/appInput.js +13 -0
- package/dist/tools/v4/system/appLaunch.js +13 -0
- package/dist/tools/v4/system/clipboardRead.js +1 -0
- package/dist/tools/v4/system/clipboardWrite.js +14 -0
- package/dist/tools/v4/system/mediaKey.js +12 -0
- package/dist/tools/v4/system/mediaSessions.js +1 -0
- package/dist/tools/v4/system/mediaTransport.js +13 -0
- package/dist/tools/v4/system/naturalEvents.js +1 -0
- package/dist/tools/v4/system/nowPlaying.js +1 -0
- package/dist/tools/v4/system/osProcessList.js +1 -0
- package/dist/tools/v4/system/screenshot.js +1 -0
- package/dist/tools/v4/system/systemInfo.js +1 -0
- package/dist/tools/v4/system/volumeSet.js +17 -0
- package/dist/tools/v4/terminal/shellExec.js +81 -9
- package/dist/tools/v4/web/deepResearch.js +1 -0
- package/dist/tools/v4/web/openUrl.js +1 -0
- package/dist/tools/v4/web/webFetch.js +1 -0
- package/dist/tools/v4/web/webPage.js +1 -0
- package/dist/tools/v4/web/webSearch.js +1 -0
- package/dist/tools/v4/web/youtubeSearch.js +1 -0
- package/package.json +13 -3
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* cli/v4/commands/daemon.ts — v4.5 Phase 4b: `aiden daemon` CLI.
|
|
10
|
+
*
|
|
11
|
+
* Sub-actions:
|
|
12
|
+
* install — write platform-appropriate OS service unit + enable
|
|
13
|
+
* uninstall — remove unit + disable
|
|
14
|
+
* start — foreground supervisor (Phase 1 startSupervisor)
|
|
15
|
+
* stop — SIGTERM the daemon via runtime.lock PID
|
|
16
|
+
* restart — SIGUSR1 on POSIX → exit 75 → respawn;
|
|
17
|
+
* stop+start on Windows (no SIGUSR1)
|
|
18
|
+
* status — query /api/daemon/status
|
|
19
|
+
* logs — tail recent logs (best-effort, platform-dependent)
|
|
20
|
+
*
|
|
21
|
+
* The OS-level supervisor (systemd/launchd) is the preferred path
|
|
22
|
+
* because it survives logout + reboot. The internal supervisor
|
|
23
|
+
* (start) is the fallback when no service manager exists or when the
|
|
24
|
+
* user doesn't want to install one.
|
|
25
|
+
*
|
|
26
|
+
* Windows install is DOCS-ONLY in v4.5 — NSSM/SCM/Scheduled Task
|
|
27
|
+
* variance + admin requirements make the install footprint too risky
|
|
28
|
+
* for an auto-installer. The docs cover pm2 + foreground patterns.
|
|
29
|
+
*/
|
|
30
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
31
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
|
+
};
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.runDaemonSubcommand = runDaemonSubcommand;
|
|
35
|
+
exports.captureUserPath = captureUserPath;
|
|
36
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
37
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
38
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
39
|
+
const node_child_process_1 = require("node:child_process");
|
|
40
|
+
const node_util_1 = require("node:util");
|
|
41
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
42
|
+
const daemon_1 = require("../../../core/v4/daemon");
|
|
43
|
+
const daemon_2 = require("../../../core/v4/daemon");
|
|
44
|
+
const paths_1 = require("../../../core/v4/paths");
|
|
45
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
46
|
+
const noopOut = (s) => { process.stdout.write(s); };
|
|
47
|
+
const noopErr = (s) => { process.stderr.write(s); };
|
|
48
|
+
/**
|
|
49
|
+
* Run `aiden daemon <action>` and return the desired process exit code.
|
|
50
|
+
*/
|
|
51
|
+
async function runDaemonSubcommand(action, args, opts = {}) {
|
|
52
|
+
const out = opts.writeOut ?? noopOut;
|
|
53
|
+
const err = opts.writeErr ?? noopErr;
|
|
54
|
+
switch (action) {
|
|
55
|
+
case 'install': return runInstall({ out, err });
|
|
56
|
+
case 'uninstall': return runUninstall({ out, err });
|
|
57
|
+
case 'start': return runStart({ out, err });
|
|
58
|
+
case 'stop': return runStop({ out, err });
|
|
59
|
+
case 'restart': return runRestart({ out, err });
|
|
60
|
+
case 'status': return runStatus({ out, err });
|
|
61
|
+
case 'logs': return runLogs({ out, err });
|
|
62
|
+
default:
|
|
63
|
+
err(`Unknown daemon action: ${action}\n`);
|
|
64
|
+
err('Actions: install, uninstall, start, stop, restart, status, logs\n');
|
|
65
|
+
return 2;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const SYSTEMD_UNIT_NAME = 'aiden.service';
|
|
69
|
+
const LAUNCHD_LABEL = 'com.aiden.daemon';
|
|
70
|
+
function systemdUnitPath() {
|
|
71
|
+
return node_path_1.default.join(node_os_1.default.homedir(), '.config', 'systemd', 'user', SYSTEMD_UNIT_NAME);
|
|
72
|
+
}
|
|
73
|
+
function launchdPlistPath() {
|
|
74
|
+
return node_path_1.default.join(node_os_1.default.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Best-effort: find the Aiden CLI entry point on disk.
|
|
78
|
+
*
|
|
79
|
+
* For packaged installs (`aiden-runtime` npm package), this is the
|
|
80
|
+
* shipped `dist-bundle/index.js`. For local dev (running via
|
|
81
|
+
* `npx tsx cli/v4/aidenCLI.ts`), we fall back to `process.argv[1]`
|
|
82
|
+
* which is the script entry point tsx/node is currently executing.
|
|
83
|
+
*/
|
|
84
|
+
function findBundlePath() {
|
|
85
|
+
// Try the well-known packaged location relative to this file.
|
|
86
|
+
const candidates = [
|
|
87
|
+
node_path_1.default.resolve(__dirname, '..', '..', '..', 'dist-bundle', 'index.js'),
|
|
88
|
+
node_path_1.default.resolve(__dirname, '..', '..', '..', 'dist-bundle', 'cli.js'),
|
|
89
|
+
];
|
|
90
|
+
for (const c of candidates) {
|
|
91
|
+
try {
|
|
92
|
+
if (node_fs_1.default.existsSync(c))
|
|
93
|
+
return c;
|
|
94
|
+
}
|
|
95
|
+
catch { /* noop */ }
|
|
96
|
+
}
|
|
97
|
+
// Fallback: whatever the user is running NOW.
|
|
98
|
+
return process.argv[1] ?? node_path_1.default.resolve(__dirname, '..', 'aidenCLI.ts');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Read the running daemon's PID from the runtime lock file.
|
|
102
|
+
* Returns null when the file doesn't exist or is malformed.
|
|
103
|
+
*/
|
|
104
|
+
function readRuntimeLockPid() {
|
|
105
|
+
const lockPath = (0, daemon_1.daemonRuntimeLockPath)((0, paths_1.resolveAidenRoot)());
|
|
106
|
+
try {
|
|
107
|
+
if (!node_fs_1.default.existsSync(lockPath))
|
|
108
|
+
return null;
|
|
109
|
+
const raw = node_fs_1.default.readFileSync(lockPath, 'utf-8').trim();
|
|
110
|
+
if (!raw)
|
|
111
|
+
return null;
|
|
112
|
+
const lines = raw.split(/\r?\n/);
|
|
113
|
+
if (lines.length < 2)
|
|
114
|
+
return null;
|
|
115
|
+
const pid = Number.parseInt(lines[1], 10);
|
|
116
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** True when the PID corresponds to a live process. */
|
|
123
|
+
function isPidAlive(pid) {
|
|
124
|
+
try {
|
|
125
|
+
process.kill(pid, 0);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const code = err.code;
|
|
130
|
+
return code === 'EPERM'; // exists, not ours
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Poll until the PID is gone, OR timeout expires. */
|
|
134
|
+
async function pollUntilDead(pid, timeoutMs) {
|
|
135
|
+
const deadline = Date.now() + timeoutMs;
|
|
136
|
+
while (Date.now() < deadline) {
|
|
137
|
+
if (!isPidAlive(pid))
|
|
138
|
+
return true;
|
|
139
|
+
await sleep(250);
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
/** Poll /health/live until 200 OR timeout. */
|
|
144
|
+
async function pollHealthLive(port, timeoutMs) {
|
|
145
|
+
const deadline = Date.now() + timeoutMs;
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
try {
|
|
148
|
+
const ok = await getJson(`http://127.0.0.1:${port}/health/live`, 1000)
|
|
149
|
+
.then((r) => r.ok === true)
|
|
150
|
+
.catch(() => false);
|
|
151
|
+
if (ok)
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch { /* noop */ }
|
|
155
|
+
await sleep(250);
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
function getJson(url, timeoutMs) {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
const req = node_http_1.default.get(url, (res) => {
|
|
162
|
+
let body = '';
|
|
163
|
+
res.setEncoding('utf-8');
|
|
164
|
+
res.on('data', (c) => { body += c; });
|
|
165
|
+
res.on('end', () => {
|
|
166
|
+
try {
|
|
167
|
+
resolve(JSON.parse(body));
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
reject(e);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
req.setTimeout(timeoutMs, () => { req.destroy(new Error('timeout')); });
|
|
175
|
+
req.on('error', reject);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function sleep(ms) {
|
|
179
|
+
return new Promise((r) => { const t = setTimeout(r, ms); if (typeof t.unref === 'function')
|
|
180
|
+
t.unref(); });
|
|
181
|
+
}
|
|
182
|
+
// ── install ─────────────────────────────────────────────────────────────────
|
|
183
|
+
async function runInstall(io) {
|
|
184
|
+
switch (process.platform) {
|
|
185
|
+
case 'linux': return runInstallLinux(io);
|
|
186
|
+
case 'darwin': return runInstallMacOS(io);
|
|
187
|
+
case 'win32': return runInstallWindows(io);
|
|
188
|
+
default:
|
|
189
|
+
io.err(`Unsupported platform: ${process.platform}\n`);
|
|
190
|
+
io.err('Run `aiden daemon start` to use the internal supervisor in the foreground.\n');
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function runInstallLinux(io) {
|
|
195
|
+
const cfg = (0, daemon_2.getDaemonConfig)();
|
|
196
|
+
const nodeBin = process.execPath;
|
|
197
|
+
const bundle = findBundlePath();
|
|
198
|
+
const unitText = (0, daemon_1.generateSystemdUnit)({
|
|
199
|
+
nodeBin,
|
|
200
|
+
bundlePath: bundle,
|
|
201
|
+
workingDir: node_os_1.default.homedir(),
|
|
202
|
+
port: cfg.port,
|
|
203
|
+
drainTimeoutMs: cfg.drainTimeoutMs,
|
|
204
|
+
});
|
|
205
|
+
const unitPath = systemdUnitPath();
|
|
206
|
+
try {
|
|
207
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(unitPath), { recursive: true });
|
|
208
|
+
node_fs_1.default.writeFileSync(unitPath, unitText, { encoding: 'utf-8', mode: 0o644 });
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
io.err(`failed to write ${unitPath}: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await execFileAsync('systemctl', ['--user', 'daemon-reload']);
|
|
216
|
+
await execFileAsync('systemctl', ['--user', 'enable', SYSTEMD_UNIT_NAME]);
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
io.err(`systemctl invocation failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
220
|
+
io.err('Unit file written but enable/reload step failed. Try manually:\n');
|
|
221
|
+
io.err(` systemctl --user daemon-reload\n`);
|
|
222
|
+
io.err(` systemctl --user enable ${SYSTEMD_UNIT_NAME}\n`);
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
io.out(`Installed: ${unitPath}\n`);
|
|
226
|
+
io.out(`Start with: systemctl --user start aiden\n`);
|
|
227
|
+
io.out(`Status: systemctl --user status aiden\n`);
|
|
228
|
+
io.out(`Restart: aiden daemon restart (sends SIGUSR1 → exit ${daemon_1.DAEMON_RESTART_EXIT_CODE} → systemd respawns)\n`);
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
async function runInstallMacOS(io) {
|
|
232
|
+
const cfg = (0, daemon_2.getDaemonConfig)();
|
|
233
|
+
const nodeBin = process.execPath;
|
|
234
|
+
const bundle = findBundlePath();
|
|
235
|
+
const userPath = captureUserPath();
|
|
236
|
+
const logsDir = node_path_1.default.join(node_os_1.default.homedir(), '.aiden', 'logs');
|
|
237
|
+
try {
|
|
238
|
+
node_fs_1.default.mkdirSync(logsDir, { recursive: true });
|
|
239
|
+
}
|
|
240
|
+
catch { /* noop */ }
|
|
241
|
+
const plistText = (0, daemon_1.generateLaunchdPlist)({
|
|
242
|
+
nodeBin,
|
|
243
|
+
bundlePath: bundle,
|
|
244
|
+
workingDir: node_os_1.default.homedir(),
|
|
245
|
+
port: cfg.port,
|
|
246
|
+
drainTimeoutMs: cfg.drainTimeoutMs,
|
|
247
|
+
userPath,
|
|
248
|
+
stdoutPath: node_path_1.default.join(logsDir, 'daemon.out.log'),
|
|
249
|
+
stderrPath: node_path_1.default.join(logsDir, 'daemon.err.log'),
|
|
250
|
+
});
|
|
251
|
+
const plistPath = launchdPlistPath();
|
|
252
|
+
try {
|
|
253
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(plistPath), { recursive: true });
|
|
254
|
+
node_fs_1.default.writeFileSync(plistPath, plistText, { encoding: 'utf-8', mode: 0o644 });
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
io.err(`failed to write ${plistPath}: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
const uid = process.getuid?.() ?? 501;
|
|
261
|
+
const domain = `gui/${uid}`;
|
|
262
|
+
// Idempotent install: bootout (ignore errors — first install
|
|
263
|
+
// won't have a previous bootstrap) then bootstrap.
|
|
264
|
+
try {
|
|
265
|
+
await execFileAsync('launchctl', ['bootout', `${domain}/${LAUNCHD_LABEL}`]);
|
|
266
|
+
}
|
|
267
|
+
catch { /* noop */ }
|
|
268
|
+
try {
|
|
269
|
+
await execFileAsync('launchctl', ['bootstrap', domain, plistPath]);
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
io.err(`launchctl bootstrap failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
io.out(`Installed: ${plistPath}\n`);
|
|
276
|
+
io.out(`Status: launchctl print ${domain}/${LAUNCHD_LABEL}\n`);
|
|
277
|
+
io.out(`Restart: aiden daemon restart (sends SIGUSR1 → exit ${daemon_1.DAEMON_RESTART_EXIT_CODE} → launchd respawns)\n`);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
async function runInstallWindows(io) {
|
|
281
|
+
io.out((0, daemon_1.windowsServiceGuidance)());
|
|
282
|
+
io.out('\n');
|
|
283
|
+
io.out('See docs/v4.5/daemon-windows.md for the full walkthrough.\n');
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
// ── uninstall ───────────────────────────────────────────────────────────────
|
|
287
|
+
async function runUninstall(io) {
|
|
288
|
+
switch (process.platform) {
|
|
289
|
+
case 'linux': return runUninstallLinux(io);
|
|
290
|
+
case 'darwin': return runUninstallMacOS(io);
|
|
291
|
+
case 'win32':
|
|
292
|
+
io.out('No service installed (Windows install is docs-only — nothing to remove).\n');
|
|
293
|
+
return 0;
|
|
294
|
+
default:
|
|
295
|
+
io.err(`Unsupported platform: ${process.platform}\n`);
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function runUninstallLinux(io) {
|
|
300
|
+
const unitPath = systemdUnitPath();
|
|
301
|
+
if (!node_fs_1.default.existsSync(unitPath)) {
|
|
302
|
+
io.out('No systemd unit installed.\n');
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
await execFileAsync('systemctl', ['--user', 'disable', SYSTEMD_UNIT_NAME]);
|
|
307
|
+
await execFileAsync('systemctl', ['--user', 'stop', SYSTEMD_UNIT_NAME]).catch(() => undefined);
|
|
308
|
+
}
|
|
309
|
+
catch { /* unit may not be enabled — fall through */ }
|
|
310
|
+
try {
|
|
311
|
+
node_fs_1.default.unlinkSync(unitPath);
|
|
312
|
+
}
|
|
313
|
+
catch { /* noop */ }
|
|
314
|
+
try {
|
|
315
|
+
await execFileAsync('systemctl', ['--user', 'daemon-reload']);
|
|
316
|
+
}
|
|
317
|
+
catch { /* noop */ }
|
|
318
|
+
io.out(`Uninstalled: ${unitPath}\n`);
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
async function runUninstallMacOS(io) {
|
|
322
|
+
const plistPath = launchdPlistPath();
|
|
323
|
+
if (!node_fs_1.default.existsSync(plistPath)) {
|
|
324
|
+
io.out('No launchd plist installed.\n');
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
const uid = process.getuid?.() ?? 501;
|
|
328
|
+
const domain = `gui/${uid}`;
|
|
329
|
+
try {
|
|
330
|
+
await execFileAsync('launchctl', ['bootout', `${domain}/${LAUNCHD_LABEL}`]);
|
|
331
|
+
}
|
|
332
|
+
catch { /* may not be loaded */ }
|
|
333
|
+
try {
|
|
334
|
+
node_fs_1.default.unlinkSync(plistPath);
|
|
335
|
+
}
|
|
336
|
+
catch { /* noop */ }
|
|
337
|
+
io.out(`Uninstalled: ${plistPath}\n`);
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
// ── start (foreground supervisor) ───────────────────────────────────────────
|
|
341
|
+
async function runStart(io) {
|
|
342
|
+
io.out('Starting Aiden daemon in foreground (Ctrl+C to stop).\n');
|
|
343
|
+
io.out('Tip: run `aiden daemon install` to register an OS service that\n');
|
|
344
|
+
io.out(' survives logout + reboot (Linux/macOS).\n');
|
|
345
|
+
const cmd = [process.execPath, findBundlePath()];
|
|
346
|
+
const handle = (0, daemon_1.startSupervisor)({
|
|
347
|
+
childCmd: cmd,
|
|
348
|
+
env: {
|
|
349
|
+
AIDEN_DAEMON: '1',
|
|
350
|
+
// The inner child IS the daemon — disable its own internal
|
|
351
|
+
// supervisor to avoid supervisor-in-supervisor recursion.
|
|
352
|
+
AIDEN_DAEMON_AUTO_RESTART: '0',
|
|
353
|
+
},
|
|
354
|
+
onChildExit: (code, signal) => io.out(`[supervisor] child exit code=${code} signal=${signal}\n`),
|
|
355
|
+
onRespawn: (attempt, delayMs) => io.out(`[supervisor] respawn attempt ${attempt} in ${delayMs}ms\n`),
|
|
356
|
+
onGiveUp: (reason) => io.err(`[supervisor] giving up: ${reason}\n`),
|
|
357
|
+
});
|
|
358
|
+
// Forward signals to the supervisor (which forwards to child).
|
|
359
|
+
const onTerm = () => { void handle.stop(); };
|
|
360
|
+
process.once('SIGINT', onTerm);
|
|
361
|
+
process.once('SIGTERM', onTerm);
|
|
362
|
+
// Hold the event loop until the supervisor stops.
|
|
363
|
+
await new Promise((resolve) => {
|
|
364
|
+
const id = setInterval(() => {
|
|
365
|
+
if (handle.childPid() === null) {
|
|
366
|
+
clearInterval(id);
|
|
367
|
+
resolve();
|
|
368
|
+
}
|
|
369
|
+
}, 1000);
|
|
370
|
+
if (typeof id.unref === 'function')
|
|
371
|
+
id.unref();
|
|
372
|
+
});
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
// ── stop ────────────────────────────────────────────────────────────────────
|
|
376
|
+
async function runStop(io) {
|
|
377
|
+
const pid = readRuntimeLockPid();
|
|
378
|
+
if (!pid) {
|
|
379
|
+
io.out('No daemon running.\n');
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
if (!isPidAlive(pid)) {
|
|
383
|
+
io.out(`No daemon running (stale lock pid ${pid}).\n`);
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
process.kill(pid, 'SIGTERM');
|
|
388
|
+
}
|
|
389
|
+
catch (e) {
|
|
390
|
+
io.err(`failed to signal pid ${pid}: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
393
|
+
const drain = (0, daemon_2.getDaemonConfig)().drainTimeoutMs;
|
|
394
|
+
const died = await pollUntilDead(pid, drain + 5000);
|
|
395
|
+
if (!died) {
|
|
396
|
+
io.err(`pid ${pid} did not exit within ${drain + 5000}ms\n`);
|
|
397
|
+
return 1;
|
|
398
|
+
}
|
|
399
|
+
io.out(`Stopped pid ${pid}\n`);
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
// ── restart ─────────────────────────────────────────────────────────────────
|
|
403
|
+
async function runRestart(io) {
|
|
404
|
+
if (process.platform === 'win32') {
|
|
405
|
+
return runRestartWindows(io);
|
|
406
|
+
}
|
|
407
|
+
const pid = readRuntimeLockPid();
|
|
408
|
+
if (!pid) {
|
|
409
|
+
io.err('No daemon running.\n');
|
|
410
|
+
return 1;
|
|
411
|
+
}
|
|
412
|
+
if (!isPidAlive(pid)) {
|
|
413
|
+
io.err(`No daemon running (stale lock pid ${pid}).\n`);
|
|
414
|
+
return 1;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
process.kill(pid, 'SIGUSR1');
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
io.err(`failed to send SIGUSR1 to pid ${pid}: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
421
|
+
return 1;
|
|
422
|
+
}
|
|
423
|
+
io.out(`Sent SIGUSR1 to pid ${pid} (graceful drain → exit ${daemon_1.DAEMON_RESTART_EXIT_CODE} → service manager respawns).\n`);
|
|
424
|
+
const drain = (0, daemon_2.getDaemonConfig)().drainTimeoutMs;
|
|
425
|
+
const died = await pollUntilDead(pid, drain + 5000);
|
|
426
|
+
if (!died) {
|
|
427
|
+
io.err(`pid ${pid} did not exit within ${drain + 5000}ms\n`);
|
|
428
|
+
return 1;
|
|
429
|
+
}
|
|
430
|
+
io.out('Old daemon exited. Waiting for new daemon to come up ...\n');
|
|
431
|
+
const port = (0, daemon_2.getDaemonConfig)().port;
|
|
432
|
+
const up = await pollHealthLive(port, 15000);
|
|
433
|
+
if (!up) {
|
|
434
|
+
io.err(`new daemon did not become live on http://127.0.0.1:${port}/health/live within 15s.\n`);
|
|
435
|
+
io.err('Check service manager logs (systemctl --user status aiden / launchctl print).\n');
|
|
436
|
+
return 1;
|
|
437
|
+
}
|
|
438
|
+
const newPid = readRuntimeLockPid();
|
|
439
|
+
io.out(`Restarted (new pid ${newPid ?? '?'})\n`);
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
async function runRestartWindows(io) {
|
|
443
|
+
io.out('SIGUSR1 not supported on Windows. Doing stop+start sequentially.\n');
|
|
444
|
+
const stopCode = await runStop(io);
|
|
445
|
+
if (stopCode !== 0)
|
|
446
|
+
return stopCode;
|
|
447
|
+
await sleep(2000);
|
|
448
|
+
// Re-spawn detached so the CLI can return.
|
|
449
|
+
const cmd = [process.execPath, findBundlePath()];
|
|
450
|
+
const child = (0, node_child_process_1.spawn)(cmd[0], cmd.slice(1), {
|
|
451
|
+
env: { ...process.env, AIDEN_DAEMON: '1' },
|
|
452
|
+
stdio: 'ignore',
|
|
453
|
+
detached: true,
|
|
454
|
+
});
|
|
455
|
+
child.unref();
|
|
456
|
+
io.out(`Spawned new daemon (pid ${child.pid ?? '?'}).\n`);
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
// ── status ──────────────────────────────────────────────────────────────────
|
|
460
|
+
async function runStatus(io) {
|
|
461
|
+
const port = (0, daemon_2.getDaemonConfig)().port;
|
|
462
|
+
try {
|
|
463
|
+
const r = await getJson(`http://127.0.0.1:${port}/api/daemon/status`, 3000);
|
|
464
|
+
io.out(JSON.stringify(r, null, 2) + '\n');
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
catch (e) {
|
|
468
|
+
io.err(`failed to query http://127.0.0.1:${port}/api/daemon/status: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
469
|
+
return 1;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// ── logs ────────────────────────────────────────────────────────────────────
|
|
473
|
+
async function runLogs(io) {
|
|
474
|
+
// Phase 4b ships a minimal logs surface: locate the platform's
|
|
475
|
+
// log destination + print the last N lines. Tail-follow (`-f`)
|
|
476
|
+
// can come in a Phase 4b.x polish.
|
|
477
|
+
if (process.platform === 'darwin') {
|
|
478
|
+
const f = node_path_1.default.join(node_os_1.default.homedir(), '.aiden', 'logs', 'daemon.out.log');
|
|
479
|
+
return tailFile(f, io);
|
|
480
|
+
}
|
|
481
|
+
if (process.platform === 'linux') {
|
|
482
|
+
try {
|
|
483
|
+
const r = await execFileAsync('journalctl', ['--user', '-u', SYSTEMD_UNIT_NAME, '-n', '200', '--no-pager']);
|
|
484
|
+
io.out(r.stdout);
|
|
485
|
+
return 0;
|
|
486
|
+
}
|
|
487
|
+
catch (e) {
|
|
488
|
+
io.err(`journalctl failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
489
|
+
io.err('Is the service installed? Run `aiden daemon install`.\n');
|
|
490
|
+
return 1;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
io.out('Log destination unknown for this platform. Run the daemon in foreground (`aiden daemon start`) to see live output.\n');
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
function tailFile(filePath, io) {
|
|
497
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
498
|
+
io.out(`No log file at ${filePath} yet.\n`);
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const content = node_fs_1.default.readFileSync(filePath, 'utf-8');
|
|
503
|
+
const lines = content.split(/\r?\n/);
|
|
504
|
+
const tail = lines.slice(-200).join('\n');
|
|
505
|
+
io.out(tail + '\n');
|
|
506
|
+
return 0;
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
io.err(`failed to read ${filePath}: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
510
|
+
return 1;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// ── PATH capture for macOS launchd ──────────────────────────────────────────
|
|
514
|
+
/**
|
|
515
|
+
* launchd starts with a minimal `/usr/bin:/bin:/usr/sbin:/sbin` PATH.
|
|
516
|
+
* That misses Homebrew (`/opt/homebrew/bin`), nvm, cargo, etc.
|
|
517
|
+
* Capture the user's login-shell PATH so the daemon can find tools
|
|
518
|
+
* the user expects.
|
|
519
|
+
*
|
|
520
|
+
* Public for tests. Synchronous + best-effort: timeout + fallback
|
|
521
|
+
* to `process.env.PATH`.
|
|
522
|
+
*/
|
|
523
|
+
function captureUserPath() {
|
|
524
|
+
const shell = process.env.SHELL ?? '/bin/zsh';
|
|
525
|
+
const isFish = shell.endsWith('fish');
|
|
526
|
+
const args = isFish
|
|
527
|
+
? ['--login', '-c', 'echo $PATH']
|
|
528
|
+
: ['-lc', 'echo $PATH'];
|
|
529
|
+
try {
|
|
530
|
+
const out = (0, node_child_process_1.execFileSync)(shell, args, {
|
|
531
|
+
encoding: 'utf-8',
|
|
532
|
+
timeout: 5000,
|
|
533
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
534
|
+
});
|
|
535
|
+
const trimmed = out.trim();
|
|
536
|
+
if (trimmed.length > 0)
|
|
537
|
+
return trimmed;
|
|
538
|
+
}
|
|
539
|
+
catch { /* fall through to process.env.PATH */ }
|
|
540
|
+
return process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
|
|
541
|
+
}
|