@sonde/agent 0.2.5 → 0.2.7
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/.turbo/turbo-build.log +6 -4
- package/.turbo/turbo-test.log +96 -23
- package/CHANGELOG.md +20 -0
- package/dist/cli/packs.d.ts +6 -0
- package/dist/cli/packs.d.ts.map +1 -1
- package/dist/cli/packs.js +42 -8
- package/dist/cli/packs.js.map +1 -1
- package/dist/cli/packs.test.js +56 -1
- package/dist/cli/packs.test.js.map +1 -1
- package/dist/cli/service.d.ts +12 -0
- package/dist/cli/service.d.ts.map +1 -0
- package/dist/cli/service.js +164 -0
- package/dist/cli/service.js.map +1 -0
- package/dist/cli/service.test.d.ts +2 -0
- package/dist/cli/service.test.d.ts.map +1 -0
- package/dist/cli/service.test.js +99 -0
- package/dist/cli/service.test.js.map +1 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +7 -8
- package/dist/cli/update.js.map +1 -1
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +55 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +126 -5
- package/dist/index.js.map +1 -1
- package/dist/tui/installer/InstallerApp.d.ts.map +1 -1
- package/dist/tui/installer/InstallerApp.js +3 -1
- package/dist/tui/installer/InstallerApp.js.map +1 -1
- package/dist/tui/installer/StepComplete.d.ts.map +1 -1
- package/dist/tui/installer/StepComplete.js +10 -1
- package/dist/tui/installer/StepComplete.js.map +1 -1
- package/dist/tui/installer/StepService.d.ts +6 -0
- package/dist/tui/installer/StepService.d.ts.map +1 -0
- package/dist/tui/installer/StepService.js +49 -0
- package/dist/tui/installer/StepService.js.map +1 -0
- package/dist/tui/manager/ManagerApp.d.ts +2 -1
- package/dist/tui/manager/ManagerApp.d.ts.map +1 -1
- package/dist/tui/manager/ManagerApp.js +3 -2
- package/dist/tui/manager/ManagerApp.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/packs.test.ts +65 -0
- package/src/cli/packs.ts +56 -10
- package/src/cli/service.test.ts +124 -0
- package/src/cli/service.ts +178 -0
- package/src/cli/update.ts +7 -8
- package/src/config.ts +57 -0
- package/src/index.ts +156 -5
- package/src/tui/installer/InstallerApp.tsx +6 -2
- package/src/tui/installer/StepComplete.tsx +12 -1
- package/src/tui/installer/StepService.tsx +112 -0
- package/src/tui/manager/ManagerApp.tsx +4 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
|
|
5
|
+
const UNIT_NAME = 'sonde-agent';
|
|
6
|
+
const UNIT_PATH = `/etc/systemd/system/${UNIT_NAME}.service`;
|
|
7
|
+
|
|
8
|
+
export interface ServiceResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isLinux(): boolean {
|
|
14
|
+
return process.platform === 'linux';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveSondeBinary(): string {
|
|
18
|
+
return execFileSync('which', ['sonde'], {
|
|
19
|
+
encoding: 'utf-8',
|
|
20
|
+
timeout: 5_000,
|
|
21
|
+
}).trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function generateUnitFile(): string {
|
|
25
|
+
const user = os.userInfo();
|
|
26
|
+
const sondeBin = resolveSondeBinary();
|
|
27
|
+
|
|
28
|
+
return `[Unit]
|
|
29
|
+
Description=Sonde Agent
|
|
30
|
+
After=network-online.target
|
|
31
|
+
Wants=network-online.target
|
|
32
|
+
|
|
33
|
+
[Service]
|
|
34
|
+
Type=simple
|
|
35
|
+
User=${user.username}
|
|
36
|
+
Environment=HOME=${user.homedir}
|
|
37
|
+
ExecStart=${sondeBin} start --headless
|
|
38
|
+
Restart=on-failure
|
|
39
|
+
RestartSec=5
|
|
40
|
+
StandardOutput=journal
|
|
41
|
+
StandardError=journal
|
|
42
|
+
SyslogIdentifier=${UNIT_NAME}
|
|
43
|
+
|
|
44
|
+
[Install]
|
|
45
|
+
WantedBy=multi-user.target
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isServiceInstalled(): boolean {
|
|
50
|
+
if (!isLinux()) return false;
|
|
51
|
+
return fs.existsSync(UNIT_PATH);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getServiceStatus(): string {
|
|
55
|
+
if (!isLinux()) return 'unsupported';
|
|
56
|
+
if (!isServiceInstalled()) return 'not-installed';
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
return execFileSync('systemctl', ['is-active', UNIT_NAME], {
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
timeout: 5_000,
|
|
62
|
+
}).trim();
|
|
63
|
+
} catch {
|
|
64
|
+
return 'inactive';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function installService(): ServiceResult {
|
|
69
|
+
if (!isLinux()) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
message: 'systemd services are only supported on Linux.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const unitContent = generateUnitFile();
|
|
78
|
+
|
|
79
|
+
execFileSync('sudo', ['tee', UNIT_PATH], {
|
|
80
|
+
input: unitContent,
|
|
81
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
82
|
+
timeout: 10_000,
|
|
83
|
+
});
|
|
84
|
+
execFileSync('sudo', ['systemctl', 'daemon-reload'], { stdio: 'pipe', timeout: 10_000 });
|
|
85
|
+
execFileSync('sudo', ['systemctl', 'enable', UNIT_NAME], { stdio: 'pipe', timeout: 10_000 });
|
|
86
|
+
execFileSync('sudo', ['systemctl', 'start', UNIT_NAME], { stdio: 'pipe', timeout: 10_000 });
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
message: `${UNIT_NAME} service installed and started.`,
|
|
91
|
+
};
|
|
92
|
+
} catch (err: unknown) {
|
|
93
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
94
|
+
return { success: false, message: `Failed to install service: ${msg}` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function uninstallService(): ServiceResult {
|
|
99
|
+
if (!isLinux()) {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
message: 'systemd services are only supported on Linux.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!isServiceInstalled()) {
|
|
107
|
+
return { success: false, message: 'Service is not installed.' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
execFileSync('sudo', ['systemctl', 'stop', UNIT_NAME], { stdio: 'pipe', timeout: 10_000 });
|
|
112
|
+
execFileSync('sudo', ['systemctl', 'disable', UNIT_NAME], { stdio: 'pipe', timeout: 10_000 });
|
|
113
|
+
execFileSync('sudo', ['rm', '-f', UNIT_PATH], { stdio: 'pipe', timeout: 5_000 });
|
|
114
|
+
execFileSync('sudo', ['systemctl', 'daemon-reload'], { stdio: 'pipe', timeout: 10_000 });
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
success: true,
|
|
118
|
+
message: `${UNIT_NAME} service removed.`,
|
|
119
|
+
};
|
|
120
|
+
} catch (err: unknown) {
|
|
121
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
message: `Failed to uninstall service: ${msg}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function stopService(): ServiceResult {
|
|
130
|
+
if (!isLinux()) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
message: 'systemd services are only supported on Linux.',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
execFileSync('systemctl', ['stop', UNIT_NAME], { stdio: 'pipe', timeout: 10_000 });
|
|
139
|
+
return { success: true, message: `Stopped ${UNIT_NAME} service.` };
|
|
140
|
+
} catch {
|
|
141
|
+
try {
|
|
142
|
+
execFileSync('sudo', ['systemctl', 'stop', UNIT_NAME], { stdio: 'inherit', timeout: 30_000 });
|
|
143
|
+
return { success: true, message: `Stopped ${UNIT_NAME} service.` };
|
|
144
|
+
} catch {
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
message: `Could not stop service. Try: sudo systemctl stop ${UNIT_NAME}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function restartService(): ServiceResult {
|
|
154
|
+
if (!isLinux()) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
message: 'systemd services are only supported on Linux.',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
execFileSync('systemctl', ['restart', UNIT_NAME], { stdio: 'pipe', timeout: 10_000 });
|
|
163
|
+
return { success: true, message: `Restarted ${UNIT_NAME} service.` };
|
|
164
|
+
} catch {
|
|
165
|
+
try {
|
|
166
|
+
execFileSync('sudo', ['systemctl', 'restart', UNIT_NAME], {
|
|
167
|
+
stdio: 'inherit',
|
|
168
|
+
timeout: 30_000,
|
|
169
|
+
});
|
|
170
|
+
return { success: true, message: `Restarted ${UNIT_NAME} service.` };
|
|
171
|
+
} catch {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
message: `Could not restart service. Try: sudo systemctl restart ${UNIT_NAME}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
package/src/cli/update.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { VERSION } from '../version.js';
|
|
3
|
+
import { isServiceInstalled, restartService } from './service.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Lightweight semver comparison: returns true if a < b.
|
|
@@ -66,13 +67,11 @@ export function performUpdate(targetVersion: string): void {
|
|
|
66
67
|
|
|
67
68
|
console.log(`Successfully updated to v${targetVersion}`);
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
console.log('
|
|
75
|
-
} catch {
|
|
76
|
-
// systemd not available or service not installed — that's fine
|
|
70
|
+
if (isServiceInstalled()) {
|
|
71
|
+
const result = restartService();
|
|
72
|
+
console.log(result.message);
|
|
73
|
+
} else {
|
|
74
|
+
console.log('Restart the agent to use the new version:');
|
|
75
|
+
console.log(' sonde restart');
|
|
77
76
|
}
|
|
78
77
|
}
|
package/src/config.ts
CHANGED
|
@@ -13,10 +13,12 @@ export interface AgentConfig {
|
|
|
13
13
|
caCertPath?: string;
|
|
14
14
|
scrubPatterns?: string[];
|
|
15
15
|
allowUnsignedPacks?: boolean;
|
|
16
|
+
disabledPacks?: string[];
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
const CONFIG_DIR = path.join(os.homedir(), '.sonde');
|
|
19
20
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
21
|
+
const PID_FILE = path.join(CONFIG_DIR, 'agent.pid');
|
|
20
22
|
|
|
21
23
|
export function getConfigPath(): string {
|
|
22
24
|
return CONFIG_FILE;
|
|
@@ -74,3 +76,58 @@ export function saveCerts(
|
|
|
74
76
|
config.keyPath = keyPath;
|
|
75
77
|
config.caCertPath = caCertPath;
|
|
76
78
|
}
|
|
79
|
+
|
|
80
|
+
export function writePidFile(pid: number): void {
|
|
81
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
82
|
+
fs.writeFileSync(PID_FILE, String(pid), 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Read PID file and verify the process is still alive.
|
|
87
|
+
* Returns undefined if file missing or process is dead.
|
|
88
|
+
*/
|
|
89
|
+
export function readPidFile(): number | undefined {
|
|
90
|
+
let raw: string;
|
|
91
|
+
try {
|
|
92
|
+
raw = fs.readFileSync(PID_FILE, 'utf-8').trim();
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const pid = Number.parseInt(raw, 10);
|
|
98
|
+
if (Number.isNaN(pid)) return undefined;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
process.kill(pid, 0);
|
|
102
|
+
return pid;
|
|
103
|
+
} catch {
|
|
104
|
+
// Process is dead — clean up stale PID file
|
|
105
|
+
removePidFile();
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function removePidFile(): void {
|
|
111
|
+
try {
|
|
112
|
+
fs.unlinkSync(PID_FILE);
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore if already removed
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Stop a running background agent if one exists.
|
|
120
|
+
* Returns true if an agent was stopped.
|
|
121
|
+
*/
|
|
122
|
+
export function stopRunningAgent(): boolean {
|
|
123
|
+
const pid = readPidFile();
|
|
124
|
+
if (pid === undefined) return false;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
process.kill(pid, 'SIGTERM');
|
|
128
|
+
} catch {
|
|
129
|
+
// Process already gone
|
|
130
|
+
}
|
|
131
|
+
removePidFile();
|
|
132
|
+
return true;
|
|
133
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
3
4
|
import os from 'node:os';
|
|
4
|
-
import {
|
|
5
|
+
import { packRegistry } from '@sonde/packs';
|
|
6
|
+
import { buildEnabledPacks, handlePacksCommand } from './cli/packs.js';
|
|
7
|
+
import {
|
|
8
|
+
getServiceStatus,
|
|
9
|
+
installService,
|
|
10
|
+
isServiceInstalled,
|
|
11
|
+
restartService,
|
|
12
|
+
stopService,
|
|
13
|
+
uninstallService,
|
|
14
|
+
} from './cli/service.js';
|
|
5
15
|
import { checkForUpdate, performUpdate } from './cli/update.js';
|
|
6
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type AgentConfig,
|
|
18
|
+
getConfigPath,
|
|
19
|
+
loadConfig,
|
|
20
|
+
removePidFile,
|
|
21
|
+
saveConfig,
|
|
22
|
+
stopRunningAgent,
|
|
23
|
+
writePidFile,
|
|
24
|
+
} from './config.js';
|
|
7
25
|
import { AgentConnection, type ConnectionEvents, enrollWithHub } from './runtime/connection.js';
|
|
8
26
|
import { ProbeExecutor } from './runtime/executor.js';
|
|
9
27
|
import { checkNotRoot } from './runtime/privilege.js';
|
|
10
28
|
import { buildPatterns } from './runtime/scrubber.js';
|
|
29
|
+
import { createSystemChecker, scanForSoftware } from './system/scanner.js';
|
|
11
30
|
import { VERSION } from './version.js';
|
|
12
31
|
|
|
13
32
|
const args = process.argv.slice(2);
|
|
@@ -25,8 +44,11 @@ function printUsage(): void {
|
|
|
25
44
|
console.log(' install Interactive guided setup (enroll + scan + packs)');
|
|
26
45
|
console.log(' enroll Enroll this agent with a hub');
|
|
27
46
|
console.log(' start Start the agent (TUI by default, --headless for daemon)');
|
|
47
|
+
console.log(' stop Stop the background agent');
|
|
48
|
+
console.log(' restart Restart the agent in background');
|
|
28
49
|
console.log(' status Show agent status');
|
|
29
50
|
console.log(' packs Manage packs (list, scan, install, uninstall)');
|
|
51
|
+
console.log(' service Manage systemd service (install, uninstall, status)');
|
|
30
52
|
console.log(' update Check for and install agent updates');
|
|
31
53
|
console.log(' mcp-bridge stdio MCP bridge (for Claude Code integration)');
|
|
32
54
|
console.log('');
|
|
@@ -61,7 +83,10 @@ function createRuntime(events: ConnectionEvents): Runtime {
|
|
|
61
83
|
process.exit(1);
|
|
62
84
|
}
|
|
63
85
|
|
|
64
|
-
const
|
|
86
|
+
const enabledPacks = buildEnabledPacks(
|
|
87
|
+
packRegistry, config.disabledPacks ?? [],
|
|
88
|
+
);
|
|
89
|
+
const executor = new ProbeExecutor(enabledPacks, undefined, buildPatterns(config.scrubPatterns));
|
|
65
90
|
const connection = new AgentConnection(config, executor, events);
|
|
66
91
|
|
|
67
92
|
return { config, executor, connection };
|
|
@@ -120,6 +145,20 @@ async function cmdEnroll(): Promise<void> {
|
|
|
120
145
|
config.enrollmentToken = undefined;
|
|
121
146
|
saveConfig(config);
|
|
122
147
|
|
|
148
|
+
// Auto-detect packs
|
|
149
|
+
const manifests = [...packRegistry.values()].map((p) => p.manifest);
|
|
150
|
+
const checker = createSystemChecker();
|
|
151
|
+
const scanResults = scanForSoftware(manifests, checker);
|
|
152
|
+
const detectedNames = scanResults
|
|
153
|
+
.filter((r) => r.detected)
|
|
154
|
+
.map((r) => r.packName);
|
|
155
|
+
const allNames = manifests.map((m) => m.name);
|
|
156
|
+
const enabledNames = ['system', ...detectedNames.filter((n) => n !== 'system')];
|
|
157
|
+
const disabledNames = allNames.filter((n) => !enabledNames.includes(n));
|
|
158
|
+
|
|
159
|
+
config.disabledPacks = disabledNames;
|
|
160
|
+
saveConfig(config);
|
|
161
|
+
|
|
123
162
|
console.log('Agent enrolled successfully.');
|
|
124
163
|
console.log(` Hub: ${hubUrl}`);
|
|
125
164
|
console.log(` Name: ${agentName}`);
|
|
@@ -129,6 +168,14 @@ async function cmdEnroll(): Promise<void> {
|
|
|
129
168
|
console.log(' mTLS: Client certificate issued and saved');
|
|
130
169
|
}
|
|
131
170
|
console.log('');
|
|
171
|
+
console.log('Pack detection:');
|
|
172
|
+
for (const name of enabledNames) {
|
|
173
|
+
console.log(` ✓ ${name}`);
|
|
174
|
+
}
|
|
175
|
+
for (const name of disabledNames) {
|
|
176
|
+
console.log(` ✗ ${name} (not detected)`);
|
|
177
|
+
}
|
|
178
|
+
console.log('');
|
|
132
179
|
console.log('Run "sonde start" to connect.');
|
|
133
180
|
}
|
|
134
181
|
|
|
@@ -160,23 +207,78 @@ function cmdStart(): void {
|
|
|
160
207
|
console.log('');
|
|
161
208
|
|
|
162
209
|
connection.start();
|
|
210
|
+
writePidFile(process.pid);
|
|
163
211
|
process.stdin.unref();
|
|
164
212
|
|
|
165
213
|
const shutdown = () => {
|
|
166
214
|
console.log('\nShutting down...');
|
|
167
215
|
connection.stop();
|
|
216
|
+
removePidFile();
|
|
168
217
|
process.exit(0);
|
|
169
218
|
};
|
|
170
219
|
process.on('SIGINT', shutdown);
|
|
171
220
|
process.on('SIGTERM', shutdown);
|
|
172
221
|
}
|
|
173
222
|
|
|
223
|
+
function spawnBackgroundAgent(): number {
|
|
224
|
+
const child = spawn(process.execPath, [process.argv[1]!, 'start', '--headless'], {
|
|
225
|
+
detached: true,
|
|
226
|
+
stdio: 'ignore',
|
|
227
|
+
});
|
|
228
|
+
child.unref();
|
|
229
|
+
return child.pid!;
|
|
230
|
+
}
|
|
231
|
+
|
|
174
232
|
async function cmdManager(): Promise<void> {
|
|
233
|
+
stopRunningAgent();
|
|
234
|
+
|
|
235
|
+
let detached = false;
|
|
175
236
|
const { render } = await import('ink');
|
|
176
237
|
const { createElement } = await import('react');
|
|
177
238
|
const { ManagerApp } = await import('./tui/manager/ManagerApp.js');
|
|
178
|
-
|
|
239
|
+
|
|
240
|
+
const onDetach = () => {
|
|
241
|
+
detached = true;
|
|
242
|
+
spawnBackgroundAgent();
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const { waitUntilExit } = render(
|
|
246
|
+
createElement(ManagerApp, { createRuntime, onDetach }),
|
|
247
|
+
);
|
|
179
248
|
await waitUntilExit();
|
|
249
|
+
|
|
250
|
+
if (detached) {
|
|
251
|
+
console.log('Agent detached to background.');
|
|
252
|
+
console.log(' sonde stop — stop the background agent');
|
|
253
|
+
console.log(' sonde start — reattach the TUI');
|
|
254
|
+
console.log(' sonde restart — restart in background');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function cmdStop(): void {
|
|
259
|
+
if (isServiceInstalled() && getServiceStatus() === 'active') {
|
|
260
|
+
const result = stopService();
|
|
261
|
+
console.log(result.message);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (stopRunningAgent()) {
|
|
266
|
+
console.log('Agent stopped.');
|
|
267
|
+
} else {
|
|
268
|
+
console.log('No running agent found.');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function cmdRestart(): void {
|
|
273
|
+
if (isServiceInstalled() && getServiceStatus() === 'active') {
|
|
274
|
+
const result = restartService();
|
|
275
|
+
console.log(result.message);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
stopRunningAgent();
|
|
280
|
+
const pid = spawnBackgroundAgent();
|
|
281
|
+
console.log(`Agent restarted in background (PID: ${pid}).`);
|
|
180
282
|
}
|
|
181
283
|
|
|
182
284
|
function cmdStatus(): void {
|
|
@@ -187,11 +289,51 @@ function cmdStatus(): void {
|
|
|
187
289
|
return;
|
|
188
290
|
}
|
|
189
291
|
|
|
190
|
-
console.log(
|
|
292
|
+
console.log(`Sonde Agent v${VERSION}`);
|
|
191
293
|
console.log(` Name: ${config.agentName}`);
|
|
192
294
|
console.log(` Hub: ${config.hubUrl}`);
|
|
193
295
|
console.log(` Agent ID: ${config.agentId ?? '(not yet assigned)'}`);
|
|
194
296
|
console.log(` Config: ${getConfigPath()}`);
|
|
297
|
+
|
|
298
|
+
if (isServiceInstalled()) {
|
|
299
|
+
console.log(` Service: ${getServiceStatus()}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function handleServiceCommand(subArgs: string[]): void {
|
|
304
|
+
const sub = subArgs[0];
|
|
305
|
+
|
|
306
|
+
switch (sub) {
|
|
307
|
+
case 'install': {
|
|
308
|
+
const result = installService();
|
|
309
|
+
console.log(result.message);
|
|
310
|
+
if (!result.success) process.exit(1);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case 'uninstall': {
|
|
314
|
+
const result = uninstallService();
|
|
315
|
+
console.log(result.message);
|
|
316
|
+
if (!result.success) process.exit(1);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case 'status': {
|
|
320
|
+
const status = getServiceStatus();
|
|
321
|
+
console.log(`sonde-agent service: ${status}`);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
default:
|
|
325
|
+
console.log('Usage: sonde service <command>');
|
|
326
|
+
console.log('');
|
|
327
|
+
console.log('Commands:');
|
|
328
|
+
console.log(' install Install systemd service (starts on boot)');
|
|
329
|
+
console.log(' uninstall Remove systemd service');
|
|
330
|
+
console.log(' status Show service status');
|
|
331
|
+
if (sub) {
|
|
332
|
+
console.error(`\nUnknown subcommand: ${sub}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
195
337
|
}
|
|
196
338
|
|
|
197
339
|
async function cmdInstall(): Promise<void> {
|
|
@@ -246,12 +388,21 @@ switch (command) {
|
|
|
246
388
|
});
|
|
247
389
|
}
|
|
248
390
|
break;
|
|
391
|
+
case 'stop':
|
|
392
|
+
cmdStop();
|
|
393
|
+
break;
|
|
394
|
+
case 'restart':
|
|
395
|
+
cmdRestart();
|
|
396
|
+
break;
|
|
249
397
|
case 'status':
|
|
250
398
|
cmdStatus();
|
|
251
399
|
break;
|
|
252
400
|
case 'packs':
|
|
253
401
|
handlePacksCommand(args.slice(1));
|
|
254
402
|
break;
|
|
403
|
+
case 'service':
|
|
404
|
+
handleServiceCommand(args.slice(1));
|
|
405
|
+
break;
|
|
255
406
|
case 'update':
|
|
256
407
|
cmdUpdate().catch((err: Error) => {
|
|
257
408
|
console.error(`Update failed: ${err.message}`);
|
|
@@ -7,8 +7,9 @@ import { StepHub } from './StepHub.js';
|
|
|
7
7
|
import { StepPacks } from './StepPacks.js';
|
|
8
8
|
import { StepPermissions } from './StepPermissions.js';
|
|
9
9
|
import { StepScan } from './StepScan.js';
|
|
10
|
+
import { StepService } from './StepService.js';
|
|
10
11
|
|
|
11
|
-
type Step = 'hub' | 'scan' | 'packs' | 'permissions' | 'complete';
|
|
12
|
+
type Step = 'hub' | 'scan' | 'packs' | 'permissions' | 'service' | 'complete';
|
|
12
13
|
|
|
13
14
|
export interface HubConfig {
|
|
14
15
|
hubUrl: string;
|
|
@@ -21,6 +22,7 @@ const STEP_LABELS: Record<Step, string> = {
|
|
|
21
22
|
scan: 'System Scan',
|
|
22
23
|
packs: 'Pack Selection',
|
|
23
24
|
permissions: 'Permissions',
|
|
25
|
+
service: 'Systemd Service',
|
|
24
26
|
complete: 'Complete',
|
|
25
27
|
};
|
|
26
28
|
|
|
@@ -75,11 +77,13 @@ export function InstallerApp({ initialHubUrl }: InstallerAppProps): JSX.Element
|
|
|
75
77
|
{step === 'permissions' && (
|
|
76
78
|
<StepPermissions
|
|
77
79
|
selectedPacks={selectedPacks}
|
|
78
|
-
onNext={() => setStep('
|
|
80
|
+
onNext={() => setStep('service')}
|
|
79
81
|
onBack={() => setStep('packs')}
|
|
80
82
|
/>
|
|
81
83
|
)}
|
|
82
84
|
|
|
85
|
+
{step === 'service' && <StepService onNext={() => setStep('complete')} />}
|
|
86
|
+
|
|
83
87
|
{step === 'complete' && <StepComplete hubConfig={hubConfig} selectedPacks={selectedPacks} />}
|
|
84
88
|
</Box>
|
|
85
89
|
);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { packRegistry } from '@sonde/packs';
|
|
1
2
|
import type { PackManifest } from '@sonde/shared';
|
|
2
3
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
4
|
import Spinner from 'ink-spinner';
|
|
4
5
|
import { useEffect, useState } from 'react';
|
|
6
|
+
import { buildEnabledPacks } from '../../cli/packs.js';
|
|
5
7
|
import { type AgentConfig, saveConfig } from '../../config.js';
|
|
6
8
|
import { enrollWithHub } from '../../runtime/connection.js';
|
|
7
9
|
import { ProbeExecutor } from '../../runtime/executor.js';
|
|
@@ -19,14 +21,23 @@ export function StepComplete({ hubConfig, selectedPacks }: StepCompleteProps): J
|
|
|
19
21
|
const [error, setError] = useState('');
|
|
20
22
|
|
|
21
23
|
useEffect(() => {
|
|
24
|
+
const selectedNames = new Set(selectedPacks.map((p) => p.name));
|
|
25
|
+
const disabledPacks = [...packRegistry.keys()]
|
|
26
|
+
.filter((name) => !selectedNames.has(name));
|
|
22
27
|
const config: AgentConfig = {
|
|
23
28
|
hubUrl: hubConfig.hubUrl,
|
|
24
29
|
apiKey: hubConfig.apiKey,
|
|
25
30
|
agentName: hubConfig.agentName,
|
|
31
|
+
disabledPacks: disabledPacks.length > 0
|
|
32
|
+
? disabledPacks
|
|
33
|
+
: undefined,
|
|
26
34
|
};
|
|
27
35
|
saveConfig(config);
|
|
28
36
|
|
|
29
|
-
const
|
|
37
|
+
const enabledPacks = buildEnabledPacks(
|
|
38
|
+
packRegistry, disabledPacks,
|
|
39
|
+
);
|
|
40
|
+
const executor = new ProbeExecutor(enabledPacks);
|
|
30
41
|
|
|
31
42
|
enrollWithHub(config, executor)
|
|
32
43
|
.then(({ agentId: id, apiKey: mintedKey }) => {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Box, Text, useInput } from 'ink';
|
|
2
|
+
import Spinner from 'ink-spinner';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { type ServiceResult, installService } from '../../cli/service.js';
|
|
5
|
+
|
|
6
|
+
interface StepServiceProps {
|
|
7
|
+
onNext: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Phase = 'prompt' | 'installing' | 'done';
|
|
11
|
+
|
|
12
|
+
export function StepService({ onNext }: StepServiceProps): JSX.Element {
|
|
13
|
+
const [phase, setPhase] = useState<Phase>('prompt');
|
|
14
|
+
const [result, setResult] = useState<ServiceResult | null>(null);
|
|
15
|
+
const isLinux = process.platform === 'linux';
|
|
16
|
+
|
|
17
|
+
useInput((input, key) => {
|
|
18
|
+
if (!isLinux) {
|
|
19
|
+
if (key.return) onNext();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (phase === 'prompt') {
|
|
24
|
+
if (input === 'y' || input === 'Y' || key.return) {
|
|
25
|
+
setPhase('installing');
|
|
26
|
+
// Run async to let the spinner render
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
const res = installService();
|
|
29
|
+
setResult(res);
|
|
30
|
+
setPhase('done');
|
|
31
|
+
}, 0);
|
|
32
|
+
} else if (input === 'n' || input === 'N') {
|
|
33
|
+
setResult(null);
|
|
34
|
+
setPhase('done');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (phase === 'done' && (key.return || input)) {
|
|
39
|
+
onNext();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!isLinux) {
|
|
44
|
+
return (
|
|
45
|
+
<Box flexDirection="column">
|
|
46
|
+
<Text bold>Systemd Service</Text>
|
|
47
|
+
<Box marginTop={1}>
|
|
48
|
+
<Text color="gray">Skipped — systemd services are only available on Linux.</Text>
|
|
49
|
+
</Box>
|
|
50
|
+
<Box marginTop={1}>
|
|
51
|
+
<Text color="gray">Press Enter to continue.</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
</Box>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (phase === 'installing') {
|
|
58
|
+
return (
|
|
59
|
+
<Box>
|
|
60
|
+
<Text color="cyan">
|
|
61
|
+
<Spinner type="dots" />
|
|
62
|
+
</Text>
|
|
63
|
+
<Text> Installing systemd service...</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (phase === 'done') {
|
|
69
|
+
if (result === null) {
|
|
70
|
+
return (
|
|
71
|
+
<Box flexDirection="column">
|
|
72
|
+
<Text bold>Systemd Service</Text>
|
|
73
|
+
<Box marginTop={1}>
|
|
74
|
+
<Text color="gray">
|
|
75
|
+
Skipped. You can set this up later with:{' '}
|
|
76
|
+
<Text color="cyan">sonde service install</Text>
|
|
77
|
+
</Text>
|
|
78
|
+
</Box>
|
|
79
|
+
<Box marginTop={1}>
|
|
80
|
+
<Text color="gray">Press any key to continue.</Text>
|
|
81
|
+
</Box>
|
|
82
|
+
</Box>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Box flexDirection="column">
|
|
88
|
+
<Text bold>Systemd Service</Text>
|
|
89
|
+
<Box marginTop={1}>
|
|
90
|
+
<Text color={result.success ? 'green' : 'red'}>
|
|
91
|
+
{result.success ? ' OK' : ' !!'} {result.message}
|
|
92
|
+
</Text>
|
|
93
|
+
</Box>
|
|
94
|
+
<Box marginTop={1}>
|
|
95
|
+
<Text color="gray">Press any key to continue.</Text>
|
|
96
|
+
</Box>
|
|
97
|
+
</Box>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Box flexDirection="column">
|
|
103
|
+
<Text bold>Systemd Service</Text>
|
|
104
|
+
<Box marginTop={1}>
|
|
105
|
+
<Text>Set up sonde-agent as a systemd service? (starts on boot)</Text>
|
|
106
|
+
</Box>
|
|
107
|
+
<Box marginTop={1}>
|
|
108
|
+
<Text color="gray">y: install service | n: skip</Text>
|
|
109
|
+
</Box>
|
|
110
|
+
</Box>
|
|
111
|
+
);
|
|
112
|
+
}
|