@hartvig/developer-control-center 0.8.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 (193) hide show
  1. package/.developer-control-center/metrics.json +1 -0
  2. package/.developer-control-center/status.json +1 -0
  3. package/.developer-control-center/timings.jsonl +3 -0
  4. package/.github/workflows/ci.yml +47 -0
  5. package/AGENTS.md +51 -0
  6. package/PLUGINS.md +145 -0
  7. package/README.md +147 -0
  8. package/developer-control-center.config.example.js +91 -0
  9. package/developer-control-center.config.js +177 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +223 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/config/index.d.ts +3 -0
  15. package/dist/config/index.d.ts.map +1 -0
  16. package/dist/config/index.js +2 -0
  17. package/dist/config/index.js.map +1 -0
  18. package/dist/config/loader.d.ts +4 -0
  19. package/dist/config/loader.d.ts.map +1 -0
  20. package/dist/config/loader.js +96 -0
  21. package/dist/config/loader.js.map +1 -0
  22. package/dist/config/loader.test.d.ts +2 -0
  23. package/dist/config/loader.test.d.ts.map +1 -0
  24. package/dist/config/loader.test.js +25 -0
  25. package/dist/config/loader.test.js.map +1 -0
  26. package/dist/config/presets/node.d.ts +10 -0
  27. package/dist/config/presets/node.d.ts.map +1 -0
  28. package/dist/config/presets/node.js +31 -0
  29. package/dist/config/presets/node.js.map +1 -0
  30. package/dist/config/presets/react.d.ts +10 -0
  31. package/dist/config/presets/react.d.ts.map +1 -0
  32. package/dist/config/presets/react.js +36 -0
  33. package/dist/config/presets/react.js.map +1 -0
  34. package/dist/config/types.d.ts +55 -0
  35. package/dist/config/types.d.ts.map +1 -0
  36. package/dist/config/types.js +2 -0
  37. package/dist/config/types.js.map +1 -0
  38. package/dist/config/types.test.d.ts +2 -0
  39. package/dist/config/types.test.d.ts.map +1 -0
  40. package/dist/config/types.test.js +23 -0
  41. package/dist/config/types.test.js.map +1 -0
  42. package/dist/core/ci.d.ts +6 -0
  43. package/dist/core/ci.d.ts.map +1 -0
  44. package/dist/core/ci.js +22 -0
  45. package/dist/core/ci.js.map +1 -0
  46. package/dist/core/ci.test.d.ts +2 -0
  47. package/dist/core/ci.test.d.ts.map +1 -0
  48. package/dist/core/ci.test.js +45 -0
  49. package/dist/core/ci.test.js.map +1 -0
  50. package/dist/core/event-bus.d.ts +18 -0
  51. package/dist/core/event-bus.d.ts.map +1 -0
  52. package/dist/core/event-bus.js +19 -0
  53. package/dist/core/event-bus.js.map +1 -0
  54. package/dist/core/event-bus.test.d.ts +2 -0
  55. package/dist/core/event-bus.test.d.ts.map +1 -0
  56. package/dist/core/event-bus.test.js +49 -0
  57. package/dist/core/event-bus.test.js.map +1 -0
  58. package/dist/core/index.d.ts +9 -0
  59. package/dist/core/index.d.ts.map +1 -0
  60. package/dist/core/index.js +7 -0
  61. package/dist/core/index.js.map +1 -0
  62. package/dist/core/notifier.d.ts +2 -0
  63. package/dist/core/notifier.d.ts.map +1 -0
  64. package/dist/core/notifier.js +28 -0
  65. package/dist/core/notifier.js.map +1 -0
  66. package/dist/core/notifier.test.d.ts +2 -0
  67. package/dist/core/notifier.test.d.ts.map +1 -0
  68. package/dist/core/notifier.test.js +25 -0
  69. package/dist/core/notifier.test.js.map +1 -0
  70. package/dist/core/runtime.d.ts +25 -0
  71. package/dist/core/runtime.d.ts.map +1 -0
  72. package/dist/core/runtime.js +85 -0
  73. package/dist/core/runtime.js.map +1 -0
  74. package/dist/core/task-runner.d.ts +26 -0
  75. package/dist/core/task-runner.d.ts.map +1 -0
  76. package/dist/core/task-runner.js +354 -0
  77. package/dist/core/task-runner.js.map +1 -0
  78. package/dist/core/timer-plugin.d.ts +3 -0
  79. package/dist/core/timer-plugin.d.ts.map +1 -0
  80. package/dist/core/timer-plugin.js +34 -0
  81. package/dist/core/timer-plugin.js.map +1 -0
  82. package/dist/core/workspaces.d.ts +6 -0
  83. package/dist/core/workspaces.d.ts.map +1 -0
  84. package/dist/core/workspaces.js +60 -0
  85. package/dist/core/workspaces.js.map +1 -0
  86. package/dist/core/workspaces.test.d.ts +2 -0
  87. package/dist/core/workspaces.test.d.ts.map +1 -0
  88. package/dist/core/workspaces.test.js +62 -0
  89. package/dist/core/workspaces.test.js.map +1 -0
  90. package/dist/index.d.ts +16 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +11 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/plugins/index.d.ts +3 -0
  95. package/dist/plugins/index.d.ts.map +1 -0
  96. package/dist/plugins/index.js +2 -0
  97. package/dist/plugins/index.js.map +1 -0
  98. package/dist/plugins/manager.d.ts +13 -0
  99. package/dist/plugins/manager.d.ts.map +1 -0
  100. package/dist/plugins/manager.js +43 -0
  101. package/dist/plugins/manager.js.map +1 -0
  102. package/dist/plugins/manager.test.d.ts +2 -0
  103. package/dist/plugins/manager.test.d.ts.map +1 -0
  104. package/dist/plugins/manager.test.js +79 -0
  105. package/dist/plugins/manager.test.js.map +1 -0
  106. package/dist/plugins/types.d.ts +17 -0
  107. package/dist/plugins/types.d.ts.map +1 -0
  108. package/dist/plugins/types.js +2 -0
  109. package/dist/plugins/types.js.map +1 -0
  110. package/dist/status/index.d.ts +3 -0
  111. package/dist/status/index.d.ts.map +1 -0
  112. package/dist/status/index.js +2 -0
  113. package/dist/status/index.js.map +1 -0
  114. package/dist/status/store.d.ts +18 -0
  115. package/dist/status/store.d.ts.map +1 -0
  116. package/dist/status/store.js +76 -0
  117. package/dist/status/store.js.map +1 -0
  118. package/dist/status/store.test.d.ts +2 -0
  119. package/dist/status/store.test.d.ts.map +1 -0
  120. package/dist/status/store.test.js +107 -0
  121. package/dist/status/store.test.js.map +1 -0
  122. package/dist/status/types.d.ts +12 -0
  123. package/dist/status/types.d.ts.map +1 -0
  124. package/dist/status/types.js +2 -0
  125. package/dist/status/types.js.map +1 -0
  126. package/dist/ui/app.d.ts +10 -0
  127. package/dist/ui/app.d.ts.map +1 -0
  128. package/dist/ui/app.js +479 -0
  129. package/dist/ui/app.js.map +1 -0
  130. package/dist/ui/command-list.d.ts +30 -0
  131. package/dist/ui/command-list.d.ts.map +1 -0
  132. package/dist/ui/command-list.js +45 -0
  133. package/dist/ui/command-list.js.map +1 -0
  134. package/dist/ui/index.d.ts +4 -0
  135. package/dist/ui/index.d.ts.map +1 -0
  136. package/dist/ui/index.js +8 -0
  137. package/dist/ui/index.js.map +1 -0
  138. package/dist/ui/metrics-panel.d.ts +10 -0
  139. package/dist/ui/metrics-panel.d.ts.map +1 -0
  140. package/dist/ui/metrics-panel.js +139 -0
  141. package/dist/ui/metrics-panel.js.map +1 -0
  142. package/dist/ui/panel.d.ts +16 -0
  143. package/dist/ui/panel.d.ts.map +1 -0
  144. package/dist/ui/panel.js +16 -0
  145. package/dist/ui/panel.js.map +1 -0
  146. package/dist/ui/status-panel.d.ts +16 -0
  147. package/dist/ui/status-panel.d.ts.map +1 -0
  148. package/dist/ui/status-panel.js +52 -0
  149. package/dist/ui/status-panel.js.map +1 -0
  150. package/docs/architecture.md +29 -0
  151. package/docs/config.md +15 -0
  152. package/docs/mvp.md +17 -0
  153. package/docs/phases.md +49 -0
  154. package/docs/technical-decisions.md +19 -0
  155. package/docs/ui.md +14 -0
  156. package/package.json +30 -0
  157. package/src/cli.ts +242 -0
  158. package/src/config/index.ts +2 -0
  159. package/src/config/loader.test.ts +30 -0
  160. package/src/config/loader.ts +123 -0
  161. package/src/config/presets/node.ts +30 -0
  162. package/src/config/presets/react.ts +35 -0
  163. package/src/config/types.test.ts +24 -0
  164. package/src/config/types.ts +52 -0
  165. package/src/core/ci.test.ts +54 -0
  166. package/src/core/ci.ts +26 -0
  167. package/src/core/event-bus.test.ts +56 -0
  168. package/src/core/event-bus.ts +34 -0
  169. package/src/core/index.ts +8 -0
  170. package/src/core/notifier.test.ts +30 -0
  171. package/src/core/notifier.ts +34 -0
  172. package/src/core/runtime.ts +99 -0
  173. package/src/core/task-runner.ts +408 -0
  174. package/src/core/timer-plugin.ts +34 -0
  175. package/src/core/workspaces.test.ts +72 -0
  176. package/src/core/workspaces.ts +73 -0
  177. package/src/index.ts +15 -0
  178. package/src/plugins/index.ts +2 -0
  179. package/src/plugins/manager.test.ts +92 -0
  180. package/src/plugins/manager.ts +54 -0
  181. package/src/plugins/types.ts +18 -0
  182. package/src/status/index.ts +2 -0
  183. package/src/status/store.test.ts +122 -0
  184. package/src/status/store.ts +88 -0
  185. package/src/status/types.ts +12 -0
  186. package/src/ui/app.tsx +606 -0
  187. package/src/ui/command-list.tsx +163 -0
  188. package/src/ui/index.tsx +10 -0
  189. package/src/ui/metrics-panel.tsx +234 -0
  190. package/src/ui/panel.tsx +76 -0
  191. package/src/ui/status-panel.tsx +160 -0
  192. package/tsconfig.json +21 -0
  193. package/vitest.config.ts +8 -0
