total-recall-brain 3.40.0 → 3.40.2

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 (187) hide show
  1. package/README.md +152 -41
  2. package/bin/total-recall.mjs +16 -76
  3. package/docs/reference/cli-reference.md +10 -10
  4. package/extension/background.js +6 -2
  5. package/extension/config.js +8 -0
  6. package/extension/lib/brain-client.js +29 -4
  7. package/extension/options/options.html +1 -0
  8. package/extension/sidepanel/sidepanel.html +1 -0
  9. package/frontend/dist/assets/{index-CvQRdT2j.css → index-AGUes-VG.css} +1 -1
  10. package/frontend/dist/assets/index-Bt5iVPZj.js +5618 -0
  11. package/frontend/dist/index.html +2 -2
  12. package/frontend/src/api/keys.ts +9 -2
  13. package/frontend/src/api/plugins.ts +4 -0
  14. package/frontend/src/components/plugins/card/CardEngine.tsx +2 -1
  15. package/frontend/src/components/plugins/card/card.css +1 -1
  16. package/frontend/src/pages/IntegrationsPage.tsx +315 -8
  17. package/frontend/src/pages/PluginsPage.tsx +532 -87
  18. package/package.json +6 -5
  19. package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
  20. package/scaffold/.agent/skills/meta-harness/references/optimized/section-09.md +1 -1
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +10 -10
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -6
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +0 -1
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/src/browser-session.mjs +309 -4
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +44 -34
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/src/google-oauth.mjs +6 -4
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +1 -1
  28. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-rotation-recipes.mjs +118 -4
  29. package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/auth.mjs +2 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +3 -3
  31. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +24 -6
  33. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +164 -123
  34. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +65 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +76 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +19 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +290 -0
  38. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +446 -0
  39. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +152 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +71 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +36 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +35 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +175 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +18 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/code-quality/LICENSE +21 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/code-quality/README.md +84 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/code-quality/cli.mjs +79 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/check.mjs +6 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/report.mjs +6 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +27 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/snapshot.mjs +163 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +757 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/config.schema.json +86 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/detect.mjs +89 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +145 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +210 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/state.mjs +20 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +2869 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +28 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +186 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +18 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +272 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/code-quality-report.mjs +129 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/LICENSE +21 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/README.md +73 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/cli.mjs +375 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/echo-tool.mjs +23 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/group.mjs +262 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/project-id.mjs +74 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +2869 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +31 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +221 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/schemas/cli_command.schema.json +107 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/skills/composable-cli/SKILL.md +63 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/command.mjs +313 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compile.mjs +19 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compose.mjs +144 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/contract.mjs +110 -0
  79. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/library.mjs +23 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/registry.mjs +192 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/resolve.mjs +221 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/risk.mjs +126 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/ssss-documents.mjs +29 -0
  84. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +18 -0
  85. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +273 -0
  86. package/scaffold/.agent/skills/total-recall/plugins/embeddings/LICENSE +21 -0
  87. package/scaffold/.agent/skills/total-recall/plugins/embeddings/README.md +20 -0
  88. package/scaffold/.agent/skills/total-recall/plugins/embeddings/cli.mjs +310 -0
  89. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +2826 -0
  90. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +11 -0
  91. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +62 -0
  92. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +297 -0
  93. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/LICENSE +21 -0
  94. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +43 -0
  95. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/cli.mjs +258 -0
  96. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4005 -0
  97. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +20 -0
  98. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +125 -0
  99. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +18 -0
  100. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +278 -0
  101. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +9 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +6 -0
  103. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4721 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -21
  105. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +14 -1
  106. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +10 -0
  107. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/headscale-client.mjs +5 -1
  108. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +4 -13
  109. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-access-verify.mjs +22 -0
  110. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-files.mjs +18 -0
  111. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +2 -0
  112. package/scaffold/.agent/skills/total-recall/plugins/mesh/test-support/memory-root.mjs +14 -0
  113. package/scaffold/.agent/skills/total-recall/plugins/openwiki/LICENSE +21 -0
  114. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +35 -0
  115. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +124 -0
  116. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4097 -0
  117. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +12 -0
  118. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +165 -0
  119. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/ingest.mjs +127 -0
  120. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +18 -0
  121. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +279 -0
  122. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +539 -0
  123. package/scaffold/.agent/skills/total-recall/plugins/security/LICENSE +21 -0
  124. package/scaffold/.agent/skills/total-recall/plugins/security/README.md +67 -0
  125. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +932 -0
  126. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3256 -0
  127. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +25 -0
  128. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +297 -0
  129. package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +4 -0
  130. package/scaffold/.agent/skills/total-recall/plugins/security/skills/security/SKILL.md +37 -0
  131. package/scaffold/.agent/skills/total-recall/plugins/security/src/android.mjs +21 -0
  132. package/scaffold/.agent/skills/total-recall/plugins/security/src/devices.mjs +94 -0
  133. package/scaffold/.agent/skills/total-recall/plugins/security/src/machines.mjs +164 -0
  134. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +41 -0
  135. package/scaffold/.agent/skills/total-recall/plugins/security/src/windows.mjs +42 -0
  136. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +18 -0
  137. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +95 -0
  138. package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +98 -0
  139. package/scaffold/.agent/skills/total-recall/references/auth.md +1 -1
  140. package/src/cli/backup.mjs +71 -3
  141. package/src/cli/brief.mjs +33 -9
  142. package/src/cli/command.mjs +2 -7
  143. package/src/cli/config.mjs +1 -0
  144. package/src/cli/connect.mjs +5 -0
  145. package/src/cli/context.mjs +2 -1
  146. package/src/cli/daemon.mjs +36 -1
  147. package/src/cli/doctor.mjs +6 -0
  148. package/src/cli/harness.mjs +3 -0
  149. package/src/cli/help.mjs +26 -0
  150. package/src/cli/init.mjs +4 -4
  151. package/src/cli/memory-entry.mjs +1 -0
  152. package/src/cli/memory-init.mjs +2 -0
  153. package/src/cli/plugin/index.mjs +11 -0
  154. package/src/cli/plugin/install.mjs +2 -1
  155. package/src/cli/rebuild.mjs +3 -1
  156. package/src/cli/startup.mjs +2 -1
  157. package/src/cli/update.mjs +19 -0
  158. package/src/core/automatic-brain-updates.mjs +67 -0
  159. package/src/core/brain-updates.mjs +21 -2
  160. package/src/core/cli-commands.mjs +71 -0
  161. package/src/core/cli-maintenance.mjs +45 -0
  162. package/src/core/cli-quick-reference.mjs +20 -0
  163. package/src/core/cli-semantic-index.mjs +722 -0
  164. package/src/core/context-compiler.mjs +27 -1
  165. package/src/core/crons.mjs +2 -13
  166. package/src/core/daemon-control.mjs +6 -4
  167. package/src/core/daemon-loop.mjs +12 -8
  168. package/src/core/default-plugins.mjs +5 -0
  169. package/src/core/global-brain-updates.mjs +39 -0
  170. package/src/core/mesh-brain-updates.mjs +30 -0
  171. package/src/core/meta-harness.mjs +108 -0
  172. package/src/core/plugin-dependencies.mjs +2 -1
  173. package/src/core/plugin-loader.mjs +2 -1
  174. package/src/core/plugin-runner.mjs +12 -4
  175. package/src/core/plugin-setup.mjs +20 -0
  176. package/src/core/plugin-store.mjs +22 -4
  177. package/src/core/plugin-tasks.mjs +1 -1
  178. package/src/core/project-brain.mjs +2 -0
  179. package/src/core/project-status.mjs +64 -0
  180. package/src/core/required-plugins.mjs +46 -0
  181. package/src/core/scaffold-plugins.mjs +17 -3
  182. package/src/core/source-brain-updates.mjs +47 -0
  183. package/src/core/startup-health.mjs +5 -3
  184. package/src/core/surface.mjs +44 -10
  185. package/src/server/routes/extension.mjs +53 -8
  186. package/templates/onboarding-interview.md +1 -1
  187. package/frontend/dist/assets/index-SOVUlZqm.js +0 -5596
