@indianic/dialout 1.1.2 → 1.2.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 +420 -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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1427 @@
|
|
|
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
|
+
const commander_1 = require("commander");
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const readline = __importStar(require("readline"));
|
|
41
|
+
const https = __importStar(require("https"));
|
|
42
|
+
const http = __importStar(require("http"));
|
|
43
|
+
const config_1 = require("./config");
|
|
44
|
+
const websocket_1 = require("./websocket");
|
|
45
|
+
const service_installer_1 = require("./service-installer");
|
|
46
|
+
const update_check_1 = require("./update-check");
|
|
47
|
+
const cowork_1 = require("./cowork");
|
|
48
|
+
const ssh_local_network_1 = require("./ssh-local-network");
|
|
49
|
+
const terminal_detect_1 = require("./terminal-detect");
|
|
50
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
|
|
51
|
+
const program = new commander_1.Command();
|
|
52
|
+
// Shared between `status` (reporting) and `repair` (which reuses the same
|
|
53
|
+
// wording when it re-checks after fixing the watchdog).
|
|
54
|
+
const supervisorLabel = {
|
|
55
|
+
'launchd-agent': 'launchd (user agent, at login)',
|
|
56
|
+
'launchd-daemon': 'launchd (system daemon, at boot)',
|
|
57
|
+
'systemd-user': 'systemd (user unit, at login)',
|
|
58
|
+
'systemd-system': 'systemd (system unit, at boot)',
|
|
59
|
+
cron: 'cron watchdog',
|
|
60
|
+
};
|
|
61
|
+
program
|
|
62
|
+
.name('dialout')
|
|
63
|
+
.description(`Dialout Agent v${pkg.version} — connects your machine to the Dialout server
|
|
64
|
+
|
|
65
|
+
Enables remote port scanning, browser-based terminal sessions,
|
|
66
|
+
and filesystem browsing from the Dialout dashboard.
|
|
67
|
+
|
|
68
|
+
Quick Start:
|
|
69
|
+
$ dialout init # interactive setup (remote/local + API key)
|
|
70
|
+
$ dialout start # run in foreground (Ctrl+C to stop)
|
|
71
|
+
$ dialout start --daemon # run in background
|
|
72
|
+
$ dialout stop # stop background agent
|
|
73
|
+
$ dialout restart # restart background agent
|
|
74
|
+
$ dialout status # check agent & service status
|
|
75
|
+
|
|
76
|
+
Profiles (local + remote configs side by side):
|
|
77
|
+
$ dialout profiles # list saved profiles
|
|
78
|
+
$ dialout use local # switch active profile
|
|
79
|
+
$ dialout use remote # switch back
|
|
80
|
+
$ dialout start --profile local # one-off run, active profile unchanged
|
|
81
|
+
|
|
82
|
+
Service Management (auto-start on boot):
|
|
83
|
+
$ dialout install-service # install launchd (macOS) / systemd (Linux) service
|
|
84
|
+
$ dialout uninstall-service # remove OS service
|
|
85
|
+
|
|
86
|
+
Watchdog (auto-restart if agent dies):
|
|
87
|
+
$ dialout setup-cron # install cron watchdog (default: every 5 min)
|
|
88
|
+
$ dialout setup-cron -i 3 # check every 3 minutes
|
|
89
|
+
$ dialout remove-cron # remove cron watchdog
|
|
90
|
+
$ dialout repair # fix a stale watchdog (e.g. after a package rename)
|
|
91
|
+
|
|
92
|
+
Configuration:
|
|
93
|
+
$ dialout config show # print current config (API key masked)
|
|
94
|
+
$ dialout config set <key> <val> # set a config value
|
|
95
|
+
$ dialout config path # show config file path
|
|
96
|
+
$ dialout config reset # reset config to defaults (keeps API key)
|
|
97
|
+
|
|
98
|
+
Updates:
|
|
99
|
+
$ dialout update # update to latest version
|
|
100
|
+
$ dialout --version # show current version
|
|
101
|
+
|
|
102
|
+
Config file: ~/.dialout/config.json
|
|
103
|
+
Docs: https://www.dialout.dev/docs/agent`)
|
|
104
|
+
.version(pkg.version);
|
|
105
|
+
// Helper: readline question
|
|
106
|
+
function createRL() {
|
|
107
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
108
|
+
}
|
|
109
|
+
function ask(rl, q) {
|
|
110
|
+
return new Promise((resolve) => rl.question(q, resolve));
|
|
111
|
+
}
|
|
112
|
+
// Interactive arrow-key multi-select. Renders checklist.ts frames in raw mode.
|
|
113
|
+
// Returns the selected tokens, or null if the user cancels (Esc/q).
|
|
114
|
+
function promptChecklist(items) {
|
|
115
|
+
const { moveCursor, toggleAt, toggleAll, selectedTokens, renderChecklist } = require('./checklist');
|
|
116
|
+
const rl = require('readline');
|
|
117
|
+
let state = { items, cursor: 0 };
|
|
118
|
+
const legend = ' \x1b[2m↑/↓ move · Space toggle · a all · Enter confirm · Esc cancel\x1b[0m\n' +
|
|
119
|
+
' \x1b[2mSelected = wrapped in tmux for remote (Shift/Fn+drag to copy). Unselected = fully native.\x1b[0m\n\n';
|
|
120
|
+
rl.emitKeypressEvents(process.stdin);
|
|
121
|
+
process.stdin.setRawMode(true);
|
|
122
|
+
process.stdin.resume();
|
|
123
|
+
let prevLines = 0;
|
|
124
|
+
const draw = () => {
|
|
125
|
+
const frame = renderChecklist(state);
|
|
126
|
+
if (prevLines > 0)
|
|
127
|
+
process.stdout.write(`\x1b[${prevLines}A`); // up N
|
|
128
|
+
process.stdout.write('\x1b[0J'); // clear to end of screen
|
|
129
|
+
process.stdout.write(frame + '\n');
|
|
130
|
+
prevLines = frame.split('\n').length;
|
|
131
|
+
};
|
|
132
|
+
process.stdout.write(legend);
|
|
133
|
+
draw();
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
const onKey = (_s, key) => {
|
|
136
|
+
if (!key)
|
|
137
|
+
return;
|
|
138
|
+
if (key.ctrl && key.name === 'c') {
|
|
139
|
+
cleanup();
|
|
140
|
+
process.exit(130);
|
|
141
|
+
}
|
|
142
|
+
if (key.name === 'up')
|
|
143
|
+
state = moveCursor(state, -1);
|
|
144
|
+
else if (key.name === 'down')
|
|
145
|
+
state = moveCursor(state, 1);
|
|
146
|
+
else if (key.name === 'space')
|
|
147
|
+
state = toggleAt(state);
|
|
148
|
+
else if (key.name === 'a')
|
|
149
|
+
state = toggleAll(state);
|
|
150
|
+
else if (key.name === 'return')
|
|
151
|
+
return finish(selectedTokens(state));
|
|
152
|
+
else if (key.name === 'escape' || key.name === 'q')
|
|
153
|
+
return finish(null);
|
|
154
|
+
else
|
|
155
|
+
return;
|
|
156
|
+
draw();
|
|
157
|
+
};
|
|
158
|
+
const cleanup = () => {
|
|
159
|
+
process.stdin.off('keypress', onKey);
|
|
160
|
+
if (process.stdin.isTTY)
|
|
161
|
+
process.stdin.setRawMode(false);
|
|
162
|
+
process.stdin.pause();
|
|
163
|
+
};
|
|
164
|
+
const finish = (result) => {
|
|
165
|
+
cleanup();
|
|
166
|
+
process.stdout.write('\n');
|
|
167
|
+
resolve(result);
|
|
168
|
+
};
|
|
169
|
+
process.stdin.on('keypress', onKey);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Helper: validate API key against the server
|
|
173
|
+
async function validateApiKey(serverUrl, apiKey) {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
try {
|
|
176
|
+
// Convert wss:// to https:// for the validation endpoint
|
|
177
|
+
const httpUrl = serverUrl
|
|
178
|
+
.replace(/^wss:\/\//, 'https://')
|
|
179
|
+
.replace(/^ws:\/\//, 'http://')
|
|
180
|
+
.replace(/\/ws\/?$/, '');
|
|
181
|
+
const url = `${httpUrl}/api/machines?userId=0`;
|
|
182
|
+
const mod = httpUrl.startsWith('https') ? https : http;
|
|
183
|
+
const req = mod.get(url, {
|
|
184
|
+
headers: { 'X-API-Key': apiKey },
|
|
185
|
+
timeout: 8000,
|
|
186
|
+
}, (res) => {
|
|
187
|
+
// The server doesn't validate API key on this endpoint directly,
|
|
188
|
+
// but we can test WebSocket connectivity by attempting a connection
|
|
189
|
+
resolve({ valid: true });
|
|
190
|
+
});
|
|
191
|
+
req.on('error', (err) => {
|
|
192
|
+
resolve({ valid: false, error: `Cannot reach server: ${err.message}` });
|
|
193
|
+
});
|
|
194
|
+
req.on('timeout', () => {
|
|
195
|
+
req.destroy();
|
|
196
|
+
resolve({ valid: false, error: 'Server connection timed out' });
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
resolve({ valid: false, error: err.message });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// Helper: validate key via WebSocket handshake (most reliable)
|
|
205
|
+
async function validateKeyViaWS(serverUrl, apiKey) {
|
|
206
|
+
return new Promise((resolve) => {
|
|
207
|
+
try {
|
|
208
|
+
const WebSocket = require('ws');
|
|
209
|
+
const url = serverUrl.replace(/\/$/, '') + '/daemon';
|
|
210
|
+
const ws = new WebSocket(url, { headers: { 'X-API-Key': apiKey } });
|
|
211
|
+
const timeout = setTimeout(() => {
|
|
212
|
+
ws.close();
|
|
213
|
+
resolve({ valid: false, error: 'Connection timed out' });
|
|
214
|
+
}, 8000);
|
|
215
|
+
ws.on('open', () => {
|
|
216
|
+
// Connected — wait briefly for auth response
|
|
217
|
+
});
|
|
218
|
+
ws.on('message', (raw) => {
|
|
219
|
+
try {
|
|
220
|
+
const msg = JSON.parse(raw.toString());
|
|
221
|
+
if (msg.type === 'auth_ok') {
|
|
222
|
+
clearTimeout(timeout);
|
|
223
|
+
ws.close();
|
|
224
|
+
resolve({ valid: true });
|
|
225
|
+
}
|
|
226
|
+
else if (msg.type === 'auth_error' || msg.type === 'error') {
|
|
227
|
+
clearTimeout(timeout);
|
|
228
|
+
ws.close();
|
|
229
|
+
resolve({ valid: false, error: msg.error || 'Invalid API key' });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch { }
|
|
233
|
+
});
|
|
234
|
+
ws.on('close', (code) => {
|
|
235
|
+
clearTimeout(timeout);
|
|
236
|
+
if (code === 4001 || code === 4003) {
|
|
237
|
+
resolve({ valid: false, error: 'Invalid API key — check your key and try again' });
|
|
238
|
+
}
|
|
239
|
+
// If we haven't resolved yet, connection closed unexpectedly
|
|
240
|
+
});
|
|
241
|
+
ws.on('error', (err) => {
|
|
242
|
+
clearTimeout(timeout);
|
|
243
|
+
resolve({ valid: false, error: `Connection failed: ${err.message}` });
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
resolve({ valid: false, error: err.message });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// --- init ---
|
|
252
|
+
program
|
|
253
|
+
.command('init')
|
|
254
|
+
.description('Configure the agent with server URL and API key (interactive)')
|
|
255
|
+
.addHelpText('after', `
|
|
256
|
+
Steps:
|
|
257
|
+
1. Go to Dialout web UI → MACHINES tab → click GENERATE KEY
|
|
258
|
+
2. Copy the API key (shown only once)
|
|
259
|
+
3. Run this command and paste the server URL + API key
|
|
260
|
+
|
|
261
|
+
Example:
|
|
262
|
+
$ dialout init
|
|
263
|
+
Server URL [wss://www.dialout.dev/ws]:
|
|
264
|
+
API Key: mch_K27F43P1HcN8AIO3EDHwkKvgZl5hkV1t`)
|
|
265
|
+
.action(async () => {
|
|
266
|
+
const rl = createRL();
|
|
267
|
+
const config = (0, config_1.loadConfig)();
|
|
268
|
+
console.log('');
|
|
269
|
+
console.log('\x1b[1mDialout Agent Setup\x1b[0m');
|
|
270
|
+
console.log('\x1b[90m' + '─'.repeat(44) + '\x1b[0m');
|
|
271
|
+
console.log('');
|
|
272
|
+
// Step 1: Which server? Saved as a named profile so local and remote
|
|
273
|
+
// configs coexist — switch anytime with "dialout use <profile>".
|
|
274
|
+
// Work out what "Remote" will actually default to BEFORE drawing the menu.
|
|
275
|
+
// Printing DEFAULT_SERVER_URL on the menu line and then defaulting the next
|
|
276
|
+
// prompt to a different saved URL is how someone ends up validating a key
|
|
277
|
+
// for one server against another and getting an unexplained 401.
|
|
278
|
+
const savedRemote = config.profiles?.remote?.serverUrl;
|
|
279
|
+
// A localhost URL saved in the "remote" profile is leftover from local
|
|
280
|
+
// testing — choosing Remote must always default to a real remote server.
|
|
281
|
+
const remoteDefault = savedRemote && !/^wss?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/.test(savedRemote)
|
|
282
|
+
? savedRemote
|
|
283
|
+
: config_1.DEFAULT_SERVER_URL;
|
|
284
|
+
const remoteIsSaved = remoteDefault !== config_1.DEFAULT_SERVER_URL;
|
|
285
|
+
console.log('\x1b[1mWhere should this agent connect?\x1b[0m');
|
|
286
|
+
console.log('');
|
|
287
|
+
console.log(` \x1b[36m1)\x1b[0m Remote server — ${remoteDefault}` +
|
|
288
|
+
(remoteIsSaved ? ' \x1b[90m(saved)\x1b[0m' : ''));
|
|
289
|
+
if (remoteIsSaved) {
|
|
290
|
+
console.log(` \x1b[90mthis build ships with ${config_1.DEFAULT_SERVER_URL} — pick 3 to switch\x1b[0m`);
|
|
291
|
+
}
|
|
292
|
+
console.log(` \x1b[36m2)\x1b[0m Local dev — ${config_1.DEFAULT_LOCAL_SERVER_URL} (Dialout running on this machine)`);
|
|
293
|
+
console.log(' \x1b[36m3)\x1b[0m Custom URL');
|
|
294
|
+
console.log('');
|
|
295
|
+
const target = (await ask(rl, 'Choose [1-3, default 1]: ')).trim();
|
|
296
|
+
let profileName;
|
|
297
|
+
let defaultUrl;
|
|
298
|
+
if (target === '2') {
|
|
299
|
+
profileName = 'local';
|
|
300
|
+
defaultUrl = config.profiles?.local?.serverUrl || config_1.DEFAULT_LOCAL_SERVER_URL;
|
|
301
|
+
}
|
|
302
|
+
else if (target === '3') {
|
|
303
|
+
profileName = (await ask(rl, `Profile name [\x1b[36mcustom\x1b[0m]: `)).trim() || 'custom';
|
|
304
|
+
defaultUrl = config.profiles?.[profileName]?.serverUrl || config.serverUrl || config_1.DEFAULT_SERVER_URL;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
profileName = 'remote';
|
|
308
|
+
defaultUrl = remoteDefault;
|
|
309
|
+
}
|
|
310
|
+
const serverUrlInput = await ask(rl, `Server URL [\x1b[36m${defaultUrl}\x1b[0m]: `);
|
|
311
|
+
const serverUrl = (serverUrlInput.trim() || defaultUrl).replace(/\/$/, '');
|
|
312
|
+
// Step 2: API Key (per profile — a local server issues different keys
|
|
313
|
+
// than the remote one)
|
|
314
|
+
const existingKey = config.profiles?.[profileName]?.apiKey || '';
|
|
315
|
+
const keyPrompt = existingKey
|
|
316
|
+
? `API Key [\x1b[90m****${existingKey.slice(-4)}\x1b[0m]: `
|
|
317
|
+
: 'API Key: ';
|
|
318
|
+
const apiKeyInput = (await ask(rl, keyPrompt)).trim();
|
|
319
|
+
const apiKey = apiKeyInput || existingKey;
|
|
320
|
+
if (!apiKey) {
|
|
321
|
+
console.error('\n\x1b[31mAPI key is required.\x1b[0m');
|
|
322
|
+
console.error('Generate one at: Dialout → MACHINES tab → GENERATE KEY');
|
|
323
|
+
rl.close();
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
// Step 3: Validate key
|
|
327
|
+
console.log('\n\x1b[90mValidating API key...\x1b[0m');
|
|
328
|
+
const result = await validateKeyViaWS(serverUrl, apiKey);
|
|
329
|
+
if (!result.valid) {
|
|
330
|
+
console.error(`\x1b[31mValidation failed: ${result.error}\x1b[0m`);
|
|
331
|
+
// A 401 here means the key is not registered on THIS server — not that
|
|
332
|
+
// the agent is misconfigured. It is almost always a key issued by one
|
|
333
|
+
// server being offered to another, so name the server that refused it
|
|
334
|
+
// rather than leaving the reader to guess from a bare status code.
|
|
335
|
+
if (/\b401\b/.test(result.error || '')) {
|
|
336
|
+
console.error('');
|
|
337
|
+
console.error(` The server at \x1b[36m${serverUrl}\x1b[0m does not recognise that key.`);
|
|
338
|
+
console.error(' Keys are issued per server, so a key from one Dialout server is');
|
|
339
|
+
console.error(' rejected by another. Check you are pointing at the right one, and');
|
|
340
|
+
console.error(' generate a key there under Settings → Machines.');
|
|
341
|
+
}
|
|
342
|
+
console.error('');
|
|
343
|
+
const retry = await ask(rl, 'Save config anyway? (y/N): ');
|
|
344
|
+
if (retry.toLowerCase() !== 'y') {
|
|
345
|
+
rl.close();
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
console.log('\x1b[32mAPI key validated successfully!\x1b[0m');
|
|
351
|
+
}
|
|
352
|
+
const saved = (0, config_1.saveProfile)(profileName, { serverUrl, apiKey });
|
|
353
|
+
Object.assign(config, saved);
|
|
354
|
+
console.log(`\nSaved profile \x1b[36m${profileName}\x1b[0m (now active) → ${serverUrl}`);
|
|
355
|
+
console.log(`Config file: ${(0, config_1.getConfigPath)()}`);
|
|
356
|
+
const otherProfiles = Object.keys(saved.profiles || {}).filter((n) => n !== profileName);
|
|
357
|
+
if (otherProfiles.length > 0) {
|
|
358
|
+
console.log(`Switch anytime: dialout use ${otherProfiles[0]}`);
|
|
359
|
+
}
|
|
360
|
+
// Step 4: Ask how to start
|
|
361
|
+
console.log('');
|
|
362
|
+
console.log('\x1b[1mHow would you like to run the agent?\x1b[0m');
|
|
363
|
+
console.log('');
|
|
364
|
+
console.log(' \x1b[36m1)\x1b[0m Foreground — run now in this terminal (for testing)');
|
|
365
|
+
console.log(' \x1b[36m2)\x1b[0m Service — start automatically (launchd/systemd; asks boot vs login)');
|
|
366
|
+
console.log(' \x1b[36m3)\x1b[0m Cron Watchdog — auto-restart if it dies (cron job)');
|
|
367
|
+
console.log(' \x1b[36m4)\x1b[0m Skip — configure later');
|
|
368
|
+
console.log('');
|
|
369
|
+
const choice = await ask(rl, 'Choose [1-4]: ');
|
|
370
|
+
// Closed here because every branch below opens its own reader when it
|
|
371
|
+
// needs one; leaving this one open would keep the process alive.
|
|
372
|
+
rl.close();
|
|
373
|
+
switch (choice.trim()) {
|
|
374
|
+
case '1':
|
|
375
|
+
console.log('\n\x1b[90mStarting in foreground (Ctrl+C to stop)...\x1b[0m\n');
|
|
376
|
+
(0, websocket_1.connect)(config, () => {
|
|
377
|
+
console.log('[dialout] Connected and ready.');
|
|
378
|
+
});
|
|
379
|
+
process.on('SIGINT', () => { (0, websocket_1.disconnect)(); process.exit(0); });
|
|
380
|
+
process.on('SIGTERM', () => { (0, websocket_1.disconnect)(); process.exit(0); });
|
|
381
|
+
break;
|
|
382
|
+
case '2': {
|
|
383
|
+
// Same decision, and the same follow-up, as `install-service`.
|
|
384
|
+
const system = await chooseServiceScope();
|
|
385
|
+
(0, service_installer_1.installService)({ system });
|
|
386
|
+
applySshLocalNetworkWorkaround();
|
|
387
|
+
// Check if cron should also be set up
|
|
388
|
+
if (!(0, service_installer_1.isCronInstalled)()) {
|
|
389
|
+
console.log('\n\x1b[33mTip:\x1b[0m Add a cron watchdog for extra reliability:');
|
|
390
|
+
console.log(' dialout setup-cron');
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case '3': {
|
|
395
|
+
const intervalStr = await (async () => {
|
|
396
|
+
const rl2 = createRL();
|
|
397
|
+
const v = await ask(rl2, `Check interval in minutes [\x1b[36m5\x1b[0m]: `);
|
|
398
|
+
rl2.close();
|
|
399
|
+
return v;
|
|
400
|
+
})();
|
|
401
|
+
const interval = parseInt(intervalStr, 10) || config.cronInterval || 5;
|
|
402
|
+
config.cronInterval = interval;
|
|
403
|
+
(0, config_1.saveConfig)(config);
|
|
404
|
+
// Start daemon first
|
|
405
|
+
const { fork } = require('child_process');
|
|
406
|
+
const child = fork(path.resolve(__dirname, 'index.js'), [], {
|
|
407
|
+
detached: true,
|
|
408
|
+
stdio: 'ignore',
|
|
409
|
+
});
|
|
410
|
+
child.unref();
|
|
411
|
+
const pidFile = (0, config_1.getPidFile)();
|
|
412
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
413
|
+
console.log(`\nAgent started in background (PID: ${child.pid})`);
|
|
414
|
+
// Install cron
|
|
415
|
+
(0, service_installer_1.installCron)(interval);
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case '4':
|
|
419
|
+
default:
|
|
420
|
+
console.log('\nRun "dialout start" when ready.');
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
/**
|
|
425
|
+
* Decide whether a service install is a boot service or a per-user one.
|
|
426
|
+
*
|
|
427
|
+
* Shared by `init` and `install-service` on purpose. They used to disagree:
|
|
428
|
+
* `init` offered "Service — auto-start on boot" and then called
|
|
429
|
+
* installService() with no arguments, which installs the per-user LaunchAgent
|
|
430
|
+
* that only starts at LOGIN. Someone following the first-run prompt got a
|
|
431
|
+
* weaker service than the one they were promised, and only found out by
|
|
432
|
+
* rebooting and noticing the agent was not there.
|
|
433
|
+
*/
|
|
434
|
+
async function chooseServiceScope() {
|
|
435
|
+
if (process.platform === 'linux' && (0, service_installer_1.defaultLinuxScope)() === 'system') {
|
|
436
|
+
// Root on Linux: no password to pay and nothing to weigh.
|
|
437
|
+
console.log('Running as root — installing the boot service (starts at boot, survives logout).');
|
|
438
|
+
console.log('Use --login for a per-user service instead.\n');
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
if (!process.stdin.isTTY) {
|
|
442
|
+
// Non-interactive: the per-user service is the only one installable
|
|
443
|
+
// without a password.
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
const rl = createRL();
|
|
447
|
+
const answer = await ask(rl, 'Start the agent at boot, before you log in? Requires admin password. [y/N]: ');
|
|
448
|
+
rl.close();
|
|
449
|
+
const system = /^y(es)?$/i.test(answer.trim());
|
|
450
|
+
if (!system && process.platform === 'linux') {
|
|
451
|
+
console.log('Installing the per-user service — lingering will be enabled so it survives logout.');
|
|
452
|
+
}
|
|
453
|
+
return system;
|
|
454
|
+
}
|
|
455
|
+
// --- profiles ---
|
|
456
|
+
program
|
|
457
|
+
.command('profiles')
|
|
458
|
+
.description('List saved connection profiles (local/remote/…)')
|
|
459
|
+
.action(() => {
|
|
460
|
+
const config = (0, config_1.loadConfig)();
|
|
461
|
+
const names = Object.keys(config.profiles || {});
|
|
462
|
+
if (names.length === 0) {
|
|
463
|
+
console.log('No profiles saved yet. Run: dialout init');
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
console.log('');
|
|
467
|
+
console.log('Connection Profiles');
|
|
468
|
+
console.log('\x1b[90m' + '─'.repeat(44) + '\x1b[0m');
|
|
469
|
+
for (const name of names) {
|
|
470
|
+
const p = config.profiles[name];
|
|
471
|
+
const active = name === config.activeProfile;
|
|
472
|
+
const marker = active ? '\x1b[32m●\x1b[0m' : ' ';
|
|
473
|
+
console.log(` ${marker} ${name.padEnd(10)} ${p.serverUrl} \x1b[90m(key ****${p.apiKey.slice(-4)})\x1b[0m${active ? ' \x1b[32mactive\x1b[0m' : ''}`);
|
|
474
|
+
}
|
|
475
|
+
console.log('\x1b[90m' + '─'.repeat(44) + '\x1b[0m');
|
|
476
|
+
console.log('Switch with: dialout use <name>');
|
|
477
|
+
console.log('');
|
|
478
|
+
});
|
|
479
|
+
// --- use ---
|
|
480
|
+
program
|
|
481
|
+
.command('use <profile>')
|
|
482
|
+
.description('Switch the active connection profile (e.g. local, remote)')
|
|
483
|
+
.addHelpText('after', `
|
|
484
|
+
Examples:
|
|
485
|
+
$ dialout use local # point the agent at your local Dialout
|
|
486
|
+
$ dialout use remote # point it back at the live server
|
|
487
|
+
$ dialout restart # apply to a running background agent`)
|
|
488
|
+
.action((name) => {
|
|
489
|
+
const config = (0, config_1.loadConfig)();
|
|
490
|
+
const profile = config.profiles?.[name];
|
|
491
|
+
if (!profile) {
|
|
492
|
+
const names = Object.keys(config.profiles || {});
|
|
493
|
+
console.error(`Unknown profile: ${name}`);
|
|
494
|
+
console.error(names.length ? `Available: ${names.join(', ')}` : 'No profiles yet — run: dialout init');
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
(0, config_1.saveProfile)(name, profile, true);
|
|
498
|
+
console.log(`Active profile: \x1b[36m${name}\x1b[0m → ${profile.serverUrl}`);
|
|
499
|
+
console.log('If the agent is running, apply with: dialout restart');
|
|
500
|
+
});
|
|
501
|
+
// --- start ---
|
|
502
|
+
program
|
|
503
|
+
.command('start')
|
|
504
|
+
.description('Start the agent and connect to the Dialout server')
|
|
505
|
+
.option('--daemon', 'Run in background (fork process, survives terminal close)')
|
|
506
|
+
.option('--profile <name>', 'Use a saved profile for this run only (active profile unchanged)')
|
|
507
|
+
.addHelpText('after', `
|
|
508
|
+
Modes:
|
|
509
|
+
dialout start Foreground — logs to terminal, Ctrl+C to stop
|
|
510
|
+
dialout start --daemon Background — forks process, use "stop" to end
|
|
511
|
+
dialout install-service Permanent — auto-starts on boot (launchd/systemd)
|
|
512
|
+
dialout setup-cron Watchdog — cron checks every N minutes, restarts if dead
|
|
513
|
+
|
|
514
|
+
Examples:
|
|
515
|
+
$ dialout start # test connection in foreground
|
|
516
|
+
$ dialout start --profile local # one-off run against local Dialout
|
|
517
|
+
$ dialout start --daemon # run in background for this session
|
|
518
|
+
$ dialout stop # stop background agent`)
|
|
519
|
+
.action(async (opts) => {
|
|
520
|
+
await (0, update_check_1.checkForUpdate)();
|
|
521
|
+
let config = (0, config_1.loadConfig)();
|
|
522
|
+
if (opts.profile) {
|
|
523
|
+
if (!config.profiles?.[opts.profile]) {
|
|
524
|
+
console.error(`Unknown profile: ${opts.profile}`);
|
|
525
|
+
console.error(`Available: ${Object.keys(config.profiles || {}).join(', ') || '(none — run: dialout init)'}`);
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
config = (0, config_1.applyProfile)(config, opts.profile);
|
|
529
|
+
console.log(`[dialout] Using profile "${opts.profile}" → ${config.serverUrl}`);
|
|
530
|
+
}
|
|
531
|
+
if (!config.serverUrl || !config.apiKey) {
|
|
532
|
+
console.error('Not configured. Run: dialout init');
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
if (opts.daemon) {
|
|
536
|
+
const { fork } = require('child_process');
|
|
537
|
+
const child = fork(path.resolve(__dirname, 'index.js'), [], {
|
|
538
|
+
detached: true,
|
|
539
|
+
stdio: 'ignore',
|
|
540
|
+
env: opts.profile
|
|
541
|
+
? { ...process.env, DEVDASH_AGENT_PROFILE: opts.profile }
|
|
542
|
+
: process.env,
|
|
543
|
+
});
|
|
544
|
+
child.unref();
|
|
545
|
+
const pidFile = (0, config_1.getPidFile)();
|
|
546
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
547
|
+
// Verify before claiming. The child exits within milliseconds when
|
|
548
|
+
// another supervisor (a systemd/launchd service, another daemon) already
|
|
549
|
+
// holds the single-instance lock for this server URL — and its stdio is
|
|
550
|
+
// 'ignore', so the "Already running — exiting" line it prints goes
|
|
551
|
+
// nowhere. Reporting the fork's pid unconditionally announced success
|
|
552
|
+
// for a process that was already dead, which reads as "the daemon keeps
|
|
553
|
+
// dying" rather than "something else is already running it".
|
|
554
|
+
await new Promise((resolve) => setTimeout(resolve, 900));
|
|
555
|
+
const stillAlive = (() => {
|
|
556
|
+
try {
|
|
557
|
+
process.kill(child.pid, 0);
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
})();
|
|
564
|
+
if (!stillAlive) {
|
|
565
|
+
try {
|
|
566
|
+
fs.unlinkSync(pidFile);
|
|
567
|
+
}
|
|
568
|
+
catch { }
|
|
569
|
+
console.error('\x1b[33mThe agent exited immediately.\x1b[0m');
|
|
570
|
+
console.error('Usually this means another supervisor is already running it for this server URL.');
|
|
571
|
+
console.error('Check what is running: dialout status');
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
console.log(`Agent started in background (PID: ${child.pid})`);
|
|
575
|
+
console.log('Stop with: dialout stop');
|
|
576
|
+
// Suggest cron if not installed
|
|
577
|
+
if (!(0, service_installer_1.isCronInstalled)() && !(0, service_installer_1.isServiceInstalled)()) {
|
|
578
|
+
console.log('\n\x1b[33mTip:\x1b[0m Set up auto-restart with: dialout setup-cron');
|
|
579
|
+
}
|
|
580
|
+
process.exit(0);
|
|
581
|
+
}
|
|
582
|
+
// Foreground mode
|
|
583
|
+
console.log('[dialout] Starting in foreground (Ctrl+C to stop)...');
|
|
584
|
+
(0, websocket_1.connect)(config, () => {
|
|
585
|
+
console.log('[dialout] Connected and ready.');
|
|
586
|
+
});
|
|
587
|
+
process.on('SIGINT', () => {
|
|
588
|
+
console.log('\n[dialout] Stopping...');
|
|
589
|
+
(0, websocket_1.disconnect)();
|
|
590
|
+
process.exit(0);
|
|
591
|
+
});
|
|
592
|
+
process.on('SIGTERM', () => {
|
|
593
|
+
(0, websocket_1.disconnect)();
|
|
594
|
+
process.exit(0);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
// --- stop ---
|
|
598
|
+
program
|
|
599
|
+
.command('stop')
|
|
600
|
+
.description('Stop the background agent (started with --daemon)')
|
|
601
|
+
.addHelpText('after', `
|
|
602
|
+
Stops the agent that was started with "dialout start --daemon".
|
|
603
|
+
Has no effect on service-installed agents — use uninstall-service for those.`)
|
|
604
|
+
.action(() => {
|
|
605
|
+
const pidFile = (0, config_1.getPidFile)();
|
|
606
|
+
if (!fs.existsSync(pidFile)) {
|
|
607
|
+
console.log('No running agent found.');
|
|
608
|
+
console.log('If running as service: dialout uninstall-service');
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
612
|
+
try {
|
|
613
|
+
process.kill(pid, 'SIGTERM');
|
|
614
|
+
console.log(`Agent stopped (PID: ${pid})`);
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
if (err.code === 'ESRCH') {
|
|
618
|
+
console.log('Agent is not running (stale PID file removed).');
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
console.error('Failed to stop agent:', err.message);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
fs.unlinkSync(pidFile);
|
|
625
|
+
});
|
|
626
|
+
// --- restart ---
|
|
627
|
+
program
|
|
628
|
+
.command('restart')
|
|
629
|
+
.description('Restart the background agent (stop + start --daemon)')
|
|
630
|
+
.addHelpText('after', `
|
|
631
|
+
Convenience command that stops any running daemon and starts a fresh one.
|
|
632
|
+
Equivalent to running "dialout stop && dialout start --daemon".
|
|
633
|
+
|
|
634
|
+
Examples:
|
|
635
|
+
$ dialout restart # restart background agent
|
|
636
|
+
$ dialout status # verify it's running`)
|
|
637
|
+
.action(async () => {
|
|
638
|
+
await (0, update_check_1.checkForUpdate)();
|
|
639
|
+
const config = (0, config_1.loadConfig)();
|
|
640
|
+
if (!config.serverUrl || !config.apiKey) {
|
|
641
|
+
console.error('Not configured. Run: dialout init');
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
// Stop existing daemon if running
|
|
645
|
+
const pidFile = (0, config_1.getPidFile)();
|
|
646
|
+
if (fs.existsSync(pidFile)) {
|
|
647
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
648
|
+
try {
|
|
649
|
+
process.kill(pid, 'SIGTERM');
|
|
650
|
+
console.log(`Stopped previous agent (PID: ${pid})`);
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
if (err.code !== 'ESRCH') {
|
|
654
|
+
console.error('Failed to stop agent:', err.message);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
fs.unlinkSync(pidFile);
|
|
658
|
+
}
|
|
659
|
+
// Start new daemon
|
|
660
|
+
const { fork } = require('child_process');
|
|
661
|
+
const child = fork(path.resolve(__dirname, 'index.js'), [], {
|
|
662
|
+
detached: true,
|
|
663
|
+
stdio: 'ignore',
|
|
664
|
+
});
|
|
665
|
+
child.unref();
|
|
666
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
667
|
+
console.log(`Agent restarted in background (PID: ${child.pid})`);
|
|
668
|
+
process.exit(0);
|
|
669
|
+
});
|
|
670
|
+
// --- status ---
|
|
671
|
+
program
|
|
672
|
+
.command('status')
|
|
673
|
+
.description('Show agent connection status, config, and service state')
|
|
674
|
+
.addHelpText('after', `
|
|
675
|
+
Shows: server URL, API key (masked), config path, service state, cron state, process state.`)
|
|
676
|
+
.action(() => {
|
|
677
|
+
const config = (0, config_1.loadConfig)();
|
|
678
|
+
const pidFile = (0, config_1.getPidFile)();
|
|
679
|
+
const svc = (0, service_installer_1.getServiceStatus)();
|
|
680
|
+
const cronInstalled = (0, service_installer_1.isCronInstalled)();
|
|
681
|
+
const kindLabel = {
|
|
682
|
+
'launchd-agent': 'launchd, at login',
|
|
683
|
+
'launchd-daemon': 'launchd, at boot',
|
|
684
|
+
'systemd-user': 'systemd, at login',
|
|
685
|
+
'systemd-system': 'systemd, at boot',
|
|
686
|
+
};
|
|
687
|
+
console.log('');
|
|
688
|
+
console.log('Dialout Agent Status');
|
|
689
|
+
console.log('\x1b[90m' + '─'.repeat(44) + '\x1b[0m');
|
|
690
|
+
const profileNames = Object.keys(config.profiles || {});
|
|
691
|
+
console.log(` Profile: ${config.activeProfile ? `\x1b[36m${config.activeProfile}\x1b[0m` : '(none)'}${profileNames.length > 1 ? ` \x1b[90m(saved: ${profileNames.join(', ')})\x1b[0m` : ''}`);
|
|
692
|
+
console.log(` Server: ${config.serverUrl || '(not configured)'}`);
|
|
693
|
+
console.log(` API Key: ${config.apiKey ? '****' + config.apiKey.slice(-4) : '(not set)'}`);
|
|
694
|
+
console.log(` Config: ${(0, config_1.getConfigPath)()}`);
|
|
695
|
+
console.log(` Service: ${svc.installed ? `\x1b[32minstalled\x1b[0m${svc.kind ? ` (${kindLabel[svc.kind]})` : ''}` : 'not installed'}`);
|
|
696
|
+
console.log(` Cron: ${cronInstalled ? `\x1b[32minstalled\x1b[0m (every ${config.cronInterval || 5} min)` : 'not installed'}`);
|
|
697
|
+
// Every supervisor that could be launching this agent, not just the
|
|
698
|
+
// first match (getServiceStatus()/isServiceInstalled() intentionally
|
|
699
|
+
// stop at the first one). Detection only — this never stops a unit,
|
|
700
|
+
// kills a process, or edits the crontab.
|
|
701
|
+
const supervisors = (0, service_installer_1.listSupervisors)();
|
|
702
|
+
if (supervisors.length > 1) {
|
|
703
|
+
console.log('');
|
|
704
|
+
console.log('\x1b[33m ⚠ Multiple supervisors are managing this agent:\x1b[0m');
|
|
705
|
+
for (const s of supervisors) {
|
|
706
|
+
// "running" means a live process was found for launchd/systemd
|
|
707
|
+
// (svc.pid !== null); cron has no live process of its own to
|
|
708
|
+
// probe — it's a scheduled trigger, not a fourth daemon — so it
|
|
709
|
+
// gets its own word instead of borrowing "running"/"not running"
|
|
710
|
+
// and implying it's an active process racing the others.
|
|
711
|
+
const state = s.kind === 'cron'
|
|
712
|
+
? `scheduled (fires every ${config.cronInterval || 5} min)`
|
|
713
|
+
: (s.pid !== null ? `running (PID ${s.pid})` : (s.running ? 'running' : 'not running'));
|
|
714
|
+
console.log(`\x1b[33m - ${supervisorLabel[s.kind]}: ${s.path} — ${state}${s.stale ? ' [STALE]' : ''}\x1b[0m`);
|
|
715
|
+
}
|
|
716
|
+
console.log('\x1b[33m More than one supervisor causes the connect/disconnect (1006) flapping you may be seeing —\x1b[0m');
|
|
717
|
+
console.log('\x1b[33m the single-instance lock will keep only one process alive and make all the others exit.\x1b[0m');
|
|
718
|
+
}
|
|
719
|
+
// A systemd user unit without lingering is a time bomb that `status`
|
|
720
|
+
// used to report as a healthy installed service: systemd-logind stops
|
|
721
|
+
// user@<uid>.service when the last session ends, so the agent dies on
|
|
722
|
+
// every logout and silently returns on the next login. Nothing else in
|
|
723
|
+
// this output would ever hint at it.
|
|
724
|
+
if (process.platform === 'linux' && supervisors.some((s) => s.kind === 'systemd-user')) {
|
|
725
|
+
const user = os.userInfo().username;
|
|
726
|
+
if (!(0, service_installer_1.isLingerEnabled)(user)) {
|
|
727
|
+
console.log('');
|
|
728
|
+
console.log('\x1b[33m ⚠ Installed as a systemd USER unit with lingering OFF.\x1b[0m');
|
|
729
|
+
console.log('\x1b[33m systemd stops it when your last session ends — the agent dies every time you log out\x1b[0m');
|
|
730
|
+
console.log('\x1b[33m and only comes back when you log in again.\x1b[0m');
|
|
731
|
+
console.log(`\x1b[33m Fix: sudo loginctl enable-linger ${user}\x1b[0m`);
|
|
732
|
+
console.log('\x1b[33m Or: dialout install-service --system (runs at boot, in system.slice)\x1b[0m');
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
for (const s of supervisors.filter((s) => s.stale)) {
|
|
736
|
+
console.log('');
|
|
737
|
+
for (const line of (0, service_installer_1.staleSupervisorAdvice)(s)) {
|
|
738
|
+
console.log(`\x1b[33m${line}\x1b[0m`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (svc.installed) {
|
|
742
|
+
// The service manager (launchd/systemd) owns the process — ask it, not the pidfile.
|
|
743
|
+
if (svc.running) {
|
|
744
|
+
console.log(` Process: \x1b[32mrunning\x1b[0m (PID: ${svc.pid}, managed by ${svc.kind?.startsWith('launchd') ? 'launchd' : 'systemd'})`);
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
console.log(` Process: \x1b[31mnot running\x1b[0m (service installed but stopped — check logs in ~/.dialout/logs)`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
else if (fs.existsSync(pidFile)) {
|
|
751
|
+
// Manual background mode (dialout start --daemon)
|
|
752
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
753
|
+
try {
|
|
754
|
+
process.kill(pid, 0);
|
|
755
|
+
console.log(` Process: \x1b[32mrunning\x1b[0m (PID: ${pid}, background)`);
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
console.log(` Process: not running (stale PID)`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
console.log(` Process: not running`);
|
|
763
|
+
}
|
|
764
|
+
console.log('\x1b[90m' + '─'.repeat(44) + '\x1b[0m');
|
|
765
|
+
console.log('');
|
|
766
|
+
});
|
|
767
|
+
// On macOS the tmux server is the process Local Network Privacy judges, so
|
|
768
|
+
// every tmux session on the machine — including ones the user opens from their
|
|
769
|
+
// own terminal app — is denied LAN access and cannot ssh to a local git remote
|
|
770
|
+
// (the measured long form is in ssh-local-network.ts). Installing the service
|
|
771
|
+
// is the moment the machine starts hosting tmux sessions, so it is also the
|
|
772
|
+
// moment to install the ssh_config default that works around it.
|
|
773
|
+
function applySshLocalNetworkWorkaround() {
|
|
774
|
+
if (!(0, ssh_local_network_1.sshLocalNetworkApplies)())
|
|
775
|
+
return;
|
|
776
|
+
const target = (0, ssh_local_network_1.defaultSshConfigPath)();
|
|
777
|
+
try {
|
|
778
|
+
const result = (0, ssh_local_network_1.installSshLocalNetworkBlock)(target);
|
|
779
|
+
// 'skipped-existing' means their own ConnectTimeout already wins;
|
|
780
|
+
// 'updated' means the block was already there. Neither is news.
|
|
781
|
+
if (result === 'skipped-existing' || result === 'updated')
|
|
782
|
+
return;
|
|
783
|
+
console.log(` ssh: added a ConnectTimeout default to ${target}`);
|
|
784
|
+
console.log(' (macOS denies local-network access to the tmux server, so without this');
|
|
785
|
+
console.log(' git/ssh to a LAN host fails inside every tmux session on this machine.');
|
|
786
|
+
console.log(' To fix it properly, allow tmux under System Settings >');
|
|
787
|
+
console.log(' Privacy & Security > Local Network.)');
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
// Never fail an install over this — the service itself is still fine.
|
|
791
|
+
console.log(` ssh: could not update ${target} (${err.message})`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function removeSshLocalNetworkWorkaround() {
|
|
795
|
+
const target = (0, ssh_local_network_1.defaultSshConfigPath)();
|
|
796
|
+
try {
|
|
797
|
+
if (!fs.existsSync(target))
|
|
798
|
+
return;
|
|
799
|
+
const before = fs.readFileSync(target, 'utf-8');
|
|
800
|
+
const after = (0, ssh_local_network_1.removeSshLocalNetworkBlock)(before);
|
|
801
|
+
if (after === before)
|
|
802
|
+
return;
|
|
803
|
+
fs.writeFileSync(target, after);
|
|
804
|
+
console.log(` ssh: removed the ConnectTimeout block from ${target}`);
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
// Best effort — a leftover block is harmless.
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
// --- install-service ---
|
|
811
|
+
program
|
|
812
|
+
.command('install-service')
|
|
813
|
+
.description('Install as OS service — auto-starts on login (or boot with --system)')
|
|
814
|
+
.option('--system', 'Run at boot, before login (LaunchDaemon / systemd system unit; needs sudo)')
|
|
815
|
+
.option('--login', 'Per-user service that starts at login (skip the interactive prompt)')
|
|
816
|
+
.addHelpText('after', `
|
|
817
|
+
Installs the agent as a system service so it starts automatically.
|
|
818
|
+
|
|
819
|
+
Default (per-user, starts at LOGIN):
|
|
820
|
+
macOS — LaunchAgent at ~/Library/LaunchAgents/com.dialout.agent.plist
|
|
821
|
+
Linux — systemd user service at ~/.config/systemd/user/dialout.service
|
|
822
|
+
|
|
823
|
+
--system (starts at BOOT, before anyone logs in — requires sudo):
|
|
824
|
+
macOS — LaunchDaemon at /Library/LaunchDaemons/com.dialout.agent.plist
|
|
825
|
+
Linux — systemd system unit at /etc/systemd/system/dialout.service
|
|
826
|
+
|
|
827
|
+
Must run "dialout init" first to configure server URL and API key.
|
|
828
|
+
|
|
829
|
+
With no flag on an interactive terminal, it asks whether to start at boot
|
|
830
|
+
(sudo) or at login.
|
|
831
|
+
|
|
832
|
+
Examples:
|
|
833
|
+
$ dialout install-service # ask: boot or login
|
|
834
|
+
$ dialout install-service --system # start at boot (prompts for sudo)
|
|
835
|
+
$ dialout install-service --login # start at login, no prompt
|
|
836
|
+
|
|
837
|
+
To remove: dialout uninstall-service`)
|
|
838
|
+
.action(async (opts) => {
|
|
839
|
+
const config = (0, config_1.loadConfig)();
|
|
840
|
+
if (!config.serverUrl || !config.apiKey) {
|
|
841
|
+
console.error('Not configured. Run "dialout init" first.');
|
|
842
|
+
process.exit(1);
|
|
843
|
+
}
|
|
844
|
+
// Explicit flags win; otherwise the same decision `init` makes, from the
|
|
845
|
+
// same helper, so the two prompts cannot drift apart again.
|
|
846
|
+
const system = opts.system ? true : opts.login ? false : await chooseServiceScope();
|
|
847
|
+
(0, service_installer_1.installService)({ system });
|
|
848
|
+
applySshLocalNetworkWorkaround();
|
|
849
|
+
process.exit(0);
|
|
850
|
+
});
|
|
851
|
+
// --- uninstall-service ---
|
|
852
|
+
program
|
|
853
|
+
.command('uninstall-service')
|
|
854
|
+
.description('Remove OS service — stops auto-start on boot')
|
|
855
|
+
.addHelpText('after', `
|
|
856
|
+
Removes the service installed by "install-service".
|
|
857
|
+
The agent will no longer start automatically on login/boot.
|
|
858
|
+
|
|
859
|
+
A boot service (install-service --system) is owned by root, so this may
|
|
860
|
+
prompt for your password — the same way installing it did.
|
|
861
|
+
|
|
862
|
+
This removes the SERVICE only. If a cron watchdog or a manual daemon is also
|
|
863
|
+
running the agent, it keeps running; the command says so and names the fix.`)
|
|
864
|
+
.action(() => {
|
|
865
|
+
const result = (0, service_installer_1.uninstallService)();
|
|
866
|
+
// Exiting 0 after removing nothing is what made this look like it worked
|
|
867
|
+
// while the daemon kept running. Fail loudly instead.
|
|
868
|
+
if (result.pending.length > 0) {
|
|
869
|
+
console.error('\n\x1b[31mService NOT removed.\x1b[0m Run the commands above, then: dialout status');
|
|
870
|
+
process.exitCode = 1;
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
removeSshLocalNetworkWorkaround();
|
|
874
|
+
if (!result.removed)
|
|
875
|
+
return;
|
|
876
|
+
// The service is gone, but it is not the only thing that can start the
|
|
877
|
+
// agent. Point at the leftovers rather than letting `status` surprise them.
|
|
878
|
+
const leftovers = (0, service_installer_1.listSupervisors)().filter((s) => s.kind === 'cron');
|
|
879
|
+
if (leftovers.length > 0) {
|
|
880
|
+
console.log('\n\x1b[33mA cron watchdog is still installed and will restart the agent.\x1b[0m');
|
|
881
|
+
console.log(' Remove it with: dialout remove-cron');
|
|
882
|
+
}
|
|
883
|
+
if (fs.existsSync((0, config_1.getPidFile)())) {
|
|
884
|
+
const pid = parseInt(fs.readFileSync((0, config_1.getPidFile)(), 'utf-8').trim(), 10);
|
|
885
|
+
let alive = false;
|
|
886
|
+
try {
|
|
887
|
+
process.kill(pid, 0);
|
|
888
|
+
alive = true;
|
|
889
|
+
}
|
|
890
|
+
catch { /* stale */ }
|
|
891
|
+
if (alive) {
|
|
892
|
+
console.log(`\n\x1b[33mA manually started agent is still running (PID ${pid}).\x1b[0m`);
|
|
893
|
+
console.log(' Stop it with: dialout stop');
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
// --- setup-cron ---
|
|
898
|
+
program
|
|
899
|
+
.command('setup-cron')
|
|
900
|
+
.description('Install cron watchdog — auto-restarts agent if it dies')
|
|
901
|
+
.option('-i, --interval <minutes>', 'Check interval in minutes (default: 5)', '5')
|
|
902
|
+
.addHelpText('after', `
|
|
903
|
+
Creates a cron job that checks if the agent is running every N minutes.
|
|
904
|
+
If the agent is dead, the watchdog restarts it automatically.
|
|
905
|
+
|
|
906
|
+
The default interval is 5 minutes. You can customize it:
|
|
907
|
+
$ dialout setup-cron # every 5 minutes (default)
|
|
908
|
+
$ dialout setup-cron -i 3 # every 3 minutes
|
|
909
|
+
$ dialout setup-cron --interval 10 # every 10 minutes
|
|
910
|
+
|
|
911
|
+
To remove: dialout remove-cron`)
|
|
912
|
+
.action((opts) => {
|
|
913
|
+
const config = (0, config_1.loadConfig)();
|
|
914
|
+
if (!config.serverUrl || !config.apiKey) {
|
|
915
|
+
console.error('Not configured. Run "dialout init" first.');
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
// A launchd/systemd service already supervises the agent and auto-restarts
|
|
919
|
+
// it on death. Adding a cron watchdog on top means TWO independent
|
|
920
|
+
// supervisors — the exact configuration that used to spawn duplicate
|
|
921
|
+
// daemons (the service's process isn't tracked in daemon.pid, so the
|
|
922
|
+
// watchdog thinks nothing is running and forks a second one). Refuse.
|
|
923
|
+
const svc = (0, service_installer_1.getServiceStatus)();
|
|
924
|
+
if (svc.installed) {
|
|
925
|
+
console.log('An OS service is already installed — it auto-restarts the agent on its own.');
|
|
926
|
+
console.log('A cron watchdog would be a second, conflicting supervisor, so this is a no-op.');
|
|
927
|
+
console.log('To use cron instead, run: dialout uninstall-service first.');
|
|
928
|
+
process.exit(0);
|
|
929
|
+
}
|
|
930
|
+
const interval = parseInt(opts.interval, 10) || 5;
|
|
931
|
+
config.cronInterval = interval;
|
|
932
|
+
(0, config_1.saveConfig)(config);
|
|
933
|
+
// Start daemon if not running. (Even if a duplicate slips through here, the
|
|
934
|
+
// agent's per-URL single-instance lock makes the extra process exit at once.)
|
|
935
|
+
const pidFile = (0, config_1.getPidFile)();
|
|
936
|
+
let isRunning = false;
|
|
937
|
+
if (fs.existsSync(pidFile)) {
|
|
938
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
939
|
+
try {
|
|
940
|
+
process.kill(pid, 0);
|
|
941
|
+
isRunning = true;
|
|
942
|
+
}
|
|
943
|
+
catch { }
|
|
944
|
+
}
|
|
945
|
+
if (!isRunning) {
|
|
946
|
+
const { fork } = require('child_process');
|
|
947
|
+
const child = fork(path.resolve(__dirname, 'index.js'), [], {
|
|
948
|
+
detached: true,
|
|
949
|
+
stdio: 'ignore',
|
|
950
|
+
});
|
|
951
|
+
child.unref();
|
|
952
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
953
|
+
console.log(`Agent started in background (PID: ${child.pid})`);
|
|
954
|
+
}
|
|
955
|
+
(0, service_installer_1.installCron)(interval);
|
|
956
|
+
process.exit(0);
|
|
957
|
+
});
|
|
958
|
+
// --- remove-cron ---
|
|
959
|
+
program
|
|
960
|
+
.command('remove-cron')
|
|
961
|
+
.description('Remove cron watchdog — stops auto-restart')
|
|
962
|
+
.addHelpText('after', `
|
|
963
|
+
Removes the cron job installed by "setup-cron".
|
|
964
|
+
The agent will no longer auto-restart if it dies.
|
|
965
|
+
Note: this does NOT stop the currently running agent.`)
|
|
966
|
+
.action(() => {
|
|
967
|
+
(0, service_installer_1.uninstallCron)();
|
|
968
|
+
});
|
|
969
|
+
// --- repair ---
|
|
970
|
+
program
|
|
971
|
+
.command('repair')
|
|
972
|
+
.description('Rewrite a stale cron watchdog left by an old install/rename')
|
|
973
|
+
.addHelpText('after', `
|
|
974
|
+
Fixes exactly one thing: a cron watchdog (~/.dialout/watchdog.sh)
|
|
975
|
+
whose SCRIPT= line still points at an old, renamed, or uninstalled
|
|
976
|
+
package build — a machine can end up resurrecting a deprecated version
|
|
977
|
+
every few minutes, fighting the real daemon for the same registration.
|
|
978
|
+
|
|
979
|
+
Repair rewrites ONLY that path. It never deletes a unit file, kills a
|
|
980
|
+
process, or edits the crontab. When more than one supervisor (launchd /
|
|
981
|
+
systemd / cron) is managing this agent, repair explains what to remove
|
|
982
|
+
and stops there — it does not choose for you.
|
|
983
|
+
|
|
984
|
+
Examples:
|
|
985
|
+
$ dialout repair
|
|
986
|
+
$ dialout status # see current supervisor state`)
|
|
987
|
+
.action(() => {
|
|
988
|
+
let repairResult;
|
|
989
|
+
try {
|
|
990
|
+
repairResult = (0, service_installer_1.repairWatchdog)();
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
994
|
+
console.log('');
|
|
995
|
+
console.log('Dialout Agent Repair');
|
|
996
|
+
console.log('\x1b[90m' + '─'.repeat(44) + '\x1b[0m');
|
|
997
|
+
console.log(`\x1b[31m ✗ Could not repair the watchdog: ${message}\x1b[0m`);
|
|
998
|
+
console.log(' Nothing was changed. Check that ~/.dialout/watchdog.sh is a regular,');
|
|
999
|
+
console.log(' readable file you have permission to modify, then try again.');
|
|
1000
|
+
console.log('\x1b[90m' + '─'.repeat(44) + '\x1b[0m');
|
|
1001
|
+
console.log('');
|
|
1002
|
+
process.exitCode = 1;
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const { repaired, from, to } = repairResult;
|
|
1006
|
+
console.log('');
|
|
1007
|
+
console.log('Dialout Agent Repair');
|
|
1008
|
+
console.log('\x1b[90m' + '─'.repeat(44) + '\x1b[0m');
|
|
1009
|
+
if (repaired) {
|
|
1010
|
+
console.log('\x1b[32m ✓ Rewrote stale watchdog:\x1b[0m ~/.dialout/watchdog.sh');
|
|
1011
|
+
console.log(` from: ${from}`);
|
|
1012
|
+
console.log(` to: ${to}`);
|
|
1013
|
+
console.log(' (previous version backed up alongside it as watchdog.sh.bak-<timestamp>)');
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
console.log(' Watchdog check: \x1b[32mnothing to repair\x1b[0m (absent, or SCRIPT= already correct).');
|
|
1017
|
+
}
|
|
1018
|
+
// Detection only, same as `status` — repair never removes a supervisor
|
|
1019
|
+
// itself (Constraint 3). This just re-reports the full picture after
|
|
1020
|
+
// fixing the one thing it's safe to fix automatically.
|
|
1021
|
+
const supervisors = (0, service_installer_1.listSupervisors)();
|
|
1022
|
+
const staleRemaining = supervisors.filter((s) => s.stale);
|
|
1023
|
+
if (supervisors.length > 1) {
|
|
1024
|
+
console.log('');
|
|
1025
|
+
console.log('\x1b[33m ⚠ Multiple supervisors are managing this agent:\x1b[0m');
|
|
1026
|
+
for (const s of supervisors) {
|
|
1027
|
+
const state = s.kind === 'cron'
|
|
1028
|
+
? 'scheduled watchdog'
|
|
1029
|
+
: (s.pid !== null ? `running (PID ${s.pid})` : (s.running ? 'running' : 'not running'));
|
|
1030
|
+
console.log(`\x1b[33m - ${supervisorLabel[s.kind]}: ${s.path} — ${state}${s.stale ? ' [STALE]' : ''}\x1b[0m`);
|
|
1031
|
+
}
|
|
1032
|
+
console.log('\x1b[33m Repair only fixes the watchdog\x27s SCRIPT= path — it does not remove a supervisor you may still depend on.\x1b[0m');
|
|
1033
|
+
// A remaining stale unit needs its own fix command — "repair" cannot
|
|
1034
|
+
// touch it, and this listing alone would otherwise leave the operator
|
|
1035
|
+
// with a [STALE] tag and no next step for that specific supervisor.
|
|
1036
|
+
for (const s of staleRemaining) {
|
|
1037
|
+
console.log('');
|
|
1038
|
+
for (const line of (0, service_installer_1.staleSupervisorAdvice)(s)) {
|
|
1039
|
+
console.log(`\x1b[33m${line}\x1b[0m`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
console.log('');
|
|
1043
|
+
console.log(' Remove the extra one(s) yourself:');
|
|
1044
|
+
if (supervisors.some((s) => s.kind === 'cron')) {
|
|
1045
|
+
console.log(' dialout remove-cron # drop the cron watchdog');
|
|
1046
|
+
}
|
|
1047
|
+
if (supervisors.some((s) => s.kind !== 'cron')) {
|
|
1048
|
+
console.log(' dialout uninstall-service # drop the launchd/systemd service');
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
else if (staleRemaining.length > 0) {
|
|
1052
|
+
// Exactly one supervisor total, and it's still stale (e.g. a lone
|
|
1053
|
+
// stale systemd/launchd unit — repair only ever touches the watchdog).
|
|
1054
|
+
// This is NOT a clean state: give the same actionable advice `status`
|
|
1055
|
+
// would, instead of a false "no conflicting supervisors" all-clear.
|
|
1056
|
+
console.log('');
|
|
1057
|
+
for (const s of staleRemaining) {
|
|
1058
|
+
for (const line of (0, service_installer_1.staleSupervisorAdvice)(s)) {
|
|
1059
|
+
console.log(`\x1b[33m${line}\x1b[0m`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
console.log('');
|
|
1065
|
+
console.log(' No conflicting supervisors detected.');
|
|
1066
|
+
}
|
|
1067
|
+
console.log('\x1b[90m' + '─'.repeat(44) + '\x1b[0m');
|
|
1068
|
+
console.log('');
|
|
1069
|
+
});
|
|
1070
|
+
// --- setup-cowork ---
|
|
1071
|
+
program
|
|
1072
|
+
.command('setup-cowork')
|
|
1073
|
+
.description('Choose which terminal app(s) auto-wrap into tmux for Dialout remote access')
|
|
1074
|
+
.option('--remove', 'Uninstall the wrapper, clear the allowlist, and go fully native')
|
|
1075
|
+
.option('--terminals <csv>', 'Non-interactive: comma-separated terminal tokens to allow')
|
|
1076
|
+
.option('--yes', 'Auto-confirm installing tmux if it is missing')
|
|
1077
|
+
.option('--adopt-all', 'Adopt all existing tmux sessions non-interactively')
|
|
1078
|
+
.option('--no-adopt', 'Skip adopting existing sessions')
|
|
1079
|
+
.action(async (opts) => {
|
|
1080
|
+
const { tmuxAvailable, listSessions } = require('./tmux-manager');
|
|
1081
|
+
const { execFileSync } = require('child_process');
|
|
1082
|
+
const { hasCommand } = require('./has-command');
|
|
1083
|
+
const config = (0, config_1.loadConfig)();
|
|
1084
|
+
const rcFiles = [path.join(os.homedir(), '.zshrc'), path.join(os.homedir(), '.bashrc')];
|
|
1085
|
+
// --remove: strip the block, clear the allowlist, go native.
|
|
1086
|
+
if (opts.remove) {
|
|
1087
|
+
for (const rc of rcFiles) {
|
|
1088
|
+
if (!fs.existsSync(rc))
|
|
1089
|
+
continue;
|
|
1090
|
+
fs.writeFileSync(rc, (0, cowork_1.removeCoworkBlock)(fs.readFileSync(rc, 'utf-8')));
|
|
1091
|
+
console.log(` removed wrapper from ${rc}`);
|
|
1092
|
+
}
|
|
1093
|
+
removeSshLocalNetworkWorkaround();
|
|
1094
|
+
config.cowork = false;
|
|
1095
|
+
config.coworkTerminals = [];
|
|
1096
|
+
(0, config_1.saveConfig)(config);
|
|
1097
|
+
console.log('Reverted to native terminals. Open terminals are unaffected; new shells start normally.');
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
// tmux check → offer to install.
|
|
1101
|
+
if (!(await tmuxAvailable())) {
|
|
1102
|
+
const plan = (0, cowork_1.pickTmuxInstall)(process.platform, hasCommand);
|
|
1103
|
+
if (!plan.canAuto) {
|
|
1104
|
+
console.log('\x1b[33mtmux is not installed.\x1b[0m');
|
|
1105
|
+
console.log(` ${plan.manual}`);
|
|
1106
|
+
process.exitCode = 1;
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
console.log('\x1b[33mtmux is not installed.\x1b[0m It will be installed with:');
|
|
1110
|
+
console.log(` ${plan.command}`);
|
|
1111
|
+
let go = opts.yes === true;
|
|
1112
|
+
if (!go) {
|
|
1113
|
+
const rl = createRL();
|
|
1114
|
+
const ans = (await ask(rl, 'Run this now? [y/N]: ')).trim().toLowerCase();
|
|
1115
|
+
rl.close();
|
|
1116
|
+
go = ans === 'y' || ans === 'yes';
|
|
1117
|
+
}
|
|
1118
|
+
if (!go) {
|
|
1119
|
+
console.log(`Declined. Install tmux manually (${plan.command}) then re-run: dialout setup-cowork`);
|
|
1120
|
+
process.exitCode = 1;
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
execFileSync('/bin/sh', ['-c', plan.command], { stdio: 'inherit' });
|
|
1125
|
+
}
|
|
1126
|
+
catch {
|
|
1127
|
+
console.log('\x1b[31mtmux install failed.\x1b[0m Install it manually then re-run setup-cowork.');
|
|
1128
|
+
process.exitCode = 1;
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (!(await tmuxAvailable())) {
|
|
1132
|
+
console.log('\x1b[31mtmux still not found after install.\x1b[0m Install it manually then re-run setup-cowork.');
|
|
1133
|
+
process.exitCode = 1;
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
// Terminal selection.
|
|
1138
|
+
const detected = (0, terminal_detect_1.detectTerminals)();
|
|
1139
|
+
// Guard rail against the OTHER reported silent-success trap: a headless
|
|
1140
|
+
// SSH-only server showed a 16-row checklist, every row "(not installed)",
|
|
1141
|
+
// none current — and even a perfect pick there could never activate
|
|
1142
|
+
// cowork, since renderCoworkBlock gates the whole wrapper on
|
|
1143
|
+
// `[ -z "$SSH_TTY" ]`. Explicit --terminals still gets the warn-don't-
|
|
1144
|
+
// refuse treatment (dotfile-syncing users keep working, matching
|
|
1145
|
+
// unmatchableTokens below); the interactive checklist is refused outright
|
|
1146
|
+
// since no selection there could ever help.
|
|
1147
|
+
const viability = (0, cowork_1.coworkViability)(process.env, detected.some((t) => t.installed));
|
|
1148
|
+
if (!viability.usable) {
|
|
1149
|
+
for (const reason of viability.reasons) {
|
|
1150
|
+
if (reason === 'ssh-session') {
|
|
1151
|
+
console.log('\x1b[33mThis is an SSH session — the cowork wrapper deliberately skips SSH logins, so no selection here can ever activate it.\x1b[0m');
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
console.log('\x1b[33mNo terminal emulator is installed on this machine — cowork wraps the app you open a terminal FROM, which runs on your local machine, not this server.\x1b[0m');
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
console.log('Instead, open a terminal from the Dialout panel: it creates a tmux session on this machine directly and needs no cowork.');
|
|
1158
|
+
if (typeof opts.terminals !== 'string') {
|
|
1159
|
+
process.exitCode = 1;
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
let selected;
|
|
1164
|
+
if (typeof opts.terminals === 'string') {
|
|
1165
|
+
selected = (0, cowork_1.sanitizeTokens)(opts.terminals.split(',').map((t) => t.trim()).filter(Boolean));
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
// installed apps first; current terminal is NOT pre-ticked (only prior picks are).
|
|
1169
|
+
const ordered = [...detected].sort((a, b) => Number(b.installed) - Number(a.installed));
|
|
1170
|
+
const prev = new Set(config.coworkTerminals ?? []);
|
|
1171
|
+
console.log('');
|
|
1172
|
+
console.log('Which terminal app(s) should be exposed to Dialout remote (auto-wrap into tmux)?');
|
|
1173
|
+
console.log('All other terminals stay native (full OS text selection + scrolling).');
|
|
1174
|
+
if (process.stdin.isTTY) {
|
|
1175
|
+
const items = ordered.map((t) => ({
|
|
1176
|
+
label: t.name,
|
|
1177
|
+
token: t.token,
|
|
1178
|
+
hint: [t.installed ? '' : 'not installed', t.current ? 'this terminal' : ''].filter(Boolean).join(', ') || undefined,
|
|
1179
|
+
checked: prev.has(t.token),
|
|
1180
|
+
}));
|
|
1181
|
+
console.log('');
|
|
1182
|
+
const picked = await promptChecklist(items);
|
|
1183
|
+
if (picked === null) {
|
|
1184
|
+
console.log('Cancelled — no changes made.');
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
selected = (0, cowork_1.sanitizeTokens)(picked);
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
// Non-TTY (piped) fallback: numbered toggle prompt.
|
|
1191
|
+
const ticks = ordered.map((t) => prev.has(t.token));
|
|
1192
|
+
ordered.forEach((t, i) => {
|
|
1193
|
+
const box = ticks[i] ? '[x]' : '[ ]';
|
|
1194
|
+
const tags = [t.installed ? '' : 'not installed', t.current ? 'this terminal' : ''].filter(Boolean).join(', ');
|
|
1195
|
+
console.log(` ${i + 1}) ${box} ${t.name}${tags ? ` (${tags})` : ''}`);
|
|
1196
|
+
});
|
|
1197
|
+
const rl = createRL();
|
|
1198
|
+
const ans = (await ask(rl, '\nToggle by number (space/comma separated), Enter to confirm: ')).trim();
|
|
1199
|
+
rl.close();
|
|
1200
|
+
for (const tok of ans.split(/[, ]+/).filter(Boolean)) {
|
|
1201
|
+
const idx = parseInt(tok, 10) - 1;
|
|
1202
|
+
if (idx >= 0 && idx < ticks.length)
|
|
1203
|
+
ticks[idx] = !ticks[idx];
|
|
1204
|
+
}
|
|
1205
|
+
selected = (0, cowork_1.sanitizeTokens)(ordered.filter((_, i) => ticks[i]).map((t) => t.token));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
// Guard rail against the reported silent no-op: warn (loudly, not
|
|
1209
|
+
// silently) when a selected token cannot possibly match on this platform
|
|
1210
|
+
// — e.g. picking "Apple_Terminal" on linux, or a Linux-only marker token
|
|
1211
|
+
// on darwin. The rc block is still written below (dotfiles may be synced
|
|
1212
|
+
// across machines); this only changes what's printed and the exit code.
|
|
1213
|
+
const badTokens = (0, cowork_1.unmatchableTokens)(selected, process.platform);
|
|
1214
|
+
for (const tok of badTokens) {
|
|
1215
|
+
console.log(`\x1b[33mWarning: "${tok}" cannot match on ${process.platform} — that terminal does not run here.\x1b[0m`);
|
|
1216
|
+
}
|
|
1217
|
+
if (selected.length > 0 && badTokens.length === selected.length) {
|
|
1218
|
+
console.log('\x1b[33mNone of the selected terminal(s) can ever match on this machine — cowork will never activate.\x1b[0m');
|
|
1219
|
+
process.exitCode = 1;
|
|
1220
|
+
}
|
|
1221
|
+
// Adopt existing tmux sessions (unchanged behavior).
|
|
1222
|
+
const all = await listSessions();
|
|
1223
|
+
const orphans = all.filter((s) => s.origin !== 'browser' && !s.folder);
|
|
1224
|
+
if (orphans.length > 0 && opts.adopt !== false) {
|
|
1225
|
+
console.log('');
|
|
1226
|
+
console.log(`Found ${orphans.length} existing tmux session(s) not managed by Dialout:`);
|
|
1227
|
+
orphans.forEach((s, i) => console.log(` ${i + 1}) ${s.name} (${s.termProgram})`));
|
|
1228
|
+
let pick;
|
|
1229
|
+
if (opts.adoptAll) {
|
|
1230
|
+
pick = 'a';
|
|
1231
|
+
}
|
|
1232
|
+
else {
|
|
1233
|
+
const rl = createRL();
|
|
1234
|
+
pick = (await ask(rl, 'Bind which to Dialout? [1,2,… / a=all / n=none]: ')).trim().toLowerCase();
|
|
1235
|
+
rl.close();
|
|
1236
|
+
}
|
|
1237
|
+
let chosen = [];
|
|
1238
|
+
if (pick === 'a')
|
|
1239
|
+
chosen = orphans;
|
|
1240
|
+
else if (pick && pick !== 'n') {
|
|
1241
|
+
const idx = pick.split(/[, ]+/).map((x) => parseInt(x, 10) - 1).filter((n) => n >= 0 && n < orphans.length);
|
|
1242
|
+
chosen = idx.map((i) => orphans[i]);
|
|
1243
|
+
}
|
|
1244
|
+
for (const s of chosen) {
|
|
1245
|
+
const tmux = (args) => { try {
|
|
1246
|
+
execFileSync('tmux', args, { timeout: 5000, stdio: 'pipe' });
|
|
1247
|
+
}
|
|
1248
|
+
catch { } };
|
|
1249
|
+
let cwd = '';
|
|
1250
|
+
try {
|
|
1251
|
+
cwd = execFileSync('tmux', ['display-message', '-p', '-t', s.name, '#{pane_current_path}'], { timeout: 5000, stdio: 'pipe' }).toString().trim();
|
|
1252
|
+
}
|
|
1253
|
+
catch { }
|
|
1254
|
+
const base = cwd.split('/').filter(Boolean).pop() || '';
|
|
1255
|
+
tmux(['set-option', '-t', s.name, '@devdash_origin', 'native']);
|
|
1256
|
+
if (s.termProgram === 'unknown')
|
|
1257
|
+
tmux(['set-option', '-t', s.name, '@term_program', 'unknown']);
|
|
1258
|
+
if (base)
|
|
1259
|
+
tmux(['set-option', '-t', s.name, '@devdash_folder', base]);
|
|
1260
|
+
if (cwd)
|
|
1261
|
+
tmux(['set-option', '-t', s.name, '@devdash_folder_path', cwd]);
|
|
1262
|
+
}
|
|
1263
|
+
if (chosen.length > 0)
|
|
1264
|
+
console.log(`\x1b[32m✓ Adopted ${chosen.length} session(s)\x1b[0m (live after the agent's next poll)`);
|
|
1265
|
+
}
|
|
1266
|
+
// Persist the allowlist and write / remove the block.
|
|
1267
|
+
config.coworkTerminals = selected;
|
|
1268
|
+
if (selected.length === 0) {
|
|
1269
|
+
for (const rc of rcFiles) {
|
|
1270
|
+
if (!fs.existsSync(rc))
|
|
1271
|
+
continue;
|
|
1272
|
+
fs.writeFileSync(rc, (0, cowork_1.removeCoworkBlock)(fs.readFileSync(rc, 'utf-8')));
|
|
1273
|
+
console.log(` removed wrapper from ${rc}`);
|
|
1274
|
+
}
|
|
1275
|
+
config.cowork = false;
|
|
1276
|
+
(0, config_1.saveConfig)(config);
|
|
1277
|
+
console.log('');
|
|
1278
|
+
console.log('No remote terminal selected — all terminals are native (OS selection + scrolling).');
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const existing = rcFiles.filter((f) => fs.existsSync(f));
|
|
1282
|
+
const targets = existing.length > 0
|
|
1283
|
+
? existing
|
|
1284
|
+
: [(process.env.SHELL || '').includes('bash') ? rcFiles[1] : rcFiles[0]];
|
|
1285
|
+
for (const rc of targets) {
|
|
1286
|
+
const result = (0, cowork_1.installCoworkBlock)(rc, selected);
|
|
1287
|
+
applySshLocalNetworkWorkaround();
|
|
1288
|
+
console.log(` ${result}: wrapper in ${rc}`);
|
|
1289
|
+
}
|
|
1290
|
+
config.cowork = true;
|
|
1291
|
+
(0, config_1.saveConfig)(config);
|
|
1292
|
+
console.log('');
|
|
1293
|
+
console.log(`\x1b[32mCowork enabled.\x1b[0m Remote app(s): ${selected.join(', ')}`);
|
|
1294
|
+
console.log('These auto-wrap into tmux and appear in Dialout → Terminals.');
|
|
1295
|
+
console.log('Every other terminal stays native (OS text selection + native scrolling).');
|
|
1296
|
+
if ((0, terminal_detect_1.currentTerminalToken)() === '') {
|
|
1297
|
+
console.log('\x1b[33mNote:\x1b[0m setup ran inside tmux, so "this terminal" could not be pre-ticked.');
|
|
1298
|
+
console.log('Re-run from a native terminal window if the checklist missed your app.');
|
|
1299
|
+
}
|
|
1300
|
+
console.log('Restart the agent to start reporting sessions: dialout restart');
|
|
1301
|
+
});
|
|
1302
|
+
// --- update ---
|
|
1303
|
+
program
|
|
1304
|
+
.command('update')
|
|
1305
|
+
.description('Update Dialout to the latest version')
|
|
1306
|
+
.addHelpText('after', `
|
|
1307
|
+
Checks the IndiaNIC registry for a newer version and installs it.
|
|
1308
|
+
If running as a service, you'll be prompted to restart it.
|
|
1309
|
+
|
|
1310
|
+
Examples:
|
|
1311
|
+
$ dialout update # update to latest
|
|
1312
|
+
$ dialout --version # check current version`)
|
|
1313
|
+
.action(async () => {
|
|
1314
|
+
await (0, update_check_1.performUpdate)();
|
|
1315
|
+
});
|
|
1316
|
+
// --- config ---
|
|
1317
|
+
const configCmd = program
|
|
1318
|
+
.command('config')
|
|
1319
|
+
.description('View or modify agent configuration')
|
|
1320
|
+
.addHelpText('after', `
|
|
1321
|
+
Subcommands:
|
|
1322
|
+
dialout config show Print current config (API key masked)
|
|
1323
|
+
dialout config path Show config file location
|
|
1324
|
+
dialout config reset Reset to defaults (keeps API key + server)
|
|
1325
|
+
dialout config set <key> <value> Set a config value
|
|
1326
|
+
|
|
1327
|
+
Config keys:
|
|
1328
|
+
serverUrl WebSocket URL (e.g., wss://dialout.example.com/ws)
|
|
1329
|
+
apiKey Machine API key (e.g., mch_xxxx)
|
|
1330
|
+
heartbeatInterval Keep-alive interval in ms (default: 30000)
|
|
1331
|
+
cronInterval Watchdog check interval in minutes (default: 5)
|
|
1332
|
+
scanPorts Comma-separated ports (e.g., 3000,8080,5173)
|
|
1333
|
+
scanRange Port range (e.g., 3000-9000)
|
|
1334
|
+
|
|
1335
|
+
Examples:
|
|
1336
|
+
$ dialout config show
|
|
1337
|
+
$ dialout config set scanPorts 3000,4200,8080
|
|
1338
|
+
$ dialout config reset`);
|
|
1339
|
+
configCmd
|
|
1340
|
+
.command('show')
|
|
1341
|
+
.description('Print current configuration (API key masked)')
|
|
1342
|
+
.action(() => {
|
|
1343
|
+
const config = (0, config_1.loadConfig)();
|
|
1344
|
+
const display = { ...config, apiKey: config.apiKey ? '****' + config.apiKey.slice(-4) : '' };
|
|
1345
|
+
console.log(JSON.stringify(display, null, 2));
|
|
1346
|
+
});
|
|
1347
|
+
configCmd
|
|
1348
|
+
.command('path')
|
|
1349
|
+
.description('Show config file location')
|
|
1350
|
+
.action(() => {
|
|
1351
|
+
console.log((0, config_1.getConfigPath)());
|
|
1352
|
+
});
|
|
1353
|
+
configCmd
|
|
1354
|
+
.command('reset')
|
|
1355
|
+
.description('Reset config to defaults (keeps API key and server URL)')
|
|
1356
|
+
.addHelpText('after', `
|
|
1357
|
+
Resets all configuration values to defaults while preserving:
|
|
1358
|
+
- serverUrl (your server connection)
|
|
1359
|
+
- apiKey (your machine authentication)
|
|
1360
|
+
|
|
1361
|
+
This is useful if you've changed scan ports, intervals, etc. and want to start fresh.
|
|
1362
|
+
|
|
1363
|
+
Examples:
|
|
1364
|
+
$ dialout config reset # reset to defaults
|
|
1365
|
+
$ dialout config show # verify the result`)
|
|
1366
|
+
.action(() => {
|
|
1367
|
+
const config = (0, config_1.loadConfig)();
|
|
1368
|
+
const { serverUrl, apiKey } = config;
|
|
1369
|
+
const defaults = {
|
|
1370
|
+
serverUrl,
|
|
1371
|
+
apiKey,
|
|
1372
|
+
scanPorts: [3000, 3001, 4200, 5173, 5174, 8000, 8080, 8081, 9000],
|
|
1373
|
+
scanRange: { from: 3000, to: 9000 },
|
|
1374
|
+
heartbeatInterval: 30000,
|
|
1375
|
+
cronInterval: 5,
|
|
1376
|
+
};
|
|
1377
|
+
(0, config_1.saveConfig)(defaults);
|
|
1378
|
+
console.log('Config reset to defaults (serverUrl and apiKey preserved).');
|
|
1379
|
+
console.log(`Config file: ${(0, config_1.getConfigPath)()}`);
|
|
1380
|
+
});
|
|
1381
|
+
configCmd
|
|
1382
|
+
.command('set <key> <value>')
|
|
1383
|
+
.description('Set a configuration value')
|
|
1384
|
+
.addHelpText('after', `
|
|
1385
|
+
Available keys:
|
|
1386
|
+
serverUrl WebSocket URL (e.g., wss://dialout.example.com/ws)
|
|
1387
|
+
apiKey Machine API key (e.g., mch_xxxx)
|
|
1388
|
+
heartbeatInterval Keep-alive interval in ms (default: 30000)
|
|
1389
|
+
cronInterval Watchdog check interval in minutes (default: 5)
|
|
1390
|
+
scanPorts Comma-separated ports (e.g., 3000,8080,5173)
|
|
1391
|
+
scanRange Port range (e.g., 3000-9000)
|
|
1392
|
+
|
|
1393
|
+
Examples:
|
|
1394
|
+
$ dialout config set serverUrl wss://new-server.com/ws
|
|
1395
|
+
$ dialout config set cronInterval 3
|
|
1396
|
+
$ dialout config set scanPorts 3000,4200,8080
|
|
1397
|
+
$ dialout config set scanRange 3000-9000
|
|
1398
|
+
$ dialout config set heartbeatInterval 60000`)
|
|
1399
|
+
.action((key, value) => {
|
|
1400
|
+
const config = (0, config_1.loadConfig)();
|
|
1401
|
+
if (!(key in config)) {
|
|
1402
|
+
console.error(`Unknown config key: ${key}`);
|
|
1403
|
+
console.error(`Valid keys: ${Object.keys(config).join(', ')}`);
|
|
1404
|
+
process.exit(1);
|
|
1405
|
+
}
|
|
1406
|
+
if (key === 'heartbeatInterval' || key === 'cronInterval') {
|
|
1407
|
+
config[key] = parseInt(value, 10);
|
|
1408
|
+
}
|
|
1409
|
+
else if (key === 'scanPorts') {
|
|
1410
|
+
config[key] = value.split(',').map((p) => parseInt(p.trim(), 10));
|
|
1411
|
+
}
|
|
1412
|
+
else if (key === 'scanRange') {
|
|
1413
|
+
const [from, to] = value.split('-').map((p) => parseInt(p.trim(), 10));
|
|
1414
|
+
config[key] = { from, to };
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
config[key] = value;
|
|
1418
|
+
}
|
|
1419
|
+
// Keep the active profile in sync when connection values change
|
|
1420
|
+
if ((key === 'serverUrl' || key === 'apiKey') && config.activeProfile && config.profiles?.[config.activeProfile]) {
|
|
1421
|
+
config.profiles[config.activeProfile][key] = config[key];
|
|
1422
|
+
}
|
|
1423
|
+
(0, config_1.saveConfig)(config);
|
|
1424
|
+
console.log(`Set ${key} = ${JSON.stringify(config[key])}`);
|
|
1425
|
+
});
|
|
1426
|
+
program.parse();
|
|
1427
|
+
//# sourceMappingURL=cli.js.map
|