cleargate 0.6.2 → 0.8.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/dist/MANIFEST.json +2 -2
- package/dist/cli.cjs +773 -484
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +778 -494
- package/dist/cli.js.map +1 -1
- package/dist/templates/cleargate-planning/MANIFEST.json +2 -2
- package/package.json +1 -1
- package/templates/cleargate-planning/MANIFEST.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { Command } from "commander";
|
|
|
14
14
|
// package.json
|
|
15
15
|
var package_default = {
|
|
16
16
|
name: "cleargate",
|
|
17
|
-
version: "0.
|
|
17
|
+
version: "0.8.0",
|
|
18
18
|
private: false,
|
|
19
19
|
type: "module",
|
|
20
20
|
description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, four-agent loop (architect/developer/qa/reporter), Karpathy-style awareness wiki.",
|
|
@@ -1272,8 +1272,8 @@ async function stampHandler(file, opts, cli) {
|
|
|
1272
1272
|
}
|
|
1273
1273
|
|
|
1274
1274
|
// src/commands/init.ts
|
|
1275
|
-
import * as
|
|
1276
|
-
import * as
|
|
1275
|
+
import * as fs14 from "fs";
|
|
1276
|
+
import * as path14 from "path";
|
|
1277
1277
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1278
1278
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
1279
1279
|
|
|
@@ -1319,9 +1319,14 @@ function copyPayload(payloadDir, targetCwd, opts) {
|
|
|
1319
1319
|
srcContent = text;
|
|
1320
1320
|
}
|
|
1321
1321
|
const srcBuffer = typeof srcContent === "string" ? Buffer.from(srcContent, "utf8") : srcContent;
|
|
1322
|
+
const srcMode = fs5.statSync(srcPath).mode;
|
|
1323
|
+
const needsExec = (srcMode & 73) !== 0 || relPath.endsWith(".sh");
|
|
1322
1324
|
if (fs5.existsSync(dstPath)) {
|
|
1323
1325
|
const dstContent = fs5.readFileSync(dstPath);
|
|
1324
1326
|
if (srcBuffer.equals(dstContent)) {
|
|
1327
|
+
if (needsExec && process.platform !== "win32") {
|
|
1328
|
+
fs5.chmodSync(dstPath, 493);
|
|
1329
|
+
}
|
|
1325
1330
|
report.skipped++;
|
|
1326
1331
|
report.actions.push({ action: "skipped", relPath });
|
|
1327
1332
|
continue;
|
|
@@ -1332,10 +1337,16 @@ function copyPayload(payloadDir, targetCwd, opts) {
|
|
|
1332
1337
|
continue;
|
|
1333
1338
|
}
|
|
1334
1339
|
fs5.writeFileSync(dstPath, srcBuffer);
|
|
1340
|
+
if (needsExec && process.platform !== "win32") {
|
|
1341
|
+
fs5.chmodSync(dstPath, 493);
|
|
1342
|
+
}
|
|
1335
1343
|
report.overwritten++;
|
|
1336
1344
|
report.actions.push({ action: "overwritten", relPath });
|
|
1337
1345
|
} else {
|
|
1338
1346
|
fs5.writeFileSync(dstPath, srcBuffer);
|
|
1347
|
+
if (needsExec && process.platform !== "win32") {
|
|
1348
|
+
fs5.chmodSync(dstPath, 493);
|
|
1349
|
+
}
|
|
1339
1350
|
report.created++;
|
|
1340
1351
|
report.actions.push({ action: "created", relPath });
|
|
1341
1352
|
}
|
|
@@ -1402,13 +1413,50 @@ function injectClaudeMd(existing, block) {
|
|
|
1402
1413
|
return existing.trimEnd() + "\n\n" + block + "\n";
|
|
1403
1414
|
}
|
|
1404
1415
|
|
|
1416
|
+
// src/init/inject-mcp-json.ts
|
|
1417
|
+
import * as fs6 from "fs";
|
|
1418
|
+
import * as path5 from "path";
|
|
1419
|
+
function mergeMcpJson(existing, entry) {
|
|
1420
|
+
const next = existing ? { ...existing } : {};
|
|
1421
|
+
const servers = { ...next.mcpServers ?? {} };
|
|
1422
|
+
servers.cleargate = entry;
|
|
1423
|
+
next.mcpServers = servers;
|
|
1424
|
+
return JSON.stringify(next, null, 2) + "\n";
|
|
1425
|
+
}
|
|
1426
|
+
var STDIO_ENTRY = {
|
|
1427
|
+
command: "cleargate",
|
|
1428
|
+
args: ["mcp", "serve"]
|
|
1429
|
+
};
|
|
1430
|
+
function injectMcpJson(cwd, _unusedUrl) {
|
|
1431
|
+
const dst = path5.join(cwd, ".mcp.json");
|
|
1432
|
+
const entry = STDIO_ENTRY;
|
|
1433
|
+
let existing = null;
|
|
1434
|
+
let existingRaw = null;
|
|
1435
|
+
if (fs6.existsSync(dst)) {
|
|
1436
|
+
existingRaw = fs6.readFileSync(dst, "utf8");
|
|
1437
|
+
try {
|
|
1438
|
+
existing = JSON.parse(existingRaw);
|
|
1439
|
+
} catch {
|
|
1440
|
+
throw new Error(
|
|
1441
|
+
`inject-mcp-json: ${dst} is not valid JSON; refusing to overwrite. Fix or remove the file and re-run init.`
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
const next = mergeMcpJson(existing, entry);
|
|
1446
|
+
if (existingRaw !== null && existingRaw === next) {
|
|
1447
|
+
return "unchanged";
|
|
1448
|
+
}
|
|
1449
|
+
fs6.writeFileSync(dst, next);
|
|
1450
|
+
return existingRaw === null ? "created" : "updated";
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1405
1453
|
// src/commands/wiki-build.ts
|
|
1406
|
-
import * as
|
|
1407
|
-
import * as
|
|
1454
|
+
import * as fs12 from "fs";
|
|
1455
|
+
import * as path11 from "path";
|
|
1408
1456
|
|
|
1409
1457
|
// src/wiki/scan.ts
|
|
1410
|
-
import * as
|
|
1411
|
-
import * as
|
|
1458
|
+
import * as fs7 from "fs";
|
|
1459
|
+
import * as path6 from "path";
|
|
1412
1460
|
|
|
1413
1461
|
// src/wiki/derive-bucket.ts
|
|
1414
1462
|
var PREFIX_MAP = [
|
|
@@ -1451,19 +1499,19 @@ var EXCLUDED_SUFFIXES = [
|
|
|
1451
1499
|
function scanRawItems(deliveryRoot, repoRoot) {
|
|
1452
1500
|
const results = [];
|
|
1453
1501
|
for (const subdir of ["pending-sync", "archive"]) {
|
|
1454
|
-
const dir =
|
|
1455
|
-
if (!
|
|
1456
|
-
const entries =
|
|
1502
|
+
const dir = path6.join(deliveryRoot, subdir);
|
|
1503
|
+
if (!fs7.existsSync(dir)) continue;
|
|
1504
|
+
const entries = fs7.readdirSync(dir, { recursive: true, encoding: "utf8" });
|
|
1457
1505
|
for (const rel of entries) {
|
|
1458
1506
|
if (!rel.endsWith(".md")) continue;
|
|
1459
1507
|
if (rel.includes("~") || rel.startsWith(".")) continue;
|
|
1460
|
-
const absPath =
|
|
1461
|
-
const stat =
|
|
1508
|
+
const absPath = path6.join(dir, rel);
|
|
1509
|
+
const stat = fs7.statSync(absPath);
|
|
1462
1510
|
if (!stat.isFile()) continue;
|
|
1463
|
-
const rawPath =
|
|
1511
|
+
const rawPath = path6.relative(repoRoot, absPath).replace(/\\/g, "/");
|
|
1464
1512
|
const isExcluded = EXCLUDED_SUFFIXES.some((excl) => rawPath.startsWith(excl));
|
|
1465
1513
|
if (isExcluded) continue;
|
|
1466
|
-
const filename =
|
|
1514
|
+
const filename = path6.basename(absPath);
|
|
1467
1515
|
let bucketInfo;
|
|
1468
1516
|
try {
|
|
1469
1517
|
bucketInfo = deriveBucket(filename);
|
|
@@ -1476,7 +1524,7 @@ function scanRawItems(deliveryRoot, repoRoot) {
|
|
|
1476
1524
|
} catch {
|
|
1477
1525
|
continue;
|
|
1478
1526
|
}
|
|
1479
|
-
const raw =
|
|
1527
|
+
const raw = fs7.readFileSync(absPath, "utf8");
|
|
1480
1528
|
let fm;
|
|
1481
1529
|
let body;
|
|
1482
1530
|
try {
|
|
@@ -1588,8 +1636,8 @@ function parseFmRaw(raw) {
|
|
|
1588
1636
|
}
|
|
1589
1637
|
|
|
1590
1638
|
// src/wiki/synthesis/active-sprint.ts
|
|
1591
|
-
import * as
|
|
1592
|
-
import * as
|
|
1639
|
+
import * as fs8 from "fs";
|
|
1640
|
+
import * as path7 from "path";
|
|
1593
1641
|
import { fileURLToPath } from "url";
|
|
1594
1642
|
|
|
1595
1643
|
// src/wiki/synthesis/render.ts
|
|
@@ -1628,7 +1676,7 @@ function renderSection(template, ctx) {
|
|
|
1628
1676
|
// src/wiki/synthesis/active-sprint.ts
|
|
1629
1677
|
function compile(state2, templateDir) {
|
|
1630
1678
|
const tplDir = templateDir ?? resolveDefaultTemplateDir();
|
|
1631
|
-
const tpl =
|
|
1679
|
+
const tpl = fs8.readFileSync(path7.join(tplDir, "active-sprint.md"), "utf8");
|
|
1632
1680
|
const sprints = state2.filter((i) => i.bucket === "sprints");
|
|
1633
1681
|
const active = sprints.filter((s) => isSet(s.fm["activated_at"]) && !isSet(s.fm["completed_at"]));
|
|
1634
1682
|
const completed = sprints.filter((s) => isSet(s.fm["completed_at"]));
|
|
@@ -1652,17 +1700,17 @@ function isSet(val) {
|
|
|
1652
1700
|
return s !== "" && s !== "null";
|
|
1653
1701
|
}
|
|
1654
1702
|
function resolveDefaultTemplateDir() {
|
|
1655
|
-
const __dirname2 =
|
|
1656
|
-
return
|
|
1703
|
+
const __dirname2 = path7.dirname(fileURLToPath(import.meta.url));
|
|
1704
|
+
return path7.resolve(__dirname2, "..", "templates", "synthesis");
|
|
1657
1705
|
}
|
|
1658
1706
|
|
|
1659
1707
|
// src/wiki/synthesis/open-gates.ts
|
|
1660
|
-
import * as
|
|
1661
|
-
import * as
|
|
1708
|
+
import * as fs9 from "fs";
|
|
1709
|
+
import * as path8 from "path";
|
|
1662
1710
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1663
1711
|
function compile2(state2, templateDir) {
|
|
1664
1712
|
const tplDir = templateDir ?? resolveDefaultTemplateDir2();
|
|
1665
|
-
const tpl =
|
|
1713
|
+
const tpl = fs9.readFileSync(path8.join(tplDir, "open-gates.md"), "utf8");
|
|
1666
1714
|
const gate1 = state2.filter((i) => {
|
|
1667
1715
|
if (i.bucket !== "proposals") return false;
|
|
1668
1716
|
const status = String(i.fm["status"] ?? "");
|
|
@@ -1691,17 +1739,17 @@ function compile2(state2, templateDir) {
|
|
|
1691
1739
|
return renderTemplate(tpl, data);
|
|
1692
1740
|
}
|
|
1693
1741
|
function resolveDefaultTemplateDir2() {
|
|
1694
|
-
const __dirname2 =
|
|
1695
|
-
return
|
|
1742
|
+
const __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
|
|
1743
|
+
return path8.resolve(__dirname2, "..", "templates", "synthesis");
|
|
1696
1744
|
}
|
|
1697
1745
|
|
|
1698
1746
|
// src/wiki/synthesis/product-state.ts
|
|
1699
|
-
import * as
|
|
1700
|
-
import * as
|
|
1747
|
+
import * as fs10 from "fs";
|
|
1748
|
+
import * as path9 from "path";
|
|
1701
1749
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1702
1750
|
function compile3(state2, templateDir) {
|
|
1703
1751
|
const tplDir = templateDir ?? resolveDefaultTemplateDir3();
|
|
1704
|
-
const tpl =
|
|
1752
|
+
const tpl = fs10.readFileSync(path9.join(tplDir, "product-state.md"), "utf8");
|
|
1705
1753
|
function countBucket(bucket) {
|
|
1706
1754
|
return state2.filter((i) => i.bucket === bucket);
|
|
1707
1755
|
}
|
|
@@ -1748,17 +1796,17 @@ function compile3(state2, templateDir) {
|
|
|
1748
1796
|
return renderTemplate(tpl, data);
|
|
1749
1797
|
}
|
|
1750
1798
|
function resolveDefaultTemplateDir3() {
|
|
1751
|
-
const __dirname2 =
|
|
1752
|
-
return
|
|
1799
|
+
const __dirname2 = path9.dirname(fileURLToPath3(import.meta.url));
|
|
1800
|
+
return path9.resolve(__dirname2, "..", "templates", "synthesis");
|
|
1753
1801
|
}
|
|
1754
1802
|
|
|
1755
1803
|
// src/wiki/synthesis/roadmap.ts
|
|
1756
|
-
import * as
|
|
1757
|
-
import * as
|
|
1804
|
+
import * as fs11 from "fs";
|
|
1805
|
+
import * as path10 from "path";
|
|
1758
1806
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1759
1807
|
function compile4(state2, templateDir) {
|
|
1760
1808
|
const tplDir = templateDir ?? resolveDefaultTemplateDir4();
|
|
1761
|
-
const tpl =
|
|
1809
|
+
const tpl = fs11.readFileSync(path10.join(tplDir, "roadmap.md"), "utf8");
|
|
1762
1810
|
const sprints = state2.filter((i) => i.bucket === "sprints");
|
|
1763
1811
|
const epics = state2.filter((i) => i.bucket === "epics");
|
|
1764
1812
|
const inFlightSprints = sprints.filter(
|
|
@@ -1811,8 +1859,8 @@ function isShippedStatus(status) {
|
|
|
1811
1859
|
return status === "Completed" || status === "Approved";
|
|
1812
1860
|
}
|
|
1813
1861
|
function resolveDefaultTemplateDir4() {
|
|
1814
|
-
const __dirname2 =
|
|
1815
|
-
return
|
|
1862
|
+
const __dirname2 = path10.dirname(fileURLToPath4(import.meta.url));
|
|
1863
|
+
return path10.resolve(__dirname2, "..", "templates", "synthesis");
|
|
1816
1864
|
}
|
|
1817
1865
|
|
|
1818
1866
|
// src/commands/wiki-build.ts
|
|
@@ -1837,16 +1885,16 @@ async function wikiBuildHandler(opts = {}) {
|
|
|
1837
1885
|
const exit = opts.exit ?? ((c) => process.exit(c));
|
|
1838
1886
|
const gitRunner = opts.gitRunner;
|
|
1839
1887
|
const templateDir = opts.templateDir;
|
|
1840
|
-
const deliveryRoot =
|
|
1841
|
-
const wikiRoot =
|
|
1842
|
-
if (!
|
|
1888
|
+
const deliveryRoot = path11.join(cwd, ".cleargate", "delivery");
|
|
1889
|
+
const wikiRoot = path11.join(cwd, ".cleargate", "wiki");
|
|
1890
|
+
if (!fs12.existsSync(deliveryRoot)) {
|
|
1843
1891
|
stderr(`wiki build: .cleargate/delivery/ not found at ${deliveryRoot}
|
|
1844
1892
|
`);
|
|
1845
1893
|
exit(1);
|
|
1846
1894
|
return;
|
|
1847
1895
|
}
|
|
1848
1896
|
for (const bucket of BUCKET_ORDER) {
|
|
1849
|
-
|
|
1897
|
+
fs12.mkdirSync(path11.join(wikiRoot, bucket), { recursive: true });
|
|
1850
1898
|
}
|
|
1851
1899
|
const items = scanRawItems(deliveryRoot, cwd);
|
|
1852
1900
|
const timestamp = now();
|
|
@@ -1869,19 +1917,19 @@ async function wikiBuildHandler(opts = {}) {
|
|
|
1869
1917
|
};
|
|
1870
1918
|
const body = buildPageBody(item, wikiPage);
|
|
1871
1919
|
const content = serializePage(wikiPage, body);
|
|
1872
|
-
const pageDir =
|
|
1873
|
-
|
|
1874
|
-
|
|
1920
|
+
const pageDir = path11.join(wikiRoot, item.bucket);
|
|
1921
|
+
fs12.mkdirSync(pageDir, { recursive: true });
|
|
1922
|
+
fs12.writeFileSync(path11.join(pageDir, `${item.id}.md`), content, "utf8");
|
|
1875
1923
|
pagesWritten++;
|
|
1876
1924
|
}
|
|
1877
1925
|
const indexContent = buildIndex(items);
|
|
1878
|
-
|
|
1926
|
+
fs12.writeFileSync(path11.join(wikiRoot, "index.md"), indexContent, "utf8");
|
|
1879
1927
|
const logContent = buildLog(items, timestamp);
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1928
|
+
fs12.writeFileSync(path11.join(wikiRoot, "log.md"), logContent, "utf8");
|
|
1929
|
+
fs12.writeFileSync(path11.join(wikiRoot, "active-sprint.md"), compile(items, templateDir), "utf8");
|
|
1930
|
+
fs12.writeFileSync(path11.join(wikiRoot, "open-gates.md"), compile2(items, templateDir), "utf8");
|
|
1931
|
+
fs12.writeFileSync(path11.join(wikiRoot, "product-state.md"), compile3(items, templateDir), "utf8");
|
|
1932
|
+
fs12.writeFileSync(path11.join(wikiRoot, "roadmap.md"), compile4(items, templateDir), "utf8");
|
|
1885
1933
|
stdout(`wiki build: OK (${pagesWritten} pages written)
|
|
1886
1934
|
`);
|
|
1887
1935
|
}
|
|
@@ -2046,8 +2094,8 @@ function buildLog(items, timestamp) {
|
|
|
2046
2094
|
|
|
2047
2095
|
// src/lib/manifest.ts
|
|
2048
2096
|
import { readFile as readFile3, writeFile as writeFile2, rename, mkdir } from "fs/promises";
|
|
2049
|
-
import { existsSync as
|
|
2050
|
-
import * as
|
|
2097
|
+
import { existsSync as existsSync8, readFileSync as readFileSync11 } from "fs";
|
|
2098
|
+
import * as path12 from "path";
|
|
2051
2099
|
|
|
2052
2100
|
// src/lib/sha256.ts
|
|
2053
2101
|
import { createHash } from "crypto";
|
|
@@ -2070,31 +2118,31 @@ function shortHash(full) {
|
|
|
2070
2118
|
// src/lib/manifest.ts
|
|
2071
2119
|
function resolveDefaultPackageRoot() {
|
|
2072
2120
|
const here = new URL(".", import.meta.url).pathname;
|
|
2073
|
-
const distCandidate =
|
|
2074
|
-
if (
|
|
2121
|
+
const distCandidate = path12.join(here, "MANIFEST.json");
|
|
2122
|
+
if (existsSync8(distCandidate)) {
|
|
2075
2123
|
return here;
|
|
2076
2124
|
}
|
|
2077
|
-
const oneLevelUp =
|
|
2078
|
-
if (
|
|
2079
|
-
return
|
|
2125
|
+
const oneLevelUp = path12.join(here, "..", "MANIFEST.json");
|
|
2126
|
+
if (existsSync8(oneLevelUp)) {
|
|
2127
|
+
return path12.join(here, "..");
|
|
2080
2128
|
}
|
|
2081
|
-
const devCandidate =
|
|
2082
|
-
if (
|
|
2083
|
-
return
|
|
2129
|
+
const devCandidate = path12.join(here, "..", "..", "..", "cleargate-planning", "MANIFEST.json");
|
|
2130
|
+
if (existsSync8(devCandidate)) {
|
|
2131
|
+
return path12.join(here, "..", "..", "..", "cleargate-planning");
|
|
2084
2132
|
}
|
|
2085
2133
|
return here;
|
|
2086
2134
|
}
|
|
2087
2135
|
function loadPackageManifest(opts) {
|
|
2088
2136
|
const packageRoot = opts?.packageRoot ?? resolveDefaultPackageRoot();
|
|
2089
|
-
const manifestPath =
|
|
2090
|
-
if (!
|
|
2137
|
+
const manifestPath = path12.join(packageRoot, "MANIFEST.json");
|
|
2138
|
+
if (!existsSync8(manifestPath)) {
|
|
2091
2139
|
throw new Error(
|
|
2092
2140
|
`MANIFEST.json not found at ${manifestPath}; run 'npm run build' to generate it.`
|
|
2093
2141
|
);
|
|
2094
2142
|
}
|
|
2095
2143
|
let raw;
|
|
2096
2144
|
try {
|
|
2097
|
-
raw =
|
|
2145
|
+
raw = readFileSync11(manifestPath, "utf-8");
|
|
2098
2146
|
} catch {
|
|
2099
2147
|
throw new Error(
|
|
2100
2148
|
`MANIFEST.json not found at ${manifestPath}; run 'npm run build' to generate it.`
|
|
@@ -2103,7 +2151,7 @@ function loadPackageManifest(opts) {
|
|
|
2103
2151
|
return JSON.parse(raw);
|
|
2104
2152
|
}
|
|
2105
2153
|
async function loadInstallSnapshot(projectRoot) {
|
|
2106
|
-
const snapshotPath =
|
|
2154
|
+
const snapshotPath = path12.join(projectRoot, ".cleargate", ".install-manifest.json");
|
|
2107
2155
|
try {
|
|
2108
2156
|
const raw = await readFile3(snapshotPath, "utf-8");
|
|
2109
2157
|
return JSON.parse(raw);
|
|
@@ -2112,7 +2160,7 @@ async function loadInstallSnapshot(projectRoot) {
|
|
|
2112
2160
|
}
|
|
2113
2161
|
}
|
|
2114
2162
|
async function computeCurrentSha(file, projectRoot) {
|
|
2115
|
-
const filePath =
|
|
2163
|
+
const filePath = path12.join(projectRoot, file.path);
|
|
2116
2164
|
try {
|
|
2117
2165
|
const raw = await readFile3(filePath);
|
|
2118
2166
|
return hashNormalized(raw);
|
|
@@ -2141,8 +2189,8 @@ function classify(pkgSha, installSha, currentSha, tier) {
|
|
|
2141
2189
|
return "both-changed";
|
|
2142
2190
|
}
|
|
2143
2191
|
async function writeDriftState(projectRoot, state2, opts) {
|
|
2144
|
-
const cleargatDir =
|
|
2145
|
-
const finalPath =
|
|
2192
|
+
const cleargatDir = path12.join(projectRoot, ".cleargate");
|
|
2193
|
+
const finalPath = path12.join(cleargatDir, ".drift-state.json");
|
|
2146
2194
|
const tmpPath = `${finalPath}.tmp`;
|
|
2147
2195
|
const lastRefreshed = opts?.lastRefreshed ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2148
2196
|
const fileContent = { last_refreshed: lastRefreshed, drift: state2 };
|
|
@@ -2151,7 +2199,7 @@ async function writeDriftState(projectRoot, state2, opts) {
|
|
|
2151
2199
|
await rename(tmpPath, finalPath);
|
|
2152
2200
|
}
|
|
2153
2201
|
async function readDriftState(projectRoot) {
|
|
2154
|
-
const driftPath =
|
|
2202
|
+
const driftPath = path12.join(projectRoot, ".cleargate", ".drift-state.json");
|
|
2155
2203
|
try {
|
|
2156
2204
|
const raw = await readFile3(driftPath, "utf-8");
|
|
2157
2205
|
const parsed = JSON.parse(raw);
|
|
@@ -2224,24 +2272,24 @@ async function promptEmail(question, defaultValue, opts) {
|
|
|
2224
2272
|
}
|
|
2225
2273
|
|
|
2226
2274
|
// src/lib/identity.ts
|
|
2227
|
-
import * as
|
|
2228
|
-
import * as
|
|
2275
|
+
import * as fs13 from "fs";
|
|
2276
|
+
import * as path13 from "path";
|
|
2229
2277
|
import * as fsPromises from "fs/promises";
|
|
2230
2278
|
import * as os3 from "os";
|
|
2231
2279
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2232
2280
|
function readParticipant(projectRoot) {
|
|
2233
|
-
const filePath =
|
|
2281
|
+
const filePath = path13.join(projectRoot, ".cleargate", ".participant.json");
|
|
2234
2282
|
try {
|
|
2235
|
-
const raw =
|
|
2283
|
+
const raw = fs13.readFileSync(filePath, "utf8");
|
|
2236
2284
|
return JSON.parse(raw);
|
|
2237
2285
|
} catch {
|
|
2238
2286
|
return null;
|
|
2239
2287
|
}
|
|
2240
2288
|
}
|
|
2241
2289
|
async function writeParticipant(projectRoot, email, source, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
2242
|
-
const cleargateDir =
|
|
2290
|
+
const cleargateDir = path13.join(projectRoot, ".cleargate");
|
|
2243
2291
|
await fsPromises.mkdir(cleargateDir, { recursive: true });
|
|
2244
|
-
const filePath =
|
|
2292
|
+
const filePath = path13.join(cleargateDir, ".participant.json");
|
|
2245
2293
|
const tmpPath = filePath + ".tmp." + Date.now();
|
|
2246
2294
|
const content = {
|
|
2247
2295
|
email,
|
|
@@ -2317,16 +2365,16 @@ var HOOK_ADDITION = {
|
|
|
2317
2365
|
};
|
|
2318
2366
|
function resolveDefaultPayloadDir() {
|
|
2319
2367
|
const thisFile = fileURLToPath5(import.meta.url);
|
|
2320
|
-
const pkgRoot =
|
|
2321
|
-
return
|
|
2368
|
+
const pkgRoot = path14.resolve(path14.dirname(thisFile), "..");
|
|
2369
|
+
return path14.join(pkgRoot, "templates", "cleargate-planning");
|
|
2322
2370
|
}
|
|
2323
2371
|
function countDeliveryItems(cwd) {
|
|
2324
|
-
const pendingSync =
|
|
2325
|
-
const archive =
|
|
2372
|
+
const pendingSync = path14.join(cwd, ".cleargate", "delivery", "pending-sync");
|
|
2373
|
+
const archive = path14.join(cwd, ".cleargate", "delivery", "archive");
|
|
2326
2374
|
let count = 0;
|
|
2327
2375
|
for (const dir of [pendingSync, archive]) {
|
|
2328
|
-
if (!
|
|
2329
|
-
const entries =
|
|
2376
|
+
if (!fs14.existsSync(dir)) continue;
|
|
2377
|
+
const entries = fs14.readdirSync(dir);
|
|
2330
2378
|
for (const f of entries) {
|
|
2331
2379
|
if (f.endsWith(".md") && f !== ".gitkeep") count++;
|
|
2332
2380
|
}
|
|
@@ -2335,12 +2383,12 @@ function countDeliveryItems(cwd) {
|
|
|
2335
2383
|
}
|
|
2336
2384
|
function writeAtomic(filePath, content) {
|
|
2337
2385
|
const tmpPath = filePath + ".tmp." + Date.now();
|
|
2338
|
-
|
|
2339
|
-
|
|
2386
|
+
fs14.writeFileSync(tmpPath, content, "utf8");
|
|
2387
|
+
fs14.renameSync(tmpPath, filePath);
|
|
2340
2388
|
}
|
|
2341
2389
|
function readPackageVersion(packageJsonPath) {
|
|
2342
2390
|
try {
|
|
2343
|
-
const raw =
|
|
2391
|
+
const raw = fs14.readFileSync(packageJsonPath, "utf8");
|
|
2344
2392
|
const pkg = JSON.parse(raw);
|
|
2345
2393
|
if (typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
2346
2394
|
return pkg.version;
|
|
@@ -2360,16 +2408,16 @@ async function initHandler(opts = {}) {
|
|
|
2360
2408
|
const promptYesNoFn = opts.promptYesNo ?? promptYesNo;
|
|
2361
2409
|
const promptEmailFn = opts.promptEmail ?? promptEmail;
|
|
2362
2410
|
const spawnSyncFn = opts.spawnSyncFn ?? spawnSync3;
|
|
2363
|
-
if (!
|
|
2411
|
+
if (!fs14.existsSync(cwd)) {
|
|
2364
2412
|
stderr(`[cleargate init] ERROR: target directory does not exist: ${cwd}
|
|
2365
2413
|
`);
|
|
2366
2414
|
exit(1);
|
|
2367
2415
|
return;
|
|
2368
2416
|
}
|
|
2369
|
-
const testWritePath =
|
|
2417
|
+
const testWritePath = path14.join(cwd, `.cleargate-init-write-test-${Date.now()}`);
|
|
2370
2418
|
try {
|
|
2371
|
-
|
|
2372
|
-
|
|
2419
|
+
fs14.writeFileSync(testWritePath, "");
|
|
2420
|
+
fs14.unlinkSync(testWritePath);
|
|
2373
2421
|
} catch {
|
|
2374
2422
|
stderr(`[cleargate init] ERROR: target directory is not writable: ${cwd}
|
|
2375
2423
|
`);
|
|
@@ -2379,7 +2427,7 @@ async function initHandler(opts = {}) {
|
|
|
2379
2427
|
stdout(`[cleargate init] Target: ${cwd}
|
|
2380
2428
|
`);
|
|
2381
2429
|
const payloadDir = opts.payloadDir ?? resolveDefaultPayloadDir();
|
|
2382
|
-
if (!
|
|
2430
|
+
if (!fs14.existsSync(payloadDir)) {
|
|
2383
2431
|
stderr(`[cleargate init] ERROR: payload directory not found: ${payloadDir}
|
|
2384
2432
|
`);
|
|
2385
2433
|
stderr(`[cleargate init] Run \`npm run prebuild\` to copy the payload first.
|
|
@@ -2387,12 +2435,12 @@ async function initHandler(opts = {}) {
|
|
|
2387
2435
|
exit(1);
|
|
2388
2436
|
return;
|
|
2389
2437
|
}
|
|
2390
|
-
const uninstalledMarkerPath =
|
|
2438
|
+
const uninstalledMarkerPath = path14.join(cwd, ".cleargate", ".uninstalled");
|
|
2391
2439
|
let uninstalledMarker = null;
|
|
2392
2440
|
let userChoseRestore = false;
|
|
2393
|
-
if (
|
|
2441
|
+
if (fs14.existsSync(uninstalledMarkerPath)) {
|
|
2394
2442
|
try {
|
|
2395
|
-
const raw =
|
|
2443
|
+
const raw = fs14.readFileSync(uninstalledMarkerPath, "utf8");
|
|
2396
2444
|
uninstalledMarker = JSON.parse(raw);
|
|
2397
2445
|
} catch {
|
|
2398
2446
|
stderr(`[cleargate init] WARNING: .uninstalled marker is malformed; ignoring it.
|
|
@@ -2404,8 +2452,8 @@ async function initHandler(opts = {}) {
|
|
|
2404
2452
|
userChoseRestore = await promptYesNoFn(question, true);
|
|
2405
2453
|
if (userChoseRestore) {
|
|
2406
2454
|
for (const preservedPath of preserved) {
|
|
2407
|
-
const absPreserved =
|
|
2408
|
-
if (
|
|
2455
|
+
const absPreserved = path14.isAbsolute(preservedPath) ? preservedPath : path14.join(cwd, preservedPath);
|
|
2456
|
+
if (fs14.existsSync(absPreserved)) {
|
|
2409
2457
|
stdout(`[cleargate init] [preserved] ${preservedPath}
|
|
2410
2458
|
`);
|
|
2411
2459
|
} else {
|
|
@@ -2425,8 +2473,8 @@ async function initHandler(opts = {}) {
|
|
|
2425
2473
|
if (opts.pin) {
|
|
2426
2474
|
pinVersion = opts.pin;
|
|
2427
2475
|
} else {
|
|
2428
|
-
const payloadParent =
|
|
2429
|
-
pinVersion = readPackageVersion(
|
|
2476
|
+
const payloadParent = path14.resolve(payloadDir, "..", "..");
|
|
2477
|
+
pinVersion = readPackageVersion(path14.join(payloadParent, "package.json")) ?? readPackageVersion(path14.join(path14.dirname(fileURLToPath5(import.meta.url)), "..", "package.json")) ?? "latest";
|
|
2430
2478
|
}
|
|
2431
2479
|
const copyReport = copyPayload(payloadDir, cwd, { force, pinVersion });
|
|
2432
2480
|
for (const action of copyReport.actions) {
|
|
@@ -2434,33 +2482,33 @@ async function initHandler(opts = {}) {
|
|
|
2434
2482
|
stdout(`[cleargate init] ${verb} ${action.relPath}
|
|
2435
2483
|
`);
|
|
2436
2484
|
}
|
|
2437
|
-
const settingsPath =
|
|
2485
|
+
const settingsPath = path14.join(cwd, ".claude", "settings.json");
|
|
2438
2486
|
let existingSettings = null;
|
|
2439
|
-
if (
|
|
2487
|
+
if (fs14.existsSync(settingsPath)) {
|
|
2440
2488
|
try {
|
|
2441
|
-
existingSettings = JSON.parse(
|
|
2489
|
+
existingSettings = JSON.parse(fs14.readFileSync(settingsPath, "utf8"));
|
|
2442
2490
|
} catch {
|
|
2443
2491
|
stderr(`[cleargate init] WARNING: could not parse ${settingsPath}; treating as empty.
|
|
2444
2492
|
`);
|
|
2445
2493
|
}
|
|
2446
2494
|
}
|
|
2447
2495
|
const mergedSettings = mergeSettings(existingSettings, HOOK_ADDITION);
|
|
2448
|
-
|
|
2496
|
+
fs14.mkdirSync(path14.dirname(settingsPath), { recursive: true });
|
|
2449
2497
|
writeAtomic(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n");
|
|
2450
2498
|
stdout(`[cleargate init] Updated .claude/settings.json: merged PostToolUse hook
|
|
2451
2499
|
`);
|
|
2452
|
-
const claudeMdPath =
|
|
2453
|
-
const claudeMdSrcPath =
|
|
2500
|
+
const claudeMdPath = path14.join(cwd, "CLAUDE.md");
|
|
2501
|
+
const claudeMdSrcPath = path14.join(payloadDir, "CLAUDE.md");
|
|
2454
2502
|
let claudeMdBlock;
|
|
2455
2503
|
try {
|
|
2456
|
-
const claudeMdSrc =
|
|
2504
|
+
const claudeMdSrc = fs14.readFileSync(claudeMdSrcPath, "utf8");
|
|
2457
2505
|
claudeMdBlock = extractBlock(claudeMdSrc);
|
|
2458
2506
|
} catch (e) {
|
|
2459
2507
|
stderr(`[cleargate init] WARNING: could not read CLAUDE.md block from payload: ${String(e)}
|
|
2460
2508
|
`);
|
|
2461
2509
|
claudeMdBlock = "<!-- CLEARGATE:START -->\n<!-- CLEARGATE:END -->";
|
|
2462
2510
|
}
|
|
2463
|
-
const existingClaudeMd =
|
|
2511
|
+
const existingClaudeMd = fs14.existsSync(claudeMdPath) ? fs14.readFileSync(claudeMdPath, "utf8") : null;
|
|
2464
2512
|
const newClaudeMd = injectClaudeMd(existingClaudeMd, claudeMdBlock);
|
|
2465
2513
|
writeAtomic(claudeMdPath, newClaudeMd);
|
|
2466
2514
|
if (existingClaudeMd === null) {
|
|
@@ -2471,6 +2519,26 @@ async function initHandler(opts = {}) {
|
|
|
2471
2519
|
`);
|
|
2472
2520
|
} else {
|
|
2473
2521
|
stdout(`[cleargate init] CLAUDE.md unchanged (block already up to date)
|
|
2522
|
+
`);
|
|
2523
|
+
}
|
|
2524
|
+
try {
|
|
2525
|
+
const action = injectMcpJson(cwd);
|
|
2526
|
+
if (action === "created") {
|
|
2527
|
+
stdout(
|
|
2528
|
+
`[cleargate init] Created .mcp.json (cleargate MCP server registered) \u2014 restart Claude Code to load it.
|
|
2529
|
+
`
|
|
2530
|
+
);
|
|
2531
|
+
} else if (action === "updated") {
|
|
2532
|
+
stdout(
|
|
2533
|
+
`[cleargate init] Updated .mcp.json (cleargate MCP server entry merged) \u2014 restart Claude Code to pick up changes.
|
|
2534
|
+
`
|
|
2535
|
+
);
|
|
2536
|
+
} else {
|
|
2537
|
+
stdout(`[cleargate init] .mcp.json unchanged (cleargate entry already present)
|
|
2538
|
+
`);
|
|
2539
|
+
}
|
|
2540
|
+
} catch (e) {
|
|
2541
|
+
stderr(`[cleargate init] WARNING: ${String(e instanceof Error ? e.message : e)}
|
|
2474
2542
|
`);
|
|
2475
2543
|
}
|
|
2476
2544
|
const itemCount = countDeliveryItems(cwd);
|
|
@@ -2484,9 +2552,9 @@ async function initHandler(opts = {}) {
|
|
|
2484
2552
|
stdout(`[cleargate init] Bootstrap: no items to ingest, skipping build
|
|
2485
2553
|
`);
|
|
2486
2554
|
}
|
|
2487
|
-
const cleargateDir =
|
|
2488
|
-
|
|
2489
|
-
const snapshotPath =
|
|
2555
|
+
const cleargateDir = path14.join(cwd, ".cleargate");
|
|
2556
|
+
fs14.mkdirSync(cleargateDir, { recursive: true });
|
|
2557
|
+
const snapshotPath = path14.join(cleargateDir, ".install-manifest.json");
|
|
2490
2558
|
try {
|
|
2491
2559
|
const readManifest = opts.readInstallManifest ?? (() => loadPackageManifest({ packageRoot: payloadDir }));
|
|
2492
2560
|
const pkgManifest = readManifest();
|
|
@@ -2501,19 +2569,19 @@ async function initHandler(opts = {}) {
|
|
|
2501
2569
|
stderr(`[cleargate init] WARNING: could not write install snapshot: ${String(e)}
|
|
2502
2570
|
`);
|
|
2503
2571
|
}
|
|
2504
|
-
if (uninstalledMarker !== null &&
|
|
2572
|
+
if (uninstalledMarker !== null && fs14.existsSync(uninstalledMarkerPath)) {
|
|
2505
2573
|
try {
|
|
2506
|
-
|
|
2574
|
+
fs14.unlinkSync(uninstalledMarkerPath);
|
|
2507
2575
|
} catch (e) {
|
|
2508
2576
|
stderr(`[cleargate init] WARNING: could not remove .uninstalled marker: ${String(e)}
|
|
2509
2577
|
`);
|
|
2510
2578
|
}
|
|
2511
2579
|
}
|
|
2512
2580
|
{
|
|
2513
|
-
const distCliPath =
|
|
2581
|
+
const distCliPath = path14.join(cwd, "cleargate-cli", "dist", "cli.js");
|
|
2514
2582
|
let branch = null;
|
|
2515
2583
|
let branchLabel = "";
|
|
2516
|
-
if (
|
|
2584
|
+
if (fs14.existsSync(distCliPath)) {
|
|
2517
2585
|
branch = { cmd: "node", args: [distCliPath, "--version"] };
|
|
2518
2586
|
branchLabel = `local dist (${distCliPath})`;
|
|
2519
2587
|
} else {
|
|
@@ -2584,8 +2652,8 @@ async function initHandler(opts = {}) {
|
|
|
2584
2652
|
}
|
|
2585
2653
|
|
|
2586
2654
|
// src/commands/wiki-ingest.ts
|
|
2587
|
-
import * as
|
|
2588
|
-
import * as
|
|
2655
|
+
import * as fs15 from "fs";
|
|
2656
|
+
import * as path15 from "path";
|
|
2589
2657
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2590
2658
|
var EXCLUDED_SUFFIXES2 = [
|
|
2591
2659
|
".cleargate/knowledge/",
|
|
@@ -2611,16 +2679,16 @@ async function wikiIngestHandler(opts) {
|
|
|
2611
2679
|
const stderr = opts.stderr ?? ((s) => process.stderr.write(s));
|
|
2612
2680
|
const exit = opts.exit ?? ((c) => process.exit(c));
|
|
2613
2681
|
const gitRunner = opts.gitRunner;
|
|
2614
|
-
const rename11 = opts.rename ??
|
|
2682
|
+
const rename11 = opts.rename ?? fs15.renameSync;
|
|
2615
2683
|
const templateDir = opts.templateDir;
|
|
2616
2684
|
const rawPath = opts.rawPath;
|
|
2617
|
-
const absRawPath =
|
|
2618
|
-
const relRawPath =
|
|
2619
|
-
const deliveryRoot =
|
|
2685
|
+
const absRawPath = path15.isAbsolute(rawPath) ? rawPath : path15.resolve(cwd, rawPath);
|
|
2686
|
+
const relRawPath = path15.relative(cwd, absRawPath).replace(/\\/g, "/");
|
|
2687
|
+
const deliveryRoot = path15.join(cwd, ".cleargate", "delivery");
|
|
2620
2688
|
const deliveryRootNorm = deliveryRoot.replace(/\\/g, "/");
|
|
2621
2689
|
const absDeliveryRoot = deliveryRoot;
|
|
2622
|
-
const relToDelivery =
|
|
2623
|
-
if (relToDelivery.startsWith("..") ||
|
|
2690
|
+
const relToDelivery = path15.relative(absDeliveryRoot, absRawPath);
|
|
2691
|
+
if (relToDelivery.startsWith("..") || path15.isAbsolute(relToDelivery)) {
|
|
2624
2692
|
stderr(`wiki ingest: ${rawPath} not under .cleargate/delivery/
|
|
2625
2693
|
`);
|
|
2626
2694
|
exit(2);
|
|
@@ -2634,7 +2702,7 @@ async function wikiIngestHandler(opts) {
|
|
|
2634
2702
|
exit(0);
|
|
2635
2703
|
return;
|
|
2636
2704
|
}
|
|
2637
|
-
const filename =
|
|
2705
|
+
const filename = path15.basename(absRawPath);
|
|
2638
2706
|
let bucketInfo;
|
|
2639
2707
|
try {
|
|
2640
2708
|
bucketInfo = deriveBucket(filename);
|
|
@@ -2654,12 +2722,12 @@ async function wikiIngestHandler(opts) {
|
|
|
2654
2722
|
return;
|
|
2655
2723
|
}
|
|
2656
2724
|
const { type, id, bucket } = bucketInfo;
|
|
2657
|
-
const wikiRoot =
|
|
2658
|
-
const pageDir =
|
|
2659
|
-
const pagePath =
|
|
2725
|
+
const wikiRoot = path15.join(cwd, ".cleargate", "wiki");
|
|
2726
|
+
const pageDir = path15.join(wikiRoot, bucket);
|
|
2727
|
+
const pagePath = path15.join(pageDir, `${id}.md`);
|
|
2660
2728
|
let rawContent;
|
|
2661
2729
|
try {
|
|
2662
|
-
rawContent =
|
|
2730
|
+
rawContent = fs15.readFileSync(absRawPath, "utf8");
|
|
2663
2731
|
} catch (e) {
|
|
2664
2732
|
stderr(`wiki ingest: cannot read ${rawPath}: ${e.message}
|
|
2665
2733
|
`);
|
|
@@ -2679,11 +2747,11 @@ async function wikiIngestHandler(opts) {
|
|
|
2679
2747
|
return;
|
|
2680
2748
|
}
|
|
2681
2749
|
const currentSha = getGitSha(absRawPath, gitRunner) ?? "";
|
|
2682
|
-
const pageExists =
|
|
2750
|
+
const pageExists = fs15.existsSync(pagePath);
|
|
2683
2751
|
if (pageExists && currentSha !== "") {
|
|
2684
2752
|
let isNoOp = false;
|
|
2685
2753
|
try {
|
|
2686
|
-
const existingPageContent =
|
|
2754
|
+
const existingPageContent = fs15.readFileSync(pagePath, "utf8");
|
|
2687
2755
|
const existingPage = parsePage(existingPageContent);
|
|
2688
2756
|
if (existingPage.last_ingest_commit === currentSha) {
|
|
2689
2757
|
const contentUnchanged = checkContentUnchanged(absRawPath, currentSha, relRawPath, gitRunner);
|
|
@@ -2718,8 +2786,8 @@ async function wikiIngestHandler(opts) {
|
|
|
2718
2786
|
};
|
|
2719
2787
|
const pageBody = buildPageBody2({ id, fm, body });
|
|
2720
2788
|
const pageContent = serializePage(wikiPage, pageBody);
|
|
2721
|
-
|
|
2722
|
-
|
|
2789
|
+
fs15.mkdirSync(pageDir, { recursive: true });
|
|
2790
|
+
fs15.writeFileSync(pagePath, pageContent, "utf8");
|
|
2723
2791
|
appendLogEntry(wikiRoot, { timestamp, action, id, relRawPath });
|
|
2724
2792
|
updateIndex(wikiRoot, { id, type, status: wikiPage.status, relRawPath, rename: rename11 });
|
|
2725
2793
|
recompileSynthesis(wikiRoot, cwd, templateDir);
|
|
@@ -2731,7 +2799,7 @@ function checkContentUnchanged(absRawPath, sha, relRawPath, gitRunner) {
|
|
|
2731
2799
|
const run = gitRunner ?? defaultGitRunner;
|
|
2732
2800
|
const gitContent = run("git", ["show", `${sha}:${relRawPath}`]);
|
|
2733
2801
|
if (!gitContent && gitContent !== "") return false;
|
|
2734
|
-
const currentContent =
|
|
2802
|
+
const currentContent = fs15.readFileSync(absRawPath, "utf8");
|
|
2735
2803
|
return gitContent === currentContent;
|
|
2736
2804
|
} catch {
|
|
2737
2805
|
return false;
|
|
@@ -2784,7 +2852,7 @@ function buildPageBody2(item) {
|
|
|
2784
2852
|
].join("\n");
|
|
2785
2853
|
}
|
|
2786
2854
|
function appendLogEntry(wikiRoot, entry) {
|
|
2787
|
-
const logPath =
|
|
2855
|
+
const logPath = path15.join(wikiRoot, "log.md");
|
|
2788
2856
|
const logEntry = [
|
|
2789
2857
|
`- timestamp: "${entry.timestamp}"`,
|
|
2790
2858
|
` actor: "cleargate wiki ingest"`,
|
|
@@ -2792,25 +2860,25 @@ function appendLogEntry(wikiRoot, entry) {
|
|
|
2792
2860
|
` target: "${entry.id}"`,
|
|
2793
2861
|
` path: "${entry.relRawPath}"`
|
|
2794
2862
|
].join("\n");
|
|
2795
|
-
if (
|
|
2796
|
-
const existing =
|
|
2863
|
+
if (fs15.existsSync(logPath)) {
|
|
2864
|
+
const existing = fs15.readFileSync(logPath, "utf8");
|
|
2797
2865
|
const newContent = existing.trimEnd() + "\n" + logEntry + "\n";
|
|
2798
|
-
|
|
2866
|
+
fs15.writeFileSync(logPath, newContent, "utf8");
|
|
2799
2867
|
} else {
|
|
2800
|
-
|
|
2801
|
-
|
|
2868
|
+
fs15.mkdirSync(wikiRoot, { recursive: true });
|
|
2869
|
+
fs15.writeFileSync(logPath, `# Wiki Event Log
|
|
2802
2870
|
|
|
2803
2871
|
${logEntry}
|
|
2804
2872
|
`, "utf8");
|
|
2805
2873
|
}
|
|
2806
2874
|
}
|
|
2807
2875
|
function updateIndex(wikiRoot, opts) {
|
|
2808
|
-
const indexPath =
|
|
2876
|
+
const indexPath = path15.join(wikiRoot, "index.md");
|
|
2809
2877
|
const tmpPath = `${indexPath}.tmp`;
|
|
2810
2878
|
const newRow = `| [[${opts.id}]] | ${opts.type} | ${opts.status} | ${opts.relRawPath} |`;
|
|
2811
2879
|
let content;
|
|
2812
|
-
if (
|
|
2813
|
-
content =
|
|
2880
|
+
if (fs15.existsSync(indexPath)) {
|
|
2881
|
+
content = fs15.readFileSync(indexPath, "utf8");
|
|
2814
2882
|
const idPattern = `[[${opts.id}]]`;
|
|
2815
2883
|
const lines = content.split("\n");
|
|
2816
2884
|
let replaced = false;
|
|
@@ -2829,7 +2897,7 @@ function updateIndex(wikiRoot, opts) {
|
|
|
2829
2897
|
} else {
|
|
2830
2898
|
content = buildMinimalIndex(opts.id, opts.type, opts.status, opts.relRawPath);
|
|
2831
2899
|
}
|
|
2832
|
-
|
|
2900
|
+
fs15.writeFileSync(tmpPath, content, "utf8");
|
|
2833
2901
|
opts.rename(tmpPath, indexPath);
|
|
2834
2902
|
}
|
|
2835
2903
|
function insertIntoSection(content, id, newRow) {
|
|
@@ -2924,39 +2992,39 @@ function buildMinimalIndex(id, type, status, relRawPath) {
|
|
|
2924
2992
|
return lines.join("\n");
|
|
2925
2993
|
}
|
|
2926
2994
|
function recompileSynthesis(wikiRoot, cwd, templateDir) {
|
|
2927
|
-
const deliveryRoot =
|
|
2995
|
+
const deliveryRoot = path15.join(cwd, ".cleargate", "delivery");
|
|
2928
2996
|
let items = [];
|
|
2929
|
-
if (
|
|
2997
|
+
if (fs15.existsSync(deliveryRoot)) {
|
|
2930
2998
|
try {
|
|
2931
2999
|
items = scanRawItems(deliveryRoot, cwd);
|
|
2932
3000
|
} catch {
|
|
2933
3001
|
}
|
|
2934
3002
|
}
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
3003
|
+
fs15.writeFileSync(path15.join(wikiRoot, "active-sprint.md"), compile(items, templateDir), "utf8");
|
|
3004
|
+
fs15.writeFileSync(path15.join(wikiRoot, "open-gates.md"), compile2(items, templateDir), "utf8");
|
|
3005
|
+
fs15.writeFileSync(path15.join(wikiRoot, "product-state.md"), compile3(items, templateDir), "utf8");
|
|
3006
|
+
fs15.writeFileSync(path15.join(wikiRoot, "roadmap.md"), compile4(items, templateDir), "utf8");
|
|
2939
3007
|
}
|
|
2940
3008
|
|
|
2941
3009
|
// src/commands/wiki-lint.ts
|
|
2942
|
-
import * as
|
|
3010
|
+
import * as path19 from "path";
|
|
2943
3011
|
|
|
2944
3012
|
// src/wiki/load-wiki.ts
|
|
2945
|
-
import * as
|
|
2946
|
-
import * as
|
|
3013
|
+
import * as fs16 from "fs";
|
|
3014
|
+
import * as path16 from "path";
|
|
2947
3015
|
var BUCKET_DIRS = ["epics", "stories", "sprints", "proposals", "crs", "bugs", "topics"];
|
|
2948
3016
|
function loadWikiPages(wikiRoot) {
|
|
2949
3017
|
const results = [];
|
|
2950
3018
|
for (const bucket of BUCKET_DIRS) {
|
|
2951
|
-
const dir =
|
|
2952
|
-
if (!
|
|
2953
|
-
const entries =
|
|
3019
|
+
const dir = path16.join(wikiRoot, bucket);
|
|
3020
|
+
if (!fs16.existsSync(dir)) continue;
|
|
3021
|
+
const entries = fs16.readdirSync(dir, { encoding: "utf8" });
|
|
2954
3022
|
for (const filename of entries) {
|
|
2955
3023
|
if (!filename.endsWith(".md")) continue;
|
|
2956
|
-
const absPath =
|
|
2957
|
-
const stat =
|
|
3024
|
+
const absPath = path16.join(dir, filename);
|
|
3025
|
+
const stat = fs16.statSync(absPath);
|
|
2958
3026
|
if (!stat.isFile()) continue;
|
|
2959
|
-
const raw =
|
|
3027
|
+
const raw = fs16.readFileSync(absPath, "utf8");
|
|
2960
3028
|
let fm;
|
|
2961
3029
|
let body;
|
|
2962
3030
|
try {
|
|
@@ -2985,8 +3053,8 @@ function loadWikiPages(wikiRoot) {
|
|
|
2985
3053
|
}
|
|
2986
3054
|
|
|
2987
3055
|
// src/wiki/lint-checks.ts
|
|
2988
|
-
import * as
|
|
2989
|
-
import * as
|
|
3056
|
+
import * as fs17 from "fs";
|
|
3057
|
+
import * as path17 from "path";
|
|
2990
3058
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
2991
3059
|
import yaml3 from "js-yaml";
|
|
2992
3060
|
|
|
@@ -3045,9 +3113,9 @@ function checkOrphan(page, repoRoot) {
|
|
|
3045
3113
|
if (!rawPath) return null;
|
|
3046
3114
|
const isExcluded = EXCLUDED_DIRS.some((excl) => rawPath.startsWith(excl));
|
|
3047
3115
|
if (isExcluded) return null;
|
|
3048
|
-
const absRaw =
|
|
3049
|
-
if (!
|
|
3050
|
-
const relPage =
|
|
3116
|
+
const absRaw = path17.join(repoRoot, rawPath);
|
|
3117
|
+
if (!fs17.existsSync(absRaw)) {
|
|
3118
|
+
const relPage = path17.relative(path17.join(repoRoot, ".cleargate", "wiki"), page.absPath).replace(/\\/g, "/");
|
|
3051
3119
|
return {
|
|
3052
3120
|
category: "orphan",
|
|
3053
3121
|
line: `orphan: ${relPage} -> missing ${rawPath} (raw missing)`
|
|
@@ -3065,7 +3133,7 @@ function checkRepoMismatch(page, repoRoot) {
|
|
|
3065
3133
|
return null;
|
|
3066
3134
|
}
|
|
3067
3135
|
if (page.page.repo !== derivedRepo) {
|
|
3068
|
-
const relPage =
|
|
3136
|
+
const relPage = path17.relative(path17.join(repoRoot, ".cleargate", "wiki"), page.absPath).replace(/\\/g, "/");
|
|
3069
3137
|
return {
|
|
3070
3138
|
category: "repo-mismatch",
|
|
3071
3139
|
line: `repo-mismatch: ${relPage} declares repo:${page.page.repo} but raw_path implies repo:${derivedRepo}`
|
|
@@ -3090,7 +3158,7 @@ function checkStaleCommit(page, repoRoot, gitRunner) {
|
|
|
3090
3158
|
}
|
|
3091
3159
|
if (!currentSha) return null;
|
|
3092
3160
|
if (storedSha !== currentSha) {
|
|
3093
|
-
const relPage =
|
|
3161
|
+
const relPage = path17.relative(path17.join(repoRoot, ".cleargate", "wiki"), page.absPath).replace(/\\/g, "/");
|
|
3094
3162
|
return {
|
|
3095
3163
|
category: "stale-commit",
|
|
3096
3164
|
line: `stale-commit: ${relPage} at ${storedSha}, current ${currentSha}`
|
|
@@ -3101,14 +3169,14 @@ function checkStaleCommit(page, repoRoot, gitRunner) {
|
|
|
3101
3169
|
function checkMissingIngest(page, repoRoot) {
|
|
3102
3170
|
const rawPath = page.page.raw_path;
|
|
3103
3171
|
if (!rawPath) return null;
|
|
3104
|
-
const absRaw =
|
|
3105
|
-
if (!
|
|
3106
|
-
const rawStat =
|
|
3107
|
-
const pageStat =
|
|
3172
|
+
const absRaw = path17.join(repoRoot, rawPath);
|
|
3173
|
+
if (!fs17.existsSync(absRaw)) return null;
|
|
3174
|
+
const rawStat = fs17.statSync(absRaw);
|
|
3175
|
+
const pageStat = fs17.statSync(page.absPath);
|
|
3108
3176
|
const rawMtimeMs = rawStat.mtimeMs;
|
|
3109
3177
|
const pageMtimeMs = pageStat.mtimeMs;
|
|
3110
3178
|
if (rawMtimeMs - pageMtimeMs > 2e3) {
|
|
3111
|
-
const relPage =
|
|
3179
|
+
const relPage = path17.relative(path17.join(repoRoot, ".cleargate", "wiki"), page.absPath).replace(/\\/g, "/");
|
|
3112
3180
|
const rawMtime = rawStat.mtime.toISOString();
|
|
3113
3181
|
const pageMtime = pageStat.mtime.toISOString();
|
|
3114
3182
|
return {
|
|
@@ -3119,7 +3187,7 @@ function checkMissingIngest(page, repoRoot) {
|
|
|
3119
3187
|
return null;
|
|
3120
3188
|
}
|
|
3121
3189
|
function checkBrokenBacklinks(pages, repoRoot) {
|
|
3122
|
-
const wikiRoot =
|
|
3190
|
+
const wikiRoot = path17.join(repoRoot, ".cleargate", "wiki");
|
|
3123
3191
|
const byId = /* @__PURE__ */ new Map();
|
|
3124
3192
|
for (const p of pages) {
|
|
3125
3193
|
if (p.page.id) byId.set(p.page.id, p);
|
|
@@ -3133,7 +3201,7 @@ function checkBrokenBacklinks(pages, repoRoot) {
|
|
|
3133
3201
|
const parentId = match[1];
|
|
3134
3202
|
const parentPage = byId.get(parentId);
|
|
3135
3203
|
if (!parentPage) {
|
|
3136
|
-
const relChild =
|
|
3204
|
+
const relChild = path17.relative(wikiRoot, childPage.absPath).replace(/\\/g, "/");
|
|
3137
3205
|
findings.push({
|
|
3138
3206
|
category: "broken-backlink",
|
|
3139
3207
|
line: `broken-backlink: ${relChild} -> ${parentId} (parent missing child entry)`
|
|
@@ -3146,7 +3214,7 @@ function checkBrokenBacklinks(pages, repoRoot) {
|
|
|
3146
3214
|
(c) => c === childRef || c === childId
|
|
3147
3215
|
);
|
|
3148
3216
|
if (!parentHasChild) {
|
|
3149
|
-
const relChild =
|
|
3217
|
+
const relChild = path17.relative(wikiRoot, childPage.absPath).replace(/\\/g, "/");
|
|
3150
3218
|
findings.push({
|
|
3151
3219
|
category: "broken-backlink",
|
|
3152
3220
|
line: `broken-backlink: ${relChild} -> ${parentId} (parent missing child entry)`
|
|
@@ -3156,7 +3224,7 @@ function checkBrokenBacklinks(pages, repoRoot) {
|
|
|
3156
3224
|
return findings;
|
|
3157
3225
|
}
|
|
3158
3226
|
function checkInvalidatedCitations(pages, repoRoot) {
|
|
3159
|
-
const wikiRoot =
|
|
3227
|
+
const wikiRoot = path17.join(repoRoot, ".cleargate", "wiki");
|
|
3160
3228
|
const byId = /* @__PURE__ */ new Map();
|
|
3161
3229
|
for (const p of pages) {
|
|
3162
3230
|
if (p.page.id) byId.set(p.page.id, p);
|
|
@@ -3164,10 +3232,10 @@ function checkInvalidatedCitations(pages, repoRoot) {
|
|
|
3164
3232
|
const findings = [];
|
|
3165
3233
|
const topicPages = pages.filter((p) => p.page.type === "topic");
|
|
3166
3234
|
for (const topicPage of topicPages) {
|
|
3167
|
-
const relTopic =
|
|
3235
|
+
const relTopic = path17.relative(wikiRoot, topicPage.absPath).replace(/\\/g, "/");
|
|
3168
3236
|
let citesList = [];
|
|
3169
3237
|
try {
|
|
3170
|
-
const raw =
|
|
3238
|
+
const raw = fs17.readFileSync(topicPage.absPath, "utf8");
|
|
3171
3239
|
const { fm } = parseFrontmatter(raw);
|
|
3172
3240
|
const rawCites = fm["cites"];
|
|
3173
3241
|
if (Array.isArray(rawCites)) {
|
|
@@ -3203,7 +3271,7 @@ function checkExcludedPathIngested(page, repoRoot) {
|
|
|
3203
3271
|
if (!rawPath) return null;
|
|
3204
3272
|
const isExcluded = EXCLUDED_DIRS.some((excl) => rawPath.startsWith(excl));
|
|
3205
3273
|
if (isExcluded) {
|
|
3206
|
-
const relPage =
|
|
3274
|
+
const relPage = path17.relative(path17.join(repoRoot, ".cleargate", "wiki"), page.absPath).replace(/\\/g, "/");
|
|
3207
3275
|
return {
|
|
3208
3276
|
category: "excluded-path-ingested",
|
|
3209
3277
|
line: `excluded-path-ingested: ${relPage} (raw_path ${rawPath} is under an excluded directory)`
|
|
@@ -3214,7 +3282,7 @@ function checkExcludedPathIngested(page, repoRoot) {
|
|
|
3214
3282
|
function checkPaginationNeeded(pages) {
|
|
3215
3283
|
const bucketCounts = /* @__PURE__ */ new Map();
|
|
3216
3284
|
for (const p of pages) {
|
|
3217
|
-
const bucket =
|
|
3285
|
+
const bucket = path17.basename(path17.dirname(p.absPath));
|
|
3218
3286
|
bucketCounts.set(bucket, (bucketCounts.get(bucket) ?? 0) + 1);
|
|
3219
3287
|
}
|
|
3220
3288
|
const findings = [];
|
|
@@ -3252,11 +3320,11 @@ function parseCachedGateResult(raw) {
|
|
|
3252
3320
|
function checkGateFailure(page, repoRoot) {
|
|
3253
3321
|
const rawPath = page.page.raw_path;
|
|
3254
3322
|
if (!rawPath) return null;
|
|
3255
|
-
const absRaw =
|
|
3256
|
-
if (!
|
|
3323
|
+
const absRaw = path17.join(repoRoot, rawPath);
|
|
3324
|
+
if (!fs17.existsSync(absRaw)) return null;
|
|
3257
3325
|
let rawFm;
|
|
3258
3326
|
try {
|
|
3259
|
-
const raw =
|
|
3327
|
+
const raw = fs17.readFileSync(absRaw, "utf8");
|
|
3260
3328
|
const { fm } = parseFrontmatter(raw);
|
|
3261
3329
|
rawFm = fm;
|
|
3262
3330
|
} catch {
|
|
@@ -3290,11 +3358,11 @@ function checkGateFailure(page, repoRoot) {
|
|
|
3290
3358
|
function checkGateStaleness(page, repoRoot) {
|
|
3291
3359
|
const rawPath = page.page.raw_path;
|
|
3292
3360
|
if (!rawPath) return null;
|
|
3293
|
-
const absRaw =
|
|
3294
|
-
if (!
|
|
3361
|
+
const absRaw = path17.join(repoRoot, rawPath);
|
|
3362
|
+
if (!fs17.existsSync(absRaw)) return null;
|
|
3295
3363
|
let rawFm;
|
|
3296
3364
|
try {
|
|
3297
|
-
const raw =
|
|
3365
|
+
const raw = fs17.readFileSync(absRaw, "utf8");
|
|
3298
3366
|
const { fm } = parseFrontmatter(raw);
|
|
3299
3367
|
rawFm = fm;
|
|
3300
3368
|
} catch {
|
|
@@ -3317,7 +3385,7 @@ function checkGateStaleness(page, repoRoot) {
|
|
|
3317
3385
|
return null;
|
|
3318
3386
|
}
|
|
3319
3387
|
function discoverPlainTextMentions(pages, repoRoot) {
|
|
3320
|
-
const wikiRoot =
|
|
3388
|
+
const wikiRoot = path17.join(repoRoot, ".cleargate", "wiki");
|
|
3321
3389
|
const byId = /* @__PURE__ */ new Map();
|
|
3322
3390
|
for (const p of pages) {
|
|
3323
3391
|
if (p.page.id) byId.set(p.page.id, true);
|
|
@@ -3326,7 +3394,7 @@ function discoverPlainTextMentions(pages, repoRoot) {
|
|
|
3326
3394
|
const ID_PATTERN = /\b((?:EPIC|STORY|SPRINT|PROPOSAL|CR|BUG)-[\w-]+)\b/g;
|
|
3327
3395
|
const LINK_PATTERN = /\[\[[\w-]+\]\]/g;
|
|
3328
3396
|
for (const page of pages) {
|
|
3329
|
-
const relPage =
|
|
3397
|
+
const relPage = path17.relative(wikiRoot, page.absPath).replace(/\\/g, "/");
|
|
3330
3398
|
const wrappedRefs = /* @__PURE__ */ new Set();
|
|
3331
3399
|
for (const m of page.body.matchAll(LINK_PATTERN)) {
|
|
3332
3400
|
const inner = m[0].slice(2, -2);
|
|
@@ -3343,11 +3411,11 @@ function discoverPlainTextMentions(pages, repoRoot) {
|
|
|
3343
3411
|
return suggestions;
|
|
3344
3412
|
}
|
|
3345
3413
|
function checkIndexBudget(repoRoot, indexTokenCeiling) {
|
|
3346
|
-
const indexPath =
|
|
3347
|
-
if (!
|
|
3414
|
+
const indexPath = path17.join(repoRoot, ".cleargate", "wiki", "index.md");
|
|
3415
|
+
if (!fs17.existsSync(indexPath)) {
|
|
3348
3416
|
return { finding: null };
|
|
3349
3417
|
}
|
|
3350
|
-
const bytes =
|
|
3418
|
+
const bytes = fs17.statSync(indexPath).size;
|
|
3351
3419
|
const tokens = Math.round(bytes / 4);
|
|
3352
3420
|
const ceiling = indexTokenCeiling;
|
|
3353
3421
|
if (tokens > ceiling) {
|
|
@@ -3364,18 +3432,18 @@ function checkIndexBudget(repoRoot, indexTokenCeiling) {
|
|
|
3364
3432
|
}
|
|
3365
3433
|
|
|
3366
3434
|
// src/lib/wiki-config.ts
|
|
3367
|
-
import * as
|
|
3368
|
-
import * as
|
|
3435
|
+
import * as fs18 from "fs";
|
|
3436
|
+
import * as path18 from "path";
|
|
3369
3437
|
import yaml4 from "js-yaml";
|
|
3370
3438
|
var DEFAULT_INDEX_TOKEN_CEILING = 8e3;
|
|
3371
3439
|
function loadWikiConfig(repoRoot) {
|
|
3372
|
-
const configPath =
|
|
3373
|
-
if (!
|
|
3440
|
+
const configPath = path18.join(repoRoot, ".cleargate", "config.yml");
|
|
3441
|
+
if (!fs18.existsSync(configPath)) {
|
|
3374
3442
|
return { wiki: { index_token_ceiling: DEFAULT_INDEX_TOKEN_CEILING }, gates: {} };
|
|
3375
3443
|
}
|
|
3376
3444
|
let raw;
|
|
3377
3445
|
try {
|
|
3378
|
-
raw =
|
|
3446
|
+
raw = fs18.readFileSync(configPath, "utf8");
|
|
3379
3447
|
} catch (err) {
|
|
3380
3448
|
throw new Error(`Failed to read ${configPath}: ${String(err)}`);
|
|
3381
3449
|
}
|
|
@@ -3436,7 +3504,7 @@ async function wikiLintHandler(opts = {}) {
|
|
|
3436
3504
|
const exit = opts.exit ?? ((c) => process.exit(c));
|
|
3437
3505
|
const gitRunner = opts.gitRunner;
|
|
3438
3506
|
const mode = opts.mode ?? "enforce";
|
|
3439
|
-
const wikiRoot =
|
|
3507
|
+
const wikiRoot = path19.join(cwd, ".cleargate", "wiki");
|
|
3440
3508
|
const repoRoot = cwd;
|
|
3441
3509
|
let pages = loadWikiPages(wikiRoot);
|
|
3442
3510
|
const findings = [];
|
|
@@ -3507,8 +3575,8 @@ async function wikiLintHandler(opts = {}) {
|
|
|
3507
3575
|
}
|
|
3508
3576
|
|
|
3509
3577
|
// src/commands/wiki-query.ts
|
|
3510
|
-
import * as
|
|
3511
|
-
import * as
|
|
3578
|
+
import * as fs19 from "fs";
|
|
3579
|
+
import * as path20 from "path";
|
|
3512
3580
|
function computeSlug(query) {
|
|
3513
3581
|
return query.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 40).replace(/-+$/, "");
|
|
3514
3582
|
}
|
|
@@ -3538,9 +3606,9 @@ async function wikiQueryHandler(opts) {
|
|
|
3538
3606
|
const query = opts.query;
|
|
3539
3607
|
const persist = opts.persist ?? false;
|
|
3540
3608
|
void stderr;
|
|
3541
|
-
const wikiRoot =
|
|
3542
|
-
const indexPath =
|
|
3543
|
-
if (!
|
|
3609
|
+
const wikiRoot = path20.join(cwd, ".cleargate", "wiki");
|
|
3610
|
+
const indexPath = path20.join(wikiRoot, "index.md");
|
|
3611
|
+
if (!fs19.existsSync(indexPath)) {
|
|
3544
3612
|
stdout(`wiki query: no index.md found at ${indexPath}
|
|
3545
3613
|
`);
|
|
3546
3614
|
stdout(`Run \`cleargate wiki build\` first.
|
|
@@ -3548,7 +3616,7 @@ async function wikiQueryHandler(opts) {
|
|
|
3548
3616
|
exit(1);
|
|
3549
3617
|
return;
|
|
3550
3618
|
}
|
|
3551
|
-
const indexContent =
|
|
3619
|
+
const indexContent = fs19.readFileSync(indexPath, "utf8");
|
|
3552
3620
|
const matches = searchIndex(indexContent, query);
|
|
3553
3621
|
if (matches.length === 0) {
|
|
3554
3622
|
stdout(`wiki query: no matches for "${query}"
|
|
@@ -3573,8 +3641,8 @@ async function wikiQueryHandler(opts) {
|
|
|
3573
3641
|
return;
|
|
3574
3642
|
}
|
|
3575
3643
|
const slug = computeSlug(query);
|
|
3576
|
-
const topicsDir =
|
|
3577
|
-
|
|
3644
|
+
const topicsDir = path20.join(wikiRoot, "topics");
|
|
3645
|
+
fs19.mkdirSync(topicsDir, { recursive: true });
|
|
3578
3646
|
const citesArray = matches.map(({ id }) => `"[[${id}]]"`);
|
|
3579
3647
|
const createdAt = now();
|
|
3580
3648
|
const frontmatter = [
|
|
@@ -3589,13 +3657,13 @@ async function wikiQueryHandler(opts) {
|
|
|
3589
3657
|
const topicContent = `${frontmatter}
|
|
3590
3658
|
|
|
3591
3659
|
${body}`;
|
|
3592
|
-
const topicPath =
|
|
3593
|
-
|
|
3660
|
+
const topicPath = path20.join(topicsDir, `${slug}.md`);
|
|
3661
|
+
fs19.writeFileSync(topicPath, topicContent, "utf8");
|
|
3594
3662
|
updateIndexTopicsSection(indexPath, slug, query, createdAt);
|
|
3595
3663
|
exit(0);
|
|
3596
3664
|
}
|
|
3597
3665
|
function updateIndexTopicsSection(indexPath, slug, query, createdAt) {
|
|
3598
|
-
let content =
|
|
3666
|
+
let content = fs19.readFileSync(indexPath, "utf8");
|
|
3599
3667
|
const row = `| ${slug} | ${query} | ${createdAt} |`;
|
|
3600
3668
|
if (content.includes("## Topics")) {
|
|
3601
3669
|
const topicsIdx = content.indexOf("## Topics");
|
|
@@ -3618,12 +3686,12 @@ ${row}
|
|
|
3618
3686
|
${row}
|
|
3619
3687
|
`;
|
|
3620
3688
|
}
|
|
3621
|
-
|
|
3689
|
+
fs19.writeFileSync(indexPath, content, "utf8");
|
|
3622
3690
|
}
|
|
3623
3691
|
|
|
3624
3692
|
// src/commands/wiki-audit-status.ts
|
|
3625
|
-
import * as
|
|
3626
|
-
import * as
|
|
3693
|
+
import * as fs20 from "fs";
|
|
3694
|
+
import * as path21 from "path";
|
|
3627
3695
|
import * as readline4 from "readline";
|
|
3628
3696
|
var TERMINAL = /* @__PURE__ */ new Set(["Completed", "Done", "Abandoned", "Closed", "Resolved"]);
|
|
3629
3697
|
async function wikiAuditStatusHandler(opts = {}) {
|
|
@@ -3636,8 +3704,8 @@ async function wikiAuditStatusHandler(opts = {}) {
|
|
|
3636
3704
|
});
|
|
3637
3705
|
const exit = opts.exit ?? ((c) => process.exit(c));
|
|
3638
3706
|
const isTTY = opts.isTTY ?? Boolean(process.stdout.isTTY);
|
|
3639
|
-
const deliveryRoot =
|
|
3640
|
-
if (!
|
|
3707
|
+
const deliveryRoot = path21.join(cwd, ".cleargate", "delivery");
|
|
3708
|
+
if (!fs20.existsSync(deliveryRoot)) {
|
|
3641
3709
|
stderr(`audit-status: .cleargate/delivery/ not found at ${deliveryRoot}
|
|
3642
3710
|
`);
|
|
3643
3711
|
exit(1);
|
|
@@ -3777,7 +3845,7 @@ async function wikiAuditStatusHandler(opts = {}) {
|
|
|
3777
3845
|
}
|
|
3778
3846
|
}
|
|
3779
3847
|
for (const d of fixable) {
|
|
3780
|
-
const rawText =
|
|
3848
|
+
const rawText = fs20.readFileSync(d.absPath, "utf8");
|
|
3781
3849
|
const updated = applyStatusFix(rawText, d.suggestedStatus);
|
|
3782
3850
|
if (!opts.quiet) {
|
|
3783
3851
|
stdout(`--- ${d.rawPath}
|
|
@@ -3793,7 +3861,7 @@ async function wikiAuditStatusHandler(opts = {}) {
|
|
|
3793
3861
|
stdout(`+${newLine}
|
|
3794
3862
|
`);
|
|
3795
3863
|
}
|
|
3796
|
-
|
|
3864
|
+
fs20.writeFileSync(d.absPath, updated, "utf8");
|
|
3797
3865
|
}
|
|
3798
3866
|
stdout(`audit-status: applied ${fixable.length} fix(es)
|
|
3799
3867
|
`);
|
|
@@ -3821,8 +3889,8 @@ function applyStatusFix(rawText, newStatus) {
|
|
|
3821
3889
|
}
|
|
3822
3890
|
|
|
3823
3891
|
// src/commands/doctor.ts
|
|
3824
|
-
import * as
|
|
3825
|
-
import * as
|
|
3892
|
+
import * as fs21 from "fs";
|
|
3893
|
+
import * as path22 from "path";
|
|
3826
3894
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
3827
3895
|
|
|
3828
3896
|
// src/lib/pricing.ts
|
|
@@ -3898,24 +3966,24 @@ function parseHookLogLine(line) {
|
|
|
3898
3966
|
};
|
|
3899
3967
|
}
|
|
3900
3968
|
function runHookHealth(stdout, cwd, now, outcome) {
|
|
3901
|
-
const cleargateDir =
|
|
3902
|
-
if (!
|
|
3969
|
+
const cleargateDir = path22.join(cwd, ".cleargate");
|
|
3970
|
+
if (!fs21.existsSync(cleargateDir)) {
|
|
3903
3971
|
stdout("cleargate misconfigured: no .cleargate/ found. Run: cleargate init");
|
|
3904
3972
|
if (outcome) outcome.configError = true;
|
|
3905
3973
|
return;
|
|
3906
3974
|
}
|
|
3907
|
-
const manifestPath =
|
|
3908
|
-
if (!
|
|
3975
|
+
const manifestPath = path22.join(cwd, "cleargate-planning", "MANIFEST.json");
|
|
3976
|
+
if (!fs21.existsSync(manifestPath)) {
|
|
3909
3977
|
stdout(`cleargate misconfigured: cleargate-planning/MANIFEST.json not found. Run: cleargate init`);
|
|
3910
3978
|
if (outcome) outcome.configError = true;
|
|
3911
3979
|
}
|
|
3912
|
-
const settingsPath =
|
|
3913
|
-
if (!
|
|
3980
|
+
const settingsPath = path22.join(cwd, ".claude", "settings.json");
|
|
3981
|
+
if (!fs21.existsSync(settingsPath)) {
|
|
3914
3982
|
stdout("[doctor] No .claude/settings.json found \u2014 hook config unavailable.");
|
|
3915
3983
|
return;
|
|
3916
3984
|
}
|
|
3917
3985
|
try {
|
|
3918
|
-
const raw =
|
|
3986
|
+
const raw = fs21.readFileSync(settingsPath, "utf-8");
|
|
3919
3987
|
const settings = JSON.parse(raw);
|
|
3920
3988
|
const hasHooks = typeof settings === "object" && settings !== null && "hooks" in settings;
|
|
3921
3989
|
if (hasHooks) {
|
|
@@ -3926,13 +3994,13 @@ function runHookHealth(stdout, cwd, now, outcome) {
|
|
|
3926
3994
|
} catch {
|
|
3927
3995
|
stdout("[doctor] .claude/settings.json is not valid JSON \u2014 cannot verify hook config.");
|
|
3928
3996
|
}
|
|
3929
|
-
const logPath =
|
|
3930
|
-
if (!
|
|
3997
|
+
const logPath = path22.join(cwd, ".cleargate", "hook-log", "gate-check.log");
|
|
3998
|
+
if (!fs21.existsSync(logPath)) {
|
|
3931
3999
|
return;
|
|
3932
4000
|
}
|
|
3933
4001
|
let logContent;
|
|
3934
4002
|
try {
|
|
3935
|
-
logContent =
|
|
4003
|
+
logContent = fs21.readFileSync(logPath, "utf-8");
|
|
3936
4004
|
} catch {
|
|
3937
4005
|
return;
|
|
3938
4006
|
}
|
|
@@ -4046,8 +4114,8 @@ function parseCachedGateResult2(raw) {
|
|
|
4046
4114
|
};
|
|
4047
4115
|
}
|
|
4048
4116
|
function emitResolverStatusLine(cwd, stdout) {
|
|
4049
|
-
const distCliPath =
|
|
4050
|
-
if (
|
|
4117
|
+
const distCliPath = path22.join(cwd, "cleargate-cli", "dist", "cli.js");
|
|
4118
|
+
if (fs21.existsSync(distCliPath)) {
|
|
4051
4119
|
stdout(`cleargate CLI: local dist \u2014 ${distCliPath}`);
|
|
4052
4120
|
return;
|
|
4053
4121
|
}
|
|
@@ -4061,10 +4129,10 @@ function emitResolverStatusLine(cwd, stdout) {
|
|
|
4061
4129
|
return;
|
|
4062
4130
|
}
|
|
4063
4131
|
let pinVersion = "unknown";
|
|
4064
|
-
const hookPath =
|
|
4065
|
-
if (
|
|
4132
|
+
const hookPath = path22.join(cwd, ".claude", "hooks", "stamp-and-gate.sh");
|
|
4133
|
+
if (fs21.existsSync(hookPath)) {
|
|
4066
4134
|
try {
|
|
4067
|
-
const hookContent =
|
|
4135
|
+
const hookContent = fs21.readFileSync(hookPath, "utf-8");
|
|
4068
4136
|
const pinMatch = hookContent.match(/^#\s*cleargate-pin:\s*(\S+)\s*$/m);
|
|
4069
4137
|
if (pinMatch?.[1]) {
|
|
4070
4138
|
pinVersion = pinMatch[1];
|
|
@@ -4096,10 +4164,10 @@ async function runSessionStart(cwd, stdout, outcome) {
|
|
|
4096
4164
|
if (outcome && resolverLines.some((l) => l.includes("\u{1F534}"))) {
|
|
4097
4165
|
outcome.configError = true;
|
|
4098
4166
|
}
|
|
4099
|
-
const pendingSyncDir =
|
|
4167
|
+
const pendingSyncDir = path22.join(cwd, ".cleargate", "delivery", "pending-sync");
|
|
4100
4168
|
let files;
|
|
4101
4169
|
try {
|
|
4102
|
-
files =
|
|
4170
|
+
files = fs21.readdirSync(pendingSyncDir).filter((f) => f.endsWith(".md")).map((f) => path22.join(pendingSyncDir, f));
|
|
4103
4171
|
} catch {
|
|
4104
4172
|
return;
|
|
4105
4173
|
}
|
|
@@ -4108,7 +4176,7 @@ async function runSessionStart(cwd, stdout, outcome) {
|
|
|
4108
4176
|
for (const filePath of files) {
|
|
4109
4177
|
let raw;
|
|
4110
4178
|
try {
|
|
4111
|
-
raw =
|
|
4179
|
+
raw = fs21.readFileSync(filePath, "utf-8");
|
|
4112
4180
|
} catch {
|
|
4113
4181
|
continue;
|
|
4114
4182
|
}
|
|
@@ -4134,13 +4202,13 @@ async function runSessionStart(cwd, stdout, outcome) {
|
|
|
4134
4202
|
}
|
|
4135
4203
|
}
|
|
4136
4204
|
if (!itemId) {
|
|
4137
|
-
itemId =
|
|
4205
|
+
itemId = path22.basename(filePath, ".md");
|
|
4138
4206
|
}
|
|
4139
4207
|
const firstCriterionId = gate2.failing_criteria.length > 0 ? gate2.failing_criteria[0]?.id ?? "" : "";
|
|
4140
4208
|
blocked.push({ id: itemId, firstCriterionId });
|
|
4141
4209
|
}
|
|
4142
|
-
const activesentinel =
|
|
4143
|
-
const sprintActive =
|
|
4210
|
+
const activesentinel = path22.join(cwd, ".cleargate", "sprint-runs", ".active");
|
|
4211
|
+
const sprintActive = fs21.existsSync(activesentinel);
|
|
4144
4212
|
const shouldRemind = !hasApprovedStory && !sprintActive;
|
|
4145
4213
|
if (shouldRemind) {
|
|
4146
4214
|
stdout(PLANNING_FIRST_REMINDER);
|
|
@@ -4175,10 +4243,10 @@ async function runPricing(filePath, cwd, stdout, stderr, exit, outcome) {
|
|
|
4175
4243
|
exit(2);
|
|
4176
4244
|
return;
|
|
4177
4245
|
}
|
|
4178
|
-
const absPath =
|
|
4246
|
+
const absPath = path22.isAbsolute(filePath) ? filePath : path22.resolve(cwd, filePath);
|
|
4179
4247
|
let raw;
|
|
4180
4248
|
try {
|
|
4181
|
-
raw =
|
|
4249
|
+
raw = fs21.readFileSync(absPath, "utf-8");
|
|
4182
4250
|
} catch {
|
|
4183
4251
|
stderr(`cleargate doctor --pricing: cannot read file: ${absPath}`);
|
|
4184
4252
|
if (outcome) outcome.configError = true;
|
|
@@ -4240,7 +4308,7 @@ async function runPricing(filePath, cwd, stdout, stderr, exit, outcome) {
|
|
|
4240
4308
|
const output = draftTokens.output ?? 0;
|
|
4241
4309
|
const cacheRead = draftTokens.cache_read ?? 0;
|
|
4242
4310
|
const cacheCreation = draftTokens.cache_creation ?? 0;
|
|
4243
|
-
const fileName =
|
|
4311
|
+
const fileName = path22.basename(absPath);
|
|
4244
4312
|
stdout(
|
|
4245
4313
|
`${fileName}: ${model} \u2014 input:${input} output:${output} cache_read:${cacheRead} cache_creation:${cacheCreation} \u2248 $${usd.toFixed(4)}`
|
|
4246
4314
|
);
|
|
@@ -4253,15 +4321,15 @@ function globMatch(pattern, filePath) {
|
|
|
4253
4321
|
return re.test(normalFile);
|
|
4254
4322
|
}
|
|
4255
4323
|
async function runCanEdit(filePath, cwd, stdout, exit, outcome) {
|
|
4256
|
-
const activeSentinel =
|
|
4257
|
-
if (
|
|
4324
|
+
const activeSentinel = path22.join(cwd, ".cleargate", "sprint-runs", ".active");
|
|
4325
|
+
if (fs21.existsSync(activeSentinel)) {
|
|
4258
4326
|
stdout("allowed: sprint active");
|
|
4259
4327
|
return;
|
|
4260
4328
|
}
|
|
4261
|
-
const pendingSyncDir =
|
|
4329
|
+
const pendingSyncDir = path22.join(cwd, ".cleargate", "delivery", "pending-sync");
|
|
4262
4330
|
let files;
|
|
4263
4331
|
try {
|
|
4264
|
-
files =
|
|
4332
|
+
files = fs21.readdirSync(pendingSyncDir).filter((f) => f.endsWith(".md")).map((f) => path22.join(pendingSyncDir, f));
|
|
4265
4333
|
} catch {
|
|
4266
4334
|
stdout("blocked: no_approved_stories");
|
|
4267
4335
|
if (outcome) outcome.blocker = true;
|
|
@@ -4273,7 +4341,7 @@ async function runCanEdit(filePath, cwd, stdout, exit, outcome) {
|
|
|
4273
4341
|
for (const storyPath of files) {
|
|
4274
4342
|
let raw;
|
|
4275
4343
|
try {
|
|
4276
|
-
raw =
|
|
4344
|
+
raw = fs21.readFileSync(storyPath, "utf-8");
|
|
4277
4345
|
} catch {
|
|
4278
4346
|
continue;
|
|
4279
4347
|
}
|
|
@@ -4370,13 +4438,13 @@ async function doctorHandler(flags, cli) {
|
|
|
4370
4438
|
}
|
|
4371
4439
|
|
|
4372
4440
|
// src/commands/gate.ts
|
|
4373
|
-
import * as
|
|
4374
|
-
import * as
|
|
4441
|
+
import * as fs25 from "fs";
|
|
4442
|
+
import * as path25 from "path";
|
|
4375
4443
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
4376
4444
|
|
|
4377
4445
|
// src/commands/execution-mode.ts
|
|
4378
|
-
import * as
|
|
4379
|
-
import * as
|
|
4446
|
+
import * as fs22 from "fs";
|
|
4447
|
+
import * as path23 from "path";
|
|
4380
4448
|
var V1_INERT_MESSAGE = "v1 mode active \u2014 command inert. Set execution_mode: v2 in sprint frontmatter to enable.";
|
|
4381
4449
|
function parseFrontmatterSimple(raw) {
|
|
4382
4450
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
|
|
@@ -4394,24 +4462,24 @@ function parseFrontmatterSimple(raw) {
|
|
|
4394
4462
|
}
|
|
4395
4463
|
function discoverSprintFile(sprintId, cwd) {
|
|
4396
4464
|
const searchDirs = [
|
|
4397
|
-
|
|
4398
|
-
|
|
4465
|
+
path23.join(cwd, ".cleargate", "delivery", "pending-sync"),
|
|
4466
|
+
path23.join(cwd, ".cleargate", "delivery", "archive")
|
|
4399
4467
|
];
|
|
4400
4468
|
for (const dir of searchDirs) {
|
|
4401
|
-
if (!
|
|
4469
|
+
if (!fs22.existsSync(dir)) continue;
|
|
4402
4470
|
let entries;
|
|
4403
4471
|
try {
|
|
4404
|
-
entries =
|
|
4472
|
+
entries = fs22.readdirSync(dir);
|
|
4405
4473
|
} catch {
|
|
4406
4474
|
continue;
|
|
4407
4475
|
}
|
|
4408
4476
|
const prefix = `${sprintId}_`;
|
|
4409
4477
|
for (const entry of entries) {
|
|
4410
4478
|
if (entry.startsWith(prefix) && entry.endsWith(".md")) {
|
|
4411
|
-
return
|
|
4479
|
+
return path23.join(dir, entry);
|
|
4412
4480
|
}
|
|
4413
4481
|
if (entry === `${sprintId}.md`) {
|
|
4414
|
-
return
|
|
4482
|
+
return path23.join(dir, entry);
|
|
4415
4483
|
}
|
|
4416
4484
|
}
|
|
4417
4485
|
}
|
|
@@ -4419,9 +4487,9 @@ function discoverSprintFile(sprintId, cwd) {
|
|
|
4419
4487
|
}
|
|
4420
4488
|
function resolveSprintIdFromSentinel(cwd) {
|
|
4421
4489
|
const resolvedCwd = cwd ?? process.cwd();
|
|
4422
|
-
const sentinelPath =
|
|
4490
|
+
const sentinelPath = path23.join(resolvedCwd, ".cleargate", "sprint-runs", ".active");
|
|
4423
4491
|
try {
|
|
4424
|
-
const content =
|
|
4492
|
+
const content = fs22.readFileSync(sentinelPath, "utf8").trim();
|
|
4425
4493
|
return content.length > 0 ? content : null;
|
|
4426
4494
|
} catch {
|
|
4427
4495
|
return null;
|
|
@@ -4440,12 +4508,12 @@ function readSprintExecutionMode(sprintId, opts = {}) {
|
|
|
4440
4508
|
if (!filePath) {
|
|
4441
4509
|
filePath = discoverSprintFile(resolvedSprintId, cwd);
|
|
4442
4510
|
}
|
|
4443
|
-
if (!filePath || !
|
|
4511
|
+
if (!filePath || !fs22.existsSync(filePath)) {
|
|
4444
4512
|
return "v1";
|
|
4445
4513
|
}
|
|
4446
4514
|
let raw;
|
|
4447
4515
|
try {
|
|
4448
|
-
raw =
|
|
4516
|
+
raw = fs22.readFileSync(filePath, "utf8");
|
|
4449
4517
|
} catch {
|
|
4450
4518
|
return "v1";
|
|
4451
4519
|
}
|
|
@@ -4463,8 +4531,8 @@ function printInertAndExit(stdoutFn, exitFn) {
|
|
|
4463
4531
|
import yaml6 from "js-yaml";
|
|
4464
4532
|
|
|
4465
4533
|
// src/lib/readiness-predicates.ts
|
|
4466
|
-
import * as
|
|
4467
|
-
import * as
|
|
4534
|
+
import * as fs23 from "fs";
|
|
4535
|
+
import * as path24 from "path";
|
|
4468
4536
|
function parsePredicate(src) {
|
|
4469
4537
|
const s = src.trim();
|
|
4470
4538
|
const fmMatch = s.match(
|
|
@@ -4628,18 +4696,18 @@ function compareValues(actual, op, expected) {
|
|
|
4628
4696
|
}
|
|
4629
4697
|
function resolveLinkedPath(ref, docAbsPath, projectRoot) {
|
|
4630
4698
|
const candidates = [
|
|
4631
|
-
|
|
4632
|
-
|
|
4699
|
+
path24.resolve(path24.dirname(docAbsPath), ref),
|
|
4700
|
+
path24.resolve(projectRoot, ref)
|
|
4633
4701
|
];
|
|
4634
4702
|
for (const candidate of candidates) {
|
|
4635
4703
|
if (!candidate.startsWith(projectRoot)) continue;
|
|
4636
|
-
if (
|
|
4704
|
+
if (fs23.existsSync(candidate)) return candidate;
|
|
4637
4705
|
}
|
|
4638
4706
|
return null;
|
|
4639
4707
|
}
|
|
4640
4708
|
function readFrontmatterFromFile(absPath) {
|
|
4641
4709
|
try {
|
|
4642
|
-
const raw =
|
|
4710
|
+
const raw = fs23.readFileSync(absPath, "utf8");
|
|
4643
4711
|
const lines = raw.split("\n");
|
|
4644
4712
|
if (lines[0] !== "---") return {};
|
|
4645
4713
|
let closeIdx = -1;
|
|
@@ -4783,14 +4851,14 @@ function applyCountOp(actual, op, n) {
|
|
|
4783
4851
|
}
|
|
4784
4852
|
}
|
|
4785
4853
|
function evalFileExists(parsed, projectRoot) {
|
|
4786
|
-
const resolved =
|
|
4787
|
-
if (!resolved.startsWith(projectRoot +
|
|
4854
|
+
const resolved = path24.resolve(projectRoot, parsed.path);
|
|
4855
|
+
if (!resolved.startsWith(projectRoot + path24.sep) && resolved !== projectRoot) {
|
|
4788
4856
|
return {
|
|
4789
4857
|
pass: false,
|
|
4790
4858
|
detail: `path '${parsed.path}' resolves outside project root (sandbox violation)`
|
|
4791
4859
|
};
|
|
4792
4860
|
}
|
|
4793
|
-
const exists =
|
|
4861
|
+
const exists = fs23.existsSync(resolved);
|
|
4794
4862
|
return {
|
|
4795
4863
|
pass: exists,
|
|
4796
4864
|
detail: exists ? `${parsed.path} exists` : `${parsed.path} not found`
|
|
@@ -4798,13 +4866,13 @@ function evalFileExists(parsed, projectRoot) {
|
|
|
4798
4866
|
}
|
|
4799
4867
|
function evalLinkTargetExists(parsed, opts) {
|
|
4800
4868
|
const projectRoot = opts?.projectRoot ?? process.cwd();
|
|
4801
|
-
const wikiIndexPath = opts?.wikiIndexPath ??
|
|
4869
|
+
const wikiIndexPath = opts?.wikiIndexPath ?? path24.join(projectRoot, ".cleargate", "wiki", "index.md");
|
|
4802
4870
|
if (!wikiIndexPath.startsWith(projectRoot)) {
|
|
4803
4871
|
return { pass: false, detail: "wikiIndexPath resolves outside project root" };
|
|
4804
4872
|
}
|
|
4805
4873
|
let indexContent;
|
|
4806
4874
|
try {
|
|
4807
|
-
indexContent =
|
|
4875
|
+
indexContent = fs23.readFileSync(wikiIndexPath, "utf8");
|
|
4808
4876
|
} catch {
|
|
4809
4877
|
return { pass: false, detail: `wiki index not found at ${wikiIndexPath}` };
|
|
4810
4878
|
}
|
|
@@ -4815,13 +4883,13 @@ function evalLinkTargetExists(parsed, opts) {
|
|
|
4815
4883
|
};
|
|
4816
4884
|
}
|
|
4817
4885
|
function evalStatusOf(parsed, opts, projectRoot) {
|
|
4818
|
-
const wikiIndexPath = opts?.wikiIndexPath ??
|
|
4886
|
+
const wikiIndexPath = opts?.wikiIndexPath ?? path24.join(projectRoot, ".cleargate", "wiki", "index.md");
|
|
4819
4887
|
if (!wikiIndexPath.startsWith(projectRoot)) {
|
|
4820
4888
|
return { pass: false, detail: "wikiIndexPath resolves outside project root" };
|
|
4821
4889
|
}
|
|
4822
4890
|
let indexContent;
|
|
4823
4891
|
try {
|
|
4824
|
-
indexContent =
|
|
4892
|
+
indexContent = fs23.readFileSync(wikiIndexPath, "utf8");
|
|
4825
4893
|
} catch {
|
|
4826
4894
|
return { pass: false, detail: `wiki index not found at ${wikiIndexPath}` };
|
|
4827
4895
|
}
|
|
@@ -4832,7 +4900,7 @@ function evalStatusOf(parsed, opts, projectRoot) {
|
|
|
4832
4900
|
return { pass: false, detail: `[[${parsed.id}]] not found in wiki index` };
|
|
4833
4901
|
}
|
|
4834
4902
|
const rawPath = rowMatch[1].trim();
|
|
4835
|
-
const fullPath =
|
|
4903
|
+
const fullPath = path24.resolve(projectRoot, rawPath);
|
|
4836
4904
|
if (!fullPath.startsWith(projectRoot)) {
|
|
4837
4905
|
return { pass: false, detail: `wiki path for ${parsed.id} resolves outside project root` };
|
|
4838
4906
|
}
|
|
@@ -4849,12 +4917,12 @@ function evalStatusOf(parsed, opts, projectRoot) {
|
|
|
4849
4917
|
}
|
|
4850
4918
|
|
|
4851
4919
|
// src/lib/frontmatter-cache.ts
|
|
4852
|
-
import * as
|
|
4920
|
+
import * as fs24 from "fs/promises";
|
|
4853
4921
|
import yaml5 from "js-yaml";
|
|
4854
4922
|
async function readCachedGate(absPath) {
|
|
4855
4923
|
let raw;
|
|
4856
4924
|
try {
|
|
4857
|
-
raw = await
|
|
4925
|
+
raw = await fs24.readFile(absPath, "utf8");
|
|
4858
4926
|
} catch {
|
|
4859
4927
|
return null;
|
|
4860
4928
|
}
|
|
@@ -4874,7 +4942,7 @@ async function writeCachedGate(absPath, result, opts) {
|
|
|
4874
4942
|
failing_criteria: result.failing_criteria,
|
|
4875
4943
|
last_gate_check: lastGateCheck
|
|
4876
4944
|
};
|
|
4877
|
-
const raw = await
|
|
4945
|
+
const raw = await fs24.readFile(absPath, "utf8");
|
|
4878
4946
|
let fm;
|
|
4879
4947
|
let body;
|
|
4880
4948
|
try {
|
|
@@ -4904,7 +4972,7 @@ async function writeCachedGate(absPath, result, opts) {
|
|
|
4904
4972
|
|
|
4905
4973
|
${body}` : `${fmBlock}
|
|
4906
4974
|
`;
|
|
4907
|
-
await
|
|
4975
|
+
await fs24.writeFile(absPath, newContent, "utf8");
|
|
4908
4976
|
}
|
|
4909
4977
|
function coerceCachedGate(val) {
|
|
4910
4978
|
if (val === void 0 || val === null) return null;
|
|
@@ -4935,7 +5003,7 @@ function coerceCachedGate(val) {
|
|
|
4935
5003
|
|
|
4936
5004
|
// src/commands/gate.ts
|
|
4937
5005
|
function loadGateBlocks(gatesDocPath) {
|
|
4938
|
-
const raw =
|
|
5006
|
+
const raw = fs25.readFileSync(gatesDocPath, "utf8");
|
|
4939
5007
|
const blocks = [];
|
|
4940
5008
|
const fenceRe = /^```yaml\n([\s\S]*?)^```/gm;
|
|
4941
5009
|
let match;
|
|
@@ -4970,14 +5038,14 @@ async function gateCheckHandler(file, opts, cli) {
|
|
|
4970
5038
|
const exitFn = cli?.exit ?? ((code) => process.exit(code));
|
|
4971
5039
|
const cwd = cli?.cwd ?? process.cwd();
|
|
4972
5040
|
const nowFn = cli?.now ?? (() => /* @__PURE__ */ new Date());
|
|
4973
|
-
const absPath =
|
|
4974
|
-
if (!
|
|
5041
|
+
const absPath = path25.isAbsolute(file) ? file : path25.resolve(cwd, file);
|
|
5042
|
+
if (!fs25.existsSync(absPath)) {
|
|
4975
5043
|
stderrFn(`[cleargate gate] error: file not found: ${absPath}`);
|
|
4976
5044
|
return exitFn(1);
|
|
4977
5045
|
}
|
|
4978
5046
|
let raw;
|
|
4979
5047
|
try {
|
|
4980
|
-
raw =
|
|
5048
|
+
raw = fs25.readFileSync(absPath, "utf8");
|
|
4981
5049
|
} catch (err) {
|
|
4982
5050
|
stderrFn(`[cleargate gate] error: cannot read file: ${absPath}`);
|
|
4983
5051
|
return exitFn(1);
|
|
@@ -4996,8 +5064,8 @@ async function gateCheckHandler(file, opts, cli) {
|
|
|
4996
5064
|
return exitFn(1);
|
|
4997
5065
|
}
|
|
4998
5066
|
const projectRoot = cwd;
|
|
4999
|
-
const gatesDocPath = cli?.gatesDocPath ??
|
|
5000
|
-
if (!
|
|
5067
|
+
const gatesDocPath = cli?.gatesDocPath ?? path25.join(projectRoot, ".cleargate", "knowledge", "readiness-gates.md");
|
|
5068
|
+
if (!fs25.existsSync(gatesDocPath)) {
|
|
5001
5069
|
stderrFn(`[cleargate gate] error: readiness-gates.md not found at: ${gatesDocPath}`);
|
|
5002
5070
|
return exitFn(1);
|
|
5003
5071
|
}
|
|
@@ -5070,8 +5138,8 @@ async function gateExplainHandler(file, cli) {
|
|
|
5070
5138
|
const stderrFn = cli?.stderr ?? ((s) => process.stderr.write(s + "\n"));
|
|
5071
5139
|
const exitFn = cli?.exit ?? ((code) => process.exit(code));
|
|
5072
5140
|
const cwd = cli?.cwd ?? process.cwd();
|
|
5073
|
-
const absPath =
|
|
5074
|
-
if (!
|
|
5141
|
+
const absPath = path25.isAbsolute(file) ? file : path25.resolve(cwd, file);
|
|
5142
|
+
if (!fs25.existsSync(absPath)) {
|
|
5075
5143
|
stderrFn(`[cleargate gate] error: file not found: ${absPath}`);
|
|
5076
5144
|
return exitFn(1);
|
|
5077
5145
|
}
|
|
@@ -5082,7 +5150,7 @@ async function gateExplainHandler(file, cli) {
|
|
|
5082
5150
|
}
|
|
5083
5151
|
let raw;
|
|
5084
5152
|
try {
|
|
5085
|
-
raw =
|
|
5153
|
+
raw = fs25.readFileSync(absPath, "utf8");
|
|
5086
5154
|
} catch {
|
|
5087
5155
|
stderrFn(`[cleargate gate] error: cannot read file: ${absPath}`);
|
|
5088
5156
|
return exitFn(1);
|
|
@@ -5103,7 +5171,7 @@ async function gateExplainHandler(file, cli) {
|
|
|
5103
5171
|
function resolveRunScriptForGate(opts) {
|
|
5104
5172
|
if (opts.runScriptPath) return opts.runScriptPath;
|
|
5105
5173
|
const cwd = opts.cwd ?? process.cwd();
|
|
5106
|
-
return
|
|
5174
|
+
return path25.join(cwd, ".cleargate", "scripts", "run_script.sh");
|
|
5107
5175
|
}
|
|
5108
5176
|
function gateQaHandler(opts, cli) {
|
|
5109
5177
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
@@ -5195,15 +5263,15 @@ function gateRunHandler(name, opts, cli) {
|
|
|
5195
5263
|
}
|
|
5196
5264
|
|
|
5197
5265
|
// src/commands/sprint.ts
|
|
5198
|
-
import * as
|
|
5199
|
-
import * as
|
|
5266
|
+
import * as fs26 from "fs";
|
|
5267
|
+
import * as path26 from "path";
|
|
5200
5268
|
import { spawnSync as spawnSync9 } from "child_process";
|
|
5201
5269
|
import yaml7 from "js-yaml";
|
|
5202
5270
|
var TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["Completed", "Done", "Abandoned", "Closed", "Resolved"]);
|
|
5203
5271
|
function resolveRunScript(opts) {
|
|
5204
5272
|
if (opts.runScriptPath) return opts.runScriptPath;
|
|
5205
5273
|
const cwd = opts.cwd ?? process.cwd();
|
|
5206
|
-
return
|
|
5274
|
+
return path26.join(cwd, ".cleargate", "scripts", "run_script.sh");
|
|
5207
5275
|
}
|
|
5208
5276
|
function defaultExit(code) {
|
|
5209
5277
|
return process.exit(code);
|
|
@@ -5297,8 +5365,8 @@ ${body}`;
|
|
|
5297
5365
|
}
|
|
5298
5366
|
function atomicWriteStr(filePath, content) {
|
|
5299
5367
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
5300
|
-
|
|
5301
|
-
|
|
5368
|
+
fs26.writeFileSync(tmp, content, "utf8");
|
|
5369
|
+
fs26.renameSync(tmp, filePath);
|
|
5302
5370
|
}
|
|
5303
5371
|
function deriveSprintBranchForArchive(sprintId) {
|
|
5304
5372
|
const match = /^SPRINT-(\d+)/.exec(sprintId);
|
|
@@ -5312,7 +5380,7 @@ function stampFile(raw, status, completedAt) {
|
|
|
5312
5380
|
return serializeFileContent(fm, body);
|
|
5313
5381
|
}
|
|
5314
5382
|
function stampSprintClose(sprintPath, now) {
|
|
5315
|
-
const previousContent =
|
|
5383
|
+
const previousContent = fs26.readFileSync(sprintPath, "utf8");
|
|
5316
5384
|
const { fm, body } = parseFileFrontmatter(previousContent);
|
|
5317
5385
|
const currentStatus = typeof fm["status"] === "string" ? fm["status"] : "";
|
|
5318
5386
|
const alreadyTerminal = TERMINAL_STATUSES2.has(currentStatus);
|
|
@@ -5372,14 +5440,14 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5372
5440
|
if (mode === "v1") {
|
|
5373
5441
|
return printInertAndExit(stdoutFn, exitFn);
|
|
5374
5442
|
}
|
|
5375
|
-
const stateFile =
|
|
5376
|
-
if (!
|
|
5443
|
+
const stateFile = path26.join(cwd, ".cleargate", "sprint-runs", opts.sprintId, "state.json");
|
|
5444
|
+
if (!fs26.existsSync(stateFile)) {
|
|
5377
5445
|
stderrFn(`[cleargate sprint archive] state.json not found at ${stateFile}`);
|
|
5378
5446
|
return exitFn(1);
|
|
5379
5447
|
}
|
|
5380
5448
|
let state2;
|
|
5381
5449
|
try {
|
|
5382
|
-
state2 = JSON.parse(
|
|
5450
|
+
state2 = JSON.parse(fs26.readFileSync(stateFile, "utf8"));
|
|
5383
5451
|
} catch (err) {
|
|
5384
5452
|
stderrFn(`[cleargate sprint archive] failed to parse state.json: ${err.message}`);
|
|
5385
5453
|
return exitFn(1);
|
|
@@ -5391,18 +5459,18 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5391
5459
|
return exitFn(1);
|
|
5392
5460
|
}
|
|
5393
5461
|
const stateStories = state2.stories ?? {};
|
|
5394
|
-
const pendingDir =
|
|
5395
|
-
const archiveDir =
|
|
5462
|
+
const pendingDir = path26.join(cwd, ".cleargate", "delivery", "pending-sync");
|
|
5463
|
+
const archiveDir = path26.join(cwd, ".cleargate", "delivery", "archive");
|
|
5396
5464
|
let sprintFile = null;
|
|
5397
|
-
for (const entry of
|
|
5465
|
+
for (const entry of fs26.readdirSync(pendingDir)) {
|
|
5398
5466
|
if ((entry.startsWith(`${opts.sprintId}_`) || entry === `${opts.sprintId}.md`) && entry.endsWith(".md")) {
|
|
5399
|
-
sprintFile =
|
|
5467
|
+
sprintFile = path26.join(pendingDir, entry);
|
|
5400
5468
|
break;
|
|
5401
5469
|
}
|
|
5402
5470
|
}
|
|
5403
5471
|
let epicIds = [];
|
|
5404
|
-
if (sprintFile &&
|
|
5405
|
-
const { fm } = parseFileFrontmatter(
|
|
5472
|
+
if (sprintFile && fs26.existsSync(sprintFile)) {
|
|
5473
|
+
const { fm } = parseFileFrontmatter(fs26.readFileSync(sprintFile, "utf8"));
|
|
5406
5474
|
const epics = fm["epics"];
|
|
5407
5475
|
if (Array.isArray(epics)) {
|
|
5408
5476
|
epicIds = epics.map(String);
|
|
@@ -5412,15 +5480,15 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5412
5480
|
if (sprintFile) {
|
|
5413
5481
|
plan.push({
|
|
5414
5482
|
src: sprintFile,
|
|
5415
|
-
destName:
|
|
5483
|
+
destName: path26.basename(sprintFile),
|
|
5416
5484
|
status: "Completed"
|
|
5417
5485
|
});
|
|
5418
5486
|
}
|
|
5419
5487
|
for (const epicId of epicIds) {
|
|
5420
|
-
for (const entry of
|
|
5488
|
+
for (const entry of fs26.readdirSync(pendingDir)) {
|
|
5421
5489
|
if ((entry.startsWith(`${epicId}_`) || entry === `${epicId}.md`) && entry.endsWith(".md")) {
|
|
5422
5490
|
plan.push({
|
|
5423
|
-
src:
|
|
5491
|
+
src: path26.join(pendingDir, entry),
|
|
5424
5492
|
destName: entry,
|
|
5425
5493
|
status: "Approved"
|
|
5426
5494
|
});
|
|
@@ -5429,10 +5497,10 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5429
5497
|
}
|
|
5430
5498
|
const storyKeys = storyKeysForEpic(stateStories, epicId);
|
|
5431
5499
|
for (const storyId of storyKeys) {
|
|
5432
|
-
for (const entry of
|
|
5500
|
+
for (const entry of fs26.readdirSync(pendingDir)) {
|
|
5433
5501
|
if ((entry.startsWith(`${storyId}_`) || entry === `${storyId}.md`) && entry.endsWith(".md")) {
|
|
5434
5502
|
plan.push({
|
|
5435
|
-
src:
|
|
5503
|
+
src: path26.join(pendingDir, entry),
|
|
5436
5504
|
destName: entry,
|
|
5437
5505
|
status: "Done"
|
|
5438
5506
|
});
|
|
@@ -5444,13 +5512,13 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5444
5512
|
const storyIdsInState = new Set(Object.keys(stateStories));
|
|
5445
5513
|
const planSrcs = new Set(plan.map((p) => p.src));
|
|
5446
5514
|
const orphans = [];
|
|
5447
|
-
for (const entry of
|
|
5515
|
+
for (const entry of fs26.readdirSync(pendingDir)) {
|
|
5448
5516
|
if (!entry.startsWith("STORY-") || !entry.endsWith(".md")) continue;
|
|
5449
|
-
const candidate =
|
|
5517
|
+
const candidate = path26.join(pendingDir, entry);
|
|
5450
5518
|
if (planSrcs.has(candidate)) continue;
|
|
5451
5519
|
let raw;
|
|
5452
5520
|
try {
|
|
5453
|
-
raw =
|
|
5521
|
+
raw = fs26.readFileSync(candidate, "utf8");
|
|
5454
5522
|
} catch {
|
|
5455
5523
|
continue;
|
|
5456
5524
|
}
|
|
@@ -5467,18 +5535,18 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5467
5535
|
}
|
|
5468
5536
|
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5469
5537
|
const sprintBranch = deriveSprintBranchForArchive(opts.sprintId);
|
|
5470
|
-
const activePath =
|
|
5538
|
+
const activePath = path26.join(cwd, ".cleargate", "sprint-runs", ".active");
|
|
5471
5539
|
if (opts.dryRun) {
|
|
5472
5540
|
stdoutFn(`[dry-run] Sprint archive plan for ${opts.sprintId}:`);
|
|
5473
5541
|
stdoutFn(` Sprint branch: ${sprintBranch}`);
|
|
5474
5542
|
stdoutFn(` Files to archive (${plan.length}):`);
|
|
5475
5543
|
for (const entry of plan) {
|
|
5476
5544
|
stdoutFn(
|
|
5477
|
-
` ${
|
|
5545
|
+
` ${path26.basename(entry.src)} \u2192 archive/${entry.destName} [stamp: status=${entry.status}, completed_at=<now>]`
|
|
5478
5546
|
);
|
|
5479
5547
|
}
|
|
5480
5548
|
if (orphans.length > 0) {
|
|
5481
|
-
stdoutFn(` Orphan files (${orphans.length}): ${orphans.map((o) =>
|
|
5549
|
+
stdoutFn(` Orphan files (${orphans.length}): ${orphans.map((o) => path26.basename(o)).join(", ")}`);
|
|
5482
5550
|
}
|
|
5483
5551
|
stdoutFn(` .active \u2192 "" (truncate)`);
|
|
5484
5552
|
stdoutFn(` git checkout main`);
|
|
@@ -5487,9 +5555,9 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5487
5555
|
return exitFn(0);
|
|
5488
5556
|
}
|
|
5489
5557
|
let sprintFileSnapshot = null;
|
|
5490
|
-
const wikiRoot =
|
|
5491
|
-
const wikiInitialised =
|
|
5492
|
-
if (sprintFile &&
|
|
5558
|
+
const wikiRoot = path26.join(cwd, ".cleargate", "wiki");
|
|
5559
|
+
const wikiInitialised = fs26.existsSync(wikiRoot);
|
|
5560
|
+
if (sprintFile && fs26.existsSync(sprintFile)) {
|
|
5493
5561
|
const { previousContent } = stampSprintClose(sprintFile, () => completedAt);
|
|
5494
5562
|
sprintFileSnapshot = previousContent;
|
|
5495
5563
|
if (wikiInitialised) {
|
|
@@ -5511,15 +5579,15 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5511
5579
|
}
|
|
5512
5580
|
}
|
|
5513
5581
|
for (const entry of plan) {
|
|
5514
|
-
if (!
|
|
5582
|
+
if (!fs26.existsSync(entry.src)) {
|
|
5515
5583
|
stderrFn(`[cleargate sprint archive] source not found: ${entry.src} \u2014 skipping`);
|
|
5516
5584
|
continue;
|
|
5517
5585
|
}
|
|
5518
|
-
const raw =
|
|
5586
|
+
const raw = fs26.readFileSync(entry.src, "utf8");
|
|
5519
5587
|
const stamped = stampFile(raw, entry.status, completedAt);
|
|
5520
|
-
const dest =
|
|
5588
|
+
const dest = path26.join(archiveDir, entry.destName);
|
|
5521
5589
|
atomicWriteStr(entry.src, stamped);
|
|
5522
|
-
|
|
5590
|
+
fs26.renameSync(entry.src, dest);
|
|
5523
5591
|
stdoutFn(`archived: ${entry.destName}`);
|
|
5524
5592
|
}
|
|
5525
5593
|
try {
|
|
@@ -5561,8 +5629,8 @@ async function sprintArchiveHandler(opts, cli) {
|
|
|
5561
5629
|
}
|
|
5562
5630
|
|
|
5563
5631
|
// src/commands/story.ts
|
|
5564
|
-
import * as
|
|
5565
|
-
import * as
|
|
5632
|
+
import * as fs27 from "fs";
|
|
5633
|
+
import * as path27 from "path";
|
|
5566
5634
|
import { spawnSync as spawnSync10 } from "child_process";
|
|
5567
5635
|
function defaultExit2(code) {
|
|
5568
5636
|
return process.exit(code);
|
|
@@ -5570,7 +5638,7 @@ function defaultExit2(code) {
|
|
|
5570
5638
|
function resolveRunScript2(opts) {
|
|
5571
5639
|
if (opts.runScriptPath) return opts.runScriptPath;
|
|
5572
5640
|
const cwd = opts.cwd ?? process.cwd();
|
|
5573
|
-
return
|
|
5641
|
+
return path27.join(cwd, ".cleargate", "scripts", "run_script.sh");
|
|
5574
5642
|
}
|
|
5575
5643
|
function deriveSprintBranch(sprintId) {
|
|
5576
5644
|
const match = /^SPRINT-(\d+)/.exec(sprintId);
|
|
@@ -5579,11 +5647,11 @@ function deriveSprintBranch(sprintId) {
|
|
|
5579
5647
|
}
|
|
5580
5648
|
function atomicWriteString(filePath, text) {
|
|
5581
5649
|
const tmpFile = `${filePath}.tmp.${process.pid}`;
|
|
5582
|
-
|
|
5583
|
-
|
|
5650
|
+
fs27.writeFileSync(tmpFile, text, "utf8");
|
|
5651
|
+
fs27.renameSync(tmpFile, filePath);
|
|
5584
5652
|
}
|
|
5585
5653
|
function stateJsonPath(cwd, sprintId) {
|
|
5586
|
-
return
|
|
5654
|
+
return path27.join(cwd, ".cleargate", "sprint-runs", sprintId, "state.json");
|
|
5587
5655
|
}
|
|
5588
5656
|
function storyStartHandler(opts, cli) {
|
|
5589
5657
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
@@ -5600,7 +5668,7 @@ function storyStartHandler(opts, cli) {
|
|
|
5600
5668
|
return printInertAndExit(stdoutFn, exitFn);
|
|
5601
5669
|
}
|
|
5602
5670
|
const sprintBranch = deriveSprintBranch(sprintId);
|
|
5603
|
-
const worktreePath =
|
|
5671
|
+
const worktreePath = path27.join(cwd, ".worktrees", opts.storyId);
|
|
5604
5672
|
const storyBranch = `story/${opts.storyId}`;
|
|
5605
5673
|
const step1 = spawnFn(
|
|
5606
5674
|
"git",
|
|
@@ -5632,13 +5700,13 @@ function storyStartHandler(opts, cli) {
|
|
|
5632
5700
|
return exitFn(step2.status ?? 1);
|
|
5633
5701
|
}
|
|
5634
5702
|
const stateFile = stateJsonPath(cwd, sprintId);
|
|
5635
|
-
if (!
|
|
5703
|
+
if (!fs27.existsSync(stateFile)) {
|
|
5636
5704
|
stderrFn(`[cleargate story start] step 3: state.json not found at ${stateFile}`);
|
|
5637
5705
|
return exitFn(1);
|
|
5638
5706
|
}
|
|
5639
5707
|
let state2;
|
|
5640
5708
|
try {
|
|
5641
|
-
state2 = JSON.parse(
|
|
5709
|
+
state2 = JSON.parse(fs27.readFileSync(stateFile, "utf8"));
|
|
5642
5710
|
} catch (err) {
|
|
5643
5711
|
stderrFn(`[cleargate story start] step 3: failed to parse state.json: ${err.message}`);
|
|
5644
5712
|
return exitFn(1);
|
|
@@ -5679,7 +5747,7 @@ function storyCompleteHandler(opts, cli) {
|
|
|
5679
5747
|
}
|
|
5680
5748
|
const sprintBranch = deriveSprintBranch(sprintId);
|
|
5681
5749
|
const storyBranch = `story/${opts.storyId}`;
|
|
5682
|
-
const worktreeRel =
|
|
5750
|
+
const worktreeRel = path27.join(".worktrees", opts.storyId);
|
|
5683
5751
|
const step1 = spawnFn(
|
|
5684
5752
|
"git",
|
|
5685
5753
|
["rev-list", "--count", `${sprintBranch}..${storyBranch}`],
|
|
@@ -5776,7 +5844,7 @@ function storyCompleteHandler(opts, cli) {
|
|
|
5776
5844
|
}
|
|
5777
5845
|
|
|
5778
5846
|
// src/commands/state.ts
|
|
5779
|
-
import * as
|
|
5847
|
+
import * as path28 from "path";
|
|
5780
5848
|
import { spawnSync as spawnSync11 } from "child_process";
|
|
5781
5849
|
function defaultExit3(code) {
|
|
5782
5850
|
return process.exit(code);
|
|
@@ -5784,7 +5852,7 @@ function defaultExit3(code) {
|
|
|
5784
5852
|
function resolveRunScript3(opts) {
|
|
5785
5853
|
if (opts.runScriptPath) return opts.runScriptPath;
|
|
5786
5854
|
const cwd = opts.cwd ?? process.cwd();
|
|
5787
|
-
return
|
|
5855
|
+
return path28.join(cwd, ".cleargate", "scripts", "run_script.sh");
|
|
5788
5856
|
}
|
|
5789
5857
|
function stateUpdateHandler(opts, cli) {
|
|
5790
5858
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
@@ -5840,20 +5908,20 @@ function stateValidateHandler(opts, cli) {
|
|
|
5840
5908
|
}
|
|
5841
5909
|
|
|
5842
5910
|
// src/commands/stamp-tokens.ts
|
|
5843
|
-
import * as
|
|
5844
|
-
import * as
|
|
5911
|
+
import * as fs29 from "fs";
|
|
5912
|
+
import * as path30 from "path";
|
|
5845
5913
|
|
|
5846
5914
|
// src/lib/ledger-reader.ts
|
|
5847
|
-
import * as
|
|
5848
|
-
import * as
|
|
5915
|
+
import * as fs28 from "fs";
|
|
5916
|
+
import * as path29 from "path";
|
|
5849
5917
|
function findSprintRunsRoot(startDir) {
|
|
5850
5918
|
let dir = startDir;
|
|
5851
5919
|
while (true) {
|
|
5852
|
-
const candidate =
|
|
5853
|
-
if (
|
|
5920
|
+
const candidate = path29.join(dir, ".cleargate", "sprint-runs");
|
|
5921
|
+
if (fs28.existsSync(candidate)) {
|
|
5854
5922
|
return candidate;
|
|
5855
5923
|
}
|
|
5856
|
-
const parent =
|
|
5924
|
+
const parent = path29.dirname(dir);
|
|
5857
5925
|
if (parent === dir) {
|
|
5858
5926
|
return null;
|
|
5859
5927
|
}
|
|
@@ -5906,13 +5974,13 @@ function readLedgerForWorkItem(workItemId, opts = {}) {
|
|
|
5906
5974
|
}
|
|
5907
5975
|
sprintRunsRoot = found;
|
|
5908
5976
|
}
|
|
5909
|
-
if (!
|
|
5977
|
+
if (!fs28.existsSync(sprintRunsRoot)) {
|
|
5910
5978
|
return [];
|
|
5911
5979
|
}
|
|
5912
5980
|
let ledgerFiles;
|
|
5913
5981
|
try {
|
|
5914
|
-
const entries =
|
|
5915
|
-
ledgerFiles = entries.filter((e) => e.isDirectory()).map((e) =>
|
|
5982
|
+
const entries = fs28.readdirSync(sprintRunsRoot, { withFileTypes: true });
|
|
5983
|
+
ledgerFiles = entries.filter((e) => e.isDirectory()).map((e) => path29.join(sprintRunsRoot, e.name, "token-ledger.jsonl")).filter((f) => fs28.existsSync(f));
|
|
5916
5984
|
} catch {
|
|
5917
5985
|
return [];
|
|
5918
5986
|
}
|
|
@@ -5920,7 +5988,7 @@ function readLedgerForWorkItem(workItemId, opts = {}) {
|
|
|
5920
5988
|
for (const ledgerFile of ledgerFiles) {
|
|
5921
5989
|
let content;
|
|
5922
5990
|
try {
|
|
5923
|
-
content =
|
|
5991
|
+
content = fs28.readFileSync(ledgerFile, "utf-8");
|
|
5924
5992
|
} catch {
|
|
5925
5993
|
continue;
|
|
5926
5994
|
}
|
|
@@ -5971,7 +6039,7 @@ async function stampTokensHandler(file, opts, cli) {
|
|
|
5971
6039
|
});
|
|
5972
6040
|
const nowFn = cli?.now ?? (() => /* @__PURE__ */ new Date());
|
|
5973
6041
|
const cwd = cli?.cwd ?? process.cwd();
|
|
5974
|
-
const absPath =
|
|
6042
|
+
const absPath = path30.isAbsolute(file) ? file : path30.resolve(cwd, file);
|
|
5975
6043
|
if (/\/\.cleargate\/delivery\/archive\//.test(absPath)) {
|
|
5976
6044
|
stdoutFn(`[frozen] ${absPath}`);
|
|
5977
6045
|
exitFn(0);
|
|
@@ -5979,7 +6047,7 @@ async function stampTokensHandler(file, opts, cli) {
|
|
|
5979
6047
|
}
|
|
5980
6048
|
let rawContent;
|
|
5981
6049
|
try {
|
|
5982
|
-
rawContent =
|
|
6050
|
+
rawContent = fs29.readFileSync(absPath, "utf-8");
|
|
5983
6051
|
} catch {
|
|
5984
6052
|
stdoutFn(`[stamp-tokens] error: cannot read file: ${absPath}`);
|
|
5985
6053
|
exitFn(1);
|
|
@@ -6051,7 +6119,7 @@ async function stampTokensHandler(file, opts, cli) {
|
|
|
6051
6119
|
return;
|
|
6052
6120
|
}
|
|
6053
6121
|
try {
|
|
6054
|
-
|
|
6122
|
+
fs29.writeFileSync(absPath, serialized, "utf-8");
|
|
6055
6123
|
} catch {
|
|
6056
6124
|
stdoutFn(`[stamp-tokens] error: cannot write file: ${absPath}`);
|
|
6057
6125
|
exitFn(1);
|
|
@@ -6068,7 +6136,7 @@ function extractWorkItemId(fm, absPath) {
|
|
|
6068
6136
|
return val.trim();
|
|
6069
6137
|
}
|
|
6070
6138
|
}
|
|
6071
|
-
const basename12 =
|
|
6139
|
+
const basename12 = path30.basename(absPath);
|
|
6072
6140
|
const match = basename12.match(/^(STORY|EPIC|PROPOSAL|CR|BUG)-\d+(-\d+)?/i);
|
|
6073
6141
|
if (match) {
|
|
6074
6142
|
return match[0].toUpperCase();
|
|
@@ -6173,7 +6241,7 @@ ${body}`;
|
|
|
6173
6241
|
|
|
6174
6242
|
// src/commands/upgrade.ts
|
|
6175
6243
|
import * as fsp from "fs/promises";
|
|
6176
|
-
import * as
|
|
6244
|
+
import * as path31 from "path";
|
|
6177
6245
|
|
|
6178
6246
|
// src/lib/claude-md-surgery.ts
|
|
6179
6247
|
var CLEARGATE_START = "<!-- CLEARGATE:START -->";
|
|
@@ -6319,7 +6387,7 @@ async function writeAtomic2(filePath, content) {
|
|
|
6319
6387
|
await fsp.rename(tmpPath, filePath);
|
|
6320
6388
|
}
|
|
6321
6389
|
async function updateSnapshotEntry(projectRoot, filePath, newSha) {
|
|
6322
|
-
const snapshotPath =
|
|
6390
|
+
const snapshotPath = path31.join(projectRoot, ".cleargate", ".install-manifest.json");
|
|
6323
6391
|
let snapshot;
|
|
6324
6392
|
try {
|
|
6325
6393
|
const raw = await fsp.readFile(snapshotPath, "utf-8");
|
|
@@ -6336,17 +6404,17 @@ async function updateSnapshotEntry(projectRoot, filePath, newSha) {
|
|
|
6336
6404
|
await writeAtomic2(snapshotPath, JSON.stringify(updated, null, 2) + "\n");
|
|
6337
6405
|
}
|
|
6338
6406
|
function isClaudeMd(filePath) {
|
|
6339
|
-
return
|
|
6407
|
+
return path31.basename(filePath) === "CLAUDE.md";
|
|
6340
6408
|
}
|
|
6341
6409
|
function isSettingsJson(filePath) {
|
|
6342
|
-
return
|
|
6410
|
+
return path31.basename(filePath) === "settings.json" && filePath.includes(".claude");
|
|
6343
6411
|
}
|
|
6344
6412
|
async function applyAlwaysOverwrite(entry, projectRoot, packageRoot, stdout) {
|
|
6345
|
-
const targetPath =
|
|
6346
|
-
const sourcePath =
|
|
6413
|
+
const targetPath = path31.join(projectRoot, entry.path);
|
|
6414
|
+
const sourcePath = path31.join(packageRoot, entry.path);
|
|
6347
6415
|
try {
|
|
6348
6416
|
const pkgContent = await fsp.readFile(sourcePath, "utf-8");
|
|
6349
|
-
await fsp.mkdir(
|
|
6417
|
+
await fsp.mkdir(path31.dirname(targetPath), { recursive: true });
|
|
6350
6418
|
await writeAtomic2(targetPath, pkgContent);
|
|
6351
6419
|
await updateSnapshotEntry(projectRoot, entry.path, entry.sha256);
|
|
6352
6420
|
stdout(`[always] overwritten: ${entry.path}`);
|
|
@@ -6356,8 +6424,8 @@ async function applyAlwaysOverwrite(entry, projectRoot, packageRoot, stdout) {
|
|
|
6356
6424
|
}
|
|
6357
6425
|
async function applyMerge3Way(entry, projectRoot, packageRoot, installSha, currentSha, flags, opts) {
|
|
6358
6426
|
const { stdout, stderr, promptMergeChoiceFn, openInEditorFn, stdin } = opts;
|
|
6359
|
-
const targetPath =
|
|
6360
|
-
const sourcePath =
|
|
6427
|
+
const targetPath = path31.join(projectRoot, entry.path);
|
|
6428
|
+
const sourcePath = path31.join(packageRoot, entry.path);
|
|
6361
6429
|
let ours = "";
|
|
6362
6430
|
let theirs = "";
|
|
6363
6431
|
try {
|
|
@@ -6420,7 +6488,7 @@ async function applyMerge3Way(entry, projectRoot, packageRoot, installSha, curre
|
|
|
6420
6488
|
mergedContent = theirs;
|
|
6421
6489
|
}
|
|
6422
6490
|
}
|
|
6423
|
-
await fsp.mkdir(
|
|
6491
|
+
await fsp.mkdir(path31.dirname(targetPath), { recursive: true });
|
|
6424
6492
|
await writeAtomic2(targetPath, mergedContent);
|
|
6425
6493
|
const newSha2 = hashNormalized(mergedContent);
|
|
6426
6494
|
await updateSnapshotEntry(projectRoot, entry.path, newSha2);
|
|
@@ -6432,7 +6500,7 @@ async function applyMerge3Way(entry, projectRoot, packageRoot, installSha, curre
|
|
|
6432
6500
|
${ours}=======
|
|
6433
6501
|
${theirs}>>>>>>> theirs (upstream)
|
|
6434
6502
|
`;
|
|
6435
|
-
await fsp.mkdir(
|
|
6503
|
+
await fsp.mkdir(path31.dirname(mergeFilePath), { recursive: true });
|
|
6436
6504
|
await writeAtomic2(mergeFilePath, conflictContent);
|
|
6437
6505
|
try {
|
|
6438
6506
|
const result = await openInEditorFn(mergeFilePath);
|
|
@@ -6566,9 +6634,9 @@ async function upgradeHandler(flags, cli) {
|
|
|
6566
6634
|
}
|
|
6567
6635
|
|
|
6568
6636
|
// src/commands/uninstall.ts
|
|
6569
|
-
import * as
|
|
6637
|
+
import * as fs30 from "fs";
|
|
6570
6638
|
import * as fsp2 from "fs/promises";
|
|
6571
|
-
import * as
|
|
6639
|
+
import * as path32 from "path";
|
|
6572
6640
|
import { execSync as execSync2 } from "child_process";
|
|
6573
6641
|
var USER_ARTIFACT_TIERS = ["user-artifact"];
|
|
6574
6642
|
var FRAMEWORK_TIERS = ["protocol", "template", "agent", "hook", "skill", "cli-config", "derived"];
|
|
@@ -6594,10 +6662,10 @@ function shouldPreserve(entry, preserveSet, removeSet) {
|
|
|
6594
6662
|
return false;
|
|
6595
6663
|
}
|
|
6596
6664
|
function resolveProjectName(target) {
|
|
6597
|
-
const pkgPath =
|
|
6598
|
-
if (
|
|
6665
|
+
const pkgPath = path32.join(target, "package.json");
|
|
6666
|
+
if (fs30.existsSync(pkgPath)) {
|
|
6599
6667
|
try {
|
|
6600
|
-
const raw =
|
|
6668
|
+
const raw = fs30.readFileSync(pkgPath, "utf-8");
|
|
6601
6669
|
const parsed = JSON.parse(raw);
|
|
6602
6670
|
if (parsed.name && typeof parsed.name === "string") {
|
|
6603
6671
|
return parsed.name;
|
|
@@ -6605,7 +6673,7 @@ function resolveProjectName(target) {
|
|
|
6605
6673
|
} catch {
|
|
6606
6674
|
}
|
|
6607
6675
|
}
|
|
6608
|
-
return
|
|
6676
|
+
return path32.basename(target);
|
|
6609
6677
|
}
|
|
6610
6678
|
function detectUncommittedChanges(target, manifestPaths, gitRunner) {
|
|
6611
6679
|
const run = gitRunner ?? ((args) => {
|
|
@@ -6634,8 +6702,8 @@ function detectUncommittedChanges(target, manifestPaths, gitRunner) {
|
|
|
6634
6702
|
return changedFiles.filter((f) => manifestSet.has(f));
|
|
6635
6703
|
}
|
|
6636
6704
|
async function removeFromPackageJson(target, dryRun) {
|
|
6637
|
-
const pkgPath =
|
|
6638
|
-
if (!
|
|
6705
|
+
const pkgPath = path32.join(target, "package.json");
|
|
6706
|
+
if (!fs30.existsSync(pkgPath)) return false;
|
|
6639
6707
|
let raw;
|
|
6640
6708
|
try {
|
|
6641
6709
|
raw = await fsp2.readFile(pkgPath, "utf-8");
|
|
@@ -6676,7 +6744,7 @@ async function removeFile(filePath) {
|
|
|
6676
6744
|
}
|
|
6677
6745
|
async function removeDir(dirPath) {
|
|
6678
6746
|
try {
|
|
6679
|
-
|
|
6747
|
+
fs30.rmSync(dirPath, { recursive: true, force: true });
|
|
6680
6748
|
} catch {
|
|
6681
6749
|
}
|
|
6682
6750
|
}
|
|
@@ -6696,12 +6764,12 @@ async function uninstallHandler(opts) {
|
|
|
6696
6764
|
for (const t of FRAMEWORK_TIERS) removeSet.add(t);
|
|
6697
6765
|
for (const u of USER_ARTIFACT_TIERS) removeSet.add(u);
|
|
6698
6766
|
}
|
|
6699
|
-
const target = opts.path ?
|
|
6700
|
-
const cleargateDir =
|
|
6701
|
-
const manifestPath =
|
|
6702
|
-
const uninstalledPath =
|
|
6703
|
-
if (!
|
|
6704
|
-
if (
|
|
6767
|
+
const target = opts.path ? path32.resolve(opts.path) : cwd;
|
|
6768
|
+
const cleargateDir = path32.join(target, ".cleargate");
|
|
6769
|
+
const manifestPath = path32.join(cleargateDir, ".install-manifest.json");
|
|
6770
|
+
const uninstalledPath = path32.join(cleargateDir, ".uninstalled");
|
|
6771
|
+
if (!fs30.existsSync(manifestPath)) {
|
|
6772
|
+
if (fs30.existsSync(uninstalledPath)) {
|
|
6705
6773
|
stdout("already uninstalled");
|
|
6706
6774
|
exit(0);
|
|
6707
6775
|
return;
|
|
@@ -6710,7 +6778,7 @@ async function uninstallHandler(opts) {
|
|
|
6710
6778
|
exit(0);
|
|
6711
6779
|
return;
|
|
6712
6780
|
}
|
|
6713
|
-
if (
|
|
6781
|
+
if (fs30.existsSync(uninstalledPath) && !fs30.existsSync(manifestPath)) {
|
|
6714
6782
|
stdout("already uninstalled");
|
|
6715
6783
|
exit(0);
|
|
6716
6784
|
return;
|
|
@@ -6732,10 +6800,10 @@ async function uninstallHandler(opts) {
|
|
|
6732
6800
|
return;
|
|
6733
6801
|
}
|
|
6734
6802
|
}
|
|
6735
|
-
const claudeMdPath =
|
|
6803
|
+
const claudeMdPath = path32.join(target, "CLAUDE.md");
|
|
6736
6804
|
let claudeMdContent = null;
|
|
6737
|
-
if (
|
|
6738
|
-
claudeMdContent =
|
|
6805
|
+
if (fs30.existsSync(claudeMdPath)) {
|
|
6806
|
+
claudeMdContent = fs30.readFileSync(claudeMdPath, "utf-8");
|
|
6739
6807
|
if (!claudeMdContent.includes(CLEARGATE_START)) {
|
|
6740
6808
|
stderr("CLAUDE.md is missing <!-- CLEARGATE:START --> marker");
|
|
6741
6809
|
exit(1);
|
|
@@ -6751,8 +6819,8 @@ async function uninstallHandler(opts) {
|
|
|
6751
6819
|
const toPreserve = [];
|
|
6752
6820
|
const toSkip = [];
|
|
6753
6821
|
for (const entry of snapshot.files) {
|
|
6754
|
-
const filePath =
|
|
6755
|
-
if (!
|
|
6822
|
+
const filePath = path32.join(target, entry.path);
|
|
6823
|
+
if (!fs30.existsSync(filePath)) {
|
|
6756
6824
|
toSkip.push(entry);
|
|
6757
6825
|
continue;
|
|
6758
6826
|
}
|
|
@@ -6809,7 +6877,7 @@ async function uninstallHandler(opts) {
|
|
|
6809
6877
|
const removedPaths = [];
|
|
6810
6878
|
const preservedPaths = [];
|
|
6811
6879
|
for (const entry of toRemove) {
|
|
6812
|
-
const filePath =
|
|
6880
|
+
const filePath = path32.join(target, entry.path);
|
|
6813
6881
|
await removeFile(filePath);
|
|
6814
6882
|
removedPaths.push(entry.path);
|
|
6815
6883
|
}
|
|
@@ -6825,10 +6893,10 @@ async function uninstallHandler(opts) {
|
|
|
6825
6893
|
stderr(`Warning: could not strip CLAUDE.md block: ${err.message}`);
|
|
6826
6894
|
}
|
|
6827
6895
|
}
|
|
6828
|
-
const settingsPath =
|
|
6829
|
-
if (
|
|
6896
|
+
const settingsPath = path32.join(target, ".claude", "settings.json");
|
|
6897
|
+
if (fs30.existsSync(settingsPath)) {
|
|
6830
6898
|
try {
|
|
6831
|
-
const raw =
|
|
6899
|
+
const raw = fs30.readFileSync(settingsPath, "utf-8");
|
|
6832
6900
|
const settings = JSON.parse(raw);
|
|
6833
6901
|
const cleaned = removeClearGateHooks(settings);
|
|
6834
6902
|
await writeAtomic3(settingsPath, JSON.stringify(cleaned, null, 2) + "\n");
|
|
@@ -6843,7 +6911,7 @@ async function uninstallHandler(opts) {
|
|
|
6843
6911
|
stdout("Removed @cleargate/cli from package.json. Run `npm install` to update package-lock.json.");
|
|
6844
6912
|
}
|
|
6845
6913
|
await removeFile(manifestPath);
|
|
6846
|
-
await removeFile(
|
|
6914
|
+
await removeFile(path32.join(cleargateDir, ".drift-state.json"));
|
|
6847
6915
|
const marker = {
|
|
6848
6916
|
uninstalled_at: now().toISOString(),
|
|
6849
6917
|
prior_version: snapshot.cleargate_version,
|
|
@@ -6867,29 +6935,29 @@ async function uninstallHandler(opts) {
|
|
|
6867
6935
|
|
|
6868
6936
|
// src/commands/sync.ts
|
|
6869
6937
|
import * as fsPromises8 from "fs/promises";
|
|
6870
|
-
import * as
|
|
6938
|
+
import * as path40 from "path";
|
|
6871
6939
|
|
|
6872
6940
|
// src/lib/sync-log.ts
|
|
6873
|
-
import * as
|
|
6941
|
+
import * as fs31 from "fs";
|
|
6874
6942
|
import * as fsPromises2 from "fs/promises";
|
|
6875
|
-
import * as
|
|
6943
|
+
import * as path33 from "path";
|
|
6876
6944
|
function resolveActiveSprintDir(projectRoot, _opts) {
|
|
6877
|
-
const sprintRunsRoot =
|
|
6878
|
-
const offSprint =
|
|
6879
|
-
if (!
|
|
6880
|
-
|
|
6881
|
-
|
|
6945
|
+
const sprintRunsRoot = path33.join(projectRoot, ".cleargate", "sprint-runs");
|
|
6946
|
+
const offSprint = path33.join(sprintRunsRoot, "_off-sprint");
|
|
6947
|
+
if (!fs31.existsSync(sprintRunsRoot)) {
|
|
6948
|
+
fs31.mkdirSync(sprintRunsRoot, { recursive: true });
|
|
6949
|
+
fs31.mkdirSync(offSprint, { recursive: true });
|
|
6882
6950
|
return offSprint;
|
|
6883
6951
|
}
|
|
6884
|
-
const entries =
|
|
6952
|
+
const entries = fs31.readdirSync(sprintRunsRoot, { withFileTypes: true });
|
|
6885
6953
|
const sprintDirs = entries.filter((e) => e.isDirectory() && e.name !== "_off-sprint").map((e) => {
|
|
6886
|
-
const fullPath =
|
|
6887
|
-
const stat =
|
|
6954
|
+
const fullPath = path33.join(sprintRunsRoot, e.name);
|
|
6955
|
+
const stat = fs31.statSync(fullPath);
|
|
6888
6956
|
return { name: e.name, fullPath, mtimeMs: stat.mtimeMs };
|
|
6889
6957
|
}).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
6890
6958
|
if (sprintDirs.length === 0) {
|
|
6891
|
-
if (!
|
|
6892
|
-
|
|
6959
|
+
if (!fs31.existsSync(offSprint)) {
|
|
6960
|
+
fs31.mkdirSync(offSprint, { recursive: true });
|
|
6893
6961
|
}
|
|
6894
6962
|
return offSprint;
|
|
6895
6963
|
}
|
|
@@ -6900,7 +6968,7 @@ function redactDetail(detail) {
|
|
|
6900
6968
|
return detail.replace(/eyJ[A-Za-z0-9._-]+/g, "[REDACTED]");
|
|
6901
6969
|
}
|
|
6902
6970
|
async function appendSyncLog(sprintRoot, entry) {
|
|
6903
|
-
const logPath =
|
|
6971
|
+
const logPath = path33.join(sprintRoot, "sync-log.jsonl");
|
|
6904
6972
|
await fsPromises2.mkdir(sprintRoot, { recursive: true });
|
|
6905
6973
|
const safeEntry = {
|
|
6906
6974
|
...entry,
|
|
@@ -6910,7 +6978,7 @@ async function appendSyncLog(sprintRoot, entry) {
|
|
|
6910
6978
|
await fsPromises2.appendFile(logPath, line, { encoding: "utf8" });
|
|
6911
6979
|
}
|
|
6912
6980
|
async function readSyncLog(sprintRoot, filters) {
|
|
6913
|
-
const logPath =
|
|
6981
|
+
const logPath = path33.join(sprintRoot, "sync-log.jsonl");
|
|
6914
6982
|
let raw;
|
|
6915
6983
|
try {
|
|
6916
6984
|
raw = await fsPromises2.readFile(logPath, "utf8");
|
|
@@ -7012,9 +7080,9 @@ function classify2(local, remote, since) {
|
|
|
7012
7080
|
}
|
|
7013
7081
|
|
|
7014
7082
|
// src/lib/merge-helper.ts
|
|
7015
|
-
import { promises as
|
|
7083
|
+
import { promises as fs32 } from "fs";
|
|
7016
7084
|
import * as os4 from "os";
|
|
7017
|
-
import * as
|
|
7085
|
+
import * as path34 from "path";
|
|
7018
7086
|
function promptFourChoice(opts) {
|
|
7019
7087
|
const { stdin, stdout } = opts;
|
|
7020
7088
|
stdout("[k]eep mine / [t]ake theirs / [e]dit in $EDITOR / [a]bort: ");
|
|
@@ -7084,7 +7152,7 @@ async function promptThreeWayMerge(opts) {
|
|
|
7084
7152
|
case "a":
|
|
7085
7153
|
return { resolution: "aborted", body: local };
|
|
7086
7154
|
case "e": {
|
|
7087
|
-
const tmpFile =
|
|
7155
|
+
const tmpFile = path34.join(os4.tmpdir(), `cleargate-merge-${itemId}-${now()}.md`);
|
|
7088
7156
|
const markerContent = `<<<<<<< local
|
|
7089
7157
|
${local}
|
|
7090
7158
|
=======
|
|
@@ -7092,16 +7160,16 @@ ${remote}
|
|
|
7092
7160
|
>>>>>>> remote
|
|
7093
7161
|
`;
|
|
7094
7162
|
try {
|
|
7095
|
-
await
|
|
7163
|
+
await fs32.writeFile(tmpFile, markerContent, "utf-8");
|
|
7096
7164
|
await openInEditor(tmpFile, { editor: editor ?? process.env["EDITOR"] ?? "vi" });
|
|
7097
|
-
const edited = await
|
|
7165
|
+
const edited = await fs32.readFile(tmpFile, "utf-8");
|
|
7098
7166
|
if (containsConflictMarkers(edited)) {
|
|
7099
7167
|
stdout("File still contains conflict markers \u2014 please resolve all conflicts.\n");
|
|
7100
7168
|
continue;
|
|
7101
7169
|
}
|
|
7102
7170
|
return { resolution: "edited", body: edited };
|
|
7103
7171
|
} finally {
|
|
7104
|
-
await
|
|
7172
|
+
await fs32.unlink(tmpFile).catch(() => {
|
|
7105
7173
|
});
|
|
7106
7174
|
}
|
|
7107
7175
|
}
|
|
@@ -7178,11 +7246,11 @@ function createMcpClient(opts) {
|
|
|
7178
7246
|
|
|
7179
7247
|
// src/lib/intake.ts
|
|
7180
7248
|
import * as fsPromises4 from "fs/promises";
|
|
7181
|
-
import * as
|
|
7249
|
+
import * as path36 from "path";
|
|
7182
7250
|
|
|
7183
7251
|
// src/lib/slug.ts
|
|
7184
7252
|
import * as fsPromises3 from "fs/promises";
|
|
7185
|
-
import * as
|
|
7253
|
+
import * as path35 from "path";
|
|
7186
7254
|
function slugify(title, max = 40) {
|
|
7187
7255
|
const normalized = title.normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
7188
7256
|
const lowered = normalized.toLowerCase();
|
|
@@ -7197,8 +7265,8 @@ function slugify(title, max = 40) {
|
|
|
7197
7265
|
var PROPOSAL_ID_RE = /^proposal_id:\s*"?PROP-(\d+)"?/m;
|
|
7198
7266
|
async function nextProposalId(projectRoot) {
|
|
7199
7267
|
const dirs = [
|
|
7200
|
-
|
|
7201
|
-
|
|
7268
|
+
path35.join(projectRoot, ".cleargate", "delivery", "pending-sync"),
|
|
7269
|
+
path35.join(projectRoot, ".cleargate", "delivery", "archive")
|
|
7202
7270
|
];
|
|
7203
7271
|
let maxN = 0;
|
|
7204
7272
|
for (const dir of dirs) {
|
|
@@ -7210,7 +7278,7 @@ async function nextProposalId(projectRoot) {
|
|
|
7210
7278
|
}
|
|
7211
7279
|
for (const entry of entries) {
|
|
7212
7280
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
7213
|
-
const fullPath =
|
|
7281
|
+
const fullPath = path35.join(dir, entry.name);
|
|
7214
7282
|
try {
|
|
7215
7283
|
const raw = await fsPromises3.readFile(fullPath, "utf8");
|
|
7216
7284
|
const fmEnd = extractFrontmatterBlock(raw);
|
|
@@ -7228,8 +7296,8 @@ async function nextProposalId(projectRoot) {
|
|
|
7228
7296
|
}
|
|
7229
7297
|
async function findByRemoteId(projectRoot, remoteId) {
|
|
7230
7298
|
const dirs = [
|
|
7231
|
-
|
|
7232
|
-
|
|
7299
|
+
path35.join(projectRoot, ".cleargate", "delivery", "pending-sync"),
|
|
7300
|
+
path35.join(projectRoot, ".cleargate", "delivery", "archive")
|
|
7233
7301
|
];
|
|
7234
7302
|
const escaped = remoteId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7235
7303
|
const re = new RegExp(`^remote_id:\\s*"?${escaped}"?\\s*$`, "m");
|
|
@@ -7242,7 +7310,7 @@ async function findByRemoteId(projectRoot, remoteId) {
|
|
|
7242
7310
|
}
|
|
7243
7311
|
for (const entry of entries) {
|
|
7244
7312
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
7245
|
-
const fullPath =
|
|
7313
|
+
const fullPath = path35.join(dir, entry.name);
|
|
7246
7314
|
try {
|
|
7247
7315
|
const raw = await fsPromises3.readFile(fullPath, "utf8");
|
|
7248
7316
|
const fm = extractFrontmatterBlock(raw);
|
|
@@ -7271,7 +7339,7 @@ function extractFrontmatterBlock(raw) {
|
|
|
7271
7339
|
// src/lib/intake.ts
|
|
7272
7340
|
async function runIntakeBranch(opts) {
|
|
7273
7341
|
const {
|
|
7274
|
-
mcp,
|
|
7342
|
+
mcp: mcp2,
|
|
7275
7343
|
identity,
|
|
7276
7344
|
sprintRoot,
|
|
7277
7345
|
projectRoot,
|
|
@@ -7279,10 +7347,10 @@ async function runIntakeBranch(opts) {
|
|
|
7279
7347
|
labelFilter = "cleargate:proposal",
|
|
7280
7348
|
now = () => (/* @__PURE__ */ new Date()).toISOString()
|
|
7281
7349
|
} = opts;
|
|
7282
|
-
const pendingSyncDir =
|
|
7350
|
+
const pendingSyncDir = path36.join(projectRoot, ".cleargate", "delivery", "pending-sync");
|
|
7283
7351
|
let remoteItems = [];
|
|
7284
7352
|
try {
|
|
7285
|
-
remoteItems = await
|
|
7353
|
+
remoteItems = await mcp2.call(
|
|
7286
7354
|
"cleargate_detect_new_items",
|
|
7287
7355
|
{ label: labelFilter }
|
|
7288
7356
|
);
|
|
@@ -7310,7 +7378,7 @@ async function runIntakeBranch(opts) {
|
|
|
7310
7378
|
const slug2 = slugify(item.title ?? "untitled");
|
|
7311
7379
|
const num2 = proposalId2.replace("PROP-", "");
|
|
7312
7380
|
const filename2 = `PROPOSAL-${num2}-remote-${slug2}.md`;
|
|
7313
|
-
const targetPath2 =
|
|
7381
|
+
const targetPath2 = path36.join(pendingSyncDir, filename2);
|
|
7314
7382
|
createdItems.push({
|
|
7315
7383
|
proposalId: proposalId2,
|
|
7316
7384
|
remoteId: item.remote_id,
|
|
@@ -7323,7 +7391,7 @@ async function runIntakeBranch(opts) {
|
|
|
7323
7391
|
const num = proposalId.replace("PROP-", "");
|
|
7324
7392
|
const slug = slugify(item.title ?? "untitled");
|
|
7325
7393
|
const filename = `PROPOSAL-${num}-remote-${slug}.md`;
|
|
7326
|
-
const targetPath =
|
|
7394
|
+
const targetPath = path36.join(pendingSyncDir, filename);
|
|
7327
7395
|
const nowTs = now();
|
|
7328
7396
|
const fm = {
|
|
7329
7397
|
proposal_id: proposalId,
|
|
@@ -7415,8 +7483,8 @@ path/to/new/file.ext - {Explanation of purpose}
|
|
|
7415
7483
|
}
|
|
7416
7484
|
async function hasAnyRemoteAuthored(projectRoot) {
|
|
7417
7485
|
const dirs = [
|
|
7418
|
-
|
|
7419
|
-
|
|
7486
|
+
path36.join(projectRoot, ".cleargate", "delivery", "pending-sync"),
|
|
7487
|
+
path36.join(projectRoot, ".cleargate", "delivery", "archive")
|
|
7420
7488
|
];
|
|
7421
7489
|
for (const dir of dirs) {
|
|
7422
7490
|
let entries;
|
|
@@ -7427,7 +7495,7 @@ async function hasAnyRemoteAuthored(projectRoot) {
|
|
|
7427
7495
|
}
|
|
7428
7496
|
for (const entry of entries) {
|
|
7429
7497
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
7430
|
-
const fullPath =
|
|
7498
|
+
const fullPath = path36.join(dir, entry.name);
|
|
7431
7499
|
try {
|
|
7432
7500
|
const raw = await fsPromises4.readFile(fullPath, "utf8");
|
|
7433
7501
|
const fmEnd = raw.indexOf("\n---", 4);
|
|
@@ -7444,9 +7512,9 @@ async function hasAnyRemoteAuthored(projectRoot) {
|
|
|
7444
7512
|
}
|
|
7445
7513
|
|
|
7446
7514
|
// src/lib/active-criteria.ts
|
|
7447
|
-
import * as
|
|
7515
|
+
import * as fs33 from "fs";
|
|
7448
7516
|
import * as fsPromises5 from "fs/promises";
|
|
7449
|
-
import * as
|
|
7517
|
+
import * as path37 from "path";
|
|
7450
7518
|
async function resolveActiveItems(projectRoot, localItems, nowFn = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
7451
7519
|
const active = /* @__PURE__ */ new Set();
|
|
7452
7520
|
const now = Date.parse(nowFn());
|
|
@@ -7471,7 +7539,7 @@ async function resolveInSprintIds(projectRoot) {
|
|
|
7471
7539
|
const ids = /* @__PURE__ */ new Set();
|
|
7472
7540
|
try {
|
|
7473
7541
|
const sprintDir = resolveActiveSprintDir(projectRoot);
|
|
7474
|
-
const sprintId =
|
|
7542
|
+
const sprintId = path37.basename(sprintDir);
|
|
7475
7543
|
if (sprintId === "_off-sprint") return ids;
|
|
7476
7544
|
const sprintFile = await findSprintFile(projectRoot, sprintId);
|
|
7477
7545
|
if (!sprintFile) return ids;
|
|
@@ -7486,14 +7554,14 @@ async function resolveInSprintIds(projectRoot) {
|
|
|
7486
7554
|
return ids;
|
|
7487
7555
|
}
|
|
7488
7556
|
async function findSprintFile(projectRoot, sprintId) {
|
|
7489
|
-
const pendingSync =
|
|
7490
|
-
const archive =
|
|
7557
|
+
const pendingSync = path37.join(projectRoot, ".cleargate", "delivery", "pending-sync");
|
|
7558
|
+
const archive = path37.join(projectRoot, ".cleargate", "delivery", "archive");
|
|
7491
7559
|
for (const dir of [pendingSync, archive]) {
|
|
7492
7560
|
try {
|
|
7493
|
-
const entries =
|
|
7561
|
+
const entries = fs33.readdirSync(dir, { withFileTypes: true });
|
|
7494
7562
|
for (const entry of entries) {
|
|
7495
7563
|
if (entry.isFile() && entry.name.startsWith(sprintId) && entry.name.endsWith(".md")) {
|
|
7496
|
-
return
|
|
7564
|
+
return path37.join(dir, entry.name);
|
|
7497
7565
|
}
|
|
7498
7566
|
}
|
|
7499
7567
|
} catch {
|
|
@@ -7504,12 +7572,12 @@ async function findSprintFile(projectRoot, sprintId) {
|
|
|
7504
7572
|
|
|
7505
7573
|
// src/lib/comments-cache.ts
|
|
7506
7574
|
import * as fsPromises6 from "fs/promises";
|
|
7507
|
-
import * as
|
|
7575
|
+
import * as path38 from "path";
|
|
7508
7576
|
function cacheDir(projectRoot) {
|
|
7509
|
-
return
|
|
7577
|
+
return path38.join(projectRoot, ".cleargate", ".comments-cache");
|
|
7510
7578
|
}
|
|
7511
7579
|
function cachePath(projectRoot, remoteId) {
|
|
7512
|
-
return
|
|
7580
|
+
return path38.join(cacheDir(projectRoot), `${remoteId}.json`);
|
|
7513
7581
|
}
|
|
7514
7582
|
async function writeCommentCache(projectRoot, remoteId, comments) {
|
|
7515
7583
|
const dir = cacheDir(projectRoot);
|
|
@@ -7523,7 +7591,7 @@ async function writeCommentCache(projectRoot, remoteId, comments) {
|
|
|
7523
7591
|
|
|
7524
7592
|
// src/lib/wiki-comments-render.ts
|
|
7525
7593
|
import * as fsPromises7 from "fs/promises";
|
|
7526
|
-
import * as
|
|
7594
|
+
import * as path39 from "path";
|
|
7527
7595
|
var START = "<!-- cleargate:comments:start -->";
|
|
7528
7596
|
var END = "<!-- cleargate:comments:end -->";
|
|
7529
7597
|
function resolveBucket(fm) {
|
|
@@ -7568,7 +7636,7 @@ async function renderCommentsSection(opts) {
|
|
|
7568
7636
|
const bucket = resolveBucket(localItem.fm);
|
|
7569
7637
|
const primaryId = getPrimaryId(localItem.fm);
|
|
7570
7638
|
if (!bucket || !primaryId) return;
|
|
7571
|
-
const wikiPath =
|
|
7639
|
+
const wikiPath = path39.join(
|
|
7572
7640
|
projectRoot,
|
|
7573
7641
|
".cleargate",
|
|
7574
7642
|
"wiki",
|
|
@@ -7604,7 +7672,7 @@ async function renderCommentsSection(opts) {
|
|
|
7604
7672
|
await writeAtomic4(wikiPath, updated);
|
|
7605
7673
|
}
|
|
7606
7674
|
async function writeAtomic4(filePath, content) {
|
|
7607
|
-
await fsPromises7.mkdir(
|
|
7675
|
+
await fsPromises7.mkdir(path39.dirname(filePath), { recursive: true });
|
|
7608
7676
|
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
7609
7677
|
await fsPromises7.writeFile(tmpPath, content, "utf8");
|
|
7610
7678
|
await fsPromises7.rename(tmpPath, filePath);
|
|
@@ -7616,11 +7684,11 @@ async function syncCheckHandler(opts = {}) {
|
|
|
7616
7684
|
const env = opts.env ?? process.env;
|
|
7617
7685
|
const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
|
|
7618
7686
|
const nowFn = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
7619
|
-
const markerPath =
|
|
7687
|
+
const markerPath = path40.join(projectRoot, ".cleargate", ".sync-marker.json");
|
|
7620
7688
|
const updateMarker = async (nowIso2) => {
|
|
7621
7689
|
try {
|
|
7622
7690
|
const content = JSON.stringify({ last_check: nowIso2 });
|
|
7623
|
-
await fsPromises8.mkdir(
|
|
7691
|
+
await fsPromises8.mkdir(path40.dirname(markerPath), { recursive: true });
|
|
7624
7692
|
const tmpPath = `${markerPath}.tmp.${Date.now()}`;
|
|
7625
7693
|
await fsPromises8.writeFile(tmpPath, content, "utf8");
|
|
7626
7694
|
await fsPromises8.rename(tmpPath, markerPath);
|
|
@@ -7641,9 +7709,9 @@ async function syncCheckHandler(opts = {}) {
|
|
|
7641
7709
|
}
|
|
7642
7710
|
} catch {
|
|
7643
7711
|
}
|
|
7644
|
-
let
|
|
7712
|
+
let mcp2;
|
|
7645
7713
|
if (opts.mcp) {
|
|
7646
|
-
|
|
7714
|
+
mcp2 = opts.mcp;
|
|
7647
7715
|
} else {
|
|
7648
7716
|
let baseUrl = env["CLEARGATE_MCP_URL"];
|
|
7649
7717
|
if (!baseUrl || !baseUrl.trim()) {
|
|
@@ -7672,10 +7740,10 @@ async function syncCheckHandler(opts = {}) {
|
|
|
7672
7740
|
await emitError("adapter-not-configured", nowIso);
|
|
7673
7741
|
return;
|
|
7674
7742
|
}
|
|
7675
|
-
|
|
7743
|
+
mcp2 = createMcpClient({ baseUrl: baseUrl.trim(), token: accessToken });
|
|
7676
7744
|
}
|
|
7677
7745
|
try {
|
|
7678
|
-
const adapterInfo = await
|
|
7746
|
+
const adapterInfo = await mcp2.adapterInfo();
|
|
7679
7747
|
if (!adapterInfo.configured || adapterInfo.name === "no-adapter-configured") {
|
|
7680
7748
|
await emitError("adapter-not-configured", nowIso);
|
|
7681
7749
|
return;
|
|
@@ -7684,7 +7752,7 @@ async function syncCheckHandler(opts = {}) {
|
|
|
7684
7752
|
}
|
|
7685
7753
|
let refs;
|
|
7686
7754
|
try {
|
|
7687
|
-
refs = await
|
|
7755
|
+
refs = await mcp2.call("cleargate_list_remote_updates", { since });
|
|
7688
7756
|
} catch (err) {
|
|
7689
7757
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7690
7758
|
await emitError(msg, nowIso);
|
|
@@ -7703,10 +7771,10 @@ async function syncHandler(opts = {}) {
|
|
|
7703
7771
|
const nowFn = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
7704
7772
|
const identity = resolveIdentity(projectRoot);
|
|
7705
7773
|
const sprintRoot = resolveActiveSprintDir(projectRoot);
|
|
7706
|
-
const sprintId =
|
|
7707
|
-
let
|
|
7774
|
+
const sprintId = path40.basename(sprintRoot);
|
|
7775
|
+
let mcp2;
|
|
7708
7776
|
if (opts.mcp) {
|
|
7709
|
-
|
|
7777
|
+
mcp2 = opts.mcp;
|
|
7710
7778
|
} else {
|
|
7711
7779
|
let baseUrl = env["CLEARGATE_MCP_URL"];
|
|
7712
7780
|
if (!baseUrl || !baseUrl.trim()) {
|
|
@@ -7749,11 +7817,11 @@ async function syncHandler(opts = {}) {
|
|
|
7749
7817
|
exit(2);
|
|
7750
7818
|
return;
|
|
7751
7819
|
}
|
|
7752
|
-
|
|
7820
|
+
mcp2 = createMcpClient({ baseUrl: baseUrl.trim(), token: accessToken });
|
|
7753
7821
|
}
|
|
7754
7822
|
let adapterInfo;
|
|
7755
7823
|
try {
|
|
7756
|
-
adapterInfo = await
|
|
7824
|
+
adapterInfo = await mcp2.adapterInfo();
|
|
7757
7825
|
} catch {
|
|
7758
7826
|
adapterInfo = { configured: true, name: "unknown" };
|
|
7759
7827
|
}
|
|
@@ -7764,7 +7832,7 @@ async function syncHandler(opts = {}) {
|
|
|
7764
7832
|
exit(2);
|
|
7765
7833
|
return;
|
|
7766
7834
|
}
|
|
7767
|
-
const wikiMetaPath =
|
|
7835
|
+
const wikiMetaPath = path40.join(projectRoot, ".cleargate", "wiki", "meta.json");
|
|
7768
7836
|
let lastRemoteSync = "1970-01-01T00:00:00.000Z";
|
|
7769
7837
|
try {
|
|
7770
7838
|
const metaRaw = await fsPromises8.readFile(wikiMetaPath, "utf8");
|
|
@@ -7774,13 +7842,13 @@ async function syncHandler(opts = {}) {
|
|
|
7774
7842
|
}
|
|
7775
7843
|
} catch {
|
|
7776
7844
|
}
|
|
7777
|
-
const remoteRefs = await
|
|
7845
|
+
const remoteRefs = await mcp2.call(
|
|
7778
7846
|
"cleargate_list_remote_updates",
|
|
7779
7847
|
{ since: lastRemoteSync }
|
|
7780
7848
|
);
|
|
7781
7849
|
const pulled = [];
|
|
7782
7850
|
for (const ref of remoteRefs) {
|
|
7783
|
-
const item = await
|
|
7851
|
+
const item = await mcp2.call(
|
|
7784
7852
|
"cleargate_pull_item",
|
|
7785
7853
|
{ remote_id: ref.remote_id }
|
|
7786
7854
|
);
|
|
@@ -7792,7 +7860,7 @@ async function syncHandler(opts = {}) {
|
|
|
7792
7860
|
let intakeResult = { created: 0, items: [] };
|
|
7793
7861
|
try {
|
|
7794
7862
|
intakeResult = await runIntakeBranch({
|
|
7795
|
-
mcp,
|
|
7863
|
+
mcp: mcp2,
|
|
7796
7864
|
identity,
|
|
7797
7865
|
sprintRoot,
|
|
7798
7866
|
projectRoot,
|
|
@@ -7818,7 +7886,7 @@ async function syncHandler(opts = {}) {
|
|
|
7818
7886
|
const activeSet = await resolveActiveItems(projectRoot, localRefs, nowFn);
|
|
7819
7887
|
for (const remoteId of activeSet) {
|
|
7820
7888
|
try {
|
|
7821
|
-
const comments = await
|
|
7889
|
+
const comments = await mcp2.call(
|
|
7822
7890
|
"cleargate_pull_comments",
|
|
7823
7891
|
{ remote_id: remoteId }
|
|
7824
7892
|
);
|
|
@@ -7972,7 +8040,7 @@ async function syncHandler(opts = {}) {
|
|
|
7972
8040
|
await appendSyncLog(sprintRoot, entry);
|
|
7973
8041
|
}
|
|
7974
8042
|
for (const { localPath, fm, body, itemId } of pushQueue) {
|
|
7975
|
-
await
|
|
8043
|
+
await mcp2.call("push_item", {
|
|
7976
8044
|
cleargate_id: itemId,
|
|
7977
8045
|
type: typeof fm["story_id"] === "string" ? "story" : typeof fm["epic_id"] === "string" ? "epic" : typeof fm["proposal_id"] === "string" ? "proposal" : "story",
|
|
7978
8046
|
payload: fm
|
|
@@ -8005,7 +8073,7 @@ async function syncHandler(opts = {}) {
|
|
|
8005
8073
|
};
|
|
8006
8074
|
await appendSyncLog(sprintRoot, entry);
|
|
8007
8075
|
}
|
|
8008
|
-
const conflictsFile =
|
|
8076
|
+
const conflictsFile = path40.join(projectRoot, ".cleargate", ".conflicts.json");
|
|
8009
8077
|
const conflictsContent = {
|
|
8010
8078
|
generated_at: nowFn(),
|
|
8011
8079
|
sprint_id: sprintId,
|
|
@@ -8013,7 +8081,7 @@ async function syncHandler(opts = {}) {
|
|
|
8013
8081
|
};
|
|
8014
8082
|
await writeAtomic5(conflictsFile, JSON.stringify(conflictsContent, null, 2) + "\n");
|
|
8015
8083
|
try {
|
|
8016
|
-
await fsPromises8.mkdir(
|
|
8084
|
+
await fsPromises8.mkdir(path40.dirname(wikiMetaPath), { recursive: true });
|
|
8017
8085
|
let meta = {};
|
|
8018
8086
|
try {
|
|
8019
8087
|
const raw = await fsPromises8.readFile(wikiMetaPath, "utf8");
|
|
@@ -8054,13 +8122,13 @@ async function applyPull(item, localPath, fm, actorEmail, nowFn) {
|
|
|
8054
8122
|
await writeAtomic5(localPath, newContent);
|
|
8055
8123
|
}
|
|
8056
8124
|
async function writeAtomic5(filePath, content) {
|
|
8057
|
-
await fsPromises8.mkdir(
|
|
8125
|
+
await fsPromises8.mkdir(path40.dirname(filePath), { recursive: true });
|
|
8058
8126
|
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
8059
8127
|
await fsPromises8.writeFile(tmpPath, content, "utf8");
|
|
8060
8128
|
await fsPromises8.rename(tmpPath, filePath);
|
|
8061
8129
|
}
|
|
8062
8130
|
async function scanLocalItems(projectRoot) {
|
|
8063
|
-
const pendingSync =
|
|
8131
|
+
const pendingSync = path40.join(projectRoot, ".cleargate", "delivery", "pending-sync");
|
|
8064
8132
|
const results = [];
|
|
8065
8133
|
let entries;
|
|
8066
8134
|
try {
|
|
@@ -8070,7 +8138,7 @@ async function scanLocalItems(projectRoot) {
|
|
|
8070
8138
|
}
|
|
8071
8139
|
for (const entry of entries) {
|
|
8072
8140
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
8073
|
-
const fullPath =
|
|
8141
|
+
const fullPath = path40.join(pendingSync, entry.name);
|
|
8074
8142
|
try {
|
|
8075
8143
|
const raw = await fsPromises8.readFile(fullPath, "utf8");
|
|
8076
8144
|
const { fm, body } = parseFrontmatter(raw);
|
|
@@ -8092,7 +8160,7 @@ function getItemId(fm) {
|
|
|
8092
8160
|
|
|
8093
8161
|
// src/commands/pull.ts
|
|
8094
8162
|
import * as fsPromises9 from "fs/promises";
|
|
8095
|
-
import * as
|
|
8163
|
+
import * as path41 from "path";
|
|
8096
8164
|
async function pullHandler(idOrRemoteId, opts = {}) {
|
|
8097
8165
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
8098
8166
|
const env = opts.env ?? process.env;
|
|
@@ -8102,9 +8170,9 @@ async function pullHandler(idOrRemoteId, opts = {}) {
|
|
|
8102
8170
|
const nowFn = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
8103
8171
|
const identity = resolveIdentity(projectRoot);
|
|
8104
8172
|
const sprintRoot = resolveActiveSprintDir(projectRoot);
|
|
8105
|
-
let
|
|
8173
|
+
let mcp2;
|
|
8106
8174
|
if (opts.mcp) {
|
|
8107
|
-
|
|
8175
|
+
mcp2 = opts.mcp;
|
|
8108
8176
|
} else {
|
|
8109
8177
|
let baseUrl = env["CLEARGATE_MCP_URL"];
|
|
8110
8178
|
if (!baseUrl || !baseUrl.trim()) {
|
|
@@ -8139,7 +8207,7 @@ async function pullHandler(idOrRemoteId, opts = {}) {
|
|
|
8139
8207
|
exit(2);
|
|
8140
8208
|
return;
|
|
8141
8209
|
}
|
|
8142
|
-
|
|
8210
|
+
mcp2 = createMcpClient({ baseUrl: baseUrl.trim(), token: accessToken });
|
|
8143
8211
|
}
|
|
8144
8212
|
const remoteId = await resolveRemoteId(idOrRemoteId, projectRoot);
|
|
8145
8213
|
if (!remoteId) {
|
|
@@ -8148,7 +8216,7 @@ async function pullHandler(idOrRemoteId, opts = {}) {
|
|
|
8148
8216
|
exit(1);
|
|
8149
8217
|
return;
|
|
8150
8218
|
}
|
|
8151
|
-
const remoteItem = await
|
|
8219
|
+
const remoteItem = await mcp2.call("cleargate_pull_item", { remote_id: remoteId });
|
|
8152
8220
|
if (!remoteItem) {
|
|
8153
8221
|
stderr(`Error: item ${remoteId} not found on MCP server.
|
|
8154
8222
|
`);
|
|
@@ -8205,10 +8273,10 @@ async function pullHandler(idOrRemoteId, opts = {}) {
|
|
|
8205
8273
|
result: "ok"
|
|
8206
8274
|
};
|
|
8207
8275
|
await appendSyncLog(sprintRoot, entry);
|
|
8208
|
-
stdout(`pull: ${remoteId} applied to ${
|
|
8276
|
+
stdout(`pull: ${remoteId} applied to ${path41.relative(projectRoot, localPath)}
|
|
8209
8277
|
`);
|
|
8210
8278
|
if (opts.comments) {
|
|
8211
|
-
const comments = await
|
|
8279
|
+
const comments = await mcp2.call(
|
|
8212
8280
|
"cleargate_pull_comments",
|
|
8213
8281
|
{ remote_id: remoteId }
|
|
8214
8282
|
);
|
|
@@ -8228,7 +8296,7 @@ async function resolveRemoteId(idOrRemoteId, projectRoot) {
|
|
|
8228
8296
|
if (/^[A-Z]+-\d+/.test(idOrRemoteId)) {
|
|
8229
8297
|
return idOrRemoteId;
|
|
8230
8298
|
}
|
|
8231
|
-
const pendingSync =
|
|
8299
|
+
const pendingSync = path41.join(projectRoot, ".cleargate", "delivery", "pending-sync");
|
|
8232
8300
|
let entries;
|
|
8233
8301
|
try {
|
|
8234
8302
|
entries = await fsPromises9.readdir(pendingSync, { withFileTypes: true });
|
|
@@ -8238,7 +8306,7 @@ async function resolveRemoteId(idOrRemoteId, projectRoot) {
|
|
|
8238
8306
|
for (const entry of entries) {
|
|
8239
8307
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
8240
8308
|
try {
|
|
8241
|
-
const raw = await fsPromises9.readFile(
|
|
8309
|
+
const raw = await fsPromises9.readFile(path41.join(pendingSync, entry.name), "utf8");
|
|
8242
8310
|
const { fm } = parseFrontmatter(raw);
|
|
8243
8311
|
for (const key of ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id"]) {
|
|
8244
8312
|
if (fm[key] === idOrRemoteId && typeof fm["remote_id"] === "string") {
|
|
@@ -8251,7 +8319,7 @@ async function resolveRemoteId(idOrRemoteId, projectRoot) {
|
|
|
8251
8319
|
return null;
|
|
8252
8320
|
}
|
|
8253
8321
|
async function findLocalFile(remoteId, projectRoot) {
|
|
8254
|
-
const pendingSync =
|
|
8322
|
+
const pendingSync = path41.join(projectRoot, ".cleargate", "delivery", "pending-sync");
|
|
8255
8323
|
let entries;
|
|
8256
8324
|
try {
|
|
8257
8325
|
entries = await fsPromises9.readdir(pendingSync, { withFileTypes: true });
|
|
@@ -8260,7 +8328,7 @@ async function findLocalFile(remoteId, projectRoot) {
|
|
|
8260
8328
|
}
|
|
8261
8329
|
for (const entry of entries) {
|
|
8262
8330
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
8263
|
-
const fullPath =
|
|
8331
|
+
const fullPath = path41.join(pendingSync, entry.name);
|
|
8264
8332
|
try {
|
|
8265
8333
|
const raw = await fsPromises9.readFile(fullPath, "utf8");
|
|
8266
8334
|
const { fm } = parseFrontmatter(raw);
|
|
@@ -8271,7 +8339,7 @@ async function findLocalFile(remoteId, projectRoot) {
|
|
|
8271
8339
|
return null;
|
|
8272
8340
|
}
|
|
8273
8341
|
async function writeAtomic6(filePath, content) {
|
|
8274
|
-
await fsPromises9.mkdir(
|
|
8342
|
+
await fsPromises9.mkdir(path41.dirname(filePath), { recursive: true });
|
|
8275
8343
|
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
8276
8344
|
await fsPromises9.writeFile(tmpPath, content, "utf8");
|
|
8277
8345
|
await fsPromises9.rename(tmpPath, filePath);
|
|
@@ -8286,7 +8354,7 @@ function getItemId2(fm) {
|
|
|
8286
8354
|
|
|
8287
8355
|
// src/commands/push.ts
|
|
8288
8356
|
import * as fsPromises10 from "fs/promises";
|
|
8289
|
-
import * as
|
|
8357
|
+
import * as path42 from "path";
|
|
8290
8358
|
async function pushHandler(fileOrId, opts = {}) {
|
|
8291
8359
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
8292
8360
|
const env = opts.env ?? process.env;
|
|
@@ -8360,7 +8428,7 @@ async function pushHandler(fileOrId, opts = {}) {
|
|
|
8360
8428
|
}
|
|
8361
8429
|
async function handlePush(filePath, ctx) {
|
|
8362
8430
|
const { projectRoot, identity, sprintRoot, nowFn, resolveMcp, stdout, stderr, exit } = ctx;
|
|
8363
|
-
const resolvedPath =
|
|
8431
|
+
const resolvedPath = path42.isAbsolute(filePath) ? filePath : path42.resolve(projectRoot, filePath);
|
|
8364
8432
|
let rawContent;
|
|
8365
8433
|
try {
|
|
8366
8434
|
rawContent = await fsPromises10.readFile(resolvedPath, "utf8");
|
|
@@ -8403,10 +8471,10 @@ async function handlePush(filePath, ctx) {
|
|
|
8403
8471
|
if (h1) payloadForPush["title"] = h1;
|
|
8404
8472
|
}
|
|
8405
8473
|
payloadForPush["body"] = body;
|
|
8406
|
-
const
|
|
8474
|
+
const mcp2 = await resolveMcp();
|
|
8407
8475
|
let result;
|
|
8408
8476
|
try {
|
|
8409
|
-
result = await
|
|
8477
|
+
result = await mcp2.call("push_item", {
|
|
8410
8478
|
cleargate_id: itemId,
|
|
8411
8479
|
type,
|
|
8412
8480
|
payload: payloadForPush,
|
|
@@ -8458,9 +8526,9 @@ async function handleRevert(idOrRemoteId, ctx) {
|
|
|
8458
8526
|
exit(1);
|
|
8459
8527
|
return;
|
|
8460
8528
|
}
|
|
8461
|
-
const
|
|
8529
|
+
const mcp2 = await resolveMcp();
|
|
8462
8530
|
try {
|
|
8463
|
-
await
|
|
8531
|
+
await mcp2.call("sync_status", {
|
|
8464
8532
|
cleargate_id: itemId,
|
|
8465
8533
|
new_status: "archived-without-shipping"
|
|
8466
8534
|
});
|
|
@@ -8486,8 +8554,8 @@ async function handleRevert(idOrRemoteId, ctx) {
|
|
|
8486
8554
|
void localPath;
|
|
8487
8555
|
}
|
|
8488
8556
|
async function resolveLocalItem(idOrRemoteId, projectRoot) {
|
|
8489
|
-
const pendingSync =
|
|
8490
|
-
const archive =
|
|
8557
|
+
const pendingSync = path42.join(projectRoot, ".cleargate", "delivery", "pending-sync");
|
|
8558
|
+
const archive = path42.join(projectRoot, ".cleargate", "delivery", "archive");
|
|
8491
8559
|
for (const dir of [pendingSync, archive]) {
|
|
8492
8560
|
let entries;
|
|
8493
8561
|
try {
|
|
@@ -8497,7 +8565,7 @@ async function resolveLocalItem(idOrRemoteId, projectRoot) {
|
|
|
8497
8565
|
}
|
|
8498
8566
|
for (const entry of entries) {
|
|
8499
8567
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
8500
|
-
const fullPath =
|
|
8568
|
+
const fullPath = path42.join(dir, entry.name);
|
|
8501
8569
|
try {
|
|
8502
8570
|
const raw = await fsPromises10.readFile(fullPath, "utf8");
|
|
8503
8571
|
const { fm } = parseFrontmatter(raw);
|
|
@@ -8516,7 +8584,7 @@ async function resolveLocalItem(idOrRemoteId, projectRoot) {
|
|
|
8516
8584
|
return null;
|
|
8517
8585
|
}
|
|
8518
8586
|
async function writeAtomic7(filePath, content) {
|
|
8519
|
-
await fsPromises10.mkdir(
|
|
8587
|
+
await fsPromises10.mkdir(path42.dirname(filePath), { recursive: true });
|
|
8520
8588
|
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
8521
8589
|
await fsPromises10.writeFile(tmpPath, content, "utf8");
|
|
8522
8590
|
await fsPromises10.rename(tmpPath, filePath);
|
|
@@ -8544,7 +8612,7 @@ function getItemType(fm) {
|
|
|
8544
8612
|
|
|
8545
8613
|
// src/commands/conflicts.ts
|
|
8546
8614
|
import * as fsPromises11 from "fs/promises";
|
|
8547
|
-
import * as
|
|
8615
|
+
import * as path43 from "path";
|
|
8548
8616
|
var RESOLUTION_HINTS = {
|
|
8549
8617
|
"local-delete-remote-edit": "remote-delete: resurrect or delete remote?",
|
|
8550
8618
|
"remote-delete-local-edit": "local-edit: push your changes or accept remote deletion?",
|
|
@@ -8580,7 +8648,7 @@ async function conflictsHandler(opts = {}) {
|
|
|
8580
8648
|
}
|
|
8581
8649
|
}
|
|
8582
8650
|
}
|
|
8583
|
-
const conflictsFile =
|
|
8651
|
+
const conflictsFile = path43.join(projectRoot, ".cleargate", ".conflicts.json");
|
|
8584
8652
|
let data;
|
|
8585
8653
|
try {
|
|
8586
8654
|
const raw = await fsPromises11.readFile(conflictsFile, "utf8");
|
|
@@ -8666,8 +8734,8 @@ function formatEntry(entry) {
|
|
|
8666
8734
|
}
|
|
8667
8735
|
|
|
8668
8736
|
// src/commands/admin-login.ts
|
|
8669
|
-
import * as
|
|
8670
|
-
import * as
|
|
8737
|
+
import * as fs34 from "fs";
|
|
8738
|
+
import * as path44 from "path";
|
|
8671
8739
|
import * as os5 from "os";
|
|
8672
8740
|
var DEFAULT_MCP_URL = "http://localhost:3000";
|
|
8673
8741
|
function resolveMcpUrl(mcpUrlFlag, env) {
|
|
@@ -8676,14 +8744,14 @@ function resolveMcpUrl(mcpUrlFlag, env) {
|
|
|
8676
8744
|
function resolveAuthFilePath(opts) {
|
|
8677
8745
|
if (opts.authFilePath) return opts.authFilePath;
|
|
8678
8746
|
const homedirFn = opts.homedir ?? os5.homedir;
|
|
8679
|
-
return
|
|
8747
|
+
return path44.join(homedirFn(), ".cleargate", "admin-auth.json");
|
|
8680
8748
|
}
|
|
8681
8749
|
function writeAdminAuth(filePath, token) {
|
|
8682
|
-
const dir =
|
|
8683
|
-
|
|
8750
|
+
const dir = path44.dirname(filePath);
|
|
8751
|
+
fs34.mkdirSync(dir, { recursive: true });
|
|
8684
8752
|
const payload = JSON.stringify({ version: 1, token }, null, 2);
|
|
8685
|
-
|
|
8686
|
-
|
|
8753
|
+
fs34.writeFileSync(filePath, payload, { encoding: "utf8", mode: 384 });
|
|
8754
|
+
fs34.chmodSync(filePath, 384);
|
|
8687
8755
|
}
|
|
8688
8756
|
async function adminLoginHandler(opts = {}) {
|
|
8689
8757
|
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
@@ -8792,8 +8860,8 @@ async function adminLoginHandler(opts = {}) {
|
|
|
8792
8860
|
}
|
|
8793
8861
|
|
|
8794
8862
|
// src/commands/hotfix.ts
|
|
8795
|
-
import * as
|
|
8796
|
-
import * as
|
|
8863
|
+
import * as fs35 from "fs";
|
|
8864
|
+
import * as path45 from "path";
|
|
8797
8865
|
function defaultExit4(code) {
|
|
8798
8866
|
return process.exit(code);
|
|
8799
8867
|
}
|
|
@@ -8803,7 +8871,7 @@ function maxHotfixId(pendingDir) {
|
|
|
8803
8871
|
let max = 0;
|
|
8804
8872
|
let entries;
|
|
8805
8873
|
try {
|
|
8806
|
-
entries =
|
|
8874
|
+
entries = fs35.readdirSync(pendingDir);
|
|
8807
8875
|
} catch {
|
|
8808
8876
|
return 0;
|
|
8809
8877
|
}
|
|
@@ -8817,13 +8885,13 @@ function maxHotfixId(pendingDir) {
|
|
|
8817
8885
|
return max;
|
|
8818
8886
|
}
|
|
8819
8887
|
function countActiveHotfixes(repoRoot) {
|
|
8820
|
-
const pendingDir =
|
|
8821
|
-
const archiveDir =
|
|
8888
|
+
const pendingDir = path45.join(repoRoot, ".cleargate", "delivery", "pending-sync");
|
|
8889
|
+
const archiveDir = path45.join(repoRoot, ".cleargate", "delivery", "archive");
|
|
8822
8890
|
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
8823
8891
|
let count = 0;
|
|
8824
8892
|
let pendingEntries = [];
|
|
8825
8893
|
try {
|
|
8826
|
-
pendingEntries =
|
|
8894
|
+
pendingEntries = fs35.readdirSync(pendingDir);
|
|
8827
8895
|
} catch {
|
|
8828
8896
|
}
|
|
8829
8897
|
for (const entry of pendingEntries) {
|
|
@@ -8831,13 +8899,13 @@ function countActiveHotfixes(repoRoot) {
|
|
|
8831
8899
|
}
|
|
8832
8900
|
let archiveEntries = [];
|
|
8833
8901
|
try {
|
|
8834
|
-
archiveEntries =
|
|
8902
|
+
archiveEntries = fs35.readdirSync(archiveDir);
|
|
8835
8903
|
} catch {
|
|
8836
8904
|
}
|
|
8837
8905
|
for (const entry of archiveEntries) {
|
|
8838
8906
|
if (entry.startsWith("HOTFIX-") && entry.endsWith(".md")) {
|
|
8839
8907
|
try {
|
|
8840
|
-
const stat =
|
|
8908
|
+
const stat = fs35.statSync(path45.join(archiveDir, entry));
|
|
8841
8909
|
if (stat.mtimeMs >= sevenDaysAgo) count++;
|
|
8842
8910
|
} catch {
|
|
8843
8911
|
}
|
|
@@ -8846,7 +8914,7 @@ function countActiveHotfixes(repoRoot) {
|
|
|
8846
8914
|
return count;
|
|
8847
8915
|
}
|
|
8848
8916
|
function resolveTemplatePath(repoRoot) {
|
|
8849
|
-
return
|
|
8917
|
+
return path45.join(repoRoot, ".cleargate", "templates", "hotfix.md");
|
|
8850
8918
|
}
|
|
8851
8919
|
function hotfixNewHandler(opts, cli) {
|
|
8852
8920
|
const stdoutFn = cli?.stdout ?? ((s) => process.stdout.write(s + "\n"));
|
|
@@ -8865,14 +8933,14 @@ function hotfixNewHandler(opts, cli) {
|
|
|
8865
8933
|
);
|
|
8866
8934
|
return exitFn(1);
|
|
8867
8935
|
}
|
|
8868
|
-
const pendingDir =
|
|
8936
|
+
const pendingDir = path45.join(repoRoot, ".cleargate", "delivery", "pending-sync");
|
|
8869
8937
|
const maxId = maxHotfixId(pendingDir);
|
|
8870
8938
|
const nextId = maxId + 1;
|
|
8871
8939
|
const idStr = `HOTFIX-${String(nextId).padStart(3, "0")}`;
|
|
8872
8940
|
const templatePath = resolveTemplatePath(repoRoot);
|
|
8873
8941
|
let templateContent;
|
|
8874
8942
|
try {
|
|
8875
|
-
templateContent =
|
|
8943
|
+
templateContent = fs35.readFileSync(templatePath, "utf8");
|
|
8876
8944
|
} catch {
|
|
8877
8945
|
stderrFn(`[cleargate hotfix new] template not found: ${templatePath}`);
|
|
8878
8946
|
return exitFn(2);
|
|
@@ -8880,10 +8948,10 @@ function hotfixNewHandler(opts, cli) {
|
|
|
8880
8948
|
const content = templateContent.replace(/\{ID\}/g, idStr).replace(/\{SLUG\}/g, opts.slug).replace(/\{ISO\}/g, now);
|
|
8881
8949
|
const fileSlug = opts.slug.replace(/-/g, "_");
|
|
8882
8950
|
const fileName = `${idStr}_${fileSlug}.md`;
|
|
8883
|
-
const outPath =
|
|
8951
|
+
const outPath = path45.join(pendingDir, fileName);
|
|
8884
8952
|
try {
|
|
8885
|
-
|
|
8886
|
-
|
|
8953
|
+
fs35.mkdirSync(pendingDir, { recursive: true });
|
|
8954
|
+
fs35.writeFileSync(outPath, content, "utf8");
|
|
8887
8955
|
} catch (err) {
|
|
8888
8956
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8889
8957
|
stderrFn(`[cleargate hotfix new] write failed: ${msg}`);
|
|
@@ -8893,6 +8961,214 @@ function hotfixNewHandler(opts, cli) {
|
|
|
8893
8961
|
return exitFn(0);
|
|
8894
8962
|
}
|
|
8895
8963
|
|
|
8964
|
+
// src/commands/mcp-serve.ts
|
|
8965
|
+
import * as readline5 from "readline";
|
|
8966
|
+
|
|
8967
|
+
// src/auth/refresh.ts
|
|
8968
|
+
async function refreshAccessToken(baseUrl, refreshToken, deps = {}) {
|
|
8969
|
+
const fetchFn = deps.fetch ?? globalThis.fetch;
|
|
8970
|
+
const res = await fetchFn(`${baseUrl}/auth/refresh`, {
|
|
8971
|
+
method: "POST",
|
|
8972
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
8973
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
8974
|
+
});
|
|
8975
|
+
if (!res.ok) {
|
|
8976
|
+
const body = await res.json().catch(() => ({}));
|
|
8977
|
+
throw new RefreshError(res.status, body.error ?? "unknown_error");
|
|
8978
|
+
}
|
|
8979
|
+
const json = await res.json();
|
|
8980
|
+
if (typeof json.access_token !== "string" || typeof json.refresh_token !== "string" || typeof json.expires_in !== "number") {
|
|
8981
|
+
throw new RefreshError(500, "malformed_response");
|
|
8982
|
+
}
|
|
8983
|
+
return json;
|
|
8984
|
+
}
|
|
8985
|
+
var RefreshError = class extends Error {
|
|
8986
|
+
constructor(status, code) {
|
|
8987
|
+
super(`refresh failed: ${status} ${code}`);
|
|
8988
|
+
this.status = status;
|
|
8989
|
+
this.code = code;
|
|
8990
|
+
this.name = "RefreshError";
|
|
8991
|
+
}
|
|
8992
|
+
status;
|
|
8993
|
+
code;
|
|
8994
|
+
};
|
|
8995
|
+
var AuthFetcher = class {
|
|
8996
|
+
constructor(opts) {
|
|
8997
|
+
this.opts = opts;
|
|
8998
|
+
}
|
|
8999
|
+
opts;
|
|
9000
|
+
accessToken = null;
|
|
9001
|
+
accessExpiresAt = 0;
|
|
9002
|
+
inflight = null;
|
|
9003
|
+
/** Returns a fresh access token, refreshing if needed. */
|
|
9004
|
+
async getAccessToken() {
|
|
9005
|
+
const now = (this.opts.now ?? (() => Date.now()))();
|
|
9006
|
+
const skewMs = (this.opts.skewSeconds ?? 60) * 1e3;
|
|
9007
|
+
if (this.accessToken && now < this.accessExpiresAt - skewMs) {
|
|
9008
|
+
return this.accessToken;
|
|
9009
|
+
}
|
|
9010
|
+
if (this.inflight) return this.inflight;
|
|
9011
|
+
this.inflight = this.refreshNow().finally(() => {
|
|
9012
|
+
this.inflight = null;
|
|
9013
|
+
});
|
|
9014
|
+
return this.inflight;
|
|
9015
|
+
}
|
|
9016
|
+
/** Force the next call to refresh. Used after a 401. */
|
|
9017
|
+
invalidate() {
|
|
9018
|
+
this.accessToken = null;
|
|
9019
|
+
this.accessExpiresAt = 0;
|
|
9020
|
+
}
|
|
9021
|
+
async refreshNow() {
|
|
9022
|
+
const stored = await this.opts.loadRefresh();
|
|
9023
|
+
if (!stored) {
|
|
9024
|
+
throw new RefreshError(401, "no_refresh_token");
|
|
9025
|
+
}
|
|
9026
|
+
const exchanged = await refreshAccessToken(this.opts.baseUrl, stored, {
|
|
9027
|
+
...this.opts.fetch ? { fetch: this.opts.fetch } : {},
|
|
9028
|
+
...this.opts.now ? { now: this.opts.now } : {}
|
|
9029
|
+
});
|
|
9030
|
+
await this.opts.saveRefresh(exchanged.refresh_token);
|
|
9031
|
+
const now = (this.opts.now ?? (() => Date.now()))();
|
|
9032
|
+
this.accessToken = exchanged.access_token;
|
|
9033
|
+
this.accessExpiresAt = now + exchanged.expires_in * 1e3;
|
|
9034
|
+
return exchanged.access_token;
|
|
9035
|
+
}
|
|
9036
|
+
};
|
|
9037
|
+
|
|
9038
|
+
// src/commands/mcp-serve.ts
|
|
9039
|
+
var DEFAULT_BASE_URL = "https://cleargate-mcp.soula.ge";
|
|
9040
|
+
async function mcpServeHandler(opts) {
|
|
9041
|
+
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
9042
|
+
const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
|
|
9043
|
+
const stderr = opts.stderr ?? ((s) => process.stderr.write(s));
|
|
9044
|
+
const exit = opts.exit ?? ((c) => process.exit(c));
|
|
9045
|
+
const cfg = loadConfig({
|
|
9046
|
+
flags: { profile: opts.profile, mcpUrl: opts.mcpUrlFlag }
|
|
9047
|
+
});
|
|
9048
|
+
const baseUrl = cfg.mcpUrl ?? DEFAULT_BASE_URL;
|
|
9049
|
+
const store = await (opts.createStore ?? createTokenStore)({
|
|
9050
|
+
...opts.keychainService !== void 0 ? { keychainService: opts.keychainService } : {},
|
|
9051
|
+
...opts.forceBackend !== void 0 ? { forceBackend: opts.forceBackend } : {}
|
|
9052
|
+
});
|
|
9053
|
+
const fetcher = new AuthFetcher({
|
|
9054
|
+
baseUrl,
|
|
9055
|
+
loadRefresh: () => store.load(opts.profile),
|
|
9056
|
+
saveRefresh: (t) => store.save(opts.profile, t),
|
|
9057
|
+
...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
|
|
9058
|
+
...opts.now !== void 0 ? { now: opts.now } : {}
|
|
9059
|
+
});
|
|
9060
|
+
try {
|
|
9061
|
+
await fetcher.getAccessToken();
|
|
9062
|
+
} catch (err) {
|
|
9063
|
+
if (err instanceof RefreshError) {
|
|
9064
|
+
stderr(
|
|
9065
|
+
`cleargate mcp serve: refresh failed (${err.status} ${err.code}). Run \`cleargate join <invite-url>\` to re-authenticate.
|
|
9066
|
+
`
|
|
9067
|
+
);
|
|
9068
|
+
} else {
|
|
9069
|
+
stderr(
|
|
9070
|
+
`cleargate mcp serve: ${err instanceof Error ? err.message : String(err)}
|
|
9071
|
+
`
|
|
9072
|
+
);
|
|
9073
|
+
}
|
|
9074
|
+
return exit(1);
|
|
9075
|
+
}
|
|
9076
|
+
const inputStream = opts.stdin ?? process.stdin;
|
|
9077
|
+
const rl = readline5.createInterface({
|
|
9078
|
+
input: inputStream,
|
|
9079
|
+
output: void 0,
|
|
9080
|
+
terminal: false
|
|
9081
|
+
});
|
|
9082
|
+
for await (const line of rl) {
|
|
9083
|
+
if (!line.trim()) continue;
|
|
9084
|
+
try {
|
|
9085
|
+
await proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr);
|
|
9086
|
+
} catch (err) {
|
|
9087
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9088
|
+
stderr(`cleargate mcp serve: proxy error: ${errMsg}
|
|
9089
|
+
`);
|
|
9090
|
+
const id = extractId(line);
|
|
9091
|
+
if (id !== void 0) {
|
|
9092
|
+
stdout(
|
|
9093
|
+
JSON.stringify({
|
|
9094
|
+
jsonrpc: "2.0",
|
|
9095
|
+
id,
|
|
9096
|
+
error: { code: -32603, message: `proxy error: ${errMsg}` }
|
|
9097
|
+
}) + "\n"
|
|
9098
|
+
);
|
|
9099
|
+
}
|
|
9100
|
+
}
|
|
9101
|
+
}
|
|
9102
|
+
}
|
|
9103
|
+
async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
|
|
9104
|
+
let parsed;
|
|
9105
|
+
try {
|
|
9106
|
+
parsed = JSON.parse(line);
|
|
9107
|
+
} catch {
|
|
9108
|
+
stderr(`cleargate mcp serve: ignoring non-JSON line: ${line.slice(0, 80)}
|
|
9109
|
+
`);
|
|
9110
|
+
return;
|
|
9111
|
+
}
|
|
9112
|
+
const isNotification = !("id" in parsed) || parsed.id === void 0 || parsed.id === null;
|
|
9113
|
+
let access = await fetcher.getAccessToken();
|
|
9114
|
+
let res = await postFrame(baseUrl, line, access, fetchFn);
|
|
9115
|
+
if (res.status === 401) {
|
|
9116
|
+
fetcher.invalidate();
|
|
9117
|
+
access = await fetcher.getAccessToken();
|
|
9118
|
+
res = await postFrame(baseUrl, line, access, fetchFn);
|
|
9119
|
+
}
|
|
9120
|
+
if (isNotification) {
|
|
9121
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
9122
|
+
return;
|
|
9123
|
+
}
|
|
9124
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
9125
|
+
if (ct.includes("text/event-stream")) {
|
|
9126
|
+
await streamSse(res, stdout);
|
|
9127
|
+
} else {
|
|
9128
|
+
const text = await res.text();
|
|
9129
|
+
if (text.length > 0) stdout(text + "\n");
|
|
9130
|
+
}
|
|
9131
|
+
}
|
|
9132
|
+
async function postFrame(baseUrl, body, accessToken, fetchFn) {
|
|
9133
|
+
return fetchFn(`${baseUrl}/mcp`, {
|
|
9134
|
+
method: "POST",
|
|
9135
|
+
headers: {
|
|
9136
|
+
"Content-Type": "application/json",
|
|
9137
|
+
Accept: "application/json, text/event-stream",
|
|
9138
|
+
Authorization: `Bearer ${accessToken}`
|
|
9139
|
+
},
|
|
9140
|
+
body
|
|
9141
|
+
});
|
|
9142
|
+
}
|
|
9143
|
+
async function streamSse(res, stdout) {
|
|
9144
|
+
if (!res.body) return;
|
|
9145
|
+
const reader = res.body.getReader();
|
|
9146
|
+
const decoder = new TextDecoder("utf-8");
|
|
9147
|
+
let buf = "";
|
|
9148
|
+
for (; ; ) {
|
|
9149
|
+
const { value, done } = await reader.read();
|
|
9150
|
+
if (done) break;
|
|
9151
|
+
buf += decoder.decode(value, { stream: true });
|
|
9152
|
+
let nl;
|
|
9153
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
9154
|
+
const ln = buf.slice(0, nl);
|
|
9155
|
+
buf = buf.slice(nl + 1);
|
|
9156
|
+
if (ln.startsWith("data:")) {
|
|
9157
|
+
const payload = ln.slice(5).trim();
|
|
9158
|
+
if (payload) stdout(payload + "\n");
|
|
9159
|
+
}
|
|
9160
|
+
}
|
|
9161
|
+
}
|
|
9162
|
+
}
|
|
9163
|
+
function extractId(line) {
|
|
9164
|
+
try {
|
|
9165
|
+
const obj = JSON.parse(line);
|
|
9166
|
+
return "id" in obj ? obj.id : void 0;
|
|
9167
|
+
} catch {
|
|
9168
|
+
return void 0;
|
|
9169
|
+
}
|
|
9170
|
+
}
|
|
9171
|
+
|
|
8896
9172
|
// src/cli.ts
|
|
8897
9173
|
var program = new Command();
|
|
8898
9174
|
program.name("cleargate").description("ClearGate CLI \u2014 connects AI agent teams to the ClearGate MCP server").version(package_default.version, "-V, --version").option("--profile <name>", "configuration profile to use", "default").option("--mcp-url <url>", "MCP server URL (overrides config file and env)").showHelpAfterError("(use `cleargate --help`)");
|
|
@@ -9135,5 +9411,13 @@ var hotfix = program.command("hotfix").description("hotfix lane commands (off-sp
|
|
|
9135
9411
|
hotfix.command("new <slug>").description("scaffold a new HOTFIX-NNN_<slug>.md in pending-sync/").action((slug) => {
|
|
9136
9412
|
hotfixNewHandler({ slug });
|
|
9137
9413
|
});
|
|
9414
|
+
var mcp = program.command("mcp").description("MCP-server bridge commands (stdio shim, registration helpers)");
|
|
9415
|
+
mcp.command("serve").description("run a stdio MCP server that proxies to the cleargate HTTP /mcp endpoint with auto-refresh Bearer auth").action(async (_opts, command) => {
|
|
9416
|
+
const globals = command.parent.parent.opts();
|
|
9417
|
+
await mcpServeHandler({
|
|
9418
|
+
profile: globals.profile,
|
|
9419
|
+
...globals.mcpUrl !== void 0 ? { mcpUrlFlag: globals.mcpUrl } : {}
|
|
9420
|
+
});
|
|
9421
|
+
});
|
|
9138
9422
|
void program.parseAsync(process.argv);
|
|
9139
9423
|
//# sourceMappingURL=cli.js.map
|