@haus-tech/haus-workflow 4.0.1 → 4.2.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 +22 -0
- package/README.md +39 -1
- package/dist/cli.js +1392 -769
- package/library/global/skills/haus-workflow/SKILL.md +5 -5
- package/library/global/skills/haus-workflow/references/new.md +116 -23
- package/library/global/skills/request-assess-impact/SKILL.md +97 -0
- package/library/global/skills/request-classify/SKILL.md +86 -0
- package/library/global/skills/request-classify/references/klassningsinstruktion.md +146 -0
- package/library/global/skills/request-sync/SKILL.md +213 -0
- package/library/global/skills/request-write-spec/SKILL.md +123 -0
- package/library/global/skills/request-write-spec/references/specformat.md +137 -0
- package/library/global/skills/slack-cowork/SKILL.md +188 -0
- package/library/global/skills/slack-cowork/references/canvas-markdown.md +160 -0
- package/library/global/skills/slack-cowork/references/synk.md +226 -0
- 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,176 +1932,21 @@ 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
|
|
|
1923
|
-
// src/catalog/load-catalog.ts
|
|
1924
|
-
init_fs();
|
|
1925
|
-
init_logger();
|
|
1926
|
-
init_paths();
|
|
1927
|
-
init_manifest_format();
|
|
1928
|
-
import path9 from "node:path";
|
|
1929
|
-
|
|
1930
|
-
// src/catalog/remote-catalog.ts
|
|
1931
|
-
init_github_tree();
|
|
1932
|
-
init_ref();
|
|
1933
|
-
init_ref();
|
|
1934
|
-
init_project_pin();
|
|
1935
|
-
init_manifest();
|
|
1936
|
-
|
|
1937
|
-
// src/catalog/remote-catalog/workflow-template.ts
|
|
1938
|
-
init_http();
|
|
1939
|
-
init_ref();
|
|
1940
|
-
import path7 from "node:path";
|
|
1941
|
-
import fs6 from "fs-extra";
|
|
1942
|
-
var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
|
|
1943
|
-
async function readWorkflowTemplate(opts = {}) {
|
|
1944
|
-
const dest = path7.join(getCacheDir(), WORKFLOW_TEMPLATE_REL);
|
|
1945
|
-
const base = await remoteBase(opts.projectRoot);
|
|
1946
|
-
const { url, headers } = await catalogContentRequest(base, WORKFLOW_TEMPLATE_REL);
|
|
1947
|
-
const text = await fetchText(url, headers);
|
|
1948
|
-
if (text === null) {
|
|
1949
|
-
if (await fs6.pathExists(dest)) return fs6.readFile(dest, "utf8");
|
|
1950
|
-
return null;
|
|
1951
|
-
}
|
|
1952
|
-
if (!opts.dryRun) {
|
|
1953
|
-
await writeTextIfChanged(dest, text);
|
|
1954
|
-
}
|
|
1955
|
-
return text;
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
// src/catalog/remote-catalog.ts
|
|
1959
|
-
init_github_tree();
|
|
1960
|
-
init_sync();
|
|
1961
|
-
|
|
1962
|
-
// src/catalog/load-catalog.ts
|
|
1963
|
-
function cacheRefMatches(cached, expected) {
|
|
1964
|
-
if (expected === null) return true;
|
|
1965
|
-
if (typeof cached !== "string" || cached === "") return true;
|
|
1966
|
-
return cached === expected;
|
|
1967
|
-
}
|
|
1968
|
-
async function loadCatalogContext(root) {
|
|
1969
|
-
const envPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
1970
|
-
if (envPath) {
|
|
1971
|
-
const data2 = await readJson(envPath);
|
|
1972
|
-
return {
|
|
1973
|
-
items: data2?.items ?? [],
|
|
1974
|
-
contentRoot: path9.dirname(envPath),
|
|
1975
|
-
source: "fixture",
|
|
1976
|
-
manifestFormat: assertSupportedManifestFormat(data2?.manifestFormat, "The fixture catalog"),
|
|
1977
|
-
globalPlugins: data2?.globalPlugins
|
|
1978
|
-
};
|
|
1979
|
-
}
|
|
1980
|
-
const cacheDir = getCacheDir();
|
|
1981
|
-
const cacheManifestPath = path9.join(cacheDir, "manifest.json");
|
|
1982
|
-
const cacheData = await readJson(cacheManifestPath);
|
|
1983
|
-
if (cacheData?.items?.length) {
|
|
1984
|
-
const wantedRef = expectedCatalogRef(root);
|
|
1985
|
-
if (!cacheRefMatches(cacheData.catalogRef, wantedRef)) {
|
|
1986
|
-
warn(
|
|
1987
|
-
`Catalog cache was populated from ${String(cacheData.catalogRef)} but this project resolves to ${wantedRef} \u2014 ignoring the cache. Run \`haus update\` to re-sync it.`
|
|
1988
|
-
);
|
|
1989
|
-
} else {
|
|
1990
|
-
return {
|
|
1991
|
-
items: cacheData.items,
|
|
1992
|
-
contentRoot: cacheDir,
|
|
1993
|
-
source: "cache",
|
|
1994
|
-
manifestFormat: assertSupportedManifestFormat(
|
|
1995
|
-
cacheData.manifestFormat,
|
|
1996
|
-
"The cached catalog"
|
|
1997
|
-
),
|
|
1998
|
-
globalPlugins: cacheData.globalPlugins
|
|
1999
|
-
};
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
const localManifest = path9.join(root, "library/catalog/manifest.json");
|
|
2003
|
-
const localData = await readJson(localManifest);
|
|
2004
|
-
if (localData?.items?.length) {
|
|
2005
|
-
return {
|
|
2006
|
-
items: localData.items,
|
|
2007
|
-
contentRoot: path9.dirname(localManifest),
|
|
2008
|
-
source: "local",
|
|
2009
|
-
manifestFormat: assertSupportedManifestFormat(
|
|
2010
|
-
localData.manifestFormat,
|
|
2011
|
-
"The project-local catalog"
|
|
2012
|
-
),
|
|
2013
|
-
globalPlugins: localData.globalPlugins
|
|
2014
|
-
};
|
|
2015
|
-
}
|
|
2016
|
-
const packageManifest = path9.join(packageRoot(), "library/catalog/manifest.json");
|
|
2017
|
-
const data = await readJson(packageManifest);
|
|
2018
|
-
return {
|
|
2019
|
-
items: data?.items ?? [],
|
|
2020
|
-
contentRoot: path9.dirname(packageManifest),
|
|
2021
|
-
source: "bundled",
|
|
2022
|
-
manifestFormat: assertSupportedManifestFormat(data?.manifestFormat, "The bundled catalog"),
|
|
2023
|
-
globalPlugins: data?.globalPlugins
|
|
2024
|
-
};
|
|
2025
|
-
}
|
|
2026
|
-
async function catalogFormatRefusal(root) {
|
|
2027
|
-
try {
|
|
2028
|
-
await loadCatalogContext(root);
|
|
2029
|
-
return null;
|
|
2030
|
-
} catch (err) {
|
|
2031
|
-
if (err instanceof UnsupportedManifestFormatError) return err.message;
|
|
2032
|
-
throw err;
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
async function loadCatalog(root) {
|
|
2036
|
-
const ctx = await loadCatalogContext(root);
|
|
2037
|
-
return ctx.items;
|
|
2038
|
-
}
|
|
2039
|
-
function catalogItemContentPath(contentRoot, item) {
|
|
2040
|
-
return path9.join(contentRoot, item.path);
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
// src/claude/gitignore-owned-check.ts
|
|
2044
|
-
init_exec();
|
|
2045
|
-
var HAUS_OWNED_TRACKED_PATHS = [
|
|
2046
|
-
".claude",
|
|
2047
|
-
".claude/skills",
|
|
2048
|
-
".claude/agents",
|
|
2049
|
-
".claude/commands",
|
|
2050
|
-
".haus-workflow"
|
|
2051
|
-
];
|
|
2052
|
-
async function findGitignoredHausPaths(root) {
|
|
2053
|
-
try {
|
|
2054
|
-
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
1943
|
// src/catalog/former-ids.ts
|
|
2079
|
-
import
|
|
1944
|
+
import path4 from "node:path";
|
|
2080
1945
|
|
|
2081
1946
|
// src/catalog/install-mapping.ts
|
|
2082
1947
|
init_paths();
|
|
2083
1948
|
init_path_safety();
|
|
2084
|
-
import
|
|
1949
|
+
import path3 from "node:path";
|
|
2085
1950
|
var FLATTEN_DIR_BY_TYPE = {
|
|
2086
1951
|
skill: "skills",
|
|
2087
1952
|
agent: "agents",
|
|
@@ -2123,7 +1988,7 @@ function installTargetFor(item) {
|
|
|
2123
1988
|
return {
|
|
2124
1989
|
ok: true,
|
|
2125
1990
|
layout: "flatten",
|
|
2126
|
-
relPath: `${flattenDir}/${
|
|
1991
|
+
relPath: `${flattenDir}/${path3.posix.basename(itemPath)}`
|
|
2127
1992
|
};
|
|
2128
1993
|
}
|
|
2129
1994
|
const rel = upstreamRelative(itemPath);
|
|
@@ -2193,39 +2058,241 @@ function buildFormerIdMap(items) {
|
|
|
2193
2058
|
map.set(formerId, item.id);
|
|
2194
2059
|
}
|
|
2195
2060
|
}
|
|
2196
|
-
return map;
|
|
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
|
+
|
|
2127
|
+
// src/catalog/load-catalog.ts
|
|
2128
|
+
init_fs();
|
|
2129
|
+
init_logger();
|
|
2130
|
+
init_paths();
|
|
2131
|
+
init_manifest_format();
|
|
2132
|
+
import path11 from "node:path";
|
|
2133
|
+
|
|
2134
|
+
// src/catalog/remote-catalog.ts
|
|
2135
|
+
init_github_tree();
|
|
2136
|
+
init_ref();
|
|
2137
|
+
init_ref();
|
|
2138
|
+
init_project_pin();
|
|
2139
|
+
init_manifest();
|
|
2140
|
+
|
|
2141
|
+
// src/catalog/remote-catalog/workflow-template.ts
|
|
2142
|
+
init_offline();
|
|
2143
|
+
init_http();
|
|
2144
|
+
init_ref();
|
|
2145
|
+
import path9 from "node:path";
|
|
2146
|
+
import fs7 from "fs-extra";
|
|
2147
|
+
var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
|
|
2148
|
+
async function readWorkflowTemplate(opts = {}) {
|
|
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
|
+
}
|
|
2154
|
+
const base = await remoteBase(opts.projectRoot);
|
|
2155
|
+
const { url, headers } = await catalogContentRequest(base, WORKFLOW_TEMPLATE_REL);
|
|
2156
|
+
const text = await fetchText(url, headers);
|
|
2157
|
+
if (text === null) {
|
|
2158
|
+
if (await fs7.pathExists(dest)) return fs7.readFile(dest, "utf8");
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
if (!opts.dryRun) {
|
|
2162
|
+
await writeTextIfChanged(dest, text);
|
|
2163
|
+
}
|
|
2164
|
+
return text;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// src/catalog/remote-catalog.ts
|
|
2168
|
+
init_github_tree();
|
|
2169
|
+
init_sync();
|
|
2170
|
+
|
|
2171
|
+
// src/catalog/load-catalog.ts
|
|
2172
|
+
function manifestVersion(raw) {
|
|
2173
|
+
return typeof raw === "string" && raw !== "" ? raw : null;
|
|
2174
|
+
}
|
|
2175
|
+
function cacheRefMatches(cached, expected) {
|
|
2176
|
+
if (expected === null) return true;
|
|
2177
|
+
if (typeof cached !== "string" || cached === "") return true;
|
|
2178
|
+
return cached === expected;
|
|
2179
|
+
}
|
|
2180
|
+
async function loadCatalogContext(root) {
|
|
2181
|
+
const envPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
2182
|
+
if (envPath) {
|
|
2183
|
+
const data2 = await readJson(envPath);
|
|
2184
|
+
return {
|
|
2185
|
+
items: data2?.items ?? [],
|
|
2186
|
+
contentRoot: path11.dirname(envPath),
|
|
2187
|
+
source: "fixture",
|
|
2188
|
+
manifestFormat: assertSupportedManifestFormat(data2?.manifestFormat, "The fixture catalog"),
|
|
2189
|
+
version: manifestVersion(data2?.version),
|
|
2190
|
+
globalPlugins: data2?.globalPlugins
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
const cacheDir = getCacheDir();
|
|
2194
|
+
const cacheManifestPath = path11.join(cacheDir, "manifest.json");
|
|
2195
|
+
const cacheData = await readJson(cacheManifestPath);
|
|
2196
|
+
if (cacheData?.items?.length) {
|
|
2197
|
+
const wantedRef = expectedCatalogRef(root);
|
|
2198
|
+
if (!cacheRefMatches(cacheData.catalogRef, wantedRef)) {
|
|
2199
|
+
warn(
|
|
2200
|
+
`Catalog cache was populated from ${String(cacheData.catalogRef)} but this project resolves to ${wantedRef} \u2014 ignoring the cache. Run \`haus update\` to re-sync it.`
|
|
2201
|
+
);
|
|
2202
|
+
} else {
|
|
2203
|
+
return {
|
|
2204
|
+
items: cacheData.items,
|
|
2205
|
+
contentRoot: cacheDir,
|
|
2206
|
+
source: "cache",
|
|
2207
|
+
manifestFormat: assertSupportedManifestFormat(
|
|
2208
|
+
cacheData.manifestFormat,
|
|
2209
|
+
"The cached catalog"
|
|
2210
|
+
),
|
|
2211
|
+
version: manifestVersion(cacheData.version),
|
|
2212
|
+
globalPlugins: cacheData.globalPlugins
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
const localManifest = path11.join(root, "library/catalog/manifest.json");
|
|
2217
|
+
const localData = await readJson(localManifest);
|
|
2218
|
+
if (localData?.items?.length) {
|
|
2219
|
+
return {
|
|
2220
|
+
items: localData.items,
|
|
2221
|
+
contentRoot: path11.dirname(localManifest),
|
|
2222
|
+
source: "local",
|
|
2223
|
+
manifestFormat: assertSupportedManifestFormat(
|
|
2224
|
+
localData.manifestFormat,
|
|
2225
|
+
"The project-local catalog"
|
|
2226
|
+
),
|
|
2227
|
+
version: manifestVersion(localData.version),
|
|
2228
|
+
globalPlugins: localData.globalPlugins
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
const packageManifest = path11.join(packageRoot(), "library/catalog/manifest.json");
|
|
2232
|
+
const data = await readJson(packageManifest);
|
|
2233
|
+
return {
|
|
2234
|
+
items: data?.items ?? [],
|
|
2235
|
+
contentRoot: path11.dirname(packageManifest),
|
|
2236
|
+
source: "bundled",
|
|
2237
|
+
manifestFormat: assertSupportedManifestFormat(data?.manifestFormat, "The bundled catalog"),
|
|
2238
|
+
version: manifestVersion(data?.version),
|
|
2239
|
+
globalPlugins: data?.globalPlugins
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
async function catalogFormatRefusal(root) {
|
|
2243
|
+
try {
|
|
2244
|
+
await loadCatalogContext(root);
|
|
2245
|
+
return null;
|
|
2246
|
+
} catch (err) {
|
|
2247
|
+
if (err instanceof UnsupportedManifestFormatError) return err.message;
|
|
2248
|
+
throw err;
|
|
2249
|
+
}
|
|
2197
2250
|
}
|
|
2198
|
-
function
|
|
2199
|
-
const
|
|
2200
|
-
return
|
|
2201
|
-
const newId = formerIdMap.get(id);
|
|
2202
|
-
return newId === void 0 ? [] : [{ oldId: id, newId }];
|
|
2203
|
-
});
|
|
2251
|
+
async function loadCatalog(root) {
|
|
2252
|
+
const ctx = await loadCatalogContext(root);
|
|
2253
|
+
return ctx.items;
|
|
2204
2254
|
}
|
|
2205
|
-
function
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2255
|
+
function catalogItemContentPath(contentRoot, item) {
|
|
2256
|
+
return path11.join(contentRoot, item.path);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// src/commands/apply.ts
|
|
2260
|
+
init_offline();
|
|
2261
|
+
|
|
2262
|
+
// src/claude/gitignore-owned-check.ts
|
|
2263
|
+
init_exec();
|
|
2264
|
+
var HAUS_OWNED_TRACKED_PATHS = [
|
|
2265
|
+
".claude",
|
|
2266
|
+
".claude/skills",
|
|
2267
|
+
".claude/agents",
|
|
2268
|
+
".claude/commands",
|
|
2269
|
+
".haus-workflow"
|
|
2270
|
+
];
|
|
2271
|
+
async function findGitignoredHausPaths(root) {
|
|
2272
|
+
try {
|
|
2273
|
+
const result = await runGit(["check-ignore", "--", ...HAUS_OWNED_TRACKED_PATHS], {
|
|
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 [];
|
|
2224
2285
|
}
|
|
2225
|
-
|
|
2286
|
+
}
|
|
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) });
|
|
@@ -5999,53 +6124,299 @@ async function runBackups(action, opts = {}) {
|
|
|
5999
6124
|
process.exitCode = 1;
|
|
6000
6125
|
return;
|
|
6001
6126
|
}
|
|
6002
|
-
keep = n;
|
|
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);
|
|
6003
6286
|
}
|
|
6004
|
-
const
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6287
|
+
const stagedFiles = await listAllFilesUnfiltered(staging);
|
|
6288
|
+
for (const rel of stagedFiles) {
|
|
6289
|
+
if (rel === MANIFEST_FILE || rel === BUNDLE_META_FILE) continue;
|
|
6290
|
+
const covered = coveredPaths.some((p) => rel === p || rel.startsWith(`${p}/`));
|
|
6291
|
+
if (!covered) {
|
|
6292
|
+
warn(`Refusing staged file outside any catalog item's path: ${rel}`);
|
|
6293
|
+
await fs28.remove(path31.join(staging, rel));
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
await pruneEmptyDirs(staging);
|
|
6297
|
+
return { accepted, rejected, contentlessIds };
|
|
6013
6298
|
}
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
const failures = [];
|
|
6020
|
-
const forbidden = new Set(FORBIDDEN_TAGS.map((w) => w.toLowerCase()));
|
|
6021
|
-
for (const item of items) {
|
|
6022
|
-
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
6023
|
-
if (forbidden.has(tag.toLowerCase())) failures.push(`${item.id} has unsupported tag ${tag}`);
|
|
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));
|
|
6024
6304
|
}
|
|
6025
|
-
|
|
6026
|
-
|
|
6305
|
+
}
|
|
6306
|
+
if (dir !== root && (await fs28.readdir(dir)).length === 0) {
|
|
6307
|
+
await fs28.remove(dir);
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
async function stripSymlinks(dir) {
|
|
6311
|
+
const removed = [];
|
|
6312
|
+
const entries = await fs28.readdir(dir, { withFileTypes: true });
|
|
6313
|
+
for (const entry of entries) {
|
|
6314
|
+
const abs = path31.join(dir, entry.name);
|
|
6315
|
+
if (entry.isSymbolicLink()) {
|
|
6316
|
+
await fs28.remove(abs);
|
|
6317
|
+
removed.push(abs);
|
|
6318
|
+
continue;
|
|
6319
|
+
}
|
|
6320
|
+
if (entry.isDirectory()) {
|
|
6321
|
+
removed.push(...await stripSymlinks(abs));
|
|
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,12 @@ 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
|
+
// `catalogInstallOk` stays true; `setupOk` is false so workspace setup cannot
|
|
10589
|
+
// record this repo as ok (issue #330).
|
|
10590
|
+
catalogInstallOk: true,
|
|
10591
|
+
setupOk: false
|
|
10091
10592
|
};
|
|
10092
10593
|
}
|
|
10093
10594
|
await writeJson(hausPath(root, "recommendation.json"), recommendation);
|
|
@@ -10116,7 +10617,9 @@ async function runSetupCore(root, opts) {
|
|
|
10116
10617
|
recommendedCount: recommendation.recommended.length,
|
|
10117
10618
|
warnings: warningLines,
|
|
10118
10619
|
hooksOk,
|
|
10119
|
-
written: []
|
|
10620
|
+
written: [],
|
|
10621
|
+
catalogInstallOk: true,
|
|
10622
|
+
setupOk: true
|
|
10120
10623
|
};
|
|
10121
10624
|
const envelope = (hooksResult) => ({
|
|
10122
10625
|
scan: scanResult,
|
|
@@ -10145,7 +10648,7 @@ async function runSetupCore(root, opts) {
|
|
|
10145
10648
|
error(sync.formatRefusal);
|
|
10146
10649
|
process.exitCode = 1;
|
|
10147
10650
|
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10148
|
-
return baseResult;
|
|
10651
|
+
return { ...baseResult, setupOk: false };
|
|
10149
10652
|
}
|
|
10150
10653
|
if (sync.newItems.length > 0) {
|
|
10151
10654
|
say(`Catalog cache populated: ${sync.newItems.length} new item(s).`);
|
|
@@ -10156,12 +10659,66 @@ async function runSetupCore(root, opts) {
|
|
|
10156
10659
|
warn(formatGithubRateLimitMessage(sync.rateLimit));
|
|
10157
10660
|
}
|
|
10158
10661
|
}
|
|
10662
|
+
const isPreview = dryRun ?? false;
|
|
10663
|
+
const wantedIds = installableRecommendedItems(recommendation.recommended).map((item) => item.id);
|
|
10664
|
+
let catalogCtx = null;
|
|
10665
|
+
let resolvedWantedIds = wantedIds;
|
|
10666
|
+
if (wantedIds.length > 0) {
|
|
10667
|
+
catalogCtx = await loadCatalogContext(root);
|
|
10668
|
+
const prevLock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
10669
|
+
const migrations = findFormerIdMigrations(
|
|
10670
|
+
prevLock.filter((entry) => Boolean(entry.id)),
|
|
10671
|
+
catalogCtx.items
|
|
10672
|
+
);
|
|
10673
|
+
const migrationByOldId = new Map(migrations.map((m) => [m.oldId, m.newId]));
|
|
10674
|
+
resolvedWantedIds = wantedIds.map((id) => migrationByOldId.get(id) ?? id);
|
|
10675
|
+
const shortfall = await countInstallableItems(catalogCtx, resolvedWantedIds);
|
|
10676
|
+
const tier = `${catalogCtx.source}${catalogCtx.version ? ` (v${catalogCtx.version})` : ""}`;
|
|
10677
|
+
if (shortfall.installable === 0) {
|
|
10678
|
+
const message2 = "No catalog content found. Run `haus update` first.";
|
|
10679
|
+
if (!opts.allowEmptyCache) {
|
|
10680
|
+
if (isPreview) {
|
|
10681
|
+
warn(`Catalog cache is empty \u2014 dry-run will skip catalog items. ${message2}`);
|
|
10682
|
+
} else {
|
|
10683
|
+
error(message2);
|
|
10684
|
+
process.exitCode = 1;
|
|
10685
|
+
emitJsonEnvelope(json, quiet, envelope(hooks));
|
|
10686
|
+
return { ...baseResult, catalogInstallOk: false, setupOk: false };
|
|
10687
|
+
}
|
|
10688
|
+
} else {
|
|
10689
|
+
warn(
|
|
10690
|
+
`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.`
|
|
10691
|
+
);
|
|
10692
|
+
}
|
|
10693
|
+
} else if (shortfall.missing.length > 0) {
|
|
10694
|
+
warn(
|
|
10695
|
+
`The ${tier} catalog tier is missing content for ${shortfall.missing.length} item(s): ${shortfall.missing.join(", ")}. They will be skipped.`
|
|
10696
|
+
);
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10159
10699
|
const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
|
|
10160
10700
|
quiet: opts.quiet,
|
|
10161
10701
|
activePluginIds
|
|
10162
10702
|
});
|
|
10163
10703
|
say("Applied files:");
|
|
10164
10704
|
files.forEach((f) => say(`- ${displayPath(root, f)}`));
|
|
10705
|
+
let catalogInstallOk = true;
|
|
10706
|
+
if (!isPreview && resolvedWantedIds.length > 0) {
|
|
10707
|
+
const { installedCount: installedWantedCount, missingWantedIds } = await recountInstalledAgainstLock(root, resolvedWantedIds);
|
|
10708
|
+
if (missingWantedIds.length > 0) {
|
|
10709
|
+
const tier = catalogCtx ? `${catalogCtx.source}${catalogCtx.version ? ` (v${catalogCtx.version})` : ""}` : "unknown";
|
|
10710
|
+
say(
|
|
10711
|
+
`${installedWantedCount} of ${resolvedWantedIds.length} catalog items installed (${tier} tier).`
|
|
10712
|
+
);
|
|
10713
|
+
if (installedWantedCount === 0) {
|
|
10714
|
+
error(
|
|
10715
|
+
"No catalog items were installed \u2014 only core files were written. Run `haus update` with network access, or import a catalog bundle."
|
|
10716
|
+
);
|
|
10717
|
+
process.exitCode = 1;
|
|
10718
|
+
catalogInstallOk = false;
|
|
10719
|
+
}
|
|
10720
|
+
}
|
|
10721
|
+
}
|
|
10165
10722
|
const hooksAfter = await verifyProjectSettingsHooksContract(root);
|
|
10166
10723
|
const hooksOkAfter = hooksAfter.ok;
|
|
10167
10724
|
if (hooksAfter.skipped) {
|
|
@@ -10173,7 +10730,13 @@ async function runSetupCore(root, opts) {
|
|
|
10173
10730
|
say(`- HOOKS OK: ${hooksAfter.message}`);
|
|
10174
10731
|
}
|
|
10175
10732
|
emitJsonEnvelope(json, quiet, envelope(hooksAfter));
|
|
10176
|
-
return {
|
|
10733
|
+
return {
|
|
10734
|
+
...baseResult,
|
|
10735
|
+
hooksOk: hooksOkAfter,
|
|
10736
|
+
written: files,
|
|
10737
|
+
catalogInstallOk,
|
|
10738
|
+
setupOk: catalogInstallOk
|
|
10739
|
+
};
|
|
10177
10740
|
}
|
|
10178
10741
|
|
|
10179
10742
|
// src/commands/init.ts
|
|
@@ -10193,6 +10756,7 @@ async function runInit(options) {
|
|
|
10193
10756
|
apply: !dryRun,
|
|
10194
10757
|
dryRun: false,
|
|
10195
10758
|
force: options.force,
|
|
10759
|
+
allowEmptyCache: options.allowEmptyCache,
|
|
10196
10760
|
confirm: () => confirm("Approve and write Claude files now?")
|
|
10197
10761
|
});
|
|
10198
10762
|
}
|
|
@@ -10323,7 +10887,7 @@ async function runInstall(options, deps = {}) {
|
|
|
10323
10887
|
|
|
10324
10888
|
// src/new-project/create-project.ts
|
|
10325
10889
|
import path53 from "node:path";
|
|
10326
|
-
import
|
|
10890
|
+
import fs42 from "fs-extra";
|
|
10327
10891
|
init_exec();
|
|
10328
10892
|
init_logger();
|
|
10329
10893
|
|
|
@@ -10477,7 +11041,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10477
11041
|
`Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
|
|
10478
11042
|
);
|
|
10479
11043
|
}
|
|
10480
|
-
if (await
|
|
11044
|
+
if (await fs42.pathExists(projectPath)) {
|
|
10481
11045
|
return fail("preflight", `Target ${projectPath} already exists \u2014 refusing to write into it.`);
|
|
10482
11046
|
}
|
|
10483
11047
|
const scaffoldArgs = stack.scaffold.args.map(
|
|
@@ -10555,15 +11119,15 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10555
11119
|
steps.push({ step: "scaffold", status: "done" });
|
|
10556
11120
|
phase = "git";
|
|
10557
11121
|
if (!opts.json) log("\u25B6 git: init/branch/commit");
|
|
10558
|
-
if (!await
|
|
11122
|
+
if (!await fs42.pathExists(path53.join(projectPath, ".git"))) {
|
|
10559
11123
|
const init = await deps.git(["init"], { cwd: projectPath });
|
|
10560
11124
|
if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
|
|
10561
11125
|
}
|
|
10562
11126
|
const branch = await deps.git(["branch", "-M", "main"], { cwd: projectPath });
|
|
10563
11127
|
if (branch.exitCode !== 0) return fail("git", `git branch -M main failed: ${branch.stderr}`);
|
|
10564
11128
|
const lockfile = opts.pm === "yarn" ? "yarn.lock" : opts.pm === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json";
|
|
10565
|
-
if (!await
|
|
10566
|
-
await
|
|
11129
|
+
if (!await fs42.pathExists(path53.join(projectPath, lockfile))) {
|
|
11130
|
+
await fs42.writeFile(path53.join(projectPath, lockfile), "");
|
|
10567
11131
|
}
|
|
10568
11132
|
const add = await deps.git(["add", "-A"], { cwd: projectPath });
|
|
10569
11133
|
if (add.exitCode !== 0) return fail("git", `git add failed: ${add.stderr}`);
|
|
@@ -10598,13 +11162,15 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10598
11162
|
phase = "init";
|
|
10599
11163
|
if (!opts.json) log("\u25B6 init: haus setup pipeline");
|
|
10600
11164
|
const exitCodeBeforeSetup = process.exitCode;
|
|
11165
|
+
let setupResult;
|
|
10601
11166
|
try {
|
|
10602
|
-
await deps.setup(projectPath, {
|
|
11167
|
+
setupResult = await deps.setup(projectPath, {
|
|
10603
11168
|
json: false,
|
|
10604
11169
|
apply: true,
|
|
10605
11170
|
dryRun: false,
|
|
10606
11171
|
quiet: opts.json,
|
|
10607
|
-
confirm: opts.yes ? async () => true : () => confirm("Approve and write Claude files now?")
|
|
11172
|
+
confirm: opts.yes ? async () => true : () => confirm("Approve and write Claude files now?"),
|
|
11173
|
+
allowEmptyCache: opts.allowEmptyCache
|
|
10608
11174
|
});
|
|
10609
11175
|
} catch (err) {
|
|
10610
11176
|
steps.push({
|
|
@@ -10614,15 +11180,26 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10614
11180
|
});
|
|
10615
11181
|
return { ok: false, projectPath, repoUrl, installHint, steps };
|
|
10616
11182
|
}
|
|
11183
|
+
const catalogInstallRefused = setupResult?.catalogInstallOk === false;
|
|
11184
|
+
const retryCmd = catalogInstallRefused ? `cd ${opts.name} && haus init --allow-empty-cache` : `cd ${opts.name} && haus init`;
|
|
10617
11185
|
if (process.exitCode === 1 && exitCodeBeforeSetup !== 1) {
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
11186
|
+
if (opts.allowEmptyCache && catalogInstallRefused) {
|
|
11187
|
+
steps.push({
|
|
11188
|
+
step: "init",
|
|
11189
|
+
status: "done",
|
|
11190
|
+
detail: "core files written; catalog items skipped (--allow-empty-cache)"
|
|
11191
|
+
});
|
|
11192
|
+
} else {
|
|
11193
|
+
steps.push({
|
|
11194
|
+
step: "init",
|
|
11195
|
+
status: "failed",
|
|
11196
|
+
detail: `haus init reported failure. Project and repo are intact \u2014 ${retryCmd} to retry.`
|
|
11197
|
+
});
|
|
11198
|
+
return { ok: false, projectPath, repoUrl, installHint, steps };
|
|
11199
|
+
}
|
|
11200
|
+
} else {
|
|
11201
|
+
steps.push({ step: "init", status: "done" });
|
|
10624
11202
|
}
|
|
10625
|
-
steps.push({ step: "init", status: "done" });
|
|
10626
11203
|
phase = "overlay";
|
|
10627
11204
|
if (!opts.json) log("\u25B6 overlay: haus config items");
|
|
10628
11205
|
let overlay;
|
|
@@ -10709,7 +11286,7 @@ async function createProject(opts, deps = defaultDeps) {
|
|
|
10709
11286
|
}
|
|
10710
11287
|
if (!opts.json) log(`\u2713 Created ${projectPath}${repoUrl ? ` \u2192 ${repoUrl}` : ""}
|
|
10711
11288
|
${installHint}`);
|
|
10712
|
-
return { ok:
|
|
11289
|
+
return { ok: process.exitCode !== 1, projectPath, repoUrl, installHint, steps };
|
|
10713
11290
|
}
|
|
10714
11291
|
|
|
10715
11292
|
// src/commands/new.ts
|
|
@@ -10742,7 +11319,8 @@ function resolveNewOptions(stackArg, nameArg, flags, parentDir) {
|
|
|
10742
11319
|
pm,
|
|
10743
11320
|
yes: flags.yes === true,
|
|
10744
11321
|
dryRun,
|
|
10745
|
-
json: flags.json === true
|
|
11322
|
+
json: flags.json === true,
|
|
11323
|
+
allowEmptyCache: flags.allowEmptyCache === true
|
|
10746
11324
|
}
|
|
10747
11325
|
};
|
|
10748
11326
|
}
|
|
@@ -10931,6 +11509,10 @@ async function runRemove(ids, options = {}) {
|
|
|
10931
11509
|
root,
|
|
10932
11510
|
cleared.map((c) => c.id)
|
|
10933
11511
|
);
|
|
11512
|
+
await addProjectExcludedIds(
|
|
11513
|
+
root,
|
|
11514
|
+
cleared.map((c) => c.id)
|
|
11515
|
+
);
|
|
10934
11516
|
try {
|
|
10935
11517
|
const remaining = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
10936
11518
|
const remainingIds = new Set(remaining.map((e) => e.id));
|
|
@@ -10946,7 +11528,6 @@ async function runRemove(ids, options = {}) {
|
|
|
10946
11528
|
}
|
|
10947
11529
|
if (!options.json) {
|
|
10948
11530
|
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
11531
|
}
|
|
10951
11532
|
emitReport(report, options.json);
|
|
10952
11533
|
}
|
|
@@ -11009,7 +11590,7 @@ async function runScan(options) {
|
|
|
11009
11590
|
|
|
11010
11591
|
// src/commands/undo.ts
|
|
11011
11592
|
import path55 from "node:path";
|
|
11012
|
-
import
|
|
11593
|
+
import fs43 from "fs-extra";
|
|
11013
11594
|
|
|
11014
11595
|
// src/claude/managed-paths.ts
|
|
11015
11596
|
init_paths();
|
|
@@ -11028,7 +11609,7 @@ init_paths();
|
|
|
11028
11609
|
async function collectCoreManagedPaths(root) {
|
|
11029
11610
|
const existing = [];
|
|
11030
11611
|
for (const abs of coreManagedAbsolutePaths(root)) {
|
|
11031
|
-
if (await
|
|
11612
|
+
if (await fs43.pathExists(abs)) existing.push(abs);
|
|
11032
11613
|
}
|
|
11033
11614
|
return existing;
|
|
11034
11615
|
}
|
|
@@ -11037,41 +11618,35 @@ async function collectLockTrackedPaths(root) {
|
|
|
11037
11618
|
const removable = [];
|
|
11038
11619
|
const preserved = [];
|
|
11039
11620
|
for (const row of lock ?? []) {
|
|
11040
|
-
const
|
|
11041
|
-
const
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11621
|
+
const id = row.id ?? "(unknown item)";
|
|
11622
|
+
const existing = await resolveExistingTrackedPaths(root, id, row.paths ?? [], log);
|
|
11623
|
+
if (existing.length === 0) continue;
|
|
11624
|
+
const gate = await hashGateEntry(root, { hash: row.hash }, existing);
|
|
11625
|
+
if (!gate.ok) {
|
|
11626
|
+
preserved.push(...existing.map((e) => e.abs));
|
|
11627
|
+
const detail = gate.reason === "no-hash" ? "has no lock hash" : "was modified locally";
|
|
11628
|
+
log(`Preserving ${id} (${detail}): ${existing.map((e) => e.rel).join(", ")}`);
|
|
11048
11629
|
continue;
|
|
11049
11630
|
}
|
|
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
|
-
}
|
|
11631
|
+
removable.push({ id, existing });
|
|
11057
11632
|
}
|
|
11058
11633
|
return { removable, preserved };
|
|
11059
11634
|
}
|
|
11060
11635
|
async function settingsHasHausContent(root) {
|
|
11061
11636
|
const settingsPath = claudePath(root, "settings.json");
|
|
11062
|
-
if (!await
|
|
11637
|
+
if (!await fs43.pathExists(settingsPath)) return false;
|
|
11063
11638
|
const settings = await readProjectSettings(root);
|
|
11064
11639
|
return settings._haus != null;
|
|
11065
11640
|
}
|
|
11066
11641
|
async function claudeMdHasHausBlock(root) {
|
|
11067
11642
|
const filePath = path55.join(root, "CLAUDE.md");
|
|
11068
|
-
if (!await
|
|
11069
|
-
const text = await
|
|
11643
|
+
if (!await fs43.pathExists(filePath)) return false;
|
|
11644
|
+
const text = await fs43.readFile(filePath, "utf8");
|
|
11070
11645
|
return text.includes(BLOCK_BEGIN);
|
|
11071
11646
|
}
|
|
11072
11647
|
async function stripProjectSettings(root) {
|
|
11073
11648
|
const settingsPath = claudePath(root, "settings.json");
|
|
11074
|
-
if (!await
|
|
11649
|
+
if (!await fs43.pathExists(settingsPath)) return false;
|
|
11075
11650
|
let settings = await readProjectSettings(root);
|
|
11076
11651
|
settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
|
|
11077
11652
|
const hasContent = Object.keys(settings).length > 0;
|
|
@@ -11080,43 +11655,43 @@ async function stripProjectSettings(root) {
|
|
|
11080
11655
|
log(`Stripped haus rules from ${path55.relative(root, settingsPath)} (user settings preserved).`);
|
|
11081
11656
|
return true;
|
|
11082
11657
|
}
|
|
11083
|
-
await
|
|
11658
|
+
await fs43.remove(settingsPath);
|
|
11084
11659
|
log(`Removed ${path55.relative(root, settingsPath)} (no user-owned settings remained).`);
|
|
11085
11660
|
return true;
|
|
11086
11661
|
}
|
|
11087
11662
|
async function stripRootClaudeMd(root) {
|
|
11088
11663
|
const filePath = path55.join(root, "CLAUDE.md");
|
|
11089
|
-
if (!await
|
|
11090
|
-
const prev = await
|
|
11664
|
+
if (!await fs43.pathExists(filePath)) return false;
|
|
11665
|
+
const prev = await fs43.readFile(filePath, "utf8");
|
|
11091
11666
|
if (!prev.includes(BLOCK_BEGIN)) return false;
|
|
11092
11667
|
const next = stripHausBlock(prev);
|
|
11093
11668
|
if (next.length === 0) {
|
|
11094
|
-
await
|
|
11669
|
+
await fs43.remove(filePath);
|
|
11095
11670
|
log("Removed CLAUDE.md (only contained haus import block).");
|
|
11096
11671
|
} else {
|
|
11097
|
-
await
|
|
11672
|
+
await fs43.writeFile(filePath, next, "utf8");
|
|
11098
11673
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
11099
11674
|
}
|
|
11100
11675
|
return true;
|
|
11101
11676
|
}
|
|
11102
11677
|
async function pruneDirIfEmpty(dir) {
|
|
11103
|
-
if (!await
|
|
11678
|
+
if (!await fs43.pathExists(dir)) return;
|
|
11104
11679
|
try {
|
|
11105
|
-
const entries = await
|
|
11106
|
-
if (entries.length === 0) await
|
|
11680
|
+
const entries = await fs43.readdir(dir);
|
|
11681
|
+
if (entries.length === 0) await fs43.remove(dir);
|
|
11107
11682
|
} catch {
|
|
11108
11683
|
}
|
|
11109
11684
|
}
|
|
11110
11685
|
async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
|
|
11111
11686
|
if (managedAbsPaths.length === 0) return;
|
|
11112
11687
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
11113
|
-
const backupRoot = hausPath(root, "backups",
|
|
11688
|
+
const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
|
|
11114
11689
|
for (const abs of managedAbsPaths) {
|
|
11115
|
-
if (!await
|
|
11690
|
+
if (!await fs43.pathExists(abs)) continue;
|
|
11116
11691
|
const rel = path55.relative(root, abs);
|
|
11117
11692
|
const backupPath = path55.join(backupRoot, rel);
|
|
11118
|
-
await
|
|
11119
|
-
await
|
|
11693
|
+
await fs43.ensureDir(path55.dirname(backupPath));
|
|
11694
|
+
await fs43.copy(abs, backupPath);
|
|
11120
11695
|
}
|
|
11121
11696
|
log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path55.relative(root, backupRoot)}.`);
|
|
11122
11697
|
}
|
|
@@ -11124,7 +11699,8 @@ async function runUndo(options) {
|
|
|
11124
11699
|
const root = options.root ?? process.cwd();
|
|
11125
11700
|
const coreManaged = await collectCoreManagedPaths(root);
|
|
11126
11701
|
const { removable: lockRemovable, preserved: lockPreserved } = await collectLockTrackedPaths(root);
|
|
11127
|
-
const
|
|
11702
|
+
const lockAbs = lockRemovable.flatMap(({ existing }) => existing.map((e) => e.abs));
|
|
11703
|
+
const managed = [.../* @__PURE__ */ new Set([...coreManaged, ...lockAbs])];
|
|
11128
11704
|
const stripSettings = await settingsHasHausContent(root);
|
|
11129
11705
|
const stripClaudeMd = await claudeMdHasHausBlock(root);
|
|
11130
11706
|
if (managed.length === 0 && !stripSettings && !stripClaudeMd) {
|
|
@@ -11136,9 +11712,7 @@ async function runUndo(options) {
|
|
|
11136
11712
|
if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
|
|
11137
11713
|
if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
|
|
11138
11714
|
if (lockPreserved.length > 0) {
|
|
11139
|
-
summaryParts.push(
|
|
11140
|
-
`(preserving ${lockPreserved.length} locally-modified catalog item file(s) \u2014 not shown above)`
|
|
11141
|
-
);
|
|
11715
|
+
summaryParts.push(`(preserving ${lockPreserved.length} catalog item file(s) \u2014 not shown above)`);
|
|
11142
11716
|
}
|
|
11143
11717
|
if (options.dryRun) {
|
|
11144
11718
|
log(`[dry-run] Would remove:
|
|
@@ -11158,11 +11732,14 @@ User-owned .claude/ files will be preserved.`
|
|
|
11158
11732
|
}
|
|
11159
11733
|
}
|
|
11160
11734
|
await backupManagedFilesBeforeUndo(root, managed);
|
|
11161
|
-
for (const abs of
|
|
11162
|
-
if (!await
|
|
11163
|
-
await
|
|
11735
|
+
for (const abs of coreManaged) {
|
|
11736
|
+
if (!await fs43.pathExists(abs)) continue;
|
|
11737
|
+
await fs43.remove(abs);
|
|
11164
11738
|
log(`Removed ${path55.relative(root, abs)}`);
|
|
11165
11739
|
}
|
|
11740
|
+
for (const { id, existing } of lockRemovable) {
|
|
11741
|
+
await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
|
|
11742
|
+
}
|
|
11166
11743
|
if (stripSettings) await stripProjectSettings(root);
|
|
11167
11744
|
if (stripClaudeMd) await stripRootClaudeMd(root);
|
|
11168
11745
|
await pruneDirIfEmpty(claudePath(root));
|
|
@@ -11176,7 +11753,7 @@ init_logger();
|
|
|
11176
11753
|
init_paths();
|
|
11177
11754
|
import crypto3 from "node:crypto";
|
|
11178
11755
|
import path56 from "node:path";
|
|
11179
|
-
import
|
|
11756
|
+
import fs44 from "fs-extra";
|
|
11180
11757
|
async function runUninstall(options = {}) {
|
|
11181
11758
|
const { force = false, removePlugins = removeGlobalPlugins } = options;
|
|
11182
11759
|
const manifest = await readManifest();
|
|
@@ -11213,7 +11790,7 @@ async function runUninstall(options = {}) {
|
|
|
11213
11790
|
);
|
|
11214
11791
|
}
|
|
11215
11792
|
for (const entry of manifest.files) {
|
|
11216
|
-
const exists =
|
|
11793
|
+
const exists = fs44.pathExistsSync(entry.destPath);
|
|
11217
11794
|
if (!exists) continue;
|
|
11218
11795
|
const content2 = await readText(entry.destPath);
|
|
11219
11796
|
if (content2 === void 0) continue;
|
|
@@ -11231,7 +11808,7 @@ async function runUninstall(options = {}) {
|
|
|
11231
11808
|
result.skipped.push(entry.destPath);
|
|
11232
11809
|
continue;
|
|
11233
11810
|
}
|
|
11234
|
-
await
|
|
11811
|
+
await fs44.remove(entry.destPath);
|
|
11235
11812
|
await pruneEmptyDir(path56.dirname(entry.destPath));
|
|
11236
11813
|
result.deleted.push(entry.destPath);
|
|
11237
11814
|
}
|
|
@@ -11241,13 +11818,13 @@ async function runUninstall(options = {}) {
|
|
|
11241
11818
|
result.hooksStripped = true;
|
|
11242
11819
|
const hausDir = path56.join(globalClaudeDir(), "haus");
|
|
11243
11820
|
const manifestPath2 = hausManifestPath();
|
|
11244
|
-
if (
|
|
11245
|
-
await
|
|
11821
|
+
if (fs44.pathExistsSync(manifestPath2)) {
|
|
11822
|
+
await fs44.remove(manifestPath2);
|
|
11246
11823
|
}
|
|
11247
|
-
if (
|
|
11824
|
+
if (fs44.pathExistsSync(hausDir)) {
|
|
11248
11825
|
try {
|
|
11249
|
-
const remaining = await
|
|
11250
|
-
if (remaining.length === 0) await
|
|
11826
|
+
const remaining = await fs44.readdir(hausDir);
|
|
11827
|
+
if (remaining.length === 0) await fs44.remove(hausDir);
|
|
11251
11828
|
} catch {
|
|
11252
11829
|
}
|
|
11253
11830
|
}
|
|
@@ -11323,7 +11900,7 @@ init_forbidden_content();
|
|
|
11323
11900
|
init_manifest_item_fields();
|
|
11324
11901
|
init_path_safety();
|
|
11325
11902
|
init_validation_rules2();
|
|
11326
|
-
import
|
|
11903
|
+
import fs45 from "node:fs";
|
|
11327
11904
|
import path57 from "node:path";
|
|
11328
11905
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
11329
11906
|
var ITEM_ID_PATTERN = /^haus\./;
|
|
@@ -11401,11 +11978,11 @@ function auditOptInMetadata(items) {
|
|
|
11401
11978
|
}
|
|
11402
11979
|
return failures;
|
|
11403
11980
|
}
|
|
11404
|
-
function auditManifestStructure(
|
|
11981
|
+
function auditManifestStructure(manifestVersion2, items) {
|
|
11405
11982
|
const failures = [];
|
|
11406
11983
|
const seenIds = /* @__PURE__ */ new Map();
|
|
11407
11984
|
const seenPaths = /* @__PURE__ */ new Map();
|
|
11408
|
-
if (typeof
|
|
11985
|
+
if (typeof manifestVersion2 !== "string" || !ITEM_SEMVER_RE.test(manifestVersion2)) {
|
|
11409
11986
|
failures.push('manifest.json: top-level "version" is not valid semver (expected X.Y.Z)');
|
|
11410
11987
|
}
|
|
11411
11988
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -11468,7 +12045,7 @@ function checkRequiredFrontmatter(text, label) {
|
|
|
11468
12045
|
}
|
|
11469
12046
|
function readForAudit(absPath, label, failures) {
|
|
11470
12047
|
try {
|
|
11471
|
-
return
|
|
12048
|
+
return fs45.readFileSync(absPath, "utf8");
|
|
11472
12049
|
} catch (err) {
|
|
11473
12050
|
failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
11474
12051
|
return null;
|
|
@@ -11500,7 +12077,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11500
12077
|
const absPath = path57.join(manifestDir, item.path);
|
|
11501
12078
|
if (item.type === "skill") {
|
|
11502
12079
|
const skillMd = path57.join(absPath, "SKILL.md");
|
|
11503
|
-
if (!
|
|
12080
|
+
if (!fs45.existsSync(skillMd)) {
|
|
11504
12081
|
failures.push(`${item.id}: missing ${path57.relative(manifestDir, skillMd)}`);
|
|
11505
12082
|
continue;
|
|
11506
12083
|
}
|
|
@@ -11511,7 +12088,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11511
12088
|
...auditForbiddenTagsInText(text, `${item.id}: ${path57.relative(manifestDir, skillMd)}`)
|
|
11512
12089
|
);
|
|
11513
12090
|
} else if (item.type === "agent") {
|
|
11514
|
-
if (!
|
|
12091
|
+
if (!fs45.existsSync(absPath)) {
|
|
11515
12092
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
11516
12093
|
continue;
|
|
11517
12094
|
}
|
|
@@ -11521,13 +12098,13 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11521
12098
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
11522
12099
|
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
11523
12100
|
} else if (item.type === "template") {
|
|
11524
|
-
if (!
|
|
12101
|
+
if (!fs45.existsSync(absPath)) {
|
|
11525
12102
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
11526
12103
|
continue;
|
|
11527
12104
|
}
|
|
11528
12105
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
11529
12106
|
} else if (item.type === "command") {
|
|
11530
|
-
if (!
|
|
12107
|
+
if (!fs45.existsSync(absPath)) {
|
|
11531
12108
|
failures.push(`${item.id}: missing command file ${item.path}`);
|
|
11532
12109
|
continue;
|
|
11533
12110
|
}
|
|
@@ -11537,15 +12114,15 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
11537
12114
|
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
11538
12115
|
failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
|
|
11539
12116
|
} else if (item.type === "config") {
|
|
11540
|
-
if (!
|
|
12117
|
+
if (!fs45.existsSync(absPath)) {
|
|
11541
12118
|
failures.push(`${item.id}: missing config path ${item.path}`);
|
|
11542
12119
|
}
|
|
11543
12120
|
} else if (item.type === "rule") {
|
|
11544
|
-
if (!
|
|
12121
|
+
if (!fs45.existsSync(absPath)) {
|
|
11545
12122
|
failures.push(`${item.id}: missing rule file ${item.path}`);
|
|
11546
12123
|
}
|
|
11547
12124
|
} else if (item.type === "asset") {
|
|
11548
|
-
if (!
|
|
12125
|
+
if (!fs45.existsSync(absPath)) {
|
|
11549
12126
|
failures.push(`${item.id}: missing asset file ${item.path}`);
|
|
11550
12127
|
}
|
|
11551
12128
|
}
|
|
@@ -11580,7 +12157,7 @@ function resolveItemType(relPath, pathTypeMap) {
|
|
|
11580
12157
|
function walkAllFiles(dir, fn, failures) {
|
|
11581
12158
|
let entries;
|
|
11582
12159
|
try {
|
|
11583
|
-
entries =
|
|
12160
|
+
entries = fs45.readdirSync(dir, { withFileTypes: true });
|
|
11584
12161
|
} catch (err) {
|
|
11585
12162
|
failures.push(
|
|
11586
12163
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -11614,7 +12191,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
11614
12191
|
const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
|
|
11615
12192
|
for (const dir of CONTENT_ROOTS) {
|
|
11616
12193
|
const abs = path57.join(manifestDir, dir);
|
|
11617
|
-
if (!
|
|
12194
|
+
if (!fs45.existsSync(abs)) continue;
|
|
11618
12195
|
walkAllFiles(
|
|
11619
12196
|
abs,
|
|
11620
12197
|
(file) => {
|
|
@@ -11631,7 +12208,7 @@ function auditDiskOrphans(manifestDir, items) {
|
|
|
11631
12208
|
function walkMd(dir, fn, failures) {
|
|
11632
12209
|
let entries;
|
|
11633
12210
|
try {
|
|
11634
|
-
entries =
|
|
12211
|
+
entries = fs45.readdirSync(dir, { withFileTypes: true });
|
|
11635
12212
|
} catch (err) {
|
|
11636
12213
|
failures.push(
|
|
11637
12214
|
`${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
|
|
@@ -11650,7 +12227,7 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
11650
12227
|
const pathTypeMap = buildItemPathTypeMap(items);
|
|
11651
12228
|
for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
|
|
11652
12229
|
const abs = path57.join(manifestDir, dir);
|
|
11653
|
-
if (!
|
|
12230
|
+
if (!fs45.existsSync(abs)) continue;
|
|
11654
12231
|
const dirType = DIR_ITEM_TYPE[dir] ?? "";
|
|
11655
12232
|
walkMd(
|
|
11656
12233
|
abs,
|
|
@@ -11678,9 +12255,9 @@ function auditMarkdownContent(manifestDir, items) {
|
|
|
11678
12255
|
}
|
|
11679
12256
|
return failures;
|
|
11680
12257
|
}
|
|
11681
|
-
function validateCatalogData(manifestDir,
|
|
12258
|
+
function validateCatalogData(manifestDir, manifestVersion2, items) {
|
|
11682
12259
|
const failures = [
|
|
11683
|
-
...auditManifestStructure(
|
|
12260
|
+
...auditManifestStructure(manifestVersion2, items),
|
|
11684
12261
|
...validateFormerIds(items),
|
|
11685
12262
|
...auditOptInMetadata(items),
|
|
11686
12263
|
...auditForbiddenStacks(items),
|
|
@@ -11726,7 +12303,7 @@ init_logger();
|
|
|
11726
12303
|
// src/workspace/worktree/add.ts
|
|
11727
12304
|
init_fs();
|
|
11728
12305
|
import path65 from "node:path";
|
|
11729
|
-
import
|
|
12306
|
+
import fs49 from "fs-extra";
|
|
11730
12307
|
|
|
11731
12308
|
// src/workspace/worktree/git-worktree.ts
|
|
11732
12309
|
init_exec();
|
|
@@ -11847,12 +12424,12 @@ async function hasUnpushedWork(cwd) {
|
|
|
11847
12424
|
|
|
11848
12425
|
// src/workspace/worktree/hydrate.ts
|
|
11849
12426
|
import path61 from "node:path";
|
|
11850
|
-
import
|
|
12427
|
+
import fs47 from "fs-extra";
|
|
11851
12428
|
|
|
11852
12429
|
// src/workspace/worktree/cow-copy.ts
|
|
11853
12430
|
init_exec();
|
|
11854
12431
|
import path59 from "node:path";
|
|
11855
|
-
import
|
|
12432
|
+
import fs46 from "fs-extra";
|
|
11856
12433
|
async function getMacFilesystemType(dir) {
|
|
11857
12434
|
const df = await runCommand("df", ["-P", dir]);
|
|
11858
12435
|
if (df.exitCode !== 0) return void 0;
|
|
@@ -11883,14 +12460,14 @@ async function detectCowStrategy(dir) {
|
|
|
11883
12460
|
return "unknown-platform";
|
|
11884
12461
|
}
|
|
11885
12462
|
async function cowCopyDir(src, dest) {
|
|
11886
|
-
const srcExists = await
|
|
12463
|
+
const srcExists = await fs46.pathExists(src);
|
|
11887
12464
|
if (!srcExists)
|
|
11888
12465
|
return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
|
|
11889
12466
|
const strategy = await detectCowStrategy(path59.dirname(dest));
|
|
11890
12467
|
if (strategy === "unsupported" || strategy === "unknown-platform") {
|
|
11891
12468
|
return { strategy, attempted: false, ok: false };
|
|
11892
12469
|
}
|
|
11893
|
-
await
|
|
12470
|
+
await fs46.ensureDir(path59.dirname(dest));
|
|
11894
12471
|
const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
|
|
11895
12472
|
const result = await runCommand("cp", args);
|
|
11896
12473
|
return {
|
|
@@ -12077,11 +12654,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12077
12654
|
for (const target of HYDRATION_TARGETS) {
|
|
12078
12655
|
const src = path61.join(mainMemberPath, target);
|
|
12079
12656
|
const dest = path61.join(worktreeMemberPath, target);
|
|
12080
|
-
if (!await
|
|
12081
|
-
const destExists = await
|
|
12657
|
+
if (!await fs47.pathExists(src)) continue;
|
|
12658
|
+
const destExists = await fs47.pathExists(dest);
|
|
12082
12659
|
if (!opts.force && destExists) continue;
|
|
12083
12660
|
if (opts.force && destExists) {
|
|
12084
|
-
await
|
|
12661
|
+
await fs47.remove(dest);
|
|
12085
12662
|
}
|
|
12086
12663
|
const copy = await cowCopyDir(src, dest);
|
|
12087
12664
|
targets.push({ target, copy });
|
|
@@ -12095,7 +12672,7 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
|
|
|
12095
12672
|
// src/workspace/worktree/local-files.ts
|
|
12096
12673
|
init_exec();
|
|
12097
12674
|
import path62 from "node:path";
|
|
12098
|
-
import
|
|
12675
|
+
import fs48 from "fs-extra";
|
|
12099
12676
|
var MACHINE_LOCAL_FILES = [
|
|
12100
12677
|
"docker-compose.local.yml",
|
|
12101
12678
|
path62.join(".claude", "settings.local.json")
|
|
@@ -12109,11 +12686,11 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
|
|
|
12109
12686
|
const copied = [];
|
|
12110
12687
|
for (const rel of MACHINE_LOCAL_FILES) {
|
|
12111
12688
|
const src = path62.join(mainMemberPath, rel);
|
|
12112
|
-
if (!await
|
|
12689
|
+
if (!await fs48.pathExists(src)) continue;
|
|
12113
12690
|
if (await isTracked(mainMemberPath, rel)) continue;
|
|
12114
12691
|
const dest = path62.join(worktreeMemberPath, rel);
|
|
12115
|
-
await
|
|
12116
|
-
await
|
|
12692
|
+
await fs48.ensureDir(path62.dirname(dest));
|
|
12693
|
+
await fs48.copy(src, dest, { overwrite: true });
|
|
12117
12694
|
copied.push(rel);
|
|
12118
12695
|
}
|
|
12119
12696
|
return copied;
|
|
@@ -12194,7 +12771,7 @@ async function runAdd(opts) {
|
|
|
12194
12771
|
}
|
|
12195
12772
|
const branch = opts.branch ?? opts.slug;
|
|
12196
12773
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
12197
|
-
if (await
|
|
12774
|
+
if (await fs49.pathExists(wsWorktreePath)) {
|
|
12198
12775
|
return {
|
|
12199
12776
|
ok: false,
|
|
12200
12777
|
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 +13140,14 @@ async function runHydrateWorktree(opts) {
|
|
|
12563
13140
|
|
|
12564
13141
|
// src/workspace/worktree/list.ts
|
|
12565
13142
|
import path69 from "node:path";
|
|
12566
|
-
import
|
|
13143
|
+
import fs50 from "fs-extra";
|
|
12567
13144
|
async function runList2() {
|
|
12568
13145
|
const resolved = await resolveWorkspaceForWorktree();
|
|
12569
13146
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
12570
13147
|
const { rootInfo, workspaceRoot } = resolved;
|
|
12571
13148
|
const dir = worktreesDir(workspaceRoot);
|
|
12572
|
-
if (!await
|
|
12573
|
-
const entries = await
|
|
13149
|
+
if (!await fs50.pathExists(dir)) return { ok: true, workspaceRoot, worktrees: [] };
|
|
13150
|
+
const entries = await fs50.readdir(dir, { withFileTypes: true });
|
|
12574
13151
|
const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
12575
13152
|
const configuredMembers = await readMembers(rootInfo);
|
|
12576
13153
|
const worktrees = [];
|
|
@@ -12581,7 +13158,7 @@ async function runList2() {
|
|
|
12581
13158
|
const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
|
|
12582
13159
|
for (const m of source) {
|
|
12583
13160
|
const memberPath = path69.join(wtPath, m.folder);
|
|
12584
|
-
const materialized = await
|
|
13161
|
+
const materialized = await fs50.pathExists(memberPath);
|
|
12585
13162
|
const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
|
|
12586
13163
|
members.push({
|
|
12587
13164
|
id: m.id,
|
|
@@ -12643,13 +13220,13 @@ async function runPrune2(opts) {
|
|
|
12643
13220
|
|
|
12644
13221
|
// src/workspace/worktree/remove.ts
|
|
12645
13222
|
import path70 from "node:path";
|
|
12646
|
-
import
|
|
13223
|
+
import fs51 from "fs-extra";
|
|
12647
13224
|
async function runRemove2(opts) {
|
|
12648
13225
|
const resolved = await resolveWorkspaceForWorktree();
|
|
12649
13226
|
if (!resolved.ok) return { ok: false, error: resolved.reason };
|
|
12650
13227
|
const { rootInfo, workspaceRoot } = resolved;
|
|
12651
13228
|
const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
|
|
12652
|
-
if (!await
|
|
13229
|
+
if (!await fs51.pathExists(wsWorktreePath)) {
|
|
12653
13230
|
return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
|
|
12654
13231
|
}
|
|
12655
13232
|
const state = await readWorktreeState(wsWorktreePath);
|
|
@@ -12674,7 +13251,7 @@ async function runRemove2(opts) {
|
|
|
12674
13251
|
}
|
|
12675
13252
|
const existingTargets = [];
|
|
12676
13253
|
for (const target of targets) {
|
|
12677
|
-
if (await
|
|
13254
|
+
if (await fs51.pathExists(target.path)) existingTargets.push(target);
|
|
12678
13255
|
}
|
|
12679
13256
|
if (!opts.force) {
|
|
12680
13257
|
const blockers = [];
|
|
@@ -12730,8 +13307,8 @@ async function runRemove2(opts) {
|
|
|
12730
13307
|
}
|
|
12731
13308
|
await pruneWorktrees(workspaceRoot);
|
|
12732
13309
|
}
|
|
12733
|
-
if (wsRemoveOk && await
|
|
12734
|
-
await
|
|
13310
|
+
if (wsRemoveOk && await fs51.pathExists(wsWorktreePath)) {
|
|
13311
|
+
await fs51.remove(wsWorktreePath);
|
|
12735
13312
|
}
|
|
12736
13313
|
return { ok: failed.length === 0, dryRun: false, slug: opts.slug, removed, failed };
|
|
12737
13314
|
}
|
|
@@ -13075,9 +13652,9 @@ init_fs();
|
|
|
13075
13652
|
init_paths();
|
|
13076
13653
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
13077
13654
|
import path71 from "node:path";
|
|
13078
|
-
var
|
|
13655
|
+
var MANIFEST_FILE2 = "workspace.manifest.json";
|
|
13079
13656
|
function manifestPath(workspaceRoot) {
|
|
13080
|
-
return hausPath(workspaceRoot,
|
|
13657
|
+
return hausPath(workspaceRoot, MANIFEST_FILE2);
|
|
13081
13658
|
}
|
|
13082
13659
|
function hausVersion() {
|
|
13083
13660
|
try {
|
|
@@ -13293,17 +13870,17 @@ init_logger();
|
|
|
13293
13870
|
|
|
13294
13871
|
// src/workspace/link-context/link.ts
|
|
13295
13872
|
import path75 from "node:path";
|
|
13296
|
-
import
|
|
13873
|
+
import fs54 from "fs-extra";
|
|
13297
13874
|
init_fs();
|
|
13298
13875
|
init_logger();
|
|
13299
13876
|
|
|
13300
13877
|
// src/workspace/link-context/plan.ts
|
|
13301
13878
|
import path74 from "node:path";
|
|
13302
|
-
import
|
|
13879
|
+
import fs53 from "fs-extra";
|
|
13303
13880
|
|
|
13304
13881
|
// src/workspace/link-context/scan-source.ts
|
|
13305
13882
|
import path73 from "node:path";
|
|
13306
|
-
import
|
|
13883
|
+
import fs52 from "fs-extra";
|
|
13307
13884
|
var ASSET_SUBDIR = {
|
|
13308
13885
|
skill: "skills",
|
|
13309
13886
|
agent: "agents",
|
|
@@ -13311,20 +13888,20 @@ var ASSET_SUBDIR = {
|
|
|
13311
13888
|
};
|
|
13312
13889
|
async function hasLinkableContext(member) {
|
|
13313
13890
|
for (const subdir of Object.values(ASSET_SUBDIR)) {
|
|
13314
|
-
if (await
|
|
13891
|
+
if (await fs52.pathExists(path73.join(member.absPath, ".claude", subdir))) return true;
|
|
13315
13892
|
}
|
|
13316
13893
|
return false;
|
|
13317
13894
|
}
|
|
13318
13895
|
async function collectSkills(member) {
|
|
13319
13896
|
const dir = path73.join(member.absPath, ".claude", "skills");
|
|
13320
|
-
if (!await
|
|
13321
|
-
const names = await
|
|
13897
|
+
if (!await fs52.pathExists(dir)) return [];
|
|
13898
|
+
const names = await fs52.readdir(dir);
|
|
13322
13899
|
const out = [];
|
|
13323
13900
|
for (const name of [...names].sort()) {
|
|
13324
13901
|
const abs = path73.join(dir, name);
|
|
13325
|
-
const stat = await
|
|
13902
|
+
const stat = await fs52.lstat(abs);
|
|
13326
13903
|
if (!stat.isDirectory()) continue;
|
|
13327
|
-
if (!await
|
|
13904
|
+
if (!await fs52.pathExists(path73.join(abs, "SKILL.md"))) continue;
|
|
13328
13905
|
out.push({ type: "skill", name, sourceRelPath: path73.posix.join(".claude", "skills", name) });
|
|
13329
13906
|
}
|
|
13330
13907
|
return out;
|
|
@@ -13332,13 +13909,13 @@ async function collectSkills(member) {
|
|
|
13332
13909
|
async function collectFlatMarkdown(member, type) {
|
|
13333
13910
|
const subdir = ASSET_SUBDIR[type];
|
|
13334
13911
|
const dir = path73.join(member.absPath, ".claude", subdir);
|
|
13335
|
-
if (!await
|
|
13336
|
-
const names = await
|
|
13912
|
+
if (!await fs52.pathExists(dir)) return [];
|
|
13913
|
+
const names = await fs52.readdir(dir);
|
|
13337
13914
|
const out = [];
|
|
13338
13915
|
for (const fileName of [...names].sort()) {
|
|
13339
13916
|
if (!fileName.endsWith(".md")) continue;
|
|
13340
13917
|
const abs = path73.join(dir, fileName);
|
|
13341
|
-
if (!(await
|
|
13918
|
+
if (!(await fs52.lstat(abs)).isFile()) continue;
|
|
13342
13919
|
const name = fileName.slice(0, -".md".length);
|
|
13343
13920
|
out.push({ type, name, sourceRelPath: path73.posix.join(".claude", subdir, fileName) });
|
|
13344
13921
|
}
|
|
@@ -13372,7 +13949,7 @@ async function buildLinkPlan(members) {
|
|
|
13372
13949
|
const byKey = /* @__PURE__ */ new Map();
|
|
13373
13950
|
const membersById = new Map(members.map((m) => [m.id, m]));
|
|
13374
13951
|
for (const member of members) {
|
|
13375
|
-
if (!await
|
|
13952
|
+
if (!await fs53.pathExists(member.absPath)) {
|
|
13376
13953
|
skipped.push({ repo: member.id, reason: `not cloned locally (${member.absPath}) \u2014 skipped` });
|
|
13377
13954
|
continue;
|
|
13378
13955
|
}
|
|
@@ -13453,15 +14030,15 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
13453
14030
|
const destRel = destRelPathFor(entry.type, entry.destKey);
|
|
13454
14031
|
const destAbs = path75.join(workspaceRoot, destRel);
|
|
13455
14032
|
if (write) {
|
|
13456
|
-
await
|
|
13457
|
-
if (await
|
|
13458
|
-
await
|
|
14033
|
+
await fs54.ensureDir(path75.dirname(destAbs));
|
|
14034
|
+
if (await fs54.pathExists(destAbs)) await fs54.remove(destAbs);
|
|
14035
|
+
await fs54.copy(entry.sourceAbsPath, destAbs, {
|
|
13459
14036
|
overwrite: true,
|
|
13460
14037
|
errorOnExist: false,
|
|
13461
14038
|
dereference: false,
|
|
13462
14039
|
filter: (s) => {
|
|
13463
14040
|
try {
|
|
13464
|
-
return !
|
|
14041
|
+
return !fs54.lstatSync(s).isSymbolicLink();
|
|
13465
14042
|
} catch {
|
|
13466
14043
|
return false;
|
|
13467
14044
|
}
|
|
@@ -13490,7 +14067,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
|
|
|
13490
14067
|
warn(`Refusing to remove linked context path outside the workspace root: ${old.path}`);
|
|
13491
14068
|
continue;
|
|
13492
14069
|
}
|
|
13493
|
-
if (await
|
|
14070
|
+
if (await fs54.pathExists(abs)) await fs54.remove(abs);
|
|
13494
14071
|
}
|
|
13495
14072
|
}
|
|
13496
14073
|
if (write) {
|
|
@@ -13546,7 +14123,7 @@ import path77 from "node:path";
|
|
|
13546
14123
|
|
|
13547
14124
|
// src/claude/write-workspace-claude-md.ts
|
|
13548
14125
|
import path76 from "node:path";
|
|
13549
|
-
import
|
|
14126
|
+
import fs55 from "fs-extra";
|
|
13550
14127
|
init_fs();
|
|
13551
14128
|
init_logger();
|
|
13552
14129
|
init_paths();
|
|
@@ -13571,8 +14148,8 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
|
|
|
13571
14148
|
const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path76.join(workspaceRoot, "CLAUDE.md");
|
|
13572
14149
|
const say = opts.quiet ? () => {
|
|
13573
14150
|
} : log;
|
|
13574
|
-
const existed = await
|
|
13575
|
-
const prev = existed ? await
|
|
14151
|
+
const existed = await fs55.pathExists(filePath);
|
|
14152
|
+
const prev = existed ? await fs55.readFile(filePath, "utf8") : "";
|
|
13576
14153
|
const next = opts.collision ? `${block}
|
|
13577
14154
|
` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
|
|
13578
14155
|
const printable = displayPath(workspaceRoot, filePath);
|
|
@@ -13644,34 +14221,49 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13644
14221
|
const repoRoot = path77.resolve(workspaceRoot, repo.path);
|
|
13645
14222
|
say(`
|
|
13646
14223
|
\u2192 ${repo.name} (${repo.path})`);
|
|
14224
|
+
let res;
|
|
13647
14225
|
try {
|
|
13648
14226
|
if (!existsSync9(repoRoot) || !statSync(repoRoot).isDirectory()) {
|
|
13649
14227
|
throw new Error(`Repo path is not a directory: ${repo.path}`);
|
|
13650
14228
|
}
|
|
13651
|
-
|
|
14229
|
+
res = await runSetupCore(repoRoot, {
|
|
13652
14230
|
apply,
|
|
13653
14231
|
dryRun: options.dryRun,
|
|
13654
14232
|
// Under --json, mute the inner pipeline entirely; we serialize the
|
|
13655
14233
|
// aggregated result below. Otherwise let it print its normal human output.
|
|
13656
|
-
quiet: options.json
|
|
14234
|
+
quiet: options.json,
|
|
14235
|
+
allowEmptyCache: options.allowEmptyCache
|
|
13657
14236
|
});
|
|
14237
|
+
} catch (err) {
|
|
14238
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
13658
14239
|
statuses.push({
|
|
13659
14240
|
name: repo.name,
|
|
13660
14241
|
path: repo.path,
|
|
13661
14242
|
root: repoRoot,
|
|
13662
|
-
status: "
|
|
13663
|
-
|
|
13664
|
-
recommendedCount: res.recommendedCount
|
|
14243
|
+
status: "failed",
|
|
14244
|
+
error: message2
|
|
13665
14245
|
});
|
|
13666
|
-
|
|
13667
|
-
|
|
13668
|
-
|
|
13669
|
-
|
|
14246
|
+
if (!options.continueOnError) {
|
|
14247
|
+
if (options.json) {
|
|
14248
|
+
const result2 = { workspaceRoot, statuses, written: [] };
|
|
14249
|
+
log(JSON.stringify(result2, null, 2));
|
|
14250
|
+
process.exitCode = 1;
|
|
14251
|
+
return result2;
|
|
14252
|
+
}
|
|
14253
|
+
throw new Error(`Setup failed for ${repo.name}: ${message2}`, { cause: err });
|
|
14254
|
+
}
|
|
14255
|
+
error(`Setup failed for ${repo.name}: ${message2}`);
|
|
14256
|
+
continue;
|
|
14257
|
+
}
|
|
14258
|
+
if (!res.setupOk) {
|
|
14259
|
+
const message2 = !res.catalogInstallOk ? "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." : "Setup wrote nothing for this repo \u2014 see the per-repo output above.";
|
|
13670
14260
|
statuses.push({
|
|
13671
14261
|
name: repo.name,
|
|
13672
14262
|
path: repo.path,
|
|
13673
14263
|
root: repoRoot,
|
|
13674
14264
|
status: "failed",
|
|
14265
|
+
roles: res.roles,
|
|
14266
|
+
recommendedCount: res.recommendedCount,
|
|
13675
14267
|
error: message2
|
|
13676
14268
|
});
|
|
13677
14269
|
if (!options.continueOnError) {
|
|
@@ -13681,10 +14273,21 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13681
14273
|
process.exitCode = 1;
|
|
13682
14274
|
return result2;
|
|
13683
14275
|
}
|
|
13684
|
-
throw new Error(`Setup failed for ${repo.name}: ${message2}
|
|
14276
|
+
throw new Error(`Setup failed for ${repo.name}: ${message2}`);
|
|
13685
14277
|
}
|
|
13686
14278
|
error(`Setup failed for ${repo.name}: ${message2}`);
|
|
14279
|
+
continue;
|
|
13687
14280
|
}
|
|
14281
|
+
statuses.push({
|
|
14282
|
+
name: repo.name,
|
|
14283
|
+
path: repo.path,
|
|
14284
|
+
root: repoRoot,
|
|
14285
|
+
status: "ok",
|
|
14286
|
+
roles: res.roles,
|
|
14287
|
+
recommendedCount: res.recommendedCount
|
|
14288
|
+
});
|
|
14289
|
+
const context = await readContextOrScan(repoRoot);
|
|
14290
|
+
aggregateInputs.push({ name: repo.name, path: repo.path, context });
|
|
13688
14291
|
}
|
|
13689
14292
|
const written = [];
|
|
13690
14293
|
if (apply && aggregateInputs.length > 0) {
|
|
@@ -13804,6 +14407,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
|
|
|
13804
14407
|
}
|
|
13805
14408
|
const ok = statuses.filter((s) => s.status === "ok").length;
|
|
13806
14409
|
const failed = statuses.length - ok;
|
|
14410
|
+
if (failed > 0) process.exitCode = 1;
|
|
13807
14411
|
const result = { workspaceRoot, statuses, written };
|
|
13808
14412
|
if (options.json) {
|
|
13809
14413
|
log(JSON.stringify(result, null, 2));
|
|
@@ -13820,7 +14424,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
|
|
|
13820
14424
|
// src/commands/workspace/undo.ts
|
|
13821
14425
|
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
13822
14426
|
import path78 from "node:path";
|
|
13823
|
-
import
|
|
14427
|
+
import fs56 from "fs-extra";
|
|
13824
14428
|
init_fs();
|
|
13825
14429
|
init_logger();
|
|
13826
14430
|
init_paths();
|
|
@@ -13871,7 +14475,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13871
14475
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
13872
14476
|
continue;
|
|
13873
14477
|
}
|
|
13874
|
-
if (await
|
|
14478
|
+
if (await fs56.pathExists(abs)) {
|
|
13875
14479
|
log(`[dry-run] Would remove ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
13876
14480
|
}
|
|
13877
14481
|
}
|
|
@@ -13881,11 +14485,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13881
14485
|
hausPath(workspaceRoot, "WORKSPACE.md")
|
|
13882
14486
|
];
|
|
13883
14487
|
for (const abs of workspaceArtifacts) {
|
|
13884
|
-
if (await
|
|
14488
|
+
if (await fs56.pathExists(abs)) {
|
|
13885
14489
|
log(`[dry-run] Would remove ${path78.relative(workspaceRoot, abs)}`);
|
|
13886
14490
|
}
|
|
13887
14491
|
}
|
|
13888
|
-
if (!await
|
|
14492
|
+
if (!await fs56.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
|
|
13889
14493
|
const claudeMd = await readText(path78.join(workspaceRoot, "CLAUDE.md"));
|
|
13890
14494
|
if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
|
|
13891
14495
|
log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
|
|
@@ -13925,26 +14529,26 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13925
14529
|
warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
|
|
13926
14530
|
continue;
|
|
13927
14531
|
}
|
|
13928
|
-
if (await
|
|
13929
|
-
await
|
|
14532
|
+
if (await fs56.pathExists(abs)) {
|
|
14533
|
+
await fs56.remove(abs);
|
|
13930
14534
|
log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
|
|
13931
14535
|
}
|
|
13932
14536
|
}
|
|
13933
14537
|
for (const rel of WORKSPACE_AGGREGATE_FILES) {
|
|
13934
14538
|
const abs = hausPath(workspaceRoot, rel);
|
|
13935
|
-
if (await
|
|
13936
|
-
await
|
|
14539
|
+
if (await fs56.pathExists(abs)) {
|
|
14540
|
+
await fs56.remove(abs);
|
|
13937
14541
|
log(`Removed ${path78.relative(workspaceRoot, abs)}`);
|
|
13938
14542
|
}
|
|
13939
14543
|
}
|
|
13940
14544
|
const manifestFile = manifestPath(workspaceRoot);
|
|
13941
|
-
if (await
|
|
13942
|
-
await
|
|
14545
|
+
if (await fs56.pathExists(manifestFile)) {
|
|
14546
|
+
await fs56.remove(manifestFile);
|
|
13943
14547
|
log(`Removed ${path78.relative(workspaceRoot, manifestFile)}`);
|
|
13944
14548
|
}
|
|
13945
14549
|
const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
|
|
13946
|
-
if (await
|
|
13947
|
-
await
|
|
14550
|
+
if (await fs56.pathExists(workspaceMdPath)) {
|
|
14551
|
+
await fs56.remove(workspaceMdPath);
|
|
13948
14552
|
log(`Removed ${path78.relative(workspaceRoot, workspaceMdPath)}`);
|
|
13949
14553
|
} else {
|
|
13950
14554
|
const claudeMdPath = path78.join(workspaceRoot, "CLAUDE.md");
|
|
@@ -13956,19 +14560,19 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
|
|
|
13956
14560
|
end: WORKSPACE_BLOCK_END
|
|
13957
14561
|
});
|
|
13958
14562
|
if (next.length === 0) {
|
|
13959
|
-
await
|
|
14563
|
+
await fs56.remove(claudeMdPath);
|
|
13960
14564
|
log("Removed CLAUDE.md (only contained the workspace haus import block).");
|
|
13961
14565
|
} else {
|
|
13962
|
-
await
|
|
14566
|
+
await fs56.writeFile(claudeMdPath, next, "utf8");
|
|
13963
14567
|
log("Removed haus import block from CLAUDE.md (user content preserved).");
|
|
13964
14568
|
}
|
|
13965
14569
|
} else if (prev && hasLegacyWorkspaceBlock(prev)) {
|
|
13966
14570
|
const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
|
|
13967
14571
|
if (next.length === 0) {
|
|
13968
|
-
await
|
|
14572
|
+
await fs56.remove(claudeMdPath);
|
|
13969
14573
|
log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
|
|
13970
14574
|
} else {
|
|
13971
|
-
await
|
|
14575
|
+
await fs56.writeFile(claudeMdPath, next, "utf8");
|
|
13972
14576
|
log(
|
|
13973
14577
|
"Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
|
|
13974
14578
|
);
|
|
@@ -14086,7 +14690,8 @@ async function runWorkspace(action, options = {}) {
|
|
|
14086
14690
|
dryRun: options.dryRun,
|
|
14087
14691
|
json: options.json,
|
|
14088
14692
|
continueOnError: options.continueOnError,
|
|
14089
|
-
only: normalizeOnly(options.only)
|
|
14693
|
+
only: normalizeOnly(options.only),
|
|
14694
|
+
allowEmptyCache: options.allowEmptyCache
|
|
14090
14695
|
});
|
|
14091
14696
|
return;
|
|
14092
14697
|
case "doctor":
|
|
@@ -14130,6 +14735,9 @@ function validateRuntimeNodeVersion() {
|
|
|
14130
14735
|
}
|
|
14131
14736
|
validateRuntimeNodeVersion();
|
|
14132
14737
|
program.name("haus").description("Haus AI workflow CLI").version(cliVersion());
|
|
14738
|
+
program.option("--offline", "Make no network calls; use cached or bundled catalog content only").hook("preAction", (thisCommand) => {
|
|
14739
|
+
setOfflineFlag(Boolean(thisCommand.opts()["offline"]));
|
|
14740
|
+
});
|
|
14133
14741
|
program.command("scan").description(
|
|
14134
14742
|
"Scan the repo for roles, dependencies, and package manager; writes context-map.json"
|
|
14135
14743
|
).option("--json").action(runScan);
|
|
@@ -14146,7 +14754,7 @@ program.command("apply").description("Write recommended catalog items and core C
|
|
|
14146
14754
|
"Install exactly these recommended item ids non-interactively (skill backend; space- or comma-separated)"
|
|
14147
14755
|
).option(
|
|
14148
14756
|
"--allow-empty-cache",
|
|
14149
|
-
"
|
|
14757
|
+
"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
14758
|
).option(
|
|
14151
14759
|
"--refill-config",
|
|
14152
14760
|
"Fill still-blank fields in an existing workflow-config.md without touching edited ones"
|
|
@@ -14180,14 +14788,26 @@ program.command("explain-recommendation").description("Print a per-item breakdow
|
|
|
14180
14788
|
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
14789
|
"--force",
|
|
14182
14790
|
"Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
|
|
14791
|
+
).option(
|
|
14792
|
+
"--allow-empty-cache",
|
|
14793
|
+
"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
14794
|
).action(runInit);
|
|
14184
|
-
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").
|
|
14795
|
+
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").option(
|
|
14796
|
+
"--allow-empty-cache",
|
|
14797
|
+
"Proceed when no catalog item has installable content: write core files and skip the catalog items. Still exits 1 if none installed (ADR-0060)"
|
|
14798
|
+
).action(runNew);
|
|
14185
14799
|
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(
|
|
14186
14800
|
(url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
|
|
14187
14801
|
);
|
|
14188
14802
|
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
14803
|
program.command("catalog-audit").description("Check the bundled catalog manifest for unsupported or forbidden stacks").action(runCatalogAudit);
|
|
14190
14804
|
program.command("validate-catalog").description("Validate a catalog manifest at an explicit path").argument("[manifest]").action(runValidateCatalog);
|
|
14805
|
+
var catalog = program.command("catalog").description("Catalog cache maintenance");
|
|
14806
|
+
var catalogBundle = catalog.command("bundle").description("Move catalog content between machines (for air-gapped installs)");
|
|
14807
|
+
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 }));
|
|
14808
|
+
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(
|
|
14809
|
+
(file, opts) => runBundleImport(file, { dryRun: opts.dryRun })
|
|
14810
|
+
);
|
|
14191
14811
|
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
14812
|
program.command("ci-gate").description(
|
|
14193
14813
|
"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 +14836,10 @@ var workspace = program.command("workspace").description("Multi-repo workspace c
|
|
|
14216
14836
|
workspace.command("init").option("-y, --yes", "Skip confirmation when haus.workspace.yaml already exists").action((opts) => runWorkspace("init", opts));
|
|
14217
14837
|
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
14838
|
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").
|
|
14839
|
+
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(
|
|
14840
|
+
"--allow-empty-cache",
|
|
14841
|
+
"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)"
|
|
14842
|
+
).action((opts) => runWorkspace("setup", opts));
|
|
14220
14843
|
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
14844
|
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
14845
|
workspace.command("link-context").description(
|