@@ -0,0 +1,163 @@
1
+ /**
2
+ * total-recall code-quality snapshot (--node <name> | --host <user@host>) [--port N] [--identity <file>] [--name <dir>] [--install] [--dry-run] [--json]
3
+ *
4
+ * Put the exact source under test on the sanctioned test node. The file list
5
+ * comes from git (tracked plus untracked files git does not ignore) in this
6
+ * repository and in the real repository behind each linked plugin, so ignored
7
+ * private state such as .env, the brain folder and node_modules never leaves
8
+ * the machine. A second deny list stops the snapshot if a secret-looking file
9
+ * is tracked anyway. A SHA-256 manifest is written beside the source so the
10
+ * node can prove which source it tested.
11
+ */
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import crypto from 'node:crypto';
16
+ import { spawnSync } from 'node:child_process';
17
+
18
+ export const PLUGINS_REL = path.join('.agent', 'skills', 'total-recall', 'plugins');
19
+
20
+ export const DENY = [
21
+ /(^|\/)\.env$/,
22
+ /(^|\/)\.env\.(?!example$)[^/]+$/,
23
+ /(^|\/)tr\.env$/,
24
+ /(^|\/)secrets\.enc$/,
25
+ /\.(pem|key|p12|pfx)$/,
26
+ /(^|\/)id_(rsa|ed25519|ecdsa)(\.pub)?$/,
27
+ /(^|\/)browser-profile\//,
28
+ // The live brain at the repository root; the scaffold's curated seeds under scaffold/.agent ship on purpose.
29
+ /^\.agent\/skills\/total-recall\/(config|logs|memory-[^/]+|plugin-state)\//,
30
+ ];
31
+
32
+ export function isDenied(rel) {
33
+ const unix = rel.split(path.sep).join('/');
34
+ return DENY.some((re) => re.test(unix));
35
+ }
36
+
37
+ /** Files git would ship from one repository: tracked and untracked-not-ignored, present on disk. */
38
+ export function listRepoFiles(repoDir, run = spawnSync) {
39
+ let r;
40
+ // EAGAIN means the machine is briefly out of processes, not that the repository is unreadable.
41
+ for (let attempt = 0; attempt < 4; attempt += 1) {
42
+ r = run('git', ['-C', repoDir, 'ls-files', '-co', '--exclude-standard', '-z'], { encoding: 'buffer', maxBuffer: 256 * 1024 * 1024 });
43
+ if (r.error?.code !== 'EAGAIN') break;
44
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (attempt + 1));
45
+ }
46
+ if (r.error || r.status !== 0) throw new Error(`${repoDir} is not a readable git repository: ${(r.stderr || '').toString().trim() || r.error?.message}`);
47
+ return r.stdout.toString('utf8').split('\0').filter(Boolean)
48
+ .filter((rel) => { try { return fs.lstatSync(path.join(repoDir, rel)).isFile(); } catch { return false; } });
49
+ }
50
+
51
+ /**
52
+ * Files the npm package ships. They are public by definition, and some (such as a
53
+ * bundled scaffold) are git-ignored, so the git list alone misses them.
54
+ */
55
+ export function listPackageFiles(root, run = spawnSync) {
56
+ if (!fs.existsSync(path.join(root, 'package.json'))) return [];
57
+ const r = run('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'], { cwd: root, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
58
+ if (r.error || r.status !== 0) throw new Error(`npm pack --dry-run failed in ${root}: ${(r.stderr || '').trim().split('\n').pop() || r.error?.message}`);
59
+ const text = String(r.stdout || '');
60
+ const parsed = JSON.parse(text.slice(text.indexOf('[')));
61
+ return (parsed[0]?.files || []).map((f) => f.path.split('/').join(path.sep))
62
+ .filter((rel) => { try { return fs.lstatSync(path.join(root, rel)).isFile(); } catch { return false; } });
63
+ }
64
+
65
+ /** Every file to copy, as { src, rel }. Linked plugins contribute their own repository's files. */
66
+ export function buildSnapshot(root, { run = spawnSync, includePackage = true } = {}) {
67
+ const own = new Set(listRepoFiles(root, run));
68
+ if (includePackage) for (const rel of listPackageFiles(root, run)) own.add(rel);
69
+ const entries = [...own].sort()
70
+ .filter((rel) => !rel.startsWith(PLUGINS_REL + path.sep) && !rel.split(path.sep).includes('node_modules'))
71
+ .map((rel) => ({ src: path.join(root, rel), rel }));
72
+ const warnings = [];
73
+ // The gate list is repo state that git ignores; the test node needs it to run the same gates.
74
+ const gateConfig = path.join('.agent', 'skills', 'code-quality', 'config.json');
75
+ if (fs.existsSync(path.join(root, gateConfig))) entries.push({ src: path.join(root, gateConfig), rel: gateConfig });
76
+ const pluginsDir = path.join(root, PLUGINS_REL);
77
+ if (fs.existsSync(pluginsDir)) {
78
+ for (const name of fs.readdirSync(pluginsDir).sort()) {
79
+ if (name.startsWith('.')) continue;
80
+ const linkPath = path.join(pluginsDir, name);
81
+ let real;
82
+ try { real = fs.realpathSync(linkPath); } catch { warnings.push(`${name}: link target is missing; skipped`); continue; }
83
+ if (!fs.existsSync(path.join(real, '.git'))) { warnings.push(`${name}: not a git repository; skipped rather than guessing which files are safe`); continue; }
84
+ for (const rel of listRepoFiles(real, run)) entries.push({ src: path.join(real, rel), rel: path.join(PLUGINS_REL, name, rel) });
85
+ }
86
+ }
87
+ const denied = entries.filter((e) => isDenied(e.rel)).map((e) => e.rel);
88
+ return { entries, denied, warnings };
89
+ }
90
+
91
+ export function manifestOf(entries) {
92
+ return entries
93
+ .map((e) => `${crypto.createHash('sha256').update(fs.readFileSync(e.src)).digest('hex')} ${e.rel.split(path.sep).join('/')}`)
94
+ .sort((a, b) => (a.slice(66) < b.slice(66) ? -1 : 1))
95
+ .join('\n') + '\n';
96
+ }
97
+
98
+ function flag(args, name) {
99
+ const i = args.indexOf(name);
100
+ return i >= 0 ? args[i + 1] : undefined;
101
+ }
102
+
103
+ /** Resolve the node through Total Recall's recorded mesh access, never a guessed login. */
104
+ export function resolveTarget(args, run = spawnSync) {
105
+ const host = flag(args, '--host');
106
+ if (host) {
107
+ if (!/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+$/.test(host)) throw new Error('--host must be user@host');
108
+ const [user, h] = host.split('@');
109
+ return { user, host: h, port: Number(flag(args, '--port') || 22), identity: flag(args, '--identity') || null };
110
+ }
111
+ const node = flag(args, '--node');
112
+ if (!node) throw new Error('snapshot needs --node <name> (recorded mesh access) or --host <user@host>');
113
+ const r = run('total-recall', ['mesh', 'access', node], { encoding: 'utf8' });
114
+ const text = String(r.stdout || '');
115
+ const start = text.indexOf('{\n');
116
+ let access = null;
117
+ try { access = JSON.parse(text.slice(start, text.lastIndexOf('}') + 1)); } catch { /* reported below */ }
118
+ if (r.status !== 0 || !access?.user || !access?.host) throw new Error(`No recorded mesh access for ${node}; record it with: total-recall mesh access ${node} --user <login>`);
119
+ return { user: access.user, host: access.host, port: Number(access.port || 22), identity: access.identity_file || null };
120
+ }
121
+
122
+ export default function snapshot(args = [], { root = process.cwd(), run = spawnSync } = {}) {
123
+ const repoRoot = String(run('git', ['-C', root, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' }).stdout || '').trim() || root;
124
+ const name = flag(args, '--name') || `${path.basename(repoRoot)}-${new Date().toISOString().slice(0, 10)}`;
125
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) throw new Error('--name may contain letters, digits, dot, dash and underscore only');
126
+ const { entries, denied, warnings } = buildSnapshot(repoRoot, { run });
127
+ if (denied.length) throw new Error(`Refusing to snapshot: secret-looking files are tracked or not ignored:\n ${denied.join('\n ')}`);
128
+ const result = { name, files: entries.length, warnings };
129
+ if (args.includes('--dry-run')) return { ...result, dryRun: true };
130
+
131
+ const target = resolveTarget(args, run);
132
+ const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'cq-snapshot-'));
133
+ try {
134
+ for (const e of entries) {
135
+ const out = path.join(stage, e.rel);
136
+ fs.mkdirSync(path.dirname(out), { recursive: true });
137
+ fs.copyFileSync(e.src, out);
138
+ const sourceStat = fs.statSync(e.src);
139
+ fs.chmodSync(out, sourceStat.mode & 0o777);
140
+ // Freshness gates compare source and bundle times. Copying in path order
141
+ // must not make a current bundle appear older than its sources.
142
+ fs.utimesSync(out, sourceStat.atime, sourceStat.mtime);
143
+ }
144
+ const manifest = manifestOf(entries);
145
+ fs.writeFileSync(path.join(stage, 'source.sha256'), manifest);
146
+ const ssh = ['ssh', '-o', 'BatchMode=yes', '-p', String(target.port), ...(target.identity ? ['-i', target.identity] : [])].join(' ');
147
+ const dest = `tr-checks/${name}/`;
148
+ const mk = run('ssh', ['-o', 'BatchMode=yes', '-p', String(target.port), ...(target.identity ? ['-i', target.identity] : []), `${target.user}@${target.host}`, `mkdir -p ${dest}`], { encoding: 'utf8' });
149
+ if (mk.status !== 0) throw new Error(`Could not prepare ${dest} on ${target.host}: ${(mk.stderr || '').trim()}`);
150
+ // --delete keeps the node copy identical to the source list; node_modules installed there are preserved.
151
+ const rs = run('rsync', ['-a', '--delete', '--exclude', 'node_modules', '-e', ssh, `${stage}/`, `${target.user}@${target.host}:${dest}`], { encoding: 'utf8' });
152
+ if (rs.status !== 0) throw new Error(`rsync to ${target.host} failed: ${(rs.stderr || '').trim()}`);
153
+ if (args.includes('--install')) {
154
+ // The snapshot carries source only; the gates need the dependencies installed beside it.
155
+ const inst = run('ssh', ['-o', 'BatchMode=yes', '-p', String(target.port), ...(target.identity ? ['-i', target.identity] : []), `${target.user}@${target.host}`, `export PATH=/opt/homebrew/bin:/usr/local/bin:$PATH; cd ${dest} && npm ci --include=dev --no-audit --no-fund`], { encoding: 'utf8' });
156
+ if (inst.status !== 0) throw new Error(`npm ci on ${target.host} failed: ${(inst.stderr || inst.stdout || '').trim().slice(-400)}`);
157
+ result.installed = true;
158
+ }
159
+ return { ...result, node: `${target.user}@${target.host}`, dest: `~/${dest}`, manifestSha256: crypto.createHash('sha256').update(manifest).digest('hex') };
160
+ } finally {
161
+ fs.rmSync(stage, { recursive: true, force: true });
162
+ }
163
+ }