@sonde/packs 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/docker/index.d.ts +3 -0
  2. package/dist/docker/index.d.ts.map +1 -0
  3. package/dist/docker/index.js +15 -0
  4. package/dist/docker/index.js.map +1 -0
  5. package/dist/docker/manifest.d.ts +3 -0
  6. package/dist/docker/manifest.d.ts.map +1 -0
  7. package/dist/docker/manifest.js +54 -0
  8. package/dist/docker/manifest.js.map +1 -0
  9. package/dist/docker/probes/containers-list.d.ts +19 -0
  10. package/dist/docker/probes/containers-list.d.ts.map +1 -0
  11. package/dist/docker/probes/containers-list.js +25 -0
  12. package/dist/docker/probes/containers-list.js.map +1 -0
  13. package/dist/docker/probes/containers-list.test.d.ts +2 -0
  14. package/dist/docker/probes/containers-list.test.d.ts.map +1 -0
  15. package/dist/docker/probes/containers-list.test.js +43 -0
  16. package/dist/docker/probes/containers-list.test.js.map +1 -0
  17. package/dist/docker/probes/daemon-info.d.ts +16 -0
  18. package/dist/docker/probes/daemon-info.d.ts.map +1 -0
  19. package/dist/docker/probes/daemon-info.js +20 -0
  20. package/dist/docker/probes/daemon-info.js.map +1 -0
  21. package/dist/docker/probes/daemon-info.test.d.ts +2 -0
  22. package/dist/docker/probes/daemon-info.test.d.ts.map +1 -0
  23. package/dist/docker/probes/daemon-info.test.js +42 -0
  24. package/dist/docker/probes/daemon-info.test.js.map +1 -0
  25. package/dist/docker/probes/images-list.d.ts +17 -0
  26. package/dist/docker/probes/images-list.d.ts.map +1 -0
  27. package/dist/docker/probes/images-list.js +23 -0
  28. package/dist/docker/probes/images-list.js.map +1 -0
  29. package/dist/docker/probes/images-list.test.d.ts +2 -0
  30. package/dist/docker/probes/images-list.test.d.ts.map +1 -0
  31. package/dist/docker/probes/images-list.test.js +41 -0
  32. package/dist/docker/probes/images-list.test.js.map +1 -0
  33. package/dist/docker/probes/logs-tail.d.ts +12 -0
  34. package/dist/docker/probes/logs-tail.d.ts.map +1 -0
  35. package/dist/docker/probes/logs-tail.js +22 -0
  36. package/dist/docker/probes/logs-tail.js.map +1 -0
  37. package/dist/docker/probes/logs-tail.test.d.ts +2 -0
  38. package/dist/docker/probes/logs-tail.test.d.ts.map +1 -0
  39. package/dist/docker/probes/logs-tail.test.js +42 -0
  40. package/dist/docker/probes/logs-tail.test.js.map +1 -0
  41. package/dist/index.d.ts +9 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +15 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/system/index.d.ts +3 -0
  46. package/dist/system/index.d.ts.map +1 -0
  47. package/dist/system/index.js +13 -0
  48. package/dist/system/index.js.map +1 -0
  49. package/dist/system/manifest.d.ts +3 -0
  50. package/dist/system/manifest.d.ts.map +1 -0
  51. package/dist/system/manifest.js +39 -0
  52. package/dist/system/manifest.js.map +1 -0
  53. package/dist/system/probes/cpu-usage.d.ts +14 -0
  54. package/dist/system/probes/cpu-usage.d.ts.map +1 -0
  55. package/dist/system/probes/cpu-usage.js +20 -0
  56. package/dist/system/probes/cpu-usage.js.map +1 -0
  57. package/dist/system/probes/cpu-usage.test.d.ts +2 -0
  58. package/dist/system/probes/cpu-usage.test.d.ts.map +1 -0
  59. package/dist/system/probes/cpu-usage.test.js +45 -0
  60. package/dist/system/probes/cpu-usage.test.js.map +1 -0
  61. package/dist/system/probes/disk-usage.d.ts +19 -0
  62. package/dist/system/probes/disk-usage.d.ts.map +1 -0
  63. package/dist/system/probes/disk-usage.js +34 -0
  64. package/dist/system/probes/disk-usage.js.map +1 -0
  65. package/dist/system/probes/disk-usage.test.d.ts +2 -0
  66. package/dist/system/probes/disk-usage.test.d.ts.map +1 -0
  67. package/dist/system/probes/disk-usage.test.js +62 -0
  68. package/dist/system/probes/disk-usage.test.js.map +1 -0
  69. package/dist/system/probes/memory-usage.d.ts +19 -0
  70. package/dist/system/probes/memory-usage.d.ts.map +1 -0
  71. package/dist/system/probes/memory-usage.js +46 -0
  72. package/dist/system/probes/memory-usage.js.map +1 -0
  73. package/dist/system/probes/memory-usage.test.d.ts +2 -0
  74. package/dist/system/probes/memory-usage.test.d.ts.map +1 -0
  75. package/dist/system/probes/memory-usage.test.js +50 -0
  76. package/dist/system/probes/memory-usage.test.js.map +1 -0
  77. package/dist/systemd/index.d.ts +3 -0
  78. package/dist/systemd/index.d.ts.map +1 -0
  79. package/dist/systemd/index.js +13 -0
  80. package/dist/systemd/index.js.map +1 -0
  81. package/dist/systemd/manifest.d.ts +3 -0
  82. package/dist/systemd/manifest.d.ts.map +1 -0
  83. package/dist/systemd/manifest.js +51 -0
  84. package/dist/systemd/manifest.js.map +1 -0
  85. package/dist/systemd/probes/journal-query.d.ts +19 -0
  86. package/dist/systemd/probes/journal-query.d.ts.map +1 -0
  87. package/dist/systemd/probes/journal-query.js +37 -0
  88. package/dist/systemd/probes/journal-query.js.map +1 -0
  89. package/dist/systemd/probes/journal-query.test.d.ts +2 -0
  90. package/dist/systemd/probes/journal-query.test.d.ts.map +1 -0
  91. package/dist/systemd/probes/journal-query.test.js +50 -0
  92. package/dist/systemd/probes/journal-query.test.js.map +1 -0
  93. package/dist/systemd/probes/service-status.d.ts +16 -0
  94. package/dist/systemd/probes/service-status.d.ts.map +1 -0
  95. package/dist/systemd/probes/service-status.js +29 -0
  96. package/dist/systemd/probes/service-status.js.map +1 -0
  97. package/dist/systemd/probes/service-status.test.d.ts +2 -0
  98. package/dist/systemd/probes/service-status.test.d.ts.map +1 -0
  99. package/dist/systemd/probes/service-status.test.js +48 -0
  100. package/dist/systemd/probes/service-status.test.js.map +1 -0
  101. package/dist/systemd/probes/services-list.d.ts +18 -0
  102. package/dist/systemd/probes/services-list.d.ts.map +1 -0
  103. package/dist/systemd/probes/services-list.js +29 -0
  104. package/dist/systemd/probes/services-list.js.map +1 -0
  105. package/dist/systemd/probes/services-list.test.d.ts +2 -0
  106. package/dist/systemd/probes/services-list.test.d.ts.map +1 -0
  107. package/dist/systemd/probes/services-list.test.js +61 -0
  108. package/dist/systemd/probes/services-list.test.js.map +1 -0
  109. package/dist/types.d.ts +11 -0
  110. package/dist/types.d.ts.map +1 -0
  111. package/dist/types.js +2 -0
  112. package/dist/types.js.map +1 -0
  113. package/dist/validation.d.ts +16 -0
  114. package/dist/validation.d.ts.map +1 -0
  115. package/dist/validation.js +44 -0
  116. package/dist/validation.js.map +1 -0
  117. package/dist/validation.test.d.ts +2 -0
  118. package/dist/validation.test.d.ts.map +1 -0
  119. package/dist/validation.test.js +76 -0
  120. package/dist/validation.test.js.map +1 -0
  121. package/package.json +26 -0
  122. package/src/docker/index.ts +16 -0
  123. package/src/docker/manifest.ts +55 -0
  124. package/src/docker/probes/containers-list.test.ts +50 -0
  125. package/src/docker/probes/containers-list.ts +42 -0
  126. package/src/docker/probes/daemon-info.test.ts +48 -0
  127. package/src/docker/probes/daemon-info.ts +33 -0
  128. package/src/docker/probes/images-list.test.ts +48 -0
  129. package/src/docker/probes/images-list.ts +39 -0
  130. package/src/docker/probes/logs-tail.test.ts +51 -0
  131. package/src/docker/probes/logs-tail.ts +32 -0
  132. package/src/index.ts +18 -0
  133. package/src/system/index.ts +14 -0
  134. package/src/system/manifest.ts +40 -0
  135. package/src/system/probes/cpu-usage.test.ts +55 -0
  136. package/src/system/probes/cpu-usage.ts +30 -0
  137. package/src/system/probes/disk-usage.test.ts +73 -0
  138. package/src/system/probes/disk-usage.ts +53 -0
  139. package/src/system/probes/memory-usage.test.ts +58 -0
  140. package/src/system/probes/memory-usage.ts +63 -0
  141. package/src/systemd/index.ts +14 -0
  142. package/src/systemd/manifest.ts +52 -0
  143. package/src/systemd/probes/journal-query.test.ts +64 -0
  144. package/src/systemd/probes/journal-query.ts +56 -0
  145. package/src/systemd/probes/service-status.test.ts +59 -0
  146. package/src/systemd/probes/service-status.ts +42 -0
  147. package/src/systemd/probes/services-list.test.ts +68 -0
  148. package/src/systemd/probes/services-list.ts +45 -0
  149. package/src/types.ts +16 -0
  150. package/src/validation.test.ts +86 -0
  151. package/src/validation.ts +52 -0
  152. package/tsconfig.json +11 -0
  153. package/vitest.config.ts +8 -0
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { ContainersListResult } from './containers-list.js';
4
+ import { containersList, parseContainersList } from './containers-list.js';
5
+
6
+ const SAMPLE_OUTPUT = `{"Command":"\\"docker-entrypoint.s…\\"","CreatedAt":"2024-01-15 10:30:00 +0000 UTC","ID":"abc123def456","Image":"nginx:latest","Labels":"","LocalVolumes":"0","Mounts":"","Names":"web-server","Networks":"bridge","Ports":"0.0.0.0:80-\\u003e80/tcp","RunningFor":"2 hours ago","Size":"0B","State":"running","Status":"Up 2 hours"}
7
+ {"Command":"\\"postgres\\"","CreatedAt":"2024-01-15 09:00:00 +0000 UTC","ID":"789xyz000111","Image":"postgres:16","Labels":"","LocalVolumes":"1","Mounts":"pgdata","Names":"db","Networks":"bridge","Ports":"5432/tcp","RunningFor":"3 hours ago","Size":"0B","State":"exited","Status":"Exited (0) 1 hour ago"}`;
8
+
9
+ describe('parseContainersList', () => {
10
+ it('parses docker ps JSON output into structured data', () => {
11
+ const result = parseContainersList(SAMPLE_OUTPUT);
12
+
13
+ expect(result.containers).toHaveLength(2);
14
+ expect(result.containers[0]).toEqual({
15
+ id: 'abc123def456',
16
+ name: 'web-server',
17
+ image: 'nginx:latest',
18
+ state: 'running',
19
+ status: 'Up 2 hours',
20
+ ports: '0.0.0.0:80->80/tcp',
21
+ });
22
+ expect(result.containers[1]).toEqual({
23
+ id: '789xyz000111',
24
+ name: 'db',
25
+ image: 'postgres:16',
26
+ state: 'exited',
27
+ status: 'Exited (0) 1 hour ago',
28
+ ports: '5432/tcp',
29
+ });
30
+ });
31
+
32
+ it('returns empty array for empty output', () => {
33
+ const result = parseContainersList('');
34
+ expect(result.containers).toHaveLength(0);
35
+ });
36
+ });
37
+
38
+ describe('containersList handler', () => {
39
+ it('calls docker ps with correct args and returns parsed result', async () => {
40
+ const mockExec: ExecFn = async (cmd, args) => {
41
+ expect(cmd).toBe('docker');
42
+ expect(args).toEqual(['ps', '-a', '--format', 'json']);
43
+ return SAMPLE_OUTPUT;
44
+ };
45
+
46
+ const result = (await containersList(undefined, mockExec)) as ContainersListResult;
47
+ expect(result.containers).toHaveLength(2);
48
+ expect(result.containers[0]?.name).toBe('web-server');
49
+ });
50
+ });
@@ -0,0 +1,42 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface ContainerInfo {
4
+ id: string;
5
+ name: string;
6
+ image: string;
7
+ state: string;
8
+ status: string;
9
+ ports: string;
10
+ }
11
+
12
+ export interface ContainersListResult {
13
+ containers: ContainerInfo[];
14
+ }
15
+
16
+ /**
17
+ * Runs `docker ps -a --format json` and parses each JSON line.
18
+ * The --format json flag outputs one JSON object per line (Docker 20.10+).
19
+ */
20
+ export const containersList: ProbeHandler = async (_params, exec) => {
21
+ const stdout = await exec('docker', ['ps', '-a', '--format', 'json']);
22
+ return parseContainersList(stdout);
23
+ };
24
+
25
+ export function parseContainersList(stdout: string): ContainersListResult {
26
+ const lines = stdout.trim().split('\n').filter(Boolean);
27
+ const containers: ContainerInfo[] = [];
28
+
29
+ for (const line of lines) {
30
+ const raw = JSON.parse(line);
31
+ containers.push({
32
+ id: raw.ID ?? '',
33
+ name: raw.Names ?? '',
34
+ image: raw.Image ?? '',
35
+ state: raw.State ?? '',
36
+ status: raw.Status ?? '',
37
+ ports: raw.Ports ?? '',
38
+ });
39
+ }
40
+
41
+ return { containers };
42
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { DaemonInfoResult } from './daemon-info.js';
4
+ import { daemonInfo, parseDaemonInfo } from './daemon-info.js';
5
+
6
+ const SAMPLE_OUTPUT = JSON.stringify({
7
+ ServerVersion: '24.0.7',
8
+ Containers: 5,
9
+ ContainersRunning: 3,
10
+ ContainersPaused: 0,
11
+ ContainersStopped: 2,
12
+ Images: 12,
13
+ Driver: 'overlay2',
14
+ OperatingSystem: 'Ubuntu 22.04.3 LTS',
15
+ MemTotal: 16777216000,
16
+ NCPU: 8,
17
+ KernelVersion: '5.15.0-91-generic',
18
+ });
19
+
20
+ describe('parseDaemonInfo', () => {
21
+ it('parses docker info JSON output into structured data', () => {
22
+ const result = parseDaemonInfo(SAMPLE_OUTPUT);
23
+
24
+ expect(result).toEqual({
25
+ serverVersion: '24.0.7',
26
+ containers: 5,
27
+ images: 12,
28
+ driver: 'overlay2',
29
+ os: 'Ubuntu 22.04.3 LTS',
30
+ memoryBytes: 16777216000,
31
+ cpus: 8,
32
+ });
33
+ });
34
+ });
35
+
36
+ describe('daemonInfo handler', () => {
37
+ it('calls docker info with correct args and returns parsed result', async () => {
38
+ const mockExec: ExecFn = async (cmd, args) => {
39
+ expect(cmd).toBe('docker');
40
+ expect(args).toEqual(['info', '--format', 'json']);
41
+ return SAMPLE_OUTPUT;
42
+ };
43
+
44
+ const result = (await daemonInfo(undefined, mockExec)) as DaemonInfoResult;
45
+ expect(result.serverVersion).toBe('24.0.7');
46
+ expect(result.cpus).toBe(8);
47
+ });
48
+ });
@@ -0,0 +1,33 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface DaemonInfoResult {
4
+ serverVersion: string;
5
+ containers: number;
6
+ images: number;
7
+ driver: string;
8
+ os: string;
9
+ memoryBytes: number;
10
+ cpus: number;
11
+ }
12
+
13
+ /**
14
+ * Runs `docker info --format json` and extracts key daemon info.
15
+ */
16
+ export const daemonInfo: ProbeHandler = async (_params, exec) => {
17
+ const stdout = await exec('docker', ['info', '--format', 'json']);
18
+ return parseDaemonInfo(stdout);
19
+ };
20
+
21
+ export function parseDaemonInfo(stdout: string): DaemonInfoResult {
22
+ const raw = JSON.parse(stdout);
23
+
24
+ return {
25
+ serverVersion: raw.ServerVersion ?? '',
26
+ containers: raw.Containers ?? 0,
27
+ images: raw.Images ?? 0,
28
+ driver: raw.Driver ?? '',
29
+ os: raw.OperatingSystem ?? '',
30
+ memoryBytes: raw.MemTotal ?? 0,
31
+ cpus: raw.NCPU ?? 0,
32
+ };
33
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { ImagesListResult } from './images-list.js';
4
+ import { imagesList, parseImagesList } from './images-list.js';
5
+
6
+ const SAMPLE_OUTPUT = `{"Containers":"N/A","CreatedAt":"2024-01-10 08:00:00 +0000 UTC","CreatedSince":"5 days ago","Digest":"\u003cnone\u003e","ID":"sha256:abc123","Repository":"nginx","SharedSize":"N/A","Size":"187MB","Tag":"latest","UniqueSize":"N/A","VirtualSize":"187MB"}
7
+ {"Containers":"N/A","CreatedAt":"2024-01-08 12:00:00 +0000 UTC","CreatedSince":"7 days ago","Digest":"\u003cnone\u003e","ID":"sha256:def456","Repository":"postgres","SharedSize":"N/A","Size":"432MB","Tag":"16","UniqueSize":"N/A","VirtualSize":"432MB"}`;
8
+
9
+ describe('parseImagesList', () => {
10
+ it('parses docker images JSON output into structured data', () => {
11
+ const result = parseImagesList(SAMPLE_OUTPUT);
12
+
13
+ expect(result.images).toHaveLength(2);
14
+ expect(result.images[0]).toEqual({
15
+ id: 'sha256:abc123',
16
+ repository: 'nginx',
17
+ tag: 'latest',
18
+ size: '187MB',
19
+ created: '5 days ago',
20
+ });
21
+ expect(result.images[1]).toEqual({
22
+ id: 'sha256:def456',
23
+ repository: 'postgres',
24
+ tag: '16',
25
+ size: '432MB',
26
+ created: '7 days ago',
27
+ });
28
+ });
29
+
30
+ it('returns empty array for empty output', () => {
31
+ const result = parseImagesList('');
32
+ expect(result.images).toHaveLength(0);
33
+ });
34
+ });
35
+
36
+ describe('imagesList handler', () => {
37
+ it('calls docker images with correct args and returns parsed result', async () => {
38
+ const mockExec: ExecFn = async (cmd, args) => {
39
+ expect(cmd).toBe('docker');
40
+ expect(args).toEqual(['images', '--format', 'json']);
41
+ return SAMPLE_OUTPUT;
42
+ };
43
+
44
+ const result = (await imagesList(undefined, mockExec)) as ImagesListResult;
45
+ expect(result.images).toHaveLength(2);
46
+ expect(result.images[0]?.repository).toBe('nginx');
47
+ });
48
+ });
@@ -0,0 +1,39 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface ImageInfo {
4
+ id: string;
5
+ repository: string;
6
+ tag: string;
7
+ size: string;
8
+ created: string;
9
+ }
10
+
11
+ export interface ImagesListResult {
12
+ images: ImageInfo[];
13
+ }
14
+
15
+ /**
16
+ * Runs `docker images --format json` and parses each JSON line.
17
+ */
18
+ export const imagesList: ProbeHandler = async (_params, exec) => {
19
+ const stdout = await exec('docker', ['images', '--format', 'json']);
20
+ return parseImagesList(stdout);
21
+ };
22
+
23
+ export function parseImagesList(stdout: string): ImagesListResult {
24
+ const lines = stdout.trim().split('\n').filter(Boolean);
25
+ const images: ImageInfo[] = [];
26
+
27
+ for (const line of lines) {
28
+ const raw = JSON.parse(line);
29
+ images.push({
30
+ id: raw.ID ?? '',
31
+ repository: raw.Repository ?? '',
32
+ tag: raw.Tag ?? '',
33
+ size: raw.Size ?? '',
34
+ created: raw.CreatedSince ?? raw.CreatedAt ?? '',
35
+ });
36
+ }
37
+
38
+ return { images };
39
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { LogsTailResult } from './logs-tail.js';
4
+ import { logsTail, parseLogsTail } from './logs-tail.js';
5
+
6
+ const SAMPLE_LOGS = `2024-01-15T10:30:00Z Starting nginx...
7
+ 2024-01-15T10:30:01Z Listening on port 80
8
+ 2024-01-15T10:30:05Z GET / 200 0.5ms
9
+ `;
10
+
11
+ describe('parseLogsTail', () => {
12
+ it('parses log output into structured data', () => {
13
+ const result = parseLogsTail('web-server', SAMPLE_LOGS);
14
+
15
+ expect(result.container).toBe('web-server');
16
+ expect(result.lines).toHaveLength(3);
17
+ expect(result.lineCount).toBe(3);
18
+ expect(result.lines[0]).toBe('2024-01-15T10:30:00Z Starting nginx...');
19
+ });
20
+
21
+ it('handles empty output', () => {
22
+ const result = parseLogsTail('web-server', '');
23
+ expect(result.lines).toHaveLength(0);
24
+ expect(result.lineCount).toBe(0);
25
+ });
26
+ });
27
+
28
+ describe('logsTail handler', () => {
29
+ it('calls docker logs with default lines and returns parsed result', async () => {
30
+ const mockExec: ExecFn = async (cmd, args) => {
31
+ expect(cmd).toBe('docker');
32
+ expect(args).toEqual(['logs', '--tail', '100', 'web-server']);
33
+ return SAMPLE_LOGS;
34
+ };
35
+
36
+ const result = (await logsTail({ container: 'web-server' }, mockExec)) as LogsTailResult;
37
+ expect(result.container).toBe('web-server');
38
+ expect(result.lineCount).toBe(3);
39
+ });
40
+
41
+ it('uses custom lines param', async () => {
42
+ const mockExec: ExecFn = async (cmd, args) => {
43
+ expect(cmd).toBe('docker');
44
+ expect(args).toEqual(['logs', '--tail', '50', 'my-app']);
45
+ return SAMPLE_LOGS;
46
+ };
47
+
48
+ const result = (await logsTail({ container: 'my-app', lines: 50 }, mockExec)) as LogsTailResult;
49
+ expect(result.container).toBe('my-app');
50
+ });
51
+ });
@@ -0,0 +1,32 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface LogsTailResult {
4
+ container: string;
5
+ lines: string[];
6
+ lineCount: number;
7
+ }
8
+
9
+ /**
10
+ * Runs `docker logs --tail N <container>` and returns the log lines.
11
+ */
12
+ export const logsTail: ProbeHandler = async (params, exec) => {
13
+ const container = params?.container as string;
14
+ const lines = (params?.lines as number) ?? 100;
15
+
16
+ const stdout = await exec('docker', ['logs', '--tail', String(lines), container]);
17
+ return parseLogsTail(container, stdout);
18
+ };
19
+
20
+ export function parseLogsTail(container: string, stdout: string): LogsTailResult {
21
+ const lines = stdout.split('\n');
22
+ // Remove trailing empty line from split
23
+ if (lines.length > 0 && lines[lines.length - 1] === '') {
24
+ lines.pop();
25
+ }
26
+
27
+ return {
28
+ container,
29
+ lines,
30
+ lineCount: lines.length,
31
+ };
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { dockerPack } from './docker/index.js';
2
+ import { systemPack } from './system/index.js';
3
+ import { systemdPack } from './systemd/index.js';
4
+ import type { Pack } from './types.js';
5
+ import { createPackRegistry } from './validation.js';
6
+
7
+ export type { ExecFn, Pack, ProbeHandler } from './types.js';
8
+ export { systemPack } from './system/index.js';
9
+ export { dockerPack } from './docker/index.js';
10
+ export { systemdPack } from './systemd/index.js';
11
+ export { createPackRegistry, PackValidationError, validatePack } from './validation.js';
12
+
13
+ /** Registry of all built-in packs, keyed by pack name */
14
+ export const packRegistry: ReadonlyMap<string, Pack> = createPackRegistry([
15
+ systemPack,
16
+ dockerPack,
17
+ systemdPack,
18
+ ]);
@@ -0,0 +1,14 @@
1
+ import type { Pack } from '../types.js';
2
+ import { systemManifest } from './manifest.js';
3
+ import { cpuUsage } from './probes/cpu-usage.js';
4
+ import { diskUsage } from './probes/disk-usage.js';
5
+ import { memoryUsage } from './probes/memory-usage.js';
6
+
7
+ export const systemPack: Pack = {
8
+ manifest: systemManifest,
9
+ handlers: {
10
+ 'system.disk.usage': diskUsage,
11
+ 'system.memory.usage': memoryUsage,
12
+ 'system.cpu.usage': cpuUsage,
13
+ },
14
+ };
@@ -0,0 +1,40 @@
1
+ import type { PackManifest } from '@sonde/shared';
2
+
3
+ export const systemManifest: PackManifest = {
4
+ name: 'system',
5
+ version: '0.1.0',
6
+ description: 'Basic system metrics: disk usage, memory usage, CPU load',
7
+ requires: {
8
+ groups: [],
9
+ files: [],
10
+ commands: ['df'],
11
+ },
12
+ probes: [
13
+ {
14
+ name: 'disk.usage',
15
+ description: 'Disk usage per mounted filesystem',
16
+ capability: 'observe',
17
+ timeout: 10_000,
18
+ },
19
+ {
20
+ name: 'memory.usage',
21
+ description: 'System memory and swap usage',
22
+ capability: 'observe',
23
+ timeout: 10_000,
24
+ },
25
+ {
26
+ name: 'cpu.usage',
27
+ description: 'CPU load averages and core count',
28
+ capability: 'observe',
29
+ timeout: 10_000,
30
+ },
31
+ ],
32
+ runbook: {
33
+ category: 'system',
34
+ probes: ['disk.usage', 'memory.usage', 'cpu.usage'],
35
+ parallel: true,
36
+ },
37
+ detect: {
38
+ files: ['/proc/loadavg'],
39
+ },
40
+ };
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { CpuUsageResult } from './cpu-usage.js';
4
+ import { cpuUsage, parseLoadAvg } from './cpu-usage.js';
5
+
6
+ describe('parseLoadAvg', () => {
7
+ it('parses /proc/loadavg and nproc output', () => {
8
+ const result = parseLoadAvg('1.52 0.89 0.65 2/345 12345\n', '4\n');
9
+
10
+ expect(result.loadAvg1).toBeCloseTo(1.52);
11
+ expect(result.loadAvg5).toBeCloseTo(0.89);
12
+ expect(result.loadAvg15).toBeCloseTo(0.65);
13
+ expect(result.cpuCount).toBe(4);
14
+ });
15
+
16
+ it('handles high load values', () => {
17
+ const result = parseLoadAvg('24.50 18.30 12.10 5/1200 99999\n', '8\n');
18
+
19
+ expect(result.loadAvg1).toBeCloseTo(24.5);
20
+ expect(result.loadAvg5).toBeCloseTo(18.3);
21
+ expect(result.loadAvg15).toBeCloseTo(12.1);
22
+ expect(result.cpuCount).toBe(8);
23
+ });
24
+
25
+ it('handles single CPU', () => {
26
+ const result = parseLoadAvg('0.01 0.02 0.00 1/50 100\n', '1\n');
27
+
28
+ expect(result.loadAvg1).toBeCloseTo(0.01);
29
+ expect(result.cpuCount).toBe(1);
30
+ });
31
+ });
32
+
33
+ describe('cpuUsage handler', () => {
34
+ it('calls cat /proc/loadavg and nproc, returns parsed result', async () => {
35
+ const calls: Array<{ cmd: string; args: string[] }> = [];
36
+
37
+ const mockExec: ExecFn = async (cmd, args) => {
38
+ calls.push({ cmd, args });
39
+ if (cmd === 'cat' && args[0] === '/proc/loadavg') {
40
+ return '2.10 1.50 0.90 3/400 54321\n';
41
+ }
42
+ if (cmd === 'nproc') {
43
+ return '16\n';
44
+ }
45
+ throw new Error(`Unexpected command: ${cmd}`);
46
+ };
47
+
48
+ const result = (await cpuUsage(undefined, mockExec)) as CpuUsageResult;
49
+ expect(result.loadAvg1).toBeCloseTo(2.1);
50
+ expect(result.loadAvg5).toBeCloseTo(1.5);
51
+ expect(result.loadAvg15).toBeCloseTo(0.9);
52
+ expect(result.cpuCount).toBe(16);
53
+ expect(calls).toHaveLength(2);
54
+ });
55
+ });
@@ -0,0 +1,30 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface CpuUsageResult {
4
+ loadAvg1: number;
5
+ loadAvg5: number;
6
+ loadAvg15: number;
7
+ cpuCount: number;
8
+ }
9
+
10
+ /**
11
+ * Reads /proc/loadavg for load averages and `nproc` for CPU count.
12
+ * Returns load averages and core count so consumers can compute utilization.
13
+ */
14
+ export const cpuUsage: ProbeHandler = async (_params, exec) => {
15
+ const [loadAvgRaw, nprocRaw] = await Promise.all([
16
+ exec('cat', ['/proc/loadavg']),
17
+ exec('nproc', []),
18
+ ]);
19
+ return parseLoadAvg(loadAvgRaw, nprocRaw);
20
+ };
21
+
22
+ export function parseLoadAvg(loadAvgRaw: string, nprocRaw: string): CpuUsageResult {
23
+ const parts = loadAvgRaw.trim().split(/\s+/);
24
+ const loadAvg1 = Number(parts[0]);
25
+ const loadAvg5 = Number(parts[1]);
26
+ const loadAvg15 = Number(parts[2]);
27
+ const cpuCount = Number.parseInt(nprocRaw.trim(), 10);
28
+
29
+ return { loadAvg1, loadAvg5, loadAvg15, cpuCount };
30
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { DiskUsageResult } from './disk-usage.js';
4
+ import { diskUsage, parseDfOutput } from './disk-usage.js';
5
+
6
+ const SAMPLE_DF_OUTPUT = `Filesystem 1024-blocks Used Available Capacity Mounted on
7
+ /dev/sda1 51474044 31285940 17548392 65% /
8
+ /dev/sdb1 103081248 45234112 52579600 47% /data
9
+ tmpfs 8152560 0 8152560 0% /dev/shm
10
+ `;
11
+
12
+ describe('parseDfOutput', () => {
13
+ it('parses df -kP output into structured data', () => {
14
+ const result = parseDfOutput(SAMPLE_DF_OUTPUT);
15
+
16
+ expect(result.filesystems).toHaveLength(2);
17
+
18
+ expect(result.filesystems[0]).toEqual({
19
+ filesystem: '/dev/sda1',
20
+ sizeKb: 51474044,
21
+ usedKb: 31285940,
22
+ availableKb: 17548392,
23
+ usePct: 65,
24
+ mountedOn: '/',
25
+ });
26
+
27
+ expect(result.filesystems[1]).toEqual({
28
+ filesystem: '/dev/sdb1',
29
+ sizeKb: 103081248,
30
+ usedKb: 45234112,
31
+ availableKb: 52579600,
32
+ usePct: 47,
33
+ mountedOn: '/data',
34
+ });
35
+ });
36
+
37
+ it('filters out tmpfs and devtmpfs', () => {
38
+ const result = parseDfOutput(SAMPLE_DF_OUTPUT);
39
+ const names = result.filesystems.map((f) => f.filesystem);
40
+ expect(names).not.toContain('tmpfs');
41
+ expect(names).not.toContain('devtmpfs');
42
+ });
43
+
44
+ it('handles single filesystem output', () => {
45
+ const output = `Filesystem 1024-blocks Used Available Capacity Mounted on
46
+ /dev/vda1 25671908 5432100 19912300 22% /
47
+ `;
48
+ const result = parseDfOutput(output);
49
+ expect(result.filesystems).toHaveLength(1);
50
+ expect(result.filesystems[0]?.usePct).toBe(22);
51
+ });
52
+
53
+ it('returns empty array for header-only output', () => {
54
+ const output = `Filesystem 1024-blocks Used Available Capacity Mounted on
55
+ `;
56
+ const result = parseDfOutput(output);
57
+ expect(result.filesystems).toHaveLength(0);
58
+ });
59
+ });
60
+
61
+ describe('diskUsage handler', () => {
62
+ it('calls df -kP and returns parsed result', async () => {
63
+ const mockExec: ExecFn = async (cmd, args) => {
64
+ expect(cmd).toBe('df');
65
+ expect(args).toEqual(['-kP']);
66
+ return SAMPLE_DF_OUTPUT;
67
+ };
68
+
69
+ const result = (await diskUsage(undefined, mockExec)) as DiskUsageResult;
70
+ expect(result.filesystems).toHaveLength(2);
71
+ expect(result.filesystems[0]?.mountedOn).toBe('/');
72
+ });
73
+ });
@@ -0,0 +1,53 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface FilesystemUsage {
4
+ filesystem: string;
5
+ sizeKb: number;
6
+ usedKb: number;
7
+ availableKb: number;
8
+ usePct: number;
9
+ mountedOn: string;
10
+ }
11
+
12
+ export interface DiskUsageResult {
13
+ filesystems: FilesystemUsage[];
14
+ }
15
+
16
+ /**
17
+ * Runs `df -kP` and parses the output into structured JSON.
18
+ * `-k` = 1K blocks, `-P` = POSIX portable output format (one line per fs).
19
+ */
20
+ export const diskUsage: ProbeHandler = async (_params, exec) => {
21
+ const stdout = await exec('df', ['-kP']);
22
+ return parseDfOutput(stdout);
23
+ };
24
+
25
+ export function parseDfOutput(stdout: string): DiskUsageResult {
26
+ const lines = stdout.trim().split('\n');
27
+ // Skip header line
28
+ const dataLines = lines.slice(1);
29
+
30
+ const filesystems: FilesystemUsage[] = [];
31
+
32
+ for (const line of dataLines) {
33
+ const parts = line.trim().split(/\s+/);
34
+ if (parts.length < 6) continue;
35
+
36
+ const [filesystem, sizeStr, usedStr, availStr, pctStr, mountedOn] = parts;
37
+ if (!filesystem || !sizeStr || !usedStr || !availStr || !pctStr || !mountedOn) continue;
38
+
39
+ // Skip pseudo-filesystems
40
+ if (filesystem === 'tmpfs' || filesystem === 'devtmpfs' || filesystem === 'none') continue;
41
+
42
+ const sizeKb = Number(sizeStr);
43
+ const usedKb = Number(usedStr);
44
+ const availableKb = Number(availStr);
45
+ const usePct = Number.parseInt(pctStr.replace('%', ''), 10);
46
+
47
+ if (Number.isNaN(sizeKb) || Number.isNaN(usedKb)) continue;
48
+
49
+ filesystems.push({ filesystem, sizeKb, usedKb, availableKb, usePct, mountedOn });
50
+ }
51
+
52
+ return { filesystems };
53
+ }