@indianic/dialout 1.1.2 → 1.2.1
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 +425 -24
- package/bin/AGENTS.md +44 -0
- package/bin/devdash-agent.js +2 -0
- package/dist/ai-adapters/claude.d.ts +3 -0
- package/dist/ai-adapters/claude.d.ts.map +1 -0
- package/dist/ai-adapters/claude.js +98 -0
- package/dist/ai-adapters/claude.js.map +1 -0
- package/dist/ai-adapters/codex.d.ts +3 -0
- package/dist/ai-adapters/codex.d.ts.map +1 -0
- package/dist/ai-adapters/codex.js +147 -0
- package/dist/ai-adapters/codex.js.map +1 -0
- package/dist/ai-adapters/grok.d.ts +3 -0
- package/dist/ai-adapters/grok.d.ts.map +1 -0
- package/dist/ai-adapters/grok.js +164 -0
- package/dist/ai-adapters/grok.js.map +1 -0
- package/dist/ai-adapters/index.d.ts +5 -0
- package/dist/ai-adapters/index.d.ts.map +1 -0
- package/dist/ai-adapters/index.js +30 -0
- package/dist/ai-adapters/index.js.map +1 -0
- package/dist/ai-adapters/types.d.ts +3 -0
- package/dist/ai-adapters/types.d.ts.map +1 -0
- package/dist/ai-adapters/types.js +7 -0
- package/dist/ai-adapters/types.js.map +1 -0
- package/dist/ai-capabilities/claude.d.ts +4 -0
- package/dist/ai-capabilities/claude.d.ts.map +1 -0
- package/dist/ai-capabilities/claude.js +174 -0
- package/dist/ai-capabilities/claude.js.map +1 -0
- package/dist/ai-capabilities/describe.d.ts +2 -0
- package/dist/ai-capabilities/describe.d.ts.map +1 -0
- package/dist/ai-capabilities/describe.js +31 -0
- package/dist/ai-capabilities/describe.js.map +1 -0
- package/dist/ai-capabilities/fsdeps.d.ts +3 -0
- package/dist/ai-capabilities/fsdeps.d.ts.map +1 -0
- package/dist/ai-capabilities/fsdeps.js +71 -0
- package/dist/ai-capabilities/fsdeps.js.map +1 -0
- package/dist/ai-capabilities/grok.d.ts +5 -0
- package/dist/ai-capabilities/grok.d.ts.map +1 -0
- package/dist/ai-capabilities/grok.js +145 -0
- package/dist/ai-capabilities/grok.js.map +1 -0
- package/dist/ai-capabilities/index.d.ts +4 -0
- package/dist/ai-capabilities/index.d.ts.map +1 -0
- package/dist/ai-capabilities/index.js +94 -0
- package/dist/ai-capabilities/index.js.map +1 -0
- package/dist/ai-capabilities/redact.d.ts +2 -0
- package/dist/ai-capabilities/redact.d.ts.map +1 -0
- package/dist/ai-capabilities/redact.js +27 -0
- package/dist/ai-capabilities/redact.js.map +1 -0
- package/dist/ai-capabilities/types.d.ts +11 -0
- package/dist/ai-capabilities/types.d.ts.map +1 -0
- package/dist/ai-capabilities/types.js +3 -0
- package/dist/ai-capabilities/types.js.map +1 -0
- package/dist/ai-launch.d.ts +35 -0
- package/dist/ai-launch.d.ts.map +1 -0
- package/dist/ai-launch.js +211 -0
- package/dist/ai-launch.js.map +1 -0
- package/dist/ai-session-detector.d.ts +19 -0
- package/dist/ai-session-detector.d.ts.map +1 -0
- package/dist/ai-session-detector.js +79 -0
- package/dist/ai-session-detector.js.map +1 -0
- package/dist/ai-sessions.d.ts +36 -0
- package/dist/ai-sessions.d.ts.map +1 -0
- package/dist/ai-sessions.js +309 -0
- package/dist/ai-sessions.js.map +1 -0
- package/dist/ai-status.d.ts +6 -0
- package/dist/ai-status.d.ts.map +1 -0
- package/dist/ai-status.js +40 -0
- package/dist/ai-status.js.map +1 -0
- package/dist/ai-transcript-locator.d.ts +35 -0
- package/dist/ai-transcript-locator.d.ts.map +1 -0
- package/dist/ai-transcript-locator.js +268 -0
- package/dist/ai-transcript-locator.js.map +1 -0
- package/dist/ai-transcript-tail.d.ts +17 -0
- package/dist/ai-transcript-tail.d.ts.map +1 -0
- package/dist/ai-transcript-tail.js +120 -0
- package/dist/ai-transcript-tail.js.map +1 -0
- package/dist/checklist.d.ts +20 -0
- package/dist/checklist.d.ts.map +1 -0
- package/dist/checklist.js +51 -0
- package/dist/checklist.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1427 -0
- package/dist/cli.js.map +1 -0
- package/dist/command-runner.d.ts +15 -0
- package/dist/command-runner.d.ts.map +1 -0
- package/dist/command-runner.js +84 -0
- package/dist/command-runner.js.map +1 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +152 -0
- package/dist/config.js.map +1 -0
- package/dist/cowork.d.ts +26 -0
- package/dist/cowork.d.ts.map +1 -0
- package/dist/cowork.js +370 -0
- package/dist/cowork.js.map +1 -0
- package/dist/fs-browser.d.ts +6 -0
- package/dist/fs-browser.d.ts.map +1 -0
- package/dist/fs-browser.js +61 -0
- package/dist/fs-browser.js.map +1 -0
- package/dist/has-command.d.ts +12 -0
- package/dist/has-command.d.ts.map +1 -0
- package/dist/has-command.js +24 -0
- package/dist/has-command.js.map +1 -0
- package/dist/heartbeat.d.ts +7 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +50 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/port-scanner.d.ts +4 -0
- package/dist/port-scanner.d.ts.map +1 -0
- package/dist/port-scanner.js +75 -0
- package/dist/port-scanner.js.map +1 -0
- package/dist/project-scanner.d.ts +15 -0
- package/dist/project-scanner.d.ts.map +1 -0
- package/dist/project-scanner.js +333 -0
- package/dist/project-scanner.js.map +1 -0
- package/dist/pty-manager.d.ts +28 -0
- package/dist/pty-manager.d.ts.map +1 -0
- package/dist/pty-manager.js +506 -0
- package/dist/pty-manager.js.map +1 -0
- package/dist/service-installer.d.ts +95 -0
- package/dist/service-installer.d.ts.map +1 -0
- package/dist/service-installer.js +911 -0
- package/dist/service-installer.js.map +1 -0
- package/dist/single-instance.d.ts +20 -0
- package/dist/single-instance.d.ts.map +1 -0
- package/dist/single-instance.js +151 -0
- package/dist/single-instance.js.map +1 -0
- package/dist/ssh-local-network.d.ts +26 -0
- package/dist/ssh-local-network.d.ts.map +1 -0
- package/dist/ssh-local-network.js +169 -0
- package/dist/ssh-local-network.js.map +1 -0
- package/dist/terminal-detect.d.ts +52 -0
- package/dist/terminal-detect.d.ts.map +1 -0
- package/dist/terminal-detect.js +147 -0
- package/dist/terminal-detect.js.map +1 -0
- package/dist/terminal-markers.d.ts +52 -0
- package/dist/terminal-markers.d.ts.map +1 -0
- package/dist/terminal-markers.js +174 -0
- package/dist/terminal-markers.js.map +1 -0
- package/dist/tmux-manager.d.ts +35 -0
- package/dist/tmux-manager.d.ts.map +1 -0
- package/dist/tmux-manager.js +274 -0
- package/dist/tmux-manager.js.map +1 -0
- package/dist/update-check.d.ts +3 -0
- package/dist/update-check.d.ts.map +1 -0
- package/dist/update-check.js +202 -0
- package/dist/update-check.js.map +1 -0
- package/dist/websocket.d.ts +8 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +472 -0
- package/dist/websocket.js.map +1 -0
- package/node_modules/@dialout/shared/README.md +16 -0
- package/node_modules/@dialout/shared/dist/ai-chat-prefs.d.ts +8 -0
- package/node_modules/@dialout/shared/dist/ai-chat-prefs.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/ai-chat-prefs.js +41 -0
- package/node_modules/@dialout/shared/dist/capabilities.d.ts +25 -0
- package/node_modules/@dialout/shared/dist/capabilities.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/capabilities.js +2 -0
- package/node_modules/@dialout/shared/dist/chat-blocks.d.ts +33 -0
- package/node_modules/@dialout/shared/dist/chat-blocks.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/chat-blocks.js +61 -0
- package/node_modules/@dialout/shared/dist/command-filter.d.ts +4 -0
- package/node_modules/@dialout/shared/dist/command-filter.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/command-filter.js +49 -0
- package/node_modules/@dialout/shared/dist/composer-behaviour.d.ts +6 -0
- package/node_modules/@dialout/shared/dist/composer-behaviour.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/composer-behaviour.js +24 -0
- package/node_modules/@dialout/shared/dist/events.d.ts +37 -0
- package/node_modules/@dialout/shared/dist/events.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/events.js +13 -0
- package/node_modules/@dialout/shared/dist/index.d.ts +21 -0
- package/node_modules/@dialout/shared/dist/index.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/index.js +37 -0
- package/node_modules/@dialout/shared/dist/kinds.d.ts +2 -0
- package/node_modules/@dialout/shared/dist/kinds.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/kinds.js +2 -0
- package/node_modules/@dialout/shared/dist/notify.d.ts +3 -0
- package/node_modules/@dialout/shared/dist/notify.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/notify.js +31 -0
- package/node_modules/@dialout/shared/dist/permission.d.ts +4 -0
- package/node_modules/@dialout/shared/dist/permission.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/permission.js +8 -0
- package/node_modules/@dialout/shared/dist/scroll-pin.d.ts +3 -0
- package/node_modules/@dialout/shared/dist/scroll-pin.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/scroll-pin.js +11 -0
- package/node_modules/@dialout/shared/dist/session.d.ts +19 -0
- package/node_modules/@dialout/shared/dist/session.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/session.js +6 -0
- package/node_modules/@dialout/shared/dist/terminal-name.d.ts +25 -0
- package/node_modules/@dialout/shared/dist/terminal-name.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/terminal-name.js +63 -0
- package/node_modules/@dialout/shared/dist/tool-appearance.d.ts +9 -0
- package/node_modules/@dialout/shared/dist/tool-appearance.d.ts.map +1 -0
- package/node_modules/@dialout/shared/dist/tool-appearance.js +30 -0
- package/node_modules/@dialout/shared/package.json +37 -0
- package/node_modules/@dialout/shared/src/ai-chat-prefs.test.ts +40 -0
- package/node_modules/@dialout/shared/src/ai-chat-prefs.ts +39 -0
- package/node_modules/@dialout/shared/src/capabilities.ts +30 -0
- package/node_modules/@dialout/shared/src/chat-blocks.test.ts +65 -0
- package/node_modules/@dialout/shared/src/chat-blocks.ts +79 -0
- package/node_modules/@dialout/shared/src/command-filter.test.ts +76 -0
- package/node_modules/@dialout/shared/src/command-filter.ts +45 -0
- package/node_modules/@dialout/shared/src/composer-behaviour.test.ts +31 -0
- package/node_modules/@dialout/shared/src/composer-behaviour.ts +22 -0
- package/node_modules/@dialout/shared/src/events.ts +27 -0
- package/node_modules/@dialout/shared/src/index.ts +32 -0
- package/node_modules/@dialout/shared/src/kinds.ts +5 -0
- package/node_modules/@dialout/shared/src/notify.test.ts +49 -0
- package/node_modules/@dialout/shared/src/notify.ts +30 -0
- package/node_modules/@dialout/shared/src/permission.ts +10 -0
- package/node_modules/@dialout/shared/src/scroll-pin.test.ts +23 -0
- package/node_modules/@dialout/shared/src/scroll-pin.ts +13 -0
- package/node_modules/@dialout/shared/src/session.ts +25 -0
- package/node_modules/@dialout/shared/src/terminal-name.ts +82 -0
- package/node_modules/@dialout/shared/src/tool-appearance.test.ts +41 -0
- package/node_modules/@dialout/shared/src/tool-appearance.ts +39 -0
- package/package.json +54 -14
- package/scripts/postinstall.js +79 -0
- package/scripts/prepack.js +45 -0
- package/bin/dialout.js +0 -13
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.serviceUser = serviceUser;
|
|
37
|
+
exports.isLingerEnabled = isLingerEnabled;
|
|
38
|
+
exports.enableLinger = enableLinger;
|
|
39
|
+
exports.defaultLinuxScope = defaultLinuxScope;
|
|
40
|
+
exports.installService = installService;
|
|
41
|
+
exports.uninstallService = uninstallService;
|
|
42
|
+
exports.isServiceInstalled = isServiceInstalled;
|
|
43
|
+
exports.getServiceStatus = getServiceStatus;
|
|
44
|
+
exports.getWatchdogScript = getWatchdogScript;
|
|
45
|
+
exports.installCron = installCron;
|
|
46
|
+
exports.uninstallCron = uninstallCron;
|
|
47
|
+
exports.isCronInstalled = isCronInstalled;
|
|
48
|
+
exports.listSupervisors = listSupervisors;
|
|
49
|
+
exports.staleSupervisorAdvice = staleSupervisorAdvice;
|
|
50
|
+
exports.repairWatchdog = repairWatchdog;
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const os = __importStar(require("os"));
|
|
54
|
+
const child_process_1 = require("child_process");
|
|
55
|
+
const config_1 = require("./config");
|
|
56
|
+
const SERVICE_NAME = 'com.dialout.agent';
|
|
57
|
+
const SYSTEMD_UNIT = 'dialout';
|
|
58
|
+
function getNodePath() {
|
|
59
|
+
try {
|
|
60
|
+
return (0, child_process_1.execSync)('which node', { encoding: 'utf-8' }).trim();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return '/usr/local/bin/node';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function getAgentScript() {
|
|
67
|
+
return path.resolve(__dirname, '../dist/index.js');
|
|
68
|
+
}
|
|
69
|
+
// The human the boot service must run AS.
|
|
70
|
+
//
|
|
71
|
+
// os.userInfo() reports the EFFECTIVE user, so under `sudo devdash-agent
|
|
72
|
+
// install-service --system` it returns "root" — and a plist with
|
|
73
|
+
// UserName=root gives the daemon root's HOME and root's tmux socket
|
|
74
|
+
// (/tmp/tmux-0), where the user's sessions do not exist. The agent then
|
|
75
|
+
// connects, authenticates, and reports an empty session list forever, which
|
|
76
|
+
// looks exactly like "nothing is running". sudo exports the original login
|
|
77
|
+
// name in SUDO_USER, so prefer it.
|
|
78
|
+
function serviceUser(env = process.env, info = () => os.userInfo().username) {
|
|
79
|
+
const sudoUser = (env.SUDO_USER || '').trim();
|
|
80
|
+
if (sudoUser && sudoUser !== 'root')
|
|
81
|
+
return sudoUser;
|
|
82
|
+
return info();
|
|
83
|
+
}
|
|
84
|
+
function isRoot() {
|
|
85
|
+
return typeof process.getuid === 'function' && process.getuid() === 0;
|
|
86
|
+
}
|
|
87
|
+
// We can drive an interactive `sudo` password prompt only on a real terminal.
|
|
88
|
+
function canPromptSudo() {
|
|
89
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
90
|
+
}
|
|
91
|
+
// Run a privileged command the way the INSTALL path already does: directly when
|
|
92
|
+
// root, otherwise through an interactive sudo, otherwise not at all.
|
|
93
|
+
//
|
|
94
|
+
// uninstall used to have no sudo branch — only isRoot() and "here are the
|
|
95
|
+
// commands, run them yourself". Since install-service --system escalates
|
|
96
|
+
// happily, the normal way to end up with a LaunchDaemon is to never have been
|
|
97
|
+
// root, and then uninstall-service could not remove what install-service had
|
|
98
|
+
// just installed. Worse, it set removed = true on that branch, so it printed
|
|
99
|
+
// no failure and exited 0 while the daemon kept running and kept restarting at
|
|
100
|
+
// boot. Returns whether the command actually ran.
|
|
101
|
+
function runPrivileged(cmd, deps = {}) {
|
|
102
|
+
const root = deps.isRoot ?? isRoot;
|
|
103
|
+
const prompt = deps.canPromptSudo ?? canPromptSudo;
|
|
104
|
+
const run = deps.run ?? ((c, o) => { (0, child_process_1.execSync)(c, o); });
|
|
105
|
+
const log = deps.log ?? console.log;
|
|
106
|
+
if (root()) {
|
|
107
|
+
try {
|
|
108
|
+
run(cmd);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (prompt()) {
|
|
116
|
+
log('Removing the boot service requires admin rights — you may be prompted for your password.\n');
|
|
117
|
+
try {
|
|
118
|
+
run(`sudo bash -c '${cmd}'`, { stdio: 'inherit' });
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
// --- macOS (launchd) ---
|
|
128
|
+
function getLaunchdPlistPath(homedir = os.homedir()) {
|
|
129
|
+
// Per-user LaunchAgent — starts at login.
|
|
130
|
+
return path.join(homedir, 'Library', 'LaunchAgents', `${SERVICE_NAME}.plist`);
|
|
131
|
+
}
|
|
132
|
+
function getLaunchDaemonPlistPath() {
|
|
133
|
+
// System LaunchDaemon — starts at boot, before login.
|
|
134
|
+
return `/Library/LaunchDaemons/${SERVICE_NAME}.plist`;
|
|
135
|
+
}
|
|
136
|
+
function generatePlist(system) {
|
|
137
|
+
const nodePath = getNodePath();
|
|
138
|
+
const script = getAgentScript();
|
|
139
|
+
const logDir = path.join((0, config_1.configDirFor)(os.homedir()), 'logs');
|
|
140
|
+
if (!fs.existsSync(logDir)) {
|
|
141
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
// A system daemon runs as root by default; pin it to the invoking user so it
|
|
144
|
+
// can read ~/.dialout/config.json and write the user's log dir.
|
|
145
|
+
const userKey = system
|
|
146
|
+
? ` <key>UserName</key>\n <string>${serviceUser()}</string>\n`
|
|
147
|
+
: '';
|
|
148
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
149
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
150
|
+
<plist version="1.0">
|
|
151
|
+
<dict>
|
|
152
|
+
<key>Label</key>
|
|
153
|
+
<string>${SERVICE_NAME}</string>
|
|
154
|
+
<key>ProgramArguments</key>
|
|
155
|
+
<array>
|
|
156
|
+
<string>${nodePath}</string>
|
|
157
|
+
<string>${script}</string>
|
|
158
|
+
</array>
|
|
159
|
+
<key>EnvironmentVariables</key>
|
|
160
|
+
<dict>
|
|
161
|
+
<key>PATH</key>
|
|
162
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${path.dirname(nodePath)}</string>
|
|
163
|
+
<!-- launchd sets no locale at all, and tmux sanitizes the \x1f separator in
|
|
164
|
+
its -F output to '_' outside a UTF-8 locale, which made list-sessions
|
|
165
|
+
unparseable and the agent report zero terminals. tmux-manager forces
|
|
166
|
+
this per-spawn too; setting it here keeps the daemon's whole
|
|
167
|
+
environment honest for anything else that shells out. -->
|
|
168
|
+
<key>LC_ALL</key>
|
|
169
|
+
<string>C.UTF-8</string>
|
|
170
|
+
</dict>
|
|
171
|
+
${userKey} <key>RunAtLoad</key>
|
|
172
|
+
<true/>
|
|
173
|
+
<key>KeepAlive</key>
|
|
174
|
+
<true/>
|
|
175
|
+
<key>ThrottleInterval</key>
|
|
176
|
+
<integer>5</integer>
|
|
177
|
+
<key>StandardOutPath</key>
|
|
178
|
+
<string>${logDir}/stdout.log</string>
|
|
179
|
+
<key>StandardErrorPath</key>
|
|
180
|
+
<string>${logDir}/stderr.log</string>
|
|
181
|
+
<key>WorkingDirectory</key>
|
|
182
|
+
<string>${os.homedir()}</string>
|
|
183
|
+
</dict>
|
|
184
|
+
</plist>`;
|
|
185
|
+
}
|
|
186
|
+
// Remove the per-user LaunchAgent (used when switching to a system daemon).
|
|
187
|
+
function removeLaunchAgent() {
|
|
188
|
+
const plistPath = getLaunchdPlistPath();
|
|
189
|
+
if (fs.existsSync(plistPath)) {
|
|
190
|
+
try {
|
|
191
|
+
(0, child_process_1.execSync)(`launchctl unload ${plistPath}`);
|
|
192
|
+
}
|
|
193
|
+
catch { /* may not be loaded */ }
|
|
194
|
+
fs.unlinkSync(plistPath);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function installMacOSAgent() {
|
|
198
|
+
const plistPath = getLaunchdPlistPath();
|
|
199
|
+
const dir = path.dirname(plistPath);
|
|
200
|
+
if (!fs.existsSync(dir)) {
|
|
201
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
// Reload cleanly so an existing service picks up the new plist.
|
|
204
|
+
try {
|
|
205
|
+
(0, child_process_1.execSync)(`launchctl unload ${plistPath} 2>/dev/null`);
|
|
206
|
+
}
|
|
207
|
+
catch { /* not loaded */ }
|
|
208
|
+
fs.writeFileSync(plistPath, generatePlist(false));
|
|
209
|
+
(0, child_process_1.execSync)(`launchctl load ${plistPath}`);
|
|
210
|
+
console.log(`Service installed: ${plistPath}`);
|
|
211
|
+
console.log('The agent will start automatically on login.');
|
|
212
|
+
}
|
|
213
|
+
function installMacOSDaemon() {
|
|
214
|
+
const target = getLaunchDaemonPlistPath();
|
|
215
|
+
const staging = path.join((0, config_1.configDirFor)(os.homedir()), `${SERVICE_NAME}.plist`);
|
|
216
|
+
fs.writeFileSync(staging, generatePlist(true));
|
|
217
|
+
// A system daemon and a per-user agent share a Label — drop the agent first.
|
|
218
|
+
removeLaunchAgent();
|
|
219
|
+
if (isRoot()) {
|
|
220
|
+
fs.copyFileSync(staging, target);
|
|
221
|
+
(0, child_process_1.execSync)(`chown root:wheel ${target}`);
|
|
222
|
+
(0, child_process_1.execSync)(`chmod 644 ${target}`);
|
|
223
|
+
try {
|
|
224
|
+
(0, child_process_1.execSync)(`launchctl bootout system ${target} 2>/dev/null`);
|
|
225
|
+
}
|
|
226
|
+
catch { /* not loaded */ }
|
|
227
|
+
try {
|
|
228
|
+
(0, child_process_1.execSync)(`launchctl bootstrap system ${target}`);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
(0, child_process_1.execSync)(`launchctl load ${target}`); // older macOS fallback
|
|
232
|
+
}
|
|
233
|
+
console.log(`System service installed: ${target}`);
|
|
234
|
+
console.log('The agent will start at boot, before login.');
|
|
235
|
+
}
|
|
236
|
+
else if (canPromptSudo()) {
|
|
237
|
+
console.log('Boot service requires admin rights — you may be prompted for your password.\n');
|
|
238
|
+
try {
|
|
239
|
+
(0, child_process_1.execSync)(`sudo bash -c 'cp "${staging}" "${target}" && chown root:wheel "${target}" && chmod 644 "${target}" && { launchctl bootout system "${target}" 2>/dev/null; launchctl bootstrap system "${target}"; }'`, { stdio: 'inherit' });
|
|
240
|
+
console.log(`\nSystem service installed: ${target}`);
|
|
241
|
+
console.log(`The agent will start at boot, before login (as user "${serviceUser()}").`);
|
|
242
|
+
console.log('Verify with: dialout status');
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
console.error('\nsudo install failed. Run these commands manually:');
|
|
246
|
+
printMacDaemonSudoSteps(staging, target);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
console.log('Boot-before-login requires root. Run these commands:');
|
|
251
|
+
printMacDaemonSudoSteps(staging, target);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function printMacDaemonSudoSteps(staging, target) {
|
|
255
|
+
console.log('');
|
|
256
|
+
console.log(` sudo cp ${staging} ${target}`);
|
|
257
|
+
console.log(` sudo chown root:wheel ${target}`);
|
|
258
|
+
console.log(` sudo chmod 644 ${target}`);
|
|
259
|
+
console.log(` sudo launchctl bootstrap system ${target}`);
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log(`Then it runs at boot as user "${serviceUser()}". Verify: dialout status`);
|
|
262
|
+
}
|
|
263
|
+
function uninstallMacOS(deps = {}) {
|
|
264
|
+
const exists = deps.existsSync ?? fs.existsSync;
|
|
265
|
+
const log = deps.log ?? console.log;
|
|
266
|
+
let removed = false;
|
|
267
|
+
const pending = [];
|
|
268
|
+
// System daemon (install-service --system). Needs root to remove, exactly as
|
|
269
|
+
// it needed root to install — so escalate the same way install does.
|
|
270
|
+
const daemonPath = getLaunchDaemonPlistPath();
|
|
271
|
+
if (exists(daemonPath)) {
|
|
272
|
+
const cmd = `launchctl bootout system "${daemonPath}" 2>/dev/null; `
|
|
273
|
+
+ `launchctl unload "${daemonPath}" 2>/dev/null; `
|
|
274
|
+
+ `rm -f "${daemonPath}"`;
|
|
275
|
+
if (runPrivileged(cmd, deps)) {
|
|
276
|
+
log('System service uninstalled.');
|
|
277
|
+
removed = true;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// NOT removed. Say so plainly and leave removed false, so the caller can
|
|
281
|
+
// exit non-zero instead of reporting a success that did not happen.
|
|
282
|
+
log('\nCould not remove the boot service automatically. Run:\n');
|
|
283
|
+
log(` sudo launchctl bootout system ${daemonPath}`);
|
|
284
|
+
log(` sudo rm ${daemonPath}\n`);
|
|
285
|
+
pending.push(`sudo launchctl bootout system ${daemonPath}`, `sudo rm ${daemonPath}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Per-user agent — owned by this user, so no escalation is ever needed.
|
|
289
|
+
const agentPath = getLaunchdPlistPath();
|
|
290
|
+
if (exists(agentPath)) {
|
|
291
|
+
removeLaunchAgent();
|
|
292
|
+
log('Service uninstalled.');
|
|
293
|
+
removed = true;
|
|
294
|
+
}
|
|
295
|
+
if (!removed && pending.length === 0)
|
|
296
|
+
log('Service not installed.');
|
|
297
|
+
return { removed, pending };
|
|
298
|
+
}
|
|
299
|
+
// Returns the running PID of a launchd job, or null if loaded-but-stopped / absent.
|
|
300
|
+
function launchdPid(domainTarget) {
|
|
301
|
+
try {
|
|
302
|
+
const out = (0, child_process_1.execSync)(`launchctl print ${domainTarget} 2>/dev/null`, { encoding: 'utf-8' });
|
|
303
|
+
const m = out.match(/\bpid\s*=\s*(\d+)/);
|
|
304
|
+
return m ? parseInt(m[1], 10) : null;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// --- Linux (systemd) ---
|
|
311
|
+
function getSystemdUnitPath(homedir = os.homedir()) {
|
|
312
|
+
return path.join(homedir, '.config', 'systemd', 'user', `${SYSTEMD_UNIT}.service`);
|
|
313
|
+
}
|
|
314
|
+
function getSystemdSystemUnitPath() {
|
|
315
|
+
return `/etc/systemd/system/${SYSTEMD_UNIT}.service`;
|
|
316
|
+
}
|
|
317
|
+
function generateSystemdUnit(system) {
|
|
318
|
+
const nodePath = getNodePath();
|
|
319
|
+
const script = getAgentScript();
|
|
320
|
+
const userLine = system ? `User=${serviceUser()}\n` : '';
|
|
321
|
+
return `[Unit]
|
|
322
|
+
Description=Dialout Agent
|
|
323
|
+
After=network-online.target
|
|
324
|
+
Wants=network-online.target
|
|
325
|
+
|
|
326
|
+
[Service]
|
|
327
|
+
${userLine}ExecStart=${nodePath} ${script}
|
|
328
|
+
# systemd, like launchd, starts a unit with no locale. tmux mangles the \x1f
|
|
329
|
+
# field separator in its -F output to '_' outside a UTF-8 locale, which makes
|
|
330
|
+
# list-sessions unparseable and the agent report zero terminals.
|
|
331
|
+
Environment=LC_ALL=C.UTF-8
|
|
332
|
+
Restart=always
|
|
333
|
+
RestartSec=5
|
|
334
|
+
Environment=NODE_ENV=production
|
|
335
|
+
|
|
336
|
+
[Install]
|
|
337
|
+
WantedBy=${system ? 'multi-user.target' : 'default.target'}
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
function isLingerEnabled(user) {
|
|
341
|
+
try {
|
|
342
|
+
return /Linger=yes/i.test((0, child_process_1.execSync)(`loginctl show-user ${user}`, { encoding: 'utf-8', stdio: 'pipe' }));
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return false; // no such user record yet — enable-linger creates one
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function enableLinger(user, deps = {}) {
|
|
349
|
+
const isEnabled = deps.isEnabled ?? isLingerEnabled;
|
|
350
|
+
const run = deps.run ?? ((cmd) => { (0, child_process_1.execSync)(cmd, { stdio: 'pipe' }); });
|
|
351
|
+
if (isEnabled(user))
|
|
352
|
+
return { ok: true, alreadyOn: true };
|
|
353
|
+
try {
|
|
354
|
+
run(`loginctl enable-linger ${user}`);
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
// Non-root without polkit rights lands here. The unit is still installed
|
|
358
|
+
// and works while logged in, so this is a warning, not an install failure.
|
|
359
|
+
return { ok: false, alreadyOn: false, error: err?.message || 'enable-linger failed' };
|
|
360
|
+
}
|
|
361
|
+
return { ok: isEnabled(user), alreadyOn: false, error: isEnabled(user) ? undefined : 'enable-linger reported success but lingering is still off' };
|
|
362
|
+
}
|
|
363
|
+
function defaultLinuxScope(deps = {}) {
|
|
364
|
+
const root = deps.isRoot ?? isRoot;
|
|
365
|
+
return root() ? 'system' : 'user';
|
|
366
|
+
}
|
|
367
|
+
function installLinuxUser() {
|
|
368
|
+
const unitPath = getSystemdUnitPath();
|
|
369
|
+
const dir = path.dirname(unitPath);
|
|
370
|
+
if (!fs.existsSync(dir)) {
|
|
371
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
372
|
+
}
|
|
373
|
+
fs.writeFileSync(unitPath, generateSystemdUnit(false));
|
|
374
|
+
(0, child_process_1.execSync)('systemctl --user daemon-reload');
|
|
375
|
+
(0, child_process_1.execSync)(`systemctl --user enable ${SYSTEMD_UNIT}`);
|
|
376
|
+
(0, child_process_1.execSync)(`systemctl --user restart ${SYSTEMD_UNIT}`);
|
|
377
|
+
console.log(`Service installed: ${unitPath}`);
|
|
378
|
+
// Without this the unit is torn down on logout — see LingerDeps above.
|
|
379
|
+
const user = serviceUser();
|
|
380
|
+
const linger = enableLinger(user);
|
|
381
|
+
if (linger.ok) {
|
|
382
|
+
console.log('The agent is running and will keep running after you log out (lingering enabled).');
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
console.log('The agent is running and will start on login.');
|
|
386
|
+
console.log('');
|
|
387
|
+
console.log('\x1b[33mWarning:\x1b[0m this service stops when your last session ends.');
|
|
388
|
+
console.log(` Could not enable lingering: ${linger.error}`);
|
|
389
|
+
console.log(` Fix with: sudo loginctl enable-linger ${user}`);
|
|
390
|
+
console.log(' Or install the boot service instead: dialout install-service --system');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function installLinuxSystem() {
|
|
394
|
+
const target = getSystemdSystemUnitPath();
|
|
395
|
+
const staging = path.join((0, config_1.configDirFor)(os.homedir()), `${SYSTEMD_UNIT}.service`);
|
|
396
|
+
fs.writeFileSync(staging, generateSystemdUnit(true));
|
|
397
|
+
if (isRoot()) {
|
|
398
|
+
fs.copyFileSync(staging, target);
|
|
399
|
+
(0, child_process_1.execSync)('systemctl daemon-reload');
|
|
400
|
+
(0, child_process_1.execSync)(`systemctl enable ${SYSTEMD_UNIT}`);
|
|
401
|
+
(0, child_process_1.execSync)(`systemctl restart ${SYSTEMD_UNIT}`);
|
|
402
|
+
console.log(`System service installed: ${target}`);
|
|
403
|
+
console.log('The agent will start at boot, before login.');
|
|
404
|
+
}
|
|
405
|
+
else if (canPromptSudo()) {
|
|
406
|
+
console.log('Boot service requires admin rights — you may be prompted for your password.\n');
|
|
407
|
+
try {
|
|
408
|
+
(0, child_process_1.execSync)(`sudo bash -c 'cp "${staging}" "${target}" && systemctl daemon-reload && systemctl enable --now ${SYSTEMD_UNIT}'`, { stdio: 'inherit' });
|
|
409
|
+
console.log(`\nSystem service installed: ${target}`);
|
|
410
|
+
console.log(`The agent will start at boot, before login (as user "${serviceUser()}").`);
|
|
411
|
+
console.log('Verify with: dialout status');
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
console.error('\nsudo install failed. Run these commands manually:');
|
|
415
|
+
printLinuxSystemSudoSteps(staging, target);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
console.log('Boot-before-login requires root. Run these commands:');
|
|
420
|
+
printLinuxSystemSudoSteps(staging, target);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function printLinuxSystemSudoSteps(staging, target) {
|
|
424
|
+
console.log('');
|
|
425
|
+
console.log(` sudo cp ${staging} ${target}`);
|
|
426
|
+
console.log(` sudo systemctl daemon-reload`);
|
|
427
|
+
console.log(` sudo systemctl enable --now ${SYSTEMD_UNIT}`);
|
|
428
|
+
console.log('');
|
|
429
|
+
console.log(`Then it runs at boot as user "${serviceUser()}". Verify: dialout status`);
|
|
430
|
+
}
|
|
431
|
+
function uninstallLinux(deps = {}) {
|
|
432
|
+
const exists = deps.existsSync ?? fs.existsSync;
|
|
433
|
+
const log = deps.log ?? console.log;
|
|
434
|
+
let removed = false;
|
|
435
|
+
const pending = [];
|
|
436
|
+
// System unit — same escalation story as the macOS LaunchDaemon above.
|
|
437
|
+
const sysPath = getSystemdSystemUnitPath();
|
|
438
|
+
if (exists(sysPath)) {
|
|
439
|
+
const cmd = `systemctl disable --now ${SYSTEMD_UNIT} 2>/dev/null; `
|
|
440
|
+
+ `rm -f "${sysPath}"; `
|
|
441
|
+
+ `systemctl daemon-reload`;
|
|
442
|
+
if (runPrivileged(cmd, deps)) {
|
|
443
|
+
log('System service uninstalled.');
|
|
444
|
+
removed = true;
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
log('\nCould not remove the boot service automatically. Run:\n');
|
|
448
|
+
log(` sudo systemctl disable --now ${SYSTEMD_UNIT}`);
|
|
449
|
+
log(` sudo rm ${sysPath}`);
|
|
450
|
+
log(` sudo systemctl daemon-reload\n`);
|
|
451
|
+
pending.push(`sudo systemctl disable --now ${SYSTEMD_UNIT}`, `sudo rm ${sysPath}`, 'sudo systemctl daemon-reload');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const userPath = getSystemdUnitPath();
|
|
455
|
+
if (exists(userPath)) {
|
|
456
|
+
try {
|
|
457
|
+
(0, child_process_1.execSync)(`systemctl --user disable --now ${SYSTEMD_UNIT}`);
|
|
458
|
+
}
|
|
459
|
+
catch { /* may not be running */ }
|
|
460
|
+
fs.unlinkSync(userPath);
|
|
461
|
+
(0, child_process_1.execSync)('systemctl --user daemon-reload');
|
|
462
|
+
log('Service uninstalled.');
|
|
463
|
+
removed = true;
|
|
464
|
+
}
|
|
465
|
+
if (!removed && pending.length === 0)
|
|
466
|
+
log('Service not installed.');
|
|
467
|
+
return { removed, pending };
|
|
468
|
+
}
|
|
469
|
+
function systemdActive(scope) {
|
|
470
|
+
const flag = scope === 'user' ? '--user ' : '';
|
|
471
|
+
let active = false;
|
|
472
|
+
let pid = null;
|
|
473
|
+
try {
|
|
474
|
+
active = (0, child_process_1.execSync)(`systemctl ${flag}is-active ${SYSTEMD_UNIT} 2>/dev/null`, { encoding: 'utf-8' }).trim() === 'active';
|
|
475
|
+
}
|
|
476
|
+
catch { /* inactive/failed */ }
|
|
477
|
+
try {
|
|
478
|
+
const mp = parseInt((0, child_process_1.execSync)(`systemctl ${flag}show -p MainPID --value ${SYSTEMD_UNIT} 2>/dev/null`, { encoding: 'utf-8' }).trim(), 10);
|
|
479
|
+
pid = mp > 0 ? mp : null;
|
|
480
|
+
}
|
|
481
|
+
catch { /* none */ }
|
|
482
|
+
return { active, pid };
|
|
483
|
+
}
|
|
484
|
+
function installService(opts = {}) {
|
|
485
|
+
const platform = os.platform();
|
|
486
|
+
if (platform === 'darwin') {
|
|
487
|
+
opts.system ? installMacOSDaemon() : installMacOSAgent();
|
|
488
|
+
}
|
|
489
|
+
else if (platform === 'linux') {
|
|
490
|
+
opts.system ? installLinuxSystem() : installLinuxUser();
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
console.error(`Unsupported platform: ${platform}. Use "dialout start --daemon" instead.`);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function uninstallService(deps = {}) {
|
|
498
|
+
const platform = deps.platform ?? os.platform();
|
|
499
|
+
if (platform === 'darwin') {
|
|
500
|
+
return uninstallMacOS(deps);
|
|
501
|
+
}
|
|
502
|
+
else if (platform === 'linux') {
|
|
503
|
+
return uninstallLinux(deps);
|
|
504
|
+
}
|
|
505
|
+
console.error(`Unsupported platform: ${platform}`);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
function isServiceInstalled() {
|
|
509
|
+
const platform = os.platform();
|
|
510
|
+
if (platform === 'darwin') {
|
|
511
|
+
return fs.existsSync(getLaunchDaemonPlistPath()) || fs.existsSync(getLaunchdPlistPath());
|
|
512
|
+
}
|
|
513
|
+
else if (platform === 'linux') {
|
|
514
|
+
return fs.existsSync(getSystemdSystemUnitPath()) || fs.existsSync(getSystemdUnitPath());
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
// Reports the REAL state of the managed service (queries launchd/systemd),
|
|
519
|
+
// not the manual-daemon pidfile.
|
|
520
|
+
function getServiceStatus() {
|
|
521
|
+
const platform = os.platform();
|
|
522
|
+
if (platform === 'darwin') {
|
|
523
|
+
if (fs.existsSync(getLaunchDaemonPlistPath())) {
|
|
524
|
+
const pid = launchdPid(`system/${SERVICE_NAME}`);
|
|
525
|
+
return { installed: true, running: pid !== null, pid, kind: 'launchd-daemon', atBoot: true };
|
|
526
|
+
}
|
|
527
|
+
if (fs.existsSync(getLaunchdPlistPath())) {
|
|
528
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
|
|
529
|
+
const pid = launchdPid(`gui/${uid}/${SERVICE_NAME}`);
|
|
530
|
+
return { installed: true, running: pid !== null, pid, kind: 'launchd-agent', atBoot: false };
|
|
531
|
+
}
|
|
532
|
+
return { installed: false, running: false, pid: null, kind: null, atBoot: false };
|
|
533
|
+
}
|
|
534
|
+
if (platform === 'linux') {
|
|
535
|
+
if (fs.existsSync(getSystemdSystemUnitPath())) {
|
|
536
|
+
const { active, pid } = systemdActive('system');
|
|
537
|
+
return { installed: true, running: active, pid, kind: 'systemd-system', atBoot: true };
|
|
538
|
+
}
|
|
539
|
+
if (fs.existsSync(getSystemdUnitPath())) {
|
|
540
|
+
const { active, pid } = systemdActive('user');
|
|
541
|
+
return { installed: true, running: active, pid, kind: 'systemd-user', atBoot: false };
|
|
542
|
+
}
|
|
543
|
+
return { installed: false, running: false, pid: null, kind: null, atBoot: false };
|
|
544
|
+
}
|
|
545
|
+
return { installed: false, running: false, pid: null, kind: null, atBoot: false };
|
|
546
|
+
}
|
|
547
|
+
// --- Cron-based watchdog ---
|
|
548
|
+
const CRON_MARKER = '# dialout-watchdog';
|
|
549
|
+
function getWatchdogPath(homedir = os.homedir()) {
|
|
550
|
+
return path.join((0, config_1.configDirFor)(homedir), 'watchdog.sh');
|
|
551
|
+
}
|
|
552
|
+
function getWatchdogScript() {
|
|
553
|
+
const nodePath = getNodePath();
|
|
554
|
+
const script = getAgentScript();
|
|
555
|
+
const pidFile = path.join((0, config_1.configDirFor)(os.homedir()), 'daemon.pid');
|
|
556
|
+
const logDir = path.join((0, config_1.configDirFor)(os.homedir()), 'logs');
|
|
557
|
+
if (!fs.existsSync(logDir)) {
|
|
558
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
559
|
+
}
|
|
560
|
+
const watchdogPath = getWatchdogPath();
|
|
561
|
+
const content = `#!/bin/bash
|
|
562
|
+
# Dialout Agent Watchdog — auto-restart if not running
|
|
563
|
+
PID_FILE="${pidFile}"
|
|
564
|
+
LOG="${logDir}/watchdog.log"
|
|
565
|
+
NODE="${nodePath}"
|
|
566
|
+
SCRIPT="${script}"
|
|
567
|
+
|
|
568
|
+
is_running() {
|
|
569
|
+
if [ -f "$PID_FILE" ]; then
|
|
570
|
+
PID=$(cat "$PID_FILE" 2>/dev/null)
|
|
571
|
+
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
|
|
572
|
+
return 0
|
|
573
|
+
fi
|
|
574
|
+
fi
|
|
575
|
+
return 1
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if is_running; then
|
|
579
|
+
exit 0
|
|
580
|
+
fi
|
|
581
|
+
|
|
582
|
+
echo "[$(date)] Agent not running — restarting..." >> "$LOG"
|
|
583
|
+
nohup "$NODE" "$SCRIPT" >> "${logDir}/stdout.log" 2>> "${logDir}/stderr.log" &
|
|
584
|
+
echo $! > "$PID_FILE"
|
|
585
|
+
echo "[$(date)] Started with PID $!" >> "$LOG"
|
|
586
|
+
`;
|
|
587
|
+
// Write-then-rename instead of a truncating in-place fs.writeFileSync.
|
|
588
|
+
// Two reasons:
|
|
589
|
+
// 1. Atomicity: if the process dies mid-write (OOM, power loss, SIGKILL),
|
|
590
|
+
// an in-place write can leave a truncated, syntactically-broken script
|
|
591
|
+
// sitting exactly where cron invokes it every N minutes. rename(2) on
|
|
592
|
+
// the same filesystem is a single atomic directory-entry swap — cron
|
|
593
|
+
// only ever sees the old complete file or the new complete file, never
|
|
594
|
+
// a partial one.
|
|
595
|
+
// 2. Mode: POSIX ignores the `mode` passed to open()/writeFileSync when
|
|
596
|
+
// O_CREAT hits a path that already exists — so { mode: 0o755 } on an
|
|
597
|
+
// in-place writeFileSync silently does nothing if the target file had
|
|
598
|
+
// already lost its executable bit (a hardening scan, a backup/restore
|
|
599
|
+
// tool, or a manual `chmod 644` while debugging). Writing to a brand
|
|
600
|
+
// new temp file always honors `mode` (there's nothing existing to
|
|
601
|
+
// ignore it for), and rename() moves that file's mode along with it,
|
|
602
|
+
// replacing whatever permissions the old target had.
|
|
603
|
+
// The temp name is unique per call (pid + high-res time + random suffix)
|
|
604
|
+
// so concurrent getWatchdogScript() calls (e.g. setup-cron racing a
|
|
605
|
+
// manual repair) can't collide on it.
|
|
606
|
+
const tmpPath = `${watchdogPath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
607
|
+
try {
|
|
608
|
+
fs.writeFileSync(tmpPath, content, { mode: 0o755 });
|
|
609
|
+
fs.chmodSync(tmpPath, 0o755); // belt-and-suspenders: independent of umask, since tmpPath is always newly created
|
|
610
|
+
fs.renameSync(tmpPath, watchdogPath);
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
try {
|
|
614
|
+
fs.unlinkSync(tmpPath);
|
|
615
|
+
}
|
|
616
|
+
catch { /* tmp file may not have been created */ }
|
|
617
|
+
throw err;
|
|
618
|
+
}
|
|
619
|
+
return watchdogPath;
|
|
620
|
+
}
|
|
621
|
+
function installCron(intervalMinutes = 5) {
|
|
622
|
+
const watchdogPath = getWatchdogScript();
|
|
623
|
+
// Get existing crontab (without our entry)
|
|
624
|
+
let existing = '';
|
|
625
|
+
try {
|
|
626
|
+
existing = (0, child_process_1.execSync)('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
627
|
+
}
|
|
628
|
+
catch { /* no existing crontab */ }
|
|
629
|
+
// Remove any old dialout entries
|
|
630
|
+
const lines = existing.split('\n').filter((l) => !l.includes(CRON_MARKER) && l.trim() !== '');
|
|
631
|
+
// Add new entry
|
|
632
|
+
lines.push(`*/${intervalMinutes} * * * * ${watchdogPath} ${CRON_MARKER}`);
|
|
633
|
+
lines.push(''); // trailing newline
|
|
634
|
+
(0, child_process_1.execSync)('crontab -', { input: lines.join('\n'), encoding: 'utf-8' });
|
|
635
|
+
console.log(`Cron watchdog installed — checks every ${intervalMinutes} minute${intervalMinutes > 1 ? 's' : ''}`);
|
|
636
|
+
console.log(`Watchdog script: ${watchdogPath}`);
|
|
637
|
+
console.log('The agent will auto-restart if it dies.');
|
|
638
|
+
}
|
|
639
|
+
function uninstallCron() {
|
|
640
|
+
let existing = '';
|
|
641
|
+
try {
|
|
642
|
+
existing = (0, child_process_1.execSync)('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
console.log('No crontab found.');
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const lines = existing.split('\n').filter((l) => !l.includes(CRON_MARKER));
|
|
649
|
+
const cleaned = lines.filter((l) => l.trim() !== '');
|
|
650
|
+
if (cleaned.length === 0) {
|
|
651
|
+
(0, child_process_1.execSync)('crontab -r 2>/dev/null', { encoding: 'utf-8' }).toString();
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
cleaned.push('');
|
|
655
|
+
(0, child_process_1.execSync)('crontab -', { input: cleaned.join('\n'), encoding: 'utf-8' });
|
|
656
|
+
}
|
|
657
|
+
// Clean up watchdog script
|
|
658
|
+
const watchdogPath = getWatchdogPath();
|
|
659
|
+
if (fs.existsSync(watchdogPath))
|
|
660
|
+
fs.unlinkSync(watchdogPath);
|
|
661
|
+
console.log('Cron watchdog removed.');
|
|
662
|
+
}
|
|
663
|
+
function isCronInstalled() {
|
|
664
|
+
try {
|
|
665
|
+
const crontab = (0, child_process_1.execSync)('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
666
|
+
return crontab.includes(CRON_MARKER);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// --- listSupervisors: report every supervisor, not just the first match ---
|
|
673
|
+
//
|
|
674
|
+
// getServiceStatus() and isServiceInstalled() (above) intentionally return on
|
|
675
|
+
// the first match — that shape is left untouched, existing callers depend on
|
|
676
|
+
// it. But it means a coexisting second supervisor (e.g. a stale user systemd
|
|
677
|
+
// unit next to a correct system unit) is invisible to them. This is how a
|
|
678
|
+
// production incident happened: three supervisors + a manually-started
|
|
679
|
+
// daemon were all racing for the single-instance lock, and `status` reported
|
|
680
|
+
// only one of them.
|
|
681
|
+
//
|
|
682
|
+
// This function checks every supervisor slot independently and returns all
|
|
683
|
+
// of them, so `dialout status` (in cli.ts) can show the full picture.
|
|
684
|
+
/** Parse the script path out of a systemd unit's `ExecStart=<node> <script>` line. */
|
|
685
|
+
function parseScriptFromUnit(content) {
|
|
686
|
+
const m = content.match(/^ExecStart=(.+)$/m);
|
|
687
|
+
if (!m)
|
|
688
|
+
return null;
|
|
689
|
+
const parts = m[1].trim().split(/\s+/);
|
|
690
|
+
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
691
|
+
}
|
|
692
|
+
/** Parse the script path out of a launchd plist's <ProgramArguments> array (2nd <string>, after the node binary). */
|
|
693
|
+
function parseScriptFromPlist(content) {
|
|
694
|
+
const arrayMatch = content.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
695
|
+
if (!arrayMatch)
|
|
696
|
+
return null;
|
|
697
|
+
const strings = [...arrayMatch[1].matchAll(/<string>([^<]*)<\/string>/g)].map((m) => m[1]);
|
|
698
|
+
return strings.length > 1 ? strings[1] : null;
|
|
699
|
+
}
|
|
700
|
+
/** Parse the script path out of the cron watchdog's `SCRIPT="…"` line. */
|
|
701
|
+
function parseScriptFromWatchdog(content) {
|
|
702
|
+
const m = content.match(/^SCRIPT="(.*)"$/m);
|
|
703
|
+
return m ? m[1] : null;
|
|
704
|
+
}
|
|
705
|
+
// A literal string compare is fooled by a symlinked global-install root
|
|
706
|
+
// (Homebrew-linked npm prefix, `npm link`, a package manager resolving
|
|
707
|
+
// through an intermediate symlink): two textually different paths can name
|
|
708
|
+
// the same real file, or vice versa. Canonicalize both sides via realpath
|
|
709
|
+
// before comparing. A *stale* target by definition may point at a file that
|
|
710
|
+
// was deleted (the pre-rename package no longer installed) — realpath throws
|
|
711
|
+
// for that, which is exactly the case staleness detection most needs to
|
|
712
|
+
// survive, so fall back to path.resolve() (no symlink following, but still
|
|
713
|
+
// normalizes `.`/`..`/separators) rather than letting the throw escape.
|
|
714
|
+
// Shared by listSupervisors() and repairWatchdog() so there is one place that
|
|
715
|
+
// decides what "the same script" means.
|
|
716
|
+
function normalizeScriptPath(p, realpath = fs.realpathSync) {
|
|
717
|
+
try {
|
|
718
|
+
return realpath(p);
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
return path.resolve(p);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/** Every supervisor present, not just the first match. */
|
|
725
|
+
function listSupervisors(deps = {}) {
|
|
726
|
+
const platform = deps.platform ?? os.platform();
|
|
727
|
+
const homedir = deps.homedir ?? os.homedir();
|
|
728
|
+
const existsSync = deps.existsSync ?? fs.existsSync;
|
|
729
|
+
const readFile = deps.readFileSync ?? ((p) => fs.readFileSync(p, 'utf-8'));
|
|
730
|
+
const launchdPidFn = deps.launchdPid ?? launchdPid;
|
|
731
|
+
const systemdActiveFn = deps.systemdActive ?? systemdActive;
|
|
732
|
+
const isCronInstalledFn = deps.isCronInstalled ?? isCronInstalled;
|
|
733
|
+
const agentScript = deps.agentScript ?? getAgentScript();
|
|
734
|
+
const realpath = deps.realpathSync ?? fs.realpathSync;
|
|
735
|
+
const readSafe = (p) => {
|
|
736
|
+
try {
|
|
737
|
+
return readFile(p);
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
return '';
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
// normalizeScriptPath (module-level, shared with repairWatchdog()) handles
|
|
744
|
+
// symlink-equivalent paths and deleted stale targets — see its comment.
|
|
745
|
+
const normalizedAgentScript = normalizeScriptPath(agentScript, realpath);
|
|
746
|
+
const isStale = (targetScript) => targetScript !== null && normalizeScriptPath(targetScript, realpath) !== normalizedAgentScript;
|
|
747
|
+
const results = [];
|
|
748
|
+
if (platform === 'darwin') {
|
|
749
|
+
const daemonPath = getLaunchDaemonPlistPath();
|
|
750
|
+
if (existsSync(daemonPath)) {
|
|
751
|
+
const pid = launchdPidFn(`system/${SERVICE_NAME}`);
|
|
752
|
+
const targetScript = parseScriptFromPlist(readSafe(daemonPath));
|
|
753
|
+
results.push({
|
|
754
|
+
kind: 'launchd-daemon',
|
|
755
|
+
path: daemonPath,
|
|
756
|
+
running: pid !== null,
|
|
757
|
+
pid,
|
|
758
|
+
atBoot: true,
|
|
759
|
+
targetScript: targetScript ?? '',
|
|
760
|
+
stale: isStale(targetScript),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
const agentPath = getLaunchdPlistPath(homedir);
|
|
764
|
+
if (existsSync(agentPath)) {
|
|
765
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
|
|
766
|
+
const pid = launchdPidFn(`gui/${uid}/${SERVICE_NAME}`);
|
|
767
|
+
const targetScript = parseScriptFromPlist(readSafe(agentPath));
|
|
768
|
+
results.push({
|
|
769
|
+
kind: 'launchd-agent',
|
|
770
|
+
path: agentPath,
|
|
771
|
+
running: pid !== null,
|
|
772
|
+
pid,
|
|
773
|
+
atBoot: false,
|
|
774
|
+
targetScript: targetScript ?? '',
|
|
775
|
+
stale: isStale(targetScript),
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
else if (platform === 'linux') {
|
|
780
|
+
const systemUnitPath = getSystemdSystemUnitPath();
|
|
781
|
+
if (existsSync(systemUnitPath)) {
|
|
782
|
+
const { active, pid } = systemdActiveFn('system');
|
|
783
|
+
const targetScript = parseScriptFromUnit(readSafe(systemUnitPath));
|
|
784
|
+
results.push({
|
|
785
|
+
kind: 'systemd-system',
|
|
786
|
+
path: systemUnitPath,
|
|
787
|
+
running: active,
|
|
788
|
+
pid,
|
|
789
|
+
atBoot: true,
|
|
790
|
+
targetScript: targetScript ?? '',
|
|
791
|
+
stale: isStale(targetScript),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
const userUnitPath = getSystemdUnitPath(homedir);
|
|
795
|
+
if (existsSync(userUnitPath)) {
|
|
796
|
+
const { active, pid } = systemdActiveFn('user');
|
|
797
|
+
const targetScript = parseScriptFromUnit(readSafe(userUnitPath));
|
|
798
|
+
results.push({
|
|
799
|
+
kind: 'systemd-user',
|
|
800
|
+
path: userUnitPath,
|
|
801
|
+
running: active,
|
|
802
|
+
pid,
|
|
803
|
+
atBoot: false,
|
|
804
|
+
targetScript: targetScript ?? '',
|
|
805
|
+
stale: isStale(targetScript),
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (isCronInstalledFn()) {
|
|
810
|
+
const watchdogPath = getWatchdogPath(homedir);
|
|
811
|
+
const targetScript = parseScriptFromWatchdog(readSafe(watchdogPath));
|
|
812
|
+
results.push({
|
|
813
|
+
kind: 'cron',
|
|
814
|
+
path: watchdogPath,
|
|
815
|
+
// Cron has no single long-lived process of its own to probe — its
|
|
816
|
+
// watchdog re-launches the agent on a schedule. Presence in the
|
|
817
|
+
// crontab (the only reason this entry exists at all) is what
|
|
818
|
+
// "running" means here.
|
|
819
|
+
running: true,
|
|
820
|
+
pid: null,
|
|
821
|
+
atBoot: false,
|
|
822
|
+
targetScript: targetScript ?? '',
|
|
823
|
+
stale: isStale(targetScript),
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
return results;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Backup filename that can't collide even if repair runs twice within the
|
|
830
|
+
* same second (or the same millisecond, under a mocked/frozen clock): start
|
|
831
|
+
* from an ISO-ish timestamp, then append an incrementing suffix until the
|
|
832
|
+
* candidate path doesn't already exist.
|
|
833
|
+
*/
|
|
834
|
+
function nextBackupPath(watchdogPath) {
|
|
835
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
836
|
+
let candidate = `${watchdogPath}.bak-${stamp}`;
|
|
837
|
+
let n = 1;
|
|
838
|
+
while (fs.existsSync(candidate)) {
|
|
839
|
+
candidate = `${watchdogPath}.bak-${stamp}-${n}`;
|
|
840
|
+
n += 1;
|
|
841
|
+
}
|
|
842
|
+
return candidate;
|
|
843
|
+
}
|
|
844
|
+
// --- staleSupervisorAdvice: the correct next step for a stale supervisor ---
|
|
845
|
+
//
|
|
846
|
+
// repairWatchdog() above rewrites exactly one file: the cron watchdog's
|
|
847
|
+
// SCRIPT= line. It never touches a launchd plist or a systemd unit. So
|
|
848
|
+
// telling the operator to run "dialout repair" for a stale unit is a
|
|
849
|
+
// dead end — repair will report "nothing to repair" while the unit stays
|
|
850
|
+
// stale.
|
|
851
|
+
//
|
|
852
|
+
// installService() (this module) regenerates a unit's ExecStart /
|
|
853
|
+
// ProgramArguments from the current getAgentScript() on every call:
|
|
854
|
+
// installMacOSAgent() and installLinuxUser() do an unconditional
|
|
855
|
+
// fs.writeFileSync over any existing file; installMacOSDaemon() and
|
|
856
|
+
// installLinuxSystem() do an unconditional fs.copyFileSync (root path) or
|
|
857
|
+
// `sudo cp` (non-root path) over the existing target. None of the four
|
|
858
|
+
// check "does a file already exist" first — they always overwrite. So
|
|
859
|
+
// re-running the matching install-service variant (plain for a login-kind
|
|
860
|
+
// unit, --system for a boot-kind unit — see SupervisorInfo.atBoot) genuinely
|
|
861
|
+
// fixes a stale unit. uninstall-service is offered as the alternative for
|
|
862
|
+
// when the stale unit is a redundant extra supervisor that should just be
|
|
863
|
+
// removed instead of kept.
|
|
864
|
+
function staleSupervisorAdvice(s) {
|
|
865
|
+
if (s.kind === 'cron') {
|
|
866
|
+
return [
|
|
867
|
+
` ⚠ ${s.path} points at a stale script (${s.targetScript}).`,
|
|
868
|
+
' Run "dialout repair" to fix it.',
|
|
869
|
+
];
|
|
870
|
+
}
|
|
871
|
+
const installCmd = s.atBoot ? 'dialout install-service --system' : 'dialout install-service';
|
|
872
|
+
return [
|
|
873
|
+
` ⚠ ${s.path} points at a stale script (${s.targetScript}).`,
|
|
874
|
+
` Run "${installCmd}" to regenerate it from the current build,`,
|
|
875
|
+
' or "dialout uninstall-service" if this supervisor is redundant.',
|
|
876
|
+
];
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Rewrite `~/.dialout/watchdog.sh` if its SCRIPT= line has drifted from
|
|
880
|
+
* the current agent script. No-op (repaired: false) when the watchdog is
|
|
881
|
+
* absent or already correct — in particular, an already-correct watchdog is
|
|
882
|
+
* left byte-for-byte and mtime-untouched, and no backup is created.
|
|
883
|
+
*/
|
|
884
|
+
function repairWatchdog() {
|
|
885
|
+
const watchdogPath = getWatchdogPath();
|
|
886
|
+
const agentScript = getAgentScript();
|
|
887
|
+
if (!fs.existsSync(watchdogPath)) {
|
|
888
|
+
return { repaired: false, from: '', to: agentScript };
|
|
889
|
+
}
|
|
890
|
+
const content = fs.readFileSync(watchdogPath, 'utf-8');
|
|
891
|
+
const currentTarget = parseScriptFromWatchdog(content);
|
|
892
|
+
const stale = currentTarget !== null && normalizeScriptPath(currentTarget) !== normalizeScriptPath(agentScript);
|
|
893
|
+
if (!stale) {
|
|
894
|
+
return { repaired: false, from: currentTarget ?? agentScript, to: agentScript };
|
|
895
|
+
}
|
|
896
|
+
// Preserve the stale file before touching it.
|
|
897
|
+
const backupPath = nextBackupPath(watchdogPath);
|
|
898
|
+
fs.copyFileSync(watchdogPath, backupPath);
|
|
899
|
+
// Reuse the one generator — this is the "single source of truth" the task
|
|
900
|
+
// requires, not a second hand-written copy of the watchdog body.
|
|
901
|
+
getWatchdogScript();
|
|
902
|
+
// Belt-and-suspenders: getWatchdogScript()'s write-then-rename already
|
|
903
|
+
// guarantees the final file is 0755 regardless of what mode the old,
|
|
904
|
+
// stale watchdog had (rename() carries the temp file's mode, not the
|
|
905
|
+
// replaced target's). Enforce it here too, explicitly, so repair's
|
|
906
|
+
// executable-bit guarantee does not rely solely on that implementation
|
|
907
|
+
// detail of the generator it happens to call.
|
|
908
|
+
fs.chmodSync(watchdogPath, 0o755);
|
|
909
|
+
return { repaired: true, from: currentTarget, to: agentScript };
|
|
910
|
+
}
|
|
911
|
+
//# sourceMappingURL=service-installer.js.map
|