dev-cockpit 0.1.0 → 0.2.1

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 (140) hide show
  1. package/README.md +64 -29
  2. package/bin/dev-cockpit.mjs +26 -4
  3. package/dist/actions/builtin.d.ts +25 -0
  4. package/dist/actions/builtin.d.ts.map +1 -0
  5. package/dist/actions/dispatch.d.ts +21 -0
  6. package/dist/actions/dispatch.d.ts.map +1 -0
  7. package/dist/actions/registry.d.ts +11 -0
  8. package/dist/actions/registry.d.ts.map +1 -0
  9. package/dist/actions/types.d.ts +76 -0
  10. package/dist/actions/types.d.ts.map +1 -0
  11. package/dist/buildCli.d.ts.map +1 -1
  12. package/dist/chunk-6XGHLLYT.js +46 -0
  13. package/dist/chunk-6XGHLLYT.js.map +7 -0
  14. package/dist/chunk-Q6677JQF.js +32609 -0
  15. package/dist/chunk-Q6677JQF.js.map +7 -0
  16. package/dist/chunk-VN6UILQW.js +1460 -0
  17. package/dist/chunk-VN6UILQW.js.map +7 -0
  18. package/dist/cockpit/Cockpit.d.ts +6 -0
  19. package/dist/cockpit/Cockpit.d.ts.map +1 -1
  20. package/dist/cockpit/Footer.d.ts +6 -4
  21. package/dist/cockpit/Footer.d.ts.map +1 -1
  22. package/dist/cockpit/TabBar.d.ts.map +1 -1
  23. package/dist/cockpit/hooks/useGlobalKeys.d.ts +15 -15
  24. package/dist/cockpit/hooks/useGlobalKeys.d.ts.map +1 -1
  25. package/dist/cockpit/hooks/useTerminalWidth.d.ts +12 -0
  26. package/dist/cockpit/hooks/useTerminalWidth.d.ts.map +1 -0
  27. package/dist/cockpit/panes/CommandModal.d.ts +18 -0
  28. package/dist/cockpit/panes/CommandModal.d.ts.map +1 -0
  29. package/dist/cockpit/panes/Help.d.ts.map +1 -1
  30. package/dist/cockpit/panes/Output.d.ts +7 -0
  31. package/dist/cockpit/panes/Output.d.ts.map +1 -1
  32. package/dist/cockpit/panes/Repos.d.ts.map +1 -1
  33. package/dist/cockpit/state/store.d.ts +14 -11
  34. package/dist/cockpit/state/store.d.ts.map +1 -1
  35. package/dist/cockpit/tab-state.d.ts +12 -0
  36. package/dist/cockpit/tab-state.d.ts.map +1 -1
  37. package/dist/commands/dev.d.ts.map +1 -1
  38. package/dist/commands/init-config-wizard.d.ts +103 -2
  39. package/dist/commands/init-config-wizard.d.ts.map +1 -1
  40. package/dist/commands/init-config.d.ts.map +1 -1
  41. package/dist/commands/migrate-config.d.ts +18 -0
  42. package/dist/commands/migrate-config.d.ts.map +1 -0
  43. package/dist/commands/mount.d.ts +17 -32
  44. package/dist/commands/mount.d.ts.map +1 -1
  45. package/dist/core/config.d.ts +73 -5
  46. package/dist/core/config.d.ts.map +1 -1
  47. package/dist/core/migrations.d.ts +33 -0
  48. package/dist/core/migrations.d.ts.map +1 -0
  49. package/dist/core/subprocess.d.ts +20 -0
  50. package/dist/core/subprocess.d.ts.map +1 -1
  51. package/dist/core/types.d.ts +36 -12
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/devtools-YXMW6JJ6.js +3720 -0
  54. package/dist/devtools-YXMW6JJ6.js.map +7 -0
  55. package/dist/docker/highlights.d.ts +14 -4
  56. package/dist/docker/highlights.d.ts.map +1 -1
  57. package/dist/docker/logs.d.ts +3 -2
  58. package/dist/docker/logs.d.ts.map +1 -1
  59. package/dist/health/builtin.d.ts.map +1 -1
  60. package/dist/index.d.ts +14 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +92837 -53
  63. package/dist/index.js.map +7 -0
  64. package/dist/ink.js +38 -1
  65. package/dist/ink.js.map +7 -0
  66. package/dist/mount/compose.d.ts +21 -0
  67. package/dist/mount/compose.d.ts.map +1 -0
  68. package/dist/mount/discovery.d.ts +35 -0
  69. package/dist/mount/discovery.d.ts.map +1 -0
  70. package/dist/mount/git-status.d.ts +12 -0
  71. package/dist/mount/git-status.d.ts.map +1 -0
  72. package/dist/mount/manifest.d.ts +16 -0
  73. package/dist/mount/manifest.d.ts.map +1 -0
  74. package/dist/mount/symlinks.d.ts +30 -0
  75. package/dist/mount/symlinks.d.ts.map +1 -0
  76. package/dist/mount/types.d.ts +60 -0
  77. package/dist/mount/types.d.ts.map +1 -0
  78. package/dist/react.js +35 -1
  79. package/dist/react.js.map +7 -0
  80. package/dist/runCockpit.d.ts +3 -0
  81. package/dist/runCockpit.d.ts.map +1 -1
  82. package/docs/commands.md +29 -16
  83. package/docs/config-reference.md +115 -11
  84. package/docs/getting-started.md +9 -6
  85. package/docs/index.md +5 -1
  86. package/docs/init-config.md +34 -8
  87. package/docs/mount.md +198 -25
  88. package/docs/notifications.md +14 -13
  89. package/docs/panes.md +36 -15
  90. package/docs/processes.md +42 -0
  91. package/package.json +93 -90
  92. package/dist/buildCli.js +0 -107
  93. package/dist/cli.js +0 -2
  94. package/dist/cockpit/Cockpit.js +0 -73
  95. package/dist/cockpit/Footer.js +0 -33
  96. package/dist/cockpit/TabBar.js +0 -12
  97. package/dist/cockpit/help/content.js +0 -22
  98. package/dist/cockpit/help/loader.js +0 -118
  99. package/dist/cockpit/help/renderer.js +0 -35
  100. package/dist/cockpit/help/types.js +0 -1
  101. package/dist/cockpit/hooks/useCockpitStore.js +0 -5
  102. package/dist/cockpit/hooks/useGlobalKeys.js +0 -173
  103. package/dist/cockpit/panes/FilterModal.js +0 -22
  104. package/dist/cockpit/panes/Health.js +0 -30
  105. package/dist/cockpit/panes/Help.js +0 -81
  106. package/dist/cockpit/panes/Output.js +0 -108
  107. package/dist/cockpit/panes/Repos.js +0 -48
  108. package/dist/cockpit/panes/SearchModal.js +0 -31
  109. package/dist/cockpit/state/store.js +0 -111
  110. package/dist/cockpit/tab-state.js +0 -7
  111. package/dist/commands/dev.js +0 -158
  112. package/dist/commands/doctor.js +0 -66
  113. package/dist/commands/init-config-wizard.js +0 -818
  114. package/dist/commands/init-config.js +0 -131
  115. package/dist/commands/mount.js +0 -150
  116. package/dist/core/config.js +0 -152
  117. package/dist/core/logger.js +0 -38
  118. package/dist/core/notifier.js +0 -100
  119. package/dist/core/paths.js +0 -18
  120. package/dist/core/subprocess.js +0 -82
  121. package/dist/core/types.js +0 -1
  122. package/dist/docker/highlights.js +0 -79
  123. package/dist/docker/logs.js +0 -172
  124. package/dist/docker/restart.js +0 -45
  125. package/dist/docker/stack-trace.js +0 -44
  126. package/dist/health/builtin.js +0 -144
  127. package/dist/health/context.js +0 -31
  128. package/dist/health/notify-resolver.js +0 -28
  129. package/dist/health/registry.js +0 -64
  130. package/dist/health/remediations.js +0 -41
  131. package/dist/health/runner.js +0 -22
  132. package/dist/health/scheduler.js +0 -107
  133. package/dist/health/types.js +0 -1
  134. package/dist/health/useHealth.js +0 -122
  135. package/dist/lint/reactive.js +0 -131
  136. package/dist/runCockpit.js +0 -75
  137. package/dist/watchers/manager.js +0 -239
  138. package/dist/watchers/path-mapper.js +0 -29
  139. package/dist/watchers/types.js +0 -9
  140. package/docs/watchers.md +0 -27