package/src/ui/app.tsx ADDED
@@ -0,0 +1,606 @@
1
+ import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
2
+ import { Box, Text, useInput, useApp, useStdout } from 'ink';
3
+ import { Runtime } from '../core/index.js';
4
+ import { ProkomConfig, ProkomCommand, mergeCommands } from '../config/index.js';
5
+ import { TaskState } from '../status/types.js';
6
+ import { CommandList, MenuGroup, MenuItem, ProfileOption } from './command-list.js';
7
+ import { MetricsPanel } from './metrics-panel.js';
8
+ import { Panel } from './panel.js';
9
+ import { StatusPanel } from './status-panel.js';
10
+
11
+ interface AppProps {
12
+ config: ProkomConfig;
13
+ runtime: Runtime;
14
+ }
15
+
16
+ type Mode = 'normal' | 'search' | 'confirm' | 'input' | 'popup';
17
+
18
+ interface PopupState {
19
+ title: string;
20
+ options: string[];
21
+ selected: number;
22
+ onSelect: (option: string) => void;
23
+ }
24
+ type Pane = 'commands' | 'status';
25
+
26
+ const PROFILE_GROUP_ID = '__profiles';
27
+ const GROUP_ORDER = ['Development', 'Build', 'Deploy', 'Management', 'Demo'];
28
+
29
+ function commandsForProfile(config: ProkomConfig, profile?: string): ProkomCommand[] {
30
+ const base = config.baseCommands ?? config.commands;
31
+ const profileCommands = profile ? (config.profiles?.[profile]?.commands ?? []) : [];
32
+ const commands = profile ? mergeCommands(base, profileCommands) : [...base];
33
+
34
+ for (const pipeline of config.pipelines ?? []) {
35
+ commands.push({
36
+ id: pipeline.id,
37
+ label: pipeline.label,
38
+ description: `Run pipeline: ${pipeline.steps.join(' → ')}`,
39
+ command: '',
40
+ confirm: pipeline.confirm,
41
+ pipelineSteps: pipeline.steps,
42
+ });
43
+ }
44
+
45
+ return commands;
46
+ }
47
+
48
+ function isProfileOption(item: MenuItem): item is ProfileOption {
49
+ return 'kind' in item && item.kind === 'profile';
50
+ }
51
+
52
+ export const App: React.FC<AppProps> = ({ config, runtime }) => {
53
+ const { exit } = useApp();
54
+ const { stdout } = useStdout();
55
+ const [mode, setMode] = useState<Mode>('normal');
56
+ const [focusedPane, setFocusedPane] = useState<Pane>('commands');
57
+ const [selectedIndex, setSelectedIndex] = useState(0);
58
+ const [tasks, setTasks] = useState<Map<string, TaskState>>(new Map());
59
+ const [searchQuery, setSearchQuery] = useState('');
60
+ const [confirmingCmd, setConfirmingCmd] = useState<ProkomCommand | null>(
61
+ null,
62
+ );
63
+ const [inputCmd, setInputCmd] = useState<ProkomCommand | null>(null);
64
+ const [inputValue, setInputValue] = useState('');
65
+ const [popup, setPopup] = useState<PopupState | null>(null);
66
+ const [scrollOffsets, setScrollOffsets] = useState<Map<string, number>>(
67
+ () => new Map(),
68
+ );
69
+ const [currentGroup, setCurrentGroup] = useState<string | null>(null);
70
+ const [multiSelected, setMultiSelected] = useState<Set<string>>(new Set());
71
+ const [activeProfile, setActiveProfile] = useState<string | undefined>(config.profile);
72
+ const menuRows = config.menuRows ?? 8;
73
+ const outputRows = config.outputRows ?? menuRows;
74
+ const terminalColumns = stdout?.columns ?? 120;
75
+ const availablePaneWidth = Math.max(90, terminalColumns - 4);
76
+ const statusPaneWidth = 28;
77
+ const commandPaneWidth = Math.min(52, Math.max(34, Math.floor(availablePaneWidth * 0.34)));
78
+ const outputPaneWidth = Math.max(30, availablePaneWidth - commandPaneWidth - statusPaneWidth - 2);
79
+
80
+ const activeCommands = useMemo(
81
+ () => commandsForProfile(config, activeProfile),
82
+ [activeProfile, config],
83
+ );
84
+
85
+ useEffect(() => {
86
+ runtime.taskRunner.setCommands(activeCommands);
87
+ }, [activeCommands, runtime]);
88
+
89
+ const modeRef = useRef(mode);
90
+ modeRef.current = mode;
91
+ const confirmingCmdRef = useRef(confirmingCmd);
92
+ confirmingCmdRef.current = confirmingCmd;
93
+ const inputCmdRef = useRef(inputCmd);
94
+ inputCmdRef.current = inputCmd;
95
+ const inputValueRef = useRef(inputValue);
96
+ inputValueRef.current = inputValue;
97
+ const tasksRef = useRef(tasks);
98
+ tasksRef.current = tasks;
99
+ const commandsRef = useRef(activeCommands);
100
+ commandsRef.current = activeCommands;
101
+ const popupRef = useRef(popup);
102
+ popupRef.current = popup;
103
+
104
+ useEffect(() => {
105
+ const onComplete = (id: string, exitCode: number | null) => {
106
+ if (exitCode != null && exitCode > 0) {
107
+ const cmd = commandsRef.current.find((c) => c.id === id);
108
+ if (cmd?.onNonZeroExit) {
109
+ setConfirmingCmd({
110
+ id: `${cmd.id}:on-nonzero`,
111
+ label: cmd.onNonZeroExit.label,
112
+ command: cmd.onNonZeroExit.command,
113
+ });
114
+ setMode('confirm');
115
+ }
116
+ }
117
+ };
118
+ runtime.eventBus.on('task:complete', onComplete);
119
+
120
+ const unsub = runtime.statusStore.subscribe((updated) => {
121
+ const map = new Map(updated);
122
+ setTasks(map);
123
+ setScrollOffsets((prev) => {
124
+ const next = new Map(prev);
125
+ for (const id of next.keys()) {
126
+ if (!map.has(id)) next.delete(id);
127
+ }
128
+ return next;
129
+ });
130
+ });
131
+ return () => {
132
+ runtime.eventBus.off('task:complete', onComplete);
133
+ unsub();
134
+ };
135
+ }, [runtime]);
136
+
137
+ const groups = useMemo(() => {
138
+ const seen = new Set<string>();
139
+ const result: MenuGroup[] = [];
140
+ for (const cmd of activeCommands) {
141
+ if (cmd.group && !seen.has(cmd.group)) {
142
+ seen.add(cmd.group);
143
+ const count = activeCommands.filter(
144
+ (c) => c.group === cmd.group,
145
+ ).length;
146
+ result.push({ id: `__group_${cmd.group}`, label: cmd.group, count });
147
+ }
148
+ }
149
+ return result.sort((a, b) => {
150
+ const ai = GROUP_ORDER.indexOf(a.label);
151
+ const bi = GROUP_ORDER.indexOf(b.label);
152
+ if (ai !== -1 && bi !== -1) return ai - bi;
153
+ if (ai !== -1) return -1;
154
+ if (bi !== -1) return 1;
155
+ return a.label.localeCompare(b.label);
156
+ });
157
+ }, [activeCommands]);
158
+
159
+ const hasGroups = groups.length > 0;
160
+ const profileNames = Object.keys(config.profiles ?? {});
161
+ const hasProfiles = profileNames.length > 0;
162
+
163
+ const menuItems = useMemo((): MenuItem[] => {
164
+ if (currentGroup === PROFILE_GROUP_ID) {
165
+ const defaultProfile: ProfileOption = {
166
+ kind: 'profile',
167
+ id: '__profile_default',
168
+ label: 'Default',
169
+ active: !activeProfile,
170
+ };
171
+ return [
172
+ defaultProfile,
173
+ ...profileNames.map((profile): ProfileOption => ({
174
+ kind: 'profile',
175
+ id: `__profile_${profile}`,
176
+ label: profile,
177
+ profile,
178
+ active: activeProfile === profile,
179
+ })),
180
+ ];
181
+ }
182
+
183
+ const profileGroup: MenuGroup[] = hasProfiles
184
+ ? [{ id: PROFILE_GROUP_ID, label: 'Profiles', count: profileNames.length + 1 }]
185
+ : [];
186
+
187
+ const pipelines = activeCommands.filter((c) => c.pipelineSteps);
188
+ const ungrouped = activeCommands.filter((c) => !c.group && !c.pipelineSteps);
189
+
190
+ if (!hasGroups) return [...ungrouped, ...profileGroup, ...pipelines];
191
+
192
+ if (currentGroup) {
193
+ return activeCommands.filter((c) => c.group === currentGroup);
194
+ }
195
+
196
+ return [...groups, ...ungrouped, ...profileGroup, ...pipelines];
197
+ }, [activeCommands, activeProfile, currentGroup, groups, hasGroups, hasProfiles, profileNames]);
198
+
199
+ const filteredItems = useMemo((): MenuItem[] => {
200
+ if (!searchQuery) return menuItems;
201
+ return menuItems.filter((item) =>
202
+ item.label.toLowerCase().includes(searchQuery.toLowerCase()),
203
+ );
204
+ }, [menuItems, searchQuery]);
205
+
206
+ useEffect(() => {
207
+ if (selectedIndex >= filteredItems.length && filteredItems.length > 0) {
208
+ setSelectedIndex(filteredItems.length - 1);
209
+ }
210
+ }, [filteredItems.length, selectedIndex]);
211
+
212
+ const breadcrumb = hasGroups && currentGroup
213
+ ? `› ${currentGroup === PROFILE_GROUP_ID ? 'Profiles' : currentGroup}`
214
+ : undefined;
215
+
216
+ const selCount = multiSelected.size;
217
+ const selectedItem = filteredItems[selectedIndex];
218
+ const footerText = selectedItem
219
+ ? isProfileOption(selectedItem)
220
+ ? selectedItem.active
221
+ ? `Active profile: ${selectedItem.label}`
222
+ : `Switch to ${selectedItem.label} profile`
223
+ : 'count' in selectedItem
224
+ ? `Open ${selectedItem.label}`
225
+ : selectedItem.description ?? 'Enter to run, Space to select, / to search, Tab to focus output'
226
+ : 'Enter to run, Space to select, / to search, Tab to focus output';
227
+
228
+ const runSingle = useCallback((cmd: ProkomCommand) => {
229
+ if (cmd.id === 'demo-confirm-overlay') {
230
+ setPopup({
231
+ title: 'Confirm action',
232
+ options: ['Yes', 'No'],
233
+ selected: 0,
234
+ onSelect: (option) => {
235
+ runtime.taskRunner.run({
236
+ id: 'demo-confirm-result',
237
+ label: `Demo overlay: ${option}`,
238
+ command: `echo "You selected: ${option}"`,
239
+ }).catch(() => {});
240
+ },
241
+ });
242
+ setMode('popup');
243
+ return;
244
+ }
245
+ if (cmd.toggle) {
246
+ const task = tasksRef.current.get(cmd.id);
247
+ if (task?.status === 'running') {
248
+ runtime.taskRunner.stop(cmd);
249
+ } else {
250
+ runtime.taskRunner.run(cmd);
251
+ }
252
+ } else if (cmd.confirm) {
253
+ setConfirmingCmd(cmd);
254
+ setMode('confirm');
255
+ } else if (cmd.input) {
256
+ setInputCmd(cmd);
257
+ setInputValue('');
258
+ setMode('input');
259
+ } else {
260
+ runtime.taskRunner.run(cmd);
261
+ }
262
+ }, [runtime]);
263
+
264
+ function selectItem(item: MenuItem): void {
265
+ if (isProfileOption(item)) {
266
+ setActiveProfile(item.profile);
267
+ setCurrentGroup(null);
268
+ setSelectedIndex(0);
269
+ setMultiSelected(new Set());
270
+ return;
271
+ }
272
+
273
+ if ('count' in item) {
274
+ setCurrentGroup(item.id === PROFILE_GROUP_ID ? PROFILE_GROUP_ID : item.label);
275
+ setSelectedIndex(0);
276
+ return;
277
+ }
278
+ runSingle(item);
279
+ }
280
+
281
+ function runMultiSelected(): void {
282
+ const selected = new Set(multiSelected);
283
+ setMultiSelected(new Set());
284
+ for (const cmd of activeCommands) {
285
+ if (selected.has(cmd.id)) {
286
+ if (!cmd.confirm && !cmd.input) {
287
+ runtime.taskRunner.run(cmd);
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ useInput((input, key) => {
294
+ if (modeRef.current === 'confirm') {
295
+ if (input === 'y' || input === 'Y' || key.return || input === '\r') {
296
+ const cmd = confirmingCmdRef.current;
297
+ setConfirmingCmd(null);
298
+ if (cmd) {
299
+ if (cmd.input) {
300
+ setInputCmd(cmd);
301
+ setInputValue('');
302
+ setMode('input');
303
+ } else {
304
+ setMode('normal');
305
+ runtime.taskRunner.run(cmd).catch((e: unknown) => {
306
+ process.stderr.write(`[dcc] run error: ${e}\n`);
307
+ });
308
+ }
309
+ } else {
310
+ setMode('normal');
311
+ }
312
+ } else if (input === 'n' || input === 'N' || key.escape) {
313
+ setConfirmingCmd(null);
314
+ setMode('normal');
315
+ }
316
+ return;
317
+ }
318
+
319
+ if (modeRef.current === 'input') {
320
+ const cmd = inputCmdRef.current;
321
+ if (key.escape) {
322
+ setInputCmd(null);
323
+ setInputValue('');
324
+ setMode('normal');
325
+ } else if (key.return || input === '\r') {
326
+ setInputCmd(null);
327
+ setMode('normal');
328
+ if (cmd && cmd.command) {
329
+ let msg = inputValueRef.current;
330
+ if (!msg && cmd.input?.default) msg = cmd.input.default;
331
+ runtime.taskRunner.run(
332
+ { ...cmd, command: cmd.command.replace(/\{input\}/g, msg) },
333
+ ).catch((e: unknown) => {
334
+ process.stderr.write(`[dcc] run error: ${e}\n`);
335
+ });
336
+ }
337
+ } else if (key.backspace || key.delete) {
338
+ setInputValue((v) => v.slice(0, -1));
339
+ } else if (input && !key.ctrl && !key.meta && input.length === 1) {
340
+ if (input >= ' ' && input !== '\x7f') {
341
+ setInputValue((v) => v + input);
342
+ }
343
+ }
344
+ return;
345
+ }
346
+
347
+ if (modeRef.current === 'popup') {
348
+ const p = popupRef.current;
349
+ if (key.escape) {
350
+ setPopup(null);
351
+ setMode('normal');
352
+ } else if (key.upArrow && p) {
353
+ setPopup({ ...p, selected: Math.max(0, p.selected - 1) });
354
+ } else if (key.downArrow && p) {
355
+ setPopup({ ...p, selected: Math.min(p.options.length - 1, p.selected + 1) });
356
+ } else if (key.return && p) {
357
+ const chosen = p.options[p.selected];
358
+ setPopup(null);
359
+ setMode('normal');
360
+ p.onSelect(chosen);
361
+ }
362
+ return;
363
+ }
364
+
365
+ if (mode === 'search') {
366
+ if (key.escape) {
367
+ setSearchQuery('');
368
+ setMode('normal');
369
+ } else if (key.return) {
370
+ const item = filteredItems[selectedIndex];
371
+ if (item) selectItem(item);
372
+ setSearchQuery('');
373
+ setMode('normal');
374
+ } else if (key.backspace || key.delete) {
375
+ setSearchQuery((q) => q.slice(0, -1));
376
+ } else if (input && !key.ctrl && !key.meta && input.length === 1) {
377
+ if (input >= ' ' && input !== '\x7f') {
378
+ setSearchQuery((q) => q + input);
379
+ }
380
+ }
381
+ if (key.upArrow) {
382
+ setSelectedIndex((i) => Math.max(0, i - 1));
383
+ } else if (key.downArrow) {
384
+ setSelectedIndex((i) => {
385
+ const max = Math.max(0, filteredItems.length - 1);
386
+ return Math.min(max, i + 1);
387
+ });
388
+ }
389
+ return;
390
+ }
391
+
392
+ if (key.tab) {
393
+ setFocusedPane((p) => (p === 'commands' ? 'status' : 'commands'));
394
+ return;
395
+ }
396
+
397
+ if (focusedPane === 'status') {
398
+ if (key.escape) {
399
+ setFocusedPane('commands');
400
+ } else if (key.upArrow) {
401
+ setScrollOffsets((prev) => {
402
+ const next = new Map(prev);
403
+ const entries = Array.from(tasks.values()).sort(
404
+ (a, b) => (b.startTime || 0) - (a.startTime || 0),
405
+ );
406
+ const target = entries[0];
407
+ if (target) {
408
+ const current = next.get(target.id) ?? 0;
409
+ next.set(target.id, current + 1);
410
+ }
411
+ return next;
412
+ });
413
+ } else if (key.downArrow) {
414
+ setScrollOffsets((prev) => {
415
+ const next = new Map(prev);
416
+ const entries = Array.from(tasks.values()).sort(
417
+ (a, b) => (b.startTime || 0) - (a.startTime || 0),
418
+ );
419
+ const target = entries[0];
420
+ if (target) {
421
+ const current = next.get(target.id) ?? 0;
422
+ next.set(target.id, Math.max(0, current - 1));
423
+ }
424
+ return next;
425
+ });
426
+ } else if (key.pageUp) {
427
+ setScrollOffsets((prev) => {
428
+ const next = new Map(prev);
429
+ const entries = Array.from(tasks.values()).sort(
430
+ (a, b) => (b.startTime || 0) - (a.startTime || 0),
431
+ );
432
+ const target = entries[0];
433
+ if (target) {
434
+ const current = next.get(target.id) ?? 0;
435
+ next.set(target.id, current + 10);
436
+ }
437
+ return next;
438
+ });
439
+ } else if (key.pageDown) {
440
+ setScrollOffsets((prev) => {
441
+ const next = new Map(prev);
442
+ const entries = Array.from(tasks.values()).sort(
443
+ (a, b) => (b.startTime || 0) - (a.startTime || 0),
444
+ );
445
+ const target = entries[0];
446
+ if (target) {
447
+ const current = next.get(target.id) ?? 0;
448
+ next.set(target.id, Math.max(0, current - 10));
449
+ }
450
+ return next;
451
+ });
452
+ }
453
+ return;
454
+ }
455
+
456
+ if (key.upArrow) {
457
+ setSelectedIndex((i) => Math.max(0, i - 1));
458
+ } else if (key.downArrow) {
459
+ setSelectedIndex((i) => {
460
+ const max = Math.max(0, filteredItems.length - 1);
461
+ return Math.min(max, i + 1);
462
+ });
463
+ } else if (key.return) {
464
+ if (multiSelected.size > 0) {
465
+ runMultiSelected();
466
+ } else {
467
+ const item = filteredItems[selectedIndex];
468
+ if (item) selectItem(item);
469
+ }
470
+ } else if (input === ' ') {
471
+ const item = filteredItems[selectedIndex];
472
+ if (item && isProfileOption(item)) {
473
+ selectItem(item);
474
+ } else if (item && !('count' in item)) {
475
+ setMultiSelected((prev) => {
476
+ const next = new Set(prev);
477
+ if (next.has(item.id)) {
478
+ next.delete(item.id);
479
+ } else {
480
+ next.add(item.id);
481
+ }
482
+ return next;
483
+ });
484
+ } else if (item) {
485
+ setCurrentGroup(item.id === PROFILE_GROUP_ID ? PROFILE_GROUP_ID : item.label);
486
+ setSelectedIndex(0);
487
+ }
488
+ } else if (input === '/') {
489
+ setSearchQuery('');
490
+ setMode('search');
491
+ } else if (key.escape) {
492
+ if (multiSelected.size > 0) {
493
+ setMultiSelected(new Set());
494
+ } else if (hasGroups && currentGroup) {
495
+ setCurrentGroup(null);
496
+ setSelectedIndex(0);
497
+ } else {
498
+ runtime.stop();
499
+ exit();
500
+ }
501
+ } else if (key.ctrl && input === 'c') {
502
+ runtime.stop();
503
+ exit();
504
+ }
505
+ });
506
+
507
+ return (
508
+ <Box flexDirection="column" padding={1}>
509
+ <Box>
510
+ <Text bold color="cyan">Prokom</Text>
511
+ <Text color="gray"> in </Text>
512
+ <Text bold>{config.name}</Text>
513
+ {runtime.gitBranch && (
514
+ <Text>
515
+ <Text color="gray"> </Text>
516
+ <Text color="cyan">⎇ {runtime.gitBranch}</Text>
517
+ </Text>
518
+ )}
519
+ {runtime.workspaces.length > 0 && (
520
+ <Text>
521
+ <Text color="gray"> </Text>
522
+ <Text color="magenta">⊞ {runtime.workspaces.length}</Text>
523
+ </Text>
524
+ )}
525
+ {activeProfile && (
526
+ <Text>
527
+ <Text color="gray"> </Text>
528
+ <Text color="yellow">⚙ {activeProfile}</Text>
529
+ </Text>
530
+ )}
531
+ {runtime.ci.isCI && (
532
+ <Text>
533
+ <Text color="gray"> </Text>
534
+ <Text color="red">⊡ {runtime.ci.name}</Text>
535
+ </Text>
536
+ )}
537
+ </Box>
538
+
539
+ {mode === 'search' && (
540
+ <Box marginY={1}>
541
+ <Text color="cyan">🔍</Text>
542
+ <Text> {searchQuery}</Text>
543
+ <Text color="gray"> ({filteredItems.length})</Text>
544
+ </Box>
545
+ )}
546
+
547
+ <Box marginTop={1}>
548
+ {mode === 'popup' && popup ? (
549
+ <Panel
550
+ title="Commands"
551
+ borderColor="cyan"
552
+ height={menuRows + 2}
553
+ width={commandPaneWidth}
554
+ >
555
+ <Box flexDirection="column" paddingLeft={2} paddingTop={1}>
556
+ <Box>
557
+ <Text bold color="cyan">{popup.title}</Text>
558
+ </Box>
559
+ <Box flexDirection="column" marginTop={1}>
560
+ {popup.options.map((option, i) => (
561
+ <Box key={option}>
562
+ <Text color={i === popup.selected ? 'green' : 'gray'}>
563
+ {i === popup.selected ? '❯' : ' '} {option}
564
+ </Text>
565
+ </Box>
566
+ ))}
567
+ </Box>
568
+ <Box marginTop={1}>
569
+ <Text color="gray">↑↓ navigate · Enter select · Esc cancel</Text>
570
+ </Box>
571
+ </Box>
572
+ </Panel>
573
+ ) : (
574
+ <CommandList
575
+ items={filteredItems}
576
+ width={commandPaneWidth}
577
+ tasks={tasks}
578
+ selectedIndex={selectedIndex}
579
+ multiSelected={multiSelected}
580
+ selCount={selCount}
581
+ breadcrumb={breadcrumb}
582
+ focused={focusedPane === 'commands'}
583
+ menuRows={menuRows}
584
+ />
585
+ )}
586
+ <Box width={1} />
587
+ <MetricsPanel tasks={tasks} menuRows={outputRows} width={statusPaneWidth} />
588
+ <Box width={1} />
589
+ <StatusPanel
590
+ tasks={tasks}
591
+ width={outputPaneWidth}
592
+ scrollOffsets={scrollOffsets}
593
+ focusedPane={focusedPane}
594
+ confirmingCommand={mode === 'confirm' ? confirmingCmd : null}
595
+ inputCommand={mode === 'input' ? inputCmd : null}
596
+ inputValue={inputValue}
597
+ menuRows={outputRows}
598
+ />
599
+ </Box>
600
+
601
+ <Box>
602
+ <Text color="gray">{footerText}</Text>
603
+ </Box>
604
+ </Box>
605
+ );
606
+ };