@jonathanleelx/meddle 0.5.0-beta.9 → 0.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.
Files changed (204) hide show
  1. package/CONFIG_STRUCTURE.md +9 -2
  2. package/README.md +18 -0
  3. package/bin/commands/mock/add.js +37 -21
  4. package/bin/commands/mock/delete.js +16 -24
  5. package/bin/commands/mock/index.js +8 -4
  6. package/bin/commands/mock/list.js +35 -18
  7. package/bin/commands/mock/toggle.js +17 -27
  8. package/bin/commands/mock/update.js +39 -30
  9. package/bin/commands/route/active.js +20 -3
  10. package/bin/commands/route/add.js +113 -94
  11. package/bin/commands/route/create.js +8 -1
  12. package/bin/commands/route/delete.js +85 -51
  13. package/bin/commands/route/index.js +20 -2
  14. package/bin/commands/route/list.js +2 -2
  15. package/bin/commands/route/show.js +10 -1
  16. package/bin/commands/route/update.js +135 -92
  17. package/bin/commands/update.js +4 -0
  18. package/bin/index.js +7 -3
  19. package/bin/lib/api-client.js +14 -2
  20. package/bin/lib/file-access.js +217 -35
  21. package/bin/lib/output.js +24 -3
  22. package/bin/lib/proxy-detect.js +99 -20
  23. package/bin/lib/proxy-or-file.js +90 -0
  24. package/bin/lib/route-write-args.js +150 -0
  25. package/bin/lib/rule-file-edit.js +263 -0
  26. package/bin/lib/ruleset-readonly.js +61 -0
  27. package/bin/lib/update-check.js +67 -6
  28. package/dist/cert.d.ts.map +1 -1
  29. package/dist/cert.js +43 -0
  30. package/dist/cert.js.map +1 -1
  31. package/dist/core/browser.d.ts +8 -1
  32. package/dist/core/browser.d.ts.map +1 -1
  33. package/dist/core/browser.js +239 -36
  34. package/dist/core/browser.js.map +1 -1
  35. package/dist/core/config-diagnostics.d.ts.map +1 -1
  36. package/dist/core/config-diagnostics.js +6 -1
  37. package/dist/core/config-diagnostics.js.map +1 -1
  38. package/dist/core/custom-plugin-loader.d.ts.map +1 -1
  39. package/dist/core/custom-plugin-loader.js +2 -0
  40. package/dist/core/custom-plugin-loader.js.map +1 -1
  41. package/dist/core/mock-handler.d.ts +59 -0
  42. package/dist/core/mock-handler.d.ts.map +1 -1
  43. package/dist/core/mock-handler.js +627 -29
  44. package/dist/core/mock-handler.js.map +1 -1
  45. package/dist/core/mock-utils.d.ts +58 -0
  46. package/dist/core/mock-utils.d.ts.map +1 -0
  47. package/dist/core/mock-utils.js +167 -0
  48. package/dist/core/mock-utils.js.map +1 -0
  49. package/dist/core/plugin-bootstrap-runner.d.ts +1 -0
  50. package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -1
  51. package/dist/core/plugin-bootstrap-runner.js +37 -1
  52. package/dist/core/plugin-bootstrap-runner.js.map +1 -1
  53. package/dist/core/plugin-source.d.ts +24 -0
  54. package/dist/core/plugin-source.d.ts.map +1 -0
  55. package/dist/core/plugin-source.js +47 -0
  56. package/dist/core/plugin-source.js.map +1 -0
  57. package/dist/core/rule-file-edit.d.ts +81 -0
  58. package/dist/core/rule-file-edit.d.ts.map +1 -0
  59. package/dist/core/rule-file-edit.js +243 -0
  60. package/dist/core/rule-file-edit.js.map +1 -0
  61. package/dist/core/static-server.d.ts.map +1 -1
  62. package/dist/core/static-server.js +15 -13
  63. package/dist/core/static-server.js.map +1 -1
  64. package/dist/core/types.d.ts +13 -1
  65. package/dist/core/types.d.ts.map +1 -1
  66. package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -1
  67. package/dist/plugins/builtin/mock-plugin.js +6 -5
  68. package/dist/plugins/builtin/mock-plugin.js.map +1 -1
  69. package/dist/server/agent/tools.d.ts.map +1 -1
  70. package/dist/server/agent/tools.js +24 -18
  71. package/dist/server/agent/tools.js.map +1 -1
  72. package/dist/server/app-info.d.ts +33 -0
  73. package/dist/server/app-info.d.ts.map +1 -0
  74. package/dist/server/app-info.js +116 -0
  75. package/dist/server/app-info.js.map +1 -0
  76. package/dist/server/config.d.ts +1 -0
  77. package/dist/server/config.d.ts.map +1 -1
  78. package/dist/server/config.js +101 -9
  79. package/dist/server/config.js.map +1 -1
  80. package/dist/server/health.d.ts.map +1 -1
  81. package/dist/server/health.js +8 -0
  82. package/dist/server/health.js.map +1 -1
  83. package/dist/server/index.d.ts +15 -0
  84. package/dist/server/index.d.ts.map +1 -1
  85. package/dist/server/index.js +3 -0
  86. package/dist/server/index.js.map +1 -1
  87. package/dist/server/mocks.d.ts +26 -0
  88. package/dist/server/mocks.d.ts.map +1 -1
  89. package/dist/server/mocks.js +110 -13
  90. package/dist/server/mocks.js.map +1 -1
  91. package/dist/server/plugins.d.ts.map +1 -1
  92. package/dist/server/plugins.js +56 -11
  93. package/dist/server/plugins.js.map +1 -1
  94. package/dist/server/rule-files.d.ts +36 -0
  95. package/dist/server/rule-files.d.ts.map +1 -1
  96. package/dist/server/rule-files.js +395 -107
  97. package/dist/server/rule-files.js.map +1 -1
  98. package/dist/server/ruleset-lock.d.ts +62 -0
  99. package/dist/server/ruleset-lock.d.ts.map +1 -0
  100. package/dist/server/ruleset-lock.js +234 -0
  101. package/dist/server/ruleset-lock.js.map +1 -0
  102. package/index.js +36 -7
  103. package/mcp-server.js +90 -88
  104. package/package.json +1 -1
  105. package/plugins/builtin/mock-plugin.ts +9 -5
  106. package/web/dist/assets/alert-B514Frte.js +1 -0
  107. package/web/dist/assets/arrow-right-BNum_060.js +1 -0
  108. package/web/dist/assets/{body-diff-view-B-4XWmL6.js → body-diff-view-SfLCLzs9.js} +2 -2
  109. package/web/dist/assets/button-BFNlGZ5p.js +1 -0
  110. package/web/dist/assets/check-BkfHQ6re.js +1 -0
  111. package/web/dist/assets/checkbox-CQyFsZzS.js +1 -0
  112. package/web/dist/assets/chevron-down-DHwUtL4O.js +1 -0
  113. package/web/dist/assets/{circle-alert-Q8cZk9Rv.js → circle-alert-zMagrFuI.js} +1 -1
  114. package/web/dist/assets/{circle-x-C9USji_R.js → circle-x-BEkinhez.js} +1 -1
  115. package/web/dist/assets/code-DD-AYxxu.js +1 -0
  116. package/web/dist/assets/{copy-BYjq1hdN.js → copy-BRnscYy1.js} +1 -1
  117. package/web/dist/assets/detail-panel-BtgWWeu3.js +4 -0
  118. package/web/dist/assets/dist-BQN0rzUS.js +1 -0
  119. package/web/dist/assets/{dist-CEd_wnRC.js → dist-C-E2R2jk.js} +1 -1
  120. package/web/dist/assets/{dist-C6eaYCUo.js → dist-CFZ4xMSF.js} +1 -1
  121. package/web/dist/assets/{dist-DWmDaXVN.js → dist-CRmX_NvH.js} +1 -1
  122. package/web/dist/assets/{dist-C8RHq5At.js → dist-DLQKRjuW.js} +1 -1
  123. package/web/dist/assets/{dist-Blyulj4_.js → dist-LiAcI8CK.js} +1 -1
  124. package/web/dist/assets/{dist-Ce4ujolc.js → dist-fzPL-8F9.js} +1 -1
  125. package/web/dist/assets/{dist-DoE7uybN.js → dist-wv9ebJK_.js} +1 -1
  126. package/web/dist/assets/editor-pane-actions-_ckwcCII.js +1 -0
  127. package/web/dist/assets/{es2015-DG46NGN7.js → es2015-DPJTBYa9.js} +1 -1
  128. package/web/dist/assets/{global-panel-shell-5cJneeis.js → global-panel-shell-NaUhzX7J.js} +3 -3
  129. package/web/dist/assets/health-panel-DhXLqcOU.js +1 -0
  130. package/web/dist/assets/index-B-6nUrqW.js +21 -0
  131. package/web/dist/assets/index-EWNlAdFB.css +2 -0
  132. package/web/dist/assets/mobile-proxy-panel-BY8Oi-qC.js +8 -0
  133. package/web/dist/assets/mock-config-TPmfJwAt.js +1 -0
  134. package/web/dist/assets/mock-editor-panel-WQwcHSTD.js +20 -0
  135. package/web/dist/assets/mocks-file-missing-note-BR5oWFLh.js +1 -0
  136. package/web/dist/assets/monaco-editor-BGLPrb0C.js +11 -0
  137. package/web/dist/assets/pane-bar-gFRISZLD.js +1 -0
  138. package/web/dist/assets/pane-split-B6aLmY7G.js +1 -0
  139. package/web/dist/assets/panel-heading-DT_nXayM.js +1 -0
  140. package/web/dist/assets/{pencil-line-Cqe41E4h.js → pencil-line-CEZ7D-de.js} +1 -1
  141. package/web/dist/assets/plugin-code-editor--Gf4ZwGb.js +2 -0
  142. package/web/dist/assets/plugin-config-DsX9xCW_.js +2 -0
  143. package/web/dist/assets/plugin-generator-Bj4f6dxo.js +2 -0
  144. package/web/dist/assets/plugin-test-dialog-DICuykBK.js +7 -0
  145. package/web/dist/assets/plus-ixZQRprm.js +1 -0
  146. package/web/dist/assets/{refresh-cw-CG3HaiQT.js → refresh-cw-BfLoHqg5.js} +1 -1
  147. package/web/dist/assets/rotate-ccw-CRguJya4.js +1 -0
  148. package/web/dist/assets/route-preview-C-LzS1lT.js +1 -0
  149. package/web/dist/assets/{rule-ai-assistant-panel-DMh0F9dr.js → rule-ai-assistant-panel-BEfPNBbx.js} +2 -2
  150. package/web/dist/assets/rule-config-Dw9pcuah.js +15 -0
  151. package/web/dist/assets/rule-overview-panel-C4QQ7U3S.js +1 -0
  152. package/web/dist/assets/{server-Cpj4KNwN.js → server-eYXq3PbE.js} +1 -1
  153. package/web/dist/assets/settings-panel-D8DEH8aH.js +1 -0
  154. package/web/dist/assets/{shield-check-OqUmVLQt.js → shield-check-hQXjdfZ7.js} +1 -1
  155. package/web/dist/assets/smartphone-D4Q7cQfm.js +1 -0
  156. package/web/dist/assets/switch-94vgsMHm.js +1 -0
  157. package/web/dist/assets/{syntax-highlight-Dk0-ewLI.js → syntax-highlight-BFgN_C0Y.js} +1 -1
  158. package/web/dist/assets/{table-2-BR5e3z8m.js → table-2-DvGHhMm4.js} +1 -1
  159. package/web/dist/assets/table-DaXD6jJv.js +1 -0
  160. package/web/dist/assets/textarea-9wB8Hsz-.js +1 -0
  161. package/web/dist/assets/toast-9yeTf7d9.js +1 -0
  162. package/web/dist/assets/use-mocks-file-status-BUIUJ4oQ.js +1 -0
  163. package/web/dist/assets/{wand-sparkles-CJ7fSnom.js → wand-sparkles-Bmt4dDOf.js} +1 -1
  164. package/web/dist/assets/{wifi-CCLKyySQ.js → wifi-Cjj_Br1u.js} +1 -1
  165. package/web/dist/assets/{zap-DR-xiUMe.js → zap-BuOtogAv.js} +1 -1
  166. package/web/dist/index.html +16 -10
  167. package/web/dist/assets/alert-DCvlZAiv.js +0 -1
  168. package/web/dist/assets/arrow-right-B4Mrlj2-.js +0 -1
  169. package/web/dist/assets/button-BTToMaBW.js +0 -1
  170. package/web/dist/assets/checkbox-D1Z9DtjF.js +0 -1
  171. package/web/dist/assets/chevron-down-CPngAx4M.js +0 -1
  172. package/web/dist/assets/chevron-right-V1DGIDbL.js +0 -1
  173. package/web/dist/assets/code-5_eJ_XpJ.js +0 -1
  174. package/web/dist/assets/code-fixer-D48sCDtp.js +0 -20
  175. package/web/dist/assets/detail-panel-BSFz9CUw.js +0 -4
  176. package/web/dist/assets/file-system-access-_og-Tg9o.js +0 -1
  177. package/web/dist/assets/health-panel-BS3L2e5d.js +0 -1
  178. package/web/dist/assets/index-CvTR1IVi.js +0 -18
  179. package/web/dist/assets/index-bfV7sxx5.css +0 -2
  180. package/web/dist/assets/mobile-proxy-panel-tU0NyJ3P.js +0 -8
  181. package/web/dist/assets/mock-config-CULJP6Vt.js +0 -1
  182. package/web/dist/assets/mock-editor-panel-9wlDaySA.js +0 -1
  183. package/web/dist/assets/monaco-editor-BttaUoOz.js +0 -11
  184. package/web/dist/assets/plugin-code-editor-BAIBp4lg.js +0 -2
  185. package/web/dist/assets/plugin-config-ButUV2SZ.js +0 -1
  186. package/web/dist/assets/plugin-generator-uceUPSCv.js +0 -2
  187. package/web/dist/assets/plugin-test-dialog-BrJ4HfRX.js +0 -7
  188. package/web/dist/assets/plus-tVxSydJ8.js +0 -1
  189. package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +0 -1
  190. package/web/dist/assets/route-preview-C4cGrS1-.js +0 -1
  191. package/web/dist/assets/rule-config-BTTGNwMs.js +0 -15
  192. package/web/dist/assets/rule-overview-panel-SFR2OloL.js +0 -1
  193. package/web/dist/assets/select-aBXQ0zNa.js +0 -1
  194. package/web/dist/assets/settings-panel-BMReDG8b.js +0 -1
  195. package/web/dist/assets/smartphone-DKeDuJTU.js +0 -1
  196. package/web/dist/assets/switch-Bes_YiJu.js +0 -1
  197. package/web/dist/assets/table-nW7uQVcl.js +0 -1
  198. package/web/dist/assets/textarea-BSI0rnW6.js +0 -1
  199. /package/web/dist/assets/{babel-DCr4VA1c.js → babel-bwZm0qi3.js} +0 -0
  200. /package/web/dist/assets/{clipboard-VhPXNN5m.js → clipboard-Bt1qY01V.js} +0 -0
  201. /package/web/dist/assets/{estree-qO_m2Qaw.js → estree-MatbZDwq.js} +0 -0
  202. /package/web/dist/assets/{html-VDGahtyk.js → html-CCrsbFYX.js} +0 -0
  203. /package/web/dist/assets/{postcss-BUm8EKMw.js → postcss-BxN7Gr8n.js} +0 -0
  204. /package/web/dist/assets/{standalone-Bj8SSd8Q.js → standalone-DsnFnsqM.js} +0 -0
