@nxuss/lemma 1.22.0 → 1.22.1
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/bin/init.js +108 -37
- package/dist/cjs/cli/lemma-proxy.d.ts +2 -0
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/cjs/cli/lemma-proxy.js +41 -0
- package/dist/cjs/cli/lemma-proxy.js.map +1 -1
- package/dist/esm/cli/lemma-proxy.d.ts +2 -0
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/esm/cli/lemma-proxy.js +39 -0
- package/dist/esm/cli/lemma-proxy.js.map +1 -1
- package/package.json +1 -1
package/bin/init.js
CHANGED
|
@@ -399,7 +399,7 @@ function configureClaudeCode() {
|
|
|
399
399
|
// license tier (semantic cache vs. exact-match cache), which has nothing to do with
|
|
400
400
|
// gateway credentials. The proxy (lemma-proxy.ts) never validates whatever token the
|
|
401
401
|
// client sends anyway: it always signs upstream requests with its OWN server-side
|
|
402
|
-
// ANTHROPIC_API_KEY (
|
|
402
|
+
// ANTHROPIC_API_KEY (loaded from ~/.lemma-cache/env). So ANTHROPIC_AUTH_TOKEN here is just a
|
|
403
403
|
// fixed local placeholder to satisfy Claude Code's "a token is present" check.
|
|
404
404
|
function configureClaudeCodeGateway(anthropicApiKey) {
|
|
405
405
|
if (!anthropicApiKey) {
|
|
@@ -795,20 +795,19 @@ function configureOpenCode() {
|
|
|
795
795
|
return true;
|
|
796
796
|
}
|
|
797
797
|
|
|
798
|
-
|
|
798
|
+
// Installs the cache-proxy daemon as a user-level launchd/systemd unit.
|
|
799
|
+
// OPT-IN ONLY: `lemma init` never calls this without an explicit --daemon flag.
|
|
800
|
+
// Secrets are never baked into the unit file: the daemon loads ~/.lemma-cache/env
|
|
801
|
+
// at startup (see lemma-proxy.cjs loadEnvFile), where `lemma init` persists keys
|
|
802
|
+
// with mode 0600 instead.
|
|
803
|
+
function installProxyDaemon(homeDir = os.homedir()) {
|
|
799
804
|
const isMac = process.platform === 'darwin';
|
|
800
805
|
const isLinux = process.platform === 'linux';
|
|
801
806
|
|
|
802
|
-
// The full proxy server (lemma-proxy.ts) needs its own server-side credentials to sign
|
|
803
|
-
// upstream requests — it never trusts whatever token the calling client sends. A launchd/
|
|
804
|
-
// systemd service does not inherit the shell's exported env, so these must be baked into
|
|
805
|
-
// the unit definition at install time or the daemon silently falls back to local Ollama.
|
|
806
|
-
const anthropicApiKey = process.env.ANTHROPIC_API_KEY || '';
|
|
807
|
-
const openaiApiKey = process.env.OPENAI_API_KEY || '';
|
|
808
807
|
const proxyArgs = ['start', '--port', PORT, '--no-configure', '--no-clipboard'];
|
|
809
808
|
|
|
810
809
|
if (isMac) {
|
|
811
|
-
const plistDir = path.join(
|
|
810
|
+
const plistDir = path.join(homeDir, 'Library', 'LaunchAgents');
|
|
812
811
|
ensureDir(plistDir);
|
|
813
812
|
const plistPath = path.join(plistDir, 'com.lemma.cache-proxy.plist');
|
|
814
813
|
const argEntries = proxyArgs.map((a) => ` <string>${a}</string>`).join('\n');
|
|
@@ -816,9 +815,7 @@ function installProxyDaemon() {
|
|
|
816
815
|
` <key>LEMMA_PROXY_PORT</key><string>${PORT}</string>`,
|
|
817
816
|
` <key>LEMMA_CACHE_DIR</key><string>${LEMMA_CACHE}</string>`,
|
|
818
817
|
` <key>LEMMA_DISABLE_CLIPBOARD</key><string>true</string>`,
|
|
819
|
-
|
|
820
|
-
openaiApiKey ? ` <key>OPENAI_API_KEY</key><string>${openaiApiKey}</string>` : '',
|
|
821
|
-
].filter(Boolean).join('\n');
|
|
818
|
+
].join('\n');
|
|
822
819
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
823
820
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
824
821
|
<plist version="1.0">
|
|
@@ -848,15 +845,13 @@ ${envEntries}
|
|
|
848
845
|
}
|
|
849
846
|
|
|
850
847
|
if (isLinux) {
|
|
851
|
-
const svcDir = path.join(
|
|
848
|
+
const svcDir = path.join(homeDir, '.config', 'systemd', 'user');
|
|
852
849
|
ensureDir(svcDir);
|
|
853
850
|
const envLines = [
|
|
854
851
|
`Environment=LEMMA_PROXY_PORT=${PORT}`,
|
|
855
852
|
`Environment=LEMMA_CACHE_DIR=${LEMMA_CACHE}`,
|
|
856
853
|
`Environment=LEMMA_DISABLE_CLIPBOARD=true`,
|
|
857
|
-
|
|
858
|
-
openaiApiKey ? `Environment=OPENAI_API_KEY=${openaiApiKey}` : '',
|
|
859
|
-
].filter(Boolean).join('\n');
|
|
854
|
+
].join('\n');
|
|
860
855
|
const svc = `[Unit]
|
|
861
856
|
Description=Lemma Cache Proxy
|
|
862
857
|
After=network.target
|
|
@@ -879,6 +874,39 @@ WantedBy=default.target
|
|
|
879
874
|
return false;
|
|
880
875
|
}
|
|
881
876
|
|
|
877
|
+
// Best-effort removal of a previously installed daemon unit. Plain `lemma init`
|
|
878
|
+
// (no --daemon) calls this so a silent or legacy install can never linger behind:
|
|
879
|
+
// the daemon only exists while the user explicitly asked for it.
|
|
880
|
+
function removeProxyDaemon(homeDir = os.homedir()) {
|
|
881
|
+
if (process.platform === 'darwin') {
|
|
882
|
+
const plistPath = path.join(homeDir, 'Library', 'LaunchAgents', 'com.lemma.cache-proxy.plist');
|
|
883
|
+
if (!fs.existsSync(plistPath)) return false;
|
|
884
|
+
try { execSync('launchctl unload ' + plistPath, { stdio: 'ignore' }); } catch {}
|
|
885
|
+
try { fs.unlinkSync(plistPath); } catch {}
|
|
886
|
+
return !fs.existsSync(plistPath);
|
|
887
|
+
}
|
|
888
|
+
if (process.platform === 'linux') {
|
|
889
|
+
const svcPath = path.join(homeDir, '.config', 'systemd', 'user', 'lemma-cache-proxy.service');
|
|
890
|
+
if (!fs.existsSync(svcPath)) return false;
|
|
891
|
+
try { execSync('systemctl --user disable lemma-cache-proxy --now', { stdio: 'ignore' }); } catch {}
|
|
892
|
+
try { fs.unlinkSync(svcPath); } catch {}
|
|
893
|
+
return !fs.existsSync(svcPath);
|
|
894
|
+
}
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Global side effects stay opt-in: a plain `lemma init` only writes project-local
|
|
899
|
+
// files + MCP registrations. --daemon installs the background proxy daemon,
|
|
900
|
+
// --gateway reroutes the user's Claude Code model traffic through that proxy.
|
|
901
|
+
function parseInitArgs(argv = process.argv) {
|
|
902
|
+
const args = argv.slice(2);
|
|
903
|
+
return {
|
|
904
|
+
showHelp: args.includes('--help') || args.includes('-h'),
|
|
905
|
+
wantDaemon: args.includes('--daemon'),
|
|
906
|
+
wantGateway: args.includes('--gateway'),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
882
910
|
function applyEnvToCurrentProcess() {
|
|
883
911
|
const proxyUrl = `http://localhost:${PORT}`;
|
|
884
912
|
process.env.LEMMA_PROXY_PORT = PORT;
|
|
@@ -889,21 +917,29 @@ function applyEnvToCurrentProcess() {
|
|
|
889
917
|
process.env.LITELLM_PROXY_BASE_URL = proxyUrl;
|
|
890
918
|
}
|
|
891
919
|
|
|
892
|
-
|
|
920
|
+
// Upstream API keys live here (mode 0600) instead of inside the launchd/systemd
|
|
921
|
+
// unit: the proxy daemon loads this file at startup (lemma-proxy.cjs loadEnvFile),
|
|
922
|
+
// so a background service gets credentials without baking secrets into a unit file.
|
|
923
|
+
function createEnvFile(envFile = ENV_FILE) {
|
|
893
924
|
const proxyUrl = `http://localhost:${PORT}`;
|
|
925
|
+
const keyLines = [
|
|
926
|
+
process.env.ANTHROPIC_API_KEY ? `export ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : '',
|
|
927
|
+
process.env.OPENAI_API_KEY ? `export OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : '',
|
|
928
|
+
].filter(Boolean).join('\n');
|
|
894
929
|
const envContent = `# Lemma Cache Proxy — Auto-generated by \`lemma init\`
|
|
895
930
|
# Source this file in your shell to route LLM traffic through the cache:
|
|
896
|
-
# source ${
|
|
931
|
+
# source ${envFile}
|
|
897
932
|
export LEMMA_PROXY_PORT=${PORT}
|
|
898
933
|
export LEMMA_CACHE_DIR=${LEMMA_CACHE}
|
|
899
934
|
export OPENAI_BASE_URL=${proxyUrl}
|
|
900
935
|
export OPENAI_API_BASE=${proxyUrl}
|
|
901
936
|
export CODEX_BASE_URL=${proxyUrl}
|
|
902
937
|
export LITELLM_PROXY_BASE_URL=${proxyUrl}
|
|
903
|
-
`;
|
|
904
|
-
fs.writeFileSync(
|
|
938
|
+
${keyLines ? keyLines + '\n' : ''}`;
|
|
939
|
+
fs.writeFileSync(envFile, envContent, 'utf8');
|
|
940
|
+
try { fs.chmodSync(envFile, 0o600); } catch {}
|
|
905
941
|
applyEnvToCurrentProcess();
|
|
906
|
-
return
|
|
942
|
+
return envFile;
|
|
907
943
|
}
|
|
908
944
|
|
|
909
945
|
function setupShellIntegration() {
|
|
@@ -942,7 +978,26 @@ function setupShellIntegration() {
|
|
|
942
978
|
return configured;
|
|
943
979
|
}
|
|
944
980
|
|
|
945
|
-
async function main() {
|
|
981
|
+
async function main(argv = process.argv) {
|
|
982
|
+
const opts = parseInitArgs(argv);
|
|
983
|
+
if (opts.showHelp) {
|
|
984
|
+
process.stderr.write(`
|
|
985
|
+
Lemma Init — project setup (project-local files + MCP registrations only)
|
|
986
|
+
|
|
987
|
+
Usage: lemma init [--daemon] [--gateway]
|
|
988
|
+
|
|
989
|
+
--daemon Install + start the background cache-proxy daemon
|
|
990
|
+
(launchd KeepAlive on macOS, systemd user unit on Linux).
|
|
991
|
+
Without it, no daemon is installed — and a previously installed
|
|
992
|
+
one is removed.
|
|
993
|
+
--gateway Reroute this user's Claude Code model traffic through the proxy
|
|
994
|
+
(~/.claude/settings.json). Requires ANTHROPIC_API_KEY in env.
|
|
995
|
+
Without it, Claude Code keeps its own auth untouched.
|
|
996
|
+
|
|
997
|
+
`);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
946
1001
|
process.stderr.write(`\n ${BOLD}${CYAN}Lemma Init — Magic Setup${RESET}\n\n`);
|
|
947
1002
|
|
|
948
1003
|
ensureDir(LEMMA_CACHE);
|
|
@@ -961,17 +1016,23 @@ async function main() {
|
|
|
961
1016
|
configureClaudeCode();
|
|
962
1017
|
ok('Claude Code / CLI configured (CLAUDE.md + .claude/settings.local.json)');
|
|
963
1018
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
if (
|
|
967
|
-
|
|
1019
|
+
// --gateway is opt-in: flipping the user's global Claude Code auth without being
|
|
1020
|
+
// asked is exactly how Pro subscribers end up billed as API usage.
|
|
1021
|
+
if (!opts.wantGateway) {
|
|
1022
|
+
skip('Claude Code gateway not requested — model traffic + auth untouched (MCP tools only). Re-run with --gateway to route it via the proxy.');
|
|
968
1023
|
} else {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1024
|
+
const anthropicApiKey = process.env.ANTHROPIC_API_KEY || '';
|
|
1025
|
+
const gatewayResult = configureClaudeCodeGateway(anthropicApiKey);
|
|
1026
|
+
if (gatewayResult.activated) {
|
|
1027
|
+
ok(`Claude Code gateway activated (~/.claude/settings.json → ANTHROPIC_BASE_URL=http://localhost:${PORT})`);
|
|
1028
|
+
} else {
|
|
1029
|
+
warn('Claude Code gateway NOT activated — no ANTHROPIC_API_KEY in this environment.');
|
|
1030
|
+
warn(` Model traffic keeps going directly to Anthropic (subscription/OAuth or your own key, unchanged).`);
|
|
1031
|
+
warn(` Only Lemma's MCP tools are active for Claude Code — no cache savings on the model traffic itself.`);
|
|
1032
|
+
warn(` To enable it: export ANTHROPIC_API_KEY=sk-ant-... and re-run \`lemma init --gateway\`.`);
|
|
1033
|
+
warn(` This SWITCHES Claude Code off your subscription/OAuth session for LLM calls — they'll be`);
|
|
1034
|
+
warn(` signed by the proxy's own ANTHROPIC_API_KEY instead, and billed/rate-limited as API usage.`);
|
|
1035
|
+
}
|
|
975
1036
|
}
|
|
976
1037
|
|
|
977
1038
|
updateAgentsMd();
|
|
@@ -1024,9 +1085,15 @@ async function main() {
|
|
|
1024
1085
|
if (hasWindsurf) ok('Windsurf detected — env auto-configured');
|
|
1025
1086
|
else skip('Windsurf not detected');
|
|
1026
1087
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1088
|
+
if (opts.wantDaemon) {
|
|
1089
|
+
const daemonOk = installProxyDaemon();
|
|
1090
|
+
if (daemonOk) ok('Cache proxy daemon installed & started (OpenAI + Anthropic /v1/messages, streaming)');
|
|
1091
|
+
else warn('Could not install daemon — run `lemma proxy start` manually');
|
|
1092
|
+
} else if (removeProxyDaemon()) {
|
|
1093
|
+
ok('Removed previously installed proxy daemon (re-run with --daemon to restore it)');
|
|
1094
|
+
} else {
|
|
1095
|
+
skip('Proxy daemon not installed (opt-in via `lemma init --daemon`)');
|
|
1096
|
+
}
|
|
1030
1097
|
|
|
1031
1098
|
const envFile = createEnvFile();
|
|
1032
1099
|
ok(`Env file created: ${envFile}`);
|
|
@@ -1037,7 +1104,11 @@ async function main() {
|
|
|
1037
1104
|
|
|
1038
1105
|
process.stderr.write(`\n ${BOLD}${GREEN}✓ Lemma init complete${RESET}\n`);
|
|
1039
1106
|
process.stderr.write(` ${DIM}──────────────────────────────────────${RESET}\n`);
|
|
1040
|
-
|
|
1107
|
+
if (opts.wantDaemon) {
|
|
1108
|
+
process.stderr.write(` ${GREEN}●${RESET} Cache proxy running on http://localhost:${PORT}\n`);
|
|
1109
|
+
} else {
|
|
1110
|
+
process.stderr.write(` ${DIM}○ Cache proxy daemon not installed (opt-in: \`lemma init --daemon\`)${RESET}\n`);
|
|
1111
|
+
}
|
|
1041
1112
|
process.stderr.write(` ${GREEN}●${RESET} MCP server ready ${DIM}(lemma mcp)${RESET}\n`);
|
|
1042
1113
|
process.stderr.write(` ${GREEN}●${RESET} Dashboard active ${DIM}(lemma stats)${RESET}\n`);
|
|
1043
1114
|
process.stderr.write(` ${GREEN}●${RESET} New terminals: env vars auto-loaded\n`);
|
|
@@ -1057,4 +1128,4 @@ if (require.main === module) {
|
|
|
1057
1128
|
});
|
|
1058
1129
|
}
|
|
1059
1130
|
|
|
1060
|
-
module.exports = { main, configureMuse, configureMuseUserSkill, updateAgentsMd, MUSE_SKILL_CONTENT, LEMMA_RULES_CONTENT };
|
|
1131
|
+
module.exports = { main, configureMuse, configureMuseUserSkill, updateAgentsMd, MUSE_SKILL_CONTENT, LEMMA_RULES_CONTENT, installProxyDaemon, removeProxyDaemon, parseInitArgs, createEnvFile };
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export declare function startBackgroundClipboardWatcher(cliOpts?: {
|
|
3
3
|
clipboard?: boolean;
|
|
4
4
|
}): void;
|
|
5
|
+
export declare const MAX_WORKSPACE_WATCHERS = 512;
|
|
6
|
+
export declare function isUnsafeWatchRoot(dir: string): boolean;
|
|
5
7
|
export declare function performAutoHeal(apply: boolean): Promise<{
|
|
6
8
|
success: boolean;
|
|
7
9
|
message: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lemma-proxy.d.ts","sourceRoot":"","sources":["../../../src/cli/lemma-proxy.ts"],"names":[],"mappings":";AA0cA,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA4DvF;
|
|
1
|
+
{"version":3,"file":"lemma-proxy.d.ts","sourceRoot":"","sources":["../../../src/cli/lemma-proxy.ts"],"names":[],"mappings":";AA0cA,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA4DvF;AAqfD,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGtD;AAyqDD,wBAAsB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA8KhL"}
|
|
@@ -4,7 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.MAX_WORKSPACE_WATCHERS = void 0;
|
|
7
8
|
exports.startBackgroundClipboardWatcher = startBackgroundClipboardWatcher;
|
|
9
|
+
exports.isUnsafeWatchRoot = isUnsafeWatchRoot;
|
|
8
10
|
exports.performAutoHeal = performAutoHeal;
|
|
9
11
|
const commander_1 = require("commander");
|
|
10
12
|
const express_1 = __importDefault(require("express"));
|
|
@@ -960,6 +962,16 @@ class WriteQueue {
|
|
|
960
962
|
}
|
|
961
963
|
}
|
|
962
964
|
const usageWriteQueue = new WriteQueue();
|
|
965
|
+
// One fs.watch FD is opened per watched directory. Watching $HOME (the launchd
|
|
966
|
+
// daemon's WorkingDirectory) or a filesystem root exhausts FDs (EMFILE), crashes
|
|
967
|
+
// the process, and a KeepAlive supervisor turns that into an infinite
|
|
968
|
+
// crash/relaunch loop — on macOS each relaunch re-triggers the TCC data-access
|
|
969
|
+
// prompt. Refuse those roots outright.
|
|
970
|
+
exports.MAX_WORKSPACE_WATCHERS = 512;
|
|
971
|
+
function isUnsafeWatchRoot(dir) {
|
|
972
|
+
const resolved = path_1.default.resolve(dir);
|
|
973
|
+
return resolved === path_1.default.resolve(os_1.default.homedir()) || resolved === path_1.default.parse(resolved).root;
|
|
974
|
+
}
|
|
963
975
|
// ── Proxy Server class ─────────────────────────────────────────────────────────
|
|
964
976
|
class LemmaServer {
|
|
965
977
|
constructor(port, projectName, openBrowser = true) {
|
|
@@ -1648,12 +1660,28 @@ class LemmaServer {
|
|
|
1648
1660
|
}
|
|
1649
1661
|
startWorkspaceTimeWatcher() {
|
|
1650
1662
|
const cwd = process.cwd();
|
|
1663
|
+
if (isUnsafeWatchRoot(cwd)) {
|
|
1664
|
+
console.log(`⏭️ [TimeTravel] Skipping workspace watcher: refusing to watch ${cwd} recursively (file-descriptor exhaustion risk). Start the proxy from a project directory instead.`);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1651
1667
|
const { watch } = require('fs');
|
|
1652
1668
|
// Debounced capture function
|
|
1653
1669
|
let debounceTimer = null;
|
|
1654
1670
|
const pendingChanges = new Set();
|
|
1671
|
+
// One FD per watched directory: cap it so giant trees degrade to a warning
|
|
1672
|
+
// instead of an EMFILE crash (which a KeepAlive supervisor turns into a
|
|
1673
|
+
// crash/relaunch loop).
|
|
1674
|
+
let watchCount = 0;
|
|
1675
|
+
let limitWarned = false;
|
|
1655
1676
|
const ignoreDirs = ['node_modules', '.git', 'dist', 'chroma_data', '.lemma', 'dashboard', 'bin', 'sdks'];
|
|
1656
1677
|
const watchDirRecursive = (dir) => {
|
|
1678
|
+
if (watchCount >= exports.MAX_WORKSPACE_WATCHERS) {
|
|
1679
|
+
if (!limitWarned) {
|
|
1680
|
+
limitWarned = true;
|
|
1681
|
+
console.log(`⚠️ [TimeTravel] Watcher limit (${exports.MAX_WORKSPACE_WATCHERS} dirs) reached at ${dir} — deeper directories won't trigger snapshots.`);
|
|
1682
|
+
}
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1657
1685
|
try {
|
|
1658
1686
|
const watcher = watch(dir, (eventType, filename) => {
|
|
1659
1687
|
if (!filename)
|
|
@@ -1675,6 +1703,19 @@ class LemmaServer {
|
|
|
1675
1703
|
// Prevent watcher from keeping process alive if in background
|
|
1676
1704
|
if (watcher.unref)
|
|
1677
1705
|
watcher.unref();
|
|
1706
|
+
watchCount++;
|
|
1707
|
+
// An async EMFILE/ENOSPC on a giant tree must never take down the server:
|
|
1708
|
+
// contain it to a single warning and keep serving.
|
|
1709
|
+
watcher.on('error', (err) => {
|
|
1710
|
+
if (!limitWarned) {
|
|
1711
|
+
limitWarned = true;
|
|
1712
|
+
console.log(`⚠️ [TimeTravel] Watcher error (${(err && err.code) || 'unknown'}) — snapshots degraded, server keeps running.`);
|
|
1713
|
+
}
|
|
1714
|
+
try {
|
|
1715
|
+
watcher.close();
|
|
1716
|
+
}
|
|
1717
|
+
catch { }
|
|
1718
|
+
});
|
|
1678
1719
|
// Recursively watch subfolders
|
|
1679
1720
|
const files = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
1680
1721
|
for (const file of files) {
|