@yemi33/minions 0.1.2265 → 0.1.2267
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/dashboard.js +152 -8
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -590,6 +590,126 @@ let _prRefVerifierOverride = null; // test seam
|
|
|
590
590
|
// (prRef, project) and returns true | false | null.
|
|
591
591
|
function _setPrRefVerifierForTest(fn) { _prRefVerifierOverride = (typeof fn === 'function') ? fn : null; }
|
|
592
592
|
|
|
593
|
+
// ── Live PR fetch helpers (W-mqtrnp7y00056bc8) ───────────────────────────────
|
|
594
|
+
//
|
|
595
|
+
// When GET /api/prs/:id is called for a PR that is not in the local tracker
|
|
596
|
+
// (e.g. merged before the engine started tracking, or from an unpolled repo),
|
|
597
|
+
// attempt a live fetch from the platform API and return a shaped record with
|
|
598
|
+
// _liveOnly: true. Falls back to the existing 404 when both lookups fail.
|
|
599
|
+
|
|
600
|
+
const LIVE_PR_FETCH_TIMEOUT_MS = 8000;
|
|
601
|
+
|
|
602
|
+
// Test seam: when set, replaces the real _fetchLivePrRecord call so unit tests
|
|
603
|
+
// can inject a mock without standing up a real gh CLI or ADO endpoint.
|
|
604
|
+
let _livePrFetchForTest = null;
|
|
605
|
+
function _setLivePrFetchForTest(fn) {
|
|
606
|
+
_livePrFetchForTest = typeof fn === 'function' ? fn : null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function _mapGhPrToRecord(ghPr, canonicalId) {
|
|
610
|
+
const isMerged = !!ghPr.merged_at;
|
|
611
|
+
const status = isMerged ? 'merged' : ghPr.state === 'closed' ? 'closed' : 'active';
|
|
612
|
+
return {
|
|
613
|
+
id: canonicalId,
|
|
614
|
+
prNumber: ghPr.number,
|
|
615
|
+
title: (ghPr.title || `PR #${ghPr.number}`).slice(0, 120),
|
|
616
|
+
agent: (ghPr.user && ghPr.user.login) ? String(ghPr.user.login).toLowerCase() : 'unknown',
|
|
617
|
+
branch: (ghPr.head && ghPr.head.ref) || '',
|
|
618
|
+
status,
|
|
619
|
+
url: ghPr.html_url || '',
|
|
620
|
+
description: typeof ghPr.body === 'string' ? ghPr.body.slice(0, 500) : '',
|
|
621
|
+
_liveOnly: true,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function _mapAdoPrToRecord(adoPr, canonicalId, ref) {
|
|
626
|
+
const status = adoPr.status === 'completed' ? 'merged'
|
|
627
|
+
: adoPr.status === 'abandoned' ? 'abandoned'
|
|
628
|
+
: 'active';
|
|
629
|
+
const branch = typeof adoPr.sourceRefName === 'string'
|
|
630
|
+
? adoPr.sourceRefName.replace(/^refs\/heads\//, '')
|
|
631
|
+
: '';
|
|
632
|
+
const author = (adoPr.createdBy && (adoPr.createdBy.uniqueName || adoPr.createdBy.displayName)) || 'unknown';
|
|
633
|
+
const prUrl = `https://dev.azure.com/${encodeURIComponent(ref.org)}/${encodeURIComponent(ref.project)}/_git/${encodeURIComponent(ref.repo)}/pullrequest/${ref.number}`;
|
|
634
|
+
return {
|
|
635
|
+
id: canonicalId,
|
|
636
|
+
prNumber: adoPr.pullRequestId || ref.number,
|
|
637
|
+
title: (adoPr.title || `PR #${ref.number}`).slice(0, 120),
|
|
638
|
+
agent: String(author).toLowerCase(),
|
|
639
|
+
branch,
|
|
640
|
+
status,
|
|
641
|
+
url: prUrl,
|
|
642
|
+
description: typeof adoPr.description === 'string' ? adoPr.description.slice(0, 500) : '',
|
|
643
|
+
_liveOnly: true,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Exported for unit testing — fetch a PR live from the platform API and return
|
|
648
|
+
// a tracker-shaped record. Returns null when the canonical id is not parseable
|
|
649
|
+
// or the host is unrecognized. Throws on fetch errors so callers can decide
|
|
650
|
+
// whether to log-and-degrade or propagate.
|
|
651
|
+
//
|
|
652
|
+
// opts: test seams only — production callers pass no opts.
|
|
653
|
+
// _resolveTokenForSlug(slug) → token string or null
|
|
654
|
+
// _shellSafeGh(args, opts) → stdout string (argv-form gh CLI)
|
|
655
|
+
// _adoFetch(url, token) → parsed JSON object
|
|
656
|
+
// _adoToken → ADO bearer token string (skips ado.getAdoToken())
|
|
657
|
+
async function _fetchLivePrRecord(canonicalId, opts) {
|
|
658
|
+
const o = opts || {};
|
|
659
|
+
const parsed = shared.parseCanonicalPrId(canonicalId);
|
|
660
|
+
if (!parsed) return null; // bare number or unknown format
|
|
661
|
+
const { scope, prNumber } = parsed;
|
|
662
|
+
const colonIdx = scope.indexOf(':');
|
|
663
|
+
const host = scope.slice(0, colonIdx).toLowerCase();
|
|
664
|
+
const scopeSlug = scope.slice(colonIdx + 1);
|
|
665
|
+
|
|
666
|
+
if (host === 'github') {
|
|
667
|
+
const [owner, repo] = scopeSlug.split('/');
|
|
668
|
+
if (!owner || !repo) return null;
|
|
669
|
+
const ghSlug = shared.validateGhSlug(`${owner}/${repo}`);
|
|
670
|
+
const num = String(prNumber);
|
|
671
|
+
const resolveToken = o._resolveTokenForSlug || ghToken.resolveTokenForSlug;
|
|
672
|
+
const token = resolveToken(`${owner}/${repo}`);
|
|
673
|
+
const ghOpts = { timeout: LIVE_PR_FETCH_TIMEOUT_MS };
|
|
674
|
+
if (token) ghOpts.env = { ...process.env, GH_TOKEN: token };
|
|
675
|
+
const shellGh = o._shellSafeGh || shared.shellSafeGh;
|
|
676
|
+
const raw = await shellGh(['api', `repos/${ghSlug}/pulls/${num}`], ghOpts);
|
|
677
|
+
const ghPr = JSON.parse(raw);
|
|
678
|
+
return _mapGhPrToRecord(ghPr, canonicalId);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (host === 'ado') {
|
|
682
|
+
const segs = scopeSlug.split('/');
|
|
683
|
+
if (segs.length < 3) return null;
|
|
684
|
+
const [org, project, repo] = segs;
|
|
685
|
+
const ref = { host: 'ado', slug: scopeSlug, org, project, repo, number: prNumber, id: canonicalId };
|
|
686
|
+
const doFetch = o._adoFetch || (async (url, adoToken) => {
|
|
687
|
+
const res = await fetch(url, {
|
|
688
|
+
headers: { Authorization: `Bearer ${adoToken}`, 'Content-Type': 'application/json' },
|
|
689
|
+
signal: AbortSignal.timeout(LIVE_PR_FETCH_TIMEOUT_MS),
|
|
690
|
+
});
|
|
691
|
+
if (!res.ok) throw new Error(`ADO API ${res.status}: ${res.statusText}`);
|
|
692
|
+
const text = await res.text();
|
|
693
|
+
if (!text || text.trimStart().startsWith('<')) {
|
|
694
|
+
throw new Error(`ADO returned HTML instead of JSON for ${url.split('?')[0]}`);
|
|
695
|
+
}
|
|
696
|
+
return JSON.parse(text);
|
|
697
|
+
});
|
|
698
|
+
const adoTok = o._adoToken != null ? o._adoToken : await ado.getAdoToken();
|
|
699
|
+
if (!adoTok) throw new Error(`Could not acquire ADO token for ${canonicalId}`);
|
|
700
|
+
const orgBase = `https://dev.azure.com/${encodeURIComponent(org)}`;
|
|
701
|
+
const projEnc = encodeURIComponent(project);
|
|
702
|
+
const repoEnc = encodeURIComponent(repo);
|
|
703
|
+
const adoPr = await doFetch(
|
|
704
|
+
`${orgBase}/${projEnc}/_apis/git/repositories/${repoEnc}/pullRequests/${prNumber}?api-version=7.1`,
|
|
705
|
+
adoTok,
|
|
706
|
+
);
|
|
707
|
+
return _mapAdoPrToRecord(adoPr, canonicalId, ref);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return null; // unknown host
|
|
711
|
+
}
|
|
712
|
+
|
|
593
713
|
function _findTrackedPrRecord(prRef, project) {
|
|
594
714
|
if (!project) return null;
|
|
595
715
|
try {
|
|
@@ -2055,10 +2175,6 @@ function _compareVersions(a, b) {
|
|
|
2055
2175
|
return 0;
|
|
2056
2176
|
}
|
|
2057
2177
|
|
|
2058
|
-
// Kick off first npm check on startup, then re-check every 4 hours
|
|
2059
|
-
checkNpmVersion().catch(() => {});
|
|
2060
|
-
setInterval(() => checkNpmVersion().catch(() => {}), _getVersionCheckInterval()).unref();
|
|
2061
|
-
|
|
2062
2178
|
// Cache disk version + git commit (only changes on deploy/pull, not per-request)
|
|
2063
2179
|
let _diskVersionCache = null;
|
|
2064
2180
|
let _diskVersionCacheTs = 0;
|
|
@@ -6398,16 +6514,35 @@ const server = http.createServer(async (req, res) => {
|
|
|
6398
6514
|
|
|
6399
6515
|
// GET /api/prs/<id> — return a single fully-enriched PR record by canonical
|
|
6400
6516
|
// id (`<host>:<slug>#<number>`) or by bare number (P-79b47b0c). The in-stack
|
|
6401
|
-
// PR modal (renderPrs.openPrDetail) calls this on demand.
|
|
6402
|
-
// record
|
|
6517
|
+
// PR modal (renderPrs.openPrDetail) calls this on demand. Returns the local
|
|
6518
|
+
// tracker record when found; falls back to a live platform fetch (GitHub REST
|
|
6519
|
+
// or ADO) for PRs that were never added to the tracker. The live record
|
|
6520
|
+
// carries _liveOnly: true so the UI can optionally surface a "start tracking"
|
|
6521
|
+
// offer. Returns {"error":"pr not found"} when both lookups fail.
|
|
6403
6522
|
async function handlePrsById(req, res, match) {
|
|
6404
6523
|
try {
|
|
6405
6524
|
const id = decodeURIComponent(match[1] || '').trim();
|
|
6406
6525
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
6407
6526
|
const prs = queries.getPullRequests();
|
|
6408
6527
|
const found = prs.find(p => p && (p.id === id || String(p.number) === id));
|
|
6409
|
-
if (
|
|
6410
|
-
|
|
6528
|
+
if (found) return jsonReply(res, 200, { pr: found });
|
|
6529
|
+
|
|
6530
|
+
// Not in local tracker — attempt live fetch from platform API.
|
|
6531
|
+
const liveFetch = _livePrFetchForTest || _fetchLivePrRecord;
|
|
6532
|
+
let liveRecord = null;
|
|
6533
|
+
try {
|
|
6534
|
+
liveRecord = await Promise.race([
|
|
6535
|
+
liveFetch(id),
|
|
6536
|
+
new Promise((_, rej) => {
|
|
6537
|
+
const t = setTimeout(() => rej(new Error(`live PR fetch timed out for ${id}`)), LIVE_PR_FETCH_TIMEOUT_MS + 1000);
|
|
6538
|
+
if (t.unref) t.unref();
|
|
6539
|
+
}),
|
|
6540
|
+
]);
|
|
6541
|
+
} catch (e) {
|
|
6542
|
+
shared.log('warn', `handlePrsById: live fetch failed for ${id}: ${e.message}`);
|
|
6543
|
+
}
|
|
6544
|
+
if (liveRecord) return jsonReply(res, 200, { pr: liveRecord });
|
|
6545
|
+
return jsonReply(res, 404, { error: 'pr not found' });
|
|
6411
6546
|
} catch (e) { return jsonReply(res, 500, { error: e.message }); }
|
|
6412
6547
|
}
|
|
6413
6548
|
|
|
@@ -14166,6 +14301,8 @@ function _installCrashHandlers() {
|
|
|
14166
14301
|
module.exports = {
|
|
14167
14302
|
getMcpServers,
|
|
14168
14303
|
_setPrRefVerifierForTest, // issue #246 — inject a fake loose-PR-ref verifier in handler tests
|
|
14304
|
+
_setLivePrFetchForTest, // W-mqtrnp7y00056bc8 — inject a mock live fetch for unit tests
|
|
14305
|
+
_fetchLivePrRecord, // W-mqtrnp7y00056bc8 — exported for direct unit testing
|
|
14169
14306
|
_parseClaudeMcpListLine,
|
|
14170
14307
|
_parseCopilotMcpListJson,
|
|
14171
14308
|
_readWorkspaceMcpServers,
|
|
@@ -14322,6 +14459,13 @@ if (require.main === module) {
|
|
|
14322
14459
|
// boot-time set. See shared.ensureAgentCopilotHome. Fail-open.
|
|
14323
14460
|
try { process.env.COPILOT_HOME = shared.ensureAgentCopilotHome(MINIONS_DIR, CONFIG?.engine); } catch {}
|
|
14324
14461
|
|
|
14462
|
+
// Kick off first npm check on startup, then re-check every 4 hours.
|
|
14463
|
+
// Guarded here so that requiring dashboard as a module (unit tests, _withDashServer)
|
|
14464
|
+
// does not spawn npm child processes that keep the event loop alive past test teardown
|
|
14465
|
+
// and cause exit-without-summary flakiness on the Windows CI runner (W-mqtrby890001cb04).
|
|
14466
|
+
checkNpmVersion().catch(() => {});
|
|
14467
|
+
setInterval(() => checkNpmVersion().catch(() => {}), _getVersionCheckInterval()).unref();
|
|
14468
|
+
|
|
14325
14469
|
// Pre-warm the per-project git-status cache before accepting requests so
|
|
14326
14470
|
// the first /api/status after restart already returns gitState='ok' with a
|
|
14327
14471
|
// real branch instead of the ~8s pending gap that hides the projects-bar
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2267",
|
|
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"
|