@haus-tech/haus-workflow 4.0.1 → 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 +12 -0
- package/README.md +36 -1
- package/dist/cli.js +1368 -771
- package/library/global/skills/haus-workflow/SKILL.md +5 -5
- package/library/global/skills/haus-workflow/references/new.md +116 -23
- package/package.json +2 -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
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 = [
|
|
@@ -2052,180 +2271,28 @@ var HAUS_OWNED_TRACKED_PATHS = [
|
|
|
2052
2271
|
async function findGitignoredHausPaths(root) {
|
|
2053
2272
|
try {
|
|
2054
2273
|
const result = await runGit(["check-ignore", "--", ...HAUS_OWNED_TRACKED_PATHS], {
|
|
2055
|
-
cwd: root,
|
|
2056
|
-
timeout: 5e3
|
|
2057
|
-
});
|
|
2058
|
-
if (result.exitCode !== 0 && result.exitCode !== 1) return [];
|
|
2059
|
-
return [
|
|
2060
|
-
...new Set(
|
|
2061
|
-
result.stdout.split("\n").map((line2) => line2.trim()).filter(Boolean)
|
|
2062
|
-
)
|
|
2063
|
-
];
|
|
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
|
-
}
|
|
2274
|
+
cwd: root,
|
|
2275
|
+
timeout: 5e3
|
|
2276
|
+
});
|
|
2277
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) return [];
|
|
2278
|
+
return [
|
|
2279
|
+
...new Set(
|
|
2280
|
+
result.stdout.split("\n").map((line2) => line2.trim()).filter(Boolean)
|
|
2281
|
+
)
|
|
2282
|
+
];
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -4471,7 +4563,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
|
|
|
4471
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
|
}
|
|
@@ -4550,7 +4642,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
|
|
|
4550
4642
|
say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
4551
4643
|
continue;
|
|
4552
4644
|
}
|
|
4553
|
-
await
|
|
4645
|
+
await fs23.remove(abs);
|
|
4554
4646
|
await pruneEmptyDir(path25.dirname(abs));
|
|
4555
4647
|
say(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
|
|
4556
4648
|
}
|
|
@@ -4605,7 +4697,7 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
|
|
|
4605
4697
|
// src/claude/write-gitignore.ts
|
|
4606
4698
|
init_exec();
|
|
4607
4699
|
import path26 from "node:path";
|
|
4608
|
-
import
|
|
4700
|
+
import fs24 from "fs-extra";
|
|
4609
4701
|
var GITIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
|
|
4610
4702
|
var GITIGNORE_END = "# HAUS:END haus-managed";
|
|
4611
4703
|
var GITIGNORED_ARTIFACT_PATHS = [
|
|
@@ -4721,14 +4813,14 @@ function injectLinkContextGitignoreBlock(existing) {
|
|
|
4721
4813
|
}
|
|
4722
4814
|
async function writeGitignore(root, dryRun, quiet = false) {
|
|
4723
4815
|
const filePath = path26.join(root, ".gitignore");
|
|
4724
|
-
const prev = await
|
|
4816
|
+
const prev = await fs24.pathExists(filePath) ? await fs24.readFile(filePath, "utf8") : "";
|
|
4725
4817
|
const next = injectGitignoreBlock(prev);
|
|
4726
4818
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
4727
4819
|
return filePath;
|
|
4728
4820
|
}
|
|
4729
4821
|
async function writeLinkContextGitignore(root, dryRun, quiet = false) {
|
|
4730
4822
|
const filePath = path26.join(root, ".gitignore");
|
|
4731
|
-
const prev = await
|
|
4823
|
+
const prev = await fs24.pathExists(filePath) ? await fs24.readFile(filePath, "utf8") : "";
|
|
4732
4824
|
const next = injectLinkContextGitignoreBlock(prev);
|
|
4733
4825
|
await writeManagedText(root, filePath, next, dryRun, quiet);
|
|
4734
4826
|
return filePath;
|
|
@@ -4770,6 +4862,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
|
|
|
4770
4862
|
|
|
4771
4863
|
// src/install/ensure-global-plugins.ts
|
|
4772
4864
|
import path27 from "node:path";
|
|
4865
|
+
init_offline();
|
|
4773
4866
|
init_exec();
|
|
4774
4867
|
init_fs();
|
|
4775
4868
|
init_logger();
|
|
@@ -4950,6 +5043,9 @@ var EXEC_ENV = { GIT_TERMINAL_PROMPT: "0" };
|
|
|
4950
5043
|
function globalPluginsDisabled() {
|
|
4951
5044
|
return process.env.HAUS_NO_GLOBAL_PLUGINS === "1";
|
|
4952
5045
|
}
|
|
5046
|
+
function globalPluginMutationsDisabled() {
|
|
5047
|
+
return globalPluginsDisabled() || isOffline();
|
|
5048
|
+
}
|
|
4953
5049
|
var realExec = (args) => runCommand("claude", args, {
|
|
4954
5050
|
timeout: EXEC_TIMEOUT_MS,
|
|
4955
5051
|
env: EXEC_ENV,
|
|
@@ -5093,7 +5189,7 @@ async function ensureGlobalPlugins(pins, deps = {}) {
|
|
|
5093
5189
|
const log2 = deps.log ?? log;
|
|
5094
5190
|
const warn2 = deps.warn ?? warn;
|
|
5095
5191
|
const dryRun = deps.dryRun ?? false;
|
|
5096
|
-
if (
|
|
5192
|
+
if (globalPluginMutationsDisabled() && deps.exec === void 0) {
|
|
5097
5193
|
return { actions: [], ensured: [], failed: [], unavailable: true, restartRequired: false };
|
|
5098
5194
|
}
|
|
5099
5195
|
const state = await probePluginEnv(deps);
|
|
@@ -5162,7 +5258,7 @@ async function removeGlobalPlugins(plugins, deps = {}) {
|
|
|
5162
5258
|
const log2 = deps.log ?? log;
|
|
5163
5259
|
const result = { removed: [], failed: [], unavailable: false };
|
|
5164
5260
|
if (plugins.length === 0) return result;
|
|
5165
|
-
if (
|
|
5261
|
+
if (globalPluginMutationsDisabled() && deps.exec === void 0) {
|
|
5166
5262
|
return { ...result, unavailable: true };
|
|
5167
5263
|
}
|
|
5168
5264
|
const state = await probePluginEnv(deps);
|
|
@@ -5211,25 +5307,25 @@ async function removeGlobalPlugins(plugins, deps = {}) {
|
|
|
5211
5307
|
|
|
5212
5308
|
// src/refs/fetch-refs.ts
|
|
5213
5309
|
import path29 from "node:path";
|
|
5214
|
-
import
|
|
5310
|
+
import fs26 from "fs-extra";
|
|
5215
5311
|
|
|
5216
5312
|
// src/refs/cache-meta.ts
|
|
5217
5313
|
import path28 from "node:path";
|
|
5218
|
-
import
|
|
5314
|
+
import fs25 from "fs-extra";
|
|
5219
5315
|
var META_FILENAME = "cache-meta.json";
|
|
5220
5316
|
async function readCacheMeta(cacheDir) {
|
|
5221
5317
|
const metaPath = path28.join(cacheDir, META_FILENAME);
|
|
5222
5318
|
try {
|
|
5223
|
-
const raw = await
|
|
5319
|
+
const raw = await fs25.readFile(metaPath, "utf8");
|
|
5224
5320
|
return JSON.parse(raw);
|
|
5225
5321
|
} catch {
|
|
5226
5322
|
return {};
|
|
5227
5323
|
}
|
|
5228
5324
|
}
|
|
5229
5325
|
async function writeCacheMeta(cacheDir, meta) {
|
|
5230
|
-
await
|
|
5326
|
+
await fs25.ensureDir(cacheDir);
|
|
5231
5327
|
const metaPath = path28.join(cacheDir, META_FILENAME);
|
|
5232
|
-
await
|
|
5328
|
+
await fs25.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
5233
5329
|
}
|
|
5234
5330
|
|
|
5235
5331
|
// src/refs/fetch-refs.ts
|
|
@@ -5250,7 +5346,7 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
5250
5346
|
const existing = meta[url];
|
|
5251
5347
|
const headers = {};
|
|
5252
5348
|
const safeFile = existing?.file && existing.file === path29.basename(existing.file);
|
|
5253
|
-
const cachedFileExists = safeFile && await
|
|
5349
|
+
const cachedFileExists = safeFile && await fs26.pathExists(path29.join(cacheDir, existing.file));
|
|
5254
5350
|
if (cachedFileExists) {
|
|
5255
5351
|
if (existing?.etag) headers["If-None-Match"] = existing.etag;
|
|
5256
5352
|
if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
|
|
@@ -5261,8 +5357,8 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
5261
5357
|
if (!res.ok) return { result: "failed" };
|
|
5262
5358
|
const text = await res.text();
|
|
5263
5359
|
const file = `${urlToSlug(url)}.md`;
|
|
5264
|
-
await
|
|
5265
|
-
await
|
|
5360
|
+
await fs26.ensureDir(cacheDir);
|
|
5361
|
+
await fs26.writeFile(path29.join(cacheDir, file), text, "utf8");
|
|
5266
5362
|
meta[url] = {
|
|
5267
5363
|
url,
|
|
5268
5364
|
etag: res.headers.get("etag") ?? void 0,
|
|
@@ -5286,7 +5382,7 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
|
5286
5382
|
delete meta[url];
|
|
5287
5383
|
removed++;
|
|
5288
5384
|
const safeFile = typeof entry.file === "string" && entry.file === path29.basename(entry.file);
|
|
5289
|
-
if (safeFile) await
|
|
5385
|
+
if (safeFile) await fs26.remove(path29.join(cacheDir, entry.file)).catch(() => {
|
|
5290
5386
|
});
|
|
5291
5387
|
}
|
|
5292
5388
|
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
@@ -5360,13 +5456,6 @@ function dryRunPrefix(dryRun, text) {
|
|
|
5360
5456
|
|
|
5361
5457
|
// src/commands/apply.ts
|
|
5362
5458
|
var STALE_CACHE_THRESHOLD_DAYS = 7;
|
|
5363
|
-
async function cacheHasItems() {
|
|
5364
|
-
const data = await readJson(path30.join(getCacheDir(), "manifest.json"));
|
|
5365
|
-
return Array.isArray(data?.items) && data.items.length > 0;
|
|
5366
|
-
}
|
|
5367
|
-
function installableRecommendedItems(recommended) {
|
|
5368
|
-
return recommended.filter((item) => item.install !== false);
|
|
5369
|
-
}
|
|
5370
5459
|
async function runApply(options) {
|
|
5371
5460
|
const mode = resolveWriteMode(options, "explicit-choice");
|
|
5372
5461
|
if (mode.kind === "unspecified") {
|
|
@@ -5383,11 +5472,11 @@ async function runApply(options) {
|
|
|
5383
5472
|
process.exitCode = 1;
|
|
5384
5473
|
return;
|
|
5385
5474
|
}
|
|
5386
|
-
const
|
|
5387
|
-
const recommended =
|
|
5475
|
+
const rec2 = await readJson(hausPath(root, "recommendation.json"));
|
|
5476
|
+
const recommended = rec2?.recommended ?? [];
|
|
5388
5477
|
const installableIds = new Set(installableRecommendedItems(recommended).map((i) => i.id));
|
|
5389
5478
|
const configIds = new Set(recommended.filter((i) => i.install === false).map((i) => i.id));
|
|
5390
|
-
const skippedIds = new Set((
|
|
5479
|
+
const skippedIds = new Set((rec2?.skipped ?? []).map((s) => s.id));
|
|
5391
5480
|
for (const id of explicitIds.filter((id2) => !installableIds.has(id2))) {
|
|
5392
5481
|
if (configIds.has(id)) {
|
|
5393
5482
|
warn(`--ids: "${id}" is a config item \u2014 install it with \`haus configs ${id}\`. Ignoring.`);
|
|
@@ -5410,12 +5499,12 @@ async function runApply(options) {
|
|
|
5410
5499
|
process.exitCode = 1;
|
|
5411
5500
|
return;
|
|
5412
5501
|
}
|
|
5413
|
-
const
|
|
5414
|
-
if (!
|
|
5502
|
+
const rec2 = await readJson(hausPath(root, "recommendation.json"));
|
|
5503
|
+
if (!rec2) {
|
|
5415
5504
|
log("No recommendation.json found \u2014 run `haus recommend` first. Writing core files only.");
|
|
5416
5505
|
selectedIds = [];
|
|
5417
5506
|
} else {
|
|
5418
|
-
const items = installableRecommendedItems(
|
|
5507
|
+
const items = installableRecommendedItems(rec2.recommended);
|
|
5419
5508
|
if (items.length === 0) {
|
|
5420
5509
|
log("Recommendation contains no catalog items. Writing core files only.");
|
|
5421
5510
|
selectedIds = [];
|
|
@@ -5435,14 +5524,31 @@ async function runApply(options) {
|
|
|
5435
5524
|
}
|
|
5436
5525
|
}
|
|
5437
5526
|
}
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
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) {
|
|
5446
5552
|
if (isDryRun) {
|
|
5447
5553
|
warn(`Catalog cache is empty \u2014 dry-run will skip catalog items. ${message2}`);
|
|
5448
5554
|
} else {
|
|
@@ -5450,25 +5556,32 @@ async function runApply(options) {
|
|
|
5450
5556
|
process.exitCode = 1;
|
|
5451
5557
|
return;
|
|
5452
5558
|
}
|
|
5453
|
-
} else
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
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
|
+
);
|
|
5462
5577
|
}
|
|
5463
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
|
+
);
|
|
5464
5583
|
}
|
|
5465
5584
|
}
|
|
5466
|
-
const formatRefusal = await catalogFormatRefusal(root);
|
|
5467
|
-
if (formatRefusal) {
|
|
5468
|
-
error(formatRefusal);
|
|
5469
|
-
process.exitCode = 1;
|
|
5470
|
-
return;
|
|
5471
|
-
}
|
|
5472
5585
|
const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
|
|
5473
5586
|
refillConfig: options.refillConfig,
|
|
5474
5587
|
force: options.force,
|
|
@@ -5485,13 +5598,19 @@ async function runApply(options) {
|
|
|
5485
5598
|
const installedIds = new Set(lock.map((entry) => entry.id));
|
|
5486
5599
|
const allItems = await loadCatalog(root);
|
|
5487
5600
|
const installedItems = allItems.filter((item) => installedIds.has(item.id));
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
)
|
|
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
|
+
}
|
|
5495
5614
|
const pruned = await pruneOrphanedRefs(cacheDir, new Set(collectLlmsTxtUrls(installedItems)));
|
|
5496
5615
|
if (pruned > 0)
|
|
5497
5616
|
log(
|
|
@@ -5514,17 +5633,32 @@ async function runApply(options) {
|
|
|
5514
5633
|
}
|
|
5515
5634
|
if (isDryRun) {
|
|
5516
5635
|
log(`Dry-run complete \u2014 ${files.length} file(s) planned, none written. Run --write to apply.`);
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
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
|
+
}
|
|
5520
5654
|
}
|
|
5521
5655
|
}
|
|
5522
5656
|
|
|
5523
5657
|
// src/commands/backups.ts
|
|
5658
|
+
import path30 from "node:path";
|
|
5659
|
+
import fs27 from "fs-extra";
|
|
5524
5660
|
init_logger();
|
|
5525
5661
|
init_paths();
|
|
5526
|
-
import path31 from "node:path";
|
|
5527
|
-
import fs26 from "fs-extra";
|
|
5528
5662
|
|
|
5529
5663
|
// src/utils/prompts.ts
|
|
5530
5664
|
import { stdin, stdout } from "node:process";
|
|
@@ -5587,25 +5721,16 @@ Enter a number (1-${choices.length})`, io);
|
|
|
5587
5721
|
}
|
|
5588
5722
|
|
|
5589
5723
|
// src/commands/backups.ts
|
|
5590
|
-
var LOCK_BACKUP_RE = /^haus\.lock\.\d+\.json$/;
|
|
5591
|
-
function classify(name) {
|
|
5592
|
-
if (LOCK_BACKUP_RE.test(name)) return "lock";
|
|
5593
|
-
if (name.startsWith("undo-")) return "undo";
|
|
5594
|
-
if (name.startsWith("prune-")) return "prune";
|
|
5595
|
-
if (name.startsWith("remove-")) return "remove";
|
|
5596
|
-
if (name.startsWith("migrate-")) return "migrate";
|
|
5597
|
-
return void 0;
|
|
5598
|
-
}
|
|
5599
5724
|
async function listBackups(root) {
|
|
5600
5725
|
const dir = hausPath(root, "backups");
|
|
5601
|
-
if (!await
|
|
5602
|
-
const names = await
|
|
5726
|
+
if (!await fs27.pathExists(dir)) return [];
|
|
5727
|
+
const names = await fs27.readdir(dir);
|
|
5603
5728
|
const entries = [];
|
|
5604
5729
|
for (const name of names) {
|
|
5605
|
-
const kind =
|
|
5730
|
+
const kind = classifyBackupName(name);
|
|
5606
5731
|
if (!kind) continue;
|
|
5607
|
-
const absPath =
|
|
5608
|
-
const stat = await
|
|
5732
|
+
const absPath = path30.join(dir, name);
|
|
5733
|
+
const stat = await fs27.lstat(absPath);
|
|
5609
5734
|
if (stat.isSymbolicLink()) continue;
|
|
5610
5735
|
const expectDirectory = kind !== "lock";
|
|
5611
5736
|
if (stat.isDirectory() !== expectDirectory) continue;
|
|
@@ -5644,13 +5769,13 @@ async function runList(root, json) {
|
|
|
5644
5769
|
}
|
|
5645
5770
|
}
|
|
5646
5771
|
async function isUnsafeWriteTarget(root, target) {
|
|
5647
|
-
const segments =
|
|
5772
|
+
const segments = path30.relative(root, target).split(path30.sep).filter(Boolean);
|
|
5648
5773
|
let current = root;
|
|
5649
5774
|
for (let i = 0; i < segments.length; i++) {
|
|
5650
|
-
current =
|
|
5775
|
+
current = path30.join(current, segments[i]);
|
|
5651
5776
|
const isLastSegment = i === segments.length - 1;
|
|
5652
5777
|
try {
|
|
5653
|
-
const stat = await
|
|
5778
|
+
const stat = await fs27.lstat(current);
|
|
5654
5779
|
if (isLastSegment) {
|
|
5655
5780
|
if (!stat.isFile()) return true;
|
|
5656
5781
|
} else if (!stat.isDirectory()) {
|
|
@@ -5665,7 +5790,7 @@ async function isUnsafeWriteTarget(root, target) {
|
|
|
5665
5790
|
}
|
|
5666
5791
|
async function lstatIfRegularFile(abs) {
|
|
5667
5792
|
try {
|
|
5668
|
-
const stat = await
|
|
5793
|
+
const stat = await fs27.lstat(abs);
|
|
5669
5794
|
return stat.isFile() ? stat : null;
|
|
5670
5795
|
} catch (err) {
|
|
5671
5796
|
if (err.code === "ENOENT") return null;
|
|
@@ -5680,8 +5805,8 @@ async function restoreLockBackup(root, entry, yes, json, dryRun) {
|
|
|
5680
5805
|
);
|
|
5681
5806
|
return { ok: false, restored: 0 };
|
|
5682
5807
|
}
|
|
5683
|
-
if (await
|
|
5684
|
-
const targetStat = await
|
|
5808
|
+
if (await fs27.pathExists(target)) {
|
|
5809
|
+
const targetStat = await fs27.stat(target);
|
|
5685
5810
|
if (targetStat.mtimeMs > entry.mtimeMs) {
|
|
5686
5811
|
warn(
|
|
5687
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.`
|
|
@@ -5715,7 +5840,7 @@ async function restoreLockBackup(root, entry, yes, json, dryRun) {
|
|
|
5715
5840
|
error(`Refusing to restore: ${entry.id} or ${target} changed since being checked.`);
|
|
5716
5841
|
return { ok: false, restored: 0 };
|
|
5717
5842
|
}
|
|
5718
|
-
await
|
|
5843
|
+
await fs27.copy(entry.absPath, target, { overwrite: true });
|
|
5719
5844
|
if (!json) log(`Restored .haus-workflow/haus.lock.json from ${entry.id}.`);
|
|
5720
5845
|
return { ok: true, restored: 1 };
|
|
5721
5846
|
}
|
|
@@ -5723,10 +5848,10 @@ async function collectFilesRecursive(dir) {
|
|
|
5723
5848
|
const files = [];
|
|
5724
5849
|
const skippedLinks = [];
|
|
5725
5850
|
const skippedOther = [];
|
|
5726
|
-
const names = await
|
|
5851
|
+
const names = await fs27.readdir(dir);
|
|
5727
5852
|
for (const name of names) {
|
|
5728
|
-
const abs =
|
|
5729
|
-
const stat = await
|
|
5853
|
+
const abs = path30.join(dir, name);
|
|
5854
|
+
const stat = await fs27.lstat(abs);
|
|
5730
5855
|
if (stat.isSymbolicLink()) {
|
|
5731
5856
|
skippedLinks.push(abs);
|
|
5732
5857
|
continue;
|
|
@@ -5748,12 +5873,12 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
5748
5873
|
const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
|
|
5749
5874
|
if (skippedLinks.length > 0) {
|
|
5750
5875
|
warn(
|
|
5751
|
-
`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(", ")}`
|
|
5752
5877
|
);
|
|
5753
5878
|
}
|
|
5754
5879
|
if (skippedOther.length > 0) {
|
|
5755
5880
|
warn(
|
|
5756
|
-
`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(", ")}`
|
|
5757
5882
|
);
|
|
5758
5883
|
}
|
|
5759
5884
|
if (files.length === 0) {
|
|
@@ -5763,8 +5888,8 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
5763
5888
|
const safeFiles = [];
|
|
5764
5889
|
const unsafeTargets = [];
|
|
5765
5890
|
for (const abs of files) {
|
|
5766
|
-
const rel =
|
|
5767
|
-
const target =
|
|
5891
|
+
const rel = path30.relative(entry.absPath, abs);
|
|
5892
|
+
const target = path30.join(root, rel);
|
|
5768
5893
|
if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
|
|
5769
5894
|
else safeFiles.push(abs);
|
|
5770
5895
|
}
|
|
@@ -5781,16 +5906,16 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
5781
5906
|
const vanished = [];
|
|
5782
5907
|
const toRestore = [];
|
|
5783
5908
|
for (const abs of safeFiles) {
|
|
5784
|
-
const rel =
|
|
5909
|
+
const rel = path30.relative(entry.absPath, abs);
|
|
5785
5910
|
const backupFileStat = await lstatIfRegularFile(abs);
|
|
5786
5911
|
if (!backupFileStat) {
|
|
5787
5912
|
vanished.push(rel);
|
|
5788
5913
|
continue;
|
|
5789
5914
|
}
|
|
5790
5915
|
toRestore.push(abs);
|
|
5791
|
-
const target =
|
|
5792
|
-
if (await
|
|
5793
|
-
const targetStat = await
|
|
5916
|
+
const target = path30.join(root, rel);
|
|
5917
|
+
if (await fs27.pathExists(target)) {
|
|
5918
|
+
const targetStat = await fs27.stat(target);
|
|
5794
5919
|
if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
|
|
5795
5920
|
stale.push({
|
|
5796
5921
|
rel,
|
|
@@ -5818,7 +5943,7 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
5818
5943
|
);
|
|
5819
5944
|
}
|
|
5820
5945
|
if (dryRun) {
|
|
5821
|
-
const rels = toRestore.map((abs) =>
|
|
5946
|
+
const rels = toRestore.map((abs) => path30.relative(entry.absPath, abs));
|
|
5822
5947
|
if (!json) {
|
|
5823
5948
|
log(`[dry-run] Would restore ${rels.length} file(s) from ${entry.id}:`);
|
|
5824
5949
|
for (const rel of rels) log(` ${rel}`);
|
|
@@ -5844,14 +5969,14 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
|
|
|
5844
5969
|
let restoredCount = 0;
|
|
5845
5970
|
const skippedAtWrite = [];
|
|
5846
5971
|
for (const abs of toRestore) {
|
|
5847
|
-
const rel =
|
|
5848
|
-
const target =
|
|
5972
|
+
const rel = path30.relative(entry.absPath, abs);
|
|
5973
|
+
const target = path30.join(root, rel);
|
|
5849
5974
|
if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
|
|
5850
5975
|
skippedAtWrite.push(rel);
|
|
5851
5976
|
continue;
|
|
5852
5977
|
}
|
|
5853
|
-
await
|
|
5854
|
-
await
|
|
5978
|
+
await fs27.ensureDir(path30.dirname(target));
|
|
5979
|
+
await fs27.copy(abs, target, { overwrite: true });
|
|
5855
5980
|
restoredCount++;
|
|
5856
5981
|
}
|
|
5857
5982
|
if (skippedAtWrite.length > 0) {
|
|
@@ -5932,7 +6057,7 @@ Keeping ${keepCount}.`
|
|
|
5932
6057
|
const failed = [];
|
|
5933
6058
|
for (const entry of removeList) {
|
|
5934
6059
|
try {
|
|
5935
|
-
await
|
|
6060
|
+
await fs27.remove(entry.absPath);
|
|
5936
6061
|
removed.push(entry.id);
|
|
5937
6062
|
} catch (err) {
|
|
5938
6063
|
failed.push({ id: entry.id, message: err instanceof Error ? err.message : String(err) });
|
|
@@ -5977,75 +6102,321 @@ async function runBackups(action, opts = {}) {
|
|
|
5977
6102
|
process.exitCode = 1;
|
|
5978
6103
|
return;
|
|
5979
6104
|
}
|
|
5980
|
-
let olderThanDays;
|
|
5981
|
-
if (opts.olderThan !== void 0) {
|
|
5982
|
-
const n = Number(opts.olderThan);
|
|
5983
|
-
if (!Number.isFinite(n) || n < 0) {
|
|
5984
|
-
const message2 = `Invalid --older-than value "${opts.olderThan}" \u2014 expected a non-negative number of days.`;
|
|
5985
|
-
if (json) log(JSON.stringify({ ok: false, error: message2 }));
|
|
5986
|
-
error(message2);
|
|
5987
|
-
process.exitCode = 1;
|
|
5988
|
-
return;
|
|
6105
|
+
let olderThanDays;
|
|
6106
|
+
if (opts.olderThan !== void 0) {
|
|
6107
|
+
const n = Number(opts.olderThan);
|
|
6108
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
6109
|
+
const message2 = `Invalid --older-than value "${opts.olderThan}" \u2014 expected a non-negative number of days.`;
|
|
6110
|
+
if (json) log(JSON.stringify({ ok: false, error: message2 }));
|
|
6111
|
+
error(message2);
|
|
6112
|
+
process.exitCode = 1;
|
|
6113
|
+
return;
|
|
6114
|
+
}
|
|
6115
|
+
olderThanDays = n;
|
|
6116
|
+
}
|
|
6117
|
+
let keep;
|
|
6118
|
+
if (opts.keep !== void 0) {
|
|
6119
|
+
const n = Number(opts.keep);
|
|
6120
|
+
if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) {
|
|
6121
|
+
const message2 = `Invalid --keep value "${opts.keep}" \u2014 expected a non-negative integer.`;
|
|
6122
|
+
if (json) log(JSON.stringify({ ok: false, error: message2 }));
|
|
6123
|
+
error(message2);
|
|
6124
|
+
process.exitCode = 1;
|
|
6125
|
+
return;
|
|
6126
|
+
}
|
|
6127
|
+
keep = n;
|
|
6128
|
+
}
|
|
6129
|
+
const result = await runPrune(root, {
|
|
6130
|
+
olderThanDays,
|
|
6131
|
+
keep,
|
|
6132
|
+
yes: Boolean(opts.yes),
|
|
6133
|
+
json,
|
|
6134
|
+
dryRun: Boolean(opts.dryRun)
|
|
6135
|
+
});
|
|
6136
|
+
if (json && !result.refused) log(JSON.stringify(result, null, 2));
|
|
6137
|
+
if (!result.ok) process.exitCode = 1;
|
|
6138
|
+
}
|
|
6139
|
+
|
|
6140
|
+
// src/commands/catalog-audit.ts
|
|
6141
|
+
init_validation_rules2();
|
|
6142
|
+
init_logger();
|
|
6143
|
+
function auditForbiddenTags(items) {
|
|
6144
|
+
const failures = [];
|
|
6145
|
+
const forbidden = new Set(FORBIDDEN_TAGS.map((w) => w.toLowerCase()));
|
|
6146
|
+
for (const item of items) {
|
|
6147
|
+
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
6148
|
+
if (forbidden.has(tag.toLowerCase())) failures.push(`${item.id} has unsupported tag ${tag}`);
|
|
6149
|
+
}
|
|
6150
|
+
for (const token of item.id.toLowerCase().split(/[^a-z0-9+]+/)) {
|
|
6151
|
+
if (forbidden.has(token)) failures.push(`${item.id} has unsupported tag ${token}`);
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
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));
|
|
5989
6294
|
}
|
|
5990
|
-
olderThanDays = n;
|
|
5991
6295
|
}
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
return;
|
|
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));
|
|
6001
6304
|
}
|
|
6002
|
-
keep = n;
|
|
6003
6305
|
}
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
yes: Boolean(opts.yes),
|
|
6008
|
-
json,
|
|
6009
|
-
dryRun: Boolean(opts.dryRun)
|
|
6010
|
-
});
|
|
6011
|
-
if (json && !result.refused) log(JSON.stringify(result, null, 2));
|
|
6012
|
-
if (!result.ok) process.exitCode = 1;
|
|
6306
|
+
if (dir !== root && (await fs28.readdir(dir)).length === 0) {
|
|
6307
|
+
await fs28.remove(dir);
|
|
6308
|
+
}
|
|
6013
6309
|
}
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
if (forbidden.has(tag.toLowerCase())) failures.push(`${item.id} has unsupported tag ${tag}`);
|
|
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;
|
|
6024
6319
|
}
|
|
6025
|
-
|
|
6026
|
-
|
|
6320
|
+
if (entry.isDirectory()) {
|
|
6321
|
+
removed.push(...await stripSymlinks(abs));
|
|
6027
6322
|
}
|
|
6028
6323
|
}
|
|
6029
|
-
return
|
|
6324
|
+
return removed;
|
|
6030
6325
|
}
|
|
6031
|
-
async function
|
|
6032
|
-
const
|
|
6033
|
-
const
|
|
6034
|
-
if (
|
|
6035
|
-
|
|
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}`);
|
|
6036
6331
|
process.exitCode = 1;
|
|
6037
6332
|
return;
|
|
6038
6333
|
}
|
|
6039
|
-
|
|
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
|
+
}
|
|
6040
6411
|
}
|
|
6041
6412
|
|
|
6042
6413
|
// src/commands/doctor.ts
|
|
6043
6414
|
import path45 from "node:path";
|
|
6044
|
-
import
|
|
6415
|
+
import fs37 from "fs-extra";
|
|
6045
6416
|
init_manifest_format();
|
|
6046
6417
|
|
|
6047
6418
|
// src/claude/former-install-orphans.ts
|
|
6048
|
-
import
|
|
6419
|
+
import fs29 from "fs-extra";
|
|
6049
6420
|
init_path_safety();
|
|
6050
6421
|
init_paths();
|
|
6051
6422
|
var toPosix = (value) => value.replace(/\\/g, "/");
|
|
@@ -6069,7 +6440,7 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
|
|
|
6069
6440
|
const projectRel = `.claude/${relPath}`;
|
|
6070
6441
|
if (trackedPaths.has(projectRel) || reported.has(projectRel)) continue;
|
|
6071
6442
|
const abs = safeJoin(claudePath(root), relPath);
|
|
6072
|
-
if (abs === null || !await
|
|
6443
|
+
if (abs === null || !await fs29.pathExists(abs)) continue;
|
|
6073
6444
|
reported.add(projectRel);
|
|
6074
6445
|
orphans.push({ formerId: entry.formerId, currentId: entry.currentId, relPath: projectRel });
|
|
6075
6446
|
}
|
|
@@ -6080,7 +6451,7 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
|
|
|
6080
6451
|
init_paths();
|
|
6081
6452
|
import path32 from "node:path";
|
|
6082
6453
|
import fg4 from "fast-glob";
|
|
6083
|
-
import
|
|
6454
|
+
import fs30 from "fs-extra";
|
|
6084
6455
|
var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
|
|
6085
6456
|
function stripFencedCodeBlocks(text) {
|
|
6086
6457
|
return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
|
|
@@ -6088,15 +6459,15 @@ function stripFencedCodeBlocks(text) {
|
|
|
6088
6459
|
async function findUnresolvedSkillReferences(root, options = {}) {
|
|
6089
6460
|
const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
|
|
6090
6461
|
const skillsRoot = claudePath(root, "skills");
|
|
6091
|
-
if (!await
|
|
6092
|
-
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);
|
|
6093
6464
|
const installedSkillNames = new Set(skillDirs);
|
|
6094
6465
|
const files = await fg4("*/**/*.md", { cwd: skillsRoot, onlyFiles: true, dot: true });
|
|
6095
6466
|
const issues = [];
|
|
6096
6467
|
for (const rel of files) {
|
|
6097
6468
|
if (rel.startsWith("shared/")) continue;
|
|
6098
6469
|
const abs = path32.join(skillsRoot, rel);
|
|
6099
|
-
const raw = await
|
|
6470
|
+
const raw = await fs30.readFile(abs, "utf8");
|
|
6100
6471
|
const lines = stripFencedCodeBlocks(raw).split("\n");
|
|
6101
6472
|
lines.forEach((line2, idx) => {
|
|
6102
6473
|
for (const match of line2.matchAll(REFERENCE_PATTERN)) {
|
|
@@ -6119,13 +6490,13 @@ async function findUnresolvedSkillReferences(root, options = {}) {
|
|
|
6119
6490
|
// src/decisions/doctor.ts
|
|
6120
6491
|
init_fs();
|
|
6121
6492
|
import path37 from "node:path";
|
|
6122
|
-
import
|
|
6493
|
+
import fs34 from "fs-extra";
|
|
6123
6494
|
|
|
6124
6495
|
// src/decisions/check.ts
|
|
6125
6496
|
init_fs();
|
|
6126
6497
|
init_paths();
|
|
6127
6498
|
import path36 from "node:path";
|
|
6128
|
-
import
|
|
6499
|
+
import fs33 from "fs-extra";
|
|
6129
6500
|
|
|
6130
6501
|
// src/decisions/dep-bump.ts
|
|
6131
6502
|
var DEP_SECTIONS = [
|
|
@@ -6318,12 +6689,12 @@ function firstMatchingRegex(filePath, patterns) {
|
|
|
6318
6689
|
|
|
6319
6690
|
// src/decisions/paths.ts
|
|
6320
6691
|
import path34 from "node:path";
|
|
6321
|
-
import
|
|
6692
|
+
import fs31 from "fs-extra";
|
|
6322
6693
|
async function resolveDecisionsDir(root) {
|
|
6323
6694
|
const primary = path34.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
6324
|
-
if (await
|
|
6695
|
+
if (await fs31.pathExists(primary)) return primary;
|
|
6325
6696
|
const legacy = path34.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
6326
|
-
if (await
|
|
6697
|
+
if (await fs31.pathExists(legacy)) return legacy;
|
|
6327
6698
|
return primary;
|
|
6328
6699
|
}
|
|
6329
6700
|
function relativeDecisionPath(decisionsDir, root) {
|
|
@@ -6333,7 +6704,7 @@ function relativeDecisionPath(decisionsDir, root) {
|
|
|
6333
6704
|
// src/decisions/validate.ts
|
|
6334
6705
|
init_fs();
|
|
6335
6706
|
import path35 from "node:path";
|
|
6336
|
-
import
|
|
6707
|
+
import fs32 from "fs-extra";
|
|
6337
6708
|
function validateDecisionContent(content2) {
|
|
6338
6709
|
const errors = [];
|
|
6339
6710
|
for (const section of DECISIONS_TRIGGERS.requiredSections) {
|
|
@@ -6363,7 +6734,7 @@ function decisionNumberFromFile(name) {
|
|
|
6363
6734
|
}
|
|
6364
6735
|
async function validateIndexSync(decisionsDir, decisionFiles) {
|
|
6365
6736
|
const readmePath = path35.join(decisionsDir, "README.md");
|
|
6366
|
-
if (!await
|
|
6737
|
+
if (!await fs32.pathExists(readmePath)) {
|
|
6367
6738
|
return ["docs/decisions/README.md is missing"];
|
|
6368
6739
|
}
|
|
6369
6740
|
const readme = await readText(readmePath) ?? "";
|
|
@@ -6535,8 +6906,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
6535
6906
|
changedFiles: stats.files
|
|
6536
6907
|
});
|
|
6537
6908
|
if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
|
|
6538
|
-
const legacyExists = await
|
|
6539
|
-
if (legacyExists && !await
|
|
6909
|
+
const legacyExists = await fs33.pathExists(path36.join(root, legacyRel));
|
|
6910
|
+
if (legacyExists && !await fs33.pathExists(decisionsDir)) {
|
|
6540
6911
|
satisfactionErrors = await checkSatisfaction({
|
|
6541
6912
|
root,
|
|
6542
6913
|
decisionsRel: legacyRel,
|
|
@@ -6558,8 +6929,8 @@ ${opts.commitMessages ?? ""}`;
|
|
|
6558
6929
|
return { triggered: true, satisfied: false, reasons, securityTriggered };
|
|
6559
6930
|
}
|
|
6560
6931
|
async function listDecisionNumbers(decisionsDir) {
|
|
6561
|
-
if (!await
|
|
6562
|
-
const entries = await
|
|
6932
|
+
if (!await fs33.pathExists(decisionsDir)) return [];
|
|
6933
|
+
const entries = await fs33.readdir(decisionsDir);
|
|
6563
6934
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
6564
6935
|
}
|
|
6565
6936
|
|
|
@@ -6568,8 +6939,8 @@ async function auditDecisionsLayout(root) {
|
|
|
6568
6939
|
const findings = [];
|
|
6569
6940
|
const primary = path37.join(root, DECISIONS_TRIGGERS.decisionsDir);
|
|
6570
6941
|
const legacy = path37.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
|
|
6571
|
-
const hasPrimary = await
|
|
6572
|
-
const hasLegacy = await
|
|
6942
|
+
const hasPrimary = await fs34.pathExists(primary);
|
|
6943
|
+
const hasLegacy = await fs34.pathExists(legacy);
|
|
6573
6944
|
if (!hasPrimary && hasLegacy) {
|
|
6574
6945
|
findings.push({
|
|
6575
6946
|
level: "advisory",
|
|
@@ -6583,7 +6954,7 @@ async function auditDecisionsLayout(root) {
|
|
|
6583
6954
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
6584
6955
|
const rel = relativeDecisionPath(decisionsDir, root);
|
|
6585
6956
|
const readmePath = path37.join(decisionsDir, "README.md");
|
|
6586
|
-
if (!await
|
|
6957
|
+
if (!await fs34.pathExists(readmePath)) {
|
|
6587
6958
|
findings.push({
|
|
6588
6959
|
level: "advisory",
|
|
6589
6960
|
line: `- DECISIONS: missing ${rel}/README.md index`,
|
|
@@ -6593,7 +6964,7 @@ async function auditDecisionsLayout(root) {
|
|
|
6593
6964
|
return findings;
|
|
6594
6965
|
}
|
|
6595
6966
|
const readme = await readText(readmePath);
|
|
6596
|
-
const entries = (await
|
|
6967
|
+
const entries = (await fs34.readdir(decisionsDir)).filter(
|
|
6597
6968
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
6598
6969
|
);
|
|
6599
6970
|
for (const file of entries) {
|
|
@@ -6642,7 +7013,7 @@ init_fs();
|
|
|
6642
7013
|
init_logger();
|
|
6643
7014
|
init_paths();
|
|
6644
7015
|
import path38 from "node:path";
|
|
6645
|
-
import
|
|
7016
|
+
import fs35 from "fs-extra";
|
|
6646
7017
|
function manifestPathIn(configDir) {
|
|
6647
7018
|
return path38.join(configDir, "haus", "install-manifest.json");
|
|
6648
7019
|
}
|
|
@@ -6672,9 +7043,9 @@ async function pruneVacatedDirs(root, vacatedFiles) {
|
|
|
6672
7043
|
async function destinationWritable(dir) {
|
|
6673
7044
|
let candidate = path38.resolve(dir);
|
|
6674
7045
|
for (; ; ) {
|
|
6675
|
-
if (await
|
|
7046
|
+
if (await fs35.pathExists(candidate)) {
|
|
6676
7047
|
try {
|
|
6677
|
-
await
|
|
7048
|
+
await fs35.access(candidate, fs35.constants.W_OK);
|
|
6678
7049
|
return true;
|
|
6679
7050
|
} catch (err) {
|
|
6680
7051
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -6706,7 +7077,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
6706
7077
|
if (!legacy || !Array.isArray(legacy.files)) {
|
|
6707
7078
|
return { ...base, reason: "no install manifest under ~/.claude" };
|
|
6708
7079
|
}
|
|
6709
|
-
if (await
|
|
7080
|
+
if (await fs35.pathExists(manifestPathIn(to))) {
|
|
6710
7081
|
return { ...base, reason: "an install manifest already exists in the resolved config dir" };
|
|
6711
7082
|
}
|
|
6712
7083
|
const writable = await destinationWritable(to);
|
|
@@ -6729,14 +7100,14 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
6729
7100
|
continue;
|
|
6730
7101
|
}
|
|
6731
7102
|
const newDest = path38.join(to, rel);
|
|
6732
|
-
if (!await
|
|
7103
|
+
if (!await fs35.pathExists(entry.destPath)) {
|
|
6733
7104
|
rewritten.push({ ...entry, destPath: newDest });
|
|
6734
7105
|
continue;
|
|
6735
7106
|
}
|
|
6736
|
-
if (await
|
|
7107
|
+
if (await fs35.pathExists(newDest)) {
|
|
6737
7108
|
if (await sameContent(entry.destPath, newDest)) {
|
|
6738
7109
|
if (!dryRun) {
|
|
6739
|
-
await
|
|
7110
|
+
await fs35.remove(entry.destPath);
|
|
6740
7111
|
vacated.push(entry.destPath);
|
|
6741
7112
|
}
|
|
6742
7113
|
moved.push(newDest);
|
|
@@ -6753,7 +7124,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
6753
7124
|
continue;
|
|
6754
7125
|
}
|
|
6755
7126
|
try {
|
|
6756
|
-
await
|
|
7127
|
+
await fs35.move(entry.destPath, newDest);
|
|
6757
7128
|
vacated.push(entry.destPath);
|
|
6758
7129
|
moved.push(newDest);
|
|
6759
7130
|
rewritten.push({ ...entry, destPath: newDest });
|
|
@@ -6778,7 +7149,7 @@ async function migrateGlobalConfigDir(options = {}) {
|
|
|
6778
7149
|
}
|
|
6779
7150
|
try {
|
|
6780
7151
|
await writeJson(manifestPathIn(to), { ...legacy, files: rewritten });
|
|
6781
|
-
await
|
|
7152
|
+
await fs35.remove(legacyManifestPath);
|
|
6782
7153
|
vacated.push(legacyManifestPath);
|
|
6783
7154
|
} catch (err) {
|
|
6784
7155
|
return {
|
|
@@ -6824,7 +7195,7 @@ function printConfigDirMigration(result) {
|
|
|
6824
7195
|
|
|
6825
7196
|
// src/scanner/read-context.ts
|
|
6826
7197
|
import path44 from "node:path";
|
|
6827
|
-
import
|
|
7198
|
+
import fs36 from "fs-extra";
|
|
6828
7199
|
|
|
6829
7200
|
// src/commands/workspace/config.ts
|
|
6830
7201
|
init_fs();
|
|
@@ -7408,10 +7779,10 @@ async function isCacheFresh(root, cachePath) {
|
|
|
7408
7779
|
path44.join(root, WORKSPACE_FILE),
|
|
7409
7780
|
path44.join(root, REPOS_MANIFEST_FILE)
|
|
7410
7781
|
];
|
|
7411
|
-
const cacheStat = await
|
|
7782
|
+
const cacheStat = await fs36.stat(cachePath);
|
|
7412
7783
|
for (const markerPath of markerPaths) {
|
|
7413
|
-
if (!await
|
|
7414
|
-
const markerStat = await
|
|
7784
|
+
if (!await fs36.pathExists(markerPath)) continue;
|
|
7785
|
+
const markerStat = await fs36.stat(markerPath);
|
|
7415
7786
|
if (markerStat.mtimeMs > cacheStat.mtimeMs) return false;
|
|
7416
7787
|
}
|
|
7417
7788
|
return true;
|
|
@@ -7425,11 +7796,15 @@ async function readContextOrScan(root) {
|
|
|
7425
7796
|
}
|
|
7426
7797
|
|
|
7427
7798
|
// src/update/npm-version.ts
|
|
7799
|
+
init_offline();
|
|
7428
7800
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
7429
7801
|
function isTestMode2() {
|
|
7430
7802
|
return process.env["HAUS_TEST_MODE"] === "1" || process.env["NODE_ENV"] === "test";
|
|
7431
7803
|
}
|
|
7432
7804
|
async function fetchNpmVersionStatus(currentVersion) {
|
|
7805
|
+
if (isOffline()) {
|
|
7806
|
+
return { current: currentVersion, latest: null, updateAvailable: false };
|
|
7807
|
+
}
|
|
7433
7808
|
if (isTestMode2() && process.env["HAUS_SKIP_NPM_CHECK"]) {
|
|
7434
7809
|
return { current: currentVersion, latest: null, updateAvailable: false };
|
|
7435
7810
|
}
|
|
@@ -7568,7 +7943,7 @@ async function runDoctor(options) {
|
|
|
7568
7943
|
const resolved = path45.resolve(root, target);
|
|
7569
7944
|
const relFromRoot = path45.relative(root, resolved);
|
|
7570
7945
|
const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path45.sep}`) || path45.isAbsolute(relFromRoot);
|
|
7571
|
-
if (escapesRoot || !await
|
|
7946
|
+
if (escapesRoot || !await fs37.pathExists(resolved)) {
|
|
7572
7947
|
flag(
|
|
7573
7948
|
`- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
|
|
7574
7949
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -7586,7 +7961,7 @@ async function runDoctor(options) {
|
|
|
7586
7961
|
);
|
|
7587
7962
|
}
|
|
7588
7963
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
7589
|
-
const workflowExists = await
|
|
7964
|
+
const workflowExists = await fs37.pathExists(workflowPath);
|
|
7590
7965
|
if (!workflowExists) {
|
|
7591
7966
|
flag(
|
|
7592
7967
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -7609,7 +7984,7 @@ async function runDoctor(options) {
|
|
|
7609
7984
|
"templates",
|
|
7610
7985
|
"agentic-workflow-standard.md"
|
|
7611
7986
|
);
|
|
7612
|
-
const templatePath = await
|
|
7987
|
+
const templatePath = await fs37.pathExists(cachePath) ? cachePath : bundledPath;
|
|
7613
7988
|
const templateContent = await readText(templatePath);
|
|
7614
7989
|
const verdict = checkManagedTamper(bodyContent, storedHashMatch?.[1], templateContent ?? null);
|
|
7615
7990
|
if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
|
|
@@ -7637,7 +8012,7 @@ async function runDoctor(options) {
|
|
|
7637
8012
|
}
|
|
7638
8013
|
}
|
|
7639
8014
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
7640
|
-
const workflowConfigExists = await
|
|
8015
|
+
const workflowConfigExists = await fs37.pathExists(workflowConfigPath);
|
|
7641
8016
|
if (!workflowConfigExists) {
|
|
7642
8017
|
flag(
|
|
7643
8018
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -7645,7 +8020,7 @@ async function runDoctor(options) {
|
|
|
7645
8020
|
"haus apply --write"
|
|
7646
8021
|
);
|
|
7647
8022
|
} else {
|
|
7648
|
-
const cfg = await
|
|
8023
|
+
const cfg = await fs37.readFile(workflowConfigPath, "utf8");
|
|
7649
8024
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
7650
8025
|
if (unfilled > 0) {
|
|
7651
8026
|
suggest(
|
|
@@ -7699,8 +8074,10 @@ async function runDoctor(options) {
|
|
|
7699
8074
|
else suggest(finding.line, finding.sentence, finding.fix);
|
|
7700
8075
|
}
|
|
7701
8076
|
let catalogItems = null;
|
|
8077
|
+
let catalogCtx = null;
|
|
7702
8078
|
try {
|
|
7703
|
-
|
|
8079
|
+
catalogCtx = await loadCatalogContext(root);
|
|
8080
|
+
catalogItems = catalogCtx.items;
|
|
7704
8081
|
} catch (err) {
|
|
7705
8082
|
if (!(err instanceof UnsupportedManifestFormatError)) throw err;
|
|
7706
8083
|
flag(
|
|
@@ -7793,6 +8170,31 @@ async function runDoctor(options) {
|
|
|
7793
8170
|
);
|
|
7794
8171
|
}
|
|
7795
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
|
+
}
|
|
7796
8198
|
const cacheAgeMs = await getCacheManifestAge();
|
|
7797
8199
|
if (cacheAgeMs === null) {
|
|
7798
8200
|
suggest(
|
|
@@ -7886,7 +8288,7 @@ async function runDoctor(options) {
|
|
|
7886
8288
|
let onDisk = false;
|
|
7887
8289
|
for (const rel of row.paths ?? []) {
|
|
7888
8290
|
const abs = resolveContainedPath(root, rel);
|
|
7889
|
-
if (abs !== null && await
|
|
8291
|
+
if (abs !== null && await fs37.pathExists(abs)) {
|
|
7890
8292
|
onDisk = true;
|
|
7891
8293
|
break;
|
|
7892
8294
|
}
|
|
@@ -7918,7 +8320,21 @@ async function runDoctor(options) {
|
|
|
7918
8320
|
}
|
|
7919
8321
|
if (attention.length > 0) process.exitCode = 1;
|
|
7920
8322
|
if (options?.json) {
|
|
7921
|
-
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
|
+
);
|
|
7922
8338
|
return;
|
|
7923
8339
|
}
|
|
7924
8340
|
if (attention.length === 0) {
|
|
@@ -7943,11 +8359,11 @@ import path48 from "node:path";
|
|
|
7943
8359
|
init_github_rate_limit();
|
|
7944
8360
|
|
|
7945
8361
|
// src/claude/refresh-project.ts
|
|
7946
|
-
import
|
|
8362
|
+
import fs38 from "fs-extra";
|
|
7947
8363
|
init_fs();
|
|
7948
8364
|
init_paths();
|
|
7949
8365
|
async function isHausProject(root) {
|
|
7950
|
-
if (await
|
|
8366
|
+
if (await fs38.pathExists(hausPath(root, "recommendation.json"))) return true;
|
|
7951
8367
|
const result = await readJsonDetailed(claudePath(root, "settings.json"));
|
|
7952
8368
|
if (result.status === "ok") return result.value._haus != null;
|
|
7953
8369
|
return false;
|
|
@@ -7964,7 +8380,7 @@ async function refreshProjectApply(root) {
|
|
|
7964
8380
|
// src/install/apply.ts
|
|
7965
8381
|
import crypto2 from "node:crypto";
|
|
7966
8382
|
import path46 from "node:path";
|
|
7967
|
-
import
|
|
8383
|
+
import fs39 from "fs-extra";
|
|
7968
8384
|
init_fs();
|
|
7969
8385
|
init_logger();
|
|
7970
8386
|
init_paths();
|
|
@@ -8053,7 +8469,7 @@ function hashContent(content2) {
|
|
|
8053
8469
|
}
|
|
8054
8470
|
function destFileMissing(destPath) {
|
|
8055
8471
|
try {
|
|
8056
|
-
return !
|
|
8472
|
+
return !fs39.statSync(destPath).isFile();
|
|
8057
8473
|
} catch {
|
|
8058
8474
|
return true;
|
|
8059
8475
|
}
|
|
@@ -8061,7 +8477,7 @@ function destFileMissing(destPath) {
|
|
|
8061
8477
|
function sourceVersion() {
|
|
8062
8478
|
try {
|
|
8063
8479
|
const pkgPath = path46.join(packageRoot(), "package.json");
|
|
8064
|
-
const pkg = JSON.parse(
|
|
8480
|
+
const pkg = JSON.parse(fs39.readFileSync(pkgPath, "utf8"));
|
|
8065
8481
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
8066
8482
|
} catch {
|
|
8067
8483
|
return "haus@0.0.0";
|
|
@@ -8073,10 +8489,10 @@ function globalSrcDir() {
|
|
|
8073
8489
|
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
8074
8490
|
const skillDir = path46.join(skillsDir, skillName);
|
|
8075
8491
|
const entries = [];
|
|
8076
|
-
for (const rel of
|
|
8492
|
+
for (const rel of fs39.readdirSync(skillDir, { recursive: true })) {
|
|
8077
8493
|
if (rel === "SKILL.md") continue;
|
|
8078
8494
|
const abs = path46.join(skillDir, rel);
|
|
8079
|
-
if (!
|
|
8495
|
+
if (!fs39.statSync(abs).isFile()) continue;
|
|
8080
8496
|
const posixRel = rel.split(path46.sep).join("/");
|
|
8081
8497
|
entries.push({
|
|
8082
8498
|
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
@@ -8089,10 +8505,10 @@ function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
|
8089
8505
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
8090
8506
|
const entries = [];
|
|
8091
8507
|
const skillsDir = path46.join(srcDir, "skills");
|
|
8092
|
-
if (
|
|
8093
|
-
for (const skillName of
|
|
8508
|
+
if (fs39.pathExistsSync(skillsDir)) {
|
|
8509
|
+
for (const skillName of fs39.readdirSync(skillsDir)) {
|
|
8094
8510
|
const skillFile = path46.join(skillsDir, skillName, "SKILL.md");
|
|
8095
|
-
if (
|
|
8511
|
+
if (fs39.pathExistsSync(skillFile)) {
|
|
8096
8512
|
entries.push({
|
|
8097
8513
|
stableId: `skill.${skillName}`,
|
|
8098
8514
|
srcRelPath: path46.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
@@ -8103,8 +8519,8 @@ function collectSourceFiles(srcDir, claudeDir) {
|
|
|
8103
8519
|
}
|
|
8104
8520
|
}
|
|
8105
8521
|
const commandsDir = path46.join(srcDir, "commands");
|
|
8106
|
-
if (
|
|
8107
|
-
for (const fileName of
|
|
8522
|
+
if (fs39.pathExistsSync(commandsDir)) {
|
|
8523
|
+
for (const fileName of fs39.readdirSync(commandsDir)) {
|
|
8108
8524
|
if (!fileName.endsWith(".md")) continue;
|
|
8109
8525
|
const commandName = fileName.slice(0, -".md".length);
|
|
8110
8526
|
entries.push({
|
|
@@ -8163,7 +8579,7 @@ async function applyInstall(options = {}) {
|
|
|
8163
8579
|
}
|
|
8164
8580
|
continue;
|
|
8165
8581
|
}
|
|
8166
|
-
const destExists =
|
|
8582
|
+
const destExists = fs39.pathExistsSync(entry.destPath);
|
|
8167
8583
|
if (destExists) {
|
|
8168
8584
|
const currentContent = await readText(entry.destPath);
|
|
8169
8585
|
if (currentContent !== void 0) {
|
|
@@ -8222,13 +8638,13 @@ async function applyInstall(options = {}) {
|
|
|
8222
8638
|
manifestFiles.push(entry);
|
|
8223
8639
|
continue;
|
|
8224
8640
|
}
|
|
8225
|
-
if (!
|
|
8641
|
+
if (!fs39.pathExistsSync(entry.destPath)) continue;
|
|
8226
8642
|
const content2 = await readText(entry.destPath);
|
|
8227
8643
|
if (!content2) continue;
|
|
8228
8644
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
8229
8645
|
const currentHash = hashContent(content2);
|
|
8230
8646
|
if (hasHeader && currentHash === entry.hash) {
|
|
8231
|
-
if (!dryRun) await
|
|
8647
|
+
if (!dryRun) await fs39.remove(entry.destPath);
|
|
8232
8648
|
result.deleted.push(entry.destPath);
|
|
8233
8649
|
} else {
|
|
8234
8650
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -8342,7 +8758,7 @@ async function backupLockfile(root) {
|
|
|
8342
8758
|
await mkdir(backupDir, { recursive: true });
|
|
8343
8759
|
let stamp = Date.now();
|
|
8344
8760
|
for (; ; ) {
|
|
8345
|
-
const dest = path47.join(backupDir,
|
|
8761
|
+
const dest = path47.join(backupDir, lockBackupFileName(stamp));
|
|
8346
8762
|
try {
|
|
8347
8763
|
await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
|
|
8348
8764
|
return;
|
|
@@ -8823,7 +9239,7 @@ async function runClone(url, opts = {}) {
|
|
|
8823
9239
|
// src/install/scaffold.ts
|
|
8824
9240
|
init_logger();
|
|
8825
9241
|
import path50 from "node:path";
|
|
8826
|
-
import
|
|
9242
|
+
import fs40 from "fs-extra";
|
|
8827
9243
|
async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
8828
9244
|
const scaffolded = [];
|
|
8829
9245
|
const skipped = [];
|
|
@@ -8836,7 +9252,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8836
9252
|
skipped.push(name);
|
|
8837
9253
|
if (outcome === "skipped-exists") skippedExisting.push(name);
|
|
8838
9254
|
};
|
|
8839
|
-
const realRoot = await
|
|
9255
|
+
const realRoot = await fs40.realpath(catalogRoot).catch(() => null);
|
|
8840
9256
|
for (const item of items) {
|
|
8841
9257
|
if (item.type !== "config") continue;
|
|
8842
9258
|
const sourcePath = path50.resolve(catalogRoot, item.path);
|
|
@@ -8846,7 +9262,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8846
9262
|
warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
|
|
8847
9263
|
continue;
|
|
8848
9264
|
}
|
|
8849
|
-
const realSource = await
|
|
9265
|
+
const realSource = await fs40.realpath(sourcePath).catch(() => null);
|
|
8850
9266
|
if (!realRoot || !realSource) {
|
|
8851
9267
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
8852
9268
|
continue;
|
|
@@ -8856,7 +9272,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8856
9272
|
warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
|
|
8857
9273
|
continue;
|
|
8858
9274
|
}
|
|
8859
|
-
const stat = await
|
|
9275
|
+
const stat = await fs40.lstat(sourcePath).catch(() => null);
|
|
8860
9276
|
if (!stat) {
|
|
8861
9277
|
warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
|
|
8862
9278
|
continue;
|
|
@@ -8870,7 +9286,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8870
9286
|
const dest = path50.join(projectRoot, filename);
|
|
8871
9287
|
record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
|
|
8872
9288
|
} else if (stat.isDirectory()) {
|
|
8873
|
-
const entries = await
|
|
9289
|
+
const entries = await fs40.readdir(sourcePath);
|
|
8874
9290
|
for (const entry of entries) {
|
|
8875
9291
|
const src = path50.join(sourcePath, entry);
|
|
8876
9292
|
const dest = path50.join(projectRoot, entry);
|
|
@@ -8882,7 +9298,7 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
|
|
|
8882
9298
|
}
|
|
8883
9299
|
async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
8884
9300
|
const destDisplay = path50.relative(projectRoot, dest) || path50.basename(dest);
|
|
8885
|
-
const srcStat = await
|
|
9301
|
+
const srcStat = await fs40.lstat(src).catch(() => null);
|
|
8886
9302
|
if (!srcStat) {
|
|
8887
9303
|
warn(`Skipping ${path50.basename(src)}: source not found`);
|
|
8888
9304
|
return "skipped-refused";
|
|
@@ -8891,7 +9307,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
8891
9307
|
warn(`Skipping ${path50.basename(src)}: source is a symlink`);
|
|
8892
9308
|
return "skipped-refused";
|
|
8893
9309
|
}
|
|
8894
|
-
const exists = await
|
|
9310
|
+
const exists = await fs40.pathExists(dest);
|
|
8895
9311
|
if (exists && !opts.force) {
|
|
8896
9312
|
warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
|
|
8897
9313
|
return "skipped-exists";
|
|
@@ -8901,16 +9317,16 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
|
|
|
8901
9317
|
return "scaffolded";
|
|
8902
9318
|
}
|
|
8903
9319
|
if (exists && opts.force) {
|
|
8904
|
-
await
|
|
9320
|
+
await fs40.remove(dest);
|
|
8905
9321
|
}
|
|
8906
|
-
await
|
|
8907
|
-
await
|
|
9322
|
+
await fs40.ensureDir(path50.dirname(dest));
|
|
9323
|
+
await fs40.copy(src, dest, {
|
|
8908
9324
|
overwrite: true,
|
|
8909
9325
|
dereference: false,
|
|
8910
9326
|
// A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
|
|
8911
9327
|
filter: (s) => {
|
|
8912
9328
|
try {
|
|
8913
|
-
return !
|
|
9329
|
+
return !fs40.lstatSync(s).isSymbolicLink();
|
|
8914
9330
|
} catch {
|
|
8915
9331
|
return false;
|
|
8916
9332
|
}
|
|
@@ -9432,6 +9848,61 @@ async function runGuard(kind, _options) {
|
|
|
9432
9848
|
// src/claude/setup-core.ts
|
|
9433
9849
|
init_github_rate_limit();
|
|
9434
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
|
+
|
|
9435
9906
|
// src/recommender/recommend.ts
|
|
9436
9907
|
init_fs();
|
|
9437
9908
|
init_paths();
|
|
@@ -9478,6 +9949,7 @@ async function readChangedFiles(root) {
|
|
|
9478
9949
|
async function recommend(root, context, opts = {}) {
|
|
9479
9950
|
const activePluginIds = opts.activePluginIds ?? [];
|
|
9480
9951
|
const items = await loadCatalog(root);
|
|
9952
|
+
const excludedIds = await readProjectExcludedIds(root);
|
|
9481
9953
|
const sources = buildSourcesReport(items);
|
|
9482
9954
|
const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
|
|
9483
9955
|
const scannerStacks = buildStackSet(context);
|
|
@@ -9687,6 +10159,7 @@ async function recommend(root, context, opts = {}) {
|
|
|
9687
10159
|
}
|
|
9688
10160
|
}
|
|
9689
10161
|
applyCoInstallSuppression(recommended, skipped);
|
|
10162
|
+
applyManualExcludes(excludedIds, items, recommended, skipped);
|
|
9690
10163
|
const includeWarnings = applyManualIncludes(opts.include ?? [], items, recommended, skipped);
|
|
9691
10164
|
const optInEligible = buildOptInEligible(items, skipped);
|
|
9692
10165
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -9715,6 +10188,29 @@ function buildStackSet(context) {
|
|
|
9715
10188
|
)
|
|
9716
10189
|
);
|
|
9717
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
|
+
}
|
|
9718
10214
|
function applyCoInstallSuppression(recommended, skipped) {
|
|
9719
10215
|
const recommendedIds = new Set(recommended.map((r) => r.id));
|
|
9720
10216
|
const rules = [
|
|
@@ -10087,7 +10583,10 @@ async function runSetupCore(root, opts) {
|
|
|
10087
10583
|
recommendedCount: 0,
|
|
10088
10584
|
warnings,
|
|
10089
10585
|
hooksOk: false,
|
|
10090
|
-
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
|
|
10091
10590
|
};
|
|
10092
10591
|
}
|
|
10093
10592
|
await writeJson(hausPath(root, "recommendation.json"), recommendation);
|
|
@@ -10116,7 +10615,8 @@ async function runSetupCore(root, opts) {
|
|
|
10116
10615
|
recommendedCount: recommendation.recommended.length,
|
|
10117
10616
|
warnings: warningLines,
|
|
10118
10617
|
hooksOk,
|
|
10119
|
-
written: []
|
|
10618
|
+
written: [],
|
|
10619
|
+
catalogInstallOk: true
|
|
10120
10620
|
};
|
|
10121
10621
|
const envelope = (hooksResult) => ({
|
|
10122
10622
|
scan: scanResult,
|
|
@@ -10156,12 +10656,66 @@ async function runSetupCore(root, opts) {
|
|
|
10156
10656
|
warn(formatGithubRateLimitMessage(sync.rateLimit));
|
|
10157
10657
|
}
|
|
10158
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
|
+
}
|
|
10159
10696
|
const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
|
|
10160
10697
|
quiet: opts.quiet,
|
|
10161
10698
|
activePluginIds
|
|
10162
10699
|
});
|
|
10163
10700
|
say("Applied files:");
|
|
10164
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
|
+
}
|
|
10165
10719
|
const hooksAfter = await verifyProjectSettingsHooksContract(root);
|
|
10166
10720
|
const hooksOkAfter = hooksAfter.ok;
|
|
10167
10721
|
if (hooksAfter.skipped) {
|
|
@@ -10173,7 +10727,7 @@ async function runSetupCore(root, opts) {
|
|
|
10173
10727
|
say(`- HOOKS OK: ${hooksAfter.message}`);
|
|
10174
10728
|
}
|
|
10175
10729
|
emitJsonEnvelope(json, quiet, envelope(hooksAfter));
|
|
10176
|
-
return { ...baseResult, hooksOk: hooksOkAfter, written: files };
|
|
10730
|
+
return { ...baseResult, hooksOk: hooksOkAfter, written: files, catalogInstallOk };
|
|
10177
10731
|
}
|
|
10178
10732
|
|
|
10179
10733
|
// src/commands/init.ts
|
|
@@ -10193,6 +10747,7 @@ async function runInit(options) {
|
|
|
10193
10747
|
apply: !dryRun,
|
|
10194
10748
|
dryRun: false,
|
|
10195
10749
|
force: options.force,
|
|
10750
|
+
allowEmptyCache: options.allowEmptyCache,
|
|
10196
10751
|
confirm: () => confirm("Approve and write Claude files now?")
|
|
10197
10752
|
});
|
|
10198
10753
|
}
|
|
@@ -10323,7 +10878,7 @@ async function runInstall(options, deps = {}) {
|
|
|
10323
10878
|
|
|
10324
10879
|
// src/new-project/create-project.ts
|
|
10325
10880
|
import path53 from "node:path";
|
|
10326
|
-
import
|
|
10881
|
+
import fs42 from "fs-extra";
|
|
10327
10882
|
init_exec();
|
|
10328
10883
|
init_logger();
|
|
10329
10884
|
|
|
@@ -10477,7 +11032,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10477
11032
|
`Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
|
|
10478
11033
|
);
|
|
10479
11034
|
}
|
|
10480
|
-
if (await
|
|
11035
|
+
if (await fs42.pathExists(projectPath)) {
|
|
10481
11036
|
return fail("preflight", `Target ${projectPath} already exists \u2014 refusing to write into it.`);
|
|
10482
11037
|
}
|
|
10483
11038
|
const scaffoldArgs = stack.scaffold.args.map(
|
|
@@ -10555,15 +11110,15 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10555
11110
|
steps.push({ step: "scaffold", status: "done" });
|
|
10556
11111
|
phase = "git";
|
|
10557
11112
|
if (!opts.json) log("\u25B6 git: init/branch/commit");
|
|
10558
|
-
if (!await
|
|
11113
|
+
if (!await fs42.pathExists(path53.join(projectPath, ".git"))) {
|
|
10559
11114
|
const init = await deps.git(["init"], { cwd: projectPath });
|
|
10560
11115
|
if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
|
|
10561
11116
|
}
|
|
10562
11117
|
const branch = await deps.git(["branch", "-M", "main"], { cwd: projectPath });
|
|
10563
11118
|
if (branch.exitCode !== 0) return fail("git", `git branch -M main failed: ${branch.stderr}`);
|
|
10564
11119
|
const lockfile = opts.pm === "yarn" ? "yarn.lock" : opts.pm === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json";
|
|
10565
|
-
if (!await
|
|
10566
|
-
await
|
|
11120
|
+
if (!await fs42.pathExists(path53.join(projectPath, lockfile))) {
|
|
11121
|
+
await fs42.writeFile(path53.join(projectPath, lockfile), "");
|
|
10567
11122
|
}
|
|
10568
11123
|
const add = await deps.git(["add", "-A"], { cwd: projectPath });
|
|
10569
11124
|
if (add.exitCode !== 0) return fail("git", `git add failed: ${add.stderr}`);
|
|
@@ -10931,6 +11486,10 @@ async function runRemove(ids, options = {}) {
|
|
|
10931
11486
|
root,
|
|
10932
11487
|
cleared.map((c) => c.id)
|
|
10933
11488
|
);
|
|
11489
|
+
await addProjectExcludedIds(
|
|
11490
|
+
root,
|
|
11491
|
+
cleared.map((c) => c.id)
|
|
11492
|
+
);
|
|
10934
11493
|
try {
|
|
10935
11494
|
const remaining = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
10936
11495
|
const remainingIds = new Set(remaining.map((e) => e.id));
|
|
@@ -10946,7 +11505,6 @@ async function runRemove(ids, options = {}) {
|
|
|
10946
11505
|
}
|
|
10947
11506
|
if (!options.json) {
|
|
10948
11507
|
log(`Removed ${report.removed.length} item(s). Restore with \`haus backups list\`.`);
|
|
10949
|
-
log("Note: a later `haus apply --write` reinstalls any of these still recommended.");
|
|
10950
11508
|
}
|
|
10951
11509
|
emitReport(report, options.json);
|
|
10952
11510
|
}
|
|
@@ -11009,7 +11567,7 @@ async function runScan(options) {
|
|
|
11009
11567
|
|
|
11010
11568
|
// src/commands/undo.ts
|
|
11011
11569
|
import path55 from "node:path";
|
|
11012
|
-
import
|
|
11570
|
+
import fs43 from "fs-extra";
|
|
11013
11571
|
|
|
11014
11572
|
// src/claude/managed-paths.ts
|
|
11015
11573
|
init_paths();
|
|
@@ -11028,7 +11586,7 @@ init_paths();
|
|
|
11028
11586
|
async function collectCoreManagedPaths(root) {
|
|
11029
11587
|
const existing = [];
|
|
11030
11588
|
for (const abs of coreManagedAbsolutePaths(root)) {
|
|
11031
|
-
if (await
|
|
11589
|
+
if (await fs43.pathExists(abs)) existing.push(abs);
|
|
11032
11590
|
}
|
|
11033
11591
|
return existing;
|
|
11034
11592
|
}
|
|
@@ -11037,41 +11595,35 @@ async function collectLockTrackedPaths(root) {
|
|
|
11037
11595
|
const removable = [];
|
|
11038
11596
|
const preserved = [];
|
|
11039
11597
|
for (const row of lock ?? []) {
|
|
11040
|
-
const
|
|
11041
|
-
const
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
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(", ")}`);
|
|
11048
11606
|
continue;
|
|
11049
11607
|
}
|
|
11050
|
-
|
|
11051
|
-
if (currentHash === row.hash) {
|
|
11052
|
-
removable.push(...existingRel.map((rel) => path55.resolve(root, rel)));
|
|
11053
|
-
} else {
|
|
11054
|
-
preserved.push(...existingRel.map((rel) => path55.resolve(root, rel)));
|
|
11055
|
-
log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
|
|
11056
|
-
}
|
|
11608
|
+
removable.push({ id, existing });
|
|
11057
11609
|
}
|
|
11058
11610
|
return { removable, preserved };
|
|
11059
11611
|
}
|
|
11060
11612
|
async function settingsHasHausContent(root) {
|
|
11061
11613
|
const settingsPath = claudePath(root, "settings.json");
|
|
11062
|
-
if (!await
|
|
11614
|
+
if (!await fs43.pathExists(settingsPath)) return false;
|
|
11063
11615
|
const settings = await readProjectSettings(root);
|
|
11064
11616
|
return settings._haus != null;
|
|
11065
11617
|
}
|
|
11066
11618
|
async function claudeMdHasHausBlock(root) {
|
|
11067
11619
|
const filePath = path55.join(root, "CLAUDE.md");
|
|
11068
|
-
if (!await
|
|
11069
|
-
const text = await
|
|
11620
|
+
if (!await fs43.pathExists(filePath)) return false;
|
|
11621
|
+
const text = await fs43.readFile(filePath, "utf8");
|
|
11070
11622
|
return text.includes(BLOCK_BEGIN);
|
|
11071
11623
|
}
|
|
11072
11624
|
async function stripProjectSettings(root) {
|
|
11073
11625
|
const settingsPath = claudePath(root, "settings.json");
|
|
11074
|
-
if (!await
|
|
11626
|
+
if (!await fs43.pathExists(settingsPath)) return false;
|
|
11075
11627
|
let settings = await readProjectSettings(root);
|
|
11076
11628
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
11077
11629
|
const hasContent = Object.keys(settings).length > 0;
|
|
@@ -11080,43 +11632,43 @@ async function stripProjectSettings(root) {
|
|
|
11080
11632
|
log(`Stripped haus rules from ${path55.relative(root, settingsPath)} (user settings preserved).`);
|
|
11081
11633
|
return true;
|
|
11082
11634
|
}
|
|
11083
|
-
await
|
|
11635
|
+
await fs43.remove(settingsPath);
|
|
11084
11636
|
log(`Removed ${path55.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
11085
11637
|
return true;
|
|
11086
11638
|
}
|
|
11087
11639
|
async function stripRootClaudeMd(root) {
|
|
11088
11640
|
const filePath = path55.join(root, "CLAUDE.md");
|
|
11089
|
-
if (!await
|
|
11090
|
-
const prev = await
|
|
11641
|
+
if (!await fs43.pathExists(filePath)) return false;
|
|
11642
|
+
const prev = await fs43.readFile(filePath, "utf8");
|
|
11091
11643
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
11092
11644
|
const next = stripHausBlock(prev);
|
|
11093
11645
|
if (next.length === 0) {
|
|
11094
|
-
await
|
|
11646
|
+
await fs43.remove(filePath);
|
|
11095
11647
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
11096
11648
|
} else {
|
|
11097
|
-
await
|
|
11649
|
+
await fs43.writeFile(filePath, next, "utf8");
|
|
11098
11650
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
11099
11651
|
}
|
|
11100
11652
|
return true;
|
|
11101
11653
|
}
|
|
11102
11654
|
async function pruneDirIfEmpty(dir) {
|
|
11103
|
-
if (!await
|
|
11655
|
+
if (!await fs43.pathExists(dir)) return;
|
|
11104
11656
|
try {
|
|
11105
|
-
const entries = await
|
|
11106
|
-
if (entries.length === 0) await
|
|
11657
|
+
const entries = await fs43.readdir(dir);
|
|
11658
|
+
if (entries.length === 0) await fs43.remove(dir);
|
|
11107
11659
|
} catch {
|
|
11108
11660
|
}
|
|
11109
11661
|
}
|
|
11110
11662
|
async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
11111
11663
|
if (managedAbsPaths.length === 0) return;
|
|
11112
11664
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
11113
|
-
const backupRoot = hausPath(root, "backups",
|
|
11665
|
+
const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
|
|
11114
11666
|
for (const abs of managedAbsPaths) {
|
|
11115
|
-
if (!await
|
|
11667
|
+
if (!await fs43.pathExists(abs)) continue;
|
|
11116
11668
|
const rel = path55.relative(root, abs);
|
|
11117
11669
|
const backupPath = path55.join(backupRoot, rel);
|
|
11118
|
-
await
|
|
11119
|
-
await
|
|
11670
|
+
await fs43.ensureDir(path55.dirname(backupPath));
|
|
11671
|
+
await fs43.copy(abs, backupPath);
|
|
11120
11672
|
}
|
|
11121
11673
|
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path55.relative(root, backupRoot)}.`);
|
|
11122
11674
|
}
|
|
@@ -11124,7 +11676,8 @@ async function runUndo(options) {
|
|
|
11124
11676
|
const root = options.root ?? process.cwd();
|
|
11125
11677
|
const coreManaged = await collectCoreManagedPaths(root);
|
|
11126
11678
|
const { removable: lockRemovable, preserved: lockPreserved } = await collectLockTrackedPaths(root);
|
|
11127
|
-
const
|
|
11679
|
+
const lockAbs = lockRemovable.flatMap(({ existing }) => existing.map((e) => e.abs));
|
|
11680
|
+
const managed = [.../* @__PURE__ */ new Set([...coreManaged, ...lockAbs])];
|
|
11128
11681
|
const stripSettings = await settingsHasHausContent(root);
|
|
11129
11682
|
const stripClaudeMd = await claudeMdHasHausBlock(root);
|
|
11130
11683
|
if (managed.length === 0 && !stripSettings && !stripClaudeMd) {
|
|
@@ -11136,9 +11689,7 @@ async function runUndo(options) {
|
|
|
11136
11689
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
11137
11690
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
11138
11691
|
if (lockPreserved.length > 0) {
|
|
11139
|
-
summaryParts.push(
|
|
11140
|
-
`(preserving ${lockPreserved.length} locally-modified catalog item file(s) \u2014 not shown above)`
|
|
11141
|
-
);
|
|
11692
|
+
summaryParts.push(`(preserving ${lockPreserved.length} catalog item file(s) \u2014 not shown above)`);
|
|
11142
11693
|
}
|
|
11143
11694
|
if (options.dryRun) {
|
|
11144
11695
|
log(`[dry-run] Would remove:
|
|
@@ -11158,11 +11709,14 @@ User-owned .claude/ files will be preserved.`
|
|
|
11158
11709
|
}
|
|
11159
11710
|
}
|
|
11160
11711
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
11161
|
-
for (const abs of
|
|
11162
|
-
if (!await
|
|
11163
|
-
await
|
|
11712
|
+
for (const abs of coreManaged) {
|
|
11713
|
+
if (!await fs43.pathExists(abs)) continue;
|
|
11714
|
+
await fs43.remove(abs);
|
|
11164
11715
|
log(`Removed ${path55.relative(root, abs)}`);
|
|
11165
11716
|
}
|
|
11717
|
+
for (const { id, existing } of lockRemovable) {
|
|
11718
|
+
await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
|
|
11719
|
+
}
|
|
11166
11720
|
if (stripSettings) await stripProjectSettings(root);
|
|
11167
11721
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
11168
11722
|
await pruneDirIfEmpty(claudePath(root));
|
|
@@ -11176,7 +11730,7 @@ init_logger();
|
|
|
11176
11730
|
init_paths();
|
|
11177
11731
|
import crypto3 from "node:crypto";
|
|
11178
11732
|
import path56 from "node:path";
|
|
11179
|
-
import
|
|
11733
|
+
import fs44 from "fs-extra";
|
|
11180
11734
|
async function runUninstall(options = {}) {
|
|
11181
11735
|
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
11182
11736
|
const manifest = await readManifest();
|
|
@@ -11213,7 +11767,7 @@ async function runUninstall(options = {}) {
|
|
|
11213
11767
|
);
|
|
11214
11768
|
}
|
|
11215
11769
|
for (const entry of manifest.files) {
|
|
11216
|
-
const exists =
|
|
11770
|
+
const exists = fs44.pathExistsSync(entry.destPath);
|
|
11217
11771
|
if (!exists) continue;
|
|
11218
11772
|
const content2 = await readText(entry.destPath);
|
|
11219
11773
|
if (content2 === void 0) continue;
|
|
@@ -11231,7 +11785,7 @@ async function runUninstall(options = {}) {
|
|
|
11231
11785
|
result.skipped.push(entry.destPath);
|
|
11232
11786
|
continue;
|
|
11233
11787
|
}
|
|
11234
|
-
await
|
|
11788
|
+
await fs44.remove(entry.destPath);
|
|
11235
11789
|
await pruneEmptyDir(path56.dirname(entry.destPath));
|
|
11236
11790
|
result.deleted.push(entry.destPath);
|
|
11237
11791
|
}
|
|
@@ -11241,13 +11795,13 @@ async function runUninstall(options = {}) {
|
|
|
11241
11795
|
result.hooksStripped = true;
|
|
11242
11796
|
const hausDir = path56.join(globalClaudeDir(), "haus");
|
|
11243
11797
|
const manifestPath2 = hausManifestPath();
|
|
11244
|
-
if (
|
|
11245
|
-
await
|
|
11798
|
+
if (fs44.pathExistsSync(manifestPath2)) {
|
|
11799
|
+
await fs44.remove(manifestPath2);
|
|
11246
11800
|
}
|
|
11247
|
-
if (
|
|
11801
|
+
if (fs44.pathExistsSync(hausDir)) {
|
|
11248
11802
|
try {
|
|
11249
|
-
const remaining = await
|
|
11250
|
-
if (remaining.length === 0) await
|
|
11803
|
+
const remaining = await fs44.readdir(hausDir);
|
|
11804
|
+
if (remaining.length === 0) await fs44.remove(hausDir);
|
|
11251
11805
|
} catch {
|
|
11252
11806
|
}
|
|
11253
11807
|
}
|
|
@@ -11323,7 +11877,7 @@ init_forbidden_content();
|
|
|
11323
11877
|
init_manifest_item_fields();
|
|
11324
11878
|
init_path_safety();
|
|
11325
11879
|
init_validation_rules2();
|
|
11326
|
-
import
|
|
11880
|
+
import fs45 from "node:fs";
|
|
11327
11881
|
import path57 from "node:path";
|
|
11328
11882
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
11329
11883
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
@@ -11401,11 +11955,11 @@ function auditOptInMetadata(items) {
|
|
|
11401
11955
|
}
|
|
11402
11956
|
return failures;
|
|
11403
11957
|
}
|
|
11404
|
-
function auditManifestStructure(
|
|
11958
|
+
function auditManifestStructure(manifestVersion2, items) {
|
|
11405
11959
|
const failures = [];
|
|
11406
11960
|
const seenIds = /* @__PURE__ */ new Map();
|
|
11407
11961
|
const seenPaths = /* @__PURE__ */ new Map();
|
|
11408
|
-
if (typeof
|
|
11962
|
+
if (typeof manifestVersion2 !== "string" || !ITEM_SEMVER_RE.test(manifestVersion2)) {
|
|
11409
11963
|
failures.push('manifest.json: top-level "version" is not valid semver (expected X.Y.Z)');
|
|
11410
11964
|
}
|
|
11411
11965
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -11468,7 +12022,7 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
11468
12022
|
}
|
|
11469
12023
|
function readForAudit(absPath, label, failures) {
|
|
11470
12024
|
try {
|
|
11471
|
-
return
|
|
12025
|
+
return fs45.readFileSync(absPath, "utf8");
|
|
11472
12026
|
} catch (err) {
|
|
11473
12027
|
failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
11474
12028
|
return null;
|
|
@@ -11500,7 +12054,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11500
12054
|
const absPath = path57.join(manifestDir, item.path);
|
|
11501
12055
|
if (item.type === "skill") {
|
|
11502
12056
|
const skillMd = path57.join(absPath, "SKILL.md");
|
|
11503
|
-
if (!
|
|
12057
|
+
if (!fs45.existsSync(skillMd)) {
|
|
11504
12058
|
failures.push(`${item.id}: missing ${path57.relative(manifestDir, skillMd)}`);
|
|
11505
12059
|
continue;
|
|
11506
12060
|
}
|
|
@@ -11511,7 +12065,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11511
12065
|
...auditForbiddenTagsInText(text, `${item.id}: ${path57.relative(manifestDir, skillMd)}`)
|
|
11512
12066
|
);
|
|
11513
12067
|
} else if (item.type === "agent") {
|
|
11514
|
-
if (!
|
|
12068
|
+
if (!fs45.existsSync(absPath)) {
|
|
11515
12069
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
11516
12070
|
continue;
|
|
11517
12071
|
}
|
|
@@ -11521,13 +12075,13 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11521
12075
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
11522
12076
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
11523
12077
|
} else if (item.type === "template") {
|
|
11524
|
-
if (!
|
|
12078
|
+
if (!fs45.existsSync(absPath)) {
|
|
11525
12079
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
11526
12080
|
continue;
|
|
11527
12081
|
}
|
|
11528
12082
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
11529
12083
|
} else if (item.type === "command") {
|
|
11530
|
-
if (!
|
|
12084
|
+
if (!fs45.existsSync(absPath)) {
|
|
11531
12085
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
11532
12086
|
continue;
|
|
11533
12087
|
}
|
|
@@ -11537,15 +12091,15 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11537
12091
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
11538
12092
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
11539
12093
|
} else if (item.type === "config") {
|
|
11540
|
-
if (!
|
|
12094
|
+
if (!fs45.existsSync(absPath)) {
|
|
11541
12095
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
11542
12096
|
}
|
|
11543
12097
|
} else if (item.type === "rule") {
|
|
11544
|
-
if (!
|
|
12098
|
+
if (!fs45.existsSync(absPath)) {
|
|
11545
12099
|
failures.push(`${item.id}: missing rule file ${item.path}`);
|
|
11546
12100
|
}
|
|
11547
12101
|
} else if (item.type === "asset") {
|
|
11548
|
-
if (!
|
|
12102
|
+
if (!fs45.existsSync(absPath)) {
|
|
11549
12103
|
failures.push(`${item.id}: missing asset file ${item.path}`);
|
|
11550
12104
|
}
|
|
11551
12105
|
}
|
|
@@ -11580,7 +12134,7 @@ function resolveItemType(relPath, pathTypeMap) {
|
|
|
11580
12134
|
function walkAllFiles(dir, fn, failures) {
|
|
11581
12135
|
let entries;
|
|
11582
12136
|
try {
|
|
11583
|
-
entries =
|
|
12137
|
+
entries = fs45.readdirSync(dir, { withFileTypes: true });
|
|
11584
12138
|
} catch (err) {
|
|
11585
12139
|
failures.push(
|
|
11586
12140
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -11614,7 +12168,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
11614
12168
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
11615
12169
|
for (const dir of CONTENT_ROOTS) {
|
|
11616
12170
|
const abs = path57.join(manifestDir, dir);
|
|
11617
|
-
if (!
|
|
12171
|
+
if (!fs45.existsSync(abs)) continue;
|
|
11618
12172
|
walkAllFiles(
|
|
11619
12173
|
abs,
|
|
11620
12174
|
(file) => {
|
|
@@ -11631,7 +12185,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
11631
12185
|
function walkMd(dir, fn, failures) {
|
|
11632
12186
|
let entries;
|
|
11633
12187
|
try {
|
|
11634
|
-
entries =
|
|
12188
|
+
entries = fs45.readdirSync(dir, { withFileTypes: true });
|
|
11635
12189
|
} catch (err) {
|
|
11636
12190
|
failures.push(
|
|
11637
12191
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -11650,7 +12204,7 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
11650
12204
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
11651
12205
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
11652
12206
|
const abs = path57.join(manifestDir, dir);
|
|
11653
|
-
if (!
|
|
12207
|
+
if (!fs45.existsSync(abs)) continue;
|
|
11654
12208
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
11655
12209
|
walkMd(
|
|
11656
12210
|
abs,
|
|
@@ -11678,9 +12232,9 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
11678
12232
|
}
|
|
11679
12233
|
return failures;
|
|
11680
12234
|
}
|
|
11681
|
-
function validateCatalogData(manifestDir,
|
|
12235
|
+
function validateCatalogData(manifestDir, manifestVersion2, items) {
|
|
11682
12236
|
const failures = [
|
|
11683
|
-
...auditManifestStructure(
|
|
12237
|
+
...auditManifestStructure(manifestVersion2, items),
|
|
11684
12238
|
...validateFormerIds(items),
|
|
11685
12239
|
...auditOptInMetadata(items),
|
|
11686
12240
|
...auditForbiddenStacks(items),
|
|
@@ -11726,7 +12280,7 @@ init_logger();
|
|
|
11726
12280
|
// src/workspace/worktree/add.ts
|
|
11727
12281
|
init_fs();
|
|
11728
12282
|
import path65 from "node:path";
|
|
11729
|
-
import
|
|
12283
|
+
import fs49 from "fs-extra";
|
|
11730
12284
|
|
|
11731
12285
|
// src/workspace/worktree/git-worktree.ts
|
|
11732
12286
|
init_exec();
|
|
@@ -11847,12 +12401,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
11847
12401
|
|
|
11848
12402
|
// src/workspace/worktree/hydrate.ts
|
|
11849
12403
|
import path61 from "node:path";
|
|
11850
|
-
import
|
|
12404
|
+
import fs47 from "fs-extra";
|
|
11851
12405
|
|
|
11852
12406
|
// src/workspace/worktree/cow-copy.ts
|
|
11853
12407
|
init_exec();
|
|
11854
12408
|
import path59 from "node:path";
|
|
11855
|
-
import
|
|
12409
|
+
import fs46 from "fs-extra";
|
|
11856
12410
|
async function getMacFilesystemType(dir) {
|
|
11857
12411
|
const df = await runCommand("df", ["-P", dir]);
|
|
11858
12412
|
if (df.exitCode !== 0) return void 0;
|
|
@@ -11883,14 +12437,14 @@ async function detectCowStrategy(dir) {
|
|
|
11883
12437
|
return "unknown-platform";
|
|
11884
12438
|
}
|
|
11885
12439
|
async function cowCopyDir(src, dest) {
|
|
11886
|
-
const srcExists = await
|
|
12440
|
+
const srcExists = await fs46.pathExists(src);
|
|
11887
12441
|
if (!srcExists)
|
|
11888
12442
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
11889
12443
|
const strategy = await detectCowStrategy(path59.dirname(dest));
|
|
11890
12444
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
11891
12445
|
return { strategy, attempted: false, ok: false };
|
|
11892
12446
|
}
|
|
11893
|
-
await
|
|
12447
|
+
await fs46.ensureDir(path59.dirname(dest));
|
|
11894
12448
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
11895
12449
|
const result = await runCommand("cp", args);
|
|
11896
12450
|
return {
|
|
@@ -12077,11 +12631,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12077
12631
|
for (const target of HYDRATION_TARGETS) {
|
|
12078
12632
|
const src = path61.join(mainMemberPath, target);
|
|
12079
12633
|
const dest = path61.join(worktreeMemberPath, target);
|
|
12080
|
-
if (!await
|
|
12081
|
-
const destExists = await
|
|
12634
|
+
if (!await fs47.pathExists(src)) continue;
|
|
12635
|
+
const destExists = await fs47.pathExists(dest);
|
|
12082
12636
|
if (!opts.force && destExists) continue;
|
|
12083
12637
|
if (opts.force && destExists) {
|
|
12084
|
-
await
|
|
12638
|
+
await fs47.remove(dest);
|
|
12085
12639
|
}
|
|
12086
12640
|
const copy = await cowCopyDir(src, dest);
|
|
12087
12641
|
targets.push({ target, copy });
|
|
@@ -12095,7 +12649,7 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12095
12649
|
// src/workspace/worktree/local-files.ts
|
|
12096
12650
|
init_exec();
|
|
12097
12651
|
import path62 from "node:path";
|
|
12098
|
-
import
|
|
12652
|
+
import fs48 from "fs-extra";
|
|
12099
12653
|
var MACHINE_LOCAL_FILES = [
|
|
12100
12654
|
"docker-compose.local.yml",
|
|
12101
12655
|
path62.join(".claude", "settings.local.json")
|
|
@@ -12109,11 +12663,11 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
12109
12663
|
const copied = [];
|
|
12110
12664
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
12111
12665
|
const src = path62.join(mainMemberPath, rel);
|
|
12112
|
-
if (!await
|
|
12666
|
+
if (!await fs48.pathExists(src)) continue;
|
|
12113
12667
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
12114
12668
|
const dest = path62.join(worktreeMemberPath, rel);
|
|
12115
|
-
await
|
|
12116
|
-
await
|
|
12669
|
+
await fs48.ensureDir(path62.dirname(dest));
|
|
12670
|
+
await fs48.copy(src, dest, { overwrite: true });
|
|
12117
12671
|
copied.push(rel);
|
|
12118
12672
|
}
|
|
12119
12673
|
return copied;
|
|
@@ -12194,7 +12748,7 @@ async function runAdd(opts) {
|
|
|
12194
12748
|
}
|
|
12195
12749
|
const branch = opts.branch ?? opts.slug;
|
|
12196
12750
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
12197
|
-
if (await
|
|
12751
|
+
if (await fs49.pathExists(wsWorktreePath)) {
|
|
12198
12752
|
return {
|
|
12199
12753
|
ok: false,
|
|
12200
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.`
|
|
@@ -12563,14 +13117,14 @@ async function runHydrateWorktree(opts) {
|
|
|
12563
13117
|
|
|
12564
13118
|
// src/workspace/worktree/list.ts
|
|
12565
13119
|
import path69 from "node:path";
|
|
12566
|
-
import
|
|
13120
|
+
import fs50 from "fs-extra";
|
|
12567
13121
|
async function runList2() {
|
|
12568
13122
|
const resolved = await resolveWorkspaceForWorktree();
|
|
12569
13123
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
12570
13124
|
const { rootInfo, workspaceRoot } = resolved;
|
|
12571
13125
|
const dir = worktreesDir(workspaceRoot);
|
|
12572
|
-
if (!await
|
|
12573
|
-
const entries = await
|
|
13126
|
+
if (!await fs50.pathExists(dir)) return { ok: true, workspaceRoot, worktrees: [] };
|
|
13127
|
+
const entries = await fs50.readdir(dir, { withFileTypes: true });
|
|
12574
13128
|
const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
12575
13129
|
const configuredMembers = await readMembers(rootInfo);
|
|
12576
13130
|
const worktrees = [];
|
|
@@ -12581,7 +13135,7 @@ async function runList2() {
|
|
|
12581
13135
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
12582
13136
|
for (const m of source) {
|
|
12583
13137
|
const memberPath = path69.join(wtPath, m.folder);
|
|
12584
|
-
const materialized = await
|
|
13138
|
+
const materialized = await fs50.pathExists(memberPath);
|
|
12585
13139
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
12586
13140
|
members.push({
|
|
12587
13141
|
id: m.id,
|
|
@@ -12643,13 +13197,13 @@ async function runPrune2(opts) {
|
|
|
12643
13197
|
|
|
12644
13198
|
// src/workspace/worktree/remove.ts
|
|
12645
13199
|
import path70 from "node:path";
|
|
12646
|
-
import
|
|
13200
|
+
import fs51 from "fs-extra";
|
|
12647
13201
|
async function runRemove2(opts) {
|
|
12648
13202
|
const resolved = await resolveWorkspaceForWorktree();
|
|
12649
13203
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
12650
13204
|
const { rootInfo, workspaceRoot } = resolved;
|
|
12651
13205
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
12652
|
-
if (!await
|
|
13206
|
+
if (!await fs51.pathExists(wsWorktreePath)) {
|
|
12653
13207
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
12654
13208
|
}
|
|
12655
13209
|
const state = await readWorktreeState(wsWorktreePath);
|
|
@@ -12674,7 +13228,7 @@ async function runRemove2(opts) {
|
|
|
12674
13228
|
}
|
|
12675
13229
|
const existingTargets = [];
|
|
12676
13230
|
for (const target of targets) {
|
|
12677
|
-
if (await
|
|
13231
|
+
if (await fs51.pathExists(target.path)) existingTargets.push(target);
|
|
12678
13232
|
}
|
|
12679
13233
|
if (!opts.force) {
|
|
12680
13234
|
const blockers = [];
|
|
@@ -12730,8 +13284,8 @@ async function runRemove2(opts) {
|
|
|
12730
13284
|
}
|
|
12731
13285
|
await pruneWorktrees(workspaceRoot);
|
|
12732
13286
|
}
|
|
12733
|
-
if (wsRemoveOk && await
|
|
12734
|
-
await
|
|
13287
|
+
if (wsRemoveOk && await fs51.pathExists(wsWorktreePath)) {
|
|
13288
|
+
await fs51.remove(wsWorktreePath);
|
|
12735
13289
|
}
|
|
12736
13290
|
return { ok: failed.length === 0, dryRun: false, slug: opts.slug, removed, failed };
|
|
12737
13291
|
}
|
|
@@ -13075,9 +13629,9 @@ init_fs();
|
|
|
13075
13629
|
init_paths();
|
|
13076
13630
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
13077
13631
|
import path71 from "node:path";
|
|
13078
|
-
var
|
|
13632
|
+
var MANIFEST_FILE2 = "workspace.manifest.json";
|
|
13079
13633
|
function manifestPath(workspaceRoot) {
|
|
13080
|
-
return hausPath(workspaceRoot,
|
|
13634
|
+
return hausPath(workspaceRoot, MANIFEST_FILE2);
|
|
13081
13635
|
}
|
|
13082
13636
|
function hausVersion() {
|
|
13083
13637
|
try {
|
|
@@ -13293,17 +13847,17 @@ init_logger();
|
|
|
13293
13847
|
|
|
13294
13848
|
// src/workspace/link-context/link.ts
|
|
13295
13849
|
import path75 from "node:path";
|
|
13296
|
-
import
|
|
13850
|
+
import fs54 from "fs-extra";
|
|
13297
13851
|
init_fs();
|
|
13298
13852
|
init_logger();
|
|
13299
13853
|
|
|
13300
13854
|
// src/workspace/link-context/plan.ts
|
|
13301
13855
|
import path74 from "node:path";
|
|
13302
|
-
import
|
|
13856
|
+
import fs53 from "fs-extra";
|
|
13303
13857
|
|
|
13304
13858
|
// src/workspace/link-context/scan-source.ts
|
|
13305
13859
|
import path73 from "node:path";
|
|
13306
|
-
import
|
|
13860
|
+
import fs52 from "fs-extra";
|
|
13307
13861
|
var ASSET_SUBDIR = {
|
|
13308
13862
|
skill: "skills",
|
|
13309
13863
|
agent: "agents",
|
|
@@ -13311,20 +13865,20 @@ var ASSET_SUBDIR = {
|
|
|
13311
13865
|
};
|
|
13312
13866
|
async function hasLinkableContext(member) {
|
|
13313
13867
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
13314
|
-
if (await
|
|
13868
|
+
if (await fs52.pathExists(path73.join(member.absPath, ".claude", subdir))) return true;
|
|
13315
13869
|
}
|
|
13316
13870
|
return false;
|
|
13317
13871
|
}
|
|
13318
13872
|
async function collectSkills(member) {
|
|
13319
13873
|
const dir = path73.join(member.absPath, ".claude", "skills");
|
|
13320
|
-
if (!await
|
|
13321
|
-
const names = await
|
|
13874
|
+
if (!await fs52.pathExists(dir)) return [];
|
|
13875
|
+
const names = await fs52.readdir(dir);
|
|
13322
13876
|
const out = [];
|
|
13323
13877
|
for (const name of [...names].sort()) {
|
|
13324
13878
|
const abs = path73.join(dir, name);
|
|
13325
|
-
const stat = await
|
|
13879
|
+
const stat = await fs52.lstat(abs);
|
|
13326
13880
|
if (!stat.isDirectory()) continue;
|
|
13327
|
-
if (!await
|
|
13881
|
+
if (!await fs52.pathExists(path73.join(abs, "SKILL.md"))) continue;
|
|
13328
13882
|
out.push({ type: "skill", name, sourceRelPath: path73.posix.join(".claude", "skills", name) });
|
|
13329
13883
|
}
|
|
13330
13884
|
return out;
|
|
@@ -13332,13 +13886,13 @@ async function collectSkills(member) {
|
|
|
13332
13886
|
async function collectFlatMarkdown(member, type) {
|
|
13333
13887
|
const subdir = ASSET_SUBDIR[type];
|
|
13334
13888
|
const dir = path73.join(member.absPath, ".claude", subdir);
|
|
13335
|
-
if (!await
|
|
13336
|
-
const names = await
|
|
13889
|
+
if (!await fs52.pathExists(dir)) return [];
|
|
13890
|
+
const names = await fs52.readdir(dir);
|
|
13337
13891
|
const out = [];
|
|
13338
13892
|
for (const fileName of [...names].sort()) {
|
|
13339
13893
|
if (!fileName.endsWith(".md")) continue;
|
|
13340
13894
|
const abs = path73.join(dir, fileName);
|
|
13341
|
-
if (!(await
|
|
13895
|
+
if (!(await fs52.lstat(abs)).isFile()) continue;
|
|
13342
13896
|
const name = fileName.slice(0, -".md".length);
|
|
13343
13897
|
out.push({ type, name, sourceRelPath: path73.posix.join(".claude", subdir, fileName) });
|
|
13344
13898
|
}
|
|
@@ -13372,7 +13926,7 @@ async function buildLinkPlan(members) {
|
|
|
13372
13926
|
const byKey = /* @__PURE__ */ new Map();
|
|
13373
13927
|
const membersById = new Map(members.map((m) => [m.id, m]));
|
|
13374
13928
|
for (const member of members) {
|
|
13375
|
-
if (!await
|
|
13929
|
+
if (!await fs53.pathExists(member.absPath)) {
|
|
13376
13930
|
skipped.push({ repo: member.id, reason: `not cloned locally (${member.absPath}) \u2014 skipped` });
|
|
13377
13931
|
continue;
|
|
13378
13932
|
}
|
|
@@ -13453,15 +14007,15 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
13453
14007
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
13454
14008
|
const destAbs = path75.join(workspaceRoot, destRel);
|
|
13455
14009
|
if (write) {
|
|
13456
|
-
await
|
|
13457
|
-
if (await
|
|
13458
|
-
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, {
|
|
13459
14013
|
overwrite: true,
|
|
13460
14014
|
errorOnExist: false,
|
|
13461
14015
|
dereference: false,
|
|
13462
14016
|
filter: (s) => {
|
|
13463
14017
|
try {
|
|
13464
|
-
return !
|
|
14018
|
+
return !fs54.lstatSync(s).isSymbolicLink();
|
|
13465
14019
|
} catch {
|
|
13466
14020
|
return false;
|
|
13467
14021
|
}
|
|
@@ -13490,7 +14044,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
13490
14044
|
warn(`Refusing to remove linked context path outside the workspace root: ${old.path}`);
|
|
13491
14045
|
continue;
|
|
13492
14046
|
}
|
|
13493
|
-
if (await
|
|
14047
|
+
if (await fs54.pathExists(abs)) await fs54.remove(abs);
|
|
13494
14048
|
}
|
|
13495
14049
|
}
|
|
13496
14050
|
if (write) {
|
|
@@ -13546,7 +14100,7 @@ import path77 from "node:path";
|
|
|
13546
14100
|
|
|
13547
14101
|
// src/claude/write-workspace-claude-md.ts
|
|
13548
14102
|
import path76 from "node:path";
|
|
13549
|
-
import
|
|
14103
|
+
import fs55 from "fs-extra";
|
|
13550
14104
|
init_fs();
|
|
13551
14105
|
init_logger();
|
|
13552
14106
|
init_paths();
|
|
@@ -13571,8 +14125,8 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
13571
14125
|
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path76.join(workspaceRoot, "CLAUDE.md");
|
|
13572
14126
|
const say = opts.quiet ? () => {
|
|
13573
14127
|
} : log;
|
|
13574
|
-
const existed = await
|
|
13575
|
-
const prev = existed ? await
|
|
14128
|
+
const existed = await fs55.pathExists(filePath);
|
|
14129
|
+
const prev = existed ? await fs55.readFile(filePath, "utf8") : "";
|
|
13576
14130
|
const next = opts.collision ? `${block}
|
|
13577
14131
|
` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
|
|
13578
14132
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -13644,34 +14198,49 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13644
14198
|
const repoRoot = path77.resolve(workspaceRoot, repo.path);
|
|
13645
14199
|
say(`
|
|
13646
14200
|
\u2192 ${repo.name} (${repo.path})`);
|
|
14201
|
+
let res;
|
|
13647
14202
|
try {
|
|
13648
14203
|
if (!existsSync9(repoRoot) || !statSync(repoRoot).isDirectory()) {
|
|
13649
14204
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
13650
14205
|
}
|
|
13651
|
-
|
|
14206
|
+
res = await runSetupCore(repoRoot, {
|
|
13652
14207
|
apply,
|
|
13653
14208
|
dryRun: options.dryRun,
|
|
13654
14209
|
// Under --json, mute the inner pipeline entirely; we serialize the
|
|
13655
14210
|
// aggregated result below. Otherwise let it print its normal human output.
|
|
13656
|
-
quiet: options.json
|
|
14211
|
+
quiet: options.json,
|
|
14212
|
+
allowEmptyCache: options.allowEmptyCache
|
|
13657
14213
|
});
|
|
14214
|
+
} catch (err) {
|
|
14215
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
13658
14216
|
statuses.push({
|
|
13659
14217
|
name: repo.name,
|
|
13660
14218
|
path: repo.path,
|
|
13661
14219
|
root: repoRoot,
|
|
13662
|
-
status: "
|
|
13663
|
-
|
|
13664
|
-
recommendedCount: res.recommendedCount
|
|
14220
|
+
status: "failed",
|
|
14221
|
+
error: message2
|
|
13665
14222
|
});
|
|
13666
|
-
|
|
13667
|
-
|
|
13668
|
-
|
|
13669
|
-
|
|
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.";
|
|
13670
14237
|
statuses.push({
|
|
13671
14238
|
name: repo.name,
|
|
13672
14239
|
path: repo.path,
|
|
13673
14240
|
root: repoRoot,
|
|
13674
14241
|
status: "failed",
|
|
14242
|
+
roles: res.roles,
|
|
14243
|
+
recommendedCount: res.recommendedCount,
|
|
13675
14244
|
error: message2
|
|
13676
14245
|
});
|
|
13677
14246
|
if (!options.continueOnError) {
|
|
@@ -13681,10 +14250,21 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13681
14250
|
process.exitCode = 1;
|
|
13682
14251
|
return result2;
|
|
13683
14252
|
}
|
|
13684
|
-
throw new Error(`Setup failed for ${repo.name}: ${message2}
|
|
14253
|
+
throw new Error(`Setup failed for ${repo.name}: ${message2}`);
|
|
13685
14254
|
}
|
|
13686
14255
|
error(`Setup failed for ${repo.name}: ${message2}`);
|
|
14256
|
+
continue;
|
|
13687
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 });
|
|
13688
14268
|
}
|
|
13689
14269
|
const written = [];
|
|
13690
14270
|
if (apply && aggregateInputs.length > 0) {
|
|
@@ -13804,6 +14384,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13804
14384
|
}
|
|
13805
14385
|
const ok = statuses.filter((s) => s.status === "ok").length;
|
|
13806
14386
|
const failed = statuses.length - ok;
|
|
14387
|
+
if (failed > 0) process.exitCode = 1;
|
|
13807
14388
|
const result = { workspaceRoot, statuses, written };
|
|
13808
14389
|
if (options.json) {
|
|
13809
14390
|
log(JSON.stringify(result, null, 2));
|
|
@@ -13820,7 +14401,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
13820
14401
|
// src/commands/workspace/undo.ts
|
|
13821
14402
|
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
13822
14403
|
import path78 from "node:path";
|
|
13823
|
-
import
|
|
14404
|
+
import fs56 from "fs-extra";
|
|
13824
14405
|
init_fs();
|
|
13825
14406
|
init_logger();
|
|
13826
14407
|
init_paths();
|
|
@@ -13871,7 +14452,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13871
14452
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
13872
14453
|
continue;
|
|
13873
14454
|
}
|
|
13874
|
-
if (await
|
|
14455
|
+
if (await fs56.pathExists(abs)) {
|
|
13875
14456
|
log(`[dry-run] Would remove ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
13876
14457
|
}
|
|
13877
14458
|
}
|
|
@@ -13881,11 +14462,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13881
14462
|
hausPath(workspaceRoot, "WORKSPACE.md")
|
|
13882
14463
|
];
|
|
13883
14464
|
for (const abs of workspaceArtifacts) {
|
|
13884
|
-
if (await
|
|
14465
|
+
if (await fs56.pathExists(abs)) {
|
|
13885
14466
|
log(`[dry-run] Would remove ${path78.relative(workspaceRoot, abs)}`);
|
|
13886
14467
|
}
|
|
13887
14468
|
}
|
|
13888
|
-
if (!await
|
|
14469
|
+
if (!await fs56.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
13889
14470
|
const claudeMd = await readText(path78.join(workspaceRoot, "CLAUDE.md"));
|
|
13890
14471
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
13891
14472
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
@@ -13925,26 +14506,26 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13925
14506
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
13926
14507
|
continue;
|
|
13927
14508
|
}
|
|
13928
|
-
if (await
|
|
13929
|
-
await
|
|
14509
|
+
if (await fs56.pathExists(abs)) {
|
|
14510
|
+
await fs56.remove(abs);
|
|
13930
14511
|
log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
13931
14512
|
}
|
|
13932
14513
|
}
|
|
13933
14514
|
for (const rel of WORKSPACE_AGGREGATE_FILES) {
|
|
13934
14515
|
const abs = hausPath(workspaceRoot, rel);
|
|
13935
|
-
if (await
|
|
13936
|
-
await
|
|
14516
|
+
if (await fs56.pathExists(abs)) {
|
|
14517
|
+
await fs56.remove(abs);
|
|
13937
14518
|
log(`Removed ${path78.relative(workspaceRoot, abs)}`);
|
|
13938
14519
|
}
|
|
13939
14520
|
}
|
|
13940
14521
|
const manifestFile = manifestPath(workspaceRoot);
|
|
13941
|
-
if (await
|
|
13942
|
-
await
|
|
14522
|
+
if (await fs56.pathExists(manifestFile)) {
|
|
14523
|
+
await fs56.remove(manifestFile);
|
|
13943
14524
|
log(`Removed ${path78.relative(workspaceRoot, manifestFile)}`);
|
|
13944
14525
|
}
|
|
13945
14526
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
13946
|
-
if (await
|
|
13947
|
-
await
|
|
14527
|
+
if (await fs56.pathExists(workspaceMdPath)) {
|
|
14528
|
+
await fs56.remove(workspaceMdPath);
|
|
13948
14529
|
log(`Removed ${path78.relative(workspaceRoot, workspaceMdPath)}`);
|
|
13949
14530
|
} else {
|
|
13950
14531
|
const claudeMdPath = path78.join(workspaceRoot, "CLAUDE.md");
|
|
@@ -13956,19 +14537,19 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13956
14537
|
end: WORKSPACE_BLOCK_END
|
|
13957
14538
|
});
|
|
13958
14539
|
if (next.length === 0) {
|
|
13959
|
-
await
|
|
14540
|
+
await fs56.remove(claudeMdPath);
|
|
13960
14541
|
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
13961
14542
|
} else {
|
|
13962
|
-
await
|
|
14543
|
+
await fs56.writeFile(claudeMdPath, next, "utf8");
|
|
13963
14544
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
13964
14545
|
}
|
|
13965
14546
|
} else if (prev && hasLegacyWorkspaceBlock(prev)) {
|
|
13966
14547
|
const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
|
|
13967
14548
|
if (next.length === 0) {
|
|
13968
|
-
await
|
|
14549
|
+
await fs56.remove(claudeMdPath);
|
|
13969
14550
|
log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
|
|
13970
14551
|
} else {
|
|
13971
|
-
await
|
|
14552
|
+
await fs56.writeFile(claudeMdPath, next, "utf8");
|
|
13972
14553
|
log(
|
|
13973
14554
|
"Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
|
|
13974
14555
|
);
|
|
@@ -14086,7 +14667,8 @@ async function runWorkspace(action, options = {}) {
|
|
|
14086
14667
|
dryRun: options.dryRun,
|
|
14087
14668
|
json: options.json,
|
|
14088
14669
|
continueOnError: options.continueOnError,
|
|
14089
|
-
only: normalizeOnly(options.only)
|
|
14670
|
+
only: normalizeOnly(options.only),
|
|
14671
|
+
allowEmptyCache: options.allowEmptyCache
|
|
14090
14672
|
});
|
|
14091
14673
|
return;
|
|
14092
14674
|
case "doctor":
|
|
@@ -14130,6 +14712,9 @@ function validateRuntimeNodeVersion() {
|
|
|
14130
14712
|
}
|
|
14131
14713
|
validateRuntimeNodeVersion();
|
|
14132
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
|
+
});
|
|
14133
14718
|
program.command("scan").description(
|
|
14134
14719
|
"Scan the repo for roles, dependencies, and package manager; writes context-map.json"
|
|
14135
14720
|
).option("--json").action(runScan);
|
|
@@ -14146,7 +14731,7 @@ program.command("apply").description("Write recommended catalog items and core C
|
|
|
14146
14731
|
"Install exactly these recommended item ids non-interactively (skill backend; space- or comma-separated)"
|
|
14147
14732
|
).option(
|
|
14148
14733
|
"--allow-empty-cache",
|
|
14149
|
-
"
|
|
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)"
|
|
14150
14735
|
).option(
|
|
14151
14736
|
"--refill-config",
|
|
14152
14737
|
"Fill still-blank fields in an existing workflow-config.md without touching edited ones"
|
|
@@ -14180,6 +14765,9 @@ program.command("explain-recommendation").description("Print a per-item breakdow
|
|
|
14180
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(
|
|
14181
14766
|
"--force",
|
|
14182
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)"
|
|
14183
14771
|
).action(runInit);
|
|
14184
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);
|
|
14185
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(
|
|
@@ -14188,6 +14776,12 @@ program.command("clone").description("Clone a single git repository by URL").arg
|
|
|
14188
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 }));
|
|
14189
14777
|
program.command("catalog-audit").description("Check the bundled catalog manifest for unsupported or forbidden stacks").action(runCatalogAudit);
|
|
14190
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
|
+
);
|
|
14191
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);
|
|
14192
14786
|
program.command("ci-gate").description(
|
|
14193
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."
|
|
@@ -14216,7 +14810,10 @@ var workspace = program.command("workspace").description("Multi-repo workspace c
|
|
|
14216
14810
|
workspace.command("init").option("-y, --yes", "Skip confirmation when haus.workspace.yaml already exists").action((opts) => runWorkspace("init", opts));
|
|
14217
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));
|
|
14218
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));
|
|
14219
|
-
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));
|
|
14220
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));
|
|
14221
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));
|
|
14222
14819
|
workspace.command("link-context").description(
|