@@ -36,15 +36,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.MocksFileInvalidError = void 0;
39
40
  exports.dedupeMockRulesById = dedupeMockRulesById;
41
+ exports.formatMocksParseError = formatMocksParseError;
42
+ exports.parseMocksFileText = parseMocksFileText;
43
+ exports.findJsonErrorOffset = findJsonErrorOffset;
44
+ exports.resolveMocksWriteTarget = resolveMocksWriteTarget;
45
+ exports.resolveConfiguredMocksPath = resolveConfiguredMocksPath;
46
+ exports.detectLegacyMocksPath = detectLegacyMocksPath;
47
+ exports.formatLegacyMocksNotice = formatLegacyMocksNotice;
48
+ exports.readConfiguredMocksPath = readConfiguredMocksPath;
40
49
  exports.createMockHandler = createMockHandler;
41
50
  const fs = __importStar(require("fs"));
42
51
  const path = __importStar(require("path"));
52
+ const os = __importStar(require("os"));
53
+ const chokidar_1 = __importDefault(require("chokidar"));
43
54
  const debug_1 = __importDefault(require("debug"));
44
55
  const map_local_1 = require("./map-local");
45
56
  const proxy_record_1 = require("./proxy-record");
46
57
  const client_identity_1 = require("./client-identity");
