@matthesketh/fleet 1.8.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -16
- package/dist/bin/fleet-agent.d.ts +2 -0
- package/dist/bin/fleet-agent.js +7 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +73 -31
- package/dist/commands/add.d.ts +2 -1
- package/dist/commands/add.js +66 -59
- package/dist/commands/audit.d.ts +1 -0
- package/dist/commands/audit.js +144 -0
- package/dist/commands/backup.d.ts +1 -0
- package/dist/commands/backup.js +510 -0
- package/dist/commands/boot-start.d.ts +3 -1
- package/dist/commands/boot-start.js +39 -47
- package/dist/commands/completions.d.ts +6 -0
- package/dist/commands/completions.js +83 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.js +96 -0
- package/dist/commands/deploy.js +3 -2
- package/dist/commands/deps.js +5 -1
- package/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.js +186 -0
- package/dist/commands/egress.d.ts +1 -1
- package/dist/commands/egress.js +13 -10
- package/dist/commands/freeze.d.ts +8 -4
- package/dist/commands/freeze.js +77 -59
- package/dist/commands/git.js +2 -2
- package/dist/commands/health.d.ts +2 -1
- package/dist/commands/health.js +38 -56
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +83 -73
- package/dist/commands/install-mcp.d.ts +3 -1
- package/dist/commands/install-mcp.js +53 -34
- package/dist/commands/list.d.ts +2 -1
- package/dist/commands/list.js +22 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/notify.d.ts +1 -0
- package/dist/commands/notify.js +51 -0
- package/dist/commands/patch-systemd.d.ts +7 -1
- package/dist/commands/patch-systemd.js +71 -31
- package/dist/commands/remove.d.ts +3 -1
- package/dist/commands/remove.js +37 -26
- package/dist/commands/restart.d.ts +4 -1
- package/dist/commands/restart.js +17 -20
- package/dist/commands/rollback.d.ts +4 -1
- package/dist/commands/rollback.js +33 -42
- package/dist/commands/secrets.js +157 -9
- package/dist/commands/start.d.ts +4 -1
- package/dist/commands/start.js +17 -20
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +21 -26
- package/dist/commands/stop.d.ts +4 -1
- package/dist/commands/stop.js +17 -20
- package/dist/commands/testflight.d.ts +1 -0
- package/dist/commands/testflight.js +193 -0
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.js +95 -0
- package/dist/core/audit/cache.d.ts +4 -0
- package/dist/core/audit/cache.js +37 -0
- package/dist/core/audit/config.d.ts +5 -0
- package/dist/core/audit/config.js +35 -0
- package/dist/core/audit/greenlight.d.ts +11 -0
- package/dist/core/audit/greenlight.js +81 -0
- package/dist/core/audit/reporters/cli.d.ts +3 -0
- package/dist/core/audit/reporters/cli.js +68 -0
- package/dist/core/audit/suppress.d.ts +6 -0
- package/dist/core/audit/suppress.js +37 -0
- package/dist/core/audit/target.d.ts +5 -0
- package/dist/core/audit/target.js +26 -0
- package/dist/core/audit/types.d.ts +54 -0
- package/dist/core/audit/types.js +5 -0
- package/dist/core/backup/browser-api.d.ts +66 -0
- package/dist/core/backup/browser-api.js +197 -0
- package/dist/core/backup/browser-server.d.ts +11 -0
- package/dist/core/backup/browser-server.js +241 -0
- package/dist/core/backup/browser-ui.d.ts +5 -0
- package/dist/core/backup/browser-ui.js +268 -0
- package/dist/core/backup/cloudflare.d.ts +7 -0
- package/dist/core/backup/cloudflare.js +82 -0
- package/dist/core/backup/config.d.ts +9 -0
- package/dist/core/backup/config.js +80 -0
- package/dist/core/backup/detect.d.ts +11 -0
- package/dist/core/backup/detect.js +71 -0
- package/dist/core/backup/dump.d.ts +11 -0
- package/dist/core/backup/dump.js +82 -0
- package/dist/core/backup/index.d.ts +9 -0
- package/dist/core/backup/index.js +9 -0
- package/dist/core/backup/repo.d.ts +71 -0
- package/dist/core/backup/repo.js +256 -0
- package/dist/core/backup/schedule.d.ts +17 -0
- package/dist/core/backup/schedule.js +90 -0
- package/dist/core/backup/sensitive.d.ts +5 -0
- package/dist/core/backup/sensitive.js +37 -0
- package/dist/core/backup/status.d.ts +3 -0
- package/dist/core/backup/status.js +29 -0
- package/dist/core/backup/statuspage.d.ts +23 -0
- package/dist/core/backup/statuspage.js +145 -0
- package/dist/core/backup/system.d.ts +24 -0
- package/dist/core/backup/system.js +209 -0
- package/dist/core/backup/totp.d.ts +16 -0
- package/dist/core/backup/totp.js +116 -0
- package/dist/core/backup/types.d.ts +70 -0
- package/dist/core/backup/types.js +7 -0
- package/dist/core/backup/unlock.d.ts +19 -0
- package/dist/core/backup/unlock.js +69 -0
- package/dist/core/boot-refresh.d.ts +1 -1
- package/dist/core/boot-refresh.js +10 -9
- package/dist/core/deps/actors/pr-creator.d.ts +5 -3
- package/dist/core/deps/actors/pr-creator.js +71 -18
- package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
- package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
- package/dist/core/deps/collectors/npm.js +3 -1
- package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
- package/dist/core/deps/collectors/vulnerability.js +31 -2
- package/dist/core/deps/config.js +6 -0
- package/dist/core/deps/scanner.js +1 -1
- package/dist/core/deps/types.d.ts +8 -0
- package/dist/core/env.d.ts +3 -0
- package/dist/core/env.js +11 -0
- package/dist/core/exec.d.ts +1 -0
- package/dist/core/exec.js +4 -0
- package/dist/core/file-lock.d.ts +18 -0
- package/dist/core/file-lock.js +44 -0
- package/dist/core/git-onboard.js +10 -13
- package/dist/core/github.d.ts +3 -1
- package/dist/core/github.js +10 -7
- package/dist/core/logs-policy.d.ts +5 -0
- package/dist/core/logs-policy.js +20 -1
- package/dist/core/operator.d.ts +21 -0
- package/dist/core/operator.js +54 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.js +26 -0
- package/dist/core/routines/schema.d.ts +11 -11
- package/dist/core/routines/schema.js +14 -3
- package/dist/core/routines/store.d.ts +8 -8
- package/dist/core/secrets-ops.d.ts +31 -6
- package/dist/core/secrets-ops.js +208 -102
- package/dist/core/secrets-providers.js +2 -2
- package/dist/core/secrets-rotation.d.ts +1 -1
- package/dist/core/secrets-rotation.js +58 -52
- package/dist/core/secrets-v2-cleanup.d.ts +19 -0
- package/dist/core/secrets-v2-cleanup.js +94 -0
- package/dist/core/secrets-v2-creds.d.ts +9 -0
- package/dist/core/secrets-v2-creds.js +44 -0
- package/dist/core/secrets-v2-install.d.ts +13 -0
- package/dist/core/secrets-v2-install.js +76 -0
- package/dist/core/secrets-v2-keypair.d.ts +10 -0
- package/dist/core/secrets-v2-keypair.js +31 -0
- package/dist/core/secrets-v2-migrate.d.ts +29 -0
- package/dist/core/secrets-v2-migrate.js +395 -0
- package/dist/core/secrets-v2-ops.d.ts +36 -0
- package/dist/core/secrets-v2-ops.js +184 -0
- package/dist/core/secrets-v2-protocol.d.ts +19 -0
- package/dist/core/secrets-v2-protocol.js +60 -0
- package/dist/core/secrets-v2-snapshot.d.ts +36 -0
- package/dist/core/secrets-v2-snapshot.js +115 -0
- package/dist/core/secrets-v2.d.ts +21 -0
- package/dist/core/secrets-v2.js +249 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +91 -11
- package/dist/core/self-update.d.ts +32 -11
- package/dist/core/self-update.js +52 -14
- package/dist/core/testflight/asc.d.ts +12 -0
- package/dist/core/testflight/asc.js +101 -0
- package/dist/core/testflight/credentials.d.ts +3 -0
- package/dist/core/testflight/credentials.js +35 -0
- package/dist/core/testflight/eas.d.ts +4 -0
- package/dist/core/testflight/eas.js +38 -0
- package/dist/core/testflight/resolve.d.ts +6 -0
- package/dist/core/testflight/resolve.js +44 -0
- package/dist/core/testflight/types.d.ts +13 -0
- package/dist/core/testflight/types.js +3 -0
- package/dist/core/testflight/workflow.d.ts +17 -0
- package/dist/core/testflight/workflow.js +65 -0
- package/dist/core/validate.d.ts +1 -0
- package/dist/core/validate.js +8 -0
- package/dist/mcp/audit-tools.d.ts +2 -0
- package/dist/mcp/audit-tools.js +94 -0
- package/dist/mcp/git-tools.js +1 -1
- package/dist/mcp/registry-bridge.d.ts +10 -0
- package/dist/mcp/registry-bridge.js +65 -0
- package/dist/mcp/secrets-tools.js +2 -2
- package/dist/mcp/server.js +16 -82
- package/dist/mcp/testflight-tools.d.ts +2 -0
- package/dist/mcp/testflight-tools.js +52 -0
- package/dist/registry/context.d.ts +7 -0
- package/dist/registry/context.js +37 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +44 -0
- package/dist/registry/parse-args.d.ts +13 -0
- package/dist/registry/parse-args.js +74 -0
- package/dist/registry/registry.d.ts +24 -0
- package/dist/registry/registry.js +26 -0
- package/dist/registry/render.d.ts +3 -0
- package/dist/registry/render.js +29 -0
- package/dist/registry/types.d.ts +50 -0
- package/dist/registry/types.js +1 -0
- package/dist/templates/agent-unit.d.ts +5 -0
- package/dist/templates/agent-unit.js +40 -0
- package/dist/templates/app-unit-edit.d.ts +2 -0
- package/dist/templates/app-unit-edit.js +46 -0
- package/dist/templates/compose-edit.d.ts +2 -0
- package/dist/templates/compose-edit.js +156 -0
- package/dist/templates/nginx.js +11 -0
- package/dist/templates/systemd.js +6 -0
- package/dist/tui/components/ArgForm.d.ts +7 -0
- package/dist/tui/components/ArgForm.js +64 -0
- package/dist/tui/components/ArgForm.test.d.ts +1 -0
- package/dist/tui/components/ArgForm.test.js +19 -0
- package/dist/tui/components/KeyHint.js +5 -0
- package/dist/tui/hooks/use-secrets.d.ts +8 -8
- package/dist/tui/hooks/use-secrets.js +7 -7
- package/dist/tui/router.d.ts +1 -0
- package/dist/tui/router.js +26 -9
- package/dist/tui/router.test.d.ts +1 -0
- package/dist/tui/router.test.js +13 -0
- package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
- package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
- package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
- package/dist/tui/tests/redaction-rerender.test.js +53 -0
- package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
- package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
- package/dist/tui/types.d.ts +1 -1
- package/dist/tui/views/CommandPalette.d.ts +5 -0
- package/dist/tui/views/CommandPalette.js +90 -0
- package/dist/tui/views/CommandPalette.test.d.ts +1 -0
- package/dist/tui/views/CommandPalette.test.js +117 -0
- package/dist/tui/views/Dashboard.js +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,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;
|
|
@@ -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 {};
|