@matthesketh/fleet 1.0.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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/data/registry.example.json +13 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +113 -0
  6. package/dist/commands/add.d.ts +1 -0
  7. package/dist/commands/add.js +95 -0
  8. package/dist/commands/deploy.d.ts +1 -0
  9. package/dist/commands/deploy.js +53 -0
  10. package/dist/commands/git.d.ts +1 -0
  11. package/dist/commands/git.js +278 -0
  12. package/dist/commands/health.d.ts +1 -0
  13. package/dist/commands/health.js +60 -0
  14. package/dist/commands/init.d.ts +1 -0
  15. package/dist/commands/init.js +157 -0
  16. package/dist/commands/install-mcp.d.ts +1 -0
  17. package/dist/commands/install-mcp.js +55 -0
  18. package/dist/commands/list.d.ts +1 -0
  19. package/dist/commands/list.js +20 -0
  20. package/dist/commands/logs.d.ts +1 -0
  21. package/dist/commands/logs.js +32 -0
  22. package/dist/commands/nginx.d.ts +1 -0
  23. package/dist/commands/nginx.js +94 -0
  24. package/dist/commands/remove.d.ts +1 -0
  25. package/dist/commands/remove.js +28 -0
  26. package/dist/commands/restart.d.ts +1 -0
  27. package/dist/commands/restart.js +22 -0
  28. package/dist/commands/secrets.d.ts +1 -0
  29. package/dist/commands/secrets.js +268 -0
  30. package/dist/commands/start.d.ts +1 -0
  31. package/dist/commands/start.js +22 -0
  32. package/dist/commands/status.d.ts +14 -0
  33. package/dist/commands/status.js +70 -0
  34. package/dist/commands/stop.d.ts +1 -0
  35. package/dist/commands/stop.js +22 -0
  36. package/dist/commands/watchdog.d.ts +1 -0
  37. package/dist/commands/watchdog.js +100 -0
  38. package/dist/core/docker.d.ts +15 -0
  39. package/dist/core/docker.js +72 -0
  40. package/dist/core/errors.d.ts +20 -0
  41. package/dist/core/errors.js +40 -0
  42. package/dist/core/exec.d.ts +14 -0
  43. package/dist/core/exec.js +30 -0
  44. package/dist/core/git-onboard.d.ts +11 -0
  45. package/dist/core/git-onboard.js +149 -0
  46. package/dist/core/git.d.ts +36 -0
  47. package/dist/core/git.js +155 -0
  48. package/dist/core/github.d.ts +22 -0
  49. package/dist/core/github.js +92 -0
  50. package/dist/core/health.d.ts +29 -0
  51. package/dist/core/health.js +56 -0
  52. package/dist/core/nginx.d.ts +17 -0
  53. package/dist/core/nginx.js +59 -0
  54. package/dist/core/registry.d.ts +38 -0
  55. package/dist/core/registry.js +47 -0
  56. package/dist/core/secrets-ops.d.ts +37 -0
  57. package/dist/core/secrets-ops.js +331 -0
  58. package/dist/core/secrets-validate.d.ts +8 -0
  59. package/dist/core/secrets-validate.js +81 -0
  60. package/dist/core/secrets.d.ts +36 -0
  61. package/dist/core/secrets.js +191 -0
  62. package/dist/core/systemd.d.ts +23 -0
  63. package/dist/core/systemd.js +106 -0
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.js +18 -0
  66. package/dist/mcp/git-tools.d.ts +2 -0
  67. package/dist/mcp/git-tools.js +148 -0
  68. package/dist/mcp/secrets-tools.d.ts +2 -0
  69. package/dist/mcp/secrets-tools.js +67 -0
  70. package/dist/mcp/server.d.ts +1 -0
  71. package/dist/mcp/server.js +179 -0
  72. package/dist/templates/gitignore.d.ts +3 -0
  73. package/dist/templates/gitignore.js +89 -0
  74. package/dist/templates/nginx.d.ts +8 -0
  75. package/dist/templates/nginx.js +111 -0
  76. package/dist/templates/systemd.d.ts +9 -0
  77. package/dist/templates/systemd.js +26 -0
  78. package/dist/templates/unseal.d.ts +1 -0
  79. package/dist/templates/unseal.js +22 -0
  80. package/dist/tui/app.d.ts +1 -0
  81. package/dist/tui/app.js +9 -0
  82. package/dist/tui/components/AppList.d.ts +12 -0
  83. package/dist/tui/components/AppList.js +32 -0
  84. package/dist/tui/components/Confirm.d.ts +2 -0
  85. package/dist/tui/components/Confirm.js +10 -0
  86. package/dist/tui/components/Header.d.ts +6 -0
  87. package/dist/tui/components/Header.js +16 -0
  88. package/dist/tui/components/KeyHint.d.ts +2 -0
  89. package/dist/tui/components/KeyHint.js +55 -0
  90. package/dist/tui/components/StatusBadge.d.ts +7 -0
  91. package/dist/tui/components/StatusBadge.js +8 -0
  92. package/dist/tui/exec-bridge.d.ts +11 -0
  93. package/dist/tui/exec-bridge.js +57 -0
  94. package/dist/tui/hooks/use-fleet-data.d.ts +9 -0
  95. package/dist/tui/hooks/use-fleet-data.js +30 -0
  96. package/dist/tui/hooks/use-health.d.ts +9 -0
  97. package/dist/tui/hooks/use-health.js +29 -0
  98. package/dist/tui/hooks/use-interval.d.ts +1 -0
  99. package/dist/tui/hooks/use-interval.js +13 -0
  100. package/dist/tui/hooks/use-keyboard.d.ts +1 -0
  101. package/dist/tui/hooks/use-keyboard.js +44 -0
  102. package/dist/tui/hooks/use-secrets.d.ts +47 -0
  103. package/dist/tui/hooks/use-secrets.js +152 -0
  104. package/dist/tui/router.d.ts +2 -0
  105. package/dist/tui/router.js +65 -0
  106. package/dist/tui/state.d.ts +12 -0
  107. package/dist/tui/state.js +83 -0
  108. package/dist/tui/theme.d.ts +11 -0
  109. package/dist/tui/theme.js +23 -0
  110. package/dist/tui/types.d.ts +41 -0
  111. package/dist/tui/types.js +1 -0
  112. package/dist/tui/views/AppDetail.d.ts +2 -0
  113. package/dist/tui/views/AppDetail.js +72 -0
  114. package/dist/tui/views/Dashboard.d.ts +2 -0
  115. package/dist/tui/views/Dashboard.js +29 -0
  116. package/dist/tui/views/HealthView.d.ts +2 -0
  117. package/dist/tui/views/HealthView.js +28 -0
  118. package/dist/tui/views/LogsView.d.ts +2 -0
  119. package/dist/tui/views/LogsView.js +71 -0
  120. package/dist/tui/views/SecretEdit.d.ts +2 -0
  121. package/dist/tui/views/SecretEdit.js +53 -0
  122. package/dist/tui/views/SecretsView.d.ts +2 -0
  123. package/dist/tui/views/SecretsView.js +108 -0
  124. package/dist/ui/confirm.d.ts +1 -0
  125. package/dist/ui/confirm.js +15 -0
  126. package/dist/ui/output.d.ts +27 -0
  127. package/dist/ui/output.js +61 -0
  128. package/package.json +64 -0
