@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,237 @@
1
+ import { TypedEmitter } from "../core/events.js"
2
+
3
+ /** Default cap for a single source's in-memory history. */
4
+ export const MAX_LOG_LINES = 5_000
5
+
6
+ export type LogStream = "stdout" | "stderr" | "system"
7
+
8
+ export interface LogEntry {
9
+ /** Monotonic per-buffer sequence number; used for cheap "has it changed?" checks. */
10
+ seq: number
11
+ timestamp: Date
12
+ stream: LogStream
13
+ text: string
14
+ }
15
+
16
+ /**
17
+ * Fixed-capacity ring buffer.
18
+ *
19
+ * Memory is bounded by construction: once `capacity` lines are stored, each new
20
+ * line overwrites the oldest one. Nothing else in the app is allowed to retain
21
+ * log lines.
22
+ */
23
+ export class LogBuffer {
24
+ private readonly entries: (LogEntry | undefined)[]
25
+ private writeIndex = 0
26
+ private count = 0
27
+ private nextSeq = 0
28
+ private pendingPartial = { stdout: "", stderr: "", system: "" }
29
+
30
+ constructor(
31
+ readonly id: string,
32
+ readonly capacity: number = MAX_LOG_LINES,
33
+ ) {
34
+ if (capacity <= 0) throw new RangeError("LogBuffer capacity must be > 0")
35
+ this.entries = new Array<LogEntry | undefined>(capacity)
36
+ }
37
+
38
+ get size(): number {
39
+ return this.count
40
+ }
41
+
42
+ /** Total lines ever written, including evicted ones. */
43
+ get totalWritten(): number {
44
+ return this.nextSeq
45
+ }
46
+
47
+ /** True when the oldest lines have been dropped. */
48
+ get truncated(): boolean {
49
+ return this.nextSeq > this.capacity
50
+ }
51
+
52
+ push(text: string, stream: LogStream = "stdout", timestamp: Date = new Date()): LogEntry {
53
+ const entry: LogEntry = { seq: this.nextSeq++, timestamp, stream, text: stripControlSequences(text) }
54
+ this.entries[this.writeIndex] = entry
55
+ this.writeIndex = (this.writeIndex + 1) % this.capacity
56
+ if (this.count < this.capacity) this.count += 1
57
+ return entry
58
+ }
59
+
60
+ /**
61
+ * Feed a raw chunk from a child process. Splits on newlines and holds back an
62
+ * unterminated trailing fragment until the rest arrives.
63
+ */
64
+ pushChunk(chunk: string, stream: LogStream = "stdout"): LogEntry[] {
65
+ const combined = this.pendingPartial[stream] + chunk
66
+ const parts = combined.split(/\r?\n/u)
67
+ this.pendingPartial[stream] = parts.pop() ?? ""
68
+ return parts.map((line) => this.push(stripTrailingCarriage(line), stream))
69
+ }
70
+
71
+ /** Flush a held-back partial line (call when the stream closes). */
72
+ flush(stream?: LogStream): LogEntry[] {
73
+ const streams: LogStream[] = stream ? [stream] : ["stdout", "stderr", "system"]
74
+ const flushed: LogEntry[] = []
75
+ for (const name of streams) {
76
+ const partial = this.pendingPartial[name]
77
+ if (partial.length > 0) {
78
+ this.pendingPartial[name] = ""
79
+ flushed.push(this.push(stripTrailingCarriage(partial), name))
80
+ }
81
+ }
82
+ return flushed
83
+ }
84
+
85
+ /** Entries in chronological order. */
86
+ toArray(): LogEntry[] {
87
+ const result: LogEntry[] = []
88
+ const start = this.count < this.capacity ? 0 : this.writeIndex
89
+ for (let i = 0; i < this.count; i += 1) {
90
+ const entry = this.entries[(start + i) % this.capacity]
91
+ if (entry) result.push(entry)
92
+ }
93
+ return result
94
+ }
95
+
96
+ /** The most recent `n` entries, chronologically ordered. */
97
+ tail(n: number): LogEntry[] {
98
+ if (n <= 0) return []
99
+ const all = this.toArray()
100
+ return all.slice(Math.max(0, all.length - n))
101
+ }
102
+
103
+ /** A window of the chronological history, used by the scrolling log viewer. */
104
+ slice(offset: number, length: number): LogEntry[] {
105
+ const all = this.toArray()
106
+ const start = Math.max(0, Math.min(offset, all.length))
107
+ return all.slice(start, start + Math.max(0, length))
108
+ }
109
+
110
+ clear(): void {
111
+ this.entries.fill(undefined)
112
+ this.writeIndex = 0
113
+ this.count = 0
114
+ this.pendingPartial = { stdout: "", stderr: "", system: "" }
115
+ }
116
+ }
117
+
118
+ export interface LogSource {
119
+ id: string
120
+ label: string
121
+ }
122
+
123
+ export type LogStoreEvents = {
124
+ "log:append": [string, LogEntry[]]
125
+ "log:clear": [string]
126
+ "log:sources": [LogSource[]]
127
+ }
128
+
129
+ /**
130
+ * Owns one `LogBuffer` per source (managed process, script run, or devcc itself)
131
+ * and announces changes so the Logs view can update without polling.
132
+ */
133
+ export class LogStore extends TypedEmitter<LogStoreEvents> {
134
+ private readonly buffers = new Map<string, LogBuffer>()
135
+ private readonly labels = new Map<string, string>()
136
+
137
+ constructor(private readonly capacity: number = MAX_LOG_LINES) {
138
+ super()
139
+ }
140
+
141
+ register(id: string, label: string): LogBuffer {
142
+ const existing = this.buffers.get(id)
143
+ if (existing) {
144
+ if (this.labels.get(id) !== label) {
145
+ this.labels.set(id, label)
146
+ this.emit("log:sources", this.sources())
147
+ }
148
+ return existing
149
+ }
150
+ const buffer = new LogBuffer(id, this.capacity)
151
+ this.buffers.set(id, buffer)
152
+ this.labels.set(id, label)
153
+ this.emit("log:sources", this.sources())
154
+ return buffer
155
+ }
156
+
157
+ unregister(id: string): void {
158
+ if (this.buffers.delete(id)) {
159
+ this.labels.delete(id)
160
+ this.emit("log:sources", this.sources())
161
+ }
162
+ }
163
+
164
+ get(id: string): LogBuffer | undefined {
165
+ return this.buffers.get(id)
166
+ }
167
+
168
+ has(id: string): boolean {
169
+ return this.buffers.has(id)
170
+ }
171
+
172
+ sources(): LogSource[] {
173
+ return [...this.buffers.keys()].map((id) => ({ id, label: this.labels.get(id) ?? id }))
174
+ }
175
+
176
+ append(id: string, text: string, stream: LogStream = "system"): void {
177
+ const buffer = this.buffers.get(id) ?? this.register(id, id)
178
+ const entry = buffer.push(text, stream)
179
+ this.emit("log:append", id, [entry])
180
+ }
181
+
182
+ appendChunk(id: string, chunk: string, stream: LogStream): void {
183
+ const buffer = this.buffers.get(id) ?? this.register(id, id)
184
+ const entries = buffer.pushChunk(chunk, stream)
185
+ if (entries.length > 0) this.emit("log:append", id, entries)
186
+ }
187
+
188
+ flush(id: string): void {
189
+ const buffer = this.buffers.get(id)
190
+ if (!buffer) return
191
+ const entries = buffer.flush()
192
+ if (entries.length > 0) this.emit("log:append", id, entries)
193
+ }
194
+
195
+ clear(id: string): void {
196
+ this.buffers.get(id)?.clear()
197
+ this.emit("log:clear", id)
198
+ }
199
+
200
+ clearAll(): void {
201
+ for (const id of this.buffers.keys()) this.clear(id)
202
+ }
203
+ }
204
+
205
+ function stripTrailingCarriage(line: string): string {
206
+ return line.endsWith("\r") ? line.slice(0, -1) : line
207
+ }
208
+
209
+ const ESC = String.fromCharCode(27)
210
+ const BEL = String.fromCharCode(7)
211
+ /** CSI sequences (`ESC [ ... final`), including screen clears and cursor moves. */
212
+ const CSI_SEQUENCE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, "gu")
213
+ /** OSC sequences (`ESC ] ... BEL` or `ESC ] ... ESC \\`), e.g. window titles and hyperlinks. */
214
+ const OSC_SEQUENCE = new RegExp(`${ESC}\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)`, "gu")
215
+ /**
216
+ * Two-byte escapes such as `ESC c` (reset) and `ESC 7`/`ESC 8` (save/restore
217
+ * cursor). Any printable after a bare ESC is an escape by definition.
218
+ */
219
+ const SIMPLE_ESCAPE = new RegExp(`${ESC}[0-~]`, "gu")
220
+ /** Remaining C0 controls apart from tab. Matching control characters is the point here. */
221
+ // eslint-disable-next-line no-control-regex
222
+ const C0_CONTROLS = /[\u0000-\u0008\u000b-\u001f\u007f]/gu
223
+
224
+ /**
225
+ * Remove terminal control sequences from a captured line.
226
+ *
227
+ * Children are told `FORCE_COLOR=0`, but watchers such as `tsc --watch` still
228
+ * clear the screen and move the cursor; rendered verbatim those bytes would
229
+ * corrupt the viewer (or, in the merged stream, someone else's line).
230
+ */
231
+ export function stripControlSequences(text: string): string {
232
+ return text
233
+ .replace(CSI_SEQUENCE, "")
234
+ .replace(OSC_SEQUENCE, "")
235
+ .replace(SIMPLE_ESCAPE, "")
236
+ .replace(C0_CONTROLS, "")
237
+ }
@@ -0,0 +1,87 @@
1
+ import type { LogBuffer, LogEntry } from "./LogBuffer.js"
2
+
3
+ /** A log line that knows which source it came from. */
4
+ export interface SourcedLogEntry {
5
+ sourceId: string
6
+ entry: LogEntry
7
+ }
8
+
9
+ /** The synthetic source id that shows every buffer merged into one stream. */
10
+ export const ALL_SOURCES = "*"
11
+
12
+ /** Upper bound on how many lines the merged stream materialises at once. */
13
+ export const MERGED_LIMIT = 2_000
14
+
15
+ /**
16
+ * Merge the tails of several chronological buffers into one chronological
17
+ * stream, bounded by `limit` lines in total.
18
+ *
19
+ * Each buffer is already ordered, so taking the last `limit` of each and
20
+ * sorting once is both simple and cheap enough for a few thousand lines.
21
+ * Ties on timestamp fall back to sequence number, which is per-buffer, so a
22
+ * stable sort keeps the input order between buffers.
23
+ */
24
+ export function mergeTails(buffers: readonly LogBuffer[], limit: number = MERGED_LIMIT): SourcedLogEntry[] {
25
+ if (limit <= 0) return []
26
+
27
+ const combined: SourcedLogEntry[] = []
28
+ for (const buffer of buffers) {
29
+ for (const entry of buffer.tail(limit)) combined.push({ sourceId: buffer.id, entry })
30
+ }
31
+
32
+ combined.sort((a, b) => {
33
+ const byTime = a.entry.timestamp.getTime() - b.entry.timestamp.getTime()
34
+ if (byTime !== 0) return byTime
35
+ if (a.sourceId === b.sourceId) return a.entry.seq - b.entry.seq
36
+ return 0
37
+ })
38
+
39
+ return combined.length > limit ? combined.slice(combined.length - limit) : combined
40
+ }
41
+
42
+ export interface LogFilter {
43
+ /** Case-insensitive substring, or a regular expression if it parses as one. */
44
+ query: string
45
+ /** Show only stderr lines. */
46
+ errorsOnly: boolean
47
+ }
48
+
49
+ export const EMPTY_LOG_FILTER: LogFilter = { query: "", errorsOnly: false }
50
+
51
+ /**
52
+ * Compile a user query. Anything that fails to parse as a regex is treated as
53
+ * a literal, so typing `(` never breaks the view.
54
+ */
55
+ export function compileQuery(query: string): ((text: string) => boolean) | null {
56
+ const trimmed = query.trim()
57
+ if (trimmed === "") return null
58
+
59
+ try {
60
+ const pattern = new RegExp(trimmed, "iu")
61
+ return (text) => pattern.test(text)
62
+ } catch {
63
+ const needle = trimmed.toLowerCase()
64
+ return (text) => text.toLowerCase().includes(needle)
65
+ }
66
+ }
67
+
68
+ export function filterEntries(entries: readonly SourcedLogEntry[], filter: LogFilter): SourcedLogEntry[] {
69
+ const matches = compileQuery(filter.query)
70
+ if (!matches && !filter.errorsOnly) return [...entries]
71
+
72
+ return entries.filter(({ entry }) => {
73
+ if (filter.errorsOnly && entry.stream !== "stderr") return false
74
+ return matches ? matches(entry.text) : true
75
+ })
76
+ }
77
+
78
+ export function isFilterActive(filter: LogFilter): boolean {
79
+ return filter.query.trim() !== "" || filter.errorsOnly
80
+ }
81
+
82
+ export function describeFilter(filter: LogFilter): string {
83
+ const parts: string[] = []
84
+ if (filter.query.trim() !== "") parts.push(`/${filter.query.trim()}/`)
85
+ if (filter.errorsOnly) parts.push("errors only")
86
+ return parts.join(" · ")
87
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Types and pure helpers describing a process Dev Command Center knows about.
3
+ *
4
+ * `ownership` is the safety boundary: `"managed"` processes were launched by us
5
+ * and may be signalled; `"detected"` processes were merely observed on the host
6
+ * and must never be killed by devcc.
7
+ */
8
+
9
+ export type ProcessStatus = "stopped" | "starting" | "running" | "stopping" | "failed"
10
+
11
+ export type ProcessOwnership = "managed" | "detected"
12
+
13
+ /** `service` processes are long-running; `task` processes are expected to exit. */
14
+ export type ProcessKind = "service" | "task"
15
+
16
+ export interface ProcessDefinition {
17
+ id: string
18
+ name: string
19
+ command: string
20
+ args?: string[]
21
+ cwd?: string
22
+ env?: Record<string, string>
23
+ kind?: ProcessKind
24
+ /** Start automatically when devcc launches. */
25
+ autoStart?: boolean
26
+ /** Informational URL shown next to the process (e.g. `localhost:3000`). */
27
+ url?: string
28
+ /** Port to probe for a readiness indicator. */
29
+ port?: number
30
+ /** Milliseconds to wait after SIGTERM before escalating to SIGKILL. */
31
+ killGraceMs?: number
32
+ /**
33
+ * Bring the process back up automatically when it exits non-zero.
34
+ * Opt-in per process: devcc never resurrects something you stopped yourself.
35
+ */
36
+ restartOnCrash?: boolean
37
+ /** Give up after this many consecutive crashes (default 5). */
38
+ maxCrashRestarts?: number
39
+ /** First backoff delay; doubles per consecutive crash (default 1000ms). */
40
+ crashBackoffMs?: number
41
+ /** Upper bound on the backoff delay (default 30000ms). */
42
+ maxCrashBackoffMs?: number
43
+ /**
44
+ * A run lasting at least this long is considered healthy, which resets the
45
+ * crash counter (default 30000ms).
46
+ */
47
+ healthyAfterMs?: number
48
+ /**
49
+ * Ids of processes that must be *ready* before this one starts. Used by
50
+ * "start all" and autostart to bring a stack up in order.
51
+ */
52
+ dependsOn?: string[]
53
+ /**
54
+ * How devcc decides the process is usable. Until it passes, the process is
55
+ * reported as `starting` rather than `running`. Without one, a process is
56
+ * considered ready the moment it has spawned.
57
+ */
58
+ readiness?: ReadinessCheck
59
+ }
60
+
61
+ export interface ReadinessCheck {
62
+ /** Ready once something accepts a TCP connection on this port. */
63
+ port?: number
64
+ host?: string
65
+ /** Ready once a line of output matches this regular expression. */
66
+ logPattern?: string
67
+ /** Give up waiting after this long and report `running` anyway (default 30000). */
68
+ timeoutMs?: number
69
+ /** How often to probe the port (default 250). */
70
+ intervalMs?: number
71
+ }
72
+
73
+ export const DEFAULT_READINESS = {
74
+ timeoutMs: 30_000,
75
+ intervalMs: 250,
76
+ } as const
77
+
78
+ export const DEFAULT_CRASH_POLICY = {
79
+ maxCrashRestarts: 5,
80
+ crashBackoffMs: 1_000,
81
+ maxCrashBackoffMs: 30_000,
82
+ healthyAfterMs: 30_000,
83
+ } as const
84
+
85
+ export interface ManagedProcess {
86
+ id: string
87
+ name: string
88
+ command: string
89
+ args?: string[]
90
+ cwd?: string
91
+ status: ProcessStatus
92
+ pid?: number
93
+ startedAt?: Date
94
+ exitCode?: number
95
+ /** Signal that terminated the process, when it was signalled. */
96
+ exitSignal?: string
97
+ stoppedAt?: Date
98
+ kind: ProcessKind
99
+ ownership: ProcessOwnership
100
+ url?: string
101
+ port?: number
102
+ /** How many times the user has restarted it this session. */
103
+ restarts: number
104
+ /** Consecutive automatic restarts since the last healthy run. */
105
+ crashRestarts: number
106
+ /** When an automatic restart is scheduled, the time it will happen. */
107
+ retryAt?: Date
108
+ /** True once devcc has given up automatically restarting this process. */
109
+ crashRestartsExhausted?: boolean
110
+ /** Last failure message, cleared on a successful start. */
111
+ lastError?: string
112
+ /** Ids this process waits for before starting. */
113
+ dependsOn: string[]
114
+ /** True once the readiness check has passed (or there is none). */
115
+ ready: boolean
116
+ /** Set when the readiness check timed out and the process was reported anyway. */
117
+ readinessTimedOut?: boolean
118
+ }
119
+
120
+ export const ACTIVE_STATUSES: readonly ProcessStatus[] = ["starting", "running", "stopping"]
121
+
122
+ export function isActive(status: ProcessStatus): boolean {
123
+ return ACTIVE_STATUSES.includes(status)
124
+ }
125
+
126
+ const TRANSITIONS: Record<ProcessStatus, readonly ProcessStatus[]> = {
127
+ stopped: ["starting"],
128
+ starting: ["running", "failed", "stopping", "stopped"],
129
+ running: ["stopping", "stopped", "failed"],
130
+ stopping: ["stopped", "failed"],
131
+ failed: ["starting"],
132
+ }
133
+
134
+ /** Whether a status change is legal. Keeps the manager's bookkeeping honest. */
135
+ export function canTransition(from: ProcessStatus, to: ProcessStatus): boolean {
136
+ if (from === to) return false
137
+ return TRANSITIONS[from].includes(to)
138
+ }
139
+
140
+ /** Build the initial snapshot for a definition. */
141
+ export function createManagedProcess(
142
+ definition: ProcessDefinition,
143
+ ownership: ProcessOwnership = "managed",
144
+ ): ManagedProcess {
145
+ return {
146
+ id: definition.id,
147
+ name: definition.name,
148
+ command: definition.command,
149
+ args: definition.args ? [...definition.args] : undefined,
150
+ cwd: definition.cwd,
151
+ status: "stopped",
152
+ kind: definition.kind ?? "service",
153
+ ownership,
154
+ url: definition.url,
155
+ port: definition.port,
156
+ restarts: 0,
157
+ crashRestarts: 0,
158
+ dependsOn: definition.dependsOn ? [...definition.dependsOn] : [],
159
+ ready: false,
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Exponential backoff for automatic restarts.
165
+ *
166
+ * `attempt` is 1-based: the first restart waits `base`, the second `2 * base`,
167
+ * and so on, capped at `max`. Keeping this a pure function makes the policy
168
+ * testable without spawning anything.
169
+ */
170
+ export function crashBackoffDelay(
171
+ attempt: number,
172
+ base: number = DEFAULT_CRASH_POLICY.crashBackoffMs,
173
+ max: number = DEFAULT_CRASH_POLICY.maxCrashBackoffMs,
174
+ ): number {
175
+ if (attempt <= 0) return 0
176
+ const safeBase = Math.max(0, base)
177
+ const exponent = Math.min(attempt - 1, 30)
178
+ return Math.min(Math.max(0, max), safeBase * 2 ** exponent)
179
+ }
180
+
181
+ /** Whether a crashed process should be brought back up automatically. */
182
+ export function shouldAutoRestart(
183
+ definition: Pick<ProcessDefinition, "restartOnCrash" | "maxCrashRestarts">,
184
+ snapshot: Pick<ManagedProcess, "crashRestarts">,
185
+ ): boolean {
186
+ if (!definition.restartOnCrash) return false
187
+ const limit = definition.maxCrashRestarts ?? DEFAULT_CRASH_POLICY.maxCrashRestarts
188
+ return snapshot.crashRestarts < limit
189
+ }
190
+
191
+ /** Human-readable one-liner of what will actually be executed. */
192
+ export function describeCommand(process: Pick<ManagedProcess, "command" | "args">): string {
193
+ const args = process.args ?? []
194
+ return args.length > 0 ? `${process.command} ${args.join(" ")}` : process.command
195
+ }