@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/core/secrets.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const VAULT_DIR: string;
|
|
2
2
|
export declare const KEY_PATH = "/etc/fleet/age.key";
|
|
3
3
|
export declare const RUNTIME_DIR = "/run/fleet-secrets";
|
|
4
|
+
export declare const MANIFEST_PATH: string;
|
|
4
5
|
export interface SecretMetadata {
|
|
5
6
|
lastRotated: string;
|
|
6
7
|
provider?: string;
|
|
@@ -17,8 +18,10 @@ export interface ManifestEntry {
|
|
|
17
18
|
/** Per-secret metadata, keyed by secret name. Backwards-compatible: missing means
|
|
18
19
|
* lastRotated falls back to lastSealedAt and provider is auto-classified at read time. */
|
|
19
20
|
secrets?: Record<string, SecretMetadata>;
|
|
20
|
-
/**
|
|
21
|
+
/** per-app age recipient public key. required when mode === 'socket'. */
|
|
21
22
|
recipient?: string;
|
|
23
|
+
/** delivery mode. defaults to 'unseal' (v1 tmpfs path) when undefined for backward compat. */
|
|
24
|
+
mode?: 'unseal' | 'socket';
|
|
22
25
|
}
|
|
23
26
|
export interface Manifest {
|
|
24
27
|
version: number;
|
|
@@ -31,9 +34,41 @@ export declare function getPublicKey(): string;
|
|
|
31
34
|
export declare function initVault(): string;
|
|
32
35
|
export declare function loadManifest(): Manifest;
|
|
33
36
|
export declare function saveManifest(manifest: Manifest): void;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Run an arbitrary transaction under the manifest's inter-process lock.
|
|
39
|
+
* The callback receives no manifest — it's responsible for any load / save
|
|
40
|
+
* it needs (the seal flows do their own validate → backup → seal → cleanup
|
|
41
|
+
* dance and don't fit a simple load/mutate/save shape).
|
|
42
|
+
*
|
|
43
|
+
* Important: NOT reentrant. Wrap only the outermost RMW boundary; inner
|
|
44
|
+
* helpers should call plain `loadManifest` / `saveManifest`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function lockManifest<T>(fn: () => Promise<T> | T): Promise<T>;
|
|
47
|
+
/**
|
|
48
|
+
* Run a simple load → mutate → save transaction against the secrets
|
|
49
|
+
* manifest under an inter-process lock. Used by callers that need a
|
|
50
|
+
* straightforward RMW with no extra side-effects.
|
|
51
|
+
*
|
|
52
|
+
* The mutator may return a different Manifest object or mutate the input
|
|
53
|
+
* and return it. The returned value is persisted on a successful run; if
|
|
54
|
+
* the mutator throws, no save happens and the lock is released.
|
|
55
|
+
*
|
|
56
|
+
* Important: NOT reentrant. Wrap only the outermost RMW boundary; inner
|
|
57
|
+
* helpers should call plain `loadManifest` / `saveManifest`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function withManifest(fn: (m: Manifest) => Manifest | Promise<Manifest>): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Create a per-op backup of an app's encrypted vault file.
|
|
62
|
+
*
|
|
63
|
+
* Each call produces a unique `.bak-<tag>` path so concurrent operations do
|
|
64
|
+
* not silently overwrite each other's recovery point. If you don't supply a
|
|
65
|
+
* tag, one is generated from PID + timestamp + an in-process monotonic
|
|
66
|
+
* counter. Callers MUST keep the returned path and pass it to
|
|
67
|
+
* `restoreVaultFile` / `removeBackup`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function backupVaultFile(app: string, tag?: string): string | null;
|
|
70
|
+
export declare function restoreVaultFile(app: string, bakPath?: string): boolean;
|
|
71
|
+
export declare function removeBackup(app: string, bakPath?: string): void;
|
|
37
72
|
export declare function ageEncrypt(plaintext: string): string;
|
|
38
73
|
export declare function ageDecrypt(ciphertext: string | Buffer): string;
|
|
39
74
|
export declare function ageDecryptFile(filePath: string): string;
|
package/dist/core/secrets.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, chmodSync, rmSync, copyFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, chmodSync, statSync, rmSync, copyFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { SecretsError, VaultNotInitializedError } from './errors.js';
|
|
5
5
|
import { execSafe } from './exec.js';
|
|
6
6
|
import { assertAppName, assertFilePath } from './validate.js';
|
|
7
|
+
import { withFileLock } from './file-lock.js';
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
|
+
// vault dir resolves from FLEET_VAULT_DIR if set; otherwise falls back to
|
|
10
|
+
// the repo-local vault dir for dev. captures at module load — tests that
|
|
11
|
+
// want to override need to set the env var before importing. matches the
|
|
12
|
+
// pattern used in secrets-v2-cleanup.ts.
|
|
13
|
+
export const VAULT_DIR = process.env.FLEET_VAULT_DIR
|
|
14
|
+
?? join(__dirname, '..', '..', 'vault');
|
|
9
15
|
export const KEY_PATH = '/etc/fleet/age.key';
|
|
10
16
|
export const RUNTIME_DIR = '/run/fleet-secrets';
|
|
11
|
-
const MANIFEST_PATH = join(VAULT_DIR, 'manifest.json');
|
|
17
|
+
export const MANIFEST_PATH = join(VAULT_DIR, 'manifest.json');
|
|
12
18
|
const SECRET_DELIMITER = '---SECRET:';
|
|
13
19
|
export function ensureAge() {
|
|
14
20
|
if (!execSafe('which', ['age']).ok) {
|
|
@@ -64,7 +70,50 @@ export function loadManifest() {
|
|
|
64
70
|
export function saveManifest(manifest) {
|
|
65
71
|
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
|
|
66
72
|
}
|
|
67
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Run an arbitrary transaction under the manifest's inter-process lock.
|
|
75
|
+
* The callback receives no manifest — it's responsible for any load / save
|
|
76
|
+
* it needs (the seal flows do their own validate → backup → seal → cleanup
|
|
77
|
+
* dance and don't fit a simple load/mutate/save shape).
|
|
78
|
+
*
|
|
79
|
+
* Important: NOT reentrant. Wrap only the outermost RMW boundary; inner
|
|
80
|
+
* helpers should call plain `loadManifest` / `saveManifest`.
|
|
81
|
+
*/
|
|
82
|
+
export async function lockManifest(fn) {
|
|
83
|
+
return withFileLock(MANIFEST_PATH, fn);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Run a simple load → mutate → save transaction against the secrets
|
|
87
|
+
* manifest under an inter-process lock. Used by callers that need a
|
|
88
|
+
* straightforward RMW with no extra side-effects.
|
|
89
|
+
*
|
|
90
|
+
* The mutator may return a different Manifest object or mutate the input
|
|
91
|
+
* and return it. The returned value is persisted on a successful run; if
|
|
92
|
+
* the mutator throws, no save happens and the lock is released.
|
|
93
|
+
*
|
|
94
|
+
* Important: NOT reentrant. Wrap only the outermost RMW boundary; inner
|
|
95
|
+
* helpers should call plain `loadManifest` / `saveManifest`.
|
|
96
|
+
*/
|
|
97
|
+
export async function withManifest(fn) {
|
|
98
|
+
await lockManifest(async () => {
|
|
99
|
+
const manifest = loadManifest();
|
|
100
|
+
const next = await fn(manifest);
|
|
101
|
+
saveManifest(next);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// monotonic counter so two backupVaultFile calls in the same millisecond
|
|
105
|
+
// (same process, same default-tag path) still produce distinct bak filenames.
|
|
106
|
+
let backupSeq = 0;
|
|
107
|
+
/**
|
|
108
|
+
* Create a per-op backup of an app's encrypted vault file.
|
|
109
|
+
*
|
|
110
|
+
* Each call produces a unique `.bak-<tag>` path so concurrent operations do
|
|
111
|
+
* not silently overwrite each other's recovery point. If you don't supply a
|
|
112
|
+
* tag, one is generated from PID + timestamp + an in-process monotonic
|
|
113
|
+
* counter. Callers MUST keep the returned path and pass it to
|
|
114
|
+
* `restoreVaultFile` / `removeBackup`.
|
|
115
|
+
*/
|
|
116
|
+
export function backupVaultFile(app, tag) {
|
|
68
117
|
const manifest = loadManifest();
|
|
69
118
|
const entry = manifest.apps[app];
|
|
70
119
|
if (!entry)
|
|
@@ -72,30 +121,61 @@ export function backupVaultFile(app) {
|
|
|
72
121
|
const src = join(VAULT_DIR, entry.encryptedFile);
|
|
73
122
|
if (!existsSync(src))
|
|
74
123
|
return null;
|
|
75
|
-
const
|
|
124
|
+
const t = tag ?? `${process.pid}-${Date.now()}-${++backupSeq}`;
|
|
125
|
+
const bak = `${src}.bak-${t}`;
|
|
76
126
|
copyFileSync(src, bak);
|
|
77
127
|
return bak;
|
|
78
128
|
}
|
|
79
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Find the newest `<encryptedFile>.bak-*` for an app, if any. Returns the full
|
|
131
|
+
* path (in VAULT_DIR) or null. Used as a best-effort fallback when no specific
|
|
132
|
+
* backup path was supplied (e.g. the simple CLI / MCP `secrets restore` flow).
|
|
133
|
+
*/
|
|
134
|
+
function findNewestBackup(encryptedFile) {
|
|
135
|
+
if (!existsSync(VAULT_DIR))
|
|
136
|
+
return null;
|
|
137
|
+
const prefix = `${encryptedFile}.bak-`;
|
|
138
|
+
let newest = null;
|
|
139
|
+
for (const name of readdirSync(VAULT_DIR)) {
|
|
140
|
+
if (!name.startsWith(prefix))
|
|
141
|
+
continue;
|
|
142
|
+
const full = join(VAULT_DIR, name);
|
|
143
|
+
try {
|
|
144
|
+
const m = statSync(full).mtimeMs;
|
|
145
|
+
if (!newest || m > newest.mtime)
|
|
146
|
+
newest = { path: full, mtime: m };
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// ignore stat failures — file may have been removed mid-scan
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return newest ? newest.path : null;
|
|
153
|
+
}
|
|
154
|
+
export function restoreVaultFile(app, bakPath) {
|
|
80
155
|
const manifest = loadManifest();
|
|
81
156
|
const entry = manifest.apps[app];
|
|
82
157
|
if (!entry)
|
|
83
158
|
return false;
|
|
84
159
|
const src = join(VAULT_DIR, entry.encryptedFile);
|
|
85
|
-
const bak =
|
|
86
|
-
if (!existsSync(bak))
|
|
160
|
+
const bak = bakPath ?? findNewestBackup(entry.encryptedFile);
|
|
161
|
+
if (!bak || !existsSync(bak))
|
|
87
162
|
return false;
|
|
88
163
|
copyFileSync(bak, src);
|
|
89
164
|
rmSync(bak, { force: true });
|
|
90
165
|
return true;
|
|
91
166
|
}
|
|
92
|
-
export function removeBackup(app) {
|
|
167
|
+
export function removeBackup(app, bakPath) {
|
|
168
|
+
if (bakPath) {
|
|
169
|
+
rmSync(bakPath, { force: true });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
93
172
|
const manifest = loadManifest();
|
|
94
173
|
const entry = manifest.apps[app];
|
|
95
174
|
if (!entry)
|
|
96
175
|
return;
|
|
97
|
-
const bak =
|
|
98
|
-
|
|
176
|
+
const bak = findNewestBackup(entry.encryptedFile);
|
|
177
|
+
if (bak)
|
|
178
|
+
rmSync(bak, { force: true });
|
|
99
179
|
}
|
|
100
180
|
export function ageEncrypt(plaintext) {
|
|
101
181
|
const pubkey = getPublicKey();
|
|
@@ -2,24 +2,45 @@
|
|
|
2
2
|
* Self-update check and apply for fleet itself.
|
|
3
3
|
*
|
|
4
4
|
* fleet is installed via `npm link`-style symlink from /usr/local/bin/fleet to
|
|
5
|
-
*
|
|
6
|
-
* 1. git
|
|
7
|
-
* 2.
|
|
5
|
+
* the repo's dist/index.js. Updates are produced by:
|
|
6
|
+
* 1. git fetch origin <channel> (channel = main by default, develop on opt-in)
|
|
7
|
+
* 2. git pull --ff-only origin <channel> in the fleet checkout
|
|
8
|
+
* 3. npm run build (rewrites dist/)
|
|
9
|
+
*
|
|
10
|
+
* Channel selection:
|
|
11
|
+
* - default: 'stable' → tracks origin/main (tagged releases only).
|
|
12
|
+
* - FLEET_UPDATE_CHANNEL=prerelease → tracks origin/develop (work in flight).
|
|
13
|
+
* - FLEET_UPDATE_BRANCH=<name> → arbitrary branch (escape hatch for forks).
|
|
14
|
+
*
|
|
15
|
+
* The check intentionally compares against the configured remote branch, not
|
|
16
|
+
* the local HEAD's tracking branch — so even if the local checkout is on
|
|
17
|
+
* `develop` the operator can opt back to the stable channel without first
|
|
18
|
+
* switching branches.
|
|
8
19
|
*
|
|
9
20
|
* checkForUpdate() does a non-blocking `git fetch` + compares HEAD with the
|
|
10
21
|
* remote. applyUpdate() runs the pull + build. Both are pure shell wrappers
|
|
11
22
|
* around execSafe — easy to mock in tests, easy to reason about under sudo.
|
|
12
23
|
*/
|
|
24
|
+
export type UpdateChannel = 'stable' | 'prerelease';
|
|
25
|
+
/** resolve the remote branch to track based on env vars. */
|
|
26
|
+
export declare function resolveChannel(): {
|
|
27
|
+
channel: UpdateChannel;
|
|
28
|
+
branch: string;
|
|
29
|
+
};
|
|
13
30
|
export interface UpdateInfo {
|
|
14
|
-
/**
|
|
31
|
+
/** true if `git rev-parse @{u}` shows commits ahead of HEAD. */
|
|
15
32
|
available: boolean;
|
|
16
|
-
/**
|
|
33
|
+
/** number of commits HEAD is behind the configured remote branch. */
|
|
17
34
|
behind: number;
|
|
18
|
-
/**
|
|
35
|
+
/** short subject of the latest remote commit (or empty string on failure). */
|
|
19
36
|
latestSubject: string;
|
|
20
|
-
/**
|
|
37
|
+
/** local branch in the working tree. */
|
|
21
38
|
branch: string;
|
|
22
|
-
/**
|
|
39
|
+
/** remote branch being tracked for updates (e.g. 'main' or 'develop'). */
|
|
40
|
+
remoteBranch: string;
|
|
41
|
+
/** stable = main (tagged releases), prerelease = develop (work in flight). */
|
|
42
|
+
channel: UpdateChannel;
|
|
43
|
+
/** why the check failed, if it did. */
|
|
23
44
|
error?: string;
|
|
24
45
|
}
|
|
25
46
|
export interface UpdateResult {
|
|
@@ -34,8 +55,8 @@ export interface UpdateResult {
|
|
|
34
55
|
*/
|
|
35
56
|
export declare function checkForUpdate(): Promise<UpdateInfo>;
|
|
36
57
|
/**
|
|
37
|
-
* Apply: git pull --ff-only + npm run build. Refuses
|
|
38
|
-
* tree is dirty (would clobber uncommitted changes).
|
|
39
|
-
* for the toast / TUI to surface.
|
|
58
|
+
* Apply: git pull --ff-only origin <channel-branch> + npm run build. Refuses
|
|
59
|
+
* to run if the working tree is dirty (would clobber uncommitted changes).
|
|
60
|
+
* Returns aggregate output for the toast / TUI to surface.
|
|
40
61
|
*/
|
|
41
62
|
export declare function applyUpdate(): Promise<UpdateResult>;
|
package/dist/core/self-update.js
CHANGED
|
@@ -2,9 +2,20 @@
|
|
|
2
2
|
* Self-update check and apply for fleet itself.
|
|
3
3
|
*
|
|
4
4
|
* fleet is installed via `npm link`-style symlink from /usr/local/bin/fleet to
|
|
5
|
-
*
|
|
6
|
-
* 1. git
|
|
7
|
-
* 2.
|
|
5
|
+
* the repo's dist/index.js. Updates are produced by:
|
|
6
|
+
* 1. git fetch origin <channel> (channel = main by default, develop on opt-in)
|
|
7
|
+
* 2. git pull --ff-only origin <channel> in the fleet checkout
|
|
8
|
+
* 3. npm run build (rewrites dist/)
|
|
9
|
+
*
|
|
10
|
+
* Channel selection:
|
|
11
|
+
* - default: 'stable' → tracks origin/main (tagged releases only).
|
|
12
|
+
* - FLEET_UPDATE_CHANNEL=prerelease → tracks origin/develop (work in flight).
|
|
13
|
+
* - FLEET_UPDATE_BRANCH=<name> → arbitrary branch (escape hatch for forks).
|
|
14
|
+
*
|
|
15
|
+
* The check intentionally compares against the configured remote branch, not
|
|
16
|
+
* the local HEAD's tracking branch — so even if the local checkout is on
|
|
17
|
+
* `develop` the operator can opt back to the stable channel without first
|
|
18
|
+
* switching branches.
|
|
8
19
|
*
|
|
9
20
|
* checkForUpdate() does a non-blocking `git fetch` + compares HEAD with the
|
|
10
21
|
* remote. applyUpdate() runs the pull + build. Both are pure shell wrappers
|
|
@@ -16,39 +27,66 @@ import { execSafe } from './exec.js';
|
|
|
16
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
28
|
// dist/core/self-update.js → repo root is two ../
|
|
18
29
|
const FLEET_REPO = process.env.FLEET_REPO_PATH ?? `${__dirname}/../..`;
|
|
30
|
+
/** resolve the remote branch to track based on env vars. */
|
|
31
|
+
export function resolveChannel() {
|
|
32
|
+
// explicit branch override wins — for forks or custom workflows.
|
|
33
|
+
const explicit = process.env.FLEET_UPDATE_BRANCH;
|
|
34
|
+
if (explicit) {
|
|
35
|
+
const channel = explicit === 'develop' ? 'prerelease' : 'stable';
|
|
36
|
+
return { channel, branch: explicit };
|
|
37
|
+
}
|
|
38
|
+
if (process.env.FLEET_UPDATE_CHANNEL === 'prerelease') {
|
|
39
|
+
return { channel: 'prerelease', branch: 'develop' };
|
|
40
|
+
}
|
|
41
|
+
return { channel: 'stable', branch: 'main' };
|
|
42
|
+
}
|
|
19
43
|
/**
|
|
20
44
|
* Non-blocking check. Does a `git fetch` (timeboxed) then compares.
|
|
21
45
|
* Returns a stable UpdateInfo even on failure (just `available=false`).
|
|
22
46
|
*/
|
|
23
47
|
export async function checkForUpdate() {
|
|
48
|
+
const { channel, branch: remoteBranch } = resolveChannel();
|
|
24
49
|
const branchR = execSafe('git', ['-C', FLEET_REPO, 'rev-parse', '--abbrev-ref', 'HEAD']);
|
|
25
50
|
if (!branchR.ok) {
|
|
26
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
available: false, behind: 0, latestSubject: '',
|
|
53
|
+
branch: '?', remoteBranch, channel,
|
|
54
|
+
error: branchR.stderr,
|
|
55
|
+
};
|
|
27
56
|
}
|
|
28
57
|
const branch = branchR.stdout;
|
|
29
58
|
// Fetch quietly, with a short timeout so we never block the TUI launch.
|
|
30
|
-
const fetchR = execSafe('git', ['-C', FLEET_REPO, 'fetch', '--quiet', 'origin',
|
|
59
|
+
const fetchR = execSafe('git', ['-C', FLEET_REPO, 'fetch', '--quiet', 'origin', remoteBranch], { timeout: 8_000 });
|
|
31
60
|
if (!fetchR.ok) {
|
|
32
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
available: false, behind: 0, latestSubject: '',
|
|
63
|
+
branch, remoteBranch, channel,
|
|
64
|
+
error: 'fetch failed',
|
|
65
|
+
};
|
|
33
66
|
}
|
|
34
|
-
const countR = execSafe('git', ['-C', FLEET_REPO, 'rev-list', '--count', `HEAD..origin/${
|
|
67
|
+
const countR = execSafe('git', ['-C', FLEET_REPO, 'rev-list', '--count', `HEAD..origin/${remoteBranch}`]);
|
|
35
68
|
if (!countR.ok) {
|
|
36
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
available: false, behind: 0, latestSubject: '',
|
|
71
|
+
branch, remoteBranch, channel,
|
|
72
|
+
error: countR.stderr,
|
|
73
|
+
};
|
|
37
74
|
}
|
|
38
75
|
const behind = parseInt(countR.stdout, 10) || 0;
|
|
39
76
|
let latestSubject = '';
|
|
40
77
|
if (behind > 0) {
|
|
41
|
-
const subR = execSafe('git', ['-C', FLEET_REPO, 'log', '-1', '--pretty=%s', `origin/${
|
|
78
|
+
const subR = execSafe('git', ['-C', FLEET_REPO, 'log', '-1', '--pretty=%s', `origin/${remoteBranch}`]);
|
|
42
79
|
latestSubject = subR.ok ? subR.stdout : '';
|
|
43
80
|
}
|
|
44
|
-
return { available: behind > 0, behind, latestSubject, branch };
|
|
81
|
+
return { available: behind > 0, behind, latestSubject, branch, remoteBranch, channel };
|
|
45
82
|
}
|
|
46
83
|
/**
|
|
47
|
-
* Apply: git pull --ff-only + npm run build. Refuses
|
|
48
|
-
* tree is dirty (would clobber uncommitted changes).
|
|
49
|
-
* for the toast / TUI to surface.
|
|
84
|
+
* Apply: git pull --ff-only origin <channel-branch> + npm run build. Refuses
|
|
85
|
+
* to run if the working tree is dirty (would clobber uncommitted changes).
|
|
86
|
+
* Returns aggregate output for the toast / TUI to surface.
|
|
50
87
|
*/
|
|
51
88
|
export async function applyUpdate() {
|
|
89
|
+
const { branch: remoteBranch } = resolveChannel();
|
|
52
90
|
const dirty = execSafe('git', ['-C', FLEET_REPO, 'status', '--porcelain']);
|
|
53
91
|
if (dirty.ok && dirty.stdout.length > 0) {
|
|
54
92
|
return {
|
|
@@ -57,7 +95,7 @@ export async function applyUpdate() {
|
|
|
57
95
|
};
|
|
58
96
|
}
|
|
59
97
|
const pre = execSafe('git', ['-C', FLEET_REPO, 'rev-parse', 'HEAD']);
|
|
60
|
-
const pull = execSafe('git', ['-C', FLEET_REPO, 'pull', '--ff-only'], { timeout: 30_000 });
|
|
98
|
+
const pull = execSafe('git', ['-C', FLEET_REPO, 'pull', '--ff-only', 'origin', remoteBranch], { timeout: 30_000 });
|
|
61
99
|
if (!pull.ok) {
|
|
62
100
|
return { ok: false, pulled: 0, buildOk: false, output: pull.stderr || pull.stdout };
|
|
63
101
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AscCredentials, TestflightBuild } from './types.js';
|
|
2
|
+
export declare function ascJwt(creds: AscCredentials, now?: number): string;
|
|
3
|
+
interface AscRequestOptions {
|
|
4
|
+
method?: string;
|
|
5
|
+
body?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function ascRequest(creds: AscCredentials, path: string, opts?: AscRequestOptions): Promise<unknown>;
|
|
8
|
+
export declare function listBuilds(creds: AscCredentials, ascAppId: string, limit?: number): Promise<TestflightBuild[]>;
|
|
9
|
+
export declare function expireBuild(creds: AscCredentials, buildId: string): Promise<void>;
|
|
10
|
+
export declare function setWhatsNew(creds: AscCredentials, buildId: string, whatsNew: string, locale?: string): Promise<void>;
|
|
11
|
+
export declare function verifyApp(creds: AscCredentials, ascAppId: string): Promise<string>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createSign } from 'node:crypto';
|
|
2
|
+
import { FleetError } from '../errors.js';
|
|
3
|
+
const ASC_BASE = 'https://api.appstoreconnect.apple.com';
|
|
4
|
+
function base64url(input) {
|
|
5
|
+
return Buffer.from(input)
|
|
6
|
+
.toString('base64')
|
|
7
|
+
.replace(/\+/g, '-')
|
|
8
|
+
.replace(/\//g, '_')
|
|
9
|
+
.replace(/=+$/, '');
|
|
10
|
+
}
|
|
11
|
+
// sign a short-lived ES256 jwt for the app store connect api. the lifetime
|
|
12
|
+
// is held well under apple's 20-minute ceiling.
|
|
13
|
+
export function ascJwt(creds, now = Date.now()) {
|
|
14
|
+
const iat = Math.floor(now / 1000);
|
|
15
|
+
const header = { alg: 'ES256', kid: creds.keyId, typ: 'JWT' };
|
|
16
|
+
const payload = { iss: creds.issuerId, iat, exp: iat + 600, aud: 'appstoreconnect-v1' };
|
|
17
|
+
const signingInput = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(payload))}`;
|
|
18
|
+
const signer = createSign('SHA256');
|
|
19
|
+
signer.update(signingInput);
|
|
20
|
+
// apple expects the raw r||s signature (ieee-p1363), not asn.1/der.
|
|
21
|
+
const signature = signer.sign({ key: creds.privateKey, dsaEncoding: 'ieee-p1363' });
|
|
22
|
+
return `${signingInput}.${base64url(signature)}`;
|
|
23
|
+
}
|
|
24
|
+
// perform an authenticated app store connect api request. a non-2xx response
|
|
25
|
+
// is surfaced as a FleetError carrying apple's first error detail.
|
|
26
|
+
export async function ascRequest(creds, path, opts = {}) {
|
|
27
|
+
const res = await fetch(`${ASC_BASE}${path}`, {
|
|
28
|
+
method: opts.method ?? 'GET',
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${ascJwt(creds)}`,
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
},
|
|
33
|
+
...(opts.body !== undefined && { body: JSON.stringify(opts.body) }),
|
|
34
|
+
});
|
|
35
|
+
if (res.status === 204)
|
|
36
|
+
return null;
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
const json = text ? JSON.parse(text) : null;
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const detail = json?.errors?.[0]?.detail;
|
|
41
|
+
throw new FleetError(`App Store Connect API ${res.status}: ${detail ?? text.slice(0, 200)}`);
|
|
42
|
+
}
|
|
43
|
+
return json;
|
|
44
|
+
}
|
|
45
|
+
// list builds for an app store connect app, newest upload first.
|
|
46
|
+
export async function listBuilds(creds, ascAppId, limit = 20) {
|
|
47
|
+
const query = `filter[app]=${encodeURIComponent(ascAppId)}&sort=-uploadedDate` +
|
|
48
|
+
`&limit=${limit}&include=preReleaseVersion`;
|
|
49
|
+
const res = (await ascRequest(creds, `/v1/builds?${query}`));
|
|
50
|
+
const preReleaseVersions = new Map((res.included ?? [])
|
|
51
|
+
.filter(i => i.type === 'preReleaseVersions')
|
|
52
|
+
.map(i => [i.id, i.attributes?.version ?? '']));
|
|
53
|
+
return (res.data ?? []).map(b => ({
|
|
54
|
+
id: b.id,
|
|
55
|
+
version: b.attributes?.version ?? '',
|
|
56
|
+
shortVersion: preReleaseVersions.get(b.relationships?.preReleaseVersion?.data?.id ?? '') ?? '',
|
|
57
|
+
processingState: b.attributes?.processingState ?? 'UNKNOWN',
|
|
58
|
+
expired: b.attributes?.expired ?? false,
|
|
59
|
+
uploadedDate: b.attributes?.uploadedDate ?? '',
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
// expire a build — the closest the api offers to "delete". an expired build
|
|
63
|
+
// leaves testflight and can no longer be installed by testers.
|
|
64
|
+
export async function expireBuild(creds, buildId) {
|
|
65
|
+
await ascRequest(creds, `/v1/builds/${buildId}`, {
|
|
66
|
+
method: 'PATCH',
|
|
67
|
+
body: { data: { type: 'builds', id: buildId, attributes: { expired: true } } },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// set the "what to test" notes for a build, creating the beta localisation
|
|
71
|
+
// when the build has none for the requested locale yet.
|
|
72
|
+
export async function setWhatsNew(creds, buildId, whatsNew, locale = 'en-GB') {
|
|
73
|
+
const existing = (await ascRequest(creds, `/v1/builds/${buildId}/betaBuildLocalizations`));
|
|
74
|
+
const match = (existing.data ?? []).find(l => l.attributes?.locale === locale) ??
|
|
75
|
+
(existing.data ?? [])[0];
|
|
76
|
+
if (match) {
|
|
77
|
+
await ascRequest(creds, `/v1/betaBuildLocalizations/${match.id}`, {
|
|
78
|
+
method: 'PATCH',
|
|
79
|
+
body: {
|
|
80
|
+
data: { type: 'betaBuildLocalizations', id: match.id, attributes: { whatsNew } },
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await ascRequest(creds, '/v1/betaBuildLocalizations', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
body: {
|
|
88
|
+
data: {
|
|
89
|
+
type: 'betaBuildLocalizations',
|
|
90
|
+
attributes: { locale, whatsNew },
|
|
91
|
+
relationships: { build: { data: { type: 'builds', id: buildId } } },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// fetch an app's name — a cheap call used to verify credentials and the
|
|
97
|
+
// configured app id resolve.
|
|
98
|
+
export async function verifyApp(creds, ascAppId) {
|
|
99
|
+
const res = (await ascRequest(creds, `/v1/apps/${ascAppId}`));
|
|
100
|
+
return res.data?.attributes?.name ?? '(unknown)';
|
|
101
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { FleetError } from '../errors.js';
|
|
3
|
+
// resolve app store connect api credentials from an environment map. the
|
|
4
|
+
// private key is supplied either inline-base64 (ASC_API_KEY_B64) or as a path
|
|
5
|
+
// to a .p8 file (ASC_API_KEY_PATH) — base64 is preferred so the key lives in
|
|
6
|
+
// the fleet secrets vault rather than as a loose file on disk.
|
|
7
|
+
export function resolveAscCredentials(env) {
|
|
8
|
+
const keyId = env.ASC_API_KEY_ID;
|
|
9
|
+
const issuerId = env.ASC_API_KEY_ISSUER_ID;
|
|
10
|
+
if (!keyId || !issuerId) {
|
|
11
|
+
throw new FleetError('App Store Connect credentials missing — set ASC_API_KEY_ID and ASC_API_KEY_ISSUER_ID.');
|
|
12
|
+
}
|
|
13
|
+
let privateKey;
|
|
14
|
+
if (env.ASC_API_KEY_B64) {
|
|
15
|
+
privateKey = Buffer.from(env.ASC_API_KEY_B64, 'base64').toString('utf-8');
|
|
16
|
+
}
|
|
17
|
+
else if (env.ASC_API_KEY_PATH && existsSync(env.ASC_API_KEY_PATH)) {
|
|
18
|
+
privateKey = readFileSync(env.ASC_API_KEY_PATH, 'utf-8');
|
|
19
|
+
}
|
|
20
|
+
if (!privateKey || !privateKey.includes('PRIVATE KEY')) {
|
|
21
|
+
throw new FleetError('App Store Connect private key missing — set ASC_API_KEY_B64 (base64 of the .p8) ' +
|
|
22
|
+
'or ASC_API_KEY_PATH (path to the .p8 file).');
|
|
23
|
+
}
|
|
24
|
+
return { keyId, issuerId, privateKey };
|
|
25
|
+
}
|
|
26
|
+
// true when full app store connect credentials are present in `env`.
|
|
27
|
+
export function hasAscCredentials(env) {
|
|
28
|
+
try {
|
|
29
|
+
resolveAscCredentials(env);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { EasEnv } from './types.js';
|
|
2
|
+
export declare function easVersion(): string | null;
|
|
3
|
+
export declare function easBuild(projectPath: string, profile: string, env: EasEnv): number;
|
|
4
|
+
export declare function easSubmit(projectPath: string, profile: string, env: EasEnv): number;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { execSafe } from '../exec.js';
|
|
3
|
+
// the eas cli is invoked through npx so a global install isn't required.
|
|
4
|
+
function npxArgs(rest) {
|
|
5
|
+
return ['--yes', 'eas-cli', ...rest];
|
|
6
|
+
}
|
|
7
|
+
// version string of the eas cli, or null when it can't be resolved.
|
|
8
|
+
export function easVersion() {
|
|
9
|
+
const res = execSafe('npx', npxArgs(['--version']), { timeout: 120_000 });
|
|
10
|
+
if (!res.ok || !res.stdout)
|
|
11
|
+
return null;
|
|
12
|
+
return res.stdout.split('\n').map(l => l.trim()).filter(Boolean).pop() ?? null;
|
|
13
|
+
}
|
|
14
|
+
// run an eas cli subcommand in the mobile project with stdio inherited so a
|
|
15
|
+
// long-running build/submit streams its progress live. returns the exit code.
|
|
16
|
+
function easLive(projectPath, env, rest) {
|
|
17
|
+
const result = spawnSync('npx', npxArgs(rest), {
|
|
18
|
+
cwd: projectPath,
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
env: { ...process.env, ...env },
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
});
|
|
23
|
+
return result.status ?? 1;
|
|
24
|
+
}
|
|
25
|
+
// build the ios app for the given eas profile.
|
|
26
|
+
export function easBuild(projectPath, profile, env) {
|
|
27
|
+
return easLive(projectPath, env, [
|
|
28
|
+
'build', '--platform', 'ios', '--profile', profile, '--non-interactive',
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
// submit the latest ios build to testflight. when no app store connect record
|
|
32
|
+
// exists for the bundle id yet, eas submit creates one — this is the "new
|
|
33
|
+
// entry" path.
|
|
34
|
+
export function easSubmit(projectPath, profile, env) {
|
|
35
|
+
return easLive(projectPath, env, [
|
|
36
|
+
'submit', '--platform', 'ios', '--profile', profile, '--non-interactive',
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { load, findApp } from '../registry.js';
|
|
4
|
+
import { FleetError } from '../errors.js';
|
|
5
|
+
const SECRETS_BASE = '/run/fleet-secrets';
|
|
6
|
+
// resolve a registered fleet app to its mobile project directory. testflight
|
|
7
|
+
// targets must be registered apps — the credentials live in the app's vault.
|
|
8
|
+
export function resolveTestflightTarget(target) {
|
|
9
|
+
const app = findApp(load(), target);
|
|
10
|
+
if (!app) {
|
|
11
|
+
throw new FleetError(`Unknown app "${target}" — not in the fleet registry.`);
|
|
12
|
+
}
|
|
13
|
+
const mobileDir = join(app.composePath, 'mobile');
|
|
14
|
+
return {
|
|
15
|
+
app: app.name,
|
|
16
|
+
projectPath: existsSync(mobileDir) ? mobileDir : app.composePath,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// minimal .env reader for an app's unsealed fleet secrets.
|
|
20
|
+
function readEnvFile(path) {
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return {};
|
|
23
|
+
const vars = {};
|
|
24
|
+
for (const line of readFileSync(path, 'utf-8').split('\n')) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
27
|
+
continue;
|
|
28
|
+
const eq = trimmed.indexOf('=');
|
|
29
|
+
if (eq < 1)
|
|
30
|
+
continue;
|
|
31
|
+
let val = trimmed.slice(eq + 1);
|
|
32
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
33
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
34
|
+
val = val.slice(1, -1);
|
|
35
|
+
}
|
|
36
|
+
vars[trimmed.slice(0, eq)] = val;
|
|
37
|
+
}
|
|
38
|
+
return vars;
|
|
39
|
+
}
|
|
40
|
+
// an app's unsealed fleet secrets layered over the current process env. the
|
|
41
|
+
// vault holds the App Store Connect / Expo credentials testflight needs.
|
|
42
|
+
export function appSecretsEnv(app) {
|
|
43
|
+
return { ...process.env, ...readEnvFile(join(SECRETS_BASE, app, '.env')) };
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AscCredentials {
|
|
2
|
+
keyId: string;
|
|
3
|
+
issuerId: string;
|
|
4
|
+
privateKey: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TestflightBuild {
|
|
7
|
+
id: string;
|
|
8
|
+
version: string;
|
|
9
|
+
shortVersion: string;
|
|
10
|
+
processingState: string;
|
|
11
|
+
expired: boolean;
|
|
12
|
+
uploadedDate: string;
|
|
13
|
+
}
|