@haus-tech/haus-workflow 4.0.0 → 4.1.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 +19 -0
- package/README.md +36 -1
- package/dist/cli.js +1500 -882
- package/library/global/skills/haus-workflow/SKILL.md +5 -5
- package/library/global/skills/haus-workflow/references/new.md +116 -23
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -14,9 +14,122 @@ var __export = (target, all) => {
|
|
|
14
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// src/catalog/offline.ts
|
|
18
|
+
function setOfflineFlag(value) {
|
|
19
|
+
flagged = value;
|
|
20
|
+
}
|
|
21
|
+
function isOffline() {
|
|
22
|
+
return flagged || process.env["HAUS_OFFLINE"] === "1";
|
|
23
|
+
}
|
|
24
|
+
var flagged;
|
|
25
|
+
var init_offline = __esm({
|
|
26
|
+
"src/catalog/offline.ts"() {
|
|
27
|
+
"use strict";
|
|
28
|
+
flagged = false;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// src/utils/paths.ts
|
|
33
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
34
|
+
import os from "node:os";
|
|
35
|
+
import path from "node:path";
|
|
36
|
+
import { fileURLToPath } from "node:url";
|
|
37
|
+
function hausPath(root, ...parts) {
|
|
38
|
+
return path.join(root, HAUS_DIR, ...parts);
|
|
39
|
+
}
|
|
40
|
+
function claudePath(root, ...parts) {
|
|
41
|
+
return path.join(root, ".claude", ...parts);
|
|
42
|
+
}
|
|
43
|
+
function claudeConfigDir() {
|
|
44
|
+
const override = process.env.CLAUDE_CONFIG_DIR;
|
|
45
|
+
if (override && override.trim().length > 0) return override;
|
|
46
|
+
return path.join(os.homedir(), ".claude");
|
|
47
|
+
}
|
|
48
|
+
function defaultClaudeConfigDir() {
|
|
49
|
+
return path.join(os.homedir(), ".claude");
|
|
50
|
+
}
|
|
51
|
+
function claudeConfigDirIsRelocated() {
|
|
52
|
+
return path.resolve(claudeConfigDir()) !== path.resolve(defaultClaudeConfigDir());
|
|
53
|
+
}
|
|
54
|
+
function claudeConfigDirLabel() {
|
|
55
|
+
return claudeConfigDirIsRelocated() ? claudeConfigDir().replace(/\\/g, "/") : "~/.claude";
|
|
56
|
+
}
|
|
57
|
+
function displayPath(root, targetPath) {
|
|
58
|
+
const rel = path.relative(root, targetPath).replace(/\\/g, "/");
|
|
59
|
+
if (rel && !rel.startsWith("../") && rel !== "..") {
|
|
60
|
+
return rel.startsWith("./") ? rel : `./${rel}`;
|
|
61
|
+
}
|
|
62
|
+
if (claudeConfigDirIsRelocated()) {
|
|
63
|
+
const configDir = claudeConfigDir();
|
|
64
|
+
const configRel = path.relative(configDir, targetPath).replace(/\\/g, "/");
|
|
65
|
+
if (configRel === "") return "$CLAUDE_CONFIG_DIR";
|
|
66
|
+
if (!configRel.startsWith("../") && configRel !== "..") {
|
|
67
|
+
return `$CLAUDE_CONFIG_DIR/${configRel}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const home = os.homedir();
|
|
71
|
+
const normalized = targetPath.replace(/\\/g, "/");
|
|
72
|
+
if (home && home.trim().length > 0) {
|
|
73
|
+
const homeRel = path.relative(home, targetPath).replace(/\\/g, "/");
|
|
74
|
+
if (homeRel && !homeRel.startsWith("../") && homeRel !== "..") {
|
|
75
|
+
return `~/${homeRel}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return normalized;
|
|
79
|
+
}
|
|
80
|
+
function packageRoot() {
|
|
81
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
82
|
+
for (let i = 0; i < 12; i++) {
|
|
83
|
+
const pkgPath = path.join(dir, "package.json");
|
|
84
|
+
if (existsSync(pkgPath)) {
|
|
85
|
+
try {
|
|
86
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
87
|
+
if (pkg.name === "haus" || pkg.name === "@haus-tech/haus-workflow") return dir;
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const parent = path.dirname(dir);
|
|
92
|
+
if (parent === dir) break;
|
|
93
|
+
dir = parent;
|
|
94
|
+
}
|
|
95
|
+
const file = fileURLToPath(import.meta.url);
|
|
96
|
+
return path.resolve(path.dirname(file), "../..");
|
|
97
|
+
}
|
|
98
|
+
var HAUS_DIR;
|
|
99
|
+
var init_paths = __esm({
|
|
100
|
+
"src/utils/paths.ts"() {
|
|
101
|
+
"use strict";
|
|
102
|
+
HAUS_DIR = ".haus-workflow";
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// src/catalog/path-safety.ts
|
|
107
|
+
import path2 from "node:path";
|
|
108
|
+
function isSafeCatalogPath(itemPath) {
|
|
109
|
+
if (!itemPath || path2.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
110
|
+
const normalized = path2.normalize(itemPath);
|
|
111
|
+
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
112
|
+
}
|
|
113
|
+
function isSafeRelativeFilePath(rel) {
|
|
114
|
+
if (!rel || rel.startsWith("/") || rel.includes("\\") || rel.includes("//")) return false;
|
|
115
|
+
if (path2.isAbsolute(rel)) return false;
|
|
116
|
+
const normalized = path2.posix.normalize(rel.replace(/\\/g, "/"));
|
|
117
|
+
return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
|
|
118
|
+
}
|
|
119
|
+
function safeJoin(base, itemPath) {
|
|
120
|
+
if (!isSafeCatalogPath(itemPath)) return null;
|
|
121
|
+
const resolved = path2.resolve(base, itemPath);
|
|
122
|
+
return resolved.startsWith(base + path2.sep) || resolved === base ? resolved : null;
|
|
123
|
+
}
|
|
124
|
+
var init_path_safety = __esm({
|
|
125
|
+
"src/catalog/path-safety.ts"() {
|
|
126
|
+
"use strict";
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
17
130
|
// src/utils/fs.ts
|
|
18
131
|
import crypto from "node:crypto";
|
|
19
|
-
import
|
|
132
|
+
import path5 from "node:path";
|
|
20
133
|
import fg from "fast-glob";
|
|
21
134
|
import fs from "fs-extra";
|
|
22
135
|
async function readJsonDetailed(file) {
|
|
@@ -66,9 +179,9 @@ async function writeText(file, value) {
|
|
|
66
179
|
await atomicWriteText(file, value);
|
|
67
180
|
}
|
|
68
181
|
async function atomicWriteText(file, value) {
|
|
69
|
-
const dir =
|
|
182
|
+
const dir = path5.dirname(file);
|
|
70
183
|
await fs.ensureDir(dir);
|
|
71
|
-
const tempPath =
|
|
184
|
+
const tempPath = path5.join(
|
|
72
185
|
dir,
|
|
73
186
|
`.tmp-haus-write-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
74
187
|
);
|
|
@@ -86,7 +199,7 @@ async function findNestedRepoDirs(root) {
|
|
|
86
199
|
});
|
|
87
200
|
const owners = /* @__PURE__ */ new Set();
|
|
88
201
|
for (const match of matches) {
|
|
89
|
-
const owner =
|
|
202
|
+
const owner = path5.posix.dirname(match);
|
|
90
203
|
if (owner === "." || owner === "") continue;
|
|
91
204
|
owners.add(owner);
|
|
92
205
|
}
|
|
@@ -120,10 +233,10 @@ function hashText(value) {
|
|
|
120
233
|
return `sha256-${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
121
234
|
}
|
|
122
235
|
function resolveContainedPath(root, rel) {
|
|
123
|
-
const resolvedRoot =
|
|
124
|
-
const resolved =
|
|
236
|
+
const resolvedRoot = path5.resolve(root);
|
|
237
|
+
const resolved = path5.resolve(root, rel);
|
|
125
238
|
if (resolved === resolvedRoot) return null;
|
|
126
|
-
const prefix = resolvedRoot.endsWith(
|
|
239
|
+
const prefix = resolvedRoot.endsWith(path5.sep) ? resolvedRoot : resolvedRoot + path5.sep;
|
|
127
240
|
if (!resolved.startsWith(prefix)) return null;
|
|
128
241
|
return resolved;
|
|
129
242
|
}
|
|
@@ -171,80 +284,6 @@ var init_logger = __esm({
|
|
|
171
284
|
}
|
|
172
285
|
});
|
|
173
286
|
|
|
174
|
-
// src/utils/paths.ts
|
|
175
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
176
|
-
import os from "node:os";
|
|
177
|
-
import path2 from "node:path";
|
|
178
|
-
import { fileURLToPath } from "node:url";
|
|
179
|
-
function hausPath(root, ...parts) {
|
|
180
|
-
return path2.join(root, HAUS_DIR, ...parts);
|
|
181
|
-
}
|
|
182
|
-
function claudePath(root, ...parts) {
|
|
183
|
-
return path2.join(root, ".claude", ...parts);
|
|
184
|
-
}
|
|
185
|
-
function claudeConfigDir() {
|
|
186
|
-
const override = process.env.CLAUDE_CONFIG_DIR;
|
|
187
|
-
if (override && override.trim().length > 0) return override;
|
|
188
|
-
return path2.join(os.homedir(), ".claude");
|
|
189
|
-
}
|
|
190
|
-
function defaultClaudeConfigDir() {
|
|
191
|
-
return path2.join(os.homedir(), ".claude");
|
|
192
|
-
}
|
|
193
|
-
function claudeConfigDirIsRelocated() {
|
|
194
|
-
return path2.resolve(claudeConfigDir()) !== path2.resolve(defaultClaudeConfigDir());
|
|
195
|
-
}
|
|
196
|
-
function claudeConfigDirLabel() {
|
|
197
|
-
return claudeConfigDirIsRelocated() ? claudeConfigDir().replace(/\\/g, "/") : "~/.claude";
|
|
198
|
-
}
|
|
199
|
-
function displayPath(root, targetPath) {
|
|
200
|
-
const rel = path2.relative(root, targetPath).replace(/\\/g, "/");
|
|
201
|
-
if (rel && !rel.startsWith("../") && rel !== "..") {
|
|
202
|
-
return rel.startsWith("./") ? rel : `./${rel}`;
|
|
203
|
-
}
|
|
204
|
-
if (claudeConfigDirIsRelocated()) {
|
|
205
|
-
const configDir = claudeConfigDir();
|
|
206
|
-
const configRel = path2.relative(configDir, targetPath).replace(/\\/g, "/");
|
|
207
|
-
if (configRel === "") return "$CLAUDE_CONFIG_DIR";
|
|
208
|
-
if (!configRel.startsWith("../") && configRel !== "..") {
|
|
209
|
-
return `$CLAUDE_CONFIG_DIR/${configRel}`;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const home = os.homedir();
|
|
213
|
-
const normalized = targetPath.replace(/\\/g, "/");
|
|
214
|
-
if (home && home.trim().length > 0) {
|
|
215
|
-
const homeRel = path2.relative(home, targetPath).replace(/\\/g, "/");
|
|
216
|
-
if (homeRel && !homeRel.startsWith("../") && homeRel !== "..") {
|
|
217
|
-
return `~/${homeRel}`;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return normalized;
|
|
221
|
-
}
|
|
222
|
-
function packageRoot() {
|
|
223
|
-
let dir = path2.dirname(fileURLToPath(import.meta.url));
|
|
224
|
-
for (let i = 0; i < 12; i++) {
|
|
225
|
-
const pkgPath = path2.join(dir, "package.json");
|
|
226
|
-
if (existsSync(pkgPath)) {
|
|
227
|
-
try {
|
|
228
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
229
|
-
if (pkg.name === "haus" || pkg.name === "@haus-tech/haus-workflow") return dir;
|
|
230
|
-
} catch {
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
const parent = path2.dirname(dir);
|
|
234
|
-
if (parent === dir) break;
|
|
235
|
-
dir = parent;
|
|
236
|
-
}
|
|
237
|
-
const file = fileURLToPath(import.meta.url);
|
|
238
|
-
return path2.resolve(path2.dirname(file), "../..");
|
|
239
|
-
}
|
|
240
|
-
var HAUS_DIR;
|
|
241
|
-
var init_paths = __esm({
|
|
242
|
-
"src/utils/paths.ts"() {
|
|
243
|
-
"use strict";
|
|
244
|
-
HAUS_DIR = ".haus-workflow";
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
|
|
248
287
|
// src/catalog/manifest-format.ts
|
|
249
288
|
function checkManifestFormat(value, sourceLabel) {
|
|
250
289
|
if (value === void 0) return { ok: true, format: 1 };
|
|
@@ -297,30 +336,6 @@ var init_constants = __esm({
|
|
|
297
336
|
}
|
|
298
337
|
});
|
|
299
338
|
|
|
300
|
-
// src/catalog/path-safety.ts
|
|
301
|
-
import path3 from "node:path";
|
|
302
|
-
function isSafeCatalogPath(itemPath) {
|
|
303
|
-
if (!itemPath || path3.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
304
|
-
const normalized = path3.normalize(itemPath);
|
|
305
|
-
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
306
|
-
}
|
|
307
|
-
function isSafeRelativeFilePath(rel) {
|
|
308
|
-
if (!rel || rel.startsWith("/") || rel.includes("\\") || rel.includes("//")) return false;
|
|
309
|
-
if (path3.isAbsolute(rel)) return false;
|
|
310
|
-
const normalized = path3.posix.normalize(rel.replace(/\\/g, "/"));
|
|
311
|
-
return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
|
|
312
|
-
}
|
|
313
|
-
function safeJoin(base, itemPath) {
|
|
314
|
-
if (!isSafeCatalogPath(itemPath)) return null;
|
|
315
|
-
const resolved = path3.resolve(base, itemPath);
|
|
316
|
-
return resolved.startsWith(base + path3.sep) || resolved === base ? resolved : null;
|
|
317
|
-
}
|
|
318
|
-
var init_path_safety = __esm({
|
|
319
|
-
"src/catalog/path-safety.ts"() {
|
|
320
|
-
"use strict";
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
339
|
// src/utils/exec.ts
|
|
325
340
|
var exec_exports = {};
|
|
326
341
|
__export(exec_exports, {
|
|
@@ -499,16 +514,16 @@ var init_github_rate_limit = __esm({
|
|
|
499
514
|
});
|
|
500
515
|
|
|
501
516
|
// src/catalog/remote-catalog/http.ts
|
|
502
|
-
import
|
|
503
|
-
import
|
|
504
|
-
async function fetchGuarded(url,
|
|
517
|
+
import path6 from "node:path";
|
|
518
|
+
import fs3 from "fs-extra";
|
|
519
|
+
async function fetchGuarded(url, extract2, headers = {}) {
|
|
505
520
|
try {
|
|
506
521
|
const res = await fetch(url, { headers, signal: AbortSignal.timeout(1e4) });
|
|
507
522
|
if (!res.ok) {
|
|
508
523
|
warn(`Catalog fetch HTTP ${res.status}: ${url}`);
|
|
509
524
|
return null;
|
|
510
525
|
}
|
|
511
|
-
return await
|
|
526
|
+
return await extract2(res);
|
|
512
527
|
} catch (e) {
|
|
513
528
|
warn(`Catalog fetch failed (${e instanceof Error ? e.constructor.name : String(e)}): ${url}`);
|
|
514
529
|
return null;
|
|
@@ -521,14 +536,14 @@ async function fetchBytes(url, headers = {}) {
|
|
|
521
536
|
return fetchGuarded(url, async (res) => Buffer.from(await res.arrayBuffer()), headers);
|
|
522
537
|
}
|
|
523
538
|
async function writeTextIfChanged(dest, text) {
|
|
524
|
-
if (await
|
|
525
|
-
const local = await
|
|
539
|
+
if (await fs3.pathExists(dest)) {
|
|
540
|
+
const local = await fs3.readFile(dest, "utf8");
|
|
526
541
|
if (local === text) return "unchanged";
|
|
527
|
-
await
|
|
542
|
+
await fs3.writeFile(dest, text, "utf8");
|
|
528
543
|
return "updated";
|
|
529
544
|
}
|
|
530
|
-
await
|
|
531
|
-
await
|
|
545
|
+
await fs3.ensureDir(path6.dirname(dest));
|
|
546
|
+
await fs3.writeFile(dest, text, "utf8");
|
|
532
547
|
return "created";
|
|
533
548
|
}
|
|
534
549
|
var init_http = __esm({
|
|
@@ -557,7 +572,7 @@ var init_semver_tag = __esm({
|
|
|
557
572
|
});
|
|
558
573
|
|
|
559
574
|
// src/catalog/remote-catalog/project-pin.ts
|
|
560
|
-
import
|
|
575
|
+
import fs4 from "fs-extra";
|
|
561
576
|
function isSafeRef(ref) {
|
|
562
577
|
return SAFE_REF.test(ref) && !ref.split("/").includes("..");
|
|
563
578
|
}
|
|
@@ -568,7 +583,7 @@ function loadProjectCatalogPin(root) {
|
|
|
568
583
|
const file = hausPath(root, PROJECT_CONFIG_FILE);
|
|
569
584
|
let raw;
|
|
570
585
|
try {
|
|
571
|
-
raw =
|
|
586
|
+
raw = fs4.readFileSync(file, "utf8");
|
|
572
587
|
} catch (err) {
|
|
573
588
|
if (err.code === "ENOENT") return { kind: "none" };
|
|
574
589
|
return { kind: "invalid", reason: `could not be read (${message(err)})` };
|
|
@@ -638,18 +653,18 @@ var init_project_pin = __esm({
|
|
|
638
653
|
});
|
|
639
654
|
|
|
640
655
|
// src/catalog/remote-catalog/ref.ts
|
|
641
|
-
import
|
|
642
|
-
import
|
|
656
|
+
import path7 from "node:path";
|
|
657
|
+
import fs5 from "fs-extra";
|
|
643
658
|
function isTestMode() {
|
|
644
659
|
return process.env["HAUS_TEST_MODE"] === "1" || process.env["NODE_ENV"] === "test";
|
|
645
660
|
}
|
|
646
661
|
function getCacheDir() {
|
|
647
|
-
return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ??
|
|
662
|
+
return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ?? path7.join(claudeConfigDir(), CATALOG_CACHE_SUBDIR);
|
|
648
663
|
}
|
|
649
664
|
function getBundledCatalogRef() {
|
|
650
665
|
try {
|
|
651
|
-
const manifestPath2 =
|
|
652
|
-
const raw =
|
|
666
|
+
const manifestPath2 = path7.join(packageRoot(), "library/catalog/manifest.json");
|
|
667
|
+
const raw = fs5.readFileSync(manifestPath2, "utf8");
|
|
653
668
|
const data = JSON.parse(raw);
|
|
654
669
|
if (typeof data.version === "string" && data.version) {
|
|
655
670
|
return data.version.startsWith("v") ? data.version : `v${data.version}`;
|
|
@@ -794,8 +809,8 @@ var init_ref = __esm({
|
|
|
794
809
|
});
|
|
795
810
|
|
|
796
811
|
// src/catalog/remote-catalog/github-tree.ts
|
|
797
|
-
import
|
|
798
|
-
import
|
|
812
|
+
import path8 from "node:path";
|
|
813
|
+
import fs6 from "fs-extra";
|
|
799
814
|
function sanitizeRelativeFilePaths(files, label) {
|
|
800
815
|
const safe = [];
|
|
801
816
|
for (const rel of files) {
|
|
@@ -811,17 +826,17 @@ async function listFilesRecursive(dir, base = dir) {
|
|
|
811
826
|
const out = [];
|
|
812
827
|
let entries;
|
|
813
828
|
try {
|
|
814
|
-
if (!await
|
|
815
|
-
entries = await
|
|
829
|
+
if (!await fs6.pathExists(dir)) return out;
|
|
830
|
+
entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
816
831
|
} catch {
|
|
817
832
|
return out;
|
|
818
833
|
}
|
|
819
834
|
for (const entry of entries) {
|
|
820
|
-
const full =
|
|
835
|
+
const full = path8.join(dir, entry.name);
|
|
821
836
|
if (entry.isDirectory()) {
|
|
822
837
|
out.push(...await listFilesRecursive(full, base));
|
|
823
838
|
} else if (entry.isFile()) {
|
|
824
|
-
out.push(
|
|
839
|
+
out.push(path8.relative(base, full).replace(/\\/g, "/"));
|
|
825
840
|
}
|
|
826
841
|
}
|
|
827
842
|
return out.sort();
|
|
@@ -1558,8 +1573,8 @@ __export(sync_exports, {
|
|
|
1558
1573
|
getCacheManifestAge: () => getCacheManifestAge,
|
|
1559
1574
|
syncRemoteCatalog: () => syncRemoteCatalog
|
|
1560
1575
|
});
|
|
1561
|
-
import
|
|
1562
|
-
import
|
|
1576
|
+
import path10 from "node:path";
|
|
1577
|
+
import fs8 from "fs-extra";
|
|
1563
1578
|
function isMarkdownPath(rel) {
|
|
1564
1579
|
return rel.toLowerCase().endsWith(".md");
|
|
1565
1580
|
}
|
|
@@ -1597,7 +1612,7 @@ function validateMarkdownFiles(item, fetched) {
|
|
|
1597
1612
|
return true;
|
|
1598
1613
|
}
|
|
1599
1614
|
async function directoryMatchesFetched(destDir, fetched) {
|
|
1600
|
-
if (!await
|
|
1615
|
+
if (!await fs8.pathExists(destDir)) return false;
|
|
1601
1616
|
const existing = await listFilesRecursive(destDir);
|
|
1602
1617
|
const relSet = new Set(fetched.map((f) => f.rel));
|
|
1603
1618
|
if (existing.length !== fetched.length) return false;
|
|
@@ -1605,30 +1620,30 @@ async function directoryMatchesFetched(destDir, fetched) {
|
|
|
1605
1620
|
if (!relSet.has(rel)) return false;
|
|
1606
1621
|
}
|
|
1607
1622
|
for (const file of fetched) {
|
|
1608
|
-
const dest =
|
|
1609
|
-
if (!await
|
|
1623
|
+
const dest = path10.join(destDir, file.rel);
|
|
1624
|
+
if (!await fs8.pathExists(dest)) return false;
|
|
1610
1625
|
if (file.kind === "text") {
|
|
1611
|
-
const local = await
|
|
1626
|
+
const local = await fs8.readFile(dest, "utf8");
|
|
1612
1627
|
if (local !== file.body) return false;
|
|
1613
1628
|
} else {
|
|
1614
|
-
const local = await
|
|
1629
|
+
const local = await fs8.readFile(dest);
|
|
1615
1630
|
if (!local.equals(file.body)) return false;
|
|
1616
1631
|
}
|
|
1617
1632
|
}
|
|
1618
1633
|
return true;
|
|
1619
1634
|
}
|
|
1620
1635
|
async function writeFetchedDirectory(destDir, fetched) {
|
|
1621
|
-
if (await
|
|
1622
|
-
await
|
|
1636
|
+
if (await fs8.pathExists(destDir)) {
|
|
1637
|
+
await fs8.remove(destDir);
|
|
1623
1638
|
}
|
|
1624
|
-
await
|
|
1639
|
+
await fs8.ensureDir(destDir);
|
|
1625
1640
|
for (const file of fetched) {
|
|
1626
|
-
const dest =
|
|
1627
|
-
await
|
|
1641
|
+
const dest = path10.join(destDir, file.rel);
|
|
1642
|
+
await fs8.ensureDir(path10.dirname(dest));
|
|
1628
1643
|
if (file.kind === "text") {
|
|
1629
|
-
await
|
|
1644
|
+
await fs8.writeFile(dest, file.body, "utf8");
|
|
1630
1645
|
} else {
|
|
1631
|
-
await
|
|
1646
|
+
await fs8.writeFile(dest, file.body);
|
|
1632
1647
|
}
|
|
1633
1648
|
}
|
|
1634
1649
|
}
|
|
@@ -1670,7 +1685,7 @@ async function syncDirectoryFromPrefix(item, catalogPrefix, destDir, base, opts)
|
|
|
1670
1685
|
}
|
|
1671
1686
|
}
|
|
1672
1687
|
}
|
|
1673
|
-
const existed = await
|
|
1688
|
+
const existed = await fs8.pathExists(destDir);
|
|
1674
1689
|
if (await directoryMatchesFetched(destDir, fetched)) {
|
|
1675
1690
|
return "unchanged";
|
|
1676
1691
|
}
|
|
@@ -1792,6 +1807,10 @@ async function syncOneItem(item, base) {
|
|
|
1792
1807
|
}
|
|
1793
1808
|
}
|
|
1794
1809
|
async function syncRemoteCatalog(projectRoot) {
|
|
1810
|
+
if (isOffline()) {
|
|
1811
|
+
log("Offline \u2014 skipping catalog sync; using cached or bundled content.");
|
|
1812
|
+
return { newItems: [], refreshed: [], unchanged: 0, failed: [], skipped: true };
|
|
1813
|
+
}
|
|
1795
1814
|
_resetBlobPathCacheForNewSync();
|
|
1796
1815
|
clearGithubRateLimitHit();
|
|
1797
1816
|
const fetched = await fetchRemoteManifest(projectRoot);
|
|
@@ -1812,9 +1831,9 @@ async function syncRemoteCatalog(projectRoot) {
|
|
|
1812
1831
|
const cacheDir = getCacheDir();
|
|
1813
1832
|
const catalogRef = expectedCatalogRef(projectRoot);
|
|
1814
1833
|
try {
|
|
1815
|
-
await
|
|
1816
|
-
await
|
|
1817
|
-
|
|
1834
|
+
await fs8.ensureDir(cacheDir);
|
|
1835
|
+
await fs8.writeFile(
|
|
1836
|
+
path10.join(cacheDir, "manifest.json"),
|
|
1818
1837
|
// globalPlugins must round-trip into the cache: the cache is what `haus update`
|
|
1819
1838
|
// and `haus rescan` read pins from, so dropping it here would make every
|
|
1820
1839
|
// catalog pin bump invisible and pin resolution fall back to the bundled default.
|
|
@@ -1879,7 +1898,7 @@ async function syncRemoteCatalog(projectRoot) {
|
|
|
1879
1898
|
}
|
|
1880
1899
|
async function getCacheManifestAge() {
|
|
1881
1900
|
try {
|
|
1882
|
-
const stat = await
|
|
1901
|
+
const stat = await fs8.stat(path10.join(getCacheDir(), "manifest.json"));
|
|
1883
1902
|
return Date.now() - stat.mtimeMs;
|
|
1884
1903
|
} catch {
|
|
1885
1904
|
return null;
|
|
@@ -1893,6 +1912,7 @@ var init_sync = __esm({
|
|
|
1893
1912
|
init_logger();
|
|
1894
1913
|
init_constants();
|
|
1895
1914
|
init_ingest_catalog();
|
|
1915
|
+
init_offline();
|
|
1896
1916
|
init_path_safety();
|
|
1897
1917
|
init_github_rate_limit();
|
|
1898
1918
|
init_github_tree();
|
|
@@ -1912,20 +1932,204 @@ var init_sync = __esm({
|
|
|
1912
1932
|
});
|
|
1913
1933
|
|
|
1914
1934
|
// src/cli.ts
|
|
1935
|
+
init_offline();
|
|
1915
1936
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1916
|
-
import
|
|
1937
|
+
import path80 from "node:path";
|
|
1917
1938
|
import { Command } from "commander";
|
|
1918
1939
|
|
|
1919
1940
|
// src/commands/apply.ts
|
|
1920
|
-
import path30 from "node:path";
|
|
1921
1941
|
import checkbox from "@inquirer/checkbox";
|
|
1922
1942
|
|
|
1943
|
+
// src/catalog/former-ids.ts
|
|
1944
|
+
import path4 from "node:path";
|
|
1945
|
+
|
|
1946
|
+
// src/catalog/install-mapping.ts
|
|
1947
|
+
init_paths();
|
|
1948
|
+
init_path_safety();
|
|
1949
|
+
import path3 from "node:path";
|
|
1950
|
+
var FLATTEN_DIR_BY_TYPE = {
|
|
1951
|
+
skill: "skills",
|
|
1952
|
+
agent: "agents",
|
|
1953
|
+
template: "templates",
|
|
1954
|
+
command: "commands"
|
|
1955
|
+
};
|
|
1956
|
+
var MIRRORED_TYPES = /* @__PURE__ */ new Set(["rule", "asset"]);
|
|
1957
|
+
function upstreamRelative(itemPath) {
|
|
1958
|
+
const match = itemPath.replace(/\\/g, "/").match(/^sources\/[^/]+\/(.+)$/);
|
|
1959
|
+
return match ? match[1] : null;
|
|
1960
|
+
}
|
|
1961
|
+
function installTargetFor(item) {
|
|
1962
|
+
const type = item.type;
|
|
1963
|
+
if (type === "config") {
|
|
1964
|
+
return {
|
|
1965
|
+
ok: false,
|
|
1966
|
+
code: "scaffold-only",
|
|
1967
|
+
reason: 'type "config" is installed by `haus configs`, not into .claude/'
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
const flattenDir = FLATTEN_DIR_BY_TYPE[type];
|
|
1971
|
+
const mirrored = MIRRORED_TYPES.has(type);
|
|
1972
|
+
if (!flattenDir && !mirrored) {
|
|
1973
|
+
return {
|
|
1974
|
+
ok: false,
|
|
1975
|
+
code: "unknown-type",
|
|
1976
|
+
reason: `type "${type}" is unknown to this haus version \u2014 upgrade the CLI to use it`
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
const itemPath = typeof item.path === "string" ? item.path : "";
|
|
1980
|
+
if (!isSafeCatalogPath(itemPath)) {
|
|
1981
|
+
return {
|
|
1982
|
+
ok: false,
|
|
1983
|
+
code: "unsafe-path",
|
|
1984
|
+
reason: `path "${itemPath}" is not a safe relative catalog path`
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
if (flattenDir) {
|
|
1988
|
+
return {
|
|
1989
|
+
ok: true,
|
|
1990
|
+
layout: "flatten",
|
|
1991
|
+
relPath: `${flattenDir}/${path3.posix.basename(itemPath)}`
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
const rel = upstreamRelative(itemPath);
|
|
1995
|
+
if (rel === null) {
|
|
1996
|
+
return {
|
|
1997
|
+
ok: false,
|
|
1998
|
+
code: "not-mirrorable",
|
|
1999
|
+
reason: `type "${type}" installs by mirroring its upstream path, but "${itemPath}" is not under sources/<source>/`
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
if (!isSafeRelativeFilePath(rel)) {
|
|
2003
|
+
return {
|
|
2004
|
+
ok: false,
|
|
2005
|
+
code: "unsafe-path",
|
|
2006
|
+
reason: `path "${itemPath}" escapes its own sources/<source>/ prefix`
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
return { ok: true, layout: "mirror", relPath: rel };
|
|
2010
|
+
}
|
|
2011
|
+
function installDestinationFor(root, item) {
|
|
2012
|
+
const target = installTargetFor(item);
|
|
2013
|
+
if (!target.ok) return target;
|
|
2014
|
+
const destination = safeJoin(claudePath(root), target.relPath);
|
|
2015
|
+
if (destination === null) {
|
|
2016
|
+
return {
|
|
2017
|
+
ok: false,
|
|
2018
|
+
code: "unsafe-path",
|
|
2019
|
+
reason: `install path "${target.relPath}" escapes .claude/`
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
return { ok: true, layout: target.layout, relPath: target.relPath, destination };
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// src/catalog/former-ids.ts
|
|
2026
|
+
function normalizeFormerIds(value) {
|
|
2027
|
+
if (value === void 0 || value === null) return [];
|
|
2028
|
+
if (!Array.isArray(value) || value.length === 0) return [];
|
|
2029
|
+
if (!value.every((entry) => typeof entry === "string")) return [];
|
|
2030
|
+
return value.filter((entry) => entry.length > 0);
|
|
2031
|
+
}
|
|
2032
|
+
function assertFormerIdsShape(itemId, formerIds) {
|
|
2033
|
+
if (formerIds === void 0) return;
|
|
2034
|
+
if (Array.isArray(formerIds) && formerIds.length === 0) return;
|
|
2035
|
+
if (!Array.isArray(formerIds) || !formerIds.every((entry) => typeof entry === "string")) {
|
|
2036
|
+
throw new Error(`${itemId}: formerIds must be a string array`);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
function buildFormerIdMap(items) {
|
|
2040
|
+
const map = /* @__PURE__ */ new Map();
|
|
2041
|
+
const currentIds = new Set(items.map((item) => item.id));
|
|
2042
|
+
for (const item of items) {
|
|
2043
|
+
assertFormerIdsShape(item.id, item.formerIds);
|
|
2044
|
+
const formerIds = item.formerIds ?? [];
|
|
2045
|
+
if (!Array.isArray(formerIds) || formerIds.length === 0) continue;
|
|
2046
|
+
for (const formerId of formerIds) {
|
|
2047
|
+
if (currentIds.has(formerId) && formerId !== item.id) {
|
|
2048
|
+
throw new Error(
|
|
2049
|
+
`formerId ${formerId} on ${item.id} conflicts with another item's current id`
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
const existingOwner = map.get(formerId);
|
|
2053
|
+
if (existingOwner !== void 0) {
|
|
2054
|
+
throw new Error(
|
|
2055
|
+
`duplicate formerId ${formerId} claimed by both ${existingOwner} and ${item.id}`
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
map.set(formerId, item.id);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return map;
|
|
2062
|
+
}
|
|
2063
|
+
function findFormerIdMigrations(lockItems, manifestItems) {
|
|
2064
|
+
const formerIdMap = buildFormerIdMap(manifestItems);
|
|
2065
|
+
return lockItems.flatMap(({ id }) => {
|
|
2066
|
+
const newId = formerIdMap.get(id);
|
|
2067
|
+
return newId === void 0 ? [] : [{ oldId: id, newId }];
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
function buildFormerInstallNames(items) {
|
|
2071
|
+
const names = [];
|
|
2072
|
+
for (const item of items) {
|
|
2073
|
+
const formerIds = normalizeFormerIds(item.formerIds);
|
|
2074
|
+
if (formerIds.length === 0) continue;
|
|
2075
|
+
const target = installTargetFor({ type: item.type, path: item.path });
|
|
2076
|
+
if (!target.ok || target.layout !== "flatten") continue;
|
|
2077
|
+
const dir = path4.posix.dirname(target.relPath);
|
|
2078
|
+
const base = path4.posix.basename(target.relPath);
|
|
2079
|
+
const ext = path4.posix.extname(base);
|
|
2080
|
+
const currentName = ext.length > 0 ? base.slice(0, -ext.length) : base;
|
|
2081
|
+
if (currentName.length === 0 || !item.id.endsWith(currentName)) continue;
|
|
2082
|
+
const prefix = item.id.slice(0, item.id.length - currentName.length);
|
|
2083
|
+
for (const formerId of formerIds) {
|
|
2084
|
+
if (!formerId.startsWith(prefix)) continue;
|
|
2085
|
+
const candidate = formerId.slice(prefix.length);
|
|
2086
|
+
if (candidate.length === 0 || candidate === currentName) continue;
|
|
2087
|
+
names.push({ formerId, currentId: item.id, dir, ext, candidate });
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return names;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// src/catalog/installable.ts
|
|
2094
|
+
init_fs();
|
|
2095
|
+
init_paths();
|
|
2096
|
+
init_path_safety();
|
|
2097
|
+
import fs2 from "fs-extra";
|
|
2098
|
+
function installableRecommendedItems(recommended) {
|
|
2099
|
+
return recommended.filter((item) => item.install !== false);
|
|
2100
|
+
}
|
|
2101
|
+
async function countInstallableItems(ctx, ids) {
|
|
2102
|
+
const wanted = ids === void 0 ? ctx.items : ctx.items.filter((item) => ids.includes(item.id));
|
|
2103
|
+
const byId = new Map(ctx.items.map((item) => [item.id, item]));
|
|
2104
|
+
const missing = [];
|
|
2105
|
+
let installable = 0;
|
|
2106
|
+
for (const item of wanted) {
|
|
2107
|
+
const contentPath = typeof item.path === "string" ? safeJoin(ctx.contentRoot, item.path) : null;
|
|
2108
|
+
if (contentPath !== null && await fs2.pathExists(contentPath)) {
|
|
2109
|
+
installable += 1;
|
|
2110
|
+
} else {
|
|
2111
|
+
missing.push(item.id);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
if (ids !== void 0) {
|
|
2115
|
+
for (const id of ids) if (!byId.has(id)) missing.push(id);
|
|
2116
|
+
}
|
|
2117
|
+
return { installable, missing };
|
|
2118
|
+
}
|
|
2119
|
+
async function recountInstalledAgainstLock(root, wantedIds) {
|
|
2120
|
+
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
2121
|
+
const installedIds = new Set(lock.map((entry) => entry.id));
|
|
2122
|
+
const installedCount = wantedIds.filter((id) => installedIds.has(id)).length;
|
|
2123
|
+
const missingWantedIds = wantedIds.filter((id) => !installedIds.has(id));
|
|
2124
|
+
return { installedCount, missingWantedIds };
|
|
2125
|
+
}
|
|
2126
|
+
|
|
1923
2127
|
// src/catalog/load-catalog.ts
|
|
1924
2128
|
init_fs();
|
|
1925
2129
|
init_logger();
|
|
1926
2130
|
init_paths();
|
|
1927
2131
|
init_manifest_format();
|
|
1928
|
-
import
|
|
2132
|
+
import path11 from "node:path";
|
|
1929
2133
|
|
|
1930
2134
|
// src/catalog/remote-catalog.ts
|
|
1931
2135
|
init_github_tree();
|
|
@@ -1935,18 +2139,23 @@ init_project_pin();
|
|
|
1935
2139
|
init_manifest();
|
|
1936
2140
|
|
|
1937
2141
|
// src/catalog/remote-catalog/workflow-template.ts
|
|
2142
|
+
init_offline();
|
|
1938
2143
|
init_http();
|
|
1939
2144
|
init_ref();
|
|
1940
|
-
import
|
|
1941
|
-
import
|
|
2145
|
+
import path9 from "node:path";
|
|
2146
|
+
import fs7 from "fs-extra";
|
|
1942
2147
|
var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
|
|
1943
2148
|
async function readWorkflowTemplate(opts = {}) {
|
|
1944
|
-
const dest =
|
|
2149
|
+
const dest = path9.join(getCacheDir(), WORKFLOW_TEMPLATE_REL);
|
|
2150
|
+
if (isOffline()) {
|
|
2151
|
+
if (await fs7.pathExists(dest)) return fs7.readFile(dest, "utf8");
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
1945
2154
|
const base = await remoteBase(opts.projectRoot);
|
|
1946
2155
|
const { url, headers } = await catalogContentRequest(base, WORKFLOW_TEMPLATE_REL);
|
|
1947
2156
|
const text = await fetchText(url, headers);
|
|
1948
2157
|
if (text === null) {
|
|
1949
|
-
if (await
|
|
2158
|
+
if (await fs7.pathExists(dest)) return fs7.readFile(dest, "utf8");
|
|
1950
2159
|
return null;
|
|
1951
2160
|
}
|
|
1952
2161
|
if (!opts.dryRun) {
|
|
@@ -1960,6 +2169,9 @@ init_github_tree();
|
|
|
1960
2169
|
init_sync();
|
|
1961
2170
|
|
|
1962
2171
|
// src/catalog/load-catalog.ts
|
|
2172
|
+
function manifestVersion(raw) {
|
|
2173
|
+
return typeof raw === "string" && raw !== "" ? raw : null;
|
|
2174
|
+
}
|
|
1963
2175
|
function cacheRefMatches(cached, expected) {
|
|
1964
2176
|
if (expected === null) return true;
|
|
1965
2177
|
if (typeof cached !== "string" || cached === "") return true;
|
|
@@ -1971,14 +2183,15 @@ async function loadCatalogContext(root) {
|
|
|
1971
2183
|
const data2 = await readJson(envPath);
|
|
1972
2184
|
return {
|
|
1973
2185
|
items: data2?.items ?? [],
|
|
1974
|
-
contentRoot:
|
|
2186
|
+
contentRoot: path11.dirname(envPath),
|
|
1975
2187
|
source: "fixture",
|
|
1976
2188
|
manifestFormat: assertSupportedManifestFormat(data2?.manifestFormat, "The fixture catalog"),
|
|
2189
|
+
version: manifestVersion(data2?.version),
|
|
1977
2190
|
globalPlugins: data2?.globalPlugins
|
|
1978
2191
|
};
|
|
1979
2192
|
}
|
|
1980
2193
|
const cacheDir = getCacheDir();
|
|
1981
|
-
const cacheManifestPath =
|
|
2194
|
+
const cacheManifestPath = path11.join(cacheDir, "manifest.json");
|
|
1982
2195
|
const cacheData = await readJson(cacheManifestPath);
|
|
1983
2196
|
if (cacheData?.items?.length) {
|
|
1984
2197
|
const wantedRef = expectedCatalogRef(root);
|
|
@@ -1995,31 +2208,34 @@ async function loadCatalogContext(root) {
|
|
|
1995
2208
|
cacheData.manifestFormat,
|
|
1996
2209
|
"The cached catalog"
|
|
1997
2210
|
),
|
|
2211
|
+
version: manifestVersion(cacheData.version),
|
|
1998
2212
|
globalPlugins: cacheData.globalPlugins
|
|
1999
2213
|
};
|
|
2000
2214
|
}
|
|
2001
2215
|
}
|
|
2002
|
-
const localManifest =
|
|
2216
|
+
const localManifest = path11.join(root, "library/catalog/manifest.json");
|
|
2003
2217
|
const localData = await readJson(localManifest);
|
|
2004
2218
|
if (localData?.items?.length) {
|
|
2005
2219
|
return {
|
|
2006
2220
|
items: localData.items,
|
|
2007
|
-
contentRoot:
|
|
2221
|
+
contentRoot: path11.dirname(localManifest),
|
|
2008
2222
|
source: "local",
|
|
2009
2223
|
manifestFormat: assertSupportedManifestFormat(
|
|
2010
2224
|
localData.manifestFormat,
|
|
2011
2225
|
"The project-local catalog"
|
|
2012
2226
|
),
|
|
2227
|
+
version: manifestVersion(localData.version),
|
|
2013
2228
|
globalPlugins: localData.globalPlugins
|
|
2014
2229
|
};
|
|
2015
2230
|
}
|
|
2016
|
-
const packageManifest =
|
|
2231
|
+
const packageManifest = path11.join(packageRoot(), "library/catalog/manifest.json");
|
|
2017
2232
|
const data = await readJson(packageManifest);
|
|
2018
2233
|
return {
|
|
2019
2234
|
items: data?.items ?? [],
|
|
2020
|
-
contentRoot:
|
|
2235
|
+
contentRoot: path11.dirname(packageManifest),
|
|
2021
2236
|
source: "bundled",
|
|
2022
2237
|
manifestFormat: assertSupportedManifestFormat(data?.manifestFormat, "The bundled catalog"),
|
|
2238
|
+
version: manifestVersion(data?.version),
|
|
2023
2239
|
globalPlugins: data?.globalPlugins
|
|
2024
2240
|
};
|
|
2025
2241
|
}
|
|
@@ -2037,9 +2253,12 @@ async function loadCatalog(root) {
|
|
|
2037
2253
|
return ctx.items;
|
|
2038
2254
|
}
|
|
2039
2255
|
function catalogItemContentPath(contentRoot, item) {
|
|
2040
|
-
return
|
|
2256
|
+
return path11.join(contentRoot, item.path);
|
|
2041
2257
|
}
|
|
2042
2258
|
|
|
2259
|
+
// src/commands/apply.ts
|
|
2260
|
+
init_offline();
|
|
2261
|
+
|
|
2043
2262
|
// src/claude/gitignore-owned-check.ts
|
|
2044
2263
|
init_exec();
|
|
2045
2264
|
var HAUS_OWNED_TRACKED_PATHS = [
|
|
@@ -2061,171 +2280,19 @@ async function findGitignoredHausPaths(root) {
|
|
|
2061
2280
|
result.stdout.split("\n").map((line2) => line2.trim()).filter(Boolean)
|
|
2062
2281
|
)
|
|
2063
2282
|
];
|
|
2064
|
-
} catch {
|
|
2065
|
-
return [];
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
function summarizeGitignoredHausDirs(paths) {
|
|
2069
|
-
const dirs = /* @__PURE__ */ new Set();
|
|
2070
|
-
for (const p of paths) dirs.add(p === ".haus-workflow" ? ".haus-workflow/" : ".claude/");
|
|
2071
|
-
return [...dirs];
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
// src/claude/write-claude-files.ts
|
|
2075
|
-
import path25 from "node:path";
|
|
2076
|
-
import fs22 from "fs-extra";
|
|
2077
|
-
|
|
2078
|
-
// src/catalog/former-ids.ts
|
|
2079
|
-
import path11 from "node:path";
|
|
2080
|
-
|
|
2081
|
-
// src/catalog/install-mapping.ts
|
|
2082
|
-
init_paths();
|
|
2083
|
-
init_path_safety();
|
|
2084
|
-
import path10 from "node:path";
|
|
2085
|
-
var FLATTEN_DIR_BY_TYPE = {
|
|
2086
|
-
skill: "skills",
|
|
2087
|
-
agent: "agents",
|
|
2088
|
-
template: "templates",
|
|
2089
|
-
command: "commands"
|
|
2090
|
-
};
|
|
2091
|
-
var MIRRORED_TYPES = /* @__PURE__ */ new Set(["rule", "asset"]);
|
|
2092
|
-
function upstreamRelative(itemPath) {
|
|
2093
|
-
const match = itemPath.replace(/\\/g, "/").match(/^sources\/[^/]+\/(.+)$/);
|
|
2094
|
-
return match ? match[1] : null;
|
|
2095
|
-
}
|
|
2096
|
-
function installTargetFor(item) {
|
|
2097
|
-
const type = item.type;
|
|
2098
|
-
if (type === "config") {
|
|
2099
|
-
return {
|
|
2100
|
-
ok: false,
|
|
2101
|
-
code: "scaffold-only",
|
|
2102
|
-
reason: 'type "config" is installed by `haus configs`, not into .claude/'
|
|
2103
|
-
};
|
|
2104
|
-
}
|
|
2105
|
-
const flattenDir = FLATTEN_DIR_BY_TYPE[type];
|
|
2106
|
-
const mirrored = MIRRORED_TYPES.has(type);
|
|
2107
|
-
if (!flattenDir && !mirrored) {
|
|
2108
|
-
return {
|
|
2109
|
-
ok: false,
|
|
2110
|
-
code: "unknown-type",
|
|
2111
|
-
reason: `type "${type}" is unknown to this haus version \u2014 upgrade the CLI to use it`
|
|
2112
|
-
};
|
|
2113
|
-
}
|
|
2114
|
-
const itemPath = typeof item.path === "string" ? item.path : "";
|
|
2115
|
-
if (!isSafeCatalogPath(itemPath)) {
|
|
2116
|
-
return {
|
|
2117
|
-
ok: false,
|
|
2118
|
-
code: "unsafe-path",
|
|
2119
|
-
reason: `path "${itemPath}" is not a safe relative catalog path`
|
|
2120
|
-
};
|
|
2121
|
-
}
|
|
2122
|
-
if (flattenDir) {
|
|
2123
|
-
return {
|
|
2124
|
-
ok: true,
|
|
2125
|
-
layout: "flatten",
|
|
2126
|
-
relPath: `${flattenDir}/${path10.posix.basename(itemPath)}`
|
|
2127
|
-
};
|
|
2128
|
-
}
|
|
2129
|
-
const rel = upstreamRelative(itemPath);
|
|
2130
|
-
if (rel === null) {
|
|
2131
|
-
return {
|
|
2132
|
-
ok: false,
|
|
2133
|
-
code: "not-mirrorable",
|
|
2134
|
-
reason: `type "${type}" installs by mirroring its upstream path, but "${itemPath}" is not under sources/<source>/`
|
|
2135
|
-
};
|
|
2136
|
-
}
|
|
2137
|
-
if (!isSafeRelativeFilePath(rel)) {
|
|
2138
|
-
return {
|
|
2139
|
-
ok: false,
|
|
2140
|
-
code: "unsafe-path",
|
|
2141
|
-
reason: `path "${itemPath}" escapes its own sources/<source>/ prefix`
|
|
2142
|
-
};
|
|
2143
|
-
}
|
|
2144
|
-
return { ok: true, layout: "mirror", relPath: rel };
|
|
2145
|
-
}
|
|
2146
|
-
function installDestinationFor(root, item) {
|
|
2147
|
-
const target = installTargetFor(item);
|
|
2148
|
-
if (!target.ok) return target;
|
|
2149
|
-
const destination = safeJoin(claudePath(root), target.relPath);
|
|
2150
|
-
if (destination === null) {
|
|
2151
|
-
return {
|
|
2152
|
-
ok: false,
|
|
2153
|
-
code: "unsafe-path",
|
|
2154
|
-
reason: `install path "${target.relPath}" escapes .claude/`
|
|
2155
|
-
};
|
|
2156
|
-
}
|
|
2157
|
-
return { ok: true, layout: target.layout, relPath: target.relPath, destination };
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
// src/catalog/former-ids.ts
|
|
2161
|
-
function normalizeFormerIds(value) {
|
|
2162
|
-
if (value === void 0 || value === null) return [];
|
|
2163
|
-
if (!Array.isArray(value) || value.length === 0) return [];
|
|
2164
|
-
if (!value.every((entry) => typeof entry === "string")) return [];
|
|
2165
|
-
return value.filter((entry) => entry.length > 0);
|
|
2166
|
-
}
|
|
2167
|
-
function assertFormerIdsShape(itemId, formerIds) {
|
|
2168
|
-
if (formerIds === void 0) return;
|
|
2169
|
-
if (Array.isArray(formerIds) && formerIds.length === 0) return;
|
|
2170
|
-
if (!Array.isArray(formerIds) || !formerIds.every((entry) => typeof entry === "string")) {
|
|
2171
|
-
throw new Error(`${itemId}: formerIds must be a string array`);
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
function buildFormerIdMap(items) {
|
|
2175
|
-
const map = /* @__PURE__ */ new Map();
|
|
2176
|
-
const currentIds = new Set(items.map((item) => item.id));
|
|
2177
|
-
for (const item of items) {
|
|
2178
|
-
assertFormerIdsShape(item.id, item.formerIds);
|
|
2179
|
-
const formerIds = item.formerIds ?? [];
|
|
2180
|
-
if (!Array.isArray(formerIds) || formerIds.length === 0) continue;
|
|
2181
|
-
for (const formerId of formerIds) {
|
|
2182
|
-
if (currentIds.has(formerId) && formerId !== item.id) {
|
|
2183
|
-
throw new Error(
|
|
2184
|
-
`formerId ${formerId} on ${item.id} conflicts with another item's current id`
|
|
2185
|
-
);
|
|
2186
|
-
}
|
|
2187
|
-
const existingOwner = map.get(formerId);
|
|
2188
|
-
if (existingOwner !== void 0) {
|
|
2189
|
-
throw new Error(
|
|
2190
|
-
`duplicate formerId ${formerId} claimed by both ${existingOwner} and ${item.id}`
|
|
2191
|
-
);
|
|
2192
|
-
}
|
|
2193
|
-
map.set(formerId, item.id);
|
|
2194
|
-
}
|
|
2283
|
+
} catch {
|
|
2284
|
+
return [];
|
|
2195
2285
|
}
|
|
2196
|
-
return map;
|
|
2197
|
-
}
|
|
2198
|
-
function findFormerIdMigrations(lockItems, manifestItems) {
|
|
2199
|
-
const formerIdMap = buildFormerIdMap(manifestItems);
|
|
2200
|
-
return lockItems.flatMap(({ id }) => {
|
|
2201
|
-
const newId = formerIdMap.get(id);
|
|
2202
|
-
return newId === void 0 ? [] : [{ oldId: id, newId }];
|
|
2203
|
-
});
|
|
2204
2286
|
}
|
|
2205
|
-
function
|
|
2206
|
-
const
|
|
2207
|
-
for (const
|
|
2208
|
-
|
|
2209
|
-
if (formerIds.length === 0) continue;
|
|
2210
|
-
const target = installTargetFor({ type: item.type, path: item.path });
|
|
2211
|
-
if (!target.ok || target.layout !== "flatten") continue;
|
|
2212
|
-
const dir = path11.posix.dirname(target.relPath);
|
|
2213
|
-
const base = path11.posix.basename(target.relPath);
|
|
2214
|
-
const ext = path11.posix.extname(base);
|
|
2215
|
-
const currentName = ext.length > 0 ? base.slice(0, -ext.length) : base;
|
|
2216
|
-
if (currentName.length === 0 || !item.id.endsWith(currentName)) continue;
|
|
2217
|
-
const prefix = item.id.slice(0, item.id.length - currentName.length);
|
|
2218
|
-
for (const formerId of formerIds) {
|
|
2219
|
-
if (!formerId.startsWith(prefix)) continue;
|
|
2220
|
-
const candidate = formerId.slice(prefix.length);
|
|
2221
|
-
if (candidate.length === 0 || candidate === currentName) continue;
|
|
2222
|
-
names.push({ formerId, currentId: item.id, dir, ext, candidate });
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
return names;
|
|
2287
|
+
function summarizeGitignoredHausDirs(paths) {
|
|
2288
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
2289
|
+
for (const p of paths) dirs.add(p === ".haus-workflow" ? ".haus-workflow/" : ".claude/");
|
|
2290
|
+
return [...dirs];
|
|
2226
2291
|
}
|
|
2227
2292
|
|
|
2228
2293
|
// src/claude/write-claude-files.ts
|
|
2294
|
+
import path25 from "node:path";
|
|
2295
|
+
import fs23 from "fs-extra";
|
|
2229
2296
|
init_ingest_catalog();
|
|
2230
2297
|
|
|
2231
2298
|
// src/recommender/orphaned-items.ts
|
|
@@ -2451,7 +2518,7 @@ function isSupersededByPlugin(lockId, activePinIds) {
|
|
|
2451
2518
|
|
|
2452
2519
|
// src/scanner/detect-package-manager.ts
|
|
2453
2520
|
import path13 from "node:path";
|
|
2454
|
-
import
|
|
2521
|
+
import fs9 from "fs-extra";
|
|
2455
2522
|
|
|
2456
2523
|
// src/utils/versions.ts
|
|
2457
2524
|
import semver from "semver";
|
|
@@ -2499,16 +2566,16 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2499
2566
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
2500
2567
|
return "unknown";
|
|
2501
2568
|
}
|
|
2502
|
-
if (
|
|
2503
|
-
if (
|
|
2504
|
-
if (
|
|
2569
|
+
if (fs9.existsSync(path13.join(root, "yarn.lock"))) return "yarn";
|
|
2570
|
+
if (fs9.existsSync(path13.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2571
|
+
if (fs9.existsSync(path13.join(root, "package-lock.json"))) return "npm";
|
|
2505
2572
|
return "unknown";
|
|
2506
2573
|
}
|
|
2507
2574
|
|
|
2508
2575
|
// src/update/hash-installed.ts
|
|
2509
2576
|
import path15 from "node:path";
|
|
2510
2577
|
import fg3 from "fast-glob";
|
|
2511
|
-
import
|
|
2578
|
+
import fs11 from "fs-extra";
|
|
2512
2579
|
|
|
2513
2580
|
// src/claude/haus-managed-header.ts
|
|
2514
2581
|
function parseHausManagedAttrs(line2) {
|
|
@@ -2551,7 +2618,7 @@ init_logger();
|
|
|
2551
2618
|
init_paths();
|
|
2552
2619
|
import path14 from "node:path";
|
|
2553
2620
|
import fg2 from "fast-glob";
|
|
2554
|
-
import
|
|
2621
|
+
import fs10 from "fs-extra";
|
|
2555
2622
|
var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
|
|
2556
2623
|
var SUPERPOWERS_SHARED_INSTALL_REL = ".claude/skills/shared";
|
|
2557
2624
|
var PATH_REWRITES = [
|
|
@@ -2574,10 +2641,10 @@ async function rewriteMarkdownTree(dir) {
|
|
|
2574
2641
|
const files = await fg2("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
|
|
2575
2642
|
for (const rel of files) {
|
|
2576
2643
|
const abs = path14.join(dir, rel);
|
|
2577
|
-
const text = await
|
|
2644
|
+
const text = await fs10.readFile(abs, "utf8");
|
|
2578
2645
|
const rewritten = rewriteSuperpowersMarkdown(text);
|
|
2579
2646
|
if (rewritten !== text) {
|
|
2580
|
-
await
|
|
2647
|
+
await fs10.writeFile(abs, rewritten, "utf8");
|
|
2581
2648
|
}
|
|
2582
2649
|
}
|
|
2583
2650
|
}
|
|
@@ -2591,40 +2658,40 @@ async function extraDestFiles(sourcePath, destination) {
|
|
|
2591
2658
|
}
|
|
2592
2659
|
async function installCatalogSkill(sourcePath, destination, opts) {
|
|
2593
2660
|
if (opts.dryRun) return;
|
|
2594
|
-
await
|
|
2595
|
-
if (await
|
|
2661
|
+
await fs10.ensureDir(path14.dirname(destination));
|
|
2662
|
+
if (await fs10.pathExists(destination)) {
|
|
2596
2663
|
const extra = await extraDestFiles(sourcePath, destination);
|
|
2597
2664
|
if (extra.length > 0) {
|
|
2598
2665
|
warn(
|
|
2599
2666
|
`${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
|
|
2600
2667
|
);
|
|
2601
2668
|
}
|
|
2602
|
-
for (const entry of await
|
|
2669
|
+
for (const entry of await fs10.readdir(destination)) {
|
|
2603
2670
|
if (SKILL_DEP_ARTIFACT_DIRS.has(entry)) continue;
|
|
2604
|
-
await
|
|
2671
|
+
await fs10.remove(path14.join(destination, entry));
|
|
2605
2672
|
}
|
|
2606
2673
|
}
|
|
2607
|
-
await
|
|
2674
|
+
await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
2608
2675
|
if (opts.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID) {
|
|
2609
2676
|
await rewriteMarkdownTree(destination);
|
|
2610
2677
|
}
|
|
2611
2678
|
}
|
|
2612
2679
|
async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
|
|
2613
2680
|
const source = path14.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
|
|
2614
|
-
if (!await
|
|
2681
|
+
if (!await fs10.pathExists(source)) return null;
|
|
2615
2682
|
const destination = claudePath(projectRoot, "skills", "shared");
|
|
2616
2683
|
if (dryRun) return path14.relative(projectRoot, destination);
|
|
2617
|
-
await
|
|
2618
|
-
if (await
|
|
2684
|
+
await fs10.ensureDir(path14.dirname(destination));
|
|
2685
|
+
if (await fs10.pathExists(destination)) {
|
|
2619
2686
|
const extra = await extraDestFiles(source, destination);
|
|
2620
2687
|
if (extra.length > 0) {
|
|
2621
2688
|
warn(
|
|
2622
2689
|
`${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
|
|
2623
2690
|
);
|
|
2624
2691
|
}
|
|
2625
|
-
await
|
|
2692
|
+
await fs10.remove(destination);
|
|
2626
2693
|
}
|
|
2627
|
-
await
|
|
2694
|
+
await fs10.copy(source, destination, { overwrite: true, errorOnExist: false });
|
|
2628
2695
|
await rewriteMarkdownTree(destination);
|
|
2629
2696
|
return path14.relative(projectRoot, destination);
|
|
2630
2697
|
}
|
|
@@ -2649,11 +2716,11 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
|
|
|
2649
2716
|
const fileDigests = [];
|
|
2650
2717
|
for (const rel of normalized) {
|
|
2651
2718
|
const abs = path15.join(root, rel);
|
|
2652
|
-
if (!await
|
|
2653
|
-
const stat = followSymlinks ? await
|
|
2719
|
+
if (!await fs11.pathExists(abs)) continue;
|
|
2720
|
+
const stat = followSymlinks ? await fs11.stat(abs) : await fs11.lstat(abs);
|
|
2654
2721
|
if (!followSymlinks && stat.isSymbolicLink()) continue;
|
|
2655
2722
|
if (stat.isFile()) {
|
|
2656
|
-
const body = await
|
|
2723
|
+
const body = await fs11.readFile(abs);
|
|
2657
2724
|
fileDigests.push({ rel, digest: digestFileContent(body) });
|
|
2658
2725
|
continue;
|
|
2659
2726
|
}
|
|
@@ -2674,9 +2741,9 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
|
|
|
2674
2741
|
});
|
|
2675
2742
|
for (const sub of inner.sort()) {
|
|
2676
2743
|
const absFile = path15.join(abs, sub);
|
|
2677
|
-
if (!followSymlinks && (await
|
|
2744
|
+
if (!followSymlinks && (await fs11.lstat(absFile)).isSymbolicLink()) continue;
|
|
2678
2745
|
const relFile = path15.join(rel, sub).replace(/\\/g, "/");
|
|
2679
|
-
const body = await
|
|
2746
|
+
const body = await fs11.readFile(absFile);
|
|
2680
2747
|
fileDigests.push({ rel: relFile, digest: digestFileContent(body) });
|
|
2681
2748
|
}
|
|
2682
2749
|
}
|
|
@@ -2715,15 +2782,15 @@ init_logger();
|
|
|
2715
2782
|
init_paths();
|
|
2716
2783
|
|
|
2717
2784
|
// src/claude/managed-write.ts
|
|
2718
|
-
import
|
|
2785
|
+
import fs12 from "fs-extra";
|
|
2719
2786
|
init_fs();
|
|
2720
2787
|
init_logger();
|
|
2721
2788
|
init_paths();
|
|
2722
2789
|
async function writeManagedText(root, filePath, nextText, dryRun, quiet = false) {
|
|
2723
2790
|
const say = quiet ? () => {
|
|
2724
2791
|
} : log;
|
|
2725
|
-
const existed = await
|
|
2726
|
-
const prev = existed ? await
|
|
2792
|
+
const existed = await fs12.pathExists(filePath);
|
|
2793
|
+
const prev = existed ? await fs12.readFile(filePath, "utf8") : "";
|
|
2727
2794
|
const printable = displayPath(root, filePath);
|
|
2728
2795
|
if (dryRun) {
|
|
2729
2796
|
if (!existed) {
|
|
@@ -2759,7 +2826,7 @@ function buildAllowRules() {
|
|
|
2759
2826
|
// src/install/settings-merge.ts
|
|
2760
2827
|
init_fs();
|
|
2761
2828
|
import path17 from "node:path";
|
|
2762
|
-
import
|
|
2829
|
+
import fs13 from "fs-extra";
|
|
2763
2830
|
|
|
2764
2831
|
// src/install/manifest.ts
|
|
2765
2832
|
init_fs();
|
|
@@ -3000,7 +3067,7 @@ function stripHausAsk(settings) {
|
|
|
3000
3067
|
async function loadHooksFragment(fragmentPath) {
|
|
3001
3068
|
let raw;
|
|
3002
3069
|
try {
|
|
3003
|
-
raw = await
|
|
3070
|
+
raw = await fs13.readJson(fragmentPath);
|
|
3004
3071
|
} catch {
|
|
3005
3072
|
return [];
|
|
3006
3073
|
}
|
|
@@ -3050,8 +3117,8 @@ function buildDenyRules() {
|
|
|
3050
3117
|
for (const command of DENY_COMMANDS) {
|
|
3051
3118
|
rules.push(`Bash(${command}:*)`);
|
|
3052
3119
|
}
|
|
3053
|
-
for (const
|
|
3054
|
-
const pattern = DENY_DIRS.has(
|
|
3120
|
+
for (const path81 of DENY_PATHS) {
|
|
3121
|
+
const pattern = DENY_DIRS.has(path81) ? `${path81}/**` : path81;
|
|
3055
3122
|
for (const tool of FILE_TOOLS) {
|
|
3056
3123
|
rules.push(`${tool}(${pattern})`);
|
|
3057
3124
|
}
|
|
@@ -3134,9 +3201,33 @@ async function applyProjectSettingsMerge(root) {
|
|
|
3134
3201
|
|
|
3135
3202
|
// src/claude/remove-items.ts
|
|
3136
3203
|
import path18 from "node:path";
|
|
3137
|
-
import
|
|
3204
|
+
import fs14 from "fs-extra";
|
|
3138
3205
|
init_fs();
|
|
3139
3206
|
init_paths();
|
|
3207
|
+
|
|
3208
|
+
// src/claude/backup-kinds.ts
|
|
3209
|
+
var DIR_BACKUP_PREFIXES = {
|
|
3210
|
+
undo: "undo",
|
|
3211
|
+
prune: "prune",
|
|
3212
|
+
remove: "remove",
|
|
3213
|
+
migrate: "migrate"
|
|
3214
|
+
};
|
|
3215
|
+
var LOCK_BACKUP_RE = /^haus\.lock\.\d+\.json$/;
|
|
3216
|
+
function dirBackupDirName(kind, stamp) {
|
|
3217
|
+
return `${DIR_BACKUP_PREFIXES[kind]}-${stamp}`;
|
|
3218
|
+
}
|
|
3219
|
+
function lockBackupFileName(epochMs) {
|
|
3220
|
+
return `haus.lock.${epochMs}.json`;
|
|
3221
|
+
}
|
|
3222
|
+
function classifyBackupName(name) {
|
|
3223
|
+
if (LOCK_BACKUP_RE.test(name)) return "lock";
|
|
3224
|
+
for (const kind of Object.keys(DIR_BACKUP_PREFIXES)) {
|
|
3225
|
+
if (name.startsWith(`${DIR_BACKUP_PREFIXES[kind]}-`)) return kind;
|
|
3226
|
+
}
|
|
3227
|
+
return void 0;
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// src/claude/remove-items.ts
|
|
3140
3231
|
async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn) {
|
|
3141
3232
|
const existing = [];
|
|
3142
3233
|
for (const rel of relPaths) {
|
|
@@ -3145,7 +3236,7 @@ async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn) {
|
|
|
3145
3236
|
warnFn(`Refusing to touch ${itemId}'s recorded path outside the project root: ${rel}`);
|
|
3146
3237
|
continue;
|
|
3147
3238
|
}
|
|
3148
|
-
if (await
|
|
3239
|
+
if (await fs14.pathExists(abs)) existing.push({ rel, abs });
|
|
3149
3240
|
}
|
|
3150
3241
|
return existing;
|
|
3151
3242
|
}
|
|
@@ -3161,13 +3252,13 @@ async function hashGateEntry(root, entry, existing) {
|
|
|
3161
3252
|
async function backupTrackedPaths(root, absPaths, say, prefix) {
|
|
3162
3253
|
if (absPaths.length === 0) return null;
|
|
3163
3254
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3164
|
-
const backupRoot = hausPath(root, "backups",
|
|
3255
|
+
const backupRoot = hausPath(root, "backups", dirBackupDirName(prefix, stamp));
|
|
3165
3256
|
for (const abs of absPaths) {
|
|
3166
|
-
if (!await
|
|
3257
|
+
if (!await fs14.pathExists(abs)) continue;
|
|
3167
3258
|
const rel = path18.relative(root, abs);
|
|
3168
3259
|
const backupPath = path18.join(backupRoot, rel);
|
|
3169
|
-
await
|
|
3170
|
-
await
|
|
3260
|
+
await fs14.ensureDir(path18.dirname(backupPath));
|
|
3261
|
+
await fs14.copy(abs, backupPath);
|
|
3171
3262
|
}
|
|
3172
3263
|
say(
|
|
3173
3264
|
`Backed up ${absPaths.length} path(s) to ${path18.relative(root, backupRoot)} before deleting them.`
|
|
@@ -3176,7 +3267,7 @@ async function backupTrackedPaths(root, absPaths, say, prefix) {
|
|
|
3176
3267
|
}
|
|
3177
3268
|
async function deleteTrackedPaths(root, itemId, existing, { verb, reason }, say) {
|
|
3178
3269
|
for (const { abs } of existing) {
|
|
3179
|
-
await
|
|
3270
|
+
await fs14.remove(abs);
|
|
3180
3271
|
await pruneEmptyDir(path18.dirname(abs));
|
|
3181
3272
|
say([verb, reason, displayPath(root, abs)].filter(Boolean).join(" ") + ` (${itemId})`);
|
|
3182
3273
|
}
|
|
@@ -3185,7 +3276,7 @@ async function deleteTrackedPaths(root, itemId, existing, { verb, reason }, say)
|
|
|
3185
3276
|
// src/claude/skill-dep-install.ts
|
|
3186
3277
|
init_exec();
|
|
3187
3278
|
import path19 from "node:path";
|
|
3188
|
-
import
|
|
3279
|
+
import fs15 from "fs-extra";
|
|
3189
3280
|
function skillDepInstallCommand(manager) {
|
|
3190
3281
|
switch (manager) {
|
|
3191
3282
|
case "npm":
|
|
@@ -3222,7 +3313,7 @@ function skillDepInstallCommand(manager) {
|
|
|
3222
3313
|
}
|
|
3223
3314
|
async function hasDeclaredDependencies(dir) {
|
|
3224
3315
|
try {
|
|
3225
|
-
const raw = await
|
|
3316
|
+
const raw = await fs15.readFile(path19.join(dir, "package.json"), "utf8");
|
|
3226
3317
|
const parsed = JSON.parse(raw);
|
|
3227
3318
|
if (typeof parsed !== "object" || parsed === null) return false;
|
|
3228
3319
|
const deps = parsed.dependencies;
|
|
@@ -3234,7 +3325,7 @@ async function hasDeclaredDependencies(dir) {
|
|
|
3234
3325
|
}
|
|
3235
3326
|
async function realpathOrSelf(target) {
|
|
3236
3327
|
try {
|
|
3237
|
-
return await
|
|
3328
|
+
return await fs15.realpath(target);
|
|
3238
3329
|
} catch {
|
|
3239
3330
|
return path19.resolve(target);
|
|
3240
3331
|
}
|
|
@@ -3279,7 +3370,7 @@ async function installSkillDependencies(opts, deps = {}) {
|
|
|
3279
3370
|
default:
|
|
3280
3371
|
return assertNeverCommandProbe(probe);
|
|
3281
3372
|
}
|
|
3282
|
-
if (!await
|
|
3373
|
+
if (!await fs15.pathExists(contained.dir)) {
|
|
3283
3374
|
return { status: "skipped", reason: `install directory disappeared (${contained.dir})` };
|
|
3284
3375
|
}
|
|
3285
3376
|
const run = deps.run ?? defaultRun;
|
|
@@ -3293,7 +3384,7 @@ async function installSkillDependencies(opts, deps = {}) {
|
|
|
3293
3384
|
// src/claude/verify-hooks-contract.ts
|
|
3294
3385
|
init_fs();
|
|
3295
3386
|
init_paths();
|
|
3296
|
-
import
|
|
3387
|
+
import fs16 from "fs-extra";
|
|
3297
3388
|
|
|
3298
3389
|
// src/claude/load-hooks.ts
|
|
3299
3390
|
var CANONICAL_HOOKS = {
|
|
@@ -3377,7 +3468,7 @@ async function assertPostApplySettingsHausContract(root) {
|
|
|
3377
3468
|
}
|
|
3378
3469
|
async function verifyProjectSettingsHooksContract(root) {
|
|
3379
3470
|
const settingsPath = claudePath(root, "settings.json");
|
|
3380
|
-
if (!await
|
|
3471
|
+
if (!await fs16.pathExists(settingsPath)) {
|
|
3381
3472
|
return {
|
|
3382
3473
|
ok: true,
|
|
3383
3474
|
skipped: true,
|
|
@@ -3405,7 +3496,7 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
3405
3496
|
|
|
3406
3497
|
// src/claude/write-decisions-seed.ts
|
|
3407
3498
|
import path20 from "node:path";
|
|
3408
|
-
import
|
|
3499
|
+
import fs17 from "fs-extra";
|
|
3409
3500
|
|
|
3410
3501
|
// library/catalog/decisions-triggers.json
|
|
3411
3502
|
var decisions_triggers_default = {
|
|
@@ -3450,15 +3541,15 @@ Why we chose X \u2014 short, dated, write-once decision log for humans and agent
|
|
|
3450
3541
|
async function writeDecisionsSeed(root, dryRun, quiet = false) {
|
|
3451
3542
|
const decisionsDir = path20.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
3452
3543
|
const readmePath = path20.join(decisionsDir, "README.md");
|
|
3453
|
-
if (await
|
|
3454
|
-
if (!dryRun) await
|
|
3544
|
+
if (await fs17.pathExists(readmePath)) return null;
|
|
3545
|
+
if (!dryRun) await fs17.ensureDir(decisionsDir);
|
|
3455
3546
|
await writeManagedText(root, readmePath, SEED_README, dryRun, quiet);
|
|
3456
3547
|
return readmePath;
|
|
3457
3548
|
}
|
|
3458
3549
|
|
|
3459
3550
|
// src/claude/write-prettierignore.ts
|
|
3460
3551
|
import path21 from "node:path";
|
|
3461
|
-
import
|
|
3552
|
+
import fs18 from "fs-extra";
|
|
3462
3553
|
var PRETTIERIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
3463
3554
|
var PRETTIERIGNORE_END = "# HAUS:END haus-managed";
|
|
3464
3555
|
var IGNORED_PATHS = [".haus-workflow/", ".claude/"];
|
|
@@ -3527,7 +3618,7 @@ ${block}
|
|
|
3527
3618
|
}
|
|
3528
3619
|
async function writePrettierIgnore(root, dryRun, quiet = false) {
|
|
3529
3620
|
const filePath = path21.join(root, ".prettierignore");
|
|
3530
|
-
const prev = await
|
|
3621
|
+
const prev = await fs18.pathExists(filePath) ? await fs18.readFile(filePath, "utf8") : "";
|
|
3531
3622
|
const next = injectPrettierIgnoreBlock(prev, buildPrettierIgnoreBlock());
|
|
3532
3623
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
3533
3624
|
return filePath;
|
|
@@ -3535,7 +3626,7 @@ async function writePrettierIgnore(root, dryRun, quiet = false) {
|
|
|
3535
3626
|
|
|
3536
3627
|
// src/claude/write-root-claude-md.ts
|
|
3537
3628
|
import path22 from "node:path";
|
|
3538
|
-
import
|
|
3629
|
+
import fs19 from "fs-extra";
|
|
3539
3630
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
3540
3631
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
3541
3632
|
var WORKSPACE_BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-workspace-imports v=1 -->";
|
|
@@ -3614,7 +3705,7 @@ ${block}
|
|
|
3614
3705
|
async function writeRootClaudeMd(root, dryRun, quiet = false) {
|
|
3615
3706
|
const filePath = path22.join(root, "CLAUDE.md");
|
|
3616
3707
|
const block = buildImportBlock();
|
|
3617
|
-
const prev = await
|
|
3708
|
+
const prev = await fs19.pathExists(filePath) ? await fs19.readFile(filePath, "utf8") : "";
|
|
3618
3709
|
const next = injectHausBlock(prev, block);
|
|
3619
3710
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
3620
3711
|
return filePath;
|
|
@@ -3625,12 +3716,12 @@ init_fs();
|
|
|
3625
3716
|
init_logger();
|
|
3626
3717
|
init_paths();
|
|
3627
3718
|
import path24 from "node:path";
|
|
3628
|
-
import
|
|
3719
|
+
import fs21 from "fs-extra";
|
|
3629
3720
|
|
|
3630
3721
|
// src/claude/derive-workflow-config.ts
|
|
3631
3722
|
init_fs();
|
|
3632
3723
|
import path23 from "node:path";
|
|
3633
|
-
import
|
|
3724
|
+
import fs20 from "fs-extra";
|
|
3634
3725
|
function binCmd(pm, bin, args) {
|
|
3635
3726
|
const tail = args ? ` ${args}` : "";
|
|
3636
3727
|
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
@@ -3649,7 +3740,7 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
3649
3740
|
return null;
|
|
3650
3741
|
};
|
|
3651
3742
|
const hasDep = (name) => deps.has(name);
|
|
3652
|
-
const exists = (rel) =>
|
|
3743
|
+
const exists = (rel) => fs20.pathExistsSync(path23.join(root, rel));
|
|
3653
3744
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
3654
3745
|
const hasCypress = hasDep("cypress");
|
|
3655
3746
|
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;
|
|
@@ -3718,7 +3809,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
3718
3809
|
} : log;
|
|
3719
3810
|
const destPath = hausPath(root, "workflow-config.md");
|
|
3720
3811
|
const printable = displayPath(root, destPath);
|
|
3721
|
-
const exists = await
|
|
3812
|
+
const exists = await fs21.pathExists(destPath);
|
|
3722
3813
|
if (exists && !opts.refill) {
|
|
3723
3814
|
if (dryRun) say(printable + ": exists (project-owned, skipping)");
|
|
3724
3815
|
return null;
|
|
@@ -3730,7 +3821,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
3730
3821
|
};
|
|
3731
3822
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
3732
3823
|
if (exists) {
|
|
3733
|
-
const current = await
|
|
3824
|
+
const current = await fs21.readFile(destPath, "utf8");
|
|
3734
3825
|
const refilled = refillContent(current, values);
|
|
3735
3826
|
if (refilled === current) {
|
|
3736
3827
|
if (dryRun) say(printable + ": no blank fields to refill");
|
|
@@ -3752,7 +3843,8 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
3752
3843
|
}
|
|
3753
3844
|
|
|
3754
3845
|
// src/claude/write-workflow.ts
|
|
3755
|
-
|
|
3846
|
+
init_offline();
|
|
3847
|
+
import fs22 from "fs-extra";
|
|
3756
3848
|
init_fs();
|
|
3757
3849
|
init_logger();
|
|
3758
3850
|
init_paths();
|
|
@@ -3784,7 +3876,7 @@ async function writeWorkflow(root, pkgVersion, dryRun, force = false, quiet = fa
|
|
|
3784
3876
|
const templateContent = await readWorkflowTemplate({ dryRun, projectRoot: root });
|
|
3785
3877
|
if (templateContent === null) {
|
|
3786
3878
|
warn(
|
|
3787
|
-
`Workflow template could not be fetched from the catalog \u2014 check your network, then re-run \`haus apply --write\` (or \`haus update\`)`
|
|
3879
|
+
isOffline() ? `Workflow template is not in the local catalog cache and this run is offline \u2014 run \`haus catalog bundle import <file>\` (or drop \`--offline\`), then re-run \`haus apply --write\`` : `Workflow template could not be fetched from the catalog \u2014 check your network, then re-run \`haus apply --write\` (or \`haus update\`)`
|
|
3788
3880
|
);
|
|
3789
3881
|
return null;
|
|
3790
3882
|
}
|
|
@@ -3794,8 +3886,8 @@ async function writeWorkflow(root, pkgVersion, dryRun, force = false, quiet = fa
|
|
|
3794
3886
|
${templateContent}`;
|
|
3795
3887
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
3796
3888
|
const printable = displayPath(root, destPath);
|
|
3797
|
-
if (await
|
|
3798
|
-
const existing = await
|
|
3889
|
+
if (await fs22.pathExists(destPath)) {
|
|
3890
|
+
const existing = await fs22.readFile(destPath, "utf8");
|
|
3799
3891
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
3800
3892
|
const parsed = parseHausManagedHeader(firstLine);
|
|
3801
3893
|
if (!parsed) {
|
|
@@ -3839,7 +3931,7 @@ ${existingContent}`;
|
|
|
3839
3931
|
}
|
|
3840
3932
|
}
|
|
3841
3933
|
if (dryRun) {
|
|
3842
|
-
const prev = await
|
|
3934
|
+
const prev = await fs22.pathExists(destPath) ? await fs22.readFile(destPath, "utf8") : "";
|
|
3843
3935
|
if (!prev) {
|
|
3844
3936
|
say(createUnifiedDiff(printable, "", next));
|
|
3845
3937
|
} else {
|
|
@@ -3894,14 +3986,14 @@ function decodeIfText(buf) {
|
|
|
3894
3986
|
return Buffer.from(asText, "utf8").equals(buf) ? asText : null;
|
|
3895
3987
|
}
|
|
3896
3988
|
async function listFilesRecursive2(dir) {
|
|
3897
|
-
if (!await
|
|
3989
|
+
if (!await fs23.pathExists(dir)) return [];
|
|
3898
3990
|
const files = [];
|
|
3899
3991
|
const walk = async (current, relPrefix) => {
|
|
3900
|
-
const names = await
|
|
3992
|
+
const names = await fs23.readdir(current);
|
|
3901
3993
|
for (const name of names) {
|
|
3902
3994
|
const abs = path25.join(current, name);
|
|
3903
3995
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
3904
|
-
const stat = await
|
|
3996
|
+
const stat = await fs23.lstat(abs);
|
|
3905
3997
|
if (stat.isSymbolicLink()) {
|
|
3906
3998
|
warn(`Skipping symlink in dry-run diff preview: ${rel}`);
|
|
3907
3999
|
continue;
|
|
@@ -3918,14 +4010,14 @@ async function listFilesRecursive2(dir) {
|
|
|
3918
4010
|
}
|
|
3919
4011
|
async function logFileDryRunDiff(root, srcFile, destFile, itemId, say) {
|
|
3920
4012
|
const printable = displayPath(root, destFile);
|
|
3921
|
-
const nextBuf = await
|
|
3922
|
-
if (!await
|
|
4013
|
+
const nextBuf = await fs23.readFile(srcFile);
|
|
4014
|
+
if (!await fs23.pathExists(destFile)) {
|
|
3923
4015
|
say(`${printable}: would create (${itemId})`);
|
|
3924
4016
|
const nextText2 = decodeIfText(nextBuf);
|
|
3925
4017
|
if (nextText2 !== null) say(createUnifiedDiff(printable, "", nextText2));
|
|
3926
4018
|
return;
|
|
3927
4019
|
}
|
|
3928
|
-
const prevBuf = await
|
|
4020
|
+
const prevBuf = await fs23.readFile(destFile);
|
|
3929
4021
|
if (prevBuf.equals(nextBuf)) {
|
|
3930
4022
|
say(`${printable}: unchanged (${itemId})`);
|
|
3931
4023
|
return;
|
|
@@ -3958,15 +4050,15 @@ async function logSkillDryRunDiff(root, sourcePath, destination, itemId, say) {
|
|
|
3958
4050
|
}
|
|
3959
4051
|
async function removeLegacyManagedStub(root, relPathSegments, stub, dryRun, say) {
|
|
3960
4052
|
const target = claudePath(root, ...relPathSegments);
|
|
3961
|
-
if (!await
|
|
3962
|
-
const content2 = await
|
|
4053
|
+
if (!await fs23.pathExists(target)) return;
|
|
4054
|
+
const content2 = await fs23.readFile(target, "utf8");
|
|
3963
4055
|
if (content2 !== stub && content2 !== `${stub}
|
|
3964
4056
|
` && content2 !== `${stub}\r
|
|
3965
4057
|
`) return;
|
|
3966
4058
|
if (dryRun) {
|
|
3967
4059
|
say(`[dry-run] would remove stale ${displayPath(root, target)}`);
|
|
3968
4060
|
} else {
|
|
3969
|
-
await
|
|
4061
|
+
await fs23.remove(target);
|
|
3970
4062
|
}
|
|
3971
4063
|
}
|
|
3972
4064
|
async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
@@ -4072,11 +4164,11 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4072
4164
|
];
|
|
4073
4165
|
for (const rel of LEGACY_PRUNED_ARTIFACTS) {
|
|
4074
4166
|
const legacyPath = hausPath(root, rel);
|
|
4075
|
-
if (await
|
|
4167
|
+
if (await fs23.pathExists(legacyPath)) {
|
|
4076
4168
|
if (dryRun) {
|
|
4077
4169
|
say(`[dry-run] would remove stale ${displayPath(root, legacyPath)}`);
|
|
4078
4170
|
} else {
|
|
4079
|
-
await
|
|
4171
|
+
await fs23.remove(legacyPath);
|
|
4080
4172
|
}
|
|
4081
4173
|
}
|
|
4082
4174
|
}
|
|
@@ -4194,7 +4286,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4194
4286
|
}
|
|
4195
4287
|
const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
|
|
4196
4288
|
const destination = placement.destination;
|
|
4197
|
-
if (await
|
|
4289
|
+
if (await fs23.pathExists(sourcePath)) {
|
|
4198
4290
|
if (dryRun) {
|
|
4199
4291
|
if (item.type === "skill") {
|
|
4200
4292
|
await logSkillDryRunDiff(root, sourcePath, destination, item.id, say);
|
|
@@ -4221,13 +4313,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4221
4313
|
)) {
|
|
4222
4314
|
continue;
|
|
4223
4315
|
} else if (item.type === "skill") {
|
|
4224
|
-
const skillFiles = (await
|
|
4316
|
+
const skillFiles = (await fs23.readdir(sourcePath, { recursive: true })).filter(
|
|
4225
4317
|
(f) => typeof f === "string" && f.endsWith(".md")
|
|
4226
4318
|
);
|
|
4227
4319
|
let skillValid = true;
|
|
4228
4320
|
for (const relFile of skillFiles) {
|
|
4229
4321
|
const mdPath = path25.join(sourcePath, relFile);
|
|
4230
|
-
const mdContent = await
|
|
4322
|
+
const mdContent = await fs23.readFile(mdPath, "utf8");
|
|
4231
4323
|
const mdValidation = validateCatalogItem(
|
|
4232
4324
|
{
|
|
4233
4325
|
id: manifestItem.id,
|
|
@@ -4260,7 +4352,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4260
4352
|
say
|
|
4261
4353
|
);
|
|
4262
4354
|
} else {
|
|
4263
|
-
const fileContent = await
|
|
4355
|
+
const fileContent = await fs23.readFile(sourcePath, "utf8");
|
|
4264
4356
|
const validation = validateCatalogItem(
|
|
4265
4357
|
{
|
|
4266
4358
|
id: manifestItem.id,
|
|
@@ -4274,8 +4366,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4274
4366
|
warn(`Skipping ${item.id}: pre-copy validation failed \u2014 ${validation.reason}`);
|
|
4275
4367
|
continue;
|
|
4276
4368
|
}
|
|
4277
|
-
await
|
|
4278
|
-
await
|
|
4369
|
+
await fs23.ensureDir(path25.dirname(destination));
|
|
4370
|
+
await fs23.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
4279
4371
|
}
|
|
4280
4372
|
files.push(destination);
|
|
4281
4373
|
const relPaths = [path25.relative(root, destination)];
|
|
@@ -4407,7 +4499,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
4407
4499
|
async function hasProtectedUnsafePath(root, relPaths) {
|
|
4408
4500
|
for (const rel of relPaths) {
|
|
4409
4501
|
if (resolveContainedPath(root, rel) !== null) continue;
|
|
4410
|
-
if (await
|
|
4502
|
+
if (await fs23.pathExists(path25.resolve(root, rel))) return true;
|
|
4411
4503
|
}
|
|
4412
4504
|
return false;
|
|
4413
4505
|
}
|
|
@@ -4441,7 +4533,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
4441
4533
|
const relPaths = entry.paths ?? [];
|
|
4442
4534
|
const existing = await resolveExistingTrackedPaths(root, migration.oldId, relPaths, warn);
|
|
4443
4535
|
if (existing.length === 0) continue;
|
|
4444
|
-
const gate = await hashGateEntry(root, {
|
|
4536
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4445
4537
|
if (!gate.ok) {
|
|
4446
4538
|
if (gate.reason === "no-hash") {
|
|
4447
4539
|
warn(
|
|
@@ -4468,10 +4560,10 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
4468
4560
|
return;
|
|
4469
4561
|
}
|
|
4470
4562
|
const allAbsPaths = toRemove.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
4471
|
-
await backupTrackedPaths(root, allAbsPaths, say, "migrate");
|
|
4563
|
+
void await backupTrackedPaths(root, allAbsPaths, say, "migrate");
|
|
4472
4564
|
for (const { existing } of toRemove) {
|
|
4473
4565
|
for (const { abs } of existing) {
|
|
4474
|
-
await
|
|
4566
|
+
await fs23.remove(abs);
|
|
4475
4567
|
await pruneEmptyDir(path25.dirname(abs));
|
|
4476
4568
|
}
|
|
4477
4569
|
}
|
|
@@ -4487,11 +4579,13 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4487
4579
|
);
|
|
4488
4580
|
let keepShared = false;
|
|
4489
4581
|
for (const entry of supersededEntries) {
|
|
4582
|
+
const id = entry.id;
|
|
4583
|
+
if (!id) continue;
|
|
4490
4584
|
const relPaths = entry.paths ?? [];
|
|
4491
4585
|
if (relPaths.length === 0) continue;
|
|
4492
|
-
const existing = await resolveExistingTrackedPaths(root,
|
|
4586
|
+
const existing = await resolveExistingTrackedPaths(root, id, relPaths, warn);
|
|
4493
4587
|
if (existing.length === 0) continue;
|
|
4494
|
-
const gate = await hashGateEntry(root, {
|
|
4588
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4495
4589
|
if (!gate.ok) keepShared = true;
|
|
4496
4590
|
}
|
|
4497
4591
|
for (const entry of prevLock) {
|
|
@@ -4503,12 +4597,15 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4503
4597
|
if (!removedFromManifest && !deprecated && !superseded) continue;
|
|
4504
4598
|
const relPaths = entry.paths ?? [];
|
|
4505
4599
|
if (relPaths.length === 0) continue;
|
|
4506
|
-
if (await hasProtectedUnsafePath(root, relPaths)) refusedIds.add(entry.id);
|
|
4507
4600
|
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
4601
|
+
if (await hasProtectedUnsafePath(root, relPaths)) {
|
|
4602
|
+
refusedIds.add(entry.id);
|
|
4603
|
+
continue;
|
|
4604
|
+
}
|
|
4508
4605
|
if (existing.length === 0) continue;
|
|
4509
4606
|
const pruneReason = superseded ? "superseded" : deprecated ? "deprecated" : "stale";
|
|
4510
4607
|
const label = superseded ? "Superseded" : deprecated ? "Deprecated" : "Stale";
|
|
4511
|
-
const gate = await hashGateEntry(root, {
|
|
4608
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4512
4609
|
if (!gate.ok) {
|
|
4513
4610
|
if (gate.reason === "no-hash") {
|
|
4514
4611
|
warn(
|
|
@@ -4533,7 +4630,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4533
4630
|
return true;
|
|
4534
4631
|
}) : existing;
|
|
4535
4632
|
if (!dryRun && removable.length > 0) {
|
|
4536
|
-
await backupTrackedPaths(
|
|
4633
|
+
void await backupTrackedPaths(
|
|
4537
4634
|
root,
|
|
4538
4635
|
removable.map((e) => e.abs),
|
|
4539
4636
|
say,
|
|
@@ -4545,7 +4642,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4545
4642
|
say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
4546
4643
|
continue;
|
|
4547
4644
|
}
|
|
4548
|
-
await
|
|
4645
|
+
await fs23.remove(abs);
|
|
4549
4646
|
await pruneEmptyDir(path25.dirname(abs));
|
|
4550
4647
|
say(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
4551
4648
|
}
|
|
@@ -4563,10 +4660,13 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
4563
4660
|
if (!entry.id) continue;
|
|
4564
4661
|
const relPaths = entry.paths ?? [];
|
|
4565
4662
|
if (relPaths.length === 0) continue;
|
|
4566
|
-
if (await hasProtectedUnsafePath(root, relPaths)) refusedIds.add(entry.id);
|
|
4567
4663
|
const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
|
|
4664
|
+
if (await hasProtectedUnsafePath(root, relPaths)) {
|
|
4665
|
+
refusedIds.add(entry.id);
|
|
4666
|
+
continue;
|
|
4667
|
+
}
|
|
4568
4668
|
if (existing.length === 0) continue;
|
|
4569
|
-
const gate = await hashGateEntry(root, {
|
|
4669
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
4570
4670
|
if (!gate.ok) {
|
|
4571
4671
|
const detail = gate.reason === "no-hash" ? "has no lock hash" : "was modified locally";
|
|
4572
4672
|
warn(
|
|
@@ -4587,7 +4687,7 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
4587
4687
|
return refusedIds;
|
|
4588
4688
|
}
|
|
4589
4689
|
const allAbsPaths = toRemove.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
4590
|
-
await backupTrackedPaths(root, allAbsPaths, say, "prune");
|
|
4690
|
+
void await backupTrackedPaths(root, allAbsPaths, say, "prune");
|
|
4591
4691
|
for (const { id, existing } of toRemove) {
|
|
4592
4692
|
await deleteTrackedPaths(root, id, existing, { verb: "Pruned", reason: "orphaned" }, say);
|
|
4593
4693
|
}
|
|
@@ -4597,14 +4697,15 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
4597
4697
|
// src/claude/write-gitignore.ts
|
|
4598
4698
|
init_exec();
|
|
4599
4699
|
import path26 from "node:path";
|
|
4600
|
-
import
|
|
4700
|
+
import fs24 from "fs-extra";
|
|
4601
4701
|
var GITIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
4602
4702
|
var GITIGNORE_END = "# HAUS:END haus-managed";
|
|
4603
4703
|
var GITIGNORED_ARTIFACT_PATHS = [
|
|
4604
4704
|
".haus-workflow/context-map.json",
|
|
4605
4705
|
".haus-workflow/recommendation.json",
|
|
4606
4706
|
".haus-workflow/sources-report.json",
|
|
4607
|
-
".haus-workflow/deep-context.json"
|
|
4707
|
+
".haus-workflow/deep-context.json",
|
|
4708
|
+
".haus-workflow/backups/"
|
|
4608
4709
|
];
|
|
4609
4710
|
function findLineMarker3(content2, marker, from = 0) {
|
|
4610
4711
|
let idx = content2.indexOf(marker, from);
|
|
@@ -4649,7 +4750,7 @@ function buildBlockFor(existing, beginMarker, endMarker, comment, patterns) {
|
|
|
4649
4750
|
if (needed.length === 0) return null;
|
|
4650
4751
|
return [beginMarker, comment, ...needed, endMarker].join("\n");
|
|
4651
4752
|
}
|
|
4652
|
-
var ARTIFACT_BLOCK_COMMENT = "# haus
|
|
4753
|
+
var ARTIFACT_BLOCK_COMMENT = "# haus machine-local artifacts \u2014 must never be committed (see ADR-0025, ADR-0058)";
|
|
4653
4754
|
function injectBlockFor(existing, beginMarker, endMarker, comment, patterns) {
|
|
4654
4755
|
const block = buildBlockFor(existing, beginMarker, endMarker, comment, patterns);
|
|
4655
4756
|
const range = findBlockRangeFor(existing, beginMarker, endMarker);
|
|
@@ -4712,14 +4813,14 @@ function injectLinkContextGitignoreBlock(existing) {
|
|
|
4712
4813
|
}
|
|
4713
4814
|
async function writeGitignore(root, dryRun, quiet = false) {
|
|
4714
4815
|
const filePath = path26.join(root, ".gitignore");
|
|
4715
|
-
const prev = await
|
|
4816
|
+
const prev = await fs24.pathExists(filePath) ? await fs24.readFile(filePath, "utf8") : "";
|
|
4716
4817
|
const next = injectGitignoreBlock(prev);
|
|
4717
4818
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
4718
4819
|
return filePath;
|
|
4719
4820
|
}
|
|
4720
4821
|
async function writeLinkContextGitignore(root, dryRun, quiet = false) {
|
|
4721
4822
|
const filePath = path26.join(root, ".gitignore");
|
|
4722
|
-
const prev = await
|
|
4823
|
+
const prev = await fs24.pathExists(filePath) ? await fs24.readFile(filePath, "utf8") : "";
|
|
4723
4824
|
const next = injectLinkContextGitignoreBlock(prev);
|
|
4724
4825
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
4725
4826
|
return filePath;
|
|
@@ -4742,7 +4843,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
4742
4843
|
for (const rel of tracked) {
|
|
4743
4844
|
if (dryRun) {
|
|
4744
4845
|
notify(
|
|
4745
|
-
`Would untrack ${rel} from git \u2014 it
|
|
4846
|
+
`Would untrack ${rel} from git \u2014 it is machine-local haus output and should never have been committed.`
|
|
4746
4847
|
);
|
|
4747
4848
|
continue;
|
|
4748
4849
|
}
|
|
@@ -4750,7 +4851,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
4750
4851
|
if (result.exitCode === 0) {
|
|
4751
4852
|
untracked.push(rel);
|
|
4752
4853
|
notify(
|
|
4753
|
-
`Untracked ${rel} from git \u2014 it
|
|
4854
|
+
`Untracked ${rel} from git \u2014 it is machine-local haus output and should never have been committed. Run \`git status\` to review before committing this change.`
|
|
4754
4855
|
);
|
|
4755
4856
|
} else {
|
|
4756
4857
|
notify(`Could not untrack ${rel} from git: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
@@ -4761,6 +4862,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
4761
4862
|
|
|
4762
4863
|
// src/install/ensure-global-plugins.ts
|
|
4763
4864
|
import path27 from "node:path";
|
|
4865
|
+
init_offline();
|
|
4764
4866
|
init_exec();
|
|
4765
4867
|
init_fs();
|
|
4766
4868
|
init_logger();
|
|
@@ -4941,6 +5043,9 @@ var EXEC_ENV = { GIT_TERMINAL_PROMPT: "0" };
|
|
|
4941
5043
|
function globalPluginsDisabled() {
|
|
4942
5044
|
return process.env.HAUS_NO_GLOBAL_PLUGINS === "1";
|
|
4943
5045
|
}
|
|
5046
|
+
function globalPluginMutationsDisabled() {
|
|
5047
|
+
return globalPluginsDisabled() || isOffline();
|
|
5048
|
+
}
|
|
4944
5049
|
var realExec = (args) => runCommand("claude", args, {
|
|
4945
5050
|
timeout: EXEC_TIMEOUT_MS,
|
|
4946
5051
|
env: EXEC_ENV,
|
|
@@ -5084,7 +5189,7 @@ async function ensureGlobalPlugins(pins, deps = {}) {
|
|
|
5084
5189
|
const log2 = deps.log ?? log;
|
|
5085
5190
|
const warn2 = deps.warn ?? warn;
|
|
5086
5191
|
const dryRun = deps.dryRun ?? false;
|
|
5087
|
-
if (
|
|
5192
|
+
if (globalPluginMutationsDisabled() && deps.exec === void 0) {
|
|
5088
5193
|
return { actions: [], ensured: [], failed: [], unavailable: true, restartRequired: false };
|
|
5089
5194
|
}
|
|
5090
5195
|
const state = await probePluginEnv(deps);
|
|
@@ -5153,7 +5258,7 @@ async function removeGlobalPlugins(plugins, deps = {}) {
|
|
|
5153
5258
|
const log2 = deps.log ?? log;
|
|
5154
5259
|
const result = { removed: [], failed: [], unavailable: false };
|
|
5155
5260
|
if (plugins.length === 0) return result;
|
|
5156
|
-
if (
|
|
5261
|
+
if (globalPluginMutationsDisabled() && deps.exec === void 0) {
|
|
5157
5262
|
return { ...result, unavailable: true };
|
|
5158
5263
|
}
|
|
5159
5264
|
const state = await probePluginEnv(deps);
|
|
@@ -5202,25 +5307,25 @@ async function removeGlobalPlugins(plugins, deps = {}) {
|
|
|
5202
5307
|
|
|
5203
5308
|
// src/refs/fetch-refs.ts
|
|
5204
5309
|
import path29 from "node:path";
|
|
5205
|
-
import
|
|
5310
|
+
import fs26 from "fs-extra";
|
|
5206
5311
|
|
|
5207
5312
|
// src/refs/cache-meta.ts
|
|
5208
5313
|
import path28 from "node:path";
|
|
5209
|
-
import
|
|
5314
|
+
import fs25 from "fs-extra";
|
|
5210
5315
|
var META_FILENAME = "cache-meta.json";
|
|
5211
5316
|
async function readCacheMeta(cacheDir) {
|
|
5212
5317
|
const metaPath = path28.join(cacheDir, META_FILENAME);
|
|
5213
5318
|
try {
|
|
5214
|
-
const raw = await
|
|
5319
|
+
const raw = await fs25.readFile(metaPath, "utf8");
|
|
5215
5320
|
return JSON.parse(raw);
|
|
5216
5321
|
} catch {
|
|
5217
5322
|
return {};
|
|
5218
5323
|
}
|
|
5219
5324
|
}
|
|
5220
5325
|
async function writeCacheMeta(cacheDir, meta) {
|
|
5221
|
-
await
|
|
5326
|
+
await fs25.ensureDir(cacheDir);
|
|
5222
5327
|
const metaPath = path28.join(cacheDir, META_FILENAME);
|
|
5223
|
-
await
|
|
5328
|
+
await fs25.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
5224
5329
|
}
|
|
5225
5330
|
|
|
5226
5331
|
// src/refs/fetch-refs.ts
|
|
@@ -5241,7 +5346,7 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
5241
5346
|
const existing = meta[url];
|
|
5242
5347
|
const headers = {};
|
|
5243
5348
|
const safeFile = existing?.file && existing.file === path29.basename(existing.file);
|
|
5244
|
-
const cachedFileExists = safeFile && await
|
|
5349
|
+
const cachedFileExists = safeFile && await fs26.pathExists(path29.join(cacheDir, existing.file));
|
|
5245
5350
|
if (cachedFileExists) {
|
|
5246
5351
|
if (existing?.etag) headers["If-None-Match"] = existing.etag;
|
|
5247
5352
|
if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
|
|
@@ -5252,8 +5357,8 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
5252
5357
|
if (!res.ok) return { result: "failed" };
|
|
5253
5358
|
const text = await res.text();
|
|
5254
5359
|
const file = `${urlToSlug(url)}.md`;
|
|
5255
|
-
await
|
|
5256
|
-
await
|
|
5360
|
+
await fs26.ensureDir(cacheDir);
|
|
5361
|
+
await fs26.writeFile(path29.join(cacheDir, file), text, "utf8");
|
|
5257
5362
|
meta[url] = {
|
|
5258
5363
|
url,
|
|
5259
5364
|
etag: res.headers.get("etag") ?? void 0,
|
|
@@ -5277,7 +5382,7 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
|
5277
5382
|
delete meta[url];
|
|
5278
5383
|
removed++;
|
|
5279
5384
|
const safeFile = typeof entry.file === "string" && entry.file === path29.basename(entry.file);
|
|
5280
|
-
if (safeFile) await
|
|
5385
|
+
if (safeFile) await fs26.remove(path29.join(cacheDir, entry.file)).catch(() => {
|
|
5281
5386
|
});
|
|
5282
5387
|
}
|
|
5283
5388
|
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
@@ -5351,13 +5456,6 @@ function dryRunPrefix(dryRun, text) {
|
|
|
5351
5456
|
|
|
5352
5457
|
// src/commands/apply.ts
|
|
5353
5458
|
var STALE_CACHE_THRESHOLD_DAYS = 7;
|
|
5354
|
-
async function cacheHasItems() {
|
|
5355
|
-
const data = await readJson(path30.join(getCacheDir(), "manifest.json"));
|
|
5356
|
-
return Array.isArray(data?.items) && data.items.length > 0;
|
|
5357
|
-
}
|
|
5358
|
-
function installableRecommendedItems(recommended) {
|
|
5359
|
-
return recommended.filter((item) => item.install !== false);
|
|
5360
|
-
}
|
|
5361
5459
|
async function runApply(options) {
|
|
5362
5460
|
const mode = resolveWriteMode(options, "explicit-choice");
|
|
5363
5461
|
if (mode.kind === "unspecified") {
|
|
@@ -5374,11 +5472,11 @@ async function runApply(options) {
|
|
|
5374
5472
|
process.exitCode = 1;
|
|
5375
5473
|
return;
|
|
5376
5474
|
}
|
|
5377
|
-
const
|
|
5378
|
-
const recommended =
|
|
5475
|
+
const rec2 = await readJson(hausPath(root, "recommendation.json"));
|
|
5476
|
+
const recommended = rec2?.recommended ?? [];
|
|
5379
5477
|
const installableIds = new Set(installableRecommendedItems(recommended).map((i) => i.id));
|
|
5380
5478
|
const configIds = new Set(recommended.filter((i) => i.install === false).map((i) => i.id));
|
|
5381
|
-
const skippedIds = new Set((
|
|
5479
|
+
const skippedIds = new Set((rec2?.skipped ?? []).map((s) => s.id));
|
|
5382
5480
|
for (const id of explicitIds.filter((id2) => !installableIds.has(id2))) {
|
|
5383
5481
|
if (configIds.has(id)) {
|
|
5384
5482
|
warn(`--ids: "${id}" is a config item \u2014 install it with \`haus configs ${id}\`. Ignoring.`);
|
|
@@ -5401,12 +5499,12 @@ async function runApply(options) {
|
|
|
5401
5499
|
process.exitCode = 1;
|
|
5402
5500
|
return;
|
|
5403
5501
|
}
|
|
5404
|
-
const
|
|
5405
|
-
if (!
|
|
5502
|
+
const rec2 = await readJson(hausPath(root, "recommendation.json"));
|
|
5503
|
+
if (!rec2) {
|
|
5406
5504
|
log("No recommendation.json found \u2014 run `haus recommend` first. Writing core files only.");
|
|
5407
5505
|
selectedIds = [];
|
|
5408
5506
|
} else {
|
|
5409
|
-
const items = installableRecommendedItems(
|
|
5507
|
+
const items = installableRecommendedItems(rec2.recommended);
|
|
5410
5508
|
if (items.length === 0) {
|
|
5411
5509
|
log("Recommendation contains no catalog items. Writing core files only.");
|
|
5412
5510
|
selectedIds = [];
|
|
@@ -5426,14 +5524,31 @@ async function runApply(options) {
|
|
|
5426
5524
|
}
|
|
5427
5525
|
}
|
|
5428
5526
|
}
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5527
|
+
const formatRefusal = await catalogFormatRefusal(root);
|
|
5528
|
+
if (formatRefusal) {
|
|
5529
|
+
error(formatRefusal);
|
|
5530
|
+
process.exitCode = 1;
|
|
5531
|
+
return;
|
|
5532
|
+
}
|
|
5533
|
+
const rec = await readJson(hausPath(root, "recommendation.json"));
|
|
5534
|
+
const installableRecommended = installableRecommendedItems(rec?.recommended ?? []);
|
|
5535
|
+
const wantedIds = selectedIds ?? installableRecommended.map((item) => item.id);
|
|
5536
|
+
let catalogCtx = null;
|
|
5537
|
+
let resolvedWantedIds = wantedIds;
|
|
5538
|
+
if (wantedIds.length > 0) {
|
|
5539
|
+
catalogCtx = await loadCatalogContext(root);
|
|
5540
|
+
const prevLock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
5541
|
+
const migrations = findFormerIdMigrations(
|
|
5542
|
+
prevLock.filter((entry) => Boolean(entry.id)),
|
|
5543
|
+
catalogCtx.items
|
|
5544
|
+
);
|
|
5545
|
+
const migrationByOldId = new Map(migrations.map((m) => [m.oldId, m.newId]));
|
|
5546
|
+
resolvedWantedIds = wantedIds.map((id) => migrationByOldId.get(id) ?? id);
|
|
5547
|
+
const shortfall = await countInstallableItems(catalogCtx, resolvedWantedIds);
|
|
5548
|
+
const tier = `${catalogCtx.source}${catalogCtx.version ? ` (v${catalogCtx.version})` : ""}`;
|
|
5549
|
+
if (shortfall.installable === 0) {
|
|
5550
|
+
const message2 = "No catalog content found. Run `haus update` first.";
|
|
5551
|
+
if (!options.allowEmptyCache) {
|
|
5437
5552
|
if (isDryRun) {
|
|
5438
5553
|
warn(`Catalog cache is empty \u2014 dry-run will skip catalog items. ${message2}`);
|
|
5439
5554
|
} else {
|
|
@@ -5441,25 +5556,32 @@ async function runApply(options) {
|
|
|
5441
5556
|
process.exitCode = 1;
|
|
5442
5557
|
return;
|
|
5443
5558
|
}
|
|
5444
|
-
} else
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5559
|
+
} else {
|
|
5560
|
+
warn(
|
|
5561
|
+
`Serving catalog metadata from the ${tier} tier, which has no installable content. Core files will be written; all ${wantedIds.length} catalog item(s) will be skipped.`
|
|
5562
|
+
);
|
|
5563
|
+
}
|
|
5564
|
+
} else if (shortfall.missing.length > 0) {
|
|
5565
|
+
warn(
|
|
5566
|
+
`The ${tier} catalog tier is missing content for ${shortfall.missing.length} item(s): ${shortfall.missing.join(", ")}. They will be skipped.`
|
|
5567
|
+
);
|
|
5568
|
+
}
|
|
5569
|
+
if (catalogCtx.source === "cache") {
|
|
5570
|
+
const cacheAgeMs = await getCacheManifestAge();
|
|
5571
|
+
if (cacheAgeMs !== null) {
|
|
5572
|
+
const cacheAgeDays = Math.floor(cacheAgeMs / (1e3 * 60 * 60 * 24));
|
|
5573
|
+
if (cacheAgeDays >= STALE_CACHE_THRESHOLD_DAYS) {
|
|
5574
|
+
warn(
|
|
5575
|
+
`Catalog cache is ${cacheAgeDays} days old \u2014 run \`haus update\` for the latest content.`
|
|
5576
|
+
);
|
|
5453
5577
|
}
|
|
5454
5578
|
}
|
|
5579
|
+
} else if (catalogCtx.source === "bundled") {
|
|
5580
|
+
warn(
|
|
5581
|
+
`Catalog is being served from the snapshot bundled with this CLI${catalogCtx.version ? ` (v${catalogCtx.version})` : ""}, frozen at publish time. Run \`haus update\` when you have network access.`
|
|
5582
|
+
);
|
|
5455
5583
|
}
|
|
5456
5584
|
}
|
|
5457
|
-
const formatRefusal = await catalogFormatRefusal(root);
|
|
5458
|
-
if (formatRefusal) {
|
|
5459
|
-
error(formatRefusal);
|
|
5460
|
-
process.exitCode = 1;
|
|
5461
|
-
return;
|
|
5462
|
-
}
|
|
5463
5585
|
const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
|
|
5464
5586
|
refillConfig: options.refillConfig,
|
|
5465
5587
|
force: options.force,
|
|
@@ -5476,13 +5598,19 @@ async function runApply(options) {
|
|
|
5476
5598
|
const installedIds = new Set(lock.map((entry) => entry.id));
|
|
5477
5599
|
const allItems = await loadCatalog(root);
|
|
5478
5600
|
const installedItems = allItems.filter((item) => installedIds.has(item.id));
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
)
|
|
5601
|
+
if (isOffline()) {
|
|
5602
|
+
const refCount = collectLlmsTxtUrls(installedItems).length;
|
|
5603
|
+
if (refCount > 0)
|
|
5604
|
+
log(`Offline \u2014 skipping ${refCount} llms.txt reference fetch(es); using the cache as-is.`);
|
|
5605
|
+
} else {
|
|
5606
|
+
const summary = await fetchRefsForItems(installedItems, cacheDir);
|
|
5607
|
+
if (summary.fetched > 0)
|
|
5608
|
+
log(`Fetched ${summary.fetched} llms.txt reference(s) to ${cacheDir}`);
|
|
5609
|
+
if (summary.failed > 0)
|
|
5610
|
+
warn(
|
|
5611
|
+
`Failed to fetch ${summary.failed} llms.txt reference(s): ${summary.failedUrls.join(", ")}`
|
|
5612
|
+
);
|
|
5613
|
+
}
|
|
5486
5614
|
const pruned = await pruneOrphanedRefs(cacheDir, new Set(collectLlmsTxtUrls(installedItems)));
|
|
5487
5615
|
if (pruned > 0)
|
|
5488
5616
|
log(
|
|
@@ -5505,17 +5633,32 @@ async function runApply(options) {
|
|
|
5505
5633
|
}
|
|
5506
5634
|
if (isDryRun) {
|
|
5507
5635
|
log(`Dry-run complete \u2014 ${files.length} file(s) planned, none written. Run --write to apply.`);
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5636
|
+
return;
|
|
5637
|
+
}
|
|
5638
|
+
log("Applied files:");
|
|
5639
|
+
files.forEach((f) => log(`- ${displayPath(root, f)}`));
|
|
5640
|
+
if (resolvedWantedIds.length > 0) {
|
|
5641
|
+
const { installedCount: installedWantedCount, missingWantedIds } = await recountInstalledAgainstLock(root, resolvedWantedIds);
|
|
5642
|
+
if (missingWantedIds.length > 0) {
|
|
5643
|
+
const tier = catalogCtx ? `${catalogCtx.source}${catalogCtx.version ? ` (v${catalogCtx.version})` : ""}` : "unknown";
|
|
5644
|
+
log(
|
|
5645
|
+
`${installedWantedCount} of ${resolvedWantedIds.length} catalog items installed (${tier} tier).`
|
|
5646
|
+
);
|
|
5647
|
+
if (installedWantedCount === 0) {
|
|
5648
|
+
error(
|
|
5649
|
+
"No catalog items were installed \u2014 only core files were written. Run `haus update` with network access, or import a catalog bundle."
|
|
5650
|
+
);
|
|
5651
|
+
process.exitCode = 1;
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5511
5654
|
}
|
|
5512
5655
|
}
|
|
5513
5656
|
|
|
5514
5657
|
// src/commands/backups.ts
|
|
5658
|
+
import path30 from "node:path";
|
|
5659
|
+
import fs27 from "fs-extra";
|
|
5515
5660
|
init_logger();
|
|
5516
5661
|
init_paths();
|
|
5517
|
-
import path31 from "node:path";
|
|
5518
|
-
import fs26 from "fs-extra";
|
|
5519
5662
|
|
|
5520
5663
|
// src/utils/prompts.ts
|
|
5521
5664
|
import { stdin, stdout } from "node:process";
|
|
@@ -5578,25 +5721,16 @@ Enter a number (1-${choices.length})`, io);
|
|
|
5578
5721
|
}
|
|
5579
5722
|
|
|
5580
5723
|
// src/commands/backups.ts
|
|
5581
|
-
var LOCK_BACKUP_RE = /^haus\.lock\.\d+\.json$/;
|
|
5582
|
-
function classify(name) {
|
|
5583
|
-
if (LOCK_BACKUP_RE.test(name)) return "lock";
|
|
5584
|
-
if (name.startsWith("undo-")) return "undo";
|
|
5585
|
-
if (name.startsWith("prune-")) return "prune";
|
|
5586
|
-
if (name.startsWith("remove-")) return "remove";
|
|
5587
|
-
if (name.startsWith("migrate-")) return "migrate";
|
|
5588
|
-
return void 0;
|
|
5589
|
-
}
|
|
5590
5724
|
async function listBackups(root) {
|
|
5591
5725
|
const dir = hausPath(root, "backups");
|
|
5592
|
-
if (!await
|
|
5593
|
-
const names = await
|
|
5726
|
+
if (!await fs27.pathExists(dir)) return [];
|
|
5727
|
+
const names = await fs27.readdir(dir);
|
|
5594
5728
|
const entries = [];
|
|
5595
5729
|
for (const name of names) {
|
|
5596
|
-
const kind =
|
|
5730
|
+
const kind = classifyBackupName(name);
|
|
5597
5731
|
if (!kind) continue;
|
|
5598
|
-
const absPath =
|
|
5599
|
-
const stat = await
|
|
5732
|
+
const absPath = path30.join(dir, name);
|
|
5733
|
+
const stat = await fs27.lstat(absPath);
|
|
5600
5734
|
if (stat.isSymbolicLink()) continue;
|
|
5601
5735
|
const expectDirectory = kind !== "lock";
|
|
5602
5736
|
if (stat.isDirectory() !== expectDirectory) continue;
|
|
@@ -5635,13 +5769,13 @@ async function runList(root, json) {
|
|
|
5635
5769
|
}
|
|
5636
5770
|
}
|
|
5637
5771
|
async function isUnsafeWriteTarget(root, target) {
|
|
5638
|
-
const segments =
|
|
5772
|
+
const segments = path30.relative(root, target).split(path30.sep).filter(Boolean);
|
|
5639
5773
|
let current = root;
|
|
5640
5774
|
for (let i = 0; i < segments.length; i++) {
|
|
5641
|
-
current =
|
|
5775
|
+
current = path30.join(current, segments[i]);
|
|
5642
5776
|
const isLastSegment = i === segments.length - 1;
|
|
5643
5777
|
try {
|
|
5644
|
-
const stat = await
|
|
5778
|
+
const stat = await fs27.lstat(current);
|
|
5645
5779
|
if (isLastSegment) {
|
|
5646
5780
|
if (!stat.isFile()) return true;
|
|
5647
5781
|
} else if (!stat.isDirectory()) {
|
|
@@ -5656,7 +5790,7 @@ async function isUnsafeWriteTarget(root, target) {
|
|
|
5656
5790
|
}
|
|
5657
5791
|
async function lstatIfRegularFile(abs) {
|
|
5658
5792
|
try {
|
|
5659
|
-
const stat = await
|
|
5793
|
+
const stat = await fs27.lstat(abs);
|
|
5660
5794
|
return stat.isFile() ? stat : null;
|
|
5661
5795
|
} catch (err) {
|
|
5662
5796
|
if (err.code === "ENOENT") return null;
|
|
@@ -5671,8 +5805,8 @@ async function restoreLockBackup(root, entry, yes, json, dryRun) {
|
|
|
5671
5805
|
);
|
|
5672
5806
|
return { ok: false, restored: 0 };
|
|
5673
5807
|
}
|
|
5674
|
-
if (await
|
|
5675
|
-
const targetStat = await
|
|
5808
|
+
if (await fs27.pathExists(target)) {
|
|
5809
|
+
const targetStat = await fs27.stat(target);
|
|
5676
5810
|
if (targetStat.mtimeMs > entry.mtimeMs) {
|
|
5677
5811
|
warn(
|
|
5678
5812
|
`Current haus.lock.json (${new Date(targetStat.mtimeMs).toISOString()}) is newer than backup ${entry.id} (${new Date(entry.mtimeMs).toISOString()}) \u2014 it will be overwritten.`
|
|
@@ -5706,7 +5840,7 @@ async function restoreLockBackup(root, entry, yes, json, dryRun) {
|
|
|
5706
5840
|
error(`Refusing to restore: ${entry.id} or ${target} changed since being checked.`);
|
|
5707
5841
|
return { ok: false, restored: 0 };
|
|
5708
5842
|
}
|
|
5709
|
-
await
|
|
5843
|
+
await fs27.copy(entry.absPath, target, { overwrite: true });
|
|
5710
5844
|
if (!json) log(`Restored .haus-workflow/haus.lock.json from ${entry.id}.`);
|
|
5711
5845
|
return { ok: true, restored: 1 };
|
|
5712
5846
|
}
|
|
@@ -5714,10 +5848,10 @@ async function collectFilesRecursive(dir) {
|
|
|
5714
5848
|
const files = [];
|
|
5715
5849
|
const skippedLinks = [];
|
|
5716
5850
|
const skippedOther = [];
|
|
5717
|
-
const names = await
|
|
5851
|
+
const names = await fs27.readdir(dir);
|
|
5718
5852
|
for (const name of names) {
|
|
5719
|
-
const abs =
|
|
5720
|
-
const stat = await
|
|
5853
|
+
const abs = path30.join(dir, name);
|
|
5854
|
+
const stat = await fs27.lstat(abs);
|
|
5721
5855
|
if (stat.isSymbolicLink()) {
|
|
5722
5856
|
skippedLinks.push(abs);
|
|
5723
5857
|
continue;
|
|
@@ -5739,12 +5873,12 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
5739
5873
|
const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
|
|
5740
5874
|
if (skippedLinks.length > 0) {
|
|
5741
5875
|
warn(
|
|
5742
|
-
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) =>
|
|
5876
|
+
`Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) => path30.relative(entry.absPath, abs)).join(", ")}`
|
|
5743
5877
|
);
|
|
5744
5878
|
}
|
|
5745
5879
|
if (skippedOther.length > 0) {
|
|
5746
5880
|
warn(
|
|
5747
|
-
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) =>
|
|
5881
|
+
`Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) => path30.relative(entry.absPath, abs)).join(", ")}`
|
|
5748
5882
|
);
|
|
5749
5883
|
}
|
|
5750
5884
|
if (files.length === 0) {
|
|
@@ -5754,8 +5888,8 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
5754
5888
|
const safeFiles = [];
|
|
5755
5889
|
const unsafeTargets = [];
|
|
5756
5890
|
for (const abs of files) {
|
|
5757
|
-
const rel =
|
|
5758
|
-
const target =
|
|
5891
|
+
const rel = path30.relative(entry.absPath, abs);
|
|
5892
|
+
const target = path30.join(root, rel);
|
|
5759
5893
|
if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
|
|
5760
5894
|
else safeFiles.push(abs);
|
|
5761
5895
|
}
|
|
@@ -5772,16 +5906,16 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
5772
5906
|
const vanished = [];
|
|
5773
5907
|
const toRestore = [];
|
|
5774
5908
|
for (const abs of safeFiles) {
|
|
5775
|
-
const rel =
|
|
5909
|
+
const rel = path30.relative(entry.absPath, abs);
|
|
5776
5910
|
const backupFileStat = await lstatIfRegularFile(abs);
|
|
5777
5911
|
if (!backupFileStat) {
|
|
5778
5912
|
vanished.push(rel);
|
|
5779
5913
|
continue;
|
|
5780
5914
|
}
|
|
5781
5915
|
toRestore.push(abs);
|
|
5782
|
-
const target =
|
|
5783
|
-
if (await
|
|
5784
|
-
const targetStat = await
|
|
5916
|
+
const target = path30.join(root, rel);
|
|
5917
|
+
if (await fs27.pathExists(target)) {
|
|
5918
|
+
const targetStat = await fs27.stat(target);
|
|
5785
5919
|
if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
|
|
5786
5920
|
stale.push({
|
|
5787
5921
|
rel,
|
|
@@ -5809,7 +5943,7 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
5809
5943
|
);
|
|
5810
5944
|
}
|
|
5811
5945
|
if (dryRun) {
|
|
5812
|
-
const rels = toRestore.map((abs) =>
|
|
5946
|
+
const rels = toRestore.map((abs) => path30.relative(entry.absPath, abs));
|
|
5813
5947
|
if (!json) {
|
|
5814
5948
|
log(`[dry-run] Would restore ${rels.length} file(s) from ${entry.id}:`);
|
|
5815
5949
|
for (const rel of rels) log(` ${rel}`);
|
|
@@ -5835,14 +5969,14 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
5835
5969
|
let restoredCount = 0;
|
|
5836
5970
|
const skippedAtWrite = [];
|
|
5837
5971
|
for (const abs of toRestore) {
|
|
5838
|
-
const rel =
|
|
5839
|
-
const target =
|
|
5972
|
+
const rel = path30.relative(entry.absPath, abs);
|
|
5973
|
+
const target = path30.join(root, rel);
|
|
5840
5974
|
if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
|
|
5841
5975
|
skippedAtWrite.push(rel);
|
|
5842
5976
|
continue;
|
|
5843
5977
|
}
|
|
5844
|
-
await
|
|
5845
|
-
await
|
|
5978
|
+
await fs27.ensureDir(path30.dirname(target));
|
|
5979
|
+
await fs27.copy(abs, target, { overwrite: true });
|
|
5846
5980
|
restoredCount++;
|
|
5847
5981
|
}
|
|
5848
5982
|
if (skippedAtWrite.length > 0) {
|
|
@@ -5923,7 +6057,7 @@ Keeping ${keepCount}.`
|
|
|
5923
6057
|
const failed = [];
|
|
5924
6058
|
for (const entry of removeList) {
|
|
5925
6059
|
try {
|
|
5926
|
-
await
|
|
6060
|
+
await fs27.remove(entry.absPath);
|
|
5927
6061
|
removed.push(entry.id);
|
|
5928
6062
|
} catch (err) {
|
|
5929
6063
|
failed.push({ id: entry.id, message: err instanceof Error ? err.message : String(err) });
|
|
@@ -6017,26 +6151,272 @@ function auditForbiddenTags(items) {
|
|
|
6017
6151
|
if (forbidden.has(token)) failures.push(`${item.id} has unsupported tag ${token}`);
|
|
6018
6152
|
}
|
|
6019
6153
|
}
|
|
6020
|
-
return failures;
|
|
6021
|
-
}
|
|
6022
|
-
async function runCatalogAudit() {
|
|
6023
|
-
const items = await loadCatalog(process.cwd());
|
|
6024
|
-
const failures = auditForbiddenTags(items);
|
|
6025
|
-
if (failures.length) {
|
|
6026
|
-
failures.forEach((f) => error(f));
|
|
6027
|
-
process.exitCode = 1;
|
|
6028
|
-
return;
|
|
6029
|
-
}
|
|
6030
|
-
log("Catalog audit passed.");
|
|
6154
|
+
return failures;
|
|
6155
|
+
}
|
|
6156
|
+
async function runCatalogAudit() {
|
|
6157
|
+
const items = await loadCatalog(process.cwd());
|
|
6158
|
+
const failures = auditForbiddenTags(items);
|
|
6159
|
+
if (failures.length) {
|
|
6160
|
+
failures.forEach((f) => error(f));
|
|
6161
|
+
process.exitCode = 1;
|
|
6162
|
+
return;
|
|
6163
|
+
}
|
|
6164
|
+
log("Catalog audit passed.");
|
|
6165
|
+
}
|
|
6166
|
+
|
|
6167
|
+
// src/commands/catalog-bundle.ts
|
|
6168
|
+
init_constants();
|
|
6169
|
+
init_ingest_catalog();
|
|
6170
|
+
init_manifest_format();
|
|
6171
|
+
init_path_safety();
|
|
6172
|
+
import os2 from "node:os";
|
|
6173
|
+
import path31 from "node:path";
|
|
6174
|
+
import fs28 from "fs-extra";
|
|
6175
|
+
import * as tar from "tar";
|
|
6176
|
+
init_fs();
|
|
6177
|
+
init_logger();
|
|
6178
|
+
var BUNDLE_META_FILE = "bundle.json";
|
|
6179
|
+
var MANIFEST_FILE = "manifest.json";
|
|
6180
|
+
var SUPERPOWERS_SHARED_ITEM = {
|
|
6181
|
+
id: "haus.superpowers-shared",
|
|
6182
|
+
type: "skill",
|
|
6183
|
+
path: SUPERPOWERS_SHARED_CATALOG_REL
|
|
6184
|
+
};
|
|
6185
|
+
async function runBundleExport(options) {
|
|
6186
|
+
const out = options.out;
|
|
6187
|
+
if (!out) {
|
|
6188
|
+
error("Specify a destination with --out <file.tgz>");
|
|
6189
|
+
process.exitCode = 1;
|
|
6190
|
+
return;
|
|
6191
|
+
}
|
|
6192
|
+
const cacheDir = getCacheDir();
|
|
6193
|
+
const manifest = await readJson(path31.join(cacheDir, MANIFEST_FILE));
|
|
6194
|
+
if (!manifest?.items?.length) {
|
|
6195
|
+
error("Catalog cache is empty \u2014 run `haus update` on a connected machine first.");
|
|
6196
|
+
process.exitCode = 1;
|
|
6197
|
+
return;
|
|
6198
|
+
}
|
|
6199
|
+
const meta = {
|
|
6200
|
+
version: typeof manifest.version === "string" ? manifest.version : null,
|
|
6201
|
+
catalogRef: typeof manifest.catalogRef === "string" ? manifest.catalogRef : null,
|
|
6202
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6203
|
+
};
|
|
6204
|
+
await writeBundleAsTar(cacheDir, meta, path31.resolve(out));
|
|
6205
|
+
log(
|
|
6206
|
+
`Exported ${manifest.items.length} catalog item(s) to ${out}${meta.catalogRef ? ` (${meta.catalogRef})` : ""}.`
|
|
6207
|
+
);
|
|
6208
|
+
log("Move it to the disconnected machine and run `haus catalog bundle import <file>`.");
|
|
6209
|
+
}
|
|
6210
|
+
async function writeBundleAsTar(cacheDir, meta, dest) {
|
|
6211
|
+
const bundleMetaPath = path31.join(cacheDir, BUNDLE_META_FILE);
|
|
6212
|
+
const previousBundleMeta = await fs28.pathExists(bundleMetaPath) ? await fs28.readFile(bundleMetaPath, "utf8") : null;
|
|
6213
|
+
await writeJson(bundleMetaPath, meta);
|
|
6214
|
+
try {
|
|
6215
|
+
await fs28.ensureDir(path31.dirname(dest));
|
|
6216
|
+
await tar.create({ gzip: true, cwd: cacheDir, file: dest }, ["."]);
|
|
6217
|
+
} finally {
|
|
6218
|
+
if (previousBundleMeta === null) await fs28.remove(bundleMetaPath);
|
|
6219
|
+
else await fs28.writeFile(bundleMetaPath, previousBundleMeta, "utf8");
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
function isMarkdownFile(relOrAbsPath) {
|
|
6223
|
+
return relOrAbsPath.toLowerCase().endsWith(".md");
|
|
6224
|
+
}
|
|
6225
|
+
async function listAllFilesUnfiltered(root, current = root) {
|
|
6226
|
+
const entries = await fs28.readdir(current, { withFileTypes: true });
|
|
6227
|
+
const out = [];
|
|
6228
|
+
for (const entry of entries) {
|
|
6229
|
+
if (entry.isSymbolicLink()) continue;
|
|
6230
|
+
const abs = path31.join(current, entry.name);
|
|
6231
|
+
if (entry.isDirectory()) {
|
|
6232
|
+
out.push(...await listAllFilesUnfiltered(root, abs));
|
|
6233
|
+
} else if (entry.isFile()) {
|
|
6234
|
+
out.push(path31.relative(root, abs).split(path31.sep).join("/"));
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
return out;
|
|
6238
|
+
}
|
|
6239
|
+
async function validateStagedItem(item, absPath) {
|
|
6240
|
+
const stat = await fs28.stat(absPath).catch(() => null);
|
|
6241
|
+
if (!stat) return { ok: true };
|
|
6242
|
+
if (stat.isFile()) {
|
|
6243
|
+
const content2 = await fs28.readFile(absPath, "utf8");
|
|
6244
|
+
return validateCatalogItem(item, content2);
|
|
6245
|
+
}
|
|
6246
|
+
const files = await listAllFilesUnfiltered(absPath);
|
|
6247
|
+
for (const rel of files) {
|
|
6248
|
+
if (!isMarkdownFile(rel)) continue;
|
|
6249
|
+
const content2 = await fs28.readFile(path31.join(absPath, rel), "utf8");
|
|
6250
|
+
const verdict = validateCatalogItem(item, content2);
|
|
6251
|
+
if (!verdict.ok) return verdict;
|
|
6252
|
+
}
|
|
6253
|
+
return { ok: true };
|
|
6254
|
+
}
|
|
6255
|
+
async function validateStagedItems(staging, items) {
|
|
6256
|
+
let rejected = 0;
|
|
6257
|
+
const accepted = [];
|
|
6258
|
+
const contentlessIds = /* @__PURE__ */ new Set();
|
|
6259
|
+
const coveredPaths = [];
|
|
6260
|
+
for (const item of [...items, SUPERPOWERS_SHARED_ITEM]) {
|
|
6261
|
+
const isSynthetic = item === SUPERPOWERS_SHARED_ITEM;
|
|
6262
|
+
const contentPath = safeJoin(staging, item.path);
|
|
6263
|
+
if (!contentPath) {
|
|
6264
|
+
if (!isSynthetic) {
|
|
6265
|
+
warn(`Refusing item with an unsafe path: ${item.id} (${item.path})`);
|
|
6266
|
+
rejected += 1;
|
|
6267
|
+
}
|
|
6268
|
+
continue;
|
|
6269
|
+
}
|
|
6270
|
+
coveredPaths.push(item.path);
|
|
6271
|
+
if (!await fs28.pathExists(contentPath)) {
|
|
6272
|
+
if (!isSynthetic) {
|
|
6273
|
+
accepted.push(item.id);
|
|
6274
|
+
contentlessIds.add(item.id);
|
|
6275
|
+
}
|
|
6276
|
+
continue;
|
|
6277
|
+
}
|
|
6278
|
+
const verdict = await validateStagedItem(item, contentPath);
|
|
6279
|
+
if (!verdict.ok) {
|
|
6280
|
+
warn(`Rejected ${item.id}: ${verdict.reason}`);
|
|
6281
|
+
await fs28.remove(contentPath);
|
|
6282
|
+
if (!isSynthetic) rejected += 1;
|
|
6283
|
+
continue;
|
|
6284
|
+
}
|
|
6285
|
+
if (!isSynthetic) accepted.push(item.id);
|
|
6286
|
+
}
|
|
6287
|
+
const stagedFiles = await listAllFilesUnfiltered(staging);
|
|
6288
|
+
for (const rel of stagedFiles) {
|
|
6289
|
+
if (rel === MANIFEST_FILE || rel === BUNDLE_META_FILE) continue;
|
|
6290
|
+
const covered = coveredPaths.some((p) => rel === p || rel.startsWith(`${p}/`));
|
|
6291
|
+
if (!covered) {
|
|
6292
|
+
warn(`Refusing staged file outside any catalog item's path: ${rel}`);
|
|
6293
|
+
await fs28.remove(path31.join(staging, rel));
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
await pruneEmptyDirs(staging);
|
|
6297
|
+
return { accepted, rejected, contentlessIds };
|
|
6298
|
+
}
|
|
6299
|
+
async function pruneEmptyDirs(root, dir = root) {
|
|
6300
|
+
const entries = await fs28.readdir(dir, { withFileTypes: true });
|
|
6301
|
+
for (const entry of entries) {
|
|
6302
|
+
if (entry.isDirectory()) {
|
|
6303
|
+
await pruneEmptyDirs(root, path31.join(dir, entry.name));
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
if (dir !== root && (await fs28.readdir(dir)).length === 0) {
|
|
6307
|
+
await fs28.remove(dir);
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
async function stripSymlinks(dir) {
|
|
6311
|
+
const removed = [];
|
|
6312
|
+
const entries = await fs28.readdir(dir, { withFileTypes: true });
|
|
6313
|
+
for (const entry of entries) {
|
|
6314
|
+
const abs = path31.join(dir, entry.name);
|
|
6315
|
+
if (entry.isSymbolicLink()) {
|
|
6316
|
+
await fs28.remove(abs);
|
|
6317
|
+
removed.push(abs);
|
|
6318
|
+
continue;
|
|
6319
|
+
}
|
|
6320
|
+
if (entry.isDirectory()) {
|
|
6321
|
+
removed.push(...await stripSymlinks(abs));
|
|
6322
|
+
}
|
|
6323
|
+
}
|
|
6324
|
+
return removed;
|
|
6325
|
+
}
|
|
6326
|
+
async function runBundleImport(file, options = {}) {
|
|
6327
|
+
const dryRun = Boolean(options.dryRun);
|
|
6328
|
+
const archive = path31.resolve(file);
|
|
6329
|
+
if (!await fs28.pathExists(archive)) {
|
|
6330
|
+
error(`No such bundle: ${file}`);
|
|
6331
|
+
process.exitCode = 1;
|
|
6332
|
+
return;
|
|
6333
|
+
}
|
|
6334
|
+
const staging = await fs28.mkdtemp(path31.join(os2.tmpdir(), "haus-catalog-bundle-import-"));
|
|
6335
|
+
try {
|
|
6336
|
+
await tar.extract({
|
|
6337
|
+
file: archive,
|
|
6338
|
+
cwd: staging,
|
|
6339
|
+
// Refuse anything that would land outside the staging dir. node-tar's own
|
|
6340
|
+
// extraction already refuses `..`, absolute paths, and symlink escapes by
|
|
6341
|
+
// default (preservePaths is left unset) — this filter is a second, independent
|
|
6342
|
+
// check using the same path-safety module the network sync uses. Both this
|
|
6343
|
+
// filter's refusals and node-tar's own built-in ones are logged — a tampered
|
|
6344
|
+
// bundle's dropped entries must be visible, not silently absorbed.
|
|
6345
|
+
filter: (entryPath) => {
|
|
6346
|
+
const safe = isSafeCatalogPath(entryPath);
|
|
6347
|
+
if (!safe) warn(`Refusing unsafe tar entry path: ${entryPath}`);
|
|
6348
|
+
return safe;
|
|
6349
|
+
},
|
|
6350
|
+
onwarn: (code, message2) => warn(`tar: ${code}: ${message2}`)
|
|
6351
|
+
});
|
|
6352
|
+
const strippedSymlinks = await stripSymlinks(staging);
|
|
6353
|
+
for (const abs of strippedSymlinks) {
|
|
6354
|
+
warn(`Refusing symlink in bundle: ${path31.relative(staging, abs)}`);
|
|
6355
|
+
}
|
|
6356
|
+
const manifest = await readJson(path31.join(staging, MANIFEST_FILE));
|
|
6357
|
+
if (!manifest?.items?.length) {
|
|
6358
|
+
error("Bundle contains no catalog manifest \u2014 refusing to import.");
|
|
6359
|
+
process.exitCode = 1;
|
|
6360
|
+
return;
|
|
6361
|
+
}
|
|
6362
|
+
assertSupportedManifestFormat(manifest.manifestFormat, "The imported catalog bundle");
|
|
6363
|
+
const { accepted, rejected, contentlessIds } = await validateStagedItems(
|
|
6364
|
+
staging,
|
|
6365
|
+
manifest.items
|
|
6366
|
+
);
|
|
6367
|
+
const meta = await readJson(path31.join(staging, BUNDLE_META_FILE));
|
|
6368
|
+
const label = meta?.catalogRef ?? (meta?.version ? `v${meta.version}` : "unknown ref");
|
|
6369
|
+
const installedCount = accepted.length;
|
|
6370
|
+
if (dryRun) {
|
|
6371
|
+
log(
|
|
6372
|
+
dryRunPrefix(
|
|
6373
|
+
true,
|
|
6374
|
+
`would import ${installedCount} catalog item(s) from ${label} into ${getCacheDir()}`
|
|
6375
|
+
)
|
|
6376
|
+
);
|
|
6377
|
+
accepted.forEach(
|
|
6378
|
+
(id) => log(
|
|
6379
|
+
dryRunPrefix(
|
|
6380
|
+
true,
|
|
6381
|
+
contentlessIds.has(id) ? ` ${id} (declared only, no content in bundle)` : ` ${id}`
|
|
6382
|
+
)
|
|
6383
|
+
)
|
|
6384
|
+
);
|
|
6385
|
+
if (rejected > 0) {
|
|
6386
|
+
log(dryRunPrefix(true, `${rejected} item(s) would be rejected by validation (see above)`));
|
|
6387
|
+
}
|
|
6388
|
+
return;
|
|
6389
|
+
}
|
|
6390
|
+
await fs28.ensureDir(getCacheDir());
|
|
6391
|
+
for (const entry of await fs28.readdir(staging)) {
|
|
6392
|
+
await fs28.copy(path31.join(staging, entry), path31.join(getCacheDir(), entry), {
|
|
6393
|
+
overwrite: true,
|
|
6394
|
+
errorOnExist: false
|
|
6395
|
+
});
|
|
6396
|
+
}
|
|
6397
|
+
log(
|
|
6398
|
+
`Imported ${installedCount} catalog item(s) from ${label}.` + (rejected > 0 ? ` ${rejected} rejected by validation.` : "")
|
|
6399
|
+
);
|
|
6400
|
+
log("Run `haus apply --write` to install them.");
|
|
6401
|
+
} catch (err) {
|
|
6402
|
+
if (err instanceof UnsupportedManifestFormatError) {
|
|
6403
|
+
error(err.message);
|
|
6404
|
+
process.exitCode = 1;
|
|
6405
|
+
return;
|
|
6406
|
+
}
|
|
6407
|
+
throw err;
|
|
6408
|
+
} finally {
|
|
6409
|
+
await fs28.remove(staging);
|
|
6410
|
+
}
|
|
6031
6411
|
}
|
|
6032
6412
|
|
|
6033
6413
|
// src/commands/doctor.ts
|
|
6034
6414
|
import path45 from "node:path";
|
|
6035
|
-
import
|
|
6415
|
+
import fs37 from "fs-extra";
|
|
6036
6416
|
init_manifest_format();
|
|
6037
6417
|
|
|
6038
6418
|
// src/claude/former-install-orphans.ts
|
|
6039
|
-
import
|
|
6419
|
+
import fs29 from "fs-extra";
|
|
6040
6420
|
init_path_safety();
|
|
6041
6421
|
init_paths();
|
|
6042
6422
|
var toPosix = (value) => value.replace(/\\/g, "/");
|
|
@@ -6060,7 +6440,7 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
|
|
|
6060
6440
|
const projectRel = `.claude/${relPath}`;
|
|
6061
6441
|
if (trackedPaths.has(projectRel) || reported.has(projectRel)) continue;
|
|
6062
6442
|
const abs = safeJoin(claudePath(root), relPath);
|
|
6063
|
-
if (abs === null || !await
|
|
6443
|
+
if (abs === null || !await fs29.pathExists(abs)) continue;
|
|
6064
6444
|
reported.add(projectRel);
|
|
6065
6445
|
orphans.push({ formerId: entry.formerId, currentId: entry.currentId, relPath: projectRel });
|
|
6066
6446
|
}
|
|
@@ -6071,7 +6451,7 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
|
|
|
6071
6451
|
init_paths();
|
|
6072
6452
|
import path32 from "node:path";
|
|
6073
6453
|
import fg4 from "fast-glob";
|
|
6074
|
-
import
|
|
6454
|
+
import fs30 from "fs-extra";
|
|
6075
6455
|
var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
|
|
6076
6456
|
function stripFencedCodeBlocks(text) {
|
|
6077
6457
|
return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
|
|
@@ -6079,15 +6459,15 @@ function stripFencedCodeBlocks(text) {
|
|
|
6079
6459
|
async function findUnresolvedSkillReferences(root, options = {}) {
|
|
6080
6460
|
const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
|
|
6081
6461
|
const skillsRoot = claudePath(root, "skills");
|
|
6082
|
-
if (!await
|
|
6083
|
-
const skillDirs = (await
|
|
6462
|
+
if (!await fs30.pathExists(skillsRoot)) return [];
|
|
6463
|
+
const skillDirs = (await fs30.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
|
|
6084
6464
|
const installedSkillNames = new Set(skillDirs);
|
|
6085
6465
|
const files = await fg4("*/**/*.md", { cwd: skillsRoot, onlyFiles: true, dot: true });
|
|
6086
6466
|
const issues = [];
|
|
6087
6467
|
for (const rel of files) {
|
|
6088
6468
|
if (rel.startsWith("shared/")) continue;
|
|
6089
6469
|
const abs = path32.join(skillsRoot, rel);
|
|
6090
|
-
const raw = await
|
|
6470
|
+
const raw = await fs30.readFile(abs, "utf8");
|
|
6091
6471
|
const lines = stripFencedCodeBlocks(raw).split("\n");
|
|
6092
6472
|
lines.forEach((line2, idx) => {
|
|
6093
6473
|
for (const match of line2.matchAll(REFERENCE_PATTERN)) {
|
|
@@ -6110,13 +6490,13 @@ async function findUnresolvedSkillReferences(root, options = {}) {
|
|
|
6110
6490
|
// src/decisions/doctor.ts
|
|
6111
6491
|
init_fs();
|
|
6112
6492
|
import path37 from "node:path";
|
|
6113
|
-
import
|
|
6493
|
+
import fs34 from "fs-extra";
|
|
6114
6494
|
|
|
6115
6495
|
// src/decisions/check.ts
|
|
6116
6496
|
init_fs();
|
|
6117
6497
|
init_paths();
|
|
6118
6498
|
import path36 from "node:path";
|
|
6119
|
-
import
|
|
6499
|
+
import fs33 from "fs-extra";
|
|
6120
6500
|
|
|
6121
6501
|
// src/decisions/dep-bump.ts
|
|
6122
6502
|
var DEP_SECTIONS = [
|
|
@@ -6309,12 +6689,12 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
6309
6689
|
|
|
6310
6690
|
// src/decisions/paths.ts
|
|
6311
6691
|
import path34 from "node:path";
|
|
6312
|
-
import
|
|
6692
|
+
import fs31 from "fs-extra";
|
|
6313
6693
|
async function resolveDecisionsDir(root) {
|
|
6314
6694
|
const primary = path34.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
6315
|
-
if (await
|
|
6695
|
+
if (await fs31.pathExists(primary)) return primary;
|
|
6316
6696
|
const legacy = path34.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
6317
|
-
if (await
|
|
6697
|
+
if (await fs31.pathExists(legacy)) return legacy;
|
|
6318
6698
|
return primary;
|
|
6319
6699
|
}
|
|
6320
6700
|
function relativeDecisionPath(decisionsDir, root) {
|
|
@@ -6324,7 +6704,7 @@ function relativeDecisionPath(decisionsDir, root) {
|
|
|
6324
6704
|
// src/decisions/validate.ts
|
|
6325
6705
|
init_fs();
|
|
6326
6706
|
import path35 from "node:path";
|
|
6327
|
-
import
|
|
6707
|
+
import fs32 from "fs-extra";
|
|
6328
6708
|
function validateDecisionContent(content2) {
|
|
6329
6709
|
const errors = [];
|
|
6330
6710
|
for (const section of DECISIONS_TRIGGERS.requiredSections) {
|
|
@@ -6354,7 +6734,7 @@ function decisionNumberFromFile(name) {
|
|
|
6354
6734
|
}
|
|
6355
6735
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
6356
6736
|
const readmePath = path35.join(decisionsDir, "README.md");
|
|
6357
|
-
if (!await
|
|
6737
|
+
if (!await fs32.pathExists(readmePath)) {
|
|
6358
6738
|
return ["docs/decisions/README.md is missing"];
|
|
6359
6739
|
}
|
|
6360
6740
|
const readme = await readText(readmePath) ?? "";
|
|
@@ -6526,8 +6906,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
6526
6906
|
changedFiles: stats.files
|
|
6527
6907
|
});
|
|
6528
6908
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
6529
|
-
const legacyExists = await
|
|
6530
|
-
if (legacyExists && !await
|
|
6909
|
+
const legacyExists = await fs33.pathExists(path36.join(root, legacyRel));
|
|
6910
|
+
if (legacyExists && !await fs33.pathExists(decisionsDir)) {
|
|
6531
6911
|
satisfactionErrors = await checkSatisfaction({
|
|
6532
6912
|
root,
|
|
6533
6913
|
decisionsRel: legacyRel,
|
|
@@ -6549,8 +6929,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
6549
6929
|
return { triggered: true, satisfied: false, reasons, securityTriggered };
|
|
6550
6930
|
}
|
|
6551
6931
|
async function listDecisionNumbers(decisionsDir) {
|
|
6552
|
-
if (!await
|
|
6553
|
-
const entries = await
|
|
6932
|
+
if (!await fs33.pathExists(decisionsDir)) return [];
|
|
6933
|
+
const entries = await fs33.readdir(decisionsDir);
|
|
6554
6934
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
6555
6935
|
}
|
|
6556
6936
|
|
|
@@ -6559,8 +6939,8 @@ async function auditDecisionsLayout(root) {
|
|
|
6559
6939
|
const findings = [];
|
|
6560
6940
|
const primary = path37.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
6561
6941
|
const legacy = path37.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
6562
|
-
const hasPrimary = await
|
|
6563
|
-
const hasLegacy = await
|
|
6942
|
+
const hasPrimary = await fs34.pathExists(primary);
|
|
6943
|
+
const hasLegacy = await fs34.pathExists(legacy);
|
|
6564
6944
|
if (!hasPrimary && hasLegacy) {
|
|
6565
6945
|
findings.push({
|
|
6566
6946
|
level: "advisory",
|
|
@@ -6574,7 +6954,7 @@ async function auditDecisionsLayout(root) {
|
|
|
6574
6954
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
6575
6955
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
6576
6956
|
const readmePath = path37.join(decisionsDir, "README.md");
|
|
6577
|
-
if (!await
|
|
6957
|
+
if (!await fs34.pathExists(readmePath)) {
|
|
6578
6958
|
findings.push({
|
|
6579
6959
|
level: "advisory",
|
|
6580
6960
|
line: `- DECISIONS: missing ${rel}/README.md index`,
|
|
@@ -6584,7 +6964,7 @@ async function auditDecisionsLayout(root) {
|
|
|
6584
6964
|
return findings;
|
|
6585
6965
|
}
|
|
6586
6966
|
const readme = await readText(readmePath);
|
|
6587
|
-
const entries = (await
|
|
6967
|
+
const entries = (await fs34.readdir(decisionsDir)).filter(
|
|
6588
6968
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
6589
6969
|
);
|
|
6590
6970
|
for (const file of entries) {
|
|
@@ -6633,7 +7013,7 @@ init_fs();
|
|
|
6633
7013
|
init_logger();
|
|
6634
7014
|
init_paths();
|
|
6635
7015
|
import path38 from "node:path";
|
|
6636
|
-
import
|
|
7016
|
+
import fs35 from "fs-extra";
|
|
6637
7017
|
function manifestPathIn(configDir) {
|
|
6638
7018
|
return path38.join(configDir, "haus", "install-manifest.json");
|
|
6639
7019
|
}
|
|
@@ -6663,9 +7043,9 @@ async function pruneVacatedDirs(root, vacatedFiles) {
|
|
|
6663
7043
|
async function destinationWritable(dir) {
|
|
6664
7044
|
let candidate = path38.resolve(dir);
|
|
6665
7045
|
for (; ; ) {
|
|
6666
|
-
if (await
|
|
7046
|
+
if (await fs35.pathExists(candidate)) {
|
|
6667
7047
|
try {
|
|
6668
|
-
await
|
|
7048
|
+
await fs35.access(candidate, fs35.constants.W_OK);
|
|
6669
7049
|
return true;
|
|
6670
7050
|
} catch (err) {
|
|
6671
7051
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -6697,7 +7077,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
6697
7077
|
if (!legacy || !Array.isArray(legacy.files)) {
|
|
6698
7078
|
return { ...base, reason: "no install manifest under ~/.claude" };
|
|
6699
7079
|
}
|
|
6700
|
-
if (await
|
|
7080
|
+
if (await fs35.pathExists(manifestPathIn(to))) {
|
|
6701
7081
|
return { ...base, reason: "an install manifest already exists in the resolved config dir" };
|
|
6702
7082
|
}
|
|
6703
7083
|
const writable = await destinationWritable(to);
|
|
@@ -6720,14 +7100,14 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
6720
7100
|
continue;
|
|
6721
7101
|
}
|
|
6722
7102
|
const newDest = path38.join(to, rel);
|
|
6723
|
-
if (!await
|
|
7103
|
+
if (!await fs35.pathExists(entry.destPath)) {
|
|
6724
7104
|
rewritten.push({ ...entry, destPath: newDest });
|
|
6725
7105
|
continue;
|
|
6726
7106
|
}
|
|
6727
|
-
if (await
|
|
7107
|
+
if (await fs35.pathExists(newDest)) {
|
|
6728
7108
|
if (await sameContent(entry.destPath, newDest)) {
|
|
6729
7109
|
if (!dryRun) {
|
|
6730
|
-
await
|
|
7110
|
+
await fs35.remove(entry.destPath);
|
|
6731
7111
|
vacated.push(entry.destPath);
|
|
6732
7112
|
}
|
|
6733
7113
|
moved.push(newDest);
|
|
@@ -6744,7 +7124,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
6744
7124
|
continue;
|
|
6745
7125
|
}
|
|
6746
7126
|
try {
|
|
6747
|
-
await
|
|
7127
|
+
await fs35.move(entry.destPath, newDest);
|
|
6748
7128
|
vacated.push(entry.destPath);
|
|
6749
7129
|
moved.push(newDest);
|
|
6750
7130
|
rewritten.push({ ...entry, destPath: newDest });
|
|
@@ -6769,7 +7149,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
6769
7149
|
}
|
|
6770
7150
|
try {
|
|
6771
7151
|
await writeJson(manifestPathIn(to), { ...legacy, files: rewritten });
|
|
6772
|
-
await
|
|
7152
|
+
await fs35.remove(legacyManifestPath);
|
|
6773
7153
|
vacated.push(legacyManifestPath);
|
|
6774
7154
|
} catch (err) {
|
|
6775
7155
|
return {
|
|
@@ -6815,7 +7195,7 @@ function printConfigDirMigration(result) {
|
|
|
6815
7195
|
|
|
6816
7196
|
// src/scanner/read-context.ts
|
|
6817
7197
|
import path44 from "node:path";
|
|
6818
|
-
import
|
|
7198
|
+
import fs36 from "fs-extra";
|
|
6819
7199
|
|
|
6820
7200
|
// src/commands/workspace/config.ts
|
|
6821
7201
|
init_fs();
|
|
@@ -7399,10 +7779,10 @@ async function isCacheFresh(root, cachePath) {
|
|
|
7399
7779
|
path44.join(root, WORKSPACE_FILE),
|
|
7400
7780
|
path44.join(root, REPOS_MANIFEST_FILE)
|
|
7401
7781
|
];
|
|
7402
|
-
const cacheStat = await
|
|
7782
|
+
const cacheStat = await fs36.stat(cachePath);
|
|
7403
7783
|
for (const markerPath of markerPaths) {
|
|
7404
|
-
if (!await
|
|
7405
|
-
const markerStat = await
|
|
7784
|
+
if (!await fs36.pathExists(markerPath)) continue;
|
|
7785
|
+
const markerStat = await fs36.stat(markerPath);
|
|
7406
7786
|
if (markerStat.mtimeMs > cacheStat.mtimeMs) return false;
|
|
7407
7787
|
}
|
|
7408
7788
|
return true;
|
|
@@ -7416,11 +7796,15 @@ async function readContextOrScan(root) {
|
|
|
7416
7796
|
}
|
|
7417
7797
|
|
|
7418
7798
|
// src/update/npm-version.ts
|
|
7799
|
+
init_offline();
|
|
7419
7800
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
7420
7801
|
function isTestMode2() {
|
|
7421
7802
|
return process.env["HAUS_TEST_MODE"] === "1" || process.env["NODE_ENV"] === "test";
|
|
7422
7803
|
}
|
|
7423
7804
|
async function fetchNpmVersionStatus(currentVersion) {
|
|
7805
|
+
if (isOffline()) {
|
|
7806
|
+
return { current: currentVersion, latest: null, updateAvailable: false };
|
|
7807
|
+
}
|
|
7424
7808
|
if (isTestMode2() && process.env["HAUS_SKIP_NPM_CHECK"]) {
|
|
7425
7809
|
return { current: currentVersion, latest: null, updateAvailable: false };
|
|
7426
7810
|
}
|
|
@@ -7559,7 +7943,7 @@ async function runDoctor(options) {
|
|
|
7559
7943
|
const resolved = path45.resolve(root, target);
|
|
7560
7944
|
const relFromRoot = path45.relative(root, resolved);
|
|
7561
7945
|
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path45.sep}`) || path45.isAbsolute(relFromRoot);
|
|
7562
|
-
if (escapesRoot || !await
|
|
7946
|
+
if (escapesRoot || !await fs37.pathExists(resolved)) {
|
|
7563
7947
|
flag(
|
|
7564
7948
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
7565
7949
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -7577,7 +7961,7 @@ async function runDoctor(options) {
|
|
|
7577
7961
|
);
|
|
7578
7962
|
}
|
|
7579
7963
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
7580
|
-
const workflowExists = await
|
|
7964
|
+
const workflowExists = await fs37.pathExists(workflowPath);
|
|
7581
7965
|
if (!workflowExists) {
|
|
7582
7966
|
flag(
|
|
7583
7967
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -7600,7 +7984,7 @@ async function runDoctor(options) {
|
|
|
7600
7984
|
"templates",
|
|
7601
7985
|
"agentic-workflow-standard.md"
|
|
7602
7986
|
);
|
|
7603
|
-
const templatePath = await
|
|
7987
|
+
const templatePath = await fs37.pathExists(cachePath) ? cachePath : bundledPath;
|
|
7604
7988
|
const templateContent = await readText(templatePath);
|
|
7605
7989
|
const verdict = checkManagedTamper(bodyContent, storedHashMatch?.[1], templateContent ?? null);
|
|
7606
7990
|
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
@@ -7628,7 +8012,7 @@ async function runDoctor(options) {
|
|
|
7628
8012
|
}
|
|
7629
8013
|
}
|
|
7630
8014
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
7631
|
-
const workflowConfigExists = await
|
|
8015
|
+
const workflowConfigExists = await fs37.pathExists(workflowConfigPath);
|
|
7632
8016
|
if (!workflowConfigExists) {
|
|
7633
8017
|
flag(
|
|
7634
8018
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -7636,7 +8020,7 @@ async function runDoctor(options) {
|
|
|
7636
8020
|
"haus apply --write"
|
|
7637
8021
|
);
|
|
7638
8022
|
} else {
|
|
7639
|
-
const cfg = await
|
|
8023
|
+
const cfg = await fs37.readFile(workflowConfigPath, "utf8");
|
|
7640
8024
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
7641
8025
|
if (unfilled > 0) {
|
|
7642
8026
|
suggest(
|
|
@@ -7665,12 +8049,12 @@ async function runDoctor(options) {
|
|
|
7665
8049
|
const trackedArtifacts = await listTrackedArtifactPaths(root);
|
|
7666
8050
|
if (trackedArtifacts.length > 0) {
|
|
7667
8051
|
flag(
|
|
7668
|
-
`- GITIGNORE: ${trackedArtifacts.length} machine-local
|
|
7669
|
-
`${trackedArtifacts.length} machine-local
|
|
8052
|
+
`- GITIGNORE: ${trackedArtifacts.length} machine-local artifact(s) still tracked in git (${trackedArtifacts.join(", ")})`,
|
|
8053
|
+
`${trackedArtifacts.length} machine-local artifact(s) are still tracked in git`,
|
|
7670
8054
|
`git rm --cached ${trackedArtifacts.join(" ")} (or run \`haus apply --write\` to do this and update .gitignore for you)`
|
|
7671
8055
|
);
|
|
7672
8056
|
} else {
|
|
7673
|
-
ok("- GITIGNORE: OK (no machine-local
|
|
8057
|
+
ok("- GITIGNORE: OK (no machine-local artifacts tracked in git)");
|
|
7674
8058
|
}
|
|
7675
8059
|
const gitignoredHausPaths = await findGitignoredHausPaths(root);
|
|
7676
8060
|
if (gitignoredHausPaths.length > 0) {
|
|
@@ -7690,8 +8074,10 @@ async function runDoctor(options) {
|
|
|
7690
8074
|
else suggest(finding.line, finding.sentence, finding.fix);
|
|
7691
8075
|
}
|
|
7692
8076
|
let catalogItems = null;
|
|
8077
|
+
let catalogCtx = null;
|
|
7693
8078
|
try {
|
|
7694
|
-
|
|
8079
|
+
catalogCtx = await loadCatalogContext(root);
|
|
8080
|
+
catalogItems = catalogCtx.items;
|
|
7695
8081
|
} catch (err) {
|
|
7696
8082
|
if (!(err instanceof UnsupportedManifestFormatError)) throw err;
|
|
7697
8083
|
flag(
|
|
@@ -7784,6 +8170,31 @@ async function runDoctor(options) {
|
|
|
7784
8170
|
);
|
|
7785
8171
|
}
|
|
7786
8172
|
}
|
|
8173
|
+
let catalogTier;
|
|
8174
|
+
let catalogVersion;
|
|
8175
|
+
let installableItems;
|
|
8176
|
+
if (catalogCtx !== null) {
|
|
8177
|
+
const { installable } = await countInstallableItems(catalogCtx);
|
|
8178
|
+
catalogTier = catalogCtx.source;
|
|
8179
|
+
catalogVersion = catalogCtx.version;
|
|
8180
|
+
installableItems = installable;
|
|
8181
|
+
const tierLabel = `${catalogTier}${catalogVersion ? ` (v${catalogVersion})` : ""}`;
|
|
8182
|
+
if (catalogTier === "bundled") {
|
|
8183
|
+
suggest(
|
|
8184
|
+
`- CATALOG TIER: ${tierLabel} \u2014 frozen at publish time, metadata only (${installableItems} installable item(s))`,
|
|
8185
|
+
`Catalog is being served from the snapshot bundled with this CLI (${tierLabel}), frozen at publish time and carrying metadata only \u2014 ${installableItems} of its items have installable content, so \`haus apply\` will write core files only`,
|
|
8186
|
+
"haus update (with network access), or `haus catalog bundle import` on a disconnected machine"
|
|
8187
|
+
);
|
|
8188
|
+
} else if (installableItems === 0) {
|
|
8189
|
+
suggest(
|
|
8190
|
+
`- CATALOG TIER: ${tierLabel} \u2014 0 installable items (\`haus apply\` will write core files only)`,
|
|
8191
|
+
`The ${catalogTier} catalog tier has 0 installable items`,
|
|
8192
|
+
"haus update (or `haus catalog bundle import` on a disconnected machine)"
|
|
8193
|
+
);
|
|
8194
|
+
} else {
|
|
8195
|
+
ok(`- CATALOG TIER: ${tierLabel} \u2014 ${installableItems} installable item(s)`);
|
|
8196
|
+
}
|
|
8197
|
+
}
|
|
7787
8198
|
const cacheAgeMs = await getCacheManifestAge();
|
|
7788
8199
|
if (cacheAgeMs === null) {
|
|
7789
8200
|
suggest(
|
|
@@ -7877,7 +8288,7 @@ async function runDoctor(options) {
|
|
|
7877
8288
|
let onDisk = false;
|
|
7878
8289
|
for (const rel of row.paths ?? []) {
|
|
7879
8290
|
const abs = resolveContainedPath(root, rel);
|
|
7880
|
-
if (abs !== null && await
|
|
8291
|
+
if (abs !== null && await fs37.pathExists(abs)) {
|
|
7881
8292
|
onDisk = true;
|
|
7882
8293
|
break;
|
|
7883
8294
|
}
|
|
@@ -7909,7 +8320,21 @@ async function runDoctor(options) {
|
|
|
7909
8320
|
}
|
|
7910
8321
|
if (attention.length > 0) process.exitCode = 1;
|
|
7911
8322
|
if (options?.json) {
|
|
7912
|
-
log(
|
|
8323
|
+
log(
|
|
8324
|
+
JSON.stringify(
|
|
8325
|
+
{
|
|
8326
|
+
ok: attention.length === 0,
|
|
8327
|
+
attention,
|
|
8328
|
+
suggestions,
|
|
8329
|
+
detail,
|
|
8330
|
+
catalogTier,
|
|
8331
|
+
catalogVersion,
|
|
8332
|
+
installableItems
|
|
8333
|
+
},
|
|
8334
|
+
null,
|
|
8335
|
+
2
|
|
8336
|
+
)
|
|
8337
|
+
);
|
|
7913
8338
|
return;
|
|
7914
8339
|
}
|
|
7915
8340
|
if (attention.length === 0) {
|
|
@@ -7934,11 +8359,11 @@ import path48 from "node:path";
|
|
|
7934
8359
|
init_github_rate_limit();
|
|
7935
8360
|
|
|
7936
8361
|
// src/claude/refresh-project.ts
|
|
7937
|
-
import
|
|
8362
|
+
import fs38 from "fs-extra";
|
|
7938
8363
|
init_fs();
|
|
7939
8364
|
init_paths();
|
|
7940
8365
|
async function isHausProject(root) {
|
|
7941
|
-
if (await
|
|
8366
|
+
if (await fs38.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
7942
8367
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
7943
8368
|
if (result.status === "ok") return result.value._haus != null;
|
|
7944
8369
|
return false;
|
|
@@ -7955,7 +8380,7 @@ async function refreshProjectApply(root) {
|
|
|
7955
8380
|
// src/install/apply.ts
|
|
7956
8381
|
import crypto2 from "node:crypto";
|
|
7957
8382
|
import path46 from "node:path";
|
|
7958
|
-
import
|
|
8383
|
+
import fs39 from "fs-extra";
|
|
7959
8384
|
init_fs();
|
|
7960
8385
|
init_logger();
|
|
7961
8386
|
init_paths();
|
|
@@ -8044,7 +8469,7 @@ function hashContent(content2) {
|
|
|
8044
8469
|
}
|
|
8045
8470
|
function destFileMissing(destPath) {
|
|
8046
8471
|
try {
|
|
8047
|
-
return !
|
|
8472
|
+
return !fs39.statSync(destPath).isFile();
|
|
8048
8473
|
} catch {
|
|
8049
8474
|
return true;
|
|
8050
8475
|
}
|
|
@@ -8052,7 +8477,7 @@ function destFileMissing(destPath) {
|
|
|
8052
8477
|
function sourceVersion() {
|
|
8053
8478
|
try {
|
|
8054
8479
|
const pkgPath = path46.join(packageRoot(), "package.json");
|
|
8055
|
-
const pkg = JSON.parse(
|
|
8480
|
+
const pkg = JSON.parse(fs39.readFileSync(pkgPath, "utf8"));
|
|
8056
8481
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
8057
8482
|
} catch {
|
|
8058
8483
|
return "haus@0.0.0";
|
|
@@ -8064,10 +8489,10 @@ function globalSrcDir() {
|
|
|
8064
8489
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
8065
8490
|
const skillDir = path46.join(skillsDir, skillName);
|
|
8066
8491
|
const entries = [];
|
|
8067
|
-
for (const rel of
|
|
8492
|
+
for (const rel of fs39.readdirSync(skillDir, { recursive: true })) {
|
|
8068
8493
|
if (rel === "SKILL.md") continue;
|
|
8069
8494
|
const abs = path46.join(skillDir, rel);
|
|
8070
|
-
if (!
|
|
8495
|
+
if (!fs39.statSync(abs).isFile()) continue;
|
|
8071
8496
|
const posixRel = rel.split(path46.sep).join("/");
|
|
8072
8497
|
entries.push({
|
|
8073
8498
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
@@ -8080,10 +8505,10 @@ function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
|
8080
8505
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
8081
8506
|
const entries = [];
|
|
8082
8507
|
const skillsDir = path46.join(srcDir, "skills");
|
|
8083
|
-
if (
|
|
8084
|
-
for (const skillName of
|
|
8508
|
+
if (fs39.pathExistsSync(skillsDir)) {
|
|
8509
|
+
for (const skillName of fs39.readdirSync(skillsDir)) {
|
|
8085
8510
|
const skillFile = path46.join(skillsDir, skillName, "SKILL.md");
|
|
8086
|
-
if (
|
|
8511
|
+
if (fs39.pathExistsSync(skillFile)) {
|
|
8087
8512
|
entries.push({
|
|
8088
8513
|
stableId: `skill.${skillName}`,
|
|
8089
8514
|
srcRelPath: path46.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
@@ -8094,8 +8519,8 @@ function collectSourceFiles(srcDir, claudeDir) {
|
|
|
8094
8519
|
}
|
|
8095
8520
|
}
|
|
8096
8521
|
const commandsDir = path46.join(srcDir, "commands");
|
|
8097
|
-
if (
|
|
8098
|
-
for (const fileName of
|
|
8522
|
+
if (fs39.pathExistsSync(commandsDir)) {
|
|
8523
|
+
for (const fileName of fs39.readdirSync(commandsDir)) {
|
|
8099
8524
|
if (!fileName.endsWith(".md")) continue;
|
|
8100
8525
|
const commandName = fileName.slice(0, -".md".length);
|
|
8101
8526
|
entries.push({
|
|
@@ -8154,7 +8579,7 @@ async function applyInstall(options = {}) {
|
|
|
8154
8579
|
}
|
|
8155
8580
|
continue;
|
|
8156
8581
|
}
|
|
8157
|
-
const destExists =
|
|
8582
|
+
const destExists = fs39.pathExistsSync(entry.destPath);
|
|
8158
8583
|
if (destExists) {
|
|
8159
8584
|
const currentContent = await readText(entry.destPath);
|
|
8160
8585
|
if (currentContent !== void 0) {
|
|
@@ -8213,13 +8638,13 @@ async function applyInstall(options = {}) {
|
|
|
8213
8638
|
manifestFiles.push(entry);
|
|
8214
8639
|
continue;
|
|
8215
8640
|
}
|
|
8216
|
-
if (!
|
|
8641
|
+
if (!fs39.pathExistsSync(entry.destPath)) continue;
|
|
8217
8642
|
const content2 = await readText(entry.destPath);
|
|
8218
8643
|
if (!content2) continue;
|
|
8219
8644
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
8220
8645
|
const currentHash = hashContent(content2);
|
|
8221
8646
|
if (hasHeader && currentHash === entry.hash) {
|
|
8222
|
-
if (!dryRun) await
|
|
8647
|
+
if (!dryRun) await fs39.remove(entry.destPath);
|
|
8223
8648
|
result.deleted.push(entry.destPath);
|
|
8224
8649
|
} else {
|
|
8225
8650
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -8291,7 +8716,8 @@ function summarizeLockDiff(before, after) {
|
|
|
8291
8716
|
}
|
|
8292
8717
|
|
|
8293
8718
|
// src/update/lockfile.ts
|
|
8294
|
-
import {
|
|
8719
|
+
import { constants as fsConstants } from "node:fs";
|
|
8720
|
+
import { copyFile, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
8295
8721
|
import path47 from "node:path";
|
|
8296
8722
|
init_fs();
|
|
8297
8723
|
init_paths();
|
|
@@ -8325,16 +8751,30 @@ async function readLockRaw(root) {
|
|
|
8325
8751
|
return "[]";
|
|
8326
8752
|
}
|
|
8327
8753
|
}
|
|
8328
|
-
async function
|
|
8754
|
+
async function backupLockfile(root) {
|
|
8329
8755
|
const lockPath = hausPath(root, "haus.lock.json");
|
|
8330
|
-
const before = await readLockRaw(root);
|
|
8331
|
-
const lock = await readJson(lockPath) ?? [];
|
|
8332
8756
|
try {
|
|
8333
8757
|
const backupDir = hausPath(root, "backups");
|
|
8334
8758
|
await mkdir(backupDir, { recursive: true });
|
|
8335
|
-
|
|
8759
|
+
let stamp = Date.now();
|
|
8760
|
+
for (; ; ) {
|
|
8761
|
+
const dest = path47.join(backupDir, lockBackupFileName(stamp));
|
|
8762
|
+
try {
|
|
8763
|
+
await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
|
|
8764
|
+
return;
|
|
8765
|
+
} catch (err) {
|
|
8766
|
+
if (err.code !== "EEXIST") throw err;
|
|
8767
|
+
stamp += 1;
|
|
8768
|
+
}
|
|
8769
|
+
}
|
|
8336
8770
|
} catch {
|
|
8337
8771
|
}
|
|
8772
|
+
}
|
|
8773
|
+
async function applyLock(root) {
|
|
8774
|
+
const lockPath = hausPath(root, "haus.lock.json");
|
|
8775
|
+
const before = await readLockRaw(root);
|
|
8776
|
+
const lock = await readJson(lockPath) ?? [];
|
|
8777
|
+
await backupLockfile(root);
|
|
8338
8778
|
const enriched = await Promise.all(
|
|
8339
8779
|
lock.map(async (x) => {
|
|
8340
8780
|
const paths = Array.isArray(x.paths) ? x.paths.map(String) : [];
|
|
@@ -8357,12 +8797,7 @@ async function removeLockEntries(root, ids) {
|
|
|
8357
8797
|
const removed = [...wanted].filter((id) => present.has(id));
|
|
8358
8798
|
const missing = [...wanted].filter((id) => !present.has(id));
|
|
8359
8799
|
if (removed.length === 0) return { removed, missing, before, after: before };
|
|
8360
|
-
|
|
8361
|
-
const backupDir = hausPath(root, "backups");
|
|
8362
|
-
await mkdir(backupDir, { recursive: true });
|
|
8363
|
-
await copyFile(lockPath, path47.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
8364
|
-
} catch {
|
|
8365
|
-
}
|
|
8800
|
+
await backupLockfile(root);
|
|
8366
8801
|
const survivors = lock.filter((item) => !wanted.has(item.id));
|
|
8367
8802
|
await writeJson(lockPath, survivors);
|
|
8368
8803
|
return { removed, missing, before, after: `${JSON.stringify(survivors, null, 2)}
|
|
@@ -8804,7 +9239,7 @@ async function runClone(url, opts = {}) {
|
|
|
8804
9239
|
// src/install/scaffold.ts
|
|
8805
9240
|
init_logger();
|
|
8806
9241
|
import path50 from "node:path";
|
|
8807
|
-
import
|
|
9242
|
+
import fs40 from "fs-extra";
|
|
8808
9243
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
8809
9244
|
const scaffolded = [];
|
|
8810
9245
|
const skipped = [];
|
|
@@ -8817,7 +9252,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8817
9252
|
skipped.push(name);
|
|
8818
9253
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
8819
9254
|
};
|
|
8820
|
-
const realRoot = await
|
|
9255
|
+
const realRoot = await fs40.realpath(catalogRoot).catch(() => null);
|
|
8821
9256
|
for (const item of items) {
|
|
8822
9257
|
if (item.type !== "config") continue;
|
|
8823
9258
|
const sourcePath = path50.resolve(catalogRoot, item.path);
|
|
@@ -8827,7 +9262,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8827
9262
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
8828
9263
|
continue;
|
|
8829
9264
|
}
|
|
8830
|
-
const realSource = await
|
|
9265
|
+
const realSource = await fs40.realpath(sourcePath).catch(() => null);
|
|
8831
9266
|
if (!realRoot || !realSource) {
|
|
8832
9267
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
8833
9268
|
continue;
|
|
@@ -8837,7 +9272,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8837
9272
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
8838
9273
|
continue;
|
|
8839
9274
|
}
|
|
8840
|
-
const stat = await
|
|
9275
|
+
const stat = await fs40.lstat(sourcePath).catch(() => null);
|
|
8841
9276
|
if (!stat) {
|
|
8842
9277
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
8843
9278
|
continue;
|
|
@@ -8851,7 +9286,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8851
9286
|
const dest = path50.join(projectRoot, filename);
|
|
8852
9287
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
8853
9288
|
} else if (stat.isDirectory()) {
|
|
8854
|
-
const entries = await
|
|
9289
|
+
const entries = await fs40.readdir(sourcePath);
|
|
8855
9290
|
for (const entry of entries) {
|
|
8856
9291
|
const src = path50.join(sourcePath, entry);
|
|
8857
9292
|
const dest = path50.join(projectRoot, entry);
|
|
@@ -8863,7 +9298,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8863
9298
|
}
|
|
8864
9299
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
8865
9300
|
const destDisplay = path50.relative(projectRoot, dest) || path50.basename(dest);
|
|
8866
|
-
const srcStat = await
|
|
9301
|
+
const srcStat = await fs40.lstat(src).catch(() => null);
|
|
8867
9302
|
if (!srcStat) {
|
|
8868
9303
|
warn(`Skipping ${path50.basename(src)}: source not found`);
|
|
8869
9304
|
return "skipped-refused";
|
|
@@ -8872,7 +9307,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
8872
9307
|
warn(`Skipping ${path50.basename(src)}: source is a symlink`);
|
|
8873
9308
|
return "skipped-refused";
|
|
8874
9309
|
}
|
|
8875
|
-
const exists = await
|
|
9310
|
+
const exists = await fs40.pathExists(dest);
|
|
8876
9311
|
if (exists && !opts.force) {
|
|
8877
9312
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
8878
9313
|
return "skipped-exists";
|
|
@@ -8882,16 +9317,16 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
8882
9317
|
return "scaffolded";
|
|
8883
9318
|
}
|
|
8884
9319
|
if (exists && opts.force) {
|
|
8885
|
-
await
|
|
9320
|
+
await fs40.remove(dest);
|
|
8886
9321
|
}
|
|
8887
|
-
await
|
|
8888
|
-
await
|
|
9322
|
+
await fs40.ensureDir(path50.dirname(dest));
|
|
9323
|
+
await fs40.copy(src, dest, {
|
|
8889
9324
|
overwrite: true,
|
|
8890
9325
|
dereference: false,
|
|
8891
9326
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
8892
9327
|
filter: (s) => {
|
|
8893
9328
|
try {
|
|
8894
|
-
return !
|
|
9329
|
+
return !fs40.lstatSync(s).isSymbolicLink();
|
|
8895
9330
|
} catch {
|
|
8896
9331
|
return false;
|
|
8897
9332
|
}
|
|
@@ -9413,6 +9848,61 @@ async function runGuard(kind, _options) {
|
|
|
9413
9848
|
// src/claude/setup-core.ts
|
|
9414
9849
|
init_github_rate_limit();
|
|
9415
9850
|
|
|
9851
|
+
// src/claude/project-exclusions.ts
|
|
9852
|
+
import fs41 from "fs-extra";
|
|
9853
|
+
init_project_pin();
|
|
9854
|
+
init_fs();
|
|
9855
|
+
init_logger();
|
|
9856
|
+
init_paths();
|
|
9857
|
+
function normalizeExcludedIds(value) {
|
|
9858
|
+
if (!Array.isArray(value)) return [];
|
|
9859
|
+
return [
|
|
9860
|
+
...new Set(
|
|
9861
|
+
value.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
9862
|
+
)
|
|
9863
|
+
];
|
|
9864
|
+
}
|
|
9865
|
+
async function readProjectExcludedIds(root) {
|
|
9866
|
+
try {
|
|
9867
|
+
const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
|
|
9868
|
+
if (!data) return [];
|
|
9869
|
+
return normalizeExcludedIds(data.excludedIds);
|
|
9870
|
+
} catch (err) {
|
|
9871
|
+
if (err.code === "ENOENT") return [];
|
|
9872
|
+
warn(
|
|
9873
|
+
`${PROJECT_CONFIG_REL} could not be read for excludedIds (${err instanceof Error ? err.message : String(err)}) \u2014 treating as none`
|
|
9874
|
+
);
|
|
9875
|
+
return [];
|
|
9876
|
+
}
|
|
9877
|
+
}
|
|
9878
|
+
function isItemExcluded(item, excluded) {
|
|
9879
|
+
if (excluded.has(item.id)) return true;
|
|
9880
|
+
return normalizeFormerIds(item.formerIds).some((formerId) => excluded.has(formerId));
|
|
9881
|
+
}
|
|
9882
|
+
async function addProjectExcludedIds(root, ids) {
|
|
9883
|
+
const toAdd = [...new Set(ids.filter((id) => id.length > 0))];
|
|
9884
|
+
if (toAdd.length === 0) return;
|
|
9885
|
+
const file = hausPath(root, PROJECT_CONFIG_FILE);
|
|
9886
|
+
let existing = {};
|
|
9887
|
+
if (await fs41.pathExists(file)) {
|
|
9888
|
+
try {
|
|
9889
|
+
const parsed = JSON.parse(await fs41.readFile(file, "utf8"));
|
|
9890
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
9891
|
+
existing = { ...parsed };
|
|
9892
|
+
} else {
|
|
9893
|
+
warn(`${PROJECT_CONFIG_REL} is not a JSON object \u2014 rewriting with excludedIds only`);
|
|
9894
|
+
}
|
|
9895
|
+
} catch (err) {
|
|
9896
|
+
warn(
|
|
9897
|
+
`${PROJECT_CONFIG_REL} could not be parsed (${err instanceof Error ? err.message : String(err)}) \u2014 rewriting with excludedIds only`
|
|
9898
|
+
);
|
|
9899
|
+
}
|
|
9900
|
+
}
|
|
9901
|
+
const merged = normalizeExcludedIds(existing.excludedIds);
|
|
9902
|
+
const next = [.../* @__PURE__ */ new Set([...merged, ...toAdd])].sort();
|
|
9903
|
+
await writeJson(file, { ...existing, excludedIds: next });
|
|
9904
|
+
}
|
|
9905
|
+
|
|
9416
9906
|
// src/recommender/recommend.ts
|
|
9417
9907
|
init_fs();
|
|
9418
9908
|
init_paths();
|
|
@@ -9459,6 +9949,7 @@ async function readChangedFiles(root) {
|
|
|
9459
9949
|
async function recommend(root, context, opts = {}) {
|
|
9460
9950
|
const activePluginIds = opts.activePluginIds ?? [];
|
|
9461
9951
|
const items = await loadCatalog(root);
|
|
9952
|
+
const excludedIds = await readProjectExcludedIds(root);
|
|
9462
9953
|
const sources = buildSourcesReport(items);
|
|
9463
9954
|
const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
|
|
9464
9955
|
const scannerStacks = buildStackSet(context);
|
|
@@ -9668,6 +10159,7 @@ async function recommend(root, context, opts = {}) {
|
|
|
9668
10159
|
}
|
|
9669
10160
|
}
|
|
9670
10161
|
applyCoInstallSuppression(recommended, skipped);
|
|
10162
|
+
applyManualExcludes(excludedIds, items, recommended, skipped);
|
|
9671
10163
|
const includeWarnings = applyManualIncludes(opts.include ?? [], items, recommended, skipped);
|
|
9672
10164
|
const optInEligible = buildOptInEligible(items, skipped);
|
|
9673
10165
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -9696,6 +10188,29 @@ function buildStackSet(context) {
|
|
|
9696
10188
|
)
|
|
9697
10189
|
);
|
|
9698
10190
|
}
|
|
10191
|
+
function applyManualExcludes(excludedIds, items, recommended, skipped) {
|
|
10192
|
+
if (excludedIds.length === 0) return;
|
|
10193
|
+
const excluded = new Set(excludedIds);
|
|
10194
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
10195
|
+
for (let i = recommended.length - 1; i >= 0; i--) {
|
|
10196
|
+
const entry = recommended[i];
|
|
10197
|
+
const item = byId.get(entry.id);
|
|
10198
|
+
if (!item || !isItemExcluded(item, excluded)) continue;
|
|
10199
|
+
recommended.splice(i, 1);
|
|
10200
|
+
skipped.push({
|
|
10201
|
+
id: entry.id,
|
|
10202
|
+
reason: "manually excluded",
|
|
10203
|
+
skipReasons: [
|
|
10204
|
+
{
|
|
10205
|
+
code: "manual-exclude",
|
|
10206
|
+
message: "manually excluded via haus remove",
|
|
10207
|
+
signal: "selection:manual-exclude"
|
|
10208
|
+
}
|
|
10209
|
+
],
|
|
10210
|
+
...entry.gates ? { gates: entry.gates } : {}
|
|
10211
|
+
});
|
|
10212
|
+
}
|
|
10213
|
+
}
|
|
9699
10214
|
function applyCoInstallSuppression(recommended, skipped) {
|
|
9700
10215
|
const recommendedIds = new Set(recommended.map((r) => r.id));
|
|
9701
10216
|
const rules = [
|
|
@@ -10068,7 +10583,10 @@ async function runSetupCore(root, opts) {
|
|
|
10068
10583
|
recommendedCount: 0,
|
|
10069
10584
|
warnings,
|
|
10070
10585
|
hooksOk: false,
|
|
10071
|
-
written: []
|
|
10586
|
+
written: [],
|
|
10587
|
+
// Pre-existing zero-signal guard, not the #325 catalog-install honesty check —
|
|
10588
|
+
// deliberately left out of scope for `catalogInstallOk` (see its doc comment).
|
|
10589
|
+
catalogInstallOk: true
|
|
10072
10590
|
};
|
|
10073
10591
|
}
|
|
10074
10592
|
await writeJson(hausPath(root, "recommendation.json"), recommendation);
|
|
@@ -10097,7 +10615,8 @@ async function runSetupCore(root, opts) {
|
|
|
10097
10615
|
recommendedCount: recommendation.recommended.length,
|
|
10098
10616
|
warnings: warningLines,
|
|
10099
10617
|
hooksOk,
|
|
10100
|
-
written: []
|
|
10618
|
+
written: [],
|
|
10619
|
+
catalogInstallOk: true
|
|
10101
10620
|
};
|
|
10102
10621
|
const envelope = (hooksResult) => ({
|
|
10103
10622
|
scan: scanResult,
|
|
@@ -10137,12 +10656,66 @@ async function runSetupCore(root, opts) {
|
|
|
10137
10656
|
warn(formatGithubRateLimitMessage(sync.rateLimit));
|
|
10138
10657
|
}
|
|
10139
10658
|
}
|
|
10659
|
+
const isPreview = dryRun ?? false;
|
|
10660
|
+
const wantedIds = installableRecommendedItems(recommendation.recommended).map((item) => item.id);
|
|
10661
|
+
let catalogCtx = null;
|
|
10662
|
+
let resolvedWantedIds = wantedIds;
|
|
10663
|
+
if (wantedIds.length > 0) {
|
|
10664
|
+
catalogCtx = await loadCatalogContext(root);
|
|
10665
|
+
const prevLock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
10666
|
+
const migrations = findFormerIdMigrations(
|
|
10667
|
+
prevLock.filter((entry) => Boolean(entry.id)),
|
|
10668
|
+
catalogCtx.items
|
|
10669
|
+
);
|
|
10670
|
+
const migrationByOldId = new Map(migrations.map((m) => [m.oldId, m.newId]));
|
|
10671
|
+
resolvedWantedIds = wantedIds.map((id) => migrationByOldId.get(id) ?? id);
|
|
10672
|
+
const shortfall = await countInstallableItems(catalogCtx, resolvedWantedIds);
|
|
10673
|
+
const tier = `${catalogCtx.source}${catalogCtx.version ? ` (v${catalogCtx.version})` : ""}`;
|
|
10674
|
+
if (shortfall.installable === 0) {
|
|
10675
|
+
const message2 = "No catalog content found. Run `haus update` first.";
|
|
10676
|
+
if (!opts.allowEmptyCache) {
|
|
10677
|
+
if (isPreview) {
|
|
10678
|
+
warn(`Catalog cache is empty \u2014 dry-run will skip catalog items. ${message2}`);
|
|
10679
|
+
} else {
|
|
10680
|
+
error(message2);
|
|
10681
|
+
process.exitCode = 1;
|
|
10682
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10683
|
+
return { ...baseResult, catalogInstallOk: false };
|
|
10684
|
+
}
|
|
10685
|
+
} else {
|
|
10686
|
+
warn(
|
|
10687
|
+
`Serving catalog metadata from the ${tier} tier, which has no installable content. Core files will be written; all ${wantedIds.length} catalog item(s) will be skipped.`
|
|
10688
|
+
);
|
|
10689
|
+
}
|
|
10690
|
+
} else if (shortfall.missing.length > 0) {
|
|
10691
|
+
warn(
|
|
10692
|
+
`The ${tier} catalog tier is missing content for ${shortfall.missing.length} item(s): ${shortfall.missing.join(", ")}. They will be skipped.`
|
|
10693
|
+
);
|
|
10694
|
+
}
|
|
10695
|
+
}
|
|
10140
10696
|
const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
|
|
10141
10697
|
quiet: opts.quiet,
|
|
10142
10698
|
activePluginIds
|
|
10143
10699
|
});
|
|
10144
10700
|
say("Applied files:");
|
|
10145
10701
|
files.forEach((f) => say(`- ${displayPath(root, f)}`));
|
|
10702
|
+
let catalogInstallOk = true;
|
|
10703
|
+
if (!isPreview && resolvedWantedIds.length > 0) {
|
|
10704
|
+
const { installedCount: installedWantedCount, missingWantedIds } = await recountInstalledAgainstLock(root, resolvedWantedIds);
|
|
10705
|
+
if (missingWantedIds.length > 0) {
|
|
10706
|
+
const tier = catalogCtx ? `${catalogCtx.source}${catalogCtx.version ? ` (v${catalogCtx.version})` : ""}` : "unknown";
|
|
10707
|
+
say(
|
|
10708
|
+
`${installedWantedCount} of ${resolvedWantedIds.length} catalog items installed (${tier} tier).`
|
|
10709
|
+
);
|
|
10710
|
+
if (installedWantedCount === 0) {
|
|
10711
|
+
error(
|
|
10712
|
+
"No catalog items were installed \u2014 only core files were written. Run `haus update` with network access, or import a catalog bundle."
|
|
10713
|
+
);
|
|
10714
|
+
process.exitCode = 1;
|
|
10715
|
+
catalogInstallOk = false;
|
|
10716
|
+
}
|
|
10717
|
+
}
|
|
10718
|
+
}
|
|
10146
10719
|
const hooksAfter = await verifyProjectSettingsHooksContract(root);
|
|
10147
10720
|
const hooksOkAfter = hooksAfter.ok;
|
|
10148
10721
|
if (hooksAfter.skipped) {
|
|
@@ -10154,7 +10727,7 @@ async function runSetupCore(root, opts) {
|
|
|
10154
10727
|
say(`- HOOKS OK: ${hooksAfter.message}`);
|
|
10155
10728
|
}
|
|
10156
10729
|
emitJsonEnvelope(json, quiet, envelope(hooksAfter));
|
|
10157
|
-
return { ...baseResult, hooksOk: hooksOkAfter, written: files };
|
|
10730
|
+
return { ...baseResult, hooksOk: hooksOkAfter, written: files, catalogInstallOk };
|
|
10158
10731
|
}
|
|
10159
10732
|
|
|
10160
10733
|
// src/commands/init.ts
|
|
@@ -10174,6 +10747,7 @@ async function runInit(options) {
|
|
|
10174
10747
|
apply: !dryRun,
|
|
10175
10748
|
dryRun: false,
|
|
10176
10749
|
force: options.force,
|
|
10750
|
+
allowEmptyCache: options.allowEmptyCache,
|
|
10177
10751
|
confirm: () => confirm("Approve and write Claude files now?")
|
|
10178
10752
|
});
|
|
10179
10753
|
}
|
|
@@ -10304,7 +10878,7 @@ async function runInstall(options, deps = {}) {
|
|
|
10304
10878
|
|
|
10305
10879
|
// src/new-project/create-project.ts
|
|
10306
10880
|
import path53 from "node:path";
|
|
10307
|
-
import
|
|
10881
|
+
import fs42 from "fs-extra";
|
|
10308
10882
|
init_exec();
|
|
10309
10883
|
init_logger();
|
|
10310
10884
|
|
|
@@ -10458,7 +11032,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10458
11032
|
`Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
|
|
10459
11033
|
);
|
|
10460
11034
|
}
|
|
10461
|
-
if (await
|
|
11035
|
+
if (await fs42.pathExists(projectPath)) {
|
|
10462
11036
|
return fail("preflight", `Target ${projectPath} already exists \u2014 refusing to write into it.`);
|
|
10463
11037
|
}
|
|
10464
11038
|
const scaffoldArgs = stack.scaffold.args.map(
|
|
@@ -10536,15 +11110,15 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10536
11110
|
steps.push({ step: "scaffold", status: "done" });
|
|
10537
11111
|
phase = "git";
|
|
10538
11112
|
if (!opts.json) log("\u25B6 git: init/branch/commit");
|
|
10539
|
-
if (!await
|
|
11113
|
+
if (!await fs42.pathExists(path53.join(projectPath, ".git"))) {
|
|
10540
11114
|
const init = await deps.git(["init"], { cwd: projectPath });
|
|
10541
11115
|
if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
|
|
10542
11116
|
}
|
|
10543
11117
|
const branch = await deps.git(["branch", "-M", "main"], { cwd: projectPath });
|
|
10544
11118
|
if (branch.exitCode !== 0) return fail("git", `git branch -M main failed: ${branch.stderr}`);
|
|
10545
11119
|
const lockfile = opts.pm === "yarn" ? "yarn.lock" : opts.pm === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json";
|
|
10546
|
-
if (!await
|
|
10547
|
-
await
|
|
11120
|
+
if (!await fs42.pathExists(path53.join(projectPath, lockfile))) {
|
|
11121
|
+
await fs42.writeFile(path53.join(projectPath, lockfile), "");
|
|
10548
11122
|
}
|
|
10549
11123
|
const add = await deps.git(["add", "-A"], { cwd: projectPath });
|
|
10550
11124
|
if (add.exitCode !== 0) return fail("git", `git add failed: ${add.stderr}`);
|
|
@@ -10799,6 +11373,7 @@ async function runRecommend(options) {
|
|
|
10799
11373
|
}
|
|
10800
11374
|
|
|
10801
11375
|
// src/commands/remove.ts
|
|
11376
|
+
import path54 from "node:path";
|
|
10802
11377
|
init_fs();
|
|
10803
11378
|
init_logger();
|
|
10804
11379
|
init_paths();
|
|
@@ -10810,7 +11385,7 @@ async function runRemove(ids, options = {}) {
|
|
|
10810
11385
|
const dryRun = Boolean(options.dryRun);
|
|
10811
11386
|
const say = options.json ? () => {
|
|
10812
11387
|
} : log;
|
|
10813
|
-
const wanted = parseIdList(ids);
|
|
11388
|
+
const wanted = [...new Set(parseIdList(ids))];
|
|
10814
11389
|
if (wanted.length === 0) {
|
|
10815
11390
|
error("Specify at least one catalog item id \u2014 see `haus doctor` for what is installed.");
|
|
10816
11391
|
process.exitCode = 1;
|
|
@@ -10856,7 +11431,7 @@ async function runRemove(ids, options = {}) {
|
|
|
10856
11431
|
cleared.push({ id, existing: [] });
|
|
10857
11432
|
continue;
|
|
10858
11433
|
}
|
|
10859
|
-
const gate = await hashGateEntry(root, {
|
|
11434
|
+
const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
|
|
10860
11435
|
if (!gate.ok) {
|
|
10861
11436
|
const detail = gate.reason === "no-hash" ? "has no lock hash" : "was modified locally";
|
|
10862
11437
|
warn(
|
|
@@ -10902,7 +11477,8 @@ async function runRemove(ids, options = {}) {
|
|
|
10902
11477
|
}
|
|
10903
11478
|
}
|
|
10904
11479
|
const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
|
|
10905
|
-
|
|
11480
|
+
const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
|
|
11481
|
+
report.backupDir = backupRoot === null ? null : path54.basename(backupRoot);
|
|
10906
11482
|
for (const { id, existing } of cleared) {
|
|
10907
11483
|
await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
|
|
10908
11484
|
}
|
|
@@ -10910,6 +11486,10 @@ async function runRemove(ids, options = {}) {
|
|
|
10910
11486
|
root,
|
|
10911
11487
|
cleared.map((c) => c.id)
|
|
10912
11488
|
);
|
|
11489
|
+
await addProjectExcludedIds(
|
|
11490
|
+
root,
|
|
11491
|
+
cleared.map((c) => c.id)
|
|
11492
|
+
);
|
|
10913
11493
|
try {
|
|
10914
11494
|
const remaining = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
10915
11495
|
const remainingIds = new Set(remaining.map((e) => e.id));
|
|
@@ -10925,7 +11505,6 @@ async function runRemove(ids, options = {}) {
|
|
|
10925
11505
|
}
|
|
10926
11506
|
if (!options.json) {
|
|
10927
11507
|
log(`Removed ${report.removed.length} item(s). Restore with \`haus backups list\`.`);
|
|
10928
|
-
log("Note: a later `haus apply --write` reinstalls any of these still recommended.");
|
|
10929
11508
|
}
|
|
10930
11509
|
emitReport(report, options.json);
|
|
10931
11510
|
}
|
|
@@ -10987,8 +11566,8 @@ async function runScan(options) {
|
|
|
10987
11566
|
}
|
|
10988
11567
|
|
|
10989
11568
|
// src/commands/undo.ts
|
|
10990
|
-
import
|
|
10991
|
-
import
|
|
11569
|
+
import path55 from "node:path";
|
|
11570
|
+
import fs43 from "fs-extra";
|
|
10992
11571
|
|
|
10993
11572
|
// src/claude/managed-paths.ts
|
|
10994
11573
|
init_paths();
|
|
@@ -11007,7 +11586,7 @@ init_paths();
|
|
|
11007
11586
|
async function collectCoreManagedPaths(root) {
|
|
11008
11587
|
const existing = [];
|
|
11009
11588
|
for (const abs of coreManagedAbsolutePaths(root)) {
|
|
11010
|
-
if (await
|
|
11589
|
+
if (await fs43.pathExists(abs)) existing.push(abs);
|
|
11011
11590
|
}
|
|
11012
11591
|
return existing;
|
|
11013
11592
|
}
|
|
@@ -11016,108 +11595,101 @@ async function collectLockTrackedPaths(root) {
|
|
|
11016
11595
|
const removable = [];
|
|
11017
11596
|
const preserved = [];
|
|
11018
11597
|
for (const row of lock ?? []) {
|
|
11019
|
-
const
|
|
11020
|
-
const
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
|
|
11598
|
+
const id = row.id ?? "(unknown item)";
|
|
11599
|
+
const existing = await resolveExistingTrackedPaths(root, id, row.paths ?? [], log);
|
|
11600
|
+
if (existing.length === 0) continue;
|
|
11601
|
+
const gate = await hashGateEntry(root, { hash: row.hash }, existing);
|
|
11602
|
+
if (!gate.ok) {
|
|
11603
|
+
preserved.push(...existing.map((e) => e.abs));
|
|
11604
|
+
const detail = gate.reason === "no-hash" ? "has no lock hash" : "was modified locally";
|
|
11605
|
+
log(`Preserving ${id} (${detail}): ${existing.map((e) => e.rel).join(", ")}`);
|
|
11027
11606
|
continue;
|
|
11028
11607
|
}
|
|
11029
|
-
|
|
11030
|
-
if (currentHash === row.hash) {
|
|
11031
|
-
removable.push(...existingRel.map((rel) => path54.resolve(root, rel)));
|
|
11032
|
-
} else {
|
|
11033
|
-
preserved.push(...existingRel.map((rel) => path54.resolve(root, rel)));
|
|
11034
|
-
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
11035
|
-
}
|
|
11608
|
+
removable.push({ id, existing });
|
|
11036
11609
|
}
|
|
11037
11610
|
return { removable, preserved };
|
|
11038
11611
|
}
|
|
11039
11612
|
async function settingsHasHausContent(root) {
|
|
11040
11613
|
const settingsPath = claudePath(root, "settings.json");
|
|
11041
|
-
if (!await
|
|
11614
|
+
if (!await fs43.pathExists(settingsPath)) return false;
|
|
11042
11615
|
const settings = await readProjectSettings(root);
|
|
11043
11616
|
return settings._haus != null;
|
|
11044
11617
|
}
|
|
11045
11618
|
async function claudeMdHasHausBlock(root) {
|
|
11046
|
-
const filePath =
|
|
11047
|
-
if (!await
|
|
11048
|
-
const text = await
|
|
11619
|
+
const filePath = path55.join(root, "CLAUDE.md");
|
|
11620
|
+
if (!await fs43.pathExists(filePath)) return false;
|
|
11621
|
+
const text = await fs43.readFile(filePath, "utf8");
|
|
11049
11622
|
return text.includes(BLOCK_BEGIN);
|
|
11050
11623
|
}
|
|
11051
11624
|
async function stripProjectSettings(root) {
|
|
11052
11625
|
const settingsPath = claudePath(root, "settings.json");
|
|
11053
|
-
if (!await
|
|
11626
|
+
if (!await fs43.pathExists(settingsPath)) return false;
|
|
11054
11627
|
let settings = await readProjectSettings(root);
|
|
11055
11628
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
11056
11629
|
const hasContent = Object.keys(settings).length > 0;
|
|
11057
11630
|
if (hasContent) {
|
|
11058
11631
|
await writeProjectSettings(root, settings);
|
|
11059
|
-
log(`Stripped haus rules from ${
|
|
11632
|
+
log(`Stripped haus rules from ${path55.relative(root, settingsPath)} (user settings preserved).`);
|
|
11060
11633
|
return true;
|
|
11061
11634
|
}
|
|
11062
|
-
await
|
|
11063
|
-
log(`Removed ${
|
|
11635
|
+
await fs43.remove(settingsPath);
|
|
11636
|
+
log(`Removed ${path55.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
11064
11637
|
return true;
|
|
11065
11638
|
}
|
|
11066
11639
|
async function stripRootClaudeMd(root) {
|
|
11067
|
-
const filePath =
|
|
11068
|
-
if (!await
|
|
11069
|
-
const prev = await
|
|
11640
|
+
const filePath = path55.join(root, "CLAUDE.md");
|
|
11641
|
+
if (!await fs43.pathExists(filePath)) return false;
|
|
11642
|
+
const prev = await fs43.readFile(filePath, "utf8");
|
|
11070
11643
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
11071
11644
|
const next = stripHausBlock(prev);
|
|
11072
11645
|
if (next.length === 0) {
|
|
11073
|
-
await
|
|
11646
|
+
await fs43.remove(filePath);
|
|
11074
11647
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
11075
11648
|
} else {
|
|
11076
|
-
await
|
|
11649
|
+
await fs43.writeFile(filePath, next, "utf8");
|
|
11077
11650
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
11078
11651
|
}
|
|
11079
11652
|
return true;
|
|
11080
11653
|
}
|
|
11081
11654
|
async function pruneDirIfEmpty(dir) {
|
|
11082
|
-
if (!await
|
|
11655
|
+
if (!await fs43.pathExists(dir)) return;
|
|
11083
11656
|
try {
|
|
11084
|
-
const entries = await
|
|
11085
|
-
if (entries.length === 0) await
|
|
11657
|
+
const entries = await fs43.readdir(dir);
|
|
11658
|
+
if (entries.length === 0) await fs43.remove(dir);
|
|
11086
11659
|
} catch {
|
|
11087
11660
|
}
|
|
11088
11661
|
}
|
|
11089
11662
|
async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
11090
11663
|
if (managedAbsPaths.length === 0) return;
|
|
11091
11664
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
11092
|
-
const backupRoot = hausPath(root, "backups",
|
|
11665
|
+
const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
|
|
11093
11666
|
for (const abs of managedAbsPaths) {
|
|
11094
|
-
if (!await
|
|
11095
|
-
const rel =
|
|
11096
|
-
const backupPath =
|
|
11097
|
-
await
|
|
11098
|
-
await
|
|
11667
|
+
if (!await fs43.pathExists(abs)) continue;
|
|
11668
|
+
const rel = path55.relative(root, abs);
|
|
11669
|
+
const backupPath = path55.join(backupRoot, rel);
|
|
11670
|
+
await fs43.ensureDir(path55.dirname(backupPath));
|
|
11671
|
+
await fs43.copy(abs, backupPath);
|
|
11099
11672
|
}
|
|
11100
|
-
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${
|
|
11673
|
+
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path55.relative(root, backupRoot)}.`);
|
|
11101
11674
|
}
|
|
11102
11675
|
async function runUndo(options) {
|
|
11103
11676
|
const root = options.root ?? process.cwd();
|
|
11104
11677
|
const coreManaged = await collectCoreManagedPaths(root);
|
|
11105
11678
|
const { removable: lockRemovable, preserved: lockPreserved } = await collectLockTrackedPaths(root);
|
|
11106
|
-
const
|
|
11679
|
+
const lockAbs = lockRemovable.flatMap(({ existing }) => existing.map((e) => e.abs));
|
|
11680
|
+
const managed = [.../* @__PURE__ */ new Set([...coreManaged, ...lockAbs])];
|
|
11107
11681
|
const stripSettings = await settingsHasHausContent(root);
|
|
11108
11682
|
const stripClaudeMd = await claudeMdHasHausBlock(root);
|
|
11109
11683
|
if (managed.length === 0 && !stripSettings && !stripClaudeMd) {
|
|
11110
11684
|
log("Nothing to remove: no haus-managed files found in this directory.");
|
|
11111
11685
|
return;
|
|
11112
11686
|
}
|
|
11113
|
-
const relTargets = managed.map((p) =>
|
|
11687
|
+
const relTargets = managed.map((p) => path55.relative(root, p));
|
|
11114
11688
|
const summaryParts = [...relTargets];
|
|
11115
11689
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
11116
11690
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
11117
11691
|
if (lockPreserved.length > 0) {
|
|
11118
|
-
summaryParts.push(
|
|
11119
|
-
`(preserving ${lockPreserved.length} locally-modified catalog item file(s) \u2014 not shown above)`
|
|
11120
|
-
);
|
|
11692
|
+
summaryParts.push(`(preserving ${lockPreserved.length} catalog item file(s) \u2014 not shown above)`);
|
|
11121
11693
|
}
|
|
11122
11694
|
if (options.dryRun) {
|
|
11123
11695
|
log(`[dry-run] Would remove:
|
|
@@ -11137,10 +11709,13 @@ User-owned .claude/ files will be preserved.`
|
|
|
11137
11709
|
}
|
|
11138
11710
|
}
|
|
11139
11711
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
11140
|
-
for (const abs of
|
|
11141
|
-
if (!await
|
|
11142
|
-
await
|
|
11143
|
-
log(`Removed ${
|
|
11712
|
+
for (const abs of coreManaged) {
|
|
11713
|
+
if (!await fs43.pathExists(abs)) continue;
|
|
11714
|
+
await fs43.remove(abs);
|
|
11715
|
+
log(`Removed ${path55.relative(root, abs)}`);
|
|
11716
|
+
}
|
|
11717
|
+
for (const { id, existing } of lockRemovable) {
|
|
11718
|
+
await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
|
|
11144
11719
|
}
|
|
11145
11720
|
if (stripSettings) await stripProjectSettings(root);
|
|
11146
11721
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
@@ -11154,8 +11729,8 @@ init_fs();
|
|
|
11154
11729
|
init_logger();
|
|
11155
11730
|
init_paths();
|
|
11156
11731
|
import crypto3 from "node:crypto";
|
|
11157
|
-
import
|
|
11158
|
-
import
|
|
11732
|
+
import path56 from "node:path";
|
|
11733
|
+
import fs44 from "fs-extra";
|
|
11159
11734
|
async function runUninstall(options = {}) {
|
|
11160
11735
|
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
11161
11736
|
const manifest = await readManifest();
|
|
@@ -11192,7 +11767,7 @@ async function runUninstall(options = {}) {
|
|
|
11192
11767
|
);
|
|
11193
11768
|
}
|
|
11194
11769
|
for (const entry of manifest.files) {
|
|
11195
|
-
const exists =
|
|
11770
|
+
const exists = fs44.pathExistsSync(entry.destPath);
|
|
11196
11771
|
if (!exists) continue;
|
|
11197
11772
|
const content2 = await readText(entry.destPath);
|
|
11198
11773
|
if (content2 === void 0) continue;
|
|
@@ -11210,23 +11785,23 @@ async function runUninstall(options = {}) {
|
|
|
11210
11785
|
result.skipped.push(entry.destPath);
|
|
11211
11786
|
continue;
|
|
11212
11787
|
}
|
|
11213
|
-
await
|
|
11214
|
-
await pruneEmptyDir(
|
|
11788
|
+
await fs44.remove(entry.destPath);
|
|
11789
|
+
await pruneEmptyDir(path56.dirname(entry.destPath));
|
|
11215
11790
|
result.deleted.push(entry.destPath);
|
|
11216
11791
|
}
|
|
11217
11792
|
const settings = await readSettings();
|
|
11218
11793
|
const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
11219
11794
|
await writeSettings(stripped);
|
|
11220
11795
|
result.hooksStripped = true;
|
|
11221
|
-
const hausDir =
|
|
11796
|
+
const hausDir = path56.join(globalClaudeDir(), "haus");
|
|
11222
11797
|
const manifestPath2 = hausManifestPath();
|
|
11223
|
-
if (
|
|
11224
|
-
await
|
|
11798
|
+
if (fs44.pathExistsSync(manifestPath2)) {
|
|
11799
|
+
await fs44.remove(manifestPath2);
|
|
11225
11800
|
}
|
|
11226
|
-
if (
|
|
11801
|
+
if (fs44.pathExistsSync(hausDir)) {
|
|
11227
11802
|
try {
|
|
11228
|
-
const remaining = await
|
|
11229
|
-
if (remaining.length === 0) await
|
|
11803
|
+
const remaining = await fs44.readdir(hausDir);
|
|
11804
|
+
if (remaining.length === 0) await fs44.remove(hausDir);
|
|
11230
11805
|
} catch {
|
|
11231
11806
|
}
|
|
11232
11807
|
}
|
|
@@ -11295,15 +11870,15 @@ async function runUninstallCommand(options) {
|
|
|
11295
11870
|
}
|
|
11296
11871
|
|
|
11297
11872
|
// src/commands/validate-catalog.ts
|
|
11298
|
-
import
|
|
11873
|
+
import path58 from "node:path";
|
|
11299
11874
|
|
|
11300
11875
|
// src/catalog/validate-core.ts
|
|
11301
11876
|
init_forbidden_content();
|
|
11302
11877
|
init_manifest_item_fields();
|
|
11303
11878
|
init_path_safety();
|
|
11304
11879
|
init_validation_rules2();
|
|
11305
|
-
import
|
|
11306
|
-
import
|
|
11880
|
+
import fs45 from "node:fs";
|
|
11881
|
+
import path57 from "node:path";
|
|
11307
11882
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
11308
11883
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
11309
11884
|
function auditForbiddenStacks(items) {
|
|
@@ -11380,11 +11955,11 @@ function auditOptInMetadata(items) {
|
|
|
11380
11955
|
}
|
|
11381
11956
|
return failures;
|
|
11382
11957
|
}
|
|
11383
|
-
function auditManifestStructure(
|
|
11958
|
+
function auditManifestStructure(manifestVersion2, items) {
|
|
11384
11959
|
const failures = [];
|
|
11385
11960
|
const seenIds = /* @__PURE__ */ new Map();
|
|
11386
11961
|
const seenPaths = /* @__PURE__ */ new Map();
|
|
11387
|
-
if (typeof
|
|
11962
|
+
if (typeof manifestVersion2 !== "string" || !ITEM_SEMVER_RE.test(manifestVersion2)) {
|
|
11388
11963
|
failures.push('manifest.json: top-level "version" is not valid semver (expected X.Y.Z)');
|
|
11389
11964
|
}
|
|
11390
11965
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -11447,14 +12022,14 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
11447
12022
|
}
|
|
11448
12023
|
function readForAudit(absPath, label, failures) {
|
|
11449
12024
|
try {
|
|
11450
|
-
return
|
|
12025
|
+
return fs45.readFileSync(absPath, "utf8");
|
|
11451
12026
|
} catch (err) {
|
|
11452
12027
|
failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
11453
12028
|
return null;
|
|
11454
12029
|
}
|
|
11455
12030
|
}
|
|
11456
12031
|
function auditTemplateContent(manifestDir, absPath, itemId) {
|
|
11457
|
-
const rel =
|
|
12032
|
+
const rel = path57.relative(manifestDir, absPath);
|
|
11458
12033
|
const failures = [];
|
|
11459
12034
|
const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
|
|
11460
12035
|
if (text === null) return failures;
|
|
@@ -11476,55 +12051,55 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11476
12051
|
const failures = [];
|
|
11477
12052
|
for (const item of items) {
|
|
11478
12053
|
if (!item.path || !isSafeCatalogPath(item.path)) continue;
|
|
11479
|
-
const absPath =
|
|
12054
|
+
const absPath = path57.join(manifestDir, item.path);
|
|
11480
12055
|
if (item.type === "skill") {
|
|
11481
|
-
const skillMd =
|
|
11482
|
-
if (!
|
|
11483
|
-
failures.push(`${item.id}: missing ${
|
|
12056
|
+
const skillMd = path57.join(absPath, "SKILL.md");
|
|
12057
|
+
if (!fs45.existsSync(skillMd)) {
|
|
12058
|
+
failures.push(`${item.id}: missing ${path57.relative(manifestDir, skillMd)}`);
|
|
11484
12059
|
continue;
|
|
11485
12060
|
}
|
|
11486
12061
|
const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
|
|
11487
12062
|
if (text === null) continue;
|
|
11488
12063
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
|
|
11489
12064
|
failures.push(
|
|
11490
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${
|
|
12065
|
+
...auditForbiddenTagsInText(text, `${item.id}: ${path57.relative(manifestDir, skillMd)}`)
|
|
11491
12066
|
);
|
|
11492
12067
|
} else if (item.type === "agent") {
|
|
11493
|
-
if (!
|
|
12068
|
+
if (!fs45.existsSync(absPath)) {
|
|
11494
12069
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
11495
12070
|
continue;
|
|
11496
12071
|
}
|
|
11497
|
-
const rel =
|
|
12072
|
+
const rel = path57.relative(manifestDir, absPath);
|
|
11498
12073
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
11499
12074
|
if (text === null) continue;
|
|
11500
12075
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
11501
12076
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
11502
12077
|
} else if (item.type === "template") {
|
|
11503
|
-
if (!
|
|
12078
|
+
if (!fs45.existsSync(absPath)) {
|
|
11504
12079
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
11505
12080
|
continue;
|
|
11506
12081
|
}
|
|
11507
12082
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
11508
12083
|
} else if (item.type === "command") {
|
|
11509
|
-
if (!
|
|
12084
|
+
if (!fs45.existsSync(absPath)) {
|
|
11510
12085
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
11511
12086
|
continue;
|
|
11512
12087
|
}
|
|
11513
|
-
const rel =
|
|
12088
|
+
const rel = path57.relative(manifestDir, absPath);
|
|
11514
12089
|
const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
|
|
11515
12090
|
if (text === null) continue;
|
|
11516
12091
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
11517
12092
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
11518
12093
|
} else if (item.type === "config") {
|
|
11519
|
-
if (!
|
|
12094
|
+
if (!fs45.existsSync(absPath)) {
|
|
11520
12095
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
11521
12096
|
}
|
|
11522
12097
|
} else if (item.type === "rule") {
|
|
11523
|
-
if (!
|
|
12098
|
+
if (!fs45.existsSync(absPath)) {
|
|
11524
12099
|
failures.push(`${item.id}: missing rule file ${item.path}`);
|
|
11525
12100
|
}
|
|
11526
12101
|
} else if (item.type === "asset") {
|
|
11527
|
-
if (!
|
|
12102
|
+
if (!fs45.existsSync(absPath)) {
|
|
11528
12103
|
failures.push(`${item.id}: missing asset file ${item.path}`);
|
|
11529
12104
|
}
|
|
11530
12105
|
}
|
|
@@ -11559,7 +12134,7 @@ function resolveItemType(relPath, pathTypeMap) {
|
|
|
11559
12134
|
function walkAllFiles(dir, fn, failures) {
|
|
11560
12135
|
let entries;
|
|
11561
12136
|
try {
|
|
11562
|
-
entries =
|
|
12137
|
+
entries = fs45.readdirSync(dir, { withFileTypes: true });
|
|
11563
12138
|
} catch (err) {
|
|
11564
12139
|
failures.push(
|
|
11565
12140
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -11568,7 +12143,7 @@ function walkAllFiles(dir, fn, failures) {
|
|
|
11568
12143
|
}
|
|
11569
12144
|
for (const entry of entries) {
|
|
11570
12145
|
if (entry.name === ".DS_Store") continue;
|
|
11571
|
-
const full =
|
|
12146
|
+
const full = path57.join(dir, entry.name);
|
|
11572
12147
|
if (entry.isDirectory()) walkAllFiles(full, fn, failures);
|
|
11573
12148
|
else fn(full);
|
|
11574
12149
|
}
|
|
@@ -11592,12 +12167,12 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
11592
12167
|
const failures = [];
|
|
11593
12168
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
11594
12169
|
for (const dir of CONTENT_ROOTS) {
|
|
11595
|
-
const abs =
|
|
11596
|
-
if (!
|
|
12170
|
+
const abs = path57.join(manifestDir, dir);
|
|
12171
|
+
if (!fs45.existsSync(abs)) continue;
|
|
11597
12172
|
walkAllFiles(
|
|
11598
12173
|
abs,
|
|
11599
12174
|
(file) => {
|
|
11600
|
-
const relPath =
|
|
12175
|
+
const relPath = path57.relative(manifestDir, file).replace(/\\/g, "/");
|
|
11601
12176
|
if (!isPathClaimed(relPath, claimedPaths)) {
|
|
11602
12177
|
failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
|
|
11603
12178
|
}
|
|
@@ -11610,7 +12185,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
11610
12185
|
function walkMd(dir, fn, failures) {
|
|
11611
12186
|
let entries;
|
|
11612
12187
|
try {
|
|
11613
|
-
entries =
|
|
12188
|
+
entries = fs45.readdirSync(dir, { withFileTypes: true });
|
|
11614
12189
|
} catch (err) {
|
|
11615
12190
|
failures.push(
|
|
11616
12191
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -11618,7 +12193,7 @@ function walkMd(dir, fn, failures) {
|
|
|
11618
12193
|
return;
|
|
11619
12194
|
}
|
|
11620
12195
|
for (const entry of entries) {
|
|
11621
|
-
const full =
|
|
12196
|
+
const full = path57.join(dir, entry.name);
|
|
11622
12197
|
if (entry.isDirectory()) walkMd(full, fn, failures);
|
|
11623
12198
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
11624
12199
|
}
|
|
@@ -11628,13 +12203,13 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
11628
12203
|
const pathSourceMap = buildItemPathSourceMap(items);
|
|
11629
12204
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
11630
12205
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
11631
|
-
const abs =
|
|
11632
|
-
if (!
|
|
12206
|
+
const abs = path57.join(manifestDir, dir);
|
|
12207
|
+
if (!fs45.existsSync(abs)) continue;
|
|
11633
12208
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
11634
12209
|
walkMd(
|
|
11635
12210
|
abs,
|
|
11636
12211
|
(file) => {
|
|
11637
|
-
const rel =
|
|
12212
|
+
const rel = path57.relative(manifestDir, file);
|
|
11638
12213
|
const text = readForAudit(file, rel, failures);
|
|
11639
12214
|
if (text === null) return;
|
|
11640
12215
|
const norm = rel.replace(/\\/g, "/");
|
|
@@ -11657,9 +12232,9 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
11657
12232
|
}
|
|
11658
12233
|
return failures;
|
|
11659
12234
|
}
|
|
11660
|
-
function validateCatalogData(manifestDir,
|
|
12235
|
+
function validateCatalogData(manifestDir, manifestVersion2, items) {
|
|
11661
12236
|
const failures = [
|
|
11662
|
-
...auditManifestStructure(
|
|
12237
|
+
...auditManifestStructure(manifestVersion2, items),
|
|
11663
12238
|
...validateFormerIds(items),
|
|
11664
12239
|
...auditOptInMetadata(items),
|
|
11665
12240
|
...auditForbiddenStacks(items),
|
|
@@ -11682,8 +12257,8 @@ async function runValidateCatalog(manifestPath2) {
|
|
|
11682
12257
|
process.exitCode = 1;
|
|
11683
12258
|
return;
|
|
11684
12259
|
}
|
|
11685
|
-
const abs =
|
|
11686
|
-
const manifestDir =
|
|
12260
|
+
const abs = path58.resolve(process.cwd(), manifestPath2);
|
|
12261
|
+
const manifestDir = path58.dirname(abs);
|
|
11687
12262
|
const data = await readJson(abs);
|
|
11688
12263
|
if (!data?.items) {
|
|
11689
12264
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -11704,8 +12279,8 @@ init_logger();
|
|
|
11704
12279
|
|
|
11705
12280
|
// src/workspace/worktree/add.ts
|
|
11706
12281
|
init_fs();
|
|
11707
|
-
import
|
|
11708
|
-
import
|
|
12282
|
+
import path65 from "node:path";
|
|
12283
|
+
import fs49 from "fs-extra";
|
|
11709
12284
|
|
|
11710
12285
|
// src/workspace/worktree/git-worktree.ts
|
|
11711
12286
|
init_exec();
|
|
@@ -11825,13 +12400,13 @@ async function hasUnpushedWork(cwd) {
|
|
|
11825
12400
|
}
|
|
11826
12401
|
|
|
11827
12402
|
// src/workspace/worktree/hydrate.ts
|
|
11828
|
-
import
|
|
11829
|
-
import
|
|
12403
|
+
import path61 from "node:path";
|
|
12404
|
+
import fs47 from "fs-extra";
|
|
11830
12405
|
|
|
11831
12406
|
// src/workspace/worktree/cow-copy.ts
|
|
11832
12407
|
init_exec();
|
|
11833
|
-
import
|
|
11834
|
-
import
|
|
12408
|
+
import path59 from "node:path";
|
|
12409
|
+
import fs46 from "fs-extra";
|
|
11835
12410
|
async function getMacFilesystemType(dir) {
|
|
11836
12411
|
const df = await runCommand("df", ["-P", dir]);
|
|
11837
12412
|
if (df.exitCode !== 0) return void 0;
|
|
@@ -11862,14 +12437,14 @@ async function detectCowStrategy(dir) {
|
|
|
11862
12437
|
return "unknown-platform";
|
|
11863
12438
|
}
|
|
11864
12439
|
async function cowCopyDir(src, dest) {
|
|
11865
|
-
const srcExists = await
|
|
12440
|
+
const srcExists = await fs46.pathExists(src);
|
|
11866
12441
|
if (!srcExists)
|
|
11867
12442
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
11868
|
-
const strategy = await detectCowStrategy(
|
|
12443
|
+
const strategy = await detectCowStrategy(path59.dirname(dest));
|
|
11869
12444
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
11870
12445
|
return { strategy, attempted: false, ok: false };
|
|
11871
12446
|
}
|
|
11872
|
-
await
|
|
12447
|
+
await fs46.ensureDir(path59.dirname(dest));
|
|
11873
12448
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
11874
12449
|
const result = await runCommand("cp", args);
|
|
11875
12450
|
return {
|
|
@@ -11883,7 +12458,7 @@ async function cowCopyDir(src, dest) {
|
|
|
11883
12458
|
// src/workspace/worktree/install.ts
|
|
11884
12459
|
init_exec();
|
|
11885
12460
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
11886
|
-
import
|
|
12461
|
+
import path60 from "node:path";
|
|
11887
12462
|
import fg6 from "fast-glob";
|
|
11888
12463
|
function detectInstallPlan(signals) {
|
|
11889
12464
|
if (signals.hasYarnLock) {
|
|
@@ -11904,10 +12479,10 @@ function detectInstallPlan(signals) {
|
|
|
11904
12479
|
return { manager: "none" };
|
|
11905
12480
|
}
|
|
11906
12481
|
async function computeLockfileSignals(dir) {
|
|
11907
|
-
const hasYarnLock = existsSync3(
|
|
11908
|
-
const hasPnpmLock = existsSync3(
|
|
11909
|
-
const hasPackageLock = existsSync3(
|
|
11910
|
-
const hasComposerLock = existsSync3(
|
|
12482
|
+
const hasYarnLock = existsSync3(path60.join(dir, "yarn.lock"));
|
|
12483
|
+
const hasPnpmLock = existsSync3(path60.join(dir, "pnpm-lock.yaml"));
|
|
12484
|
+
const hasPackageLock = existsSync3(path60.join(dir, "package-lock.json"));
|
|
12485
|
+
const hasComposerLock = existsSync3(path60.join(dir, "composer.lock"));
|
|
11911
12486
|
const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
|
|
11912
12487
|
cwd: dir,
|
|
11913
12488
|
onlyFiles: true,
|
|
@@ -11921,7 +12496,7 @@ async function computeLockfileSignals(dir) {
|
|
|
11921
12496
|
});
|
|
11922
12497
|
const hasDotnetProject = dotnetFiles.length > 0;
|
|
11923
12498
|
let hasPackageManagerField = false;
|
|
11924
|
-
const pkgPath =
|
|
12499
|
+
const pkgPath = path60.join(dir, "package.json");
|
|
11925
12500
|
if (existsSync3(pkgPath)) {
|
|
11926
12501
|
try {
|
|
11927
12502
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
@@ -11964,7 +12539,7 @@ function missingYarnrcEnvVars(content2, env) {
|
|
|
11964
12539
|
return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
|
|
11965
12540
|
}
|
|
11966
12541
|
function missingYarnrcEnvVarsInDir(dir, env) {
|
|
11967
|
-
const yarnrcPath =
|
|
12542
|
+
const yarnrcPath = path60.join(dir, ".yarnrc.yml");
|
|
11968
12543
|
if (!existsSync3(yarnrcPath)) return [];
|
|
11969
12544
|
try {
|
|
11970
12545
|
return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
|
|
@@ -12054,13 +12629,13 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12054
12629
|
const start = Date.now();
|
|
12055
12630
|
const targets = [];
|
|
12056
12631
|
for (const target of HYDRATION_TARGETS) {
|
|
12057
|
-
const src =
|
|
12058
|
-
const dest =
|
|
12059
|
-
if (!await
|
|
12060
|
-
const destExists = await
|
|
12632
|
+
const src = path61.join(mainMemberPath, target);
|
|
12633
|
+
const dest = path61.join(worktreeMemberPath, target);
|
|
12634
|
+
if (!await fs47.pathExists(src)) continue;
|
|
12635
|
+
const destExists = await fs47.pathExists(dest);
|
|
12061
12636
|
if (!opts.force && destExists) continue;
|
|
12062
12637
|
if (opts.force && destExists) {
|
|
12063
|
-
await
|
|
12638
|
+
await fs47.remove(dest);
|
|
12064
12639
|
}
|
|
12065
12640
|
const copy = await cowCopyDir(src, dest);
|
|
12066
12641
|
targets.push({ target, copy });
|
|
@@ -12073,11 +12648,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12073
12648
|
|
|
12074
12649
|
// src/workspace/worktree/local-files.ts
|
|
12075
12650
|
init_exec();
|
|
12076
|
-
import
|
|
12077
|
-
import
|
|
12651
|
+
import path62 from "node:path";
|
|
12652
|
+
import fs48 from "fs-extra";
|
|
12078
12653
|
var MACHINE_LOCAL_FILES = [
|
|
12079
12654
|
"docker-compose.local.yml",
|
|
12080
|
-
|
|
12655
|
+
path62.join(".claude", "settings.local.json")
|
|
12081
12656
|
];
|
|
12082
12657
|
async function isTracked(repoRoot, relFile) {
|
|
12083
12658
|
const gitPath = relFile.replace(/\\/g, "/");
|
|
@@ -12087,12 +12662,12 @@ async function isTracked(repoRoot, relFile) {
|
|
|
12087
12662
|
async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
12088
12663
|
const copied = [];
|
|
12089
12664
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
12090
|
-
const src =
|
|
12091
|
-
if (!await
|
|
12665
|
+
const src = path62.join(mainMemberPath, rel);
|
|
12666
|
+
if (!await fs48.pathExists(src)) continue;
|
|
12092
12667
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
12093
|
-
const dest =
|
|
12094
|
-
await
|
|
12095
|
-
await
|
|
12668
|
+
const dest = path62.join(worktreeMemberPath, rel);
|
|
12669
|
+
await fs48.ensureDir(path62.dirname(dest));
|
|
12670
|
+
await fs48.copy(src, dest, { overwrite: true });
|
|
12096
12671
|
copied.push(rel);
|
|
12097
12672
|
}
|
|
12098
12673
|
return copied;
|
|
@@ -12100,15 +12675,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
12100
12675
|
|
|
12101
12676
|
// src/workspace/worktree/root.ts
|
|
12102
12677
|
import { existsSync as existsSync4 } from "node:fs";
|
|
12103
|
-
import
|
|
12678
|
+
import path63 from "node:path";
|
|
12104
12679
|
async function resolveWorkspaceForWorktree(start) {
|
|
12105
12680
|
const rootInfo = await resolveRoots(start);
|
|
12106
12681
|
if (!rootInfo.isGitRepo) {
|
|
12107
12682
|
return { ok: false, reason: "Not inside a git repository." };
|
|
12108
12683
|
}
|
|
12109
12684
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12110
|
-
const hasYaml = existsSync4(
|
|
12111
|
-
const hasManifest = existsSync4(
|
|
12685
|
+
const hasYaml = existsSync4(path63.join(workspaceRoot, WORKSPACE_FILE));
|
|
12686
|
+
const hasManifest = existsSync4(path63.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
12112
12687
|
if (!hasYaml && !hasManifest) {
|
|
12113
12688
|
return {
|
|
12114
12689
|
ok: false,
|
|
@@ -12134,10 +12709,10 @@ function selectMembers(members, only) {
|
|
|
12134
12709
|
|
|
12135
12710
|
// src/workspace/worktree/state.ts
|
|
12136
12711
|
init_fs();
|
|
12137
|
-
import
|
|
12712
|
+
import path64 from "node:path";
|
|
12138
12713
|
var WORKTREE_STATE_FILE = ".haus-worktree.json";
|
|
12139
12714
|
function worktreesDir(mainRoot) {
|
|
12140
|
-
return
|
|
12715
|
+
return path64.join(mainRoot, ".claude", "worktrees");
|
|
12141
12716
|
}
|
|
12142
12717
|
function isValidWorktreeSlug(slug) {
|
|
12143
12718
|
if (slug === "" || slug === "." || slug === "..") return false;
|
|
@@ -12150,15 +12725,15 @@ function worktreePath(mainRoot, slug) {
|
|
|
12150
12725
|
`Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
|
|
12151
12726
|
);
|
|
12152
12727
|
}
|
|
12153
|
-
return
|
|
12728
|
+
return path64.join(worktreesDir(mainRoot), slug);
|
|
12154
12729
|
}
|
|
12155
12730
|
async function readWorktreeState(worktreeRoot) {
|
|
12156
|
-
const state = await readJson(
|
|
12731
|
+
const state = await readJson(path64.join(worktreeRoot, WORKTREE_STATE_FILE));
|
|
12157
12732
|
if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
|
|
12158
12733
|
return state;
|
|
12159
12734
|
}
|
|
12160
12735
|
async function writeWorktreeState(worktreeRoot, state) {
|
|
12161
|
-
await writeJson(
|
|
12736
|
+
await writeJson(path64.join(worktreeRoot, WORKTREE_STATE_FILE), state);
|
|
12162
12737
|
}
|
|
12163
12738
|
|
|
12164
12739
|
// src/workspace/worktree/add.ts
|
|
@@ -12173,7 +12748,7 @@ async function runAdd(opts) {
|
|
|
12173
12748
|
}
|
|
12174
12749
|
const branch = opts.branch ?? opts.slug;
|
|
12175
12750
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
12176
|
-
if (await
|
|
12751
|
+
if (await fs49.pathExists(wsWorktreePath)) {
|
|
12177
12752
|
return {
|
|
12178
12753
|
ok: false,
|
|
12179
12754
|
error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate --materialize\` from inside it to check out any missing members (e.g. a worktree created by plain \`git worktree add\`), or \`remove\` first to start over.`
|
|
@@ -12192,7 +12767,7 @@ async function runAdd(opts) {
|
|
|
12192
12767
|
const memberResults = [];
|
|
12193
12768
|
for (const member of selected) {
|
|
12194
12769
|
const start = Date.now();
|
|
12195
|
-
const memberWorktreePath =
|
|
12770
|
+
const memberWorktreePath = path65.join(wsWorktreePath, member.folder);
|
|
12196
12771
|
const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
|
|
12197
12772
|
preferDefaultBranchFrom: true
|
|
12198
12773
|
});
|
|
@@ -12222,7 +12797,7 @@ async function runAdd(opts) {
|
|
|
12222
12797
|
await mapWithConcurrency(okResults, async (result) => {
|
|
12223
12798
|
const member = selected.find((m) => m.id === result.member);
|
|
12224
12799
|
if (!member) return;
|
|
12225
|
-
const memberWorktreePath =
|
|
12800
|
+
const memberWorktreePath = path65.join(wsWorktreePath, member.folder);
|
|
12226
12801
|
const hydrationStart = Date.now();
|
|
12227
12802
|
if (opts.hydrate !== false) {
|
|
12228
12803
|
result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
|
|
@@ -12289,25 +12864,25 @@ async function runDryRun(args) {
|
|
|
12289
12864
|
|
|
12290
12865
|
// src/workspace/worktree/doctor.ts
|
|
12291
12866
|
import { existsSync as existsSync6 } from "node:fs";
|
|
12292
|
-
import
|
|
12867
|
+
import path67 from "node:path";
|
|
12293
12868
|
|
|
12294
12869
|
// src/workspace/worktree/orphans.ts
|
|
12295
12870
|
import { existsSync as existsSync5 } from "node:fs";
|
|
12296
|
-
import
|
|
12871
|
+
import path66 from "node:path";
|
|
12297
12872
|
var WORKSPACE_REPO_ID = "(workspace)";
|
|
12298
12873
|
async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
12299
12874
|
const orphans = [];
|
|
12300
12875
|
for (const repo of repos) {
|
|
12301
12876
|
const entries = await listWorktrees(repo.absPath);
|
|
12302
12877
|
for (const entry of entries) {
|
|
12303
|
-
const entryPath =
|
|
12304
|
-
const marker = `${
|
|
12878
|
+
const entryPath = path66.normalize(entry.path);
|
|
12879
|
+
const marker = `${path66.sep}.claude${path66.sep}worktrees${path66.sep}`;
|
|
12305
12880
|
const idx = entryPath.indexOf(marker);
|
|
12306
12881
|
if (idx === -1) continue;
|
|
12307
12882
|
const rest = entryPath.slice(idx + marker.length);
|
|
12308
|
-
const slug = rest.split(
|
|
12883
|
+
const slug = rest.split(path66.sep)[0];
|
|
12309
12884
|
if (!slug) continue;
|
|
12310
|
-
if (existsSync5(
|
|
12885
|
+
if (existsSync5(path66.join(wsWorktreesDir, slug))) continue;
|
|
12311
12886
|
orphans.push({
|
|
12312
12887
|
repoId: repo.id,
|
|
12313
12888
|
ownerRepo: repo.absPath,
|
|
@@ -12322,7 +12897,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
|
|
|
12322
12897
|
// src/workspace/worktree/doctor.ts
|
|
12323
12898
|
var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
|
|
12324
12899
|
function looksHydrated(memberPath) {
|
|
12325
|
-
return HYDRATION_MARKERS.some((marker) => existsSync6(
|
|
12900
|
+
return HYDRATION_MARKERS.some((marker) => existsSync6(path67.join(memberPath, marker)));
|
|
12326
12901
|
}
|
|
12327
12902
|
async function runDoctor2() {
|
|
12328
12903
|
const rootInfo = await resolveRoots();
|
|
@@ -12338,7 +12913,7 @@ async function runDoctor2() {
|
|
|
12338
12913
|
};
|
|
12339
12914
|
}
|
|
12340
12915
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12341
|
-
const isWorkspace = existsSync6(
|
|
12916
|
+
const isWorkspace = existsSync6(path67.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path67.join(workspaceRoot, REPOS_MANIFEST_FILE));
|
|
12342
12917
|
if (!isWorkspace) {
|
|
12343
12918
|
return {
|
|
12344
12919
|
isWorkspace: false,
|
|
@@ -12353,7 +12928,7 @@ async function runDoctor2() {
|
|
|
12353
12928
|
};
|
|
12354
12929
|
}
|
|
12355
12930
|
const wsWorktreesDir = worktreesDir(workspaceRoot);
|
|
12356
|
-
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${
|
|
12931
|
+
const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path67.sep}`);
|
|
12357
12932
|
const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
|
|
12358
12933
|
const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
|
|
12359
12934
|
const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
|
|
@@ -12364,10 +12939,10 @@ async function runDoctor2() {
|
|
|
12364
12939
|
let slug;
|
|
12365
12940
|
if (inWorkspaceWorktree) {
|
|
12366
12941
|
const wtPath = rootInfo.repoRoot;
|
|
12367
|
-
slug = rootInfo.worktreeName ??
|
|
12942
|
+
slug = rootInfo.worktreeName ?? path67.basename(wtPath);
|
|
12368
12943
|
const state = await readWorktreeState(wtPath);
|
|
12369
12944
|
for (const member of members) {
|
|
12370
|
-
const memberPath =
|
|
12945
|
+
const memberPath = path67.join(wtPath, member.folder);
|
|
12371
12946
|
const materialized = existsSync6(memberPath);
|
|
12372
12947
|
const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
|
|
12373
12948
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
@@ -12407,7 +12982,7 @@ async function runDoctor2() {
|
|
|
12407
12982
|
wsWorktreesDir
|
|
12408
12983
|
);
|
|
12409
12984
|
const orphans = orphanScan.map(
|
|
12410
|
-
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${
|
|
12985
|
+
(o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path67.join(wsWorktreesDir, o.slug)} no longer exists.`
|
|
12411
12986
|
);
|
|
12412
12987
|
if (orphans.length > 0) {
|
|
12413
12988
|
problems.push(...orphans);
|
|
@@ -12428,7 +13003,7 @@ async function runDoctor2() {
|
|
|
12428
13003
|
// src/workspace/worktree/hydrate-command.ts
|
|
12429
13004
|
init_fs();
|
|
12430
13005
|
import { existsSync as existsSync7 } from "node:fs";
|
|
12431
|
-
import
|
|
13006
|
+
import path68 from "node:path";
|
|
12432
13007
|
async function runHydrateWorktree(opts) {
|
|
12433
13008
|
const rootInfo = await resolveRoots();
|
|
12434
13009
|
if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
|
|
@@ -12439,7 +13014,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12439
13014
|
}
|
|
12440
13015
|
const workspaceRoot = rootInfo.mainRoot;
|
|
12441
13016
|
const wtPath = rootInfo.repoRoot;
|
|
12442
|
-
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${
|
|
13017
|
+
if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path68.sep}`)) {
|
|
12443
13018
|
return {
|
|
12444
13019
|
ok: false,
|
|
12445
13020
|
error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
|
|
@@ -12451,13 +13026,13 @@ async function runHydrateWorktree(opts) {
|
|
|
12451
13026
|
return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
|
|
12452
13027
|
}
|
|
12453
13028
|
const state = await readWorktreeState(wtPath);
|
|
12454
|
-
const slug = state?.slug ??
|
|
13029
|
+
const slug = state?.slug ?? path68.basename(wtPath);
|
|
12455
13030
|
const detectedBranch = await currentBranch(wtPath);
|
|
12456
13031
|
const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
|
|
12457
13032
|
if (opts.dryRun) {
|
|
12458
13033
|
const results2 = await Promise.all(
|
|
12459
13034
|
selected.map(async (member) => {
|
|
12460
|
-
const memberPath =
|
|
13035
|
+
const memberPath = path68.join(wtPath, member.folder);
|
|
12461
13036
|
if (existsSync7(memberPath)) {
|
|
12462
13037
|
return { member: member.id, folder: member.folder, status: "planned" };
|
|
12463
13038
|
}
|
|
@@ -12478,7 +13053,7 @@ async function runHydrateWorktree(opts) {
|
|
|
12478
13053
|
const results = await mapWithConcurrency(
|
|
12479
13054
|
selected,
|
|
12480
13055
|
async (member) => {
|
|
12481
|
-
const memberPath =
|
|
13056
|
+
const memberPath = path68.join(wtPath, member.folder);
|
|
12482
13057
|
let branchAction;
|
|
12483
13058
|
if (!existsSync7(memberPath)) {
|
|
12484
13059
|
if (!opts.materialize) {
|
|
@@ -12541,26 +13116,26 @@ async function runHydrateWorktree(opts) {
|
|
|
12541
13116
|
}
|
|
12542
13117
|
|
|
12543
13118
|
// src/workspace/worktree/list.ts
|
|
12544
|
-
import
|
|
12545
|
-
import
|
|
13119
|
+
import path69 from "node:path";
|
|
13120
|
+
import fs50 from "fs-extra";
|
|
12546
13121
|
async function runList2() {
|
|
12547
13122
|
const resolved = await resolveWorkspaceForWorktree();
|
|
12548
13123
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
12549
13124
|
const { rootInfo, workspaceRoot } = resolved;
|
|
12550
13125
|
const dir = worktreesDir(workspaceRoot);
|
|
12551
|
-
if (!await
|
|
12552
|
-
const entries = await
|
|
13126
|
+
if (!await fs50.pathExists(dir)) return { ok: true, workspaceRoot, worktrees: [] };
|
|
13127
|
+
const entries = await fs50.readdir(dir, { withFileTypes: true });
|
|
12553
13128
|
const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
12554
13129
|
const configuredMembers = await readMembers(rootInfo);
|
|
12555
13130
|
const worktrees = [];
|
|
12556
13131
|
for (const slug of slugs) {
|
|
12557
|
-
const wtPath =
|
|
13132
|
+
const wtPath = path69.join(dir, slug);
|
|
12558
13133
|
const state = await readWorktreeState(wtPath);
|
|
12559
13134
|
const members = [];
|
|
12560
13135
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
12561
13136
|
for (const m of source) {
|
|
12562
|
-
const memberPath =
|
|
12563
|
-
const materialized = await
|
|
13137
|
+
const memberPath = path69.join(wtPath, m.folder);
|
|
13138
|
+
const materialized = await fs50.pathExists(memberPath);
|
|
12564
13139
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
12565
13140
|
members.push({
|
|
12566
13141
|
id: m.id,
|
|
@@ -12621,14 +13196,14 @@ async function runPrune2(opts) {
|
|
|
12621
13196
|
}
|
|
12622
13197
|
|
|
12623
13198
|
// src/workspace/worktree/remove.ts
|
|
12624
|
-
import
|
|
12625
|
-
import
|
|
13199
|
+
import path70 from "node:path";
|
|
13200
|
+
import fs51 from "fs-extra";
|
|
12626
13201
|
async function runRemove2(opts) {
|
|
12627
13202
|
const resolved = await resolveWorkspaceForWorktree();
|
|
12628
13203
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
12629
13204
|
const { rootInfo, workspaceRoot } = resolved;
|
|
12630
13205
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
12631
|
-
if (!await
|
|
13206
|
+
if (!await fs51.pathExists(wsWorktreePath)) {
|
|
12632
13207
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
12633
13208
|
}
|
|
12634
13209
|
const state = await readWorktreeState(wsWorktreePath);
|
|
@@ -12648,12 +13223,12 @@ async function runRemove2(opts) {
|
|
|
12648
13223
|
targets.push({
|
|
12649
13224
|
repo: configMember?.id ?? stateMember.id,
|
|
12650
13225
|
ownerRepo,
|
|
12651
|
-
path:
|
|
13226
|
+
path: path70.join(wsWorktreePath, stateMember.folder)
|
|
12652
13227
|
});
|
|
12653
13228
|
}
|
|
12654
13229
|
const existingTargets = [];
|
|
12655
13230
|
for (const target of targets) {
|
|
12656
|
-
if (await
|
|
13231
|
+
if (await fs51.pathExists(target.path)) existingTargets.push(target);
|
|
12657
13232
|
}
|
|
12658
13233
|
if (!opts.force) {
|
|
12659
13234
|
const blockers = [];
|
|
@@ -12709,8 +13284,8 @@ async function runRemove2(opts) {
|
|
|
12709
13284
|
}
|
|
12710
13285
|
await pruneWorktrees(workspaceRoot);
|
|
12711
13286
|
}
|
|
12712
|
-
if (wsRemoveOk && await
|
|
12713
|
-
await
|
|
13287
|
+
if (wsRemoveOk && await fs51.pathExists(wsWorktreePath)) {
|
|
13288
|
+
await fs51.remove(wsWorktreePath);
|
|
12714
13289
|
}
|
|
12715
13290
|
return { ok: failed.length === 0, dryRun: false, slug: opts.slug, removed, failed };
|
|
12716
13291
|
}
|
|
@@ -12986,7 +13561,7 @@ async function runWorktree(action, opts = {}) {
|
|
|
12986
13561
|
|
|
12987
13562
|
// src/commands/workspace.ts
|
|
12988
13563
|
import { existsSync as existsSync11, statSync as statSync3 } from "node:fs";
|
|
12989
|
-
import
|
|
13564
|
+
import path79 from "node:path";
|
|
12990
13565
|
init_fs();
|
|
12991
13566
|
init_logger();
|
|
12992
13567
|
|
|
@@ -13045,7 +13620,7 @@ ${summaries.map(
|
|
|
13045
13620
|
|
|
13046
13621
|
// src/commands/workspace/doctor.ts
|
|
13047
13622
|
import { existsSync as existsSync8 } from "node:fs";
|
|
13048
|
-
import
|
|
13623
|
+
import path72 from "node:path";
|
|
13049
13624
|
init_logger();
|
|
13050
13625
|
init_paths();
|
|
13051
13626
|
|
|
@@ -13053,14 +13628,14 @@ init_paths();
|
|
|
13053
13628
|
init_fs();
|
|
13054
13629
|
init_paths();
|
|
13055
13630
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
13056
|
-
import
|
|
13057
|
-
var
|
|
13631
|
+
import path71 from "node:path";
|
|
13632
|
+
var MANIFEST_FILE2 = "workspace.manifest.json";
|
|
13058
13633
|
function manifestPath(workspaceRoot) {
|
|
13059
|
-
return hausPath(workspaceRoot,
|
|
13634
|
+
return hausPath(workspaceRoot, MANIFEST_FILE2);
|
|
13060
13635
|
}
|
|
13061
13636
|
function hausVersion() {
|
|
13062
13637
|
try {
|
|
13063
|
-
const pkg = JSON.parse(readFileSync4(
|
|
13638
|
+
const pkg = JSON.parse(readFileSync4(path71.join(packageRoot(), "package.json"), "utf8"));
|
|
13064
13639
|
return pkg.version ?? "0.0.0";
|
|
13065
13640
|
} catch {
|
|
13066
13641
|
return "0.0.0";
|
|
@@ -13152,7 +13727,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
13152
13727
|
const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
|
|
13153
13728
|
const catalogRefByRepo = [];
|
|
13154
13729
|
for (const repo of config.repos) {
|
|
13155
|
-
const repoRoot =
|
|
13730
|
+
const repoRoot = path72.resolve(workspaceRoot, repo.path);
|
|
13156
13731
|
const entry = manifestByName.get(repo.name);
|
|
13157
13732
|
if (!entry) {
|
|
13158
13733
|
flag({
|
|
@@ -13225,7 +13800,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
|
|
|
13225
13800
|
});
|
|
13226
13801
|
continue;
|
|
13227
13802
|
}
|
|
13228
|
-
if (!existsSync8(
|
|
13803
|
+
if (!existsSync8(path72.join(workspaceRoot, entry.path))) {
|
|
13229
13804
|
flag({
|
|
13230
13805
|
repo: entry.repo,
|
|
13231
13806
|
kind: "missing-linked-context-copy",
|
|
@@ -13271,18 +13846,18 @@ function emit(args) {
|
|
|
13271
13846
|
init_logger();
|
|
13272
13847
|
|
|
13273
13848
|
// src/workspace/link-context/link.ts
|
|
13274
|
-
import
|
|
13275
|
-
import
|
|
13849
|
+
import path75 from "node:path";
|
|
13850
|
+
import fs54 from "fs-extra";
|
|
13276
13851
|
init_fs();
|
|
13277
13852
|
init_logger();
|
|
13278
13853
|
|
|
13279
13854
|
// src/workspace/link-context/plan.ts
|
|
13280
|
-
import
|
|
13281
|
-
import
|
|
13855
|
+
import path74 from "node:path";
|
|
13856
|
+
import fs53 from "fs-extra";
|
|
13282
13857
|
|
|
13283
13858
|
// src/workspace/link-context/scan-source.ts
|
|
13284
|
-
import
|
|
13285
|
-
import
|
|
13859
|
+
import path73 from "node:path";
|
|
13860
|
+
import fs52 from "fs-extra";
|
|
13286
13861
|
var ASSET_SUBDIR = {
|
|
13287
13862
|
skill: "skills",
|
|
13288
13863
|
agent: "agents",
|
|
@@ -13290,36 +13865,36 @@ var ASSET_SUBDIR = {
|
|
|
13290
13865
|
};
|
|
13291
13866
|
async function hasLinkableContext(member) {
|
|
13292
13867
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
13293
|
-
if (await
|
|
13868
|
+
if (await fs52.pathExists(path73.join(member.absPath, ".claude", subdir))) return true;
|
|
13294
13869
|
}
|
|
13295
13870
|
return false;
|
|
13296
13871
|
}
|
|
13297
13872
|
async function collectSkills(member) {
|
|
13298
|
-
const dir =
|
|
13299
|
-
if (!await
|
|
13300
|
-
const names = await
|
|
13873
|
+
const dir = path73.join(member.absPath, ".claude", "skills");
|
|
13874
|
+
if (!await fs52.pathExists(dir)) return [];
|
|
13875
|
+
const names = await fs52.readdir(dir);
|
|
13301
13876
|
const out = [];
|
|
13302
13877
|
for (const name of [...names].sort()) {
|
|
13303
|
-
const abs =
|
|
13304
|
-
const stat = await
|
|
13878
|
+
const abs = path73.join(dir, name);
|
|
13879
|
+
const stat = await fs52.lstat(abs);
|
|
13305
13880
|
if (!stat.isDirectory()) continue;
|
|
13306
|
-
if (!await
|
|
13307
|
-
out.push({ type: "skill", name, sourceRelPath:
|
|
13881
|
+
if (!await fs52.pathExists(path73.join(abs, "SKILL.md"))) continue;
|
|
13882
|
+
out.push({ type: "skill", name, sourceRelPath: path73.posix.join(".claude", "skills", name) });
|
|
13308
13883
|
}
|
|
13309
13884
|
return out;
|
|
13310
13885
|
}
|
|
13311
13886
|
async function collectFlatMarkdown(member, type) {
|
|
13312
13887
|
const subdir = ASSET_SUBDIR[type];
|
|
13313
|
-
const dir =
|
|
13314
|
-
if (!await
|
|
13315
|
-
const names = await
|
|
13888
|
+
const dir = path73.join(member.absPath, ".claude", subdir);
|
|
13889
|
+
if (!await fs52.pathExists(dir)) return [];
|
|
13890
|
+
const names = await fs52.readdir(dir);
|
|
13316
13891
|
const out = [];
|
|
13317
13892
|
for (const fileName of [...names].sort()) {
|
|
13318
13893
|
if (!fileName.endsWith(".md")) continue;
|
|
13319
|
-
const abs =
|
|
13320
|
-
if (!(await
|
|
13894
|
+
const abs = path73.join(dir, fileName);
|
|
13895
|
+
if (!(await fs52.lstat(abs)).isFile()) continue;
|
|
13321
13896
|
const name = fileName.slice(0, -".md".length);
|
|
13322
|
-
out.push({ type, name, sourceRelPath:
|
|
13897
|
+
out.push({ type, name, sourceRelPath: path73.posix.join(".claude", subdir, fileName) });
|
|
13323
13898
|
}
|
|
13324
13899
|
return out;
|
|
13325
13900
|
}
|
|
@@ -13334,9 +13909,9 @@ async function collectSourceAssets(member) {
|
|
|
13334
13909
|
|
|
13335
13910
|
// src/workspace/link-context/plan.ts
|
|
13336
13911
|
function repoFolderPrefix(member) {
|
|
13337
|
-
const base =
|
|
13912
|
+
const base = path74.posix.basename(member.folder);
|
|
13338
13913
|
if (base && base !== "." && base !== "/") return base;
|
|
13339
|
-
const fromAbs =
|
|
13914
|
+
const fromAbs = path74.basename(member.absPath);
|
|
13340
13915
|
return fromAbs || member.id;
|
|
13341
13916
|
}
|
|
13342
13917
|
function assetKey(type, destKey) {
|
|
@@ -13351,7 +13926,7 @@ async function buildLinkPlan(members) {
|
|
|
13351
13926
|
const byKey = /* @__PURE__ */ new Map();
|
|
13352
13927
|
const membersById = new Map(members.map((m) => [m.id, m]));
|
|
13353
13928
|
for (const member of members) {
|
|
13354
|
-
if (!await
|
|
13929
|
+
if (!await fs53.pathExists(member.absPath)) {
|
|
13355
13930
|
skipped.push({ repo: member.id, reason: `not cloned locally (${member.absPath}) \u2014 skipped` });
|
|
13356
13931
|
continue;
|
|
13357
13932
|
}
|
|
@@ -13393,7 +13968,7 @@ async function buildLinkPlan(members) {
|
|
|
13393
13968
|
name: asset.name,
|
|
13394
13969
|
destKey,
|
|
13395
13970
|
sourceRelPath: asset.sourceRelPath,
|
|
13396
|
-
sourceAbsPath:
|
|
13971
|
+
sourceAbsPath: path74.join(member.absPath, asset.sourceRelPath),
|
|
13397
13972
|
sourceHash
|
|
13398
13973
|
});
|
|
13399
13974
|
}
|
|
@@ -13404,7 +13979,7 @@ async function buildLinkPlan(members) {
|
|
|
13404
13979
|
function destRelPathFor(type, destKey) {
|
|
13405
13980
|
const subdir = `${type}s`;
|
|
13406
13981
|
const leaf = type === "skill" ? destKey : `${destKey}.md`;
|
|
13407
|
-
return
|
|
13982
|
+
return path75.posix.join(".claude", subdir, leaf);
|
|
13408
13983
|
}
|
|
13409
13984
|
async function runLinkContext(workspaceRoot, opts = {}) {
|
|
13410
13985
|
const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
|
|
@@ -13430,17 +14005,17 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
13430
14005
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13431
14006
|
for (const entry of plan.entries) {
|
|
13432
14007
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
13433
|
-
const destAbs =
|
|
14008
|
+
const destAbs = path75.join(workspaceRoot, destRel);
|
|
13434
14009
|
if (write) {
|
|
13435
|
-
await
|
|
13436
|
-
if (await
|
|
13437
|
-
await
|
|
14010
|
+
await fs54.ensureDir(path75.dirname(destAbs));
|
|
14011
|
+
if (await fs54.pathExists(destAbs)) await fs54.remove(destAbs);
|
|
14012
|
+
await fs54.copy(entry.sourceAbsPath, destAbs, {
|
|
13438
14013
|
overwrite: true,
|
|
13439
14014
|
errorOnExist: false,
|
|
13440
14015
|
dereference: false,
|
|
13441
14016
|
filter: (s) => {
|
|
13442
14017
|
try {
|
|
13443
|
-
return !
|
|
14018
|
+
return !fs54.lstatSync(s).isSymbolicLink();
|
|
13444
14019
|
} catch {
|
|
13445
14020
|
return false;
|
|
13446
14021
|
}
|
|
@@ -13469,7 +14044,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
13469
14044
|
warn(`Refusing to remove linked context path outside the workspace root: ${old.path}`);
|
|
13470
14045
|
continue;
|
|
13471
14046
|
}
|
|
13472
|
-
if (await
|
|
14047
|
+
if (await fs54.pathExists(abs)) await fs54.remove(abs);
|
|
13473
14048
|
}
|
|
13474
14049
|
}
|
|
13475
14050
|
if (write) {
|
|
@@ -13521,11 +14096,11 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
|
|
|
13521
14096
|
|
|
13522
14097
|
// src/commands/workspace/setup.ts
|
|
13523
14098
|
import { existsSync as existsSync9, statSync } from "node:fs";
|
|
13524
|
-
import
|
|
14099
|
+
import path77 from "node:path";
|
|
13525
14100
|
|
|
13526
14101
|
// src/claude/write-workspace-claude-md.ts
|
|
13527
|
-
import
|
|
13528
|
-
import
|
|
14102
|
+
import path76 from "node:path";
|
|
14103
|
+
import fs55 from "fs-extra";
|
|
13529
14104
|
init_fs();
|
|
13530
14105
|
init_logger();
|
|
13531
14106
|
init_paths();
|
|
@@ -13547,11 +14122,11 @@ ${WORKSPACE_BLOCK_END}`;
|
|
|
13547
14122
|
async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
13548
14123
|
const block = buildWorkspaceImportBlock(opts.client, opts.members);
|
|
13549
14124
|
const dryRun = opts.dryRun ?? false;
|
|
13550
|
-
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") :
|
|
14125
|
+
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path76.join(workspaceRoot, "CLAUDE.md");
|
|
13551
14126
|
const say = opts.quiet ? () => {
|
|
13552
14127
|
} : log;
|
|
13553
|
-
const existed = await
|
|
13554
|
-
const prev = existed ? await
|
|
14128
|
+
const existed = await fs55.pathExists(filePath);
|
|
14129
|
+
const prev = existed ? await fs55.readFile(filePath, "utf8") : "";
|
|
13555
14130
|
const next = opts.collision ? `${block}
|
|
13556
14131
|
` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
|
|
13557
14132
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -13580,20 +14155,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
13580
14155
|
init_fs();
|
|
13581
14156
|
init_logger();
|
|
13582
14157
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
13583
|
-
let dir =
|
|
14158
|
+
let dir = path77.resolve(start);
|
|
13584
14159
|
for (; ; ) {
|
|
13585
|
-
if (existsSync9(
|
|
13586
|
-
const parent =
|
|
13587
|
-
if (parent === dir) return
|
|
14160
|
+
if (existsSync9(path77.join(dir, WORKSPACE_FILE))) return dir;
|
|
14161
|
+
const parent = path77.dirname(dir);
|
|
14162
|
+
if (parent === dir) return path77.resolve(start);
|
|
13588
14163
|
dir = parent;
|
|
13589
14164
|
}
|
|
13590
14165
|
}
|
|
13591
14166
|
function isRootRepo(workspaceRoot, repoPath) {
|
|
13592
|
-
return
|
|
14167
|
+
return path77.resolve(workspaceRoot, repoPath) === path77.resolve(workspaceRoot);
|
|
13593
14168
|
}
|
|
13594
14169
|
async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
13595
14170
|
const apply = options.write ?? false;
|
|
13596
|
-
const configText = await readText(
|
|
14171
|
+
const configText = await readText(path77.join(workspaceRoot, WORKSPACE_FILE));
|
|
13597
14172
|
if (!configText) {
|
|
13598
14173
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
13599
14174
|
process.exitCode = 1;
|
|
@@ -13620,37 +14195,52 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13620
14195
|
const statuses = [];
|
|
13621
14196
|
const aggregateInputs = [];
|
|
13622
14197
|
for (const repo of repos) {
|
|
13623
|
-
const repoRoot =
|
|
14198
|
+
const repoRoot = path77.resolve(workspaceRoot, repo.path);
|
|
13624
14199
|
say(`
|
|
13625
14200
|
\u2192 ${repo.name} (${repo.path})`);
|
|
14201
|
+
let res;
|
|
13626
14202
|
try {
|
|
13627
14203
|
if (!existsSync9(repoRoot) || !statSync(repoRoot).isDirectory()) {
|
|
13628
14204
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
13629
14205
|
}
|
|
13630
|
-
|
|
14206
|
+
res = await runSetupCore(repoRoot, {
|
|
13631
14207
|
apply,
|
|
13632
14208
|
dryRun: options.dryRun,
|
|
13633
14209
|
// Under --json, mute the inner pipeline entirely; we serialize the
|
|
13634
14210
|
// aggregated result below. Otherwise let it print its normal human output.
|
|
13635
|
-
quiet: options.json
|
|
14211
|
+
quiet: options.json,
|
|
14212
|
+
allowEmptyCache: options.allowEmptyCache
|
|
13636
14213
|
});
|
|
14214
|
+
} catch (err) {
|
|
14215
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
13637
14216
|
statuses.push({
|
|
13638
14217
|
name: repo.name,
|
|
13639
14218
|
path: repo.path,
|
|
13640
14219
|
root: repoRoot,
|
|
13641
|
-
status: "
|
|
13642
|
-
|
|
13643
|
-
recommendedCount: res.recommendedCount
|
|
14220
|
+
status: "failed",
|
|
14221
|
+
error: message2
|
|
13644
14222
|
});
|
|
13645
|
-
|
|
13646
|
-
|
|
13647
|
-
|
|
13648
|
-
|
|
14223
|
+
if (!options.continueOnError) {
|
|
14224
|
+
if (options.json) {
|
|
14225
|
+
const result2 = { workspaceRoot, statuses, written: [] };
|
|
14226
|
+
log(JSON.stringify(result2, null, 2));
|
|
14227
|
+
process.exitCode = 1;
|
|
14228
|
+
return result2;
|
|
14229
|
+
}
|
|
14230
|
+
throw new Error(`Setup failed for ${repo.name}: ${message2}`, { cause: err });
|
|
14231
|
+
}
|
|
14232
|
+
error(`Setup failed for ${repo.name}: ${message2}`);
|
|
14233
|
+
continue;
|
|
14234
|
+
}
|
|
14235
|
+
if (!res.catalogInstallOk) {
|
|
14236
|
+
const message2 = "No catalog items were installed for this repo (zero installable content) \u2014 see the per-repo output above, or re-run with --allow-empty-cache.";
|
|
13649
14237
|
statuses.push({
|
|
13650
14238
|
name: repo.name,
|
|
13651
14239
|
path: repo.path,
|
|
13652
14240
|
root: repoRoot,
|
|
13653
14241
|
status: "failed",
|
|
14242
|
+
roles: res.roles,
|
|
14243
|
+
recommendedCount: res.recommendedCount,
|
|
13654
14244
|
error: message2
|
|
13655
14245
|
});
|
|
13656
14246
|
if (!options.continueOnError) {
|
|
@@ -13660,10 +14250,21 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13660
14250
|
process.exitCode = 1;
|
|
13661
14251
|
return result2;
|
|
13662
14252
|
}
|
|
13663
|
-
throw new Error(`Setup failed for ${repo.name}: ${message2}
|
|
14253
|
+
throw new Error(`Setup failed for ${repo.name}: ${message2}`);
|
|
13664
14254
|
}
|
|
13665
14255
|
error(`Setup failed for ${repo.name}: ${message2}`);
|
|
14256
|
+
continue;
|
|
13666
14257
|
}
|
|
14258
|
+
statuses.push({
|
|
14259
|
+
name: repo.name,
|
|
14260
|
+
path: repo.path,
|
|
14261
|
+
root: repoRoot,
|
|
14262
|
+
status: "ok",
|
|
14263
|
+
roles: res.roles,
|
|
14264
|
+
recommendedCount: res.recommendedCount
|
|
14265
|
+
});
|
|
14266
|
+
const context = await readContextOrScan(repoRoot);
|
|
14267
|
+
aggregateInputs.push({ name: repo.name, path: repo.path, context });
|
|
13667
14268
|
}
|
|
13668
14269
|
const written = [];
|
|
13669
14270
|
if (apply && aggregateInputs.length > 0) {
|
|
@@ -13710,7 +14311,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13710
14311
|
const status = statusByName.get(repo.name);
|
|
13711
14312
|
const role = repo.role ?? status?.roles?.[0] ?? "auto";
|
|
13712
14313
|
if (status?.status === "ok") {
|
|
13713
|
-
const lock = await checkLock(
|
|
14314
|
+
const lock = await checkLock(path77.resolve(workspaceRoot, repo.path));
|
|
13714
14315
|
manifestRepos.push({
|
|
13715
14316
|
name: repo.name,
|
|
13716
14317
|
path: repo.path,
|
|
@@ -13766,7 +14367,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13766
14367
|
try {
|
|
13767
14368
|
const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
|
|
13768
14369
|
if (linkResult.ok) {
|
|
13769
|
-
written.push(...linkResult.added.map((rel) =>
|
|
14370
|
+
written.push(...linkResult.added.map((rel) => path77.join(workspaceRoot, rel)));
|
|
13770
14371
|
if (!options.json) {
|
|
13771
14372
|
say(
|
|
13772
14373
|
`Linked ${linkResult.linked.length} cross-repo skill/agent/command copy(ies) into .claude/ (${linkResult.added.length} new, ${linkResult.removed.length} removed)${linkResult.skipped.length > 0 ? ` \u2014 ${linkResult.skipped.length} member(s) skipped, see above` : ""}.`
|
|
@@ -13783,6 +14384,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13783
14384
|
}
|
|
13784
14385
|
const ok = statuses.filter((s) => s.status === "ok").length;
|
|
13785
14386
|
const failed = statuses.length - ok;
|
|
14387
|
+
if (failed > 0) process.exitCode = 1;
|
|
13786
14388
|
const result = { workspaceRoot, statuses, written };
|
|
13787
14389
|
if (options.json) {
|
|
13788
14390
|
log(JSON.stringify(result, null, 2));
|
|
@@ -13798,8 +14400,8 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
13798
14400
|
|
|
13799
14401
|
// src/commands/workspace/undo.ts
|
|
13800
14402
|
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
13801
|
-
import
|
|
13802
|
-
import
|
|
14403
|
+
import path78 from "node:path";
|
|
14404
|
+
import fs56 from "fs-extra";
|
|
13803
14405
|
init_fs();
|
|
13804
14406
|
init_logger();
|
|
13805
14407
|
init_paths();
|
|
@@ -13829,7 +14431,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13829
14431
|
}
|
|
13830
14432
|
if (options.dryRun) {
|
|
13831
14433
|
for (const repo of config.repos) {
|
|
13832
|
-
const repoRoot =
|
|
14434
|
+
const repoRoot = path78.resolve(workspaceRoot, repo.path);
|
|
13833
14435
|
log(`
|
|
13834
14436
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13835
14437
|
try {
|
|
@@ -13850,7 +14452,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13850
14452
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
13851
14453
|
continue;
|
|
13852
14454
|
}
|
|
13853
|
-
if (await
|
|
14455
|
+
if (await fs56.pathExists(abs)) {
|
|
13854
14456
|
log(`[dry-run] Would remove ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
13855
14457
|
}
|
|
13856
14458
|
}
|
|
@@ -13860,12 +14462,12 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13860
14462
|
hausPath(workspaceRoot, "WORKSPACE.md")
|
|
13861
14463
|
];
|
|
13862
14464
|
for (const abs of workspaceArtifacts) {
|
|
13863
|
-
if (await
|
|
13864
|
-
log(`[dry-run] Would remove ${
|
|
14465
|
+
if (await fs56.pathExists(abs)) {
|
|
14466
|
+
log(`[dry-run] Would remove ${path78.relative(workspaceRoot, abs)}`);
|
|
13865
14467
|
}
|
|
13866
14468
|
}
|
|
13867
|
-
if (!await
|
|
13868
|
-
const claudeMd = await readText(
|
|
14469
|
+
if (!await fs56.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
14470
|
+
const claudeMd = await readText(path78.join(workspaceRoot, "CLAUDE.md"));
|
|
13869
14471
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
13870
14472
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
13871
14473
|
}
|
|
@@ -13884,7 +14486,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13884
14486
|
}
|
|
13885
14487
|
let anyRepoFailed = false;
|
|
13886
14488
|
for (const repo of config.repos) {
|
|
13887
|
-
const repoRoot =
|
|
14489
|
+
const repoRoot = path78.resolve(workspaceRoot, repo.path);
|
|
13888
14490
|
log(`
|
|
13889
14491
|
\u2192 ${repo.name} (${repo.path})`);
|
|
13890
14492
|
try {
|
|
@@ -13904,29 +14506,29 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13904
14506
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
13905
14507
|
continue;
|
|
13906
14508
|
}
|
|
13907
|
-
if (await
|
|
13908
|
-
await
|
|
14509
|
+
if (await fs56.pathExists(abs)) {
|
|
14510
|
+
await fs56.remove(abs);
|
|
13909
14511
|
log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
13910
14512
|
}
|
|
13911
14513
|
}
|
|
13912
14514
|
for (const rel of WORKSPACE_AGGREGATE_FILES) {
|
|
13913
14515
|
const abs = hausPath(workspaceRoot, rel);
|
|
13914
|
-
if (await
|
|
13915
|
-
await
|
|
13916
|
-
log(`Removed ${
|
|
14516
|
+
if (await fs56.pathExists(abs)) {
|
|
14517
|
+
await fs56.remove(abs);
|
|
14518
|
+
log(`Removed ${path78.relative(workspaceRoot, abs)}`);
|
|
13917
14519
|
}
|
|
13918
14520
|
}
|
|
13919
14521
|
const manifestFile = manifestPath(workspaceRoot);
|
|
13920
|
-
if (await
|
|
13921
|
-
await
|
|
13922
|
-
log(`Removed ${
|
|
14522
|
+
if (await fs56.pathExists(manifestFile)) {
|
|
14523
|
+
await fs56.remove(manifestFile);
|
|
14524
|
+
log(`Removed ${path78.relative(workspaceRoot, manifestFile)}`);
|
|
13923
14525
|
}
|
|
13924
14526
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
13925
|
-
if (await
|
|
13926
|
-
await
|
|
13927
|
-
log(`Removed ${
|
|
14527
|
+
if (await fs56.pathExists(workspaceMdPath)) {
|
|
14528
|
+
await fs56.remove(workspaceMdPath);
|
|
14529
|
+
log(`Removed ${path78.relative(workspaceRoot, workspaceMdPath)}`);
|
|
13928
14530
|
} else {
|
|
13929
|
-
const claudeMdPath =
|
|
14531
|
+
const claudeMdPath = path78.join(workspaceRoot, "CLAUDE.md");
|
|
13930
14532
|
if (existsSync10(claudeMdPath)) {
|
|
13931
14533
|
const prev = await readText(claudeMdPath);
|
|
13932
14534
|
if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
|
|
@@ -13935,19 +14537,19 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13935
14537
|
end: WORKSPACE_BLOCK_END
|
|
13936
14538
|
});
|
|
13937
14539
|
if (next.length === 0) {
|
|
13938
|
-
await
|
|
14540
|
+
await fs56.remove(claudeMdPath);
|
|
13939
14541
|
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
13940
14542
|
} else {
|
|
13941
|
-
await
|
|
14543
|
+
await fs56.writeFile(claudeMdPath, next, "utf8");
|
|
13942
14544
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
13943
14545
|
}
|
|
13944
14546
|
} else if (prev && hasLegacyWorkspaceBlock(prev)) {
|
|
13945
14547
|
const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
|
|
13946
14548
|
if (next.length === 0) {
|
|
13947
|
-
await
|
|
14549
|
+
await fs56.remove(claudeMdPath);
|
|
13948
14550
|
log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
|
|
13949
14551
|
} else {
|
|
13950
|
-
await
|
|
14552
|
+
await fs56.writeFile(claudeMdPath, next, "utf8");
|
|
13951
14553
|
log(
|
|
13952
14554
|
"Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
|
|
13953
14555
|
);
|
|
@@ -13995,7 +14597,7 @@ relationships: []
|
|
|
13995
14597
|
log("Workspace initialized.");
|
|
13996
14598
|
}
|
|
13997
14599
|
async function scanWorkspace(workspaceRoot, opts) {
|
|
13998
|
-
const configText = await readText(
|
|
14600
|
+
const configText = await readText(path79.join(workspaceRoot, WORKSPACE_FILE));
|
|
13999
14601
|
if (!configText) {
|
|
14000
14602
|
error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
|
|
14001
14603
|
process.exitCode = 1;
|
|
@@ -14017,7 +14619,7 @@ async function scanWorkspace(workspaceRoot, opts) {
|
|
|
14017
14619
|
const inputs = [];
|
|
14018
14620
|
const failures = [];
|
|
14019
14621
|
for (const repo of config.repos) {
|
|
14020
|
-
const repoRoot =
|
|
14622
|
+
const repoRoot = path79.resolve(workspaceRoot, repo.path);
|
|
14021
14623
|
try {
|
|
14022
14624
|
if (!existsSync11(repoRoot) || !statSync3(repoRoot).isDirectory()) {
|
|
14023
14625
|
throw new Error("path is not a directory");
|
|
@@ -14065,7 +14667,8 @@ async function runWorkspace(action, options = {}) {
|
|
|
14065
14667
|
dryRun: options.dryRun,
|
|
14066
14668
|
json: options.json,
|
|
14067
14669
|
continueOnError: options.continueOnError,
|
|
14068
|
-
only: normalizeOnly(options.only)
|
|
14670
|
+
only: normalizeOnly(options.only),
|
|
14671
|
+
allowEmptyCache: options.allowEmptyCache
|
|
14069
14672
|
});
|
|
14070
14673
|
return;
|
|
14071
14674
|
case "doctor":
|
|
@@ -14085,7 +14688,7 @@ init_logger();
|
|
|
14085
14688
|
init_paths();
|
|
14086
14689
|
function cliVersion() {
|
|
14087
14690
|
try {
|
|
14088
|
-
const pkgPath =
|
|
14691
|
+
const pkgPath = path80.join(packageRoot(), "package.json");
|
|
14089
14692
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
14090
14693
|
return pkg.version ?? "0.0.0";
|
|
14091
14694
|
} catch {
|
|
@@ -14095,7 +14698,7 @@ function cliVersion() {
|
|
|
14095
14698
|
var program = new Command();
|
|
14096
14699
|
function validateRuntimeNodeVersion() {
|
|
14097
14700
|
try {
|
|
14098
|
-
const pkgPath =
|
|
14701
|
+
const pkgPath = path80.join(packageRoot(), "package.json");
|
|
14099
14702
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
14100
14703
|
const requiredRange = pkg.engines?.node;
|
|
14101
14704
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -14109,6 +14712,9 @@ function validateRuntimeNodeVersion() {
|
|
|
14109
14712
|
}
|
|
14110
14713
|
validateRuntimeNodeVersion();
|
|
14111
14714
|
program.name("haus").description("Haus AI workflow CLI").version(cliVersion());
|
|
14715
|
+
program.option("--offline", "Make no network calls; use cached or bundled catalog content only").hook("preAction", (thisCommand) => {
|
|
14716
|
+
setOfflineFlag(Boolean(thisCommand.opts()["offline"]));
|
|
14717
|
+
});
|
|
14112
14718
|
program.command("scan").description(
|
|
14113
14719
|
"Scan the repo for roles, dependencies, and package manager; writes context-map.json"
|
|
14114
14720
|
).option("--json").action(runScan);
|
|
@@ -14125,7 +14731,7 @@ program.command("apply").description("Write recommended catalog items and core C
|
|
|
14125
14731
|
"Install exactly these recommended item ids non-interactively (skill backend; space- or comma-separated)"
|
|
14126
14732
|
).option(
|
|
14127
14733
|
"--allow-empty-cache",
|
|
14128
|
-
"
|
|
14734
|
+
"Proceed when no catalog item has installable content: write core files and skip the catalog items. Still exits 1 if none installed (ADR-0060)"
|
|
14129
14735
|
).option(
|
|
14130
14736
|
"--refill-config",
|
|
14131
14737
|
"Fill still-blank fields in an existing workflow-config.md without touching edited ones"
|
|
@@ -14159,6 +14765,9 @@ program.command("explain-recommendation").description("Print a per-item breakdow
|
|
|
14159
14765
|
program.command("init").description("Full setup: scan, recommend, and optionally apply Claude files (safe to re-run)").option("--dry-run", "Preview the `.claude/` write; scan artifacts are still written").option("--json", "Emit one JSON envelope on stdout (requires --dry-run)").option(
|
|
14160
14766
|
"--force",
|
|
14161
14767
|
"Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
|
|
14768
|
+
).option(
|
|
14769
|
+
"--allow-empty-cache",
|
|
14770
|
+
"Proceed when no catalog item has installable content: write core files and skip the catalog items. Still exits 1 if none installed (ADR-0060)"
|
|
14162
14771
|
).action(runInit);
|
|
14163
14772
|
program.command("new").description("Create a new project from an allowed Haus stack, push it to GitHub, and run init").argument("[stack]", "Stack id (omit to pick interactively; see --list)").argument("[name]", "Project directory and repo name").option("--list", "Print the allowed stacks and exit").option("--org <org>", "GitHub org to create the repo in (default: personal account)").option("--public", "Create the GitHub repo public").option("--private", "Create the GitHub repo private (default)").option("--no-github", "Skip GitHub repo creation and push").option("--pm <pm>", "Package manager: yarn | npm | pnpm", "yarn").option("-y, --yes", "Skip confirmations (non-interactive)").option("--dry-run", "Print the full plan without executing anything").option("--json", "Emit a machine-readable result document").action(runNew);
|
|
14164
14773
|
program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
|
|
@@ -14167,6 +14776,12 @@ program.command("clone").description("Clone a single git repository by URL").arg
|
|
|
14167
14776
|
program.command("rescan").description("Re-scan the project, refresh recommendations, and update scan artifacts").option("--dry-run", "Report what would change without writing scan artifacts").action((opts) => runRescan({ dryRun: opts.dryRun }));
|
|
14168
14777
|
program.command("catalog-audit").description("Check the bundled catalog manifest for unsupported or forbidden stacks").action(runCatalogAudit);
|
|
14169
14778
|
program.command("validate-catalog").description("Validate a catalog manifest at an explicit path").argument("[manifest]").action(runValidateCatalog);
|
|
14779
|
+
var catalog = program.command("catalog").description("Catalog cache maintenance");
|
|
14780
|
+
var catalogBundle = catalog.command("bundle").description("Move catalog content between machines (for air-gapped installs)");
|
|
14781
|
+
catalogBundle.command("export").description("Write the populated catalog cache to a portable archive").requiredOption("--out <file>", "Destination .tgz path").action((opts) => runBundleExport({ out: opts.out }));
|
|
14782
|
+
catalogBundle.command("import <file>").description("Unpack a catalog bundle into the local cache (validated like a network sync)").option("--dry-run", "Print what would be imported without writing anything").action(
|
|
14783
|
+
(file, opts) => runBundleImport(file, { dryRun: opts.dryRun })
|
|
14784
|
+
);
|
|
14170
14785
|
program.command("update").description("Sync the remote catalog cache, refresh the lockfile, and check for CLI updates").option("--check").option("--fast", "With --check, skip per-item content hashing (cheap count+ref only)").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
|
|
14171
14786
|
program.command("ci-gate").description(
|
|
14172
14787
|
"Aggregate `doctor` + `decisions check` + `update --check --fast` into one pass/fail gate for CI. Uses the fast/cheap update-check tier (no per-item content hashing) to keep CI runs quick \u2014 run `haus update --check` directly for the fully-hashed tier. Each constituent command remains independently runnable with its own unchanged exit-code contract."
|
|
@@ -14195,7 +14810,10 @@ var workspace = program.command("workspace").description("Multi-repo workspace c
|
|
|
14195
14810
|
workspace.command("init").option("-y, --yes", "Skip confirmation when haus.workspace.yaml already exists").action((opts) => runWorkspace("init", opts));
|
|
14196
14811
|
workspace.command("discover").description("Auto-find member repos and write/merge haus.workspace.yaml").option("--write", "Persist haus.workspace.yaml (default previews only)").option("--json", "Output the discovered repos and proposed config as JSON").option("--max-depth <n>", "Max directory depth to traverse (default 3)").option("--client <name>", "Set the workspace client name").action((opts) => runWorkspace("discover", opts));
|
|
14197
14812
|
workspace.command("scan").description("Aggregate a cross-repo summary from a fast scan of each repo").option("--json", "Output the written artifact paths as JSON").action((opts) => runWorkspace("scan", opts));
|
|
14198
|
-
workspace.command("setup").description("Per-repo setup loop + workspace layer + manifest").option("--write", "Apply changes (default previews only)").option("--dry-run", "Preview changes without writing").option("--json", "Emit machine-readable per-repo output").option("--continue-on-error", "Keep going past a failed repo (default fail-fast)").option("--only <names>", "Restrict to comma-separated repo names").
|
|
14813
|
+
workspace.command("setup").description("Per-repo setup loop + workspace layer + manifest").option("--write", "Apply changes (default previews only)").option("--dry-run", "Preview changes without writing").option("--json", "Emit machine-readable per-repo output").option("--continue-on-error", "Keep going past a failed repo (default fail-fast)").option("--only <names>", "Restrict to comma-separated repo names").option(
|
|
14814
|
+
"--allow-empty-cache",
|
|
14815
|
+
"Proceed when a member repo has no installable catalog content: write core files and skip catalog items for that repo. That repo is still recorded failed if nothing installed (ADR-0060)"
|
|
14816
|
+
).action((opts) => runWorkspace("setup", opts));
|
|
14199
14817
|
workspace.command("doctor").description("Report workspace drift against the manifest").option("--json", "Output the manifest and drift array as JSON").action((opts) => runWorkspace("doctor", opts));
|
|
14200
14818
|
workspace.command("undo").description("Revert haus setup for every configured repo, plus workspace-root artifacts").option("-y, --yes", "Skip confirmation").option("--dry-run", "Print what would be removed without deleting anything").action((opts) => runWorkspace("undo", opts));
|
|
14201
14819
|
workspace.command("link-context").description(
|