47
58
  const body_utils_1 = require("./body-utils");
59
+ const mock_utils_1 = require("./mock-utils");
48
60
  const proxyDebug = (0, debug_1.default)('proxy');
49
61
  /** 按 id 去重,后出现的条目覆盖先前的(与前端 use-mocks 行为一致) */
50
62
  function dedupeMockRulesById(rules) {
@@ -56,13 +68,261 @@ function dedupeMockRulesById(rules) {
56
68
  }
57
69
  return Array.from(byId.values()).sort((a, b) => a.id - b.id);
58
70
  }
71
+ /** Thrown when a write would overwrite a mocks file that failed to parse (issue #120). */
72
+ class MocksFileInvalidError extends Error {
73
+ constructor(filePath, parseError) {
74
+ super(formatMocksParseError(filePath, parseError));
75
+ this.filePath = filePath;
76
+ this.parseError = parseError;
77
+ this.code = 'MOCKS_FILE_INVALID';
78
+ this.name = 'MocksFileInvalidError';
79
+ }
80
+ }
81
+ exports.MocksFileInvalidError = MocksFileInvalidError;
82
+ function formatMocksParseError(filePath, err) {
83
+ const loc = err.line != null ? `第 ${err.line} 行第 ${err.column} 列:` : '';
84
+ return `Mock 文件格式错误,已禁止写入以免覆盖: ${filePath} ${loc}${err.message}`;
85
+ }
86
+ /**
87
+ * Parse mocks file text. Issue #120: empty / whitespace-only text is an empty
88
+ * rule list (not an error); anything else that fails JSON.parse returns a
89
+ * parse error with 1-based line/column.
90
+ */
91
+ function parseMocksFileText(text) {
92
+ if (text.replace(/^\uFEFF/, '').trim() === '')
93
+ return { data: {}, error: null };
94
+ try {
95
+ return { data: JSON.parse(text.replace(/^\uFEFF/, '')), error: null };
96
+ }
97
+ catch (e) {
98
+ const message = String(e?.message || e);
99
+ let line = null;
100
+ let column = null;
101
+ const body = text.replace(/^\uFEFF/, '');
102
+ const lc = /\(line (\d+) column (\d+)\)/.exec(message);
103
+ const pos = /at position (\d+)/.exec(message);
104
+ const offset = lc ? null : pos ? Number(pos[1]) : findJsonErrorOffset(body);
105
+ if (lc) {
106
+ line = Number(lc[1]);
107
+ column = Number(lc[2]);
108
+ }
109
+ else {
110
+ const before = body.slice(0, offset ?? body.length).split('\n');
111
+ line = before.length;
112
+ column = before[before.length - 1].length + 1;
113
+ }
114
+ // Drop V8's quoted source snippet and the "(line x column y)" suffix; position is reported separately.
115
+ const short = message.replace(/\s*\(line \d+ column \d+\)/, '').split(/,\s*(?:\.\.\.)?"/s)[0].replace(/\s+in JSON at position \d+/, '').trim();
116
+ return { data: null, error: { line, column, message: short } };
117
+ }
118
+ }
119
+ /**
120
+ * Offset of the first JSON syntax error (V8 does not always report a
121
+ * position, e.g. "Unexpected token ']', ... is not valid JSON").
122
+ */
123
+ function findJsonErrorOffset(t) {
124
+ let i = 0;
125
+ const ws = () => { while (i < t.length && ' \t\n\r'.includes(t[i]))
126
+ i++; };
127
+ const fail = () => { throw i; };
128
+ const str = () => {
129
+ i++;
130
+ while (i < t.length && t[i] !== '"') {
131
+ if (t[i] === '\\')
132
+ i++;
133
+ else if (t.charCodeAt(i) < 0x20)
134
+ fail();
135
+ i++;
136
+ }
137
+ if (i >= t.length)
138
+ fail();
139
+ i++;
140
+ };
141
+ const value = () => {
142
+ ws();
143
+ const c = t[i];
144
+ if (c === '{') {
145
+ i++;
146
+ ws();
147
+ if (t[i] === '}') {
148
+ i++;
149
+ return;
150
+ }
151
+ for (;;) {
152
+ ws();
153
+ if (t[i] !== '"')
154
+ fail();
155
+ str();
156
+ ws();
157
+ if (t[i] !== ':')
158
+ fail();
159
+ i++;
160
+ value();
161
+ ws();
162
+ if (t[i] === ',') {
163
+ i++;
164
+ continue;
165
+ }
166
+ if (t[i] === '}') {
167
+ i++;
168
+ return;
169
+ }
170
+ fail();
171
+ }
172
+ }
173
+ else if (c === '[') {
174
+ i++;
175
+ ws();
176
+ if (t[i] === ']') {
177
+ i++;
178
+ return;
179
+ }
180
+ for (;;) {
181
+ value();
182
+ ws();
183
+ if (t[i] === ',') {
184
+ i++;
185
+ continue;
186
+ }
187
+ if (t[i] === ']') {
188
+ i++;
189
+ return;
190
+ }
191
+ fail();
192
+ }
193
+ }
194
+ else if (c === '"')
195
+ str();
196
+ else if (/[-0-9]/.test(c || '')) {
197
+ const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(t.slice(i));
198
+ if (!m)
199
+ fail();
200
+ i += m[0].length;
201
+ }
202
+ else {
203
+ for (const w of ['true', 'false', 'null'])
204
+ if (t.startsWith(w, i)) {
205
+ i += w.length;
206
+ return;
207
+ }
208
+ fail();
209
+ }
210
+ };
211
+ try {
212
+ value();
213
+ ws();
214
+ return i < t.length ? i : t.length;
215
+ }
216
+ catch (at) {
217
+ return typeof at === 'number' ? Math.min(at, t.length) : t.length;
218
+ }
219
+ }
220
+ /**
221
+ * Issue #123: the real file a write to `file` must replace. Symlinks are
222
+ * followed so the link itself is never replaced. A dangling link (target
223
+ * missing) creates the target at the link destination when its directory
224
+ * exists, otherwise throws a clear error (the link is left untouched).
225
+ */
226
+ /** Follow a symlink chain lexically (works for dangling links). */
227
+ function followSymlinkChain(file) {
228
+ let cur = path.resolve(file);
229
+ for (let hops = 0; hops < 40; hops++) {
230
+ let st;
231
+ try {
232
+ st = fs.lstatSync(cur);
233
+ }
234
+ catch (_) {
235
+ break;
236
+ }
237
+ if (!st.isSymbolicLink())
238
+ break;
239
+ cur = path.resolve(path.dirname(cur), fs.readlinkSync(cur));
240
+ }
241
+ return cur;
242
+ }
243
+ function resolveMocksWriteTarget(file) {
244
+ try {
245
+ return fs.realpathSync(file);
246
+ }
247
+ catch (_) { /* missing or dangling */ }
248
+ const cur = followSymlinkChain(file);
249
+ if (cur !== path.resolve(file)) {
250
+ const parent = path.dirname(cur);
251
+ let parentReal;
252
+ try {
253
+ parentReal = fs.realpathSync(parent);
254
+ }
255
+ catch (_) {
256
+ throw new Error(`Mock 文件是指向不存在位置的符号链接,无法写入: ${file} -> ${cur}(目录 ${parent} 不存在)`);
257
+ }
258
+ return path.join(parentReal, path.basename(cur));
259
+ }
260
+ return cur;
261
+ }
262
+ /**
263
+ * Issue #121: turn the configured `mocksFilePath` into an absolute path.
264
+ * `~` / `~/…` expand to the user's home directory; a relative path is
265
+ * resolved against the meddle directory (MEDDLE_HOME), not the directory the
266
+ * proxy happened to be started from.
267
+ */
268
+ function resolveConfiguredMocksPath(raw, meddleDir, homeDir = os.homedir()) {
269
+ const v = typeof raw === 'string' ? raw.trim() : '';
270
+ if (!v)
271
+ return null;
272
+ let p = v;
273
+ if (p === '~')
274
+ p = homeDir;
275
+ else if (p.startsWith('~/') || p.startsWith('~\\'))
276
+ p = path.join(homeDir, p.slice(2));
277
+ return path.resolve(meddleDir, p);
278
+ }
279
+ /**
280
+ * Issue #121 migration notice: a relative `mocksFilePath` used to resolve
281
+ * against the proxy's start directory. If the file exists there but not
282
+ * under the new rule (MEDDLE_HOME), report both paths so the user can switch
283
+ * to the old absolute path. Never migrates or rewrites settings.
284
+ */
285
+ function detectLegacyMocksPath(raw, meddleDir, cwd = process.cwd(), homeDir = os.homedir()) {
286
+ const v = typeof raw === 'string' ? raw.trim() : '';
287
+ if (!v || path.isAbsolute(v) || v === '~' || v.startsWith('~/') || v.startsWith('~\\'))
288
+ return null;
289
+ const newPath = resolveConfiguredMocksPath(v, meddleDir, homeDir);
290
+ const oldPath = path.resolve(cwd, v);
291
+ if (!newPath || oldPath === newPath)
292
+ return null;
293
+ const isFile = (p) => { try {
294
+ return fs.statSync(p).isFile();
295
+ }
296
+ catch (_) {
297
+ return false;
298
+ } };
299
+ if (!isFile(oldPath) || isFile(newPath))
300
+ return null;
301
+ return { configured: v, oldPath, newPath };
302
+ }
303
+ function formatLegacyMocksNotice(l) {
304
+ return `Mock 文件路径「${l.configured}」现在相对于 Meddle 配置目录解析为 ${l.newPath}(不存在);` +
305
+ `按旧规则(相对于启动目录)找到的文件是 ${l.oldPath}。如需继续使用它,请把路径改为该绝对路径。`;
306
+ }
307
+ /** Read the raw (unresolved) mocksFilePath from settings.json. */
308
+ function readConfiguredMocksPath(settingsPath) {
309
+ try {
310
+ if (!fs.existsSync(settingsPath))
311
+ return null;
312
+ const v = JSON.parse(fs.readFileSync(settingsPath, 'utf8')).mocksFilePath;
313
+ return typeof v === 'string' ? v : null;
314
+ }
315
+ catch (_) {
316
+ return null;
317
+ }
318
+ }
59
319
  function createMockHandler(ctx) {
60
320
  const DEFAULT_MOCK_FILE = path.join(ctx.meddleDir, 'mocks.json');
61
321
  function loadCustomPathsFromSettings() {
62
322
  try {
63
323
  if (fs.existsSync(ctx.settingsPath)) {
64
324
  const settings = JSON.parse(fs.readFileSync(ctx.settingsPath, 'utf8'));
65
- return { mocksFilePath: settings.mocksFilePath || null };
325
+ return { mocksFilePath: resolveConfiguredMocksPath(settings.mocksFilePath, ctx.meddleDir) };
66
326
  }
67
327
  }
68
328
  catch (error) {
@@ -70,45 +330,165 @@ function createMockHandler(ctx) {
70
330
  }
71
331
  return { mocksFilePath: null };
72
332
  }
333
+ /**
334
+ * Effective mocks file. Issue #118: a configured `mocksFilePath` is honoured
335
+ * even when the file does not exist yet (same as the CLI file mode) — the
336
+ * proxy starts with an empty rule list and the file is created on the first
337
+ * save. Previously a missing custom file silently fell back to the default
338
+ * file, so "switching" to a new file kept reading/writing the old one.
339
+ */
73
340
  function getMockFilePath() {
74
341
  if (ctx.currentMocksPath)
75
342
  return ctx.currentMocksPath;
76
343
  const customPaths = loadCustomPathsFromSettings();
77
- if (customPaths.mocksFilePath && fs.existsSync(customPaths.mocksFilePath)) {
78
- ctx.currentMocksPath = customPaths.mocksFilePath;
79
- return ctx.currentMocksPath;
344
+ ctx.currentMocksPath = customPaths.mocksFilePath || DEFAULT_MOCK_FILE;
345
+ return ctx.currentMocksPath;
346
+ }
347
+ /** Path whose contents are currently in ctx.mockRules (null before first load). */
348
+ let loadedMocksPath = null;
349
+ /** Issue #120: parse failure of the loaded file; while set, every write is refused. */
350
+ let parseError = null;
351
+ function getMocksParseError() { return parseError; }
352
+ /**
353
+ * Issue #123 (CI flake root cause): chokidar does not report writes that
354
+ * land while it is still setting up (ignoreInitial). Remember what was
355
+ * loaded so the watcher can re-check once it is ready.
356
+ */
357
+ let loadedSig = null;
358
+ function fileSig(file) {
359
+ try {
360
+ const st = fs.statSync(file);
361
+ return `${st.ino}:${st.size}:${st.mtimeMs}`;
362
+ }
363
+ catch (_) {
364
+ return 'missing';
80
365
  }
81
- ctx.currentMocksPath = DEFAULT_MOCK_FILE;
82
- return DEFAULT_MOCK_FILE;
83
366
  }
84
367
  function loadMockRules() {
368
+ const mockFile = getMockFilePath();
369
+ // Issue #118: switching to another file must not inherit the old
370
+ // file's id counter or rules; re-reading the same file keeps the
371
+ // counter monotonic (issue #115).
372
+ const switched = loadedMocksPath !== null && loadedMocksPath !== mockFile;
373
+ const firstLoadOfThisFile = loadedMocksPath !== mockFile;
374
+ if (switched) {
375
+ proxyDebug(`Mock 文件切换: ${loadedMocksPath} -> ${mockFile}`);
376
+ ctx.mockIdSeq = 1;
377
+ }
378
+ loadedMocksPath = mockFile;
379
+ if (watcherState && watcherState.file !== mockFile)
380
+ startWatcher(mockFile);
85
381
  try {
86
- const mockFile = getMockFilePath();
87
382
  if (fs.existsSync(mockFile)) {
88
- const data = JSON.parse(fs.readFileSync(mockFile, 'utf8'));
383
+ loadedSig = fileSig(mockFile);
384
+ const parsed = parseMocksFileText(fs.readFileSync(mockFile, 'utf8'));
385
+ if (parsed.error) {
386
+ // Issue #120: never treat a broken file as empty-and-writable.
387
+ const wasSame = parseError && parseError.message === parsed.error.message && parseError.line === parsed.error.line;
388
+ parseError = parsed.error;
389
+ ctx.mockRules = [];
390
+ if (!wasSame)
391
+ console.error(formatMocksParseError(mockFile, parsed.error));
392
+ return;
393
+ }
394
+ if (parseError)
395
+ console.log(`Mock 文件已修复,恢复正常: ${mockFile}`);
396
+ parseError = null;
397
+ const data = parsed.data || {};
89
398
  const loaded = Array.isArray(data.rules) ? data.rules : [];
90
- ctx.mockRules = dedupeMockRulesById(loaded);
91
- if (loaded.length !== ctx.mockRules.length) {
92
- proxyDebug(`Mock 规则去重: ${loaded.length} -> ${ctx.mockRules.length}`);
399
+ // Issue #115: never drop rules that share an id — give the
400
+ // later duplicates fresh ids instead (no silent data loss).
401
+ const { rules, reassigned } = (0, mock_utils_1.ensureUniqueMockRuleIds)(loaded);
402
+ ctx.mockRules = rules;
403
+ if (reassigned > 0) {
404
+ console.warn(`Mock 规则 id 重复或缺失,已为 ${reassigned} 条规则重新分配 id (${mockFile})`);
93
405
  }
94
- ctx.mockIdSeq = (data.nextId || Math.max(0, ...ctx.mockRules.map(r => r.id || 0))) + 1;
406
+ // nextId is the next id to hand out; a stale/missing value must
407
+ // never collide with ids already on disk (issue #115).
408
+ ctx.mockIdSeq = (0, mock_utils_1.nextMockRuleId)(ctx.mockRules, Math.max(Number(data.nextId) || 0, (Number(data.lastId) || 0) + 1, ctx.mockIdSeq || 0));
95
409
  proxyDebug(`已加载 ${ctx.mockRules.length} 条 Mock 规则 (${mockFile})`);
96
410
  }
411
+ else {
412
+ // File removed / never created — clear in-memory rules (watcher
413
+ // unlink, or a configured path that does not exist yet). The
414
+ // file is created on the first save; nothing else is touched.
415
+ ctx.mockRules = [];
416
+ parseError = null;
417
+ loadedSig = 'missing';
418
+ if (firstLoadOfThisFile && mockFile !== DEFAULT_MOCK_FILE) {
419
+ console.warn(`Mock 规则文件不存在,将以空列表开始,首次保存时创建: ${mockFile}`);
420
+ }
421
+ proxyDebug(`Mock 规则文件不存在,已清空内存规则 (${mockFile})`);
422
+ }
97
423
  }
98
424
  catch (err) {
99
425
  console.error('加载 mock 规则失败:', err.message);
100
426
  ctx.mockRules = [];
101
427
  }
102
428
  }
429
+ function assertMocksWritable() {
430
+ const mockFile = getMockFilePath();
431
+ // Re-validate what is on disk right before writing: the file may have
432
+ // been broken after the last load (watcher not fired yet, stale tab).
433
+ if (!parseError && fs.existsSync(mockFile)) {
434
+ try {
435
+ const parsed = parseMocksFileText(fs.readFileSync(mockFile, 'utf8'));
436
+ if (parsed.error) {
437
+ parseError = parsed.error;
438
+ console.error(formatMocksParseError(mockFile, parsed.error));
439
+ }
440
+ }
441
+ catch (_) { /* unreadable: let the write report it */ }
442
+ }
443
+ if (parseError)
444
+ throw new MocksFileInvalidError(mockFile, parseError);
445
+ }
103
446
  function saveMockRules() {
447
+ // Issue #120: single choke point for every write — refuse to overwrite a broken file.
448
+ assertMocksWritable();
104
449
  try {
105
- ctx.mockRules = dedupeMockRulesById(ctx.mockRules);
450
+ // Issue #115: keep every rule (re-id duplicates instead of dropping
451
+ // them) and persist a sequence that is above every stored id.
452
+ ctx.mockRules = (0, mock_utils_1.ensureUniqueMockRuleIds)(ctx.mockRules).rules;
453
+ ctx.mockIdSeq = (0, mock_utils_1.nextMockRuleId)(ctx.mockRules, ctx.mockIdSeq);
106
454
  const mockFile = getMockFilePath();
107
- fs.writeFileSync(mockFile, JSON.stringify({ nextId: ctx.mockIdSeq, rules: ctx.mockRules }, null, 2), 'utf8');
455
+ const dir = path.dirname(mockFile);
456
+ if (!fs.existsSync(dir))
457
+ fs.mkdirSync(dir, { recursive: true });
458
+ // Issue #123: write next to the real file so a symlinked mocks file
459
+ // (e.g. into a dotfiles repo) stays a symlink, and keep its mode/owner.
460
+ const target = resolveMocksWriteTarget(mockFile);
461
+ let existing = null;
462
+ try {
463
+ existing = fs.statSync(target);
464
+ }
465
+ catch (_) { /* new file */ }
466
+ // Write to a temp file then rename, so a crash never leaves half a file (issue #120).
467
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
468
+ fs.writeFileSync(tmp, JSON.stringify({ nextId: ctx.mockIdSeq, rules: ctx.mockRules }, null, 2), { encoding: 'utf8', mode: existing ? existing.mode & 0o7777 : 0o666 });
469
+ try {
470
+ if (existing) {
471
+ fs.chmodSync(tmp, existing.mode & 0o7777);
472
+ try {
473
+ fs.chownSync(tmp, existing.uid, existing.gid);
474
+ }
475
+ catch (_) { /* not permitted: best effort */ }
476
+ }
477
+ fs.renameSync(tmp, target);
478
+ loadedSig = fileSig(mockFile);
479
+ }
480
+ catch (e) {
481
+ try {
482
+ fs.unlinkSync(tmp);
483
+ }
484
+ catch (_) { /* ignore */ }
485
+ throw e;
486
+ }
108
487
  proxyDebug(`Mock 规则已保存到 ${mockFile}`);
109
488
  }
110
489
  catch (err) {
111
490
  console.error('保存 mock 规则失败:', err.message);
491
+ throw err;
112
492
  }
113
493
  }
114
494
  function matchMockRule(url, method) {
@@ -117,25 +497,23 @@ function createMockHandler(ctx) {
117
497
  return false;
118
498
  if (rule.method && rule.method !== '*' && rule.method.toUpperCase() !== method.toUpperCase())
119
499
  return false;
120
- try {
121
- return new RegExp(rule.urlPattern).test(url);
122
- }
123
- catch {
124
- return url.includes(rule.urlPattern);
125
- }
500
+ if (!(0, mock_utils_1.matchUrlPattern)(url, rule.urlPattern))
501
+ return false;
502
+ if (!(0, mock_utils_1.matchQueryCondition)(url, rule.query))
503
+ return false;
504
+ return true;
126
505
  }) || null;
127
506
  }
128
- function buildMockResponseForTest(rule) {
507
+ function buildMockResponseForTest(rule, requestHeaders) {
129
508
  const statusCode = rule.statusCode || 200;
130
509
  const ruleHeaders = {};
131
510
  for (const [key, value] of Object.entries(rule.headers || {})) {
132
511
  ruleHeaders[key.toLowerCase()] = value;
133
512
  }
134
- const headers = {
513
+ const headers = (0, mock_utils_1.resolveHeaderPlaceholders)((0, mock_utils_1.mergeResponseHeaders)({
135
514
  'x-mock-rule': encodeURIComponent(rule.name || rule.id?.toString() || rule.urlPattern || ''),
136
- 'content-type': ruleHeaders['content-type'] || 'application/json',
137
- ...ruleHeaders,
138
- };
515
+ 'content-type': 'application/json',
516
+ }, ruleHeaders), requestHeaders);
139
517
  let body = '';
140
518
  if (rule.bodyType === 'file' && rule.body) {
141
519
  let filePath = rule.body.replace(/^file:\/\//, '').replace(/^\/[A-Za-z]:\//, '');
@@ -171,13 +549,12 @@ function createMockHandler(ctx) {
171
549
  for (const [key, value] of Object.entries(rule.headers || {})) {
172
550
  ruleHeaders[key.toLowerCase()] = value;
173
551
  }
174
- const responseHeaders = {
552
+ const responseHeaders = (0, mock_utils_1.resolveHeaderPlaceholders)((0, mock_utils_1.mergeResponseHeaders)({
175
553
  'X-Mock-Rule': encodeURIComponent(mockRuleName),
176
554
  'Access-Control-Allow-Origin': '*',
177
555
  'Access-Control-Allow-Methods': '*',
178
556
  'Access-Control-Allow-Headers': '*',
179
- ...ruleHeaders
180
- };
557
+ }, ruleHeaders), req.headers);
181
558
  let finalStatusCode = statusCode;
182
559
  let statusMessage = 'OK (Mock)';
183
560
  if (isFileBody) {
@@ -338,9 +715,230 @@ function createMockHandler(ctx) {
338
715
  doSend();
339
716
  }
340
717
  }
718
+ /** Active file watcher (issue #118: follows the effective mocks file). */
719
+ let watcherState = null;
720
+ let watcherOnReload;
721
+ function startWatcher(mockFile) {
722
+ if (watcherState) {
723
+ watcherState.close();
724
+ proxyDebug(`stopped watching mocks file: ${watcherState.file}`);
725
+ }
726
+ const dir = path.dirname(mockFile);
727
+ const base = path.basename(mockFile);
728
+ try {
729
+ if (!fs.existsSync(dir))
730
+ fs.mkdirSync(dir, { recursive: true });
731
+ }
732
+ catch (_) { /* ignore */ }
733
+ const debugWatch = process.env.MEDDLE_DEBUG_WATCH === '1';
734
+ const t0 = Date.now();
735
+ const dbg = (msg) => { if (debugWatch)
736
+ console.error(`[watch +${Date.now() - t0}ms] ${msg}`); };
737
+ // Issue #123: when the mocks path is a symlink, edits land on the real
738
+ // target (git pull in a dotfiles repo, fixing JSON there), so watch the
739
+ // target's directory too and accept events for either path.
740
+ const resolveTarget = () => {
741
+ try {
742
+ return resolveMocksWriteTarget(mockFile);
743
+ }
744
+ catch (_) {
745
+ // Issue #125: a dangling link whose target directory does not
746
+ // exist yet still has a destination to wait for.
747
+ return followSymlinkChain(mockFile);
748
+ }
749
+ };
750
+ let target = resolveTarget();
751
+ let targetDir = null;
752
+ // Watch the directory (not only the file) so atomic replace / recreate
753
+ // still triggers — same pattern as route-rules watcher.
754
+ const initialTargetDir = path.dirname(target);
755
+ targetDir = path.resolve(initialTargetDir) !== path.resolve(dir) && fs.existsSync(initialTargetDir) ? initialTargetDir : null;
756
+ // Both directories go into the initial watch so they are ready together.
757
+ const watcher = chokidar_1.default.watch(targetDir ? [dir, targetDir] : dir, {
758
+ ignoreInitial: true,
759
+ depth: 0,
760
+ // Follow the symlink target explicitly (below) instead of relying on
761
+ // chokidar's implicit following, which differs per platform backend.
762
+ followSymlinks: false,
763
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },
764
+ });
765
+ // Catch up on a write chokidar did not report because it landed while a
766
+ // directory watch was still being set up (ignoreInitial).
767
+ const catchUp = (why) => {
768
+ if (closed || !watcherState || watcherState.file !== mockFile)
769
+ return;
770
+ recheckTargetDir(why);
771
+ if (fileSig(mockFile) !== loadedSig)
772
+ reload(why);
773
+ };
774
+ // Issue #125: the target directory may not exist yet (dangling link to a
775
+ // repo that is cloned later). Watch its nearest existing ancestor
776
+ // instead and move on to the real directory once it shows up.
777
+ // A separate watcher instance is used for the ancestor: chokidar 3's
778
+ // unwatch() adds '<dir>/**' to the ignore list, which would also
779
+ // swallow every later event for the link / target directories below it.
780
+ let pendingAncestor = null;
781
+ let ancestorWatcher = null;
782
+ const isWithin = (parent, child) => {
783
+ const rel = path.relative(path.resolve(parent), path.resolve(child));
784
+ return rel === '' || !(rel === '..' || rel.startsWith('..' + path.sep) || path.isAbsolute(rel));
785
+ };
786
+ const nearestExistingAncestor = (p) => {
787
+ let cur = path.dirname(p);
788
+ while (!fs.existsSync(cur) && path.dirname(cur) !== cur)
789
+ cur = path.dirname(cur);
790
+ return cur;
791
+ };
792
+ const stopAncestorWatch = () => {
793
+ if (ancestorWatcher)
794
+ ancestorWatcher.close().catch(() => { });
795
+ ancestorWatcher = null;
796
+ pendingAncestor = null;
797
+ };
798
+ const watchTargetDir = (force = false) => {
799
+ const want = path.dirname(target);
800
+ const next = path.resolve(want) === path.resolve(dir) ? null : want;
801
+ const exists = !!next && fs.existsSync(next);
802
+ if (!force && next === targetDir && (exists || !next) && !pendingAncestor)
803
+ return;
804
+ stopAncestorWatch();
805
+ if (targetDir && (targetDir !== next || !exists))
806
+ watcher.unwatch(targetDir);
807
+ targetDir = next;
808
+ if (targetDir && exists) {
809
+ watcher.add(targetDir);
810
+ // chokidar emits no 'ready' for add(); re-check a few times
811
+ // while the new directory watch comes up.
812
+ for (const ms of [100, 300, 1000]) {
813
+ setTimeout(() => catchUp('changed while target watch was starting'), ms).unref?.();
814
+ }
815
+ }
816
+ else if (targetDir) {
817
+ // Issue #125: the target directory does not exist (yet, or any
818
+ // more): wait for it in its nearest existing ancestor.
819
+ const ancestor = nearestExistingAncestor(targetDir);
820
+ pendingAncestor = ancestor;
821
+ const aw = chokidar_1.default.watch(ancestor, { ignoreInitial: true, depth: 0, followSymlinks: false });
822
+ aw.on('addDir', (p) => {
823
+ if (closed || ancestorWatcher !== aw || !targetDir)
824
+ return;
825
+ if (isWithin(p, targetDir))
826
+ watchTargetDir(true);
827
+ });
828
+ // Issue #141: the ancestor itself can vanish too — `rm -rf repo`
829
+ // removes repo/dotfiles first, and if that unlinkDir is handled
830
+ // before repo is gone, the ancestor picked here is repo, which a
831
+ // moment later no longer exists: a watch on a deleted directory
832
+ // never fires again. The directory may also have appeared while
833
+ // this watch was starting. Re-validate on ready and on any raw
834
+ // event (inotify reports the watched directory's own removal).
835
+ const recheck = (why) => { if (!closed && ancestorWatcher === aw)
836
+ recheckTargetDir(why); };
837
+ aw.on('ready', () => { dbg(`ancestor watch ready: ${ancestor}`); recheck('ancestor watch ready'); });
838
+ aw.on('unlinkDir', () => recheck('ancestor directory removed'));
839
+ aw.on('raw', () => recheck('ancestor raw event'));
840
+ aw.on('error', () => recheck('ancestor watch error'));
841
+ ancestorWatcher = aw;
842
+ }
843
+ dbg(`target ${target}; extra dir ${targetDir ?? '(none)'}${pendingAncestor ? ` (missing; waiting in ${pendingAncestor})` : ''}`);
844
+ };
845
+ /**
846
+ * Issue #141: make sure the watch plan still matches the disk. chokidar
847
+ * does not report every directory removal: a target directory removed
848
+ * while the watch is still starting yields no unlinkDir, and an
849
+ * ancestor removed right after it was picked leaves a dead watch.
850
+ * Either way the recreated directory would never be noticed again.
851
+ */
852
+ const recheckTargetDir = (why) => {
853
+ if (closed || !targetDir)
854
+ return;
855
+ const stale = pendingAncestor
856
+ ? fs.existsSync(targetDir) || nearestExistingAncestor(targetDir) !== pendingAncestor
857
+ : !fs.existsSync(targetDir);
858
+ if (!stale)
859
+ return;
860
+ dbg(`re-planning target watch (${why})`);
861
+ watchTargetDir(true);
862
+ };
863
+ watchTargetDir();
864
+ let closed = false;
865
+ let trailingCheck = null;
866
+ const reload = (event, changedPath) => {
867
+ // Ignore late events from a watcher that was replaced, and any
868
+ // file in the directory other than the active mocks file / its target.
869
+ if (closed || !watcherState || watcherState.file !== mockFile)
870
+ return;
871
+ if (changedPath) {
872
+ const p = path.resolve(changedPath);
873
+ if (p !== path.resolve(mockFile) && p !== target)
874
+ return;
875
+ }
876
+ // The link itself may have been retargeted: follow the new target.
877
+ const nextTarget = resolveTarget();
878
+ if (nextTarget !== target) {
879
+ target = nextTarget;
880
+ watchTargetDir();
881
+ }
882
+ else
883
+ recheckTargetDir(event);
884
+ proxyDebug(`mocks file ${event}: ${base}`);
885
+ loadMockRules();
886
+ if (watcherOnReload)
887
+ watcherOnReload();
888
+ // Issue #116: chokidar throttles raw fs events per file on the
889
+ // leading edge (5ms, no trailing emit), so a write landing right
890
+ // after the one being handled (e.g. broken -> fixed a few ms apart,
891
+ // especially while the watcher is still starting) can be dropped.
892
+ // Re-check the file signature once the burst is over.
893
+ if (trailingCheck)
894
+ clearTimeout(trailingCheck);
895
+ trailingCheck = setTimeout(() => { trailingCheck = null; catchUp('changed during a burst of writes'); }, 150);
896
+ trailingCheck.unref?.();
897
+ };
898
+ if (debugWatch) {
899
+ watcher.on('all', (ev, p) => dbg(`${ev} ${p}`));
900
+ }
901
+ watcher.on('ready', () => {
902
+ dbg('ready');
903
+ catchUp('changed before watcher ready');
904
+ });
905
+ watcher.on('change', (p) => reload('changed', p));
906
+ watcher.on('add', (p) => reload('added', p));
907
+ watcher.on('unlink', (p) => reload('removed', p));
908
+ // Issue #125: the target directory itself was removed (repo deleted /
909
+ // re-cloned): fall back to waiting for it in its nearest ancestor.
910
+ watcher.on('unlinkDir', (p) => {
911
+ if (closed || !targetDir)
912
+ return;
913
+ if (path.resolve(p) === path.resolve(targetDir))
914
+ watchTargetDir(true);
915
+ });
916
+ watcherState = {
917
+ file: mockFile,
918
+ close: () => {
919
+ closed = true;
920
+ if (trailingCheck)
921
+ clearTimeout(trailingCheck);
922
+ stopAncestorWatch();
923
+ watcher.close().catch(() => { });
924
+ },
925
+ };
926
+ proxyDebug(`watching mocks file: ${mockFile}`);
927
+ }
928
+ /**
929
+ * Watch the mocks file for external edits (CLI file-mode / hand edits).
930
+ * Aligns with route-rules watcher so file-only writes still hot-reload (issue #85).
931
+ * When the effective mocks file changes (issue #118), loadMockRules moves
932
+ * the watcher to the new file automatically.
933
+ */
934
+ function initMockFileWatcher(onReload) {
935
+ watcherOnReload = onReload;
936
+ startWatcher(getMockFilePath());
937
+ }
341
938
  return {
342
- getMockFilePath, loadMockRules, saveMockRules, matchMockRule,
939
+ getMockFilePath, loadMockRules, saveMockRules, assertMocksWritable, getMocksParseError, matchMockRule,
343
940
  buildMockResponseForTest, sendMockResponse, loadCustomPathsFromSettings,
941
+ initMockFileWatcher,
344
942
  };
345
943
  }
346
944
  //# sourceMappingURL=mock-handler.js.map