@haus-tech/haus-workflow 0.22.1 → 0.23.1
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 +9 -0
- package/dist/cli.js +642 -288
- package/library/catalog/manifest.json +70 -310
- 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;
|
|
504
627
|
}
|
|
505
|
-
|
|
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
|
+
}
|
|
695
|
+
}
|
|
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,13 +1881,12 @@ 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"),
|
|
1571
1888
|
claudePath(root, "rules", "security.md"),
|
|
1572
|
-
claudePath(root, "commands", "haus-doctor.md")
|
|
1573
|
-
claudePath(root, "commands", "haus-review.md")
|
|
1889
|
+
claudePath(root, "commands", "haus-doctor.md")
|
|
1574
1890
|
];
|
|
1575
1891
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
1576
1892
|
const workflowPath = await writeWorkflow(root, hausVersion2, dryRun, opts.force);
|
|
@@ -1596,7 +1912,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1596
1912
|
await assertPostApplySettingsHausContract(root);
|
|
1597
1913
|
}
|
|
1598
1914
|
const configPath = hausPath(root, "config.json");
|
|
1599
|
-
if (!await
|
|
1915
|
+
if (!await fs12.pathExists(configPath)) {
|
|
1600
1916
|
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
1601
1917
|
}
|
|
1602
1918
|
await writeManagedText(
|
|
@@ -1605,12 +1921,20 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1605
1921
|
"Run `haus doctor`.",
|
|
1606
1922
|
dryRun
|
|
1607
1923
|
);
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
'Run `haus context --task "code review"` then review diff.'
|
|
1612
|
-
|
|
1613
|
-
|
|
1924
|
+
const legacyReviewPath = claudePath(root, "commands", "haus-review.md");
|
|
1925
|
+
if (await fs12.pathExists(legacyReviewPath)) {
|
|
1926
|
+
const content2 = await fs12.readFile(legacyReviewPath, "utf8");
|
|
1927
|
+
const stub = 'Run `haus context --task "code review"` then review diff.';
|
|
1928
|
+
if (content2 === stub || content2 === `${stub}
|
|
1929
|
+
` || content2 === `${stub}\r
|
|
1930
|
+
`) {
|
|
1931
|
+
if (dryRun) {
|
|
1932
|
+
log(`[dry-run] would remove stale ${displayPath(root, legacyReviewPath)}`);
|
|
1933
|
+
} else {
|
|
1934
|
+
await fs12.remove(legacyReviewPath);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1614
1938
|
await writeManagedText(
|
|
1615
1939
|
root,
|
|
1616
1940
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -1629,6 +1953,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1629
1953
|
const installedIds = /* @__PURE__ */ new Set();
|
|
1630
1954
|
const catalogItems = selectedIds !== void 0 ? rec.recommended.filter((r) => selectedIds.includes(r.id)) : rec.recommended;
|
|
1631
1955
|
let curatedReviewStatusSkips = 0;
|
|
1956
|
+
let superpowersSharedInstalled = false;
|
|
1632
1957
|
for (const item of catalogItems) {
|
|
1633
1958
|
const manifestItem = manifestById.get(item.id);
|
|
1634
1959
|
if (!manifestItem?.path) continue;
|
|
@@ -1655,20 +1980,34 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1655
1980
|
);
|
|
1656
1981
|
continue;
|
|
1657
1982
|
}
|
|
1658
|
-
const destination = claudePath(root, target,
|
|
1659
|
-
if (await
|
|
1983
|
+
const destination = claudePath(root, target, path13.basename(sourcePath));
|
|
1984
|
+
if (await fs12.pathExists(sourcePath)) {
|
|
1660
1985
|
if (dryRun) {
|
|
1661
|
-
const exists = await
|
|
1986
|
+
const exists = await fs12.pathExists(destination);
|
|
1662
1987
|
log(
|
|
1663
1988
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
1664
1989
|
);
|
|
1990
|
+
} else if (item.type === "skill") {
|
|
1991
|
+
await installCatalogSkill(sourcePath, destination, {
|
|
1992
|
+
originSourceId: manifestItem.originSourceId,
|
|
1993
|
+
dryRun: false
|
|
1994
|
+
});
|
|
1665
1995
|
} else {
|
|
1666
|
-
await
|
|
1667
|
-
await
|
|
1996
|
+
await fs12.ensureDir(path13.dirname(destination));
|
|
1997
|
+
await fs12.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
1668
1998
|
}
|
|
1669
1999
|
files.push(destination);
|
|
2000
|
+
const relPaths = [path13.relative(root, destination)];
|
|
2001
|
+
if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
|
|
2002
|
+
const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
|
|
2003
|
+
if (sharedRel) {
|
|
2004
|
+
superpowersSharedInstalled = true;
|
|
2005
|
+
relPaths.push(sharedRel);
|
|
2006
|
+
files.push(path13.join(root, sharedRel));
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
1670
2009
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
1671
|
-
installedPathsByItem.set(item.id, [...current,
|
|
2010
|
+
installedPathsByItem.set(item.id, [...current, ...relPaths]);
|
|
1672
2011
|
installedIds.add(item.id);
|
|
1673
2012
|
} else {
|
|
1674
2013
|
warn(
|
|
@@ -1738,7 +2077,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
1738
2077
|
if (relPaths.length === 0) continue;
|
|
1739
2078
|
const existing = [];
|
|
1740
2079
|
for (const rel of relPaths) {
|
|
1741
|
-
if (await
|
|
2080
|
+
if (await fs12.pathExists(path13.join(root, rel))) existing.push(rel);
|
|
1742
2081
|
}
|
|
1743
2082
|
if (existing.length === 0) continue;
|
|
1744
2083
|
if (entry.hash === void 0) {
|
|
@@ -1755,13 +2094,13 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
1755
2094
|
continue;
|
|
1756
2095
|
}
|
|
1757
2096
|
for (const rel of existing) {
|
|
1758
|
-
const abs =
|
|
2097
|
+
const abs = path13.join(root, rel);
|
|
1759
2098
|
if (dryRun) {
|
|
1760
2099
|
log(`[dry-run] would remove stale ${displayPath(root, abs)} (${entry.id})`);
|
|
1761
2100
|
continue;
|
|
1762
2101
|
}
|
|
1763
|
-
await
|
|
1764
|
-
await pruneEmptyDir(
|
|
2102
|
+
await fs12.remove(abs);
|
|
2103
|
+
await pruneEmptyDir(path13.dirname(abs));
|
|
1765
2104
|
log(`Removed stale ${displayPath(root, abs)} (${entry.id})`);
|
|
1766
2105
|
}
|
|
1767
2106
|
}
|
|
@@ -1769,7 +2108,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
|
|
|
1769
2108
|
|
|
1770
2109
|
// src/commands/apply.ts
|
|
1771
2110
|
async function cacheHasItems() {
|
|
1772
|
-
const data = await readJson(
|
|
2111
|
+
const data = await readJson(path14.join(getCacheDir(), "manifest.json"));
|
|
1773
2112
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
1774
2113
|
}
|
|
1775
2114
|
async function runApply(options) {
|
|
@@ -1835,8 +2174,8 @@ async function runApply(options) {
|
|
|
1835
2174
|
}
|
|
1836
2175
|
}
|
|
1837
2176
|
async function isHausProject(root) {
|
|
1838
|
-
if (await
|
|
1839
|
-
if (await
|
|
2177
|
+
if (await fs13.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
2178
|
+
if (await fs13.pathExists(claudePath(root, "settings.json"))) {
|
|
1840
2179
|
const settings = await readProjectSettings(root);
|
|
1841
2180
|
if (settings._haus != null) return true;
|
|
1842
2181
|
}
|
|
@@ -1874,7 +2213,7 @@ async function runCatalogAudit() {
|
|
|
1874
2213
|
|
|
1875
2214
|
// src/commands/clone.ts
|
|
1876
2215
|
import { existsSync as existsSync2 } from "fs";
|
|
1877
|
-
import
|
|
2216
|
+
import path15 from "path";
|
|
1878
2217
|
|
|
1879
2218
|
// src/utils/exec.ts
|
|
1880
2219
|
import { execa } from "execa";
|
|
@@ -1902,6 +2241,20 @@ async function runGit(args, options = {}) {
|
|
|
1902
2241
|
}
|
|
1903
2242
|
|
|
1904
2243
|
// src/commands/clone.ts
|
|
2244
|
+
var GIT_LOCATION_VARS = [
|
|
2245
|
+
"GIT_DIR",
|
|
2246
|
+
"GIT_WORK_TREE",
|
|
2247
|
+
"GIT_INDEX_FILE",
|
|
2248
|
+
"GIT_COMMON_DIR",
|
|
2249
|
+
"GIT_OBJECT_DIRECTORY",
|
|
2250
|
+
"GIT_NAMESPACE",
|
|
2251
|
+
"GIT_PREFIX"
|
|
2252
|
+
];
|
|
2253
|
+
function cloneEnv() {
|
|
2254
|
+
const env = { ...process.env };
|
|
2255
|
+
for (const key of GIT_LOCATION_VARS) delete env[key];
|
|
2256
|
+
return env;
|
|
2257
|
+
}
|
|
1905
2258
|
function repoNameFromUrl(url) {
|
|
1906
2259
|
const trimmed = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
1907
2260
|
const tail = trimmed.split(/[/:]/).pop() ?? "";
|
|
@@ -1913,16 +2266,16 @@ async function runClone(url, opts = {}) {
|
|
|
1913
2266
|
process.exitCode = 1;
|
|
1914
2267
|
return;
|
|
1915
2268
|
}
|
|
1916
|
-
const target =
|
|
2269
|
+
const target = path15.resolve(opts.dir?.trim() || repoNameFromUrl(url));
|
|
1917
2270
|
if (existsSync2(target)) {
|
|
1918
|
-
log(`\u2022 ${
|
|
2271
|
+
log(`\u2022 ${path15.basename(target)} already present at ${target} \u2014 skipped`);
|
|
1919
2272
|
return;
|
|
1920
2273
|
}
|
|
1921
2274
|
if (opts.dryRun) {
|
|
1922
2275
|
log(`would clone ${url} \u2192 ${target}`);
|
|
1923
2276
|
return;
|
|
1924
2277
|
}
|
|
1925
|
-
const res = await runGit(["clone", url, target]);
|
|
2278
|
+
const res = await runGit(["clone", url, target], { env: cloneEnv(), extendEnv: false });
|
|
1926
2279
|
if (res.exitCode !== 0) {
|
|
1927
2280
|
error(`clone failed for ${url}: ${(res.stderr || res.stdout).trim()}`);
|
|
1928
2281
|
process.exitCode = 1;
|
|
@@ -1932,7 +2285,7 @@ async function runClone(url, opts = {}) {
|
|
|
1932
2285
|
}
|
|
1933
2286
|
|
|
1934
2287
|
// src/commands/config.ts
|
|
1935
|
-
import
|
|
2288
|
+
import path16 from "path";
|
|
1936
2289
|
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
1937
2290
|
var HOOK_ALIASES = {
|
|
1938
2291
|
"hook.context": "context"
|
|
@@ -1945,7 +2298,7 @@ async function runConfig(key, action) {
|
|
|
1945
2298
|
);
|
|
1946
2299
|
}
|
|
1947
2300
|
const root = process.cwd();
|
|
1948
|
-
const configPath =
|
|
2301
|
+
const configPath = path16.join(root, CONFIG_PATH2);
|
|
1949
2302
|
const existing = await readJson(configPath);
|
|
1950
2303
|
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
1951
2304
|
cfg.hooks ??= {};
|
|
@@ -2325,7 +2678,7 @@ function selectRules(recommended, task, taskIntents) {
|
|
|
2325
2678
|
|
|
2326
2679
|
// src/scanner/scan-project.ts
|
|
2327
2680
|
import { readFile as readFile2 } from "fs/promises";
|
|
2328
|
-
import
|
|
2681
|
+
import path20 from "path";
|
|
2329
2682
|
|
|
2330
2683
|
// src/utils/audit-checks.ts
|
|
2331
2684
|
function isRecord(v) {
|
|
@@ -2352,8 +2705,8 @@ function compareVersions(a, b) {
|
|
|
2352
2705
|
}
|
|
2353
2706
|
|
|
2354
2707
|
// src/scanner/detect-package-manager.ts
|
|
2355
|
-
import
|
|
2356
|
-
import
|
|
2708
|
+
import path17 from "path";
|
|
2709
|
+
import fs14 from "fs-extra";
|
|
2357
2710
|
function detectPackageManager(root, packageManagerField) {
|
|
2358
2711
|
const field = String(packageManagerField ?? "").trim();
|
|
2359
2712
|
if (field.startsWith("yarn@")) {
|
|
@@ -2371,9 +2724,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2371
2724
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2372
2725
|
return "unknown";
|
|
2373
2726
|
}
|
|
2374
|
-
if (
|
|
2375
|
-
if (
|
|
2376
|
-
if (
|
|
2727
|
+
if (fs14.existsSync(path17.join(root, "yarn.lock"))) return "yarn";
|
|
2728
|
+
if (fs14.existsSync(path17.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2729
|
+
if (fs14.existsSync(path17.join(root, "package-lock.json"))) return "npm";
|
|
2377
2730
|
return "unknown";
|
|
2378
2731
|
}
|
|
2379
2732
|
|
|
@@ -2570,7 +2923,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
2570
2923
|
}
|
|
2571
2924
|
|
|
2572
2925
|
// src/scanner/detection.ts
|
|
2573
|
-
import
|
|
2926
|
+
import path18 from "path";
|
|
2574
2927
|
var UNSUPPORTED_MARKERS = {
|
|
2575
2928
|
"requirements.txt": "python",
|
|
2576
2929
|
"pyproject.toml": "python",
|
|
@@ -2624,14 +2977,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
2624
2977
|
function collectUnsupportedSignals(files) {
|
|
2625
2978
|
return [
|
|
2626
2979
|
...new Set(
|
|
2627
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
2980
|
+
files.map((f) => UNSUPPORTED_MARKERS[path18.basename(f)]).filter((s) => Boolean(s))
|
|
2628
2981
|
)
|
|
2629
2982
|
].sort();
|
|
2630
2983
|
}
|
|
2631
2984
|
|
|
2632
2985
|
// src/scanner/render.ts
|
|
2633
2986
|
import { readFile } from "fs/promises";
|
|
2634
|
-
import
|
|
2987
|
+
import path19 from "path";
|
|
2635
2988
|
|
|
2636
2989
|
// src/scanner/role-labels.ts
|
|
2637
2990
|
var ROLE_LABELS = {
|
|
@@ -2693,7 +3046,7 @@ async function buildContentBlob(root, files) {
|
|
|
2693
3046
|
const slice = candidates.slice(0, 300);
|
|
2694
3047
|
const parts = await mapWithConcurrency(slice, async (rel) => {
|
|
2695
3048
|
try {
|
|
2696
|
-
return await readFile(
|
|
3049
|
+
return await readFile(path19.join(root, rel), "utf8");
|
|
2697
3050
|
} catch {
|
|
2698
3051
|
return "";
|
|
2699
3052
|
}
|
|
@@ -2791,8 +3144,8 @@ var SAFE_FILES = [
|
|
|
2791
3144
|
"Gemfile"
|
|
2792
3145
|
];
|
|
2793
3146
|
async function scanProject(root, mode = "fast") {
|
|
2794
|
-
const pkg = await readJson(
|
|
2795
|
-
const composer = await readJson(
|
|
3147
|
+
const pkg = await readJson(path20.join(root, "package.json"));
|
|
3148
|
+
const composer = await readJson(path20.join(root, "composer.json"));
|
|
2796
3149
|
const files = await listFiles(root, SAFE_FILES);
|
|
2797
3150
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
2798
3151
|
const deps = dependencySet(pkg, composer);
|
|
@@ -2826,7 +3179,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2826
3179
|
mode,
|
|
2827
3180
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2828
3181
|
root,
|
|
2829
|
-
repoName: String(pkg?.name ??
|
|
3182
|
+
repoName: String(pkg?.name ?? path20.basename(root)),
|
|
2830
3183
|
packageManager,
|
|
2831
3184
|
repoRoles: roles,
|
|
2832
3185
|
detectedStacks: stacks,
|
|
@@ -2844,7 +3197,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2844
3197
|
const scanHashes = Object.fromEntries(
|
|
2845
3198
|
await mapWithConcurrency(
|
|
2846
3199
|
safeFiles,
|
|
2847
|
-
async (f) => [f, hashText(await readFile2(
|
|
3200
|
+
async (f) => [f, hashText(await readFile2(path20.join(root, f), "utf8"))]
|
|
2848
3201
|
)
|
|
2849
3202
|
);
|
|
2850
3203
|
const repoSummary = renderSummary(context);
|
|
@@ -2929,8 +3282,8 @@ async function runContext(options) {
|
|
|
2929
3282
|
}
|
|
2930
3283
|
|
|
2931
3284
|
// src/commands/doctor.ts
|
|
2932
|
-
import
|
|
2933
|
-
import
|
|
3285
|
+
import path21 from "path";
|
|
3286
|
+
import fs15 from "fs-extra";
|
|
2934
3287
|
|
|
2935
3288
|
// src/update/npm-version.ts
|
|
2936
3289
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
@@ -3010,7 +3363,7 @@ async function runDoctor(options) {
|
|
|
3010
3363
|
const enabled = await isHookEnabled(root, key);
|
|
3011
3364
|
ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
3012
3365
|
}
|
|
3013
|
-
const rootClaudeMdPath =
|
|
3366
|
+
const rootClaudeMdPath = path21.join(root, "CLAUDE.md");
|
|
3014
3367
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
3015
3368
|
if (!rootClaudeMdContent) {
|
|
3016
3369
|
flag(
|
|
@@ -3038,7 +3391,7 @@ async function runDoctor(options) {
|
|
|
3038
3391
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
3039
3392
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
3040
3393
|
for (const target of importTargets) {
|
|
3041
|
-
if (!await
|
|
3394
|
+
if (!await fs15.pathExists(hausPath(root, target))) {
|
|
3042
3395
|
flag(
|
|
3043
3396
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
3044
3397
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -3049,7 +3402,7 @@ async function runDoctor(options) {
|
|
|
3049
3402
|
}
|
|
3050
3403
|
}
|
|
3051
3404
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
3052
|
-
const workflowExists = await
|
|
3405
|
+
const workflowExists = await fs15.pathExists(workflowPath);
|
|
3053
3406
|
if (!workflowExists) {
|
|
3054
3407
|
flag(
|
|
3055
3408
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -3072,15 +3425,15 @@ async function runDoctor(options) {
|
|
|
3072
3425
|
"haus apply --write --force"
|
|
3073
3426
|
);
|
|
3074
3427
|
} else {
|
|
3075
|
-
const cachePath =
|
|
3076
|
-
const bundledPath =
|
|
3428
|
+
const cachePath = path21.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
3429
|
+
const bundledPath = path21.join(
|
|
3077
3430
|
packageRoot(),
|
|
3078
3431
|
"library",
|
|
3079
3432
|
"global",
|
|
3080
3433
|
"templates",
|
|
3081
3434
|
"agentic-workflow-standard.md"
|
|
3082
3435
|
);
|
|
3083
|
-
const templatePath = await
|
|
3436
|
+
const templatePath = await fs15.pathExists(cachePath) ? cachePath : bundledPath;
|
|
3084
3437
|
const templateContent = await readText(templatePath);
|
|
3085
3438
|
if (storedHashMatch && templateContent) {
|
|
3086
3439
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
@@ -3100,7 +3453,7 @@ async function runDoctor(options) {
|
|
|
3100
3453
|
}
|
|
3101
3454
|
}
|
|
3102
3455
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
3103
|
-
const workflowConfigExists = await
|
|
3456
|
+
const workflowConfigExists = await fs15.pathExists(workflowConfigPath);
|
|
3104
3457
|
if (!workflowConfigExists) {
|
|
3105
3458
|
flag(
|
|
3106
3459
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -3108,7 +3461,7 @@ async function runDoctor(options) {
|
|
|
3108
3461
|
"haus apply --write"
|
|
3109
3462
|
);
|
|
3110
3463
|
} else {
|
|
3111
|
-
const cfg = await
|
|
3464
|
+
const cfg = await fs15.readFile(workflowConfigPath, "utf8");
|
|
3112
3465
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
3113
3466
|
if (unfilled > 0) {
|
|
3114
3467
|
flag(
|
|
@@ -3139,7 +3492,7 @@ async function runDoctor(options) {
|
|
|
3139
3492
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
3140
3493
|
}
|
|
3141
3494
|
}
|
|
3142
|
-
const pkgJson = await readJson(
|
|
3495
|
+
const pkgJson = await readJson(path21.join(packageRoot(), "package.json"));
|
|
3143
3496
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3144
3497
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3145
3498
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3225,15 +3578,16 @@ import { readFileSync as readFileSync2 } from "fs";
|
|
|
3225
3578
|
|
|
3226
3579
|
// src/security/guard-bash.ts
|
|
3227
3580
|
function guardBash(command) {
|
|
3228
|
-
const matched =
|
|
3581
|
+
const matched = DENY_COMMANDS.find((token) => command.includes(token));
|
|
3229
3582
|
if (matched) return `I didn't run that \u2014 it can permanently change or delete things: ${command}`;
|
|
3230
3583
|
return void 0;
|
|
3231
3584
|
}
|
|
3232
3585
|
|
|
3233
3586
|
// src/security/guard-file-access.ts
|
|
3234
3587
|
function guardFileAccess(candidate) {
|
|
3235
|
-
const matched =
|
|
3236
|
-
if (matched)
|
|
3588
|
+
const matched = DENY_PATHS.find((token) => candidate.includes(token.replace(/\*/g, "")));
|
|
3589
|
+
if (matched)
|
|
3590
|
+
return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
|
|
3237
3591
|
return void 0;
|
|
3238
3592
|
}
|
|
3239
3593
|
|
|
@@ -3282,8 +3636,8 @@ async function runGuard(kind, _options) {
|
|
|
3282
3636
|
}
|
|
3283
3637
|
|
|
3284
3638
|
// src/commands/init.ts
|
|
3285
|
-
import
|
|
3286
|
-
import
|
|
3639
|
+
import path22 from "path";
|
|
3640
|
+
import fs16 from "fs-extra";
|
|
3287
3641
|
|
|
3288
3642
|
// src/utils/prompts.ts
|
|
3289
3643
|
import { stdin as input, stdout as output } from "process";
|
|
@@ -3644,8 +3998,8 @@ async function runSetupProject(options) {
|
|
|
3644
3998
|
// src/commands/init.ts
|
|
3645
3999
|
async function runInit(options) {
|
|
3646
4000
|
const root = process.cwd();
|
|
3647
|
-
const hausDir =
|
|
3648
|
-
const alreadyInit = await
|
|
4001
|
+
const hausDir = path22.join(root, ".haus-workflow");
|
|
4002
|
+
const alreadyInit = await fs16.pathExists(hausDir);
|
|
3649
4003
|
if (alreadyInit) {
|
|
3650
4004
|
log("Haus AI already initialized in this project.");
|
|
3651
4005
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -3657,8 +4011,8 @@ async function runInit(options) {
|
|
|
3657
4011
|
|
|
3658
4012
|
// src/install/apply.ts
|
|
3659
4013
|
import crypto2 from "crypto";
|
|
3660
|
-
import
|
|
3661
|
-
import
|
|
4014
|
+
import path23 from "path";
|
|
4015
|
+
import fs17 from "fs-extra";
|
|
3662
4016
|
|
|
3663
4017
|
// src/install/header.ts
|
|
3664
4018
|
var MD_PREFIX = "<!-- HAUS-MANAGED";
|
|
@@ -3746,40 +4100,40 @@ function hashContent(content2) {
|
|
|
3746
4100
|
}
|
|
3747
4101
|
function sourceVersion() {
|
|
3748
4102
|
try {
|
|
3749
|
-
const pkgPath =
|
|
3750
|
-
const pkg = JSON.parse(
|
|
4103
|
+
const pkgPath = path23.join(packageRoot(), "package.json");
|
|
4104
|
+
const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
|
|
3751
4105
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
3752
4106
|
} catch {
|
|
3753
4107
|
return "haus@0.0.0";
|
|
3754
4108
|
}
|
|
3755
4109
|
}
|
|
3756
4110
|
function globalSrcDir() {
|
|
3757
|
-
return
|
|
4111
|
+
return path23.join(packageRoot(), "library", "global");
|
|
3758
4112
|
}
|
|
3759
4113
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
3760
4114
|
const entries = [];
|
|
3761
|
-
const skillsDir =
|
|
3762
|
-
if (
|
|
3763
|
-
for (const skillName of
|
|
3764
|
-
const skillFile =
|
|
3765
|
-
if (
|
|
4115
|
+
const skillsDir = path23.join(srcDir, "skills");
|
|
4116
|
+
if (fs17.pathExistsSync(skillsDir)) {
|
|
4117
|
+
for (const skillName of fs17.readdirSync(skillsDir)) {
|
|
4118
|
+
const skillFile = path23.join(skillsDir, skillName, "SKILL.md");
|
|
4119
|
+
if (fs17.pathExistsSync(skillFile)) {
|
|
3766
4120
|
entries.push({
|
|
3767
4121
|
stableId: `skill.${skillName}`,
|
|
3768
|
-
srcRelPath:
|
|
3769
|
-
destPath:
|
|
4122
|
+
srcRelPath: path23.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
4123
|
+
destPath: path23.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
3770
4124
|
});
|
|
3771
4125
|
}
|
|
3772
4126
|
}
|
|
3773
4127
|
}
|
|
3774
|
-
const commandsDir =
|
|
3775
|
-
if (
|
|
3776
|
-
for (const fileName of
|
|
4128
|
+
const commandsDir = path23.join(srcDir, "commands");
|
|
4129
|
+
if (fs17.pathExistsSync(commandsDir)) {
|
|
4130
|
+
for (const fileName of fs17.readdirSync(commandsDir)) {
|
|
3777
4131
|
if (!fileName.endsWith(".md")) continue;
|
|
3778
4132
|
const commandName = fileName.slice(0, -".md".length);
|
|
3779
4133
|
entries.push({
|
|
3780
4134
|
stableId: `command.${commandName}`,
|
|
3781
|
-
srcRelPath:
|
|
3782
|
-
destPath:
|
|
4135
|
+
srcRelPath: path23.join("library", "global", "commands", fileName),
|
|
4136
|
+
destPath: path23.join(claudeDir, "commands", fileName)
|
|
3783
4137
|
});
|
|
3784
4138
|
}
|
|
3785
4139
|
}
|
|
@@ -3803,7 +4157,7 @@ async function applyInstall(options = {}) {
|
|
|
3803
4157
|
};
|
|
3804
4158
|
const manifestFiles = [];
|
|
3805
4159
|
for (const entry of sourceFiles) {
|
|
3806
|
-
const srcPath =
|
|
4160
|
+
const srcPath = path23.join(packageRoot(), entry.srcRelPath);
|
|
3807
4161
|
const rawContent = await readText(srcPath);
|
|
3808
4162
|
if (rawContent === void 0) {
|
|
3809
4163
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -3823,7 +4177,7 @@ async function applyInstall(options = {}) {
|
|
|
3823
4177
|
}
|
|
3824
4178
|
continue;
|
|
3825
4179
|
}
|
|
3826
|
-
const destExists =
|
|
4180
|
+
const destExists = fs17.pathExistsSync(entry.destPath);
|
|
3827
4181
|
if (destExists) {
|
|
3828
4182
|
const currentContent = await readText(entry.destPath);
|
|
3829
4183
|
if (currentContent !== void 0) {
|
|
@@ -3859,24 +4213,25 @@ async function applyInstall(options = {}) {
|
|
|
3859
4213
|
schemaVersion: SCHEMA_VERSION2
|
|
3860
4214
|
});
|
|
3861
4215
|
}
|
|
3862
|
-
const fragmentPath =
|
|
4216
|
+
const fragmentPath = path23.join(srcDir, "settings-fragments", "hooks.json");
|
|
3863
4217
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
3864
4218
|
const settings = await readSettings();
|
|
3865
4219
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
3866
4220
|
const { settings: deniedSettings } = mergeDenyRules(hookSettings, buildDenyRules());
|
|
3867
|
-
const { settings:
|
|
4221
|
+
const { settings: allowedSettings } = mergeAllowRules(deniedSettings, buildAllowRules());
|
|
4222
|
+
const { settings: mergedSettings } = mergeAskRules(allowedSettings, buildAskRules());
|
|
3868
4223
|
result.hookIds = addedIds;
|
|
3869
4224
|
if (!check && existingManifest) {
|
|
3870
4225
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
3871
4226
|
for (const entry of existingManifest.files) {
|
|
3872
4227
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
3873
|
-
if (!
|
|
4228
|
+
if (!fs17.pathExistsSync(entry.destPath)) continue;
|
|
3874
4229
|
const content2 = await readText(entry.destPath);
|
|
3875
4230
|
if (!content2) continue;
|
|
3876
4231
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
3877
4232
|
const currentHash = hashContent(content2);
|
|
3878
4233
|
if (hasHeader && currentHash === entry.hash) {
|
|
3879
|
-
if (!dryRun) await
|
|
4234
|
+
if (!dryRun) await fs17.remove(entry.destPath);
|
|
3880
4235
|
result.deleted.push(entry.destPath);
|
|
3881
4236
|
} else {
|
|
3882
4237
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -4003,15 +4358,14 @@ async function runScan(options) {
|
|
|
4003
4358
|
}
|
|
4004
4359
|
|
|
4005
4360
|
// src/commands/undo.ts
|
|
4006
|
-
import
|
|
4007
|
-
import
|
|
4361
|
+
import path24 from "path";
|
|
4362
|
+
import fs18 from "fs-extra";
|
|
4008
4363
|
|
|
4009
4364
|
// src/claude/managed-paths.ts
|
|
4010
4365
|
var PROJECT_MANAGED_CLAUDE_REL = [
|
|
4011
4366
|
"rules/haus.md",
|
|
4012
4367
|
"rules/security.md",
|
|
4013
|
-
"commands/haus-doctor.md"
|
|
4014
|
-
"commands/haus-review.md"
|
|
4368
|
+
"commands/haus-doctor.md"
|
|
4015
4369
|
];
|
|
4016
4370
|
var PROJECT_MANAGED_HAUS_REL = [
|
|
4017
4371
|
"selected-context.json",
|
|
@@ -4030,61 +4384,61 @@ async function collectManagedPaths(root) {
|
|
|
4030
4384
|
const lock = await readJson(hausPath(root, "haus.lock.json"));
|
|
4031
4385
|
for (const row of lock ?? []) {
|
|
4032
4386
|
for (const rel of row.paths ?? []) {
|
|
4033
|
-
paths.add(
|
|
4387
|
+
paths.add(path24.resolve(root, rel));
|
|
4034
4388
|
}
|
|
4035
4389
|
}
|
|
4036
4390
|
const existing = [];
|
|
4037
4391
|
for (const abs of paths) {
|
|
4038
|
-
if (await
|
|
4392
|
+
if (await fs18.pathExists(abs)) existing.push(abs);
|
|
4039
4393
|
}
|
|
4040
4394
|
return existing;
|
|
4041
4395
|
}
|
|
4042
4396
|
async function settingsHasHausContent(root) {
|
|
4043
4397
|
const settingsPath = claudePath(root, "settings.json");
|
|
4044
|
-
if (!await
|
|
4398
|
+
if (!await fs18.pathExists(settingsPath)) return false;
|
|
4045
4399
|
const settings = await readProjectSettings(root);
|
|
4046
4400
|
return settings._haus != null;
|
|
4047
4401
|
}
|
|
4048
4402
|
async function claudeMdHasHausBlock(root) {
|
|
4049
|
-
const filePath =
|
|
4050
|
-
if (!await
|
|
4051
|
-
const text = await
|
|
4403
|
+
const filePath = path24.join(root, "CLAUDE.md");
|
|
4404
|
+
if (!await fs18.pathExists(filePath)) return false;
|
|
4405
|
+
const text = await fs18.readFile(filePath, "utf8");
|
|
4052
4406
|
return text.includes(BLOCK_BEGIN);
|
|
4053
4407
|
}
|
|
4054
4408
|
async function stripProjectSettings(root) {
|
|
4055
4409
|
const settingsPath = claudePath(root, "settings.json");
|
|
4056
|
-
if (!await
|
|
4410
|
+
if (!await fs18.pathExists(settingsPath)) return false;
|
|
4057
4411
|
let settings = await readProjectSettings(root);
|
|
4058
|
-
settings = stripHausAllow(stripHausDeny(
|
|
4412
|
+
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4059
4413
|
const hasContent = Object.keys(settings).length > 0;
|
|
4060
4414
|
if (hasContent) {
|
|
4061
4415
|
await writeProjectSettings(root, settings);
|
|
4062
|
-
log(`Stripped haus rules from ${
|
|
4416
|
+
log(`Stripped haus rules from ${path24.relative(root, settingsPath)} (user settings preserved).`);
|
|
4063
4417
|
return true;
|
|
4064
4418
|
}
|
|
4065
|
-
await
|
|
4066
|
-
log(`Removed ${
|
|
4419
|
+
await fs18.remove(settingsPath);
|
|
4420
|
+
log(`Removed ${path24.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
4067
4421
|
return true;
|
|
4068
4422
|
}
|
|
4069
4423
|
async function stripRootClaudeMd(root) {
|
|
4070
|
-
const filePath =
|
|
4071
|
-
if (!await
|
|
4072
|
-
const prev = await
|
|
4424
|
+
const filePath = path24.join(root, "CLAUDE.md");
|
|
4425
|
+
if (!await fs18.pathExists(filePath)) return false;
|
|
4426
|
+
const prev = await fs18.readFile(filePath, "utf8");
|
|
4073
4427
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
4074
4428
|
const next = stripHausBlock(prev);
|
|
4075
4429
|
if (next.length === 0) {
|
|
4076
|
-
await
|
|
4430
|
+
await fs18.remove(filePath);
|
|
4077
4431
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
4078
4432
|
} else {
|
|
4079
|
-
await
|
|
4433
|
+
await fs18.writeFile(filePath, next, "utf8");
|
|
4080
4434
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
4081
4435
|
}
|
|
4082
4436
|
return true;
|
|
4083
4437
|
}
|
|
4084
4438
|
async function pruneDirIfEmpty(dir) {
|
|
4085
|
-
if (!await
|
|
4086
|
-
const entries = await
|
|
4087
|
-
if (entries.length === 0) await
|
|
4439
|
+
if (!await fs18.pathExists(dir)) return;
|
|
4440
|
+
const entries = await fs18.readdir(dir);
|
|
4441
|
+
if (entries.length === 0) await fs18.remove(dir);
|
|
4088
4442
|
}
|
|
4089
4443
|
async function runUndo(options) {
|
|
4090
4444
|
const root = process.cwd();
|
|
@@ -4095,7 +4449,7 @@ async function runUndo(options) {
|
|
|
4095
4449
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
4096
4450
|
return;
|
|
4097
4451
|
}
|
|
4098
|
-
const relTargets = managed.map((p) =>
|
|
4452
|
+
const relTargets = managed.map((p) => path24.relative(root, p));
|
|
4099
4453
|
const summaryParts = [...relTargets];
|
|
4100
4454
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
4101
4455
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
@@ -4111,9 +4465,9 @@ User-owned .claude/ files will be preserved.`
|
|
|
4111
4465
|
}
|
|
4112
4466
|
}
|
|
4113
4467
|
for (const abs of managed) {
|
|
4114
|
-
if (!await
|
|
4115
|
-
await
|
|
4116
|
-
log(`Removed ${
|
|
4468
|
+
if (!await fs18.pathExists(abs)) continue;
|
|
4469
|
+
await fs18.remove(abs);
|
|
4470
|
+
log(`Removed ${path24.relative(root, abs)}`);
|
|
4117
4471
|
}
|
|
4118
4472
|
if (stripSettings) await stripProjectSettings(root);
|
|
4119
4473
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -4124,8 +4478,8 @@ User-owned .claude/ files will be preserved.`
|
|
|
4124
4478
|
|
|
4125
4479
|
// src/install/uninstall.ts
|
|
4126
4480
|
import crypto3 from "crypto";
|
|
4127
|
-
import
|
|
4128
|
-
import
|
|
4481
|
+
import path25 from "path";
|
|
4482
|
+
import fs19 from "fs-extra";
|
|
4129
4483
|
async function runUninstall(options = {}) {
|
|
4130
4484
|
const { force = false } = options;
|
|
4131
4485
|
const manifest = await readManifest();
|
|
@@ -4135,7 +4489,7 @@ async function runUninstall(options = {}) {
|
|
|
4135
4489
|
return result;
|
|
4136
4490
|
}
|
|
4137
4491
|
for (const entry of manifest.files) {
|
|
4138
|
-
const exists =
|
|
4492
|
+
const exists = fs19.pathExistsSync(entry.destPath);
|
|
4139
4493
|
if (!exists) continue;
|
|
4140
4494
|
const content2 = await readText(entry.destPath);
|
|
4141
4495
|
if (content2 === void 0) continue;
|
|
@@ -4153,22 +4507,22 @@ async function runUninstall(options = {}) {
|
|
|
4153
4507
|
result.skipped.push(entry.destPath);
|
|
4154
4508
|
continue;
|
|
4155
4509
|
}
|
|
4156
|
-
await
|
|
4157
|
-
await pruneEmptyDir(
|
|
4510
|
+
await fs19.remove(entry.destPath);
|
|
4511
|
+
await pruneEmptyDir(path25.dirname(entry.destPath));
|
|
4158
4512
|
result.deleted.push(entry.destPath);
|
|
4159
4513
|
}
|
|
4160
4514
|
const settings = await readSettings();
|
|
4161
|
-
const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
|
|
4515
|
+
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
4162
4516
|
await writeSettings(stripped);
|
|
4163
4517
|
result.hooksStripped = true;
|
|
4164
|
-
const hausDir =
|
|
4518
|
+
const hausDir = path25.join(globalClaudeDir(), "haus");
|
|
4165
4519
|
const manifestPath2 = hausManifestPath();
|
|
4166
|
-
if (
|
|
4167
|
-
await
|
|
4520
|
+
if (fs19.pathExistsSync(manifestPath2)) {
|
|
4521
|
+
await fs19.remove(manifestPath2);
|
|
4168
4522
|
}
|
|
4169
|
-
if (
|
|
4170
|
-
const remaining = await
|
|
4171
|
-
if (remaining.length === 0) await
|
|
4523
|
+
if (fs19.pathExistsSync(hausDir)) {
|
|
4524
|
+
const remaining = await fs19.readdir(hausDir);
|
|
4525
|
+
if (remaining.length === 0) await fs19.remove(hausDir);
|
|
4172
4526
|
}
|
|
4173
4527
|
return result;
|
|
4174
4528
|
}
|
|
@@ -4199,7 +4553,7 @@ async function runUninstallCommand(options) {
|
|
|
4199
4553
|
}
|
|
4200
4554
|
|
|
4201
4555
|
// src/commands/update.ts
|
|
4202
|
-
import
|
|
4556
|
+
import path27 from "path";
|
|
4203
4557
|
|
|
4204
4558
|
// src/update/diff-generated-files.ts
|
|
4205
4559
|
function diffGeneratedFiles() {
|
|
@@ -4226,7 +4580,7 @@ function summarizeLockDiff(before, after) {
|
|
|
4226
4580
|
|
|
4227
4581
|
// src/update/lockfile.ts
|
|
4228
4582
|
import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
|
|
4229
|
-
import
|
|
4583
|
+
import path26 from "path";
|
|
4230
4584
|
async function checkLock(root) {
|
|
4231
4585
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
4232
4586
|
const hasValidVersions = lock.every(
|
|
@@ -4257,7 +4611,7 @@ async function applyLock(root) {
|
|
|
4257
4611
|
try {
|
|
4258
4612
|
const backupDir = hausPath(root, "backups");
|
|
4259
4613
|
await mkdir(backupDir, { recursive: true });
|
|
4260
|
-
await copyFile(lockPath,
|
|
4614
|
+
await copyFile(lockPath, path26.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
4261
4615
|
} catch {
|
|
4262
4616
|
}
|
|
4263
4617
|
const enriched = await Promise.all(
|
|
@@ -4279,7 +4633,7 @@ function diffLock(before, after) {
|
|
|
4279
4633
|
}
|
|
4280
4634
|
async function hasLocalOverrides(root) {
|
|
4281
4635
|
try {
|
|
4282
|
-
await readFile3(
|
|
4636
|
+
await readFile3(path26.join(root, ".claude", "settings.json"), "utf8");
|
|
4283
4637
|
return true;
|
|
4284
4638
|
} catch {
|
|
4285
4639
|
return false;
|
|
@@ -4291,7 +4645,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
4291
4645
|
async function runUpdate(options) {
|
|
4292
4646
|
const root = process.cwd();
|
|
4293
4647
|
if (options.check) {
|
|
4294
|
-
const pkgJson2 = await readJson(
|
|
4648
|
+
const pkgJson2 = await readJson(path27.join(packageRoot(), "package.json"));
|
|
4295
4649
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
4296
4650
|
const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
|
|
4297
4651
|
checkLock(root),
|
|
@@ -4321,7 +4675,7 @@ async function runUpdate(options) {
|
|
|
4321
4675
|
if (status.driftCount > 0) process.exitCode = 1;
|
|
4322
4676
|
return;
|
|
4323
4677
|
}
|
|
4324
|
-
const pkgJson = await readJson(
|
|
4678
|
+
const pkgJson = await readJson(path27.join(packageRoot(), "package.json"));
|
|
4325
4679
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
4326
4680
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
4327
4681
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -4398,8 +4752,8 @@ async function detectGlobalInstallDrift() {
|
|
|
4398
4752
|
}
|
|
4399
4753
|
|
|
4400
4754
|
// src/commands/validate-catalog.ts
|
|
4401
|
-
import
|
|
4402
|
-
import
|
|
4755
|
+
import fs20 from "fs";
|
|
4756
|
+
import path28 from "path";
|
|
4403
4757
|
function auditForbiddenStacks(items) {
|
|
4404
4758
|
const failures = [];
|
|
4405
4759
|
for (const item of items) {
|
|
@@ -4476,40 +4830,40 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4476
4830
|
const failures = [];
|
|
4477
4831
|
for (const item of items) {
|
|
4478
4832
|
if (!item.path) continue;
|
|
4479
|
-
const absPath =
|
|
4833
|
+
const absPath = path28.join(manifestDir, item.path);
|
|
4480
4834
|
if (item.type === "skill") {
|
|
4481
|
-
const skillMd =
|
|
4482
|
-
if (!
|
|
4483
|
-
failures.push(`${item.id}: missing ${
|
|
4835
|
+
const skillMd = path28.join(absPath, "SKILL.md");
|
|
4836
|
+
if (!fs20.existsSync(skillMd)) {
|
|
4837
|
+
failures.push(`${item.id}: missing ${path28.relative(manifestDir, skillMd)}`);
|
|
4484
4838
|
continue;
|
|
4485
4839
|
}
|
|
4486
|
-
const text =
|
|
4840
|
+
const text = fs20.readFileSync(skillMd, "utf8");
|
|
4487
4841
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
4488
4842
|
failures.push(
|
|
4489
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
4843
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path28.relative(manifestDir, skillMd)}`)
|
|
4490
4844
|
);
|
|
4491
4845
|
} else if (item.type === "agent") {
|
|
4492
|
-
if (!
|
|
4846
|
+
if (!fs20.existsSync(absPath)) {
|
|
4493
4847
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4494
4848
|
continue;
|
|
4495
4849
|
}
|
|
4496
|
-
const text =
|
|
4497
|
-
const rel =
|
|
4850
|
+
const text = fs20.readFileSync(absPath, "utf8");
|
|
4851
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
4498
4852
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4499
4853
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
4500
4854
|
} else if (item.type === "template") {
|
|
4501
|
-
if (!
|
|
4855
|
+
if (!fs20.existsSync(absPath)) {
|
|
4502
4856
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
4503
4857
|
continue;
|
|
4504
4858
|
}
|
|
4505
4859
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4506
4860
|
} else if (item.type === "command") {
|
|
4507
|
-
if (!
|
|
4861
|
+
if (!fs20.existsSync(absPath)) {
|
|
4508
4862
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
4509
4863
|
continue;
|
|
4510
4864
|
}
|
|
4511
|
-
const text =
|
|
4512
|
-
const rel =
|
|
4865
|
+
const text = fs20.readFileSync(absPath, "utf8");
|
|
4866
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
4513
4867
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4514
4868
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
4515
4869
|
}
|
|
@@ -4517,8 +4871,8 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4517
4871
|
return failures;
|
|
4518
4872
|
}
|
|
4519
4873
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
4520
|
-
const rel =
|
|
4521
|
-
const text =
|
|
4874
|
+
const rel = path28.relative(manifestDir, absPath);
|
|
4875
|
+
const text = fs20.readFileSync(absPath, "utf8");
|
|
4522
4876
|
const failures = [];
|
|
4523
4877
|
const lines = text.split(/\r?\n/);
|
|
4524
4878
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4540,11 +4894,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4540
4894
|
const failures = [];
|
|
4541
4895
|
const dirs = ["skills", "agents", "templates", "commands"];
|
|
4542
4896
|
for (const dir of dirs) {
|
|
4543
|
-
const abs =
|
|
4544
|
-
if (!
|
|
4897
|
+
const abs = path28.join(manifestDir, dir);
|
|
4898
|
+
if (!fs20.existsSync(abs)) continue;
|
|
4545
4899
|
walkMd(abs, (file) => {
|
|
4546
|
-
const text =
|
|
4547
|
-
const rel =
|
|
4900
|
+
const text = fs20.readFileSync(file, "utf8");
|
|
4901
|
+
const rel = path28.relative(manifestDir, file);
|
|
4548
4902
|
const lines = text.split(/\r?\n/);
|
|
4549
4903
|
for (let i = 0; i < lines.length; i++) {
|
|
4550
4904
|
const line2 = lines[i] ?? "";
|
|
@@ -4563,8 +4917,8 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4563
4917
|
return failures;
|
|
4564
4918
|
}
|
|
4565
4919
|
function walkMd(dir, fn) {
|
|
4566
|
-
for (const entry of
|
|
4567
|
-
const full =
|
|
4920
|
+
for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
|
|
4921
|
+
const full = path28.join(dir, entry.name);
|
|
4568
4922
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4569
4923
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4570
4924
|
}
|
|
@@ -4575,8 +4929,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4575
4929
|
process.exitCode = 1;
|
|
4576
4930
|
return;
|
|
4577
4931
|
}
|
|
4578
|
-
const abs =
|
|
4579
|
-
const manifestDir =
|
|
4932
|
+
const abs = path28.resolve(process.cwd(), manifestPath2);
|
|
4933
|
+
const manifestDir = path28.dirname(abs);
|
|
4580
4934
|
const data = await readJson(abs);
|
|
4581
4935
|
if (!data?.items) {
|
|
4582
4936
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -4606,7 +4960,7 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
4606
4960
|
|
|
4607
4961
|
// src/commands/workspace.ts
|
|
4608
4962
|
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
4609
|
-
import
|
|
4963
|
+
import path35 from "path";
|
|
4610
4964
|
|
|
4611
4965
|
// src/commands/workspace/aggregate.ts
|
|
4612
4966
|
async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
|
|
@@ -4660,7 +5014,7 @@ ${summaries.map(
|
|
|
4660
5014
|
}
|
|
4661
5015
|
|
|
4662
5016
|
// src/commands/workspace/config.ts
|
|
4663
|
-
import
|
|
5017
|
+
import path29 from "path";
|
|
4664
5018
|
import YAML from "yaml";
|
|
4665
5019
|
var WORKSPACE_FILE = "haus.workspace.yaml";
|
|
4666
5020
|
function parseWorkspaceConfig(text) {
|
|
@@ -4683,12 +5037,12 @@ function parseWorkspaceConfig(text) {
|
|
|
4683
5037
|
};
|
|
4684
5038
|
}
|
|
4685
5039
|
async function readWorkspaceConfig(workspaceRoot) {
|
|
4686
|
-
return parseWorkspaceConfig(await readText(
|
|
5040
|
+
return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
|
|
4687
5041
|
}
|
|
4688
5042
|
|
|
4689
5043
|
// src/commands/workspace/discover.ts
|
|
4690
|
-
import
|
|
4691
|
-
import
|
|
5044
|
+
import path30 from "path";
|
|
5045
|
+
import fg4 from "fast-glob";
|
|
4692
5046
|
import YAML2 from "yaml";
|
|
4693
5047
|
var DEFAULT_MAX_DEPTH = 3;
|
|
4694
5048
|
var REPO_MARKERS = ["**/.git", "**/package.json", "**/composer.json"];
|
|
@@ -4704,7 +5058,7 @@ function isDescendant(child, ancestor) {
|
|
|
4704
5058
|
return child === ancestor ? false : child.startsWith(`${ancestor}/`);
|
|
4705
5059
|
}
|
|
4706
5060
|
async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
4707
|
-
const matches = await
|
|
5061
|
+
const matches = await fg4(REPO_MARKERS, {
|
|
4708
5062
|
cwd: workspaceRoot,
|
|
4709
5063
|
dot: true,
|
|
4710
5064
|
onlyFiles: false,
|
|
@@ -4715,8 +5069,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4715
5069
|
const gitDirs = /* @__PURE__ */ new Set();
|
|
4716
5070
|
const manifestDirs = /* @__PURE__ */ new Set();
|
|
4717
5071
|
for (const match of matches) {
|
|
4718
|
-
const base =
|
|
4719
|
-
const dir =
|
|
5072
|
+
const base = path30.posix.basename(match);
|
|
5073
|
+
const dir = path30.posix.dirname(match);
|
|
4720
5074
|
const owner = dir === "." ? "." : dir;
|
|
4721
5075
|
if (base === ".git") gitDirs.add(owner);
|
|
4722
5076
|
else manifestDirs.add(owner);
|
|
@@ -4732,9 +5086,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
|
|
|
4732
5086
|
}
|
|
4733
5087
|
repoRoots.sort((a, b) => a.localeCompare(b));
|
|
4734
5088
|
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 :
|
|
5089
|
+
const absDir = path30.resolve(workspaceRoot, relDir);
|
|
5090
|
+
const pkg = await readJson(path30.join(absDir, "package.json"));
|
|
5091
|
+
const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path30.basename(relDir === "." ? workspaceRoot : absDir);
|
|
4738
5092
|
let role = "auto";
|
|
4739
5093
|
try {
|
|
4740
5094
|
const scan = await scanProject(absDir, "fast");
|
|
@@ -4777,7 +5131,7 @@ function renderWorkspaceYaml(config2) {
|
|
|
4777
5131
|
});
|
|
4778
5132
|
}
|
|
4779
5133
|
async function runDiscover(workspaceRoot, opts = {}) {
|
|
4780
|
-
const yamlPath =
|
|
5134
|
+
const yamlPath = path30.join(workspaceRoot, "haus.workspace.yaml");
|
|
4781
5135
|
const existingText = await readText(yamlPath);
|
|
4782
5136
|
const existing = parseWorkspaceConfig(existingText);
|
|
4783
5137
|
if (existingText && !existing) {
|
|
@@ -4811,18 +5165,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
|
|
|
4811
5165
|
|
|
4812
5166
|
// src/commands/workspace/doctor.ts
|
|
4813
5167
|
import { existsSync as existsSync3 } from "fs";
|
|
4814
|
-
import
|
|
5168
|
+
import path32 from "path";
|
|
4815
5169
|
|
|
4816
5170
|
// src/commands/workspace/manifest.ts
|
|
4817
5171
|
import { readFileSync as readFileSync3 } from "fs";
|
|
4818
|
-
import
|
|
5172
|
+
import path31 from "path";
|
|
4819
5173
|
var MANIFEST_FILE = "workspace.manifest.json";
|
|
4820
5174
|
function manifestPath(workspaceRoot) {
|
|
4821
5175
|
return hausPath(workspaceRoot, MANIFEST_FILE);
|
|
4822
5176
|
}
|
|
4823
5177
|
function hausVersion() {
|
|
4824
5178
|
try {
|
|
4825
|
-
const pkg = JSON.parse(readFileSync3(
|
|
5179
|
+
const pkg = JSON.parse(readFileSync3(path31.join(packageRoot(), "package.json"), "utf8"));
|
|
4826
5180
|
return pkg.version ?? "0.0.0";
|
|
4827
5181
|
} catch {
|
|
4828
5182
|
return "0.0.0";
|
|
@@ -4888,7 +5242,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
4888
5242
|
}
|
|
4889
5243
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
4890
5244
|
for (const repo of config2.repos) {
|
|
4891
|
-
const repoRoot =
|
|
5245
|
+
const repoRoot = path32.resolve(workspaceRoot, repo.path);
|
|
4892
5246
|
const entry = manifestByName.get(repo.name);
|
|
4893
5247
|
if (!entry) {
|
|
4894
5248
|
flag({
|
|
@@ -4962,11 +5316,11 @@ function emit(args) {
|
|
|
4962
5316
|
|
|
4963
5317
|
// src/commands/workspace/setup.ts
|
|
4964
5318
|
import { existsSync as existsSync4, statSync } from "fs";
|
|
4965
|
-
import
|
|
5319
|
+
import path34 from "path";
|
|
4966
5320
|
|
|
4967
5321
|
// src/claude/write-workspace-claude-md.ts
|
|
4968
|
-
import
|
|
4969
|
-
import
|
|
5322
|
+
import path33 from "path";
|
|
5323
|
+
import fs21 from "fs-extra";
|
|
4970
5324
|
function buildWorkspaceImportBlock(client, members) {
|
|
4971
5325
|
const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
|
|
4972
5326
|
const body = [
|
|
@@ -4984,8 +5338,8 @@ ${BLOCK_END}`;
|
|
|
4984
5338
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
4985
5339
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
4986
5340
|
const dryRun = opts.dryRun ?? false;
|
|
4987
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
4988
|
-
const prev = await
|
|
5341
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path33.join(workspaceRoot, "CLAUDE.md");
|
|
5342
|
+
const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
|
|
4989
5343
|
const next = opts.collision ? `${block}
|
|
4990
5344
|
` : injectHausBlock(prev, block);
|
|
4991
5345
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -5010,21 +5364,21 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
5010
5364
|
|
|
5011
5365
|
// src/commands/workspace/setup.ts
|
|
5012
5366
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
5013
|
-
let dir =
|
|
5367
|
+
let dir = path34.resolve(start);
|
|
5014
5368
|
for (; ; ) {
|
|
5015
|
-
if (existsSync4(
|
|
5016
|
-
const parent =
|
|
5017
|
-
if (parent === dir) return
|
|
5369
|
+
if (existsSync4(path34.join(dir, WORKSPACE_FILE))) return dir;
|
|
5370
|
+
const parent = path34.dirname(dir);
|
|
5371
|
+
if (parent === dir) return path34.resolve(start);
|
|
5018
5372
|
dir = parent;
|
|
5019
5373
|
}
|
|
5020
5374
|
}
|
|
5021
5375
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
5022
|
-
return
|
|
5376
|
+
return path34.resolve(workspaceRoot, repoPath) === path34.resolve(workspaceRoot);
|
|
5023
5377
|
}
|
|
5024
5378
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
5025
5379
|
const mode = options.mode ?? "fast";
|
|
5026
5380
|
const apply = options.write ?? false;
|
|
5027
|
-
const configText = await readText(
|
|
5381
|
+
const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
|
|
5028
5382
|
if (!configText) {
|
|
5029
5383
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5030
5384
|
process.exitCode = 1;
|
|
@@ -5041,7 +5395,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5041
5395
|
const statuses = [];
|
|
5042
5396
|
const aggregateInputs = [];
|
|
5043
5397
|
for (const repo of repos) {
|
|
5044
|
-
const repoRoot =
|
|
5398
|
+
const repoRoot = path34.resolve(workspaceRoot, repo.path);
|
|
5045
5399
|
log(`
|
|
5046
5400
|
\u2192 ${repo.name} (${repo.path})`);
|
|
5047
5401
|
try {
|
|
@@ -5110,7 +5464,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
5110
5464
|
const status = statusByName.get(repo.name);
|
|
5111
5465
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
5112
5466
|
if (status?.status === "ok") {
|
|
5113
|
-
const lock = await checkLock(
|
|
5467
|
+
const lock = await checkLock(path34.resolve(workspaceRoot, repo.path));
|
|
5114
5468
|
manifestRepos.push({
|
|
5115
5469
|
name: repo.name,
|
|
5116
5470
|
path: repo.path,
|
|
@@ -5190,7 +5544,7 @@ relationships: []
|
|
|
5190
5544
|
log("Workspace initialized.");
|
|
5191
5545
|
}
|
|
5192
5546
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
5193
|
-
const configText = await readText(
|
|
5547
|
+
const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
|
|
5194
5548
|
if (!configText) {
|
|
5195
5549
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
5196
5550
|
process.exitCode = 1;
|
|
@@ -5211,7 +5565,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
5211
5565
|
}
|
|
5212
5566
|
const inputs = [];
|
|
5213
5567
|
for (const repo of config2.repos) {
|
|
5214
|
-
const repoRoot =
|
|
5568
|
+
const repoRoot = path35.resolve(workspaceRoot, repo.path);
|
|
5215
5569
|
if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
|
|
5216
5570
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
5217
5571
|
}
|
|
@@ -5262,7 +5616,7 @@ async function runWorkspace(action, options = {}) {
|
|
|
5262
5616
|
// src/cli.ts
|
|
5263
5617
|
function cliVersion() {
|
|
5264
5618
|
try {
|
|
5265
|
-
const pkgPath =
|
|
5619
|
+
const pkgPath = path36.join(packageRoot(), "package.json");
|
|
5266
5620
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5267
5621
|
return pkg.version ?? "0.0.0";
|
|
5268
5622
|
} catch {
|
|
@@ -5272,7 +5626,7 @@ function cliVersion() {
|
|
|
5272
5626
|
var program = new Command();
|
|
5273
5627
|
function validateRuntimeNodeVersion() {
|
|
5274
5628
|
try {
|
|
5275
|
-
const pkgPath =
|
|
5629
|
+
const pkgPath = path36.join(packageRoot(), "package.json");
|
|
5276
5630
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
5277
5631
|
const requiredRange = pkg.engines?.node;
|
|
5278
5632
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|