pitroom 0.17.0 → 0.19.0
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +28 -0
- package/README.md +23 -3
- package/dist/lib.mjs +1 -1
- package/dist/pitroom.mjs +354 -256
- package/dist/ui/app.js +27 -27
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
package/dist/pitroom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import fs36 from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/core/errors.ts
|
|
7
7
|
var UserError = class extends Error {
|
|
@@ -555,16 +555,33 @@ var codex = {
|
|
|
555
555
|
|
|
556
556
|
// src/backends/gemini/index.ts
|
|
557
557
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
558
|
-
import
|
|
558
|
+
import fs6 from "node:fs";
|
|
559
559
|
import os5 from "node:os";
|
|
560
560
|
import path5 from "node:path";
|
|
561
561
|
import { fileURLToPath } from "node:url";
|
|
562
562
|
|
|
563
563
|
// src/core/store.ts
|
|
564
|
-
import
|
|
564
|
+
import fs5 from "node:fs";
|
|
565
565
|
import os4 from "node:os";
|
|
566
566
|
import path4 from "node:path";
|
|
567
567
|
import crypto from "node:crypto";
|
|
568
|
+
|
|
569
|
+
// src/core/fs-atomic.ts
|
|
570
|
+
import fs4 from "node:fs";
|
|
571
|
+
var BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
572
|
+
function renameOver(tmp, file2) {
|
|
573
|
+
for (let attempt3 = 0; ; attempt3++) {
|
|
574
|
+
try {
|
|
575
|
+
fs4.renameSync(tmp, file2);
|
|
576
|
+
return;
|
|
577
|
+
} catch (e) {
|
|
578
|
+
if (process.platform !== "win32" || !BUSY.has(e.code ?? "") || attempt3 >= 40) throw e;
|
|
579
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/core/store.ts
|
|
568
585
|
var TERMINAL = ["done", "failed", "timeout", "stopped"];
|
|
569
586
|
var isActive = (s) => !TERMINAL.includes(s);
|
|
570
587
|
function home() {
|
|
@@ -591,11 +608,11 @@ function useArchive(hooks) {
|
|
|
591
608
|
archive = { meta: hooks.meta, id: hooks.id };
|
|
592
609
|
}
|
|
593
610
|
function writeMeta(meta) {
|
|
594
|
-
|
|
611
|
+
fs5.mkdirSync(runDir(meta.id), { recursive: true });
|
|
595
612
|
const file2 = runFile(meta.id, "meta.json");
|
|
596
613
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
597
|
-
|
|
598
|
-
|
|
614
|
+
fs5.writeFileSync(tmp, JSON.stringify(meta, null, 2));
|
|
615
|
+
renameOver(tmp, file2);
|
|
599
616
|
if (TERMINAL.includes(meta.state)) {
|
|
600
617
|
try {
|
|
601
618
|
onFinished?.(meta);
|
|
@@ -605,11 +622,11 @@ function writeMeta(meta) {
|
|
|
605
622
|
}
|
|
606
623
|
function readMeta(id) {
|
|
607
624
|
const file2 = runFile(id, "meta.json");
|
|
608
|
-
if (!
|
|
625
|
+
if (!fs5.existsSync(file2)) {
|
|
609
626
|
const kept = archive?.meta(id);
|
|
610
627
|
if (kept) return upgrade(kept);
|
|
611
628
|
}
|
|
612
|
-
return upgrade(JSON.parse(
|
|
629
|
+
return upgrade(JSON.parse(fs5.readFileSync(file2, "utf8")));
|
|
613
630
|
}
|
|
614
631
|
function upgrade(raw) {
|
|
615
632
|
if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
|
|
@@ -622,8 +639,8 @@ function upgrade(raw) {
|
|
|
622
639
|
return raw;
|
|
623
640
|
}
|
|
624
641
|
function listRunIds() {
|
|
625
|
-
if (!
|
|
626
|
-
return
|
|
642
|
+
if (!fs5.existsSync(runsDir())) return [];
|
|
643
|
+
return fs5.readdirSync(runsDir()).filter((d) => fs5.existsSync(runFile(d, "meta.json"))).sort();
|
|
627
644
|
}
|
|
628
645
|
function resolveRun(ref) {
|
|
629
646
|
const ids = listRunIds();
|
|
@@ -649,13 +666,16 @@ function isAlive(pid) {
|
|
|
649
666
|
return e.code === "EPERM";
|
|
650
667
|
}
|
|
651
668
|
}
|
|
669
|
+
var STOP_FILE = "stop-requested";
|
|
670
|
+
var requestStop = (id) => fs5.writeFileSync(runFile(id, STOP_FILE), "");
|
|
652
671
|
function freshMeta(id) {
|
|
653
672
|
const meta = readMeta(id);
|
|
654
673
|
if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
|
|
655
674
|
const latest = readMeta(id);
|
|
656
675
|
if (TERMINAL.includes(latest.state)) return latest;
|
|
657
|
-
|
|
658
|
-
|
|
676
|
+
const stopped = latest.stopRequested || fs5.existsSync(runFile(id, STOP_FILE));
|
|
677
|
+
latest.state = stopped ? "stopped" : "failed";
|
|
678
|
+
if (!stopped) latest.error ??= "worker process exited unexpectedly";
|
|
659
679
|
latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
|
|
660
680
|
writeMeta(latest);
|
|
661
681
|
return latest;
|
|
@@ -787,22 +807,22 @@ var policyDir = () => path5.resolve(path5.dirname(fileURLToPath(import.meta.url)
|
|
|
787
807
|
function workerHome() {
|
|
788
808
|
const root = path5.join(home(), "gemini-home");
|
|
789
809
|
const dir = path5.join(root, ".gemini");
|
|
790
|
-
const base2 = JSON.parse(
|
|
810
|
+
const base2 = JSON.parse(fs6.readFileSync(path5.join(policyDir(), "worker-settings.json"), "utf8"));
|
|
791
811
|
const auth = settings().security?.auth;
|
|
792
812
|
const body = `${JSON.stringify(auth ? { ...base2, security: { ...base2.security, auth } } : base2, null, 2)}
|
|
793
813
|
`;
|
|
794
814
|
const file2 = path5.join(dir, "settings.json");
|
|
795
815
|
try {
|
|
796
|
-
if (
|
|
816
|
+
if (fs6.readFileSync(file2, "utf8") === body) return root;
|
|
797
817
|
} catch {
|
|
798
818
|
}
|
|
799
|
-
|
|
800
|
-
|
|
819
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
820
|
+
fs6.writeFileSync(file2, body);
|
|
801
821
|
for (const name of ["oauth_creds.json", "google_accounts.json"]) {
|
|
802
822
|
const from = path5.join(geminiHome(), name);
|
|
803
823
|
const to = path5.join(dir, name);
|
|
804
824
|
try {
|
|
805
|
-
if (
|
|
825
|
+
if (fs6.existsSync(from) && !fs6.existsSync(to)) fs6.symlinkSync(from, to);
|
|
806
826
|
} catch {
|
|
807
827
|
}
|
|
808
828
|
}
|
|
@@ -829,7 +849,7 @@ function invocation3(req) {
|
|
|
829
849
|
const { command, prefix } = resolveCommand(binary3());
|
|
830
850
|
const env = { GEMINI_CLI_HOME: workerHome() };
|
|
831
851
|
const trusted = path5.join(geminiHome(), "trustedFolders.json");
|
|
832
|
-
if (
|
|
852
|
+
if (fs6.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
|
|
833
853
|
return { command, args: [...prefix, ...args], env };
|
|
834
854
|
}
|
|
835
855
|
function classify3(message) {
|
|
@@ -854,7 +874,7 @@ function geminiHome() {
|
|
|
854
874
|
}
|
|
855
875
|
function settings() {
|
|
856
876
|
try {
|
|
857
|
-
return JSON.parse(
|
|
877
|
+
return JSON.parse(fs6.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
|
|
858
878
|
} catch {
|
|
859
879
|
return {};
|
|
860
880
|
}
|
|
@@ -873,7 +893,7 @@ function doctor3({ models, hasFallback }) {
|
|
|
873
893
|
const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary3()}` }];
|
|
874
894
|
const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
875
895
|
const vertex = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true" || process.env.GOOGLE_GENAI_USE_VERTEXAI === "1";
|
|
876
|
-
const signedIn =
|
|
896
|
+
const signedIn = fs6.existsSync(path5.join(geminiHome(), "oauth_creds.json"));
|
|
877
897
|
const type = settings().security?.auth?.selectedType;
|
|
878
898
|
if (key || vertex) {
|
|
879
899
|
checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
|
|
@@ -1032,7 +1052,7 @@ function describe2(input) {
|
|
|
1032
1052
|
}
|
|
1033
1053
|
|
|
1034
1054
|
// src/backends/opencode/profiles.ts
|
|
1035
|
-
import
|
|
1055
|
+
import fs7 from "node:fs";
|
|
1036
1056
|
import os6 from "node:os";
|
|
1037
1057
|
import path6 from "node:path";
|
|
1038
1058
|
var AGENT = { read: "pitroom-read", write: "pitroom-write" };
|
|
@@ -1143,7 +1163,7 @@ function scratchDirs() {
|
|
|
1143
1163
|
}
|
|
1144
1164
|
function realpath(p) {
|
|
1145
1165
|
try {
|
|
1146
|
-
return
|
|
1166
|
+
return fs7.realpathSync(p);
|
|
1147
1167
|
} catch {
|
|
1148
1168
|
return p;
|
|
1149
1169
|
}
|
|
@@ -1401,10 +1421,10 @@ function getBackend(id) {
|
|
|
1401
1421
|
}
|
|
1402
1422
|
|
|
1403
1423
|
// src/cli/args.ts
|
|
1404
|
-
import
|
|
1424
|
+
import fs9 from "node:fs";
|
|
1405
1425
|
|
|
1406
1426
|
// src/core/config.ts
|
|
1407
|
-
import
|
|
1427
|
+
import fs8 from "node:fs";
|
|
1408
1428
|
import os7 from "node:os";
|
|
1409
1429
|
import path7 from "node:path";
|
|
1410
1430
|
var SCHEMA = {
|
|
@@ -1434,10 +1454,10 @@ function loadConfig() {
|
|
|
1434
1454
|
const file2 = configPath();
|
|
1435
1455
|
const config = {};
|
|
1436
1456
|
const warnings = [];
|
|
1437
|
-
if (
|
|
1457
|
+
if (fs8.existsSync(file2)) {
|
|
1438
1458
|
let raw;
|
|
1439
1459
|
try {
|
|
1440
|
-
raw = JSON.parse(
|
|
1460
|
+
raw = JSON.parse(fs8.readFileSync(file2, "utf8"));
|
|
1441
1461
|
} catch (e) {
|
|
1442
1462
|
warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
|
|
1443
1463
|
}
|
|
@@ -1644,9 +1664,9 @@ function exitCodeFor(meta) {
|
|
|
1644
1664
|
}
|
|
1645
1665
|
function readTask(p) {
|
|
1646
1666
|
const file2 = flag(p, "task-file");
|
|
1647
|
-
if (file2) return
|
|
1667
|
+
if (file2) return fs9.readFileSync(file2, "utf8");
|
|
1648
1668
|
const words = p.positional;
|
|
1649
|
-
if (words.length === 1 && words[0] === "-") return
|
|
1669
|
+
if (words.length === 1 && words[0] === "-") return fs9.readFileSync(0, "utf8");
|
|
1650
1670
|
return words.join(" ");
|
|
1651
1671
|
}
|
|
1652
1672
|
function runOptions(p, task) {
|
|
@@ -1695,13 +1715,13 @@ import readline from "node:readline";
|
|
|
1695
1715
|
|
|
1696
1716
|
// src/core/run.ts
|
|
1697
1717
|
import { spawn as spawn2, spawnSync as spawnSync6 } from "node:child_process";
|
|
1698
|
-
import
|
|
1718
|
+
import fs25 from "node:fs";
|
|
1699
1719
|
import path20 from "node:path";
|
|
1700
1720
|
|
|
1701
1721
|
// src/vcs/git.ts
|
|
1702
1722
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1703
1723
|
import crypto2 from "node:crypto";
|
|
1704
|
-
import
|
|
1724
|
+
import fs10 from "node:fs";
|
|
1705
1725
|
import os8 from "node:os";
|
|
1706
1726
|
import path8 from "node:path";
|
|
1707
1727
|
var SAFE = [
|
|
@@ -1733,16 +1753,23 @@ function must(cwd, args, env) {
|
|
|
1733
1753
|
function gitAvailable() {
|
|
1734
1754
|
return git(process.cwd(), ["--version"]).code === 0;
|
|
1735
1755
|
}
|
|
1756
|
+
function canonical(dir) {
|
|
1757
|
+
try {
|
|
1758
|
+
return fs10.realpathSync.native(dir);
|
|
1759
|
+
} catch {
|
|
1760
|
+
return dir;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1736
1763
|
function repoRoot(dir) {
|
|
1737
1764
|
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
1738
|
-
return r.code === 0 ? path8.resolve(r.stdout.trim()) : void 0;
|
|
1765
|
+
return r.code === 0 ? canonical(path8.resolve(r.stdout.trim())) : void 0;
|
|
1739
1766
|
}
|
|
1740
1767
|
function snapshotTree(root, exclude = [], drop2 = []) {
|
|
1741
1768
|
const tmp = path8.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto2.randomBytes(4).toString("hex")}`);
|
|
1742
1769
|
const real = path8.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
|
|
1743
1770
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
1744
1771
|
try {
|
|
1745
|
-
if (
|
|
1772
|
+
if (fs10.existsSync(real)) fs10.copyFileSync(real, tmp);
|
|
1746
1773
|
else must(root, ["read-tree", "--empty"], env);
|
|
1747
1774
|
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop2.map((p) => `:(top,literal,exclude)${p}`)], env);
|
|
1748
1775
|
if (drop2.length) {
|
|
@@ -1751,8 +1778,8 @@ function snapshotTree(root, exclude = [], drop2 = []) {
|
|
|
1751
1778
|
}
|
|
1752
1779
|
return must(root, ["write-tree"], env).trim();
|
|
1753
1780
|
} finally {
|
|
1754
|
-
|
|
1755
|
-
|
|
1781
|
+
fs10.rmSync(tmp, { force: true });
|
|
1782
|
+
fs10.rmSync(`${tmp}.lock`, { force: true });
|
|
1756
1783
|
}
|
|
1757
1784
|
}
|
|
1758
1785
|
var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
|
|
@@ -1773,9 +1800,9 @@ function diffTrees(root, a, b) {
|
|
|
1773
1800
|
}
|
|
1774
1801
|
function createIsolatedCopy(root, tree, dest) {
|
|
1775
1802
|
const objects = path8.join(path8.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
|
|
1776
|
-
|
|
1803
|
+
fs10.mkdirSync(path8.dirname(dest), { recursive: true });
|
|
1777
1804
|
must(path8.dirname(dest), ["init", "--quiet", dest]);
|
|
1778
|
-
|
|
1805
|
+
fs10.writeFileSync(path8.join(dest, ".git", "objects", "info", "alternates"), `${objects}
|
|
1779
1806
|
`);
|
|
1780
1807
|
must(dest, ["read-tree", tree]);
|
|
1781
1808
|
must(dest, ["checkout-index", "--all", "--force"]);
|
|
@@ -1783,16 +1810,16 @@ function createIsolatedCopy(root, tree, dest) {
|
|
|
1783
1810
|
function removeIsolatedCopy(dest, ownedBy) {
|
|
1784
1811
|
const rel = path8.relative(ownedBy, dest);
|
|
1785
1812
|
if (!rel || rel.startsWith("..") || path8.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
|
|
1786
|
-
|
|
1813
|
+
fs10.rmSync(dest, { recursive: true, force: true });
|
|
1787
1814
|
}
|
|
1788
1815
|
function linkIntoWorktree(root, worktree, rels) {
|
|
1789
1816
|
const linked = [];
|
|
1790
1817
|
for (const rel of rels) {
|
|
1791
1818
|
const src = path8.join(root, rel);
|
|
1792
1819
|
const dst = path8.join(worktree, rel);
|
|
1793
|
-
if (!
|
|
1794
|
-
|
|
1795
|
-
|
|
1820
|
+
if (!fs10.existsSync(src) || fs10.existsSync(dst)) continue;
|
|
1821
|
+
fs10.mkdirSync(path8.dirname(dst), { recursive: true });
|
|
1822
|
+
fs10.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
|
|
1796
1823
|
linked.push(rel);
|
|
1797
1824
|
}
|
|
1798
1825
|
return linked;
|
|
@@ -1898,10 +1925,10 @@ function resolveChain(flags = {}) {
|
|
|
1898
1925
|
|
|
1899
1926
|
// src/core/process.ts
|
|
1900
1927
|
import { spawn } from "node:child_process";
|
|
1901
|
-
import
|
|
1928
|
+
import fs12 from "node:fs";
|
|
1902
1929
|
|
|
1903
1930
|
// src/vcs/guard.ts
|
|
1904
|
-
import
|
|
1931
|
+
import fs11 from "node:fs";
|
|
1905
1932
|
import path9 from "node:path";
|
|
1906
1933
|
var VERSION = 1;
|
|
1907
1934
|
var SHIM = `#!/bin/sh
|
|
@@ -1984,8 +2011,8 @@ function findRealGit(skip) {
|
|
|
1984
2011
|
if (path9.resolve(dir) === skip) continue;
|
|
1985
2012
|
const p = path9.join(dir, "git");
|
|
1986
2013
|
try {
|
|
1987
|
-
|
|
1988
|
-
if (
|
|
2014
|
+
fs11.accessSync(p, fs11.constants.X_OK);
|
|
2015
|
+
if (fs11.statSync(p).isFile()) return p;
|
|
1989
2016
|
} catch {
|
|
1990
2017
|
}
|
|
1991
2018
|
}
|
|
@@ -2000,11 +2027,11 @@ exit 1
|
|
|
2000
2027
|
`;
|
|
2001
2028
|
var hooksDir = () => path9.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
2002
2029
|
function writeIfChanged(file2, content) {
|
|
2003
|
-
if (
|
|
2004
|
-
|
|
2030
|
+
if (fs11.existsSync(file2) && fs11.readFileSync(file2, "utf8") === content) return;
|
|
2031
|
+
fs11.mkdirSync(path9.dirname(file2), { recursive: true });
|
|
2005
2032
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
2006
|
-
|
|
2007
|
-
|
|
2033
|
+
fs11.writeFileSync(tmp, content, { mode: 493 });
|
|
2034
|
+
fs11.renameSync(tmp, file2);
|
|
2008
2035
|
}
|
|
2009
2036
|
function withGitConfig(env, entries) {
|
|
2010
2037
|
const out = { ...env };
|
|
@@ -2040,8 +2067,8 @@ function guardEnv(env) {
|
|
|
2040
2067
|
|
|
2041
2068
|
// src/core/process.ts
|
|
2042
2069
|
async function spawnWorker(inv, opts) {
|
|
2043
|
-
const out =
|
|
2044
|
-
const err =
|
|
2070
|
+
const out = fs12.openSync(opts.stdoutFile, "w");
|
|
2071
|
+
const err = fs12.openSync(opts.stderrFile, "w");
|
|
2045
2072
|
const res = { code: null, timedOut: false, stopped: false };
|
|
2046
2073
|
const child = spawn(inv.command, inv.args, {
|
|
2047
2074
|
cwd: opts.cwd,
|
|
@@ -2064,7 +2091,7 @@ async function spawnWorker(inv, opts) {
|
|
|
2064
2091
|
}, opts.timeoutSec * 1e3);
|
|
2065
2092
|
res.code = await new Promise((resolve2) => {
|
|
2066
2093
|
child.on("error", (e) => {
|
|
2067
|
-
res.spawnError = e.code === "ENOENT" ? `${inv.command} not found` : e.message;
|
|
2094
|
+
res.spawnError = e.code === "ENOENT" ? fs12.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
|
|
2068
2095
|
resolve2(127);
|
|
2069
2096
|
});
|
|
2070
2097
|
child.on("close", (c) => resolve2(c));
|
|
@@ -2073,8 +2100,8 @@ async function spawnWorker(inv, opts) {
|
|
|
2073
2100
|
if (killer) clearTimeout(killer);
|
|
2074
2101
|
process.off("SIGINT", stop);
|
|
2075
2102
|
process.off("SIGTERM", stop);
|
|
2076
|
-
|
|
2077
|
-
|
|
2103
|
+
fs12.closeSync(out);
|
|
2104
|
+
fs12.closeSync(err);
|
|
2078
2105
|
return res;
|
|
2079
2106
|
}
|
|
2080
2107
|
|
|
@@ -2106,7 +2133,7 @@ function parseAudit(text) {
|
|
|
2106
2133
|
}
|
|
2107
2134
|
|
|
2108
2135
|
// src/core/cooldown.ts
|
|
2109
|
-
import
|
|
2136
|
+
import fs13 from "node:fs";
|
|
2110
2137
|
import path10 from "node:path";
|
|
2111
2138
|
var MAX_MS = 24 * 36e5;
|
|
2112
2139
|
var OVERLOAD_MS = 20 * 6e4;
|
|
@@ -2134,18 +2161,18 @@ function cooldownMs(message) {
|
|
|
2134
2161
|
}
|
|
2135
2162
|
function read() {
|
|
2136
2163
|
try {
|
|
2137
|
-
const raw = JSON.parse(
|
|
2164
|
+
const raw = JSON.parse(fs13.readFileSync(file(), "utf8"));
|
|
2138
2165
|
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
2139
2166
|
} catch {
|
|
2140
2167
|
return {};
|
|
2141
2168
|
}
|
|
2142
2169
|
}
|
|
2143
2170
|
function write(all) {
|
|
2144
|
-
|
|
2171
|
+
fs13.mkdirSync(path10.dirname(file()), { recursive: true });
|
|
2145
2172
|
const tmp = `${file()}.${process.pid}.tmp`;
|
|
2146
|
-
|
|
2173
|
+
fs13.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
|
|
2147
2174
|
`);
|
|
2148
|
-
|
|
2175
|
+
renameOver(tmp, file());
|
|
2149
2176
|
}
|
|
2150
2177
|
function activeCooldowns(now = Date.now()) {
|
|
2151
2178
|
return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
|
|
@@ -2173,13 +2200,13 @@ function untilText(c, now = Date.now()) {
|
|
|
2173
2200
|
}
|
|
2174
2201
|
|
|
2175
2202
|
// src/core/snapshot.ts
|
|
2176
|
-
import
|
|
2203
|
+
import fs15 from "node:fs";
|
|
2177
2204
|
import path12 from "node:path";
|
|
2178
2205
|
import crypto3 from "node:crypto";
|
|
2179
2206
|
|
|
2180
2207
|
// src/core/secrets.ts
|
|
2181
2208
|
import { execFileSync } from "node:child_process";
|
|
2182
|
-
import
|
|
2209
|
+
import fs14 from "node:fs";
|
|
2183
2210
|
import path11 from "node:path";
|
|
2184
2211
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
2185
2212
|
var MAX_VISITED = 2e4;
|
|
@@ -2197,7 +2224,7 @@ function findSecretFiles(dir) {
|
|
|
2197
2224
|
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
2198
2225
|
let entries;
|
|
2199
2226
|
try {
|
|
2200
|
-
entries =
|
|
2227
|
+
entries = fs14.readdirSync(d, { withFileTypes: true });
|
|
2201
2228
|
} catch {
|
|
2202
2229
|
return;
|
|
2203
2230
|
}
|
|
@@ -2206,7 +2233,7 @@ function findSecretFiles(dir) {
|
|
|
2206
2233
|
if (e.isDirectory()) {
|
|
2207
2234
|
if (!SKIP_DIRS.has(e.name)) walk(path11.join(d, e.name), depth + 1);
|
|
2208
2235
|
} else if (e.isFile() && looksSecret(e.name)) {
|
|
2209
|
-
found.push(path11.relative(dir, path11.join(d, e.name)));
|
|
2236
|
+
found.push(slash(path11.relative(dir, path11.join(d, e.name))));
|
|
2210
2237
|
}
|
|
2211
2238
|
}
|
|
2212
2239
|
};
|
|
@@ -2216,7 +2243,7 @@ function findSecretFiles(dir) {
|
|
|
2216
2243
|
function findSecretFilesInTree(root, dir) {
|
|
2217
2244
|
try {
|
|
2218
2245
|
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
2219
|
-
return out.split("\0").filter((f) => f && looksSecret(path11.basename(f)) && !path11.relative(dir, path11.join(root, f)).startsWith("..")).map((f) => path11.relative(dir, path11.join(root, f))).sort();
|
|
2246
|
+
return out.split("\0").filter((f) => f && looksSecret(path11.basename(f)) && !path11.relative(dir, path11.join(root, f)).startsWith("..")).map((f) => slash(path11.relative(dir, path11.join(root, f)))).sort();
|
|
2220
2247
|
} catch {
|
|
2221
2248
|
return [];
|
|
2222
2249
|
}
|
|
@@ -2228,6 +2255,7 @@ function secretWarning(files, mode) {
|
|
|
2228
2255
|
const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
|
|
2229
2256
|
return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
|
|
2230
2257
|
}
|
|
2258
|
+
var slash = (p) => p.split(path11.sep).join("/");
|
|
2231
2259
|
|
|
2232
2260
|
// src/core/snapshot.ts
|
|
2233
2261
|
var READ_IN = ["auto", "snapshot", "project"];
|
|
@@ -2245,7 +2273,7 @@ function readSnapshot(root) {
|
|
|
2245
2273
|
const drop2 = findSecretFilesInTree(root, root);
|
|
2246
2274
|
const tree = snapshotTree(root, [], drop2);
|
|
2247
2275
|
const dest = path12.join(snapshotsDir(), tree);
|
|
2248
|
-
if (
|
|
2276
|
+
if (fs15.existsSync(path12.join(dest, ".git"))) {
|
|
2249
2277
|
touch(dest);
|
|
2250
2278
|
return { dir: dest, tree, created: false };
|
|
2251
2279
|
}
|
|
@@ -2254,9 +2282,9 @@ function readSnapshot(root) {
|
|
|
2254
2282
|
createIsolatedCopy(root, tree, tmp);
|
|
2255
2283
|
readOnly(tmp);
|
|
2256
2284
|
try {
|
|
2257
|
-
|
|
2285
|
+
fs15.renameSync(tmp, dest);
|
|
2258
2286
|
} catch (e) {
|
|
2259
|
-
if (!
|
|
2287
|
+
if (!fs15.existsSync(path12.join(dest, ".git"))) throw e;
|
|
2260
2288
|
removeIsolatedCopy(tmp, snapshotsDir());
|
|
2261
2289
|
}
|
|
2262
2290
|
} catch (e) {
|
|
@@ -2272,17 +2300,17 @@ function readSnapshot(root) {
|
|
|
2272
2300
|
function touch(dir) {
|
|
2273
2301
|
try {
|
|
2274
2302
|
const now = /* @__PURE__ */ new Date();
|
|
2275
|
-
|
|
2303
|
+
fs15.utimesSync(dir, now, now);
|
|
2276
2304
|
} catch {
|
|
2277
2305
|
}
|
|
2278
2306
|
}
|
|
2279
2307
|
function readOnly(dir) {
|
|
2280
2308
|
const walk = (d) => {
|
|
2281
|
-
for (const e of
|
|
2309
|
+
for (const e of fs15.readdirSync(d, { withFileTypes: true })) {
|
|
2282
2310
|
if (e.name === ".git") continue;
|
|
2283
2311
|
const p = path12.join(d, e.name);
|
|
2284
2312
|
if (e.isDirectory()) walk(p);
|
|
2285
|
-
else if (e.isFile())
|
|
2313
|
+
else if (e.isFile()) fs15.chmodSync(p, fs15.statSync(p).mode & ~146);
|
|
2286
2314
|
}
|
|
2287
2315
|
};
|
|
2288
2316
|
try {
|
|
@@ -2293,7 +2321,7 @@ function readOnly(dir) {
|
|
|
2293
2321
|
function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
|
|
2294
2322
|
let names;
|
|
2295
2323
|
try {
|
|
2296
|
-
names =
|
|
2324
|
+
names = fs15.readdirSync(snapshotsDir());
|
|
2297
2325
|
} catch {
|
|
2298
2326
|
return 0;
|
|
2299
2327
|
}
|
|
@@ -2308,7 +2336,7 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
2308
2336
|
const entries = names.map((n) => {
|
|
2309
2337
|
const dir = path12.join(snapshotsDir(), n);
|
|
2310
2338
|
try {
|
|
2311
|
-
return { n, dir, age: now -
|
|
2339
|
+
return { n, dir, age: now - fs15.statSync(dir).mtimeMs };
|
|
2312
2340
|
} catch {
|
|
2313
2341
|
return void 0;
|
|
2314
2342
|
}
|
|
@@ -2332,10 +2360,10 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
2332
2360
|
}
|
|
2333
2361
|
function makeWritable(dir) {
|
|
2334
2362
|
const walk = (d) => {
|
|
2335
|
-
for (const e of
|
|
2363
|
+
for (const e of fs15.readdirSync(d, { withFileTypes: true })) {
|
|
2336
2364
|
const p = path12.join(d, e.name);
|
|
2337
2365
|
if (e.isDirectory()) walk(p);
|
|
2338
|
-
else if (e.isFile())
|
|
2366
|
+
else if (e.isFile()) fs15.chmodSync(p, fs15.statSync(p).mode | 128);
|
|
2339
2367
|
}
|
|
2340
2368
|
};
|
|
2341
2369
|
try {
|
|
@@ -2346,7 +2374,7 @@ function makeWritable(dir) {
|
|
|
2346
2374
|
function backToProject(text, snapshotDir, root) {
|
|
2347
2375
|
let real = snapshotDir;
|
|
2348
2376
|
try {
|
|
2349
|
-
real =
|
|
2377
|
+
real = fs15.realpathSync(snapshotDir);
|
|
2350
2378
|
} catch {
|
|
2351
2379
|
}
|
|
2352
2380
|
let out = text;
|
|
@@ -2427,7 +2455,7 @@ function auditBadge(meta) {
|
|
|
2427
2455
|
}
|
|
2428
2456
|
|
|
2429
2457
|
// src/core/plan.ts
|
|
2430
|
-
import
|
|
2458
|
+
import fs16 from "node:fs";
|
|
2431
2459
|
import path13 from "node:path";
|
|
2432
2460
|
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
2433
2461
|
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
@@ -2476,8 +2504,8 @@ function parsePlan(text, file2 = "") {
|
|
|
2476
2504
|
}
|
|
2477
2505
|
function loadPlan(file2) {
|
|
2478
2506
|
const abs = path13.resolve(file2);
|
|
2479
|
-
if (!
|
|
2480
|
-
const plan = parsePlan(
|
|
2507
|
+
if (!fs16.existsSync(abs) || !fs16.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
2508
|
+
const plan = parsePlan(fs16.readFileSync(abs, "utf8"), fs16.realpathSync(abs));
|
|
2481
2509
|
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
2482
2510
|
return plan;
|
|
2483
2511
|
}
|
|
@@ -2499,11 +2527,11 @@ function brief(plan, task) {
|
|
|
2499
2527
|
var planName = (file2) => path13.basename(file2).replace(/\.md$/i, "");
|
|
2500
2528
|
|
|
2501
2529
|
// src/core/templates.ts
|
|
2502
|
-
import
|
|
2530
|
+
import fs18 from "node:fs";
|
|
2503
2531
|
import path15 from "node:path";
|
|
2504
2532
|
|
|
2505
2533
|
// src/core/install.ts
|
|
2506
|
-
import
|
|
2534
|
+
import fs17 from "node:fs";
|
|
2507
2535
|
import os9 from "node:os";
|
|
2508
2536
|
import path14 from "node:path";
|
|
2509
2537
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -2513,18 +2541,30 @@ function packageRoot() {
|
|
|
2513
2541
|
}
|
|
2514
2542
|
function skillNames(root = packageRoot()) {
|
|
2515
2543
|
const dir = path14.join(root, "skills");
|
|
2516
|
-
if (!
|
|
2517
|
-
return
|
|
2544
|
+
if (!fs17.existsSync(dir)) return [];
|
|
2545
|
+
return fs17.readdirSync(dir).filter((d) => fs17.existsSync(path14.join(dir, d, "SKILL.md"))).sort();
|
|
2518
2546
|
}
|
|
2519
2547
|
function skillTargets() {
|
|
2520
2548
|
const home3 = os9.homedir();
|
|
2521
2549
|
const targets = [path14.join(home3, ".agents", "skills")];
|
|
2522
|
-
if (
|
|
2550
|
+
if (fs17.existsSync(path14.join(home3, ".claude"))) targets.push(path14.join(home3, ".claude", "skills"));
|
|
2523
2551
|
return targets;
|
|
2524
2552
|
}
|
|
2525
|
-
var launcherPath = () => path14.join(os9.homedir(), ".local", "bin", "pitroom");
|
|
2553
|
+
var launcherPath = () => path14.join(os9.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
2526
2554
|
var LAUNCHER_MARK = "# pitroom launcher";
|
|
2527
2555
|
function launcherScript(bundle) {
|
|
2556
|
+
if (process.platform === "win32") {
|
|
2557
|
+
const q2 = (v) => v.replace(/%/g, "%%");
|
|
2558
|
+
return `@echo off\r
|
|
2559
|
+
rem ${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)\r
|
|
2560
|
+
if defined PITROOM_NODE (\r
|
|
2561
|
+
"%PITROOM_NODE%" "${q2(bundle)}" %*\r
|
|
2562
|
+
) else (\r
|
|
2563
|
+
"${q2(process.execPath)}" "${q2(bundle)}" %*\r
|
|
2564
|
+
)\r
|
|
2565
|
+
exit /b %ERRORLEVEL%\r
|
|
2566
|
+
`;
|
|
2567
|
+
}
|
|
2528
2568
|
const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
2529
2569
|
return `#!/bin/sh
|
|
2530
2570
|
${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)
|
|
@@ -2540,7 +2580,7 @@ exit 127
|
|
|
2540
2580
|
function isOurLauncher(file2, root) {
|
|
2541
2581
|
if (linksInto(file2, root)) return true;
|
|
2542
2582
|
try {
|
|
2543
|
-
const s =
|
|
2583
|
+
const s = fs17.readFileSync(file2, "utf8");
|
|
2544
2584
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
2545
2585
|
} catch {
|
|
2546
2586
|
return false;
|
|
@@ -2548,42 +2588,42 @@ function isOurLauncher(file2, root) {
|
|
|
2548
2588
|
}
|
|
2549
2589
|
function isPitroomLauncher(file2) {
|
|
2550
2590
|
try {
|
|
2551
|
-
return
|
|
2591
|
+
return fs17.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
2552
2592
|
} catch {
|
|
2553
2593
|
return false;
|
|
2554
2594
|
}
|
|
2555
2595
|
}
|
|
2556
2596
|
function placeLauncher(bundle, root, force) {
|
|
2557
2597
|
const dest = launcherPath();
|
|
2558
|
-
|
|
2559
|
-
const exists =
|
|
2598
|
+
fs17.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
2599
|
+
const exists = fs17.lstatSync(dest, { throwIfNoEntry: false });
|
|
2560
2600
|
if (exists && !isOurLauncher(dest, root)) {
|
|
2561
2601
|
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
2562
|
-
|
|
2602
|
+
fs17.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
2563
2603
|
} else if (exists) {
|
|
2564
|
-
|
|
2604
|
+
fs17.rmSync(dest, { force: true });
|
|
2565
2605
|
}
|
|
2566
|
-
|
|
2606
|
+
fs17.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
2567
2607
|
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
2568
2608
|
}
|
|
2569
2609
|
function linksInto(link, dir) {
|
|
2570
|
-
const st =
|
|
2610
|
+
const st = fs17.lstatSync(link, { throwIfNoEntry: false });
|
|
2571
2611
|
if (!st?.isSymbolicLink()) return false;
|
|
2572
|
-
const target = path14.resolve(path14.dirname(link),
|
|
2612
|
+
const target = path14.resolve(path14.dirname(link), fs17.readlinkSync(link));
|
|
2573
2613
|
const rel = path14.relative(dir, target);
|
|
2574
2614
|
return !rel.startsWith("..") && !path14.isAbsolute(rel);
|
|
2575
2615
|
}
|
|
2576
2616
|
function place(src, dest, opts) {
|
|
2577
|
-
|
|
2578
|
-
const st =
|
|
2617
|
+
fs17.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
2618
|
+
const st = fs17.lstatSync(dest, { throwIfNoEntry: false });
|
|
2579
2619
|
if (st && !st.isSymbolicLink()) {
|
|
2580
2620
|
if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
|
|
2581
|
-
|
|
2621
|
+
fs17.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
2582
2622
|
} else if (st) {
|
|
2583
|
-
|
|
2623
|
+
fs17.unlinkSync(dest);
|
|
2584
2624
|
}
|
|
2585
|
-
if (opts.copy)
|
|
2586
|
-
else
|
|
2625
|
+
if (opts.copy) fs17.cpSync(src, dest, { recursive: true });
|
|
2626
|
+
else fs17.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs17.statSync(src).isDirectory() ? "dir" : "file");
|
|
2587
2627
|
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
2588
2628
|
}
|
|
2589
2629
|
function install(opts) {
|
|
@@ -2595,14 +2635,14 @@ function install(opts) {
|
|
|
2595
2635
|
for (const legacy of LEGACY) {
|
|
2596
2636
|
const l = path14.join(base2, legacy);
|
|
2597
2637
|
if (!skills2.includes(legacy) && linksInto(l, root)) {
|
|
2598
|
-
|
|
2638
|
+
fs17.unlinkSync(l);
|
|
2599
2639
|
out.push(`\u2714 removed old link ${l}`);
|
|
2600
2640
|
}
|
|
2601
2641
|
}
|
|
2602
2642
|
for (const name of skills2) out.push(place(path14.join(root, "skills", name), path14.join(base2, name), opts));
|
|
2603
2643
|
}
|
|
2604
2644
|
const bundle = path14.join(root, "dist", "pitroom.mjs");
|
|
2605
|
-
if (
|
|
2645
|
+
if (fs17.existsSync(bundle)) {
|
|
2606
2646
|
out.push(placeLauncher(bundle, root, opts.force));
|
|
2607
2647
|
const onPath = (process.env.PATH ?? "").split(path14.delimiter).some((d) => path14.resolve(d) === path14.dirname(launcherPath()));
|
|
2608
2648
|
if (!onPath) out.push(`! ${path14.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
|
|
@@ -2613,24 +2653,24 @@ function uninstall() {
|
|
|
2613
2653
|
const root = packageRoot();
|
|
2614
2654
|
const out = [];
|
|
2615
2655
|
for (const base2 of skillTargets()) {
|
|
2616
|
-
if (!
|
|
2617
|
-
for (const name of
|
|
2656
|
+
if (!fs17.existsSync(base2)) continue;
|
|
2657
|
+
for (const name of fs17.readdirSync(base2)) {
|
|
2618
2658
|
const l = path14.join(base2, name);
|
|
2619
2659
|
if (linksInto(l, root)) {
|
|
2620
|
-
|
|
2660
|
+
fs17.unlinkSync(l);
|
|
2621
2661
|
out.push(`\u2714 removed ${l}`);
|
|
2622
2662
|
}
|
|
2623
2663
|
}
|
|
2624
2664
|
}
|
|
2625
2665
|
if (isOurLauncher(launcherPath(), root)) {
|
|
2626
|
-
|
|
2666
|
+
fs17.rmSync(launcherPath(), { force: true });
|
|
2627
2667
|
out.push(`\u2714 removed ${launcherPath()}`);
|
|
2628
2668
|
}
|
|
2629
2669
|
return out.length ? out : ["nothing to remove"];
|
|
2630
2670
|
}
|
|
2631
2671
|
function installedSkills() {
|
|
2632
2672
|
const names = skillNames();
|
|
2633
|
-
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) =>
|
|
2673
|
+
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs17.existsSync(path14.join(base2, n, "SKILL.md"))) }));
|
|
2634
2674
|
}
|
|
2635
2675
|
|
|
2636
2676
|
// src/core/templates.ts
|
|
@@ -2642,8 +2682,8 @@ var FILES = {
|
|
|
2642
2682
|
};
|
|
2643
2683
|
function loadTemplate(name, root = packageRoot()) {
|
|
2644
2684
|
const file2 = path15.join(root, "skills", FILES[name]);
|
|
2645
|
-
if (!
|
|
2646
|
-
return
|
|
2685
|
+
if (!fs18.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
2686
|
+
return fs18.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
2647
2687
|
}
|
|
2648
2688
|
function fill(template, values) {
|
|
2649
2689
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -2653,7 +2693,7 @@ function fill(template, values) {
|
|
|
2653
2693
|
|
|
2654
2694
|
// src/core/slots.ts
|
|
2655
2695
|
import crypto5 from "node:crypto";
|
|
2656
|
-
import
|
|
2696
|
+
import fs19 from "node:fs";
|
|
2657
2697
|
import path16 from "node:path";
|
|
2658
2698
|
var slotsDir = () => path16.join(home(), "slots");
|
|
2659
2699
|
var locksDir = () => path16.join(home(), "locks");
|
|
@@ -2670,23 +2710,23 @@ function holderActive(runId, self) {
|
|
|
2670
2710
|
}
|
|
2671
2711
|
}
|
|
2672
2712
|
function tryClaim(file2, runId) {
|
|
2673
|
-
|
|
2713
|
+
fs19.mkdirSync(path16.dirname(file2), { recursive: true });
|
|
2674
2714
|
try {
|
|
2675
|
-
|
|
2715
|
+
fs19.writeFileSync(file2, runId, { flag: "wx" });
|
|
2676
2716
|
return true;
|
|
2677
2717
|
} catch (e) {
|
|
2678
2718
|
if (e.code !== "EEXIST") throw e;
|
|
2679
2719
|
}
|
|
2680
2720
|
let owner = "";
|
|
2681
2721
|
try {
|
|
2682
|
-
owner =
|
|
2722
|
+
owner = fs19.readFileSync(file2, "utf8").trim();
|
|
2683
2723
|
} catch {
|
|
2684
2724
|
}
|
|
2685
2725
|
if (owner === runId) return true;
|
|
2686
2726
|
if (holderActive(owner, runId)) return false;
|
|
2687
|
-
|
|
2727
|
+
fs19.rmSync(file2, { force: true });
|
|
2688
2728
|
try {
|
|
2689
|
-
|
|
2729
|
+
fs19.writeFileSync(file2, runId, { flag: "wx" });
|
|
2690
2730
|
return true;
|
|
2691
2731
|
} catch {
|
|
2692
2732
|
return false;
|
|
@@ -2695,7 +2735,7 @@ function tryClaim(file2, runId) {
|
|
|
2695
2735
|
function releaseIfOwner(file2, runId) {
|
|
2696
2736
|
if (!file2) return;
|
|
2697
2737
|
try {
|
|
2698
|
-
if (
|
|
2738
|
+
if (fs19.readFileSync(file2, "utf8").trim() === runId) fs19.rmSync(file2, { force: true });
|
|
2699
2739
|
} catch {
|
|
2700
2740
|
}
|
|
2701
2741
|
}
|
|
@@ -2713,7 +2753,7 @@ function acquireWriteLock(repoRoot2, runId) {
|
|
|
2713
2753
|
if (tryClaim(file2, runId)) return;
|
|
2714
2754
|
let owner = "";
|
|
2715
2755
|
try {
|
|
2716
|
-
owner =
|
|
2756
|
+
owner = fs19.readFileSync(file2, "utf8").trim();
|
|
2717
2757
|
} catch {
|
|
2718
2758
|
}
|
|
2719
2759
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -2760,7 +2800,7 @@ ${task}`;
|
|
|
2760
2800
|
}
|
|
2761
2801
|
|
|
2762
2802
|
// src/core/receipt.ts
|
|
2763
|
-
import
|
|
2803
|
+
import fs20 from "node:fs";
|
|
2764
2804
|
import path17 from "node:path";
|
|
2765
2805
|
var PRESETS = {
|
|
2766
2806
|
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
@@ -2797,13 +2837,13 @@ function record(meta) {
|
|
|
2797
2837
|
saved: meta.savedUsd ?? 0,
|
|
2798
2838
|
price: primaryPrice().name
|
|
2799
2839
|
};
|
|
2800
|
-
|
|
2801
|
-
|
|
2840
|
+
fs20.mkdirSync(path17.dirname(ledgerFile()), { recursive: true });
|
|
2841
|
+
fs20.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
2802
2842
|
`);
|
|
2803
2843
|
}
|
|
2804
2844
|
function readLedger(sinceMs2) {
|
|
2805
|
-
if (!
|
|
2806
|
-
return
|
|
2845
|
+
if (!fs20.existsSync(ledgerFile())) return [];
|
|
2846
|
+
return fs20.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2807
2847
|
try {
|
|
2808
2848
|
return [JSON.parse(l)];
|
|
2809
2849
|
} catch {
|
|
@@ -2878,7 +2918,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
2878
2918
|
}
|
|
2879
2919
|
|
|
2880
2920
|
// src/core/refs.ts
|
|
2881
|
-
import
|
|
2921
|
+
import fs21 from "node:fs";
|
|
2882
2922
|
import path18 from "node:path";
|
|
2883
2923
|
var FILE_EXTENSIONS = new Set(
|
|
2884
2924
|
"ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
|
|
@@ -2935,8 +2975,8 @@ function verifyRefs(refs, dirs) {
|
|
|
2935
2975
|
const lineCache = /* @__PURE__ */ new Map();
|
|
2936
2976
|
const load = (file2) => {
|
|
2937
2977
|
if (!lineCache.has(file2)) {
|
|
2938
|
-
const size =
|
|
2939
|
-
const lines = size > MAX_BYTES ? null :
|
|
2978
|
+
const size = fs21.statSync(file2).size;
|
|
2979
|
+
const lines = size > MAX_BYTES ? null : fs21.readFileSync(file2, "utf8").split("\n");
|
|
2940
2980
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
2941
2981
|
lineCache.set(file2, lines);
|
|
2942
2982
|
}
|
|
@@ -2949,7 +2989,7 @@ function verifyRefs(refs, dirs) {
|
|
|
2949
2989
|
if (direct) return [direct];
|
|
2950
2990
|
if (path18.isAbsolute(ref.file)) return [];
|
|
2951
2991
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
2952
|
-
return (byName().get(path18.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path18.basename(f) === wanted).slice(0, 20);
|
|
2992
|
+
return (byName().get(path18.basename(wanted)) ?? []).filter((f) => f.split(path18.sep).join("/").endsWith(`/${wanted}`) || path18.basename(f) === wanted).slice(0, 20);
|
|
2953
2993
|
};
|
|
2954
2994
|
const check = (file2, ref) => {
|
|
2955
2995
|
const lines = load(file2);
|
|
@@ -2994,7 +3034,7 @@ function indexFiles(roots) {
|
|
|
2994
3034
|
const walk = (dir) => {
|
|
2995
3035
|
let entries;
|
|
2996
3036
|
try {
|
|
2997
|
-
entries =
|
|
3037
|
+
entries = fs21.readdirSync(dir, { withFileTypes: true });
|
|
2998
3038
|
} catch {
|
|
2999
3039
|
return;
|
|
3000
3040
|
}
|
|
@@ -3017,7 +3057,7 @@ function resolve(ref, roots) {
|
|
|
3017
3057
|
const candidates = path18.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path18.join(r, ref));
|
|
3018
3058
|
return candidates.find((c) => {
|
|
3019
3059
|
try {
|
|
3020
|
-
return
|
|
3060
|
+
return fs21.statSync(c).isFile() && inside(c, roots);
|
|
3021
3061
|
} catch {
|
|
3022
3062
|
return false;
|
|
3023
3063
|
}
|
|
@@ -3032,13 +3072,13 @@ function inside(file2, roots) {
|
|
|
3032
3072
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3033
3073
|
|
|
3034
3074
|
// src/core/history.ts
|
|
3035
|
-
import
|
|
3075
|
+
import fs23 from "node:fs";
|
|
3036
3076
|
import { createRequire } from "node:module";
|
|
3037
3077
|
import path19 from "node:path";
|
|
3038
3078
|
import zlib from "node:zlib";
|
|
3039
3079
|
|
|
3040
3080
|
// src/core/ui.ts
|
|
3041
|
-
import
|
|
3081
|
+
import fs22 from "node:fs";
|
|
3042
3082
|
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
3043
3083
|
var RECENT = 40;
|
|
3044
3084
|
var RECENT_HOURS = 1;
|
|
@@ -3129,9 +3169,9 @@ function cardsFor(m) {
|
|
|
3129
3169
|
const out = [];
|
|
3130
3170
|
for (const [phase, text] of phases) {
|
|
3131
3171
|
const marker = runFile(m.id, `card-${phase}`);
|
|
3132
|
-
if (
|
|
3172
|
+
if (fs22.existsSync(marker)) continue;
|
|
3133
3173
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
3134
|
-
|
|
3174
|
+
fs22.writeFileSync(marker, "");
|
|
3135
3175
|
}
|
|
3136
3176
|
return out;
|
|
3137
3177
|
}
|
|
@@ -3142,10 +3182,10 @@ function hookCards(input) {
|
|
|
3142
3182
|
const response = event.tool_response;
|
|
3143
3183
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
3144
3184
|
const ids = [...new Set(`${command}
|
|
3145
|
-
${output}`.match(RUN_ID) ?? [])].filter((id) =>
|
|
3185
|
+
${output}`.match(RUN_ID) ?? [])].filter((id) => fs22.existsSync(`${runsDir()}/${id}`));
|
|
3146
3186
|
const since2 = Date.now() - RECENT_HOURS * 36e5;
|
|
3147
3187
|
for (const id of listRunIds().slice(-RECENT)) {
|
|
3148
|
-
if (ids.includes(id) ||
|
|
3188
|
+
if (ids.includes(id) || fs22.existsSync(runFile(id, "card-ended"))) continue;
|
|
3149
3189
|
try {
|
|
3150
3190
|
const m = readMeta(id);
|
|
3151
3191
|
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id);
|
|
@@ -3181,7 +3221,7 @@ function openDb() {
|
|
|
3181
3221
|
} finally {
|
|
3182
3222
|
process.emitWarning = emit;
|
|
3183
3223
|
}
|
|
3184
|
-
|
|
3224
|
+
fs23.mkdirSync(path19.dirname(file2), { recursive: true });
|
|
3185
3225
|
db = new DatabaseSync(file2);
|
|
3186
3226
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
3187
3227
|
migrate(db);
|
|
@@ -3216,30 +3256,30 @@ function migrate(db) {
|
|
|
3216
3256
|
function readRunFile(id, name) {
|
|
3217
3257
|
const f = runFile(id, name);
|
|
3218
3258
|
try {
|
|
3219
|
-
if (
|
|
3220
|
-
if (
|
|
3259
|
+
if (fs23.existsSync(f)) return fs23.readFileSync(f, "utf8");
|
|
3260
|
+
if (fs23.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs23.readFileSync(`${f}.gz`)).toString("utf8");
|
|
3221
3261
|
} catch {
|
|
3222
3262
|
}
|
|
3223
3263
|
return void 0;
|
|
3224
3264
|
}
|
|
3225
3265
|
function gzipFile(f) {
|
|
3226
|
-
if (!
|
|
3227
|
-
|
|
3228
|
-
|
|
3266
|
+
if (!fs23.existsSync(f)) return;
|
|
3267
|
+
fs23.writeFileSync(`${f}.gz`, zlib.gzipSync(fs23.readFileSync(f)));
|
|
3268
|
+
fs23.rmSync(f);
|
|
3229
3269
|
}
|
|
3230
3270
|
function compactRun(meta) {
|
|
3231
3271
|
if (!TERMINAL.includes(meta.state)) return;
|
|
3232
3272
|
const dir = path19.dirname(runFile(meta.id, "meta.json"));
|
|
3233
3273
|
let names;
|
|
3234
3274
|
try {
|
|
3235
|
-
names =
|
|
3275
|
+
names = fs23.readdirSync(dir);
|
|
3236
3276
|
} catch {
|
|
3237
3277
|
return;
|
|
3238
3278
|
}
|
|
3239
3279
|
for (const n of names) {
|
|
3240
3280
|
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path19.join(dir, n));
|
|
3241
3281
|
else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
|
|
3242
|
-
if (meta.state === "done" && !/attempt/.test(n))
|
|
3282
|
+
if (meta.state === "done" && !/attempt/.test(n)) fs23.rmSync(path19.join(dir, n), { force: true });
|
|
3243
3283
|
else gzipFile(path19.join(dir, n));
|
|
3244
3284
|
}
|
|
3245
3285
|
}
|
|
@@ -3395,8 +3435,8 @@ function listHistory(q = {}) {
|
|
|
3395
3435
|
const base2 = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
3396
3436
|
try {
|
|
3397
3437
|
const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
|
|
3398
|
-
const
|
|
3399
|
-
const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict, ${VERIFY_FAILED} AS verify_failed FROM runs r ${
|
|
3438
|
+
const page2 = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
|
|
3439
|
+
const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict, ${VERIFY_FAILED} AS verify_failed FROM runs r ${page2} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
|
|
3400
3440
|
return {
|
|
3401
3441
|
total,
|
|
3402
3442
|
rows: rows.map((r) => ({
|
|
@@ -3493,7 +3533,7 @@ function historyStats(sinceMs2) {
|
|
|
3493
3533
|
}
|
|
3494
3534
|
|
|
3495
3535
|
// src/core/report.ts
|
|
3496
|
-
import
|
|
3536
|
+
import fs24 from "node:fs";
|
|
3497
3537
|
|
|
3498
3538
|
// src/core/style.ts
|
|
3499
3539
|
var enabled = () => {
|
|
@@ -3553,7 +3593,7 @@ function duration(meta) {
|
|
|
3553
3593
|
}
|
|
3554
3594
|
function readSummary(meta) {
|
|
3555
3595
|
const f = runFile(meta.id, "summary.md");
|
|
3556
|
-
return
|
|
3596
|
+
return fs24.existsSync(f) ? fs24.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
|
|
3557
3597
|
}
|
|
3558
3598
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
3559
3599
|
const out = [];
|
|
@@ -3625,7 +3665,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3625
3665
|
}
|
|
3626
3666
|
if (meta.verifyResult) {
|
|
3627
3667
|
const v = meta.verifyResult;
|
|
3628
|
-
const notFound = !v.ok && v.code === 127 && /not found|no such file/i.test(v.tail);
|
|
3668
|
+
const notFound = !v.ok && v.code === 127 && /not found|no such file|is not recognized as an internal or external command/i.test(v.tail);
|
|
3629
3669
|
out.push(`\u2500\u2500 verify: \`${meta.verify}\` ${v.ok ? "\u2714 passed" : notFound ? "\u2718 could not run (exit 127: command not found)" : `\u2718 failed (exit ${v.code})`}`);
|
|
3630
3670
|
if (notFound) out.push(" the command is not on the PATH Pitroom runs with (an app such as an MCP client may start it without your shell's PATH): give its full path, or set PATH in the command");
|
|
3631
3671
|
if (!v.ok && v.tail) out.push(...v.tail.split("\n").map((l) => ` ${l}`));
|
|
@@ -3639,8 +3679,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3639
3679
|
}
|
|
3640
3680
|
function live(meta) {
|
|
3641
3681
|
const f = runFile(meta.id, "events.jsonl");
|
|
3642
|
-
if (!
|
|
3643
|
-
const p = getBackend((meta.ran ?? meta.worker).backend).parse(
|
|
3682
|
+
if (!fs24.existsSync(f)) return { steps: 0, toolCalls: 0 };
|
|
3683
|
+
const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs24.readFileSync(f, "utf8"));
|
|
3644
3684
|
return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
|
|
3645
3685
|
}
|
|
3646
3686
|
function progress(meta) {
|
|
@@ -3651,7 +3691,7 @@ function progress(meta) {
|
|
|
3651
3691
|
}
|
|
3652
3692
|
|
|
3653
3693
|
// src/core/run.ts
|
|
3654
|
-
var VERSION2 = true ? "0.
|
|
3694
|
+
var VERSION2 = true ? "0.19.0" : "0.0.0-dev";
|
|
3655
3695
|
function planWork(o) {
|
|
3656
3696
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
3657
3697
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -3683,7 +3723,7 @@ function prepareRun(o) {
|
|
|
3683
3723
|
parent = readMeta(o.continueFrom);
|
|
3684
3724
|
if (o.worker || o.tier || o.effort) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier/--effort");
|
|
3685
3725
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
3686
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
3726
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs25.existsSync(parent.worktree))) {
|
|
3687
3727
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
3688
3728
|
}
|
|
3689
3729
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -3702,18 +3742,18 @@ function prepareRun(o) {
|
|
|
3702
3742
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
3703
3743
|
}
|
|
3704
3744
|
const mode = parent?.mode ?? o.mode;
|
|
3705
|
-
const dir = path20.resolve(parent?.dir ?? o.dir);
|
|
3706
|
-
if (!
|
|
3745
|
+
const dir = canonical(path20.resolve(parent?.dir ?? o.dir));
|
|
3746
|
+
if (!fs25.existsSync(dir) || !fs25.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
3707
3747
|
const root = repoRoot(dir);
|
|
3708
3748
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
3709
3749
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
3710
3750
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
3711
3751
|
}
|
|
3712
3752
|
const files = o.files.map((f) => path20.resolve(f));
|
|
3713
|
-
for (const f of files) if (!
|
|
3753
|
+
for (const f of files) if (!fs25.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
3714
3754
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
3715
3755
|
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
3716
|
-
if (parent?.snapshot?.dir && !
|
|
3756
|
+
if (parent?.snapshot?.dir && !fs25.existsSync(parent.snapshot.dir)) {
|
|
3717
3757
|
throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
|
|
3718
3758
|
}
|
|
3719
3759
|
if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
@@ -3760,13 +3800,13 @@ function prepareRun(o) {
|
|
|
3760
3800
|
try {
|
|
3761
3801
|
acquireWriteLock(root, meta.id);
|
|
3762
3802
|
} catch (e) {
|
|
3763
|
-
|
|
3803
|
+
fs25.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
3764
3804
|
throw e;
|
|
3765
3805
|
}
|
|
3766
3806
|
}
|
|
3767
|
-
|
|
3807
|
+
fs25.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
3768
3808
|
`);
|
|
3769
|
-
if (work.brief)
|
|
3809
|
+
if (work.brief) fs25.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
3770
3810
|
return meta;
|
|
3771
3811
|
}
|
|
3772
3812
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -3846,7 +3886,7 @@ async function execute(meta) {
|
|
|
3846
3886
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
3847
3887
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
3848
3888
|
const from = runFile(meta.id, f);
|
|
3849
|
-
if (
|
|
3889
|
+
if (fs25.existsSync(from)) fs25.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
3850
3890
|
}
|
|
3851
3891
|
writeMeta(meta);
|
|
3852
3892
|
}
|
|
@@ -3911,7 +3951,7 @@ function prepareTree(meta) {
|
|
|
3911
3951
|
meta.snapshot.dir = s.dir;
|
|
3912
3952
|
meta.snapshot.tree = s.tree;
|
|
3913
3953
|
meta.cwd = path20.join(s.dir, path20.relative(root, meta.dir));
|
|
3914
|
-
if (!
|
|
3954
|
+
if (!fs25.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
3915
3955
|
if (shown) meta.warnings.push(`read in a clean snapshot of your project: the secret-looking files (${shown}) and git-ignored files are not in it; --in-place reads the directory itself`);
|
|
3916
3956
|
} catch (e) {
|
|
3917
3957
|
meta.snapshot = void 0;
|
|
@@ -3946,7 +3986,7 @@ function attempt(meta, backend, target) {
|
|
|
3946
3986
|
timeoutSec: meta.timeoutSec
|
|
3947
3987
|
});
|
|
3948
3988
|
}
|
|
3949
|
-
var read2 = (f) =>
|
|
3989
|
+
var read2 = (f) => fs25.existsSync(f) ? fs25.readFileSync(f, "utf8") : "";
|
|
3950
3990
|
function retryableFailure(meta, backend, res) {
|
|
3951
3991
|
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
3952
3992
|
if (run2.usage.steps > 0) return void 0;
|
|
@@ -3966,12 +4006,12 @@ function finalize(meta, res) {
|
|
|
3966
4006
|
if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
3967
4007
|
meta.sessionId = run2.sessionId ?? meta.sessionId;
|
|
3968
4008
|
meta.usage = run2.usage;
|
|
3969
|
-
|
|
4009
|
+
fs25.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
|
|
3970
4010
|
`);
|
|
3971
4011
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
|
|
3972
4012
|
if (meta.reviewOf) {
|
|
3973
4013
|
meta.verdict = parseVerdict(run2.finalText);
|
|
3974
|
-
if (meta.packageFile)
|
|
4014
|
+
if (meta.packageFile) fs25.rmSync(meta.packageFile, { force: true });
|
|
3975
4015
|
}
|
|
3976
4016
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
3977
4017
|
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
@@ -4019,7 +4059,7 @@ function captureChanges(meta) {
|
|
|
4019
4059
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4020
4060
|
meta.changes = d.changes;
|
|
4021
4061
|
meta.stats = d.stats;
|
|
4022
|
-
|
|
4062
|
+
fs25.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4023
4063
|
}
|
|
4024
4064
|
function verifyPath() {
|
|
4025
4065
|
const extra = [path20.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
@@ -4038,8 +4078,9 @@ function runVerify(meta) {
|
|
|
4038
4078
|
env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4039
4079
|
});
|
|
4040
4080
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4041
|
-
|
|
4042
|
-
|
|
4081
|
+
fs25.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4082
|
+
const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
|
|
4083
|
+
return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4043
4084
|
}
|
|
4044
4085
|
function applyRun(meta, allowDelete = false) {
|
|
4045
4086
|
if (meta.mode !== "isolate") throw new UserError(`run ${meta.id} edited your tree directly (${meta.mode}); nothing to apply`);
|
|
@@ -4075,6 +4116,15 @@ ${res.message}`, 1);
|
|
|
4075
4116
|
writeMeta(meta);
|
|
4076
4117
|
return `reverted ${meta.changes.length} file(s) changed by ${meta.id}`;
|
|
4077
4118
|
}
|
|
4119
|
+
function stopRun(id) {
|
|
4120
|
+
const meta = freshMeta(id);
|
|
4121
|
+
if (!isActive(meta.state) || !isAlive(meta.pid)) return void 0;
|
|
4122
|
+
requestStop(meta.id);
|
|
4123
|
+
meta.stopRequested = true;
|
|
4124
|
+
writeMeta(meta);
|
|
4125
|
+
process.kill(meta.pid, "SIGTERM");
|
|
4126
|
+
return meta;
|
|
4127
|
+
}
|
|
4078
4128
|
function discardRun(meta) {
|
|
4079
4129
|
if (meta.mode !== "isolate") throw new UserError("only --isolate runs have an isolated copy to discard");
|
|
4080
4130
|
cleanupWorktree(meta);
|
|
@@ -4083,11 +4133,11 @@ function discardRun(meta) {
|
|
|
4083
4133
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4084
4134
|
}
|
|
4085
4135
|
function cleanupWorktree(meta) {
|
|
4086
|
-
if (meta.worktree &&
|
|
4136
|
+
if (meta.worktree && fs25.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4087
4137
|
}
|
|
4088
4138
|
|
|
4089
4139
|
// src/cli/mcp-extras.ts
|
|
4090
|
-
import
|
|
4140
|
+
import fs26 from "node:fs";
|
|
4091
4141
|
import path21 from "node:path";
|
|
4092
4142
|
|
|
4093
4143
|
// src/cli/mcp-support.ts
|
|
@@ -4447,7 +4497,7 @@ function skillPrompts() {
|
|
|
4447
4497
|
const root = packageRoot();
|
|
4448
4498
|
for (const name of skillNames(root)) {
|
|
4449
4499
|
try {
|
|
4450
|
-
const text =
|
|
4500
|
+
const text = fs26.readFileSync(path21.join(root, "skills", name, "SKILL.md"), "utf8");
|
|
4451
4501
|
const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
4452
4502
|
const body = (front ? text.slice(front[0].length) : text).trim();
|
|
4453
4503
|
const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
|
|
@@ -4964,14 +5014,14 @@ async function serveMcp() {
|
|
|
4964
5014
|
|
|
4965
5015
|
// src/cli/mcp-http.ts
|
|
4966
5016
|
import crypto7 from "node:crypto";
|
|
4967
|
-
import
|
|
5017
|
+
import fs28 from "node:fs";
|
|
4968
5018
|
import http2 from "node:http";
|
|
4969
5019
|
import path23 from "node:path";
|
|
4970
5020
|
|
|
4971
5021
|
// src/core/dash.ts
|
|
4972
5022
|
import { spawn as spawn4 } from "node:child_process";
|
|
4973
5023
|
import crypto6 from "node:crypto";
|
|
4974
|
-
import
|
|
5024
|
+
import fs27 from "node:fs";
|
|
4975
5025
|
import http from "node:http";
|
|
4976
5026
|
import path22 from "node:path";
|
|
4977
5027
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
@@ -5091,9 +5141,9 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
5091
5141
|
// src/core/dash-page.ts
|
|
5092
5142
|
var THEME_SCRIPT = "(function(){try{var t=localStorage.getItem('pitroom-theme');var d=t?t==='dark':matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d)}catch(e){}})()";
|
|
5093
5143
|
var ICON3 = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23ff6a2b'/%3E%3Cpath d='M9 8h4v4H9zm8 0h4v4h-4zm-4 4h4v4h-4zm8 0h4v4h-4zM9 16h4v4H9zm8 0h4v4h-4zm-4 4h4v4h-4zm8 0h4v4h-4z' fill='%23fff'/%3E%3C/svg%3E";
|
|
5094
|
-
var
|
|
5144
|
+
var page = (token) => `<!doctype html>
|
|
5095
5145
|
<html lang="en" class="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5096
|
-
<meta name="color-scheme" content="dark light"><title>Pitroom</title><link rel="icon" href="${ICON3}">
|
|
5146
|
+
<meta name="color-scheme" content="dark light"><meta name="pitroom-token" content="${token}"><title>Pitroom</title><link rel="icon" href="${ICON3}">
|
|
5097
5147
|
<script>${THEME_SCRIPT}</script><link rel="stylesheet" href="/assets/app.css"></head>
|
|
5098
5148
|
<body><div id="root"></div><script type="module" src="/assets/app.js"></script></body></html>`;
|
|
5099
5149
|
var MISSING = `<!doctype html><meta charset="utf-8"><title>Pitroom</title><body style="font:15px system-ui;padding:3rem;max-width:40rem;margin:auto"><h1>Pitroom dash</h1><p>The dashboard files are missing (dist/ui). Run <code>npm run build</code> in the Pitroom repository, or reinstall the package.</p></body>`;
|
|
@@ -5114,6 +5164,7 @@ function findings(m) {
|
|
|
5114
5164
|
const parts = [v.critical && `${v.critical} critical`, v.important && `${v.important} important`, v.minor && `${v.minor} minor`].filter(Boolean);
|
|
5115
5165
|
return parts.length ? parts.join(" \xB7 ") : "no findings";
|
|
5116
5166
|
}
|
|
5167
|
+
var canDiscard = (m) => m.mode === "isolate" && !isActive(m.state) && !m.applied && !m.discarded;
|
|
5117
5168
|
function toRun(m) {
|
|
5118
5169
|
const l = m.state === "running" ? live(m) : void 0;
|
|
5119
5170
|
return {
|
|
@@ -5134,6 +5185,7 @@ function toRun(m) {
|
|
|
5134
5185
|
audit: auditBadge(m),
|
|
5135
5186
|
changes: m.changes?.length || void 0,
|
|
5136
5187
|
applied: m.applied || void 0,
|
|
5188
|
+
discardable: canDiscard(m) || void 0,
|
|
5137
5189
|
note: l?.last ? oneLine7(String(l.last), 140) : isActive(m.state) ? "" : m.verifyResult && !m.verifyResult.ok ? `verify failed: ${m.verify ?? ""}` : m.verdict ? findings(m) : m.auditOf && m.state === "done" ? m.auditDisputed?.length ? `${m.auditDisputed.length} disputed` : "nothing disputed" : headline(m, 200) || oneLine7(m.error ?? "", 200)
|
|
5138
5190
|
};
|
|
5139
5191
|
}
|
|
@@ -5239,28 +5291,54 @@ var assetCache = /* @__PURE__ */ new Map();
|
|
|
5239
5291
|
function readAsset(route) {
|
|
5240
5292
|
try {
|
|
5241
5293
|
const file2 = path22.join(assetsDir(), path22.basename(route));
|
|
5242
|
-
const { mtimeMs } =
|
|
5294
|
+
const { mtimeMs } = fs27.statSync(file2);
|
|
5243
5295
|
const hit = assetCache.get(route);
|
|
5244
5296
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
5245
|
-
const data =
|
|
5297
|
+
const data = fs27.readFileSync(file2);
|
|
5246
5298
|
assetCache.set(route, { mtimeMs, data });
|
|
5247
5299
|
return data;
|
|
5248
5300
|
} catch {
|
|
5249
5301
|
return void 0;
|
|
5250
5302
|
}
|
|
5251
5303
|
}
|
|
5252
|
-
function handler(touch2) {
|
|
5304
|
+
function handler(touch2, token) {
|
|
5253
5305
|
const send = (res, code, type, body) => {
|
|
5254
5306
|
res.writeHead(code, { ...HEADERS, "content-type": type });
|
|
5255
5307
|
res.end(body);
|
|
5256
5308
|
};
|
|
5257
5309
|
const json = (res, code, value) => send(res, code, "application/json; charset=utf-8", JSON.stringify(value));
|
|
5310
|
+
const tokenBuf = Buffer.from(token);
|
|
5311
|
+
const act = (req, res, pathname) => {
|
|
5312
|
+
req.resume();
|
|
5313
|
+
const given = Buffer.from(String(req.headers["x-pitroom-token"] ?? ""));
|
|
5314
|
+
if (given.length !== tokenBuf.length || !crypto6.timingSafeEqual(given, tokenBuf)) return json(res, 403, { error: "forbidden" });
|
|
5315
|
+
if (req.headers.origin !== `http://${req.headers.host}`) return json(res, 403, { error: "forbidden origin" });
|
|
5316
|
+
const m = /^\/api\/run\/([^/]+)\/(stop|discard)$/.exec(pathname);
|
|
5317
|
+
if (!m) return json(res, 404, { error: "not found" });
|
|
5318
|
+
let meta;
|
|
5319
|
+
try {
|
|
5320
|
+
if (!RUN_ID3.test(m[1])) throw new Error("bad id");
|
|
5321
|
+
meta = freshMeta(m[1]);
|
|
5322
|
+
} catch {
|
|
5323
|
+
return json(res, 404, { error: "no such run" });
|
|
5324
|
+
}
|
|
5325
|
+
try {
|
|
5326
|
+
if (m[2] === "stop") {
|
|
5327
|
+
return stopRun(meta.id) ? json(res, 200, { ok: true, message: `stopping ${meta.id}` }) : json(res, 409, { error: "run is not active" });
|
|
5328
|
+
}
|
|
5329
|
+
if (!canDiscard(meta)) return json(res, 409, { error: "only a finished, not yet applied isolate run can be discarded" });
|
|
5330
|
+
return json(res, 200, { ok: true, message: discardRun(meta) });
|
|
5331
|
+
} catch (e) {
|
|
5332
|
+
return json(res, 409, { error: e.message });
|
|
5333
|
+
}
|
|
5334
|
+
};
|
|
5258
5335
|
return (req, res) => {
|
|
5259
5336
|
touch2();
|
|
5260
5337
|
if (!LOCAL_HOST.test(req.headers.host ?? "")) return json(res, 403, { error: "forbidden host" });
|
|
5261
|
-
if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
|
|
5262
5338
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5263
|
-
if (
|
|
5339
|
+
if (req.method === "POST") return act(req, res, url.pathname);
|
|
5340
|
+
if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
|
|
5341
|
+
if (url.pathname === "/") return send(res, 200, "text/html; charset=utf-8", readAsset("/assets/app.js") ? page(token) : MISSING);
|
|
5264
5342
|
const type = ASSETS[url.pathname];
|
|
5265
5343
|
if (type) {
|
|
5266
5344
|
const data = readAsset(url.pathname);
|
|
@@ -5301,7 +5379,7 @@ function handler(touch2) {
|
|
|
5301
5379
|
}
|
|
5302
5380
|
function startDash(opts) {
|
|
5303
5381
|
let lastRequest = Date.now();
|
|
5304
|
-
const server = http.createServer(handler(() => lastRequest = Date.now()));
|
|
5382
|
+
const server = http.createServer(handler(() => lastRequest = Date.now(), crypto6.randomBytes(24).toString("hex")));
|
|
5305
5383
|
return new Promise((resolve2, reject) => {
|
|
5306
5384
|
server.once("error", reject);
|
|
5307
5385
|
server.listen(opts.port, "127.0.0.1", () => {
|
|
@@ -5332,13 +5410,17 @@ async function ping(port) {
|
|
|
5332
5410
|
}
|
|
5333
5411
|
}
|
|
5334
5412
|
async function runningDash() {
|
|
5413
|
+
let r;
|
|
5335
5414
|
try {
|
|
5336
|
-
|
|
5337
|
-
if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
|
|
5415
|
+
r = JSON.parse(fs27.readFileSync(registryFile(), "utf8"));
|
|
5338
5416
|
} catch {
|
|
5417
|
+
return void 0;
|
|
5339
5418
|
}
|
|
5340
|
-
|
|
5341
|
-
|
|
5419
|
+
if (!isAlive(r.pid)) {
|
|
5420
|
+
fs27.rmSync(registryFile(), { force: true });
|
|
5421
|
+
return void 0;
|
|
5422
|
+
}
|
|
5423
|
+
return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
|
|
5342
5424
|
}
|
|
5343
5425
|
function openBrowser(url) {
|
|
5344
5426
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
@@ -5352,7 +5434,7 @@ async function dashCommand(o) {
|
|
|
5352
5434
|
if (o.stop) {
|
|
5353
5435
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
5354
5436
|
process.kill(existing.pid, "SIGTERM");
|
|
5355
|
-
|
|
5437
|
+
fs27.rmSync(registryFile(), { force: true });
|
|
5356
5438
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
5357
5439
|
return 0;
|
|
5358
5440
|
}
|
|
@@ -5369,7 +5451,7 @@ async function dashCommand(o) {
|
|
|
5369
5451
|
});
|
|
5370
5452
|
child.on("error", () => void 0);
|
|
5371
5453
|
child.unref();
|
|
5372
|
-
for (let i = 0; i <
|
|
5454
|
+
for (let i = 0; i < 200; i++) {
|
|
5373
5455
|
await new Promise((r2) => setTimeout(r2, 100));
|
|
5374
5456
|
const r = await runningDash();
|
|
5375
5457
|
if (r && r.pid === child.pid) {
|
|
@@ -5389,8 +5471,10 @@ async function dashCommand(o) {
|
|
|
5389
5471
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
5390
5472
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
5391
5473
|
}
|
|
5392
|
-
|
|
5393
|
-
|
|
5474
|
+
fs27.mkdirSync(home(), { recursive: true });
|
|
5475
|
+
const tmp = `${registryFile()}.${process.pid}.tmp`;
|
|
5476
|
+
fs27.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
5477
|
+
renameOver(tmp, registryFile());
|
|
5394
5478
|
const stop = () => void dash.close();
|
|
5395
5479
|
process.once("SIGINT", stop);
|
|
5396
5480
|
process.once("SIGTERM", stop);
|
|
@@ -5399,7 +5483,7 @@ async function dashCommand(o) {
|
|
|
5399
5483
|
if (o.open) openBrowser(dash.url);
|
|
5400
5484
|
await dash.closed;
|
|
5401
5485
|
try {
|
|
5402
|
-
if (JSON.parse(
|
|
5486
|
+
if (JSON.parse(fs27.readFileSync(registryFile(), "utf8")).pid === process.pid) fs27.rmSync(registryFile(), { force: true });
|
|
5403
5487
|
} catch {
|
|
5404
5488
|
}
|
|
5405
5489
|
return 0;
|
|
@@ -5414,7 +5498,7 @@ var PATH = "/mcp";
|
|
|
5414
5498
|
var tokenFile = () => path23.join(home(), "mcp-token");
|
|
5415
5499
|
var readToken = (file2) => {
|
|
5416
5500
|
try {
|
|
5417
|
-
const kept =
|
|
5501
|
+
const kept = fs28.readFileSync(file2, "utf8").trim();
|
|
5418
5502
|
return kept.length >= 16 ? kept : void 0;
|
|
5419
5503
|
} catch {
|
|
5420
5504
|
return void 0;
|
|
@@ -5429,10 +5513,10 @@ function mcpToken() {
|
|
|
5429
5513
|
const file2 = tokenFile();
|
|
5430
5514
|
let token = readToken(file2);
|
|
5431
5515
|
if (!token) {
|
|
5432
|
-
|
|
5516
|
+
fs28.mkdirSync(path23.dirname(file2), { recursive: true });
|
|
5433
5517
|
token = crypto7.randomBytes(32).toString("hex");
|
|
5434
5518
|
try {
|
|
5435
|
-
|
|
5519
|
+
fs28.writeFileSync(file2, `${token}
|
|
5436
5520
|
`, { mode: 384, flag: "wx" });
|
|
5437
5521
|
} catch (e) {
|
|
5438
5522
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -5441,7 +5525,7 @@ function mcpToken() {
|
|
|
5441
5525
|
}
|
|
5442
5526
|
}
|
|
5443
5527
|
try {
|
|
5444
|
-
|
|
5528
|
+
fs28.chmodSync(file2, 384);
|
|
5445
5529
|
} catch {
|
|
5446
5530
|
}
|
|
5447
5531
|
return { token, from: file2 };
|
|
@@ -5623,16 +5707,16 @@ async function serveMcpHttp(opts) {
|
|
|
5623
5707
|
|
|
5624
5708
|
// src/cli/commands.ts
|
|
5625
5709
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5626
|
-
import
|
|
5710
|
+
import fs34 from "node:fs";
|
|
5627
5711
|
import path29 from "node:path";
|
|
5628
5712
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5629
5713
|
|
|
5630
5714
|
// src/core/init.ts
|
|
5631
|
-
import
|
|
5715
|
+
import fs29 from "node:fs";
|
|
5632
5716
|
import path24 from "node:path";
|
|
5633
5717
|
var FREE = /-free$/;
|
|
5634
5718
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
5635
|
-
var isFound = (binary5) => path24.isAbsolute(binary5) &&
|
|
5719
|
+
var isFound = (binary5) => path24.isAbsolute(binary5) && fs29.existsSync(binary5);
|
|
5636
5720
|
function planInit(opts = {}) {
|
|
5637
5721
|
const file2 = configPath();
|
|
5638
5722
|
const workers = backendIds().map((id) => {
|
|
@@ -5678,15 +5762,15 @@ function planInit(opts = {}) {
|
|
|
5678
5762
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
5679
5763
|
if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
|
|
5680
5764
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
5681
|
-
return { path: file2, exists:
|
|
5765
|
+
return { path: file2, exists: fs29.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
5682
5766
|
}
|
|
5683
5767
|
function writeInit(plan, force) {
|
|
5684
5768
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
5685
5769
|
if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
|
|
5686
5770
|
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path24.basename(plan.path)}.bak)`);
|
|
5687
|
-
|
|
5688
|
-
if (plan.exists)
|
|
5689
|
-
|
|
5771
|
+
fs29.mkdirSync(path24.dirname(plan.path), { recursive: true });
|
|
5772
|
+
if (plan.exists) fs29.copyFileSync(plan.path, `${plan.path}.bak`);
|
|
5773
|
+
fs29.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
|
|
5690
5774
|
`);
|
|
5691
5775
|
return plan.path;
|
|
5692
5776
|
}
|
|
@@ -5707,7 +5791,7 @@ function formatInit(plan, written) {
|
|
|
5707
5791
|
|
|
5708
5792
|
// src/core/mcp-install.ts
|
|
5709
5793
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5710
|
-
import
|
|
5794
|
+
import fs30 from "node:fs";
|
|
5711
5795
|
import os10 from "node:os";
|
|
5712
5796
|
import path25 from "node:path";
|
|
5713
5797
|
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
@@ -5717,7 +5801,7 @@ function mcpCommand() {
|
|
|
5717
5801
|
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
5718
5802
|
for (const dir of (process.env.PATH ?? "").split(path25.delimiter).filter(Boolean)) {
|
|
5719
5803
|
const p = path25.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
5720
|
-
if (
|
|
5804
|
+
if (fs30.existsSync(p)) return { command: path25.resolve(p), args: ["mcp"] };
|
|
5721
5805
|
}
|
|
5722
5806
|
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
5723
5807
|
}
|
|
@@ -5733,7 +5817,7 @@ function run(backendId, args) {
|
|
|
5733
5817
|
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
5734
5818
|
var readJson = (file2) => {
|
|
5735
5819
|
try {
|
|
5736
|
-
return JSON.parse(
|
|
5820
|
+
return JSON.parse(fs30.readFileSync(file2, "utf8"));
|
|
5737
5821
|
} catch {
|
|
5738
5822
|
return void 0;
|
|
5739
5823
|
}
|
|
@@ -5766,29 +5850,29 @@ function jsonClient(id, name, file2, dir) {
|
|
|
5766
5850
|
return {
|
|
5767
5851
|
id,
|
|
5768
5852
|
name,
|
|
5769
|
-
found: () =>
|
|
5853
|
+
found: () => fs30.existsSync(dir()),
|
|
5770
5854
|
state: (c) => jsonState(file2(), c),
|
|
5771
5855
|
add: (c) => {
|
|
5772
5856
|
let config = {};
|
|
5773
|
-
if (
|
|
5857
|
+
if (fs30.existsSync(file2())) {
|
|
5774
5858
|
config = readJson(file2());
|
|
5775
5859
|
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
5776
5860
|
const backup = `${file2()}.bak-pitroom`;
|
|
5777
|
-
if (!
|
|
5861
|
+
if (!fs30.existsSync(backup)) fs30.copyFileSync(file2(), backup);
|
|
5778
5862
|
}
|
|
5779
5863
|
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
5780
|
-
|
|
5864
|
+
fs30.mkdirSync(path25.dirname(file2()), { recursive: true });
|
|
5781
5865
|
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
5782
|
-
|
|
5866
|
+
fs30.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
5783
5867
|
`);
|
|
5784
|
-
|
|
5868
|
+
fs30.renameSync(tmp, file2());
|
|
5785
5869
|
return { ok: true, message: rel() };
|
|
5786
5870
|
},
|
|
5787
5871
|
remove: () => {
|
|
5788
5872
|
const config = readJson(file2());
|
|
5789
5873
|
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
5790
5874
|
delete config.mcpServers[NAME];
|
|
5791
|
-
|
|
5875
|
+
fs30.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
5792
5876
|
`);
|
|
5793
5877
|
return { ok: true, message: rel() };
|
|
5794
5878
|
}
|
|
@@ -5802,7 +5886,7 @@ function desktopFile() {
|
|
|
5802
5886
|
function codexState(c) {
|
|
5803
5887
|
let text;
|
|
5804
5888
|
try {
|
|
5805
|
-
text =
|
|
5889
|
+
text = fs30.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
5806
5890
|
} catch {
|
|
5807
5891
|
return "absent";
|
|
5808
5892
|
}
|
|
@@ -5906,7 +5990,7 @@ var parseClients = (value) => {
|
|
|
5906
5990
|
|
|
5907
5991
|
// src/core/plan-status.ts
|
|
5908
5992
|
import crypto8 from "node:crypto";
|
|
5909
|
-
import
|
|
5993
|
+
import fs31 from "node:fs";
|
|
5910
5994
|
import path26 from "node:path";
|
|
5911
5995
|
function notesFile(plan) {
|
|
5912
5996
|
const id = crypto8.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
@@ -5914,8 +5998,8 @@ function notesFile(plan) {
|
|
|
5914
5998
|
}
|
|
5915
5999
|
function readNotes(plan) {
|
|
5916
6000
|
const f = notesFile(plan);
|
|
5917
|
-
if (!
|
|
5918
|
-
return
|
|
6001
|
+
if (!fs31.existsSync(f)) return [];
|
|
6002
|
+
return fs31.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
5919
6003
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
5920
6004
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
5921
6005
|
});
|
|
@@ -5923,11 +6007,11 @@ function readNotes(plan) {
|
|
|
5923
6007
|
function addNote(planFile, text) {
|
|
5924
6008
|
const plan = loadPlan(planFile);
|
|
5925
6009
|
const f = notesFile(plan);
|
|
5926
|
-
|
|
5927
|
-
if (!
|
|
6010
|
+
fs31.mkdirSync(path26.dirname(f), { recursive: true });
|
|
6011
|
+
if (!fs31.existsSync(f)) fs31.writeFileSync(f, `# plan: ${plan.file}
|
|
5928
6012
|
`);
|
|
5929
6013
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
5930
|
-
|
|
6014
|
+
fs31.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
5931
6015
|
`);
|
|
5932
6016
|
return f;
|
|
5933
6017
|
}
|
|
@@ -5995,13 +6079,13 @@ ${rulings}`,
|
|
|
5995
6079
|
|
|
5996
6080
|
// src/core/cache.ts
|
|
5997
6081
|
import crypto9 from "node:crypto";
|
|
5998
|
-
import
|
|
6082
|
+
import fs32 from "node:fs";
|
|
5999
6083
|
import path27 from "node:path";
|
|
6000
6084
|
var SCAN2 = 500;
|
|
6001
6085
|
function cacheKey(o) {
|
|
6002
6086
|
if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
|
|
6003
6087
|
if (!(effective().cacheDays.value > 0)) return void 0;
|
|
6004
|
-
const dir = path27.resolve(o.dir);
|
|
6088
|
+
const dir = canonical(path27.resolve(o.dir));
|
|
6005
6089
|
const root = repoRoot(dir);
|
|
6006
6090
|
if (!root) return void 0;
|
|
6007
6091
|
try {
|
|
@@ -6009,7 +6093,7 @@ function cacheKey(o) {
|
|
|
6009
6093
|
const head = commitOf(root, "HEAD") ?? "";
|
|
6010
6094
|
const files = o.files.map((f) => {
|
|
6011
6095
|
const abs = path27.resolve(f);
|
|
6012
|
-
return [abs, crypto9.createHash("sha256").update(
|
|
6096
|
+
return [abs, crypto9.createHash("sha256").update(fs32.readFileSync(abs)).digest("hex")];
|
|
6013
6097
|
});
|
|
6014
6098
|
const question = o.task.replace(/\s+/g, " ").trim();
|
|
6015
6099
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
@@ -6058,7 +6142,7 @@ var cachedNote = (m) => `pitroom \u27F2 cached answer \xB7 the same question on
|
|
|
6058
6142
|
|
|
6059
6143
|
// src/core/review.ts
|
|
6060
6144
|
import crypto10 from "node:crypto";
|
|
6061
|
-
import
|
|
6145
|
+
import fs33 from "node:fs";
|
|
6062
6146
|
import path28 from "node:path";
|
|
6063
6147
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
6064
6148
|
var section = (title, body) => `## ${title}
|
|
@@ -6077,7 +6161,7 @@ function ancestors(m) {
|
|
|
6077
6161
|
function briefOf(m) {
|
|
6078
6162
|
const root = [m, ...ancestors(m)].at(-1);
|
|
6079
6163
|
const f = runFile(root.id, "brief.md");
|
|
6080
|
-
return
|
|
6164
|
+
return fs33.existsSync(f) ? fs33.readFileSync(f, "utf8") : root.task;
|
|
6081
6165
|
}
|
|
6082
6166
|
function latestReview(ids) {
|
|
6083
6167
|
for (const id of listRunIds().reverse()) {
|
|
@@ -6091,7 +6175,7 @@ function latestReview(ids) {
|
|
|
6091
6175
|
}
|
|
6092
6176
|
function diffOf(m, from = m.baseTree) {
|
|
6093
6177
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
6094
|
-
if (where &&
|
|
6178
|
+
if (where && fs33.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
6095
6179
|
if (from !== m.baseTree) {
|
|
6096
6180
|
throw new UserError(`the isolated copy of ${m.id} is gone, so its fix round cannot be shown on its own; review a run that is not applied yet`, 3);
|
|
6097
6181
|
}
|
|
@@ -6105,7 +6189,7 @@ function runReview(id) {
|
|
|
6105
6189
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
6106
6190
|
const chain = ancestors(m);
|
|
6107
6191
|
const previous = latestReview(chain.map((a) => a.id));
|
|
6108
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
6192
|
+
const dir = m.mode === "isolate" && m.worktree && fs33.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
6109
6193
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
6110
6194
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
6111
6195
|
if (!previous) {
|
|
@@ -6181,19 +6265,19 @@ function writePackage(job) {
|
|
|
6181
6265
|
const root = repoRoot(job.dir);
|
|
6182
6266
|
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
6183
6267
|
const name = `review-${crypto10.randomBytes(4).toString("hex")}.md`;
|
|
6184
|
-
const inside2 = !path28.relative(
|
|
6268
|
+
const inside2 = !path28.relative(fs33.realpathSync(root), fs33.realpathSync(g)).startsWith("..");
|
|
6185
6269
|
const file2 = inside2 ? path28.join(g, "pitroom", name) : path28.join(root, ".pitroom", name);
|
|
6186
6270
|
if (!inside2) ignoreInGit(root, "/.pitroom/");
|
|
6187
|
-
|
|
6188
|
-
|
|
6271
|
+
fs33.mkdirSync(path28.dirname(file2), { recursive: true });
|
|
6272
|
+
fs33.writeFileSync(file2, job.package);
|
|
6189
6273
|
return file2;
|
|
6190
6274
|
}
|
|
6191
6275
|
function ignoreInGit(root, pattern) {
|
|
6192
6276
|
const exclude = path28.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
|
|
6193
|
-
const text =
|
|
6277
|
+
const text = fs33.existsSync(exclude) ? fs33.readFileSync(exclude, "utf8") : "";
|
|
6194
6278
|
if (text.split(/\r?\n/).includes(pattern)) return;
|
|
6195
|
-
|
|
6196
|
-
|
|
6279
|
+
fs33.mkdirSync(path28.dirname(exclude), { recursive: true });
|
|
6280
|
+
fs33.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
|
|
6197
6281
|
${pattern}
|
|
6198
6282
|
`);
|
|
6199
6283
|
}
|
|
@@ -6349,10 +6433,10 @@ async function cmdReview(p) {
|
|
|
6349
6433
|
writeMeta(meta);
|
|
6350
6434
|
}
|
|
6351
6435
|
} catch (e) {
|
|
6352
|
-
|
|
6436
|
+
fs34.rmSync(packageFile, { force: true });
|
|
6353
6437
|
throw e;
|
|
6354
6438
|
}
|
|
6355
|
-
|
|
6439
|
+
fs34.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
6356
6440
|
return launch(p, meta);
|
|
6357
6441
|
}
|
|
6358
6442
|
async function cmdAudit(p) {
|
|
@@ -6420,7 +6504,7 @@ function cmdPlan(p) {
|
|
|
6420
6504
|
}
|
|
6421
6505
|
function cmdCrew(p) {
|
|
6422
6506
|
const file2 = flag(p, "task-file");
|
|
6423
|
-
const tasks = (file2 ?
|
|
6507
|
+
const tasks = (file2 ? fs34.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
6424
6508
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
6425
6509
|
if (has(p, "write") && tasks.length > 1) {
|
|
6426
6510
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -6606,11 +6690,8 @@ function cmdStop(p) {
|
|
|
6606
6690
|
const ids = selectIds(p, () => [resolveRun(void 0)]);
|
|
6607
6691
|
let stopped = 0;
|
|
6608
6692
|
for (const id of ids) {
|
|
6609
|
-
const meta =
|
|
6610
|
-
if (!
|
|
6611
|
-
meta.stopRequested = true;
|
|
6612
|
-
writeMeta(meta);
|
|
6613
|
-
process.kill(meta.pid, "SIGTERM");
|
|
6693
|
+
const meta = stopRun(id);
|
|
6694
|
+
if (!meta) continue;
|
|
6614
6695
|
console.log(`stopping ${meta.id} (${meta.state})`);
|
|
6615
6696
|
stopped++;
|
|
6616
6697
|
}
|
|
@@ -6623,13 +6704,13 @@ function sinceMs(s) {
|
|
|
6623
6704
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
6624
6705
|
return Date.now() - Number(m[1]) * 864e5;
|
|
6625
6706
|
}
|
|
6626
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
6707
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs34.readFileSync(0, "utf8");
|
|
6627
6708
|
function cmdStatusline(p) {
|
|
6628
6709
|
const input = readStdin();
|
|
6629
6710
|
const lines = [];
|
|
6630
6711
|
const then = flag(p, "then");
|
|
6631
6712
|
if (then) {
|
|
6632
|
-
const r = spawnSync8("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
|
|
6713
|
+
const r = process.platform === "win32" ? spawnSync8(then, { shell: true, input, encoding: "utf8", timeout: 5e3 }) : spawnSync8("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
|
|
6633
6714
|
if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
|
|
6634
6715
|
}
|
|
6635
6716
|
try {
|
|
@@ -6664,7 +6745,7 @@ function cmdModels(p) {
|
|
|
6664
6745
|
}
|
|
6665
6746
|
function cmdHookStart() {
|
|
6666
6747
|
const script = path29.join(path29.dirname(path29.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
|
|
6667
|
-
if (
|
|
6748
|
+
if (fs34.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
6668
6749
|
return 0;
|
|
6669
6750
|
}
|
|
6670
6751
|
function cmdInit(p) {
|
|
@@ -6704,7 +6785,7 @@ function cmdSavings(p) {
|
|
|
6704
6785
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
6705
6786
|
const out = flag(p, "card");
|
|
6706
6787
|
if (out) {
|
|
6707
|
-
|
|
6788
|
+
fs34.writeFileSync(out, card(t, period));
|
|
6708
6789
|
console.log(`card written to ${path29.resolve(out)}`);
|
|
6709
6790
|
}
|
|
6710
6791
|
if (has(p, "badge")) console.log(`})`);
|
|
@@ -6747,7 +6828,7 @@ function cmdConfig(p) {
|
|
|
6747
6828
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
6748
6829
|
return 0;
|
|
6749
6830
|
}
|
|
6750
|
-
console.log(`config file: ${configPath()}${
|
|
6831
|
+
console.log(`config file: ${configPath()}${fs34.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
6751
6832
|
for (const [key, s] of Object.entries(eff)) {
|
|
6752
6833
|
const v = Array.isArray(s.value) ? s.value.join(", ") || "\u2014" : s.value && typeof s.value === "object" ? Object.entries(s.value).map(([k, m]) => `${k}=${m}`).join(", ") || "\u2014" : s.value === void 0 ? key === "model" ? "the worker's default" : "\u2014" : String(s.value);
|
|
6753
6834
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -6771,7 +6852,7 @@ function cmdClean(p) {
|
|
|
6771
6852
|
const m = readMeta(id);
|
|
6772
6853
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
6773
6854
|
recordRun(m);
|
|
6774
|
-
|
|
6855
|
+
fs34.rmSync(runDir(id), { recursive: true, force: true });
|
|
6775
6856
|
}
|
|
6776
6857
|
const snapshots = prune(Date.now(), 0);
|
|
6777
6858
|
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
@@ -6782,7 +6863,7 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
6782
6863
|
|
|
6783
6864
|
// src/core/doctor.ts
|
|
6784
6865
|
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
6785
|
-
import
|
|
6866
|
+
import fs35 from "node:fs";
|
|
6786
6867
|
import os11 from "node:os";
|
|
6787
6868
|
import path30 from "node:path";
|
|
6788
6869
|
var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
@@ -6793,11 +6874,24 @@ var NEXT = [
|
|
|
6793
6874
|
{ when: /not logged in/, command: "claude auth login", why: "sign in the Claude Code worker (Codex: codex login)" },
|
|
6794
6875
|
{ when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
|
|
6795
6876
|
];
|
|
6877
|
+
function usageLine() {
|
|
6878
|
+
const ids = listRunIds();
|
|
6879
|
+
if (!ids.length) {
|
|
6880
|
+
return 'no runs yet. Ask your agent to use Pitroom (for example "have a worker find where X is defined"), or try it yourself: pitroom run "where is <something> defined?"';
|
|
6881
|
+
}
|
|
6882
|
+
let when2 = "";
|
|
6883
|
+
try {
|
|
6884
|
+
const hours = Math.max(0, Math.round((Date.now() - Date.parse(readMeta(ids[ids.length - 1]).startedAt)) / 36e5));
|
|
6885
|
+
when2 = hours < 1 ? ", the last one within the hour" : hours < 48 ? `, the last one ${hours} h ago` : `, the last one ${Math.round(hours / 24)} days ago`;
|
|
6886
|
+
} catch {
|
|
6887
|
+
}
|
|
6888
|
+
return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
|
|
6889
|
+
}
|
|
6796
6890
|
function firstNodeOnPath() {
|
|
6797
6891
|
for (const dir of (process.env.PATH ?? "").split(path30.delimiter).filter(Boolean)) {
|
|
6798
6892
|
const file2 = path30.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
6799
6893
|
try {
|
|
6800
|
-
if (!
|
|
6894
|
+
if (!fs35.statSync(file2).isFile()) continue;
|
|
6801
6895
|
} catch {
|
|
6802
6896
|
continue;
|
|
6803
6897
|
}
|
|
@@ -6829,9 +6923,11 @@ function doctor5(probe) {
|
|
|
6829
6923
|
}
|
|
6830
6924
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
6831
6925
|
const cfg = loadConfig();
|
|
6832
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
6926
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs35.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
6833
6927
|
for (const w of cfg.warnings) add("warn", w);
|
|
6834
|
-
if (process.platform
|
|
6928
|
+
if (process.platform === "win32") {
|
|
6929
|
+
add("warn", "git guard: not available on Windows yet, so Pitroom does not block a worker's git history changes or pushes there (read-only runs are still limited by each worker CLI's own rules, and --isolate keeps edits in a copy); prefer --isolate and check the patch before apply");
|
|
6930
|
+
} else {
|
|
6835
6931
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
6836
6932
|
add(guarded ? "ok" : "warn", guarded ? "git guard shim ready" : "git guard unavailable (git not on PATH)");
|
|
6837
6933
|
}
|
|
@@ -6908,6 +7004,8 @@ function doctor5(probe) {
|
|
|
6908
7004
|
for (const m of stale) add("warn", `${m.name} runs a different command for pitroom mcp than this install's: \`pitroom install --mcp --no-skills\` updates it`);
|
|
6909
7005
|
section2("Skills and agents");
|
|
6910
7006
|
addAll(skillChecks());
|
|
7007
|
+
section2("Use");
|
|
7008
|
+
add("ok", usageLine());
|
|
6911
7009
|
if (probe && chain[0]) {
|
|
6912
7010
|
section2("Live probe");
|
|
6913
7011
|
const p = liveProbe(chain[0]);
|
|
@@ -6994,8 +7092,8 @@ function skillChecks() {
|
|
|
6994
7092
|
);
|
|
6995
7093
|
}
|
|
6996
7094
|
const launcher = launcherPath();
|
|
6997
|
-
if (
|
|
6998
|
-
const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
7095
|
+
if (fs35.existsSync(launcher)) {
|
|
7096
|
+
const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
6999
7097
|
checks.push(
|
|
7000
7098
|
r.status === 0 ? { level: "ok", message: `launcher ${launcher} \u2192 pitroom ${r.stdout.trim()}` } : { level: "fail", message: `launcher ${launcher} does not start: ${(r.stderr || r.stdout).trim().slice(0, 200)}` }
|
|
7001
7099
|
);
|
|
@@ -7006,12 +7104,12 @@ function skillChecks() {
|
|
|
7006
7104
|
}
|
|
7007
7105
|
function geminiExtensionInstalled() {
|
|
7008
7106
|
const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
|
|
7009
|
-
return
|
|
7107
|
+
return fs35.existsSync(path30.join(home3, ".gemini", "extensions", "pitroom"));
|
|
7010
7108
|
}
|
|
7011
7109
|
function pluginInstalled() {
|
|
7012
7110
|
try {
|
|
7013
7111
|
const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7014
|
-
const plugins = JSON.parse(
|
|
7112
|
+
const plugins = JSON.parse(fs35.readFileSync(f, "utf8")).plugins ?? {};
|
|
7015
7113
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
7016
7114
|
} catch {
|
|
7017
7115
|
return false;
|
|
@@ -7030,14 +7128,14 @@ function superpowersActive() {
|
|
|
7030
7128
|
}
|
|
7031
7129
|
for (const base2 of [path30.join(os11.homedir(), ".agents", "skills"), path30.join(os11.homedir(), ".claude", "skills")]) {
|
|
7032
7130
|
const dir = path30.join(base2, "using-superpowers");
|
|
7033
|
-
if (
|
|
7131
|
+
if (fs35.existsSync(path30.join(dir, "SKILL.md"))) found.push(dir);
|
|
7034
7132
|
}
|
|
7035
7133
|
return found;
|
|
7036
7134
|
}
|
|
7037
7135
|
function claudePlugins() {
|
|
7038
7136
|
try {
|
|
7039
7137
|
const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7040
|
-
return Object.keys(JSON.parse(
|
|
7138
|
+
return Object.keys(JSON.parse(fs35.readFileSync(f, "utf8")).plugins ?? {});
|
|
7041
7139
|
} catch {
|
|
7042
7140
|
return [];
|
|
7043
7141
|
}
|
|
@@ -7045,7 +7143,7 @@ function claudePlugins() {
|
|
|
7045
7143
|
function claudePluginEnabled(key) {
|
|
7046
7144
|
try {
|
|
7047
7145
|
const f = path30.join(os11.homedir(), ".claude", "settings.json");
|
|
7048
|
-
return JSON.parse(
|
|
7146
|
+
return JSON.parse(fs35.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
7049
7147
|
} catch {
|
|
7050
7148
|
return true;
|
|
7051
7149
|
}
|
|
@@ -7054,7 +7152,7 @@ function codexPlugins() {
|
|
|
7054
7152
|
const plugins = /* @__PURE__ */ new Map();
|
|
7055
7153
|
let text;
|
|
7056
7154
|
try {
|
|
7057
|
-
text =
|
|
7155
|
+
text = fs35.readFileSync(path30.join(process.env.CODEX_HOME ?? path30.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
|
|
7058
7156
|
} catch {
|
|
7059
7157
|
return plugins;
|
|
7060
7158
|
}
|
|
@@ -7076,7 +7174,7 @@ function openCodePlugins() {
|
|
|
7076
7174
|
const plugins = [];
|
|
7077
7175
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
7078
7176
|
try {
|
|
7079
|
-
const text =
|
|
7177
|
+
const text = fs35.readFileSync(path30.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
7080
7178
|
const list2 = JSON.parse(text).plugin;
|
|
7081
7179
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
7082
7180
|
} catch {
|
|
@@ -7084,7 +7182,7 @@ function openCodePlugins() {
|
|
|
7084
7182
|
}
|
|
7085
7183
|
for (const folder of ["plugin", "plugins"]) {
|
|
7086
7184
|
try {
|
|
7087
|
-
for (const f of
|
|
7185
|
+
for (const f of fs35.readdirSync(path30.join(dir, folder))) plugins.push(path30.join(dir, folder, f));
|
|
7088
7186
|
} catch {
|
|
7089
7187
|
}
|
|
7090
7188
|
}
|
|
@@ -7201,7 +7299,7 @@ var COMMANDS = {
|
|
|
7201
7299
|
if (port !== void 0 && !(/^\d+$/.test(port) && Number(port) <= 65535)) throw new UserError("--port takes a number from 0 to 65535 (0 picks a free one)");
|
|
7202
7300
|
const dir = flag(p, "dir");
|
|
7203
7301
|
if (dir !== void 0) {
|
|
7204
|
-
if (!
|
|
7302
|
+
if (!fs36.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
|
|
7205
7303
|
process.chdir(dir);
|
|
7206
7304
|
process.env.PWD = process.cwd();
|
|
7207
7305
|
}
|