@optave/codegraph 2.1.0 → 2.1.1-dev.00f091c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -20
- package/package.json +5 -5
- package/src/builder.js +238 -33
- package/src/cli.js +20 -0
- package/src/db.js +4 -0
- package/src/extractors/csharp.js +6 -1
- package/src/extractors/go.js +6 -1
- package/src/extractors/java.js +4 -1
- package/src/extractors/javascript.js +23 -5
- package/src/extractors/php.js +8 -2
- package/src/extractors/python.js +8 -1
- package/src/extractors/ruby.js +4 -1
- package/src/extractors/rust.js +12 -2
- package/src/index.js +1 -0
- package/src/journal.js +109 -0
- package/src/mcp.js +45 -3
- package/src/parser.js +1 -0
- package/src/queries.js +396 -0
- package/src/watcher.js +25 -0
package/src/queries.js
CHANGED
|
@@ -945,6 +945,402 @@ export function fnDeps(name, customDbPath, opts = {}) {
|
|
|
945
945
|
}
|
|
946
946
|
}
|
|
947
947
|
|
|
948
|
+
// ─── Context helpers (private) ──────────────────────────────────────────
|
|
949
|
+
|
|
950
|
+
function readSourceRange(repoRoot, file, startLine, endLine) {
|
|
951
|
+
try {
|
|
952
|
+
const absPath = path.resolve(repoRoot, file);
|
|
953
|
+
const content = fs.readFileSync(absPath, 'utf-8');
|
|
954
|
+
const lines = content.split('\n');
|
|
955
|
+
const start = Math.max(0, (startLine || 1) - 1);
|
|
956
|
+
const end = Math.min(lines.length, endLine || startLine + 50);
|
|
957
|
+
return lines.slice(start, end).join('\n');
|
|
958
|
+
} catch {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function extractSummary(fileLines, line) {
|
|
964
|
+
if (!fileLines || !line || line <= 1) return null;
|
|
965
|
+
const idx = line - 2; // line above the definition (0-indexed)
|
|
966
|
+
// Scan up to 10 lines above for JSDoc or comment
|
|
967
|
+
let jsdocEnd = -1;
|
|
968
|
+
for (let i = idx; i >= Math.max(0, idx - 10); i--) {
|
|
969
|
+
const trimmed = fileLines[i].trim();
|
|
970
|
+
if (trimmed.endsWith('*/')) {
|
|
971
|
+
jsdocEnd = i;
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('#')) {
|
|
975
|
+
// Single-line comment immediately above
|
|
976
|
+
const text = trimmed
|
|
977
|
+
.replace(/^\/\/\s*/, '')
|
|
978
|
+
.replace(/^#\s*/, '')
|
|
979
|
+
.trim();
|
|
980
|
+
return text.length > 100 ? `${text.slice(0, 100)}...` : text;
|
|
981
|
+
}
|
|
982
|
+
if (trimmed !== '' && !trimmed.startsWith('*') && !trimmed.startsWith('/*')) break;
|
|
983
|
+
}
|
|
984
|
+
if (jsdocEnd >= 0) {
|
|
985
|
+
// Find opening /**
|
|
986
|
+
for (let i = jsdocEnd; i >= Math.max(0, jsdocEnd - 20); i--) {
|
|
987
|
+
if (fileLines[i].trim().startsWith('/**')) {
|
|
988
|
+
// Extract first non-tag, non-empty line
|
|
989
|
+
for (let j = i + 1; j <= jsdocEnd; j++) {
|
|
990
|
+
const docLine = fileLines[j]
|
|
991
|
+
.trim()
|
|
992
|
+
.replace(/^\*\s?/, '')
|
|
993
|
+
.trim();
|
|
994
|
+
if (docLine && !docLine.startsWith('@') && docLine !== '/' && docLine !== '*/') {
|
|
995
|
+
return docLine.length > 100 ? `${docLine.slice(0, 100)}...` : docLine;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function extractSignature(fileLines, line) {
|
|
1006
|
+
if (!fileLines || !line) return null;
|
|
1007
|
+
const idx = line - 1;
|
|
1008
|
+
// Gather up to 5 lines to handle multi-line params
|
|
1009
|
+
const chunk = fileLines.slice(idx, Math.min(fileLines.length, idx + 5)).join('\n');
|
|
1010
|
+
|
|
1011
|
+
// JS/TS: function name(params) or (params) => or async function
|
|
1012
|
+
let m = chunk.match(
|
|
1013
|
+
/(?:export\s+)?(?:async\s+)?function\s*\*?\s*\w*\s*\(([^)]*)\)\s*(?::\s*([^\n{]+))?/,
|
|
1014
|
+
);
|
|
1015
|
+
if (m) {
|
|
1016
|
+
return {
|
|
1017
|
+
params: m[1].trim() || null,
|
|
1018
|
+
returnType: m[2] ? m[2].trim().replace(/\s*\{$/, '') : null,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
// Arrow: const name = (params) => or (params):ReturnType =>
|
|
1022
|
+
m = chunk.match(/=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*([^=>\n{]+))?\s*=>/);
|
|
1023
|
+
if (m) {
|
|
1024
|
+
return {
|
|
1025
|
+
params: m[1].trim() || null,
|
|
1026
|
+
returnType: m[2] ? m[2].trim() : null,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
// Python: def name(params) -> return:
|
|
1030
|
+
m = chunk.match(/def\s+\w+\s*\(([^)]*)\)\s*(?:->\s*([^:\n]+))?/);
|
|
1031
|
+
if (m) {
|
|
1032
|
+
return {
|
|
1033
|
+
params: m[1].trim() || null,
|
|
1034
|
+
returnType: m[2] ? m[2].trim() : null,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
// Go: func (recv) name(params) (returns)
|
|
1038
|
+
m = chunk.match(/func\s+(?:\([^)]*\)\s+)?\w+\s*\(([^)]*)\)\s*(?:\(([^)]+)\)|(\w[^\n{]*))?/);
|
|
1039
|
+
if (m) {
|
|
1040
|
+
return {
|
|
1041
|
+
params: m[1].trim() || null,
|
|
1042
|
+
returnType: (m[2] || m[3] || '').trim() || null,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
// Rust: fn name(params) -> ReturnType
|
|
1046
|
+
m = chunk.match(/fn\s+\w+\s*\(([^)]*)\)\s*(?:->\s*([^\n{]+))?/);
|
|
1047
|
+
if (m) {
|
|
1048
|
+
return {
|
|
1049
|
+
params: m[1].trim() || null,
|
|
1050
|
+
returnType: m[2] ? m[2].trim() : null,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// ─── contextData ────────────────────────────────────────────────────────
|
|
1057
|
+
|
|
1058
|
+
export function contextData(name, customDbPath, opts = {}) {
|
|
1059
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
1060
|
+
const depth = opts.depth || 0;
|
|
1061
|
+
const noSource = opts.noSource || false;
|
|
1062
|
+
const noTests = opts.noTests || false;
|
|
1063
|
+
const includeTests = opts.includeTests || false;
|
|
1064
|
+
|
|
1065
|
+
const dbPath = findDbPath(customDbPath);
|
|
1066
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
1067
|
+
|
|
1068
|
+
let nodes = db
|
|
1069
|
+
.prepare(
|
|
1070
|
+
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function', 'method', 'class') ORDER BY file, line`,
|
|
1071
|
+
)
|
|
1072
|
+
.all(`%${name}%`);
|
|
1073
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
1074
|
+
if (nodes.length === 0) {
|
|
1075
|
+
db.close();
|
|
1076
|
+
return { name, results: [] };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Limit to first 5 results
|
|
1080
|
+
nodes = nodes.slice(0, 5);
|
|
1081
|
+
|
|
1082
|
+
// File-lines cache to avoid re-reading the same file
|
|
1083
|
+
const fileCache = new Map();
|
|
1084
|
+
function getFileLines(file) {
|
|
1085
|
+
if (fileCache.has(file)) return fileCache.get(file);
|
|
1086
|
+
try {
|
|
1087
|
+
const absPath = path.resolve(repoRoot, file);
|
|
1088
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
1089
|
+
fileCache.set(file, lines);
|
|
1090
|
+
return lines;
|
|
1091
|
+
} catch {
|
|
1092
|
+
fileCache.set(file, null);
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const results = nodes.map((node) => {
|
|
1098
|
+
const fileLines = getFileLines(node.file);
|
|
1099
|
+
|
|
1100
|
+
// Source
|
|
1101
|
+
const source = noSource ? null : readSourceRange(repoRoot, node.file, node.line, node.end_line);
|
|
1102
|
+
|
|
1103
|
+
// Signature
|
|
1104
|
+
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
1105
|
+
|
|
1106
|
+
// Callees
|
|
1107
|
+
const calleeRows = db
|
|
1108
|
+
.prepare(
|
|
1109
|
+
`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
1110
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
1111
|
+
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
1112
|
+
)
|
|
1113
|
+
.all(node.id);
|
|
1114
|
+
const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
|
|
1115
|
+
|
|
1116
|
+
const callees = filteredCallees.map((c) => {
|
|
1117
|
+
const cLines = getFileLines(c.file);
|
|
1118
|
+
const summary = cLines ? extractSummary(cLines, c.line) : null;
|
|
1119
|
+
let calleeSource = null;
|
|
1120
|
+
if (depth >= 1) {
|
|
1121
|
+
calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
name: c.name,
|
|
1125
|
+
kind: c.kind,
|
|
1126
|
+
file: c.file,
|
|
1127
|
+
line: c.line,
|
|
1128
|
+
endLine: c.end_line || null,
|
|
1129
|
+
summary,
|
|
1130
|
+
source: calleeSource,
|
|
1131
|
+
};
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
// Deep callee expansion via BFS (depth > 1, capped at 5)
|
|
1135
|
+
if (depth > 1) {
|
|
1136
|
+
const visited = new Set(filteredCallees.map((c) => c.id));
|
|
1137
|
+
visited.add(node.id);
|
|
1138
|
+
let frontier = filteredCallees.map((c) => c.id);
|
|
1139
|
+
const maxDepth = Math.min(depth, 5);
|
|
1140
|
+
for (let d = 2; d <= maxDepth; d++) {
|
|
1141
|
+
const nextFrontier = [];
|
|
1142
|
+
for (const fid of frontier) {
|
|
1143
|
+
const deeper = db
|
|
1144
|
+
.prepare(
|
|
1145
|
+
`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
1146
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
1147
|
+
WHERE e.source_id = ? AND e.kind = 'calls'`,
|
|
1148
|
+
)
|
|
1149
|
+
.all(fid);
|
|
1150
|
+
for (const c of deeper) {
|
|
1151
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
1152
|
+
visited.add(c.id);
|
|
1153
|
+
nextFrontier.push(c.id);
|
|
1154
|
+
const cLines = getFileLines(c.file);
|
|
1155
|
+
callees.push({
|
|
1156
|
+
name: c.name,
|
|
1157
|
+
kind: c.kind,
|
|
1158
|
+
file: c.file,
|
|
1159
|
+
line: c.line,
|
|
1160
|
+
endLine: c.end_line || null,
|
|
1161
|
+
summary: cLines ? extractSummary(cLines, c.line) : null,
|
|
1162
|
+
source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
frontier = nextFrontier;
|
|
1168
|
+
if (frontier.length === 0) break;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Callers
|
|
1173
|
+
let callerRows = db
|
|
1174
|
+
.prepare(
|
|
1175
|
+
`SELECT n.name, n.kind, n.file, n.line
|
|
1176
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1177
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
1178
|
+
)
|
|
1179
|
+
.all(node.id);
|
|
1180
|
+
|
|
1181
|
+
// Method hierarchy resolution
|
|
1182
|
+
if (node.kind === 'method' && node.name.includes('.')) {
|
|
1183
|
+
const methodName = node.name.split('.').pop();
|
|
1184
|
+
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
1185
|
+
for (const rm of relatedMethods) {
|
|
1186
|
+
if (rm.id === node.id) continue;
|
|
1187
|
+
const extraCallers = db
|
|
1188
|
+
.prepare(
|
|
1189
|
+
`SELECT n.name, n.kind, n.file, n.line
|
|
1190
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1191
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
1192
|
+
)
|
|
1193
|
+
.all(rm.id);
|
|
1194
|
+
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
|
|
1198
|
+
|
|
1199
|
+
const callers = callerRows.map((c) => ({
|
|
1200
|
+
name: c.name,
|
|
1201
|
+
kind: c.kind,
|
|
1202
|
+
file: c.file,
|
|
1203
|
+
line: c.line,
|
|
1204
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
1205
|
+
}));
|
|
1206
|
+
|
|
1207
|
+
// Related tests: callers that live in test files
|
|
1208
|
+
const testCallerRows = db
|
|
1209
|
+
.prepare(
|
|
1210
|
+
`SELECT n.name, n.kind, n.file, n.line
|
|
1211
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
1212
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
1213
|
+
)
|
|
1214
|
+
.all(node.id);
|
|
1215
|
+
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
1216
|
+
|
|
1217
|
+
const testsByFile = new Map();
|
|
1218
|
+
for (const tc of testCallers) {
|
|
1219
|
+
if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
|
|
1220
|
+
testsByFile.get(tc.file).push(tc);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const relatedTests = [];
|
|
1224
|
+
for (const [file] of testsByFile) {
|
|
1225
|
+
const tLines = getFileLines(file);
|
|
1226
|
+
const testNames = [];
|
|
1227
|
+
if (tLines) {
|
|
1228
|
+
for (const tl of tLines) {
|
|
1229
|
+
const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
1230
|
+
if (tm) testNames.push(tm[1]);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
|
|
1234
|
+
relatedTests.push({
|
|
1235
|
+
file,
|
|
1236
|
+
testCount: testNames.length,
|
|
1237
|
+
testNames,
|
|
1238
|
+
source: testSource,
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
return {
|
|
1243
|
+
name: node.name,
|
|
1244
|
+
kind: node.kind,
|
|
1245
|
+
file: node.file,
|
|
1246
|
+
line: node.line,
|
|
1247
|
+
endLine: node.end_line || null,
|
|
1248
|
+
source,
|
|
1249
|
+
signature,
|
|
1250
|
+
callees,
|
|
1251
|
+
callers,
|
|
1252
|
+
relatedTests,
|
|
1253
|
+
};
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
db.close();
|
|
1257
|
+
return { name, results };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
export function context(name, customDbPath, opts = {}) {
|
|
1261
|
+
const data = contextData(name, customDbPath, opts);
|
|
1262
|
+
if (opts.json) {
|
|
1263
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (data.results.length === 0) {
|
|
1267
|
+
console.log(`No function/method/class matching "${name}"`);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
for (const r of data.results) {
|
|
1272
|
+
const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
|
|
1273
|
+
console.log(`\n# ${r.name} (${r.kind}) — ${r.file}:${lineRange}\n`);
|
|
1274
|
+
|
|
1275
|
+
// Signature
|
|
1276
|
+
if (r.signature) {
|
|
1277
|
+
console.log('## Type/Shape Info');
|
|
1278
|
+
if (r.signature.params != null) console.log(` Parameters: (${r.signature.params})`);
|
|
1279
|
+
if (r.signature.returnType) console.log(` Returns: ${r.signature.returnType}`);
|
|
1280
|
+
console.log();
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Source
|
|
1284
|
+
if (r.source) {
|
|
1285
|
+
console.log('## Source');
|
|
1286
|
+
for (const line of r.source.split('\n')) {
|
|
1287
|
+
console.log(` ${line}`);
|
|
1288
|
+
}
|
|
1289
|
+
console.log();
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Callees
|
|
1293
|
+
if (r.callees.length > 0) {
|
|
1294
|
+
console.log(`## Direct Dependencies (${r.callees.length})`);
|
|
1295
|
+
for (const c of r.callees) {
|
|
1296
|
+
const summary = c.summary ? ` — ${c.summary}` : '';
|
|
1297
|
+
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${summary}`);
|
|
1298
|
+
if (c.source) {
|
|
1299
|
+
for (const line of c.source.split('\n').slice(0, 10)) {
|
|
1300
|
+
console.log(` | ${line}`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
console.log();
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Callers
|
|
1308
|
+
if (r.callers.length > 0) {
|
|
1309
|
+
console.log(`## Callers (${r.callers.length})`);
|
|
1310
|
+
for (const c of r.callers) {
|
|
1311
|
+
const via = c.viaHierarchy ? ` (via ${c.viaHierarchy})` : '';
|
|
1312
|
+
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${via}`);
|
|
1313
|
+
}
|
|
1314
|
+
console.log();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Related tests
|
|
1318
|
+
if (r.relatedTests.length > 0) {
|
|
1319
|
+
console.log('## Related Tests');
|
|
1320
|
+
for (const t of r.relatedTests) {
|
|
1321
|
+
console.log(` ${t.file} — ${t.testCount} tests`);
|
|
1322
|
+
for (const tn of t.testNames) {
|
|
1323
|
+
console.log(` - ${tn}`);
|
|
1324
|
+
}
|
|
1325
|
+
if (t.source) {
|
|
1326
|
+
console.log(' Source:');
|
|
1327
|
+
for (const line of t.source.split('\n').slice(0, 20)) {
|
|
1328
|
+
console.log(` | ${line}`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
console.log();
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
if (r.callees.length === 0 && r.callers.length === 0 && r.relatedTests.length === 0) {
|
|
1336
|
+
console.log(
|
|
1337
|
+
' (no call edges or tests found — may be invoked dynamically or via re-exports)',
|
|
1338
|
+
);
|
|
1339
|
+
console.log();
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
948
1344
|
export function fnImpact(name, customDbPath, opts = {}) {
|
|
949
1345
|
const data = fnImpactData(name, customDbPath, opts);
|
|
950
1346
|
if (opts.json) {
|
package/src/watcher.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
|
|
4
4
|
import { initSchema, openDb } from './db.js';
|
|
5
|
+
import { appendJournalEntries } from './journal.js';
|
|
5
6
|
import { info, warn } from './logger.js';
|
|
6
7
|
import { createParseTreeCache, getActiveEngine, parseFileIncremental } from './parser.js';
|
|
7
8
|
import { resolveImportPath } from './resolve.js';
|
|
@@ -205,6 +206,19 @@ export async function watchProject(rootDir, opts = {}) {
|
|
|
205
206
|
}
|
|
206
207
|
const updates = results;
|
|
207
208
|
|
|
209
|
+
// Append processed files to journal for Tier 0 detection on next build
|
|
210
|
+
if (updates.length > 0) {
|
|
211
|
+
const entries = updates.map((r) => ({
|
|
212
|
+
file: r.file,
|
|
213
|
+
deleted: r.deleted || false,
|
|
214
|
+
}));
|
|
215
|
+
try {
|
|
216
|
+
appendJournalEntries(rootDir, entries);
|
|
217
|
+
} catch {
|
|
218
|
+
/* journal write failure is non-fatal */
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
208
222
|
for (const r of updates) {
|
|
209
223
|
const nodeDelta = r.nodesAdded - r.nodesRemoved;
|
|
210
224
|
const nodeStr = nodeDelta >= 0 ? `+${nodeDelta}` : `${nodeDelta}`;
|
|
@@ -234,6 +248,17 @@ export async function watchProject(rootDir, opts = {}) {
|
|
|
234
248
|
process.on('SIGINT', () => {
|
|
235
249
|
console.log('\nStopping watcher...');
|
|
236
250
|
watcher.close();
|
|
251
|
+
// Flush any pending file paths to journal before exit
|
|
252
|
+
if (pending.size > 0) {
|
|
253
|
+
const entries = [...pending].map((filePath) => ({
|
|
254
|
+
file: normalizePath(path.relative(rootDir, filePath)),
|
|
255
|
+
}));
|
|
256
|
+
try {
|
|
257
|
+
appendJournalEntries(rootDir, entries);
|
|
258
|
+
} catch {
|
|
259
|
+
/* best-effort */
|
|
260
|
+
}
|
|
261
|
+
}
|
|
237
262
|
if (cache) cache.clear();
|
|
238
263
|
db.close();
|
|
239
264
|
process.exit(0);
|