@rigour-labs/core 6.7.1 → 6.7.2
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/README.md +1 -1
- package/dist/review/review.js +19 -4
- package/dist/review/review.test.js +11 -0
- package/dist/services/temporal-drift.js +1 -1
- package/dist/storage/db.d.ts +4 -6
- package/dist/storage/db.js +63 -90
- package/dist/storage/db.test.d.ts +1 -0
- package/dist/storage/db.test.js +46 -0
- package/package.json +8 -9
package/README.md
CHANGED
|
@@ -23,6 +23,6 @@ console.log(result.status, result.findings);
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
What each check finds and whether it blocks: [What Rigour checks](https://github.com/rigour-labs/rigour/blob/main/docs/CHECKS.md). Every setting:
|
|
26
|
-
[Configuration reference](https://github.com/rigour-labs/rigour/blob/main/docs/CONFIG_REFERENCE.md). Node 22 or later.
|
|
26
|
+
[Configuration reference](https://github.com/rigour-labs/rigour/blob/main/docs/CONFIG_REFERENCE.md). Node 22.13 or later.
|
|
27
27
|
|
|
28
28
|
MIT © [Rigour Labs](https://github.com/rigour-labs)
|
package/dist/review/review.js
CHANGED
|
@@ -43,13 +43,28 @@ export async function reviewChange(input) {
|
|
|
43
43
|
const preexisting = await dropPreexisting(input, report, targets);
|
|
44
44
|
if (input.diffTests && deep)
|
|
45
45
|
report.failures.push(...await diffTestFailures(input.cwd, input.source, deep));
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
// The review's own checks, each recorded in the summary beside the gates, so a report says everything that ran.
|
|
47
|
+
const reviewCheck = (id, key, failures) => {
|
|
48
|
+
const enabled = input.config.gates[key]?.enabled;
|
|
49
|
+
report.summary[id] = !enabled ? 'SKIP' : failures.length ? 'FAIL' : 'PASS';
|
|
50
|
+
report.failures.push(...failures);
|
|
51
|
+
};
|
|
52
|
+
reviewCheck('migration-order', 'migration_order', migrationOrderFailures(input.cwd, diff, input.source, input.config));
|
|
53
|
+
reviewCheck('unused-exports', 'unused_exports', unusedExportFailures(input.cwd, diff, input.config));
|
|
54
|
+
reviewCheck('orphan-files', 'orphan_files', orphanFileFailures(input.cwd, diff, input.config));
|
|
55
|
+
reviewCheck('query-patterns', 'query_patterns', queryPatternFailures(input.cwd, changedLines, input.config));
|
|
56
|
+
reviewCheck('optional-params', 'optional_params', optionalParamFailures(input.cwd, changedLines, input.config));
|
|
57
|
+
reviewCheck('duplicate-functions', 'duplicate_functions', duplicateFunctionFailures(input.cwd, changedLines, input.config));
|
|
58
|
+
reviewCheck('change-sweep', 'change_sweep', [
|
|
59
|
+
...loopCopyFailures(input.cwd, changedLines, input.config),
|
|
60
|
+
...partialFixFailures(input.cwd, changedLines, input.config),
|
|
61
|
+
...partialWiringFailures(input.cwd, changedLines, input.config),
|
|
62
|
+
]);
|
|
49
63
|
const typed = input.typed ? typedChecks(input.cwd, changedLines, input.config) : { failures: [], hints: [] };
|
|
50
|
-
report.failures.push(...typed.failures);
|
|
51
64
|
if (typed.error)
|
|
52
65
|
report.summary[TYPED_CHECKS] = 'ERROR'; // a check that could not run is a crashed gate, never a pass
|
|
66
|
+
else if (input.typed)
|
|
67
|
+
reviewCheck('redundancy', 'redundancy', typed.failures);
|
|
53
68
|
const split = splitByChangedLines(report.failures, changedLines, deep ? changedFunctionSpans(input.cwd, changedLines) : {}, removedByFile(diff));
|
|
54
69
|
const deepError = deepAnalysisError(report);
|
|
55
70
|
const quiet = quietSplit(input.cwd, split.findings, input.config.review?.include_heuristics, input.trustedRef);
|
|
@@ -117,6 +117,17 @@ describe('git-backed review', () => {
|
|
|
117
117
|
expect(semantic.map(f => [f.files?.[0], f.line])).toEqual([['src/notify.ts', 2]]);
|
|
118
118
|
expect(toReviewFinding(semantic[0])).toMatchObject({ id: 'semantic-bugs', file: 'src/notify.ts', line: 2, severity: 'high' });
|
|
119
119
|
});
|
|
120
|
+
it("names every check it ran in the summary, the review's own beside the gates", async () => {
|
|
121
|
+
write('src/a.ts', 'export const a = 1;\n');
|
|
122
|
+
git('add', '-A');
|
|
123
|
+
git('commit', '-qm', 'init');
|
|
124
|
+
write('src/b.ts', 'export const unused = 2;\n');
|
|
125
|
+
git('add', '-A');
|
|
126
|
+
const config = ConfigSchema.parse({ gates: { migration_order: { enabled: false } } });
|
|
127
|
+
const { report } = await reviewChange({ cwd: repo, config });
|
|
128
|
+
expect(report?.summary).toMatchObject({ 'unused-exports': 'FAIL', 'migration-order': 'SKIP', 'query-patterns': 'PASS', 'change-sweep': 'PASS' });
|
|
129
|
+
expect(report?.summary).not.toHaveProperty('redundancy'); // typed checks were not asked for
|
|
130
|
+
});
|
|
120
131
|
it('reports what the change introduced, not what the touched code already had', async () => {
|
|
121
132
|
const branchy = (name, extra = '') => [
|
|
122
133
|
`export function ${name}(x: number) {`,
|
|
@@ -94,7 +94,7 @@ export async function generateTemporalDriftReport(cwd, maxScans = 200) {
|
|
|
94
94
|
if (!db) {
|
|
95
95
|
if (!_sqliteWarningShown) {
|
|
96
96
|
_sqliteWarningShown = true;
|
|
97
|
-
Logger.warn('Temporal drift: SQLite not available
|
|
97
|
+
Logger.warn('Temporal drift: SQLite not available (it is built into Node 22.13 and later), so there is no scan history');
|
|
98
98
|
}
|
|
99
99
|
return null;
|
|
100
100
|
}
|
package/dist/storage/db.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
declare const RIGOUR_DIR: string;
|
|
2
2
|
declare const DB_PATH: string;
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The database behind a promise API: run/get/all/exec return Promises, so callers do not change
|
|
5
|
+
* with the driver, and a later move to a worker or an async driver stays inside this file.
|
|
6
6
|
*/
|
|
7
7
|
export interface RigourDB {
|
|
8
8
|
/** Execute a write statement (INSERT/UPDATE/DELETE). Returns { changes }. */
|
|
@@ -23,7 +23,7 @@ export interface RigourDB {
|
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Open (or create) the Rigour SQLite database.
|
|
26
|
-
* Returns null
|
|
26
|
+
* Returns null where this Node has no node:sqlite.
|
|
27
27
|
*/
|
|
28
28
|
export declare function openDatabase(dbPath?: string): Promise<RigourDB | null>;
|
|
29
29
|
/**
|
|
@@ -44,8 +44,6 @@ export declare function getDatabaseSize(): number;
|
|
|
44
44
|
* Reset the database — delete and recreate from scratch.
|
|
45
45
|
*/
|
|
46
46
|
export declare function resetDatabase(): void;
|
|
47
|
-
/**
|
|
48
|
-
* Check if SQLite is available (sqlite3 installed)
|
|
49
|
-
*/
|
|
47
|
+
/** Whether this Node has SQLite (node:sqlite, Node 22.13 or later). */
|
|
50
48
|
export declare function isSQLiteAvailable(): boolean;
|
|
51
49
|
export { RIGOUR_DIR, DB_PATH };
|
package/dist/storage/db.js
CHANGED
|
@@ -3,44 +3,38 @@
|
|
|
3
3
|
* Single file at ~/.rigour/rigour.db stores all scan history, findings,
|
|
4
4
|
* learned patterns, and feedback. ACID-safe, portable, queryable.
|
|
5
5
|
*
|
|
6
|
-
* Uses
|
|
7
|
-
* All public APIs are async.
|
|
6
|
+
* Uses Node's built-in SQLite (node:sqlite): nothing to install or compile, and no native addon to
|
|
7
|
+
* crash a worker thread. All public APIs are async. Where node:sqlite is missing (Node before
|
|
8
|
+
* 22.13), storage is off and every caller degrades as before.
|
|
8
9
|
*/
|
|
9
10
|
import path from 'path';
|
|
10
11
|
import fs from 'fs-extra';
|
|
11
12
|
import { createRequire } from 'module';
|
|
12
13
|
import { rigourUserDir } from '../utils/user-state.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function
|
|
19
|
-
if (
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (sqlite3Module)
|
|
36
|
-
return sqlite3Module;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
// Try next path
|
|
40
|
-
}
|
|
14
|
+
let sqliteModule;
|
|
15
|
+
/**
|
|
16
|
+
* node:sqlite, or null where this Node has none. Node 22 prints an "experimental feature" warning on
|
|
17
|
+
* the first load; it says nothing a Rigour user can act on, so that one warning is not shown.
|
|
18
|
+
*/
|
|
19
|
+
function loadSqlite() {
|
|
20
|
+
if (sqliteModule !== undefined)
|
|
21
|
+
return sqliteModule;
|
|
22
|
+
const emitWarning = process.emitWarning;
|
|
23
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
24
|
+
if (String(warning?.message ?? warning).startsWith('SQLite is an experimental feature'))
|
|
25
|
+
return;
|
|
26
|
+
return emitWarning.call(process, warning, ...rest);
|
|
27
|
+
});
|
|
28
|
+
try {
|
|
29
|
+
sqliteModule = createRequire(import.meta.url)('node:sqlite');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
sqliteModule = null;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
process.emitWarning = emitWarning;
|
|
41
36
|
}
|
|
42
|
-
|
|
43
|
-
return sqlite3Module;
|
|
37
|
+
return sqliteModule;
|
|
44
38
|
}
|
|
45
39
|
// RIGOUR_HOME when set (tests, sandboxes) so nothing but a real run touches ~/.rigour.
|
|
46
40
|
const RIGOUR_DIR = rigourUserDir();
|
|
@@ -254,52 +248,41 @@ CREATE INDEX IF NOT EXISTS idx_repositories_last_seen ON repositories(last_seen)
|
|
|
254
248
|
CREATE INDEX IF NOT EXISTS idx_interaction_events_repo_created ON interaction_events(repository_id, created_at);
|
|
255
249
|
CREATE INDEX IF NOT EXISTS idx_interaction_events_request ON interaction_events(request_id);
|
|
256
250
|
`;
|
|
251
|
+
/**
|
|
252
|
+
* A value as SQLite stores it. node:sqlite refuses what sqlite3 converted silently, so the same
|
|
253
|
+
* conversions happen here: undefined is NULL, a boolean is 1 or 0, a Date is its epoch milliseconds.
|
|
254
|
+
*/
|
|
255
|
+
function bindable(value) {
|
|
256
|
+
if (value === undefined)
|
|
257
|
+
return null;
|
|
258
|
+
if (typeof value === 'boolean')
|
|
259
|
+
return value ? 1 : 0;
|
|
260
|
+
if (value instanceof Date)
|
|
261
|
+
return value.getTime();
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
/** Rows as plain objects (node:sqlite returns them without a prototype). */
|
|
265
|
+
function plain(row) {
|
|
266
|
+
return row && typeof row === 'object' ? { ...row } : row;
|
|
267
|
+
}
|
|
257
268
|
function wrapDatabase(raw) {
|
|
269
|
+
const prepare = (sql) => raw.prepare(sql);
|
|
258
270
|
const db = {
|
|
259
|
-
run(sql, ...params) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (err)
|
|
263
|
-
return reject(err);
|
|
264
|
-
resolve({ changes: this.changes, lastID: this.lastID });
|
|
265
|
-
});
|
|
266
|
-
});
|
|
271
|
+
async run(sql, ...params) {
|
|
272
|
+
const result = prepare(sql).run(...params.map(bindable));
|
|
273
|
+
return { changes: Number(result.changes), lastID: Number(result.lastInsertRowid) };
|
|
267
274
|
},
|
|
268
|
-
get(sql, ...params) {
|
|
269
|
-
return
|
|
270
|
-
raw.get(sql, ...params, (err, row) => {
|
|
271
|
-
if (err)
|
|
272
|
-
return reject(err);
|
|
273
|
-
resolve(row);
|
|
274
|
-
});
|
|
275
|
-
});
|
|
275
|
+
async get(sql, ...params) {
|
|
276
|
+
return plain(prepare(sql).get(...params.map(bindable)));
|
|
276
277
|
},
|
|
277
|
-
all(sql, ...params) {
|
|
278
|
-
return
|
|
279
|
-
raw.all(sql, ...params, (err, rows) => {
|
|
280
|
-
if (err)
|
|
281
|
-
return reject(err);
|
|
282
|
-
resolve(rows || []);
|
|
283
|
-
});
|
|
284
|
-
});
|
|
278
|
+
async all(sql, ...params) {
|
|
279
|
+
return prepare(sql).all(...params.map(bindable)).map(plain);
|
|
285
280
|
},
|
|
286
|
-
exec(sql) {
|
|
287
|
-
|
|
288
|
-
raw.exec(sql, (err) => {
|
|
289
|
-
if (err)
|
|
290
|
-
return reject(err);
|
|
291
|
-
resolve();
|
|
292
|
-
});
|
|
293
|
-
});
|
|
281
|
+
async exec(sql) {
|
|
282
|
+
raw.exec(sql);
|
|
294
283
|
},
|
|
295
|
-
close() {
|
|
296
|
-
|
|
297
|
-
raw.close((err) => {
|
|
298
|
-
if (err)
|
|
299
|
-
return reject(err);
|
|
300
|
-
resolve();
|
|
301
|
-
});
|
|
302
|
-
});
|
|
284
|
+
async close() {
|
|
285
|
+
raw.close();
|
|
303
286
|
},
|
|
304
287
|
async transaction(fn) {
|
|
305
288
|
await db.exec('BEGIN TRANSACTION');
|
|
@@ -318,22 +301,15 @@ function wrapDatabase(raw) {
|
|
|
318
301
|
}
|
|
319
302
|
/**
|
|
320
303
|
* Open (or create) the Rigour SQLite database.
|
|
321
|
-
* Returns null
|
|
304
|
+
* Returns null where this Node has no node:sqlite.
|
|
322
305
|
*/
|
|
323
306
|
export async function openDatabase(dbPath) {
|
|
324
|
-
const
|
|
325
|
-
if (!
|
|
307
|
+
const sqlite = loadSqlite();
|
|
308
|
+
if (!sqlite)
|
|
326
309
|
return null;
|
|
327
310
|
const resolvedPath = dbPath || DB_PATH;
|
|
328
311
|
fs.ensureDirSync(path.dirname(resolvedPath));
|
|
329
|
-
const
|
|
330
|
-
const instance = new sqlite3.Database(resolvedPath, (err) => {
|
|
331
|
-
if (err)
|
|
332
|
-
return reject(err);
|
|
333
|
-
resolve(instance);
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
const db = wrapDatabase(raw);
|
|
312
|
+
const db = wrapDatabase(new sqlite.DatabaseSync(resolvedPath));
|
|
337
313
|
// Wait for a lock instead of failing at once: a hook and an MCP server often open the database
|
|
338
314
|
// at the same moment, and without this the second open fails with SQLITE_BUSY.
|
|
339
315
|
await db.exec('PRAGMA busy_timeout = 5000');
|
|
@@ -528,7 +504,7 @@ async function enableWal(db, attempts = 10) {
|
|
|
528
504
|
return;
|
|
529
505
|
}
|
|
530
506
|
catch (error) {
|
|
531
|
-
if (!/SQLITE_BUSY
|
|
507
|
+
if (!/SQLITE_BUSY|database is locked/i.test(String(error)) || attempt >= attempts)
|
|
532
508
|
throw error;
|
|
533
509
|
await new Promise(resolve => setTimeout(resolve, 25 * attempt));
|
|
534
510
|
}
|
|
@@ -555,8 +531,7 @@ async function addColumnIfMissing(db, table, column, alterSql) {
|
|
|
555
531
|
* Compact the database — prune old data, reclaim disk space.
|
|
556
532
|
*/
|
|
557
533
|
export async function compactDatabase(retainDays = 90) {
|
|
558
|
-
|
|
559
|
-
if (!sqlite3)
|
|
534
|
+
if (!loadSqlite())
|
|
560
535
|
return { pruned: 0, patternsDecayed: 0, sizeBefore: 0, sizeAfter: 0 };
|
|
561
536
|
const resolvedPath = DB_PATH;
|
|
562
537
|
const sizeBefore = fs.existsSync(resolvedPath) ? fs.statSync(resolvedPath).size : 0;
|
|
@@ -604,10 +579,8 @@ export function resetDatabase() {
|
|
|
604
579
|
if (fs.existsSync(DB_PATH + '-shm'))
|
|
605
580
|
fs.removeSync(DB_PATH + '-shm');
|
|
606
581
|
}
|
|
607
|
-
/**
|
|
608
|
-
* Check if SQLite is available (sqlite3 installed)
|
|
609
|
-
*/
|
|
582
|
+
/** Whether this Node has SQLite (node:sqlite, Node 22.13 or later). */
|
|
610
583
|
export function isSQLiteAvailable() {
|
|
611
|
-
return
|
|
584
|
+
return loadSqlite() !== null;
|
|
612
585
|
}
|
|
613
586
|
export { RIGOUR_DIR, DB_PATH };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { isSQLiteAvailable, openDatabase } from './db.js';
|
|
6
|
+
let dir;
|
|
7
|
+
let db;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-db-'));
|
|
10
|
+
db = (await openDatabase(path.join(dir, 'rigour.db')));
|
|
11
|
+
});
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await db?.close();
|
|
14
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
describe('the database (node:sqlite)', () => {
|
|
17
|
+
it('is built in: no package to install', () => {
|
|
18
|
+
expect(isSQLiteAvailable()).toBe(true);
|
|
19
|
+
expect(db).not.toBeNull();
|
|
20
|
+
});
|
|
21
|
+
it('stores what callers pass the way sqlite3 did: undefined as NULL, booleans as 1 or 0, a Date as epoch ms', async () => {
|
|
22
|
+
await db.exec('CREATE TABLE t (a, b, c, d)');
|
|
23
|
+
const when = new Date('2026-01-02T03:04:05Z');
|
|
24
|
+
const written = await db.run('INSERT INTO t VALUES (?, ?, ?, ?)', undefined, true, false, when);
|
|
25
|
+
expect(written).toEqual({ changes: 1, lastID: 1 });
|
|
26
|
+
const row = await db.get('SELECT * FROM t');
|
|
27
|
+
expect(row).toEqual({ a: null, b: 1, c: 0, d: when.getTime() });
|
|
28
|
+
expect(Object.getPrototypeOf(row)).toBe(Object.prototype); // a plain object, as callers spread and compare
|
|
29
|
+
expect(await db.get('SELECT * FROM t WHERE a = 1')).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
it('rolls a failed transaction back whole', async () => {
|
|
32
|
+
await db.exec('CREATE TABLE t (a UNIQUE)');
|
|
33
|
+
await expect(db.transaction(async (tx) => {
|
|
34
|
+
await tx.run('INSERT INTO t VALUES (?)', 1);
|
|
35
|
+
await tx.run('INSERT INTO t VALUES (?)', 1); // violates UNIQUE
|
|
36
|
+
})).rejects.toThrow(/UNIQUE/);
|
|
37
|
+
expect(await db.all('SELECT * FROM t')).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
it('opens an existing database again with its schema and data, in WAL mode', async () => {
|
|
40
|
+
await db.run("INSERT INTO meta (key, value) VALUES ('probe', 'kept')");
|
|
41
|
+
await db.close();
|
|
42
|
+
db = (await openDatabase(path.join(dir, 'rigour.db')));
|
|
43
|
+
expect(await db.get("SELECT value FROM meta WHERE key = 'probe'")).toEqual({ value: 'kept' });
|
|
44
|
+
expect(await db.get('PRAGMA journal_mode')).toEqual({ journal_mode: 'wal' });
|
|
45
|
+
});
|
|
46
|
+
});
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.7.
|
|
3
|
+
"version": "6.7.2",
|
|
4
4
|
"description": "Rigour's review engine: deterministic gates on changed lines, rules and lessons learned from your team's fixes, and per-check precision from what you fix versus dismiss, across TypeScript, JavaScript, Python, Go, Ruby and C#.",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=22"
|
|
6
|
+
"node": ">=22.13"
|
|
7
7
|
},
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"homepage": "https://rigour.run",
|
|
@@ -71,13 +71,12 @@
|
|
|
71
71
|
"optionalDependencies": {
|
|
72
72
|
"@anthropic-ai/sdk": "^0.30.1",
|
|
73
73
|
"pg": "^8.16.3",
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"@rigour-labs/brain-
|
|
77
|
-
"@rigour-labs/brain-
|
|
78
|
-
"@rigour-labs/brain-
|
|
79
|
-
"@rigour-labs/brain-linux-
|
|
80
|
-
"@rigour-labs/brain-win-x64": "6.7.1"
|
|
74
|
+
"openai": "^5.23.2",
|
|
75
|
+
"@rigour-labs/brain-darwin-x64": "6.7.2",
|
|
76
|
+
"@rigour-labs/brain-linux-x64": "6.7.2",
|
|
77
|
+
"@rigour-labs/brain-win-x64": "6.7.2",
|
|
78
|
+
"@rigour-labs/brain-darwin-arm64": "6.7.2",
|
|
79
|
+
"@rigour-labs/brain-linux-arm64": "6.7.2"
|
|
81
80
|
},
|
|
82
81
|
"devDependencies": {
|
|
83
82
|
"@types/fs-extra": "^11.0.4",
|