@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.
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 +10 -7
  228. package/dist/tui/views/HealthView.js +14 -5
  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 +9 -6
@@ -33,7 +33,7 @@ export type BuildResult = {
33
33
  reason: 'build-failed';
34
34
  };
35
35
  export declare function buildIfStale(app: AppEntry, currentHead: string): BuildResult;
36
- export declare function recordBuiltCommit(appName: string, commit: string): void;
36
+ export declare function recordBuiltCommit(appName: string, commit: string): Promise<void>;
37
37
  export declare const KILL_SWITCH = "/etc/fleet/no-auto-refresh";
38
38
  export declare const DEFAULT_WALL_CLOCK_MS = 900000;
39
39
  export type RefreshResult = {
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { isGitRepo, getGitStatus } from './git.js';
3
3
  import { execGit } from './exec.js';
4
- import { load, save } from './registry.js';
4
+ import { withRegistry } from './registry.js';
5
5
  import { composeBuild } from './docker.js';
6
6
  export function preflight(projectRoot) {
7
7
  if (!isGitRepo(projectRoot))
@@ -53,13 +53,14 @@ export function buildIfStale(app, currentHead) {
53
53
  return { ok: false, reason: 'build-failed' };
54
54
  return { ok: true, built: true };
55
55
  }
56
- export function recordBuiltCommit(appName, commit) {
57
- const reg = load();
58
- const i = reg.apps.findIndex(a => a.name === appName);
59
- if (i < 0)
60
- return;
61
- reg.apps[i] = { ...reg.apps[i], lastBuiltCommit: commit };
62
- save(reg);
56
+ export async function recordBuiltCommit(appName, commit) {
57
+ await withRegistry(reg => {
58
+ const i = reg.apps.findIndex(a => a.name === appName);
59
+ if (i < 0)
60
+ return reg;
61
+ reg.apps[i] = { ...reg.apps[i], lastBuiltCommit: commit };
62
+ return reg;
63
+ });
63
64
  }
64
65
  export const KILL_SWITCH = '/etc/fleet/no-auto-refresh';
65
66
  function killSwitchPath() {
@@ -80,7 +81,7 @@ async function doRefresh(app) {
80
81
  if (!build.ok)
81
82
  return { kind: 'failed-safe', step: 'build', detail: build.reason };
82
83
  if (build.built)
83
- recordBuiltCommit(app.name, ff.newHead);
84
+ await recordBuiltCommit(app.name, ff.newHead);
84
85
  if (!ff.changed && !build.built)
85
86
  return { kind: 'no-change', head: ff.newHead };
86
87
  return { kind: 'refreshed', head: ff.newHead, built: build.built };
@@ -2,13 +2,15 @@ import type { AppEntry } from '../../registry.js';
2
2
  import type { Finding } from '../types.js';
3
3
  export interface VersionBump {
4
4
  file: string;
5
- search: string;
5
+ searchRegex: RegExp;
6
6
  replace: string;
7
7
  }
8
8
  export declare function generateVersionBump(finding: Finding): VersionBump | null;
9
9
  export declare function buildPrBody(findings: Finding[]): string;
10
- export declare function createDepsPr(app: AppEntry, findings: Finding[], dryRun: boolean): {
10
+ export interface CreateDepsPrResult {
11
11
  branch: string;
12
12
  bumps: VersionBump[];
13
13
  prUrl?: string;
14
- };
14
+ error?: string;
15
+ }
16
+ export declare function createDepsPr(app: AppEntry, findings: Finding[], dryRun: boolean): CreateDepsPrResult;
@@ -1,34 +1,41 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { execSafe } from '../../exec.js';
4
+ import { getGitStatus } from '../../git.js';
5
+ function escapeRegex(s) {
6
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+ }
4
8
  export function generateVersionBump(finding) {
5
9
  if (!finding.fixable || !finding.package || !finding.currentVersion || !finding.latestVersion) {
6
10
  return null;
7
11
  }
12
+ const escName = escapeRegex(finding.package);
13
+ const escCurrent = escapeRegex(finding.currentVersion);
8
14
  switch (finding.source) {
9
15
  case 'npm':
10
16
  return {
11
17
  file: 'package.json',
12
- search: `"${finding.package}": "${finding.currentVersion}"`,
13
- replace: `"${finding.package}": "${finding.latestVersion}"`,
18
+ // capture optional leading range operator (^, ~, >=, <=, >, <, =) so it survives the rewrite
19
+ searchRegex: new RegExp(`("${escName}"\\s*:\\s*")([\\^~>=<]*)${escCurrent}(")`),
20
+ replace: `$1$2${finding.latestVersion}$3`,
14
21
  };
15
22
  case 'composer':
16
23
  return {
17
24
  file: 'composer.json',
18
- search: `"${finding.package}": "${finding.currentVersion}"`,
19
- replace: `"${finding.package}": "${finding.latestVersion}"`,
25
+ searchRegex: new RegExp(`("${escName}"\\s*:\\s*")([\\^~>=<]*)${escCurrent}(")`),
26
+ replace: `$1$2${finding.latestVersion}$3`,
20
27
  };
21
28
  case 'pip':
22
29
  return {
23
30
  file: 'requirements.txt',
24
- search: `${finding.package}==${finding.currentVersion}`,
25
- replace: `${finding.package}==${finding.latestVersion}`,
31
+ searchRegex: new RegExp(`(${escName}==)${escCurrent}\\b`),
32
+ replace: `$1${finding.latestVersion}`,
26
33
  };
27
34
  case 'docker-image':
28
35
  return {
29
36
  file: 'Dockerfile',
30
- search: `${finding.package}:${finding.currentVersion}`,
31
- replace: `${finding.package}:${finding.latestVersion}`,
37
+ searchRegex: new RegExp(`(${escName}:)${escCurrent}\\b`),
38
+ replace: `$1${finding.latestVersion}`,
32
39
  };
33
40
  default:
34
41
  return null;
@@ -74,25 +81,71 @@ export function createDepsPr(app, findings, dryRun) {
74
81
  if (dryRun) {
75
82
  return { branch, bumps };
76
83
  }
84
+ // Working-tree precheck: refuse to operate on a dirty repo. Otherwise the
85
+ // checkout/pull below can fail mid-way and leave the repo in a partial state,
86
+ // or worse, run on stale content.
87
+ const status = getGitStatus(app.composePath);
88
+ if (!status.initialised) {
89
+ return { branch: '', bumps: [], error: `${app.composePath} is not a git repo` };
90
+ }
91
+ if (!status.clean) {
92
+ return {
93
+ branch: '',
94
+ bumps: [],
95
+ error: `working tree at ${app.composePath} is dirty (${status.staged} staged, ${status.modified} modified, ${status.untracked} untracked) — commit or stash before running deps fix`,
96
+ };
97
+ }
77
98
  const sshEnv = process.env.SSH_AUTH_SOCK ? { SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK } : {};
78
- execSafe('git', ['checkout', 'develop'], { cwd: app.composePath });
79
- execSafe('git', ['pull'], { cwd: app.composePath, env: sshEnv });
80
- execSafe('git', ['checkout', '-b', branch], { cwd: app.composePath });
99
+ const checkoutDevelop = execSafe('git', ['checkout', 'develop'], { cwd: app.composePath });
100
+ if (!checkoutDevelop.ok) {
101
+ return { branch: '', bumps: [], error: `git checkout develop failed: ${checkoutDevelop.stderr}` };
102
+ }
103
+ const pull = execSafe('git', ['pull'], { cwd: app.composePath, env: sshEnv });
104
+ if (!pull.ok) {
105
+ return { branch: '', bumps: [], error: `git pull failed: ${pull.stderr}` };
106
+ }
107
+ const checkoutBranch = execSafe('git', ['checkout', '-b', branch], { cwd: app.composePath });
108
+ if (!checkoutBranch.ok) {
109
+ return { branch: '', bumps: [], error: `git checkout -b ${branch} failed: ${checkoutBranch.stderr}` };
110
+ }
111
+ // Apply each bump and track which files actually changed.
112
+ const changedFiles = new Set();
81
113
  for (const bump of bumps) {
82
114
  const filePath = join(app.composePath, bump.file);
83
115
  if (!existsSync(filePath))
84
116
  continue;
85
- let content = readFileSync(filePath, 'utf-8');
86
- content = content.replace(bump.search, bump.replace);
87
- writeFileSync(filePath, content);
117
+ const before = readFileSync(filePath, 'utf-8');
118
+ const after = before.replace(bump.searchRegex, bump.replace);
119
+ if (after !== before) {
120
+ writeFileSync(filePath, after);
121
+ changedFiles.add(bump.file);
122
+ }
123
+ }
124
+ // Range mismatch / unknown file content / nothing to update — abort before
125
+ // committing or pushing so we never open an empty PR.
126
+ if (changedFiles.size === 0) {
127
+ return {
128
+ branch: '',
129
+ bumps: [],
130
+ error: `no files changed: regex did not match any of ${[...new Set(bumps.map(b => b.file))].join(', ')}; the manifest may already be at the target version, or the version range syntax is unsupported`,
131
+ };
132
+ }
133
+ const files = [...changedFiles];
134
+ const add = execSafe('git', ['add', ...files], { cwd: app.composePath });
135
+ if (!add.ok) {
136
+ return { branch: '', bumps: [], error: `git add failed: ${add.stderr}` };
88
137
  }
89
- const files = [...new Set(bumps.map(b => b.file))];
90
- execSafe('git', ['add', ...files], { cwd: app.composePath });
91
138
  const commitMsg = bumps.length === 1
92
139
  ? `chore(deps): update ${fixable[0].package} from ${fixable[0].currentVersion} to ${fixable[0].latestVersion}`
93
140
  : `chore(deps): update ${bumps.length} dependencies`;
94
- execSafe('git', ['commit', '-m', commitMsg], { cwd: app.composePath });
95
- execSafe('git', ['push', '-u', 'origin', branch], { cwd: app.composePath, env: sshEnv });
141
+ const commit = execSafe('git', ['commit', '-m', commitMsg], { cwd: app.composePath });
142
+ if (!commit.ok) {
143
+ return { branch: '', bumps: [], error: `git commit failed: ${commit.stderr}` };
144
+ }
145
+ const push = execSafe('git', ['push', '-u', 'origin', branch], { cwd: app.composePath, env: sshEnv });
146
+ if (!push.ok) {
147
+ return { branch: '', bumps: [], error: `git push failed: ${push.stderr}` };
148
+ }
96
149
  if (!app.gitRepo)
97
150
  return { branch, bumps };
98
151
  const prBody = buildPrBody(fixable);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * fetch wrapper that aborts after `timeoutMs`.
3
+ *
4
+ * Used by collectors that hit third-party HTTP services (OSV, npm registry).
5
+ * Without this, a hung peer would stall an entire scan indefinitely.
6
+ */
7
+ export declare function fetchWithTimeout(url: string, init?: RequestInit, timeoutMs?: number): Promise<Response>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * fetch wrapper that aborts after `timeoutMs`.
3
+ *
4
+ * Used by collectors that hit third-party HTTP services (OSV, npm registry).
5
+ * Without this, a hung peer would stall an entire scan indefinitely.
6
+ */
7
+ export async function fetchWithTimeout(url, init = {}, timeoutMs = 10_000) {
8
+ const controller = new AbortController();
9
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
10
+ try {
11
+ return await fetch(url, { ...init, signal: controller.signal });
12
+ }
13
+ finally {
14
+ clearTimeout(timer);
15
+ }
16
+ }
@@ -1,6 +1,8 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { severityFromVersionDelta } from '../severity.js';
4
+ import { fetchWithTimeout } from './fetch-with-timeout.js';
5
+ const NPM_REGISTRY_TIMEOUT_MS = 10_000;
4
6
  export class NpmCollector {
5
7
  overrides;
6
8
  type = 'npm';
@@ -37,7 +39,7 @@ export class NpmCollector {
37
39
  async checkPackage(appName, name, currentRaw) {
38
40
  const current = currentRaw.replace(/^[^\d]*/, '');
39
41
  try {
40
- const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`);
42
+ const res = await fetchWithTimeout(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, {}, NPM_REGISTRY_TIMEOUT_MS);
41
43
  if (!res.ok)
42
44
  return null;
43
45
  const data = await res.json();
@@ -2,6 +2,14 @@ import type { AppEntry } from '../../registry.js';
2
2
  import type { Collector, Finding } from '../types.js';
3
3
  export declare class VulnerabilityCollector implements Collector {
4
4
  type: "vulnerability";
5
+ private skipRegexes;
6
+ /**
7
+ * @param osvSkipPatterns regex strings matched against bare package names.
8
+ * Matching packages are NOT sent to OSV (https://api.osv.dev), which is a
9
+ * third-party service. This prevents leaking private/internal package
10
+ * manifests to a third party.
11
+ */
12
+ constructor(osvSkipPatterns?: string[]);
5
13
  detect(appPath: string): boolean;
6
14
  collect(app: AppEntry): Promise<Finding[]>;
7
15
  private extractPackages;
@@ -1,8 +1,29 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { severityFromCvss } from '../severity.js';
4
+ import { fetchWithTimeout } from './fetch-with-timeout.js';
5
+ const OSV_TIMEOUT_MS = 10_000;
4
6
  export class VulnerabilityCollector {
5
7
  type = 'vulnerability';
8
+ skipRegexes;
9
+ /**
10
+ * @param osvSkipPatterns regex strings matched against bare package names.
11
+ * Matching packages are NOT sent to OSV (https://api.osv.dev), which is a
12
+ * third-party service. This prevents leaking private/internal package
13
+ * manifests to a third party.
14
+ */
15
+ constructor(osvSkipPatterns = []) {
16
+ this.skipRegexes = osvSkipPatterns
17
+ .map(p => {
18
+ try {
19
+ return new RegExp(p);
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ })
25
+ .filter((r) => r !== null);
26
+ }
6
27
  detect(appPath) {
7
28
  return (existsSync(join(appPath, 'package.json')) ||
8
29
  existsSync(join(appPath, 'composer.json')) ||
@@ -63,18 +84,26 @@ export class VulnerabilityCollector {
63
84
  }
64
85
  catch { /* skip */ }
65
86
  }
87
+ // Filter out packages that match any configured skip pattern. OSV is a
88
+ // third-party service (api.osv.dev, operated by Google), so the request
89
+ // payload is the package name + version. Internal/private package names
90
+ // would otherwise be exfiltrated. The default config skips the user's
91
+ // own npm scope.
92
+ if (this.skipRegexes.length > 0) {
93
+ return packages.filter(p => !this.skipRegexes.some(rx => rx.test(p.name)));
94
+ }
66
95
  return packages;
67
96
  }
68
97
  async queryOsv(appName, pkg) {
69
98
  try {
70
- const res = await fetch('https://api.osv.dev/v1/query', {
99
+ const res = await fetchWithTimeout('https://api.osv.dev/v1/query', {
71
100
  method: 'POST',
72
101
  headers: { 'Content-Type': 'application/json' },
73
102
  body: JSON.stringify({
74
103
  version: pkg.version,
75
104
  package: { name: pkg.name, ecosystem: pkg.ecosystem },
76
105
  }),
77
- });
106
+ }, OSV_TIMEOUT_MS);
78
107
  if (!res.ok)
79
108
  return [];
80
109
  const data = await res.json();
@@ -21,6 +21,12 @@ export function defaultConfig() {
21
21
  minorVersionBehind: 'medium',
22
22
  patchVersionBehind: 'low',
23
23
  },
24
+ // Skip OSV lookups for the user's own npm scope by default — OSV is a
25
+ // third-party service (Google) and sending internal package names there
26
+ // leaks the proprietary dependency manifest. Backwards compat: if an
27
+ // existing deps-config.json is missing this field, mergeConfig fills it
28
+ // in from these defaults.
29
+ osvSkipPatterns: ['^@matthesketh/'],
24
30
  };
25
31
  }
26
32
  export function mergeConfig(base, overrides) {
@@ -14,7 +14,7 @@ export function createCollectors(config) {
14
14
  new DockerImageCollector(config.severityOverrides),
15
15
  new DockerRunningCollector(config.severityOverrides),
16
16
  new EolCollector(config.severityOverrides.eolDaysWarning),
17
- new VulnerabilityCollector(),
17
+ new VulnerabilityCollector(config.osvSkipPatterns),
18
18
  new GitHubPrCollector(),
19
19
  ];
20
20
  }
@@ -48,6 +48,14 @@ export interface DepsConfig {
48
48
  minorVersionBehind: Severity;
49
49
  patchVersionBehind: Severity;
50
50
  };
51
+ /**
52
+ * Regex strings (matched against bare package name) of packages that must
53
+ * NOT be queried against OSV (https://api.osv.dev). OSV is a third-party
54
+ * service operated by Google; sending private/internal package names there
55
+ * leaks the proprietary dependency manifest. Default skips the user's own
56
+ * npm scope.
57
+ */
58
+ osvSkipPatterns: string[];
51
59
  }
52
60
  export interface DepsCache {
53
61
  version: 1;
@@ -0,0 +1,3 @@
1
+ /** returns a required env var, or throws a clear error. use for secrets,
2
+ * keys and credential paths where a silent default would be dangerous. */
3
+ export declare function requireEnv(name: string): string;
@@ -0,0 +1,11 @@
1
+ import { FleetError } from './errors.js';
2
+ /** returns a required env var, or throws a clear error. use for secrets,
3
+ * keys and credential paths where a silent default would be dangerous. */
4
+ export function requireEnv(name) {
5
+ const value = process.env[name];
6
+ if (value === undefined || value === '') {
7
+ throw new FleetError(`required environment variable ${name} is not set — ` +
8
+ `it has no safe default and must be provided explicitly`);
9
+ }
10
+ return value;
11
+ }
@@ -9,6 +9,7 @@ export declare function execSafe(cmd: string, args: string[], opts?: {
9
9
  cwd?: string;
10
10
  env?: Record<string, string>;
11
11
  input?: string;
12
+ maxBuffer?: number;
12
13
  }): ExecResult;
13
14
  export declare function execGit(args: string[], opts: {
14
15
  cwd: string;
package/dist/core/exec.js CHANGED
@@ -7,6 +7,10 @@ export function execSafe(cmd, args, opts = {}) {
7
7
  encoding: 'utf-8',
8
8
  stdio: 'pipe',
9
9
  input: opts.input,
10
+ // node's default is 1mb. restic --json on a long-running snapshot emits
11
+ // hundreds of progress lines that easily blow past that; bump to 256mb
12
+ // so a multi-hour run can't hit ENOBUFS just from status chatter.
13
+ maxBuffer: opts.maxBuffer ?? 256 * 1024 * 1024,
10
14
  });
11
15
  if (result.error) {
12
16
  return {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Inter-process lock around a state-file path. Uses proper-lockfile (the same
3
+ * dependency the claude-cli runner uses for its mutex), which creates a
4
+ * <path>.lock directory atomically via mkdir(2).
5
+ *
6
+ * The wrapped path itself does not need to exist yet — `realpath: false`
7
+ * tells proper-lockfile to skip the realpath check, so we can lock around a
8
+ * registry/manifest file that hasn't been written for the first time. The
9
+ * parent directory of <path> must exist (we ensureDir below) so the .lock
10
+ * mkdir can succeed.
11
+ *
12
+ * Important: this lock is NOT reentrant. Callers should wrap the outermost
13
+ * read-modify-write boundary (e.g. a CLI command, an MCP tool handler, a
14
+ * cron entry) and let inner helpers do plain unlocked reads/writes; the lock
15
+ * bounds the whole RMW. Locking inside helpers that the outer caller already
16
+ * locked will deadlock.
17
+ */
18
+ export declare function withFileLock<T>(path: string, fn: () => Promise<T> | T): Promise<T>;
@@ -0,0 +1,44 @@
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import lockfile from 'proper-lockfile';
4
+ /**
5
+ * Inter-process lock around a state-file path. Uses proper-lockfile (the same
6
+ * dependency the claude-cli runner uses for its mutex), which creates a
7
+ * <path>.lock directory atomically via mkdir(2).
8
+ *
9
+ * The wrapped path itself does not need to exist yet — `realpath: false`
10
+ * tells proper-lockfile to skip the realpath check, so we can lock around a
11
+ * registry/manifest file that hasn't been written for the first time. The
12
+ * parent directory of <path> must exist (we ensureDir below) so the .lock
13
+ * mkdir can succeed.
14
+ *
15
+ * Important: this lock is NOT reentrant. Callers should wrap the outermost
16
+ * read-modify-write boundary (e.g. a CLI command, an MCP tool handler, a
17
+ * cron entry) and let inner helpers do plain unlocked reads/writes; the lock
18
+ * bounds the whole RMW. Locking inside helpers that the outer caller already
19
+ * locked will deadlock.
20
+ */
21
+ export async function withFileLock(path, fn) {
22
+ const dir = dirname(path);
23
+ if (!existsSync(dir))
24
+ mkdirSync(dir, { recursive: true });
25
+ const release = await lockfile.lock(path, {
26
+ // Inter-process contention is normally microseconds (one process writes,
27
+ // releases). Retry up to ~5s of backoff so a slow disk / paused process
28
+ // doesn't immediately error out the second caller.
29
+ retries: { retries: 5, factor: 1.5, minTimeout: 50, maxTimeout: 500 },
30
+ // If a process crashes mid-lock, the .lock dir's mtime stops being
31
+ // refreshed. Anyone waiting longer than `stale` ms treats the lock as
32
+ // abandoned and steals it. 30s is generous for the kinds of operations
33
+ // that touch the registry/manifest (a write is < 100ms typically).
34
+ stale: 30_000,
35
+ // Allow locking paths that don't exist on disk yet (first-write case).
36
+ realpath: false,
37
+ });
38
+ try {
39
+ return await fn();
40
+ }
41
+ finally {
42
+ await release();
43
+ }
44
+ }
@@ -4,23 +4,20 @@ import { load, findApp, save } from './registry.js';
4
4
  export function detectScenario(status) {
5
5
  if (!status.initialised)
6
6
  return 'fresh';
7
- if (status.remoteUrl && status.remoteUrl.includes('heskethwebdesign/'))
8
- return 'resume';
9
- if (status.remoteUrl && status.remoteUrl.includes('wrxck/'))
10
- return 'migrate';
11
7
  if (!status.remoteUrl)
12
8
  return 'no-remote';
13
- return 'fresh';
9
+ // a remote already on the configured org is a resume; any other org is a migrate.
10
+ return status.remoteUrl.includes(`${github.githubOrg()}/`) ? 'resume' : 'migrate';
14
11
  }
15
12
  export function describeOnboardPlan(scenario, repoName, _status) {
16
- const repoUrl = `git@github.com:heskethwebdesign/${repoName}.git`;
13
+ const repoUrl = `git@github.com:${github.githubOrg()}/${repoName}.git`;
17
14
  const steps = [];
18
15
  switch (scenario) {
19
16
  case 'fresh':
20
17
  steps.push('generate .gitignore');
21
18
  steps.push('git init -b main');
22
19
  steps.push('git add . && git commit -m "initial commit"');
23
- steps.push(`create private repo heskethwebdesign/${repoName}`);
20
+ steps.push(`create private repo ${github.githubOrg()}/${repoName}`);
24
21
  steps.push(`add remote origin ${repoUrl}`);
25
22
  steps.push('push main');
26
23
  steps.push('create and push develop branch');
@@ -29,7 +26,7 @@ export function describeOnboardPlan(scenario, repoName, _status) {
29
26
  break;
30
27
  case 'migrate':
31
28
  steps.push('ensure .gitignore exists');
32
- steps.push(`create private repo heskethwebdesign/${repoName}`);
29
+ steps.push(`create private repo ${github.githubOrg()}/${repoName}`);
33
30
  steps.push(`git remote set-url origin ${repoUrl}`);
34
31
  steps.push('git push --all origin');
35
32
  steps.push('ensure develop branch exists');
@@ -39,7 +36,7 @@ export function describeOnboardPlan(scenario, repoName, _status) {
39
36
  case 'no-remote':
40
37
  steps.push('ensure .gitignore exists');
41
38
  steps.push('commit any outstanding changes');
42
- steps.push(`create private repo heskethwebdesign/${repoName}`);
39
+ steps.push(`create private repo ${github.githubOrg()}/${repoName}`);
43
40
  steps.push(`add remote origin ${repoUrl}`);
44
41
  steps.push('git push --all origin');
45
42
  steps.push('ensure develop branch exists');
@@ -79,7 +76,7 @@ export function executeOnboard(scenario, cwd, repoName, appName, status) {
79
76
  gitCommit(cwd, 'Initial commit');
80
77
  steps.push('created initial commit');
81
78
  github.createRepo(repoName);
82
- steps.push(`created private repo heskethwebdesign/${repoName}`);
79
+ steps.push(`created private repo ${github.githubOrg()}/${repoName}`);
83
80
  gitAddRemote(cwd, 'origin', repoUrl);
84
81
  gitPush(cwd, 'main', true);
85
82
  steps.push('pushed main to origin');
@@ -90,7 +87,7 @@ export function executeOnboard(scenario, cwd, repoName, appName, status) {
90
87
  }
91
88
  case 'migrate': {
92
89
  github.createRepo(repoName);
93
- steps.push(`created private repo heskethwebdesign/${repoName}`);
90
+ steps.push(`created private repo ${github.githubOrg()}/${repoName}`);
94
91
  gitSetRemoteUrl(cwd, repoUrl);
95
92
  steps.push(`updated remote to ${repoUrl}`);
96
93
  gitPushAll(cwd);
@@ -110,7 +107,7 @@ export function executeOnboard(scenario, cwd, repoName, appName, status) {
110
107
  steps.push('created initial commit');
111
108
  }
112
109
  github.createRepo(repoName);
113
- steps.push(`created private repo heskethwebdesign/${repoName}`);
110
+ steps.push(`created private repo ${github.githubOrg()}/${repoName}`);
114
111
  gitAddRemote(cwd, 'origin', repoUrl);
115
112
  gitPushAll(cwd);
116
113
  steps.push('added remote and pushed all branches');
@@ -139,7 +136,7 @@ export function executeOnboard(scenario, cwd, repoName, appName, status) {
139
136
  const reg = load();
140
137
  const app = findApp(reg, appName);
141
138
  if (app) {
142
- app.gitRepo = `heskethwebdesign/${repoName}`;
139
+ app.gitRepo = `${github.githubOrg()}/${repoName}`;
143
140
  app.gitRemoteUrl = repoUrl;
144
141
  app.gitOnboardedAt = new Date().toISOString();
145
142
  save(reg);
@@ -1,4 +1,6 @@
1
- export declare const GITHUB_ORG = "heskethwebdesign";
1
+ /** the GitHub org this fleet instance publishes to — from operator config,
2
+ * with no default: guessing another operator's org is never correct. */
3
+ export declare function githubOrg(): string;
2
4
  export interface PullRequest {
3
5
  number: number;
4
6
  title: string;
@@ -3,8 +3,11 @@ import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { execSafe } from './exec.js';
5
5
  import { GitError } from './errors.js';
6
+ import { loadOperator } from './operator.js';
6
7
  import { assertAppName } from './validate.js';
7
- export const GITHUB_ORG = 'heskethwebdesign';
8
+ /** the GitHub org this fleet instance publishes to — from operator config,
9
+ * with no default: guessing another operator's org is never correct. */
10
+ export function githubOrg() { return loadOperator().githubOrg; }
8
11
  export function isGhAuthenticated() {
9
12
  return execSafe('gh', ['auth', 'status'], { timeout: 10_000 }).ok;
10
13
  }
@@ -15,25 +18,25 @@ export function requireGhAuth() {
15
18
  }
16
19
  export function repoExists(name) {
17
20
  assertAppName(name);
18
- return execSafe('gh', ['repo', 'view', `${GITHUB_ORG}/${name}`, '--json', 'name'], { timeout: 15_000 }).ok;
21
+ return execSafe('gh', ['repo', 'view', `${githubOrg()}/${name}`, '--json', 'name'], { timeout: 15_000 }).ok;
19
22
  }
20
23
  export function createRepo(name) {
21
24
  requireGhAuth();
22
25
  assertAppName(name);
23
26
  if (repoExists(name))
24
27
  return;
25
- const r = execSafe('gh', ['repo', 'create', `${GITHUB_ORG}/${name}`, '--private'], { timeout: 30_000 });
28
+ const r = execSafe('gh', ['repo', 'create', `${githubOrg()}/${name}`, '--private'], { timeout: 30_000 });
26
29
  if (!r.ok)
27
30
  throw new GitError(`failed to create repo: ${r.stderr}`);
28
31
  }
29
32
  export function getRepoUrl(name) {
30
- return `git@github.com:${GITHUB_ORG}/${name}.git`;
33
+ return `git@github.com:${githubOrg()}/${name}.git`;
31
34
  }
32
35
  export function createPullRequest(repo, opts) {
33
36
  requireGhAuth();
34
37
  const r = execSafe('gh', [
35
38
  'pr', 'create',
36
- '--repo', `${GITHUB_ORG}/${repo}`,
39
+ '--repo', `${githubOrg()}/${repo}`,
37
40
  '--title', opts.title,
38
41
  '--body', opts.body ?? '',
39
42
  '--head', opts.head,
@@ -63,7 +66,7 @@ export function listPullRequests(repo, state = 'open') {
63
66
  requireGhAuth();
64
67
  const r = execSafe('gh', [
65
68
  'pr', 'list',
66
- '--repo', `${GITHUB_ORG}/${repo}`,
69
+ '--repo', `${githubOrg()}/${repo}`,
67
70
  '--state', state,
68
71
  '--json', 'number,title,url,headRefName,baseRefName,state',
69
72
  ], { timeout: 15_000 });
@@ -97,7 +100,7 @@ export function protectBranch(repo, branch) {
97
100
  try {
98
101
  const r = execSafe('gh', [
99
102
  'api', '-X', 'PUT',
100
- `repos/${GITHUB_ORG}/${repo}/branches/${branch}/protection`,
103
+ `repos/${githubOrg()}/${repo}/branches/${branch}/protection`,
101
104
  '--input', tmpFile,
102
105
  ], { timeout: 15_000 });
103
106
  return r.ok;
@@ -18,6 +18,11 @@ export declare function effectivePolicy(app: AppEntry): LogPolicy;
18
18
  */
19
19
  export declare function buildComposeOverride(app: AppEntry, policy: LogPolicy): string;
20
20
  export declare function overridePath(app: AppEntry): string;
21
+ /** ensure the app repo's .gitignore covers .fleet/ so the auto-generated
22
+ * override file doesn't get committed accidentally. no-op when there's no
23
+ * .gitignore (operator may be using a different vcs or none at all) and
24
+ * idempotent — never appends a duplicate entry. */
25
+ export declare function ensureFleetGitignored(composePath: string): void;
21
26
  export declare function writeComposeOverride(app: AppEntry, policy: LogPolicy): string;
22
27
  export interface LogStatus {
23
28
  app: string;