@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
|
@@ -5,16 +5,20 @@ import Spinner from 'ink-spinner';
|
|
|
5
5
|
import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
|
|
6
6
|
import { ScrollableList } from '@matthesketh/ink-scrollable-list';
|
|
7
7
|
import { useAvailableHeight } from '@matthesketh/ink-viewport';
|
|
8
|
-
import { useAppState, useAppDispatch,
|
|
8
|
+
import { useAppState, useAppDispatch, redactName } from '../state.js';
|
|
9
9
|
import { useFleetData } from '../hooks/use-fleet-data.js';
|
|
10
10
|
import { colors } from '../theme.js';
|
|
11
11
|
export function Dashboard() {
|
|
12
12
|
const state = useAppState();
|
|
13
13
|
const dispatch = useAppDispatch();
|
|
14
14
|
const { status, loading, error } = useFleetData();
|
|
15
|
-
const redact = useRedact();
|
|
16
15
|
const availableHeight = useAvailableHeight();
|
|
17
|
-
const items = useMemo(() => status?.apps.map(app => ({
|
|
16
|
+
const items = useMemo(() => (status?.apps ?? []).map(app => ({
|
|
17
|
+
...app,
|
|
18
|
+
// bake the redacted label onto the item: ScrollableList memoises rows by
|
|
19
|
+
// item identity, so a redaction toggle must yield fresh item objects.
|
|
20
|
+
displayLabel: state.redacted ? redactName(app.name) : app.name,
|
|
21
|
+
})), [status, state.redacted]);
|
|
18
22
|
const handler = (input, key) => {
|
|
19
23
|
if (items.length === 0)
|
|
20
24
|
return false;
|
|
@@ -46,9 +50,8 @@ export function Dashboard() {
|
|
|
46
50
|
if (!status)
|
|
47
51
|
return _jsx(Text, { color: colors.muted, children: "No data" });
|
|
48
52
|
const listHeight = Math.max(5, availableHeight - 4);
|
|
49
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: [status.totalApps, " apps"] }), _jsxs(Text, { color: colors.success, children: [status.healthy, " healthy"] }), status.unhealthy > 0 && (_jsxs(Text, { color: colors.error, children: [status.unhealthy, " unhealthy"] }))
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: selected ? colors.primary : colors.muted, children: selected ? '> ' : ' ' }), _jsx(Box, { width: 24, children: _jsx(Text, { bold: selected, color: selected ? colors.primary : colors.text, children: displayName.length > 22 ? displayName.slice(0, 19) + '...' : displayName }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: app.systemd.slice(0, 12) }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: app.containers }) }), _jsx(Box, { width: 12, children: _jsx(Text, { children: app.health.slice(0, 10) }) })] }));
|
|
53
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: [status.totalApps, " apps"] }), _jsxs(Text, { color: colors.success, children: [status.healthy, " healthy"] }), status.unhealthy > 0 && (_jsxs(Text, { color: colors.error, children: [status.unhealthy, " unhealthy"] }))] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ['APP'.padEnd(24), 'SYSTEMD'.padEnd(14), 'CONTAINERS'.padEnd(14), 'HEALTH'.padEnd(12)] }) }), _jsx(ScrollableList, { items: items, selectedIndex: Math.min(state.dashboardIndex, items.length - 1), maxVisible: listHeight, renderItem: (item, selected) => {
|
|
54
|
+
const label = item.displayLabel;
|
|
55
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: selected ? colors.primary : colors.muted, children: selected ? '> ' : ' ' }), _jsx(Box, { width: 24, children: _jsx(Text, { bold: selected, color: selected ? colors.primary : colors.text, children: label.length > 22 ? label.slice(0, 19) + '...' : label }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: item.systemd.slice(0, 12) }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: item.containers }) }), _jsx(Box, { width: 12, children: _jsx(Text, { children: item.health.slice(0, 10) }) })] }));
|
|
53
56
|
} })] }));
|
|
54
57
|
}
|
|
@@ -7,19 +7,28 @@ import { ScrollableList } from '@matthesketh/ink-scrollable-list';
|
|
|
7
7
|
import { useAvailableHeight } from '@matthesketh/ink-viewport';
|
|
8
8
|
import { useHealth } from '../hooks/use-health.js';
|
|
9
9
|
import { StatusBadge } from '../components/StatusBadge.js';
|
|
10
|
-
import { useAppState, useAppDispatch,
|
|
10
|
+
import { useAppState, useAppDispatch, redactName } from '../state.js';
|
|
11
11
|
import { colors } from '../theme.js';
|
|
12
12
|
export function HealthView() {
|
|
13
13
|
const state = useAppState();
|
|
14
14
|
const dispatch = useAppDispatch();
|
|
15
15
|
const { results, loading, error } = useHealth();
|
|
16
|
-
|
|
16
|
+
// only show spinner during the very first load. background polls (every
|
|
17
|
+
// 15s) flip `loading` true/false too, but the data is already on screen —
|
|
18
|
+
// ticking a spinner there causes the whole table to redraw at frame rate.
|
|
19
|
+
const initialLoad = loading && results.length === 0;
|
|
17
20
|
const availableHeight = useAvailableHeight();
|
|
18
21
|
const counts = useMemo(() => ({
|
|
19
22
|
healthy: results.filter(r => r.overall === 'healthy').length,
|
|
20
23
|
degraded: results.filter(r => r.overall === 'degraded').length,
|
|
21
24
|
down: results.filter(r => r.overall === 'down').length,
|
|
22
25
|
}), [results]);
|
|
26
|
+
const items = useMemo(() => results.map(r => ({
|
|
27
|
+
...r,
|
|
28
|
+
// same reasoning as the dashboard view — ScrollableList memoises rows by
|
|
29
|
+
// item identity, so the redacted label must live on the item itself.
|
|
30
|
+
displayApp: state.redacted ? redactName(r.app) : r.app,
|
|
31
|
+
})), [results, state.redacted]);
|
|
23
32
|
const handler = (input, key) => {
|
|
24
33
|
if (results.length === 0)
|
|
25
34
|
return false;
|
|
@@ -34,14 +43,14 @@ export function HealthView() {
|
|
|
34
43
|
return false;
|
|
35
44
|
};
|
|
36
45
|
useRegisterHandler(handler);
|
|
37
|
-
if (
|
|
46
|
+
if (initialLoad) {
|
|
38
47
|
return (_jsx(Box, { padding: 1, children: _jsxs(Text, { children: [_jsx(Spinner, { type: "dots" }), " Running health checks..."] }) }));
|
|
39
48
|
}
|
|
40
49
|
if (error && results.length === 0) {
|
|
41
50
|
return (_jsx(Box, { padding: 1, children: _jsxs(Text, { color: colors.error, children: ["Error: ", error] }) }));
|
|
42
51
|
}
|
|
43
52
|
const listHeight = Math.max(5, availableHeight - 4);
|
|
44
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, children: "Health Monitor" }), _jsxs(Text, { color: colors.success, children: [counts.healthy, " healthy"] }), counts.degraded > 0 && _jsxs(Text, { color: colors.warning, children: [counts.degraded, " degraded"] }), counts.down > 0 && _jsxs(Text, { color: colors.error, children: [counts.down, " down"] })
|
|
53
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, children: "Health Monitor" }), _jsxs(Text, { color: colors.success, children: [counts.healthy, " healthy"] }), counts.degraded > 0 && _jsxs(Text, { color: colors.warning, children: [counts.degraded, " degraded"] }), counts.down > 0 && _jsxs(Text, { color: colors.error, children: [counts.down, " down"] })] }), _jsxs(Text, { bold: true, children: [' APP'.padEnd(26), 'SYSTEMD'.padEnd(12), 'CONTAINERS'.padEnd(20), 'HTTP'.padEnd(10), "OVERALL"] }), _jsx(ScrollableList, { items: items, selectedIndex: Math.min(state.healthIndex, items.length - 1), maxVisible: listHeight, renderItem: (result, selected) => {
|
|
45
54
|
const runningCount = result.containers.filter(c => c.running).length;
|
|
46
55
|
const containerStr = `${runningCount}/${result.containers.length}`;
|
|
47
56
|
// 404 → "no /health" (app never implemented one — distinct from a real failure).
|
|
@@ -53,6 +62,6 @@ export function HealthView() {
|
|
|
53
62
|
? 'no /health'
|
|
54
63
|
: `${result.http.status ?? 'err'}`
|
|
55
64
|
: 'n/a';
|
|
56
|
-
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: selected ? colors.primary : colors.muted, children: selected ? '> ' : ' ' }), _jsx(Text, { children:
|
|
65
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: selected ? colors.primary : colors.muted, children: selected ? '> ' : ' ' }), _jsx(Text, { children: result.displayApp.padEnd(24) }), _jsx(Box, { width: 12, children: _jsx(StatusBadge, { value: result.systemd.state, type: "systemd" }) }), _jsx(Text, { children: containerStr.padEnd(20) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: result.http?.ok ? colors.success : result.http ? colors.error : colors.muted, children: httpStr }) }), _jsx(StatusBadge, { value: result.overall, type: "health" })] }));
|
|
57
66
|
} })] }));
|
|
58
67
|
}
|
|
@@ -5,7 +5,6 @@ import TextInput from 'ink-text-input';
|
|
|
5
5
|
import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
|
|
6
6
|
import { useAppState, useAppDispatch } from '../state.js';
|
|
7
7
|
import { useSecrets } from '../hooks/use-secrets.js';
|
|
8
|
-
import { getSecret as getCoreSecret } from '../../core/secrets-ops.js';
|
|
9
8
|
import { colors } from '../theme.js';
|
|
10
9
|
export function SecretEdit() {
|
|
11
10
|
const { selectedApp, selectedSecret } = useAppState();
|
|
@@ -23,22 +22,20 @@ export function SecretEdit() {
|
|
|
23
22
|
clearTimeout(timerRef.current);
|
|
24
23
|
};
|
|
25
24
|
}, []);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
catch {
|
|
34
|
-
// ignore
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}, [isNew, selectedApp, selectedSecret]);
|
|
38
|
-
const save = () => {
|
|
25
|
+
// SECURITY: existing secret values are NEVER preloaded into editor state.
|
|
26
|
+
// The TextInput's `mask="*"` only changes the rendered glyph — the
|
|
27
|
+
// underlying React state would still hold plaintext, exposing it to
|
|
28
|
+
// DevTools dumps, error boundary captures, and process memory dumps.
|
|
29
|
+
// Editing requires re-typing the value, matching the CLI posture in
|
|
30
|
+
// `src/commands/secrets.ts` which rejects argv values for the same reason.
|
|
31
|
+
const save = async () => {
|
|
39
32
|
if (!selectedApp || !keyName)
|
|
40
33
|
return;
|
|
41
|
-
const result = secrets.saveSecret(selectedApp, keyName, value);
|
|
34
|
+
const result = await secrets.saveSecret(selectedApp, keyName, value);
|
|
35
|
+
// Clear the local plaintext from React state immediately, regardless of
|
|
36
|
+
// success/failure. The state holds plaintext only for the duration of
|
|
37
|
+
// the save call.
|
|
38
|
+
setValue('');
|
|
42
39
|
if (result.ok) {
|
|
43
40
|
setStatus('Saved and re-sealed');
|
|
44
41
|
timerRef.current = setTimeout(() => {
|
|
@@ -60,5 +57,7 @@ export function SecretEdit() {
|
|
|
60
57
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { bold: true, color: colors.primary, children: [isNew ? 'Add Secret' : 'Edit Secret', " - ", selectedApp] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Key: " }), isNew && phase === 'key' ? (_jsx(TextInput, { value: keyName, onChange: setKeyName, onSubmit: () => {
|
|
61
58
|
if (keyName)
|
|
62
59
|
setPhase('value');
|
|
63
|
-
} })) : (_jsx(Text, { bold: true, children: keyName }))] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Value: " }), phase === 'value' ? (_jsx(TextInput, { value: value, onChange: setValue, onSubmit: save, mask: "*" })) : (_jsx(Text, { color: colors.muted, children: "(press Enter on key first)" }))] })] }), status && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: status.startsWith('Error') ? colors.error : colors.success, children: status }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children:
|
|
60
|
+
} })) : (_jsx(Text, { bold: true, children: keyName }))] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Value: " }), phase === 'value' ? (_jsx(TextInput, { value: value, onChange: setValue, onSubmit: save, mask: "*" })) : (_jsx(Text, { color: colors.muted, children: "(press Enter on key first)" }))] })] }), status && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: status.startsWith('Error') ? colors.error : colors.success, children: status }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: isNew
|
|
61
|
+
? 'Adding new secret. Type the key name, then the value.'
|
|
62
|
+
: `Editing ${keyName} - paste new value to replace. (Current value not displayed.)` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "Enter to save | Esc to cancel" }) })] }));
|
|
64
63
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import { InputDispatcher } from '@matthesketh/ink-input-dispatcher';
|
|
5
|
+
import { AppStateContext, AppDispatchContext, initialState } from '../state.js';
|
|
6
|
+
import { SecretEdit } from './SecretEdit.js';
|
|
7
|
+
// Mock useSecrets so the component doesn't try to touch the real vault.
|
|
8
|
+
vi.mock('../hooks/use-secrets.js', () => ({
|
|
9
|
+
useSecrets: () => ({
|
|
10
|
+
initialized: true,
|
|
11
|
+
sealed: false,
|
|
12
|
+
apps: [],
|
|
13
|
+
secrets: [],
|
|
14
|
+
revealedValues: {},
|
|
15
|
+
loading: false,
|
|
16
|
+
error: null,
|
|
17
|
+
refresh: () => { },
|
|
18
|
+
loadAppSecrets: () => { },
|
|
19
|
+
saveSecret: () => ({ ok: true }),
|
|
20
|
+
deleteSecret: () => ({ ok: true }),
|
|
21
|
+
revealSecret: () => { },
|
|
22
|
+
hideSecret: () => { },
|
|
23
|
+
unseal: () => ({ ok: true }),
|
|
24
|
+
seal: () => ({ ok: true }),
|
|
25
|
+
importEnv: () => ({ ok: true }),
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
function renderWithState(state) {
|
|
29
|
+
const dispatch = () => { };
|
|
30
|
+
return render(_jsx(AppStateContext.Provider, { value: state, children: _jsx(AppDispatchContext.Provider, { value: dispatch, children: _jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(SecretEdit, {}) }) }) }));
|
|
31
|
+
}
|
|
32
|
+
describe('SecretEdit (security policy: never preload secret values)', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
it('Edit case: shows the existing key but the value field starts empty', () => {
|
|
37
|
+
// Policy: editing an existing secret means re-typing the value.
|
|
38
|
+
// The TUI must NOT decrypt the existing value into React state because
|
|
39
|
+
// mask="*" only affects rendering, not the underlying string.
|
|
40
|
+
const state = {
|
|
41
|
+
...initialState,
|
|
42
|
+
selectedApp: 'my-app',
|
|
43
|
+
selectedSecret: 'API_KEY',
|
|
44
|
+
};
|
|
45
|
+
const { lastFrame } = renderWithState(state);
|
|
46
|
+
const frame = lastFrame();
|
|
47
|
+
expect(frame).toContain('Edit Secret');
|
|
48
|
+
expect(frame).toContain('API_KEY');
|
|
49
|
+
// Helper text must announce the policy.
|
|
50
|
+
expect(frame).toContain('Current value not displayed');
|
|
51
|
+
// Even though TextInput is rendered for the value, no plaintext or
|
|
52
|
+
// mask glyphs should appear for the value (it is empty).
|
|
53
|
+
// The "Value:" label is present, and any content after it on that
|
|
54
|
+
// line must not contain '*' characters from a preloaded masked value.
|
|
55
|
+
const valueLine = frame.split('\n').find(l => l.includes('Value:'));
|
|
56
|
+
expect(valueLine).not.toMatch(/\*/);
|
|
57
|
+
});
|
|
58
|
+
it('New case: prompts for key first and shows the new-secret helper text', () => {
|
|
59
|
+
const state = {
|
|
60
|
+
...initialState,
|
|
61
|
+
selectedApp: 'my-app',
|
|
62
|
+
selectedSecret: null,
|
|
63
|
+
};
|
|
64
|
+
const { lastFrame } = renderWithState(state);
|
|
65
|
+
const frame = lastFrame();
|
|
66
|
+
expect(frame).toContain('Add Secret');
|
|
67
|
+
expect(frame).toContain('Adding new secret');
|
|
68
|
+
// Until the user enters a key, the value field shows the placeholder
|
|
69
|
+
// and is NOT yet active.
|
|
70
|
+
expect(frame).toContain('press Enter on key first');
|
|
71
|
+
});
|
|
72
|
+
it('does not import getSecret (no preload code path remains)', async () => {
|
|
73
|
+
// Static guard: the SecretEdit module must not import getCoreSecret
|
|
74
|
+
// from secrets-ops. If a future change re-introduces the preload,
|
|
75
|
+
// this test fails.
|
|
76
|
+
const fs = await import('node:fs');
|
|
77
|
+
const url = await import('node:url');
|
|
78
|
+
const source = fs.readFileSync(url.fileURLToPath(new URL('./SecretEdit.tsx', import.meta.url)), 'utf8');
|
|
79
|
+
expect(source).not.toMatch(/getSecret as getCoreSecret/);
|
|
80
|
+
expect(source).not.toMatch(/from '\.\.\/\.\.\/core\/secrets-ops/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useCallback } from 'react';
|
|
2
|
+
import { useEffect, useCallback, useMemo } from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
|
|
5
5
|
import { ScrollableList } from '@matthesketh/ink-scrollable-list';
|
|
6
6
|
import { useAvailableHeight } from '@matthesketh/ink-viewport';
|
|
7
|
-
import { useAppState, useAppDispatch, useRedact } from '../state.js';
|
|
7
|
+
import { useAppState, useAppDispatch, useRedact, redactName } from '../state.js';
|
|
8
8
|
import { useSecrets } from '../hooks/use-secrets.js';
|
|
9
9
|
import { colors } from '../theme.js';
|
|
10
10
|
export function SecretsView() {
|
|
@@ -14,6 +14,12 @@ export function SecretsView() {
|
|
|
14
14
|
const secrets = useSecrets();
|
|
15
15
|
const availableHeight = useAvailableHeight();
|
|
16
16
|
const { secretsSubView: subView, secretsIndex: selectedIndex, selectedApp } = state;
|
|
17
|
+
const appItems = useMemo(() => secrets.apps.map(a => ({
|
|
18
|
+
...a,
|
|
19
|
+
// same reasoning as the dashboard view — ScrollableList memoises rows by
|
|
20
|
+
// item identity, so the redacted label must live on the item itself.
|
|
21
|
+
displayApp: state.redacted ? redactName(a.app) : a.app,
|
|
22
|
+
})), [secrets.apps, state.redacted]);
|
|
17
23
|
const refresh = secrets.refresh;
|
|
18
24
|
useEffect(() => {
|
|
19
25
|
refresh();
|
|
@@ -47,11 +53,14 @@ export function SecretsView() {
|
|
|
47
53
|
return true;
|
|
48
54
|
}
|
|
49
55
|
if (input === 'l') {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
// Seal acquires a manifest lock and may take a moment; fire-and-forget
|
|
57
|
+
// from the input handler, dispatch errors / refresh on completion.
|
|
58
|
+
secrets.seal().then(result => {
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
dispatch({ type: 'SET_ERROR', error: result.error ?? 'Seal failed' });
|
|
61
|
+
}
|
|
62
|
+
secrets.refresh();
|
|
63
|
+
});
|
|
55
64
|
return true;
|
|
56
65
|
}
|
|
57
66
|
}
|
|
@@ -89,14 +98,15 @@ export function SecretsView() {
|
|
|
89
98
|
label: `Delete secret "${secretKey}"?`,
|
|
90
99
|
description: `This will remove ${secretKey} from ${redact(appName)}'s vault.`,
|
|
91
100
|
onConfirm: () => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
secrets.deleteSecret(appName, secretKey).then(result => {
|
|
102
|
+
if (result.ok) {
|
|
103
|
+
secrets.loadAppSecrets(appName);
|
|
104
|
+
secrets.refresh();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
dispatch({ type: 'SET_ERROR', error: result.error ?? 'Delete failed' });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
100
110
|
},
|
|
101
111
|
},
|
|
102
112
|
});
|
|
@@ -117,7 +127,7 @@ export function SecretsView() {
|
|
|
117
127
|
}, [subView, selectedIndex, selectedApp, secrets, dispatch, redact]);
|
|
118
128
|
useRegisterHandler(handler);
|
|
119
129
|
const listHeight = Math.max(5, availableHeight - 5);
|
|
120
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, paddingX: 1, gap: 2, children: [_jsx(Text, { bold: true, children: "Vault:" }), !secrets.initialized ? (_jsx(Text, { color: colors.error, children: "Not initialized" })) : secrets.sealed ? (_jsx(Text, { color: colors.warning, bold: true, children: "SEALED" })) : (_jsx(Text, { color: colors.success, bold: true, children: "UNSEALED" })), _jsxs(Text, { color: colors.muted, children: [secrets.apps.length, " apps | ", secrets.apps.reduce((sum, a) => sum + a.keyCount, 0), " keys"] })] }), secrets.error && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.error, children: secrets.error }) })), subView === 'app-list' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Apps with secrets:" }), _jsx(ScrollableList, { items:
|
|
130
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, paddingX: 1, gap: 2, children: [_jsx(Text, { bold: true, children: "Vault:" }), !secrets.initialized ? (_jsx(Text, { color: colors.error, children: "Not initialized" })) : secrets.sealed ? (_jsx(Text, { color: colors.warning, bold: true, children: "SEALED" })) : (_jsx(Text, { color: colors.success, bold: true, children: "UNSEALED" })), _jsxs(Text, { color: colors.muted, children: [secrets.apps.length, " apps | ", secrets.apps.reduce((sum, a) => sum + a.keyCount, 0), " keys"] })] }), secrets.error && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.error, children: secrets.error }) })), subView === 'app-list' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Apps with secrets:" }), _jsx(ScrollableList, { items: appItems, selectedIndex: Math.min(selectedIndex, appItems.length - 1), maxVisible: listHeight, emptyText: " No secrets managed", renderItem: (app, selected) => (_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, children: selected ? '> ' : ' ' }), _jsx(Text, { bold: selected, color: selected ? colors.primary : colors.text, children: app.displayApp.padEnd(24) }), _jsx(Text, { color: colors.muted, children: app.type.padEnd(14) }), _jsxs(Text, { children: [String(app.keyCount).padEnd(8), " keys"] })] })) })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.primary, children: redact(selectedApp ?? '') }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ScrollableList, { items: secrets.secrets, selectedIndex: Math.min(selectedIndex, secrets.secrets.length - 1), maxVisible: listHeight, emptyText: " No secrets found", renderItem: (secret, selected) => {
|
|
121
131
|
const revealed = secrets.revealedValues[secret.key];
|
|
122
132
|
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, children: selected ? '> ' : ' ' }), _jsx(Text, { bold: selected, color: selected ? colors.primary : colors.text, children: secret.key.padEnd(30) }), _jsx(Text, { color: revealed ? colors.warning : colors.muted, children: revealed ?? secret.maskedValue })] }));
|
|
123
133
|
} }) })] }))] }));
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthesketh/fleet",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Docker production management CLI + MCP server for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"fleet": "dist/index.js"
|
|
8
|
+
"fleet": "dist/index.js",
|
|
9
|
+
"fleet-agent": "dist/bin/fleet-agent.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"dist/",
|
|
@@ -15,9 +16,10 @@
|
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
|
-
"build": "tsc && tsc-alias",
|
|
19
|
+
"build": "tsc && tsc-alias --resolve-full-paths",
|
|
19
20
|
"dev": "tsx src/index.ts",
|
|
20
21
|
"test": "vitest run",
|
|
22
|
+
"changelog": "node scripts/gen-changelog.mjs > CHANGELOG.md",
|
|
21
23
|
"prepublishOnly": "npm run build"
|
|
22
24
|
},
|
|
23
25
|
"keywords": [
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
"@matthesketh/ink-modal": "^0.1.0",
|
|
60
62
|
"@matthesketh/ink-pipeline": "^0.1.0",
|
|
61
63
|
"@matthesketh/ink-rule": "^0.1.0",
|
|
62
|
-
"@matthesketh/ink-scrollable-list": "
|
|
64
|
+
"@matthesketh/ink-scrollable-list": "0.2.0",
|
|
63
65
|
"@matthesketh/ink-split-pane": "^0.1.0",
|
|
64
66
|
"@matthesketh/ink-stable-state": "^0.1.0",
|
|
65
67
|
"@matthesketh/ink-status-bar": "^0.1.0",
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
"@matthesketh/ink-task-list": "0.1.0",
|
|
69
71
|
"@matthesketh/ink-timeline": "0.1.0",
|
|
70
72
|
"@matthesketh/ink-toast": "^0.1.0",
|
|
71
|
-
"@matthesketh/ink-viewport": "^0.1.
|
|
73
|
+
"@matthesketh/ink-viewport": "^0.1.1",
|
|
72
74
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
73
75
|
"better-sqlite3": "12.9.0",
|
|
74
76
|
"chokidar": "5.0.0",
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
"ink-text-input": "^6.0.0",
|
|
78
80
|
"proper-lockfile": "4.1.2",
|
|
79
81
|
"react": "^18.3.1",
|
|
82
|
+
"yaml": "2.8.4",
|
|
80
83
|
"zod": "^3.24.0"
|
|
81
84
|
},
|
|
82
85
|
"devDependencies": {
|
|
@@ -90,4 +93,4 @@
|
|
|
90
93
|
"typescript": "5.6.3",
|
|
91
94
|
"vitest": "4.0.18"
|
|
92
95
|
}
|
|
93
|
-
}
|
|
96
|
+
}
|