@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,122 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Entry point for `devcc` / `pnpm dev:center`.
4
+ *
5
+ * OpenTUI talks to a native Zig core over FFI, which currently means Bun 1.3+
6
+ * or Node 26.4+ with `--experimental-ffi`. The launcher in `bin/devcc.mjs`
7
+ * re-execs under a supported runtime; this file fails loudly if something
8
+ * bypassed it.
9
+ */
10
+ import { createRequire } from "node:module"
11
+ import { pathToFileURL } from "node:url"
12
+ import { App } from "./app/App.js"
13
+ import { errorMessage } from "./utils/errors.js"
14
+ import { findProjectRoot } from "./utils/paths.js"
15
+
16
+ interface CliArgs {
17
+ cwd?: string
18
+ help: boolean
19
+ version: boolean
20
+ }
21
+
22
+ /**
23
+ * Read the version from package.json rather than duplicating it here, so the
24
+ * CLI can never disagree with what was published. `src/devcc/cli.ts` and
25
+ * `dist/devcc/cli.js` sit at the same depth, so one relative path serves both.
26
+ */
27
+ function readVersion(): string {
28
+ try {
29
+ const require = createRequire(import.meta.url)
30
+ const pkg = require("../../package.json") as { version?: unknown }
31
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0"
32
+ } catch {
33
+ return "0.0.0"
34
+ }
35
+ }
36
+
37
+ const VERSION = readVersion()
38
+
39
+ function parseArgs(argv: readonly string[]): CliArgs {
40
+ const args: CliArgs = { help: false, version: false }
41
+ for (let index = 0; index < argv.length; index += 1) {
42
+ const arg = argv[index]
43
+ if (arg === "--help" || arg === "-h") args.help = true
44
+ else if (arg === "--version" || arg === "-v") args.version = true
45
+ else if (arg === "--cwd" || arg === "-C") {
46
+ const value = argv[index + 1]
47
+ if (value) {
48
+ args.cwd = value
49
+ index += 1
50
+ }
51
+ } else if (arg?.startsWith("--cwd=")) {
52
+ args.cwd = arg.slice("--cwd=".length)
53
+ }
54
+ }
55
+ return args
56
+ }
57
+
58
+ const USAGE = `dev command center ${VERSION}
59
+
60
+ Usage:
61
+ devcc [options]
62
+
63
+ Options:
64
+ -C, --cwd <dir> Project directory (default: nearest project root)
65
+ -h, --help Show this help
66
+ -v, --version Show the version
67
+
68
+ Environment:
69
+ DEVCC_LOG_LEVEL debug | info | warn | error | silent (default: info)
70
+
71
+ Configuration:
72
+ Optional .devcc.ts or devcc.config.ts in the project root.
73
+ `
74
+
75
+ export async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
76
+ const args = parseArgs(argv)
77
+
78
+ if (args.help) {
79
+ process.stdout.write(USAGE)
80
+ return 0
81
+ }
82
+ if (args.version) {
83
+ process.stdout.write(`${VERSION}\n`)
84
+ return 0
85
+ }
86
+
87
+ if (!process.stdout.isTTY) {
88
+ process.stderr.write("devcc needs an interactive terminal (stdout is not a TTY).\n")
89
+ return 1
90
+ }
91
+
92
+ const app = new App({ cwd: args.cwd ? args.cwd : findProjectRoot() })
93
+
94
+ try {
95
+ await app.run()
96
+ return 0
97
+ } catch (error) {
98
+ // The renderer may still own the terminal; tear it down before printing.
99
+ try {
100
+ await app.quit()
101
+ } catch {
102
+ // Nothing more we can do here.
103
+ }
104
+ process.stderr.write(`devcc failed to start: ${errorMessage(error)}\n`)
105
+ if (error instanceof Error && error.stack) process.stderr.write(`${error.stack}\n`)
106
+ return 1
107
+ }
108
+ }
109
+
110
+ const entry = process.argv[1]
111
+ const isDirectRun = entry !== undefined && import.meta.url === pathToFileURL(entry).href
112
+
113
+ if (isDirectRun) {
114
+ main()
115
+ .then((code) => {
116
+ process.exitCode = code
117
+ })
118
+ .catch((error: unknown) => {
119
+ process.stderr.write(`${errorMessage(error)}\n`)
120
+ process.exitCode = 1
121
+ })
122
+ }
@@ -0,0 +1,136 @@
1
+ import { BoxRenderable, InputRenderable, TextRenderable, type RenderContext } from "@opentui/core"
2
+ import { theme } from "../app/theme.js"
3
+ import type { CommandDefinition } from "../core/commands.js"
4
+ import { fit } from "../utils/format.js"
5
+ import { Overlay } from "./Overlay.js"
6
+ import { RowList, type ListRow } from "./primitives.js"
7
+ import { chunk, dim, styled } from "./styled.js"
8
+
9
+ /**
10
+ * Fuzzy command palette (`/`).
11
+ *
12
+ * The palette is the discoverable surface for every registered command,
13
+ * including ones contributed by project config, so nothing is reachable only
14
+ * through a hidden key binding.
15
+ */
16
+ export class CommandPalette extends Overlay {
17
+ readonly input: InputRenderable
18
+ private readonly list: RowList
19
+ private readonly counter: TextRenderable
20
+ private results: CommandDefinition[] = []
21
+ private selectedIndex = 0
22
+
23
+ constructor(ctx: RenderContext) {
24
+ super(ctx, {
25
+ title: " Command palette ",
26
+ widthRatio: 0.7,
27
+ minWidth: 42,
28
+ maxWidth: 96,
29
+ heightRatio: 0.6,
30
+ minHeight: 9,
31
+ maxHeight: 24,
32
+ })
33
+
34
+ const inputWrapper = new BoxRenderable(ctx, {
35
+ height: 1,
36
+ flexShrink: 0,
37
+ flexDirection: "row",
38
+ marginTop: 1,
39
+ backgroundColor: theme.panelAlt,
40
+ paddingLeft: 1,
41
+ paddingRight: 1,
42
+ })
43
+ const marker = new TextRenderable(ctx, {
44
+ content: "› ",
45
+ fg: theme.accent,
46
+ width: 2,
47
+ selectable: false,
48
+ })
49
+ this.input = new InputRenderable(ctx, {
50
+ flexGrow: 1,
51
+ backgroundColor: theme.panelAlt,
52
+ textColor: theme.textBright,
53
+ placeholder: "Type a command…",
54
+ placeholderColor: theme.textDim,
55
+ })
56
+ inputWrapper.add(marker)
57
+ inputWrapper.add(this.input)
58
+
59
+ this.list = new RowList(ctx, { marginTop: 1, flexGrow: 1, onViewportChange: () => this.paint() })
60
+
61
+ this.counter = new TextRenderable(ctx, {
62
+ content: "",
63
+ fg: theme.textDim,
64
+ height: 1,
65
+ flexShrink: 0,
66
+ selectable: false,
67
+ truncate: true,
68
+ })
69
+
70
+ this.box.add(inputWrapper)
71
+ this.box.add(this.list.box)
72
+ this.box.add(this.counter)
73
+ }
74
+
75
+ open(): void {
76
+ this.input.value = ""
77
+ this.selectedIndex = 0
78
+ this.show()
79
+ this.input.focus()
80
+ }
81
+
82
+ close(): void {
83
+ this.input.blur()
84
+ this.hide()
85
+ }
86
+
87
+ get query(): string {
88
+ return this.input.value
89
+ }
90
+
91
+ setResults(results: CommandDefinition[]): void {
92
+ this.results = results
93
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, results.length - 1))
94
+ this.paint()
95
+ }
96
+
97
+ move(delta: number): void {
98
+ if (this.results.length === 0) return
99
+ const next = this.selectedIndex + delta
100
+ this.selectedIndex = ((next % this.results.length) + this.results.length) % this.results.length
101
+ this.paint()
102
+ }
103
+
104
+ get selected(): CommandDefinition | undefined {
105
+ return this.results[this.selectedIndex]
106
+ }
107
+
108
+ private paint(): void {
109
+ const width = Math.max(20, this.box.width - 4)
110
+ const categoryWidth = 12
111
+ const shortcutWidth = 8
112
+
113
+ const rows: ListRow[] = this.results.map((command) => ({
114
+ key: command.id,
115
+ content: styled(
116
+ dim(fit(command.category ?? "", categoryWidth)),
117
+ chunk(fit(command.title, Math.max(8, width - categoryWidth - shortcutWidth)), theme.text),
118
+ dim(command.shortcut ? fit(command.shortcut, shortcutWidth) : ""),
119
+ ),
120
+ }))
121
+
122
+ if (rows.length === 0) {
123
+ rows.push({ key: "__empty", content: dimRow("No matching commands"), disabled: true })
124
+ }
125
+
126
+ this.list.setRows(rows, this.selectedIndex)
127
+ this.counter.content = styled(
128
+ dim(`${this.results.length} command${this.results.length === 1 ? "" : "s"}`),
129
+ dim(" ↑↓ move enter run esc close"),
130
+ )
131
+ }
132
+ }
133
+
134
+ function dimRow(text: string) {
135
+ return styled(dim(text))
136
+ }
@@ -0,0 +1,174 @@
1
+ import { BoxRenderable, InputRenderable, TextRenderable, type RenderContext } from "@opentui/core"
2
+ import { theme } from "../app/theme.js"
3
+ import { Overlay } from "./Overlay.js"
4
+ import { chunk, dim, styled } from "./styled.js"
5
+
6
+ /**
7
+ * Yes/no modal for anything destructive or irreversible.
8
+ *
9
+ * Nothing in devcc runs a destructive command without one of these: `git
10
+ * restore`, `git push`, `docker compose down`, and any config-declared command
11
+ * marked `confirm` all route through here.
12
+ */
13
+ export class ConfirmDialog extends Overlay {
14
+ private readonly message: TextRenderable
15
+ private readonly detail: TextRenderable
16
+ private resolver: ((value: boolean) => void) | null = null
17
+
18
+ constructor(ctx: RenderContext) {
19
+ super(ctx, {
20
+ title: " Confirm ",
21
+ widthRatio: 0.5,
22
+ minWidth: 36,
23
+ maxWidth: 72,
24
+ heightRatio: 0.3,
25
+ minHeight: 7,
26
+ maxHeight: 11,
27
+ })
28
+
29
+ this.box.borderColor = theme.warning
30
+ this.box.titleColor = theme.warning
31
+
32
+ this.message = new TextRenderable(ctx, {
33
+ content: "",
34
+ fg: theme.textBright,
35
+ height: 1,
36
+ marginTop: 1,
37
+ selectable: false,
38
+ truncate: true,
39
+ })
40
+ this.detail = new TextRenderable(ctx, {
41
+ content: "",
42
+ fg: theme.text,
43
+ flexGrow: 1,
44
+ wrapMode: "word",
45
+ selectable: false,
46
+ })
47
+ const hints = new TextRenderable(ctx, {
48
+ content: styled(
49
+ chunk("y", theme.success),
50
+ dim(" confirm "),
51
+ chunk("n / esc", theme.danger),
52
+ dim(" cancel"),
53
+ ),
54
+ height: 1,
55
+ selectable: false,
56
+ })
57
+
58
+ this.box.add(this.message)
59
+ this.box.add(this.detail)
60
+ this.box.add(hints)
61
+ }
62
+
63
+ /** Show the dialog; resolves once the user answers or dismisses it. */
64
+ ask(title: string, message: string): Promise<boolean> {
65
+ this.resolve(false)
66
+ this.message.content = title
67
+ this.detail.content = message
68
+ this.show()
69
+ return new Promise<boolean>((resolve) => {
70
+ this.resolver = resolve
71
+ })
72
+ }
73
+
74
+ resolve(value: boolean): void {
75
+ const resolver = this.resolver
76
+ this.resolver = null
77
+ this.hide()
78
+ resolver?.(value)
79
+ }
80
+
81
+ get pending(): boolean {
82
+ return this.resolver !== null
83
+ }
84
+ }
85
+
86
+ /** Single-line text prompt, used for commit messages and similar input. */
87
+ export class PromptDialog extends Overlay {
88
+ readonly input: InputRenderable
89
+ private readonly label: TextRenderable
90
+ private resolver: ((value: string | null) => void) | null = null
91
+
92
+ constructor(ctx: RenderContext) {
93
+ super(ctx, {
94
+ title: " Input ",
95
+ widthRatio: 0.6,
96
+ minWidth: 40,
97
+ maxWidth: 90,
98
+ heightRatio: 0.25,
99
+ minHeight: 7,
100
+ maxHeight: 9,
101
+ })
102
+
103
+ this.label = new TextRenderable(ctx, {
104
+ content: "",
105
+ fg: theme.textBright,
106
+ height: 1,
107
+ marginTop: 1,
108
+ selectable: false,
109
+ truncate: true,
110
+ })
111
+
112
+ const inputWrapper = new BoxRenderable(ctx, {
113
+ height: 3,
114
+ marginTop: 1,
115
+ border: true,
116
+ borderColor: theme.border,
117
+ backgroundColor: theme.panelAlt,
118
+ paddingLeft: 1,
119
+ paddingRight: 1,
120
+ })
121
+ this.input = new InputRenderable(ctx, {
122
+ backgroundColor: theme.panelAlt,
123
+ textColor: theme.textBright,
124
+ placeholderColor: theme.textDim,
125
+ })
126
+ inputWrapper.add(this.input)
127
+
128
+ const hints = new TextRenderable(ctx, {
129
+ content: styled(
130
+ chunk("enter", theme.success),
131
+ dim(" submit "),
132
+ chunk("esc", theme.danger),
133
+ dim(" cancel"),
134
+ ),
135
+ height: 1,
136
+ marginTop: 1,
137
+ selectable: false,
138
+ })
139
+
140
+ this.box.add(this.label)
141
+ this.box.add(inputWrapper)
142
+ this.box.add(hints)
143
+ }
144
+
145
+ ask(title: string, placeholder = ""): Promise<string | null> {
146
+ this.resolve(null)
147
+ this.label.content = title
148
+ this.input.value = ""
149
+ this.input.placeholder = placeholder
150
+ this.show()
151
+ this.input.focus()
152
+ return new Promise<string | null>((resolve) => {
153
+ this.resolver = resolve
154
+ })
155
+ }
156
+
157
+ resolve(value: string | null): void {
158
+ const resolver = this.resolver
159
+ this.resolver = null
160
+ if (resolver) {
161
+ this.input.blur()
162
+ this.hide()
163
+ resolver(value)
164
+ }
165
+ }
166
+
167
+ submit(): void {
168
+ this.resolve(this.input.value)
169
+ }
170
+
171
+ get pending(): boolean {
172
+ return this.resolver !== null
173
+ }
174
+ }
@@ -0,0 +1,85 @@
1
+ import { BoxRenderable, TextRenderable, type RenderContext, type StyledText } from "@opentui/core"
2
+ import { theme } from "../app/theme.js"
3
+ import { chunk, dim, joinStyled, styled } from "./styled.js"
4
+
5
+ export interface KeyHint {
6
+ keys: string
7
+ label: string
8
+ }
9
+
10
+ /**
11
+ * Bottom bar: context-sensitive key hints on the left, transient status on the
12
+ * right. Hints are dropped from the end when the terminal is too narrow.
13
+ */
14
+ export class Footer {
15
+ readonly box: BoxRenderable
16
+ private readonly hints: TextRenderable
17
+ private readonly status: TextRenderable
18
+ private currentHints: KeyHint[] = []
19
+ private width = 80
20
+
21
+ constructor(ctx: RenderContext) {
22
+ this.box = new BoxRenderable(ctx, {
23
+ height: 2,
24
+ flexShrink: 0,
25
+ flexDirection: "row",
26
+ alignItems: "center",
27
+ paddingLeft: 1,
28
+ paddingRight: 1,
29
+ border: ["top"],
30
+ borderColor: theme.border,
31
+ backgroundColor: theme.footerBackground,
32
+ })
33
+
34
+ this.hints = new TextRenderable(ctx, {
35
+ content: "",
36
+ fg: theme.textDim,
37
+ height: 1,
38
+ flexGrow: 1,
39
+ flexShrink: 1,
40
+ selectable: false,
41
+ truncate: true,
42
+ })
43
+ this.status = new TextRenderable(ctx, {
44
+ content: "",
45
+ fg: theme.textDim,
46
+ height: 1,
47
+ flexShrink: 0,
48
+ selectable: false,
49
+ truncate: true,
50
+ })
51
+
52
+ this.box.add(this.hints)
53
+ this.box.add(this.status)
54
+ }
55
+
56
+ setWidth(width: number): void {
57
+ this.width = width
58
+ this.render()
59
+ }
60
+
61
+ setHints(hints: KeyHint[]): void {
62
+ this.currentHints = hints
63
+ this.render()
64
+ }
65
+
66
+ setStatus(message: string | null, level: "info" | "error" | "success" = "info"): void {
67
+ this.status.content = message ? ` ${message} ` : ""
68
+ this.status.fg = level === "error" ? theme.danger : level === "success" ? theme.success : theme.textDim
69
+ }
70
+
71
+ private render(): void {
72
+ const budget = Math.max(10, this.width - 26)
73
+ const parts: StyledText[] = []
74
+ let used = 0
75
+
76
+ for (const hint of this.currentHints) {
77
+ const length = hint.keys.length + hint.label.length + 4
78
+ if (used + length > budget) break
79
+ used += length
80
+ parts.push(styled(chunk(hint.keys, theme.text), " ", dim(hint.label)))
81
+ }
82
+
83
+ this.hints.content = joinStyled(parts, " ")
84
+ }
85
+ }
@@ -0,0 +1,93 @@
1
+ import { BoxRenderable, fg, t, TextRenderable, type RenderContext } from "@opentui/core"
2
+ import { theme } from "../app/theme.js"
3
+ import type { AppState } from "../core/state.js"
4
+ import { formatClock } from "../utils/format.js"
5
+ import { truncatePath, tildify } from "../utils/paths.js"
6
+
7
+ /**
8
+ * Top bar: product name on the left, project identity and clock on the right.
9
+ * Collapses to just the project name on narrow terminals.
10
+ */
11
+ /** `HH:MM` plus a trailing space. */
12
+ const CLOCK_WIDTH = 6
13
+
14
+ export class Header {
15
+ readonly box: BoxRenderable
16
+ private readonly title: TextRenderable
17
+ private readonly spacer: BoxRenderable
18
+ private readonly meta: TextRenderable
19
+ private readonly clock: TextRenderable
20
+
21
+ constructor(ctx: RenderContext) {
22
+ this.box = new BoxRenderable(ctx, {
23
+ height: 2,
24
+ flexShrink: 0,
25
+ flexDirection: "row",
26
+ alignItems: "center",
27
+ paddingLeft: 1,
28
+ paddingRight: 1,
29
+ border: ["bottom"],
30
+ borderColor: theme.border,
31
+ backgroundColor: theme.headerBackground,
32
+ })
33
+
34
+ this.title = new TextRenderable(ctx, {
35
+ content: "DEV COMMAND CENTER",
36
+ fg: theme.accent,
37
+ height: 1,
38
+ selectable: false,
39
+ truncate: true,
40
+ })
41
+ // A flexible spacer absorbs the slack, so the project block and clock sit
42
+ // flush right without any padding arithmetic.
43
+ this.spacer = new BoxRenderable(ctx, {
44
+ flexGrow: 1,
45
+ flexShrink: 1,
46
+ height: 1,
47
+ backgroundColor: "transparent",
48
+ })
49
+ this.meta = new TextRenderable(ctx, {
50
+ content: "",
51
+ fg: theme.textDim,
52
+ height: 1,
53
+ marginLeft: 2,
54
+ flexShrink: 1,
55
+ selectable: false,
56
+ truncate: true,
57
+ })
58
+ // The clock never gets truncated away: it sits outside the flexible column.
59
+ this.clock = new TextRenderable(ctx, {
60
+ content: "",
61
+ fg: theme.text,
62
+ height: 1,
63
+ // Fixed width so the flexible project block can never squeeze it out.
64
+ width: CLOCK_WIDTH,
65
+ flexShrink: 0,
66
+ selectable: false,
67
+ })
68
+
69
+ this.box.add(this.title)
70
+ this.box.add(this.spacer)
71
+ this.box.add(this.meta)
72
+ this.box.add(this.clock)
73
+ }
74
+
75
+ update(state: AppState): void {
76
+ const { width } = state.ui
77
+ const narrow = width < 64
78
+
79
+ this.title.content = narrow ? "DEVCC" : "DEV COMMAND CENTER"
80
+
81
+ const clockText = formatClock(state.ui.clock)
82
+ this.clock.content = t`${fg(theme.text)(clockText)} `
83
+
84
+ // Drop detail as the terminal narrows rather than letting it be clipped.
85
+ const pieces = [state.project.name]
86
+ if (width >= 64) pieces.push(state.project.packageManager.manager)
87
+ if (width >= 96)
88
+ pieces.push(truncatePath(tildify(state.project.rootDir), Math.max(12, Math.floor(width / 3))))
89
+
90
+ const label = pieces.join(" · ")
91
+ this.meta.content = t`${fg(theme.textDim)(label)} `
92
+ }
93
+ }
@@ -0,0 +1,62 @@
1
+ import type { RenderContext } from "@opentui/core"
2
+ import { theme } from "../app/theme.js"
3
+ import { fit } from "../utils/format.js"
4
+ import { Overlay } from "./Overlay.js"
5
+ import { RowList, type ListRow } from "./primitives.js"
6
+ import { chunk, dim, styled } from "./styled.js"
7
+
8
+ export interface HelpEntry {
9
+ keys: string
10
+ description: string
11
+ }
12
+
13
+ export interface HelpSection {
14
+ title: string
15
+ entries: HelpEntry[]
16
+ }
17
+
18
+ /** Scrollable in-app manual (`?` / `F1`). */
19
+ export class HelpOverlay extends Overlay {
20
+ private readonly list: RowList
21
+
22
+ constructor(ctx: RenderContext, sections: HelpSection[]) {
23
+ super(ctx, {
24
+ title: " Manual ",
25
+ // Wide enough that a full description line fits on an 80-column terminal.
26
+ widthRatio: 0.85,
27
+ minWidth: 60,
28
+ maxWidth: 100,
29
+ heightRatio: 0.85,
30
+ minHeight: 10,
31
+ maxHeight: 40,
32
+ })
33
+
34
+ this.list = new RowList(ctx, { marginTop: 1, flexGrow: 1, scrollPadding: 0 })
35
+ this.box.add(this.list.box)
36
+ this.setSections(sections)
37
+ }
38
+
39
+ setSections(sections: HelpSection[]): void {
40
+ const rows: ListRow[] = []
41
+ for (const [index, section] of sections.entries()) {
42
+ if (index > 0) rows.push({ key: `spacer-${index}`, content: "", disabled: true })
43
+ rows.push({
44
+ key: `section-${section.title}`,
45
+ content: styled(chunk(section.title.toUpperCase(), theme.accent)),
46
+ disabled: true,
47
+ })
48
+ for (const entry of section.entries) {
49
+ rows.push({
50
+ key: `${section.title}-${entry.keys}`,
51
+ content: styled(chunk(fit(entry.keys, 14), theme.textBright), dim(entry.description)),
52
+ disabled: true,
53
+ })
54
+ }
55
+ }
56
+ this.list.setRows(rows, 0)
57
+ }
58
+
59
+ scroll(delta: number): void {
60
+ this.list.scrollBy(delta)
61
+ }
62
+ }