@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,112 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+
4
+ export const PACKAGE_MANAGERS = ["pnpm", "yarn", "npm", "bun"] as const
5
+ export type PackageManager = (typeof PACKAGE_MANAGERS)[number]
6
+
7
+ export interface PackageManagerDetection {
8
+ manager: PackageManager
9
+ /** How the decision was made - surfaced in the Settings view. */
10
+ source: "packageManager-field" | "lockfile" | "user-agent" | "default"
11
+ /** The lockfile that decided it, when applicable. */
12
+ evidence?: string
13
+ }
14
+
15
+ const LOCKFILES: ReadonlyArray<{ file: string; manager: PackageManager }> = [
16
+ { file: "pnpm-lock.yaml", manager: "pnpm" },
17
+ { file: "bun.lock", manager: "bun" },
18
+ { file: "bun.lockb", manager: "bun" },
19
+ { file: "yarn.lock", manager: "yarn" },
20
+ { file: "package-lock.json", manager: "npm" },
21
+ { file: "npm-shrinkwrap.json", manager: "npm" },
22
+ ]
23
+
24
+ function isPackageManager(value: string): value is PackageManager {
25
+ return (PACKAGE_MANAGERS as readonly string[]).includes(value)
26
+ }
27
+
28
+ /**
29
+ * Parse the `packageManager` field of a package.json (corepack format,
30
+ * e.g. `pnpm@10.26.2+sha512...`).
31
+ */
32
+ export function parsePackageManagerField(value: unknown): PackageManager | null {
33
+ if (typeof value !== "string") return null
34
+ const name = value.split("@")[0]?.trim().toLowerCase()
35
+ return name && isPackageManager(name) ? name : null
36
+ }
37
+
38
+ /** Parse the `npm_config_user_agent` env var, e.g. `pnpm/10.26.2 npm/? node/v24`. */
39
+ export function parseUserAgent(userAgent: string | undefined): PackageManager | null {
40
+ if (!userAgent) return null
41
+ const name = userAgent.split("/")[0]?.trim().toLowerCase()
42
+ return name && isPackageManager(name) ? name : null
43
+ }
44
+
45
+ export interface DetectPackageManagerInput {
46
+ rootDir: string
47
+ packageJson?: Record<string, unknown> | null
48
+ userAgent?: string | undefined
49
+ /** Injected for tests. */
50
+ fileExists?: (path: string) => boolean
51
+ }
52
+
53
+ /**
54
+ * Detect the package manager, preferring explicit declarations over guesses:
55
+ * `packageManager` field -> lockfile -> the manager that launched us -> npm.
56
+ */
57
+ export function detectPackageManager(input: DetectPackageManagerInput): PackageManagerDetection {
58
+ const fileExists = input.fileExists ?? existsSync
59
+
60
+ const declared = parsePackageManagerField(input.packageJson?.packageManager)
61
+ if (declared) return { manager: declared, source: "packageManager-field" }
62
+
63
+ for (const { file, manager } of LOCKFILES) {
64
+ if (fileExists(join(input.rootDir, file))) {
65
+ return { manager, source: "lockfile", evidence: file }
66
+ }
67
+ }
68
+
69
+ const fromAgent = parseUserAgent(input.userAgent)
70
+ if (fromAgent) return { manager: fromAgent, source: "user-agent" }
71
+
72
+ return { manager: "npm", source: "default" }
73
+ }
74
+
75
+ /** The argv needed to run a package.json script with the given manager. */
76
+ export function runScriptArgs(manager: PackageManager, script: string): string[] {
77
+ switch (manager) {
78
+ case "npm":
79
+ return ["run", script]
80
+ case "yarn":
81
+ return [script]
82
+ case "pnpm":
83
+ case "bun":
84
+ return ["run", script]
85
+ }
86
+ }
87
+
88
+ /** Read and parse package.json, returning `null` when absent or invalid. */
89
+ export function readPackageJson(rootDir: string): Record<string, unknown> | null {
90
+ const path = join(rootDir, "package.json")
91
+ try {
92
+ if (!existsSync(path)) return null
93
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"))
94
+ return typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>) : null
95
+ } catch {
96
+ return null
97
+ }
98
+ }
99
+
100
+ export interface PackageScript {
101
+ name: string
102
+ command: string
103
+ }
104
+
105
+ /** Extract `scripts` from a parsed package.json as a stable, sorted-by-declaration list. */
106
+ export function readScripts(packageJson: Record<string, unknown> | null): PackageScript[] {
107
+ const scripts = packageJson?.scripts
108
+ if (typeof scripts !== "object" || scripts === null) return []
109
+ return Object.entries(scripts as Record<string, unknown>)
110
+ .filter((entry): entry is [string, string] => typeof entry[1] === "string")
111
+ .map(([name, command]) => ({ name, command }))
112
+ }
@@ -0,0 +1,39 @@
1
+ import { existsSync } from "node:fs"
2
+ import { dirname, join, resolve } from "node:path"
3
+
4
+ const PROJECT_MARKERS = ["package.json", ".git", "pnpm-workspace.yaml", "deno.json", "Cargo.toml"] as const
5
+
6
+ /**
7
+ * Walk upwards from `startDir` looking for a project root marker.
8
+ * Falls back to `startDir` so the app always has somewhere to run.
9
+ */
10
+ export function findProjectRoot(startDir: string = process.cwd()): string {
11
+ let current = resolve(startDir)
12
+
13
+ for (;;) {
14
+ for (const marker of PROJECT_MARKERS) {
15
+ if (existsSync(join(current, marker))) return current
16
+ }
17
+ const parent = dirname(current)
18
+ if (parent === current) return resolve(startDir)
19
+ current = parent
20
+ }
21
+ }
22
+
23
+ /** Directory holding devcc's own runtime artefacts (logs, future state). */
24
+ export function devccDir(rootDir: string): string {
25
+ return join(rootDir, ".devcc")
26
+ }
27
+
28
+ /** Replace the home directory with `~` for compact display. */
29
+ export function tildify(path: string, home = process.env.HOME ?? ""): string {
30
+ if (home && path.startsWith(home)) return `~${path.slice(home.length)}`
31
+ return path
32
+ }
33
+
34
+ /** Truncate a path from the left so the most specific segments stay visible. */
35
+ export function truncatePath(path: string, maxLength: number): string {
36
+ if (path.length <= maxLength) return path
37
+ if (maxLength <= 1) return path.slice(-maxLength)
38
+ return `…${path.slice(-(maxLength - 1))}`
39
+ }
@@ -0,0 +1,206 @@
1
+ import type { KeyEvent } from "@opentui/core"
2
+ import { theme } from "../app/theme.js"
3
+ import type { KeyHint } from "../components/Footer.js"
4
+ import { RowList, type ListRow } from "../components/primitives.js"
5
+ import { chunk, dim, styled } from "../components/styled.js"
6
+ import { setSelection, type AppState, type ViewId } from "../core/state.js"
7
+ import type { OutdatedPackage, UpdateSeverity } from "../deps/parse.js"
8
+ import { summariseOutdated } from "../deps/parse.js"
9
+ import { fit, formatClock } from "../utils/format.js"
10
+ import { View, type ViewDeps } from "./View.js"
11
+
12
+ /**
13
+ * Rows rendered above the package list (summary + spacer).
14
+ *
15
+ * Fixed so the view and the palette command agree on what
16
+ * `ui.selection.dependencies` points at.
17
+ */
18
+ export const DEPENDENCY_HEADER_ROWS = 2
19
+
20
+ /** Map a selection index to an index into the package list. */
21
+ export function selectedPackageIndex(selection: number): number {
22
+ return Math.max(0, selection - DEPENDENCY_HEADER_ROWS)
23
+ }
24
+
25
+ /**
26
+ * Outdated dependencies, grouped by how big the jump is.
27
+ *
28
+ * This is the only panel that never refreshes on a timer: it queries the
29
+ * registry over the network, so it runs when you ask it to (`r`) and not before.
30
+ */
31
+ export class DependenciesView extends View {
32
+ readonly id: ViewId = "dependencies"
33
+ readonly title = "Dependencies"
34
+
35
+ private readonly list: RowList
36
+ private packages: OutdatedPackage[] = []
37
+
38
+ constructor(deps: ViewDeps) {
39
+ super(deps)
40
+ this.list = new RowList(deps.ctx, { flexGrow: 1 })
41
+ this.box.add(this.list.box)
42
+ }
43
+
44
+ update(state: AppState): void {
45
+ const snapshot = state.dependencies
46
+ this.packages = snapshot.packages
47
+
48
+ const width = Math.max(40, state.ui.width - (state.ui.sidebarVisible ? 26 : 4))
49
+ const nameWidth = Math.min(34, Math.max(14, Math.floor(width * 0.3)))
50
+ const rows: ListRow[] = []
51
+
52
+ rows.push({ key: "__summary", disabled: true, content: this.summaryRow(state) })
53
+ rows.push({ key: "__blank", disabled: true, content: "" })
54
+
55
+ // A status row only appears when there is no package list, which keeps the
56
+ // header height constant at DEPENDENCY_HEADER_ROWS whenever selection matters.
57
+ if (this.packages.length > 0 && snapshot.error) {
58
+ rows[0] = { key: "__summary", disabled: true, content: this.summaryRow(state) }
59
+ } else if (!snapshot.supported) {
60
+ rows.push({
61
+ key: "__unsupported",
62
+ disabled: true,
63
+ content: styled(
64
+ chunk(`⚠ ${snapshot.error ?? "not supported by this package manager"}`, theme.warning),
65
+ ),
66
+ })
67
+ } else if (snapshot.error) {
68
+ rows.push({
69
+ key: "__error",
70
+ disabled: true,
71
+ content: styled(chunk(`⚠ ${snapshot.error}`, theme.danger)),
72
+ })
73
+ } else if (snapshot.checkedAt === null) {
74
+ rows.push({
75
+ key: "__prompt",
76
+ disabled: true,
77
+ content: styled(dim("Press r to check the registry for updates.")),
78
+ })
79
+ } else if (this.packages.length === 0) {
80
+ rows.push({
81
+ key: "__uptodate",
82
+ disabled: true,
83
+ content: styled(chunk("✓ everything is up to date", theme.success)),
84
+ })
85
+ }
86
+
87
+ for (const entry of this.packages) {
88
+ rows.push({
89
+ key: entry.name,
90
+ content: styled(
91
+ chunk(fit(severityLabel(entry.severity), 7), severityColor(entry.severity)),
92
+ chunk(fit(entry.name, nameWidth), theme.textBright),
93
+ dim(fit(entry.current, 12)),
94
+ dim(fit("→", 2)),
95
+ chunk(fit(entry.latest, 12), severityColor(entry.severity)),
96
+ dim(fit(kindLabel(entry.kind), 6)),
97
+ entry.deprecated ? chunk("deprecated", theme.danger) : dim(entry.onlyLatest ? "outside range" : ""),
98
+ ),
99
+ })
100
+ }
101
+
102
+ this.list.setRows(rows, state.ui.selection.dependencies)
103
+ }
104
+
105
+ override handleKey(key: KeyEvent, state: AppState): boolean {
106
+ const selected = state.ui.selection.dependencies
107
+ const first = this.firstPackageRow()
108
+ const last = first + this.packages.length - 1
109
+
110
+ switch (key.name) {
111
+ case "up":
112
+ case "k":
113
+ if (this.packages.length > 0) {
114
+ setSelection(this.deps.app.store, "dependencies", Math.max(first, selected - 1))
115
+ }
116
+ return true
117
+ case "down":
118
+ case "j":
119
+ if (this.packages.length > 0) {
120
+ setSelection(this.deps.app.store, "dependencies", Math.min(last, Math.max(first, selected + 1)))
121
+ }
122
+ return true
123
+ case "r":
124
+ void this.deps.commands.run("deps.check", this.deps.app)
125
+ return true
126
+ case "u":
127
+ void this.updateSelected(selected)
128
+ return true
129
+ default:
130
+ return false
131
+ }
132
+ }
133
+
134
+ override hints(): KeyHint[] {
135
+ return [
136
+ { keys: "↑↓/jk", label: "select" },
137
+ { keys: "r", label: "check registry" },
138
+ { keys: "u", label: "update package" },
139
+ { keys: "/", label: "commands" },
140
+ ]
141
+ }
142
+
143
+ override onActivate(state: AppState): void {
144
+ // Land on the first real package rather than the summary header.
145
+ if (state.ui.selection.dependencies < this.firstPackageRow()) {
146
+ setSelection(this.deps.app.store, "dependencies", this.firstPackageRow())
147
+ }
148
+ }
149
+
150
+ private firstPackageRow(): number {
151
+ return DEPENDENCY_HEADER_ROWS
152
+ }
153
+
154
+ private summaryRow(state: AppState) {
155
+ const snapshot = state.dependencies
156
+ if (snapshot.checking) return styled(chunk("checking the registry…", theme.warning))
157
+
158
+ const counts = summariseOutdated(snapshot.packages)
159
+ const checked = snapshot.checkedAt ? formatClock(snapshot.checkedAt) : "never"
160
+ return styled(
161
+ chunk(state.project.packageManager.manager, theme.text),
162
+ dim(" · "),
163
+ chunk(`${counts.total} outdated`, counts.total > 0 ? theme.warning : theme.success),
164
+ dim(" · "),
165
+ chunk(`${counts.major} major`, counts.major > 0 ? theme.danger : theme.textDim),
166
+ dim(` · ${counts.minor} minor · ${counts.patch} patch · checked ${checked}`),
167
+ snapshot.error ? chunk(` · ⚠ ${snapshot.error}`, theme.danger) : dim(""),
168
+ )
169
+ }
170
+
171
+ private async updateSelected(index: number): Promise<void> {
172
+ const entry = this.packages[selectedPackageIndex(index)]
173
+ if (!entry) return
174
+ await this.deps.app.updateDependency(entry.name, entry.latest)
175
+ }
176
+ }
177
+
178
+ function severityLabel(severity: UpdateSeverity): string {
179
+ return severity === "unknown" ? "?" : severity
180
+ }
181
+
182
+ function severityColor(severity: UpdateSeverity): string {
183
+ switch (severity) {
184
+ case "major":
185
+ return theme.danger
186
+ case "minor":
187
+ return theme.warning
188
+ case "patch":
189
+ return theme.success
190
+ case "unknown":
191
+ return theme.textDim
192
+ }
193
+ }
194
+
195
+ function kindLabel(kind: OutdatedPackage["kind"]): string {
196
+ switch (kind) {
197
+ case "devDependencies":
198
+ return "dev"
199
+ case "peerDependencies":
200
+ return "peer"
201
+ case "optionalDependencies":
202
+ return "opt"
203
+ case "dependencies":
204
+ return "prod"
205
+ }
206
+ }
@@ -0,0 +1,259 @@
1
+ import type { KeyEvent } from "@opentui/core"
2
+ import { theme } from "../app/theme.js"
3
+ import type { KeyHint } from "../components/Footer.js"
4
+ import { RowList, type ListRow } from "../components/primitives.js"
5
+ import { chunk, dim, styled } from "../components/styled.js"
6
+ import { setSelection, type AppState, type ViewId } from "../core/state.js"
7
+ import { summariseStatus, summariseTracking, type GitFile } from "../git/parse.js"
8
+ import { errorMessage } from "../utils/errors.js"
9
+ import { fit, truncate } from "../utils/format.js"
10
+ import { View, type ViewDeps } from "./View.js"
11
+
12
+ interface GitRow {
13
+ file?: GitFile
14
+ }
15
+
16
+ /**
17
+ * Git dashboard.
18
+ *
19
+ * Read operations refresh on a timer; every write is explicit, and the three
20
+ * that cannot be undone locally - discard, push, and hard operations from the
21
+ * palette - require a confirmation.
22
+ */
23
+ export class GitView extends View {
24
+ readonly id: ViewId = "git"
25
+ readonly title = "Git"
26
+
27
+ private readonly list: RowList
28
+ private rowMeta: GitRow[] = []
29
+
30
+ constructor(deps: ViewDeps) {
31
+ super(deps)
32
+ this.list = new RowList(deps.ctx, { flexGrow: 1 })
33
+ this.box.add(this.list.box)
34
+ }
35
+
36
+ update(state: AppState): void {
37
+ const rows: ListRow[] = []
38
+ const meta: GitRow[] = []
39
+ const width = Math.max(40, state.ui.width - (state.ui.sidebarVisible ? 26 : 4))
40
+ const git = state.git
41
+
42
+ if (git.lastRefreshedAt === null) {
43
+ rows.push({ key: "__checking", disabled: true, content: styled(dim("checking git status…")) })
44
+ } else if (!git.available) {
45
+ rows.push({
46
+ key: "__nogit",
47
+ disabled: true,
48
+ content: styled(chunk("git is not installed", theme.danger)),
49
+ })
50
+ } else if (!git.isRepository) {
51
+ rows.push({
52
+ key: "__norepo",
53
+ disabled: true,
54
+ content: styled(dim("This directory is not a git repository")),
55
+ })
56
+ } else {
57
+ const branch = git.status.branch ?? `detached @ ${git.status.head ?? "?"}`
58
+ rows.push({
59
+ key: "__branch",
60
+ disabled: true,
61
+ content: styled(
62
+ dim(fit("branch", 12)),
63
+ chunk(branch, theme.textBright),
64
+ dim(` ${summariseTracking(git.status)}`),
65
+ ),
66
+ })
67
+ rows.push({
68
+ key: "__status",
69
+ disabled: true,
70
+ content: styled(
71
+ dim(fit("status", 12)),
72
+ chunk(summariseStatus(git.status), git.status.clean ? theme.success : theme.warning),
73
+ ),
74
+ })
75
+ meta.push({}, {})
76
+
77
+ rows.push({ key: "__blank1", disabled: true, content: "" })
78
+ meta.push({})
79
+ rows.push({ key: "__files", disabled: true, content: styled(chunk("CHANGES", theme.accent)) })
80
+ meta.push({})
81
+
82
+ if (git.status.files.length === 0) {
83
+ rows.push({ key: "__clean", disabled: true, content: styled(dim(" working tree clean")) })
84
+ meta.push({})
85
+ }
86
+
87
+ for (const file of git.status.files) {
88
+ rows.push({
89
+ key: `file-${file.path}`,
90
+ content: styled(
91
+ chunk(fit(file.code, 4), stateColor(file.state)),
92
+ chunk(fit(file.state, 11), stateColor(file.state)),
93
+ chunk(truncate(file.path, Math.max(10, width - 20)), theme.text),
94
+ ),
95
+ })
96
+ meta.push({ file })
97
+ }
98
+
99
+ if (git.commits.length > 0 && state.ui.height > 22) {
100
+ rows.push({ key: "__blank2", disabled: true, content: "" })
101
+ meta.push({})
102
+ rows.push({
103
+ key: "__commits",
104
+ disabled: true,
105
+ content: styled(chunk("RECENT COMMITS", theme.accent)),
106
+ })
107
+ meta.push({})
108
+ for (const commit of git.commits.slice(0, 6)) {
109
+ rows.push({
110
+ key: `commit-${commit.hash}`,
111
+ disabled: true,
112
+ content: styled(
113
+ dim(fit(commit.shortHash, 9)),
114
+ chunk(truncate(commit.subject, Math.max(10, width - 34)), theme.text),
115
+ dim(` ${commit.relativeDate}`),
116
+ ),
117
+ })
118
+ meta.push({})
119
+ }
120
+ }
121
+ }
122
+
123
+ if (git.error) {
124
+ rows.push({ key: "__error", disabled: true, content: styled(chunk(`⚠ ${git.error}`, theme.danger)) })
125
+ meta.push({})
126
+ }
127
+
128
+ this.rowMeta = meta
129
+ this.list.setRows(rows, state.ui.selection.git)
130
+ }
131
+
132
+ override handleKey(key: KeyEvent, state: AppState): boolean {
133
+ const selected = state.ui.selection.git
134
+
135
+ switch (key.name) {
136
+ case "up":
137
+ case "k":
138
+ this.move(selected, -1)
139
+ return true
140
+ case "down":
141
+ case "j":
142
+ this.move(selected, 1)
143
+ return true
144
+ case "r":
145
+ void this.refresh()
146
+ return true
147
+ case "s":
148
+ void this.stage(selected)
149
+ return true
150
+ case "u":
151
+ void this.unstage(selected)
152
+ return true
153
+ case "d":
154
+ void this.discard(selected)
155
+ return true
156
+ case "c":
157
+ void this.deps.commands.run("git.commit", this.deps.app)
158
+ return true
159
+ case "p":
160
+ // Shift+P arrives as name "p" with `shift` set.
161
+ void this.deps.commands.run(key.shift ? "git.push" : "git.pull", this.deps.app)
162
+ return true
163
+ case "f":
164
+ void this.deps.commands.run("git.fetch", this.deps.app)
165
+ return true
166
+ default:
167
+ return false
168
+ }
169
+ }
170
+
171
+ override hints(): KeyHint[] {
172
+ return [
173
+ { keys: "↑↓/jk", label: "select" },
174
+ { keys: "s/u", label: "stage/unstage" },
175
+ { keys: "d", label: "discard" },
176
+ { keys: "c", label: "commit" },
177
+ { keys: "p/P", label: "pull/push" },
178
+ { keys: "f", label: "fetch" },
179
+ { keys: "r", label: "refresh" },
180
+ ]
181
+ }
182
+
183
+ private move(from: number, direction: 1 | -1): void {
184
+ let index = from
185
+ for (let steps = 0; steps < this.rowMeta.length; steps += 1) {
186
+ index += direction
187
+ if (index < 0 || index >= this.rowMeta.length) return
188
+ if (this.rowMeta[index]?.file) {
189
+ setSelection(this.deps.app.store, "git", index)
190
+ return
191
+ }
192
+ }
193
+ }
194
+
195
+ private selectedFile(index: number): GitFile | undefined {
196
+ return this.rowMeta[index]?.file
197
+ }
198
+
199
+ private async refresh(): Promise<void> {
200
+ await this.deps.app.git.refresh()
201
+ this.deps.app.notify("info", "Git status refreshed")
202
+ }
203
+
204
+ private async stage(index: number): Promise<void> {
205
+ const file = this.selectedFile(index)
206
+ if (!file) return
207
+ try {
208
+ await this.deps.app.git.stage([file.path])
209
+ this.deps.app.notify("success", `Staged ${file.path}`)
210
+ } catch (error) {
211
+ this.deps.app.notify("error", errorMessage(error))
212
+ }
213
+ }
214
+
215
+ private async unstage(index: number): Promise<void> {
216
+ const file = this.selectedFile(index)
217
+ if (!file) return
218
+ try {
219
+ await this.deps.app.git.unstage([file.path])
220
+ this.deps.app.notify("success", `Unstaged ${file.path}`)
221
+ } catch (error) {
222
+ this.deps.app.notify("error", errorMessage(error))
223
+ }
224
+ }
225
+
226
+ /** Destructive: always confirmed, never applied to untracked files. */
227
+ private async discard(index: number): Promise<void> {
228
+ const file = this.selectedFile(index)
229
+ if (!file) return
230
+ if (file.state === "untracked") {
231
+ this.deps.app.notify("warn", "Refusing to delete untracked files; remove them manually")
232
+ return
233
+ }
234
+ const confirmed = await this.deps.app.confirm(
235
+ `Discard changes in ${file.path}?`,
236
+ "This runs `git restore` and permanently throws away your local edits to this file.",
237
+ )
238
+ if (!confirmed) return
239
+ try {
240
+ await this.deps.app.git.discard([file.path])
241
+ this.deps.app.notify("success", `Discarded changes in ${file.path}`)
242
+ } catch (error) {
243
+ this.deps.app.notify("error", errorMessage(error))
244
+ }
245
+ }
246
+ }
247
+
248
+ function stateColor(state: GitFile["state"]): string {
249
+ switch (state) {
250
+ case "staged":
251
+ return theme.success
252
+ case "modified":
253
+ return theme.warning
254
+ case "untracked":
255
+ return theme.textDim
256
+ case "conflicted":
257
+ return theme.danger
258
+ }
259
+ }