@haus-tech/haus-workflow 1.5.4 → 1.5.5
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/CHANGELOG.md +16 -0
- package/dist/cli.js +1041 -769
- package/library/catalog/manifest.json +361 -138
- package/library/catalog/validation-rules.json +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -60,11 +60,11 @@ var init_exec = __esm({
|
|
|
60
60
|
|
|
61
61
|
// src/cli.ts
|
|
62
62
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
63
|
-
import
|
|
63
|
+
import path72 from "node:path";
|
|
64
64
|
import { Command } from "commander";
|
|
65
65
|
|
|
66
66
|
// src/commands/apply.ts
|
|
67
|
-
import
|
|
67
|
+
import path24 from "node:path";
|
|
68
68
|
import checkbox from "@inquirer/checkbox";
|
|
69
69
|
|
|
70
70
|
// src/catalog/load-catalog.ts
|
|
@@ -185,6 +185,14 @@ async function listFiles(root, patterns) {
|
|
|
185
185
|
function hashText(value) {
|
|
186
186
|
return `sha256-${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
187
187
|
}
|
|
188
|
+
function resolveContainedPath(root, rel) {
|
|
189
|
+
const resolvedRoot = path.resolve(root);
|
|
190
|
+
const resolved = path.resolve(root, rel);
|
|
191
|
+
if (resolved === resolvedRoot) return null;
|
|
192
|
+
const prefix = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
|
|
193
|
+
if (!resolved.startsWith(prefix)) return null;
|
|
194
|
+
return resolved;
|
|
195
|
+
}
|
|
188
196
|
async function mapWithConcurrency(items, fn, concurrency = 24) {
|
|
189
197
|
const size = Number.isFinite(concurrency) ? Math.max(1, Math.floor(concurrency)) : 24;
|
|
190
198
|
const results = new Array(items.length);
|
|
@@ -929,6 +937,7 @@ var validation_rules_default = {
|
|
|
929
937
|
"haus",
|
|
930
938
|
"security",
|
|
931
939
|
"quality",
|
|
940
|
+
"a11y",
|
|
932
941
|
"frontend",
|
|
933
942
|
"backend",
|
|
934
943
|
"testing",
|
|
@@ -1552,7 +1561,7 @@ function summarizeGitignoredHausDirs(paths) {
|
|
|
1552
1561
|
}
|
|
1553
1562
|
|
|
1554
1563
|
// src/claude/write-claude-files.ts
|
|
1555
|
-
import
|
|
1564
|
+
import path20 from "node:path";
|
|
1556
1565
|
import fs18 from "fs-extra";
|
|
1557
1566
|
|
|
1558
1567
|
// src/catalog/former-ids.ts
|
|
@@ -1608,8 +1617,199 @@ function findOrphanedLockEntries(lock, recommendedIds) {
|
|
|
1608
1617
|
);
|
|
1609
1618
|
}
|
|
1610
1619
|
|
|
1611
|
-
// src/
|
|
1620
|
+
// src/scanner/detection.ts
|
|
1612
1621
|
import path10 from "node:path";
|
|
1622
|
+
|
|
1623
|
+
// src/security/sensitive-paths.ts
|
|
1624
|
+
var DENY_PATHS = [
|
|
1625
|
+
"*.pem",
|
|
1626
|
+
"*.key",
|
|
1627
|
+
"*.p12",
|
|
1628
|
+
"*.pfx",
|
|
1629
|
+
"id_rsa",
|
|
1630
|
+
"id_ed25519",
|
|
1631
|
+
"customer-data",
|
|
1632
|
+
"secrets",
|
|
1633
|
+
"certs"
|
|
1634
|
+
];
|
|
1635
|
+
var DENY_DIRS = /* @__PURE__ */ new Set(["customer-data", "secrets", "certs"]);
|
|
1636
|
+
var DENY_PATH_REGEXES = [
|
|
1637
|
+
/(^|\/)[^/]+\.pem$/i,
|
|
1638
|
+
/(^|\/)[^/]+\.key$/i,
|
|
1639
|
+
/(^|\/)[^/]+\.p12$/i,
|
|
1640
|
+
/(^|\/)[^/]+\.pfx$/i,
|
|
1641
|
+
/(^|\/)id_rsa$/i,
|
|
1642
|
+
/(^|\/)id_ed25519$/i,
|
|
1643
|
+
/(^|\/)customer-data(\/|$)/i,
|
|
1644
|
+
/(^|\/)secrets(\/|$)/i,
|
|
1645
|
+
/(^|\/)certs(\/|$)/i
|
|
1646
|
+
];
|
|
1647
|
+
function normalizePathForMatch(candidate) {
|
|
1648
|
+
return candidate.replace(/\\/g, "/").replace(/\/+/g, "/").trim();
|
|
1649
|
+
}
|
|
1650
|
+
var ASK_PATHS = [
|
|
1651
|
+
{ pattern: ".env", tools: ["Edit", "Write"] },
|
|
1652
|
+
{ pattern: ".env.*", tools: ["Edit", "Write"] },
|
|
1653
|
+
{ pattern: "storage/logs/**", tools: ["Edit", "Write"] },
|
|
1654
|
+
{ pattern: "exports/**", tools: ["Edit", "Write"] },
|
|
1655
|
+
{ pattern: "*.dump", tools: ["Read", "Edit", "Write"] },
|
|
1656
|
+
{ pattern: "*.backup", tools: ["Read", "Edit", "Write"] },
|
|
1657
|
+
{ pattern: "*.bak", tools: ["Read", "Edit", "Write"] },
|
|
1658
|
+
{ pattern: "wp-content/uploads/**", tools: ["Read", "Edit", "Write"] },
|
|
1659
|
+
{ pattern: "uploads/**", tools: ["Read", "Edit", "Write"] }
|
|
1660
|
+
];
|
|
1661
|
+
var SENSITIVE_PATH_REGEXES = [
|
|
1662
|
+
/^\.env(\.|$)/,
|
|
1663
|
+
/(^|\/)\.env(\.|$)/,
|
|
1664
|
+
/\.pem$/,
|
|
1665
|
+
/\.key$/,
|
|
1666
|
+
/\.p12$/,
|
|
1667
|
+
/\.pfx$/,
|
|
1668
|
+
/\.sql$/,
|
|
1669
|
+
/\.dump$/,
|
|
1670
|
+
/customer-data/,
|
|
1671
|
+
/exports/,
|
|
1672
|
+
/certs/,
|
|
1673
|
+
/secrets/,
|
|
1674
|
+
/(^|\/)storage\/logs(\/|$)/,
|
|
1675
|
+
/(^|\/)wp-content\/uploads(\/|$)/,
|
|
1676
|
+
/(^|\/)uploads(\/|$)/
|
|
1677
|
+
];
|
|
1678
|
+
var SENSITIVE_ITEM_KEYWORDS = [
|
|
1679
|
+
".env",
|
|
1680
|
+
"secrets",
|
|
1681
|
+
"certs",
|
|
1682
|
+
"customer-data",
|
|
1683
|
+
"exports",
|
|
1684
|
+
".pem",
|
|
1685
|
+
".key"
|
|
1686
|
+
];
|
|
1687
|
+
|
|
1688
|
+
// src/utils/audit-checks.ts
|
|
1689
|
+
function isRecord(v) {
|
|
1690
|
+
return typeof v === "object" && v !== null;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/scanner/detection.ts
|
|
1694
|
+
var UNSUPPORTED_MARKERS = {
|
|
1695
|
+
"requirements.txt": "python",
|
|
1696
|
+
"pyproject.toml": "python",
|
|
1697
|
+
"go.mod": "go",
|
|
1698
|
+
"Cargo.toml": "rust",
|
|
1699
|
+
"pom.xml": "java",
|
|
1700
|
+
"build.gradle": "java",
|
|
1701
|
+
"build.gradle.kts": "java",
|
|
1702
|
+
Gemfile: "ruby"
|
|
1703
|
+
};
|
|
1704
|
+
var WEAK_STACK_SIGNALS = /* @__PURE__ */ new Set(["missing-prettier", "missing-eslint"]);
|
|
1705
|
+
function computeDetectionStatus(roles, stacks, unsupportedSignals) {
|
|
1706
|
+
const hasRealStack = Object.entries(stacks).some(
|
|
1707
|
+
([bucket, names]) => bucket !== "packageManagers" && names.some((n) => !WEAK_STACK_SIGNALS.has(n))
|
|
1708
|
+
);
|
|
1709
|
+
const hasRealSignal = roles.length > 0 || hasRealStack;
|
|
1710
|
+
if (!hasRealSignal) return "unknown";
|
|
1711
|
+
return unsupportedSignals.length > 0 ? "partial" : "supported";
|
|
1712
|
+
}
|
|
1713
|
+
function describeUnknownDetection(unsupportedSignals, isLinkedWorktree) {
|
|
1714
|
+
const markers = unsupportedSignals.join(", ");
|
|
1715
|
+
const base = markers ? `Stack not recognised \u2014 detected ${markers}, which haus does not support.` : "Stack not recognised \u2014 no supported framework detected.";
|
|
1716
|
+
const worktreeNote = isLinkedWorktree ? "You're running from inside a git worktree \u2014 sibling repos may not be on disk here, so this could be a false zero-signal reading rather than truly no stack." : "";
|
|
1717
|
+
return [base, worktreeNote, "Only stack-agnostic workflow and security guidance is applied."].filter(Boolean).join(" ");
|
|
1718
|
+
}
|
|
1719
|
+
function blocked(rel) {
|
|
1720
|
+
return SENSITIVE_PATH_REGEXES.some((x) => x.test(rel));
|
|
1721
|
+
}
|
|
1722
|
+
function dependencyVersionMap(pkg, composer) {
|
|
1723
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1724
|
+
const pushObj = (obj) => {
|
|
1725
|
+
if (!isRecord(obj)) return;
|
|
1726
|
+
for (const key of Object.keys(obj)) {
|
|
1727
|
+
if (versions.has(key)) continue;
|
|
1728
|
+
const value = obj[key];
|
|
1729
|
+
versions.set(key, typeof value === "string" ? value : "");
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
pushObj(pkg?.dependencies);
|
|
1733
|
+
pushObj(pkg?.devDependencies);
|
|
1734
|
+
pushObj(composer?.require);
|
|
1735
|
+
pushObj(composer?.["require-dev"]);
|
|
1736
|
+
return versions;
|
|
1737
|
+
}
|
|
1738
|
+
function finalizeRoles(registryRoles, deps, files) {
|
|
1739
|
+
const roles = new Set(registryRoles);
|
|
1740
|
+
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
1741
|
+
const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
|
|
1742
|
+
if (hasWpConfig && hasBedrockLayout) {
|
|
1743
|
+
roles.add("wordpress-bedrock-site");
|
|
1744
|
+
roles.add("wordpress-site");
|
|
1745
|
+
} else if (hasWpConfig) {
|
|
1746
|
+
roles.add("wordpress-vanilla-site");
|
|
1747
|
+
roles.add("wordpress-site");
|
|
1748
|
+
} else if (deps.includes("roots/wordpress")) {
|
|
1749
|
+
roles.add("wordpress-bedrock-site");
|
|
1750
|
+
roles.add("wordpress-site");
|
|
1751
|
+
}
|
|
1752
|
+
return [...roles].sort();
|
|
1753
|
+
}
|
|
1754
|
+
function collectUnsupportedSignals(files) {
|
|
1755
|
+
return [
|
|
1756
|
+
...new Set(
|
|
1757
|
+
files.map((f) => UNSUPPORTED_MARKERS[path10.basename(f)]).filter((s) => Boolean(s))
|
|
1758
|
+
)
|
|
1759
|
+
].sort();
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// src/recommender/policies.ts
|
|
1763
|
+
var UNSUPPORTED = FORBIDDEN_TAGS;
|
|
1764
|
+
function matchRequiresAny(clauses, ctx) {
|
|
1765
|
+
for (const clause of clauses) {
|
|
1766
|
+
if ("stack" in clause) {
|
|
1767
|
+
if (ctx.stackSet.has(clause.stack.toLowerCase())) {
|
|
1768
|
+
return { matched: true, signal: `stack:${clause.stack}` };
|
|
1769
|
+
}
|
|
1770
|
+
} else if ("dependency" in clause) {
|
|
1771
|
+
if (ctx.depSet.has(clause.dependency.toLowerCase())) {
|
|
1772
|
+
return { matched: true, signal: `dependency:${clause.dependency}` };
|
|
1773
|
+
}
|
|
1774
|
+
} else if ("packageNamePattern" in clause) {
|
|
1775
|
+
const pattern = clause.packageNamePattern.toLowerCase();
|
|
1776
|
+
const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
|
|
1777
|
+
for (const dep2 of ctx.depSet) {
|
|
1778
|
+
if (pattern.endsWith("*") ? dep2.startsWith(prefix) : dep2 === pattern) {
|
|
1779
|
+
return {
|
|
1780
|
+
matched: true,
|
|
1781
|
+
signal: `packageNamePattern:${clause.packageNamePattern}`
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
} else if ("role" in clause) {
|
|
1786
|
+
if (ctx.roleSet.has(clause.role.toLowerCase())) {
|
|
1787
|
+
return { matched: true, signal: `role:${clause.role}` };
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
return { matched: false };
|
|
1792
|
+
}
|
|
1793
|
+
function describeRequiresAny(clauses) {
|
|
1794
|
+
return clauses.map((c) => {
|
|
1795
|
+
if ("stack" in c) return `stack=${c.stack}`;
|
|
1796
|
+
if ("dependency" in c) return `dependency=${c.dependency}`;
|
|
1797
|
+
if ("packageNamePattern" in c) return `packageNamePattern=${c.packageNamePattern}`;
|
|
1798
|
+
if ("role" in c) return `role=${c.role}`;
|
|
1799
|
+
return "unknown";
|
|
1800
|
+
}).join(" | ");
|
|
1801
|
+
}
|
|
1802
|
+
function mergeRecommendationWarnings(context, isLinkedWorktree = false) {
|
|
1803
|
+
const markers = context.unsupportedSignals?.join(", ");
|
|
1804
|
+
const statusLines = context.detectionStatus === "unknown" ? [describeUnknownDetection(context.unsupportedSignals ?? [], isLinkedWorktree)] : context.detectionStatus === "partial" && markers ? [
|
|
1805
|
+
`Partially supported \u2014 found unsupported ${markers} alongside recognised stacks; guidance covers the supported parts only.`
|
|
1806
|
+
] : [];
|
|
1807
|
+
const riskLines = (context.securityRisks?.length ?? 0) > 0 ? [`Scan reported security signals: ${context.securityRisks.join("; ")}`] : [];
|
|
1808
|
+
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/update/hash-installed.ts
|
|
1812
|
+
import path11 from "node:path";
|
|
1613
1813
|
import fg2 from "fast-glob";
|
|
1614
1814
|
import fs7 from "fs-extra";
|
|
1615
1815
|
|
|
@@ -1666,7 +1866,7 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
|
|
|
1666
1866
|
const normalized = [...new Set(relPaths.map((p) => p.replace(/\\/g, "/")))].sort();
|
|
1667
1867
|
const fileDigests = [];
|
|
1668
1868
|
for (const rel of normalized) {
|
|
1669
|
-
const abs =
|
|
1869
|
+
const abs = path11.join(root, rel);
|
|
1670
1870
|
if (!await fs7.pathExists(abs)) continue;
|
|
1671
1871
|
const stat = followSymlinks ? await fs7.stat(abs) : await fs7.lstat(abs);
|
|
1672
1872
|
if (!followSymlinks && stat.isSymbolicLink()) continue;
|
|
@@ -1683,9 +1883,9 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
|
|
|
1683
1883
|
followSymbolicLinks: followSymlinks
|
|
1684
1884
|
});
|
|
1685
1885
|
for (const sub of inner.sort()) {
|
|
1686
|
-
const absFile =
|
|
1886
|
+
const absFile = path11.join(abs, sub);
|
|
1687
1887
|
if (!followSymlinks && (await fs7.lstat(absFile)).isSymbolicLink()) continue;
|
|
1688
|
-
const relFile =
|
|
1888
|
+
const relFile = path11.join(rel, sub).replace(/\\/g, "/");
|
|
1689
1889
|
const body = await fs7.readFile(absFile);
|
|
1690
1890
|
fileDigests.push({ rel: relFile, digest: digestFileContent(body) });
|
|
1691
1891
|
}
|
|
@@ -1757,18 +1957,18 @@ function buildAllowRules() {
|
|
|
1757
1957
|
}
|
|
1758
1958
|
|
|
1759
1959
|
// src/install/settings-merge.ts
|
|
1760
|
-
import
|
|
1960
|
+
import path13 from "node:path";
|
|
1761
1961
|
import fs9 from "fs-extra";
|
|
1762
1962
|
|
|
1763
1963
|
// src/install/manifest.ts
|
|
1764
1964
|
import os3 from "node:os";
|
|
1765
|
-
import
|
|
1965
|
+
import path12 from "node:path";
|
|
1766
1966
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
1767
1967
|
function globalClaudeDir() {
|
|
1768
|
-
return
|
|
1968
|
+
return path12.join(os3.homedir(), ".claude");
|
|
1769
1969
|
}
|
|
1770
1970
|
function hausManifestPath() {
|
|
1771
|
-
return
|
|
1971
|
+
return path12.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
1772
1972
|
}
|
|
1773
1973
|
async function readManifest() {
|
|
1774
1974
|
return readJson(hausManifestPath());
|
|
@@ -1788,7 +1988,7 @@ function buildManifest(source, files, hooks) {
|
|
|
1788
1988
|
|
|
1789
1989
|
// src/install/settings-merge.ts
|
|
1790
1990
|
function settingsJsonPath() {
|
|
1791
|
-
return
|
|
1991
|
+
return path13.join(globalClaudeDir(), "settings.json");
|
|
1792
1992
|
}
|
|
1793
1993
|
async function readSettings() {
|
|
1794
1994
|
const filePath = settingsJsonPath();
|
|
@@ -2017,71 +2217,6 @@ var ASK_COMMANDS = [
|
|
|
2017
2217
|
"php artisan migrate --force"
|
|
2018
2218
|
];
|
|
2019
2219
|
|
|
2020
|
-
// src/security/sensitive-paths.ts
|
|
2021
|
-
var DENY_PATHS = [
|
|
2022
|
-
"*.pem",
|
|
2023
|
-
"*.key",
|
|
2024
|
-
"*.p12",
|
|
2025
|
-
"*.pfx",
|
|
2026
|
-
"id_rsa",
|
|
2027
|
-
"id_ed25519",
|
|
2028
|
-
"customer-data",
|
|
2029
|
-
"secrets",
|
|
2030
|
-
"certs"
|
|
2031
|
-
];
|
|
2032
|
-
var DENY_DIRS = /* @__PURE__ */ new Set(["customer-data", "secrets", "certs"]);
|
|
2033
|
-
var DENY_PATH_REGEXES = [
|
|
2034
|
-
/(^|\/)[^/]+\.pem$/i,
|
|
2035
|
-
/(^|\/)[^/]+\.key$/i,
|
|
2036
|
-
/(^|\/)[^/]+\.p12$/i,
|
|
2037
|
-
/(^|\/)[^/]+\.pfx$/i,
|
|
2038
|
-
/(^|\/)id_rsa$/i,
|
|
2039
|
-
/(^|\/)id_ed25519$/i,
|
|
2040
|
-
/(^|\/)customer-data(\/|$)/i,
|
|
2041
|
-
/(^|\/)secrets(\/|$)/i,
|
|
2042
|
-
/(^|\/)certs(\/|$)/i
|
|
2043
|
-
];
|
|
2044
|
-
function normalizePathForMatch(candidate) {
|
|
2045
|
-
return candidate.replace(/\\/g, "/").replace(/\/+/g, "/").trim();
|
|
2046
|
-
}
|
|
2047
|
-
var ASK_PATHS = [
|
|
2048
|
-
{ pattern: ".env", tools: ["Edit", "Write"] },
|
|
2049
|
-
{ pattern: ".env.*", tools: ["Edit", "Write"] },
|
|
2050
|
-
{ pattern: "storage/logs/**", tools: ["Edit", "Write"] },
|
|
2051
|
-
{ pattern: "exports/**", tools: ["Edit", "Write"] },
|
|
2052
|
-
{ pattern: "*.dump", tools: ["Read", "Edit", "Write"] },
|
|
2053
|
-
{ pattern: "*.backup", tools: ["Read", "Edit", "Write"] },
|
|
2054
|
-
{ pattern: "*.bak", tools: ["Read", "Edit", "Write"] },
|
|
2055
|
-
{ pattern: "wp-content/uploads/**", tools: ["Read", "Edit", "Write"] },
|
|
2056
|
-
{ pattern: "uploads/**", tools: ["Read", "Edit", "Write"] }
|
|
2057
|
-
];
|
|
2058
|
-
var SENSITIVE_PATH_REGEXES = [
|
|
2059
|
-
/^\.env(\.|$)/,
|
|
2060
|
-
/(^|\/)\.env(\.|$)/,
|
|
2061
|
-
/\.pem$/,
|
|
2062
|
-
/\.key$/,
|
|
2063
|
-
/\.p12$/,
|
|
2064
|
-
/\.pfx$/,
|
|
2065
|
-
/\.sql$/,
|
|
2066
|
-
/\.dump$/,
|
|
2067
|
-
/customer-data/,
|
|
2068
|
-
/exports/,
|
|
2069
|
-
/certs/,
|
|
2070
|
-
/secrets/,
|
|
2071
|
-
/(^|\/)storage\/logs(\/|$)/,
|
|
2072
|
-
/(^|\/)wp-content\/uploads(\/|$)/,
|
|
2073
|
-
/(^|\/)uploads(\/|$)/
|
|
2074
|
-
];
|
|
2075
|
-
var SENSITIVE_ITEM_KEYWORDS = [
|
|
2076
|
-
".env",
|
|
2077
|
-
"secrets",
|
|
2078
|
-
"certs",
|
|
2079
|
-
"customer-data",
|
|
2080
|
-
"exports",
|
|
2081
|
-
".pem",
|
|
2082
|
-
".key"
|
|
2083
|
-
];
|
|
2084
|
-
|
|
2085
2220
|
// src/security/ask-rules.ts
|
|
2086
2221
|
function buildAskRules() {
|
|
2087
2222
|
const rules = [];
|
|
@@ -2103,8 +2238,8 @@ function buildDenyRules() {
|
|
|
2103
2238
|
for (const command of DENY_COMMANDS) {
|
|
2104
2239
|
rules.push(`Bash(${command}:*)`);
|
|
2105
2240
|
}
|
|
2106
|
-
for (const
|
|
2107
|
-
const pattern = DENY_DIRS.has(
|
|
2241
|
+
for (const path73 of DENY_PATHS) {
|
|
2242
|
+
const pattern = DENY_DIRS.has(path73) ? `${path73}/**` : path73;
|
|
2108
2243
|
for (const tool of FILE_TOOLS) {
|
|
2109
2244
|
rules.push(`${tool}(${pattern})`);
|
|
2110
2245
|
}
|
|
@@ -2183,24 +2318,28 @@ async function applyProjectSettingsMerge(root) {
|
|
|
2183
2318
|
}
|
|
2184
2319
|
|
|
2185
2320
|
// src/claude/superpowers-install.ts
|
|
2186
|
-
import
|
|
2321
|
+
import path14 from "node:path";
|
|
2187
2322
|
import fg3 from "fast-glob";
|
|
2188
2323
|
import fs10 from "fs-extra";
|
|
2189
2324
|
var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
|
|
2190
2325
|
var PATH_REWRITES = [
|
|
2191
2326
|
["skills/shared/", ".claude/skills/shared/"]
|
|
2192
2327
|
];
|
|
2328
|
+
var PREFIX_REWRITES = ["superpowers-extended-cc:"];
|
|
2193
2329
|
function rewriteSuperpowersMarkdown(text) {
|
|
2194
2330
|
let out = text;
|
|
2195
2331
|
for (const [from, to] of PATH_REWRITES) {
|
|
2196
2332
|
out = out.split(from).join(to);
|
|
2197
2333
|
}
|
|
2334
|
+
for (const prefix of PREFIX_REWRITES) {
|
|
2335
|
+
out = out.split(prefix).join("");
|
|
2336
|
+
}
|
|
2198
2337
|
return out;
|
|
2199
2338
|
}
|
|
2200
2339
|
async function rewriteMarkdownTree(dir) {
|
|
2201
2340
|
const files = await fg3("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
|
|
2202
2341
|
for (const rel of files) {
|
|
2203
|
-
const abs =
|
|
2342
|
+
const abs = path14.join(dir, rel);
|
|
2204
2343
|
const text = await fs10.readFile(abs, "utf8");
|
|
2205
2344
|
const rewritten = rewriteSuperpowersMarkdown(text);
|
|
2206
2345
|
if (rewritten !== text) {
|
|
@@ -2208,10 +2347,24 @@ async function rewriteMarkdownTree(dir) {
|
|
|
2208
2347
|
}
|
|
2209
2348
|
}
|
|
2210
2349
|
}
|
|
2350
|
+
async function extraDestFiles(sourcePath, destination) {
|
|
2351
|
+
const [sourceFiles, destFiles] = await Promise.all([
|
|
2352
|
+
fg3("**/*", { cwd: sourcePath, onlyFiles: true, dot: true }),
|
|
2353
|
+
fg3("**/*", { cwd: destination, onlyFiles: true, dot: true })
|
|
2354
|
+
]);
|
|
2355
|
+
const sourceSet = new Set(sourceFiles);
|
|
2356
|
+
return destFiles.filter((f) => !sourceSet.has(f));
|
|
2357
|
+
}
|
|
2211
2358
|
async function installCatalogSkill(sourcePath, destination, opts) {
|
|
2212
2359
|
if (opts.dryRun) return;
|
|
2213
|
-
await fs10.ensureDir(
|
|
2360
|
+
await fs10.ensureDir(path14.dirname(destination));
|
|
2214
2361
|
if (await fs10.pathExists(destination)) {
|
|
2362
|
+
const extra = await extraDestFiles(sourcePath, destination);
|
|
2363
|
+
if (extra.length > 0) {
|
|
2364
|
+
warn(
|
|
2365
|
+
`${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2215
2368
|
await fs10.remove(destination);
|
|
2216
2369
|
}
|
|
2217
2370
|
await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
@@ -2220,17 +2373,23 @@ async function installCatalogSkill(sourcePath, destination, opts) {
|
|
|
2220
2373
|
}
|
|
2221
2374
|
}
|
|
2222
2375
|
async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
|
|
2223
|
-
const source =
|
|
2376
|
+
const source = path14.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
|
|
2224
2377
|
if (!await fs10.pathExists(source)) return null;
|
|
2225
2378
|
const destination = claudePath(projectRoot, "skills", "shared");
|
|
2226
|
-
if (dryRun) return
|
|
2227
|
-
await fs10.ensureDir(
|
|
2379
|
+
if (dryRun) return path14.relative(projectRoot, destination);
|
|
2380
|
+
await fs10.ensureDir(path14.dirname(destination));
|
|
2228
2381
|
if (await fs10.pathExists(destination)) {
|
|
2382
|
+
const extra = await extraDestFiles(source, destination);
|
|
2383
|
+
if (extra.length > 0) {
|
|
2384
|
+
warn(
|
|
2385
|
+
`${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2229
2388
|
await fs10.remove(destination);
|
|
2230
2389
|
}
|
|
2231
2390
|
await fs10.copy(source, destination, { overwrite: true, errorOnExist: false });
|
|
2232
2391
|
await rewriteMarkdownTree(destination);
|
|
2233
|
-
return
|
|
2392
|
+
return path14.relative(projectRoot, destination);
|
|
2234
2393
|
}
|
|
2235
2394
|
|
|
2236
2395
|
// src/claude/verify-hooks-contract.ts
|
|
@@ -2345,7 +2504,7 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
2345
2504
|
}
|
|
2346
2505
|
|
|
2347
2506
|
// src/claude/write-decisions-seed.ts
|
|
2348
|
-
import
|
|
2507
|
+
import path15 from "node:path";
|
|
2349
2508
|
import fs12 from "fs-extra";
|
|
2350
2509
|
|
|
2351
2510
|
// library/catalog/decisions-triggers.json
|
|
@@ -2389,8 +2548,8 @@ Why we chose X \u2014 short, dated, write-once decision log for humans and agent
|
|
|
2389
2548
|
| --- | ----- | -------------- | ------ |
|
|
2390
2549
|
`;
|
|
2391
2550
|
async function writeDecisionsSeed(root, dryRun) {
|
|
2392
|
-
const decisionsDir =
|
|
2393
|
-
const readmePath =
|
|
2551
|
+
const decisionsDir = path15.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
2552
|
+
const readmePath = path15.join(decisionsDir, "README.md");
|
|
2394
2553
|
if (await fs12.pathExists(readmePath)) return null;
|
|
2395
2554
|
if (!dryRun) await fs12.ensureDir(decisionsDir);
|
|
2396
2555
|
await writeManagedText(root, readmePath, SEED_README, dryRun);
|
|
@@ -2398,7 +2557,7 @@ async function writeDecisionsSeed(root, dryRun) {
|
|
|
2398
2557
|
}
|
|
2399
2558
|
|
|
2400
2559
|
// src/claude/write-prettierignore.ts
|
|
2401
|
-
import
|
|
2560
|
+
import path16 from "node:path";
|
|
2402
2561
|
import fs13 from "fs-extra";
|
|
2403
2562
|
var PRETTIERIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
2404
2563
|
var PRETTIERIGNORE_END = "# HAUS:END haus-managed";
|
|
@@ -2467,7 +2626,7 @@ ${block}
|
|
|
2467
2626
|
`;
|
|
2468
2627
|
}
|
|
2469
2628
|
async function writePrettierIgnore(root, dryRun) {
|
|
2470
|
-
const filePath =
|
|
2629
|
+
const filePath = path16.join(root, ".prettierignore");
|
|
2471
2630
|
const prev = await fs13.pathExists(filePath) ? await fs13.readFile(filePath, "utf8") : "";
|
|
2472
2631
|
const next = injectPrettierIgnoreBlock(prev, buildPrettierIgnoreBlock());
|
|
2473
2632
|
await writeManagedText(root, filePath, next, dryRun);
|
|
@@ -2475,7 +2634,7 @@ async function writePrettierIgnore(root, dryRun) {
|
|
|
2475
2634
|
}
|
|
2476
2635
|
|
|
2477
2636
|
// src/claude/write-root-claude-md.ts
|
|
2478
|
-
import
|
|
2637
|
+
import path17 from "node:path";
|
|
2479
2638
|
import fs14 from "fs-extra";
|
|
2480
2639
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
2481
2640
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
@@ -2553,7 +2712,7 @@ ${block}
|
|
|
2553
2712
|
`;
|
|
2554
2713
|
}
|
|
2555
2714
|
async function writeRootClaudeMd(root, dryRun) {
|
|
2556
|
-
const filePath =
|
|
2715
|
+
const filePath = path17.join(root, "CLAUDE.md");
|
|
2557
2716
|
const block = buildImportBlock();
|
|
2558
2717
|
const prev = await fs14.pathExists(filePath) ? await fs14.readFile(filePath, "utf8") : "";
|
|
2559
2718
|
const next = injectHausBlock(prev, block);
|
|
@@ -2562,11 +2721,11 @@ async function writeRootClaudeMd(root, dryRun) {
|
|
|
2562
2721
|
}
|
|
2563
2722
|
|
|
2564
2723
|
// src/claude/write-workflow-config.ts
|
|
2565
|
-
import
|
|
2724
|
+
import path19 from "node:path";
|
|
2566
2725
|
import fs16 from "fs-extra";
|
|
2567
2726
|
|
|
2568
2727
|
// src/claude/derive-workflow-config.ts
|
|
2569
|
-
import
|
|
2728
|
+
import path18 from "node:path";
|
|
2570
2729
|
import fs15 from "fs-extra";
|
|
2571
2730
|
function binCmd(pm, bin, args) {
|
|
2572
2731
|
const tail = args ? ` ${args}` : "";
|
|
@@ -2576,7 +2735,7 @@ function binCmd(pm, bin, args) {
|
|
|
2576
2735
|
}
|
|
2577
2736
|
async function deriveWorkflowConfig(root, ctx) {
|
|
2578
2737
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
2579
|
-
const pkg = await readJson(
|
|
2738
|
+
const pkg = await readJson(path18.join(root, "package.json"));
|
|
2580
2739
|
const scripts = pkg?.scripts ?? {};
|
|
2581
2740
|
const deps = new Set(ctx.dependencies);
|
|
2582
2741
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -2586,7 +2745,7 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
2586
2745
|
return null;
|
|
2587
2746
|
};
|
|
2588
2747
|
const hasDep = (name) => deps.has(name);
|
|
2589
|
-
const exists = (rel) => fs15.pathExistsSync(
|
|
2748
|
+
const exists = (rel) => fs15.pathExistsSync(path18.join(root, rel));
|
|
2590
2749
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
2591
2750
|
const hasCypress = hasDep("cypress");
|
|
2592
2751
|
const preCommitTool = exists("lefthook.yml") || exists("lefthook.yaml") ? "lefthook" : exists(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
|
|
@@ -2661,7 +2820,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
2661
2820
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
2662
2821
|
...FALLBACK_CONTEXT,
|
|
2663
2822
|
root,
|
|
2664
|
-
repoName:
|
|
2823
|
+
repoName: path19.basename(root)
|
|
2665
2824
|
};
|
|
2666
2825
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
2667
2826
|
if (exists) {
|
|
@@ -2706,7 +2865,8 @@ function checkManagedTamper(body, storedHash, templateContent) {
|
|
|
2706
2865
|
// src/claude/write-workflow.ts
|
|
2707
2866
|
var STABLE_ID = "template.workflow";
|
|
2708
2867
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
2709
|
-
|
|
2868
|
+
const hashAttr = contentHash === void 0 ? "" : ` hash=${contentHash}`;
|
|
2869
|
+
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion}${hashAttr} -->`;
|
|
2710
2870
|
}
|
|
2711
2871
|
async function writeWorkflow(root, pkgVersion, dryRun, force = false) {
|
|
2712
2872
|
const templateContent = await readWorkflowTemplate({ dryRun });
|
|
@@ -2747,12 +2907,12 @@ ${templateContent}`;
|
|
|
2747
2907
|
return null;
|
|
2748
2908
|
}
|
|
2749
2909
|
if (verdict.status === "legacy-diverged" && !force) {
|
|
2750
|
-
const migratedHeader = makeWorkflowHeader(pkgVersion
|
|
2910
|
+
const migratedHeader = makeWorkflowHeader(pkgVersion);
|
|
2751
2911
|
const migrated = `${migratedHeader}
|
|
2752
2912
|
${existingContent}`;
|
|
2753
2913
|
if (hasTextChanged(existing, migrated)) {
|
|
2754
2914
|
if (dryRun) {
|
|
2755
|
-
log(`${printable}: migrating legacy
|
|
2915
|
+
log(`${printable}: migrating legacy header (staying hash-less; body preserved)`);
|
|
2756
2916
|
} else {
|
|
2757
2917
|
await writeText(destPath, migrated);
|
|
2758
2918
|
}
|
|
@@ -2799,7 +2959,7 @@ async function listFilesRecursive2(dir) {
|
|
|
2799
2959
|
const walk = async (current, relPrefix) => {
|
|
2800
2960
|
const names = await fs18.readdir(current);
|
|
2801
2961
|
for (const name of names) {
|
|
2802
|
-
const abs =
|
|
2962
|
+
const abs = path20.join(current, name);
|
|
2803
2963
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
2804
2964
|
const stat = await fs18.lstat(abs);
|
|
2805
2965
|
if (stat.isSymbolicLink()) {
|
|
@@ -2843,8 +3003,8 @@ async function logSkillDryRunDiff(root, sourcePath, destination, itemId, say) {
|
|
|
2843
3003
|
for (const rel of sourceFiles) {
|
|
2844
3004
|
await logFileDryRunDiff(
|
|
2845
3005
|
root,
|
|
2846
|
-
|
|
2847
|
-
|
|
3006
|
+
path20.join(sourcePath, rel),
|
|
3007
|
+
path20.join(destination, rel),
|
|
2848
3008
|
itemId,
|
|
2849
3009
|
say
|
|
2850
3010
|
);
|
|
@@ -2852,7 +3012,7 @@ async function logSkillDryRunDiff(root, sourcePath, destination, itemId, say) {
|
|
|
2852
3012
|
const sourceFileSet = new Set(sourceFiles);
|
|
2853
3013
|
for (const rel of destFiles) {
|
|
2854
3014
|
if (!sourceFileSet.has(rel)) {
|
|
2855
|
-
say(`${displayPath(root,
|
|
3015
|
+
say(`${displayPath(root, path20.join(destination, rel))}: would remove (${itemId})`);
|
|
2856
3016
|
}
|
|
2857
3017
|
}
|
|
2858
3018
|
}
|
|
@@ -2872,7 +3032,9 @@ async function removeLegacyManagedStub(root, relPathSegments, stub, dryRun, say)
|
|
|
2872
3032
|
async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
2873
3033
|
const say = opts.quiet ? () => {
|
|
2874
3034
|
} : log;
|
|
2875
|
-
const
|
|
3035
|
+
const recResult = await readJsonDetailed(hausPath(root, "recommendation.json"));
|
|
3036
|
+
const recommendationUnusable = recResult.status !== "ok";
|
|
3037
|
+
const rec = recResult.status === "ok" ? recResult.value : {
|
|
2876
3038
|
recommended: [],
|
|
2877
3039
|
skipped: [],
|
|
2878
3040
|
warnings: [],
|
|
@@ -2882,7 +3044,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2882
3044
|
estimatedTokenReductionPct: 0
|
|
2883
3045
|
};
|
|
2884
3046
|
const pkgRoot = packageRoot();
|
|
2885
|
-
const hausVersion2 = (await readJson(
|
|
3047
|
+
const hausVersion2 = (await readJson(path20.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
2886
3048
|
const coreFiles = [claudePath(root, "settings.json"), claudePath(root, "rules", "haus.md")];
|
|
2887
3049
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
2888
3050
|
const decisionsSeedPath = await writeDecisionsSeed(root, dryRun);
|
|
@@ -2972,6 +3134,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2972
3134
|
const { items: manifestItems, contentRoot } = await loadCatalogContext(root);
|
|
2973
3135
|
const manifestById = new Map(manifestItems.map((item) => [item.id, item]));
|
|
2974
3136
|
const prevLock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3137
|
+
const prevLockById = new Map(
|
|
3138
|
+
prevLock.filter((entry) => Boolean(entry.id)).map((entry) => [entry.id, entry])
|
|
3139
|
+
);
|
|
2975
3140
|
const allMigrations = findFormerIdMigrations(
|
|
2976
3141
|
prevLock.filter((entry) => Boolean(entry.id)),
|
|
2977
3142
|
manifestItems
|
|
@@ -3032,6 +3197,17 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3032
3197
|
warn(`Skipping ${item.id}: reviewStatus is deprecated`);
|
|
3033
3198
|
continue;
|
|
3034
3199
|
}
|
|
3200
|
+
const manifestItemTags = (manifestItem.tags ?? []).map((t) => t.toLowerCase());
|
|
3201
|
+
if (UNSUPPORTED.some((x) => manifestItemTags.includes(x))) {
|
|
3202
|
+
warn(`Skipping ${item.id}: unsupported stack policy`);
|
|
3203
|
+
continue;
|
|
3204
|
+
}
|
|
3205
|
+
const sensitiveInId = SENSITIVE_ITEM_KEYWORDS.some((x) => item.id.toLowerCase().includes(x));
|
|
3206
|
+
const sensitiveInTags = SENSITIVE_ITEM_KEYWORDS.some((x) => manifestItemTags.includes(x));
|
|
3207
|
+
if (sensitiveInId || sensitiveInTags) {
|
|
3208
|
+
warn(`Skipping ${item.id}: sensitive content policy block`);
|
|
3209
|
+
continue;
|
|
3210
|
+
}
|
|
3035
3211
|
if (manifestItem.source === "curated") {
|
|
3036
3212
|
if (manifestItem.reviewStatus !== "approved") {
|
|
3037
3213
|
curatedReviewStatusSkips++;
|
|
@@ -3056,7 +3232,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3056
3232
|
);
|
|
3057
3233
|
continue;
|
|
3058
3234
|
}
|
|
3059
|
-
const destination = claudePath(root, target,
|
|
3235
|
+
const destination = claudePath(root, target, path20.basename(sourcePath));
|
|
3060
3236
|
if (await fs18.pathExists(sourcePath)) {
|
|
3061
3237
|
if (dryRun) {
|
|
3062
3238
|
if (item.type === "skill") {
|
|
@@ -3064,13 +3240,23 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3064
3240
|
} else {
|
|
3065
3241
|
await logFileDryRunDiff(root, sourcePath, destination, item.id, say);
|
|
3066
3242
|
}
|
|
3243
|
+
} else if (await skipLocallyEditedItem(
|
|
3244
|
+
root,
|
|
3245
|
+
item.id,
|
|
3246
|
+
prevLockById.get(item.id),
|
|
3247
|
+
opts.force,
|
|
3248
|
+
files,
|
|
3249
|
+
installedPathsByItem,
|
|
3250
|
+
installedIds
|
|
3251
|
+
)) {
|
|
3252
|
+
continue;
|
|
3067
3253
|
} else if (item.type === "skill") {
|
|
3068
3254
|
const skillFiles = (await fs18.readdir(sourcePath, { recursive: true })).filter(
|
|
3069
3255
|
(f) => typeof f === "string" && f.endsWith(".md")
|
|
3070
3256
|
);
|
|
3071
3257
|
let skillValid = true;
|
|
3072
3258
|
for (const relFile of skillFiles) {
|
|
3073
|
-
const mdPath =
|
|
3259
|
+
const mdPath = path20.join(sourcePath, relFile);
|
|
3074
3260
|
const mdContent = await fs18.readFile(mdPath, "utf8");
|
|
3075
3261
|
const mdValidation = validateCatalogItem(
|
|
3076
3262
|
{
|
|
@@ -3109,17 +3295,17 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3109
3295
|
warn(`Skipping ${item.id}: pre-copy validation failed \u2014 ${validation.reason}`);
|
|
3110
3296
|
continue;
|
|
3111
3297
|
}
|
|
3112
|
-
await fs18.ensureDir(
|
|
3298
|
+
await fs18.ensureDir(path20.dirname(destination));
|
|
3113
3299
|
await fs18.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
3114
3300
|
}
|
|
3115
3301
|
files.push(destination);
|
|
3116
|
-
const relPaths = [
|
|
3302
|
+
const relPaths = [path20.relative(root, destination)];
|
|
3117
3303
|
if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
|
|
3118
3304
|
const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
|
|
3119
3305
|
if (sharedRel) {
|
|
3120
3306
|
superpowersSharedInstalled = true;
|
|
3121
3307
|
relPaths.push(sharedRel);
|
|
3122
|
-
files.push(
|
|
3308
|
+
files.push(path20.join(root, sharedRel));
|
|
3123
3309
|
}
|
|
3124
3310
|
}
|
|
3125
3311
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
@@ -3137,18 +3323,31 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3137
3323
|
);
|
|
3138
3324
|
}
|
|
3139
3325
|
await cleanupStaleCatalogItems(root, cleanupManifestById, dryRun, opts.quiet);
|
|
3326
|
+
const refuseDestructiveLockChange = recommendationUnusable && prevLock.length > 0 && !opts.force;
|
|
3140
3327
|
if (opts.prune) {
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3328
|
+
if (refuseDestructiveLockChange) {
|
|
3329
|
+
warn(
|
|
3330
|
+
"recommendation.json is missing or unreadable \u2014 refusing --prune (every installed item would look orphaned). Run `haus recommend` first, or pass --force to override."
|
|
3331
|
+
);
|
|
3332
|
+
} else {
|
|
3333
|
+
const notOrphaned = new Set(rec.recommended.map((r) => r.id));
|
|
3334
|
+
for (const migration of allMigrations) notOrphaned.add(migration.oldId);
|
|
3335
|
+
for (const entry of prevLock) {
|
|
3336
|
+
if (!entry.id) continue;
|
|
3337
|
+
const manifestItem = manifestById.get(entry.id);
|
|
3338
|
+
if (!manifestItem || manifestItem.reviewStatus === "deprecated") notOrphaned.add(entry.id);
|
|
3339
|
+
}
|
|
3340
|
+
await pruneOrphanedCatalogItems(root, prevLock, notOrphaned, dryRun, opts.quiet);
|
|
3147
3341
|
}
|
|
3148
|
-
await pruneOrphanedCatalogItems(root, prevLock, notOrphaned, dryRun, opts.quiet);
|
|
3149
3342
|
}
|
|
3150
3343
|
const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
|
|
3151
3344
|
if (dryRun) {
|
|
3345
|
+
if (refuseDestructiveLockChange) {
|
|
3346
|
+
say(
|
|
3347
|
+
"[dry-run] recommendation.json is missing or unreadable \u2014 haus.lock.json would be left unchanged. Run `haus recommend` first, or pass --force."
|
|
3348
|
+
);
|
|
3349
|
+
return [...new Set(files)];
|
|
3350
|
+
}
|
|
3152
3351
|
const prevIds = new Set(
|
|
3153
3352
|
prevLock.map((e) => e.id ? migrationByOldId.get(e.id) ?? e.id : void 0).filter((id) => Boolean(id))
|
|
3154
3353
|
);
|
|
@@ -3159,6 +3358,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3159
3358
|
for (const id of removed) say(`[dry-run] haus.lock.json would remove: ${id}`);
|
|
3160
3359
|
return [...new Set(files)];
|
|
3161
3360
|
}
|
|
3361
|
+
if (refuseDestructiveLockChange) {
|
|
3362
|
+
warn(
|
|
3363
|
+
"recommendation.json is missing or unreadable \u2014 leaving haus.lock.json unchanged rather than writing an empty lock. Run `haus recommend` first, or pass --force to overwrite anyway."
|
|
3364
|
+
);
|
|
3365
|
+
return [...new Set(files)];
|
|
3366
|
+
}
|
|
3162
3367
|
const prevRefById = new Map(
|
|
3163
3368
|
prevLock.filter((e) => e.id && e.catalogRef).map((e) => [migrationByOldId.get(e.id) ?? e.id, e.catalogRef])
|
|
3164
3369
|
);
|
|
@@ -3192,6 +3397,26 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3192
3397
|
await writeManagedJson(root, hausPath(root, "haus.lock.json"), lock, false);
|
|
3193
3398
|
return [...new Set(files)];
|
|
3194
3399
|
}
|
|
3400
|
+
async function skipLocallyEditedItem(root, itemId, prevEntry, force, files, installedPathsByItem, installedIds) {
|
|
3401
|
+
if (!prevEntry || prevEntry.hash === void 0 || force) return false;
|
|
3402
|
+
const existing = await resolveExistingTrackedPaths(root, itemId, prevEntry.paths ?? [], warn);
|
|
3403
|
+
if (existing.length === 0) return false;
|
|
3404
|
+
const currentHash = await hashInstalledPaths(
|
|
3405
|
+
root,
|
|
3406
|
+
existing.map((e) => e.rel)
|
|
3407
|
+
);
|
|
3408
|
+
if (currentHash === prevEntry.hash) return false;
|
|
3409
|
+
warn(
|
|
3410
|
+
`User edited ${itemId} (skipping reinstall): ${existing.map((e) => e.rel).join(", ")} \u2014 use --force to overwrite`
|
|
3411
|
+
);
|
|
3412
|
+
files.push(...existing.map((e) => e.abs));
|
|
3413
|
+
installedPathsByItem.set(
|
|
3414
|
+
itemId,
|
|
3415
|
+
existing.map((e) => e.rel)
|
|
3416
|
+
);
|
|
3417
|
+
installedIds.add(itemId);
|
|
3418
|
+
return true;
|
|
3419
|
+
}
|
|
3195
3420
|
async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, quiet) {
|
|
3196
3421
|
const say = quiet ? () => {
|
|
3197
3422
|
} : log;
|
|
@@ -3199,26 +3424,25 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
3199
3424
|
const entry = prevLock.find((candidate) => candidate.id === migration.oldId);
|
|
3200
3425
|
if (!entry) continue;
|
|
3201
3426
|
const relPaths = entry.paths ?? [];
|
|
3202
|
-
const existing =
|
|
3203
|
-
for (const rel of relPaths) {
|
|
3204
|
-
if (await fs18.pathExists(path19.join(root, rel))) existing.push(rel);
|
|
3205
|
-
}
|
|
3427
|
+
const existing = await resolveExistingTrackedPaths(root, migration.oldId, relPaths, warn);
|
|
3206
3428
|
if (existing.length === 0) continue;
|
|
3207
3429
|
if (entry.hash === void 0) {
|
|
3208
3430
|
warn(
|
|
3209
|
-
`Former catalog item ${migration.oldId} has no lock hash \u2014 leaving old paths in place: ${existing.join(", ")}`
|
|
3431
|
+
`Former catalog item ${migration.oldId} has no lock hash \u2014 leaving old paths in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3210
3432
|
);
|
|
3211
3433
|
continue;
|
|
3212
3434
|
}
|
|
3213
|
-
const currentHash = await hashInstalledPaths(
|
|
3435
|
+
const currentHash = await hashInstalledPaths(
|
|
3436
|
+
root,
|
|
3437
|
+
existing.map((e) => e.rel)
|
|
3438
|
+
);
|
|
3214
3439
|
if (currentHash !== entry.hash) {
|
|
3215
3440
|
warn(
|
|
3216
|
-
`Former catalog item ${migration.oldId} was modified locally \u2014 leaving old paths in place: ${existing.join(", ")}`
|
|
3441
|
+
`Former catalog item ${migration.oldId} was modified locally \u2014 leaving old paths in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3217
3442
|
);
|
|
3218
3443
|
continue;
|
|
3219
3444
|
}
|
|
3220
|
-
for (const
|
|
3221
|
-
const abs = path19.join(root, rel);
|
|
3445
|
+
for (const { abs } of existing) {
|
|
3222
3446
|
if (dryRun) {
|
|
3223
3447
|
say(
|
|
3224
3448
|
`[dry-run] would remove renamed ${displayPath(root, abs)} (${migration.oldId} \u2192 ${migration.newId})`
|
|
@@ -3226,10 +3450,22 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
3226
3450
|
continue;
|
|
3227
3451
|
}
|
|
3228
3452
|
await fs18.remove(abs);
|
|
3229
|
-
await pruneEmptyDir(
|
|
3453
|
+
await pruneEmptyDir(path20.dirname(abs));
|
|
3230
3454
|
}
|
|
3231
3455
|
}
|
|
3232
3456
|
}
|
|
3457
|
+
async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn) {
|
|
3458
|
+
const existing = [];
|
|
3459
|
+
for (const rel of relPaths) {
|
|
3460
|
+
const abs = resolveContainedPath(root, rel);
|
|
3461
|
+
if (abs === null) {
|
|
3462
|
+
warnFn(`Refusing to touch ${itemId}'s recorded path outside the project root: ${rel}`);
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
if (await fs18.pathExists(abs)) existing.push({ rel, abs });
|
|
3466
|
+
}
|
|
3467
|
+
return existing;
|
|
3468
|
+
}
|
|
3233
3469
|
async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
3234
3470
|
const say = quiet ? () => {
|
|
3235
3471
|
} : log;
|
|
@@ -3243,33 +3479,32 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
|
3243
3479
|
if (!removedFromManifest && !deprecated) continue;
|
|
3244
3480
|
const relPaths = entry.paths ?? [];
|
|
3245
3481
|
if (relPaths.length === 0) continue;
|
|
3246
|
-
const existing =
|
|
3247
|
-
for (const rel of relPaths) {
|
|
3248
|
-
if (await fs18.pathExists(path19.join(root, rel))) existing.push(rel);
|
|
3249
|
-
}
|
|
3482
|
+
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
3250
3483
|
if (existing.length === 0) continue;
|
|
3251
3484
|
const pruneReason = deprecated ? "deprecated" : "stale";
|
|
3252
3485
|
if (entry.hash === void 0) {
|
|
3253
3486
|
warn(
|
|
3254
|
-
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.join(", ")}`
|
|
3487
|
+
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3255
3488
|
);
|
|
3256
3489
|
continue;
|
|
3257
3490
|
}
|
|
3258
|
-
const currentHash = await hashInstalledPaths(
|
|
3491
|
+
const currentHash = await hashInstalledPaths(
|
|
3492
|
+
root,
|
|
3493
|
+
existing.map((e) => e.rel)
|
|
3494
|
+
);
|
|
3259
3495
|
if (currentHash !== entry.hash) {
|
|
3260
3496
|
warn(
|
|
3261
|
-
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.join(", ")}`
|
|
3497
|
+
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3262
3498
|
);
|
|
3263
3499
|
continue;
|
|
3264
3500
|
}
|
|
3265
|
-
for (const
|
|
3266
|
-
const abs = path19.join(root, rel);
|
|
3501
|
+
for (const { abs } of existing) {
|
|
3267
3502
|
if (dryRun) {
|
|
3268
3503
|
say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
3269
3504
|
continue;
|
|
3270
3505
|
}
|
|
3271
3506
|
await fs18.remove(abs);
|
|
3272
|
-
await pruneEmptyDir(
|
|
3507
|
+
await pruneEmptyDir(path20.dirname(abs));
|
|
3273
3508
|
say(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
3274
3509
|
}
|
|
3275
3510
|
}
|
|
@@ -3280,12 +3515,12 @@ async function backupBeforePrune(root, absPaths, say) {
|
|
|
3280
3515
|
const backupRoot = hausPath(root, "backups", `prune-${stamp}`);
|
|
3281
3516
|
for (const abs of absPaths) {
|
|
3282
3517
|
if (!await fs18.pathExists(abs)) continue;
|
|
3283
|
-
const rel =
|
|
3284
|
-
const backupPath =
|
|
3285
|
-
await fs18.ensureDir(
|
|
3518
|
+
const rel = path20.relative(root, abs);
|
|
3519
|
+
const backupPath = path20.join(backupRoot, rel);
|
|
3520
|
+
await fs18.ensureDir(path20.dirname(backupPath));
|
|
3286
3521
|
await fs18.copy(abs, backupPath);
|
|
3287
3522
|
}
|
|
3288
|
-
say(`Backed up ${absPaths.length} path(s) to ${
|
|
3523
|
+
say(`Backed up ${absPaths.length} path(s) to ${path20.relative(root, backupRoot)} before pruning.`);
|
|
3289
3524
|
}
|
|
3290
3525
|
async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun, quiet) {
|
|
3291
3526
|
const say = quiet ? () => {
|
|
@@ -3297,21 +3532,21 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
3297
3532
|
if (!entry.id) continue;
|
|
3298
3533
|
const relPaths = entry.paths ?? [];
|
|
3299
3534
|
if (relPaths.length === 0) continue;
|
|
3300
|
-
const existing =
|
|
3301
|
-
for (const rel of relPaths) {
|
|
3302
|
-
if (await fs18.pathExists(path19.join(root, rel))) existing.push(rel);
|
|
3303
|
-
}
|
|
3535
|
+
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
3304
3536
|
if (existing.length === 0) continue;
|
|
3305
3537
|
if (entry.hash === void 0) {
|
|
3306
3538
|
warn(
|
|
3307
|
-
`Orphaned catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.join(", ")}`
|
|
3539
|
+
`Orphaned catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3308
3540
|
);
|
|
3309
3541
|
continue;
|
|
3310
3542
|
}
|
|
3311
|
-
const currentHash = await hashInstalledPaths(
|
|
3543
|
+
const currentHash = await hashInstalledPaths(
|
|
3544
|
+
root,
|
|
3545
|
+
existing.map((e) => e.rel)
|
|
3546
|
+
);
|
|
3312
3547
|
if (currentHash !== entry.hash) {
|
|
3313
3548
|
warn(
|
|
3314
|
-
`Orphaned catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.join(", ")}`
|
|
3549
|
+
`Orphaned catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3315
3550
|
);
|
|
3316
3551
|
continue;
|
|
3317
3552
|
}
|
|
@@ -3320,21 +3555,18 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
3320
3555
|
if (toRemove.length === 0) return;
|
|
3321
3556
|
if (dryRun) {
|
|
3322
3557
|
for (const { id, existing } of toRemove) {
|
|
3323
|
-
for (const
|
|
3324
|
-
say(`[dry-run] would prune orphaned ${displayPath(root,
|
|
3558
|
+
for (const { abs } of existing) {
|
|
3559
|
+
say(`[dry-run] would prune orphaned ${displayPath(root, abs)} (${id})`);
|
|
3325
3560
|
}
|
|
3326
3561
|
}
|
|
3327
3562
|
return;
|
|
3328
3563
|
}
|
|
3329
|
-
const allAbsPaths = toRemove.flatMap(
|
|
3330
|
-
({ existing }) => existing.map((rel) => path19.join(root, rel))
|
|
3331
|
-
);
|
|
3564
|
+
const allAbsPaths = toRemove.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
3332
3565
|
await backupBeforePrune(root, allAbsPaths, say);
|
|
3333
3566
|
for (const { id, existing } of toRemove) {
|
|
3334
|
-
for (const
|
|
3335
|
-
const abs = path19.join(root, rel);
|
|
3567
|
+
for (const { abs } of existing) {
|
|
3336
3568
|
await fs18.remove(abs);
|
|
3337
|
-
await pruneEmptyDir(
|
|
3569
|
+
await pruneEmptyDir(path20.dirname(abs));
|
|
3338
3570
|
say(`Pruned orphaned ${displayPath(root, abs)} (${id})`);
|
|
3339
3571
|
}
|
|
3340
3572
|
}
|
|
@@ -3342,7 +3574,7 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
3342
3574
|
|
|
3343
3575
|
// src/claude/write-gitignore.ts
|
|
3344
3576
|
init_exec();
|
|
3345
|
-
import
|
|
3577
|
+
import path21 from "node:path";
|
|
3346
3578
|
import fs19 from "fs-extra";
|
|
3347
3579
|
var GITIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
3348
3580
|
var GITIGNORE_END = "# HAUS:END haus-managed";
|
|
@@ -3457,14 +3689,14 @@ function injectLinkContextGitignoreBlock(existing) {
|
|
|
3457
3689
|
);
|
|
3458
3690
|
}
|
|
3459
3691
|
async function writeGitignore(root, dryRun) {
|
|
3460
|
-
const filePath =
|
|
3692
|
+
const filePath = path21.join(root, ".gitignore");
|
|
3461
3693
|
const prev = await fs19.pathExists(filePath) ? await fs19.readFile(filePath, "utf8") : "";
|
|
3462
3694
|
const next = injectGitignoreBlock(prev);
|
|
3463
3695
|
await writeManagedText(root, filePath, next, dryRun);
|
|
3464
3696
|
return filePath;
|
|
3465
3697
|
}
|
|
3466
3698
|
async function writeLinkContextGitignore(root, dryRun) {
|
|
3467
|
-
const filePath =
|
|
3699
|
+
const filePath = path21.join(root, ".gitignore");
|
|
3468
3700
|
const prev = await fs19.pathExists(filePath) ? await fs19.readFile(filePath, "utf8") : "";
|
|
3469
3701
|
const next = injectLinkContextGitignoreBlock(prev);
|
|
3470
3702
|
await writeManagedText(root, filePath, next, dryRun);
|
|
@@ -3506,15 +3738,15 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
3506
3738
|
}
|
|
3507
3739
|
|
|
3508
3740
|
// src/refs/fetch-refs.ts
|
|
3509
|
-
import
|
|
3741
|
+
import path23 from "node:path";
|
|
3510
3742
|
import fs21 from "fs-extra";
|
|
3511
3743
|
|
|
3512
3744
|
// src/refs/cache-meta.ts
|
|
3513
|
-
import
|
|
3745
|
+
import path22 from "node:path";
|
|
3514
3746
|
import fs20 from "fs-extra";
|
|
3515
3747
|
var META_FILENAME = "cache-meta.json";
|
|
3516
3748
|
async function readCacheMeta(cacheDir) {
|
|
3517
|
-
const metaPath =
|
|
3749
|
+
const metaPath = path22.join(cacheDir, META_FILENAME);
|
|
3518
3750
|
try {
|
|
3519
3751
|
const raw = await fs20.readFile(metaPath, "utf8");
|
|
3520
3752
|
return JSON.parse(raw);
|
|
@@ -3524,7 +3756,7 @@ async function readCacheMeta(cacheDir) {
|
|
|
3524
3756
|
}
|
|
3525
3757
|
async function writeCacheMeta(cacheDir, meta) {
|
|
3526
3758
|
await fs20.ensureDir(cacheDir);
|
|
3527
|
-
const metaPath =
|
|
3759
|
+
const metaPath = path22.join(cacheDir, META_FILENAME);
|
|
3528
3760
|
await fs20.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
3529
3761
|
}
|
|
3530
3762
|
|
|
@@ -3545,8 +3777,8 @@ function isLlmsTxtUrl(ref) {
|
|
|
3545
3777
|
async function fetchSingleRef(url, cacheDir, meta) {
|
|
3546
3778
|
const existing = meta[url];
|
|
3547
3779
|
const headers = {};
|
|
3548
|
-
const safeFile = existing?.file && existing.file ===
|
|
3549
|
-
const cachedFileExists = safeFile && await fs21.pathExists(
|
|
3780
|
+
const safeFile = existing?.file && existing.file === path23.basename(existing.file);
|
|
3781
|
+
const cachedFileExists = safeFile && await fs21.pathExists(path23.join(cacheDir, existing.file));
|
|
3550
3782
|
if (cachedFileExists) {
|
|
3551
3783
|
if (existing?.etag) headers["If-None-Match"] = existing.etag;
|
|
3552
3784
|
if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
|
|
@@ -3558,7 +3790,7 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
3558
3790
|
const text = await res.text();
|
|
3559
3791
|
const file = `${urlToSlug(url)}.md`;
|
|
3560
3792
|
await fs21.ensureDir(cacheDir);
|
|
3561
|
-
await fs21.writeFile(
|
|
3793
|
+
await fs21.writeFile(path23.join(cacheDir, file), text, "utf8");
|
|
3562
3794
|
meta[url] = {
|
|
3563
3795
|
url,
|
|
3564
3796
|
etag: res.headers.get("etag") ?? void 0,
|
|
@@ -3581,8 +3813,8 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
|
3581
3813
|
if (keepUrls.has(url)) continue;
|
|
3582
3814
|
delete meta[url];
|
|
3583
3815
|
removed++;
|
|
3584
|
-
const safeFile = typeof entry.file === "string" && entry.file ===
|
|
3585
|
-
if (safeFile) await fs21.remove(
|
|
3816
|
+
const safeFile = typeof entry.file === "string" && entry.file === path23.basename(entry.file);
|
|
3817
|
+
if (safeFile) await fs21.remove(path23.join(cacheDir, entry.file)).catch(() => {
|
|
3586
3818
|
});
|
|
3587
3819
|
}
|
|
3588
3820
|
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
@@ -3604,10 +3836,10 @@ async function fetchRefsForItems(items, cacheDir) {
|
|
|
3604
3836
|
const outcome = await fetchSingleRef(url, cacheDir, meta);
|
|
3605
3837
|
if (outcome.result === "fetched") {
|
|
3606
3838
|
summary.fetched++;
|
|
3607
|
-
summary.cachedFiles[url] =
|
|
3839
|
+
summary.cachedFiles[url] = path23.join(cacheDir, outcome.file);
|
|
3608
3840
|
} else if (outcome.result === "unchanged") {
|
|
3609
3841
|
summary.unchanged++;
|
|
3610
|
-
if (meta[url]?.file) summary.cachedFiles[url] =
|
|
3842
|
+
if (meta[url]?.file) summary.cachedFiles[url] = path23.join(cacheDir, meta[url].file);
|
|
3611
3843
|
} else {
|
|
3612
3844
|
summary.failed++;
|
|
3613
3845
|
summary.failedUrls.push(url);
|
|
@@ -3630,7 +3862,7 @@ function parseIdList(value) {
|
|
|
3630
3862
|
// src/commands/apply.ts
|
|
3631
3863
|
var STALE_CACHE_THRESHOLD_DAYS = 7;
|
|
3632
3864
|
async function cacheHasItems() {
|
|
3633
|
-
const data = await readJson(
|
|
3865
|
+
const data = await readJson(path24.join(getCacheDir(), "manifest.json"));
|
|
3634
3866
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
3635
3867
|
}
|
|
3636
3868
|
function installableRecommendedItems(recommended) {
|
|
@@ -3781,7 +4013,7 @@ async function runApply(options) {
|
|
|
3781
4013
|
}
|
|
3782
4014
|
|
|
3783
4015
|
// src/commands/backups.ts
|
|
3784
|
-
import
|
|
4016
|
+
import path25 from "node:path";
|
|
3785
4017
|
import fs22 from "fs-extra";
|
|
3786
4018
|
|
|
3787
4019
|
// src/utils/prompts.ts
|
|
@@ -3818,7 +4050,7 @@ async function listBackups(root) {
|
|
|
3818
4050
|
for (const name of names) {
|
|
3819
4051
|
const kind = classify(name);
|
|
3820
4052
|
if (!kind) continue;
|
|
3821
|
-
const absPath =
|
|
4053
|
+
const absPath = path25.join(dir, name);
|
|
3822
4054
|
const stat = await fs22.lstat(absPath);
|
|
3823
4055
|
if (stat.isSymbolicLink()) continue;
|
|
3824
4056
|
const expectDirectory = kind !== "lock";
|
|
@@ -3848,10 +4080,10 @@ async function runList(root) {
|
|
|
3848
4080
|
}
|
|
3849
4081
|
}
|
|
3850
4082
|
async function isUnsafeWriteTarget(root, target) {
|
|
3851
|
-
const segments =
|
|
4083
|
+
const segments = path25.relative(root, target).split(path25.sep).filter(Boolean);
|
|
3852
4084
|
let current = root;
|
|
3853
4085
|
for (let i = 0; i < segments.length; i++) {
|
|
3854
|
-
current =
|
|
4086
|
+
current = path25.join(current, segments[i]);
|
|
3855
4087
|
const isLastSegment = i === segments.length - 1;
|
|
3856
4088
|
try {
|
|
3857
4089
|
const stat = await fs22.lstat(current);
|
|
@@ -3915,7 +4147,7 @@ async function collectFilesRecursive(dir) {
|
|
|
3915
4147
|
const skippedOther = [];
|
|
3916
4148
|
const names = await fs22.readdir(dir);
|
|
3917
4149
|
for (const name of names) {
|
|
3918
|
-
const abs =
|
|
4150
|
+
const abs = path25.join(dir, name);
|
|
3919
4151
|
const stat = await fs22.lstat(abs);
|
|
3920
4152
|
if (stat.isSymbolicLink()) {
|
|
3921
4153
|
skippedLinks.push(abs);
|
|
@@ -3938,12 +4170,12 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
3938
4170
|
const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
|
|
3939
4171
|
if (skippedLinks.length > 0) {
|
|
3940
4172
|
warn(
|
|
3941
|
-
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) =>
|
|
4173
|
+
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) => path25.relative(entry.absPath, abs)).join(", ")}`
|
|
3942
4174
|
);
|
|
3943
4175
|
}
|
|
3944
4176
|
if (skippedOther.length > 0) {
|
|
3945
4177
|
warn(
|
|
3946
|
-
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) =>
|
|
4178
|
+
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) => path25.relative(entry.absPath, abs)).join(", ")}`
|
|
3947
4179
|
);
|
|
3948
4180
|
}
|
|
3949
4181
|
if (files.length === 0) {
|
|
@@ -3953,8 +4185,8 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
3953
4185
|
const safeFiles = [];
|
|
3954
4186
|
const unsafeTargets = [];
|
|
3955
4187
|
for (const abs of files) {
|
|
3956
|
-
const rel =
|
|
3957
|
-
const target =
|
|
4188
|
+
const rel = path25.relative(entry.absPath, abs);
|
|
4189
|
+
const target = path25.join(root, rel);
|
|
3958
4190
|
if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
|
|
3959
4191
|
else safeFiles.push(abs);
|
|
3960
4192
|
}
|
|
@@ -3971,14 +4203,14 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
3971
4203
|
const vanished = [];
|
|
3972
4204
|
const toRestore = [];
|
|
3973
4205
|
for (const abs of safeFiles) {
|
|
3974
|
-
const rel =
|
|
4206
|
+
const rel = path25.relative(entry.absPath, abs);
|
|
3975
4207
|
const backupFileStat = await lstatIfRegularFile(abs);
|
|
3976
4208
|
if (!backupFileStat) {
|
|
3977
4209
|
vanished.push(rel);
|
|
3978
4210
|
continue;
|
|
3979
4211
|
}
|
|
3980
4212
|
toRestore.push(abs);
|
|
3981
|
-
const target =
|
|
4213
|
+
const target = path25.join(root, rel);
|
|
3982
4214
|
if (await fs22.pathExists(target)) {
|
|
3983
4215
|
const targetStat = await fs22.stat(target);
|
|
3984
4216
|
if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
|
|
@@ -4019,13 +4251,13 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4019
4251
|
let restoredCount = 0;
|
|
4020
4252
|
const skippedAtWrite = [];
|
|
4021
4253
|
for (const abs of toRestore) {
|
|
4022
|
-
const rel =
|
|
4023
|
-
const target =
|
|
4254
|
+
const rel = path25.relative(entry.absPath, abs);
|
|
4255
|
+
const target = path25.join(root, rel);
|
|
4024
4256
|
if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
|
|
4025
4257
|
skippedAtWrite.push(rel);
|
|
4026
4258
|
continue;
|
|
4027
4259
|
}
|
|
4028
|
-
await fs22.ensureDir(
|
|
4260
|
+
await fs22.ensureDir(path25.dirname(target));
|
|
4029
4261
|
await fs22.copy(abs, target, { overwrite: true });
|
|
4030
4262
|
restoredCount++;
|
|
4031
4263
|
}
|
|
@@ -4164,16 +4396,53 @@ async function runCatalogAudit() {
|
|
|
4164
4396
|
}
|
|
4165
4397
|
|
|
4166
4398
|
// src/commands/doctor.ts
|
|
4167
|
-
import
|
|
4168
|
-
import
|
|
4399
|
+
import path39 from "node:path";
|
|
4400
|
+
import fs30 from "fs-extra";
|
|
4401
|
+
|
|
4402
|
+
// src/claude/skill-reference-check.ts
|
|
4403
|
+
import path26 from "node:path";
|
|
4404
|
+
import fg4 from "fast-glob";
|
|
4405
|
+
import fs23 from "fs-extra";
|
|
4406
|
+
var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
|
|
4407
|
+
function stripFencedCodeBlocks(text) {
|
|
4408
|
+
return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
|
|
4409
|
+
}
|
|
4410
|
+
async function findUnresolvedSkillReferences(root) {
|
|
4411
|
+
const skillsRoot = claudePath(root, "skills");
|
|
4412
|
+
if (!await fs23.pathExists(skillsRoot)) return [];
|
|
4413
|
+
const skillDirs = (await fs23.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
|
|
4414
|
+
const installedSkillNames = new Set(skillDirs);
|
|
4415
|
+
const files = await fg4("*/**/*.md", { cwd: skillsRoot, onlyFiles: true, dot: true });
|
|
4416
|
+
const issues = [];
|
|
4417
|
+
for (const rel of files) {
|
|
4418
|
+
if (rel.startsWith("shared/")) continue;
|
|
4419
|
+
const abs = path26.join(skillsRoot, rel);
|
|
4420
|
+
const raw = await fs23.readFile(abs, "utf8");
|
|
4421
|
+
const lines = stripFencedCodeBlocks(raw).split("\n");
|
|
4422
|
+
lines.forEach((line2, idx) => {
|
|
4423
|
+
for (const match of line2.matchAll(REFERENCE_PATTERN)) {
|
|
4424
|
+
const [reference, , bareName] = match;
|
|
4425
|
+
if (!reference || !bareName) continue;
|
|
4426
|
+
issues.push({
|
|
4427
|
+
file: path26.join(".claude/skills", rel),
|
|
4428
|
+
line: idx + 1,
|
|
4429
|
+
reference,
|
|
4430
|
+
bareName,
|
|
4431
|
+
bareInstalled: installedSkillNames.has(bareName)
|
|
4432
|
+
});
|
|
4433
|
+
}
|
|
4434
|
+
});
|
|
4435
|
+
}
|
|
4436
|
+
return issues;
|
|
4437
|
+
}
|
|
4169
4438
|
|
|
4170
4439
|
// src/decisions/doctor.ts
|
|
4171
|
-
import
|
|
4172
|
-
import
|
|
4440
|
+
import path31 from "node:path";
|
|
4441
|
+
import fs27 from "fs-extra";
|
|
4173
4442
|
|
|
4174
4443
|
// src/decisions/check.ts
|
|
4175
|
-
import
|
|
4176
|
-
import
|
|
4444
|
+
import path30 from "node:path";
|
|
4445
|
+
import fs26 from "fs-extra";
|
|
4177
4446
|
|
|
4178
4447
|
// src/decisions/dep-bump.ts
|
|
4179
4448
|
var DEP_SECTIONS = [
|
|
@@ -4182,15 +4451,15 @@ var DEP_SECTIONS = [
|
|
|
4182
4451
|
"peerDependencies",
|
|
4183
4452
|
"optionalDependencies"
|
|
4184
4453
|
];
|
|
4185
|
-
function
|
|
4454
|
+
function isRecord2(value) {
|
|
4186
4455
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
4187
4456
|
}
|
|
4188
4457
|
function extractDepMap(pkg) {
|
|
4189
4458
|
const map = /* @__PURE__ */ new Map();
|
|
4190
|
-
if (!
|
|
4459
|
+
if (!isRecord2(pkg)) return map;
|
|
4191
4460
|
for (const section of DEP_SECTIONS) {
|
|
4192
4461
|
const block = pkg[section];
|
|
4193
|
-
if (!
|
|
4462
|
+
if (!isRecord2(block)) continue;
|
|
4194
4463
|
for (const [name, ver] of Object.entries(block)) {
|
|
4195
4464
|
if (typeof ver === "string") map.set(`${section}:${name}`, ver);
|
|
4196
4465
|
}
|
|
@@ -4216,7 +4485,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
4216
4485
|
} catch {
|
|
4217
4486
|
return false;
|
|
4218
4487
|
}
|
|
4219
|
-
if (!
|
|
4488
|
+
if (!isRecord2(before) || !isRecord2(after)) return false;
|
|
4220
4489
|
const beforeDeps = extractDepMap(before);
|
|
4221
4490
|
const afterDeps = extractDepMap(after);
|
|
4222
4491
|
if (beforeDeps.size === 0 && afterDeps.size === 0) return false;
|
|
@@ -4232,7 +4501,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
4232
4501
|
|
|
4233
4502
|
// src/decisions/diff.ts
|
|
4234
4503
|
init_exec();
|
|
4235
|
-
import
|
|
4504
|
+
import path27 from "node:path";
|
|
4236
4505
|
function isPackageJsonPath(file) {
|
|
4237
4506
|
return file === "package.json" || file.endsWith("/package.json");
|
|
4238
4507
|
}
|
|
@@ -4259,6 +4528,11 @@ function parseDiffRange(range) {
|
|
|
4259
4528
|
}
|
|
4260
4529
|
return null;
|
|
4261
4530
|
}
|
|
4531
|
+
function toAncestryLogRange(range) {
|
|
4532
|
+
const parsed = parseDiffRange(range);
|
|
4533
|
+
if (!parsed) return `${range}..HEAD`;
|
|
4534
|
+
return `${parsed.left}..${parsed.right}`;
|
|
4535
|
+
}
|
|
4262
4536
|
async function getCurrentBranch(root) {
|
|
4263
4537
|
const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root });
|
|
4264
4538
|
if (result.exitCode !== 0) return void 0;
|
|
@@ -4288,7 +4562,7 @@ async function resolvePackageJsonPair(root, file, opts) {
|
|
|
4288
4562
|
return { before: before2, after: after2 };
|
|
4289
4563
|
}
|
|
4290
4564
|
const before = await readBlob(root, `${range}:${file}`) ?? empty;
|
|
4291
|
-
const after = await readText(
|
|
4565
|
+
const after = await readText(path27.join(root, file)) ?? empty;
|
|
4292
4566
|
return { before, after };
|
|
4293
4567
|
}
|
|
4294
4568
|
async function collectDiffStats(root, opts) {
|
|
@@ -4359,22 +4633,22 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
4359
4633
|
}
|
|
4360
4634
|
|
|
4361
4635
|
// src/decisions/paths.ts
|
|
4362
|
-
import
|
|
4363
|
-
import
|
|
4636
|
+
import path28 from "node:path";
|
|
4637
|
+
import fs24 from "fs-extra";
|
|
4364
4638
|
async function resolveDecisionsDir(root) {
|
|
4365
|
-
const primary =
|
|
4366
|
-
if (await
|
|
4367
|
-
const legacy =
|
|
4368
|
-
if (await
|
|
4639
|
+
const primary = path28.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4640
|
+
if (await fs24.pathExists(primary)) return primary;
|
|
4641
|
+
const legacy = path28.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4642
|
+
if (await fs24.pathExists(legacy)) return legacy;
|
|
4369
4643
|
return primary;
|
|
4370
4644
|
}
|
|
4371
4645
|
function relativeDecisionPath(decisionsDir, root) {
|
|
4372
|
-
return
|
|
4646
|
+
return path28.relative(root, decisionsDir).replace(/\\/g, "/");
|
|
4373
4647
|
}
|
|
4374
4648
|
|
|
4375
4649
|
// src/decisions/validate.ts
|
|
4376
|
-
import
|
|
4377
|
-
import
|
|
4650
|
+
import path29 from "node:path";
|
|
4651
|
+
import fs25 from "fs-extra";
|
|
4378
4652
|
function validateDecisionContent(content2) {
|
|
4379
4653
|
const errors = [];
|
|
4380
4654
|
for (const section of DECISIONS_TRIGGERS.requiredSections) {
|
|
@@ -4403,14 +4677,14 @@ function decisionNumberFromFile(name) {
|
|
|
4403
4677
|
return match?.[1] ?? null;
|
|
4404
4678
|
}
|
|
4405
4679
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
4406
|
-
const readmePath =
|
|
4407
|
-
if (!await
|
|
4680
|
+
const readmePath = path29.join(decisionsDir, "README.md");
|
|
4681
|
+
if (!await fs25.pathExists(readmePath)) {
|
|
4408
4682
|
return ["docs/decisions/README.md is missing"];
|
|
4409
4683
|
}
|
|
4410
4684
|
const readme = await readText(readmePath) ?? "";
|
|
4411
4685
|
const errors = [];
|
|
4412
4686
|
for (const file of decisionFiles) {
|
|
4413
|
-
const num = decisionNumberFromFile(
|
|
4687
|
+
const num = decisionNumberFromFile(path29.basename(file));
|
|
4414
4688
|
if (num && !indexReferencesNumber(readme, num)) {
|
|
4415
4689
|
errors.push(`README.md missing index row for ADR-${num}`);
|
|
4416
4690
|
}
|
|
@@ -4505,22 +4779,22 @@ async function checkSatisfaction(input2) {
|
|
|
4505
4779
|
const errors = [];
|
|
4506
4780
|
const decisionsPrefix = `${input2.decisionsRel}/`;
|
|
4507
4781
|
const newDecisionFiles = input2.changedFiles.filter(
|
|
4508
|
-
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(
|
|
4782
|
+
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path30.basename(f))
|
|
4509
4783
|
);
|
|
4510
4784
|
if (newDecisionFiles.length === 0) {
|
|
4511
4785
|
errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
|
|
4512
4786
|
return errors;
|
|
4513
4787
|
}
|
|
4514
|
-
const decisionsDir =
|
|
4788
|
+
const decisionsDir = path30.join(input2.root, input2.decisionsRel);
|
|
4515
4789
|
for (const rel of newDecisionFiles) {
|
|
4516
|
-
const abs =
|
|
4790
|
+
const abs = path30.join(input2.root, rel);
|
|
4517
4791
|
const content2 = await readText(abs) ?? "";
|
|
4518
4792
|
const valid = validateDecisionContent(content2);
|
|
4519
4793
|
if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
|
|
4520
4794
|
}
|
|
4521
4795
|
const indexErrors = await validateIndexSync(
|
|
4522
4796
|
decisionsDir,
|
|
4523
|
-
newDecisionFiles.map((f) =>
|
|
4797
|
+
newDecisionFiles.map((f) => path30.basename(f))
|
|
4524
4798
|
);
|
|
4525
4799
|
errors.push(...indexErrors);
|
|
4526
4800
|
return errors;
|
|
@@ -4576,8 +4850,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
4576
4850
|
changedFiles: stats.files
|
|
4577
4851
|
});
|
|
4578
4852
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
4579
|
-
const legacyExists = await
|
|
4580
|
-
if (legacyExists && !await
|
|
4853
|
+
const legacyExists = await fs26.pathExists(path30.join(root, legacyRel));
|
|
4854
|
+
if (legacyExists && !await fs26.pathExists(decisionsDir)) {
|
|
4581
4855
|
satisfactionErrors = await checkSatisfaction({
|
|
4582
4856
|
root,
|
|
4583
4857
|
decisionsRel: legacyRel,
|
|
@@ -4599,18 +4873,18 @@ ${opts.commitMessages ?? ""}`;
|
|
|
4599
4873
|
return { triggered: true, satisfied: false, reasons, securityTriggered };
|
|
4600
4874
|
}
|
|
4601
4875
|
async function listDecisionNumbers(decisionsDir) {
|
|
4602
|
-
if (!await
|
|
4603
|
-
const entries = await
|
|
4876
|
+
if (!await fs26.pathExists(decisionsDir)) return [];
|
|
4877
|
+
const entries = await fs26.readdir(decisionsDir);
|
|
4604
4878
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
4605
4879
|
}
|
|
4606
4880
|
|
|
4607
4881
|
// src/decisions/doctor.ts
|
|
4608
4882
|
async function auditDecisionsLayout(root) {
|
|
4609
4883
|
const findings = [];
|
|
4610
|
-
const primary =
|
|
4611
|
-
const legacy =
|
|
4612
|
-
const hasPrimary = await
|
|
4613
|
-
const hasLegacy = await
|
|
4884
|
+
const primary = path31.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4885
|
+
const legacy = path31.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4886
|
+
const hasPrimary = await fs27.pathExists(primary);
|
|
4887
|
+
const hasLegacy = await fs27.pathExists(legacy);
|
|
4614
4888
|
if (!hasPrimary && hasLegacy) {
|
|
4615
4889
|
findings.push({
|
|
4616
4890
|
level: "advisory",
|
|
@@ -4623,8 +4897,8 @@ async function auditDecisionsLayout(root) {
|
|
|
4623
4897
|
if (!hasPrimary) return findings;
|
|
4624
4898
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
4625
4899
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
4626
|
-
const readmePath =
|
|
4627
|
-
if (!await
|
|
4900
|
+
const readmePath = path31.join(decisionsDir, "README.md");
|
|
4901
|
+
if (!await fs27.pathExists(readmePath)) {
|
|
4628
4902
|
findings.push({
|
|
4629
4903
|
level: "advisory",
|
|
4630
4904
|
line: `- DECISIONS: missing ${rel}/README.md index`,
|
|
@@ -4634,7 +4908,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4634
4908
|
return findings;
|
|
4635
4909
|
}
|
|
4636
4910
|
const readme = await readText(readmePath);
|
|
4637
|
-
const entries = (await
|
|
4911
|
+
const entries = (await fs27.readdir(decisionsDir)).filter(
|
|
4638
4912
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
4639
4913
|
);
|
|
4640
4914
|
for (const file of entries) {
|
|
@@ -4648,7 +4922,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4648
4922
|
});
|
|
4649
4923
|
}
|
|
4650
4924
|
}
|
|
4651
|
-
const claude = await readText(
|
|
4925
|
+
const claude = await readText(path31.join(root, "CLAUDE.md"));
|
|
4652
4926
|
if (claude && !claude.includes(`@${rel}/README.md`)) {
|
|
4653
4927
|
findings.push({
|
|
4654
4928
|
level: "advisory",
|
|
@@ -4679,11 +4953,11 @@ async function auditDecisionsLayout(root) {
|
|
|
4679
4953
|
}
|
|
4680
4954
|
|
|
4681
4955
|
// src/scanner/read-context.ts
|
|
4682
|
-
import
|
|
4683
|
-
import
|
|
4956
|
+
import path38 from "node:path";
|
|
4957
|
+
import fs29 from "fs-extra";
|
|
4684
4958
|
|
|
4685
4959
|
// src/commands/workspace/config.ts
|
|
4686
|
-
import
|
|
4960
|
+
import path32 from "node:path";
|
|
4687
4961
|
import YAML from "yaml";
|
|
4688
4962
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4689
4963
|
function parseWorkspaceConfig(text) {
|
|
@@ -4706,11 +4980,11 @@ function parseWorkspaceConfig(text) {
|
|
|
4706
4980
|
};
|
|
4707
4981
|
}
|
|
4708
4982
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4709
|
-
return parseWorkspaceConfig(await readText(
|
|
4983
|
+
return parseWorkspaceConfig(await readText(path32.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4710
4984
|
}
|
|
4711
4985
|
|
|
4712
4986
|
// src/workspace/members.ts
|
|
4713
|
-
import
|
|
4987
|
+
import path33 from "node:path";
|
|
4714
4988
|
var REPOS_MANIFEST_FILE = "repos.manifest.json";
|
|
4715
4989
|
var REPOS_LOCAL_FILE = "repos.local.json";
|
|
4716
4990
|
var MemberConfigError = class extends Error {
|
|
@@ -4723,14 +4997,14 @@ var MemberConfigError = class extends Error {
|
|
|
4723
4997
|
};
|
|
4724
4998
|
function resolveAbsPath(workspaceRoot, folder, overrides) {
|
|
4725
4999
|
const override = overrides[folder];
|
|
4726
|
-
if (override) return
|
|
4727
|
-
return
|
|
5000
|
+
if (override) return path33.resolve(workspaceRoot, override);
|
|
5001
|
+
return path33.resolve(workspaceRoot, folder);
|
|
4728
5002
|
}
|
|
4729
5003
|
function isSelfMember(workspaceRoot, member) {
|
|
4730
|
-
return
|
|
5004
|
+
return path33.resolve(member.absPath) === path33.resolve(workspaceRoot);
|
|
4731
5005
|
}
|
|
4732
5006
|
async function readPathOverrides(workspaceRoot) {
|
|
4733
|
-
const filePath =
|
|
5007
|
+
const filePath = path33.join(workspaceRoot, REPOS_LOCAL_FILE);
|
|
4734
5008
|
const result = await readJsonDetailed(filePath);
|
|
4735
5009
|
if (result.status === "missing") return {};
|
|
4736
5010
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -4746,7 +5020,7 @@ async function readPathOverrides(workspaceRoot) {
|
|
|
4746
5020
|
return overrides;
|
|
4747
5021
|
}
|
|
4748
5022
|
async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
4749
|
-
const filePath =
|
|
5023
|
+
const filePath = path33.join(workspaceRoot, WORKSPACE_FILE);
|
|
4750
5024
|
const text = await readText(filePath);
|
|
4751
5025
|
if (text === void 0) return void 0;
|
|
4752
5026
|
const config = parseWorkspaceConfig(text);
|
|
@@ -4759,7 +5033,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
|
4759
5033
|
}));
|
|
4760
5034
|
}
|
|
4761
5035
|
async function readFromManifest(workspaceRoot, overrides) {
|
|
4762
|
-
const filePath =
|
|
5036
|
+
const filePath = path33.join(workspaceRoot, REPOS_MANIFEST_FILE);
|
|
4763
5037
|
const result = await readJsonDetailed(filePath);
|
|
4764
5038
|
if (result.status === "missing") return void 0;
|
|
4765
5039
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -4801,16 +5075,11 @@ async function readMembers(rootInfo) {
|
|
|
4801
5075
|
}
|
|
4802
5076
|
|
|
4803
5077
|
// src/scanner/scan-project.ts
|
|
4804
|
-
import
|
|
4805
|
-
|
|
4806
|
-
// src/utils/audit-checks.ts
|
|
4807
|
-
function isRecord2(v) {
|
|
4808
|
-
return typeof v === "object" && v !== null;
|
|
4809
|
-
}
|
|
5078
|
+
import path37 from "node:path";
|
|
4810
5079
|
|
|
4811
5080
|
// src/utils/git-root.ts
|
|
4812
5081
|
init_exec();
|
|
4813
|
-
import
|
|
5082
|
+
import path34 from "node:path";
|
|
4814
5083
|
function fallback(start) {
|
|
4815
5084
|
return {
|
|
4816
5085
|
cwd: start,
|
|
@@ -4826,11 +5095,11 @@ function fallback(start) {
|
|
|
4826
5095
|
async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
|
|
4827
5096
|
const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
|
|
4828
5097
|
const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
|
|
4829
|
-
if (raw &&
|
|
5098
|
+
if (raw && path34.isAbsolute(raw)) return path34.resolve(raw);
|
|
4830
5099
|
const plain = await runGit(["rev-parse", flag], { cwd });
|
|
4831
5100
|
const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
|
|
4832
5101
|
if (!plainRaw) return "";
|
|
4833
|
-
return
|
|
5102
|
+
return path34.isAbsolute(plainRaw) ? path34.resolve(plainRaw) : path34.resolve(repoRoot, plainRaw);
|
|
4834
5103
|
}
|
|
4835
5104
|
async function resolveRoots(start) {
|
|
4836
5105
|
const cwd = start ?? process.cwd();
|
|
@@ -4839,15 +5108,15 @@ async function resolveRoots(start) {
|
|
|
4839
5108
|
if (toplevel.exitCode !== 0) return fallback(cwd);
|
|
4840
5109
|
const toplevelRaw = toplevel.stdout.trim();
|
|
4841
5110
|
if (!toplevelRaw) return fallback(cwd);
|
|
4842
|
-
const repoRoot =
|
|
5111
|
+
const repoRoot = path34.resolve(toplevelRaw);
|
|
4843
5112
|
const [gitDir, gitCommonDir] = await Promise.all([
|
|
4844
5113
|
resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
|
|
4845
5114
|
resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
|
|
4846
5115
|
]);
|
|
4847
5116
|
if (!gitDir || !gitCommonDir) return fallback(cwd);
|
|
4848
5117
|
const isLinkedWorktree = gitDir !== gitCommonDir;
|
|
4849
|
-
const mainRoot = isLinkedWorktree &&
|
|
4850
|
-
const worktreeName = isLinkedWorktree ?
|
|
5118
|
+
const mainRoot = isLinkedWorktree && path34.basename(gitCommonDir) === ".git" ? path34.dirname(gitCommonDir) : repoRoot;
|
|
5119
|
+
const worktreeName = isLinkedWorktree ? path34.basename(gitDir) : null;
|
|
4851
5120
|
return {
|
|
4852
5121
|
cwd,
|
|
4853
5122
|
repoRoot,
|
|
@@ -4892,8 +5161,8 @@ function resolveMajorVersion(range) {
|
|
|
4892
5161
|
}
|
|
4893
5162
|
|
|
4894
5163
|
// src/scanner/detect-package-manager.ts
|
|
4895
|
-
import
|
|
4896
|
-
import
|
|
5164
|
+
import path35 from "node:path";
|
|
5165
|
+
import fs28 from "fs-extra";
|
|
4897
5166
|
function detectPackageManager(root, packageManagerField) {
|
|
4898
5167
|
const field = String(packageManagerField ?? "").trim();
|
|
4899
5168
|
if (field.startsWith("yarn@")) {
|
|
@@ -4911,9 +5180,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
4911
5180
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
4912
5181
|
return "unknown";
|
|
4913
5182
|
}
|
|
4914
|
-
if (
|
|
4915
|
-
if (
|
|
4916
|
-
if (
|
|
5183
|
+
if (fs28.existsSync(path35.join(root, "yarn.lock"))) return "yarn";
|
|
5184
|
+
if (fs28.existsSync(path35.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
5185
|
+
if (fs28.existsSync(path35.join(root, "package-lock.json"))) return "npm";
|
|
4917
5186
|
return "unknown";
|
|
4918
5187
|
}
|
|
4919
5188
|
|
|
@@ -5030,6 +5299,7 @@ var STACK_RULES = [
|
|
|
5030
5299
|
stack: ["frontend", "shadcn"],
|
|
5031
5300
|
all: [fileEndsWith("components.json"), dep("class-variance-authority")]
|
|
5032
5301
|
},
|
|
5302
|
+
{ stack: ["frontend", "radix"], any: [depPrefix("@radix-ui/")] },
|
|
5033
5303
|
{ stack: ["tooling", "typescript5"], any: [depMajor("typescript", 5)] },
|
|
5034
5304
|
{ stack: ["backend", "sanity"], any: [dep("sanity"), dep("next-sanity"), dep("@sanity/client")] },
|
|
5035
5305
|
{ stack: ["backend", "strapi"], any: [dep("@strapi/strapi"), depPrefix("@strapi/")] },
|
|
@@ -5100,114 +5370,44 @@ var STACK_RULES = [
|
|
|
5100
5370
|
{ stack: ["testing", "playwright"], any: [dep("@playwright/test")] },
|
|
5101
5371
|
{ stack: ["testing", "storybook"], any: [fileIncludes(".storybook")] },
|
|
5102
5372
|
{ stack: ["testing", "testing-library"], any: [depPrefix("@testing-library/")] },
|
|
5103
|
-
{ stack: ["testing", "phpunit"], any: [fileEndsWith("phpunit.xml")] },
|
|
5104
|
-
{ stack: ["testing", "storybook"], any: [depPrefix("@storybook/")] },
|
|
5105
|
-
{ stack: ["testing", "vitest"], any: [dep("vitest")] },
|
|
5106
|
-
{ stack: ["testing", "jest"], any: [dep("jest"), dep("jest-environment-jsdom")] },
|
|
5107
|
-
{ stack: ["databases", "postgresql"], any: [dep("pg")] },
|
|
5108
|
-
{ stack: ["databases", "mariadb"], any: [dep("mariadb"), dep("mysql2")] },
|
|
5109
|
-
{ stack: ["databases", "mysql"], any: [dep("mysql"), dep("mysql2")] },
|
|
5110
|
-
{ stack: ["databases", "mssql"], any: [dep("mssql")] },
|
|
5111
|
-
{ stack: ["databases", "elasticsearch"], any: [dep("@elastic/elasticsearch")] },
|
|
5112
|
-
{ stack: ["databases", "redis"], any: [dep("predis/predis"), dep("ioredis"), dep("redis")] },
|
|
5113
|
-
{ stack: ["auth", "oidc"], any: [content("openid")] },
|
|
5114
|
-
{ stack: ["auth", "azure-ad"], any: [content("AZURE_AD")] },
|
|
5115
|
-
{ stack: ["auth", "bankid"], any: [content("BANKID")] },
|
|
5116
|
-
{
|
|
5117
|
-
stack: ["auth", "saml2"],
|
|
5118
|
-
any: [dep("24slides/laravel-saml2"), dep("aacotroneo/laravel-saml2")]
|
|
5119
|
-
},
|
|
5120
|
-
{ stack: ["auth", "next-auth"], any: [dep("next-auth"), dep("@auth/core")] }
|
|
5121
|
-
];
|
|
5122
|
-
function runDetection(ctx, rules = STACK_RULES) {
|
|
5123
|
-
const roles = [];
|
|
5124
|
-
for (const rule of ROLE_RULES) {
|
|
5125
|
-
if (rule.role && matchRule(rule, ctx) && !roles.includes(rule.role)) roles.push(rule.role);
|
|
5126
|
-
}
|
|
5127
|
-
const stacks = {};
|
|
5128
|
-
for (const bucket of STACK_BUCKETS) stacks[bucket] = [];
|
|
5129
|
-
for (const rule of rules) {
|
|
5130
|
-
if (!rule.stack || !matchRule(rule, ctx)) continue;
|
|
5131
|
-
const [bucket, name] = rule.stack;
|
|
5132
|
-
stacks[bucket] ??= [];
|
|
5133
|
-
if (!stacks[bucket].includes(name)) stacks[bucket].push(name);
|
|
5134
|
-
}
|
|
5135
|
-
return { roles, stacks };
|
|
5136
|
-
}
|
|
5137
|
-
|
|
5138
|
-
// src/scanner/detection.ts
|
|
5139
|
-
import path34 from "node:path";
|
|
5140
|
-
var UNSUPPORTED_MARKERS = {
|
|
5141
|
-
"requirements.txt": "python",
|
|
5142
|
-
"pyproject.toml": "python",
|
|
5143
|
-
"go.mod": "go",
|
|
5144
|
-
"Cargo.toml": "rust",
|
|
5145
|
-
"pom.xml": "java",
|
|
5146
|
-
"build.gradle": "java",
|
|
5147
|
-
"build.gradle.kts": "java",
|
|
5148
|
-
Gemfile: "ruby"
|
|
5149
|
-
};
|
|
5150
|
-
var WEAK_STACK_SIGNALS = /* @__PURE__ */ new Set(["missing-prettier", "missing-eslint"]);
|
|
5151
|
-
function computeDetectionStatus(roles, stacks, unsupportedSignals) {
|
|
5152
|
-
const hasRealStack = Object.entries(stacks).some(
|
|
5153
|
-
([bucket, names]) => bucket !== "packageManagers" && names.some((n) => !WEAK_STACK_SIGNALS.has(n))
|
|
5154
|
-
);
|
|
5155
|
-
const hasRealSignal = roles.length > 0 || hasRealStack;
|
|
5156
|
-
if (!hasRealSignal) return "unknown";
|
|
5157
|
-
return unsupportedSignals.length > 0 ? "partial" : "supported";
|
|
5158
|
-
}
|
|
5159
|
-
function describeUnknownDetection(unsupportedSignals, isLinkedWorktree) {
|
|
5160
|
-
const markers = unsupportedSignals.join(", ");
|
|
5161
|
-
const base = markers ? `Stack not recognised \u2014 detected ${markers}, which haus does not support.` : "Stack not recognised \u2014 no supported framework detected.";
|
|
5162
|
-
const worktreeNote = isLinkedWorktree ? "You're running from inside a git worktree \u2014 sibling repos may not be on disk here, so this could be a false zero-signal reading rather than truly no stack." : "";
|
|
5163
|
-
return [base, worktreeNote, "Only stack-agnostic workflow and security guidance is applied."].filter(Boolean).join(" ");
|
|
5164
|
-
}
|
|
5165
|
-
function blocked(rel) {
|
|
5166
|
-
return SENSITIVE_PATH_REGEXES.some((x) => x.test(rel));
|
|
5167
|
-
}
|
|
5168
|
-
function dependencyVersionMap(pkg, composer) {
|
|
5169
|
-
const versions = /* @__PURE__ */ new Map();
|
|
5170
|
-
const pushObj = (obj) => {
|
|
5171
|
-
if (!isRecord2(obj)) return;
|
|
5172
|
-
for (const key of Object.keys(obj)) {
|
|
5173
|
-
if (versions.has(key)) continue;
|
|
5174
|
-
const value = obj[key];
|
|
5175
|
-
versions.set(key, typeof value === "string" ? value : "");
|
|
5176
|
-
}
|
|
5177
|
-
};
|
|
5178
|
-
pushObj(pkg?.dependencies);
|
|
5179
|
-
pushObj(pkg?.devDependencies);
|
|
5180
|
-
pushObj(composer?.require);
|
|
5181
|
-
pushObj(composer?.["require-dev"]);
|
|
5182
|
-
return versions;
|
|
5183
|
-
}
|
|
5184
|
-
function finalizeRoles(registryRoles, deps, files) {
|
|
5185
|
-
const roles = new Set(registryRoles);
|
|
5186
|
-
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
5187
|
-
const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
|
|
5188
|
-
if (hasWpConfig && hasBedrockLayout) {
|
|
5189
|
-
roles.add("wordpress-bedrock-site");
|
|
5190
|
-
roles.add("wordpress-site");
|
|
5191
|
-
} else if (hasWpConfig) {
|
|
5192
|
-
roles.add("wordpress-vanilla-site");
|
|
5193
|
-
roles.add("wordpress-site");
|
|
5194
|
-
} else if (deps.includes("roots/wordpress")) {
|
|
5195
|
-
roles.add("wordpress-bedrock-site");
|
|
5196
|
-
roles.add("wordpress-site");
|
|
5373
|
+
{ stack: ["testing", "phpunit"], any: [fileEndsWith("phpunit.xml")] },
|
|
5374
|
+
{ stack: ["testing", "storybook"], any: [depPrefix("@storybook/")] },
|
|
5375
|
+
{ stack: ["testing", "vitest"], any: [dep("vitest")] },
|
|
5376
|
+
{ stack: ["testing", "jest"], any: [dep("jest"), dep("jest-environment-jsdom")] },
|
|
5377
|
+
{ stack: ["databases", "postgresql"], any: [dep("pg")] },
|
|
5378
|
+
{ stack: ["databases", "mariadb"], any: [dep("mariadb"), dep("mysql2")] },
|
|
5379
|
+
{ stack: ["databases", "mysql"], any: [dep("mysql"), dep("mysql2")] },
|
|
5380
|
+
{ stack: ["databases", "mssql"], any: [dep("mssql")] },
|
|
5381
|
+
{ stack: ["databases", "elasticsearch"], any: [dep("@elastic/elasticsearch")] },
|
|
5382
|
+
{ stack: ["databases", "redis"], any: [dep("predis/predis"), dep("ioredis"), dep("redis")] },
|
|
5383
|
+
{ stack: ["auth", "oidc"], any: [content("openid")] },
|
|
5384
|
+
{ stack: ["auth", "azure-ad"], any: [content("AZURE_AD")] },
|
|
5385
|
+
{ stack: ["auth", "bankid"], any: [content("BANKID")] },
|
|
5386
|
+
{
|
|
5387
|
+
stack: ["auth", "saml2"],
|
|
5388
|
+
any: [dep("24slides/laravel-saml2"), dep("aacotroneo/laravel-saml2")]
|
|
5389
|
+
},
|
|
5390
|
+
{ stack: ["auth", "next-auth"], any: [dep("next-auth"), dep("@auth/core")] }
|
|
5391
|
+
];
|
|
5392
|
+
function runDetection(ctx, rules = STACK_RULES) {
|
|
5393
|
+
const roles = [];
|
|
5394
|
+
for (const rule of ROLE_RULES) {
|
|
5395
|
+
if (rule.role && matchRule(rule, ctx) && !roles.includes(rule.role)) roles.push(rule.role);
|
|
5197
5396
|
}
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
)
|
|
5205
|
-
|
|
5397
|
+
const stacks = {};
|
|
5398
|
+
for (const bucket of STACK_BUCKETS) stacks[bucket] = [];
|
|
5399
|
+
for (const rule of rules) {
|
|
5400
|
+
if (!rule.stack || !matchRule(rule, ctx)) continue;
|
|
5401
|
+
const [bucket, name] = rule.stack;
|
|
5402
|
+
stacks[bucket] ??= [];
|
|
5403
|
+
if (!stacks[bucket].includes(name)) stacks[bucket].push(name);
|
|
5404
|
+
}
|
|
5405
|
+
return { roles, stacks };
|
|
5206
5406
|
}
|
|
5207
5407
|
|
|
5208
5408
|
// src/scanner/render.ts
|
|
5209
5409
|
import { readFile } from "node:fs/promises";
|
|
5210
|
-
import
|
|
5410
|
+
import path36 from "node:path";
|
|
5211
5411
|
async function buildContentBlob(root, files) {
|
|
5212
5412
|
const candidates = files.filter(
|
|
5213
5413
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -5215,7 +5415,7 @@ async function buildContentBlob(root, files) {
|
|
|
5215
5415
|
const slice = candidates.slice(0, 300);
|
|
5216
5416
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
5217
5417
|
try {
|
|
5218
|
-
return await readFile(
|
|
5418
|
+
return await readFile(path36.join(root, rel), "utf8");
|
|
5219
5419
|
} catch {
|
|
5220
5420
|
return "";
|
|
5221
5421
|
}
|
|
@@ -5307,8 +5507,8 @@ var SAFE_FILES = [
|
|
|
5307
5507
|
"Gemfile"
|
|
5308
5508
|
];
|
|
5309
5509
|
async function scanProject(root) {
|
|
5310
|
-
const pkg = await readJson(
|
|
5311
|
-
const composer = await readJson(
|
|
5510
|
+
const pkg = await readJson(path37.join(root, "package.json"));
|
|
5511
|
+
const composer = await readJson(path37.join(root, "composer.json"));
|
|
5312
5512
|
const files = await listFiles(root, SAFE_FILES);
|
|
5313
5513
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
5314
5514
|
const depVersions = dependencyVersionMap(pkg, composer);
|
|
@@ -5328,7 +5528,7 @@ async function scanProject(root) {
|
|
|
5328
5528
|
const warnings = [];
|
|
5329
5529
|
const securityRisks = [];
|
|
5330
5530
|
const crossRepoHints = [];
|
|
5331
|
-
const nodeEngine =
|
|
5531
|
+
const nodeEngine = isRecord(pkg?.engines) ? String(pkg.engines.node ?? "") : "";
|
|
5332
5532
|
if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
|
|
5333
5533
|
warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
|
|
5334
5534
|
}
|
|
@@ -5338,7 +5538,7 @@ async function scanProject(root) {
|
|
|
5338
5538
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
5339
5539
|
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
5340
5540
|
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
5341
|
-
const repoNameFallback = pkg?.name ? String(pkg.name) :
|
|
5541
|
+
const repoNameFallback = pkg?.name ? String(pkg.name) : path37.basename((await resolveRoots(root)).mainRoot);
|
|
5342
5542
|
const context = {
|
|
5343
5543
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5344
5544
|
repoName: repoNameFallback,
|
|
@@ -5360,14 +5560,15 @@ async function scanProject(root) {
|
|
|
5360
5560
|
// src/scanner/read-context.ts
|
|
5361
5561
|
async function isCacheFresh(root, cachePath) {
|
|
5362
5562
|
const markerPaths = [
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5563
|
+
path38.join(root, "package.json"),
|
|
5564
|
+
path38.join(root, "composer.json"),
|
|
5565
|
+
path38.join(root, WORKSPACE_FILE),
|
|
5566
|
+
path38.join(root, REPOS_MANIFEST_FILE)
|
|
5366
5567
|
];
|
|
5367
|
-
const cacheStat = await
|
|
5568
|
+
const cacheStat = await fs29.stat(cachePath);
|
|
5368
5569
|
for (const markerPath of markerPaths) {
|
|
5369
|
-
if (!await
|
|
5370
|
-
const markerStat = await
|
|
5570
|
+
if (!await fs29.pathExists(markerPath)) continue;
|
|
5571
|
+
const markerStat = await fs29.stat(markerPath);
|
|
5371
5572
|
if (markerStat.mtimeMs > cacheStat.mtimeMs) return false;
|
|
5372
5573
|
}
|
|
5373
5574
|
return true;
|
|
@@ -5468,7 +5669,7 @@ async function runDoctor(options) {
|
|
|
5468
5669
|
} else {
|
|
5469
5670
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
5470
5671
|
}
|
|
5471
|
-
const rootClaudeMdPath =
|
|
5672
|
+
const rootClaudeMdPath = path39.join(root, "CLAUDE.md");
|
|
5472
5673
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
5473
5674
|
if (!rootClaudeMdContent) {
|
|
5474
5675
|
flag(
|
|
@@ -5511,10 +5712,10 @@ async function runDoctor(options) {
|
|
|
5511
5712
|
ok("- CLAUDE.md: import block present");
|
|
5512
5713
|
}
|
|
5513
5714
|
for (const target of actualTargets) {
|
|
5514
|
-
const resolved =
|
|
5515
|
-
const relFromRoot =
|
|
5516
|
-
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${
|
|
5517
|
-
if (escapesRoot || !await
|
|
5715
|
+
const resolved = path39.resolve(root, target);
|
|
5716
|
+
const relFromRoot = path39.relative(root, resolved);
|
|
5717
|
+
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path39.sep}`) || path39.isAbsolute(relFromRoot);
|
|
5718
|
+
if (escapesRoot || !await fs30.pathExists(resolved)) {
|
|
5518
5719
|
flag(
|
|
5519
5720
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
5520
5721
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -5532,7 +5733,7 @@ async function runDoctor(options) {
|
|
|
5532
5733
|
);
|
|
5533
5734
|
}
|
|
5534
5735
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
5535
|
-
const workflowExists = await
|
|
5736
|
+
const workflowExists = await fs30.pathExists(workflowPath);
|
|
5536
5737
|
if (!workflowExists) {
|
|
5537
5738
|
flag(
|
|
5538
5739
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -5547,15 +5748,15 @@ async function runDoctor(options) {
|
|
|
5547
5748
|
} else {
|
|
5548
5749
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
5549
5750
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
5550
|
-
const cachePath =
|
|
5551
|
-
const bundledPath =
|
|
5751
|
+
const cachePath = path39.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
5752
|
+
const bundledPath = path39.join(
|
|
5552
5753
|
packageRoot(),
|
|
5553
5754
|
"library",
|
|
5554
5755
|
"global",
|
|
5555
5756
|
"templates",
|
|
5556
5757
|
"agentic-workflow-standard.md"
|
|
5557
5758
|
);
|
|
5558
|
-
const templatePath = await
|
|
5759
|
+
const templatePath = await fs30.pathExists(cachePath) ? cachePath : bundledPath;
|
|
5559
5760
|
const templateContent = await readText(templatePath);
|
|
5560
5761
|
const verdict = checkManagedTamper(bodyContent, storedHashMatch?.[1], templateContent ?? null);
|
|
5561
5762
|
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
@@ -5576,7 +5777,7 @@ async function runDoctor(options) {
|
|
|
5576
5777
|
}
|
|
5577
5778
|
}
|
|
5578
5779
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
5579
|
-
const workflowConfigExists = await
|
|
5780
|
+
const workflowConfigExists = await fs30.pathExists(workflowConfigPath);
|
|
5580
5781
|
if (!workflowConfigExists) {
|
|
5581
5782
|
flag(
|
|
5582
5783
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -5584,7 +5785,7 @@ async function runDoctor(options) {
|
|
|
5584
5785
|
"haus apply --write"
|
|
5585
5786
|
);
|
|
5586
5787
|
} else {
|
|
5587
|
-
const cfg = await
|
|
5788
|
+
const cfg = await fs30.readFile(workflowConfigPath, "utf8");
|
|
5588
5789
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
5589
5790
|
if (unfilled > 0) {
|
|
5590
5791
|
suggest(
|
|
@@ -5597,7 +5798,7 @@ async function runDoctor(options) {
|
|
|
5597
5798
|
}
|
|
5598
5799
|
}
|
|
5599
5800
|
if (workflowExists) {
|
|
5600
|
-
const prettierIgnore = await readText(
|
|
5801
|
+
const prettierIgnore = await readText(path39.join(root, ".prettierignore")) ?? "";
|
|
5601
5802
|
const lines = prettierIgnore.split("\n");
|
|
5602
5803
|
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
5603
5804
|
if (missing.length > 0) {
|
|
@@ -5637,6 +5838,29 @@ async function runDoctor(options) {
|
|
|
5637
5838
|
if (finding.level === "ok") ok(finding.line);
|
|
5638
5839
|
else suggest(finding.line, finding.sentence, finding.fix);
|
|
5639
5840
|
}
|
|
5841
|
+
const skillRefIssues = await findUnresolvedSkillReferences(root);
|
|
5842
|
+
if (skillRefIssues.length > 0) {
|
|
5843
|
+
const prefixBug = skillRefIssues.filter((i) => i.bareInstalled);
|
|
5844
|
+
const missingDep = skillRefIssues.filter((i) => !i.bareInstalled);
|
|
5845
|
+
const describe = (i) => `${i.file}:${i.line} "${i.reference}"`;
|
|
5846
|
+
if (prefixBug.length > 0) {
|
|
5847
|
+
flag(
|
|
5848
|
+
`- SKILL REFS: ${prefixBug.length} reference(s) use an unresolvable plugin-namespace prefix, though the target skill IS installed (${prefixBug.slice(0, 3).map(describe).join("; ")}${prefixBug.length > 3 ? "; \u2026" : ""})`,
|
|
5849
|
+
`${prefixBug.length} skill reference(s) use a prefix that can't resolve, even though the target skill is installed`,
|
|
5850
|
+
"haus apply --write (re-vendors the skill body with the prefix stripped)"
|
|
5851
|
+
);
|
|
5852
|
+
}
|
|
5853
|
+
if (missingDep.length > 0) {
|
|
5854
|
+
const missingNames = [...new Set(missingDep.map((i) => i.bareName))];
|
|
5855
|
+
flag(
|
|
5856
|
+
`- SKILL REFS: ${missingDep.length} reference(s) point at a skill that is not installed (${missingNames.join(", ")}) (${missingDep.slice(0, 3).map(describe).join("; ")}${missingDep.length > 3 ? "; \u2026" : ""})`,
|
|
5857
|
+
`${missingDep.length} skill reference(s) point at a sub-skill (${missingNames.join(", ")}) that isn't installed`,
|
|
5858
|
+
"find its catalog id and run `haus recommend --include <id>` then `haus apply --write`, or remove the reference"
|
|
5859
|
+
);
|
|
5860
|
+
}
|
|
5861
|
+
} else {
|
|
5862
|
+
ok("- SKILL REFS: OK (no unresolved cross-skill references)");
|
|
5863
|
+
}
|
|
5640
5864
|
const cacheAgeMs = await getCacheManifestAge();
|
|
5641
5865
|
if (cacheAgeMs === null) {
|
|
5642
5866
|
suggest(
|
|
@@ -5657,20 +5881,28 @@ async function runDoctor(options) {
|
|
|
5657
5881
|
}
|
|
5658
5882
|
}
|
|
5659
5883
|
if (recommendation) {
|
|
5660
|
-
|
|
5661
|
-
const recommendedIds = new Set(
|
|
5662
|
-
recommendation.recommended.map((r) => r.id).filter((id) => typeof id === "string")
|
|
5663
|
-
);
|
|
5664
|
-
const orphaned = findOrphanedLockEntries(lock ?? [], recommendedIds).map((entry) => entry.id);
|
|
5665
|
-
if (orphaned.length > 0) {
|
|
5884
|
+
if (!Array.isArray(recommendation.recommended)) {
|
|
5666
5885
|
suggest(
|
|
5667
|
-
`- CATALOG ITEMS:
|
|
5668
|
-
|
|
5669
|
-
"
|
|
5886
|
+
`- CATALOG ITEMS: recommendation.json's "recommended" field is not an array \u2014 skipping orphaned-item check`,
|
|
5887
|
+
'recommendation.json looks malformed (its "recommended" field is not an array)',
|
|
5888
|
+
"run `haus recommend` to regenerate it"
|
|
5889
|
+
);
|
|
5890
|
+
} else {
|
|
5891
|
+
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
5892
|
+
const recommendedIds = new Set(
|
|
5893
|
+
recommendation.recommended.map((r) => r.id).filter((id) => typeof id === "string")
|
|
5670
5894
|
);
|
|
5895
|
+
const orphaned = findOrphanedLockEntries(lock ?? [], recommendedIds).map((entry) => entry.id);
|
|
5896
|
+
if (orphaned.length > 0) {
|
|
5897
|
+
suggest(
|
|
5898
|
+
`- CATALOG ITEMS: ${orphaned.length} installed item(s) no longer in the current recommendation (${orphaned.join(", ")})`,
|
|
5899
|
+
`${orphaned.length} installed item(s) are no longer recommended for this project`,
|
|
5900
|
+
"review whether they are still needed; haus apply --write leaves them in place until removed from the catalog"
|
|
5901
|
+
);
|
|
5902
|
+
}
|
|
5671
5903
|
}
|
|
5672
5904
|
}
|
|
5673
|
-
const pkgJson = await readJson(
|
|
5905
|
+
const pkgJson = await readJson(path39.join(packageRoot(), "package.json"));
|
|
5674
5906
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5675
5907
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
5676
5908
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -5703,12 +5935,12 @@ async function runDoctor(options) {
|
|
|
5703
5935
|
}
|
|
5704
5936
|
|
|
5705
5937
|
// src/commands/update.ts
|
|
5706
|
-
import
|
|
5938
|
+
import path42 from "node:path";
|
|
5707
5939
|
|
|
5708
5940
|
// src/claude/refresh-project.ts
|
|
5709
|
-
import
|
|
5941
|
+
import fs31 from "fs-extra";
|
|
5710
5942
|
async function isHausProject(root) {
|
|
5711
|
-
if (await
|
|
5943
|
+
if (await fs31.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
5712
5944
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
5713
5945
|
if (result.status === "ok") return result.value._haus != null;
|
|
5714
5946
|
return false;
|
|
@@ -5720,8 +5952,8 @@ async function refreshProjectApply(root) {
|
|
|
5720
5952
|
|
|
5721
5953
|
// src/install/apply.ts
|
|
5722
5954
|
import crypto2 from "node:crypto";
|
|
5723
|
-
import
|
|
5724
|
-
import
|
|
5955
|
+
import path40 from "node:path";
|
|
5956
|
+
import fs32 from "fs-extra";
|
|
5725
5957
|
|
|
5726
5958
|
// src/install/header.ts
|
|
5727
5959
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -5805,59 +6037,66 @@ var SCHEMA_VERSION2 = "1";
|
|
|
5805
6037
|
function hashContent(content2) {
|
|
5806
6038
|
return `sha256-${crypto2.createHash("sha256").update(normaliseLF(content2)).digest("hex")}`;
|
|
5807
6039
|
}
|
|
6040
|
+
function destFileMissing(destPath) {
|
|
6041
|
+
try {
|
|
6042
|
+
return !fs32.statSync(destPath).isFile();
|
|
6043
|
+
} catch {
|
|
6044
|
+
return true;
|
|
6045
|
+
}
|
|
6046
|
+
}
|
|
5808
6047
|
function sourceVersion() {
|
|
5809
6048
|
try {
|
|
5810
|
-
const pkgPath =
|
|
5811
|
-
const pkg = JSON.parse(
|
|
6049
|
+
const pkgPath = path40.join(packageRoot(), "package.json");
|
|
6050
|
+
const pkg = JSON.parse(fs32.readFileSync(pkgPath, "utf8"));
|
|
5812
6051
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
5813
6052
|
} catch {
|
|
5814
6053
|
return "haus@0.0.0";
|
|
5815
6054
|
}
|
|
5816
6055
|
}
|
|
5817
6056
|
function globalSrcDir() {
|
|
5818
|
-
return
|
|
6057
|
+
return path40.join(packageRoot(), "library", "global");
|
|
5819
6058
|
}
|
|
5820
6059
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
5821
|
-
const skillDir =
|
|
6060
|
+
const skillDir = path40.join(skillsDir, skillName);
|
|
5822
6061
|
const entries = [];
|
|
5823
|
-
for (const rel of
|
|
6062
|
+
for (const rel of fs32.readdirSync(skillDir, { recursive: true })) {
|
|
5824
6063
|
if (rel === "SKILL.md") continue;
|
|
5825
|
-
const abs =
|
|
5826
|
-
if (!
|
|
5827
|
-
const posixRel = rel.split(
|
|
6064
|
+
const abs = path40.join(skillDir, rel);
|
|
6065
|
+
if (!fs32.statSync(abs).isFile()) continue;
|
|
6066
|
+
const posixRel = rel.split(path40.sep).join("/");
|
|
5828
6067
|
entries.push({
|
|
5829
6068
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
5830
|
-
srcRelPath:
|
|
5831
|
-
destPath:
|
|
6069
|
+
srcRelPath: path40.join("library", "global", "skills", skillName, rel),
|
|
6070
|
+
destPath: path40.join(claudeDir, "skills", skillName, rel)
|
|
5832
6071
|
});
|
|
5833
6072
|
}
|
|
5834
6073
|
return entries;
|
|
5835
6074
|
}
|
|
5836
6075
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
5837
6076
|
const entries = [];
|
|
5838
|
-
const skillsDir =
|
|
5839
|
-
if (
|
|
5840
|
-
for (const skillName of
|
|
5841
|
-
const skillFile =
|
|
5842
|
-
if (
|
|
6077
|
+
const skillsDir = path40.join(srcDir, "skills");
|
|
6078
|
+
if (fs32.pathExistsSync(skillsDir)) {
|
|
6079
|
+
for (const skillName of fs32.readdirSync(skillsDir)) {
|
|
6080
|
+
const skillFile = path40.join(skillsDir, skillName, "SKILL.md");
|
|
6081
|
+
if (fs32.pathExistsSync(skillFile)) {
|
|
5843
6082
|
entries.push({
|
|
5844
6083
|
stableId: `skill.${skillName}`,
|
|
5845
|
-
srcRelPath:
|
|
5846
|
-
destPath:
|
|
6084
|
+
srcRelPath: path40.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
6085
|
+
destPath: path40.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
5847
6086
|
});
|
|
5848
6087
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
5849
6088
|
}
|
|
5850
6089
|
}
|
|
5851
6090
|
}
|
|
5852
|
-
const commandsDir =
|
|
5853
|
-
if (
|
|
5854
|
-
for (const fileName of
|
|
6091
|
+
const commandsDir = path40.join(srcDir, "commands");
|
|
6092
|
+
if (fs32.pathExistsSync(commandsDir)) {
|
|
6093
|
+
for (const fileName of fs32.readdirSync(commandsDir)) {
|
|
5855
6094
|
if (!fileName.endsWith(".md")) continue;
|
|
5856
6095
|
const commandName = fileName.slice(0, -".md".length);
|
|
5857
6096
|
entries.push({
|
|
5858
6097
|
stableId: `command.${commandName}`,
|
|
5859
|
-
srcRelPath:
|
|
5860
|
-
destPath:
|
|
6098
|
+
srcRelPath: path40.join("library", "global", "commands", fileName),
|
|
6099
|
+
destPath: path40.join(claudeDir, "commands", fileName)
|
|
5861
6100
|
});
|
|
5862
6101
|
}
|
|
5863
6102
|
}
|
|
@@ -5883,7 +6122,7 @@ async function applyInstall(options = {}) {
|
|
|
5883
6122
|
};
|
|
5884
6123
|
const manifestFiles = [];
|
|
5885
6124
|
for (const entry of sourceFiles) {
|
|
5886
|
-
const srcPath =
|
|
6125
|
+
const srcPath = path40.join(packageRoot(), entry.srcRelPath);
|
|
5887
6126
|
const rawContent = await readText(srcPath);
|
|
5888
6127
|
if (rawContent === void 0) {
|
|
5889
6128
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -5897,13 +6136,14 @@ async function applyInstall(options = {}) {
|
|
|
5897
6136
|
const newHash = hashContent(stamped);
|
|
5898
6137
|
const existing = manifestByDest.get(entry.destPath);
|
|
5899
6138
|
if (check) {
|
|
5900
|
-
|
|
6139
|
+
const destMissing = destFileMissing(entry.destPath);
|
|
6140
|
+
if (destMissing || existing && existing.hash !== newHash) {
|
|
5901
6141
|
result.drift = true;
|
|
5902
6142
|
result.skipped.push(entry.destPath);
|
|
5903
6143
|
}
|
|
5904
6144
|
continue;
|
|
5905
6145
|
}
|
|
5906
|
-
const destExists =
|
|
6146
|
+
const destExists = fs32.pathExistsSync(entry.destPath);
|
|
5907
6147
|
if (destExists) {
|
|
5908
6148
|
const currentContent = await readText(entry.destPath);
|
|
5909
6149
|
if (currentContent !== void 0) {
|
|
@@ -5943,7 +6183,7 @@ async function applyInstall(options = {}) {
|
|
|
5943
6183
|
}
|
|
5944
6184
|
let mergedSettings;
|
|
5945
6185
|
if (!check) {
|
|
5946
|
-
const fragmentPath =
|
|
6186
|
+
const fragmentPath = path40.join(srcDir, "settings-fragments", "hooks.json");
|
|
5947
6187
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
5948
6188
|
const settings = await readSettings();
|
|
5949
6189
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -5957,13 +6197,13 @@ async function applyInstall(options = {}) {
|
|
|
5957
6197
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
5958
6198
|
for (const entry of existingManifest.files) {
|
|
5959
6199
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
5960
|
-
if (!
|
|
6200
|
+
if (!fs32.pathExistsSync(entry.destPath)) continue;
|
|
5961
6201
|
const content2 = await readText(entry.destPath);
|
|
5962
6202
|
if (!content2) continue;
|
|
5963
6203
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
5964
6204
|
const currentHash = hashContent(content2);
|
|
5965
6205
|
if (hasHeader && currentHash === entry.hash) {
|
|
5966
|
-
if (!dryRun) await
|
|
6206
|
+
if (!dryRun) await fs32.remove(entry.destPath);
|
|
5967
6207
|
result.deleted.push(entry.destPath);
|
|
5968
6208
|
} else {
|
|
5969
6209
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -6032,7 +6272,7 @@ function summarizeLockDiff(before, after) {
|
|
|
6032
6272
|
|
|
6033
6273
|
// src/update/lockfile.ts
|
|
6034
6274
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
6035
|
-
import
|
|
6275
|
+
import path41 from "node:path";
|
|
6036
6276
|
async function readLockSummary(root) {
|
|
6037
6277
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
6038
6278
|
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
@@ -6056,19 +6296,21 @@ async function checkLock(root) {
|
|
|
6056
6296
|
const ok = lock.length > 0 && hasValidVersions && drift.length === 0;
|
|
6057
6297
|
return { ok, count: lock.length, catalogRef, drift, driftCount: drift.length };
|
|
6058
6298
|
}
|
|
6059
|
-
async function
|
|
6060
|
-
const lockPath = hausPath(root, "haus.lock.json");
|
|
6061
|
-
let before = "[]";
|
|
6299
|
+
async function readLockRaw(root) {
|
|
6062
6300
|
try {
|
|
6063
|
-
|
|
6301
|
+
return await readFile2(hausPath(root, "haus.lock.json"), "utf8");
|
|
6064
6302
|
} catch {
|
|
6065
|
-
|
|
6303
|
+
return "[]";
|
|
6066
6304
|
}
|
|
6305
|
+
}
|
|
6306
|
+
async function applyLock(root) {
|
|
6307
|
+
const lockPath = hausPath(root, "haus.lock.json");
|
|
6308
|
+
const before = await readLockRaw(root);
|
|
6067
6309
|
const lock = await readJson(lockPath) ?? [];
|
|
6068
6310
|
try {
|
|
6069
6311
|
const backupDir = hausPath(root, "backups");
|
|
6070
6312
|
await mkdir(backupDir, { recursive: true });
|
|
6071
|
-
await copyFile(lockPath,
|
|
6313
|
+
await copyFile(lockPath, path41.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
6072
6314
|
} catch {
|
|
6073
6315
|
}
|
|
6074
6316
|
const enriched = await Promise.all(
|
|
@@ -6090,7 +6332,7 @@ function diffLock(before, after) {
|
|
|
6090
6332
|
}
|
|
6091
6333
|
async function hasLocalOverrides(root) {
|
|
6092
6334
|
try {
|
|
6093
|
-
await readFile2(
|
|
6335
|
+
await readFile2(path41.join(root, ".claude", "settings.json"), "utf8");
|
|
6094
6336
|
return true;
|
|
6095
6337
|
} catch {
|
|
6096
6338
|
return false;
|
|
@@ -6106,7 +6348,7 @@ async function runUpdate(options) {
|
|
|
6106
6348
|
return;
|
|
6107
6349
|
}
|
|
6108
6350
|
if (options.check) {
|
|
6109
|
-
const pkgJson2 = await readJson(
|
|
6351
|
+
const pkgJson2 = await readJson(path42.join(packageRoot(), "package.json"));
|
|
6110
6352
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
6111
6353
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift, catalogItems, lockItems] = await Promise.all([
|
|
6112
6354
|
options.fast ? fastLockStatus(root) : checkLock(root),
|
|
@@ -6138,10 +6380,12 @@ async function runUpdate(options) {
|
|
|
6138
6380
|
)
|
|
6139
6381
|
);
|
|
6140
6382
|
const hasHashDrift = !options.fast && status.count > 0 && "ok" in status && !status.ok;
|
|
6141
|
-
if (hasHashDrift || formerIdMigrations.length > 0
|
|
6383
|
+
if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true) {
|
|
6384
|
+
process.exitCode = 1;
|
|
6385
|
+
}
|
|
6142
6386
|
return;
|
|
6143
6387
|
}
|
|
6144
|
-
const pkgJson = await readJson(
|
|
6388
|
+
const pkgJson = await readJson(path42.join(packageRoot(), "package.json"));
|
|
6145
6389
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6146
6390
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
6147
6391
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -6153,9 +6397,7 @@ async function runUpdate(options) {
|
|
|
6153
6397
|
if (await hasLocalOverrides(root)) {
|
|
6154
6398
|
log("Existing .claude/settings.json \u2014 haus rules will be merged, not replaced.");
|
|
6155
6399
|
}
|
|
6156
|
-
const
|
|
6157
|
-
log(diffLock(before, after));
|
|
6158
|
-
log(summarizeLockDiff(before, after));
|
|
6400
|
+
const lockBefore = await readLockRaw(root);
|
|
6159
6401
|
log("Syncing remote catalog...");
|
|
6160
6402
|
const sync = await syncRemoteCatalog();
|
|
6161
6403
|
if (sync.newItems.length > 0) {
|
|
@@ -6177,13 +6419,16 @@ async function runUpdate(options) {
|
|
|
6177
6419
|
}
|
|
6178
6420
|
await refreshGlobalInstall();
|
|
6179
6421
|
await refreshProjectFiles(root);
|
|
6422
|
+
const { after } = await applyLock(root);
|
|
6423
|
+
log(diffLock(lockBefore, after));
|
|
6424
|
+
log(summarizeLockDiff(lockBefore, after));
|
|
6180
6425
|
log("Update applied with backup in .haus-workflow/backups/. Run haus doctor.");
|
|
6181
6426
|
}
|
|
6182
6427
|
async function runFromHookCheck(root) {
|
|
6183
6428
|
try {
|
|
6184
6429
|
const summary = await readLockSummary(root);
|
|
6185
6430
|
if (summary.count === 0) return;
|
|
6186
|
-
const pkgJson = await readJson(
|
|
6431
|
+
const pkgJson = await readJson(path42.join(packageRoot(), "package.json"));
|
|
6187
6432
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6188
6433
|
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
6189
6434
|
fetchNpmVersionStatus(currentVersion),
|
|
@@ -6333,7 +6578,8 @@ async function runCiGate(options = {}) {
|
|
|
6333
6578
|
// src/commands/clone.ts
|
|
6334
6579
|
init_exec();
|
|
6335
6580
|
import { existsSync as existsSync2 } from "node:fs";
|
|
6336
|
-
import
|
|
6581
|
+
import { readdir } from "node:fs/promises";
|
|
6582
|
+
import path43 from "node:path";
|
|
6337
6583
|
var GIT_LOCATION_VARS = [
|
|
6338
6584
|
"GIT_DIR",
|
|
6339
6585
|
"GIT_WORK_TREE",
|
|
@@ -6362,6 +6608,16 @@ function normalizeGitUrl(url) {
|
|
|
6362
6608
|
s = s.replace(/\.git$/i, "");
|
|
6363
6609
|
return s.toLowerCase();
|
|
6364
6610
|
}
|
|
6611
|
+
async function isEmptyDirectory(dir) {
|
|
6612
|
+
try {
|
|
6613
|
+
const entries = await readdir(dir);
|
|
6614
|
+
return entries.length === 0;
|
|
6615
|
+
} catch (err) {
|
|
6616
|
+
const code = err.code;
|
|
6617
|
+
if (code === "ENOTDIR" || code === "ENOENT") return false;
|
|
6618
|
+
throw err;
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6365
6621
|
async function checkExistingTarget(target, url) {
|
|
6366
6622
|
const isRepo = await runGit(["-C", target, "rev-parse", "--is-inside-work-tree"], {
|
|
6367
6623
|
env: cloneEnv(),
|
|
@@ -6383,11 +6639,11 @@ async function runClone(url, opts = {}) {
|
|
|
6383
6639
|
process.exitCode = 1;
|
|
6384
6640
|
return;
|
|
6385
6641
|
}
|
|
6386
|
-
const target =
|
|
6387
|
-
if (existsSync2(target)) {
|
|
6642
|
+
const target = path43.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
6643
|
+
if (existsSync2(target) && !await isEmptyDirectory(target)) {
|
|
6388
6644
|
const check = await checkExistingTarget(target, url);
|
|
6389
6645
|
if (check.kind === "matches") {
|
|
6390
|
-
log(`\u2022 ${
|
|
6646
|
+
log(`\u2022 ${path43.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
6391
6647
|
return;
|
|
6392
6648
|
}
|
|
6393
6649
|
const message = check.kind === "not-a-repo" ? `${target} already exists and is not a git repository \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].` : `${target} already exists as a different repository (origin: ${check.origin ?? "(no origin remote)"}) \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].`;
|
|
@@ -6427,7 +6683,7 @@ function readHookPayload() {
|
|
|
6427
6683
|
if (!raw) return { payload: {}, malformed: false };
|
|
6428
6684
|
try {
|
|
6429
6685
|
const parsed = JSON.parse(raw);
|
|
6430
|
-
return { payload:
|
|
6686
|
+
return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
|
|
6431
6687
|
} catch {
|
|
6432
6688
|
return { payload: {}, malformed: true };
|
|
6433
6689
|
}
|
|
@@ -6466,7 +6722,7 @@ function readPayload(options) {
|
|
|
6466
6722
|
if (options.stdinPayload != null) {
|
|
6467
6723
|
try {
|
|
6468
6724
|
const parsed = JSON.parse(options.stdinPayload);
|
|
6469
|
-
return { payload:
|
|
6725
|
+
return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
|
|
6470
6726
|
} catch {
|
|
6471
6727
|
return { payload: {}, malformed: true };
|
|
6472
6728
|
}
|
|
@@ -6479,15 +6735,14 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6479
6735
|
emitPreToolUseDeny("Malformed hook payload");
|
|
6480
6736
|
return;
|
|
6481
6737
|
}
|
|
6482
|
-
const toolInput =
|
|
6738
|
+
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
6483
6739
|
const command = String(toolInput.command ?? "");
|
|
6484
6740
|
if (!PR_CREATE_PATTERN.test(command)) return;
|
|
6485
6741
|
const root = process.cwd();
|
|
6486
6742
|
const baseRef = await resolveBaseRef(root);
|
|
6487
6743
|
if (!baseRef) return;
|
|
6488
6744
|
const range = `${baseRef}...HEAD`;
|
|
6489
|
-
const
|
|
6490
|
-
const commitMessages = (await runGit(["log", "--format=%B", logRange], { cwd: root })).stdout;
|
|
6745
|
+
const commitMessages = (await runGit(["log", "--format=%B", toAncestryLogRange(range)], { cwd: root })).stdout;
|
|
6491
6746
|
const result = await runDecisionsCheck(root, { range, commitMessages });
|
|
6492
6747
|
if (result.triggered && !result.satisfied) {
|
|
6493
6748
|
emitPreToolUseDeny(
|
|
@@ -6497,7 +6752,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6497
6752
|
}
|
|
6498
6753
|
|
|
6499
6754
|
// src/commands/decisions.ts
|
|
6500
|
-
import
|
|
6755
|
+
import path44 from "node:path";
|
|
6501
6756
|
|
|
6502
6757
|
// src/decisions/next-number.ts
|
|
6503
6758
|
async function nextDecisionNumber(root) {
|
|
@@ -6572,7 +6827,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6572
6827
|
const root = process.cwd();
|
|
6573
6828
|
if (sub === "check") {
|
|
6574
6829
|
const commitMessages = options.range != null ? (await Promise.resolve().then(() => (init_exec(), exec_exports)).then(
|
|
6575
|
-
(m) => m.runGit(["log", "--format=%B", options.range], { cwd: root })
|
|
6830
|
+
(m) => m.runGit(["log", "--format=%B", toAncestryLogRange(options.range)], { cwd: root })
|
|
6576
6831
|
)).stdout : "";
|
|
6577
6832
|
const result = await runDecisionsCheck(root, {
|
|
6578
6833
|
staged: options.staged,
|
|
@@ -6607,7 +6862,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6607
6862
|
return;
|
|
6608
6863
|
}
|
|
6609
6864
|
if (sub === "validate") {
|
|
6610
|
-
const target = arg ?
|
|
6865
|
+
const target = arg ? path44.resolve(root, arg) : void 0;
|
|
6611
6866
|
if (!target) {
|
|
6612
6867
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
6613
6868
|
process.exitCode = 1;
|
|
@@ -6859,7 +7114,7 @@ async function runGuard(kind, _options) {
|
|
|
6859
7114
|
emitPreToolUseDeny("Malformed hook payload");
|
|
6860
7115
|
return;
|
|
6861
7116
|
}
|
|
6862
|
-
const toolInput =
|
|
7117
|
+
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
6863
7118
|
if (kind === "file-access") {
|
|
6864
7119
|
const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
|
|
6865
7120
|
const reason2 = guardFileAccess(candidate);
|
|
@@ -6876,8 +7131,8 @@ async function runGuard(kind, _options) {
|
|
|
6876
7131
|
}
|
|
6877
7132
|
|
|
6878
7133
|
// src/commands/init.ts
|
|
6879
|
-
import
|
|
6880
|
-
import
|
|
7134
|
+
import path46 from "node:path";
|
|
7135
|
+
import fs33 from "fs-extra";
|
|
6881
7136
|
|
|
6882
7137
|
// src/recommender/git-signal.ts
|
|
6883
7138
|
init_exec();
|
|
@@ -6900,55 +7155,6 @@ async function readChangedFiles(root) {
|
|
|
6900
7155
|
return [.../* @__PURE__ */ new Set([...unstaged, ...staged, ...untracked])].sort();
|
|
6901
7156
|
}
|
|
6902
7157
|
|
|
6903
|
-
// src/recommender/policies.ts
|
|
6904
|
-
var UNSUPPORTED = FORBIDDEN_TAGS;
|
|
6905
|
-
function matchRequiresAny(clauses, ctx) {
|
|
6906
|
-
for (const clause of clauses) {
|
|
6907
|
-
if ("stack" in clause) {
|
|
6908
|
-
if (ctx.stackSet.has(clause.stack.toLowerCase())) {
|
|
6909
|
-
return { matched: true, signal: `stack:${clause.stack}` };
|
|
6910
|
-
}
|
|
6911
|
-
} else if ("dependency" in clause) {
|
|
6912
|
-
if (ctx.depSet.has(clause.dependency.toLowerCase())) {
|
|
6913
|
-
return { matched: true, signal: `dependency:${clause.dependency}` };
|
|
6914
|
-
}
|
|
6915
|
-
} else if ("packageNamePattern" in clause) {
|
|
6916
|
-
const pattern = clause.packageNamePattern.toLowerCase();
|
|
6917
|
-
const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
|
|
6918
|
-
for (const dep2 of ctx.depSet) {
|
|
6919
|
-
if (pattern.endsWith("*") ? dep2.startsWith(prefix) : dep2 === pattern) {
|
|
6920
|
-
return {
|
|
6921
|
-
matched: true,
|
|
6922
|
-
signal: `packageNamePattern:${clause.packageNamePattern}`
|
|
6923
|
-
};
|
|
6924
|
-
}
|
|
6925
|
-
}
|
|
6926
|
-
} else if ("role" in clause) {
|
|
6927
|
-
if (ctx.roleSet.has(clause.role.toLowerCase())) {
|
|
6928
|
-
return { matched: true, signal: `role:${clause.role}` };
|
|
6929
|
-
}
|
|
6930
|
-
}
|
|
6931
|
-
}
|
|
6932
|
-
return { matched: false };
|
|
6933
|
-
}
|
|
6934
|
-
function describeRequiresAny(clauses) {
|
|
6935
|
-
return clauses.map((c) => {
|
|
6936
|
-
if ("stack" in c) return `stack=${c.stack}`;
|
|
6937
|
-
if ("dependency" in c) return `dependency=${c.dependency}`;
|
|
6938
|
-
if ("packageNamePattern" in c) return `packageNamePattern=${c.packageNamePattern}`;
|
|
6939
|
-
if ("role" in c) return `role=${c.role}`;
|
|
6940
|
-
return "unknown";
|
|
6941
|
-
}).join(" | ");
|
|
6942
|
-
}
|
|
6943
|
-
function mergeRecommendationWarnings(context, isLinkedWorktree = false) {
|
|
6944
|
-
const markers = context.unsupportedSignals?.join(", ");
|
|
6945
|
-
const statusLines = context.detectionStatus === "unknown" ? [describeUnknownDetection(context.unsupportedSignals ?? [], isLinkedWorktree)] : context.detectionStatus === "partial" && markers ? [
|
|
6946
|
-
`Partially supported \u2014 found unsupported ${markers} alongside recognised stacks; guidance covers the supported parts only.`
|
|
6947
|
-
] : [];
|
|
6948
|
-
const riskLines = (context.securityRisks?.length ?? 0) > 0 ? [`Scan reported security signals: ${context.securityRisks.join("; ")}`] : [];
|
|
6949
|
-
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
6950
|
-
}
|
|
6951
|
-
|
|
6952
7158
|
// src/recommender/recommend.ts
|
|
6953
7159
|
async function recommend(root, context, opts = {}) {
|
|
6954
7160
|
const items = await loadCatalog(root);
|
|
@@ -7265,6 +7471,7 @@ function applyCoInstallSuppression(recommended, skipped) {
|
|
|
7265
7471
|
}
|
|
7266
7472
|
var HARD_SKIP_CODES = /* @__PURE__ */ new Set([
|
|
7267
7473
|
"former-id",
|
|
7474
|
+
"invalid-source",
|
|
7268
7475
|
"unsupported-policy",
|
|
7269
7476
|
"deprecated",
|
|
7270
7477
|
"curated-not-approved",
|
|
@@ -7287,8 +7494,10 @@ function applyManualIncludes(include, items, recommended, skipped) {
|
|
|
7287
7494
|
if (recommendedIds.has(id)) continue;
|
|
7288
7495
|
const skipIdx = skipped.findIndex((s) => s.id === id);
|
|
7289
7496
|
const skipCode = skipIdx >= 0 ? skipped[skipIdx].skipReasons[0]?.code : void 0;
|
|
7290
|
-
|
|
7291
|
-
|
|
7497
|
+
const failingGateNames = skipIdx >= 0 ? (skipped[skipIdx].gates ?? []).filter((g) => !g.passed).map((g) => g.name) : [];
|
|
7498
|
+
const hardBlock = failingGateNames.find((name) => HARD_SKIP_CODES.has(name));
|
|
7499
|
+
if (hardBlock) {
|
|
7500
|
+
warnings.push(`--include: "${id}" cannot be force-installed (blocked by ${hardBlock})`);
|
|
7292
7501
|
continue;
|
|
7293
7502
|
}
|
|
7294
7503
|
if (skipCode === "requires-any-unsatisfied") {
|
|
@@ -7344,8 +7553,8 @@ function buildOptInEligible(items, skipped) {
|
|
|
7344
7553
|
}
|
|
7345
7554
|
|
|
7346
7555
|
// src/commands/workspace/discover.ts
|
|
7347
|
-
import
|
|
7348
|
-
import
|
|
7556
|
+
import path45 from "node:path";
|
|
7557
|
+
import fg5 from "fast-glob";
|
|
7349
7558
|
import YAML2 from "yaml";
|
|
7350
7559
|
var DEFAULT_MAX_DEPTH = 3;
|
|
7351
7560
|
var GIT_MARKER = ["**/.git"];
|
|
@@ -7379,16 +7588,16 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7379
7588
|
suppressErrors: true
|
|
7380
7589
|
};
|
|
7381
7590
|
const [gitMatches, manifestMatches] = await Promise.all([
|
|
7382
|
-
|
|
7383
|
-
|
|
7591
|
+
fg5(GIT_MARKER, { ...commonOpts, ignore: IGNORE }),
|
|
7592
|
+
fg5(MANIFEST_MARKERS, { ...commonOpts, ignore: [...IGNORE, "**/.git/**"] })
|
|
7384
7593
|
]);
|
|
7385
7594
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
7386
7595
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
7387
7596
|
for (const match of gitMatches) {
|
|
7388
|
-
gitDirs.add(
|
|
7597
|
+
gitDirs.add(path45.posix.dirname(match));
|
|
7389
7598
|
}
|
|
7390
7599
|
for (const match of manifestMatches) {
|
|
7391
|
-
manifestDirs.add(
|
|
7600
|
+
manifestDirs.add(path45.posix.dirname(match));
|
|
7392
7601
|
}
|
|
7393
7602
|
const repoRoots = [...gitDirs];
|
|
7394
7603
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -7405,9 +7614,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7405
7614
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
7406
7615
|
const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
|
|
7407
7616
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
7408
|
-
const absDir =
|
|
7409
|
-
const pkg = await readJson(
|
|
7410
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
7617
|
+
const absDir = path45.resolve(workspaceRoot, relDir);
|
|
7618
|
+
const pkg = await readJson(path45.join(absDir, "package.json"));
|
|
7619
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path45.basename(absDir);
|
|
7411
7620
|
let role = "auto";
|
|
7412
7621
|
try {
|
|
7413
7622
|
const scan = await scanProject(absDir);
|
|
@@ -7450,7 +7659,7 @@ function renderWorkspaceYaml(config) {
|
|
|
7450
7659
|
});
|
|
7451
7660
|
}
|
|
7452
7661
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
7453
|
-
const yamlPath =
|
|
7662
|
+
const yamlPath = path45.join(workspaceRoot, "haus.workspace.yaml");
|
|
7454
7663
|
const existingText = await readText(yamlPath);
|
|
7455
7664
|
const existing = parseWorkspaceConfig(existingText);
|
|
7456
7665
|
if (existingText && !existing) {
|
|
@@ -7628,8 +7837,8 @@ async function runSetupProject(options) {
|
|
|
7628
7837
|
// src/commands/init.ts
|
|
7629
7838
|
async function runInit(options) {
|
|
7630
7839
|
const root = process.cwd();
|
|
7631
|
-
const hausDir =
|
|
7632
|
-
const alreadyInit = await
|
|
7840
|
+
const hausDir = path46.join(root, ".haus-workflow");
|
|
7841
|
+
const alreadyInit = await fs33.pathExists(hausDir);
|
|
7633
7842
|
if (alreadyInit) {
|
|
7634
7843
|
log("Haus AI already initialized in this project.");
|
|
7635
7844
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -7764,8 +7973,8 @@ async function runRefresh() {
|
|
|
7764
7973
|
}
|
|
7765
7974
|
|
|
7766
7975
|
// src/install/scaffold.ts
|
|
7767
|
-
import
|
|
7768
|
-
import
|
|
7976
|
+
import path47 from "node:path";
|
|
7977
|
+
import fs34 from "fs-extra";
|
|
7769
7978
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
7770
7979
|
const scaffolded = [];
|
|
7771
7980
|
const skipped = [];
|
|
@@ -7778,27 +7987,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7778
7987
|
skipped.push(name);
|
|
7779
7988
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
7780
7989
|
};
|
|
7781
|
-
const realRoot = await
|
|
7990
|
+
const realRoot = await fs34.realpath(catalogRoot).catch(() => null);
|
|
7782
7991
|
for (const item of items) {
|
|
7783
7992
|
if (item.type !== "config") continue;
|
|
7784
|
-
const sourcePath =
|
|
7785
|
-
const rel =
|
|
7786
|
-
const escapes = rel === "" ||
|
|
7993
|
+
const sourcePath = path47.resolve(catalogRoot, item.path);
|
|
7994
|
+
const rel = path47.relative(catalogRoot, sourcePath);
|
|
7995
|
+
const escapes = rel === "" || path47.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
7787
7996
|
if (escapes) {
|
|
7788
7997
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
7789
7998
|
continue;
|
|
7790
7999
|
}
|
|
7791
|
-
const realSource = await
|
|
8000
|
+
const realSource = await fs34.realpath(sourcePath).catch(() => null);
|
|
7792
8001
|
if (!realRoot || !realSource) {
|
|
7793
8002
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
7794
8003
|
continue;
|
|
7795
8004
|
}
|
|
7796
|
-
const realRel =
|
|
7797
|
-
if (realRel !== "" && (
|
|
8005
|
+
const realRel = path47.relative(realRoot, realSource);
|
|
8006
|
+
if (realRel !== "" && (path47.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
7798
8007
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
7799
8008
|
continue;
|
|
7800
8009
|
}
|
|
7801
|
-
const stat = await
|
|
8010
|
+
const stat = await fs34.lstat(sourcePath).catch(() => null);
|
|
7802
8011
|
if (!stat) {
|
|
7803
8012
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
7804
8013
|
continue;
|
|
@@ -7808,14 +8017,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7808
8017
|
continue;
|
|
7809
8018
|
}
|
|
7810
8019
|
if (stat.isFile()) {
|
|
7811
|
-
const filename =
|
|
7812
|
-
const dest =
|
|
8020
|
+
const filename = path47.basename(sourcePath);
|
|
8021
|
+
const dest = path47.join(projectRoot, filename);
|
|
7813
8022
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
7814
8023
|
} else if (stat.isDirectory()) {
|
|
7815
|
-
const entries = await
|
|
8024
|
+
const entries = await fs34.readdir(sourcePath);
|
|
7816
8025
|
for (const entry of entries) {
|
|
7817
|
-
const src =
|
|
7818
|
-
const dest =
|
|
8026
|
+
const src = path47.join(sourcePath, entry);
|
|
8027
|
+
const dest = path47.join(projectRoot, entry);
|
|
7819
8028
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
7820
8029
|
}
|
|
7821
8030
|
}
|
|
@@ -7823,42 +8032,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7823
8032
|
return { scaffolded, skipped, skippedExisting };
|
|
7824
8033
|
}
|
|
7825
8034
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
7826
|
-
const destDisplay =
|
|
7827
|
-
const srcStat = await
|
|
8035
|
+
const destDisplay = path47.relative(projectRoot, dest) || path47.basename(dest);
|
|
8036
|
+
const srcStat = await fs34.lstat(src).catch(() => null);
|
|
7828
8037
|
if (!srcStat) {
|
|
7829
|
-
warn(`Skipping ${
|
|
8038
|
+
warn(`Skipping ${path47.basename(src)}: source not found`);
|
|
7830
8039
|
return "skipped-refused";
|
|
7831
8040
|
}
|
|
7832
8041
|
if (srcStat.isSymbolicLink()) {
|
|
7833
|
-
warn(`Skipping ${
|
|
8042
|
+
warn(`Skipping ${path47.basename(src)}: source is a symlink`);
|
|
7834
8043
|
return "skipped-refused";
|
|
7835
8044
|
}
|
|
7836
|
-
const exists = await
|
|
8045
|
+
const exists = await fs34.pathExists(dest);
|
|
7837
8046
|
if (exists && !opts.force) {
|
|
7838
8047
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
7839
8048
|
return "skipped-exists";
|
|
7840
8049
|
}
|
|
7841
8050
|
if (opts.dryRun) {
|
|
7842
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
8051
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path47.basename(dest)} (${itemId})`);
|
|
7843
8052
|
return "scaffolded";
|
|
7844
8053
|
}
|
|
7845
8054
|
if (exists && opts.force) {
|
|
7846
|
-
await
|
|
8055
|
+
await fs34.remove(dest);
|
|
7847
8056
|
}
|
|
7848
|
-
await
|
|
7849
|
-
await
|
|
8057
|
+
await fs34.ensureDir(path47.dirname(dest));
|
|
8058
|
+
await fs34.copy(src, dest, {
|
|
7850
8059
|
overwrite: true,
|
|
7851
8060
|
dereference: false,
|
|
7852
8061
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
7853
8062
|
filter: (s) => {
|
|
7854
8063
|
try {
|
|
7855
|
-
return !
|
|
8064
|
+
return !fs34.lstatSync(s).isSymbolicLink();
|
|
7856
8065
|
} catch {
|
|
7857
8066
|
return false;
|
|
7858
8067
|
}
|
|
7859
8068
|
}
|
|
7860
8069
|
});
|
|
7861
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
8070
|
+
log(`${exists ? "Overwrote" : "Created"} ${path47.basename(dest)} (${itemId})`);
|
|
7862
8071
|
return "scaffolded";
|
|
7863
8072
|
}
|
|
7864
8073
|
|
|
@@ -7923,8 +8132,8 @@ async function runScan(options) {
|
|
|
7923
8132
|
}
|
|
7924
8133
|
|
|
7925
8134
|
// src/commands/undo.ts
|
|
7926
|
-
import
|
|
7927
|
-
import
|
|
8135
|
+
import path48 from "node:path";
|
|
8136
|
+
import fs35 from "fs-extra";
|
|
7928
8137
|
|
|
7929
8138
|
// src/claude/managed-paths.ts
|
|
7930
8139
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md"];
|
|
@@ -7939,7 +8148,7 @@ function coreManagedAbsolutePaths(root) {
|
|
|
7939
8148
|
async function collectCoreManagedPaths(root) {
|
|
7940
8149
|
const existing = [];
|
|
7941
8150
|
for (const abs of coreManagedAbsolutePaths(root)) {
|
|
7942
|
-
if (await
|
|
8151
|
+
if (await fs35.pathExists(abs)) existing.push(abs);
|
|
7943
8152
|
}
|
|
7944
8153
|
return existing;
|
|
7945
8154
|
}
|
|
@@ -7951,18 +8160,18 @@ async function collectLockTrackedPaths(root) {
|
|
|
7951
8160
|
const relPaths = row.paths ?? [];
|
|
7952
8161
|
const existingRel = [];
|
|
7953
8162
|
for (const rel of relPaths) {
|
|
7954
|
-
if (await
|
|
8163
|
+
if (await fs35.pathExists(path48.resolve(root, rel))) existingRel.push(rel);
|
|
7955
8164
|
}
|
|
7956
8165
|
if (existingRel.length === 0) continue;
|
|
7957
8166
|
if (row.hash === void 0) {
|
|
7958
|
-
removable.push(...existingRel.map((rel) =>
|
|
8167
|
+
removable.push(...existingRel.map((rel) => path48.resolve(root, rel)));
|
|
7959
8168
|
continue;
|
|
7960
8169
|
}
|
|
7961
8170
|
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
7962
8171
|
if (currentHash === row.hash) {
|
|
7963
|
-
removable.push(...existingRel.map((rel) =>
|
|
8172
|
+
removable.push(...existingRel.map((rel) => path48.resolve(root, rel)));
|
|
7964
8173
|
} else {
|
|
7965
|
-
preserved.push(...existingRel.map((rel) =>
|
|
8174
|
+
preserved.push(...existingRel.map((rel) => path48.resolve(root, rel)));
|
|
7966
8175
|
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
7967
8176
|
}
|
|
7968
8177
|
}
|
|
@@ -7970,51 +8179,51 @@ async function collectLockTrackedPaths(root) {
|
|
|
7970
8179
|
}
|
|
7971
8180
|
async function settingsHasHausContent(root) {
|
|
7972
8181
|
const settingsPath = claudePath(root, "settings.json");
|
|
7973
|
-
if (!await
|
|
8182
|
+
if (!await fs35.pathExists(settingsPath)) return false;
|
|
7974
8183
|
const settings = await readProjectSettings(root);
|
|
7975
8184
|
return settings._haus != null;
|
|
7976
8185
|
}
|
|
7977
8186
|
async function claudeMdHasHausBlock(root) {
|
|
7978
|
-
const filePath =
|
|
7979
|
-
if (!await
|
|
7980
|
-
const text = await
|
|
8187
|
+
const filePath = path48.join(root, "CLAUDE.md");
|
|
8188
|
+
if (!await fs35.pathExists(filePath)) return false;
|
|
8189
|
+
const text = await fs35.readFile(filePath, "utf8");
|
|
7981
8190
|
return text.includes(BLOCK_BEGIN);
|
|
7982
8191
|
}
|
|
7983
8192
|
async function stripProjectSettings(root) {
|
|
7984
8193
|
const settingsPath = claudePath(root, "settings.json");
|
|
7985
|
-
if (!await
|
|
8194
|
+
if (!await fs35.pathExists(settingsPath)) return false;
|
|
7986
8195
|
let settings = await readProjectSettings(root);
|
|
7987
8196
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
7988
8197
|
const hasContent = Object.keys(settings).length > 0;
|
|
7989
8198
|
if (hasContent) {
|
|
7990
8199
|
await writeProjectSettings(root, settings);
|
|
7991
|
-
log(`Stripped haus rules from ${
|
|
8200
|
+
log(`Stripped haus rules from ${path48.relative(root, settingsPath)} (user settings preserved).`);
|
|
7992
8201
|
return true;
|
|
7993
8202
|
}
|
|
7994
|
-
await
|
|
7995
|
-
log(`Removed ${
|
|
8203
|
+
await fs35.remove(settingsPath);
|
|
8204
|
+
log(`Removed ${path48.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
7996
8205
|
return true;
|
|
7997
8206
|
}
|
|
7998
8207
|
async function stripRootClaudeMd(root) {
|
|
7999
|
-
const filePath =
|
|
8000
|
-
if (!await
|
|
8001
|
-
const prev = await
|
|
8208
|
+
const filePath = path48.join(root, "CLAUDE.md");
|
|
8209
|
+
if (!await fs35.pathExists(filePath)) return false;
|
|
8210
|
+
const prev = await fs35.readFile(filePath, "utf8");
|
|
8002
8211
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
8003
8212
|
const next = stripHausBlock(prev);
|
|
8004
8213
|
if (next.length === 0) {
|
|
8005
|
-
await
|
|
8214
|
+
await fs35.remove(filePath);
|
|
8006
8215
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
8007
8216
|
} else {
|
|
8008
|
-
await
|
|
8217
|
+
await fs35.writeFile(filePath, next, "utf8");
|
|
8009
8218
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
8010
8219
|
}
|
|
8011
8220
|
return true;
|
|
8012
8221
|
}
|
|
8013
8222
|
async function pruneDirIfEmpty(dir) {
|
|
8014
|
-
if (!await
|
|
8223
|
+
if (!await fs35.pathExists(dir)) return;
|
|
8015
8224
|
try {
|
|
8016
|
-
const entries = await
|
|
8017
|
-
if (entries.length === 0) await
|
|
8225
|
+
const entries = await fs35.readdir(dir);
|
|
8226
|
+
if (entries.length === 0) await fs35.remove(dir);
|
|
8018
8227
|
} catch {
|
|
8019
8228
|
}
|
|
8020
8229
|
}
|
|
@@ -8023,13 +8232,13 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
8023
8232
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
8024
8233
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
8025
8234
|
for (const abs of managedAbsPaths) {
|
|
8026
|
-
if (!await
|
|
8027
|
-
const rel =
|
|
8028
|
-
const backupPath =
|
|
8029
|
-
await
|
|
8030
|
-
await
|
|
8235
|
+
if (!await fs35.pathExists(abs)) continue;
|
|
8236
|
+
const rel = path48.relative(root, abs);
|
|
8237
|
+
const backupPath = path48.join(backupRoot, rel);
|
|
8238
|
+
await fs35.ensureDir(path48.dirname(backupPath));
|
|
8239
|
+
await fs35.copy(abs, backupPath);
|
|
8031
8240
|
}
|
|
8032
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
8241
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path48.relative(root, backupRoot)}.`);
|
|
8033
8242
|
}
|
|
8034
8243
|
async function runUndo(options) {
|
|
8035
8244
|
const root = options.root ?? process.cwd();
|
|
@@ -8042,7 +8251,7 @@ async function runUndo(options) {
|
|
|
8042
8251
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
8043
8252
|
return;
|
|
8044
8253
|
}
|
|
8045
|
-
const relTargets = managed.map((p) =>
|
|
8254
|
+
const relTargets = managed.map((p) => path48.relative(root, p));
|
|
8046
8255
|
const summaryParts = [...relTargets];
|
|
8047
8256
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
8048
8257
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -8064,9 +8273,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
8064
8273
|
}
|
|
8065
8274
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
8066
8275
|
for (const abs of managed) {
|
|
8067
|
-
if (!await
|
|
8068
|
-
await
|
|
8069
|
-
log(`Removed ${
|
|
8276
|
+
if (!await fs35.pathExists(abs)) continue;
|
|
8277
|
+
await fs35.remove(abs);
|
|
8278
|
+
log(`Removed ${path48.relative(root, abs)}`);
|
|
8070
8279
|
}
|
|
8071
8280
|
if (stripSettings) await stripProjectSettings(root);
|
|
8072
8281
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -8077,8 +8286,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
8077
8286
|
|
|
8078
8287
|
// src/install/uninstall.ts
|
|
8079
8288
|
import crypto3 from "node:crypto";
|
|
8080
|
-
import
|
|
8081
|
-
import
|
|
8289
|
+
import path49 from "node:path";
|
|
8290
|
+
import fs36 from "fs-extra";
|
|
8082
8291
|
async function runUninstall(options = {}) {
|
|
8083
8292
|
const { force = false } = options;
|
|
8084
8293
|
const manifest = await readManifest();
|
|
@@ -8088,7 +8297,7 @@ async function runUninstall(options = {}) {
|
|
|
8088
8297
|
return result;
|
|
8089
8298
|
}
|
|
8090
8299
|
for (const entry of manifest.files) {
|
|
8091
|
-
const exists =
|
|
8300
|
+
const exists = fs36.pathExistsSync(entry.destPath);
|
|
8092
8301
|
if (!exists) continue;
|
|
8093
8302
|
const content2 = await readText(entry.destPath);
|
|
8094
8303
|
if (content2 === void 0) continue;
|
|
@@ -8106,23 +8315,23 @@ async function runUninstall(options = {}) {
|
|
|
8106
8315
|
result.skipped.push(entry.destPath);
|
|
8107
8316
|
continue;
|
|
8108
8317
|
}
|
|
8109
|
-
await
|
|
8110
|
-
await pruneEmptyDir(
|
|
8318
|
+
await fs36.remove(entry.destPath);
|
|
8319
|
+
await pruneEmptyDir(path49.dirname(entry.destPath));
|
|
8111
8320
|
result.deleted.push(entry.destPath);
|
|
8112
8321
|
}
|
|
8113
8322
|
const settings = await readSettings();
|
|
8114
8323
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
8115
8324
|
await writeSettings(stripped);
|
|
8116
8325
|
result.hooksStripped = true;
|
|
8117
|
-
const hausDir =
|
|
8326
|
+
const hausDir = path49.join(globalClaudeDir(), "haus");
|
|
8118
8327
|
const manifestPath2 = hausManifestPath();
|
|
8119
|
-
if (
|
|
8120
|
-
await
|
|
8328
|
+
if (fs36.pathExistsSync(manifestPath2)) {
|
|
8329
|
+
await fs36.remove(manifestPath2);
|
|
8121
8330
|
}
|
|
8122
|
-
if (
|
|
8331
|
+
if (fs36.pathExistsSync(hausDir)) {
|
|
8123
8332
|
try {
|
|
8124
|
-
const remaining = await
|
|
8125
|
-
if (remaining.length === 0) await
|
|
8333
|
+
const remaining = await fs36.readdir(hausDir);
|
|
8334
|
+
if (remaining.length === 0) await fs36.remove(hausDir);
|
|
8126
8335
|
} catch {
|
|
8127
8336
|
}
|
|
8128
8337
|
}
|
|
@@ -8145,6 +8354,19 @@ function printUninstallResult(result) {
|
|
|
8145
8354
|
// src/commands/uninstall.ts
|
|
8146
8355
|
async function runUninstallCommand(options) {
|
|
8147
8356
|
try {
|
|
8357
|
+
if (!options.yes) {
|
|
8358
|
+
const manifest = await readManifest();
|
|
8359
|
+
if (manifest) {
|
|
8360
|
+
const fileCount = manifest.files.length;
|
|
8361
|
+
const ok = await confirm(
|
|
8362
|
+
fileCount > 0 ? `Remove ${fileCount} haus-managed file(s) from ~/.claude/ and strip haus hook entries from settings.json?` : "Strip haus hook entries from ~/.claude/settings.json and remove the install manifest?"
|
|
8363
|
+
);
|
|
8364
|
+
if (!ok) {
|
|
8365
|
+
log("Cancelled.");
|
|
8366
|
+
return;
|
|
8367
|
+
}
|
|
8368
|
+
}
|
|
8369
|
+
}
|
|
8148
8370
|
const result = await runUninstall({ force: options.force });
|
|
8149
8371
|
printUninstallResult(result);
|
|
8150
8372
|
log("haus uninstall complete");
|
|
@@ -8155,11 +8377,11 @@ async function runUninstallCommand(options) {
|
|
|
8155
8377
|
}
|
|
8156
8378
|
|
|
8157
8379
|
// src/commands/validate-catalog.ts
|
|
8158
|
-
import
|
|
8380
|
+
import path51 from "node:path";
|
|
8159
8381
|
|
|
8160
8382
|
// src/catalog/validate-core.ts
|
|
8161
|
-
import
|
|
8162
|
-
import
|
|
8383
|
+
import fs37 from "node:fs";
|
|
8384
|
+
import path50 from "node:path";
|
|
8163
8385
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
8164
8386
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
8165
8387
|
function auditForbiddenStacks(items) {
|
|
@@ -8301,14 +8523,14 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
8301
8523
|
}
|
|
8302
8524
|
function readForAudit(absPath, label, failures) {
|
|
8303
8525
|
try {
|
|
8304
|
-
return
|
|
8526
|
+
return fs37.readFileSync(absPath, "utf8");
|
|
8305
8527
|
} catch (err) {
|
|
8306
8528
|
failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
8307
8529
|
return null;
|
|
8308
8530
|
}
|
|
8309
8531
|
}
|
|
8310
8532
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
8311
|
-
const rel =
|
|
8533
|
+
const rel = path50.relative(manifestDir, absPath);
|
|
8312
8534
|
const failures = [];
|
|
8313
8535
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
8314
8536
|
if (text === null) return failures;
|
|
@@ -8330,47 +8552,47 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8330
8552
|
const failures = [];
|
|
8331
8553
|
for (const item of items) {
|
|
8332
8554
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
8333
|
-
const absPath =
|
|
8555
|
+
const absPath = path50.join(manifestDir, item.path);
|
|
8334
8556
|
if (item.type === "skill") {
|
|
8335
|
-
const skillMd =
|
|
8336
|
-
if (!
|
|
8337
|
-
failures.push(`${item.id}: missing ${
|
|
8557
|
+
const skillMd = path50.join(absPath, "SKILL.md");
|
|
8558
|
+
if (!fs37.existsSync(skillMd)) {
|
|
8559
|
+
failures.push(`${item.id}: missing ${path50.relative(manifestDir, skillMd)}`);
|
|
8338
8560
|
continue;
|
|
8339
8561
|
}
|
|
8340
8562
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
8341
8563
|
if (text === null) continue;
|
|
8342
8564
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
8343
8565
|
failures.push(
|
|
8344
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
8566
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path50.relative(manifestDir, skillMd)}`)
|
|
8345
8567
|
);
|
|
8346
8568
|
} else if (item.type === "agent") {
|
|
8347
|
-
if (!
|
|
8569
|
+
if (!fs37.existsSync(absPath)) {
|
|
8348
8570
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
8349
8571
|
continue;
|
|
8350
8572
|
}
|
|
8351
|
-
const rel =
|
|
8573
|
+
const rel = path50.relative(manifestDir, absPath);
|
|
8352
8574
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8353
8575
|
if (text === null) continue;
|
|
8354
8576
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
8355
8577
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
8356
8578
|
} else if (item.type === "template") {
|
|
8357
|
-
if (!
|
|
8579
|
+
if (!fs37.existsSync(absPath)) {
|
|
8358
8580
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
8359
8581
|
continue;
|
|
8360
8582
|
}
|
|
8361
8583
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
8362
8584
|
} else if (item.type === "command") {
|
|
8363
|
-
if (!
|
|
8585
|
+
if (!fs37.existsSync(absPath)) {
|
|
8364
8586
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
8365
8587
|
continue;
|
|
8366
8588
|
}
|
|
8367
|
-
const rel =
|
|
8589
|
+
const rel = path50.relative(manifestDir, absPath);
|
|
8368
8590
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8369
8591
|
if (text === null) continue;
|
|
8370
8592
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
8371
8593
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
8372
8594
|
} else if (item.type === "config") {
|
|
8373
|
-
if (!
|
|
8595
|
+
if (!fs37.existsSync(absPath)) {
|
|
8374
8596
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
8375
8597
|
}
|
|
8376
8598
|
}
|
|
@@ -8387,7 +8609,7 @@ var CONTENT_ROOTS = ["skills", "agents", "templates", "commands", "configs"];
|
|
|
8387
8609
|
function walkAllFiles(dir, fn, failures) {
|
|
8388
8610
|
let entries;
|
|
8389
8611
|
try {
|
|
8390
|
-
entries =
|
|
8612
|
+
entries = fs37.readdirSync(dir, { withFileTypes: true });
|
|
8391
8613
|
} catch (err) {
|
|
8392
8614
|
failures.push(
|
|
8393
8615
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -8396,7 +8618,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
8396
8618
|
}
|
|
8397
8619
|
for (const entry of entries) {
|
|
8398
8620
|
if (entry.name === ".DS_Store") continue;
|
|
8399
|
-
const full =
|
|
8621
|
+
const full = path50.join(dir, entry.name);
|
|
8400
8622
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
8401
8623
|
else fn(full);
|
|
8402
8624
|
}
|
|
@@ -8414,12 +8636,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
8414
8636
|
const failures = [];
|
|
8415
8637
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
8416
8638
|
for (const dir of CONTENT_ROOTS) {
|
|
8417
|
-
const abs =
|
|
8418
|
-
if (!
|
|
8639
|
+
const abs = path50.join(manifestDir, dir);
|
|
8640
|
+
if (!fs37.existsSync(abs)) continue;
|
|
8419
8641
|
walkAllFiles(
|
|
8420
8642
|
abs,
|
|
8421
8643
|
(file) => {
|
|
8422
|
-
const relPath =
|
|
8644
|
+
const relPath = path50.relative(manifestDir, file).replace(/\\/g, "/");
|
|
8423
8645
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
8424
8646
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
8425
8647
|
}
|
|
@@ -8432,7 +8654,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
8432
8654
|
function walkMd(dir, fn, failures) {
|
|
8433
8655
|
let entries;
|
|
8434
8656
|
try {
|
|
8435
|
-
entries =
|
|
8657
|
+
entries = fs37.readdirSync(dir, { withFileTypes: true });
|
|
8436
8658
|
} catch (err) {
|
|
8437
8659
|
failures.push(
|
|
8438
8660
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -8440,7 +8662,7 @@ function walkMd(dir, fn, failures) {
|
|
|
8440
8662
|
return;
|
|
8441
8663
|
}
|
|
8442
8664
|
for (const entry of entries) {
|
|
8443
|
-
const full =
|
|
8665
|
+
const full = path50.join(dir, entry.name);
|
|
8444
8666
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
8445
8667
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
8446
8668
|
}
|
|
@@ -8449,13 +8671,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
8449
8671
|
const failures = [];
|
|
8450
8672
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
8451
8673
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
8452
|
-
const abs =
|
|
8453
|
-
if (!
|
|
8674
|
+
const abs = path50.join(manifestDir, dir);
|
|
8675
|
+
if (!fs37.existsSync(abs)) continue;
|
|
8454
8676
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
8455
8677
|
walkMd(
|
|
8456
8678
|
abs,
|
|
8457
8679
|
(file) => {
|
|
8458
|
-
const rel =
|
|
8680
|
+
const rel = path50.relative(manifestDir, file);
|
|
8459
8681
|
const text = readForAudit(file, rel, failures);
|
|
8460
8682
|
if (text === null) return;
|
|
8461
8683
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -8500,8 +8722,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8500
8722
|
process.exitCode = 1;
|
|
8501
8723
|
return;
|
|
8502
8724
|
}
|
|
8503
|
-
const abs =
|
|
8504
|
-
const manifestDir =
|
|
8725
|
+
const abs = path51.resolve(process.cwd(), manifestPath2);
|
|
8726
|
+
const manifestDir = path51.dirname(abs);
|
|
8505
8727
|
const data = await readJson(abs);
|
|
8506
8728
|
if (!data?.items) {
|
|
8507
8729
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -8518,8 +8740,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8518
8740
|
}
|
|
8519
8741
|
|
|
8520
8742
|
// src/workspace/worktree/add.ts
|
|
8521
|
-
import
|
|
8522
|
-
import
|
|
8743
|
+
import path58 from "node:path";
|
|
8744
|
+
import fs41 from "fs-extra";
|
|
8523
8745
|
|
|
8524
8746
|
// src/workspace/worktree/git-worktree.ts
|
|
8525
8747
|
init_exec();
|
|
@@ -8638,13 +8860,13 @@ async function hasUnpushedWork(cwd) {
|
|
|
8638
8860
|
}
|
|
8639
8861
|
|
|
8640
8862
|
// src/workspace/worktree/hydrate.ts
|
|
8641
|
-
import
|
|
8642
|
-
import
|
|
8863
|
+
import path54 from "node:path";
|
|
8864
|
+
import fs39 from "fs-extra";
|
|
8643
8865
|
|
|
8644
8866
|
// src/workspace/worktree/cow-copy.ts
|
|
8645
8867
|
init_exec();
|
|
8646
|
-
import
|
|
8647
|
-
import
|
|
8868
|
+
import path52 from "node:path";
|
|
8869
|
+
import fs38 from "fs-extra";
|
|
8648
8870
|
async function getMacFilesystemType(dir) {
|
|
8649
8871
|
const df = await runCommand("df", ["-P", dir]);
|
|
8650
8872
|
if (df.exitCode !== 0) return void 0;
|
|
@@ -8675,14 +8897,14 @@ async function detectCowStrategy(dir) {
|
|
|
8675
8897
|
return "unknown-platform";
|
|
8676
8898
|
}
|
|
8677
8899
|
async function cowCopyDir(src, dest) {
|
|
8678
|
-
const srcExists = await
|
|
8900
|
+
const srcExists = await fs38.pathExists(src);
|
|
8679
8901
|
if (!srcExists)
|
|
8680
8902
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
8681
|
-
const strategy = await detectCowStrategy(
|
|
8903
|
+
const strategy = await detectCowStrategy(path52.dirname(dest));
|
|
8682
8904
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
8683
8905
|
return { strategy, attempted: false, ok: false };
|
|
8684
8906
|
}
|
|
8685
|
-
await
|
|
8907
|
+
await fs38.ensureDir(path52.dirname(dest));
|
|
8686
8908
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
8687
8909
|
const result = await runCommand("cp", args);
|
|
8688
8910
|
return {
|
|
@@ -8696,8 +8918,8 @@ async function cowCopyDir(src, dest) {
|
|
|
8696
8918
|
// src/workspace/worktree/install.ts
|
|
8697
8919
|
init_exec();
|
|
8698
8920
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
8699
|
-
import
|
|
8700
|
-
import
|
|
8921
|
+
import path53 from "node:path";
|
|
8922
|
+
import fg6 from "fast-glob";
|
|
8701
8923
|
function detectInstallPlan(signals) {
|
|
8702
8924
|
if (signals.hasYarnLock) {
|
|
8703
8925
|
return signals.hasPackageManagerField ? { manager: "yarn", command: "corepack", args: ["yarn", "install"] } : { manager: "yarn", command: "yarn", args: ["install"] };
|
|
@@ -8717,11 +8939,11 @@ function detectInstallPlan(signals) {
|
|
|
8717
8939
|
return { manager: "none" };
|
|
8718
8940
|
}
|
|
8719
8941
|
async function computeLockfileSignals(dir) {
|
|
8720
|
-
const hasYarnLock = existsSync3(
|
|
8721
|
-
const hasPnpmLock = existsSync3(
|
|
8722
|
-
const hasPackageLock = existsSync3(
|
|
8723
|
-
const hasComposerLock = existsSync3(
|
|
8724
|
-
const dotnetFiles = await
|
|
8942
|
+
const hasYarnLock = existsSync3(path53.join(dir, "yarn.lock"));
|
|
8943
|
+
const hasPnpmLock = existsSync3(path53.join(dir, "pnpm-lock.yaml"));
|
|
8944
|
+
const hasPackageLock = existsSync3(path53.join(dir, "package-lock.json"));
|
|
8945
|
+
const hasComposerLock = existsSync3(path53.join(dir, "composer.lock"));
|
|
8946
|
+
const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
|
|
8725
8947
|
cwd: dir,
|
|
8726
8948
|
onlyFiles: true,
|
|
8727
8949
|
suppressErrors: true,
|
|
@@ -8734,7 +8956,7 @@ async function computeLockfileSignals(dir) {
|
|
|
8734
8956
|
});
|
|
8735
8957
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
8736
8958
|
let hasPackageManagerField = false;
|
|
8737
|
-
const pkgPath =
|
|
8959
|
+
const pkgPath = path53.join(dir, "package.json");
|
|
8738
8960
|
if (existsSync3(pkgPath)) {
|
|
8739
8961
|
try {
|
|
8740
8962
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -8775,13 +8997,13 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
8775
8997
|
const start = Date.now();
|
|
8776
8998
|
const targets = [];
|
|
8777
8999
|
for (const target of HYDRATION_TARGETS) {
|
|
8778
|
-
const src =
|
|
8779
|
-
const dest =
|
|
8780
|
-
if (!await
|
|
8781
|
-
const destExists = await
|
|
9000
|
+
const src = path54.join(mainMemberPath, target);
|
|
9001
|
+
const dest = path54.join(worktreeMemberPath, target);
|
|
9002
|
+
if (!await fs39.pathExists(src)) continue;
|
|
9003
|
+
const destExists = await fs39.pathExists(dest);
|
|
8782
9004
|
if (!opts.force && destExists) continue;
|
|
8783
9005
|
if (opts.force && destExists) {
|
|
8784
|
-
await
|
|
9006
|
+
await fs39.remove(dest);
|
|
8785
9007
|
}
|
|
8786
9008
|
const copy = await cowCopyDir(src, dest);
|
|
8787
9009
|
targets.push({ target, copy });
|
|
@@ -8794,11 +9016,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
8794
9016
|
|
|
8795
9017
|
// src/workspace/worktree/local-files.ts
|
|
8796
9018
|
init_exec();
|
|
8797
|
-
import
|
|
8798
|
-
import
|
|
9019
|
+
import path55 from "node:path";
|
|
9020
|
+
import fs40 from "fs-extra";
|
|
8799
9021
|
var MACHINE_LOCAL_FILES = [
|
|
8800
9022
|
"docker-compose.local.yml",
|
|
8801
|
-
|
|
9023
|
+
path55.join(".claude", "settings.local.json")
|
|
8802
9024
|
];
|
|
8803
9025
|
async function isTracked(repoRoot, relFile) {
|
|
8804
9026
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -8808,12 +9030,12 @@ async function isTracked(repoRoot, relFile) {
|
|
|
8808
9030
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
8809
9031
|
const copied = [];
|
|
8810
9032
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
8811
|
-
const src =
|
|
8812
|
-
if (!await
|
|
9033
|
+
const src = path55.join(mainMemberPath, rel);
|
|
9034
|
+
if (!await fs40.pathExists(src)) continue;
|
|
8813
9035
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
8814
|
-
const dest =
|
|
8815
|
-
await
|
|
8816
|
-
await
|
|
9036
|
+
const dest = path55.join(worktreeMemberPath, rel);
|
|
9037
|
+
await fs40.ensureDir(path55.dirname(dest));
|
|
9038
|
+
await fs40.copy(src, dest, { overwrite: true });
|
|
8817
9039
|
copied.push(rel);
|
|
8818
9040
|
}
|
|
8819
9041
|
return copied;
|
|
@@ -8821,15 +9043,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
8821
9043
|
|
|
8822
9044
|
// src/workspace/worktree/root.ts
|
|
8823
9045
|
import { existsSync as existsSync4 } from "node:fs";
|
|
8824
|
-
import
|
|
9046
|
+
import path56 from "node:path";
|
|
8825
9047
|
async function resolveWorkspaceForWorktree(start) {
|
|
8826
9048
|
const rootInfo = await resolveRoots(start);
|
|
8827
9049
|
if (!rootInfo.isGitRepo) {
|
|
8828
9050
|
return { ok: false, reason: "Not inside a git repository." };
|
|
8829
9051
|
}
|
|
8830
9052
|
const workspaceRoot = rootInfo.mainRoot;
|
|
8831
|
-
const hasYaml = existsSync4(
|
|
8832
|
-
const hasManifest = existsSync4(
|
|
9053
|
+
const hasYaml = existsSync4(path56.join(workspaceRoot, WORKSPACE_FILE));
|
|
9054
|
+
const hasManifest = existsSync4(path56.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
8833
9055
|
if (!hasYaml && !hasManifest) {
|
|
8834
9056
|
return {
|
|
8835
9057
|
ok: false,
|
|
@@ -8854,19 +9076,31 @@ function selectMembers(members, only) {
|
|
|
8854
9076
|
}
|
|
8855
9077
|
|
|
8856
9078
|
// src/workspace/worktree/state.ts
|
|
8857
|
-
import
|
|
9079
|
+
import path57 from "node:path";
|
|
8858
9080
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
8859
9081
|
function worktreesDir(mainRoot) {
|
|
8860
|
-
return
|
|
9082
|
+
return path57.join(mainRoot, ".claude", "worktrees");
|
|
9083
|
+
}
|
|
9084
|
+
function isValidWorktreeSlug(slug) {
|
|
9085
|
+
if (slug === "" || slug === "." || slug === "..") return false;
|
|
9086
|
+
if (slug.includes("/") || slug.includes("\\")) return false;
|
|
9087
|
+
return /^[A-Za-z0-9._-]+$/.test(slug);
|
|
8861
9088
|
}
|
|
8862
9089
|
function worktreePath(mainRoot, slug) {
|
|
8863
|
-
|
|
9090
|
+
if (!isValidWorktreeSlug(slug)) {
|
|
9091
|
+
throw new Error(
|
|
9092
|
+
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
9093
|
+
);
|
|
9094
|
+
}
|
|
9095
|
+
return path57.join(worktreesDir(mainRoot), slug);
|
|
8864
9096
|
}
|
|
8865
9097
|
async function readWorktreeState(worktreeRoot) {
|
|
8866
|
-
|
|
9098
|
+
const state = await readJson(path57.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
9099
|
+
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
9100
|
+
return state;
|
|
8867
9101
|
}
|
|
8868
9102
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
8869
|
-
await writeJson(
|
|
9103
|
+
await writeJson(path57.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
8870
9104
|
}
|
|
8871
9105
|
|
|
8872
9106
|
// src/workspace/worktree/add.ts
|
|
@@ -8881,7 +9115,7 @@ async function runAdd(opts) {
|
|
|
8881
9115
|
}
|
|
8882
9116
|
const branch = opts.branch ?? opts.slug;
|
|
8883
9117
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
8884
|
-
if (await
|
|
9118
|
+
if (await fs41.pathExists(wsWorktreePath)) {
|
|
8885
9119
|
return {
|
|
8886
9120
|
ok: false,
|
|
8887
9121
|
error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate --materialize\` from inside it to check out any missing members (e.g. a worktree created by plain \`git worktree add\`), or \`remove\` first to start over.`
|
|
@@ -8900,7 +9134,7 @@ async function runAdd(opts) {
|
|
|
8900
9134
|
const memberResults = [];
|
|
8901
9135
|
for (const member of selected) {
|
|
8902
9136
|
const start = Date.now();
|
|
8903
|
-
const memberWorktreePath =
|
|
9137
|
+
const memberWorktreePath = path58.join(wsWorktreePath, member.folder);
|
|
8904
9138
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
8905
9139
|
preferDefaultBranchFrom: true
|
|
8906
9140
|
});
|
|
@@ -8930,7 +9164,7 @@ async function runAdd(opts) {
|
|
|
8930
9164
|
await mapWithConcurrency(okResults, async (result) => {
|
|
8931
9165
|
const member = selected.find((m) => m.id === result.member);
|
|
8932
9166
|
if (!member) return;
|
|
8933
|
-
const memberWorktreePath =
|
|
9167
|
+
const memberWorktreePath = path58.join(wsWorktreePath, member.folder);
|
|
8934
9168
|
const hydrationStart = Date.now();
|
|
8935
9169
|
if (opts.hydrate !== false) {
|
|
8936
9170
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -8997,10 +9231,10 @@ async function runDryRun(args) {
|
|
|
8997
9231
|
|
|
8998
9232
|
// src/workspace/worktree/doctor.ts
|
|
8999
9233
|
import { existsSync as existsSync5 } from "node:fs";
|
|
9000
|
-
import
|
|
9234
|
+
import path59 from "node:path";
|
|
9001
9235
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
9002
9236
|
function looksHydrated(memberPath) {
|
|
9003
|
-
return HYDRATION_MARKERS.some((marker) => existsSync5(
|
|
9237
|
+
return HYDRATION_MARKERS.some((marker) => existsSync5(path59.join(memberPath, marker)));
|
|
9004
9238
|
}
|
|
9005
9239
|
async function runDoctor2() {
|
|
9006
9240
|
const rootInfo = await resolveRoots();
|
|
@@ -9014,7 +9248,7 @@ async function runDoctor2() {
|
|
|
9014
9248
|
};
|
|
9015
9249
|
}
|
|
9016
9250
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9017
|
-
const isWorkspace = existsSync5(
|
|
9251
|
+
const isWorkspace = existsSync5(path59.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path59.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
9018
9252
|
if (!isWorkspace) {
|
|
9019
9253
|
return {
|
|
9020
9254
|
isWorkspace: false,
|
|
@@ -9027,7 +9261,7 @@ async function runDoctor2() {
|
|
|
9027
9261
|
};
|
|
9028
9262
|
}
|
|
9029
9263
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
9030
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
9264
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path59.sep}`);
|
|
9031
9265
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
9032
9266
|
const members = await readMembers(rootInfo);
|
|
9033
9267
|
const problems = [];
|
|
@@ -9035,10 +9269,10 @@ async function runDoctor2() {
|
|
|
9035
9269
|
let slug;
|
|
9036
9270
|
if (inWorkspaceWorktree) {
|
|
9037
9271
|
const wtPath = rootInfo.repoRoot;
|
|
9038
|
-
slug = rootInfo.worktreeName ??
|
|
9272
|
+
slug = rootInfo.worktreeName ?? path59.basename(wtPath);
|
|
9039
9273
|
const state = await readWorktreeState(wtPath);
|
|
9040
9274
|
for (const member of members) {
|
|
9041
|
-
const memberPath =
|
|
9275
|
+
const memberPath = path59.join(wtPath, member.folder);
|
|
9042
9276
|
const materialized = existsSync5(memberPath);
|
|
9043
9277
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
9044
9278
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -9069,16 +9303,16 @@ async function runDoctor2() {
|
|
|
9069
9303
|
for (const member of members) {
|
|
9070
9304
|
const entries = await listWorktrees(member.absPath);
|
|
9071
9305
|
for (const entry of entries) {
|
|
9072
|
-
const entryPath =
|
|
9073
|
-
const marker = `${
|
|
9306
|
+
const entryPath = path59.normalize(entry.path);
|
|
9307
|
+
const marker = `${path59.sep}.claude${path59.sep}worktrees${path59.sep}`;
|
|
9074
9308
|
const idx = entryPath.indexOf(marker);
|
|
9075
9309
|
if (idx === -1) continue;
|
|
9076
9310
|
const rest = entryPath.slice(idx + marker.length);
|
|
9077
|
-
const slugName = rest.split(
|
|
9311
|
+
const slugName = rest.split(path59.sep)[0];
|
|
9078
9312
|
if (!slugName) continue;
|
|
9079
|
-
if (!existsSync5(
|
|
9313
|
+
if (!existsSync5(path59.join(wsWorktreesDir, slugName))) {
|
|
9080
9314
|
orphans.push(
|
|
9081
|
-
`${member.id}: worktree registered at ${entry.path} but ${
|
|
9315
|
+
`${member.id}: worktree registered at ${entry.path} but ${path59.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
|
|
9082
9316
|
);
|
|
9083
9317
|
}
|
|
9084
9318
|
}
|
|
@@ -9089,7 +9323,7 @@ async function runDoctor2() {
|
|
|
9089
9323
|
|
|
9090
9324
|
// src/workspace/worktree/hydrate-command.ts
|
|
9091
9325
|
import { existsSync as existsSync6 } from "node:fs";
|
|
9092
|
-
import
|
|
9326
|
+
import path60 from "node:path";
|
|
9093
9327
|
async function runHydrateWorktree(opts) {
|
|
9094
9328
|
const rootInfo = await resolveRoots();
|
|
9095
9329
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -9100,7 +9334,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9100
9334
|
}
|
|
9101
9335
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9102
9336
|
const wtPath = rootInfo.repoRoot;
|
|
9103
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
9337
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path60.sep}`)) {
|
|
9104
9338
|
return {
|
|
9105
9339
|
ok: false,
|
|
9106
9340
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -9112,13 +9346,13 @@ async function runHydrateWorktree(opts) {
|
|
|
9112
9346
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
9113
9347
|
}
|
|
9114
9348
|
const state = await readWorktreeState(wtPath);
|
|
9115
|
-
const slug = state?.slug ??
|
|
9349
|
+
const slug = state?.slug ?? path60.basename(wtPath);
|
|
9116
9350
|
const detectedBranch = await currentBranch(wtPath);
|
|
9117
9351
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
9118
9352
|
if (opts.dryRun) {
|
|
9119
9353
|
const results2 = await Promise.all(
|
|
9120
9354
|
selected.map(async (member) => {
|
|
9121
|
-
const memberPath =
|
|
9355
|
+
const memberPath = path60.join(wtPath, member.folder);
|
|
9122
9356
|
if (existsSync6(memberPath)) {
|
|
9123
9357
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
9124
9358
|
}
|
|
@@ -9139,7 +9373,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9139
9373
|
const results = await mapWithConcurrency(
|
|
9140
9374
|
selected,
|
|
9141
9375
|
async (member) => {
|
|
9142
|
-
const memberPath =
|
|
9376
|
+
const memberPath = path60.join(wtPath, member.folder);
|
|
9143
9377
|
let branchAction;
|
|
9144
9378
|
if (!existsSync6(memberPath)) {
|
|
9145
9379
|
if (!opts.materialize) {
|
|
@@ -9202,26 +9436,26 @@ async function runHydrateWorktree(opts) {
|
|
|
9202
9436
|
}
|
|
9203
9437
|
|
|
9204
9438
|
// src/workspace/worktree/list.ts
|
|
9205
|
-
import
|
|
9206
|
-
import
|
|
9439
|
+
import path61 from "node:path";
|
|
9440
|
+
import fs42 from "fs-extra";
|
|
9207
9441
|
async function runList2() {
|
|
9208
9442
|
const resolved = await resolveWorkspaceForWorktree();
|
|
9209
9443
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
9210
9444
|
const { rootInfo, workspaceRoot } = resolved;
|
|
9211
9445
|
const dir = worktreesDir(workspaceRoot);
|
|
9212
|
-
if (!await
|
|
9213
|
-
const entries = await
|
|
9446
|
+
if (!await fs42.pathExists(dir)) return { ok: true, workspaceRoot, worktrees: [] };
|
|
9447
|
+
const entries = await fs42.readdir(dir, { withFileTypes: true });
|
|
9214
9448
|
const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9215
9449
|
const configuredMembers = await readMembers(rootInfo);
|
|
9216
9450
|
const worktrees = [];
|
|
9217
9451
|
for (const slug of slugs) {
|
|
9218
|
-
const wtPath =
|
|
9452
|
+
const wtPath = path61.join(dir, slug);
|
|
9219
9453
|
const state = await readWorktreeState(wtPath);
|
|
9220
9454
|
const members = [];
|
|
9221
9455
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
9222
9456
|
for (const m of source) {
|
|
9223
|
-
const memberPath =
|
|
9224
|
-
const materialized = await
|
|
9457
|
+
const memberPath = path61.join(wtPath, m.folder);
|
|
9458
|
+
const materialized = await fs42.pathExists(memberPath);
|
|
9225
9459
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
9226
9460
|
members.push({
|
|
9227
9461
|
id: m.id,
|
|
@@ -9242,14 +9476,14 @@ async function runList2() {
|
|
|
9242
9476
|
}
|
|
9243
9477
|
|
|
9244
9478
|
// src/workspace/worktree/remove.ts
|
|
9245
|
-
import
|
|
9246
|
-
import
|
|
9479
|
+
import path62 from "node:path";
|
|
9480
|
+
import fs43 from "fs-extra";
|
|
9247
9481
|
async function runRemove(opts) {
|
|
9248
9482
|
const resolved = await resolveWorkspaceForWorktree();
|
|
9249
9483
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
9250
9484
|
const { rootInfo, workspaceRoot } = resolved;
|
|
9251
9485
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
9252
|
-
if (!await
|
|
9486
|
+
if (!await fs43.pathExists(wsWorktreePath)) {
|
|
9253
9487
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
9254
9488
|
}
|
|
9255
9489
|
const state = await readWorktreeState(wsWorktreePath);
|
|
@@ -9269,12 +9503,12 @@ async function runRemove(opts) {
|
|
|
9269
9503
|
targets.push({
|
|
9270
9504
|
repo: configMember?.id ?? stateMember.id,
|
|
9271
9505
|
ownerRepo,
|
|
9272
|
-
path:
|
|
9506
|
+
path: path62.join(wsWorktreePath, stateMember.folder)
|
|
9273
9507
|
});
|
|
9274
9508
|
}
|
|
9275
9509
|
const existingTargets = [];
|
|
9276
9510
|
for (const target of targets) {
|
|
9277
|
-
if (await
|
|
9511
|
+
if (await fs43.pathExists(target.path)) existingTargets.push(target);
|
|
9278
9512
|
}
|
|
9279
9513
|
if (!opts.force) {
|
|
9280
9514
|
const blockers = [];
|
|
@@ -9319,14 +9553,19 @@ async function runRemove(opts) {
|
|
|
9319
9553
|
await pruneWorktrees(target.ownerRepo);
|
|
9320
9554
|
}
|
|
9321
9555
|
const wsTarget = existingTargets.find((t) => t.repo === "(workspace)");
|
|
9556
|
+
let wsRemoveOk = false;
|
|
9322
9557
|
if (wsTarget) {
|
|
9323
9558
|
const wsResult = await removeWorktree(workspaceRoot, wsWorktreePath);
|
|
9324
|
-
if (wsResult.ok)
|
|
9325
|
-
|
|
9559
|
+
if (wsResult.ok) {
|
|
9560
|
+
removed.push("(workspace)");
|
|
9561
|
+
wsRemoveOk = true;
|
|
9562
|
+
} else {
|
|
9563
|
+
failed.push({ repo: "(workspace)", error: wsResult.stderr ?? "unknown error" });
|
|
9564
|
+
}
|
|
9326
9565
|
await pruneWorktrees(workspaceRoot);
|
|
9327
9566
|
}
|
|
9328
|
-
if (await
|
|
9329
|
-
await
|
|
9567
|
+
if (wsRemoveOk && await fs43.pathExists(wsWorktreePath)) {
|
|
9568
|
+
await fs43.remove(wsWorktreePath);
|
|
9330
9569
|
}
|
|
9331
9570
|
return { ok: failed.length === 0, dryRun: false, slug: opts.slug, removed, failed };
|
|
9332
9571
|
}
|
|
@@ -9541,7 +9780,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
9541
9780
|
|
|
9542
9781
|
// src/commands/workspace.ts
|
|
9543
9782
|
import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
|
|
9544
|
-
import
|
|
9783
|
+
import path71 from "node:path";
|
|
9545
9784
|
|
|
9546
9785
|
// src/commands/workspace/aggregate.ts
|
|
9547
9786
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -9596,18 +9835,18 @@ ${summaries.map(
|
|
|
9596
9835
|
|
|
9597
9836
|
// src/commands/workspace/doctor.ts
|
|
9598
9837
|
import { existsSync as existsSync7 } from "node:fs";
|
|
9599
|
-
import
|
|
9838
|
+
import path64 from "node:path";
|
|
9600
9839
|
|
|
9601
9840
|
// src/commands/workspace/manifest.ts
|
|
9602
9841
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
9603
|
-
import
|
|
9842
|
+
import path63 from "node:path";
|
|
9604
9843
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
9605
9844
|
function manifestPath(workspaceRoot) {
|
|
9606
9845
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
9607
9846
|
}
|
|
9608
9847
|
function hausVersion() {
|
|
9609
9848
|
try {
|
|
9610
|
-
const pkg = JSON.parse(readFileSync4(
|
|
9849
|
+
const pkg = JSON.parse(readFileSync4(path63.join(packageRoot(), "package.json"), "utf8"));
|
|
9611
9850
|
return pkg.version ?? "0.0.0";
|
|
9612
9851
|
} catch {
|
|
9613
9852
|
return "0.0.0";
|
|
@@ -9688,10 +9927,18 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
9688
9927
|
});
|
|
9689
9928
|
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
9690
9929
|
}
|
|
9930
|
+
if (!Array.isArray(manifest.repos)) {
|
|
9931
|
+
flag({
|
|
9932
|
+
repo: "(workspace)",
|
|
9933
|
+
kind: "no-manifest",
|
|
9934
|
+
detail: 'workspace.manifest.json\'s "repos" field is not an array \u2014 run `haus workspace setup --write` to regenerate it.'
|
|
9935
|
+
});
|
|
9936
|
+
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
9937
|
+
}
|
|
9691
9938
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
9692
9939
|
const catalogRefByRepo = [];
|
|
9693
9940
|
for (const repo of config.repos) {
|
|
9694
|
-
const repoRoot =
|
|
9941
|
+
const repoRoot = path64.resolve(workspaceRoot, repo.path);
|
|
9695
9942
|
const entry = manifestByName.get(repo.name);
|
|
9696
9943
|
if (!entry) {
|
|
9697
9944
|
flag({
|
|
@@ -9764,7 +10011,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
9764
10011
|
});
|
|
9765
10012
|
continue;
|
|
9766
10013
|
}
|
|
9767
|
-
if (!existsSync7(
|
|
10014
|
+
if (!existsSync7(path64.join(workspaceRoot, entry.path))) {
|
|
9768
10015
|
flag({
|
|
9769
10016
|
repo: entry.repo,
|
|
9770
10017
|
kind: "missing-linked-context-copy",
|
|
@@ -9807,16 +10054,16 @@ function emit(args) {
|
|
|
9807
10054
|
}
|
|
9808
10055
|
|
|
9809
10056
|
// src/workspace/link-context/link.ts
|
|
10057
|
+
import path67 from "node:path";
|
|
10058
|
+
import fs46 from "fs-extra";
|
|
10059
|
+
|
|
10060
|
+
// src/workspace/link-context/plan.ts
|
|
9810
10061
|
import path66 from "node:path";
|
|
9811
10062
|
import fs45 from "fs-extra";
|
|
9812
10063
|
|
|
9813
|
-
// src/workspace/link-context/
|
|
10064
|
+
// src/workspace/link-context/scan-source.ts
|
|
9814
10065
|
import path65 from "node:path";
|
|
9815
10066
|
import fs44 from "fs-extra";
|
|
9816
|
-
|
|
9817
|
-
// src/workspace/link-context/scan-source.ts
|
|
9818
|
-
import path64 from "node:path";
|
|
9819
|
-
import fs43 from "fs-extra";
|
|
9820
10067
|
var ASSET_SUBDIR = {
|
|
9821
10068
|
skill: "skills",
|
|
9822
10069
|
agent: "agents",
|
|
@@ -9824,36 +10071,36 @@ var ASSET_SUBDIR = {
|
|
|
9824
10071
|
};
|
|
9825
10072
|
async function hasLinkableContext(member) {
|
|
9826
10073
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
9827
|
-
if (await
|
|
10074
|
+
if (await fs44.pathExists(path65.join(member.absPath, ".claude", subdir))) return true;
|
|
9828
10075
|
}
|
|
9829
10076
|
return false;
|
|
9830
10077
|
}
|
|
9831
10078
|
async function collectSkills(member) {
|
|
9832
|
-
const dir =
|
|
9833
|
-
if (!await
|
|
9834
|
-
const names = await
|
|
10079
|
+
const dir = path65.join(member.absPath, ".claude", "skills");
|
|
10080
|
+
if (!await fs44.pathExists(dir)) return [];
|
|
10081
|
+
const names = await fs44.readdir(dir);
|
|
9835
10082
|
const out = [];
|
|
9836
10083
|
for (const name of [...names].sort()) {
|
|
9837
|
-
const abs =
|
|
9838
|
-
const stat = await
|
|
10084
|
+
const abs = path65.join(dir, name);
|
|
10085
|
+
const stat = await fs44.lstat(abs);
|
|
9839
10086
|
if (!stat.isDirectory()) continue;
|
|
9840
|
-
if (!await
|
|
9841
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
10087
|
+
if (!await fs44.pathExists(path65.join(abs, "SKILL.md"))) continue;
|
|
10088
|
+
out.push({ type: "skill", name, sourceRelPath: path65.posix.join(".claude", "skills", name) });
|
|
9842
10089
|
}
|
|
9843
10090
|
return out;
|
|
9844
10091
|
}
|
|
9845
10092
|
async function collectFlatMarkdown(member, type) {
|
|
9846
10093
|
const subdir = ASSET_SUBDIR[type];
|
|
9847
|
-
const dir =
|
|
9848
|
-
if (!await
|
|
9849
|
-
const names = await
|
|
10094
|
+
const dir = path65.join(member.absPath, ".claude", subdir);
|
|
10095
|
+
if (!await fs44.pathExists(dir)) return [];
|
|
10096
|
+
const names = await fs44.readdir(dir);
|
|
9850
10097
|
const out = [];
|
|
9851
10098
|
for (const fileName of [...names].sort()) {
|
|
9852
10099
|
if (!fileName.endsWith(".md")) continue;
|
|
9853
|
-
const abs =
|
|
9854
|
-
if (!(await
|
|
10100
|
+
const abs = path65.join(dir, fileName);
|
|
10101
|
+
if (!(await fs44.lstat(abs)).isFile()) continue;
|
|
9855
10102
|
const name = fileName.slice(0, -".md".length);
|
|
9856
|
-
out.push({ type, name, sourceRelPath:
|
|
10103
|
+
out.push({ type, name, sourceRelPath: path65.posix.join(".claude", subdir, fileName) });
|
|
9857
10104
|
}
|
|
9858
10105
|
return out;
|
|
9859
10106
|
}
|
|
@@ -9868,9 +10115,9 @@ async function collectSourceAssets(member) {
|
|
|
9868
10115
|
|
|
9869
10116
|
// src/workspace/link-context/plan.ts
|
|
9870
10117
|
function repoFolderPrefix(member) {
|
|
9871
|
-
const base =
|
|
10118
|
+
const base = path66.posix.basename(member.folder);
|
|
9872
10119
|
if (base && base !== "." && base !== "/") return base;
|
|
9873
|
-
const fromAbs =
|
|
10120
|
+
const fromAbs = path66.basename(member.absPath);
|
|
9874
10121
|
return fromAbs || member.id;
|
|
9875
10122
|
}
|
|
9876
10123
|
function assetKey(type, destKey) {
|
|
@@ -9885,7 +10132,7 @@ async function buildLinkPlan(members) {
|
|
|
9885
10132
|
const byKey = /* @__PURE__ */ new Map();
|
|
9886
10133
|
const membersById = new Map(members.map((m) => [m.id, m]));
|
|
9887
10134
|
for (const member of members) {
|
|
9888
|
-
if (!await
|
|
10135
|
+
if (!await fs45.pathExists(member.absPath)) {
|
|
9889
10136
|
skipped.push({ repo: member.id, reason: `not cloned locally (${member.absPath}) \u2014 skipped` });
|
|
9890
10137
|
continue;
|
|
9891
10138
|
}
|
|
@@ -9927,7 +10174,7 @@ async function buildLinkPlan(members) {
|
|
|
9927
10174
|
name: asset.name,
|
|
9928
10175
|
destKey,
|
|
9929
10176
|
sourceRelPath: asset.sourceRelPath,
|
|
9930
|
-
sourceAbsPath:
|
|
10177
|
+
sourceAbsPath: path66.join(member.absPath, asset.sourceRelPath),
|
|
9931
10178
|
sourceHash
|
|
9932
10179
|
});
|
|
9933
10180
|
}
|
|
@@ -9938,7 +10185,7 @@ async function buildLinkPlan(members) {
|
|
|
9938
10185
|
function destRelPathFor(type, destKey) {
|
|
9939
10186
|
const subdir = `${type}s`;
|
|
9940
10187
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
9941
|
-
return
|
|
10188
|
+
return path67.posix.join(".claude", subdir, leaf);
|
|
9942
10189
|
}
|
|
9943
10190
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
9944
10191
|
const write = opts.write ?? false;
|
|
@@ -9964,17 +10211,17 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
9964
10211
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9965
10212
|
for (const entry of plan.entries) {
|
|
9966
10213
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
9967
|
-
const destAbs =
|
|
10214
|
+
const destAbs = path67.join(workspaceRoot, destRel);
|
|
9968
10215
|
if (write) {
|
|
9969
|
-
await
|
|
9970
|
-
if (await
|
|
9971
|
-
await
|
|
10216
|
+
await fs46.ensureDir(path67.dirname(destAbs));
|
|
10217
|
+
if (await fs46.pathExists(destAbs)) await fs46.remove(destAbs);
|
|
10218
|
+
await fs46.copy(entry.sourceAbsPath, destAbs, {
|
|
9972
10219
|
overwrite: true,
|
|
9973
10220
|
errorOnExist: false,
|
|
9974
10221
|
dereference: false,
|
|
9975
10222
|
filter: (s) => {
|
|
9976
10223
|
try {
|
|
9977
|
-
return !
|
|
10224
|
+
return !fs46.lstatSync(s).isSymbolicLink();
|
|
9978
10225
|
} catch {
|
|
9979
10226
|
return false;
|
|
9980
10227
|
}
|
|
@@ -9998,8 +10245,12 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
9998
10245
|
if (nextPaths.has(old.path)) continue;
|
|
9999
10246
|
removed.push(old.path);
|
|
10000
10247
|
if (write) {
|
|
10001
|
-
const abs =
|
|
10002
|
-
if (
|
|
10248
|
+
const abs = resolveContainedPath(workspaceRoot, old.path);
|
|
10249
|
+
if (abs === null) {
|
|
10250
|
+
warn(`Refusing to remove linked context path outside the workspace root: ${old.path}`);
|
|
10251
|
+
continue;
|
|
10252
|
+
}
|
|
10253
|
+
if (await fs46.pathExists(abs)) await fs46.remove(abs);
|
|
10003
10254
|
}
|
|
10004
10255
|
}
|
|
10005
10256
|
if (write) {
|
|
@@ -10051,11 +10302,11 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
10051
10302
|
|
|
10052
10303
|
// src/commands/workspace/setup.ts
|
|
10053
10304
|
import { existsSync as existsSync8, statSync } from "node:fs";
|
|
10054
|
-
import
|
|
10305
|
+
import path69 from "node:path";
|
|
10055
10306
|
|
|
10056
10307
|
// src/claude/write-workspace-claude-md.ts
|
|
10057
|
-
import
|
|
10058
|
-
import
|
|
10308
|
+
import path68 from "node:path";
|
|
10309
|
+
import fs47 from "fs-extra";
|
|
10059
10310
|
var WORKSPACE_SENTINELS = { begin: WORKSPACE_BLOCK_BEGIN, end: WORKSPACE_BLOCK_END };
|
|
10060
10311
|
function buildWorkspaceImportBlock(client, members) {
|
|
10061
10312
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
@@ -10074,11 +10325,11 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
10074
10325
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
10075
10326
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
10076
10327
|
const dryRun = opts.dryRun ?? false;
|
|
10077
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
10328
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path68.join(workspaceRoot, "CLAUDE.md");
|
|
10078
10329
|
const say = opts.quiet ? () => {
|
|
10079
10330
|
} : log;
|
|
10080
|
-
const existed = await
|
|
10081
|
-
const prev = existed ? await
|
|
10331
|
+
const existed = await fs47.pathExists(filePath);
|
|
10332
|
+
const prev = existed ? await fs47.readFile(filePath, "utf8") : "";
|
|
10082
10333
|
const next = opts.collision ? `${block}
|
|
10083
10334
|
` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
|
|
10084
10335
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -10105,20 +10356,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
10105
10356
|
|
|
10106
10357
|
// src/commands/workspace/setup.ts
|
|
10107
10358
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
10108
|
-
let dir =
|
|
10359
|
+
let dir = path69.resolve(start);
|
|
10109
10360
|
for (; ; ) {
|
|
10110
|
-
if (existsSync8(
|
|
10111
|
-
const parent =
|
|
10112
|
-
if (parent === dir) return
|
|
10361
|
+
if (existsSync8(path69.join(dir, WORKSPACE_FILE))) return dir;
|
|
10362
|
+
const parent = path69.dirname(dir);
|
|
10363
|
+
if (parent === dir) return path69.resolve(start);
|
|
10113
10364
|
dir = parent;
|
|
10114
10365
|
}
|
|
10115
10366
|
}
|
|
10116
10367
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
10117
|
-
return
|
|
10368
|
+
return path69.resolve(workspaceRoot, repoPath) === path69.resolve(workspaceRoot);
|
|
10118
10369
|
}
|
|
10119
10370
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
10120
10371
|
const apply = options.write ?? false;
|
|
10121
|
-
const configText = await readText(
|
|
10372
|
+
const configText = await readText(path69.join(workspaceRoot, WORKSPACE_FILE));
|
|
10122
10373
|
if (!configText) {
|
|
10123
10374
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10124
10375
|
process.exitCode = 1;
|
|
@@ -10145,7 +10396,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10145
10396
|
const statuses = [];
|
|
10146
10397
|
const aggregateInputs = [];
|
|
10147
10398
|
for (const repo of repos) {
|
|
10148
|
-
const repoRoot =
|
|
10399
|
+
const repoRoot = path69.resolve(workspaceRoot, repo.path);
|
|
10149
10400
|
say(`
|
|
10150
10401
|
\u2192 ${repo.name} (${repo.path})`);
|
|
10151
10402
|
try {
|
|
@@ -10178,7 +10429,15 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10178
10429
|
status: "failed",
|
|
10179
10430
|
error: message
|
|
10180
10431
|
});
|
|
10181
|
-
if (!options.continueOnError)
|
|
10432
|
+
if (!options.continueOnError) {
|
|
10433
|
+
if (options.json) {
|
|
10434
|
+
const result2 = { workspaceRoot, statuses, written: [] };
|
|
10435
|
+
log(JSON.stringify(result2, null, 2));
|
|
10436
|
+
process.exitCode = 1;
|
|
10437
|
+
return result2;
|
|
10438
|
+
}
|
|
10439
|
+
throw err;
|
|
10440
|
+
}
|
|
10182
10441
|
error(`Setup failed for ${repo.name}: ${message}`);
|
|
10183
10442
|
}
|
|
10184
10443
|
}
|
|
@@ -10227,7 +10486,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10227
10486
|
const status = statusByName.get(repo.name);
|
|
10228
10487
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
10229
10488
|
if (status?.status === "ok") {
|
|
10230
|
-
const lock = await checkLock(
|
|
10489
|
+
const lock = await checkLock(path69.resolve(workspaceRoot, repo.path));
|
|
10231
10490
|
manifestRepos.push({
|
|
10232
10491
|
name: repo.name,
|
|
10233
10492
|
path: repo.path,
|
|
@@ -10283,7 +10542,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10283
10542
|
try {
|
|
10284
10543
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
10285
10544
|
if (linkResult.ok) {
|
|
10286
|
-
written.push(...linkResult.added.map((rel) =>
|
|
10545
|
+
written.push(...linkResult.added.map((rel) => path69.join(workspaceRoot, rel)));
|
|
10287
10546
|
if (!options.json) {
|
|
10288
10547
|
say(
|
|
10289
10548
|
`Linked ${linkResult.linked.length} cross-repo skill/agent/command copy(ies) into .claude/ (${linkResult.added.length} new, ${linkResult.removed.length} removed)${linkResult.skipped.length > 0 ? ` \u2014 ${linkResult.skipped.length} member(s) skipped, see above` : ""}.`
|
|
@@ -10315,8 +10574,8 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
10315
10574
|
|
|
10316
10575
|
// src/commands/workspace/undo.ts
|
|
10317
10576
|
import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
|
|
10318
|
-
import
|
|
10319
|
-
import
|
|
10577
|
+
import path70 from "node:path";
|
|
10578
|
+
import fs48 from "fs-extra";
|
|
10320
10579
|
var WORKSPACE_AGGREGATE_FILES = [
|
|
10321
10580
|
"workspace-summary.json",
|
|
10322
10581
|
"dependency-ownership-map.json",
|
|
@@ -10352,7 +10611,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10352
10611
|
}
|
|
10353
10612
|
let anyRepoFailed = false;
|
|
10354
10613
|
for (const repo of config.repos) {
|
|
10355
|
-
const repoRoot =
|
|
10614
|
+
const repoRoot = path70.resolve(workspaceRoot, repo.path);
|
|
10356
10615
|
log(`
|
|
10357
10616
|
\u2192 ${repo.name} (${repo.path})`);
|
|
10358
10617
|
try {
|
|
@@ -10367,30 +10626,34 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10367
10626
|
}
|
|
10368
10627
|
const linkedContext = await readLinkedContext(workspaceRoot);
|
|
10369
10628
|
for (const entry of linkedContext) {
|
|
10370
|
-
const abs =
|
|
10371
|
-
if (
|
|
10372
|
-
|
|
10629
|
+
const abs = resolveContainedPath(workspaceRoot, entry.path);
|
|
10630
|
+
if (abs === null) {
|
|
10631
|
+
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
10632
|
+
continue;
|
|
10633
|
+
}
|
|
10634
|
+
if (await fs48.pathExists(abs)) {
|
|
10635
|
+
await fs48.remove(abs);
|
|
10373
10636
|
log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
10374
10637
|
}
|
|
10375
10638
|
}
|
|
10376
10639
|
for (const rel of WORKSPACE_AGGREGATE_FILES) {
|
|
10377
10640
|
const abs = hausPath(workspaceRoot, rel);
|
|
10378
|
-
if (await
|
|
10379
|
-
await
|
|
10380
|
-
log(`Removed ${
|
|
10641
|
+
if (await fs48.pathExists(abs)) {
|
|
10642
|
+
await fs48.remove(abs);
|
|
10643
|
+
log(`Removed ${path70.relative(workspaceRoot, abs)}`);
|
|
10381
10644
|
}
|
|
10382
10645
|
}
|
|
10383
10646
|
const manifestFile = manifestPath(workspaceRoot);
|
|
10384
|
-
if (await
|
|
10385
|
-
await
|
|
10386
|
-
log(`Removed ${
|
|
10647
|
+
if (await fs48.pathExists(manifestFile)) {
|
|
10648
|
+
await fs48.remove(manifestFile);
|
|
10649
|
+
log(`Removed ${path70.relative(workspaceRoot, manifestFile)}`);
|
|
10387
10650
|
}
|
|
10388
10651
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
10389
|
-
if (await
|
|
10390
|
-
await
|
|
10391
|
-
log(`Removed ${
|
|
10652
|
+
if (await fs48.pathExists(workspaceMdPath)) {
|
|
10653
|
+
await fs48.remove(workspaceMdPath);
|
|
10654
|
+
log(`Removed ${path70.relative(workspaceRoot, workspaceMdPath)}`);
|
|
10392
10655
|
} else {
|
|
10393
|
-
const claudeMdPath =
|
|
10656
|
+
const claudeMdPath = path70.join(workspaceRoot, "CLAUDE.md");
|
|
10394
10657
|
if (existsSync9(claudeMdPath)) {
|
|
10395
10658
|
const prev = await readText(claudeMdPath);
|
|
10396
10659
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -10399,19 +10662,19 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10399
10662
|
end: WORKSPACE_BLOCK_END
|
|
10400
10663
|
});
|
|
10401
10664
|
if (next.length === 0) {
|
|
10402
|
-
await
|
|
10665
|
+
await fs48.remove(claudeMdPath);
|
|
10403
10666
|
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
10404
10667
|
} else {
|
|
10405
|
-
await
|
|
10668
|
+
await fs48.writeFile(claudeMdPath, next, "utf8");
|
|
10406
10669
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
10407
10670
|
}
|
|
10408
10671
|
} else if (prev && hasLegacyWorkspaceBlock(prev)) {
|
|
10409
10672
|
const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
|
|
10410
10673
|
if (next.length === 0) {
|
|
10411
|
-
await
|
|
10674
|
+
await fs48.remove(claudeMdPath);
|
|
10412
10675
|
log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
|
|
10413
10676
|
} else {
|
|
10414
|
-
await
|
|
10677
|
+
await fs48.writeFile(claudeMdPath, next, "utf8");
|
|
10415
10678
|
log(
|
|
10416
10679
|
"Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
|
|
10417
10680
|
);
|
|
@@ -10442,7 +10705,16 @@ function normalizeMaxDepth(maxDepth) {
|
|
|
10442
10705
|
warn(`Ignoring invalid --max-depth "${String(maxDepth)}"; using the default depth.`);
|
|
10443
10706
|
return void 0;
|
|
10444
10707
|
}
|
|
10445
|
-
async function initWorkspace() {
|
|
10708
|
+
async function initWorkspace(options = {}) {
|
|
10709
|
+
if (existsSync10(WORKSPACE_FILE) && !options.yes) {
|
|
10710
|
+
const ok = await confirm(
|
|
10711
|
+
`${WORKSPACE_FILE} already exists \u2014 overwrite with a fresh placeholder? Existing client/repos/relationships will be lost. Run \`haus workspace discover\` instead to merge in newly found repos.`
|
|
10712
|
+
);
|
|
10713
|
+
if (!ok) {
|
|
10714
|
+
log("Cancelled.");
|
|
10715
|
+
return;
|
|
10716
|
+
}
|
|
10717
|
+
}
|
|
10446
10718
|
await writeText(
|
|
10447
10719
|
WORKSPACE_FILE,
|
|
10448
10720
|
`client: unknown
|
|
@@ -10456,7 +10728,7 @@ relationships: []
|
|
|
10456
10728
|
log("Workspace initialized.");
|
|
10457
10729
|
}
|
|
10458
10730
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
10459
|
-
const configText = await readText(
|
|
10731
|
+
const configText = await readText(path71.join(workspaceRoot, WORKSPACE_FILE));
|
|
10460
10732
|
if (!configText) {
|
|
10461
10733
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10462
10734
|
process.exitCode = 1;
|
|
@@ -10478,7 +10750,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
10478
10750
|
const inputs = [];
|
|
10479
10751
|
const failures = [];
|
|
10480
10752
|
for (const repo of config.repos) {
|
|
10481
|
-
const repoRoot =
|
|
10753
|
+
const repoRoot = path71.resolve(workspaceRoot, repo.path);
|
|
10482
10754
|
try {
|
|
10483
10755
|
if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
10484
10756
|
throw new Error("path is not a directory");
|
|
@@ -10504,7 +10776,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
10504
10776
|
}
|
|
10505
10777
|
async function runWorkspace(action, options = {}) {
|
|
10506
10778
|
if (action === "init") {
|
|
10507
|
-
await initWorkspace();
|
|
10779
|
+
await initWorkspace({ yes: options.yes });
|
|
10508
10780
|
return;
|
|
10509
10781
|
}
|
|
10510
10782
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
@@ -10544,7 +10816,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
10544
10816
|
// src/cli.ts
|
|
10545
10817
|
function cliVersion() {
|
|
10546
10818
|
try {
|
|
10547
|
-
const pkgPath =
|
|
10819
|
+
const pkgPath = path72.join(packageRoot(), "package.json");
|
|
10548
10820
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10549
10821
|
return pkg.version ?? "0.0.0";
|
|
10550
10822
|
} catch {
|
|
@@ -10554,7 +10826,7 @@ function cliVersion() {
|
|
|
10554
10826
|
var program = new Command();
|
|
10555
10827
|
function validateRuntimeNodeVersion() {
|
|
10556
10828
|
try {
|
|
10557
|
-
const pkgPath =
|
|
10829
|
+
const pkgPath = path72.join(packageRoot(), "package.json");
|
|
10558
10830
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10559
10831
|
const requiredRange = pkg.engines?.node;
|
|
10560
10832
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -10615,7 +10887,7 @@ program.command("install").option("--dry-run").option("--force").option("--check
|
|
|
10615
10887
|
program.command("scaffold [ids...]").description(
|
|
10616
10888
|
"Copy catalog config items (ESLint, Prettier) into the project root. Pass item IDs to scaffold specific items, or omit to scaffold all available config items."
|
|
10617
10889
|
).option("--force", "Overwrite existing files").option("--dry-run", "Preview what would be written without changing anything").option("--root <path>", "Project root (defaults to cwd)", process.cwd()).action(runScaffold);
|
|
10618
|
-
program.command("uninstall").option("--force").action(runUninstallCommand);
|
|
10890
|
+
program.command("uninstall").option("--force").option("-y, --yes", "Skip confirmation").action(runUninstallCommand);
|
|
10619
10891
|
var guard = program.command("guard");
|
|
10620
10892
|
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
10621
10893
|
guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
@@ -10632,7 +10904,7 @@ function registerDecisionsCommand(name) {
|
|
|
10632
10904
|
registerDecisionsCommand("decisions");
|
|
10633
10905
|
registerDecisionsCommand("adr");
|
|
10634
10906
|
var workspace = program.command("workspace");
|
|
10635
|
-
workspace.command("init").action(() => runWorkspace("init"));
|
|
10907
|
+
workspace.command("init").option("-y, --yes", "Skip confirmation when haus.workspace.yaml already exists").action((opts) => runWorkspace("init", opts));
|
|
10636
10908
|
workspace.command("discover").description("Auto-find member repos and write/merge haus.workspace.yaml").option("--write", "Persist haus.workspace.yaml (default previews only)").option("--json", "Output the discovered repos and proposed config as JSON").option("--max-depth <n>", "Max directory depth to traverse (default 3)").option("--client <name>", "Set the workspace client name").action((opts) => runWorkspace("discover", opts));
|
|
10637
10909
|
workspace.command("scan").description("Aggregate a cross-repo summary from a fast scan of each repo").option("--json", "Output the written artifact paths as JSON").action((opts) => runWorkspace("scan", opts));
|
|
10638
10910
|
workspace.command("setup").description("Per-repo setup loop + workspace layer + manifest").option("--write", "Apply changes (default previews only)").option("--dry-run", "Preview changes without writing").option("--json", "Emit machine-readable per-repo output").option("--continue-on-error", "Keep going past a failed repo (default fail-fast)").option("--only <names>", "Restrict to comma-separated repo names").action((opts) => runWorkspace("setup", opts));
|