@haus-tech/haus-workflow 1.5.4 → 1.5.6
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 +22 -0
- package/dist/cli.js +1089 -785
- package/library/catalog/manifest.json +373 -150
- 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);
|
|
@@ -685,10 +693,17 @@ function validateReferences(itemId, references) {
|
|
|
685
693
|
if (references === void 0) return null;
|
|
686
694
|
if (!isStringArray(references)) return `${itemId}: references must be a string array`;
|
|
687
695
|
for (const ref of references) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
696
|
+
let parsed;
|
|
697
|
+
try {
|
|
698
|
+
parsed = new URL(ref);
|
|
699
|
+
} catch {
|
|
700
|
+
return `${itemId}: reference is not a valid URL: ${ref}`;
|
|
701
|
+
}
|
|
702
|
+
if (parsed.protocol === "http:") {
|
|
703
|
+
return `${itemId}: reference uses insecure http:// URL: ${ref}`;
|
|
704
|
+
}
|
|
705
|
+
if (parsed.protocol !== "https:") {
|
|
706
|
+
return `${itemId}: reference must be https:// URL: ${ref}`;
|
|
692
707
|
}
|
|
693
708
|
}
|
|
694
709
|
return null;
|
|
@@ -743,16 +758,29 @@ function validateCuratedProvenance(item) {
|
|
|
743
758
|
if (!RISK_LEVELS.has(item.riskLevel)) {
|
|
744
759
|
return `${item.id}: invalid riskLevel "${item.riskLevel}"`;
|
|
745
760
|
}
|
|
746
|
-
if (
|
|
761
|
+
if (!isNonEmptyString(item.originSourceId)) {
|
|
762
|
+
return `${item.id}: curated item missing originSourceId`;
|
|
763
|
+
}
|
|
764
|
+
if (!isNonEmptyString(item.originUrl)) {
|
|
765
|
+
return `${item.id}: curated item missing originUrl`;
|
|
766
|
+
}
|
|
767
|
+
if (!isNonEmptyString(item.useMode)) {
|
|
768
|
+
return `${item.id}: curated item missing useMode`;
|
|
769
|
+
}
|
|
770
|
+
if (!USE_MODES.has(item.useMode)) {
|
|
747
771
|
return `${item.id}: invalid useMode "${item.useMode}"`;
|
|
748
772
|
}
|
|
749
|
-
if (
|
|
773
|
+
if (!isNonEmptyString(item.license)) {
|
|
774
|
+
return `${item.id}: curated item missing license`;
|
|
775
|
+
}
|
|
776
|
+
if (!isNonEmptyString(item.licenseConfidence)) {
|
|
777
|
+
return `${item.id}: curated item missing licenseConfidence`;
|
|
778
|
+
}
|
|
779
|
+
if (!LICENSE_CONFIDENCES.has(item.licenseConfidence)) {
|
|
750
780
|
return `${item.id}: invalid licenseConfidence "${item.licenseConfidence}"`;
|
|
751
781
|
}
|
|
752
|
-
if (item.originUrl !==
|
|
753
|
-
|
|
754
|
-
return `${item.id}: originUrl must be an https:// URL`;
|
|
755
|
-
}
|
|
782
|
+
if (typeof item.originUrl !== "string" || !item.originUrl.startsWith("https://")) {
|
|
783
|
+
return `${item.id}: originUrl must be an https:// URL`;
|
|
756
784
|
}
|
|
757
785
|
return null;
|
|
758
786
|
}
|
|
@@ -774,9 +802,6 @@ function isRequiresAnyClause(value) {
|
|
|
774
802
|
if (!REQUIRES_ANY_KEYS.includes(key)) return false;
|
|
775
803
|
return typeof value[key] === "string";
|
|
776
804
|
}
|
|
777
|
-
function isRequiresAnyArray(value) {
|
|
778
|
-
return Array.isArray(value) && value.every(isRequiresAnyClause);
|
|
779
|
-
}
|
|
780
805
|
function safeParse(json) {
|
|
781
806
|
return JSON.parse(json, (key, value) => {
|
|
782
807
|
if (POLLUTION_KEYS.has(key)) return void 0;
|
|
@@ -803,6 +828,7 @@ function parseManifest(json) {
|
|
|
803
828
|
const items = [];
|
|
804
829
|
const seenIds = /* @__PURE__ */ new Set();
|
|
805
830
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
831
|
+
const warnings = [];
|
|
806
832
|
for (let i = 0; i < root.items.length; i++) {
|
|
807
833
|
const raw = root.items[i];
|
|
808
834
|
if (!raw || typeof raw !== "object") {
|
|
@@ -837,8 +863,21 @@ function parseManifest(json) {
|
|
|
837
863
|
if (!isStringArray2(item.repoRoles)) {
|
|
838
864
|
return { ok: false, error: `${item.id}: repoRoles must be a string array` };
|
|
839
865
|
}
|
|
840
|
-
if (item.requiresAny !== void 0
|
|
841
|
-
|
|
866
|
+
if (item.requiresAny !== void 0) {
|
|
867
|
+
if (!Array.isArray(item.requiresAny)) {
|
|
868
|
+
return { ok: false, error: `${item.id}: requiresAny must be an array of clause objects` };
|
|
869
|
+
}
|
|
870
|
+
const kept = [];
|
|
871
|
+
for (const clause of item.requiresAny) {
|
|
872
|
+
if (isRequiresAnyClause(clause)) {
|
|
873
|
+
kept.push(clause);
|
|
874
|
+
} else {
|
|
875
|
+
warnings.push(
|
|
876
|
+
`${item.id}: dropping unrecognized requiresAny clause (catalog schema evolved ahead of this CLI version) \u2014 ${JSON.stringify(clause)}`
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
item.requiresAny = kept;
|
|
842
881
|
}
|
|
843
882
|
if (typeof item.tokenEstimate !== "number" || !Number.isFinite(item.tokenEstimate)) {
|
|
844
883
|
return { ok: false, error: `${item.id}: tokenEstimate must be a finite number` };
|
|
@@ -855,7 +894,7 @@ function parseManifest(json) {
|
|
|
855
894
|
if (formerIdFailures.length > 0) {
|
|
856
895
|
return { ok: false, error: formerIdFailures[0] };
|
|
857
896
|
}
|
|
858
|
-
return { ok: true, manifest: { version: root.version, items } };
|
|
897
|
+
return { ok: true, manifest: { version: root.version, items }, warnings };
|
|
859
898
|
}
|
|
860
899
|
|
|
861
900
|
// src/catalog/remote-catalog/manifest.ts
|
|
@@ -868,6 +907,7 @@ async function fetchRemoteManifest() {
|
|
|
868
907
|
warn(`Remote manifest failed schema validation: ${parsed.error}`);
|
|
869
908
|
return null;
|
|
870
909
|
}
|
|
910
|
+
for (const warning of parsed.warnings) warn(warning);
|
|
871
911
|
if (!parsed.manifest.items.length) return null;
|
|
872
912
|
return parsed.manifest;
|
|
873
913
|
}
|
|
@@ -929,6 +969,7 @@ var validation_rules_default = {
|
|
|
929
969
|
"haus",
|
|
930
970
|
"security",
|
|
931
971
|
"quality",
|
|
972
|
+
"a11y",
|
|
932
973
|
"frontend",
|
|
933
974
|
"backend",
|
|
934
975
|
"testing",
|
|
@@ -1552,7 +1593,7 @@ function summarizeGitignoredHausDirs(paths) {
|
|
|
1552
1593
|
}
|
|
1553
1594
|
|
|
1554
1595
|
// src/claude/write-claude-files.ts
|
|
1555
|
-
import
|
|
1596
|
+
import path20 from "node:path";
|
|
1556
1597
|
import fs18 from "fs-extra";
|
|
1557
1598
|
|
|
1558
1599
|
// src/catalog/former-ids.ts
|
|
@@ -1608,8 +1649,199 @@ function findOrphanedLockEntries(lock, recommendedIds) {
|
|
|
1608
1649
|
);
|
|
1609
1650
|
}
|
|
1610
1651
|
|
|
1611
|
-
// src/
|
|
1652
|
+
// src/scanner/detection.ts
|
|
1612
1653
|
import path10 from "node:path";
|
|
1654
|
+
|
|
1655
|
+
// src/security/sensitive-paths.ts
|
|
1656
|
+
var DENY_PATHS = [
|
|
1657
|
+
"*.pem",
|
|
1658
|
+
"*.key",
|
|
1659
|
+
"*.p12",
|
|
1660
|
+
"*.pfx",
|
|
1661
|
+
"id_rsa",
|
|
1662
|
+
"id_ed25519",
|
|
1663
|
+
"customer-data",
|
|
1664
|
+
"secrets",
|
|
1665
|
+
"certs"
|
|
1666
|
+
];
|
|
1667
|
+
var DENY_DIRS = /* @__PURE__ */ new Set(["customer-data", "secrets", "certs"]);
|
|
1668
|
+
var DENY_PATH_REGEXES = [
|
|
1669
|
+
/(^|\/)[^/]+\.pem$/i,
|
|
1670
|
+
/(^|\/)[^/]+\.key$/i,
|
|
1671
|
+
/(^|\/)[^/]+\.p12$/i,
|
|
1672
|
+
/(^|\/)[^/]+\.pfx$/i,
|
|
1673
|
+
/(^|\/)id_rsa$/i,
|
|
1674
|
+
/(^|\/)id_ed25519$/i,
|
|
1675
|
+
/(^|\/)customer-data(\/|$)/i,
|
|
1676
|
+
/(^|\/)secrets(\/|$)/i,
|
|
1677
|
+
/(^|\/)certs(\/|$)/i
|
|
1678
|
+
];
|
|
1679
|
+
function normalizePathForMatch(candidate) {
|
|
1680
|
+
return candidate.replace(/\\/g, "/").replace(/\/+/g, "/").trim();
|
|
1681
|
+
}
|
|
1682
|
+
var ASK_PATHS = [
|
|
1683
|
+
{ pattern: ".env", tools: ["Edit", "Write"] },
|
|
1684
|
+
{ pattern: ".env.*", tools: ["Edit", "Write"] },
|
|
1685
|
+
{ pattern: "storage/logs/**", tools: ["Edit", "Write"] },
|
|
1686
|
+
{ pattern: "exports/**", tools: ["Edit", "Write"] },
|
|
1687
|
+
{ pattern: "*.dump", tools: ["Read", "Edit", "Write"] },
|
|
1688
|
+
{ pattern: "*.backup", tools: ["Read", "Edit", "Write"] },
|
|
1689
|
+
{ pattern: "*.bak", tools: ["Read", "Edit", "Write"] },
|
|
1690
|
+
{ pattern: "wp-content/uploads/**", tools: ["Read", "Edit", "Write"] },
|
|
1691
|
+
{ pattern: "uploads/**", tools: ["Read", "Edit", "Write"] }
|
|
1692
|
+
];
|
|
1693
|
+
var SENSITIVE_PATH_REGEXES = [
|
|
1694
|
+
/^\.env(\.|$)/,
|
|
1695
|
+
/(^|\/)\.env(\.|$)/,
|
|
1696
|
+
/\.pem$/,
|
|
1697
|
+
/\.key$/,
|
|
1698
|
+
/\.p12$/,
|
|
1699
|
+
/\.pfx$/,
|
|
1700
|
+
/\.sql$/,
|
|
1701
|
+
/\.dump$/,
|
|
1702
|
+
/customer-data/,
|
|
1703
|
+
/exports/,
|
|
1704
|
+
/certs/,
|
|
1705
|
+
/secrets/,
|
|
1706
|
+
/(^|\/)storage\/logs(\/|$)/,
|
|
1707
|
+
/(^|\/)wp-content\/uploads(\/|$)/,
|
|
1708
|
+
/(^|\/)uploads(\/|$)/
|
|
1709
|
+
];
|
|
1710
|
+
var SENSITIVE_ITEM_KEYWORDS = [
|
|
1711
|
+
".env",
|
|
1712
|
+
"secrets",
|
|
1713
|
+
"certs",
|
|
1714
|
+
"customer-data",
|
|
1715
|
+
"exports",
|
|
1716
|
+
".pem",
|
|
1717
|
+
".key"
|
|
1718
|
+
];
|
|
1719
|
+
|
|
1720
|
+
// src/utils/audit-checks.ts
|
|
1721
|
+
function isRecord(v) {
|
|
1722
|
+
return typeof v === "object" && v !== null;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/scanner/detection.ts
|
|
1726
|
+
var UNSUPPORTED_MARKERS = {
|
|
1727
|
+
"requirements.txt": "python",
|
|
1728
|
+
"pyproject.toml": "python",
|
|
1729
|
+
"go.mod": "go",
|
|
1730
|
+
"Cargo.toml": "rust",
|
|
1731
|
+
"pom.xml": "java",
|
|
1732
|
+
"build.gradle": "java",
|
|
1733
|
+
"build.gradle.kts": "java",
|
|
1734
|
+
Gemfile: "ruby"
|
|
1735
|
+
};
|
|
1736
|
+
var WEAK_STACK_SIGNALS = /* @__PURE__ */ new Set(["missing-prettier", "missing-eslint"]);
|
|
1737
|
+
function computeDetectionStatus(roles, stacks, unsupportedSignals) {
|
|
1738
|
+
const hasRealStack = Object.entries(stacks).some(
|
|
1739
|
+
([bucket, names]) => bucket !== "packageManagers" && names.some((n) => !WEAK_STACK_SIGNALS.has(n))
|
|
1740
|
+
);
|
|
1741
|
+
const hasRealSignal = roles.length > 0 || hasRealStack;
|
|
1742
|
+
if (!hasRealSignal) return "unknown";
|
|
1743
|
+
return unsupportedSignals.length > 0 ? "partial" : "supported";
|
|
1744
|
+
}
|
|
1745
|
+
function describeUnknownDetection(unsupportedSignals, isLinkedWorktree) {
|
|
1746
|
+
const markers = unsupportedSignals.join(", ");
|
|
1747
|
+
const base = markers ? `Stack not recognised \u2014 detected ${markers}, which haus does not support.` : "Stack not recognised \u2014 no supported framework detected.";
|
|
1748
|
+
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." : "";
|
|
1749
|
+
return [base, worktreeNote, "Only stack-agnostic workflow and security guidance is applied."].filter(Boolean).join(" ");
|
|
1750
|
+
}
|
|
1751
|
+
function blocked(rel) {
|
|
1752
|
+
return SENSITIVE_PATH_REGEXES.some((x) => x.test(rel));
|
|
1753
|
+
}
|
|
1754
|
+
function dependencyVersionMap(pkg, composer) {
|
|
1755
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1756
|
+
const pushObj = (obj) => {
|
|
1757
|
+
if (!isRecord(obj)) return;
|
|
1758
|
+
for (const key of Object.keys(obj)) {
|
|
1759
|
+
if (versions.has(key)) continue;
|
|
1760
|
+
const value = obj[key];
|
|
1761
|
+
versions.set(key, typeof value === "string" ? value : "");
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
pushObj(pkg?.dependencies);
|
|
1765
|
+
pushObj(pkg?.devDependencies);
|
|
1766
|
+
pushObj(composer?.require);
|
|
1767
|
+
pushObj(composer?.["require-dev"]);
|
|
1768
|
+
return versions;
|
|
1769
|
+
}
|
|
1770
|
+
function finalizeRoles(registryRoles, deps, files) {
|
|
1771
|
+
const roles = new Set(registryRoles);
|
|
1772
|
+
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
1773
|
+
const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
|
|
1774
|
+
if (hasWpConfig && hasBedrockLayout) {
|
|
1775
|
+
roles.add("wordpress-bedrock-site");
|
|
1776
|
+
roles.add("wordpress-site");
|
|
1777
|
+
} else if (hasWpConfig) {
|
|
1778
|
+
roles.add("wordpress-vanilla-site");
|
|
1779
|
+
roles.add("wordpress-site");
|
|
1780
|
+
} else if (deps.includes("roots/wordpress")) {
|
|
1781
|
+
roles.add("wordpress-bedrock-site");
|
|
1782
|
+
roles.add("wordpress-site");
|
|
1783
|
+
}
|
|
1784
|
+
return [...roles].sort();
|
|
1785
|
+
}
|
|
1786
|
+
function collectUnsupportedSignals(files) {
|
|
1787
|
+
return [
|
|
1788
|
+
...new Set(
|
|
1789
|
+
files.map((f) => UNSUPPORTED_MARKERS[path10.basename(f)]).filter((s) => Boolean(s))
|
|
1790
|
+
)
|
|
1791
|
+
].sort();
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// src/recommender/policies.ts
|
|
1795
|
+
var UNSUPPORTED = FORBIDDEN_TAGS;
|
|
1796
|
+
function matchRequiresAny(clauses, ctx) {
|
|
1797
|
+
for (const clause of clauses) {
|
|
1798
|
+
if ("stack" in clause) {
|
|
1799
|
+
if (ctx.stackSet.has(clause.stack.toLowerCase())) {
|
|
1800
|
+
return { matched: true, signal: `stack:${clause.stack}` };
|
|
1801
|
+
}
|
|
1802
|
+
} else if ("dependency" in clause) {
|
|
1803
|
+
if (ctx.depSet.has(clause.dependency.toLowerCase())) {
|
|
1804
|
+
return { matched: true, signal: `dependency:${clause.dependency}` };
|
|
1805
|
+
}
|
|
1806
|
+
} else if ("packageNamePattern" in clause) {
|
|
1807
|
+
const pattern = clause.packageNamePattern.toLowerCase();
|
|
1808
|
+
const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
|
|
1809
|
+
for (const dep2 of ctx.depSet) {
|
|
1810
|
+
if (pattern.endsWith("*") ? dep2.startsWith(prefix) : dep2 === pattern) {
|
|
1811
|
+
return {
|
|
1812
|
+
matched: true,
|
|
1813
|
+
signal: `packageNamePattern:${clause.packageNamePattern}`
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
} else if ("role" in clause) {
|
|
1818
|
+
if (ctx.roleSet.has(clause.role.toLowerCase())) {
|
|
1819
|
+
return { matched: true, signal: `role:${clause.role}` };
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return { matched: false };
|
|
1824
|
+
}
|
|
1825
|
+
function describeRequiresAny(clauses) {
|
|
1826
|
+
return clauses.map((c) => {
|
|
1827
|
+
if ("stack" in c) return `stack=${c.stack}`;
|
|
1828
|
+
if ("dependency" in c) return `dependency=${c.dependency}`;
|
|
1829
|
+
if ("packageNamePattern" in c) return `packageNamePattern=${c.packageNamePattern}`;
|
|
1830
|
+
if ("role" in c) return `role=${c.role}`;
|
|
1831
|
+
return "unknown";
|
|
1832
|
+
}).join(" | ");
|
|
1833
|
+
}
|
|
1834
|
+
function mergeRecommendationWarnings(context, isLinkedWorktree = false) {
|
|
1835
|
+
const markers = context.unsupportedSignals?.join(", ");
|
|
1836
|
+
const statusLines = context.detectionStatus === "unknown" ? [describeUnknownDetection(context.unsupportedSignals ?? [], isLinkedWorktree)] : context.detectionStatus === "partial" && markers ? [
|
|
1837
|
+
`Partially supported \u2014 found unsupported ${markers} alongside recognised stacks; guidance covers the supported parts only.`
|
|
1838
|
+
] : [];
|
|
1839
|
+
const riskLines = (context.securityRisks?.length ?? 0) > 0 ? [`Scan reported security signals: ${context.securityRisks.join("; ")}`] : [];
|
|
1840
|
+
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// src/update/hash-installed.ts
|
|
1844
|
+
import path11 from "node:path";
|
|
1613
1845
|
import fg2 from "fast-glob";
|
|
1614
1846
|
import fs7 from "fs-extra";
|
|
1615
1847
|
|
|
@@ -1666,7 +1898,7 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
|
|
|
1666
1898
|
const normalized = [...new Set(relPaths.map((p) => p.replace(/\\/g, "/")))].sort();
|
|
1667
1899
|
const fileDigests = [];
|
|
1668
1900
|
for (const rel of normalized) {
|
|
1669
|
-
const abs =
|
|
1901
|
+
const abs = path11.join(root, rel);
|
|
1670
1902
|
if (!await fs7.pathExists(abs)) continue;
|
|
1671
1903
|
const stat = followSymlinks ? await fs7.stat(abs) : await fs7.lstat(abs);
|
|
1672
1904
|
if (!followSymlinks && stat.isSymbolicLink()) continue;
|
|
@@ -1683,9 +1915,9 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
|
|
|
1683
1915
|
followSymbolicLinks: followSymlinks
|
|
1684
1916
|
});
|
|
1685
1917
|
for (const sub of inner.sort()) {
|
|
1686
|
-
const absFile =
|
|
1918
|
+
const absFile = path11.join(abs, sub);
|
|
1687
1919
|
if (!followSymlinks && (await fs7.lstat(absFile)).isSymbolicLink()) continue;
|
|
1688
|
-
const relFile =
|
|
1920
|
+
const relFile = path11.join(rel, sub).replace(/\\/g, "/");
|
|
1689
1921
|
const body = await fs7.readFile(absFile);
|
|
1690
1922
|
fileDigests.push({ rel: relFile, digest: digestFileContent(body) });
|
|
1691
1923
|
}
|
|
@@ -1757,18 +1989,18 @@ function buildAllowRules() {
|
|
|
1757
1989
|
}
|
|
1758
1990
|
|
|
1759
1991
|
// src/install/settings-merge.ts
|
|
1760
|
-
import
|
|
1992
|
+
import path13 from "node:path";
|
|
1761
1993
|
import fs9 from "fs-extra";
|
|
1762
1994
|
|
|
1763
1995
|
// src/install/manifest.ts
|
|
1764
1996
|
import os3 from "node:os";
|
|
1765
|
-
import
|
|
1997
|
+
import path12 from "node:path";
|
|
1766
1998
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
1767
1999
|
function globalClaudeDir() {
|
|
1768
|
-
return
|
|
2000
|
+
return path12.join(os3.homedir(), ".claude");
|
|
1769
2001
|
}
|
|
1770
2002
|
function hausManifestPath() {
|
|
1771
|
-
return
|
|
2003
|
+
return path12.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
1772
2004
|
}
|
|
1773
2005
|
async function readManifest() {
|
|
1774
2006
|
return readJson(hausManifestPath());
|
|
@@ -1788,7 +2020,7 @@ function buildManifest(source, files, hooks) {
|
|
|
1788
2020
|
|
|
1789
2021
|
// src/install/settings-merge.ts
|
|
1790
2022
|
function settingsJsonPath() {
|
|
1791
|
-
return
|
|
2023
|
+
return path13.join(globalClaudeDir(), "settings.json");
|
|
1792
2024
|
}
|
|
1793
2025
|
async function readSettings() {
|
|
1794
2026
|
const filePath = settingsJsonPath();
|
|
@@ -2017,71 +2249,6 @@ var ASK_COMMANDS = [
|
|
|
2017
2249
|
"php artisan migrate --force"
|
|
2018
2250
|
];
|
|
2019
2251
|
|
|
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
2252
|
// src/security/ask-rules.ts
|
|
2086
2253
|
function buildAskRules() {
|
|
2087
2254
|
const rules = [];
|
|
@@ -2103,8 +2270,8 @@ function buildDenyRules() {
|
|
|
2103
2270
|
for (const command of DENY_COMMANDS) {
|
|
2104
2271
|
rules.push(`Bash(${command}:*)`);
|
|
2105
2272
|
}
|
|
2106
|
-
for (const
|
|
2107
|
-
const pattern = DENY_DIRS.has(
|
|
2273
|
+
for (const path73 of DENY_PATHS) {
|
|
2274
|
+
const pattern = DENY_DIRS.has(path73) ? `${path73}/**` : path73;
|
|
2108
2275
|
for (const tool of FILE_TOOLS) {
|
|
2109
2276
|
rules.push(`${tool}(${pattern})`);
|
|
2110
2277
|
}
|
|
@@ -2183,24 +2350,28 @@ async function applyProjectSettingsMerge(root) {
|
|
|
2183
2350
|
}
|
|
2184
2351
|
|
|
2185
2352
|
// src/claude/superpowers-install.ts
|
|
2186
|
-
import
|
|
2353
|
+
import path14 from "node:path";
|
|
2187
2354
|
import fg3 from "fast-glob";
|
|
2188
2355
|
import fs10 from "fs-extra";
|
|
2189
2356
|
var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
|
|
2190
2357
|
var PATH_REWRITES = [
|
|
2191
2358
|
["skills/shared/", ".claude/skills/shared/"]
|
|
2192
2359
|
];
|
|
2360
|
+
var PREFIX_REWRITES = ["superpowers-extended-cc:"];
|
|
2193
2361
|
function rewriteSuperpowersMarkdown(text) {
|
|
2194
2362
|
let out = text;
|
|
2195
2363
|
for (const [from, to] of PATH_REWRITES) {
|
|
2196
2364
|
out = out.split(from).join(to);
|
|
2197
2365
|
}
|
|
2366
|
+
for (const prefix of PREFIX_REWRITES) {
|
|
2367
|
+
out = out.split(prefix).join("");
|
|
2368
|
+
}
|
|
2198
2369
|
return out;
|
|
2199
2370
|
}
|
|
2200
2371
|
async function rewriteMarkdownTree(dir) {
|
|
2201
2372
|
const files = await fg3("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
|
|
2202
2373
|
for (const rel of files) {
|
|
2203
|
-
const abs =
|
|
2374
|
+
const abs = path14.join(dir, rel);
|
|
2204
2375
|
const text = await fs10.readFile(abs, "utf8");
|
|
2205
2376
|
const rewritten = rewriteSuperpowersMarkdown(text);
|
|
2206
2377
|
if (rewritten !== text) {
|
|
@@ -2208,10 +2379,24 @@ async function rewriteMarkdownTree(dir) {
|
|
|
2208
2379
|
}
|
|
2209
2380
|
}
|
|
2210
2381
|
}
|
|
2382
|
+
async function extraDestFiles(sourcePath, destination) {
|
|
2383
|
+
const [sourceFiles, destFiles] = await Promise.all([
|
|
2384
|
+
fg3("**/*", { cwd: sourcePath, onlyFiles: true, dot: true }),
|
|
2385
|
+
fg3("**/*", { cwd: destination, onlyFiles: true, dot: true })
|
|
2386
|
+
]);
|
|
2387
|
+
const sourceSet = new Set(sourceFiles);
|
|
2388
|
+
return destFiles.filter((f) => !sourceSet.has(f));
|
|
2389
|
+
}
|
|
2211
2390
|
async function installCatalogSkill(sourcePath, destination, opts) {
|
|
2212
2391
|
if (opts.dryRun) return;
|
|
2213
|
-
await fs10.ensureDir(
|
|
2392
|
+
await fs10.ensureDir(path14.dirname(destination));
|
|
2214
2393
|
if (await fs10.pathExists(destination)) {
|
|
2394
|
+
const extra = await extraDestFiles(sourcePath, destination);
|
|
2395
|
+
if (extra.length > 0) {
|
|
2396
|
+
warn(
|
|
2397
|
+
`${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2215
2400
|
await fs10.remove(destination);
|
|
2216
2401
|
}
|
|
2217
2402
|
await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
@@ -2220,17 +2405,23 @@ async function installCatalogSkill(sourcePath, destination, opts) {
|
|
|
2220
2405
|
}
|
|
2221
2406
|
}
|
|
2222
2407
|
async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
|
|
2223
|
-
const source =
|
|
2408
|
+
const source = path14.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
|
|
2224
2409
|
if (!await fs10.pathExists(source)) return null;
|
|
2225
2410
|
const destination = claudePath(projectRoot, "skills", "shared");
|
|
2226
|
-
if (dryRun) return
|
|
2227
|
-
await fs10.ensureDir(
|
|
2411
|
+
if (dryRun) return path14.relative(projectRoot, destination);
|
|
2412
|
+
await fs10.ensureDir(path14.dirname(destination));
|
|
2228
2413
|
if (await fs10.pathExists(destination)) {
|
|
2414
|
+
const extra = await extraDestFiles(source, destination);
|
|
2415
|
+
if (extra.length > 0) {
|
|
2416
|
+
warn(
|
|
2417
|
+
`${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2229
2420
|
await fs10.remove(destination);
|
|
2230
2421
|
}
|
|
2231
2422
|
await fs10.copy(source, destination, { overwrite: true, errorOnExist: false });
|
|
2232
2423
|
await rewriteMarkdownTree(destination);
|
|
2233
|
-
return
|
|
2424
|
+
return path14.relative(projectRoot, destination);
|
|
2234
2425
|
}
|
|
2235
2426
|
|
|
2236
2427
|
// src/claude/verify-hooks-contract.ts
|
|
@@ -2345,7 +2536,7 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
2345
2536
|
}
|
|
2346
2537
|
|
|
2347
2538
|
// src/claude/write-decisions-seed.ts
|
|
2348
|
-
import
|
|
2539
|
+
import path15 from "node:path";
|
|
2349
2540
|
import fs12 from "fs-extra";
|
|
2350
2541
|
|
|
2351
2542
|
// library/catalog/decisions-triggers.json
|
|
@@ -2389,8 +2580,8 @@ Why we chose X \u2014 short, dated, write-once decision log for humans and agent
|
|
|
2389
2580
|
| --- | ----- | -------------- | ------ |
|
|
2390
2581
|
`;
|
|
2391
2582
|
async function writeDecisionsSeed(root, dryRun) {
|
|
2392
|
-
const decisionsDir =
|
|
2393
|
-
const readmePath =
|
|
2583
|
+
const decisionsDir = path15.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
2584
|
+
const readmePath = path15.join(decisionsDir, "README.md");
|
|
2394
2585
|
if (await fs12.pathExists(readmePath)) return null;
|
|
2395
2586
|
if (!dryRun) await fs12.ensureDir(decisionsDir);
|
|
2396
2587
|
await writeManagedText(root, readmePath, SEED_README, dryRun);
|
|
@@ -2398,7 +2589,7 @@ async function writeDecisionsSeed(root, dryRun) {
|
|
|
2398
2589
|
}
|
|
2399
2590
|
|
|
2400
2591
|
// src/claude/write-prettierignore.ts
|
|
2401
|
-
import
|
|
2592
|
+
import path16 from "node:path";
|
|
2402
2593
|
import fs13 from "fs-extra";
|
|
2403
2594
|
var PRETTIERIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
2404
2595
|
var PRETTIERIGNORE_END = "# HAUS:END haus-managed";
|
|
@@ -2467,7 +2658,7 @@ ${block}
|
|
|
2467
2658
|
`;
|
|
2468
2659
|
}
|
|
2469
2660
|
async function writePrettierIgnore(root, dryRun) {
|
|
2470
|
-
const filePath =
|
|
2661
|
+
const filePath = path16.join(root, ".prettierignore");
|
|
2471
2662
|
const prev = await fs13.pathExists(filePath) ? await fs13.readFile(filePath, "utf8") : "";
|
|
2472
2663
|
const next = injectPrettierIgnoreBlock(prev, buildPrettierIgnoreBlock());
|
|
2473
2664
|
await writeManagedText(root, filePath, next, dryRun);
|
|
@@ -2475,7 +2666,7 @@ async function writePrettierIgnore(root, dryRun) {
|
|
|
2475
2666
|
}
|
|
2476
2667
|
|
|
2477
2668
|
// src/claude/write-root-claude-md.ts
|
|
2478
|
-
import
|
|
2669
|
+
import path17 from "node:path";
|
|
2479
2670
|
import fs14 from "fs-extra";
|
|
2480
2671
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
2481
2672
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
@@ -2553,7 +2744,7 @@ ${block}
|
|
|
2553
2744
|
`;
|
|
2554
2745
|
}
|
|
2555
2746
|
async function writeRootClaudeMd(root, dryRun) {
|
|
2556
|
-
const filePath =
|
|
2747
|
+
const filePath = path17.join(root, "CLAUDE.md");
|
|
2557
2748
|
const block = buildImportBlock();
|
|
2558
2749
|
const prev = await fs14.pathExists(filePath) ? await fs14.readFile(filePath, "utf8") : "";
|
|
2559
2750
|
const next = injectHausBlock(prev, block);
|
|
@@ -2562,11 +2753,11 @@ async function writeRootClaudeMd(root, dryRun) {
|
|
|
2562
2753
|
}
|
|
2563
2754
|
|
|
2564
2755
|
// src/claude/write-workflow-config.ts
|
|
2565
|
-
import
|
|
2756
|
+
import path19 from "node:path";
|
|
2566
2757
|
import fs16 from "fs-extra";
|
|
2567
2758
|
|
|
2568
2759
|
// src/claude/derive-workflow-config.ts
|
|
2569
|
-
import
|
|
2760
|
+
import path18 from "node:path";
|
|
2570
2761
|
import fs15 from "fs-extra";
|
|
2571
2762
|
function binCmd(pm, bin, args) {
|
|
2572
2763
|
const tail = args ? ` ${args}` : "";
|
|
@@ -2576,7 +2767,7 @@ function binCmd(pm, bin, args) {
|
|
|
2576
2767
|
}
|
|
2577
2768
|
async function deriveWorkflowConfig(root, ctx) {
|
|
2578
2769
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
2579
|
-
const pkg = await readJson(
|
|
2770
|
+
const pkg = await readJson(path18.join(root, "package.json"));
|
|
2580
2771
|
const scripts = pkg?.scripts ?? {};
|
|
2581
2772
|
const deps = new Set(ctx.dependencies);
|
|
2582
2773
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -2586,7 +2777,7 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
2586
2777
|
return null;
|
|
2587
2778
|
};
|
|
2588
2779
|
const hasDep = (name) => deps.has(name);
|
|
2589
|
-
const exists = (rel) => fs15.pathExistsSync(
|
|
2780
|
+
const exists = (rel) => fs15.pathExistsSync(path18.join(root, rel));
|
|
2590
2781
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
2591
2782
|
const hasCypress = hasDep("cypress");
|
|
2592
2783
|
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 +2852,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
2661
2852
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
2662
2853
|
...FALLBACK_CONTEXT,
|
|
2663
2854
|
root,
|
|
2664
|
-
repoName:
|
|
2855
|
+
repoName: path19.basename(root)
|
|
2665
2856
|
};
|
|
2666
2857
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
2667
2858
|
if (exists) {
|
|
@@ -2706,7 +2897,8 @@ function checkManagedTamper(body, storedHash, templateContent) {
|
|
|
2706
2897
|
// src/claude/write-workflow.ts
|
|
2707
2898
|
var STABLE_ID = "template.workflow";
|
|
2708
2899
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
2709
|
-
|
|
2900
|
+
const hashAttr = contentHash === void 0 ? "" : ` hash=${contentHash}`;
|
|
2901
|
+
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion}${hashAttr} -->`;
|
|
2710
2902
|
}
|
|
2711
2903
|
async function writeWorkflow(root, pkgVersion, dryRun, force = false) {
|
|
2712
2904
|
const templateContent = await readWorkflowTemplate({ dryRun });
|
|
@@ -2747,12 +2939,12 @@ ${templateContent}`;
|
|
|
2747
2939
|
return null;
|
|
2748
2940
|
}
|
|
2749
2941
|
if (verdict.status === "legacy-diverged" && !force) {
|
|
2750
|
-
const migratedHeader = makeWorkflowHeader(pkgVersion
|
|
2942
|
+
const migratedHeader = makeWorkflowHeader(pkgVersion);
|
|
2751
2943
|
const migrated = `${migratedHeader}
|
|
2752
2944
|
${existingContent}`;
|
|
2753
2945
|
if (hasTextChanged(existing, migrated)) {
|
|
2754
2946
|
if (dryRun) {
|
|
2755
|
-
log(`${printable}: migrating legacy
|
|
2947
|
+
log(`${printable}: migrating legacy header (staying hash-less; body preserved)`);
|
|
2756
2948
|
} else {
|
|
2757
2949
|
await writeText(destPath, migrated);
|
|
2758
2950
|
}
|
|
@@ -2799,7 +2991,7 @@ async function listFilesRecursive2(dir) {
|
|
|
2799
2991
|
const walk = async (current, relPrefix) => {
|
|
2800
2992
|
const names = await fs18.readdir(current);
|
|
2801
2993
|
for (const name of names) {
|
|
2802
|
-
const abs =
|
|
2994
|
+
const abs = path20.join(current, name);
|
|
2803
2995
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
2804
2996
|
const stat = await fs18.lstat(abs);
|
|
2805
2997
|
if (stat.isSymbolicLink()) {
|
|
@@ -2843,8 +3035,8 @@ async function logSkillDryRunDiff(root, sourcePath, destination, itemId, say) {
|
|
|
2843
3035
|
for (const rel of sourceFiles) {
|
|
2844
3036
|
await logFileDryRunDiff(
|
|
2845
3037
|
root,
|
|
2846
|
-
|
|
2847
|
-
|
|
3038
|
+
path20.join(sourcePath, rel),
|
|
3039
|
+
path20.join(destination, rel),
|
|
2848
3040
|
itemId,
|
|
2849
3041
|
say
|
|
2850
3042
|
);
|
|
@@ -2852,7 +3044,7 @@ async function logSkillDryRunDiff(root, sourcePath, destination, itemId, say) {
|
|
|
2852
3044
|
const sourceFileSet = new Set(sourceFiles);
|
|
2853
3045
|
for (const rel of destFiles) {
|
|
2854
3046
|
if (!sourceFileSet.has(rel)) {
|
|
2855
|
-
say(`${displayPath(root,
|
|
3047
|
+
say(`${displayPath(root, path20.join(destination, rel))}: would remove (${itemId})`);
|
|
2856
3048
|
}
|
|
2857
3049
|
}
|
|
2858
3050
|
}
|
|
@@ -2872,7 +3064,9 @@ async function removeLegacyManagedStub(root, relPathSegments, stub, dryRun, say)
|
|
|
2872
3064
|
async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
2873
3065
|
const say = opts.quiet ? () => {
|
|
2874
3066
|
} : log;
|
|
2875
|
-
const
|
|
3067
|
+
const recResult = await readJsonDetailed(hausPath(root, "recommendation.json"));
|
|
3068
|
+
const recommendationUnusable = recResult.status !== "ok";
|
|
3069
|
+
const rec = recResult.status === "ok" ? recResult.value : {
|
|
2876
3070
|
recommended: [],
|
|
2877
3071
|
skipped: [],
|
|
2878
3072
|
warnings: [],
|
|
@@ -2882,7 +3076,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2882
3076
|
estimatedTokenReductionPct: 0
|
|
2883
3077
|
};
|
|
2884
3078
|
const pkgRoot = packageRoot();
|
|
2885
|
-
const hausVersion2 = (await readJson(
|
|
3079
|
+
const hausVersion2 = (await readJson(path20.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
2886
3080
|
const coreFiles = [claudePath(root, "settings.json"), claudePath(root, "rules", "haus.md")];
|
|
2887
3081
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
2888
3082
|
const decisionsSeedPath = await writeDecisionsSeed(root, dryRun);
|
|
@@ -2972,6 +3166,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2972
3166
|
const { items: manifestItems, contentRoot } = await loadCatalogContext(root);
|
|
2973
3167
|
const manifestById = new Map(manifestItems.map((item) => [item.id, item]));
|
|
2974
3168
|
const prevLock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3169
|
+
const prevLockById = new Map(
|
|
3170
|
+
prevLock.filter((entry) => Boolean(entry.id)).map((entry) => [entry.id, entry])
|
|
3171
|
+
);
|
|
2975
3172
|
const allMigrations = findFormerIdMigrations(
|
|
2976
3173
|
prevLock.filter((entry) => Boolean(entry.id)),
|
|
2977
3174
|
manifestItems
|
|
@@ -3032,6 +3229,17 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3032
3229
|
warn(`Skipping ${item.id}: reviewStatus is deprecated`);
|
|
3033
3230
|
continue;
|
|
3034
3231
|
}
|
|
3232
|
+
const manifestItemTags = (manifestItem.tags ?? []).map((t) => t.toLowerCase());
|
|
3233
|
+
if (UNSUPPORTED.some((x) => manifestItemTags.includes(x))) {
|
|
3234
|
+
warn(`Skipping ${item.id}: unsupported stack policy`);
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
const sensitiveInId = SENSITIVE_ITEM_KEYWORDS.some((x) => item.id.toLowerCase().includes(x));
|
|
3238
|
+
const sensitiveInTags = SENSITIVE_ITEM_KEYWORDS.some((x) => manifestItemTags.includes(x));
|
|
3239
|
+
if (sensitiveInId || sensitiveInTags) {
|
|
3240
|
+
warn(`Skipping ${item.id}: sensitive content policy block`);
|
|
3241
|
+
continue;
|
|
3242
|
+
}
|
|
3035
3243
|
if (manifestItem.source === "curated") {
|
|
3036
3244
|
if (manifestItem.reviewStatus !== "approved") {
|
|
3037
3245
|
curatedReviewStatusSkips++;
|
|
@@ -3056,7 +3264,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3056
3264
|
);
|
|
3057
3265
|
continue;
|
|
3058
3266
|
}
|
|
3059
|
-
const destination = claudePath(root, target,
|
|
3267
|
+
const destination = claudePath(root, target, path20.basename(sourcePath));
|
|
3060
3268
|
if (await fs18.pathExists(sourcePath)) {
|
|
3061
3269
|
if (dryRun) {
|
|
3062
3270
|
if (item.type === "skill") {
|
|
@@ -3064,13 +3272,23 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3064
3272
|
} else {
|
|
3065
3273
|
await logFileDryRunDiff(root, sourcePath, destination, item.id, say);
|
|
3066
3274
|
}
|
|
3275
|
+
} else if (await skipLocallyEditedItem(
|
|
3276
|
+
root,
|
|
3277
|
+
item.id,
|
|
3278
|
+
prevLockById.get(item.id),
|
|
3279
|
+
opts.force,
|
|
3280
|
+
files,
|
|
3281
|
+
installedPathsByItem,
|
|
3282
|
+
installedIds
|
|
3283
|
+
)) {
|
|
3284
|
+
continue;
|
|
3067
3285
|
} else if (item.type === "skill") {
|
|
3068
3286
|
const skillFiles = (await fs18.readdir(sourcePath, { recursive: true })).filter(
|
|
3069
3287
|
(f) => typeof f === "string" && f.endsWith(".md")
|
|
3070
3288
|
);
|
|
3071
3289
|
let skillValid = true;
|
|
3072
3290
|
for (const relFile of skillFiles) {
|
|
3073
|
-
const mdPath =
|
|
3291
|
+
const mdPath = path20.join(sourcePath, relFile);
|
|
3074
3292
|
const mdContent = await fs18.readFile(mdPath, "utf8");
|
|
3075
3293
|
const mdValidation = validateCatalogItem(
|
|
3076
3294
|
{
|
|
@@ -3109,17 +3327,17 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3109
3327
|
warn(`Skipping ${item.id}: pre-copy validation failed \u2014 ${validation.reason}`);
|
|
3110
3328
|
continue;
|
|
3111
3329
|
}
|
|
3112
|
-
await fs18.ensureDir(
|
|
3330
|
+
await fs18.ensureDir(path20.dirname(destination));
|
|
3113
3331
|
await fs18.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
3114
3332
|
}
|
|
3115
3333
|
files.push(destination);
|
|
3116
|
-
const relPaths = [
|
|
3334
|
+
const relPaths = [path20.relative(root, destination)];
|
|
3117
3335
|
if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
|
|
3118
3336
|
const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
|
|
3119
3337
|
if (sharedRel) {
|
|
3120
3338
|
superpowersSharedInstalled = true;
|
|
3121
3339
|
relPaths.push(sharedRel);
|
|
3122
|
-
files.push(
|
|
3340
|
+
files.push(path20.join(root, sharedRel));
|
|
3123
3341
|
}
|
|
3124
3342
|
}
|
|
3125
3343
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
@@ -3137,18 +3355,31 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3137
3355
|
);
|
|
3138
3356
|
}
|
|
3139
3357
|
await cleanupStaleCatalogItems(root, cleanupManifestById, dryRun, opts.quiet);
|
|
3358
|
+
const refuseDestructiveLockChange = recommendationUnusable && prevLock.length > 0 && !opts.force;
|
|
3140
3359
|
if (opts.prune) {
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3360
|
+
if (refuseDestructiveLockChange) {
|
|
3361
|
+
warn(
|
|
3362
|
+
"recommendation.json is missing or unreadable \u2014 refusing --prune (every installed item would look orphaned). Run `haus recommend` first, or pass --force to override."
|
|
3363
|
+
);
|
|
3364
|
+
} else {
|
|
3365
|
+
const notOrphaned = new Set(rec.recommended.map((r) => r.id));
|
|
3366
|
+
for (const migration of allMigrations) notOrphaned.add(migration.oldId);
|
|
3367
|
+
for (const entry of prevLock) {
|
|
3368
|
+
if (!entry.id) continue;
|
|
3369
|
+
const manifestItem = manifestById.get(entry.id);
|
|
3370
|
+
if (!manifestItem || manifestItem.reviewStatus === "deprecated") notOrphaned.add(entry.id);
|
|
3371
|
+
}
|
|
3372
|
+
await pruneOrphanedCatalogItems(root, prevLock, notOrphaned, dryRun, opts.quiet);
|
|
3147
3373
|
}
|
|
3148
|
-
await pruneOrphanedCatalogItems(root, prevLock, notOrphaned, dryRun, opts.quiet);
|
|
3149
3374
|
}
|
|
3150
3375
|
const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
|
|
3151
3376
|
if (dryRun) {
|
|
3377
|
+
if (refuseDestructiveLockChange) {
|
|
3378
|
+
say(
|
|
3379
|
+
"[dry-run] recommendation.json is missing or unreadable \u2014 haus.lock.json would be left unchanged. Run `haus recommend` first, or pass --force."
|
|
3380
|
+
);
|
|
3381
|
+
return [...new Set(files)];
|
|
3382
|
+
}
|
|
3152
3383
|
const prevIds = new Set(
|
|
3153
3384
|
prevLock.map((e) => e.id ? migrationByOldId.get(e.id) ?? e.id : void 0).filter((id) => Boolean(id))
|
|
3154
3385
|
);
|
|
@@ -3159,6 +3390,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3159
3390
|
for (const id of removed) say(`[dry-run] haus.lock.json would remove: ${id}`);
|
|
3160
3391
|
return [...new Set(files)];
|
|
3161
3392
|
}
|
|
3393
|
+
if (refuseDestructiveLockChange) {
|
|
3394
|
+
warn(
|
|
3395
|
+
"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."
|
|
3396
|
+
);
|
|
3397
|
+
return [...new Set(files)];
|
|
3398
|
+
}
|
|
3162
3399
|
const prevRefById = new Map(
|
|
3163
3400
|
prevLock.filter((e) => e.id && e.catalogRef).map((e) => [migrationByOldId.get(e.id) ?? e.id, e.catalogRef])
|
|
3164
3401
|
);
|
|
@@ -3192,6 +3429,26 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
3192
3429
|
await writeManagedJson(root, hausPath(root, "haus.lock.json"), lock, false);
|
|
3193
3430
|
return [...new Set(files)];
|
|
3194
3431
|
}
|
|
3432
|
+
async function skipLocallyEditedItem(root, itemId, prevEntry, force, files, installedPathsByItem, installedIds) {
|
|
3433
|
+
if (!prevEntry || prevEntry.hash === void 0 || force) return false;
|
|
3434
|
+
const existing = await resolveExistingTrackedPaths(root, itemId, prevEntry.paths ?? [], warn);
|
|
3435
|
+
if (existing.length === 0) return false;
|
|
3436
|
+
const currentHash = await hashInstalledPaths(
|
|
3437
|
+
root,
|
|
3438
|
+
existing.map((e) => e.rel)
|
|
3439
|
+
);
|
|
3440
|
+
if (currentHash === prevEntry.hash) return false;
|
|
3441
|
+
warn(
|
|
3442
|
+
`User edited ${itemId} (skipping reinstall): ${existing.map((e) => e.rel).join(", ")} \u2014 use --force to overwrite`
|
|
3443
|
+
);
|
|
3444
|
+
files.push(...existing.map((e) => e.abs));
|
|
3445
|
+
installedPathsByItem.set(
|
|
3446
|
+
itemId,
|
|
3447
|
+
existing.map((e) => e.rel)
|
|
3448
|
+
);
|
|
3449
|
+
installedIds.add(itemId);
|
|
3450
|
+
return true;
|
|
3451
|
+
}
|
|
3195
3452
|
async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, quiet) {
|
|
3196
3453
|
const say = quiet ? () => {
|
|
3197
3454
|
} : log;
|
|
@@ -3199,26 +3456,25 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
3199
3456
|
const entry = prevLock.find((candidate) => candidate.id === migration.oldId);
|
|
3200
3457
|
if (!entry) continue;
|
|
3201
3458
|
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
|
-
}
|
|
3459
|
+
const existing = await resolveExistingTrackedPaths(root, migration.oldId, relPaths, warn);
|
|
3206
3460
|
if (existing.length === 0) continue;
|
|
3207
3461
|
if (entry.hash === void 0) {
|
|
3208
3462
|
warn(
|
|
3209
|
-
`Former catalog item ${migration.oldId} has no lock hash \u2014 leaving old paths in place: ${existing.join(", ")}`
|
|
3463
|
+
`Former catalog item ${migration.oldId} has no lock hash \u2014 leaving old paths in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3210
3464
|
);
|
|
3211
3465
|
continue;
|
|
3212
3466
|
}
|
|
3213
|
-
const currentHash = await hashInstalledPaths(
|
|
3467
|
+
const currentHash = await hashInstalledPaths(
|
|
3468
|
+
root,
|
|
3469
|
+
existing.map((e) => e.rel)
|
|
3470
|
+
);
|
|
3214
3471
|
if (currentHash !== entry.hash) {
|
|
3215
3472
|
warn(
|
|
3216
|
-
`Former catalog item ${migration.oldId} was modified locally \u2014 leaving old paths in place: ${existing.join(", ")}`
|
|
3473
|
+
`Former catalog item ${migration.oldId} was modified locally \u2014 leaving old paths in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3217
3474
|
);
|
|
3218
3475
|
continue;
|
|
3219
3476
|
}
|
|
3220
|
-
for (const
|
|
3221
|
-
const abs = path19.join(root, rel);
|
|
3477
|
+
for (const { abs } of existing) {
|
|
3222
3478
|
if (dryRun) {
|
|
3223
3479
|
say(
|
|
3224
3480
|
`[dry-run] would remove renamed ${displayPath(root, abs)} (${migration.oldId} \u2192 ${migration.newId})`
|
|
@@ -3226,9 +3482,21 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
3226
3482
|
continue;
|
|
3227
3483
|
}
|
|
3228
3484
|
await fs18.remove(abs);
|
|
3229
|
-
await pruneEmptyDir(
|
|
3485
|
+
await pruneEmptyDir(path20.dirname(abs));
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn) {
|
|
3490
|
+
const existing = [];
|
|
3491
|
+
for (const rel of relPaths) {
|
|
3492
|
+
const abs = resolveContainedPath(root, rel);
|
|
3493
|
+
if (abs === null) {
|
|
3494
|
+
warnFn(`Refusing to touch ${itemId}'s recorded path outside the project root: ${rel}`);
|
|
3495
|
+
continue;
|
|
3230
3496
|
}
|
|
3497
|
+
if (await fs18.pathExists(abs)) existing.push({ rel, abs });
|
|
3231
3498
|
}
|
|
3499
|
+
return existing;
|
|
3232
3500
|
}
|
|
3233
3501
|
async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
3234
3502
|
const say = quiet ? () => {
|
|
@@ -3243,33 +3511,32 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
|
|
|
3243
3511
|
if (!removedFromManifest && !deprecated) continue;
|
|
3244
3512
|
const relPaths = entry.paths ?? [];
|
|
3245
3513
|
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
|
-
}
|
|
3514
|
+
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
3250
3515
|
if (existing.length === 0) continue;
|
|
3251
3516
|
const pruneReason = deprecated ? "deprecated" : "stale";
|
|
3252
3517
|
if (entry.hash === void 0) {
|
|
3253
3518
|
warn(
|
|
3254
|
-
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.join(", ")}`
|
|
3519
|
+
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3255
3520
|
);
|
|
3256
3521
|
continue;
|
|
3257
3522
|
}
|
|
3258
|
-
const currentHash = await hashInstalledPaths(
|
|
3523
|
+
const currentHash = await hashInstalledPaths(
|
|
3524
|
+
root,
|
|
3525
|
+
existing.map((e) => e.rel)
|
|
3526
|
+
);
|
|
3259
3527
|
if (currentHash !== entry.hash) {
|
|
3260
3528
|
warn(
|
|
3261
|
-
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.join(", ")}`
|
|
3529
|
+
`${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3262
3530
|
);
|
|
3263
3531
|
continue;
|
|
3264
3532
|
}
|
|
3265
|
-
for (const
|
|
3266
|
-
const abs = path19.join(root, rel);
|
|
3533
|
+
for (const { abs } of existing) {
|
|
3267
3534
|
if (dryRun) {
|
|
3268
3535
|
say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
3269
3536
|
continue;
|
|
3270
3537
|
}
|
|
3271
3538
|
await fs18.remove(abs);
|
|
3272
|
-
await pruneEmptyDir(
|
|
3539
|
+
await pruneEmptyDir(path20.dirname(abs));
|
|
3273
3540
|
say(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
3274
3541
|
}
|
|
3275
3542
|
}
|
|
@@ -3280,12 +3547,12 @@ async function backupBeforePrune(root, absPaths, say) {
|
|
|
3280
3547
|
const backupRoot = hausPath(root, "backups", `prune-${stamp}`);
|
|
3281
3548
|
for (const abs of absPaths) {
|
|
3282
3549
|
if (!await fs18.pathExists(abs)) continue;
|
|
3283
|
-
const rel =
|
|
3284
|
-
const backupPath =
|
|
3285
|
-
await fs18.ensureDir(
|
|
3550
|
+
const rel = path20.relative(root, abs);
|
|
3551
|
+
const backupPath = path20.join(backupRoot, rel);
|
|
3552
|
+
await fs18.ensureDir(path20.dirname(backupPath));
|
|
3286
3553
|
await fs18.copy(abs, backupPath);
|
|
3287
3554
|
}
|
|
3288
|
-
say(`Backed up ${absPaths.length} path(s) to ${
|
|
3555
|
+
say(`Backed up ${absPaths.length} path(s) to ${path20.relative(root, backupRoot)} before pruning.`);
|
|
3289
3556
|
}
|
|
3290
3557
|
async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun, quiet) {
|
|
3291
3558
|
const say = quiet ? () => {
|
|
@@ -3297,21 +3564,21 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
3297
3564
|
if (!entry.id) continue;
|
|
3298
3565
|
const relPaths = entry.paths ?? [];
|
|
3299
3566
|
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
|
-
}
|
|
3567
|
+
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
3304
3568
|
if (existing.length === 0) continue;
|
|
3305
3569
|
if (entry.hash === void 0) {
|
|
3306
3570
|
warn(
|
|
3307
|
-
`Orphaned catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.join(", ")}`
|
|
3571
|
+
`Orphaned catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3308
3572
|
);
|
|
3309
3573
|
continue;
|
|
3310
3574
|
}
|
|
3311
|
-
const currentHash = await hashInstalledPaths(
|
|
3575
|
+
const currentHash = await hashInstalledPaths(
|
|
3576
|
+
root,
|
|
3577
|
+
existing.map((e) => e.rel)
|
|
3578
|
+
);
|
|
3312
3579
|
if (currentHash !== entry.hash) {
|
|
3313
3580
|
warn(
|
|
3314
|
-
`Orphaned catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.join(", ")}`
|
|
3581
|
+
`Orphaned catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
|
|
3315
3582
|
);
|
|
3316
3583
|
continue;
|
|
3317
3584
|
}
|
|
@@ -3320,21 +3587,18 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
3320
3587
|
if (toRemove.length === 0) return;
|
|
3321
3588
|
if (dryRun) {
|
|
3322
3589
|
for (const { id, existing } of toRemove) {
|
|
3323
|
-
for (const
|
|
3324
|
-
say(`[dry-run] would prune orphaned ${displayPath(root,
|
|
3590
|
+
for (const { abs } of existing) {
|
|
3591
|
+
say(`[dry-run] would prune orphaned ${displayPath(root, abs)} (${id})`);
|
|
3325
3592
|
}
|
|
3326
3593
|
}
|
|
3327
3594
|
return;
|
|
3328
3595
|
}
|
|
3329
|
-
const allAbsPaths = toRemove.flatMap(
|
|
3330
|
-
({ existing }) => existing.map((rel) => path19.join(root, rel))
|
|
3331
|
-
);
|
|
3596
|
+
const allAbsPaths = toRemove.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
3332
3597
|
await backupBeforePrune(root, allAbsPaths, say);
|
|
3333
3598
|
for (const { id, existing } of toRemove) {
|
|
3334
|
-
for (const
|
|
3335
|
-
const abs = path19.join(root, rel);
|
|
3599
|
+
for (const { abs } of existing) {
|
|
3336
3600
|
await fs18.remove(abs);
|
|
3337
|
-
await pruneEmptyDir(
|
|
3601
|
+
await pruneEmptyDir(path20.dirname(abs));
|
|
3338
3602
|
say(`Pruned orphaned ${displayPath(root, abs)} (${id})`);
|
|
3339
3603
|
}
|
|
3340
3604
|
}
|
|
@@ -3342,7 +3606,7 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
3342
3606
|
|
|
3343
3607
|
// src/claude/write-gitignore.ts
|
|
3344
3608
|
init_exec();
|
|
3345
|
-
import
|
|
3609
|
+
import path21 from "node:path";
|
|
3346
3610
|
import fs19 from "fs-extra";
|
|
3347
3611
|
var GITIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
3348
3612
|
var GITIGNORE_END = "# HAUS:END haus-managed";
|
|
@@ -3457,14 +3721,14 @@ function injectLinkContextGitignoreBlock(existing) {
|
|
|
3457
3721
|
);
|
|
3458
3722
|
}
|
|
3459
3723
|
async function writeGitignore(root, dryRun) {
|
|
3460
|
-
const filePath =
|
|
3724
|
+
const filePath = path21.join(root, ".gitignore");
|
|
3461
3725
|
const prev = await fs19.pathExists(filePath) ? await fs19.readFile(filePath, "utf8") : "";
|
|
3462
3726
|
const next = injectGitignoreBlock(prev);
|
|
3463
3727
|
await writeManagedText(root, filePath, next, dryRun);
|
|
3464
3728
|
return filePath;
|
|
3465
3729
|
}
|
|
3466
3730
|
async function writeLinkContextGitignore(root, dryRun) {
|
|
3467
|
-
const filePath =
|
|
3731
|
+
const filePath = path21.join(root, ".gitignore");
|
|
3468
3732
|
const prev = await fs19.pathExists(filePath) ? await fs19.readFile(filePath, "utf8") : "";
|
|
3469
3733
|
const next = injectLinkContextGitignoreBlock(prev);
|
|
3470
3734
|
await writeManagedText(root, filePath, next, dryRun);
|
|
@@ -3506,15 +3770,15 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
3506
3770
|
}
|
|
3507
3771
|
|
|
3508
3772
|
// src/refs/fetch-refs.ts
|
|
3509
|
-
import
|
|
3773
|
+
import path23 from "node:path";
|
|
3510
3774
|
import fs21 from "fs-extra";
|
|
3511
3775
|
|
|
3512
3776
|
// src/refs/cache-meta.ts
|
|
3513
|
-
import
|
|
3777
|
+
import path22 from "node:path";
|
|
3514
3778
|
import fs20 from "fs-extra";
|
|
3515
3779
|
var META_FILENAME = "cache-meta.json";
|
|
3516
3780
|
async function readCacheMeta(cacheDir) {
|
|
3517
|
-
const metaPath =
|
|
3781
|
+
const metaPath = path22.join(cacheDir, META_FILENAME);
|
|
3518
3782
|
try {
|
|
3519
3783
|
const raw = await fs20.readFile(metaPath, "utf8");
|
|
3520
3784
|
return JSON.parse(raw);
|
|
@@ -3524,7 +3788,7 @@ async function readCacheMeta(cacheDir) {
|
|
|
3524
3788
|
}
|
|
3525
3789
|
async function writeCacheMeta(cacheDir, meta) {
|
|
3526
3790
|
await fs20.ensureDir(cacheDir);
|
|
3527
|
-
const metaPath =
|
|
3791
|
+
const metaPath = path22.join(cacheDir, META_FILENAME);
|
|
3528
3792
|
await fs20.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
3529
3793
|
}
|
|
3530
3794
|
|
|
@@ -3545,8 +3809,8 @@ function isLlmsTxtUrl(ref) {
|
|
|
3545
3809
|
async function fetchSingleRef(url, cacheDir, meta) {
|
|
3546
3810
|
const existing = meta[url];
|
|
3547
3811
|
const headers = {};
|
|
3548
|
-
const safeFile = existing?.file && existing.file ===
|
|
3549
|
-
const cachedFileExists = safeFile && await fs21.pathExists(
|
|
3812
|
+
const safeFile = existing?.file && existing.file === path23.basename(existing.file);
|
|
3813
|
+
const cachedFileExists = safeFile && await fs21.pathExists(path23.join(cacheDir, existing.file));
|
|
3550
3814
|
if (cachedFileExists) {
|
|
3551
3815
|
if (existing?.etag) headers["If-None-Match"] = existing.etag;
|
|
3552
3816
|
if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
|
|
@@ -3558,7 +3822,7 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
3558
3822
|
const text = await res.text();
|
|
3559
3823
|
const file = `${urlToSlug(url)}.md`;
|
|
3560
3824
|
await fs21.ensureDir(cacheDir);
|
|
3561
|
-
await fs21.writeFile(
|
|
3825
|
+
await fs21.writeFile(path23.join(cacheDir, file), text, "utf8");
|
|
3562
3826
|
meta[url] = {
|
|
3563
3827
|
url,
|
|
3564
3828
|
etag: res.headers.get("etag") ?? void 0,
|
|
@@ -3581,8 +3845,8 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
|
3581
3845
|
if (keepUrls.has(url)) continue;
|
|
3582
3846
|
delete meta[url];
|
|
3583
3847
|
removed++;
|
|
3584
|
-
const safeFile = typeof entry.file === "string" && entry.file ===
|
|
3585
|
-
if (safeFile) await fs21.remove(
|
|
3848
|
+
const safeFile = typeof entry.file === "string" && entry.file === path23.basename(entry.file);
|
|
3849
|
+
if (safeFile) await fs21.remove(path23.join(cacheDir, entry.file)).catch(() => {
|
|
3586
3850
|
});
|
|
3587
3851
|
}
|
|
3588
3852
|
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
@@ -3604,10 +3868,10 @@ async function fetchRefsForItems(items, cacheDir) {
|
|
|
3604
3868
|
const outcome = await fetchSingleRef(url, cacheDir, meta);
|
|
3605
3869
|
if (outcome.result === "fetched") {
|
|
3606
3870
|
summary.fetched++;
|
|
3607
|
-
summary.cachedFiles[url] =
|
|
3871
|
+
summary.cachedFiles[url] = path23.join(cacheDir, outcome.file);
|
|
3608
3872
|
} else if (outcome.result === "unchanged") {
|
|
3609
3873
|
summary.unchanged++;
|
|
3610
|
-
if (meta[url]?.file) summary.cachedFiles[url] =
|
|
3874
|
+
if (meta[url]?.file) summary.cachedFiles[url] = path23.join(cacheDir, meta[url].file);
|
|
3611
3875
|
} else {
|
|
3612
3876
|
summary.failed++;
|
|
3613
3877
|
summary.failedUrls.push(url);
|
|
@@ -3630,7 +3894,7 @@ function parseIdList(value) {
|
|
|
3630
3894
|
// src/commands/apply.ts
|
|
3631
3895
|
var STALE_CACHE_THRESHOLD_DAYS = 7;
|
|
3632
3896
|
async function cacheHasItems() {
|
|
3633
|
-
const data = await readJson(
|
|
3897
|
+
const data = await readJson(path24.join(getCacheDir(), "manifest.json"));
|
|
3634
3898
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
3635
3899
|
}
|
|
3636
3900
|
function installableRecommendedItems(recommended) {
|
|
@@ -3781,7 +4045,7 @@ async function runApply(options) {
|
|
|
3781
4045
|
}
|
|
3782
4046
|
|
|
3783
4047
|
// src/commands/backups.ts
|
|
3784
|
-
import
|
|
4048
|
+
import path25 from "node:path";
|
|
3785
4049
|
import fs22 from "fs-extra";
|
|
3786
4050
|
|
|
3787
4051
|
// src/utils/prompts.ts
|
|
@@ -3818,7 +4082,7 @@ async function listBackups(root) {
|
|
|
3818
4082
|
for (const name of names) {
|
|
3819
4083
|
const kind = classify(name);
|
|
3820
4084
|
if (!kind) continue;
|
|
3821
|
-
const absPath =
|
|
4085
|
+
const absPath = path25.join(dir, name);
|
|
3822
4086
|
const stat = await fs22.lstat(absPath);
|
|
3823
4087
|
if (stat.isSymbolicLink()) continue;
|
|
3824
4088
|
const expectDirectory = kind !== "lock";
|
|
@@ -3848,10 +4112,10 @@ async function runList(root) {
|
|
|
3848
4112
|
}
|
|
3849
4113
|
}
|
|
3850
4114
|
async function isUnsafeWriteTarget(root, target) {
|
|
3851
|
-
const segments =
|
|
4115
|
+
const segments = path25.relative(root, target).split(path25.sep).filter(Boolean);
|
|
3852
4116
|
let current = root;
|
|
3853
4117
|
for (let i = 0; i < segments.length; i++) {
|
|
3854
|
-
current =
|
|
4118
|
+
current = path25.join(current, segments[i]);
|
|
3855
4119
|
const isLastSegment = i === segments.length - 1;
|
|
3856
4120
|
try {
|
|
3857
4121
|
const stat = await fs22.lstat(current);
|
|
@@ -3915,7 +4179,7 @@ async function collectFilesRecursive(dir) {
|
|
|
3915
4179
|
const skippedOther = [];
|
|
3916
4180
|
const names = await fs22.readdir(dir);
|
|
3917
4181
|
for (const name of names) {
|
|
3918
|
-
const abs =
|
|
4182
|
+
const abs = path25.join(dir, name);
|
|
3919
4183
|
const stat = await fs22.lstat(abs);
|
|
3920
4184
|
if (stat.isSymbolicLink()) {
|
|
3921
4185
|
skippedLinks.push(abs);
|
|
@@ -3938,12 +4202,12 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
3938
4202
|
const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
|
|
3939
4203
|
if (skippedLinks.length > 0) {
|
|
3940
4204
|
warn(
|
|
3941
|
-
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) =>
|
|
4205
|
+
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) => path25.relative(entry.absPath, abs)).join(", ")}`
|
|
3942
4206
|
);
|
|
3943
4207
|
}
|
|
3944
4208
|
if (skippedOther.length > 0) {
|
|
3945
4209
|
warn(
|
|
3946
|
-
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) =>
|
|
4210
|
+
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) => path25.relative(entry.absPath, abs)).join(", ")}`
|
|
3947
4211
|
);
|
|
3948
4212
|
}
|
|
3949
4213
|
if (files.length === 0) {
|
|
@@ -3953,8 +4217,8 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
3953
4217
|
const safeFiles = [];
|
|
3954
4218
|
const unsafeTargets = [];
|
|
3955
4219
|
for (const abs of files) {
|
|
3956
|
-
const rel =
|
|
3957
|
-
const target =
|
|
4220
|
+
const rel = path25.relative(entry.absPath, abs);
|
|
4221
|
+
const target = path25.join(root, rel);
|
|
3958
4222
|
if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
|
|
3959
4223
|
else safeFiles.push(abs);
|
|
3960
4224
|
}
|
|
@@ -3971,14 +4235,14 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
3971
4235
|
const vanished = [];
|
|
3972
4236
|
const toRestore = [];
|
|
3973
4237
|
for (const abs of safeFiles) {
|
|
3974
|
-
const rel =
|
|
4238
|
+
const rel = path25.relative(entry.absPath, abs);
|
|
3975
4239
|
const backupFileStat = await lstatIfRegularFile(abs);
|
|
3976
4240
|
if (!backupFileStat) {
|
|
3977
4241
|
vanished.push(rel);
|
|
3978
4242
|
continue;
|
|
3979
4243
|
}
|
|
3980
4244
|
toRestore.push(abs);
|
|
3981
|
-
const target =
|
|
4245
|
+
const target = path25.join(root, rel);
|
|
3982
4246
|
if (await fs22.pathExists(target)) {
|
|
3983
4247
|
const targetStat = await fs22.stat(target);
|
|
3984
4248
|
if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
|
|
@@ -4019,13 +4283,13 @@ async function restoreDirBackup(root, entry, yes) {
|
|
|
4019
4283
|
let restoredCount = 0;
|
|
4020
4284
|
const skippedAtWrite = [];
|
|
4021
4285
|
for (const abs of toRestore) {
|
|
4022
|
-
const rel =
|
|
4023
|
-
const target =
|
|
4286
|
+
const rel = path25.relative(entry.absPath, abs);
|
|
4287
|
+
const target = path25.join(root, rel);
|
|
4024
4288
|
if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
|
|
4025
4289
|
skippedAtWrite.push(rel);
|
|
4026
4290
|
continue;
|
|
4027
4291
|
}
|
|
4028
|
-
await fs22.ensureDir(
|
|
4292
|
+
await fs22.ensureDir(path25.dirname(target));
|
|
4029
4293
|
await fs22.copy(abs, target, { overwrite: true });
|
|
4030
4294
|
restoredCount++;
|
|
4031
4295
|
}
|
|
@@ -4164,16 +4428,53 @@ async function runCatalogAudit() {
|
|
|
4164
4428
|
}
|
|
4165
4429
|
|
|
4166
4430
|
// src/commands/doctor.ts
|
|
4167
|
-
import
|
|
4168
|
-
import
|
|
4431
|
+
import path39 from "node:path";
|
|
4432
|
+
import fs30 from "fs-extra";
|
|
4433
|
+
|
|
4434
|
+
// src/claude/skill-reference-check.ts
|
|
4435
|
+
import path26 from "node:path";
|
|
4436
|
+
import fg4 from "fast-glob";
|
|
4437
|
+
import fs23 from "fs-extra";
|
|
4438
|
+
var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
|
|
4439
|
+
function stripFencedCodeBlocks(text) {
|
|
4440
|
+
return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
|
|
4441
|
+
}
|
|
4442
|
+
async function findUnresolvedSkillReferences(root) {
|
|
4443
|
+
const skillsRoot = claudePath(root, "skills");
|
|
4444
|
+
if (!await fs23.pathExists(skillsRoot)) return [];
|
|
4445
|
+
const skillDirs = (await fs23.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
|
|
4446
|
+
const installedSkillNames = new Set(skillDirs);
|
|
4447
|
+
const files = await fg4("*/**/*.md", { cwd: skillsRoot, onlyFiles: true, dot: true });
|
|
4448
|
+
const issues = [];
|
|
4449
|
+
for (const rel of files) {
|
|
4450
|
+
if (rel.startsWith("shared/")) continue;
|
|
4451
|
+
const abs = path26.join(skillsRoot, rel);
|
|
4452
|
+
const raw = await fs23.readFile(abs, "utf8");
|
|
4453
|
+
const lines = stripFencedCodeBlocks(raw).split("\n");
|
|
4454
|
+
lines.forEach((line2, idx) => {
|
|
4455
|
+
for (const match of line2.matchAll(REFERENCE_PATTERN)) {
|
|
4456
|
+
const [reference, , bareName] = match;
|
|
4457
|
+
if (!reference || !bareName) continue;
|
|
4458
|
+
issues.push({
|
|
4459
|
+
file: path26.join(".claude/skills", rel),
|
|
4460
|
+
line: idx + 1,
|
|
4461
|
+
reference,
|
|
4462
|
+
bareName,
|
|
4463
|
+
bareInstalled: installedSkillNames.has(bareName)
|
|
4464
|
+
});
|
|
4465
|
+
}
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
return issues;
|
|
4469
|
+
}
|
|
4169
4470
|
|
|
4170
4471
|
// src/decisions/doctor.ts
|
|
4171
|
-
import
|
|
4172
|
-
import
|
|
4472
|
+
import path31 from "node:path";
|
|
4473
|
+
import fs27 from "fs-extra";
|
|
4173
4474
|
|
|
4174
4475
|
// src/decisions/check.ts
|
|
4175
|
-
import
|
|
4176
|
-
import
|
|
4476
|
+
import path30 from "node:path";
|
|
4477
|
+
import fs26 from "fs-extra";
|
|
4177
4478
|
|
|
4178
4479
|
// src/decisions/dep-bump.ts
|
|
4179
4480
|
var DEP_SECTIONS = [
|
|
@@ -4182,15 +4483,15 @@ var DEP_SECTIONS = [
|
|
|
4182
4483
|
"peerDependencies",
|
|
4183
4484
|
"optionalDependencies"
|
|
4184
4485
|
];
|
|
4185
|
-
function
|
|
4486
|
+
function isRecord2(value) {
|
|
4186
4487
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
4187
4488
|
}
|
|
4188
4489
|
function extractDepMap(pkg) {
|
|
4189
4490
|
const map = /* @__PURE__ */ new Map();
|
|
4190
|
-
if (!
|
|
4491
|
+
if (!isRecord2(pkg)) return map;
|
|
4191
4492
|
for (const section of DEP_SECTIONS) {
|
|
4192
4493
|
const block = pkg[section];
|
|
4193
|
-
if (!
|
|
4494
|
+
if (!isRecord2(block)) continue;
|
|
4194
4495
|
for (const [name, ver] of Object.entries(block)) {
|
|
4195
4496
|
if (typeof ver === "string") map.set(`${section}:${name}`, ver);
|
|
4196
4497
|
}
|
|
@@ -4216,7 +4517,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
4216
4517
|
} catch {
|
|
4217
4518
|
return false;
|
|
4218
4519
|
}
|
|
4219
|
-
if (!
|
|
4520
|
+
if (!isRecord2(before) || !isRecord2(after)) return false;
|
|
4220
4521
|
const beforeDeps = extractDepMap(before);
|
|
4221
4522
|
const afterDeps = extractDepMap(after);
|
|
4222
4523
|
if (beforeDeps.size === 0 && afterDeps.size === 0) return false;
|
|
@@ -4232,7 +4533,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
|
|
|
4232
4533
|
|
|
4233
4534
|
// src/decisions/diff.ts
|
|
4234
4535
|
init_exec();
|
|
4235
|
-
import
|
|
4536
|
+
import path27 from "node:path";
|
|
4236
4537
|
function isPackageJsonPath(file) {
|
|
4237
4538
|
return file === "package.json" || file.endsWith("/package.json");
|
|
4238
4539
|
}
|
|
@@ -4259,6 +4560,11 @@ function parseDiffRange(range) {
|
|
|
4259
4560
|
}
|
|
4260
4561
|
return null;
|
|
4261
4562
|
}
|
|
4563
|
+
function toAncestryLogRange(range) {
|
|
4564
|
+
const parsed = parseDiffRange(range);
|
|
4565
|
+
if (!parsed) return `${range}..HEAD`;
|
|
4566
|
+
return `${parsed.left}..${parsed.right}`;
|
|
4567
|
+
}
|
|
4262
4568
|
async function getCurrentBranch(root) {
|
|
4263
4569
|
const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root });
|
|
4264
4570
|
if (result.exitCode !== 0) return void 0;
|
|
@@ -4288,7 +4594,7 @@ async function resolvePackageJsonPair(root, file, opts) {
|
|
|
4288
4594
|
return { before: before2, after: after2 };
|
|
4289
4595
|
}
|
|
4290
4596
|
const before = await readBlob(root, `${range}:${file}`) ?? empty;
|
|
4291
|
-
const after = await readText(
|
|
4597
|
+
const after = await readText(path27.join(root, file)) ?? empty;
|
|
4292
4598
|
return { before, after };
|
|
4293
4599
|
}
|
|
4294
4600
|
async function collectDiffStats(root, opts) {
|
|
@@ -4359,22 +4665,22 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
4359
4665
|
}
|
|
4360
4666
|
|
|
4361
4667
|
// src/decisions/paths.ts
|
|
4362
|
-
import
|
|
4363
|
-
import
|
|
4668
|
+
import path28 from "node:path";
|
|
4669
|
+
import fs24 from "fs-extra";
|
|
4364
4670
|
async function resolveDecisionsDir(root) {
|
|
4365
|
-
const primary =
|
|
4366
|
-
if (await
|
|
4367
|
-
const legacy =
|
|
4368
|
-
if (await
|
|
4671
|
+
const primary = path28.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4672
|
+
if (await fs24.pathExists(primary)) return primary;
|
|
4673
|
+
const legacy = path28.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4674
|
+
if (await fs24.pathExists(legacy)) return legacy;
|
|
4369
4675
|
return primary;
|
|
4370
4676
|
}
|
|
4371
4677
|
function relativeDecisionPath(decisionsDir, root) {
|
|
4372
|
-
return
|
|
4678
|
+
return path28.relative(root, decisionsDir).replace(/\\/g, "/");
|
|
4373
4679
|
}
|
|
4374
4680
|
|
|
4375
4681
|
// src/decisions/validate.ts
|
|
4376
|
-
import
|
|
4377
|
-
import
|
|
4682
|
+
import path29 from "node:path";
|
|
4683
|
+
import fs25 from "fs-extra";
|
|
4378
4684
|
function validateDecisionContent(content2) {
|
|
4379
4685
|
const errors = [];
|
|
4380
4686
|
for (const section of DECISIONS_TRIGGERS.requiredSections) {
|
|
@@ -4403,14 +4709,14 @@ function decisionNumberFromFile(name) {
|
|
|
4403
4709
|
return match?.[1] ?? null;
|
|
4404
4710
|
}
|
|
4405
4711
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
4406
|
-
const readmePath =
|
|
4407
|
-
if (!await
|
|
4712
|
+
const readmePath = path29.join(decisionsDir, "README.md");
|
|
4713
|
+
if (!await fs25.pathExists(readmePath)) {
|
|
4408
4714
|
return ["docs/decisions/README.md is missing"];
|
|
4409
4715
|
}
|
|
4410
4716
|
const readme = await readText(readmePath) ?? "";
|
|
4411
4717
|
const errors = [];
|
|
4412
4718
|
for (const file of decisionFiles) {
|
|
4413
|
-
const num = decisionNumberFromFile(
|
|
4719
|
+
const num = decisionNumberFromFile(path29.basename(file));
|
|
4414
4720
|
if (num && !indexReferencesNumber(readme, num)) {
|
|
4415
4721
|
errors.push(`README.md missing index row for ADR-${num}`);
|
|
4416
4722
|
}
|
|
@@ -4505,22 +4811,22 @@ async function checkSatisfaction(input2) {
|
|
|
4505
4811
|
const errors = [];
|
|
4506
4812
|
const decisionsPrefix = `${input2.decisionsRel}/`;
|
|
4507
4813
|
const newDecisionFiles = input2.changedFiles.filter(
|
|
4508
|
-
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(
|
|
4814
|
+
(f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path30.basename(f))
|
|
4509
4815
|
);
|
|
4510
4816
|
if (newDecisionFiles.length === 0) {
|
|
4511
4817
|
errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
|
|
4512
4818
|
return errors;
|
|
4513
4819
|
}
|
|
4514
|
-
const decisionsDir =
|
|
4820
|
+
const decisionsDir = path30.join(input2.root, input2.decisionsRel);
|
|
4515
4821
|
for (const rel of newDecisionFiles) {
|
|
4516
|
-
const abs =
|
|
4822
|
+
const abs = path30.join(input2.root, rel);
|
|
4517
4823
|
const content2 = await readText(abs) ?? "";
|
|
4518
4824
|
const valid = validateDecisionContent(content2);
|
|
4519
4825
|
if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
|
|
4520
4826
|
}
|
|
4521
4827
|
const indexErrors = await validateIndexSync(
|
|
4522
4828
|
decisionsDir,
|
|
4523
|
-
newDecisionFiles.map((f) =>
|
|
4829
|
+
newDecisionFiles.map((f) => path30.basename(f))
|
|
4524
4830
|
);
|
|
4525
4831
|
errors.push(...indexErrors);
|
|
4526
4832
|
return errors;
|
|
@@ -4576,8 +4882,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
4576
4882
|
changedFiles: stats.files
|
|
4577
4883
|
});
|
|
4578
4884
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
4579
|
-
const legacyExists = await
|
|
4580
|
-
if (legacyExists && !await
|
|
4885
|
+
const legacyExists = await fs26.pathExists(path30.join(root, legacyRel));
|
|
4886
|
+
if (legacyExists && !await fs26.pathExists(decisionsDir)) {
|
|
4581
4887
|
satisfactionErrors = await checkSatisfaction({
|
|
4582
4888
|
root,
|
|
4583
4889
|
decisionsRel: legacyRel,
|
|
@@ -4599,18 +4905,18 @@ ${opts.commitMessages ?? ""}`;
|
|
|
4599
4905
|
return { triggered: true, satisfied: false, reasons, securityTriggered };
|
|
4600
4906
|
}
|
|
4601
4907
|
async function listDecisionNumbers(decisionsDir) {
|
|
4602
|
-
if (!await
|
|
4603
|
-
const entries = await
|
|
4908
|
+
if (!await fs26.pathExists(decisionsDir)) return [];
|
|
4909
|
+
const entries = await fs26.readdir(decisionsDir);
|
|
4604
4910
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
4605
4911
|
}
|
|
4606
4912
|
|
|
4607
4913
|
// src/decisions/doctor.ts
|
|
4608
4914
|
async function auditDecisionsLayout(root) {
|
|
4609
4915
|
const findings = [];
|
|
4610
|
-
const primary =
|
|
4611
|
-
const legacy =
|
|
4612
|
-
const hasPrimary = await
|
|
4613
|
-
const hasLegacy = await
|
|
4916
|
+
const primary = path31.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
4917
|
+
const legacy = path31.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
4918
|
+
const hasPrimary = await fs27.pathExists(primary);
|
|
4919
|
+
const hasLegacy = await fs27.pathExists(legacy);
|
|
4614
4920
|
if (!hasPrimary && hasLegacy) {
|
|
4615
4921
|
findings.push({
|
|
4616
4922
|
level: "advisory",
|
|
@@ -4623,8 +4929,8 @@ async function auditDecisionsLayout(root) {
|
|
|
4623
4929
|
if (!hasPrimary) return findings;
|
|
4624
4930
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
4625
4931
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
4626
|
-
const readmePath =
|
|
4627
|
-
if (!await
|
|
4932
|
+
const readmePath = path31.join(decisionsDir, "README.md");
|
|
4933
|
+
if (!await fs27.pathExists(readmePath)) {
|
|
4628
4934
|
findings.push({
|
|
4629
4935
|
level: "advisory",
|
|
4630
4936
|
line: `- DECISIONS: missing ${rel}/README.md index`,
|
|
@@ -4634,7 +4940,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4634
4940
|
return findings;
|
|
4635
4941
|
}
|
|
4636
4942
|
const readme = await readText(readmePath);
|
|
4637
|
-
const entries = (await
|
|
4943
|
+
const entries = (await fs27.readdir(decisionsDir)).filter(
|
|
4638
4944
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
4639
4945
|
);
|
|
4640
4946
|
for (const file of entries) {
|
|
@@ -4648,7 +4954,7 @@ async function auditDecisionsLayout(root) {
|
|
|
4648
4954
|
});
|
|
4649
4955
|
}
|
|
4650
4956
|
}
|
|
4651
|
-
const claude = await readText(
|
|
4957
|
+
const claude = await readText(path31.join(root, "CLAUDE.md"));
|
|
4652
4958
|
if (claude && !claude.includes(`@${rel}/README.md`)) {
|
|
4653
4959
|
findings.push({
|
|
4654
4960
|
level: "advisory",
|
|
@@ -4679,11 +4985,11 @@ async function auditDecisionsLayout(root) {
|
|
|
4679
4985
|
}
|
|
4680
4986
|
|
|
4681
4987
|
// src/scanner/read-context.ts
|
|
4682
|
-
import
|
|
4683
|
-
import
|
|
4988
|
+
import path38 from "node:path";
|
|
4989
|
+
import fs29 from "fs-extra";
|
|
4684
4990
|
|
|
4685
4991
|
// src/commands/workspace/config.ts
|
|
4686
|
-
import
|
|
4992
|
+
import path32 from "node:path";
|
|
4687
4993
|
import YAML from "yaml";
|
|
4688
4994
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4689
4995
|
function parseWorkspaceConfig(text) {
|
|
@@ -4706,11 +5012,11 @@ function parseWorkspaceConfig(text) {
|
|
|
4706
5012
|
};
|
|
4707
5013
|
}
|
|
4708
5014
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4709
|
-
return parseWorkspaceConfig(await readText(
|
|
5015
|
+
return parseWorkspaceConfig(await readText(path32.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4710
5016
|
}
|
|
4711
5017
|
|
|
4712
5018
|
// src/workspace/members.ts
|
|
4713
|
-
import
|
|
5019
|
+
import path33 from "node:path";
|
|
4714
5020
|
var REPOS_MANIFEST_FILE = "repos.manifest.json";
|
|
4715
5021
|
var REPOS_LOCAL_FILE = "repos.local.json";
|
|
4716
5022
|
var MemberConfigError = class extends Error {
|
|
@@ -4723,14 +5029,14 @@ var MemberConfigError = class extends Error {
|
|
|
4723
5029
|
};
|
|
4724
5030
|
function resolveAbsPath(workspaceRoot, folder, overrides) {
|
|
4725
5031
|
const override = overrides[folder];
|
|
4726
|
-
if (override) return
|
|
4727
|
-
return
|
|
5032
|
+
if (override) return path33.resolve(workspaceRoot, override);
|
|
5033
|
+
return path33.resolve(workspaceRoot, folder);
|
|
4728
5034
|
}
|
|
4729
5035
|
function isSelfMember(workspaceRoot, member) {
|
|
4730
|
-
return
|
|
5036
|
+
return path33.resolve(member.absPath) === path33.resolve(workspaceRoot);
|
|
4731
5037
|
}
|
|
4732
5038
|
async function readPathOverrides(workspaceRoot) {
|
|
4733
|
-
const filePath =
|
|
5039
|
+
const filePath = path33.join(workspaceRoot, REPOS_LOCAL_FILE);
|
|
4734
5040
|
const result = await readJsonDetailed(filePath);
|
|
4735
5041
|
if (result.status === "missing") return {};
|
|
4736
5042
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -4746,7 +5052,7 @@ async function readPathOverrides(workspaceRoot) {
|
|
|
4746
5052
|
return overrides;
|
|
4747
5053
|
}
|
|
4748
5054
|
async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
4749
|
-
const filePath =
|
|
5055
|
+
const filePath = path33.join(workspaceRoot, WORKSPACE_FILE);
|
|
4750
5056
|
const text = await readText(filePath);
|
|
4751
5057
|
if (text === void 0) return void 0;
|
|
4752
5058
|
const config = parseWorkspaceConfig(text);
|
|
@@ -4759,7 +5065,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
|
|
|
4759
5065
|
}));
|
|
4760
5066
|
}
|
|
4761
5067
|
async function readFromManifest(workspaceRoot, overrides) {
|
|
4762
|
-
const filePath =
|
|
5068
|
+
const filePath = path33.join(workspaceRoot, REPOS_MANIFEST_FILE);
|
|
4763
5069
|
const result = await readJsonDetailed(filePath);
|
|
4764
5070
|
if (result.status === "missing") return void 0;
|
|
4765
5071
|
if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
|
|
@@ -4801,16 +5107,11 @@ async function readMembers(rootInfo) {
|
|
|
4801
5107
|
}
|
|
4802
5108
|
|
|
4803
5109
|
// 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
|
-
}
|
|
5110
|
+
import path37 from "node:path";
|
|
4810
5111
|
|
|
4811
5112
|
// src/utils/git-root.ts
|
|
4812
5113
|
init_exec();
|
|
4813
|
-
import
|
|
5114
|
+
import path34 from "node:path";
|
|
4814
5115
|
function fallback(start) {
|
|
4815
5116
|
return {
|
|
4816
5117
|
cwd: start,
|
|
@@ -4826,11 +5127,11 @@ function fallback(start) {
|
|
|
4826
5127
|
async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
|
|
4827
5128
|
const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
|
|
4828
5129
|
const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
|
|
4829
|
-
if (raw &&
|
|
5130
|
+
if (raw && path34.isAbsolute(raw)) return path34.resolve(raw);
|
|
4830
5131
|
const plain = await runGit(["rev-parse", flag], { cwd });
|
|
4831
5132
|
const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
|
|
4832
5133
|
if (!plainRaw) return "";
|
|
4833
|
-
return
|
|
5134
|
+
return path34.isAbsolute(plainRaw) ? path34.resolve(plainRaw) : path34.resolve(repoRoot, plainRaw);
|
|
4834
5135
|
}
|
|
4835
5136
|
async function resolveRoots(start) {
|
|
4836
5137
|
const cwd = start ?? process.cwd();
|
|
@@ -4839,15 +5140,15 @@ async function resolveRoots(start) {
|
|
|
4839
5140
|
if (toplevel.exitCode !== 0) return fallback(cwd);
|
|
4840
5141
|
const toplevelRaw = toplevel.stdout.trim();
|
|
4841
5142
|
if (!toplevelRaw) return fallback(cwd);
|
|
4842
|
-
const repoRoot =
|
|
5143
|
+
const repoRoot = path34.resolve(toplevelRaw);
|
|
4843
5144
|
const [gitDir, gitCommonDir] = await Promise.all([
|
|
4844
5145
|
resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
|
|
4845
5146
|
resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
|
|
4846
5147
|
]);
|
|
4847
5148
|
if (!gitDir || !gitCommonDir) return fallback(cwd);
|
|
4848
5149
|
const isLinkedWorktree = gitDir !== gitCommonDir;
|
|
4849
|
-
const mainRoot = isLinkedWorktree &&
|
|
4850
|
-
const worktreeName = isLinkedWorktree ?
|
|
5150
|
+
const mainRoot = isLinkedWorktree && path34.basename(gitCommonDir) === ".git" ? path34.dirname(gitCommonDir) : repoRoot;
|
|
5151
|
+
const worktreeName = isLinkedWorktree ? path34.basename(gitDir) : null;
|
|
4851
5152
|
return {
|
|
4852
5153
|
cwd,
|
|
4853
5154
|
repoRoot,
|
|
@@ -4892,8 +5193,8 @@ function resolveMajorVersion(range) {
|
|
|
4892
5193
|
}
|
|
4893
5194
|
|
|
4894
5195
|
// src/scanner/detect-package-manager.ts
|
|
4895
|
-
import
|
|
4896
|
-
import
|
|
5196
|
+
import path35 from "node:path";
|
|
5197
|
+
import fs28 from "fs-extra";
|
|
4897
5198
|
function detectPackageManager(root, packageManagerField) {
|
|
4898
5199
|
const field = String(packageManagerField ?? "").trim();
|
|
4899
5200
|
if (field.startsWith("yarn@")) {
|
|
@@ -4911,9 +5212,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
4911
5212
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
4912
5213
|
return "unknown";
|
|
4913
5214
|
}
|
|
4914
|
-
if (
|
|
4915
|
-
if (
|
|
4916
|
-
if (
|
|
5215
|
+
if (fs28.existsSync(path35.join(root, "yarn.lock"))) return "yarn";
|
|
5216
|
+
if (fs28.existsSync(path35.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
5217
|
+
if (fs28.existsSync(path35.join(root, "package-lock.json"))) return "npm";
|
|
4917
5218
|
return "unknown";
|
|
4918
5219
|
}
|
|
4919
5220
|
|
|
@@ -5030,6 +5331,7 @@ var STACK_RULES = [
|
|
|
5030
5331
|
stack: ["frontend", "shadcn"],
|
|
5031
5332
|
all: [fileEndsWith("components.json"), dep("class-variance-authority")]
|
|
5032
5333
|
},
|
|
5334
|
+
{ stack: ["frontend", "radix"], any: [depPrefix("@radix-ui/")] },
|
|
5033
5335
|
{ stack: ["tooling", "typescript5"], any: [depMajor("typescript", 5)] },
|
|
5034
5336
|
{ stack: ["backend", "sanity"], any: [dep("sanity"), dep("next-sanity"), dep("@sanity/client")] },
|
|
5035
5337
|
{ stack: ["backend", "strapi"], any: [dep("@strapi/strapi"), depPrefix("@strapi/")] },
|
|
@@ -5100,114 +5402,44 @@ var STACK_RULES = [
|
|
|
5100
5402
|
{ stack: ["testing", "playwright"], any: [dep("@playwright/test")] },
|
|
5101
5403
|
{ stack: ["testing", "storybook"], any: [fileIncludes(".storybook")] },
|
|
5102
5404
|
{ 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");
|
|
5405
|
+
{ stack: ["testing", "phpunit"], any: [fileEndsWith("phpunit.xml")] },
|
|
5406
|
+
{ stack: ["testing", "storybook"], any: [depPrefix("@storybook/")] },
|
|
5407
|
+
{ stack: ["testing", "vitest"], any: [dep("vitest")] },
|
|
5408
|
+
{ stack: ["testing", "jest"], any: [dep("jest"), dep("jest-environment-jsdom")] },
|
|
5409
|
+
{ stack: ["databases", "postgresql"], any: [dep("pg")] },
|
|
5410
|
+
{ stack: ["databases", "mariadb"], any: [dep("mariadb"), dep("mysql2")] },
|
|
5411
|
+
{ stack: ["databases", "mysql"], any: [dep("mysql"), dep("mysql2")] },
|
|
5412
|
+
{ stack: ["databases", "mssql"], any: [dep("mssql")] },
|
|
5413
|
+
{ stack: ["databases", "elasticsearch"], any: [dep("@elastic/elasticsearch")] },
|
|
5414
|
+
{ stack: ["databases", "redis"], any: [dep("predis/predis"), dep("ioredis"), dep("redis")] },
|
|
5415
|
+
{ stack: ["auth", "oidc"], any: [content("openid")] },
|
|
5416
|
+
{ stack: ["auth", "azure-ad"], any: [content("AZURE_AD")] },
|
|
5417
|
+
{ stack: ["auth", "bankid"], any: [content("BANKID")] },
|
|
5418
|
+
{
|
|
5419
|
+
stack: ["auth", "saml2"],
|
|
5420
|
+
any: [dep("24slides/laravel-saml2"), dep("aacotroneo/laravel-saml2")]
|
|
5421
|
+
},
|
|
5422
|
+
{ stack: ["auth", "next-auth"], any: [dep("next-auth"), dep("@auth/core")] }
|
|
5423
|
+
];
|
|
5424
|
+
function runDetection(ctx, rules = STACK_RULES) {
|
|
5425
|
+
const roles = [];
|
|
5426
|
+
for (const rule of ROLE_RULES) {
|
|
5427
|
+
if (rule.role && matchRule(rule, ctx) && !roles.includes(rule.role)) roles.push(rule.role);
|
|
5197
5428
|
}
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
)
|
|
5205
|
-
|
|
5429
|
+
const stacks = {};
|
|
5430
|
+
for (const bucket of STACK_BUCKETS) stacks[bucket] = [];
|
|
5431
|
+
for (const rule of rules) {
|
|
5432
|
+
if (!rule.stack || !matchRule(rule, ctx)) continue;
|
|
5433
|
+
const [bucket, name] = rule.stack;
|
|
5434
|
+
stacks[bucket] ??= [];
|
|
5435
|
+
if (!stacks[bucket].includes(name)) stacks[bucket].push(name);
|
|
5436
|
+
}
|
|
5437
|
+
return { roles, stacks };
|
|
5206
5438
|
}
|
|
5207
5439
|
|
|
5208
5440
|
// src/scanner/render.ts
|
|
5209
5441
|
import { readFile } from "node:fs/promises";
|
|
5210
|
-
import
|
|
5442
|
+
import path36 from "node:path";
|
|
5211
5443
|
async function buildContentBlob(root, files) {
|
|
5212
5444
|
const candidates = files.filter(
|
|
5213
5445
|
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
|
|
@@ -5215,7 +5447,7 @@ async function buildContentBlob(root, files) {
|
|
|
5215
5447
|
const slice = candidates.slice(0, 300);
|
|
5216
5448
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
5217
5449
|
try {
|
|
5218
|
-
return await readFile(
|
|
5450
|
+
return await readFile(path36.join(root, rel), "utf8");
|
|
5219
5451
|
} catch {
|
|
5220
5452
|
return "";
|
|
5221
5453
|
}
|
|
@@ -5307,8 +5539,8 @@ var SAFE_FILES = [
|
|
|
5307
5539
|
"Gemfile"
|
|
5308
5540
|
];
|
|
5309
5541
|
async function scanProject(root) {
|
|
5310
|
-
const pkg = await readJson(
|
|
5311
|
-
const composer = await readJson(
|
|
5542
|
+
const pkg = await readJson(path37.join(root, "package.json"));
|
|
5543
|
+
const composer = await readJson(path37.join(root, "composer.json"));
|
|
5312
5544
|
const files = await listFiles(root, SAFE_FILES);
|
|
5313
5545
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
5314
5546
|
const depVersions = dependencyVersionMap(pkg, composer);
|
|
@@ -5328,7 +5560,7 @@ async function scanProject(root) {
|
|
|
5328
5560
|
const warnings = [];
|
|
5329
5561
|
const securityRisks = [];
|
|
5330
5562
|
const crossRepoHints = [];
|
|
5331
|
-
const nodeEngine =
|
|
5563
|
+
const nodeEngine = isRecord(pkg?.engines) ? String(pkg.engines.node ?? "") : "";
|
|
5332
5564
|
if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
|
|
5333
5565
|
warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
|
|
5334
5566
|
}
|
|
@@ -5338,7 +5570,7 @@ async function scanProject(root) {
|
|
|
5338
5570
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
5339
5571
|
const unsupportedSignals = collectUnsupportedSignals(safeFiles);
|
|
5340
5572
|
const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
|
|
5341
|
-
const repoNameFallback = pkg?.name ? String(pkg.name) :
|
|
5573
|
+
const repoNameFallback = pkg?.name ? String(pkg.name) : path37.basename((await resolveRoots(root)).mainRoot);
|
|
5342
5574
|
const context = {
|
|
5343
5575
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5344
5576
|
repoName: repoNameFallback,
|
|
@@ -5360,14 +5592,15 @@ async function scanProject(root) {
|
|
|
5360
5592
|
// src/scanner/read-context.ts
|
|
5361
5593
|
async function isCacheFresh(root, cachePath) {
|
|
5362
5594
|
const markerPaths = [
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5595
|
+
path38.join(root, "package.json"),
|
|
5596
|
+
path38.join(root, "composer.json"),
|
|
5597
|
+
path38.join(root, WORKSPACE_FILE),
|
|
5598
|
+
path38.join(root, REPOS_MANIFEST_FILE)
|
|
5366
5599
|
];
|
|
5367
|
-
const cacheStat = await
|
|
5600
|
+
const cacheStat = await fs29.stat(cachePath);
|
|
5368
5601
|
for (const markerPath of markerPaths) {
|
|
5369
|
-
if (!await
|
|
5370
|
-
const markerStat = await
|
|
5602
|
+
if (!await fs29.pathExists(markerPath)) continue;
|
|
5603
|
+
const markerStat = await fs29.stat(markerPath);
|
|
5371
5604
|
if (markerStat.mtimeMs > cacheStat.mtimeMs) return false;
|
|
5372
5605
|
}
|
|
5373
5606
|
return true;
|
|
@@ -5468,7 +5701,7 @@ async function runDoctor(options) {
|
|
|
5468
5701
|
} else {
|
|
5469
5702
|
ok(`- HOOKS OK: ${hooks.message}`);
|
|
5470
5703
|
}
|
|
5471
|
-
const rootClaudeMdPath =
|
|
5704
|
+
const rootClaudeMdPath = path39.join(root, "CLAUDE.md");
|
|
5472
5705
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
5473
5706
|
if (!rootClaudeMdContent) {
|
|
5474
5707
|
flag(
|
|
@@ -5511,10 +5744,10 @@ async function runDoctor(options) {
|
|
|
5511
5744
|
ok("- CLAUDE.md: import block present");
|
|
5512
5745
|
}
|
|
5513
5746
|
for (const target of actualTargets) {
|
|
5514
|
-
const resolved =
|
|
5515
|
-
const relFromRoot =
|
|
5516
|
-
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${
|
|
5517
|
-
if (escapesRoot || !await
|
|
5747
|
+
const resolved = path39.resolve(root, target);
|
|
5748
|
+
const relFromRoot = path39.relative(root, resolved);
|
|
5749
|
+
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path39.sep}`) || path39.isAbsolute(relFromRoot);
|
|
5750
|
+
if (escapesRoot || !await fs30.pathExists(resolved)) {
|
|
5518
5751
|
flag(
|
|
5519
5752
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
5520
5753
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -5532,7 +5765,7 @@ async function runDoctor(options) {
|
|
|
5532
5765
|
);
|
|
5533
5766
|
}
|
|
5534
5767
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
5535
|
-
const workflowExists = await
|
|
5768
|
+
const workflowExists = await fs30.pathExists(workflowPath);
|
|
5536
5769
|
if (!workflowExists) {
|
|
5537
5770
|
flag(
|
|
5538
5771
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -5547,15 +5780,15 @@ async function runDoctor(options) {
|
|
|
5547
5780
|
} else {
|
|
5548
5781
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
5549
5782
|
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
5550
|
-
const cachePath =
|
|
5551
|
-
const bundledPath =
|
|
5783
|
+
const cachePath = path39.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
5784
|
+
const bundledPath = path39.join(
|
|
5552
5785
|
packageRoot(),
|
|
5553
5786
|
"library",
|
|
5554
5787
|
"global",
|
|
5555
5788
|
"templates",
|
|
5556
5789
|
"agentic-workflow-standard.md"
|
|
5557
5790
|
);
|
|
5558
|
-
const templatePath = await
|
|
5791
|
+
const templatePath = await fs30.pathExists(cachePath) ? cachePath : bundledPath;
|
|
5559
5792
|
const templateContent = await readText(templatePath);
|
|
5560
5793
|
const verdict = checkManagedTamper(bodyContent, storedHashMatch?.[1], templateContent ?? null);
|
|
5561
5794
|
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
@@ -5576,7 +5809,7 @@ async function runDoctor(options) {
|
|
|
5576
5809
|
}
|
|
5577
5810
|
}
|
|
5578
5811
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
5579
|
-
const workflowConfigExists = await
|
|
5812
|
+
const workflowConfigExists = await fs30.pathExists(workflowConfigPath);
|
|
5580
5813
|
if (!workflowConfigExists) {
|
|
5581
5814
|
flag(
|
|
5582
5815
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -5584,7 +5817,7 @@ async function runDoctor(options) {
|
|
|
5584
5817
|
"haus apply --write"
|
|
5585
5818
|
);
|
|
5586
5819
|
} else {
|
|
5587
|
-
const cfg = await
|
|
5820
|
+
const cfg = await fs30.readFile(workflowConfigPath, "utf8");
|
|
5588
5821
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
5589
5822
|
if (unfilled > 0) {
|
|
5590
5823
|
suggest(
|
|
@@ -5597,7 +5830,7 @@ async function runDoctor(options) {
|
|
|
5597
5830
|
}
|
|
5598
5831
|
}
|
|
5599
5832
|
if (workflowExists) {
|
|
5600
|
-
const prettierIgnore = await readText(
|
|
5833
|
+
const prettierIgnore = await readText(path39.join(root, ".prettierignore")) ?? "";
|
|
5601
5834
|
const lines = prettierIgnore.split("\n");
|
|
5602
5835
|
const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
|
|
5603
5836
|
if (missing.length > 0) {
|
|
@@ -5637,6 +5870,29 @@ async function runDoctor(options) {
|
|
|
5637
5870
|
if (finding.level === "ok") ok(finding.line);
|
|
5638
5871
|
else suggest(finding.line, finding.sentence, finding.fix);
|
|
5639
5872
|
}
|
|
5873
|
+
const skillRefIssues = await findUnresolvedSkillReferences(root);
|
|
5874
|
+
if (skillRefIssues.length > 0) {
|
|
5875
|
+
const prefixBug = skillRefIssues.filter((i) => i.bareInstalled);
|
|
5876
|
+
const missingDep = skillRefIssues.filter((i) => !i.bareInstalled);
|
|
5877
|
+
const describe = (i) => `${i.file}:${i.line} "${i.reference}"`;
|
|
5878
|
+
if (prefixBug.length > 0) {
|
|
5879
|
+
flag(
|
|
5880
|
+
`- 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" : ""})`,
|
|
5881
|
+
`${prefixBug.length} skill reference(s) use a prefix that can't resolve, even though the target skill is installed`,
|
|
5882
|
+
"haus apply --write (re-vendors the skill body with the prefix stripped)"
|
|
5883
|
+
);
|
|
5884
|
+
}
|
|
5885
|
+
if (missingDep.length > 0) {
|
|
5886
|
+
const missingNames = [...new Set(missingDep.map((i) => i.bareName))];
|
|
5887
|
+
flag(
|
|
5888
|
+
`- 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" : ""})`,
|
|
5889
|
+
`${missingDep.length} skill reference(s) point at a sub-skill (${missingNames.join(", ")}) that isn't installed`,
|
|
5890
|
+
"find its catalog id and run `haus recommend --include <id>` then `haus apply --write`, or remove the reference"
|
|
5891
|
+
);
|
|
5892
|
+
}
|
|
5893
|
+
} else {
|
|
5894
|
+
ok("- SKILL REFS: OK (no unresolved cross-skill references)");
|
|
5895
|
+
}
|
|
5640
5896
|
const cacheAgeMs = await getCacheManifestAge();
|
|
5641
5897
|
if (cacheAgeMs === null) {
|
|
5642
5898
|
suggest(
|
|
@@ -5657,20 +5913,28 @@ async function runDoctor(options) {
|
|
|
5657
5913
|
}
|
|
5658
5914
|
}
|
|
5659
5915
|
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) {
|
|
5916
|
+
if (!Array.isArray(recommendation.recommended)) {
|
|
5666
5917
|
suggest(
|
|
5667
|
-
`- CATALOG ITEMS:
|
|
5668
|
-
|
|
5669
|
-
"
|
|
5918
|
+
`- CATALOG ITEMS: recommendation.json's "recommended" field is not an array \u2014 skipping orphaned-item check`,
|
|
5919
|
+
'recommendation.json looks malformed (its "recommended" field is not an array)',
|
|
5920
|
+
"run `haus recommend` to regenerate it"
|
|
5921
|
+
);
|
|
5922
|
+
} else {
|
|
5923
|
+
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
5924
|
+
const recommendedIds = new Set(
|
|
5925
|
+
recommendation.recommended.map((r) => r.id).filter((id) => typeof id === "string")
|
|
5670
5926
|
);
|
|
5927
|
+
const orphaned = findOrphanedLockEntries(lock ?? [], recommendedIds).map((entry) => entry.id);
|
|
5928
|
+
if (orphaned.length > 0) {
|
|
5929
|
+
suggest(
|
|
5930
|
+
`- CATALOG ITEMS: ${orphaned.length} installed item(s) no longer in the current recommendation (${orphaned.join(", ")})`,
|
|
5931
|
+
`${orphaned.length} installed item(s) are no longer recommended for this project`,
|
|
5932
|
+
"review whether they are still needed; haus apply --write leaves them in place until removed from the catalog"
|
|
5933
|
+
);
|
|
5934
|
+
}
|
|
5671
5935
|
}
|
|
5672
5936
|
}
|
|
5673
|
-
const pkgJson = await readJson(
|
|
5937
|
+
const pkgJson = await readJson(path39.join(packageRoot(), "package.json"));
|
|
5674
5938
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5675
5939
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
5676
5940
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -5703,12 +5967,12 @@ async function runDoctor(options) {
|
|
|
5703
5967
|
}
|
|
5704
5968
|
|
|
5705
5969
|
// src/commands/update.ts
|
|
5706
|
-
import
|
|
5970
|
+
import path42 from "node:path";
|
|
5707
5971
|
|
|
5708
5972
|
// src/claude/refresh-project.ts
|
|
5709
|
-
import
|
|
5973
|
+
import fs31 from "fs-extra";
|
|
5710
5974
|
async function isHausProject(root) {
|
|
5711
|
-
if (await
|
|
5975
|
+
if (await fs31.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
5712
5976
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
5713
5977
|
if (result.status === "ok") return result.value._haus != null;
|
|
5714
5978
|
return false;
|
|
@@ -5720,8 +5984,8 @@ async function refreshProjectApply(root) {
|
|
|
5720
5984
|
|
|
5721
5985
|
// src/install/apply.ts
|
|
5722
5986
|
import crypto2 from "node:crypto";
|
|
5723
|
-
import
|
|
5724
|
-
import
|
|
5987
|
+
import path40 from "node:path";
|
|
5988
|
+
import fs32 from "fs-extra";
|
|
5725
5989
|
|
|
5726
5990
|
// src/install/header.ts
|
|
5727
5991
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -5805,59 +6069,66 @@ var SCHEMA_VERSION2 = "1";
|
|
|
5805
6069
|
function hashContent(content2) {
|
|
5806
6070
|
return `sha256-${crypto2.createHash("sha256").update(normaliseLF(content2)).digest("hex")}`;
|
|
5807
6071
|
}
|
|
6072
|
+
function destFileMissing(destPath) {
|
|
6073
|
+
try {
|
|
6074
|
+
return !fs32.statSync(destPath).isFile();
|
|
6075
|
+
} catch {
|
|
6076
|
+
return true;
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
5808
6079
|
function sourceVersion() {
|
|
5809
6080
|
try {
|
|
5810
|
-
const pkgPath =
|
|
5811
|
-
const pkg = JSON.parse(
|
|
6081
|
+
const pkgPath = path40.join(packageRoot(), "package.json");
|
|
6082
|
+
const pkg = JSON.parse(fs32.readFileSync(pkgPath, "utf8"));
|
|
5812
6083
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
5813
6084
|
} catch {
|
|
5814
6085
|
return "haus@0.0.0";
|
|
5815
6086
|
}
|
|
5816
6087
|
}
|
|
5817
6088
|
function globalSrcDir() {
|
|
5818
|
-
return
|
|
6089
|
+
return path40.join(packageRoot(), "library", "global");
|
|
5819
6090
|
}
|
|
5820
6091
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
5821
|
-
const skillDir =
|
|
6092
|
+
const skillDir = path40.join(skillsDir, skillName);
|
|
5822
6093
|
const entries = [];
|
|
5823
|
-
for (const rel of
|
|
6094
|
+
for (const rel of fs32.readdirSync(skillDir, { recursive: true })) {
|
|
5824
6095
|
if (rel === "SKILL.md") continue;
|
|
5825
|
-
const abs =
|
|
5826
|
-
if (!
|
|
5827
|
-
const posixRel = rel.split(
|
|
6096
|
+
const abs = path40.join(skillDir, rel);
|
|
6097
|
+
if (!fs32.statSync(abs).isFile()) continue;
|
|
6098
|
+
const posixRel = rel.split(path40.sep).join("/");
|
|
5828
6099
|
entries.push({
|
|
5829
6100
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
5830
|
-
srcRelPath:
|
|
5831
|
-
destPath:
|
|
6101
|
+
srcRelPath: path40.join("library", "global", "skills", skillName, rel),
|
|
6102
|
+
destPath: path40.join(claudeDir, "skills", skillName, rel)
|
|
5832
6103
|
});
|
|
5833
6104
|
}
|
|
5834
6105
|
return entries;
|
|
5835
6106
|
}
|
|
5836
6107
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
5837
6108
|
const entries = [];
|
|
5838
|
-
const skillsDir =
|
|
5839
|
-
if (
|
|
5840
|
-
for (const skillName of
|
|
5841
|
-
const skillFile =
|
|
5842
|
-
if (
|
|
6109
|
+
const skillsDir = path40.join(srcDir, "skills");
|
|
6110
|
+
if (fs32.pathExistsSync(skillsDir)) {
|
|
6111
|
+
for (const skillName of fs32.readdirSync(skillsDir)) {
|
|
6112
|
+
const skillFile = path40.join(skillsDir, skillName, "SKILL.md");
|
|
6113
|
+
if (fs32.pathExistsSync(skillFile)) {
|
|
5843
6114
|
entries.push({
|
|
5844
6115
|
stableId: `skill.${skillName}`,
|
|
5845
|
-
srcRelPath:
|
|
5846
|
-
destPath:
|
|
6116
|
+
srcRelPath: path40.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
6117
|
+
destPath: path40.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
5847
6118
|
});
|
|
5848
6119
|
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
5849
6120
|
}
|
|
5850
6121
|
}
|
|
5851
6122
|
}
|
|
5852
|
-
const commandsDir =
|
|
5853
|
-
if (
|
|
5854
|
-
for (const fileName of
|
|
6123
|
+
const commandsDir = path40.join(srcDir, "commands");
|
|
6124
|
+
if (fs32.pathExistsSync(commandsDir)) {
|
|
6125
|
+
for (const fileName of fs32.readdirSync(commandsDir)) {
|
|
5855
6126
|
if (!fileName.endsWith(".md")) continue;
|
|
5856
6127
|
const commandName = fileName.slice(0, -".md".length);
|
|
5857
6128
|
entries.push({
|
|
5858
6129
|
stableId: `command.${commandName}`,
|
|
5859
|
-
srcRelPath:
|
|
5860
|
-
destPath:
|
|
6130
|
+
srcRelPath: path40.join("library", "global", "commands", fileName),
|
|
6131
|
+
destPath: path40.join(claudeDir, "commands", fileName)
|
|
5861
6132
|
});
|
|
5862
6133
|
}
|
|
5863
6134
|
}
|
|
@@ -5883,7 +6154,7 @@ async function applyInstall(options = {}) {
|
|
|
5883
6154
|
};
|
|
5884
6155
|
const manifestFiles = [];
|
|
5885
6156
|
for (const entry of sourceFiles) {
|
|
5886
|
-
const srcPath =
|
|
6157
|
+
const srcPath = path40.join(packageRoot(), entry.srcRelPath);
|
|
5887
6158
|
const rawContent = await readText(srcPath);
|
|
5888
6159
|
if (rawContent === void 0) {
|
|
5889
6160
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -5897,13 +6168,14 @@ async function applyInstall(options = {}) {
|
|
|
5897
6168
|
const newHash = hashContent(stamped);
|
|
5898
6169
|
const existing = manifestByDest.get(entry.destPath);
|
|
5899
6170
|
if (check) {
|
|
5900
|
-
|
|
6171
|
+
const destMissing = destFileMissing(entry.destPath);
|
|
6172
|
+
if (destMissing || existing && existing.hash !== newHash) {
|
|
5901
6173
|
result.drift = true;
|
|
5902
6174
|
result.skipped.push(entry.destPath);
|
|
5903
6175
|
}
|
|
5904
6176
|
continue;
|
|
5905
6177
|
}
|
|
5906
|
-
const destExists =
|
|
6178
|
+
const destExists = fs32.pathExistsSync(entry.destPath);
|
|
5907
6179
|
if (destExists) {
|
|
5908
6180
|
const currentContent = await readText(entry.destPath);
|
|
5909
6181
|
if (currentContent !== void 0) {
|
|
@@ -5943,7 +6215,7 @@ async function applyInstall(options = {}) {
|
|
|
5943
6215
|
}
|
|
5944
6216
|
let mergedSettings;
|
|
5945
6217
|
if (!check) {
|
|
5946
|
-
const fragmentPath =
|
|
6218
|
+
const fragmentPath = path40.join(srcDir, "settings-fragments", "hooks.json");
|
|
5947
6219
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
5948
6220
|
const settings = await readSettings();
|
|
5949
6221
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -5957,13 +6229,13 @@ async function applyInstall(options = {}) {
|
|
|
5957
6229
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
5958
6230
|
for (const entry of existingManifest.files) {
|
|
5959
6231
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
5960
|
-
if (!
|
|
6232
|
+
if (!fs32.pathExistsSync(entry.destPath)) continue;
|
|
5961
6233
|
const content2 = await readText(entry.destPath);
|
|
5962
6234
|
if (!content2) continue;
|
|
5963
6235
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
5964
6236
|
const currentHash = hashContent(content2);
|
|
5965
6237
|
if (hasHeader && currentHash === entry.hash) {
|
|
5966
|
-
if (!dryRun) await
|
|
6238
|
+
if (!dryRun) await fs32.remove(entry.destPath);
|
|
5967
6239
|
result.deleted.push(entry.destPath);
|
|
5968
6240
|
} else {
|
|
5969
6241
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -6032,7 +6304,7 @@ function summarizeLockDiff(before, after) {
|
|
|
6032
6304
|
|
|
6033
6305
|
// src/update/lockfile.ts
|
|
6034
6306
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
6035
|
-
import
|
|
6307
|
+
import path41 from "node:path";
|
|
6036
6308
|
async function readLockSummary(root) {
|
|
6037
6309
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
6038
6310
|
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
@@ -6056,19 +6328,21 @@ async function checkLock(root) {
|
|
|
6056
6328
|
const ok = lock.length > 0 && hasValidVersions && drift.length === 0;
|
|
6057
6329
|
return { ok, count: lock.length, catalogRef, drift, driftCount: drift.length };
|
|
6058
6330
|
}
|
|
6059
|
-
async function
|
|
6060
|
-
const lockPath = hausPath(root, "haus.lock.json");
|
|
6061
|
-
let before = "[]";
|
|
6331
|
+
async function readLockRaw(root) {
|
|
6062
6332
|
try {
|
|
6063
|
-
|
|
6333
|
+
return await readFile2(hausPath(root, "haus.lock.json"), "utf8");
|
|
6064
6334
|
} catch {
|
|
6065
|
-
|
|
6335
|
+
return "[]";
|
|
6066
6336
|
}
|
|
6337
|
+
}
|
|
6338
|
+
async function applyLock(root) {
|
|
6339
|
+
const lockPath = hausPath(root, "haus.lock.json");
|
|
6340
|
+
const before = await readLockRaw(root);
|
|
6067
6341
|
const lock = await readJson(lockPath) ?? [];
|
|
6068
6342
|
try {
|
|
6069
6343
|
const backupDir = hausPath(root, "backups");
|
|
6070
6344
|
await mkdir(backupDir, { recursive: true });
|
|
6071
|
-
await copyFile(lockPath,
|
|
6345
|
+
await copyFile(lockPath, path41.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
6072
6346
|
} catch {
|
|
6073
6347
|
}
|
|
6074
6348
|
const enriched = await Promise.all(
|
|
@@ -6090,7 +6364,7 @@ function diffLock(before, after) {
|
|
|
6090
6364
|
}
|
|
6091
6365
|
async function hasLocalOverrides(root) {
|
|
6092
6366
|
try {
|
|
6093
|
-
await readFile2(
|
|
6367
|
+
await readFile2(path41.join(root, ".claude", "settings.json"), "utf8");
|
|
6094
6368
|
return true;
|
|
6095
6369
|
} catch {
|
|
6096
6370
|
return false;
|
|
@@ -6106,7 +6380,7 @@ async function runUpdate(options) {
|
|
|
6106
6380
|
return;
|
|
6107
6381
|
}
|
|
6108
6382
|
if (options.check) {
|
|
6109
|
-
const pkgJson2 = await readJson(
|
|
6383
|
+
const pkgJson2 = await readJson(path42.join(packageRoot(), "package.json"));
|
|
6110
6384
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
6111
6385
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift, catalogItems, lockItems] = await Promise.all([
|
|
6112
6386
|
options.fast ? fastLockStatus(root) : checkLock(root),
|
|
@@ -6138,10 +6412,12 @@ async function runUpdate(options) {
|
|
|
6138
6412
|
)
|
|
6139
6413
|
);
|
|
6140
6414
|
const hasHashDrift = !options.fast && status.count > 0 && "ok" in status && !status.ok;
|
|
6141
|
-
if (hasHashDrift || formerIdMigrations.length > 0
|
|
6415
|
+
if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true) {
|
|
6416
|
+
process.exitCode = 1;
|
|
6417
|
+
}
|
|
6142
6418
|
return;
|
|
6143
6419
|
}
|
|
6144
|
-
const pkgJson = await readJson(
|
|
6420
|
+
const pkgJson = await readJson(path42.join(packageRoot(), "package.json"));
|
|
6145
6421
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6146
6422
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
6147
6423
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -6153,9 +6429,7 @@ async function runUpdate(options) {
|
|
|
6153
6429
|
if (await hasLocalOverrides(root)) {
|
|
6154
6430
|
log("Existing .claude/settings.json \u2014 haus rules will be merged, not replaced.");
|
|
6155
6431
|
}
|
|
6156
|
-
const
|
|
6157
|
-
log(diffLock(before, after));
|
|
6158
|
-
log(summarizeLockDiff(before, after));
|
|
6432
|
+
const lockBefore = await readLockRaw(root);
|
|
6159
6433
|
log("Syncing remote catalog...");
|
|
6160
6434
|
const sync = await syncRemoteCatalog();
|
|
6161
6435
|
if (sync.newItems.length > 0) {
|
|
@@ -6177,13 +6451,16 @@ async function runUpdate(options) {
|
|
|
6177
6451
|
}
|
|
6178
6452
|
await refreshGlobalInstall();
|
|
6179
6453
|
await refreshProjectFiles(root);
|
|
6454
|
+
const { after } = await applyLock(root);
|
|
6455
|
+
log(diffLock(lockBefore, after));
|
|
6456
|
+
log(summarizeLockDiff(lockBefore, after));
|
|
6180
6457
|
log("Update applied with backup in .haus-workflow/backups/. Run haus doctor.");
|
|
6181
6458
|
}
|
|
6182
6459
|
async function runFromHookCheck(root) {
|
|
6183
6460
|
try {
|
|
6184
6461
|
const summary = await readLockSummary(root);
|
|
6185
6462
|
if (summary.count === 0) return;
|
|
6186
|
-
const pkgJson = await readJson(
|
|
6463
|
+
const pkgJson = await readJson(path42.join(packageRoot(), "package.json"));
|
|
6187
6464
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6188
6465
|
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
6189
6466
|
fetchNpmVersionStatus(currentVersion),
|
|
@@ -6333,7 +6610,8 @@ async function runCiGate(options = {}) {
|
|
|
6333
6610
|
// src/commands/clone.ts
|
|
6334
6611
|
init_exec();
|
|
6335
6612
|
import { existsSync as existsSync2 } from "node:fs";
|
|
6336
|
-
import
|
|
6613
|
+
import { readdir } from "node:fs/promises";
|
|
6614
|
+
import path43 from "node:path";
|
|
6337
6615
|
var GIT_LOCATION_VARS = [
|
|
6338
6616
|
"GIT_DIR",
|
|
6339
6617
|
"GIT_WORK_TREE",
|
|
@@ -6362,6 +6640,16 @@ function normalizeGitUrl(url) {
|
|
|
6362
6640
|
s = s.replace(/\.git$/i, "");
|
|
6363
6641
|
return s.toLowerCase();
|
|
6364
6642
|
}
|
|
6643
|
+
async function isEmptyDirectory(dir) {
|
|
6644
|
+
try {
|
|
6645
|
+
const entries = await readdir(dir);
|
|
6646
|
+
return entries.length === 0;
|
|
6647
|
+
} catch (err) {
|
|
6648
|
+
const code = err.code;
|
|
6649
|
+
if (code === "ENOTDIR" || code === "ENOENT") return false;
|
|
6650
|
+
throw err;
|
|
6651
|
+
}
|
|
6652
|
+
}
|
|
6365
6653
|
async function checkExistingTarget(target, url) {
|
|
6366
6654
|
const isRepo = await runGit(["-C", target, "rev-parse", "--is-inside-work-tree"], {
|
|
6367
6655
|
env: cloneEnv(),
|
|
@@ -6383,11 +6671,11 @@ async function runClone(url, opts = {}) {
|
|
|
6383
6671
|
process.exitCode = 1;
|
|
6384
6672
|
return;
|
|
6385
6673
|
}
|
|
6386
|
-
const target =
|
|
6387
|
-
if (existsSync2(target)) {
|
|
6674
|
+
const target = path43.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
6675
|
+
if (existsSync2(target) && !await isEmptyDirectory(target)) {
|
|
6388
6676
|
const check = await checkExistingTarget(target, url);
|
|
6389
6677
|
if (check.kind === "matches") {
|
|
6390
|
-
log(`\u2022 ${
|
|
6678
|
+
log(`\u2022 ${path43.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
|
|
6391
6679
|
return;
|
|
6392
6680
|
}
|
|
6393
6681
|
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 +6715,7 @@ function readHookPayload() {
|
|
|
6427
6715
|
if (!raw) return { payload: {}, malformed: false };
|
|
6428
6716
|
try {
|
|
6429
6717
|
const parsed = JSON.parse(raw);
|
|
6430
|
-
return { payload:
|
|
6718
|
+
return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
|
|
6431
6719
|
} catch {
|
|
6432
6720
|
return { payload: {}, malformed: true };
|
|
6433
6721
|
}
|
|
@@ -6466,7 +6754,7 @@ function readPayload(options) {
|
|
|
6466
6754
|
if (options.stdinPayload != null) {
|
|
6467
6755
|
try {
|
|
6468
6756
|
const parsed = JSON.parse(options.stdinPayload);
|
|
6469
|
-
return { payload:
|
|
6757
|
+
return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
|
|
6470
6758
|
} catch {
|
|
6471
6759
|
return { payload: {}, malformed: true };
|
|
6472
6760
|
}
|
|
@@ -6479,15 +6767,14 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6479
6767
|
emitPreToolUseDeny("Malformed hook payload");
|
|
6480
6768
|
return;
|
|
6481
6769
|
}
|
|
6482
|
-
const toolInput =
|
|
6770
|
+
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
6483
6771
|
const command = String(toolInput.command ?? "");
|
|
6484
6772
|
if (!PR_CREATE_PATTERN.test(command)) return;
|
|
6485
6773
|
const root = process.cwd();
|
|
6486
6774
|
const baseRef = await resolveBaseRef(root);
|
|
6487
6775
|
if (!baseRef) return;
|
|
6488
6776
|
const range = `${baseRef}...HEAD`;
|
|
6489
|
-
const
|
|
6490
|
-
const commitMessages = (await runGit(["log", "--format=%B", logRange], { cwd: root })).stdout;
|
|
6777
|
+
const commitMessages = (await runGit(["log", "--format=%B", toAncestryLogRange(range)], { cwd: root })).stdout;
|
|
6491
6778
|
const result = await runDecisionsCheck(root, { range, commitMessages });
|
|
6492
6779
|
if (result.triggered && !result.satisfied) {
|
|
6493
6780
|
emitPreToolUseDeny(
|
|
@@ -6497,7 +6784,7 @@ async function runDecisionsGuard(options = {}) {
|
|
|
6497
6784
|
}
|
|
6498
6785
|
|
|
6499
6786
|
// src/commands/decisions.ts
|
|
6500
|
-
import
|
|
6787
|
+
import path44 from "node:path";
|
|
6501
6788
|
|
|
6502
6789
|
// src/decisions/next-number.ts
|
|
6503
6790
|
async function nextDecisionNumber(root) {
|
|
@@ -6572,7 +6859,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6572
6859
|
const root = process.cwd();
|
|
6573
6860
|
if (sub === "check") {
|
|
6574
6861
|
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 })
|
|
6862
|
+
(m) => m.runGit(["log", "--format=%B", toAncestryLogRange(options.range)], { cwd: root })
|
|
6576
6863
|
)).stdout : "";
|
|
6577
6864
|
const result = await runDecisionsCheck(root, {
|
|
6578
6865
|
staged: options.staged,
|
|
@@ -6607,7 +6894,7 @@ async function runDecisions(sub, arg, options = {}) {
|
|
|
6607
6894
|
return;
|
|
6608
6895
|
}
|
|
6609
6896
|
if (sub === "validate") {
|
|
6610
|
-
const target = arg ?
|
|
6897
|
+
const target = arg ? path44.resolve(root, arg) : void 0;
|
|
6611
6898
|
if (!target) {
|
|
6612
6899
|
error("usage: haus decisions validate <path-to-decision.md>");
|
|
6613
6900
|
process.exitCode = 1;
|
|
@@ -6859,7 +7146,7 @@ async function runGuard(kind, _options) {
|
|
|
6859
7146
|
emitPreToolUseDeny("Malformed hook payload");
|
|
6860
7147
|
return;
|
|
6861
7148
|
}
|
|
6862
|
-
const toolInput =
|
|
7149
|
+
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
6863
7150
|
if (kind === "file-access") {
|
|
6864
7151
|
const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
|
|
6865
7152
|
const reason2 = guardFileAccess(candidate);
|
|
@@ -6876,8 +7163,8 @@ async function runGuard(kind, _options) {
|
|
|
6876
7163
|
}
|
|
6877
7164
|
|
|
6878
7165
|
// src/commands/init.ts
|
|
6879
|
-
import
|
|
6880
|
-
import
|
|
7166
|
+
import path46 from "node:path";
|
|
7167
|
+
import fs33 from "fs-extra";
|
|
6881
7168
|
|
|
6882
7169
|
// src/recommender/git-signal.ts
|
|
6883
7170
|
init_exec();
|
|
@@ -6900,55 +7187,6 @@ async function readChangedFiles(root) {
|
|
|
6900
7187
|
return [.../* @__PURE__ */ new Set([...unstaged, ...staged, ...untracked])].sort();
|
|
6901
7188
|
}
|
|
6902
7189
|
|
|
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
7190
|
// src/recommender/recommend.ts
|
|
6953
7191
|
async function recommend(root, context, opts = {}) {
|
|
6954
7192
|
const items = await loadCatalog(root);
|
|
@@ -7265,6 +7503,7 @@ function applyCoInstallSuppression(recommended, skipped) {
|
|
|
7265
7503
|
}
|
|
7266
7504
|
var HARD_SKIP_CODES = /* @__PURE__ */ new Set([
|
|
7267
7505
|
"former-id",
|
|
7506
|
+
"invalid-source",
|
|
7268
7507
|
"unsupported-policy",
|
|
7269
7508
|
"deprecated",
|
|
7270
7509
|
"curated-not-approved",
|
|
@@ -7287,8 +7526,10 @@ function applyManualIncludes(include, items, recommended, skipped) {
|
|
|
7287
7526
|
if (recommendedIds.has(id)) continue;
|
|
7288
7527
|
const skipIdx = skipped.findIndex((s) => s.id === id);
|
|
7289
7528
|
const skipCode = skipIdx >= 0 ? skipped[skipIdx].skipReasons[0]?.code : void 0;
|
|
7290
|
-
|
|
7291
|
-
|
|
7529
|
+
const failingGateNames = skipIdx >= 0 ? (skipped[skipIdx].gates ?? []).filter((g) => !g.passed).map((g) => g.name) : [];
|
|
7530
|
+
const hardBlock = failingGateNames.find((name) => HARD_SKIP_CODES.has(name));
|
|
7531
|
+
if (hardBlock) {
|
|
7532
|
+
warnings.push(`--include: "${id}" cannot be force-installed (blocked by ${hardBlock})`);
|
|
7292
7533
|
continue;
|
|
7293
7534
|
}
|
|
7294
7535
|
if (skipCode === "requires-any-unsatisfied") {
|
|
@@ -7344,8 +7585,8 @@ function buildOptInEligible(items, skipped) {
|
|
|
7344
7585
|
}
|
|
7345
7586
|
|
|
7346
7587
|
// src/commands/workspace/discover.ts
|
|
7347
|
-
import
|
|
7348
|
-
import
|
|
7588
|
+
import path45 from "node:path";
|
|
7589
|
+
import fg5 from "fast-glob";
|
|
7349
7590
|
import YAML2 from "yaml";
|
|
7350
7591
|
var DEFAULT_MAX_DEPTH = 3;
|
|
7351
7592
|
var GIT_MARKER = ["**/.git"];
|
|
@@ -7379,16 +7620,16 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7379
7620
|
suppressErrors: true
|
|
7380
7621
|
};
|
|
7381
7622
|
const [gitMatches, manifestMatches] = await Promise.all([
|
|
7382
|
-
|
|
7383
|
-
|
|
7623
|
+
fg5(GIT_MARKER, { ...commonOpts, ignore: IGNORE }),
|
|
7624
|
+
fg5(MANIFEST_MARKERS, { ...commonOpts, ignore: [...IGNORE, "**/.git/**"] })
|
|
7384
7625
|
]);
|
|
7385
7626
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
7386
7627
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
7387
7628
|
for (const match of gitMatches) {
|
|
7388
|
-
gitDirs.add(
|
|
7629
|
+
gitDirs.add(path45.posix.dirname(match));
|
|
7389
7630
|
}
|
|
7390
7631
|
for (const match of manifestMatches) {
|
|
7391
|
-
manifestDirs.add(
|
|
7632
|
+
manifestDirs.add(path45.posix.dirname(match));
|
|
7392
7633
|
}
|
|
7393
7634
|
const repoRoots = [...gitDirs];
|
|
7394
7635
|
const manifestSorted = [...manifestDirs].sort(
|
|
@@ -7405,9 +7646,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
7405
7646
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
7406
7647
|
const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
|
|
7407
7648
|
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 :
|
|
7649
|
+
const absDir = path45.resolve(workspaceRoot, relDir);
|
|
7650
|
+
const pkg = await readJson(path45.join(absDir, "package.json"));
|
|
7651
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path45.basename(absDir);
|
|
7411
7652
|
let role = "auto";
|
|
7412
7653
|
try {
|
|
7413
7654
|
const scan = await scanProject(absDir);
|
|
@@ -7450,7 +7691,7 @@ function renderWorkspaceYaml(config) {
|
|
|
7450
7691
|
});
|
|
7451
7692
|
}
|
|
7452
7693
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
7453
|
-
const yamlPath =
|
|
7694
|
+
const yamlPath = path45.join(workspaceRoot, "haus.workspace.yaml");
|
|
7454
7695
|
const existingText = await readText(yamlPath);
|
|
7455
7696
|
const existing = parseWorkspaceConfig(existingText);
|
|
7456
7697
|
if (existingText && !existing) {
|
|
@@ -7628,8 +7869,8 @@ async function runSetupProject(options) {
|
|
|
7628
7869
|
// src/commands/init.ts
|
|
7629
7870
|
async function runInit(options) {
|
|
7630
7871
|
const root = process.cwd();
|
|
7631
|
-
const hausDir =
|
|
7632
|
-
const alreadyInit = await
|
|
7872
|
+
const hausDir = path46.join(root, ".haus-workflow");
|
|
7873
|
+
const alreadyInit = await fs33.pathExists(hausDir);
|
|
7633
7874
|
if (alreadyInit) {
|
|
7634
7875
|
log("Haus AI already initialized in this project.");
|
|
7635
7876
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -7764,8 +8005,8 @@ async function runRefresh() {
|
|
|
7764
8005
|
}
|
|
7765
8006
|
|
|
7766
8007
|
// src/install/scaffold.ts
|
|
7767
|
-
import
|
|
7768
|
-
import
|
|
8008
|
+
import path47 from "node:path";
|
|
8009
|
+
import fs34 from "fs-extra";
|
|
7769
8010
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
7770
8011
|
const scaffolded = [];
|
|
7771
8012
|
const skipped = [];
|
|
@@ -7778,27 +8019,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7778
8019
|
skipped.push(name);
|
|
7779
8020
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
7780
8021
|
};
|
|
7781
|
-
const realRoot = await
|
|
8022
|
+
const realRoot = await fs34.realpath(catalogRoot).catch(() => null);
|
|
7782
8023
|
for (const item of items) {
|
|
7783
8024
|
if (item.type !== "config") continue;
|
|
7784
|
-
const sourcePath =
|
|
7785
|
-
const rel =
|
|
7786
|
-
const escapes = rel === "" ||
|
|
8025
|
+
const sourcePath = path47.resolve(catalogRoot, item.path);
|
|
8026
|
+
const rel = path47.relative(catalogRoot, sourcePath);
|
|
8027
|
+
const escapes = rel === "" || path47.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
|
|
7787
8028
|
if (escapes) {
|
|
7788
8029
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
7789
8030
|
continue;
|
|
7790
8031
|
}
|
|
7791
|
-
const realSource = await
|
|
8032
|
+
const realSource = await fs34.realpath(sourcePath).catch(() => null);
|
|
7792
8033
|
if (!realRoot || !realSource) {
|
|
7793
8034
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
7794
8035
|
continue;
|
|
7795
8036
|
}
|
|
7796
|
-
const realRel =
|
|
7797
|
-
if (realRel !== "" && (
|
|
8037
|
+
const realRel = path47.relative(realRoot, realSource);
|
|
8038
|
+
if (realRel !== "" && (path47.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
|
|
7798
8039
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
7799
8040
|
continue;
|
|
7800
8041
|
}
|
|
7801
|
-
const stat = await
|
|
8042
|
+
const stat = await fs34.lstat(sourcePath).catch(() => null);
|
|
7802
8043
|
if (!stat) {
|
|
7803
8044
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
7804
8045
|
continue;
|
|
@@ -7808,14 +8049,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7808
8049
|
continue;
|
|
7809
8050
|
}
|
|
7810
8051
|
if (stat.isFile()) {
|
|
7811
|
-
const filename =
|
|
7812
|
-
const dest =
|
|
8052
|
+
const filename = path47.basename(sourcePath);
|
|
8053
|
+
const dest = path47.join(projectRoot, filename);
|
|
7813
8054
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
7814
8055
|
} else if (stat.isDirectory()) {
|
|
7815
|
-
const entries = await
|
|
8056
|
+
const entries = await fs34.readdir(sourcePath);
|
|
7816
8057
|
for (const entry of entries) {
|
|
7817
|
-
const src =
|
|
7818
|
-
const dest =
|
|
8058
|
+
const src = path47.join(sourcePath, entry);
|
|
8059
|
+
const dest = path47.join(projectRoot, entry);
|
|
7819
8060
|
record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
|
|
7820
8061
|
}
|
|
7821
8062
|
}
|
|
@@ -7823,42 +8064,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
7823
8064
|
return { scaffolded, skipped, skippedExisting };
|
|
7824
8065
|
}
|
|
7825
8066
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
7826
|
-
const destDisplay =
|
|
7827
|
-
const srcStat = await
|
|
8067
|
+
const destDisplay = path47.relative(projectRoot, dest) || path47.basename(dest);
|
|
8068
|
+
const srcStat = await fs34.lstat(src).catch(() => null);
|
|
7828
8069
|
if (!srcStat) {
|
|
7829
|
-
warn(`Skipping ${
|
|
8070
|
+
warn(`Skipping ${path47.basename(src)}: source not found`);
|
|
7830
8071
|
return "skipped-refused";
|
|
7831
8072
|
}
|
|
7832
8073
|
if (srcStat.isSymbolicLink()) {
|
|
7833
|
-
warn(`Skipping ${
|
|
8074
|
+
warn(`Skipping ${path47.basename(src)}: source is a symlink`);
|
|
7834
8075
|
return "skipped-refused";
|
|
7835
8076
|
}
|
|
7836
|
-
const exists = await
|
|
8077
|
+
const exists = await fs34.pathExists(dest);
|
|
7837
8078
|
if (exists && !opts.force) {
|
|
7838
8079
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
7839
8080
|
return "skipped-exists";
|
|
7840
8081
|
}
|
|
7841
8082
|
if (opts.dryRun) {
|
|
7842
|
-
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${
|
|
8083
|
+
log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path47.basename(dest)} (${itemId})`);
|
|
7843
8084
|
return "scaffolded";
|
|
7844
8085
|
}
|
|
7845
8086
|
if (exists && opts.force) {
|
|
7846
|
-
await
|
|
8087
|
+
await fs34.remove(dest);
|
|
7847
8088
|
}
|
|
7848
|
-
await
|
|
7849
|
-
await
|
|
8089
|
+
await fs34.ensureDir(path47.dirname(dest));
|
|
8090
|
+
await fs34.copy(src, dest, {
|
|
7850
8091
|
overwrite: true,
|
|
7851
8092
|
dereference: false,
|
|
7852
8093
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
7853
8094
|
filter: (s) => {
|
|
7854
8095
|
try {
|
|
7855
|
-
return !
|
|
8096
|
+
return !fs34.lstatSync(s).isSymbolicLink();
|
|
7856
8097
|
} catch {
|
|
7857
8098
|
return false;
|
|
7858
8099
|
}
|
|
7859
8100
|
}
|
|
7860
8101
|
});
|
|
7861
|
-
log(`${exists ? "Overwrote" : "Created"} ${
|
|
8102
|
+
log(`${exists ? "Overwrote" : "Created"} ${path47.basename(dest)} (${itemId})`);
|
|
7862
8103
|
return "scaffolded";
|
|
7863
8104
|
}
|
|
7864
8105
|
|
|
@@ -7923,8 +8164,8 @@ async function runScan(options) {
|
|
|
7923
8164
|
}
|
|
7924
8165
|
|
|
7925
8166
|
// src/commands/undo.ts
|
|
7926
|
-
import
|
|
7927
|
-
import
|
|
8167
|
+
import path48 from "node:path";
|
|
8168
|
+
import fs35 from "fs-extra";
|
|
7928
8169
|
|
|
7929
8170
|
// src/claude/managed-paths.ts
|
|
7930
8171
|
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md"];
|
|
@@ -7939,7 +8180,7 @@ function coreManagedAbsolutePaths(root) {
|
|
|
7939
8180
|
async function collectCoreManagedPaths(root) {
|
|
7940
8181
|
const existing = [];
|
|
7941
8182
|
for (const abs of coreManagedAbsolutePaths(root)) {
|
|
7942
|
-
if (await
|
|
8183
|
+
if (await fs35.pathExists(abs)) existing.push(abs);
|
|
7943
8184
|
}
|
|
7944
8185
|
return existing;
|
|
7945
8186
|
}
|
|
@@ -7951,18 +8192,18 @@ async function collectLockTrackedPaths(root) {
|
|
|
7951
8192
|
const relPaths = row.paths ?? [];
|
|
7952
8193
|
const existingRel = [];
|
|
7953
8194
|
for (const rel of relPaths) {
|
|
7954
|
-
if (await
|
|
8195
|
+
if (await fs35.pathExists(path48.resolve(root, rel))) existingRel.push(rel);
|
|
7955
8196
|
}
|
|
7956
8197
|
if (existingRel.length === 0) continue;
|
|
7957
8198
|
if (row.hash === void 0) {
|
|
7958
|
-
removable.push(...existingRel.map((rel) =>
|
|
8199
|
+
removable.push(...existingRel.map((rel) => path48.resolve(root, rel)));
|
|
7959
8200
|
continue;
|
|
7960
8201
|
}
|
|
7961
8202
|
const currentHash = await hashInstalledPaths(root, existingRel);
|
|
7962
8203
|
if (currentHash === row.hash) {
|
|
7963
|
-
removable.push(...existingRel.map((rel) =>
|
|
8204
|
+
removable.push(...existingRel.map((rel) => path48.resolve(root, rel)));
|
|
7964
8205
|
} else {
|
|
7965
|
-
preserved.push(...existingRel.map((rel) =>
|
|
8206
|
+
preserved.push(...existingRel.map((rel) => path48.resolve(root, rel)));
|
|
7966
8207
|
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
7967
8208
|
}
|
|
7968
8209
|
}
|
|
@@ -7970,51 +8211,51 @@ async function collectLockTrackedPaths(root) {
|
|
|
7970
8211
|
}
|
|
7971
8212
|
async function settingsHasHausContent(root) {
|
|
7972
8213
|
const settingsPath = claudePath(root, "settings.json");
|
|
7973
|
-
if (!await
|
|
8214
|
+
if (!await fs35.pathExists(settingsPath)) return false;
|
|
7974
8215
|
const settings = await readProjectSettings(root);
|
|
7975
8216
|
return settings._haus != null;
|
|
7976
8217
|
}
|
|
7977
8218
|
async function claudeMdHasHausBlock(root) {
|
|
7978
|
-
const filePath =
|
|
7979
|
-
if (!await
|
|
7980
|
-
const text = await
|
|
8219
|
+
const filePath = path48.join(root, "CLAUDE.md");
|
|
8220
|
+
if (!await fs35.pathExists(filePath)) return false;
|
|
8221
|
+
const text = await fs35.readFile(filePath, "utf8");
|
|
7981
8222
|
return text.includes(BLOCK_BEGIN);
|
|
7982
8223
|
}
|
|
7983
8224
|
async function stripProjectSettings(root) {
|
|
7984
8225
|
const settingsPath = claudePath(root, "settings.json");
|
|
7985
|
-
if (!await
|
|
8226
|
+
if (!await fs35.pathExists(settingsPath)) return false;
|
|
7986
8227
|
let settings = await readProjectSettings(root);
|
|
7987
8228
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
7988
8229
|
const hasContent = Object.keys(settings).length > 0;
|
|
7989
8230
|
if (hasContent) {
|
|
7990
8231
|
await writeProjectSettings(root, settings);
|
|
7991
|
-
log(`Stripped haus rules from ${
|
|
8232
|
+
log(`Stripped haus rules from ${path48.relative(root, settingsPath)} (user settings preserved).`);
|
|
7992
8233
|
return true;
|
|
7993
8234
|
}
|
|
7994
|
-
await
|
|
7995
|
-
log(`Removed ${
|
|
8235
|
+
await fs35.remove(settingsPath);
|
|
8236
|
+
log(`Removed ${path48.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
7996
8237
|
return true;
|
|
7997
8238
|
}
|
|
7998
8239
|
async function stripRootClaudeMd(root) {
|
|
7999
|
-
const filePath =
|
|
8000
|
-
if (!await
|
|
8001
|
-
const prev = await
|
|
8240
|
+
const filePath = path48.join(root, "CLAUDE.md");
|
|
8241
|
+
if (!await fs35.pathExists(filePath)) return false;
|
|
8242
|
+
const prev = await fs35.readFile(filePath, "utf8");
|
|
8002
8243
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
8003
8244
|
const next = stripHausBlock(prev);
|
|
8004
8245
|
if (next.length === 0) {
|
|
8005
|
-
await
|
|
8246
|
+
await fs35.remove(filePath);
|
|
8006
8247
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
8007
8248
|
} else {
|
|
8008
|
-
await
|
|
8249
|
+
await fs35.writeFile(filePath, next, "utf8");
|
|
8009
8250
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
8010
8251
|
}
|
|
8011
8252
|
return true;
|
|
8012
8253
|
}
|
|
8013
8254
|
async function pruneDirIfEmpty(dir) {
|
|
8014
|
-
if (!await
|
|
8255
|
+
if (!await fs35.pathExists(dir)) return;
|
|
8015
8256
|
try {
|
|
8016
|
-
const entries = await
|
|
8017
|
-
if (entries.length === 0) await
|
|
8257
|
+
const entries = await fs35.readdir(dir);
|
|
8258
|
+
if (entries.length === 0) await fs35.remove(dir);
|
|
8018
8259
|
} catch {
|
|
8019
8260
|
}
|
|
8020
8261
|
}
|
|
@@ -8023,13 +8264,13 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
|
8023
8264
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
8024
8265
|
const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
|
|
8025
8266
|
for (const abs of managedAbsPaths) {
|
|
8026
|
-
if (!await
|
|
8027
|
-
const rel =
|
|
8028
|
-
const backupPath =
|
|
8029
|
-
await
|
|
8030
|
-
await
|
|
8267
|
+
if (!await fs35.pathExists(abs)) continue;
|
|
8268
|
+
const rel = path48.relative(root, abs);
|
|
8269
|
+
const backupPath = path48.join(backupRoot, rel);
|
|
8270
|
+
await fs35.ensureDir(path48.dirname(backupPath));
|
|
8271
|
+
await fs35.copy(abs, backupPath);
|
|
8031
8272
|
}
|
|
8032
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
8273
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path48.relative(root, backupRoot)}.`);
|
|
8033
8274
|
}
|
|
8034
8275
|
async function runUndo(options) {
|
|
8035
8276
|
const root = options.root ?? process.cwd();
|
|
@@ -8042,7 +8283,7 @@ async function runUndo(options) {
|
|
|
8042
8283
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
8043
8284
|
return;
|
|
8044
8285
|
}
|
|
8045
|
-
const relTargets = managed.map((p) =>
|
|
8286
|
+
const relTargets = managed.map((p) => path48.relative(root, p));
|
|
8046
8287
|
const summaryParts = [...relTargets];
|
|
8047
8288
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
8048
8289
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -8064,9 +8305,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
8064
8305
|
}
|
|
8065
8306
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
8066
8307
|
for (const abs of managed) {
|
|
8067
|
-
if (!await
|
|
8068
|
-
await
|
|
8069
|
-
log(`Removed ${
|
|
8308
|
+
if (!await fs35.pathExists(abs)) continue;
|
|
8309
|
+
await fs35.remove(abs);
|
|
8310
|
+
log(`Removed ${path48.relative(root, abs)}`);
|
|
8070
8311
|
}
|
|
8071
8312
|
if (stripSettings) await stripProjectSettings(root);
|
|
8072
8313
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -8077,8 +8318,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
8077
8318
|
|
|
8078
8319
|
// src/install/uninstall.ts
|
|
8079
8320
|
import crypto3 from "node:crypto";
|
|
8080
|
-
import
|
|
8081
|
-
import
|
|
8321
|
+
import path49 from "node:path";
|
|
8322
|
+
import fs36 from "fs-extra";
|
|
8082
8323
|
async function runUninstall(options = {}) {
|
|
8083
8324
|
const { force = false } = options;
|
|
8084
8325
|
const manifest = await readManifest();
|
|
@@ -8088,7 +8329,7 @@ async function runUninstall(options = {}) {
|
|
|
8088
8329
|
return result;
|
|
8089
8330
|
}
|
|
8090
8331
|
for (const entry of manifest.files) {
|
|
8091
|
-
const exists =
|
|
8332
|
+
const exists = fs36.pathExistsSync(entry.destPath);
|
|
8092
8333
|
if (!exists) continue;
|
|
8093
8334
|
const content2 = await readText(entry.destPath);
|
|
8094
8335
|
if (content2 === void 0) continue;
|
|
@@ -8106,23 +8347,23 @@ async function runUninstall(options = {}) {
|
|
|
8106
8347
|
result.skipped.push(entry.destPath);
|
|
8107
8348
|
continue;
|
|
8108
8349
|
}
|
|
8109
|
-
await
|
|
8110
|
-
await pruneEmptyDir(
|
|
8350
|
+
await fs36.remove(entry.destPath);
|
|
8351
|
+
await pruneEmptyDir(path49.dirname(entry.destPath));
|
|
8111
8352
|
result.deleted.push(entry.destPath);
|
|
8112
8353
|
}
|
|
8113
8354
|
const settings = await readSettings();
|
|
8114
8355
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
8115
8356
|
await writeSettings(stripped);
|
|
8116
8357
|
result.hooksStripped = true;
|
|
8117
|
-
const hausDir =
|
|
8358
|
+
const hausDir = path49.join(globalClaudeDir(), "haus");
|
|
8118
8359
|
const manifestPath2 = hausManifestPath();
|
|
8119
|
-
if (
|
|
8120
|
-
await
|
|
8360
|
+
if (fs36.pathExistsSync(manifestPath2)) {
|
|
8361
|
+
await fs36.remove(manifestPath2);
|
|
8121
8362
|
}
|
|
8122
|
-
if (
|
|
8363
|
+
if (fs36.pathExistsSync(hausDir)) {
|
|
8123
8364
|
try {
|
|
8124
|
-
const remaining = await
|
|
8125
|
-
if (remaining.length === 0) await
|
|
8365
|
+
const remaining = await fs36.readdir(hausDir);
|
|
8366
|
+
if (remaining.length === 0) await fs36.remove(hausDir);
|
|
8126
8367
|
} catch {
|
|
8127
8368
|
}
|
|
8128
8369
|
}
|
|
@@ -8145,6 +8386,19 @@ function printUninstallResult(result) {
|
|
|
8145
8386
|
// src/commands/uninstall.ts
|
|
8146
8387
|
async function runUninstallCommand(options) {
|
|
8147
8388
|
try {
|
|
8389
|
+
if (!options.yes) {
|
|
8390
|
+
const manifest = await readManifest();
|
|
8391
|
+
if (manifest) {
|
|
8392
|
+
const fileCount = manifest.files.length;
|
|
8393
|
+
const ok = await confirm(
|
|
8394
|
+
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?"
|
|
8395
|
+
);
|
|
8396
|
+
if (!ok) {
|
|
8397
|
+
log("Cancelled.");
|
|
8398
|
+
return;
|
|
8399
|
+
}
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8148
8402
|
const result = await runUninstall({ force: options.force });
|
|
8149
8403
|
printUninstallResult(result);
|
|
8150
8404
|
log("haus uninstall complete");
|
|
@@ -8155,11 +8409,11 @@ async function runUninstallCommand(options) {
|
|
|
8155
8409
|
}
|
|
8156
8410
|
|
|
8157
8411
|
// src/commands/validate-catalog.ts
|
|
8158
|
-
import
|
|
8412
|
+
import path51 from "node:path";
|
|
8159
8413
|
|
|
8160
8414
|
// src/catalog/validate-core.ts
|
|
8161
|
-
import
|
|
8162
|
-
import
|
|
8415
|
+
import fs37 from "node:fs";
|
|
8416
|
+
import path50 from "node:path";
|
|
8163
8417
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
8164
8418
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
8165
8419
|
function auditForbiddenStacks(items) {
|
|
@@ -8301,14 +8555,14 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
8301
8555
|
}
|
|
8302
8556
|
function readForAudit(absPath, label, failures) {
|
|
8303
8557
|
try {
|
|
8304
|
-
return
|
|
8558
|
+
return fs37.readFileSync(absPath, "utf8");
|
|
8305
8559
|
} catch (err) {
|
|
8306
8560
|
failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
8307
8561
|
return null;
|
|
8308
8562
|
}
|
|
8309
8563
|
}
|
|
8310
8564
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
8311
|
-
const rel =
|
|
8565
|
+
const rel = path50.relative(manifestDir, absPath);
|
|
8312
8566
|
const failures = [];
|
|
8313
8567
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
8314
8568
|
if (text === null) return failures;
|
|
@@ -8330,47 +8584,47 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
8330
8584
|
const failures = [];
|
|
8331
8585
|
for (const item of items) {
|
|
8332
8586
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
8333
|
-
const absPath =
|
|
8587
|
+
const absPath = path50.join(manifestDir, item.path);
|
|
8334
8588
|
if (item.type === "skill") {
|
|
8335
|
-
const skillMd =
|
|
8336
|
-
if (!
|
|
8337
|
-
failures.push(`${item.id}: missing ${
|
|
8589
|
+
const skillMd = path50.join(absPath, "SKILL.md");
|
|
8590
|
+
if (!fs37.existsSync(skillMd)) {
|
|
8591
|
+
failures.push(`${item.id}: missing ${path50.relative(manifestDir, skillMd)}`);
|
|
8338
8592
|
continue;
|
|
8339
8593
|
}
|
|
8340
8594
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
8341
8595
|
if (text === null) continue;
|
|
8342
8596
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
8343
8597
|
failures.push(
|
|
8344
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
8598
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path50.relative(manifestDir, skillMd)}`)
|
|
8345
8599
|
);
|
|
8346
8600
|
} else if (item.type === "agent") {
|
|
8347
|
-
if (!
|
|
8601
|
+
if (!fs37.existsSync(absPath)) {
|
|
8348
8602
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
8349
8603
|
continue;
|
|
8350
8604
|
}
|
|
8351
|
-
const rel =
|
|
8605
|
+
const rel = path50.relative(manifestDir, absPath);
|
|
8352
8606
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8353
8607
|
if (text === null) continue;
|
|
8354
8608
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
8355
8609
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
8356
8610
|
} else if (item.type === "template") {
|
|
8357
|
-
if (!
|
|
8611
|
+
if (!fs37.existsSync(absPath)) {
|
|
8358
8612
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
8359
8613
|
continue;
|
|
8360
8614
|
}
|
|
8361
8615
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
8362
8616
|
} else if (item.type === "command") {
|
|
8363
|
-
if (!
|
|
8617
|
+
if (!fs37.existsSync(absPath)) {
|
|
8364
8618
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
8365
8619
|
continue;
|
|
8366
8620
|
}
|
|
8367
|
-
const rel =
|
|
8621
|
+
const rel = path50.relative(manifestDir, absPath);
|
|
8368
8622
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
8369
8623
|
if (text === null) continue;
|
|
8370
8624
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
8371
8625
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
8372
8626
|
} else if (item.type === "config") {
|
|
8373
|
-
if (!
|
|
8627
|
+
if (!fs37.existsSync(absPath)) {
|
|
8374
8628
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
8375
8629
|
}
|
|
8376
8630
|
}
|
|
@@ -8387,7 +8641,7 @@ var CONTENT_ROOTS = ["skills", "agents", "templates", "commands", "configs"];
|
|
|
8387
8641
|
function walkAllFiles(dir, fn, failures) {
|
|
8388
8642
|
let entries;
|
|
8389
8643
|
try {
|
|
8390
|
-
entries =
|
|
8644
|
+
entries = fs37.readdirSync(dir, { withFileTypes: true });
|
|
8391
8645
|
} catch (err) {
|
|
8392
8646
|
failures.push(
|
|
8393
8647
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -8396,7 +8650,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
8396
8650
|
}
|
|
8397
8651
|
for (const entry of entries) {
|
|
8398
8652
|
if (entry.name === ".DS_Store") continue;
|
|
8399
|
-
const full =
|
|
8653
|
+
const full = path50.join(dir, entry.name);
|
|
8400
8654
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
8401
8655
|
else fn(full);
|
|
8402
8656
|
}
|
|
@@ -8414,12 +8668,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
8414
8668
|
const failures = [];
|
|
8415
8669
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
8416
8670
|
for (const dir of CONTENT_ROOTS) {
|
|
8417
|
-
const abs =
|
|
8418
|
-
if (!
|
|
8671
|
+
const abs = path50.join(manifestDir, dir);
|
|
8672
|
+
if (!fs37.existsSync(abs)) continue;
|
|
8419
8673
|
walkAllFiles(
|
|
8420
8674
|
abs,
|
|
8421
8675
|
(file) => {
|
|
8422
|
-
const relPath =
|
|
8676
|
+
const relPath = path50.relative(manifestDir, file).replace(/\\/g, "/");
|
|
8423
8677
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
8424
8678
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
8425
8679
|
}
|
|
@@ -8432,7 +8686,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
8432
8686
|
function walkMd(dir, fn, failures) {
|
|
8433
8687
|
let entries;
|
|
8434
8688
|
try {
|
|
8435
|
-
entries =
|
|
8689
|
+
entries = fs37.readdirSync(dir, { withFileTypes: true });
|
|
8436
8690
|
} catch (err) {
|
|
8437
8691
|
failures.push(
|
|
8438
8692
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -8440,7 +8694,7 @@ function walkMd(dir, fn, failures) {
|
|
|
8440
8694
|
return;
|
|
8441
8695
|
}
|
|
8442
8696
|
for (const entry of entries) {
|
|
8443
|
-
const full =
|
|
8697
|
+
const full = path50.join(dir, entry.name);
|
|
8444
8698
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
8445
8699
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
8446
8700
|
}
|
|
@@ -8449,13 +8703,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
8449
8703
|
const failures = [];
|
|
8450
8704
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
8451
8705
|
for (const dir of ["skills", "agents", "templates", "commands"]) {
|
|
8452
|
-
const abs =
|
|
8453
|
-
if (!
|
|
8706
|
+
const abs = path50.join(manifestDir, dir);
|
|
8707
|
+
if (!fs37.existsSync(abs)) continue;
|
|
8454
8708
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
8455
8709
|
walkMd(
|
|
8456
8710
|
abs,
|
|
8457
8711
|
(file) => {
|
|
8458
|
-
const rel =
|
|
8712
|
+
const rel = path50.relative(manifestDir, file);
|
|
8459
8713
|
const text = readForAudit(file, rel, failures);
|
|
8460
8714
|
if (text === null) return;
|
|
8461
8715
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -8500,8 +8754,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8500
8754
|
process.exitCode = 1;
|
|
8501
8755
|
return;
|
|
8502
8756
|
}
|
|
8503
|
-
const abs =
|
|
8504
|
-
const manifestDir =
|
|
8757
|
+
const abs = path51.resolve(process.cwd(), manifestPath2);
|
|
8758
|
+
const manifestDir = path51.dirname(abs);
|
|
8505
8759
|
const data = await readJson(abs);
|
|
8506
8760
|
if (!data?.items) {
|
|
8507
8761
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -8518,8 +8772,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
8518
8772
|
}
|
|
8519
8773
|
|
|
8520
8774
|
// src/workspace/worktree/add.ts
|
|
8521
|
-
import
|
|
8522
|
-
import
|
|
8775
|
+
import path58 from "node:path";
|
|
8776
|
+
import fs41 from "fs-extra";
|
|
8523
8777
|
|
|
8524
8778
|
// src/workspace/worktree/git-worktree.ts
|
|
8525
8779
|
init_exec();
|
|
@@ -8638,13 +8892,13 @@ async function hasUnpushedWork(cwd) {
|
|
|
8638
8892
|
}
|
|
8639
8893
|
|
|
8640
8894
|
// src/workspace/worktree/hydrate.ts
|
|
8641
|
-
import
|
|
8642
|
-
import
|
|
8895
|
+
import path54 from "node:path";
|
|
8896
|
+
import fs39 from "fs-extra";
|
|
8643
8897
|
|
|
8644
8898
|
// src/workspace/worktree/cow-copy.ts
|
|
8645
8899
|
init_exec();
|
|
8646
|
-
import
|
|
8647
|
-
import
|
|
8900
|
+
import path52 from "node:path";
|
|
8901
|
+
import fs38 from "fs-extra";
|
|
8648
8902
|
async function getMacFilesystemType(dir) {
|
|
8649
8903
|
const df = await runCommand("df", ["-P", dir]);
|
|
8650
8904
|
if (df.exitCode !== 0) return void 0;
|
|
@@ -8675,14 +8929,14 @@ async function detectCowStrategy(dir) {
|
|
|
8675
8929
|
return "unknown-platform";
|
|
8676
8930
|
}
|
|
8677
8931
|
async function cowCopyDir(src, dest) {
|
|
8678
|
-
const srcExists = await
|
|
8932
|
+
const srcExists = await fs38.pathExists(src);
|
|
8679
8933
|
if (!srcExists)
|
|
8680
8934
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
8681
|
-
const strategy = await detectCowStrategy(
|
|
8935
|
+
const strategy = await detectCowStrategy(path52.dirname(dest));
|
|
8682
8936
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
8683
8937
|
return { strategy, attempted: false, ok: false };
|
|
8684
8938
|
}
|
|
8685
|
-
await
|
|
8939
|
+
await fs38.ensureDir(path52.dirname(dest));
|
|
8686
8940
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
8687
8941
|
const result = await runCommand("cp", args);
|
|
8688
8942
|
return {
|
|
@@ -8696,8 +8950,8 @@ async function cowCopyDir(src, dest) {
|
|
|
8696
8950
|
// src/workspace/worktree/install.ts
|
|
8697
8951
|
init_exec();
|
|
8698
8952
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
8699
|
-
import
|
|
8700
|
-
import
|
|
8953
|
+
import path53 from "node:path";
|
|
8954
|
+
import fg6 from "fast-glob";
|
|
8701
8955
|
function detectInstallPlan(signals) {
|
|
8702
8956
|
if (signals.hasYarnLock) {
|
|
8703
8957
|
return signals.hasPackageManagerField ? { manager: "yarn", command: "corepack", args: ["yarn", "install"] } : { manager: "yarn", command: "yarn", args: ["install"] };
|
|
@@ -8717,11 +8971,11 @@ function detectInstallPlan(signals) {
|
|
|
8717
8971
|
return { manager: "none" };
|
|
8718
8972
|
}
|
|
8719
8973
|
async function computeLockfileSignals(dir) {
|
|
8720
|
-
const hasYarnLock = existsSync3(
|
|
8721
|
-
const hasPnpmLock = existsSync3(
|
|
8722
|
-
const hasPackageLock = existsSync3(
|
|
8723
|
-
const hasComposerLock = existsSync3(
|
|
8724
|
-
const dotnetFiles = await
|
|
8974
|
+
const hasYarnLock = existsSync3(path53.join(dir, "yarn.lock"));
|
|
8975
|
+
const hasPnpmLock = existsSync3(path53.join(dir, "pnpm-lock.yaml"));
|
|
8976
|
+
const hasPackageLock = existsSync3(path53.join(dir, "package-lock.json"));
|
|
8977
|
+
const hasComposerLock = existsSync3(path53.join(dir, "composer.lock"));
|
|
8978
|
+
const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
|
|
8725
8979
|
cwd: dir,
|
|
8726
8980
|
onlyFiles: true,
|
|
8727
8981
|
suppressErrors: true,
|
|
@@ -8734,7 +8988,7 @@ async function computeLockfileSignals(dir) {
|
|
|
8734
8988
|
});
|
|
8735
8989
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
8736
8990
|
let hasPackageManagerField = false;
|
|
8737
|
-
const pkgPath =
|
|
8991
|
+
const pkgPath = path53.join(dir, "package.json");
|
|
8738
8992
|
if (existsSync3(pkgPath)) {
|
|
8739
8993
|
try {
|
|
8740
8994
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -8775,13 +9029,13 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
8775
9029
|
const start = Date.now();
|
|
8776
9030
|
const targets = [];
|
|
8777
9031
|
for (const target of HYDRATION_TARGETS) {
|
|
8778
|
-
const src =
|
|
8779
|
-
const dest =
|
|
8780
|
-
if (!await
|
|
8781
|
-
const destExists = await
|
|
9032
|
+
const src = path54.join(mainMemberPath, target);
|
|
9033
|
+
const dest = path54.join(worktreeMemberPath, target);
|
|
9034
|
+
if (!await fs39.pathExists(src)) continue;
|
|
9035
|
+
const destExists = await fs39.pathExists(dest);
|
|
8782
9036
|
if (!opts.force && destExists) continue;
|
|
8783
9037
|
if (opts.force && destExists) {
|
|
8784
|
-
await
|
|
9038
|
+
await fs39.remove(dest);
|
|
8785
9039
|
}
|
|
8786
9040
|
const copy = await cowCopyDir(src, dest);
|
|
8787
9041
|
targets.push({ target, copy });
|
|
@@ -8794,11 +9048,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
8794
9048
|
|
|
8795
9049
|
// src/workspace/worktree/local-files.ts
|
|
8796
9050
|
init_exec();
|
|
8797
|
-
import
|
|
8798
|
-
import
|
|
9051
|
+
import path55 from "node:path";
|
|
9052
|
+
import fs40 from "fs-extra";
|
|
8799
9053
|
var MACHINE_LOCAL_FILES = [
|
|
8800
9054
|
"docker-compose.local.yml",
|
|
8801
|
-
|
|
9055
|
+
path55.join(".claude", "settings.local.json")
|
|
8802
9056
|
];
|
|
8803
9057
|
async function isTracked(repoRoot, relFile) {
|
|
8804
9058
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -8808,12 +9062,12 @@ async function isTracked(repoRoot, relFile) {
|
|
|
8808
9062
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
8809
9063
|
const copied = [];
|
|
8810
9064
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
8811
|
-
const src =
|
|
8812
|
-
if (!await
|
|
9065
|
+
const src = path55.join(mainMemberPath, rel);
|
|
9066
|
+
if (!await fs40.pathExists(src)) continue;
|
|
8813
9067
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
8814
|
-
const dest =
|
|
8815
|
-
await
|
|
8816
|
-
await
|
|
9068
|
+
const dest = path55.join(worktreeMemberPath, rel);
|
|
9069
|
+
await fs40.ensureDir(path55.dirname(dest));
|
|
9070
|
+
await fs40.copy(src, dest, { overwrite: true });
|
|
8817
9071
|
copied.push(rel);
|
|
8818
9072
|
}
|
|
8819
9073
|
return copied;
|
|
@@ -8821,15 +9075,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
8821
9075
|
|
|
8822
9076
|
// src/workspace/worktree/root.ts
|
|
8823
9077
|
import { existsSync as existsSync4 } from "node:fs";
|
|
8824
|
-
import
|
|
9078
|
+
import path56 from "node:path";
|
|
8825
9079
|
async function resolveWorkspaceForWorktree(start) {
|
|
8826
9080
|
const rootInfo = await resolveRoots(start);
|
|
8827
9081
|
if (!rootInfo.isGitRepo) {
|
|
8828
9082
|
return { ok: false, reason: "Not inside a git repository." };
|
|
8829
9083
|
}
|
|
8830
9084
|
const workspaceRoot = rootInfo.mainRoot;
|
|
8831
|
-
const hasYaml = existsSync4(
|
|
8832
|
-
const hasManifest = existsSync4(
|
|
9085
|
+
const hasYaml = existsSync4(path56.join(workspaceRoot, WORKSPACE_FILE));
|
|
9086
|
+
const hasManifest = existsSync4(path56.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
8833
9087
|
if (!hasYaml && !hasManifest) {
|
|
8834
9088
|
return {
|
|
8835
9089
|
ok: false,
|
|
@@ -8854,19 +9108,31 @@ function selectMembers(members, only) {
|
|
|
8854
9108
|
}
|
|
8855
9109
|
|
|
8856
9110
|
// src/workspace/worktree/state.ts
|
|
8857
|
-
import
|
|
9111
|
+
import path57 from "node:path";
|
|
8858
9112
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
8859
9113
|
function worktreesDir(mainRoot) {
|
|
8860
|
-
return
|
|
9114
|
+
return path57.join(mainRoot, ".claude", "worktrees");
|
|
9115
|
+
}
|
|
9116
|
+
function isValidWorktreeSlug(slug) {
|
|
9117
|
+
if (slug === "" || slug === "." || slug === "..") return false;
|
|
9118
|
+
if (slug.includes("/") || slug.includes("\\")) return false;
|
|
9119
|
+
return /^[A-Za-z0-9._-]+$/.test(slug);
|
|
8861
9120
|
}
|
|
8862
9121
|
function worktreePath(mainRoot, slug) {
|
|
8863
|
-
|
|
9122
|
+
if (!isValidWorktreeSlug(slug)) {
|
|
9123
|
+
throw new Error(
|
|
9124
|
+
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
9125
|
+
);
|
|
9126
|
+
}
|
|
9127
|
+
return path57.join(worktreesDir(mainRoot), slug);
|
|
8864
9128
|
}
|
|
8865
9129
|
async function readWorktreeState(worktreeRoot) {
|
|
8866
|
-
|
|
9130
|
+
const state = await readJson(path57.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
9131
|
+
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
9132
|
+
return state;
|
|
8867
9133
|
}
|
|
8868
9134
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
8869
|
-
await writeJson(
|
|
9135
|
+
await writeJson(path57.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
8870
9136
|
}
|
|
8871
9137
|
|
|
8872
9138
|
// src/workspace/worktree/add.ts
|
|
@@ -8881,7 +9147,7 @@ async function runAdd(opts) {
|
|
|
8881
9147
|
}
|
|
8882
9148
|
const branch = opts.branch ?? opts.slug;
|
|
8883
9149
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
8884
|
-
if (await
|
|
9150
|
+
if (await fs41.pathExists(wsWorktreePath)) {
|
|
8885
9151
|
return {
|
|
8886
9152
|
ok: false,
|
|
8887
9153
|
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 +9166,7 @@ async function runAdd(opts) {
|
|
|
8900
9166
|
const memberResults = [];
|
|
8901
9167
|
for (const member of selected) {
|
|
8902
9168
|
const start = Date.now();
|
|
8903
|
-
const memberWorktreePath =
|
|
9169
|
+
const memberWorktreePath = path58.join(wsWorktreePath, member.folder);
|
|
8904
9170
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
8905
9171
|
preferDefaultBranchFrom: true
|
|
8906
9172
|
});
|
|
@@ -8930,7 +9196,7 @@ async function runAdd(opts) {
|
|
|
8930
9196
|
await mapWithConcurrency(okResults, async (result) => {
|
|
8931
9197
|
const member = selected.find((m) => m.id === result.member);
|
|
8932
9198
|
if (!member) return;
|
|
8933
|
-
const memberWorktreePath =
|
|
9199
|
+
const memberWorktreePath = path58.join(wsWorktreePath, member.folder);
|
|
8934
9200
|
const hydrationStart = Date.now();
|
|
8935
9201
|
if (opts.hydrate !== false) {
|
|
8936
9202
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -8997,10 +9263,10 @@ async function runDryRun(args) {
|
|
|
8997
9263
|
|
|
8998
9264
|
// src/workspace/worktree/doctor.ts
|
|
8999
9265
|
import { existsSync as existsSync5 } from "node:fs";
|
|
9000
|
-
import
|
|
9266
|
+
import path59 from "node:path";
|
|
9001
9267
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
9002
9268
|
function looksHydrated(memberPath) {
|
|
9003
|
-
return HYDRATION_MARKERS.some((marker) => existsSync5(
|
|
9269
|
+
return HYDRATION_MARKERS.some((marker) => existsSync5(path59.join(memberPath, marker)));
|
|
9004
9270
|
}
|
|
9005
9271
|
async function runDoctor2() {
|
|
9006
9272
|
const rootInfo = await resolveRoots();
|
|
@@ -9014,7 +9280,7 @@ async function runDoctor2() {
|
|
|
9014
9280
|
};
|
|
9015
9281
|
}
|
|
9016
9282
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9017
|
-
const isWorkspace = existsSync5(
|
|
9283
|
+
const isWorkspace = existsSync5(path59.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path59.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
9018
9284
|
if (!isWorkspace) {
|
|
9019
9285
|
return {
|
|
9020
9286
|
isWorkspace: false,
|
|
@@ -9027,7 +9293,7 @@ async function runDoctor2() {
|
|
|
9027
9293
|
};
|
|
9028
9294
|
}
|
|
9029
9295
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
9030
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
9296
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path59.sep}`);
|
|
9031
9297
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
9032
9298
|
const members = await readMembers(rootInfo);
|
|
9033
9299
|
const problems = [];
|
|
@@ -9035,10 +9301,10 @@ async function runDoctor2() {
|
|
|
9035
9301
|
let slug;
|
|
9036
9302
|
if (inWorkspaceWorktree) {
|
|
9037
9303
|
const wtPath = rootInfo.repoRoot;
|
|
9038
|
-
slug = rootInfo.worktreeName ??
|
|
9304
|
+
slug = rootInfo.worktreeName ?? path59.basename(wtPath);
|
|
9039
9305
|
const state = await readWorktreeState(wtPath);
|
|
9040
9306
|
for (const member of members) {
|
|
9041
|
-
const memberPath =
|
|
9307
|
+
const memberPath = path59.join(wtPath, member.folder);
|
|
9042
9308
|
const materialized = existsSync5(memberPath);
|
|
9043
9309
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
9044
9310
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -9069,16 +9335,16 @@ async function runDoctor2() {
|
|
|
9069
9335
|
for (const member of members) {
|
|
9070
9336
|
const entries = await listWorktrees(member.absPath);
|
|
9071
9337
|
for (const entry of entries) {
|
|
9072
|
-
const entryPath =
|
|
9073
|
-
const marker = `${
|
|
9338
|
+
const entryPath = path59.normalize(entry.path);
|
|
9339
|
+
const marker = `${path59.sep}.claude${path59.sep}worktrees${path59.sep}`;
|
|
9074
9340
|
const idx = entryPath.indexOf(marker);
|
|
9075
9341
|
if (idx === -1) continue;
|
|
9076
9342
|
const rest = entryPath.slice(idx + marker.length);
|
|
9077
|
-
const slugName = rest.split(
|
|
9343
|
+
const slugName = rest.split(path59.sep)[0];
|
|
9078
9344
|
if (!slugName) continue;
|
|
9079
|
-
if (!existsSync5(
|
|
9345
|
+
if (!existsSync5(path59.join(wsWorktreesDir, slugName))) {
|
|
9080
9346
|
orphans.push(
|
|
9081
|
-
`${member.id}: worktree registered at ${entry.path} but ${
|
|
9347
|
+
`${member.id}: worktree registered at ${entry.path} but ${path59.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
|
|
9082
9348
|
);
|
|
9083
9349
|
}
|
|
9084
9350
|
}
|
|
@@ -9089,7 +9355,7 @@ async function runDoctor2() {
|
|
|
9089
9355
|
|
|
9090
9356
|
// src/workspace/worktree/hydrate-command.ts
|
|
9091
9357
|
import { existsSync as existsSync6 } from "node:fs";
|
|
9092
|
-
import
|
|
9358
|
+
import path60 from "node:path";
|
|
9093
9359
|
async function runHydrateWorktree(opts) {
|
|
9094
9360
|
const rootInfo = await resolveRoots();
|
|
9095
9361
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -9100,7 +9366,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9100
9366
|
}
|
|
9101
9367
|
const workspaceRoot = rootInfo.mainRoot;
|
|
9102
9368
|
const wtPath = rootInfo.repoRoot;
|
|
9103
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
9369
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path60.sep}`)) {
|
|
9104
9370
|
return {
|
|
9105
9371
|
ok: false,
|
|
9106
9372
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -9112,13 +9378,13 @@ async function runHydrateWorktree(opts) {
|
|
|
9112
9378
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
9113
9379
|
}
|
|
9114
9380
|
const state = await readWorktreeState(wtPath);
|
|
9115
|
-
const slug = state?.slug ??
|
|
9381
|
+
const slug = state?.slug ?? path60.basename(wtPath);
|
|
9116
9382
|
const detectedBranch = await currentBranch(wtPath);
|
|
9117
9383
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
9118
9384
|
if (opts.dryRun) {
|
|
9119
9385
|
const results2 = await Promise.all(
|
|
9120
9386
|
selected.map(async (member) => {
|
|
9121
|
-
const memberPath =
|
|
9387
|
+
const memberPath = path60.join(wtPath, member.folder);
|
|
9122
9388
|
if (existsSync6(memberPath)) {
|
|
9123
9389
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
9124
9390
|
}
|
|
@@ -9139,7 +9405,7 @@ async function runHydrateWorktree(opts) {
|
|
|
9139
9405
|
const results = await mapWithConcurrency(
|
|
9140
9406
|
selected,
|
|
9141
9407
|
async (member) => {
|
|
9142
|
-
const memberPath =
|
|
9408
|
+
const memberPath = path60.join(wtPath, member.folder);
|
|
9143
9409
|
let branchAction;
|
|
9144
9410
|
if (!existsSync6(memberPath)) {
|
|
9145
9411
|
if (!opts.materialize) {
|
|
@@ -9202,26 +9468,26 @@ async function runHydrateWorktree(opts) {
|
|
|
9202
9468
|
}
|
|
9203
9469
|
|
|
9204
9470
|
// src/workspace/worktree/list.ts
|
|
9205
|
-
import
|
|
9206
|
-
import
|
|
9471
|
+
import path61 from "node:path";
|
|
9472
|
+
import fs42 from "fs-extra";
|
|
9207
9473
|
async function runList2() {
|
|
9208
9474
|
const resolved = await resolveWorkspaceForWorktree();
|
|
9209
9475
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
9210
9476
|
const { rootInfo, workspaceRoot } = resolved;
|
|
9211
9477
|
const dir = worktreesDir(workspaceRoot);
|
|
9212
|
-
if (!await
|
|
9213
|
-
const entries = await
|
|
9478
|
+
if (!await fs42.pathExists(dir)) return { ok: true, workspaceRoot, worktrees: [] };
|
|
9479
|
+
const entries = await fs42.readdir(dir, { withFileTypes: true });
|
|
9214
9480
|
const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9215
9481
|
const configuredMembers = await readMembers(rootInfo);
|
|
9216
9482
|
const worktrees = [];
|
|
9217
9483
|
for (const slug of slugs) {
|
|
9218
|
-
const wtPath =
|
|
9484
|
+
const wtPath = path61.join(dir, slug);
|
|
9219
9485
|
const state = await readWorktreeState(wtPath);
|
|
9220
9486
|
const members = [];
|
|
9221
9487
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
9222
9488
|
for (const m of source) {
|
|
9223
|
-
const memberPath =
|
|
9224
|
-
const materialized = await
|
|
9489
|
+
const memberPath = path61.join(wtPath, m.folder);
|
|
9490
|
+
const materialized = await fs42.pathExists(memberPath);
|
|
9225
9491
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
9226
9492
|
members.push({
|
|
9227
9493
|
id: m.id,
|
|
@@ -9242,14 +9508,14 @@ async function runList2() {
|
|
|
9242
9508
|
}
|
|
9243
9509
|
|
|
9244
9510
|
// src/workspace/worktree/remove.ts
|
|
9245
|
-
import
|
|
9246
|
-
import
|
|
9511
|
+
import path62 from "node:path";
|
|
9512
|
+
import fs43 from "fs-extra";
|
|
9247
9513
|
async function runRemove(opts) {
|
|
9248
9514
|
const resolved = await resolveWorkspaceForWorktree();
|
|
9249
9515
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
9250
9516
|
const { rootInfo, workspaceRoot } = resolved;
|
|
9251
9517
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
9252
|
-
if (!await
|
|
9518
|
+
if (!await fs43.pathExists(wsWorktreePath)) {
|
|
9253
9519
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
9254
9520
|
}
|
|
9255
9521
|
const state = await readWorktreeState(wsWorktreePath);
|
|
@@ -9269,12 +9535,12 @@ async function runRemove(opts) {
|
|
|
9269
9535
|
targets.push({
|
|
9270
9536
|
repo: configMember?.id ?? stateMember.id,
|
|
9271
9537
|
ownerRepo,
|
|
9272
|
-
path:
|
|
9538
|
+
path: path62.join(wsWorktreePath, stateMember.folder)
|
|
9273
9539
|
});
|
|
9274
9540
|
}
|
|
9275
9541
|
const existingTargets = [];
|
|
9276
9542
|
for (const target of targets) {
|
|
9277
|
-
if (await
|
|
9543
|
+
if (await fs43.pathExists(target.path)) existingTargets.push(target);
|
|
9278
9544
|
}
|
|
9279
9545
|
if (!opts.force) {
|
|
9280
9546
|
const blockers = [];
|
|
@@ -9319,14 +9585,19 @@ async function runRemove(opts) {
|
|
|
9319
9585
|
await pruneWorktrees(target.ownerRepo);
|
|
9320
9586
|
}
|
|
9321
9587
|
const wsTarget = existingTargets.find((t) => t.repo === "(workspace)");
|
|
9588
|
+
let wsRemoveOk = false;
|
|
9322
9589
|
if (wsTarget) {
|
|
9323
9590
|
const wsResult = await removeWorktree(workspaceRoot, wsWorktreePath);
|
|
9324
|
-
if (wsResult.ok)
|
|
9325
|
-
|
|
9591
|
+
if (wsResult.ok) {
|
|
9592
|
+
removed.push("(workspace)");
|
|
9593
|
+
wsRemoveOk = true;
|
|
9594
|
+
} else {
|
|
9595
|
+
failed.push({ repo: "(workspace)", error: wsResult.stderr ?? "unknown error" });
|
|
9596
|
+
}
|
|
9326
9597
|
await pruneWorktrees(workspaceRoot);
|
|
9327
9598
|
}
|
|
9328
|
-
if (await
|
|
9329
|
-
await
|
|
9599
|
+
if (wsRemoveOk && await fs43.pathExists(wsWorktreePath)) {
|
|
9600
|
+
await fs43.remove(wsWorktreePath);
|
|
9330
9601
|
}
|
|
9331
9602
|
return { ok: failed.length === 0, dryRun: false, slug: opts.slug, removed, failed };
|
|
9332
9603
|
}
|
|
@@ -9541,7 +9812,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
9541
9812
|
|
|
9542
9813
|
// src/commands/workspace.ts
|
|
9543
9814
|
import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
|
|
9544
|
-
import
|
|
9815
|
+
import path71 from "node:path";
|
|
9545
9816
|
|
|
9546
9817
|
// src/commands/workspace/aggregate.ts
|
|
9547
9818
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -9596,18 +9867,18 @@ ${summaries.map(
|
|
|
9596
9867
|
|
|
9597
9868
|
// src/commands/workspace/doctor.ts
|
|
9598
9869
|
import { existsSync as existsSync7 } from "node:fs";
|
|
9599
|
-
import
|
|
9870
|
+
import path64 from "node:path";
|
|
9600
9871
|
|
|
9601
9872
|
// src/commands/workspace/manifest.ts
|
|
9602
9873
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
9603
|
-
import
|
|
9874
|
+
import path63 from "node:path";
|
|
9604
9875
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
9605
9876
|
function manifestPath(workspaceRoot) {
|
|
9606
9877
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
9607
9878
|
}
|
|
9608
9879
|
function hausVersion() {
|
|
9609
9880
|
try {
|
|
9610
|
-
const pkg = JSON.parse(readFileSync4(
|
|
9881
|
+
const pkg = JSON.parse(readFileSync4(path63.join(packageRoot(), "package.json"), "utf8"));
|
|
9611
9882
|
return pkg.version ?? "0.0.0";
|
|
9612
9883
|
} catch {
|
|
9613
9884
|
return "0.0.0";
|
|
@@ -9688,10 +9959,18 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
9688
9959
|
});
|
|
9689
9960
|
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
9690
9961
|
}
|
|
9962
|
+
if (!Array.isArray(manifest.repos)) {
|
|
9963
|
+
flag({
|
|
9964
|
+
repo: "(workspace)",
|
|
9965
|
+
kind: "no-manifest",
|
|
9966
|
+
detail: 'workspace.manifest.json\'s "repos" field is not an array \u2014 run `haus workspace setup --write` to regenerate it.'
|
|
9967
|
+
});
|
|
9968
|
+
return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
|
|
9969
|
+
}
|
|
9691
9970
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
9692
9971
|
const catalogRefByRepo = [];
|
|
9693
9972
|
for (const repo of config.repos) {
|
|
9694
|
-
const repoRoot =
|
|
9973
|
+
const repoRoot = path64.resolve(workspaceRoot, repo.path);
|
|
9695
9974
|
const entry = manifestByName.get(repo.name);
|
|
9696
9975
|
if (!entry) {
|
|
9697
9976
|
flag({
|
|
@@ -9764,7 +10043,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
9764
10043
|
});
|
|
9765
10044
|
continue;
|
|
9766
10045
|
}
|
|
9767
|
-
if (!existsSync7(
|
|
10046
|
+
if (!existsSync7(path64.join(workspaceRoot, entry.path))) {
|
|
9768
10047
|
flag({
|
|
9769
10048
|
repo: entry.repo,
|
|
9770
10049
|
kind: "missing-linked-context-copy",
|
|
@@ -9807,16 +10086,16 @@ function emit(args) {
|
|
|
9807
10086
|
}
|
|
9808
10087
|
|
|
9809
10088
|
// src/workspace/link-context/link.ts
|
|
10089
|
+
import path67 from "node:path";
|
|
10090
|
+
import fs46 from "fs-extra";
|
|
10091
|
+
|
|
10092
|
+
// src/workspace/link-context/plan.ts
|
|
9810
10093
|
import path66 from "node:path";
|
|
9811
10094
|
import fs45 from "fs-extra";
|
|
9812
10095
|
|
|
9813
|
-
// src/workspace/link-context/
|
|
10096
|
+
// src/workspace/link-context/scan-source.ts
|
|
9814
10097
|
import path65 from "node:path";
|
|
9815
10098
|
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
10099
|
var ASSET_SUBDIR = {
|
|
9821
10100
|
skill: "skills",
|
|
9822
10101
|
agent: "agents",
|
|
@@ -9824,36 +10103,36 @@ var ASSET_SUBDIR = {
|
|
|
9824
10103
|
};
|
|
9825
10104
|
async function hasLinkableContext(member) {
|
|
9826
10105
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
9827
|
-
if (await
|
|
10106
|
+
if (await fs44.pathExists(path65.join(member.absPath, ".claude", subdir))) return true;
|
|
9828
10107
|
}
|
|
9829
10108
|
return false;
|
|
9830
10109
|
}
|
|
9831
10110
|
async function collectSkills(member) {
|
|
9832
|
-
const dir =
|
|
9833
|
-
if (!await
|
|
9834
|
-
const names = await
|
|
10111
|
+
const dir = path65.join(member.absPath, ".claude", "skills");
|
|
10112
|
+
if (!await fs44.pathExists(dir)) return [];
|
|
10113
|
+
const names = await fs44.readdir(dir);
|
|
9835
10114
|
const out = [];
|
|
9836
10115
|
for (const name of [...names].sort()) {
|
|
9837
|
-
const abs =
|
|
9838
|
-
const stat = await
|
|
10116
|
+
const abs = path65.join(dir, name);
|
|
10117
|
+
const stat = await fs44.lstat(abs);
|
|
9839
10118
|
if (!stat.isDirectory()) continue;
|
|
9840
|
-
if (!await
|
|
9841
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
10119
|
+
if (!await fs44.pathExists(path65.join(abs, "SKILL.md"))) continue;
|
|
10120
|
+
out.push({ type: "skill", name, sourceRelPath: path65.posix.join(".claude", "skills", name) });
|
|
9842
10121
|
}
|
|
9843
10122
|
return out;
|
|
9844
10123
|
}
|
|
9845
10124
|
async function collectFlatMarkdown(member, type) {
|
|
9846
10125
|
const subdir = ASSET_SUBDIR[type];
|
|
9847
|
-
const dir =
|
|
9848
|
-
if (!await
|
|
9849
|
-
const names = await
|
|
10126
|
+
const dir = path65.join(member.absPath, ".claude", subdir);
|
|
10127
|
+
if (!await fs44.pathExists(dir)) return [];
|
|
10128
|
+
const names = await fs44.readdir(dir);
|
|
9850
10129
|
const out = [];
|
|
9851
10130
|
for (const fileName of [...names].sort()) {
|
|
9852
10131
|
if (!fileName.endsWith(".md")) continue;
|
|
9853
|
-
const abs =
|
|
9854
|
-
if (!(await
|
|
10132
|
+
const abs = path65.join(dir, fileName);
|
|
10133
|
+
if (!(await fs44.lstat(abs)).isFile()) continue;
|
|
9855
10134
|
const name = fileName.slice(0, -".md".length);
|
|
9856
|
-
out.push({ type, name, sourceRelPath:
|
|
10135
|
+
out.push({ type, name, sourceRelPath: path65.posix.join(".claude", subdir, fileName) });
|
|
9857
10136
|
}
|
|
9858
10137
|
return out;
|
|
9859
10138
|
}
|
|
@@ -9868,9 +10147,9 @@ async function collectSourceAssets(member) {
|
|
|
9868
10147
|
|
|
9869
10148
|
// src/workspace/link-context/plan.ts
|
|
9870
10149
|
function repoFolderPrefix(member) {
|
|
9871
|
-
const base =
|
|
10150
|
+
const base = path66.posix.basename(member.folder);
|
|
9872
10151
|
if (base && base !== "." && base !== "/") return base;
|
|
9873
|
-
const fromAbs =
|
|
10152
|
+
const fromAbs = path66.basename(member.absPath);
|
|
9874
10153
|
return fromAbs || member.id;
|
|
9875
10154
|
}
|
|
9876
10155
|
function assetKey(type, destKey) {
|
|
@@ -9885,7 +10164,7 @@ async function buildLinkPlan(members) {
|
|
|
9885
10164
|
const byKey = /* @__PURE__ */ new Map();
|
|
9886
10165
|
const membersById = new Map(members.map((m) => [m.id, m]));
|
|
9887
10166
|
for (const member of members) {
|
|
9888
|
-
if (!await
|
|
10167
|
+
if (!await fs45.pathExists(member.absPath)) {
|
|
9889
10168
|
skipped.push({ repo: member.id, reason: `not cloned locally (${member.absPath}) \u2014 skipped` });
|
|
9890
10169
|
continue;
|
|
9891
10170
|
}
|
|
@@ -9927,7 +10206,7 @@ async function buildLinkPlan(members) {
|
|
|
9927
10206
|
name: asset.name,
|
|
9928
10207
|
destKey,
|
|
9929
10208
|
sourceRelPath: asset.sourceRelPath,
|
|
9930
|
-
sourceAbsPath:
|
|
10209
|
+
sourceAbsPath: path66.join(member.absPath, asset.sourceRelPath),
|
|
9931
10210
|
sourceHash
|
|
9932
10211
|
});
|
|
9933
10212
|
}
|
|
@@ -9938,7 +10217,7 @@ async function buildLinkPlan(members) {
|
|
|
9938
10217
|
function destRelPathFor(type, destKey) {
|
|
9939
10218
|
const subdir = `${type}s`;
|
|
9940
10219
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
9941
|
-
return
|
|
10220
|
+
return path67.posix.join(".claude", subdir, leaf);
|
|
9942
10221
|
}
|
|
9943
10222
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
9944
10223
|
const write = opts.write ?? false;
|
|
@@ -9964,17 +10243,17 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
9964
10243
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9965
10244
|
for (const entry of plan.entries) {
|
|
9966
10245
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
9967
|
-
const destAbs =
|
|
10246
|
+
const destAbs = path67.join(workspaceRoot, destRel);
|
|
9968
10247
|
if (write) {
|
|
9969
|
-
await
|
|
9970
|
-
if (await
|
|
9971
|
-
await
|
|
10248
|
+
await fs46.ensureDir(path67.dirname(destAbs));
|
|
10249
|
+
if (await fs46.pathExists(destAbs)) await fs46.remove(destAbs);
|
|
10250
|
+
await fs46.copy(entry.sourceAbsPath, destAbs, {
|
|
9972
10251
|
overwrite: true,
|
|
9973
10252
|
errorOnExist: false,
|
|
9974
10253
|
dereference: false,
|
|
9975
10254
|
filter: (s) => {
|
|
9976
10255
|
try {
|
|
9977
|
-
return !
|
|
10256
|
+
return !fs46.lstatSync(s).isSymbolicLink();
|
|
9978
10257
|
} catch {
|
|
9979
10258
|
return false;
|
|
9980
10259
|
}
|
|
@@ -9998,8 +10277,12 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
9998
10277
|
if (nextPaths.has(old.path)) continue;
|
|
9999
10278
|
removed.push(old.path);
|
|
10000
10279
|
if (write) {
|
|
10001
|
-
const abs =
|
|
10002
|
-
if (
|
|
10280
|
+
const abs = resolveContainedPath(workspaceRoot, old.path);
|
|
10281
|
+
if (abs === null) {
|
|
10282
|
+
warn(`Refusing to remove linked context path outside the workspace root: ${old.path}`);
|
|
10283
|
+
continue;
|
|
10284
|
+
}
|
|
10285
|
+
if (await fs46.pathExists(abs)) await fs46.remove(abs);
|
|
10003
10286
|
}
|
|
10004
10287
|
}
|
|
10005
10288
|
if (write) {
|
|
@@ -10051,11 +10334,11 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
10051
10334
|
|
|
10052
10335
|
// src/commands/workspace/setup.ts
|
|
10053
10336
|
import { existsSync as existsSync8, statSync } from "node:fs";
|
|
10054
|
-
import
|
|
10337
|
+
import path69 from "node:path";
|
|
10055
10338
|
|
|
10056
10339
|
// src/claude/write-workspace-claude-md.ts
|
|
10057
|
-
import
|
|
10058
|
-
import
|
|
10340
|
+
import path68 from "node:path";
|
|
10341
|
+
import fs47 from "fs-extra";
|
|
10059
10342
|
var WORKSPACE_SENTINELS = { begin: WORKSPACE_BLOCK_BEGIN, end: WORKSPACE_BLOCK_END };
|
|
10060
10343
|
function buildWorkspaceImportBlock(client, members) {
|
|
10061
10344
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
@@ -10074,11 +10357,11 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
10074
10357
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
10075
10358
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
10076
10359
|
const dryRun = opts.dryRun ?? false;
|
|
10077
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
10360
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path68.join(workspaceRoot, "CLAUDE.md");
|
|
10078
10361
|
const say = opts.quiet ? () => {
|
|
10079
10362
|
} : log;
|
|
10080
|
-
const existed = await
|
|
10081
|
-
const prev = existed ? await
|
|
10363
|
+
const existed = await fs47.pathExists(filePath);
|
|
10364
|
+
const prev = existed ? await fs47.readFile(filePath, "utf8") : "";
|
|
10082
10365
|
const next = opts.collision ? `${block}
|
|
10083
10366
|
` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
|
|
10084
10367
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -10105,20 +10388,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
10105
10388
|
|
|
10106
10389
|
// src/commands/workspace/setup.ts
|
|
10107
10390
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
10108
|
-
let dir =
|
|
10391
|
+
let dir = path69.resolve(start);
|
|
10109
10392
|
for (; ; ) {
|
|
10110
|
-
if (existsSync8(
|
|
10111
|
-
const parent =
|
|
10112
|
-
if (parent === dir) return
|
|
10393
|
+
if (existsSync8(path69.join(dir, WORKSPACE_FILE))) return dir;
|
|
10394
|
+
const parent = path69.dirname(dir);
|
|
10395
|
+
if (parent === dir) return path69.resolve(start);
|
|
10113
10396
|
dir = parent;
|
|
10114
10397
|
}
|
|
10115
10398
|
}
|
|
10116
10399
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
10117
|
-
return
|
|
10400
|
+
return path69.resolve(workspaceRoot, repoPath) === path69.resolve(workspaceRoot);
|
|
10118
10401
|
}
|
|
10119
10402
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
10120
10403
|
const apply = options.write ?? false;
|
|
10121
|
-
const configText = await readText(
|
|
10404
|
+
const configText = await readText(path69.join(workspaceRoot, WORKSPACE_FILE));
|
|
10122
10405
|
if (!configText) {
|
|
10123
10406
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10124
10407
|
process.exitCode = 1;
|
|
@@ -10145,7 +10428,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10145
10428
|
const statuses = [];
|
|
10146
10429
|
const aggregateInputs = [];
|
|
10147
10430
|
for (const repo of repos) {
|
|
10148
|
-
const repoRoot =
|
|
10431
|
+
const repoRoot = path69.resolve(workspaceRoot, repo.path);
|
|
10149
10432
|
say(`
|
|
10150
10433
|
\u2192 ${repo.name} (${repo.path})`);
|
|
10151
10434
|
try {
|
|
@@ -10178,7 +10461,15 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10178
10461
|
status: "failed",
|
|
10179
10462
|
error: message
|
|
10180
10463
|
});
|
|
10181
|
-
if (!options.continueOnError)
|
|
10464
|
+
if (!options.continueOnError) {
|
|
10465
|
+
if (options.json) {
|
|
10466
|
+
const result2 = { workspaceRoot, statuses, written: [] };
|
|
10467
|
+
log(JSON.stringify(result2, null, 2));
|
|
10468
|
+
process.exitCode = 1;
|
|
10469
|
+
return result2;
|
|
10470
|
+
}
|
|
10471
|
+
throw err;
|
|
10472
|
+
}
|
|
10182
10473
|
error(`Setup failed for ${repo.name}: ${message}`);
|
|
10183
10474
|
}
|
|
10184
10475
|
}
|
|
@@ -10227,7 +10518,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10227
10518
|
const status = statusByName.get(repo.name);
|
|
10228
10519
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
10229
10520
|
if (status?.status === "ok") {
|
|
10230
|
-
const lock = await checkLock(
|
|
10521
|
+
const lock = await checkLock(path69.resolve(workspaceRoot, repo.path));
|
|
10231
10522
|
manifestRepos.push({
|
|
10232
10523
|
name: repo.name,
|
|
10233
10524
|
path: repo.path,
|
|
@@ -10283,7 +10574,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
10283
10574
|
try {
|
|
10284
10575
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
10285
10576
|
if (linkResult.ok) {
|
|
10286
|
-
written.push(...linkResult.added.map((rel) =>
|
|
10577
|
+
written.push(...linkResult.added.map((rel) => path69.join(workspaceRoot, rel)));
|
|
10287
10578
|
if (!options.json) {
|
|
10288
10579
|
say(
|
|
10289
10580
|
`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 +10606,8 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
10315
10606
|
|
|
10316
10607
|
// src/commands/workspace/undo.ts
|
|
10317
10608
|
import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
|
|
10318
|
-
import
|
|
10319
|
-
import
|
|
10609
|
+
import path70 from "node:path";
|
|
10610
|
+
import fs48 from "fs-extra";
|
|
10320
10611
|
var WORKSPACE_AGGREGATE_FILES = [
|
|
10321
10612
|
"workspace-summary.json",
|
|
10322
10613
|
"dependency-ownership-map.json",
|
|
@@ -10352,7 +10643,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10352
10643
|
}
|
|
10353
10644
|
let anyRepoFailed = false;
|
|
10354
10645
|
for (const repo of config.repos) {
|
|
10355
|
-
const repoRoot =
|
|
10646
|
+
const repoRoot = path70.resolve(workspaceRoot, repo.path);
|
|
10356
10647
|
log(`
|
|
10357
10648
|
\u2192 ${repo.name} (${repo.path})`);
|
|
10358
10649
|
try {
|
|
@@ -10367,30 +10658,34 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10367
10658
|
}
|
|
10368
10659
|
const linkedContext = await readLinkedContext(workspaceRoot);
|
|
10369
10660
|
for (const entry of linkedContext) {
|
|
10370
|
-
const abs =
|
|
10371
|
-
if (
|
|
10372
|
-
|
|
10661
|
+
const abs = resolveContainedPath(workspaceRoot, entry.path);
|
|
10662
|
+
if (abs === null) {
|
|
10663
|
+
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
10664
|
+
continue;
|
|
10665
|
+
}
|
|
10666
|
+
if (await fs48.pathExists(abs)) {
|
|
10667
|
+
await fs48.remove(abs);
|
|
10373
10668
|
log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
10374
10669
|
}
|
|
10375
10670
|
}
|
|
10376
10671
|
for (const rel of WORKSPACE_AGGREGATE_FILES) {
|
|
10377
10672
|
const abs = hausPath(workspaceRoot, rel);
|
|
10378
|
-
if (await
|
|
10379
|
-
await
|
|
10380
|
-
log(`Removed ${
|
|
10673
|
+
if (await fs48.pathExists(abs)) {
|
|
10674
|
+
await fs48.remove(abs);
|
|
10675
|
+
log(`Removed ${path70.relative(workspaceRoot, abs)}`);
|
|
10381
10676
|
}
|
|
10382
10677
|
}
|
|
10383
10678
|
const manifestFile = manifestPath(workspaceRoot);
|
|
10384
|
-
if (await
|
|
10385
|
-
await
|
|
10386
|
-
log(`Removed ${
|
|
10679
|
+
if (await fs48.pathExists(manifestFile)) {
|
|
10680
|
+
await fs48.remove(manifestFile);
|
|
10681
|
+
log(`Removed ${path70.relative(workspaceRoot, manifestFile)}`);
|
|
10387
10682
|
}
|
|
10388
10683
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
10389
|
-
if (await
|
|
10390
|
-
await
|
|
10391
|
-
log(`Removed ${
|
|
10684
|
+
if (await fs48.pathExists(workspaceMdPath)) {
|
|
10685
|
+
await fs48.remove(workspaceMdPath);
|
|
10686
|
+
log(`Removed ${path70.relative(workspaceRoot, workspaceMdPath)}`);
|
|
10392
10687
|
} else {
|
|
10393
|
-
const claudeMdPath =
|
|
10688
|
+
const claudeMdPath = path70.join(workspaceRoot, "CLAUDE.md");
|
|
10394
10689
|
if (existsSync9(claudeMdPath)) {
|
|
10395
10690
|
const prev = await readText(claudeMdPath);
|
|
10396
10691
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -10399,19 +10694,19 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
10399
10694
|
end: WORKSPACE_BLOCK_END
|
|
10400
10695
|
});
|
|
10401
10696
|
if (next.length === 0) {
|
|
10402
|
-
await
|
|
10697
|
+
await fs48.remove(claudeMdPath);
|
|
10403
10698
|
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
10404
10699
|
} else {
|
|
10405
|
-
await
|
|
10700
|
+
await fs48.writeFile(claudeMdPath, next, "utf8");
|
|
10406
10701
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
10407
10702
|
}
|
|
10408
10703
|
} else if (prev && hasLegacyWorkspaceBlock(prev)) {
|
|
10409
10704
|
const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
|
|
10410
10705
|
if (next.length === 0) {
|
|
10411
|
-
await
|
|
10706
|
+
await fs48.remove(claudeMdPath);
|
|
10412
10707
|
log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
|
|
10413
10708
|
} else {
|
|
10414
|
-
await
|
|
10709
|
+
await fs48.writeFile(claudeMdPath, next, "utf8");
|
|
10415
10710
|
log(
|
|
10416
10711
|
"Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
|
|
10417
10712
|
);
|
|
@@ -10442,7 +10737,16 @@ function normalizeMaxDepth(maxDepth) {
|
|
|
10442
10737
|
warn(`Ignoring invalid --max-depth "${String(maxDepth)}"; using the default depth.`);
|
|
10443
10738
|
return void 0;
|
|
10444
10739
|
}
|
|
10445
|
-
async function initWorkspace() {
|
|
10740
|
+
async function initWorkspace(options = {}) {
|
|
10741
|
+
if (existsSync10(WORKSPACE_FILE) && !options.yes) {
|
|
10742
|
+
const ok = await confirm(
|
|
10743
|
+
`${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.`
|
|
10744
|
+
);
|
|
10745
|
+
if (!ok) {
|
|
10746
|
+
log("Cancelled.");
|
|
10747
|
+
return;
|
|
10748
|
+
}
|
|
10749
|
+
}
|
|
10446
10750
|
await writeText(
|
|
10447
10751
|
WORKSPACE_FILE,
|
|
10448
10752
|
`client: unknown
|
|
@@ -10456,7 +10760,7 @@ relationships: []
|
|
|
10456
10760
|
log("Workspace initialized.");
|
|
10457
10761
|
}
|
|
10458
10762
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
10459
|
-
const configText = await readText(
|
|
10763
|
+
const configText = await readText(path71.join(workspaceRoot, WORKSPACE_FILE));
|
|
10460
10764
|
if (!configText) {
|
|
10461
10765
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
10462
10766
|
process.exitCode = 1;
|
|
@@ -10478,7 +10782,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
10478
10782
|
const inputs = [];
|
|
10479
10783
|
const failures = [];
|
|
10480
10784
|
for (const repo of config.repos) {
|
|
10481
|
-
const repoRoot =
|
|
10785
|
+
const repoRoot = path71.resolve(workspaceRoot, repo.path);
|
|
10482
10786
|
try {
|
|
10483
10787
|
if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
10484
10788
|
throw new Error("path is not a directory");
|
|
@@ -10504,7 +10808,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
10504
10808
|
}
|
|
10505
10809
|
async function runWorkspace(action, options = {}) {
|
|
10506
10810
|
if (action === "init") {
|
|
10507
|
-
await initWorkspace();
|
|
10811
|
+
await initWorkspace({ yes: options.yes });
|
|
10508
10812
|
return;
|
|
10509
10813
|
}
|
|
10510
10814
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
@@ -10544,7 +10848,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
10544
10848
|
// src/cli.ts
|
|
10545
10849
|
function cliVersion() {
|
|
10546
10850
|
try {
|
|
10547
|
-
const pkgPath =
|
|
10851
|
+
const pkgPath = path72.join(packageRoot(), "package.json");
|
|
10548
10852
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10549
10853
|
return pkg.version ?? "0.0.0";
|
|
10550
10854
|
} catch {
|
|
@@ -10554,7 +10858,7 @@ function cliVersion() {
|
|
|
10554
10858
|
var program = new Command();
|
|
10555
10859
|
function validateRuntimeNodeVersion() {
|
|
10556
10860
|
try {
|
|
10557
|
-
const pkgPath =
|
|
10861
|
+
const pkgPath = path72.join(packageRoot(), "package.json");
|
|
10558
10862
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
10559
10863
|
const requiredRange = pkg.engines?.node;
|
|
10560
10864
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -10615,7 +10919,7 @@ program.command("install").option("--dry-run").option("--force").option("--check
|
|
|
10615
10919
|
program.command("scaffold [ids...]").description(
|
|
10616
10920
|
"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
10921
|
).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);
|
|
10922
|
+
program.command("uninstall").option("--force").option("-y, --yes", "Skip confirmation").action(runUninstallCommand);
|
|
10619
10923
|
var guard = program.command("guard");
|
|
10620
10924
|
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
10621
10925
|
guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
@@ -10632,7 +10936,7 @@ function registerDecisionsCommand(name) {
|
|
|
10632
10936
|
registerDecisionsCommand("decisions");
|
|
10633
10937
|
registerDecisionsCommand("adr");
|
|
10634
10938
|
var workspace = program.command("workspace");
|
|
10635
|
-
workspace.command("init").action(() => runWorkspace("init"));
|
|
10939
|
+
workspace.command("init").option("-y, --yes", "Skip confirmation when haus.workspace.yaml already exists").action((opts) => runWorkspace("init", opts));
|
|
10636
10940
|
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
10941
|
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
10942
|
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));
|