@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,140 @@
1
+ import type { DependencyService } from "../deps/DependencyService.js"
2
+ import type { GitService } from "../git/GitService.js"
3
+ import type { LogStore } from "../logs/LogBuffer.js"
4
+ import type { ProcessManager } from "../processes/ProcessManager.js"
5
+ import type { ServiceManager } from "../services/ServiceManager.js"
6
+ import type { ProjectEntry } from "../projects/ProjectRegistry.js"
7
+ import type { SystemMonitor } from "../system/SystemMonitor.js"
8
+ import { fuzzyFilter } from "../utils/fuzzy.js"
9
+ import type { AppStore, NotificationLevel, ViewId } from "./state.js"
10
+
11
+ /**
12
+ * Everything a command is allowed to touch.
13
+ *
14
+ * Commands are the single extension point: the palette, the keymap and any
15
+ * user-provided config entry all end up here, so a new capability only has to
16
+ * be registered once.
17
+ */
18
+ export interface CommandContext {
19
+ store: AppStore
20
+ processes: ProcessManager
21
+ services: ServiceManager
22
+ git: GitService
23
+ logs: LogStore
24
+ system: SystemMonitor
25
+ deps: DependencyService
26
+
27
+ navigate: (view: ViewId) => void
28
+ notify: (level: NotificationLevel, message: string, detail?: string) => void
29
+ /** Modal yes/no. Resolves false when dismissed. */
30
+ confirm: (title: string, message: string) => Promise<boolean>
31
+ /** Modal single-line prompt. Resolves null when cancelled. */
32
+ prompt: (title: string, placeholder?: string) => Promise<string | null>
33
+ /** Open the in-app manual. */
34
+ showHelp: () => void
35
+ runScript: (name: string) => Promise<void>
36
+ /** Update one dependency to `version`, after confirmation. */
37
+ updateDependency: (name: string, version: string) => Promise<void>
38
+ /** Projects the switcher can offer, current one first. */
39
+ listProjects: () => ProjectEntry[]
40
+ /** Projects currently open, in the order they were opened. */
41
+ openProjects: () => ProjectEntry[]
42
+ /** Open a project (or bring an open one to the front) without closing others. */
43
+ switchProject: (path: string) => Promise<void>
44
+ /** Close an open project, stopping what devcc started there. Defaults to the current one. */
45
+ closeProject: (path?: string) => Promise<void>
46
+ quit: () => void
47
+ }
48
+
49
+ export interface CommandDefinition {
50
+ id: string
51
+ title: string
52
+ category?: string
53
+ /** Displayed hint, e.g. `r`. Key handling itself lives in the keymap. */
54
+ shortcut?: string
55
+ keywords?: string[]
56
+ /** Hide the command when it cannot apply right now. */
57
+ when?: (context: CommandContext) => boolean
58
+ /** Ask for confirmation before running. Used for destructive operations. */
59
+ confirm?: { title: string; message: string }
60
+ execute: (context: CommandContext) => Promise<void> | void
61
+ }
62
+
63
+ export interface CommandRegistryEvents {
64
+ changed: []
65
+ }
66
+
67
+ /**
68
+ * Central command registry. Ids are unique; re-registering an id replaces it,
69
+ * which is what lets project config override a built-in.
70
+ */
71
+ export class CommandRegistry {
72
+ private readonly commands = new Map<string, CommandDefinition>()
73
+
74
+ register(command: CommandDefinition): void {
75
+ this.commands.set(command.id, command)
76
+ }
77
+
78
+ registerAll(commands: readonly CommandDefinition[]): void {
79
+ for (const command of commands) this.register(command)
80
+ }
81
+
82
+ unregister(id: string): boolean {
83
+ return this.commands.delete(id)
84
+ }
85
+
86
+ /** Drop every command. Used when the dashboard reattaches to another project. */
87
+ clear(): void {
88
+ this.commands.clear()
89
+ }
90
+
91
+ has(id: string): boolean {
92
+ return this.commands.has(id)
93
+ }
94
+
95
+ get(id: string): CommandDefinition | undefined {
96
+ return this.commands.get(id)
97
+ }
98
+
99
+ list(): CommandDefinition[] {
100
+ return [...this.commands.values()]
101
+ }
102
+
103
+ /** Commands whose `when` guard passes right now. */
104
+ available(context: CommandContext): CommandDefinition[] {
105
+ return this.list().filter((command) => (command.when ? safeWhen(command, context) : true))
106
+ }
107
+
108
+ /** Fuzzy search over `category title keywords`. */
109
+ search(query: string, context: CommandContext, limit = 40): CommandDefinition[] {
110
+ const candidates = this.available(context)
111
+ return fuzzyFilter(candidates, query, searchText, limit).map((match) => match.item)
112
+ }
113
+
114
+ /**
115
+ * Run a command by id, honouring its `confirm` guard.
116
+ * Returns false when the command does not exist or the user declined.
117
+ */
118
+ async run(id: string, context: CommandContext): Promise<boolean> {
119
+ const command = this.commands.get(id)
120
+ if (!command) return false
121
+ if (command.confirm) {
122
+ const confirmed = await context.confirm(command.confirm.title, command.confirm.message)
123
+ if (!confirmed) return false
124
+ }
125
+ await command.execute(context)
126
+ return true
127
+ }
128
+ }
129
+
130
+ export function searchText(command: CommandDefinition): string {
131
+ return [command.category, command.title, ...(command.keywords ?? [])].filter(Boolean).join(" ")
132
+ }
133
+
134
+ function safeWhen(command: CommandDefinition, context: CommandContext): boolean {
135
+ try {
136
+ return command.when?.(context) ?? true
137
+ } catch {
138
+ return false
139
+ }
140
+ }
@@ -0,0 +1,405 @@
1
+ import { existsSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import type { ProcessDefinition, ProcessKind, ReadinessCheck } from "../processes/ManagedProcess.js"
4
+ import { errorMessage } from "../utils/errors.js"
5
+ import type { LogLevel } from "../utils/logger.js"
6
+ import { isLogLevel } from "../utils/logger.js"
7
+
8
+ /** A command declared in project config: runs an executable and streams its logs. */
9
+ export interface CommandConfigEntry {
10
+ id: string
11
+ title: string
12
+ category?: string
13
+ command: string
14
+ args?: string[]
15
+ cwd?: string
16
+ keywords?: string[]
17
+ /** Require a yes/no confirmation before running. */
18
+ confirm?: { title: string; message: string } | boolean
19
+ }
20
+
21
+ /** An extra port/HTTP endpoint to watch in the Services view. */
22
+ export interface ServiceConfigEntry {
23
+ id: string
24
+ name: string
25
+ port: number
26
+ host?: string
27
+ healthPath?: string
28
+ category?: "container" | "database" | "runtime" | "ai" | "web"
29
+ }
30
+
31
+ export interface RefreshIntervals {
32
+ system?: number
33
+ services?: number
34
+ git?: number
35
+ clock?: number
36
+ }
37
+
38
+ export interface DevccConfig {
39
+ /** Overrides the project name shown in the header. */
40
+ name?: string
41
+ /**
42
+ * Extra project paths to offer in the project switcher, absolute or relative
43
+ * to this project. Recents and sibling directories are found automatically.
44
+ */
45
+ projects?: string[]
46
+ /** Extra directories to scan for sibling projects (default: the parent). */
47
+ projectSearchDirs?: string[]
48
+ processes?: ProcessDefinition[]
49
+ commands?: CommandConfigEntry[]
50
+ services?: ServiceConfigEntry[]
51
+ refresh?: RefreshIntervals
52
+ /** Cap for each log source's ring buffer. */
53
+ maxLogLines?: number
54
+ logLevel?: LogLevel
55
+ }
56
+
57
+ export type ResolvedConfig = Required<
58
+ Pick<DevccConfig, "processes" | "commands" | "services" | "projects" | "projectSearchDirs">
59
+ > &
60
+ Omit<DevccConfig, "processes" | "commands" | "services" | "projects" | "projectSearchDirs"> & {
61
+ refresh: Required<RefreshIntervals>
62
+ }
63
+
64
+ export const DEFAULT_REFRESH: Required<RefreshIntervals> = {
65
+ system: 1_500,
66
+ services: 5_000,
67
+ git: 8_000,
68
+ clock: 1_000,
69
+ }
70
+
71
+ export const EMPTY_CONFIG: ResolvedConfig = {
72
+ processes: [],
73
+ commands: [],
74
+ services: [],
75
+ projects: [],
76
+ projectSearchDirs: [],
77
+ refresh: DEFAULT_REFRESH,
78
+ }
79
+
80
+ /** Identity helper that gives config files full type checking and completion. */
81
+ export function defineConfig(config: DevccConfig): DevccConfig {
82
+ return config
83
+ }
84
+
85
+ export const CONFIG_FILENAMES = [
86
+ ".devcc.ts",
87
+ "devcc.config.ts",
88
+ ".devcc.mts",
89
+ "devcc.config.mts",
90
+ ".devcc.js",
91
+ "devcc.config.js",
92
+ ".devcc.mjs",
93
+ "devcc.config.mjs",
94
+ ".devcc.json",
95
+ ] as const
96
+
97
+ export function findConfigFile(
98
+ rootDir: string,
99
+ exists: (path: string) => boolean = existsSync,
100
+ ): string | null {
101
+ for (const name of CONFIG_FILENAMES) {
102
+ const path = join(rootDir, name)
103
+ if (exists(path)) return path
104
+ }
105
+ return null
106
+ }
107
+
108
+ export interface ValidationResult {
109
+ config: ResolvedConfig
110
+ issues: string[]
111
+ }
112
+
113
+ const PROCESS_KINDS: readonly ProcessKind[] = ["service", "task"]
114
+
115
+ /**
116
+ * Validate and normalise a raw config object.
117
+ *
118
+ * Invalid entries are dropped with an explanatory issue rather than throwing:
119
+ * a typo in one process definition must not stop the dashboard from starting.
120
+ */
121
+ export function validateConfig(raw: unknown): ValidationResult {
122
+ const issues: string[] = []
123
+ if (raw === null || typeof raw !== "object") {
124
+ return { config: { ...EMPTY_CONFIG }, issues: ["Config must export an object"] }
125
+ }
126
+
127
+ const input = raw as Record<string, unknown>
128
+ const seenProcessIds = new Set<string>()
129
+ const processes: ProcessDefinition[] = []
130
+
131
+ for (const [index, entry] of asArray(input.processes, "processes", issues).entries()) {
132
+ const record = asRecord(entry)
133
+ if (!record) {
134
+ issues.push(`processes[${index}] must be an object`)
135
+ continue
136
+ }
137
+ const id = asString(record.id)
138
+ const command = asString(record.command)
139
+ if (!id) {
140
+ issues.push(`processes[${index}] is missing "id"`)
141
+ continue
142
+ }
143
+ if (!command) {
144
+ issues.push(`processes[${index}] ("${id}") is missing "command"`)
145
+ continue
146
+ }
147
+ if (seenProcessIds.has(id)) {
148
+ issues.push(`processes[${index}] duplicates id "${id}"`)
149
+ continue
150
+ }
151
+ seenProcessIds.add(id)
152
+
153
+ const kind = asString(record.kind)
154
+ processes.push({
155
+ id,
156
+ name: asString(record.name) ?? id,
157
+ command,
158
+ args: asStringArray(record.args),
159
+ cwd: asString(record.cwd),
160
+ env: asStringRecord(record.env),
161
+ kind: kind && (PROCESS_KINDS as readonly string[]).includes(kind) ? (kind as ProcessKind) : "service",
162
+ autoStart: record.autoStart === true,
163
+ url: asString(record.url),
164
+ port: asPositiveInt(record.port),
165
+ killGraceMs: asPositiveInt(record.killGraceMs),
166
+ restartOnCrash: record.restartOnCrash === true,
167
+ maxCrashRestarts: asPositiveInt(record.maxCrashRestarts),
168
+ crashBackoffMs: asPositiveInt(record.crashBackoffMs),
169
+ maxCrashBackoffMs: asPositiveInt(record.maxCrashBackoffMs),
170
+ healthyAfterMs: asPositiveInt(record.healthyAfterMs),
171
+ dependsOn: asStringArray(record.dependsOn),
172
+ readiness: asReadiness(record.readiness, id, issues),
173
+ })
174
+ }
175
+
176
+ for (const definition of processes) {
177
+ for (const dependency of definition.dependsOn ?? []) {
178
+ if (!seenProcessIds.has(dependency)) {
179
+ issues.push(`processes "${definition.id}": dependsOn "${dependency}" is not a declared process`)
180
+ }
181
+ }
182
+ }
183
+
184
+ const seenCommandIds = new Set<string>()
185
+ const commands: CommandConfigEntry[] = []
186
+ for (const [index, entry] of asArray(input.commands, "commands", issues).entries()) {
187
+ const record = asRecord(entry)
188
+ if (!record) {
189
+ issues.push(`commands[${index}] must be an object`)
190
+ continue
191
+ }
192
+ const id = asString(record.id)
193
+ const command = asString(record.command)
194
+ if (!id || !command) {
195
+ issues.push(`commands[${index}] needs both "id" and "command"`)
196
+ continue
197
+ }
198
+ if (seenCommandIds.has(id)) {
199
+ issues.push(`commands[${index}] duplicates id "${id}"`)
200
+ continue
201
+ }
202
+ seenCommandIds.add(id)
203
+
204
+ const confirm = record.confirm
205
+ commands.push({
206
+ id,
207
+ title: asString(record.title) ?? id,
208
+ category: asString(record.category) ?? "Project",
209
+ command,
210
+ args: asStringArray(record.args),
211
+ cwd: asString(record.cwd),
212
+ keywords: asStringArray(record.keywords),
213
+ confirm:
214
+ confirm === true
215
+ ? true
216
+ : (() => {
217
+ const confirmRecord = asRecord(confirm)
218
+ const title = asString(confirmRecord?.title)
219
+ const message = asString(confirmRecord?.message)
220
+ return title && message ? { title, message } : undefined
221
+ })(),
222
+ })
223
+ }
224
+
225
+ const services: ServiceConfigEntry[] = []
226
+ const seenServiceIds = new Set<string>()
227
+ for (const [index, entry] of asArray(input.services, "services", issues).entries()) {
228
+ const record = asRecord(entry)
229
+ if (!record) {
230
+ issues.push(`services[${index}] must be an object`)
231
+ continue
232
+ }
233
+ const id = asString(record.id)
234
+ const port = asPositiveInt(record.port)
235
+ if (!id || port === undefined) {
236
+ issues.push(`services[${index}] needs "id" and a numeric "port"`)
237
+ continue
238
+ }
239
+ if (seenServiceIds.has(id)) {
240
+ issues.push(`services[${index}] duplicates id "${id}"`)
241
+ continue
242
+ }
243
+ seenServiceIds.add(id)
244
+ services.push({
245
+ id,
246
+ name: asString(record.name) ?? id,
247
+ port,
248
+ host: asString(record.host),
249
+ healthPath: asString(record.healthPath),
250
+ category: asServiceCategory(record.category),
251
+ })
252
+ }
253
+
254
+ const refreshInput = asRecord(input.refresh) ?? {}
255
+ const refresh: Required<RefreshIntervals> = {
256
+ system: asPositiveInt(refreshInput.system) ?? DEFAULT_REFRESH.system,
257
+ services: asPositiveInt(refreshInput.services) ?? DEFAULT_REFRESH.services,
258
+ git: asPositiveInt(refreshInput.git) ?? DEFAULT_REFRESH.git,
259
+ clock: asPositiveInt(refreshInput.clock) ?? DEFAULT_REFRESH.clock,
260
+ }
261
+
262
+ const logLevel = asString(input.logLevel)
263
+ if (logLevel && !isLogLevel(logLevel)) issues.push(`Unknown logLevel "${logLevel}"`)
264
+
265
+ return {
266
+ config: {
267
+ name: asString(input.name),
268
+ projects: asStringArray(input.projects) ?? [],
269
+ projectSearchDirs: asStringArray(input.projectSearchDirs) ?? [],
270
+ processes,
271
+ commands,
272
+ services,
273
+ refresh,
274
+ maxLogLines: asPositiveInt(input.maxLogLines),
275
+ logLevel: logLevel && isLogLevel(logLevel) ? logLevel : undefined,
276
+ },
277
+ issues,
278
+ }
279
+ }
280
+
281
+ export interface LoadedConfig extends ValidationResult {
282
+ filePath: string | null
283
+ }
284
+
285
+ /**
286
+ * Load project config if present.
287
+ *
288
+ * TypeScript config files are transpiled on the fly with jiti so `.devcc.ts`
289
+ * works identically under Bun and Node.
290
+ */
291
+ export async function loadConfig(rootDir: string): Promise<LoadedConfig> {
292
+ const filePath = findConfigFile(rootDir)
293
+ if (!filePath) return { filePath: null, config: { ...EMPTY_CONFIG }, issues: [] }
294
+
295
+ try {
296
+ const raw = await importConfigModule(filePath)
297
+ const result = validateConfig(raw)
298
+ return { ...result, filePath }
299
+ } catch (error) {
300
+ return {
301
+ filePath,
302
+ config: { ...EMPTY_CONFIG },
303
+ issues: [`Failed to load ${filePath}: ${errorMessage(error)}`],
304
+ }
305
+ }
306
+ }
307
+
308
+ async function importConfigModule(filePath: string): Promise<unknown> {
309
+ if (filePath.endsWith(".json")) {
310
+ const { readFile } = await import("node:fs/promises")
311
+ return JSON.parse(await readFile(filePath, "utf8")) as unknown
312
+ }
313
+
314
+ const { createJiti } = await import("jiti")
315
+ const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false })
316
+ const loaded = await jiti.import(filePath)
317
+ return unwrapDefault(loaded)
318
+ }
319
+
320
+ function unwrapDefault(value: unknown): unknown {
321
+ const record = asRecord(value)
322
+ if (record && "default" in record) return unwrapDefault(record.default)
323
+ return value
324
+ }
325
+
326
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
327
+ return typeof value === "object" && value !== null && !Array.isArray(value)
328
+ ? (value as Record<string, unknown>)
329
+ : undefined
330
+ }
331
+
332
+ function asArray(value: unknown, field: string, issues: string[]): unknown[] {
333
+ if (value === undefined) return []
334
+ if (!Array.isArray(value)) {
335
+ issues.push(`"${field}" must be an array`)
336
+ return []
337
+ }
338
+ return value
339
+ }
340
+
341
+ function asString(value: unknown): string | undefined {
342
+ return typeof value === "string" && value.length > 0 ? value : undefined
343
+ }
344
+
345
+ function asStringArray(value: unknown): string[] | undefined {
346
+ if (!Array.isArray(value)) return undefined
347
+ const items = value.filter((item): item is string => typeof item === "string")
348
+ return items.length > 0 ? items : undefined
349
+ }
350
+
351
+ function asStringRecord(value: unknown): Record<string, string> | undefined {
352
+ const record = asRecord(value)
353
+ if (!record) return undefined
354
+ const entries = Object.entries(record).filter(
355
+ (entry): entry is [string, string] => typeof entry[1] === "string",
356
+ )
357
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined
358
+ }
359
+
360
+ function asPositiveInt(value: unknown): number | undefined {
361
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined
362
+ }
363
+
364
+ /**
365
+ * Normalise a readiness block. `port` alone or `logPattern` alone is enough;
366
+ * a block with neither is dropped with an issue rather than silently waiting
367
+ * for nothing.
368
+ */
369
+ function asReadiness(value: unknown, processId: string, issues: string[]): ReadinessCheck | undefined {
370
+ if (value === undefined) return undefined
371
+ const record = asRecord(value)
372
+ if (!record) {
373
+ issues.push(`processes "${processId}": readiness must be an object`)
374
+ return undefined
375
+ }
376
+ const port = asPositiveInt(record.port)
377
+ const logPattern = asString(record.logPattern)
378
+ if (port === undefined && logPattern === undefined) {
379
+ issues.push(`processes "${processId}": readiness needs a "port" or a "logPattern"`)
380
+ return undefined
381
+ }
382
+ if (logPattern !== undefined) {
383
+ try {
384
+ new RegExp(logPattern, "iu")
385
+ } catch {
386
+ issues.push(`processes "${processId}": readiness.logPattern is not a valid regex`)
387
+ return undefined
388
+ }
389
+ }
390
+ return {
391
+ port,
392
+ host: asString(record.host),
393
+ logPattern,
394
+ timeoutMs: asPositiveInt(record.timeoutMs),
395
+ intervalMs: asPositiveInt(record.intervalMs),
396
+ }
397
+ }
398
+
399
+ function asServiceCategory(value: unknown): ServiceConfigEntry["category"] {
400
+ const categories = ["container", "database", "runtime", "ai", "web"] as const
401
+ const name = asString(value)
402
+ return name && (categories as readonly string[]).includes(name)
403
+ ? (name as ServiceConfigEntry["category"])
404
+ : "web"
405
+ }
@@ -0,0 +1,41 @@
1
+ import { EventEmitter } from "node:events"
2
+
3
+ /**
4
+ * Typed event bus.
5
+ *
6
+ * Backend services (processes, git, docker, system) emit events; the app layer
7
+ * translates them into state updates; views read state. Views never call into
8
+ * the backends' internals directly.
9
+ */
10
+ export type EventMap = Record<string, unknown[]>
11
+
12
+ export class TypedEmitter<Events extends EventMap> extends EventEmitter<Events> {}
13
+
14
+ /** A disposable subscription. Calling it twice is safe. */
15
+ export type Unsubscribe = () => void
16
+
17
+ /** Collects unsubscribe functions so a component can tear everything down at once. */
18
+ export class Subscriptions {
19
+ private disposers: Unsubscribe[] = []
20
+
21
+ add(unsubscribe: Unsubscribe): Unsubscribe {
22
+ this.disposers.push(unsubscribe)
23
+ return unsubscribe
24
+ }
25
+
26
+ dispose(): void {
27
+ const disposers = this.disposers
28
+ this.disposers = []
29
+ for (const dispose of disposers) {
30
+ try {
31
+ dispose()
32
+ } catch {
33
+ // Teardown must never throw.
34
+ }
35
+ }
36
+ }
37
+
38
+ get size(): number {
39
+ return this.disposers.length
40
+ }
41
+ }