hatch3r 1.3.0 → 1.4.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/README.md +2 -1
- package/agents/hatch3r-a11y-auditor.md +7 -11
- package/agents/hatch3r-architect.md +7 -11
- package/agents/hatch3r-ci-watcher.md +7 -10
- package/agents/hatch3r-context-rules.md +5 -7
- package/agents/hatch3r-dependency-auditor.md +7 -13
- package/agents/hatch3r-devops.md +7 -13
- package/agents/hatch3r-docs-writer.md +7 -11
- package/agents/hatch3r-fixer.md +2 -8
- package/agents/hatch3r-implementer.md +2 -8
- package/agents/hatch3r-learnings-loader.md +5 -7
- package/agents/hatch3r-lint-fixer.md +7 -9
- package/agents/hatch3r-perf-profiler.md +7 -11
- package/agents/hatch3r-researcher.md +6 -8
- package/agents/hatch3r-reviewer.md +7 -10
- package/agents/hatch3r-security-auditor.md +7 -12
- package/agents/hatch3r-test-writer.md +7 -11
- package/agents/shared/external-knowledge.md +21 -0
- package/agents/shared/quality-charter.md +78 -0
- package/commands/board/pickup-azure-devops.md +4 -0
- package/commands/board/pickup-delegation-multi.md +3 -0
- package/commands/board/pickup-delegation.md +3 -0
- package/commands/board/pickup-github.md +4 -0
- package/commands/board/pickup-gitlab.md +4 -0
- package/commands/board/pickup-post-impl.md +8 -1
- package/commands/board/shared-azure-devops.md +13 -3
- package/commands/board/shared-github.md +1 -0
- package/commands/board/shared-gitlab.md +9 -2
- package/commands/hatch3r-agent-customize.md +5 -1
- package/commands/hatch3r-board-groom.md +55 -2
- package/commands/hatch3r-board-init.md +5 -2
- package/commands/hatch3r-board-shared.md +37 -2
- package/commands/hatch3r-command-customize.md +4 -0
- package/commands/hatch3r-hooks.md +1 -1
- package/commands/hatch3r-quick-change.md +29 -3
- package/commands/hatch3r-revision.md +136 -16
- package/commands/hatch3r-rule-customize.md +4 -0
- package/commands/hatch3r-skill-customize.md +4 -0
- package/commands/hatch3r-workflow.md +10 -1
- package/dist/cli/index.js +522 -360
- package/dist/cli/index.js.map +1 -1
- package/package.json +12 -9
- package/rules/hatch3r-agent-orchestration-detail.md +159 -0
- package/rules/hatch3r-agent-orchestration-detail.mdc +156 -0
- package/rules/hatch3r-agent-orchestration.md +91 -330
- package/rules/hatch3r-agent-orchestration.mdc +127 -149
- package/rules/hatch3r-code-standards.mdc +10 -2
- package/rules/hatch3r-component-conventions.mdc +0 -1
- package/rules/hatch3r-deep-context.mdc +30 -8
- package/rules/hatch3r-dependency-management.mdc +17 -5
- package/rules/hatch3r-i18n.mdc +0 -1
- package/rules/hatch3r-migrations.mdc +12 -1
- package/rules/hatch3r-observability.mdc +289 -0
- package/rules/hatch3r-security-patterns.mdc +11 -0
- package/rules/hatch3r-testing.mdc +1 -1
- package/rules/hatch3r-theming.mdc +0 -1
- package/rules/hatch3r-tooling-hierarchy.mdc +18 -4
- package/skills/hatch3r-agent-customize/SKILL.md +4 -72
- package/skills/hatch3r-command-customize/SKILL.md +4 -62
- package/skills/hatch3r-customize/SKILL.md +117 -0
- package/skills/hatch3r-rule-customize/SKILL.md +4 -65
- package/skills/hatch3r-skill-customize/SKILL.md +4 -62
package/dist/cli/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import ora from "ora";
|
|
|
12
12
|
import boxen from "boxen";
|
|
13
13
|
|
|
14
14
|
// src/version.ts
|
|
15
|
-
var HATCH3R_VERSION = "1.
|
|
15
|
+
var HATCH3R_VERSION = "1.4.0";
|
|
16
16
|
|
|
17
17
|
// src/cli/shared/ui.ts
|
|
18
18
|
var CYAN = chalk.hex("#06b6d4");
|
|
@@ -140,9 +140,10 @@ var MANAGED_BLOCK_END = "<!-- HATCH3R:END -->";
|
|
|
140
140
|
var HATCH3R_PREFIX = "hatch3r-";
|
|
141
141
|
var AGENTS_DIR = ".agents";
|
|
142
142
|
var HatchError = class extends Error {
|
|
143
|
-
constructor(message, exitCode = 1) {
|
|
143
|
+
constructor(message, exitCode = 1, errorCode = "UNKNOWN_ERROR") {
|
|
144
144
|
super(message);
|
|
145
145
|
this.exitCode = exitCode;
|
|
146
|
+
this.errorCode = errorCode;
|
|
146
147
|
this.name = "HatchError";
|
|
147
148
|
}
|
|
148
149
|
};
|
|
@@ -538,7 +539,7 @@ async function worktreeSetupCommand(worktreePath, opts = {}) {
|
|
|
538
539
|
error("Worktree path is required when running from the main repo.");
|
|
539
540
|
console.log(chalk3.dim(" Usage: hatch3r worktree-setup <worktree-path>"));
|
|
540
541
|
console.log(chalk3.dim(" Or run this command from inside a worktree.\n"));
|
|
541
|
-
throw new HatchError("Missing worktree path", 1);
|
|
542
|
+
throw new HatchError("Missing worktree path", 1, "VALIDATION_ERROR");
|
|
542
543
|
}
|
|
543
544
|
targetRoot = join3(cwd, worktreePath);
|
|
544
545
|
}
|
|
@@ -550,7 +551,7 @@ async function worktreeSetupCommand(worktreePath, opts = {}) {
|
|
|
550
551
|
if (err.code === "ENOENT") {
|
|
551
552
|
error(`No ${WORKTREE_INCLUDE_FILE} found in ${mainRoot}`);
|
|
552
553
|
console.log(chalk3.dim(" Run `hatch3r init` or `hatch3r sync` to generate it.\n"));
|
|
553
|
-
throw new HatchError(`Missing ${WORKTREE_INCLUDE_FILE}`, 1);
|
|
554
|
+
throw new HatchError(`Missing ${WORKTREE_INCLUDE_FILE}`, 1, "FS_ERROR");
|
|
554
555
|
}
|
|
555
556
|
throw err;
|
|
556
557
|
}
|
|
@@ -631,7 +632,8 @@ import {
|
|
|
631
632
|
access,
|
|
632
633
|
rename,
|
|
633
634
|
unlink as unlink2,
|
|
634
|
-
open
|
|
635
|
+
open,
|
|
636
|
+
copyFile as copyFile2
|
|
635
637
|
} from "fs/promises";
|
|
636
638
|
import { dirname as dirname3, basename } from "path";
|
|
637
639
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -778,7 +780,16 @@ var DENY_PATTERNS = [
|
|
|
778
780
|
/override\s+(all\s+)?security/i,
|
|
779
781
|
/(?:atob|Buffer\.from)\s*\([^)]*(?:eval|exec|require)/i,
|
|
780
782
|
/(?:chmod|chown)\s+[0-7]{3,4}/i,
|
|
781
|
-
/(?:api[_-]?key|password|token|secret)\s*[:=]\s*.{8,}/i
|
|
783
|
+
/(?:api[_-]?key|password|token|secret)\s*[:=]\s*.{8,}/i,
|
|
784
|
+
// Prompt injection indicators
|
|
785
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
786
|
+
/disregard\s+(all\s+)?(previous|prior|above)/i,
|
|
787
|
+
/you\s+are\s+now\s+(?:a|an|the)\s/i,
|
|
788
|
+
/new\s+instructions\s*:/i,
|
|
789
|
+
/system\s+prompt\s*:/i,
|
|
790
|
+
/forget\s+(all\s+)?(previous|prior|above)\s+(instructions|rules|context)/i,
|
|
791
|
+
/act\s+as\s+(?:a|an)\s+(?:unrestricted|unfiltered|jailbroken)/i,
|
|
792
|
+
/do\s+not\s+follow\s+(?:any|the|your)\s+(?:previous|prior|above|original)\s/i
|
|
782
793
|
];
|
|
783
794
|
var ZERO_WIDTH_CHARS = /[\u200B\u200C\u200D\uFEFF\u00AD]/g;
|
|
784
795
|
var MAX_CUSTOMIZE_MD_BYTES = 10240;
|
|
@@ -1012,11 +1023,13 @@ async function safeWriteFile(filePath, content, options = {}) {
|
|
|
1012
1023
|
try {
|
|
1013
1024
|
merged = insertManagedBlock(existingContent, options.managedContent);
|
|
1014
1025
|
} catch {
|
|
1026
|
+
const bakPath = filePath + ".bak";
|
|
1027
|
+
await copyFile2(filePath, bakPath);
|
|
1015
1028
|
await atomicWriteFile(filePath, content);
|
|
1016
1029
|
return {
|
|
1017
1030
|
path: filePath,
|
|
1018
1031
|
action: "updated",
|
|
1019
|
-
warning: `Auto-repaired corrupted managed block in ${filePath}`
|
|
1032
|
+
warning: `Auto-repaired corrupted managed block in ${filePath} (backup saved to ${bakPath})`
|
|
1020
1033
|
};
|
|
1021
1034
|
}
|
|
1022
1035
|
await atomicWriteFile(filePath, merged);
|
|
@@ -1322,10 +1335,10 @@ async function ensureEnvMcp(rootDir, servers) {
|
|
|
1322
1335
|
}
|
|
1323
1336
|
|
|
1324
1337
|
// src/cli/commands/update.ts
|
|
1325
|
-
import { cp as
|
|
1338
|
+
import { cp as cp3, mkdir as mkdir5, readdir as readdir7, stat as stat2 } from "fs/promises";
|
|
1326
1339
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1327
1340
|
import { fileURLToPath } from "url";
|
|
1328
|
-
import { dirname as
|
|
1341
|
+
import { dirname as dirname8, join as join16 } from "path";
|
|
1329
1342
|
import chalk4 from "chalk";
|
|
1330
1343
|
import inquirer from "inquirer";
|
|
1331
1344
|
|
|
@@ -1425,7 +1438,7 @@ async function readGlobMd(baseDir, fileType) {
|
|
|
1425
1438
|
let entries;
|
|
1426
1439
|
try {
|
|
1427
1440
|
const all = await readdir(baseDir, { recursive: true });
|
|
1428
|
-
entries = all.filter((f) => f.endsWith(".md"));
|
|
1441
|
+
entries = all.filter((f) => f.endsWith(".md")).sort();
|
|
1429
1442
|
} catch (err) {
|
|
1430
1443
|
if (err.code !== "ENOENT") throw err;
|
|
1431
1444
|
return [];
|
|
@@ -1461,7 +1474,7 @@ async function readGlobMd(baseDir, fileType) {
|
|
|
1461
1474
|
async function readSkillSubdirs(baseDir) {
|
|
1462
1475
|
let dirents;
|
|
1463
1476
|
try {
|
|
1464
|
-
dirents = await readdir(baseDir, { withFileTypes: true });
|
|
1477
|
+
dirents = (await readdir(baseDir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
1465
1478
|
} catch (err) {
|
|
1466
1479
|
if (err.code !== "ENOENT") throw err;
|
|
1467
1480
|
return [];
|
|
@@ -2159,7 +2172,7 @@ var AmazonQAdapter = class extends BaseAdapter {
|
|
|
2159
2172
|
if (mcp && Object.keys(mcp).length > 0) {
|
|
2160
2173
|
const entries = this.buildStdMcpEntries(mcp);
|
|
2161
2174
|
if (Object.keys(entries).length > 0) {
|
|
2162
|
-
results.push(output(".amazonq/
|
|
2175
|
+
results.push(output(".amazonq/mcp.json", JSON.stringify({ mcpServers: entries }, null, 2)));
|
|
2163
2176
|
}
|
|
2164
2177
|
}
|
|
2165
2178
|
return results;
|
|
@@ -2178,9 +2191,9 @@ var AmpAdapter = class extends BaseAdapter {
|
|
|
2178
2191
|
text: `**Recommended model:** \`${m}\`. Use Smart mode for Opus, Rush for Haiku, Deep for Codex.`
|
|
2179
2192
|
}))
|
|
2180
2193
|
].join("\n");
|
|
2181
|
-
results.push(output("
|
|
2194
|
+
results.push(output("AGENTS.md", wrapInManagedBlock(inner), inner));
|
|
2182
2195
|
results.push(
|
|
2183
|
-
...await this.processSkillsRaw(ctx, (id) => `.
|
|
2196
|
+
...await this.processSkillsRaw(ctx, (id) => `.agents/skills/${toPrefixedId(id)}/SKILL.md`)
|
|
2184
2197
|
);
|
|
2185
2198
|
const mcp = await this.readFilteredMcp(ctx);
|
|
2186
2199
|
if (mcp && Object.keys(mcp).length > 0) {
|
|
@@ -2984,7 +2997,7 @@ var CursorAdapter = class extends BaseAdapter {
|
|
|
2984
2997
|
const lines = [`name: ${agent.id}`, `description: ${desc}`];
|
|
2985
2998
|
if (model) lines.push(`model: ${model}`);
|
|
2986
2999
|
if (agent.readonly) lines.push("readonly: true");
|
|
2987
|
-
if (agent.background) lines.push("
|
|
3000
|
+
if (agent.background) lines.push("is_background: true");
|
|
2988
3001
|
const fm = `---
|
|
2989
3002
|
${lines.join("\n")}
|
|
2990
3003
|
---`;
|
|
@@ -3427,7 +3440,7 @@ function isGlobPattern(scope) {
|
|
|
3427
3440
|
function ruleTrigger(scope) {
|
|
3428
3441
|
if (!scope) return "model_decision";
|
|
3429
3442
|
if (scope === "always") return "always_on";
|
|
3430
|
-
return "
|
|
3443
|
+
return "glob";
|
|
3431
3444
|
}
|
|
3432
3445
|
var WindsurfAdapter = class extends BaseAdapter {
|
|
3433
3446
|
name = "windsurf";
|
|
@@ -3464,7 +3477,7 @@ var WindsurfAdapter = class extends BaseAdapter {
|
|
|
3464
3477
|
if (skip) continue;
|
|
3465
3478
|
const scope = overrides.scope ?? rule.scope;
|
|
3466
3479
|
const trigger = ruleTrigger(scope);
|
|
3467
|
-
const globScope = trigger === "
|
|
3480
|
+
const globScope = trigger === "glob" && scope ? isGlobPattern(scope) ? scope : `${scope}/**` : void 0;
|
|
3468
3481
|
const fm = `---
|
|
3469
3482
|
trigger: ${trigger}${globScope ? `
|
|
3470
3483
|
globs: "${globScope}"` : ""}
|
|
@@ -3649,7 +3662,7 @@ function validateIntegrityManifest(data) {
|
|
|
3649
3662
|
for (const val of Object.values(obj.files)) {
|
|
3650
3663
|
if (typeof val !== "string") return false;
|
|
3651
3664
|
}
|
|
3652
|
-
if (
|
|
3665
|
+
if (typeof obj.checksum !== "string") return false;
|
|
3653
3666
|
return true;
|
|
3654
3667
|
}
|
|
3655
3668
|
async function readIntegrityManifest(agentsDir) {
|
|
@@ -3670,12 +3683,10 @@ async function verifyIntegrity(agentsDir) {
|
|
|
3670
3683
|
return [];
|
|
3671
3684
|
}
|
|
3672
3685
|
const results = [];
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
return results;
|
|
3678
|
-
}
|
|
3686
|
+
const expected = createHash("sha256").update(JSON.stringify(manifest.files)).digest("hex");
|
|
3687
|
+
if (manifest.checksum !== expected) {
|
|
3688
|
+
results.push({ file: INTEGRITY_FILE, status: "tampered" });
|
|
3689
|
+
return results;
|
|
3679
3690
|
}
|
|
3680
3691
|
const manifestFiles = new Set(Object.keys(manifest.files));
|
|
3681
3692
|
for (const [filePath, expectedHash] of Object.entries(manifest.files)) {
|
|
@@ -3723,17 +3734,209 @@ async function verifyIntegrity(agentsDir) {
|
|
|
3723
3734
|
return results;
|
|
3724
3735
|
}
|
|
3725
3736
|
|
|
3737
|
+
// src/archive/index.ts
|
|
3738
|
+
import { access as access3, cp, mkdir as mkdir3, readFile as readFile12, readdir as readdir5, rm, stat, writeFile as writeFile3 } from "fs/promises";
|
|
3739
|
+
import { dirname as dirname6, join as join14, sep } from "path";
|
|
3740
|
+
function toPosixPath(p) {
|
|
3741
|
+
return sep === "\\" ? p.replaceAll("\\", "/") : p;
|
|
3742
|
+
}
|
|
3743
|
+
var ARCHIVE_DIR = ".hatch3r-archive";
|
|
3744
|
+
var TOOL_PATH_PREFIXES = {
|
|
3745
|
+
cursor: [".cursor/"],
|
|
3746
|
+
claude: [".claude/", "CLAUDE.md", ".mcp.json"],
|
|
3747
|
+
copilot: [".github/copilot-instructions.md", ".github/workflows/copilot-setup-steps.yml", ".vscode/mcp.json"],
|
|
3748
|
+
windsurf: [".windsurf/", ".windsurfrules"],
|
|
3749
|
+
amp: [".amp/"],
|
|
3750
|
+
codex: [".codex/"],
|
|
3751
|
+
gemini: [".gemini/", "GEMINI.md"],
|
|
3752
|
+
cline: [".roo/", ".roomodes"],
|
|
3753
|
+
aider: ["CONVENTIONS.md", ".aider.conf.yml"],
|
|
3754
|
+
kiro: [".kiro/"],
|
|
3755
|
+
opencode: ["opencode.json"],
|
|
3756
|
+
goose: [".goosehints"],
|
|
3757
|
+
zed: [".rules"],
|
|
3758
|
+
"amazon-q": [".amazonq/"],
|
|
3759
|
+
antigravity: [".antigravity/"]
|
|
3760
|
+
};
|
|
3761
|
+
var PATH_PATTERNS = [
|
|
3762
|
+
{ pattern: /\/rules\/([^/]+)\.(mdc|md)$/, type: "rules" },
|
|
3763
|
+
{ pattern: /\/agents\/([^/]+)\.md$/, type: "agents" },
|
|
3764
|
+
{ pattern: /\/skills\/([^/]+)\/SKILL\.md$/, type: "skills" },
|
|
3765
|
+
{ pattern: /\/commands\/([^/]+)\.md$/, type: "commands" }
|
|
3766
|
+
];
|
|
3767
|
+
function parseOutputPath(filePath) {
|
|
3768
|
+
for (const { pattern, type } of PATH_PATTERNS) {
|
|
3769
|
+
const match = filePath.match(pattern);
|
|
3770
|
+
if (match) {
|
|
3771
|
+
let id = match[1];
|
|
3772
|
+
if (id.startsWith(HATCH3R_PREFIX)) {
|
|
3773
|
+
id = id.slice(HATCH3R_PREFIX.length);
|
|
3774
|
+
}
|
|
3775
|
+
id = sanitizeId(id);
|
|
3776
|
+
if (id.length > 0) return { type, id };
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
return null;
|
|
3780
|
+
}
|
|
3781
|
+
function stripFrontmatter(content) {
|
|
3782
|
+
const trimmed = content.trimStart();
|
|
3783
|
+
if (trimmed.startsWith("---")) {
|
|
3784
|
+
const endIdx = trimmed.indexOf("\n---", 3);
|
|
3785
|
+
if (endIdx !== -1) {
|
|
3786
|
+
return trimmed.slice(endIdx + 4).trim();
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
return content.trim();
|
|
3790
|
+
}
|
|
3791
|
+
async function fileExists2(path) {
|
|
3792
|
+
try {
|
|
3793
|
+
await access3(path);
|
|
3794
|
+
return true;
|
|
3795
|
+
} catch {
|
|
3796
|
+
return false;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
async function collectToolFiles(rootDir, tool) {
|
|
3800
|
+
const prefixes = TOOL_PATH_PREFIXES[tool];
|
|
3801
|
+
if (!prefixes) return [];
|
|
3802
|
+
const files = [];
|
|
3803
|
+
for (const prefix of prefixes) {
|
|
3804
|
+
const absPath = join14(rootDir, prefix);
|
|
3805
|
+
if (prefix.endsWith("/")) {
|
|
3806
|
+
try {
|
|
3807
|
+
const entries = await readdir5(absPath, { recursive: true, withFileTypes: true });
|
|
3808
|
+
for (const entry of entries) {
|
|
3809
|
+
if (entry.isFile()) {
|
|
3810
|
+
const parent = entry.parentPath ?? entry.path ?? absPath;
|
|
3811
|
+
const relPath = toPosixPath(join14(prefix, parent.slice(absPath.length), entry.name));
|
|
3812
|
+
files.push(relPath);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
} catch {
|
|
3816
|
+
}
|
|
3817
|
+
} else if (await fileExists2(absPath)) {
|
|
3818
|
+
files.push(prefix);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
return files;
|
|
3822
|
+
}
|
|
3823
|
+
async function archiveToolOutputs(rootDir, tool) {
|
|
3824
|
+
const filesToArchive = await collectToolFiles(rootDir, tool);
|
|
3825
|
+
if (filesToArchive.length === 0) {
|
|
3826
|
+
return { archivedFiles: [], migrations: [] };
|
|
3827
|
+
}
|
|
3828
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3829
|
+
const archiveBase = join14(rootDir, ARCHIVE_DIR, tool, timestamp);
|
|
3830
|
+
const archivedFiles = [];
|
|
3831
|
+
const migrations = [];
|
|
3832
|
+
for (const relPath of filesToArchive) {
|
|
3833
|
+
const absPath = join14(rootDir, relPath);
|
|
3834
|
+
if (!await fileExists2(absPath)) continue;
|
|
3835
|
+
let content;
|
|
3836
|
+
try {
|
|
3837
|
+
content = await readFile12(absPath, "utf-8");
|
|
3838
|
+
} catch {
|
|
3839
|
+
continue;
|
|
3840
|
+
}
|
|
3841
|
+
if (hasManagedBlock(content)) {
|
|
3842
|
+
const customContent = stripFrontmatter(extractCustomContent(content));
|
|
3843
|
+
if (customContent.length > 0) {
|
|
3844
|
+
const parsed = parseOutputPath(relPath);
|
|
3845
|
+
if (parsed) {
|
|
3846
|
+
const customizePath = join14(rootDir, ".hatch3r", parsed.type, `${parsed.id}.customize.md`);
|
|
3847
|
+
if (!await fileExists2(customizePath)) {
|
|
3848
|
+
await mkdir3(dirname6(customizePath), { recursive: true });
|
|
3849
|
+
await writeFile3(customizePath, customContent + "\n", "utf-8");
|
|
3850
|
+
migrations.push({
|
|
3851
|
+
from: relPath,
|
|
3852
|
+
to: `.hatch3r/${parsed.type}/${parsed.id}.customize.md`,
|
|
3853
|
+
type: parsed.type,
|
|
3854
|
+
id: parsed.id
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
const archiveDest = join14(archiveBase, relPath);
|
|
3861
|
+
await mkdir3(dirname6(archiveDest), { recursive: true });
|
|
3862
|
+
await cp(absPath, archiveDest);
|
|
3863
|
+
const srcStat = await stat(absPath);
|
|
3864
|
+
const destStat = await stat(archiveDest);
|
|
3865
|
+
if (destStat.size !== srcStat.size) {
|
|
3866
|
+
throw new Error(`Archive copy size mismatch for ${relPath}: source=${srcStat.size}, dest=${destStat.size}`);
|
|
3867
|
+
}
|
|
3868
|
+
await rm(absPath);
|
|
3869
|
+
archivedFiles.push(relPath);
|
|
3870
|
+
}
|
|
3871
|
+
await cleanEmptyDirs(rootDir, filesToArchive);
|
|
3872
|
+
return { archivedFiles, migrations };
|
|
3873
|
+
}
|
|
3874
|
+
async function cleanEmptyDirs(rootDir, paths) {
|
|
3875
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
3876
|
+
for (const p of paths) {
|
|
3877
|
+
let dir = dirname6(join14(rootDir, p));
|
|
3878
|
+
while (dir !== rootDir && dir.length > rootDir.length) {
|
|
3879
|
+
dirs.add(dir);
|
|
3880
|
+
dir = dirname6(dir);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
const sorted = [...dirs].sort((a, b) => b.length - a.length);
|
|
3884
|
+
for (const dir of sorted) {
|
|
3885
|
+
try {
|
|
3886
|
+
const entries = await readdir5(dir);
|
|
3887
|
+
if (entries.length === 0) {
|
|
3888
|
+
await rm(dir, { recursive: true });
|
|
3889
|
+
}
|
|
3890
|
+
} catch {
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
function removeManagedFilesForPaths(manifest, paths) {
|
|
3895
|
+
const pathSet = new Set(paths);
|
|
3896
|
+
manifest.managedFiles = manifest.managedFiles.filter((f) => !pathSet.has(f));
|
|
3897
|
+
}
|
|
3898
|
+
var MAX_ARCHIVE_ENTRIES = 5;
|
|
3899
|
+
async function pruneArchives(rootDir) {
|
|
3900
|
+
const archiveRoot = join14(rootDir, ARCHIVE_DIR);
|
|
3901
|
+
const pruned = [];
|
|
3902
|
+
let toolDirs;
|
|
3903
|
+
try {
|
|
3904
|
+
toolDirs = await readdir5(archiveRoot);
|
|
3905
|
+
} catch (err) {
|
|
3906
|
+
if (err.code === "ENOENT") return [];
|
|
3907
|
+
throw err;
|
|
3908
|
+
}
|
|
3909
|
+
for (const toolDir of toolDirs) {
|
|
3910
|
+
const toolPath = join14(archiveRoot, toolDir);
|
|
3911
|
+
let entries;
|
|
3912
|
+
try {
|
|
3913
|
+
const s = await stat(toolPath);
|
|
3914
|
+
if (!s.isDirectory()) continue;
|
|
3915
|
+
entries = await readdir5(toolPath);
|
|
3916
|
+
} catch {
|
|
3917
|
+
continue;
|
|
3918
|
+
}
|
|
3919
|
+
entries.sort((a, b) => b.localeCompare(a));
|
|
3920
|
+
for (const entry of entries.slice(MAX_ARCHIVE_ENTRIES)) {
|
|
3921
|
+
const entryPath = join14(toolPath, entry);
|
|
3922
|
+
await rm(entryPath, { recursive: true, force: true });
|
|
3923
|
+
pruned.push(`${toolDir}/${entry}`);
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
return pruned;
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3726
3929
|
// src/content/index.ts
|
|
3727
|
-
import { readFile as
|
|
3728
|
-
import { join as
|
|
3930
|
+
import { readFile as readFile13, readdir as readdir6, writeFile as writeFile4, cp as cp2, mkdir as mkdir4, rm as rm2 } from "fs/promises";
|
|
3931
|
+
import { join as join15, dirname as dirname7, normalize, isAbsolute } from "path";
|
|
3729
3932
|
function assertSafePath(relativePath, label2) {
|
|
3730
3933
|
const sanitized = relativePath.replace(/\0/g, "");
|
|
3731
3934
|
const normalized = normalize(sanitized);
|
|
3732
3935
|
if (normalized.startsWith("..") || isAbsolute(normalized)) {
|
|
3733
|
-
throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1);
|
|
3936
|
+
throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
|
|
3734
3937
|
}
|
|
3735
3938
|
if (sanitized !== relativePath) {
|
|
3736
|
-
throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1);
|
|
3939
|
+
throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
|
|
3737
3940
|
}
|
|
3738
3941
|
}
|
|
3739
3942
|
function extractContentReferences(content) {
|
|
@@ -3751,8 +3954,8 @@ async function validateCrossReferences(contentRoot, index) {
|
|
|
3751
3954
|
for (const item of index.items) {
|
|
3752
3955
|
let content;
|
|
3753
3956
|
try {
|
|
3754
|
-
const filePath = item.type === "skill" ?
|
|
3755
|
-
content = await
|
|
3957
|
+
const filePath = item.type === "skill" ? join15(contentRoot, item.relativePath, "SKILL.md") : join15(contentRoot, `${item.relativePath}`);
|
|
3958
|
+
content = await readFile13(filePath, "utf-8");
|
|
3756
3959
|
} catch {
|
|
3757
3960
|
continue;
|
|
3758
3961
|
}
|
|
@@ -3789,6 +3992,12 @@ function validateOrchestrationDependencies(selection) {
|
|
|
3789
3992
|
}
|
|
3790
3993
|
return warnings;
|
|
3791
3994
|
}
|
|
3995
|
+
function typeIdKey(type, id) {
|
|
3996
|
+
return `${type}:${id}`;
|
|
3997
|
+
}
|
|
3998
|
+
function getAllItemsById(index, id) {
|
|
3999
|
+
return index.items.filter((item) => item.id === id);
|
|
4000
|
+
}
|
|
3792
4001
|
var CONTENT_TYPE_CONFIGS = [
|
|
3793
4002
|
{ dir: "agents", type: "agent", strategy: "glob" },
|
|
3794
4003
|
{ dir: "commands", type: "command", strategy: "glob" },
|
|
@@ -3801,20 +4010,20 @@ var CONTENT_TYPE_CONFIGS = [
|
|
|
3801
4010
|
async function buildContentIndex(contentRoot) {
|
|
3802
4011
|
const items = [];
|
|
3803
4012
|
for (const config of CONTENT_TYPE_CONFIGS) {
|
|
3804
|
-
const dirPath =
|
|
4013
|
+
const dirPath = join15(contentRoot, config.dir);
|
|
3805
4014
|
if (config.strategy === "subdirectory") {
|
|
3806
4015
|
let dirents;
|
|
3807
4016
|
try {
|
|
3808
|
-
dirents = await
|
|
4017
|
+
dirents = (await readdir6(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
3809
4018
|
} catch (err) {
|
|
3810
4019
|
if (err.code === "ENOENT") continue;
|
|
3811
4020
|
throw err;
|
|
3812
4021
|
}
|
|
3813
4022
|
for (const dirent of dirents) {
|
|
3814
4023
|
if (!dirent.isDirectory()) continue;
|
|
3815
|
-
const skillPath =
|
|
4024
|
+
const skillPath = join15(dirPath, dirent.name, "SKILL.md");
|
|
3816
4025
|
try {
|
|
3817
|
-
const raw = await
|
|
4026
|
+
const raw = await readFile13(skillPath, "utf-8");
|
|
3818
4027
|
const { metadata } = parseFrontmatter(raw);
|
|
3819
4028
|
const id = metadata.id || metadata.name || dirent.name;
|
|
3820
4029
|
items.push({
|
|
@@ -3823,7 +4032,7 @@ async function buildContentIndex(contentRoot) {
|
|
|
3823
4032
|
description: metadata.description ?? "",
|
|
3824
4033
|
tags: metadata.tags ?? [],
|
|
3825
4034
|
protected: metadata.protected,
|
|
3826
|
-
relativePath:
|
|
4035
|
+
relativePath: join15(config.dir, dirent.name)
|
|
3827
4036
|
});
|
|
3828
4037
|
} catch (err) {
|
|
3829
4038
|
if (err.code !== "ENOENT") throw err;
|
|
@@ -3832,15 +4041,15 @@ async function buildContentIndex(contentRoot) {
|
|
|
3832
4041
|
} else {
|
|
3833
4042
|
let entries;
|
|
3834
4043
|
try {
|
|
3835
|
-
const all = await
|
|
3836
|
-
entries = all.filter((f) => f.endsWith(".md"));
|
|
4044
|
+
const all = await readdir6(dirPath);
|
|
4045
|
+
entries = all.filter((f) => f.endsWith(".md")).sort();
|
|
3837
4046
|
} catch (err) {
|
|
3838
4047
|
if (err.code === "ENOENT") continue;
|
|
3839
4048
|
throw err;
|
|
3840
4049
|
}
|
|
3841
4050
|
for (const file of entries) {
|
|
3842
|
-
const filePath =
|
|
3843
|
-
const raw = await
|
|
4051
|
+
const filePath = join15(dirPath, file);
|
|
4052
|
+
const raw = await readFile13(filePath, "utf-8");
|
|
3844
4053
|
const { metadata } = parseFrontmatter(raw);
|
|
3845
4054
|
const id = metadata.id || metadata.name || file.replace(/\.md$/, "");
|
|
3846
4055
|
const item = {
|
|
@@ -3849,13 +4058,13 @@ async function buildContentIndex(contentRoot) {
|
|
|
3849
4058
|
description: metadata.description ?? "",
|
|
3850
4059
|
tags: metadata.tags ?? [],
|
|
3851
4060
|
protected: metadata.protected,
|
|
3852
|
-
relativePath:
|
|
4061
|
+
relativePath: join15(config.dir, file)
|
|
3853
4062
|
};
|
|
3854
4063
|
if (config.type === "rule") {
|
|
3855
4064
|
const mdcFile = file.replace(/\.md$/, ".mdc");
|
|
3856
4065
|
try {
|
|
3857
|
-
await
|
|
3858
|
-
item.companionPath =
|
|
4066
|
+
await readFile13(join15(dirPath, mdcFile), "utf-8");
|
|
4067
|
+
item.companionPath = join15(config.dir, mdcFile);
|
|
3859
4068
|
} catch {
|
|
3860
4069
|
}
|
|
3861
4070
|
}
|
|
@@ -3865,10 +4074,12 @@ async function buildContentIndex(contentRoot) {
|
|
|
3865
4074
|
}
|
|
3866
4075
|
const byType = {};
|
|
3867
4076
|
const byId = /* @__PURE__ */ new Map();
|
|
4077
|
+
const byTypeAndId = /* @__PURE__ */ new Map();
|
|
3868
4078
|
const collisions = [];
|
|
3869
4079
|
for (const item of items) {
|
|
3870
4080
|
if (!byType[item.type]) byType[item.type] = [];
|
|
3871
4081
|
byType[item.type].push(item);
|
|
4082
|
+
byTypeAndId.set(typeIdKey(item.type, item.id), item);
|
|
3872
4083
|
const existing = byId.get(item.id);
|
|
3873
4084
|
if (existing) {
|
|
3874
4085
|
const kind = existing.type !== item.type ? "cross-type" : "same-type";
|
|
@@ -3882,7 +4093,7 @@ async function buildContentIndex(contentRoot) {
|
|
|
3882
4093
|
});
|
|
3883
4094
|
if (kind === "cross-type") {
|
|
3884
4095
|
console.warn(
|
|
3885
|
-
`[hatch3r] Content ID collision: "${item.id}" exists as both ${existing.type} (${existing.relativePath}) and ${item.type} (${item.relativePath}).
|
|
4096
|
+
`[hatch3r] Content ID collision: "${item.id}" exists as both ${existing.type} (${existing.relativePath}) and ${item.type} (${item.relativePath}). Use index.byTypeAndId for collision-safe lookup.`
|
|
3886
4097
|
);
|
|
3887
4098
|
} else {
|
|
3888
4099
|
console.warn(
|
|
@@ -3892,7 +4103,7 @@ async function buildContentIndex(contentRoot) {
|
|
|
3892
4103
|
}
|
|
3893
4104
|
byId.set(item.id, item);
|
|
3894
4105
|
}
|
|
3895
|
-
return { items, byType, byId, collisions };
|
|
4106
|
+
return { items, byType, byId, byTypeAndId, collisions };
|
|
3896
4107
|
}
|
|
3897
4108
|
var TYPE_TO_SELECTION_KEY = {
|
|
3898
4109
|
agent: "agents",
|
|
@@ -4022,21 +4233,21 @@ async function copySelectedContent(contentRoot, agentsDir, selection, index) {
|
|
|
4022
4233
|
if (item.companionPath) {
|
|
4023
4234
|
assertSafePath(item.companionPath, "copySelectedContent companion");
|
|
4024
4235
|
}
|
|
4025
|
-
const srcPath =
|
|
4026
|
-
const destPath =
|
|
4236
|
+
const srcPath = join15(contentRoot, item.relativePath);
|
|
4237
|
+
const destPath = join15(agentsDir, item.relativePath);
|
|
4027
4238
|
if (item.type === "skill") {
|
|
4028
|
-
await
|
|
4029
|
-
await
|
|
4239
|
+
await mkdir4(destPath, { recursive: true });
|
|
4240
|
+
await cp2(srcPath, destPath, { recursive: true, force: true });
|
|
4030
4241
|
copied.push(item.relativePath);
|
|
4031
4242
|
} else {
|
|
4032
|
-
await
|
|
4033
|
-
await
|
|
4243
|
+
await mkdir4(dirname7(destPath), { recursive: true });
|
|
4244
|
+
await cp2(srcPath, destPath, { force: true });
|
|
4034
4245
|
copied.push(item.relativePath);
|
|
4035
4246
|
if (item.companionPath) {
|
|
4036
|
-
const mdcSrc =
|
|
4037
|
-
const mdcDest =
|
|
4247
|
+
const mdcSrc = join15(contentRoot, item.companionPath);
|
|
4248
|
+
const mdcDest = join15(agentsDir, item.companionPath);
|
|
4038
4249
|
try {
|
|
4039
|
-
await
|
|
4250
|
+
await cp2(mdcSrc, mdcDest, { force: true });
|
|
4040
4251
|
copied.push(item.companionPath);
|
|
4041
4252
|
} catch (err) {
|
|
4042
4253
|
if (err.code !== "ENOENT") throw err;
|
|
@@ -4047,31 +4258,31 @@ async function copySelectedContent(contentRoot, agentsDir, selection, index) {
|
|
|
4047
4258
|
for (const config of CONTENT_TYPE_CONFIGS) {
|
|
4048
4259
|
if (config.strategy !== "glob") continue;
|
|
4049
4260
|
try {
|
|
4050
|
-
const dirEntries = await
|
|
4261
|
+
const dirEntries = await readdir6(join15(contentRoot, config.dir), { withFileTypes: true });
|
|
4051
4262
|
for (const entry of dirEntries) {
|
|
4052
4263
|
if (!entry.isDirectory() || entry.name.startsWith("hatch3r-")) continue;
|
|
4053
|
-
const subSrc =
|
|
4054
|
-
const subDest =
|
|
4055
|
-
await
|
|
4056
|
-
await
|
|
4264
|
+
const subSrc = join15(contentRoot, config.dir, entry.name);
|
|
4265
|
+
const subDest = join15(agentsDir, config.dir, entry.name);
|
|
4266
|
+
await mkdir4(subDest, { recursive: true });
|
|
4267
|
+
await cp2(subSrc, subDest, { recursive: true, force: true });
|
|
4057
4268
|
}
|
|
4058
4269
|
} catch (err) {
|
|
4059
4270
|
if (err.code !== "ENOENT") throw err;
|
|
4060
4271
|
}
|
|
4061
4272
|
}
|
|
4062
4273
|
try {
|
|
4063
|
-
const checksSrc =
|
|
4064
|
-
const checksDest =
|
|
4065
|
-
await
|
|
4066
|
-
await
|
|
4274
|
+
const checksSrc = join15(contentRoot, "checks");
|
|
4275
|
+
const checksDest = join15(agentsDir, "checks");
|
|
4276
|
+
await mkdir4(checksDest, { recursive: true });
|
|
4277
|
+
await cp2(checksSrc, checksDest, { recursive: true, force: true });
|
|
4067
4278
|
} catch (err) {
|
|
4068
4279
|
if (err.code !== "ENOENT") throw err;
|
|
4069
4280
|
}
|
|
4070
4281
|
try {
|
|
4071
|
-
const mcpSrc =
|
|
4072
|
-
const mcpDest =
|
|
4073
|
-
await
|
|
4074
|
-
await
|
|
4282
|
+
const mcpSrc = join15(contentRoot, "mcp");
|
|
4283
|
+
const mcpDest = join15(agentsDir, "mcp");
|
|
4284
|
+
await mkdir4(mcpDest, { recursive: true });
|
|
4285
|
+
await cp2(mcpSrc, mcpDest, { recursive: true, force: true });
|
|
4075
4286
|
} catch (err) {
|
|
4076
4287
|
if (err.code !== "ENOENT") throw err;
|
|
4077
4288
|
}
|
|
@@ -4088,16 +4299,16 @@ async function buildSelectionsFromDisk(agentsDir) {
|
|
|
4088
4299
|
githubAgents: []
|
|
4089
4300
|
};
|
|
4090
4301
|
for (const config of CONTENT_TYPE_CONFIGS) {
|
|
4091
|
-
const dirPath =
|
|
4302
|
+
const dirPath = join15(agentsDir, config.dir);
|
|
4092
4303
|
const key = TYPE_TO_SELECTION_KEY[config.type];
|
|
4093
4304
|
if (!key) continue;
|
|
4094
4305
|
if (config.strategy === "subdirectory") {
|
|
4095
4306
|
try {
|
|
4096
|
-
const dirents = await
|
|
4307
|
+
const dirents = await readdir6(dirPath, { withFileTypes: true });
|
|
4097
4308
|
for (const d of dirents) {
|
|
4098
4309
|
if (!d.isDirectory()) continue;
|
|
4099
4310
|
try {
|
|
4100
|
-
const raw = await
|
|
4311
|
+
const raw = await readFile13(join15(dirPath, d.name, "SKILL.md"), "utf-8");
|
|
4101
4312
|
const { metadata } = parseFrontmatter(raw);
|
|
4102
4313
|
items[key].push(metadata.id || metadata.name || d.name);
|
|
4103
4314
|
} catch {
|
|
@@ -4107,9 +4318,9 @@ async function buildSelectionsFromDisk(agentsDir) {
|
|
|
4107
4318
|
}
|
|
4108
4319
|
} else {
|
|
4109
4320
|
try {
|
|
4110
|
-
const files = await
|
|
4321
|
+
const files = await readdir6(dirPath);
|
|
4111
4322
|
for (const f of files.filter((f2) => f2.endsWith(".md"))) {
|
|
4112
|
-
const raw = await
|
|
4323
|
+
const raw = await readFile13(join15(dirPath, f), "utf-8");
|
|
4113
4324
|
const { metadata } = parseFrontmatter(raw);
|
|
4114
4325
|
items[key].push(metadata.id || metadata.name || f.replace(/\.md$/, ""));
|
|
4115
4326
|
}
|
|
@@ -4129,20 +4340,20 @@ async function addContentItem(contentRoot, agentsDir, item) {
|
|
|
4129
4340
|
if (item.companionPath) {
|
|
4130
4341
|
assertSafePath(item.companionPath, "addContentItem companion");
|
|
4131
4342
|
}
|
|
4132
|
-
const srcPath =
|
|
4133
|
-
const destPath =
|
|
4343
|
+
const srcPath = join15(contentRoot, item.relativePath);
|
|
4344
|
+
const destPath = join15(agentsDir, item.relativePath);
|
|
4134
4345
|
try {
|
|
4135
4346
|
if (item.type === "skill") {
|
|
4136
|
-
await
|
|
4137
|
-
await
|
|
4347
|
+
await mkdir4(destPath, { recursive: true });
|
|
4348
|
+
await cp2(srcPath, destPath, { recursive: true, force: true });
|
|
4138
4349
|
} else {
|
|
4139
|
-
await
|
|
4140
|
-
await
|
|
4350
|
+
await mkdir4(dirname7(destPath), { recursive: true });
|
|
4351
|
+
await cp2(srcPath, destPath, { force: true });
|
|
4141
4352
|
if (item.companionPath) {
|
|
4142
4353
|
try {
|
|
4143
|
-
await
|
|
4144
|
-
|
|
4145
|
-
|
|
4354
|
+
await cp2(
|
|
4355
|
+
join15(contentRoot, item.companionPath),
|
|
4356
|
+
join15(agentsDir, item.companionPath),
|
|
4146
4357
|
{ force: true }
|
|
4147
4358
|
);
|
|
4148
4359
|
} catch (err) {
|
|
@@ -4154,7 +4365,8 @@ async function addContentItem(contentRoot, agentsDir, item) {
|
|
|
4154
4365
|
if (err.code === "ENOENT") {
|
|
4155
4366
|
throw new HatchError(
|
|
4156
4367
|
`Content "${item.id}" (${item.type}) not found in package at ${item.relativePath}. It may have been renamed or removed in this hatch3r version.`,
|
|
4157
|
-
1
|
|
4368
|
+
1,
|
|
4369
|
+
"FS_ERROR"
|
|
4158
4370
|
);
|
|
4159
4371
|
}
|
|
4160
4372
|
throw err;
|
|
@@ -4165,13 +4377,13 @@ async function removeContentItem(agentsDir, item, options) {
|
|
|
4165
4377
|
if (item.companionPath) {
|
|
4166
4378
|
assertSafePath(item.companionPath, "removeContentItem companion");
|
|
4167
4379
|
}
|
|
4168
|
-
const destPath =
|
|
4380
|
+
const destPath = join15(agentsDir, item.relativePath);
|
|
4169
4381
|
if (item.type === "skill") {
|
|
4170
|
-
await
|
|
4382
|
+
await rm2(destPath, { recursive: true, force: true });
|
|
4171
4383
|
} else {
|
|
4172
|
-
await
|
|
4384
|
+
await rm2(destPath, { force: true });
|
|
4173
4385
|
if (item.companionPath) {
|
|
4174
|
-
await
|
|
4386
|
+
await rm2(join15(agentsDir, item.companionPath), { force: true });
|
|
4175
4387
|
}
|
|
4176
4388
|
}
|
|
4177
4389
|
if (options?.rootDir) {
|
|
@@ -4183,10 +4395,10 @@ async function removeContentItem(agentsDir, item, options) {
|
|
|
4183
4395
|
};
|
|
4184
4396
|
const customDir = typeToDir[item.type];
|
|
4185
4397
|
if (customDir) {
|
|
4186
|
-
const yamlPath =
|
|
4187
|
-
const mdPath =
|
|
4188
|
-
await
|
|
4189
|
-
await
|
|
4398
|
+
const yamlPath = join15(options.rootDir, ".hatch3r", customDir, `${item.id}.customize.yaml`);
|
|
4399
|
+
const mdPath = join15(options.rootDir, ".hatch3r", customDir, `${item.id}.customize.md`);
|
|
4400
|
+
await rm2(yamlPath, { force: true });
|
|
4401
|
+
await rm2(mdPath, { force: true });
|
|
4190
4402
|
}
|
|
4191
4403
|
}
|
|
4192
4404
|
}
|
|
@@ -4218,40 +4430,40 @@ function selectionSummary(selection) {
|
|
|
4218
4430
|
}
|
|
4219
4431
|
|
|
4220
4432
|
// src/cli/commands/update.ts
|
|
4221
|
-
var __dirname =
|
|
4433
|
+
var __dirname = dirname8(fileURLToPath(import.meta.url));
|
|
4222
4434
|
var CONTENT_DIRS = ["agents", "commands", "rules", "skills", "prompts", "github-agents", "mcp", "hooks"];
|
|
4223
4435
|
var ALWAYS_COPY_FILES = /* @__PURE__ */ new Set(["mcp.json"]);
|
|
4224
4436
|
async function copyHatch3rFiles(srcDir, destDir, insideHatch3rDir = false, selectedIds) {
|
|
4225
4437
|
const copied = [];
|
|
4226
4438
|
let entries;
|
|
4227
4439
|
try {
|
|
4228
|
-
entries = await
|
|
4440
|
+
entries = await readdir7(srcDir, { withFileTypes: true });
|
|
4229
4441
|
} catch (err) {
|
|
4230
4442
|
if (err.code === "ENOENT") return [];
|
|
4231
4443
|
throw err;
|
|
4232
4444
|
}
|
|
4233
4445
|
for (const entry of entries) {
|
|
4234
|
-
const srcPath =
|
|
4235
|
-
const destPath =
|
|
4446
|
+
const srcPath = join16(srcDir, entry.name);
|
|
4447
|
+
const destPath = join16(destDir, entry.name);
|
|
4236
4448
|
if (entry.isDirectory()) {
|
|
4237
4449
|
if (selectedIds && entry.name.startsWith(HATCH3R_PREFIX)) {
|
|
4238
4450
|
if (!selectedIds.has(entry.name)) continue;
|
|
4239
4451
|
}
|
|
4240
|
-
await
|
|
4452
|
+
await mkdir5(destPath, { recursive: true });
|
|
4241
4453
|
const subCopied = await copyHatch3rFiles(
|
|
4242
4454
|
srcPath,
|
|
4243
4455
|
destPath,
|
|
4244
4456
|
insideHatch3rDir || !entry.name.startsWith(HATCH3R_PREFIX),
|
|
4245
4457
|
selectedIds
|
|
4246
4458
|
);
|
|
4247
|
-
copied.push(...subCopied.map((p) =>
|
|
4459
|
+
copied.push(...subCopied.map((p) => join16(entry.name, p)));
|
|
4248
4460
|
} else if (entry.name.startsWith(HATCH3R_PREFIX) || insideHatch3rDir || ALWAYS_COPY_FILES.has(entry.name)) {
|
|
4249
4461
|
if (selectedIds && entry.name.startsWith(HATCH3R_PREFIX)) {
|
|
4250
4462
|
const baseId = entry.name.replace(/\.(md|mdc)$/, "");
|
|
4251
4463
|
if (!selectedIds.has(baseId)) continue;
|
|
4252
4464
|
}
|
|
4253
|
-
await
|
|
4254
|
-
await
|
|
4465
|
+
await mkdir5(dirname8(destPath), { recursive: true });
|
|
4466
|
+
await cp3(srcPath, destPath, { force: true });
|
|
4255
4467
|
copied.push(entry.name);
|
|
4256
4468
|
}
|
|
4257
4469
|
}
|
|
@@ -4260,7 +4472,7 @@ async function copyHatch3rFiles(srcDir, destDir, insideHatch3rDir = false, selec
|
|
|
4260
4472
|
async function runUpdate(rootDir, manifest, options = {}) {
|
|
4261
4473
|
const offset = options.stepOffset ?? 0;
|
|
4262
4474
|
const total = options.totalSteps ?? 4;
|
|
4263
|
-
const agentsDir =
|
|
4475
|
+
const agentsDir = join16(rootDir, AGENTS_DIR);
|
|
4264
4476
|
let contentRoot = findPackageRoot(__dirname);
|
|
4265
4477
|
const pm = await detectPackageManager(rootDir);
|
|
4266
4478
|
const s0 = createSpinner(step(offset + 1, total, "Updating package..."));
|
|
@@ -4274,7 +4486,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
|
|
|
4274
4486
|
const msg = isTimeout ? "Package update timed out after 30s. Check network connectivity and retry." : err instanceof Error ? err.message : String(err);
|
|
4275
4487
|
s0.fail(step(offset + 1, total, "Failed to update package"));
|
|
4276
4488
|
error(msg);
|
|
4277
|
-
throw new HatchError(msg, 1);
|
|
4489
|
+
throw new HatchError(msg, 1, isTimeout ? "NETWORK_ERROR" : "UNKNOWN_ERROR");
|
|
4278
4490
|
}
|
|
4279
4491
|
s0.succeed(step(offset + 1, total, "Package updated"));
|
|
4280
4492
|
const s1 = createSpinner(step(offset + 2, total, "Updating canonical files..."));
|
|
@@ -4288,16 +4500,16 @@ async function runUpdate(rootDir, manifest, options = {}) {
|
|
|
4288
4500
|
}
|
|
4289
4501
|
const copied = [];
|
|
4290
4502
|
for (const dir of CONTENT_DIRS) {
|
|
4291
|
-
const srcDir =
|
|
4503
|
+
const srcDir = join16(contentRoot, dir);
|
|
4292
4504
|
try {
|
|
4293
|
-
const dirCopied = await copyHatch3rFiles(srcDir,
|
|
4294
|
-
copied.push(...dirCopied.map((p) =>
|
|
4505
|
+
const dirCopied = await copyHatch3rFiles(srcDir, join16(agentsDir, dir), false, selectedIds);
|
|
4506
|
+
copied.push(...dirCopied.map((p) => join16(dir, p)));
|
|
4295
4507
|
} catch (err) {
|
|
4296
4508
|
if (err.code !== "ENOENT") throw err;
|
|
4297
4509
|
}
|
|
4298
4510
|
}
|
|
4299
4511
|
const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
|
|
4300
|
-
await safeWriteFile(
|
|
4512
|
+
await safeWriteFile(join16(agentsDir, "AGENTS.md"), canonicalAgentsMd);
|
|
4301
4513
|
s1.succeed(step(offset + 2, total, `Updated ${copied.length} canonical files`));
|
|
4302
4514
|
const s2 = createSpinner(step(offset + 3, total, "Re-syncing adapter output..."));
|
|
4303
4515
|
s2.start();
|
|
@@ -4310,7 +4522,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
|
|
|
4310
4522
|
warn(w);
|
|
4311
4523
|
}
|
|
4312
4524
|
for (const out of outputs) {
|
|
4313
|
-
const fullPath =
|
|
4525
|
+
const fullPath = join16(rootDir, out.path);
|
|
4314
4526
|
if (out.managedContent) {
|
|
4315
4527
|
await safeWriteFile(fullPath, out.content, {
|
|
4316
4528
|
managedContent: out.managedContent
|
|
@@ -4332,7 +4544,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
|
|
|
4332
4544
|
}
|
|
4333
4545
|
if (adapterFailures.length === manifest.tools.length) {
|
|
4334
4546
|
s2.fail(step(offset + 3, total, "All adapters failed"));
|
|
4335
|
-
throw new HatchError("All adapters failed", 1);
|
|
4547
|
+
throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
|
|
4336
4548
|
}
|
|
4337
4549
|
}
|
|
4338
4550
|
s2.succeed(step(offset + 3, total, adapterFailures.length > 0 ? `Re-synced ${manifest.tools.length - adapterFailures.length}/${manifest.tools.length} tool(s)` : `Re-synced ${manifest.tools.length} tool(s)`));
|
|
@@ -4342,6 +4554,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
|
|
|
4342
4554
|
await writeManifest(rootDir, manifest);
|
|
4343
4555
|
const integrityManifest = await generateIntegrityManifest(agentsDir, HATCH3R_VERSION);
|
|
4344
4556
|
await writeIntegrityManifest(agentsDir, integrityManifest);
|
|
4557
|
+
await pruneArchives(rootDir);
|
|
4345
4558
|
s3.succeed(step(offset + 4, total, "Manifest updated"));
|
|
4346
4559
|
return {
|
|
4347
4560
|
copiedFiles: copied.length,
|
|
@@ -4354,35 +4567,40 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4354
4567
|
{
|
|
4355
4568
|
id: "content-selections-init",
|
|
4356
4569
|
condition: async (manifest) => manifest.content === void 0,
|
|
4357
|
-
execute: async (manifest, rootDir) => {
|
|
4358
|
-
const agentsDir =
|
|
4570
|
+
execute: async (manifest, rootDir, headless) => {
|
|
4571
|
+
const agentsDir = join16(rootDir, AGENTS_DIR);
|
|
4359
4572
|
const content = await buildSelectionsFromDisk(agentsDir);
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4573
|
+
if (headless) {
|
|
4574
|
+
content.projectType = "brownfield";
|
|
4575
|
+
content.teamSize = "team";
|
|
4576
|
+
} else {
|
|
4577
|
+
const { projectType } = await inquirer.prompt([
|
|
4578
|
+
{
|
|
4579
|
+
type: "list",
|
|
4580
|
+
name: "projectType",
|
|
4581
|
+
message: "For content tracking \u2014 is this a greenfield or brownfield project?",
|
|
4582
|
+
choices: [
|
|
4583
|
+
{ name: "Greenfield \u2014 new project", value: "greenfield" },
|
|
4584
|
+
{ name: "Brownfield \u2014 existing codebase", value: "brownfield" }
|
|
4585
|
+
],
|
|
4586
|
+
default: "brownfield"
|
|
4587
|
+
}
|
|
4588
|
+
]);
|
|
4589
|
+
const { teamSize } = await inquirer.prompt([
|
|
4590
|
+
{
|
|
4591
|
+
type: "list",
|
|
4592
|
+
name: "teamSize",
|
|
4593
|
+
message: "Solo developer or team?",
|
|
4594
|
+
choices: [
|
|
4595
|
+
{ name: "Solo", value: "solo" },
|
|
4596
|
+
{ name: "Team", value: "team" }
|
|
4597
|
+
],
|
|
4598
|
+
default: "team"
|
|
4599
|
+
}
|
|
4600
|
+
]);
|
|
4601
|
+
content.projectType = projectType;
|
|
4602
|
+
content.teamSize = teamSize;
|
|
4603
|
+
}
|
|
4386
4604
|
return {
|
|
4387
4605
|
manifest: { ...manifest, content },
|
|
4388
4606
|
notices: ["Migrated to explicit content tracking (all existing items preserved)"]
|
|
@@ -4392,20 +4610,26 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4392
4610
|
{
|
|
4393
4611
|
id: "platform-selection",
|
|
4394
4612
|
condition: async (manifest) => !manifest.platform,
|
|
4395
|
-
execute: async (manifest) => {
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4613
|
+
execute: async (manifest, _rootDir, headless) => {
|
|
4614
|
+
let platform;
|
|
4615
|
+
if (headless) {
|
|
4616
|
+
platform = "github";
|
|
4617
|
+
} else {
|
|
4618
|
+
const answer = await inquirer.prompt([
|
|
4619
|
+
{
|
|
4620
|
+
type: "list",
|
|
4621
|
+
name: "platform",
|
|
4622
|
+
message: "hatch3r now supports multiple platforms. Select your platform:",
|
|
4623
|
+
choices: [
|
|
4624
|
+
{ name: "GitHub", value: "github" },
|
|
4625
|
+
{ name: "Azure DevOps", value: "azure-devops" },
|
|
4626
|
+
{ name: "GitLab", value: "gitlab" }
|
|
4627
|
+
],
|
|
4628
|
+
default: "github"
|
|
4629
|
+
}
|
|
4630
|
+
]);
|
|
4631
|
+
platform = answer.platform;
|
|
4632
|
+
}
|
|
4409
4633
|
const updated = { ...manifest, platform };
|
|
4410
4634
|
const notices = [];
|
|
4411
4635
|
if (platform === "github") {
|
|
@@ -4433,12 +4657,12 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4433
4657
|
{
|
|
4434
4658
|
id: "customize-yaml-size",
|
|
4435
4659
|
condition: async (_manifest, rootDir) => {
|
|
4436
|
-
const agentsDir =
|
|
4660
|
+
const agentsDir = join16(rootDir, AGENTS_DIR);
|
|
4437
4661
|
try {
|
|
4438
|
-
const entries = await
|
|
4662
|
+
const entries = await readdir7(agentsDir, { recursive: true });
|
|
4439
4663
|
for (const entry of entries) {
|
|
4440
4664
|
if (typeof entry === "string" && entry.endsWith(".customize.yaml")) {
|
|
4441
|
-
const s = await
|
|
4665
|
+
const s = await stat2(join16(agentsDir, entry));
|
|
4442
4666
|
if (s.size > 10240) return true;
|
|
4443
4667
|
}
|
|
4444
4668
|
}
|
|
@@ -4447,14 +4671,14 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4447
4671
|
}
|
|
4448
4672
|
return false;
|
|
4449
4673
|
},
|
|
4450
|
-
execute: async (manifest, rootDir) => {
|
|
4674
|
+
execute: async (manifest, rootDir, _headless) => {
|
|
4451
4675
|
const notices = [];
|
|
4452
|
-
const agentsDir =
|
|
4676
|
+
const agentsDir = join16(rootDir, AGENTS_DIR);
|
|
4453
4677
|
try {
|
|
4454
|
-
const entries = await
|
|
4678
|
+
const entries = await readdir7(agentsDir, { recursive: true });
|
|
4455
4679
|
for (const entry of entries) {
|
|
4456
4680
|
if (typeof entry === "string" && entry.endsWith(".customize.yaml")) {
|
|
4457
|
-
const s = await
|
|
4681
|
+
const s = await stat2(join16(agentsDir, entry));
|
|
4458
4682
|
if (s.size > 10240) {
|
|
4459
4683
|
notices.push(`Large customize file detected: ${entry} (${Math.round(s.size / 1024)}KB) \u2014 consider splitting`);
|
|
4460
4684
|
}
|
|
@@ -4473,18 +4697,24 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4473
4697
|
const worktreeCapableTools = /* @__PURE__ */ new Set(["claude"]);
|
|
4474
4698
|
return manifest.tools.some((t) => worktreeCapableTools.has(t));
|
|
4475
4699
|
},
|
|
4476
|
-
execute: async (manifest, rootDir) => {
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4700
|
+
execute: async (manifest, rootDir, headless) => {
|
|
4701
|
+
let enabled;
|
|
4702
|
+
if (headless) {
|
|
4703
|
+
enabled = true;
|
|
4704
|
+
} else {
|
|
4705
|
+
const answer = await inquirer.prompt([{
|
|
4706
|
+
type: "confirm",
|
|
4707
|
+
name: "enabled",
|
|
4708
|
+
message: "hatch3r now supports worktree file isolation for parallel agent sessions. Enable it?",
|
|
4709
|
+
default: true
|
|
4710
|
+
}]);
|
|
4711
|
+
enabled = answer.enabled;
|
|
4712
|
+
}
|
|
4483
4713
|
const updated = { ...manifest, worktree: { enabled } };
|
|
4484
4714
|
const notices = [];
|
|
4485
4715
|
if (enabled) {
|
|
4486
4716
|
const wtContent = await generateWorktreeInclude(updated, rootDir);
|
|
4487
|
-
await safeWriteFile(
|
|
4717
|
+
await safeWriteFile(join16(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
|
|
4488
4718
|
appendIfNoBlock: true
|
|
4489
4719
|
});
|
|
4490
4720
|
notices.push("Worktree isolation enabled \u2014 .worktreeinclude generated");
|
|
@@ -4495,12 +4725,12 @@ var MIGRATION_CHECKPOINTS = [
|
|
|
4495
4725
|
}
|
|
4496
4726
|
}
|
|
4497
4727
|
];
|
|
4498
|
-
async function runMigrationCheckpoints(manifest, rootDir) {
|
|
4728
|
+
async function runMigrationCheckpoints(manifest, rootDir, headless = false) {
|
|
4499
4729
|
let current = manifest;
|
|
4500
4730
|
const allNotices = [];
|
|
4501
4731
|
for (const checkpoint of MIGRATION_CHECKPOINTS) {
|
|
4502
4732
|
if (await checkpoint.condition(current, rootDir)) {
|
|
4503
|
-
const { manifest: updated, notices } = await checkpoint.execute(current, rootDir);
|
|
4733
|
+
const { manifest: updated, notices } = await checkpoint.execute(current, rootDir, headless);
|
|
4504
4734
|
current = updated;
|
|
4505
4735
|
allNotices.push(...notices);
|
|
4506
4736
|
}
|
|
@@ -4514,9 +4744,10 @@ async function updateCommand(_opts) {
|
|
|
4514
4744
|
if (!manifest) {
|
|
4515
4745
|
error("No .agents/hatch.json found.");
|
|
4516
4746
|
console.log(chalk4.dim(" Run `npx hatch3r init` to set up your project first.\n"));
|
|
4517
|
-
throw new HatchError("No .agents/hatch.json found.", 1);
|
|
4747
|
+
throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
|
|
4518
4748
|
}
|
|
4519
|
-
const
|
|
4749
|
+
const headless = !!_opts?.yes;
|
|
4750
|
+
const { manifest: migrated, allNotices } = await runMigrationCheckpoints(manifest, rootDir, headless);
|
|
4520
4751
|
const m = migrated;
|
|
4521
4752
|
for (const notice of allNotices) {
|
|
4522
4753
|
warn(notice);
|
|
@@ -4537,177 +4768,22 @@ async function updateCommand(_opts) {
|
|
|
4537
4768
|
], "success");
|
|
4538
4769
|
}
|
|
4539
4770
|
|
|
4540
|
-
// src/archive/index.ts
|
|
4541
|
-
import { access as access3, cp as cp3, mkdir as mkdir5, readFile as readFile13, readdir as readdir7, rm as rm2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
4542
|
-
import { dirname as dirname8, join as join16, sep } from "path";
|
|
4543
|
-
function toPosixPath(p) {
|
|
4544
|
-
return sep === "\\" ? p.replaceAll("\\", "/") : p;
|
|
4545
|
-
}
|
|
4546
|
-
var ARCHIVE_DIR = ".hatch3r-archive";
|
|
4547
|
-
var TOOL_PATH_PREFIXES = {
|
|
4548
|
-
cursor: [".cursor/"],
|
|
4549
|
-
claude: [".claude/", "CLAUDE.md", ".mcp.json"],
|
|
4550
|
-
copilot: [".github/copilot-instructions.md", ".github/workflows/copilot-setup-steps.yml", ".vscode/mcp.json"],
|
|
4551
|
-
windsurf: [".windsurf/", ".windsurfrules"],
|
|
4552
|
-
amp: [".amp/"],
|
|
4553
|
-
codex: [".codex/"],
|
|
4554
|
-
gemini: [".gemini/", "GEMINI.md"],
|
|
4555
|
-
cline: [".roo/", ".roomodes"],
|
|
4556
|
-
aider: ["CONVENTIONS.md", ".aider.conf.yml"],
|
|
4557
|
-
kiro: [".kiro/"],
|
|
4558
|
-
opencode: ["opencode.json"],
|
|
4559
|
-
goose: [".goosehints"],
|
|
4560
|
-
zed: [".rules"],
|
|
4561
|
-
"amazon-q": [".amazonq/"],
|
|
4562
|
-
antigravity: [".antigravity/"]
|
|
4563
|
-
};
|
|
4564
|
-
var PATH_PATTERNS = [
|
|
4565
|
-
{ pattern: /\/rules\/([^/]+)\.(mdc|md)$/, type: "rules" },
|
|
4566
|
-
{ pattern: /\/agents\/([^/]+)\.md$/, type: "agents" },
|
|
4567
|
-
{ pattern: /\/skills\/([^/]+)\/SKILL\.md$/, type: "skills" },
|
|
4568
|
-
{ pattern: /\/commands\/([^/]+)\.md$/, type: "commands" }
|
|
4569
|
-
];
|
|
4570
|
-
function parseOutputPath(filePath) {
|
|
4571
|
-
for (const { pattern, type } of PATH_PATTERNS) {
|
|
4572
|
-
const match = filePath.match(pattern);
|
|
4573
|
-
if (match) {
|
|
4574
|
-
let id = match[1];
|
|
4575
|
-
if (id.startsWith(HATCH3R_PREFIX)) {
|
|
4576
|
-
id = id.slice(HATCH3R_PREFIX.length);
|
|
4577
|
-
}
|
|
4578
|
-
id = sanitizeId(id);
|
|
4579
|
-
if (id.length > 0) return { type, id };
|
|
4580
|
-
}
|
|
4581
|
-
}
|
|
4582
|
-
return null;
|
|
4583
|
-
}
|
|
4584
|
-
function stripFrontmatter(content) {
|
|
4585
|
-
const trimmed = content.trimStart();
|
|
4586
|
-
if (trimmed.startsWith("---")) {
|
|
4587
|
-
const endIdx = trimmed.indexOf("\n---", 3);
|
|
4588
|
-
if (endIdx !== -1) {
|
|
4589
|
-
return trimmed.slice(endIdx + 4).trim();
|
|
4590
|
-
}
|
|
4591
|
-
}
|
|
4592
|
-
return content.trim();
|
|
4593
|
-
}
|
|
4594
|
-
async function fileExists2(path) {
|
|
4595
|
-
try {
|
|
4596
|
-
await access3(path);
|
|
4597
|
-
return true;
|
|
4598
|
-
} catch {
|
|
4599
|
-
return false;
|
|
4600
|
-
}
|
|
4601
|
-
}
|
|
4602
|
-
async function collectToolFiles(rootDir, tool) {
|
|
4603
|
-
const prefixes = TOOL_PATH_PREFIXES[tool];
|
|
4604
|
-
if (!prefixes) return [];
|
|
4605
|
-
const files = [];
|
|
4606
|
-
for (const prefix of prefixes) {
|
|
4607
|
-
const absPath = join16(rootDir, prefix);
|
|
4608
|
-
if (prefix.endsWith("/")) {
|
|
4609
|
-
try {
|
|
4610
|
-
const entries = await readdir7(absPath, { recursive: true, withFileTypes: true });
|
|
4611
|
-
for (const entry of entries) {
|
|
4612
|
-
if (entry.isFile()) {
|
|
4613
|
-
const parent = entry.parentPath ?? entry.path ?? absPath;
|
|
4614
|
-
const relPath = toPosixPath(join16(prefix, parent.slice(absPath.length), entry.name));
|
|
4615
|
-
files.push(relPath);
|
|
4616
|
-
}
|
|
4617
|
-
}
|
|
4618
|
-
} catch {
|
|
4619
|
-
}
|
|
4620
|
-
} else if (await fileExists2(absPath)) {
|
|
4621
|
-
files.push(prefix);
|
|
4622
|
-
}
|
|
4623
|
-
}
|
|
4624
|
-
return files;
|
|
4625
|
-
}
|
|
4626
|
-
async function archiveToolOutputs(rootDir, tool) {
|
|
4627
|
-
const filesToArchive = await collectToolFiles(rootDir, tool);
|
|
4628
|
-
if (filesToArchive.length === 0) {
|
|
4629
|
-
return { archivedFiles: [], migrations: [] };
|
|
4630
|
-
}
|
|
4631
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4632
|
-
const archiveBase = join16(rootDir, ARCHIVE_DIR, tool, timestamp);
|
|
4633
|
-
const archivedFiles = [];
|
|
4634
|
-
const migrations = [];
|
|
4635
|
-
for (const relPath of filesToArchive) {
|
|
4636
|
-
const absPath = join16(rootDir, relPath);
|
|
4637
|
-
if (!await fileExists2(absPath)) continue;
|
|
4638
|
-
let content;
|
|
4639
|
-
try {
|
|
4640
|
-
content = await readFile13(absPath, "utf-8");
|
|
4641
|
-
} catch {
|
|
4642
|
-
continue;
|
|
4643
|
-
}
|
|
4644
|
-
if (hasManagedBlock(content)) {
|
|
4645
|
-
const customContent = stripFrontmatter(extractCustomContent(content));
|
|
4646
|
-
if (customContent.length > 0) {
|
|
4647
|
-
const parsed = parseOutputPath(relPath);
|
|
4648
|
-
if (parsed) {
|
|
4649
|
-
const customizePath = join16(rootDir, ".hatch3r", parsed.type, `${parsed.id}.customize.md`);
|
|
4650
|
-
if (!await fileExists2(customizePath)) {
|
|
4651
|
-
await mkdir5(dirname8(customizePath), { recursive: true });
|
|
4652
|
-
await writeFile3(customizePath, customContent + "\n", "utf-8");
|
|
4653
|
-
migrations.push({
|
|
4654
|
-
from: relPath,
|
|
4655
|
-
to: `.hatch3r/${parsed.type}/${parsed.id}.customize.md`,
|
|
4656
|
-
type: parsed.type,
|
|
4657
|
-
id: parsed.id
|
|
4658
|
-
});
|
|
4659
|
-
}
|
|
4660
|
-
}
|
|
4661
|
-
}
|
|
4662
|
-
}
|
|
4663
|
-
const archiveDest = join16(archiveBase, relPath);
|
|
4664
|
-
await mkdir5(dirname8(archiveDest), { recursive: true });
|
|
4665
|
-
await cp3(absPath, archiveDest);
|
|
4666
|
-
const srcStat = await stat2(absPath);
|
|
4667
|
-
const destStat = await stat2(archiveDest);
|
|
4668
|
-
if (destStat.size !== srcStat.size) {
|
|
4669
|
-
throw new Error(`Archive copy size mismatch for ${relPath}: source=${srcStat.size}, dest=${destStat.size}`);
|
|
4670
|
-
}
|
|
4671
|
-
await rm2(absPath);
|
|
4672
|
-
archivedFiles.push(relPath);
|
|
4673
|
-
}
|
|
4674
|
-
await cleanEmptyDirs(rootDir, filesToArchive);
|
|
4675
|
-
return { archivedFiles, migrations };
|
|
4676
|
-
}
|
|
4677
|
-
async function cleanEmptyDirs(rootDir, paths) {
|
|
4678
|
-
const dirs = /* @__PURE__ */ new Set();
|
|
4679
|
-
for (const p of paths) {
|
|
4680
|
-
let dir = dirname8(join16(rootDir, p));
|
|
4681
|
-
while (dir !== rootDir && dir.length > rootDir.length) {
|
|
4682
|
-
dirs.add(dir);
|
|
4683
|
-
dir = dirname8(dir);
|
|
4684
|
-
}
|
|
4685
|
-
}
|
|
4686
|
-
const sorted = [...dirs].sort((a, b) => b.length - a.length);
|
|
4687
|
-
for (const dir of sorted) {
|
|
4688
|
-
try {
|
|
4689
|
-
const entries = await readdir7(dir);
|
|
4690
|
-
if (entries.length === 0) {
|
|
4691
|
-
await rm2(dir, { recursive: true });
|
|
4692
|
-
}
|
|
4693
|
-
} catch {
|
|
4694
|
-
}
|
|
4695
|
-
}
|
|
4696
|
-
}
|
|
4697
|
-
function removeManagedFilesForPaths(manifest, paths) {
|
|
4698
|
-
const pathSet = new Set(paths);
|
|
4699
|
-
manifest.managedFiles = manifest.managedFiles.filter((f) => !pathSet.has(f));
|
|
4700
|
-
}
|
|
4701
|
-
|
|
4702
4771
|
// src/workspace/manifest.ts
|
|
4703
4772
|
import { readFile as readFile14 } from "fs/promises";
|
|
4704
|
-
import { join as join17 } from "path";
|
|
4773
|
+
import { join as join17, normalize as normalize2, isAbsolute as isAbsolute2 } from "path";
|
|
4705
4774
|
|
|
4706
4775
|
// src/workspace/types.ts
|
|
4707
4776
|
var WORKSPACE_MANIFEST_FILE = "workspace.json";
|
|
4708
4777
|
var WORKSPACE_MANIFEST_VERSION = "1.0.0";
|
|
4709
4778
|
|
|
4710
4779
|
// src/workspace/manifest.ts
|
|
4780
|
+
function isUnsafeRepoPath(repoPath) {
|
|
4781
|
+
if (repoPath.includes("\0")) return true;
|
|
4782
|
+
if (isAbsolute2(repoPath)) return true;
|
|
4783
|
+
const normalized = normalize2(repoPath);
|
|
4784
|
+
if (normalized.startsWith("..")) return true;
|
|
4785
|
+
return false;
|
|
4786
|
+
}
|
|
4711
4787
|
function validateWorkspaceManifest(data) {
|
|
4712
4788
|
if (!data || typeof data !== "object") return false;
|
|
4713
4789
|
const obj = data;
|
|
@@ -4736,6 +4812,7 @@ function validateWorkspaceManifest(data) {
|
|
|
4736
4812
|
if (!repo || typeof repo !== "object") return false;
|
|
4737
4813
|
const r = repo;
|
|
4738
4814
|
if (typeof r.path !== "string") return false;
|
|
4815
|
+
if (isUnsafeRepoPath(r.path)) return false;
|
|
4739
4816
|
if (typeof r.sync !== "boolean") return false;
|
|
4740
4817
|
if (r.owner !== void 0 && typeof r.owner !== "string") return false;
|
|
4741
4818
|
if (r.repo !== void 0 && typeof r.repo !== "string") return false;
|
|
@@ -4761,13 +4838,15 @@ async function readWorkspaceManifest(rootDir) {
|
|
|
4761
4838
|
} catch (err) {
|
|
4762
4839
|
throw new HatchError(
|
|
4763
4840
|
`Malformed JSON in ${manifestPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
4764
|
-
1
|
|
4841
|
+
1,
|
|
4842
|
+
"CONFIG_ERROR"
|
|
4765
4843
|
);
|
|
4766
4844
|
}
|
|
4767
4845
|
if (!validateWorkspaceManifest(parsed)) {
|
|
4768
4846
|
throw new HatchError(
|
|
4769
4847
|
`Invalid workspace manifest in ${manifestPath}: required fields missing or malformed.`,
|
|
4770
|
-
1
|
|
4848
|
+
1,
|
|
4849
|
+
"VALIDATION_ERROR"
|
|
4771
4850
|
);
|
|
4772
4851
|
}
|
|
4773
4852
|
return parsed;
|
|
@@ -4849,17 +4928,19 @@ import { dirname as dirname10 } from "path";
|
|
|
4849
4928
|
import { access as access5, readFile as readFile15, readdir as readdir9 } from "fs/promises";
|
|
4850
4929
|
import { join as join19 } from "path";
|
|
4851
4930
|
async function analyzeRepo(rootDir) {
|
|
4852
|
-
const [languages, pm, isMonorepo, hasExistingAgents, existingTools] = await Promise.all([
|
|
4931
|
+
const [languages, pm, isMonorepo, hasExistingAgents, existingTools, frameworks] = await Promise.all([
|
|
4853
4932
|
detectLanguages(rootDir),
|
|
4854
4933
|
detectPackageManager(rootDir),
|
|
4855
4934
|
detectMonorepo(rootDir),
|
|
4856
4935
|
detectExistingAgents(rootDir),
|
|
4857
|
-
detectExistingTools(rootDir)
|
|
4936
|
+
detectExistingTools(rootDir),
|
|
4937
|
+
detectFrameworks(rootDir)
|
|
4858
4938
|
]);
|
|
4859
4939
|
const packageManager = pm.name;
|
|
4860
4940
|
return {
|
|
4861
4941
|
languages,
|
|
4862
4942
|
packageManager,
|
|
4943
|
+
frameworks,
|
|
4863
4944
|
isMonorepo,
|
|
4864
4945
|
hasExistingAgents,
|
|
4865
4946
|
existingTools,
|
|
@@ -4951,6 +5032,71 @@ async function detectExistingTools(rootDir) {
|
|
|
4951
5032
|
(r) => r.status === "fulfilled" && r.value !== null
|
|
4952
5033
|
).map((r) => r.value);
|
|
4953
5034
|
}
|
|
5035
|
+
var FRAMEWORK_CONFIG_INDICATORS = [
|
|
5036
|
+
{ framework: "next", configs: ["next.config.js", "next.config.mjs", "next.config.ts"] },
|
|
5037
|
+
{ framework: "angular", configs: ["angular.json"] },
|
|
5038
|
+
{ framework: "svelte", configs: ["svelte.config.js", "svelte.config.ts"] },
|
|
5039
|
+
{ framework: "nuxt", configs: ["nuxt.config.js", "nuxt.config.ts"] },
|
|
5040
|
+
{ framework: "astro", configs: ["astro.config.mjs", "astro.config.ts"] }
|
|
5041
|
+
];
|
|
5042
|
+
var FRAMEWORK_DEP_INDICATORS = [
|
|
5043
|
+
{ framework: "next", deps: ["next"] },
|
|
5044
|
+
{ framework: "angular", deps: ["@angular/core"] },
|
|
5045
|
+
{ framework: "sveltekit", deps: ["@sveltejs/kit"] },
|
|
5046
|
+
{ framework: "svelte", deps: ["svelte"] },
|
|
5047
|
+
{ framework: "nuxt", deps: ["nuxt"] },
|
|
5048
|
+
{ framework: "remix", deps: ["@remix-run/react"] },
|
|
5049
|
+
{ framework: "astro", deps: ["astro"] },
|
|
5050
|
+
{ framework: "vue", deps: ["vue"] },
|
|
5051
|
+
{ framework: "react", deps: ["react"] },
|
|
5052
|
+
{ framework: "express", deps: ["express"] },
|
|
5053
|
+
{ framework: "fastify", deps: ["fastify"] },
|
|
5054
|
+
{ framework: "hono", deps: ["hono"] }
|
|
5055
|
+
];
|
|
5056
|
+
var FRAMEWORK_SUPPRESSION = {
|
|
5057
|
+
next: "react",
|
|
5058
|
+
remix: "react",
|
|
5059
|
+
nuxt: "vue",
|
|
5060
|
+
sveltekit: "svelte"
|
|
5061
|
+
};
|
|
5062
|
+
async function detectFrameworks(rootDir) {
|
|
5063
|
+
const detected = /* @__PURE__ */ new Set();
|
|
5064
|
+
const configResults = await Promise.allSettled(
|
|
5065
|
+
FRAMEWORK_CONFIG_INDICATORS.map(async ({ framework, configs }) => {
|
|
5066
|
+
for (const cfg of configs) {
|
|
5067
|
+
if (await pathExists(join19(rootDir, cfg))) return framework;
|
|
5068
|
+
}
|
|
5069
|
+
return null;
|
|
5070
|
+
})
|
|
5071
|
+
);
|
|
5072
|
+
for (const r of configResults) {
|
|
5073
|
+
if (r.status === "fulfilled" && r.value !== null) {
|
|
5074
|
+
detected.add(r.value);
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
try {
|
|
5078
|
+
const raw = await readFile15(join19(rootDir, "package.json"), "utf-8");
|
|
5079
|
+
const pkg = JSON.parse(raw);
|
|
5080
|
+
const allDeps = {
|
|
5081
|
+
...pkg.dependencies,
|
|
5082
|
+
...pkg.devDependencies
|
|
5083
|
+
};
|
|
5084
|
+
for (const { framework, deps } of FRAMEWORK_DEP_INDICATORS) {
|
|
5085
|
+
if (deps.some((d) => d in allDeps)) {
|
|
5086
|
+
detected.add(framework);
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
} catch (err) {
|
|
5090
|
+
const isExpected = err.code === "ENOENT" || err instanceof SyntaxError;
|
|
5091
|
+
if (!isExpected) throw err;
|
|
5092
|
+
}
|
|
5093
|
+
for (const [meta, base] of Object.entries(FRAMEWORK_SUPPRESSION)) {
|
|
5094
|
+
if (detected.has(meta)) {
|
|
5095
|
+
detected.delete(base);
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
return [...detected];
|
|
5099
|
+
}
|
|
4954
5100
|
async function pathExists(path) {
|
|
4955
5101
|
try {
|
|
4956
5102
|
await access5(path);
|
|
@@ -5082,19 +5228,20 @@ var CHARS_PER_TOKEN = 4;
|
|
|
5082
5228
|
async function estimateTokensForContent(contentIds, index) {
|
|
5083
5229
|
let totalChars = 0;
|
|
5084
5230
|
for (const id of contentIds) {
|
|
5085
|
-
const
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5231
|
+
const items = getAllItemsById(index, id);
|
|
5232
|
+
for (const item of items) {
|
|
5233
|
+
try {
|
|
5234
|
+
if (item.type === "skill") {
|
|
5235
|
+
const skillPath = join20(CONTENT_ROOT, item.relativePath, "SKILL.md");
|
|
5236
|
+
const content = await readFile16(skillPath, "utf-8");
|
|
5237
|
+
totalChars += content.length;
|
|
5238
|
+
} else {
|
|
5239
|
+
const filePath = join20(CONTENT_ROOT, item.relativePath);
|
|
5240
|
+
const content = await readFile16(filePath, "utf-8");
|
|
5241
|
+
totalChars += content.length;
|
|
5242
|
+
}
|
|
5243
|
+
} catch {
|
|
5096
5244
|
}
|
|
5097
|
-
} catch {
|
|
5098
5245
|
}
|
|
5099
5246
|
}
|
|
5100
5247
|
return Math.ceil(totalChars / CHARS_PER_TOKEN);
|
|
@@ -5184,8 +5331,8 @@ async function syncSingleRepo(workspaceRoot, wsManifest, wsChecksum, repoEntry,
|
|
|
5184
5331
|
await mkdir6(repoAgentsDir, { recursive: true });
|
|
5185
5332
|
await copySelectedContent(CONTENT_ROOT, repoAgentsDir, effectiveSelection, index);
|
|
5186
5333
|
for (const id of toRemove) {
|
|
5187
|
-
const
|
|
5188
|
-
|
|
5334
|
+
const items = getAllItemsById(index, id);
|
|
5335
|
+
for (const item of items) {
|
|
5189
5336
|
await removeContentItem(repoAgentsDir, item, { rootDir: repoDir });
|
|
5190
5337
|
}
|
|
5191
5338
|
}
|
|
@@ -5425,7 +5572,7 @@ async function configCommand() {
|
|
|
5425
5572
|
if (!manifest) {
|
|
5426
5573
|
error("No .agents/hatch.json found.");
|
|
5427
5574
|
console.log(chalk5.dim(" Run `npx hatch3r init` to set up your project first.\n"));
|
|
5428
|
-
throw new HatchError("No .agents/hatch.json found.", 1);
|
|
5575
|
+
throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
|
|
5429
5576
|
}
|
|
5430
5577
|
if (manifest.workspace) {
|
|
5431
5578
|
warn(
|
|
@@ -5505,7 +5652,7 @@ async function configCommand() {
|
|
|
5505
5652
|
const tools = toolAnswers.tools;
|
|
5506
5653
|
if (tools.length === 0) {
|
|
5507
5654
|
error("At least one tool must be selected.");
|
|
5508
|
-
throw new HatchError("At least one tool must be selected.", 1);
|
|
5655
|
+
throw new HatchError("At least one tool must be selected.", 1, "VALIDATION_ERROR");
|
|
5509
5656
|
}
|
|
5510
5657
|
const currentFeatureKeys = Object.keys(DEFAULT_FEATURES).filter((k) => manifest.features[k]);
|
|
5511
5658
|
const featureAnswers = await inquirer2.prompt([
|
|
@@ -6129,7 +6276,7 @@ async function runInit(options) {
|
|
|
6129
6276
|
}
|
|
6130
6277
|
if (adapterFailures.length === tools.length) {
|
|
6131
6278
|
s3.fail(step(3, totalSteps, "All adapters failed"));
|
|
6132
|
-
throw new HatchError("All adapters failed", 1);
|
|
6279
|
+
throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
|
|
6133
6280
|
}
|
|
6134
6281
|
}
|
|
6135
6282
|
s3.succeed(step(3, totalSteps, adapterFailures.length > 0 ? `Adapter output generated (${adapterFailures.length} failed)` : "Adapter output generated"));
|
|
@@ -6249,7 +6396,7 @@ function validateFlag(value, valid, fallback, name) {
|
|
|
6249
6396
|
if (!value) return fallback;
|
|
6250
6397
|
if (!valid.includes(value)) {
|
|
6251
6398
|
error(`Invalid --${name}: "${value}". Valid: ${valid.join(", ")}`);
|
|
6252
|
-
throw new HatchError(`Invalid --${name}: "${value}"`, 1);
|
|
6399
|
+
throw new HatchError(`Invalid --${name}: "${value}"`, 1, "VALIDATION_ERROR");
|
|
6253
6400
|
}
|
|
6254
6401
|
return value;
|
|
6255
6402
|
}
|
|
@@ -6313,7 +6460,7 @@ async function initCommand(opts = {}) {
|
|
|
6313
6460
|
if (invalid.length > 0) {
|
|
6314
6461
|
error(`Invalid tool(s): ${invalid.join(", ")}`);
|
|
6315
6462
|
console.log(chalk6.dim(` Valid tools: ${[...VALID_TOOLS].join(", ")}`));
|
|
6316
|
-
throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1);
|
|
6463
|
+
throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1, "VALIDATION_ERROR");
|
|
6317
6464
|
}
|
|
6318
6465
|
tools2 = rawTools;
|
|
6319
6466
|
} else if (repoInfo.existingTools.length > 0) {
|
|
@@ -6958,7 +7105,7 @@ async function syncCommand(opts = {}) {
|
|
|
6958
7105
|
if (!manifest) {
|
|
6959
7106
|
error("No .agents/hatch.json found.");
|
|
6960
7107
|
console.log(chalk7.dim(" Run `npx hatch3r init` to set up your project first.\n"));
|
|
6961
|
-
throw new HatchError("No .agents/hatch.json found.", 1);
|
|
7108
|
+
throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
|
|
6962
7109
|
}
|
|
6963
7110
|
const m = manifest;
|
|
6964
7111
|
const integrityResults = await verifyIntegrity(agentsDir);
|
|
@@ -7032,7 +7179,7 @@ async function syncCommand(opts = {}) {
|
|
|
7032
7179
|
error(`Failed to generate ${f.tool}: ${f.error}`);
|
|
7033
7180
|
}
|
|
7034
7181
|
if (adapterFailures.length === m.tools.length) {
|
|
7035
|
-
throw new HatchError("All adapters failed", 1);
|
|
7182
|
+
throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
|
|
7036
7183
|
}
|
|
7037
7184
|
}
|
|
7038
7185
|
for (const tool of m.tools) {
|
|
@@ -7065,6 +7212,9 @@ async function syncCommand(opts = {}) {
|
|
|
7065
7212
|
info(`Run this, then start or restart your editor: ${getSourceEnvMcpCommand()}`);
|
|
7066
7213
|
}
|
|
7067
7214
|
}
|
|
7215
|
+
const integrityManifest = await generateIntegrityManifest(agentsDir, HATCH3R_VERSION);
|
|
7216
|
+
await writeIntegrityManifest(agentsDir, integrityManifest);
|
|
7217
|
+
await pruneArchives(rootDir);
|
|
7068
7218
|
await checkSpecFreshness(rootDir);
|
|
7069
7219
|
console.log();
|
|
7070
7220
|
const icons = {
|
|
@@ -7503,7 +7653,7 @@ async function validateCommand() {
|
|
|
7503
7653
|
spinner.fail("Validation failed");
|
|
7504
7654
|
error(".agents/ directory not found. Run `hatch3r init` first.");
|
|
7505
7655
|
console.log();
|
|
7506
|
-
throw new HatchError(".agents/ directory not found.", 1);
|
|
7656
|
+
throw new HatchError(".agents/ directory not found.", 1, "CONFIG_ERROR");
|
|
7507
7657
|
}
|
|
7508
7658
|
const manifest = await readManifest(rootDir);
|
|
7509
7659
|
await validateManifest2(rootDir, manifest, result);
|
|
@@ -7525,6 +7675,18 @@ async function validateCommand() {
|
|
|
7525
7675
|
result.warnings.push(w);
|
|
7526
7676
|
}
|
|
7527
7677
|
}
|
|
7678
|
+
const EXPECTED_CROSS_TYPE_PAIRS = /* @__PURE__ */ new Set(["command", "skill"]);
|
|
7679
|
+
for (const collision of index.collisions) {
|
|
7680
|
+
if (collision.kind === "cross-type") {
|
|
7681
|
+
const types = /* @__PURE__ */ new Set([collision.existingType, collision.duplicateType]);
|
|
7682
|
+
if (types.size === 2 && [...types].every((t) => EXPECTED_CROSS_TYPE_PAIRS.has(t))) {
|
|
7683
|
+
continue;
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
result.warnings.push(
|
|
7687
|
+
`Content ID collision: "${collision.id}" exists as ${collision.existingType} (${collision.existingPath}) and ${collision.duplicateType} (${collision.duplicatePath})`
|
|
7688
|
+
);
|
|
7689
|
+
}
|
|
7528
7690
|
} catch {
|
|
7529
7691
|
}
|
|
7530
7692
|
if (manifest.content) {
|
|
@@ -7572,7 +7734,7 @@ async function validateCommand() {
|
|
|
7572
7734
|
`${chalk8.yellow("\u26A0")} ${result.warnings.length} warning(s)`
|
|
7573
7735
|
];
|
|
7574
7736
|
printBox("Validation failed", summaryLines, "error");
|
|
7575
|
-
throw new HatchError("Validation failed", 1);
|
|
7737
|
+
throw new HatchError("Validation failed", 1, "VALIDATION_ERROR");
|
|
7576
7738
|
} else {
|
|
7577
7739
|
const summaryLines = [
|
|
7578
7740
|
`${chalk8.green("\u2714")} 0 errors`,
|
|
@@ -7610,7 +7772,7 @@ async function verifyCommand() {
|
|
|
7610
7772
|
spinner.fail("No integrity manifest found");
|
|
7611
7773
|
error("Missing .agents/.integrity.json \u2014 run `hatch3r init` or `hatch3r update` to generate it.");
|
|
7612
7774
|
console.log();
|
|
7613
|
-
throw new HatchError("Missing .agents/.integrity.json", 1);
|
|
7775
|
+
throw new HatchError("Missing .agents/.integrity.json", 1, "INTEGRITY_ERROR");
|
|
7614
7776
|
}
|
|
7615
7777
|
const results = await verifyIntegrity(agentsDir);
|
|
7616
7778
|
spinner.stop();
|
|
@@ -7659,7 +7821,7 @@ async function verifyCommand() {
|
|
|
7659
7821
|
info(`Modified files may have been tampered with. Run ${chalk9.bold("hatch3r update")} to restore originals.`);
|
|
7660
7822
|
}
|
|
7661
7823
|
console.log();
|
|
7662
|
-
throw new HatchError("Integrity check failed", 1);
|
|
7824
|
+
throw new HatchError("Integrity check failed", 1, "INTEGRITY_ERROR");
|
|
7663
7825
|
} else {
|
|
7664
7826
|
printBox("Integrity check passed", summaryLines, "success");
|
|
7665
7827
|
}
|
|
@@ -7696,7 +7858,7 @@ async function statusCommand() {
|
|
|
7696
7858
|
if (!manifest) {
|
|
7697
7859
|
error("No .agents/hatch.json found.");
|
|
7698
7860
|
console.log(chalk10.dim(" Run `npx hatch3r init` to set up your project first.\n"));
|
|
7699
|
-
throw new HatchError("No .agents/hatch.json found.", 1);
|
|
7861
|
+
throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
|
|
7700
7862
|
}
|
|
7701
7863
|
const spinner = createSpinner("Checking sync status...");
|
|
7702
7864
|
spinner.start();
|
|
@@ -7794,7 +7956,7 @@ program.command("init").description("Install a complete agent setup into the cur
|
|
|
7794
7956
|
).option("--yes", "Skip interactive prompts, use defaults").option("--preset <preset>", "Content preset: minimal, standard, full").option("--project-type <type>", "Project type: greenfield, brownfield").option("--team-size <size>", "Team size: solo, team").option("--workspace", "Initialize as a multi-repo workspace").action(initCommand);
|
|
7795
7957
|
program.command("sync").description("Re-generate tool outputs from canonical .agents/ state").option("--repos [paths...]", "Sync workspace content to sub-repos (all opted-in if no paths given)").option("--dry-run", "Show what would change without modifying files").option("--force", "Overwrite locally modified files in sub-repos").option("--minimal", "Generate stripped-down output (no comments, minimal formatting) to reduce token usage").action(syncCommand);
|
|
7796
7958
|
program.command("status").description("Check sync status between canonical .agents/ and generated files").action(statusCommand);
|
|
7797
|
-
program.command("update").description("Pull latest hatch3r templates with safe merge").action(updateCommand);
|
|
7959
|
+
program.command("update").description("Pull latest hatch3r templates with safe merge").option("--yes", "Skip interactive prompts, use defaults").action(updateCommand);
|
|
7798
7960
|
program.command("validate").description("Validate the canonical .agents/ structure").action(validateCommand);
|
|
7799
7961
|
program.command("verify").description("Verify integrity of canonical agent files").action(verifyCommand);
|
|
7800
7962
|
program.command("config").description("Reconfigure tools, MCP servers, features, and platform").action(configCommand);
|