@mnemonik/scanner 5.151.0 → 5.151.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.
@@ -0,0 +1,107 @@
1
+ import { readFile, readdir } from 'fs/promises';
2
+ import { join, resolve } from 'path';
3
+ /** Directories to skip during discovery walk */
4
+ const SKIP_DIRS = new Set([
5
+ 'node_modules',
6
+ '.git',
7
+ 'dist',
8
+ 'build',
9
+ '.next',
10
+ '.nuxt',
11
+ '.output',
12
+ '__pycache__',
13
+ '.venv',
14
+ 'venv',
15
+ '.tox',
16
+ 'target',
17
+ '.cache',
18
+ 'coverage',
19
+ '.turbo',
20
+ '.vercel',
21
+ '.svelte-kit',
22
+ ]);
23
+ export class ProjectDiscovery {
24
+ roots;
25
+ maxDepth;
26
+ timeoutMs;
27
+ constructor(roots, options) {
28
+ this.roots = roots;
29
+ this.maxDepth = options?.maxDepth ?? 3;
30
+ this.timeoutMs = options?.timeoutMs ?? 30_000;
31
+ }
32
+ /**
33
+ * Discover all projects with .mnemonik.json files under the configured roots.
34
+ * Deduplicates by projectId (same project found at multiple paths = first wins).
35
+ */
36
+ async discover() {
37
+ const seen = new Map();
38
+ for (const root of this.roots) {
39
+ const absRoot = resolve(root.replace(/^~/, process.env.HOME || ''));
40
+ try {
41
+ await this.walkWithTimeout(absRoot, 0, seen);
42
+ }
43
+ catch (err) {
44
+ if (err instanceof DiscoveryTimeoutError) {
45
+ console.warn(`[scanner] Discovery timeout for root: ${absRoot} (>${this.timeoutMs}ms)`);
46
+ }
47
+ else {
48
+ console.warn(`[scanner] Error scanning root ${absRoot}:`, err.message);
49
+ }
50
+ }
51
+ }
52
+ return Array.from(seen.values());
53
+ }
54
+ async walkWithTimeout(dir, depth, seen) {
55
+ const deadline = Date.now() + this.timeoutMs;
56
+ await this.walk(dir, depth, seen, deadline);
57
+ }
58
+ async walk(dir, depth, seen, deadline) {
59
+ if (depth > this.maxDepth)
60
+ return;
61
+ if (Date.now() > deadline)
62
+ throw new DiscoveryTimeoutError();
63
+ // Check for .mnemonik.json in this directory
64
+ const configPath = join(dir, '.mnemonik.json');
65
+ try {
66
+ const raw = await readFile(configPath, 'utf-8');
67
+ const parsed = JSON.parse(raw);
68
+ if (typeof parsed.projectId === 'string' && parsed.projectId.length > 0) {
69
+ if (!seen.has(parsed.projectId)) {
70
+ seen.set(parsed.projectId, {
71
+ projectId: parsed.projectId,
72
+ path: dir,
73
+ projectName: typeof parsed.projectName === 'string' ? parsed.projectName : undefined,
74
+ });
75
+ }
76
+ }
77
+ // Don't recurse into subdirectories of a project — the project owns this tree
78
+ return;
79
+ }
80
+ catch {
81
+ // No .mnemonik.json here, continue walking
82
+ }
83
+ // Recurse into subdirectories
84
+ try {
85
+ const entries = await readdir(dir, { withFileTypes: true });
86
+ for (const entry of entries) {
87
+ if (!entry.isDirectory())
88
+ continue;
89
+ if (SKIP_DIRS.has(entry.name))
90
+ continue;
91
+ if (entry.name.startsWith('.') && entry.name !== '.mnemonik')
92
+ continue;
93
+ await this.walk(join(dir, entry.name), depth + 1, seen, deadline);
94
+ }
95
+ }
96
+ catch {
97
+ // Permission denied or inaccessible directory
98
+ }
99
+ }
100
+ }
101
+ class DiscoveryTimeoutError extends Error {
102
+ constructor() {
103
+ super('Discovery walk timed out');
104
+ this.name = 'DiscoveryTimeoutError';
105
+ }
106
+ }
107
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function runDoctor(): Promise<void>;
package/dist/doctor.js ADDED
@@ -0,0 +1,233 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync, readFileSync, readdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import { fileURLToPath } from 'url';
6
+ import { pidIsScanner } from './pid.js';
7
+ /**
8
+ * `mnemonik-scanner doctor` — the guard that turns the prose upgrade procedure
9
+ * into an executable check. It asserts the invariants that every past botched
10
+ * reinstall violated, and prints the one blessed remediation. Any single ✗
11
+ * exits non-zero so a wrapper (or a human) can react.
12
+ *
13
+ * Invariants:
14
+ * 1. Exactly one `mnemonik-scanner` on PATH, inside the user npm prefix
15
+ * (a second copy under /usr is the classic PATH-shadow version skew).
16
+ * 2. If a systemd user unit exists, it is active and its ExecStart points at
17
+ * that same binary.
18
+ * 3. Exactly one live daemon, and under systemd its PID == the unit MainPID
19
+ * (a stray `node …/scanner/dist/index.js` is a duplicate local-build daemon).
20
+ * 4. No leftover legacy per-project daemons.
21
+ * 5. Installed version is not behind npm latest (warning, not failure).
22
+ */
23
+ const HOME = homedir();
24
+ const MNEMONIK_DIR = join(HOME, '.mnemonik');
25
+ const PID_FILE = join(MNEMONIK_DIR, 'daemon.pid');
26
+ const LEGACY_DAEMONS_DIR = join(MNEMONIK_DIR, 'daemons');
27
+ const UNIT_FILE = join(HOME, '.config/systemd/user/mnemonik-scanner.service');
28
+ function sh(cmd, args, timeoutMs = 5000) {
29
+ try {
30
+ return execFileSync(cmd, args, {
31
+ encoding: 'utf-8',
32
+ stdio: ['ignore', 'pipe', 'ignore'],
33
+ timeout: timeoutMs,
34
+ }).trim();
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ function pidAlive(pid) {
41
+ try {
42
+ process.kill(pid, 0);
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ // Existence alone is not identity — after PID reuse the recorded number can
48
+ // belong to an unrelated process. A live-but-foreign PID is dead to us.
49
+ return pidIsScanner(pid);
50
+ }
51
+ function installedVersion() {
52
+ try {
53
+ const here = fileURLToPath(new URL('.', import.meta.url));
54
+ const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf-8'));
55
+ return pkg.version ?? null;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ export async function runDoctor() {
62
+ const checks = [];
63
+ // 1. Binary uniqueness + location.
64
+ const prefix = sh('npm', ['config', 'get', 'prefix']);
65
+ const whichOut = sh('which', ['-a', 'mnemonik-scanner']);
66
+ const binPaths = whichOut
67
+ ? whichOut
68
+ .split('\n')
69
+ .map((s) => s.trim())
70
+ .filter(Boolean)
71
+ : [];
72
+ if (binPaths.length === 0) {
73
+ checks.push({
74
+ level: 'fail',
75
+ label: 'binary on PATH',
76
+ detail: 'mnemonik-scanner not found on PATH',
77
+ });
78
+ }
79
+ else if (binPaths.length > 1) {
80
+ checks.push({
81
+ level: 'fail',
82
+ label: 'single binary on PATH',
83
+ detail: `found ${binPaths.length} copies (PATH shadow → version skew):\n ${binPaths.join('\n ')}`,
84
+ });
85
+ }
86
+ else if (prefix && !binPaths[0].startsWith(prefix)) {
87
+ checks.push({
88
+ level: 'warn',
89
+ label: 'binary in user npm prefix',
90
+ detail: `${binPaths[0]} is not under npm prefix ${prefix}`,
91
+ });
92
+ }
93
+ else {
94
+ checks.push({ level: 'ok', label: 'single binary on PATH', detail: binPaths[0] });
95
+ }
96
+ const binPath = binPaths.length === 1 ? binPaths[0] : null;
97
+ // 2. systemd unit health + ExecStart match.
98
+ let unitMainPid = null;
99
+ const hasUnit = existsSync(UNIT_FILE);
100
+ if (hasUnit) {
101
+ const show = sh('systemctl', [
102
+ '--user',
103
+ 'show',
104
+ 'mnemonik-scanner',
105
+ '-p',
106
+ 'ActiveState',
107
+ '-p',
108
+ 'MainPID',
109
+ '-p',
110
+ 'ExecStart',
111
+ ]);
112
+ const active = /ActiveState=active/.test(show ?? '');
113
+ const mainPidMatch = /MainPID=(\d+)/.exec(show ?? '');
114
+ unitMainPid = mainPidMatch ? parseInt(mainPidMatch[1], 10) : null;
115
+ const execMatch = /path=([^\s;]+)/.exec(show ?? '');
116
+ const execPath = execMatch ? execMatch[1] : null;
117
+ if (!active) {
118
+ checks.push({ level: 'fail', label: 'systemd unit active', detail: 'unit is not active' });
119
+ }
120
+ else if (binPath && execPath && execPath !== binPath) {
121
+ checks.push({
122
+ level: 'fail',
123
+ label: 'ExecStart matches binary',
124
+ detail: `unit runs ${execPath} but PATH resolves ${binPath}`,
125
+ });
126
+ }
127
+ else {
128
+ checks.push({
129
+ level: 'ok',
130
+ label: 'systemd unit active',
131
+ detail: `MainPID ${unitMainPid ?? '?'}`,
132
+ });
133
+ }
134
+ }
135
+ else {
136
+ checks.push({
137
+ level: 'warn',
138
+ label: 'systemd unit',
139
+ detail: 'no user unit — daemon must be supervised another way',
140
+ });
141
+ }
142
+ // 3. Single live daemon; under systemd it must equal MainPID.
143
+ let filePid = null;
144
+ try {
145
+ filePid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
146
+ if (Number.isNaN(filePid))
147
+ filePid = null;
148
+ }
149
+ catch {
150
+ filePid = null;
151
+ }
152
+ const psOut = sh('ps', ['-eo', 'pid,cmd']);
153
+ const daemonPids = (psOut ?? '')
154
+ .split('\n')
155
+ // Only the long-running `start` daemon counts — never a `doctor`/`status`/
156
+ // `log`/`stop` invocation (which also runs …/scanner/dist/index.js), and
157
+ // never this doctor process itself.
158
+ .filter((l) => /(mnemonik-scanner|scanner\/dist\/index\.js)/.test(l) && /\bstart\b/.test(l))
159
+ .map((l) => parseInt(l.trim().split(/\s+/)[0], 10))
160
+ .filter((n) => !Number.isNaN(n) && n !== process.pid);
161
+ if (daemonPids.length === 0) {
162
+ checks.push({ level: 'fail', label: 'daemon running', detail: 'no daemon process found' });
163
+ }
164
+ else if (daemonPids.length > 1) {
165
+ checks.push({
166
+ level: 'fail',
167
+ label: 'single daemon',
168
+ detail: `${daemonPids.length} daemon processes (duplicate) — PIDs ${daemonPids.join(', ')}`,
169
+ });
170
+ }
171
+ else if (hasUnit && unitMainPid && daemonPids[0] !== unitMainPid) {
172
+ checks.push({
173
+ level: 'fail',
174
+ label: 'daemon is the systemd one',
175
+ detail: `running PID ${daemonPids[0]} != unit MainPID ${unitMainPid} (stray local-build daemon)`,
176
+ });
177
+ }
178
+ else if (filePid && !pidAlive(filePid)) {
179
+ checks.push({
180
+ level: 'warn',
181
+ label: 'PID file fresh',
182
+ detail: `stale ${PID_FILE} (PID ${filePid} dead)`,
183
+ });
184
+ }
185
+ else {
186
+ checks.push({ level: 'ok', label: 'single daemon', detail: `PID ${daemonPids[0]}` });
187
+ }
188
+ // 4. Legacy per-project daemons.
189
+ try {
190
+ const legacy = readdirSync(LEGACY_DAEMONS_DIR)
191
+ .filter((f) => f.endsWith('.pid'))
192
+ .map((f) => parseInt(readFileSync(join(LEGACY_DAEMONS_DIR, f), 'utf-8').trim(), 10))
193
+ .filter((n) => !Number.isNaN(n) && pidAlive(n));
194
+ if (legacy.length > 0) {
195
+ checks.push({
196
+ level: 'fail',
197
+ label: 'no legacy daemons',
198
+ detail: `legacy per-project daemons alive: ${legacy.join(', ')}`,
199
+ });
200
+ }
201
+ }
202
+ catch {
203
+ // dir absent — nothing to check
204
+ }
205
+ // 5. Version vs npm latest (informational).
206
+ const local = installedVersion();
207
+ const latest = sh('npm', ['view', '@mnemonik/scanner', 'version'], 15000);
208
+ if (local && latest && local !== latest) {
209
+ checks.push({
210
+ level: 'warn',
211
+ label: 'up to date',
212
+ detail: `installed ${local}, npm latest ${latest} — run: make scanner-deploy`,
213
+ });
214
+ }
215
+ else if (local) {
216
+ checks.push({ level: 'ok', label: 'up to date', detail: `v${local}` });
217
+ }
218
+ // Report.
219
+ const icon = { ok: '✓', warn: '⚠', fail: '✗' };
220
+ console.log('mnemonik-scanner doctor\n');
221
+ for (const c of checks) {
222
+ console.log(` ${icon[c.level]} ${c.label}${c.detail ? `: ${c.detail}` : ''}`);
223
+ }
224
+ const failed = checks.filter((c) => c.level === 'fail');
225
+ if (failed.length > 0) {
226
+ console.log(`\n${failed.length} problem(s). Canonical fix: \`make scanner-deploy\` (build → publish shared-first → npm i -g → systemctl restart → re-verify).\n` +
227
+ 'Do NOT hand-copy dist/ or run `make daemon-*` on a systemd host — those are what cause this.');
228
+ process.exit(1);
229
+ }
230
+ console.log('\nAll invariants hold.');
231
+ process.exit(0);
232
+ }
233
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Append-only sink that mirrors the daemon's console output to a file the
3
+ * `mnemonik-scanner log` command can tail. Rotates to `<file>.old` once the
4
+ * file would pass `maxSize`, so a daemon that runs for weeks can't grow it
5
+ * without bound. Every operation is best-effort: a logging failure must never
6
+ * crash the daemon, so all filesystem errors are swallowed.
7
+ */
8
+ export declare function createRotatingFileSink(logFile: string, maxSize: number): (chunk: string) => void;
9
+ /**
10
+ * Tee `process.stdout`/`process.stderr` into `logFile` on top of their normal
11
+ * destination. This is what makes `mnemonik-scanner log` reflect live activity
12
+ * regardless of how the daemon is supervised: under systemd stdout is wired to
13
+ * the journal (a socket), under a bare shell to the terminal — either way the
14
+ * file now receives the same lines. Call once at daemon start, before the
15
+ * daemon writes anything worth capturing.
16
+ */
17
+ export declare function installFileLogging(logFile: string, maxSize: number): void;
@@ -0,0 +1,70 @@
1
+ import { appendFileSync, renameSync, statSync } from 'fs';
2
+ /**
3
+ * Append-only sink that mirrors the daemon's console output to a file the
4
+ * `mnemonik-scanner log` command can tail. Rotates to `<file>.old` once the
5
+ * file would pass `maxSize`, so a daemon that runs for weeks can't grow it
6
+ * without bound. Every operation is best-effort: a logging failure must never
7
+ * crash the daemon, so all filesystem errors are swallowed.
8
+ */
9
+ export function createRotatingFileSink(logFile, maxSize) {
10
+ let size = 0;
11
+ try {
12
+ size = statSync(logFile).size;
13
+ }
14
+ catch {
15
+ size = 0;
16
+ }
17
+ return (chunk) => {
18
+ if (!chunk)
19
+ return;
20
+ try {
21
+ const bytes = Buffer.byteLength(chunk);
22
+ if (size > 0 && size + bytes > maxSize) {
23
+ try {
24
+ renameSync(logFile, logFile + '.old');
25
+ }
26
+ catch {
27
+ // Rotation failed (e.g. cross-device) — keep appending to the
28
+ // current file rather than losing the line.
29
+ }
30
+ size = 0;
31
+ }
32
+ appendFileSync(logFile, chunk);
33
+ size += bytes;
34
+ }
35
+ catch {
36
+ // Never let logging break the daemon.
37
+ }
38
+ };
39
+ }
40
+ /**
41
+ * Tee `process.stdout`/`process.stderr` into `logFile` on top of their normal
42
+ * destination. This is what makes `mnemonik-scanner log` reflect live activity
43
+ * regardless of how the daemon is supervised: under systemd stdout is wired to
44
+ * the journal (a socket), under a bare shell to the terminal — either way the
45
+ * file now receives the same lines. Call once at daemon start, before the
46
+ * daemon writes anything worth capturing.
47
+ */
48
+ export function installFileLogging(logFile, maxSize) {
49
+ const sink = createRotatingFileSink(logFile, maxSize);
50
+ for (const stream of [process.stdout, process.stderr]) {
51
+ const original = stream.write.bind(stream);
52
+ stream.write = ((chunk, encoding, cb) => {
53
+ try {
54
+ if (typeof chunk === 'string') {
55
+ sink(chunk);
56
+ }
57
+ else if (Buffer.isBuffer(chunk)) {
58
+ const enc = typeof encoding === 'string' ? encoding : 'utf8';
59
+ sink(chunk.toString(enc));
60
+ }
61
+ }
62
+ catch {
63
+ // ignore — mirroring is best-effort
64
+ }
65
+ // Preserve the real stream's overloads (chunk, cb) / (chunk, enc, cb).
66
+ return original(chunk, encoding, cb);
67
+ });
68
+ }
69
+ }
70
+ //# sourceMappingURL=fileLog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileLog.js","sourceRoot":"","sources":["../src/fileLog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAe,EAAE,OAAe;IACrE,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,CAAC;QACH,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,CAAC,CAAC;IACX,CAAC;IAED,OAAO,CAAC,KAAa,EAAQ,EAAE;QAC7B,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YACvC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK,GAAG,OAAO,EAAE,CAAC;gBACvC,IAAI,CAAC;oBACH,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;gBACxC,CAAC;gBAAC,MAAM,CAAC;oBACP,8DAA8D;oBAC9D,4CAA4C;gBAC9C,CAAC;gBACD,IAAI,GAAG,CAAC,CAAC;YACX,CAAC;YACD,cAAc,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC/B,IAAI,IAAI,KAAK,CAAC;QAChB,CAAC;QAAC,MAAM,CAAC;YACP,sCAAsC;QACxC,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,OAAe;IACjE,MAAM,IAAI,GAAG,sBAAsB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAEtD,KAAK,MAAM,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAoC,CAAC;QAC9E,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,KAAc,EAAE,QAAkB,EAAE,EAAY,EAAW,EAAE;YAC5E,IAAI,CAAC;gBACH,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;oBAC9B,IAAI,CAAC,KAAK,CAAC,CAAC;gBACd,CAAC;qBAAM,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;oBAClC,MAAM,GAAG,GAAG,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAE,QAA2B,CAAC,CAAC,CAAC,MAAM,CAAC;oBACjF,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,oCAAoC;YACtC,CAAC;YACD,uEAAuE;YACvE,OAAO,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;QACvC,CAAC,CAAwB,CAAC;IAC5B,CAAC;AACH,CAAC"}
package/dist/git.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Git commit extraction on the scanner daemon side.
3
+ *
4
+ * The production server runs in ECS and cannot `git log` the user's repo —
5
+ * the repo lives on the user's machine, next to the daemon. We run `git log`
6
+ * here and push the parsed commits alongside scan results. Server-side
7
+ * `GitMiner.mineCommits` consumes them without touching the filesystem.
8
+ */
9
+ export interface ParsedCommit {
10
+ sha: string;
11
+ author: string;
12
+ date: string;
13
+ message: string;
14
+ files: string[];
15
+ }
16
+ /**
17
+ * True when `repoRoot/.git` exists and `git log -1` returns a commit.
18
+ * Non-git projects, `git init` with no commits, and corrupted repos all
19
+ * return false without throwing.
20
+ */
21
+ export declare function probeGit(repoRoot: string): Promise<boolean>;
22
+ /**
23
+ * Fetch commits between `sinceCommit` and HEAD (or the last MAX_COMMITS on
24
+ * first run). Returns [] on failure — caller omits the field from the push.
25
+ *
26
+ * Uses `-z` NUL-separated output so `--name-only` file entries cannot collide
27
+ * with commit metadata. Parser mirrors the server-side token-walking parser
28
+ * in GitMiner.parseLogOutput.
29
+ */
30
+ export declare function fetchCommits(repoRoot: string, sinceCommit: string | null): Promise<ParsedCommit[]>;
31
+ export declare function parseLogOutput(raw: string): ParsedCommit[];
package/dist/git.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Git commit extraction on the scanner daemon side.
3
+ *
4
+ * The production server runs in ECS and cannot `git log` the user's repo —
5
+ * the repo lives on the user's machine, next to the daemon. We run `git log`
6
+ * here and push the parsed commits alongside scan results. Server-side
7
+ * `GitMiner.mineCommits` consumes them without touching the filesystem.
8
+ */
9
+ import { execFile } from 'child_process';
10
+ import { promisify } from 'util';
11
+ import { access } from 'fs/promises';
12
+ import { join } from 'path';
13
+ const execFileAsync = promisify(execFile);
14
+ // Keep in sync with server-side MAX_COMMITS_PER_RUN in GitMiner.ts and the
15
+ // `commits` array cap in scanPushSchema.
16
+ const MAX_COMMITS = 100;
17
+ const GIT_TIMEOUT_MS = 20_000;
18
+ /**
19
+ * True when `repoRoot/.git` exists and `git log -1` returns a commit.
20
+ * Non-git projects, `git init` with no commits, and corrupted repos all
21
+ * return false without throwing.
22
+ */
23
+ export async function probeGit(repoRoot) {
24
+ try {
25
+ await access(join(repoRoot, '.git'));
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ try {
31
+ const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%H'], {
32
+ cwd: repoRoot,
33
+ encoding: 'utf-8',
34
+ timeout: GIT_TIMEOUT_MS,
35
+ });
36
+ return /^[0-9a-f]{40}/m.test(stdout);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ /**
43
+ * Fetch commits between `sinceCommit` and HEAD (or the last MAX_COMMITS on
44
+ * first run). Returns [] on failure — caller omits the field from the push.
45
+ *
46
+ * Uses `-z` NUL-separated output so `--name-only` file entries cannot collide
47
+ * with commit metadata. Parser mirrors the server-side token-walking parser
48
+ * in GitMiner.parseLogOutput.
49
+ */
50
+ export async function fetchCommits(repoRoot, sinceCommit) {
51
+ const args = [
52
+ 'log',
53
+ '-z',
54
+ '--format=%H%n%an%n%aI%n%B',
55
+ '--name-only',
56
+ `--max-count=${MAX_COMMITS}`,
57
+ ];
58
+ if (sinceCommit)
59
+ args.push(`${sinceCommit}..HEAD`);
60
+ let raw;
61
+ try {
62
+ const result = await execFileAsync('git', args, {
63
+ cwd: repoRoot,
64
+ encoding: 'utf-8',
65
+ timeout: GIT_TIMEOUT_MS,
66
+ maxBuffer: 50 * 1024 * 1024,
67
+ });
68
+ raw = result.stdout;
69
+ }
70
+ catch (err) {
71
+ const msg = err.message;
72
+ // "fatal: Invalid revision range <sha>..HEAD" — the daemon's cached
73
+ // last_mined_commit is stale (e.g. history was rewritten or the server
74
+ // was reseeded). Fall back to a fresh full-history scan.
75
+ if (sinceCommit && /Invalid (?:revision range|symmetric difference expression)/i.test(msg)) {
76
+ return fetchCommits(repoRoot, null);
77
+ }
78
+ return [];
79
+ }
80
+ return parseLogOutput(raw);
81
+ }
82
+ export function parseLogOutput(raw) {
83
+ const commits = [];
84
+ const tokens = raw.split('\0').filter((t) => t.length > 0);
85
+ let current = null;
86
+ for (const token of tokens) {
87
+ if (token.includes('\n')) {
88
+ if (current)
89
+ commits.push(current);
90
+ const lines = token.split('\n');
91
+ const sha = lines[0]?.trim() ?? '';
92
+ const author = lines[1]?.trim() ?? '';
93
+ const date = lines[2]?.trim() ?? '';
94
+ const message = lines.slice(3).join('\n').trim();
95
+ if (!sha || !author || !date || !message) {
96
+ current = null;
97
+ continue;
98
+ }
99
+ current = { sha, author, date, message, files: [] };
100
+ }
101
+ else if (current) {
102
+ const file = token.trim();
103
+ if (file.length > 0)
104
+ current.files.push(file);
105
+ }
106
+ }
107
+ if (current)
108
+ commits.push(current);
109
+ return commits;
110
+ }
111
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reject plaintext http:// server URLs except when talking to a local
4
+ * daemon on the same machine (localhost/127.0.0.1/[::1]) — e.g. during
5
+ * development against a locally-run server. Everything else must be https://
6
+ * since the scanner pushes full file content (see O2/O8b,
7
+ * codebase-indexing-audit-2026-07-02.md §5) and a plaintext channel would
8
+ * expose that content (and the API key) to network eavesdropping.
9
+ * Extracted as a pure function so the guard is unit-testable without
10
+ * spinning up the CLI.
11
+ */
12
+ export declare function validateServerUrl(url: string): {
13
+ valid: boolean;
14
+ error?: string;
15
+ };