@matthesketh/fleet 1.8.0 → 1.11.0
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.
- package/README.md +186 -16
- package/dist/bin/fleet-agent.d.ts +2 -0
- package/dist/bin/fleet-agent.js +7 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +73 -31
- package/dist/commands/add.d.ts +2 -1
- package/dist/commands/add.js +66 -59
- package/dist/commands/audit.d.ts +1 -0
- package/dist/commands/audit.js +144 -0
- package/dist/commands/backup.d.ts +1 -0
- package/dist/commands/backup.js +510 -0
- package/dist/commands/boot-start.d.ts +3 -1
- package/dist/commands/boot-start.js +39 -47
- package/dist/commands/completions.d.ts +6 -0
- package/dist/commands/completions.js +83 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.js +96 -0
- package/dist/commands/deploy.js +3 -2
- package/dist/commands/deps.js +5 -1
- package/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.js +186 -0
- package/dist/commands/egress.d.ts +1 -1
- package/dist/commands/egress.js +13 -10
- package/dist/commands/freeze.d.ts +8 -4
- package/dist/commands/freeze.js +77 -59
- package/dist/commands/git.js +2 -2
- package/dist/commands/health.d.ts +2 -1
- package/dist/commands/health.js +38 -56
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +83 -73
- package/dist/commands/install-mcp.d.ts +3 -1
- package/dist/commands/install-mcp.js +53 -34
- package/dist/commands/list.d.ts +2 -1
- package/dist/commands/list.js +22 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/notify.d.ts +1 -0
- package/dist/commands/notify.js +51 -0
- package/dist/commands/patch-systemd.d.ts +7 -1
- package/dist/commands/patch-systemd.js +71 -31
- package/dist/commands/remove.d.ts +3 -1
- package/dist/commands/remove.js +37 -26
- package/dist/commands/restart.d.ts +4 -1
- package/dist/commands/restart.js +17 -20
- package/dist/commands/rollback.d.ts +4 -1
- package/dist/commands/rollback.js +33 -42
- package/dist/commands/secrets.js +157 -9
- package/dist/commands/start.d.ts +4 -1
- package/dist/commands/start.js +17 -20
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +21 -26
- package/dist/commands/stop.d.ts +4 -1
- package/dist/commands/stop.js +17 -20
- package/dist/commands/testflight.d.ts +1 -0
- package/dist/commands/testflight.js +193 -0
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.js +95 -0
- package/dist/core/audit/cache.d.ts +4 -0
- package/dist/core/audit/cache.js +37 -0
- package/dist/core/audit/config.d.ts +5 -0
- package/dist/core/audit/config.js +35 -0
- package/dist/core/audit/greenlight.d.ts +11 -0
- package/dist/core/audit/greenlight.js +81 -0
- package/dist/core/audit/reporters/cli.d.ts +3 -0
- package/dist/core/audit/reporters/cli.js +68 -0
- package/dist/core/audit/suppress.d.ts +6 -0
- package/dist/core/audit/suppress.js +37 -0
- package/dist/core/audit/target.d.ts +5 -0
- package/dist/core/audit/target.js +26 -0
- package/dist/core/audit/types.d.ts +54 -0
- package/dist/core/audit/types.js +5 -0
- package/dist/core/backup/browser-api.d.ts +66 -0
- package/dist/core/backup/browser-api.js +197 -0
- package/dist/core/backup/browser-server.d.ts +11 -0
- package/dist/core/backup/browser-server.js +241 -0
- package/dist/core/backup/browser-ui.d.ts +5 -0
- package/dist/core/backup/browser-ui.js +268 -0
- package/dist/core/backup/cloudflare.d.ts +7 -0
- package/dist/core/backup/cloudflare.js +82 -0
- package/dist/core/backup/config.d.ts +9 -0
- package/dist/core/backup/config.js +80 -0
- package/dist/core/backup/detect.d.ts +11 -0
- package/dist/core/backup/detect.js +71 -0
- package/dist/core/backup/dump.d.ts +11 -0
- package/dist/core/backup/dump.js +82 -0
- package/dist/core/backup/index.d.ts +9 -0
- package/dist/core/backup/index.js +9 -0
- package/dist/core/backup/repo.d.ts +71 -0
- package/dist/core/backup/repo.js +256 -0
- package/dist/core/backup/schedule.d.ts +17 -0
- package/dist/core/backup/schedule.js +90 -0
- package/dist/core/backup/sensitive.d.ts +5 -0
- package/dist/core/backup/sensitive.js +37 -0
- package/dist/core/backup/status.d.ts +3 -0
- package/dist/core/backup/status.js +29 -0
- package/dist/core/backup/statuspage.d.ts +23 -0
- package/dist/core/backup/statuspage.js +145 -0
- package/dist/core/backup/system.d.ts +24 -0
- package/dist/core/backup/system.js +209 -0
- package/dist/core/backup/totp.d.ts +16 -0
- package/dist/core/backup/totp.js +116 -0
- package/dist/core/backup/types.d.ts +70 -0
- package/dist/core/backup/types.js +7 -0
- package/dist/core/backup/unlock.d.ts +19 -0
- package/dist/core/backup/unlock.js +69 -0
- package/dist/core/boot-refresh.d.ts +1 -1
- package/dist/core/boot-refresh.js +10 -9
- package/dist/core/deps/actors/pr-creator.d.ts +5 -3
- package/dist/core/deps/actors/pr-creator.js +71 -18
- package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
- package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
- package/dist/core/deps/collectors/npm.js +3 -1
- package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
- package/dist/core/deps/collectors/vulnerability.js +31 -2
- package/dist/core/deps/config.js +6 -0
- package/dist/core/deps/scanner.js +1 -1
- package/dist/core/deps/types.d.ts +8 -0
- package/dist/core/env.d.ts +3 -0
- package/dist/core/env.js +11 -0
- package/dist/core/exec.d.ts +1 -0
- package/dist/core/exec.js +4 -0
- package/dist/core/file-lock.d.ts +18 -0
- package/dist/core/file-lock.js +44 -0
- package/dist/core/git-onboard.js +10 -13
- package/dist/core/github.d.ts +3 -1
- package/dist/core/github.js +10 -7
- package/dist/core/logs-policy.d.ts +5 -0
- package/dist/core/logs-policy.js +20 -1
- package/dist/core/operator.d.ts +21 -0
- package/dist/core/operator.js +54 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.js +26 -0
- package/dist/core/routines/schema.d.ts +11 -11
- package/dist/core/routines/schema.js +14 -3
- package/dist/core/routines/store.d.ts +8 -8
- package/dist/core/secrets-ops.d.ts +31 -6
- package/dist/core/secrets-ops.js +208 -102
- package/dist/core/secrets-providers.js +2 -2
- package/dist/core/secrets-rotation.d.ts +1 -1
- package/dist/core/secrets-rotation.js +58 -52
- package/dist/core/secrets-v2-cleanup.d.ts +19 -0
- package/dist/core/secrets-v2-cleanup.js +94 -0
- package/dist/core/secrets-v2-creds.d.ts +9 -0
- package/dist/core/secrets-v2-creds.js +44 -0
- package/dist/core/secrets-v2-install.d.ts +13 -0
- package/dist/core/secrets-v2-install.js +76 -0
- package/dist/core/secrets-v2-keypair.d.ts +10 -0
- package/dist/core/secrets-v2-keypair.js +31 -0
- package/dist/core/secrets-v2-migrate.d.ts +29 -0
- package/dist/core/secrets-v2-migrate.js +395 -0
- package/dist/core/secrets-v2-ops.d.ts +36 -0
- package/dist/core/secrets-v2-ops.js +184 -0
- package/dist/core/secrets-v2-protocol.d.ts +19 -0
- package/dist/core/secrets-v2-protocol.js +60 -0
- package/dist/core/secrets-v2-snapshot.d.ts +36 -0
- package/dist/core/secrets-v2-snapshot.js +115 -0
- package/dist/core/secrets-v2.d.ts +21 -0
- package/dist/core/secrets-v2.js +249 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +91 -11
- package/dist/core/self-update.d.ts +32 -11
- package/dist/core/self-update.js +52 -14
- package/dist/core/testflight/asc.d.ts +12 -0
- package/dist/core/testflight/asc.js +101 -0
- package/dist/core/testflight/credentials.d.ts +3 -0
- package/dist/core/testflight/credentials.js +35 -0
- package/dist/core/testflight/eas.d.ts +4 -0
- package/dist/core/testflight/eas.js +38 -0
- package/dist/core/testflight/resolve.d.ts +6 -0
- package/dist/core/testflight/resolve.js +44 -0
- package/dist/core/testflight/types.d.ts +13 -0
- package/dist/core/testflight/types.js +3 -0
- package/dist/core/testflight/workflow.d.ts +17 -0
- package/dist/core/testflight/workflow.js +65 -0
- package/dist/core/validate.d.ts +1 -0
- package/dist/core/validate.js +8 -0
- package/dist/mcp/audit-tools.d.ts +2 -0
- package/dist/mcp/audit-tools.js +94 -0
- package/dist/mcp/git-tools.js +1 -1
- package/dist/mcp/registry-bridge.d.ts +10 -0
- package/dist/mcp/registry-bridge.js +65 -0
- package/dist/mcp/secrets-tools.js +2 -2
- package/dist/mcp/server.js +16 -82
- package/dist/mcp/testflight-tools.d.ts +2 -0
- package/dist/mcp/testflight-tools.js +52 -0
- package/dist/registry/context.d.ts +7 -0
- package/dist/registry/context.js +37 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +44 -0
- package/dist/registry/parse-args.d.ts +13 -0
- package/dist/registry/parse-args.js +74 -0
- package/dist/registry/registry.d.ts +24 -0
- package/dist/registry/registry.js +26 -0
- package/dist/registry/render.d.ts +3 -0
- package/dist/registry/render.js +29 -0
- package/dist/registry/types.d.ts +50 -0
- package/dist/registry/types.js +1 -0
- package/dist/templates/agent-unit.d.ts +5 -0
- package/dist/templates/agent-unit.js +40 -0
- package/dist/templates/app-unit-edit.d.ts +2 -0
- package/dist/templates/app-unit-edit.js +46 -0
- package/dist/templates/compose-edit.d.ts +2 -0
- package/dist/templates/compose-edit.js +156 -0
- package/dist/templates/nginx.js +11 -0
- package/dist/templates/systemd.js +6 -0
- package/dist/tui/components/ArgForm.d.ts +7 -0
- package/dist/tui/components/ArgForm.js +64 -0
- package/dist/tui/components/ArgForm.test.d.ts +1 -0
- package/dist/tui/components/ArgForm.test.js +19 -0
- package/dist/tui/components/KeyHint.js +5 -0
- package/dist/tui/hooks/use-secrets.d.ts +8 -8
- package/dist/tui/hooks/use-secrets.js +7 -7
- package/dist/tui/router.d.ts +1 -0
- package/dist/tui/router.js +26 -9
- package/dist/tui/router.test.d.ts +1 -0
- package/dist/tui/router.test.js +13 -0
- package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
- package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
- package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
- package/dist/tui/tests/redaction-rerender.test.js +53 -0
- package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
- package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
- package/dist/tui/types.d.ts +1 -1
- package/dist/tui/views/CommandPalette.d.ts +5 -0
- package/dist/tui/views/CommandPalette.js +90 -0
- package/dist/tui/views/CommandPalette.test.d.ts +1 -0
- package/dist/tui/views/CommandPalette.test.js +117 -0
- package/dist/tui/views/Dashboard.js +10 -7
- package/dist/tui/views/HealthView.js +14 -5
- package/dist/tui/views/SecretEdit.js +15 -16
- package/dist/tui/views/SecretEdit.test.d.ts +1 -0
- package/dist/tui/views/SecretEdit.test.js +82 -0
- package/dist/tui/views/SecretsView.js +26 -16
- package/package.json +9 -6
|
@@ -48,9 +48,9 @@ export function useSecrets() {
|
|
|
48
48
|
}));
|
|
49
49
|
}
|
|
50
50
|
}, []);
|
|
51
|
-
const saveSecret = useCallback((app, key, value) => {
|
|
51
|
+
const saveSecret = useCallback(async (app, key, value) => {
|
|
52
52
|
try {
|
|
53
|
-
setSecret(app, key, value);
|
|
53
|
+
await setSecret(app, key, value);
|
|
54
54
|
// Re-unseal to update runtime
|
|
55
55
|
try {
|
|
56
56
|
unsealAll();
|
|
@@ -62,7 +62,7 @@ export function useSecrets() {
|
|
|
62
62
|
return { ok: false, error: err instanceof Error ? err.message : 'Failed to save secret' };
|
|
63
63
|
}
|
|
64
64
|
}, []);
|
|
65
|
-
const deleteSecret = useCallback((app, key) => {
|
|
65
|
+
const deleteSecret = useCallback(async (app, key) => {
|
|
66
66
|
try {
|
|
67
67
|
const plaintext = decryptApp(app);
|
|
68
68
|
const manifest = loadManifest();
|
|
@@ -114,9 +114,9 @@ export function useSecrets() {
|
|
|
114
114
|
return { ok: false, error: err instanceof Error ? err.message : 'Failed to unseal' };
|
|
115
115
|
}
|
|
116
116
|
}, []);
|
|
117
|
-
const seal = useCallback(() => {
|
|
117
|
+
const seal = useCallback(async () => {
|
|
118
118
|
try {
|
|
119
|
-
sealFromRuntime();
|
|
119
|
+
await sealFromRuntime();
|
|
120
120
|
setState(prev => ({ ...prev, sealed: true }));
|
|
121
121
|
return { ok: true };
|
|
122
122
|
}
|
|
@@ -124,9 +124,9 @@ export function useSecrets() {
|
|
|
124
124
|
return { ok: false, error: err instanceof Error ? err.message : 'Failed to seal' };
|
|
125
125
|
}
|
|
126
126
|
}, []);
|
|
127
|
-
const importEnv = useCallback((app, path) => {
|
|
127
|
+
const importEnv = useCallback(async (app, path) => {
|
|
128
128
|
try {
|
|
129
|
-
importEnvFile(app, path);
|
|
129
|
+
await importEnvFile(app, path);
|
|
130
130
|
try {
|
|
131
131
|
unsealAll();
|
|
132
132
|
}
|
package/dist/tui/router.d.ts
CHANGED
package/dist/tui/router.js
CHANGED
|
@@ -17,6 +17,7 @@ import { SecretsView } from './views/SecretsView.js';
|
|
|
17
17
|
import { SecretEdit } from './views/SecretEdit.js';
|
|
18
18
|
import { HealthView } from './views/HealthView.js';
|
|
19
19
|
import { LogsView } from './views/LogsView.js';
|
|
20
|
+
import { CommandPalette } from './views/CommandPalette.js';
|
|
20
21
|
import { isSealed, isInitialized } from '../core/secrets.js';
|
|
21
22
|
const HELP_GROUPS = [
|
|
22
23
|
{
|
|
@@ -47,8 +48,9 @@ const HELP_GROUPS = [
|
|
|
47
48
|
],
|
|
48
49
|
},
|
|
49
50
|
];
|
|
50
|
-
function ViewRouter() {
|
|
51
|
+
export function ViewRouter() {
|
|
51
52
|
const state = React.useContext(AppStateContext);
|
|
53
|
+
const dispatch = React.useContext(AppDispatchContext);
|
|
52
54
|
switch (state.currentView) {
|
|
53
55
|
case 'dashboard':
|
|
54
56
|
return _jsx(Dashboard, {});
|
|
@@ -62,6 +64,8 @@ function ViewRouter() {
|
|
|
62
64
|
return _jsx(SecretEdit, {});
|
|
63
65
|
case 'logs':
|
|
64
66
|
return _jsx(LogsView, {});
|
|
67
|
+
case 'command-palette':
|
|
68
|
+
return (_jsx(CommandPalette, { onClose: () => dispatch({ type: 'GO_BACK' }), onOpenView: view => dispatch({ type: 'NAVIGATE', view: view }) }));
|
|
65
69
|
default:
|
|
66
70
|
return _jsx(Dashboard, {});
|
|
67
71
|
}
|
|
@@ -75,7 +79,10 @@ function UpdateBanner({ info, inProgress }) {
|
|
|
75
79
|
}
|
|
76
80
|
const ahead = info.behind;
|
|
77
81
|
const subject = info.latestSubject ? ` — ${info.latestSubject}` : '';
|
|
78
|
-
|
|
82
|
+
// channel label only surfaces on prerelease so the stable case stays
|
|
83
|
+
// visually identical to what operators have seen for several releases.
|
|
84
|
+
const channelLabel = info.channel === 'prerelease' ? ' (prerelease)' : '';
|
|
85
|
+
return (_jsx(Box, { paddingX: 1, children: _jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { color: "cyan", children: ["\u2191 Update available", channelLabel, ": ", ahead, " commit", ahead === 1 ? '' : 's', " ahead", subject, ". Press "] }), _jsx(Text, { color: "cyan", bold: true, children: "U" }), _jsx(Text, { color: "cyan", children: " to install." })] }) }));
|
|
79
86
|
}
|
|
80
87
|
export function App() {
|
|
81
88
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
@@ -141,27 +148,37 @@ export function App() {
|
|
|
141
148
|
}
|
|
142
149
|
return true;
|
|
143
150
|
}
|
|
144
|
-
|
|
151
|
+
// command-palette and secret-edit capture raw text input — the global
|
|
152
|
+
// single-key shortcuts must not fire while either is open.
|
|
153
|
+
const isInputView = state.currentView === 'secret-edit' || state.currentView === 'command-palette';
|
|
154
|
+
if (input === '?' && !isInputView) {
|
|
145
155
|
setShowHelp(true);
|
|
146
156
|
return true;
|
|
147
157
|
}
|
|
148
|
-
if (input === '
|
|
158
|
+
if (input === ':' && !isInputView) {
|
|
159
|
+
dispatch({ type: 'NAVIGATE', view: 'command-palette' });
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (input === 'q' && !isInputView) {
|
|
149
163
|
process.exit(0);
|
|
150
164
|
return true;
|
|
151
165
|
}
|
|
152
|
-
if (input === 'x' &&
|
|
166
|
+
if (input === 'x' && !isInputView) {
|
|
153
167
|
dispatch({ type: 'TOGGLE_REDACT' });
|
|
154
168
|
return true;
|
|
155
169
|
}
|
|
156
170
|
// U → apply pending update. Only fires when one is actually available.
|
|
157
|
-
if ((input === 'U' || input === 'u') &&
|
|
171
|
+
if ((input === 'U' || input === 'u') && !isInputView) {
|
|
158
172
|
const info = updateInfoRef.current;
|
|
159
173
|
if (info?.available && !updateInProgressRef.current) {
|
|
160
174
|
setUpdateInProgress(true);
|
|
161
175
|
applyUpdate().then(result => {
|
|
162
176
|
setUpdateInProgress(false);
|
|
163
177
|
if (result.ok) {
|
|
164
|
-
setUpdateInfo({
|
|
178
|
+
setUpdateInfo({
|
|
179
|
+
available: false, behind: 0, latestSubject: '',
|
|
180
|
+
branch: info.branch, remoteBranch: info.remoteBranch, channel: info.channel,
|
|
181
|
+
});
|
|
165
182
|
}
|
|
166
183
|
// Result reported via UpdateBanner below.
|
|
167
184
|
App.__lastUpdateOutput = result.output;
|
|
@@ -172,7 +189,7 @@ export function App() {
|
|
|
172
189
|
return true;
|
|
173
190
|
}
|
|
174
191
|
}
|
|
175
|
-
if (key.tab) {
|
|
192
|
+
if (key.tab && state.currentView !== 'command-palette') {
|
|
176
193
|
const topViews = ['dashboard', 'health', 'secrets', 'logs-multi'];
|
|
177
194
|
const base = topViews.includes(state.currentView)
|
|
178
195
|
? state.currentView
|
|
@@ -180,7 +197,7 @@ export function App() {
|
|
|
180
197
|
dispatch({ type: 'NAVIGATE', view: nextTopView(base) });
|
|
181
198
|
return true;
|
|
182
199
|
}
|
|
183
|
-
if (key.escape && state.previousView) {
|
|
200
|
+
if (key.escape && state.previousView && state.currentView !== 'command-palette') {
|
|
184
201
|
dispatch({ type: 'GO_BACK' });
|
|
185
202
|
return true;
|
|
186
203
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { InputDispatcher } from '@matthesketh/ink-input-dispatcher';
|
|
5
|
+
import { ViewRouter } from './router.js';
|
|
6
|
+
import { AppStateContext, AppDispatchContext, initialState } from './state.js';
|
|
7
|
+
describe('command palette routing', () => {
|
|
8
|
+
it('renders the command palette for the command-palette view', async () => {
|
|
9
|
+
const { lastFrame } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(AppStateContext.Provider, { value: { ...initialState, currentView: 'command-palette' }, children: _jsx(AppDispatchContext.Provider, { value: () => { }, children: _jsx(ViewRouter, {}) }) }) }));
|
|
10
|
+
await new Promise(r => setTimeout(r, 30));
|
|
11
|
+
expect(lastFrame() ?? '').toContain('Command palette');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -21,9 +21,9 @@ describe('SignalsGrid', () => {
|
|
|
21
21
|
expect(frame).toContain('CI');
|
|
22
22
|
});
|
|
23
23
|
it('renders a row with repo name when signals present', () => {
|
|
24
|
-
const rows = [{ repo: '
|
|
24
|
+
const rows = [{ repo: 'movers-co', signals: [mkSignal('git-clean', 'ok')] }];
|
|
25
25
|
const { lastFrame } = render(_jsx(SignalsGrid, { rows: rows, selectedIndex: 0, kinds: ['git-clean'] }));
|
|
26
|
-
expect(lastFrame()).toContain('
|
|
26
|
+
expect(lastFrame()).toContain('movers-co');
|
|
27
27
|
});
|
|
28
28
|
it('shows empty-state message with no repos', () => {
|
|
29
29
|
const { lastFrame } = render(_jsx(SignalsGrid, { rows: [], selectedIndex: 0, kinds: ['git-clean'] }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { InputDispatcher } from '@matthesketh/ink-input-dispatcher';
|
|
5
|
+
// stable mock fleet status. the reference must stay constant across renders:
|
|
6
|
+
// dashboard memoises its list items, so a changing status object would rebuild
|
|
7
|
+
// them anyway and mask the redaction-staleness bug this test guards.
|
|
8
|
+
const { MOCK_STATUS } = vi.hoisted(() => ({
|
|
9
|
+
MOCK_STATUS: {
|
|
10
|
+
totalApps: 3,
|
|
11
|
+
healthy: 3,
|
|
12
|
+
unhealthy: 0,
|
|
13
|
+
apps: [
|
|
14
|
+
{ name: 'alpha-service', systemd: 'active', containers: '1/1', health: 'healthy' },
|
|
15
|
+
{ name: 'bravo-service', systemd: 'active', containers: '1/1', health: 'healthy' },
|
|
16
|
+
{ name: 'charlie-service', systemd: 'active', containers: '1/1', health: 'healthy' },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
vi.mock('../hooks/use-fleet-data', () => ({
|
|
21
|
+
useFleetData: () => ({ status: MOCK_STATUS, loading: false, error: null }),
|
|
22
|
+
}));
|
|
23
|
+
// fixed available height so the list renders every row under a test stdout.
|
|
24
|
+
vi.mock('@matthesketh/ink-viewport', () => ({
|
|
25
|
+
useAvailableHeight: () => 20,
|
|
26
|
+
Viewport: ({ children }) => children,
|
|
27
|
+
}));
|
|
28
|
+
import { Dashboard } from '../views/Dashboard.js';
|
|
29
|
+
import { AppStateContext, AppDispatchContext, initialState } from '../state.js';
|
|
30
|
+
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
31
|
+
function Harness({ redacted }) {
|
|
32
|
+
return (_jsx(AppStateContext.Provider, { value: { ...initialState, redacted }, children: _jsx(AppDispatchContext.Provider, { value: () => { }, children: _jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(Dashboard, {}) }) }) }));
|
|
33
|
+
}
|
|
34
|
+
describe('redaction re-renders every visible row', () => {
|
|
35
|
+
it('toggling redaction updates all app rows without scrolling', async () => {
|
|
36
|
+
const { lastFrame, rerender } = render(_jsx(Harness, { redacted: false }));
|
|
37
|
+
await delay(50);
|
|
38
|
+
const plain = lastFrame() ?? '';
|
|
39
|
+
expect(plain).toContain('alpha-service');
|
|
40
|
+
expect(plain).toContain('bravo-service');
|
|
41
|
+
expect(plain).toContain('charlie-service');
|
|
42
|
+
// flip redaction — equivalent to pressing 'x'. no j/k/arrow keys are sent,
|
|
43
|
+
// so nothing scrolls; every visible row must still pick up the new label.
|
|
44
|
+
rerender(_jsx(Harness, { redacted: true }));
|
|
45
|
+
await delay(50);
|
|
46
|
+
const out = lastFrame() ?? '';
|
|
47
|
+
expect(out).not.toContain('alpha-service');
|
|
48
|
+
expect(out).not.toContain('bravo-service');
|
|
49
|
+
expect(out).not.toContain('charlie-service');
|
|
50
|
+
// redacted labels follow the app-NN pattern from redactName()
|
|
51
|
+
expect(out).toMatch(/app-\d\d/);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useReducer } from 'react';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
5
|
+
// Force Ink's real (non-CI) render path so the flicker branch is reachable.
|
|
6
|
+
// ink-testing-library renders in debug mode, where Ink's onRender returns
|
|
7
|
+
// before the clear-screen branch — so it cannot observe the flicker at all.
|
|
8
|
+
// This proof drives Ink's genuine production render() instead.
|
|
9
|
+
vi.mock('is-in-ci', () => ({ default: false }));
|
|
10
|
+
// eslint-disable-next-line import/first
|
|
11
|
+
import { render, Box, Text } from 'ink';
|
|
12
|
+
// eslint-disable-next-line import/first
|
|
13
|
+
import { Viewport, useAvailableHeight } from '@matthesketh/ink-viewport';
|
|
14
|
+
// eslint-disable-next-line import/first
|
|
15
|
+
import { ScrollableList } from '@matthesketh/ink-scrollable-list';
|
|
16
|
+
// eslint-disable-next-line import/first
|
|
17
|
+
import { InputDispatcher, useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
|
|
18
|
+
// ESC (0x1B) built without a control-char literal in source.
|
|
19
|
+
const ESC = String.fromCharCode(27);
|
|
20
|
+
// Ink writes this exact sequence (ansiEscapes.clearTerminal) before a frame
|
|
21
|
+
// whenever the frame height is >= stdout.rows — a full-screen wipe on every
|
|
22
|
+
// re-render. That wipe IS the flicker; counting it proves its presence.
|
|
23
|
+
const CLEAR_TERMINAL = `${ESC}[2J${ESC}[3J${ESC}[H`;
|
|
24
|
+
const DOWN_ARROW = `${ESC}[B`;
|
|
25
|
+
const ROWS = 24;
|
|
26
|
+
const COLUMNS = 120;
|
|
27
|
+
const SCROLL_STEPS = 20;
|
|
28
|
+
const APPS = Array.from({ length: 200 }, (_, i) => `app-${i}`);
|
|
29
|
+
/** A stdout that records every byte Ink writes and reports a fixed size. */
|
|
30
|
+
class RecordingStdout extends EventEmitter {
|
|
31
|
+
columns = COLUMNS;
|
|
32
|
+
rows = ROWS;
|
|
33
|
+
isTTY = true;
|
|
34
|
+
writes = [];
|
|
35
|
+
write = (data) => {
|
|
36
|
+
this.writes.push(data);
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** A TTY-like stdin mirroring ink-testing-library's: a keypress is delivered
|
|
41
|
+
* via both the 'readable'/read() and 'data' paths. */
|
|
42
|
+
class FakeStdin extends EventEmitter {
|
|
43
|
+
isTTY = true;
|
|
44
|
+
data = null;
|
|
45
|
+
press = (data) => {
|
|
46
|
+
this.data = data;
|
|
47
|
+
this.emit('readable');
|
|
48
|
+
this.emit('data', data);
|
|
49
|
+
};
|
|
50
|
+
setEncoding() { }
|
|
51
|
+
setRawMode() { }
|
|
52
|
+
resume() { }
|
|
53
|
+
pause() { }
|
|
54
|
+
ref() { }
|
|
55
|
+
unref() { }
|
|
56
|
+
read = () => {
|
|
57
|
+
const d = this.data;
|
|
58
|
+
this.data = null;
|
|
59
|
+
return d;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
63
|
+
/** Mirrors Fleet's Dashboard: a windowed ScrollableList sized off
|
|
64
|
+
* useAvailableHeight(), driven by j / down-arrow via the input dispatcher. */
|
|
65
|
+
function MockDashboard() {
|
|
66
|
+
const [index, move] = useReducer((cur, delta) => Math.max(0, Math.min(APPS.length - 1, cur + delta)), 0);
|
|
67
|
+
const available = useAvailableHeight();
|
|
68
|
+
const listHeight = Math.max(5, available - 4); // same formula as Dashboard.tsx
|
|
69
|
+
const handler = (input, key) => {
|
|
70
|
+
if (input === 'j' || key.downArrow) {
|
|
71
|
+
move(1);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (input === 'k' || key.upArrow) {
|
|
75
|
+
move(-1);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
useRegisterHandler(handler);
|
|
81
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { bold: true, children: [APPS.length, " apps"] }), _jsx(ScrollableList, { items: APPS, selectedIndex: index, maxVisible: listHeight, renderItem: (item, selected) => (_jsxs(Text, { color: selected ? 'cyan' : undefined, children: [selected ? '> ' : ' ', item] })) })] }));
|
|
82
|
+
}
|
|
83
|
+
/** Fleet's real chrome: InputDispatcher outside, Viewport(chrome) inside. */
|
|
84
|
+
function FleetUI() {
|
|
85
|
+
return (_jsx(InputDispatcher, { globalHandler: () => false, children: _jsxs(Viewport, { chrome: 6, children: [_jsx(Text, { children: "fleet" }), _jsx(MockDashboard, {})] }) }));
|
|
86
|
+
}
|
|
87
|
+
/** The pre-fix viewport: a root box pinned to the FULL terminal height. */
|
|
88
|
+
function LegacyChrome() {
|
|
89
|
+
return (_jsx(InputDispatcher, { globalHandler: () => false, children: _jsxs(Box, { flexDirection: "column", height: ROWS, children: [_jsx(Text, { children: "fleet" }), _jsx(MockDashboard, {})] }) }));
|
|
90
|
+
}
|
|
91
|
+
/** Mount with Ink's production render path, scroll SCROLL_STEPS rows, and
|
|
92
|
+
* record every byte Ink writes to the terminal. */
|
|
93
|
+
async function scrollAndRecord(node) {
|
|
94
|
+
const stdout = new RecordingStdout();
|
|
95
|
+
const stdin = new FakeStdin();
|
|
96
|
+
const app = render(node, {
|
|
97
|
+
stdout: stdout,
|
|
98
|
+
stdin: stdin,
|
|
99
|
+
debug: false,
|
|
100
|
+
exitOnCtrlC: false,
|
|
101
|
+
patchConsole: false,
|
|
102
|
+
});
|
|
103
|
+
await sleep(40);
|
|
104
|
+
for (let i = 0; i < SCROLL_STEPS; i++) {
|
|
105
|
+
stdin.press(DOWN_ARROW);
|
|
106
|
+
await sleep(45); // > Ink's 32ms render throttle, so each scroll renders
|
|
107
|
+
}
|
|
108
|
+
await sleep(60);
|
|
109
|
+
app.unmount();
|
|
110
|
+
const output = stdout.writes.join('');
|
|
111
|
+
return { clears: output.split(CLEAR_TERMINAL).length - 1, output };
|
|
112
|
+
}
|
|
113
|
+
describe('Fleet TUI scroll flicker proof', () => {
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
process.setMaxListeners(0);
|
|
116
|
+
Object.defineProperty(process.stdout, 'rows', {
|
|
117
|
+
value: ROWS,
|
|
118
|
+
writable: true,
|
|
119
|
+
configurable: true,
|
|
120
|
+
});
|
|
121
|
+
Object.defineProperty(process.stdout, 'columns', {
|
|
122
|
+
value: COLUMNS,
|
|
123
|
+
writable: true,
|
|
124
|
+
configurable: true,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// skipped on ci: ink picks up github_actions / ci directly and gates the
|
|
128
|
+
// clearterminal branch on tty-ish heuristics that the in-process recorder
|
|
129
|
+
// can't fully spoof. the control passes locally where the production
|
|
130
|
+
// render path is reachable, so the proof below still has its anchor when
|
|
131
|
+
// run pre-commit; gh actions just doesn't reproduce the legacy bug.
|
|
132
|
+
it.skipIf(Boolean(process.env.CI))('CONTROL: a full-terminal-height frame flickers on every scroll render', async () => {
|
|
133
|
+
const { clears } = await scrollAndRecord(_jsx(LegacyChrome, {}));
|
|
134
|
+
expect(clears).toBeGreaterThan(0);
|
|
135
|
+
});
|
|
136
|
+
it('Fleet UI scrolls 20 rows with ZERO full-screen clears', async () => {
|
|
137
|
+
const { clears, output } = await scrollAndRecord(_jsx(FleetUI, {}));
|
|
138
|
+
// The scroll genuinely happened: ScrollableList only renders its
|
|
139
|
+
// "more above" indicator once the window has scrolled down.
|
|
140
|
+
expect(output).toContain('more above');
|
|
141
|
+
// ...and across every one of those re-renders, Ink never wiped the
|
|
142
|
+
// screen. Zero clearTerminal sequences => zero flicker.
|
|
143
|
+
expect(clears).toBe(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
package/dist/tui/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type View = 'dashboard' | 'app-detail' | 'health' | 'secrets' | 'secret-edit' | 'logs' | 'logs-multi';
|
|
1
|
+
export type View = 'dashboard' | 'app-detail' | 'health' | 'secrets' | 'secret-edit' | 'logs' | 'logs-multi' | 'command-palette';
|
|
2
2
|
export type SecretsSubView = 'app-list' | 'secret-list';
|
|
3
3
|
export interface TuiState {
|
|
4
4
|
currentView: View;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
|
|
5
|
+
import { ScrollableList } from '@matthesketh/ink-scrollable-list';
|
|
6
|
+
import { loadRegistry } from '../../registry/index.js';
|
|
7
|
+
import { allCommands } from '../../registry/registry.js';
|
|
8
|
+
import { ArgForm } from '../components/ArgForm.js';
|
|
9
|
+
import { runFleetCommand } from '../exec-bridge.js';
|
|
10
|
+
import { colors } from '../theme.js';
|
|
11
|
+
export function CommandPalette(props) {
|
|
12
|
+
// load the registry once and snapshot the visible commands. a lazy useState
|
|
13
|
+
// initialiser runs exactly once — unlike a bare call in the render body.
|
|
14
|
+
const [commands] = useState(() => {
|
|
15
|
+
loadRegistry();
|
|
16
|
+
return allCommands().filter(c => !c.cliOnly);
|
|
17
|
+
});
|
|
18
|
+
const [query, setQuery] = useState('');
|
|
19
|
+
const [index, setIndex] = useState(0);
|
|
20
|
+
const [chosen, setChosen] = useState(null);
|
|
21
|
+
const [output, setOutput] = useState(null);
|
|
22
|
+
const filtered = useMemo(() => commands.filter(c => (c.name + ' ' + c.summary).toLowerCase().includes(query.toLowerCase())), [commands, query]);
|
|
23
|
+
const listHandler = (input, key) => {
|
|
24
|
+
if (chosen || output !== null)
|
|
25
|
+
return false;
|
|
26
|
+
if (key.escape) {
|
|
27
|
+
props.onClose();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (key.downArrow) {
|
|
31
|
+
setIndex(i => Math.min(i + 1, filtered.length - 1));
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (key.upArrow) {
|
|
35
|
+
setIndex(i => Math.max(i - 1, 0));
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (key.return) {
|
|
39
|
+
const cmd = filtered[index];
|
|
40
|
+
if (!cmd)
|
|
41
|
+
return true;
|
|
42
|
+
if (cmd.tui && typeof cmd.tui === 'object') {
|
|
43
|
+
props.onOpenView(cmd.tui.view);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
setChosen(cmd);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (key.backspace || key.delete) {
|
|
50
|
+
setQuery(q => q.slice(0, -1));
|
|
51
|
+
setIndex(0);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (input && !key.ctrl && !key.meta) {
|
|
55
|
+
setQuery(q => q + input);
|
|
56
|
+
setIndex(0);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
useRegisterHandler(listHandler);
|
|
62
|
+
if (output !== null) {
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { bold: true, color: colors.primary, children: [chosen?.name, " result"] }), _jsx(Text, { children: output }), _jsx(Text, { color: colors.muted, children: "esc to close" }), _jsx(CloseOnEscape, { onClose: () => { setOutput(null); setChosen(null); } })] }));
|
|
64
|
+
}
|
|
65
|
+
if (chosen) {
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: colors.primary, children: chosen.name }), _jsx(ArgForm, { schema: chosen.args, onCancel: () => setChosen(null), onSubmit: async (values) => {
|
|
67
|
+
const argv = [chosen.name];
|
|
68
|
+
for (const [k, v] of Object.entries(values)) {
|
|
69
|
+
if (v === true)
|
|
70
|
+
argv.push(`--${k}`);
|
|
71
|
+
else if (v !== false && v !== '' && v != null)
|
|
72
|
+
argv.push(`--${k}`, String(v));
|
|
73
|
+
}
|
|
74
|
+
const r = await runFleetCommand(argv);
|
|
75
|
+
setOutput(r.output);
|
|
76
|
+
} })] }));
|
|
77
|
+
}
|
|
78
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: colors.primary, children: "Command palette" }), _jsxs(Text, { color: colors.muted, children: ["filter: ", query || '(type to filter)'] }), _jsx(ScrollableList, { items: filtered, selectedIndex: Math.min(index, Math.max(0, filtered.length - 1)), maxVisible: 12, emptyText: " no matching commands", renderItem: (cmd, selected) => (_jsxs(Box, { children: [_jsx(Text, { color: selected ? colors.primary : colors.muted, children: selected ? '> ' : ' ' }), _jsx(Box, { width: 20, children: _jsx(Text, { bold: selected, children: cmd.name }) }), _jsx(Text, { color: colors.muted, children: cmd.summary })] })) })] }));
|
|
79
|
+
}
|
|
80
|
+
function CloseOnEscape(props) {
|
|
81
|
+
const handler = (_input, key) => {
|
|
82
|
+
if (key.escape) {
|
|
83
|
+
props.onClose();
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
};
|
|
88
|
+
useRegisterHandler(handler);
|
|
89
|
+
return _jsx(_Fragment, {});
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { InputDispatcher } from '@matthesketh/ink-input-dispatcher';
|
|
6
|
+
vi.mock('../exec-bridge', () => ({
|
|
7
|
+
runFleetCommand: vi.fn(async () => ({ ok: true, output: 'done' })),
|
|
8
|
+
}));
|
|
9
|
+
import { runFleetCommand } from '../exec-bridge.js';
|
|
10
|
+
import { register, defineCommand } from '../../registry/registry.js';
|
|
11
|
+
import { loadRegistry, _resetLoader } from '../../registry/index.js';
|
|
12
|
+
import { CommandPalette } from './CommandPalette.js';
|
|
13
|
+
/** flush ink's render queue after a keystroke */
|
|
14
|
+
const flush = () => new Promise(r => setTimeout(r, 30));
|
|
15
|
+
beforeEach(() => _resetLoader());
|
|
16
|
+
afterEach(() => _resetLoader());
|
|
17
|
+
describe('CommandPalette', () => {
|
|
18
|
+
it('lists registry commands', async () => {
|
|
19
|
+
const { lastFrame } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(CommandPalette, { onOpenView: () => { }, onClose: () => { } }) }));
|
|
20
|
+
await flush();
|
|
21
|
+
// assert against the first alphabetical command — the palette caps
|
|
22
|
+
// visible items at 12 so commands later in the list (status, stop,
|
|
23
|
+
// whoami, ...) may scroll off when the registry grows.
|
|
24
|
+
expect(lastFrame() ?? '').toContain('add');
|
|
25
|
+
});
|
|
26
|
+
it('hides non-matching commands when a query is typed', async () => {
|
|
27
|
+
const { lastFrame, stdin } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(CommandPalette, { onOpenView: () => { }, onClose: () => { } }) }));
|
|
28
|
+
await flush();
|
|
29
|
+
// sanity: at least the first command is visible before filtering
|
|
30
|
+
expect(lastFrame() ?? '').toContain('add');
|
|
31
|
+
// type a query that matches nothing
|
|
32
|
+
stdin.write('z');
|
|
33
|
+
await flush();
|
|
34
|
+
stdin.write('z');
|
|
35
|
+
await flush();
|
|
36
|
+
stdin.write('z');
|
|
37
|
+
await flush();
|
|
38
|
+
stdin.write('z');
|
|
39
|
+
await flush();
|
|
40
|
+
const frame = lastFrame() ?? '';
|
|
41
|
+
expect(frame).not.toContain('status');
|
|
42
|
+
expect(frame).toContain('no matching commands');
|
|
43
|
+
});
|
|
44
|
+
it('calls onOpenView when enter is pressed on a command with a tui view', async () => {
|
|
45
|
+
// type 'stat' to filter to only status, then press enter.
|
|
46
|
+
const onOpenView = vi.fn();
|
|
47
|
+
const { stdin } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(CommandPalette, { onOpenView: onOpenView, onClose: () => { } }) }));
|
|
48
|
+
await flush();
|
|
49
|
+
stdin.write('s');
|
|
50
|
+
await flush();
|
|
51
|
+
stdin.write('t');
|
|
52
|
+
await flush();
|
|
53
|
+
stdin.write('a');
|
|
54
|
+
await flush();
|
|
55
|
+
stdin.write('t');
|
|
56
|
+
await flush();
|
|
57
|
+
stdin.write('\r');
|
|
58
|
+
await flush();
|
|
59
|
+
expect(onOpenView).toHaveBeenCalledWith('dashboard');
|
|
60
|
+
});
|
|
61
|
+
it('builds argv from an empty-args command and calls runFleetCommand', async () => {
|
|
62
|
+
// register an ad-hoc command with no tui and an empty args schema.
|
|
63
|
+
// loadRegistry() first so the loaded flag is set and the component's own
|
|
64
|
+
// call does not wipe the ad-hoc registration.
|
|
65
|
+
loadRegistry();
|
|
66
|
+
register(defineCommand({
|
|
67
|
+
name: 'demo-run',
|
|
68
|
+
summary: 'a demo command',
|
|
69
|
+
args: z.object({}),
|
|
70
|
+
async run() { return { ok: true, summary: 'ok', data: null }; },
|
|
71
|
+
}));
|
|
72
|
+
const { stdin } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(CommandPalette, { onOpenView: () => { }, onClose: () => { } }) }));
|
|
73
|
+
await flush();
|
|
74
|
+
// type 'demo-run' to filter to exactly this command so the test is
|
|
75
|
+
// stable regardless of how many other commands are in the registry.
|
|
76
|
+
for (const ch of 'demo-run') {
|
|
77
|
+
stdin.write(ch);
|
|
78
|
+
await flush();
|
|
79
|
+
}
|
|
80
|
+
// press enter on demo-run → ArgForm shown (no tui field).
|
|
81
|
+
stdin.write('\r');
|
|
82
|
+
await flush();
|
|
83
|
+
// ArgForm with no fields: press enter → onSubmit({}) → runFleetCommand.
|
|
84
|
+
stdin.write('\r');
|
|
85
|
+
await flush();
|
|
86
|
+
expect(vi.mocked(runFleetCommand)).toHaveBeenCalledWith(['demo-run']);
|
|
87
|
+
});
|
|
88
|
+
it('builds --flag argv when a boolean field is toggled before submit', async () => {
|
|
89
|
+
// register a command with a boolean arg.
|
|
90
|
+
loadRegistry();
|
|
91
|
+
register(defineCommand({
|
|
92
|
+
name: 'demo-flag',
|
|
93
|
+
summary: 'a flag demo',
|
|
94
|
+
args: z.object({ force: z.boolean().default(false) }),
|
|
95
|
+
async run() { return { ok: true, summary: 'ok', data: null }; },
|
|
96
|
+
}));
|
|
97
|
+
const { stdin } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(CommandPalette, { onOpenView: () => { }, onClose: () => { } }) }));
|
|
98
|
+
await flush();
|
|
99
|
+
// type 'demo-flag' to filter to exactly this command so the test is
|
|
100
|
+
// stable regardless of how many other commands are in the registry.
|
|
101
|
+
for (const ch of 'demo-flag') {
|
|
102
|
+
stdin.write(ch);
|
|
103
|
+
await flush();
|
|
104
|
+
}
|
|
105
|
+
// press enter on demo-flag.
|
|
106
|
+
stdin.write('\r');
|
|
107
|
+
await flush();
|
|
108
|
+
// ArgForm is now showing the 'force' boolean field (cursor on it).
|
|
109
|
+
// press space to toggle force → true.
|
|
110
|
+
stdin.write(' ');
|
|
111
|
+
await flush();
|
|
112
|
+
// press enter to submit { force: true }.
|
|
113
|
+
stdin.write('\r');
|
|
114
|
+
await flush();
|
|
115
|
+
expect(vi.mocked(runFleetCommand)).toHaveBeenCalledWith(['demo-flag', '--force']);
|
|
116
|
+
});
|
|
117
|
+
});
|