@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,187 @@
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 { chunk, dim, styled } from "../components/styled.js"
6
+ import { setSelection, type AppState, type ScriptRun, type ViewId } from "../core/state.js"
7
+ import { errorMessage } from "../utils/errors.js"
8
+ import { fit, formatDuration, singleLine } from "../utils/format.js"
9
+ import { fuzzyFilter } from "../utils/fuzzy.js"
10
+ import { View, type ViewDeps } from "./View.js"
11
+
12
+ /**
13
+ * package.json scripts, run through the detected package manager.
14
+ *
15
+ * devcc does not reimplement script running: it shells out to `pnpm run x` (or
16
+ * the npm/yarn/bun equivalent) as a managed process so the output lands in the
17
+ * log viewer like anything else.
18
+ */
19
+ export class ScriptsView extends View {
20
+ readonly id: ViewId = "scripts"
21
+ readonly title = "Scripts"
22
+
23
+ private readonly list: RowList
24
+ private filter = ""
25
+ private visible: { name: string; command: string }[] = []
26
+
27
+ constructor(deps: ViewDeps) {
28
+ super(deps)
29
+ this.list = new RowList(deps.ctx, { flexGrow: 1 })
30
+ this.box.add(this.list.box)
31
+ }
32
+
33
+ update(state: AppState): void {
34
+ const width = Math.max(40, state.ui.width - (state.ui.sidebarVisible ? 26 : 4))
35
+ const nameWidth = Math.min(24, Math.max(12, Math.floor(width * 0.24)))
36
+
37
+ this.visible =
38
+ this.filter.trim() === ""
39
+ ? state.scripts.available
40
+ : fuzzyFilter(state.scripts.available, this.filter, (script) => script.name).map(
41
+ (match) => match.item,
42
+ )
43
+
44
+ const rows: ListRow[] = this.visible.map((script) => {
45
+ const run = state.scripts.runs[script.name]
46
+ return {
47
+ key: script.name,
48
+ content: styled(
49
+ runGlyph(run),
50
+ " ",
51
+ chunk(fit(script.name, nameWidth), theme.textBright),
52
+ dim(fit(singleLine(script.command), Math.max(10, width - nameWidth - 24))),
53
+ runDetail(run),
54
+ ),
55
+ }
56
+ })
57
+
58
+ if (rows.length === 0) {
59
+ rows.push({
60
+ key: "__empty",
61
+ disabled: true,
62
+ content: styled(
63
+ dim(this.filter ? `No scripts match "${this.filter}"` : "No scripts in package.json"),
64
+ ),
65
+ })
66
+ }
67
+
68
+ this.list.setRows(rows, state.ui.selection.scripts)
69
+ }
70
+
71
+ override handleKey(key: KeyEvent, state: AppState): boolean {
72
+ const count = this.visible.length
73
+ const selected = state.ui.selection.scripts
74
+
75
+ switch (key.name) {
76
+ case "up":
77
+ case "k":
78
+ if (count > 0) setSelection(this.deps.app.store, "scripts", Math.max(0, selected - 1))
79
+ return true
80
+ case "down":
81
+ case "j":
82
+ if (count > 0) setSelection(this.deps.app.store, "scripts", Math.min(count - 1, selected + 1))
83
+ return true
84
+ case "return":
85
+ case "s":
86
+ void this.run(selected)
87
+ return true
88
+ case "x":
89
+ void this.stop(selected)
90
+ return true
91
+ case "f":
92
+ void this.promptFilter()
93
+ return true
94
+ case "c":
95
+ this.setFilter("")
96
+ return true
97
+ case "l":
98
+ this.openLogs(selected)
99
+ return false
100
+ default:
101
+ return false
102
+ }
103
+ }
104
+
105
+ override hints(): KeyHint[] {
106
+ return [
107
+ { keys: "↑↓/jk", label: "select" },
108
+ { keys: "enter", label: "run" },
109
+ { keys: "x", label: "stop" },
110
+ { keys: "f", label: "filter" },
111
+ { keys: "c", label: "clear filter" },
112
+ { keys: "l", label: "logs" },
113
+ ]
114
+ }
115
+
116
+ private async promptFilter(): Promise<void> {
117
+ const value = await this.deps.app.prompt("Filter scripts", this.filter || "e.g. test")
118
+ if (value !== null) this.setFilter(value)
119
+ }
120
+
121
+ private setFilter(value: string): void {
122
+ this.filter = value
123
+ setSelection(this.deps.app.store, "scripts", 0)
124
+ this.update(this.deps.app.store.getState())
125
+ }
126
+
127
+ private async run(index: number): Promise<void> {
128
+ const script = this.visible[index]
129
+ if (!script) return
130
+ try {
131
+ await this.deps.app.runScript(script.name)
132
+ } catch (error) {
133
+ this.deps.app.notify("error", errorMessage(error))
134
+ }
135
+ }
136
+
137
+ private async stop(index: number): Promise<void> {
138
+ const script = this.visible[index]
139
+ if (!script) return
140
+ const processId = scriptProcessId(script.name)
141
+ if (!this.deps.app.processes.isRunning(processId)) {
142
+ this.deps.app.notify("warn", `${script.name} is not running`)
143
+ return
144
+ }
145
+ try {
146
+ await this.deps.app.processes.stop(processId)
147
+ this.deps.app.notify("info", `Stopped ${script.name}`)
148
+ } catch (error) {
149
+ this.deps.app.notify("error", errorMessage(error))
150
+ }
151
+ }
152
+
153
+ private openLogs(index: number): void {
154
+ const script = this.visible[index]
155
+ if (!script) return
156
+ this.deps.app.store.setState((state) => ({
157
+ ...state,
158
+ ui: { ...state.ui, logSource: scriptProcessId(script.name) },
159
+ }))
160
+ }
161
+ }
162
+
163
+ /** Log/process id used for a script run. */
164
+ export function scriptProcessId(scriptName: string): string {
165
+ return `script:${scriptName}`
166
+ }
167
+
168
+ function runGlyph(run: ScriptRun | undefined) {
169
+ if (!run) return dim("·")
170
+ switch (run.status) {
171
+ case "running":
172
+ return chunk("◐", theme.warning)
173
+ case "succeeded":
174
+ return chunk("✓", theme.success)
175
+ case "failed":
176
+ return chunk("✖", theme.danger)
177
+ }
178
+ }
179
+
180
+ function runDetail(run: ScriptRun | undefined) {
181
+ if (!run) return dim("")
182
+ const elapsed = (run.finishedAt ?? new Date()).getTime() - run.startedAt.getTime()
183
+ if (run.status === "running") return chunk(fit(`running ${formatDuration(elapsed)}`, 20), theme.warning)
184
+ const code = run.exitCode ?? 0
185
+ const label = `exit ${code} · ${formatDuration(elapsed)}`
186
+ return chunk(fit(label, 20), run.status === "failed" ? theme.danger : theme.textDim)
187
+ }
@@ -0,0 +1,188 @@
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 { serviceBadge, serviceGlyph } 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 type { ServiceSnapshot } from "../services/types.js"
9
+ import { errorMessage } from "../utils/errors.js"
10
+ import { fit } from "../utils/format.js"
11
+ import { View, type ViewDeps } from "./View.js"
12
+
13
+ interface Entry {
14
+ kind: "service" | "item" | "heading"
15
+ service?: ServiceSnapshot
16
+ }
17
+
18
+ /**
19
+ * Detected services, grouped by category.
20
+ *
21
+ * Actions are only offered when the adapter implements them; a database devcc
22
+ * merely observes has no start/stop keys.
23
+ */
24
+ export class ServicesView extends View {
25
+ readonly id: ViewId = "services"
26
+ readonly title = "Services"
27
+
28
+ private readonly list: RowList
29
+ private entries: Entry[] = []
30
+
31
+ constructor(deps: ViewDeps) {
32
+ super(deps)
33
+ this.list = new RowList(deps.ctx, { flexGrow: 1 })
34
+ this.box.add(this.list.box)
35
+ }
36
+
37
+ update(state: AppState): void {
38
+ const rows: ListRow[] = []
39
+ const entries: Entry[] = []
40
+ const width = Math.max(40, state.ui.width - (state.ui.sidebarVisible ? 26 : 4))
41
+
42
+ const byCategory = new Map<string, ServiceSnapshot[]>()
43
+ for (const service of state.services) {
44
+ const bucket = byCategory.get(service.category) ?? []
45
+ bucket.push(service)
46
+ byCategory.set(service.category, bucket)
47
+ }
48
+
49
+ if (state.services.length === 0) {
50
+ rows.push({
51
+ key: "__empty",
52
+ disabled: true,
53
+ content: styled(dim("No services detected yet. Detection runs in the background at startup.")),
54
+ })
55
+ entries.push({ kind: "heading" })
56
+ }
57
+
58
+ for (const [category, services] of [...byCategory.entries()].sort()) {
59
+ rows.push({
60
+ key: `heading-${category}`,
61
+ disabled: true,
62
+ content: styled(chunk(category.toUpperCase(), theme.accent)),
63
+ })
64
+ entries.push({ kind: "heading" })
65
+
66
+ for (const service of services) {
67
+ rows.push({
68
+ key: service.id,
69
+ content: styled(
70
+ ...serviceBadge(service.status.state).chunks,
71
+ chunk(fit(service.name, 20), theme.textBright),
72
+ dim(fit(service.status.detail ?? "", Math.max(10, width - 56))),
73
+ dim(service.status.endpoint ?? ""),
74
+ ),
75
+ })
76
+ entries.push({ kind: "service", service })
77
+
78
+ for (const item of service.status.items ?? []) {
79
+ rows.push({
80
+ key: `${service.id}-${item.name}`,
81
+ disabled: true,
82
+ content: styled(
83
+ " ",
84
+ serviceGlyph(item.state).chunks[0] ?? chunk(" "),
85
+ " ",
86
+ dim(fit(item.name, 20)),
87
+ dim(item.detail ?? ""),
88
+ ),
89
+ })
90
+ entries.push({ kind: "item" })
91
+ }
92
+ }
93
+ }
94
+
95
+ this.entries = entries
96
+ this.list.setRows(rows, state.ui.selection.services)
97
+ }
98
+
99
+ override handleKey(key: KeyEvent, state: AppState): boolean {
100
+ const selected = state.ui.selection.services
101
+
102
+ switch (key.name) {
103
+ case "up":
104
+ case "k":
105
+ this.moveSelection(selected, -1)
106
+ return true
107
+ case "down":
108
+ case "j":
109
+ this.moveSelection(selected, 1)
110
+ return true
111
+ case "s":
112
+ void this.toggle(selected)
113
+ return true
114
+ case "r":
115
+ void this.action(selected, "restart")
116
+ return true
117
+ case "d":
118
+ void this.composeDown(selected)
119
+ return true
120
+ default:
121
+ return false
122
+ }
123
+ }
124
+
125
+ override hints(): KeyHint[] {
126
+ return [
127
+ { keys: "↑↓/jk", label: "select" },
128
+ { keys: "s", label: "start/stop" },
129
+ { keys: "r", label: "restart" },
130
+ { keys: "d", label: "compose down" },
131
+ { keys: "/", label: "commands" },
132
+ ]
133
+ }
134
+
135
+ /** Skip headings and sub-items when moving. */
136
+ private moveSelection(from: number, direction: 1 | -1): void {
137
+ let index = from
138
+ for (let steps = 0; steps < this.entries.length; steps += 1) {
139
+ index += direction
140
+ if (index < 0 || index >= this.entries.length) return
141
+ if (this.entries[index]?.kind === "service") {
142
+ setSelection(this.deps.app.store, "services", index)
143
+ return
144
+ }
145
+ }
146
+ }
147
+
148
+ private selectedService(index: number): ServiceSnapshot | undefined {
149
+ return this.entries[index]?.service
150
+ }
151
+
152
+ private async toggle(index: number): Promise<void> {
153
+ const service = this.selectedService(index)
154
+ if (!service) return
155
+ const running = service.status.state === "running" || service.status.state === "degraded"
156
+ await this.action(index, running ? "stop" : "start")
157
+ }
158
+
159
+ private async action(index: number, action: "start" | "stop" | "restart"): Promise<void> {
160
+ const service = this.selectedService(index)
161
+ if (!service) return
162
+
163
+ const supported =
164
+ action === "start" ? service.canStart : action === "stop" ? service.canStop : service.canRestart
165
+ if (!supported) {
166
+ this.deps.app.notify("warn", `${service.name} cannot be ${action}ed from devcc`)
167
+ return
168
+ }
169
+
170
+ try {
171
+ this.deps.app.notify("info", `${action}ing ${service.name}…`)
172
+ await this.deps.app.services.runAction(service.id, action, { confirm: this.deps.app.confirm })
173
+ this.deps.app.notify("success", `${service.name} ${action} complete`)
174
+ } catch (error) {
175
+ this.deps.app.notify("error", errorMessage(error))
176
+ }
177
+ }
178
+
179
+ /** `docker compose down` removes containers; always confirmed. */
180
+ private async composeDown(index: number): Promise<void> {
181
+ const service = this.selectedService(index)
182
+ if (!service || service.id !== "docker") {
183
+ this.deps.app.notify("warn", "Select the Docker Compose service first")
184
+ return
185
+ }
186
+ await this.deps.commands.run("docker.down", this.deps.app)
187
+ }
188
+ }
@@ -0,0 +1,146 @@
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 { chunk, dim, styled } from "../components/styled.js"
6
+ import type { AppState, Notification, ViewId } from "../core/state.js"
7
+ import { fit, formatTimestamp, singleLine } from "../utils/format.js"
8
+ import { tildify } from "../utils/paths.js"
9
+ import { View, type ViewDeps } from "./View.js"
10
+
11
+ /**
12
+ * Configuration, detection results and the diagnostics log.
13
+ *
14
+ * This is where errors go to be inspected: the footer only shows the newest
15
+ * message, the full history (with detail) lives here.
16
+ */
17
+ export class SettingsView extends View {
18
+ readonly id: ViewId = "settings"
19
+ readonly title = "Settings"
20
+
21
+ private readonly list: RowList
22
+
23
+ constructor(deps: ViewDeps) {
24
+ super(deps)
25
+ this.list = new RowList(deps.ctx, { flexGrow: 1, scrollPadding: 0 })
26
+ this.box.add(this.list.box)
27
+ }
28
+
29
+ update(state: AppState): void {
30
+ const rows: ListRow[] = []
31
+
32
+ rows.push(heading("Project"))
33
+ rows.push(row("name", state.project.name))
34
+ rows.push(row("root", tildify(state.project.rootDir)))
35
+ rows.push(
36
+ row(
37
+ "package manager",
38
+ `${state.project.packageManager.manager} (${state.project.packageManager.source}${
39
+ state.project.packageManager.evidence ? `: ${state.project.packageManager.evidence}` : ""
40
+ })`,
41
+ ),
42
+ )
43
+ rows.push(
44
+ row("config", state.project.configPath ? tildify(state.project.configPath) : "none (using defaults)"),
45
+ )
46
+ rows.push(row("log file", tildify(logPath(state))))
47
+
48
+ if (state.project.configIssues.length > 0) {
49
+ rows.push(blank())
50
+ rows.push(heading("Config issues"))
51
+ for (const issue of state.project.configIssues) {
52
+ rows.push({
53
+ key: `issue-${issue}`,
54
+ disabled: true,
55
+ content: styled(chunk(`⚠ ${issue}`, theme.warning)),
56
+ })
57
+ }
58
+ }
59
+
60
+ rows.push(blank())
61
+ rows.push(heading("Counts"))
62
+ rows.push(row("managed processes", String(state.processes.length)))
63
+ rows.push(row("detected services", String(state.services.length)))
64
+ rows.push(row("package scripts", String(state.scripts.available.length)))
65
+ rows.push(row("log sources", String(state.logSources.length)))
66
+
67
+ rows.push(blank())
68
+ rows.push(heading(`Diagnostics (${state.notifications.length})`))
69
+ if (state.notifications.length === 0) {
70
+ rows.push({ key: "no-diag", disabled: true, content: styled(dim(" nothing to report")) })
71
+ }
72
+ for (const notification of state.notifications.slice(0, 40)) {
73
+ rows.push({
74
+ key: `notification-${notification.id}`,
75
+ content: styled(
76
+ dim(fit(formatTimestamp(notification.createdAt), 13)),
77
+ chunk(fit(notification.level, 8), levelColor(notification.level)),
78
+ chunk(singleLine(notification.message, 90), theme.text),
79
+ ),
80
+ })
81
+ }
82
+
83
+ this.list.setRows(rows, state.ui.selection.settings)
84
+ }
85
+
86
+ override handleKey(key: KeyEvent, _state: AppState): boolean {
87
+ switch (key.name) {
88
+ case "up":
89
+ case "k":
90
+ this.list.scrollBy(-1)
91
+ return true
92
+ case "down":
93
+ case "j":
94
+ this.list.scrollBy(1)
95
+ return true
96
+ case "c":
97
+ this.deps.app.store.setState((state) => ({ ...state, notifications: [] }))
98
+ return true
99
+ default:
100
+ return false
101
+ }
102
+ }
103
+
104
+ override hints(): KeyHint[] {
105
+ return [
106
+ { keys: "j/k", label: "scroll" },
107
+ { keys: "c", label: "clear diagnostics" },
108
+ { keys: "/", label: "commands" },
109
+ ]
110
+ }
111
+ }
112
+
113
+ const LABEL_WIDTH = 20
114
+
115
+ function logPath(state: AppState): string {
116
+ return `${state.project.rootDir}/.devcc/devcc.log`
117
+ }
118
+
119
+ function heading(title: string): ListRow {
120
+ return { key: `h-${title}`, disabled: true, content: styled(chunk(title.toUpperCase(), theme.accent)) }
121
+ }
122
+
123
+ function blank(): ListRow {
124
+ return { key: `b-${Math.random().toString(36).slice(2)}`, disabled: true, content: "" }
125
+ }
126
+
127
+ function row(label: string, value: string): ListRow {
128
+ return {
129
+ key: `r-${label}`,
130
+ disabled: true,
131
+ content: styled(dim(fit(label, LABEL_WIDTH)), chunk(value, theme.text)),
132
+ }
133
+ }
134
+
135
+ function levelColor(level: Notification["level"]): string {
136
+ switch (level) {
137
+ case "error":
138
+ return theme.danger
139
+ case "warn":
140
+ return theme.warning
141
+ case "success":
142
+ return theme.success
143
+ case "info":
144
+ return theme.info
145
+ }
146
+ }
@@ -0,0 +1,110 @@
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 { chunk, dim, styled } from "../components/styled.js"
5
+ import type { AppState, ViewId } from "../core/state.js"
6
+ import { fit, formatBytes, formatDuration, meter, toGigabytes } from "../utils/format.js"
7
+ import { View, type ViewDeps } from "./View.js"
8
+
9
+ /** Host information and live resource usage. Read-only. */
10
+ export class SystemView extends View {
11
+ readonly id: ViewId = "system"
12
+ readonly title = "System"
13
+
14
+ private readonly list: RowList
15
+
16
+ constructor(deps: ViewDeps) {
17
+ super(deps)
18
+ this.list = new RowList(deps.ctx, { flexGrow: 1, scrollPadding: 0 })
19
+ this.box.add(this.list.box)
20
+ }
21
+
22
+ update(state: AppState): void {
23
+ const host = state.project.host
24
+ const tools = state.project.tools
25
+ const metrics = state.system
26
+ const rows: ListRow[] = []
27
+
28
+ rows.push(heading("Host"))
29
+ rows.push(row("hostname", host.hostname))
30
+ rows.push(row("os", `${host.platform} ${host.release}`))
31
+ rows.push(row("arch", host.arch))
32
+ rows.push(row("cpu", `${host.cpuModel} (${host.cpuCount} cores)`))
33
+ rows.push(row("memory", `${toGigabytes(host.totalMemory)} GB total`))
34
+
35
+ rows.push(blank())
36
+ rows.push(heading("Toolchain"))
37
+ rows.push(row("runtime", host.runtime))
38
+ rows.push(row("node", host.nodeVersion))
39
+ rows.push(
40
+ row("package manager", `${state.project.packageManager.manager} ${tools.packageManager ?? ""}`.trim()),
41
+ )
42
+ rows.push(row("git", tools.git ?? "not found"))
43
+ rows.push(row("docker", tools.docker ?? "not found"))
44
+
45
+ rows.push(blank())
46
+ rows.push(heading("Usage"))
47
+ if (!metrics) {
48
+ rows.push(row("", "sampling…"))
49
+ } else {
50
+ rows.push(gauge("cpu", metrics.cpu, `${(metrics.cpu * 100).toFixed(1)}%`))
51
+ rows.push(
52
+ gauge(
53
+ "memory",
54
+ metrics.memoryRatio,
55
+ `${toGigabytes(metrics.memoryUsed)} / ${toGigabytes(metrics.memoryTotal)} GB`,
56
+ ),
57
+ )
58
+ if (metrics.disk) {
59
+ rows.push(
60
+ gauge(
61
+ "disk",
62
+ metrics.disk.ratio,
63
+ `${formatBytes(metrics.disk.used)} / ${formatBytes(metrics.disk.total)}`,
64
+ ),
65
+ )
66
+ }
67
+ rows.push(row("load average", metrics.loadAverage.map((value) => value.toFixed(2)).join(" ")))
68
+ rows.push(row("host uptime", formatDuration(metrics.uptimeMs)))
69
+ rows.push(row("devcc uptime", formatDuration(metrics.processUptimeMs)))
70
+ rows.push(row("sampled", metrics.sampledAt.toLocaleTimeString()))
71
+ }
72
+
73
+ this.list.setRows(rows, -1)
74
+ }
75
+
76
+ override hints(): KeyHint[] {
77
+ return [
78
+ { keys: "↑↓", label: "scroll" },
79
+ { keys: "/", label: "commands" },
80
+ { keys: "?", label: "help" },
81
+ ]
82
+ }
83
+ }
84
+
85
+ const LABEL_WIDTH = 18
86
+
87
+ function heading(title: string): ListRow {
88
+ return { key: `h-${title}`, disabled: true, content: styled(chunk(title.toUpperCase(), theme.accent)) }
89
+ }
90
+
91
+ function blank(): ListRow {
92
+ return { key: `b-${Math.random().toString(36).slice(2)}`, disabled: true, content: "" }
93
+ }
94
+
95
+ function row(label: string, value: string): ListRow {
96
+ return {
97
+ key: `r-${label}`,
98
+ disabled: true,
99
+ content: styled(dim(fit(label, LABEL_WIDTH)), chunk(value, theme.text)),
100
+ }
101
+ }
102
+
103
+ function gauge(label: string, ratio: number, value: string): ListRow {
104
+ const color = ratio > 0.85 ? theme.danger : ratio > 0.6 ? theme.warning : theme.success
105
+ return {
106
+ key: `g-${label}`,
107
+ disabled: true,
108
+ content: styled(dim(fit(label, LABEL_WIDTH)), chunk(meter(ratio, 16), color), dim(` ${value}`)),
109
+ }
110
+ }
@@ -0,0 +1,63 @@
1
+ import { BoxRenderable, type KeyEvent, type RenderContext } from "@opentui/core"
2
+ import { theme } from "../app/theme.js"
3
+ import type { KeyHint } from "../components/Footer.js"
4
+ import type { CommandContext, CommandRegistry } from "../core/commands.js"
5
+ import type { AppState, ViewId } from "../core/state.js"
6
+
7
+ export interface ViewDeps {
8
+ ctx: RenderContext
9
+ /** The same context commands receive; views and commands share one surface. */
10
+ app: CommandContext
11
+ commands: CommandRegistry
12
+ }
13
+
14
+ /**
15
+ * Base class for the content panes.
16
+ *
17
+ * Views own a subtree and are told when state changes; they never poll. Key
18
+ * handling is view-first: a view may claim a key (returning `true`) before the
19
+ * global keymap sees it.
20
+ */
21
+ export abstract class View {
22
+ abstract readonly id: ViewId
23
+ abstract readonly title: string
24
+
25
+ readonly box: BoxRenderable
26
+
27
+ constructor(protected readonly deps: ViewDeps) {
28
+ this.box = new BoxRenderable(deps.ctx, {
29
+ flexGrow: 1,
30
+ flexDirection: "column",
31
+ paddingLeft: 1,
32
+ paddingRight: 1,
33
+ paddingTop: 1,
34
+ backgroundColor: "transparent",
35
+ visible: false,
36
+ })
37
+ }
38
+
39
+ /** Called whenever application state changes while this view is active. */
40
+ abstract update(state: AppState): void
41
+
42
+ /** Return true when the key was handled and must not fall through. */
43
+ handleKey(_key: KeyEvent, _state: AppState): boolean {
44
+ return false
45
+ }
46
+
47
+ /** Footer hints for this view. */
48
+ hints(): KeyHint[] {
49
+ return []
50
+ }
51
+
52
+ onActivate(_state: AppState): void {}
53
+
54
+ onDeactivate(): void {}
55
+
56
+ dispose(): void {
57
+ this.box.destroyRecursively()
58
+ }
59
+
60
+ protected get accentColor(): string {
61
+ return theme.accent
62
+ }
63
+ }