@matthesketh/fleet 1.8.1 → 1.11.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.
- 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/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/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/index.js +0 -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 +9 -6
- package/dist/tui/views/HealthView.js +9 -4
- 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 +8 -5
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** build the templated systemd unit for fleet-secrets-agent@%i.service.
|
|
2
|
+
* the vault path comes from the caller — production code passes whatever
|
|
3
|
+
* FLEET_VAULT_DIR or the repo-local default resolves to, so this template
|
|
4
|
+
* never carries an operator-specific assumption. */
|
|
5
|
+
export function generateAgentUnit(vaultPath) {
|
|
6
|
+
return [
|
|
7
|
+
'[Unit]',
|
|
8
|
+
'Description=Fleet Secrets Agent for %i',
|
|
9
|
+
'After=network.target',
|
|
10
|
+
'PartOf=docker-%i.service',
|
|
11
|
+
'',
|
|
12
|
+
'[Service]',
|
|
13
|
+
'Type=notify',
|
|
14
|
+
'DynamicUser=yes',
|
|
15
|
+
'RuntimeDirectory=fleet-secrets',
|
|
16
|
+
'RuntimeDirectoryPreserve=yes',
|
|
17
|
+
'LoadCredentialEncrypted=age-key:/etc/fleet/credentials/%i.cred',
|
|
18
|
+
`ExecStart=/usr/local/bin/fleet-agent --app %i --vault ${vaultPath} --socket /run/fleet-secrets/%i.sock`,
|
|
19
|
+
'Restart=on-failure',
|
|
20
|
+
'RestartSec=2',
|
|
21
|
+
'',
|
|
22
|
+
'# hardening',
|
|
23
|
+
'ProtectSystem=strict',
|
|
24
|
+
'ProtectHome=read-only',
|
|
25
|
+
`ReadOnlyPaths=${vaultPath}`,
|
|
26
|
+
'PrivateTmp=yes',
|
|
27
|
+
'NoNewPrivileges=yes',
|
|
28
|
+
'ProtectKernelTunables=yes',
|
|
29
|
+
'ProtectKernelModules=yes',
|
|
30
|
+
'ProtectControlGroups=yes',
|
|
31
|
+
'RestrictAddressFamilies=AF_UNIX',
|
|
32
|
+
'RestrictNamespaces=yes',
|
|
33
|
+
'SystemCallFilter=@system-service',
|
|
34
|
+
'SystemCallFilter=~@privileged @resources @mount',
|
|
35
|
+
'',
|
|
36
|
+
'[Install]',
|
|
37
|
+
'WantedBy=multi-user.target',
|
|
38
|
+
'',
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function findUnitSection(content) {
|
|
2
|
+
const lines = content.split('\n');
|
|
3
|
+
let start = -1;
|
|
4
|
+
let end = -1;
|
|
5
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6
|
+
if (lines[i].trim() === '[Unit]') {
|
|
7
|
+
start = i;
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (start >= 0 && lines[i].startsWith('[') && lines[i].endsWith(']')) {
|
|
11
|
+
end = i;
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (start < 0)
|
|
16
|
+
throw new Error('no [Unit] section found');
|
|
17
|
+
if (end < 0)
|
|
18
|
+
end = lines.length;
|
|
19
|
+
return { start, end };
|
|
20
|
+
}
|
|
21
|
+
export function addAgentDependency(content, app) {
|
|
22
|
+
const lines = content.split('\n');
|
|
23
|
+
const requires = `Requires=fleet-secrets-agent@${app}.service`;
|
|
24
|
+
const after = `After=fleet-secrets-agent@${app}.service`;
|
|
25
|
+
if (lines.includes(requires) && lines.includes(after))
|
|
26
|
+
return content;
|
|
27
|
+
const section = findUnitSection(content);
|
|
28
|
+
let insertAt = section.end;
|
|
29
|
+
while (insertAt > section.start + 1 && lines[insertAt - 1].trim() === '')
|
|
30
|
+
insertAt--;
|
|
31
|
+
const toInsert = [];
|
|
32
|
+
if (!lines.includes(requires))
|
|
33
|
+
toInsert.push(requires);
|
|
34
|
+
if (!lines.includes(after))
|
|
35
|
+
toInsert.push(after);
|
|
36
|
+
lines.splice(insertAt, 0, ...toInsert);
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
export function removeAgentDependency(content, app) {
|
|
40
|
+
const requires = `Requires=fleet-secrets-agent@${app}.service`;
|
|
41
|
+
const after = `After=fleet-secrets-agent@${app}.service`;
|
|
42
|
+
return content
|
|
43
|
+
.split('\n')
|
|
44
|
+
.filter(l => l !== requires && l !== after)
|
|
45
|
+
.join('\n');
|
|
46
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { parseDocument, Scalar, isMap, isSeq, isScalar } from 'yaml';
|
|
2
|
+
const FLEET_ENV_KEY = 'FLEET_SECRETS_SOCKET';
|
|
3
|
+
const FLEET_ENV_VAL = '/run/fleet.sock';
|
|
4
|
+
function v1EnvFile(app) {
|
|
5
|
+
return `/run/fleet-secrets/${app}/.env`;
|
|
6
|
+
}
|
|
7
|
+
function v2SocketMount(app) {
|
|
8
|
+
return `/run/fleet-secrets/${app}.sock:/run/fleet.sock:ro`;
|
|
9
|
+
}
|
|
10
|
+
function getService(doc, service) {
|
|
11
|
+
const svc = doc.getIn(['services', service], true);
|
|
12
|
+
if (!svc || !isMap(svc)) {
|
|
13
|
+
throw new Error(`service '${service}' not found in compose file`);
|
|
14
|
+
}
|
|
15
|
+
return svc;
|
|
16
|
+
}
|
|
17
|
+
function removeEnvFileEntry(svc, app) {
|
|
18
|
+
const v1Path = v1EnvFile(app);
|
|
19
|
+
const raw = svc.get('env_file', true);
|
|
20
|
+
if (raw === undefined || raw === null)
|
|
21
|
+
return;
|
|
22
|
+
if (isScalar(raw)) {
|
|
23
|
+
if (raw.value === v1Path) {
|
|
24
|
+
svc.delete('env_file');
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (isSeq(raw)) {
|
|
29
|
+
const seq = raw;
|
|
30
|
+
const idx = seq.items.findIndex(item => isScalar(item) && item.value === v1Path);
|
|
31
|
+
if (idx !== -1) {
|
|
32
|
+
seq.items.splice(idx, 1);
|
|
33
|
+
}
|
|
34
|
+
if (seq.items.length === 0) {
|
|
35
|
+
svc.delete('env_file');
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (typeof raw === 'string' && raw === v1Path) {
|
|
40
|
+
svc.delete('env_file');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function ensureEnvVar(svc) {
|
|
44
|
+
const envRaw = svc.get('environment', true);
|
|
45
|
+
if (!envRaw) {
|
|
46
|
+
svc.set('environment', { [FLEET_ENV_KEY]: FLEET_ENV_VAL });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (isMap(envRaw)) {
|
|
50
|
+
const env = envRaw;
|
|
51
|
+
if (!env.has(FLEET_ENV_KEY)) {
|
|
52
|
+
env.set(FLEET_ENV_KEY, FLEET_ENV_VAL);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (isSeq(envRaw)) {
|
|
57
|
+
const seq = envRaw;
|
|
58
|
+
const kvPrefix = `${FLEET_ENV_KEY}=`;
|
|
59
|
+
const already = seq.items.some(item => isScalar(item) && typeof item.value === 'string' && item.value.startsWith(kvPrefix));
|
|
60
|
+
if (!already) {
|
|
61
|
+
seq.add(`${FLEET_ENV_KEY}=${FLEET_ENV_VAL}`);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function removeEnvVar(svc) {
|
|
67
|
+
const envRaw = svc.get('environment', true);
|
|
68
|
+
if (!envRaw)
|
|
69
|
+
return;
|
|
70
|
+
if (isMap(envRaw)) {
|
|
71
|
+
const env = envRaw;
|
|
72
|
+
env.delete(FLEET_ENV_KEY);
|
|
73
|
+
if (env.items.length === 0) {
|
|
74
|
+
svc.delete('environment');
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (isSeq(envRaw)) {
|
|
79
|
+
const seq = envRaw;
|
|
80
|
+
const kvPrefix = `${FLEET_ENV_KEY}=`;
|
|
81
|
+
const idx = seq.items.findIndex(item => isScalar(item) && typeof item.value === 'string' && item.value.startsWith(kvPrefix));
|
|
82
|
+
if (idx !== -1) {
|
|
83
|
+
seq.items.splice(idx, 1);
|
|
84
|
+
}
|
|
85
|
+
if (seq.items.length === 0) {
|
|
86
|
+
svc.delete('environment');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function ensureSocketMount(svc, app) {
|
|
91
|
+
const mount = v2SocketMount(app);
|
|
92
|
+
const volRaw = svc.get('volumes', true);
|
|
93
|
+
if (!volRaw) {
|
|
94
|
+
svc.set('volumes', [mount]);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (isSeq(volRaw)) {
|
|
98
|
+
const seq = volRaw;
|
|
99
|
+
const already = seq.items.some(item => isScalar(item) && item.value === mount);
|
|
100
|
+
if (!already) {
|
|
101
|
+
seq.add(mount);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function removeSocketMount(svc, app) {
|
|
107
|
+
const mount = v2SocketMount(app);
|
|
108
|
+
const volRaw = svc.get('volumes', true);
|
|
109
|
+
if (!volRaw || !isSeq(volRaw))
|
|
110
|
+
return;
|
|
111
|
+
const seq = volRaw;
|
|
112
|
+
const idx = seq.items.findIndex(item => isScalar(item) && item.value === mount);
|
|
113
|
+
if (idx !== -1) {
|
|
114
|
+
seq.items.splice(idx, 1);
|
|
115
|
+
}
|
|
116
|
+
if (seq.items.length === 0) {
|
|
117
|
+
svc.delete('volumes');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function restoreEnvFile(svc, app) {
|
|
121
|
+
const v1Path = v1EnvFile(app);
|
|
122
|
+
const raw = svc.get('env_file', true);
|
|
123
|
+
if (!raw) {
|
|
124
|
+
svc.set('env_file', v1Path);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (isScalar(raw)) {
|
|
128
|
+
if (raw.value !== v1Path) {
|
|
129
|
+
svc.set('env_file', v1Path);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (isSeq(raw)) {
|
|
134
|
+
const seq = raw;
|
|
135
|
+
const already = seq.items.some(item => isScalar(item) && item.value === v1Path);
|
|
136
|
+
if (!already) {
|
|
137
|
+
seq.items.unshift(new Scalar(v1Path));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function migrateComposeToV2(yamlContent, app, service) {
|
|
142
|
+
const doc = parseDocument(yamlContent);
|
|
143
|
+
const svc = getService(doc, service);
|
|
144
|
+
removeEnvFileEntry(svc, app);
|
|
145
|
+
ensureEnvVar(svc);
|
|
146
|
+
ensureSocketMount(svc, app);
|
|
147
|
+
return doc.toString();
|
|
148
|
+
}
|
|
149
|
+
export function revertComposeFromV2(yamlContent, app, service) {
|
|
150
|
+
const doc = parseDocument(yamlContent);
|
|
151
|
+
const svc = getService(doc, service);
|
|
152
|
+
removeSocketMount(svc, app);
|
|
153
|
+
removeEnvVar(svc);
|
|
154
|
+
restoreEnvFile(svc, app);
|
|
155
|
+
return doc.toString();
|
|
156
|
+
}
|
package/dist/templates/nginx.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
import { assertDomain, assertHealthPath } from '../core/validate.js';
|
|
1
2
|
export function generateNginxConfig(opts) {
|
|
3
|
+
// defence-in-depth: callers are expected to validate already, but a
|
|
4
|
+
// missed call must not produce a config that injects directives via
|
|
5
|
+
// ${domain} (server_name interpolation) or ${port} (proxy_pass).
|
|
6
|
+
// mirrors the assertComposeFile pattern in src/templates/systemd.ts.
|
|
7
|
+
assertDomain(opts.domain);
|
|
8
|
+
if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
|
|
9
|
+
throw new Error(`invalid port: ${opts.port}`);
|
|
10
|
+
}
|
|
11
|
+
if (opts.apiPrefix !== undefined)
|
|
12
|
+
assertHealthPath(opts.apiPrefix);
|
|
2
13
|
const { domain, port, type } = opts;
|
|
3
14
|
const apiPrefix = opts.apiPrefix ?? '/api';
|
|
4
15
|
const securityHeaders = ` add_header X-Content-Type-Options "nosniff" always;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { assertComposeFile } from '../core/validate.js';
|
|
1
2
|
export function generateServiceFile(opts) {
|
|
3
|
+
// Defence-in-depth: even if a caller skipped upstream validation, refuse to
|
|
4
|
+
// emit a unit file with a composeFile value that could break out of the
|
|
5
|
+
// quoted -f argument and inject extra docker-compose flags or shell.
|
|
6
|
+
if (opts.composeFile)
|
|
7
|
+
assertComposeFile(opts.composeFile);
|
|
2
8
|
const fileFlag = opts.composeFile ? ` -f "${opts.composeFile}"` : '';
|
|
3
9
|
const dbDep = opts.dependsOnDatabases ? ' docker-databases.service' : '';
|
|
4
10
|
return `[Unit]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
|
|
6
|
+
import { colors } from '../theme.js';
|
|
7
|
+
function describeField(name, schema) {
|
|
8
|
+
let s = schema;
|
|
9
|
+
while (s instanceof z.ZodOptional || s instanceof z.ZodDefault) {
|
|
10
|
+
s = s._def.innerType;
|
|
11
|
+
}
|
|
12
|
+
if (s instanceof z.ZodBoolean)
|
|
13
|
+
return { name, kind: 'boolean' };
|
|
14
|
+
if (s instanceof z.ZodNumber)
|
|
15
|
+
return { name, kind: 'number' };
|
|
16
|
+
if (s instanceof z.ZodEnum)
|
|
17
|
+
return { name, kind: 'enum', options: s._def.values };
|
|
18
|
+
return { name, kind: 'string' };
|
|
19
|
+
}
|
|
20
|
+
export function ArgForm(props) {
|
|
21
|
+
const fields = Object.entries(props.schema.shape).map(([n, s]) => describeField(n, s));
|
|
22
|
+
const [cursor, setCursor] = useState(0);
|
|
23
|
+
const [values, setValues] = useState(() => Object.fromEntries(fields.map(f => [f.name, f.kind === 'boolean' ? false : ''])));
|
|
24
|
+
const handler = (input, key) => {
|
|
25
|
+
if (key.escape) {
|
|
26
|
+
props.onCancel();
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (key.return) {
|
|
30
|
+
props.onSubmit(values);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (key.downArrow) {
|
|
34
|
+
setCursor(c => Math.min(c + 1, fields.length - 1));
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (key.upArrow) {
|
|
38
|
+
setCursor(c => Math.max(c - 1, 0));
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const field = fields[cursor];
|
|
42
|
+
if (!field)
|
|
43
|
+
return false;
|
|
44
|
+
if (field.kind === 'boolean') {
|
|
45
|
+
if (input === ' ') {
|
|
46
|
+
setValues(v => ({ ...v, [field.name]: !v[field.name] }));
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (key.backspace || key.delete) {
|
|
51
|
+
setValues(v => ({ ...v, [field.name]: String(v[field.name] ?? '').slice(0, -1) }));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
55
|
+
setValues(v => ({ ...v, [field.name]: String(v[field.name] ?? '') + input }));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
useRegisterHandler(handler);
|
|
61
|
+
return (_jsxs(Box, { flexDirection: "column", children: [fields.map((f, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: i === cursor ? colors.primary : colors.muted, children: [i === cursor ? '> ' : ' ', f.name, ":", ' '] }), _jsx(Text, { children: f.kind === 'boolean'
|
|
62
|
+
? (values[f.name] ? '[x]' : '[ ]')
|
|
63
|
+
: String(values[f.name] ?? '') })] }, f.name))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "\u2191\u2193 field \u00B7 space toggle \u00B7 enter run \u00B7 esc cancel" }) })] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
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 { z } from 'zod';
|
|
5
|
+
import { ArgForm } from './ArgForm.js';
|
|
6
|
+
describe('ArgForm', () => {
|
|
7
|
+
it('renders one labelled field per schema property', () => {
|
|
8
|
+
const schema = z.object({ app: z.string(), force: z.boolean().default(false) });
|
|
9
|
+
const { lastFrame } = render(_jsx(ArgForm, { schema: schema, onSubmit: () => { }, onCancel: () => { } }));
|
|
10
|
+
const frame = lastFrame() ?? '';
|
|
11
|
+
expect(frame).toContain('app');
|
|
12
|
+
expect(frame).toContain('force');
|
|
13
|
+
});
|
|
14
|
+
it('shows a toggle hint for boolean fields', () => {
|
|
15
|
+
const schema = z.object({ force: z.boolean().default(false) });
|
|
16
|
+
const { lastFrame } = render(_jsx(ArgForm, { schema: schema, onSubmit: () => { }, onCancel: () => { } }));
|
|
17
|
+
expect(lastFrame() ?? '').toMatch(/false|off|\[ \]/i);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -54,6 +54,11 @@ const viewHints = {
|
|
|
54
54
|
{ key: 'L', label: 'level' },
|
|
55
55
|
{ key: 'q', label: 'quit' },
|
|
56
56
|
],
|
|
57
|
+
'command-palette': [
|
|
58
|
+
{ key: '↑/↓', label: 'navigate' },
|
|
59
|
+
{ key: 'Enter', label: 'run' },
|
|
60
|
+
{ key: 'Esc', label: 'close' },
|
|
61
|
+
],
|
|
57
62
|
};
|
|
58
63
|
export function KeyHint() {
|
|
59
64
|
const { currentView, confirmAction } = useAppState();
|
|
@@ -20,28 +20,28 @@ interface SecretsState {
|
|
|
20
20
|
interface SecretsActions {
|
|
21
21
|
refresh: () => void;
|
|
22
22
|
loadAppSecrets: (app: string) => void;
|
|
23
|
-
saveSecret: (app: string, key: string, value: string) => {
|
|
23
|
+
saveSecret: (app: string, key: string, value: string) => Promise<{
|
|
24
24
|
ok: boolean;
|
|
25
25
|
error?: string;
|
|
26
|
-
}
|
|
27
|
-
deleteSecret: (app: string, key: string) => {
|
|
26
|
+
}>;
|
|
27
|
+
deleteSecret: (app: string, key: string) => Promise<{
|
|
28
28
|
ok: boolean;
|
|
29
29
|
error?: string;
|
|
30
|
-
}
|
|
30
|
+
}>;
|
|
31
31
|
revealSecret: (app: string, key: string) => void;
|
|
32
32
|
hideSecret: (key: string) => void;
|
|
33
33
|
unseal: () => {
|
|
34
34
|
ok: boolean;
|
|
35
35
|
error?: string;
|
|
36
36
|
};
|
|
37
|
-
seal: () => {
|
|
37
|
+
seal: () => Promise<{
|
|
38
38
|
ok: boolean;
|
|
39
39
|
error?: string;
|
|
40
|
-
}
|
|
41
|
-
importEnv: (app: string, path: string) => {
|
|
40
|
+
}>;
|
|
41
|
+
importEnv: (app: string, path: string) => Promise<{
|
|
42
42
|
ok: boolean;
|
|
43
43
|
error?: string;
|
|
44
|
-
}
|
|
44
|
+
}>;
|
|
45
45
|
}
|
|
46
46
|
export declare function useSecrets(): SecretsState & SecretsActions;
|
|
47
47
|
export {};
|
|
@@ -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 {};
|