procm-cli 0.0.1

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/lib.test.ts ADDED
@@ -0,0 +1,258 @@
1
+ import { test, expect, describe } from "bun:test"
2
+ import {
3
+ parseProcessLine,
4
+ pad,
5
+ rpad,
6
+ cpuColor,
7
+ memColor,
8
+ statLabel,
9
+ statColor,
10
+ sortProcs,
11
+ sortFields,
12
+ c,
13
+ type Process,
14
+ } from "./lib"
15
+
16
+ // ── parseProcessLine ────────────────────────────────────────────────────────────
17
+
18
+ describe("parseProcessLine", () => {
19
+ test("parses a valid ps output line", () => {
20
+ const line = " 123 1 2.3 0.5 Ss ?? Thu Feb 20 14:30:00 2026 /usr/sbin/syslogd"
21
+ const p = parseProcessLine(line)
22
+ expect(p).toEqual({
23
+ pid: 123,
24
+ ppid: 1,
25
+ cpu: 2.3,
26
+ mem: 0.5,
27
+ stat: "Ss",
28
+ tty: "??",
29
+ command: "/usr/sbin/syslogd",
30
+ })
31
+ })
32
+
33
+ test("parses command with spaces", () => {
34
+ const line = " 200 1 0.0 0.1 Ss ?? Thu Feb 20 14:30:00 2026 /usr/bin/some command with spaces"
35
+ const p = parseProcessLine(line)
36
+ expect(p).not.toBeNull()
37
+ expect(p!.command).toBe("/usr/bin/some command with spaces")
38
+ })
39
+
40
+ test("skips header line", () => {
41
+ const line = " PID PPID %CPU %MEM STAT TT LSTART COMMAND"
42
+ expect(parseProcessLine(line)).toBeNull()
43
+ })
44
+
45
+ test("skips empty lines", () => {
46
+ expect(parseProcessLine("")).toBeNull()
47
+ })
48
+
49
+ test("includes processes with tty=??", () => {
50
+ const line = " 300 1 1.0 0.2 R+ ?? Thu Feb 20 14:30:00 2026 /usr/bin/daemon"
51
+ const p = parseProcessLine(line)
52
+ expect(p).not.toBeNull()
53
+ expect(p!.pid).toBe(300)
54
+ })
55
+
56
+ test("includes sleeping process with a TTY", () => {
57
+ const line = " 400 1 0.0 0.1 S ttys000 Thu Feb 20 14:30:00 2026 /bin/zsh"
58
+ const p = parseProcessLine(line)
59
+ expect(p).not.toBeNull()
60
+ expect(p!.pid).toBe(400)
61
+ })
62
+
63
+ test("excludes running process with a TTY", () => {
64
+ const line = " 500 1 5.2 1.3 R+ ttys000 Thu Feb 20 14:30:00 2026 vim test.txt"
65
+ expect(parseProcessLine(line)).toBeNull()
66
+ })
67
+ })
68
+
69
+ // ── pad / rpad ──────────────────────────────────────────────────────────────────
70
+
71
+ describe("pad", () => {
72
+ test("right-pads short string", () => {
73
+ expect(pad("foo", 10)).toBe("foo ")
74
+ })
75
+
76
+ test("truncates long string with ~", () => {
77
+ expect(pad("very long string", 5)).toBe("very~")
78
+ })
79
+
80
+ test("returns exact length string unchanged", () => {
81
+ expect(pad("hello", 5)).toBe("hello")
82
+ })
83
+ })
84
+
85
+ describe("rpad", () => {
86
+ test("left-pads short string", () => {
87
+ expect(rpad("42", 7)).toBe(" 42")
88
+ })
89
+
90
+ test("truncates long string with ~", () => {
91
+ expect(rpad("very long string", 5)).toBe("very~")
92
+ })
93
+
94
+ test("returns exact length string unchanged", () => {
95
+ expect(rpad("hello", 5)).toBe("hello")
96
+ })
97
+ })
98
+
99
+ // ── cpuColor / memColor ─────────────────────────────────────────────────────────
100
+
101
+ describe("cpuColor", () => {
102
+ test(">50 returns red", () => {
103
+ expect(cpuColor(51)).toBe(c.red)
104
+ })
105
+
106
+ test(">20 returns orange", () => {
107
+ expect(cpuColor(21)).toBe(c.orange)
108
+ })
109
+
110
+ test(">5 returns yellow", () => {
111
+ expect(cpuColor(6)).toBe(c.yellow)
112
+ })
113
+
114
+ test("<=5 returns green", () => {
115
+ expect(cpuColor(5)).toBe(c.green)
116
+ expect(cpuColor(0)).toBe(c.green)
117
+ })
118
+ })
119
+
120
+ describe("memColor", () => {
121
+ test(">30 returns red", () => {
122
+ expect(memColor(31)).toBe(c.red)
123
+ })
124
+
125
+ test(">10 returns orange", () => {
126
+ expect(memColor(11)).toBe(c.orange)
127
+ })
128
+
129
+ test(">3 returns yellow", () => {
130
+ expect(memColor(4)).toBe(c.yellow)
131
+ })
132
+
133
+ test("<=3 returns green", () => {
134
+ expect(memColor(3)).toBe(c.green)
135
+ expect(memColor(0)).toBe(c.green)
136
+ })
137
+ })
138
+
139
+ // ── statLabel / statColor ───────────────────────────────────────────────────────
140
+
141
+ describe("statLabel", () => {
142
+ test("R+ → Running", () => {
143
+ expect(statLabel("R+")).toBe("Running")
144
+ })
145
+
146
+ test("Ss → Sleeping", () => {
147
+ expect(statLabel("Ss")).toBe("Sleeping")
148
+ })
149
+
150
+ test("D → Disk", () => {
151
+ expect(statLabel("D")).toBe("Disk")
152
+ })
153
+
154
+ test("T → Stopped", () => {
155
+ expect(statLabel("T")).toBe("Stopped")
156
+ })
157
+
158
+ test("Z → Zombie", () => {
159
+ expect(statLabel("Z")).toBe("Zombie")
160
+ })
161
+
162
+ test("I → Idle", () => {
163
+ expect(statLabel("I")).toBe("Idle")
164
+ })
165
+
166
+ test("unknown status returns raw string", () => {
167
+ expect(statLabel("X+")).toBe("X+")
168
+ })
169
+ })
170
+
171
+ describe("statColor", () => {
172
+ test("R → green", () => {
173
+ expect(statColor("R+")).toBe(c.green)
174
+ })
175
+
176
+ test("T → yellow", () => {
177
+ expect(statColor("T")).toBe(c.yellow)
178
+ })
179
+
180
+ test("Z → red", () => {
181
+ expect(statColor("Z")).toBe(c.red)
182
+ })
183
+
184
+ test("D → orange", () => {
185
+ expect(statColor("D")).toBe(c.orange)
186
+ })
187
+
188
+ test("S → dim (default)", () => {
189
+ expect(statColor("Ss")).toBe(c.dim)
190
+ })
191
+ })
192
+
193
+ // ── sortProcs ───────────────────────────────────────────────────────────────────
194
+
195
+ function makeProc(overrides: Partial<Process> = {}): Process {
196
+ return {
197
+ pid: 1,
198
+ ppid: 0,
199
+ cpu: 0,
200
+ mem: 0,
201
+ stat: "Ss",
202
+ tty: "??",
203
+ command: "/bin/test",
204
+ ...overrides,
205
+ }
206
+ }
207
+
208
+ describe("sortProcs", () => {
209
+ test("sorts by CPU descending (default)", () => {
210
+ const procs = [
211
+ makeProc({ pid: 1, cpu: 5 }),
212
+ makeProc({ pid: 2, cpu: 50 }),
213
+ makeProc({ pid: 3, cpu: 20 }),
214
+ ]
215
+ const sorted = sortProcs(procs, "cpu", false)
216
+ expect(sorted.map((p) => p.cpu)).toEqual([50, 20, 5])
217
+ })
218
+
219
+ test("sorts by PID ascending", () => {
220
+ const procs = [
221
+ makeProc({ pid: 300 }),
222
+ makeProc({ pid: 100 }),
223
+ makeProc({ pid: 200 }),
224
+ ]
225
+ const sorted = sortProcs(procs, "pid", true)
226
+ expect(sorted.map((p) => p.pid)).toEqual([100, 200, 300])
227
+ })
228
+
229
+ test("sorts by command alphabetically ascending", () => {
230
+ const procs = [
231
+ makeProc({ command: "zsh" }),
232
+ makeProc({ command: "bash" }),
233
+ makeProc({ command: "fish" }),
234
+ ]
235
+ const sorted = sortProcs(procs, "command", true)
236
+ expect(sorted.map((p) => p.command)).toEqual(["bash", "fish", "zsh"])
237
+ })
238
+
239
+ test("flipping sort direction reverses order", () => {
240
+ const procs = [
241
+ makeProc({ pid: 1, cpu: 5 }),
242
+ makeProc({ pid: 2, cpu: 50 }),
243
+ makeProc({ pid: 3, cpu: 20 }),
244
+ ]
245
+ const asc = sortProcs([...procs], "cpu", true)
246
+ const desc = sortProcs([...procs], "cpu", false)
247
+ expect(asc.map((p) => p.cpu)).toEqual([5, 20, 50])
248
+ expect(desc.map((p) => p.cpu)).toEqual([50, 20, 5])
249
+ })
250
+ })
251
+
252
+ // ── sortFields ──────────────────────────────────────────────────────────────────
253
+
254
+ describe("sortFields", () => {
255
+ test("contains all expected fields", () => {
256
+ expect(sortFields).toEqual(["pid", "cpu", "mem", "status", "command"])
257
+ })
258
+ })
package/lib.ts ADDED
@@ -0,0 +1,122 @@
1
+ // ── Types ──────────────────────────────────────────────────────────────────────
2
+
3
+ export interface Process {
4
+ pid: number
5
+ ppid: number
6
+ cpu: number
7
+ mem: number
8
+ stat: string
9
+ tty: string
10
+ command: string
11
+ }
12
+
13
+ // ── Process data ───────────────────────────────────────────────────────────────
14
+
15
+ export const PS_REGEX =
16
+ /^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(\S+)\s+\w+\s+\w+\s+\d+\s+[\d:]+\s+\d+\s+(.+)$/
17
+
18
+ export function parseProcessLine(line: string): Process | null {
19
+ if (line.length === 0) return null
20
+ const m = PS_REGEX.exec(line)
21
+ if (!m) return null
22
+
23
+ const stat = m[5]
24
+ const tty = m[6]
25
+ // Daemon filter: no TTY or sleeping/idle
26
+ const ch0 = stat.charCodeAt(0)
27
+ if (tty !== "??" && ch0 !== 83 && ch0 !== 115 && ch0 !== 73) return null // S=83,s=115,I=73
28
+
29
+ return {
30
+ pid: +m[1],
31
+ ppid: +m[2],
32
+ cpu: +m[3],
33
+ mem: +m[4],
34
+ stat,
35
+ tty,
36
+ command: m[7],
37
+ }
38
+ }
39
+
40
+ // ── Colors ─────────────────────────────────────────────────────────────────────
41
+
42
+ export const c = {
43
+ bg: "#1a1b26",
44
+ bgAlt: "#24283b",
45
+ bgHl: "#364a82",
46
+ border: "#565f89",
47
+ text: "#c0caf5",
48
+ dim: "#565f89",
49
+ muted: "#a9b1d6",
50
+ green: "#9ece6a",
51
+ red: "#f7768e",
52
+ yellow: "#e0af68",
53
+ blue: "#7aa2f7",
54
+ cyan: "#7dcfff",
55
+ magenta: "#bb9af7",
56
+ orange: "#ff9e64",
57
+ }
58
+
59
+ // ── Helpers ────────────────────────────────────────────────────────────────────
60
+
61
+ export function pad(s: string, n: number) {
62
+ return s.length > n ? s.slice(0, n - 1) + "~" : s.padEnd(n)
63
+ }
64
+
65
+ export function rpad(s: string, n: number) {
66
+ return s.length > n ? s.slice(0, n - 1) + "~" : s.padStart(n)
67
+ }
68
+
69
+ export function cpuColor(v: number) {
70
+ if (v > 50) return c.red
71
+ if (v > 20) return c.orange
72
+ if (v > 5) return c.yellow
73
+ return c.green
74
+ }
75
+
76
+ export function memColor(v: number) {
77
+ if (v > 30) return c.red
78
+ if (v > 10) return c.orange
79
+ if (v > 3) return c.yellow
80
+ return c.green
81
+ }
82
+
83
+ export function statLabel(s: string) {
84
+ switch (s.charCodeAt(0)) {
85
+ case 82: return "Running" // R
86
+ case 83: return "Sleeping" // S
87
+ case 68: return "Disk" // D
88
+ case 84: return "Stopped" // T
89
+ case 90: return "Zombie" // Z
90
+ case 73: return "Idle" // I
91
+ default: return s
92
+ }
93
+ }
94
+
95
+ export function statColor(s: string) {
96
+ switch (s.charCodeAt(0)) {
97
+ case 82: return c.green // R
98
+ case 84: return c.yellow // T
99
+ case 90: return c.red // Z
100
+ case 68: return c.orange // D
101
+ default: return c.dim
102
+ }
103
+ }
104
+
105
+ // ── Sort ────────────────────────────────────────────────────────────────────────
106
+
107
+ export type SortField = "pid" | "cpu" | "mem" | "status" | "command"
108
+ export const sortFields: SortField[] = ["pid", "cpu", "mem", "status", "command"]
109
+
110
+ export function sortProcs(procs: Process[], sortBy: SortField, sortAsc: boolean) {
111
+ return procs.sort((a, b) => {
112
+ let v = 0
113
+ switch (sortBy) {
114
+ case "pid": v = a.pid - b.pid; break
115
+ case "cpu": v = a.cpu - b.cpu; break
116
+ case "mem": v = a.mem - b.mem; break
117
+ case "status": v = a.stat.localeCompare(b.stat); break
118
+ case "command": v = a.command.localeCompare(b.command); break
119
+ }
120
+ return sortAsc ? v : -v
121
+ })
122
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "procm-cli",
3
+ "version": "0.0.1",
4
+ "description": "Terminal process manager with sorting, filtering, and kill/restart",
5
+ "module": "index.ts",
6
+ "type": "module",
7
+ "bin": {
8
+ "procm": "./index.ts"
9
+ },
10
+ "scripts": {
11
+ "start": "bun index.ts"
12
+ },
13
+ "keywords": [
14
+ "process",
15
+ "manager",
16
+ "tui",
17
+ "terminal",
18
+ "cli",
19
+ "daemon"
20
+ ],
21
+ "license": "MIT",
22
+ "devDependencies": {
23
+ "@types/bun": "latest"
24
+ },
25
+ "peerDependencies": {
26
+ "typescript": "^5"
27
+ },
28
+ "dependencies": {
29
+ "@opentui/core": "^0.1.80"
30
+ }
31
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }