@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,205 @@
1
+ import { TypedEmitter } from "../core/events.js"
2
+ import { DevccError } from "../utils/errors.js"
3
+ import { exec, type ExecResult } from "../utils/exec.js"
4
+ import type { ScopedLogger } from "../utils/logger.js"
5
+ import {
6
+ EMPTY_GIT_STATUS,
7
+ LOG_FORMAT,
8
+ parseCommits,
9
+ parsePorcelainV2,
10
+ type GitCommit,
11
+ type GitStatus,
12
+ } from "./parse.js"
13
+
14
+ export interface GitSnapshot {
15
+ available: boolean
16
+ isRepository: boolean
17
+ status: GitStatus
18
+ commits: GitCommit[]
19
+ version: string | null
20
+ lastRefreshedAt: Date | null
21
+ /** Set when the last refresh failed; the previous snapshot is kept. */
22
+ error: string | null
23
+ }
24
+
25
+ export const EMPTY_GIT_SNAPSHOT: GitSnapshot = {
26
+ available: false,
27
+ isRepository: false,
28
+ status: EMPTY_GIT_STATUS,
29
+ commits: [],
30
+ version: null,
31
+ lastRefreshedAt: null,
32
+ error: null,
33
+ }
34
+
35
+ export type GitServiceEvents = {
36
+ "git:changed": [GitSnapshot]
37
+ "git:error": [DevccError]
38
+ }
39
+
40
+ export interface GitServiceOptions {
41
+ rootDir: string
42
+ logger: ScopedLogger
43
+ commitLimit?: number
44
+ timeoutMs?: number
45
+ }
46
+
47
+ /**
48
+ * Thin wrapper around the `git` executable.
49
+ *
50
+ * We call git directly rather than adding a git abstraction library: the six
51
+ * commands the dashboard needs are stable plumbing, and porcelain v2 output is
52
+ * far easier to keep honest than a dependency's changing object model.
53
+ */
54
+ export class GitService extends TypedEmitter<GitServiceEvents> {
55
+ private snapshot: GitSnapshot = { ...EMPTY_GIT_SNAPSHOT }
56
+ private refreshing: Promise<GitSnapshot> | null = null
57
+ private readonly commitLimit: number
58
+ private readonly timeoutMs: number
59
+
60
+ constructor(private readonly options: GitServiceOptions) {
61
+ super()
62
+ this.commitLimit = options.commitLimit ?? 8
63
+ this.timeoutMs = options.timeoutMs ?? 5_000
64
+ }
65
+
66
+ getSnapshot(): GitSnapshot {
67
+ return this.snapshot
68
+ }
69
+
70
+ /** Refresh branch/status/commits. Concurrent calls share one run. */
71
+ async refresh(): Promise<GitSnapshot> {
72
+ if (this.refreshing) return this.refreshing
73
+ this.refreshing = this.doRefresh().finally(() => {
74
+ this.refreshing = null
75
+ })
76
+ return this.refreshing
77
+ }
78
+
79
+ private async doRefresh(): Promise<GitSnapshot> {
80
+ const [versionResult, statusResult, logResult] = await Promise.all([
81
+ this.git(["--version"]),
82
+ this.git(["status", "--porcelain=v2", "--branch", "--untracked-files=all"]),
83
+ this.git(["log", `--max-count=${this.commitLimit}`, `--pretty=format:${LOG_FORMAT}`]),
84
+ ])
85
+
86
+ const available = versionResult.spawnError === undefined
87
+ if (!available) {
88
+ return this.publish({
89
+ ...EMPTY_GIT_SNAPSHOT,
90
+ available: false,
91
+ lastRefreshedAt: new Date(),
92
+ error: "git executable not found",
93
+ })
94
+ }
95
+
96
+ const version = versionResult.ok ? versionResult.stdout.trim().replace(/^git version /u, "") : null
97
+
98
+ if (!statusResult.ok) {
99
+ const notARepo = /not a git repository/iu.test(statusResult.stderr)
100
+ return this.publish({
101
+ ...this.snapshot,
102
+ available: true,
103
+ isRepository: false,
104
+ status: EMPTY_GIT_STATUS,
105
+ commits: [],
106
+ version,
107
+ lastRefreshedAt: new Date(),
108
+ error: notARepo ? null : statusResult.stderr.trim() || "git status failed",
109
+ })
110
+ }
111
+
112
+ return this.publish({
113
+ available: true,
114
+ isRepository: true,
115
+ status: parsePorcelainV2(statusResult.stdout),
116
+ // A fresh repository with no commits fails `git log`; an empty list is correct there.
117
+ commits: logResult.ok ? parseCommits(logResult.stdout) : [],
118
+ version,
119
+ lastRefreshedAt: new Date(),
120
+ error: null,
121
+ })
122
+ }
123
+
124
+ /** `git fetch` is deliberately manual: it touches the network. */
125
+ async fetch(): Promise<void> {
126
+ await this.mutate(["fetch", "--prune"], "git fetch", 30_000)
127
+ }
128
+
129
+ async pull(): Promise<void> {
130
+ await this.mutate(["pull", "--ff-only"], "git pull", 60_000)
131
+ }
132
+
133
+ async push(): Promise<void> {
134
+ await this.mutate(["push"], "git push", 60_000)
135
+ }
136
+
137
+ async stage(paths: readonly string[]): Promise<void> {
138
+ if (paths.length === 0) return
139
+ await this.mutate(["add", "--", ...paths], "git add")
140
+ }
141
+
142
+ async stageAll(): Promise<void> {
143
+ await this.mutate(["add", "--all"], "git add --all")
144
+ }
145
+
146
+ async unstage(paths: readonly string[]): Promise<void> {
147
+ if (paths.length === 0) return
148
+ await this.mutate(["restore", "--staged", "--", ...paths], "git restore --staged")
149
+ }
150
+
151
+ /** Destructive: throws away worktree changes. Callers must confirm first. */
152
+ async discard(paths: readonly string[]): Promise<void> {
153
+ if (paths.length === 0) return
154
+ await this.mutate(["restore", "--worktree", "--", ...paths], "git restore")
155
+ }
156
+
157
+ async commit(message: string): Promise<void> {
158
+ const trimmed = message.trim()
159
+ if (trimmed === "") {
160
+ throw new DevccError({ scope: "git", message: "Commit message is empty" })
161
+ }
162
+ await this.mutate(["commit", "--message", trimmed], "git commit")
163
+ }
164
+
165
+ /** Read-only diff for the inspect panel. */
166
+ async diff(path: string, staged: boolean): Promise<string> {
167
+ const args = ["--no-pager", "diff", "--no-color"]
168
+ if (staged) args.push("--staged")
169
+ args.push("--", path)
170
+ const result = await this.git(args, 10_000)
171
+ return result.ok ? result.stdout : result.stderr
172
+ }
173
+
174
+ private async mutate(args: readonly string[], label: string, timeoutMs?: number): Promise<void> {
175
+ const result = await this.git(args, timeoutMs)
176
+ if (!result.ok) {
177
+ const detail = [result.stdout, result.stderr].filter(Boolean).join("\n").trim()
178
+ this.options.logger.warn(`${label} failed`, { code: result.code, detail })
179
+ const error = new DevccError({
180
+ scope: "git",
181
+ message: `${label} failed`,
182
+ detail: detail || (result.timedOut ? "timed out" : undefined),
183
+ })
184
+ this.emit("git:error", error)
185
+ throw error
186
+ }
187
+ this.options.logger.info(`${label} ok`)
188
+ await this.refresh()
189
+ }
190
+
191
+ private git(args: readonly string[], timeoutMs?: number): Promise<ExecResult> {
192
+ return exec("git", args, {
193
+ cwd: this.options.rootDir,
194
+ timeoutMs: timeoutMs ?? this.timeoutMs,
195
+ // Never let git open an editor or a credential prompt under the TUI.
196
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0", GIT_EDITOR: "true" },
197
+ })
198
+ }
199
+
200
+ private publish(snapshot: GitSnapshot): GitSnapshot {
201
+ this.snapshot = snapshot
202
+ this.emit("git:changed", snapshot)
203
+ return snapshot
204
+ }
205
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Pure parsers for git plumbing output.
3
+ *
4
+ * Kept separate from `GitService` so they can be unit tested without a repo,
5
+ * and so the shape of the data the UI consumes is defined in one place.
6
+ */
7
+
8
+ export type GitFileState = "staged" | "modified" | "untracked" | "conflicted"
9
+
10
+ export interface GitFile {
11
+ path: string
12
+ state: GitFileState
13
+ /** Two-letter porcelain code, e.g. `M.`, `.M`, `A.`, `??`. */
14
+ code: string
15
+ /** Original path for renames. */
16
+ origPath?: string
17
+ }
18
+
19
+ export interface GitStatus {
20
+ branch: string | null
21
+ upstream: string | null
22
+ ahead: number
23
+ behind: number
24
+ /** Short commit hash of HEAD, when known. */
25
+ head: string | null
26
+ files: GitFile[]
27
+ clean: boolean
28
+ }
29
+
30
+ export interface GitCommit {
31
+ hash: string
32
+ shortHash: string
33
+ author: string
34
+ relativeDate: string
35
+ subject: string
36
+ }
37
+
38
+ export const EMPTY_GIT_STATUS: GitStatus = {
39
+ branch: null,
40
+ upstream: null,
41
+ ahead: 0,
42
+ behind: 0,
43
+ head: null,
44
+ files: [],
45
+ clean: true,
46
+ }
47
+
48
+ /**
49
+ * Parse `git status --porcelain=v2 --branch --untracked-files=all`.
50
+ *
51
+ * Format reference: `man git-status`, "Porcelain Format Version 2".
52
+ */
53
+ export function parsePorcelainV2(output: string): GitStatus {
54
+ const status: GitStatus = { ...EMPTY_GIT_STATUS, files: [] }
55
+
56
+ for (const rawLine of output.split("\n")) {
57
+ const line = rawLine.replace(/\r$/u, "")
58
+ if (line.length === 0) continue
59
+
60
+ if (line.startsWith("# branch.head ")) {
61
+ const value = line.slice("# branch.head ".length).trim()
62
+ status.branch = value === "(detached)" ? null : value
63
+ continue
64
+ }
65
+ if (line.startsWith("# branch.oid ")) {
66
+ const value = line.slice("# branch.oid ".length).trim()
67
+ status.head = value === "(initial)" ? null : value.slice(0, 7)
68
+ continue
69
+ }
70
+ if (line.startsWith("# branch.upstream ")) {
71
+ status.upstream = line.slice("# branch.upstream ".length).trim()
72
+ continue
73
+ }
74
+ if (line.startsWith("# branch.ab ")) {
75
+ for (const part of line.slice("# branch.ab ".length).trim().split(" ")) {
76
+ const value = Number.parseInt(part.slice(1), 10)
77
+ if (Number.isNaN(value)) continue
78
+ if (part.startsWith("+")) status.ahead = value
79
+ else if (part.startsWith("-")) status.behind = value
80
+ }
81
+ continue
82
+ }
83
+ if (line.startsWith("# ")) continue
84
+
85
+ const kind = line[0]
86
+ if (kind === "1") {
87
+ // 1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
88
+ const fields = splitFields(line, 8)
89
+ const code = fields[1] ?? ".."
90
+ const path = fields[8] ?? ""
91
+ if (path) status.files.push({ path, code, state: stateFromCode(code) })
92
+ continue
93
+ }
94
+ if (kind === "2") {
95
+ // 2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path><TAB><origPath>
96
+ const fields = splitFields(line, 9)
97
+ const code = fields[1] ?? ".."
98
+ const rest = fields[9] ?? ""
99
+ const [path = "", origPath] = rest.split("\t")
100
+ if (path) status.files.push({ path, code, state: stateFromCode(code), origPath })
101
+ continue
102
+ }
103
+ if (kind === "u") {
104
+ const fields = splitFields(line, 10)
105
+ const path = fields[10] ?? ""
106
+ if (path) status.files.push({ path, code: fields[1] ?? "UU", state: "conflicted" })
107
+ continue
108
+ }
109
+ if (kind === "?") {
110
+ const path = line.slice(2)
111
+ if (path) status.files.push({ path, code: "??", state: "untracked" })
112
+ continue
113
+ }
114
+ }
115
+
116
+ status.clean = status.files.length === 0
117
+ return status
118
+ }
119
+
120
+ /**
121
+ * Porcelain v2 codes are `XY` where X is the staged change and Y the worktree
122
+ * change; `.` means "unchanged".
123
+ */
124
+ function stateFromCode(code: string): GitFileState {
125
+ const staged = code[0] ?? "."
126
+ return staged !== "." && staged !== "?" ? "staged" : "modified"
127
+ }
128
+
129
+ /** Split a porcelain line into `count` space-separated fields plus the remainder. */
130
+ function splitFields(line: string, count: number): string[] {
131
+ const fields: string[] = []
132
+ let index = 0
133
+ for (let i = 0; i < count; i += 1) {
134
+ const next = line.indexOf(" ", index)
135
+ if (next === -1) {
136
+ fields.push(line.slice(index))
137
+ return fields
138
+ }
139
+ fields.push(line.slice(index, next))
140
+ index = next + 1
141
+ }
142
+ fields.push(line.slice(index))
143
+ return fields
144
+ }
145
+
146
+ /** ASCII unit separator (0x1F), used as the field separator in our git log format. */
147
+ export const LOG_SEPARATOR = String.fromCharCode(31)
148
+
149
+ /** Format string matching {@link parseCommits}. */
150
+ export const LOG_FORMAT = ["%H", "%h", "%an", "%ar", "%s"].join(LOG_SEPARATOR)
151
+
152
+ /** Parse `git log --pretty=format:<LOG_FORMAT>`. */
153
+ export function parseCommits(output: string): GitCommit[] {
154
+ const commits: GitCommit[] = []
155
+ for (const line of output.split("\n")) {
156
+ if (line.trim() === "") continue
157
+ const [hash, shortHash, author, relativeDate, ...subject] = line.split(LOG_SEPARATOR)
158
+ if (!hash || !shortHash) continue
159
+ commits.push({
160
+ hash,
161
+ shortHash,
162
+ author: author ?? "",
163
+ relativeDate: relativeDate ?? "",
164
+ subject: subject.join(LOG_SEPARATOR),
165
+ })
166
+ }
167
+ return commits
168
+ }
169
+
170
+ export interface GitCounts {
171
+ staged: number
172
+ modified: number
173
+ untracked: number
174
+ conflicted: number
175
+ }
176
+
177
+ export function countByState(files: readonly GitFile[]): GitCounts {
178
+ const counts: GitCounts = { staged: 0, modified: 0, untracked: 0, conflicted: 0 }
179
+ for (const file of files) counts[file.state] += 1
180
+ return counts
181
+ }
182
+
183
+ /** Short summary such as `3 staged / 2 modified / 1 untracked`, or `clean`. */
184
+ export function summariseStatus(status: GitStatus): string {
185
+ if (status.clean) return "clean"
186
+ const counts = countByState(status.files)
187
+ const parts: string[] = []
188
+ if (counts.staged) parts.push(`${counts.staged} staged`)
189
+ if (counts.modified) parts.push(`${counts.modified} modified`)
190
+ if (counts.untracked) parts.push(`${counts.untracked} untracked`)
191
+ if (counts.conflicted) parts.push(`${counts.conflicted} conflicted`)
192
+ return parts.join(" / ")
193
+ }
194
+
195
+ /** `ahead 2, behind 1`, or `up to date` / `no upstream`. */
196
+ export function summariseTracking(status: GitStatus): string {
197
+ if (!status.upstream) return "no upstream"
198
+ if (status.ahead === 0 && status.behind === 0) return "up to date"
199
+ const parts: string[] = []
200
+ if (status.ahead) parts.push(`ahead ${status.ahead}`)
201
+ if (status.behind) parts.push(`behind ${status.behind}`)
202
+ return parts.join(", ")
203
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Public API for Dev Command Center.
3
+ *
4
+ * Project config files import `defineConfig` from here; embedders can drive the
5
+ * `App` directly or reuse individual pieces (process manager, service adapters,
6
+ * git parsing) without the UI.
7
+ */
8
+
9
+ export { App, type AppOptions } from "./app/App.js"
10
+ export { createBuiltinCommands, createConfigCommands, createScriptCommands } from "./app/builtinCommands.js"
11
+ export { HELP_SECTIONS, VIEW_SHORTCUTS } from "./app/keymap.js"
12
+ export { theme, glyphs, processStatusStyle, serviceStateStyle } from "./app/theme.js"
13
+
14
+ export { CommandRegistry, type CommandContext, type CommandDefinition } from "./core/commands.js"
15
+ export {
16
+ defineConfig,
17
+ loadConfig,
18
+ validateConfig,
19
+ findConfigFile,
20
+ CONFIG_FILENAMES,
21
+ DEFAULT_REFRESH,
22
+ type CommandConfigEntry,
23
+ type DevccConfig,
24
+ type ResolvedConfig,
25
+ type ServiceConfigEntry,
26
+ } from "./core/config.js"
27
+ export { TypedEmitter, Subscriptions, type Unsubscribe } from "./core/events.js"
28
+ export {
29
+ Store,
30
+ VIEW_IDS,
31
+ createInitialState,
32
+ type AppState,
33
+ type Notification,
34
+ type ProjectState,
35
+ type UiState,
36
+ type ViewId,
37
+ } from "./core/state.js"
38
+
39
+ export { DependencyService, updateArgs, type DependencySnapshot } from "./deps/DependencyService.js"
40
+ export {
41
+ outdatedArgs,
42
+ parseOutdated,
43
+ severityBetween,
44
+ sortOutdated,
45
+ summariseOutdated,
46
+ supportsOutdatedJson,
47
+ type OutdatedPackage,
48
+ type UpdateSeverity,
49
+ } from "./deps/parse.js"
50
+
51
+ export { GitService, type GitSnapshot } from "./git/GitService.js"
52
+ export {
53
+ parseCommits,
54
+ parsePorcelainV2,
55
+ summariseStatus,
56
+ summariseTracking,
57
+ type GitCommit,
58
+ type GitFile,
59
+ type GitStatus,
60
+ } from "./git/parse.js"
61
+
62
+ export {
63
+ LogBuffer,
64
+ LogStore,
65
+ MAX_LOG_LINES,
66
+ type LogEntry,
67
+ type LogSource,
68
+ type LogStream,
69
+ } from "./logs/LogBuffer.js"
70
+
71
+ export {
72
+ canTransition,
73
+ crashBackoffDelay,
74
+ shouldAutoRestart,
75
+ DEFAULT_CRASH_POLICY,
76
+ createManagedProcess,
77
+ describeCommand,
78
+ isActive,
79
+ type ManagedProcess,
80
+ type ProcessDefinition,
81
+ type ProcessStatus,
82
+ type ReadinessCheck,
83
+ } from "./processes/ManagedProcess.js"
84
+ export { dependentsOf, planStartOrder, type StartPlan } from "./processes/graph.js"
85
+ export { ProcessManager, type ProcessManagerOptions } from "./processes/ProcessManager.js"
86
+
87
+ export {
88
+ buildProjectList,
89
+ discoverProjects,
90
+ isProjectDirectory,
91
+ readRecents,
92
+ recordRecent,
93
+ type ProjectEntry,
94
+ } from "./projects/ProjectRegistry.js"
95
+
96
+ export { ServiceManager } from "./services/ServiceManager.js"
97
+ export { DockerService, findComposeFile, parseComposePs } from "./services/docker.js"
98
+ export { PortService, detectNodeServices } from "./services/node.js"
99
+ export { OllamaService } from "./services/ollama.js"
100
+ export { PostgresService, parsePostgresUrl } from "./services/postgres.js"
101
+ export { RedisService, parseRedisUrl } from "./services/redis.js"
102
+ export type { ServiceAdapter, ServiceSnapshot, ServiceState, ServiceStatus } from "./services/types.js"
103
+
104
+ export { SystemMonitor, cpuUsageBetween, sampleCpu, type SystemMetrics } from "./system/SystemMonitor.js"
105
+
106
+ export { DevccError, errorMessage, toDevccError } from "./utils/errors.js"
107
+ export { Logger, createLogger, type LogLevel } from "./utils/logger.js"
108
+ export {
109
+ detectPackageManager,
110
+ readPackageJson,
111
+ readScripts,
112
+ runScriptArgs,
113
+ type PackageManager,
114
+ } from "./utils/packageManager.js"
115
+ export { findProjectRoot } from "./utils/paths.js"