@delorenj/pjangler 1.2.1 → 1.2.2
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/index.js +1033 -103
- package/dist/mcp-server.js +669 -111
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
6
|
+
import { basename as basename4, join as join11, resolve as resolve3 } from "node:path";
|
|
4
7
|
import { Command as Command3 } from "commander";
|
|
5
8
|
|
|
6
9
|
// src/commands/hermes/types.ts
|
|
@@ -1438,9 +1441,12 @@ function createRecipe(name, context) {
|
|
|
1438
1441
|
return new info.class(context);
|
|
1439
1442
|
}
|
|
1440
1443
|
|
|
1444
|
+
// src/index.ts
|
|
1445
|
+
import { cancel as cancel2, multiselect, text as text3, isCancel as isCancel5 } from "@clack/prompts";
|
|
1446
|
+
|
|
1441
1447
|
// src/parity/index.ts
|
|
1442
1448
|
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync, copyFileSync } from "node:fs";
|
|
1443
|
-
import { dirname as dirname5, join as join8, relative, resolve } from "node:path";
|
|
1449
|
+
import { basename as basename2, dirname as dirname5, join as join8, relative, resolve } from "node:path";
|
|
1444
1450
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1445
1451
|
import { homedir as homedir4 } from "node:os";
|
|
1446
1452
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
@@ -1460,6 +1466,28 @@ enter = [
|
|
|
1460
1466
|
patterns = ["AGENTS.md"]
|
|
1461
1467
|
task = "link-agentfiles"
|
|
1462
1468
|
|
|
1469
|
+
[tasks.link-agentfiles]
|
|
1470
|
+
description = "Symlink all agent files to AGENTS.md"
|
|
1471
|
+
run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
|
|
1472
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [
|
|
1473
|
+
"{{config_root}}/.mise/scripts/link-agentfiles.sh",
|
|
1474
|
+
"op inject -i .env.op > .env"
|
|
1475
|
+
];
|
|
1476
|
+
var LINK_AGENTFILES_HOOKS_BLOCK = `# This block will handle the linking of
|
|
1477
|
+
# agent files to the main AGENTS.md file.
|
|
1478
|
+
#
|
|
1479
|
+
# TODO: Ensure this works for all levels of nesting.
|
|
1480
|
+
# i.e. All linked agent files MUST be siblings at
|
|
1481
|
+
# any given level of nesting.
|
|
1482
|
+
[hooks]
|
|
1483
|
+
enter = [
|
|
1484
|
+
"{{config_root}}/.mise/scripts/link-agentfiles.sh",
|
|
1485
|
+
"op inject -i .env.op > .env",
|
|
1486
|
+
]`;
|
|
1487
|
+
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
1488
|
+
patterns = ["AGENTS.md"]
|
|
1489
|
+
task = "link-agentfiles"
|
|
1490
|
+
|
|
1463
1491
|
[tasks.link-agentfiles]
|
|
1464
1492
|
description = "Symlink all agent files to AGENTS.md"
|
|
1465
1493
|
run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
|
|
@@ -1515,10 +1543,10 @@ function writeText(path, content) {
|
|
|
1515
1543
|
ensureParent(path);
|
|
1516
1544
|
writeFileSync4(path, content);
|
|
1517
1545
|
}
|
|
1518
|
-
function tryParseJson(
|
|
1519
|
-
if (!
|
|
1546
|
+
function tryParseJson(text4) {
|
|
1547
|
+
if (!text4) return null;
|
|
1520
1548
|
try {
|
|
1521
|
-
return JSON.parse(
|
|
1549
|
+
return JSON.parse(text4);
|
|
1522
1550
|
} catch {
|
|
1523
1551
|
return null;
|
|
1524
1552
|
}
|
|
@@ -1577,9 +1605,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
1577
1605
|
}
|
|
1578
1606
|
return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
|
|
1579
1607
|
}
|
|
1580
|
-
function yamlGet(
|
|
1608
|
+
function yamlGet(text4, keyPath) {
|
|
1581
1609
|
const parts = keyPath.split(".");
|
|
1582
|
-
const lines =
|
|
1610
|
+
const lines = text4.split("\n");
|
|
1583
1611
|
let start = 0;
|
|
1584
1612
|
let indent = 0;
|
|
1585
1613
|
for (let idx = 0; idx < parts.length; idx += 1) {
|
|
@@ -1614,25 +1642,25 @@ function discoverRoles(repoRoot) {
|
|
|
1614
1642
|
const roleDir = join8(rolesDir, entry.name);
|
|
1615
1643
|
const roleYamlPath = join8(roleDir, "role.yaml");
|
|
1616
1644
|
if (!existsSync7(roleYamlPath)) return null;
|
|
1617
|
-
const
|
|
1618
|
-
const runtimeRepoRaw = yamlGet(
|
|
1645
|
+
const text4 = readText(roleYamlPath);
|
|
1646
|
+
const runtimeRepoRaw = yamlGet(text4, "runtime.github_repo");
|
|
1619
1647
|
return {
|
|
1620
|
-
role: yamlGet(
|
|
1648
|
+
role: yamlGet(text4, "role") || entry.name,
|
|
1621
1649
|
roleDir,
|
|
1622
1650
|
roleYamlPath,
|
|
1623
|
-
repo: yamlGet(
|
|
1624
|
-
agentId: yamlGet(
|
|
1625
|
-
profileName: yamlGet(
|
|
1626
|
-
displayName: yamlGet(
|
|
1627
|
-
purpose: yamlGet(
|
|
1628
|
-
botHandle: yamlGet(
|
|
1651
|
+
repo: yamlGet(text4, "repo"),
|
|
1652
|
+
agentId: yamlGet(text4, "agent_id"),
|
|
1653
|
+
profileName: yamlGet(text4, "profile") || yamlGet(text4, "agent_id"),
|
|
1654
|
+
displayName: yamlGet(text4, "display_name"),
|
|
1655
|
+
purpose: yamlGet(text4, "purpose"),
|
|
1656
|
+
botHandle: yamlGet(text4, "telegram.bot_username"),
|
|
1629
1657
|
runtimeRepo: runtimeRepoRaw.includes("/") ? runtimeRepoRaw.split("/").slice(-1)[0] ?? runtimeRepoRaw : runtimeRepoRaw,
|
|
1630
|
-
runtimeOwner: yamlGet(
|
|
1631
|
-
planeWorkspace: yamlGet(
|
|
1632
|
-
ticketProviderName: yamlGet(
|
|
1633
|
-
ticketProviderBoardId: yamlGet(
|
|
1634
|
-
ticketProviderBoardUrl: yamlGet(
|
|
1635
|
-
ticketProviderIdentifier: yamlGet(
|
|
1658
|
+
runtimeOwner: yamlGet(text4, "runtime.github_owner"),
|
|
1659
|
+
planeWorkspace: yamlGet(text4, "ticket_provider.workspace") || yamlGet(text4, "plane.workspace"),
|
|
1660
|
+
ticketProviderName: yamlGet(text4, "ticket_provider.name"),
|
|
1661
|
+
ticketProviderBoardId: yamlGet(text4, "ticket_provider.board_id"),
|
|
1662
|
+
ticketProviderBoardUrl: yamlGet(text4, "ticket_provider.board_url"),
|
|
1663
|
+
ticketProviderIdentifier: yamlGet(text4, "plane.identifier")
|
|
1636
1664
|
};
|
|
1637
1665
|
}).filter((value) => Boolean(value));
|
|
1638
1666
|
}
|
|
@@ -1657,23 +1685,39 @@ function templateVersioningScript(ctx) {
|
|
|
1657
1685
|
function templateLinkAgentfilesScript(ctx) {
|
|
1658
1686
|
return templateScript(ctx, "link-agentfiles.sh");
|
|
1659
1687
|
}
|
|
1688
|
+
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
1689
|
+
const project = readProjectJson(ctx);
|
|
1690
|
+
const projectName = String(project?.project_name ?? basename2(ctx.repoRoot) ?? "project");
|
|
1691
|
+
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
1692
|
+
}
|
|
1693
|
+
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
1694
|
+
const targetPath = join8(ctx.repoRoot, "mise.toml");
|
|
1695
|
+
if (existsSync7(targetPath)) return false;
|
|
1696
|
+
const sourcePath = join8(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
1697
|
+
if (!existsSync7(sourcePath)) return false;
|
|
1698
|
+
changedFiles.push(targetPath);
|
|
1699
|
+
if (!ctx.dryRun) {
|
|
1700
|
+
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
1701
|
+
}
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1660
1704
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1661
1705
|
const packageJson = join8(repoRoot, "package.json");
|
|
1662
1706
|
return existsSync7(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
|
|
1663
1707
|
}
|
|
1664
|
-
function replaceOrAppendManagedBlock(
|
|
1665
|
-
if (startMarker.test(
|
|
1666
|
-
return
|
|
1708
|
+
function replaceOrAppendManagedBlock(text4, startMarker, block, beforePattern) {
|
|
1709
|
+
if (startMarker.test(text4)) {
|
|
1710
|
+
return text4.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
|
|
1667
1711
|
}
|
|
1668
1712
|
if (beforePattern) {
|
|
1669
|
-
const match =
|
|
1713
|
+
const match = text4.match(beforePattern);
|
|
1670
1714
|
if (match && typeof match.index === "number") {
|
|
1671
|
-
return `${
|
|
1715
|
+
return `${text4.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
|
|
1672
1716
|
|
|
1673
|
-
${
|
|
1717
|
+
${text4.slice(match.index)}`;
|
|
1674
1718
|
}
|
|
1675
1719
|
}
|
|
1676
|
-
return `${
|
|
1720
|
+
return `${text4.replace(/\s*$/, "")}
|
|
1677
1721
|
|
|
1678
1722
|
${block}
|
|
1679
1723
|
`;
|
|
@@ -1687,18 +1731,18 @@ function requiredMisePathEntries(ctx) {
|
|
|
1687
1731
|
}
|
|
1688
1732
|
return required;
|
|
1689
1733
|
}
|
|
1690
|
-
function upsertMisePath(
|
|
1734
|
+
function upsertMisePath(text4, required = BASE_MISE_PATH_ENTRIES) {
|
|
1691
1735
|
const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
1692
|
-
const envMatch =
|
|
1736
|
+
const envMatch = text4.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
|
|
1693
1737
|
if (!envMatch || typeof envMatch.index !== "number") {
|
|
1694
1738
|
return `[env]
|
|
1695
1739
|
${render(required)}
|
|
1696
1740
|
|
|
1697
|
-
${
|
|
1741
|
+
${text4.replace(/^\s+/, "")}`;
|
|
1698
1742
|
}
|
|
1699
|
-
const prefix =
|
|
1743
|
+
const prefix = text4.slice(0, envMatch.index + envMatch[1].length);
|
|
1700
1744
|
const section = envMatch[2];
|
|
1701
|
-
const suffix =
|
|
1745
|
+
const suffix = text4.slice(envMatch.index + envMatch[1].length + section.length);
|
|
1702
1746
|
const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
|
|
1703
1747
|
if (!pathLine) {
|
|
1704
1748
|
return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
|
|
@@ -1709,26 +1753,128 @@ ${text3.replace(/^\s+/, "")}`;
|
|
|
1709
1753
|
if (!merged.includes(value)) merged.push(value);
|
|
1710
1754
|
}
|
|
1711
1755
|
const nextLine = render(merged);
|
|
1712
|
-
if (pathLine[0] === nextLine) return
|
|
1756
|
+
if (pathLine[0] === nextLine) return text4;
|
|
1713
1757
|
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
1714
1758
|
}
|
|
1715
|
-
function
|
|
1716
|
-
const
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1759
|
+
function removeTomlSection(text4, headerPattern, marker, options) {
|
|
1760
|
+
const lines = text4.split("\n");
|
|
1761
|
+
let start = -1;
|
|
1762
|
+
let end = -1;
|
|
1763
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1764
|
+
if (!headerPattern.test(lines[i])) continue;
|
|
1765
|
+
if (marker) {
|
|
1766
|
+
let hasMarker = false;
|
|
1767
|
+
for (let j = i + 1; j < lines.length && !/^\[[^\]]+\]/.test(lines[j]); j++) {
|
|
1768
|
+
if (marker.test(lines[j])) {
|
|
1769
|
+
hasMarker = true;
|
|
1770
|
+
break;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (!hasMarker) continue;
|
|
1774
|
+
}
|
|
1775
|
+
start = i;
|
|
1776
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1777
|
+
if (/^\[[^\]]+\]/.test(lines[j])) {
|
|
1778
|
+
end = j;
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (end === -1) end = lines.length;
|
|
1783
|
+
break;
|
|
1720
1784
|
}
|
|
1721
|
-
|
|
1785
|
+
if (start === -1) return text4;
|
|
1786
|
+
if (options?.includePrecedingComments) {
|
|
1787
|
+
while (start > 0 && lines[start - 1].trim().startsWith("#")) {
|
|
1788
|
+
start--;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
|
|
1792
|
+
return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
1793
|
+
}
|
|
1794
|
+
function insertTomlBlockBeforeVersioning(text4, block) {
|
|
1795
|
+
const versioningIndex = text4.indexOf("# >>> mise-versioning >>>");
|
|
1722
1796
|
if (versioningIndex >= 0) {
|
|
1723
|
-
return `${
|
|
1797
|
+
return `${text4.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
|
|
1724
1798
|
|
|
1725
|
-
${
|
|
1799
|
+
${text4.slice(versioningIndex)}`;
|
|
1726
1800
|
}
|
|
1727
|
-
return `${
|
|
1801
|
+
return `${text4.replace(/\s*$/, "")}
|
|
1728
1802
|
|
|
1729
|
-
${
|
|
1803
|
+
${block}
|
|
1730
1804
|
`;
|
|
1731
1805
|
}
|
|
1806
|
+
function extractTomlStrings(text4) {
|
|
1807
|
+
const values = [];
|
|
1808
|
+
const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
|
|
1809
|
+
for (const match of text4.matchAll(stringPattern)) {
|
|
1810
|
+
if (match[1] !== void 0) {
|
|
1811
|
+
try {
|
|
1812
|
+
values.push(JSON.parse(`"${match[1]}"`));
|
|
1813
|
+
} catch {
|
|
1814
|
+
values.push(match[1]);
|
|
1815
|
+
}
|
|
1816
|
+
} else if (match[2] !== void 0) {
|
|
1817
|
+
values.push(match[2]);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return values;
|
|
1821
|
+
}
|
|
1822
|
+
function isManagedHookEntry(value) {
|
|
1823
|
+
const trimmed = value.trim();
|
|
1824
|
+
return trimmed === "op inject -i .env.op > .env" || /(^|\/)link-agentfiles\.sh$/.test(trimmed);
|
|
1825
|
+
}
|
|
1826
|
+
function renderHookEntries(entries, indent = "") {
|
|
1827
|
+
return [
|
|
1828
|
+
`${indent}enter = [`,
|
|
1829
|
+
...entries.map((entry) => `${indent} ${JSON.stringify(entry)},`),
|
|
1830
|
+
`${indent}]`
|
|
1831
|
+
];
|
|
1832
|
+
}
|
|
1833
|
+
function upsertLinkAgentfilesHooks(text4) {
|
|
1834
|
+
const lines = text4.split("\n");
|
|
1835
|
+
const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
|
|
1836
|
+
if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text4, LINK_AGENTFILES_HOOKS_BLOCK);
|
|
1837
|
+
let hooksEnd = lines.length;
|
|
1838
|
+
for (let i = hooksStart + 1; i < lines.length; i++) {
|
|
1839
|
+
if (/^\[[^\]]+\]/.test(lines[i].trim())) {
|
|
1840
|
+
hooksEnd = i;
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
let enterStart = -1;
|
|
1845
|
+
let enterEnd = -1;
|
|
1846
|
+
for (let i = hooksStart + 1; i < hooksEnd; i++) {
|
|
1847
|
+
if (!/^\s*enter\s*=/.test(lines[i])) continue;
|
|
1848
|
+
enterStart = i;
|
|
1849
|
+
enterEnd = i + 1;
|
|
1850
|
+
const afterEquals = lines[i].slice(lines[i].indexOf("=") + 1);
|
|
1851
|
+
if (afterEquals.includes("[") && !afterEquals.includes("]")) {
|
|
1852
|
+
while (enterEnd < hooksEnd && !lines[enterEnd].includes("]")) enterEnd++;
|
|
1853
|
+
if (enterEnd < hooksEnd) enterEnd++;
|
|
1854
|
+
}
|
|
1855
|
+
break;
|
|
1856
|
+
}
|
|
1857
|
+
const existingBlock = enterStart >= 0 ? lines.slice(enterStart, enterEnd).join("\n") : "";
|
|
1858
|
+
const preserved = extractTomlStrings(existingBlock).filter((entry) => !isManagedHookEntry(entry));
|
|
1859
|
+
const merged = [...LINK_AGENTFILES_HOOK_ENTRIES];
|
|
1860
|
+
for (const entry of preserved) {
|
|
1861
|
+
if (!merged.includes(entry)) merged.push(entry);
|
|
1862
|
+
}
|
|
1863
|
+
const indent = enterStart >= 0 ? lines[enterStart].match(/^\s*/)?.[0] ?? "" : "";
|
|
1864
|
+
const rendered = renderHookEntries(merged, indent);
|
|
1865
|
+
if (enterStart >= 0) {
|
|
1866
|
+
return lines.slice(0, enterStart).concat(rendered, lines.slice(enterEnd)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
1867
|
+
}
|
|
1868
|
+
return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
1869
|
+
}
|
|
1870
|
+
function upsertLinkAgentfilesBlock(text4, ctx) {
|
|
1871
|
+
const withPath = upsertMisePath(text4, requiredMisePathEntries(ctx));
|
|
1872
|
+
if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
|
|
1873
|
+
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
1874
|
+
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
1875
|
+
cleaned = upsertLinkAgentfilesHooks(cleaned);
|
|
1876
|
+
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
1877
|
+
}
|
|
1732
1878
|
function readProjectJson(ctx) {
|
|
1733
1879
|
return tryParseJson(safeReadText(join8(ctx.repoRoot, ".project.json")));
|
|
1734
1880
|
}
|
|
@@ -1742,23 +1888,35 @@ function canonicalProjectJson(ctx) {
|
|
|
1742
1888
|
workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
|
|
1743
1889
|
identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
|
|
1744
1890
|
board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
|
|
1745
|
-
board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || "")
|
|
1891
|
+
board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || ""),
|
|
1892
|
+
state: String((existing.ticket_provider?.state ?? "planned") || "planned")
|
|
1746
1893
|
};
|
|
1894
|
+
const existingAgents = existing.agents ?? {};
|
|
1895
|
+
const discoveredAgents = Object.fromEntries(
|
|
1896
|
+
roles.map((role) => [
|
|
1897
|
+
role.agentId || `${slug}-${role.role}`,
|
|
1898
|
+
{
|
|
1899
|
+
role: role.role,
|
|
1900
|
+
role_dir: relative(ctx.repoRoot, role.roleDir)
|
|
1901
|
+
}
|
|
1902
|
+
])
|
|
1903
|
+
);
|
|
1904
|
+
const agents = { ...existingAgents };
|
|
1905
|
+
for (const [agentId, discovered] of Object.entries(discoveredAgents)) {
|
|
1906
|
+
const existingAgent = existingAgents[agentId] ?? {};
|
|
1907
|
+
agents[agentId] = {
|
|
1908
|
+
role: discovered.role,
|
|
1909
|
+
role_dir: discovered.role_dir,
|
|
1910
|
+
provisioning_state: existingAgent.provisioning_state
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1747
1913
|
return {
|
|
1748
1914
|
project_name: String(existing.project_name ?? titleCaseSlug(slug)),
|
|
1749
1915
|
project_description: String(existing.project_description ?? ""),
|
|
1750
1916
|
project_slug: slug,
|
|
1751
1917
|
repo_path: ctx.repoRoot,
|
|
1752
1918
|
ticket_provider: ticketProvider,
|
|
1753
|
-
agents
|
|
1754
|
-
roles.map((role) => [
|
|
1755
|
-
role.agentId || `${slug}-${role.role}`,
|
|
1756
|
-
{
|
|
1757
|
-
role: role.role,
|
|
1758
|
-
role_dir: relative(ctx.repoRoot, role.roleDir)
|
|
1759
|
-
}
|
|
1760
|
-
])
|
|
1761
|
-
)
|
|
1919
|
+
agents
|
|
1762
1920
|
};
|
|
1763
1921
|
}
|
|
1764
1922
|
function projectJsonFinding(ctx) {
|
|
@@ -1790,7 +1948,7 @@ function projectJsonFinding(ctx) {
|
|
|
1790
1948
|
}
|
|
1791
1949
|
}
|
|
1792
1950
|
const ticketProvider = data.ticket_provider ?? {};
|
|
1793
|
-
for (const key of ["type", "workspace", "identifier", "board_id", "board_url"]) {
|
|
1951
|
+
for (const key of ["type", "workspace", "identifier", "board_id", "board_url", "state"]) {
|
|
1794
1952
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
1795
1953
|
}
|
|
1796
1954
|
if (existsSync7(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
@@ -1937,9 +2095,9 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
|
|
|
1937
2095
|
return path;
|
|
1938
2096
|
}
|
|
1939
2097
|
function profileMetaInheritsDefault(path) {
|
|
1940
|
-
const
|
|
2098
|
+
const text4 = safeReadText(path);
|
|
1941
2099
|
return Boolean(
|
|
1942
|
-
|
|
2100
|
+
text4 && /^config:\s*$/m.test(text4) && /^\s+inherit_from:\s*default\s*$/m.test(text4) && /^\s+save_mode:\s*delta\s*$/m.test(text4)
|
|
1943
2101
|
);
|
|
1944
2102
|
}
|
|
1945
2103
|
function upsertInheritedProfileMeta(path, changedFiles, dryRun) {
|
|
@@ -1997,17 +2155,17 @@ var RULES = [
|
|
|
1997
2155
|
if (!existsSync7(misePath)) {
|
|
1998
2156
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
1999
2157
|
}
|
|
2000
|
-
const
|
|
2158
|
+
const text4 = readText(misePath);
|
|
2001
2159
|
const details = [];
|
|
2002
2160
|
const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2003
2161
|
if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2004
|
-
const pathValues = [...(
|
|
2162
|
+
const pathValues = [...(text4.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2005
2163
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2006
2164
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
2007
|
-
if (!
|
|
2008
|
-
if (!
|
|
2009
|
-
if (!
|
|
2010
|
-
if (!
|
|
2165
|
+
if (!text4.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
|
|
2166
|
+
if (!text4.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
|
|
2167
|
+
if (!text4.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
|
|
2168
|
+
if (!text4.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
|
|
2011
2169
|
return {
|
|
2012
2170
|
id: "mise.config-root",
|
|
2013
2171
|
title: "mise config_root + AGENTS link hooks",
|
|
@@ -2020,15 +2178,22 @@ var RULES = [
|
|
|
2020
2178
|
migrate: (ctx, finding) => {
|
|
2021
2179
|
const path = join8(ctx.repoRoot, "mise.toml");
|
|
2022
2180
|
const changedFiles = [];
|
|
2181
|
+
const details = [];
|
|
2023
2182
|
if (!existsSync7(path)) {
|
|
2024
|
-
|
|
2183
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2184
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
2185
|
+
}
|
|
2186
|
+
details.push("Initialized mise.toml from generated-project template");
|
|
2187
|
+
if (ctx.dryRun) {
|
|
2188
|
+
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
2189
|
+
}
|
|
2025
2190
|
}
|
|
2026
|
-
let
|
|
2027
|
-
const next = upsertLinkAgentfilesBlock(
|
|
2028
|
-
if (next !==
|
|
2029
|
-
changedFiles.push(path);
|
|
2191
|
+
let text4 = readText(path);
|
|
2192
|
+
const next = upsertLinkAgentfilesBlock(text4, ctx);
|
|
2193
|
+
if (next !== text4) {
|
|
2194
|
+
if (!changedFiles.includes(path)) changedFiles.push(path);
|
|
2030
2195
|
if (!ctx.dryRun) writeText(path, next);
|
|
2031
|
-
|
|
2196
|
+
text4 = next;
|
|
2032
2197
|
}
|
|
2033
2198
|
const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2034
2199
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
@@ -2060,8 +2225,8 @@ var RULES = [
|
|
|
2060
2225
|
const misePath = join8(ctx.repoRoot, "mise.toml");
|
|
2061
2226
|
const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2062
2227
|
const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2063
|
-
const
|
|
2064
|
-
if (!
|
|
2228
|
+
const text4 = safeReadText(misePath);
|
|
2229
|
+
if (!text4?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2065
2230
|
if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2066
2231
|
if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2067
2232
|
return {
|
|
@@ -2075,14 +2240,21 @@ var RULES = [
|
|
|
2075
2240
|
},
|
|
2076
2241
|
migrate: (ctx, finding) => {
|
|
2077
2242
|
const changedFiles = [];
|
|
2243
|
+
const details = [];
|
|
2078
2244
|
const misePath = join8(ctx.repoRoot, "mise.toml");
|
|
2079
2245
|
if (!existsSync7(misePath)) {
|
|
2080
|
-
|
|
2246
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2247
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
2248
|
+
}
|
|
2249
|
+
details.push("Initialized mise.toml from generated-project template");
|
|
2250
|
+
if (ctx.dryRun) {
|
|
2251
|
+
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
2252
|
+
}
|
|
2081
2253
|
}
|
|
2082
2254
|
const currentMise = readText(misePath);
|
|
2083
2255
|
const nextMise = replaceOrAppendManagedBlock(currentMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
2084
2256
|
if (nextMise !== currentMise) {
|
|
2085
|
-
changedFiles.push(misePath);
|
|
2257
|
+
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2086
2258
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2087
2259
|
}
|
|
2088
2260
|
const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
@@ -2222,7 +2394,8 @@ var RULES = [
|
|
|
2222
2394
|
} else {
|
|
2223
2395
|
const invalidLines = envOp.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).filter((line) => {
|
|
2224
2396
|
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
2225
|
-
|
|
2397
|
+
const quotedLiteral = /^"[^"\r\n]*"$/.test(value) || /^'[^'\r\n]*'$/.test(value);
|
|
2398
|
+
return !value.startsWith("op://") && !/^https?:\/\//.test(value) && !/^[A-Za-z0-9_.:-]+$/.test(value) && !quotedLiteral;
|
|
2226
2399
|
});
|
|
2227
2400
|
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals: ${invalidLines.join(", ")}`);
|
|
2228
2401
|
}
|
|
@@ -2274,19 +2447,19 @@ var RULES = [
|
|
|
2274
2447
|
audit: (ctx) => {
|
|
2275
2448
|
const details = [];
|
|
2276
2449
|
const path = join8(ctx.repoRoot, ".copier-answers.yml");
|
|
2277
|
-
const
|
|
2450
|
+
const text4 = safeReadText(path);
|
|
2278
2451
|
const project = readProjectJson(ctx);
|
|
2279
|
-
if (!
|
|
2452
|
+
if (!text4) {
|
|
2280
2453
|
details.push(".copier-answers.yml missing");
|
|
2281
2454
|
} else {
|
|
2282
|
-
if (!
|
|
2283
|
-
if (!
|
|
2455
|
+
if (!text4.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
|
|
2456
|
+
if (!text4.includes("_src_path:")) details.push("_src_path missing");
|
|
2284
2457
|
if (project?.project_name) {
|
|
2285
|
-
const nameMatch =
|
|
2458
|
+
const nameMatch = text4.match(/project_name:\s*(.+)/);
|
|
2286
2459
|
if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
|
|
2287
2460
|
}
|
|
2288
2461
|
if (project?.project_description) {
|
|
2289
|
-
const descMatch =
|
|
2462
|
+
const descMatch = text4.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
|
|
2290
2463
|
const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
|
|
2291
2464
|
if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
|
|
2292
2465
|
}
|
|
@@ -2303,16 +2476,16 @@ var RULES = [
|
|
|
2303
2476
|
migrate: (ctx, finding) => {
|
|
2304
2477
|
const changedFiles = [];
|
|
2305
2478
|
const project = canonicalProjectJson(ctx);
|
|
2306
|
-
const
|
|
2479
|
+
const text4 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2307
2480
|
_src_path: ${join8(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2308
2481
|
project_description: ${String(project.project_description)}
|
|
2309
2482
|
project_name: ${String(project.project_name)}
|
|
2310
2483
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2311
2484
|
`;
|
|
2312
2485
|
const path = join8(ctx.repoRoot, ".copier-answers.yml");
|
|
2313
|
-
if (safeReadText(path) !==
|
|
2486
|
+
if (safeReadText(path) !== text4) {
|
|
2314
2487
|
changedFiles.push(path);
|
|
2315
|
-
if (!ctx.dryRun) writeText(path,
|
|
2488
|
+
if (!ctx.dryRun) writeText(path, text4);
|
|
2316
2489
|
}
|
|
2317
2490
|
return {
|
|
2318
2491
|
id: finding.id,
|
|
@@ -2508,6 +2681,9 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
2508
2681
|
if (mode) chmodSync(path, mode);
|
|
2509
2682
|
}
|
|
2510
2683
|
}
|
|
2684
|
+
function getParityRuleIds() {
|
|
2685
|
+
return RULES.map((rule) => rule.id);
|
|
2686
|
+
}
|
|
2511
2687
|
function runAudit(repoArg) {
|
|
2512
2688
|
const pjanglerRoot = resolvePjanglerRoot();
|
|
2513
2689
|
const ctx = {
|
|
@@ -2524,7 +2700,7 @@ function runAudit(repoArg) {
|
|
|
2524
2700
|
rules
|
|
2525
2701
|
};
|
|
2526
2702
|
}
|
|
2527
|
-
function
|
|
2703
|
+
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
2528
2704
|
const pjanglerRoot = resolvePjanglerRoot();
|
|
2529
2705
|
const ctx = {
|
|
2530
2706
|
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
@@ -2532,9 +2708,9 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2532
2708
|
pjanglerRoot,
|
|
2533
2709
|
homeDir: homedir4()
|
|
2534
2710
|
};
|
|
2535
|
-
const selected =
|
|
2711
|
+
const selected = RULES.filter((rule) => ruleIds.includes(rule.id));
|
|
2536
2712
|
if (!selected.length) {
|
|
2537
|
-
throw new Error(`Unknown parity
|
|
2713
|
+
throw new Error(`Unknown parity rules: ${ruleIds.join(", ")}`);
|
|
2538
2714
|
}
|
|
2539
2715
|
const results = selected.map((rule) => {
|
|
2540
2716
|
try {
|
|
@@ -2560,6 +2736,10 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2560
2736
|
changedFiles
|
|
2561
2737
|
};
|
|
2562
2738
|
}
|
|
2739
|
+
function runMigration(selector, repoArg, dryRun, all) {
|
|
2740
|
+
const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
|
|
2741
|
+
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
2742
|
+
}
|
|
2563
2743
|
function formatAuditReport(report) {
|
|
2564
2744
|
const lines = [`repo: ${report.repo}`, `ok: ${report.ok}`, `audited_at: ${report.auditedAt}`, "rules:"];
|
|
2565
2745
|
for (const rule of report.rules) {
|
|
@@ -2584,19 +2764,421 @@ function formatMigrationReport(report) {
|
|
|
2584
2764
|
`;
|
|
2585
2765
|
}
|
|
2586
2766
|
|
|
2767
|
+
// src/project/index.ts
|
|
2768
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2769
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2770
|
+
import { homedir as homedir5 } from "node:os";
|
|
2771
|
+
import { basename as basename3, dirname as dirname6, join as join9, resolve as resolve2 } from "node:path";
|
|
2772
|
+
import YAML from "yaml";
|
|
2773
|
+
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
2774
|
+
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
2775
|
+
var KNOWN_SKILL_ROOTS = [
|
|
2776
|
+
"/home/delorenj/code/skillex/all-skills",
|
|
2777
|
+
"/home/delorenj/code/CoachingAgentFramework/.agents/skills",
|
|
2778
|
+
"/home/delorenj/code/pjangler/.agents/skills",
|
|
2779
|
+
join9(homedir5(), ".codex", "skills")
|
|
2780
|
+
];
|
|
2781
|
+
function projectRegistryPath(env = process.env) {
|
|
2782
|
+
return expandHome(env[PROJECT_REGISTRY_ENV] || join9(homedir5(), ".config", "pjangler", "projects.yaml"));
|
|
2783
|
+
}
|
|
2784
|
+
function emptyProjectRegistry() {
|
|
2785
|
+
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
2786
|
+
}
|
|
2787
|
+
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
2788
|
+
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
2789
|
+
const raw = YAML.parse(readFileSync4(path, "utf8"));
|
|
2790
|
+
if (raw == null) return emptyProjectRegistry();
|
|
2791
|
+
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
2792
|
+
const registry = raw;
|
|
2793
|
+
const normalized = {
|
|
2794
|
+
schema_version: Number(registry.schema_version ?? PROJECT_REGISTRY_SCHEMA_VERSION),
|
|
2795
|
+
projects: isRecord(registry.projects) ? registry.projects : {}
|
|
2796
|
+
};
|
|
2797
|
+
validateProjectRegistry(normalized);
|
|
2798
|
+
return normalized;
|
|
2799
|
+
}
|
|
2800
|
+
function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
2801
|
+
validateProjectRegistry(registry);
|
|
2802
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
2803
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
2804
|
+
writeFileSync5(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
2805
|
+
renameSync2(temp, path);
|
|
2806
|
+
}
|
|
2807
|
+
function validateProjectRegistry(registry) {
|
|
2808
|
+
if (registry.schema_version !== PROJECT_REGISTRY_SCHEMA_VERSION) {
|
|
2809
|
+
throw new Error(`Unsupported project registry schema_version: ${registry.schema_version}`);
|
|
2810
|
+
}
|
|
2811
|
+
if (!isRecord(registry.projects)) throw new Error("Project registry projects must be a mapping");
|
|
2812
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2813
|
+
const repoPaths = /* @__PURE__ */ new Map();
|
|
2814
|
+
const identifiers = /* @__PURE__ */ new Map();
|
|
2815
|
+
for (const [slug, project] of Object.entries(registry.projects)) {
|
|
2816
|
+
validateProjectRecord(project, slug);
|
|
2817
|
+
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
2818
|
+
slugs.add(project.slug);
|
|
2819
|
+
const repoKey = resolve2(project.repo_path);
|
|
2820
|
+
const existingRepoSlug = repoPaths.get(repoKey);
|
|
2821
|
+
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
2822
|
+
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
2823
|
+
}
|
|
2824
|
+
repoPaths.set(repoKey, slug);
|
|
2825
|
+
const identifier = project.ticket_provider.identifier?.toUpperCase();
|
|
2826
|
+
if (identifier) {
|
|
2827
|
+
const existingIdentifierSlug = identifiers.get(identifier);
|
|
2828
|
+
if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
|
|
2829
|
+
throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
|
|
2830
|
+
}
|
|
2831
|
+
identifiers.set(identifier, slug);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
function slugifyProjectName(value) {
|
|
2836
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
2837
|
+
}
|
|
2838
|
+
function deriveProjectIdentifier(value) {
|
|
2839
|
+
const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
2840
|
+
const identifier = compact.slice(0, 4) || "PROJ";
|
|
2841
|
+
return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
|
|
2842
|
+
}
|
|
2843
|
+
function normalizeAgentRole(value) {
|
|
2844
|
+
return value?.trim() || "pm";
|
|
2845
|
+
}
|
|
2846
|
+
function jsonStable(value) {
|
|
2847
|
+
return JSON.stringify(value);
|
|
2848
|
+
}
|
|
2849
|
+
function projectRecordEquivalent(a, b) {
|
|
2850
|
+
if (!a) return false;
|
|
2851
|
+
const { created_at: _aCreated, updated_at: _aUpdated, ...aComparable } = a;
|
|
2852
|
+
const { created_at: _bCreated, updated_at: _bUpdated, ...bComparable } = b;
|
|
2853
|
+
return jsonStable(aComparable) === jsonStable(bComparable);
|
|
2854
|
+
}
|
|
2855
|
+
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
2856
|
+
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
2857
|
+
return resolve2(dirname6(resolve2(cwd)), compactName);
|
|
2858
|
+
}
|
|
2859
|
+
function resolveSourceSkillPath(sourceSkill) {
|
|
2860
|
+
if (!sourceSkill) return void 0;
|
|
2861
|
+
const expanded = expandHome(sourceSkill);
|
|
2862
|
+
const direct = resolve2(expanded);
|
|
2863
|
+
if (existsSync8(direct)) return direct;
|
|
2864
|
+
const name = basename3(sourceSkill);
|
|
2865
|
+
for (const root of KNOWN_SKILL_ROOTS) {
|
|
2866
|
+
const candidate = join9(root, name);
|
|
2867
|
+
if (existsSync8(candidate)) return candidate;
|
|
2868
|
+
}
|
|
2869
|
+
const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
|
|
2870
|
+
const hint = existsSync8(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
|
|
2871
|
+
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
2872
|
+
}
|
|
2873
|
+
function planProjectInit(input) {
|
|
2874
|
+
if (!input.name.trim()) throw new Error("Project name is required");
|
|
2875
|
+
const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
2876
|
+
const registry = loadProjectRegistry(registryPath2);
|
|
2877
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2878
|
+
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
2879
|
+
const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
2880
|
+
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
2881
|
+
const existing = registry.projects[slug];
|
|
2882
|
+
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
2883
|
+
const overwrite = input.overwrite ?? input.force ?? false;
|
|
2884
|
+
const agentRole = normalizeAgentRole(input.agentRole);
|
|
2885
|
+
const agents = input.provisionAgent ? {
|
|
2886
|
+
...existing?.agents ?? {},
|
|
2887
|
+
[agentRole]: {
|
|
2888
|
+
role: agentRole,
|
|
2889
|
+
provisioning_state: "planned"
|
|
2890
|
+
}
|
|
2891
|
+
} : existing?.agents ?? {};
|
|
2892
|
+
const scaffold = input.scaffold ?? true;
|
|
2893
|
+
const candidateProject = {
|
|
2894
|
+
name: input.name,
|
|
2895
|
+
slug,
|
|
2896
|
+
repo_path: targetDir,
|
|
2897
|
+
description: input.description ?? "",
|
|
2898
|
+
status: "planned",
|
|
2899
|
+
source_artifacts: sourceSkillPath ? [{ kind: "skill", path: sourceSkillPath, package_name: input.packageName ?? slug }] : [],
|
|
2900
|
+
template: {
|
|
2901
|
+
commonproject: {
|
|
2902
|
+
enabled: true,
|
|
2903
|
+
primary_language: input.primaryLanguage ?? "python"
|
|
2904
|
+
}
|
|
2905
|
+
},
|
|
2906
|
+
ticket_provider: {
|
|
2907
|
+
type: input.ticketProvider ?? "plane",
|
|
2908
|
+
workspace: input.planeWorkspace ?? "33god",
|
|
2909
|
+
identifier,
|
|
2910
|
+
board_id: input.planeProjectId ?? "",
|
|
2911
|
+
board_url: input.planeProjectId ? `https://plane.delo.sh/${input.planeWorkspace ?? "33god"}/projects/${input.planeProjectId}/issues/` : "",
|
|
2912
|
+
state: input.live ? "planned" : "planned"
|
|
2913
|
+
},
|
|
2914
|
+
agents,
|
|
2915
|
+
created_at: existing?.created_at ?? now,
|
|
2916
|
+
updated_at: now
|
|
2917
|
+
};
|
|
2918
|
+
const project = {
|
|
2919
|
+
...candidateProject,
|
|
2920
|
+
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
2921
|
+
};
|
|
2922
|
+
validateNoDuplicateProject(registry, project, overwrite);
|
|
2923
|
+
const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot2());
|
|
2924
|
+
const manifest = projectManifestFromRegistryProject(project);
|
|
2925
|
+
const apply = input.apply ?? false;
|
|
2926
|
+
const live = input.live ?? false;
|
|
2927
|
+
const actions = [
|
|
2928
|
+
{ kind: "registry.upsert", registryPath: registryPath2, slug, project }
|
|
2929
|
+
];
|
|
2930
|
+
if (scaffold) {
|
|
2931
|
+
actions.push(buildCommonProjectCopierAction({
|
|
2932
|
+
pjanglerRoot,
|
|
2933
|
+
targetDir,
|
|
2934
|
+
projectName: project.name,
|
|
2935
|
+
projectDescription: project.description,
|
|
2936
|
+
projectSlug: project.slug,
|
|
2937
|
+
ticketProvider: project.ticket_provider.type,
|
|
2938
|
+
planeWorkspace: project.ticket_provider.workspace ?? "33god",
|
|
2939
|
+
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
2940
|
+
projectIdentifier: identifier,
|
|
2941
|
+
primaryLanguage: project.template.commonproject.primary_language,
|
|
2942
|
+
overwrite
|
|
2943
|
+
}));
|
|
2944
|
+
}
|
|
2945
|
+
actions.push(
|
|
2946
|
+
{ kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
|
|
2947
|
+
{
|
|
2948
|
+
kind: "plane.create-or-link",
|
|
2949
|
+
enabled: live,
|
|
2950
|
+
live,
|
|
2951
|
+
workspace: project.ticket_provider.workspace ?? "33god",
|
|
2952
|
+
identifier,
|
|
2953
|
+
state: live ? "planned" : "planned",
|
|
2954
|
+
reason: live ? void 0 : "network/cloud actions require --live"
|
|
2955
|
+
},
|
|
2956
|
+
{
|
|
2957
|
+
kind: "hermes.provision-agent",
|
|
2958
|
+
enabled: input.provisionAgent ?? false,
|
|
2959
|
+
local: !live,
|
|
2960
|
+
targetDir,
|
|
2961
|
+
targetRepo: slug,
|
|
2962
|
+
role: agentRole,
|
|
2963
|
+
context: {
|
|
2964
|
+
skipRuntimeRepo: !live,
|
|
2965
|
+
skipPlane: !live,
|
|
2966
|
+
skipBloodbank: !live,
|
|
2967
|
+
skipSystemd: !live || process.platform === "darwin"
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
);
|
|
2971
|
+
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
2972
|
+
}
|
|
2973
|
+
function executeProjectInitPlan(plan) {
|
|
2974
|
+
const logs = [];
|
|
2975
|
+
const errors = [];
|
|
2976
|
+
const changedFiles = [];
|
|
2977
|
+
if (!plan.apply) return { ok: true, plan, logs, errors, changedFiles };
|
|
2978
|
+
const registry = loadProjectRegistry(plan.registryPath);
|
|
2979
|
+
let pendingRegistryAction;
|
|
2980
|
+
for (const action of plan.actions) {
|
|
2981
|
+
if (action.kind === "copier.copy.commonproject") {
|
|
2982
|
+
mkdirSync6(dirname6(action.targetDir), { recursive: true });
|
|
2983
|
+
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
2984
|
+
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
2985
|
+
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
2986
|
+
if (result.error) {
|
|
2987
|
+
const code = result.error.code;
|
|
2988
|
+
errors.push(
|
|
2989
|
+
code === "ENOENT" ? "copier not found on PATH. Install with: uv tool install copier or pip install copier" : `copier failed: ${result.error.message}`
|
|
2990
|
+
);
|
|
2991
|
+
break;
|
|
2992
|
+
}
|
|
2993
|
+
if (result.status !== 0) {
|
|
2994
|
+
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
2995
|
+
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
2996
|
+
break;
|
|
2997
|
+
}
|
|
2998
|
+
changedFiles.push(action.targetDir);
|
|
2999
|
+
} else if (action.kind === "project.write-manifest") {
|
|
3000
|
+
mkdirSync6(dirname6(action.path), { recursive: true });
|
|
3001
|
+
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
3002
|
+
`;
|
|
3003
|
+
const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
|
|
3004
|
+
if (current !== next) {
|
|
3005
|
+
writeFileSync5(action.path, next, "utf8");
|
|
3006
|
+
changedFiles.push(action.path);
|
|
3007
|
+
}
|
|
3008
|
+
} else if (action.kind === "registry.upsert") {
|
|
3009
|
+
pendingRegistryAction = action;
|
|
3010
|
+
} else if (action.kind === "plane.create-or-link") {
|
|
3011
|
+
logs.push(action.enabled ? "plane.create-or-link requires a live provider integration" : "plane.create-or-link skipped (requires --live)");
|
|
3012
|
+
} else if (action.kind === "hermes.provision-agent") {
|
|
3013
|
+
logs.push(action.enabled ? "hermes.provision-agent planned for the caller to execute" : "hermes.provision-agent skipped");
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
if (pendingRegistryAction && errors.length === 0) {
|
|
3017
|
+
if (!projectRecordEquivalent(registry.projects[pendingRegistryAction.slug], pendingRegistryAction.project)) {
|
|
3018
|
+
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
3019
|
+
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
3020
|
+
changedFiles.push(pendingRegistryAction.registryPath);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
3024
|
+
}
|
|
3025
|
+
function projectManifestFromRegistryProject(project) {
|
|
3026
|
+
const agents = Object.fromEntries(
|
|
3027
|
+
Object.entries(project.agents).map(([name, agent]) => [
|
|
3028
|
+
`${project.slug}-${name}`,
|
|
3029
|
+
{
|
|
3030
|
+
role: agent.role,
|
|
3031
|
+
role_dir: agent.role_dir,
|
|
3032
|
+
provisioning_state: agent.provisioning_state
|
|
3033
|
+
}
|
|
3034
|
+
])
|
|
3035
|
+
);
|
|
3036
|
+
return {
|
|
3037
|
+
project_name: project.name,
|
|
3038
|
+
project_description: project.description,
|
|
3039
|
+
project_slug: project.slug,
|
|
3040
|
+
repo_path: project.repo_path,
|
|
3041
|
+
ticket_provider: {
|
|
3042
|
+
type: project.ticket_provider.type,
|
|
3043
|
+
workspace: project.ticket_provider.workspace ?? "",
|
|
3044
|
+
identifier: project.ticket_provider.identifier ?? "",
|
|
3045
|
+
board_id: project.ticket_provider.board_id ?? "",
|
|
3046
|
+
board_url: project.ticket_provider.board_url ?? "",
|
|
3047
|
+
state: project.ticket_provider.state
|
|
3048
|
+
},
|
|
3049
|
+
agents
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
function formatProjectInitPlan(plan) {
|
|
3053
|
+
const lines = [
|
|
3054
|
+
`${plan.dryRun ? "[DRY RUN] " : ""}Project init plan: ${plan.project.name} (${plan.project.slug})`,
|
|
3055
|
+
`Registry: ${plan.registryPath}`,
|
|
3056
|
+
`Target: ${plan.project.repo_path}`,
|
|
3057
|
+
"Actions:"
|
|
3058
|
+
];
|
|
3059
|
+
for (const action of plan.actions) {
|
|
3060
|
+
lines.push(` - ${action.kind}`);
|
|
3061
|
+
if (action.kind === "copier.copy.commonproject") lines.push(` target: ${action.targetDir}`);
|
|
3062
|
+
if (action.kind === "project.write-manifest") lines.push(` path: ${action.path}`);
|
|
3063
|
+
if (action.kind === "plane.create-or-link" && action.reason) lines.push(` note: ${action.reason}`);
|
|
3064
|
+
}
|
|
3065
|
+
return lines.join("\n");
|
|
3066
|
+
}
|
|
3067
|
+
function formatProjectList(registry) {
|
|
3068
|
+
const projects = Object.values(registry.projects).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
3069
|
+
if (!projects.length) return "No projects registered.";
|
|
3070
|
+
return projects.map((project) => `${project.slug.padEnd(18)} ${String(project.ticket_provider.identifier ?? "").padEnd(6)} ${project.status.padEnd(8)} ${project.repo_path}`).join("\n");
|
|
3071
|
+
}
|
|
3072
|
+
function getProject(registry, slug) {
|
|
3073
|
+
const project = registry.projects[slug];
|
|
3074
|
+
if (!project) throw new Error(`Project not found in registry: ${slug}`);
|
|
3075
|
+
return project;
|
|
3076
|
+
}
|
|
3077
|
+
function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
3078
|
+
const issues = [];
|
|
3079
|
+
const registry = loadProjectRegistry(registryPath2);
|
|
3080
|
+
const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
|
|
3081
|
+
for (const [projectSlug, project] of projects) {
|
|
3082
|
+
if (!existsSync8(project.repo_path)) {
|
|
3083
|
+
issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
|
|
3084
|
+
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
3085
|
+
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
3086
|
+
} else {
|
|
3087
|
+
const manifestPath = join9(project.repo_path, ".project.json");
|
|
3088
|
+
if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
3089
|
+
}
|
|
3090
|
+
for (const artifact of project.source_artifacts) {
|
|
3091
|
+
if (artifact.path && !existsSync8(artifact.path)) {
|
|
3092
|
+
issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
return {
|
|
3097
|
+
ok: !issues.some((issue) => issue.level === "error"),
|
|
3098
|
+
registryPath: registryPath2,
|
|
3099
|
+
checkedProjects: projects.map(([projectSlug]) => projectSlug),
|
|
3100
|
+
issues
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
function buildCommonProjectCopierAction(input) {
|
|
3104
|
+
const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
|
|
3105
|
+
const data = {
|
|
3106
|
+
project_name: input.projectName,
|
|
3107
|
+
project_description: input.projectDescription ?? "",
|
|
3108
|
+
project_slug: input.projectSlug,
|
|
3109
|
+
ticket_provider: input.ticketProvider,
|
|
3110
|
+
plane_workspace: input.planeWorkspace,
|
|
3111
|
+
plane_project_id: input.planeProjectId ?? "",
|
|
3112
|
+
project_identifier: input.projectIdentifier,
|
|
3113
|
+
primary_language: input.primaryLanguage
|
|
3114
|
+
};
|
|
3115
|
+
const command = ["copier", "copy", "--trust", templateDir, input.targetDir, "--defaults"];
|
|
3116
|
+
for (const [key, value] of Object.entries(data)) command.push("--data", `${key}=${value}`);
|
|
3117
|
+
if (input.overwrite) command.push("--overwrite");
|
|
3118
|
+
return {
|
|
3119
|
+
kind: "copier.copy.commonproject",
|
|
3120
|
+
cwd: input.pjanglerRoot,
|
|
3121
|
+
command,
|
|
3122
|
+
targetDir: input.targetDir,
|
|
3123
|
+
data,
|
|
3124
|
+
overwrite: input.overwrite
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
function resolvePjanglerRoot2() {
|
|
3128
|
+
let dir = dirname6(new URL(import.meta.url).pathname);
|
|
3129
|
+
while (dir !== dirname6(dir)) {
|
|
3130
|
+
if (existsSync8(join9(dir, "package.json")) && existsSync8(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
3131
|
+
dir = dirname6(dir);
|
|
3132
|
+
}
|
|
3133
|
+
return resolve2(process.cwd());
|
|
3134
|
+
}
|
|
3135
|
+
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
3136
|
+
const existingSameSlug = registry.projects[project.slug];
|
|
3137
|
+
if (existingSameSlug && !overwrite && resolve2(existingSameSlug.repo_path) !== resolve2(project.repo_path)) {
|
|
3138
|
+
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
3139
|
+
}
|
|
3140
|
+
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
3141
|
+
if (slug === project.slug) continue;
|
|
3142
|
+
if (resolve2(existing.repo_path) === resolve2(project.repo_path)) {
|
|
3143
|
+
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
3144
|
+
}
|
|
3145
|
+
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
3146
|
+
throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
function validateProjectRecord(project, key) {
|
|
3151
|
+
if (!isRecord(project)) throw new Error(`Project ${key} must be a mapping`);
|
|
3152
|
+
if (!project.name) throw new Error(`Project ${key} missing name`);
|
|
3153
|
+
if (!project.slug) throw new Error(`Project ${key} missing slug`);
|
|
3154
|
+
if (project.slug !== key) throw new Error(`Project key ${key} does not match slug ${project.slug}`);
|
|
3155
|
+
if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
|
|
3156
|
+
if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
|
|
3157
|
+
if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
|
|
3158
|
+
if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
|
|
3159
|
+
}
|
|
3160
|
+
function expandHome(path) {
|
|
3161
|
+
if (path === "~") return homedir5();
|
|
3162
|
+
if (path.startsWith("~/")) return join9(homedir5(), path.slice(2));
|
|
3163
|
+
return path;
|
|
3164
|
+
}
|
|
3165
|
+
function isRecord(value) {
|
|
3166
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3167
|
+
}
|
|
3168
|
+
|
|
2587
3169
|
// src/utils/version.ts
|
|
2588
|
-
import { readFileSync as
|
|
2589
|
-
import { dirname as
|
|
3170
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3171
|
+
import { dirname as dirname7, join as join10 } from "node:path";
|
|
2590
3172
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2591
3173
|
var PJANGLER_VERSION = (() => {
|
|
2592
3174
|
try {
|
|
2593
|
-
let dir =
|
|
3175
|
+
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2594
3176
|
for (let i = 0; i < 4; i++) {
|
|
2595
3177
|
try {
|
|
2596
|
-
const raw =
|
|
3178
|
+
const raw = readFileSync5(join10(dir, "package.json"), "utf8");
|
|
2597
3179
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
2598
3180
|
} catch {
|
|
2599
|
-
const parent =
|
|
3181
|
+
const parent = dirname7(dir);
|
|
2600
3182
|
if (parent === dir) break;
|
|
2601
3183
|
dir = parent;
|
|
2602
3184
|
}
|
|
@@ -2607,6 +3189,190 @@ var PJANGLER_VERSION = (() => {
|
|
|
2607
3189
|
})();
|
|
2608
3190
|
|
|
2609
3191
|
// src/index.ts
|
|
3192
|
+
function printMigrationReport(report, asJson) {
|
|
3193
|
+
if (asJson) {
|
|
3194
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3195
|
+
} else {
|
|
3196
|
+
console.log(formatMigrationReport(report));
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
async function promptForRuleIds(rules) {
|
|
3200
|
+
const options = rules.filter((rule) => rule.fixable).map((rule) => ({
|
|
3201
|
+
value: rule.id,
|
|
3202
|
+
label: `${rule.id} [${rule.status}] ${rule.title}`,
|
|
3203
|
+
hint: rule.summary
|
|
3204
|
+
}));
|
|
3205
|
+
if (!options.length) {
|
|
3206
|
+
return [];
|
|
3207
|
+
}
|
|
3208
|
+
const initialValues = rules.filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => rule.id);
|
|
3209
|
+
const selected = await multiselect({
|
|
3210
|
+
message: "Select parity rules to apply (space to toggle, enter to confirm):",
|
|
3211
|
+
options,
|
|
3212
|
+
initialValues
|
|
3213
|
+
});
|
|
3214
|
+
if (isCancel5(selected)) {
|
|
3215
|
+
return [];
|
|
3216
|
+
}
|
|
3217
|
+
return selected;
|
|
3218
|
+
}
|
|
3219
|
+
function readJson(path) {
|
|
3220
|
+
if (!existsSync9(path)) return void 0;
|
|
3221
|
+
try {
|
|
3222
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
3223
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
3224
|
+
} catch {
|
|
3225
|
+
return void 0;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
function findGitRoot(cwd) {
|
|
3229
|
+
const result = spawnSync6("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
3230
|
+
if (result.status !== 0) return void 0;
|
|
3231
|
+
return resolve3(result.stdout.trim());
|
|
3232
|
+
}
|
|
3233
|
+
function packageNameToProjectName(value) {
|
|
3234
|
+
if (!value) return void 0;
|
|
3235
|
+
const name = value.split("/").pop() ?? value;
|
|
3236
|
+
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
3237
|
+
}
|
|
3238
|
+
function deriveProjectDefaults(targetDir) {
|
|
3239
|
+
const manifest = readJson(join11(targetDir, ".project.json"));
|
|
3240
|
+
const pkg = readJson(join11(targetDir, "package.json"));
|
|
3241
|
+
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
|
|
3242
|
+
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
3243
|
+
return {
|
|
3244
|
+
name,
|
|
3245
|
+
description: String(manifest?.project_description ?? pkg?.description ?? ""),
|
|
3246
|
+
slug: typeof manifest?.project_slug === "string" ? manifest.project_slug : void 0,
|
|
3247
|
+
identifier: typeof ticketProvider.identifier === "string" ? ticketProvider.identifier : void 0
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
function isInteractiveProjectInit(options) {
|
|
3251
|
+
return !options.json && !options.yes && options.tui !== false && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
3252
|
+
}
|
|
3253
|
+
async function promptTextValue(message, initialValue) {
|
|
3254
|
+
const value = await text3({
|
|
3255
|
+
message,
|
|
3256
|
+
initialValue,
|
|
3257
|
+
validate: (input) => input?.trim() ? void 0 : "Required"
|
|
3258
|
+
});
|
|
3259
|
+
if (isCancel5(value)) {
|
|
3260
|
+
cancel2("project init cancelled");
|
|
3261
|
+
process.exit(1);
|
|
3262
|
+
}
|
|
3263
|
+
return value.trim();
|
|
3264
|
+
}
|
|
3265
|
+
function projectInitActionLabel(kind) {
|
|
3266
|
+
switch (kind) {
|
|
3267
|
+
case "registry.upsert":
|
|
3268
|
+
return "Register/update project registry entry";
|
|
3269
|
+
case "copier.copy.commonproject":
|
|
3270
|
+
return "Render CommonProject scaffold";
|
|
3271
|
+
case "project.write-manifest":
|
|
3272
|
+
return "Write repo-local .project.json projection";
|
|
3273
|
+
case "plane.create-or-link":
|
|
3274
|
+
return "Create/link ticket provider project";
|
|
3275
|
+
case "hermes.provision-agent":
|
|
3276
|
+
return "Provision Hermes agent";
|
|
3277
|
+
default:
|
|
3278
|
+
return kind;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
function registryNeedsUpsert(plan) {
|
|
3282
|
+
const registry = loadProjectRegistry(plan.registryPath);
|
|
3283
|
+
const existing = registry.projects[plan.project.slug];
|
|
3284
|
+
if (!existing) return true;
|
|
3285
|
+
const { created_at: _existingCreated, updated_at: _existingUpdated, ...existingComparable } = existing;
|
|
3286
|
+
const { created_at: _projectCreated, updated_at: _projectUpdated, ...projectComparable } = plan.project;
|
|
3287
|
+
return JSON.stringify(existingComparable) !== JSON.stringify(projectComparable);
|
|
3288
|
+
}
|
|
3289
|
+
function actionNeedsRun(plan, kind, syncMode) {
|
|
3290
|
+
if (kind === "registry.upsert") return registryNeedsUpsert(plan);
|
|
3291
|
+
if (kind === "project.write-manifest") {
|
|
3292
|
+
const action = plan.actions.find((item) => item.kind === "project.write-manifest");
|
|
3293
|
+
if (!action || action.kind !== "project.write-manifest") return false;
|
|
3294
|
+
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
3295
|
+
`;
|
|
3296
|
+
return !existsSync9(action.path) || readFileSync6(action.path, "utf8") !== next;
|
|
3297
|
+
}
|
|
3298
|
+
if (kind === "copier.copy.commonproject") return true;
|
|
3299
|
+
if (kind === "plane.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
3300
|
+
if (kind === "hermes.provision-agent") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
3301
|
+
return true;
|
|
3302
|
+
}
|
|
3303
|
+
async function selectProjectInitOperations(input) {
|
|
3304
|
+
const planOperations = input.plan.actions.filter((action) => actionNeedsRun(input.plan, action.kind, input.syncMode)).map((action) => ({
|
|
3305
|
+
value: action.kind,
|
|
3306
|
+
label: projectInitActionLabel(action.kind),
|
|
3307
|
+
hint: action.kind === "registry.upsert" ? input.plan.registryPath : action.kind
|
|
3308
|
+
}));
|
|
3309
|
+
const parityOperations = input.auditRules.filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => ({
|
|
3310
|
+
value: `parity:${rule.id}`,
|
|
3311
|
+
label: `${rule.title}`,
|
|
3312
|
+
hint: `${rule.id}: ${rule.summary}`
|
|
3313
|
+
}));
|
|
3314
|
+
const operations = [...planOperations, ...parityOperations];
|
|
3315
|
+
const all = operations.map((operation) => operation.value);
|
|
3316
|
+
if (input.options.yes || input.options.apply && !isInteractiveProjectInit(input.options)) {
|
|
3317
|
+
return {
|
|
3318
|
+
selectedOperations: all,
|
|
3319
|
+
selectedParityRules: parityOperations.map((operation) => operation.value.replace(/^parity:/, ""))
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
if (input.options.dryRun || !isInteractiveProjectInit(input.options)) {
|
|
3323
|
+
return { selectedOperations: [], selectedParityRules: [] };
|
|
3324
|
+
}
|
|
3325
|
+
if (!operations.length) return { selectedOperations: [], selectedParityRules: [] };
|
|
3326
|
+
const selected = await multiselect({
|
|
3327
|
+
message: "Select project init operations to run:",
|
|
3328
|
+
options: operations,
|
|
3329
|
+
initialValues: all
|
|
3330
|
+
});
|
|
3331
|
+
if (isCancel5(selected)) {
|
|
3332
|
+
cancel2("project init cancelled");
|
|
3333
|
+
process.exit(1);
|
|
3334
|
+
}
|
|
3335
|
+
return {
|
|
3336
|
+
selectedOperations: selected,
|
|
3337
|
+
selectedParityRules: selected.filter((value) => value.startsWith("parity:")).map((value) => value.replace(/^parity:/, ""))
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
async function resolveProjectInitTarget(name, options) {
|
|
3341
|
+
const interactive = isInteractiveProjectInit(options);
|
|
3342
|
+
const cwd = process.cwd();
|
|
3343
|
+
const cwdGitRoot = findGitRoot(cwd);
|
|
3344
|
+
let targetDir = options.targetDir ? resolve3(options.targetDir) : void 0;
|
|
3345
|
+
if (!targetDir && cwdGitRoot) {
|
|
3346
|
+
targetDir = cwdGitRoot;
|
|
3347
|
+
}
|
|
3348
|
+
if (!targetDir && interactive) {
|
|
3349
|
+
const defaultName = name ?? basename4(cwd);
|
|
3350
|
+
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
3351
|
+
const defaultDir = join11(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3352
|
+
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
3353
|
+
name = promptedName;
|
|
3354
|
+
}
|
|
3355
|
+
if (!targetDir) {
|
|
3356
|
+
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
3357
|
+
targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3358
|
+
}
|
|
3359
|
+
const targetExists = existsSync9(targetDir);
|
|
3360
|
+
if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
3361
|
+
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
3362
|
+
const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));
|
|
3363
|
+
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename4(targetDir)) ?? "Project", description: "" };
|
|
3364
|
+
if (!name && interactive && !syncMode) {
|
|
3365
|
+
name = await promptTextValue("Project name", defaults.name);
|
|
3366
|
+
}
|
|
3367
|
+
return {
|
|
3368
|
+
name: name ?? defaults.name,
|
|
3369
|
+
targetDir,
|
|
3370
|
+
description: options.description ?? defaults.description,
|
|
3371
|
+
syncMode,
|
|
3372
|
+
slug: options.slug ?? defaults.slug,
|
|
3373
|
+
identifier: options.identifier ?? defaults.identifier
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
2610
3376
|
var program = new Command3();
|
|
2611
3377
|
program.name("pjangler").description("Project subsystem bootstrapper CLI").version(PJANGLER_VERSION);
|
|
2612
3378
|
program.command("init").argument("<subsystem>", "Subsystem to initialize").description("Initialize a project subsystem").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Overwrite existing files").action(async (subsystem, options) => {
|
|
@@ -2640,6 +3406,144 @@ program.command("list").description("List available subsystems").action(() => {
|
|
|
2640
3406
|
console.log(" pjangler init docker");
|
|
2641
3407
|
console.log(" pjangler init node");
|
|
2642
3408
|
});
|
|
3409
|
+
var projectCmd = program.command("project").description("Manage the pjangler project registry");
|
|
3410
|
+
projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
|
|
3411
|
+
try {
|
|
3412
|
+
const target = await resolveProjectInitTarget(name, options);
|
|
3413
|
+
const interactive = isInteractiveProjectInit(options);
|
|
3414
|
+
const apply = Boolean(!options.dryRun && (options.yes || options.apply || interactive));
|
|
3415
|
+
const plan = planProjectInit({
|
|
3416
|
+
name: target.name,
|
|
3417
|
+
description: target.description,
|
|
3418
|
+
targetDir: target.targetDir,
|
|
3419
|
+
sourceSkill: options.sourceSkill,
|
|
3420
|
+
primaryLanguage: options.primaryLanguage,
|
|
3421
|
+
provisionAgent: options.provisionAgent ?? false,
|
|
3422
|
+
agentRole: options.agentRole,
|
|
3423
|
+
apply,
|
|
3424
|
+
live: options.live ?? false,
|
|
3425
|
+
projectSlug: target.slug,
|
|
3426
|
+
projectIdentifier: target.identifier,
|
|
3427
|
+
registryPath: options.registry,
|
|
3428
|
+
force: options.force ?? false,
|
|
3429
|
+
overwrite: options.force ?? false,
|
|
3430
|
+
cwd: process.cwd(),
|
|
3431
|
+
scaffold: !target.syncMode
|
|
3432
|
+
});
|
|
3433
|
+
const audit = target.syncMode ? runAudit(target.targetDir) : void 0;
|
|
3434
|
+
const selection = await selectProjectInitOperations({
|
|
3435
|
+
plan,
|
|
3436
|
+
auditRules: audit?.rules ?? [],
|
|
3437
|
+
syncMode: target.syncMode,
|
|
3438
|
+
options
|
|
3439
|
+
});
|
|
3440
|
+
const selectedPlanActionKinds = new Set(selection.selectedOperations.filter((value) => !value.startsWith("parity:")));
|
|
3441
|
+
const selectedPlan = {
|
|
3442
|
+
...plan,
|
|
3443
|
+
apply,
|
|
3444
|
+
dryRun: !apply,
|
|
3445
|
+
actions: apply ? plan.actions.filter((action) => selectedPlanActionKinds.has(action.kind)) : plan.actions
|
|
3446
|
+
};
|
|
3447
|
+
if (!apply) {
|
|
3448
|
+
const payload = {
|
|
3449
|
+
...plan,
|
|
3450
|
+
mode: target.syncMode ? "sync" : "create",
|
|
3451
|
+
audit,
|
|
3452
|
+
proposedOperations: [
|
|
3453
|
+
...plan.actions.filter((action) => actionNeedsRun(plan, action.kind, target.syncMode)).map((action) => action.kind),
|
|
3454
|
+
...(audit?.rules ?? []).filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => `parity:${rule.id}`)
|
|
3455
|
+
]
|
|
3456
|
+
};
|
|
3457
|
+
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
3458
|
+
else {
|
|
3459
|
+
console.log(formatProjectInitPlan(plan));
|
|
3460
|
+
if (payload.proposedOperations.length) {
|
|
3461
|
+
console.log("Proposed sync operations:");
|
|
3462
|
+
for (const operation of payload.proposedOperations) console.log(` - ${operation}`);
|
|
3463
|
+
} else {
|
|
3464
|
+
console.log("Project is already in parity.");
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
const initResult = selectedPlan.actions.length ? executeProjectInitPlan(selectedPlan) : { ok: true, plan: selectedPlan, logs: [], errors: [], changedFiles: [] };
|
|
3470
|
+
const migrationReport = selection.selectedParityRules.length ? runMigrationForRules(selection.selectedParityRules, target.targetDir, false) : void 0;
|
|
3471
|
+
const migrationErrors = migrationReport?.results.filter((result2) => result2.status === "blocked").map((result2) => `${result2.id}: ${result2.summary}`) ?? [];
|
|
3472
|
+
const changedFiles = Array.from(/* @__PURE__ */ new Set([
|
|
3473
|
+
...initResult.changedFiles,
|
|
3474
|
+
...migrationReport?.changedFiles ?? []
|
|
3475
|
+
])).sort();
|
|
3476
|
+
const result = {
|
|
3477
|
+
ok: initResult.ok && (migrationReport?.ok ?? true),
|
|
3478
|
+
mode: target.syncMode ? "sync" : "create",
|
|
3479
|
+
plan: selectedPlan,
|
|
3480
|
+
audit,
|
|
3481
|
+
selectedOperations: selection.selectedOperations,
|
|
3482
|
+
selectedParityRules: selection.selectedParityRules,
|
|
3483
|
+
logs: initResult.logs,
|
|
3484
|
+
errors: [...initResult.errors, ...migrationErrors],
|
|
3485
|
+
changedFiles,
|
|
3486
|
+
migrationReport
|
|
3487
|
+
};
|
|
3488
|
+
if (options.json) {
|
|
3489
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3490
|
+
} else {
|
|
3491
|
+
console.log(formatProjectInitPlan(selectedPlan));
|
|
3492
|
+
for (const line of result.logs) console.log(line);
|
|
3493
|
+
for (const line of result.errors) console.error(line);
|
|
3494
|
+
if (migrationReport) console.log(formatMigrationReport(migrationReport));
|
|
3495
|
+
if (result.ok && changedFiles.length) console.log(`Project synchronized: ${plan.project.slug}`);
|
|
3496
|
+
if (result.ok && changedFiles.length === 0) console.log(`Project already in parity: ${plan.project.slug}`);
|
|
3497
|
+
}
|
|
3498
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
3499
|
+
} catch (err) {
|
|
3500
|
+
if (options.json) {
|
|
3501
|
+
console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
|
|
3502
|
+
} else {
|
|
3503
|
+
console.error("\u274C project init failed:", err instanceof Error ? err.message : err);
|
|
3504
|
+
}
|
|
3505
|
+
process.exit(1);
|
|
3506
|
+
}
|
|
3507
|
+
});
|
|
3508
|
+
projectCmd.command("list").description("List projects in the pjangler registry").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((options) => {
|
|
3509
|
+
try {
|
|
3510
|
+
const registry = loadProjectRegistry(options.registry ?? projectRegistryPath());
|
|
3511
|
+
if (options.json) console.log(JSON.stringify(registry, null, 2));
|
|
3512
|
+
else console.log(formatProjectList(registry));
|
|
3513
|
+
} catch (err) {
|
|
3514
|
+
console.error("\u274C project list failed:", err instanceof Error ? err.message : err);
|
|
3515
|
+
process.exit(1);
|
|
3516
|
+
}
|
|
3517
|
+
});
|
|
3518
|
+
projectCmd.command("show").argument("<slug>", "Project slug").description("Show one project from the pjangler registry").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((slug, options) => {
|
|
3519
|
+
try {
|
|
3520
|
+
const project = getProject(loadProjectRegistry(options.registry ?? projectRegistryPath()), slug);
|
|
3521
|
+
if (options.json) console.log(JSON.stringify(project, null, 2));
|
|
3522
|
+
else console.log(`${project.name} (${project.slug})
|
|
3523
|
+
${project.repo_path}
|
|
3524
|
+
${project.description}`);
|
|
3525
|
+
} catch (err) {
|
|
3526
|
+
console.error("\u274C project show failed:", err instanceof Error ? err.message : err);
|
|
3527
|
+
process.exit(1);
|
|
3528
|
+
}
|
|
3529
|
+
});
|
|
3530
|
+
projectCmd.command("doctor").argument("[slug]", "Optional project slug").description("Validate the project registry and local projections").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((slug, options) => {
|
|
3531
|
+
try {
|
|
3532
|
+
const report = doctorProjectRegistry(options.registry ?? projectRegistryPath(), slug);
|
|
3533
|
+
if (options.json) {
|
|
3534
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3535
|
+
} else if (!report.issues.length) {
|
|
3536
|
+
console.log(`Project registry OK: ${report.registryPath}`);
|
|
3537
|
+
} else {
|
|
3538
|
+
console.log(`Project registry issues: ${report.registryPath}`);
|
|
3539
|
+
for (const issue of report.issues) console.log(` [${issue.level}] ${issue.slug ?? "registry"}: ${issue.message}`);
|
|
3540
|
+
}
|
|
3541
|
+
process.exit(report.ok ? 0 : 1);
|
|
3542
|
+
} catch (err) {
|
|
3543
|
+
console.error("\u274C project doctor failed:", err instanceof Error ? err.message : err);
|
|
3544
|
+
process.exit(1);
|
|
3545
|
+
}
|
|
3546
|
+
});
|
|
2643
3547
|
var recipeCmd = program.command("recipe").description("Manage pjangler recipes");
|
|
2644
3548
|
recipeCmd.command("list").description("List all available recipes").action(() => {
|
|
2645
3549
|
console.log("\u{1F4E6} Available Recipes:");
|
|
@@ -2772,24 +3676,50 @@ program.command("audit").argument("[repo]", "Path to repo to audit (default: cwd
|
|
|
2772
3676
|
process.exit(1);
|
|
2773
3677
|
}
|
|
2774
3678
|
});
|
|
2775
|
-
program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit
|
|
3679
|
+
program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to open interactive rule selector)").argument("[repo]", "Path to repo (default: cwd)").description("Idempotent migration recipe for a parity rule (or open the rule selector)").option("--all", "Apply every migration recipe in order").option("--dry-run", "Preview changes without writing files").option("--json", "Output machine-parseable JSON").action(async (ruleId, repo, options) => {
|
|
2776
3680
|
try {
|
|
2777
3681
|
const all = options.all ?? false;
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
3682
|
+
const dryRun = options.dryRun ?? false;
|
|
3683
|
+
if (all) {
|
|
3684
|
+
let actualRepo = repo;
|
|
3685
|
+
if (ruleId && !actualRepo) {
|
|
3686
|
+
actualRepo = ruleId;
|
|
3687
|
+
}
|
|
3688
|
+
const report2 = runMigration(void 0, actualRepo, dryRun, true);
|
|
3689
|
+
printMigrationReport(report2, options.json);
|
|
3690
|
+
process.exit(report2.ok ? 0 : 1);
|
|
3691
|
+
}
|
|
3692
|
+
if (ruleId && repo) {
|
|
3693
|
+
if (!getParityRuleIds().includes(ruleId)) {
|
|
3694
|
+
console.error(`\u274C Unknown parity rule: ${ruleId}`);
|
|
3695
|
+
process.exit(1);
|
|
3696
|
+
}
|
|
3697
|
+
const report2 = runMigration(ruleId, repo, dryRun, false);
|
|
3698
|
+
printMigrationReport(report2, options.json);
|
|
3699
|
+
process.exit(report2.ok ? 0 : 1);
|
|
2781
3700
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
3701
|
+
if (ruleId && getParityRuleIds().includes(ruleId)) {
|
|
3702
|
+
const report2 = runMigration(ruleId, void 0, dryRun, false);
|
|
3703
|
+
printMigrationReport(report2, options.json);
|
|
3704
|
+
process.exit(report2.ok ? 0 : 1);
|
|
2786
3705
|
}
|
|
2787
|
-
const report = runMigration(actualRuleId, actualRepo, options.dryRun ?? false, all);
|
|
2788
3706
|
if (options.json) {
|
|
2789
|
-
console.
|
|
2790
|
-
|
|
2791
|
-
|
|
3707
|
+
console.error("\u274C JSON output requires a rule-id or --all");
|
|
3708
|
+
process.exit(1);
|
|
3709
|
+
}
|
|
3710
|
+
if (!process.stdin.isTTY) {
|
|
3711
|
+
console.error("\u274C Provide a rule-id, use --all, or run in an interactive terminal");
|
|
3712
|
+
process.exit(1);
|
|
3713
|
+
}
|
|
3714
|
+
const targetRepo = ruleId ?? repo;
|
|
3715
|
+
const audit = runAudit(targetRepo);
|
|
3716
|
+
const ruleIds = await promptForRuleIds(audit.rules);
|
|
3717
|
+
if (!ruleIds.length) {
|
|
3718
|
+
console.log("No rules selected; nothing to migrate.");
|
|
3719
|
+
process.exit(0);
|
|
2792
3720
|
}
|
|
3721
|
+
const report = runMigrationForRules(ruleIds, targetRepo, dryRun);
|
|
3722
|
+
printMigrationReport(report, false);
|
|
2793
3723
|
process.exit(report.ok ? 0 : 1);
|
|
2794
3724
|
} catch (err) {
|
|
2795
3725
|
console.error("\u274C migrate failed:", err);
|