@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.
Files changed (54) hide show
  1. package/.turbo/turbo-build.log +6 -4
  2. package/.turbo/turbo-test.log +96 -23
  3. package/CHANGELOG.md +20 -0
  4. package/dist/cli/packs.d.ts +6 -0
  5. package/dist/cli/packs.d.ts.map +1 -1
  6. package/dist/cli/packs.js +42 -8
  7. package/dist/cli/packs.js.map +1 -1
  8. package/dist/cli/packs.test.js +56 -1
  9. package/dist/cli/packs.test.js.map +1 -1
  10. package/dist/cli/service.d.ts +12 -0
  11. package/dist/cli/service.d.ts.map +1 -0
  12. package/dist/cli/service.js +164 -0
  13. package/dist/cli/service.js.map +1 -0
  14. package/dist/cli/service.test.d.ts +2 -0
  15. package/dist/cli/service.test.d.ts.map +1 -0
  16. package/dist/cli/service.test.js +99 -0
  17. package/dist/cli/service.test.js.map +1 -0
  18. package/dist/cli/update.d.ts.map +1 -1
  19. package/dist/cli/update.js +7 -8
  20. package/dist/cli/update.js.map +1 -1
  21. package/dist/config.d.ts +13 -0
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +55 -0
  24. package/dist/config.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +126 -5
  27. package/dist/index.js.map +1 -1
  28. package/dist/tui/installer/InstallerApp.d.ts.map +1 -1
  29. package/dist/tui/installer/InstallerApp.js +3 -1
  30. package/dist/tui/installer/InstallerApp.js.map +1 -1
  31. package/dist/tui/installer/StepComplete.d.ts.map +1 -1
  32. package/dist/tui/installer/StepComplete.js +10 -1
  33. package/dist/tui/installer/StepComplete.js.map +1 -1
  34. package/dist/tui/installer/StepService.d.ts +6 -0
  35. package/dist/tui/installer/StepService.d.ts.map +1 -0
  36. package/dist/tui/installer/StepService.js +49 -0
  37. package/dist/tui/installer/StepService.js.map +1 -0
  38. package/dist/tui/manager/ManagerApp.d.ts +2 -1
  39. package/dist/tui/manager/ManagerApp.d.ts.map +1 -1
  40. package/dist/tui/manager/ManagerApp.js +3 -2
  41. package/dist/tui/manager/ManagerApp.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/cli/packs.test.ts +65 -0
  44. package/src/cli/packs.ts +56 -10
  45. package/src/cli/service.test.ts +124 -0
  46. package/src/cli/service.ts +178 -0
  47. package/src/cli/update.ts +7 -8
  48. package/src/config.ts +57 -0
  49. package/src/index.ts +156 -5
  50. package/src/tui/installer/InstallerApp.tsx +6 -2
  51. package/src/tui/installer/StepComplete.tsx +12 -1
  52. package/src/tui/installer/StepService.tsx +112 -0
  53. package/src/tui/manager/ManagerApp.tsx +4 -2
  54. 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
- // Best-effort systemd restart
70
- try {
71
- execFileSync('systemctl', ['restart', 'sonde-agent'], {
72
- timeout: 10_000,
73
- });
74
- console.log('Restarted sonde-agent systemd service');
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 { handlePacksCommand } from './cli/packs.js';
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 { type AgentConfig, getConfigPath, loadConfig, saveConfig } from './config.js';
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 executor = new ProbeExecutor(undefined, undefined, buildPatterns(config.scrubPatterns));
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
- const { waitUntilExit } = render(createElement(ManagerApp, { createRuntime }));
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('Sonde Agent Status');
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('complete')}
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 executor = new ProbeExecutor();
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
+ }