@sonde/agent 0.2.6 → 0.2.8
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 +65 -23
- package/CHANGELOG.md +20 -0
- 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 +11 -8
- package/dist/cli/update.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -12
- 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/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/status/PackToggleView.d.ts +15 -0
- package/dist/tui/status/PackToggleView.d.ts.map +1 -0
- package/dist/tui/status/PackToggleView.js +30 -0
- package/dist/tui/status/PackToggleView.js.map +1 -0
- package/dist/tui/status/StatusApp.d.ts +6 -0
- package/dist/tui/status/StatusApp.d.ts.map +1 -0
- package/dist/tui/status/StatusApp.js +98 -0
- package/dist/tui/status/StatusApp.js.map +1 -0
- package/dist/tui/status/StatusInfoView.d.ts +13 -0
- package/dist/tui/status/StatusInfoView.d.ts.map +1 -0
- package/dist/tui/status/StatusInfoView.js +14 -0
- package/dist/tui/status/StatusInfoView.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/service.test.ts +124 -0
- package/src/cli/service.ts +178 -0
- package/src/cli/update.ts +11 -8
- package/src/index.ts +71 -13
- package/src/tui/installer/InstallerApp.tsx +6 -2
- package/src/tui/installer/StepService.tsx +112 -0
- package/src/tui/status/PackToggleView.tsx +69 -0
- package/src/tui/status/StatusApp.tsx +167 -0
- package/src/tui/status/StatusInfoView.tsx +88 -0
- 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,15 @@ 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
|
-
|
|
76
|
-
|
|
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');
|
|
76
|
+
if (process.platform === 'linux') {
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log('Tip: Run "sonde service install" to start on boot.');
|
|
79
|
+
}
|
|
77
80
|
}
|
|
78
81
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,14 @@ import { spawn } from 'node:child_process';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import { packRegistry } from '@sonde/packs';
|
|
6
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';
|
|
7
15
|
import { checkForUpdate, performUpdate } from './cli/update.js';
|
|
8
16
|
import {
|
|
9
17
|
type AgentConfig,
|
|
@@ -40,6 +48,7 @@ function printUsage(): void {
|
|
|
40
48
|
console.log(' restart Restart the agent in background');
|
|
41
49
|
console.log(' status Show agent status');
|
|
42
50
|
console.log(' packs Manage packs (list, scan, install, uninstall)');
|
|
51
|
+
console.log(' service Manage systemd service (install, uninstall, status)');
|
|
43
52
|
console.log(' update Check for and install agent updates');
|
|
44
53
|
console.log(' mcp-bridge stdio MCP bridge (for Claude Code integration)');
|
|
45
54
|
console.log('');
|
|
@@ -247,6 +256,12 @@ async function cmdManager(): Promise<void> {
|
|
|
247
256
|
}
|
|
248
257
|
|
|
249
258
|
function cmdStop(): void {
|
|
259
|
+
if (isServiceInstalled() && getServiceStatus() === 'active') {
|
|
260
|
+
const result = stopService();
|
|
261
|
+
console.log(result.message);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
250
265
|
if (stopRunningAgent()) {
|
|
251
266
|
console.log('Agent stopped.');
|
|
252
267
|
} else {
|
|
@@ -255,24 +270,61 @@ function cmdStop(): void {
|
|
|
255
270
|
}
|
|
256
271
|
|
|
257
272
|
function cmdRestart(): void {
|
|
273
|
+
if (isServiceInstalled() && getServiceStatus() === 'active') {
|
|
274
|
+
const result = restartService();
|
|
275
|
+
console.log(result.message);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
258
279
|
stopRunningAgent();
|
|
259
280
|
const pid = spawnBackgroundAgent();
|
|
260
281
|
console.log(`Agent restarted in background (PID: ${pid}).`);
|
|
261
282
|
}
|
|
262
283
|
|
|
263
|
-
function cmdStatus(): void {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
284
|
+
async function cmdStatus(): Promise<void> {
|
|
285
|
+
const { render } = await import('ink');
|
|
286
|
+
const { createElement } = await import('react');
|
|
287
|
+
const { StatusApp } = await import('./tui/status/StatusApp.js');
|
|
288
|
+
const { waitUntilExit } = render(
|
|
289
|
+
createElement(StatusApp, { respawnAgent: spawnBackgroundAgent }),
|
|
290
|
+
);
|
|
291
|
+
await waitUntilExit();
|
|
292
|
+
}
|
|
270
293
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
294
|
+
function handleServiceCommand(subArgs: string[]): void {
|
|
295
|
+
const sub = subArgs[0];
|
|
296
|
+
|
|
297
|
+
switch (sub) {
|
|
298
|
+
case 'install': {
|
|
299
|
+
const result = installService();
|
|
300
|
+
console.log(result.message);
|
|
301
|
+
if (!result.success) process.exit(1);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
case 'uninstall': {
|
|
305
|
+
const result = uninstallService();
|
|
306
|
+
console.log(result.message);
|
|
307
|
+
if (!result.success) process.exit(1);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case 'status': {
|
|
311
|
+
const status = getServiceStatus();
|
|
312
|
+
console.log(`sonde-agent service: ${status}`);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
default:
|
|
316
|
+
console.log('Usage: sonde service <command>');
|
|
317
|
+
console.log('');
|
|
318
|
+
console.log('Commands:');
|
|
319
|
+
console.log(' install Install systemd service (starts on boot)');
|
|
320
|
+
console.log(' uninstall Remove systemd service');
|
|
321
|
+
console.log(' status Show service status');
|
|
322
|
+
if (sub) {
|
|
323
|
+
console.error(`\nUnknown subcommand: ${sub}`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
276
328
|
}
|
|
277
329
|
|
|
278
330
|
async function cmdInstall(): Promise<void> {
|
|
@@ -334,11 +386,17 @@ switch (command) {
|
|
|
334
386
|
cmdRestart();
|
|
335
387
|
break;
|
|
336
388
|
case 'status':
|
|
337
|
-
cmdStatus()
|
|
389
|
+
cmdStatus().catch((err: Error) => {
|
|
390
|
+
console.error(err.message);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
|
338
393
|
break;
|
|
339
394
|
case 'packs':
|
|
340
395
|
handlePacksCommand(args.slice(1));
|
|
341
396
|
break;
|
|
397
|
+
case 'service':
|
|
398
|
+
handleServiceCommand(args.slice(1));
|
|
399
|
+
break;
|
|
342
400
|
case 'update':
|
|
343
401
|
cmdUpdate().catch((err: Error) => {
|
|
344
402
|
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
|
);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Box, Text, useInput } from 'ink';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface PackRow {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
probeCount: number;
|
|
8
|
+
detected: boolean;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PackToggleViewProps {
|
|
13
|
+
initialRows: PackRow[];
|
|
14
|
+
onConfirm: (enabledNames: string[]) => void;
|
|
15
|
+
onBack: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function PackToggleView({
|
|
19
|
+
initialRows,
|
|
20
|
+
onConfirm,
|
|
21
|
+
onBack,
|
|
22
|
+
}: PackToggleViewProps): JSX.Element {
|
|
23
|
+
const [rows, setRows] = useState<PackRow[]>(initialRows);
|
|
24
|
+
const [cursor, setCursor] = useState(0);
|
|
25
|
+
|
|
26
|
+
useInput((input, key) => {
|
|
27
|
+
if (key.upArrow) {
|
|
28
|
+
setCursor((prev) => (prev > 0 ? prev - 1 : rows.length - 1));
|
|
29
|
+
} else if (key.downArrow) {
|
|
30
|
+
setCursor((prev) => (prev < rows.length - 1 ? prev + 1 : 0));
|
|
31
|
+
} else if (input === ' ') {
|
|
32
|
+
setRows((prev) =>
|
|
33
|
+
prev.map((row, i) => (i === cursor ? { ...row, enabled: !row.enabled } : row)),
|
|
34
|
+
);
|
|
35
|
+
} else if (key.return) {
|
|
36
|
+
onConfirm(rows.filter((r) => r.enabled).map((r) => r.name));
|
|
37
|
+
} else if (input === 'b') {
|
|
38
|
+
onBack();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Box flexDirection="column">
|
|
44
|
+
<Text color="gray">Toggle packs on/off. Press Enter to save, b to go back.</Text>
|
|
45
|
+
<Box marginTop={1} flexDirection="column">
|
|
46
|
+
{rows.map((row, i) => {
|
|
47
|
+
const isCursor = i === cursor;
|
|
48
|
+
const checkbox = row.enabled ? '[x]' : '[ ]';
|
|
49
|
+
return (
|
|
50
|
+
<Box key={row.name}>
|
|
51
|
+
<Text color={isCursor ? 'cyan' : 'white'} bold={isCursor}>
|
|
52
|
+
{isCursor ? '> ' : ' '}
|
|
53
|
+
{checkbox} {row.name}
|
|
54
|
+
</Text>
|
|
55
|
+
<Text color="gray">
|
|
56
|
+
{' '}
|
|
57
|
+
({row.probeCount} probes) — {row.description}
|
|
58
|
+
</Text>
|
|
59
|
+
{row.detected && <Text color="green"> [detected]</Text>}
|
|
60
|
+
</Box>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
</Box>
|
|
64
|
+
<Box marginTop={1}>
|
|
65
|
+
<Text color="gray">Up/Down: navigate | Space: toggle | Enter: save | b: back</Text>
|
|
66
|
+
</Box>
|
|
67
|
+
</Box>
|
|
68
|
+
);
|
|
69
|
+
}
|