@yemi33/minions 0.1.2113 → 0.1.2115
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/bin/minions.js +26 -5
- package/engine/db/index.js +13 -4
- package/engine/db/migrations/010-pr-links.js +61 -0
- package/engine/pull-requests-store.js +34 -1
- package/engine/shared.js +39 -8
- package/engine/small-state-store.js +118 -0
- package/engine/supervisor.js +11 -2
- package/engine/work-items-store.js +39 -1
- package/package.json +1 -1
package/bin/minions.js
CHANGED
|
@@ -231,6 +231,27 @@ function _openStdioLog(name) {
|
|
|
231
231
|
return shared.openAppendLogFd(name, dir, { fallback: 'ignore' }).fd;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Node flag args required for SQLite to load. On Node 22.x, `node:sqlite`
|
|
236
|
+
* is gated behind `--experimental-sqlite`; on Node 24+ it is unflagged.
|
|
237
|
+
* Issue #3035: without this, every engine/dashboard/supervisor spawn on
|
|
238
|
+
* Node 22.x loses the SQL store and falls back to the JSON path —
|
|
239
|
+
* /api/work-items returns [] from the aggregate reader because the
|
|
240
|
+
* Phase-9-canonical SQL path is unreachable.
|
|
241
|
+
*
|
|
242
|
+
* Honors NODE_OPTIONS / MINIONS_FORCE_JSON: if the caller already has the
|
|
243
|
+
* flag set (via NODE_OPTIONS) or has opted out of SQL entirely
|
|
244
|
+
* (MINIONS_FORCE_JSON=1), we add nothing — the child inherits the env.
|
|
245
|
+
*/
|
|
246
|
+
function _sqliteSpawnFlags() {
|
|
247
|
+
if (process.env.MINIONS_FORCE_JSON === '1') return [];
|
|
248
|
+
const major = parseInt(String(process.versions.node).split('.')[0], 10);
|
|
249
|
+
if (!Number.isFinite(major) || major < 22 || major >= 24) return [];
|
|
250
|
+
const nodeOpts = String(process.env.NODE_OPTIONS || '');
|
|
251
|
+
if (nodeOpts.includes('--experimental-sqlite')) return [];
|
|
252
|
+
return ['--experimental-sqlite'];
|
|
253
|
+
}
|
|
254
|
+
|
|
234
255
|
/** Spawn a detached dashboard with self-open suppressed — the CLI decides
|
|
235
256
|
* when to open a browser based on whether a real tab reconnects post-health.
|
|
236
257
|
* stdout/stderr land in engine/dashboard-stdio.log so silent startup crashes
|
|
@@ -239,7 +260,7 @@ function spawnDashboard() {
|
|
|
239
260
|
const env = { ...process.env, MINIONS_NO_AUTO_OPEN: '1' };
|
|
240
261
|
const out = _openStdioLog('dashboard-stdio.log');
|
|
241
262
|
const err = _openStdioLog('dashboard-stdio.log');
|
|
242
|
-
const proc = spawn(process.execPath, [path.join(MINIONS_HOME, 'dashboard.js')], {
|
|
263
|
+
const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'dashboard.js')], {
|
|
243
264
|
cwd: MINIONS_HOME, stdio: ['ignore', out, err], detached: true, windowsHide: true, env
|
|
244
265
|
});
|
|
245
266
|
proc.unref();
|
|
@@ -255,7 +276,7 @@ function spawnDashboard() {
|
|
|
255
276
|
function spawnSupervisor() {
|
|
256
277
|
const out = _openStdioLog('supervisor-stdio.log');
|
|
257
278
|
const err = _openStdioLog('supervisor-stdio.log');
|
|
258
|
-
const proc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine', 'supervisor.js')], {
|
|
279
|
+
const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine', 'supervisor.js')], {
|
|
259
280
|
cwd: MINIONS_HOME, stdio: ['ignore', out, err], detached: true, windowsHide: true,
|
|
260
281
|
});
|
|
261
282
|
proc.unref();
|
|
@@ -277,7 +298,7 @@ const _supervisorPidPath = () => path.join(MINIONS_HOME, 'engine', 'supervisor.p
|
|
|
277
298
|
function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs }) {
|
|
278
299
|
const engineOut = _openStdioLog('engine-stdio.log');
|
|
279
300
|
const engineErr = _openStdioLog('engine-stdio.log');
|
|
280
|
-
const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
|
|
301
|
+
const engineProc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
|
|
281
302
|
cwd: MINIONS_HOME, stdio: ['ignore', engineOut, engineErr], detached: true, windowsHide: true
|
|
282
303
|
});
|
|
283
304
|
engineProc.unref();
|
|
@@ -631,7 +652,7 @@ function init() {
|
|
|
631
652
|
console.log(isUpgrade
|
|
632
653
|
? `\n Upgrade complete (${pkgVersion}). Restarting engine and dashboard...\n`
|
|
633
654
|
: '\n Starting engine and dashboard...\n');
|
|
634
|
-
const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start'], {
|
|
655
|
+
const engineProc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine.js'), 'start'], {
|
|
635
656
|
cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
|
|
636
657
|
});
|
|
637
658
|
engineProc.unref();
|
|
@@ -840,7 +861,7 @@ function ensureInstalled() {
|
|
|
840
861
|
|
|
841
862
|
function delegate(script, args) {
|
|
842
863
|
ensureInstalled();
|
|
843
|
-
const child = spawn(process.execPath, [path.join(MINIONS_HOME, script), ...args], {
|
|
864
|
+
const child = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, script), ...args], {
|
|
844
865
|
stdio: 'inherit',
|
|
845
866
|
cwd: MINIONS_HOME,
|
|
846
867
|
env: { ...process.env, MINIONS_HOME },
|
package/engine/db/index.js
CHANGED
|
@@ -54,6 +54,14 @@ function _installExperimentalWarningFilter() {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function getDb() {
|
|
57
|
+
// Hard opt-out (issue #3035, option #3): MINIONS_FORCE_JSON=1 makes
|
|
58
|
+
// every getDb() call throw, forcing the entire stack onto the JSON
|
|
59
|
+
// fallback path. Useful for users who can't enable
|
|
60
|
+
// --experimental-sqlite on Node 22.x and for regression tests that
|
|
61
|
+
// need to exercise the JSON-fallback branches deterministically.
|
|
62
|
+
if (process.env.MINIONS_FORCE_JSON === '1') {
|
|
63
|
+
throw new Error('engine/db: SQL disabled via MINIONS_FORCE_JSON=1');
|
|
64
|
+
}
|
|
57
65
|
// Re-resolve the DB path on every call so tests that swap MINIONS_TEST_DIR
|
|
58
66
|
// (or production-side configuration that changes MINIONS_HOME between
|
|
59
67
|
// operations) get a fresh connection to the new location instead of
|
|
@@ -94,10 +102,11 @@ function getDb() {
|
|
|
94
102
|
runMigrations(_db);
|
|
95
103
|
return _db;
|
|
96
104
|
} catch (e) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`Node 22.
|
|
100
|
-
|
|
105
|
+
const nodeMajor = parseInt(String(process.versions.node).split('.')[0], 10);
|
|
106
|
+
const flagHint = nodeMajor === 22
|
|
107
|
+
? ` On Node 22.x, node:sqlite is gated behind --experimental-sqlite. Restart with NODE_OPTIONS=--experimental-sqlite, upgrade to Node 24+, or set MINIONS_FORCE_JSON=1 to bypass SQL.`
|
|
108
|
+
: ` Node 22.5+ (with --experimental-sqlite on 22.x) or 24+ required for built-in 'node:sqlite' support.`;
|
|
109
|
+
_dbInitError = new Error(`engine/db: failed to open SQLite — ${e.message}.${flagHint}`);
|
|
101
110
|
throw _dbInitError;
|
|
102
111
|
}
|
|
103
112
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// engine/db/migrations/010-pr-links.js
|
|
2
|
+
//
|
|
3
|
+
// Phase 9.1: move `engine/pr-links.json` into SQL.
|
|
4
|
+
//
|
|
5
|
+
// pr-links.json is the *fallback* PR → work-item linkage store. The
|
|
6
|
+
// primary source is `pull_requests.prdItems[]` (managed via the
|
|
7
|
+
// pull-requests-store). Stray PR IDs that aren't covered by a project
|
|
8
|
+
// pull-requests record still need to be discoverable — that's what this
|
|
9
|
+
// table holds.
|
|
10
|
+
//
|
|
11
|
+
// Shape: { [prId]: <value> } where <value> is typically an array of item IDs
|
|
12
|
+
// but may also be a bare string from legacy writes. Readers normalize.
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
|
|
17
|
+
function _resolveMinionsDir() {
|
|
18
|
+
const envHome = process.env.MINIONS_HOME || process.env.MINIONS_TEST_DIR;
|
|
19
|
+
if (envHome) return envHome;
|
|
20
|
+
try { return require('../../shared').MINIONS_DIR; } catch { return null; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _readJsonOr(filePath, fallback) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
} catch { return fallback; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
version: 10,
|
|
32
|
+
description: 'pr_links: schema + pr-links.json backfill (fallback static store)',
|
|
33
|
+
up(db) {
|
|
34
|
+
db.exec(`
|
|
35
|
+
CREATE TABLE pr_links (
|
|
36
|
+
pr_id TEXT PRIMARY KEY,
|
|
37
|
+
data TEXT NOT NULL,
|
|
38
|
+
updated_at INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
const minionsDir = _resolveMinionsDir();
|
|
43
|
+
if (!minionsDir) return;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
let inserted = 0;
|
|
46
|
+
|
|
47
|
+
const raw = _readJsonOr(path.join(minionsDir, 'engine', 'pr-links.json'), null);
|
|
48
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
49
|
+
const ins = db.prepare(`INSERT INTO pr_links (pr_id, data, updated_at) VALUES (?, ?, ?)`);
|
|
50
|
+
for (const [prId, value] of Object.entries(raw)) {
|
|
51
|
+
try {
|
|
52
|
+
ins.run(String(prId), JSON.stringify(value), now);
|
|
53
|
+
inserted += 1;
|
|
54
|
+
} catch { /* duplicate / corrupt — skip */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.log(`[db-migrate] v10: backfilled ${inserted} pr-links rows; pr-links.json kept as dual-write mirror`);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -149,11 +149,44 @@ function readPullRequestsForScope(scope) {
|
|
|
149
149
|
return out;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// Enumerate scopes that have a JSON file on disk — used as the
|
|
153
|
+
// SQL-unavailable fallback for `readAllPullRequests`. Mirrors
|
|
154
|
+
// work-items-store's `_enumerateJsonScopes` shape.
|
|
155
|
+
function _enumerateJsonScopes() {
|
|
156
|
+
const shared = require('./shared');
|
|
157
|
+
const scopes = [];
|
|
158
|
+
const centralPath = path.join(shared.MINIONS_DIR, 'pull-requests.json');
|
|
159
|
+
if (fs.existsSync(centralPath)) scopes.push('central');
|
|
160
|
+
const projectsDir = path.join(shared.MINIONS_DIR, 'projects');
|
|
161
|
+
try {
|
|
162
|
+
for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
|
|
163
|
+
if (!d.isDirectory()) continue;
|
|
164
|
+
const fp = path.join(projectsDir, d.name, 'pull-requests.json');
|
|
165
|
+
if (fs.existsSync(fp)) scopes.push(d.name);
|
|
166
|
+
}
|
|
167
|
+
} catch { /* projects dir missing on fresh install */ }
|
|
168
|
+
return scopes;
|
|
169
|
+
}
|
|
170
|
+
|
|
152
171
|
function readAllPullRequests() {
|
|
153
172
|
const { getDb } = require('./db');
|
|
154
173
|
let db;
|
|
155
174
|
try { db = getDb(); }
|
|
156
|
-
catch {
|
|
175
|
+
catch {
|
|
176
|
+
// Issue #3035: SQLite unavailable (Node 22.x without
|
|
177
|
+
// --experimental-sqlite) — read every JSON scope on disk so the
|
|
178
|
+
// aggregate readers (queries.getPullRequests, shared.getPrLinks)
|
|
179
|
+
// continue to return real data instead of [].
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const scope of _enumerateJsonScopes()) {
|
|
182
|
+
for (const pr of _readJsonArrayFallback(scope)) {
|
|
183
|
+
if (!pr || typeof pr !== 'object') continue;
|
|
184
|
+
pr._scope = scope;
|
|
185
|
+
out.push(pr);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
157
190
|
|
|
158
191
|
try { _resyncScopeIfJsonDiverged(db, 'central'); } catch {}
|
|
159
192
|
const knownScopes = db.prepare('SELECT DISTINCT scope FROM pull_requests').all().map(r => r.scope);
|
package/engine/shared.js
CHANGED
|
@@ -532,6 +532,10 @@ function _routeJsonReadToSql(p) {
|
|
|
532
532
|
const store = require('./small-state-store');
|
|
533
533
|
return { value: store.readQaSessions() };
|
|
534
534
|
}
|
|
535
|
+
if (norm.endsWith('/engine/pr-links.json')) {
|
|
536
|
+
const store = require('./small-state-store');
|
|
537
|
+
return { value: store.readPrLinks() };
|
|
538
|
+
}
|
|
535
539
|
// Per-project work-items.json — match `/projects/<name>/work-items.json`.
|
|
536
540
|
// When SQL has no rows for the scope AND the JSON file is absent on
|
|
537
541
|
// disk, preserve the legacy "file missing → null" semantic. This guards
|
|
@@ -582,9 +586,9 @@ function _routeJsonReadToSql(p) {
|
|
|
582
586
|
*
|
|
583
587
|
* **SQL routing (Phase 9):** when the path matches a migrated state file
|
|
584
588
|
* (work-items, pull-requests, dispatch, metrics, watches, schedule-runs,
|
|
585
|
-
* pipeline-runs, managed-processes, worktree-pool, qa-runs, qa-sessions
|
|
586
|
-
* the read is served from SQLite via `_routeJsonReadToSql` — the
|
|
587
|
-
* on disk is no longer authoritative.
|
|
589
|
+
* pipeline-runs, managed-processes, worktree-pool, qa-runs, qa-sessions,
|
|
590
|
+
* pr-links), the read is served from SQLite via `_routeJsonReadToSql` — the
|
|
591
|
+
* JSON file on disk is no longer authoritative.
|
|
588
592
|
*
|
|
589
593
|
* Counterpart: `safeJsonNoRestore` for terminal artifacts and "missing == gone"
|
|
590
594
|
* reads (cooldowns, archived PRDs, ephemeral session state) where reviving a
|
|
@@ -4988,9 +4992,19 @@ function getPrLinks() {
|
|
|
4988
4992
|
mergePrLinkItems(links, pr.id, pr.prdItems || []);
|
|
4989
4993
|
}
|
|
4990
4994
|
} catch { /* SQL unavailable — skip primary source */ }
|
|
4991
|
-
// Fallback: static pr-links.json
|
|
4995
|
+
// Fallback: pr_links SQL store (was static pr-links.json file). Phase 9.1
|
|
4996
|
+
// routes both reads and writes through small-state-store; the JSON file is
|
|
4997
|
+
// kept as a dual-write mirror. SQL failures fall through to the JSON file.
|
|
4998
|
+
let static_ = null;
|
|
4992
4999
|
try {
|
|
4993
|
-
const
|
|
5000
|
+
const store = require('./small-state-store');
|
|
5001
|
+
static_ = store.readPrLinks();
|
|
5002
|
+
} catch { static_ = null; }
|
|
5003
|
+
if (!static_ || Object.keys(static_).length === 0) {
|
|
5004
|
+
try { static_ = JSON.parse(fs.readFileSync(PR_LINKS_PATH, 'utf8')); }
|
|
5005
|
+
catch { static_ = null; }
|
|
5006
|
+
}
|
|
5007
|
+
if (static_ && typeof static_ === 'object' && !Array.isArray(static_)) {
|
|
4994
5008
|
for (const [k, v] of Object.entries(static_)) {
|
|
4995
5009
|
const canonical = parseCanonicalPrId(k);
|
|
4996
5010
|
let normalizedKey = canonical ? `${canonical.scope}#${canonical.prNumber}` : k;
|
|
@@ -5004,7 +5018,7 @@ function getPrLinks() {
|
|
|
5004
5018
|
}
|
|
5005
5019
|
if (!links[normalizedKey]) mergePrLinkItems(links, normalizedKey, v);
|
|
5006
5020
|
}
|
|
5007
|
-
}
|
|
5021
|
+
}
|
|
5008
5022
|
return links;
|
|
5009
5023
|
}
|
|
5010
5024
|
|
|
@@ -5030,7 +5044,7 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
5030
5044
|
const effectivePrId = canonicalPrId || prId;
|
|
5031
5045
|
if (!effectivePrId || !itemId) return;
|
|
5032
5046
|
const legacyPrId = String(prId || '');
|
|
5033
|
-
|
|
5047
|
+
const mutator = (links) => {
|
|
5034
5048
|
if (!links || Array.isArray(links) || typeof links !== 'object') links = {};
|
|
5035
5049
|
const mergedCurrent = new Set(normalizePrLinkItems(links[effectivePrId]));
|
|
5036
5050
|
if (legacyPrId && legacyPrId !== effectivePrId && links[legacyPrId]) {
|
|
@@ -5040,7 +5054,24 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
5040
5054
|
if (!mergedCurrent.has(itemId)) mergedCurrent.add(itemId);
|
|
5041
5055
|
links[effectivePrId] = [...mergedCurrent];
|
|
5042
5056
|
return links;
|
|
5043
|
-
}
|
|
5057
|
+
};
|
|
5058
|
+
// Phase 9.1: pr-links is SQL-canonical via small-state-store; the JSON file
|
|
5059
|
+
// is a dual-write mirror. SQLite failures fall through to the legacy JSON
|
|
5060
|
+
// path so older installs without --experimental-sqlite still work.
|
|
5061
|
+
let routedViaSql = false;
|
|
5062
|
+
try {
|
|
5063
|
+
const store = require('./small-state-store');
|
|
5064
|
+
store.applyPrLinksMutation(mutator);
|
|
5065
|
+
try { store._mirrorPrLinksJson(); } catch { /* mirror best-effort */ }
|
|
5066
|
+
routedViaSql = true;
|
|
5067
|
+
} catch (e) {
|
|
5068
|
+
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) {
|
|
5069
|
+
throw e;
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
if (!routedViaSql) {
|
|
5073
|
+
mutateJsonFileLocked(PR_LINKS_PATH, mutator, { defaultValue: {} });
|
|
5074
|
+
}
|
|
5044
5075
|
|
|
5045
5076
|
if (!project) return;
|
|
5046
5077
|
const prPath = projectPrPath(project);
|
|
@@ -829,6 +829,118 @@ function _mirrorQaSessionsJson(filePath) {
|
|
|
829
829
|
} catch { /* mirror best-effort */ }
|
|
830
830
|
}
|
|
831
831
|
|
|
832
|
+
// ─── pr_links ───────────────────────────────────────────────────────────────
|
|
833
|
+
// Shape: { [prId]: <value> } where <value> is typically array of item IDs
|
|
834
|
+
// but may also be a bare string (legacy). Readers normalize.
|
|
835
|
+
|
|
836
|
+
let _prLinksHash = null;
|
|
837
|
+
|
|
838
|
+
function _hydratePrLinks(db) {
|
|
839
|
+
const fp = _resolveFilePath('pr-links.json');
|
|
840
|
+
const raw = _readJson(fp) || {};
|
|
841
|
+
db.prepare('DELETE FROM pr_links').run();
|
|
842
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
|
843
|
+
const now = Date.now();
|
|
844
|
+
const ins = db.prepare('INSERT INTO pr_links (pr_id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(pr_id) DO NOTHING');
|
|
845
|
+
for (const [prId, value] of Object.entries(raw)) {
|
|
846
|
+
ins.run(String(prId), JSON.stringify(value), now);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function _resyncPrLinksIfDiverged(db) {
|
|
851
|
+
const fp = _resolveFilePath('pr-links.json');
|
|
852
|
+
const currentHash = _fileContentHash(fp);
|
|
853
|
+
if (currentHash == null) return;
|
|
854
|
+
if (_prLinksHash != null && currentHash === _prLinksHash) return;
|
|
855
|
+
if (_prLinksHash == null) {
|
|
856
|
+
const sqlHas = db.prepare('SELECT 1 FROM pr_links LIMIT 1').get();
|
|
857
|
+
if (sqlHas) {
|
|
858
|
+
_prLinksHash = currentHash;
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
_hydratePrLinks(db);
|
|
863
|
+
_prLinksHash = currentHash;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function _readPrLinksFromSql(db) {
|
|
867
|
+
const rows = db.prepare('SELECT pr_id, data FROM pr_links').all();
|
|
868
|
+
const out = {};
|
|
869
|
+
for (const row of rows) {
|
|
870
|
+
try { out[row.pr_id] = JSON.parse(row.data); }
|
|
871
|
+
catch { /* skip malformed */ }
|
|
872
|
+
}
|
|
873
|
+
return out;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function readPrLinks() {
|
|
877
|
+
const { getDb } = require('./db');
|
|
878
|
+
let db;
|
|
879
|
+
try { db = getDb(); }
|
|
880
|
+
catch { return _readJson(_resolveFilePath('pr-links.json')) || {}; }
|
|
881
|
+
try { _resyncPrLinksIfDiverged(db); }
|
|
882
|
+
catch { /* table may be missing on a stale install — fall back to JSON */ }
|
|
883
|
+
let out;
|
|
884
|
+
try { out = _readPrLinksFromSql(db); }
|
|
885
|
+
catch { return _readJson(_resolveFilePath('pr-links.json')) || {}; }
|
|
886
|
+
if (Object.keys(out).length === 0) {
|
|
887
|
+
const fallback = _readJson(_resolveFilePath('pr-links.json'));
|
|
888
|
+
if (fallback && Object.keys(fallback).length > 0) return fallback;
|
|
889
|
+
return {};
|
|
890
|
+
}
|
|
891
|
+
return out;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function applyPrLinksMutation(mutator) {
|
|
895
|
+
const { getDb, withTransaction } = require('./db');
|
|
896
|
+
let db;
|
|
897
|
+
try { db = getDb(); }
|
|
898
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
899
|
+
|
|
900
|
+
return withTransaction(db, () => {
|
|
901
|
+
_resyncPrLinksIfDiverged(db);
|
|
902
|
+
const before = _readPrLinksFromSql(db);
|
|
903
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
904
|
+
const next = mutator(before);
|
|
905
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
906
|
+
if (!after || typeof after !== 'object' || Array.isArray(after)) {
|
|
907
|
+
return { wrote: false, result: beforeSnap };
|
|
908
|
+
}
|
|
909
|
+
const afterIds = new Set(Object.keys(after));
|
|
910
|
+
const beforeIds = new Set(Object.keys(beforeSnap));
|
|
911
|
+
let wrote = false;
|
|
912
|
+
const now = Date.now();
|
|
913
|
+
const upsert = db.prepare(`
|
|
914
|
+
INSERT INTO pr_links (pr_id, data, updated_at)
|
|
915
|
+
VALUES (?, ?, ?)
|
|
916
|
+
ON CONFLICT(pr_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
917
|
+
`);
|
|
918
|
+
const del = db.prepare('DELETE FROM pr_links WHERE pr_id = ?');
|
|
919
|
+
for (const id of afterIds) {
|
|
920
|
+
if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
|
|
921
|
+
upsert.run(id, JSON.stringify(after[id]), now);
|
|
922
|
+
wrote = true;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
for (const id of beforeIds) {
|
|
926
|
+
if (!afterIds.has(id)) { del.run(id); wrote = true; }
|
|
927
|
+
}
|
|
928
|
+
return { wrote, result: after };
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function _mirrorPrLinksJson(filePath) {
|
|
933
|
+
try {
|
|
934
|
+
const shared = require('./shared');
|
|
935
|
+
const { getDb } = require('./db');
|
|
936
|
+
const obj = _readPrLinksFromSql(getDb());
|
|
937
|
+
const target = filePath || _resolveFilePath('pr-links.json');
|
|
938
|
+
shared.safeWrite(target, obj);
|
|
939
|
+
const h = _fileContentHash(target);
|
|
940
|
+
if (h != null) _prLinksHash = h;
|
|
941
|
+
} catch { /* mirror best-effort */ }
|
|
942
|
+
}
|
|
943
|
+
|
|
832
944
|
// ─── Test seam ─────────────────────────────────────────────────────────────
|
|
833
945
|
|
|
834
946
|
function _resetAllForTest() {
|
|
@@ -841,6 +953,7 @@ function _resetAllForTest() {
|
|
|
841
953
|
db.exec('DELETE FROM worktree_pool');
|
|
842
954
|
try { db.exec('DELETE FROM qa_runs'); } catch { /* migration not applied */ }
|
|
843
955
|
try { db.exec('DELETE FROM qa_sessions'); } catch { /* migration not applied */ }
|
|
956
|
+
try { db.exec('DELETE FROM pr_links'); } catch { /* migration not applied */ }
|
|
844
957
|
} catch { /* not initialized */ }
|
|
845
958
|
_scheduleRunsHash = null;
|
|
846
959
|
_pipelineRunsHash = null;
|
|
@@ -848,6 +961,7 @@ function _resetAllForTest() {
|
|
|
848
961
|
_worktreePoolHash = null;
|
|
849
962
|
_qaRunsHash = null;
|
|
850
963
|
_qaSessionsHash = null;
|
|
964
|
+
_prLinksHash = null;
|
|
851
965
|
}
|
|
852
966
|
|
|
853
967
|
module.exports = {
|
|
@@ -875,6 +989,10 @@ module.exports = {
|
|
|
875
989
|
readQaSessions,
|
|
876
990
|
applyQaSessionsMutation,
|
|
877
991
|
_mirrorQaSessionsJson,
|
|
992
|
+
// pr_links
|
|
993
|
+
readPrLinks,
|
|
994
|
+
applyPrLinksMutation,
|
|
995
|
+
_mirrorPrLinksJson,
|
|
878
996
|
// test seam
|
|
879
997
|
_resetAllForTest,
|
|
880
998
|
};
|
package/engine/supervisor.js
CHANGED
|
@@ -133,10 +133,19 @@ function openAppendFd(name) {
|
|
|
133
133
|
try { return fs.openSync(path.join(dir, name), 'a'); } catch { return 'ignore'; }
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
function _sqliteSpawnFlags() {
|
|
137
|
+
if (process.env.MINIONS_FORCE_JSON === '1') return [];
|
|
138
|
+
const major = parseInt(String(process.versions.node).split('.')[0], 10);
|
|
139
|
+
if (!Number.isFinite(major) || major < 22 || major >= 24) return [];
|
|
140
|
+
const nodeOpts = String(process.env.NODE_OPTIONS || '');
|
|
141
|
+
if (nodeOpts.includes('--experimental-sqlite')) return [];
|
|
142
|
+
return ['--experimental-sqlite'];
|
|
143
|
+
}
|
|
144
|
+
|
|
136
145
|
function spawnEngine() {
|
|
137
146
|
const out = openAppendFd('engine-stdio.log');
|
|
138
147
|
const err = openAppendFd('engine-stdio.log');
|
|
139
|
-
const proc = spawn(process.execPath, [path.join(MINIONS_DIR, 'engine.js'), 'start'], {
|
|
148
|
+
const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_DIR, 'engine.js'), 'start'], {
|
|
140
149
|
cwd: MINIONS_DIR,
|
|
141
150
|
stdio: ['ignore', out, err],
|
|
142
151
|
detached: true,
|
|
@@ -150,7 +159,7 @@ function spawnDashboard() {
|
|
|
150
159
|
const out = openAppendFd('dashboard-stdio.log');
|
|
151
160
|
const err = openAppendFd('dashboard-stdio.log');
|
|
152
161
|
const env = { ...process.env, MINIONS_NO_AUTO_OPEN: '1' };
|
|
153
|
-
const proc = spawn(process.execPath, [path.join(MINIONS_DIR, 'dashboard.js')], {
|
|
162
|
+
const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_DIR, 'dashboard.js')], {
|
|
154
163
|
cwd: MINIONS_DIR,
|
|
155
164
|
stdio: ['ignore', out, err],
|
|
156
165
|
detached: true,
|
|
@@ -138,14 +138,52 @@ function _resyncScopeIfJsonDiverged(db, scope) {
|
|
|
138
138
|
_lastMirrorHashByScope.set(scope, currentHash);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// Enumerate scopes that have a JSON file on disk — used as the SQL-
|
|
142
|
+
// unavailable fallback for `readAllWorkItems`. Walks `<MINIONS_DIR>/
|
|
143
|
+
// projects/*/work-items.json` plus the central `<MINIONS_DIR>/work-
|
|
144
|
+
// items.json`. Silently swallows missing dirs (fresh installs).
|
|
145
|
+
function _enumerateJsonScopes() {
|
|
146
|
+
const shared = require('./shared');
|
|
147
|
+
const scopes = [];
|
|
148
|
+
const centralPath = path.join(shared.MINIONS_DIR, 'work-items.json');
|
|
149
|
+
if (fs.existsSync(centralPath)) scopes.push('central');
|
|
150
|
+
const projectsDir = path.join(shared.MINIONS_DIR, 'projects');
|
|
151
|
+
try {
|
|
152
|
+
for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
|
|
153
|
+
if (!d.isDirectory()) continue;
|
|
154
|
+
const fp = path.join(projectsDir, d.name, 'work-items.json');
|
|
155
|
+
if (fs.existsSync(fp)) scopes.push(d.name);
|
|
156
|
+
}
|
|
157
|
+
} catch { /* projects dir missing on fresh install */ }
|
|
158
|
+
return scopes;
|
|
159
|
+
}
|
|
160
|
+
|
|
141
161
|
// Read all rows across all scopes — used by queries.getWorkItems which
|
|
142
162
|
// needs to surface central + every project's items in a single shot,
|
|
143
163
|
// tagged with their source scope.
|
|
164
|
+
//
|
|
165
|
+
// Issue #3035: SQL-unavailable installs (Node 22.x without
|
|
166
|
+
// --experimental-sqlite, prior to v0.1.2113) returned [] from this path
|
|
167
|
+
// because `getDb()` throws and the legacy `return null` signal was
|
|
168
|
+
// swallowed by `queries.getWorkItems`'s `|| []`. Mirror the per-scope
|
|
169
|
+
// reader's JSON-fallback shape so the aggregate API stays useful on
|
|
170
|
+
// Node versions that don't have node:sqlite enabled.
|
|
144
171
|
function readAllWorkItems() {
|
|
145
172
|
const { getDb } = require('./db');
|
|
146
173
|
let db;
|
|
147
174
|
try { db = getDb(); }
|
|
148
|
-
catch {
|
|
175
|
+
catch {
|
|
176
|
+
// SQLite unavailable — read every JSON scope on disk directly.
|
|
177
|
+
const out = [];
|
|
178
|
+
for (const scope of _enumerateJsonScopes()) {
|
|
179
|
+
for (const wi of _readJsonArrayFallback(scope)) {
|
|
180
|
+
if (!wi || typeof wi !== 'object') continue;
|
|
181
|
+
wi._source = scope;
|
|
182
|
+
out.push(wi);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
149
187
|
|
|
150
188
|
// Pick up any external JSON edits for every scope SQL knows about.
|
|
151
189
|
// Also resync 'central' explicitly so first-time reads on a JSON-only
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2115",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|