@rness/cli 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -8
- package/dist/bin/rness.js +1 -1
- package/dist/{chunk-ONLF4VFA.js → chunk-N3F6N6GA.js} +736 -207
- package/dist/chunk-N3F6N6GA.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ONLF4VFA.js.map +0 -1
|
@@ -743,6 +743,21 @@ async function isClean(dir, ignore = []) {
|
|
|
743
743
|
dir
|
|
744
744
|
)).trim() === "";
|
|
745
745
|
}
|
|
746
|
+
async function changedPaths(dir, paths) {
|
|
747
|
+
if (paths.length === 0) return [];
|
|
748
|
+
const entries = (await git(
|
|
749
|
+
["status", "--porcelain", "-z", "--untracked-files=all", "--", ...paths],
|
|
750
|
+
dir
|
|
751
|
+
)).split("\0");
|
|
752
|
+
const changed = [];
|
|
753
|
+
for (let i = 0; i < entries.length; i++) {
|
|
754
|
+
const entry = entries[i] ?? "";
|
|
755
|
+
if (entry.length < 4) continue;
|
|
756
|
+
changed.push(entry.slice(3));
|
|
757
|
+
if (entry[0] === "R" || entry[0] === "C") i++;
|
|
758
|
+
}
|
|
759
|
+
return changed;
|
|
760
|
+
}
|
|
746
761
|
async function pullFastForward(dir, credentials) {
|
|
747
762
|
await git(["pull", "--ff-only", "--quiet"], dir, credentials);
|
|
748
763
|
}
|
|
@@ -797,7 +812,7 @@ function classifyProbe(url, exitCode, stderr) {
|
|
|
797
812
|
var STDERR_FLUSH_MS = 200;
|
|
798
813
|
async function probeRemote(url, timeoutMs = 6e4, credentials) {
|
|
799
814
|
positional(url, "repository url");
|
|
800
|
-
return new Promise((
|
|
815
|
+
return new Promise((resolve7) => {
|
|
801
816
|
const auth = withCredentials(credentials);
|
|
802
817
|
const child = spawn(
|
|
803
818
|
"git",
|
|
@@ -816,7 +831,7 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
|
|
|
816
831
|
settled = true;
|
|
817
832
|
clearTimeout(timer);
|
|
818
833
|
child.stderr?.destroy();
|
|
819
|
-
|
|
834
|
+
resolve7(probe);
|
|
820
835
|
};
|
|
821
836
|
const timer = setTimeout(() => {
|
|
822
837
|
child.kill("SIGKILL");
|
|
@@ -1198,8 +1213,8 @@ function warn(text) {
|
|
|
1198
1213
|
var WORD = [" \u2588\u2580\u2588 \u2588\u2584 \u2588 \u2588\u2580\u2580 \u2588\u2580\u2580 \u2588\u2580\u2580", " \u2588\u2580\u2584 \u2588 \u2580\u2588 \u2588\u2588\u2584 \u2584\u2584\u2588 \u2584\u2584\u2588"];
|
|
1199
1214
|
var FROM = [63, 185, 80];
|
|
1200
1215
|
var TO = [57, 197, 207];
|
|
1201
|
-
function gradient(
|
|
1202
|
-
const chars = [...
|
|
1216
|
+
function gradient(line2) {
|
|
1217
|
+
const chars = [...line2];
|
|
1203
1218
|
const last = Math.max(1, chars.length - 1);
|
|
1204
1219
|
const painted = chars.map((ch, i) => {
|
|
1205
1220
|
if (ch === " ") return ch;
|
|
@@ -1216,7 +1231,7 @@ function banner(version, stream = process.stdout) {
|
|
|
1216
1231
|
const coloured = paint("done", "x", stream) !== "x";
|
|
1217
1232
|
const trueColour = coloured && typeof stream.hasColors === "function" && stream.hasColors(2 ** 24);
|
|
1218
1233
|
const word = WORD.map(
|
|
1219
|
-
(
|
|
1234
|
+
(line2) => trueColour ? gradient(line2) : paint("done", line2, stream)
|
|
1220
1235
|
);
|
|
1221
1236
|
return [
|
|
1222
1237
|
"",
|
|
@@ -1248,7 +1263,7 @@ async function detectGithubSsh(opts) {
|
|
|
1248
1263
|
...opts.interactive ? [] : ["-o", "BatchMode=yes"],
|
|
1249
1264
|
"git@github.com"
|
|
1250
1265
|
];
|
|
1251
|
-
return new Promise((
|
|
1266
|
+
return new Promise((resolve7) => {
|
|
1252
1267
|
const child = spawn2(opts.command ?? "ssh", args, {
|
|
1253
1268
|
stdio: [opts.interactive ? "inherit" : "ignore", "ignore", "pipe"]
|
|
1254
1269
|
});
|
|
@@ -1261,7 +1276,7 @@ async function detectGithubSsh(opts) {
|
|
|
1261
1276
|
settled = true;
|
|
1262
1277
|
clearTimeout(timer);
|
|
1263
1278
|
child.stderr?.destroy();
|
|
1264
|
-
|
|
1279
|
+
resolve7(access2);
|
|
1265
1280
|
};
|
|
1266
1281
|
const timer = setTimeout(() => {
|
|
1267
1282
|
child.kill("SIGKILL");
|
|
@@ -1390,7 +1405,7 @@ function reportError(e) {
|
|
|
1390
1405
|
}
|
|
1391
1406
|
|
|
1392
1407
|
// src/version.ts
|
|
1393
|
-
var VERSION = "0.
|
|
1408
|
+
var VERSION = "0.9.1";
|
|
1394
1409
|
|
|
1395
1410
|
// src/core/block.ts
|
|
1396
1411
|
import { createHash } from "crypto";
|
|
@@ -1427,9 +1442,9 @@ function renderBlock({
|
|
|
1427
1442
|
scope,
|
|
1428
1443
|
org,
|
|
1429
1444
|
depth,
|
|
1430
|
-
context
|
|
1445
|
+
context: context2
|
|
1431
1446
|
}) {
|
|
1432
|
-
const standards =
|
|
1447
|
+
const standards = context2.collections.find((c) => c.name === "standards")?.files ?? [];
|
|
1433
1448
|
const rules = standards.length === 0 ? ["_No standards apply to this scope yet._"] : standards.map(
|
|
1434
1449
|
(f) => `<!-- rness: standards/${f.rel} -->
|
|
1435
1450
|
${toLf(f.content).trim()}`
|
|
@@ -1442,8 +1457,8 @@ ${rules.join("\n\n")}`;
|
|
|
1442
1457
|
const text = [BEGIN, header(scope, hash), body, END].join("\n");
|
|
1443
1458
|
return { text, hash, bytes: Buffer.byteLength(text, "utf8") };
|
|
1444
1459
|
}
|
|
1445
|
-
function parseHeader(
|
|
1446
|
-
const m = HEADER.exec(
|
|
1460
|
+
function parseHeader(line2) {
|
|
1461
|
+
const m = HEADER.exec(line2.trimEnd());
|
|
1447
1462
|
if (m === null) return null;
|
|
1448
1463
|
return { version: m[1] ?? null, scope: m[2] ?? "", hash: m[3] ?? "" };
|
|
1449
1464
|
}
|
|
@@ -1614,8 +1629,8 @@ import { join as join8 } from "path";
|
|
|
1614
1629
|
function findBlock(lines) {
|
|
1615
1630
|
const begins = [];
|
|
1616
1631
|
const ends = [];
|
|
1617
|
-
lines.forEach((
|
|
1618
|
-
const l =
|
|
1632
|
+
lines.forEach((line2, i) => {
|
|
1633
|
+
const l = line2.trimEnd();
|
|
1619
1634
|
if (l === BEGIN) begins.push(i);
|
|
1620
1635
|
if (l === END) ends.push(i);
|
|
1621
1636
|
});
|
|
@@ -1695,14 +1710,38 @@ var TARGETS = {
|
|
|
1695
1710
|
claude: {
|
|
1696
1711
|
name: "claude",
|
|
1697
1712
|
label: "Claude Code",
|
|
1698
|
-
|
|
1699
|
-
// Read the context repository from a repository without a prompt. The
|
|
1700
|
-
// path is resolved against the project directory, and only once Claude
|
|
1701
|
-
// Code has trusted it (verified on 2.1.284, spec 0011 §7).
|
|
1702
|
-
guarantees: [
|
|
1713
|
+
files: [
|
|
1703
1714
|
{
|
|
1704
|
-
|
|
1705
|
-
|
|
1715
|
+
file: ".claude/settings.json",
|
|
1716
|
+
guarantees: [
|
|
1717
|
+
// Read the context repository from a repository without a prompt.
|
|
1718
|
+
// Resolved against the project directory, and applied once Claude
|
|
1719
|
+
// Code has trusted it (verified on 2.1.284, spec 0011 §7).
|
|
1720
|
+
{
|
|
1721
|
+
path: ["permissions", "additionalDirectories"],
|
|
1722
|
+
contains: "../../.rness"
|
|
1723
|
+
}
|
|
1724
|
+
]
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
file: ".mcp.json",
|
|
1728
|
+
guarantees: [
|
|
1729
|
+
// The pinned copy, by a path relative to the repository — the
|
|
1730
|
+
// directory Claude Code starts the server in (spec 0014 §6). Not
|
|
1731
|
+
// pre-approved in settings: each developer approves it once, since
|
|
1732
|
+
// a repository approving its own command would run any change to
|
|
1733
|
+
// it unasked (spec 0014 §3, 0.9.1).
|
|
1734
|
+
{
|
|
1735
|
+
path: ["mcpServers", "rness"],
|
|
1736
|
+
value: {
|
|
1737
|
+
command: "node",
|
|
1738
|
+
args: [
|
|
1739
|
+
"../../.rness/node_modules/@rness/cli/dist/bin/rness.js",
|
|
1740
|
+
"mcp"
|
|
1741
|
+
]
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
]
|
|
1706
1745
|
}
|
|
1707
1746
|
]
|
|
1708
1747
|
}
|
|
@@ -1728,7 +1767,7 @@ function parse3(text) {
|
|
|
1728
1767
|
}
|
|
1729
1768
|
return isObject(data) ? { ok: true, data } : { ok: false, reason: "not a JSON object" };
|
|
1730
1769
|
}
|
|
1731
|
-
function
|
|
1770
|
+
function parentOf(root, g, create) {
|
|
1732
1771
|
let node = root;
|
|
1733
1772
|
for (const [i, key] of g.path.slice(0, -1).entries()) {
|
|
1734
1773
|
const next = node[key];
|
|
@@ -1742,17 +1781,32 @@ function listAt(root, g, create) {
|
|
|
1742
1781
|
};
|
|
1743
1782
|
node = node[key];
|
|
1744
1783
|
}
|
|
1784
|
+
return { kind: "node", node };
|
|
1785
|
+
}
|
|
1786
|
+
function apply(root, g, create) {
|
|
1787
|
+
const found = parentOf(root, g, create);
|
|
1788
|
+
if (found.kind === "absent") return { holds: false, changed: false };
|
|
1789
|
+
if (found.kind === "invalid") return { invalid: found.reason };
|
|
1790
|
+
const parent = found.node;
|
|
1745
1791
|
const last = g.path[g.path.length - 1] ?? "";
|
|
1746
|
-
const value =
|
|
1792
|
+
const value = parent[last];
|
|
1793
|
+
if ("value" in g) {
|
|
1794
|
+
if (value !== void 0) return { holds: true, changed: false };
|
|
1795
|
+
if (!create) return { holds: false, changed: false };
|
|
1796
|
+
parent[last] = structuredClone(g.value);
|
|
1797
|
+
return { holds: true, changed: true };
|
|
1798
|
+
}
|
|
1747
1799
|
if (value === void 0) {
|
|
1748
|
-
if (!create) return {
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
return { kind: "list", list };
|
|
1800
|
+
if (!create) return { holds: false, changed: false };
|
|
1801
|
+
parent[last] = [g.contains];
|
|
1802
|
+
return { holds: true, changed: true };
|
|
1752
1803
|
}
|
|
1753
1804
|
if (!Array.isArray(value))
|
|
1754
|
-
return {
|
|
1755
|
-
return {
|
|
1805
|
+
return { invalid: `${g.path.join(".")} is not a list` };
|
|
1806
|
+
if (value.includes(g.contains)) return { holds: true, changed: false };
|
|
1807
|
+
if (!create) return { holds: false, changed: false };
|
|
1808
|
+
value.push(g.contains);
|
|
1809
|
+
return { holds: true, changed: true };
|
|
1756
1810
|
}
|
|
1757
1811
|
function styleOf(text) {
|
|
1758
1812
|
const indent = text.split(/\r?\n/).slice(1).map((l) => /^[ \t]+/.exec(l)?.[0]).find((w) => w !== void 0);
|
|
@@ -1767,13 +1821,9 @@ function ensureGuarantees(existing, guarantees) {
|
|
|
1767
1821
|
if (!parsed.ok) return { kind: "invalid", reason: parsed.reason };
|
|
1768
1822
|
let changed = false;
|
|
1769
1823
|
for (const g of guarantees) {
|
|
1770
|
-
const
|
|
1771
|
-
if (
|
|
1772
|
-
|
|
1773
|
-
if (found.kind === "list" && !found.list.includes(g.contains)) {
|
|
1774
|
-
found.list.push(g.contains);
|
|
1775
|
-
changed = true;
|
|
1776
|
-
}
|
|
1824
|
+
const r = apply(parsed.data, g, true);
|
|
1825
|
+
if ("invalid" in r) return { kind: "invalid", reason: r.invalid };
|
|
1826
|
+
changed ||= r.changed;
|
|
1777
1827
|
}
|
|
1778
1828
|
if (existing === null)
|
|
1779
1829
|
return {
|
|
@@ -1792,15 +1842,14 @@ function missingGuarantees(existing, guarantees) {
|
|
|
1792
1842
|
if (!parsed.ok) return { invalid: parsed.reason };
|
|
1793
1843
|
const missing = [];
|
|
1794
1844
|
for (const g of guarantees) {
|
|
1795
|
-
const
|
|
1796
|
-
if (
|
|
1797
|
-
if (
|
|
1798
|
-
missing.push(g);
|
|
1845
|
+
const r = apply(parsed.data, g, false);
|
|
1846
|
+
if ("invalid" in r) return { invalid: r.invalid };
|
|
1847
|
+
if (!r.holds) missing.push(g);
|
|
1799
1848
|
}
|
|
1800
1849
|
return missing;
|
|
1801
1850
|
}
|
|
1802
1851
|
function describeGuarantee(g) {
|
|
1803
|
-
return `${g.path.join(".")} lacks ${g.contains}`;
|
|
1852
|
+
return "value" in g ? `${g.path.join(".")} is missing` : `${g.path.join(".")} lacks ${g.contains}`;
|
|
1804
1853
|
}
|
|
1805
1854
|
|
|
1806
1855
|
// src/core/agent-targets.ts
|
|
@@ -1809,11 +1858,12 @@ async function filesOf(root, manifest, targets) {
|
|
|
1809
1858
|
for (const repo of Object.keys(manifest.repos)) {
|
|
1810
1859
|
if (!await exists(join9(root, "org", repo))) continue;
|
|
1811
1860
|
for (const target of targets)
|
|
1812
|
-
files
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1861
|
+
for (const owned of target.files)
|
|
1862
|
+
files.push({
|
|
1863
|
+
guarantees: owned.guarantees,
|
|
1864
|
+
file: join9(root, "org", repo, ...owned.file.split("/")),
|
|
1865
|
+
label: `org/${repo}/${owned.file}`
|
|
1866
|
+
});
|
|
1817
1867
|
}
|
|
1818
1868
|
return files;
|
|
1819
1869
|
}
|
|
@@ -1822,16 +1872,23 @@ function declared(manifest) {
|
|
|
1822
1872
|
(name) => Object.hasOwn(TARGETS, name) ? [TARGETS[name]] : []
|
|
1823
1873
|
);
|
|
1824
1874
|
}
|
|
1875
|
+
var BLOCK_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
1876
|
+
function writtenFiles(manifest) {
|
|
1877
|
+
return [
|
|
1878
|
+
...BLOCK_FILES,
|
|
1879
|
+
...declared(manifest).flatMap((t) => t.files.map((f) => f.file))
|
|
1880
|
+
];
|
|
1881
|
+
}
|
|
1825
1882
|
async function agentTargets(root, manifest, opts) {
|
|
1826
1883
|
const outcomes = [];
|
|
1827
|
-
for (const {
|
|
1884
|
+
for (const { guarantees, file, label } of await filesOf(
|
|
1828
1885
|
root,
|
|
1829
1886
|
manifest,
|
|
1830
1887
|
declared(manifest)
|
|
1831
1888
|
)) {
|
|
1832
1889
|
const existing = await readOrNull(file);
|
|
1833
1890
|
if (opts.check) {
|
|
1834
|
-
const missing = missingGuarantees(existing,
|
|
1891
|
+
const missing = missingGuarantees(existing, guarantees);
|
|
1835
1892
|
if ("invalid" in missing)
|
|
1836
1893
|
outcomes.push({ label, status: "invalid", detail: missing.invalid });
|
|
1837
1894
|
else if (missing.length > 0)
|
|
@@ -1843,7 +1900,7 @@ async function agentTargets(root, manifest, opts) {
|
|
|
1843
1900
|
else outcomes.push({ label, status: "unchanged", detail: null });
|
|
1844
1901
|
continue;
|
|
1845
1902
|
}
|
|
1846
|
-
const ensured = ensureGuarantees(existing,
|
|
1903
|
+
const ensured = ensureGuarantees(existing, guarantees);
|
|
1847
1904
|
if (ensured.kind === "invalid") {
|
|
1848
1905
|
outcomes.push({ label, status: "invalid", detail: ensured.reason });
|
|
1849
1906
|
continue;
|
|
@@ -1865,14 +1922,14 @@ async function leftoverTargets(root, manifest) {
|
|
|
1865
1922
|
(t) => !agents.includes(t.name)
|
|
1866
1923
|
);
|
|
1867
1924
|
const leftovers = [];
|
|
1868
|
-
for (const {
|
|
1925
|
+
for (const { guarantees, file, label } of await filesOf(
|
|
1869
1926
|
root,
|
|
1870
1927
|
manifest,
|
|
1871
1928
|
undeclared
|
|
1872
1929
|
)) {
|
|
1873
1930
|
const existing = await readOrNull(file);
|
|
1874
1931
|
if (existing === null) continue;
|
|
1875
|
-
const missing = missingGuarantees(existing,
|
|
1932
|
+
const missing = missingGuarantees(existing, guarantees);
|
|
1876
1933
|
if (!("invalid" in missing) && missing.length === 0) leftovers.push(label);
|
|
1877
1934
|
}
|
|
1878
1935
|
return leftovers;
|
|
@@ -1920,7 +1977,7 @@ async function step(options, work) {
|
|
|
1920
1977
|
const rawErr = process.stderr.write.bind(
|
|
1921
1978
|
process.stderr
|
|
1922
1979
|
);
|
|
1923
|
-
const
|
|
1980
|
+
const guard2 = (raw) => ((...args) => {
|
|
1924
1981
|
erase();
|
|
1925
1982
|
return raw(...args);
|
|
1926
1983
|
});
|
|
@@ -1935,8 +1992,8 @@ async function step(options, work) {
|
|
|
1935
1992
|
} else {
|
|
1936
1993
|
draw("\u2026");
|
|
1937
1994
|
}
|
|
1938
|
-
stream.write =
|
|
1939
|
-
if (process.stderr !== stream) process.stderr.write =
|
|
1995
|
+
stream.write = guard2(rawWrite);
|
|
1996
|
+
if (process.stderr !== stream) process.stderr.write = guard2(rawErr);
|
|
1940
1997
|
try {
|
|
1941
1998
|
return await work();
|
|
1942
1999
|
} finally {
|
|
@@ -1983,8 +2040,8 @@ var plainUi = {
|
|
|
1983
2040
|
{ label: options.doing, animate: options.git !== true },
|
|
1984
2041
|
work
|
|
1985
2042
|
);
|
|
1986
|
-
const
|
|
1987
|
-
if (
|
|
2043
|
+
const line2 = options.quiet === true ? null : done?.(result) ?? null;
|
|
2044
|
+
if (line2 !== null) this.line(line2[0], line2[1]);
|
|
1988
2045
|
return result;
|
|
1989
2046
|
}
|
|
1990
2047
|
};
|
|
@@ -2017,7 +2074,7 @@ function sessionUi(p) {
|
|
|
2017
2074
|
p.log.warn(text);
|
|
2018
2075
|
},
|
|
2019
2076
|
note: (title, lines) => {
|
|
2020
|
-
p.note(lines.join("\n"), title, { format: (
|
|
2077
|
+
p.note(lines.join("\n"), title, { format: (line2) => grey(line2) });
|
|
2021
2078
|
},
|
|
2022
2079
|
hint: (text) => {
|
|
2023
2080
|
p.log.message(paint("idle", text));
|
|
@@ -2031,20 +2088,20 @@ function sessionUi(p) {
|
|
|
2031
2088
|
async step(options, work, done) {
|
|
2032
2089
|
const label = options.sentence ?? tidy(options.doing);
|
|
2033
2090
|
const finish2 = (result) => done?.(result) ?? null;
|
|
2034
|
-
const label_ = (
|
|
2091
|
+
const label_ = (line2) => `${line2[1]} ${line2[0]}`;
|
|
2035
2092
|
if (options.git === true && !this.gitIsSilent) {
|
|
2036
2093
|
p.log.message(paint("idle", `${label}\u2026`));
|
|
2037
2094
|
const result = await work();
|
|
2038
|
-
const
|
|
2039
|
-
if (
|
|
2095
|
+
const line2 = finish2(result);
|
|
2096
|
+
if (line2 !== null) say(line2[0], label_(line2));
|
|
2040
2097
|
return result;
|
|
2041
2098
|
}
|
|
2042
2099
|
const spin = p.spinner();
|
|
2043
2100
|
spin.start(label);
|
|
2044
2101
|
try {
|
|
2045
2102
|
const result = await work();
|
|
2046
|
-
const
|
|
2047
|
-
spin.stop(
|
|
2103
|
+
const line2 = finish2(result);
|
|
2104
|
+
spin.stop(line2 === null ? label : label_(line2));
|
|
2048
2105
|
return result;
|
|
2049
2106
|
} catch (e) {
|
|
2050
2107
|
spin.error(`${label} \u2014 failed`);
|
|
@@ -2070,7 +2127,6 @@ async function makeUi(terminal) {
|
|
|
2070
2127
|
}
|
|
2071
2128
|
|
|
2072
2129
|
// src/commands/sync.ts
|
|
2073
|
-
var MANAGED_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
2074
2130
|
function targetsOf(manifest, root, only) {
|
|
2075
2131
|
const targets = [];
|
|
2076
2132
|
if (only === void 0)
|
|
@@ -2260,13 +2316,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
2260
2316
|
}
|
|
2261
2317
|
} else if (opts.pull === true && !check) {
|
|
2262
2318
|
try {
|
|
2263
|
-
|
|
2264
|
-
...MANAGED_FILES,
|
|
2265
|
-
...(manifest.agents ?? []).flatMap(
|
|
2266
|
-
(a) => Object.hasOwn(TARGETS, a) ? [TARGETS[a].file] : []
|
|
2267
|
-
)
|
|
2268
|
-
];
|
|
2269
|
-
if (!await isClean(dir, written)) {
|
|
2319
|
+
if (!await isClean(dir, writtenFiles(manifest))) {
|
|
2270
2320
|
lines.push(["skipped", `${label} (working tree not clean)`]);
|
|
2271
2321
|
continue;
|
|
2272
2322
|
}
|
|
@@ -2309,14 +2359,14 @@ async function syncCommand(opts, deps = {}) {
|
|
|
2309
2359
|
continue;
|
|
2310
2360
|
}
|
|
2311
2361
|
const [agentsLinked, claudeLinked] = await Promise.all(
|
|
2312
|
-
|
|
2362
|
+
BLOCK_FILES.map((f) => isSymlink(join10(t.dir, f)))
|
|
2313
2363
|
);
|
|
2314
2364
|
const atRoot = t.scope === null;
|
|
2315
2365
|
if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
|
|
2316
2366
|
lines.push(["skipped", `${t.label} (symlink)`]);
|
|
2317
2367
|
continue;
|
|
2318
2368
|
}
|
|
2319
|
-
const
|
|
2369
|
+
const context2 = await assembleContext({
|
|
2320
2370
|
rnessDir: ws.rnessDir,
|
|
2321
2371
|
manifest,
|
|
2322
2372
|
scope: t.scope
|
|
@@ -2325,7 +2375,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
2325
2375
|
scope: t.scope,
|
|
2326
2376
|
org,
|
|
2327
2377
|
depth: t.depth,
|
|
2328
|
-
context
|
|
2378
|
+
context: context2
|
|
2329
2379
|
});
|
|
2330
2380
|
if (block.bytes > BLOCK_SIZE_WARNING) {
|
|
2331
2381
|
ui.warn(
|
|
@@ -3062,10 +3112,10 @@ async function runPinnedSync(root) {
|
|
|
3062
3112
|
function summariseSync(stdout) {
|
|
3063
3113
|
const counts = /* @__PURE__ */ new Map();
|
|
3064
3114
|
const others = [];
|
|
3065
|
-
for (const
|
|
3066
|
-
if (
|
|
3067
|
-
const verb = /^(updated|unchanged|stale)\s/.exec(
|
|
3068
|
-
if (verb === void 0) others.push(
|
|
3115
|
+
for (const line2 of stdout.split("\n")) {
|
|
3116
|
+
if (line2.trim() === "") continue;
|
|
3117
|
+
const verb = /^(updated|unchanged|stale)\s/.exec(line2)?.[1];
|
|
3118
|
+
if (verb === void 0) others.push(line2);
|
|
3069
3119
|
else counts.set(verb, (counts.get(verb) ?? 0) + 1);
|
|
3070
3120
|
}
|
|
3071
3121
|
const total = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
@@ -3177,12 +3227,33 @@ function scaffoldMessage(subject, version) {
|
|
|
3177
3227
|
|
|
3178
3228
|
${SCAFFOLD_TRAILER}: ${version}`;
|
|
3179
3229
|
}
|
|
3230
|
+
async function commitUpgrade(rnessDir, message) {
|
|
3231
|
+
const add = await run(rnessDir, ["add", "-A"]);
|
|
3232
|
+
if (add.code !== 0) return { kind: "refused", output: add.stderr.trim() };
|
|
3233
|
+
const staged = await run(rnessDir, ["diff", "--cached", "--quiet"]);
|
|
3234
|
+
if (staged.code === 0 && !await mergeInProgress(rnessDir))
|
|
3235
|
+
return { kind: "nothing" };
|
|
3236
|
+
const commit = await run(rnessDir, ["commit", "-q", "-m", message]);
|
|
3237
|
+
if (commit.code !== 0)
|
|
3238
|
+
return {
|
|
3239
|
+
kind: "refused",
|
|
3240
|
+
output: `${commit.stdout}
|
|
3241
|
+
${commit.stderr}`.trim()
|
|
3242
|
+
};
|
|
3243
|
+
return { kind: "committed" };
|
|
3244
|
+
}
|
|
3245
|
+
async function mergeInProgress(dir) {
|
|
3246
|
+
const r = await run(dir, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]);
|
|
3247
|
+
return r.code === 0;
|
|
3248
|
+
}
|
|
3180
3249
|
async function findScaffoldBase(rnessDir) {
|
|
3250
|
+
const merging = await mergeInProgress(rnessDir);
|
|
3181
3251
|
const log = await run(rnessDir, [
|
|
3182
3252
|
"log",
|
|
3183
3253
|
"--topo-order",
|
|
3184
3254
|
`--format=%H%x1f%P%x1f%s%x1f%(trailers:key=${SCAFFOLD_TRAILER},valueonly,separator=%x2C)%x1e`,
|
|
3185
|
-
"HEAD"
|
|
3255
|
+
"HEAD",
|
|
3256
|
+
...merging ? ["MERGE_HEAD"] : []
|
|
3186
3257
|
]);
|
|
3187
3258
|
if (log.code !== 0) return null;
|
|
3188
3259
|
const commits = log.stdout.split("").map((r) => r.trim()).filter((r) => r !== "").map((r) => {
|
|
@@ -3269,8 +3340,8 @@ async function mergeScaffold(rnessDir, commit, base) {
|
|
|
3269
3340
|
"--name-status",
|
|
3270
3341
|
"HEAD"
|
|
3271
3342
|
]);
|
|
3272
|
-
for (const
|
|
3273
|
-
const [letter = "", path = ""] =
|
|
3343
|
+
for (const line2 of status2.split("\n")) {
|
|
3344
|
+
const [letter = "", path = ""] = line2.split(" ");
|
|
3274
3345
|
if (path === "" || conflicts.includes(path)) continue;
|
|
3275
3346
|
if (letter === "A") changes.push(["added", path]);
|
|
3276
3347
|
else if (letter === "D") changes.push(["removed", path]);
|
|
@@ -3413,8 +3484,8 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
|
|
|
3413
3484
|
return 1;
|
|
3414
3485
|
}
|
|
3415
3486
|
const outcome = syncOutcome(result, { sshWorks: sshWorks === true });
|
|
3416
|
-
for (const
|
|
3417
|
-
for (const
|
|
3487
|
+
for (const line2 of outcome.hints) ui.hint(line2);
|
|
3488
|
+
for (const line2 of outcome.errors) ui.error(line2);
|
|
3418
3489
|
return outcome.code;
|
|
3419
3490
|
}
|
|
3420
3491
|
|
|
@@ -3519,7 +3590,7 @@ async function createBlank(input) {
|
|
|
3519
3590
|
`Workspace \`${name}\` is ready in ${shown}/.`,
|
|
3520
3591
|
"",
|
|
3521
3592
|
"Next:",
|
|
3522
|
-
...next.map((
|
|
3593
|
+
...next.map((line2) => ` ${line2}`),
|
|
3523
3594
|
""
|
|
3524
3595
|
].join("\n")
|
|
3525
3596
|
);
|
|
@@ -3530,8 +3601,8 @@ async function createBlank(input) {
|
|
|
3530
3601
|
function firstLine4(text) {
|
|
3531
3602
|
return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "unknown error";
|
|
3532
3603
|
}
|
|
3533
|
-
function splitSpecs(
|
|
3534
|
-
return
|
|
3604
|
+
function splitSpecs(list2) {
|
|
3605
|
+
return list2.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
3535
3606
|
}
|
|
3536
3607
|
function cancelled(ui) {
|
|
3537
3608
|
ui.cancelled();
|
|
@@ -4162,7 +4233,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
|
|
|
4162
4233
|
`Workspace for \`${org}\` is ready in ${shown}/.`,
|
|
4163
4234
|
"",
|
|
4164
4235
|
"Next:",
|
|
4165
|
-
...next.map((
|
|
4236
|
+
...next.map((line2) => ` ${line2}`),
|
|
4166
4237
|
""
|
|
4167
4238
|
].join("\n")
|
|
4168
4239
|
);
|
|
@@ -4203,11 +4274,67 @@ async function logoutCommand() {
|
|
|
4203
4274
|
}
|
|
4204
4275
|
}
|
|
4205
4276
|
|
|
4277
|
+
// src/core/contract.ts
|
|
4278
|
+
import { join as join20 } from "path";
|
|
4279
|
+
var STATUSES = {
|
|
4280
|
+
adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
|
|
4281
|
+
specs: [
|
|
4282
|
+
"Draft",
|
|
4283
|
+
"Proposed",
|
|
4284
|
+
"Approved",
|
|
4285
|
+
"Implemented",
|
|
4286
|
+
"Superseded",
|
|
4287
|
+
"Rejected"
|
|
4288
|
+
],
|
|
4289
|
+
plans: ["Draft", "Ready", "In progress", "Blocked", "Completed", "Abandoned"]
|
|
4290
|
+
};
|
|
4291
|
+
var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
|
|
4292
|
+
var PLACEMENT_KEYS = ["scopes", "scope"];
|
|
4293
|
+
async function checkContract(rnessDir) {
|
|
4294
|
+
const problems = [];
|
|
4295
|
+
for (const name of COLLECTIONS) {
|
|
4296
|
+
const allowed = STATUSES[name];
|
|
4297
|
+
const items = await collectMarkdown(join20(rnessDir, name));
|
|
4298
|
+
for (const item of items) {
|
|
4299
|
+
const where = `${name}/${item.rel}`;
|
|
4300
|
+
if (SKIP.has(where)) continue;
|
|
4301
|
+
if (item.fieldsError !== null) {
|
|
4302
|
+
problems.push(`${where}: invalid front matter (${item.fieldsError})`);
|
|
4303
|
+
continue;
|
|
4304
|
+
}
|
|
4305
|
+
if (item.fields !== null) {
|
|
4306
|
+
for (const key of PLACEMENT_KEYS) {
|
|
4307
|
+
if (Object.hasOwn(item.fields, key)) {
|
|
4308
|
+
problems.push(
|
|
4309
|
+
`${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`
|
|
4310
|
+
);
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
if (allowed === void 0) continue;
|
|
4315
|
+
if (item.fields === null) {
|
|
4316
|
+
problems.push(`${where}: missing a front-matter block`);
|
|
4317
|
+
continue;
|
|
4318
|
+
}
|
|
4319
|
+
if (item.title === null) problems.push(`${where}: missing a "# " title`);
|
|
4320
|
+
const status2 = item.fields["status"];
|
|
4321
|
+
if (status2 === void 0 || status2 === null || status2 === "") {
|
|
4322
|
+
problems.push(`${where}: missing "status"`);
|
|
4323
|
+
} else if (typeof status2 !== "string" || !allowed.includes(status2)) {
|
|
4324
|
+
problems.push(
|
|
4325
|
+
`${where}: unknown status ${JSON.stringify(status2)} (expected ${allowed.join(", ")})`
|
|
4326
|
+
);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
return problems;
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4206
4333
|
// src/commands/upgrade.ts
|
|
4207
4334
|
import { execFile as execFile6 } from "child_process";
|
|
4208
4335
|
import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
|
|
4209
4336
|
import { tmpdir as tmpdir3 } from "os";
|
|
4210
|
-
import { basename as basename5, join as
|
|
4337
|
+
import { basename as basename5, join as join21, relative as relative5 } from "path";
|
|
4211
4338
|
import { promisify as promisify6 } from "util";
|
|
4212
4339
|
var execFileP6 = promisify6(execFile6);
|
|
4213
4340
|
var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
|
|
@@ -4227,10 +4354,10 @@ async function latestVersion() {
|
|
|
4227
4354
|
async function scaffoldFor(version, rnessDir) {
|
|
4228
4355
|
if (version === VERSION) return { dir: scaffoldDir() };
|
|
4229
4356
|
const installed = await readPinnedCli(rnessDir);
|
|
4230
|
-
const inside = installed === null ? null :
|
|
4231
|
-
if (installed?.version === version && inside !== null && await exists(
|
|
4357
|
+
const inside = installed === null ? null : join21(installed.dir, "scaffold");
|
|
4358
|
+
if (installed?.version === version && inside !== null && await exists(join21(inside, "package.json")))
|
|
4232
4359
|
return { dir: inside };
|
|
4233
|
-
const scratch = await mkdtemp3(
|
|
4360
|
+
const scratch = await mkdtemp3(join21(tmpdir3(), "rness-pack-"));
|
|
4234
4361
|
const cleanup = () => rm7(scratch, { recursive: true, force: true });
|
|
4235
4362
|
try {
|
|
4236
4363
|
const { stdout } = await execFileP6(
|
|
@@ -4245,8 +4372,8 @@ async function scaffoldFor(version, rnessDir) {
|
|
|
4245
4372
|
{ timeout: 6e4 }
|
|
4246
4373
|
);
|
|
4247
4374
|
const tarball = stdout.trim().split("\n").at(-1) ?? "";
|
|
4248
|
-
await execFileP6("tar", ["-xzf",
|
|
4249
|
-
return { dir:
|
|
4375
|
+
await execFileP6("tar", ["-xzf", join21(scratch, tarball), "-C", scratch]);
|
|
4376
|
+
return { dir: join21(scratch, "package", "scaffold"), cleanup };
|
|
4250
4377
|
} catch (e) {
|
|
4251
4378
|
await cleanup();
|
|
4252
4379
|
throw new Error(
|
|
@@ -4258,21 +4385,43 @@ async function scaffoldFor(version, rnessDir) {
|
|
|
4258
4385
|
async function declaredManager(rnessDir, pm) {
|
|
4259
4386
|
try {
|
|
4260
4387
|
const pkg = JSON.parse(
|
|
4261
|
-
await readFile9(
|
|
4388
|
+
await readFile9(join21(rnessDir, "package.json"), "utf8")
|
|
4262
4389
|
);
|
|
4263
4390
|
if (typeof pkg.packageManager === "string") return pkg.packageManager;
|
|
4264
4391
|
} catch {
|
|
4265
4392
|
}
|
|
4266
4393
|
return `${pm}@${await packageManagerVersion(pm)}`;
|
|
4267
4394
|
}
|
|
4268
|
-
function
|
|
4395
|
+
async function clonesToCommit(root, cwd, manifest) {
|
|
4396
|
+
const files = writtenFiles(manifest);
|
|
4397
|
+
const clones = [];
|
|
4398
|
+
for (const repo of Object.keys(manifest.repos)) {
|
|
4399
|
+
const dir = join21(root, "org", repo);
|
|
4400
|
+
if (!await exists(dir)) continue;
|
|
4401
|
+
try {
|
|
4402
|
+
const paths = await changedPaths(dir, files);
|
|
4403
|
+
if (paths.length > 0)
|
|
4404
|
+
clones.push({ dir: relative5(cwd, dir) || ".", paths });
|
|
4405
|
+
} catch {
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
return clones;
|
|
4409
|
+
}
|
|
4410
|
+
function finish(ui, cwd, rnessDir, label, target, ending) {
|
|
4269
4411
|
const rel = relative5(cwd, rnessDir) || ".";
|
|
4270
|
-
const
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4412
|
+
const message = `chore: rness ${target}`;
|
|
4413
|
+
const next = [];
|
|
4414
|
+
if (ending.commit === "committed") next.push(`git -C ${rel} push`);
|
|
4415
|
+
if (ending.commit === "refused")
|
|
4416
|
+
next.push(`git -C ${rel} commit -m "${message}"`);
|
|
4417
|
+
for (const c of ending.clones)
|
|
4418
|
+
next.push(
|
|
4419
|
+
`git -C ${c.dir} add ${c.paths.join(" ")} && git -C ${c.dir} commit -m "${message}"`
|
|
4420
|
+
);
|
|
4421
|
+
if (ending.commit !== "nothing")
|
|
4422
|
+
next.push("# teammates: git pull \u2014 the next rness command installs it");
|
|
4274
4423
|
if (ui.session) {
|
|
4275
|
-
ui.note("Next steps", next);
|
|
4424
|
+
if (next.length > 0) ui.note("Next steps", next);
|
|
4276
4425
|
ui.outro(`${label} runs @rness/cli ${target}`);
|
|
4277
4426
|
} else
|
|
4278
4427
|
process.stdout.write(
|
|
@@ -4280,12 +4429,10 @@ function finish(ui, cwd, rnessDir, label, target) {
|
|
|
4280
4429
|
"",
|
|
4281
4430
|
status("upgraded", `${label} to @rness/cli ${target}`),
|
|
4282
4431
|
"",
|
|
4283
|
-
"Next:",
|
|
4284
|
-
...next.map((line) => ` ${line}`),
|
|
4285
|
-
""
|
|
4432
|
+
...next.length > 0 ? ["Next:", ...next.map((line2) => ` ${line2}`), ""] : []
|
|
4286
4433
|
].join("\n")
|
|
4287
4434
|
);
|
|
4288
|
-
return 0;
|
|
4435
|
+
return ending.commit === "refused" ? 1 : 0;
|
|
4289
4436
|
}
|
|
4290
4437
|
async function upgradeCommand(version, opts, terminal = defaultTerminal, deps = {}) {
|
|
4291
4438
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -4334,6 +4481,13 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
4334
4481
|
`);
|
|
4335
4482
|
return 1;
|
|
4336
4483
|
}
|
|
4484
|
+
if (await mergeInProgress(ws.rnessDir)) {
|
|
4485
|
+
process.stderr.write(
|
|
4486
|
+
`${label} has a merge in progress; commit it, or abort it with git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort, then ${rnessCommand()} upgrade
|
|
4487
|
+
`
|
|
4488
|
+
);
|
|
4489
|
+
return 1;
|
|
4490
|
+
}
|
|
4337
4491
|
if (!await isClean(ws.rnessDir)) {
|
|
4338
4492
|
process.stderr.write(
|
|
4339
4493
|
`${label} has uncommitted changes; commit or stash them, then ${rnessCommand()} upgrade
|
|
@@ -4435,62 +4589,85 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
4435
4589
|
await source.cleanup?.();
|
|
4436
4590
|
}
|
|
4437
4591
|
}
|
|
4438
|
-
const packageFile =
|
|
4592
|
+
const packageFile = join21(ws.rnessDir, "package.json");
|
|
4439
4593
|
const restore = /* @__PURE__ */ new Map([
|
|
4440
4594
|
[packageFile, await readFile9(packageFile, "utf8")]
|
|
4441
4595
|
]);
|
|
4442
4596
|
const moved = await readPin(ws.rnessDir);
|
|
4443
4597
|
if (moved !== null && moved.spec !== target)
|
|
4444
4598
|
await writePin(ws.rnessDir, moved.spec, target);
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
);
|
|
4453
|
-
} catch (e) {
|
|
4454
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
4455
|
-
ui.error(message);
|
|
4456
|
-
if (!merged) {
|
|
4457
|
-
for (const [file, text] of restore) await writeFileAtomic(file, text);
|
|
4458
|
-
ui.hint(
|
|
4459
|
-
`restored ${relative5(cwd, packageFile) || packageFile}`,
|
|
4460
|
-
"stderr"
|
|
4599
|
+
const mustInstall = !(pin.spec === target && installed === target);
|
|
4600
|
+
if (mustInstall) {
|
|
4601
|
+
try {
|
|
4602
|
+
await ui.step(
|
|
4603
|
+
{ doing: `installing dependencies with ${pm}` },
|
|
4604
|
+
() => installDependencies(pm, ws.rnessDir),
|
|
4605
|
+
() => ["installed", `dependencies with ${pm}`]
|
|
4461
4606
|
);
|
|
4607
|
+
} catch (e) {
|
|
4608
|
+
const message2 = e instanceof Error ? e.message : String(e);
|
|
4609
|
+
ui.error(message2);
|
|
4610
|
+
if (!merged) {
|
|
4611
|
+
for (const [file, text] of restore) await writeFileAtomic(file, text);
|
|
4612
|
+
ui.hint(
|
|
4613
|
+
`restored ${relative5(cwd, packageFile) || packageFile}`,
|
|
4614
|
+
"stderr"
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
if (merged)
|
|
4618
|
+
ui.hint(
|
|
4619
|
+
`the scaffold merge is still staged; undo it with: git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort`,
|
|
4620
|
+
"stderr"
|
|
4621
|
+
);
|
|
4622
|
+
if (/minimum.?release.?age|NO_MATURE/i.test(message2))
|
|
4623
|
+
ui.hint(
|
|
4624
|
+
"pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
|
|
4625
|
+
"stderr"
|
|
4626
|
+
);
|
|
4627
|
+
return 1;
|
|
4462
4628
|
}
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
"
|
|
4467
|
-
);
|
|
4468
|
-
if (/minimum.?release.?age|NO_MATURE/i.test(message))
|
|
4469
|
-
ui.hint(
|
|
4470
|
-
"pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
|
|
4471
|
-
"stderr"
|
|
4472
|
-
);
|
|
4473
|
-
return 1;
|
|
4474
|
-
}
|
|
4475
|
-
const now = (await readPinnedCli(ws.rnessDir))?.version ?? null;
|
|
4476
|
-
if (now !== target) {
|
|
4477
|
-
process.stderr.write(
|
|
4478
|
-
`${pm} install left @rness/cli ${now ?? "not installed"} in ${label}, not ${target}
|
|
4629
|
+
const now = (await readPinnedCli(ws.rnessDir))?.version ?? null;
|
|
4630
|
+
if (now !== target) {
|
|
4631
|
+
process.stderr.write(
|
|
4632
|
+
`${pm} install left @rness/cli ${now ?? "not installed"} in ${label}, not ${target}
|
|
4479
4633
|
`
|
|
4480
|
-
|
|
4481
|
-
|
|
4634
|
+
);
|
|
4635
|
+
return 1;
|
|
4636
|
+
}
|
|
4482
4637
|
}
|
|
4483
4638
|
ui.gitIsSilent = true;
|
|
4484
4639
|
const code = await syncBlocks(ui, ws.root, basename5(ws.root));
|
|
4485
4640
|
if (code !== 0) return code;
|
|
4486
|
-
|
|
4641
|
+
const message = `chore: rness ${target}`;
|
|
4642
|
+
const committed = await commitUpgrade(ws.rnessDir, message);
|
|
4643
|
+
if (committed.kind === "committed")
|
|
4644
|
+
ui.line(
|
|
4645
|
+
"committed",
|
|
4646
|
+
`${label} \u2014 ${message}`,
|
|
4647
|
+
`Committed ${label}: ${message}`
|
|
4648
|
+
);
|
|
4649
|
+
if (committed.kind === "refused") {
|
|
4650
|
+
ui.error(
|
|
4651
|
+
`the commit of ${label} was refused; everything is staged \u2014 deal with what git said, then commit`
|
|
4652
|
+
);
|
|
4653
|
+
for (const line2 of committed.output.split("\n"))
|
|
4654
|
+
if (line2.trim() !== "") ui.hint(line2, "stderr");
|
|
4655
|
+
}
|
|
4656
|
+
return finish(ui, cwd, ws.rnessDir, label, target, {
|
|
4657
|
+
commit: committed.kind,
|
|
4658
|
+
clones: await clonesToCommit(
|
|
4659
|
+
ws.root,
|
|
4660
|
+
cwd,
|
|
4661
|
+
await loadManifest(ws.rnessDir)
|
|
4662
|
+
)
|
|
4663
|
+
});
|
|
4487
4664
|
} catch (e) {
|
|
4488
4665
|
return reportError(e);
|
|
4489
4666
|
}
|
|
4490
4667
|
}
|
|
4491
4668
|
|
|
4492
4669
|
// src/core/blocks.ts
|
|
4493
|
-
import { join as
|
|
4670
|
+
import { join as join22 } from "path";
|
|
4494
4671
|
var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
|
|
4495
4672
|
async function checkBlocks({
|
|
4496
4673
|
root,
|
|
@@ -4507,7 +4684,7 @@ async function checkBlocks({
|
|
|
4507
4684
|
warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
|
|
4508
4685
|
checks
|
|
4509
4686
|
});
|
|
4510
|
-
if (!await exists(
|
|
4687
|
+
if (!await exists(join22(root, "org"))) return derive();
|
|
4511
4688
|
const targets = [
|
|
4512
4689
|
{ scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
|
|
4513
4690
|
{ scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
|
|
@@ -4516,7 +4693,7 @@ async function checkBlocks({
|
|
|
4516
4693
|
const segments = entry.path.split("/");
|
|
4517
4694
|
targets.push({
|
|
4518
4695
|
scope: name,
|
|
4519
|
-
dir:
|
|
4696
|
+
dir: join22(root, ...segments),
|
|
4520
4697
|
depth: segments.length,
|
|
4521
4698
|
file: "AGENTS.md",
|
|
4522
4699
|
label: `${entry.path}/AGENTS.md`
|
|
@@ -4531,7 +4708,7 @@ async function checkBlocks({
|
|
|
4531
4708
|
);
|
|
4532
4709
|
continue;
|
|
4533
4710
|
}
|
|
4534
|
-
const text = await readOrNull(
|
|
4711
|
+
const text = await readOrNull(join22(t.dir, t.file));
|
|
4535
4712
|
const lines = text === null ? [] : text.split(/\r?\n/);
|
|
4536
4713
|
const found = findBlock(lines);
|
|
4537
4714
|
if (found.kind === "none") {
|
|
@@ -4546,7 +4723,7 @@ async function checkBlocks({
|
|
|
4546
4723
|
note(t.label, "malformed", `${t.label}: ${found.message}`);
|
|
4547
4724
|
continue;
|
|
4548
4725
|
}
|
|
4549
|
-
const
|
|
4726
|
+
const context2 = await assembleContext({
|
|
4550
4727
|
rnessDir,
|
|
4551
4728
|
manifest,
|
|
4552
4729
|
scope: t.scope
|
|
@@ -4555,7 +4732,7 @@ async function checkBlocks({
|
|
|
4555
4732
|
scope: t.scope,
|
|
4556
4733
|
org,
|
|
4557
4734
|
depth: t.depth,
|
|
4558
|
-
context
|
|
4735
|
+
context: context2
|
|
4559
4736
|
});
|
|
4560
4737
|
const pointerLeft = t.file === "CLAUDE.md" && lines.some((l) => l.trim() === "@AGENTS.md");
|
|
4561
4738
|
if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash) && !pointerLeft)
|
|
@@ -4570,62 +4747,6 @@ async function checkBlocks({
|
|
|
4570
4747
|
return derive();
|
|
4571
4748
|
}
|
|
4572
4749
|
|
|
4573
|
-
// src/core/contract.ts
|
|
4574
|
-
import { join as join22 } from "path";
|
|
4575
|
-
var STATUSES = {
|
|
4576
|
-
adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
|
|
4577
|
-
specs: [
|
|
4578
|
-
"Draft",
|
|
4579
|
-
"Proposed",
|
|
4580
|
-
"Approved",
|
|
4581
|
-
"Implemented",
|
|
4582
|
-
"Superseded",
|
|
4583
|
-
"Rejected"
|
|
4584
|
-
],
|
|
4585
|
-
plans: ["Draft", "Ready", "In progress", "Blocked", "Completed", "Abandoned"]
|
|
4586
|
-
};
|
|
4587
|
-
var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
|
|
4588
|
-
var PLACEMENT_KEYS = ["scopes", "scope"];
|
|
4589
|
-
async function checkContract(rnessDir) {
|
|
4590
|
-
const problems = [];
|
|
4591
|
-
for (const name of COLLECTIONS) {
|
|
4592
|
-
const allowed = STATUSES[name];
|
|
4593
|
-
const items = await collectMarkdown(join22(rnessDir, name));
|
|
4594
|
-
for (const item of items) {
|
|
4595
|
-
const where = `${name}/${item.rel}`;
|
|
4596
|
-
if (SKIP.has(where)) continue;
|
|
4597
|
-
if (item.fieldsError !== null) {
|
|
4598
|
-
problems.push(`${where}: invalid front matter (${item.fieldsError})`);
|
|
4599
|
-
continue;
|
|
4600
|
-
}
|
|
4601
|
-
if (item.fields !== null) {
|
|
4602
|
-
for (const key of PLACEMENT_KEYS) {
|
|
4603
|
-
if (Object.hasOwn(item.fields, key)) {
|
|
4604
|
-
problems.push(
|
|
4605
|
-
`${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`
|
|
4606
|
-
);
|
|
4607
|
-
}
|
|
4608
|
-
}
|
|
4609
|
-
}
|
|
4610
|
-
if (allowed === void 0) continue;
|
|
4611
|
-
if (item.fields === null) {
|
|
4612
|
-
problems.push(`${where}: missing a front-matter block`);
|
|
4613
|
-
continue;
|
|
4614
|
-
}
|
|
4615
|
-
if (item.title === null) problems.push(`${where}: missing a "# " title`);
|
|
4616
|
-
const status2 = item.fields["status"];
|
|
4617
|
-
if (status2 === void 0 || status2 === null || status2 === "") {
|
|
4618
|
-
problems.push(`${where}: missing "status"`);
|
|
4619
|
-
} else if (typeof status2 !== "string" || !allowed.includes(status2)) {
|
|
4620
|
-
problems.push(
|
|
4621
|
-
`${where}: unknown status ${JSON.stringify(status2)} (expected ${allowed.join(", ")})`
|
|
4622
|
-
);
|
|
4623
|
-
}
|
|
4624
|
-
}
|
|
4625
|
-
}
|
|
4626
|
-
return problems;
|
|
4627
|
-
}
|
|
4628
|
-
|
|
4629
4750
|
// src/commands/validate.ts
|
|
4630
4751
|
async function validateCommand(opts, deps = {}) {
|
|
4631
4752
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -4714,9 +4835,9 @@ async function readStdin() {
|
|
|
4714
4835
|
async function gitCredentialCommand(operation, input) {
|
|
4715
4836
|
if (operation !== "get") return 0;
|
|
4716
4837
|
const fields = /* @__PURE__ */ new Map();
|
|
4717
|
-
for (const
|
|
4718
|
-
const eq =
|
|
4719
|
-
if (eq > 0) fields.set(
|
|
4838
|
+
for (const line2 of (input ?? await readStdin()).split("\n")) {
|
|
4839
|
+
const eq = line2.indexOf("=");
|
|
4840
|
+
if (eq > 0) fields.set(line2.slice(0, eq), line2.slice(eq + 1).trimEnd());
|
|
4720
4841
|
}
|
|
4721
4842
|
if (fields.get("protocol") !== "https" || fields.get("host") !== "github.com")
|
|
4722
4843
|
return 0;
|
|
@@ -4728,6 +4849,409 @@ password=${resolved.token}
|
|
|
4728
4849
|
return 0;
|
|
4729
4850
|
}
|
|
4730
4851
|
|
|
4852
|
+
// src/commands/mcp.ts
|
|
4853
|
+
import { createInterface } from "readline";
|
|
4854
|
+
|
|
4855
|
+
// src/mcp/protocol.ts
|
|
4856
|
+
var MODERN_VERSIONS = ["2026-07-28"];
|
|
4857
|
+
var LEGACY_VERSIONS = [
|
|
4858
|
+
"2025-11-25",
|
|
4859
|
+
"2025-06-18",
|
|
4860
|
+
"2025-03-26",
|
|
4861
|
+
"2024-11-05"
|
|
4862
|
+
];
|
|
4863
|
+
var SUPPORTED = [...MODERN_VERSIONS, ...LEGACY_VERSIONS];
|
|
4864
|
+
var VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
4865
|
+
var CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
|
|
4866
|
+
var SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
|
|
4867
|
+
function isObject2(value) {
|
|
4868
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4869
|
+
}
|
|
4870
|
+
function errorLine(id, code, message, data) {
|
|
4871
|
+
return JSON.stringify({
|
|
4872
|
+
jsonrpc: "2.0",
|
|
4873
|
+
id,
|
|
4874
|
+
error: data === void 0 ? { code, message } : { code, message, data }
|
|
4875
|
+
});
|
|
4876
|
+
}
|
|
4877
|
+
function createProtocol(opts) {
|
|
4878
|
+
const serverInfo = { name: opts.name, version: opts.version };
|
|
4879
|
+
return {
|
|
4880
|
+
async handle(line2) {
|
|
4881
|
+
let message;
|
|
4882
|
+
try {
|
|
4883
|
+
message = JSON.parse(line2);
|
|
4884
|
+
} catch {
|
|
4885
|
+
return errorLine(null, -32700, "Parse error");
|
|
4886
|
+
}
|
|
4887
|
+
const rawId = isObject2(message) ? message["id"] : void 0;
|
|
4888
|
+
const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
|
|
4889
|
+
if (!isObject2(message) || message["jsonrpc"] !== "2.0" || typeof message["method"] !== "string")
|
|
4890
|
+
return errorLine(id, -32600, "Invalid Request");
|
|
4891
|
+
if (!("id" in message)) return null;
|
|
4892
|
+
if (id === null) return errorLine(null, -32600, "Invalid Request");
|
|
4893
|
+
const method = message["method"];
|
|
4894
|
+
const params = isObject2(message["params"]) ? message["params"] : {};
|
|
4895
|
+
const meta = isObject2(params["_meta"]) ? params["_meta"] : null;
|
|
4896
|
+
const modern = meta !== null && typeof meta[VERSION_KEY] === "string";
|
|
4897
|
+
if (modern) {
|
|
4898
|
+
const requested = meta[VERSION_KEY];
|
|
4899
|
+
if (!MODERN_VERSIONS.includes(requested))
|
|
4900
|
+
return errorLine(id, -32022, "Unsupported protocol version", {
|
|
4901
|
+
supported: SUPPORTED,
|
|
4902
|
+
requested
|
|
4903
|
+
});
|
|
4904
|
+
if (!isObject2(meta[CAPABILITIES_KEY]))
|
|
4905
|
+
return errorLine(
|
|
4906
|
+
id,
|
|
4907
|
+
-32602,
|
|
4908
|
+
`Invalid params: ${CAPABILITIES_KEY} is required`
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4911
|
+
const answer = (result) => JSON.stringify({
|
|
4912
|
+
jsonrpc: "2.0",
|
|
4913
|
+
id,
|
|
4914
|
+
result: modern ? {
|
|
4915
|
+
resultType: "complete",
|
|
4916
|
+
...result,
|
|
4917
|
+
_meta: { [SERVER_INFO_KEY]: serverInfo }
|
|
4918
|
+
} : result
|
|
4919
|
+
});
|
|
4920
|
+
switch (method) {
|
|
4921
|
+
case "server/discover":
|
|
4922
|
+
return answer({
|
|
4923
|
+
supportedVersions: SUPPORTED,
|
|
4924
|
+
capabilities: { tools: {} }
|
|
4925
|
+
});
|
|
4926
|
+
case "initialize": {
|
|
4927
|
+
const asked = params["protocolVersion"];
|
|
4928
|
+
const protocolVersion = typeof asked === "string" && LEGACY_VERSIONS.includes(asked) ? asked : LEGACY_VERSIONS[0];
|
|
4929
|
+
return JSON.stringify({
|
|
4930
|
+
jsonrpc: "2.0",
|
|
4931
|
+
id,
|
|
4932
|
+
result: {
|
|
4933
|
+
protocolVersion,
|
|
4934
|
+
capabilities: { tools: {} },
|
|
4935
|
+
serverInfo
|
|
4936
|
+
}
|
|
4937
|
+
});
|
|
4938
|
+
}
|
|
4939
|
+
case "ping":
|
|
4940
|
+
return answer({});
|
|
4941
|
+
case "tools/list":
|
|
4942
|
+
return answer({
|
|
4943
|
+
tools: opts.tools.map((t) => ({
|
|
4944
|
+
name: t.name,
|
|
4945
|
+
title: t.title,
|
|
4946
|
+
description: t.description,
|
|
4947
|
+
inputSchema: t.inputSchema
|
|
4948
|
+
}))
|
|
4949
|
+
});
|
|
4950
|
+
case "tools/call": {
|
|
4951
|
+
const name = params["name"];
|
|
4952
|
+
const tool = opts.tools.find((t) => t.name === name);
|
|
4953
|
+
if (tool === void 0)
|
|
4954
|
+
return errorLine(id, -32602, `Unknown tool: ${String(name)}`);
|
|
4955
|
+
const args = isObject2(params["arguments"]) ? params["arguments"] : {};
|
|
4956
|
+
try {
|
|
4957
|
+
const r = await tool.call(args);
|
|
4958
|
+
return answer({
|
|
4959
|
+
content: [{ type: "text", text: r.text }],
|
|
4960
|
+
isError: r.isError === true
|
|
4961
|
+
});
|
|
4962
|
+
} catch (e) {
|
|
4963
|
+
return answer({
|
|
4964
|
+
content: [
|
|
4965
|
+
{
|
|
4966
|
+
type: "text",
|
|
4967
|
+
text: `${tool.name} failed: ${e instanceof Error ? e.message : String(e)}`
|
|
4968
|
+
}
|
|
4969
|
+
],
|
|
4970
|
+
isError: true
|
|
4971
|
+
});
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
default:
|
|
4975
|
+
return errorLine(id, -32601, "Method not found");
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
};
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4981
|
+
// src/mcp/tools.ts
|
|
4982
|
+
import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
|
|
4983
|
+
import { isAbsolute, join as join23, posix as posix4, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
|
|
4984
|
+
var LABELS = {
|
|
4985
|
+
standards: "Standards",
|
|
4986
|
+
adr: "Decisions",
|
|
4987
|
+
specs: "Specifications",
|
|
4988
|
+
plans: "Plans",
|
|
4989
|
+
skills: "Skills"
|
|
4990
|
+
};
|
|
4991
|
+
var TEMPLATE = "adr/0000-template.md";
|
|
4992
|
+
var READ_LIMIT = 256 * 1024;
|
|
4993
|
+
var SEARCH_LIMIT = 20;
|
|
4994
|
+
var LINES_PER_HIT = 3;
|
|
4995
|
+
var LINE_WIDTH = 200;
|
|
4996
|
+
var refuse = (text) => ({ text, isError: true });
|
|
4997
|
+
async function guard(run3) {
|
|
4998
|
+
try {
|
|
4999
|
+
return await run3();
|
|
5000
|
+
} catch (e) {
|
|
5001
|
+
return refuse(e instanceof Error ? e.message : String(e));
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
async function workspaceAt(cwd) {
|
|
5005
|
+
const ws = await findWorkspace(cwd);
|
|
5006
|
+
return { ws, manifest: await loadManifest(ws.rnessDir) };
|
|
5007
|
+
}
|
|
5008
|
+
function titleOf(heading, id) {
|
|
5009
|
+
if (id === null) return heading;
|
|
5010
|
+
const rest = new RegExp(`^${id}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
|
|
5011
|
+
return rest ?? heading;
|
|
5012
|
+
}
|
|
5013
|
+
async function documents(rnessDir, only) {
|
|
5014
|
+
const docs = [];
|
|
5015
|
+
for (const collection of COLLECTIONS) {
|
|
5016
|
+
if (only !== void 0 && collection !== only) continue;
|
|
5017
|
+
for (const item of await collectMarkdown(join23(rnessDir, collection))) {
|
|
5018
|
+
const path = `${collection}/${item.rel}`;
|
|
5019
|
+
if (path === TEMPLATE) continue;
|
|
5020
|
+
const status2 = item.fields?.["status"];
|
|
5021
|
+
const id = /^(\d{4})-/.exec(posix4.basename(item.rel))?.[1] ?? null;
|
|
5022
|
+
docs.push({
|
|
5023
|
+
collection,
|
|
5024
|
+
path,
|
|
5025
|
+
scope: ownerScope(item.rel),
|
|
5026
|
+
id,
|
|
5027
|
+
status: typeof status2 === "string" ? status2 : null,
|
|
5028
|
+
title: titleOf(item.title ?? posix4.basename(item.rel, ".md"), id),
|
|
5029
|
+
text: item.body
|
|
5030
|
+
});
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
return docs;
|
|
5034
|
+
}
|
|
5035
|
+
function line(d, withScope = false) {
|
|
5036
|
+
const head = [d.id, d.status].filter((x) => x !== null).join(" ");
|
|
5037
|
+
return `- ${head === "" ? "" : `${head} \u2014 `}${d.title}${withScope ? ` \u2014 ${d.scope ?? "global"}` : ""} \u2014 ${d.path}`;
|
|
5038
|
+
}
|
|
5039
|
+
function isCollection(value) {
|
|
5040
|
+
return COLLECTIONS.includes(value);
|
|
5041
|
+
}
|
|
5042
|
+
var collectionError = (value) => refuse(
|
|
5043
|
+
`unknown collection ${JSON.stringify(value)}; one of ${COLLECTIONS.join(", ")}`
|
|
5044
|
+
);
|
|
5045
|
+
async function context(cwd, args) {
|
|
5046
|
+
const { ws, manifest } = await workspaceAt(cwd);
|
|
5047
|
+
let scope;
|
|
5048
|
+
if (args["scope"] !== void 0) {
|
|
5049
|
+
const known = Object.keys(manifest.scopes).sort();
|
|
5050
|
+
if (typeof args["scope"] !== "string" || !Object.hasOwn(manifest.scopes, args["scope"]))
|
|
5051
|
+
return refuse(
|
|
5052
|
+
`unknown scope ${JSON.stringify(args["scope"])}; known: ${known.length === 0 ? "none" : known.join(", ")}`
|
|
5053
|
+
);
|
|
5054
|
+
scope = args["scope"];
|
|
5055
|
+
} else
|
|
5056
|
+
scope = resolveScope(
|
|
5057
|
+
manifest,
|
|
5058
|
+
relative6(ws.root, cwd).split(sep4).join(posix4.sep)
|
|
5059
|
+
);
|
|
5060
|
+
const entry = scope === null ? void 0 : manifest.scopes[scope];
|
|
5061
|
+
const where = scope === null || entry === void 0 ? "global scope" : `scope ${scope} (${entry.path}${entry.extends.length > 0 ? `, extends ${entry.extends.join(", ")}` : ""})`;
|
|
5062
|
+
const ctx = await assembleContext({ rnessDir: ws.rnessDir, manifest, scope });
|
|
5063
|
+
const index = new Map(
|
|
5064
|
+
(await documents(ws.rnessDir)).map((d) => [d.path, d])
|
|
5065
|
+
);
|
|
5066
|
+
const out = [`Workspace ${workspaceName(manifest, ws.root)} \xB7 ${where}`];
|
|
5067
|
+
for (const collection of ctx.collections) {
|
|
5068
|
+
const docs = collection.files.map((f) => index.get(`${collection.name}/${f.rel}`)).filter((d) => d !== void 0);
|
|
5069
|
+
if (docs.length === 0) continue;
|
|
5070
|
+
out.push(`${LABELS[collection.name]} (${docs.length}):`);
|
|
5071
|
+
for (const d of docs) out.push(line(d));
|
|
5072
|
+
}
|
|
5073
|
+
out.push("Read one with rness_read; search with rness_search.");
|
|
5074
|
+
return { text: out.join("\n") };
|
|
5075
|
+
}
|
|
5076
|
+
async function list(cwd, args) {
|
|
5077
|
+
const { ws } = await workspaceAt(cwd);
|
|
5078
|
+
const collection = args["collection"];
|
|
5079
|
+
if (!isCollection(collection)) return collectionError(collection);
|
|
5080
|
+
const status2 = args["status"];
|
|
5081
|
+
const allowed = STATUSES[collection];
|
|
5082
|
+
if (status2 !== void 0) {
|
|
5083
|
+
if (allowed === void 0)
|
|
5084
|
+
return refuse(`${collection} have no status to filter on`);
|
|
5085
|
+
if (typeof status2 !== "string" || !allowed.includes(status2))
|
|
5086
|
+
return refuse(
|
|
5087
|
+
`unknown status ${JSON.stringify(status2)} for ${collection}; one of ${allowed.join(", ")}`
|
|
5088
|
+
);
|
|
5089
|
+
}
|
|
5090
|
+
const docs = (await documents(ws.rnessDir, collection)).filter(
|
|
5091
|
+
(d) => status2 === void 0 || d.status === status2
|
|
5092
|
+
);
|
|
5093
|
+
if (docs.length === 0)
|
|
5094
|
+
return {
|
|
5095
|
+
text: `No document in ${collection}${status2 === void 0 ? "" : ` with status ${String(status2)}`}.`
|
|
5096
|
+
};
|
|
5097
|
+
return {
|
|
5098
|
+
text: [
|
|
5099
|
+
`${LABELS[collection]} (${docs.length}):`,
|
|
5100
|
+
...docs.map((d) => line(d, true))
|
|
5101
|
+
].join("\n")
|
|
5102
|
+
};
|
|
5103
|
+
}
|
|
5104
|
+
async function read(cwd, args) {
|
|
5105
|
+
const { ws } = await workspaceAt(cwd);
|
|
5106
|
+
const path = args["path"];
|
|
5107
|
+
if (typeof path !== "string" || path.trim() === "")
|
|
5108
|
+
return refuse("path is required, relative to .rness/");
|
|
5109
|
+
const segments = path.split(/[\\/]/);
|
|
5110
|
+
if (isAbsolute(path) || segments.includes("..") || segments[0] === ".git" || segments.includes("node_modules"))
|
|
5111
|
+
return refuse(
|
|
5112
|
+
`${path}: not a document of .rness/ (a relative path, no "..", nothing under .git/ or node_modules/)`
|
|
5113
|
+
);
|
|
5114
|
+
const root = await realpath3(ws.rnessDir);
|
|
5115
|
+
let real;
|
|
5116
|
+
try {
|
|
5117
|
+
real = await realpath3(resolve6(ws.rnessDir, path));
|
|
5118
|
+
} catch {
|
|
5119
|
+
return refuse(`${path}: no such file in .rness/`);
|
|
5120
|
+
}
|
|
5121
|
+
if (!real.startsWith(`${root}${sep4}`))
|
|
5122
|
+
return refuse(`${path}: leads outside .rness/`);
|
|
5123
|
+
const info = await stat4(real);
|
|
5124
|
+
if (!info.isFile()) return refuse(`${path}: not a file`);
|
|
5125
|
+
if (info.size > READ_LIMIT)
|
|
5126
|
+
return refuse(
|
|
5127
|
+
`${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
|
|
5128
|
+
);
|
|
5129
|
+
return { text: await readFile10(real, "utf8") };
|
|
5130
|
+
}
|
|
5131
|
+
async function search(cwd, args) {
|
|
5132
|
+
const { ws } = await workspaceAt(cwd);
|
|
5133
|
+
const query = args["query"];
|
|
5134
|
+
if (typeof query !== "string" || query.trim() === "")
|
|
5135
|
+
return refuse("query is required");
|
|
5136
|
+
const collection = args["collection"];
|
|
5137
|
+
if (collection !== void 0 && !isCollection(collection))
|
|
5138
|
+
return collectionError(collection);
|
|
5139
|
+
const needle = query.toLowerCase();
|
|
5140
|
+
const hits = (await documents(ws.rnessDir, collection)).map((d) => ({
|
|
5141
|
+
d,
|
|
5142
|
+
lines: d.text.split(/\r?\n/).map((text, i) => ({ n: i + 1, text })).filter((l) => l.text.toLowerCase().includes(needle))
|
|
5143
|
+
})).filter((h) => h.lines.length > 0).sort(
|
|
5144
|
+
(a, b) => b.lines.length - a.lines.length || a.d.path.localeCompare(b.d.path)
|
|
5145
|
+
);
|
|
5146
|
+
if (hits.length === 0)
|
|
5147
|
+
return { text: `No document matches ${JSON.stringify(query)}.` };
|
|
5148
|
+
const shown = hits.slice(0, SEARCH_LIMIT);
|
|
5149
|
+
const out = [
|
|
5150
|
+
`${hits.length} document${hits.length === 1 ? "" : "s"} match${hits.length === 1 ? "es" : ""} ${JSON.stringify(query)}${hits.length > SEARCH_LIMIT ? ` (the ${SEARCH_LIMIT} most matching)` : ""}`
|
|
5151
|
+
];
|
|
5152
|
+
for (const h of shown) {
|
|
5153
|
+
out.push(line(h.d, true));
|
|
5154
|
+
for (const l of h.lines.slice(0, LINES_PER_HIT))
|
|
5155
|
+
out.push(` ${l.n}: ${l.text.trim().slice(0, LINE_WIDTH)}`);
|
|
5156
|
+
}
|
|
5157
|
+
return { text: out.join("\n") };
|
|
5158
|
+
}
|
|
5159
|
+
var collectionProperty = {
|
|
5160
|
+
type: "string",
|
|
5161
|
+
enum: [...COLLECTIONS],
|
|
5162
|
+
description: "standards (rules), adr (decisions), specs (specifications), plans, skills"
|
|
5163
|
+
};
|
|
5164
|
+
function mcpTools(cwd) {
|
|
5165
|
+
return [
|
|
5166
|
+
{
|
|
5167
|
+
name: "rness_context",
|
|
5168
|
+
title: "rness: what applies here",
|
|
5169
|
+
description: "The rness scope of the current repository (or the one named) and what applies to it: the standards, decisions (ADRs), specifications and plans of that scope and of the scopes it extends \u2014 ids, statuses, titles and paths, no bodies. Start here, then read a document with rness_read.",
|
|
5170
|
+
inputSchema: {
|
|
5171
|
+
type: "object",
|
|
5172
|
+
properties: {
|
|
5173
|
+
scope: {
|
|
5174
|
+
type: "string",
|
|
5175
|
+
description: "A scope of rness.json; default: the one owning the working directory"
|
|
5176
|
+
}
|
|
5177
|
+
},
|
|
5178
|
+
additionalProperties: false
|
|
5179
|
+
},
|
|
5180
|
+
call: (args) => guard(() => context(cwd, args))
|
|
5181
|
+
},
|
|
5182
|
+
{
|
|
5183
|
+
name: "rness_list",
|
|
5184
|
+
title: "rness: a collection",
|
|
5185
|
+
description: "Every document of one collection of .rness/, across all scopes, with id, status, title, scope and path; optionally only those with a given status (for example the Accepted decisions).",
|
|
5186
|
+
inputSchema: {
|
|
5187
|
+
type: "object",
|
|
5188
|
+
properties: {
|
|
5189
|
+
collection: collectionProperty,
|
|
5190
|
+
status: {
|
|
5191
|
+
type: "string",
|
|
5192
|
+
description: "adr: Proposed, Accepted, Rejected, Superseded; specs: Draft, Proposed, Approved, Implemented, Superseded, Rejected; plans: Draft, Ready, In progress, Blocked, Completed, Abandoned"
|
|
5193
|
+
}
|
|
5194
|
+
},
|
|
5195
|
+
required: ["collection"],
|
|
5196
|
+
additionalProperties: false
|
|
5197
|
+
},
|
|
5198
|
+
call: (args) => guard(() => list(cwd, args))
|
|
5199
|
+
},
|
|
5200
|
+
{
|
|
5201
|
+
name: "rness_read",
|
|
5202
|
+
title: "rness: read a document",
|
|
5203
|
+
description: "One file of .rness/ as it is, by its path relative to .rness/ (as rness_context, rness_list and rness_search give it). Nothing outside .rness/.",
|
|
5204
|
+
inputSchema: {
|
|
5205
|
+
type: "object",
|
|
5206
|
+
properties: {
|
|
5207
|
+
path: {
|
|
5208
|
+
type: "string",
|
|
5209
|
+
description: "For example adr/0006-workspace.md"
|
|
5210
|
+
}
|
|
5211
|
+
},
|
|
5212
|
+
required: ["path"],
|
|
5213
|
+
additionalProperties: false
|
|
5214
|
+
},
|
|
5215
|
+
call: (args) => guard(() => read(cwd, args))
|
|
5216
|
+
},
|
|
5217
|
+
{
|
|
5218
|
+
name: "rness_search",
|
|
5219
|
+
title: "rness: search the context",
|
|
5220
|
+
description: "Where a subject is decided, specified or planned: a case-insensitive search of the documents of .rness/, most matching first, with the matching lines.",
|
|
5221
|
+
inputSchema: {
|
|
5222
|
+
type: "object",
|
|
5223
|
+
properties: {
|
|
5224
|
+
query: { type: "string", description: "Text to find" },
|
|
5225
|
+
collection: collectionProperty
|
|
5226
|
+
},
|
|
5227
|
+
required: ["query"],
|
|
5228
|
+
additionalProperties: false
|
|
5229
|
+
},
|
|
5230
|
+
call: (args) => guard(() => search(cwd, args))
|
|
5231
|
+
}
|
|
5232
|
+
];
|
|
5233
|
+
}
|
|
5234
|
+
|
|
5235
|
+
// src/commands/mcp.ts
|
|
5236
|
+
async function mcpCommand(opts, io = {
|
|
5237
|
+
input: process.stdin,
|
|
5238
|
+
output: process.stdout
|
|
5239
|
+
}) {
|
|
5240
|
+
const protocol = createProtocol({
|
|
5241
|
+
name: "rness",
|
|
5242
|
+
version: VERSION,
|
|
5243
|
+
tools: mcpTools(opts.cwd ?? process.cwd())
|
|
5244
|
+
});
|
|
5245
|
+
const lines = createInterface({ input: io.input, crlfDelay: Infinity });
|
|
5246
|
+
for await (const line2 of lines) {
|
|
5247
|
+
if (line2.trim() === "") continue;
|
|
5248
|
+
const answer = await protocol.handle(line2);
|
|
5249
|
+
if (answer !== null) io.output.write(`${answer}
|
|
5250
|
+
`);
|
|
5251
|
+
}
|
|
5252
|
+
return 0;
|
|
5253
|
+
}
|
|
5254
|
+
|
|
4731
5255
|
// src/cli.ts
|
|
4732
5256
|
function buildProgram(state) {
|
|
4733
5257
|
const program = new Command().name("rness").description("Configuration-plane CLI for rness workspaces").version(VERSION, "-v, --version", "print the version").exitOverride().showHelpAfterError().configureOutput({
|
|
@@ -4772,6 +5296,11 @@ function buildProgram(state) {
|
|
|
4772
5296
|
).option("-y, --yes", "do not ask for confirmation").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
4773
5297
|
state.code = await syncCommand(opts);
|
|
4774
5298
|
});
|
|
5299
|
+
program.command("mcp").description(
|
|
5300
|
+
"Serve the workspace context to agents over MCP (stdio, read-only)"
|
|
5301
|
+
).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
5302
|
+
state.code = await mcpCommand(opts);
|
|
5303
|
+
});
|
|
4775
5304
|
program.command("add").description(
|
|
4776
5305
|
"Clone (or adopt) a repository under org/ and declare it in rness.json"
|
|
4777
5306
|
).argument("<repo>", "<repo>, <owner>/<repo>, or a clone URL").option(
|
|
@@ -4965,13 +5494,13 @@ export {
|
|
|
4965
5494
|
loginCommand,
|
|
4966
5495
|
createCommand,
|
|
4967
5496
|
logoutCommand,
|
|
4968
|
-
upgradeCommand,
|
|
4969
|
-
checkBlocks,
|
|
4970
5497
|
STATUSES,
|
|
4971
5498
|
checkContract,
|
|
5499
|
+
upgradeCommand,
|
|
5500
|
+
checkBlocks,
|
|
4972
5501
|
validateCommand,
|
|
4973
5502
|
run2 as run,
|
|
4974
5503
|
OWN_COMMANDS,
|
|
4975
5504
|
catchUp
|
|
4976
5505
|
};
|
|
4977
|
-
//# sourceMappingURL=chunk-
|
|
5506
|
+
//# sourceMappingURL=chunk-N3F6N6GA.js.map
|