engine-kit 0.2.4 → 0.2.5
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/kit/budget.mjs +4 -1
- package/kit/diagnose.mjs +58 -0
- package/kit/doctor.mjs +6 -0
- package/kit/ensure.mjs +1 -0
- package/kit/provision.mjs +34 -59
- package/kit/trust.mjs +19 -0
- package/package.json +2 -2
package/kit/budget.mjs
CHANGED
|
@@ -41,9 +41,12 @@ const ROOT = resolvePath(KIT, "..");
|
|
|
41
41
|
// e update.mjs (`npx engine-kit ensure <motor>` com autoatualização exigida pelo gate de release), +6.3 KB.
|
|
42
42
|
// Escopo novo (comando de instalação), rodado pela mão de quem instala, nunca importado por consumidor:
|
|
43
43
|
// o caminho do `import "engine-kit"` não mudou.
|
|
44
|
+
// 2026-11-09 tooling 31000 → 34000: diagnóstico de vida (pid, porta, idade do runtime) e do ponteiro
|
|
45
|
+
// `cli.json` no doctor (diagnose.mjs), +2.7 KB. Só leitura, rodado na mão de quem diagnostica; o caminho do
|
|
46
|
+
// `import "engine-kit"` não mudou.
|
|
44
47
|
const BUDGETS = {
|
|
45
48
|
runtime: { codeBytes: 27000, baseline: 24519, loc: 900 },
|
|
46
|
-
tooling: { codeBytes:
|
|
49
|
+
tooling: { codeBytes: 34000, baseline: 16420, loc: 950 },
|
|
47
50
|
adapter: { codeBytes: 5000, baseline: 1859, loc: 150 },
|
|
48
51
|
};
|
|
49
52
|
|
package/kit/diagnose.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// engine-kit/diagnose.mjs — diagnóstico SOMENTE LEITURA de vida e de ponteiro, usado pelo doctor.
|
|
2
|
+
// Não sobe, não derruba e não fala com o motor: lê o runtime que o motor anuncia e o `cli.json`
|
|
3
|
+
// que ele publica (CONTRATO-MOTOR §4). Nunca lança.
|
|
4
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
6
|
+
import { readRuntime } from "./lifecycle.mjs";
|
|
7
|
+
|
|
8
|
+
const alive = (pid) => {
|
|
9
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e.code === "EPERM"; }
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Vida do motor segundo o arquivo de runtime que ele declara: pid vivo, porta e idade do heartbeat.
|
|
14
|
+
* Estados: SEM-DECLARAÇÃO (o registry não declara arquivo de runtime), PARADO (sem runtime),
|
|
15
|
+
* VIVO (pid existe), ÓRFÃO (runtime aponta para pid que não existe mais).
|
|
16
|
+
* @returns {{state:string, detail:string}}
|
|
17
|
+
*/
|
|
18
|
+
export function lifeOf(engine, now = Date.now()) {
|
|
19
|
+
const rt = engine.runtime;
|
|
20
|
+
if (!rt?.runtimeFile) { return { state: "SEM-DECLARAÇÃO", detail: rt?.transport ? `transporte ${rt.transport}, sem arquivo de runtime` : "motor sem bloco runtime" }; }
|
|
21
|
+
const data = readRuntime(engine);
|
|
22
|
+
if (!data) { return { state: "PARADO", detail: `sem ${rt.runtimeFile}` }; }
|
|
23
|
+
let age = "";
|
|
24
|
+
try {
|
|
25
|
+
const ms = now - statSync(join(engine.homeDir, rt.runtimeFile)).mtimeMs;
|
|
26
|
+
const limit = rt.heartbeatMs ? ` (limite ${Math.round((rt.heartbeatMs * 3) / 1000)}s)` : "";
|
|
27
|
+
age = `, runtime atualizado há ${Math.round(ms / 1000)}s${limit}`;
|
|
28
|
+
} catch { /* o arquivo sumiu entre a leitura e o stat */ }
|
|
29
|
+
const where = `pid ${data.pid ?? "?"}${data.port ? `, porta ${data.port}` : ""}${age}`;
|
|
30
|
+
return data.pid && alive(data.pid)
|
|
31
|
+
? { state: "VIVO", detail: where }
|
|
32
|
+
: { state: "ÓRFÃO", detail: `${where}; o pid não existe mais` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ponteiro `<home>/cli.json`: todo caminho absoluto que o motor publica precisa existir, e o kit
|
|
37
|
+
* avisa quando o ponteiro vive fora da instalação que ele provisionou.
|
|
38
|
+
* Estados: AUSENTE, ILEGÍVEL, QUEBRADO (algum caminho não existe), FORA-DA-INSTALAÇÃO, OK.
|
|
39
|
+
* @returns {{state:string, detail:string}}
|
|
40
|
+
*/
|
|
41
|
+
export function pointerOf(engine) {
|
|
42
|
+
const file = join(engine.homeDir, "cli.json");
|
|
43
|
+
if (!existsSync(file)) { return { state: "AUSENTE", detail: "o motor ainda não publicou o ponteiro" }; }
|
|
44
|
+
let data;
|
|
45
|
+
try { data = JSON.parse(readFileSync(file, "utf8")); }
|
|
46
|
+
catch { return { state: "ILEGÍVEL", detail: `${file} não é JSON válido` }; }
|
|
47
|
+
const paths = Object.entries(data).filter(([, v]) => typeof v === "string" && isAbsolute(v));
|
|
48
|
+
const missing = paths.filter(([, v]) => !existsSync(v)).map(([k]) => k);
|
|
49
|
+
if (missing.length) { return { state: "QUEBRADO", detail: `aponta para caminho inexistente: ${missing.join(", ")}` }; }
|
|
50
|
+
const binDir = join(engine.homeDir, engine.install.extractTo ?? "bin");
|
|
51
|
+
const outside = paths.filter(([k, v]) => {
|
|
52
|
+
const rel = relative(binDir, v);
|
|
53
|
+
return k !== "node" && (rel.startsWith("..") || isAbsolute(rel));
|
|
54
|
+
});
|
|
55
|
+
return outside.length
|
|
56
|
+
? { state: "FORA-DA-INSTALAÇÃO", detail: `${outside.map(([k]) => k).join(", ")} fora de ${binDir}` }
|
|
57
|
+
: { state: "OK", detail: `${paths.length} caminho(s) existem` };
|
|
58
|
+
}
|
package/kit/doctor.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { join } from "node:path";
|
|
|
17
17
|
import { resolve } from "./resolve.mjs";
|
|
18
18
|
import { provision, satisfiesPin } from "./provision.mjs";
|
|
19
19
|
import { shutdown } from "./lifecycle.mjs";
|
|
20
|
+
import { lifeOf, pointerOf } from "./diagnose.mjs";
|
|
20
21
|
|
|
21
22
|
const args = process.argv.slice(2);
|
|
22
23
|
const useLocal = args.includes("--local");
|
|
@@ -85,6 +86,11 @@ const w = Math.max(12, ...rows.map((r) => r.name.length));
|
|
|
85
86
|
for (const r of rows) {
|
|
86
87
|
const mark = (r.state === "OK" || r.state === "SELF-MANAGED") ? " OK " : r.state === "ERRO" ? " ERRO " : " !! ";
|
|
87
88
|
console.log(`${mark} ${pad(r.name, w)} ${pad(r.state, 14)} ${r.detail}`);
|
|
89
|
+
if (r.engine && r.engine.status !== "self-managed") {
|
|
90
|
+
const life = lifeOf(r.engine), ptr = pointerOf(r.engine);
|
|
91
|
+
console.log(` ${pad("", w)} vida: ${life.state} (${life.detail})`);
|
|
92
|
+
console.log(` ${pad("", w)} ponteiro cli.json: ${ptr.state} (${ptr.detail})`);
|
|
93
|
+
}
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
const pending = rows.filter((r) => r.needsWork);
|
package/kit/ensure.mjs
CHANGED
|
@@ -63,6 +63,7 @@ export async function ensureCommand(name, { version, registryUrl, bundled, manif
|
|
|
63
63
|
const p = await provision(r.engine, {
|
|
64
64
|
log,
|
|
65
65
|
trustedKeys: { [name]: publicKey },
|
|
66
|
+
allowDowngrade: Boolean(version),
|
|
66
67
|
...(fetcher ? { fetcher } : {}),
|
|
67
68
|
// Windows: arquivo em uso não se sobrescreve. Derruba o daemon pelo shutdownPath que o registry declara.
|
|
68
69
|
onBeforeReplace: async () => {
|
package/kit/provision.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { join, dirname } from "node:path";
|
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { verifyBlob, DEFAULT_ALGORITHM } from "./signature.mjs";
|
|
15
|
-
import { checkTrust, trustPath } from "./trust.mjs";
|
|
15
|
+
import { checkTrust, trustPath, cmpVer, rollbackReason, noteVersion } from "./trust.mjs";
|
|
16
16
|
|
|
17
17
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
18
18
|
|
|
@@ -77,22 +77,29 @@ function claimLock(lock) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const parseVer = (v) => String(v || "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
81
|
-
|
|
82
80
|
/** true se a versão instalada serve o pin: MESMO major e >= (protocolo é atado ao major). */
|
|
83
|
-
export
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
export const satisfiesPin = (installed, pinned) => String(installed).split(".")[0] === String(pinned).split(".")[0] && cmpVer(installed, pinned) >= 0;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Baixa o artefato e confere o sidecar SHA256 (obrigatório, fail-closed) em memória.
|
|
85
|
+
* @returns {Promise<{buf:Buffer} | {reason:string}>}
|
|
86
|
+
*/
|
|
87
|
+
async function downloadChecked(engine, { log, fetcher, timeoutMs }) {
|
|
88
|
+
let expected;
|
|
89
|
+
try {
|
|
90
|
+
expected = String(await fetcher(engine.checksumUrl, 15000)).trim().split(/\s+/)[0].toLowerCase();
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { reason: `${engine.name}: sha256 sidecar inacessível (${e.status || e.message}) → ABORT (fail-closed)` };
|
|
90
93
|
}
|
|
91
|
-
return
|
|
92
|
-
}
|
|
94
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) { return { reason: `${engine.name}: sha256 sidecar malformado → ABORT` }; }
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
log(`[engine-kit] baixando ${engine.assetUrl} …`);
|
|
97
|
+
let buf;
|
|
98
|
+
try { buf = await fetcher(engine.assetUrl, timeoutMs); }
|
|
99
|
+
catch (e) { return { reason: `${engine.name}: download falhou (${e.status || e.message})` }; }
|
|
100
|
+
|
|
101
|
+
const actual = createHash("sha256").update(buf).digest("hex").toLowerCase();
|
|
102
|
+
return actual === expected ? { buf } : { reason: `${engine.name}: SHA256 mismatch (esperado ${expected.slice(0, 12)}…, obtido ${actual.slice(0, 12)}…) → ABORT` };
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
async function fetchBuf(url, timeoutMs) {
|
|
@@ -183,26 +190,9 @@ export async function fetchArtifact(engine, { log = () => {}, destDir, downloadT
|
|
|
183
190
|
return { ok: false, reason: `${engine.name}: não consegui criar ${dir}: ${e?.message || e}` };
|
|
184
191
|
}
|
|
185
192
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
expected = String(buf).trim().split(/\s+/)[0].toLowerCase();
|
|
190
|
-
} catch (e) {
|
|
191
|
-
return { ok: false, reason: `${engine.name}: sha256 sidecar inacessível (${e.status || e.message}) → ABORT (fail-closed)` };
|
|
192
|
-
}
|
|
193
|
-
if (!/^[0-9a-f]{64}$/.test(expected)) {
|
|
194
|
-
return { ok: false, reason: `${engine.name}: sha256 sidecar malformado → ABORT` };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
log(`[engine-kit] baixando ${engine.assetUrl} …`);
|
|
198
|
-
let buf;
|
|
199
|
-
try { buf = await fetcher(engine.assetUrl, downloadTimeoutMs); }
|
|
200
|
-
catch (e) { return { ok: false, reason: `${engine.name}: download falhou (${e.status || e.message})` }; }
|
|
201
|
-
|
|
202
|
-
const actual = createHash("sha256").update(buf).digest("hex").toLowerCase();
|
|
203
|
-
if (actual !== expected) {
|
|
204
|
-
return { ok: false, reason: `${engine.name}: SHA256 mismatch (esperado ${expected.slice(0, 12)}…, obtido ${actual.slice(0, 12)}…) → ABORT` };
|
|
205
|
-
}
|
|
193
|
+
const dl = await downloadChecked(engine, { log, fetcher, timeoutMs: downloadTimeoutMs });
|
|
194
|
+
if (!dl.buf) { return { ok: false, reason: dl.reason }; }
|
|
195
|
+
const buf = dl.buf;
|
|
206
196
|
|
|
207
197
|
const auth = await verifyAuthenticity(engine, buf, { log, fetcher, trustedKeys });
|
|
208
198
|
if (!auth.ok) { return { ok: false, reason: auth.reason }; }
|
|
@@ -225,7 +215,7 @@ export async function fetchArtifact(engine, { log = () => {}, destDir, downloadT
|
|
|
225
215
|
* adivinha como derrubar um daemon: quem sabe é o descritor/consumidor).
|
|
226
216
|
* @returns {Promise<{ok:true, entryPath:string, version:string, reused?:boolean, installed?:boolean} | {ok:false, reason:string}>}
|
|
227
217
|
*/
|
|
228
|
-
export async function provision(engine, { log = () => {}, allowNetwork = true, onBeforeReplace, downloadTimeoutMs = 120000, fetcher = fetchBuf, trustedKeys } = {}) {
|
|
218
|
+
export async function provision(engine, { log = () => {}, allowNetwork = true, onBeforeReplace, downloadTimeoutMs = 120000, fetcher = fetchBuf, trustedKeys, allowDowngrade } = {}) {
|
|
229
219
|
const home = engine.homeDir;
|
|
230
220
|
const binDir = join(home, engine.install.extractTo ?? "bin");
|
|
231
221
|
const entryPath = join(home, engine.install.entry);
|
|
@@ -239,6 +229,7 @@ export async function provision(engine, { log = () => {}, allowNetwork = true, o
|
|
|
239
229
|
if (existsSync(entryPath)) {
|
|
240
230
|
const inst = installedVersion(binDir);
|
|
241
231
|
if (inst && satisfiesPin(inst, engine.version)) {
|
|
232
|
+
noteVersion(engine.name, inst);
|
|
242
233
|
return { ok: true, entryPath, version: inst, reused: true };
|
|
243
234
|
}
|
|
244
235
|
if (!inst && engine.status !== "self-managed") {
|
|
@@ -266,6 +257,9 @@ export async function provision(engine, { log = () => {}, allowNetwork = true, o
|
|
|
266
257
|
return { ok: false, reason: `${engine.name} ainda não publicou release (status: pending-release)` };
|
|
267
258
|
}
|
|
268
259
|
|
|
260
|
+
const rb = !allowDowngrade && rollbackReason(engine.name, engine.version);
|
|
261
|
+
if (rb) { return { ok: false, reason: rb }; }
|
|
262
|
+
|
|
269
263
|
mkdirSync(home, { recursive: true });
|
|
270
264
|
|
|
271
265
|
// LOCK atômico: só um consumidor provisiona; os demais aguardam o resultado dele.
|
|
@@ -284,35 +278,15 @@ export async function provision(engine, { log = () => {}, allowNetwork = true, o
|
|
|
284
278
|
}
|
|
285
279
|
|
|
286
280
|
try {
|
|
287
|
-
// 1) SHA256
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
expected = String(buf).trim().split(/\s+/)[0].toLowerCase();
|
|
292
|
-
} catch (e) {
|
|
293
|
-
return { ok: false, reason: `${engine.name}: sha256 sidecar inacessível (${e.status || e.message}) → ABORT (fail-closed)` };
|
|
294
|
-
}
|
|
295
|
-
if (!/^[0-9a-f]{64}$/.test(expected)) {
|
|
296
|
-
return { ok: false, reason: `${engine.name}: sha256 sidecar malformado → ABORT` };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// 2) Download → .part → rename atômico.
|
|
300
|
-
log(`[engine-kit] baixando ${engine.assetUrl} …`);
|
|
301
|
-
let buf;
|
|
302
|
-
try { buf = await fetcher(engine.assetUrl, downloadTimeoutMs); }
|
|
303
|
-
catch (e) { return { ok: false, reason: `${engine.name}: download falhou (${e.status || e.message})` }; }
|
|
281
|
+
// 1-3) Sidecar SHA256 obrigatório, download e integridade: nada toca o disco antes de conferir.
|
|
282
|
+
const dl = await downloadChecked(engine, { log, fetcher, timeoutMs: downloadTimeoutMs });
|
|
283
|
+
if (!dl.buf) { return { ok: false, reason: dl.reason }; }
|
|
284
|
+
const buf = dl.buf;
|
|
304
285
|
const part = join(home, `${engine.assetName}.part`);
|
|
305
286
|
const tgz = join(home, engine.assetName);
|
|
306
287
|
writeFileSync(part, buf);
|
|
307
288
|
renameSync(part, tgz);
|
|
308
289
|
|
|
309
|
-
// 3) Integridade (mismatch = apaga + ABORT).
|
|
310
|
-
const actual = sha256File(tgz);
|
|
311
|
-
if (actual !== expected) {
|
|
312
|
-
dropFile(tgz);
|
|
313
|
-
return { ok: false, reason: `${engine.name}: SHA256 mismatch (esperado ${expected.slice(0, 12)}…, obtido ${actual.slice(0, 12)}…) → ABORT` };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
290
|
// 3.5) AUTENTICIDADE — antes de extrair. Um artefato não confiável não chega a tocar o disco
|
|
317
291
|
// final: se a assinatura não confere, o .tgz é apagado e nada é instalado.
|
|
318
292
|
const auth = await verifyAuthenticity(engine, buf, { log, fetcher, trustedKeys });
|
|
@@ -373,6 +347,7 @@ export async function provision(engine, { log = () => {}, allowNetwork = true, o
|
|
|
373
347
|
if (!existsSync(entryPath)) {
|
|
374
348
|
return { ok: false, reason: `${engine.name}: artefato extraído incompleto (sem ${engine.install.entry})` };
|
|
375
349
|
}
|
|
350
|
+
noteVersion(engine.name, engine.version);
|
|
376
351
|
log(`[engine-kit] ${engine.name} instalado em ${binDir} (v${engine.version})`);
|
|
377
352
|
return { ok: true, entryPath, version: engine.version, installed: true, signed: auth.signed };
|
|
378
353
|
} catch (e) {
|
package/kit/trust.mjs
CHANGED
|
@@ -70,3 +70,22 @@ export function checkTrust(engineName, publicKey, { trustedKeys } = {}) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
export const trustPath = () => trustFile();
|
|
73
|
+
|
|
74
|
+
/** Compara versões `x.y.z` numericamente: negativo, zero ou positivo. */
|
|
75
|
+
export const cmpVer = (a, b) => String(a).localeCompare(String(b), "en", { numeric: true });
|
|
76
|
+
|
|
77
|
+
/** Motivo do ABORT se `version` é menor que a maior já instalada do motor (anti-rollback); falsy se pode seguir. */
|
|
78
|
+
export function rollbackReason(name, version) {
|
|
79
|
+
const max = readTrust()[name]?.maxVersion;
|
|
80
|
+
return max && cmpVer(version, max) < 0 && `${name}: v${version} < v${max} já instalada → ABORT (rollback; downgrade só com --version)`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Grava `version` como a maior vista do motor, se superar a registrada. */
|
|
84
|
+
export function noteVersion(name, version) {
|
|
85
|
+
const store = readTrust(), max = store[name]?.maxVersion;
|
|
86
|
+
if (max && cmpVer(version, max) <= 0) return;
|
|
87
|
+
try {
|
|
88
|
+
mkdirSync(trustDir(), { recursive: true });
|
|
89
|
+
writeFileSync(trustFile(), JSON.stringify({ ...store, [name]: { ...store[name], maxVersion: version } }, null, 2));
|
|
90
|
+
} catch { /* sem gravar, a proteção vale a partir da próxima instalação */ }
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engine-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Cliente das dependências compartilhadas (motores) do engine-registry: resolve, provisiona com SHA256 e assinatura Ed25519 fail-closed, e mantém o motor vivo — uma instalação por máquina, N consumidores.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"node": ">=18"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"test": "node kit/budget.mjs && node tests/test-silencio.mjs && node tests/smoke.mjs && node tests/test-edge.mjs && node tests/test-signature.mjs && node tests/test-ensure.mjs && node tests/test-ensure-daemon.mjs && node tests/test-update.mjs && node tests/test-trust.mjs && node tests/test-tar.mjs && node tests/test-apps.mjs && node tests/test-resolve-cache.mjs && node tests/test-doctor-exit.mjs && node tests/test-mcp-gateway-token.mjs && node tests/test-gateway-provision.mjs && node tests/test-neural-link-alignment.mjs && node tests/test-site.mjs && node tests/test-pages-branch.mjs && node tests/test-readme-versions.mjs && node tests/test-pages-gate.mjs && node tests/test-regras-escopo.mjs",
|
|
27
|
+
"test": "node kit/budget.mjs && node tests/test-silencio.mjs && node tests/smoke.mjs && node tests/test-edge.mjs && node tests/test-signature.mjs && node tests/test-ensure.mjs && node tests/test-ensure-daemon.mjs && node tests/test-update.mjs && node tests/test-trust.mjs && node tests/test-rollback.mjs && node tests/test-diagnose.mjs && node tests/test-tar.mjs && node tests/test-apps.mjs && node tests/test-resolve-cache.mjs && node tests/test-doctor-exit.mjs && node tests/test-mcp-gateway-token.mjs && node tests/test-gateway-provision.mjs && node tests/test-neural-link-alignment.mjs && node tests/test-site.mjs && node tests/test-pages-branch.mjs && node tests/test-readme-versions.mjs && node tests/test-pages-gate.mjs && node tests/test-regras-escopo.mjs",
|
|
28
28
|
"test:pages-live": "node tests/test-pages-gate.mjs --live",
|
|
29
29
|
"test:network": "node tests/smoke.mjs --network && node tests/test-signature.mjs --network",
|
|
30
30
|
"test:e2e": "node tests/e2e-install.mjs",
|