@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,65 @@
|
|
|
1
|
+
import { loadRegistry } from '../registry/index.js';
|
|
2
|
+
import { allCommands } from '../registry/registry.js';
|
|
3
|
+
import { makeMcpContext } from '../registry/context.js';
|
|
4
|
+
/**
|
|
5
|
+
* the mcp tool name for a registry command. ':' namespacing is not legal in
|
|
6
|
+
* tool names, so subcommands collapse to underscores. this string is the
|
|
7
|
+
* cross-surface contract, so both bridge functions derive it the same way.
|
|
8
|
+
*/
|
|
9
|
+
function toMcpToolName(commandName) {
|
|
10
|
+
return 'fleet_' + commandName.replace(/:/g, '_');
|
|
11
|
+
}
|
|
12
|
+
/** registry commands as flat tool descriptors, for inspection and tests. */
|
|
13
|
+
export function collectRegistryTools() {
|
|
14
|
+
loadRegistry();
|
|
15
|
+
return allCommands()
|
|
16
|
+
.filter(def => !def.cliOnly)
|
|
17
|
+
.map(def => ({ toolName: toMcpToolName(def.name), summary: def.summary }));
|
|
18
|
+
}
|
|
19
|
+
/** registers every non-cliOnly registry command as an mcp tool. */
|
|
20
|
+
export function registerRegistryTools(server) {
|
|
21
|
+
loadRegistry();
|
|
22
|
+
for (const def of allCommands()) {
|
|
23
|
+
if (def.cliOnly)
|
|
24
|
+
continue;
|
|
25
|
+
server.tool(toMcpToolName(def.name), def.summary, def.args.shape, async (args) => {
|
|
26
|
+
// `confirm` is mcp surface plumbing, not a command arg — read it from the
|
|
27
|
+
// raw input before the schema parse below strips unknown keys.
|
|
28
|
+
const ctx = makeMcpContext(args.confirm === true);
|
|
29
|
+
try {
|
|
30
|
+
// validate, coerce and default args against the command schema — the
|
|
31
|
+
// same safeParse the cli dispatcher runs via parseArgs, so both
|
|
32
|
+
// surfaces invoke `run` with an identically-validated args shape.
|
|
33
|
+
const parsed = def.args.safeParse(args);
|
|
34
|
+
if (!parsed.success) {
|
|
35
|
+
const detail = parsed.error.issues
|
|
36
|
+
.map(iss => `${iss.path.join('.')}: ${iss.message}`)
|
|
37
|
+
.join('; ');
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: 'text', text: `invalid arguments: ${detail}` }],
|
|
40
|
+
isError: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const result = await def.run(parsed.data, ctx);
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: 'text', text: result.summary }],
|
|
46
|
+
// structuredContent must be an object — only attach it when the
|
|
47
|
+
// command actually returned one, otherwise the sdk rejects the shape.
|
|
48
|
+
...(result.data && typeof result.data === 'object'
|
|
49
|
+
? { structuredContent: result.data }
|
|
50
|
+
: {}),
|
|
51
|
+
isError: !result.ok,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// the bridge is the single funnel for every migrated command — a
|
|
56
|
+
// thrown handler must still surface as a structured tool failure,
|
|
57
|
+
// not an opaque transport-level rejection.
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -19,7 +19,7 @@ export function registerSecretsTools(server) {
|
|
|
19
19
|
value: z.string().describe('Secret value'),
|
|
20
20
|
}, async ({ app, key, value }) => {
|
|
21
21
|
requireVault();
|
|
22
|
-
setSecret(app, key, value);
|
|
22
|
+
await setSecret(app, key, value);
|
|
23
23
|
return text(`Set ${key} for ${app} in vault. Run fleet_secrets_unseal + restart the app to apply at runtime.`);
|
|
24
24
|
});
|
|
25
25
|
server.tool('fleet_secrets_get', 'Get a single decrypted secret value from the vault. ' +
|
|
@@ -41,7 +41,7 @@ export function registerSecretsTools(server) {
|
|
|
41
41
|
app: z.string().optional().describe('App name (omit to seal all apps)'),
|
|
42
42
|
}, async ({ app }) => {
|
|
43
43
|
requireVault();
|
|
44
|
-
const sealed = sealFromRuntime(app);
|
|
44
|
+
const sealed = await sealFromRuntime(app);
|
|
45
45
|
return text(`Sealed ${sealed.length} app(s): ${sealed.join(', ')}. Changes will now persist across reboots.`);
|
|
46
46
|
});
|
|
47
47
|
server.tool('fleet_secrets_drift', 'Detect drift between vault (encrypted, survives reboot) and runtime (/run/fleet-secrets/, lost on reboot). ' +
|
package/dist/mcp/server.js
CHANGED
|
@@ -4,26 +4,25 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { startService, stopService, restartService } from '../core/systemd.js';
|
|
7
|
+
import { load, findApp, addApp, withRegistry } from '../core/registry.js';
|
|
8
|
+
import { restartService } from '../core/systemd.js';
|
|
10
9
|
import { getContainerLogs, getContainersByCompose } from '../core/docker.js';
|
|
11
|
-
import { checkHealth, checkAllHealth } from '../core/health.js';
|
|
12
10
|
import { listSites, installConfig, testConfig, reload, removeConfig } from '../core/nginx.js';
|
|
13
11
|
import { generateNginxConfig } from '../templates/nginx.js';
|
|
14
12
|
import { composeBuild } from '../core/docker.js';
|
|
15
|
-
import { execSafe } from '../core/exec.js';
|
|
16
13
|
import { AppNotFoundError } from '../core/errors.js';
|
|
17
|
-
import { assertAppName, assertServiceName, assertFilePath, assertDomain } from '../core/validate.js';
|
|
14
|
+
import { assertAppName, assertServiceName, assertFilePath, assertDomain, assertComposeFile } from '../core/validate.js';
|
|
18
15
|
import { loadManifest, listSecrets, isInitialized } from '../core/secrets.js';
|
|
19
16
|
import { unsealAll, getStatus as getSecretsStatus } from '../core/secrets-ops.js';
|
|
20
17
|
import { validateApp, validateAll } from '../core/secrets-validate.js';
|
|
21
|
-
import { freezeApp, unfreezeApp } from '../commands/freeze.js';
|
|
22
18
|
import { registerGitTools } from './git-tools.js';
|
|
23
19
|
import { registerSecretsTools } from './secrets-tools.js';
|
|
20
|
+
import { registerRegistryTools } from './registry-bridge.js';
|
|
24
21
|
import { readContainerLogs, getLogStatus, effectivePolicy } from '../core/logs-policy.js';
|
|
25
22
|
import { snapshotEgress } from '../core/egress.js';
|
|
26
23
|
import { registerDepsTools } from './deps-tools.js';
|
|
24
|
+
import { registerAuditTools } from './audit-tools.js';
|
|
25
|
+
import { registerTestflightTools } from './testflight-tools.js';
|
|
27
26
|
function requireApp(name) {
|
|
28
27
|
const reg = load();
|
|
29
28
|
const app = findApp(reg, name);
|
|
@@ -41,29 +40,7 @@ export async function startMcpServer() {
|
|
|
41
40
|
name: 'fleet',
|
|
42
41
|
version: pkg.version,
|
|
43
42
|
});
|
|
44
|
-
server
|
|
45
|
-
const data = getStatusData();
|
|
46
|
-
return text(JSON.stringify(data, null, 2));
|
|
47
|
-
});
|
|
48
|
-
server.tool('fleet_list', 'List all registered apps with their configuration', async () => {
|
|
49
|
-
const reg = load();
|
|
50
|
-
return text(JSON.stringify(reg.apps, null, 2));
|
|
51
|
-
});
|
|
52
|
-
server.tool('fleet_start', 'Start an app via systemctl', { app: z.string().describe('App name') }, async ({ app }) => {
|
|
53
|
-
const entry = requireApp(app);
|
|
54
|
-
const ok = startService(entry.serviceName);
|
|
55
|
-
return text(ok ? `Started ${entry.name}` : `Failed to start ${entry.name}`);
|
|
56
|
-
});
|
|
57
|
-
server.tool('fleet_stop', 'Stop an app via systemctl', { app: z.string().describe('App name') }, async ({ app }) => {
|
|
58
|
-
const entry = requireApp(app);
|
|
59
|
-
const ok = stopService(entry.serviceName);
|
|
60
|
-
return text(ok ? `Stopped ${entry.name}` : `Failed to stop ${entry.name}`);
|
|
61
|
-
});
|
|
62
|
-
server.tool('fleet_restart', 'Restart an app via systemctl', { app: z.string().describe('App name') }, async ({ app }) => {
|
|
63
|
-
const entry = requireApp(app);
|
|
64
|
-
const ok = restartService(entry.serviceName);
|
|
65
|
-
return text(ok ? `Restarted ${entry.name}` : `Failed to restart ${entry.name}`);
|
|
66
|
-
});
|
|
43
|
+
registerRegistryTools(server);
|
|
67
44
|
server.tool('fleet_logs', 'DEPRECATED — prefer fleet_logs_recent (token-conservative defaults) or fleet_logs_summary. Get recent container logs for an app.', {
|
|
68
45
|
app: z.string().describe('App name'),
|
|
69
46
|
container: z.string().optional().describe('Container name (omit to list available containers, or get logs from first)'),
|
|
@@ -196,18 +173,6 @@ export async function startMcpServer() {
|
|
|
196
173
|
}
|
|
197
174
|
return text(JSON.stringify(out, null, 2));
|
|
198
175
|
});
|
|
199
|
-
server.tool('fleet_health', 'Run health checks for one or all apps', { app: z.string().optional().describe('App name (omit for all apps)') }, async ({ app }) => {
|
|
200
|
-
const reg = load();
|
|
201
|
-
if (app) {
|
|
202
|
-
const entry = findApp(reg, app);
|
|
203
|
-
if (!entry)
|
|
204
|
-
throw new AppNotFoundError(app);
|
|
205
|
-
const result = checkHealth(entry);
|
|
206
|
-
return text(JSON.stringify(result, null, 2));
|
|
207
|
-
}
|
|
208
|
-
const results = checkAllHealth(reg.apps);
|
|
209
|
-
return text(JSON.stringify(results, null, 2));
|
|
210
|
-
});
|
|
211
176
|
server.tool('fleet_deploy', 'Deploy an app: build and restart', { app: z.string().describe('App name') }, async ({ app }) => {
|
|
212
177
|
const entry = requireApp(app);
|
|
213
178
|
const buildOk = composeBuild(entry.composePath, entry.composeFile, entry.name);
|
|
@@ -287,7 +252,7 @@ export async function startMcpServer() {
|
|
|
287
252
|
if (params.serviceName)
|
|
288
253
|
assertServiceName(params.serviceName);
|
|
289
254
|
if (params.composeFile)
|
|
290
|
-
|
|
255
|
+
assertComposeFile(params.composeFile);
|
|
291
256
|
for (const d of (params.domains ?? []))
|
|
292
257
|
assertDomain(d);
|
|
293
258
|
}
|
|
@@ -297,8 +262,6 @@ export async function startMcpServer() {
|
|
|
297
262
|
if (!existsSync(params.composePath)) {
|
|
298
263
|
return text(`Error: composePath does not exist: ${params.composePath}`);
|
|
299
264
|
}
|
|
300
|
-
const reg = load();
|
|
301
|
-
const existing = findApp(reg, params.name);
|
|
302
265
|
let containers = params.containers;
|
|
303
266
|
if (!containers || containers.length === 0) {
|
|
304
267
|
containers = getContainersByCompose(params.composePath, params.composeFile ?? null);
|
|
@@ -319,48 +282,19 @@ export async function startMcpServer() {
|
|
|
319
282
|
dependsOnDatabases: params.dependsOnDatabases,
|
|
320
283
|
registeredAt: new Date().toISOString(),
|
|
321
284
|
};
|
|
322
|
-
|
|
323
|
-
|
|
285
|
+
let existed = false;
|
|
286
|
+
await withRegistry(reg => {
|
|
287
|
+
existed = !!findApp(reg, params.name);
|
|
288
|
+
return addApp(reg, entry);
|
|
289
|
+
});
|
|
290
|
+
const action = existed ? 'Updated' : 'Registered';
|
|
324
291
|
return text(`${action} app "${params.name}":\n${JSON.stringify(entry, null, 2)}`);
|
|
325
292
|
});
|
|
326
|
-
server.tool('fleet_freeze', 'Freeze a crash-looping service: stop it, disable it, and mark it frozen in the registry. Requires manual unfreezing.', {
|
|
327
|
-
app: z.string().describe('App name'),
|
|
328
|
-
reason: z.string().optional().describe('Reason for freezing'),
|
|
329
|
-
}, async ({ app, reason }) => {
|
|
330
|
-
freezeApp(app, reason);
|
|
331
|
-
return text(`Frozen ${app}${reason ? `: ${reason}` : ''}`);
|
|
332
|
-
});
|
|
333
|
-
server.tool('fleet_unfreeze', 'Unfreeze a frozen service: clear frozen state, enable and start the service.', { app: z.string().describe('App name') }, async ({ app }) => {
|
|
334
|
-
unfreezeApp(app);
|
|
335
|
-
return text(`Unfrozen ${app} — service enabled and started`);
|
|
336
|
-
});
|
|
337
|
-
server.tool('fleet_rollback', 'Roll back an app to its previous image (tagged <repo>:fleet-previous before the last build) and restart the service. Use this when a recent deploy or boot-refresh produced a broken image.', { app: z.string().describe('App name') }, async ({ app }) => {
|
|
338
|
-
const entry = requireApp(app);
|
|
339
|
-
// Resolve current image name via compose config
|
|
340
|
-
const config = execSafe('docker', ['compose', ...(entry.composeFile ? ['-f', entry.composeFile] : []), 'config', '--images'], { cwd: entry.composePath, timeout: 15_000 });
|
|
341
|
-
if (!config.ok)
|
|
342
|
-
return text(`Could not resolve image name for ${entry.name}: ${config.stderr}`);
|
|
343
|
-
const latest = config.stdout.split('\n').filter(Boolean)[0];
|
|
344
|
-
if (!latest)
|
|
345
|
-
return text(`Could not resolve image name for ${entry.name}`);
|
|
346
|
-
// Compute previous tag via lastIndexOf (handles registry:port/repo:tag)
|
|
347
|
-
const lastColon = latest.lastIndexOf(':');
|
|
348
|
-
const base = lastColon > 0 ? latest.slice(0, lastColon) : latest;
|
|
349
|
-
const previous = `${base}:fleet-previous`;
|
|
350
|
-
if (!execSafe('docker', ['image', 'inspect', previous], { timeout: 10_000 }).ok) {
|
|
351
|
-
return text(`No previous image found (${previous}) — nothing to roll back to`);
|
|
352
|
-
}
|
|
353
|
-
const tag = execSafe('docker', ['tag', previous, latest], { timeout: 10_000 });
|
|
354
|
-
if (!tag.ok)
|
|
355
|
-
return text(`docker tag failed: ${tag.stderr}`);
|
|
356
|
-
const ok = restartService(entry.serviceName);
|
|
357
|
-
return text(ok
|
|
358
|
-
? `Rolled back ${entry.name} to ${previous}`
|
|
359
|
-
: `Tag flipped but service restart failed for ${entry.serviceName}`);
|
|
360
|
-
});
|
|
361
293
|
registerGitTools(server);
|
|
362
294
|
registerSecretsTools(server);
|
|
363
295
|
registerDepsTools(server);
|
|
296
|
+
registerAuditTools(server);
|
|
297
|
+
registerTestflightTools(server);
|
|
364
298
|
const transport = new StdioServerTransport();
|
|
365
299
|
await server.connect(transport);
|
|
366
300
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { resolveTestflightTarget, appSecretsEnv } from '../core/testflight/resolve.js';
|
|
3
|
+
import { resolveAscCredentials, hasAscCredentials } from '../core/testflight/credentials.js';
|
|
4
|
+
import { listBuilds, verifyApp } from '../core/testflight/asc.js';
|
|
5
|
+
import { ghVersion, resolveRepo } from '../core/testflight/workflow.js';
|
|
6
|
+
function text(msg) {
|
|
7
|
+
return { content: [{ type: 'text', text: msg }] };
|
|
8
|
+
}
|
|
9
|
+
export function registerTestflightTools(server) {
|
|
10
|
+
server.tool('fleet_testflight_builds', "List an app's TestFlight builds via the App Store Connect API — build number, " +
|
|
11
|
+
'version, processing state and expiry. Requires the app\'s ASC credentials and ' +
|
|
12
|
+
'ASC_APP_ID in its fleet secrets.', { app: z.string().describe('Registered fleet app name') }, async ({ app }) => {
|
|
13
|
+
const { app: name } = resolveTestflightTarget(app);
|
|
14
|
+
const env = appSecretsEnv(name);
|
|
15
|
+
if (!hasAscCredentials(env)) {
|
|
16
|
+
return text(`App Store Connect credentials missing for ${name}.`);
|
|
17
|
+
}
|
|
18
|
+
const ascAppId = env.ASC_APP_ID;
|
|
19
|
+
if (!ascAppId)
|
|
20
|
+
return text(`ASC_APP_ID not set for ${name}.`);
|
|
21
|
+
const builds = await listBuilds(resolveAscCredentials(env), ascAppId);
|
|
22
|
+
return text(JSON.stringify(builds, null, 2));
|
|
23
|
+
});
|
|
24
|
+
server.tool('fleet_testflight_doctor', 'Check TestFlight publishing readiness for an app: GitHub CLI availability, the ' +
|
|
25
|
+
'GitHub repo backing the build workflow, App Store Connect credentials, and — when ' +
|
|
26
|
+
'ASC_APP_ID is set — that the ASC API is reachable.', { app: z.string().describe('Registered fleet app name') }, async ({ app }) => {
|
|
27
|
+
const { app: name, projectPath } = resolveTestflightTarget(app);
|
|
28
|
+
const env = appSecretsEnv(name);
|
|
29
|
+
const lines = [
|
|
30
|
+
`gh cli: ${ghVersion() ?? 'not found'}`,
|
|
31
|
+
`github repo: ${resolveRepo(projectPath) ?? 'not resolved'}`,
|
|
32
|
+
];
|
|
33
|
+
if (!hasAscCredentials(env)) {
|
|
34
|
+
lines.push('asc credentials: missing — need ASC_API_KEY_ID, ASC_API_KEY_ISSUER_ID, ASC_API_KEY_B64');
|
|
35
|
+
return text(lines.join('\n'));
|
|
36
|
+
}
|
|
37
|
+
lines.push('asc credentials: present');
|
|
38
|
+
if (env.ASC_APP_ID) {
|
|
39
|
+
try {
|
|
40
|
+
const appName = await verifyApp(resolveAscCredentials(env), env.ASC_APP_ID);
|
|
41
|
+
lines.push(`asc api: reachable — app "${appName}"`);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
lines.push(`asc api: check failed — ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
lines.push('asc app id: not set');
|
|
49
|
+
}
|
|
50
|
+
return text(lines.join('\n'));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CommandContext } from './types.js';
|
|
2
|
+
/** cli context: confirm prompts on stdin, log writes to stderr. */
|
|
3
|
+
export declare function makeCliContext(): CommandContext;
|
|
4
|
+
/** mcp context: confirm is pre-resolved from the tool's `confirm` argument.
|
|
5
|
+
* per-event logs are silently dropped — the command result's `summary` is
|
|
6
|
+
* the sole output channel on the mcp surface. */
|
|
7
|
+
export declare function makeMcpContext(confirmGranted: boolean): CommandContext;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
/** cli context: confirm prompts on stdin, log writes to stderr. */
|
|
3
|
+
export function makeCliContext() {
|
|
4
|
+
return {
|
|
5
|
+
env: process.env,
|
|
6
|
+
log(event) {
|
|
7
|
+
process.stderr.write(`[${event.level}] ${event.message}\n`);
|
|
8
|
+
},
|
|
9
|
+
confirm(prompt) {
|
|
10
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
// stdin EOF / SIGINT closes the interface — treat that as a "no".
|
|
13
|
+
rl.on('close', () => resolve(false));
|
|
14
|
+
rl.question(`${prompt} [y/N] `, answer => {
|
|
15
|
+
// resolve before close: the first resolve wins, so the close
|
|
16
|
+
// handler firing afterwards is a harmless no-op.
|
|
17
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
18
|
+
rl.close();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** mcp context: confirm is pre-resolved from the tool's `confirm` argument.
|
|
25
|
+
* per-event logs are silently dropped — the command result's `summary` is
|
|
26
|
+
* the sole output channel on the mcp surface. */
|
|
27
|
+
export function makeMcpContext(confirmGranted) {
|
|
28
|
+
return {
|
|
29
|
+
env: process.env,
|
|
30
|
+
log() {
|
|
31
|
+
// mcp surfaces collect output via the result; per-event logs are dropped.
|
|
32
|
+
},
|
|
33
|
+
async confirm() {
|
|
34
|
+
return confirmGranted;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** registers every CommandDef. idempotent — safe to call from each surface. */
|
|
2
|
+
export declare function loadRegistry(): void;
|
|
3
|
+
/** test-only: resets the loaded flag and clears the registry, so a test can
|
|
4
|
+
* re-run loadRegistry from a clean state. */
|
|
5
|
+
export declare function _resetLoader(): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { register, _resetRegistry } from './registry.js';
|
|
2
|
+
import { addCommand } from '../commands/add.js';
|
|
3
|
+
import { listCommand } from '../commands/list.js';
|
|
4
|
+
import { statusCommand } from '../commands/status.js';
|
|
5
|
+
import { startCommand } from '../commands/start.js';
|
|
6
|
+
import { stopCommand } from '../commands/stop.js';
|
|
7
|
+
import { restartCommand } from '../commands/restart.js';
|
|
8
|
+
import { healthCommand } from '../commands/health.js';
|
|
9
|
+
import { freezeCommand, unfreezeCommand } from '../commands/freeze.js';
|
|
10
|
+
import { rollbackCommand } from '../commands/rollback.js';
|
|
11
|
+
import { removeCommand } from '../commands/remove.js';
|
|
12
|
+
import { initCommand } from '../commands/init.js';
|
|
13
|
+
import { patchSystemdCommand } from '../commands/patch-systemd.js';
|
|
14
|
+
import { bootStartCommand } from '../commands/boot-start.js';
|
|
15
|
+
import { installMcpCommand } from '../commands/install-mcp.js';
|
|
16
|
+
import { updateCommand } from '../commands/update.js';
|
|
17
|
+
import { doctorCommand } from '../commands/doctor.js';
|
|
18
|
+
import { configCommand, whoamiCommand } from '../commands/config.js';
|
|
19
|
+
import { completionsCommand } from '../commands/completions.js';
|
|
20
|
+
/** every command definition. commands are added here as they are migrated
|
|
21
|
+
* onto the registry. */
|
|
22
|
+
const ALL = [
|
|
23
|
+
addCommand, statusCommand, listCommand, startCommand, stopCommand, restartCommand,
|
|
24
|
+
healthCommand, freezeCommand, unfreezeCommand, rollbackCommand, removeCommand,
|
|
25
|
+
initCommand, patchSystemdCommand, bootStartCommand, installMcpCommand,
|
|
26
|
+
updateCommand, doctorCommand, configCommand, whoamiCommand, completionsCommand,
|
|
27
|
+
];
|
|
28
|
+
let loaded = false;
|
|
29
|
+
/** registers every CommandDef. idempotent — safe to call from each surface. */
|
|
30
|
+
export function loadRegistry() {
|
|
31
|
+
if (loaded)
|
|
32
|
+
return;
|
|
33
|
+
_resetRegistry();
|
|
34
|
+
for (const def of ALL) {
|
|
35
|
+
register(def);
|
|
36
|
+
}
|
|
37
|
+
loaded = true;
|
|
38
|
+
}
|
|
39
|
+
/** test-only: resets the loaded flag and clears the registry, so a test can
|
|
40
|
+
* re-run loadRegistry from a clean state. */
|
|
41
|
+
export function _resetLoader() {
|
|
42
|
+
loaded = false;
|
|
43
|
+
_resetRegistry();
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export type ParseResult = {
|
|
3
|
+
help: true;
|
|
4
|
+
} | {
|
|
5
|
+
help: false;
|
|
6
|
+
ok: true;
|
|
7
|
+
values: Record<string, unknown>;
|
|
8
|
+
} | {
|
|
9
|
+
help: false;
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function parseArgs(schema: z.ZodObject<z.ZodRawShape>, argv: string[]): ParseResult;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** single-dash short flags, mapped to the long-form (schema) field they set.
|
|
3
|
+
* short flags only ever set a boolean field true. */
|
|
4
|
+
const SHORT_FLAGS = { y: 'yes' };
|
|
5
|
+
/** true when a schema field (unwrapping optional/default) is a boolean. */
|
|
6
|
+
function isBooleanField(schema) {
|
|
7
|
+
let s = schema;
|
|
8
|
+
while (s instanceof z.ZodOptional || s instanceof z.ZodDefault) {
|
|
9
|
+
s = s._def.innerType;
|
|
10
|
+
}
|
|
11
|
+
return s instanceof z.ZodBoolean;
|
|
12
|
+
}
|
|
13
|
+
export function parseArgs(schema, argv) {
|
|
14
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
15
|
+
return { help: true };
|
|
16
|
+
}
|
|
17
|
+
const shape = schema.shape;
|
|
18
|
+
const fieldNames = Object.keys(shape);
|
|
19
|
+
const booleanFields = new Set(fieldNames.filter(n => isBooleanField(shape[n])));
|
|
20
|
+
const values = {};
|
|
21
|
+
const positionals = [];
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const token = argv[i];
|
|
24
|
+
if (token.startsWith('--')) {
|
|
25
|
+
const body = token.slice(2);
|
|
26
|
+
const eq = body.indexOf('=');
|
|
27
|
+
if (eq !== -1) {
|
|
28
|
+
// --key=value form
|
|
29
|
+
values[body.slice(0, eq)] = body.slice(eq + 1);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (booleanFields.has(body)) {
|
|
33
|
+
values[body] = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const next = argv[i + 1];
|
|
37
|
+
if (next === undefined || next.startsWith('--')) {
|
|
38
|
+
return { help: false, ok: false, error: `flag --${body} requires a value` };
|
|
39
|
+
}
|
|
40
|
+
values[body] = next;
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
else if (/^-[A-Za-z]$/.test(token)) {
|
|
44
|
+
// single-dash short flag (e.g. -y) — resolve via the alias table
|
|
45
|
+
const long = SHORT_FLAGS[token.slice(1)];
|
|
46
|
+
if (long === undefined || !(long in shape)) {
|
|
47
|
+
return { help: false, ok: false, error: `unknown flag: ${token}` };
|
|
48
|
+
}
|
|
49
|
+
values[long] = true;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
positionals.push(token);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// reject flags that aren't in the schema — a mistyped flag must not be dropped silently
|
|
56
|
+
const unknownFlags = Object.keys(values).filter(k => !(k in shape));
|
|
57
|
+
if (unknownFlags.length > 0) {
|
|
58
|
+
return { help: false, ok: false, error: `unknown flag(s): ${unknownFlags.join(', ')}` };
|
|
59
|
+
}
|
|
60
|
+
// assign leftover positionals to non-boolean fields in declaration order
|
|
61
|
+
const positionalFields = fieldNames.filter(n => !booleanFields.has(n) && !(n in values));
|
|
62
|
+
for (let i = 0; i < positionals.length && i < positionalFields.length; i++) {
|
|
63
|
+
values[positionalFields[i]] = positionals[i];
|
|
64
|
+
}
|
|
65
|
+
const parsed = schema.safeParse(values);
|
|
66
|
+
if (!parsed.success) {
|
|
67
|
+
return {
|
|
68
|
+
help: false,
|
|
69
|
+
ok: false,
|
|
70
|
+
error: parsed.error.issues.map(iss => `${iss.path.join('.')}: ${iss.message}`).join('; '),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { help: false, ok: true, values: parsed.data };
|
|
74
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { CommandContext, CommandDef, CommandResult } from './types.js';
|
|
3
|
+
export declare function register(def: CommandDef): void;
|
|
4
|
+
/**
|
|
5
|
+
* defines a command with full argument-type inference: the `args` schema and
|
|
6
|
+
* the `run` handler's first parameter are tied through the shape generic `S`.
|
|
7
|
+
* returns the erased `CommandDef` the registry stores.
|
|
8
|
+
*/
|
|
9
|
+
export declare function defineCommand<S extends z.ZodRawShape, D>(def: {
|
|
10
|
+
name: string;
|
|
11
|
+
summary: string;
|
|
12
|
+
args: z.ZodObject<S>;
|
|
13
|
+
destructive?: boolean;
|
|
14
|
+
cliOnly?: boolean;
|
|
15
|
+
tui?: 'palette' | {
|
|
16
|
+
view: string;
|
|
17
|
+
};
|
|
18
|
+
run(args: z.infer<z.ZodObject<S>>, ctx: CommandContext): Promise<CommandResult<D>>;
|
|
19
|
+
}): CommandDef<D>;
|
|
20
|
+
export declare function getCommand(name: string): CommandDef | undefined;
|
|
21
|
+
export declare function allCommands(): CommandDef[];
|
|
22
|
+
/** test-only: clears the registry between tests. prefer _resetLoader() from
|
|
23
|
+
* registry/index.ts if you also need to reset the loadRegistry guard. */
|
|
24
|
+
export declare function _resetRegistry(): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const registry = new Map();
|
|
2
|
+
export function register(def) {
|
|
3
|
+
if (registry.has(def.name)) {
|
|
4
|
+
throw new Error(`duplicate command registration: ${def.name}`);
|
|
5
|
+
}
|
|
6
|
+
registry.set(def.name, def);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* defines a command with full argument-type inference: the `args` schema and
|
|
10
|
+
* the `run` handler's first parameter are tied through the shape generic `S`.
|
|
11
|
+
* returns the erased `CommandDef` the registry stores.
|
|
12
|
+
*/
|
|
13
|
+
export function defineCommand(def) {
|
|
14
|
+
return def;
|
|
15
|
+
}
|
|
16
|
+
export function getCommand(name) {
|
|
17
|
+
return registry.get(name);
|
|
18
|
+
}
|
|
19
|
+
export function allCommands() {
|
|
20
|
+
return [...registry.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
21
|
+
}
|
|
22
|
+
/** test-only: clears the registry between tests. prefer _resetLoader() from
|
|
23
|
+
* registry/index.ts if you also need to reset the loadRegistry guard. */
|
|
24
|
+
export function _resetRegistry() {
|
|
25
|
+
registry.clear();
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** turns a RenderModel into a plain-text block for cli output. */
|
|
2
|
+
export function renderToText(model) {
|
|
3
|
+
switch (model.kind) {
|
|
4
|
+
case 'lines':
|
|
5
|
+
return model.lines.join('\n');
|
|
6
|
+
case 'keyValue': {
|
|
7
|
+
const width = Math.max(0, ...model.pairs.map(([k]) => k.length));
|
|
8
|
+
return model.pairs.map(([k, v]) => `${k.padEnd(width + 2)}${v}`).join('\n');
|
|
9
|
+
}
|
|
10
|
+
case 'table': {
|
|
11
|
+
const all = [model.columns, ...model.rows];
|
|
12
|
+
const widths = model.columns.map((_, i) => Math.max(...all.map(row => (row[i] ?? '').length)));
|
|
13
|
+
const fmt = (row) => row
|
|
14
|
+
.slice(0, model.columns.length)
|
|
15
|
+
.map((cell, i) => (i === model.columns.length - 1 ? cell : cell.padEnd(widths[i] + 2)))
|
|
16
|
+
.join('');
|
|
17
|
+
return all.map(fmt).join('\n');
|
|
18
|
+
}
|
|
19
|
+
case 'tree':
|
|
20
|
+
return treeLines(model.root, 0).join('\n');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function treeLines(node, depth) {
|
|
24
|
+
const out = [' '.repeat(depth) + node.label];
|
|
25
|
+
for (const child of node.children ?? []) {
|
|
26
|
+
out.push(...treeLines(child, depth + 1));
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
/** a renderable model each surface turns into text (cli) or components (tui). */
|
|
3
|
+
export type RenderModel = {
|
|
4
|
+
kind: 'lines';
|
|
5
|
+
lines: string[];
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'keyValue';
|
|
8
|
+
pairs: Array<[string, string]>;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'table';
|
|
11
|
+
columns: string[];
|
|
12
|
+
rows: string[][];
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'tree';
|
|
15
|
+
root: TreeNode;
|
|
16
|
+
};
|
|
17
|
+
export interface TreeNode {
|
|
18
|
+
label: string;
|
|
19
|
+
children?: TreeNode[];
|
|
20
|
+
}
|
|
21
|
+
/** the surface-agnostic result every command handler returns. */
|
|
22
|
+
export interface CommandResult<D = unknown> {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
summary: string;
|
|
25
|
+
data: D;
|
|
26
|
+
render?: RenderModel;
|
|
27
|
+
}
|
|
28
|
+
/** surface-neutral services passed to every handler. */
|
|
29
|
+
export interface CommandContext {
|
|
30
|
+
confirm(prompt: string): Promise<boolean>;
|
|
31
|
+
log(event: {
|
|
32
|
+
level: 'info' | 'warn' | 'error';
|
|
33
|
+
message: string;
|
|
34
|
+
}): void;
|
|
35
|
+
env: NodeJS.ProcessEnv;
|
|
36
|
+
}
|
|
37
|
+
/** a command defined once; all three surfaces are derived from this. the
|
|
38
|
+
* registry stores the erased form — `run` receives the already-parsed args
|
|
39
|
+
* as a plain record. use `defineCommand` for inference at definition sites. */
|
|
40
|
+
export interface CommandDef<D = unknown> {
|
|
41
|
+
name: string;
|
|
42
|
+
summary: string;
|
|
43
|
+
args: z.ZodObject<z.ZodRawShape>;
|
|
44
|
+
destructive?: boolean;
|
|
45
|
+
cliOnly?: boolean;
|
|
46
|
+
tui?: 'palette' | {
|
|
47
|
+
view: string;
|
|
48
|
+
};
|
|
49
|
+
run(args: Record<string, unknown>, ctx: CommandContext): Promise<CommandResult<D>>;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
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 declare function generateAgentUnit(vaultPath: string): string;
|