@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,220 @@
1
+ import { existsSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { DevccError } from "../utils/errors.js"
4
+ import { exec } from "../utils/exec.js"
5
+ import type {
6
+ ServiceActionContext,
7
+ ServiceAdapter,
8
+ ServiceState,
9
+ ServiceStatus,
10
+ ServiceStatusItem,
11
+ } from "./types.js"
12
+
13
+ const COMPOSE_FILES = [
14
+ "docker-compose.yml",
15
+ "docker-compose.yaml",
16
+ "compose.yml",
17
+ "compose.yaml",
18
+ "docker/docker-compose.yml",
19
+ ]
20
+
21
+ export function findComposeFile(
22
+ rootDir: string,
23
+ exists: (path: string) => boolean = existsSync,
24
+ ): string | null {
25
+ for (const file of COMPOSE_FILES) {
26
+ if (exists(join(rootDir, file))) return file
27
+ }
28
+ return null
29
+ }
30
+
31
+ interface ComposePsEntry {
32
+ Name?: string
33
+ Service?: string
34
+ State?: string
35
+ Status?: string
36
+ Health?: string
37
+ Publishers?: { PublishedPort?: number; TargetPort?: number }[]
38
+ }
39
+
40
+ /**
41
+ * `docker compose ps --format json` emits either a JSON array or one JSON
42
+ * object per line depending on the CLI version. Handle both.
43
+ */
44
+ export function parseComposePs(output: string): ServiceStatusItem[] {
45
+ const trimmed = output.trim()
46
+ if (trimmed === "") return []
47
+
48
+ const entries: ComposePsEntry[] = []
49
+ if (trimmed.startsWith("[")) {
50
+ try {
51
+ const parsed: unknown = JSON.parse(trimmed)
52
+ if (Array.isArray(parsed)) entries.push(...(parsed as ComposePsEntry[]))
53
+ } catch {
54
+ return []
55
+ }
56
+ } else {
57
+ for (const line of trimmed.split("\n")) {
58
+ if (line.trim() === "") continue
59
+ try {
60
+ entries.push(JSON.parse(line) as ComposePsEntry)
61
+ } catch {
62
+ // Skip malformed lines rather than losing the whole listing.
63
+ }
64
+ }
65
+ }
66
+
67
+ return entries.map((entry) => {
68
+ const name = entry.Service ?? entry.Name ?? "container"
69
+ const ports = (entry.Publishers ?? [])
70
+ .map((publisher) => publisher.PublishedPort)
71
+ .filter((port): port is number => typeof port === "number" && port > 0)
72
+ const detail = [entry.Status, ports.length > 0 ? `:${[...new Set(ports)].join(",")}` : null]
73
+ .filter(Boolean)
74
+ .join(" ")
75
+ return { name, state: composeState(entry), detail: detail || undefined }
76
+ })
77
+ }
78
+
79
+ function composeState(entry: ComposePsEntry): ServiceState {
80
+ const health = entry.Health?.toLowerCase()
81
+ if (health === "unhealthy" || health === "starting") return "degraded"
82
+ switch (entry.State?.toLowerCase()) {
83
+ case "running":
84
+ return "running"
85
+ case "restarting":
86
+ case "paused":
87
+ return "degraded"
88
+ case "exited":
89
+ case "dead":
90
+ case "created":
91
+ return "stopped"
92
+ default:
93
+ return "unknown"
94
+ }
95
+ }
96
+
97
+ export interface DockerServiceOptions {
98
+ rootDir: string
99
+ timeoutMs?: number
100
+ }
101
+
102
+ /**
103
+ * Docker / Docker Compose adapter.
104
+ *
105
+ * `stop` runs `docker compose stop` (not `down`): stopping must never remove
106
+ * containers, networks or volumes without an explicit confirmation.
107
+ */
108
+ export class DockerService implements ServiceAdapter {
109
+ readonly id = "docker"
110
+ readonly name = "Docker Compose"
111
+ readonly category = "container" as const
112
+
113
+ private composeFile: string | null
114
+ private readonly timeoutMs: number
115
+
116
+ constructor(private readonly options: DockerServiceOptions) {
117
+ this.composeFile = findComposeFile(options.rootDir)
118
+ this.timeoutMs = options.timeoutMs ?? 6_000
119
+ }
120
+
121
+ get hasCompose(): boolean {
122
+ return this.composeFile !== null
123
+ }
124
+
125
+ async detect(): Promise<boolean> {
126
+ this.composeFile = findComposeFile(this.options.rootDir)
127
+ const result = await exec("docker", ["--version"], { timeoutMs: 3_000 })
128
+ return result.spawnError === undefined
129
+ }
130
+
131
+ async getStatus(): Promise<ServiceStatus> {
132
+ const checkedAt = new Date()
133
+ const info = await exec("docker", ["info", "--format", "{{.ServerVersion}}"], {
134
+ cwd: this.options.rootDir,
135
+ timeoutMs: this.timeoutMs,
136
+ })
137
+
138
+ if (!info.ok) {
139
+ return {
140
+ state: "unavailable",
141
+ detail: info.spawnError ? "docker not installed" : "daemon unreachable",
142
+ checkedAt,
143
+ }
144
+ }
145
+
146
+ const serverVersion = info.stdout.trim()
147
+ if (!this.composeFile) {
148
+ return { state: "running", detail: `daemon ${serverVersion}`, checkedAt }
149
+ }
150
+
151
+ const ps = await exec("docker", ["compose", "ps", "--all", "--format", "json"], {
152
+ cwd: this.options.rootDir,
153
+ timeoutMs: this.timeoutMs,
154
+ })
155
+
156
+ if (!ps.ok) {
157
+ return { state: "degraded", detail: `daemon ${serverVersion}, compose ps failed`, checkedAt }
158
+ }
159
+
160
+ const items = parseComposePs(ps.stdout)
161
+ const running = items.filter((item) => item.state === "running").length
162
+ const state: ServiceState =
163
+ items.length === 0 ? "stopped" : running === items.length ? "running" : "degraded"
164
+
165
+ return {
166
+ state,
167
+ detail:
168
+ items.length === 0 ? `no containers (${this.composeFile})` : `${running}/${items.length} running`,
169
+ items,
170
+ checkedAt,
171
+ }
172
+ }
173
+
174
+ async start(): Promise<void> {
175
+ this.requireCompose()
176
+ await this.compose(["up", "--detach"], 120_000)
177
+ }
178
+
179
+ async stop(): Promise<void> {
180
+ this.requireCompose()
181
+ // `stop`, never `down` - see the class comment.
182
+ await this.compose(["stop"], 60_000)
183
+ }
184
+
185
+ async restart(): Promise<void> {
186
+ this.requireCompose()
187
+ await this.compose(["restart"], 120_000)
188
+ }
189
+
190
+ /** `docker compose down` removes containers and networks, so it must be confirmed. */
191
+ async down(context: ServiceActionContext, removeVolumes = false): Promise<void> {
192
+ this.requireCompose()
193
+ const message = removeVolumes
194
+ ? "This removes containers, networks AND named volumes. Data will be lost."
195
+ : "This removes containers and networks created by compose."
196
+ const confirmed = await context.confirm(
197
+ removeVolumes ? "docker compose down --volumes" : "docker compose down",
198
+ message,
199
+ )
200
+ if (!confirmed) return
201
+ await this.compose(removeVolumes ? ["down", "--volumes"] : ["down"], 120_000)
202
+ }
203
+
204
+ private requireCompose(): void {
205
+ if (!this.composeFile) {
206
+ throw new DevccError({ scope: "docker", message: "No docker compose file found in this project" })
207
+ }
208
+ }
209
+
210
+ private async compose(args: string[], timeoutMs: number): Promise<void> {
211
+ const result = await exec("docker", ["compose", ...args], { cwd: this.options.rootDir, timeoutMs })
212
+ if (!result.ok) {
213
+ throw new DevccError({
214
+ scope: "docker",
215
+ message: `docker compose ${args.join(" ")} failed`,
216
+ detail: result.stderr.trim() || result.stdout.trim() || (result.timedOut ? "timed out" : undefined),
217
+ })
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,137 @@
1
+ import { probeTcpPort } from "../utils/exec.js"
2
+ import type { ServiceAdapter, ServiceStatus } from "./types.js"
3
+
4
+ export interface PortServiceOptions {
5
+ id: string
6
+ name: string
7
+ port: number
8
+ host?: string
9
+ category?: ServiceAdapter["category"]
10
+ /** When set, a successful HTTP request to this path upgrades the detail line. */
11
+ healthPath?: string
12
+ /** Auto-detected services hide themselves when nothing is listening. */
13
+ onlyWhenListening?: boolean
14
+ timeoutMs?: number
15
+ }
16
+
17
+ /**
18
+ * A generic "is something serving on this port?" adapter.
19
+ *
20
+ * This is how Next.js, Vite, Payload, Typesense and any user-declared HTTP
21
+ * service are represented - devcc observes them, it does not own them, so it
22
+ * deliberately exposes no start/stop actions here. Use a managed process for
23
+ * something devcc should control.
24
+ */
25
+ export class PortService implements ServiceAdapter {
26
+ readonly id: string
27
+ readonly name: string
28
+ readonly category: ServiceAdapter["category"]
29
+
30
+ private readonly host: string
31
+ private readonly port: number
32
+ private readonly healthPath: string | undefined
33
+ private readonly onlyWhenListening: boolean
34
+ private readonly timeoutMs: number
35
+
36
+ constructor(options: PortServiceOptions) {
37
+ this.id = options.id
38
+ this.name = options.name
39
+ this.category = options.category ?? "web"
40
+ this.host = options.host ?? "127.0.0.1"
41
+ this.port = options.port
42
+ this.healthPath = options.healthPath
43
+ this.onlyWhenListening = options.onlyWhenListening ?? false
44
+ this.timeoutMs = options.timeoutMs ?? 1_000
45
+ }
46
+
47
+ async detect(): Promise<boolean> {
48
+ if (!this.onlyWhenListening) return true
49
+ return probeTcpPort(this.port, this.host, this.timeoutMs)
50
+ }
51
+
52
+ async getStatus(): Promise<ServiceStatus> {
53
+ const checkedAt = new Date()
54
+ const endpoint = `${this.host}:${this.port}`
55
+ const listening = await probeTcpPort(this.port, this.host, this.timeoutMs)
56
+ if (!listening) return { state: "stopped", endpoint, detail: "not listening", checkedAt }
57
+
58
+ if (!this.healthPath) return { state: "running", endpoint, detail: "listening", checkedAt }
59
+
60
+ try {
61
+ const response = await fetch(`http://${endpoint}${this.healthPath}`, {
62
+ signal: AbortSignal.timeout(this.timeoutMs),
63
+ })
64
+ return {
65
+ state: response.ok ? "running" : "degraded",
66
+ endpoint,
67
+ detail: `HTTP ${response.status}`,
68
+ checkedAt,
69
+ }
70
+ } catch {
71
+ return { state: "degraded", endpoint, detail: "listening, health check failed", checkedAt }
72
+ }
73
+ }
74
+ }
75
+
76
+ interface FrameworkHint {
77
+ /** Dependency name to look for in package.json. */
78
+ dependency: string
79
+ id: string
80
+ name: string
81
+ port: number
82
+ category?: ServiceAdapter["category"]
83
+ healthPath?: string
84
+ }
85
+
86
+ /** Well-known local dev servers and their conventional ports. */
87
+ const FRAMEWORK_HINTS: readonly FrameworkHint[] = [
88
+ { dependency: "next", id: "next", name: "Next.js", port: 3000 },
89
+ { dependency: "vite", id: "vite", name: "Vite", port: 5173 },
90
+ { dependency: "@remix-run/dev", id: "remix", name: "Remix", port: 3000 },
91
+ { dependency: "nuxt", id: "nuxt", name: "Nuxt", port: 3000 },
92
+ { dependency: "astro", id: "astro", name: "Astro", port: 4321 },
93
+ { dependency: "payload", id: "payload", name: "Payload CMS", port: 3000, healthPath: "/admin" },
94
+ { dependency: "typesense", id: "typesense", name: "Typesense", port: 8108, category: "database" },
95
+ { dependency: "express", id: "express", name: "Express API", port: 3001, category: "runtime" },
96
+ { dependency: "fastify", id: "fastify", name: "Fastify API", port: 3001, category: "runtime" },
97
+ { dependency: "@nestjs/core", id: "nest", name: "NestJS API", port: 3000, category: "runtime" },
98
+ ]
99
+
100
+ function dependencyNames(packageJson: Record<string, unknown> | null): Set<string> {
101
+ const names = new Set<string>()
102
+ for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
103
+ const value = packageJson?.[field]
104
+ if (typeof value === "object" && value !== null) {
105
+ for (const name of Object.keys(value)) names.add(name)
106
+ }
107
+ }
108
+ return names
109
+ }
110
+
111
+ /**
112
+ * Build port adapters for frameworks this project actually depends on.
113
+ * Nothing is assumed: a project with none of these gets no adapters.
114
+ */
115
+ export function detectNodeServices(packageJson: Record<string, unknown> | null): PortService[] {
116
+ const dependencies = dependencyNames(packageJson)
117
+ const seenPorts = new Set<number>()
118
+ const adapters: PortService[] = []
119
+
120
+ for (const hint of FRAMEWORK_HINTS) {
121
+ if (!dependencies.has(hint.dependency)) continue
122
+ if (seenPorts.has(hint.port)) continue
123
+ seenPorts.add(hint.port)
124
+ adapters.push(
125
+ new PortService({
126
+ id: hint.id,
127
+ name: hint.name,
128
+ port: hint.port,
129
+ category: hint.category ?? "web",
130
+ healthPath: hint.healthPath,
131
+ onlyWhenListening: true,
132
+ }),
133
+ )
134
+ }
135
+
136
+ return adapters
137
+ }
@@ -0,0 +1,80 @@
1
+ import { probeTcpPort } from "../utils/exec.js"
2
+ import type { ServiceAdapter, ServiceStatus } from "./types.js"
3
+
4
+ const DEFAULT_HOST = "127.0.0.1"
5
+ const DEFAULT_PORT = 11434
6
+
7
+ interface OllamaTagsResponse {
8
+ models?: { name?: string }[]
9
+ }
10
+
11
+ export interface OllamaServiceOptions {
12
+ host?: string
13
+ port?: number
14
+ timeoutMs?: number
15
+ }
16
+
17
+ /**
18
+ * Ollama adapter. Uses the HTTP API for a model count when the port is open;
19
+ * falls back to a plain port probe if the API call fails.
20
+ */
21
+ export class OllamaService implements ServiceAdapter {
22
+ readonly id = "ollama"
23
+ readonly name = "Ollama"
24
+ readonly category = "ai" as const
25
+
26
+ private readonly host: string
27
+ private readonly port: number
28
+ private readonly timeoutMs: number
29
+
30
+ constructor(options: OllamaServiceOptions = {}) {
31
+ const fromEnv = parseHostPort(process.env.OLLAMA_HOST)
32
+ this.host = options.host ?? fromEnv?.host ?? DEFAULT_HOST
33
+ this.port = options.port ?? fromEnv?.port ?? DEFAULT_PORT
34
+ this.timeoutMs = options.timeoutMs ?? 1_500
35
+ }
36
+
37
+ async detect(): Promise<boolean> {
38
+ return probeTcpPort(this.port, this.host)
39
+ }
40
+
41
+ async getStatus(): Promise<ServiceStatus> {
42
+ const checkedAt = new Date()
43
+ const endpoint = `${this.host}:${this.port}`
44
+ const listening = await probeTcpPort(this.port, this.host)
45
+ if (!listening) return { state: "stopped", endpoint, detail: "not listening", checkedAt }
46
+
47
+ const models = await this.listModels()
48
+ return {
49
+ state: "running",
50
+ endpoint,
51
+ detail: models === null ? "listening" : `${models.length} model${models.length === 1 ? "" : "s"}`,
52
+ items: models?.slice(0, 6).map((name) => ({ name, state: "running" as const })),
53
+ checkedAt,
54
+ }
55
+ }
56
+
57
+ private async listModels(): Promise<string[] | null> {
58
+ try {
59
+ const response = await fetch(`http://${this.host}:${this.port}/api/tags`, {
60
+ signal: AbortSignal.timeout(this.timeoutMs),
61
+ })
62
+ if (!response.ok) return null
63
+ const body = (await response.json()) as OllamaTagsResponse
64
+ return (body.models ?? []).map((model) => model.name ?? "unnamed")
65
+ } catch {
66
+ return null
67
+ }
68
+ }
69
+ }
70
+
71
+ export function parseHostPort(value: string | undefined): { host: string; port: number } | null {
72
+ if (!value) return null
73
+ const withScheme = value.includes("://") ? value : `http://${value}`
74
+ try {
75
+ const parsed = new URL(withScheme)
76
+ return { host: parsed.hostname || DEFAULT_HOST, port: Number(parsed.port || DEFAULT_PORT) }
77
+ } catch {
78
+ return null
79
+ }
80
+ }
@@ -0,0 +1,57 @@
1
+ import { probeTcpPort } from "../utils/exec.js"
2
+ import type { ServiceAdapter, ServiceStatus } from "./types.js"
3
+
4
+ export interface PostgresServiceOptions {
5
+ host?: string
6
+ port?: number
7
+ /** Only report the service when one of these is true. */
8
+ hints?: boolean
9
+ }
10
+
11
+ /** Parse `postgres://user:pass@host:5432/db` style URLs for host/port. */
12
+ export function parsePostgresUrl(url: string | undefined): { host: string; port: number } | null {
13
+ if (!url) return null
14
+ try {
15
+ const parsed = new URL(url)
16
+ if (!parsed.protocol.startsWith("postgres")) return null
17
+ return { host: parsed.hostname || "127.0.0.1", port: Number(parsed.port || 5432) }
18
+ } catch {
19
+ return null
20
+ }
21
+ }
22
+
23
+ /**
24
+ * PostgreSQL adapter.
25
+ *
26
+ * Detection is intentionally conservative: we only claim Postgres is relevant
27
+ * when the project mentions it (DATABASE_URL / a pg dependency) or something is
28
+ * actually listening on the port.
29
+ */
30
+ export class PostgresService implements ServiceAdapter {
31
+ readonly id = "postgres"
32
+ readonly name = "PostgreSQL"
33
+ readonly category = "database" as const
34
+
35
+ private readonly host: string
36
+ private readonly port: number
37
+
38
+ constructor(options: PostgresServiceOptions = {}) {
39
+ const fromEnv = parsePostgresUrl(process.env.DATABASE_URL ?? process.env.POSTGRES_URL)
40
+ this.host = options.host ?? fromEnv?.host ?? "127.0.0.1"
41
+ this.port = options.port ?? fromEnv?.port ?? 5432
42
+ }
43
+
44
+ async detect(): Promise<boolean> {
45
+ return probeTcpPort(this.port, this.host)
46
+ }
47
+
48
+ async getStatus(): Promise<ServiceStatus> {
49
+ const listening = await probeTcpPort(this.port, this.host)
50
+ return {
51
+ state: listening ? "running" : "stopped",
52
+ endpoint: `${this.host}:${this.port}`,
53
+ detail: listening ? "accepting connections" : "not listening",
54
+ checkedAt: new Date(),
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,47 @@
1
+ import { probeTcpPort } from "../utils/exec.js"
2
+ import type { ServiceAdapter, ServiceStatus } from "./types.js"
3
+
4
+ export interface RedisServiceOptions {
5
+ host?: string
6
+ port?: number
7
+ }
8
+
9
+ export function parseRedisUrl(url: string | undefined): { host: string; port: number } | null {
10
+ if (!url) return null
11
+ try {
12
+ const parsed = new URL(url)
13
+ if (!parsed.protocol.startsWith("redis")) return null
14
+ return { host: parsed.hostname || "127.0.0.1", port: Number(parsed.port || 6379) }
15
+ } catch {
16
+ return null
17
+ }
18
+ }
19
+
20
+ export class RedisService implements ServiceAdapter {
21
+ readonly id = "redis"
22
+ readonly name = "Redis"
23
+ readonly category = "database" as const
24
+
25
+ private readonly host: string
26
+ private readonly port: number
27
+
28
+ constructor(options: RedisServiceOptions = {}) {
29
+ const fromEnv = parseRedisUrl(process.env.REDIS_URL)
30
+ this.host = options.host ?? fromEnv?.host ?? "127.0.0.1"
31
+ this.port = options.port ?? fromEnv?.port ?? 6379
32
+ }
33
+
34
+ async detect(): Promise<boolean> {
35
+ return probeTcpPort(this.port, this.host)
36
+ }
37
+
38
+ async getStatus(): Promise<ServiceStatus> {
39
+ const listening = await probeTcpPort(this.port, this.host)
40
+ return {
41
+ state: listening ? "running" : "stopped",
42
+ endpoint: `${this.host}:${this.port}`,
43
+ detail: listening ? "accepting connections" : "not listening",
44
+ checkedAt: new Date(),
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Service adapters describe things the project depends on but that devcc does
3
+ * not necessarily own: Docker, a database, a local model server, a dev server
4
+ * someone started in another terminal.
5
+ *
6
+ * Adapters are detected at runtime. Nothing here assumes a given project has
7
+ * any particular service.
8
+ */
9
+
10
+ export type ServiceState = "running" | "stopped" | "degraded" | "unknown" | "unavailable"
11
+
12
+ export interface ServiceStatusItem {
13
+ name: string
14
+ state: ServiceState
15
+ detail?: string
16
+ }
17
+
18
+ export interface ServiceStatus {
19
+ state: ServiceState
20
+ /** One-line summary shown next to the service name. */
21
+ detail?: string
22
+ /** Where to reach it, e.g. `localhost:5432`. */
23
+ endpoint?: string
24
+ /** Sub-entries, e.g. individual compose containers. */
25
+ items?: ServiceStatusItem[]
26
+ checkedAt: Date
27
+ }
28
+
29
+ export interface ServiceActionContext {
30
+ /** Ask the user before doing something destructive. Resolves to the answer. */
31
+ confirm: (title: string, message: string) => Promise<boolean>
32
+ }
33
+
34
+ export interface ServiceAdapter {
35
+ id: string
36
+ name: string
37
+ /** Grouping label shown in the Services view. */
38
+ category: "container" | "database" | "runtime" | "ai" | "web"
39
+
40
+ /** Cheap check: is this service relevant to this project/host at all? */
41
+ detect: () => Promise<boolean>
42
+ getStatus: () => Promise<ServiceStatus>
43
+
44
+ start?: (context: ServiceActionContext) => Promise<void>
45
+ stop?: (context: ServiceActionContext) => Promise<void>
46
+ restart?: (context: ServiceActionContext) => Promise<void>
47
+ }
48
+
49
+ export interface ServiceSnapshot {
50
+ id: string
51
+ name: string
52
+ category: ServiceAdapter["category"]
53
+ status: ServiceStatus
54
+ canStart: boolean
55
+ canStop: boolean
56
+ canRestart: boolean
57
+ }
58
+
59
+ export const UNKNOWN_STATUS: ServiceStatus = { state: "unknown", checkedAt: new Date(0) }