novac 2.1.1 → 2.2.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 (138) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +0 -0
  3. package/demo.nv +0 -0
  4. package/demo_builtins.nv +0 -0
  5. package/demo_http.nv +0 -0
  6. package/examples/bf.nv +69 -0
  7. package/examples/math.nv +21 -0
  8. package/kits/birdAPI/kitdef.js +954 -0
  9. package/kits/kitRNG/kitdef.js +740 -0
  10. package/kits/kitSSH/kitdef.js +1272 -0
  11. package/kits/kitadb/kitdef.js +606 -0
  12. package/kits/kitai/kitdef.js +2185 -0
  13. package/kits/kitcanvas/kitdef.js +914 -0
  14. package/kits/kitclippy/kitdef.js +925 -0
  15. package/kits/kitgps/kitdef.js +1862 -0
  16. package/kits/kitlibproc/kitdef.js +3 -2
  17. package/kits/kitmorse/kitdef.js +229 -0
  18. package/kits/kitmpatch/kitdef.js +906 -0
  19. package/kits/kitnet/kitdef.js +1401 -0
  20. package/kits/kitproto/kitdef.js +613 -0
  21. package/kits/kitqr/kitdef.js +637 -0
  22. package/kits/kitrequire/kitdef.js +1599 -0
  23. package/kits/libtea/kitdef.js +2691 -0
  24. package/kits/libterm/kitdef.js +2 -0
  25. package/novac/LICENSE +21 -0
  26. package/novac/README.md +1823 -0
  27. package/novac/bin/novac +950 -0
  28. package/novac/bin/nvc +522 -0
  29. package/novac/bin/nvml +542 -0
  30. package/novac/demo.nv +245 -0
  31. package/novac/demo_builtins.nv +209 -0
  32. package/novac/demo_http.nv +62 -0
  33. package/novac/examples/bf.nv +69 -0
  34. package/novac/examples/math.nv +21 -0
  35. package/novac/kits/kitai/kitdef.js +2185 -0
  36. package/novac/kits/kitansi/kitdef.js +1402 -0
  37. package/novac/kits/kitformat/kitdef.js +1485 -0
  38. package/novac/kits/kitgps/kitdef.js +1862 -0
  39. package/novac/kits/kitlibfs/kitdef.js +231 -0
  40. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  41. package/novac/kits/kitmatrix/ex.js +19 -0
  42. package/novac/kits/kitmatrix/kitdef.js +960 -0
  43. package/novac/kits/kitmpatch/kitdef.js +906 -0
  44. package/novac/kits/kitnovacweb/README.md +1572 -0
  45. package/novac/kits/kitnovacweb/demo.nv +12 -0
  46. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  47. package/novac/kits/kitnovacweb/index.nova +12 -0
  48. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  49. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  50. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  51. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  52. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  53. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  54. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  55. package/novac/kits/kitparse/kitdef.js +1688 -0
  56. package/novac/kits/kitregex++/kitdef.js +1353 -0
  57. package/novac/kits/kitrequire/kitdef.js +1599 -0
  58. package/novac/kits/kitx11/kitdef.js +1 -0
  59. package/novac/kits/kitx11/kitx11.js +2472 -0
  60. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  61. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  62. package/novac/kits/libterm/ex.js +285 -0
  63. package/novac/kits/libterm/kitdef.js +1927 -0
  64. package/novac/node_modules/chalk/license +9 -0
  65. package/novac/node_modules/chalk/package.json +83 -0
  66. package/novac/node_modules/chalk/readme.md +297 -0
  67. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  68. package/novac/node_modules/chalk/source/index.js +225 -0
  69. package/novac/node_modules/chalk/source/utilities.js +33 -0
  70. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  71. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  72. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  73. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  74. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  75. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  76. package/novac/node_modules/commander/LICENSE +22 -0
  77. package/novac/node_modules/commander/Readme.md +1176 -0
  78. package/novac/node_modules/commander/esm.mjs +16 -0
  79. package/novac/node_modules/commander/index.js +24 -0
  80. package/novac/node_modules/commander/lib/argument.js +150 -0
  81. package/novac/node_modules/commander/lib/command.js +2777 -0
  82. package/novac/node_modules/commander/lib/error.js +39 -0
  83. package/novac/node_modules/commander/lib/help.js +747 -0
  84. package/novac/node_modules/commander/lib/option.js +380 -0
  85. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  86. package/novac/node_modules/commander/package-support.json +19 -0
  87. package/novac/node_modules/commander/package.json +82 -0
  88. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  89. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  90. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  91. package/novac/node_modules/node-addon-api/README.md +95 -0
  92. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  93. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  94. package/novac/node_modules/node-addon-api/index.js +14 -0
  95. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  96. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  97. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  98. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  99. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  100. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  101. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  102. package/novac/node_modules/node-addon-api/package.json +480 -0
  103. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  104. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  105. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  106. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  107. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  108. package/novac/node_modules/serialize-javascript/README.md +149 -0
  109. package/novac/node_modules/serialize-javascript/index.js +297 -0
  110. package/novac/node_modules/serialize-javascript/package.json +33 -0
  111. package/novac/package.json +27 -0
  112. package/novac/scripts/update-bin.js +24 -0
  113. package/novac/src/core/bstd.js +1035 -0
  114. package/novac/src/core/config.js +155 -0
  115. package/novac/src/core/describe.js +187 -0
  116. package/novac/src/core/emitter.js +499 -0
  117. package/novac/src/core/error.js +86 -0
  118. package/novac/src/core/executor.js +5606 -0
  119. package/novac/src/core/formatter.js +686 -0
  120. package/novac/src/core/lexer.js +1026 -0
  121. package/novac/src/core/nova_builtins.js +717 -0
  122. package/novac/src/core/nova_thread_worker.js +166 -0
  123. package/novac/src/core/parser.js +2181 -0
  124. package/novac/src/core/types.js +112 -0
  125. package/novac/src/index.js +28 -0
  126. package/novac/src/runtime/stdlib.js +244 -0
  127. package/package.json +3 -2
  128. package/scripts/update-bin.js +0 -0
  129. package/src/core/bstd.js +835 -361
  130. package/src/core/executor.js +427 -246
  131. package/src/core/lexer.js +19 -2
  132. package/src/core/parser.js +13 -0
  133. package/src/index.js +0 -0
  134. package/examples/example-project/README.md +0 -3
  135. package/examples/example-project/src/main.nova +0 -3
  136. package/src/core/environment.js +0 -0
  137. /package/{kits → novac/kits}/libtea/tf.js +0 -0
  138. /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
