symbiot 0.9.0 → 0.9.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/index.mjs +69 -16
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -493,15 +493,58 @@ function loadDeploys() { try { return JSON.parse(readFileSync(join(CONFIG_DIR, "
|
|
|
493
493
|
function ciState(repo, def) {
|
|
494
494
|
if (!sh("command -v gh 2>/dev/null").trim()) return null;
|
|
495
495
|
if (!/github\.com/i.test(sh(`git -C ${JSON.stringify(repo)} remote get-url origin 2>/dev/null`))) return null;
|
|
496
|
-
const j = sh(`cd ${JSON.stringify(repo)} && gh run list --branch ${def} --limit 1 --json conclusion,status
|
|
496
|
+
const j = sh(`cd ${JSON.stringify(repo)} && gh run list --branch ${def} --limit 1 --json databaseId,conclusion,status 2>/dev/null`).trim();
|
|
497
497
|
if (!j) return null;
|
|
498
|
-
try {
|
|
498
|
+
try {
|
|
499
|
+
const a = JSON.parse(j); if (!a.length) return null; const r = a[0];
|
|
499
500
|
if (r.status && r.status !== "completed") return { level: "info", text: `CI on ${def}: ${r.status}` };
|
|
500
|
-
if (r.conclusion === "
|
|
501
|
-
if (r.conclusion
|
|
502
|
-
|
|
501
|
+
if (r.conclusion === "success") return { level: "info", text: `CI on ${def}: passing` };
|
|
502
|
+
if (r.conclusion === "failure") {
|
|
503
|
+
// Deterministic "not running" signal: every job has 0 steps (never started).
|
|
504
|
+
let notRun = false, reason = "";
|
|
505
|
+
try {
|
|
506
|
+
const jobs = (JSON.parse(sh(`cd ${JSON.stringify(repo)} && gh run view ${r.databaseId} --json jobs 2>/dev/null`)).jobs) || [];
|
|
507
|
+
notRun = jobs.length > 0 && jobs.every((x) => ((x.steps || []).length === 0));
|
|
508
|
+
if (notRun && jobs[0] && jobs[0].databaseId) {
|
|
509
|
+
// Only on the not-running path: fetch the one job's annotation to quote
|
|
510
|
+
// GitHub's exact reason (billing / spending limit / runner / disabled).
|
|
511
|
+
const cru = sh(`cd ${JSON.stringify(repo)} && gh api repos/{owner}/{repo}/actions/jobs/${jobs[0].databaseId} --jq .check_run_url 2>/dev/null`).trim();
|
|
512
|
+
const crid = (cru.match(/check-runs\/(\d+)/) || [])[1];
|
|
513
|
+
if (crid) { const m = sh(`cd ${JSON.stringify(repo)} && gh api repos/{owner}/{repo}/check-runs/${crid}/annotations --jq '.[0].message' 2>/dev/null`).trim(); if (m) reason = m.slice(0, 240); }
|
|
514
|
+
}
|
|
515
|
+
} catch {}
|
|
516
|
+
if (notRun) return { level: "warn", text: reason ? `CI is NOT running on ${def}: "${reason}"` : `CI is NOT running on ${def} — jobs never started (reason unavailable; usually a billing/spending-limit stop), not failing tests` };
|
|
517
|
+
return { level: "warn", text: `CI's last run on ${def} failed (a real failure — jobs ran)` };
|
|
518
|
+
}
|
|
519
|
+
if (r.conclusion) return { level: "warn", text: `CI on ${def}: ${r.conclusion}` };
|
|
520
|
+
return null;
|
|
503
521
|
} catch { return null; }
|
|
504
522
|
}
|
|
523
|
+
// For merges that aren't on the default branch, tell "likely never landed"
|
|
524
|
+
// (added files missing from default) from "merged another way" (all present).
|
|
525
|
+
function mergedOffDefault(p, defRef) {
|
|
526
|
+
const raw = sh(`git -C ${JSON.stringify(p)} log --merges --all --not ${defRef} --format='%H|%s' 2>/dev/null`)
|
|
527
|
+
.split("\n").filter((l) => /merge pull request/i.test(l));
|
|
528
|
+
if (!raw.length) return [];
|
|
529
|
+
const defFiles = new Set(sh(`git -C ${JSON.stringify(p)} ls-tree -r --name-only ${defRef} 2>/dev/null`).split("\n").filter(Boolean));
|
|
530
|
+
const hits = [];
|
|
531
|
+
for (const line of raw.slice(0, 12)) {
|
|
532
|
+
const [sha, subj] = line.split("|");
|
|
533
|
+
const prNum = (subj.match(/#(\d+)/) || [])[1] || "?";
|
|
534
|
+
const parents = sh(`git -C ${JSON.stringify(p)} rev-list --parents -n1 ${sha} 2>/dev/null`).trim().split(/\s+/);
|
|
535
|
+
const head = parents[2]; // 2nd parent = the merged branch head
|
|
536
|
+
let added = 0, missing = 0;
|
|
537
|
+
if (head) {
|
|
538
|
+
const mb = sh(`git -C ${JSON.stringify(p)} merge-base ${head} ${defRef} 2>/dev/null`).trim();
|
|
539
|
+
const files = sh(`git -C ${JSON.stringify(p)} diff --name-status ${mb || defRef} ${head} 2>/dev/null`)
|
|
540
|
+
.split("\n").map((l) => l.trim()).filter((l) => /^A\b|^A\t/.test(l)).map((l) => l.split(/\s+/).pop());
|
|
541
|
+
added = files.length;
|
|
542
|
+
for (const f of files) if (f && !defFiles.has(f)) missing++;
|
|
543
|
+
}
|
|
544
|
+
hits.push({ prNum, sha: sha.slice(0, 9), added, missing });
|
|
545
|
+
}
|
|
546
|
+
return hits;
|
|
547
|
+
}
|
|
505
548
|
// Drift facts for ONE repo. Compares against origin/<default> when a remote
|
|
506
549
|
// exists, else the local default branch (so it also works on local-only repos).
|
|
507
550
|
function driftRepo(p, opts = {}) {
|
|
@@ -510,19 +553,28 @@ function driftRepo(p, opts = {}) {
|
|
|
510
553
|
const originDef = sh(`git -C ${JSON.stringify(p)} rev-parse --verify -q refs/remotes/origin/${def} 2>/dev/null`).trim();
|
|
511
554
|
const localDef = sh(`git -C ${JSON.stringify(p)} rev-parse --verify -q refs/heads/${def} 2>/dev/null`).trim();
|
|
512
555
|
const defRef = originDef ? `origin/${def}` : (localDef ? def : "");
|
|
556
|
+
const cmd = opts.deploys && (opts.deploys[p] || opts.deploys[name]);
|
|
557
|
+
// Fetch only when asked, or for a deploy-configured repo (comparing a live
|
|
558
|
+
// deployed sha against a stale origin gives a confidently wrong answer).
|
|
559
|
+
if (originDef && (opts.fetch || cmd)) sh(`git -C ${JSON.stringify(p)} fetch -q origin 2>/dev/null`);
|
|
560
|
+
const fa = fetchAgeDays(p);
|
|
561
|
+
const stale = fa != null && fa > 1 ? ` (as of ${fa}d ago)` : "";
|
|
562
|
+
|
|
513
563
|
if (st.stale) flags.push({ level: "warn", text: `checkout is stale — working tree ≈ ${st.staleBy ? "HEAD~" + st.staleBy : "an older commit"}, not new work` });
|
|
514
564
|
else if (st.dirty) flags.push({ level: "info", text: `${st.dirty} uncommitted (${st.mod} mod / ${st.del} del / ${st.add} new)` });
|
|
515
|
-
if (st.behind) flags.push({ level: "warn", text: `${st.behind} behind upstream on ${st.branch}` });
|
|
565
|
+
if (st.behind) flags.push({ level: "warn", text: `${st.behind} behind upstream on ${st.branch}${stale}` });
|
|
516
566
|
const wc = worktreeCount(p); if (wc > 1) flags.push({ level: "info", text: `${wc} checkouts of this repo` });
|
|
517
567
|
if (originDef) {
|
|
518
568
|
const unmerged = sh(`git -C ${JSON.stringify(p)} branch -r --no-merged origin/${def} 2>/dev/null`).split("\n").map((s) => s.trim()).filter((b) => b && !b.startsWith("origin/HEAD"));
|
|
519
569
|
if (unmerged.length) flags.push({ level: "info", text: `${unmerged.length} branch(es) with work not on ${def}` });
|
|
520
570
|
}
|
|
521
571
|
if (defRef) {
|
|
522
|
-
const
|
|
523
|
-
|
|
572
|
+
const hits = mergedOffDefault(p, defRef);
|
|
573
|
+
const gone = hits.filter((h) => h.added > 0 && h.missing > 0);
|
|
574
|
+
const other = hits.length - gone.length;
|
|
575
|
+
if (gone.length) flags.push({ level: "warn", text: `${gone.length} PR(s) merged off ${def} with added files MISSING from ${def} — likely never landed`, evidence: gone.slice(0, 5).map((h) => `#${h.prNum} (${h.missing}/${h.added} files missing)`).join(" | ") });
|
|
576
|
+
if (other > 0) flags.push({ level: "info", text: `${other} other PR merge(s) off ${def} (files present — probably re-done/squashed)` });
|
|
524
577
|
}
|
|
525
|
-
const cmd = opts.deploys && (opts.deploys[p] || opts.deploys[name]);
|
|
526
578
|
if (cmd && defRef) {
|
|
527
579
|
const sha = sh(cmd).trim().split(/\s+/)[0];
|
|
528
580
|
if (sha) {
|
|
@@ -535,15 +587,15 @@ function driftRepo(p, opts = {}) {
|
|
|
535
587
|
}
|
|
536
588
|
}
|
|
537
589
|
if (opts.ci) { const ci = ciState(p, def); if (ci) flags.push(ci); }
|
|
538
|
-
return { name, path: p, def, flags, fetchAgeDays:
|
|
590
|
+
return { name, path: p, def, flags, fetchAgeDays: fa };
|
|
539
591
|
}
|
|
540
592
|
function computeDrift(opts = {}) {
|
|
541
593
|
const deploys = loadDeploys();
|
|
542
|
-
const repos = findAllRepos(BASE).slice(0, 20).map((r) => driftRepo(r.path, { deploys, ci: opts.ci }));
|
|
594
|
+
const repos = findAllRepos(BASE).slice(0, 20).map((r) => driftRepo(r.path, { deploys, ci: opts.ci, fetch: opts.fetch }));
|
|
543
595
|
return { repos, ci: !!opts.ci };
|
|
544
596
|
}
|
|
545
597
|
function cmdDrift() {
|
|
546
|
-
const d = computeDrift({ ci: has("ci") });
|
|
598
|
+
const d = computeDrift({ ci: has("ci"), fetch: has("fetch") });
|
|
547
599
|
const warn = d.repos.filter((r) => r.flags.some((f) => f.level === "warn"));
|
|
548
600
|
console.log(`\n${c.g("●")} ${c.b("Symbiot drift")} ${c.d("· " + d.repos.length + " repos · " + warn.length + " with risks")}\n`);
|
|
549
601
|
for (const r of d.repos) {
|
|
@@ -974,7 +1026,8 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
974
1026
|
</section>
|
|
975
1027
|
<section id="panel-drift" class="hidden">
|
|
976
1028
|
<div class="row"><span class="muted">What's out of sync, stuck or at risk across your repos — local git facts.</span>
|
|
977
|
-
<label class="muted" style="margin-left:auto"><input type="checkbox" id="
|
|
1029
|
+
<label class="muted" style="margin-left:auto"><input type="checkbox" id="driftfetch"> fetch latest</label>
|
|
1030
|
+
<label class="muted"><input type="checkbox" id="driftci"> check CI (needs gh)</label>
|
|
978
1031
|
<button class="ghost" id="driftrun">Rescan</button></div>
|
|
979
1032
|
<div id="driftout"></div>
|
|
980
1033
|
</section>
|
|
@@ -1088,8 +1141,8 @@ row.querySelector('.taskchk').addEventListener('change',function(){api('/api/tas
|
|
|
1088
1141
|
row.querySelector('.rm').addEventListener('click',function(){api('/api/tasks/remove',{id:id}).then(function(){row.remove();if(!el.querySelector('.task'))renderTasks([]);});});});}
|
|
1089
1142
|
function loadTasks(){api('/api/tasks').then(renderTasks);}
|
|
1090
1143
|
function loadDrift(){var out=document.getElementById('driftout');out.innerHTML="<div class='muted' style='margin-top:12px'>Reading your repos…</div>";
|
|
1091
|
-
var ci=document.getElementById('driftci').checked?'1':'0';
|
|
1092
|
-
api('/api/drift?ci='+ci).then(function(d){driftLoaded=true;var repos=d.repos||[];var risky=repos.filter(function(r){return r.flags.some(function(f){return f.level==='warn';});});
|
|
1144
|
+
var ci=document.getElementById('driftci').checked?'1':'0';var ft=document.getElementById('driftfetch').checked?'1':'0';
|
|
1145
|
+
api('/api/drift?ci='+ci+'&fetch='+ft).then(function(d){driftLoaded=true;var repos=d.repos||[];var risky=repos.filter(function(r){return r.flags.some(function(f){return f.level==='warn';});});
|
|
1093
1146
|
var h="<div class='k' style='margin:10px 0'><b>"+repos.length+"</b> repos · <b>"+risky.length+"</b> with risks</div>";
|
|
1094
1147
|
repos.forEach(function(r){if(!r.flags.length)return;var warn=r.flags.some(function(f){return f.level==='warn';});
|
|
1095
1148
|
h+="<div class='drift'><div class='dh'><span class='"+(warn?'dot-w':'dot-c')+"'></span><span class='dn'>"+esc(r.name)+"</span><span class='dd'>"+esc(r.def)+(r.fetchAgeDays!=null&&r.fetchAgeDays>3?" · fetch "+r.fetchAgeDays+"d old":"")+"</span></div><ul>";
|
|
@@ -1186,7 +1239,7 @@ async function cmdApp() {
|
|
|
1186
1239
|
if (u.pathname === "/api/status") { const r = resolveProvider(); return json(res, r ? { connected: true, line: `${PROVIDERS[r.provider].label} · ${r.model}` } : { connected: false }); }
|
|
1187
1240
|
if (u.pathname === "/api/map") return json(res, buildMap()); // local git only — no AI key needed
|
|
1188
1241
|
if (u.pathname === "/api/models") { const hw = detectHardware(); return json(res, { hardware: hw, rec: recommendModels(hw) }); }
|
|
1189
|
-
if (u.pathname === "/api/drift") return json(res, computeDrift({ ci: u.searchParams.get("ci") === "1" }));
|
|
1242
|
+
if (u.pathname === "/api/drift") return json(res, computeDrift({ ci: u.searchParams.get("ci") === "1", fetch: u.searchParams.get("fetch") === "1" }));
|
|
1190
1243
|
if (u.pathname === "/api/node") return json(res, nodeDetail(u.searchParams.get("id") || "")); // local
|
|
1191
1244
|
if (u.pathname === "/api/suggest" && req.method === "POST") { const b = await readBody(req); return json(res, await repoSuggest(String(b.path || ""))); }
|
|
1192
1245
|
if (u.pathname === "/api/review" && req.method === "POST") { const b = await readBody(req); return json(res, await repoReview(String(b.path || ""))); }
|
package/package.json
CHANGED