@doriandev/devcc 0.1.2

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 (284) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +659 -0
  3. package/bin/devcc.mjs +66 -0
  4. package/dist/devcc/app/App.d.ts +178 -0
  5. package/dist/devcc/app/App.d.ts.map +1 -0
  6. package/dist/devcc/app/App.js +1255 -0
  7. package/dist/devcc/app/App.js.map +1 -0
  8. package/dist/devcc/app/builtinCommands.d.ts +17 -0
  9. package/dist/devcc/app/builtinCommands.d.ts.map +1 -0
  10. package/dist/devcc/app/builtinCommands.js +389 -0
  11. package/dist/devcc/app/builtinCommands.js.map +1 -0
  12. package/dist/devcc/app/keymap.d.ts +21 -0
  13. package/dist/devcc/app/keymap.d.ts.map +1 -0
  14. package/dist/devcc/app/keymap.js +162 -0
  15. package/dist/devcc/app/keymap.js.map +1 -0
  16. package/dist/devcc/app/theme.d.ts +49 -0
  17. package/dist/devcc/app/theme.d.ts.map +1 -0
  18. package/dist/devcc/app/theme.js +68 -0
  19. package/dist/devcc/app/theme.js.map +1 -0
  20. package/dist/devcc/cli.d.ts +3 -0
  21. package/dist/devcc/cli.d.ts.map +1 -0
  22. package/dist/devcc/cli.js +113 -0
  23. package/dist/devcc/cli.js.map +1 -0
  24. package/dist/devcc/components/CommandPalette.d.ts +26 -0
  25. package/dist/devcc/components/CommandPalette.d.ts.map +1 -0
  26. package/dist/devcc/components/CommandPalette.js +113 -0
  27. package/dist/devcc/components/CommandPalette.js.map +1 -0
  28. package/dist/devcc/components/ConfirmDialog.d.ts +31 -0
  29. package/dist/devcc/components/ConfirmDialog.d.ts.map +1 -0
  30. package/dist/devcc/components/ConfirmDialog.js +147 -0
  31. package/dist/devcc/components/ConfirmDialog.js.map +1 -0
  32. package/dist/devcc/components/Footer.d.ts +22 -0
  33. package/dist/devcc/components/Footer.d.ts.map +1 -0
  34. package/dist/devcc/components/Footer.js +72 -0
  35. package/dist/devcc/components/Footer.js.map +1 -0
  36. package/dist/devcc/components/Header.d.ts +12 -0
  37. package/dist/devcc/components/Header.d.ts.map +1 -0
  38. package/dist/devcc/components/Header.js +84 -0
  39. package/dist/devcc/components/Header.js.map +1 -0
  40. package/dist/devcc/components/HelpOverlay.d.ts +18 -0
  41. package/dist/devcc/components/HelpOverlay.d.ts.map +1 -0
  42. package/dist/devcc/components/HelpOverlay.js +48 -0
  43. package/dist/devcc/components/HelpOverlay.js.map +1 -0
  44. package/dist/devcc/components/Overlay.d.ts +31 -0
  45. package/dist/devcc/components/Overlay.d.ts.map +1 -0
  46. package/dist/devcc/components/Overlay.js +60 -0
  47. package/dist/devcc/components/Overlay.js.map +1 -0
  48. package/dist/devcc/components/ProjectPicker.d.ts +26 -0
  49. package/dist/devcc/components/ProjectPicker.d.ts.map +1 -0
  50. package/dist/devcc/components/ProjectPicker.js +114 -0
  51. package/dist/devcc/components/ProjectPicker.js.map +1 -0
  52. package/dist/devcc/components/Sidebar.d.ts +42 -0
  53. package/dist/devcc/components/Sidebar.d.ts.map +1 -0
  54. package/dist/devcc/components/Sidebar.js +127 -0
  55. package/dist/devcc/components/Sidebar.js.map +1 -0
  56. package/dist/devcc/components/StatusBadge.d.ts +16 -0
  57. package/dist/devcc/components/StatusBadge.d.ts.map +1 -0
  58. package/dist/devcc/components/StatusBadge.js +30 -0
  59. package/dist/devcc/components/StatusBadge.js.map +1 -0
  60. package/dist/devcc/components/primitives.d.ts +62 -0
  61. package/dist/devcc/components/primitives.d.ts.map +1 -0
  62. package/dist/devcc/components/primitives.js +169 -0
  63. package/dist/devcc/components/primitives.js.map +1 -0
  64. package/dist/devcc/components/styled.d.ts +16 -0
  65. package/dist/devcc/components/styled.d.ts.map +1 -0
  66. package/dist/devcc/components/styled.js +35 -0
  67. package/dist/devcc/components/styled.js.map +1 -0
  68. package/dist/devcc/core/commands.d.ts +89 -0
  69. package/dist/devcc/core/commands.d.ts.map +1 -0
  70. package/dist/devcc/core/commands.js +68 -0
  71. package/dist/devcc/core/commands.js.map +1 -0
  72. package/dist/devcc/core/config.d.ts +81 -0
  73. package/dist/devcc/core/config.d.ts.map +1 -0
  74. package/dist/devcc/core/config.js +313 -0
  75. package/dist/devcc/core/config.js.map +1 -0
  76. package/dist/devcc/core/events.d.ts +21 -0
  77. package/dist/devcc/core/events.d.ts.map +1 -0
  78. package/dist/devcc/core/events.js +27 -0
  79. package/dist/devcc/core/events.js.map +1 -0
  80. package/dist/devcc/core/state.d.ts +104 -0
  81. package/dist/devcc/core/state.d.ts.map +1 -0
  82. package/dist/devcc/core/state.js +139 -0
  83. package/dist/devcc/core/state.js.map +1 -0
  84. package/dist/devcc/deps/DependencyService.d.ts +50 -0
  85. package/dist/devcc/deps/DependencyService.d.ts.map +1 -0
  86. package/dist/devcc/deps/DependencyService.js +109 -0
  87. package/dist/devcc/deps/DependencyService.js.map +1 -0
  88. package/dist/devcc/deps/parse.d.ts +49 -0
  89. package/dist/devcc/deps/parse.d.ts.map +1 -0
  90. package/dist/devcc/deps/parse.js +158 -0
  91. package/dist/devcc/deps/parse.js.map +1 -0
  92. package/dist/devcc/git/GitService.d.ts +60 -0
  93. package/dist/devcc/git/GitService.d.ts.map +1 -0
  94. package/dist/devcc/git/GitService.js +160 -0
  95. package/dist/devcc/git/GitService.js.map +1 -0
  96. package/dist/devcc/git/parse.d.ts +57 -0
  97. package/dist/devcc/git/parse.d.ts.map +1 -0
  98. package/dist/devcc/git/parse.js +174 -0
  99. package/dist/devcc/git/parse.js.map +1 -0
  100. package/dist/devcc/index.d.ts +37 -0
  101. package/dist/devcc/index.d.ts.map +1 -0
  102. package/dist/devcc/index.js +36 -0
  103. package/dist/devcc/index.js.map +1 -0
  104. package/dist/devcc/logs/LogBuffer.d.ts +86 -0
  105. package/dist/devcc/logs/LogBuffer.d.ts.map +1 -0
  106. package/dist/devcc/logs/LogBuffer.js +200 -0
  107. package/dist/devcc/logs/LogBuffer.js.map +1 -0
  108. package/dist/devcc/logs/merge.d.ts +36 -0
  109. package/dist/devcc/logs/merge.d.ts.map +1 -0
  110. package/dist/devcc/logs/merge.js +71 -0
  111. package/dist/devcc/logs/merge.js.map +1 -0
  112. package/dist/devcc/processes/ManagedProcess.d.ts +129 -0
  113. package/dist/devcc/processes/ManagedProcess.d.ts.map +1 -0
  114. package/dist/devcc/processes/ManagedProcess.js +80 -0
  115. package/dist/devcc/processes/ManagedProcess.js.map +1 -0
  116. package/dist/devcc/processes/ProcessManager.d.ts +112 -0
  117. package/dist/devcc/processes/ProcessManager.d.ts.map +1 -0
  118. package/dist/devcc/processes/ProcessManager.js +525 -0
  119. package/dist/devcc/processes/ProcessManager.js.map +1 -0
  120. package/dist/devcc/processes/graph.d.ts +35 -0
  121. package/dist/devcc/processes/graph.d.ts.map +1 -0
  122. package/dist/devcc/processes/graph.js +68 -0
  123. package/dist/devcc/processes/graph.js.map +1 -0
  124. package/dist/devcc/projects/ProjectRegistry.d.ts +52 -0
  125. package/dist/devcc/projects/ProjectRegistry.d.ts.map +1 -0
  126. package/dist/devcc/projects/ProjectRegistry.js +177 -0
  127. package/dist/devcc/projects/ProjectRegistry.js.map +1 -0
  128. package/dist/devcc/services/ServiceManager.d.ts +48 -0
  129. package/dist/devcc/services/ServiceManager.d.ts.map +1 -0
  130. package/dist/devcc/services/ServiceManager.js +146 -0
  131. package/dist/devcc/services/ServiceManager.js.map +1 -0
  132. package/dist/devcc/services/docker.d.ts +37 -0
  133. package/dist/devcc/services/docker.d.ts.map +1 -0
  134. package/dist/devcc/services/docker.js +179 -0
  135. package/dist/devcc/services/docker.js.map +1 -0
  136. package/dist/devcc/services/node.d.ts +40 -0
  137. package/dist/devcc/services/node.d.ts.map +1 -0
  138. package/dist/devcc/services/node.js +107 -0
  139. package/dist/devcc/services/node.js.map +1 -0
  140. package/dist/devcc/services/ollama.d.ts +27 -0
  141. package/dist/devcc/services/ollama.d.ts.map +1 -0
  142. package/dist/devcc/services/ollama.js +66 -0
  143. package/dist/devcc/services/ollama.js.map +1 -0
  144. package/dist/devcc/services/postgres.d.ts +30 -0
  145. package/dist/devcc/services/postgres.d.ts.map +1 -0
  146. package/dist/devcc/services/postgres.js +47 -0
  147. package/dist/devcc/services/postgres.js.map +1 -0
  148. package/dist/devcc/services/redis.d.ts +20 -0
  149. package/dist/devcc/services/redis.d.ts.map +1 -0
  150. package/dist/devcc/services/redis.js +39 -0
  151. package/dist/devcc/services/redis.js.map +1 -0
  152. package/dist/devcc/services/types.d.ts +51 -0
  153. package/dist/devcc/services/types.d.ts.map +1 -0
  154. package/dist/devcc/services/types.js +10 -0
  155. package/dist/devcc/services/types.js.map +1 -0
  156. package/dist/devcc/system/SystemMonitor.d.ts +87 -0
  157. package/dist/devcc/system/SystemMonitor.d.ts.map +1 -0
  158. package/dist/devcc/system/SystemMonitor.js +142 -0
  159. package/dist/devcc/system/SystemMonitor.js.map +1 -0
  160. package/dist/devcc/utils/errors.d.ts +31 -0
  161. package/dist/devcc/utils/errors.d.ts.map +1 -0
  162. package/dist/devcc/utils/errors.js +52 -0
  163. package/dist/devcc/utils/errors.js.map +1 -0
  164. package/dist/devcc/utils/exec.d.ts +41 -0
  165. package/dist/devcc/utils/exec.d.ts.map +1 -0
  166. package/dist/devcc/utils/exec.js +112 -0
  167. package/dist/devcc/utils/exec.js.map +1 -0
  168. package/dist/devcc/utils/format.d.ts +18 -0
  169. package/dist/devcc/utils/format.d.ts.map +1 -0
  170. package/dist/devcc/utils/format.js +85 -0
  171. package/dist/devcc/utils/format.js.map +1 -0
  172. package/dist/devcc/utils/fuzzy.d.ts +12 -0
  173. package/dist/devcc/utils/fuzzy.d.ts.map +1 -0
  174. package/dist/devcc/utils/fuzzy.js +20 -0
  175. package/dist/devcc/utils/fuzzy.js.map +1 -0
  176. package/dist/devcc/utils/logger.d.ts +41 -0
  177. package/dist/devcc/utils/logger.d.ts.map +1 -0
  178. package/dist/devcc/utils/logger.js +117 -0
  179. package/dist/devcc/utils/logger.js.map +1 -0
  180. package/dist/devcc/utils/packageManager.d.ts +39 -0
  181. package/dist/devcc/utils/packageManager.d.ts.map +1 -0
  182. package/dist/devcc/utils/packageManager.js +85 -0
  183. package/dist/devcc/utils/packageManager.js.map +1 -0
  184. package/dist/devcc/utils/paths.d.ts +12 -0
  185. package/dist/devcc/utils/paths.d.ts.map +1 -0
  186. package/dist/devcc/utils/paths.js +39 -0
  187. package/dist/devcc/utils/paths.js.map +1 -0
  188. package/dist/devcc/views/DependenciesView.d.ts +34 -0
  189. package/dist/devcc/views/DependenciesView.d.ts.map +1 -0
  190. package/dist/devcc/views/DependenciesView.js +170 -0
  191. package/dist/devcc/views/DependenciesView.js.map +1 -0
  192. package/dist/devcc/views/GitView.d.ts +29 -0
  193. package/dist/devcc/views/GitView.d.ts.map +1 -0
  194. package/dist/devcc/views/GitView.js +226 -0
  195. package/dist/devcc/views/GitView.js.map +1 -0
  196. package/dist/devcc/views/LogsView.d.ts +39 -0
  197. package/dist/devcc/views/LogsView.d.ts.map +1 -0
  198. package/dist/devcc/views/LogsView.js +249 -0
  199. package/dist/devcc/views/LogsView.js.map +1 -0
  200. package/dist/devcc/views/OverviewView.d.ts +16 -0
  201. package/dist/devcc/views/OverviewView.d.ts.map +1 -0
  202. package/dist/devcc/views/OverviewView.js +135 -0
  203. package/dist/devcc/views/OverviewView.js.map +1 -0
  204. package/dist/devcc/views/ProcessesView.d.ts +26 -0
  205. package/dist/devcc/views/ProcessesView.d.ts.map +1 -0
  206. package/dist/devcc/views/ProcessesView.js +161 -0
  207. package/dist/devcc/views/ProcessesView.js.map +1 -0
  208. package/dist/devcc/views/ScriptsView.d.ts +30 -0
  209. package/dist/devcc/views/ScriptsView.d.ts.map +1 -0
  210. package/dist/devcc/views/ScriptsView.js +168 -0
  211. package/dist/devcc/views/ScriptsView.js.map +1 -0
  212. package/dist/devcc/views/ServicesView.d.ts +28 -0
  213. package/dist/devcc/views/ServicesView.d.ts.map +1 -0
  214. package/dist/devcc/views/ServicesView.js +153 -0
  215. package/dist/devcc/views/ServicesView.js.map +1 -0
  216. package/dist/devcc/views/SettingsView.d.ts +20 -0
  217. package/dist/devcc/views/SettingsView.d.ts.map +1 -0
  218. package/dist/devcc/views/SettingsView.js +114 -0
  219. package/dist/devcc/views/SettingsView.js.map +1 -0
  220. package/dist/devcc/views/SystemView.d.ts +13 -0
  221. package/dist/devcc/views/SystemView.d.ts.map +1 -0
  222. package/dist/devcc/views/SystemView.js +82 -0
  223. package/dist/devcc/views/SystemView.js.map +1 -0
  224. package/dist/devcc/views/View.d.ts +35 -0
  225. package/dist/devcc/views/View.d.ts.map +1 -0
  226. package/dist/devcc/views/View.js +42 -0
  227. package/dist/devcc/views/View.js.map +1 -0
  228. package/package.json +79 -0
  229. package/src/devcc/app/App.ts +1463 -0
  230. package/src/devcc/app/builtinCommands.ts +429 -0
  231. package/src/devcc/app/keymap.ts +167 -0
  232. package/src/devcc/app/theme.ts +83 -0
  233. package/src/devcc/cli.ts +122 -0
  234. package/src/devcc/components/CommandPalette.ts +136 -0
  235. package/src/devcc/components/ConfirmDialog.ts +174 -0
  236. package/src/devcc/components/Footer.ts +85 -0
  237. package/src/devcc/components/Header.ts +93 -0
  238. package/src/devcc/components/HelpOverlay.ts +62 -0
  239. package/src/devcc/components/Overlay.ts +91 -0
  240. package/src/devcc/components/ProjectPicker.ts +138 -0
  241. package/src/devcc/components/Sidebar.ts +163 -0
  242. package/src/devcc/components/StatusBadge.ts +36 -0
  243. package/src/devcc/components/primitives.ts +213 -0
  244. package/src/devcc/components/styled.ts +44 -0
  245. package/src/devcc/core/commands.ts +140 -0
  246. package/src/devcc/core/config.ts +405 -0
  247. package/src/devcc/core/events.ts +41 -0
  248. package/src/devcc/core/state.ts +236 -0
  249. package/src/devcc/deps/DependencyService.ts +152 -0
  250. package/src/devcc/deps/parse.ts +210 -0
  251. package/src/devcc/git/GitService.ts +205 -0
  252. package/src/devcc/git/parse.ts +203 -0
  253. package/src/devcc/index.ts +115 -0
  254. package/src/devcc/logs/LogBuffer.ts +237 -0
  255. package/src/devcc/logs/merge.ts +87 -0
  256. package/src/devcc/processes/ManagedProcess.ts +195 -0
  257. package/src/devcc/processes/ProcessManager.ts +625 -0
  258. package/src/devcc/processes/graph.ts +98 -0
  259. package/src/devcc/projects/ProjectRegistry.ts +219 -0
  260. package/src/devcc/services/ServiceManager.ts +180 -0
  261. package/src/devcc/services/docker.ts +220 -0
  262. package/src/devcc/services/node.ts +137 -0
  263. package/src/devcc/services/ollama.ts +80 -0
  264. package/src/devcc/services/postgres.ts +57 -0
  265. package/src/devcc/services/redis.ts +47 -0
  266. package/src/devcc/services/types.ts +59 -0
  267. package/src/devcc/system/SystemMonitor.ts +207 -0
  268. package/src/devcc/utils/errors.ts +64 -0
  269. package/src/devcc/utils/exec.ts +165 -0
  270. package/src/devcc/utils/format.ts +83 -0
  271. package/src/devcc/utils/fuzzy.ts +44 -0
  272. package/src/devcc/utils/logger.ts +140 -0
  273. package/src/devcc/utils/packageManager.ts +112 -0
  274. package/src/devcc/utils/paths.ts +39 -0
  275. package/src/devcc/views/DependenciesView.ts +206 -0
  276. package/src/devcc/views/GitView.ts +259 -0
  277. package/src/devcc/views/LogsView.ts +300 -0
  278. package/src/devcc/views/OverviewView.ts +187 -0
  279. package/src/devcc/views/ProcessesView.ts +177 -0
  280. package/src/devcc/views/ScriptsView.ts +187 -0
  281. package/src/devcc/views/ServicesView.ts +188 -0
  282. package/src/devcc/views/SettingsView.ts +146 -0
  283. package/src/devcc/views/SystemView.ts +110 -0
  284. package/src/devcc/views/View.ts +63 -0