@@ -1,173 +0,0 @@
1
- /**
2
- * Global keyboard handler for the cockpit TUI.
3
- *
4
- * Globals (active in every tab):
5
- * ← / → cycle tabs (backward / forward)
6
- * tab / shift+tab cycle tabs (alias) — except inside Help, where these
7
- * are repurposed for page navigation. Use ←/→ to leave.
8
- * q onQuit()
9
- * n toggleSessionNotifications
10
- *
11
- * Per-tab keys are dispatched to the relevant handler callback. Domain-aware
12
- * behavior (rebuild, lint, remediation lookup, editor launch) lives in the
13
- * profile / consumer; this hook just routes keystrokes to the handlers and
14
- * manages the minimum-visibility-hold for in-flight banners.
15
- */
16
- import { useInput } from 'ink';
17
- import { cockpitStore } from '../state/store.js';
18
- /**
19
- * Minimum time an in-flight-action banner stays visible, even if the
20
- * underlying work completes faster. Without this, fast-finishing actions
21
- * (instant failures, no-op installs) flash by unnoticed.
22
- */
23
- const MIN_ACTION_BANNER_MS = 1500;
24
- function withMinHold(promise, onDone) {
25
- const startedAt = Date.now();
26
- void promise.finally(() => {
27
- const elapsed = Date.now() - startedAt;
28
- const remaining = Math.max(0, MIN_ACTION_BANNER_MS - elapsed);
29
- if (remaining === 0)
30
- onDone();
31
- else
32
- setTimeout(onDone, remaining);
33
- });
34
- }
35
- export function useGlobalKeys(opts) {
36
- const { onQuit, runRepoAction, onWatchToggle, onLint, onOpenError, runRemediation } = opts;
37
- useInput((input, key) => {
38
- const state = cockpitStore.getState();
39
- // Modal is open — don't process global keys.
40
- if (state.activeModal !== null)
41
- return;
42
- // Tab navigation (always available)
43
- if (key.leftArrow) {
44
- state.cycleFocus('backward');
45
- return;
46
- }
47
- if (key.rightArrow) {
48
- state.cycleFocus('forward');
49
- return;
50
- }
51
- // Tab / Shift-Tab cycle tabs globally — but inside Help they're aliased
52
- // to page navigation by the Help pane itself.
53
- if (state.focus !== 'help') {
54
- if (key.tab && key.shift) {
55
- state.cycleFocus('backward');
56
- return;
57
- }
58
- if (key.tab) {
59
- state.cycleFocus('forward');
60
- return;
61
- }
62
- }
63
- if (input === 'q') {
64
- onQuit();
65
- return;
66
- }
67
- if (input === 'n') {
68
- state.toggleSessionNotifications();
69
- return;
70
- }
71
- // ── Repos ─────────────────────────────────────────────────────────────
72
- if (state.focus === 'repos') {
73
- if (key.upArrow) {
74
- state.moveSelection('up');
75
- return;
76
- }
77
- if (key.downArrow) {
78
- state.moveSelection('down');
79
- return;
80
- }
81
- const selectedKey = state.repoOrder[state.selectedRepoIndex];
82
- const selectedEntry = selectedKey ? state.repos[selectedKey] : undefined;
83
- if (input === 'r') {
84
- if (!selectedKey || !selectedEntry || !runRepoAction)
85
- return;
86
- const dispatch = runRepoAction(selectedKey, selectedEntry);
87
- if (!dispatch)
88
- return;
89
- state.setActiveRepoAction({ repoKey: selectedKey, label: dispatch.label });
90
- withMinHold(dispatch.promise, () => cockpitStore.getState().setActiveRepoAction(null));
91
- return;
92
- }
93
- if (input === 'w') {
94
- if (selectedKey && selectedEntry && selectedEntry.kind === 'repo' && onWatchToggle) {
95
- onWatchToggle(selectedKey, selectedEntry);
96
- }
97
- return;
98
- }
99
- if (input === 'l') {
100
- if (selectedKey && selectedEntry && selectedEntry.kind === 'repo' && onLint) {
101
- onLint(selectedKey, selectedEntry);
102
- }
103
- return;
104
- }
105
- }
106
- // ── Output ────────────────────────────────────────────────────────────
107
- if (state.focus === 'output') {
108
- if (input === 'c') {
109
- state.clearOutput();
110
- return;
111
- }
112
- if (input === '/') {
113
- state.setActiveModal('search');
114
- return;
115
- }
116
- if (input === 'f') {
117
- state.setActiveModal('filter');
118
- return;
119
- }
120
- if (input === 'e' && onOpenError) {
121
- const target = state.recentErrors.find((er) => er.file && typeof er.line === 'number');
122
- if (target?.file && typeof target.line === 'number') {
123
- onOpenError({
124
- file: target.file,
125
- line: target.line,
126
- service: target.service,
127
- text: target.text,
128
- });
129
- }
130
- else {
131
- onOpenError(null);
132
- }
133
- return;
134
- }
135
- }
136
- // ── Health ────────────────────────────────────────────────────────────
137
- if (state.focus === 'health') {
138
- if (key.upArrow) {
139
- state.moveHealthSelection('up');
140
- return;
141
- }
142
- if (key.downArrow) {
143
- state.moveHealthSelection('down');
144
- return;
145
- }
146
- if (key.return) {
147
- const selectedCheck = state.health[state.selectedHealthIndex];
148
- if (selectedCheck) {
149
- state.toggleHealthExpand(selectedCheck.id);
150
- }
151
- return;
152
- }
153
- // Any single letter is treated as a potential remediation key.
154
- // The input is uppercased so users can press the key without Shift —
155
- // a remediation declared with `key: 'R'` matches both `r` and `R`.
156
- const upperInput = input.toUpperCase();
157
- if (runRemediation &&
158
- upperInput.length === 1 &&
159
- upperInput >= 'A' &&
160
- upperInput <= 'Z') {
161
- const dispatch = runRemediation(upperInput);
162
- if (!dispatch)
163
- return;
164
- state.setActiveRemediation({
165
- healthId: dispatch.healthId,
166
- healthLabel: dispatch.healthLabel,
167
- });
168
- withMinHold(dispatch.promise, () => cockpitStore.getState().setActiveRemediation(null));
169
- return;
170
- }
171
- }
172
- });
173
- }
@@ -1,22 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import { cockpitStore } from '../state/store.js';
5
- import { useCockpitStore } from '../hooks/useCockpitStore.js';
6
- export function FilterModal() {
7
- const outputFilter = useCockpitStore((s) => s.outputFilter);
8
- const [severityDraft, setSeverityDraft] = useState(outputFilter.severity);
9
- const SEVERITIES = [undefined, 'info', 'warn', 'error'];
10
- useInput((input, key) => {
11
- if (input === 's') {
12
- const idx = SEVERITIES.indexOf(severityDraft);
13
- const next = SEVERITIES[(idx + 1) % SEVERITIES.length];
14
- setSeverityDraft(next);
15
- }
16
- if (key.return || key.escape) {
17
- cockpitStore.getState().setFilter({ ...outputFilter, severity: severityDraft });
18
- cockpitStore.getState().setActiveModal(null);
19
- }
20
- });
21
- return (_jsx(Box, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, children: " Filter Output " }), _jsx(Text, { children: " " }), _jsxs(Box, { children: [_jsx(Text, { children: "Severity (s to cycle): " }), _jsx(Text, { color: "cyan", children: severityDraft ?? 'all' })] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Enter = apply \u00B7 Esc = cancel" })] }) }));
22
- }
@@ -1,30 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { useCockpitStore } from '../hooks/useCockpitStore.js';
4
- const SEVERITY_GLYPH = { ok: '✓', warn: '⚠', error: '✗' };
5
- const SEVERITY_COLOR = {
6
- ok: 'green',
7
- warn: 'yellow',
8
- error: 'red',
9
- };
10
- export function Health() {
11
- const health = useCockpitStore((s) => s.health);
12
- const selectedHealthIndex = useCockpitStore((s) => s.selectedHealthIndex);
13
- const expandedHealthId = useCockpitStore((s) => s.expandedHealthId);
14
- const focus = useCockpitStore((s) => s.focus);
15
- const notificationsEnabledSession = useCockpitStore((s) => s.notificationsEnabledSession);
16
- const activeRemediation = useCockpitStore((s) => s.activeRemediation);
17
- const isFocused = focus === 'health';
18
- if (health.length === 0) {
19
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "running health checks\u2026" }) }));
20
- }
21
- return (_jsxs(Box, { flexDirection: "column", children: [activeRemediation && (_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsxs(Text, { color: "yellow", children: ['… ', activeRemediation.healthLabel, ' running — switch to Output (←) to watch'] }) })), health.map((item, idx) => {
22
- const isSelected = isFocused && idx === selectedHealthIndex;
23
- const isExpanded = expandedHealthId === item.id;
24
- const isRunning = activeRemediation?.healthId === item.id;
25
- const glyph = isRunning ? '…' : (SEVERITY_GLYPH[item.severity] ?? '?');
26
- const color = isRunning ? 'yellow' : (SEVERITY_COLOR[item.severity] ?? 'white');
27
- const remKey = item.remediationKey ? ` [${item.remediationKey}]` : '';
28
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: isSelected ? '▶ ' : ' ' }), _jsxs(Text, { color: color, children: [glyph, " "] }), _jsx(Text, { bold: isSelected, children: item.label }), _jsx(Text, { dimColor: true, children: remKey }), isRunning && _jsx(Text, { color: "yellow", children: ' (running…)' })] }), isExpanded && (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, children: item.detail }) }))] }, item.id));
29
- }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ['notifications: ', _jsx(Text, { color: notificationsEnabledSession ? 'green' : 'yellow', children: notificationsEnabledSession ? 'on' : 'off (session)' })] }) })] }));
30
- }
@@ -1,81 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- /**
3
- * Help pane — reads pages merged via loader.ts (core docs + profile sources)
4
- * and renders the active page through marked-terminal.
5
- *
6
- * Per-tab keys:
7
- * ↑/↓ scroll body up/down
8
- * j / k previous / next page
9
- * shift+tab / tab same as j / k (alias for non-vim users; the global
10
- * tab-cycle handler suspends on Help)
11
- * g / G jump to top / bottom of body
12
- */
13
- import { useEffect, useMemo, useRef, useState } from 'react';
14
- import { Box, Text, useInput, useStdout } from 'ink';
15
- import { useCockpitStore } from '../hooks/useCockpitStore.js';
16
- import { loadHelpPages } from '../help/loader.js';
17
- import { renderMarkdown } from '../help/renderer.js';
18
- import { FALLBACK_PAGE } from '../help/content.js';
19
- export function Help() {
20
- const focus = useCockpitStore((s) => s.focus);
21
- const helpConfig = useCockpitStore((s) => s.helpConfig);
22
- const { stdout } = useStdout();
23
- const isActive = focus === 'help';
24
- const initial = useMemo(() => {
25
- const result = loadHelpPages({
26
- sources: helpConfig.sources,
27
- defaultPage: helpConfig.defaultPage,
28
- });
29
- if (result)
30
- return result;
31
- return { pages: [FALLBACK_PAGE], defaultIndex: 0 };
32
- }, [helpConfig.sources, helpConfig.defaultPage]);
33
- const pages = initial.pages;
34
- const [pageIndex, setPageIndex] = useState(initial.defaultIndex);
35
- const [scrollOffset, setScrollOffset] = useState(0);
36
- const lastPageIndexRef = useRef(pageIndex);
37
- useEffect(() => {
38
- if (lastPageIndexRef.current !== pageIndex) {
39
- lastPageIndexRef.current = pageIndex;
40
- setScrollOffset(0);
41
- }
42
- }, [pageIndex]);
43
- const activePage = pages[pageIndex] ?? pages[0];
44
- const renderedLines = useMemo(() => renderMarkdown(activePage.body).split('\n'), [activePage.body]);
45
- const bodyHeight = Math.max(5, (stdout.rows ?? 24) - 8);
46
- const maxOffset = Math.max(0, renderedLines.length - bodyHeight);
47
- useInput((input, key) => {
48
- if (key.upArrow) {
49
- setScrollOffset((o) => Math.max(0, o - 1));
50
- return;
51
- }
52
- if (key.downArrow) {
53
- setScrollOffset((o) => Math.min(maxOffset, o + 1));
54
- return;
55
- }
56
- if (input === 'j' || (key.tab && key.shift)) {
57
- setPageIndex((i) => Math.max(0, i - 1));
58
- return;
59
- }
60
- if (input === 'k' || (key.tab && !key.shift)) {
61
- setPageIndex((i) => Math.min(pages.length - 1, i + 1));
62
- return;
63
- }
64
- if (input === 'g') {
65
- setScrollOffset(0);
66
- return;
67
- }
68
- if (input === 'G') {
69
- setScrollOffset(maxOffset);
70
- return;
71
- }
72
- }, { isActive });
73
- const visibleLines = renderedLines.slice(scrollOffset, scrollOffset + bodyHeight);
74
- const scrollHint = renderedLines.length > bodyHeight
75
- ? `${scrollOffset + 1}-${Math.min(scrollOffset + bodyHeight, renderedLines.length)} / ${renderedLines.length}`
76
- : `${renderedLines.length} lines`;
77
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingTop: 1, children: [_jsxs(Box, { flexDirection: "row", paddingX: 1, marginBottom: 1, children: [pages.map((p, idx) => {
78
- const isCurrent = idx === pageIndex;
79
- return (_jsx(Box, { marginRight: 2, children: _jsxs(Text, { inverse: isCurrent, bold: isCurrent, color: isCurrent ? 'cyan' : undefined, children: [' ', p.title, ' '] }) }, p.slug));
80
- }), _jsx(Box, { flexGrow: 1, justifyContent: "flex-end", children: _jsxs(Text, { dimColor: true, children: [scrollHint, " \u00B7 [j/k or tab] page [\u2191\u2193] scroll [g/G] top/bot [\u2190\u2192] tabs"] }) })] }), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: _jsx(Text, { children: visibleLines.join('\n') }) })] }));
81
- }
@@ -1,108 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- /**
3
- * Output pane — streams source-tagged log lines.
4
- *
5
- * - Virtualized scrollback (hand-rolled slice against visible terminal height).
6
- * - Per-source color prefixes (deterministic source → color hash).
7
- * - outputFilter applied (sources, severity, search text).
8
- * - Auto-scroll to bottom unless the user has scrolled up.
9
- *
10
- * Domain handlers (e.g. "open error in editor") are NOT imported here. They
11
- * live in the global keybindings layer; this pane is purely presentational.
12
- */
13
- import { useState } from 'react';
14
- import { Box, Text, useStdout, useInput } from 'ink';
15
- import chalk from 'chalk';
16
- import { useCockpitStore } from '../hooks/useCockpitStore.js';
17
- const SOURCE_COLORS = ['cyan', 'yellow', 'magenta', 'blue', 'green', 'red', 'white'];
18
- const colorCache = new Map();
19
- function sourceColor(source) {
20
- const cached = colorCache.get(source);
21
- if (cached)
22
- return cached;
23
- let hash = 0;
24
- for (let i = 0; i < source.length; i++) {
25
- hash = (hash * 31 + source.charCodeAt(i)) | 0;
26
- }
27
- const color = SOURCE_COLORS[Math.abs(hash) % SOURCE_COLORS.length] ?? 'white';
28
- colorCache.set(source, color);
29
- return color;
30
- }
31
- function applyFilter(lines, filter) {
32
- let result = lines;
33
- if (filter.sources && filter.sources.length > 0) {
34
- const srcs = new Set(filter.sources);
35
- result = result.filter((l) => srcs.has(l.source));
36
- }
37
- if (filter.severity) {
38
- result = result.filter((l) => l.severity === filter.severity);
39
- }
40
- if (filter.search && filter.search.length > 0) {
41
- const needle = filter.search.toLowerCase();
42
- result = result.filter((l) => l.text.toLowerCase().includes(needle));
43
- }
44
- return result;
45
- }
46
- function severityColor(severity) {
47
- switch (severity) {
48
- case 'error':
49
- return 'red';
50
- case 'warn':
51
- return 'yellow';
52
- default:
53
- return 'white';
54
- }
55
- }
56
- function OutputLineRow({ line }) {
57
- const color = sourceColor(line.source);
58
- const prefix = chalk[color](`[${line.source}]`);
59
- return (_jsxs(Box, { children: [_jsxs(Text, { children: [prefix, " "] }), _jsx(Text, { color: severityColor(line.severity), children: line.text })] }));
60
- }
61
- const CHROME_LINES = 3;
62
- const MIN_VISIBLE = 3;
63
- export function Output() {
64
- const { stdout } = useStdout();
65
- const terminalHeight = stdout?.rows ?? 24;
66
- const visibleLines = Math.max(MIN_VISIBLE, terminalHeight - CHROME_LINES * 3);
67
- const output = useCockpitStore((s) => s.output);
68
- const outputFilter = useCockpitStore((s) => s.outputFilter);
69
- const focus = useCockpitStore((s) => s.focus);
70
- const recentErrors = useCockpitStore((s) => s.recentErrors);
71
- const isFocused = focus === 'output';
72
- const [scrollOffset, setScrollOffset] = useState(0);
73
- const filtered = applyFilter(output, outputFilter);
74
- const total = filtered.length;
75
- const effectiveOffset = Math.min(scrollOffset, Math.max(0, total - visibleLines));
76
- const startIdx = Math.max(0, total - visibleLines - effectiveOffset);
77
- const visible = filtered.slice(startIdx, startIdx + visibleLines);
78
- const atBottom = effectiveOffset === 0 || total <= visibleLines;
79
- useInput((_input, key) => {
80
- if (!isFocused)
81
- return;
82
- if (key.upArrow) {
83
- setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, total - visibleLines)));
84
- }
85
- else if (key.downArrow) {
86
- setScrollOffset((prev) => Math.max(0, prev - 1));
87
- }
88
- else if (key.pageUp) {
89
- setScrollOffset((prev) => Math.min(prev + visibleLines, Math.max(0, total - visibleLines)));
90
- }
91
- else if (key.pageDown) {
92
- setScrollOffset((prev) => Math.max(0, prev - visibleLines));
93
- }
94
- else if (key.return) {
95
- setScrollOffset(0);
96
- }
97
- }, { isActive: isFocused });
98
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: isFocused, children: " Output " }), outputFilter.sources && outputFilter.sources.length > 0 && (_jsxs(Text, { dimColor: true, children: ["[src: ", outputFilter.sources.join(','), "] "] })), outputFilter.severity && _jsxs(Text, { dimColor: true, children: ["[", outputFilter.severity, "] "] }), outputFilter.search && _jsxs(Text, { dimColor: true, children: ["[/", outputFilter.search, "/] "] }), !atBottom && _jsxs(Text, { dimColor: true, children: [" \u2191 scrolled (", effectiveOffset, " lines up)"] })] }), recentErrors.length > 0 && _jsx(RecentErrorsStrip, { errors: recentErrors.slice(0, 3) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visible.length === 0 ? (_jsx(Text, { dimColor: true, children: " no output yet" })) : (visible.map((line, idx) => _jsx(OutputLineRow, { line: line }, `${line.ts}-${idx}`))) }), isFocused && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [' ↑↓ scroll · PgUp/PgDn · Enter=bottom · c=clear · /=search · f=filter', recentErrors.length > 0 ? ' · e=open error' : ''] }) }))] }));
99
- }
100
- function RecentErrorsStrip({ errors }) {
101
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "red", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Recent errors" }), errors.map((err, idx) => (_jsx(RecentErrorRow, { err: err }, `${err.ts}-${idx}`)))] }));
102
- }
103
- function RecentErrorRow({ err }) {
104
- const sevColor = err.severity === 'error' ? 'red' : 'yellow';
105
- const glyph = err.severity === 'error' ? '✗' : '⚠';
106
- const trailer = err.file && typeof err.line === 'number' ? ` ↳ ${err.file}:${err.line}` : '';
107
- return (_jsxs(Box, { children: [_jsxs(Text, { color: sevColor, children: [glyph, " "] }), _jsxs(Text, { dimColor: true, children: ["[", err.service, "] "] }), _jsx(Text, { children: err.text }), trailer && _jsx(Text, { dimColor: true, children: trailer })] }));
108
- }
@@ -1,48 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { useCockpitStore } from '../hooks/useCockpitStore.js';
4
- function statusGlyph(status) {
5
- switch (status) {
6
- case 'running':
7
- return { glyph: '●', color: 'green' };
8
- case 'failing':
9
- return { glyph: '✗', color: 'red' };
10
- case 'idle':
11
- default:
12
- return { glyph: '○', color: 'gray' };
13
- }
14
- }
15
- function lintGlyph(lint) {
16
- switch (lint) {
17
- case 'pass':
18
- return { glyph: '✓', color: 'green' };
19
- case 'fail':
20
- return { glyph: '!', color: 'red' };
21
- case 'unknown':
22
- default:
23
- return null;
24
- }
25
- }
26
- function RepoRow({ entry, lint, isSelected, isRunningAction }) {
27
- const { glyph, color } = isRunningAction
28
- ? { glyph: '…', color: 'yellow' }
29
- : statusGlyph(entry.status);
30
- const lg = lintGlyph(lint);
31
- return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: isSelected ? '>' : ' ' }), _jsxs(Text, { color: color, children: [glyph, " "] }), _jsx(Text, { bold: isSelected, children: entry.name }), entry.kind === 'docker' && _jsx(Text, { dimColor: true, children: " [docker]" }), lg && (_jsxs(Text, { dimColor: true, color: lg.color, children: [' ', lg.glyph] })), isRunningAction && _jsx(Text, { color: "yellow", children: ' (running…)' })] }));
32
- }
33
- export function Repos() {
34
- const repos = useCockpitStore((s) => s.repos);
35
- const repoLintStatus = useCockpitStore((s) => s.repoLintStatus);
36
- const repoOrder = useCockpitStore((s) => s.repoOrder);
37
- const focus = useCockpitStore((s) => s.focus);
38
- const selectedRepoIndex = useCockpitStore((s) => s.selectedRepoIndex);
39
- const activeRepoAction = useCockpitStore((s) => s.activeRepoAction);
40
- const isFocused = focus === 'repos';
41
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", borderColor: isFocused ? 'cyan' : undefined, children: [_jsx(Text, { bold: isFocused, children: " Repos " }), activeRepoAction && (_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsxs(Text, { color: "yellow", children: ['… ', activeRepoAction.label, ' — switch to Output (→) to watch'] }) })), repoOrder.map((key, idx) => {
42
- const entry = repos[key];
43
- if (!entry)
44
- return null;
45
- const lint = repoLintStatus[key] ?? 'unknown';
46
- return (_jsx(RepoRow, { entry: entry, lint: lint, isSelected: isFocused && idx === selectedRepoIndex, isRunningAction: activeRepoAction?.repoKey === key }, key));
47
- })] }));
48
- }
@@ -1,31 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import { cockpitStore } from '../state/store.js';
5
- import { useCockpitStore } from '../hooks/useCockpitStore.js';
6
- export function SearchModal() {
7
- const outputFilter = useCockpitStore((s) => s.outputFilter);
8
- const [query, setQuery] = useState(outputFilter.search ?? '');
9
- useInput((input, key) => {
10
- if (key.escape) {
11
- cockpitStore.getState().setActiveModal(null);
12
- return;
13
- }
14
- if (key.return) {
15
- cockpitStore.getState().setFilter({
16
- ...outputFilter,
17
- search: query.length > 0 ? query : undefined,
18
- });
19
- cockpitStore.getState().setActiveModal(null);
20
- return;
21
- }
22
- if (key.backspace || key.delete) {
23
- setQuery((prev) => prev.slice(0, -1));
24
- return;
25
- }
26
- if (input && !key.ctrl && !key.meta) {
27
- setQuery((prev) => prev + input);
28
- }
29
- });
30
- return (_jsx(Box, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, children: " Search Output " }), _jsx(Text, { children: " " }), _jsxs(Box, { children: [_jsx(Text, { children: "Query: " }), _jsxs(Text, { color: "cyan", children: [query, _jsx(Text, { color: "gray", children: "\u2588" })] })] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Enter = apply \u00B7 Esc = cancel \u00B7 Backspace = delete" })] }) }));
31
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * Zustand store — single source of truth for cockpit state.
3
- *
4
- * Slices: repos, repoLintStatus, repoOrder, focus, selectedRepoIndex, output,
5
- * outputFilter, activeModal, health, previousHealth, selectedHealthIndex,
6
- * notificationsEnabledSession, expandedHealthId, recentErrors, activeRemediation,
7
- * activeRepoAction, helpConfig.
8
- *
9
- * No per-pane local stores. All cockpit state lives here. Domain-specific
10
- * extensions (e.g. profile-specific repo metadata) flow in through the
11
- * pluggable repo `kind` and the helpConfig sources mechanism.
12
- */
13
- import { createStore } from 'zustand/vanilla';
14
- const MAX_RECENT_ERRORS = 20;
15
- const MAX_OUTPUT_LINES = 5000;
16
- const FOCUS_ORDER = ['repos', 'output', 'health', 'help'];
17
- export const cockpitStore = createStore()((set, get) => ({
18
- repos: {},
19
- repoLintStatus: {},
20
- repoOrder: [],
21
- focus: 'repos',
22
- selectedRepoIndex: 0,
23
- output: [],
24
- outputFilter: {},
25
- activeModal: null,
26
- health: [],
27
- previousHealth: [],
28
- selectedHealthIndex: 0,
29
- notificationsEnabledSession: true,
30
- expandedHealthId: null,
31
- recentErrors: [],
32
- selectedRecentErrorIndex: 0,
33
- activeRemediation: null,
34
- activeRepoAction: null,
35
- helpConfig: {},
36
- setRepoStatus: (key, status) => set((state) => {
37
- const existing = state.repos[key];
38
- if (!existing)
39
- return state;
40
- return { repos: { ...state.repos, [key]: { ...existing, status } } };
41
- }),
42
- setLintStatus: (key, lintStatus) => set((state) => ({
43
- repoLintStatus: { ...state.repoLintStatus, [key]: lintStatus },
44
- })),
45
- cycleFocus: (direction) => set((state) => {
46
- const idx = FOCUS_ORDER.indexOf(state.focus);
47
- const len = FOCUS_ORDER.length;
48
- const nextIdx = direction === 'forward' ? (idx + 1) % len : (idx - 1 + len) % len;
49
- const next = FOCUS_ORDER[nextIdx];
50
- if (!next)
51
- return state;
52
- return { focus: next };
53
- }),
54
- initRepos: (repos, repoOrder) => {
55
- const repoLintStatus = {};
56
- for (const key of repoOrder) {
57
- repoLintStatus[key] = 'unknown';
58
- }
59
- return set({ repos, repoOrder, repoLintStatus, selectedRepoIndex: 0 });
60
- },
61
- moveSelection: (direction) => set((state) => {
62
- const len = state.repoOrder.length;
63
- if (len === 0)
64
- return state;
65
- const current = state.selectedRepoIndex;
66
- const next = direction === 'up' ? Math.max(0, current - 1) : Math.min(len - 1, current + 1);
67
- return { selectedRepoIndex: next };
68
- }),
69
- appendOutput: (line) => set((state) => {
70
- const next = [...state.output, line];
71
- const capped = next.length > MAX_OUTPUT_LINES ? next.slice(next.length - MAX_OUTPUT_LINES) : next;
72
- return { output: capped };
73
- }),
74
- clearOutput: () => set({ output: [] }),
75
- setFilter: (filter) => set({ outputFilter: filter }),
76
- setActiveModal: (modal) => set({ activeModal: modal }),
77
- setHealth: (next, onTransition) => {
78
- const prev = get().health;
79
- if (onTransition)
80
- onTransition(prev, next);
81
- set({ health: next, previousHealth: prev });
82
- },
83
- moveHealthSelection: (direction) => set((state) => {
84
- const len = state.health.length;
85
- if (len === 0)
86
- return state;
87
- const current = state.selectedHealthIndex;
88
- const next = direction === 'up' ? Math.max(0, current - 1) : Math.min(len - 1, current + 1);
89
- return { selectedHealthIndex: next };
90
- }),
91
- toggleSessionNotifications: () => set((state) => ({ notificationsEnabledSession: !state.notificationsEnabledSession })),
92
- toggleHealthExpand: (id) => set((state) => ({
93
- expandedHealthId: state.expandedHealthId === id ? null : id,
94
- })),
95
- pushRecentError: (err) => set((state) => {
96
- const next = [err, ...state.recentErrors].slice(0, MAX_RECENT_ERRORS);
97
- const selectedRecentErrorIndex = Math.min(state.selectedRecentErrorIndex, Math.max(0, next.length - 1));
98
- return { recentErrors: next, selectedRecentErrorIndex };
99
- }),
100
- moveRecentErrorSelection: (direction) => set((state) => {
101
- const len = state.recentErrors.length;
102
- if (len === 0)
103
- return state;
104
- const current = state.selectedRecentErrorIndex;
105
- const next = direction === 'up' ? Math.max(0, current - 1) : Math.min(len - 1, current + 1);
106
- return { selectedRecentErrorIndex: next };
107
- }),
108
- setActiveRemediation: (remediation) => set({ activeRemediation: remediation }),
109
- setActiveRepoAction: (action) => set({ activeRepoAction: action }),
110
- setHelpConfig: (config) => set({ helpConfig: config }),
111
- }));
@@ -1,7 +0,0 @@
1
- export const TAB_ORDER = ['repos', 'output', 'health', 'help'];
2
- export const TAB_LABELS = {
3
- repos: 'Repos',
4
- output: 'Output',
5
- health: 'Health',
6
- help: 'Help',
7
- };