@blamejs/core 0.4.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. package/package.json +39 -0
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ /**
3
+ * deprecate — runtime deprecation API for the framework's LTS contract.
4
+ *
5
+ * Operators see a one-time stderr warning the first time deprecated
6
+ * surface is used, with the version it was deprecated in and the
7
+ * version it'll be removed in. Production runs suppress warnings by
8
+ * default (operators don't want stderr noise from deprecated paths in
9
+ * code they don't own), but BLAMEJS_DEPRECATIONS env var inverts that
10
+ * for visibility-on-demand.
11
+ *
12
+ * var dep = b.deprecate;
13
+ *
14
+ * // Direct warning at the call site
15
+ * dep.warn("auth.legacyVerify", {
16
+ * since: "0.2.0",
17
+ * removeIn: "0.4.0",
18
+ * message: "use auth.password.verify(stored, plain) instead",
19
+ * hint: "see MIGRATING.md#0-2-to-0-4",
20
+ * });
21
+ *
22
+ * // Wrap an old function so calls trigger the warning automatically
23
+ * var legacyVerify = dep.wrap(newVerify, "auth.legacyVerify", {
24
+ * since: "0.2.0", removeIn: "0.4.0",
25
+ * message: "renamed to auth.password.verify",
26
+ * });
27
+ *
28
+ * // Mark a property as deprecated; access triggers the warning
29
+ * dep.alias(targetObj, "oldKey", "newKey", {
30
+ * since: "0.2.0", removeIn: "0.4.0",
31
+ * });
32
+ *
33
+ * dep.list(); // → [{ name, since, removeIn, callCount, firstSeen }]
34
+ * dep.reset(); // clears the seen-set; tests
35
+ *
36
+ * BLAMEJS_DEPRECATIONS env var controls runtime behavior:
37
+ * "warn" — stderr warning on first use of each (name, since) pair
38
+ * (default outside production)
39
+ * "silent" — skip entirely (default in production)
40
+ * "error" — throw on first use; development tool to surface every
41
+ * deprecated call site as a hard failure during a sweep
42
+ *
43
+ * Mode resolution order:
44
+ * 1. process.env.BLAMEJS_DEPRECATIONS if set
45
+ * 2. "silent" when process.env.NODE_ENV === "production"
46
+ * 3. "warn" otherwise
47
+ *
48
+ * Warnings dedupe by (name, since) — calling deprecate.warn(...) ten
49
+ * thousand times with the same args produces one stderr line. The
50
+ * call counter is still incremented so dep.list() shows usage volume.
51
+ */
52
+
53
+ var { FrameworkError } = require("./framework-error");
54
+
55
+ class DeprecateError extends FrameworkError {
56
+ constructor(code, message) {
57
+ super(message, code);
58
+ this.name = "DeprecateError";
59
+ this.permanent = true;
60
+ this.isDeprecateError = true;
61
+ }
62
+ }
63
+
64
+ // Map of "<name>:<since>" → { name, since, removeIn, callCount, firstSeen }
65
+ var _seen = new Map();
66
+
67
+ function _modeFromEnv() {
68
+ var env = process.env.BLAMEJS_DEPRECATIONS;
69
+ if (typeof env === "string" && env.length > 0) {
70
+ var v = env.toLowerCase();
71
+ if (v === "warn" || v === "silent" || v === "error") return v;
72
+ }
73
+ if (process.env.NODE_ENV === "production") return "silent";
74
+ return "warn";
75
+ }
76
+
77
+ function _format(name, opts) {
78
+ opts = opts || {};
79
+ var line = "[blamejs:deprecated] " + name;
80
+ if (opts.since) line += " (since " + opts.since + ")";
81
+ if (opts.removeIn) line += "; removed in " + opts.removeIn;
82
+ if (opts.message) line += " — " + opts.message;
83
+ if (opts.hint) line += " · " + opts.hint;
84
+ return line;
85
+ }
86
+
87
+ function _validateOpts(opts, fnName) {
88
+ if (!opts || typeof opts !== "object") {
89
+ throw new DeprecateError("deprecate/bad-opts",
90
+ fnName + ": opts is required (with at least 'since' and 'removeIn')");
91
+ }
92
+ if (typeof opts.since !== "string" || opts.since.length === 0) {
93
+ throw new DeprecateError("deprecate/bad-opts",
94
+ fnName + ": opts.since (version string) is required");
95
+ }
96
+ if (typeof opts.removeIn !== "string" || opts.removeIn.length === 0) {
97
+ throw new DeprecateError("deprecate/bad-opts",
98
+ fnName + ": opts.removeIn (version string) is required");
99
+ }
100
+ }
101
+
102
+ function warn(name, opts) {
103
+ if (typeof name !== "string" || name.length === 0) {
104
+ throw new DeprecateError("deprecate/bad-name",
105
+ "warn: name is required (the public identifier being deprecated)");
106
+ }
107
+ _validateOpts(opts, "warn");
108
+
109
+ var key = name + ":" + opts.since;
110
+ var entry = _seen.get(key);
111
+ if (!entry) {
112
+ entry = {
113
+ name: name,
114
+ since: opts.since,
115
+ removeIn: opts.removeIn,
116
+ message: opts.message || null,
117
+ hint: opts.hint || null,
118
+ callCount: 0,
119
+ firstSeen: new Date().toISOString(),
120
+ };
121
+ _seen.set(key, entry);
122
+ }
123
+ entry.callCount++;
124
+
125
+ var mode = _modeFromEnv();
126
+ if (mode === "silent") return;
127
+
128
+ // Emit on first occurrence only (dedupe)
129
+ if (entry.callCount > 1) return;
130
+
131
+ var line = _format(name, opts);
132
+ if (mode === "error") {
133
+ throw new DeprecateError("deprecate/used-in-error-mode",
134
+ line + " — BLAMEJS_DEPRECATIONS=error in effect");
135
+ }
136
+ // mode === "warn"
137
+ try { process.stderr.write(line + "\n"); }
138
+ catch (_e) { /* stderr write best-effort */ }
139
+ }
140
+
141
+ // Wrap a function so calling it issues a deprecation warning + delegates.
142
+ // The wrapper preserves the original function's `.length` (arity) so
143
+ // callers introspecting it as a callable see the same shape.
144
+ function wrap(fn, name, opts) {
145
+ if (typeof fn !== "function") {
146
+ throw new DeprecateError("deprecate/bad-target",
147
+ "wrap: first arg must be the replacement function (the new API)");
148
+ }
149
+ if (typeof name !== "string" || name.length === 0) {
150
+ throw new DeprecateError("deprecate/bad-name",
151
+ "wrap: name is required (the deprecated identifier)");
152
+ }
153
+ _validateOpts(opts, "wrap");
154
+ var wrapper = function () {
155
+ warn(name, opts);
156
+ return fn.apply(this, arguments);
157
+ };
158
+ // Preserve identity hints
159
+ Object.defineProperty(wrapper, "name", { value: name + ":deprecated", configurable: true });
160
+ return wrapper;
161
+ }
162
+
163
+ // Define `oldKey` on `target` as a getter that warns then returns
164
+ // `target[newKey]`. The setter writes through so existing assignments
165
+ // still work, but the getter access trips the warning.
166
+ function alias(target, oldKey, newKey, opts) {
167
+ if (!target || typeof target !== "object") {
168
+ throw new DeprecateError("deprecate/bad-target",
169
+ "alias: target must be an object");
170
+ }
171
+ if (typeof oldKey !== "string" || oldKey.length === 0) {
172
+ throw new DeprecateError("deprecate/bad-name",
173
+ "alias: oldKey is required");
174
+ }
175
+ if (typeof newKey !== "string" || newKey.length === 0) {
176
+ throw new DeprecateError("deprecate/bad-name",
177
+ "alias: newKey is required");
178
+ }
179
+ _validateOpts(opts, "alias");
180
+ var aliasName = opts.aliasName ||
181
+ ((target.constructor && target.constructor.name &&
182
+ target.constructor.name !== "Object" ? target.constructor.name + "." : "") + oldKey);
183
+ var fullOpts = Object.assign({
184
+ message: "use '" + newKey + "' instead",
185
+ }, opts);
186
+ Object.defineProperty(target, oldKey, {
187
+ configurable: true,
188
+ enumerable: false,
189
+ get: function () { warn(aliasName, fullOpts); return target[newKey]; },
190
+ set: function (v) { warn(aliasName, fullOpts); target[newKey] = v; },
191
+ });
192
+ }
193
+
194
+ function list() {
195
+ var out = [];
196
+ _seen.forEach(function (v) {
197
+ out.push({
198
+ name: v.name,
199
+ since: v.since,
200
+ removeIn: v.removeIn,
201
+ callCount: v.callCount,
202
+ firstSeen: v.firstSeen,
203
+ });
204
+ });
205
+ // Stable order: most-frequent first, ties broken by first-seen
206
+ out.sort(function (a, b) {
207
+ if (a.callCount !== b.callCount) return b.callCount - a.callCount;
208
+ return a.firstSeen < b.firstSeen ? -1 : 1;
209
+ });
210
+ return out;
211
+ }
212
+
213
+ function reset() { _seen.clear(); }
214
+
215
+ // Export the resolved mode so tests + ops dashboards can introspect
216
+ function getMode() { return _modeFromEnv(); }
217
+
218
+ module.exports = {
219
+ warn: warn,
220
+ wrap: wrap,
221
+ alias: alias,
222
+ list: list,
223
+ reset: reset,
224
+ getMode: getMode,
225
+ DeprecateError: DeprecateError,
226
+ };
package/lib/dev.js ADDED
@@ -0,0 +1,324 @@
1
+ "use strict";
2
+ /**
3
+ * dev — file-watch + child-process restart for iteration loops.
4
+ *
5
+ * The framework's hot-reload primitive. Spawn the app as a child
6
+ * process, watch the source directories, and restart the child when a
7
+ * file changes. On-disk state (vault keys, encrypted DB, sealed
8
+ * cookies) survives the restart because the child re-opens the files
9
+ * — only in-process state is lost, which is the correct semantic for
10
+ * "I just edited a route handler and want to see it."
11
+ *
12
+ * var dev = b.dev.create({
13
+ * command: "node",
14
+ * args: ["./server.js"],
15
+ * watch: ["./routes", "./views", "./public", "./lib"],
16
+ * ignore: [/node_modules/, /\.db$/, /\.tmp$/, /^\./],
17
+ * graceMs: 250, // debounce: collapse bursts into one restart
18
+ * killSignal: "SIGTERM",
19
+ * killTimeoutMs: 4000, // SIGKILL after this if SIGTERM is ignored
20
+ * log: logInstance, // optional structured logger
21
+ * env: { ...process.env, BLAMEJS_DEV: "1" },
22
+ * cwd: process.cwd(),
23
+ * });
24
+ *
25
+ * await dev.start(); // launches child + arms watchers
26
+ * await dev.stop(); // signals child + closes watchers
27
+ *
28
+ * dev.stats(); // → { pid, running, restarts, lastRestartAt }
29
+ *
30
+ * Operator-side, this is what `blamejs dev` will call (next CLI slice).
31
+ *
32
+ * Engine hygiene:
33
+ * - Bursts of file events (a saved-everything keystroke, a multi-
34
+ * file format-on-save) collapse into one restart via debounce.
35
+ * - The child is spawned with stdio: 'inherit' so the operator sees
36
+ * their app's output unchanged.
37
+ * - Parent SIGINT/SIGTERM are forwarded: stop() before exit so an
38
+ * orphan child can't outlive the dev session.
39
+ * - Restart in-flight when a new event arrives: queue one followup,
40
+ * no more — many edits during a slow restart still result in only
41
+ * two restarts, not N.
42
+ *
43
+ * Test seams:
44
+ * opts._spawn(cmd, args, sopts) → child-process-shaped object
45
+ * opts._watch(dir, wopts, listener) → fs.watcher-shaped object
46
+ * These default to child_process.spawn and fs.watch; tests pass
47
+ * fakes to drive the engine without real subprocesses.
48
+ */
49
+
50
+ var path = require("path");
51
+ var { FrameworkError } = require("./framework-error");
52
+
53
+ class DevError extends FrameworkError {
54
+ constructor(code, message) {
55
+ super(message, code);
56
+ this.name = "DevError";
57
+ this.permanent = true;
58
+ this.isDevError = true;
59
+ }
60
+ }
61
+
62
+ var DEFAULT_GRACE_MS = 250;
63
+ var DEFAULT_KILL_TIMEOUT_MS = 4000;
64
+ var DEFAULT_KILL_SIGNAL = "SIGTERM";
65
+ var DEFAULT_IGNORE = [
66
+ /node_modules/,
67
+ /\.git\b/,
68
+ /^\./, // dotfiles + dotdirs
69
+ /\.db$/, /\.db-journal$/, /\.db-wal$/, /\.db-shm$/,
70
+ /\.log$/,
71
+ /\.swp$/, /~$/, // editor scratch files
72
+ ];
73
+
74
+ function _matchesAny(patterns, value) {
75
+ for (var i = 0; i < patterns.length; i++) {
76
+ var p = patterns[i];
77
+ if (p instanceof RegExp) { if (p.test(value)) return true; }
78
+ else if (typeof p === "string") { if (value.indexOf(p) !== -1) return true; }
79
+ }
80
+ return false;
81
+ }
82
+
83
+ function _logVia(log, level, message, fields) {
84
+ if (log && typeof log[level] === "function") {
85
+ try { log[level](message, fields); }
86
+ catch (_e) { /* logger best-effort */ }
87
+ return;
88
+ }
89
+ // Fallback: prefixed line on stdout/stderr
90
+ var line = "[blamejs:dev] " + message + (fields ? " " + JSON.stringify(fields) : "");
91
+ if (level === "error" || level === "warn" || level === "fatal") console.error(line);
92
+ else console.log(line);
93
+ }
94
+
95
+ function create(opts) {
96
+ opts = opts || {};
97
+ if (typeof opts.command !== "string" || opts.command.length === 0) {
98
+ throw new DevError("dev/no-command",
99
+ "dev.create requires opts.command (the program to spawn)");
100
+ }
101
+ var args = Array.isArray(opts.args) ? opts.args.slice() : [];
102
+ var watch = Array.isArray(opts.watch) && opts.watch.length > 0
103
+ ? opts.watch.slice() : ["."];
104
+ var ignore = Array.isArray(opts.ignore)
105
+ ? DEFAULT_IGNORE.concat(opts.ignore)
106
+ : DEFAULT_IGNORE.slice();
107
+ var graceMs = typeof opts.graceMs === "number" && opts.graceMs >= 0
108
+ ? opts.graceMs : DEFAULT_GRACE_MS;
109
+ var killSignal = typeof opts.killSignal === "string" ? opts.killSignal : DEFAULT_KILL_SIGNAL;
110
+ var killTimeoutMs = typeof opts.killTimeoutMs === "number" && opts.killTimeoutMs >= 0
111
+ ? opts.killTimeoutMs : DEFAULT_KILL_TIMEOUT_MS;
112
+ var log = opts.log || null;
113
+ var env = opts.env || process.env;
114
+ var cwd = opts.cwd || process.cwd();
115
+
116
+ // Test seams
117
+ var spawnFn = opts._spawn || function (cmd, sargs, sopts) {
118
+ return require("child_process").spawn(cmd, sargs, sopts);
119
+ };
120
+ var watchFn = opts._watch || function (dir, wopts, listener) {
121
+ return require("fs").watch(dir, wopts, listener);
122
+ };
123
+ var setTimeoutFn = opts._setTimeout || setTimeout;
124
+ var clearTimeoutFn = opts._clearTimeout || clearTimeout;
125
+
126
+ var child = null;
127
+ var watchers = [];
128
+ var debounceTimer = null;
129
+ var killTimer = null;
130
+ var started = false;
131
+ var stopping = false;
132
+ var restarting = false;
133
+ var queuedRestart = false;
134
+ var restartCount = 0;
135
+ var lastRestartAt = null;
136
+
137
+ function _spawnChild() {
138
+ var c = spawnFn(opts.command, args, {
139
+ stdio: opts.stdio || "inherit",
140
+ env: env,
141
+ cwd: cwd,
142
+ });
143
+ c.on("exit", function (code, signal) {
144
+ // If the child exits while we're not stopping/restarting, that's
145
+ // a crash — log it and wait for a file change to retry. This
146
+ // matches nodemon's behavior: a bad commit leaves the child
147
+ // corpse around without spawn-thrashing.
148
+ if (!stopping && !restarting) {
149
+ _logVia(log, "warn",
150
+ "child exited unexpectedly — waiting for file change to restart",
151
+ { code: code, signal: signal });
152
+ child = null;
153
+ }
154
+ });
155
+ c.on("error", function (err) {
156
+ _logVia(log, "error", "spawn error", { error: (err && err.message) || String(err) });
157
+ });
158
+ return c;
159
+ }
160
+
161
+ function _killChild() {
162
+ return new Promise(function (resolve) {
163
+ if (!child) return resolve();
164
+ var c = child;
165
+ var settled = false;
166
+ function done() {
167
+ if (settled) return;
168
+ settled = true;
169
+ if (killTimer) { try { clearTimeoutFn(killTimer); } catch (_e) {} killTimer = null; }
170
+ resolve();
171
+ }
172
+ c.once("exit", done);
173
+ try { c.kill(killSignal); }
174
+ catch (e) {
175
+ _logVia(log, "warn", "kill threw, child may already be gone",
176
+ { error: (e && e.message) || String(e) });
177
+ return done();
178
+ }
179
+ // Hard-kill if the child ignores SIGTERM
180
+ killTimer = setTimeoutFn(function () {
181
+ if (settled) return;
182
+ _logVia(log, "warn",
183
+ "child did not exit after " + killTimeoutMs + "ms — sending SIGKILL");
184
+ try { c.kill("SIGKILL"); }
185
+ catch (_e) { done(); }
186
+ }, killTimeoutMs);
187
+ if (killTimer && typeof killTimer.unref === "function") killTimer.unref();
188
+ });
189
+ }
190
+
191
+ async function _restart() {
192
+ if (restarting) {
193
+ // Coalesce: a restart-in-flight gets one followup queued, never more.
194
+ queuedRestart = true;
195
+ return;
196
+ }
197
+ restarting = true;
198
+ try {
199
+ await _killChild();
200
+ child = null;
201
+ if (stopping) return;
202
+ child = _spawnChild();
203
+ restartCount += 1;
204
+ lastRestartAt = new Date().toISOString();
205
+ _logVia(log, "info", "restarted",
206
+ { pid: child && child.pid, restarts: restartCount });
207
+ } catch (e) {
208
+ _logVia(log, "error", "restart failed",
209
+ { error: (e && e.message) || String(e) });
210
+ } finally {
211
+ restarting = false;
212
+ if (queuedRestart && !stopping) {
213
+ queuedRestart = false;
214
+ // Tail-call the queued restart on the next microtask so we
215
+ // unwind the current stack first.
216
+ Promise.resolve().then(_restart);
217
+ }
218
+ }
219
+ }
220
+
221
+ function _scheduleRestart(reason) {
222
+ if (stopping) return;
223
+ if (debounceTimer) clearTimeoutFn(debounceTimer);
224
+ debounceTimer = setTimeoutFn(function () {
225
+ debounceTimer = null;
226
+ _logVia(log, "info", "change detected, restarting", { reason: reason });
227
+ _restart();
228
+ }, graceMs);
229
+ if (debounceTimer && typeof debounceTimer.unref === "function") debounceTimer.unref();
230
+ }
231
+
232
+ function _onWatchEvent(dir, eventType, filename) {
233
+ if (!filename) return;
234
+ var rel = String(filename);
235
+ var full = path.join(dir, rel);
236
+ if (_matchesAny(ignore, rel) || _matchesAny(ignore, full)) return;
237
+ _scheduleRestart(eventType + ":" + rel);
238
+ }
239
+
240
+ function _armWatchers() {
241
+ for (var i = 0; i < watch.length; i++) {
242
+ (function (dir) {
243
+ var resolved = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
244
+ var w;
245
+ try {
246
+ w = watchFn(resolved, { recursive: true, persistent: false }, function (eventType, filename) {
247
+ _onWatchEvent(resolved, eventType, filename);
248
+ });
249
+ } catch (e) {
250
+ // Missing dir, permission error, platform that doesn't
251
+ // support recursive — log and move on so a bad watch entry
252
+ // doesn't take the whole dev loop down.
253
+ _logVia(log, "warn", "could not watch " + resolved,
254
+ { error: (e && e.message) || String(e) });
255
+ return;
256
+ }
257
+ if (w && typeof w.on === "function") {
258
+ w.on("error", function (err) {
259
+ _logVia(log, "warn", "watcher error on " + resolved,
260
+ { error: (err && err.message) || String(err) });
261
+ });
262
+ }
263
+ watchers.push(w);
264
+ })(watch[i]);
265
+ }
266
+ }
267
+
268
+ function _disarmWatchers() {
269
+ for (var i = 0; i < watchers.length; i++) {
270
+ try { if (watchers[i] && typeof watchers[i].close === "function") watchers[i].close(); }
271
+ catch (_e) { /* close best-effort */ }
272
+ }
273
+ watchers = [];
274
+ }
275
+
276
+ async function start() {
277
+ if (started) return;
278
+ started = true;
279
+ stopping = false;
280
+ _armWatchers();
281
+ child = _spawnChild();
282
+ _logVia(log, "info", "started", { pid: child && child.pid, watch: watch });
283
+ }
284
+
285
+ async function stop() {
286
+ if (!started) return;
287
+ stopping = true;
288
+ if (debounceTimer) { try { clearTimeoutFn(debounceTimer); } catch (_e) {} debounceTimer = null; }
289
+ _disarmWatchers();
290
+ await _killChild();
291
+ child = null;
292
+ started = false;
293
+ stopping = false;
294
+ queuedRestart = false;
295
+ _logVia(log, "info", "stopped", { restarts: restartCount });
296
+ }
297
+
298
+ async function restart() { await _restart(); }
299
+
300
+ function stats() {
301
+ return {
302
+ pid: child && child.pid !== undefined ? child.pid : null,
303
+ running: !!child && started && !stopping,
304
+ restarts: restartCount,
305
+ lastRestartAt: lastRestartAt,
306
+ watchers: watchers.length,
307
+ };
308
+ }
309
+
310
+ return {
311
+ start: start,
312
+ stop: stop,
313
+ restart: restart,
314
+ stats: stats,
315
+ // Test hook: simulate a watcher event for engine tests
316
+ _emit: function (dir, eventType, filename) { _onWatchEvent(dir, eventType, filename); },
317
+ };
318
+ }
319
+
320
+ module.exports = {
321
+ create: create,
322
+ DevError: DevError,
323
+ DEFAULT_IGNORE: DEFAULT_IGNORE,
324
+ };