@rness/cli 0.6.1 → 0.7.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 +63 -3
- package/dist/bin/rness.js +1 -1
- package/dist/{chunk-YAIZOFYV.js → chunk-VYP4L7BQ.js} +499 -109
- package/dist/chunk-VYP4L7BQ.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/scaffold/.github/workflows/validate.yml +2 -2
- package/dist/chunk-YAIZOFYV.js.map +0 -1
|
@@ -735,6 +735,9 @@ async function isClean(dir, ignore = []) {
|
|
|
735
735
|
[
|
|
736
736
|
"status",
|
|
737
737
|
"--porcelain",
|
|
738
|
+
// Every untracked file on its own line: a directory folded into one
|
|
739
|
+
// `?? dir/` entry would slip past a pathspec naming a file inside it.
|
|
740
|
+
"--untracked-files=all",
|
|
738
741
|
...pathspec.length > 0 ? ["--", ...pathspec] : []
|
|
739
742
|
],
|
|
740
743
|
dir
|
|
@@ -857,7 +860,7 @@ function isWorkspaceClone(name) {
|
|
|
857
860
|
}
|
|
858
861
|
var NAME_RULE = "lowercase letters, digits, '.', '_' and '-', not starting with '-'";
|
|
859
862
|
var ORG_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
860
|
-
var KEYS = ["contract", "org", "repos", "scopes"];
|
|
863
|
+
var KEYS = ["contract", "org", "agents", "repos", "scopes"];
|
|
861
864
|
function fail(message) {
|
|
862
865
|
throw new Error(`rness.json: ${message}`);
|
|
863
866
|
}
|
|
@@ -886,6 +889,15 @@ function readOrg(value) {
|
|
|
886
889
|
function workspaceName(manifest, root) {
|
|
887
890
|
return manifest.org ?? basename2(root);
|
|
888
891
|
}
|
|
892
|
+
var AGENT_NAME = /^[a-z][a-z0-9-]*$/;
|
|
893
|
+
function readAgents(value) {
|
|
894
|
+
if (value === void 0) return null;
|
|
895
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === "string" && AGENT_NAME.test(v)) || new Set(value).size !== value.length)
|
|
896
|
+
fail(
|
|
897
|
+
`"agents" must be a list of unique agent names (got ${JSON.stringify(value)})`
|
|
898
|
+
);
|
|
899
|
+
return [...value];
|
|
900
|
+
}
|
|
889
901
|
function readRepos(value) {
|
|
890
902
|
const raw = value ?? {};
|
|
891
903
|
if (!isRecord(raw)) fail('"repos" must be an object');
|
|
@@ -944,6 +956,7 @@ async function loadManifest(rnessDir) {
|
|
|
944
956
|
return {
|
|
945
957
|
contract: 1,
|
|
946
958
|
org: readOrg(data.org),
|
|
959
|
+
agents: readAgents(data.agents),
|
|
947
960
|
repos: readRepos(data.repos),
|
|
948
961
|
scopes: readScopes(data.scopes)
|
|
949
962
|
};
|
|
@@ -959,6 +972,10 @@ async function writeManifest(rnessDir, manifest) {
|
|
|
959
972
|
const lines = ["{", ' "contract": 1,'];
|
|
960
973
|
if (manifest.org !== null)
|
|
961
974
|
lines.push(` "org": ${JSON.stringify(manifest.org)},`);
|
|
975
|
+
if (manifest.agents !== null)
|
|
976
|
+
lines.push(
|
|
977
|
+
` "agents": [${manifest.agents.map((a) => JSON.stringify(a)).join(", ")}],`
|
|
978
|
+
);
|
|
962
979
|
lines.push(
|
|
963
980
|
' "repos": {',
|
|
964
981
|
repos.join(",\n"),
|
|
@@ -1045,13 +1062,13 @@ async function addRepository(input) {
|
|
|
1045
1062
|
await clone(url, dir, input.credentialsFor?.(url) ?? null);
|
|
1046
1063
|
action = "cloned";
|
|
1047
1064
|
}
|
|
1048
|
-
const
|
|
1065
|
+
const declared2 = [name];
|
|
1049
1066
|
for (const { path, base } of subs) {
|
|
1050
1067
|
if (!await exists(join4(input.root, ...path.split("/")))) {
|
|
1051
1068
|
if (action === "cloned") await rm3(dir, { recursive: true, force: true });
|
|
1052
1069
|
throw new Error(`${path} does not exist`);
|
|
1053
1070
|
}
|
|
1054
|
-
|
|
1071
|
+
declared2.push(base);
|
|
1055
1072
|
}
|
|
1056
1073
|
const manifest = {
|
|
1057
1074
|
...input.manifest,
|
|
@@ -1059,7 +1076,7 @@ async function addRepository(input) {
|
|
|
1059
1076
|
scopes
|
|
1060
1077
|
};
|
|
1061
1078
|
await writeManifest(input.rnessDir, manifest);
|
|
1062
|
-
return { manifest, name, url, action, declared };
|
|
1079
|
+
return { manifest, name, url, action, declared: declared2 };
|
|
1063
1080
|
}
|
|
1064
1081
|
async function workspaceDirs(repoDir) {
|
|
1065
1082
|
let patterns = [];
|
|
@@ -1372,6 +1389,9 @@ function reportError(e) {
|
|
|
1372
1389
|
return 1;
|
|
1373
1390
|
}
|
|
1374
1391
|
|
|
1392
|
+
// src/version.ts
|
|
1393
|
+
var VERSION = "0.7.0";
|
|
1394
|
+
|
|
1375
1395
|
// src/core/block.ts
|
|
1376
1396
|
import { createHash } from "crypto";
|
|
1377
1397
|
var BEGIN = "<!-- BEGIN rness -->";
|
|
@@ -1637,6 +1657,17 @@ function mergeBlock(existing, block) {
|
|
|
1637
1657
|
}
|
|
1638
1658
|
return { ok: true, text, changed: text !== existing };
|
|
1639
1659
|
}
|
|
1660
|
+
var POINTER = "@AGENTS.md";
|
|
1661
|
+
function claudeMdAtRoot(existing, block) {
|
|
1662
|
+
if (existing === null) return mergeBlock(null, block);
|
|
1663
|
+
const lines = existing.split(/\r?\n/);
|
|
1664
|
+
if (!lines.some((l) => l.trim() === POINTER))
|
|
1665
|
+
return mergeBlock(existing, block);
|
|
1666
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
1667
|
+
const rest = lines.filter((l) => l.trim() !== POINTER).join(eol);
|
|
1668
|
+
const merged = mergeBlock(rest.trim() === "" ? null : rest, block);
|
|
1669
|
+
return merged.ok ? { ...merged, changed: merged.text !== existing } : merged;
|
|
1670
|
+
}
|
|
1640
1671
|
async function ensureClaudeMd(dir) {
|
|
1641
1672
|
const file = join8(dir, "CLAUDE.md");
|
|
1642
1673
|
if (await isSymlink(file)) return "unchanged";
|
|
@@ -1653,7 +1684,214 @@ async function ensureClaudeMd(dir) {
|
|
|
1653
1684
|
}
|
|
1654
1685
|
|
|
1655
1686
|
// src/commands/sync.ts
|
|
1656
|
-
import { join as
|
|
1687
|
+
import { join as join10 } from "path";
|
|
1688
|
+
|
|
1689
|
+
// src/core/agent-targets.ts
|
|
1690
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
1691
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
1692
|
+
|
|
1693
|
+
// src/core/agents.ts
|
|
1694
|
+
var TARGETS = {
|
|
1695
|
+
claude: {
|
|
1696
|
+
name: "claude",
|
|
1697
|
+
label: "Claude Code",
|
|
1698
|
+
file: ".claude/settings.json",
|
|
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: [
|
|
1703
|
+
{
|
|
1704
|
+
path: ["permissions", "additionalDirectories"],
|
|
1705
|
+
contains: "../../.rness"
|
|
1706
|
+
}
|
|
1707
|
+
]
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
var SUPPORTED_AGENTS = Object.keys(TARGETS);
|
|
1711
|
+
function unsupportedAgents(names) {
|
|
1712
|
+
return names.filter((n) => !Object.hasOwn(TARGETS, n));
|
|
1713
|
+
}
|
|
1714
|
+
function unsupportedMessage(name) {
|
|
1715
|
+
return `rness.json: agent "${name}" is not supported by @rness/cli ${VERSION} (supported: ${SUPPORTED_AGENTS.join(", ")})`;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/core/json-guarantees.ts
|
|
1719
|
+
function isObject(value) {
|
|
1720
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1721
|
+
}
|
|
1722
|
+
function parse3(text) {
|
|
1723
|
+
let data;
|
|
1724
|
+
try {
|
|
1725
|
+
data = JSON.parse(text);
|
|
1726
|
+
} catch {
|
|
1727
|
+
return { ok: false, reason: "not valid JSON" };
|
|
1728
|
+
}
|
|
1729
|
+
return isObject(data) ? { ok: true, data } : { ok: false, reason: "not a JSON object" };
|
|
1730
|
+
}
|
|
1731
|
+
function listAt(root, g, create) {
|
|
1732
|
+
let node = root;
|
|
1733
|
+
for (const [i, key] of g.path.slice(0, -1).entries()) {
|
|
1734
|
+
const next = node[key];
|
|
1735
|
+
if (next === void 0) {
|
|
1736
|
+
if (!create) return { kind: "absent" };
|
|
1737
|
+
node[key] = {};
|
|
1738
|
+
} else if (!isObject(next))
|
|
1739
|
+
return {
|
|
1740
|
+
kind: "invalid",
|
|
1741
|
+
reason: `${g.path.slice(0, i + 1).join(".")} is not an object`
|
|
1742
|
+
};
|
|
1743
|
+
node = node[key];
|
|
1744
|
+
}
|
|
1745
|
+
const last = g.path[g.path.length - 1] ?? "";
|
|
1746
|
+
const value = node[last];
|
|
1747
|
+
if (value === void 0) {
|
|
1748
|
+
if (!create) return { kind: "absent" };
|
|
1749
|
+
const list = [];
|
|
1750
|
+
node[last] = list;
|
|
1751
|
+
return { kind: "list", list };
|
|
1752
|
+
}
|
|
1753
|
+
if (!Array.isArray(value))
|
|
1754
|
+
return { kind: "invalid", reason: `${g.path.join(".")} is not a list` };
|
|
1755
|
+
return { kind: "list", list: value };
|
|
1756
|
+
}
|
|
1757
|
+
function styleOf(text) {
|
|
1758
|
+
const indent = text.split(/\r?\n/).slice(1).map((l) => /^[ \t]+/.exec(l)?.[0]).find((w) => w !== void 0);
|
|
1759
|
+
return {
|
|
1760
|
+
indent: indent ?? " ",
|
|
1761
|
+
eol: text.includes("\r\n") ? "\r\n" : "\n",
|
|
1762
|
+
final: text.endsWith("\n")
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
function ensureGuarantees(existing, guarantees) {
|
|
1766
|
+
const parsed = existing === null ? { ok: true, data: {} } : parse3(existing);
|
|
1767
|
+
if (!parsed.ok) return { kind: "invalid", reason: parsed.reason };
|
|
1768
|
+
let changed = false;
|
|
1769
|
+
for (const g of guarantees) {
|
|
1770
|
+
const found = listAt(parsed.data, g, true);
|
|
1771
|
+
if (found.kind === "invalid")
|
|
1772
|
+
return { kind: "invalid", reason: found.reason };
|
|
1773
|
+
if (found.kind === "list" && !found.list.includes(g.contains)) {
|
|
1774
|
+
found.list.push(g.contains);
|
|
1775
|
+
changed = true;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
if (existing === null)
|
|
1779
|
+
return {
|
|
1780
|
+
kind: "created",
|
|
1781
|
+
text: `${JSON.stringify(parsed.data, null, 2)}
|
|
1782
|
+
`
|
|
1783
|
+
};
|
|
1784
|
+
if (!changed) return { kind: "unchanged", text: existing };
|
|
1785
|
+
const { indent, eol, final } = styleOf(existing);
|
|
1786
|
+
const body = JSON.stringify(parsed.data, null, indent).split("\n").join(eol);
|
|
1787
|
+
return { kind: "updated", text: final ? `${body}${eol}` : body };
|
|
1788
|
+
}
|
|
1789
|
+
function missingGuarantees(existing, guarantees) {
|
|
1790
|
+
if (existing === null) return [...guarantees];
|
|
1791
|
+
const parsed = parse3(existing);
|
|
1792
|
+
if (!parsed.ok) return { invalid: parsed.reason };
|
|
1793
|
+
const missing = [];
|
|
1794
|
+
for (const g of guarantees) {
|
|
1795
|
+
const found = listAt(parsed.data, g, false);
|
|
1796
|
+
if (found.kind === "invalid") return { invalid: found.reason };
|
|
1797
|
+
if (found.kind === "absent" || !found.list.includes(g.contains))
|
|
1798
|
+
missing.push(g);
|
|
1799
|
+
}
|
|
1800
|
+
return missing;
|
|
1801
|
+
}
|
|
1802
|
+
function describeGuarantee(g) {
|
|
1803
|
+
return `${g.path.join(".")} lacks ${g.contains}`;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/core/agent-targets.ts
|
|
1807
|
+
async function filesOf(root, manifest, targets) {
|
|
1808
|
+
const files = [];
|
|
1809
|
+
for (const repo of Object.keys(manifest.repos)) {
|
|
1810
|
+
if (!await exists(join9(root, "org", repo))) continue;
|
|
1811
|
+
for (const target of targets)
|
|
1812
|
+
files.push({
|
|
1813
|
+
target,
|
|
1814
|
+
file: join9(root, "org", repo, ...target.file.split("/")),
|
|
1815
|
+
label: `org/${repo}/${target.file}`
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
return files;
|
|
1819
|
+
}
|
|
1820
|
+
function declared(manifest) {
|
|
1821
|
+
return (manifest.agents ?? []).flatMap(
|
|
1822
|
+
(name) => Object.hasOwn(TARGETS, name) ? [TARGETS[name]] : []
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
async function agentTargets(root, manifest, opts) {
|
|
1826
|
+
const outcomes = [];
|
|
1827
|
+
for (const { target, file, label } of await filesOf(
|
|
1828
|
+
root,
|
|
1829
|
+
manifest,
|
|
1830
|
+
declared(manifest)
|
|
1831
|
+
)) {
|
|
1832
|
+
const existing = await readOrNull(file);
|
|
1833
|
+
if (opts.check) {
|
|
1834
|
+
const missing = missingGuarantees(existing, target.guarantees);
|
|
1835
|
+
if ("invalid" in missing)
|
|
1836
|
+
outcomes.push({ label, status: "invalid", detail: missing.invalid });
|
|
1837
|
+
else if (missing.length > 0)
|
|
1838
|
+
outcomes.push({
|
|
1839
|
+
label,
|
|
1840
|
+
status: "stale",
|
|
1841
|
+
detail: missing.map(describeGuarantee).join("; ")
|
|
1842
|
+
});
|
|
1843
|
+
else outcomes.push({ label, status: "unchanged", detail: null });
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1846
|
+
const ensured = ensureGuarantees(existing, target.guarantees);
|
|
1847
|
+
if (ensured.kind === "invalid") {
|
|
1848
|
+
outcomes.push({ label, status: "invalid", detail: ensured.reason });
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
if (ensured.kind === "unchanged") {
|
|
1852
|
+
outcomes.push({ label, status: "unchanged", detail: null });
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
await mkdir2(dirname4(file), { recursive: true });
|
|
1856
|
+
await writeFileAtomic(file, ensured.text);
|
|
1857
|
+
outcomes.push({ label, status: "updated", detail: null });
|
|
1858
|
+
}
|
|
1859
|
+
return outcomes;
|
|
1860
|
+
}
|
|
1861
|
+
async function leftoverTargets(root, manifest) {
|
|
1862
|
+
if (manifest.agents === null) return [];
|
|
1863
|
+
const agents = manifest.agents;
|
|
1864
|
+
const undeclared = Object.values(TARGETS).filter(
|
|
1865
|
+
(t) => !agents.includes(t.name)
|
|
1866
|
+
);
|
|
1867
|
+
const leftovers = [];
|
|
1868
|
+
for (const { target, file, label } of await filesOf(
|
|
1869
|
+
root,
|
|
1870
|
+
manifest,
|
|
1871
|
+
undeclared
|
|
1872
|
+
)) {
|
|
1873
|
+
const existing = await readOrNull(file);
|
|
1874
|
+
if (existing === null) continue;
|
|
1875
|
+
const missing = missingGuarantees(existing, target.guarantees);
|
|
1876
|
+
if (!("invalid" in missing) && missing.length === 0) leftovers.push(label);
|
|
1877
|
+
}
|
|
1878
|
+
return leftovers;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// src/core/ask-agents.ts
|
|
1882
|
+
var AGENTS_QUESTION = "Which agents does your team use?";
|
|
1883
|
+
async function askAgents(p) {
|
|
1884
|
+
const answer = await p.multiselect({
|
|
1885
|
+
message: AGENTS_QUESTION,
|
|
1886
|
+
options: Object.values(TARGETS).map((t) => ({
|
|
1887
|
+
value: t.name,
|
|
1888
|
+
label: t.label
|
|
1889
|
+
})),
|
|
1890
|
+
required: false
|
|
1891
|
+
});
|
|
1892
|
+
if (p.isCancel(answer)) return null;
|
|
1893
|
+
return Array.isArray(answer) ? answer : [];
|
|
1894
|
+
}
|
|
1657
1895
|
|
|
1658
1896
|
// src/core/progress.ts
|
|
1659
1897
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -1842,13 +2080,25 @@ function targetsOf(manifest, root, only) {
|
|
|
1842
2080
|
const segments = entry.path.split("/");
|
|
1843
2081
|
targets.push({
|
|
1844
2082
|
scope: name,
|
|
1845
|
-
dir:
|
|
2083
|
+
dir: join10(root, ...segments),
|
|
1846
2084
|
depth: segments.length,
|
|
1847
2085
|
label: `${entry.path}/AGENTS.md`
|
|
1848
2086
|
});
|
|
1849
2087
|
}
|
|
1850
2088
|
return targets;
|
|
1851
2089
|
}
|
|
2090
|
+
async function syncBlockFile(file, block, check, rootClaudeMd = false) {
|
|
2091
|
+
const existing = await readOrNull(file);
|
|
2092
|
+
const existingLines = existing === null ? [] : existing.split(/\r?\n/);
|
|
2093
|
+
const span = findBlock(existingLines);
|
|
2094
|
+
const current = span.kind === "one" && isCurrentBlock(existingLines.slice(span.begin, span.end + 1), block.hash) && !(rootClaudeMd && existingLines.some((l) => l.trim() === "@AGENTS.md"));
|
|
2095
|
+
const merged = current ? { ok: true, text: existing ?? "", changed: false } : rootClaudeMd ? claudeMdAtRoot(existing, block.text) : mergeBlock(existing, block.text);
|
|
2096
|
+
if (!merged.ok) return { error: merged.error };
|
|
2097
|
+
if (!merged.changed) return "unchanged";
|
|
2098
|
+
if (check) return "stale";
|
|
2099
|
+
await writeFileAtomic(file, merged.text);
|
|
2100
|
+
return "updated";
|
|
2101
|
+
}
|
|
1852
2102
|
var SELECT_ALL = "*";
|
|
1853
2103
|
async function askWhatToSync(input) {
|
|
1854
2104
|
if (!input.terminal.isTty()) {
|
|
@@ -1894,16 +2144,60 @@ async function syncCommand(opts, deps = {}) {
|
|
|
1894
2144
|
const check = opts.check === true;
|
|
1895
2145
|
const ownUi = deps.ui === void 0;
|
|
1896
2146
|
const ui = deps.ui ?? (opts.yes !== true && !check ? await makeUi(terminal) : { ...plainUi });
|
|
2147
|
+
const agentFlags = opts.agent ?? [];
|
|
2148
|
+
if (agentFlags.length > 0 && check) {
|
|
2149
|
+
process.stderr.write(
|
|
2150
|
+
"--agent cannot be combined with --check: it declares agents in rness.json\n"
|
|
2151
|
+
);
|
|
2152
|
+
return 2;
|
|
2153
|
+
}
|
|
2154
|
+
const unknown = unsupportedAgents(agentFlags)[0];
|
|
2155
|
+
if (unknown !== void 0) {
|
|
2156
|
+
process.stderr.write(
|
|
2157
|
+
`unknown agent "${unknown}" (supported: ${SUPPORTED_AGENTS.join(", ")})
|
|
2158
|
+
`
|
|
2159
|
+
);
|
|
2160
|
+
return 2;
|
|
2161
|
+
}
|
|
1897
2162
|
try {
|
|
1898
2163
|
const ws = await findWorkspace(cwd);
|
|
1899
2164
|
const manifest = await loadManifest(ws.rnessDir);
|
|
1900
2165
|
const org = workspaceName(manifest, ws.root);
|
|
2166
|
+
const unsupported = unsupportedAgents(manifest.agents ?? [])[0];
|
|
2167
|
+
if (unsupported !== void 0) {
|
|
2168
|
+
process.stderr.write(`${unsupportedMessage(unsupported)}
|
|
2169
|
+
`);
|
|
2170
|
+
return 1;
|
|
2171
|
+
}
|
|
1901
2172
|
if (ownUi) ui.intro(`Sync workspace ${org}`);
|
|
1902
2173
|
if (opts.scope !== void 0 && !Object.hasOwn(manifest.scopes, opts.scope)) {
|
|
1903
2174
|
process.stderr.write(`unknown scope: ${opts.scope}
|
|
1904
2175
|
`);
|
|
1905
2176
|
return 1;
|
|
1906
2177
|
}
|
|
2178
|
+
let declare = null;
|
|
2179
|
+
if (agentFlags.length > 0) {
|
|
2180
|
+
const current = manifest.agents ?? [];
|
|
2181
|
+
const added = agentFlags.filter((a) => !current.includes(a));
|
|
2182
|
+
if (added.length > 0) declare = [...current, ...new Set(added)];
|
|
2183
|
+
} else if (manifest.agents === null && !check && opts.yes !== true && opts.scope === void 0 && terminal.isTty()) {
|
|
2184
|
+
const answer = await askAgents(await terminal.prompts());
|
|
2185
|
+
if (answer === null) {
|
|
2186
|
+
ui.cancelled();
|
|
2187
|
+
return 1;
|
|
2188
|
+
}
|
|
2189
|
+
declare = answer;
|
|
2190
|
+
}
|
|
2191
|
+
if (declare !== null) {
|
|
2192
|
+
const before = manifest.agents ?? [];
|
|
2193
|
+
manifest.agents = declare;
|
|
2194
|
+
await writeManifest(ws.rnessDir, manifest);
|
|
2195
|
+
for (const name of declare.filter((a) => !before.includes(a)))
|
|
2196
|
+
ui.line("declared", `agent ${name} in .rness/rness.json`);
|
|
2197
|
+
if (declare.length === 0)
|
|
2198
|
+
ui.line("declared", "no agent in .rness/rness.json");
|
|
2199
|
+
ui.hint("rness.json changed: commit it in .rness, for the whole team");
|
|
2200
|
+
}
|
|
1907
2201
|
const clones = await clonesIn(ws.root);
|
|
1908
2202
|
const missing = Object.keys(manifest.repos).filter((n) => !clones.has(n));
|
|
1909
2203
|
let toClone = new Set(opts.all === true && !check ? missing : []);
|
|
@@ -1945,7 +2239,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
1945
2239
|
try {
|
|
1946
2240
|
const notCloned = /* @__PURE__ */ new Set();
|
|
1947
2241
|
for (const [name, repo] of Object.entries(manifest.repos)) {
|
|
1948
|
-
const dir =
|
|
2242
|
+
const dir = join10(ws.root, "org", name);
|
|
1949
2243
|
const label = `org/${name}`;
|
|
1950
2244
|
if (!await exists(dir)) {
|
|
1951
2245
|
if (!toClone.has(name)) {
|
|
@@ -1966,7 +2260,13 @@ async function syncCommand(opts, deps = {}) {
|
|
|
1966
2260
|
}
|
|
1967
2261
|
} else if (opts.pull === true && !check) {
|
|
1968
2262
|
try {
|
|
1969
|
-
|
|
2263
|
+
const written = [
|
|
2264
|
+
...MANAGED_FILES,
|
|
2265
|
+
...(manifest.agents ?? []).flatMap(
|
|
2266
|
+
(a) => Object.hasOwn(TARGETS, a) ? [TARGETS[a].file] : []
|
|
2267
|
+
)
|
|
2268
|
+
];
|
|
2269
|
+
if (!await isClean(dir, written)) {
|
|
1970
2270
|
lines.push(["skipped", `${label} (working tree not clean)`]);
|
|
1971
2271
|
continue;
|
|
1972
2272
|
}
|
|
@@ -1997,7 +2297,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
1997
2297
|
lines.push(
|
|
1998
2298
|
`not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
|
|
1999
2299
|
);
|
|
2000
|
-
if (!await exists(
|
|
2300
|
+
if (!await exists(join10(ws.root, "org"))) {
|
|
2001
2301
|
lines.push(["skipped", "blocks (no org/ directory here)"]);
|
|
2002
2302
|
} else {
|
|
2003
2303
|
let differences = 0;
|
|
@@ -2008,10 +2308,11 @@ async function syncCommand(opts, deps = {}) {
|
|
|
2008
2308
|
lines.push(["skipped", `${t.label} (directory not present)`]);
|
|
2009
2309
|
continue;
|
|
2010
2310
|
}
|
|
2011
|
-
const
|
|
2012
|
-
MANAGED_FILES.map((f) => isSymlink(
|
|
2311
|
+
const [agentsLinked, claudeLinked] = await Promise.all(
|
|
2312
|
+
MANAGED_FILES.map((f) => isSymlink(join10(t.dir, f)))
|
|
2013
2313
|
);
|
|
2014
|
-
|
|
2314
|
+
const atRoot = t.scope === null;
|
|
2315
|
+
if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
|
|
2015
2316
|
lines.push(["skipped", `${t.label} (symlink)`]);
|
|
2016
2317
|
continue;
|
|
2017
2318
|
}
|
|
@@ -2031,37 +2332,59 @@ async function syncCommand(opts, deps = {}) {
|
|
|
2031
2332
|
`${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
|
|
2032
2333
|
);
|
|
2033
2334
|
}
|
|
2034
|
-
const
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
const span = findBlock(existingLines);
|
|
2038
|
-
const current = span.kind === "one" && isCurrentBlock(
|
|
2039
|
-
existingLines.slice(span.begin, span.end + 1),
|
|
2040
|
-
block.hash
|
|
2041
|
-
);
|
|
2042
|
-
const merged = current ? { ok: true, text: existing ?? "", changed: false } : mergeBlock(existing, block.text);
|
|
2043
|
-
if (!merged.ok) {
|
|
2044
|
-
problems.push(`${t.label}: ${merged.error}`);
|
|
2335
|
+
const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join10(t.dir, "AGENTS.md"), block, check);
|
|
2336
|
+
if (typeof agents === "object") {
|
|
2337
|
+
problems.push(`${t.label}: ${agents.error}`);
|
|
2045
2338
|
continue;
|
|
2046
2339
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2340
|
+
lines.push(
|
|
2341
|
+
agents === "skipped" ? ["skipped", `${t.label} (symlink)`] : [agents, t.label]
|
|
2342
|
+
);
|
|
2343
|
+
if (agents === "stale") differences += 1;
|
|
2344
|
+
if (!atRoot) {
|
|
2049
2345
|
if (!check) await ensureClaudeMd(t.dir);
|
|
2050
2346
|
continue;
|
|
2051
2347
|
}
|
|
2052
|
-
if (
|
|
2053
|
-
|
|
2054
|
-
|
|
2348
|
+
if (claudeLinked === true) continue;
|
|
2349
|
+
const claude = await syncBlockFile(
|
|
2350
|
+
join10(t.dir, "CLAUDE.md"),
|
|
2351
|
+
block,
|
|
2352
|
+
check,
|
|
2353
|
+
true
|
|
2354
|
+
);
|
|
2355
|
+
if (typeof claude === "object") {
|
|
2356
|
+
problems.push(`CLAUDE.md: ${claude.error}`);
|
|
2055
2357
|
continue;
|
|
2056
2358
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
lines.push(["updated", `${t.label}`]);
|
|
2359
|
+
if (claude === "stale") differences += 1;
|
|
2360
|
+
if (claude !== agents) lines.push([claude, "CLAUDE.md"]);
|
|
2060
2361
|
}
|
|
2061
2362
|
if (check && differences > 0)
|
|
2062
2363
|
problems.push(
|
|
2063
2364
|
`${differences} block(s) out of date \u2014 run ${rnessCommand()} sync`
|
|
2064
2365
|
);
|
|
2366
|
+
if (opts.scope === void 0) {
|
|
2367
|
+
let stale = 0;
|
|
2368
|
+
for (const o of await agentTargets(ws.root, manifest, { check })) {
|
|
2369
|
+
if (o.status === "invalid")
|
|
2370
|
+
problems.push(
|
|
2371
|
+
`${o.label}: ${o.detail ?? "unreadable"}; fix it, then ${rnessCommand()} sync`
|
|
2372
|
+
);
|
|
2373
|
+
else if (o.status === "stale") {
|
|
2374
|
+
stale += 1;
|
|
2375
|
+
lines.push(["stale", `${o.label} (${o.detail ?? ""})`]);
|
|
2376
|
+
} else lines.push([o.status, o.label]);
|
|
2377
|
+
}
|
|
2378
|
+
if (stale > 0)
|
|
2379
|
+
problems.push(
|
|
2380
|
+
`${stale} agent file(s) out of date \u2014 run ${rnessCommand()} sync`
|
|
2381
|
+
);
|
|
2382
|
+
if (!check)
|
|
2383
|
+
for (const label of await leftoverTargets(ws.root, manifest))
|
|
2384
|
+
lines.push(
|
|
2385
|
+
`${label} still carries the values of an agent no longer in rness.json (left as is)`
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2065
2388
|
}
|
|
2066
2389
|
return problems.length > 0 ? 1 : 0;
|
|
2067
2390
|
} finally {
|
|
@@ -2081,7 +2404,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
2081
2404
|
}
|
|
2082
2405
|
|
|
2083
2406
|
// src/commands/add.ts
|
|
2084
|
-
import { basename as basename3, join as
|
|
2407
|
+
import { basename as basename3, join as join11 } from "path";
|
|
2085
2408
|
function printDeclared(ui, result, already) {
|
|
2086
2409
|
for (const name of result.declared) {
|
|
2087
2410
|
if (already.has(name)) continue;
|
|
@@ -2174,7 +2497,7 @@ async function addCommand(spec, opts, deps = {}) {
|
|
|
2174
2497
|
}
|
|
2175
2498
|
parsed = parseRepoSpec(spec, org, host);
|
|
2176
2499
|
const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
2177
|
-
const dir =
|
|
2500
|
+
const dir = join11(ws.root, "org", parsed.name);
|
|
2178
2501
|
const present = await exists(dir);
|
|
2179
2502
|
if (interactive) {
|
|
2180
2503
|
const { confirm, isCancel } = await terminal.prompts();
|
|
@@ -2300,26 +2623,23 @@ async function contextCommand(opts) {
|
|
|
2300
2623
|
}
|
|
2301
2624
|
}
|
|
2302
2625
|
|
|
2303
|
-
// src/version.ts
|
|
2304
|
-
var VERSION = "0.6.1";
|
|
2305
|
-
|
|
2306
2626
|
// src/core/delegate.ts
|
|
2307
2627
|
import { readFile as readFile6 } from "fs/promises";
|
|
2308
|
-
import { join as
|
|
2628
|
+
import { join as join12 } from "path";
|
|
2309
2629
|
var DEFAULT_BIN = "dist/bin/rness.js";
|
|
2310
2630
|
async function readPinnedCli(rnessDir) {
|
|
2311
|
-
const dir =
|
|
2631
|
+
const dir = join12(rnessDir, "node_modules", "@rness", "cli");
|
|
2312
2632
|
let pkg;
|
|
2313
2633
|
try {
|
|
2314
|
-
pkg = JSON.parse(await readFile6(
|
|
2634
|
+
pkg = JSON.parse(await readFile6(join12(dir, "package.json"), "utf8"));
|
|
2315
2635
|
} catch {
|
|
2316
2636
|
return null;
|
|
2317
2637
|
}
|
|
2318
|
-
const
|
|
2638
|
+
const declared2 = pkg.bin;
|
|
2319
2639
|
let bin = DEFAULT_BIN;
|
|
2320
|
-
if (typeof
|
|
2321
|
-
else if (
|
|
2322
|
-
const named =
|
|
2640
|
+
if (typeof declared2 === "string") bin = declared2;
|
|
2641
|
+
else if (declared2 !== null && typeof declared2 === "object") {
|
|
2642
|
+
const named = declared2["rness"];
|
|
2323
2643
|
if (typeof named === "string") bin = named;
|
|
2324
2644
|
}
|
|
2325
2645
|
return {
|
|
@@ -2331,7 +2651,7 @@ async function readPinnedCli(rnessDir) {
|
|
|
2331
2651
|
async function pinnedBinPath(rnessDir) {
|
|
2332
2652
|
const pinned = await readPinnedCli(rnessDir);
|
|
2333
2653
|
if (pinned === null) return null;
|
|
2334
|
-
return
|
|
2654
|
+
return join12(pinned.dir, ...pinned.bin.split("/"));
|
|
2335
2655
|
}
|
|
2336
2656
|
async function findDelegate(cwd, ownVersion) {
|
|
2337
2657
|
let root;
|
|
@@ -2347,12 +2667,12 @@ async function findDelegate(cwd, ownVersion) {
|
|
|
2347
2667
|
if (pinned === null) return null;
|
|
2348
2668
|
const { version } = pinned;
|
|
2349
2669
|
if (version === null || version === ownVersion) return null;
|
|
2350
|
-
return { entry:
|
|
2670
|
+
return { entry: join12(pinned.dir, "dist", "index.js"), version, root };
|
|
2351
2671
|
}
|
|
2352
2672
|
|
|
2353
2673
|
// src/core/scaffold.ts
|
|
2354
2674
|
import { statSync } from "fs";
|
|
2355
|
-
import { dirname as
|
|
2675
|
+
import { dirname as dirname5, join as join13 } from "path";
|
|
2356
2676
|
import { fileURLToPath } from "url";
|
|
2357
2677
|
var SCAFFOLD_FILES = [
|
|
2358
2678
|
"AGENTS.md",
|
|
@@ -2374,30 +2694,30 @@ var SCAFFOLD_FILES = [
|
|
|
2374
2694
|
"docs/.gitkeep"
|
|
2375
2695
|
];
|
|
2376
2696
|
function scaffoldDir() {
|
|
2377
|
-
let dir =
|
|
2697
|
+
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
2378
2698
|
for (let i = 0; i < 4; i += 1) {
|
|
2379
|
-
const candidate =
|
|
2699
|
+
const candidate = join13(dir, "scaffold");
|
|
2380
2700
|
try {
|
|
2381
|
-
if (statSync(
|
|
2701
|
+
if (statSync(join13(candidate, "rness.json")).isFile()) return candidate;
|
|
2382
2702
|
} catch {
|
|
2383
2703
|
}
|
|
2384
|
-
dir =
|
|
2704
|
+
dir = dirname5(dir);
|
|
2385
2705
|
}
|
|
2386
2706
|
throw new Error("scaffold directory not found next to the @rness/cli package");
|
|
2387
2707
|
}
|
|
2388
2708
|
|
|
2389
2709
|
// src/core/scaffold-copy.ts
|
|
2390
|
-
import { chmod as chmod2, mkdir as
|
|
2391
|
-
import { dirname as
|
|
2710
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile7, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
2711
|
+
import { dirname as dirname6, join as join14 } from "path";
|
|
2392
2712
|
var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
|
|
2393
2713
|
async function copyScaffold(dest, tokens) {
|
|
2394
2714
|
const source = scaffoldDir();
|
|
2395
2715
|
for (const rel of SCAFFOLD_FILES) {
|
|
2396
2716
|
if (rel === "rness.json") continue;
|
|
2397
|
-
const from =
|
|
2717
|
+
const from = join14(source, ...rel.split("/"));
|
|
2398
2718
|
const targetRel = rel === "_gitignore" ? ".gitignore" : rel;
|
|
2399
|
-
const to =
|
|
2400
|
-
await
|
|
2719
|
+
const to = join14(dest, ...targetRel.split("/"));
|
|
2720
|
+
await mkdir3(dirname6(to), { recursive: true });
|
|
2401
2721
|
let content = await readFile7(from, "utf8");
|
|
2402
2722
|
if (TOKENISED.has(rel)) {
|
|
2403
2723
|
content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
|
|
@@ -2558,18 +2878,18 @@ function revokeUrl() {
|
|
|
2558
2878
|
}
|
|
2559
2879
|
|
|
2560
2880
|
// src/commands/create.ts
|
|
2561
|
-
import { mkdir as
|
|
2881
|
+
import { mkdir as mkdir6, mkdtemp, rename as rename2, rm as rm5 } from "fs/promises";
|
|
2562
2882
|
import { tmpdir } from "os";
|
|
2563
|
-
import { join as
|
|
2883
|
+
import { join as join18, relative as relative4, resolve as resolve4 } from "path";
|
|
2564
2884
|
|
|
2565
2885
|
// src/core/new-workspace.ts
|
|
2566
|
-
import { mkdir as
|
|
2567
|
-
import { dirname as
|
|
2886
|
+
import { mkdir as mkdir4, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
|
|
2887
|
+
import { dirname as dirname7, join as join16, relative as relative2 } from "path";
|
|
2568
2888
|
|
|
2569
2889
|
// src/core/pinned.ts
|
|
2570
2890
|
import { execFile as execFile4 } from "child_process";
|
|
2571
2891
|
import { readFile as readFile8 } from "fs/promises";
|
|
2572
|
-
import { basename as basename4, join as
|
|
2892
|
+
import { basename as basename4, join as join15 } from "path";
|
|
2573
2893
|
import { promisify as promisify4 } from "util";
|
|
2574
2894
|
var execFileP4 = promisify4(execFile4);
|
|
2575
2895
|
var PACKAGE = "@rness/cli";
|
|
@@ -2594,7 +2914,7 @@ function compareVersions(a, b) {
|
|
|
2594
2914
|
async function readPackageJson(rnessDir) {
|
|
2595
2915
|
try {
|
|
2596
2916
|
const parsed = JSON.parse(
|
|
2597
|
-
await readFile8(
|
|
2917
|
+
await readFile8(join15(rnessDir, "package.json"), "utf8")
|
|
2598
2918
|
);
|
|
2599
2919
|
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
2600
2920
|
} catch {
|
|
@@ -2613,7 +2933,7 @@ async function readPin(rnessDir) {
|
|
|
2613
2933
|
return null;
|
|
2614
2934
|
}
|
|
2615
2935
|
async function writePin(rnessDir, from, to) {
|
|
2616
|
-
const file =
|
|
2936
|
+
const file = join15(rnessDir, "package.json");
|
|
2617
2937
|
const text = await readFile8(file, "utf8");
|
|
2618
2938
|
const entry = new RegExp(
|
|
2619
2939
|
`("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
|
|
@@ -2630,18 +2950,18 @@ var LOCKFILES = [
|
|
|
2630
2950
|
["package-lock.json", "npm"]
|
|
2631
2951
|
];
|
|
2632
2952
|
async function workspacePackageManager(rnessDir) {
|
|
2633
|
-
const
|
|
2634
|
-
if (typeof
|
|
2635
|
-
const name =
|
|
2953
|
+
const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
|
|
2954
|
+
if (typeof declared2 === "string") {
|
|
2955
|
+
const name = declared2.split("@")[0] ?? "";
|
|
2636
2956
|
if (isPackageManager(name)) return name;
|
|
2637
2957
|
}
|
|
2638
2958
|
for (const [lockfile, pm] of LOCKFILES)
|
|
2639
|
-
if (await exists(
|
|
2959
|
+
if (await exists(join15(rnessDir, lockfile))) return pm;
|
|
2640
2960
|
return "npm";
|
|
2641
2961
|
}
|
|
2642
2962
|
async function hasLockfile(rnessDir, pm) {
|
|
2643
2963
|
for (const [lockfile, owner] of LOCKFILES)
|
|
2644
|
-
if (owner === pm && await exists(
|
|
2964
|
+
if (owner === pm && await exists(join15(rnessDir, lockfile))) return true;
|
|
2645
2965
|
return false;
|
|
2646
2966
|
}
|
|
2647
2967
|
async function pinDrift(rnessDir) {
|
|
@@ -2697,9 +3017,9 @@ async function ensureInstalled(rnessDir, opts = {}) {
|
|
|
2697
3017
|
}
|
|
2698
3018
|
}
|
|
2699
3019
|
async function runPinnedSync(root) {
|
|
2700
|
-
const installed = await readPinnedCli(
|
|
3020
|
+
const installed = await readPinnedCli(join15(root, ".rness"));
|
|
2701
3021
|
if (installed === null) return null;
|
|
2702
|
-
const pinned =
|
|
3022
|
+
const pinned = join15(installed.dir, ...installed.bin.split("/"));
|
|
2703
3023
|
if (!await exists(pinned))
|
|
2704
3024
|
return {
|
|
2705
3025
|
code: 1,
|
|
@@ -2761,7 +3081,7 @@ function syncOutcome(result, opts = {}) {
|
|
|
2761
3081
|
};
|
|
2762
3082
|
}
|
|
2763
3083
|
async function syncPinned(root, shown, fallback) {
|
|
2764
|
-
const rnessDir =
|
|
3084
|
+
const rnessDir = join15(root, ".rness");
|
|
2765
3085
|
const installed = await readPinnedCli(rnessDir);
|
|
2766
3086
|
if (installed === null) {
|
|
2767
3087
|
if (fallback !== void 0) return fallback();
|
|
@@ -2771,7 +3091,7 @@ async function syncPinned(root, shown, fallback) {
|
|
|
2771
3091
|
);
|
|
2772
3092
|
return 1;
|
|
2773
3093
|
}
|
|
2774
|
-
const pinned =
|
|
3094
|
+
const pinned = join15(installed.dir, ...installed.bin.split("/"));
|
|
2775
3095
|
if (!await exists(pinned)) {
|
|
2776
3096
|
process.stderr.write(
|
|
2777
3097
|
`@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
|
|
@@ -2818,11 +3138,11 @@ async function insideWorkspace(dir) {
|
|
|
2818
3138
|
}
|
|
2819
3139
|
async function targetProblem(cwd, root, pm) {
|
|
2820
3140
|
const shown = relative2(cwd, root) || ".";
|
|
2821
|
-
if (await insideWorkspace(cwd) || await insideWorkspace(
|
|
3141
|
+
if (await insideWorkspace(cwd) || await insideWorkspace(dirname7(root)))
|
|
2822
3142
|
return "already inside an rness workspace; create runs from outside one";
|
|
2823
3143
|
if (!await exists(root)) return null;
|
|
2824
|
-
if (await exists(
|
|
2825
|
-
if (!await exists(
|
|
3144
|
+
if (await exists(join16(root, ".rness", "rness.json"))) {
|
|
3145
|
+
if (!await exists(join16(root, ".rness", "node_modules")))
|
|
2826
3146
|
return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then ${rnessCommand()} sync`;
|
|
2827
3147
|
return `${shown} already holds an rness workspace (.rness/); run ${rnessCommand()} add inside it to add repositories`;
|
|
2828
3148
|
}
|
|
@@ -2845,9 +3165,9 @@ async function installContext(input) {
|
|
|
2845
3165
|
}
|
|
2846
3166
|
async function buildContext(input) {
|
|
2847
3167
|
const { ui, root, shown, pm } = input;
|
|
2848
|
-
const rnessDir =
|
|
3168
|
+
const rnessDir = join16(root, ".rness");
|
|
2849
3169
|
const pmVersion = await packageManagerVersion(pm);
|
|
2850
|
-
await
|
|
3170
|
+
await mkdir4(root, { recursive: true });
|
|
2851
3171
|
try {
|
|
2852
3172
|
await copyScaffold(rnessDir, {
|
|
2853
3173
|
version: VERSION,
|
|
@@ -2913,8 +3233,8 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
|
|
|
2913
3233
|
}
|
|
2914
3234
|
|
|
2915
3235
|
// src/commands/create-blank.ts
|
|
2916
|
-
import { mkdir as
|
|
2917
|
-
import { join as
|
|
3236
|
+
import { mkdir as mkdir5 } from "fs/promises";
|
|
3237
|
+
import { join as join17, relative as relative3, resolve as resolve3 } from "path";
|
|
2918
3238
|
var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
|
|
2919
3239
|
function workspaceNameError(name) {
|
|
2920
3240
|
return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
|
|
@@ -2960,6 +3280,14 @@ async function createBlank(input) {
|
|
|
2960
3280
|
);
|
|
2961
3281
|
return 2;
|
|
2962
3282
|
}
|
|
3283
|
+
let agents = input.agents.length > 0 ? [...input.agents] : null;
|
|
3284
|
+
if (agents === null && input.interactive) {
|
|
3285
|
+
agents = await askAgents(await input.prompts());
|
|
3286
|
+
if (agents === null) {
|
|
3287
|
+
ui.cancelled();
|
|
3288
|
+
return 0;
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
2963
3291
|
if (input.interactive && !prompted) {
|
|
2964
3292
|
const p = await input.prompts();
|
|
2965
3293
|
const ok = await p.confirm({
|
|
@@ -2974,14 +3302,14 @@ async function createBlank(input) {
|
|
|
2974
3302
|
ui,
|
|
2975
3303
|
root,
|
|
2976
3304
|
shown,
|
|
2977
|
-
manifest: { contract: 1, org: null, repos: {}, scopes: {} },
|
|
3305
|
+
manifest: { contract: 1, org: null, agents, repos: {}, scopes: {} },
|
|
2978
3306
|
pm,
|
|
2979
3307
|
skipInstall: input.skipInstall,
|
|
2980
3308
|
kind: "blank workspace"
|
|
2981
3309
|
});
|
|
2982
3310
|
if (!built) return 1;
|
|
2983
|
-
await
|
|
2984
|
-
await commitContext({ ui, rnessDir:
|
|
3311
|
+
await mkdir5(join17(root, "org"), { recursive: true });
|
|
3312
|
+
await commitContext({ ui, rnessDir: join17(root, ".rness"), shown });
|
|
2985
3313
|
const code = await syncBlocks(
|
|
2986
3314
|
ui,
|
|
2987
3315
|
root,
|
|
@@ -3141,7 +3469,7 @@ async function cloneSelection(input) {
|
|
|
3141
3469
|
{ doing: `cloning org/${repo}`, git: true },
|
|
3142
3470
|
() => clone(
|
|
3143
3471
|
entry.url,
|
|
3144
|
-
|
|
3472
|
+
join18(input.root, "org", repo),
|
|
3145
3473
|
input.provider.credentialsFor(entry.url)
|
|
3146
3474
|
),
|
|
3147
3475
|
() => ["cloned", `org/${repo}`]
|
|
@@ -3272,6 +3600,15 @@ async function createCommand(opts, deps = {}) {
|
|
|
3272
3600
|
`);
|
|
3273
3601
|
return 2;
|
|
3274
3602
|
}
|
|
3603
|
+
const agentFlags = [...new Set(opts.agent ?? [])];
|
|
3604
|
+
const unknownAgent = unsupportedAgents(agentFlags)[0];
|
|
3605
|
+
if (unknownAgent !== void 0) {
|
|
3606
|
+
process.stderr.write(
|
|
3607
|
+
`unknown agent "${unknownAgent}" (supported: ${SUPPORTED_AGENTS.join(", ")})
|
|
3608
|
+
`
|
|
3609
|
+
);
|
|
3610
|
+
return 2;
|
|
3611
|
+
}
|
|
3275
3612
|
if (opts.ssh === true && opts.https === true) {
|
|
3276
3613
|
process.stderr.write("--ssh and --https cannot be combined\n");
|
|
3277
3614
|
return 2;
|
|
@@ -3345,6 +3682,7 @@ async function createCommand(opts, deps = {}) {
|
|
|
3345
3682
|
if (blank)
|
|
3346
3683
|
return await createBlank({
|
|
3347
3684
|
name: opts.name,
|
|
3685
|
+
agents: agentFlags,
|
|
3348
3686
|
cwd,
|
|
3349
3687
|
pm,
|
|
3350
3688
|
interactive,
|
|
@@ -3428,6 +3766,13 @@ async function createCommand(opts, deps = {}) {
|
|
|
3428
3766
|
return 1;
|
|
3429
3767
|
}
|
|
3430
3768
|
const joining = probe.kind === "found";
|
|
3769
|
+
if (joining && agentFlags.length > 0) {
|
|
3770
|
+
process.stderr.write(
|
|
3771
|
+
`${org}/.rness exists: --agent only applies to a new workspace; after joining, run ${rnessCommand()} sync${agentFlags.map((a) => ` --agent ${a}`).join("")}
|
|
3772
|
+
`
|
|
3773
|
+
);
|
|
3774
|
+
return 2;
|
|
3775
|
+
}
|
|
3431
3776
|
if (joining)
|
|
3432
3777
|
ui.line(
|
|
3433
3778
|
"found",
|
|
@@ -3440,10 +3785,16 @@ async function createCommand(opts, deps = {}) {
|
|
|
3440
3785
|
`${org}/.rness (or not visible to you) \u2014 starting a new workspace`,
|
|
3441
3786
|
`${org} has no workspace yet (or none you can see) \u2014 starting one`
|
|
3442
3787
|
);
|
|
3443
|
-
let catalogue = {
|
|
3788
|
+
let catalogue = {
|
|
3789
|
+
contract: 1,
|
|
3790
|
+
org,
|
|
3791
|
+
agents: null,
|
|
3792
|
+
repos: {},
|
|
3793
|
+
scopes: {}
|
|
3794
|
+
};
|
|
3444
3795
|
const staged = async () => {
|
|
3445
|
-
staging ??= await mkdtemp(
|
|
3446
|
-
return
|
|
3796
|
+
staging ??= await mkdtemp(join18(tmpdir(), "rness-join-"));
|
|
3797
|
+
return join18(staging, ".rness");
|
|
3447
3798
|
};
|
|
3448
3799
|
if (joining) {
|
|
3449
3800
|
const stagedDir = await staged();
|
|
@@ -3486,6 +3837,14 @@ async function createCommand(opts, deps = {}) {
|
|
|
3486
3837
|
} else {
|
|
3487
3838
|
specs = Object.keys(catalogue.repos);
|
|
3488
3839
|
}
|
|
3840
|
+
if (!joining) {
|
|
3841
|
+
if (agentFlags.length > 0) catalogue.agents = agentFlags;
|
|
3842
|
+
else if (interactive) {
|
|
3843
|
+
const answer = await askAgents(await prompts());
|
|
3844
|
+
if (answer === null) return cancelled(ui);
|
|
3845
|
+
catalogue.agents = answer;
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3489
3848
|
if (interactive && !prompted) {
|
|
3490
3849
|
const p = await prompts();
|
|
3491
3850
|
const ok = await p.confirm({
|
|
@@ -3493,10 +3852,10 @@ async function createCommand(opts, deps = {}) {
|
|
|
3493
3852
|
});
|
|
3494
3853
|
if (p.isCancel(ok) || ok !== true) return cancelled(ui);
|
|
3495
3854
|
}
|
|
3496
|
-
const rnessDir =
|
|
3855
|
+
const rnessDir = join18(root, ".rness");
|
|
3497
3856
|
const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
|
|
3498
3857
|
if (joining) {
|
|
3499
|
-
await
|
|
3858
|
+
await mkdir6(root, { recursive: true });
|
|
3500
3859
|
await placeContext(
|
|
3501
3860
|
await staged(),
|
|
3502
3861
|
rnessDir,
|
|
@@ -3506,7 +3865,7 @@ async function createCommand(opts, deps = {}) {
|
|
|
3506
3865
|
ui.line("cloned", `${shown}/.rness (joined ${org})`);
|
|
3507
3866
|
if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
|
|
3508
3867
|
await install();
|
|
3509
|
-
await
|
|
3868
|
+
await mkdir6(join18(root, "org"), { recursive: true });
|
|
3510
3869
|
const failures2 = await cloneSelection({
|
|
3511
3870
|
root,
|
|
3512
3871
|
rnessDir,
|
|
@@ -3559,7 +3918,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
|
|
|
3559
3918
|
kind: "new workspace"
|
|
3560
3919
|
});
|
|
3561
3920
|
if (!built) return 1;
|
|
3562
|
-
await
|
|
3921
|
+
await mkdir6(join18(root, "org"), { recursive: true });
|
|
3563
3922
|
const failures = await cloneSelection({
|
|
3564
3923
|
root,
|
|
3565
3924
|
rnessDir,
|
|
@@ -3660,8 +4019,8 @@ async function logoutCommand() {
|
|
|
3660
4019
|
|
|
3661
4020
|
// src/commands/upgrade.ts
|
|
3662
4021
|
import { execFile as execFile5 } from "child_process";
|
|
3663
|
-
import { mkdir as
|
|
3664
|
-
import { basename as basename5, dirname as
|
|
4022
|
+
import { mkdir as mkdir7, readFile as readFile9, readdir as readdir3 } from "fs/promises";
|
|
4023
|
+
import { basename as basename5, dirname as dirname8, join as join19, relative as relative5 } from "path";
|
|
3665
4024
|
import { promisify as promisify5 } from "util";
|
|
3666
4025
|
var execFileP5 = promisify5(execFile5);
|
|
3667
4026
|
var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
|
|
@@ -3759,15 +4118,15 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
|
|
|
3759
4118
|
return 1;
|
|
3760
4119
|
}
|
|
3761
4120
|
}
|
|
3762
|
-
const packageFile =
|
|
4121
|
+
const packageFile = join19(ws.rnessDir, "package.json");
|
|
3763
4122
|
const restore = /* @__PURE__ */ new Map([
|
|
3764
4123
|
[packageFile, await readFile9(packageFile, "utf8")]
|
|
3765
4124
|
]);
|
|
3766
4125
|
if (pin.spec !== target) await writePin(ws.rnessDir, pin.spec, target);
|
|
3767
|
-
const workflows =
|
|
4126
|
+
const workflows = join19(ws.rnessDir, ".github", "workflows");
|
|
3768
4127
|
for (const name of await readdir3(workflows).catch(() => [])) {
|
|
3769
4128
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
3770
|
-
const file =
|
|
4129
|
+
const file = join19(workflows, name);
|
|
3771
4130
|
const text = await readOrNull(file);
|
|
3772
4131
|
if (text === null) continue;
|
|
3773
4132
|
const shown = `.github/workflows/${name}`;
|
|
@@ -3785,14 +4144,14 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
|
|
|
3785
4144
|
ui.warn(`${shown} names @rness/cli@${pin.spec}; update it by hand`);
|
|
3786
4145
|
}
|
|
3787
4146
|
}
|
|
3788
|
-
const dependabot =
|
|
4147
|
+
const dependabot = join19(ws.rnessDir, ".github", "dependabot.yml");
|
|
3789
4148
|
if (await exists(dependabot)) {
|
|
3790
4149
|
ui.hint(".github/dependabot.yml is already there \u2014 left as it is");
|
|
3791
4150
|
} else {
|
|
3792
|
-
await
|
|
4151
|
+
await mkdir7(dirname8(dependabot), { recursive: true });
|
|
3793
4152
|
await writeFileAtomic(
|
|
3794
4153
|
dependabot,
|
|
3795
|
-
await readFile9(
|
|
4154
|
+
await readFile9(join19(scaffoldDir(), ".github", "dependabot.yml"), "utf8")
|
|
3796
4155
|
);
|
|
3797
4156
|
ui.line(
|
|
3798
4157
|
"wrote",
|
|
@@ -3854,7 +4213,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
|
|
|
3854
4213
|
}
|
|
3855
4214
|
|
|
3856
4215
|
// src/core/blocks.ts
|
|
3857
|
-
import { join as
|
|
4216
|
+
import { join as join20 } from "path";
|
|
3858
4217
|
var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
|
|
3859
4218
|
async function checkBlocks({
|
|
3860
4219
|
root,
|
|
@@ -3871,14 +4230,18 @@ async function checkBlocks({
|
|
|
3871
4230
|
warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
|
|
3872
4231
|
checks
|
|
3873
4232
|
});
|
|
3874
|
-
if (!await exists(
|
|
3875
|
-
const targets = [
|
|
4233
|
+
if (!await exists(join20(root, "org"))) return derive();
|
|
4234
|
+
const targets = [
|
|
4235
|
+
{ scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
|
|
4236
|
+
{ scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
|
|
4237
|
+
];
|
|
3876
4238
|
for (const [name, entry] of Object.entries(manifest.scopes)) {
|
|
3877
4239
|
const segments = entry.path.split("/");
|
|
3878
4240
|
targets.push({
|
|
3879
4241
|
scope: name,
|
|
3880
|
-
dir:
|
|
4242
|
+
dir: join20(root, ...segments),
|
|
3881
4243
|
depth: segments.length,
|
|
4244
|
+
file: "AGENTS.md",
|
|
3882
4245
|
label: `${entry.path}/AGENTS.md`
|
|
3883
4246
|
});
|
|
3884
4247
|
}
|
|
@@ -3891,7 +4254,7 @@ async function checkBlocks({
|
|
|
3891
4254
|
);
|
|
3892
4255
|
continue;
|
|
3893
4256
|
}
|
|
3894
|
-
const text = await readOrNull(
|
|
4257
|
+
const text = await readOrNull(join20(t.dir, t.file));
|
|
3895
4258
|
const lines = text === null ? [] : text.split(/\r?\n/);
|
|
3896
4259
|
const found = findBlock(lines);
|
|
3897
4260
|
if (found.kind === "none") {
|
|
@@ -3917,7 +4280,8 @@ async function checkBlocks({
|
|
|
3917
4280
|
depth: t.depth,
|
|
3918
4281
|
context
|
|
3919
4282
|
});
|
|
3920
|
-
|
|
4283
|
+
const pointerLeft = t.file === "CLAUDE.md" && lines.some((l) => l.trim() === "@AGENTS.md");
|
|
4284
|
+
if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash) && !pointerLeft)
|
|
3921
4285
|
note(t.label, "current");
|
|
3922
4286
|
else
|
|
3923
4287
|
note(
|
|
@@ -3930,7 +4294,7 @@ async function checkBlocks({
|
|
|
3930
4294
|
}
|
|
3931
4295
|
|
|
3932
4296
|
// src/core/contract.ts
|
|
3933
|
-
import { join as
|
|
4297
|
+
import { join as join21 } from "path";
|
|
3934
4298
|
var STATUSES = {
|
|
3935
4299
|
adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
|
|
3936
4300
|
specs: [
|
|
@@ -3949,7 +4313,7 @@ async function checkContract(rnessDir) {
|
|
|
3949
4313
|
const problems = [];
|
|
3950
4314
|
for (const name of COLLECTIONS) {
|
|
3951
4315
|
const allowed = STATUSES[name];
|
|
3952
|
-
const items = await collectMarkdown(
|
|
4316
|
+
const items = await collectMarkdown(join21(rnessDir, name));
|
|
3953
4317
|
for (const item of items) {
|
|
3954
4318
|
const where = `${name}/${item.rel}`;
|
|
3955
4319
|
if (SKIP.has(where)) continue;
|
|
@@ -4001,6 +4365,16 @@ async function validateCommand(opts, deps = {}) {
|
|
|
4001
4365
|
org
|
|
4002
4366
|
});
|
|
4003
4367
|
problems.push(...blocks.problems);
|
|
4368
|
+
for (const name of unsupportedAgents(manifest.agents ?? []))
|
|
4369
|
+
problems.push(unsupportedMessage(name));
|
|
4370
|
+
const targets = await agentTargets(ws.root, manifest, { check: true });
|
|
4371
|
+
for (const o of targets)
|
|
4372
|
+
if (o.status === "stale")
|
|
4373
|
+
problems.push(
|
|
4374
|
+
`${o.label}: ${o.detail ?? ""} (run ${rnessCommand()} sync)`
|
|
4375
|
+
);
|
|
4376
|
+
else if (o.status === "invalid")
|
|
4377
|
+
problems.push(`${o.label}: ${o.detail ?? "unreadable"}`);
|
|
4004
4378
|
const ui = await makeUi(deps.terminal ?? defaultTerminal);
|
|
4005
4379
|
if (!ui.session) {
|
|
4006
4380
|
for (const p of problems) process.stderr.write(`${p}
|
|
@@ -4012,11 +4386,19 @@ async function validateCommand(opts, deps = {}) {
|
|
|
4012
4386
|
return 0;
|
|
4013
4387
|
}
|
|
4014
4388
|
ui.intro(`Validate ${org}`);
|
|
4015
|
-
|
|
4389
|
+
const rootStatus = blocks.checks.find(
|
|
4390
|
+
(c) => c.label === "AGENTS.md"
|
|
4391
|
+
)?.status;
|
|
4392
|
+
const shown = blocks.checks.filter(
|
|
4393
|
+
(c) => !(c.label === "CLAUDE.md" && c.status === rootStatus)
|
|
4394
|
+
);
|
|
4395
|
+
for (const c of shown)
|
|
4016
4396
|
if (c.message === null) ui.line("current", c.label);
|
|
4017
4397
|
else if (c.status === "stale" || c.status === "malformed")
|
|
4018
4398
|
ui.error(c.message);
|
|
4019
4399
|
else ui.warn(c.message);
|
|
4400
|
+
for (const o of targets)
|
|
4401
|
+
if (o.status === "unchanged") ui.line("current", o.label);
|
|
4020
4402
|
for (const p of problems) if (!blocks.problems.includes(p)) ui.error(p);
|
|
4021
4403
|
ui.outro(
|
|
4022
4404
|
problems.length > 0 ? "Context mismatch" : "Context OK",
|
|
@@ -4091,7 +4473,11 @@ function buildProgram(state) {
|
|
|
4091
4473
|
).option("--scope <name>", "only this scope (the root block is skipped)").option(
|
|
4092
4474
|
"--check",
|
|
4093
4475
|
"render and compare only; exit 1 when a block is out of date"
|
|
4094
|
-
).option("--pull", "git pull --ff-only in every clean clone").option("--all", "clone every rness.json repository missing from org/").option(
|
|
4476
|
+
).option("--pull", "git pull --ff-only in every clean clone").option("--all", "clone every rness.json repository missing from org/").option(
|
|
4477
|
+
"--agent <name>",
|
|
4478
|
+
"declare an agent in rness.json, then write its files (repeatable; supported: claude)",
|
|
4479
|
+
(value, previous = []) => [...previous, value]
|
|
4480
|
+
).option("-y, --yes", "do not ask for confirmation").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
4095
4481
|
state.code = await syncCommand(opts);
|
|
4096
4482
|
});
|
|
4097
4483
|
program.command("add").description(
|
|
@@ -4139,6 +4525,10 @@ function buildProgram(state) {
|
|
|
4139
4525
|
).option("--https", "force https://github.com/").option(
|
|
4140
4526
|
"--blank",
|
|
4141
4527
|
"a local workspace with no GitHub organization: .rness/, an empty org/, the root files"
|
|
4528
|
+
).option(
|
|
4529
|
+
"--agent <name>",
|
|
4530
|
+
"a new workspace's agent, declared in rness.json (repeatable; supported: claude)",
|
|
4531
|
+
(value, previous = []) => [...previous, value]
|
|
4142
4532
|
).option("--skip-install", "do not install .rness/ dependencies").option("-y, --yes", "do not ask for confirmation").option("--template <name>", "reserved").addOption(new Option("--host <base>").hideHelp()).addOption(new Option("--github-api <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (orgArg, opts) => {
|
|
4143
4533
|
if (opts.blank === true) {
|
|
4144
4534
|
state.code = await createCommand(
|
|
@@ -4254,6 +4644,7 @@ export {
|
|
|
4254
4644
|
defaultTransport,
|
|
4255
4645
|
findWorkspace,
|
|
4256
4646
|
reportError,
|
|
4647
|
+
VERSION,
|
|
4257
4648
|
BEGIN,
|
|
4258
4649
|
END,
|
|
4259
4650
|
BLOCK_SIZE_WARNING,
|
|
@@ -4275,7 +4666,6 @@ export {
|
|
|
4275
4666
|
addCommand,
|
|
4276
4667
|
renderMarkdown,
|
|
4277
4668
|
contextCommand,
|
|
4278
|
-
VERSION,
|
|
4279
4669
|
findDelegate,
|
|
4280
4670
|
SCAFFOLD_FILES,
|
|
4281
4671
|
scaffoldDir,
|
|
@@ -4292,4 +4682,4 @@ export {
|
|
|
4292
4682
|
OWN_COMMANDS,
|
|
4293
4683
|
catchUp
|
|
4294
4684
|
};
|
|
4295
|
-
//# sourceMappingURL=chunk-
|
|
4685
|
+
//# sourceMappingURL=chunk-VYP4L7BQ.js.map
|