@sonde/agent 0.2.7 → 0.2.9
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 +1 -1
- package/.turbo/turbo-test.log +30 -43
- package/CHANGELOG.md +20 -0
- package/dist/cli/service.d.ts.map +1 -1
- package/dist/cli/service.js +20 -1
- package/dist/cli/service.js.map +1 -1
- package/dist/cli/service.test.js +30 -1
- package/dist/cli/service.test.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +4 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -16
- package/dist/index.js.map +1 -1
- 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 +17 -0
- package/dist/tui/status/StatusInfoView.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/service.test.ts +34 -8
- package/src/cli/service.ts +20 -1
- package/src/cli/update.ts +4 -0
- package/src/index.ts +12 -18
- package/src/tui/status/PackToggleView.tsx +69 -0
- package/src/tui/status/StatusApp.tsx +167 -0
- package/src/tui/status/StatusInfoView.tsx +89 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatusApp.js","sourceRoot":"","sources":["../../../src/tui/status/StatusApp.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentConfig } from '../../config.js';
|
|
2
|
+
interface StatusInfoViewProps {
|
|
3
|
+
config: AgentConfig | undefined;
|
|
4
|
+
version: string;
|
|
5
|
+
configPath: string;
|
|
6
|
+
serviceInstalled: boolean;
|
|
7
|
+
serviceStatus: string;
|
|
8
|
+
daemonPid: number | undefined;
|
|
9
|
+
enabledPackNames: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function StatusInfoView({ config, version, configPath, serviceInstalled, serviceStatus, daemonPid, enabledPackNames, }: StatusInfoViewProps): JSX.Element;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=StatusInfoView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatusInfoView.d.ts","sourceRoot":"","sources":["../../../src/tui/status/StatusInfoView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,OAAO,EACP,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,SAAS,EACT,gBAAgB,GACjB,EAAE,mBAAmB,GAAG,GAAG,CAAC,OAAO,CAmEnC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function StatusInfoView({ config, version, configPath, serviceInstalled, serviceStatus, daemonPid, enabledPackNames, }) {
|
|
4
|
+
if (!config) {
|
|
5
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: ["Not enrolled. Run ", _jsx(Text, { color: "cyan", children: "sonde enroll" }), " or", ' ', _jsx(Text, { color: "cyan", children: "sonde install" }), " to get started."] }) }));
|
|
6
|
+
}
|
|
7
|
+
const isServiceActive = serviceInstalled && serviceStatus === 'active';
|
|
8
|
+
const processStatus = isServiceActive
|
|
9
|
+
? 'service active'
|
|
10
|
+
: daemonPid
|
|
11
|
+
? `running (PID ${daemonPid})`
|
|
12
|
+
: serviceInstalled
|
|
13
|
+
? `service ${serviceStatus}`
|
|
14
|
+
: 'stopped';
|
|
15
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: "white", children: "Agent" }), _jsxs(Text, { children: [' ', "Name: ", _jsx(Text, { color: "cyan", children: config.agentName })] }), _jsxs(Text, { children: [' ', "Hub: ", _jsx(Text, { color: "cyan", children: config.hubUrl })] }), _jsxs(Text, { children: [' ', "ID: ", _jsx(Text, { color: "cyan", children: config.agentId ?? '(not assigned)' })] }), _jsxs(Text, { children: [' ', "Config: ", _jsx(Text, { color: "cyan", children: configPath })] }), _jsxs(Text, { children: [' ', "Version: ", _jsxs(Text, { color: "cyan", children: ["v", version] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: "white", children: "Process" }), _jsxs(Text, { children: [' ', "Status:", ' ', _jsx(Text, { color: isServiceActive || daemonPid ? 'green' : 'yellow', children: processStatus })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "white", children: ["Packs (", enabledPackNames.length, ")"] }), enabledPackNames.map((name) => (_jsxs(Text, { children: [' ', _jsx(Text, { color: "cyan", children: name })] }, name)))] })] }));
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=StatusInfoView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatusInfoView.js","sourceRoot":"","sources":["../../../src/tui/status/StatusInfoView.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAahC,MAAM,UAAU,cAAc,CAAC,EAC7B,MAAM,EACN,OAAO,EACP,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,SAAS,EACT,gBAAgB,GACI;IACpB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CACL,KAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,YACzB,MAAC,IAAI,qCACe,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,6BAAoB,SAAI,GAAG,EAC/D,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,8BAAqB,wBAClC,GACH,CACP,CAAC;IACJ,CAAC;IAED,MAAM,eAAe,GAAG,gBAAgB,IAAI,aAAa,KAAK,QAAQ,CAAC;IACvE,MAAM,aAAa,GAAG,eAAe;QACnC,CAAC,CAAC,gBAAgB;QAClB,CAAC,CAAC,SAAS;YACT,CAAC,CAAC,gBAAgB,SAAS,GAAG;YAC9B,CAAC,CAAC,gBAAgB;gBAChB,CAAC,CAAC,WAAW,aAAa,EAAE;gBAC5B,CAAC,CAAC,SAAS,CAAC;IAElB,OAAO,CACL,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,aACzB,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,EAAC,YAAY,EAAE,CAAC,aACzC,KAAC,IAAI,IAAC,IAAI,QAAC,KAAK,EAAC,OAAO,sBAEjB,EACP,MAAC,IAAI,eACF,IAAI,YAAO,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,YAAE,MAAM,CAAC,SAAS,GAAQ,IACnD,EACP,MAAC,IAAI,eACF,IAAI,WAAM,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,YAAE,MAAM,CAAC,MAAM,GAAQ,IAC/C,EACP,MAAC,IAAI,eACF,IAAI,UAAK,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,YAAE,MAAM,CAAC,OAAO,IAAI,gBAAgB,GAAQ,IACnE,EACP,MAAC,IAAI,eACF,IAAI,cAAS,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,YAAE,UAAU,GAAQ,IAC/C,EACP,MAAC,IAAI,eACF,IAAI,eAAU,MAAC,IAAI,IAAC,KAAK,EAAC,MAAM,kBAAG,OAAO,IAAQ,IAC9C,IACH,EAEN,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,EAAC,YAAY,EAAE,CAAC,aACzC,KAAC,IAAI,IAAC,IAAI,QAAC,KAAK,EAAC,OAAO,wBAEjB,EACP,MAAC,IAAI,eACF,IAAI,aAAS,GAAG,EACjB,KAAC,IAAI,IAAC,KAAK,EAAE,eAAe,IAAI,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,YAAG,aAAa,GAAQ,IACjF,IACH,EAEN,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,aACzB,MAAC,IAAI,IAAC,IAAI,QAAC,KAAK,EAAC,OAAO,wBACd,gBAAgB,CAAC,MAAM,SAC1B,EACN,gBAAgB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAC9B,MAAC,IAAI,eACF,IAAI,EACL,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,YAAE,IAAI,GAAQ,KAFvB,IAAI,CAGR,CACR,CAAC,IACE,IACF,CACP,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
package/src/cli/service.test.ts
CHANGED
|
@@ -19,16 +19,19 @@ vi.mock('node:os', () => ({
|
|
|
19
19
|
|
|
20
20
|
import { execFileSync } from 'node:child_process';
|
|
21
21
|
import fs from 'node:fs';
|
|
22
|
-
import {
|
|
23
|
-
generateUnitFile,
|
|
24
|
-
getServiceStatus,
|
|
25
|
-
isServiceInstalled,
|
|
26
|
-
} from './service.js';
|
|
22
|
+
import { generateUnitFile, getServiceStatus, isServiceInstalled } from './service.js';
|
|
27
23
|
|
|
28
24
|
const mockExec = vi.mocked(execFileSync);
|
|
29
25
|
const mockExists = vi.mocked(fs.existsSync);
|
|
30
26
|
|
|
31
27
|
describe('generateUnitFile', () => {
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
// biome-ignore lint/performance/noDelete: process.env coerces undefined to "undefined"
|
|
30
|
+
delete process.env.SUDO_USER;
|
|
31
|
+
// biome-ignore lint/performance/noDelete: process.env coerces undefined to "undefined"
|
|
32
|
+
delete process.env.SUDO_UID;
|
|
33
|
+
});
|
|
34
|
+
|
|
32
35
|
it('includes the current user and home directory', () => {
|
|
33
36
|
mockExec.mockReturnValueOnce('/usr/local/bin/sonde\n');
|
|
34
37
|
const unit = generateUnitFile();
|
|
@@ -36,12 +39,35 @@ describe('generateUnitFile', () => {
|
|
|
36
39
|
expect(unit).toContain('Environment=HOME=/home/testuser');
|
|
37
40
|
});
|
|
38
41
|
|
|
42
|
+
it('uses SUDO_USER when running under sudo', () => {
|
|
43
|
+
process.env.SUDO_USER = 'realuser';
|
|
44
|
+
process.env.SUDO_UID = '1000';
|
|
45
|
+
// getent passwd call, then which sonde call
|
|
46
|
+
mockExec
|
|
47
|
+
.mockReturnValueOnce('realuser:x:1000:1000::/home/realuser:/bin/bash\n')
|
|
48
|
+
.mockReturnValueOnce('/usr/local/bin/sonde\n');
|
|
49
|
+
const unit = generateUnitFile();
|
|
50
|
+
expect(unit).toContain('User=realuser');
|
|
51
|
+
expect(unit).toContain('Environment=HOME=/home/realuser');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('falls back to /home/<user> when getent fails under sudo', () => {
|
|
55
|
+
process.env.SUDO_USER = 'realuser';
|
|
56
|
+
process.env.SUDO_UID = '1000';
|
|
57
|
+
mockExec
|
|
58
|
+
.mockImplementationOnce(() => {
|
|
59
|
+
throw new Error('getent failed');
|
|
60
|
+
})
|
|
61
|
+
.mockReturnValueOnce('/usr/local/bin/sonde\n');
|
|
62
|
+
const unit = generateUnitFile();
|
|
63
|
+
expect(unit).toContain('User=realuser');
|
|
64
|
+
expect(unit).toContain('Environment=HOME=/home/realuser');
|
|
65
|
+
});
|
|
66
|
+
|
|
39
67
|
it('includes the resolved sonde binary path', () => {
|
|
40
68
|
mockExec.mockReturnValueOnce('/usr/local/bin/sonde\n');
|
|
41
69
|
const unit = generateUnitFile();
|
|
42
|
-
expect(unit).toContain(
|
|
43
|
-
'ExecStart=/usr/local/bin/sonde start --headless',
|
|
44
|
-
);
|
|
70
|
+
expect(unit).toContain('ExecStart=/usr/local/bin/sonde start --headless');
|
|
45
71
|
});
|
|
46
72
|
|
|
47
73
|
it('sets restart on failure with 5s delay', () => {
|
package/src/cli/service.ts
CHANGED
|
@@ -21,8 +21,27 @@ function resolveSondeBinary(): string {
|
|
|
21
21
|
}).trim();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function resolveInvokingUser(): { username: string; homedir: string } {
|
|
25
|
+
const sudoUser = process.env.SUDO_USER;
|
|
26
|
+
if (sudoUser) {
|
|
27
|
+
const homedir = process.env.SUDO_UID ? `/home/${sudoUser}` : os.userInfo().homedir;
|
|
28
|
+
try {
|
|
29
|
+
const passwd = execFileSync('getent', ['passwd', sudoUser], {
|
|
30
|
+
encoding: 'utf-8',
|
|
31
|
+
timeout: 5_000,
|
|
32
|
+
}).trim();
|
|
33
|
+
const fields = passwd.split(':');
|
|
34
|
+
return { username: sudoUser, homedir: fields[5] ?? homedir };
|
|
35
|
+
} catch {
|
|
36
|
+
return { username: sudoUser, homedir };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const info = os.userInfo();
|
|
40
|
+
return { username: info.username, homedir: info.homedir };
|
|
41
|
+
}
|
|
42
|
+
|
|
24
43
|
export function generateUnitFile(): string {
|
|
25
|
-
const user =
|
|
44
|
+
const user = resolveInvokingUser();
|
|
26
45
|
const sondeBin = resolveSondeBinary();
|
|
27
46
|
|
|
28
47
|
return `[Unit]
|
package/src/cli/update.ts
CHANGED
|
@@ -73,5 +73,9 @@ export function performUpdate(targetVersion: string): void {
|
|
|
73
73
|
} else {
|
|
74
74
|
console.log('Restart the agent to use the new version:');
|
|
75
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
|
+
}
|
|
76
80
|
}
|
|
77
81
|
}
|
package/src/index.ts
CHANGED
|
@@ -281,23 +281,14 @@ function cmdRestart(): void {
|
|
|
281
281
|
console.log(`Agent restarted in background (PID: ${pid}).`);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
function cmdStatus(): void {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
console.log(`Sonde Agent v${VERSION}`);
|
|
293
|
-
console.log(` Name: ${config.agentName}`);
|
|
294
|
-
console.log(` Hub: ${config.hubUrl}`);
|
|
295
|
-
console.log(` Agent ID: ${config.agentId ?? '(not yet assigned)'}`);
|
|
296
|
-
console.log(` Config: ${getConfigPath()}`);
|
|
297
|
-
|
|
298
|
-
if (isServiceInstalled()) {
|
|
299
|
-
console.log(` Service: ${getServiceStatus()}`);
|
|
300
|
-
}
|
|
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();
|
|
301
292
|
}
|
|
302
293
|
|
|
303
294
|
function handleServiceCommand(subArgs: string[]): void {
|
|
@@ -395,7 +386,10 @@ switch (command) {
|
|
|
395
386
|
cmdRestart();
|
|
396
387
|
break;
|
|
397
388
|
case 'status':
|
|
398
|
-
cmdStatus()
|
|
389
|
+
cmdStatus().catch((err: Error) => {
|
|
390
|
+
console.error(err.message);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
|
399
393
|
break;
|
|
400
394
|
case 'packs':
|
|
401
395
|
handlePacksCommand(args.slice(1));
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { packRegistry } from '@sonde/packs';
|
|
2
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { getServiceStatus, isServiceInstalled, restartService } from '../../cli/service.js';
|
|
5
|
+
import {
|
|
6
|
+
getConfigPath,
|
|
7
|
+
loadConfig,
|
|
8
|
+
readPidFile,
|
|
9
|
+
saveConfig,
|
|
10
|
+
stopRunningAgent,
|
|
11
|
+
} from '../../config.js';
|
|
12
|
+
import { createSystemChecker, scanForSoftware } from '../../system/scanner.js';
|
|
13
|
+
import { VERSION } from '../../version.js';
|
|
14
|
+
import type { PackRow } from './PackToggleView.js';
|
|
15
|
+
import { PackToggleView } from './PackToggleView.js';
|
|
16
|
+
import { StatusInfoView } from './StatusInfoView.js';
|
|
17
|
+
|
|
18
|
+
type View = 'status' | 'packs';
|
|
19
|
+
|
|
20
|
+
interface StatusAppProps {
|
|
21
|
+
respawnAgent: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function StatusApp({ respawnAgent }: StatusAppProps): JSX.Element {
|
|
25
|
+
const { exit } = useApp();
|
|
26
|
+
const [view, setView] = useState<View>('status');
|
|
27
|
+
const [message, setMessage] = useState<string | undefined>();
|
|
28
|
+
|
|
29
|
+
const initial = useMemo(() => {
|
|
30
|
+
const config = loadConfig();
|
|
31
|
+
const configPath = getConfigPath();
|
|
32
|
+
const serviceInstalled = isServiceInstalled();
|
|
33
|
+
const serviceStatus = serviceInstalled ? getServiceStatus() : 'not-installed';
|
|
34
|
+
const daemonPid = readPidFile();
|
|
35
|
+
|
|
36
|
+
const manifests = [...packRegistry.values()].map((p) => p.manifest);
|
|
37
|
+
const disabledSet = new Set(config?.disabledPacks ?? []);
|
|
38
|
+
const checker = createSystemChecker();
|
|
39
|
+
const scanResults = scanForSoftware(manifests, checker);
|
|
40
|
+
const scanMap = new Map(scanResults.map((r) => [r.packName, r.detected]));
|
|
41
|
+
|
|
42
|
+
const packRows: PackRow[] = manifests.map((m) => ({
|
|
43
|
+
name: m.name,
|
|
44
|
+
description: m.description,
|
|
45
|
+
probeCount: m.probes.length,
|
|
46
|
+
detected: scanMap.get(m.name) ?? false,
|
|
47
|
+
enabled: !disabledSet.has(m.name),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const enabledPackNames = packRows.filter((r) => r.enabled).map((r) => r.name);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
config,
|
|
54
|
+
configPath,
|
|
55
|
+
serviceInstalled,
|
|
56
|
+
serviceStatus,
|
|
57
|
+
daemonPid,
|
|
58
|
+
packRows,
|
|
59
|
+
enabledPackNames,
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const [packRows, setPackRows] = useState(initial.packRows);
|
|
64
|
+
const [enabledPackNames, setEnabledPackNames] = useState(initial.enabledPackNames);
|
|
65
|
+
|
|
66
|
+
useInput((input) => {
|
|
67
|
+
if (view !== 'status') return;
|
|
68
|
+
if (input === 'p' && initial.config) {
|
|
69
|
+
setMessage(undefined);
|
|
70
|
+
setView('packs');
|
|
71
|
+
} else if (input === 'q') {
|
|
72
|
+
exit();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function handlePackConfirm(enabledNames: string[]): void {
|
|
77
|
+
const config = loadConfig();
|
|
78
|
+
if (!config) return;
|
|
79
|
+
|
|
80
|
+
const allNames = packRows.map((r) => r.name);
|
|
81
|
+
const enabledSet = new Set(enabledNames);
|
|
82
|
+
const disabledPacks = allNames.filter((n) => !enabledSet.has(n));
|
|
83
|
+
|
|
84
|
+
config.disabledPacks = disabledPacks;
|
|
85
|
+
saveConfig(config);
|
|
86
|
+
|
|
87
|
+
let resultMessage: string;
|
|
88
|
+
const svcInstalled = isServiceInstalled();
|
|
89
|
+
const svcStatus = svcInstalled ? getServiceStatus() : '';
|
|
90
|
+
|
|
91
|
+
if (svcInstalled && svcStatus === 'active') {
|
|
92
|
+
const result = restartService();
|
|
93
|
+
resultMessage = result.success
|
|
94
|
+
? 'Packs saved. Service restarted.'
|
|
95
|
+
: `Packs saved. ${result.message}`;
|
|
96
|
+
} else if (readPidFile() !== undefined) {
|
|
97
|
+
stopRunningAgent();
|
|
98
|
+
respawnAgent();
|
|
99
|
+
resultMessage = 'Packs saved. Agent restarted.';
|
|
100
|
+
} else {
|
|
101
|
+
resultMessage = 'Packs saved. Changes take effect on next start.';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const updatedRows = packRows.map((r) => ({
|
|
105
|
+
...r,
|
|
106
|
+
enabled: enabledSet.has(r.name),
|
|
107
|
+
}));
|
|
108
|
+
setPackRows(updatedRows);
|
|
109
|
+
setEnabledPackNames(enabledNames);
|
|
110
|
+
setMessage(resultMessage);
|
|
111
|
+
setView('status');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handlePackBack(): void {
|
|
115
|
+
setView('status');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const enrolled = initial.config !== undefined;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1}>
|
|
122
|
+
<Box marginBottom={1}>
|
|
123
|
+
<Text bold color="cyan">
|
|
124
|
+
Sonde Agent Status
|
|
125
|
+
</Text>
|
|
126
|
+
</Box>
|
|
127
|
+
|
|
128
|
+
{message && (
|
|
129
|
+
<Box marginBottom={1}>
|
|
130
|
+
<Text color="green">{message}</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{view === 'status' && (
|
|
135
|
+
<StatusInfoView
|
|
136
|
+
config={initial.config}
|
|
137
|
+
version={VERSION}
|
|
138
|
+
configPath={initial.configPath}
|
|
139
|
+
serviceInstalled={initial.serviceInstalled}
|
|
140
|
+
serviceStatus={initial.serviceStatus}
|
|
141
|
+
daemonPid={initial.daemonPid}
|
|
142
|
+
enabledPackNames={enabledPackNames}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{view === 'packs' && (
|
|
147
|
+
<PackToggleView
|
|
148
|
+
initialRows={packRows}
|
|
149
|
+
onConfirm={handlePackConfirm}
|
|
150
|
+
onBack={handlePackBack}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
<Box marginTop={1}>
|
|
155
|
+
{enrolled && (
|
|
156
|
+
<>
|
|
157
|
+
<Text color={view === 'packs' ? 'cyan' : 'gray'} bold={view === 'packs'}>
|
|
158
|
+
p:packs
|
|
159
|
+
</Text>
|
|
160
|
+
<Text> </Text>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
<Text color="gray">q:quit</Text>
|
|
164
|
+
</Box>
|
|
165
|
+
</Box>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import type { AgentConfig } from '../../config.js';
|
|
3
|
+
|
|
4
|
+
interface StatusInfoViewProps {
|
|
5
|
+
config: AgentConfig | undefined;
|
|
6
|
+
version: string;
|
|
7
|
+
configPath: string;
|
|
8
|
+
serviceInstalled: boolean;
|
|
9
|
+
serviceStatus: string;
|
|
10
|
+
daemonPid: number | undefined;
|
|
11
|
+
enabledPackNames: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function StatusInfoView({
|
|
15
|
+
config,
|
|
16
|
+
version,
|
|
17
|
+
configPath,
|
|
18
|
+
serviceInstalled,
|
|
19
|
+
serviceStatus,
|
|
20
|
+
daemonPid,
|
|
21
|
+
enabledPackNames,
|
|
22
|
+
}: StatusInfoViewProps): JSX.Element {
|
|
23
|
+
if (!config) {
|
|
24
|
+
return (
|
|
25
|
+
<Box flexDirection="column">
|
|
26
|
+
<Text>
|
|
27
|
+
Not enrolled. Run <Text color="cyan">sonde enroll</Text> or{' '}
|
|
28
|
+
<Text color="cyan">sonde install</Text> to get started.
|
|
29
|
+
</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const isServiceActive = serviceInstalled && serviceStatus === 'active';
|
|
35
|
+
const processStatus = isServiceActive
|
|
36
|
+
? 'service active'
|
|
37
|
+
: daemonPid
|
|
38
|
+
? `running (PID ${daemonPid})`
|
|
39
|
+
: serviceInstalled
|
|
40
|
+
? `service ${serviceStatus}`
|
|
41
|
+
: 'stopped';
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Box flexDirection="column">
|
|
45
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
46
|
+
<Text bold color="white">
|
|
47
|
+
Agent
|
|
48
|
+
</Text>
|
|
49
|
+
<Text>
|
|
50
|
+
{' '}Name: <Text color="cyan">{config.agentName}</Text>
|
|
51
|
+
</Text>
|
|
52
|
+
<Text>
|
|
53
|
+
{' '}Hub: <Text color="cyan">{config.hubUrl}</Text>
|
|
54
|
+
</Text>
|
|
55
|
+
<Text>
|
|
56
|
+
{' '}ID: <Text color="cyan">{config.agentId ?? '(not assigned)'}</Text>
|
|
57
|
+
</Text>
|
|
58
|
+
<Text>
|
|
59
|
+
{' '}Config: <Text color="cyan">{configPath}</Text>
|
|
60
|
+
</Text>
|
|
61
|
+
<Text>
|
|
62
|
+
{' '}Version: <Text color="cyan">v{version}</Text>
|
|
63
|
+
</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
|
|
66
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
67
|
+
<Text bold color="white">
|
|
68
|
+
Process
|
|
69
|
+
</Text>
|
|
70
|
+
<Text>
|
|
71
|
+
{' '}Status:{' '}
|
|
72
|
+
<Text color={isServiceActive || daemonPid ? 'green' : 'yellow'}>{processStatus}</Text>
|
|
73
|
+
</Text>
|
|
74
|
+
</Box>
|
|
75
|
+
|
|
76
|
+
<Box flexDirection="column">
|
|
77
|
+
<Text bold color="white">
|
|
78
|
+
Packs ({enabledPackNames.length})
|
|
79
|
+
</Text>
|
|
80
|
+
{enabledPackNames.map((name) => (
|
|
81
|
+
<Text key={name}>
|
|
82
|
+
{' '}
|
|
83
|
+
<Text color="cyan">{name}</Text>
|
|
84
|
+
</Text>
|
|
85
|
+
))}
|
|
86
|
+
</Box>
|
|
87
|
+
</Box>
|
|
88
|
+
);
|
|
89
|
+
}
|