@@ -0,0 +1,179 @@
1
+ import { z } from 'zod';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { getStatusData } from '../commands/status.js';
5
+ import { existsSync } from 'node:fs';
6
+ import { load, findApp, save, addApp } from '../core/registry.js';
7
+ import { startService, stopService, restartService } from '../core/systemd.js';
8
+ import { getContainerLogs, getContainersByCompose } from '../core/docker.js';
9
+ import { checkHealth, checkAllHealth } from '../core/health.js';
10
+ import { listSites, installConfig, testConfig, reload, removeConfig } from '../core/nginx.js';
11
+ import { generateNginxConfig } from '../templates/nginx.js';
12
+ import { composeBuild } from '../core/docker.js';
13
+ import { AppNotFoundError } from '../core/errors.js';
14
+ import { loadManifest, listSecrets, isInitialized } from '../core/secrets.js';
15
+ import { unsealAll, getStatus as getSecretsStatus } from '../core/secrets-ops.js';
16
+ import { validateApp, validateAll } from '../core/secrets-validate.js';
17
+ import { registerGitTools } from './git-tools.js';
18
+ import { registerSecretsTools } from './secrets-tools.js';
19
+ function requireApp(name) {
20
+ const reg = load();
21
+ const app = findApp(reg, name);
22
+ if (!app)
23
+ throw new AppNotFoundError(name);
24
+ return app;
25
+ }
26
+ function text(msg) {
27
+ return { content: [{ type: 'text', text: msg }] };
28
+ }
29
+ export async function startMcpServer() {
30
+ const server = new McpServer({
31
+ name: 'fleet',
32
+ version: '1.0.0',
33
+ });
34
+ server.tool('fleet_status', 'Dashboard data for all apps: systemd state, containers, health', async () => {
35
+ const data = getStatusData();
36
+ return text(JSON.stringify(data, null, 2));
37
+ });
38
+ server.tool('fleet_list', 'List all registered apps with their configuration', async () => {
39
+ const reg = load();
40
+ return text(JSON.stringify(reg.apps, null, 2));
41
+ });
42
+ server.tool('fleet_start', 'Start an app via systemctl', { app: z.string().describe('App name') }, async ({ app }) => {
43
+ const entry = requireApp(app);
44
+ const ok = startService(entry.serviceName);
45
+ return text(ok ? `Started ${entry.name}` : `Failed to start ${entry.name}`);
46
+ });
47
+ server.tool('fleet_stop', 'Stop an app via systemctl', { app: z.string().describe('App name') }, async ({ app }) => {
48
+ const entry = requireApp(app);
49
+ const ok = stopService(entry.serviceName);
50
+ return text(ok ? `Stopped ${entry.name}` : `Failed to stop ${entry.name}`);
51
+ });
52
+ server.tool('fleet_restart', 'Restart an app via systemctl', { app: z.string().describe('App name') }, async ({ app }) => {
53
+ const entry = requireApp(app);
54
+ const ok = restartService(entry.serviceName);
55
+ return text(ok ? `Restarted ${entry.name}` : `Failed to restart ${entry.name}`);
56
+ });
57
+ server.tool('fleet_logs', 'Get recent container logs for an app', {
58
+ app: z.string().describe('App name'),
59
+ lines: z.number().optional().default(100).describe('Number of log lines'),
60
+ }, async ({ app, lines }) => {
61
+ const entry = requireApp(app);
62
+ const container = entry.containers[0];
63
+ if (!container)
64
+ return text('No containers registered');
65
+ const logs = getContainerLogs(container, lines);
66
+ return text(logs);
67
+ });
68
+ 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 }) => {
69
+ const reg = load();
70
+ if (app) {
71
+ const entry = findApp(reg, app);
72
+ if (!entry)
73
+ throw new AppNotFoundError(app);
74
+ const result = checkHealth(entry);
75
+ return text(JSON.stringify(result, null, 2));
76
+ }
77
+ const results = checkAllHealth(reg.apps);
78
+ return text(JSON.stringify(results, null, 2));
79
+ });
80
+ server.tool('fleet_deploy', 'Deploy an app: build and restart', { app: z.string().describe('App name') }, async ({ app }) => {
81
+ const entry = requireApp(app);
82
+ const buildOk = composeBuild(entry.composePath, entry.composeFile, entry.name);
83
+ if (!buildOk)
84
+ return text(`Build failed for ${entry.name}`);
85
+ const ok = restartService(entry.serviceName);
86
+ return text(ok ? `Deployed ${entry.name}` : `Deploy failed for ${entry.name}`);
87
+ });
88
+ server.tool('fleet_nginx_add', 'Create an nginx config for a domain', {
89
+ domain: z.string().describe('Domain name'),
90
+ port: z.number().describe('Backend port'),
91
+ type: z.enum(['proxy', 'spa', 'nextjs']).optional().default('proxy').describe('Config type'),
92
+ }, async ({ domain, port, type }) => {
93
+ const config = generateNginxConfig({ domain, port, type });
94
+ installConfig(domain, config);
95
+ const test = testConfig();
96
+ if (!test.ok) {
97
+ removeConfig(domain);
98
+ return text(`Config test failed: ${test.output}`);
99
+ }
100
+ reload();
101
+ return text(`Created and activated nginx config for ${domain}`);
102
+ });
103
+ server.tool('fleet_nginx_list', 'List all nginx site configs', async () => {
104
+ const sites = listSites();
105
+ return text(JSON.stringify(sites, null, 2));
106
+ });
107
+ server.tool('fleet_secrets_status', 'Show vault initialisation state, sealed/unsealed, counts. The vault is the encrypted source of truth that survives reboots. Runtime (/run/fleet-secrets/) is the decrypted copy used by apps — it is lost on reboot.', async () => {
108
+ const status = getSecretsStatus();
109
+ return text(JSON.stringify(status, null, 2));
110
+ });
111
+ server.tool('fleet_secrets_list', 'List managed secrets for an app (masked values). Shows vault contents — use fleet_secrets_drift to check if runtime differs.', { app: z.string().optional().describe('App name (omit for all apps)') }, async ({ app }) => {
112
+ if (!isInitialized())
113
+ return text('Vault not initialised');
114
+ if (app) {
115
+ const secrets = listSecrets(app);
116
+ return text(JSON.stringify(secrets, null, 2));
117
+ }
118
+ const manifest = loadManifest();
119
+ return text(JSON.stringify(manifest.apps, null, 2));
120
+ });
121
+ server.tool('fleet_secrets_unseal', 'Decrypt vault to /run/fleet-secrets/. WARNING: This overwrites any runtime changes that were not sealed back to the vault. Use fleet_secrets_drift first to check for unsaved changes.', async () => {
122
+ if (!isInitialized())
123
+ return text('Vault not initialised');
124
+ unsealAll();
125
+ return text('Unsealed all secrets to /run/fleet-secrets/');
126
+ });
127
+ server.tool('fleet_secrets_validate', 'Validate compose secrets match vault. Returns missing/extra secrets per app. This checks that docker-compose secret references have matching entries in the vault.', { app: z.string().optional().describe('App name (omit for all apps)') }, async ({ app }) => {
128
+ if (!isInitialized())
129
+ return text('Vault not initialised');
130
+ const results = app ? [validateApp(app)] : validateAll();
131
+ return text(JSON.stringify(results, null, 2));
132
+ });
133
+ server.tool('fleet_register', 'Register a new app in the fleet registry', {
134
+ name: z.string().describe('App name (kebab-case identifier)'),
135
+ composePath: z.string().describe('Absolute path to docker-compose directory'),
136
+ displayName: z.string().optional().describe('Human-friendly name'),
137
+ composeFile: z.string().optional().describe('Custom compose filename'),
138
+ serviceName: z.string().optional().describe('Systemd service name'),
139
+ domains: z.array(z.string()).optional().default([]).describe('Domain names'),
140
+ port: z.number().optional().describe('Backend port'),
141
+ type: z.enum(['proxy', 'spa', 'nextjs', 'service']).optional().default('service').describe('App type'),
142
+ containers: z.array(z.string()).optional().describe('Container names (auto-detected if omitted)'),
143
+ usesSharedDb: z.boolean().optional().default(false).describe('Uses shared database'),
144
+ dependsOnDatabases: z.boolean().optional().default(false).describe('Depends on docker-databases'),
145
+ }, async (params) => {
146
+ if (!existsSync(params.composePath)) {
147
+ return text(`Error: composePath does not exist: ${params.composePath}`);
148
+ }
149
+ const reg = load();
150
+ const existing = findApp(reg, params.name);
151
+ let containers = params.containers;
152
+ if (!containers || containers.length === 0) {
153
+ containers = getContainersByCompose(params.composePath, params.composeFile ?? null);
154
+ if (containers.length === 0)
155
+ containers = [params.name];
156
+ }
157
+ const entry = {
158
+ name: params.name,
159
+ displayName: params.displayName ?? params.name,
160
+ composePath: params.composePath,
161
+ composeFile: params.composeFile ?? null,
162
+ serviceName: params.serviceName ?? params.name,
163
+ domains: params.domains,
164
+ port: params.port ?? null,
165
+ type: params.type,
166
+ containers,
167
+ usesSharedDb: params.usesSharedDb,
168
+ dependsOnDatabases: params.dependsOnDatabases,
169
+ registeredAt: new Date().toISOString(),
170
+ };
171
+ save(addApp(reg, entry));
172
+ const action = existing ? 'Updated' : 'Registered';
173
+ return text(`${action} app "${params.name}":\n${JSON.stringify(entry, null, 2)}`);
174
+ });
175
+ registerGitTools(server);
176
+ registerSecretsTools(server);
177
+ const transport = new StdioServerTransport();
178
+ await server.connect(transport);
179
+ }
@@ -0,0 +1,3 @@
1
+ export type ProjectType = 'node' | 'nextjs' | 'go' | 'php' | 'generic';
2
+ export declare function detectProjectType(dir: string): ProjectType;
3
+ export declare function generateGitignore(type: ProjectType): string;
@@ -0,0 +1,89 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export function detectProjectType(dir) {
4
+ if (existsSync(join(dir, 'next.config.js')) || existsSync(join(dir, 'next.config.mjs')) || existsSync(join(dir, 'next.config.ts'))) {
5
+ return 'nextjs';
6
+ }
7
+ if (existsSync(join(dir, 'package.json')))
8
+ return 'node';
9
+ if (existsSync(join(dir, 'go.mod')))
10
+ return 'go';
11
+ if (existsSync(join(dir, 'composer.json')))
12
+ return 'php';
13
+ return 'generic';
14
+ }
15
+ const COMMON = `# ===== SECRETS - NEVER COMMIT =====
16
+ .env
17
+ .env.*
18
+ !.env.example
19
+ *.pem
20
+ *.key
21
+
22
+ # OS / editors
23
+ .DS_Store
24
+ Thumbs.db
25
+ .vscode/
26
+ .idea/
27
+ *.swp
28
+ *.swo
29
+ *~
30
+
31
+ # Logs
32
+ *.log
33
+ npm-debug.log*
34
+ yarn-debug.log*
35
+ yarn-error.log*
36
+
37
+ # Docker overrides
38
+ docker-compose.override.yml
39
+ `;
40
+ const NODE = `# Node
41
+ node_modules/
42
+ dist/
43
+ build/
44
+ coverage/
45
+ .npm
46
+ `;
47
+ const NEXTJS = `# Node
48
+ node_modules/
49
+ dist/
50
+ build/
51
+ coverage/
52
+ .npm
53
+
54
+ # Next.js
55
+ .next/
56
+ out/
57
+ *.tsbuildinfo
58
+ `;
59
+ const GO = `# Go
60
+ bin/
61
+ vendor/
62
+ *.exe
63
+ `;
64
+ const PHP = `# PHP
65
+ /vendor/
66
+ composer.phar
67
+ `;
68
+ const FOOTER = `
69
+ # ===== REMINDER: check for secrets before committing =====
70
+ `;
71
+ export function generateGitignore(type) {
72
+ let content = COMMON;
73
+ switch (type) {
74
+ case 'nextjs':
75
+ content += NEXTJS;
76
+ break;
77
+ case 'node':
78
+ content += NODE;
79
+ break;
80
+ case 'go':
81
+ content += GO;
82
+ break;
83
+ case 'php':
84
+ content += PHP;
85
+ break;
86
+ case 'generic': break;
87
+ }
88
+ return content + FOOTER;
89
+ }
@@ -0,0 +1,8 @@
1
+ interface NginxOpts {
2
+ domain: string;
3
+ port: number;
4
+ type: 'proxy' | 'spa' | 'nextjs';
5
+ apiPrefix?: string;
6
+ }
7
+ export declare function generateNginxConfig(opts: NginxOpts): string;
8
+ export {};
@@ -0,0 +1,111 @@
1
+ export function generateNginxConfig(opts) {
2
+ const { domain, port, type } = opts;
3
+ const apiPrefix = opts.apiPrefix ?? '/api';
4
+ const securityHeaders = ` add_header X-Content-Type-Options "nosniff" always;
5
+ add_header X-Frame-Options "SAMEORIGIN" always;
6
+ add_header X-XSS-Protection "1; mode=block" always;
7
+ add_header Referrer-Policy "no-referrer-when-downgrade" always;`;
8
+ const proxyHeaders = ` proxy_http_version 1.1;
9
+ proxy_set_header Upgrade $http_upgrade;
10
+ proxy_set_header Connection 'upgrade';
11
+ proxy_set_header Host $host;
12
+ proxy_set_header X-Real-IP $remote_addr;
13
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
14
+ proxy_set_header X-Forwarded-Proto $scheme;
15
+ proxy_cache_bypass $http_upgrade;`;
16
+ if (type === 'proxy') {
17
+ return proxyTemplate(domain, port, securityHeaders, proxyHeaders);
18
+ }
19
+ if (type === 'nextjs') {
20
+ return nextjsTemplate(domain, port, securityHeaders, proxyHeaders);
21
+ }
22
+ return spaTemplate(domain, port, apiPrefix, securityHeaders, proxyHeaders);
23
+ }
24
+ function proxyTemplate(domain, port, security, proxy) {
25
+ return `server {
26
+ server_name ${domain} www.${domain};
27
+
28
+ ${security}
29
+
30
+ location / {
31
+ proxy_pass http://127.0.0.1:${port};
32
+ ${proxy}
33
+ }
34
+
35
+ listen 80;
36
+ listen [::]:80;
37
+ }
38
+ `;
39
+ }
40
+ function spaTemplate(domain, port, apiPrefix, security, proxy) {
41
+ return `server {
42
+ server_name ${domain} www.${domain};
43
+
44
+ ${security}
45
+
46
+ # Gzip
47
+ gzip on;
48
+ gzip_vary on;
49
+ gzip_min_length 1024;
50
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
51
+
52
+ # API proxy
53
+ location ${apiPrefix}/ {
54
+ proxy_pass http://127.0.0.1:${port};
55
+ ${proxy}
56
+ proxy_read_timeout 60s;
57
+ proxy_connect_timeout 60s;
58
+ }
59
+
60
+ # Sitemap and robots
61
+ location = /sitemap.xml {
62
+ proxy_pass http://127.0.0.1:${port};
63
+ ${proxy}
64
+ }
65
+ location = /robots.txt {
66
+ proxy_pass http://127.0.0.1:${port};
67
+ ${proxy}
68
+ }
69
+
70
+ # Static assets
71
+ location ~* \\.(?:css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
72
+ expires 1y;
73
+ add_header Cache-Control "public, immutable";
74
+ try_files $uri =404;
75
+ }
76
+
77
+ # SPA fallback
78
+ location / {
79
+ try_files $uri $uri/ /index.html;
80
+ }
81
+
82
+ listen 80;
83
+ listen [::]:80;
84
+ }
85
+ `;
86
+ }
87
+ function nextjsTemplate(domain, port, security, proxy) {
88
+ return `server {
89
+ server_name ${domain} www.${domain};
90
+
91
+ ${security}
92
+
93
+ # Next.js static assets
94
+ location /_next/static/ {
95
+ proxy_pass http://127.0.0.1:${port};
96
+ ${proxy}
97
+ expires 1y;
98
+ add_header Cache-Control "public, immutable";
99
+ }
100
+
101
+ # All traffic to Next.js
102
+ location / {
103
+ proxy_pass http://127.0.0.1:${port};
104
+ ${proxy}
105
+ }
106
+
107
+ listen 80;
108
+ listen [::]:80;
109
+ }
110
+ `;
111
+ }
@@ -0,0 +1,9 @@
1
+ interface SystemdOpts {
2
+ serviceName: string;
3
+ description: string;
4
+ workingDirectory: string;
5
+ composeFile: string | null;
6
+ dependsOnDatabases: boolean;
7
+ }
8
+ export declare function generateServiceFile(opts: SystemdOpts): string;
9
+ export {};
@@ -0,0 +1,26 @@
1
+ export function generateServiceFile(opts) {
2
+ const fileFlag = opts.composeFile ? ` -f ${opts.composeFile}` : '';
3
+ const dbDep = opts.dependsOnDatabases ? ' docker-databases.service' : '';
4
+ return `[Unit]
5
+ Description=${opts.description}
6
+ Requires=docker.service${dbDep}
7
+ After=docker.service${dbDep} network-online.target
8
+ Wants=network-online.target
9
+
10
+ [Service]
11
+ Type=oneshot
12
+ RemainAfterExit=yes
13
+ WorkingDirectory=${opts.workingDirectory}
14
+ ExecStartPre=-/usr/bin/docker compose${fileFlag} down
15
+ ExecStart=/usr/bin/docker compose${fileFlag} up -d --force-recreate
16
+ ExecStop=/usr/bin/docker compose${fileFlag} down --timeout 30
17
+ ExecReload=/usr/bin/docker compose${fileFlag} restart
18
+ TimeoutStartSec=300
19
+ TimeoutStopSec=60
20
+ Restart=on-failure
21
+ RestartSec=10
22
+
23
+ [Install]
24
+ WantedBy=multi-user.target
25
+ `;
26
+ }
@@ -0,0 +1 @@
1
+ export declare function generateUnsealService(): string;
@@ -0,0 +1,22 @@
1
+ import { load } from '../core/registry.js';
2
+ export function generateUnsealService() {
3
+ const reg = load();
4
+ const serviceNames = reg.apps.map(a => a.serviceName + '.service');
5
+ const dbService = reg.infrastructure.databases.serviceName + '.service';
6
+ const allServices = [dbService, ...serviceNames].join(' ');
7
+ return `[Unit]
8
+ Description=Fleet Secrets Unseal
9
+ After=local-fs.target
10
+ Before=${allServices}
11
+
12
+ [Service]
13
+ Type=oneshot
14
+ RemainAfterExit=yes
15
+ ExecStart=/usr/bin/node /home/matt/fleet/dist/index.js secrets unseal
16
+ ExecStop=/bin/rm -rf /run/fleet-secrets
17
+ TimeoutStartSec=30
18
+
19
+ [Install]
20
+ WantedBy=multi-user.target
21
+ `;
22
+ }
@@ -0,0 +1 @@
1
+ export declare function launchTui(): void;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { App } from './router.js';
4
+ export function launchTui() {
5
+ const { waitUntilExit } = render(_jsx(App, {}));
6
+ waitUntilExit().then(() => {
7
+ process.exit(0);
8
+ });
9
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ interface AppListItem {
3
+ name: string;
4
+ label?: string;
5
+ }
6
+ interface AppListProps {
7
+ items: AppListItem[];
8
+ onSelect: (item: AppListItem) => void;
9
+ renderItem?: (item: AppListItem, selected: boolean) => React.JSX.Element;
10
+ }
11
+ export declare function AppList({ items, onSelect, renderItem }: AppListProps): React.JSX.Element;
12
+ export {};
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { colors } from '../theme.js';
5
+ export function AppList({ items, onSelect, renderItem }) {
6
+ const [selectedIndex, setSelectedIndex] = useState(0);
7
+ useInput((input, key) => {
8
+ if (items.length === 0)
9
+ return;
10
+ if (input === 'j' || key.downArrow) {
11
+ setSelectedIndex(prev => Math.min(prev + 1, items.length - 1));
12
+ }
13
+ else if (input === 'k' || key.upArrow) {
14
+ setSelectedIndex(prev => Math.max(prev - 1, 0));
15
+ }
16
+ else if (key.return) {
17
+ if (items[selectedIndex]) {
18
+ onSelect(items[selectedIndex]);
19
+ }
20
+ }
21
+ });
22
+ if (items.length === 0) {
23
+ return _jsx(Text, { color: colors.muted, children: "No items" });
24
+ }
25
+ return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => {
26
+ const selected = i === selectedIndex;
27
+ if (renderItem) {
28
+ return (_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, children: selected ? '> ' : ' ' }), renderItem(item, selected)] }, item.name));
29
+ }
30
+ return (_jsxs(Text, { bold: selected, color: selected ? colors.primary : colors.text, children: [selected ? '> ' : ' ', item.label ?? item.name] }, item.name));
31
+ }) }));
32
+ }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare function Confirm(): React.JSX.Element | null;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useAppState } from '../state.js';
4
+ import { colors } from '../theme.js';
5
+ export function Confirm() {
6
+ const { confirmAction } = useAppState();
7
+ if (!confirmAction)
8
+ return null;
9
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.warning, paddingX: 2, paddingY: 1, marginY: 1, children: [_jsx(Text, { bold: true, color: colors.warning, children: confirmAction.label }), _jsx(Text, { color: colors.muted, children: confirmAction.description }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: colors.success, children: "y" }), " confirm"] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, color: colors.error, children: "n" }), " cancel"] })] })] }));
10
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface HeaderProps {
3
+ vaultSealed: boolean;
4
+ }
5
+ export declare function Header({ vaultSealed }: HeaderProps): React.JSX.Element;
6
+ export {};
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useAppState } from '../state.js';
4
+ import { colors } from '../theme.js';
5
+ const TABS = [
6
+ { view: 'dashboard', label: 'Dashboard' },
7
+ { view: 'health', label: 'Health' },
8
+ { view: 'secrets', label: 'Secrets' },
9
+ ];
10
+ function VaultIndicator({ sealed }) {
11
+ return (_jsx(Text, { color: sealed ? colors.warning : colors.success, children: sealed ? '[SEALED]' : '[UNSEALED]' }));
12
+ }
13
+ export function Header({ vaultSealed }) {
14
+ const { currentView, redacted } = useAppState();
15
+ return (_jsxs(Box, { borderStyle: "single", borderBottom: true, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: colors.primary, children: "Fleet" }), _jsx(Text, { color: colors.muted, children: "|" }), TABS.map(tab => (_jsx(Text, { bold: currentView === tab.view || currentView === 'app-detail' && tab.view === 'dashboard' || currentView === 'secret-edit' && tab.view === 'secrets' || currentView === 'logs' && tab.view === 'dashboard', color: currentView === tab.view ? colors.primary : colors.muted, children: tab.label }, tab.view)))] }), _jsxs(Box, { gap: 1, children: [redacted && _jsx(Text, { color: "magenta", bold: true, children: "[REDACTED]" }), _jsx(VaultIndicator, { sealed: vaultSealed })] })] }));
16
+ }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare function KeyHint(): React.JSX.Element;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useAppState } from '../state.js';
4
+ import { colors } from '../theme.js';
5
+ const viewHints = {
6
+ dashboard: [
7
+ { key: 'j/k', label: 'navigate' },
8
+ { key: 'Enter', label: 'select' },
9
+ { key: 'Tab', label: 'switch view' },
10
+ { key: 'x', label: 'redact' },
11
+ { key: 'q', label: 'quit' },
12
+ ],
13
+ 'app-detail': [
14
+ { key: 'j/k', label: 'navigate' },
15
+ { key: 'Enter', label: 'run action' },
16
+ { key: 'x', label: 'redact' },
17
+ { key: 'Esc', label: 'back' },
18
+ { key: 'q', label: 'quit' },
19
+ ],
20
+ health: [
21
+ { key: 'j/k', label: 'navigate' },
22
+ { key: 'Tab', label: 'switch view' },
23
+ { key: 'x', label: 'redact' },
24
+ { key: 'q', label: 'quit' },
25
+ ],
26
+ secrets: [
27
+ { key: 'j/k', label: 'navigate' },
28
+ { key: 'Enter', label: 'select' },
29
+ { key: 'u', label: 'unseal' },
30
+ { key: 'l', label: 'seal' },
31
+ { key: 'a', label: 'add' },
32
+ { key: 'd', label: 'delete' },
33
+ { key: 'r', label: 'reveal' },
34
+ { key: 'x', label: 'redact' },
35
+ { key: 'Esc', label: 'back' },
36
+ { key: 'q', label: 'quit' },
37
+ ],
38
+ 'secret-edit': [
39
+ { key: 'Enter', label: 'save' },
40
+ { key: 'Esc', label: 'cancel' },
41
+ ],
42
+ logs: [
43
+ { key: 'f', label: 'follow' },
44
+ { key: 'x', label: 'redact' },
45
+ { key: 'Esc', label: 'back' },
46
+ { key: 'q', label: 'quit' },
47
+ ],
48
+ };
49
+ export function KeyHint() {
50
+ const { currentView, confirmAction } = useAppState();
51
+ const hints = confirmAction
52
+ ? [{ key: 'y', label: 'confirm' }, { key: 'n', label: 'cancel' }]
53
+ : viewHints[currentView] ?? [];
54
+ return (_jsx(Box, { borderStyle: "single", borderTop: true, paddingX: 1, gap: 2, children: hints.map(hint => (_jsxs(Box, { gap: 0, children: [_jsx(Text, { bold: true, color: colors.primary, children: hint.key }), _jsxs(Text, { color: colors.muted, children: [" ", hint.label] })] }, hint.key))) }));
55
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface StatusBadgeProps {
3
+ value: string;
4
+ type?: 'systemd' | 'health';
5
+ }
6
+ export declare function StatusBadge({ value, type }: StatusBadgeProps): React.JSX.Element;
7
+ export {};
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { statusColor, healthColor } from '../theme.js';
4
+ export function StatusBadge({ value, type = 'health' }) {
5
+ const colorMap = type === 'systemd' ? statusColor : healthColor;
6
+ const color = colorMap[value] ?? 'gray';
7
+ return _jsx(Text, { color: color, children: value });
8
+ }
@@ -0,0 +1,11 @@
1
+ export interface CommandResult {
2
+ ok: boolean;
3
+ output: string;
4
+ }
5
+ export declare function runFleetCommand(args: string[]): Promise<CommandResult>;
6
+ export declare function runFleetJson<T>(args: string[]): Promise<T | null>;
7
+ export interface StreamHandle {
8
+ kill: () => void;
9
+ onData: (cb: (line: string) => void) => void;
10
+ }
11
+ export declare function streamFleetCommand(args: string[]): StreamHandle;