@haus-tech/haus-workflow 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +9 -6
- package/dist/cli.js +590 -421
- package/library/catalog/manifest.json +1 -1
- package/library/catalog/validation-rules.json +0 -7
- package/library/global/commands/haus-cloneandsetup.md +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.22.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.22.0...v0.22.1) (2026-06-11)
|
|
4
|
+
|
|
5
|
+
## [0.22.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.21.0...v0.22.0) (2026-06-11)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **audit:** offline, perf cleanup, CI ([#92](https://github.com/WeAreHausTech/haus-workflow/issues/92)) ([a133f06](https://github.com/WeAreHausTech/haus-workflow/commit/a133f06d52b9a9e2a15f949c443c7042ed125735))
|
|
10
|
+
- **catalog:** ingest chokepoint ([#91](https://github.com/WeAreHausTech/haus-workflow/issues/91)) ([712d72b](https://github.com/WeAreHausTech/haus-workflow/commit/712d72b252ae3181bcdb9ce02614ede098c5bab5))
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- **cloneandsetup:** read repo setup docs before file-heuristics ([#95](https://github.com/WeAreHausTech/haus-workflow/issues/95)) ([8219b92](https://github.com/WeAreHausTech/haus-workflow/commit/8219b92d1c40b71ecb78661929ca57e1fe858603))
|
|
15
|
+
|
|
3
16
|
## [0.21.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.20.0...v0.21.0) (2026-06-11)
|
|
4
17
|
|
|
5
18
|
### Features
|
package/README.md
CHANGED
|
@@ -34,12 +34,14 @@ The `project:*` tasks act on the current repo. The unprefixed verbs (`update`,
|
|
|
34
34
|
(`~/.claude` + npm), like `npm install -g`. The short legacy names still work.
|
|
35
35
|
|
|
36
36
|
```
|
|
37
|
-
/haus-workflow
|
|
38
|
-
/haus-workflow project:init
|
|
39
|
-
/haus-workflow project:
|
|
40
|
-
/haus-workflow project:
|
|
41
|
-
/haus-workflow
|
|
42
|
-
/haus-workflow
|
|
37
|
+
/haus-workflow # interactive menu — pick a task
|
|
38
|
+
/haus-workflow project:init # [project] add haus to an EXISTING repo — AI skills, commands, workflow + docs
|
|
39
|
+
/haus-workflow project:clone [name] # [project] clone a workspace's repos (repos.manifest.json), or find & clone one repo by name from GitHub
|
|
40
|
+
/haus-workflow project:cloneandsetup [name] # [project] like project:clone, then set up each repo locally (node version, deps, .env)
|
|
41
|
+
/haus-workflow project:refresh # [project] refresh .claude/ and regenerate CLAUDE.md imports
|
|
42
|
+
/haus-workflow project:doctor # [project] health check for drift
|
|
43
|
+
/haus-workflow update # [global] update npm package + catalog + ~/.claude/
|
|
44
|
+
/haus-workflow catalog # [global] fetch only the latest catalog
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
Without an argument, the skill presents a menu so you can pick the task. With an argument, it runs immediately.
|
|
@@ -63,6 +65,7 @@ Scans the repo, recommends context assets, and writes `.claude/` and `.haus-work
|
|
|
63
65
|
```bash
|
|
64
66
|
haus init # first-run setup (scan → recommend → apply)
|
|
65
67
|
haus setup-project # re-run setup on existing project
|
|
68
|
+
haus clone <url> [dir] # clone a single git repo by URL (the primitive project:clone / project:cloneandsetup loop over)
|
|
66
69
|
haus scan # scan repo and write context-map
|
|
67
70
|
haus recommend # recommend catalog items (binary eligibility)
|
|
68
71
|
haus apply --dry-run # preview what would be written
|
package/dist/cli.js
CHANGED
|
@@ -12,8 +12,66 @@ import fs12 from "fs-extra";
|
|
|
12
12
|
|
|
13
13
|
// src/catalog/remote-catalog.ts
|
|
14
14
|
import os from "os";
|
|
15
|
+
import path2 from "path";
|
|
16
|
+
import fs2 from "fs-extra";
|
|
17
|
+
|
|
18
|
+
// src/utils/fs.ts
|
|
19
|
+
import crypto from "crypto";
|
|
15
20
|
import path from "path";
|
|
21
|
+
import fg from "fast-glob";
|
|
16
22
|
import fs from "fs-extra";
|
|
23
|
+
async function readJson(file) {
|
|
24
|
+
try {
|
|
25
|
+
return await fs.readJson(file);
|
|
26
|
+
} catch {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function writeJson(file, value) {
|
|
31
|
+
await fs.ensureDir(path.dirname(file));
|
|
32
|
+
await fs.writeFile(file, `${JSON.stringify(value, null, 2)}
|
|
33
|
+
`, "utf8");
|
|
34
|
+
}
|
|
35
|
+
async function pruneEmptyDir(dir) {
|
|
36
|
+
try {
|
|
37
|
+
const entries = await fs.readdir(dir);
|
|
38
|
+
if (entries.length === 0) await fs.remove(dir);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function readText(file) {
|
|
43
|
+
try {
|
|
44
|
+
return await fs.readFile(file, "utf8");
|
|
45
|
+
} catch {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function writeText(file, value) {
|
|
50
|
+
await fs.ensureDir(path.dirname(file));
|
|
51
|
+
await fs.writeFile(file, value, "utf8");
|
|
52
|
+
}
|
|
53
|
+
async function listFiles(root, patterns) {
|
|
54
|
+
const files = await fg(patterns, {
|
|
55
|
+
cwd: root,
|
|
56
|
+
dot: true,
|
|
57
|
+
onlyFiles: true,
|
|
58
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
|
|
59
|
+
});
|
|
60
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
61
|
+
}
|
|
62
|
+
function hashText(value) {
|
|
63
|
+
return `sha256-${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
64
|
+
}
|
|
65
|
+
async function mapWithConcurrency(items, fn, concurrency = 24) {
|
|
66
|
+
const size = Number.isFinite(concurrency) ? Math.max(1, Math.floor(concurrency)) : 24;
|
|
67
|
+
const results = new Array(items.length);
|
|
68
|
+
for (let i = 0; i < items.length; i += size) {
|
|
69
|
+
const batch = items.slice(i, i + size);
|
|
70
|
+
const settled = await Promise.all(batch.map((item, j) => fn(item, i + j)));
|
|
71
|
+
for (let j = 0; j < settled.length; j += 1) results[i + j] = settled[j];
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
17
75
|
|
|
18
76
|
// src/utils/logger.ts
|
|
19
77
|
var log = (msg, ...args) => {
|
|
@@ -28,15 +86,347 @@ var error = (msg, ...args) => {
|
|
|
28
86
|
|
|
29
87
|
// src/catalog/constants.ts
|
|
30
88
|
var CATALOG_REPO_URL = "https://raw.githubusercontent.com/wearehaustech/haus-workflow-catalog";
|
|
31
|
-
var CATALOG_REF = process.env.HAUS_CATALOG_REF
|
|
89
|
+
var CATALOG_REF = process.env.HAUS_CATALOG_REF;
|
|
32
90
|
var CATALOG_CACHE_SUBDIR = ".claude/haus/catalog-cache";
|
|
33
91
|
|
|
92
|
+
// library/catalog/validation-rules.json
|
|
93
|
+
var validation_rules_default = {
|
|
94
|
+
forbiddenTags: [
|
|
95
|
+
"python",
|
|
96
|
+
"django",
|
|
97
|
+
"go",
|
|
98
|
+
"rust",
|
|
99
|
+
"java",
|
|
100
|
+
"spring",
|
|
101
|
+
"kotlin",
|
|
102
|
+
"swift",
|
|
103
|
+
"android",
|
|
104
|
+
"flutter",
|
|
105
|
+
"dart",
|
|
106
|
+
"c++",
|
|
107
|
+
"perl",
|
|
108
|
+
"defi",
|
|
109
|
+
"trading"
|
|
110
|
+
],
|
|
111
|
+
requiredSkillFrontmatter: ["description"],
|
|
112
|
+
riskyInstallPatterns: [
|
|
113
|
+
{ source: "\\bnpx\\s+-y\\b", flags: "i" },
|
|
114
|
+
{ source: "\\bnpx\\s+--yes\\b", flags: "i" },
|
|
115
|
+
{ source: "\\byarn\\s+dlx\\b", flags: "i" },
|
|
116
|
+
{ source: "\\bpnpm\\s+dlx\\b", flags: "i" }
|
|
117
|
+
],
|
|
118
|
+
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
119
|
+
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
120
|
+
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
121
|
+
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
122
|
+
allowedStacks: [
|
|
123
|
+
"haus",
|
|
124
|
+
"security",
|
|
125
|
+
"quality",
|
|
126
|
+
"frontend",
|
|
127
|
+
"backend",
|
|
128
|
+
"testing",
|
|
129
|
+
"review",
|
|
130
|
+
"workflow",
|
|
131
|
+
"reference-pack",
|
|
132
|
+
"core-skill",
|
|
133
|
+
"workflow-skill",
|
|
134
|
+
"stack-skill",
|
|
135
|
+
"review-skill",
|
|
136
|
+
"agent",
|
|
137
|
+
"hook",
|
|
138
|
+
"rule",
|
|
139
|
+
"react",
|
|
140
|
+
"typescript",
|
|
141
|
+
"php",
|
|
142
|
+
"csharp",
|
|
143
|
+
"vendure",
|
|
144
|
+
"vendure3",
|
|
145
|
+
"nestjs",
|
|
146
|
+
"graphql",
|
|
147
|
+
"nx21",
|
|
148
|
+
"turbo",
|
|
149
|
+
"nextjs",
|
|
150
|
+
"react19",
|
|
151
|
+
"typescript5",
|
|
152
|
+
"vite8",
|
|
153
|
+
"tanstack-query",
|
|
154
|
+
"tanstack-router",
|
|
155
|
+
"radix",
|
|
156
|
+
"radix-ui",
|
|
157
|
+
"shadcn",
|
|
158
|
+
"shadcn-ui",
|
|
159
|
+
"tailwind",
|
|
160
|
+
"tailwindcss",
|
|
161
|
+
"scss",
|
|
162
|
+
"scss-modules",
|
|
163
|
+
"vue",
|
|
164
|
+
"expressjs",
|
|
165
|
+
"soup-base",
|
|
166
|
+
"laravel",
|
|
167
|
+
"laravel-nova",
|
|
168
|
+
"wordpress",
|
|
169
|
+
"bedrock",
|
|
170
|
+
"elementor-pro",
|
|
171
|
+
"acf-pro",
|
|
172
|
+
"jetengine",
|
|
173
|
+
"dotnet",
|
|
174
|
+
"oidc",
|
|
175
|
+
"azure-ad",
|
|
176
|
+
"bankid",
|
|
177
|
+
"crypto",
|
|
178
|
+
"postgresql",
|
|
179
|
+
"mariadb",
|
|
180
|
+
"mssql",
|
|
181
|
+
"elasticsearch",
|
|
182
|
+
"yarn4",
|
|
183
|
+
"pnpm89",
|
|
184
|
+
"playwright",
|
|
185
|
+
"testing-library",
|
|
186
|
+
"phpunit",
|
|
187
|
+
"storybook",
|
|
188
|
+
"vitest",
|
|
189
|
+
"jest",
|
|
190
|
+
"redis",
|
|
191
|
+
"sanity",
|
|
192
|
+
"strapi",
|
|
193
|
+
"prisma",
|
|
194
|
+
"cms",
|
|
195
|
+
"database",
|
|
196
|
+
"mysql",
|
|
197
|
+
"saml2",
|
|
198
|
+
"next-auth",
|
|
199
|
+
"auth",
|
|
200
|
+
"expo",
|
|
201
|
+
"react-native",
|
|
202
|
+
"mobile",
|
|
203
|
+
"i18next",
|
|
204
|
+
"i18n",
|
|
205
|
+
"bullmq",
|
|
206
|
+
"queue",
|
|
207
|
+
"sentry",
|
|
208
|
+
"observability",
|
|
209
|
+
"tooling",
|
|
210
|
+
"prettier",
|
|
211
|
+
"eslint",
|
|
212
|
+
"missing-prettier",
|
|
213
|
+
"missing-eslint",
|
|
214
|
+
"docker",
|
|
215
|
+
"pm2",
|
|
216
|
+
"stripe",
|
|
217
|
+
"qliro",
|
|
218
|
+
"supabase",
|
|
219
|
+
"payments"
|
|
220
|
+
],
|
|
221
|
+
alwaysAllowedTags: [
|
|
222
|
+
"haus",
|
|
223
|
+
"security",
|
|
224
|
+
"quality",
|
|
225
|
+
"review",
|
|
226
|
+
"workflow",
|
|
227
|
+
"baseline",
|
|
228
|
+
"project-instructions"
|
|
229
|
+
],
|
|
230
|
+
patternTagSuffixes: ["-patterns"]
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/catalog/validation-rules.ts
|
|
234
|
+
var toRegExp = (r) => new RegExp(r.source, r.flags);
|
|
235
|
+
var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
|
|
236
|
+
var REQUIRED_SKILL_FRONTMATTER = validation_rules_default.requiredSkillFrontmatter;
|
|
237
|
+
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
238
|
+
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
239
|
+
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
240
|
+
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
241
|
+
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
242
|
+
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
243
|
+
var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
|
|
244
|
+
var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
|
|
245
|
+
var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
|
|
246
|
+
function isTagAllowed(tag) {
|
|
247
|
+
const lower = tag.toLowerCase();
|
|
248
|
+
if (ALLOWED_SET.has(lower)) return true;
|
|
249
|
+
return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
250
|
+
}
|
|
251
|
+
function auditDisallowedTags(items) {
|
|
252
|
+
const failures = [];
|
|
253
|
+
for (const item of items) {
|
|
254
|
+
if (!item.id) continue;
|
|
255
|
+
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
256
|
+
if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return failures;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/catalog/forbidden-content.ts
|
|
263
|
+
var PROSE_FORBIDDEN_TAGS = FORBIDDEN_TAGS.filter((t) => t.toLowerCase() !== "go");
|
|
264
|
+
function escapeRegExp(value) {
|
|
265
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
266
|
+
}
|
|
267
|
+
function extractUseWhenSection(text) {
|
|
268
|
+
const marker = "## Use when";
|
|
269
|
+
const idx = text.toLowerCase().indexOf(marker.toLowerCase());
|
|
270
|
+
if (idx < 0) return "";
|
|
271
|
+
const tail = text.slice(idx + marker.length);
|
|
272
|
+
const next = tail.search(/\n##\s+/);
|
|
273
|
+
return next < 0 ? tail : tail.slice(0, next);
|
|
274
|
+
}
|
|
275
|
+
function isYamlBlockScalarHeader(rest) {
|
|
276
|
+
return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
|
|
277
|
+
}
|
|
278
|
+
function extractFrontmatterValue(text, key) {
|
|
279
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
280
|
+
if (!m) return "";
|
|
281
|
+
const lines = m[1].split(/\r?\n/);
|
|
282
|
+
const keyRe = new RegExp(`^${escapeRegExp(key)}:[ \\t]*`);
|
|
283
|
+
const idx = lines.findIndex((l) => keyRe.test(l));
|
|
284
|
+
if (idx < 0) return "";
|
|
285
|
+
const rest = lines[idx].replace(keyRe, "").trim();
|
|
286
|
+
if (!rest) return "";
|
|
287
|
+
if (!isYamlBlockScalarHeader(rest)) {
|
|
288
|
+
return rest.replace(/^["']|["']$/g, "").trim();
|
|
289
|
+
}
|
|
290
|
+
const body = [];
|
|
291
|
+
for (let j = idx + 1; j < lines.length; j++) {
|
|
292
|
+
const line2 = lines[j];
|
|
293
|
+
if (/^[a-zA-Z_][\w.-]*:[ \t]/.test(line2)) break;
|
|
294
|
+
if (line2.trim() === "") continue;
|
|
295
|
+
if (/^\s+/.test(line2)) body.push(line2.trimStart());
|
|
296
|
+
else break;
|
|
297
|
+
}
|
|
298
|
+
return body.join(" ").replace(/\s+/g, " ").trim();
|
|
299
|
+
}
|
|
300
|
+
function extractFrontmatterDescription(text) {
|
|
301
|
+
return extractFrontmatterValue(text, "description");
|
|
302
|
+
}
|
|
303
|
+
function auditForbiddenTagsInText(text, label) {
|
|
304
|
+
const body = `${extractFrontmatterDescription(text)}
|
|
305
|
+
${extractUseWhenSection(text)}`;
|
|
306
|
+
if (!body.trim()) return [];
|
|
307
|
+
const failures = [];
|
|
308
|
+
for (const word of PROSE_FORBIDDEN_TAGS) {
|
|
309
|
+
const re = new RegExp(`\\b${escapeRegExp(word)}\\b`, "i");
|
|
310
|
+
if (re.test(body)) failures.push(`${label}: forbidden stack/tag "${word}" in content`);
|
|
311
|
+
}
|
|
312
|
+
return failures;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/catalog/ingest-catalog.ts
|
|
316
|
+
function validateCatalogItem(item, content2) {
|
|
317
|
+
const label = item.id;
|
|
318
|
+
const lines = content2.split(/\r?\n/);
|
|
319
|
+
for (let i = 0; i < lines.length; i++) {
|
|
320
|
+
const line2 = lines[i] ?? "";
|
|
321
|
+
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
|
|
322
|
+
return { ok: false, reason: `${label}: risky install pattern at line ${i + 1}` };
|
|
323
|
+
}
|
|
324
|
+
if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
325
|
+
return { ok: false, reason: `${label}: disallowed npx at line ${i + 1}` };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const tagFailures = auditForbiddenTagsInText(content2, label);
|
|
329
|
+
if (tagFailures.length > 0) {
|
|
330
|
+
return { ok: false, reason: tagFailures[0] };
|
|
331
|
+
}
|
|
332
|
+
return { ok: true };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/catalog/manifest-schema.ts
|
|
336
|
+
var POLLUTION_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
337
|
+
function isNonEmptyString(value) {
|
|
338
|
+
return typeof value === "string" && value.length > 0;
|
|
339
|
+
}
|
|
340
|
+
function safeParse(json) {
|
|
341
|
+
return JSON.parse(json, (key, value) => {
|
|
342
|
+
if (POLLUTION_KEYS.has(key)) return void 0;
|
|
343
|
+
return value;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function parseManifest(json) {
|
|
347
|
+
let data;
|
|
348
|
+
try {
|
|
349
|
+
data = safeParse(json);
|
|
350
|
+
} catch {
|
|
351
|
+
return { ok: false, error: "invalid JSON" };
|
|
352
|
+
}
|
|
353
|
+
if (!data || typeof data !== "object") {
|
|
354
|
+
return { ok: false, error: "manifest must be an object" };
|
|
355
|
+
}
|
|
356
|
+
const root = data;
|
|
357
|
+
if (!isNonEmptyString(root.version)) {
|
|
358
|
+
return { ok: false, error: "missing version" };
|
|
359
|
+
}
|
|
360
|
+
if (!Array.isArray(root.items)) {
|
|
361
|
+
return { ok: false, error: "missing items array" };
|
|
362
|
+
}
|
|
363
|
+
const items = [];
|
|
364
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
365
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
366
|
+
for (let i = 0; i < root.items.length; i++) {
|
|
367
|
+
const raw = root.items[i];
|
|
368
|
+
if (!raw || typeof raw !== "object") {
|
|
369
|
+
return { ok: false, error: `item[${i}]: invalid entry` };
|
|
370
|
+
}
|
|
371
|
+
const item = raw;
|
|
372
|
+
if (!isNonEmptyString(item.id)) {
|
|
373
|
+
return { ok: false, error: `item[${i}]: missing id` };
|
|
374
|
+
}
|
|
375
|
+
if (!isNonEmptyString(item.type)) {
|
|
376
|
+
return { ok: false, error: `${item.id}: missing type` };
|
|
377
|
+
}
|
|
378
|
+
if (!isNonEmptyString(item.path)) {
|
|
379
|
+
return { ok: false, error: `${item.id}: missing path` };
|
|
380
|
+
}
|
|
381
|
+
if (seenIds.has(item.id)) {
|
|
382
|
+
return { ok: false, error: `${item.id}: duplicate id` };
|
|
383
|
+
}
|
|
384
|
+
seenIds.add(item.id);
|
|
385
|
+
const normPath = item.path.replace(/\\/g, "/");
|
|
386
|
+
if (seenPaths.has(normPath)) {
|
|
387
|
+
return { ok: false, error: `${item.id}: duplicate path "${normPath}"` };
|
|
388
|
+
}
|
|
389
|
+
seenPaths.add(normPath);
|
|
390
|
+
if (item.source === "curated") {
|
|
391
|
+
if (!isNonEmptyString(item.reviewStatus)) {
|
|
392
|
+
return { ok: false, error: `${item.id}: curated item missing reviewStatus` };
|
|
393
|
+
}
|
|
394
|
+
if (!isNonEmptyString(item.riskLevel)) {
|
|
395
|
+
return { ok: false, error: `${item.id}: curated item missing riskLevel` };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
items.push(raw);
|
|
399
|
+
}
|
|
400
|
+
return { ok: true, manifest: { version: root.version, items } };
|
|
401
|
+
}
|
|
402
|
+
|
|
34
403
|
// src/catalog/remote-catalog.ts
|
|
35
404
|
function getCacheDir() {
|
|
36
|
-
return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ??
|
|
405
|
+
return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ?? path2.join(os.homedir(), CATALOG_CACHE_SUBDIR);
|
|
406
|
+
}
|
|
407
|
+
var cachedCatalogRef;
|
|
408
|
+
function getResolvedCatalogRef() {
|
|
409
|
+
return cachedCatalogRef ?? process.env["HAUS_CATALOG_REF"] ?? "main";
|
|
410
|
+
}
|
|
411
|
+
function isCatalogRefResolved() {
|
|
412
|
+
return cachedCatalogRef !== void 0 || process.env["HAUS_CATALOG_REF"] !== void 0;
|
|
413
|
+
}
|
|
414
|
+
async function resolveCatalogRef(opts) {
|
|
415
|
+
const env = opts?.env ?? process.env;
|
|
416
|
+
if (env["HAUS_CATALOG_REF"]) return env["HAUS_CATALOG_REF"];
|
|
417
|
+
const fetchLatest = opts?.fetchLatestTag ?? fetchLatestCatalogTag;
|
|
418
|
+
const tag = await fetchLatest();
|
|
419
|
+
return tag ?? "main";
|
|
420
|
+
}
|
|
421
|
+
async function remoteBase() {
|
|
422
|
+
if (process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
423
|
+
return process.env["HAUS_CATALOG_REMOTE_BASE"];
|
|
424
|
+
}
|
|
425
|
+
if (cachedCatalogRef === void 0) {
|
|
426
|
+
cachedCatalogRef = await resolveCatalogRef();
|
|
427
|
+
}
|
|
428
|
+
return `${CATALOG_REPO_URL}/${cachedCatalogRef}`;
|
|
37
429
|
}
|
|
38
|
-
var REMOTE_BASE = process.env["HAUS_CATALOG_REMOTE_BASE"] ?? `${CATALOG_REPO_URL}/${CATALOG_REF}`;
|
|
39
|
-
var REMOTE_MANIFEST_URL = `${REMOTE_BASE}/manifest.json`;
|
|
40
430
|
async function fetchText(url) {
|
|
41
431
|
try {
|
|
42
432
|
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
@@ -47,55 +437,59 @@ async function fetchText(url) {
|
|
|
47
437
|
}
|
|
48
438
|
}
|
|
49
439
|
async function fetchRemoteManifest() {
|
|
50
|
-
const
|
|
440
|
+
const base = await remoteBase();
|
|
441
|
+
const text = await fetchText(`${base}/manifest.json`);
|
|
51
442
|
if (!text) return null;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} catch {
|
|
443
|
+
const parsed = parseManifest(text);
|
|
444
|
+
if (!parsed.ok) {
|
|
445
|
+
warn(`Remote manifest failed schema validation: ${parsed.error}`);
|
|
56
446
|
return null;
|
|
57
447
|
}
|
|
448
|
+
if (!parsed.manifest.items.length) return null;
|
|
449
|
+
return parsed.manifest;
|
|
58
450
|
}
|
|
59
451
|
async function writeTextIfChanged(dest, text) {
|
|
60
|
-
if (await
|
|
61
|
-
const local = await
|
|
452
|
+
if (await fs2.pathExists(dest)) {
|
|
453
|
+
const local = await fs2.readFile(dest, "utf8");
|
|
62
454
|
if (local === text) return "unchanged";
|
|
63
|
-
await
|
|
455
|
+
await fs2.writeFile(dest, text, "utf8");
|
|
64
456
|
return "updated";
|
|
65
457
|
}
|
|
66
|
-
await
|
|
67
|
-
await
|
|
458
|
+
await fs2.ensureDir(path2.dirname(dest));
|
|
459
|
+
await fs2.writeFile(dest, text, "utf8");
|
|
68
460
|
return "created";
|
|
69
461
|
}
|
|
70
462
|
var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
|
|
71
463
|
async function readWorkflowTemplate(opts = {}) {
|
|
72
|
-
const dest =
|
|
73
|
-
const
|
|
464
|
+
const dest = path2.join(getCacheDir(), WORKFLOW_TEMPLATE_REL);
|
|
465
|
+
const base = await remoteBase();
|
|
466
|
+
const text = await fetchText(`${base}/${WORKFLOW_TEMPLATE_REL}`);
|
|
74
467
|
if (text === null) {
|
|
75
|
-
if (await
|
|
468
|
+
if (await fs2.pathExists(dest)) return fs2.readFile(dest, "utf8");
|
|
76
469
|
return null;
|
|
77
470
|
}
|
|
78
471
|
if (!opts.dryRun) {
|
|
79
472
|
await writeTextIfChanged(dest, text);
|
|
80
|
-
} else if (await
|
|
81
|
-
return
|
|
473
|
+
} else if (await fs2.pathExists(dest)) {
|
|
474
|
+
return fs2.readFile(dest, "utf8");
|
|
82
475
|
}
|
|
83
476
|
return text;
|
|
84
477
|
}
|
|
85
478
|
function isSafeCatalogPath(itemPath) {
|
|
86
|
-
if (!itemPath ||
|
|
87
|
-
const normalized =
|
|
479
|
+
if (!itemPath || path2.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
480
|
+
const normalized = path2.normalize(itemPath);
|
|
88
481
|
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
89
482
|
}
|
|
90
483
|
function safeJoin(base, itemPath) {
|
|
91
484
|
if (!isSafeCatalogPath(itemPath)) return null;
|
|
92
|
-
const resolved =
|
|
93
|
-
return resolved.startsWith(base +
|
|
485
|
+
const resolved = path2.resolve(base, itemPath);
|
|
486
|
+
return resolved.startsWith(base + path2.sep) || resolved === base ? resolved : null;
|
|
94
487
|
}
|
|
488
|
+
var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command"]);
|
|
95
489
|
function isExternalReference(ref) {
|
|
96
490
|
return /^[a-z][a-z0-9+.-]*:\/\//i.test(ref);
|
|
97
491
|
}
|
|
98
|
-
async function downloadSkillReferences(item, destDir) {
|
|
492
|
+
async function downloadSkillReferences(item, destDir, base) {
|
|
99
493
|
for (const ref of item.references ?? []) {
|
|
100
494
|
if (isExternalReference(ref)) continue;
|
|
101
495
|
const refDest = safeJoin(destDir, ref);
|
|
@@ -103,7 +497,7 @@ async function downloadSkillReferences(item, destDir) {
|
|
|
103
497
|
warn(`Skipping reference "${ref}" for ${item.id}: path traversal detected`);
|
|
104
498
|
continue;
|
|
105
499
|
}
|
|
106
|
-
const text = await fetchText(`${
|
|
500
|
+
const text = await fetchText(`${base}/${item.path}/${ref}`);
|
|
107
501
|
if (text === null) {
|
|
108
502
|
warn(`Failed to fetch reference "${ref}" for ${item.id}`);
|
|
109
503
|
continue;
|
|
@@ -111,18 +505,79 @@ async function downloadSkillReferences(item, destDir) {
|
|
|
111
505
|
await writeTextIfChanged(refDest, text);
|
|
112
506
|
}
|
|
113
507
|
}
|
|
508
|
+
async function syncOneItem(item, base) {
|
|
509
|
+
if (!KNOWN_ITEM_TYPES.has(item.type)) {
|
|
510
|
+
warn(
|
|
511
|
+
`Skipping ${item.id}: type "${item.type}" is unknown to this haus version \u2014 upgrade to use it`
|
|
512
|
+
);
|
|
513
|
+
return "failed";
|
|
514
|
+
}
|
|
515
|
+
if (!item.path) return "failed";
|
|
516
|
+
if (!isSafeCatalogPath(item.path)) {
|
|
517
|
+
warn(`Skipping ${item.id}: invalid path "${item.path}"`);
|
|
518
|
+
return "failed";
|
|
519
|
+
}
|
|
520
|
+
if (item.type === "skill") {
|
|
521
|
+
const destDir = safeJoin(getCacheDir(), item.path);
|
|
522
|
+
if (!destDir) {
|
|
523
|
+
warn(`Skipping ${item.id}: path traversal detected`);
|
|
524
|
+
return "failed";
|
|
525
|
+
}
|
|
526
|
+
const dest2 = path2.join(destDir, "SKILL.md");
|
|
527
|
+
const text2 = await fetchText(`${base}/${item.path}/SKILL.md`);
|
|
528
|
+
if (!text2) {
|
|
529
|
+
warn(`Failed to fetch content for ${item.id}`);
|
|
530
|
+
return "failed";
|
|
531
|
+
}
|
|
532
|
+
const verdict2 = validateCatalogItem(item, text2);
|
|
533
|
+
if (!verdict2.ok) {
|
|
534
|
+
warn(`Rejected ${item.id} at ingest: ${verdict2.reason}`);
|
|
535
|
+
return "failed";
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
const outcome = await writeTextIfChanged(dest2, text2);
|
|
539
|
+
await downloadSkillReferences(item, destDir, base);
|
|
540
|
+
return outcome;
|
|
541
|
+
} catch (err) {
|
|
542
|
+
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
543
|
+
return "failed";
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const dest = safeJoin(getCacheDir(), item.path);
|
|
547
|
+
if (!dest) {
|
|
548
|
+
warn(`Skipping ${item.id}: path traversal detected`);
|
|
549
|
+
return "failed";
|
|
550
|
+
}
|
|
551
|
+
const text = await fetchText(`${base}/${item.path}`);
|
|
552
|
+
if (!text) {
|
|
553
|
+
warn(`Failed to fetch content for ${item.id}`);
|
|
554
|
+
return "failed";
|
|
555
|
+
}
|
|
556
|
+
const verdict = validateCatalogItem(item, text);
|
|
557
|
+
if (!verdict.ok) {
|
|
558
|
+
warn(`Rejected ${item.id} at ingest: ${verdict.reason}`);
|
|
559
|
+
return "failed";
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
return await writeTextIfChanged(dest, text);
|
|
563
|
+
} catch (err) {
|
|
564
|
+
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
565
|
+
return "failed";
|
|
566
|
+
}
|
|
567
|
+
}
|
|
114
568
|
async function syncRemoteCatalog() {
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
569
|
+
const manifest = await fetchRemoteManifest();
|
|
570
|
+
if (!manifest) {
|
|
117
571
|
warn("Remote catalog fetch failed \u2014 using bundled catalog");
|
|
118
572
|
return { newItems: [], refreshed: [], unchanged: 0, failed: [] };
|
|
119
573
|
}
|
|
574
|
+
const { version, items } = manifest;
|
|
120
575
|
const cacheDir = getCacheDir();
|
|
121
576
|
try {
|
|
122
|
-
await
|
|
123
|
-
await
|
|
124
|
-
|
|
125
|
-
`${JSON.stringify({ items }, null, 2)}
|
|
577
|
+
await fs2.ensureDir(cacheDir);
|
|
578
|
+
await fs2.writeFile(
|
|
579
|
+
path2.join(cacheDir, "manifest.json"),
|
|
580
|
+
`${JSON.stringify({ version, items }, null, 2)}
|
|
126
581
|
`,
|
|
127
582
|
"utf8"
|
|
128
583
|
);
|
|
@@ -136,63 +591,15 @@ async function syncRemoteCatalog() {
|
|
|
136
591
|
const refreshed = [];
|
|
137
592
|
let unchanged = 0;
|
|
138
593
|
const failed = [];
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
const destDir = safeJoin(getCacheDir(), item.path);
|
|
149
|
-
if (!destDir) {
|
|
150
|
-
warn(`Skipping ${item.id}: path traversal detected`);
|
|
151
|
-
failed.push(item.id);
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const dest = path.join(destDir, "SKILL.md");
|
|
155
|
-
const url = `${REMOTE_BASE}/${item.path}/SKILL.md`;
|
|
156
|
-
const text = await fetchText(url);
|
|
157
|
-
if (!text) {
|
|
158
|
-
warn(`Failed to fetch content for ${item.id}`);
|
|
159
|
-
failed.push(item.id);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
try {
|
|
163
|
-
const outcome = await writeTextIfChanged(dest, text);
|
|
164
|
-
await downloadSkillReferences(item, destDir);
|
|
165
|
-
if (outcome === "created") newItems.push(item.id);
|
|
166
|
-
else if (outcome === "updated") refreshed.push(item.id);
|
|
167
|
-
else unchanged++;
|
|
168
|
-
} catch (err) {
|
|
169
|
-
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
170
|
-
failed.push(item.id);
|
|
171
|
-
}
|
|
172
|
-
} else {
|
|
173
|
-
const dest = safeJoin(getCacheDir(), item.path);
|
|
174
|
-
if (!dest) {
|
|
175
|
-
warn(`Skipping ${item.id}: path traversal detected`);
|
|
176
|
-
failed.push(item.id);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
const url = `${REMOTE_BASE}/${item.path}`;
|
|
180
|
-
const text = await fetchText(url);
|
|
181
|
-
if (!text) {
|
|
182
|
-
warn(`Failed to fetch content for ${item.id}`);
|
|
183
|
-
failed.push(item.id);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
187
|
-
const outcome = await writeTextIfChanged(dest, text);
|
|
188
|
-
if (outcome === "created") newItems.push(item.id);
|
|
189
|
-
else if (outcome === "updated") refreshed.push(item.id);
|
|
190
|
-
else unchanged++;
|
|
191
|
-
} catch (err) {
|
|
192
|
-
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
193
|
-
failed.push(item.id);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
594
|
+
const base = await remoteBase();
|
|
595
|
+
const outcomes = await mapWithConcurrency(items, (item) => syncOneItem(item, base), 8);
|
|
596
|
+
for (let i = 0; i < items.length; i++) {
|
|
597
|
+
const item = items[i];
|
|
598
|
+
const outcome = outcomes[i];
|
|
599
|
+
if (outcome === "created") newItems.push(item.id);
|
|
600
|
+
else if (outcome === "updated") refreshed.push(item.id);
|
|
601
|
+
else if (outcome === "unchanged") unchanged++;
|
|
602
|
+
else if (outcome === "failed") failed.push(item.id);
|
|
196
603
|
}
|
|
197
604
|
return { newItems, refreshed, unchanged, failed };
|
|
198
605
|
}
|
|
@@ -213,7 +620,7 @@ async function fetchLatestCatalogTag() {
|
|
|
213
620
|
}
|
|
214
621
|
async function getCacheManifestAge() {
|
|
215
622
|
try {
|
|
216
|
-
const stat = await
|
|
623
|
+
const stat = await fs2.stat(path2.join(getCacheDir(), "manifest.json"));
|
|
217
624
|
return Date.now() - stat.mtimeMs;
|
|
218
625
|
} catch {
|
|
219
626
|
return null;
|
|
@@ -237,64 +644,6 @@ function buildAllowRules() {
|
|
|
237
644
|
import path4 from "path";
|
|
238
645
|
import fs3 from "fs-extra";
|
|
239
646
|
|
|
240
|
-
// src/utils/fs.ts
|
|
241
|
-
import crypto from "crypto";
|
|
242
|
-
import path2 from "path";
|
|
243
|
-
import fg from "fast-glob";
|
|
244
|
-
import fs2 from "fs-extra";
|
|
245
|
-
async function readJson(file) {
|
|
246
|
-
try {
|
|
247
|
-
return await fs2.readJson(file);
|
|
248
|
-
} catch {
|
|
249
|
-
return void 0;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
async function writeJson(file, value) {
|
|
253
|
-
await fs2.ensureDir(path2.dirname(file));
|
|
254
|
-
await fs2.writeFile(file, `${JSON.stringify(value, null, 2)}
|
|
255
|
-
`, "utf8");
|
|
256
|
-
}
|
|
257
|
-
async function pruneEmptyDir(dir) {
|
|
258
|
-
try {
|
|
259
|
-
const entries = await fs2.readdir(dir);
|
|
260
|
-
if (entries.length === 0) await fs2.remove(dir);
|
|
261
|
-
} catch {
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
async function readText(file) {
|
|
265
|
-
try {
|
|
266
|
-
return await fs2.readFile(file, "utf8");
|
|
267
|
-
} catch {
|
|
268
|
-
return void 0;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
async function writeText(file, value) {
|
|
272
|
-
await fs2.ensureDir(path2.dirname(file));
|
|
273
|
-
await fs2.writeFile(file, value, "utf8");
|
|
274
|
-
}
|
|
275
|
-
async function listFiles(root, patterns) {
|
|
276
|
-
const files = await fg(patterns, {
|
|
277
|
-
cwd: root,
|
|
278
|
-
dot: true,
|
|
279
|
-
onlyFiles: true,
|
|
280
|
-
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
|
|
281
|
-
});
|
|
282
|
-
return files.sort((a, b) => a.localeCompare(b));
|
|
283
|
-
}
|
|
284
|
-
function hashText(value) {
|
|
285
|
-
return `sha256-${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
286
|
-
}
|
|
287
|
-
async function mapWithConcurrency(items, fn, concurrency = 24) {
|
|
288
|
-
const size = Number.isFinite(concurrency) ? Math.max(1, Math.floor(concurrency)) : 24;
|
|
289
|
-
const results = new Array(items.length);
|
|
290
|
-
for (let i = 0; i < items.length; i += size) {
|
|
291
|
-
const batch = items.slice(i, i + size);
|
|
292
|
-
const settled = await Promise.all(batch.map((item, j) => fn(item, i + j)));
|
|
293
|
-
for (let j = 0; j < settled.length; j += 1) results[i + j] = settled[j];
|
|
294
|
-
}
|
|
295
|
-
return results;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
647
|
// src/install/manifest.ts
|
|
299
648
|
import os2 from "os";
|
|
300
649
|
import path3 from "path";
|
|
@@ -1196,6 +1545,13 @@ ${templateContent}`;
|
|
|
1196
1545
|
}
|
|
1197
1546
|
|
|
1198
1547
|
// src/claude/write-claude-files.ts
|
|
1548
|
+
function targetDirForType(type) {
|
|
1549
|
+
if (type === "agent") return "agents";
|
|
1550
|
+
if (type === "template") return "templates";
|
|
1551
|
+
if (type === "command") return "commands";
|
|
1552
|
+
if (type === "skill") return "skills";
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1199
1555
|
async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
1200
1556
|
const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
|
|
1201
1557
|
mode: "fast",
|
|
@@ -1272,14 +1628,18 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1272
1628
|
const installedPathsByItem = /* @__PURE__ */ new Map();
|
|
1273
1629
|
const installedIds = /* @__PURE__ */ new Set();
|
|
1274
1630
|
const catalogItems = selectedIds !== void 0 ? rec.recommended.filter((r) => selectedIds.includes(r.id)) : rec.recommended;
|
|
1631
|
+
let curatedReviewStatusSkips = 0;
|
|
1275
1632
|
for (const item of catalogItems) {
|
|
1276
1633
|
const manifestItem = manifestById.get(item.id);
|
|
1277
1634
|
if (!manifestItem?.path) continue;
|
|
1278
1635
|
if (manifestItem.source === "curated") {
|
|
1279
1636
|
if (manifestItem.reviewStatus !== "approved") {
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1637
|
+
curatedReviewStatusSkips++;
|
|
1638
|
+
if (curatedReviewStatusSkips === 1) {
|
|
1639
|
+
warn(
|
|
1640
|
+
`Skipping curated item ${item.id}: reviewStatus is not approved (${manifestItem.reviewStatus ?? "unset"})`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1283
1643
|
continue;
|
|
1284
1644
|
}
|
|
1285
1645
|
if (manifestItem.riskLevel === "blocked") {
|
|
@@ -1287,8 +1647,14 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1287
1647
|
continue;
|
|
1288
1648
|
}
|
|
1289
1649
|
}
|
|
1290
|
-
const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
|
|
1291
|
-
const target = item.type
|
|
1650
|
+
const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
|
|
1651
|
+
const target = targetDirForType(item.type);
|
|
1652
|
+
if (!target) {
|
|
1653
|
+
warn(
|
|
1654
|
+
`Skipping ${item.id}: type "${item.type}" is unknown to this haus version \u2014 upgrade the CLI to use it`
|
|
1655
|
+
);
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1292
1658
|
const destination = claudePath(root, target, path12.basename(sourcePath));
|
|
1293
1659
|
if (await fs11.pathExists(sourcePath)) {
|
|
1294
1660
|
if (dryRun) {
|
|
@@ -1310,9 +1676,19 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1310
1676
|
);
|
|
1311
1677
|
}
|
|
1312
1678
|
}
|
|
1679
|
+
if (curatedReviewStatusSkips > 1) {
|
|
1680
|
+
warn(
|
|
1681
|
+
`${curatedReviewStatusSkips} curated items skipped: reviewStatus is not approved \u2014 possible catalog field rename upstream`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1313
1684
|
await cleanupStaleCatalogItems(root, new Set(manifestById.keys()), dryRun);
|
|
1314
1685
|
if (dryRun) return [...new Set(files)];
|
|
1315
1686
|
const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
|
|
1687
|
+
const prevLock = await readJson(hausPath(root, "haus.lock.json"));
|
|
1688
|
+
const prevRefById = new Map(
|
|
1689
|
+
(prevLock ?? []).filter((e) => e.id && e.catalogRef).map((e) => [e.id, e.catalogRef])
|
|
1690
|
+
);
|
|
1691
|
+
const lockCatalogRef = (itemId) => isCatalogRefResolved() ? getResolvedCatalogRef() : prevRefById.get(itemId) ?? getResolvedCatalogRef();
|
|
1316
1692
|
await writeManagedJson(
|
|
1317
1693
|
root,
|
|
1318
1694
|
hausPath(root, "selected-context.json"),
|
|
@@ -1334,7 +1710,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1334
1710
|
type: r.type,
|
|
1335
1711
|
source: isCurated ? "curated" : "haus",
|
|
1336
1712
|
version: hausVersion2,
|
|
1337
|
-
catalogRef:
|
|
1713
|
+
catalogRef: lockCatalogRef(r.id),
|
|
1338
1714
|
hash: await hashInstalledPaths(root, relPaths),
|
|
1339
1715
|
installMode: "copied",
|
|
1340
1716
|
paths: relPaths
|
|
@@ -1437,9 +1813,14 @@ async function runApply(options) {
|
|
|
1437
1813
|
const rec = await readJson(hausPath(root, "recommendation.json"));
|
|
1438
1814
|
const catalogItemCount = selectedIds !== void 0 ? selectedIds.length : rec?.recommended.length ?? 0;
|
|
1439
1815
|
if (catalogItemCount > 0 && !await cacheHasItems()) {
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1816
|
+
const message = "No catalog content found. Run `haus update` first.";
|
|
1817
|
+
if (isDryRun) {
|
|
1818
|
+
warn(`Catalog cache is empty \u2014 dry-run will skip catalog items. ${message}`);
|
|
1819
|
+
} else {
|
|
1820
|
+
error(message);
|
|
1821
|
+
process.exitCode = 1;
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1443
1824
|
}
|
|
1444
1825
|
}
|
|
1445
1826
|
const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
|
|
@@ -1466,185 +1847,6 @@ async function refreshProjectApply(root) {
|
|
|
1466
1847
|
return writeClaudeFiles(root, false, void 0, { refillConfig: false });
|
|
1467
1848
|
}
|
|
1468
1849
|
|
|
1469
|
-
// library/catalog/validation-rules.json
|
|
1470
|
-
var validation_rules_default = {
|
|
1471
|
-
forbiddenTags: [
|
|
1472
|
-
"python",
|
|
1473
|
-
"django",
|
|
1474
|
-
"go",
|
|
1475
|
-
"rust",
|
|
1476
|
-
"java",
|
|
1477
|
-
"spring",
|
|
1478
|
-
"kotlin",
|
|
1479
|
-
"swift",
|
|
1480
|
-
"android",
|
|
1481
|
-
"flutter",
|
|
1482
|
-
"dart",
|
|
1483
|
-
"c++",
|
|
1484
|
-
"perl",
|
|
1485
|
-
"defi",
|
|
1486
|
-
"trading"
|
|
1487
|
-
],
|
|
1488
|
-
bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
|
|
1489
|
-
requiredSkillFrontmatter: ["description"],
|
|
1490
|
-
requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
|
|
1491
|
-
riskyInstallPatterns: [
|
|
1492
|
-
{ source: "\\bnpx\\s+-y\\b", flags: "i" },
|
|
1493
|
-
{ source: "\\bnpx\\s+--yes\\b", flags: "i" },
|
|
1494
|
-
{ source: "\\byarn\\s+dlx\\b", flags: "i" },
|
|
1495
|
-
{ source: "\\bpnpm\\s+dlx\\b", flags: "i" }
|
|
1496
|
-
],
|
|
1497
|
-
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
1498
|
-
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
1499
|
-
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
1500
|
-
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
1501
|
-
allowedStacks: [
|
|
1502
|
-
"haus",
|
|
1503
|
-
"security",
|
|
1504
|
-
"quality",
|
|
1505
|
-
"frontend",
|
|
1506
|
-
"backend",
|
|
1507
|
-
"testing",
|
|
1508
|
-
"review",
|
|
1509
|
-
"workflow",
|
|
1510
|
-
"reference-pack",
|
|
1511
|
-
"core-skill",
|
|
1512
|
-
"workflow-skill",
|
|
1513
|
-
"stack-skill",
|
|
1514
|
-
"review-skill",
|
|
1515
|
-
"agent",
|
|
1516
|
-
"hook",
|
|
1517
|
-
"rule",
|
|
1518
|
-
"react",
|
|
1519
|
-
"typescript",
|
|
1520
|
-
"php",
|
|
1521
|
-
"csharp",
|
|
1522
|
-
"vendure",
|
|
1523
|
-
"vendure3",
|
|
1524
|
-
"nestjs",
|
|
1525
|
-
"graphql",
|
|
1526
|
-
"nx21",
|
|
1527
|
-
"turbo",
|
|
1528
|
-
"nextjs",
|
|
1529
|
-
"react19",
|
|
1530
|
-
"typescript5",
|
|
1531
|
-
"vite8",
|
|
1532
|
-
"tanstack-query",
|
|
1533
|
-
"tanstack-router",
|
|
1534
|
-
"radix",
|
|
1535
|
-
"radix-ui",
|
|
1536
|
-
"shadcn",
|
|
1537
|
-
"shadcn-ui",
|
|
1538
|
-
"tailwind",
|
|
1539
|
-
"tailwindcss",
|
|
1540
|
-
"scss",
|
|
1541
|
-
"scss-modules",
|
|
1542
|
-
"vue",
|
|
1543
|
-
"expressjs",
|
|
1544
|
-
"soup-base",
|
|
1545
|
-
"laravel",
|
|
1546
|
-
"laravel-nova",
|
|
1547
|
-
"wordpress",
|
|
1548
|
-
"bedrock",
|
|
1549
|
-
"elementor-pro",
|
|
1550
|
-
"acf-pro",
|
|
1551
|
-
"jetengine",
|
|
1552
|
-
"dotnet",
|
|
1553
|
-
"oidc",
|
|
1554
|
-
"azure-ad",
|
|
1555
|
-
"bankid",
|
|
1556
|
-
"myid",
|
|
1557
|
-
"cgi",
|
|
1558
|
-
"crypto",
|
|
1559
|
-
"collection2",
|
|
1560
|
-
"postgresql",
|
|
1561
|
-
"mariadb",
|
|
1562
|
-
"mssql",
|
|
1563
|
-
"elasticsearch",
|
|
1564
|
-
"yarn4",
|
|
1565
|
-
"pnpm89",
|
|
1566
|
-
"playwright",
|
|
1567
|
-
"testing-library",
|
|
1568
|
-
"phpunit",
|
|
1569
|
-
"storybook",
|
|
1570
|
-
"wisest",
|
|
1571
|
-
"vitest",
|
|
1572
|
-
"jest",
|
|
1573
|
-
"redis",
|
|
1574
|
-
"sanity",
|
|
1575
|
-
"strapi",
|
|
1576
|
-
"prisma",
|
|
1577
|
-
"cms",
|
|
1578
|
-
"database",
|
|
1579
|
-
"mysql",
|
|
1580
|
-
"saml2",
|
|
1581
|
-
"next-auth",
|
|
1582
|
-
"auth",
|
|
1583
|
-
"expo",
|
|
1584
|
-
"react-native",
|
|
1585
|
-
"mobile",
|
|
1586
|
-
"i18next",
|
|
1587
|
-
"i18n",
|
|
1588
|
-
"bullmq",
|
|
1589
|
-
"queue",
|
|
1590
|
-
"sentry",
|
|
1591
|
-
"observability",
|
|
1592
|
-
"tooling",
|
|
1593
|
-
"prettier",
|
|
1594
|
-
"eslint",
|
|
1595
|
-
"missing-prettier",
|
|
1596
|
-
"missing-eslint",
|
|
1597
|
-
"docker",
|
|
1598
|
-
"pm2",
|
|
1599
|
-
"deployer-php",
|
|
1600
|
-
"stripe",
|
|
1601
|
-
"qliro",
|
|
1602
|
-
"supabase",
|
|
1603
|
-
"payments"
|
|
1604
|
-
],
|
|
1605
|
-
alwaysAllowedTags: [
|
|
1606
|
-
"haus",
|
|
1607
|
-
"security",
|
|
1608
|
-
"quality",
|
|
1609
|
-
"review",
|
|
1610
|
-
"workflow",
|
|
1611
|
-
"baseline",
|
|
1612
|
-
"project-instructions"
|
|
1613
|
-
],
|
|
1614
|
-
patternTagSuffixes: ["-patterns"]
|
|
1615
|
-
};
|
|
1616
|
-
|
|
1617
|
-
// src/catalog/validation-rules.ts
|
|
1618
|
-
var toRegExp = (r) => new RegExp(r.source, r.flags);
|
|
1619
|
-
var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
|
|
1620
|
-
var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
|
|
1621
|
-
var REQUIRED_SKILL_FRONTMATTER = validation_rules_default.requiredSkillFrontmatter;
|
|
1622
|
-
var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
|
|
1623
|
-
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
1624
|
-
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
1625
|
-
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
1626
|
-
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
1627
|
-
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
1628
|
-
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
1629
|
-
var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
|
|
1630
|
-
var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
|
|
1631
|
-
var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
|
|
1632
|
-
function isTagAllowed(tag) {
|
|
1633
|
-
const lower = tag.toLowerCase();
|
|
1634
|
-
if (ALLOWED_SET.has(lower)) return true;
|
|
1635
|
-
return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
1636
|
-
}
|
|
1637
|
-
function auditDisallowedTags(items) {
|
|
1638
|
-
const failures = [];
|
|
1639
|
-
for (const item of items) {
|
|
1640
|
-
if (!item.id) continue;
|
|
1641
|
-
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
1642
|
-
if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
return failures;
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
1850
|
// src/commands/catalog-audit.ts
|
|
1649
1851
|
function auditForbiddenTags(items) {
|
|
1650
1852
|
const failures = [];
|
|
@@ -1758,6 +1960,17 @@ async function runConfig(key, action) {
|
|
|
1758
1960
|
log(`${key} ${action}d`);
|
|
1759
1961
|
}
|
|
1760
1962
|
|
|
1963
|
+
// src/recommender/token-estimate.ts
|
|
1964
|
+
var TOKENS_PER_ITEM = 320;
|
|
1965
|
+
function estimateContextTokens(selectedCount) {
|
|
1966
|
+
return selectedCount * TOKENS_PER_ITEM;
|
|
1967
|
+
}
|
|
1968
|
+
function tokenReductionPct(selected, skipped) {
|
|
1969
|
+
const total = selected + skipped;
|
|
1970
|
+
if (total === 0) return 0;
|
|
1971
|
+
return Math.max(0, Math.round(skipped / total * 100));
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1761
1974
|
// src/recommender/explain-recommendation.ts
|
|
1762
1975
|
function normalizeRecommendation(input2) {
|
|
1763
1976
|
const recommended = (input2.recommended ?? []).map((item) => {
|
|
@@ -1792,13 +2005,10 @@ function normalizeRecommendation(input2) {
|
|
|
1792
2005
|
recommended,
|
|
1793
2006
|
skipped,
|
|
1794
2007
|
warnings: input2.warnings ?? [],
|
|
1795
|
-
estimatedContextTokens: input2.estimatedContextTokens ?? recommended.length
|
|
2008
|
+
estimatedContextTokens: input2.estimatedContextTokens ?? estimateContextTokens(recommended.length),
|
|
1796
2009
|
selectedRules: input2.selectedRules ?? recommended.length,
|
|
1797
2010
|
skippedRules: input2.skippedRules ?? skipped.length,
|
|
1798
|
-
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ??
|
|
1799
|
-
0,
|
|
1800
|
-
Math.round(skipped.length / Math.max(recommended.length + skipped.length, 1) * 100)
|
|
1801
|
-
)
|
|
2011
|
+
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? tokenReductionPct(recommended.length, skipped.length)
|
|
1802
2012
|
};
|
|
1803
2013
|
}
|
|
1804
2014
|
function buildRecommendationExplanation(recommendation) {
|
|
@@ -2171,10 +2381,26 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
2171
2381
|
var dep = (value) => ({ kind: "dep", value });
|
|
2172
2382
|
var depPrefix = (value) => ({ kind: "depPrefix", value });
|
|
2173
2383
|
var depAbsent = (value) => ({ kind: "depAbsent", value });
|
|
2174
|
-
var fileEndsWith = (value) => ({
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2384
|
+
var fileEndsWith = (value) => ({
|
|
2385
|
+
kind: "file",
|
|
2386
|
+
value,
|
|
2387
|
+
mode: "endsWith"
|
|
2388
|
+
});
|
|
2389
|
+
var fileIncludes = (value) => ({
|
|
2390
|
+
kind: "file",
|
|
2391
|
+
value,
|
|
2392
|
+
mode: "includes"
|
|
2393
|
+
});
|
|
2394
|
+
var fileEquals = (value) => ({
|
|
2395
|
+
kind: "file",
|
|
2396
|
+
value,
|
|
2397
|
+
mode: "equals"
|
|
2398
|
+
});
|
|
2399
|
+
var fileStartsWith = (value) => ({
|
|
2400
|
+
kind: "file",
|
|
2401
|
+
value,
|
|
2402
|
+
mode: "startsWith"
|
|
2403
|
+
});
|
|
2178
2404
|
var content = (value) => ({ kind: "content", value });
|
|
2179
2405
|
var STACK_BUCKETS = [
|
|
2180
2406
|
"backend",
|
|
@@ -2241,7 +2467,10 @@ var STACK_RULES = [
|
|
|
2241
2467
|
{ stack: ["frontend", "vue"], any: [dep("vue")] },
|
|
2242
2468
|
{ stack: ["frontend", "vite8"], any: [dep("vite")] },
|
|
2243
2469
|
{ stack: ["frontend", "react-router-v7"], all: [dep("react-router"), dep("@react-router/node")] },
|
|
2244
|
-
{
|
|
2470
|
+
{
|
|
2471
|
+
stack: ["frontend", "tailwindcss"],
|
|
2472
|
+
any: [dep("tailwindcss"), fileIncludes("tailwind.config.")]
|
|
2473
|
+
},
|
|
2245
2474
|
{
|
|
2246
2475
|
stack: ["frontend", "shadcn"],
|
|
2247
2476
|
all: [fileEndsWith("components.json"), dep("class-variance-authority")]
|
|
@@ -2254,10 +2483,12 @@ var STACK_RULES = [
|
|
|
2254
2483
|
{ stack: ["frontend", "react-native"], any: [dep("react-native")] },
|
|
2255
2484
|
{ stack: ["tooling", "i18next"], any: [dep("i18next"), dep("react-i18next")] },
|
|
2256
2485
|
{ stack: ["tooling", "bullmq"], any: [dep("bullmq")] },
|
|
2257
|
-
{
|
|
2486
|
+
{
|
|
2487
|
+
stack: ["tooling", "docker"],
|
|
2488
|
+
any: [fileEquals("Dockerfile"), fileStartsWith("docker-compose")]
|
|
2489
|
+
},
|
|
2258
2490
|
{ stack: ["tooling", "pm2"], any: [dep("pm2"), fileIncludes("ecosystem.config")] },
|
|
2259
2491
|
{ stack: ["tooling", "sentry"], any: [depPrefix("@sentry/")] },
|
|
2260
|
-
{ stack: ["tooling", "deployer-php"], any: [dep("deployer/deployer")] },
|
|
2261
2492
|
{ stack: ["tooling", "missing-prettier"], any: [depAbsent("prettier")] },
|
|
2262
2493
|
{ stack: ["tooling", "missing-eslint"], any: [depAbsent("eslint")] },
|
|
2263
2494
|
{
|
|
@@ -2274,7 +2505,10 @@ var STACK_RULES = [
|
|
|
2274
2505
|
{ stack: ["backend", "nestjs"], any: [content("NestFactory")] },
|
|
2275
2506
|
{ stack: ["backend", "vendure3"], any: [content("@VendurePlugin")] },
|
|
2276
2507
|
{ stack: ["backend", "graphql"], any: [dep("graphql"), dep("@nestjs/graphql")] },
|
|
2277
|
-
{
|
|
2508
|
+
{
|
|
2509
|
+
stack: ["backend", "graphql"],
|
|
2510
|
+
any: [fileEndsWith(".graphql"), fileEndsWith("schema.graphql")]
|
|
2511
|
+
},
|
|
2278
2512
|
{ stack: ["backend", "laravel"], any: [dep("laravel/framework")] },
|
|
2279
2513
|
{ stack: ["backend", "laravel"], any: [fileIncludes("app/Providers/"), fileIncludes("routes/")] },
|
|
2280
2514
|
{ stack: ["backend", "wordpress"], any: [fileEndsWith("wp-config.php"), dep("roots/wordpress")] },
|
|
@@ -3236,7 +3470,9 @@ async function recommend(root, context) {
|
|
|
3236
3470
|
(t) => goals.includes(t) || goals.includes(t.replace(/-/g, " "))
|
|
3237
3471
|
);
|
|
3238
3472
|
if (goalMatch) push("goal-match", "guided goal match", `goal:${goalMatch}`);
|
|
3239
|
-
|
|
3473
|
+
const pm = context.packageManager;
|
|
3474
|
+
const pmVersionedMatch = pm === "yarn" || pm === "pnpm" ? item.tags.includes(pm) || item.tags.includes(`${pm}4`) || item.tags.includes(`${pm}89`) : item.tags.includes(pm);
|
|
3475
|
+
if (pmVersionedMatch) {
|
|
3240
3476
|
push(
|
|
3241
3477
|
"package-manager-match",
|
|
3242
3478
|
"package manager match",
|
|
@@ -3287,13 +3523,10 @@ async function recommend(root, context) {
|
|
|
3287
3523
|
}
|
|
3288
3524
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
3289
3525
|
skipped.sort((a, b) => a.id.localeCompare(b.id));
|
|
3290
|
-
const estimatedContextTokens = recommended.length * 320;
|
|
3291
3526
|
const selectedRules = recommended.length;
|
|
3292
3527
|
const skippedRules = skipped.length;
|
|
3293
|
-
const
|
|
3294
|
-
|
|
3295
|
-
Math.round(skippedRules / Math.max(selectedRules + skippedRules, 1) * 100)
|
|
3296
|
-
);
|
|
3528
|
+
const estimatedContextTokens = estimateContextTokens(selectedRules);
|
|
3529
|
+
const estimatedTokenReductionPct = tokenReductionPct(selectedRules, skippedRules);
|
|
3297
3530
|
return {
|
|
3298
3531
|
mode: context.mode,
|
|
3299
3532
|
recommended,
|
|
@@ -4167,61 +4400,6 @@ async function detectGlobalInstallDrift() {
|
|
|
4167
4400
|
// src/commands/validate-catalog.ts
|
|
4168
4401
|
import fs19 from "fs";
|
|
4169
4402
|
import path27 from "path";
|
|
4170
|
-
|
|
4171
|
-
// src/catalog/forbidden-content.ts
|
|
4172
|
-
var PROSE_FORBIDDEN_TAGS = FORBIDDEN_TAGS.filter((t) => t.toLowerCase() !== "go");
|
|
4173
|
-
function escapeRegExp(value) {
|
|
4174
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4175
|
-
}
|
|
4176
|
-
function extractUseWhenSection(text) {
|
|
4177
|
-
const marker = "## Use when";
|
|
4178
|
-
const idx = text.toLowerCase().indexOf(marker.toLowerCase());
|
|
4179
|
-
if (idx < 0) return "";
|
|
4180
|
-
const tail = text.slice(idx + marker.length);
|
|
4181
|
-
const next = tail.search(/\n##\s+/);
|
|
4182
|
-
return next < 0 ? tail : tail.slice(0, next);
|
|
4183
|
-
}
|
|
4184
|
-
function isYamlBlockScalarHeader(rest) {
|
|
4185
|
-
return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
|
|
4186
|
-
}
|
|
4187
|
-
function extractFrontmatterValue(text, key) {
|
|
4188
|
-
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
4189
|
-
if (!m) return "";
|
|
4190
|
-
const lines = m[1].split(/\r?\n/);
|
|
4191
|
-
const keyRe = new RegExp(`^${escapeRegExp(key)}:[ \\t]*`);
|
|
4192
|
-
const idx = lines.findIndex((l) => keyRe.test(l));
|
|
4193
|
-
if (idx < 0) return "";
|
|
4194
|
-
const rest = lines[idx].replace(keyRe, "").trim();
|
|
4195
|
-
if (!rest) return "";
|
|
4196
|
-
if (!isYamlBlockScalarHeader(rest)) {
|
|
4197
|
-
return rest.replace(/^["']|["']$/g, "").trim();
|
|
4198
|
-
}
|
|
4199
|
-
const body = [];
|
|
4200
|
-
for (let j = idx + 1; j < lines.length; j++) {
|
|
4201
|
-
const line2 = lines[j];
|
|
4202
|
-
if (/^[a-zA-Z_][\w.-]*:[ \t]/.test(line2)) break;
|
|
4203
|
-
if (line2.trim() === "") continue;
|
|
4204
|
-
if (/^\s+/.test(line2)) body.push(line2.trimStart());
|
|
4205
|
-
else break;
|
|
4206
|
-
}
|
|
4207
|
-
return body.join(" ").replace(/\s+/g, " ").trim();
|
|
4208
|
-
}
|
|
4209
|
-
function extractFrontmatterDescription(text) {
|
|
4210
|
-
return extractFrontmatterValue(text, "description");
|
|
4211
|
-
}
|
|
4212
|
-
function auditForbiddenTagsInText(text, label) {
|
|
4213
|
-
const body = `${extractFrontmatterDescription(text)}
|
|
4214
|
-
${extractUseWhenSection(text)}`;
|
|
4215
|
-
if (!body.trim()) return [];
|
|
4216
|
-
const failures = [];
|
|
4217
|
-
for (const word of PROSE_FORBIDDEN_TAGS) {
|
|
4218
|
-
const re = new RegExp(`\\b${escapeRegExp(word)}\\b`, "i");
|
|
4219
|
-
if (re.test(body)) failures.push(`${label}: forbidden stack/tag "${word}" in content`);
|
|
4220
|
-
}
|
|
4221
|
-
return failures;
|
|
4222
|
-
}
|
|
4223
|
-
|
|
4224
|
-
// src/commands/validate-catalog.ts
|
|
4225
4403
|
function auditForbiddenStacks(items) {
|
|
4226
4404
|
const failures = [];
|
|
4227
4405
|
for (const item of items) {
|
|
@@ -4316,18 +4494,9 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4316
4494
|
continue;
|
|
4317
4495
|
}
|
|
4318
4496
|
const text = fs19.readFileSync(absPath, "utf8");
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
}
|
|
4323
|
-
const lower = text.toLowerCase();
|
|
4324
|
-
for (const phrase of BANNED_AGENT_PHRASES) {
|
|
4325
|
-
if (lower.includes(phrase))
|
|
4326
|
-
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
4327
|
-
}
|
|
4328
|
-
failures.push(
|
|
4329
|
-
...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, absPath)}`)
|
|
4330
|
-
);
|
|
4497
|
+
const rel = path27.relative(manifestDir, absPath);
|
|
4498
|
+
failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
|
|
4499
|
+
failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
|
|
4331
4500
|
} else if (item.type === "template") {
|
|
4332
4501
|
if (!fs19.existsSync(absPath)) {
|
|
4333
4502
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
@@ -16,9 +16,7 @@
|
|
|
16
16
|
"defi",
|
|
17
17
|
"trading"
|
|
18
18
|
],
|
|
19
|
-
"bannedAgentPhrases": ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
|
|
20
19
|
"requiredSkillFrontmatter": ["description"],
|
|
21
|
-
"requiredAgentSections": ["## Use when", "## Do not use when", "## Verification"],
|
|
22
20
|
"riskyInstallPatterns": [
|
|
23
21
|
{ "source": "\\bnpx\\s+-y\\b", "flags": "i" },
|
|
24
22
|
{ "source": "\\bnpx\\s+--yes\\b", "flags": "i" },
|
|
@@ -84,10 +82,7 @@
|
|
|
84
82
|
"oidc",
|
|
85
83
|
"azure-ad",
|
|
86
84
|
"bankid",
|
|
87
|
-
"myid",
|
|
88
|
-
"cgi",
|
|
89
85
|
"crypto",
|
|
90
|
-
"collection2",
|
|
91
86
|
"postgresql",
|
|
92
87
|
"mariadb",
|
|
93
88
|
"mssql",
|
|
@@ -98,7 +93,6 @@
|
|
|
98
93
|
"testing-library",
|
|
99
94
|
"phpunit",
|
|
100
95
|
"storybook",
|
|
101
|
-
"wisest",
|
|
102
96
|
"vitest",
|
|
103
97
|
"jest",
|
|
104
98
|
"redis",
|
|
@@ -127,7 +121,6 @@
|
|
|
127
121
|
"missing-eslint",
|
|
128
122
|
"docker",
|
|
129
123
|
"pm2",
|
|
130
|
-
"deployer-php",
|
|
131
124
|
"stripe",
|
|
132
125
|
"qliro",
|
|
133
126
|
"supabase",
|
|
@@ -26,7 +26,9 @@ For each repo directory, run its setup **in that directory**, detecting what's n
|
|
|
26
26
|
bash -lc 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; cd "<repo>" && nvm install && corepack enable && yarn install'
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
**Read the repo's own setup docs first — they win.** Before applying the file-heuristics below, look for the repo's canonical setup instructions: `docs/setup.md`, `CLAUDE.md`, `README.md` (or follow `docs/SUMMARY.md` to the setup page). If present, **follow them as authoritative** — they capture nested or non-standard builds a root file-scan can't see. Use the heuristics below only to fill gaps, or when the repo ships no setup doc. Example: a WordPress/Bedrock repo has **no root `package.json`** — its JS/theme build lives under `web/app/themes/<theme>` and is only described in the docs, so a root-only scan wrongly reports "no JS". When the docs point at a nested build, scan subdirectories for the relevant `package.json` / build script and run it.
|
|
30
|
+
|
|
31
|
+
Adjust per repo (gap-fill, or when no setup doc exists):
|
|
30
32
|
|
|
31
33
|
1. **Node version.** If `.nvmrc` (or `engines.node` in `package.json`) is present, select it with `nvm install` (reads `.nvmrc`, installs the version if missing, then switches to it). If the user uses `fnm` instead, `fnm use --install-if-missing`. If neither is available, tell the user the required version and continue on the current node.
|
|
32
34
|
2. **JS dependencies.** Enable the pinned package manager with `corepack enable`, then install based on what's present:
|