@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,236 @@
1
+ import type { GitSnapshot } from "../git/GitService.js"
2
+ import { EMPTY_GIT_SNAPSHOT } from "../git/GitService.js"
3
+ import type { LogSource } from "../logs/LogBuffer.js"
4
+ import type { ManagedProcess } from "../processes/ManagedProcess.js"
5
+ import type { DependencySnapshot } from "../deps/DependencyService.js"
6
+ import { EMPTY_DEPENDENCY_SNAPSHOT } from "../deps/DependencyService.js"
7
+ import type { ProjectEntry } from "../projects/ProjectRegistry.js"
8
+ import type { ServiceSnapshot } from "../services/types.js"
9
+ import type { HostInfo, SystemMetrics, ToolVersions } from "../system/SystemMonitor.js"
10
+ import type { PackageManagerDetection } from "../utils/packageManager.js"
11
+
12
+ export const VIEW_IDS = [
13
+ "overview",
14
+ "processes",
15
+ "services",
16
+ "scripts",
17
+ "git",
18
+ "logs",
19
+ "dependencies",
20
+ "system",
21
+ "settings",
22
+ ] as const
23
+
24
+ export type ViewId = (typeof VIEW_IDS)[number]
25
+
26
+ export type OverlayKind = "none" | "palette" | "help" | "confirm" | "prompt" | "projects" | "inspect"
27
+
28
+ export type NotificationLevel = "info" | "success" | "warn" | "error"
29
+
30
+ export interface Notification {
31
+ id: number
32
+ level: NotificationLevel
33
+ message: string
34
+ detail?: string
35
+ createdAt: Date
36
+ }
37
+
38
+ export interface ProjectState {
39
+ name: string
40
+ rootDir: string
41
+ packageManager: PackageManagerDetection
42
+ configPath: string | null
43
+ configIssues: string[]
44
+ host: HostInfo
45
+ tools: ToolVersions
46
+ }
47
+
48
+ export interface ScriptRun {
49
+ scriptName: string
50
+ /** Process id in the ProcessManager, also the log source id. */
51
+ processId: string
52
+ startedAt: Date
53
+ finishedAt?: Date
54
+ exitCode?: number
55
+ status: "running" | "succeeded" | "failed"
56
+ }
57
+
58
+ export interface ScriptsState {
59
+ available: { name: string; command: string }[]
60
+ runs: Record<string, ScriptRun>
61
+ }
62
+
63
+ export interface UiState {
64
+ view: ViewId
65
+ overlay: OverlayKind
66
+ /** Selected row index per view; keeps position when switching back. */
67
+ selection: Record<ViewId, number>
68
+ logSource: string | null
69
+ logFollow: boolean
70
+ /** Terminal size; views degrade gracefully below the thresholds. */
71
+ width: number
72
+ height: number
73
+ sidebarVisible: boolean
74
+ statusMessage: string | null
75
+ clock: Date
76
+ }
77
+
78
+ export interface AppState {
79
+ ui: UiState
80
+ project: ProjectState
81
+ processes: ManagedProcess[]
82
+ services: ServiceSnapshot[]
83
+ scripts: ScriptsState
84
+ git: GitSnapshot
85
+ dependencies: DependencySnapshot
86
+ system: SystemMetrics | null
87
+ logSources: LogSource[]
88
+ notifications: Notification[]
89
+ /** Projects devcc can switch to, including the current one. */
90
+ projects: ProjectEntry[]
91
+ }
92
+
93
+ /** Terminal narrower than this hides the sidebar. */
94
+ export const NARROW_WIDTH = 76
95
+ /** Terminal shorter than this drops decorative rows. */
96
+ export const SHORT_HEIGHT = 20
97
+
98
+ export function createInitialSelection(): Record<ViewId, number> {
99
+ return Object.fromEntries(VIEW_IDS.map((id) => [id, 0])) as Record<ViewId, number>
100
+ }
101
+
102
+ export function createInitialState(project: ProjectState, width: number, height: number): AppState {
103
+ return {
104
+ ui: {
105
+ view: "overview",
106
+ overlay: "none",
107
+ selection: createInitialSelection(),
108
+ logSource: null,
109
+ logFollow: true,
110
+ width,
111
+ height,
112
+ sidebarVisible: width >= NARROW_WIDTH,
113
+ statusMessage: null,
114
+ clock: new Date(),
115
+ },
116
+ project,
117
+ processes: [],
118
+ services: [],
119
+ scripts: { available: [], runs: {} },
120
+ git: EMPTY_GIT_SNAPSHOT,
121
+ dependencies: EMPTY_DEPENDENCY_SNAPSHOT,
122
+ system: null,
123
+ logSources: [],
124
+ notifications: [],
125
+ projects: [],
126
+ }
127
+ }
128
+
129
+ export type Listener<T> = (value: T, previous: T) => void
130
+
131
+ /**
132
+ * Minimal observable store.
133
+ *
134
+ * `watch()` subscribes to a slice: the listener only fires when that slice
135
+ * actually changes, so a 1 Hz CPU sample does not repaint the git panel.
136
+ */
137
+ export class Store<S extends object> {
138
+ private state: S
139
+ private readonly watchers = new Set<{
140
+ select: (state: S) => unknown
141
+ equals: (a: unknown, b: unknown) => boolean
142
+ listener: Listener<never>
143
+ last: unknown
144
+ }>()
145
+
146
+ constructor(initialState: S) {
147
+ this.state = initialState
148
+ }
149
+
150
+ getState(): S {
151
+ return this.state
152
+ }
153
+
154
+ /** Replace state via an updater; watchers are notified with the new slices. */
155
+ setState(updater: (state: S) => S): void {
156
+ const previous = this.state
157
+ const next = updater(previous)
158
+ if (next === previous) return
159
+ this.state = next
160
+ this.notify()
161
+ }
162
+
163
+ /** Patch top-level keys. Reference-equal patches are ignored. */
164
+ patch(patch: Partial<S>): void {
165
+ this.setState((state) => {
166
+ let changed = false
167
+ for (const key of Object.keys(patch) as (keyof S)[]) {
168
+ if (patch[key] !== state[key]) {
169
+ changed = true
170
+ break
171
+ }
172
+ }
173
+ return changed ? { ...state, ...patch } : state
174
+ })
175
+ }
176
+
177
+ watch<T>(
178
+ select: (state: S) => T,
179
+ listener: Listener<T>,
180
+ equals: (a: T, b: T) => boolean = Object.is,
181
+ ): () => void {
182
+ const entry = {
183
+ select: select as (state: S) => unknown,
184
+ equals: equals as (a: unknown, b: unknown) => boolean,
185
+ listener: listener as unknown as Listener<never>,
186
+ last: select(this.state) as unknown,
187
+ }
188
+ this.watchers.add(entry)
189
+ return () => {
190
+ this.watchers.delete(entry)
191
+ }
192
+ }
193
+
194
+ private notify(): void {
195
+ for (const entry of [...this.watchers]) {
196
+ const value = entry.select(this.state)
197
+ if (entry.equals(value, entry.last)) continue
198
+ const last = entry.last
199
+ entry.last = value
200
+ ;(entry.listener as unknown as Listener<unknown>)(value, last)
201
+ }
202
+ }
203
+ }
204
+
205
+ export type AppStore = Store<AppState>
206
+
207
+ /** Shallow array equality, for watching lists of snapshots. */
208
+ export function shallowArrayEqual<T>(a: readonly T[], b: readonly T[]): boolean {
209
+ if (a === b) return true
210
+ if (a.length !== b.length) return false
211
+ for (let i = 0; i < a.length; i += 1) {
212
+ if (a[i] !== b[i]) return false
213
+ }
214
+ return true
215
+ }
216
+
217
+ /** Convenience updaters used by the app layer. */
218
+ export function updateUi(store: AppStore, patch: Partial<UiState>): void {
219
+ store.setState((state) => {
220
+ let changed = false
221
+ for (const key of Object.keys(patch) as (keyof UiState)[]) {
222
+ if (patch[key] !== state.ui[key]) {
223
+ changed = true
224
+ break
225
+ }
226
+ }
227
+ return changed ? { ...state, ui: { ...state.ui, ...patch } } : state
228
+ })
229
+ }
230
+
231
+ export function setSelection(store: AppStore, view: ViewId, index: number): void {
232
+ store.setState((state) => {
233
+ if (state.ui.selection[view] === index) return state
234
+ return { ...state, ui: { ...state.ui, selection: { ...state.ui.selection, [view]: index } } }
235
+ })
236
+ }
@@ -0,0 +1,152 @@
1
+ import { TypedEmitter } from "../core/events.js"
2
+ import { DevccError } from "../utils/errors.js"
3
+ import { exec } from "../utils/exec.js"
4
+ import type { ScopedLogger } from "../utils/logger.js"
5
+ import type { PackageManager } from "../utils/packageManager.js"
6
+ import {
7
+ outdatedArgs,
8
+ parseOutdated,
9
+ sortOutdated,
10
+ supportsOutdatedJson,
11
+ type OutdatedPackage,
12
+ } from "./parse.js"
13
+
14
+ export interface DependencySnapshot {
15
+ packages: OutdatedPackage[]
16
+ /** Null until the first check has run. */
17
+ checkedAt: Date | null
18
+ checking: boolean
19
+ /** Set when the check could not run at all (unsupported manager, no network). */
20
+ error: string | null
21
+ supported: boolean
22
+ }
23
+
24
+ export const EMPTY_DEPENDENCY_SNAPSHOT: DependencySnapshot = {
25
+ packages: [],
26
+ checkedAt: null,
27
+ checking: false,
28
+ error: null,
29
+ supported: true,
30
+ }
31
+
32
+ export type DependencyServiceEvents = {
33
+ "deps:changed": [DependencySnapshot]
34
+ "deps:error": [DevccError]
35
+ }
36
+
37
+ export interface DependencyServiceOptions {
38
+ rootDir: string
39
+ manager: PackageManager
40
+ logger: ScopedLogger
41
+ /** Registry lookups are slow; the default budget is generous. */
42
+ timeoutMs?: number
43
+ }
44
+
45
+ /**
46
+ * Checks for outdated dependencies via the project's own package manager.
47
+ *
48
+ * This is the one panel that never polls: it talks to the registry, so it runs
49
+ * only when asked. `outdated` exits non-zero when it finds updates, which is a
50
+ * result and not a failure - only an empty parse plus a non-zero exit is an
51
+ * error worth reporting.
52
+ */
53
+ export class DependencyService extends TypedEmitter<DependencyServiceEvents> {
54
+ private snapshot: DependencySnapshot = { ...EMPTY_DEPENDENCY_SNAPSHOT }
55
+ private inFlight: Promise<DependencySnapshot> | null = null
56
+ private readonly timeoutMs: number
57
+
58
+ constructor(private readonly options: DependencyServiceOptions) {
59
+ super()
60
+ this.timeoutMs = options.timeoutMs ?? 90_000
61
+ this.snapshot = { ...EMPTY_DEPENDENCY_SNAPSHOT, supported: supportsOutdatedJson(options.manager) }
62
+ }
63
+
64
+ getSnapshot(): DependencySnapshot {
65
+ return this.snapshot
66
+ }
67
+
68
+ /** Run the check. Concurrent calls share one run. */
69
+ async check(): Promise<DependencySnapshot> {
70
+ if (this.inFlight) return this.inFlight
71
+ this.inFlight = this.run().finally(() => {
72
+ this.inFlight = null
73
+ })
74
+ return this.inFlight
75
+ }
76
+
77
+ private async run(): Promise<DependencySnapshot> {
78
+ const { manager, rootDir } = this.options
79
+
80
+ if (!supportsOutdatedJson(manager)) {
81
+ return this.publish({
82
+ ...this.snapshot,
83
+ supported: false,
84
+ checking: false,
85
+ checkedAt: new Date(),
86
+ error: `${manager} does not provide machine-readable "outdated" output`,
87
+ })
88
+ }
89
+
90
+ this.publish({ ...this.snapshot, checking: true, error: null })
91
+
92
+ const result = await exec(manager, outdatedArgs(manager), {
93
+ cwd: rootDir,
94
+ timeoutMs: this.timeoutMs,
95
+ maxBuffer: 8 * 1024 * 1024,
96
+ })
97
+
98
+ if (result.spawnError) {
99
+ return this.fail(`${manager} not found on PATH`)
100
+ }
101
+ if (result.timedOut) {
102
+ return this.fail(`${manager} outdated timed out`)
103
+ }
104
+
105
+ const packages = sortOutdated(parseOutdated(manager, result.stdout))
106
+
107
+ // `outdated` exits 1 when updates exist, so only treat a non-zero exit with
108
+ // nothing parsed as a real failure.
109
+ if (packages.length === 0 && !result.ok && result.stdout.trim() === "") {
110
+ const detail = result.stderr.trim()
111
+ if (detail !== "") {
112
+ this.options.logger.warn("outdated check failed", { code: result.code, detail })
113
+ return this.fail(detail.split("\n")[0] ?? "outdated check failed")
114
+ }
115
+ }
116
+
117
+ this.options.logger.info("outdated check complete", { count: packages.length })
118
+ return this.publish({
119
+ packages,
120
+ checkedAt: new Date(),
121
+ checking: false,
122
+ error: null,
123
+ supported: true,
124
+ })
125
+ }
126
+
127
+ private fail(message: string): DependencySnapshot {
128
+ const error = new DevccError({ scope: "command", message: `Dependency check failed: ${message}` })
129
+ this.emit("deps:error", error)
130
+ return this.publish({ ...this.snapshot, checking: false, checkedAt: new Date(), error: message })
131
+ }
132
+
133
+ private publish(snapshot: DependencySnapshot): DependencySnapshot {
134
+ this.snapshot = snapshot
135
+ this.emit("deps:changed", snapshot)
136
+ return snapshot
137
+ }
138
+ }
139
+
140
+ /** The argv that updates a single package to its latest version. */
141
+ export function updateArgs(manager: PackageManager, name: string): string[] {
142
+ switch (manager) {
143
+ case "pnpm":
144
+ return ["update", "--latest", name]
145
+ case "npm":
146
+ return ["install", `${name}@latest`]
147
+ case "yarn":
148
+ return ["upgrade", `${name}@latest`]
149
+ case "bun":
150
+ return ["update", "--latest", name]
151
+ }
152
+ }
@@ -0,0 +1,210 @@
1
+ import type { PackageManager } from "../utils/packageManager.js"
2
+
3
+ export type DependencyKind = "dependencies" | "devDependencies" | "peerDependencies" | "optionalDependencies"
4
+
5
+ /** How far behind a package is, derived from the version strings. */
6
+ export type UpdateSeverity = "major" | "minor" | "patch" | "unknown"
7
+
8
+ export interface OutdatedPackage {
9
+ name: string
10
+ /** Installed version. */
11
+ current: string
12
+ /** Highest version allowed by the range in package.json. */
13
+ wanted: string
14
+ /** Highest version published. */
15
+ latest: string
16
+ kind: DependencyKind
17
+ severity: UpdateSeverity
18
+ /** True when `current` already satisfies the range but `latest` is newer. */
19
+ onlyLatest: boolean
20
+ deprecated?: boolean
21
+ }
22
+
23
+ /** Managers whose `outdated` command can emit machine-readable output. */
24
+ export const JSON_CAPABLE_MANAGERS: readonly PackageManager[] = ["pnpm", "npm", "yarn"]
25
+
26
+ export function supportsOutdatedJson(manager: PackageManager): boolean {
27
+ return JSON_CAPABLE_MANAGERS.includes(manager)
28
+ }
29
+
30
+ /** The argv that produces JSON `outdated` output for each manager. */
31
+ export function outdatedArgs(manager: PackageManager): string[] {
32
+ switch (manager) {
33
+ case "pnpm":
34
+ return ["outdated", "--format", "json"]
35
+ case "npm":
36
+ return ["outdated", "--json", "--long"]
37
+ case "yarn":
38
+ return ["outdated", "--json"]
39
+ case "bun":
40
+ return ["outdated"]
41
+ }
42
+ }
43
+
44
+ /** Strip range operators and build metadata: `^1.2.3-beta.1` -> `[1, 2, 3]`. */
45
+ export function parseVersion(version: string): [number, number, number] | null {
46
+ const match = /(\d+)\.(\d+)\.(\d+)/u.exec(version.trim())
47
+ if (!match) return null
48
+ return [Number(match[1]), Number(match[2]), Number(match[3])]
49
+ }
50
+
51
+ /**
52
+ * Classify the gap between two versions. `unknown` when either side is not a
53
+ * plain semver triple (git refs, `latest`, workspace protocols).
54
+ */
55
+ export function severityBetween(current: string, latest: string): UpdateSeverity {
56
+ const from = parseVersion(current)
57
+ const to = parseVersion(latest)
58
+ if (!from || !to) return "unknown"
59
+ if (to[0] !== from[0]) return "major"
60
+ if (to[1] !== from[1]) return "minor"
61
+ if (to[2] !== from[2]) return "patch"
62
+ return "unknown"
63
+ }
64
+
65
+ function normaliseKind(value: unknown): DependencyKind {
66
+ switch (typeof value === "string" ? value.toLowerCase() : "") {
67
+ case "devdependencies":
68
+ case "dev":
69
+ return "devDependencies"
70
+ case "peerdependencies":
71
+ case "peer":
72
+ return "peerDependencies"
73
+ case "optionaldependencies":
74
+ case "optional":
75
+ return "optionalDependencies"
76
+ default:
77
+ return "dependencies"
78
+ }
79
+ }
80
+
81
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
82
+ return typeof value === "object" && value !== null && !Array.isArray(value)
83
+ ? (value as Record<string, unknown>)
84
+ : undefined
85
+ }
86
+
87
+ function asString(value: unknown): string | undefined {
88
+ return typeof value === "string" && value.length > 0 ? value : undefined
89
+ }
90
+
91
+ function buildEntry(
92
+ name: string,
93
+ current: string,
94
+ wanted: string,
95
+ latest: string,
96
+ kind: DependencyKind,
97
+ deprecated?: boolean,
98
+ ): OutdatedPackage {
99
+ return {
100
+ name,
101
+ current,
102
+ wanted,
103
+ latest,
104
+ kind,
105
+ severity: severityBetween(current, latest),
106
+ onlyLatest: current === wanted && wanted !== latest,
107
+ deprecated,
108
+ }
109
+ }
110
+
111
+ /**
112
+ * pnpm (`--format json`) and npm (`--json`) both emit an object keyed by package
113
+ * name; only the field names around it differ slightly.
114
+ */
115
+ export function parseNpmStyleOutdated(json: unknown): OutdatedPackage[] {
116
+ const root = asRecord(json)
117
+ if (!root) return []
118
+
119
+ const packages: OutdatedPackage[] = []
120
+ for (const [name, raw] of Object.entries(root)) {
121
+ // npm --json can produce an array when a package is present at several paths.
122
+ const entry = asRecord(Array.isArray(raw) ? raw[0] : raw)
123
+ if (!entry) continue
124
+
125
+ const current = asString(entry.current)
126
+ const latest = asString(entry.latest)
127
+ if (!latest) continue
128
+
129
+ packages.push(
130
+ buildEntry(
131
+ name,
132
+ current ?? "-",
133
+ asString(entry.wanted) ?? current ?? "-",
134
+ latest,
135
+ normaliseKind(entry.dependencyType ?? entry.type),
136
+ entry.isDeprecated === true,
137
+ ),
138
+ )
139
+ }
140
+ return packages
141
+ }
142
+
143
+ /** yarn classic emits newline-delimited JSON with a `table` payload. */
144
+ export function parseYarnOutdated(output: string): OutdatedPackage[] {
145
+ for (const line of output.split("\n")) {
146
+ if (line.trim() === "") continue
147
+ let parsed: unknown
148
+ try {
149
+ parsed = JSON.parse(line)
150
+ } catch {
151
+ continue
152
+ }
153
+ const record = asRecord(parsed)
154
+ if (record?.type !== "table") continue
155
+
156
+ const body = asRecord(record.data)?.body
157
+ if (!Array.isArray(body)) continue
158
+
159
+ return body
160
+ .filter((row): row is unknown[] => Array.isArray(row))
161
+ .map((row) => {
162
+ const [name, current, wanted, latest, kind] = row.map((cell) =>
163
+ typeof cell === "string" ? cell : "",
164
+ )
165
+ return buildEntry(name ?? "", current ?? "-", wanted ?? "-", latest ?? "-", normaliseKind(kind))
166
+ })
167
+ .filter((entry) => entry.name !== "")
168
+ }
169
+ return []
170
+ }
171
+
172
+ /** Dispatch to the right parser for the manager that produced `output`. */
173
+ export function parseOutdated(manager: PackageManager, output: string): OutdatedPackage[] {
174
+ const trimmed = output.trim()
175
+ if (trimmed === "") return []
176
+
177
+ if (manager === "yarn") return parseYarnOutdated(trimmed)
178
+
179
+ try {
180
+ return parseNpmStyleOutdated(JSON.parse(trimmed))
181
+ } catch {
182
+ return []
183
+ }
184
+ }
185
+
186
+ const SEVERITY_ORDER: Record<UpdateSeverity, number> = { major: 0, minor: 1, patch: 2, unknown: 3 }
187
+
188
+ /** Most-significant updates first, then alphabetically. */
189
+ export function sortOutdated(packages: readonly OutdatedPackage[]): OutdatedPackage[] {
190
+ return [...packages].sort((a, b) => {
191
+ const bySeverity = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
192
+ return bySeverity !== 0 ? bySeverity : a.name.localeCompare(b.name)
193
+ })
194
+ }
195
+
196
+ export interface OutdatedSummary {
197
+ total: number
198
+ major: number
199
+ minor: number
200
+ patch: number
201
+ }
202
+
203
+ export function summariseOutdated(packages: readonly OutdatedPackage[]): OutdatedSummary {
204
+ return {
205
+ total: packages.length,
206
+ major: packages.filter((entry) => entry.severity === "major").length,
207
+ minor: packages.filter((entry) => entry.severity === "minor").length,
208
+ patch: packages.filter((entry) => entry.severity === "patch").length,
209
+ }
210
+ }