@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,300 @@
1
+ import type { KeyEvent } from "@opentui/core"
2
+ import { BoxRenderable, TextRenderable } from "@opentui/core"
3
+ import { theme } from "../app/theme.js"
4
+ import type { KeyHint } from "../components/Footer.js"
5
+ import { RowList, type ListRow } from "../components/primitives.js"
6
+ import { chunk, dim, styled } from "../components/styled.js"
7
+ import { updateUi, type AppState, type ViewId } from "../core/state.js"
8
+ import type { LogBuffer } from "../logs/LogBuffer.js"
9
+ import {
10
+ ALL_SOURCES,
11
+ describeFilter,
12
+ EMPTY_LOG_FILTER,
13
+ filterEntries,
14
+ isFilterActive,
15
+ mergeTails,
16
+ type LogFilter,
17
+ type SourcedLogEntry,
18
+ } from "../logs/merge.js"
19
+ import { fit, formatTimestamp } from "../utils/format.js"
20
+ import { View, type ViewDeps } from "./View.js"
21
+
22
+ const PAGE_SIZE = 15
23
+
24
+ /** Stable per-source colours for the merged stream. */
25
+ const SOURCE_COLOURS = [
26
+ "#6aa9ff",
27
+ "#4cc38a",
28
+ "#d9a441",
29
+ "#c77dff",
30
+ "#3fc1c9",
31
+ "#f28c6a",
32
+ "#9ad36b",
33
+ "#f2a2c9",
34
+ ]
35
+
36
+ /**
37
+ * Log viewer: per-source buffers or one merged stream, with follow mode,
38
+ * manual scrolling and a search filter.
39
+ *
40
+ * The viewer never keeps its own copy of the history. Per-source views window
41
+ * straight into that source's ring buffer; the merged view materialises at most
42
+ * `MERGED_LIMIT` recent lines on demand, so memory stays bounded either way.
43
+ */
44
+ export class LogsView extends View {
45
+ readonly id: ViewId = "logs"
46
+ readonly title = "Logs"
47
+
48
+ private readonly header: TextRenderable
49
+ private readonly filterLine: TextRenderable
50
+ private readonly list: RowList
51
+ private scrollOffset = 0
52
+ private follow = true
53
+ private sourceIds: string[] = []
54
+ private currentSource: string | null = null
55
+ private entryCount = 0
56
+ private filter: LogFilter = { ...EMPTY_LOG_FILTER }
57
+ private colourIndex = new Map<string, number>()
58
+
59
+ constructor(deps: ViewDeps) {
60
+ super(deps)
61
+
62
+ const headerBox = new BoxRenderable(deps.ctx, {
63
+ height: 3,
64
+ flexShrink: 0,
65
+ flexDirection: "column",
66
+ border: ["bottom"],
67
+ borderColor: theme.border,
68
+ backgroundColor: "transparent",
69
+ })
70
+ this.header = new TextRenderable(deps.ctx, { content: "", height: 1, selectable: false, truncate: true })
71
+ this.filterLine = new TextRenderable(deps.ctx, {
72
+ content: "",
73
+ height: 1,
74
+ selectable: false,
75
+ truncate: true,
76
+ })
77
+ headerBox.add(this.header)
78
+ headerBox.add(this.filterLine)
79
+
80
+ // The viewer slices the history itself, so it must recompute that slice
81
+ // whenever the viewport height changes.
82
+ this.list = new RowList(deps.ctx, {
83
+ flexGrow: 1,
84
+ marginTop: 1,
85
+ scrollPadding: 0,
86
+ onViewportChange: () => this.update(deps.app.store.getState()),
87
+ })
88
+
89
+ this.box.add(headerBox)
90
+ this.box.add(this.list.box)
91
+ }
92
+
93
+ update(state: AppState): void {
94
+ // The merged stream is offered first, so tab from it lands on the first real source.
95
+ this.sourceIds = [ALL_SOURCES, ...state.logSources.map((source) => source.id)]
96
+ this.follow = state.ui.logFollow
97
+
98
+ const sourceId = state.ui.logSource ?? this.sourceIds[1] ?? ALL_SOURCES
99
+ if (sourceId !== this.currentSource) {
100
+ this.currentSource = sourceId
101
+ this.scrollOffset = 0
102
+ }
103
+
104
+ const merged = sourceId === ALL_SOURCES
105
+ const entries = this.collect(sourceId, state)
106
+ this.entryCount = entries.length
107
+
108
+ const visible = Math.max(1, this.list.visibleCount)
109
+ const maxOffset = Math.max(0, entries.length - visible)
110
+ if (this.follow) this.scrollOffset = maxOffset
111
+ this.scrollOffset = Math.min(this.scrollOffset, maxOffset)
112
+
113
+ const window = entries.slice(this.scrollOffset, this.scrollOffset + visible)
114
+ const width = state.ui.width - (state.ui.sidebarVisible ? 26 : 4)
115
+ const rows: ListRow[] = window.map((item) => ({
116
+ key: `${item.sourceId}:${item.entry.seq}`,
117
+ disabled: true,
118
+ content: this.renderEntry(item, width, merged),
119
+ }))
120
+
121
+ if (rows.length === 0) {
122
+ rows.push({
123
+ key: "__empty",
124
+ disabled: true,
125
+ content: styled(
126
+ dim(
127
+ isFilterActive(this.filter)
128
+ ? "Nothing matches the current filter"
129
+ : state.logSources.length === 0
130
+ ? "No log sources - start a process or run a script"
131
+ : "No output yet",
132
+ ),
133
+ ),
134
+ })
135
+ }
136
+
137
+ this.list.setRows(rows, -1)
138
+
139
+ const label = merged
140
+ ? `all sources (${state.logSources.length})`
141
+ : (state.logSources.find((source) => source.id === sourceId)?.label ?? sourceId)
142
+ const position =
143
+ entries.length === 0
144
+ ? "0/0"
145
+ : `${Math.min(this.scrollOffset + visible, entries.length)}/${entries.length}`
146
+ const buffer = merged ? undefined : this.deps.app.logs.get(sourceId)
147
+
148
+ this.header.content = styled(
149
+ dim(fit("source", 8)),
150
+ chunk(fit(label, 26), theme.textBright),
151
+ this.follow ? chunk("● follow", theme.success) : dim("‖ paused"),
152
+ dim(` ${position}`),
153
+ buffer?.truncated ? dim(" (older lines dropped)") : dim(""),
154
+ )
155
+ this.filterLine.content = isFilterActive(this.filter)
156
+ ? styled(dim(fit("filter", 8)), chunk(describeFilter(this.filter), theme.warning), dim(" x clears"))
157
+ : styled(dim(fit("filter", 8)), dim("none s search e errors only a all sources"))
158
+ }
159
+
160
+ override handleKey(key: KeyEvent, state: AppState): boolean {
161
+ switch (key.name) {
162
+ case "f":
163
+ updateUi(this.deps.app.store, { logFollow: !state.ui.logFollow })
164
+ return true
165
+ case "c":
166
+ if (this.currentSource === ALL_SOURCES) {
167
+ void this.deps.commands.run("logs.clearAll", this.deps.app)
168
+ } else if (this.currentSource) {
169
+ this.deps.app.logs.clear(this.currentSource)
170
+ this.deps.app.notify("info", "Cleared logs")
171
+ }
172
+ return true
173
+ case "s":
174
+ void this.promptFilter()
175
+ return true
176
+ case "x":
177
+ this.setFilter({ ...this.filter, query: "" })
178
+ return true
179
+ case "e":
180
+ this.setFilter({ ...this.filter, errorsOnly: !this.filter.errorsOnly })
181
+ return true
182
+ case "a":
183
+ updateUi(this.deps.app.store, { logSource: ALL_SOURCES })
184
+ return true
185
+ case "tab":
186
+ case "right":
187
+ case "l":
188
+ this.cycleSource(1)
189
+ return true
190
+ case "left":
191
+ case "h":
192
+ this.cycleSource(-1)
193
+ return true
194
+ case "j":
195
+ case "down":
196
+ this.scroll(1)
197
+ return true
198
+ case "k":
199
+ case "up":
200
+ this.scroll(-1)
201
+ return true
202
+ case "pagedown":
203
+ this.scroll(PAGE_SIZE)
204
+ return true
205
+ case "pageup":
206
+ this.scroll(-PAGE_SIZE)
207
+ return true
208
+ case "end":
209
+ this.scrollOffset = Number.MAX_SAFE_INTEGER
210
+ updateUi(this.deps.app.store, { logFollow: true })
211
+ return true
212
+ case "home":
213
+ this.scrollOffset = 0
214
+ updateUi(this.deps.app.store, { logFollow: false })
215
+ return true
216
+ default:
217
+ return false
218
+ }
219
+ }
220
+
221
+ override hints(): KeyHint[] {
222
+ return [
223
+ { keys: "j/k", label: "scroll" },
224
+ { keys: "s", label: "search" },
225
+ { keys: "e", label: "errors" },
226
+ { keys: "a", label: "all sources" },
227
+ { keys: "tab", label: "next source" },
228
+ { keys: "f", label: "follow" },
229
+ { keys: "c", label: "clear" },
230
+ ]
231
+ }
232
+
233
+ /** Entries for the selected source, filtered. */
234
+ private collect(sourceId: string, state: AppState): SourcedLogEntry[] {
235
+ let entries: SourcedLogEntry[]
236
+ if (sourceId === ALL_SOURCES) {
237
+ const buffers = state.logSources
238
+ .map((source) => this.deps.app.logs.get(source.id))
239
+ .filter((buffer): buffer is LogBuffer => buffer !== undefined)
240
+ entries = mergeTails(buffers)
241
+ } else {
242
+ const buffer = this.deps.app.logs.get(sourceId)
243
+ entries = (buffer?.toArray() ?? []).map((entry) => ({ sourceId, entry }))
244
+ }
245
+ return filterEntries(entries, this.filter)
246
+ }
247
+
248
+ private renderEntry(item: SourcedLogEntry, width: number, merged: boolean) {
249
+ const { entry, sourceId } = item
250
+ const colour =
251
+ entry.stream === "stderr" ? theme.danger : entry.stream === "system" ? theme.accent : theme.text
252
+ const sourceWidth = merged ? 12 : 0
253
+ const available = Math.max(10, width - 14 - sourceWidth)
254
+ return styled(
255
+ dim(fit(formatTimestamp(entry.timestamp), 13)),
256
+ merged ? chunk(fit(sourceId, sourceWidth), this.colourFor(sourceId)) : "",
257
+ chunk(fit(entry.text, available), colour),
258
+ )
259
+ }
260
+
261
+ private colourFor(sourceId: string): string {
262
+ let index = this.colourIndex.get(sourceId)
263
+ if (index === undefined) {
264
+ index = this.colourIndex.size % SOURCE_COLOURS.length
265
+ this.colourIndex.set(sourceId, index)
266
+ }
267
+ return SOURCE_COLOURS[index] ?? theme.text
268
+ }
269
+
270
+ private async promptFilter(): Promise<void> {
271
+ const value = await this.deps.app.prompt(
272
+ "Filter logs (regex or text)",
273
+ this.filter.query || "e.g. error|warn",
274
+ )
275
+ if (value !== null) this.setFilter({ ...this.filter, query: value })
276
+ }
277
+
278
+ private setFilter(filter: LogFilter): void {
279
+ this.filter = filter
280
+ this.scrollOffset = 0
281
+ updateUi(this.deps.app.store, { logFollow: true })
282
+ this.update(this.deps.app.store.getState())
283
+ }
284
+
285
+ private scroll(delta: number): void {
286
+ const visible = Math.max(1, this.list.visibleCount)
287
+ const maxOffset = Math.max(0, this.entryCount - visible)
288
+ this.scrollOffset = Math.max(0, Math.min(maxOffset, this.scrollOffset + delta))
289
+ // Scrolling up pauses follow; scrolling to the bottom resumes it.
290
+ updateUi(this.deps.app.store, { logFollow: this.scrollOffset >= maxOffset })
291
+ this.update(this.deps.app.store.getState())
292
+ }
293
+
294
+ private cycleSource(direction: 1 | -1): void {
295
+ if (this.sourceIds.length === 0) return
296
+ const current = this.currentSource ? this.sourceIds.indexOf(this.currentSource) : -1
297
+ const next = (current + direction + this.sourceIds.length) % this.sourceIds.length
298
+ updateUi(this.deps.app.store, { logSource: this.sourceIds[next] ?? null })
299
+ }
300
+ }
@@ -0,0 +1,187 @@
1
+ import { theme } from "../app/theme.js"
2
+ import type { KeyHint } from "../components/Footer.js"
3
+ import { RowList, type ListRow } from "../components/primitives.js"
4
+ import { processGlyph, serviceGlyph } from "../components/StatusBadge.js"
5
+ import { chunk, dim, styled } from "../components/styled.js"
6
+ import { summariseStatus, summariseTracking } from "../git/parse.js"
7
+ import { isActive } from "../processes/ManagedProcess.js"
8
+ import type { AppState, ViewId } from "../core/state.js"
9
+ import { fit, formatDuration, meter, toGigabytes } from "../utils/format.js"
10
+ import { View, type ViewDeps } from "./View.js"
11
+
12
+ /**
13
+ * The landing pane: one screenful answering "is everything up, and is my tree
14
+ * clean?". Every row here is a summary of a dedicated view.
15
+ */
16
+ export class OverviewView extends View {
17
+ readonly id: ViewId = "overview"
18
+ readonly title = "Overview"
19
+
20
+ private readonly list: RowList
21
+
22
+ constructor(deps: ViewDeps) {
23
+ super(deps)
24
+ this.list = new RowList(deps.ctx, { flexGrow: 1, scrollPadding: 0 })
25
+ this.box.add(this.list.box)
26
+ }
27
+
28
+ update(state: AppState): void {
29
+ const rows: ListRow[] = []
30
+ const compact = state.ui.height < 26
31
+ const labelWidth = 14
32
+
33
+ // --- Git -------------------------------------------------------------
34
+ rows.push(section("Git"))
35
+ if (state.git.lastRefreshedAt === null) {
36
+ rows.push(info("git", "checking…"))
37
+ } else if (!state.git.available) {
38
+ rows.push(info("git", "not installed"))
39
+ } else if (!state.git.isRepository) {
40
+ rows.push(info("repository", "not a git repository"))
41
+ } else {
42
+ const branch = state.git.status.branch ?? `detached @ ${state.git.status.head ?? "?"}`
43
+ rows.push(info("branch", branch, labelWidth))
44
+ rows.push(
45
+ infoStyled(
46
+ "status",
47
+ styled(
48
+ chunk(summariseStatus(state.git.status), state.git.status.clean ? theme.success : theme.warning),
49
+ ),
50
+ labelWidth,
51
+ ),
52
+ )
53
+ rows.push(info("tracking", summariseTracking(state.git.status), labelWidth))
54
+ }
55
+
56
+ // --- Development processes -------------------------------------------
57
+ rows.push(blank())
58
+ rows.push(section("Development"))
59
+ if (state.processes.length === 0) {
60
+ rows.push(info("", "no managed processes - add some in .devcc.ts"))
61
+ } else {
62
+ for (const process of state.processes.slice(0, compact ? 4 : 8)) {
63
+ const detail = process.url ?? (process.pid ? `pid ${process.pid}` : "")
64
+ const uptime =
65
+ process.startedAt && isActive(process.status)
66
+ ? formatDuration(Date.now() - process.startedAt.getTime())
67
+ : ""
68
+ rows.push({
69
+ key: `process-${process.id}`,
70
+ disabled: true,
71
+ content: styled(
72
+ " ",
73
+ processGlyph(process.status).chunks[0] ?? chunk(" "),
74
+ " ",
75
+ chunk(fit(process.name, 16), theme.text),
76
+ dim(fit(process.status, 10)),
77
+ dim(fit(detail, 22)),
78
+ dim(uptime),
79
+ ),
80
+ })
81
+ }
82
+ }
83
+
84
+ // --- Services ---------------------------------------------------------
85
+ const services = state.services.filter((service) => service.status.state !== "unknown")
86
+ if (services.length > 0) {
87
+ rows.push(blank())
88
+ rows.push(section("Services"))
89
+ for (const service of services.slice(0, compact ? 3 : 6)) {
90
+ rows.push({
91
+ key: `service-${service.id}`,
92
+ disabled: true,
93
+ content: styled(
94
+ " ",
95
+ serviceGlyph(service.status.state).chunks[0] ?? chunk(" "),
96
+ " ",
97
+ chunk(fit(service.name, 16), theme.text),
98
+ dim(fit(service.status.detail ?? service.status.state, 26)),
99
+ dim(service.status.endpoint ?? ""),
100
+ ),
101
+ })
102
+ }
103
+ }
104
+
105
+ // --- System -----------------------------------------------------------
106
+ rows.push(blank())
107
+ rows.push(section("System"))
108
+ const metrics = state.system
109
+ if (!metrics) {
110
+ rows.push(info("", "sampling…"))
111
+ } else {
112
+ rows.push(
113
+ infoStyled(
114
+ "cpu",
115
+ styled(
116
+ chunk(meter(metrics.cpu, 12), usageColor(metrics.cpu)),
117
+ dim(` ${(metrics.cpu * 100).toFixed(0)}%`),
118
+ dim(` load ${metrics.loadAverage[0].toFixed(2)}`),
119
+ ),
120
+ labelWidth,
121
+ ),
122
+ )
123
+ rows.push(
124
+ infoStyled(
125
+ "memory",
126
+ styled(
127
+ chunk(meter(metrics.memoryRatio, 12), usageColor(metrics.memoryRatio)),
128
+ dim(` ${toGigabytes(metrics.memoryUsed)} / ${toGigabytes(metrics.memoryTotal)} GB`),
129
+ ),
130
+ labelWidth,
131
+ ),
132
+ )
133
+ if (!compact) {
134
+ rows.push(info("uptime", formatDuration(metrics.uptimeMs), labelWidth))
135
+ rows.push(info("node", `${state.project.host.runtime}`, labelWidth))
136
+ }
137
+ }
138
+
139
+ this.list.setRows(rows, -1)
140
+ }
141
+
142
+ override hints(): KeyHint[] {
143
+ return [
144
+ { keys: "↑↓", label: "navigate" },
145
+ { keys: "p", label: "processes" },
146
+ { keys: "g", label: "git" },
147
+ { keys: "l", label: "logs" },
148
+ { keys: "/", label: "commands" },
149
+ { keys: "?", label: "help" },
150
+ { keys: "q", label: "quit" },
151
+ ]
152
+ }
153
+ }
154
+
155
+ function section(title: string): ListRow {
156
+ return {
157
+ key: `section-${title}`,
158
+ disabled: true,
159
+ content: styled(chunk(title.toUpperCase(), theme.accent)),
160
+ }
161
+ }
162
+
163
+ function blank(): ListRow {
164
+ return { key: `blank-${Math.random()}`, disabled: true, content: "" }
165
+ }
166
+
167
+ function info(label: string, value: string, labelWidth = 14): ListRow {
168
+ return {
169
+ key: `info-${label}-${value}`,
170
+ disabled: true,
171
+ content: styled(" ", dim(fit(label, labelWidth)), chunk(value, theme.text)),
172
+ }
173
+ }
174
+
175
+ function infoStyled(label: string, value: ReturnType<typeof styled>, labelWidth = 14): ListRow {
176
+ return {
177
+ key: `info-${label}`,
178
+ disabled: true,
179
+ content: styled(" ", dim(fit(label, labelWidth)), ...value.chunks),
180
+ }
181
+ }
182
+
183
+ function usageColor(ratio: number): string {
184
+ if (ratio > 0.85) return theme.danger
185
+ if (ratio > 0.6) return theme.warning
186
+ return theme.success
187
+ }
@@ -0,0 +1,177 @@
1
+ import type { KeyEvent } from "@opentui/core"
2
+ import { theme } from "../app/theme.js"
3
+ import type { KeyHint } from "../components/Footer.js"
4
+ import { RowList, type ListRow } from "../components/primitives.js"
5
+ import { processBadge } from "../components/StatusBadge.js"
6
+ import { chunk, dim, styled } from "../components/styled.js"
7
+ import { setSelection, type AppState, type ViewId } from "../core/state.js"
8
+ import { describeCommand, isActive, type ManagedProcess } from "../processes/ManagedProcess.js"
9
+ import { errorMessage } from "../utils/errors.js"
10
+ import { fit, formatDuration } from "../utils/format.js"
11
+ import { View, type ViewDeps } from "./View.js"
12
+
13
+ /**
14
+ * Processes devcc owns.
15
+ *
16
+ * Only entries with `ownership: "managed"` accept start/stop/restart; anything
17
+ * merely detected on the host is displayed read-only.
18
+ */
19
+ export class ProcessesView extends View {
20
+ readonly id: ViewId = "processes"
21
+ readonly title = "Processes"
22
+
23
+ private readonly list: RowList
24
+ private processes: ManagedProcess[] = []
25
+
26
+ constructor(deps: ViewDeps) {
27
+ super(deps)
28
+ this.list = new RowList(deps.ctx, { flexGrow: 1 })
29
+ this.box.add(this.list.box)
30
+ }
31
+
32
+ update(state: AppState): void {
33
+ this.processes = state.processes
34
+ const selected = state.ui.selection.processes
35
+ const width = Math.max(40, state.ui.width - (state.ui.sidebarVisible ? 26 : 4))
36
+ const nameWidth = Math.min(22, Math.max(12, Math.floor(width * 0.22)))
37
+
38
+ const rows: ListRow[] = this.processes.map((process) => {
39
+ const uptime =
40
+ process.startedAt && isActive(process.status)
41
+ ? formatDuration(Date.now() - process.startedAt.getTime())
42
+ : ""
43
+ const detail = process.lastError
44
+ ? chunk(fit(process.lastError, Math.max(10, width - nameWidth - 26)), theme.danger)
45
+ : dim(fit(process.url ?? describeCommand(process), Math.max(10, width - nameWidth - 26)))
46
+
47
+ return {
48
+ key: process.id,
49
+ content: styled(
50
+ ...processBadge(process.status).chunks,
51
+ " ",
52
+ chunk(fit(process.name, nameWidth), theme.textBright),
53
+ detail,
54
+ dim(fit(process.pid ? `pid ${process.pid}` : "", 10)),
55
+ dim(uptime),
56
+ ),
57
+ }
58
+ })
59
+
60
+ if (rows.length === 0) {
61
+ rows.push({
62
+ key: "__empty",
63
+ disabled: true,
64
+ content: styled(dim("No managed processes. Declare them in .devcc.ts to control them here.")),
65
+ })
66
+ }
67
+
68
+ this.list.setRows(rows, selected)
69
+ }
70
+
71
+ override handleKey(key: KeyEvent, state: AppState): boolean {
72
+ const count = this.processes.length
73
+ const selected = state.ui.selection.processes
74
+
75
+ switch (key.name) {
76
+ case "up":
77
+ case "k":
78
+ if (count > 0) setSelection(this.deps.app.store, "processes", Math.max(0, selected - 1))
79
+ return true
80
+ case "down":
81
+ case "j":
82
+ if (count > 0) setSelection(this.deps.app.store, "processes", Math.min(count - 1, selected + 1))
83
+ return true
84
+ case "s":
85
+ void this.toggle(selected)
86
+ return true
87
+ case "r":
88
+ void this.restart(selected)
89
+ return true
90
+ case "return":
91
+ this.openLogs(selected)
92
+ return true
93
+ case "i":
94
+ this.inspect(selected)
95
+ return true
96
+ default:
97
+ return false
98
+ }
99
+ }
100
+
101
+ override hints(): KeyHint[] {
102
+ return [
103
+ { keys: "↑↓/jk", label: "select" },
104
+ { keys: "s", label: "start/stop" },
105
+ { keys: "r", label: "restart" },
106
+ { keys: "enter", label: "logs" },
107
+ { keys: "i", label: "inspect" },
108
+ { keys: "/", label: "commands" },
109
+ ]
110
+ }
111
+
112
+ private current(index: number): ManagedProcess | undefined {
113
+ return this.processes[index]
114
+ }
115
+
116
+ private async toggle(index: number): Promise<void> {
117
+ const process = this.current(index)
118
+ if (!process) return
119
+ if (process.ownership !== "managed") {
120
+ this.deps.app.notify("warn", `${process.name} was detected, not launched by devcc`)
121
+ return
122
+ }
123
+ try {
124
+ if (isActive(process.status)) {
125
+ await this.deps.app.processes.stop(process.id)
126
+ this.deps.app.notify("info", `Stopped ${process.name}`)
127
+ } else {
128
+ await this.deps.app.processes.start(process.id)
129
+ this.deps.app.notify("success", `Started ${process.name}`)
130
+ }
131
+ } catch (error) {
132
+ this.deps.app.notify("error", errorMessage(error))
133
+ }
134
+ }
135
+
136
+ private async restart(index: number): Promise<void> {
137
+ const process = this.current(index)
138
+ if (!process) return
139
+ if (process.ownership !== "managed") {
140
+ this.deps.app.notify("warn", `${process.name} was detected, not launched by devcc`)
141
+ return
142
+ }
143
+ try {
144
+ await this.deps.app.processes.restart(process.id)
145
+ this.deps.app.notify("success", `Restarted ${process.name}`)
146
+ } catch (error) {
147
+ this.deps.app.notify("error", errorMessage(error))
148
+ }
149
+ }
150
+
151
+ private openLogs(index: number): void {
152
+ const process = this.current(index)
153
+ if (!process) return
154
+ this.deps.app.store.setState((state) => ({
155
+ ...state,
156
+ ui: { ...state.ui, logSource: process.id },
157
+ }))
158
+ this.deps.app.navigate("logs")
159
+ }
160
+
161
+ private inspect(index: number): void {
162
+ const process = this.current(index)
163
+ if (!process) return
164
+ const lines = [
165
+ `id ${process.id}`,
166
+ `command ${describeCommand(process)}`,
167
+ `cwd ${process.cwd ?? "(project root)"}`,
168
+ `status ${process.status}`,
169
+ `pid ${process.pid ?? "-"}`,
170
+ `started ${process.startedAt?.toLocaleTimeString() ?? "-"}`,
171
+ `exit ${process.exitCode ?? "-"}${process.exitSignal ? ` (${process.exitSignal})` : ""}`,
172
+ `restarts ${process.restarts}`,
173
+ `ownership ${process.ownership}`,
174
+ ]
175
+ this.deps.app.notify("info", `${process.name}`, lines.join("\n"))
176
+ }
177
+ }