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.
- package/LICENSE +1 -1
- package/README.md +0 -0
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/kitdef.js +2 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +3 -2
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +835 -361
- package/src/core/executor.js +427 -246
- package/src/core/lexer.js +19 -2
- package/src/core/parser.js +13 -0
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{kits → novac/kits}/libtea/tf.js +0 -0
- /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
|
+
};
|