@@ -0,0 +1,207 @@
1
+ import { statfs } from "node:fs/promises"
2
+ import { arch, cpus, freemem, homedir, hostname, loadavg, platform, release, totalmem, uptime } from "node:os"
3
+ import { TypedEmitter } from "../core/events.js"
4
+ import { exec } from "../utils/exec.js"
5
+
6
+ export interface CpuSample {
7
+ idle: number
8
+ total: number
9
+ }
10
+
11
+ export interface DiskUsage {
12
+ total: number
13
+ free: number
14
+ used: number
15
+ ratio: number
16
+ }
17
+
18
+ export interface SystemMetrics {
19
+ /** 0..1, averaged across cores since the previous sample. */
20
+ cpu: number
21
+ memoryTotal: number
22
+ memoryFree: number
23
+ memoryUsed: number
24
+ memoryRatio: number
25
+ loadAverage: [number, number, number]
26
+ /** Host uptime in milliseconds. */
27
+ uptimeMs: number
28
+ /** devcc's own uptime in milliseconds. */
29
+ processUptimeMs: number
30
+ disk: DiskUsage | null
31
+ sampledAt: Date
32
+ }
33
+
34
+ export interface HostInfo {
35
+ hostname: string
36
+ platform: string
37
+ release: string
38
+ arch: string
39
+ cpuModel: string
40
+ cpuCount: number
41
+ totalMemory: number
42
+ home: string
43
+ runtime: string
44
+ nodeVersion: string
45
+ }
46
+
47
+ /** Versions of external tools; populated lazily so startup is never blocked. */
48
+ export interface ToolVersions {
49
+ git: string | null
50
+ docker: string | null
51
+ packageManager: string | null
52
+ }
53
+
54
+ export type SystemMonitorEvents = {
55
+ "system:metrics": [SystemMetrics]
56
+ "system:tools": [ToolVersions]
57
+ }
58
+
59
+ /** Aggregate `os.cpus()` times into idle/total counters. */
60
+ export function sampleCpu(cpuInfos: ReturnType<typeof cpus> = cpus()): CpuSample {
61
+ let idle = 0
62
+ let total = 0
63
+ for (const cpu of cpuInfos) {
64
+ idle += cpu.times.idle
65
+ total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq
66
+ }
67
+ return { idle, total }
68
+ }
69
+
70
+ /**
71
+ * CPU utilisation between two samples, as a 0..1 ratio.
72
+ * Returns 0 when the samples are identical (no elapsed time).
73
+ */
74
+ export function cpuUsageBetween(previous: CpuSample, current: CpuSample): number {
75
+ const idleDelta = current.idle - previous.idle
76
+ const totalDelta = current.total - previous.total
77
+ if (totalDelta <= 0) return 0
78
+ const usage = 1 - idleDelta / totalDelta
79
+ return Math.max(0, Math.min(1, usage))
80
+ }
81
+
82
+ export function detectRuntime(): string {
83
+ const bunVersion = (globalThis as { Bun?: { version: string } }).Bun?.version
84
+ if (bunVersion) return `bun ${bunVersion}`
85
+ return `node ${process.versions.node}`
86
+ }
87
+
88
+ export interface SystemMonitorOptions {
89
+ rootDir: string
90
+ intervalMs?: number
91
+ }
92
+
93
+ /**
94
+ * Samples host metrics on a timer using Node APIs only.
95
+ *
96
+ * The one exception is tool versions (`git`, `docker`), which have no API; those
97
+ * are read once, lazily, off the startup path.
98
+ */
99
+ export class SystemMonitor extends TypedEmitter<SystemMonitorEvents> {
100
+ private timer: NodeJS.Timeout | null = null
101
+ private previousSample: CpuSample = sampleCpu()
102
+ private metrics: SystemMetrics | null = null
103
+ private tools: ToolVersions = { git: null, docker: null, packageManager: null }
104
+ private readonly intervalMs: number
105
+ private readonly startedAt = Date.now()
106
+
107
+ constructor(private readonly options: SystemMonitorOptions) {
108
+ super()
109
+ this.intervalMs = options.intervalMs ?? 1_500
110
+ }
111
+
112
+ getMetrics(): SystemMetrics | null {
113
+ return this.metrics
114
+ }
115
+
116
+ getTools(): ToolVersions {
117
+ return this.tools
118
+ }
119
+
120
+ getHostInfo(): HostInfo {
121
+ const cpuList = cpus()
122
+ return {
123
+ hostname: hostname(),
124
+ platform: platform(),
125
+ release: release(),
126
+ arch: arch(),
127
+ cpuModel: cpuList[0]?.model.trim() ?? "unknown",
128
+ cpuCount: cpuList.length,
129
+ totalMemory: totalmem(),
130
+ home: homedir(),
131
+ runtime: detectRuntime(),
132
+ nodeVersion: process.versions.node,
133
+ }
134
+ }
135
+
136
+ start(): void {
137
+ if (this.timer) return
138
+ void this.sample()
139
+ this.timer = setInterval(() => void this.sample(), this.intervalMs)
140
+ this.timer.unref?.()
141
+ }
142
+
143
+ stop(): void {
144
+ if (!this.timer) return
145
+ clearInterval(this.timer)
146
+ this.timer = null
147
+ }
148
+
149
+ async sample(): Promise<SystemMetrics> {
150
+ const current = sampleCpu()
151
+ const cpu = cpuUsageBetween(this.previousSample, current)
152
+ this.previousSample = current
153
+
154
+ const total = totalmem()
155
+ const free = freemem()
156
+ const [one = 0, five = 0, fifteen = 0] = loadavg()
157
+
158
+ const metrics: SystemMetrics = {
159
+ cpu,
160
+ memoryTotal: total,
161
+ memoryFree: free,
162
+ memoryUsed: total - free,
163
+ memoryRatio: total > 0 ? (total - free) / total : 0,
164
+ loadAverage: [one, five, fifteen],
165
+ uptimeMs: uptime() * 1000,
166
+ processUptimeMs: Date.now() - this.startedAt,
167
+ disk: await this.readDisk(),
168
+ sampledAt: new Date(),
169
+ }
170
+
171
+ this.metrics = metrics
172
+ this.emit("system:metrics", metrics)
173
+ return metrics
174
+ }
175
+
176
+ /** Read tool versions once. Safe to call repeatedly; results are cached. */
177
+ async refreshTools(packageManager: string): Promise<ToolVersions> {
178
+ const [git, docker, pm] = await Promise.all([
179
+ versionOf("git", ["--version"], /git version (\S+)/u),
180
+ versionOf("docker", ["--version"], /Docker version ([^,\s]+)/u),
181
+ versionOf(packageManager, ["--version"], /(\S+)/u),
182
+ ])
183
+ this.tools = { git, docker, packageManager: pm }
184
+ this.emit("system:tools", this.tools)
185
+ return this.tools
186
+ }
187
+
188
+ private async readDisk(): Promise<DiskUsage | null> {
189
+ try {
190
+ const stats = await statfs(this.options.rootDir)
191
+ const total = Number(stats.blocks) * Number(stats.bsize)
192
+ const free = Number(stats.bavail) * Number(stats.bsize)
193
+ if (!Number.isFinite(total) || total <= 0) return null
194
+ const used = total - free
195
+ return { total, free, used, ratio: used / total }
196
+ } catch {
197
+ return null
198
+ }
199
+ }
200
+ }
201
+
202
+ async function versionOf(command: string, args: string[], pattern: RegExp): Promise<string | null> {
203
+ const result = await exec(command, args, { timeoutMs: 3_000 })
204
+ if (!result.ok) return null
205
+ const text = result.stdout.trim().split("\n")[0] ?? ""
206
+ return pattern.exec(text)?.[1] ?? (text === "" ? null : text)
207
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A single, consistent error model for everything Dev Command Center does.
3
+ *
4
+ * Every failure that reaches the UI is normalised into a `DevccError` so the
5
+ * notification centre, the log file and the diagnostics panel can all render it
6
+ * the same way.
7
+ */
8
+
9
+ export type DevccErrorScope =
10
+ "config" | "process" | "script" | "git" | "docker" | "service" | "system" | "command" | "internal"
11
+
12
+ export interface DevccErrorOptions {
13
+ scope: DevccErrorScope
14
+ /** Short, user facing summary. Shown in the notification area. */
15
+ message: string
16
+ /** Longer text (command output, stack, ...) shown when the error is inspected. */
17
+ detail?: string
18
+ cause?: unknown
19
+ /** Whether the application can keep running normally. Defaults to `true`. */
20
+ recoverable?: boolean
21
+ }
22
+
23
+ export class DevccError extends Error {
24
+ readonly scope: DevccErrorScope
25
+ readonly detail?: string
26
+ readonly recoverable: boolean
27
+
28
+ constructor(options: DevccErrorOptions) {
29
+ super(options.message, options.cause === undefined ? undefined : { cause: options.cause })
30
+ this.name = "DevccError"
31
+ this.scope = options.scope
32
+ this.detail = options.detail
33
+ this.recoverable = options.recoverable ?? true
34
+ }
35
+ }
36
+
37
+ /** Best-effort message extraction from an unknown thrown value. */
38
+ export function errorMessage(error: unknown): string {
39
+ if (error instanceof Error) return error.message
40
+ if (typeof error === "string") return error
41
+ try {
42
+ return JSON.stringify(error)
43
+ } catch {
44
+ return String(error)
45
+ }
46
+ }
47
+
48
+ /** Stack (or message) of an unknown thrown value, for the log file. */
49
+ export function errorDetail(error: unknown): string | undefined {
50
+ if (error instanceof DevccError) return error.detail ?? error.stack
51
+ if (error instanceof Error) return error.stack
52
+ return undefined
53
+ }
54
+
55
+ /** Normalise anything thrown into a `DevccError` without losing information. */
56
+ export function toDevccError(error: unknown, scope: DevccErrorScope, fallbackMessage?: string): DevccError {
57
+ if (error instanceof DevccError) return error
58
+ return new DevccError({
59
+ scope,
60
+ message: fallbackMessage ? `${fallbackMessage}: ${errorMessage(error)}` : errorMessage(error),
61
+ detail: errorDetail(error),
62
+ cause: error,
63
+ })
64
+ }
@@ -0,0 +1,165 @@
1
+ import { spawn, type SpawnOptions } from "node:child_process"
2
+ import { connect } from "node:net"
3
+
4
+ /**
5
+ * Small, safe process helpers.
6
+ *
7
+ * Everything goes through `spawn(executable, args[])` - never a shell string -
8
+ * so no caller can accidentally build an injectable command line.
9
+ */
10
+
11
+ export interface ExecOptions {
12
+ cwd?: string
13
+ env?: NodeJS.ProcessEnv
14
+ /** Hard timeout in milliseconds. Defaults to 5s: nothing in the UI may hang. */
15
+ timeoutMs?: number
16
+ /** Bytes of stdout/stderr to retain. Defaults to 1 MiB. */
17
+ maxBuffer?: number
18
+ input?: string
19
+ signal?: AbortSignal
20
+ }
21
+
22
+ export interface ExecResult {
23
+ ok: boolean
24
+ code: number | null
25
+ signal: NodeJS.Signals | null
26
+ stdout: string
27
+ stderr: string
28
+ timedOut: boolean
29
+ durationMs: number
30
+ /** Set when the executable could not be spawned at all (e.g. ENOENT). */
31
+ spawnError?: Error
32
+ }
33
+
34
+ export const DEFAULT_EXEC_TIMEOUT_MS = 5_000
35
+
36
+ /**
37
+ * Run a command and collect its output. Never rejects - inspect `ok`/`spawnError`.
38
+ */
39
+ export function exec(
40
+ command: string,
41
+ args: readonly string[] = [],
42
+ options: ExecOptions = {},
43
+ ): Promise<ExecResult> {
44
+ const { cwd, env, timeoutMs = DEFAULT_EXEC_TIMEOUT_MS, maxBuffer = 1024 * 1024, input, signal } = options
45
+
46
+ return new Promise<ExecResult>((resolve) => {
47
+ const startedAt = Date.now()
48
+ const spawnOptions: SpawnOptions = {
49
+ cwd,
50
+ env: env ?? process.env,
51
+ stdio: ["pipe", "pipe", "pipe"],
52
+ windowsHide: true,
53
+ }
54
+
55
+ let settled = false
56
+ let timedOut = false
57
+ let stdout = ""
58
+ let stderr = ""
59
+
60
+ const child = spawn(command, [...args], spawnOptions)
61
+
62
+ const finish = (result: Omit<ExecResult, "durationMs">) => {
63
+ if (settled) return
64
+ settled = true
65
+ clearTimeout(timer)
66
+ signal?.removeEventListener("abort", onAbort)
67
+ resolve({ ...result, durationMs: Date.now() - startedAt })
68
+ }
69
+
70
+ const timer = setTimeout(() => {
71
+ timedOut = true
72
+ child.kill("SIGKILL")
73
+ }, timeoutMs)
74
+ timer.unref?.()
75
+
76
+ const onAbort = () => child.kill("SIGKILL")
77
+ signal?.addEventListener("abort", onAbort, { once: true })
78
+
79
+ child.stdout?.setEncoding("utf8")
80
+ child.stdout?.on("data", (chunk: string) => {
81
+ if (stdout.length < maxBuffer) stdout += chunk
82
+ })
83
+ child.stderr?.setEncoding("utf8")
84
+ child.stderr?.on("data", (chunk: string) => {
85
+ if (stderr.length < maxBuffer) stderr += chunk
86
+ })
87
+
88
+ child.on("error", (error: Error) => {
89
+ finish({ ok: false, code: null, signal: null, stdout, stderr, timedOut, spawnError: error })
90
+ })
91
+
92
+ child.on("close", (code, closeSignal) => {
93
+ finish({
94
+ ok: !timedOut && code === 0,
95
+ code,
96
+ signal: closeSignal,
97
+ stdout,
98
+ stderr,
99
+ timedOut,
100
+ })
101
+ })
102
+
103
+ if (input !== undefined) {
104
+ child.stdin?.end(input)
105
+ } else {
106
+ child.stdin?.end()
107
+ }
108
+ })
109
+ }
110
+
111
+ /** Convenience wrapper that returns trimmed stdout, or `null` when the command failed. */
112
+ export async function execOutput(
113
+ command: string,
114
+ args: readonly string[] = [],
115
+ options: ExecOptions = {},
116
+ ): Promise<string | null> {
117
+ const result = await exec(command, args, options)
118
+ return result.ok ? result.stdout.trim() : null
119
+ }
120
+
121
+ /** True when `command --version` (or the given probe args) succeeds. */
122
+ export async function commandExists(
123
+ command: string,
124
+ probeArgs: readonly string[] = ["--version"],
125
+ ): Promise<boolean> {
126
+ const result = await exec(command, probeArgs, { timeoutMs: 3_000 })
127
+ return result.ok
128
+ }
129
+
130
+ /** Open a TCP connection to check whether something is listening. */
131
+ export function probeTcpPort(port: number, host = "127.0.0.1", timeoutMs = 700): Promise<boolean> {
132
+ return new Promise<boolean>((resolve) => {
133
+ const socket = connect({ port, host })
134
+ let settled = false
135
+
136
+ const done = (value: boolean) => {
137
+ if (settled) return
138
+ settled = true
139
+ socket.destroy()
140
+ resolve(value)
141
+ }
142
+
143
+ socket.setTimeout(timeoutMs)
144
+ socket.once("connect", () => done(true))
145
+ socket.once("timeout", () => done(false))
146
+ socket.once("error", () => done(false))
147
+ })
148
+ }
149
+
150
+ /** Resolve a promise to a fallback value when it takes too long. */
151
+ export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, fallback: T): Promise<T> {
152
+ return new Promise<T>((resolve) => {
153
+ const timer = setTimeout(() => resolve(fallback), timeoutMs)
154
+ timer.unref?.()
155
+ promise
156
+ .then((value) => {
157
+ clearTimeout(timer)
158
+ resolve(value)
159
+ })
160
+ .catch(() => {
161
+ clearTimeout(timer)
162
+ resolve(fallback)
163
+ })
164
+ })
165
+ }
@@ -0,0 +1,83 @@
1
+ /** Formatting helpers shared by the views. Pure functions, easy to test. */
2
+
3
+ export function formatBytes(bytes: number, fractionDigits = 1): string {
4
+ if (!Number.isFinite(bytes) || bytes < 0) return "-"
5
+ const units = ["B", "KB", "MB", "GB", "TB", "PB"]
6
+ let value = bytes
7
+ let unit = 0
8
+ while (value >= 1024 && unit < units.length - 1) {
9
+ value /= 1024
10
+ unit += 1
11
+ }
12
+ const digits = unit === 0 ? 0 : fractionDigits
13
+ return `${value.toFixed(digits)} ${units[unit]}`
14
+ }
15
+
16
+ /** Bytes rendered in GB without a unit suffix, for `7.2 / 36 GB` style pairs. */
17
+ export function toGigabytes(bytes: number, fractionDigits = 1): string {
18
+ return (bytes / 1024 ** 3).toFixed(fractionDigits)
19
+ }
20
+
21
+ export function formatPercent(ratio: number, fractionDigits = 0): string {
22
+ if (!Number.isFinite(ratio)) return "-"
23
+ return `${(ratio * 100).toFixed(fractionDigits)}%`
24
+ }
25
+
26
+ /** Compact duration: `4s`, `2m 10s`, `1h 04m`, `3d 02h`. */
27
+ export function formatDuration(ms: number): string {
28
+ if (!Number.isFinite(ms) || ms < 0) return "-"
29
+ const seconds = Math.floor(ms / 1000)
30
+ if (seconds < 60) return `${seconds}s`
31
+ const minutes = Math.floor(seconds / 60)
32
+ if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`
33
+ const hours = Math.floor(minutes / 60)
34
+ if (hours < 24) return `${hours}h ${String(minutes % 60).padStart(2, "0")}m`
35
+ const days = Math.floor(hours / 24)
36
+ return `${days}d ${String(hours % 24).padStart(2, "0")}h`
37
+ }
38
+
39
+ export function formatClock(date: Date): string {
40
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`
41
+ }
42
+
43
+ export function formatTimestamp(date: Date): string {
44
+ return (
45
+ `${String(date.getHours()).padStart(2, "0")}:` +
46
+ `${String(date.getMinutes()).padStart(2, "0")}:` +
47
+ `${String(date.getSeconds()).padStart(2, "0")}.` +
48
+ `${String(date.getMilliseconds()).padStart(3, "0")}`
49
+ )
50
+ }
51
+
52
+ /** Pad or truncate to an exact display width. */
53
+ export function fit(text: string, width: number): string {
54
+ if (width <= 0) return ""
55
+ if (text.length === width) return text
56
+ if (text.length < width) return text.padEnd(width, " ")
57
+ if (width === 1) return "…"
58
+ return `${text.slice(0, width - 1)}…`
59
+ }
60
+
61
+ export function truncate(text: string, maxLength: number): string {
62
+ if (maxLength <= 0) return ""
63
+ if (text.length <= maxLength) return text
64
+ if (maxLength === 1) return "…"
65
+ return `${text.slice(0, maxLength - 1)}…`
66
+ }
67
+
68
+ /** Single-line preview of a possibly multi-line string. */
69
+ export function singleLine(text: string, maxLength = 120): string {
70
+ return truncate(text.replace(/\s+/gu, " ").trim(), maxLength)
71
+ }
72
+
73
+ /** A small horizontal meter, e.g. `████░░░░░░`. */
74
+ export function meter(ratio: number, width = 10, filledChar = "█", emptyChar = "░"): string {
75
+ if (width <= 0) return ""
76
+ const clamped = Math.max(0, Math.min(1, Number.isFinite(ratio) ? ratio : 0))
77
+ const filled = Math.round(clamped * width)
78
+ return filledChar.repeat(filled) + emptyChar.repeat(width - filled)
79
+ }
80
+
81
+ export function pluralise(count: number, singular: string, plural = `${singular}s`): string {
82
+ return `${count} ${count === 1 ? singular : plural}`
83
+ }
@@ -0,0 +1,44 @@
1
+ import { Fzf, type FzfResultItem } from "fzf"
2
+
3
+ export interface FuzzyMatch<T> {
4
+ item: T
5
+ score: number
6
+ /** Indices of matched characters in the haystack, for highlighting. */
7
+ positions: number[]
8
+ }
9
+
10
+ /**
11
+ * `fzf`'s constructor overloads are expressed with a conditional tuple type
12
+ * that cannot be resolved through our own generic parameter. This narrow view
13
+ * of the constructor keeps the wrapper below fully typed for callers.
14
+ */
15
+ type FzfConstructor = new <U>(
16
+ list: readonly U[],
17
+ options: { selector: (item: U) => string; limit?: number },
18
+ ) => { find(query: string): FzfResultItem<U>[] }
19
+
20
+ const TypedFzf = Fzf as unknown as FzfConstructor
21
+
22
+ /**
23
+ * Thin wrapper over the `fzf` package so the command palette and the script
24
+ * list share one ranking implementation (and one place to tune it).
25
+ */
26
+ export function fuzzyFilter<T>(
27
+ items: readonly T[],
28
+ query: string,
29
+ selector: (item: T) => string,
30
+ limit = 50,
31
+ ): FuzzyMatch<T>[] {
32
+ if (items.length === 0) return []
33
+
34
+ if (query.trim() === "") {
35
+ return items.slice(0, limit).map((item) => ({ item, score: 0, positions: [] }))
36
+ }
37
+
38
+ const finder = new TypedFzf<T>(items, { selector, limit })
39
+ return finder.find(query).map((entry) => ({
40
+ item: entry.item,
41
+ score: entry.score,
42
+ positions: [...entry.positions].sort((a, b) => a - b),
43
+ }))
44
+ }
@@ -0,0 +1,140 @@
1
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+
4
+ /**
5
+ * Internal diagnostics logger.
6
+ *
7
+ * Writes to `.devcc/devcc.log` inside the project. Never writes to stdout/stderr
8
+ * while the TUI owns the terminal - that would corrupt the rendered frame.
9
+ */
10
+
11
+ export const LOG_LEVELS = ["debug", "info", "warn", "error", "silent"] as const
12
+ export type LogLevel = (typeof LOG_LEVELS)[number]
13
+
14
+ const LEVEL_WEIGHT: Record<LogLevel, number> = {
15
+ debug: 10,
16
+ info: 20,
17
+ warn: 30,
18
+ error: 40,
19
+ silent: 100,
20
+ }
21
+
22
+ export function isLogLevel(value: string): value is LogLevel {
23
+ return (LOG_LEVELS as readonly string[]).includes(value)
24
+ }
25
+
26
+ export interface LoggerOptions {
27
+ level?: LogLevel
28
+ filePath?: string | null
29
+ /** Extra sink, used by tests and by the in-app diagnostics panel. */
30
+ sink?: (line: string, level: LogLevel) => void
31
+ }
32
+
33
+ export class Logger {
34
+ private level: LogLevel
35
+ private filePath: string | null
36
+ private sink: ((line: string, level: LogLevel) => void) | undefined
37
+ private failed = false
38
+
39
+ constructor(options: LoggerOptions = {}) {
40
+ this.level = options.level ?? "info"
41
+ this.filePath = options.filePath ?? null
42
+ this.sink = options.sink
43
+ }
44
+
45
+ setLevel(level: LogLevel): void {
46
+ this.level = level
47
+ }
48
+
49
+ getLevel(): LogLevel {
50
+ return this.level
51
+ }
52
+
53
+ getFilePath(): string | null {
54
+ return this.filePath
55
+ }
56
+
57
+ child(namespace: string): ScopedLogger {
58
+ return new ScopedLogger(this, namespace)
59
+ }
60
+
61
+ enabled(level: LogLevel): boolean {
62
+ return LEVEL_WEIGHT[level] >= LEVEL_WEIGHT[this.level]
63
+ }
64
+
65
+ log(level: LogLevel, namespace: string, message: string, data?: unknown): void {
66
+ if (!this.enabled(level) || level === "silent") return
67
+
68
+ const stamp = new Date().toISOString()
69
+ const suffix = data === undefined ? "" : ` ${safeSerialise(data)}`
70
+ const line = `${stamp} ${level.toUpperCase().padEnd(5)} [${namespace}] ${message}${suffix}`
71
+
72
+ this.sink?.(line, level)
73
+
74
+ if (!this.filePath || this.failed) return
75
+ try {
76
+ ensureLogDirectory(dirname(this.filePath))
77
+ appendFileSync(this.filePath, `${line}\n`, "utf8")
78
+ } catch {
79
+ // Never let logging take the app down; disable the file sink instead.
80
+ this.failed = true
81
+ }
82
+ }
83
+ }
84
+
85
+ export class ScopedLogger {
86
+ constructor(
87
+ private readonly logger: Logger,
88
+ private readonly namespace: string,
89
+ ) {}
90
+
91
+ debug(message: string, data?: unknown): void {
92
+ this.logger.log("debug", this.namespace, message, data)
93
+ }
94
+
95
+ info(message: string, data?: unknown): void {
96
+ this.logger.log("info", this.namespace, message, data)
97
+ }
98
+
99
+ warn(message: string, data?: unknown): void {
100
+ this.logger.log("warn", this.namespace, message, data)
101
+ }
102
+
103
+ error(message: string, data?: unknown): void {
104
+ this.logger.log("error", this.namespace, message, data)
105
+ }
106
+
107
+ child(namespace: string): ScopedLogger {
108
+ return this.logger.child(`${this.namespace}:${namespace}`)
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Create `.devcc/` and self-ignore it, so devcc's own diagnostics never show up
114
+ * as untracked changes in the user's repository.
115
+ */
116
+ function ensureLogDirectory(directory: string): void {
117
+ mkdirSync(directory, { recursive: true })
118
+ const ignorePath = join(directory, ".gitignore")
119
+ if (!existsSync(ignorePath)) writeFileSync(ignorePath, "*\n", "utf8")
120
+ }
121
+
122
+ function safeSerialise(data: unknown): string {
123
+ if (data instanceof Error) return `${data.name}: ${data.message}`
124
+ if (typeof data === "string") return data
125
+ try {
126
+ return JSON.stringify(data)
127
+ } catch {
128
+ return String(data)
129
+ }
130
+ }
131
+
132
+ export function defaultLogFilePath(rootDir: string): string {
133
+ return join(rootDir, ".devcc", "devcc.log")
134
+ }
135
+
136
+ export function createLogger(rootDir: string, level?: LogLevel): Logger {
137
+ const envLevel = process.env.DEVCC_LOG_LEVEL
138
+ const resolved: LogLevel = level ?? (envLevel && isLogLevel(envLevel) ? envLevel : "info")
139
+ return new Logger({ level: resolved, filePath: defaultLogFilePath(rootDir) })
140
+ }