aiden-runtime 4.1.5 → 4.5.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.
- package/README.md +250 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +351 -53
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +138 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +92 -0
- package/dist/cli/v4/commands/browserDepth.js +45 -0
- package/dist/cli/v4/commands/cron.js +264 -0
- package/dist/cli/v4/commands/daemon.js +541 -0
- package/dist/cli/v4/commands/daemonStatus.js +253 -0
- package/dist/cli/v4/commands/help.js +7 -0
- package/dist/cli/v4/commands/index.js +20 -1
- package/dist/cli/v4/commands/runs.js +203 -0
- package/dist/cli/v4/commands/sandbox.js +48 -0
- package/dist/cli/v4/commands/suggestions.js +68 -0
- package/dist/cli/v4/commands/tce.js +41 -0
- package/dist/cli/v4/commands/trigger.js +378 -0
- package/dist/cli/v4/commands/update.js +95 -3
- package/dist/cli/v4/daemonAgentBuilder.js +142 -0
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/display/capabilityCard.js +26 -0
- package/dist/cli/v4/display.js +18 -8
- package/dist/cli/v4/replyRenderer.js +31 -23
- package/dist/cli/v4/updateBootPrompt.js +170 -0
- package/dist/core/playwrightBridge.js +129 -0
- package/dist/core/v4/aidenAgent.js +308 -4
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +604 -0
- package/dist/core/v4/daemon/cleanShutdown.js +154 -0
- package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
- package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
- package/dist/core/v4/daemon/cron/migration.js +199 -0
- package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
- package/dist/core/v4/daemon/daemonConfig.js +90 -0
- package/dist/core/v4/daemon/db/connection.js +106 -0
- package/dist/core/v4/daemon/db/migrations.js +296 -0
- package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
- package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
- package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
- package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
- package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
- package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
- package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
- package/dist/core/v4/daemon/dispatcher/index.js +53 -0
- package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
- package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
- package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
- package/dist/core/v4/daemon/drain.js +156 -0
- package/dist/core/v4/daemon/eventLoopLag.js +73 -0
- package/dist/core/v4/daemon/health.js +159 -0
- package/dist/core/v4/daemon/idempotencyStore.js +204 -0
- package/dist/core/v4/daemon/index.js +179 -0
- package/dist/core/v4/daemon/instanceTracker.js +99 -0
- package/dist/core/v4/daemon/resourceRegistry.js +150 -0
- package/dist/core/v4/daemon/restartCode.js +32 -0
- package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
- package/dist/core/v4/daemon/runStore.js +114 -0
- package/dist/core/v4/daemon/runtimeLock.js +167 -0
- package/dist/core/v4/daemon/signals.js +50 -0
- package/dist/core/v4/daemon/supervisor.js +272 -0
- package/dist/core/v4/daemon/triggerBus.js +279 -0
- package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
- package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
- package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
- package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
- package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
- package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
- package/dist/core/v4/daemon/triggers/email/index.js +332 -0
- package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
- package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
- package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
- package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
- package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
- package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
- package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
- package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
- package/dist/core/v4/daemon/triggers/webhook.js +376 -0
- package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
- package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
- package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
- package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
- package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
- package/dist/core/v4/daemon/types.js +15 -0
- package/dist/core/v4/dockerSession.js +461 -0
- package/dist/core/v4/dryRun.js +117 -0
- package/dist/core/v4/failureClassifier.js +779 -0
- package/dist/core/v4/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +187 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +18 -0
- package/dist/core/v4/turnState.js +587 -0
- package/dist/core/v4/update/checkUpdate.js +63 -3
- package/dist/core/v4/update/installMethodDetect.js +115 -0
- package/dist/core/v4/update/registryClient.js +121 -0
- package/dist/core/v4/update/skipState.js +75 -0
- package/dist/core/v4/verifier.js +448 -0
- package/dist/core/version.js +1 -1
- package/dist/tools/v4/browser/_observer.js +224 -0
- package/dist/tools/v4/browser/browserBlocker.js +396 -0
- package/dist/tools/v4/browser/browserClick.js +18 -1
- package/dist/tools/v4/browser/browserClose.js +18 -1
- package/dist/tools/v4/browser/browserExtract.js +5 -1
- package/dist/tools/v4/browser/browserFill.js +17 -1
- package/dist/tools/v4/browser/browserGetUrl.js +5 -1
- package/dist/tools/v4/browser/browserNavigate.js +16 -1
- package/dist/tools/v4/browser/browserScreenshot.js +5 -1
- package/dist/tools/v4/browser/browserScroll.js +18 -1
- package/dist/tools/v4/browser/browserType.js +17 -1
- package/dist/tools/v4/browser/captchaCheck.js +5 -1
- package/dist/tools/v4/executeCode.js +1 -0
- package/dist/tools/v4/files/fileCopy.js +56 -2
- package/dist/tools/v4/files/fileDelete.js +38 -1
- package/dist/tools/v4/files/fileList.js +12 -1
- package/dist/tools/v4/files/fileMove.js +59 -2
- package/dist/tools/v4/files/filePatch.js +43 -1
- package/dist/tools/v4/files/fileRead.js +12 -1
- package/dist/tools/v4/files/fileWrite.js +41 -1
- package/dist/tools/v4/index.js +71 -58
- package/dist/tools/v4/memory/memoryAdd.js +14 -0
- package/dist/tools/v4/memory/memoryRemove.js +14 -0
- package/dist/tools/v4/memory/memoryReplace.js +15 -0
- package/dist/tools/v4/memory/sessionSummary.js +12 -0
- package/dist/tools/v4/process/processKill.js +19 -0
- package/dist/tools/v4/process/processList.js +1 -0
- package/dist/tools/v4/process/processLogRead.js +1 -0
- package/dist/tools/v4/process/processSpawn.js +13 -0
- package/dist/tools/v4/process/processWait.js +1 -0
- package/dist/tools/v4/sessions/recallSession.js +1 -0
- package/dist/tools/v4/sessions/sessionList.js +1 -0
- package/dist/tools/v4/sessions/sessionSearch.js +1 -0
- package/dist/tools/v4/skills/lookupToolSchema.js +2 -0
- package/dist/tools/v4/skills/skillManage.js +13 -0
- package/dist/tools/v4/skills/skillView.js +1 -0
- package/dist/tools/v4/skills/skillsList.js +1 -0
- package/dist/tools/v4/subagent/subagentFanout.js +1 -0
- package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
- package/dist/tools/v4/system/appClose.js +13 -0
- package/dist/tools/v4/system/appInput.js +13 -0
- package/dist/tools/v4/system/appLaunch.js +13 -0
- package/dist/tools/v4/system/clipboardRead.js +1 -0
- package/dist/tools/v4/system/clipboardWrite.js +14 -0
- package/dist/tools/v4/system/mediaKey.js +12 -0
- package/dist/tools/v4/system/mediaSessions.js +1 -0
- package/dist/tools/v4/system/mediaTransport.js +13 -0
- package/dist/tools/v4/system/naturalEvents.js +1 -0
- package/dist/tools/v4/system/nowPlaying.js +1 -0
- package/dist/tools/v4/system/osProcessList.js +1 -0
- package/dist/tools/v4/system/screenshot.js +1 -0
- package/dist/tools/v4/system/systemInfo.js +1 -0
- package/dist/tools/v4/system/volumeSet.js +17 -0
- package/dist/tools/v4/terminal/shellExec.js +81 -9
- package/dist/tools/v4/web/deepResearch.js +1 -0
- package/dist/tools/v4/web/openUrl.js +1 -0
- package/dist/tools/v4/web/webFetch.js +1 -0
- package/dist/tools/v4/web/webPage.js +1 -0
- package/dist/tools/v4/web/webSearch.js +1 -0
- package/dist/tools/v4/web/youtubeSearch.js +1 -0
- package/package.json +7 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/triggers/fileWatcher.ts — v4.5 Phase 2: the
|
|
10
|
+
* file-watcher trigger source.
|
|
11
|
+
*
|
|
12
|
+
* Wires chokidar 4.x into the Phase 1 trigger bus. One module-level
|
|
13
|
+
* factory `createFileWatcher(spec)` returns a handle that:
|
|
14
|
+
* - subscribes to add/change/unlink events
|
|
15
|
+
* - filters via the glob matcher (default ignores + spec globs)
|
|
16
|
+
* - debounces per-path (default 750ms)
|
|
17
|
+
* - settles via a stat loop (default 1s) — second layer on top
|
|
18
|
+
* of chokidar's `awaitWriteFinish`
|
|
19
|
+
* - upserts a file_observations row + inserts a trigger_event
|
|
20
|
+
* under the bus's UNIQUE(source, idempotency_key) dedup
|
|
21
|
+
* - registers itself in the resourceRegistry for shutdown reap
|
|
22
|
+
* - exposes `stats()` for /metrics + diagnostics
|
|
23
|
+
*
|
|
24
|
+
* Backpressure: per-watcher queue depth (default 100). When full,
|
|
25
|
+
* new events DROP with a log + stats counter bump (Q-P2-5 default).
|
|
26
|
+
* Pausing chokidar would cause a resync-burst on resume — drop-new
|
|
27
|
+
* is the correct trade-off.
|
|
28
|
+
*/
|
|
29
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
30
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
|
+
};
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.createFileWatcher = createFileWatcher;
|
|
34
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
35
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
36
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
37
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
38
|
+
const globMatcher_1 = require("./globMatcher");
|
|
39
|
+
const fsIdentity_1 = require("./fsIdentity");
|
|
40
|
+
const settleStat_1 = require("./settleStat");
|
|
41
|
+
const noopLog = (_level, _msg) => undefined;
|
|
42
|
+
function createFileWatcher(opts) {
|
|
43
|
+
const { watcherId, spec, triggerBus, obsStore, registry } = opts;
|
|
44
|
+
const log = opts.log ?? noopLog;
|
|
45
|
+
const matcher = (0, globMatcher_1.compileGlobMatcher)({
|
|
46
|
+
includeGlobs: spec.includeGlobs,
|
|
47
|
+
excludeGlobs: spec.excludeGlobs,
|
|
48
|
+
ignoreTemp: spec.ignoreTemp,
|
|
49
|
+
});
|
|
50
|
+
// Per-path pending queue. Map key = absPath; one entry per path.
|
|
51
|
+
const queue = new Map();
|
|
52
|
+
const acceptedEventTypes = new Set(spec.eventTypes);
|
|
53
|
+
let paused = false;
|
|
54
|
+
const stats = {
|
|
55
|
+
queueDepth: 0,
|
|
56
|
+
emitted: 0,
|
|
57
|
+
coalesced: 0,
|
|
58
|
+
skipped: 0,
|
|
59
|
+
dropped: 0,
|
|
60
|
+
overflowed: false,
|
|
61
|
+
lastError: null,
|
|
62
|
+
};
|
|
63
|
+
// ── chokidar setup ──────────────────────────────────────────────────────
|
|
64
|
+
const watcher = chokidar_1.default.watch(spec.paths, {
|
|
65
|
+
persistent: true,
|
|
66
|
+
ignoreInitial: true, // boot-time reconciliation runs separately
|
|
67
|
+
depth: spec.recursive ? undefined : 0,
|
|
68
|
+
awaitWriteFinish: {
|
|
69
|
+
stabilityThreshold: Math.max(50, spec.settleMs),
|
|
70
|
+
pollInterval: 100,
|
|
71
|
+
},
|
|
72
|
+
usePolling: spec.polling?.enabled === true,
|
|
73
|
+
interval: spec.polling?.intervalMs,
|
|
74
|
+
binaryInterval: spec.polling?.binaryIntervalMs,
|
|
75
|
+
// chokidar 4.x removed built-in globs — we filter via picomatch
|
|
76
|
+
// in handleEvent below.
|
|
77
|
+
});
|
|
78
|
+
// ── core event handler ──────────────────────────────────────────────────
|
|
79
|
+
const handleEvent = (eventType, absPath) => {
|
|
80
|
+
if (paused)
|
|
81
|
+
return;
|
|
82
|
+
if (!acceptedEventTypes.has(eventType)) {
|
|
83
|
+
stats.skipped += 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!matcher.match(absPath)) {
|
|
87
|
+
stats.skipped += 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Unlink bypasses debounce + settle — the file is gone.
|
|
91
|
+
if (eventType === 'unlink') {
|
|
92
|
+
void emit(absPath, 'unlink', null);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Coalesce repeated events for the same path within the debounce window.
|
|
96
|
+
const existing = queue.get(absPath);
|
|
97
|
+
if (existing) {
|
|
98
|
+
existing.coalesced += 1;
|
|
99
|
+
// Reset the debounce timer.
|
|
100
|
+
if (existing.timer)
|
|
101
|
+
clearTimeout(existing.timer);
|
|
102
|
+
existing.timer = setTimeout(() => { void flush(absPath); }, spec.debounceMs);
|
|
103
|
+
if (typeof existing.timer.unref === 'function')
|
|
104
|
+
existing.timer.unref();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (queue.size >= spec.maxQueueDepth) {
|
|
108
|
+
stats.dropped += 1;
|
|
109
|
+
if (!stats.overflowed) {
|
|
110
|
+
stats.overflowed = true;
|
|
111
|
+
log('warn', `[file-watcher] queue overflow at ${watcherId} — dropping events (depth=${queue.size}, max=${spec.maxQueueDepth})`);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const entry = {
|
|
116
|
+
absPath,
|
|
117
|
+
eventType,
|
|
118
|
+
enqueuedAt: Date.now(),
|
|
119
|
+
coalesced: 0,
|
|
120
|
+
timer: null,
|
|
121
|
+
};
|
|
122
|
+
entry.timer = setTimeout(() => { void flush(absPath); }, spec.debounceMs);
|
|
123
|
+
if (entry.timer && typeof entry.timer.unref === 'function')
|
|
124
|
+
entry.timer.unref();
|
|
125
|
+
queue.set(absPath, entry);
|
|
126
|
+
stats.queueDepth = queue.size;
|
|
127
|
+
};
|
|
128
|
+
// ── flush + settle + emit ───────────────────────────────────────────────
|
|
129
|
+
const flush = async (absPath) => {
|
|
130
|
+
const entry = queue.get(absPath);
|
|
131
|
+
if (!entry)
|
|
132
|
+
return;
|
|
133
|
+
queue.delete(absPath);
|
|
134
|
+
stats.queueDepth = queue.size;
|
|
135
|
+
if (stats.overflowed && queue.size <= Math.floor(spec.maxQueueDepth / 2)) {
|
|
136
|
+
stats.overflowed = false;
|
|
137
|
+
log('info', `[file-watcher] queue drained at ${watcherId}`);
|
|
138
|
+
}
|
|
139
|
+
// Settle: stat repeatedly until stable.
|
|
140
|
+
const stable = await (0, settleStat_1.settleStat)(absPath, {
|
|
141
|
+
intervalMs: Math.max(50, spec.settleMs),
|
|
142
|
+
maxSettleMs: spec.maxSettleMs,
|
|
143
|
+
});
|
|
144
|
+
if (stable === null) {
|
|
145
|
+
// File vanished mid-settle; treat as unlink.
|
|
146
|
+
void emit(absPath, 'unlink', null, entry.coalesced);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
void emit(absPath, entry.eventType, stable, entry.coalesced);
|
|
150
|
+
};
|
|
151
|
+
const emit = async (absPath, eventType, stable, coalescedDelta = 0) => {
|
|
152
|
+
try {
|
|
153
|
+
const fileKey = (0, fsIdentity_1.computeFileKey)(absPath);
|
|
154
|
+
const size = stable?.size ?? null;
|
|
155
|
+
const mtimeMs = stable?.mtimeMs ?? Date.now();
|
|
156
|
+
const contentHash = spec.contentHash && eventType !== 'unlink' && stable !== null
|
|
157
|
+
? await sha256OfFile(absPath)
|
|
158
|
+
: null;
|
|
159
|
+
const observationId = obsStore.upsert({
|
|
160
|
+
watcherId,
|
|
161
|
+
absPath,
|
|
162
|
+
fileKey,
|
|
163
|
+
size,
|
|
164
|
+
mtimeMs,
|
|
165
|
+
contentHash,
|
|
166
|
+
eventType,
|
|
167
|
+
coalescedDelta,
|
|
168
|
+
});
|
|
169
|
+
const idempotencyKey = `${absPath}::${mtimeMs}::${size ?? 'null'}`;
|
|
170
|
+
const insertResult = triggerBus.insert({
|
|
171
|
+
source: 'file',
|
|
172
|
+
sourceKey: watcherId,
|
|
173
|
+
idempotencyKey,
|
|
174
|
+
payload: {
|
|
175
|
+
absPath,
|
|
176
|
+
eventType,
|
|
177
|
+
mtime: mtimeMs,
|
|
178
|
+
size,
|
|
179
|
+
fileKey,
|
|
180
|
+
contentHash,
|
|
181
|
+
watcherId,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
obsStore.markProcessed({
|
|
185
|
+
observationId,
|
|
186
|
+
eventId: insertResult.id,
|
|
187
|
+
status: 'pending',
|
|
188
|
+
});
|
|
189
|
+
stats.emitted += 1;
|
|
190
|
+
stats.coalesced += coalescedDelta;
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
194
|
+
stats.lastError = msg;
|
|
195
|
+
log('error', `[file-watcher] emit failed for ${absPath}: ${msg}`);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
// ── wire chokidar events ────────────────────────────────────────────────
|
|
199
|
+
watcher.on('add', (p) => handleEvent('add', node_path_1.default.resolve(p)));
|
|
200
|
+
watcher.on('change', (p) => handleEvent('change', node_path_1.default.resolve(p)));
|
|
201
|
+
watcher.on('unlink', (p) => handleEvent('unlink', node_path_1.default.resolve(p)));
|
|
202
|
+
watcher.on('error', (err) => {
|
|
203
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
204
|
+
stats.lastError = msg;
|
|
205
|
+
log('error', `[file-watcher] chokidar error at ${watcherId}: ${msg}`);
|
|
206
|
+
});
|
|
207
|
+
// ── resource registry ───────────────────────────────────────────────────
|
|
208
|
+
const close = async () => {
|
|
209
|
+
paused = true;
|
|
210
|
+
// Cancel pending debounce timers.
|
|
211
|
+
for (const e of queue.values()) {
|
|
212
|
+
if (e.timer)
|
|
213
|
+
clearTimeout(e.timer);
|
|
214
|
+
}
|
|
215
|
+
queue.clear();
|
|
216
|
+
stats.queueDepth = 0;
|
|
217
|
+
try {
|
|
218
|
+
await watcher.close();
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
log('warn', `[file-watcher] close error: ${e instanceof Error ? e.message : String(e)}`);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const resourceId = registry.register({
|
|
225
|
+
kind: 'file_watcher',
|
|
226
|
+
owner: watcherId,
|
|
227
|
+
metadata: { paths: spec.paths },
|
|
228
|
+
close,
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
watcherId,
|
|
232
|
+
resourceId,
|
|
233
|
+
pause() { paused = true; },
|
|
234
|
+
resume() { paused = false; },
|
|
235
|
+
close,
|
|
236
|
+
stats() { return { ...stats }; },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// ── SHA-256 of file contents (opt-in via spec.contentHash) ─────────────────
|
|
240
|
+
function sha256OfFile(absPath) {
|
|
241
|
+
return new Promise((resolve) => {
|
|
242
|
+
try {
|
|
243
|
+
const h = node_crypto_1.default.createHash('sha256');
|
|
244
|
+
const stream = node_fs_1.default.createReadStream(absPath);
|
|
245
|
+
stream.on('data', (chunk) => { h.update(chunk); });
|
|
246
|
+
stream.on('error', () => resolve(null));
|
|
247
|
+
stream.on('end', () => resolve(h.digest('hex')));
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
resolve(null);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/triggers/fileWatcherSpec.ts — v4.5 Phase 2.
|
|
10
|
+
*
|
|
11
|
+
* Trigger-spec shape stored in `triggers.spec_json` for
|
|
12
|
+
* `source='file'`. Pure types + parse helpers; no I/O.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DEFAULT_FILE_WATCHER_SPEC = void 0;
|
|
16
|
+
exports.parseFileWatcherSpec = parseFileWatcherSpec;
|
|
17
|
+
exports.DEFAULT_FILE_WATCHER_SPEC = {
|
|
18
|
+
recursive: true,
|
|
19
|
+
eventTypes: ['add', 'change', 'unlink'],
|
|
20
|
+
debounceMs: 750,
|
|
21
|
+
settleMs: 1000,
|
|
22
|
+
maxSettleMs: 30000,
|
|
23
|
+
maxQueueDepth: 100,
|
|
24
|
+
ignoreTemp: true,
|
|
25
|
+
contentHash: false,
|
|
26
|
+
reconcile: 'skip_existing',
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Parse + fill in defaults. Throws when `paths` is missing/empty.
|
|
30
|
+
* Tolerates extra keys (forward-compatible).
|
|
31
|
+
*/
|
|
32
|
+
function parseFileWatcherSpec(raw) {
|
|
33
|
+
const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
34
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
35
|
+
throw new Error('FileWatcherSpec: input must be an object');
|
|
36
|
+
}
|
|
37
|
+
const o = obj;
|
|
38
|
+
const paths = Array.isArray(o.paths) ? o.paths.filter((p) => typeof p === 'string') : [];
|
|
39
|
+
if (paths.length === 0) {
|
|
40
|
+
throw new Error('FileWatcherSpec: at least one path required');
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
paths,
|
|
44
|
+
recursive: typeof o.recursive === 'boolean' ? o.recursive : exports.DEFAULT_FILE_WATCHER_SPEC.recursive,
|
|
45
|
+
includeGlobs: Array.isArray(o.includeGlobs) ? o.includeGlobs.filter((s) => typeof s === 'string') : undefined,
|
|
46
|
+
excludeGlobs: Array.isArray(o.excludeGlobs) ? o.excludeGlobs.filter((s) => typeof s === 'string') : undefined,
|
|
47
|
+
eventTypes: sanitizeEventTypes(o.eventTypes),
|
|
48
|
+
debounceMs: sanitizeNum(o.debounceMs, exports.DEFAULT_FILE_WATCHER_SPEC.debounceMs, 0),
|
|
49
|
+
settleMs: sanitizeNum(o.settleMs, exports.DEFAULT_FILE_WATCHER_SPEC.settleMs, 0),
|
|
50
|
+
maxSettleMs: sanitizeNum(o.maxSettleMs, exports.DEFAULT_FILE_WATCHER_SPEC.maxSettleMs, 0),
|
|
51
|
+
maxQueueDepth: sanitizeNum(o.maxQueueDepth, exports.DEFAULT_FILE_WATCHER_SPEC.maxQueueDepth, 1),
|
|
52
|
+
ignoreTemp: typeof o.ignoreTemp === 'boolean' ? o.ignoreTemp : exports.DEFAULT_FILE_WATCHER_SPEC.ignoreTemp,
|
|
53
|
+
contentHash: typeof o.contentHash === 'boolean' ? o.contentHash : exports.DEFAULT_FILE_WATCHER_SPEC.contentHash,
|
|
54
|
+
reconcile: sanitizeReconcile(o.reconcile),
|
|
55
|
+
polling: sanitizePolling(o.polling),
|
|
56
|
+
promptTemplate: typeof o.promptTemplate === 'string' ? o.promptTemplate : undefined,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function sanitizeNum(v, fallback, min) {
|
|
60
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v < min)
|
|
61
|
+
return fallback;
|
|
62
|
+
return v;
|
|
63
|
+
}
|
|
64
|
+
function sanitizeEventTypes(v) {
|
|
65
|
+
if (!Array.isArray(v))
|
|
66
|
+
return [...exports.DEFAULT_FILE_WATCHER_SPEC.eventTypes];
|
|
67
|
+
const valid = new Set(['add', 'change', 'unlink']);
|
|
68
|
+
const out = v.filter((s) => typeof s === 'string' && valid.has(s));
|
|
69
|
+
return out.length > 0 ? out : [...exports.DEFAULT_FILE_WATCHER_SPEC.eventTypes];
|
|
70
|
+
}
|
|
71
|
+
function sanitizeReconcile(v) {
|
|
72
|
+
if (v === 'skip_existing' || v === 'process_new_since_last_seen' || v === 'full_rescan') {
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
return exports.DEFAULT_FILE_WATCHER_SPEC.reconcile;
|
|
76
|
+
}
|
|
77
|
+
function sanitizePolling(v) {
|
|
78
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
79
|
+
return undefined;
|
|
80
|
+
const o = v;
|
|
81
|
+
if (typeof o.enabled !== 'boolean' || !o.enabled)
|
|
82
|
+
return undefined;
|
|
83
|
+
return {
|
|
84
|
+
enabled: true,
|
|
85
|
+
intervalMs: typeof o.intervalMs === 'number' && o.intervalMs > 0 ? o.intervalMs : undefined,
|
|
86
|
+
binaryIntervalMs: typeof o.binaryIntervalMs === 'number' && o.binaryIntervalMs > 0 ? o.binaryIntervalMs : undefined,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/triggers/fsIdentity.ts — v4.5 Phase 2.
|
|
10
|
+
*
|
|
11
|
+
* Compute a best-effort identity key for a file. Used by
|
|
12
|
+
* `file_observations.file_key` so future v4.5.x revisions can
|
|
13
|
+
* detect renames-within-watcher without re-scanning the world.
|
|
14
|
+
*
|
|
15
|
+
* Platforms:
|
|
16
|
+
* - POSIX: ino (inode) — stable across rename
|
|
17
|
+
* - Windows: fs.statSync({bigint:true}).ino — packs the NTFS
|
|
18
|
+
* file index; stable across rename on the same volume
|
|
19
|
+
*
|
|
20
|
+
* Returns '' on stat failure (permission denied, file vanished).
|
|
21
|
+
* Empty string is acceptable — the (watcher_id, abs_path) UNIQUE
|
|
22
|
+
* index in file_observations is the primary key; file_key is a
|
|
23
|
+
* diagnostic/future-feature column.
|
|
24
|
+
*/
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.computeFileKey = computeFileKey;
|
|
30
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
31
|
+
function computeFileKey(absPath) {
|
|
32
|
+
try {
|
|
33
|
+
const st = node_fs_1.default.statSync(absPath, { bigint: true });
|
|
34
|
+
// BigInt ino — stringify so SQLite stores it as TEXT.
|
|
35
|
+
if (typeof st.ino === 'bigint')
|
|
36
|
+
return st.ino.toString();
|
|
37
|
+
return String(st.ino);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/triggers/globMatcher.ts — v4.5 Phase 2.
|
|
10
|
+
*
|
|
11
|
+
* chokidar 4.x removed built-in glob matching. We use `picomatch`
|
|
12
|
+
* (already in Aiden's transitive deps) to filter paths AFTER
|
|
13
|
+
* chokidar emits — same semantics, less bundle weight.
|
|
14
|
+
*
|
|
15
|
+
* Match semantics (applied in order):
|
|
16
|
+
* 1. ignoreTemp default deny list (editor temps, .git/, node_modules/, …)
|
|
17
|
+
* 2. spec.excludeGlobs deny list
|
|
18
|
+
* 3. spec.includeGlobs allow list (default ['**∕*'])
|
|
19
|
+
*
|
|
20
|
+
* Compiled matchers are cached per-spec so we don't recompile on
|
|
21
|
+
* every event.
|
|
22
|
+
*/
|
|
23
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.DEFAULT_IGNORE_PATTERNS = void 0;
|
|
28
|
+
exports.normalizeGlobPattern = normalizeGlobPattern;
|
|
29
|
+
exports.compileGlobMatcher = compileGlobMatcher;
|
|
30
|
+
const picomatch_1 = __importDefault(require("picomatch"));
|
|
31
|
+
/** Default ignore patterns when FileWatcherSpec.ignoreTemp = true. */
|
|
32
|
+
exports.DEFAULT_IGNORE_PATTERNS = Object.freeze([
|
|
33
|
+
// editor temps
|
|
34
|
+
'**/*.swp', '**/*.swo', '**/*~',
|
|
35
|
+
'**/.*.swp', '**/.*.swo',
|
|
36
|
+
'**/*.tmp', '**/*.temp', '**/*.part',
|
|
37
|
+
'**/.#*', // emacs lock
|
|
38
|
+
'**/~$*', // MS Office temp
|
|
39
|
+
// OS metadata
|
|
40
|
+
'**/.DS_Store', '**/Thumbs.db', '**/desktop.ini',
|
|
41
|
+
// VCS
|
|
42
|
+
'**/.git/**', '**/.svn/**', '**/.hg/**',
|
|
43
|
+
// dependency / build outputs
|
|
44
|
+
'**/node_modules/**',
|
|
45
|
+
'**/dist/**', '**/build/**', '**/.next/**',
|
|
46
|
+
'**/__pycache__/**', '**/*.pyc',
|
|
47
|
+
'**/.venv/**', '**/venv/**',
|
|
48
|
+
'**/target/**',
|
|
49
|
+
]);
|
|
50
|
+
/**
|
|
51
|
+
* Normalize a glob pattern so it matches absolute paths sensibly.
|
|
52
|
+
*
|
|
53
|
+
* The user mental model is "`*.txt` matches `.txt` files anywhere",
|
|
54
|
+
* but picomatch's `*` does NOT span path separators — so a bare
|
|
55
|
+
* basename glob like `*.txt` never matches `/some/dir/foo.txt`.
|
|
56
|
+
*
|
|
57
|
+
* Rule: if a pattern doesn't already begin with double-star,
|
|
58
|
+
* a leading slash, a Windows drive letter (`C:`), or contain a
|
|
59
|
+
* `/` directory separator, treat it as a basename pattern and
|
|
60
|
+
* prepend a depth-spanning prefix so it matches at any depth.
|
|
61
|
+
* Patterns that already express locality (containing `/` or
|
|
62
|
+
* starting with double-star) are left alone.
|
|
63
|
+
*/
|
|
64
|
+
function normalizeGlobPattern(pat) {
|
|
65
|
+
if (pat.startsWith('**'))
|
|
66
|
+
return pat;
|
|
67
|
+
if (pat.startsWith('/'))
|
|
68
|
+
return pat;
|
|
69
|
+
if (/^[A-Za-z]:/.test(pat))
|
|
70
|
+
return pat; // absolute Windows path
|
|
71
|
+
if (pat.includes('/'))
|
|
72
|
+
return pat;
|
|
73
|
+
return '**/' + pat;
|
|
74
|
+
}
|
|
75
|
+
function compileGlobMatcher(opts) {
|
|
76
|
+
const include = opts.includeGlobs && opts.includeGlobs.length > 0
|
|
77
|
+
? opts.includeGlobs
|
|
78
|
+
: ['**/*'];
|
|
79
|
+
const exclude = [
|
|
80
|
+
...(opts.excludeGlobs ?? []),
|
|
81
|
+
...(opts.ignoreTemp !== false ? exports.DEFAULT_IGNORE_PATTERNS : []),
|
|
82
|
+
];
|
|
83
|
+
const opt = { dot: true, nocase: process.platform === 'win32' };
|
|
84
|
+
const compile = (p) => (0, picomatch_1.default)(normalizeGlobPattern(p), opt);
|
|
85
|
+
const includeFns = include.map(compile);
|
|
86
|
+
const excludeFns = exclude.map(compile);
|
|
87
|
+
return {
|
|
88
|
+
match(absPath) {
|
|
89
|
+
// Normalize to forward slashes for cross-platform glob matching.
|
|
90
|
+
const norm = absPath.replace(/\\/g, '/');
|
|
91
|
+
for (const fn of excludeFns)
|
|
92
|
+
if (fn(norm))
|
|
93
|
+
return false;
|
|
94
|
+
for (const fn of includeFns)
|
|
95
|
+
if (fn(norm))
|
|
96
|
+
return true;
|
|
97
|
+
return false;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/triggers/reconcile.ts — v4.5 Phase 2.
|
|
10
|
+
*
|
|
11
|
+
* Boot-time reconciliation for file-watcher triggers. Walks the
|
|
12
|
+
* watched directory trees (glob-pruning excluded dirs) and decides
|
|
13
|
+
* per-policy whether to emit catch-up events for files that changed
|
|
14
|
+
* while the daemon was down.
|
|
15
|
+
*
|
|
16
|
+
* Three policies (configurable per watcher):
|
|
17
|
+
* - `skip_existing` (default) — walk + stat, write observation
|
|
18
|
+
* rows with last_status='done' and NO trigger_event emission.
|
|
19
|
+
* Future changes after boot emit normally. Matches the IMAP
|
|
20
|
+
* `_seen_uids`-on-connect philosophy referenced in the audit.
|
|
21
|
+
*
|
|
22
|
+
* - `process_new_since_last_seen` — for each file, compare its
|
|
23
|
+
* current mtime to the existing observation row. If newer (or
|
|
24
|
+
* no observation row exists), emit a synthetic 'change' (or
|
|
25
|
+
* 'add') trigger_event. Catch-up mode.
|
|
26
|
+
*
|
|
27
|
+
* - `full_rescan` — emit a synthetic 'add' for every matched
|
|
28
|
+
* file regardless of prior observations. One-shot indexing
|
|
29
|
+
* mode — use with care on large trees.
|
|
30
|
+
*/
|
|
31
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
32
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
33
|
+
};
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.reconcileFileWatcher = reconcileFileWatcher;
|
|
36
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
37
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
38
|
+
const globMatcher_1 = require("./globMatcher");
|
|
39
|
+
const fsIdentity_1 = require("./fsIdentity");
|
|
40
|
+
const noopLog = (_l, _m) => undefined;
|
|
41
|
+
function reconcileFileWatcher(opts) {
|
|
42
|
+
const { watcherId, spec, triggerBus, obsStore } = opts;
|
|
43
|
+
const log = opts.log ?? noopLog;
|
|
44
|
+
const max = opts.maxEntries ?? 100000;
|
|
45
|
+
const matcher = (0, globMatcher_1.compileGlobMatcher)({
|
|
46
|
+
includeGlobs: spec.includeGlobs,
|
|
47
|
+
excludeGlobs: spec.excludeGlobs,
|
|
48
|
+
ignoreTemp: spec.ignoreTemp,
|
|
49
|
+
});
|
|
50
|
+
const result = {
|
|
51
|
+
walked: 0, matched: 0, recorded: 0, emitted: 0, skipped: 0,
|
|
52
|
+
};
|
|
53
|
+
for (const root of spec.paths) {
|
|
54
|
+
walkDir(root, spec.recursive, (absPath) => {
|
|
55
|
+
result.walked += 1;
|
|
56
|
+
if (result.walked > max)
|
|
57
|
+
return false;
|
|
58
|
+
if (!matcher.match(absPath)) {
|
|
59
|
+
result.skipped += 1;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
result.matched += 1;
|
|
63
|
+
const stat = tryStat(absPath);
|
|
64
|
+
if (!stat)
|
|
65
|
+
return true;
|
|
66
|
+
const prev = obsStore.get(watcherId, absPath);
|
|
67
|
+
const fileKey = (0, fsIdentity_1.computeFileKey)(absPath);
|
|
68
|
+
switch (spec.reconcile) {
|
|
69
|
+
case 'skip_existing': {
|
|
70
|
+
// Write observation but do NOT emit a trigger_event.
|
|
71
|
+
const obsId = obsStore.upsert({
|
|
72
|
+
watcherId,
|
|
73
|
+
absPath,
|
|
74
|
+
fileKey,
|
|
75
|
+
size: stat.size,
|
|
76
|
+
mtimeMs: stat.mtimeMs,
|
|
77
|
+
contentHash: null,
|
|
78
|
+
eventType: 'add',
|
|
79
|
+
});
|
|
80
|
+
obsStore.markProcessed({
|
|
81
|
+
observationId: obsId,
|
|
82
|
+
eventId: null,
|
|
83
|
+
status: 'done',
|
|
84
|
+
});
|
|
85
|
+
result.recorded += 1;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 'process_new_since_last_seen': {
|
|
89
|
+
const isNew = !prev;
|
|
90
|
+
const isChanged = prev && (prev.mtimeMs !== stat.mtimeMs || prev.size !== stat.size);
|
|
91
|
+
if (isNew || isChanged) {
|
|
92
|
+
emitFor(opts, watcherId, absPath, isNew ? 'add' : 'change', stat, fileKey);
|
|
93
|
+
result.emitted += 1;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Up-to-date — refresh last_seen_at.
|
|
97
|
+
obsStore.upsert({
|
|
98
|
+
watcherId, absPath, fileKey,
|
|
99
|
+
size: stat.size, mtimeMs: stat.mtimeMs,
|
|
100
|
+
contentHash: null, eventType: 'change',
|
|
101
|
+
});
|
|
102
|
+
result.skipped += 1;
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'full_rescan': {
|
|
107
|
+
emitFor(opts, watcherId, absPath, 'add', stat, fileKey);
|
|
108
|
+
result.emitted += 1;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}, opts);
|
|
114
|
+
}
|
|
115
|
+
log('info', `[file-watcher] reconcile ${spec.reconcile} for ${watcherId}: walked=${result.walked} matched=${result.matched} recorded=${result.recorded} emitted=${result.emitted}`);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function emitFor(opts, watcherId, absPath, eventType, stat, fileKey) {
|
|
119
|
+
const obsId = opts.obsStore.upsert({
|
|
120
|
+
watcherId,
|
|
121
|
+
absPath,
|
|
122
|
+
fileKey,
|
|
123
|
+
size: stat.size,
|
|
124
|
+
mtimeMs: stat.mtimeMs,
|
|
125
|
+
contentHash: null,
|
|
126
|
+
eventType,
|
|
127
|
+
});
|
|
128
|
+
const ev = opts.triggerBus.insert({
|
|
129
|
+
source: 'file',
|
|
130
|
+
sourceKey: watcherId,
|
|
131
|
+
idempotencyKey: `${absPath}::${stat.mtimeMs}::${stat.size}`,
|
|
132
|
+
payload: {
|
|
133
|
+
absPath,
|
|
134
|
+
eventType,
|
|
135
|
+
mtime: stat.mtimeMs,
|
|
136
|
+
size: stat.size,
|
|
137
|
+
fileKey,
|
|
138
|
+
contentHash: null,
|
|
139
|
+
watcherId,
|
|
140
|
+
reconciled: true,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
opts.obsStore.markProcessed({
|
|
144
|
+
observationId: obsId,
|
|
145
|
+
eventId: ev.id,
|
|
146
|
+
status: 'pending',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function tryStat(p) {
|
|
150
|
+
try {
|
|
151
|
+
const s = node_fs_1.default.statSync(p);
|
|
152
|
+
if (!s.isFile())
|
|
153
|
+
return null;
|
|
154
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Glob-pruning directory walker. Calls `cb(absPath)` for each FILE
|
|
162
|
+
* (not directory) encountered. Returning false from the callback
|
|
163
|
+
* aborts the walk (used for entry caps).
|
|
164
|
+
*
|
|
165
|
+
* Dir-level pruning: when a directory's path matches the ignoreTemp/
|
|
166
|
+
* excludeGlobs patterns, we don't recurse into it — never enumerate
|
|
167
|
+
* `node_modules/` etc.
|
|
168
|
+
*/
|
|
169
|
+
function walkDir(root, recursive, cb, opts) {
|
|
170
|
+
const dirMatcher = (0, globMatcher_1.compileGlobMatcher)({
|
|
171
|
+
excludeGlobs: opts.spec.excludeGlobs,
|
|
172
|
+
ignoreTemp: opts.spec.ignoreTemp,
|
|
173
|
+
// Allow EVERYTHING for the directory walker; the exclude list
|
|
174
|
+
// does the pruning. Include filter is applied per-file via the
|
|
175
|
+
// caller's matcher.
|
|
176
|
+
includeGlobs: ['**/*'],
|
|
177
|
+
});
|
|
178
|
+
const stack = [node_path_1.default.resolve(root)];
|
|
179
|
+
while (stack.length > 0) {
|
|
180
|
+
const dir = stack.pop();
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
for (const ent of entries) {
|
|
189
|
+
const abs = node_path_1.default.join(dir, ent.name);
|
|
190
|
+
if (ent.isDirectory()) {
|
|
191
|
+
// Prune via the *negated* glob match — a directory whose
|
|
192
|
+
// path does NOT match the exclude/ignoreTemp deny list is
|
|
193
|
+
// walked into. Since includeGlobs = ['**/*'], a dir matches
|
|
194
|
+
// iff no exclude/ignore pattern hit it.
|
|
195
|
+
if (recursive && dirMatcher.match(abs)) {
|
|
196
|
+
stack.push(abs);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (ent.isFile()) {
|
|
200
|
+
const cont = cb(abs);
|
|
201
|
+
if (!cont)
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|