@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.
- package/LICENSE +15 -0
- package/README.md +659 -0
- package/bin/devcc.mjs +66 -0
- package/dist/devcc/app/App.d.ts +178 -0
- package/dist/devcc/app/App.d.ts.map +1 -0
- package/dist/devcc/app/App.js +1255 -0
- package/dist/devcc/app/App.js.map +1 -0
- package/dist/devcc/app/builtinCommands.d.ts +17 -0
- package/dist/devcc/app/builtinCommands.d.ts.map +1 -0
- package/dist/devcc/app/builtinCommands.js +389 -0
- package/dist/devcc/app/builtinCommands.js.map +1 -0
- package/dist/devcc/app/keymap.d.ts +21 -0
- package/dist/devcc/app/keymap.d.ts.map +1 -0
- package/dist/devcc/app/keymap.js +162 -0
- package/dist/devcc/app/keymap.js.map +1 -0
- package/dist/devcc/app/theme.d.ts +49 -0
- package/dist/devcc/app/theme.d.ts.map +1 -0
- package/dist/devcc/app/theme.js +68 -0
- package/dist/devcc/app/theme.js.map +1 -0
- package/dist/devcc/cli.d.ts +3 -0
- package/dist/devcc/cli.d.ts.map +1 -0
- package/dist/devcc/cli.js +113 -0
- package/dist/devcc/cli.js.map +1 -0
- package/dist/devcc/components/CommandPalette.d.ts +26 -0
- package/dist/devcc/components/CommandPalette.d.ts.map +1 -0
- package/dist/devcc/components/CommandPalette.js +113 -0
- package/dist/devcc/components/CommandPalette.js.map +1 -0
- package/dist/devcc/components/ConfirmDialog.d.ts +31 -0
- package/dist/devcc/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/devcc/components/ConfirmDialog.js +147 -0
- package/dist/devcc/components/ConfirmDialog.js.map +1 -0
- package/dist/devcc/components/Footer.d.ts +22 -0
- package/dist/devcc/components/Footer.d.ts.map +1 -0
- package/dist/devcc/components/Footer.js +72 -0
- package/dist/devcc/components/Footer.js.map +1 -0
- package/dist/devcc/components/Header.d.ts +12 -0
- package/dist/devcc/components/Header.d.ts.map +1 -0
- package/dist/devcc/components/Header.js +84 -0
- package/dist/devcc/components/Header.js.map +1 -0
- package/dist/devcc/components/HelpOverlay.d.ts +18 -0
- package/dist/devcc/components/HelpOverlay.d.ts.map +1 -0
- package/dist/devcc/components/HelpOverlay.js +48 -0
- package/dist/devcc/components/HelpOverlay.js.map +1 -0
- package/dist/devcc/components/Overlay.d.ts +31 -0
- package/dist/devcc/components/Overlay.d.ts.map +1 -0
- package/dist/devcc/components/Overlay.js +60 -0
- package/dist/devcc/components/Overlay.js.map +1 -0
- package/dist/devcc/components/ProjectPicker.d.ts +26 -0
- package/dist/devcc/components/ProjectPicker.d.ts.map +1 -0
- package/dist/devcc/components/ProjectPicker.js +114 -0
- package/dist/devcc/components/ProjectPicker.js.map +1 -0
- package/dist/devcc/components/Sidebar.d.ts +42 -0
- package/dist/devcc/components/Sidebar.d.ts.map +1 -0
- package/dist/devcc/components/Sidebar.js +127 -0
- package/dist/devcc/components/Sidebar.js.map +1 -0
- package/dist/devcc/components/StatusBadge.d.ts +16 -0
- package/dist/devcc/components/StatusBadge.d.ts.map +1 -0
- package/dist/devcc/components/StatusBadge.js +30 -0
- package/dist/devcc/components/StatusBadge.js.map +1 -0
- package/dist/devcc/components/primitives.d.ts +62 -0
- package/dist/devcc/components/primitives.d.ts.map +1 -0
- package/dist/devcc/components/primitives.js +169 -0
- package/dist/devcc/components/primitives.js.map +1 -0
- package/dist/devcc/components/styled.d.ts +16 -0
- package/dist/devcc/components/styled.d.ts.map +1 -0
- package/dist/devcc/components/styled.js +35 -0
- package/dist/devcc/components/styled.js.map +1 -0
- package/dist/devcc/core/commands.d.ts +89 -0
- package/dist/devcc/core/commands.d.ts.map +1 -0
- package/dist/devcc/core/commands.js +68 -0
- package/dist/devcc/core/commands.js.map +1 -0
- package/dist/devcc/core/config.d.ts +81 -0
- package/dist/devcc/core/config.d.ts.map +1 -0
- package/dist/devcc/core/config.js +313 -0
- package/dist/devcc/core/config.js.map +1 -0
- package/dist/devcc/core/events.d.ts +21 -0
- package/dist/devcc/core/events.d.ts.map +1 -0
- package/dist/devcc/core/events.js +27 -0
- package/dist/devcc/core/events.js.map +1 -0
- package/dist/devcc/core/state.d.ts +104 -0
- package/dist/devcc/core/state.d.ts.map +1 -0
- package/dist/devcc/core/state.js +139 -0
- package/dist/devcc/core/state.js.map +1 -0
- package/dist/devcc/deps/DependencyService.d.ts +50 -0
- package/dist/devcc/deps/DependencyService.d.ts.map +1 -0
- package/dist/devcc/deps/DependencyService.js +109 -0
- package/dist/devcc/deps/DependencyService.js.map +1 -0
- package/dist/devcc/deps/parse.d.ts +49 -0
- package/dist/devcc/deps/parse.d.ts.map +1 -0
- package/dist/devcc/deps/parse.js +158 -0
- package/dist/devcc/deps/parse.js.map +1 -0
- package/dist/devcc/git/GitService.d.ts +60 -0
- package/dist/devcc/git/GitService.d.ts.map +1 -0
- package/dist/devcc/git/GitService.js +160 -0
- package/dist/devcc/git/GitService.js.map +1 -0
- package/dist/devcc/git/parse.d.ts +57 -0
- package/dist/devcc/git/parse.d.ts.map +1 -0
- package/dist/devcc/git/parse.js +174 -0
- package/dist/devcc/git/parse.js.map +1 -0
- package/dist/devcc/index.d.ts +37 -0
- package/dist/devcc/index.d.ts.map +1 -0
- package/dist/devcc/index.js +36 -0
- package/dist/devcc/index.js.map +1 -0
- package/dist/devcc/logs/LogBuffer.d.ts +86 -0
- package/dist/devcc/logs/LogBuffer.d.ts.map +1 -0
- package/dist/devcc/logs/LogBuffer.js +200 -0
- package/dist/devcc/logs/LogBuffer.js.map +1 -0
- package/dist/devcc/logs/merge.d.ts +36 -0
- package/dist/devcc/logs/merge.d.ts.map +1 -0
- package/dist/devcc/logs/merge.js +71 -0
- package/dist/devcc/logs/merge.js.map +1 -0
- package/dist/devcc/processes/ManagedProcess.d.ts +129 -0
- package/dist/devcc/processes/ManagedProcess.d.ts.map +1 -0
- package/dist/devcc/processes/ManagedProcess.js +80 -0
- package/dist/devcc/processes/ManagedProcess.js.map +1 -0
- package/dist/devcc/processes/ProcessManager.d.ts +112 -0
- package/dist/devcc/processes/ProcessManager.d.ts.map +1 -0
- package/dist/devcc/processes/ProcessManager.js +525 -0
- package/dist/devcc/processes/ProcessManager.js.map +1 -0
- package/dist/devcc/processes/graph.d.ts +35 -0
- package/dist/devcc/processes/graph.d.ts.map +1 -0
- package/dist/devcc/processes/graph.js +68 -0
- package/dist/devcc/processes/graph.js.map +1 -0
- package/dist/devcc/projects/ProjectRegistry.d.ts +52 -0
- package/dist/devcc/projects/ProjectRegistry.d.ts.map +1 -0
- package/dist/devcc/projects/ProjectRegistry.js +177 -0
- package/dist/devcc/projects/ProjectRegistry.js.map +1 -0
- package/dist/devcc/services/ServiceManager.d.ts +48 -0
- package/dist/devcc/services/ServiceManager.d.ts.map +1 -0
- package/dist/devcc/services/ServiceManager.js +146 -0
- package/dist/devcc/services/ServiceManager.js.map +1 -0
- package/dist/devcc/services/docker.d.ts +37 -0
- package/dist/devcc/services/docker.d.ts.map +1 -0
- package/dist/devcc/services/docker.js +179 -0
- package/dist/devcc/services/docker.js.map +1 -0
- package/dist/devcc/services/node.d.ts +40 -0
- package/dist/devcc/services/node.d.ts.map +1 -0
- package/dist/devcc/services/node.js +107 -0
- package/dist/devcc/services/node.js.map +1 -0
- package/dist/devcc/services/ollama.d.ts +27 -0
- package/dist/devcc/services/ollama.d.ts.map +1 -0
- package/dist/devcc/services/ollama.js +66 -0
- package/dist/devcc/services/ollama.js.map +1 -0
- package/dist/devcc/services/postgres.d.ts +30 -0
- package/dist/devcc/services/postgres.d.ts.map +1 -0
- package/dist/devcc/services/postgres.js +47 -0
- package/dist/devcc/services/postgres.js.map +1 -0
- package/dist/devcc/services/redis.d.ts +20 -0
- package/dist/devcc/services/redis.d.ts.map +1 -0
- package/dist/devcc/services/redis.js +39 -0
- package/dist/devcc/services/redis.js.map +1 -0
- package/dist/devcc/services/types.d.ts +51 -0
- package/dist/devcc/services/types.d.ts.map +1 -0
- package/dist/devcc/services/types.js +10 -0
- package/dist/devcc/services/types.js.map +1 -0
- package/dist/devcc/system/SystemMonitor.d.ts +87 -0
- package/dist/devcc/system/SystemMonitor.d.ts.map +1 -0
- package/dist/devcc/system/SystemMonitor.js +142 -0
- package/dist/devcc/system/SystemMonitor.js.map +1 -0
- package/dist/devcc/utils/errors.d.ts +31 -0
- package/dist/devcc/utils/errors.d.ts.map +1 -0
- package/dist/devcc/utils/errors.js +52 -0
- package/dist/devcc/utils/errors.js.map +1 -0
- package/dist/devcc/utils/exec.d.ts +41 -0
- package/dist/devcc/utils/exec.d.ts.map +1 -0
- package/dist/devcc/utils/exec.js +112 -0
- package/dist/devcc/utils/exec.js.map +1 -0
- package/dist/devcc/utils/format.d.ts +18 -0
- package/dist/devcc/utils/format.d.ts.map +1 -0
- package/dist/devcc/utils/format.js +85 -0
- package/dist/devcc/utils/format.js.map +1 -0
- package/dist/devcc/utils/fuzzy.d.ts +12 -0
- package/dist/devcc/utils/fuzzy.d.ts.map +1 -0
- package/dist/devcc/utils/fuzzy.js +20 -0
- package/dist/devcc/utils/fuzzy.js.map +1 -0
- package/dist/devcc/utils/logger.d.ts +41 -0
- package/dist/devcc/utils/logger.d.ts.map +1 -0
- package/dist/devcc/utils/logger.js +117 -0
- package/dist/devcc/utils/logger.js.map +1 -0
- package/dist/devcc/utils/packageManager.d.ts +39 -0
- package/dist/devcc/utils/packageManager.d.ts.map +1 -0
- package/dist/devcc/utils/packageManager.js +85 -0
- package/dist/devcc/utils/packageManager.js.map +1 -0
- package/dist/devcc/utils/paths.d.ts +12 -0
- package/dist/devcc/utils/paths.d.ts.map +1 -0
- package/dist/devcc/utils/paths.js +39 -0
- package/dist/devcc/utils/paths.js.map +1 -0
- package/dist/devcc/views/DependenciesView.d.ts +34 -0
- package/dist/devcc/views/DependenciesView.d.ts.map +1 -0
- package/dist/devcc/views/DependenciesView.js +170 -0
- package/dist/devcc/views/DependenciesView.js.map +1 -0
- package/dist/devcc/views/GitView.d.ts +29 -0
- package/dist/devcc/views/GitView.d.ts.map +1 -0
- package/dist/devcc/views/GitView.js +226 -0
- package/dist/devcc/views/GitView.js.map +1 -0
- package/dist/devcc/views/LogsView.d.ts +39 -0
- package/dist/devcc/views/LogsView.d.ts.map +1 -0
- package/dist/devcc/views/LogsView.js +249 -0
- package/dist/devcc/views/LogsView.js.map +1 -0
- package/dist/devcc/views/OverviewView.d.ts +16 -0
- package/dist/devcc/views/OverviewView.d.ts.map +1 -0
- package/dist/devcc/views/OverviewView.js +135 -0
- package/dist/devcc/views/OverviewView.js.map +1 -0
- package/dist/devcc/views/ProcessesView.d.ts +26 -0
- package/dist/devcc/views/ProcessesView.d.ts.map +1 -0
- package/dist/devcc/views/ProcessesView.js +161 -0
- package/dist/devcc/views/ProcessesView.js.map +1 -0
- package/dist/devcc/views/ScriptsView.d.ts +30 -0
- package/dist/devcc/views/ScriptsView.d.ts.map +1 -0
- package/dist/devcc/views/ScriptsView.js +168 -0
- package/dist/devcc/views/ScriptsView.js.map +1 -0
- package/dist/devcc/views/ServicesView.d.ts +28 -0
- package/dist/devcc/views/ServicesView.d.ts.map +1 -0
- package/dist/devcc/views/ServicesView.js +153 -0
- package/dist/devcc/views/ServicesView.js.map +1 -0
- package/dist/devcc/views/SettingsView.d.ts +20 -0
- package/dist/devcc/views/SettingsView.d.ts.map +1 -0
- package/dist/devcc/views/SettingsView.js +114 -0
- package/dist/devcc/views/SettingsView.js.map +1 -0
- package/dist/devcc/views/SystemView.d.ts +13 -0
- package/dist/devcc/views/SystemView.d.ts.map +1 -0
- package/dist/devcc/views/SystemView.js +82 -0
- package/dist/devcc/views/SystemView.js.map +1 -0
- package/dist/devcc/views/View.d.ts +35 -0
- package/dist/devcc/views/View.d.ts.map +1 -0
- package/dist/devcc/views/View.js +42 -0
- package/dist/devcc/views/View.js.map +1 -0
- package/package.json +79 -0
- package/src/devcc/app/App.ts +1463 -0
- package/src/devcc/app/builtinCommands.ts +429 -0
- package/src/devcc/app/keymap.ts +167 -0
- package/src/devcc/app/theme.ts +83 -0
- package/src/devcc/cli.ts +122 -0
- package/src/devcc/components/CommandPalette.ts +136 -0
- package/src/devcc/components/ConfirmDialog.ts +174 -0
- package/src/devcc/components/Footer.ts +85 -0
- package/src/devcc/components/Header.ts +93 -0
- package/src/devcc/components/HelpOverlay.ts +62 -0
- package/src/devcc/components/Overlay.ts +91 -0
- package/src/devcc/components/ProjectPicker.ts +138 -0
- package/src/devcc/components/Sidebar.ts +163 -0
- package/src/devcc/components/StatusBadge.ts +36 -0
- package/src/devcc/components/primitives.ts +213 -0
- package/src/devcc/components/styled.ts +44 -0
- package/src/devcc/core/commands.ts +140 -0
- package/src/devcc/core/config.ts +405 -0
- package/src/devcc/core/events.ts +41 -0
- package/src/devcc/core/state.ts +236 -0
- package/src/devcc/deps/DependencyService.ts +152 -0
- package/src/devcc/deps/parse.ts +210 -0
- package/src/devcc/git/GitService.ts +205 -0
- package/src/devcc/git/parse.ts +203 -0
- package/src/devcc/index.ts +115 -0
- package/src/devcc/logs/LogBuffer.ts +237 -0
- package/src/devcc/logs/merge.ts +87 -0
- package/src/devcc/processes/ManagedProcess.ts +195 -0
- package/src/devcc/processes/ProcessManager.ts +625 -0
- package/src/devcc/processes/graph.ts +98 -0
- package/src/devcc/projects/ProjectRegistry.ts +219 -0
- package/src/devcc/services/ServiceManager.ts +180 -0
- package/src/devcc/services/docker.ts +220 -0
- package/src/devcc/services/node.ts +137 -0
- package/src/devcc/services/ollama.ts +80 -0
- package/src/devcc/services/postgres.ts +57 -0
- package/src/devcc/services/redis.ts +47 -0
- package/src/devcc/services/types.ts +59 -0
- package/src/devcc/system/SystemMonitor.ts +207 -0
- package/src/devcc/utils/errors.ts +64 -0
- package/src/devcc/utils/exec.ts +165 -0
- package/src/devcc/utils/format.ts +83 -0
- package/src/devcc/utils/fuzzy.ts +44 -0
- package/src/devcc/utils/logger.ts +140 -0
- package/src/devcc/utils/packageManager.ts +112 -0
- package/src/devcc/utils/paths.ts +39 -0
- package/src/devcc/views/DependenciesView.ts +206 -0
- package/src/devcc/views/GitView.ts +259 -0
- package/src/devcc/views/LogsView.ts +300 -0
- package/src/devcc/views/OverviewView.ts +187 -0
- package/src/devcc/views/ProcessesView.ts +177 -0
- package/src/devcc/views/ScriptsView.ts +187 -0
- package/src/devcc/views/ServicesView.ts +188 -0
- package/src/devcc/views/SettingsView.ts +146 -0
- package/src/devcc/views/SystemView.ts +110 -0
- 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
|
+
}
|