@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.
Files changed (233) hide show
  1. package/README.md +186 -16
  2. package/dist/bin/fleet-agent.d.ts +2 -0
  3. package/dist/bin/fleet-agent.js +7 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +73 -31
  6. package/dist/commands/add.d.ts +2 -1
  7. package/dist/commands/add.js +66 -59
  8. package/dist/commands/audit.d.ts +1 -0
  9. package/dist/commands/audit.js +144 -0
  10. package/dist/commands/backup.d.ts +1 -0
  11. package/dist/commands/backup.js +510 -0
  12. package/dist/commands/boot-start.d.ts +3 -1
  13. package/dist/commands/boot-start.js +39 -47
  14. package/dist/commands/completions.d.ts +6 -0
  15. package/dist/commands/completions.js +83 -0
  16. package/dist/commands/config.d.ts +16 -0
  17. package/dist/commands/config.js +96 -0
  18. package/dist/commands/deploy.js +3 -2
  19. package/dist/commands/deps.js +5 -1
  20. package/dist/commands/doctor.d.ts +32 -0
  21. package/dist/commands/doctor.js +186 -0
  22. package/dist/commands/egress.d.ts +1 -1
  23. package/dist/commands/egress.js +13 -10
  24. package/dist/commands/freeze.d.ts +8 -4
  25. package/dist/commands/freeze.js +77 -59
  26. package/dist/commands/git.js +2 -2
  27. package/dist/commands/health.d.ts +2 -1
  28. package/dist/commands/health.js +38 -56
  29. package/dist/commands/init.d.ts +2 -1
  30. package/dist/commands/init.js +83 -73
  31. package/dist/commands/install-mcp.d.ts +3 -1
  32. package/dist/commands/install-mcp.js +53 -34
  33. package/dist/commands/list.d.ts +2 -1
  34. package/dist/commands/list.js +22 -19
  35. package/dist/commands/logs.js +1 -1
  36. package/dist/commands/notify.d.ts +1 -0
  37. package/dist/commands/notify.js +51 -0
  38. package/dist/commands/patch-systemd.d.ts +7 -1
  39. package/dist/commands/patch-systemd.js +71 -31
  40. package/dist/commands/remove.d.ts +3 -1
  41. package/dist/commands/remove.js +37 -26
  42. package/dist/commands/restart.d.ts +4 -1
  43. package/dist/commands/restart.js +17 -20
  44. package/dist/commands/rollback.d.ts +4 -1
  45. package/dist/commands/rollback.js +33 -42
  46. package/dist/commands/secrets.js +157 -9
  47. package/dist/commands/start.d.ts +4 -1
  48. package/dist/commands/start.js +17 -20
  49. package/dist/commands/status.d.ts +1 -1
  50. package/dist/commands/status.js +21 -26
  51. package/dist/commands/stop.d.ts +4 -1
  52. package/dist/commands/stop.js +17 -20
  53. package/dist/commands/testflight.d.ts +1 -0
  54. package/dist/commands/testflight.js +193 -0
  55. package/dist/commands/update.d.ts +16 -0
  56. package/dist/commands/update.js +95 -0
  57. package/dist/core/audit/cache.d.ts +4 -0
  58. package/dist/core/audit/cache.js +37 -0
  59. package/dist/core/audit/config.d.ts +5 -0
  60. package/dist/core/audit/config.js +35 -0
  61. package/dist/core/audit/greenlight.d.ts +11 -0
  62. package/dist/core/audit/greenlight.js +81 -0
  63. package/dist/core/audit/reporters/cli.d.ts +3 -0
  64. package/dist/core/audit/reporters/cli.js +68 -0
  65. package/dist/core/audit/suppress.d.ts +6 -0
  66. package/dist/core/audit/suppress.js +37 -0
  67. package/dist/core/audit/target.d.ts +5 -0
  68. package/dist/core/audit/target.js +26 -0
  69. package/dist/core/audit/types.d.ts +54 -0
  70. package/dist/core/audit/types.js +5 -0
  71. package/dist/core/backup/browser-api.d.ts +66 -0
  72. package/dist/core/backup/browser-api.js +197 -0
  73. package/dist/core/backup/browser-server.d.ts +11 -0
  74. package/dist/core/backup/browser-server.js +241 -0
  75. package/dist/core/backup/browser-ui.d.ts +5 -0
  76. package/dist/core/backup/browser-ui.js +268 -0
  77. package/dist/core/backup/cloudflare.d.ts +7 -0
  78. package/dist/core/backup/cloudflare.js +82 -0
  79. package/dist/core/backup/config.d.ts +9 -0
  80. package/dist/core/backup/config.js +80 -0
  81. package/dist/core/backup/detect.d.ts +11 -0
  82. package/dist/core/backup/detect.js +71 -0
  83. package/dist/core/backup/dump.d.ts +11 -0
  84. package/dist/core/backup/dump.js +82 -0
  85. package/dist/core/backup/index.d.ts +9 -0
  86. package/dist/core/backup/index.js +9 -0
  87. package/dist/core/backup/repo.d.ts +71 -0
  88. package/dist/core/backup/repo.js +256 -0
  89. package/dist/core/backup/schedule.d.ts +17 -0
  90. package/dist/core/backup/schedule.js +90 -0
  91. package/dist/core/backup/sensitive.d.ts +5 -0
  92. package/dist/core/backup/sensitive.js +37 -0
  93. package/dist/core/backup/status.d.ts +3 -0
  94. package/dist/core/backup/status.js +29 -0
  95. package/dist/core/backup/statuspage.d.ts +23 -0
  96. package/dist/core/backup/statuspage.js +145 -0
  97. package/dist/core/backup/system.d.ts +24 -0
  98. package/dist/core/backup/system.js +209 -0
  99. package/dist/core/backup/totp.d.ts +16 -0
  100. package/dist/core/backup/totp.js +116 -0
  101. package/dist/core/backup/types.d.ts +70 -0
  102. package/dist/core/backup/types.js +7 -0
  103. package/dist/core/backup/unlock.d.ts +19 -0
  104. package/dist/core/backup/unlock.js +69 -0
  105. package/dist/core/boot-refresh.d.ts +1 -1
  106. package/dist/core/boot-refresh.js +10 -9
  107. package/dist/core/deps/actors/pr-creator.d.ts +5 -3
  108. package/dist/core/deps/actors/pr-creator.js +71 -18
  109. package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
  110. package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
  111. package/dist/core/deps/collectors/npm.js +3 -1
  112. package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
  113. package/dist/core/deps/collectors/vulnerability.js +31 -2
  114. package/dist/core/deps/config.js +6 -0
  115. package/dist/core/deps/scanner.js +1 -1
  116. package/dist/core/deps/types.d.ts +8 -0
  117. package/dist/core/env.d.ts +3 -0
  118. package/dist/core/env.js +11 -0
  119. package/dist/core/exec.d.ts +1 -0
  120. package/dist/core/exec.js +4 -0
  121. package/dist/core/file-lock.d.ts +18 -0
  122. package/dist/core/file-lock.js +44 -0
  123. package/dist/core/git-onboard.js +10 -13
  124. package/dist/core/github.d.ts +3 -1
  125. package/dist/core/github.js +10 -7
  126. package/dist/core/logs-policy.d.ts +5 -0
  127. package/dist/core/logs-policy.js +20 -1
  128. package/dist/core/operator.d.ts +21 -0
  129. package/dist/core/operator.js +54 -0
  130. package/dist/core/registry.d.ts +18 -0
  131. package/dist/core/registry.js +26 -0
  132. package/dist/core/routines/schema.d.ts +11 -11
  133. package/dist/core/routines/schema.js +14 -3
  134. package/dist/core/routines/store.d.ts +8 -8
  135. package/dist/core/secrets-ops.d.ts +31 -6
  136. package/dist/core/secrets-ops.js +208 -102
  137. package/dist/core/secrets-providers.js +2 -2
  138. package/dist/core/secrets-rotation.d.ts +1 -1
  139. package/dist/core/secrets-rotation.js +58 -52
  140. package/dist/core/secrets-v2-cleanup.d.ts +19 -0
  141. package/dist/core/secrets-v2-cleanup.js +94 -0
  142. package/dist/core/secrets-v2-creds.d.ts +9 -0
  143. package/dist/core/secrets-v2-creds.js +44 -0
  144. package/dist/core/secrets-v2-install.d.ts +13 -0
  145. package/dist/core/secrets-v2-install.js +76 -0
  146. package/dist/core/secrets-v2-keypair.d.ts +10 -0
  147. package/dist/core/secrets-v2-keypair.js +31 -0
  148. package/dist/core/secrets-v2-migrate.d.ts +29 -0
  149. package/dist/core/secrets-v2-migrate.js +395 -0
  150. package/dist/core/secrets-v2-ops.d.ts +36 -0
  151. package/dist/core/secrets-v2-ops.js +184 -0
  152. package/dist/core/secrets-v2-protocol.d.ts +19 -0
  153. package/dist/core/secrets-v2-protocol.js +60 -0
  154. package/dist/core/secrets-v2-snapshot.d.ts +36 -0
  155. package/dist/core/secrets-v2-snapshot.js +115 -0
  156. package/dist/core/secrets-v2.d.ts +21 -0
  157. package/dist/core/secrets-v2.js +249 -0
  158. package/dist/core/secrets.d.ts +39 -4
  159. package/dist/core/secrets.js +91 -11
  160. package/dist/core/self-update.d.ts +32 -11
  161. package/dist/core/self-update.js +52 -14
  162. package/dist/core/testflight/asc.d.ts +12 -0
  163. package/dist/core/testflight/asc.js +101 -0
  164. package/dist/core/testflight/credentials.d.ts +3 -0
  165. package/dist/core/testflight/credentials.js +35 -0
  166. package/dist/core/testflight/eas.d.ts +4 -0
  167. package/dist/core/testflight/eas.js +38 -0
  168. package/dist/core/testflight/resolve.d.ts +6 -0
  169. package/dist/core/testflight/resolve.js +44 -0
  170. package/dist/core/testflight/types.d.ts +13 -0
  171. package/dist/core/testflight/types.js +3 -0
  172. package/dist/core/testflight/workflow.d.ts +17 -0
  173. package/dist/core/testflight/workflow.js +65 -0
  174. package/dist/core/validate.d.ts +1 -0
  175. package/dist/core/validate.js +8 -0
  176. package/dist/mcp/audit-tools.d.ts +2 -0
  177. package/dist/mcp/audit-tools.js +94 -0
  178. package/dist/mcp/git-tools.js +1 -1
  179. package/dist/mcp/registry-bridge.d.ts +10 -0
  180. package/dist/mcp/registry-bridge.js +65 -0
  181. package/dist/mcp/secrets-tools.js +2 -2
  182. package/dist/mcp/server.js +16 -82
  183. package/dist/mcp/testflight-tools.d.ts +2 -0
  184. package/dist/mcp/testflight-tools.js +52 -0
  185. package/dist/registry/context.d.ts +7 -0
  186. package/dist/registry/context.js +37 -0
  187. package/dist/registry/index.d.ts +5 -0
  188. package/dist/registry/index.js +44 -0
  189. package/dist/registry/parse-args.d.ts +13 -0
  190. package/dist/registry/parse-args.js +74 -0
  191. package/dist/registry/registry.d.ts +24 -0
  192. package/dist/registry/registry.js +26 -0
  193. package/dist/registry/render.d.ts +3 -0
  194. package/dist/registry/render.js +29 -0
  195. package/dist/registry/types.d.ts +50 -0
  196. package/dist/registry/types.js +1 -0
  197. package/dist/templates/agent-unit.d.ts +5 -0
  198. package/dist/templates/agent-unit.js +40 -0
  199. package/dist/templates/app-unit-edit.d.ts +2 -0
  200. package/dist/templates/app-unit-edit.js +46 -0
  201. package/dist/templates/compose-edit.d.ts +2 -0
  202. package/dist/templates/compose-edit.js +156 -0
  203. package/dist/templates/nginx.js +11 -0
  204. package/dist/templates/systemd.js +6 -0
  205. package/dist/tui/components/ArgForm.d.ts +7 -0
  206. package/dist/tui/components/ArgForm.js +64 -0
  207. package/dist/tui/components/ArgForm.test.d.ts +1 -0
  208. package/dist/tui/components/ArgForm.test.js +19 -0
  209. package/dist/tui/components/KeyHint.js +5 -0
  210. package/dist/tui/hooks/use-secrets.d.ts +8 -8
  211. package/dist/tui/hooks/use-secrets.js +7 -7
  212. package/dist/tui/router.d.ts +1 -0
  213. package/dist/tui/router.js +26 -9
  214. package/dist/tui/router.test.d.ts +1 -0
  215. package/dist/tui/router.test.js +13 -0
  216. package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
  217. package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
  218. package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
  219. package/dist/tui/tests/redaction-rerender.test.js +53 -0
  220. package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
  221. package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
  222. package/dist/tui/types.d.ts +1 -1
  223. package/dist/tui/views/CommandPalette.d.ts +5 -0
  224. package/dist/tui/views/CommandPalette.js +90 -0
  225. package/dist/tui/views/CommandPalette.test.d.ts +1 -0
  226. package/dist/tui/views/CommandPalette.test.js +117 -0
  227. package/dist/tui/views/Dashboard.js +9 -6
  228. package/dist/tui/views/HealthView.js +9 -4
  229. package/dist/tui/views/SecretEdit.js +15 -16
  230. package/dist/tui/views/SecretEdit.test.d.ts +1 -0
  231. package/dist/tui/views/SecretEdit.test.js +82 -0
  232. package/dist/tui/views/SecretsView.js +26 -16
  233. package/package.json +8 -5
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod';
2
+ import { checkForUpdate, applyUpdate, resolveChannel } from '../core/self-update.js';
3
+ import { defineCommand } from '../registry/registry.js';
4
+ /** CLI surface for self-update. equivalent to the TUI banner + U key, but
5
+ * driveable from a dumb terminal, a cron job, or a Claude session. */
6
+ export const updateCommand = defineCommand({
7
+ name: 'update',
8
+ summary: 'Check for or install a fleet update from the configured channel',
9
+ args: z.object({
10
+ check: z.boolean().default(false),
11
+ channel: z.enum(['stable', 'prerelease']).optional(),
12
+ branch: z.string().optional(),
13
+ }),
14
+ async run(args, _ctx) {
15
+ // env-var overrides apply for the lifetime of this invocation so the
16
+ // existing resolveChannel logic stays the single source of truth. we
17
+ // restore the prior environment afterward so an in-process MCP call
18
+ // doesn't leak the override into subsequent commands.
19
+ const previous = {
20
+ channel: process.env.FLEET_UPDATE_CHANNEL,
21
+ branch: process.env.FLEET_UPDATE_BRANCH,
22
+ };
23
+ if (args.channel)
24
+ process.env.FLEET_UPDATE_CHANNEL = args.channel;
25
+ if (args.branch)
26
+ process.env.FLEET_UPDATE_BRANCH = args.branch;
27
+ try {
28
+ const info = await checkForUpdate();
29
+ const channelInfo = resolveChannel();
30
+ const base = {
31
+ channel: info.channel,
32
+ remoteBranch: info.remoteBranch,
33
+ localBranch: info.branch,
34
+ available: info.available,
35
+ behind: info.behind,
36
+ latestSubject: info.latestSubject,
37
+ ...(info.error ? { error: info.error } : {}),
38
+ };
39
+ if (info.error) {
40
+ return {
41
+ ok: false,
42
+ summary: `check failed: ${info.error}`,
43
+ data: base,
44
+ };
45
+ }
46
+ if (!info.available) {
47
+ return {
48
+ ok: true,
49
+ summary: `up to date (channel=${channelInfo.channel}, branch=${channelInfo.branch})`,
50
+ data: base,
51
+ };
52
+ }
53
+ if (args.check) {
54
+ const subject = info.latestSubject ? ` — ${info.latestSubject}` : '';
55
+ return {
56
+ ok: true,
57
+ summary: `update available (channel=${channelInfo.channel}): ${info.behind} commit${info.behind === 1 ? '' : 's'} ahead${subject}`,
58
+ data: base,
59
+ };
60
+ }
61
+ const result = await applyUpdate();
62
+ const data = {
63
+ ...base,
64
+ pulled: result.pulled,
65
+ buildOk: result.buildOk,
66
+ output: result.output,
67
+ };
68
+ if (!result.ok) {
69
+ return {
70
+ ok: false,
71
+ summary: `update failed: ${result.output}`,
72
+ data,
73
+ };
74
+ }
75
+ return {
76
+ ok: true,
77
+ summary: result.pulled === 0
78
+ ? 'no changes pulled; rebuild ran'
79
+ : `updated ${result.pulled} commit${result.pulled === 1 ? '' : 's'}, rebuilt`,
80
+ data,
81
+ };
82
+ }
83
+ finally {
84
+ // restore the prior env unconditionally — even if the call threw.
85
+ if (previous.channel === undefined)
86
+ delete process.env.FLEET_UPDATE_CHANNEL;
87
+ else
88
+ process.env.FLEET_UPDATE_CHANNEL = previous.channel;
89
+ if (previous.branch === undefined)
90
+ delete process.env.FLEET_UPDATE_BRANCH;
91
+ else
92
+ process.env.FLEET_UPDATE_BRANCH = previous.branch;
93
+ }
94
+ },
95
+ });
@@ -0,0 +1,4 @@
1
+ import type { AuditCache, AuditRecord } from './types.js';
2
+ export declare function loadAuditCache(path?: string): AuditCache;
3
+ export declare function saveAuditRecord(record: AuditRecord, path?: string): void;
4
+ export declare function auditCachePath(): string;
@@ -0,0 +1,37 @@
1
+ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const DEFAULT_CACHE_PATH = join(__dirname, '..', '..', '..', 'data', 'audit-cache.json');
6
+ function emptyCache() {
7
+ return { version: 1, audits: {} };
8
+ }
9
+ // load the audit cache, returning an empty cache when the file is absent or
10
+ // unreadable — a stale/corrupt cache must never block a fresh audit.
11
+ export function loadAuditCache(path = DEFAULT_CACHE_PATH) {
12
+ if (!existsSync(path))
13
+ return emptyCache();
14
+ try {
15
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
16
+ if (!parsed || typeof parsed !== 'object' || !parsed.audits)
17
+ return emptyCache();
18
+ return parsed;
19
+ }
20
+ catch {
21
+ return emptyCache();
22
+ }
23
+ }
24
+ // upsert one audit record, keyed by target, via an atomic tmp-file rename.
25
+ export function saveAuditRecord(record, path = DEFAULT_CACHE_PATH) {
26
+ const cache = loadAuditCache(path);
27
+ cache.audits[record.target] = record;
28
+ const dir = dirname(path);
29
+ if (!existsSync(dir))
30
+ mkdirSync(dir, { recursive: true });
31
+ const tmp = path + '.tmp';
32
+ writeFileSync(tmp, JSON.stringify(cache, null, 2) + '\n');
33
+ renameSync(tmp, path);
34
+ }
35
+ export function auditCachePath() {
36
+ return DEFAULT_CACHE_PATH;
37
+ }
@@ -0,0 +1,5 @@
1
+ import type { AuditConfig } from './types.js';
2
+ export declare function defaultAuditConfig(): AuditConfig;
3
+ export declare function loadAuditConfig(path?: string): AuditConfig;
4
+ export declare function saveAuditConfig(config: AuditConfig, path?: string): void;
5
+ export declare function auditConfigPath(): string;
@@ -0,0 +1,35 @@
1
+ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const DEFAULT_CONFIG_PATH = join(__dirname, '..', '..', '..', 'data', 'audit-config.json');
6
+ export function defaultAuditConfig() {
7
+ return { version: 1, ignore: [] };
8
+ }
9
+ // load the audit config (ignore rules). a missing or corrupt file yields the
10
+ // default empty config — a bad config must never abort an audit.
11
+ export function loadAuditConfig(path = DEFAULT_CONFIG_PATH) {
12
+ if (!existsSync(path))
13
+ return defaultAuditConfig();
14
+ try {
15
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
16
+ if (!parsed || !Array.isArray(parsed.ignore))
17
+ return defaultAuditConfig();
18
+ return { version: 1, ignore: parsed.ignore };
19
+ }
20
+ catch {
21
+ return defaultAuditConfig();
22
+ }
23
+ }
24
+ // persist the audit config via an atomic tmp-file rename.
25
+ export function saveAuditConfig(config, path = DEFAULT_CONFIG_PATH) {
26
+ const dir = dirname(path);
27
+ if (!existsSync(dir))
28
+ mkdirSync(dir, { recursive: true });
29
+ const tmp = path + '.tmp';
30
+ writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n');
31
+ renameSync(tmp, path);
32
+ }
33
+ export function auditConfigPath() {
34
+ return DEFAULT_CONFIG_PATH;
35
+ }
@@ -0,0 +1,11 @@
1
+ import type { GreenlightReport } from './types.js';
2
+ export declare const GREENLIGHT_INSTALL_HINT: string;
3
+ export declare function findGreenlight(): string | null;
4
+ export declare function requireGreenlight(): string;
5
+ export declare function greenlightVersion(bin: string): string | null;
6
+ export interface PreflightOptions {
7
+ ipaPath?: string;
8
+ timeoutMs?: number;
9
+ }
10
+ export declare function runPreflight(projectPath: string, opts?: PreflightOptions, bin?: string): GreenlightReport;
11
+ export declare function runGuidelines(args: string[], bin?: string): string;
@@ -0,0 +1,81 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { execSafe } from '../exec.js';
5
+ import { FleetError } from '../errors.js';
6
+ export const GREENLIGHT_INSTALL_HINT = [
7
+ 'greenlight binary not found. Install it with one of:',
8
+ ' go install github.com/RevylAI/greenlight/cmd/greenlight@latest',
9
+ ' brew install revylai/tap/greenlight',
10
+ 'Then re-run, or point GREENLIGHT_BIN at an absolute path.',
11
+ ].join('\n');
12
+ // locate the greenlight binary. honours $GREENLIGHT_BIN first, then anything
13
+ // on $PATH, then the usual `go install` and homebrew destinations. returns the
14
+ // command/path execSafe should invoke, or null when nothing usable is found.
15
+ export function findGreenlight() {
16
+ const override = process.env.GREENLIGHT_BIN;
17
+ if (override)
18
+ return existsSync(override) ? override : null;
19
+ if (execSafe('greenlight', ['--help'], { timeout: 10_000 }).ok) {
20
+ return 'greenlight';
21
+ }
22
+ const candidates = [];
23
+ if (process.env.GOBIN)
24
+ candidates.push(join(process.env.GOBIN, 'greenlight'));
25
+ const gopath = execSafe('go', ['env', 'GOPATH'], { timeout: 10_000 });
26
+ if (gopath.ok && gopath.stdout) {
27
+ candidates.push(join(gopath.stdout.split('\n')[0].trim(), 'bin', 'greenlight'));
28
+ }
29
+ candidates.push(join(homedir(), 'go', 'bin', 'greenlight'), '/opt/homebrew/bin/greenlight', '/usr/local/bin/greenlight');
30
+ for (const candidate of candidates) {
31
+ if (existsSync(candidate))
32
+ return candidate;
33
+ }
34
+ return null;
35
+ }
36
+ // resolve the binary or fail loudly with install guidance.
37
+ export function requireGreenlight() {
38
+ const bin = findGreenlight();
39
+ if (!bin)
40
+ throw new FleetError(GREENLIGHT_INSTALL_HINT);
41
+ return bin;
42
+ }
43
+ // best-effort version string for diagnostics; null if it can't be read.
44
+ export function greenlightVersion(bin) {
45
+ const res = execSafe(bin, ['version'], { timeout: 10_000 });
46
+ if (res.ok && res.stdout)
47
+ return res.stdout.split('\n')[0].trim();
48
+ return null;
49
+ }
50
+ // run `greenlight preflight <project> --format json` and parse the report.
51
+ // greenlight exits 0 for any successful scan regardless of how many issues it
52
+ // finds — a non-zero exit means the scan itself failed (bad path, binary
53
+ // error), so that is the only case treated as an error here.
54
+ export function runPreflight(projectPath, opts = {}, bin = requireGreenlight()) {
55
+ const args = ['preflight', projectPath, '--format', 'json'];
56
+ if (opts.ipaPath)
57
+ args.push('--ipa', opts.ipaPath);
58
+ const res = execSafe(bin, args, { timeout: opts.timeoutMs ?? 120_000 });
59
+ if (!res.ok) {
60
+ throw new FleetError(`greenlight preflight failed: ${res.stderr || res.stdout || 'unknown error'}`);
61
+ }
62
+ // greenlight prints a human banner (project path, scanner list) to stdout
63
+ // before the json document even under --format json, so slice from the
64
+ // first brace. the banner is plain text and never contains one.
65
+ const jsonStart = res.stdout.indexOf('{');
66
+ if (jsonStart === -1) {
67
+ throw new FleetError(`greenlight produced no json report:\n${res.stdout.slice(0, 500)}`);
68
+ }
69
+ try {
70
+ return JSON.parse(res.stdout.slice(jsonStart));
71
+ }
72
+ catch {
73
+ throw new FleetError(`greenlight returned output that is not valid json:\n${res.stdout.slice(0, 500)}`);
74
+ }
75
+ }
76
+ // passthrough to `greenlight guidelines <args>` (list | show <section> |
77
+ // search <term>) — returns the rendered text for the caller to print.
78
+ export function runGuidelines(args, bin = requireGreenlight()) {
79
+ const res = execSafe(bin, ['guidelines', ...args], { timeout: 15_000 });
80
+ return (res.stdout || res.stderr).trim();
81
+ }
@@ -0,0 +1,3 @@
1
+ import type { GreenlightReport } from '../types.js';
2
+ export declare function severityIcon(severity: string): string;
3
+ export declare function formatReport(report: GreenlightReport): string[];
@@ -0,0 +1,68 @@
1
+ import { c, icon } from '../../../ui/output.js';
2
+ const SEVERITY_ORDER = ['CRITICAL', 'WARN', 'INFO'];
3
+ export function severityIcon(severity) {
4
+ switch (severity) {
5
+ case 'CRITICAL': return icon.err;
6
+ case 'WARN': return icon.warn;
7
+ default: return icon.info;
8
+ }
9
+ }
10
+ function severityHeading(severity) {
11
+ switch (severity) {
12
+ case 'CRITICAL': return `${c.red}${c.bold}Critical — will be rejected`;
13
+ case 'WARN': return `${c.yellow}${c.bold}Warning — high rejection risk`;
14
+ default: return `${c.dim}${c.bold}Info — best practice`;
15
+ }
16
+ }
17
+ // render a parsed greenlight report into printable terminal lines, grouped by
18
+ // severity with a verdict footer. ansi colours come from ui/output.
19
+ export function formatReport(report) {
20
+ const lines = [];
21
+ if (report.app_name)
22
+ lines.push(` ${c.dim}App${c.reset} ${report.app_name}`);
23
+ if (report.bundle_id)
24
+ lines.push(` ${c.dim}Bundle${c.reset} ${report.bundle_id}`);
25
+ lines.push(` ${c.dim}Privacy manifest${c.reset} ` +
26
+ (report.has_privacy_info ? `${icon.ok} present` : `${icon.warn} missing`));
27
+ if (report.tracking_sdks && report.tracking_sdks.length > 0) {
28
+ lines.push(` ${c.dim}Tracking SDKs${c.reset} ${report.tracking_sdks.join(', ')}`);
29
+ }
30
+ if (report.detected_apis && report.detected_apis.length > 0) {
31
+ lines.push(` ${c.dim}Required-reason APIs${c.reset} ${report.detected_apis.join(', ')}`);
32
+ }
33
+ for (const severity of SEVERITY_ORDER) {
34
+ const group = report.findings.filter(f => f.severity === severity);
35
+ if (group.length === 0)
36
+ continue;
37
+ lines.push('');
38
+ lines.push(`${severityHeading(severity)} (${group.length})${c.reset}`);
39
+ for (const finding of group)
40
+ lines.push(...formatFinding(finding));
41
+ }
42
+ const s = report.summary;
43
+ lines.push('');
44
+ lines.push(` ${c.dim}${'-'.repeat(48)}${c.reset}`);
45
+ if (s.passed) {
46
+ lines.push(` ${c.green}${c.bold}GREENLIT${c.reset} — no critical issues found`);
47
+ }
48
+ else {
49
+ lines.push(` ${c.red}${c.bold}NOT READY${c.reset} — ${s.critical} critical issue(s) must be fixed`);
50
+ }
51
+ lines.push(` ${c.dim}${s.total} findings: ${s.critical} critical, ${s.warns} warn, ` +
52
+ `${s.infos} info — scanned in ${report.elapsed}${c.reset}`);
53
+ return lines;
54
+ }
55
+ function formatFinding(finding) {
56
+ const out = [];
57
+ const ref = finding.guideline ? `${c.cyan}§${finding.guideline}${c.reset} ` : '';
58
+ out.push(` ${severityIcon(finding.severity)} ${c.dim}[${finding.source}]${c.reset} ` +
59
+ `${ref}${c.bold}${finding.title}${c.reset}`);
60
+ if (finding.file) {
61
+ const loc = finding.line ? `${finding.file}:${finding.line}` : finding.file;
62
+ out.push(` ${c.dim}${loc}${c.reset}`);
63
+ }
64
+ out.push(` ${c.dim}${finding.detail}${c.reset}`);
65
+ if (finding.fix)
66
+ out.push(` ${c.green}fix:${c.reset} ${finding.fix}`);
67
+ return out;
68
+ }
@@ -0,0 +1,6 @@
1
+ import type { AuditIgnoreRule, GreenlightReport } from './types.js';
2
+ export interface SuppressionResult {
3
+ report: GreenlightReport;
4
+ suppressed: number;
5
+ }
6
+ export declare function applySuppressions(report: GreenlightReport, target: string, rules: AuditIgnoreRule[]): SuppressionResult;
@@ -0,0 +1,37 @@
1
+ // true when `rule` dismisses `finding` for the audit target `target`.
2
+ function ruleCovers(rule, finding, target) {
3
+ if (rule.target && rule.target !== target)
4
+ return false;
5
+ if (rule.title !== finding.title)
6
+ return false;
7
+ if (rule.contains) {
8
+ const haystack = `${finding.file ?? ''}\n${finding.code ?? ''}`;
9
+ if (!haystack.includes(rule.contains))
10
+ return false;
11
+ }
12
+ return true;
13
+ }
14
+ function recomputeSummary(findings) {
15
+ return {
16
+ total: findings.length,
17
+ critical: findings.filter(f => f.severity === 'CRITICAL').length,
18
+ warns: findings.filter(f => f.severity === 'WARN').length,
19
+ infos: findings.filter(f => f.severity === 'INFO').length,
20
+ passed: findings.every(f => f.severity !== 'CRITICAL'),
21
+ };
22
+ }
23
+ // drops findings covered by an active ignore rule and recomputes the summary
24
+ // so the verdict reflects what is left. this is how confirmed scanner false
25
+ // positives are dismissed — each rule carries a written reason.
26
+ export function applySuppressions(report, target, rules) {
27
+ if (rules.length === 0)
28
+ return { report, suppressed: 0 };
29
+ const kept = report.findings.filter(finding => !rules.some(rule => ruleCovers(rule, finding, target)));
30
+ const suppressed = report.findings.length - kept.length;
31
+ if (suppressed === 0)
32
+ return { report, suppressed: 0 };
33
+ return {
34
+ report: { ...report, findings: kept, summary: recomputeSummary(kept) },
35
+ suppressed,
36
+ };
37
+ }
@@ -0,0 +1,5 @@
1
+ export interface AuditTarget {
2
+ target: string;
3
+ projectPath: string;
4
+ }
5
+ export declare function resolveAuditTarget(target: string): AuditTarget;
@@ -0,0 +1,26 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { load, findApp } from '../registry.js';
4
+ import { FleetError } from '../errors.js';
5
+ // resolve an audit target to the mobile project directory to scan.
6
+ //
7
+ // a target is either an existing directory path (used as-is) or a registered
8
+ // fleet app name. for a registered app the convention is a `mobile/` subdir of
9
+ // its compose root — that is where an expo / ios project lives in a repo whose
10
+ // root holds docker-compose — falling back to the compose root when there is
11
+ // no such subdir.
12
+ export function resolveAuditTarget(target) {
13
+ const asPath = resolve(target);
14
+ if (existsSync(asPath) && statSync(asPath).isDirectory()) {
15
+ return { target, projectPath: asPath };
16
+ }
17
+ const app = findApp(load(), target);
18
+ if (!app) {
19
+ throw new FleetError(`Audit target "${target}" is neither an existing directory nor a registered app.`);
20
+ }
21
+ const mobileDir = join(app.composePath, 'mobile');
22
+ return {
23
+ target: app.name,
24
+ projectPath: existsSync(mobileDir) ? mobileDir : app.composePath,
25
+ };
26
+ }
@@ -0,0 +1,54 @@
1
+ export type GreenlightSeverity = 'CRITICAL' | 'WARN' | 'INFO';
2
+ export type GreenlightSource = 'metadata' | 'codescan' | 'privacy' | 'ipa';
3
+ export interface GreenlightFinding {
4
+ source: GreenlightSource | string;
5
+ severity: GreenlightSeverity | string;
6
+ guideline?: string;
7
+ title: string;
8
+ detail: string;
9
+ fix?: string;
10
+ file?: string;
11
+ line?: number;
12
+ code?: string;
13
+ }
14
+ export interface GreenlightSummary {
15
+ total: number;
16
+ critical: number;
17
+ warns: number;
18
+ infos: number;
19
+ passed: boolean;
20
+ }
21
+ export interface GreenlightReport {
22
+ project_path: string;
23
+ ipa_path?: string;
24
+ app_name?: string;
25
+ bundle_id?: string;
26
+ has_privacy_info: boolean;
27
+ detected_apis?: string[];
28
+ tracking_sdks?: string[];
29
+ findings: GreenlightFinding[];
30
+ summary: GreenlightSummary;
31
+ elapsed: string;
32
+ }
33
+ export interface AuditRecord {
34
+ target: string;
35
+ projectPath: string;
36
+ ipaPath?: string;
37
+ ranAt: string;
38
+ report: GreenlightReport;
39
+ }
40
+ export interface AuditCache {
41
+ version: 1;
42
+ audits: Record<string, AuditRecord>;
43
+ }
44
+ export interface AuditIgnoreRule {
45
+ target?: string;
46
+ title: string;
47
+ contains?: string;
48
+ reason: string;
49
+ addedAt: string;
50
+ }
51
+ export interface AuditConfig {
52
+ version: 1;
53
+ ignore: AuditIgnoreRule[];
54
+ }
@@ -0,0 +1,5 @@
1
+ // shapes for the App Store compliance audit feature. the report types mirror
2
+ // the json emitted by `greenlight preflight --format json` (RevylAI/greenlight,
3
+ // internal/cli/preflight.go writePreflightJSON) — kept structurally identical
4
+ // so the binary's output deserialises straight into these.
5
+ export {};
@@ -0,0 +1,66 @@
1
+ import { StatusReport } from './statuspage.js';
2
+ import { SnapshotInfo } from './types.js';
3
+ import { TreeEntry } from './repo.js';
4
+ export interface RestoreResult {
5
+ target: string;
6
+ fileCount: number;
7
+ bytes: number;
8
+ durationMs: number;
9
+ }
10
+ export interface StagingDir {
11
+ path: string;
12
+ bytes: number;
13
+ age: string;
14
+ }
15
+ /** everything the router needs, injected so handlers stay pure + testable. */
16
+ export interface ApiContext {
17
+ now(): number;
18
+ totpSecret: string;
19
+ sessionSecret: string;
20
+ sessionTtlMs: number;
21
+ /** the deployment domain — the same-origin check accepts only this host. */
22
+ domain: string;
23
+ listApps(): string[];
24
+ statusReport(): StatusReport;
25
+ snapshots(app: string): SnapshotInfo[];
26
+ lsTree(app: string, snap: string, path: string): TreeEntry[];
27
+ fileMeta(app: string, snap: string, path: string): {
28
+ size: number;
29
+ sensitive: boolean;
30
+ } | null;
31
+ restore(app: string, snap: string, path: string): RestoreResult;
32
+ listStaging(): StagingDir[];
33
+ deleteStaging(path: string): void;
34
+ }
35
+ export interface ApiRequest {
36
+ method: string;
37
+ path: string;
38
+ query: Record<string, string>;
39
+ headers: Record<string, string>;
40
+ body?: unknown;
41
+ cookies: Record<string, string>;
42
+ }
43
+ export type ApiResponse = {
44
+ kind: 'json';
45
+ status: number;
46
+ body: unknown;
47
+ setCookie?: string;
48
+ } | {
49
+ kind: 'html';
50
+ status: number;
51
+ body: string;
52
+ } | {
53
+ kind: 'stream';
54
+ status: number;
55
+ app: string;
56
+ snap: string;
57
+ path: string;
58
+ filename: string;
59
+ contentType: string;
60
+ disposition: 'inline' | 'attachment';
61
+ } | {
62
+ kind: 'redirect';
63
+ status: number;
64
+ location: string;
65
+ };
66
+ export declare function handle(req: ApiRequest, ctx: ApiContext): ApiResponse;