@rness/cli 0.8.0 → 0.9.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/README.md +62 -5
- package/dist/bin/rness.js +1 -1
- package/dist/{chunk-VQMX52HT.js → chunk-DL72BCPT.js} +628 -158
- package/dist/chunk-DL72BCPT.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-VQMX52HT.js.map +0 -1
|
@@ -797,7 +797,7 @@ function classifyProbe(url, exitCode, stderr) {
|
|
|
797
797
|
var STDERR_FLUSH_MS = 200;
|
|
798
798
|
async function probeRemote(url, timeoutMs = 6e4, credentials) {
|
|
799
799
|
positional(url, "repository url");
|
|
800
|
-
return new Promise((
|
|
800
|
+
return new Promise((resolve7) => {
|
|
801
801
|
const auth = withCredentials(credentials);
|
|
802
802
|
const child = spawn(
|
|
803
803
|
"git",
|
|
@@ -816,7 +816,7 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
|
|
|
816
816
|
settled = true;
|
|
817
817
|
clearTimeout(timer);
|
|
818
818
|
child.stderr?.destroy();
|
|
819
|
-
|
|
819
|
+
resolve7(probe);
|
|
820
820
|
};
|
|
821
821
|
const timer = setTimeout(() => {
|
|
822
822
|
child.kill("SIGKILL");
|
|
@@ -1198,8 +1198,8 @@ function warn(text) {
|
|
|
1198
1198
|
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
1199
|
var FROM = [63, 185, 80];
|
|
1200
1200
|
var TO = [57, 197, 207];
|
|
1201
|
-
function gradient(
|
|
1202
|
-
const chars = [...
|
|
1201
|
+
function gradient(line2) {
|
|
1202
|
+
const chars = [...line2];
|
|
1203
1203
|
const last = Math.max(1, chars.length - 1);
|
|
1204
1204
|
const painted = chars.map((ch, i) => {
|
|
1205
1205
|
if (ch === " ") return ch;
|
|
@@ -1216,7 +1216,7 @@ function banner(version, stream = process.stdout) {
|
|
|
1216
1216
|
const coloured = paint("done", "x", stream) !== "x";
|
|
1217
1217
|
const trueColour = coloured && typeof stream.hasColors === "function" && stream.hasColors(2 ** 24);
|
|
1218
1218
|
const word = WORD.map(
|
|
1219
|
-
(
|
|
1219
|
+
(line2) => trueColour ? gradient(line2) : paint("done", line2, stream)
|
|
1220
1220
|
);
|
|
1221
1221
|
return [
|
|
1222
1222
|
"",
|
|
@@ -1248,7 +1248,7 @@ async function detectGithubSsh(opts) {
|
|
|
1248
1248
|
...opts.interactive ? [] : ["-o", "BatchMode=yes"],
|
|
1249
1249
|
"git@github.com"
|
|
1250
1250
|
];
|
|
1251
|
-
return new Promise((
|
|
1251
|
+
return new Promise((resolve7) => {
|
|
1252
1252
|
const child = spawn2(opts.command ?? "ssh", args, {
|
|
1253
1253
|
stdio: [opts.interactive ? "inherit" : "ignore", "ignore", "pipe"]
|
|
1254
1254
|
});
|
|
@@ -1261,7 +1261,7 @@ async function detectGithubSsh(opts) {
|
|
|
1261
1261
|
settled = true;
|
|
1262
1262
|
clearTimeout(timer);
|
|
1263
1263
|
child.stderr?.destroy();
|
|
1264
|
-
|
|
1264
|
+
resolve7(access2);
|
|
1265
1265
|
};
|
|
1266
1266
|
const timer = setTimeout(() => {
|
|
1267
1267
|
child.kill("SIGKILL");
|
|
@@ -1390,7 +1390,7 @@ function reportError(e) {
|
|
|
1390
1390
|
}
|
|
1391
1391
|
|
|
1392
1392
|
// src/version.ts
|
|
1393
|
-
var VERSION = "0.
|
|
1393
|
+
var VERSION = "0.9.0";
|
|
1394
1394
|
|
|
1395
1395
|
// src/core/block.ts
|
|
1396
1396
|
import { createHash } from "crypto";
|
|
@@ -1427,9 +1427,9 @@ function renderBlock({
|
|
|
1427
1427
|
scope,
|
|
1428
1428
|
org,
|
|
1429
1429
|
depth,
|
|
1430
|
-
context
|
|
1430
|
+
context: context2
|
|
1431
1431
|
}) {
|
|
1432
|
-
const standards =
|
|
1432
|
+
const standards = context2.collections.find((c) => c.name === "standards")?.files ?? [];
|
|
1433
1433
|
const rules = standards.length === 0 ? ["_No standards apply to this scope yet._"] : standards.map(
|
|
1434
1434
|
(f) => `<!-- rness: standards/${f.rel} -->
|
|
1435
1435
|
${toLf(f.content).trim()}`
|
|
@@ -1442,8 +1442,8 @@ ${rules.join("\n\n")}`;
|
|
|
1442
1442
|
const text = [BEGIN, header(scope, hash), body, END].join("\n");
|
|
1443
1443
|
return { text, hash, bytes: Buffer.byteLength(text, "utf8") };
|
|
1444
1444
|
}
|
|
1445
|
-
function parseHeader(
|
|
1446
|
-
const m = HEADER.exec(
|
|
1445
|
+
function parseHeader(line2) {
|
|
1446
|
+
const m = HEADER.exec(line2.trimEnd());
|
|
1447
1447
|
if (m === null) return null;
|
|
1448
1448
|
return { version: m[1] ?? null, scope: m[2] ?? "", hash: m[3] ?? "" };
|
|
1449
1449
|
}
|
|
@@ -1614,8 +1614,8 @@ import { join as join8 } from "path";
|
|
|
1614
1614
|
function findBlock(lines) {
|
|
1615
1615
|
const begins = [];
|
|
1616
1616
|
const ends = [];
|
|
1617
|
-
lines.forEach((
|
|
1618
|
-
const l =
|
|
1617
|
+
lines.forEach((line2, i) => {
|
|
1618
|
+
const l = line2.trimEnd();
|
|
1619
1619
|
if (l === BEGIN) begins.push(i);
|
|
1620
1620
|
if (l === END) ends.push(i);
|
|
1621
1621
|
});
|
|
@@ -1695,14 +1695,37 @@ var TARGETS = {
|
|
|
1695
1695
|
claude: {
|
|
1696
1696
|
name: "claude",
|
|
1697
1697
|
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: [
|
|
1698
|
+
files: [
|
|
1703
1699
|
{
|
|
1704
|
-
|
|
1705
|
-
|
|
1700
|
+
file: ".claude/settings.json",
|
|
1701
|
+
guarantees: [
|
|
1702
|
+
// Read the context repository from a repository without a prompt.
|
|
1703
|
+
// Resolved against the project directory, and applied once Claude
|
|
1704
|
+
// Code has trusted it (verified on 2.1.284, spec 0011 §7).
|
|
1705
|
+
{
|
|
1706
|
+
path: ["permissions", "additionalDirectories"],
|
|
1707
|
+
contains: "../../.rness"
|
|
1708
|
+
},
|
|
1709
|
+
// Start the rness MCP server of .mcp.json without asking.
|
|
1710
|
+
{ path: ["enabledMcpjsonServers"], contains: "rness" }
|
|
1711
|
+
]
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
file: ".mcp.json",
|
|
1715
|
+
guarantees: [
|
|
1716
|
+
// The pinned copy, by a path relative to the repository — the
|
|
1717
|
+
// directory Claude Code starts the server in (spec 0014 §6).
|
|
1718
|
+
{
|
|
1719
|
+
path: ["mcpServers", "rness"],
|
|
1720
|
+
value: {
|
|
1721
|
+
command: "node",
|
|
1722
|
+
args: [
|
|
1723
|
+
"../../.rness/node_modules/@rness/cli/dist/bin/rness.js",
|
|
1724
|
+
"mcp"
|
|
1725
|
+
]
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
]
|
|
1706
1729
|
}
|
|
1707
1730
|
]
|
|
1708
1731
|
}
|
|
@@ -1728,7 +1751,7 @@ function parse3(text) {
|
|
|
1728
1751
|
}
|
|
1729
1752
|
return isObject(data) ? { ok: true, data } : { ok: false, reason: "not a JSON object" };
|
|
1730
1753
|
}
|
|
1731
|
-
function
|
|
1754
|
+
function parentOf(root, g, create) {
|
|
1732
1755
|
let node = root;
|
|
1733
1756
|
for (const [i, key] of g.path.slice(0, -1).entries()) {
|
|
1734
1757
|
const next = node[key];
|
|
@@ -1742,17 +1765,32 @@ function listAt(root, g, create) {
|
|
|
1742
1765
|
};
|
|
1743
1766
|
node = node[key];
|
|
1744
1767
|
}
|
|
1768
|
+
return { kind: "node", node };
|
|
1769
|
+
}
|
|
1770
|
+
function apply(root, g, create) {
|
|
1771
|
+
const found = parentOf(root, g, create);
|
|
1772
|
+
if (found.kind === "absent") return { holds: false, changed: false };
|
|
1773
|
+
if (found.kind === "invalid") return { invalid: found.reason };
|
|
1774
|
+
const parent = found.node;
|
|
1745
1775
|
const last = g.path[g.path.length - 1] ?? "";
|
|
1746
|
-
const value =
|
|
1776
|
+
const value = parent[last];
|
|
1777
|
+
if ("value" in g) {
|
|
1778
|
+
if (value !== void 0) return { holds: true, changed: false };
|
|
1779
|
+
if (!create) return { holds: false, changed: false };
|
|
1780
|
+
parent[last] = structuredClone(g.value);
|
|
1781
|
+
return { holds: true, changed: true };
|
|
1782
|
+
}
|
|
1747
1783
|
if (value === void 0) {
|
|
1748
|
-
if (!create) return {
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
return { kind: "list", list };
|
|
1784
|
+
if (!create) return { holds: false, changed: false };
|
|
1785
|
+
parent[last] = [g.contains];
|
|
1786
|
+
return { holds: true, changed: true };
|
|
1752
1787
|
}
|
|
1753
1788
|
if (!Array.isArray(value))
|
|
1754
|
-
return {
|
|
1755
|
-
return {
|
|
1789
|
+
return { invalid: `${g.path.join(".")} is not a list` };
|
|
1790
|
+
if (value.includes(g.contains)) return { holds: true, changed: false };
|
|
1791
|
+
if (!create) return { holds: false, changed: false };
|
|
1792
|
+
value.push(g.contains);
|
|
1793
|
+
return { holds: true, changed: true };
|
|
1756
1794
|
}
|
|
1757
1795
|
function styleOf(text) {
|
|
1758
1796
|
const indent = text.split(/\r?\n/).slice(1).map((l) => /^[ \t]+/.exec(l)?.[0]).find((w) => w !== void 0);
|
|
@@ -1767,13 +1805,9 @@ function ensureGuarantees(existing, guarantees) {
|
|
|
1767
1805
|
if (!parsed.ok) return { kind: "invalid", reason: parsed.reason };
|
|
1768
1806
|
let changed = false;
|
|
1769
1807
|
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
|
-
}
|
|
1808
|
+
const r = apply(parsed.data, g, true);
|
|
1809
|
+
if ("invalid" in r) return { kind: "invalid", reason: r.invalid };
|
|
1810
|
+
changed ||= r.changed;
|
|
1777
1811
|
}
|
|
1778
1812
|
if (existing === null)
|
|
1779
1813
|
return {
|
|
@@ -1792,15 +1826,14 @@ function missingGuarantees(existing, guarantees) {
|
|
|
1792
1826
|
if (!parsed.ok) return { invalid: parsed.reason };
|
|
1793
1827
|
const missing = [];
|
|
1794
1828
|
for (const g of guarantees) {
|
|
1795
|
-
const
|
|
1796
|
-
if (
|
|
1797
|
-
if (
|
|
1798
|
-
missing.push(g);
|
|
1829
|
+
const r = apply(parsed.data, g, false);
|
|
1830
|
+
if ("invalid" in r) return { invalid: r.invalid };
|
|
1831
|
+
if (!r.holds) missing.push(g);
|
|
1799
1832
|
}
|
|
1800
1833
|
return missing;
|
|
1801
1834
|
}
|
|
1802
1835
|
function describeGuarantee(g) {
|
|
1803
|
-
return `${g.path.join(".")} lacks ${g.contains}`;
|
|
1836
|
+
return "value" in g ? `${g.path.join(".")} is missing` : `${g.path.join(".")} lacks ${g.contains}`;
|
|
1804
1837
|
}
|
|
1805
1838
|
|
|
1806
1839
|
// src/core/agent-targets.ts
|
|
@@ -1809,11 +1842,12 @@ async function filesOf(root, manifest, targets) {
|
|
|
1809
1842
|
for (const repo of Object.keys(manifest.repos)) {
|
|
1810
1843
|
if (!await exists(join9(root, "org", repo))) continue;
|
|
1811
1844
|
for (const target of targets)
|
|
1812
|
-
files
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1845
|
+
for (const owned of target.files)
|
|
1846
|
+
files.push({
|
|
1847
|
+
guarantees: owned.guarantees,
|
|
1848
|
+
file: join9(root, "org", repo, ...owned.file.split("/")),
|
|
1849
|
+
label: `org/${repo}/${owned.file}`
|
|
1850
|
+
});
|
|
1817
1851
|
}
|
|
1818
1852
|
return files;
|
|
1819
1853
|
}
|
|
@@ -1824,14 +1858,14 @@ function declared(manifest) {
|
|
|
1824
1858
|
}
|
|
1825
1859
|
async function agentTargets(root, manifest, opts) {
|
|
1826
1860
|
const outcomes = [];
|
|
1827
|
-
for (const {
|
|
1861
|
+
for (const { guarantees, file, label } of await filesOf(
|
|
1828
1862
|
root,
|
|
1829
1863
|
manifest,
|
|
1830
1864
|
declared(manifest)
|
|
1831
1865
|
)) {
|
|
1832
1866
|
const existing = await readOrNull(file);
|
|
1833
1867
|
if (opts.check) {
|
|
1834
|
-
const missing = missingGuarantees(existing,
|
|
1868
|
+
const missing = missingGuarantees(existing, guarantees);
|
|
1835
1869
|
if ("invalid" in missing)
|
|
1836
1870
|
outcomes.push({ label, status: "invalid", detail: missing.invalid });
|
|
1837
1871
|
else if (missing.length > 0)
|
|
@@ -1843,7 +1877,7 @@ async function agentTargets(root, manifest, opts) {
|
|
|
1843
1877
|
else outcomes.push({ label, status: "unchanged", detail: null });
|
|
1844
1878
|
continue;
|
|
1845
1879
|
}
|
|
1846
|
-
const ensured = ensureGuarantees(existing,
|
|
1880
|
+
const ensured = ensureGuarantees(existing, guarantees);
|
|
1847
1881
|
if (ensured.kind === "invalid") {
|
|
1848
1882
|
outcomes.push({ label, status: "invalid", detail: ensured.reason });
|
|
1849
1883
|
continue;
|
|
@@ -1865,14 +1899,14 @@ async function leftoverTargets(root, manifest) {
|
|
|
1865
1899
|
(t) => !agents.includes(t.name)
|
|
1866
1900
|
);
|
|
1867
1901
|
const leftovers = [];
|
|
1868
|
-
for (const {
|
|
1902
|
+
for (const { guarantees, file, label } of await filesOf(
|
|
1869
1903
|
root,
|
|
1870
1904
|
manifest,
|
|
1871
1905
|
undeclared
|
|
1872
1906
|
)) {
|
|
1873
1907
|
const existing = await readOrNull(file);
|
|
1874
1908
|
if (existing === null) continue;
|
|
1875
|
-
const missing = missingGuarantees(existing,
|
|
1909
|
+
const missing = missingGuarantees(existing, guarantees);
|
|
1876
1910
|
if (!("invalid" in missing) && missing.length === 0) leftovers.push(label);
|
|
1877
1911
|
}
|
|
1878
1912
|
return leftovers;
|
|
@@ -1920,7 +1954,7 @@ async function step(options, work) {
|
|
|
1920
1954
|
const rawErr = process.stderr.write.bind(
|
|
1921
1955
|
process.stderr
|
|
1922
1956
|
);
|
|
1923
|
-
const
|
|
1957
|
+
const guard2 = (raw) => ((...args) => {
|
|
1924
1958
|
erase();
|
|
1925
1959
|
return raw(...args);
|
|
1926
1960
|
});
|
|
@@ -1935,8 +1969,8 @@ async function step(options, work) {
|
|
|
1935
1969
|
} else {
|
|
1936
1970
|
draw("\u2026");
|
|
1937
1971
|
}
|
|
1938
|
-
stream.write =
|
|
1939
|
-
if (process.stderr !== stream) process.stderr.write =
|
|
1972
|
+
stream.write = guard2(rawWrite);
|
|
1973
|
+
if (process.stderr !== stream) process.stderr.write = guard2(rawErr);
|
|
1940
1974
|
try {
|
|
1941
1975
|
return await work();
|
|
1942
1976
|
} finally {
|
|
@@ -1983,8 +2017,8 @@ var plainUi = {
|
|
|
1983
2017
|
{ label: options.doing, animate: options.git !== true },
|
|
1984
2018
|
work
|
|
1985
2019
|
);
|
|
1986
|
-
const
|
|
1987
|
-
if (
|
|
2020
|
+
const line2 = options.quiet === true ? null : done?.(result) ?? null;
|
|
2021
|
+
if (line2 !== null) this.line(line2[0], line2[1]);
|
|
1988
2022
|
return result;
|
|
1989
2023
|
}
|
|
1990
2024
|
};
|
|
@@ -2017,7 +2051,7 @@ function sessionUi(p) {
|
|
|
2017
2051
|
p.log.warn(text);
|
|
2018
2052
|
},
|
|
2019
2053
|
note: (title, lines) => {
|
|
2020
|
-
p.note(lines.join("\n"), title, { format: (
|
|
2054
|
+
p.note(lines.join("\n"), title, { format: (line2) => grey(line2) });
|
|
2021
2055
|
},
|
|
2022
2056
|
hint: (text) => {
|
|
2023
2057
|
p.log.message(paint("idle", text));
|
|
@@ -2031,20 +2065,20 @@ function sessionUi(p) {
|
|
|
2031
2065
|
async step(options, work, done) {
|
|
2032
2066
|
const label = options.sentence ?? tidy(options.doing);
|
|
2033
2067
|
const finish2 = (result) => done?.(result) ?? null;
|
|
2034
|
-
const label_ = (
|
|
2068
|
+
const label_ = (line2) => `${line2[1]} ${line2[0]}`;
|
|
2035
2069
|
if (options.git === true && !this.gitIsSilent) {
|
|
2036
2070
|
p.log.message(paint("idle", `${label}\u2026`));
|
|
2037
2071
|
const result = await work();
|
|
2038
|
-
const
|
|
2039
|
-
if (
|
|
2072
|
+
const line2 = finish2(result);
|
|
2073
|
+
if (line2 !== null) say(line2[0], label_(line2));
|
|
2040
2074
|
return result;
|
|
2041
2075
|
}
|
|
2042
2076
|
const spin = p.spinner();
|
|
2043
2077
|
spin.start(label);
|
|
2044
2078
|
try {
|
|
2045
2079
|
const result = await work();
|
|
2046
|
-
const
|
|
2047
|
-
spin.stop(
|
|
2080
|
+
const line2 = finish2(result);
|
|
2081
|
+
spin.stop(line2 === null ? label : label_(line2));
|
|
2048
2082
|
return result;
|
|
2049
2083
|
} catch (e) {
|
|
2050
2084
|
spin.error(`${label} \u2014 failed`);
|
|
@@ -2263,7 +2297,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
2263
2297
|
const written = [
|
|
2264
2298
|
...MANAGED_FILES,
|
|
2265
2299
|
...(manifest.agents ?? []).flatMap(
|
|
2266
|
-
(a) => Object.hasOwn(TARGETS, a) ?
|
|
2300
|
+
(a) => Object.hasOwn(TARGETS, a) ? TARGETS[a].files.map((f) => f.file) : []
|
|
2267
2301
|
)
|
|
2268
2302
|
];
|
|
2269
2303
|
if (!await isClean(dir, written)) {
|
|
@@ -2316,7 +2350,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
2316
2350
|
lines.push(["skipped", `${t.label} (symlink)`]);
|
|
2317
2351
|
continue;
|
|
2318
2352
|
}
|
|
2319
|
-
const
|
|
2353
|
+
const context2 = await assembleContext({
|
|
2320
2354
|
rnessDir: ws.rnessDir,
|
|
2321
2355
|
manifest,
|
|
2322
2356
|
scope: t.scope
|
|
@@ -2325,7 +2359,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
2325
2359
|
scope: t.scope,
|
|
2326
2360
|
org,
|
|
2327
2361
|
depth: t.depth,
|
|
2328
|
-
context
|
|
2362
|
+
context: context2
|
|
2329
2363
|
});
|
|
2330
2364
|
if (block.bytes > BLOCK_SIZE_WARNING) {
|
|
2331
2365
|
ui.warn(
|
|
@@ -3062,10 +3096,10 @@ async function runPinnedSync(root) {
|
|
|
3062
3096
|
function summariseSync(stdout) {
|
|
3063
3097
|
const counts = /* @__PURE__ */ new Map();
|
|
3064
3098
|
const others = [];
|
|
3065
|
-
for (const
|
|
3066
|
-
if (
|
|
3067
|
-
const verb = /^(updated|unchanged|stale)\s/.exec(
|
|
3068
|
-
if (verb === void 0) others.push(
|
|
3099
|
+
for (const line2 of stdout.split("\n")) {
|
|
3100
|
+
if (line2.trim() === "") continue;
|
|
3101
|
+
const verb = /^(updated|unchanged|stale)\s/.exec(line2)?.[1];
|
|
3102
|
+
if (verb === void 0) others.push(line2);
|
|
3069
3103
|
else counts.set(verb, (counts.get(verb) ?? 0) + 1);
|
|
3070
3104
|
}
|
|
3071
3105
|
const total = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
@@ -3269,8 +3303,8 @@ async function mergeScaffold(rnessDir, commit, base) {
|
|
|
3269
3303
|
"--name-status",
|
|
3270
3304
|
"HEAD"
|
|
3271
3305
|
]);
|
|
3272
|
-
for (const
|
|
3273
|
-
const [letter = "", path = ""] =
|
|
3306
|
+
for (const line2 of status2.split("\n")) {
|
|
3307
|
+
const [letter = "", path = ""] = line2.split(" ");
|
|
3274
3308
|
if (path === "" || conflicts.includes(path)) continue;
|
|
3275
3309
|
if (letter === "A") changes.push(["added", path]);
|
|
3276
3310
|
else if (letter === "D") changes.push(["removed", path]);
|
|
@@ -3291,6 +3325,24 @@ async function isRepository2(rnessDir) {
|
|
|
3291
3325
|
const real = (p) => realpath2(p).catch(() => resolve3(p));
|
|
3292
3326
|
return await real(r.stdout.trim()) === await real(rnessDir);
|
|
3293
3327
|
}
|
|
3328
|
+
async function withoutRedundantKeeps(rnessDir, files) {
|
|
3329
|
+
const kept = [];
|
|
3330
|
+
for (const file of files) {
|
|
3331
|
+
if (file.path.endsWith("/.gitkeep")) {
|
|
3332
|
+
const dir = file.path.slice(0, -"/.gitkeep".length);
|
|
3333
|
+
const listed = await run(rnessDir, [
|
|
3334
|
+
"ls-tree",
|
|
3335
|
+
"--name-only",
|
|
3336
|
+
"HEAD",
|
|
3337
|
+
"--",
|
|
3338
|
+
`${dir}/`
|
|
3339
|
+
]);
|
|
3340
|
+
if (listed.code === 0 && listed.stdout.trim() !== "") continue;
|
|
3341
|
+
}
|
|
3342
|
+
kept.push(file);
|
|
3343
|
+
}
|
|
3344
|
+
return kept;
|
|
3345
|
+
}
|
|
3294
3346
|
|
|
3295
3347
|
// src/core/new-workspace.ts
|
|
3296
3348
|
async function isEmptyDir(dir) {
|
|
@@ -3395,8 +3447,8 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
|
|
|
3395
3447
|
return 1;
|
|
3396
3448
|
}
|
|
3397
3449
|
const outcome = syncOutcome(result, { sshWorks: sshWorks === true });
|
|
3398
|
-
for (const
|
|
3399
|
-
for (const
|
|
3450
|
+
for (const line2 of outcome.hints) ui.hint(line2);
|
|
3451
|
+
for (const line2 of outcome.errors) ui.error(line2);
|
|
3400
3452
|
return outcome.code;
|
|
3401
3453
|
}
|
|
3402
3454
|
|
|
@@ -3501,7 +3553,7 @@ async function createBlank(input) {
|
|
|
3501
3553
|
`Workspace \`${name}\` is ready in ${shown}/.`,
|
|
3502
3554
|
"",
|
|
3503
3555
|
"Next:",
|
|
3504
|
-
...next.map((
|
|
3556
|
+
...next.map((line2) => ` ${line2}`),
|
|
3505
3557
|
""
|
|
3506
3558
|
].join("\n")
|
|
3507
3559
|
);
|
|
@@ -3512,8 +3564,8 @@ async function createBlank(input) {
|
|
|
3512
3564
|
function firstLine4(text) {
|
|
3513
3565
|
return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "unknown error";
|
|
3514
3566
|
}
|
|
3515
|
-
function splitSpecs(
|
|
3516
|
-
return
|
|
3567
|
+
function splitSpecs(list2) {
|
|
3568
|
+
return list2.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
3517
3569
|
}
|
|
3518
3570
|
function cancelled(ui) {
|
|
3519
3571
|
ui.cancelled();
|
|
@@ -4144,7 +4196,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
|
|
|
4144
4196
|
`Workspace for \`${org}\` is ready in ${shown}/.`,
|
|
4145
4197
|
"",
|
|
4146
4198
|
"Next:",
|
|
4147
|
-
...next.map((
|
|
4199
|
+
...next.map((line2) => ` ${line2}`),
|
|
4148
4200
|
""
|
|
4149
4201
|
].join("\n")
|
|
4150
4202
|
);
|
|
@@ -4185,11 +4237,67 @@ async function logoutCommand() {
|
|
|
4185
4237
|
}
|
|
4186
4238
|
}
|
|
4187
4239
|
|
|
4240
|
+
// src/core/contract.ts
|
|
4241
|
+
import { join as join20 } from "path";
|
|
4242
|
+
var STATUSES = {
|
|
4243
|
+
adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
|
|
4244
|
+
specs: [
|
|
4245
|
+
"Draft",
|
|
4246
|
+
"Proposed",
|
|
4247
|
+
"Approved",
|
|
4248
|
+
"Implemented",
|
|
4249
|
+
"Superseded",
|
|
4250
|
+
"Rejected"
|
|
4251
|
+
],
|
|
4252
|
+
plans: ["Draft", "Ready", "In progress", "Blocked", "Completed", "Abandoned"]
|
|
4253
|
+
};
|
|
4254
|
+
var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
|
|
4255
|
+
var PLACEMENT_KEYS = ["scopes", "scope"];
|
|
4256
|
+
async function checkContract(rnessDir) {
|
|
4257
|
+
const problems = [];
|
|
4258
|
+
for (const name of COLLECTIONS) {
|
|
4259
|
+
const allowed = STATUSES[name];
|
|
4260
|
+
const items = await collectMarkdown(join20(rnessDir, name));
|
|
4261
|
+
for (const item of items) {
|
|
4262
|
+
const where = `${name}/${item.rel}`;
|
|
4263
|
+
if (SKIP.has(where)) continue;
|
|
4264
|
+
if (item.fieldsError !== null) {
|
|
4265
|
+
problems.push(`${where}: invalid front matter (${item.fieldsError})`);
|
|
4266
|
+
continue;
|
|
4267
|
+
}
|
|
4268
|
+
if (item.fields !== null) {
|
|
4269
|
+
for (const key of PLACEMENT_KEYS) {
|
|
4270
|
+
if (Object.hasOwn(item.fields, key)) {
|
|
4271
|
+
problems.push(
|
|
4272
|
+
`${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`
|
|
4273
|
+
);
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
if (allowed === void 0) continue;
|
|
4278
|
+
if (item.fields === null) {
|
|
4279
|
+
problems.push(`${where}: missing a front-matter block`);
|
|
4280
|
+
continue;
|
|
4281
|
+
}
|
|
4282
|
+
if (item.title === null) problems.push(`${where}: missing a "# " title`);
|
|
4283
|
+
const status2 = item.fields["status"];
|
|
4284
|
+
if (status2 === void 0 || status2 === null || status2 === "") {
|
|
4285
|
+
problems.push(`${where}: missing "status"`);
|
|
4286
|
+
} else if (typeof status2 !== "string" || !allowed.includes(status2)) {
|
|
4287
|
+
problems.push(
|
|
4288
|
+
`${where}: unknown status ${JSON.stringify(status2)} (expected ${allowed.join(", ")})`
|
|
4289
|
+
);
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
return problems;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4188
4296
|
// src/commands/upgrade.ts
|
|
4189
4297
|
import { execFile as execFile6 } from "child_process";
|
|
4190
4298
|
import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
|
|
4191
4299
|
import { tmpdir as tmpdir3 } from "os";
|
|
4192
|
-
import { basename as basename5, join as
|
|
4300
|
+
import { basename as basename5, join as join21, relative as relative5 } from "path";
|
|
4193
4301
|
import { promisify as promisify6 } from "util";
|
|
4194
4302
|
var execFileP6 = promisify6(execFile6);
|
|
4195
4303
|
var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
|
|
@@ -4209,10 +4317,10 @@ async function latestVersion() {
|
|
|
4209
4317
|
async function scaffoldFor(version, rnessDir) {
|
|
4210
4318
|
if (version === VERSION) return { dir: scaffoldDir() };
|
|
4211
4319
|
const installed = await readPinnedCli(rnessDir);
|
|
4212
|
-
const inside = installed === null ? null :
|
|
4213
|
-
if (installed?.version === version && inside !== null && await exists(
|
|
4320
|
+
const inside = installed === null ? null : join21(installed.dir, "scaffold");
|
|
4321
|
+
if (installed?.version === version && inside !== null && await exists(join21(inside, "package.json")))
|
|
4214
4322
|
return { dir: inside };
|
|
4215
|
-
const scratch = await mkdtemp3(
|
|
4323
|
+
const scratch = await mkdtemp3(join21(tmpdir3(), "rness-pack-"));
|
|
4216
4324
|
const cleanup = () => rm7(scratch, { recursive: true, force: true });
|
|
4217
4325
|
try {
|
|
4218
4326
|
const { stdout } = await execFileP6(
|
|
@@ -4227,8 +4335,8 @@ async function scaffoldFor(version, rnessDir) {
|
|
|
4227
4335
|
{ timeout: 6e4 }
|
|
4228
4336
|
);
|
|
4229
4337
|
const tarball = stdout.trim().split("\n").at(-1) ?? "";
|
|
4230
|
-
await execFileP6("tar", ["-xzf",
|
|
4231
|
-
return { dir:
|
|
4338
|
+
await execFileP6("tar", ["-xzf", join21(scratch, tarball), "-C", scratch]);
|
|
4339
|
+
return { dir: join21(scratch, "package", "scaffold"), cleanup };
|
|
4232
4340
|
} catch (e) {
|
|
4233
4341
|
await cleanup();
|
|
4234
4342
|
throw new Error(
|
|
@@ -4240,7 +4348,7 @@ async function scaffoldFor(version, rnessDir) {
|
|
|
4240
4348
|
async function declaredManager(rnessDir, pm) {
|
|
4241
4349
|
try {
|
|
4242
4350
|
const pkg = JSON.parse(
|
|
4243
|
-
await readFile9(
|
|
4351
|
+
await readFile9(join21(rnessDir, "package.json"), "utf8")
|
|
4244
4352
|
);
|
|
4245
4353
|
if (typeof pkg.packageManager === "string") return pkg.packageManager;
|
|
4246
4354
|
} catch {
|
|
@@ -4263,7 +4371,7 @@ function finish(ui, cwd, rnessDir, label, target) {
|
|
|
4263
4371
|
status("upgraded", `${label} to @rness/cli ${target}`),
|
|
4264
4372
|
"",
|
|
4265
4373
|
"Next:",
|
|
4266
|
-
...next.map((
|
|
4374
|
+
...next.map((line2) => ` ${line2}`),
|
|
4267
4375
|
""
|
|
4268
4376
|
].join("\n")
|
|
4269
4377
|
);
|
|
@@ -4341,7 +4449,11 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
4341
4449
|
}
|
|
4342
4450
|
const pm = await workspacePackageManager(ws.rnessDir);
|
|
4343
4451
|
if (pin.spec === target && installed === target)
|
|
4344
|
-
ui.line(
|
|
4452
|
+
ui.line(
|
|
4453
|
+
"scaffold",
|
|
4454
|
+
`@rness/cli ${target} \u2014 merging it into ${label}`,
|
|
4455
|
+
`Merging the @rness/cli ${target} scaffold into ${label}`
|
|
4456
|
+
);
|
|
4345
4457
|
else if (pin.spec === target)
|
|
4346
4458
|
ui.line(
|
|
4347
4459
|
"install",
|
|
@@ -4378,13 +4490,19 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
4378
4490
|
if (base === null)
|
|
4379
4491
|
ui.line(
|
|
4380
4492
|
"adopting",
|
|
4381
|
-
`the @rness/cli ${target} scaffold (no scaffold commit in ${label} yet)
|
|
4493
|
+
`the @rness/cli ${target} scaffold (no scaffold commit in ${label} yet)`,
|
|
4494
|
+
`Adopting the @rness/cli ${target} scaffold \u2014 ${label} has no scaffold commit yet`
|
|
4495
|
+
);
|
|
4496
|
+
else
|
|
4497
|
+
ui.line(
|
|
4498
|
+
"merging",
|
|
4499
|
+
`the @rness/cli ${target} scaffold`,
|
|
4500
|
+
`Merging the @rness/cli ${target} scaffold`
|
|
4382
4501
|
);
|
|
4383
|
-
else ui.line("merging", `the @rness/cli ${target} scaffold`);
|
|
4384
4502
|
const commit = await buildScaffoldCommit(
|
|
4385
4503
|
ws.rnessDir,
|
|
4386
4504
|
base?.commit ?? null,
|
|
4387
|
-
files,
|
|
4505
|
+
base === null ? await withoutRedundantKeeps(ws.rnessDir, files) : files,
|
|
4388
4506
|
target
|
|
4389
4507
|
);
|
|
4390
4508
|
const result = await mergeScaffold(
|
|
@@ -4407,7 +4525,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
4407
4525
|
await source.cleanup?.();
|
|
4408
4526
|
}
|
|
4409
4527
|
}
|
|
4410
|
-
const packageFile =
|
|
4528
|
+
const packageFile = join21(ws.rnessDir, "package.json");
|
|
4411
4529
|
const restore = /* @__PURE__ */ new Map([
|
|
4412
4530
|
[packageFile, await readFile9(packageFile, "utf8")]
|
|
4413
4531
|
]);
|
|
@@ -4462,7 +4580,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
4462
4580
|
}
|
|
4463
4581
|
|
|
4464
4582
|
// src/core/blocks.ts
|
|
4465
|
-
import { join as
|
|
4583
|
+
import { join as join22 } from "path";
|
|
4466
4584
|
var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
|
|
4467
4585
|
async function checkBlocks({
|
|
4468
4586
|
root,
|
|
@@ -4479,7 +4597,7 @@ async function checkBlocks({
|
|
|
4479
4597
|
warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
|
|
4480
4598
|
checks
|
|
4481
4599
|
});
|
|
4482
|
-
if (!await exists(
|
|
4600
|
+
if (!await exists(join22(root, "org"))) return derive();
|
|
4483
4601
|
const targets = [
|
|
4484
4602
|
{ scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
|
|
4485
4603
|
{ scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
|
|
@@ -4488,7 +4606,7 @@ async function checkBlocks({
|
|
|
4488
4606
|
const segments = entry.path.split("/");
|
|
4489
4607
|
targets.push({
|
|
4490
4608
|
scope: name,
|
|
4491
|
-
dir:
|
|
4609
|
+
dir: join22(root, ...segments),
|
|
4492
4610
|
depth: segments.length,
|
|
4493
4611
|
file: "AGENTS.md",
|
|
4494
4612
|
label: `${entry.path}/AGENTS.md`
|
|
@@ -4503,7 +4621,7 @@ async function checkBlocks({
|
|
|
4503
4621
|
);
|
|
4504
4622
|
continue;
|
|
4505
4623
|
}
|
|
4506
|
-
const text = await readOrNull(
|
|
4624
|
+
const text = await readOrNull(join22(t.dir, t.file));
|
|
4507
4625
|
const lines = text === null ? [] : text.split(/\r?\n/);
|
|
4508
4626
|
const found = findBlock(lines);
|
|
4509
4627
|
if (found.kind === "none") {
|
|
@@ -4518,7 +4636,7 @@ async function checkBlocks({
|
|
|
4518
4636
|
note(t.label, "malformed", `${t.label}: ${found.message}`);
|
|
4519
4637
|
continue;
|
|
4520
4638
|
}
|
|
4521
|
-
const
|
|
4639
|
+
const context2 = await assembleContext({
|
|
4522
4640
|
rnessDir,
|
|
4523
4641
|
manifest,
|
|
4524
4642
|
scope: t.scope
|
|
@@ -4527,7 +4645,7 @@ async function checkBlocks({
|
|
|
4527
4645
|
scope: t.scope,
|
|
4528
4646
|
org,
|
|
4529
4647
|
depth: t.depth,
|
|
4530
|
-
context
|
|
4648
|
+
context: context2
|
|
4531
4649
|
});
|
|
4532
4650
|
const pointerLeft = t.file === "CLAUDE.md" && lines.some((l) => l.trim() === "@AGENTS.md");
|
|
4533
4651
|
if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash) && !pointerLeft)
|
|
@@ -4542,62 +4660,6 @@ async function checkBlocks({
|
|
|
4542
4660
|
return derive();
|
|
4543
4661
|
}
|
|
4544
4662
|
|
|
4545
|
-
// src/core/contract.ts
|
|
4546
|
-
import { join as join22 } from "path";
|
|
4547
|
-
var STATUSES = {
|
|
4548
|
-
adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
|
|
4549
|
-
specs: [
|
|
4550
|
-
"Draft",
|
|
4551
|
-
"Proposed",
|
|
4552
|
-
"Approved",
|
|
4553
|
-
"Implemented",
|
|
4554
|
-
"Superseded",
|
|
4555
|
-
"Rejected"
|
|
4556
|
-
],
|
|
4557
|
-
plans: ["Draft", "Ready", "In progress", "Blocked", "Completed", "Abandoned"]
|
|
4558
|
-
};
|
|
4559
|
-
var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
|
|
4560
|
-
var PLACEMENT_KEYS = ["scopes", "scope"];
|
|
4561
|
-
async function checkContract(rnessDir) {
|
|
4562
|
-
const problems = [];
|
|
4563
|
-
for (const name of COLLECTIONS) {
|
|
4564
|
-
const allowed = STATUSES[name];
|
|
4565
|
-
const items = await collectMarkdown(join22(rnessDir, name));
|
|
4566
|
-
for (const item of items) {
|
|
4567
|
-
const where = `${name}/${item.rel}`;
|
|
4568
|
-
if (SKIP.has(where)) continue;
|
|
4569
|
-
if (item.fieldsError !== null) {
|
|
4570
|
-
problems.push(`${where}: invalid front matter (${item.fieldsError})`);
|
|
4571
|
-
continue;
|
|
4572
|
-
}
|
|
4573
|
-
if (item.fields !== null) {
|
|
4574
|
-
for (const key of PLACEMENT_KEYS) {
|
|
4575
|
-
if (Object.hasOwn(item.fields, key)) {
|
|
4576
|
-
problems.push(
|
|
4577
|
-
`${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`
|
|
4578
|
-
);
|
|
4579
|
-
}
|
|
4580
|
-
}
|
|
4581
|
-
}
|
|
4582
|
-
if (allowed === void 0) continue;
|
|
4583
|
-
if (item.fields === null) {
|
|
4584
|
-
problems.push(`${where}: missing a front-matter block`);
|
|
4585
|
-
continue;
|
|
4586
|
-
}
|
|
4587
|
-
if (item.title === null) problems.push(`${where}: missing a "# " title`);
|
|
4588
|
-
const status2 = item.fields["status"];
|
|
4589
|
-
if (status2 === void 0 || status2 === null || status2 === "") {
|
|
4590
|
-
problems.push(`${where}: missing "status"`);
|
|
4591
|
-
} else if (typeof status2 !== "string" || !allowed.includes(status2)) {
|
|
4592
|
-
problems.push(
|
|
4593
|
-
`${where}: unknown status ${JSON.stringify(status2)} (expected ${allowed.join(", ")})`
|
|
4594
|
-
);
|
|
4595
|
-
}
|
|
4596
|
-
}
|
|
4597
|
-
}
|
|
4598
|
-
return problems;
|
|
4599
|
-
}
|
|
4600
|
-
|
|
4601
4663
|
// src/commands/validate.ts
|
|
4602
4664
|
async function validateCommand(opts, deps = {}) {
|
|
4603
4665
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -4686,9 +4748,9 @@ async function readStdin() {
|
|
|
4686
4748
|
async function gitCredentialCommand(operation, input) {
|
|
4687
4749
|
if (operation !== "get") return 0;
|
|
4688
4750
|
const fields = /* @__PURE__ */ new Map();
|
|
4689
|
-
for (const
|
|
4690
|
-
const eq =
|
|
4691
|
-
if (eq > 0) fields.set(
|
|
4751
|
+
for (const line2 of (input ?? await readStdin()).split("\n")) {
|
|
4752
|
+
const eq = line2.indexOf("=");
|
|
4753
|
+
if (eq > 0) fields.set(line2.slice(0, eq), line2.slice(eq + 1).trimEnd());
|
|
4692
4754
|
}
|
|
4693
4755
|
if (fields.get("protocol") !== "https" || fields.get("host") !== "github.com")
|
|
4694
4756
|
return 0;
|
|
@@ -4700,6 +4762,409 @@ password=${resolved.token}
|
|
|
4700
4762
|
return 0;
|
|
4701
4763
|
}
|
|
4702
4764
|
|
|
4765
|
+
// src/commands/mcp.ts
|
|
4766
|
+
import { createInterface } from "readline";
|
|
4767
|
+
|
|
4768
|
+
// src/mcp/protocol.ts
|
|
4769
|
+
var MODERN_VERSIONS = ["2026-07-28"];
|
|
4770
|
+
var LEGACY_VERSIONS = [
|
|
4771
|
+
"2025-11-25",
|
|
4772
|
+
"2025-06-18",
|
|
4773
|
+
"2025-03-26",
|
|
4774
|
+
"2024-11-05"
|
|
4775
|
+
];
|
|
4776
|
+
var SUPPORTED = [...MODERN_VERSIONS, ...LEGACY_VERSIONS];
|
|
4777
|
+
var VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
4778
|
+
var CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
|
|
4779
|
+
var SERVER_INFO_KEY = "io.modelcontextprotocol/serverInfo";
|
|
4780
|
+
function isObject2(value) {
|
|
4781
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4782
|
+
}
|
|
4783
|
+
function errorLine(id, code, message, data) {
|
|
4784
|
+
return JSON.stringify({
|
|
4785
|
+
jsonrpc: "2.0",
|
|
4786
|
+
id,
|
|
4787
|
+
error: data === void 0 ? { code, message } : { code, message, data }
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
function createProtocol(opts) {
|
|
4791
|
+
const serverInfo = { name: opts.name, version: opts.version };
|
|
4792
|
+
return {
|
|
4793
|
+
async handle(line2) {
|
|
4794
|
+
let message;
|
|
4795
|
+
try {
|
|
4796
|
+
message = JSON.parse(line2);
|
|
4797
|
+
} catch {
|
|
4798
|
+
return errorLine(null, -32700, "Parse error");
|
|
4799
|
+
}
|
|
4800
|
+
const rawId = isObject2(message) ? message["id"] : void 0;
|
|
4801
|
+
const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : null;
|
|
4802
|
+
if (!isObject2(message) || message["jsonrpc"] !== "2.0" || typeof message["method"] !== "string")
|
|
4803
|
+
return errorLine(id, -32600, "Invalid Request");
|
|
4804
|
+
if (!("id" in message)) return null;
|
|
4805
|
+
if (id === null) return errorLine(null, -32600, "Invalid Request");
|
|
4806
|
+
const method = message["method"];
|
|
4807
|
+
const params = isObject2(message["params"]) ? message["params"] : {};
|
|
4808
|
+
const meta = isObject2(params["_meta"]) ? params["_meta"] : null;
|
|
4809
|
+
const modern = meta !== null && typeof meta[VERSION_KEY] === "string";
|
|
4810
|
+
if (modern) {
|
|
4811
|
+
const requested = meta[VERSION_KEY];
|
|
4812
|
+
if (!MODERN_VERSIONS.includes(requested))
|
|
4813
|
+
return errorLine(id, -32022, "Unsupported protocol version", {
|
|
4814
|
+
supported: SUPPORTED,
|
|
4815
|
+
requested
|
|
4816
|
+
});
|
|
4817
|
+
if (!isObject2(meta[CAPABILITIES_KEY]))
|
|
4818
|
+
return errorLine(
|
|
4819
|
+
id,
|
|
4820
|
+
-32602,
|
|
4821
|
+
`Invalid params: ${CAPABILITIES_KEY} is required`
|
|
4822
|
+
);
|
|
4823
|
+
}
|
|
4824
|
+
const answer = (result) => JSON.stringify({
|
|
4825
|
+
jsonrpc: "2.0",
|
|
4826
|
+
id,
|
|
4827
|
+
result: modern ? {
|
|
4828
|
+
resultType: "complete",
|
|
4829
|
+
...result,
|
|
4830
|
+
_meta: { [SERVER_INFO_KEY]: serverInfo }
|
|
4831
|
+
} : result
|
|
4832
|
+
});
|
|
4833
|
+
switch (method) {
|
|
4834
|
+
case "server/discover":
|
|
4835
|
+
return answer({
|
|
4836
|
+
supportedVersions: SUPPORTED,
|
|
4837
|
+
capabilities: { tools: {} }
|
|
4838
|
+
});
|
|
4839
|
+
case "initialize": {
|
|
4840
|
+
const asked = params["protocolVersion"];
|
|
4841
|
+
const protocolVersion = typeof asked === "string" && LEGACY_VERSIONS.includes(asked) ? asked : LEGACY_VERSIONS[0];
|
|
4842
|
+
return JSON.stringify({
|
|
4843
|
+
jsonrpc: "2.0",
|
|
4844
|
+
id,
|
|
4845
|
+
result: {
|
|
4846
|
+
protocolVersion,
|
|
4847
|
+
capabilities: { tools: {} },
|
|
4848
|
+
serverInfo
|
|
4849
|
+
}
|
|
4850
|
+
});
|
|
4851
|
+
}
|
|
4852
|
+
case "ping":
|
|
4853
|
+
return answer({});
|
|
4854
|
+
case "tools/list":
|
|
4855
|
+
return answer({
|
|
4856
|
+
tools: opts.tools.map((t) => ({
|
|
4857
|
+
name: t.name,
|
|
4858
|
+
title: t.title,
|
|
4859
|
+
description: t.description,
|
|
4860
|
+
inputSchema: t.inputSchema
|
|
4861
|
+
}))
|
|
4862
|
+
});
|
|
4863
|
+
case "tools/call": {
|
|
4864
|
+
const name = params["name"];
|
|
4865
|
+
const tool = opts.tools.find((t) => t.name === name);
|
|
4866
|
+
if (tool === void 0)
|
|
4867
|
+
return errorLine(id, -32602, `Unknown tool: ${String(name)}`);
|
|
4868
|
+
const args = isObject2(params["arguments"]) ? params["arguments"] : {};
|
|
4869
|
+
try {
|
|
4870
|
+
const r = await tool.call(args);
|
|
4871
|
+
return answer({
|
|
4872
|
+
content: [{ type: "text", text: r.text }],
|
|
4873
|
+
isError: r.isError === true
|
|
4874
|
+
});
|
|
4875
|
+
} catch (e) {
|
|
4876
|
+
return answer({
|
|
4877
|
+
content: [
|
|
4878
|
+
{
|
|
4879
|
+
type: "text",
|
|
4880
|
+
text: `${tool.name} failed: ${e instanceof Error ? e.message : String(e)}`
|
|
4881
|
+
}
|
|
4882
|
+
],
|
|
4883
|
+
isError: true
|
|
4884
|
+
});
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
default:
|
|
4888
|
+
return errorLine(id, -32601, "Method not found");
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
};
|
|
4892
|
+
}
|
|
4893
|
+
|
|
4894
|
+
// src/mcp/tools.ts
|
|
4895
|
+
import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
|
|
4896
|
+
import { isAbsolute, join as join23, posix as posix4, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
|
|
4897
|
+
var LABELS = {
|
|
4898
|
+
standards: "Standards",
|
|
4899
|
+
adr: "Decisions",
|
|
4900
|
+
specs: "Specifications",
|
|
4901
|
+
plans: "Plans",
|
|
4902
|
+
skills: "Skills"
|
|
4903
|
+
};
|
|
4904
|
+
var TEMPLATE = "adr/0000-template.md";
|
|
4905
|
+
var READ_LIMIT = 256 * 1024;
|
|
4906
|
+
var SEARCH_LIMIT = 20;
|
|
4907
|
+
var LINES_PER_HIT = 3;
|
|
4908
|
+
var LINE_WIDTH = 200;
|
|
4909
|
+
var refuse = (text) => ({ text, isError: true });
|
|
4910
|
+
async function guard(run3) {
|
|
4911
|
+
try {
|
|
4912
|
+
return await run3();
|
|
4913
|
+
} catch (e) {
|
|
4914
|
+
return refuse(e instanceof Error ? e.message : String(e));
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
async function workspaceAt(cwd) {
|
|
4918
|
+
const ws = await findWorkspace(cwd);
|
|
4919
|
+
return { ws, manifest: await loadManifest(ws.rnessDir) };
|
|
4920
|
+
}
|
|
4921
|
+
function titleOf(heading, id) {
|
|
4922
|
+
if (id === null) return heading;
|
|
4923
|
+
const rest = new RegExp(`^${id}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
|
|
4924
|
+
return rest ?? heading;
|
|
4925
|
+
}
|
|
4926
|
+
async function documents(rnessDir, only) {
|
|
4927
|
+
const docs = [];
|
|
4928
|
+
for (const collection of COLLECTIONS) {
|
|
4929
|
+
if (only !== void 0 && collection !== only) continue;
|
|
4930
|
+
for (const item of await collectMarkdown(join23(rnessDir, collection))) {
|
|
4931
|
+
const path = `${collection}/${item.rel}`;
|
|
4932
|
+
if (path === TEMPLATE) continue;
|
|
4933
|
+
const status2 = item.fields?.["status"];
|
|
4934
|
+
const id = /^(\d{4})-/.exec(posix4.basename(item.rel))?.[1] ?? null;
|
|
4935
|
+
docs.push({
|
|
4936
|
+
collection,
|
|
4937
|
+
path,
|
|
4938
|
+
scope: ownerScope(item.rel),
|
|
4939
|
+
id,
|
|
4940
|
+
status: typeof status2 === "string" ? status2 : null,
|
|
4941
|
+
title: titleOf(item.title ?? posix4.basename(item.rel, ".md"), id),
|
|
4942
|
+
text: item.body
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
return docs;
|
|
4947
|
+
}
|
|
4948
|
+
function line(d, withScope = false) {
|
|
4949
|
+
const head = [d.id, d.status].filter((x) => x !== null).join(" ");
|
|
4950
|
+
return `- ${head === "" ? "" : `${head} \u2014 `}${d.title}${withScope ? ` \u2014 ${d.scope ?? "global"}` : ""} \u2014 ${d.path}`;
|
|
4951
|
+
}
|
|
4952
|
+
function isCollection(value) {
|
|
4953
|
+
return COLLECTIONS.includes(value);
|
|
4954
|
+
}
|
|
4955
|
+
var collectionError = (value) => refuse(
|
|
4956
|
+
`unknown collection ${JSON.stringify(value)}; one of ${COLLECTIONS.join(", ")}`
|
|
4957
|
+
);
|
|
4958
|
+
async function context(cwd, args) {
|
|
4959
|
+
const { ws, manifest } = await workspaceAt(cwd);
|
|
4960
|
+
let scope;
|
|
4961
|
+
if (args["scope"] !== void 0) {
|
|
4962
|
+
const known = Object.keys(manifest.scopes).sort();
|
|
4963
|
+
if (typeof args["scope"] !== "string" || !Object.hasOwn(manifest.scopes, args["scope"]))
|
|
4964
|
+
return refuse(
|
|
4965
|
+
`unknown scope ${JSON.stringify(args["scope"])}; known: ${known.length === 0 ? "none" : known.join(", ")}`
|
|
4966
|
+
);
|
|
4967
|
+
scope = args["scope"];
|
|
4968
|
+
} else
|
|
4969
|
+
scope = resolveScope(
|
|
4970
|
+
manifest,
|
|
4971
|
+
relative6(ws.root, cwd).split(sep4).join(posix4.sep)
|
|
4972
|
+
);
|
|
4973
|
+
const entry = scope === null ? void 0 : manifest.scopes[scope];
|
|
4974
|
+
const where = scope === null || entry === void 0 ? "global scope" : `scope ${scope} (${entry.path}${entry.extends.length > 0 ? `, extends ${entry.extends.join(", ")}` : ""})`;
|
|
4975
|
+
const ctx = await assembleContext({ rnessDir: ws.rnessDir, manifest, scope });
|
|
4976
|
+
const index = new Map(
|
|
4977
|
+
(await documents(ws.rnessDir)).map((d) => [d.path, d])
|
|
4978
|
+
);
|
|
4979
|
+
const out = [`Workspace ${workspaceName(manifest, ws.root)} \xB7 ${where}`];
|
|
4980
|
+
for (const collection of ctx.collections) {
|
|
4981
|
+
const docs = collection.files.map((f) => index.get(`${collection.name}/${f.rel}`)).filter((d) => d !== void 0);
|
|
4982
|
+
if (docs.length === 0) continue;
|
|
4983
|
+
out.push(`${LABELS[collection.name]} (${docs.length}):`);
|
|
4984
|
+
for (const d of docs) out.push(line(d));
|
|
4985
|
+
}
|
|
4986
|
+
out.push("Read one with rness_read; search with rness_search.");
|
|
4987
|
+
return { text: out.join("\n") };
|
|
4988
|
+
}
|
|
4989
|
+
async function list(cwd, args) {
|
|
4990
|
+
const { ws } = await workspaceAt(cwd);
|
|
4991
|
+
const collection = args["collection"];
|
|
4992
|
+
if (!isCollection(collection)) return collectionError(collection);
|
|
4993
|
+
const status2 = args["status"];
|
|
4994
|
+
const allowed = STATUSES[collection];
|
|
4995
|
+
if (status2 !== void 0) {
|
|
4996
|
+
if (allowed === void 0)
|
|
4997
|
+
return refuse(`${collection} have no status to filter on`);
|
|
4998
|
+
if (typeof status2 !== "string" || !allowed.includes(status2))
|
|
4999
|
+
return refuse(
|
|
5000
|
+
`unknown status ${JSON.stringify(status2)} for ${collection}; one of ${allowed.join(", ")}`
|
|
5001
|
+
);
|
|
5002
|
+
}
|
|
5003
|
+
const docs = (await documents(ws.rnessDir, collection)).filter(
|
|
5004
|
+
(d) => status2 === void 0 || d.status === status2
|
|
5005
|
+
);
|
|
5006
|
+
if (docs.length === 0)
|
|
5007
|
+
return {
|
|
5008
|
+
text: `No document in ${collection}${status2 === void 0 ? "" : ` with status ${String(status2)}`}.`
|
|
5009
|
+
};
|
|
5010
|
+
return {
|
|
5011
|
+
text: [
|
|
5012
|
+
`${LABELS[collection]} (${docs.length}):`,
|
|
5013
|
+
...docs.map((d) => line(d, true))
|
|
5014
|
+
].join("\n")
|
|
5015
|
+
};
|
|
5016
|
+
}
|
|
5017
|
+
async function read(cwd, args) {
|
|
5018
|
+
const { ws } = await workspaceAt(cwd);
|
|
5019
|
+
const path = args["path"];
|
|
5020
|
+
if (typeof path !== "string" || path.trim() === "")
|
|
5021
|
+
return refuse("path is required, relative to .rness/");
|
|
5022
|
+
const segments = path.split(/[\\/]/);
|
|
5023
|
+
if (isAbsolute(path) || segments.includes("..") || segments[0] === ".git" || segments.includes("node_modules"))
|
|
5024
|
+
return refuse(
|
|
5025
|
+
`${path}: not a document of .rness/ (a relative path, no "..", nothing under .git/ or node_modules/)`
|
|
5026
|
+
);
|
|
5027
|
+
const root = await realpath3(ws.rnessDir);
|
|
5028
|
+
let real;
|
|
5029
|
+
try {
|
|
5030
|
+
real = await realpath3(resolve6(ws.rnessDir, path));
|
|
5031
|
+
} catch {
|
|
5032
|
+
return refuse(`${path}: no such file in .rness/`);
|
|
5033
|
+
}
|
|
5034
|
+
if (!real.startsWith(`${root}${sep4}`))
|
|
5035
|
+
return refuse(`${path}: leads outside .rness/`);
|
|
5036
|
+
const info = await stat4(real);
|
|
5037
|
+
if (!info.isFile()) return refuse(`${path}: not a file`);
|
|
5038
|
+
if (info.size > READ_LIMIT)
|
|
5039
|
+
return refuse(
|
|
5040
|
+
`${path}: ${info.size} bytes, over the 256 KiB rness_read returns`
|
|
5041
|
+
);
|
|
5042
|
+
return { text: await readFile10(real, "utf8") };
|
|
5043
|
+
}
|
|
5044
|
+
async function search(cwd, args) {
|
|
5045
|
+
const { ws } = await workspaceAt(cwd);
|
|
5046
|
+
const query = args["query"];
|
|
5047
|
+
if (typeof query !== "string" || query.trim() === "")
|
|
5048
|
+
return refuse("query is required");
|
|
5049
|
+
const collection = args["collection"];
|
|
5050
|
+
if (collection !== void 0 && !isCollection(collection))
|
|
5051
|
+
return collectionError(collection);
|
|
5052
|
+
const needle = query.toLowerCase();
|
|
5053
|
+
const hits = (await documents(ws.rnessDir, collection)).map((d) => ({
|
|
5054
|
+
d,
|
|
5055
|
+
lines: d.text.split(/\r?\n/).map((text, i) => ({ n: i + 1, text })).filter((l) => l.text.toLowerCase().includes(needle))
|
|
5056
|
+
})).filter((h) => h.lines.length > 0).sort(
|
|
5057
|
+
(a, b) => b.lines.length - a.lines.length || a.d.path.localeCompare(b.d.path)
|
|
5058
|
+
);
|
|
5059
|
+
if (hits.length === 0)
|
|
5060
|
+
return { text: `No document matches ${JSON.stringify(query)}.` };
|
|
5061
|
+
const shown = hits.slice(0, SEARCH_LIMIT);
|
|
5062
|
+
const out = [
|
|
5063
|
+
`${hits.length} document${hits.length === 1 ? "" : "s"} match${hits.length === 1 ? "es" : ""} ${JSON.stringify(query)}${hits.length > SEARCH_LIMIT ? ` (the ${SEARCH_LIMIT} most matching)` : ""}`
|
|
5064
|
+
];
|
|
5065
|
+
for (const h of shown) {
|
|
5066
|
+
out.push(line(h.d, true));
|
|
5067
|
+
for (const l of h.lines.slice(0, LINES_PER_HIT))
|
|
5068
|
+
out.push(` ${l.n}: ${l.text.trim().slice(0, LINE_WIDTH)}`);
|
|
5069
|
+
}
|
|
5070
|
+
return { text: out.join("\n") };
|
|
5071
|
+
}
|
|
5072
|
+
var collectionProperty = {
|
|
5073
|
+
type: "string",
|
|
5074
|
+
enum: [...COLLECTIONS],
|
|
5075
|
+
description: "standards (rules), adr (decisions), specs (specifications), plans, skills"
|
|
5076
|
+
};
|
|
5077
|
+
function mcpTools(cwd) {
|
|
5078
|
+
return [
|
|
5079
|
+
{
|
|
5080
|
+
name: "rness_context",
|
|
5081
|
+
title: "rness: what applies here",
|
|
5082
|
+
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.",
|
|
5083
|
+
inputSchema: {
|
|
5084
|
+
type: "object",
|
|
5085
|
+
properties: {
|
|
5086
|
+
scope: {
|
|
5087
|
+
type: "string",
|
|
5088
|
+
description: "A scope of rness.json; default: the one owning the working directory"
|
|
5089
|
+
}
|
|
5090
|
+
},
|
|
5091
|
+
additionalProperties: false
|
|
5092
|
+
},
|
|
5093
|
+
call: (args) => guard(() => context(cwd, args))
|
|
5094
|
+
},
|
|
5095
|
+
{
|
|
5096
|
+
name: "rness_list",
|
|
5097
|
+
title: "rness: a collection",
|
|
5098
|
+
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).",
|
|
5099
|
+
inputSchema: {
|
|
5100
|
+
type: "object",
|
|
5101
|
+
properties: {
|
|
5102
|
+
collection: collectionProperty,
|
|
5103
|
+
status: {
|
|
5104
|
+
type: "string",
|
|
5105
|
+
description: "adr: Proposed, Accepted, Rejected, Superseded; specs: Draft, Proposed, Approved, Implemented, Superseded, Rejected; plans: Draft, Ready, In progress, Blocked, Completed, Abandoned"
|
|
5106
|
+
}
|
|
5107
|
+
},
|
|
5108
|
+
required: ["collection"],
|
|
5109
|
+
additionalProperties: false
|
|
5110
|
+
},
|
|
5111
|
+
call: (args) => guard(() => list(cwd, args))
|
|
5112
|
+
},
|
|
5113
|
+
{
|
|
5114
|
+
name: "rness_read",
|
|
5115
|
+
title: "rness: read a document",
|
|
5116
|
+
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/.",
|
|
5117
|
+
inputSchema: {
|
|
5118
|
+
type: "object",
|
|
5119
|
+
properties: {
|
|
5120
|
+
path: {
|
|
5121
|
+
type: "string",
|
|
5122
|
+
description: "For example adr/0006-workspace.md"
|
|
5123
|
+
}
|
|
5124
|
+
},
|
|
5125
|
+
required: ["path"],
|
|
5126
|
+
additionalProperties: false
|
|
5127
|
+
},
|
|
5128
|
+
call: (args) => guard(() => read(cwd, args))
|
|
5129
|
+
},
|
|
5130
|
+
{
|
|
5131
|
+
name: "rness_search",
|
|
5132
|
+
title: "rness: search the context",
|
|
5133
|
+
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.",
|
|
5134
|
+
inputSchema: {
|
|
5135
|
+
type: "object",
|
|
5136
|
+
properties: {
|
|
5137
|
+
query: { type: "string", description: "Text to find" },
|
|
5138
|
+
collection: collectionProperty
|
|
5139
|
+
},
|
|
5140
|
+
required: ["query"],
|
|
5141
|
+
additionalProperties: false
|
|
5142
|
+
},
|
|
5143
|
+
call: (args) => guard(() => search(cwd, args))
|
|
5144
|
+
}
|
|
5145
|
+
];
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
// src/commands/mcp.ts
|
|
5149
|
+
async function mcpCommand(opts, io = {
|
|
5150
|
+
input: process.stdin,
|
|
5151
|
+
output: process.stdout
|
|
5152
|
+
}) {
|
|
5153
|
+
const protocol = createProtocol({
|
|
5154
|
+
name: "rness",
|
|
5155
|
+
version: VERSION,
|
|
5156
|
+
tools: mcpTools(opts.cwd ?? process.cwd())
|
|
5157
|
+
});
|
|
5158
|
+
const lines = createInterface({ input: io.input, crlfDelay: Infinity });
|
|
5159
|
+
for await (const line2 of lines) {
|
|
5160
|
+
if (line2.trim() === "") continue;
|
|
5161
|
+
const answer = await protocol.handle(line2);
|
|
5162
|
+
if (answer !== null) io.output.write(`${answer}
|
|
5163
|
+
`);
|
|
5164
|
+
}
|
|
5165
|
+
return 0;
|
|
5166
|
+
}
|
|
5167
|
+
|
|
4703
5168
|
// src/cli.ts
|
|
4704
5169
|
function buildProgram(state) {
|
|
4705
5170
|
const program = new Command().name("rness").description("Configuration-plane CLI for rness workspaces").version(VERSION, "-v, --version", "print the version").exitOverride().showHelpAfterError().configureOutput({
|
|
@@ -4744,6 +5209,11 @@ function buildProgram(state) {
|
|
|
4744
5209
|
).option("-y, --yes", "do not ask for confirmation").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
4745
5210
|
state.code = await syncCommand(opts);
|
|
4746
5211
|
});
|
|
5212
|
+
program.command("mcp").description(
|
|
5213
|
+
"Serve the workspace context to agents over MCP (stdio, read-only)"
|
|
5214
|
+
).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
5215
|
+
state.code = await mcpCommand(opts);
|
|
5216
|
+
});
|
|
4747
5217
|
program.command("add").description(
|
|
4748
5218
|
"Clone (or adopt) a repository under org/ and declare it in rness.json"
|
|
4749
5219
|
).argument("<repo>", "<repo>, <owner>/<repo>, or a clone URL").option(
|
|
@@ -4937,13 +5407,13 @@ export {
|
|
|
4937
5407
|
loginCommand,
|
|
4938
5408
|
createCommand,
|
|
4939
5409
|
logoutCommand,
|
|
4940
|
-
upgradeCommand,
|
|
4941
|
-
checkBlocks,
|
|
4942
5410
|
STATUSES,
|
|
4943
5411
|
checkContract,
|
|
5412
|
+
upgradeCommand,
|
|
5413
|
+
checkBlocks,
|
|
4944
5414
|
validateCommand,
|
|
4945
5415
|
run2 as run,
|
|
4946
5416
|
OWN_COMMANDS,
|
|
4947
5417
|
catchUp
|
|
4948
5418
|
};
|
|
4949
|
-
//# sourceMappingURL=chunk-
|
|
5419
|
+
//# sourceMappingURL=chunk-DL72BCPT.js.map
|