hunter-harness 0.1.1 → 0.1.3
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/dist/bin.js
CHANGED
|
@@ -414,7 +414,7 @@ function normalize(value) {
|
|
|
414
414
|
return value.map(normalize);
|
|
415
415
|
}
|
|
416
416
|
if (value !== null && typeof value === "object") {
|
|
417
|
-
return Object.fromEntries(Object.entries(value).filter(([,
|
|
417
|
+
return Object.fromEntries(Object.entries(value).filter(([, item2]) => item2 !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item2]) => [key, normalize(item2)]));
|
|
418
418
|
}
|
|
419
419
|
return value;
|
|
420
420
|
}
|
|
@@ -789,8 +789,8 @@ function normalizeManagedPath(input) {
|
|
|
789
789
|
}
|
|
790
790
|
function assertNoCaseCollisions(paths) {
|
|
791
791
|
const seen = /* @__PURE__ */ new Map();
|
|
792
|
-
for (const
|
|
793
|
-
const normalized = normalizeManagedPath(
|
|
792
|
+
for (const item2 of paths) {
|
|
793
|
+
const normalized = normalizeManagedPath(item2);
|
|
794
794
|
const folded = normalized.toLocaleLowerCase("en-US");
|
|
795
795
|
const existing = seen.get(folded);
|
|
796
796
|
if (existing !== void 0 && existing !== normalized) {
|
|
@@ -817,8 +817,8 @@ async function assertNoSymlinks(root, relativePath) {
|
|
|
817
817
|
for (const segment of normalized.split("/")) {
|
|
818
818
|
current = join(current, segment);
|
|
819
819
|
try {
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
820
|
+
const stat6 = await lstat(current);
|
|
821
|
+
if (stat6.isSymbolicLink()) {
|
|
822
822
|
throw new UnsafePathError("symbolic links are not managed");
|
|
823
823
|
}
|
|
824
824
|
} catch (error) {
|
|
@@ -834,7 +834,7 @@ async function assertNoSymlinks(root, relativePath) {
|
|
|
834
834
|
async function withRetry(action, options) {
|
|
835
835
|
const attempts = options.attempts ?? 3;
|
|
836
836
|
const sleep = options.sleep ?? (async (milliseconds) => {
|
|
837
|
-
await new Promise((
|
|
837
|
+
await new Promise((resolve7) => setTimeout(resolve7, milliseconds));
|
|
838
838
|
});
|
|
839
839
|
let lastError;
|
|
840
840
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
@@ -1094,6 +1094,17 @@ function removeManagedBlock(original) {
|
|
|
1094
1094
|
const after = original.slice(end).replace(/^(?:\r?\n){1,2}/, "");
|
|
1095
1095
|
return before + after;
|
|
1096
1096
|
}
|
|
1097
|
+
function refreshManagedBlock(original, blockContent) {
|
|
1098
|
+
const starts = markerCount(original, MANAGED_BLOCK_START);
|
|
1099
|
+
const ends = markerCount(original, MANAGED_BLOCK_END);
|
|
1100
|
+
const absent = starts === 0 && ends === 0;
|
|
1101
|
+
const malformed = !absent && (starts !== 1 || ends !== 1 || original.indexOf(MANAGED_BLOCK_START) > original.indexOf(MANAGED_BLOCK_END));
|
|
1102
|
+
if (malformed) {
|
|
1103
|
+
return { content: original, action: "preserved_conflict", conflict: true };
|
|
1104
|
+
}
|
|
1105
|
+
const action = absent ? "appended" : "refreshed";
|
|
1106
|
+
return { content: upsertManagedBlock(original, blockContent), action, conflict: false };
|
|
1107
|
+
}
|
|
1097
1108
|
function escapeRe(value) {
|
|
1098
1109
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1099
1110
|
}
|
|
@@ -1250,7 +1261,7 @@ import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
|
1250
1261
|
|
|
1251
1262
|
// ../core/dist/transaction/transaction.js
|
|
1252
1263
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1253
|
-
import { copyFile, mkdir as mkdir3, readFile as readFile2, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
1264
|
+
import { copyFile, mkdir as mkdir3, readFile as readFile2, readdir, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
1254
1265
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
1255
1266
|
|
|
1256
1267
|
// ../core/dist/state/layout.js
|
|
@@ -1276,9 +1287,7 @@ async function ensureStateLayout(projectRoot) {
|
|
|
1276
1287
|
mkdir2(layout.baseline, { recursive: true }),
|
|
1277
1288
|
mkdir2(layout.transactions, { recursive: true }),
|
|
1278
1289
|
mkdir2(layout.locks, { recursive: true }),
|
|
1279
|
-
mkdir2(layout.local, { recursive: true })
|
|
1280
|
-
mkdir2(layout.serverArtifacts, { recursive: true }),
|
|
1281
|
-
mkdir2(layout.reports, { recursive: true })
|
|
1290
|
+
mkdir2(layout.local, { recursive: true })
|
|
1282
1291
|
]);
|
|
1283
1292
|
return layout;
|
|
1284
1293
|
}
|
|
@@ -1321,6 +1330,31 @@ async function writeJournal(transactionRoot, journal) {
|
|
|
1321
1330
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1322
1331
|
});
|
|
1323
1332
|
}
|
|
1333
|
+
async function pruneOlderSuccessful(layout, currentId, kind) {
|
|
1334
|
+
if (kind === void 0)
|
|
1335
|
+
return;
|
|
1336
|
+
let entries;
|
|
1337
|
+
try {
|
|
1338
|
+
entries = await readdir(layout.transactions);
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
1341
|
+
return;
|
|
1342
|
+
throw error;
|
|
1343
|
+
}
|
|
1344
|
+
for (const name of entries) {
|
|
1345
|
+
if (name === currentId)
|
|
1346
|
+
continue;
|
|
1347
|
+
const txRoot = join3(layout.transactions, name);
|
|
1348
|
+
try {
|
|
1349
|
+
const journal = JSON.parse(await readFile2(join3(txRoot, "journal.json"), "utf8"));
|
|
1350
|
+
if (journal.state === "committed" && journal.kind === kind) {
|
|
1351
|
+
await rm2(txRoot, { recursive: true, force: true });
|
|
1352
|
+
}
|
|
1353
|
+
} catch {
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1324
1358
|
async function snapshotPaths(projectRoot, transactionRoot, paths) {
|
|
1325
1359
|
const snapshots = [];
|
|
1326
1360
|
for (const path of paths) {
|
|
@@ -1460,7 +1494,6 @@ async function runTransaction(projectRoot, rawOperations, options = {}) {
|
|
|
1460
1494
|
await atomicWriteJson(join3(transactionRoot, "after", "manifest.json"), after);
|
|
1461
1495
|
journal.state = "committed";
|
|
1462
1496
|
await writeJournal(transactionRoot, journal);
|
|
1463
|
-
return { transactionId, status: "committed" };
|
|
1464
1497
|
} catch (error) {
|
|
1465
1498
|
if (error instanceof InterruptedTransactionError) {
|
|
1466
1499
|
throw error;
|
|
@@ -1470,11 +1503,35 @@ async function runTransaction(projectRoot, rawOperations, options = {}) {
|
|
|
1470
1503
|
await rollbackTransaction(projectRoot, transactionId);
|
|
1471
1504
|
throw error;
|
|
1472
1505
|
}
|
|
1506
|
+
await rm2(join3(transactionRoot, "staged"), { recursive: true, force: true });
|
|
1507
|
+
await pruneOlderSuccessful(layout, transactionId, options.kind);
|
|
1508
|
+
return { transactionId, status: "committed" };
|
|
1473
1509
|
}
|
|
1474
1510
|
|
|
1511
|
+
// ../core/dist/project/managed-content.js
|
|
1512
|
+
var AGENTS_MANAGED_BLOCK_CONTENT = [
|
|
1513
|
+
"# Hunter Harness",
|
|
1514
|
+
"",
|
|
1515
|
+
"Use .harness/context-index.json to route rules, Knowledge, and codebase maps.",
|
|
1516
|
+
"Treat .claude/skills/harness-* as editable adapter working copies.",
|
|
1517
|
+
"Do not modify .harness/state or .harness/cache directly."
|
|
1518
|
+
].join("\n");
|
|
1519
|
+
var CLAUDE_MANAGED_BLOCK_CONTENT = [
|
|
1520
|
+
"@AGENTS.md",
|
|
1521
|
+
"",
|
|
1522
|
+
"# Hunter Harness",
|
|
1523
|
+
"",
|
|
1524
|
+
"- Rules: .claude/rules/",
|
|
1525
|
+
"- Skills: .claude/skills/harness-*/",
|
|
1526
|
+
"- Knowledge: .harness/knowledge/",
|
|
1527
|
+
"- Codebase map: .harness/codebase/map/"
|
|
1528
|
+
].join("\n");
|
|
1529
|
+
var HARNESS_GENERAL_RULES_CONTENT = "# Hunter Harness Rules\n\n- Report evidence honestly.\n- Do not execute destructive actions without confirmation.\n";
|
|
1530
|
+
var HARNESS_JAVA_RULES_CONTENT = "# Java Profile\n\n- Verify builds and tests with the project build tool.\n";
|
|
1531
|
+
|
|
1475
1532
|
// ../core/dist/project/profile-bundle.js
|
|
1476
1533
|
import { createHash as createHash2 } from "node:crypto";
|
|
1477
|
-
import { readFile as readFile3 } from "node:fs/promises";
|
|
1534
|
+
import { readFile as readFile3, readdir as readdir2 } from "node:fs/promises";
|
|
1478
1535
|
import { join as join4 } from "node:path";
|
|
1479
1536
|
function isProfile(value) {
|
|
1480
1537
|
return value === "general" || value === "java";
|
|
@@ -1490,16 +1547,16 @@ async function loadProfileBundle(resourcesRoot, profile) {
|
|
|
1490
1547
|
throw new Error(`invalid ${profile} Harness Bundle manifest`);
|
|
1491
1548
|
}
|
|
1492
1549
|
const files = /* @__PURE__ */ new Map();
|
|
1493
|
-
for (const
|
|
1494
|
-
validateRelativeBundlePath(
|
|
1495
|
-
if (typeof
|
|
1550
|
+
for (const item2 of raw.files) {
|
|
1551
|
+
validateRelativeBundlePath(item2.path);
|
|
1552
|
+
if (typeof item2.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(item2.sha256) || files.has(item2.path)) {
|
|
1496
1553
|
throw new Error(`invalid ${profile} Harness Bundle manifest entry`);
|
|
1497
1554
|
}
|
|
1498
|
-
const bytes = await readFile3(join4(resourcesRoot, "harness", profile,
|
|
1499
|
-
if (createHash2("sha256").update(bytes).digest("hex") !==
|
|
1500
|
-
throw new Error(`Harness Bundle hash mismatch: ${
|
|
1555
|
+
const bytes = await readFile3(join4(resourcesRoot, "harness", profile, item2.path));
|
|
1556
|
+
if (createHash2("sha256").update(bytes).digest("hex") !== item2.sha256) {
|
|
1557
|
+
throw new Error(`Harness Bundle hash mismatch: ${item2.path}`);
|
|
1501
1558
|
}
|
|
1502
|
-
files.set(
|
|
1559
|
+
files.set(item2.path, bytes);
|
|
1503
1560
|
}
|
|
1504
1561
|
return { manifest: raw, files };
|
|
1505
1562
|
}
|
|
@@ -1510,22 +1567,123 @@ async function managedBundleTargets(resourcesRoot, profile) {
|
|
|
1510
1567
|
targets.add(".claude/rules/harness-profile-java.md");
|
|
1511
1568
|
return targets;
|
|
1512
1569
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
const agent =
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1570
|
+
var AGENT_SOURCE_PATH = /^agents\/([^/]+\.md)$/;
|
|
1571
|
+
function projectBundle(bundle) {
|
|
1572
|
+
const records = [];
|
|
1573
|
+
for (const [sourcePath, bytes] of bundle.files) {
|
|
1574
|
+
validateRelativeBundlePath(sourcePath);
|
|
1575
|
+
const agent = AGENT_SOURCE_PATH.exec(sourcePath);
|
|
1576
|
+
const projectedTarget = agent?.[1] !== void 0 ? `.claude/agents/${agent[1]}` : `.claude/skills/${sourcePath}`;
|
|
1577
|
+
const manifestEntry = bundle.manifest.files.find((entry) => entry.path === sourcePath);
|
|
1578
|
+
if (manifestEntry === void 0) {
|
|
1579
|
+
throw new Error(`Harness Bundle missing manifest entry: ${sourcePath}`);
|
|
1522
1580
|
}
|
|
1581
|
+
records.push({
|
|
1582
|
+
source_path: sourcePath,
|
|
1583
|
+
target_path: normalizeManagedPath(projectedTarget),
|
|
1584
|
+
sha256: manifestEntry.sha256,
|
|
1585
|
+
bytes
|
|
1586
|
+
});
|
|
1523
1587
|
}
|
|
1524
|
-
|
|
1588
|
+
assertNoCaseCollisions(records.map((record) => record.target_path));
|
|
1589
|
+
return records.sort((left, right) => left.target_path.localeCompare(right.target_path));
|
|
1590
|
+
}
|
|
1591
|
+
function bundleTargetContents(bundle) {
|
|
1592
|
+
return new Map(projectBundle(bundle).map((record) => [record.target_path, record.bytes]));
|
|
1593
|
+
}
|
|
1594
|
+
function ruleTarget(sourcePath, targetPath, content) {
|
|
1595
|
+
return {
|
|
1596
|
+
source_path: sourcePath,
|
|
1597
|
+
target_path: targetPath,
|
|
1598
|
+
sha256: createHash2("sha256").update(content).digest("hex"),
|
|
1599
|
+
bytes: new TextEncoder().encode(content)
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
async function managedTargets(resourcesRoot, profile) {
|
|
1603
|
+
const bundle = await loadProfileBundle(resourcesRoot, profile);
|
|
1604
|
+
const records = projectBundle(bundle).map((record) => ({
|
|
1605
|
+
source_path: record.source_path,
|
|
1606
|
+
target_path: record.target_path,
|
|
1607
|
+
sha256: record.sha256,
|
|
1608
|
+
bytes: record.bytes
|
|
1609
|
+
}));
|
|
1610
|
+
records.push(ruleTarget("rules/harness-general.md", ".claude/rules/harness-general.md", HARNESS_GENERAL_RULES_CONTENT));
|
|
1611
|
+
if (profile === "java") {
|
|
1612
|
+
records.push(ruleTarget("rules/harness-profile-java.md", ".claude/rules/harness-profile-java.md", HARNESS_JAVA_RULES_CONTENT));
|
|
1613
|
+
}
|
|
1614
|
+
assertNoCaseCollisions(records.map((record) => record.target_path));
|
|
1615
|
+
return records.sort((left, right) => left.target_path.localeCompare(right.target_path));
|
|
1525
1616
|
}
|
|
1526
1617
|
function parseHarnessProfile(value) {
|
|
1527
1618
|
return isProfile(value) ? value : null;
|
|
1528
1619
|
}
|
|
1620
|
+
function parseMigrationManifest(raw) {
|
|
1621
|
+
if (raw === null || typeof raw !== "object") {
|
|
1622
|
+
throw new Error("invalid Harness migration manifest");
|
|
1623
|
+
}
|
|
1624
|
+
const record = raw;
|
|
1625
|
+
if (record.schema_version !== 1 || !isProfile(record.profile) || typeof record.bundle_version !== "string" || !/^sha256:[a-f0-9]{64}$/.test(String(record.bundle_manifest_hash)) || !Array.isArray(record.projection)) {
|
|
1626
|
+
throw new Error("invalid Harness migration manifest");
|
|
1627
|
+
}
|
|
1628
|
+
const projection = [];
|
|
1629
|
+
for (const entry of record.projection) {
|
|
1630
|
+
if (entry === null || typeof entry !== "object") {
|
|
1631
|
+
throw new Error("invalid Harness migration manifest entry");
|
|
1632
|
+
}
|
|
1633
|
+
const item2 = entry;
|
|
1634
|
+
validateRelativeBundlePath(item2.source_path);
|
|
1635
|
+
if (typeof item2.target_path !== "string" || typeof item2.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(item2.sha256)) {
|
|
1636
|
+
throw new Error("invalid Harness migration manifest entry");
|
|
1637
|
+
}
|
|
1638
|
+
const targetPath = normalizeManagedPath(item2.target_path);
|
|
1639
|
+
if (!targetPath.startsWith(".claude/")) {
|
|
1640
|
+
throw new Error("invalid Harness migration manifest target");
|
|
1641
|
+
}
|
|
1642
|
+
projection.push({
|
|
1643
|
+
source_path: item2.source_path,
|
|
1644
|
+
target_path: targetPath,
|
|
1645
|
+
sha256: item2.sha256
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
assertNoCaseCollisions(projection.map((item2) => item2.target_path));
|
|
1649
|
+
return {
|
|
1650
|
+
schema_version: 1,
|
|
1651
|
+
profile: record.profile,
|
|
1652
|
+
bundle_version: record.bundle_version,
|
|
1653
|
+
bundle_manifest_hash: record.bundle_manifest_hash,
|
|
1654
|
+
projection
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
async function loadMigrationManifests(resourcesRoot) {
|
|
1658
|
+
const migrationsRoot = join4(resourcesRoot, "harness", "migrations");
|
|
1659
|
+
let versionDirs;
|
|
1660
|
+
try {
|
|
1661
|
+
versionDirs = await readdir2(migrationsRoot);
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1664
|
+
return [];
|
|
1665
|
+
}
|
|
1666
|
+
throw error;
|
|
1667
|
+
}
|
|
1668
|
+
const manifests = [];
|
|
1669
|
+
for (const versionDir of versionDirs) {
|
|
1670
|
+
let files;
|
|
1671
|
+
try {
|
|
1672
|
+
files = await readdir2(join4(migrationsRoot, versionDir));
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
throw error;
|
|
1678
|
+
}
|
|
1679
|
+
for (const file of files) {
|
|
1680
|
+
if (!file.endsWith(".json"))
|
|
1681
|
+
continue;
|
|
1682
|
+
manifests.push(parseMigrationManifest(JSON.parse(await readFile3(join4(migrationsRoot, versionDir, file), "utf8"))));
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return manifests;
|
|
1686
|
+
}
|
|
1529
1687
|
|
|
1530
1688
|
// ../core/dist/project/uuid-v7.js
|
|
1531
1689
|
import { randomBytes } from "node:crypto";
|
|
@@ -1580,24 +1738,13 @@ async function operationFor(root, path, content) {
|
|
|
1580
1738
|
return await exists2(join5(root, path)) ? { operation: "modify", path, content } : { operation: "add", path, content };
|
|
1581
1739
|
}
|
|
1582
1740
|
var INSTALLED_BUNDLE_PATH = ".harness/state/local/installed-harness-bundle.json";
|
|
1583
|
-
async function previousInstalledProfile(root) {
|
|
1584
|
-
const content = await readOptional(join5(root, INSTALLED_BUNDLE_PATH));
|
|
1585
|
-
if (content === "")
|
|
1586
|
-
return null;
|
|
1587
|
-
try {
|
|
1588
|
-
const parsed = JSON.parse(content);
|
|
1589
|
-
return parsed.schema_version === 1 ? parseHarnessProfile(parsed.profile) : null;
|
|
1590
|
-
} catch {
|
|
1591
|
-
return null;
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
1741
|
async function initializeProject(options) {
|
|
1595
1742
|
const root = resolve3(options.projectRoot);
|
|
1596
1743
|
const config = initConfigSchema.parse(options.config);
|
|
1597
1744
|
const existing = await existingProjectConfig(root);
|
|
1598
1745
|
const profile = config.profile;
|
|
1599
1746
|
const bundle = await loadProfileBundle(options.resourcesRoot, profile);
|
|
1600
|
-
const
|
|
1747
|
+
const managed = await managedTargets(options.resourcesRoot, profile);
|
|
1601
1748
|
const bundleHash = sha256Bytes(canonicalJson(bundle.manifest.files));
|
|
1602
1749
|
const projectConfig = projectConfigSchema.parse({
|
|
1603
1750
|
harness: { name: "hunter-harness", schema_version: 1 },
|
|
@@ -1621,23 +1768,8 @@ async function initializeProject(options) {
|
|
|
1621
1768
|
artifact_manifest_hash: null,
|
|
1622
1769
|
files: {}
|
|
1623
1770
|
});
|
|
1624
|
-
const agentsContent = upsertManagedBlock(await readOptional(join5(root, "AGENTS.md")),
|
|
1625
|
-
|
|
1626
|
-
"",
|
|
1627
|
-
"Use .harness/context-index.json to route rules, Knowledge, and codebase maps.",
|
|
1628
|
-
"Treat .claude/skills/harness-* as editable adapter working copies.",
|
|
1629
|
-
"Do not modify .harness/state or .harness/cache directly."
|
|
1630
|
-
].join("\n"));
|
|
1631
|
-
const claudeContent = upsertManagedBlock(await readOptional(join5(root, "CLAUDE.md")), [
|
|
1632
|
-
"@AGENTS.md",
|
|
1633
|
-
"",
|
|
1634
|
-
"# Hunter Harness",
|
|
1635
|
-
"",
|
|
1636
|
-
"- Rules: .claude/rules/",
|
|
1637
|
-
"- Skills: .claude/skills/harness-*/",
|
|
1638
|
-
"- Knowledge: .harness/knowledge/",
|
|
1639
|
-
"- Codebase map: .harness/codebase/map/"
|
|
1640
|
-
].join("\n"));
|
|
1771
|
+
const agentsContent = upsertManagedBlock(await readOptional(join5(root, "AGENTS.md")), AGENTS_MANAGED_BLOCK_CONTENT);
|
|
1772
|
+
const claudeContent = upsertManagedBlock(await readOptional(join5(root, "CLAUDE.md")), CLAUDE_MANAGED_BLOCK_CONTENT);
|
|
1641
1773
|
const files = /* @__PURE__ */ new Map([
|
|
1642
1774
|
[
|
|
1643
1775
|
".harness/project.yaml",
|
|
@@ -1665,46 +1797,29 @@ async function initializeProject(options) {
|
|
|
1665
1797
|
".harness/knowledge/index.json",
|
|
1666
1798
|
JSON.stringify({ schema_version: 1, generated_at: null, entries: [] }, null, 2) + "\n"
|
|
1667
1799
|
],
|
|
1668
|
-
[".harness/knowledge/_candidates/.gitkeep", ""],
|
|
1669
|
-
[".harness/knowledge/project-local/.gitkeep", ""],
|
|
1670
|
-
[".harness/codebase/map/.gitkeep", ""],
|
|
1671
|
-
[".harness/state/local/.gitkeep", ""],
|
|
1672
|
-
[".harness/cache/server-artifacts/.gitkeep", ""],
|
|
1673
|
-
[".harness/reports/.gitkeep", ""],
|
|
1674
|
-
[
|
|
1675
|
-
".harness/README.md",
|
|
1676
|
-
"# Hunter Harness\n\nUse npx hunter-harness, update, and push.\n"
|
|
1677
|
-
],
|
|
1678
1800
|
["AGENTS.md", agentsContent],
|
|
1679
|
-
["CLAUDE.md", claudeContent]
|
|
1680
|
-
[
|
|
1681
|
-
".claude/rules/harness-general.md",
|
|
1682
|
-
"# Hunter Harness Rules\n\n- Report evidence honestly.\n- Do not execute destructive actions without confirmation.\n"
|
|
1683
|
-
]
|
|
1801
|
+
["CLAUDE.md", claudeContent]
|
|
1684
1802
|
]);
|
|
1685
|
-
|
|
1686
|
-
files.set(
|
|
1687
|
-
}
|
|
1688
|
-
for (const [targetPath, bytes] of bundleFiles) {
|
|
1689
|
-
files.set(targetPath, bytes);
|
|
1803
|
+
for (const target of managed) {
|
|
1804
|
+
files.set(target.target_path, target.bytes);
|
|
1690
1805
|
}
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
installedFileList.push(".claude/rules/harness-profile-java.md");
|
|
1694
|
-
const installedManifest = {
|
|
1695
|
-
schema_version: 1,
|
|
1806
|
+
const installedState = {
|
|
1807
|
+
schema_version: 2,
|
|
1696
1808
|
profile,
|
|
1697
|
-
|
|
1809
|
+
bundle_version: bundle.manifest.bundle_version,
|
|
1810
|
+
bundle_manifest_hash: bundleHash,
|
|
1811
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1812
|
+
files: managed.map((target) => ({
|
|
1813
|
+
source_path: target.source_path,
|
|
1814
|
+
target_path: target.target_path,
|
|
1815
|
+
sha256: target.sha256
|
|
1816
|
+
})).sort((left, right) => left.target_path.localeCompare(right.target_path))
|
|
1698
1817
|
};
|
|
1699
|
-
files.set(INSTALLED_BUNDLE_PATH, JSON.stringify(
|
|
1700
|
-
const newManaged = await managedBundleTargets(options.resourcesRoot, profile);
|
|
1701
|
-
const oldProfile = await previousInstalledProfile(root);
|
|
1702
|
-
const oldManaged = oldProfile === null ? /* @__PURE__ */ new Set() : await managedBundleTargets(options.resourcesRoot, oldProfile);
|
|
1703
|
-
const deleteOperations = [...oldManaged].filter((path) => !newManaged.has(path)).map((path) => ({ operation: "delete", path }));
|
|
1818
|
+
files.set(INSTALLED_BUNDLE_PATH, JSON.stringify(installedState, null, 2) + "\n");
|
|
1704
1819
|
const paths = [...files.keys()].sort((left, right) => left.localeCompare(right));
|
|
1705
1820
|
if (!options.dryRun) {
|
|
1706
1821
|
const writeOperations = await Promise.all([...files.entries()].map(async ([path, content]) => operationFor(root, path, content)));
|
|
1707
|
-
await runTransaction(root,
|
|
1822
|
+
await runTransaction(root, writeOperations, { kind: "init" });
|
|
1708
1823
|
}
|
|
1709
1824
|
return {
|
|
1710
1825
|
projectConfig,
|
|
@@ -1714,6 +1829,325 @@ async function initializeProject(options) {
|
|
|
1714
1829
|
};
|
|
1715
1830
|
}
|
|
1716
1831
|
|
|
1832
|
+
// ../core/dist/project/refresh.js
|
|
1833
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1834
|
+
import { readFile as readFile5, readdir as readdir3, rmdir, stat as stat3 } from "node:fs/promises";
|
|
1835
|
+
import { dirname as dirname3, join as join6, resolve as resolve4 } from "node:path";
|
|
1836
|
+
import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
|
|
1837
|
+
var INSTALLED_STATE_PATH = ".harness/state/local/installed-harness-bundle.json";
|
|
1838
|
+
var CONTEXT_INDEX_PATH = ".harness/context-index.json";
|
|
1839
|
+
async function exists3(path) {
|
|
1840
|
+
try {
|
|
1841
|
+
await stat3(path);
|
|
1842
|
+
return true;
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1845
|
+
return false;
|
|
1846
|
+
}
|
|
1847
|
+
throw error;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
async function fileHex(path) {
|
|
1851
|
+
try {
|
|
1852
|
+
return createHash3("sha256").update(await readFile5(path)).digest("hex");
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1855
|
+
return null;
|
|
1856
|
+
}
|
|
1857
|
+
throw error;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
async function readOptionalText(path) {
|
|
1861
|
+
try {
|
|
1862
|
+
return await readFile5(path, "utf8");
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1865
|
+
return "";
|
|
1866
|
+
}
|
|
1867
|
+
throw error;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
async function readInstalledState(root) {
|
|
1871
|
+
const content = await readOptionalText(join6(root, INSTALLED_STATE_PATH));
|
|
1872
|
+
if (content === "") {
|
|
1873
|
+
return { profile: null, schemaVersion: null, trusted: /* @__PURE__ */ new Map() };
|
|
1874
|
+
}
|
|
1875
|
+
let parsed;
|
|
1876
|
+
try {
|
|
1877
|
+
parsed = JSON.parse(content);
|
|
1878
|
+
} catch {
|
|
1879
|
+
return { profile: null, schemaVersion: null, trusted: /* @__PURE__ */ new Map() };
|
|
1880
|
+
}
|
|
1881
|
+
const profile = parseHarnessProfile(parsed.profile);
|
|
1882
|
+
const trusted = /* @__PURE__ */ new Map();
|
|
1883
|
+
if (parsed.schema_version === 2 && Array.isArray(parsed.files)) {
|
|
1884
|
+
for (const entry of parsed.files) {
|
|
1885
|
+
if (entry !== null && typeof entry === "object" && "target_path" in entry && "sha256" in entry) {
|
|
1886
|
+
const target = entry.target_path;
|
|
1887
|
+
const sha = entry.sha256;
|
|
1888
|
+
if (typeof target === "string" && typeof sha === "string") {
|
|
1889
|
+
trusted.set(target, sha);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
return { profile, schemaVersion: typeof parsed.schema_version === "number" ? parsed.schema_version : null, trusted };
|
|
1895
|
+
}
|
|
1896
|
+
async function readContextIndexBundleHash(root) {
|
|
1897
|
+
const content = await readOptionalText(join6(root, CONTEXT_INDEX_PATH));
|
|
1898
|
+
if (content === "")
|
|
1899
|
+
return null;
|
|
1900
|
+
try {
|
|
1901
|
+
const record = JSON.parse(content);
|
|
1902
|
+
const hash = record.skill_bundle?.bundle_hash;
|
|
1903
|
+
return typeof hash === "string" ? hash : null;
|
|
1904
|
+
} catch {
|
|
1905
|
+
return null;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
async function pruneEmptyParentDirs(root, deletedPaths) {
|
|
1909
|
+
const claudeRoot = join6(root, ".claude");
|
|
1910
|
+
const boundaries = /* @__PURE__ */ new Set([
|
|
1911
|
+
claudeRoot,
|
|
1912
|
+
join6(claudeRoot, "skills"),
|
|
1913
|
+
join6(claudeRoot, "agents")
|
|
1914
|
+
]);
|
|
1915
|
+
for (const deleted of deletedPaths) {
|
|
1916
|
+
let dir = dirname3(join6(root, deleted));
|
|
1917
|
+
while (dir.startsWith(claudeRoot) && !boundaries.has(dir)) {
|
|
1918
|
+
let entries;
|
|
1919
|
+
try {
|
|
1920
|
+
entries = await readdir3(dir);
|
|
1921
|
+
} catch {
|
|
1922
|
+
break;
|
|
1923
|
+
}
|
|
1924
|
+
if (entries.length > 0)
|
|
1925
|
+
break;
|
|
1926
|
+
try {
|
|
1927
|
+
await rmdir(dir);
|
|
1928
|
+
} catch {
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
dir = dirname3(dir);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
function item(target, action, reason, oldSha, incomingSha) {
|
|
1936
|
+
return {
|
|
1937
|
+
source_path: target.source_path,
|
|
1938
|
+
target_path: target.target_path,
|
|
1939
|
+
action,
|
|
1940
|
+
reason,
|
|
1941
|
+
old_sha256: oldSha,
|
|
1942
|
+
incoming_sha256: incomingSha
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
function conflict(target, reason, oldSha, incomingSha) {
|
|
1946
|
+
return {
|
|
1947
|
+
source_path: target.source_path,
|
|
1948
|
+
target_path: target.target_path,
|
|
1949
|
+
reason,
|
|
1950
|
+
old_sha256: oldSha,
|
|
1951
|
+
incoming_sha256: incomingSha
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function sortByTarget(items) {
|
|
1955
|
+
return [...items].sort((left, right) => left.target_path.localeCompare(right.target_path));
|
|
1956
|
+
}
|
|
1957
|
+
async function refreshMarkdownBlock(root, fileName, blockContent, ops, conflicts, preserved) {
|
|
1958
|
+
const original = await readOptionalText(join6(root, fileName));
|
|
1959
|
+
const current = original === "" ? null : createHash3("sha256").update(original).digest("hex");
|
|
1960
|
+
const refresh = refreshManagedBlock(original, blockContent);
|
|
1961
|
+
const synthetic = {
|
|
1962
|
+
source_path: fileName,
|
|
1963
|
+
target_path: fileName,
|
|
1964
|
+
sha256: createHash3("sha256").update(blockContent).digest("hex"),
|
|
1965
|
+
bytes: new TextEncoder().encode(blockContent)
|
|
1966
|
+
};
|
|
1967
|
+
if (refresh.conflict) {
|
|
1968
|
+
preserved.push(item(synthetic, "preserve", "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
|
|
1969
|
+
conflicts.push(conflict(synthetic, "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
if (refresh.content === original) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
ops.push({
|
|
1976
|
+
operation: original === "" ? "add" : "modify",
|
|
1977
|
+
path: fileName,
|
|
1978
|
+
content: refresh.content
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
async function reconcileContextIndex(root, bundleVersion, bundleManifestHash) {
|
|
1982
|
+
const existing = await readOptionalText(join6(root, CONTEXT_INDEX_PATH));
|
|
1983
|
+
let record = {};
|
|
1984
|
+
if (existing !== "") {
|
|
1985
|
+
try {
|
|
1986
|
+
record = JSON.parse(existing);
|
|
1987
|
+
} catch {
|
|
1988
|
+
record = {};
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
if (Object.keys(record).length === 0) {
|
|
1992
|
+
record = {
|
|
1993
|
+
schema_version: 1,
|
|
1994
|
+
project: { claude_md: "CLAUDE.md", agents_md: "AGENTS.md" },
|
|
1995
|
+
rules: [".claude/rules/harness-general.md"],
|
|
1996
|
+
knowledge: { index: ".harness/knowledge/index.json" },
|
|
1997
|
+
codebase: { map: ".harness/codebase/map", status: "missing" }
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
record.skill_bundle = {
|
|
2001
|
+
registry_version: bundleVersion,
|
|
2002
|
+
bundle_hash: bundleManifestHash
|
|
2003
|
+
};
|
|
2004
|
+
return {
|
|
2005
|
+
operation: existing === "" ? "add" : "modify",
|
|
2006
|
+
path: CONTEXT_INDEX_PATH,
|
|
2007
|
+
content: JSON.stringify(record, null, 2) + "\n"
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
async function profileTransitionOperation(root, previousProfile, profile) {
|
|
2011
|
+
if (previousProfile === null || previousProfile === profile)
|
|
2012
|
+
return null;
|
|
2013
|
+
const path = ".harness/project.yaml";
|
|
2014
|
+
const content = await readOptionalText(join6(root, path));
|
|
2015
|
+
const project = projectConfigSchema.parse(parseYaml3(content));
|
|
2016
|
+
return {
|
|
2017
|
+
operation: "modify",
|
|
2018
|
+
path,
|
|
2019
|
+
content: stringifyYaml2({
|
|
2020
|
+
...project,
|
|
2021
|
+
project: { ...project.project, profiles: [profile] }
|
|
2022
|
+
}, { sortMapEntries: true })
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
async function refreshProject(options) {
|
|
2026
|
+
const root = resolve4(options.projectRoot);
|
|
2027
|
+
const profile = options.profile;
|
|
2028
|
+
const newManaged = await managedTargets(options.resourcesRoot, profile);
|
|
2029
|
+
const newBundle = await loadProfileBundle(options.resourcesRoot, profile);
|
|
2030
|
+
const bundleManifestHash = sha256Bytes(canonicalJson(newBundle.manifest.files));
|
|
2031
|
+
const installed = await readInstalledState(root);
|
|
2032
|
+
const previousProfile = installed.profile;
|
|
2033
|
+
let trusted = installed.trusted;
|
|
2034
|
+
let migrationOldPaths = null;
|
|
2035
|
+
if (installed.schemaVersion === 1) {
|
|
2036
|
+
const contextHash = await readContextIndexBundleHash(root);
|
|
2037
|
+
if (contextHash !== null) {
|
|
2038
|
+
const migrations = await loadMigrationManifests(options.resourcesRoot);
|
|
2039
|
+
const match = migrations.find((m) => m.bundle_manifest_hash === contextHash && m.profile === installed.profile);
|
|
2040
|
+
if (match !== void 0) {
|
|
2041
|
+
trusted = new Map(match.projection.map((entry) => [entry.target_path, entry.sha256]));
|
|
2042
|
+
migrationOldPaths = new Set(match.projection.map((entry) => entry.target_path));
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
const newTargetSet = new Set(newManaged.map((target) => target.target_path));
|
|
2047
|
+
let oldOnly = [];
|
|
2048
|
+
if (migrationOldPaths !== null) {
|
|
2049
|
+
for (const targetPath of migrationOldPaths) {
|
|
2050
|
+
if (!newTargetSet.has(targetPath)) {
|
|
2051
|
+
oldOnly.push({
|
|
2052
|
+
source_path: targetPath,
|
|
2053
|
+
target_path: targetPath,
|
|
2054
|
+
sha256: trusted.get(targetPath) ?? "",
|
|
2055
|
+
bytes: new Uint8Array()
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
} else if (previousProfile !== null && previousProfile !== profile) {
|
|
2060
|
+
const oldManaged = await managedTargets(options.resourcesRoot, previousProfile);
|
|
2061
|
+
oldOnly = oldManaged.filter((target) => !newTargetSet.has(target.target_path));
|
|
2062
|
+
}
|
|
2063
|
+
const applied = [];
|
|
2064
|
+
const removed = [];
|
|
2065
|
+
const preserved = [];
|
|
2066
|
+
const unchanged = [];
|
|
2067
|
+
const conflicts = [];
|
|
2068
|
+
const ops = [];
|
|
2069
|
+
const newStateFiles = [];
|
|
2070
|
+
for (const target of newManaged) {
|
|
2071
|
+
const incoming = target.sha256;
|
|
2072
|
+
const current = await fileHex(join6(root, target.target_path));
|
|
2073
|
+
if (current === null) {
|
|
2074
|
+
applied.push(item(target, "add", "MISSING_TARGET", null, incoming));
|
|
2075
|
+
ops.push({ operation: "add", path: target.target_path, content: target.bytes });
|
|
2076
|
+
newStateFiles.push({ source_path: target.source_path, target_path: target.target_path, sha256: incoming });
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
if (current === incoming) {
|
|
2080
|
+
unchanged.push(item(target, "unchanged", "ALREADY_CURRENT", current, incoming));
|
|
2081
|
+
newStateFiles.push({ source_path: target.source_path, target_path: target.target_path, sha256: incoming });
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
const trustedHash = trusted.get(target.target_path);
|
|
2085
|
+
if (trustedHash !== void 0 && current === trustedHash || options.forceManaged) {
|
|
2086
|
+
const reason = options.forceManaged ? "FORCE_MANAGED" : "BASELINE_CLEAN";
|
|
2087
|
+
applied.push(item(target, "replace", reason, current, incoming));
|
|
2088
|
+
ops.push({ operation: "modify", path: target.target_path, content: target.bytes });
|
|
2089
|
+
newStateFiles.push({ source_path: target.source_path, target_path: target.target_path, sha256: incoming });
|
|
2090
|
+
} else {
|
|
2091
|
+
const reason = trustedHash === void 0 ? "LEGACY_BASELINE_UNKNOWN" : "LOCAL_MODIFICATION";
|
|
2092
|
+
preserved.push(item(target, "preserve", reason, current, incoming));
|
|
2093
|
+
conflicts.push(conflict(target, reason, current, incoming));
|
|
2094
|
+
if (trustedHash !== void 0) {
|
|
2095
|
+
newStateFiles.push({ source_path: target.source_path, target_path: target.target_path, sha256: trustedHash });
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
for (const target of oldOnly) {
|
|
2100
|
+
const current = await fileHex(join6(root, target.target_path));
|
|
2101
|
+
if (current === null) {
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
const trustedHash = trusted.get(target.target_path);
|
|
2105
|
+
const clean = trustedHash !== void 0 && current === trustedHash;
|
|
2106
|
+
if (clean || options.forceManaged) {
|
|
2107
|
+
const reason = clean ? "BASELINE_CLEAN" : "FORCE_MANAGED";
|
|
2108
|
+
removed.push(item(target, "delete", reason, current, null));
|
|
2109
|
+
ops.push({ operation: "delete", path: target.target_path });
|
|
2110
|
+
} else {
|
|
2111
|
+
const reason = trustedHash === void 0 ? "LEGACY_BASELINE_UNKNOWN" : "LEGACY_PROFILE_FILE_MODIFIED";
|
|
2112
|
+
preserved.push(item(target, "preserve", reason, current, null));
|
|
2113
|
+
conflicts.push(conflict(target, reason, current, null));
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
await refreshMarkdownBlock(root, "AGENTS.md", AGENTS_MANAGED_BLOCK_CONTENT, ops, conflicts, preserved);
|
|
2117
|
+
await refreshMarkdownBlock(root, "CLAUDE.md", CLAUDE_MANAGED_BLOCK_CONTENT, ops, conflicts, preserved);
|
|
2118
|
+
const profileOperation = await profileTransitionOperation(root, previousProfile, profile);
|
|
2119
|
+
if (profileOperation !== null)
|
|
2120
|
+
ops.push(profileOperation);
|
|
2121
|
+
ops.push(await reconcileContextIndex(root, newBundle.manifest.bundle_version, bundleManifestHash));
|
|
2122
|
+
const installedState = {
|
|
2123
|
+
schema_version: 2,
|
|
2124
|
+
profile,
|
|
2125
|
+
bundle_version: newBundle.manifest.bundle_version,
|
|
2126
|
+
bundle_manifest_hash: bundleManifestHash,
|
|
2127
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2128
|
+
files: newStateFiles.sort((left, right) => left.target_path.localeCompare(right.target_path))
|
|
2129
|
+
};
|
|
2130
|
+
ops.push({
|
|
2131
|
+
operation: await exists3(join6(root, INSTALLED_STATE_PATH)) ? "modify" : "add",
|
|
2132
|
+
path: INSTALLED_STATE_PATH,
|
|
2133
|
+
content: JSON.stringify(installedState, null, 2) + "\n"
|
|
2134
|
+
});
|
|
2135
|
+
if (!options.dryRun) {
|
|
2136
|
+
await runTransaction(root, ops, { kind: "refresh" });
|
|
2137
|
+
await pruneEmptyParentDirs(root, removed.map((item2) => item2.target_path));
|
|
2138
|
+
}
|
|
2139
|
+
return {
|
|
2140
|
+
profile,
|
|
2141
|
+
previous_profile: previousProfile,
|
|
2142
|
+
dry_run: options.dryRun,
|
|
2143
|
+
applied: sortByTarget(applied),
|
|
2144
|
+
removed: sortByTarget(removed),
|
|
2145
|
+
preserved: sortByTarget(preserved),
|
|
2146
|
+
unchanged: sortByTarget(unchanged),
|
|
2147
|
+
conflicts: sortByTarget(conflicts)
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
|
|
1717
2151
|
// ../core/dist/proposal/diff.js
|
|
1718
2152
|
function operationPath(operation) {
|
|
1719
2153
|
return operation.operation === "rename" ? operation.to_path : operation.path;
|
|
@@ -1857,7 +2291,7 @@ function highEntropyCandidates(content) {
|
|
|
1857
2291
|
value: match[0],
|
|
1858
2292
|
offset: match.index,
|
|
1859
2293
|
entropy: shannonEntropy(match[0])
|
|
1860
|
-
})).filter((
|
|
2294
|
+
})).filter((item2) => item2.entropy >= 4.5);
|
|
1861
2295
|
}
|
|
1862
2296
|
|
|
1863
2297
|
// ../core/dist/security/scanner.js
|
|
@@ -1953,8 +2387,8 @@ function scanSensitiveFiles(files, options = {}) {
|
|
|
1953
2387
|
sha256Bytes(raw.value)
|
|
1954
2388
|
].join("\0"));
|
|
1955
2389
|
const overridable = raw.severity !== "high";
|
|
1956
|
-
const explicit = options.overrides?.find((
|
|
1957
|
-
const inline = ignores.find((
|
|
2390
|
+
const explicit = options.overrides?.find((item2) => item2.finding_fingerprint === fingerprint);
|
|
2391
|
+
const inline = ignores.find((item2) => item2.ruleId === raw.ruleId);
|
|
1958
2392
|
const override = overridable ? explicit ?? (inline === void 0 ? void 0 : {
|
|
1959
2393
|
finding_fingerprint: fingerprint,
|
|
1960
2394
|
actor: "inline-annotation",
|
|
@@ -2012,28 +2446,28 @@ function generateProposalPreview(input, scanOptions = {}) {
|
|
|
2012
2446
|
}
|
|
2013
2447
|
|
|
2014
2448
|
// ../core/dist/push/push.js
|
|
2015
|
-
import { lstat as lstat2, readFile as
|
|
2016
|
-
import { join as
|
|
2017
|
-
import { parse as
|
|
2449
|
+
import { lstat as lstat2, readFile as readFile8, readdir as readdir4, rm as rm4 } from "node:fs/promises";
|
|
2450
|
+
import { join as join9, relative, resolve as resolve5 } from "node:path";
|
|
2451
|
+
import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
|
|
2018
2452
|
|
|
2019
2453
|
// ../core/dist/state/baseline.js
|
|
2020
|
-
import { readFile as
|
|
2021
|
-
import { join as
|
|
2454
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
2455
|
+
import { join as join7 } from "node:path";
|
|
2022
2456
|
async function readBaseline(projectRoot) {
|
|
2023
|
-
const content = await
|
|
2457
|
+
const content = await readFile6(join7(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
|
|
2024
2458
|
return baselineManifestSchema.parse(JSON.parse(content));
|
|
2025
2459
|
}
|
|
2026
2460
|
|
|
2027
2461
|
// ../core/dist/state/locks.js
|
|
2028
2462
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2029
|
-
import { readFile as
|
|
2030
|
-
import { join as
|
|
2463
|
+
import { readFile as readFile7, rename as rename3, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
|
|
2464
|
+
import { join as join8 } from "node:path";
|
|
2031
2465
|
async function readLock(path) {
|
|
2032
|
-
return JSON.parse(await
|
|
2466
|
+
return JSON.parse(await readFile7(path, "utf8"));
|
|
2033
2467
|
}
|
|
2034
2468
|
async function acquireProtocolLock(projectRoot, operation, options = {}) {
|
|
2035
2469
|
const layout = await ensureStateLayout(projectRoot);
|
|
2036
|
-
const lockPath =
|
|
2470
|
+
const lockPath = join8(layout.locks, "protocol.lock");
|
|
2037
2471
|
const now = options.now ?? Date.now();
|
|
2038
2472
|
const staleAfterMs = options.staleAfterMs ?? 15 * 60 * 1e3;
|
|
2039
2473
|
const record = {
|
|
@@ -2096,7 +2530,7 @@ var MANAGED_FILES = [
|
|
|
2096
2530
|
".harness/project.yaml",
|
|
2097
2531
|
".harness/context-index.json"
|
|
2098
2532
|
];
|
|
2099
|
-
async function
|
|
2533
|
+
async function exists4(path) {
|
|
2100
2534
|
try {
|
|
2101
2535
|
await lstat2(path);
|
|
2102
2536
|
return true;
|
|
@@ -2108,43 +2542,43 @@ async function exists3(path) {
|
|
|
2108
2542
|
}
|
|
2109
2543
|
}
|
|
2110
2544
|
async function walkFiles(root, current, output) {
|
|
2111
|
-
if (!await
|
|
2545
|
+
if (!await exists4(current)) {
|
|
2112
2546
|
return;
|
|
2113
2547
|
}
|
|
2114
|
-
for (const
|
|
2115
|
-
const path =
|
|
2116
|
-
if (
|
|
2548
|
+
for (const item2 of await readdir4(current, { withFileTypes: true })) {
|
|
2549
|
+
const path = join9(current, item2.name);
|
|
2550
|
+
if (item2.isSymbolicLink()) {
|
|
2117
2551
|
throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
|
|
2118
2552
|
}
|
|
2119
|
-
if (
|
|
2553
|
+
if (item2.isDirectory()) {
|
|
2120
2554
|
await walkFiles(root, path, output);
|
|
2121
|
-
} else if (
|
|
2555
|
+
} else if (item2.isFile()) {
|
|
2122
2556
|
output.push(normalizeManagedPath(relative(root, path).replaceAll("\\", "/")));
|
|
2123
2557
|
}
|
|
2124
2558
|
}
|
|
2125
2559
|
}
|
|
2126
2560
|
async function managedFiles(projectRoot) {
|
|
2127
|
-
const root =
|
|
2561
|
+
const root = resolve5(projectRoot);
|
|
2128
2562
|
const paths = [];
|
|
2129
2563
|
for (const path of MANAGED_FILES) {
|
|
2130
|
-
if (await
|
|
2564
|
+
if (await exists4(join9(root, path))) {
|
|
2131
2565
|
paths.push(path);
|
|
2132
2566
|
}
|
|
2133
2567
|
}
|
|
2134
2568
|
for (const path of MANAGED_ROOTS) {
|
|
2135
|
-
await walkFiles(root,
|
|
2569
|
+
await walkFiles(root, join9(root, path), paths);
|
|
2136
2570
|
}
|
|
2137
|
-
const skillsRoot =
|
|
2138
|
-
if (await
|
|
2139
|
-
for (const
|
|
2140
|
-
if (
|
|
2141
|
-
await walkFiles(root,
|
|
2571
|
+
const skillsRoot = join9(root, ".claude", "skills");
|
|
2572
|
+
if (await exists4(skillsRoot)) {
|
|
2573
|
+
for (const item2 of await readdir4(skillsRoot, { withFileTypes: true })) {
|
|
2574
|
+
if (item2.isDirectory() && item2.name.startsWith("harness-")) {
|
|
2575
|
+
await walkFiles(root, join9(skillsRoot, item2.name), paths);
|
|
2142
2576
|
}
|
|
2143
2577
|
}
|
|
2144
2578
|
}
|
|
2145
2579
|
const result = {};
|
|
2146
2580
|
for (const path of [...new Set(paths)].sort()) {
|
|
2147
|
-
result[path] = await
|
|
2581
|
+
result[path] = await readFile8(join9(root, path), "utf8");
|
|
2148
2582
|
}
|
|
2149
2583
|
return result;
|
|
2150
2584
|
}
|
|
@@ -2153,14 +2587,14 @@ function proposalBaseline(baseline) {
|
|
|
2153
2587
|
}
|
|
2154
2588
|
async function readProject(root) {
|
|
2155
2589
|
try {
|
|
2156
|
-
return projectConfigSchema.parse(
|
|
2590
|
+
return projectConfigSchema.parse(parseYaml4(await readFile8(join9(root, ".harness", "project.yaml"), "utf8")));
|
|
2157
2591
|
} catch {
|
|
2158
2592
|
throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
|
|
2159
2593
|
}
|
|
2160
2594
|
}
|
|
2161
2595
|
async function readOptionalJson(path) {
|
|
2162
2596
|
try {
|
|
2163
|
-
return JSON.parse(await
|
|
2597
|
+
return JSON.parse(await readFile8(path, "utf8"));
|
|
2164
2598
|
} catch (error) {
|
|
2165
2599
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2166
2600
|
return null;
|
|
@@ -2172,7 +2606,7 @@ async function clientIdFor(root, explicit) {
|
|
|
2172
2606
|
if (explicit !== void 0) {
|
|
2173
2607
|
return explicit;
|
|
2174
2608
|
}
|
|
2175
|
-
const path =
|
|
2609
|
+
const path = join9(root, ".harness", "state", "local", "client.json");
|
|
2176
2610
|
const existing = await readOptionalJson(path);
|
|
2177
2611
|
if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
|
|
2178
2612
|
return existing.client_id;
|
|
@@ -2244,7 +2678,7 @@ async function bindProject(root, project, baseline, projectId) {
|
|
|
2244
2678
|
{
|
|
2245
2679
|
operation: "modify",
|
|
2246
2680
|
path: ".harness/project.yaml",
|
|
2247
|
-
content:
|
|
2681
|
+
content: stringifyYaml3(nextProject, { sortMapEntries: true })
|
|
2248
2682
|
},
|
|
2249
2683
|
{
|
|
2250
2684
|
operation: "modify",
|
|
@@ -2255,7 +2689,7 @@ async function bindProject(root, project, baseline, projectId) {
|
|
|
2255
2689
|
return { project: nextProject, baseline: nextBaseline };
|
|
2256
2690
|
}
|
|
2257
2691
|
async function pushProject(options) {
|
|
2258
|
-
const root =
|
|
2692
|
+
const root = resolve5(options.projectRoot);
|
|
2259
2693
|
let project = await readProject(root);
|
|
2260
2694
|
let baseline = await readBaseline(root);
|
|
2261
2695
|
const profile = parseHarnessProfile(project.project.profiles[0]);
|
|
@@ -2291,7 +2725,7 @@ async function pushProject(options) {
|
|
|
2291
2725
|
if (parsedServerUrl.protocol !== "https:") {
|
|
2292
2726
|
throw new PushWorkflowError("server_url must use HTTPS", 3, "SERVER_URL_INVALID");
|
|
2293
2727
|
}
|
|
2294
|
-
const workflowPath =
|
|
2728
|
+
const workflowPath = join9(root, ".harness", "state", "local", "push-workflow.json");
|
|
2295
2729
|
const priorWorkflow = await readOptionalJson(workflowPath);
|
|
2296
2730
|
const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
|
|
2297
2731
|
const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
|
|
@@ -2382,7 +2816,7 @@ async function pushProject(options) {
|
|
|
2382
2816
|
}
|
|
2383
2817
|
}
|
|
2384
2818
|
const finalized = await client.finalizeProposal(session.session_id, { schema_version: 1, manifest_sha256: proposalManifestHash }, requestId, workflow.finalize_idempotency_key);
|
|
2385
|
-
await atomicWriteJson(
|
|
2819
|
+
await atomicWriteJson(join9(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
|
|
2386
2820
|
schema_version: 1,
|
|
2387
2821
|
request_id: requestId,
|
|
2388
2822
|
project_id: projectId,
|
|
@@ -2407,12 +2841,75 @@ async function pushProject(options) {
|
|
|
2407
2841
|
}
|
|
2408
2842
|
}
|
|
2409
2843
|
|
|
2844
|
+
// ../core/dist/state/cleanup.js
|
|
2845
|
+
import { readFile as readFile9, readdir as readdir5, rm as rm5 } from "node:fs/promises";
|
|
2846
|
+
import { join as join10 } from "node:path";
|
|
2847
|
+
function isSafeEntryName(name) {
|
|
2848
|
+
return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
|
|
2849
|
+
}
|
|
2850
|
+
async function listDir(path) {
|
|
2851
|
+
try {
|
|
2852
|
+
return await readdir5(path);
|
|
2853
|
+
} catch (error) {
|
|
2854
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2855
|
+
return [];
|
|
2856
|
+
}
|
|
2857
|
+
throw error;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
async function cleanupProject(options) {
|
|
2861
|
+
const layout = stateLayout(options.projectRoot);
|
|
2862
|
+
const pruned = [];
|
|
2863
|
+
const removedCache = [];
|
|
2864
|
+
const committedByKind = /* @__PURE__ */ new Map();
|
|
2865
|
+
for (const name of await listDir(layout.transactions)) {
|
|
2866
|
+
if (!isSafeEntryName(name))
|
|
2867
|
+
continue;
|
|
2868
|
+
let journal;
|
|
2869
|
+
try {
|
|
2870
|
+
journal = JSON.parse(await readFile9(join10(layout.transactions, name, "journal.json"), "utf8"));
|
|
2871
|
+
} catch {
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
if (journal.state === "committed" && typeof journal.kind === "string") {
|
|
2875
|
+
const arr = committedByKind.get(journal.kind) ?? [];
|
|
2876
|
+
arr.push({ id: name, createdAt: typeof journal.created_at === "string" ? journal.created_at : "" });
|
|
2877
|
+
committedByKind.set(journal.kind, arr);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
for (const arr of committedByKind.values()) {
|
|
2881
|
+
arr.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
2882
|
+
for (let index = 1; index < arr.length; index += 1) {
|
|
2883
|
+
const entry = arr[index];
|
|
2884
|
+
if (entry === void 0)
|
|
2885
|
+
continue;
|
|
2886
|
+
pruned.push(entry.id);
|
|
2887
|
+
if (!options.dryRun) {
|
|
2888
|
+
await rm5(join10(layout.transactions, entry.id), { recursive: true, force: true });
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
for (const name of await listDir(layout.serverArtifacts)) {
|
|
2893
|
+
if (!isSafeEntryName(name))
|
|
2894
|
+
continue;
|
|
2895
|
+
removedCache.push(name);
|
|
2896
|
+
if (!options.dryRun) {
|
|
2897
|
+
await rm5(join10(layout.serverArtifacts, name), { recursive: true, force: true });
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
return {
|
|
2901
|
+
dry_run: options.dryRun,
|
|
2902
|
+
pruned_transactions: pruned.sort(),
|
|
2903
|
+
removed_cache: removedCache.sort()
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2410
2907
|
// ../core/dist/skill/frontmatter.js
|
|
2411
|
-
import { parse as
|
|
2908
|
+
import { parse as parseYaml5 } from "yaml";
|
|
2412
2909
|
import { z as z12 } from "zod";
|
|
2413
2910
|
|
|
2414
2911
|
// ../core/dist/skill/fixer.js
|
|
2415
|
-
import { parse as
|
|
2912
|
+
import { parse as parseYaml6, stringify as stringifyYaml4 } from "yaml";
|
|
2416
2913
|
|
|
2417
2914
|
// ../core/dist/skill/agents.js
|
|
2418
2915
|
var AGENT_DESCRIPTORS = {
|
|
@@ -2451,10 +2948,10 @@ var AGENT_DESCRIPTORS = {
|
|
|
2451
2948
|
var INSTALLABLE_AGENTS = Object.keys(AGENT_DESCRIPTORS).filter((agent) => AGENT_DESCRIPTORS[agent]?.installable === true);
|
|
2452
2949
|
|
|
2453
2950
|
// ../core/dist/transaction/recovery.js
|
|
2454
|
-
import { readFile as
|
|
2455
|
-
import { join as
|
|
2951
|
+
import { readFile as readFile10, readdir as readdir6, rm as rm6, stat as stat4 } from "node:fs/promises";
|
|
2952
|
+
import { join as join11 } from "node:path";
|
|
2456
2953
|
async function recoverTransaction(projectRoot, transactionId) {
|
|
2457
|
-
const journal = JSON.parse(await
|
|
2954
|
+
const journal = JSON.parse(await readFile10(join11(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
|
|
2458
2955
|
if (journal.state === "committed") {
|
|
2459
2956
|
return { transactionId, status: "committed" };
|
|
2460
2957
|
}
|
|
@@ -2471,7 +2968,7 @@ async function listTransactions(projectRoot) {
|
|
|
2471
2968
|
const root = stateLayout(projectRoot).transactions;
|
|
2472
2969
|
let names;
|
|
2473
2970
|
try {
|
|
2474
|
-
names = await
|
|
2971
|
+
names = await readdir6(root);
|
|
2475
2972
|
} catch (error) {
|
|
2476
2973
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2477
2974
|
return [];
|
|
@@ -2481,7 +2978,7 @@ async function listTransactions(projectRoot) {
|
|
|
2481
2978
|
const transactions = [];
|
|
2482
2979
|
for (const transactionId of names) {
|
|
2483
2980
|
try {
|
|
2484
|
-
const journal = JSON.parse(await
|
|
2981
|
+
const journal = JSON.parse(await readFile10(join11(root, transactionId, "journal.json"), "utf8"));
|
|
2485
2982
|
transactions.push({
|
|
2486
2983
|
transactionId,
|
|
2487
2984
|
kind: journal.kind,
|
|
@@ -2494,11 +2991,11 @@ async function listTransactions(projectRoot) {
|
|
|
2494
2991
|
return transactions.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
2495
2992
|
}
|
|
2496
2993
|
async function pendingTransactions(projectRoot) {
|
|
2497
|
-
return (await listTransactions(projectRoot)).filter((
|
|
2994
|
+
return (await listTransactions(projectRoot)).filter((item2) => RECOVERY_STATES.has(item2.state));
|
|
2498
2995
|
}
|
|
2499
2996
|
async function pathExists(path) {
|
|
2500
2997
|
try {
|
|
2501
|
-
await
|
|
2998
|
+
await stat4(path);
|
|
2502
2999
|
return true;
|
|
2503
3000
|
} catch (error) {
|
|
2504
3001
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -2508,17 +3005,17 @@ async function pathExists(path) {
|
|
|
2508
3005
|
}
|
|
2509
3006
|
}
|
|
2510
3007
|
async function rollbackLatestCommittedUpdate(projectRoot) {
|
|
2511
|
-
const latest = (await listTransactions(projectRoot)).find((
|
|
3008
|
+
const latest = (await listTransactions(projectRoot)).find((item2) => item2.kind === "update" && item2.state === "committed");
|
|
2512
3009
|
if (latest === void 0) {
|
|
2513
3010
|
throw new Error("no committed update transaction is available for rollback");
|
|
2514
3011
|
}
|
|
2515
|
-
const transactionRoot =
|
|
2516
|
-
const journal = JSON.parse(await
|
|
2517
|
-
const after = JSON.parse(await
|
|
3012
|
+
const transactionRoot = join11(stateLayout(projectRoot).transactions, latest.transactionId);
|
|
3013
|
+
const journal = JSON.parse(await readFile10(join11(transactionRoot, "journal.json"), "utf8"));
|
|
3014
|
+
const after = JSON.parse(await readFile10(join11(transactionRoot, "after", "manifest.json"), "utf8"));
|
|
2518
3015
|
for (const entry of after) {
|
|
2519
|
-
const target =
|
|
2520
|
-
const
|
|
2521
|
-
if (
|
|
3016
|
+
const target = join11(projectRoot, entry.path);
|
|
3017
|
+
const exists6 = await pathExists(target);
|
|
3018
|
+
if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
|
|
2522
3019
|
throw new Error("cannot rollback dirty path: " + entry.path);
|
|
2523
3020
|
}
|
|
2524
3021
|
}
|
|
@@ -2529,16 +3026,16 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
|
|
|
2529
3026
|
continue;
|
|
2530
3027
|
}
|
|
2531
3028
|
seen.add(snapshot.path);
|
|
2532
|
-
const target =
|
|
2533
|
-
const
|
|
3029
|
+
const target = join11(projectRoot, snapshot.path);
|
|
3030
|
+
const exists6 = await pathExists(target);
|
|
2534
3031
|
if (snapshot.existed && snapshot.snapshot_name !== null) {
|
|
2535
|
-
const content = await
|
|
3032
|
+
const content = await readFile10(join11(transactionRoot, "before", snapshot.snapshot_name));
|
|
2536
3033
|
operations.push({
|
|
2537
|
-
operation:
|
|
3034
|
+
operation: exists6 ? "modify" : "add",
|
|
2538
3035
|
path: snapshot.path,
|
|
2539
3036
|
content
|
|
2540
3037
|
});
|
|
2541
|
-
} else if (
|
|
3038
|
+
} else if (exists6) {
|
|
2542
3039
|
operations.push({ operation: "delete", path: snapshot.path });
|
|
2543
3040
|
}
|
|
2544
3041
|
}
|
|
@@ -2549,23 +3046,23 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
|
|
|
2549
3046
|
}
|
|
2550
3047
|
async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Date()) {
|
|
2551
3048
|
const transactions = await listTransactions(projectRoot);
|
|
2552
|
-
const committedUpdates = transactions.filter((
|
|
2553
|
-
const keepCommitted = new Set(committedUpdates.slice(0, 10).map((
|
|
3049
|
+
const committedUpdates = transactions.filter((item2) => item2.kind === "update" && item2.state === "committed");
|
|
3050
|
+
const keepCommitted = new Set(committedUpdates.slice(0, 10).map((item2) => item2.transactionId));
|
|
2554
3051
|
const removed = [];
|
|
2555
|
-
for (const
|
|
2556
|
-
if (RECOVERY_STATES.has(
|
|
3052
|
+
for (const item2 of transactions) {
|
|
3053
|
+
if (RECOVERY_STATES.has(item2.state) || keepCommitted.has(item2.transactionId)) {
|
|
2557
3054
|
continue;
|
|
2558
3055
|
}
|
|
2559
|
-
const ageDays = (now.getTime() - Date.parse(
|
|
2560
|
-
const removable =
|
|
3056
|
+
const ageDays = (now.getTime() - Date.parse(item2.createdAt)) / 864e5;
|
|
3057
|
+
const removable = item2.state === "rolled_back" ? ageDays > 7 : item2.state === "committed" && ageDays > 30;
|
|
2561
3058
|
if (!removable) {
|
|
2562
3059
|
continue;
|
|
2563
3060
|
}
|
|
2564
|
-
await
|
|
3061
|
+
await rm6(join11(stateLayout(projectRoot).transactions, item2.transactionId), {
|
|
2565
3062
|
recursive: true,
|
|
2566
3063
|
force: true
|
|
2567
3064
|
});
|
|
2568
|
-
removed.push(
|
|
3065
|
+
removed.push(item2.transactionId);
|
|
2569
3066
|
}
|
|
2570
3067
|
return removed;
|
|
2571
3068
|
}
|
|
@@ -2593,9 +3090,9 @@ function managedBlockDirty(currentContent, managedBlockHash) {
|
|
|
2593
3090
|
}
|
|
2594
3091
|
|
|
2595
3092
|
// ../core/dist/update/update.js
|
|
2596
|
-
import { lstat as lstat3, readFile as
|
|
2597
|
-
import { join as
|
|
2598
|
-
import { parse as
|
|
3093
|
+
import { lstat as lstat3, readFile as readFile11, rm as rm7 } from "node:fs/promises";
|
|
3094
|
+
import { join as join12, resolve as resolve6 } from "node:path";
|
|
3095
|
+
import { parse as parseYaml7 } from "yaml";
|
|
2599
3096
|
var UpdateWorkflowError = class extends Error {
|
|
2600
3097
|
exitCode;
|
|
2601
3098
|
code;
|
|
@@ -2618,7 +3115,7 @@ async function pathExists2(path) {
|
|
|
2618
3115
|
}
|
|
2619
3116
|
}
|
|
2620
3117
|
async function optionalContent(path) {
|
|
2621
|
-
return await pathExists2(path) ?
|
|
3118
|
+
return await pathExists2(path) ? readFile11(path, "utf8") : null;
|
|
2622
3119
|
}
|
|
2623
3120
|
function manifestPayloadHash(manifest) {
|
|
2624
3121
|
const payload = { ...manifest };
|
|
@@ -2636,14 +3133,14 @@ async function loadBlob(root, client, artifactId, operation, requestId, dryRun)
|
|
|
2636
3133
|
return null;
|
|
2637
3134
|
}
|
|
2638
3135
|
const hash = operation.content_sha256;
|
|
2639
|
-
const cacheRoot =
|
|
2640
|
-
const cachePath =
|
|
3136
|
+
const cacheRoot = join12(root, ".harness", "cache", "server-artifacts", artifactId);
|
|
3137
|
+
const cachePath = join12(cacheRoot, "blobs", hash.replace(":", "_"));
|
|
2641
3138
|
if (await pathExists2(cachePath) && await sha256File(cachePath) === hash) {
|
|
2642
|
-
return
|
|
3139
|
+
return readFile11(cachePath, "utf8");
|
|
2643
3140
|
}
|
|
2644
3141
|
const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
|
|
2645
3142
|
if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
|
|
2646
|
-
await
|
|
3143
|
+
await rm7(cachePath, { force: true });
|
|
2647
3144
|
throw new UpdateWorkflowError("artifact blob size or hash mismatch", 4, "ARTIFACT_HASH_MISMATCH");
|
|
2648
3145
|
}
|
|
2649
3146
|
if (!dryRun) {
|
|
@@ -2662,34 +3159,34 @@ function nextBaselineEntry(operation, finalContent, projectVersion) {
|
|
|
2662
3159
|
...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
|
|
2663
3160
|
};
|
|
2664
3161
|
}
|
|
2665
|
-
function transactionOperation(
|
|
2666
|
-
if (
|
|
3162
|
+
function transactionOperation(item2) {
|
|
3163
|
+
if (item2.equivalent) {
|
|
2667
3164
|
return null;
|
|
2668
3165
|
}
|
|
2669
|
-
const operation =
|
|
3166
|
+
const operation = item2.operation;
|
|
2670
3167
|
const target = operationTargetPath(operation);
|
|
2671
3168
|
if (operation.operation === "delete") {
|
|
2672
|
-
return
|
|
3169
|
+
return item2.finalContent === null ? { operation: "delete", path: target } : { operation: "modify", path: target, content: item2.finalContent };
|
|
2673
3170
|
}
|
|
2674
3171
|
if (operation.operation === "rename") {
|
|
2675
3172
|
return {
|
|
2676
3173
|
operation: "rename",
|
|
2677
3174
|
from_path: operation.from_path,
|
|
2678
3175
|
to_path: operation.to_path,
|
|
2679
|
-
content:
|
|
3176
|
+
content: item2.finalContent ?? item2.content ?? ""
|
|
2680
3177
|
};
|
|
2681
3178
|
}
|
|
2682
3179
|
return {
|
|
2683
3180
|
operation: "modify",
|
|
2684
3181
|
path: target,
|
|
2685
|
-
content:
|
|
3182
|
+
content: item2.finalContent ?? item2.content ?? ""
|
|
2686
3183
|
};
|
|
2687
3184
|
}
|
|
2688
3185
|
async function updateProject(options) {
|
|
2689
|
-
const root =
|
|
3186
|
+
const root = resolve6(options.projectRoot);
|
|
2690
3187
|
let project;
|
|
2691
3188
|
try {
|
|
2692
|
-
project = projectConfigSchema.parse(
|
|
3189
|
+
project = projectConfigSchema.parse(parseYaml7(await readFile11(join12(root, ".harness", "project.yaml"), "utf8")));
|
|
2693
3190
|
} catch {
|
|
2694
3191
|
throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
|
|
2695
3192
|
}
|
|
@@ -2774,8 +3271,8 @@ async function updateProject(options) {
|
|
|
2774
3271
|
skipped.push({ path: target, operation: operation.operation, reason: "baseline-diverged" });
|
|
2775
3272
|
continue;
|
|
2776
3273
|
}
|
|
2777
|
-
const sourceContent = await optionalContent(
|
|
2778
|
-
const targetContent = target === source ? sourceContent : await optionalContent(
|
|
3274
|
+
const sourceContent = await optionalContent(join12(root, source));
|
|
3275
|
+
const targetContent = target === source ? sourceContent : await optionalContent(join12(root, target));
|
|
2779
3276
|
const incoming = blobs.get(operation) ?? null;
|
|
2780
3277
|
const incomingHash = contentHash(operation);
|
|
2781
3278
|
let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
|
|
@@ -2820,7 +3317,7 @@ async function updateProject(options) {
|
|
|
2820
3317
|
}
|
|
2821
3318
|
prepared.push({ operation, content: incoming, finalContent, equivalent });
|
|
2822
3319
|
}
|
|
2823
|
-
const applied = prepared.map((
|
|
3320
|
+
const applied = prepared.map((item2) => operationTargetPath(item2.operation));
|
|
2824
3321
|
if (options.dryRun) {
|
|
2825
3322
|
return {
|
|
2826
3323
|
requestId,
|
|
@@ -2834,18 +3331,18 @@ async function updateProject(options) {
|
|
|
2834
3331
|
dryRun: true
|
|
2835
3332
|
};
|
|
2836
3333
|
}
|
|
2837
|
-
await atomicWriteJson(
|
|
3334
|
+
await atomicWriteJson(join12(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
|
|
2838
3335
|
const lock = await acquireProtocolLock(root, "update", { requestId });
|
|
2839
3336
|
try {
|
|
2840
3337
|
const nextBaseline = baselineManifestSchema.parse(structuredClone(baseline));
|
|
2841
|
-
for (const
|
|
2842
|
-
const target = operationTargetPath(
|
|
2843
|
-
nextBaseline.files[target] = nextBaselineEntry(
|
|
2844
|
-
if (
|
|
2845
|
-
nextBaseline.files[
|
|
3338
|
+
for (const item2 of prepared) {
|
|
3339
|
+
const target = operationTargetPath(item2.operation);
|
|
3340
|
+
nextBaseline.files[target] = nextBaselineEntry(item2.operation, item2.finalContent, manifest.project_version);
|
|
3341
|
+
if (item2.operation.operation === "rename") {
|
|
3342
|
+
nextBaseline.files[item2.operation.from_path] = {
|
|
2846
3343
|
baseline_hash: null,
|
|
2847
3344
|
local_hash_at_apply: null,
|
|
2848
|
-
file_kind:
|
|
3345
|
+
file_kind: item2.operation.file_kind,
|
|
2849
3346
|
last_applied_version: manifest.project_version,
|
|
2850
3347
|
deleted: true
|
|
2851
3348
|
};
|
|
@@ -2867,7 +3364,7 @@ async function updateProject(options) {
|
|
|
2867
3364
|
skipped,
|
|
2868
3365
|
transaction_id: transactionId
|
|
2869
3366
|
};
|
|
2870
|
-
const operations = prepared.map((
|
|
3367
|
+
const operations = prepared.map((item2) => transactionOperation(item2)).filter((item2) => item2 !== null);
|
|
2871
3368
|
operations.push({
|
|
2872
3369
|
operation: "modify",
|
|
2873
3370
|
path: ".harness/state/baseline/manifest.json",
|
|
@@ -2915,8 +3412,8 @@ async function updateProject(options) {
|
|
|
2915
3412
|
}
|
|
2916
3413
|
|
|
2917
3414
|
// src/config/init-config.ts
|
|
2918
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2919
|
-
import { readFile as
|
|
3415
|
+
import { isAbsolute as isAbsolute2, join as join13 } from "node:path";
|
|
3416
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
2920
3417
|
var InitConfigurationError = class extends Error {
|
|
2921
3418
|
exitCode;
|
|
2922
3419
|
constructor(message, exitCode = 3, options) {
|
|
@@ -2929,14 +3426,14 @@ function normalizeProfile(value) {
|
|
|
2929
3426
|
if (value === void 0) return void 0;
|
|
2930
3427
|
if (value === "" || value === "1" || value === "general") return "general";
|
|
2931
3428
|
if (value === "2" || value === "java") return "java";
|
|
2932
|
-
throw new InitConfigurationError("
|
|
3429
|
+
throw new InitConfigurationError("\u914D\u7F6E\u7C7B\u578B\u5FC5\u987B\u4E3A general \u6216 java");
|
|
2933
3430
|
}
|
|
2934
3431
|
async function resolveInitConfig(cwd, flags, promptMissing) {
|
|
2935
3432
|
let fileConfig = {};
|
|
2936
3433
|
if (flags.config !== void 0) {
|
|
2937
|
-
const path = isAbsolute2(flags.config) ? flags.config :
|
|
3434
|
+
const path = isAbsolute2(flags.config) ? flags.config : join13(cwd, flags.config);
|
|
2938
3435
|
try {
|
|
2939
|
-
fileConfig = JSON.parse(await
|
|
3436
|
+
fileConfig = JSON.parse(await readFile12(path, "utf8"));
|
|
2940
3437
|
} catch (error) {
|
|
2941
3438
|
throw new InitConfigurationError(
|
|
2942
3439
|
"unable to read init config: " + (error instanceof Error ? error.message : String(error)),
|
|
@@ -2978,8 +3475,192 @@ function serializeCliResult(result) {
|
|
|
2978
3475
|
return JSON.stringify(result) + "\n";
|
|
2979
3476
|
}
|
|
2980
3477
|
|
|
3478
|
+
// src/commands/refresh.ts
|
|
3479
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
3480
|
+
import { join as join14 } from "node:path";
|
|
3481
|
+
import { parse as parseYaml8 } from "yaml";
|
|
3482
|
+
async function detectProject(root) {
|
|
3483
|
+
let content;
|
|
3484
|
+
try {
|
|
3485
|
+
content = await readFile13(join14(root, ".harness", "project.yaml"), "utf8");
|
|
3486
|
+
} catch (error) {
|
|
3487
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
3488
|
+
return { status: "absent" };
|
|
3489
|
+
}
|
|
3490
|
+
throw error;
|
|
3491
|
+
}
|
|
3492
|
+
const parsed = projectConfigSchema.safeParse(parseYaml8(content));
|
|
3493
|
+
if (!parsed.success) {
|
|
3494
|
+
return { status: "invalid" };
|
|
3495
|
+
}
|
|
3496
|
+
return { status: "valid", config: parsed.data };
|
|
3497
|
+
}
|
|
3498
|
+
function parseProfile(value, current) {
|
|
3499
|
+
if (value === void 0 || value === "") {
|
|
3500
|
+
return current;
|
|
3501
|
+
}
|
|
3502
|
+
if (value === "1" || value === "general") return "general";
|
|
3503
|
+
if (value === "2" || value === "java") return "java";
|
|
3504
|
+
throw new Error("\u914D\u7F6E\u7C7B\u578B\u5FC5\u987B\u4E3A general \u6216 java");
|
|
3505
|
+
}
|
|
3506
|
+
function summarize(result) {
|
|
3507
|
+
const items = [
|
|
3508
|
+
...result.applied.map((item2) => ({ ...item2, status: result.dry_run ? "planned" : "applied" })),
|
|
3509
|
+
...result.removed.map((item2) => ({ ...item2, status: result.dry_run ? "planned" : "removed" })),
|
|
3510
|
+
...result.preserved.map((item2) => ({ ...item2, status: "preserved" })),
|
|
3511
|
+
...result.unchanged.map((item2) => ({ ...item2, status: "unchanged" }))
|
|
3512
|
+
];
|
|
3513
|
+
const exitCode = result.conflicts.length > 0 ? 5 : 0;
|
|
3514
|
+
return {
|
|
3515
|
+
schema_version: 1,
|
|
3516
|
+
command: "refresh",
|
|
3517
|
+
request_id: uuidV7(),
|
|
3518
|
+
dry_run: result.dry_run,
|
|
3519
|
+
ok: exitCode === 0,
|
|
3520
|
+
exit_code: exitCode,
|
|
3521
|
+
project_id: null,
|
|
3522
|
+
summary: {
|
|
3523
|
+
applied: result.dry_run ? 0 : result.applied.length,
|
|
3524
|
+
removed: result.dry_run ? 0 : result.removed.length,
|
|
3525
|
+
preserved: result.preserved.length,
|
|
3526
|
+
unchanged: result.unchanged.length,
|
|
3527
|
+
conflicts: result.conflicts.length
|
|
3528
|
+
},
|
|
3529
|
+
items,
|
|
3530
|
+
warnings: result.conflicts,
|
|
3531
|
+
errors: []
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
function renderProfileTransitionPreview(result) {
|
|
3535
|
+
const items = [
|
|
3536
|
+
...result.applied,
|
|
3537
|
+
...result.removed,
|
|
3538
|
+
...result.preserved,
|
|
3539
|
+
...result.unchanged
|
|
3540
|
+
];
|
|
3541
|
+
const actionLabel = {
|
|
3542
|
+
add: "\u65B0\u589E",
|
|
3543
|
+
replace: "\u66FF\u6362",
|
|
3544
|
+
delete: "\u5220\u9664",
|
|
3545
|
+
preserve: "\u4FDD\u7559",
|
|
3546
|
+
unchanged: "\u65E0\u9700\u53D8\u66F4"
|
|
3547
|
+
};
|
|
3548
|
+
const reasonLabel = {
|
|
3549
|
+
MISSING_TARGET: "\u76EE\u6807\u7F3A\u5931",
|
|
3550
|
+
BASELINE_CLEAN: "\u57FA\u7EBF\u672A\u4FEE\u6539",
|
|
3551
|
+
ALREADY_CURRENT: "\u5DF2\u662F\u6700\u65B0",
|
|
3552
|
+
LOCAL_MODIFICATION: "\u68C0\u6D4B\u5230\u672C\u5730\u4FEE\u6539",
|
|
3553
|
+
MALFORMED_MANAGED_BLOCK: "\u53D7\u7BA1\u533A\u5757\u683C\u5F0F\u5F02\u5E38",
|
|
3554
|
+
LEGACY_PROFILE_FILE_MODIFIED: "\u65E7\u914D\u7F6E\u6587\u4EF6\u5DF2\u4FEE\u6539",
|
|
3555
|
+
LEGACY_BASELINE_UNKNOWN: "\u65E7\u7248\u57FA\u7EBF\u672A\u77E5",
|
|
3556
|
+
FORCE_MANAGED: "\u5F3A\u5236\u66F4\u65B0"
|
|
3557
|
+
};
|
|
3558
|
+
return "\u914D\u7F6E\u5207\u6362\u9884\u89C8\uFF1A\n" + items.map((item2) => `- ${actionLabel[item2.action]}\uFF1A${item2.target_path}\uFF08${reasonLabel[item2.reason]}\uFF09`).join("\n") + "\n";
|
|
3559
|
+
}
|
|
3560
|
+
async function runRefresh(options, dependencies) {
|
|
3561
|
+
const requestId = uuidV7();
|
|
3562
|
+
const detection = await detectProject(dependencies.cwd);
|
|
3563
|
+
if (detection.status === "absent") {
|
|
3564
|
+
dependencies.stderr("\u5C1A\u672A\u521D\u59CB\u5316 Hunter Harness\uFF1B\u8BF7\u5148\u8FD0\u884C `hunter-harness`\u3002\n");
|
|
3565
|
+
return 3;
|
|
3566
|
+
}
|
|
3567
|
+
if (detection.status === "invalid") {
|
|
3568
|
+
dependencies.stderr("PROJECT_CONFIG_INVALID\uFF1A.harness/project.yaml \u65E0\u6548\n");
|
|
3569
|
+
return 3;
|
|
3570
|
+
}
|
|
3571
|
+
const currentProfile = detection.config.project.profiles[0] ?? "general";
|
|
3572
|
+
let targetProfile;
|
|
3573
|
+
try {
|
|
3574
|
+
targetProfile = parseProfile(options.profile, currentProfile);
|
|
3575
|
+
} catch (error) {
|
|
3576
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3577
|
+
dependencies.stderr(message + "\n");
|
|
3578
|
+
return 3;
|
|
3579
|
+
}
|
|
3580
|
+
const dryRun = options.dryRun === true;
|
|
3581
|
+
if (targetProfile !== currentProfile && !dryRun) {
|
|
3582
|
+
try {
|
|
3583
|
+
const preview = await refreshProject({
|
|
3584
|
+
projectRoot: dependencies.cwd,
|
|
3585
|
+
resourcesRoot: dependencies.resourcesRoot,
|
|
3586
|
+
profile: targetProfile,
|
|
3587
|
+
dryRun: true,
|
|
3588
|
+
forceManaged: options.forceManaged === true
|
|
3589
|
+
});
|
|
3590
|
+
const rendered = renderProfileTransitionPreview(preview);
|
|
3591
|
+
if (options.json === true) {
|
|
3592
|
+
dependencies.stderr(rendered);
|
|
3593
|
+
} else {
|
|
3594
|
+
dependencies.stdout(rendered);
|
|
3595
|
+
}
|
|
3596
|
+
} catch (error) {
|
|
3597
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3598
|
+
dependencies.stderr(message + "\n");
|
|
3599
|
+
return 1;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
if (options.confirmed !== true) {
|
|
3603
|
+
if (options.nonInteractive === true) {
|
|
3604
|
+
if (!options.yes && !dryRun) {
|
|
3605
|
+
dependencies.stderr("\u975E\u4EA4\u4E92\u6A21\u5F0F\u5237\u65B0\u9700\u8981 --yes\n");
|
|
3606
|
+
return 2;
|
|
3607
|
+
}
|
|
3608
|
+
} else if (!options.yes && !dryRun) {
|
|
3609
|
+
const label = targetProfile === currentProfile ? `\u5237\u65B0\u5F53\u524D\u914D\u7F6E\uFF08${currentProfile}\uFF09` : `\u5207\u6362\u914D\u7F6E\uFF1A${currentProfile} \u2192 ${targetProfile}`;
|
|
3610
|
+
const answer = await dependencies.prompt(`${label}\uFF1F[y/N]\uFF1A`);
|
|
3611
|
+
if (!/^(?:y|yes)$/i.test(answer.trim())) {
|
|
3612
|
+
return 2;
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
try {
|
|
3617
|
+
const result = await refreshProject({
|
|
3618
|
+
projectRoot: dependencies.cwd,
|
|
3619
|
+
resourcesRoot: dependencies.resourcesRoot,
|
|
3620
|
+
profile: targetProfile,
|
|
3621
|
+
dryRun,
|
|
3622
|
+
forceManaged: options.forceManaged === true
|
|
3623
|
+
});
|
|
3624
|
+
const output = summarize(result);
|
|
3625
|
+
if (options.json === true) {
|
|
3626
|
+
dependencies.stdout(serializeCliResult({ ...output, request_id: requestId }));
|
|
3627
|
+
} else {
|
|
3628
|
+
const parts = [];
|
|
3629
|
+
if (result.applied.length > 0) parts.push(`\u5DF2\u66F4\u65B0 ${result.applied.length} \u4E2A`);
|
|
3630
|
+
if (result.removed.length > 0) parts.push(`\u5DF2\u5220\u9664 ${result.removed.length} \u4E2A`);
|
|
3631
|
+
if (result.preserved.length > 0) parts.push(`\u5DF2\u4FDD\u7559 ${result.preserved.length} \u4E2A`);
|
|
3632
|
+
if (result.unchanged.length > 0) parts.push(`\u65E0\u9700\u53D8\u66F4 ${result.unchanged.length} \u4E2A`);
|
|
3633
|
+
dependencies.stdout(`Harness \u5237\u65B0\uFF08${targetProfile}\uFF09\uFF1A${parts.join("\uFF0C") || "\u6CA1\u6709\u53D8\u66F4"}\u3002
|
|
3634
|
+
`);
|
|
3635
|
+
}
|
|
3636
|
+
return output.exit_code;
|
|
3637
|
+
} catch (error) {
|
|
3638
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3639
|
+
dependencies.stderr(message + "\n");
|
|
3640
|
+
if (options.json === true) {
|
|
3641
|
+
dependencies.stdout(serializeCliResult({
|
|
3642
|
+
schema_version: 1,
|
|
3643
|
+
command: "refresh",
|
|
3644
|
+
request_id: requestId,
|
|
3645
|
+
dry_run: dryRun,
|
|
3646
|
+
ok: false,
|
|
3647
|
+
exit_code: 1,
|
|
3648
|
+
project_id: null,
|
|
3649
|
+
summary: { applied: 0, removed: 0, preserved: 0, unchanged: 0, conflicts: 0 },
|
|
3650
|
+
items: [],
|
|
3651
|
+
warnings: [],
|
|
3652
|
+
errors: [{ message }]
|
|
3653
|
+
}));
|
|
3654
|
+
}
|
|
3655
|
+
return 1;
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
|
|
2981
3659
|
// src/commands/configure.ts
|
|
2982
|
-
|
|
3660
|
+
function otherProfile(current) {
|
|
3661
|
+
return current === "general" ? "java" : "general";
|
|
3662
|
+
}
|
|
3663
|
+
async function runFirstInstall(options, dependencies) {
|
|
2983
3664
|
const requestId = uuidV7();
|
|
2984
3665
|
try {
|
|
2985
3666
|
const config = await resolveInitConfig(
|
|
@@ -2990,7 +3671,7 @@ async function runConfigure(options, dependencies) {
|
|
|
2990
3671
|
).then((answer) => answer.trim())
|
|
2991
3672
|
);
|
|
2992
3673
|
if (options.nonInteractive === true && options.yes !== true && options.dryRun !== true) {
|
|
2993
|
-
dependencies.stderr("
|
|
3674
|
+
dependencies.stderr("\u975E\u4EA4\u4E92\u6A21\u5F0F\u6267\u884C\u5199\u5165\u64CD\u4F5C\u9700\u8981 --yes\n");
|
|
2994
3675
|
return 2;
|
|
2995
3676
|
}
|
|
2996
3677
|
const result = await initializeProject({
|
|
@@ -3012,7 +3693,7 @@ async function runConfigure(options, dependencies) {
|
|
|
3012
3693
|
warnings: [],
|
|
3013
3694
|
errors: []
|
|
3014
3695
|
};
|
|
3015
|
-
dependencies.stdout(options.json === true ? serializeCliResult(output) : "Hunter Harness
|
|
3696
|
+
dependencies.stdout(options.json === true ? serializeCliResult(output) : "Hunter Harness \u521D\u59CB\u5316\u5B8C\u6210\uFF0C\u5171\u5904\u7406 " + result.paths.length + " \u4E2A\u6587\u4EF6\u3002\n");
|
|
3016
3697
|
return 0;
|
|
3017
3698
|
} catch (error) {
|
|
3018
3699
|
const exitCode = error instanceof InitConfigurationError ? error.exitCode : 1;
|
|
@@ -3036,12 +3717,129 @@ async function runConfigure(options, dependencies) {
|
|
|
3036
3717
|
return exitCode;
|
|
3037
3718
|
}
|
|
3038
3719
|
}
|
|
3720
|
+
async function runExistingProject(options, dependencies, currentProfile) {
|
|
3721
|
+
const refreshOptions = {};
|
|
3722
|
+
if (options.profile !== void 0) refreshOptions.profile = options.profile;
|
|
3723
|
+
if (options.nonInteractive !== void 0) refreshOptions.nonInteractive = options.nonInteractive;
|
|
3724
|
+
if (options.yes !== void 0) refreshOptions.yes = options.yes;
|
|
3725
|
+
if (options.dryRun !== void 0) refreshOptions.dryRun = options.dryRun;
|
|
3726
|
+
if (options.json !== void 0) refreshOptions.json = options.json;
|
|
3727
|
+
if (options.forceManaged !== void 0) refreshOptions.forceManaged = options.forceManaged;
|
|
3728
|
+
if (options.nonInteractive === true) {
|
|
3729
|
+
return runRefresh(refreshOptions, dependencies);
|
|
3730
|
+
}
|
|
3731
|
+
const menu = await dependencies.prompt(
|
|
3732
|
+
`Hunter Harness \u5DF2\u521D\u59CB\u5316\uFF08profile: ${currentProfile}\uFF09\u3002
|
|
3733
|
+
1. \u5237\u65B0\u5F53\u524D\u914D\u7F6E\uFF08\u9ED8\u8BA4\u4E14\u63A8\u8350\uFF09
|
|
3734
|
+
2. \u5207\u6362\u5230\u53E6\u4E00\u79CD\u914D\u7F6E
|
|
3735
|
+
3. \u53D6\u6D88
|
|
3736
|
+
\u8BF7\u9009\u62E9 [1]: `
|
|
3737
|
+
);
|
|
3738
|
+
const choice = menu.trim();
|
|
3739
|
+
if (choice === "3" || /^c/i.test(choice)) {
|
|
3740
|
+
return 2;
|
|
3741
|
+
}
|
|
3742
|
+
if (choice === "2" || /^s/i.test(choice)) {
|
|
3743
|
+
refreshOptions.profile = otherProfile(currentProfile);
|
|
3744
|
+
} else {
|
|
3745
|
+
refreshOptions.profile = currentProfile;
|
|
3746
|
+
}
|
|
3747
|
+
refreshOptions.confirmed = true;
|
|
3748
|
+
return runRefresh(refreshOptions, dependencies);
|
|
3749
|
+
}
|
|
3750
|
+
async function runConfigure(options, dependencies) {
|
|
3751
|
+
const detection = await detectProject(dependencies.cwd);
|
|
3752
|
+
if (detection.status === "invalid") {
|
|
3753
|
+
dependencies.stderr("PROJECT_CONFIG_INVALID: .harness/project.yaml is invalid; not initializing over it.\n");
|
|
3754
|
+
if (options.json === true) {
|
|
3755
|
+
dependencies.stdout(serializeCliResult({
|
|
3756
|
+
schema_version: 1,
|
|
3757
|
+
command: "configure",
|
|
3758
|
+
request_id: uuidV7(),
|
|
3759
|
+
dry_run: options.dryRun === true,
|
|
3760
|
+
ok: false,
|
|
3761
|
+
exit_code: 3,
|
|
3762
|
+
project_id: null,
|
|
3763
|
+
summary: { planned: 0, applied: 0 },
|
|
3764
|
+
items: [],
|
|
3765
|
+
warnings: [],
|
|
3766
|
+
errors: [{ code: "PROJECT_CONFIG_INVALID", message: "project.yaml is invalid" }]
|
|
3767
|
+
}));
|
|
3768
|
+
}
|
|
3769
|
+
return 3;
|
|
3770
|
+
}
|
|
3771
|
+
if (detection.status === "valid") {
|
|
3772
|
+
const currentProfile = detection.config.project.profiles[0] ?? "general";
|
|
3773
|
+
return runExistingProject(options, dependencies, currentProfile);
|
|
3774
|
+
}
|
|
3775
|
+
return runFirstInstall(options, dependencies);
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
// src/commands/cleanup.ts
|
|
3779
|
+
async function runCleanup(options, dependencies) {
|
|
3780
|
+
const requestId = uuidV7();
|
|
3781
|
+
const dryRun = options.dryRun === true;
|
|
3782
|
+
if (options.nonInteractive === true && options.yes !== true && !dryRun) {
|
|
3783
|
+
dependencies.stderr("\u975E\u4EA4\u4E92\u6A21\u5F0F\u6E05\u7406\u9700\u8981 --yes\n");
|
|
3784
|
+
return 2;
|
|
3785
|
+
}
|
|
3786
|
+
try {
|
|
3787
|
+
const result = await cleanupProject({ projectRoot: dependencies.cwd, dryRun });
|
|
3788
|
+
const output = {
|
|
3789
|
+
schema_version: 1,
|
|
3790
|
+
command: "cleanup",
|
|
3791
|
+
request_id: requestId,
|
|
3792
|
+
dry_run: result.dry_run,
|
|
3793
|
+
ok: true,
|
|
3794
|
+
exit_code: 0,
|
|
3795
|
+
project_id: null,
|
|
3796
|
+
summary: {
|
|
3797
|
+
pruned_transactions: result.pruned_transactions.length,
|
|
3798
|
+
removed_cache: result.removed_cache.length
|
|
3799
|
+
},
|
|
3800
|
+
items: [
|
|
3801
|
+
...result.pruned_transactions.map((id) => ({ path: id, status: "pruned" })),
|
|
3802
|
+
...result.removed_cache.map((id) => ({ path: id, status: "removed" }))
|
|
3803
|
+
],
|
|
3804
|
+
warnings: [],
|
|
3805
|
+
errors: []
|
|
3806
|
+
};
|
|
3807
|
+
if (options.json === true) {
|
|
3808
|
+
dependencies.stdout(serializeCliResult(output));
|
|
3809
|
+
} else {
|
|
3810
|
+
dependencies.stdout(
|
|
3811
|
+
`Harness \u6E05\u7406\u5B8C\u6210\uFF1A\u5DF2\u88C1\u526A ${result.pruned_transactions.length} \u4E2A\u4E8B\u52A1\uFF0C\u5DF2\u5220\u9664 ${result.removed_cache.length} \u4E2A\u7F13\u5B58\u6761\u76EE\u3002
|
|
3812
|
+
`
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
return 0;
|
|
3816
|
+
} catch (error) {
|
|
3817
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3818
|
+
dependencies.stderr(message + "\n");
|
|
3819
|
+
if (options.json === true) {
|
|
3820
|
+
dependencies.stdout(serializeCliResult({
|
|
3821
|
+
schema_version: 1,
|
|
3822
|
+
command: "cleanup",
|
|
3823
|
+
request_id: requestId,
|
|
3824
|
+
dry_run: dryRun,
|
|
3825
|
+
ok: false,
|
|
3826
|
+
exit_code: 1,
|
|
3827
|
+
project_id: null,
|
|
3828
|
+
summary: { pruned_transactions: 0, removed_cache: 0 },
|
|
3829
|
+
items: [],
|
|
3830
|
+
warnings: [],
|
|
3831
|
+
errors: [{ message }]
|
|
3832
|
+
}));
|
|
3833
|
+
}
|
|
3834
|
+
return 1;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3039
3837
|
|
|
3040
3838
|
// src/commands/push.ts
|
|
3041
3839
|
async function runPush(options, dependencies) {
|
|
3042
3840
|
const requestId = uuidV7();
|
|
3043
3841
|
if (options.nonInteractive === true && options.yes !== true && options.dryRun !== true) {
|
|
3044
|
-
dependencies.stderr("
|
|
3842
|
+
dependencies.stderr("\u975E\u4EA4\u4E92\u6A21\u5F0F\u63A8\u9001\u9700\u8981 --yes\n");
|
|
3045
3843
|
return 2;
|
|
3046
3844
|
}
|
|
3047
3845
|
try {
|
|
@@ -3120,7 +3918,7 @@ async function runPush(options, dependencies) {
|
|
|
3120
3918
|
async function runUpdate(options, dependencies) {
|
|
3121
3919
|
const requestId = uuidV7();
|
|
3122
3920
|
if (options.nonInteractive === true && options.yes !== true && options.dryRun !== true) {
|
|
3123
|
-
dependencies.stderr("
|
|
3921
|
+
dependencies.stderr("\u975E\u4EA4\u4E92\u6A21\u5F0F\u66F4\u65B0\u9700\u8981 --yes\n");
|
|
3124
3922
|
return 2;
|
|
3125
3923
|
}
|
|
3126
3924
|
const execute = async (dryRun) => updateProject({
|
|
@@ -3136,7 +3934,7 @@ async function runUpdate(options, dependencies) {
|
|
|
3136
3934
|
if (options.dryRun !== true && options.yes !== true && options.nonInteractive !== true) {
|
|
3137
3935
|
const preview = await execute(true);
|
|
3138
3936
|
const answer = await dependencies.prompt(
|
|
3139
|
-
"
|
|
3937
|
+
"\u5E94\u7528 " + preview.applied.length + " \u4E2A\u53EF\u66F4\u65B0\u6761\u76EE\uFF1F[y/N]\uFF1A"
|
|
3140
3938
|
);
|
|
3141
3939
|
if (!/^(?:y|yes)$/i.test(answer.trim())) {
|
|
3142
3940
|
return 2;
|
|
@@ -3146,7 +3944,7 @@ async function runUpdate(options, dependencies) {
|
|
|
3146
3944
|
result = await execute(options.dryRun === true);
|
|
3147
3945
|
}
|
|
3148
3946
|
const applied = new Set(result.applied);
|
|
3149
|
-
const skippedByPath = new Map(result.skipped.map((
|
|
3947
|
+
const skippedByPath = new Map(result.skipped.map((item2) => [item2.path, item2]));
|
|
3150
3948
|
const items = result.operations.map((operation) => {
|
|
3151
3949
|
const path = operation.operation === "rename" ? operation.to_path : operation.path;
|
|
3152
3950
|
const skipped = skippedByPath.get(path);
|
|
@@ -3179,7 +3977,7 @@ async function runUpdate(options, dependencies) {
|
|
|
3179
3977
|
warnings: result.skipped,
|
|
3180
3978
|
errors: []
|
|
3181
3979
|
};
|
|
3182
|
-
dependencies.stdout(options.json === true ? serializeCliResult(output) : result.artifactId === null ? "
|
|
3980
|
+
dependencies.stdout(options.json === true ? serializeCliResult(output) : result.artifactId === null ? "\u6CA1\u6709\u53EF\u5E94\u7528\u7684\u5DF2\u6279\u51C6\u66F4\u65B0\u3002\n" : "\u66F4\u65B0\u5B8C\u6210\uFF1A\u5DF2\u5E94\u7528 " + result.applied.length + " \u4E2A\u6761\u76EE\uFF0C\u8DF3\u8FC7 " + result.skipped.length + " \u4E2A\u3002\n");
|
|
3183
3981
|
return exitCode;
|
|
3184
3982
|
} catch (error) {
|
|
3185
3983
|
const exitCode = error instanceof UpdateWorkflowError ? error.exitCode : 1;
|
|
@@ -3208,11 +4006,11 @@ async function runUpdate(options, dependencies) {
|
|
|
3208
4006
|
}
|
|
3209
4007
|
|
|
3210
4008
|
// src/commands/recovery.ts
|
|
3211
|
-
import { stat as
|
|
3212
|
-
import { join as
|
|
3213
|
-
async function
|
|
4009
|
+
import { stat as stat5 } from "node:fs/promises";
|
|
4010
|
+
import { join as join15 } from "node:path";
|
|
4011
|
+
async function exists5(path) {
|
|
3214
4012
|
try {
|
|
3215
|
-
await
|
|
4013
|
+
await stat5(path);
|
|
3216
4014
|
return true;
|
|
3217
4015
|
} catch {
|
|
3218
4016
|
return false;
|
|
@@ -3225,16 +4023,16 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
|
|
|
3225
4023
|
const pending = await pendingTransactions(dependencies.cwd);
|
|
3226
4024
|
if (pending.length > 0) {
|
|
3227
4025
|
if (options.nonInteractive === true) {
|
|
3228
|
-
dependencies.stderr("
|
|
4026
|
+
dependencies.stderr("\u5B58\u5728\u672A\u5B8C\u6210\u4E8B\u52A1\uFF0C\u9700\u8981\u4EE5\u4EA4\u4E92\u6A21\u5F0F\u6062\u590D\u3002\n");
|
|
3229
4027
|
return 5;
|
|
3230
4028
|
}
|
|
3231
4029
|
const answer2 = await dependencies.prompt([
|
|
3232
|
-
"
|
|
3233
|
-
"1.
|
|
3234
|
-
"2.
|
|
3235
|
-
"3.
|
|
3236
|
-
"4.
|
|
3237
|
-
"
|
|
4030
|
+
"\u68C0\u6D4B\u5230\u672A\u5B8C\u6210\u7684 Hunter Harness \u4E8B\u52A1\u3002",
|
|
4031
|
+
"1. \u6062\u590D\u6700\u8FD1\u4E00\u6B21\u5931\u8D25\u7684\u66F4\u65B0",
|
|
4032
|
+
"2. \u56DE\u6EDA\u6700\u8FD1\u4E00\u6B21\u5DF2\u63D0\u4EA4\u7684\u66F4\u65B0",
|
|
4033
|
+
"3. \u67E5\u770B\u4E8B\u52A1\u72B6\u6001",
|
|
4034
|
+
"4. \u6E05\u7406\u65E7\u4E8B\u52A1",
|
|
4035
|
+
"\u8BF7\u9009\u62E9 [1-4]\uFF1A"
|
|
3238
4036
|
].join("\n"));
|
|
3239
4037
|
try {
|
|
3240
4038
|
if (answer2.trim() === "1") {
|
|
@@ -3242,12 +4040,12 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
|
|
|
3242
4040
|
dependencies.cwd,
|
|
3243
4041
|
pending[0]?.transactionId ?? ""
|
|
3244
4042
|
);
|
|
3245
|
-
dependencies.stdout("
|
|
4043
|
+
dependencies.stdout("\u6062\u590D\u5B8C\u6210\uFF1A" + result.status + "\u3002\n");
|
|
3246
4044
|
return 0;
|
|
3247
4045
|
}
|
|
3248
4046
|
if (answer2.trim() === "2") {
|
|
3249
4047
|
const result = await rollbackLatestCommittedUpdate(dependencies.cwd);
|
|
3250
|
-
dependencies.stdout("
|
|
4048
|
+
dependencies.stdout("\u56DE\u6EDA\u5B8C\u6210\uFF1A" + result.status + "\u3002\n");
|
|
3251
4049
|
return 0;
|
|
3252
4050
|
}
|
|
3253
4051
|
if (answer2.trim() === "3") {
|
|
@@ -3260,7 +4058,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
|
|
|
3260
4058
|
}
|
|
3261
4059
|
if (answer2.trim() === "4") {
|
|
3262
4060
|
const removed = await cleanupOldTransactions(dependencies.cwd);
|
|
3263
|
-
dependencies.stdout("
|
|
4061
|
+
dependencies.stdout("\u5DF2\u6E05\u7406 " + removed.length + " \u4E2A\u65E7\u4E8B\u52A1\u3002\n");
|
|
3264
4062
|
return 0;
|
|
3265
4063
|
}
|
|
3266
4064
|
return 2;
|
|
@@ -3269,18 +4067,18 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
|
|
|
3269
4067
|
return 5;
|
|
3270
4068
|
}
|
|
3271
4069
|
}
|
|
3272
|
-
const initialized = await
|
|
4070
|
+
const initialized = await exists5(join15(dependencies.cwd, ".harness", "project.yaml"));
|
|
3273
4071
|
if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
|
|
3274
4072
|
return null;
|
|
3275
4073
|
}
|
|
3276
4074
|
const answer = await dependencies.prompt([
|
|
3277
|
-
"Hunter Harness
|
|
3278
|
-
"1.
|
|
3279
|
-
"2.
|
|
3280
|
-
"3.
|
|
3281
|
-
"4.
|
|
3282
|
-
"5.
|
|
3283
|
-
"
|
|
4075
|
+
"Hunter Harness \u9879\u76EE\u83DC\u5355\u3002",
|
|
4076
|
+
"1. \u914D\u7F6E\u9879\u76EE",
|
|
4077
|
+
"2. \u56DE\u6EDA\u6700\u8FD1\u4E00\u6B21\u66F4\u65B0",
|
|
4078
|
+
"3. \u6E05\u7406\u65E7\u4E8B\u52A1",
|
|
4079
|
+
"4. \u67E5\u770B\u4E8B\u52A1\u72B6\u6001",
|
|
4080
|
+
"5. \u9000\u51FA",
|
|
4081
|
+
"\u8BF7\u9009\u62E9 [1-5]\uFF1A"
|
|
3284
4082
|
].join("\n"));
|
|
3285
4083
|
try {
|
|
3286
4084
|
if (answer.trim() === "1") {
|
|
@@ -3288,12 +4086,12 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
|
|
|
3288
4086
|
}
|
|
3289
4087
|
if (answer.trim() === "2") {
|
|
3290
4088
|
const result = await rollbackLatestCommittedUpdate(dependencies.cwd);
|
|
3291
|
-
dependencies.stdout("
|
|
4089
|
+
dependencies.stdout("\u56DE\u6EDA\u5B8C\u6210\uFF1A" + result.status + "\u3002\n");
|
|
3292
4090
|
return 0;
|
|
3293
4091
|
}
|
|
3294
4092
|
if (answer.trim() === "3") {
|
|
3295
4093
|
const removed = await cleanupOldTransactions(dependencies.cwd);
|
|
3296
|
-
dependencies.stdout("
|
|
4094
|
+
dependencies.stdout("\u5DF2\u6E05\u7406 " + removed.length + " \u4E2A\u65E7\u4E8B\u52A1\u3002\n");
|
|
3297
4095
|
return 0;
|
|
3298
4096
|
}
|
|
3299
4097
|
if (answer.trim() === "4") {
|
|
@@ -3337,7 +4135,7 @@ function addCommonOptions(command) {
|
|
|
3337
4135
|
}
|
|
3338
4136
|
async function runCli(argv, overrides = {}) {
|
|
3339
4137
|
const dependencies = defaultDependencies(overrides);
|
|
3340
|
-
const program = addCommonOptions(new Command()).name("hunter-harness").description("Local-first, server-governed agent harness").option("--profile <name>").option("--config <file>").showHelpAfterError().exitOverride().configureOutput({
|
|
4138
|
+
const program = addCommonOptions(new Command()).name("hunter-harness").description("Local-first, server-governed agent harness").option("--profile <name>").option("--config <file>").option("--force-managed").showHelpAfterError().exitOverride().configureOutput({
|
|
3341
4139
|
writeOut: dependencies.stdout,
|
|
3342
4140
|
writeErr: dependencies.stderr
|
|
3343
4141
|
});
|
|
@@ -3350,15 +4148,27 @@ async function runCli(argv, overrides = {}) {
|
|
|
3350
4148
|
}
|
|
3351
4149
|
exitCode = await runConfigure(options, dependencies);
|
|
3352
4150
|
});
|
|
3353
|
-
addCommonOptions(program.command("
|
|
4151
|
+
addCommonOptions(program.command("refresh")).description("\u672C\u5730\u4FDD\u5B88\u5237\u65B0\u5DF2\u5B89\u88C5\u7684 Harness \u9879\u76EE").option("--profile <name>").option("--force-managed").action(async (options) => {
|
|
4152
|
+
exitCode = await runRefresh(
|
|
4153
|
+
{ ...program.opts(), ...options },
|
|
4154
|
+
dependencies
|
|
4155
|
+
);
|
|
4156
|
+
});
|
|
4157
|
+
addCommonOptions(program.command("update")).description("\u5E94\u7528\u5DF2\u6279\u51C6\u7684\u670D\u52A1\u7AEF\u4EA7\u7269").action(async (options) => {
|
|
3354
4158
|
exitCode = await runUpdate(
|
|
3355
4159
|
{ ...program.opts(), ...options },
|
|
3356
4160
|
dependencies
|
|
3357
4161
|
);
|
|
3358
4162
|
});
|
|
3359
|
-
addCommonOptions(program.command("push")).description("
|
|
4163
|
+
addCommonOptions(program.command("push")).description("\u521B\u5EFA\u53D7\u6CBB\u7406\u7684\u53D8\u66F4\u63D0\u6848").action(async (options) => {
|
|
3360
4164
|
exitCode = await runPush({ ...program.opts(), ...options }, dependencies);
|
|
3361
4165
|
});
|
|
4166
|
+
addCommonOptions(program.command("cleanup")).description("\u6E05\u7406\u5DF2\u5B8C\u6210\u4E8B\u52A1\u548C\u8FC7\u671F\u670D\u52A1\u7AEF\u7F13\u5B58").action(async (options) => {
|
|
4167
|
+
exitCode = await runCleanup(
|
|
4168
|
+
{ ...program.opts(), ...options },
|
|
4169
|
+
dependencies
|
|
4170
|
+
);
|
|
4171
|
+
});
|
|
3362
4172
|
try {
|
|
3363
4173
|
await program.parseAsync(["node", "hunter-harness", ...argv]);
|
|
3364
4174
|
return exitCode;
|