@@ -0,0 +1,1599 @@
1
+ 'use strict';
2
+
3
+ // ============================================================
4
+ // kitrequires.js — Runtime requirements, guards & assertions
5
+ // Cross-platform. Async-capable. Bulk-report failures.
6
+ // module.exports = { kitdef: { ...all exports } }
7
+ // ============================================================
8
+
9
+ const os = require('os');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { execSync, spawnSync } = require('child_process');
13
+
14
+ // ────────────────────────────────────────────────────────────
15
+ // SECTION 1: RESULT TYPE
16
+ // ────────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Create a passing result.
20
+ * @param {*} [value]
21
+ * @returns {{ ok: true, value: * }}
22
+ */
23
+ function pass(value) { return { ok: true, value }; }
24
+
25
+ /**
26
+ * Create a failing result.
27
+ * @param {string} reason
28
+ * @param {*} [value]
29
+ * @returns {{ ok: false, reason: string, value: * }}
30
+ */
31
+ function fail(reason, value) { return { ok: false, reason, value }; }
32
+
33
+ // ────────────────────────────────────────────────────────────
34
+ // SECTION 2: REQUIRE ENGINE
35
+ // ────────────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * RequireEngine — bulk assertion collector.
39
+ *
40
+ * Usage:
41
+ * const kr = new RequireEngine();
42
+ * await kr.require(
43
+ * kr.admin.isAdmin(),
44
+ * kr.internet.connected(),
45
+ * kr.commands.has('git'),
46
+ * );
47
+ * // throws KitRequireError listing ALL failures if any
48
+ *
49
+ * // Or soft check:
50
+ * const result = await kr.check(kr.node.version('>=18'));
51
+ */
52
+ class RequireEngine {
53
+ constructor(options = {}) {
54
+ this._options = {
55
+ exitOnFail: options.exitOnFail || false,
56
+ exitCode: options.exitCode || 1,
57
+ logger: options.logger || null,
58
+ };
59
+
60
+ // Bind namespaces
61
+ this.admin = _bindNS(adminChecks, this);
62
+ this.os = _bindNS(osChecks, this);
63
+ this.cpu = _bindNS(cpuChecks, this);
64
+ this.memory = _bindNS(memoryChecks, this);
65
+ this.env = _bindNS(envChecks, this);
66
+ this.node = _bindNS(nodeChecks, this);
67
+ this.commands = _bindNS(commandChecks, this);
68
+ this.fs = _bindNS(fsChecks, this);
69
+ this.network = _bindNS(networkChecks, this);
70
+ this.process = _bindNS(processChecks, this);
71
+ this.display = _bindNS(displayChecks, this);
72
+ this.io = new IOHelper(this._options);
73
+ this.tty = _bindNS(ttyChecks, this);
74
+ this.user = _bindNS(userChecks, this);
75
+ this.arch = _bindNS(archChecks, this);
76
+ this.ci = _bindNS(ciChecks, this);
77
+ this.container= _bindNS(containerChecks,this);
78
+ this.vm = _bindNS(vmChecks, this);
79
+ this.time = _bindNS(timeChecks, this);
80
+ this.pkg = _bindNS(pkgChecks, this);
81
+ }
82
+
83
+ /**
84
+ * Assert all given checks pass. Collects ALL failures, then throws.
85
+ * Accepts sync results, async promises, or bare booleans.
86
+ *
87
+ * @param {...(Result|Promise<Result>|boolean)} checks
88
+ * @throws {KitRequireError}
89
+ */
90
+ async require(...checks) {
91
+ const results = await Promise.all(checks.map(c => Promise.resolve(c)));
92
+ const failures = results.filter(r => {
93
+ if (typeof r === 'boolean') return !r;
94
+ return r && r.ok === false;
95
+ }).map(r => (typeof r === 'boolean' ? fail('check returned false') : r));
96
+
97
+ if (failures.length > 0) {
98
+ const err = new KitRequireError(failures);
99
+ if (this._options.exitOnFail) {
100
+ this.io.error(err.message);
101
+ process.exit(this._options.exitCode);
102
+ }
103
+ throw err;
104
+ }
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * Soft check — returns { ok, failures, results } without throwing.
110
+ * @param {...(Result|Promise<Result>|boolean)} checks
111
+ * @returns {Promise<{ ok: boolean, failures: Result[], results: Result[] }>}
112
+ */
113
+ async check(...checks) {
114
+ const results = await Promise.all(checks.map(c => Promise.resolve(c)));
115
+ const normalized = results.map(r => typeof r === 'boolean' ? (r ? pass(r) : fail('check returned false')) : r);
116
+ const failures = normalized.filter(r => !r.ok);
117
+ return { ok: failures.length === 0, failures, results: normalized };
118
+ }
119
+
120
+ /**
121
+ * Assert a single check and throw immediately if it fails.
122
+ * @param {Result|Promise<Result>|boolean} check
123
+ * @param {string} [message]
124
+ */
125
+ async assert(check, message) {
126
+ const r = await Promise.resolve(check);
127
+ const result = typeof r === 'boolean' ? (r ? pass(r) : fail(message || 'assertion failed')) : r;
128
+ if (!result.ok) {
129
+ const err = new KitRequireError([result]);
130
+ if (message) err.message = message;
131
+ throw err;
132
+ }
133
+ return true;
134
+ }
135
+
136
+ /**
137
+ * Guard: run fn only if all checks pass, otherwise run fallback.
138
+ * @param {Array} checks
139
+ * @param {function} fn
140
+ * @param {function} [fallback]
141
+ */
142
+ async guard(checks, fn, fallback) {
143
+ const { ok, failures } = await this.check(...checks);
144
+ if (ok) return fn();
145
+ if (fallback) return fallback(failures);
146
+ }
147
+
148
+ /**
149
+ * isTTY — direct shorthand.
150
+ */
151
+ isTTY() { return ttyChecks.isTTY(); }
152
+ }
153
+
154
+ function _bindNS(ns, engine) {
155
+ const bound = {};
156
+ for (const [k, fn] of Object.entries(ns)) {
157
+ bound[k] = (...args) => fn(...args);
158
+ }
159
+ return bound;
160
+ }
161
+
162
+ // ────────────────────────────────────────────────────────────
163
+ // SECTION 3: ERROR TYPE
164
+ // ────────────────────────────────────────────────────────────
165
+
166
+ class KitRequireError extends Error {
167
+ /**
168
+ * @param {Array<{ok:false, reason:string}>} failures
169
+ */
170
+ constructor(failures) {
171
+ const lines = failures.map((f, i) => ` [${i+1}] ${f.reason}`).join('\n');
172
+ super(`kitrequires: ${failures.length} requirement(s) not met:\n${lines}`);
173
+ this.name = 'KitRequireError';
174
+ this.failures = failures;
175
+ this.count = failures.length;
176
+ }
177
+ }
178
+
179
+ // ────────────────────────────────────────────────────────────
180
+ // SECTION 4: IO HELPER
181
+ // ────────────────────────────────────────────────────────────
182
+
183
+ const ANSI = {
184
+ reset: '\x1b[0m',
185
+ bold: '\x1b[1m',
186
+ red: '\x1b[31m',
187
+ yellow: '\x1b[33m',
188
+ green: '\x1b[32m',
189
+ cyan: '\x1b[36m',
190
+ gray: '\x1b[90m',
191
+ bgRed: '\x1b[41m',
192
+ bgYellow:'\x1b[43m',
193
+ bgGreen: '\x1b[42m',
194
+ };
195
+
196
+ function _color(code, str) {
197
+ if (!process.stdout.isTTY) return str;
198
+ return code + str + ANSI.reset;
199
+ }
200
+
201
+ class IOHelper {
202
+ constructor(opts = {}) { this._opts = opts; }
203
+
204
+ log(msg, ...args) {
205
+ console.log(_color(ANSI.cyan, ' ℹ'), msg, ...args);
206
+ }
207
+
208
+ warn(msg, ...args) {
209
+ console.warn(_color(ANSI.yellow + ANSI.bold, ' ⚠'), _color(ANSI.yellow, msg), ...args);
210
+ }
211
+
212
+ error(msg, ...args) {
213
+ console.error(_color(ANSI.red + ANSI.bold, ' ✖'), _color(ANSI.red, msg), ...args);
214
+ }
215
+
216
+ success(msg, ...args) {
217
+ console.log(_color(ANSI.green + ANSI.bold, ' ✔'), _color(ANSI.green, msg), ...args);
218
+ }
219
+
220
+ /** Print a formatted requirements report */
221
+ report(checkResult) {
222
+ const { ok, results } = checkResult;
223
+ console.log(_color(ANSI.bold, '\n── Requirements Report ──────────────────'));
224
+ for (const r of results) {
225
+ const icon = r.ok
226
+ ? _color(ANSI.green, ' ✔')
227
+ : _color(ANSI.red, ' ✖');
228
+ const msg = r.ok
229
+ ? _color(ANSI.green, String(r.value ?? 'ok'))
230
+ : _color(ANSI.red, r.reason);
231
+ console.log(`${icon} ${msg}`);
232
+ }
233
+ const summary = ok
234
+ ? _color(ANSI.green + ANSI.bold, '\n All requirements met.')
235
+ : _color(ANSI.red + ANSI.bold, `\n ${checkResult.failures.length} requirement(s) failed.`);
236
+ console.log(summary + '\n');
237
+ }
238
+
239
+ /** Prompt user (sync, TTY only) */
240
+ prompt(question) {
241
+ if (!process.stdin.isTTY) return null;
242
+ try {
243
+ const result = spawnSync('node', ['-e',
244
+ `process.stdout.write(${JSON.stringify(question)});
245
+ const buf = Buffer.alloc(256);
246
+ const n = require('fs').readSync(0, buf, 0, 256);
247
+ process.stdout.write(buf.slice(0, n).toString().replace(/\\r?\\n$/, ''));`
248
+ ], { stdio: ['inherit', 'pipe', 'inherit'] });
249
+ return result.stdout ? result.stdout.toString().trim() : null;
250
+ } catch { return null; }
251
+ }
252
+ }
253
+
254
+ // ────────────────────────────────────────────────────────────
255
+ // SECTION 5: ADMIN / PRIVILEGE CHECKS
256
+ // ────────────────────────────────────────────────────────────
257
+
258
+ const adminChecks = {
259
+ /** Check if running as root/Administrator */
260
+ isAdmin() {
261
+ try {
262
+ if (process.platform === 'win32') {
263
+ const r = spawnSync('net', ['session'], { stdio: 'pipe' });
264
+ return r.status === 0 ? pass('Running as Administrator') : fail('Not running as Administrator');
265
+ }
266
+ // Unix: uid 0 = root
267
+ return process.getuid() === 0
268
+ ? pass('Running as root (uid=0)')
269
+ : fail(`Not running as root (uid=${process.getuid()})`);
270
+ } catch {
271
+ return fail('Could not determine admin status');
272
+ }
273
+ },
274
+
275
+ /** Check if running with elevated privileges (broader check) */
276
+ isElevated() {
277
+ return adminChecks.isAdmin();
278
+ },
279
+
280
+ /** Check effective user ID on Unix */
281
+ isRoot() {
282
+ if (process.platform === 'win32') return fail('isRoot is Unix-only');
283
+ try {
284
+ return process.getuid() === 0
285
+ ? pass('uid=0 (root)')
286
+ : fail(`uid=${process.getuid()} (not root)`);
287
+ } catch { return fail('Could not get UID'); }
288
+ },
289
+
290
+ /** Request elevation — launches a new process with sudo/runas */
291
+ request(scriptPath, args = []) {
292
+ scriptPath = scriptPath || process.argv[1];
293
+ try {
294
+ if (process.platform === 'win32') {
295
+ // Windows: use PowerShell Start-Process with RunAs
296
+ const ps = `Start-Process -Verb RunAs -FilePath node -ArgumentList '${scriptPath} ${args.join(' ')}'`;
297
+ spawnSync('powershell', ['-Command', ps], { stdio: 'inherit' });
298
+ } else {
299
+ spawnSync('sudo', ['node', scriptPath, ...args], { stdio: 'inherit' });
300
+ }
301
+ return pass('Elevation requested');
302
+ } catch (e) {
303
+ return fail(`Elevation failed: ${e.message}`);
304
+ }
305
+ },
306
+
307
+ /** Check if sudo is available (Unix) */
308
+ hasSudo() {
309
+ if (process.platform === 'win32') return fail('sudo not applicable on Windows');
310
+ return commandChecks.has('sudo');
311
+ },
312
+ };
313
+
314
+ // ────────────────────────────────────────────────────────────
315
+ // SECTION 6: OS / PLATFORM CHECKS
316
+ // ────────────────────────────────────────────────────────────
317
+
318
+ const osChecks = {
319
+ isWindows() { return process.platform === 'win32' ? pass('Windows') : fail(`Not Windows (${process.platform})`); },
320
+ isLinux() { return process.platform === 'linux' ? pass('Linux') : fail(`Not Linux (${process.platform})`); },
321
+ isMac() { return process.platform === 'darwin' ? pass('macOS') : fail(`Not macOS (${process.platform})`); },
322
+ isAndroid() {
323
+ const r = osChecks.isLinux();
324
+ if (!r.ok) return fail('Not Android');
325
+ try {
326
+ const v = fs.readFileSync('/proc/version', 'utf8');
327
+ return v.toLowerCase().includes('android') ? pass('Android') : fail('Linux but not Android');
328
+ } catch { return fail('Could not detect Android'); }
329
+ },
330
+ isBSD() {
331
+ return ['freebsd','openbsd','netbsd'].includes(process.platform)
332
+ ? pass(process.platform) : fail(`Not BSD (${process.platform})`);
333
+ },
334
+ isFreeBSD() { return process.platform === 'freebsd' ? pass('FreeBSD') : fail(`Not FreeBSD`); },
335
+ isOpenBSD() { return process.platform === 'openbsd' ? pass('OpenBSD') : fail(`Not OpenBSD`); },
336
+ isSunOS() { return process.platform === 'sunos' ? pass('SunOS') : fail(`Not SunOS`); },
337
+ isUnix() {
338
+ return ['linux','darwin','freebsd','openbsd','netbsd','sunos','aix'].includes(process.platform)
339
+ ? pass(process.platform) : fail(`Not Unix (${process.platform})`);
340
+ },
341
+ is64Bit() { return os.arch() === 'x64' || os.arch() === 'arm64' ? pass(os.arch()) : fail(`Not 64-bit (${os.arch()})`); },
342
+ is32Bit() { return os.arch() === 'ia32' || os.arch() === 'arm' ? pass(os.arch()) : fail(`Not 32-bit (${os.arch()})`); },
343
+
344
+ /** Check platform string directly */
345
+ is(platform) {
346
+ return process.platform === platform
347
+ ? pass(platform) : fail(`Platform is '${process.platform}', expected '${platform}'`);
348
+ },
349
+
350
+ /** Get OS release info */
351
+ info() {
352
+ return pass({
353
+ platform: process.platform,
354
+ arch: os.arch(),
355
+ release: os.release(),
356
+ type: os.type(),
357
+ version: os.version ? os.version() : 'unknown',
358
+ hostname: os.hostname(),
359
+ homedir: os.homedir(),
360
+ tmpdir: os.tmpdir(),
361
+ endian: os.endianness(),
362
+ });
363
+ },
364
+
365
+ /** Check OS uptime >= minSeconds */
366
+ uptimeAtLeast(minSeconds) {
367
+ const up = os.uptime();
368
+ return up >= minSeconds
369
+ ? pass(`uptime=${up}s`)
370
+ : fail(`Uptime ${up}s < required ${minSeconds}s`);
371
+ },
372
+
373
+ /** Check kernel release matches regex */
374
+ releaseMatches(pattern) {
375
+ const re = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
376
+ const rel = os.release();
377
+ return re.test(rel) ? pass(rel) : fail(`OS release '${rel}' does not match ${re}`);
378
+ },
379
+ };
380
+
381
+ // ────────────────────────────────────────────────────────────
382
+ // SECTION 7: CPU CHECKS
383
+ // ────────────────────────────────────────────────────────────
384
+
385
+ const cpuChecks = {
386
+ /** Check architecture */
387
+ isArch(arch) {
388
+ const a = os.arch();
389
+ return a === arch ? pass(a) : fail(`CPU arch is '${a}', expected '${arch}'`);
390
+ },
391
+ isX64() { return cpuChecks.isArch('x64'); },
392
+ isX86() { return cpuChecks.isArch('ia32'); },
393
+ isARM() { const a = os.arch(); return a.startsWith('arm') ? pass(a) : fail(`Not ARM (${a})`); },
394
+ isARM64() { return cpuChecks.isArch('arm64'); },
395
+ isRISCV() { return cpuChecks.isArch('riscv64'); },
396
+ isMIPS() { const a = os.arch(); return a.startsWith('mips') ? pass(a) : fail(`Not MIPS (${a})`); },
397
+
398
+ /** Check minimum core count */
399
+ atLeastCores(n) {
400
+ const c = os.cpus().length;
401
+ return c >= n ? pass(`${c} cores`) : fail(`Only ${c} CPU core(s), need ${n}`);
402
+ },
403
+
404
+ /** Check max load average (Unix, 1-min) */
405
+ loadBelow(max) {
406
+ if (process.platform === 'win32') return pass('load average N/A on Windows');
407
+ const load = os.loadavg()[0];
408
+ return load < max ? pass(`load=${load.toFixed(2)}`) : fail(`Load ${load.toFixed(2)} >= ${max}`);
409
+ },
410
+
411
+ /** Get CPU model string */
412
+ model() {
413
+ const cpus = os.cpus();
414
+ return cpus.length ? pass(cpus[0].model) : fail('No CPU info available');
415
+ },
416
+
417
+ /** Endianness */
418
+ isBigEndian() { return os.endianness() === 'BE' ? pass('Big-endian') : fail('Not big-endian (LE)'); },
419
+ isLittleEndian() { return os.endianness() === 'LE' ? pass('Little-endian') : fail('Not little-endian (BE)'); },
420
+ };
421
+
422
+ // ────────────────────────────────────────────────────────────
423
+ // SECTION 8: MEMORY CHECKS
424
+ // ────────────────────────────────────────────────────────────
425
+
426
+ const memoryChecks = {
427
+ /** Check free memory >= bytes */
428
+ freeAtLeast(bytes) {
429
+ const free = os.freemem();
430
+ return free >= bytes
431
+ ? pass(`${_humanBytes(free)} free`)
432
+ : fail(`Only ${_humanBytes(free)} free, need ${_humanBytes(bytes)}`);
433
+ },
434
+
435
+ /** Check free memory >= MB */
436
+ freeMBAtLeast(mb) { return memoryChecks.freeAtLeast(mb * 1024 * 1024); },
437
+
438
+ /** Check total memory >= bytes */
439
+ totalAtLeast(bytes) {
440
+ const total = os.totalmem();
441
+ return total >= bytes
442
+ ? pass(`${_humanBytes(total)} total`)
443
+ : fail(`Total RAM ${_humanBytes(total)}, need ${_humanBytes(bytes)}`);
444
+ },
445
+
446
+ totalGBAtLeast(gb) { return memoryChecks.totalAtLeast(gb * 1024 ** 3); },
447
+
448
+ /** Check free memory percentage */
449
+ freePercentAtLeast(pct) {
450
+ const free = os.freemem(), total = os.totalmem();
451
+ const actual = (free / total) * 100;
452
+ return actual >= pct
453
+ ? pass(`${actual.toFixed(1)}% free`)
454
+ : fail(`Only ${actual.toFixed(1)}% RAM free, need ${pct}%`);
455
+ },
456
+
457
+ /** Get memory info */
458
+ info() {
459
+ const total = os.totalmem(), free = os.freemem();
460
+ return pass({ total, free, used: total-free, freePct: ((free/total)*100).toFixed(1) });
461
+ },
462
+
463
+ /** Node.js heap usage check */
464
+ heapBelow(bytes) {
465
+ const used = process.memoryUsage().heapUsed;
466
+ return used < bytes
467
+ ? pass(`heap=${_humanBytes(used)}`)
468
+ : fail(`Heap usage ${_humanBytes(used)} >= ${_humanBytes(bytes)}`);
469
+ },
470
+
471
+ heapMBBelow(mb) { return memoryChecks.heapBelow(mb * 1024 * 1024); },
472
+ };
473
+
474
+ // ────────────────────────────────────────────────────────────
475
+ // SECTION 9: ENVIRONMENT CHECKS
476
+ // ────────────────────────────────────────────────────────────
477
+
478
+ const envChecks = {
479
+ /** Check env var is set (non-empty) */
480
+ has(name) {
481
+ const val = process.env[name];
482
+ return val !== undefined && val !== ''
483
+ ? pass(`${name}=${val}`)
484
+ : fail(`Environment variable '${name}' is not set`);
485
+ },
486
+
487
+ /** Check env var equals value */
488
+ equals(name, value) {
489
+ const val = process.env[name];
490
+ return val === String(value)
491
+ ? pass(`${name}=${val}`)
492
+ : fail(`${name}='${val}', expected '${value}'`);
493
+ },
494
+
495
+ /** Check env var matches regex */
496
+ matches(name, pattern) {
497
+ const val = process.env[name] || '';
498
+ const re = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
499
+ return re.test(val)
500
+ ? pass(`${name}=${val}`)
501
+ : fail(`${name}='${val}' does not match ${re}`);
502
+ },
503
+
504
+ /** Check env var is NOT set */
505
+ notSet(name) {
506
+ return process.env[name] === undefined
507
+ ? pass(`${name} not set`)
508
+ : fail(`Environment variable '${name}' should not be set (value: '${process.env[name]}')`);
509
+ },
510
+
511
+ /** Check NODE_ENV */
512
+ isProduction() { return envChecks.equals('NODE_ENV', 'production'); },
513
+ isDevelopment() { return envChecks.equals('NODE_ENV', 'development'); },
514
+ isTest() { return envChecks.equals('NODE_ENV', 'test'); },
515
+
516
+ /** Get all env vars matching a prefix */
517
+ withPrefix(prefix) {
518
+ const matches = Object.entries(process.env)
519
+ .filter(([k]) => k.startsWith(prefix))
520
+ .reduce((a,[k,v]) => { a[k]=v; return a; }, {});
521
+ return Object.keys(matches).length > 0
522
+ ? pass(matches)
523
+ : fail(`No environment variables with prefix '${prefix}'`);
524
+ },
525
+
526
+ /** Check PATH includes a directory */
527
+ pathIncludes(dir) {
528
+ const pathDirs = (process.env.PATH || '').split(path.delimiter);
529
+ return pathDirs.some(d => d === dir)
530
+ ? pass(`PATH includes '${dir}'`)
531
+ : fail(`PATH does not include '${dir}'`);
532
+ },
533
+ };
534
+
535
+ // ────────────────────────────────────────────────────────────
536
+ // SECTION 10: NODE.JS CHECKS
537
+ // ────────────────────────────────────────────────────────────
538
+
539
+ const nodeChecks = {
540
+ /** Check Node.js version constraint (semver-like: >=18, ^16, =20) */
541
+ version(constraint) {
542
+ const current = process.versions.node;
543
+ const ok = _semverSatisfies(current, constraint);
544
+ return ok
545
+ ? pass(`Node.js v${current} satisfies ${constraint}`)
546
+ : fail(`Node.js v${current} does not satisfy ${constraint}`);
547
+ },
548
+
549
+ /** Check minimum version */
550
+ atLeast(major, minor = 0, patch = 0) {
551
+ const [cMaj, cMin, cPat] = process.versions.node.split('.').map(Number);
552
+ const ok = cMaj > major || (cMaj === major && cMin > minor) ||
553
+ (cMaj === major && cMin === minor && cPat >= patch);
554
+ return ok
555
+ ? pass(`Node.js v${process.versions.node}`)
556
+ : fail(`Node.js v${process.versions.node} < required v${major}.${minor}.${patch}`);
557
+ },
558
+
559
+ /** Check if worker_threads is available */
560
+ hasWorkerThreads() {
561
+ try { require('worker_threads'); return pass('worker_threads available'); }
562
+ catch { return fail('worker_threads not available'); }
563
+ },
564
+
565
+ /** Check if ESM (import/export) is supported */
566
+ hasESM() {
567
+ const [maj] = process.versions.node.split('.').map(Number);
568
+ return maj >= 12 ? pass('ESM supported') : fail(`ESM requires Node >=12 (have ${process.versions.node})`);
569
+ },
570
+
571
+ /** Check if async/await is available */
572
+ hasAsync() {
573
+ const [maj] = process.versions.node.split('.').map(Number);
574
+ return maj >= 8 ? pass('async/await supported') : fail(`async/await requires Node >=8`);
575
+ },
576
+
577
+ /** Check if a core module is available */
578
+ hasModule(name) {
579
+ try { require(name); return pass(`module '${name}' available`); }
580
+ catch { return fail(`module '${name}' not available`); }
581
+ },
582
+
583
+ /** Check if an npm package is installed */
584
+ hasPackage(name) {
585
+ try { require.resolve(name); return pass(`package '${name}' installed`); }
586
+ catch { return fail(`package '${name}' not installed (run: npm install ${name})`); }
587
+ },
588
+
589
+ /** Check V8 version */
590
+ v8Version() { return pass(process.versions.v8); },
591
+
592
+ /** Check if running in strict mode */
593
+ isStrict() {
594
+ try {
595
+ // In strict mode, arguments.callee throws
596
+ (function() { 'use strict'; (function() { return arguments.callee; })(); })();
597
+ return fail('Not in strict mode');
598
+ } catch {
599
+ return pass('Strict mode active');
600
+ }
601
+ },
602
+
603
+ /** Check if TypeScript types are available (ts-node / tsx) */
604
+ hasTypeScript() {
605
+ return nodeChecks.hasPackage('typescript');
606
+ },
607
+
608
+ /** Check if running under ts-node */
609
+ isTsNode() {
610
+ return process[Symbol.for('ts-node.register.instance')]
611
+ ? pass('Running under ts-node')
612
+ : fail('Not running under ts-node');
613
+ },
614
+
615
+ /** Get all Node.js version info */
616
+ versions() { return pass(process.versions); },
617
+ };
618
+
619
+ // ────────────────────────────────────────────────────────────
620
+ // SECTION 11: COMMAND / BINARY CHECKS
621
+ // ────────────────────────────────────────────────────────────
622
+
623
+ const commandChecks = {
624
+ /** Check if a command exists on PATH */
625
+ has(cmd) {
626
+ try {
627
+ const which = process.platform === 'win32' ? 'where' : 'which';
628
+ const r = spawnSync(which, [cmd], { stdio: 'pipe' });
629
+ return r.status === 0
630
+ ? pass(`'${cmd}' found at ${r.stdout.toString().trim().split('\n')[0]}`)
631
+ : fail(`Command '${cmd}' not found in PATH`);
632
+ } catch {
633
+ return fail(`Could not check for command '${cmd}'`);
634
+ }
635
+ },
636
+
637
+ /** Check multiple commands exist */
638
+ hasAll(...cmds) {
639
+ const results = cmds.map(c => commandChecks.has(c));
640
+ const missing = results.filter(r => !r.ok).map(r => r.reason);
641
+ return missing.length === 0
642
+ ? pass(`All commands found: ${cmds.join(', ')}`)
643
+ : fail(missing.join('; '));
644
+ },
645
+
646
+ /** Check command version output matches constraint */
647
+ version(cmd, versionFlag = '--version', constraint) {
648
+ try {
649
+ const r = spawnSync(cmd, [versionFlag], { stdio: 'pipe' });
650
+ if (r.status !== 0) return fail(`'${cmd}' exited with code ${r.status}`);
651
+ const output = r.stdout.toString().trim();
652
+ if (!constraint) return pass(output);
653
+ const match = output.match(/(\d+\.\d+(?:\.\d+)?)/);
654
+ if (!match) return fail(`Could not parse version from: ${output}`);
655
+ return _semverSatisfies(match[1], constraint)
656
+ ? pass(`${cmd} ${match[1]} satisfies ${constraint}`)
657
+ : fail(`${cmd} ${match[1]} does not satisfy ${constraint}`);
658
+ } catch (e) {
659
+ return fail(`Failed to run '${cmd}': ${e.message}`);
660
+ }
661
+ },
662
+
663
+ /** Check git is available and in a repo */
664
+ inGitRepo() {
665
+ try {
666
+ const r = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { stdio: 'pipe' });
667
+ return r.status === 0 ? pass('Inside a git repository') : fail('Not inside a git repository');
668
+ } catch { return fail('git not available'); }
669
+ },
670
+
671
+ /** Check npm is available */
672
+ hasNpm() { return commandChecks.has('npm'); },
673
+ hasYarn() { return commandChecks.has('yarn'); },
674
+ hasPnpm() { return commandChecks.has('pnpm'); },
675
+ hasGit() { return commandChecks.has('git'); },
676
+ hasCurl() { return commandChecks.has('curl'); },
677
+ hasWget() { return commandChecks.has('wget'); },
678
+ hasMake() { return commandChecks.has('make'); },
679
+ hasPython() {
680
+ const r = commandChecks.has('python3');
681
+ return r.ok ? r : commandChecks.has('python');
682
+ },
683
+ };
684
+
685
+ // ────────────────────────────────────────────────────────────
686
+ // SECTION 12: FILESYSTEM CHECKS
687
+ // ────────────────────────────────────────────────────────────
688
+
689
+ const fsChecks = {
690
+ /** Check path exists */
691
+ exists(p) {
692
+ return fs.existsSync(p) ? pass(`'${p}' exists`) : fail(`Path '${p}' does not exist`);
693
+ },
694
+
695
+ /** Check path is a file */
696
+ isFile(p) {
697
+ try {
698
+ return fs.statSync(p).isFile() ? pass(`'${p}' is a file`) : fail(`'${p}' is not a file`);
699
+ } catch { return fail(`'${p}' does not exist`); }
700
+ },
701
+
702
+ /** Check path is a directory */
703
+ isDir(p) {
704
+ try {
705
+ return fs.statSync(p).isDirectory() ? pass(`'${p}' is a directory`) : fail(`'${p}' is not a directory`);
706
+ } catch { return fail(`'${p}' does not exist`); }
707
+ },
708
+
709
+ /** Check path is readable */
710
+ isReadable(p) {
711
+ try { fs.accessSync(p, fs.constants.R_OK); return pass(`'${p}' is readable`); }
712
+ catch { return fail(`'${p}' is not readable`); }
713
+ },
714
+
715
+ /** Check path is writable */
716
+ isWritable(p) {
717
+ try { fs.accessSync(p, fs.constants.W_OK); return pass(`'${p}' is writable`); }
718
+ catch { return fail(`'${p}' is not writable`); }
719
+ },
720
+
721
+ /** Check path is executable */
722
+ isExecutable(p) {
723
+ try { fs.accessSync(p, fs.constants.X_OK); return pass(`'${p}' is executable`); }
724
+ catch { return fail(`'${p}' is not executable`); }
725
+ },
726
+
727
+ /** Check file size >= bytes */
728
+ sizeAtLeast(p, bytes) {
729
+ try {
730
+ const size = fs.statSync(p).size;
731
+ return size >= bytes ? pass(`${p}: ${_humanBytes(size)}`) : fail(`${p}: ${_humanBytes(size)} < ${_humanBytes(bytes)}`);
732
+ } catch { return fail(`'${p}' does not exist`); }
733
+ },
734
+
735
+ /** Check file is not empty */
736
+ notEmpty(p) { return fsChecks.sizeAtLeast(p, 1); },
737
+
738
+ /** Check disk space free >= bytes on a path's filesystem */
739
+ diskFreeAtLeast(dir, bytes) {
740
+ try {
741
+ if (process.platform === 'win32') {
742
+ const drive = path.parse(path.resolve(dir)).root;
743
+ const r = spawnSync('wmic', ['logicaldisk', 'where', `DeviceID='${drive.slice(0,-1)}'`, 'get', 'FreeSpace'], { stdio: 'pipe' });
744
+ const lines = r.stdout.toString().trim().split('\n');
745
+ const free = parseInt(lines[lines.length-1].trim(), 10);
746
+ return free >= bytes ? pass(`${_humanBytes(free)} free on ${drive}`) : fail(`Only ${_humanBytes(free)} free, need ${_humanBytes(bytes)}`);
747
+ } else {
748
+ const r = spawnSync('df', ['-k', dir], { stdio: 'pipe' });
749
+ const lines = r.stdout.toString().trim().split('\n');
750
+ const cols = lines[1].trim().split(/\s+/);
751
+ const freeKB = parseInt(cols[3], 10);
752
+ const freeBytes = freeKB * 1024;
753
+ return freeBytes >= bytes ? pass(`${_humanBytes(freeBytes)} free on ${dir}`) : fail(`Only ${_humanBytes(freeBytes)} free, need ${_humanBytes(bytes)}`);
754
+ }
755
+ } catch (e) {
756
+ return fail(`Could not check disk space: ${e.message}`);
757
+ }
758
+ },
759
+
760
+ diskFreeMBAtLeast(dir, mb) { return fsChecks.diskFreeAtLeast(dir, mb * 1024 * 1024); },
761
+ diskFreeGBAtLeast(dir, gb) { return fsChecks.diskFreeAtLeast(dir, gb * 1024 ** 3); },
762
+
763
+ /** Check symlink target */
764
+ isSymlink(p) {
765
+ try {
766
+ return fs.lstatSync(p).isSymbolicLink() ? pass(`'${p}' is a symlink`) : fail(`'${p}' is not a symlink`);
767
+ } catch { return fail(`'${p}' does not exist`); }
768
+ },
769
+
770
+ /** Check file modified within last N seconds */
771
+ modifiedWithin(p, seconds) {
772
+ try {
773
+ const stat = fs.statSync(p);
774
+ const age = (Date.now() - stat.mtimeMs) / 1000;
775
+ return age <= seconds
776
+ ? pass(`'${p}' modified ${age.toFixed(0)}s ago`)
777
+ : fail(`'${p}' was modified ${age.toFixed(0)}s ago (limit: ${seconds}s)`);
778
+ } catch { return fail(`'${p}' does not exist`); }
779
+ },
780
+
781
+ /** Check current working directory */
782
+ cwdIs(p) {
783
+ const cwd = process.cwd();
784
+ const resolved = path.resolve(p);
785
+ return cwd === resolved ? pass(`cwd=${cwd}`) : fail(`cwd='${cwd}', expected '${resolved}'`);
786
+ },
787
+ };
788
+
789
+ // ────────────────────────────────────────────────────────────
790
+ // SECTION 13: NETWORK CHECKS (async)
791
+ // ────────────────────────────────────────────────────────────
792
+
793
+ const networkChecks = {
794
+ /** Check internet connectivity by DNS lookup */
795
+ async connected(hostname = 'dns.google') {
796
+ const dns = require('dns').promises;
797
+ try {
798
+ await dns.lookup(hostname);
799
+ return pass(`Internet reachable (resolved ${hostname})`);
800
+ } catch (e) {
801
+ return fail(`No internet connection: ${e.message}`);
802
+ }
803
+ },
804
+
805
+ /** Check DNS resolution for a hostname */
806
+ async resolves(hostname) {
807
+ const dns = require('dns').promises;
808
+ try {
809
+ const result = await dns.lookup(hostname);
810
+ return pass(`'${hostname}' resolves to ${result.address}`);
811
+ } catch (e) {
812
+ return fail(`DNS resolution failed for '${hostname}': ${e.message}`);
813
+ }
814
+ },
815
+
816
+ /** Check if a TCP port is open on a host */
817
+ async portOpen(host, port, timeoutMs = 3000) {
818
+ const net = require('net');
819
+ return new Promise(resolve => {
820
+ const socket = new net.Socket();
821
+ const timer = setTimeout(() => {
822
+ socket.destroy();
823
+ resolve(fail(`Port ${host}:${port} timed out after ${timeoutMs}ms`));
824
+ }, timeoutMs);
825
+ socket.connect(port, host, () => {
826
+ clearTimeout(timer);
827
+ socket.destroy();
828
+ resolve(pass(`Port ${host}:${port} is open`));
829
+ });
830
+ socket.on('error', (e) => {
831
+ clearTimeout(timer);
832
+ resolve(fail(`Port ${host}:${port} is closed: ${e.message}`));
833
+ });
834
+ });
835
+ },
836
+
837
+ /** Check if a local port is free (not in use) */
838
+ async portFree(port) {
839
+ const r = await networkChecks.portOpen('127.0.0.1', port, 500);
840
+ return r.ok
841
+ ? fail(`Port ${port} is in use`)
842
+ : pass(`Port ${port} is free`);
843
+ },
844
+
845
+ /** Check network latency <= maxMs to a host */
846
+ async latencyBelow(host, port, maxMs, timeoutMs = 5000) {
847
+ const net = require('net');
848
+ return new Promise(resolve => {
849
+ const start = Date.now();
850
+ const socket = new net.Socket();
851
+ const timer = setTimeout(() => {
852
+ socket.destroy();
853
+ resolve(fail(`Latency check to ${host}:${port} timed out`));
854
+ }, timeoutMs);
855
+ socket.connect(port, host, () => {
856
+ const ms = Date.now() - start;
857
+ clearTimeout(timer);
858
+ socket.destroy();
859
+ resolve(ms <= maxMs
860
+ ? pass(`Latency to ${host}:${port} = ${ms}ms`)
861
+ : fail(`Latency to ${host}:${port} = ${ms}ms > ${maxMs}ms`));
862
+ });
863
+ socket.on('error', (e) => {
864
+ clearTimeout(timer);
865
+ resolve(fail(`Could not connect to ${host}:${port}: ${e.message}`));
866
+ });
867
+ });
868
+ },
869
+
870
+ /** Check HTTP/HTTPS URL responds with expected status */
871
+ async urlResponds(url, expectedStatus = 200, timeoutMs = 5000) {
872
+ const mod = url.startsWith('https') ? require('https') : require('http');
873
+ return new Promise(resolve => {
874
+ const req = mod.get(url, { timeout: timeoutMs }, (res) => {
875
+ res.resume();
876
+ resolve(res.statusCode === expectedStatus
877
+ ? pass(`${url} → ${res.statusCode}`)
878
+ : fail(`${url} → ${res.statusCode} (expected ${expectedStatus})`));
879
+ });
880
+ req.on('timeout', () => { req.destroy(); resolve(fail(`${url} timed out`)); });
881
+ req.on('error', (e) => resolve(fail(`${url} failed: ${e.message}`)));
882
+ });
883
+ },
884
+
885
+ /** Get all network interfaces */
886
+ interfaces() {
887
+ return pass(os.networkInterfaces());
888
+ },
889
+
890
+ /** Check if any non-loopback network interface is up */
891
+ hasNetworkInterface() {
892
+ const ifaces = os.networkInterfaces();
893
+ const up = Object.values(ifaces).flat().some(i => !i.internal);
894
+ return up ? pass('Network interface available') : fail('No external network interface found');
895
+ },
896
+ };
897
+
898
+ // ────────────────────────────────────────────────────────────
899
+ // SECTION 14: PROCESS CHECKS
900
+ // ────────────────────────────────────────────────────────────
901
+
902
+ const processChecks = {
903
+ /** Check a PID exists */
904
+ pidExists(pid) {
905
+ try {
906
+ process.kill(pid, 0);
907
+ return pass(`PID ${pid} exists`);
908
+ } catch (e) {
909
+ return e.code === 'EPERM'
910
+ ? pass(`PID ${pid} exists (no permission to signal)`)
911
+ : fail(`PID ${pid} does not exist`);
912
+ }
913
+ },
914
+
915
+ /** Check current process PID */
916
+ pid() { return pass(process.pid); },
917
+
918
+ /** Check if a signal is supported */
919
+ supportsSignal(sig) {
920
+ const supported = Object.keys(process.binding ? process.binding('constants').os.signals || {} : {})
921
+ .concat(['SIGTERM','SIGINT','SIGHUP','SIGKILL','SIGABRT','SIGUSR1','SIGUSR2','SIGPIPE','SIGQUIT']);
922
+ const sigName = typeof sig === 'number' ? `SIG${sig}` : sig.toUpperCase();
923
+ return supported.includes(sigName) || (process.platform !== 'win32')
924
+ ? pass(`Signal ${sigName} supported`)
925
+ : fail(`Signal ${sig} not supported on ${process.platform}`);
926
+ },
927
+
928
+ /** Check process uptime >= seconds */
929
+ uptimeAtLeast(seconds) {
930
+ const up = process.uptime();
931
+ return up >= seconds ? pass(`Process uptime ${up.toFixed(1)}s`) : fail(`Uptime ${up.toFixed(1)}s < ${seconds}s`);
932
+ },
933
+
934
+ /** Check exit code of a command */
935
+ commandSucceeds(cmd, args = []) {
936
+ try {
937
+ const r = spawnSync(cmd, args, { stdio: 'pipe' });
938
+ return r.status === 0
939
+ ? pass(`'${cmd}' exited 0`)
940
+ : fail(`'${cmd}' exited ${r.status}: ${(r.stderr||'').toString().trim()}`);
941
+ } catch (e) {
942
+ return fail(`Failed to run '${cmd}': ${e.message}`);
943
+ }
944
+ },
945
+
946
+ /** Check if running in a subprocess (parent is not shell) */
947
+ hasParent() {
948
+ return process.ppid && process.ppid !== process.pid
949
+ ? pass(`Parent PID: ${process.ppid}`)
950
+ : fail('No parent process or parent is self');
951
+ },
952
+
953
+ /** Check if process was started with a specific argv flag */
954
+ hasArgv(flag) {
955
+ return process.argv.includes(flag)
956
+ ? pass(`argv includes '${flag}'`)
957
+ : fail(`argv does not include '${flag}'`);
958
+ },
959
+
960
+ /** Check if running as a daemon (no TTY) */
961
+ isDaemon() {
962
+ return !process.stdin.isTTY && !process.stdout.isTTY
963
+ ? pass('Running as daemon (no TTY)')
964
+ : fail('Not a daemon (TTY detected)');
965
+ },
966
+
967
+ /** Get process memory usage */
968
+ memoryUsage() { return pass(process.memoryUsage()); },
969
+
970
+ /** Check if process can write to stdout */
971
+ hasStdout() {
972
+ return process.stdout.writable ? pass('stdout is writable') : fail('stdout is not writable');
973
+ },
974
+
975
+ /** Check if process has stdin */
976
+ hasStdin() {
977
+ return process.stdin.readable ? pass('stdin is readable') : fail('stdin is not readable');
978
+ },
979
+ };
980
+
981
+ // ────────────────────────────────────────────────────────────
982
+ // SECTION 15: DISPLAY CHECKS
983
+ // ────────────────────────────────────────────────────────────
984
+
985
+ const displayChecks = {
986
+ /** Check if DISPLAY env var is set (X11) */
987
+ hasDisplay() {
988
+ const d = process.env.DISPLAY || process.env.WAYLAND_DISPLAY;
989
+ return d
990
+ ? pass(`Display: ${d}`)
991
+ : fail('No DISPLAY or WAYLAND_DISPLAY set');
992
+ },
993
+
994
+ /** Check if running headless */
995
+ isHeadless() {
996
+ const d = process.env.DISPLAY || process.env.WAYLAND_DISPLAY;
997
+ return !d
998
+ ? pass('Headless (no display server)')
999
+ : fail(`Display server present (${d})`);
1000
+ },
1001
+
1002
+ /** Check X11 specifically */
1003
+ hasX11() {
1004
+ const d = process.env.DISPLAY;
1005
+ return d ? pass(`X11 DISPLAY=${d}`) : fail('DISPLAY not set (no X11)');
1006
+ },
1007
+
1008
+ /** Check Wayland */
1009
+ hasWayland() {
1010
+ const d = process.env.WAYLAND_DISPLAY;
1011
+ return d ? pass(`Wayland WAYLAND_DISPLAY=${d}`) : fail('WAYLAND_DISPLAY not set');
1012
+ },
1013
+
1014
+ /** Check if running in a graphical session (X11 or Wayland) */
1015
+ hasGUI() {
1016
+ const has = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY || process.env.MIR_SOCKET);
1017
+ return has ? pass('GUI session detected') : fail('No GUI session (headless)');
1018
+ },
1019
+
1020
+ /** Check TERM is set */
1021
+ hasTerm() {
1022
+ const t = process.env.TERM;
1023
+ return t ? pass(`TERM=${t}`) : fail('TERM not set');
1024
+ },
1025
+
1026
+ /** Check TERM supports colors */
1027
+ termSupportsColor() {
1028
+ const t = (process.env.TERM || '').toLowerCase();
1029
+ const ct = (process.env.COLORTERM || '').toLowerCase();
1030
+ const ok = t.includes('color') || t.includes('xterm') || ct === 'truecolor' || ct === '24bit' || !!process.env.WT_SESSION;
1031
+ return ok ? pass(`Color terminal: TERM=${t}`) : fail(`Terminal '${t}' may not support colors`);
1032
+ },
1033
+
1034
+ /** Check if running in a GUI app context (Electron / NW.js) */
1035
+ isElectron() {
1036
+ return process.versions && process.versions.electron
1037
+ ? pass(`Electron v${process.versions.electron}`)
1038
+ : fail('Not running in Electron');
1039
+ },
1040
+
1041
+ /** Get display info */
1042
+ info() {
1043
+ return pass({
1044
+ DISPLAY: process.env.DISPLAY,
1045
+ WAYLAND_DISPLAY: process.env.WAYLAND_DISPLAY,
1046
+ TERM: process.env.TERM,
1047
+ COLORTERM: process.env.COLORTERM,
1048
+ isHeadless: !(process.env.DISPLAY || process.env.WAYLAND_DISPLAY),
1049
+ });
1050
+ },
1051
+ };
1052
+
1053
+ // ────────────────────────────────────────────────────────────
1054
+ // SECTION 16: TTY CHECKS
1055
+ // ────────────────────────────────────────────────────────────
1056
+
1057
+ const ttyChecks = {
1058
+ isTTY() {
1059
+ return process.stdout.isTTY
1060
+ ? pass('stdout is a TTY')
1061
+ : fail('stdout is not a TTY (piped or redirected)');
1062
+ },
1063
+ isInteractive() {
1064
+ return process.stdin.isTTY && process.stdout.isTTY
1065
+ ? pass('Interactive terminal')
1066
+ : fail('Not interactive (stdin/stdout not TTY)');
1067
+ },
1068
+ stdinIsTTY() { return process.stdin.isTTY ? pass('stdin is TTY') : fail('stdin is not TTY'); },
1069
+ stderrIsTTY() { return process.stderr.isTTY ? pass('stderr is TTY') : fail('stderr is not TTY'); },
1070
+ termWidth(min = 80) {
1071
+ const cols = process.stdout.columns || 0;
1072
+ return cols >= min ? pass(`Terminal width: ${cols}`) : fail(`Terminal width ${cols} < ${min}`);
1073
+ },
1074
+ termHeight(min = 24) {
1075
+ const rows = process.stdout.rows || 0;
1076
+ return rows >= min ? pass(`Terminal height: ${rows}`) : fail(`Terminal height ${rows} < ${min}`);
1077
+ },
1078
+ info() {
1079
+ return pass({
1080
+ isTTY: !!process.stdout.isTTY,
1081
+ columns: process.stdout.columns,
1082
+ rows: process.stdout.rows,
1083
+ type: process.env.TERM,
1084
+ });
1085
+ },
1086
+ };
1087
+
1088
+ // ────────────────────────────────────────────────────────────
1089
+ // SECTION 17: USER CHECKS
1090
+ // ────────────────────────────────────────────────────────────
1091
+
1092
+ const userChecks = {
1093
+ /** Get current username */
1094
+ username() {
1095
+ try {
1096
+ const info = os.userInfo();
1097
+ return pass(info.username);
1098
+ } catch { return fail('Could not get username'); }
1099
+ },
1100
+
1101
+ /** Check username equals value */
1102
+ is(name) {
1103
+ try {
1104
+ const info = os.userInfo();
1105
+ return info.username === name
1106
+ ? pass(`User is '${name}'`)
1107
+ : fail(`User is '${info.username}', expected '${name}'`);
1108
+ } catch { return fail('Could not get username'); }
1109
+ },
1110
+
1111
+ /** Check UID (Unix) */
1112
+ uid(expected) {
1113
+ if (process.platform === 'win32') return fail('UID not applicable on Windows');
1114
+ try {
1115
+ const uid = process.getuid();
1116
+ return uid === expected ? pass(`UID=${uid}`) : fail(`UID=${uid}, expected ${expected}`);
1117
+ } catch { return fail('Could not get UID'); }
1118
+ },
1119
+
1120
+ /** Check GID (Unix) */
1121
+ gid(expected) {
1122
+ if (process.platform === 'win32') return fail('GID not applicable on Windows');
1123
+ try {
1124
+ const gid = process.getgid();
1125
+ return gid === expected ? pass(`GID=${gid}`) : fail(`GID=${gid}, expected ${expected}`);
1126
+ } catch { return fail('Could not get GID'); }
1127
+ },
1128
+
1129
+ /** Check home directory exists */
1130
+ hasHomeDir() {
1131
+ const home = os.homedir();
1132
+ return fs.existsSync(home) ? pass(`Home: ${home}`) : fail(`Home directory '${home}' does not exist`);
1133
+ },
1134
+
1135
+ /** Check user is in a group (Unix) */
1136
+ inGroup(groupName) {
1137
+ if (process.platform === 'win32') return fail('Group check not supported on Windows');
1138
+ try {
1139
+ const r = spawnSync('id', ['-Gn'], { stdio: 'pipe' });
1140
+ const groups = r.stdout.toString().trim().split(/\s+/);
1141
+ return groups.includes(groupName)
1142
+ ? pass(`User is in group '${groupName}'`)
1143
+ : fail(`User is not in group '${groupName}' (groups: ${groups.join(', ')})`);
1144
+ } catch { return fail('Could not check groups'); }
1145
+ },
1146
+
1147
+ /** Get user info */
1148
+ info() {
1149
+ try {
1150
+ return pass(os.userInfo());
1151
+ } catch { return fail('Could not get user info'); }
1152
+ },
1153
+ };
1154
+
1155
+ // ────────────────────────────────────────────────────────────
1156
+ // SECTION 18: ARCHITECTURE CHECKS
1157
+ // ────────────────────────────────────────────────────────────
1158
+
1159
+ const archChecks = {
1160
+ is(arch) { return cpuChecks.isArch(arch); },
1161
+ isX64() { return cpuChecks.isX64(); },
1162
+ isX86() { return cpuChecks.isX86(); },
1163
+ isARM() { return cpuChecks.isARM(); },
1164
+ isARM64() { return cpuChecks.isARM64(); },
1165
+
1166
+ /** Check process architecture */
1167
+ processArch(expected) {
1168
+ return process.arch === expected
1169
+ ? pass(`process.arch=${process.arch}`)
1170
+ : fail(`process.arch='${process.arch}', expected '${expected}'`);
1171
+ },
1172
+
1173
+ /** Check if running in 64-bit process */
1174
+ is64BitProcess() {
1175
+ return process.arch === 'x64' || process.arch === 'arm64'
1176
+ ? pass(`64-bit process (${process.arch})`)
1177
+ : fail(`Not a 64-bit process (${process.arch})`);
1178
+ },
1179
+ };
1180
+
1181
+ // ────────────────────────────────────────────────────────────
1182
+ // SECTION 19: CI / CD ENVIRONMENT CHECKS
1183
+ // ────────────────────────────────────────────────────────────
1184
+
1185
+ const CI_VARS = {
1186
+ 'CI': 'Generic CI',
1187
+ 'CONTINUOUS_INTEGRATION': 'Generic CI',
1188
+ 'GITHUB_ACTIONS': 'GitHub Actions',
1189
+ 'TRAVIS': 'Travis CI',
1190
+ 'CIRCLECI': 'CircleCI',
1191
+ 'JENKINS_URL': 'Jenkins',
1192
+ 'GITLAB_CI': 'GitLab CI',
1193
+ 'BITBUCKET_BUILD_NUMBER': 'Bitbucket Pipelines',
1194
+ 'TEAMCITY_VERSION': 'TeamCity',
1195
+ 'BUILDKITE': 'Buildkite',
1196
+ 'DRONE': 'Drone CI',
1197
+ 'SEMAPHORE': 'Semaphore CI',
1198
+ 'APPVEYOR': 'AppVeyor',
1199
+ 'BUDDY': 'Buddy CI',
1200
+ 'HEROKU_TEST_RUN_ID':'Heroku CI',
1201
+ 'TF_BUILD': 'Azure DevOps',
1202
+ 'CODEBUILD_BUILD_ID':'AWS CodeBuild',
1203
+ 'GOOGLE_CLOUD_BUILD':'Google Cloud Build',
1204
+ };
1205
+
1206
+ const ciChecks = {
1207
+ /** Check if running in any CI environment */
1208
+ isCI() {
1209
+ for (const [key, name] of Object.entries(CI_VARS)) {
1210
+ if (process.env[key]) return pass(`Running in ${name} (${key}=${process.env[key]})`);
1211
+ }
1212
+ return fail('Not running in a CI environment');
1213
+ },
1214
+
1215
+ /** Check specific CI provider */
1216
+ is(provider) {
1217
+ const entry = Object.entries(CI_VARS).find(([,name]) => name.toLowerCase().includes(provider.toLowerCase()));
1218
+ if (!entry) return fail(`Unknown CI provider: ${provider}`);
1219
+ const [key, name] = entry;
1220
+ return process.env[key]
1221
+ ? pass(`Running in ${name}`)
1222
+ : fail(`Not running in ${name}`);
1223
+ },
1224
+
1225
+ isGitHub() { return ciChecks.is('GitHub'); },
1226
+ isTravis() { return ciChecks.is('Travis'); },
1227
+ isCircle() { return ciChecks.is('CircleCI'); },
1228
+ isJenkins() { return ciChecks.is('Jenkins'); },
1229
+ isGitLab() { return ciChecks.is('GitLab'); },
1230
+ isAzure() { return ciChecks.is('Azure'); },
1231
+
1232
+ /** Detect CI name */
1233
+ name() {
1234
+ for (const [key, name] of Object.entries(CI_VARS)) {
1235
+ if (process.env[key]) return pass(name);
1236
+ }
1237
+ return fail('Not in CI');
1238
+ },
1239
+
1240
+ /** Check if running in a pull request */
1241
+ isPR() {
1242
+ const pr =
1243
+ process.env.GITHUB_EVENT_NAME === 'pull_request' ||
1244
+ !!process.env.TRAVIS_PULL_REQUEST ||
1245
+ !!process.env.CI_PULL_REQUEST ||
1246
+ !!process.env.CIRCLE_PULL_REQUEST ||
1247
+ !!process.env.CHANGE_ID; // Jenkins
1248
+ return pr ? pass('Running in a pull request') : fail('Not running in a pull request');
1249
+ },
1250
+ };
1251
+
1252
+ // ────────────────────────────────────────────────────────────
1253
+ // SECTION 20: CONTAINER / VIRTUALIZATION CHECKS
1254
+ // ────────────────────────────────────────────────────────────
1255
+
1256
+ const containerChecks = {
1257
+ /** Check if running inside Docker */
1258
+ isDocker() {
1259
+ try {
1260
+ if (fs.existsSync('/.dockerenv')) return pass('Running in Docker (/.dockerenv)');
1261
+ const cgroup = fs.readFileSync('/proc/1/cgroup', 'utf8');
1262
+ if (cgroup.includes('docker')) return pass('Running in Docker (cgroup)');
1263
+ return fail('Not in Docker');
1264
+ } catch { return fail('Not in Docker'); }
1265
+ },
1266
+
1267
+ /** Check if running inside a container (Docker, Podman, LXC, etc.) */
1268
+ isContainer() {
1269
+ const dockerResult = containerChecks.isDocker();
1270
+ if (dockerResult.ok) return dockerResult;
1271
+ try {
1272
+ const cgroup = fs.readFileSync('/proc/1/cgroup', 'utf8');
1273
+ if (cgroup.includes('lxc')) return pass('Running in LXC');
1274
+ if (cgroup.includes('kubepods')) return pass('Running in Kubernetes pod');
1275
+ if (cgroup.includes('containerd')) return pass('Running in containerd');
1276
+ if (fs.existsSync('/run/.containerenv')) return pass('Running in Podman');
1277
+ } catch {}
1278
+ return fail('Not in a container');
1279
+ },
1280
+
1281
+ /** Check if running in Kubernetes */
1282
+ isKubernetes() {
1283
+ return process.env.KUBERNETES_SERVICE_HOST
1284
+ ? pass(`Kubernetes (service=${process.env.KUBERNETES_SERVICE_HOST})`)
1285
+ : fail('Not in Kubernetes');
1286
+ },
1287
+
1288
+ /** Check WSL (Windows Subsystem for Linux) */
1289
+ isWSL() {
1290
+ if (process.platform !== 'linux') return fail('Not on Linux');
1291
+ try {
1292
+ const ver = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
1293
+ const r = fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop');
1294
+ return (ver.includes('microsoft') || ver.includes('wsl') || r)
1295
+ ? pass('Running in WSL')
1296
+ : fail('Not in WSL');
1297
+ } catch { return fail('Could not detect WSL'); }
1298
+ },
1299
+
1300
+ /** Check if running in WSL2 specifically */
1301
+ isWSL2() {
1302
+ if (!containerChecks.isWSL().ok) return fail('Not in WSL');
1303
+ try {
1304
+ const ver = fs.readFileSync('/proc/version', 'utf8');
1305
+ return ver.includes('WSL2') || ver.includes('microsoft-standard-WSL2')
1306
+ ? pass('Running in WSL2')
1307
+ : fail('WSL1 (not WSL2)');
1308
+ } catch { return fail('Could not detect WSL version'); }
1309
+ },
1310
+
1311
+ /** Check if running under systemd-nspawn */
1312
+ isNspawn() {
1313
+ try {
1314
+ const env = fs.readFileSync('/proc/1/environ', 'utf8');
1315
+ return env.includes('container=systemd-nspawn')
1316
+ ? pass('Running in systemd-nspawn')
1317
+ : fail('Not in nspawn');
1318
+ } catch { return fail('Not in nspawn'); }
1319
+ },
1320
+ };
1321
+
1322
+ // ────────────────────────────────────────────────────────────
1323
+ // SECTION 21: VM DETECTION
1324
+ // ────────────────────────────────────────────────────────────
1325
+
1326
+ const vmChecks = {
1327
+ /** Check if running in a virtual machine (best-effort) */
1328
+ isVM() {
1329
+ if (process.platform === 'linux') {
1330
+ try {
1331
+ const r = spawnSync('systemd-detect-virt', ['--vm'], { stdio: 'pipe' });
1332
+ if (r.status === 0) {
1333
+ const virt = r.stdout.toString().trim();
1334
+ return virt !== 'none' ? pass(`VM detected: ${virt}`) : fail('Not a VM (systemd-detect-virt)');
1335
+ }
1336
+ } catch {}
1337
+ try {
1338
+ // Check DMI info
1339
+ const dmi = spawnSync('cat', ['/sys/class/dmi/id/product_name'], { stdio: 'pipe' });
1340
+ const name = (dmi.stdout || '').toString().toLowerCase();
1341
+ const vmSignals = ['virtualbox','vmware','kvm','qemu','xen','hyper-v','microsoft corporation'];
1342
+ const found = vmSignals.find(s => name.includes(s));
1343
+ if (found) return pass(`VM detected: ${found}`);
1344
+ } catch {}
1345
+ }
1346
+ if (process.platform === 'win32') {
1347
+ try {
1348
+ const r = spawnSync('wmic', ['computersystem', 'get', 'model'], { stdio: 'pipe' });
1349
+ const model = r.stdout.toString().toLowerCase();
1350
+ if (model.includes('virtual') || model.includes('vmware') || model.includes('hyper-v'))
1351
+ return pass(`VM detected: ${model.trim()}`);
1352
+ } catch {}
1353
+ }
1354
+ return fail('Could not confirm VM environment');
1355
+ },
1356
+
1357
+ isVirtualBox() {
1358
+ return _checkDMI(['virtualbox','innotek']) ? pass('VirtualBox') : fail('Not VirtualBox');
1359
+ },
1360
+ isVMware() {
1361
+ return _checkDMI(['vmware']) ? pass('VMware') : fail('Not VMware');
1362
+ },
1363
+ isKVM() {
1364
+ return _checkDMI(['kvm','qemu']) ? pass('KVM/QEMU') : fail('Not KVM');
1365
+ },
1366
+ isHyperV() {
1367
+ return _checkDMI(['microsoft','hyper-v']) ? pass('Hyper-V') : fail('Not Hyper-V');
1368
+ },
1369
+ isXen() {
1370
+ return _checkDMI(['xen']) ? pass('Xen') : fail('Not Xen');
1371
+ },
1372
+ };
1373
+
1374
+ function _checkDMI(signals) {
1375
+ if (process.platform !== 'linux') return false;
1376
+ try {
1377
+ const files = [
1378
+ '/sys/class/dmi/id/product_name',
1379
+ '/sys/class/dmi/id/sys_vendor',
1380
+ '/sys/class/dmi/id/board_vendor',
1381
+ ];
1382
+ for (const f of files) {
1383
+ try {
1384
+ const content = fs.readFileSync(f, 'utf8').toLowerCase();
1385
+ if (signals.some(s => content.includes(s))) return true;
1386
+ } catch {}
1387
+ }
1388
+ } catch {}
1389
+ return false;
1390
+ }
1391
+
1392
+ // ────────────────────────────────────────────────────────────
1393
+ // SECTION 22: TIME / CLOCK CHECKS
1394
+ // ────────────────────────────────────────────────────────────
1395
+
1396
+ const timeChecks = {
1397
+ /** Check current hour is within range (local time) */
1398
+ hourBetween(from, to) {
1399
+ const h = new Date().getHours();
1400
+ const ok = from <= to ? (h >= from && h < to) : (h >= from || h < to);
1401
+ return ok ? pass(`Hour ${h} is within ${from}–${to}`) : fail(`Hour ${h} is outside ${from}–${to}`);
1402
+ },
1403
+
1404
+ /** Check current day of week (0=Sun, 6=Sat) */
1405
+ isDayOfWeek(day) {
1406
+ const d = new Date().getDay();
1407
+ return d === day ? pass(`Day of week: ${d}`) : fail(`Day ${d} !== ${day}`);
1408
+ },
1409
+
1410
+ /** Check it's a weekday */
1411
+ isWeekday() {
1412
+ const d = new Date().getDay();
1413
+ return d >= 1 && d <= 5 ? pass('Weekday') : fail('Not a weekday');
1414
+ },
1415
+
1416
+ /** Check Unix timestamp is in a range */
1417
+ timestampBetween(from, to) {
1418
+ const now = Date.now();
1419
+ return now >= from && now <= to
1420
+ ? pass(`timestamp ${now} in [${from}, ${to}]`)
1421
+ : fail(`timestamp ${now} outside [${from}, ${to}]`);
1422
+ },
1423
+
1424
+ /** Check system clock is approximately synced (offset from Date.now vs process start) */
1425
+ clockSynced() {
1426
+ // Best-effort: check NTP service is running on Linux
1427
+ if (process.platform === 'linux') {
1428
+ try {
1429
+ const r = spawnSync('timedatectl', ['status'], { stdio: 'pipe' });
1430
+ const out = r.stdout.toString();
1431
+ if (out.includes('synchronized: yes') || out.includes('NTP synchronized: yes'))
1432
+ return pass('Clock is NTP synchronized');
1433
+ if (out.includes('synchronized: no'))
1434
+ return fail('Clock is NOT NTP synchronized');
1435
+ } catch {}
1436
+ }
1437
+ return pass('Clock sync unknown (assume ok)');
1438
+ },
1439
+
1440
+ /** Current timestamp info */
1441
+ now() { return pass({ ts: Date.now(), iso: new Date().toISOString(), uptime: process.uptime() }); },
1442
+ };
1443
+
1444
+ // ────────────────────────────────────────────────────────────
1445
+ // SECTION 23: PACKAGE / LOCK FILE CHECKS
1446
+ // ────────────────────────────────────────────────────────────
1447
+
1448
+ const pkgChecks = {
1449
+ /** Check package.json exists in cwd */
1450
+ hasPackageJson() {
1451
+ const p = path.join(process.cwd(), 'package.json');
1452
+ return fs.existsSync(p) ? pass(`package.json found at ${p}`) : fail(`No package.json in ${process.cwd()}`);
1453
+ },
1454
+
1455
+ /** Check lock file exists */
1456
+ hasLockFile() {
1457
+ const locks = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'shrinkwrap.json'];
1458
+ const found = locks.find(l => fs.existsSync(path.join(process.cwd(), l)));
1459
+ return found ? pass(`Lock file: ${found}`) : fail('No lock file found (npm/yarn/pnpm)');
1460
+ },
1461
+
1462
+ /** Check node_modules exists */
1463
+ hasNodeModules() {
1464
+ const p = path.join(process.cwd(), 'node_modules');
1465
+ return fs.existsSync(p) ? pass(`node_modules found`) : fail(`No node_modules in ${process.cwd()} (run npm install)`);
1466
+ },
1467
+
1468
+ /** Read and return package.json contents */
1469
+ packageJson() {
1470
+ const p = path.join(process.cwd(), 'package.json');
1471
+ try {
1472
+ return pass(JSON.parse(fs.readFileSync(p, 'utf8')));
1473
+ } catch (e) { return fail(`Could not read package.json: ${e.message}`); }
1474
+ },
1475
+
1476
+ /** Check package.json has a specific field */
1477
+ hasField(field) {
1478
+ const r = pkgChecks.packageJson();
1479
+ if (!r.ok) return r;
1480
+ return r.value[field] !== undefined
1481
+ ? pass(`package.json has field '${field}'`)
1482
+ : fail(`package.json missing field '${field}'`);
1483
+ },
1484
+
1485
+ /** Check engines.node constraint */
1486
+ enginesNode() {
1487
+ const r = pkgChecks.packageJson();
1488
+ if (!r.ok) return r;
1489
+ const engines = r.value.engines;
1490
+ if (!engines || !engines.node) return pass('No engines.node constraint');
1491
+ return nodeChecks.version(engines.node);
1492
+ },
1493
+ };
1494
+
1495
+ // ────────────────────────────────────────────────────────────
1496
+ // SECTION 24: SEMVER UTILITIES
1497
+ // ────────────────────────────────────────────────────────────
1498
+
1499
+ /**
1500
+ * Very lightweight semver constraint checker.
1501
+ * Supports: >=, <=, >, <, =, ^, ~, exact
1502
+ */
1503
+ function _semverSatisfies(version, constraint) {
1504
+ const [cMaj, cMin = 0, cPat = 0] = _parseSemver(version);
1505
+ constraint = constraint.trim();
1506
+
1507
+ if (constraint.startsWith('>=')) {
1508
+ const [rMaj, rMin=0, rPat=0] = _parseSemver(constraint.slice(2).trim());
1509
+ return _semverCompare(cMaj,cMin,cPat, rMaj,rMin,rPat) >= 0;
1510
+ }
1511
+ if (constraint.startsWith('<=')) {
1512
+ const [rMaj, rMin=0, rPat=0] = _parseSemver(constraint.slice(2).trim());
1513
+ return _semverCompare(cMaj,cMin,cPat, rMaj,rMin,rPat) <= 0;
1514
+ }
1515
+ if (constraint.startsWith('>')) {
1516
+ const [rMaj, rMin=0, rPat=0] = _parseSemver(constraint.slice(1).trim());
1517
+ return _semverCompare(cMaj,cMin,cPat, rMaj,rMin,rPat) > 0;
1518
+ }
1519
+ if (constraint.startsWith('<')) {
1520
+ const [rMaj, rMin=0, rPat=0] = _parseSemver(constraint.slice(1).trim());
1521
+ return _semverCompare(cMaj,cMin,cPat, rMaj,rMin,rPat) < 0;
1522
+ }
1523
+ if (constraint.startsWith('^')) {
1524
+ // Compatible with: same major, >= minor.patch
1525
+ const [rMaj, rMin=0, rPat=0] = _parseSemver(constraint.slice(1).trim());
1526
+ return cMaj === rMaj && _semverCompare(cMaj,cMin,cPat, rMaj,rMin,rPat) >= 0;
1527
+ }
1528
+ if (constraint.startsWith('~')) {
1529
+ // Approximately: same major.minor, >= patch
1530
+ const [rMaj, rMin=0, rPat=0] = _parseSemver(constraint.slice(1).trim());
1531
+ return cMaj === rMaj && cMin === rMin && cPat >= rPat;
1532
+ }
1533
+ if (constraint.startsWith('=')) {
1534
+ const [rMaj, rMin=0, rPat=0] = _parseSemver(constraint.slice(1).trim());
1535
+ return _semverCompare(cMaj,cMin,cPat, rMaj,rMin,rPat) === 0;
1536
+ }
1537
+ // exact
1538
+ const [rMaj, rMin=0, rPat=0] = _parseSemver(constraint);
1539
+ return _semverCompare(cMaj,cMin,cPat, rMaj,rMin,rPat) === 0;
1540
+ }
1541
+
1542
+ function _parseSemver(s) {
1543
+ return (s.replace(/^v/, '').match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/) || [0,0,0,0])
1544
+ .slice(1).map(n => parseInt(n || '0', 10));
1545
+ }
1546
+
1547
+ function _semverCompare(aMaj,aMin,aPat, bMaj,bMin,bPat) {
1548
+ if (aMaj !== bMaj) return aMaj - bMaj;
1549
+ if (aMin !== bMin) return aMin - bMin;
1550
+ return aPat - bPat;
1551
+ }
1552
+
1553
+ function _humanBytes(bytes) {
1554
+ if (bytes < 1024) return `${bytes}B`;
1555
+ if (bytes < 1024**2) return `${(bytes/1024).toFixed(1)}KB`;
1556
+ if (bytes < 1024**3) return `${(bytes/1024**2).toFixed(1)}MB`;
1557
+ return `${(bytes/1024**3).toFixed(1)}GB`;
1558
+ }
1559
+
1560
+ // ────────────────────────────────────────────────────────────
1561
+ // EXPORTS
1562
+ // ────────────────────────────────────────────────────────────
1563
+
1564
+ module.exports = {
1565
+ kitdef: {
1566
+ // Core types
1567
+ pass, fail,
1568
+ KitRequireError,
1569
+ RequireEngine,
1570
+ IOHelper,
1571
+
1572
+ // Namespace check objects (for use without an engine instance)
1573
+ adminChecks,
1574
+ osChecks,
1575
+ cpuChecks,
1576
+ memoryChecks,
1577
+ envChecks,
1578
+ nodeChecks,
1579
+ commandChecks,
1580
+ fsChecks,
1581
+ networkChecks,
1582
+ processChecks,
1583
+ displayChecks,
1584
+ ttyChecks,
1585
+ userChecks,
1586
+ archChecks,
1587
+ ciChecks,
1588
+ containerChecks,
1589
+ vmChecks,
1590
+ timeChecks,
1591
+ pkgChecks,
1592
+
1593
+ // Semver util
1594
+ semverSatisfies: _semverSatisfies,
1595
+
1596
+ // Factory shorthand
1597
+ create: (opts) => new RequireEngine(opts),
1598
+ }
1599
+ };