@haus-tech/haus-workflow 0.22.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/cli.js +626 -278
- package/library/catalog/manifest.json +35 -8
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync4 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path36 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
9
|
-
import
|
|
9
|
+
import path14 from "path";
|
|
10
10
|
import checkbox from "@inquirer/checkbox";
|
|
11
|
-
import
|
|
11
|
+
import fs13 from "fs-extra";
|
|
12
12
|
|
|
13
13
|
// src/catalog/remote-catalog.ts
|
|
14
14
|
import os from "os";
|
|
@@ -86,6 +86,8 @@ var error = (msg, ...args) => {
|
|
|
86
86
|
|
|
87
87
|
// src/catalog/constants.ts
|
|
88
88
|
var CATALOG_REPO_URL = "https://raw.githubusercontent.com/wearehaustech/haus-workflow-catalog";
|
|
89
|
+
var CATALOG_GITHUB_API_URL = "https://api.github.com/repos/WeAreHausTech/haus-workflow-catalog";
|
|
90
|
+
var SUPERPOWERS_SHARED_CATALOG_REL = "skills/superpowers/shared";
|
|
89
91
|
var CATALOG_REF = process.env.HAUS_CATALOG_REF;
|
|
90
92
|
var CATALOG_CACHE_SUBDIR = ".claude/haus/catalog-cache";
|
|
91
93
|
|
|
@@ -405,6 +407,7 @@ function getCacheDir() {
|
|
|
405
407
|
return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ?? path2.join(os.homedir(), CATALOG_CACHE_SUBDIR);
|
|
406
408
|
}
|
|
407
409
|
var cachedCatalogRef;
|
|
410
|
+
var cachedBlobPaths;
|
|
408
411
|
function getResolvedCatalogRef() {
|
|
409
412
|
return cachedCatalogRef ?? process.env["HAUS_CATALOG_REF"] ?? "main";
|
|
410
413
|
}
|
|
@@ -436,6 +439,15 @@ async function fetchText(url) {
|
|
|
436
439
|
return null;
|
|
437
440
|
}
|
|
438
441
|
}
|
|
442
|
+
async function fetchBytes(url) {
|
|
443
|
+
try {
|
|
444
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
445
|
+
if (!res.ok) return null;
|
|
446
|
+
return Buffer.from(await res.arrayBuffer());
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
439
451
|
async function fetchRemoteManifest() {
|
|
440
452
|
const base = await remoteBase();
|
|
441
453
|
const text = await fetchText(`${base}/manifest.json`);
|
|
@@ -480,29 +492,246 @@ function isSafeCatalogPath(itemPath) {
|
|
|
480
492
|
const normalized = path2.normalize(itemPath);
|
|
481
493
|
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
482
494
|
}
|
|
495
|
+
function isSafeRelativeFilePath(rel) {
|
|
496
|
+
if (!rel || rel.startsWith("/") || rel.includes("\\") || rel.includes("//")) return false;
|
|
497
|
+
if (path2.isAbsolute(rel)) return false;
|
|
498
|
+
const normalized = path2.posix.normalize(rel.replace(/\\/g, "/"));
|
|
499
|
+
return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
|
|
500
|
+
}
|
|
501
|
+
function githubApiHeaders() {
|
|
502
|
+
const headers = { Accept: "application/vnd.github+json" };
|
|
503
|
+
const auth = process.env["HAUS_GITHUB_TOKEN"] ?? process.env["GITHUB_TOKEN"];
|
|
504
|
+
if (auth) headers["Authorization"] = `Bearer ${auth}`;
|
|
505
|
+
return headers;
|
|
506
|
+
}
|
|
507
|
+
function sanitizeRelativeFilePaths(files, label) {
|
|
508
|
+
const safe = [];
|
|
509
|
+
for (const rel of files) {
|
|
510
|
+
if (!isSafeRelativeFilePath(rel)) {
|
|
511
|
+
warn(`Rejected unsafe path in ${label}: ${rel}`);
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
safe.push(rel);
|
|
515
|
+
}
|
|
516
|
+
return safe;
|
|
517
|
+
}
|
|
483
518
|
function safeJoin(base, itemPath) {
|
|
484
519
|
if (!isSafeCatalogPath(itemPath)) return null;
|
|
485
520
|
const resolved = path2.resolve(base, itemPath);
|
|
486
521
|
return resolved.startsWith(base + path2.sep) || resolved === base ? resolved : null;
|
|
487
522
|
}
|
|
488
523
|
var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command"]);
|
|
489
|
-
function
|
|
490
|
-
return
|
|
491
|
-
}
|
|
492
|
-
async function
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
524
|
+
function isMarkdownPath(rel) {
|
|
525
|
+
return rel.toLowerCase().endsWith(".md");
|
|
526
|
+
}
|
|
527
|
+
async function listFilesRecursive(dir, base = dir) {
|
|
528
|
+
const out = [];
|
|
529
|
+
if (!await fs2.pathExists(dir)) return out;
|
|
530
|
+
for (const entry of await fs2.readdir(dir, { withFileTypes: true })) {
|
|
531
|
+
const full = path2.join(dir, entry.name);
|
|
532
|
+
if (entry.isDirectory()) {
|
|
533
|
+
out.push(...await listFilesRecursive(full, base));
|
|
534
|
+
} else if (entry.isFile()) {
|
|
535
|
+
out.push(path2.relative(base, full).replace(/\\/g, "/"));
|
|
499
536
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
537
|
+
}
|
|
538
|
+
return out.sort();
|
|
539
|
+
}
|
|
540
|
+
async function listMockPrefixFiles(base, prefix) {
|
|
541
|
+
const text = await fetchText(`${base}/__haus_tree__/${encodeURIComponent(prefix)}`);
|
|
542
|
+
if (text === null) return null;
|
|
543
|
+
try {
|
|
544
|
+
const parsed = JSON.parse(text);
|
|
545
|
+
if (!Array.isArray(parsed) || !parsed.every((e) => typeof e === "string")) return null;
|
|
546
|
+
return parsed;
|
|
547
|
+
} catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function fetchGitHubRecursiveBlobPaths(ref) {
|
|
552
|
+
try {
|
|
553
|
+
const headers = githubApiHeaders();
|
|
554
|
+
const commitRes = await fetch(`${CATALOG_GITHUB_API_URL}/commits/${encodeURIComponent(ref)}`, {
|
|
555
|
+
signal: AbortSignal.timeout(15e3),
|
|
556
|
+
headers
|
|
557
|
+
});
|
|
558
|
+
if (!commitRes.ok) return null;
|
|
559
|
+
const commit = await commitRes.json();
|
|
560
|
+
const treeSha = commit.commit.tree.sha;
|
|
561
|
+
const treeRes = await fetch(`${CATALOG_GITHUB_API_URL}/git/trees/${treeSha}?recursive=1`, {
|
|
562
|
+
signal: AbortSignal.timeout(3e4),
|
|
563
|
+
headers
|
|
564
|
+
});
|
|
565
|
+
if (!treeRes.ok) return null;
|
|
566
|
+
const tree = await treeRes.json();
|
|
567
|
+
if (tree.truncated) {
|
|
568
|
+
warn("Catalog GitHub tree listing was truncated \u2014 refusing partial cache sync");
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
return tree.tree.filter((e) => e.type === "blob").map((e) => e.path);
|
|
572
|
+
} catch {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function fetchCatalogBlobPaths(_base) {
|
|
577
|
+
if (cachedBlobPaths) return cachedBlobPaths;
|
|
578
|
+
if (process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
579
|
+
const ref = getResolvedCatalogRef();
|
|
580
|
+
const paths = await fetchGitHubRecursiveBlobPaths(ref);
|
|
581
|
+
if (paths) cachedBlobPaths = paths;
|
|
582
|
+
return paths;
|
|
583
|
+
}
|
|
584
|
+
async function listFilesUnderCatalogPrefix(prefix, base) {
|
|
585
|
+
const normalized = prefix.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
586
|
+
const prefixSlash = `${normalized}/`;
|
|
587
|
+
let relFiles;
|
|
588
|
+
if (process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
589
|
+
relFiles = await listMockPrefixFiles(base, normalized);
|
|
590
|
+
} else {
|
|
591
|
+
const blobs = await fetchCatalogBlobPaths(base);
|
|
592
|
+
if (!blobs) return null;
|
|
593
|
+
relFiles = blobs.filter((p) => p.startsWith(prefixSlash)).map((p) => p.slice(prefixSlash.length)).sort();
|
|
594
|
+
}
|
|
595
|
+
if (!relFiles) return null;
|
|
596
|
+
return sanitizeRelativeFilePaths(relFiles, normalized);
|
|
597
|
+
}
|
|
598
|
+
async function fetchPrefixFiles(catalogPrefix, relFiles, base, label) {
|
|
599
|
+
const fetched = [];
|
|
600
|
+
for (const rel of relFiles) {
|
|
601
|
+
const url = `${base}/${catalogPrefix}/${rel}`;
|
|
602
|
+
if (isMarkdownPath(rel)) {
|
|
603
|
+
const text = await fetchText(url);
|
|
604
|
+
if (text === null) {
|
|
605
|
+
warn(`Failed to fetch ${rel} for ${label}`);
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
fetched.push({ rel, kind: "text", body: text });
|
|
609
|
+
} else {
|
|
610
|
+
const bytes = await fetchBytes(url);
|
|
611
|
+
if (bytes === null) {
|
|
612
|
+
warn(`Failed to fetch ${rel} for ${label}`);
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
fetched.push({ rel, kind: "binary", body: bytes });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return fetched;
|
|
619
|
+
}
|
|
620
|
+
function validateMarkdownFiles(item, fetched) {
|
|
621
|
+
for (const file of fetched) {
|
|
622
|
+
if (file.kind !== "text" || !isMarkdownPath(file.rel)) continue;
|
|
623
|
+
const verdict = validateCatalogItem(item, file.body);
|
|
624
|
+
if (!verdict.ok) {
|
|
625
|
+
warn(`Rejected ${item.id} at ingest: ${verdict.reason}`);
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
async function directoryMatchesFetched(destDir, fetched) {
|
|
632
|
+
if (!await fs2.pathExists(destDir)) return false;
|
|
633
|
+
const existing = await listFilesRecursive(destDir);
|
|
634
|
+
const relSet = new Set(fetched.map((f) => f.rel));
|
|
635
|
+
if (existing.length !== fetched.length) return false;
|
|
636
|
+
for (const rel of existing) {
|
|
637
|
+
if (!relSet.has(rel)) return false;
|
|
638
|
+
}
|
|
639
|
+
for (const file of fetched) {
|
|
640
|
+
const dest = path2.join(destDir, file.rel);
|
|
641
|
+
if (!await fs2.pathExists(dest)) return false;
|
|
642
|
+
if (file.kind === "text") {
|
|
643
|
+
const local = await fs2.readFile(dest, "utf8");
|
|
644
|
+
if (local !== file.body) return false;
|
|
645
|
+
} else {
|
|
646
|
+
const local = await fs2.readFile(dest);
|
|
647
|
+
if (!local.equals(file.body)) return false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
async function writeFetchedDirectory(destDir, fetched) {
|
|
653
|
+
if (await fs2.pathExists(destDir)) {
|
|
654
|
+
await fs2.remove(destDir);
|
|
655
|
+
}
|
|
656
|
+
await fs2.ensureDir(destDir);
|
|
657
|
+
for (const file of fetched) {
|
|
658
|
+
const dest = path2.join(destDir, file.rel);
|
|
659
|
+
await fs2.ensureDir(path2.dirname(dest));
|
|
660
|
+
if (file.kind === "text") {
|
|
661
|
+
await fs2.writeFile(dest, file.body, "utf8");
|
|
662
|
+
} else {
|
|
663
|
+
await fs2.writeFile(dest, file.body);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async function syncDirectoryFromPrefix(item, catalogPrefix, destDir, base, opts) {
|
|
668
|
+
const relFiles = await listFilesUnderCatalogPrefix(catalogPrefix, base);
|
|
669
|
+
if (!relFiles) {
|
|
670
|
+
warn(`Failed to list files for ${item.id}`);
|
|
671
|
+
return "failed";
|
|
672
|
+
}
|
|
673
|
+
if (!relFiles.includes("SKILL.md") && catalogPrefix !== SUPERPOWERS_SHARED_CATALOG_REL) {
|
|
674
|
+
warn(`Failed to fetch content for ${item.id}: missing SKILL.md`);
|
|
675
|
+
return "failed";
|
|
676
|
+
}
|
|
677
|
+
if (relFiles.length === 0) {
|
|
678
|
+
return "unchanged";
|
|
679
|
+
}
|
|
680
|
+
const fetched = await fetchPrefixFiles(catalogPrefix, relFiles, base, item.id);
|
|
681
|
+
if (!fetched) return "failed";
|
|
682
|
+
if (opts.validateMarkdown && "type" in item) {
|
|
683
|
+
if (!validateMarkdownFiles(item, fetched)) return "failed";
|
|
684
|
+
} else if (opts.validateMarkdown) {
|
|
685
|
+
for (const file of fetched) {
|
|
686
|
+
if (file.kind !== "text" || !isMarkdownPath(file.rel)) continue;
|
|
687
|
+
const verdict = validateCatalogItem(
|
|
688
|
+
{ id: item.id, type: "skill", path: catalogPrefix },
|
|
689
|
+
file.body
|
|
690
|
+
);
|
|
691
|
+
if (!verdict.ok) {
|
|
692
|
+
warn(`Rejected ${item.id} at ingest: ${verdict.reason}`);
|
|
693
|
+
return "failed";
|
|
694
|
+
}
|
|
504
695
|
}
|
|
505
|
-
|
|
696
|
+
}
|
|
697
|
+
const existed = await fs2.pathExists(destDir);
|
|
698
|
+
if (await directoryMatchesFetched(destDir, fetched)) {
|
|
699
|
+
return "unchanged";
|
|
700
|
+
}
|
|
701
|
+
await writeFetchedDirectory(destDir, fetched);
|
|
702
|
+
return existed ? "updated" : "created";
|
|
703
|
+
}
|
|
704
|
+
async function syncSkillDirectory(item, base) {
|
|
705
|
+
const destDir = safeJoin(getCacheDir(), item.path);
|
|
706
|
+
if (!destDir) {
|
|
707
|
+
warn(`Skipping ${item.id}: path traversal detected`);
|
|
708
|
+
return "failed";
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
return await syncDirectoryFromPrefix(item, item.path, destDir, base, {
|
|
712
|
+
validateMarkdown: true
|
|
713
|
+
});
|
|
714
|
+
} catch (err) {
|
|
715
|
+
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
716
|
+
return "failed";
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
async function syncSuperpowersShared(base) {
|
|
720
|
+
const relFiles = await listFilesUnderCatalogPrefix(SUPERPOWERS_SHARED_CATALOG_REL, base);
|
|
721
|
+
if (!relFiles || relFiles.length === 0) return "skipped";
|
|
722
|
+
const destDir = safeJoin(getCacheDir(), SUPERPOWERS_SHARED_CATALOG_REL);
|
|
723
|
+
if (!destDir) return "failed";
|
|
724
|
+
try {
|
|
725
|
+
return await syncDirectoryFromPrefix(
|
|
726
|
+
{ id: "haus.superpowers-shared", path: SUPERPOWERS_SHARED_CATALOG_REL },
|
|
727
|
+
SUPERPOWERS_SHARED_CATALOG_REL,
|
|
728
|
+
destDir,
|
|
729
|
+
base,
|
|
730
|
+
{ validateMarkdown: true }
|
|
731
|
+
);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
warn(`Failed to cache superpowers shared: ${err instanceof Error ? err.message : String(err)}`);
|
|
734
|
+
return "failed";
|
|
506
735
|
}
|
|
507
736
|
}
|
|
508
737
|
async function syncOneItem(item, base) {
|
|
@@ -518,30 +747,7 @@ async function syncOneItem(item, base) {
|
|
|
518
747
|
return "failed";
|
|
519
748
|
}
|
|
520
749
|
if (item.type === "skill") {
|
|
521
|
-
|
|
522
|
-
if (!destDir) {
|
|
523
|
-
warn(`Skipping ${item.id}: path traversal detected`);
|
|
524
|
-
return "failed";
|
|
525
|
-
}
|
|
526
|
-
const dest2 = path2.join(destDir, "SKILL.md");
|
|
527
|
-
const text2 = await fetchText(`${base}/${item.path}/SKILL.md`);
|
|
528
|
-
if (!text2) {
|
|
529
|
-
warn(`Failed to fetch content for ${item.id}`);
|
|
530
|
-
return "failed";
|
|
531
|
-
}
|
|
532
|
-
const verdict2 = validateCatalogItem(item, text2);
|
|
533
|
-
if (!verdict2.ok) {
|
|
534
|
-
warn(`Rejected ${item.id} at ingest: ${verdict2.reason}`);
|
|
535
|
-
return "failed";
|
|
536
|
-
}
|
|
537
|
-
try {
|
|
538
|
-
const outcome = await writeTextIfChanged(dest2, text2);
|
|
539
|
-
await downloadSkillReferences(item, destDir, base);
|
|
540
|
-
return outcome;
|
|
541
|
-
} catch (err) {
|
|
542
|
-
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
543
|
-
return "failed";
|
|
544
|
-
}
|
|
750
|
+
return syncSkillDirectory(item, base);
|
|
545
751
|
}
|
|
546
752
|
const dest = safeJoin(getCacheDir(), item.path);
|
|
547
753
|
if (!dest) {
|
|
@@ -566,6 +772,7 @@ async function syncOneItem(item, base) {
|
|
|
566
772
|
}
|
|
567
773
|
}
|
|
568
774
|
async function syncRemoteCatalog() {
|
|
775
|
+
cachedBlobPaths = void 0;
|
|
569
776
|
const manifest = await fetchRemoteManifest();
|
|
570
777
|
if (!manifest) {
|
|
571
778
|
warn("Remote catalog fetch failed \u2014 using bundled catalog");
|
|
@@ -593,6 +800,10 @@ async function syncRemoteCatalog() {
|
|
|
593
800
|
const failed = [];
|
|
594
801
|
const base = await remoteBase();
|
|
595
802
|
const outcomes = await mapWithConcurrency(items, (item) => syncOneItem(item, base), 8);
|
|
803
|
+
const sharedOutcome = await syncSuperpowersShared(base);
|
|
804
|
+
if (sharedOutcome === "failed") {
|
|
805
|
+
warn("Failed to cache superpowers shared support files");
|
|
806
|
+
}
|
|
596
807
|
for (let i = 0; i < items.length; i++) {
|
|
597
808
|
const item = items[i];
|
|
598
809
|
const outcome = outcomes[i];
|
|
@@ -609,7 +820,7 @@ async function fetchLatestCatalogTag() {
|
|
|
609
820
|
try {
|
|
610
821
|
const res = await fetch(CATALOG_TAGS_API_URL, {
|
|
611
822
|
signal: AbortSignal.timeout(5e3),
|
|
612
|
-
headers:
|
|
823
|
+
headers: githubApiHeaders()
|
|
613
824
|
});
|
|
614
825
|
if (!res.ok) return null;
|
|
615
826
|
const tags = await res.json();
|
|
@@ -718,9 +929,10 @@ function mergeHooks(settings, fragments) {
|
|
|
718
929
|
updated._haus = {
|
|
719
930
|
hooks: [...existing, ...addedIds],
|
|
720
931
|
hookCommands: [...existingCommands, ...addedCommands],
|
|
721
|
-
// Preserve deny/allow tracking so hook, deny, and
|
|
932
|
+
// Preserve deny/allow/ask tracking so hook, deny, allow, and ask merges are order-independent.
|
|
722
933
|
...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
723
|
-
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
|
|
934
|
+
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
|
|
935
|
+
...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
|
|
724
936
|
};
|
|
725
937
|
return { settings: updated, addedIds };
|
|
726
938
|
}
|
|
@@ -743,7 +955,8 @@ function mergeDenyRules(settings, rules) {
|
|
|
743
955
|
hooks: settings._haus?.hooks ?? [],
|
|
744
956
|
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
745
957
|
denyRules: [...trackedDeny, ...addedRules],
|
|
746
|
-
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
|
|
958
|
+
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
|
|
959
|
+
...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
|
|
747
960
|
};
|
|
748
961
|
return { settings: updated, addedRules };
|
|
749
962
|
}
|
|
@@ -766,7 +979,8 @@ function mergeAllowRules(settings, rules) {
|
|
|
766
979
|
hooks: settings._haus?.hooks ?? [],
|
|
767
980
|
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
768
981
|
...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
769
|
-
allowRules: [...trackedAllow, ...addedRules]
|
|
982
|
+
allowRules: [...trackedAllow, ...addedRules],
|
|
983
|
+
...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
|
|
770
984
|
};
|
|
771
985
|
return { settings: updated, addedRules };
|
|
772
986
|
}
|
|
@@ -783,7 +997,7 @@ function stripHausAllow(settings) {
|
|
|
783
997
|
else delete updated.permissions;
|
|
784
998
|
const haus = { ...prevHaus };
|
|
785
999
|
delete haus.allowRules;
|
|
786
|
-
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0;
|
|
1000
|
+
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0 || (haus.askRules?.length ?? 0) > 0;
|
|
787
1001
|
if (stillTracking) updated._haus = haus;
|
|
788
1002
|
else delete updated._haus;
|
|
789
1003
|
return updated;
|
|
@@ -801,7 +1015,7 @@ function stripHausDeny(settings) {
|
|
|
801
1015
|
else delete updated.permissions;
|
|
802
1016
|
const haus = { ...prevHaus };
|
|
803
1017
|
delete haus.denyRules;
|
|
804
|
-
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0;
|
|
1018
|
+
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0 || (haus.askRules?.length ?? 0) > 0;
|
|
805
1019
|
if (stillTracking) updated._haus = haus;
|
|
806
1020
|
else delete updated._haus;
|
|
807
1021
|
return updated;
|
|
@@ -823,6 +1037,48 @@ function stripHausHooks(settings) {
|
|
|
823
1037
|
void _;
|
|
824
1038
|
return rest;
|
|
825
1039
|
}
|
|
1040
|
+
function mergeAskRules(settings, rules) {
|
|
1041
|
+
const existingAsk = settings.permissions?.ask ?? [];
|
|
1042
|
+
const seen = new Set(existingAsk);
|
|
1043
|
+
const trackedAsk = settings._haus?.askRules ?? [];
|
|
1044
|
+
const addedRules = [];
|
|
1045
|
+
for (const rule of rules) {
|
|
1046
|
+
if (seen.has(rule)) continue;
|
|
1047
|
+
seen.add(rule);
|
|
1048
|
+
addedRules.push(rule);
|
|
1049
|
+
}
|
|
1050
|
+
const updated = { ...settings };
|
|
1051
|
+
updated.permissions = {
|
|
1052
|
+
...settings.permissions ?? {},
|
|
1053
|
+
ask: [...existingAsk, ...addedRules]
|
|
1054
|
+
};
|
|
1055
|
+
updated._haus = {
|
|
1056
|
+
hooks: settings._haus?.hooks ?? [],
|
|
1057
|
+
...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
|
|
1058
|
+
...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
|
|
1059
|
+
...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
|
|
1060
|
+
askRules: [...trackedAsk, ...addedRules]
|
|
1061
|
+
};
|
|
1062
|
+
return { settings: updated, addedRules };
|
|
1063
|
+
}
|
|
1064
|
+
function stripHausAsk(settings) {
|
|
1065
|
+
const prevHaus = settings._haus;
|
|
1066
|
+
if (!prevHaus?.askRules || prevHaus.askRules.length === 0) return settings;
|
|
1067
|
+
const ownedSet = new Set(prevHaus.askRules);
|
|
1068
|
+
const updated = { ...settings };
|
|
1069
|
+
const remainingAsk = (settings.permissions?.ask ?? []).filter((rule) => !ownedSet.has(rule));
|
|
1070
|
+
const permissions = { ...settings.permissions ?? {} };
|
|
1071
|
+
if (remainingAsk.length > 0) permissions.ask = remainingAsk;
|
|
1072
|
+
else delete permissions.ask;
|
|
1073
|
+
if (Object.keys(permissions).length > 0) updated.permissions = permissions;
|
|
1074
|
+
else delete updated.permissions;
|
|
1075
|
+
const haus = { ...prevHaus };
|
|
1076
|
+
delete haus.askRules;
|
|
1077
|
+
const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0;
|
|
1078
|
+
if (stillTracking) updated._haus = haus;
|
|
1079
|
+
else delete updated._haus;
|
|
1080
|
+
return updated;
|
|
1081
|
+
}
|
|
826
1082
|
async function loadHooksFragment(fragmentPath) {
|
|
827
1083
|
let raw;
|
|
828
1084
|
try {
|
|
@@ -835,44 +1091,48 @@ async function loadHooksFragment(fragmentPath) {
|
|
|
835
1091
|
}
|
|
836
1092
|
|
|
837
1093
|
// src/security/dangerous-commands.ts
|
|
838
|
-
var
|
|
839
|
-
"rm -rf",
|
|
1094
|
+
var DENY_COMMANDS = [
|
|
840
1095
|
"sudo",
|
|
841
1096
|
"chmod -R 777",
|
|
842
|
-
"chown -R",
|
|
843
1097
|
"git push --force",
|
|
844
|
-
"git reset --hard",
|
|
845
|
-
"docker system prune",
|
|
846
1098
|
"drop database",
|
|
847
1099
|
"truncate table",
|
|
848
|
-
"php artisan migrate --force",
|
|
849
1100
|
"npm publish",
|
|
850
1101
|
"yarn npm publish",
|
|
851
1102
|
"pnpm publish"
|
|
852
1103
|
];
|
|
1104
|
+
var ASK_COMMANDS = [
|
|
1105
|
+
"rm -rf",
|
|
1106
|
+
"chown -R",
|
|
1107
|
+
"git reset --hard",
|
|
1108
|
+
"docker system prune",
|
|
1109
|
+
"php artisan migrate --force"
|
|
1110
|
+
];
|
|
853
1111
|
|
|
854
1112
|
// src/security/sensitive-paths.ts
|
|
855
|
-
var
|
|
856
|
-
".env",
|
|
857
|
-
".env.*",
|
|
1113
|
+
var DENY_PATHS = [
|
|
858
1114
|
"*.pem",
|
|
859
1115
|
"*.key",
|
|
860
1116
|
"*.p12",
|
|
861
1117
|
"*.pfx",
|
|
862
1118
|
"id_rsa",
|
|
863
1119
|
"id_ed25519",
|
|
864
|
-
"*.sql",
|
|
865
|
-
"*.dump",
|
|
866
|
-
"*.backup",
|
|
867
|
-
"*.bak",
|
|
868
|
-
"storage/logs",
|
|
869
|
-
"wp-content/uploads",
|
|
870
|
-
"uploads",
|
|
871
1120
|
"customer-data",
|
|
872
|
-
"exports",
|
|
873
1121
|
"secrets",
|
|
874
1122
|
"certs"
|
|
875
1123
|
];
|
|
1124
|
+
var DENY_DIRS = /* @__PURE__ */ new Set(["customer-data", "secrets", "certs"]);
|
|
1125
|
+
var ASK_PATHS = [
|
|
1126
|
+
{ pattern: ".env", tools: ["Edit", "Write"] },
|
|
1127
|
+
{ pattern: ".env.*", tools: ["Edit", "Write"] },
|
|
1128
|
+
{ pattern: "storage/logs/**", tools: ["Edit", "Write"] },
|
|
1129
|
+
{ pattern: "exports/**", tools: ["Edit", "Write"] },
|
|
1130
|
+
{ pattern: "*.dump", tools: ["Read", "Edit", "Write"] },
|
|
1131
|
+
{ pattern: "*.backup", tools: ["Read", "Edit", "Write"] },
|
|
1132
|
+
{ pattern: "*.bak", tools: ["Read", "Edit", "Write"] },
|
|
1133
|
+
{ pattern: "wp-content/uploads/**", tools: ["Read", "Edit", "Write"] },
|
|
1134
|
+
{ pattern: "uploads/**", tools: ["Read", "Edit", "Write"] }
|
|
1135
|
+
];
|
|
876
1136
|
var SENSITIVE_PATH_REGEXES = [
|
|
877
1137
|
/^\.env(\.|$)/,
|
|
878
1138
|
/(^|\/)\.env(\.|$)/,
|
|
@@ -900,24 +1160,29 @@ var SENSITIVE_ITEM_KEYWORDS = [
|
|
|
900
1160
|
".key"
|
|
901
1161
|
];
|
|
902
1162
|
|
|
1163
|
+
// src/security/ask-rules.ts
|
|
1164
|
+
function buildAskRules() {
|
|
1165
|
+
const rules = [];
|
|
1166
|
+
for (const command of ASK_COMMANDS) {
|
|
1167
|
+
rules.push(`Bash(${command}:*)`);
|
|
1168
|
+
}
|
|
1169
|
+
for (const { pattern, tools } of ASK_PATHS) {
|
|
1170
|
+
for (const tool of tools) {
|
|
1171
|
+
rules.push(`${tool}(${pattern})`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return [...new Set(rules)];
|
|
1175
|
+
}
|
|
1176
|
+
|
|
903
1177
|
// src/security/deny-rules.ts
|
|
904
|
-
var SENSITIVE_DIRS = /* @__PURE__ */ new Set([
|
|
905
|
-
"storage/logs",
|
|
906
|
-
"wp-content/uploads",
|
|
907
|
-
"uploads",
|
|
908
|
-
"customer-data",
|
|
909
|
-
"exports",
|
|
910
|
-
"secrets",
|
|
911
|
-
"certs"
|
|
912
|
-
]);
|
|
913
1178
|
var FILE_TOOLS = ["Read", "Edit", "Write"];
|
|
914
1179
|
function buildDenyRules() {
|
|
915
1180
|
const rules = [];
|
|
916
|
-
for (const command of
|
|
1181
|
+
for (const command of DENY_COMMANDS) {
|
|
917
1182
|
rules.push(`Bash(${command}:*)`);
|
|
918
1183
|
}
|
|
919
|
-
for (const
|
|
920
|
-
const pattern =
|
|
1184
|
+
for (const path37 of DENY_PATHS) {
|
|
1185
|
+
const pattern = DENY_DIRS.has(path37) ? `${path37}/**` : path37;
|
|
921
1186
|
for (const tool of FILE_TOOLS) {
|
|
922
1187
|
rules.push(`${tool}(${pattern})`);
|
|
923
1188
|
}
|
|
@@ -1005,7 +1270,8 @@ async function mergeProjectSettings(root) {
|
|
|
1005
1270
|
const base = await readProjectSettings(root);
|
|
1006
1271
|
const { settings: withHooks } = mergeHooks(base, PROJECT_HOOK_FRAGMENTS);
|
|
1007
1272
|
const { settings: withDeny } = mergeDenyRules(withHooks, buildDenyRules());
|
|
1008
|
-
const { settings:
|
|
1273
|
+
const { settings: withAllow } = mergeAllowRules(withDeny, buildAllowRules());
|
|
1274
|
+
const { settings: merged } = mergeAskRules(withAllow, buildAskRules());
|
|
1009
1275
|
return merged;
|
|
1010
1276
|
}
|
|
1011
1277
|
async function applyProjectSettingsMerge(root) {
|
|
@@ -1015,8 +1281,8 @@ async function applyProjectSettingsMerge(root) {
|
|
|
1015
1281
|
}
|
|
1016
1282
|
|
|
1017
1283
|
// src/claude/write-claude-files.ts
|
|
1018
|
-
import
|
|
1019
|
-
import
|
|
1284
|
+
import path13 from "path";
|
|
1285
|
+
import fs12 from "fs-extra";
|
|
1020
1286
|
|
|
1021
1287
|
// src/catalog/load-catalog.ts
|
|
1022
1288
|
import path6 from "path";
|
|
@@ -1183,8 +1449,59 @@ async function writeManagedJson(root, filePath, value, dryRun) {
|
|
|
1183
1449
|
await writeManagedText(root, filePath, nextText, dryRun);
|
|
1184
1450
|
}
|
|
1185
1451
|
|
|
1186
|
-
// src/claude/
|
|
1452
|
+
// src/claude/superpowers-install.ts
|
|
1453
|
+
import path9 from "path";
|
|
1454
|
+
import fg3 from "fast-glob";
|
|
1187
1455
|
import fs6 from "fs-extra";
|
|
1456
|
+
var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
|
|
1457
|
+
var PATH_REWRITES = [
|
|
1458
|
+
["skills/shared/", ".claude/skills/shared/"]
|
|
1459
|
+
];
|
|
1460
|
+
function rewriteSuperpowersMarkdown(text) {
|
|
1461
|
+
let out = text;
|
|
1462
|
+
for (const [from, to] of PATH_REWRITES) {
|
|
1463
|
+
out = out.split(from).join(to);
|
|
1464
|
+
}
|
|
1465
|
+
return out;
|
|
1466
|
+
}
|
|
1467
|
+
async function rewriteMarkdownTree(dir) {
|
|
1468
|
+
const files = await fg3("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
|
|
1469
|
+
for (const rel of files) {
|
|
1470
|
+
const abs = path9.join(dir, rel);
|
|
1471
|
+
const text = await fs6.readFile(abs, "utf8");
|
|
1472
|
+
const rewritten = rewriteSuperpowersMarkdown(text);
|
|
1473
|
+
if (rewritten !== text) {
|
|
1474
|
+
await fs6.writeFile(abs, rewritten, "utf8");
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
async function installCatalogSkill(sourcePath, destination, opts) {
|
|
1479
|
+
if (opts.dryRun) return;
|
|
1480
|
+
await fs6.ensureDir(path9.dirname(destination));
|
|
1481
|
+
if (await fs6.pathExists(destination)) {
|
|
1482
|
+
await fs6.remove(destination);
|
|
1483
|
+
}
|
|
1484
|
+
await fs6.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
1485
|
+
if (opts.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID) {
|
|
1486
|
+
await rewriteMarkdownTree(destination);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
|
|
1490
|
+
const source = path9.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
|
|
1491
|
+
if (!await fs6.pathExists(source)) return null;
|
|
1492
|
+
const destination = claudePath(projectRoot, "skills", "shared");
|
|
1493
|
+
if (dryRun) return path9.relative(projectRoot, destination);
|
|
1494
|
+
await fs6.ensureDir(path9.dirname(destination));
|
|
1495
|
+
if (await fs6.pathExists(destination)) {
|
|
1496
|
+
await fs6.remove(destination);
|
|
1497
|
+
}
|
|
1498
|
+
await fs6.copy(source, destination, { overwrite: true, errorOnExist: false });
|
|
1499
|
+
await rewriteMarkdownTree(destination);
|
|
1500
|
+
return path9.relative(projectRoot, destination);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// src/claude/verify-hooks-contract.ts
|
|
1504
|
+
import fs7 from "fs-extra";
|
|
1188
1505
|
|
|
1189
1506
|
// src/claude/load-hooks.ts
|
|
1190
1507
|
var CANONICAL_HOOKS = {
|
|
@@ -1212,7 +1529,7 @@ var STABLE_HOOK_IDS = {
|
|
|
1212
1529
|
"haus guard bash --from-hook": "haus.guard-bash"
|
|
1213
1530
|
};
|
|
1214
1531
|
async function loadClaudeHooksSettings() {
|
|
1215
|
-
return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules() } };
|
|
1532
|
+
return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules(), ask: buildAskRules() } };
|
|
1216
1533
|
}
|
|
1217
1534
|
function flattenRecommendedHooks(settings) {
|
|
1218
1535
|
const out = [];
|
|
@@ -1278,7 +1595,7 @@ async function assertPostApplySettingsHausContract(root) {
|
|
|
1278
1595
|
}
|
|
1279
1596
|
async function verifyProjectSettingsHooksContract(root) {
|
|
1280
1597
|
const settingsPath = claudePath(root, "settings.json");
|
|
1281
|
-
if (!await
|
|
1598
|
+
if (!await fs7.pathExists(settingsPath)) {
|
|
1282
1599
|
return {
|
|
1283
1600
|
ok: true,
|
|
1284
1601
|
skipped: true,
|
|
@@ -1305,8 +1622,8 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
1305
1622
|
}
|
|
1306
1623
|
|
|
1307
1624
|
// src/claude/write-root-claude-md.ts
|
|
1308
|
-
import
|
|
1309
|
-
import
|
|
1625
|
+
import path10 from "path";
|
|
1626
|
+
import fs8 from "fs-extra";
|
|
1310
1627
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
1311
1628
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
1312
1629
|
var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
|
|
@@ -1346,21 +1663,21 @@ ${block}
|
|
|
1346
1663
|
`;
|
|
1347
1664
|
}
|
|
1348
1665
|
async function writeRootClaudeMd(root, dryRun) {
|
|
1349
|
-
const filePath =
|
|
1666
|
+
const filePath = path10.join(root, "CLAUDE.md");
|
|
1350
1667
|
const block = buildImportBlock();
|
|
1351
|
-
const prev = await
|
|
1668
|
+
const prev = await fs8.pathExists(filePath) ? await fs8.readFile(filePath, "utf8") : "";
|
|
1352
1669
|
const next = injectHausBlock(prev, block);
|
|
1353
1670
|
await writeManagedText(root, filePath, next, dryRun);
|
|
1354
1671
|
return filePath;
|
|
1355
1672
|
}
|
|
1356
1673
|
|
|
1357
1674
|
// src/claude/write-workflow-config.ts
|
|
1358
|
-
import
|
|
1359
|
-
import
|
|
1675
|
+
import path12 from "path";
|
|
1676
|
+
import fs10 from "fs-extra";
|
|
1360
1677
|
|
|
1361
1678
|
// src/claude/derive-workflow-config.ts
|
|
1362
|
-
import
|
|
1363
|
-
import
|
|
1679
|
+
import path11 from "path";
|
|
1680
|
+
import fs9 from "fs-extra";
|
|
1364
1681
|
function binCmd(pm, bin, args) {
|
|
1365
1682
|
const tail = args ? ` ${args}` : "";
|
|
1366
1683
|
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
@@ -1369,7 +1686,7 @@ function binCmd(pm, bin, args) {
|
|
|
1369
1686
|
}
|
|
1370
1687
|
async function deriveWorkflowConfig(root, ctx) {
|
|
1371
1688
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
1372
|
-
const pkg = await readJson(
|
|
1689
|
+
const pkg = await readJson(path11.join(root, "package.json"));
|
|
1373
1690
|
const scripts = pkg?.scripts ?? {};
|
|
1374
1691
|
const deps = new Set(ctx.dependencies);
|
|
1375
1692
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -1379,7 +1696,7 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
1379
1696
|
return null;
|
|
1380
1697
|
};
|
|
1381
1698
|
const hasDep = (name) => deps.has(name);
|
|
1382
|
-
const exists = (rel) =>
|
|
1699
|
+
const exists = (rel) => fs9.pathExistsSync(path11.join(root, rel));
|
|
1383
1700
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
1384
1701
|
const hasCypress = hasDep("cypress");
|
|
1385
1702
|
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;
|
|
@@ -1448,7 +1765,7 @@ var FALLBACK_CONTEXT = {
|
|
|
1448
1765
|
async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
1449
1766
|
const destPath = hausPath(root, "workflow-config.md");
|
|
1450
1767
|
const printable = displayPath(root, destPath);
|
|
1451
|
-
const exists = await
|
|
1768
|
+
const exists = await fs10.pathExists(destPath);
|
|
1452
1769
|
if (exists && !opts.refill) {
|
|
1453
1770
|
if (dryRun) log(printable + ": exists (project-owned, skipping)");
|
|
1454
1771
|
return null;
|
|
@@ -1456,11 +1773,11 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
1456
1773
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
1457
1774
|
...FALLBACK_CONTEXT,
|
|
1458
1775
|
root,
|
|
1459
|
-
repoName:
|
|
1776
|
+
repoName: path12.basename(root)
|
|
1460
1777
|
};
|
|
1461
1778
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
1462
1779
|
if (exists) {
|
|
1463
|
-
const current = await
|
|
1780
|
+
const current = await fs10.readFile(destPath, "utf8");
|
|
1464
1781
|
const refilled = refillContent(current, values);
|
|
1465
1782
|
if (refilled === current) {
|
|
1466
1783
|
if (dryRun) log(printable + ": no blank fields to refill");
|
|
@@ -1482,7 +1799,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
1482
1799
|
}
|
|
1483
1800
|
|
|
1484
1801
|
// src/claude/write-workflow.ts
|
|
1485
|
-
import
|
|
1802
|
+
import fs11 from "fs-extra";
|
|
1486
1803
|
var STABLE_ID = "template.workflow";
|
|
1487
1804
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
1488
1805
|
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
@@ -1501,8 +1818,8 @@ async function writeWorkflow(root, pkgVersion, dryRun, force = false) {
|
|
|
1501
1818
|
${templateContent}`;
|
|
1502
1819
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
1503
1820
|
const printable = displayPath(root, destPath);
|
|
1504
|
-
if (await
|
|
1505
|
-
const existing = await
|
|
1821
|
+
if (await fs11.pathExists(destPath)) {
|
|
1822
|
+
const existing = await fs11.readFile(destPath, "utf8");
|
|
1506
1823
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
1507
1824
|
const parsed = parseHausManagedHeader(firstLine);
|
|
1508
1825
|
if (!parsed) {
|
|
@@ -1530,7 +1847,7 @@ ${templateContent}`;
|
|
|
1530
1847
|
}
|
|
1531
1848
|
}
|
|
1532
1849
|
if (dryRun) {
|
|
1533
|
-
const prev = await
|
|
1850
|
+
const prev = await fs11.pathExists(destPath) ? await fs11.readFile(destPath, "utf8") : "";
|
|
1534
1851
|
if (!prev) {
|
|
1535
1852
|
log(createUnifiedDiff(printable, "", next));
|
|
1536
1853
|
} else {
|
|
@@ -1564,7 +1881,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1564
1881
|
estimatedTokenReductionPct: 0
|
|
1565
1882
|
};
|
|
1566
1883
|
const pkgRoot = packageRoot();
|
|
1567
|
-
const hausVersion2 = (await readJson(
|
|
1884
|
+
const hausVersion2 = (await readJson(path13.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
1568
1885
|
const coreFiles = [
|
|
1569
1886
|
claudePath(root, "settings.json"),
|
|
1570
1887
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -1596,7 +1913,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1596
1913
|
await assertPostApplySettingsHausContract(root);
|
|
1597
1914
|
}
|
|
1598
1915
|
const configPath = hausPath(root, "config.json");
|
|
1599
|
-
if (!await
|
|
1916
|
+
if (!await fs12.pathExists(configPath)) {
|
|
1600
1917
|
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
1601
1918
|
}
|
|
1602
1919
|
await writeManagedText(
|
|
@@ -1629,6 +1946,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1629
1946
|
const installedIds = /* @__PURE__ */ new Set();
|
|
1630
1947
|
const catalogItems = selectedIds !== void 0 ? rec.recommended.filter((r) => selectedIds.includes(r.id)) : rec.recommended;
|
|
1631
1948
|
let curatedReviewStatusSkips = 0;
|
|
1949
|
+
let superpowersSharedInstalled = false;
|
|
1632
1950
|
for (const item of catalogItems) {
|
|
1633
1951
|
const manifestItem = manifestById.get(item.id);
|
|
1634
1952
|
if (!manifestItem?.path) continue;
|
|
@@ -1655,20 +1973,34 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1655
1973
|
);
|
|
1656
1974
|
continue;
|
|
1657
1975
|
}
|
|
1658
|
-
const destination = claudePath(root, target,
|
|
1659
|
-
if (await
|
|
1976
|
+
const destination = claudePath(root, target, path13.basename(sourcePath));
|
|
1977
|
+
if (await fs12.pathExists(sourcePath)) {
|
|
1660
1978
|
if (dryRun) {
|
|
1661
|
-
const exists = await
|
|
1979
|
+
const exists = await fs12.pathExists(destination);
|
|
1662
1980
|
log(
|
|
1663
1981
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
1664
1982
|
);
|
|
1983
|
+
} else if (item.type === "skill") {
|
|
1984
|
+
await installCatalogSkill(sourcePath, destination, {
|
|
1985
|
+
originSourceId: manifestItem.originSourceId,
|
|
1986
|
+
dryRun: false
|
|
1987
|
+
});
|
|
1665
1988
|
} else {
|
|
1666
|
-
await
|
|
1667
|
-
await
|
|
1989
|
+
await fs12.ensureDir(path13.dirname(destination));
|
|
1990
|
+
await fs12.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
1668
1991
|
}
|
|
1669
1992
|
files.push(destination);
|
|
1993
|
+
const relPaths = [path13.relative(root, destination)];
|
|
1994
|
+
if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
|
|
1995
|
+
const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
|
|
1996
|
+
if (sharedRel) {
|
|
1997
|
+
superpowersSharedInstalled = true;
|
|
1998
|
+
relPaths.push(sharedRel);
|
|
1999
|
+
files.push(path13.join(root, sharedRel));
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
1670
2002
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
1671
|
-
installedPathsByItem.set(item.id, [...current,
|
|
2003
|
+
installedPathsByItem.set(item.id, [...current, ...relPaths]);
|
|
1672
2004
|
installedIds.add(item.id);
|
|
1673
2005
|
} else {
|
|
1674
2006
|
warn(
|
|
@@ -1738,7 +2070,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
1738
2070
|
if (relPaths.length === 0) continue;
|
|
1739
2071
|
const existing = [];
|
|
1740
2072
|
for (const rel of relPaths) {
|
|
1741
|
-
if (await
|
|
2073
|
+
if (await fs12.pathExists(path13.join(root, rel))) existing.push(rel);
|
|
1742
2074
|
}
|
|
1743
2075
|
if (existing.length === 0) continue;
|
|
1744
2076
|
if (entry.hash === void 0) {
|
|
@@ -1755,13 +2087,13 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
1755
2087
|
continue;
|
|
1756
2088
|
}
|
|
1757
2089
|
for (const rel of existing) {
|
|
1758
|
-
const abs =
|
|
2090
|
+
const abs = path13.join(root, rel);
|
|
1759
2091
|
if (dryRun) {
|
|
1760
2092
|
log(`[dry-run] would remove stale ${displayPath(root, abs)} (${entry.id})`);
|
|
1761
2093
|
continue;
|
|
1762
2094
|
}
|
|
1763
|
-
await
|
|
1764
|
-
await pruneEmptyDir(
|
|
2095
|
+
await fs12.remove(abs);
|
|
2096
|
+
await pruneEmptyDir(path13.dirname(abs));
|
|
1765
2097
|
log(`Removed stale ${displayPath(root, abs)} (${entry.id})`);
|
|
1766
2098
|
}
|
|
1767
2099
|
}
|
|
@@ -1769,7 +2101,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
1769
2101
|
|
|
1770
2102
|
// src/commands/apply.ts
|
|
1771
2103
|
async function cacheHasItems() {
|
|
1772
|
-
const data = await readJson(
|
|
2104
|
+
const data = await readJson(path14.join(getCacheDir(), "manifest.json"));
|
|
1773
2105
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
1774
2106
|
}
|
|
1775
2107
|
async function runApply(options) {
|
|
@@ -1835,8 +2167,8 @@ async function runApply(options) {
|
|
|
1835
2167
|
}
|
|
1836
2168
|
}
|
|
1837
2169
|
async function isHausProject(root) {
|
|
1838
|
-
if (await
|
|
1839
|
-
if (await
|
|
2170
|
+
if (await fs13.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
2171
|
+
if (await fs13.pathExists(claudePath(root, "settings.json"))) {
|
|
1840
2172
|
const settings = await readProjectSettings(root);
|
|
1841
2173
|
if (settings._haus != null) return true;
|
|
1842
2174
|
}
|
|
@@ -1874,7 +2206,7 @@ async function runCatalogAudit() {
|
|
|
1874
2206
|
|
|
1875
2207
|
// src/commands/clone.ts
|
|
1876
2208
|
import { existsSync as existsSync2 } from "fs";
|
|
1877
|
-
import
|
|
2209
|
+
import path15 from "path";
|
|
1878
2210
|
|
|
1879
2211
|
// src/utils/exec.ts
|
|
1880
2212
|
import { execa } from "execa";
|
|
@@ -1902,6 +2234,20 @@ async function runGit(args, options = {}) {
|
|
|
1902
2234
|
}
|
|
1903
2235
|
|
|
1904
2236
|
// src/commands/clone.ts
|
|
2237
|
+
var GIT_LOCATION_VARS = [
|
|
2238
|
+
"GIT_DIR",
|
|
2239
|
+
"GIT_WORK_TREE",
|
|
2240
|
+
"GIT_INDEX_FILE",
|
|
2241
|
+
"GIT_COMMON_DIR",
|
|
2242
|
+
"GIT_OBJECT_DIRECTORY",
|
|
2243
|
+
"GIT_NAMESPACE",
|
|
2244
|
+
"GIT_PREFIX"
|
|
2245
|
+
];
|
|
2246
|
+
function cloneEnv() {
|
|
2247
|
+
const env = { ...process.env };
|
|
2248
|
+
for (const key of GIT_LOCATION_VARS) delete env[key];
|
|
2249
|
+
return env;
|
|
2250
|
+
}
|
|
1905
2251
|
function repoNameFromUrl(url) {
|
|
1906
2252
|
const trimmed = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
1907
2253
|
const tail = trimmed.split(/[/:]/).pop() ?? "";
|
|
@@ -1913,16 +2259,16 @@ async function runClone(url, opts = {}) {
|
|
|
1913
2259
|
process.exitCode = 1;
|
|
1914
2260
|
return;
|
|
1915
2261
|
}
|
|
1916
|
-
const target =
|
|
2262
|
+
const target = path15.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
1917
2263
|
if (existsSync2(target)) {
|
|
1918
|
-
log(`\u2022 ${
|
|
2264
|
+
log(`\u2022 ${path15.basename(target)} already present at ${target} \u2014 skipped`);
|
|
1919
2265
|
return;
|
|
1920
2266
|
}
|
|
1921
2267
|
if (opts.dryRun) {
|
|
1922
2268
|
log(`would clone ${url} \u2192 ${target}`);
|
|
1923
2269
|
return;
|
|
1924
2270
|
}
|
|
1925
|
-
const res = await runGit(["clone", url, target]);
|
|
2271
|
+
const res = await runGit(["clone", url, target], { env: cloneEnv(), extendEnv: false });
|
|
1926
2272
|
if (res.exitCode !== 0) {
|
|
1927
2273
|
error(`clone failed for ${url}: ${(res.stderr || res.stdout).trim()}`);
|
|
1928
2274
|
process.exitCode = 1;
|
|
@@ -1932,7 +2278,7 @@ async function runClone(url, opts = {}) {
|
|
|
1932
2278
|
}
|
|
1933
2279
|
|
|
1934
2280
|
// src/commands/config.ts
|
|
1935
|
-
import
|
|
2281
|
+
import path16 from "path";
|
|
1936
2282
|
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
1937
2283
|
var HOOK_ALIASES = {
|
|
1938
2284
|
"hook.context": "context"
|
|
@@ -1945,7 +2291,7 @@ async function runConfig(key, action) {
|
|
|
1945
2291
|
);
|
|
1946
2292
|
}
|
|
1947
2293
|
const root = process.cwd();
|
|
1948
|
-
const configPath =
|
|
2294
|
+
const configPath = path16.join(root, CONFIG_PATH2);
|
|
1949
2295
|
const existing = await readJson(configPath);
|
|
1950
2296
|
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
1951
2297
|
cfg.hooks ??= {};
|
|
@@ -2325,7 +2671,7 @@ function selectRules(recommended, task, taskIntents) {
|
|
|
2325
2671
|
|
|
2326
2672
|
// src/scanner/scan-project.ts
|
|
2327
2673
|
import { readFile as readFile2 } from "fs/promises";
|
|
2328
|
-
import
|
|
2674
|
+
import path20 from "path";
|
|
2329
2675
|
|
|
2330
2676
|
// src/utils/audit-checks.ts
|
|
2331
2677
|
function isRecord(v) {
|
|
@@ -2352,8 +2698,8 @@ function compareVersions(a, b) {
|
|
|
2352
2698
|
}
|
|
2353
2699
|
|
|
2354
2700
|
// src/scanner/detect-package-manager.ts
|
|
2355
|
-
import
|
|
2356
|
-
import
|
|
2701
|
+
import path17 from "path";
|
|
2702
|
+
import fs14 from "fs-extra";
|
|
2357
2703
|
function detectPackageManager(root, packageManagerField) {
|
|
2358
2704
|
const field = String(packageManagerField ?? "").trim();
|
|
2359
2705
|
if (field.startsWith("yarn@")) {
|
|
@@ -2371,9 +2717,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2371
2717
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2372
2718
|
return "unknown";
|
|
2373
2719
|
}
|
|
2374
|
-
if (
|
|
2375
|
-
if (
|
|
2376
|
-
if (
|
|
2720
|
+
if (fs14.existsSync(path17.join(root, "yarn.lock"))) return "yarn";
|
|
2721
|
+
if (fs14.existsSync(path17.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2722
|
+
if (fs14.existsSync(path17.join(root, "package-lock.json"))) return "npm";
|
|
2377
2723
|
return "unknown";
|
|
2378
2724
|
}
|
|
2379
2725
|
|
|
@@ -2570,7 +2916,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
2570
2916
|
}
|
|
2571
2917
|
|
|
2572
2918
|
// src/scanner/detection.ts
|
|
2573
|
-
import
|
|
2919
|
+
import path18 from "path";
|
|
2574
2920
|
var UNSUPPORTED_MARKERS = {
|
|
2575
2921
|
"requirements.txt": "python",
|
|
2576
2922
|
"pyproject.toml": "python",
|
|
@@ -2624,14 +2970,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
2624
2970
|
function collectUnsupportedSignals(files) {
|
|
2625
2971
|
return [
|
|
2626
2972
|
...new Set(
|
|
2627
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
2973
|
+
files.map((f) => UNSUPPORTED_MARKERS[path18.basename(f)]).filter((s) => Boolean(s))
|
|
2628
2974
|
)
|
|
2629
2975
|
].sort();
|
|
2630
2976
|
}
|
|
2631
2977
|
|
|
2632
2978
|
// src/scanner/render.ts
|
|
2633
2979
|
import { readFile } from "fs/promises";
|
|
2634
|
-
import
|
|
2980
|
+
import path19 from "path";
|
|
2635
2981
|
|
|
2636
2982
|
// src/scanner/role-labels.ts
|
|
2637
2983
|
var ROLE_LABELS = {
|
|
@@ -2693,7 +3039,7 @@ async function buildContentBlob(root, files) {
|
|
|
2693
3039
|
const slice = candidates.slice(0, 300);
|
|
2694
3040
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
2695
3041
|
try {
|
|
2696
|
-
return await readFile(
|
|
3042
|
+
return await readFile(path19.join(root, rel), "utf8");
|
|
2697
3043
|
} catch {
|
|
2698
3044
|
return "";
|
|
2699
3045
|
}
|
|
@@ -2791,8 +3137,8 @@ var SAFE_FILES = [
|
|
|
2791
3137
|
"Gemfile"
|
|
2792
3138
|
];
|
|
2793
3139
|
async function scanProject(root, mode = "fast") {
|
|
2794
|
-
const pkg = await readJson(
|
|
2795
|
-
const composer = await readJson(
|
|
3140
|
+
const pkg = await readJson(path20.join(root, "package.json"));
|
|
3141
|
+
const composer = await readJson(path20.join(root, "composer.json"));
|
|
2796
3142
|
const files = await listFiles(root, SAFE_FILES);
|
|
2797
3143
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
2798
3144
|
const deps = dependencySet(pkg, composer);
|
|
@@ -2826,7 +3172,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2826
3172
|
mode,
|
|
2827
3173
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2828
3174
|
root,
|
|
2829
|
-
repoName: String(pkg?.name ??
|
|
3175
|
+
repoName: String(pkg?.name ?? path20.basename(root)),
|
|
2830
3176
|
packageManager,
|
|
2831
3177
|
repoRoles: roles,
|
|
2832
3178
|
detectedStacks: stacks,
|
|
@@ -2844,7 +3190,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2844
3190
|
const scanHashes = Object.fromEntries(
|
|
2845
3191
|
await mapWithConcurrency(
|
|
2846
3192
|
safeFiles,
|
|
2847
|
-
async (f) => [f, hashText(await readFile2(
|
|
3193
|
+
async (f) => [f, hashText(await readFile2(path20.join(root, f), "utf8"))]
|
|
2848
3194
|
)
|
|
2849
3195
|
);
|
|
2850
3196
|
const repoSummary = renderSummary(context);
|
|
@@ -2929,8 +3275,8 @@ async function runContext(options) {
|
|
|
2929
3275
|
}
|
|
2930
3276
|
|
|
2931
3277
|
// src/commands/doctor.ts
|
|
2932
|
-
import
|
|
2933
|
-
import
|
|
3278
|
+
import path21 from "path";
|
|
3279
|
+
import fs15 from "fs-extra";
|
|
2934
3280
|
|
|
2935
3281
|
// src/update/npm-version.ts
|
|
2936
3282
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
@@ -3010,7 +3356,7 @@ async function runDoctor(options) {
|
|
|
3010
3356
|
const enabled = await isHookEnabled(root, key);
|
|
3011
3357
|
ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
3012
3358
|
}
|
|
3013
|
-
const rootClaudeMdPath =
|
|
3359
|
+
const rootClaudeMdPath = path21.join(root, "CLAUDE.md");
|
|
3014
3360
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
3015
3361
|
if (!rootClaudeMdContent) {
|
|
3016
3362
|
flag(
|
|
@@ -3038,7 +3384,7 @@ async function runDoctor(options) {
|
|
|
3038
3384
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
3039
3385
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
3040
3386
|
for (const target of importTargets) {
|
|
3041
|
-
if (!await
|
|
3387
|
+
if (!await fs15.pathExists(hausPath(root, target))) {
|
|
3042
3388
|
flag(
|
|
3043
3389
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
3044
3390
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -3049,7 +3395,7 @@ async function runDoctor(options) {
|
|
|
3049
3395
|
}
|
|
3050
3396
|
}
|
|
3051
3397
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
3052
|
-
const workflowExists = await
|
|
3398
|
+
const workflowExists = await fs15.pathExists(workflowPath);
|
|
3053
3399
|
if (!workflowExists) {
|
|
3054
3400
|
flag(
|
|
3055
3401
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -3072,15 +3418,15 @@ async function runDoctor(options) {
|
|
|
3072
3418
|
"haus apply --write --force"
|
|
3073
3419
|
);
|
|
3074
3420
|
} else {
|
|
3075
|
-
const cachePath =
|
|
3076
|
-
const bundledPath =
|
|
3421
|
+
const cachePath = path21.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
3422
|
+
const bundledPath = path21.join(
|
|
3077
3423
|
packageRoot(),
|
|
3078
3424
|
"library",
|
|
3079
3425
|
"global",
|
|
3080
3426
|
"templates",
|
|
3081
3427
|
"agentic-workflow-standard.md"
|
|
3082
3428
|
);
|
|
3083
|
-
const templatePath = await
|
|
3429
|
+
const templatePath = await fs15.pathExists(cachePath) ? cachePath : bundledPath;
|
|
3084
3430
|
const templateContent = await readText(templatePath);
|
|
3085
3431
|
if (storedHashMatch && templateContent) {
|
|
3086
3432
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
@@ -3100,7 +3446,7 @@ async function runDoctor(options) {
|
|
|
3100
3446
|
}
|
|
3101
3447
|
}
|
|
3102
3448
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
3103
|
-
const workflowConfigExists = await
|
|
3449
|
+
const workflowConfigExists = await fs15.pathExists(workflowConfigPath);
|
|
3104
3450
|
if (!workflowConfigExists) {
|
|
3105
3451
|
flag(
|
|
3106
3452
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -3108,7 +3454,7 @@ async function runDoctor(options) {
|
|
|
3108
3454
|
"haus apply --write"
|
|
3109
3455
|
);
|
|
3110
3456
|
} else {
|
|
3111
|
-
const cfg = await
|
|
3457
|
+
const cfg = await fs15.readFile(workflowConfigPath, "utf8");
|
|
3112
3458
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
3113
3459
|
if (unfilled > 0) {
|
|
3114
3460
|
flag(
|
|
@@ -3139,7 +3485,7 @@ async function runDoctor(options) {
|
|
|
3139
3485
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
3140
3486
|
}
|
|
3141
3487
|
}
|
|
3142
|
-
const pkgJson = await readJson(
|
|
3488
|
+
const pkgJson = await readJson(path21.join(packageRoot(), "package.json"));
|
|
3143
3489
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3144
3490
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3145
3491
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3225,15 +3571,16 @@ import { readFileSync as readFileSync2 } from "fs";
|
|
|
3225
3571
|
|
|
3226
3572
|
// src/security/guard-bash.ts
|
|
3227
3573
|
function guardBash(command) {
|
|
3228
|
-
const matched =
|
|
3574
|
+
const matched = DENY_COMMANDS.find((token) => command.includes(token));
|
|
3229
3575
|
if (matched) return `I didn't run that \u2014 it can permanently change or delete things: ${command}`;
|
|
3230
3576
|
return void 0;
|
|
3231
3577
|
}
|
|
3232
3578
|
|
|
3233
3579
|
// src/security/guard-file-access.ts
|
|
3234
3580
|
function guardFileAccess(candidate) {
|
|
3235
|
-
const matched =
|
|
3236
|
-
if (matched)
|
|
3581
|
+
const matched = DENY_PATHS.find((token) => candidate.includes(token.replace(/\*/g, "")));
|
|
3582
|
+
if (matched)
|
|
3583
|
+
return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
|
|
3237
3584
|
return void 0;
|
|
3238
3585
|
}
|
|
3239
3586
|
|
|
@@ -3282,8 +3629,8 @@ async function runGuard(kind, _options) {
|
|
|
3282
3629
|
}
|
|
3283
3630
|
|
|
3284
3631
|
// src/commands/init.ts
|
|
3285
|
-
import
|
|
3286
|
-
import
|
|
3632
|
+
import path22 from "path";
|
|
3633
|
+
import fs16 from "fs-extra";
|
|
3287
3634
|
|
|
3288
3635
|
// src/utils/prompts.ts
|
|
3289
3636
|
import { stdin as input, stdout as output } from "process";
|
|
@@ -3644,8 +3991,8 @@ async function runSetupProject(options) {
|
|
|
3644
3991
|
// src/commands/init.ts
|
|
3645
3992
|
async function runInit(options) {
|
|
3646
3993
|
const root = process.cwd();
|
|
3647
|
-
const hausDir =
|
|
3648
|
-
const alreadyInit = await
|
|
3994
|
+
const hausDir = path22.join(root, ".haus-workflow");
|
|
3995
|
+
const alreadyInit = await fs16.pathExists(hausDir);
|
|
3649
3996
|
if (alreadyInit) {
|
|
3650
3997
|
log("Haus AI already initialized in this project.");
|
|
3651
3998
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -3657,8 +4004,8 @@ async function runInit(options) {
|
|
|
3657
4004
|
|
|
3658
4005
|
// src/install/apply.ts
|
|
3659
4006
|
import crypto2 from "crypto";
|
|
3660
|
-
import
|
|
3661
|
-
import
|
|
4007
|
+
import path23 from "path";
|
|
4008
|
+
import fs17 from "fs-extra";
|
|
3662
4009
|
|
|
3663
4010
|
// src/install/header.ts
|
|
3664
4011
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -3746,40 +4093,40 @@ function hashContent(content2) {
|
|
|
3746
4093
|
}
|
|
3747
4094
|
function sourceVersion() {
|
|
3748
4095
|
try {
|
|
3749
|
-
const pkgPath =
|
|
3750
|
-
const pkg = JSON.parse(
|
|
4096
|
+
const pkgPath = path23.join(packageRoot(), "package.json");
|
|
4097
|
+
const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
|
|
3751
4098
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
3752
4099
|
} catch {
|
|
3753
4100
|
return "haus@0.0.0";
|
|
3754
4101
|
}
|
|
3755
4102
|
}
|
|
3756
4103
|
function globalSrcDir() {
|
|
3757
|
-
return
|
|
4104
|
+
return path23.join(packageRoot(), "library", "global");
|
|
3758
4105
|
}
|
|
3759
4106
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
3760
4107
|
const entries = [];
|
|
3761
|
-
const skillsDir =
|
|
3762
|
-
if (
|
|
3763
|
-
for (const skillName of
|
|
3764
|
-
const skillFile =
|
|
3765
|
-
if (
|
|
4108
|
+
const skillsDir = path23.join(srcDir, "skills");
|
|
4109
|
+
if (fs17.pathExistsSync(skillsDir)) {
|
|
4110
|
+
for (const skillName of fs17.readdirSync(skillsDir)) {
|
|
4111
|
+
const skillFile = path23.join(skillsDir, skillName, "SKILL.md");
|
|
4112
|
+
if (fs17.pathExistsSync(skillFile)) {
|
|
3766
4113
|
entries.push({
|
|
3767
4114
|
stableId: `skill.${skillName}`,
|
|
3768
|
-
srcRelPath:
|
|
3769
|
-
destPath:
|
|
4115
|
+
srcRelPath: path23.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
4116
|
+
destPath: path23.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
3770
4117
|
});
|
|
3771
4118
|
}
|
|
3772
4119
|
}
|
|
3773
4120
|
}
|
|
3774
|
-
const commandsDir =
|
|
3775
|
-
if (
|
|
3776
|
-
for (const fileName of
|
|
4121
|
+
const commandsDir = path23.join(srcDir, "commands");
|
|
4122
|
+
if (fs17.pathExistsSync(commandsDir)) {
|
|
4123
|
+
for (const fileName of fs17.readdirSync(commandsDir)) {
|
|
3777
4124
|
if (!fileName.endsWith(".md")) continue;
|
|
3778
4125
|
const commandName = fileName.slice(0, -".md".length);
|
|
3779
4126
|
entries.push({
|
|
3780
4127
|
stableId: `command.${commandName}`,
|
|
3781
|
-
srcRelPath:
|
|
3782
|
-
destPath:
|
|
4128
|
+
srcRelPath: path23.join("library", "global", "commands", fileName),
|
|
4129
|
+
destPath: path23.join(claudeDir, "commands", fileName)
|
|
3783
4130
|
});
|
|
3784
4131
|
}
|
|
3785
4132
|
}
|
|
@@ -3803,7 +4150,7 @@ async function applyInstall(options = {}) {
|
|
|
3803
4150
|
};
|
|
3804
4151
|
const manifestFiles = [];
|
|
3805
4152
|
for (const entry of sourceFiles) {
|
|
3806
|
-
const srcPath =
|
|
4153
|
+
const srcPath = path23.join(packageRoot(), entry.srcRelPath);
|
|
3807
4154
|
const rawContent = await readText(srcPath);
|
|
3808
4155
|
if (rawContent === void 0) {
|
|
3809
4156
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -3823,7 +4170,7 @@ async function applyInstall(options = {}) {
|
|
|
3823
4170
|
}
|
|
3824
4171
|
continue;
|
|
3825
4172
|
}
|
|
3826
|
-
const destExists =
|
|
4173
|
+
const destExists = fs17.pathExistsSync(entry.destPath);
|
|
3827
4174
|
if (destExists) {
|
|
3828
4175
|
const currentContent = await readText(entry.destPath);
|
|
3829
4176
|
if (currentContent !== void 0) {
|
|
@@ -3859,24 +4206,25 @@ async function applyInstall(options = {}) {
|
|
|
3859
4206
|
schemaVersion: SCHEMA_VERSION2
|
|
3860
4207
|
});
|
|
3861
4208
|
}
|
|
3862
|
-
const fragmentPath =
|
|
4209
|
+
const fragmentPath = path23.join(srcDir, "settings-fragments", "hooks.json");
|
|
3863
4210
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
3864
4211
|
const settings = await readSettings();
|
|
3865
4212
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
3866
4213
|
const { settings: deniedSettings } = mergeDenyRules(hookSettings, buildDenyRules());
|
|
3867
|
-
const { settings:
|
|
4214
|
+
const { settings: allowedSettings } = mergeAllowRules(deniedSettings, buildAllowRules());
|
|
4215
|
+
const { settings: mergedSettings } = mergeAskRules(allowedSettings, buildAskRules());
|
|
3868
4216
|
result.hookIds = addedIds;
|
|
3869
4217
|
if (!check && existingManifest) {
|
|
3870
4218
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
3871
4219
|
for (const entry of existingManifest.files) {
|
|
3872
4220
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
3873
|
-
if (!
|
|
4221
|
+
if (!fs17.pathExistsSync(entry.destPath)) continue;
|
|
3874
4222
|
const content2 = await readText(entry.destPath);
|
|
3875
4223
|
if (!content2) continue;
|
|
3876
4224
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
3877
4225
|
const currentHash = hashContent(content2);
|
|
3878
4226
|
if (hasHeader && currentHash === entry.hash) {
|
|
3879
|
-
if (!dryRun) await
|
|
4227
|
+
if (!dryRun) await fs17.remove(entry.destPath);
|
|
3880
4228
|
result.deleted.push(entry.destPath);
|
|
3881
4229
|
} else {
|
|
3882
4230
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -4003,8 +4351,8 @@ async function runScan(options) {
|
|
|
4003
4351
|
}
|
|
4004
4352
|
|
|
4005
4353
|
// src/commands/undo.ts
|
|
4006
|
-
import
|
|
4007
|
-
import
|
|
4354
|
+
import path24 from "path";
|
|
4355
|
+
import fs18 from "fs-extra";
|
|
4008
4356
|
|
|
4009
4357
|
// src/claude/managed-paths.ts
|
|
4010
4358
|
var PROJECT_MANAGED_CLAUDE_REL = [
|
|
@@ -4030,61 +4378,61 @@ async function collectManagedPaths(root) {
|
|
|
4030
4378
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
4031
4379
|
for (const row of lock ?? []) {
|
|
4032
4380
|
for (const rel of row.paths ?? []) {
|
|
4033
|
-
paths.add(
|
|
4381
|
+
paths.add(path24.resolve(root, rel));
|
|
4034
4382
|
}
|
|
4035
4383
|
}
|
|
4036
4384
|
const existing = [];
|
|
4037
4385
|
for (const abs of paths) {
|
|
4038
|
-
if (await
|
|
4386
|
+
if (await fs18.pathExists(abs)) existing.push(abs);
|
|
4039
4387
|
}
|
|
4040
4388
|
return existing;
|
|
4041
4389
|
}
|
|
4042
4390
|
async function settingsHasHausContent(root) {
|
|
4043
4391
|
const settingsPath = claudePath(root, "settings.json");
|
|
4044
|
-
if (!await
|
|
4392
|
+
if (!await fs18.pathExists(settingsPath)) return false;
|
|
4045
4393
|
const settings = await readProjectSettings(root);
|
|
4046
4394
|
return settings._haus != null;
|
|
4047
4395
|
}
|
|
4048
4396
|
async function claudeMdHasHausBlock(root) {
|
|
4049
|
-
const filePath =
|
|
4050
|
-
if (!await
|
|
4051
|
-
const text = await
|
|
4397
|
+
const filePath = path24.join(root, "CLAUDE.md");
|
|
4398
|
+
if (!await fs18.pathExists(filePath)) return false;
|
|
4399
|
+
const text = await fs18.readFile(filePath, "utf8");
|
|
4052
4400
|
return text.includes(BLOCK_BEGIN);
|
|
4053
4401
|
}
|
|
4054
4402
|
async function stripProjectSettings(root) {
|
|
4055
4403
|
const settingsPath = claudePath(root, "settings.json");
|
|
4056
|
-
if (!await
|
|
4404
|
+
if (!await fs18.pathExists(settingsPath)) return false;
|
|
4057
4405
|
let settings = await readProjectSettings(root);
|
|
4058
|
-
settings = stripHausAllow(stripHausDeny(
|
|
4406
|
+
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4059
4407
|
const hasContent = Object.keys(settings).length > 0;
|
|
4060
4408
|
if (hasContent) {
|
|
4061
4409
|
await writeProjectSettings(root, settings);
|
|
4062
|
-
log(`Stripped haus rules from ${
|
|
4410
|
+
log(`Stripped haus rules from ${path24.relative(root, settingsPath)} (user settings preserved).`);
|
|
4063
4411
|
return true;
|
|
4064
4412
|
}
|
|
4065
|
-
await
|
|
4066
|
-
log(`Removed ${
|
|
4413
|
+
await fs18.remove(settingsPath);
|
|
4414
|
+
log(`Removed ${path24.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
4067
4415
|
return true;
|
|
4068
4416
|
}
|
|
4069
4417
|
async function stripRootClaudeMd(root) {
|
|
4070
|
-
const filePath =
|
|
4071
|
-
if (!await
|
|
4072
|
-
const prev = await
|
|
4418
|
+
const filePath = path24.join(root, "CLAUDE.md");
|
|
4419
|
+
if (!await fs18.pathExists(filePath)) return false;
|
|
4420
|
+
const prev = await fs18.readFile(filePath, "utf8");
|
|
4073
4421
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
4074
4422
|
const next = stripHausBlock(prev);
|
|
4075
4423
|
if (next.length === 0) {
|
|
4076
|
-
await
|
|
4424
|
+
await fs18.remove(filePath);
|
|
4077
4425
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
4078
4426
|
} else {
|
|
4079
|
-
await
|
|
4427
|
+
await fs18.writeFile(filePath, next, "utf8");
|
|
4080
4428
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
4081
4429
|
}
|
|
4082
4430
|
return true;
|
|
4083
4431
|
}
|
|
4084
4432
|
async function pruneDirIfEmpty(dir) {
|
|
4085
|
-
if (!await
|
|
4086
|
-
const entries = await
|
|
4087
|
-
if (entries.length === 0) await
|
|
4433
|
+
if (!await fs18.pathExists(dir)) return;
|
|
4434
|
+
const entries = await fs18.readdir(dir);
|
|
4435
|
+
if (entries.length === 0) await fs18.remove(dir);
|
|
4088
4436
|
}
|
|
4089
4437
|
async function runUndo(options) {
|
|
4090
4438
|
const root = process.cwd();
|
|
@@ -4095,7 +4443,7 @@ async function runUndo(options) {
|
|
|
4095
4443
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
4096
4444
|
return;
|
|
4097
4445
|
}
|
|
4098
|
-
const relTargets = managed.map((p) =>
|
|
4446
|
+
const relTargets = managed.map((p) => path24.relative(root, p));
|
|
4099
4447
|
const summaryParts = [...relTargets];
|
|
4100
4448
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
4101
4449
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -4111,9 +4459,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
4111
4459
|
}
|
|
4112
4460
|
}
|
|
4113
4461
|
for (const abs of managed) {
|
|
4114
|
-
if (!await
|
|
4115
|
-
await
|
|
4116
|
-
log(`Removed ${
|
|
4462
|
+
if (!await fs18.pathExists(abs)) continue;
|
|
4463
|
+
await fs18.remove(abs);
|
|
4464
|
+
log(`Removed ${path24.relative(root, abs)}`);
|
|
4117
4465
|
}
|
|
4118
4466
|
if (stripSettings) await stripProjectSettings(root);
|
|
4119
4467
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -4124,8 +4472,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
4124
4472
|
|
|
4125
4473
|
// src/install/uninstall.ts
|
|
4126
4474
|
import crypto3 from "crypto";
|
|
4127
|
-
import
|
|
4128
|
-
import
|
|
4475
|
+
import path25 from "path";
|
|
4476
|
+
import fs19 from "fs-extra";
|
|
4129
4477
|
async function runUninstall(options = {}) {
|
|
4130
4478
|
const { force = false } = options;
|
|
4131
4479
|
const manifest = await readManifest();
|
|
@@ -4135,7 +4483,7 @@ async function runUninstall(options = {}) {
|
|
|
4135
4483
|
return result;
|
|
4136
4484
|
}
|
|
4137
4485
|
for (const entry of manifest.files) {
|
|
4138
|
-
const exists =
|
|
4486
|
+
const exists = fs19.pathExistsSync(entry.destPath);
|
|
4139
4487
|
if (!exists) continue;
|
|
4140
4488
|
const content2 = await readText(entry.destPath);
|
|
4141
4489
|
if (content2 === void 0) continue;
|
|
@@ -4153,22 +4501,22 @@ async function runUninstall(options = {}) {
|
|
|
4153
4501
|
result.skipped.push(entry.destPath);
|
|
4154
4502
|
continue;
|
|
4155
4503
|
}
|
|
4156
|
-
await
|
|
4157
|
-
await pruneEmptyDir(
|
|
4504
|
+
await fs19.remove(entry.destPath);
|
|
4505
|
+
await pruneEmptyDir(path25.dirname(entry.destPath));
|
|
4158
4506
|
result.deleted.push(entry.destPath);
|
|
4159
4507
|
}
|
|
4160
4508
|
const settings = await readSettings();
|
|
4161
|
-
const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
|
|
4509
|
+
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4162
4510
|
await writeSettings(stripped);
|
|
4163
4511
|
result.hooksStripped = true;
|
|
4164
|
-
const hausDir =
|
|
4512
|
+
const hausDir = path25.join(globalClaudeDir(), "haus");
|
|
4165
4513
|
const manifestPath2 = hausManifestPath();
|
|
4166
|
-
if (
|
|
4167
|
-
await
|
|
4514
|
+
if (fs19.pathExistsSync(manifestPath2)) {
|
|
4515
|
+
await fs19.remove(manifestPath2);
|
|
4168
4516
|
}
|
|
4169
|
-
if (
|
|
4170
|
-
const remaining = await
|
|
4171
|
-
if (remaining.length === 0) await
|
|
4517
|
+
if (fs19.pathExistsSync(hausDir)) {
|
|
4518
|
+
const remaining = await fs19.readdir(hausDir);
|
|
4519
|
+
if (remaining.length === 0) await fs19.remove(hausDir);
|
|
4172
4520
|
}
|
|
4173
4521
|
return result;
|
|
4174
4522
|
}
|
|
@@ -4199,7 +4547,7 @@ async function runUninstallCommand(options) {
|
|
|
4199
4547
|
}
|
|
4200
4548
|
|
|
4201
4549
|
// src/commands/update.ts
|
|
4202
|
-
import
|
|
4550
|
+
import path27 from "path";
|
|
4203
4551
|
|
|
4204
4552
|
// src/update/diff-generated-files.ts
|
|
4205
4553
|
function diffGeneratedFiles() {
|
|
@@ -4226,7 +4574,7 @@ function summarizeLockDiff(before, after) {
|
|
|
4226
4574
|
|
|
4227
4575
|
// src/update/lockfile.ts
|
|
4228
4576
|
import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
|
|
4229
|
-
import
|
|
4577
|
+
import path26 from "path";
|
|
4230
4578
|
async function checkLock(root) {
|
|
4231
4579
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
4232
4580
|
const hasValidVersions = lock.every(
|
|
@@ -4257,7 +4605,7 @@ async function applyLock(root) {
|
|
|
4257
4605
|
try {
|
|
4258
4606
|
const backupDir = hausPath(root, "backups");
|
|
4259
4607
|
await mkdir(backupDir, { recursive: true });
|
|
4260
|
-
await copyFile(lockPath,
|
|
4608
|
+
await copyFile(lockPath, path26.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
4261
4609
|
} catch {
|
|
4262
4610
|
}
|
|
4263
4611
|
const enriched = await Promise.all(
|
|
@@ -4279,7 +4627,7 @@ function diffLock(before, after) {
|
|
|
4279
4627
|
}
|
|
4280
4628
|
async function hasLocalOverrides(root) {
|
|
4281
4629
|
try {
|
|
4282
|
-
await readFile3(
|
|
4630
|
+
await readFile3(path26.join(root, ".claude", "settings.json"), "utf8");
|
|
4283
4631
|
return true;
|
|
4284
4632
|
} catch {
|
|
4285
4633
|
return false;
|
|
@@ -4291,7 +4639,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
4291
4639
|
async function runUpdate(options) {
|
|
4292
4640
|
const root = process.cwd();
|
|
4293
4641
|
if (options.check) {
|
|
4294
|
-
const pkgJson2 = await readJson(
|
|
4642
|
+
const pkgJson2 = await readJson(path27.join(packageRoot(), "package.json"));
|
|
4295
4643
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
4296
4644
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
|
|
4297
4645
|
checkLock(root),
|
|
@@ -4321,7 +4669,7 @@ async function runUpdate(options) {
|
|
|
4321
4669
|
if (status.driftCount > 0) process.exitCode = 1;
|
|
4322
4670
|
return;
|
|
4323
4671
|
}
|
|
4324
|
-
const pkgJson = await readJson(
|
|
4672
|
+
const pkgJson = await readJson(path27.join(packageRoot(), "package.json"));
|
|
4325
4673
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4326
4674
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4327
4675
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4398,8 +4746,8 @@ async function detectGlobalInstallDrift() {
|
|
|
4398
4746
|
}
|
|
4399
4747
|
|
|
4400
4748
|
// src/commands/validate-catalog.ts
|
|
4401
|
-
import
|
|
4402
|
-
import
|
|
4749
|
+
import fs20 from "fs";
|
|
4750
|
+
import path28 from "path";
|
|
4403
4751
|
function auditForbiddenStacks(items) {
|
|
4404
4752
|
const failures = [];
|
|
4405
4753
|
for (const item of items) {
|
|
@@ -4476,40 +4824,40 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4476
4824
|
const failures = [];
|
|
4477
4825
|
for (const item of items) {
|
|
4478
4826
|
if (!item.path) continue;
|
|
4479
|
-
const absPath =
|
|
4827
|
+
const absPath = path28.join(manifestDir, item.path);
|
|
4480
4828
|
if (item.type === "skill") {
|
|
4481
|
-
const skillMd =
|
|
4482
|
-
if (!
|
|
4483
|
-
failures.push(`${item.id}: missing ${
|
|
4829
|
+
const skillMd = path28.join(absPath, "SKILL.md");
|
|
4830
|
+
if (!fs20.existsSync(skillMd)) {
|
|
4831
|
+
failures.push(`${item.id}: missing ${path28.relative(manifestDir, skillMd)}`);
|
|
4484
4832
|
continue;
|
|
4485
4833
|
}
|
|
4486
|
-
const text =
|
|
4834
|
+
const text = fs20.readFileSync(skillMd, "utf8");
|
|
4487
4835
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
4488
4836
|
failures.push(
|
|
4489
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
4837
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path28.relative(manifestDir, skillMd)}`)
|
|
4490
4838
|
);
|
|
4491
4839
|
} else if (item.type === "agent") {
|
|
4492
|
-
if (!
|
|
4840
|
+
if (!fs20.existsSync(absPath)) {
|
|
4493
4841
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4494
4842
|
continue;
|
|
4495
4843
|
}
|
|
4496
|
-
const text =
|
|
4497
|
-
const rel =
|
|
4844
|
+
const text = fs20.readFileSync(absPath, "utf8");
|
|
4845
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
4498
4846
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4499
4847
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
4500
4848
|
} else if (item.type === "template") {
|
|
4501
|
-
if (!
|
|
4849
|
+
if (!fs20.existsSync(absPath)) {
|
|
4502
4850
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
4503
4851
|
continue;
|
|
4504
4852
|
}
|
|
4505
4853
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4506
4854
|
} else if (item.type === "command") {
|
|
4507
|
-
if (!
|
|
4855
|
+
if (!fs20.existsSync(absPath)) {
|
|
4508
4856
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
4509
4857
|
continue;
|
|
4510
4858
|
}
|
|
4511
|
-
const text =
|
|
4512
|
-
const rel =
|
|
4859
|
+
const text = fs20.readFileSync(absPath, "utf8");
|
|
4860
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
4513
4861
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4514
4862
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4515
4863
|
}
|
|
@@ -4517,8 +4865,8 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4517
4865
|
return failures;
|
|
4518
4866
|
}
|
|
4519
4867
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4520
|
-
const rel =
|
|
4521
|
-
const text =
|
|
4868
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
4869
|
+
const text = fs20.readFileSync(absPath, "utf8");
|
|
4522
4870
|
const failures = [];
|
|
4523
4871
|
const lines = text.split(/\r?\n/);
|
|
4524
4872
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4540,11 +4888,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4540
4888
|
const failures = [];
|
|
4541
4889
|
const dirs = ["skills", "agents", "templates", "commands"];
|
|
4542
4890
|
for (const dir of dirs) {
|
|
4543
|
-
const abs =
|
|
4544
|
-
if (!
|
|
4891
|
+
const abs = path28.join(manifestDir, dir);
|
|
4892
|
+
if (!fs20.existsSync(abs)) continue;
|
|
4545
4893
|
walkMd(abs, (file) => {
|
|
4546
|
-
const text =
|
|
4547
|
-
const rel =
|
|
4894
|
+
const text = fs20.readFileSync(file, "utf8");
|
|
4895
|
+
const rel = path28.relative(manifestDir, file);
|
|
4548
4896
|
const lines = text.split(/\r?\n/);
|
|
4549
4897
|
for (let i = 0; i < lines.length; i++) {
|
|
4550
4898
|
const line2 = lines[i] ?? "";
|
|
@@ -4563,8 +4911,8 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4563
4911
|
return failures;
|
|
4564
4912
|
}
|
|
4565
4913
|
function walkMd(dir, fn) {
|
|
4566
|
-
for (const entry of
|
|
4567
|
-
const full =
|
|
4914
|
+
for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
|
|
4915
|
+
const full = path28.join(dir, entry.name);
|
|
4568
4916
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4569
4917
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4570
4918
|
}
|
|
@@ -4575,8 +4923,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4575
4923
|
process.exitCode = 1;
|
|
4576
4924
|
return;
|
|
4577
4925
|
}
|
|
4578
|
-
const abs =
|
|
4579
|
-
const manifestDir =
|
|
4926
|
+
const abs = path28.resolve(process.cwd(), manifestPath2);
|
|
4927
|
+
const manifestDir = path28.dirname(abs);
|
|
4580
4928
|
const data = await readJson(abs);
|
|
4581
4929
|
if (!data?.items) {
|
|
4582
4930
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -4606,7 +4954,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4606
4954
|
|
|
4607
4955
|
// src/commands/workspace.ts
|
|
4608
4956
|
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
4609
|
-
import
|
|
4957
|
+
import path35 from "path";
|
|
4610
4958
|
|
|
4611
4959
|
// src/commands/workspace/aggregate.ts
|
|
4612
4960
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -4660,7 +5008,7 @@ ${summaries.map(
|
|
|
4660
5008
|
}
|
|
4661
5009
|
|
|
4662
5010
|
// src/commands/workspace/config.ts
|
|
4663
|
-
import
|
|
5011
|
+
import path29 from "path";
|
|
4664
5012
|
import YAML from "yaml";
|
|
4665
5013
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4666
5014
|
function parseWorkspaceConfig(text) {
|
|
@@ -4683,12 +5031,12 @@ function parseWorkspaceConfig(text) {
|
|
|
4683
5031
|
};
|
|
4684
5032
|
}
|
|
4685
5033
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4686
|
-
return parseWorkspaceConfig(await readText(
|
|
5034
|
+
return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4687
5035
|
}
|
|
4688
5036
|
|
|
4689
5037
|
// src/commands/workspace/discover.ts
|
|
4690
|
-
import
|
|
4691
|
-
import
|
|
5038
|
+
import path30 from "path";
|
|
5039
|
+
import fg4 from "fast-glob";
|
|
4692
5040
|
import YAML2 from "yaml";
|
|
4693
5041
|
var DEFAULT_MAX_DEPTH = 3;
|
|
4694
5042
|
var REPO_MARKERS = ["**/.git", "**/package.json", "**/composer.json"];
|
|
@@ -4704,7 +5052,7 @@ function isDescendant(child, ancestor) {
|
|
|
4704
5052
|
return child === ancestor ? false : child.startsWith(`${ancestor}/`);
|
|
4705
5053
|
}
|
|
4706
5054
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
4707
|
-
const matches = await
|
|
5055
|
+
const matches = await fg4(REPO_MARKERS, {
|
|
4708
5056
|
cwd: workspaceRoot,
|
|
4709
5057
|
dot: true,
|
|
4710
5058
|
onlyFiles: false,
|
|
@@ -4715,8 +5063,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4715
5063
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
4716
5064
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
4717
5065
|
for (const match of matches) {
|
|
4718
|
-
const base =
|
|
4719
|
-
const dir =
|
|
5066
|
+
const base = path30.posix.basename(match);
|
|
5067
|
+
const dir = path30.posix.dirname(match);
|
|
4720
5068
|
const owner = dir === "." ? "." : dir;
|
|
4721
5069
|
if (base === ".git") gitDirs.add(owner);
|
|
4722
5070
|
else manifestDirs.add(owner);
|
|
@@ -4732,9 +5080,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4732
5080
|
}
|
|
4733
5081
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
4734
5082
|
return mapWithConcurrency(repoRoots, async (relDir) => {
|
|
4735
|
-
const absDir =
|
|
4736
|
-
const pkg = await readJson(
|
|
4737
|
-
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name :
|
|
5083
|
+
const absDir = path30.resolve(workspaceRoot, relDir);
|
|
5084
|
+
const pkg = await readJson(path30.join(absDir, "package.json"));
|
|
5085
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path30.basename(relDir === "." ? workspaceRoot : absDir);
|
|
4738
5086
|
let role = "auto";
|
|
4739
5087
|
try {
|
|
4740
5088
|
const scan = await scanProject(absDir, "fast");
|
|
@@ -4777,7 +5125,7 @@ function renderWorkspaceYaml(config2) {
|
|
|
4777
5125
|
});
|
|
4778
5126
|
}
|
|
4779
5127
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
4780
|
-
const yamlPath =
|
|
5128
|
+
const yamlPath = path30.join(workspaceRoot, "haus.workspace.yaml");
|
|
4781
5129
|
const existingText = await readText(yamlPath);
|
|
4782
5130
|
const existing = parseWorkspaceConfig(existingText);
|
|
4783
5131
|
if (existingText && !existing) {
|
|
@@ -4811,18 +5159,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
4811
5159
|
|
|
4812
5160
|
// src/commands/workspace/doctor.ts
|
|
4813
5161
|
import { existsSync as existsSync3 } from "fs";
|
|
4814
|
-
import
|
|
5162
|
+
import path32 from "path";
|
|
4815
5163
|
|
|
4816
5164
|
// src/commands/workspace/manifest.ts
|
|
4817
5165
|
import { readFileSync as readFileSync3 } from "fs";
|
|
4818
|
-
import
|
|
5166
|
+
import path31 from "path";
|
|
4819
5167
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
4820
5168
|
function manifestPath(workspaceRoot) {
|
|
4821
5169
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
4822
5170
|
}
|
|
4823
5171
|
function hausVersion() {
|
|
4824
5172
|
try {
|
|
4825
|
-
const pkg = JSON.parse(readFileSync3(
|
|
5173
|
+
const pkg = JSON.parse(readFileSync3(path31.join(packageRoot(), "package.json"), "utf8"));
|
|
4826
5174
|
return pkg.version ?? "0.0.0";
|
|
4827
5175
|
} catch {
|
|
4828
5176
|
return "0.0.0";
|
|
@@ -4888,7 +5236,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
4888
5236
|
}
|
|
4889
5237
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
4890
5238
|
for (const repo of config2.repos) {
|
|
4891
|
-
const repoRoot =
|
|
5239
|
+
const repoRoot = path32.resolve(workspaceRoot, repo.path);
|
|
4892
5240
|
const entry = manifestByName.get(repo.name);
|
|
4893
5241
|
if (!entry) {
|
|
4894
5242
|
flag({
|
|
@@ -4962,11 +5310,11 @@ function emit(args) {
|
|
|
4962
5310
|
|
|
4963
5311
|
// src/commands/workspace/setup.ts
|
|
4964
5312
|
import { existsSync as existsSync4, statSync } from "fs";
|
|
4965
|
-
import
|
|
5313
|
+
import path34 from "path";
|
|
4966
5314
|
|
|
4967
5315
|
// src/claude/write-workspace-claude-md.ts
|
|
4968
|
-
import
|
|
4969
|
-
import
|
|
5316
|
+
import path33 from "path";
|
|
5317
|
+
import fs21 from "fs-extra";
|
|
4970
5318
|
function buildWorkspaceImportBlock(client, members) {
|
|
4971
5319
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
4972
5320
|
const body = [
|
|
@@ -4984,8 +5332,8 @@ ${BLOCK_END}`;
|
|
|
4984
5332
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
4985
5333
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
4986
5334
|
const dryRun = opts.dryRun ?? false;
|
|
4987
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
4988
|
-
const prev = await
|
|
5335
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path33.join(workspaceRoot, "CLAUDE.md");
|
|
5336
|
+
const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
|
|
4989
5337
|
const next = opts.collision ? `${block}
|
|
4990
5338
|
` : injectHausBlock(prev, block);
|
|
4991
5339
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -5010,21 +5358,21 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
5010
5358
|
|
|
5011
5359
|
// src/commands/workspace/setup.ts
|
|
5012
5360
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
5013
|
-
let dir =
|
|
5361
|
+
let dir = path34.resolve(start);
|
|
5014
5362
|
for (; ; ) {
|
|
5015
|
-
if (existsSync4(
|
|
5016
|
-
const parent =
|
|
5017
|
-
if (parent === dir) return
|
|
5363
|
+
if (existsSync4(path34.join(dir, WORKSPACE_FILE))) return dir;
|
|
5364
|
+
const parent = path34.dirname(dir);
|
|
5365
|
+
if (parent === dir) return path34.resolve(start);
|
|
5018
5366
|
dir = parent;
|
|
5019
5367
|
}
|
|
5020
5368
|
}
|
|
5021
5369
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
5022
|
-
return
|
|
5370
|
+
return path34.resolve(workspaceRoot, repoPath) === path34.resolve(workspaceRoot);
|
|
5023
5371
|
}
|
|
5024
5372
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
5025
5373
|
const mode = options.mode ?? "fast";
|
|
5026
5374
|
const apply = options.write ?? false;
|
|
5027
|
-
const configText = await readText(
|
|
5375
|
+
const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
|
|
5028
5376
|
if (!configText) {
|
|
5029
5377
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5030
5378
|
process.exitCode = 1;
|
|
@@ -5041,7 +5389,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5041
5389
|
const statuses = [];
|
|
5042
5390
|
const aggregateInputs = [];
|
|
5043
5391
|
for (const repo of repos) {
|
|
5044
|
-
const repoRoot =
|
|
5392
|
+
const repoRoot = path34.resolve(workspaceRoot, repo.path);
|
|
5045
5393
|
log(`
|
|
5046
5394
|
\u2192 ${repo.name} (${repo.path})`);
|
|
5047
5395
|
try {
|
|
@@ -5110,7 +5458,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5110
5458
|
const status = statusByName.get(repo.name);
|
|
5111
5459
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
5112
5460
|
if (status?.status === "ok") {
|
|
5113
|
-
const lock = await checkLock(
|
|
5461
|
+
const lock = await checkLock(path34.resolve(workspaceRoot, repo.path));
|
|
5114
5462
|
manifestRepos.push({
|
|
5115
5463
|
name: repo.name,
|
|
5116
5464
|
path: repo.path,
|
|
@@ -5190,7 +5538,7 @@ relationships: []
|
|
|
5190
5538
|
log("Workspace initialized.");
|
|
5191
5539
|
}
|
|
5192
5540
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
5193
|
-
const configText = await readText(
|
|
5541
|
+
const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
|
|
5194
5542
|
if (!configText) {
|
|
5195
5543
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5196
5544
|
process.exitCode = 1;
|
|
@@ -5211,7 +5559,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
5211
5559
|
}
|
|
5212
5560
|
const inputs = [];
|
|
5213
5561
|
for (const repo of config2.repos) {
|
|
5214
|
-
const repoRoot =
|
|
5562
|
+
const repoRoot = path35.resolve(workspaceRoot, repo.path);
|
|
5215
5563
|
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
5216
5564
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
5217
5565
|
}
|
|
@@ -5262,7 +5610,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5262
5610
|
// src/cli.ts
|
|
5263
5611
|
function cliVersion() {
|
|
5264
5612
|
try {
|
|
5265
|
-
const pkgPath =
|
|
5613
|
+
const pkgPath = path36.join(packageRoot(), "package.json");
|
|
5266
5614
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5267
5615
|
return pkg.version ?? "0.0.0";
|
|
5268
5616
|
} catch {
|
|
@@ -5272,7 +5620,7 @@ function cliVersion() {
|
|
|
5272
5620
|
var program = new Command();
|
|
5273
5621
|
function validateRuntimeNodeVersion() {
|
|
5274
5622
|
try {
|
|
5275
|
-
const pkgPath =
|
|
5623
|
+
const pkgPath = path36.join(packageRoot(), "package.json");
|
|
5276
5624
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5277
5625
|
const requiredRange = pkg.engines?.node;
|
|
5278
5626
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|