@matthesketh/fleet 1.8.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -16
- package/dist/bin/fleet-agent.d.ts +2 -0
- package/dist/bin/fleet-agent.js +7 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +73 -31
- package/dist/commands/add.d.ts +2 -1
- package/dist/commands/add.js +66 -59
- package/dist/commands/audit.d.ts +1 -0
- package/dist/commands/audit.js +144 -0
- package/dist/commands/backup.d.ts +1 -0
- package/dist/commands/backup.js +510 -0
- package/dist/commands/boot-start.d.ts +3 -1
- package/dist/commands/boot-start.js +39 -47
- package/dist/commands/completions.d.ts +6 -0
- package/dist/commands/completions.js +83 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.js +96 -0
- package/dist/commands/deploy.js +3 -2
- package/dist/commands/deps.js +5 -1
- package/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.js +186 -0
- package/dist/commands/egress.d.ts +1 -1
- package/dist/commands/egress.js +13 -10
- package/dist/commands/freeze.d.ts +8 -4
- package/dist/commands/freeze.js +77 -59
- package/dist/commands/git.js +2 -2
- package/dist/commands/health.d.ts +2 -1
- package/dist/commands/health.js +38 -56
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +83 -73
- package/dist/commands/install-mcp.d.ts +3 -1
- package/dist/commands/install-mcp.js +53 -34
- package/dist/commands/list.d.ts +2 -1
- package/dist/commands/list.js +22 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/notify.d.ts +1 -0
- package/dist/commands/notify.js +51 -0
- package/dist/commands/patch-systemd.d.ts +7 -1
- package/dist/commands/patch-systemd.js +71 -31
- package/dist/commands/remove.d.ts +3 -1
- package/dist/commands/remove.js +37 -26
- package/dist/commands/restart.d.ts +4 -1
- package/dist/commands/restart.js +17 -20
- package/dist/commands/rollback.d.ts +4 -1
- package/dist/commands/rollback.js +33 -42
- package/dist/commands/secrets.js +157 -9
- package/dist/commands/start.d.ts +4 -1
- package/dist/commands/start.js +17 -20
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +21 -26
- package/dist/commands/stop.d.ts +4 -1
- package/dist/commands/stop.js +17 -20
- package/dist/commands/testflight.d.ts +1 -0
- package/dist/commands/testflight.js +193 -0
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.js +95 -0
- package/dist/core/audit/cache.d.ts +4 -0
- package/dist/core/audit/cache.js +37 -0
- package/dist/core/audit/config.d.ts +5 -0
- package/dist/core/audit/config.js +35 -0
- package/dist/core/audit/greenlight.d.ts +11 -0
- package/dist/core/audit/greenlight.js +81 -0
- package/dist/core/audit/reporters/cli.d.ts +3 -0
- package/dist/core/audit/reporters/cli.js +68 -0
- package/dist/core/audit/suppress.d.ts +6 -0
- package/dist/core/audit/suppress.js +37 -0
- package/dist/core/audit/target.d.ts +5 -0
- package/dist/core/audit/target.js +26 -0
- package/dist/core/audit/types.d.ts +54 -0
- package/dist/core/audit/types.js +5 -0
- package/dist/core/backup/browser-api.d.ts +66 -0
- package/dist/core/backup/browser-api.js +197 -0
- package/dist/core/backup/browser-server.d.ts +11 -0
- package/dist/core/backup/browser-server.js +241 -0
- package/dist/core/backup/browser-ui.d.ts +5 -0
- package/dist/core/backup/browser-ui.js +268 -0
- package/dist/core/backup/cloudflare.d.ts +7 -0
- package/dist/core/backup/cloudflare.js +82 -0
- package/dist/core/backup/config.d.ts +9 -0
- package/dist/core/backup/config.js +80 -0
- package/dist/core/backup/detect.d.ts +11 -0
- package/dist/core/backup/detect.js +71 -0
- package/dist/core/backup/dump.d.ts +11 -0
- package/dist/core/backup/dump.js +82 -0
- package/dist/core/backup/index.d.ts +9 -0
- package/dist/core/backup/index.js +9 -0
- package/dist/core/backup/repo.d.ts +71 -0
- package/dist/core/backup/repo.js +256 -0
- package/dist/core/backup/schedule.d.ts +17 -0
- package/dist/core/backup/schedule.js +90 -0
- package/dist/core/backup/sensitive.d.ts +5 -0
- package/dist/core/backup/sensitive.js +37 -0
- package/dist/core/backup/status.d.ts +3 -0
- package/dist/core/backup/status.js +29 -0
- package/dist/core/backup/statuspage.d.ts +23 -0
- package/dist/core/backup/statuspage.js +145 -0
- package/dist/core/backup/system.d.ts +24 -0
- package/dist/core/backup/system.js +209 -0
- package/dist/core/backup/totp.d.ts +16 -0
- package/dist/core/backup/totp.js +116 -0
- package/dist/core/backup/types.d.ts +70 -0
- package/dist/core/backup/types.js +7 -0
- package/dist/core/backup/unlock.d.ts +19 -0
- package/dist/core/backup/unlock.js +69 -0
- package/dist/core/boot-refresh.d.ts +1 -1
- package/dist/core/boot-refresh.js +10 -9
- package/dist/core/deps/actors/pr-creator.d.ts +5 -3
- package/dist/core/deps/actors/pr-creator.js +71 -18
- package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
- package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
- package/dist/core/deps/collectors/npm.js +3 -1
- package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
- package/dist/core/deps/collectors/vulnerability.js +31 -2
- package/dist/core/deps/config.js +6 -0
- package/dist/core/deps/scanner.js +1 -1
- package/dist/core/deps/types.d.ts +8 -0
- package/dist/core/env.d.ts +3 -0
- package/dist/core/env.js +11 -0
- package/dist/core/exec.d.ts +1 -0
- package/dist/core/exec.js +4 -0
- package/dist/core/file-lock.d.ts +18 -0
- package/dist/core/file-lock.js +44 -0
- package/dist/core/git-onboard.js +10 -13
- package/dist/core/github.d.ts +3 -1
- package/dist/core/github.js +10 -7
- package/dist/core/logs-policy.d.ts +5 -0
- package/dist/core/logs-policy.js +20 -1
- package/dist/core/operator.d.ts +21 -0
- package/dist/core/operator.js +54 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.js +26 -0
- package/dist/core/routines/schema.d.ts +11 -11
- package/dist/core/routines/schema.js +14 -3
- package/dist/core/routines/store.d.ts +8 -8
- package/dist/core/secrets-ops.d.ts +31 -6
- package/dist/core/secrets-ops.js +208 -102
- package/dist/core/secrets-providers.js +2 -2
- package/dist/core/secrets-rotation.d.ts +1 -1
- package/dist/core/secrets-rotation.js +58 -52
- package/dist/core/secrets-v2-cleanup.d.ts +19 -0
- package/dist/core/secrets-v2-cleanup.js +94 -0
- package/dist/core/secrets-v2-creds.d.ts +9 -0
- package/dist/core/secrets-v2-creds.js +44 -0
- package/dist/core/secrets-v2-install.d.ts +13 -0
- package/dist/core/secrets-v2-install.js +76 -0
- package/dist/core/secrets-v2-keypair.d.ts +10 -0
- package/dist/core/secrets-v2-keypair.js +31 -0
- package/dist/core/secrets-v2-migrate.d.ts +29 -0
- package/dist/core/secrets-v2-migrate.js +395 -0
- package/dist/core/secrets-v2-ops.d.ts +36 -0
- package/dist/core/secrets-v2-ops.js +184 -0
- package/dist/core/secrets-v2-protocol.d.ts +19 -0
- package/dist/core/secrets-v2-protocol.js +60 -0
- package/dist/core/secrets-v2-snapshot.d.ts +36 -0
- package/dist/core/secrets-v2-snapshot.js +115 -0
- package/dist/core/secrets-v2.d.ts +21 -0
- package/dist/core/secrets-v2.js +249 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +91 -11
- package/dist/core/self-update.d.ts +32 -11
- package/dist/core/self-update.js +52 -14
- package/dist/core/testflight/asc.d.ts +12 -0
- package/dist/core/testflight/asc.js +101 -0
- package/dist/core/testflight/credentials.d.ts +3 -0
- package/dist/core/testflight/credentials.js +35 -0
- package/dist/core/testflight/eas.d.ts +4 -0
- package/dist/core/testflight/eas.js +38 -0
- package/dist/core/testflight/resolve.d.ts +6 -0
- package/dist/core/testflight/resolve.js +44 -0
- package/dist/core/testflight/types.d.ts +13 -0
- package/dist/core/testflight/types.js +3 -0
- package/dist/core/testflight/workflow.d.ts +17 -0
- package/dist/core/testflight/workflow.js +65 -0
- package/dist/core/validate.d.ts +1 -0
- package/dist/core/validate.js +8 -0
- package/dist/mcp/audit-tools.d.ts +2 -0
- package/dist/mcp/audit-tools.js +94 -0
- package/dist/mcp/git-tools.js +1 -1
- package/dist/mcp/registry-bridge.d.ts +10 -0
- package/dist/mcp/registry-bridge.js +65 -0
- package/dist/mcp/secrets-tools.js +2 -2
- package/dist/mcp/server.js +16 -82
- package/dist/mcp/testflight-tools.d.ts +2 -0
- package/dist/mcp/testflight-tools.js +52 -0
- package/dist/registry/context.d.ts +7 -0
- package/dist/registry/context.js +37 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +44 -0
- package/dist/registry/parse-args.d.ts +13 -0
- package/dist/registry/parse-args.js +74 -0
- package/dist/registry/registry.d.ts +24 -0
- package/dist/registry/registry.js +26 -0
- package/dist/registry/render.d.ts +3 -0
- package/dist/registry/render.js +29 -0
- package/dist/registry/types.d.ts +50 -0
- package/dist/registry/types.js +1 -0
- package/dist/templates/agent-unit.d.ts +5 -0
- package/dist/templates/agent-unit.js +40 -0
- package/dist/templates/app-unit-edit.d.ts +2 -0
- package/dist/templates/app-unit-edit.js +46 -0
- package/dist/templates/compose-edit.d.ts +2 -0
- package/dist/templates/compose-edit.js +156 -0
- package/dist/templates/nginx.js +11 -0
- package/dist/templates/systemd.js +6 -0
- package/dist/tui/components/ArgForm.d.ts +7 -0
- package/dist/tui/components/ArgForm.js +64 -0
- package/dist/tui/components/ArgForm.test.d.ts +1 -0
- package/dist/tui/components/ArgForm.test.js +19 -0
- package/dist/tui/components/KeyHint.js +5 -0
- package/dist/tui/hooks/use-secrets.d.ts +8 -8
- package/dist/tui/hooks/use-secrets.js +7 -7
- package/dist/tui/router.d.ts +1 -0
- package/dist/tui/router.js +26 -9
- package/dist/tui/router.test.d.ts +1 -0
- package/dist/tui/router.test.js +13 -0
- package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
- package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
- package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
- package/dist/tui/tests/redaction-rerender.test.js +53 -0
- package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
- package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
- package/dist/tui/types.d.ts +1 -1
- package/dist/tui/views/CommandPalette.d.ts +5 -0
- package/dist/tui/views/CommandPalette.js +90 -0
- package/dist/tui/views/CommandPalette.test.d.ts +1 -0
- package/dist/tui/views/CommandPalette.test.js +117 -0
- package/dist/tui/views/Dashboard.js +10 -7
- package/dist/tui/views/HealthView.js +14 -5
- package/dist/tui/views/SecretEdit.js +15 -16
- package/dist/tui/views/SecretEdit.test.d.ts +1 -0
- package/dist/tui/views/SecretEdit.test.js +82 -0
- package/dist/tui/views/SecretsView.js +26 -16
- package/package.json +9 -6
package/dist/commands/secrets.js
CHANGED
|
@@ -19,6 +19,10 @@ import { checkHealth } from '../core/health.js';
|
|
|
19
19
|
import { listSnapshots, restoreSnapshot, snapshotApp } from '../core/secrets-snapshots.js';
|
|
20
20
|
import { auditLog } from '../core/secrets-audit.js';
|
|
21
21
|
import { summariseSecrets, formatSecretsMotd, generateSecretsMotdScript } from '../core/secrets-motd.js';
|
|
22
|
+
import { migrateAppToV2, revertAppFromV2 } from '../core/secrets-v2-migrate.js';
|
|
23
|
+
import { cleanupV2Backups } from '../core/secrets-v2-cleanup.js';
|
|
24
|
+
import { getV2Status } from '../core/secrets-v2-ops.js';
|
|
25
|
+
import { installV2 } from '../core/secrets-v2-install.js';
|
|
22
26
|
function getDbSecretsDir() {
|
|
23
27
|
const reg = load();
|
|
24
28
|
return join(reg.infrastructure.databases.composePath, 'secrets');
|
|
@@ -46,8 +50,13 @@ export async function secretsCommand(args) {
|
|
|
46
50
|
case 'snapshots': return secretsSnapshots(rest);
|
|
47
51
|
case 'motd-init': return secretsMotdInit();
|
|
48
52
|
case 'seal-runtime': return secretsSeal(rest);
|
|
53
|
+
case 'migrate-v2': return secretsMigrateV2(rest);
|
|
54
|
+
case 'revert-v2': return secretsRevertV2(rest);
|
|
55
|
+
case 'cleanup-v2': return secretsCleanupV2(rest);
|
|
56
|
+
case 'status-v2': return secretsStatusV2(rest);
|
|
57
|
+
case 'install-v2': return secretsInstallV2(rest);
|
|
49
58
|
default:
|
|
50
|
-
error('Usage: fleet secrets <init|list|set|get|import|export|seal|unseal|rotate|rotate-key|ages|rollback|snapshots|validate|status|drift|restore>');
|
|
59
|
+
error('Usage: fleet secrets <init|list|set|get|import|export|seal|unseal|rotate|rotate-key|ages|rollback|snapshots|validate|status|drift|restore|migrate-v2|revert-v2|cleanup-v2|status-v2|install-v2>');
|
|
51
60
|
process.exit(1);
|
|
52
61
|
}
|
|
53
62
|
}
|
|
@@ -129,7 +138,7 @@ async function secretsSet(args) {
|
|
|
129
138
|
error('Empty value — aborting');
|
|
130
139
|
process.exit(1);
|
|
131
140
|
}
|
|
132
|
-
setSecret(app, key, value, { allowWeak });
|
|
141
|
+
await setSecret(app, key, value, { allowWeak });
|
|
133
142
|
success(`Set ${key} for ${app}`);
|
|
134
143
|
}
|
|
135
144
|
function secretsGet(args) {
|
|
@@ -145,7 +154,7 @@ function secretsGet(args) {
|
|
|
145
154
|
}
|
|
146
155
|
process.stdout.write(val + '\n');
|
|
147
156
|
}
|
|
148
|
-
function secretsImport(args) {
|
|
157
|
+
async function secretsImport(args) {
|
|
149
158
|
const app = args.find(a => !a.startsWith('-'));
|
|
150
159
|
const pathArg = args[1] && !args[1].startsWith('-') ? args[1] : null;
|
|
151
160
|
if (!app) {
|
|
@@ -154,7 +163,7 @@ function secretsImport(args) {
|
|
|
154
163
|
}
|
|
155
164
|
if (app === 'docker-databases') {
|
|
156
165
|
const dir = pathArg || getDbSecretsDir();
|
|
157
|
-
const count = importDbSecrets(app, dir);
|
|
166
|
+
const count = await importDbSecrets(app, dir);
|
|
158
167
|
success(`Imported ${count} secret files from ${dir}`);
|
|
159
168
|
return;
|
|
160
169
|
}
|
|
@@ -170,7 +179,7 @@ function secretsImport(args) {
|
|
|
170
179
|
else {
|
|
171
180
|
throw new SecretsError(`App not in registry and no path given: ${app}`);
|
|
172
181
|
}
|
|
173
|
-
const count = importEnvFile(app, envPath);
|
|
182
|
+
const count = await importEnvFile(app, envPath);
|
|
174
183
|
success(`Imported ${count} keys from ${envPath}`);
|
|
175
184
|
}
|
|
176
185
|
function secretsExport(args) {
|
|
@@ -187,9 +196,9 @@ function secretsUnseal() {
|
|
|
187
196
|
const count = Object.keys(manifest.apps).length;
|
|
188
197
|
success(`Unsealed ${count} apps to /run/fleet-secrets/`);
|
|
189
198
|
}
|
|
190
|
-
function secretsSeal(args) {
|
|
199
|
+
async function secretsSeal(args) {
|
|
191
200
|
const app = args.find(a => !a.startsWith('-')) || undefined;
|
|
192
|
-
const sealed = sealFromRuntime(app);
|
|
201
|
+
const sealed = await sealFromRuntime(app);
|
|
193
202
|
for (const a of sealed) {
|
|
194
203
|
success(`Sealed ${a}`);
|
|
195
204
|
}
|
|
@@ -200,7 +209,7 @@ async function secretsRotateKey(args) {
|
|
|
200
209
|
info('Cancelled');
|
|
201
210
|
return;
|
|
202
211
|
}
|
|
203
|
-
const result = rotateKey();
|
|
212
|
+
const result = await rotateKey();
|
|
204
213
|
success(`Key rotated`);
|
|
205
214
|
info(`Old: ${result.oldPubkey}`);
|
|
206
215
|
info(`New: ${result.newPubkey}`);
|
|
@@ -294,7 +303,7 @@ async function rotateOneInteractive(app, secret, opts) {
|
|
|
294
303
|
info('Cancelled');
|
|
295
304
|
return { acted: false, succeeded: false };
|
|
296
305
|
}
|
|
297
|
-
const result = performRotation(app, secret.name, newValue, {
|
|
306
|
+
const result = await performRotation(app, secret.name, newValue, {
|
|
298
307
|
dryRun: opts.dryRun,
|
|
299
308
|
dataMigrated: opts.dataMigrated,
|
|
300
309
|
});
|
|
@@ -699,3 +708,142 @@ function secretsRestore(args) {
|
|
|
699
708
|
success(`Restored vault backup for ${app}`);
|
|
700
709
|
info('Run "fleet secrets unseal" to apply to runtime');
|
|
701
710
|
}
|
|
711
|
+
async function secretsMigrateV2(args) {
|
|
712
|
+
const app = args[0];
|
|
713
|
+
if (!app || app.startsWith('-')) {
|
|
714
|
+
error('Usage: fleet secrets migrate-v2 <app> [--no-restart-app] [--dry-run]');
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
const noRestartApp = args.includes('--no-restart-app');
|
|
718
|
+
const dryRun = args.includes('--dry-run');
|
|
719
|
+
heading(`Migrating ${app} to v2 (mode=socket)`);
|
|
720
|
+
if (dryRun)
|
|
721
|
+
info('DRY RUN — no actual changes will be applied');
|
|
722
|
+
try {
|
|
723
|
+
const result = await migrateAppToV2({ app, noRestartApp, dryRun });
|
|
724
|
+
if (result.rolledBack) {
|
|
725
|
+
error(`Migration failed; rolled back from snapshot ${result.snapshotDir}`);
|
|
726
|
+
for (const step of result.steps) {
|
|
727
|
+
if (!step.ok)
|
|
728
|
+
info(` step ${step.step} (${step.name}): ${step.detail}`);
|
|
729
|
+
}
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
success(`Migrated ${app} to v2`);
|
|
733
|
+
if (result.snapshotDir)
|
|
734
|
+
info(`Snapshot: ${result.snapshotDir}`);
|
|
735
|
+
for (const step of result.steps) {
|
|
736
|
+
info(` step ${step.step} (${step.name}): ${step.ok ? 'OK' : 'FAILED'}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
error(`Migration failed: ${err.message}`);
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async function secretsRevertV2(args) {
|
|
745
|
+
const app = args[0];
|
|
746
|
+
if (!app || app.startsWith('-')) {
|
|
747
|
+
error('Usage: fleet secrets revert-v2 <app> [--snapshot <timestamp>]');
|
|
748
|
+
process.exit(1);
|
|
749
|
+
}
|
|
750
|
+
const snapIdx = args.indexOf('--snapshot');
|
|
751
|
+
const snapshotTimestamp = snapIdx >= 0 ? args[snapIdx + 1] : undefined;
|
|
752
|
+
heading(`Reverting ${app} from v2`);
|
|
753
|
+
try {
|
|
754
|
+
const result = await revertAppFromV2({ app, snapshotTimestamp });
|
|
755
|
+
if (result.ok) {
|
|
756
|
+
success(`Reverted ${app} to v1; restored from ${result.snapshotUsed}`);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
error(`Revert reported issues — see steps below`);
|
|
760
|
+
}
|
|
761
|
+
for (const step of result.steps) {
|
|
762
|
+
const status = step.ok ? 'OK' : 'FAILED';
|
|
763
|
+
info(` step ${step.step} (${step.name}): ${status}${step.detail ? ' — ' + step.detail : ''}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
error(`Revert failed: ${err.message}`);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async function secretsCleanupV2(args) {
|
|
772
|
+
const app = args[0];
|
|
773
|
+
if (!app || app.startsWith('-')) {
|
|
774
|
+
error('Usage: fleet secrets cleanup-v2 <app> [--retention-days N] [--dry-run]');
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
const retIdx = args.indexOf('--retention-days');
|
|
778
|
+
const retentionDays = retIdx >= 0 ? parseInt(args[retIdx + 1], 10) : 30;
|
|
779
|
+
const dryRun = args.includes('--dry-run');
|
|
780
|
+
if (Number.isNaN(retentionDays) || retentionDays < 0) {
|
|
781
|
+
error(`--retention-days must be a non-negative integer`);
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
heading(`Cleaning up v2 backups for ${app} (retention=${retentionDays}d)`);
|
|
785
|
+
if (dryRun)
|
|
786
|
+
info('DRY RUN — no actual deletions');
|
|
787
|
+
try {
|
|
788
|
+
const result = await cleanupV2Backups({ app, retentionDays, dryRun });
|
|
789
|
+
if (result.removedBak)
|
|
790
|
+
info(`v1 backup blob ${dryRun ? 'would be removed' : 'removed'}`);
|
|
791
|
+
info(`Snapshots removed: ${result.removedSnapshots.length}`);
|
|
792
|
+
info(`Snapshots kept: ${result.keptSnapshots.length}`);
|
|
793
|
+
for (const ts of result.removedSnapshots)
|
|
794
|
+
info(` - ${ts}`);
|
|
795
|
+
success('Cleanup complete');
|
|
796
|
+
}
|
|
797
|
+
catch (err) {
|
|
798
|
+
error(`Cleanup failed: ${err.message}`);
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function secretsInstallV2(args) {
|
|
803
|
+
const dryRun = args.includes('--dry-run');
|
|
804
|
+
heading(`Installing fleet-secrets-agent v2 host components`);
|
|
805
|
+
if (dryRun)
|
|
806
|
+
info('DRY RUN');
|
|
807
|
+
try {
|
|
808
|
+
const result = await installV2({ dryRun });
|
|
809
|
+
if (result.agentBinaryInstalled)
|
|
810
|
+
success('Agent binary installed at /usr/local/bin/fleet-agent');
|
|
811
|
+
else
|
|
812
|
+
info('Agent binary already current');
|
|
813
|
+
if (result.unitFileInstalled)
|
|
814
|
+
success('Templated unit installed at /etc/systemd/system/fleet-secrets-agent@.service');
|
|
815
|
+
else
|
|
816
|
+
info('Unit file already current');
|
|
817
|
+
if (result.daemonReloaded)
|
|
818
|
+
success('systemctl daemon-reload completed');
|
|
819
|
+
if (!result.templateParseable && !dryRun)
|
|
820
|
+
warn('Templated unit did not parse cleanly — investigate');
|
|
821
|
+
else if (!dryRun)
|
|
822
|
+
success('Templated unit verified');
|
|
823
|
+
}
|
|
824
|
+
catch (err) {
|
|
825
|
+
error(`Install failed: ${err.message}`);
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function secretsStatusV2(_args) {
|
|
830
|
+
const report = getV2Status();
|
|
831
|
+
heading('Fleet secrets v2 status');
|
|
832
|
+
info(`v1 (unseal): ${report.v1Count} apps`);
|
|
833
|
+
info(`v2 (socket): ${report.v2Count} apps`);
|
|
834
|
+
info('');
|
|
835
|
+
if (report.apps.length === 0) {
|
|
836
|
+
info('No apps in manifest');
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const headers = ['App', 'Mode', 'Agent', 'Socket', 'Keys', 'Last sealed'];
|
|
840
|
+
const rows = report.apps.map(a => [
|
|
841
|
+
a.name,
|
|
842
|
+
a.mode,
|
|
843
|
+
a.mode === 'socket' ? (a.agentActive ? 'active' : 'inactive') : '—',
|
|
844
|
+
a.mode === 'socket' ? (a.socketOk ? 'ok' : 'BAD') : '—',
|
|
845
|
+
String(a.keyCount),
|
|
846
|
+
a.lastSealedAt.slice(0, 10),
|
|
847
|
+
]);
|
|
848
|
+
table(headers, rows);
|
|
849
|
+
}
|
package/dist/commands/start.d.ts
CHANGED
package/dist/commands/start.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { load, findApp } from '../core/registry.js';
|
|
2
3
|
import { startService } from '../core/systemd.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
error(`Failed to start ${app.name}`);
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
4
|
+
import { defineCommand } from '../registry/registry.js';
|
|
5
|
+
export const startCommand = defineCommand({
|
|
6
|
+
name: 'start',
|
|
7
|
+
summary: 'Start an app via systemctl',
|
|
8
|
+
args: z.object({ app: z.string() }),
|
|
9
|
+
async run(args) {
|
|
10
|
+
const app = findApp(load(), args.app);
|
|
11
|
+
if (!app) {
|
|
12
|
+
return { ok: false, summary: `app not found: ${args.app}`, data: { app: args.app, service: '' } };
|
|
13
|
+
}
|
|
14
|
+
if (!startService(app.serviceName)) {
|
|
15
|
+
return { ok: false, summary: `failed to start ${app.name}`, data: { app: app.name, service: app.serviceName } };
|
|
16
|
+
}
|
|
17
|
+
return { ok: true, summary: `started ${app.name}`, data: { app: app.name, service: app.serviceName } };
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -11,4 +11,4 @@ export interface StatusData {
|
|
|
11
11
|
unhealthy: number;
|
|
12
12
|
}
|
|
13
13
|
export declare function getStatusData(): StatusData;
|
|
14
|
-
export declare
|
|
14
|
+
export declare const statusCommand: import("../registry/types.js").CommandDef<StatusData>;
|
package/dist/commands/status.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { load } from '../core/registry.js';
|
|
2
3
|
import { getMultipleServiceStatuses, systemdAvailable } from '../core/systemd.js';
|
|
3
4
|
import { listContainers } from '../core/docker.js';
|
|
4
|
-
import {
|
|
5
|
+
import { defineCommand } from '../registry/registry.js';
|
|
5
6
|
export function getStatusData() {
|
|
6
7
|
const reg = load();
|
|
7
8
|
const containers = listContainers();
|
|
@@ -47,28 +48,22 @@ export function getStatusData() {
|
|
|
47
48
|
unhealthy: apps.filter(a => a.health !== 'healthy').length,
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
|
-
export
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
:
|
|
62
|
-
:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`${healthIcon} ${app.health}`,
|
|
70
|
-
];
|
|
71
|
-
});
|
|
72
|
-
table(['APP', 'SYSTEMD', 'CONTAINERS', 'HEALTH'], rows);
|
|
73
|
-
process.stdout.write('\n');
|
|
74
|
-
}
|
|
51
|
+
export const statusCommand = defineCommand({
|
|
52
|
+
name: 'status',
|
|
53
|
+
summary: 'Dashboard: all apps, systemd state, containers, health',
|
|
54
|
+
args: z.object({}),
|
|
55
|
+
tui: { view: 'dashboard' },
|
|
56
|
+
async run() {
|
|
57
|
+
const data = getStatusData();
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
summary: `${data.totalApps} apps | ${data.healthy} healthy | ${data.unhealthy} unhealthy`,
|
|
61
|
+
data,
|
|
62
|
+
render: {
|
|
63
|
+
kind: 'table',
|
|
64
|
+
columns: ['APP', 'SYSTEMD', 'CONTAINERS', 'HEALTH'],
|
|
65
|
+
rows: data.apps.map(a => [a.name, a.systemd, a.containers, a.health]),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
package/dist/commands/stop.d.ts
CHANGED
package/dist/commands/stop.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { load, findApp } from '../core/registry.js';
|
|
2
3
|
import { stopService } from '../core/systemd.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
error(`Failed to stop ${app.name}`);
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
4
|
+
import { defineCommand } from '../registry/registry.js';
|
|
5
|
+
export const stopCommand = defineCommand({
|
|
6
|
+
name: 'stop',
|
|
7
|
+
summary: 'Stop an app via systemctl',
|
|
8
|
+
args: z.object({ app: z.string() }),
|
|
9
|
+
async run(args) {
|
|
10
|
+
const app = findApp(load(), args.app);
|
|
11
|
+
if (!app) {
|
|
12
|
+
return { ok: false, summary: `app not found: ${args.app}`, data: { app: args.app, service: '' } };
|
|
13
|
+
}
|
|
14
|
+
if (!stopService(app.serviceName)) {
|
|
15
|
+
return { ok: false, summary: `failed to stop ${app.name}`, data: { app: app.name, service: app.serviceName } };
|
|
16
|
+
}
|
|
17
|
+
return { ok: true, summary: `stopped ${app.name}`, data: { app: app.name, service: app.serviceName } };
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function testflightCommand(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { resolveAscCredentials, hasAscCredentials } from '../core/testflight/credentials.js';
|
|
2
|
+
import { ghVersion, resolveRepo, repoSecrets, dispatchWorkflow, latestRun, watchRun, } from '../core/testflight/workflow.js';
|
|
3
|
+
import { listBuilds, expireBuild, setWhatsNew, verifyApp } from '../core/testflight/asc.js';
|
|
4
|
+
import { resolveTestflightTarget, appSecretsEnv } from '../core/testflight/resolve.js';
|
|
5
|
+
import { heading, success, error, info, warn, table } from '../ui/output.js';
|
|
6
|
+
// the build workflow this command dispatches by default — a macos-runner
|
|
7
|
+
// workflow committed to the app's repo at .github/workflows/.
|
|
8
|
+
const DEFAULT_WORKFLOW = 'ios-testflight.yml';
|
|
9
|
+
// the actions secrets the build workflow needs to sign and upload an .ipa.
|
|
10
|
+
const REQUIRED_REPO_SECRETS = [
|
|
11
|
+
'ASC_API_KEY_ID', 'ASC_API_KEY_ISSUER_ID', 'ASC_API_KEY_B64', 'APPLE_TEAM_ID',
|
|
12
|
+
];
|
|
13
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
14
|
+
// `fleet testflight` — publish a mobile app to TestFlight by dispatching its
|
|
15
|
+
// repo's macOS build workflow, and manage its builds through the App Store
|
|
16
|
+
// Connect API. an iOS .ipa can only be built on macOS, so publish runs the
|
|
17
|
+
// build on a github-hosted runner rather than locally.
|
|
18
|
+
export async function testflightCommand(args) {
|
|
19
|
+
switch (args[0]) {
|
|
20
|
+
case 'doctor': return tfDoctor(args.slice(1));
|
|
21
|
+
case 'publish': return tfPublish(args.slice(1));
|
|
22
|
+
case 'builds': return tfBuilds(args.slice(1));
|
|
23
|
+
case 'update': return tfUpdate(args.slice(1));
|
|
24
|
+
case 'delete': return tfDelete(args.slice(1));
|
|
25
|
+
default:
|
|
26
|
+
error('Usage: fleet testflight <doctor|publish|builds|update|delete>');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function extractFlag(args, flag) {
|
|
31
|
+
const idx = args.indexOf(flag);
|
|
32
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
33
|
+
return undefined;
|
|
34
|
+
return args[idx + 1];
|
|
35
|
+
}
|
|
36
|
+
async function tfDoctor(args) {
|
|
37
|
+
heading('TestFlight — readiness');
|
|
38
|
+
const gh = ghVersion();
|
|
39
|
+
if (gh)
|
|
40
|
+
success(`GitHub CLI available: ${gh}`);
|
|
41
|
+
else
|
|
42
|
+
error('GitHub CLI (gh) not found — required to dispatch the build workflow');
|
|
43
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
44
|
+
if (!appName) {
|
|
45
|
+
info('Pass an app name to check its repo + credentials: fleet testflight doctor <app>');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const { app, projectPath } = resolveTestflightTarget(appName);
|
|
49
|
+
const repo = resolveRepo(projectPath);
|
|
50
|
+
if (repo) {
|
|
51
|
+
success(`GitHub repo: ${repo}`);
|
|
52
|
+
const secrets = repoSecrets(repo);
|
|
53
|
+
if (secrets) {
|
|
54
|
+
const missing = REQUIRED_REPO_SECRETS.filter(s => !secrets.includes(s));
|
|
55
|
+
if (missing.length === 0)
|
|
56
|
+
success('Repo Actions secrets present — the build workflow can sign and upload');
|
|
57
|
+
else
|
|
58
|
+
warn(`Repo Actions secrets missing: ${missing.join(', ')} — set them with "gh secret set"`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
warn('Could not list repo Actions secrets — check "gh auth status"');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
warn(`No GitHub repo resolved for ${app} — publish needs a gh checkout at ${projectPath}`);
|
|
66
|
+
}
|
|
67
|
+
const env = appSecretsEnv(app);
|
|
68
|
+
if (!hasAscCredentials(env)) {
|
|
69
|
+
error(`App Store Connect credentials missing for ${app}`);
|
|
70
|
+
info('Required vault secrets: ASC_API_KEY_ID, ASC_API_KEY_ISSUER_ID, ASC_API_KEY_B64');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
success('App Store Connect credentials present (builds/update/delete)');
|
|
74
|
+
const ascAppId = env.ASC_APP_ID;
|
|
75
|
+
if (!ascAppId) {
|
|
76
|
+
warn('ASC_APP_ID not set — builds/update/delete need it');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const name = await verifyApp(resolveAscCredentials(env), ascAppId);
|
|
81
|
+
success(`App Store Connect reachable — app: ${name}`);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
error(`App Store Connect check failed: ${err.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function tfPublish(args) {
|
|
89
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
90
|
+
if (!appName) {
|
|
91
|
+
error('Usage: fleet testflight publish <app> [--workflow <file>] [--ref <branch>] [--watch]');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const workflow = extractFlag(args, '--workflow') ?? DEFAULT_WORKFLOW;
|
|
95
|
+
const ref = extractFlag(args, '--ref');
|
|
96
|
+
const watch = args.includes('--watch');
|
|
97
|
+
const { app, projectPath } = resolveTestflightTarget(appName);
|
|
98
|
+
heading(`TestFlight publish: ${app}`);
|
|
99
|
+
if (!ghVersion()) {
|
|
100
|
+
error('GitHub CLI (gh) not found — required to dispatch the build workflow');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const repo = resolveRepo(projectPath);
|
|
104
|
+
if (!repo) {
|
|
105
|
+
error(`Could not resolve a GitHub repo for ${app} — is ${projectPath} a gh checkout?`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
// remember the newest run so the one this dispatch queues can be told
|
|
109
|
+
// apart from it — `gh workflow run` returns no run id of its own.
|
|
110
|
+
const before = latestRun(repo, workflow)?.databaseId ?? 0;
|
|
111
|
+
info(`Dispatching ${workflow} on ${repo}${ref ? ` (${ref})` : ''}...`);
|
|
112
|
+
const dispatch = dispatchWorkflow(repo, workflow, ref);
|
|
113
|
+
if (!dispatch.ok) {
|
|
114
|
+
error(`Workflow dispatch failed: ${dispatch.message}`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
success('Workflow dispatched — the iOS build runs on a macOS runner (~15-30 min)');
|
|
118
|
+
// the queued run is not addressable straight away; poll briefly for it.
|
|
119
|
+
let run = latestRun(repo, workflow);
|
|
120
|
+
for (let i = 0; i < 10 && (!run || run.databaseId === before); i++) {
|
|
121
|
+
await sleep(3000);
|
|
122
|
+
run = latestRun(repo, workflow);
|
|
123
|
+
}
|
|
124
|
+
if (!run || run.databaseId === before) {
|
|
125
|
+
info(`Track it at https://github.com/${repo}/actions/workflows/${workflow}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
info(`Run: ${run.url}`);
|
|
129
|
+
if (watch) {
|
|
130
|
+
const code = watchRun(repo, run.databaseId);
|
|
131
|
+
if (code !== 0) {
|
|
132
|
+
error('The build workflow failed — see the run log above');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
success(`${app} built and uploaded to TestFlight`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function tfBuilds(args) {
|
|
139
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
140
|
+
const json = args.includes('--json');
|
|
141
|
+
if (!appName) {
|
|
142
|
+
error('Usage: fleet testflight builds <app> [--app-id <id>] [--json]');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
const { app } = resolveTestflightTarget(appName);
|
|
146
|
+
const env = appSecretsEnv(app);
|
|
147
|
+
const ascAppId = extractFlag(args, '--app-id') ?? env.ASC_APP_ID;
|
|
148
|
+
if (!ascAppId) {
|
|
149
|
+
error('App Store Connect app id required — set ASC_APP_ID or pass --app-id');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const builds = await listBuilds(resolveAscCredentials(env), ascAppId);
|
|
153
|
+
if (json) {
|
|
154
|
+
process.stdout.write(JSON.stringify(builds, null, 2) + '\n');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
heading(`TestFlight builds: ${app}`);
|
|
158
|
+
if (builds.length === 0) {
|
|
159
|
+
info('No builds found.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
table(['BUILD', 'VERSION', 'STATE', 'EXPIRED', 'UPLOADED'], builds.map(b => [
|
|
163
|
+
b.version,
|
|
164
|
+
b.shortVersion,
|
|
165
|
+
b.processingState,
|
|
166
|
+
b.expired ? 'yes' : 'no',
|
|
167
|
+
b.uploadedDate.slice(0, 10),
|
|
168
|
+
]));
|
|
169
|
+
process.stdout.write('\n');
|
|
170
|
+
}
|
|
171
|
+
async function tfUpdate(args) {
|
|
172
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
173
|
+
const buildId = extractFlag(args, '--build');
|
|
174
|
+
const whatsNew = extractFlag(args, '--whats-new');
|
|
175
|
+
if (!appName || !buildId || !whatsNew) {
|
|
176
|
+
error('Usage: fleet testflight update <app> --build <build-id> --whats-new "..."');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
const { app } = resolveTestflightTarget(appName);
|
|
180
|
+
await setWhatsNew(resolveAscCredentials(appSecretsEnv(app)), buildId, whatsNew);
|
|
181
|
+
success(`Updated the "What to Test" notes for build ${buildId}`);
|
|
182
|
+
}
|
|
183
|
+
async function tfDelete(args) {
|
|
184
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
185
|
+
const buildId = extractFlag(args, '--build');
|
|
186
|
+
if (!appName || !buildId) {
|
|
187
|
+
error('Usage: fleet testflight delete <app> --build <build-id>');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const { app } = resolveTestflightTarget(appName);
|
|
191
|
+
await expireBuild(resolveAscCredentials(appSecretsEnv(app)), buildId);
|
|
192
|
+
success(`Expired build ${buildId} — it is no longer installable from TestFlight`);
|
|
193
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface UpdateData {
|
|
2
|
+
channel: 'stable' | 'prerelease';
|
|
3
|
+
remoteBranch: string;
|
|
4
|
+
localBranch: string;
|
|
5
|
+
available: boolean;
|
|
6
|
+
behind: number;
|
|
7
|
+
latestSubject: string;
|
|
8
|
+
/** populated only when an apply actually ran. */
|
|
9
|
+
pulled?: number;
|
|
10
|
+
buildOk?: boolean;
|
|
11
|
+
output?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
/** CLI surface for self-update. equivalent to the TUI banner + U key, but
|
|
15
|
+
* driveable from a dumb terminal, a cron job, or a Claude session. */
|
|
16
|
+
export declare const updateCommand: import("../registry/types.js").CommandDef<UpdateData>;
|