@uzysjung/agent-harness 26.131.1 → 26.133.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/dist/index.js +517 -369
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -712,7 +712,7 @@ var cac = (name = "") => new CAC(name);
|
|
|
712
712
|
// package.json
|
|
713
713
|
var package_default = {
|
|
714
714
|
name: "@uzysjung/agent-harness",
|
|
715
|
-
version: "26.
|
|
715
|
+
version: "26.133.0",
|
|
716
716
|
description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
|
|
717
717
|
type: "module",
|
|
718
718
|
publishConfig: {
|
|
@@ -906,18 +906,18 @@ function padDisplay(s, width) {
|
|
|
906
906
|
init_esm_shims();
|
|
907
907
|
import {
|
|
908
908
|
chmodSync as chmodSync2,
|
|
909
|
-
existsSync as
|
|
910
|
-
mkdirSync as
|
|
909
|
+
existsSync as existsSync13,
|
|
910
|
+
mkdirSync as mkdirSync6,
|
|
911
911
|
readdirSync as readdirSync4,
|
|
912
|
-
readFileSync as
|
|
913
|
-
writeFileSync as
|
|
912
|
+
readFileSync as readFileSync12,
|
|
913
|
+
writeFileSync as writeFileSync7
|
|
914
914
|
} from "fs";
|
|
915
|
-
import { basename as basename4, dirname as
|
|
915
|
+
import { basename as basename4, dirname as dirname6, join as join11, resolve } from "path";
|
|
916
916
|
|
|
917
917
|
// src/antigravity/transform.ts
|
|
918
918
|
init_esm_shims();
|
|
919
|
-
import { existsSync as
|
|
920
|
-
import { basename, join as
|
|
919
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
920
|
+
import { basename, join as join3 } from "path";
|
|
921
921
|
|
|
922
922
|
// src/codex/agents-md.ts
|
|
923
923
|
init_esm_shims();
|
|
@@ -960,6 +960,11 @@ function portBody(body) {
|
|
|
960
960
|
return renameSlashes(body).replace(/CLAUDE_PROJECT_DIR/g, "CODEX_PROJECT_DIR").trimEnd();
|
|
961
961
|
}
|
|
962
962
|
|
|
963
|
+
// src/owned-write.ts
|
|
964
|
+
init_esm_shims();
|
|
965
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
966
|
+
import { dirname as dirname3, relative, sep } from "path";
|
|
967
|
+
|
|
963
968
|
// src/fs-ops.ts
|
|
964
969
|
init_esm_shims();
|
|
965
970
|
import {
|
|
@@ -1050,6 +1055,218 @@ function ensureProjectSkeleton(projectDir) {
|
|
|
1050
1055
|
}
|
|
1051
1056
|
}
|
|
1052
1057
|
|
|
1058
|
+
// src/install-log.ts
|
|
1059
|
+
init_esm_shims();
|
|
1060
|
+
import { createHash } from "crypto";
|
|
1061
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1062
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1063
|
+
var INSTALL_LOG_FILENAME = ".harness-install.json";
|
|
1064
|
+
var INSTALL_LOG_VERSION = 1;
|
|
1065
|
+
var POLICY_DIRS = [
|
|
1066
|
+
{ dir: "rules", ext: ".md" },
|
|
1067
|
+
{ dir: "agents", ext: ".md" },
|
|
1068
|
+
{ dir: "commands/uzys", ext: ".md" },
|
|
1069
|
+
{ dir: "hooks", ext: ".sh" }
|
|
1070
|
+
];
|
|
1071
|
+
function buildAssetEntries(report, scope) {
|
|
1072
|
+
if (!report) return [];
|
|
1073
|
+
return report.attempted.filter((r) => r.ok).map((r) => assetToLogEntry(r.asset, scope, r.version));
|
|
1074
|
+
}
|
|
1075
|
+
function assetToLogEntry(asset, scope, version) {
|
|
1076
|
+
const detail = methodDetail(asset.method);
|
|
1077
|
+
const entry = {
|
|
1078
|
+
id: asset.id,
|
|
1079
|
+
category: asset.category,
|
|
1080
|
+
method: asset.method.kind,
|
|
1081
|
+
scope,
|
|
1082
|
+
detail
|
|
1083
|
+
};
|
|
1084
|
+
if (version) entry.version = version;
|
|
1085
|
+
return entry;
|
|
1086
|
+
}
|
|
1087
|
+
function methodDetail(method) {
|
|
1088
|
+
switch (method.kind) {
|
|
1089
|
+
case "plugin":
|
|
1090
|
+
return { marketplace: method.marketplace, pluginId: method.pluginId };
|
|
1091
|
+
case "skill":
|
|
1092
|
+
return { source: method.source, ...method.skill ? { skill: method.skill } : {} };
|
|
1093
|
+
case "npm":
|
|
1094
|
+
return { pkg: method.pkg };
|
|
1095
|
+
case "npx-run":
|
|
1096
|
+
return { cmd: method.cmd, args: (method.args ?? []).join(" ") };
|
|
1097
|
+
case "shell-script":
|
|
1098
|
+
return { script: method.script, args: method.args.join(" ") };
|
|
1099
|
+
case "internal":
|
|
1100
|
+
return { key: method.key };
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
|
|
1104
|
+
const templates = {
|
|
1105
|
+
claudeDir: ".claude/",
|
|
1106
|
+
...spec.cli.includes("codex") ? { codexDir: ".codex/" } : {},
|
|
1107
|
+
...spec.cli.includes("opencode") ? { opencodeDir: ".opencode/" } : {},
|
|
1108
|
+
...rootClaudeMd ? { rootClaudeMd } : {}
|
|
1109
|
+
};
|
|
1110
|
+
const log = {
|
|
1111
|
+
schemaVersion: INSTALL_LOG_VERSION,
|
|
1112
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1113
|
+
scope,
|
|
1114
|
+
spec: {
|
|
1115
|
+
tracks: spec.tracks,
|
|
1116
|
+
cli: spec.cli
|
|
1117
|
+
},
|
|
1118
|
+
// 이번 설치가 만든 항목이 이기고, 이번에 안 만든 항목은 이전 값을 그대로 둔다.
|
|
1119
|
+
// (예: claude 로 깔고 나중에 codex 만 추가 설치해도 root CLAUDE.md 기록이 살아남는다)
|
|
1120
|
+
templates: { ...previous?.templates, ...templates },
|
|
1121
|
+
assets: mergeAssets(
|
|
1122
|
+
claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
|
|
1123
|
+
buildAssetEntries(external, scope)
|
|
1124
|
+
)
|
|
1125
|
+
};
|
|
1126
|
+
const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
|
|
1127
|
+
if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
|
|
1128
|
+
return log;
|
|
1129
|
+
}
|
|
1130
|
+
function mergeRootFiles(previous, current) {
|
|
1131
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1132
|
+
for (const file of [...previous ?? [], ...current]) {
|
|
1133
|
+
const prior = byPath.get(file.path);
|
|
1134
|
+
byPath.set(
|
|
1135
|
+
file.path,
|
|
1136
|
+
prior ? {
|
|
1137
|
+
path: file.path,
|
|
1138
|
+
change: prior.change === "created" ? "created" : file.change,
|
|
1139
|
+
notes: [.../* @__PURE__ */ new Set([...prior.notes, ...file.notes])]
|
|
1140
|
+
} : file
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
return [...byPath.values()];
|
|
1144
|
+
}
|
|
1145
|
+
function survivesClaudeDirRename(asset) {
|
|
1146
|
+
if (asset.scope === "global") return true;
|
|
1147
|
+
switch (asset.method) {
|
|
1148
|
+
case "plugin":
|
|
1149
|
+
return true;
|
|
1150
|
+
// `~/.claude/plugins/cache` — 프로젝트 밖
|
|
1151
|
+
case "npm":
|
|
1152
|
+
return true;
|
|
1153
|
+
// `node_modules/`
|
|
1154
|
+
case "skill":
|
|
1155
|
+
return false;
|
|
1156
|
+
// `npx skills add` project scope → `.claude/skills/`
|
|
1157
|
+
case "shell-script":
|
|
1158
|
+
return false;
|
|
1159
|
+
// ecc-prune → `.claude/local-plugins/`
|
|
1160
|
+
case "npx-run":
|
|
1161
|
+
return false;
|
|
1162
|
+
case "internal":
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function mergeAssets(previous, current) {
|
|
1167
|
+
if (!previous || previous.length === 0) return [...current];
|
|
1168
|
+
const currentById = new Map(current.map((a) => [a.id, a]));
|
|
1169
|
+
const previousIds = new Set(previous.map((a) => a.id));
|
|
1170
|
+
return [
|
|
1171
|
+
...previous.map((a) => currentById.get(a.id) ?? a),
|
|
1172
|
+
...current.filter((a) => !previousIds.has(a.id))
|
|
1173
|
+
];
|
|
1174
|
+
}
|
|
1175
|
+
function hashContent(content) {
|
|
1176
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1177
|
+
}
|
|
1178
|
+
function isHarnessOwned(baseline, key, current) {
|
|
1179
|
+
const recorded = baseline.get(key);
|
|
1180
|
+
return recorded !== void 0 && recorded === hashContent(current);
|
|
1181
|
+
}
|
|
1182
|
+
function mergeExternalFiles(projectDir, previous, current) {
|
|
1183
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1184
|
+
for (const file of [...previous ?? [], ...current]) byPath.set(file.path, file);
|
|
1185
|
+
return [...byPath.values()].filter((f) => existsSync2(join2(projectDir, f.path)));
|
|
1186
|
+
}
|
|
1187
|
+
function collectSkillHashes(projectDir) {
|
|
1188
|
+
const skillsDir = join2(projectDir, ".claude/skills");
|
|
1189
|
+
return listFilesRecursive(skillsDir).map((rel) => ({
|
|
1190
|
+
path: rel,
|
|
1191
|
+
sha256: hashContent(readFileSync2(join2(skillsDir, rel), "utf8"))
|
|
1192
|
+
}));
|
|
1193
|
+
}
|
|
1194
|
+
function collectPolicyHashes(projectDir, templatesDir) {
|
|
1195
|
+
const out = [];
|
|
1196
|
+
for (const { dir, ext } of POLICY_DIRS) {
|
|
1197
|
+
const targetDir = join2(projectDir, ".claude", dir);
|
|
1198
|
+
const sourceDir = join2(templatesDir, dir);
|
|
1199
|
+
for (const rel of listFilesRecursive(targetDir)) {
|
|
1200
|
+
if (!rel.endsWith(ext)) continue;
|
|
1201
|
+
if (!existsSync2(join2(sourceDir, rel))) continue;
|
|
1202
|
+
out.push({
|
|
1203
|
+
path: `${dir}/${rel}`,
|
|
1204
|
+
sha256: hashContent(readFileSync2(join2(targetDir, rel), "utf8"))
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return out;
|
|
1209
|
+
}
|
|
1210
|
+
function writeInstallLog(projectDir, log) {
|
|
1211
|
+
const path = join2(projectDir, ".claude", INSTALL_LOG_FILENAME);
|
|
1212
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1213
|
+
writeFileSync(path, `${JSON.stringify(log, null, 2)}
|
|
1214
|
+
`, "utf8");
|
|
1215
|
+
return path;
|
|
1216
|
+
}
|
|
1217
|
+
function readInstallLog(projectDir) {
|
|
1218
|
+
const path = join2(projectDir, ".claude", INSTALL_LOG_FILENAME);
|
|
1219
|
+
if (!existsSync2(path)) return null;
|
|
1220
|
+
try {
|
|
1221
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1222
|
+
if (Array.isArray(parsed.assets)) {
|
|
1223
|
+
parsed.assets = parsed.assets.map(
|
|
1224
|
+
(a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
return parsed;
|
|
1228
|
+
} catch {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function installLogPath(projectDir) {
|
|
1233
|
+
return join2(projectDir, ".claude", INSTALL_LOG_FILENAME);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/owned-write.ts
|
|
1237
|
+
function createOwnedWriter(projectDir, baseline, now = /* @__PURE__ */ new Date()) {
|
|
1238
|
+
const written = /* @__PURE__ */ new Map();
|
|
1239
|
+
const backedUp = [];
|
|
1240
|
+
const backupPaths = [];
|
|
1241
|
+
return {
|
|
1242
|
+
write(absPath, content) {
|
|
1243
|
+
const rel = relative(projectDir, absPath).split(sep).join("/");
|
|
1244
|
+
const digest = hashContent(content);
|
|
1245
|
+
if (!existsSync3(absPath)) {
|
|
1246
|
+
mkdirSync3(dirname3(absPath), { recursive: true });
|
|
1247
|
+
writeFileSync2(absPath, content);
|
|
1248
|
+
} else {
|
|
1249
|
+
const current = readFileSync3(absPath, "utf8");
|
|
1250
|
+
if (current !== content) {
|
|
1251
|
+
if (!isHarnessOwned(baseline, rel, current)) {
|
|
1252
|
+
backupPaths.push(backupFile(absPath, now));
|
|
1253
|
+
backedUp.push(rel);
|
|
1254
|
+
}
|
|
1255
|
+
writeFileSync2(absPath, content);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
written.set(rel, digest);
|
|
1259
|
+
},
|
|
1260
|
+
result() {
|
|
1261
|
+
return {
|
|
1262
|
+
files: [...written].map(([path, sha256]) => ({ path, sha256 })),
|
|
1263
|
+
backedUp: [...backedUp],
|
|
1264
|
+
backupPaths: [...backupPaths]
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1053
1270
|
// src/project-claude-merge.ts
|
|
1054
1271
|
init_esm_shims();
|
|
1055
1272
|
var TRACK_DISPLAY_NAMES = {
|
|
@@ -1144,48 +1361,48 @@ function expandTracks(tracks) {
|
|
|
1144
1361
|
|
|
1145
1362
|
// src/antigravity/transform.ts
|
|
1146
1363
|
function runAntigravityTransform(params) {
|
|
1147
|
-
const { harnessRoot, projectDir, selectedInternalSkills = [] } = params;
|
|
1148
|
-
const
|
|
1364
|
+
const { harnessRoot, projectDir, selectedInternalSkills = [], baseline } = params;
|
|
1365
|
+
const writer = createOwnedWriter(projectDir, baseline);
|
|
1366
|
+
const rulesFile = writeRules(harnessRoot, projectDir, writer);
|
|
1149
1367
|
const skillFiles = [];
|
|
1150
1368
|
for (const id of selectedInternalSkills) {
|
|
1151
|
-
const src =
|
|
1152
|
-
if (!
|
|
1369
|
+
const src = join3(harnessRoot, "templates/skills", id, "SKILL.md");
|
|
1370
|
+
if (!existsSync4(src)) {
|
|
1153
1371
|
continue;
|
|
1154
1372
|
}
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
const target = join2(skillDir, "SKILL.md");
|
|
1158
|
-
writeFileSync(target, renderBundledSkill(readFileSync2(src, "utf8")));
|
|
1373
|
+
const target = join3(projectDir, ".agents", "skills", id, "SKILL.md");
|
|
1374
|
+
writer.write(target, renderBundledSkill(readFileSync4(src, "utf8")));
|
|
1159
1375
|
skillFiles.push(target);
|
|
1160
1376
|
}
|
|
1161
|
-
return {
|
|
1377
|
+
return {
|
|
1378
|
+
rulesFile,
|
|
1379
|
+
skillFiles,
|
|
1380
|
+
ownership: writer.result()
|
|
1381
|
+
};
|
|
1162
1382
|
}
|
|
1163
|
-
function writeRules(harnessRoot, projectDir) {
|
|
1164
|
-
const claudeMdPath =
|
|
1165
|
-
const templatePath =
|
|
1166
|
-
if (!
|
|
1383
|
+
function writeRules(harnessRoot, projectDir, writer) {
|
|
1384
|
+
const claudeMdPath = join3(harnessRoot, "templates/CLAUDE.md");
|
|
1385
|
+
const templatePath = join3(harnessRoot, "templates/antigravity/AGENTS.md.template");
|
|
1386
|
+
if (!existsSync4(claudeMdPath) || !existsSync4(templatePath)) {
|
|
1167
1387
|
return null;
|
|
1168
1388
|
}
|
|
1169
|
-
const claudeMd =
|
|
1170
|
-
const template =
|
|
1171
|
-
const
|
|
1172
|
-
ensureDir(rulesDir);
|
|
1173
|
-
const target = join2(rulesDir, "uzys-harness.md");
|
|
1389
|
+
const claudeMd = readFileSync4(claudeMdPath, "utf8");
|
|
1390
|
+
const template = readFileSync4(templatePath, "utf8");
|
|
1391
|
+
const target = join3(projectDir, ".agents", "rules", "uzys-harness.md");
|
|
1174
1392
|
const rulesOut = renderAgentsMd({
|
|
1175
1393
|
template,
|
|
1176
1394
|
claudeMd,
|
|
1177
1395
|
projectName: basename(projectDir),
|
|
1178
1396
|
projectContext: renderFillScaffold()
|
|
1179
1397
|
});
|
|
1180
|
-
|
|
1181
|
-
writeFileSync(target, rulesOut);
|
|
1398
|
+
writer.write(target, rulesOut);
|
|
1182
1399
|
return target;
|
|
1183
1400
|
}
|
|
1184
1401
|
|
|
1185
1402
|
// src/ci-scaffold.ts
|
|
1186
1403
|
init_esm_shims();
|
|
1187
|
-
import { existsSync as
|
|
1188
|
-
import { join as
|
|
1404
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1405
|
+
import { join as join4 } from "path";
|
|
1189
1406
|
var PYTHON_TRACKS = "data|csr-fastapi|full";
|
|
1190
1407
|
var VARIANTS = [
|
|
1191
1408
|
{
|
|
@@ -1209,12 +1426,12 @@ function installCiScaffold(ctx) {
|
|
|
1209
1426
|
for (const v2 of VARIANTS) {
|
|
1210
1427
|
if (!v2.applies(ctx.tracks)) continue;
|
|
1211
1428
|
const relTarget = `.github/workflows/${v2.target}`;
|
|
1212
|
-
const target =
|
|
1213
|
-
if (
|
|
1429
|
+
const target = join4(ctx.projectDir, relTarget);
|
|
1430
|
+
if (existsSync5(target)) {
|
|
1214
1431
|
report.skippedExisting.push(relTarget);
|
|
1215
1432
|
continue;
|
|
1216
1433
|
}
|
|
1217
|
-
copyFile(
|
|
1434
|
+
copyFile(join4(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
|
|
1218
1435
|
report.written.push(relTarget);
|
|
1219
1436
|
}
|
|
1220
1437
|
return report;
|
|
@@ -1223,18 +1440,18 @@ function installCiScaffold(ctx) {
|
|
|
1223
1440
|
// src/codex/opt-in.ts
|
|
1224
1441
|
init_esm_shims();
|
|
1225
1442
|
import { homedir } from "os";
|
|
1226
|
-
import { join as
|
|
1443
|
+
import { join as join5 } from "path";
|
|
1227
1444
|
|
|
1228
1445
|
// src/codex/trust-entry.ts
|
|
1229
1446
|
init_esm_shims();
|
|
1230
|
-
import { existsSync as
|
|
1231
|
-
import { dirname as
|
|
1447
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1448
|
+
import { dirname as dirname4 } from "path";
|
|
1232
1449
|
var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
|
|
1233
1450
|
function registerTrustEntry(opts) {
|
|
1234
1451
|
const { configPath, projectDir } = opts;
|
|
1235
1452
|
try {
|
|
1236
|
-
|
|
1237
|
-
const existing =
|
|
1453
|
+
mkdirSync4(dirname4(configPath), { recursive: true });
|
|
1454
|
+
const existing = existsSync6(configPath) ? readFileSync5(configPath, "utf8") : "";
|
|
1238
1455
|
if (hasTrustEntry(existing, projectDir)) {
|
|
1239
1456
|
return { status: "already-present" };
|
|
1240
1457
|
}
|
|
@@ -1242,7 +1459,7 @@ function registerTrustEntry(opts) {
|
|
|
1242
1459
|
[projects."${projectDir}"]
|
|
1243
1460
|
trust_level = "trusted"
|
|
1244
1461
|
`;
|
|
1245
|
-
|
|
1462
|
+
writeFileSync3(configPath, existing + block);
|
|
1246
1463
|
return { status: "registered" };
|
|
1247
1464
|
} catch (e2) {
|
|
1248
1465
|
return {
|
|
@@ -1258,8 +1475,8 @@ function hasTrustEntry(configContent, projectDir) {
|
|
|
1258
1475
|
|
|
1259
1476
|
// src/codex/opt-in.ts
|
|
1260
1477
|
function runCodexOptIn(ctx) {
|
|
1261
|
-
const codexHome = ctx.codexHome ??
|
|
1262
|
-
const configPath =
|
|
1478
|
+
const codexHome = ctx.codexHome ?? join5(homedir(), ".codex");
|
|
1479
|
+
const configPath = join5(codexHome, "config.toml");
|
|
1263
1480
|
const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
|
|
1264
1481
|
return {
|
|
1265
1482
|
trustEntry: {
|
|
@@ -1272,8 +1489,8 @@ function runCodexOptIn(ctx) {
|
|
|
1272
1489
|
|
|
1273
1490
|
// src/codex/transform.ts
|
|
1274
1491
|
init_esm_shims();
|
|
1275
|
-
import { chmodSync, existsSync as
|
|
1276
|
-
import { basename as basename2, join as
|
|
1492
|
+
import { chmodSync, existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
1493
|
+
import { basename as basename2, join as join6 } from "path";
|
|
1277
1494
|
|
|
1278
1495
|
// src/codex/config-toml.ts
|
|
1279
1496
|
init_esm_shims();
|
|
@@ -1341,13 +1558,14 @@ function quoteIfNeeded(name) {
|
|
|
1341
1558
|
var HOOK_NAMES = ["session-start"];
|
|
1342
1559
|
var ENV_VAR_RENAME = /CLAUDE_PROJECT_DIR/g;
|
|
1343
1560
|
function runCodexTransform(params) {
|
|
1344
|
-
const { harnessRoot, projectDir, selectedInternalSkills = [] } = params;
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
1347
|
-
const
|
|
1561
|
+
const { harnessRoot, projectDir, selectedInternalSkills = [], baseline } = params;
|
|
1562
|
+
const writer = createOwnedWriter(projectDir, baseline);
|
|
1563
|
+
const claudeMd = readRequired(join6(harnessRoot, "templates/CLAUDE.md"));
|
|
1564
|
+
const agentsTemplate = readRequired(join6(harnessRoot, "templates/codex/AGENTS.md.template"));
|
|
1565
|
+
const configTemplate = readRequired(join6(harnessRoot, "templates/codex/config.toml.template"));
|
|
1348
1566
|
const projectName = basename2(projectDir);
|
|
1349
|
-
const mcp = readOptionalJson(
|
|
1350
|
-
const agentsMdPath =
|
|
1567
|
+
const mcp = readOptionalJson(join6(harnessRoot, ".mcp.json"));
|
|
1568
|
+
const agentsMdPath = join6(projectDir, "AGENTS.md");
|
|
1351
1569
|
ensureDir(projectDir);
|
|
1352
1570
|
const agentsMdOut = renderAgentsMd({
|
|
1353
1571
|
template: agentsTemplate,
|
|
@@ -1355,11 +1573,9 @@ function runCodexTransform(params) {
|
|
|
1355
1573
|
projectName,
|
|
1356
1574
|
projectContext: renderFillScaffold()
|
|
1357
1575
|
});
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
ensureDir(join5(projectDir, ".codex"));
|
|
1362
|
-
writeFileSync3(
|
|
1576
|
+
writer.write(agentsMdPath, agentsMdOut);
|
|
1577
|
+
const configTomlPath = join6(projectDir, ".codex/config.toml");
|
|
1578
|
+
writer.write(
|
|
1363
1579
|
configTomlPath,
|
|
1364
1580
|
renderConfigToml({
|
|
1365
1581
|
template: configTemplate,
|
|
@@ -1368,46 +1584,49 @@ function runCodexTransform(params) {
|
|
|
1368
1584
|
mcp
|
|
1369
1585
|
})
|
|
1370
1586
|
);
|
|
1371
|
-
const hookDir =
|
|
1372
|
-
ensureDir(hookDir);
|
|
1587
|
+
const hookDir = join6(projectDir, ".codex/hooks");
|
|
1373
1588
|
const hookFiles = [];
|
|
1374
1589
|
for (const hook of HOOK_NAMES) {
|
|
1375
|
-
const src =
|
|
1376
|
-
if (!
|
|
1590
|
+
const src = join6(harnessRoot, "templates/hooks", `${hook}.sh`);
|
|
1591
|
+
if (!existsSync7(src)) {
|
|
1377
1592
|
continue;
|
|
1378
1593
|
}
|
|
1379
|
-
const ported =
|
|
1380
|
-
const target =
|
|
1381
|
-
|
|
1594
|
+
const ported = readFileSync6(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
|
|
1595
|
+
const target = join6(hookDir, `${hook}.sh`);
|
|
1596
|
+
writer.write(target, ported);
|
|
1382
1597
|
chmodSync(target, 493);
|
|
1383
1598
|
hookFiles.push(target);
|
|
1384
1599
|
}
|
|
1385
1600
|
const skillFiles = [];
|
|
1386
1601
|
for (const id of selectedInternalSkills) {
|
|
1387
|
-
const src =
|
|
1388
|
-
if (!
|
|
1602
|
+
const src = join6(harnessRoot, "templates/skills", id, "SKILL.md");
|
|
1603
|
+
if (!existsSync7(src)) {
|
|
1389
1604
|
continue;
|
|
1390
1605
|
}
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
const target = join5(skillDir, "SKILL.md");
|
|
1394
|
-
writeFileSync3(target, renderBundledSkill(readFileSync4(src, "utf8")));
|
|
1606
|
+
const target = join6(projectDir, ".agents", "skills", id, "SKILL.md");
|
|
1607
|
+
writer.write(target, renderBundledSkill(readFileSync6(src, "utf8")));
|
|
1395
1608
|
skillFiles.push(target);
|
|
1396
1609
|
}
|
|
1397
|
-
return {
|
|
1610
|
+
return {
|
|
1611
|
+
agentsMdPath,
|
|
1612
|
+
configTomlPath,
|
|
1613
|
+
hookFiles,
|
|
1614
|
+
skillFiles,
|
|
1615
|
+
ownership: writer.result()
|
|
1616
|
+
};
|
|
1398
1617
|
}
|
|
1399
1618
|
function readRequired(path) {
|
|
1400
|
-
if (!
|
|
1619
|
+
if (!existsSync7(path)) {
|
|
1401
1620
|
throw new Error(`Codex transform: required source missing: ${path}`);
|
|
1402
1621
|
}
|
|
1403
|
-
return
|
|
1622
|
+
return readFileSync6(path, "utf8");
|
|
1404
1623
|
}
|
|
1405
1624
|
function readOptionalJson(path) {
|
|
1406
|
-
if (!
|
|
1625
|
+
if (!existsSync7(path)) {
|
|
1407
1626
|
return null;
|
|
1408
1627
|
}
|
|
1409
1628
|
try {
|
|
1410
|
-
return JSON.parse(
|
|
1629
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1411
1630
|
} catch {
|
|
1412
1631
|
return null;
|
|
1413
1632
|
}
|
|
@@ -1415,8 +1634,8 @@ function readOptionalJson(path) {
|
|
|
1415
1634
|
|
|
1416
1635
|
// src/env-files.ts
|
|
1417
1636
|
init_esm_shims();
|
|
1418
|
-
import { appendFileSync, existsSync as
|
|
1419
|
-
import { join as
|
|
1637
|
+
import { appendFileSync, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
1638
|
+
import { join as join7 } from "path";
|
|
1420
1639
|
var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
|
|
1421
1640
|
# Copy to .env (gitignored) and fill in values: cp .env.example .env
|
|
1422
1641
|
|
|
@@ -1453,24 +1672,24 @@ function writeEnvExample(projectDir, tracks) {
|
|
|
1453
1672
|
if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
|
|
1454
1673
|
return false;
|
|
1455
1674
|
}
|
|
1456
|
-
const path =
|
|
1457
|
-
if (
|
|
1675
|
+
const path = join7(projectDir, ".env.example");
|
|
1676
|
+
if (existsSync8(path)) {
|
|
1458
1677
|
return false;
|
|
1459
1678
|
}
|
|
1460
1679
|
writeFileSync4(path, ENV_EXAMPLE_BODY);
|
|
1461
1680
|
return true;
|
|
1462
1681
|
}
|
|
1463
1682
|
function addGitignoreEnv(projectDir) {
|
|
1464
|
-
const path =
|
|
1465
|
-
if (!
|
|
1683
|
+
const path = join7(projectDir, ".gitignore");
|
|
1684
|
+
if (!existsSync8(path)) {
|
|
1466
1685
|
return false;
|
|
1467
1686
|
}
|
|
1468
|
-
const content =
|
|
1687
|
+
const content = readFileSync7(path, "utf8");
|
|
1469
1688
|
if (GITIGNORE_ENV_PATTERN.test(content)) {
|
|
1470
1689
|
return false;
|
|
1471
1690
|
}
|
|
1472
|
-
const
|
|
1473
|
-
appendFileSync(path, `${
|
|
1691
|
+
const sep2 = content.endsWith("\n") ? "" : "\n";
|
|
1692
|
+
appendFileSync(path, `${sep2}
|
|
1474
1693
|
# Secret env (auto-added by agent-harness install)
|
|
1475
1694
|
.env
|
|
1476
1695
|
`);
|
|
@@ -1479,11 +1698,11 @@ function addGitignoreEnv(projectDir) {
|
|
|
1479
1698
|
var NPX_SKILLS_AGENT_DIRS = [".factory/", ".goose/"];
|
|
1480
1699
|
var GITIGNORE_NPX_SKILLS_HEADER = "# npx skills add multi-CLI cache (auto-added by agent-harness)";
|
|
1481
1700
|
function addGitignoreNpxSkillsAgents(projectDir) {
|
|
1482
|
-
const path =
|
|
1483
|
-
if (!
|
|
1701
|
+
const path = join7(projectDir, ".gitignore");
|
|
1702
|
+
if (!existsSync8(path)) {
|
|
1484
1703
|
return [];
|
|
1485
1704
|
}
|
|
1486
|
-
const content =
|
|
1705
|
+
const content = readFileSync7(path, "utf8");
|
|
1487
1706
|
const missing = NPX_SKILLS_AGENT_DIRS.filter((pattern) => {
|
|
1488
1707
|
const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
|
|
1489
1708
|
return !lineRegex.test(content);
|
|
@@ -1491,25 +1710,25 @@ function addGitignoreNpxSkillsAgents(projectDir) {
|
|
|
1491
1710
|
if (missing.length === 0) {
|
|
1492
1711
|
return [];
|
|
1493
1712
|
}
|
|
1494
|
-
const
|
|
1713
|
+
const sep2 = content.endsWith("\n") ? "" : "\n";
|
|
1495
1714
|
const block = [GITIGNORE_NPX_SKILLS_HEADER, ...missing].join("\n");
|
|
1496
|
-
appendFileSync(path, `${
|
|
1715
|
+
appendFileSync(path, `${sep2}
|
|
1497
1716
|
${block}
|
|
1498
1717
|
`);
|
|
1499
1718
|
return [...missing];
|
|
1500
1719
|
}
|
|
1501
1720
|
function writeMcpAllowlist(projectDir) {
|
|
1502
|
-
const allowlistPath =
|
|
1503
|
-
if (
|
|
1721
|
+
const allowlistPath = join7(projectDir, ".mcp-allowlist");
|
|
1722
|
+
if (existsSync8(allowlistPath)) {
|
|
1504
1723
|
return null;
|
|
1505
1724
|
}
|
|
1506
|
-
const mcpPath =
|
|
1507
|
-
if (!
|
|
1725
|
+
const mcpPath = join7(projectDir, ".mcp.json");
|
|
1726
|
+
if (!existsSync8(mcpPath)) {
|
|
1508
1727
|
return null;
|
|
1509
1728
|
}
|
|
1510
1729
|
let names = [];
|
|
1511
1730
|
try {
|
|
1512
|
-
const parsed = JSON.parse(
|
|
1731
|
+
const parsed = JSON.parse(readFileSync7(mcpPath, "utf8"));
|
|
1513
1732
|
names = Object.keys(parsed.mcpServers ?? {}).sort();
|
|
1514
1733
|
} catch {
|
|
1515
1734
|
return null;
|
|
@@ -1532,9 +1751,9 @@ function writeMcpAllowlist(projectDir) {
|
|
|
1532
1751
|
// src/external-installer.ts
|
|
1533
1752
|
init_esm_shims();
|
|
1534
1753
|
import { spawnSync } from "child_process";
|
|
1535
|
-
import { existsSync as
|
|
1754
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
|
|
1536
1755
|
import { homedir as homedir2 } from "os";
|
|
1537
|
-
import { join as
|
|
1756
|
+
import { join as join8 } from "path";
|
|
1538
1757
|
var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
|
|
1539
1758
|
function selectExternalTargets(assets, ctx) {
|
|
1540
1759
|
const conditionPassed = filterApplicableAssets(assets, ctx).filter(
|
|
@@ -1610,8 +1829,8 @@ function installOne(asset, ctx) {
|
|
|
1610
1829
|
cwd
|
|
1611
1830
|
);
|
|
1612
1831
|
case "shell-script": {
|
|
1613
|
-
const scriptPath =
|
|
1614
|
-
if (!
|
|
1832
|
+
const scriptPath = join8(ctx.harnessRoot, method.script);
|
|
1833
|
+
if (!existsSync9(scriptPath)) {
|
|
1615
1834
|
return {
|
|
1616
1835
|
asset,
|
|
1617
1836
|
ok: false,
|
|
@@ -1701,17 +1920,17 @@ function detectVersion(method, spawn) {
|
|
|
1701
1920
|
if (at2 <= 0) return void 0;
|
|
1702
1921
|
const plugin = method.pluginId.slice(0, at2);
|
|
1703
1922
|
const marketplaceShort = method.pluginId.slice(at2 + 1);
|
|
1704
|
-
const cacheBase =
|
|
1705
|
-
if (!
|
|
1923
|
+
const cacheBase = join8(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
|
|
1924
|
+
if (!existsSync9(cacheBase)) return void 0;
|
|
1706
1925
|
const versions = readdirSync2(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
|
|
1707
1926
|
return versions.at(-1);
|
|
1708
1927
|
}
|
|
1709
1928
|
case "npm": {
|
|
1710
1929
|
const npmRoot = getNpmGlobalRoot(spawn);
|
|
1711
1930
|
if (!npmRoot) return void 0;
|
|
1712
|
-
const pkgJson =
|
|
1713
|
-
if (!
|
|
1714
|
-
const parsed = JSON.parse(
|
|
1931
|
+
const pkgJson = join8(npmRoot, method.pkg, "package.json");
|
|
1932
|
+
if (!existsSync9(pkgJson)) return void 0;
|
|
1933
|
+
const parsed = JSON.parse(readFileSync8(pkgJson, "utf8"));
|
|
1715
1934
|
return parsed.version;
|
|
1716
1935
|
}
|
|
1717
1936
|
default:
|
|
@@ -1736,156 +1955,9 @@ function getNpmGlobalRoot(spawn) {
|
|
|
1736
1955
|
return void 0;
|
|
1737
1956
|
}
|
|
1738
1957
|
|
|
1739
|
-
// src/install-log.ts
|
|
1740
|
-
init_esm_shims();
|
|
1741
|
-
import { createHash } from "crypto";
|
|
1742
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1743
|
-
import { dirname as dirname3, join as join8 } from "path";
|
|
1744
|
-
var INSTALL_LOG_FILENAME = ".harness-install.json";
|
|
1745
|
-
var INSTALL_LOG_VERSION = 1;
|
|
1746
|
-
function buildAssetEntries(report, scope) {
|
|
1747
|
-
if (!report) return [];
|
|
1748
|
-
return report.attempted.filter((r) => r.ok).map((r) => assetToLogEntry(r.asset, scope, r.version));
|
|
1749
|
-
}
|
|
1750
|
-
function assetToLogEntry(asset, scope, version) {
|
|
1751
|
-
const detail = methodDetail(asset.method);
|
|
1752
|
-
const entry = {
|
|
1753
|
-
id: asset.id,
|
|
1754
|
-
category: asset.category,
|
|
1755
|
-
method: asset.method.kind,
|
|
1756
|
-
scope,
|
|
1757
|
-
detail
|
|
1758
|
-
};
|
|
1759
|
-
if (version) entry.version = version;
|
|
1760
|
-
return entry;
|
|
1761
|
-
}
|
|
1762
|
-
function methodDetail(method) {
|
|
1763
|
-
switch (method.kind) {
|
|
1764
|
-
case "plugin":
|
|
1765
|
-
return { marketplace: method.marketplace, pluginId: method.pluginId };
|
|
1766
|
-
case "skill":
|
|
1767
|
-
return { source: method.source, ...method.skill ? { skill: method.skill } : {} };
|
|
1768
|
-
case "npm":
|
|
1769
|
-
return { pkg: method.pkg };
|
|
1770
|
-
case "npx-run":
|
|
1771
|
-
return { cmd: method.cmd, args: (method.args ?? []).join(" ") };
|
|
1772
|
-
case "shell-script":
|
|
1773
|
-
return { script: method.script, args: method.args.join(" ") };
|
|
1774
|
-
case "internal":
|
|
1775
|
-
return { key: method.key };
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
|
|
1779
|
-
const templates = {
|
|
1780
|
-
claudeDir: ".claude/",
|
|
1781
|
-
...spec.cli.includes("codex") ? { codexDir: ".codex/" } : {},
|
|
1782
|
-
...spec.cli.includes("opencode") ? { opencodeDir: ".opencode/" } : {},
|
|
1783
|
-
...rootClaudeMd ? { rootClaudeMd } : {}
|
|
1784
|
-
};
|
|
1785
|
-
const log = {
|
|
1786
|
-
schemaVersion: INSTALL_LOG_VERSION,
|
|
1787
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1788
|
-
scope,
|
|
1789
|
-
spec: {
|
|
1790
|
-
tracks: spec.tracks,
|
|
1791
|
-
cli: spec.cli
|
|
1792
|
-
},
|
|
1793
|
-
// 이번 설치가 만든 항목이 이기고, 이번에 안 만든 항목은 이전 값을 그대로 둔다.
|
|
1794
|
-
// (예: claude 로 깔고 나중에 codex 만 추가 설치해도 root CLAUDE.md 기록이 살아남는다)
|
|
1795
|
-
templates: { ...previous?.templates, ...templates },
|
|
1796
|
-
assets: mergeAssets(
|
|
1797
|
-
claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
|
|
1798
|
-
buildAssetEntries(external, scope)
|
|
1799
|
-
)
|
|
1800
|
-
};
|
|
1801
|
-
const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
|
|
1802
|
-
if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
|
|
1803
|
-
return log;
|
|
1804
|
-
}
|
|
1805
|
-
function mergeRootFiles(previous, current) {
|
|
1806
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
1807
|
-
for (const file of [...previous ?? [], ...current]) {
|
|
1808
|
-
const prior = byPath.get(file.path);
|
|
1809
|
-
byPath.set(
|
|
1810
|
-
file.path,
|
|
1811
|
-
prior ? {
|
|
1812
|
-
path: file.path,
|
|
1813
|
-
change: prior.change === "created" ? "created" : file.change,
|
|
1814
|
-
notes: [.../* @__PURE__ */ new Set([...prior.notes, ...file.notes])]
|
|
1815
|
-
} : file
|
|
1816
|
-
);
|
|
1817
|
-
}
|
|
1818
|
-
return [...byPath.values()];
|
|
1819
|
-
}
|
|
1820
|
-
function survivesClaudeDirRename(asset) {
|
|
1821
|
-
if (asset.scope === "global") return true;
|
|
1822
|
-
switch (asset.method) {
|
|
1823
|
-
case "plugin":
|
|
1824
|
-
return true;
|
|
1825
|
-
// `~/.claude/plugins/cache` — 프로젝트 밖
|
|
1826
|
-
case "npm":
|
|
1827
|
-
return true;
|
|
1828
|
-
// `node_modules/`
|
|
1829
|
-
case "skill":
|
|
1830
|
-
return false;
|
|
1831
|
-
// `npx skills add` project scope → `.claude/skills/`
|
|
1832
|
-
case "shell-script":
|
|
1833
|
-
return false;
|
|
1834
|
-
// ecc-prune → `.claude/local-plugins/`
|
|
1835
|
-
case "npx-run":
|
|
1836
|
-
return false;
|
|
1837
|
-
case "internal":
|
|
1838
|
-
return false;
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
function mergeAssets(previous, current) {
|
|
1842
|
-
if (!previous || previous.length === 0) return [...current];
|
|
1843
|
-
const currentById = new Map(current.map((a) => [a.id, a]));
|
|
1844
|
-
const previousIds = new Set(previous.map((a) => a.id));
|
|
1845
|
-
return [
|
|
1846
|
-
...previous.map((a) => currentById.get(a.id) ?? a),
|
|
1847
|
-
...current.filter((a) => !previousIds.has(a.id))
|
|
1848
|
-
];
|
|
1849
|
-
}
|
|
1850
|
-
function hashContent(content) {
|
|
1851
|
-
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1852
|
-
}
|
|
1853
|
-
function collectSkillHashes(projectDir) {
|
|
1854
|
-
const skillsDir = join8(projectDir, ".claude/skills");
|
|
1855
|
-
return listFilesRecursive(skillsDir).map((rel) => ({
|
|
1856
|
-
path: rel,
|
|
1857
|
-
sha256: hashContent(readFileSync7(join8(skillsDir, rel), "utf8"))
|
|
1858
|
-
}));
|
|
1859
|
-
}
|
|
1860
|
-
function writeInstallLog(projectDir, log) {
|
|
1861
|
-
const path = join8(projectDir, ".claude", INSTALL_LOG_FILENAME);
|
|
1862
|
-
mkdirSync3(dirname3(path), { recursive: true });
|
|
1863
|
-
writeFileSync5(path, `${JSON.stringify(log, null, 2)}
|
|
1864
|
-
`, "utf8");
|
|
1865
|
-
return path;
|
|
1866
|
-
}
|
|
1867
|
-
function readInstallLog(projectDir) {
|
|
1868
|
-
const path = join8(projectDir, ".claude", INSTALL_LOG_FILENAME);
|
|
1869
|
-
if (!existsSync8(path)) return null;
|
|
1870
|
-
try {
|
|
1871
|
-
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
1872
|
-
if (Array.isArray(parsed.assets)) {
|
|
1873
|
-
parsed.assets = parsed.assets.map(
|
|
1874
|
-
(a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
|
|
1875
|
-
);
|
|
1876
|
-
}
|
|
1877
|
-
return parsed;
|
|
1878
|
-
} catch {
|
|
1879
|
-
return null;
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
function installLogPath(projectDir) {
|
|
1883
|
-
return join8(projectDir, ".claude", INSTALL_LOG_FILENAME);
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
1958
|
// src/mcp-merge.ts
|
|
1887
1959
|
init_esm_shims();
|
|
1888
|
-
import { existsSync as
|
|
1960
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
1889
1961
|
function parseTrackMcpMap(raw) {
|
|
1890
1962
|
const rows = [];
|
|
1891
1963
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -1936,15 +2008,15 @@ function mergeMcpServers(base, rows, tracks) {
|
|
|
1936
2008
|
return out;
|
|
1937
2009
|
}
|
|
1938
2010
|
function composeMcpJson(opts) {
|
|
1939
|
-
const base = JSON.parse(
|
|
1940
|
-
const merged = opts.existingPath &&
|
|
1941
|
-
const mapRaw =
|
|
2011
|
+
const base = JSON.parse(readFileSync9(opts.templateMcpPath, "utf8"));
|
|
2012
|
+
const merged = opts.existingPath && existsSync10(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
|
|
2013
|
+
const mapRaw = existsSync10(opts.trackMapPath) ? readFileSync9(opts.trackMapPath, "utf8") : "";
|
|
1942
2014
|
const rows = parseTrackMcpMap(mapRaw);
|
|
1943
2015
|
return mergeMcpServers(merged, rows, opts.tracks);
|
|
1944
2016
|
}
|
|
1945
2017
|
function mergeUserBase(base, existingPath) {
|
|
1946
2018
|
try {
|
|
1947
|
-
const existing = JSON.parse(
|
|
2019
|
+
const existing = JSON.parse(readFileSync9(existingPath, "utf8"));
|
|
1948
2020
|
return {
|
|
1949
2021
|
...base,
|
|
1950
2022
|
mcpServers: { ...base.mcpServers, ...existing.mcpServers }
|
|
@@ -1954,13 +2026,13 @@ function mergeUserBase(base, existingPath) {
|
|
|
1954
2026
|
}
|
|
1955
2027
|
}
|
|
1956
2028
|
function writeMcpJson(path, mcp) {
|
|
1957
|
-
|
|
2029
|
+
writeFileSync5(path, `${JSON.stringify(mcp, null, 2)}
|
|
1958
2030
|
`);
|
|
1959
2031
|
}
|
|
1960
2032
|
|
|
1961
2033
|
// src/opencode/transform.ts
|
|
1962
2034
|
init_esm_shims();
|
|
1963
|
-
import { existsSync as
|
|
2035
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
1964
2036
|
import { basename as basename3, join as join9 } from "path";
|
|
1965
2037
|
|
|
1966
2038
|
// src/opencode/agents-md.ts
|
|
@@ -2061,7 +2133,8 @@ function parseTemplate(template) {
|
|
|
2061
2133
|
|
|
2062
2134
|
// src/opencode/transform.ts
|
|
2063
2135
|
function runOpencodeTransform(params) {
|
|
2064
|
-
const { harnessRoot, projectDir, selectedInternalSkills = [] } = params;
|
|
2136
|
+
const { harnessRoot, projectDir, selectedInternalSkills = [], baseline } = params;
|
|
2137
|
+
const writer = createOwnedWriter(projectDir, baseline);
|
|
2065
2138
|
const claudeMd = readRequired2(join9(harnessRoot, "templates/CLAUDE.md"));
|
|
2066
2139
|
const agentsTemplate = readRequired2(join9(harnessRoot, "templates/opencode/AGENTS.md.template"));
|
|
2067
2140
|
const opencodeTemplate = readRequired2(
|
|
@@ -2077,40 +2150,41 @@ function runOpencodeTransform(params) {
|
|
|
2077
2150
|
projectName,
|
|
2078
2151
|
projectContext: renderFillScaffold()
|
|
2079
2152
|
});
|
|
2080
|
-
|
|
2081
|
-
writeFileSync7(agentsMdPath, agentsMdOut);
|
|
2153
|
+
writer.write(agentsMdPath, agentsMdOut);
|
|
2082
2154
|
const opencodeJsonPath = join9(projectDir, "opencode.json");
|
|
2083
|
-
|
|
2155
|
+
writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
|
|
2084
2156
|
const cmdDir = join9(projectDir, ".opencode/commands");
|
|
2085
2157
|
ensureDir(cmdDir);
|
|
2086
2158
|
const commandFiles = [];
|
|
2087
2159
|
for (const id of selectedInternalSkills) {
|
|
2088
2160
|
const src = join9(harnessRoot, "templates/skills", id, "SKILL.md");
|
|
2089
|
-
if (!
|
|
2161
|
+
if (!existsSync11(src)) {
|
|
2090
2162
|
continue;
|
|
2091
2163
|
}
|
|
2092
2164
|
const target = join9(cmdDir, `${id}.md`);
|
|
2093
|
-
const shellDependent =
|
|
2094
|
-
|
|
2095
|
-
target,
|
|
2096
|
-
renderCommandFromSkill(readFileSync9(src, "utf8"), id, { shellDependent })
|
|
2097
|
-
);
|
|
2165
|
+
const shellDependent = existsSync11(join9(harnessRoot, "templates/skills", id, "scripts"));
|
|
2166
|
+
writer.write(target, renderCommandFromSkill(readFileSync10(src, "utf8"), id, { shellDependent }));
|
|
2098
2167
|
commandFiles.push(target);
|
|
2099
2168
|
}
|
|
2100
|
-
return {
|
|
2169
|
+
return {
|
|
2170
|
+
agentsMdPath,
|
|
2171
|
+
opencodeJsonPath,
|
|
2172
|
+
commandFiles,
|
|
2173
|
+
ownership: writer.result()
|
|
2174
|
+
};
|
|
2101
2175
|
}
|
|
2102
2176
|
function readRequired2(path) {
|
|
2103
|
-
if (!
|
|
2177
|
+
if (!existsSync11(path)) {
|
|
2104
2178
|
throw new Error(`OpenCode transform: required source missing: ${path}`);
|
|
2105
2179
|
}
|
|
2106
|
-
return
|
|
2180
|
+
return readFileSync10(path, "utf8");
|
|
2107
2181
|
}
|
|
2108
2182
|
function readOptionalJson2(path) {
|
|
2109
|
-
if (!
|
|
2183
|
+
if (!existsSync11(path)) {
|
|
2110
2184
|
return null;
|
|
2111
2185
|
}
|
|
2112
2186
|
try {
|
|
2113
|
-
return JSON.parse(
|
|
2187
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
2114
2188
|
} catch {
|
|
2115
2189
|
return null;
|
|
2116
2190
|
}
|
|
@@ -2144,14 +2218,14 @@ function addPreToolUseHook(settings, matcher, command) {
|
|
|
2144
2218
|
init_esm_shims();
|
|
2145
2219
|
import {
|
|
2146
2220
|
copyFileSync as copyFileSync2,
|
|
2147
|
-
existsSync as
|
|
2148
|
-
mkdirSync as
|
|
2221
|
+
existsSync as existsSync12,
|
|
2222
|
+
mkdirSync as mkdirSync5,
|
|
2149
2223
|
readdirSync as readdirSync3,
|
|
2150
|
-
readFileSync as
|
|
2224
|
+
readFileSync as readFileSync11,
|
|
2151
2225
|
unlinkSync,
|
|
2152
|
-
writeFileSync as
|
|
2226
|
+
writeFileSync as writeFileSync6
|
|
2153
2227
|
} from "fs";
|
|
2154
|
-
import { dirname as
|
|
2228
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
2155
2229
|
function buildUpdateSpec(projectDir, tracks) {
|
|
2156
2230
|
return {
|
|
2157
2231
|
tracks: [...tracks],
|
|
@@ -2167,38 +2241,21 @@ function runUpdateMode(projectDir, templatesDir) {
|
|
|
2167
2241
|
pruned: {},
|
|
2168
2242
|
staleHookRefs: [],
|
|
2169
2243
|
claudeMdUpdated: false,
|
|
2170
|
-
skillsBackedUp: []
|
|
2244
|
+
skillsBackedUp: [],
|
|
2245
|
+
policyBackedUp: []
|
|
2171
2246
|
};
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
},
|
|
2185
|
-
{
|
|
2186
|
-
target: join10(claudeDir, "commands/uzys"),
|
|
2187
|
-
source: join10(templatesDir, "commands/uzys"),
|
|
2188
|
-
pattern: ".md",
|
|
2189
|
-
label: ".claude/commands/uzys"
|
|
2190
|
-
},
|
|
2191
|
-
{
|
|
2192
|
-
target: join10(claudeDir, "hooks"),
|
|
2193
|
-
source: join10(templatesDir, "hooks"),
|
|
2194
|
-
pattern: ".sh",
|
|
2195
|
-
label: ".claude/hooks"
|
|
2196
|
-
}
|
|
2197
|
-
];
|
|
2198
|
-
for (const t of targets) {
|
|
2199
|
-
report.updated[t.label] = updateDir(t.target, t.source, t.pattern);
|
|
2200
|
-
report.pruned[t.label] = pruneOrphans(t.target, t.source, t.pattern);
|
|
2201
|
-
}
|
|
2247
|
+
const policyBase = policyBaseline(projectDir);
|
|
2248
|
+
for (const { dir, ext } of POLICY_DIRS) {
|
|
2249
|
+
const target = join10(claudeDir, dir);
|
|
2250
|
+
const source = join10(templatesDir, dir);
|
|
2251
|
+
const label = `.claude/${dir}`;
|
|
2252
|
+
const ctx = { prefix: dir, baseline: policyBase };
|
|
2253
|
+
const synced = updateDir(target, source, ext, ctx);
|
|
2254
|
+
report.updated[label] = synced.updated;
|
|
2255
|
+
report.policyBackedUp.push(...synced.backedUp);
|
|
2256
|
+
report.pruned[label] = pruneOrphans(target, source, ext, ctx);
|
|
2257
|
+
}
|
|
2258
|
+
refreshPolicyBaseline(projectDir, templatesDir);
|
|
2202
2259
|
const skillSync = syncSkills(
|
|
2203
2260
|
join10(claudeDir, "skills"),
|
|
2204
2261
|
join10(templatesDir, "skills"),
|
|
@@ -2209,55 +2266,66 @@ function runUpdateMode(projectDir, templatesDir) {
|
|
|
2209
2266
|
refreshSkillBaseline(projectDir);
|
|
2210
2267
|
const claudeMd = join10(claudeDir, "CLAUDE.md");
|
|
2211
2268
|
const templateMd = join10(templatesDir, "CLAUDE.md");
|
|
2212
|
-
if (
|
|
2269
|
+
if (existsSync12(claudeMd) && existsSync12(templateMd)) {
|
|
2213
2270
|
copyFileSync2(templateMd, claudeMd);
|
|
2214
2271
|
report.claudeMdUpdated = true;
|
|
2215
2272
|
}
|
|
2216
2273
|
const settingsPath = join10(claudeDir, "settings.json");
|
|
2217
|
-
if (
|
|
2274
|
+
if (existsSync12(settingsPath)) {
|
|
2218
2275
|
report.staleHookRefs = cleanStaleHookRefs(settingsPath, join10(claudeDir, "hooks"));
|
|
2219
2276
|
}
|
|
2220
2277
|
return report;
|
|
2221
2278
|
}
|
|
2222
|
-
function
|
|
2223
|
-
|
|
2224
|
-
|
|
2279
|
+
function isHarnessOwned2(ctx, rel, current) {
|
|
2280
|
+
const recorded = ctx.baseline.get(`${ctx.prefix}/${rel}`);
|
|
2281
|
+
return recorded !== void 0 && recorded === hashContent(current);
|
|
2282
|
+
}
|
|
2283
|
+
function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
2284
|
+
if (!existsSync12(target) || !existsSync12(source)) return { updated: 0, backedUp: [] };
|
|
2285
|
+
let updated = 0;
|
|
2286
|
+
const backedUp = [];
|
|
2225
2287
|
for (const file of readdirSync3(target)) {
|
|
2226
2288
|
if (!file.endsWith(ext)) continue;
|
|
2227
2289
|
const targetFile = join10(target, file);
|
|
2228
2290
|
const sourceFile = join10(source, file);
|
|
2229
|
-
if (
|
|
2230
|
-
|
|
2231
|
-
|
|
2291
|
+
if (!existsSync12(sourceFile)) continue;
|
|
2292
|
+
const next = readFileSync11(sourceFile, "utf8");
|
|
2293
|
+
const current = readFileSync11(targetFile, "utf8");
|
|
2294
|
+
if (current === next) continue;
|
|
2295
|
+
if (!isHarnessOwned2(ctx, file, current)) {
|
|
2296
|
+
backupFile(targetFile, now);
|
|
2297
|
+
backedUp.push(`${ctx.prefix}/${file}`);
|
|
2232
2298
|
}
|
|
2299
|
+
writeFileSync6(targetFile, next);
|
|
2300
|
+
updated++;
|
|
2233
2301
|
}
|
|
2234
|
-
return
|
|
2302
|
+
return { updated, backedUp };
|
|
2235
2303
|
}
|
|
2236
2304
|
function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date()) {
|
|
2237
|
-
if (!
|
|
2305
|
+
if (!existsSync12(targetDir) || !existsSync12(sourceDir)) return { updated: 0, backedUp: [] };
|
|
2238
2306
|
let updated = 0;
|
|
2239
2307
|
const backedUp = [];
|
|
2240
2308
|
for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
|
|
2241
2309
|
if (!skill.isDirectory()) continue;
|
|
2242
2310
|
const targetSkill = join10(targetDir, skill.name);
|
|
2243
|
-
if (!
|
|
2311
|
+
if (!existsSync12(targetSkill)) continue;
|
|
2244
2312
|
for (const rel of listFilesRecursive(join10(sourceDir, skill.name))) {
|
|
2245
2313
|
const targetFile = join10(targetSkill, rel);
|
|
2246
|
-
const next =
|
|
2247
|
-
if (!
|
|
2248
|
-
|
|
2249
|
-
|
|
2314
|
+
const next = readFileSync11(join10(sourceDir, skill.name, rel), "utf8");
|
|
2315
|
+
if (!existsSync12(targetFile)) {
|
|
2316
|
+
mkdirSync5(dirname5(targetFile), { recursive: true });
|
|
2317
|
+
writeFileSync6(targetFile, next);
|
|
2250
2318
|
updated++;
|
|
2251
2319
|
continue;
|
|
2252
2320
|
}
|
|
2253
|
-
const current =
|
|
2321
|
+
const current = readFileSync11(targetFile, "utf8");
|
|
2254
2322
|
if (current === next) continue;
|
|
2255
2323
|
const recorded = baseline.get(`${skill.name}/${rel}`);
|
|
2256
2324
|
if (recorded === void 0 || hashContent(current) !== recorded) {
|
|
2257
2325
|
backupFile(targetFile, now);
|
|
2258
2326
|
backedUp.push(`${skill.name}/${rel}`);
|
|
2259
2327
|
}
|
|
2260
|
-
|
|
2328
|
+
writeFileSync6(targetFile, next);
|
|
2261
2329
|
updated++;
|
|
2262
2330
|
}
|
|
2263
2331
|
}
|
|
@@ -2267,6 +2335,22 @@ function skillBaseline(projectDir) {
|
|
|
2267
2335
|
const log = readInstallLog(projectDir);
|
|
2268
2336
|
return new Map((log?.skillFiles ?? []).map((f) => [f.path, f.sha256]));
|
|
2269
2337
|
}
|
|
2338
|
+
function policyBaseline(projectDir) {
|
|
2339
|
+
const log = readInstallLog(projectDir);
|
|
2340
|
+
return new Map((log?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
|
|
2341
|
+
}
|
|
2342
|
+
function refreshPolicyBaseline(projectDir, templatesDir) {
|
|
2343
|
+
const log = readInstallLog(projectDir);
|
|
2344
|
+
if (!log) return;
|
|
2345
|
+
const policyFiles = collectPolicyHashes(projectDir, templatesDir);
|
|
2346
|
+
const next = { ...log };
|
|
2347
|
+
if (policyFiles.length > 0) next.policyFiles = policyFiles;
|
|
2348
|
+
else delete next.policyFiles;
|
|
2349
|
+
try {
|
|
2350
|
+
writeInstallLog(projectDir, next);
|
|
2351
|
+
} catch {
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2270
2354
|
function refreshSkillBaseline(projectDir) {
|
|
2271
2355
|
const log = readInstallLog(projectDir);
|
|
2272
2356
|
if (!log) return;
|
|
@@ -2279,15 +2363,18 @@ function refreshSkillBaseline(projectDir) {
|
|
|
2279
2363
|
} catch {
|
|
2280
2364
|
}
|
|
2281
2365
|
}
|
|
2282
|
-
function pruneOrphans(target, source, ext) {
|
|
2283
|
-
if (!
|
|
2366
|
+
function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
2367
|
+
if (!existsSync12(target) || !existsSync12(source)) return [];
|
|
2284
2368
|
const removed = [];
|
|
2285
2369
|
for (const file of readdirSync3(target)) {
|
|
2286
2370
|
if (!file.endsWith(ext)) continue;
|
|
2287
2371
|
const sourceFile = join10(source, file);
|
|
2288
|
-
if (!
|
|
2372
|
+
if (!existsSync12(sourceFile)) {
|
|
2289
2373
|
const targetFile = join10(target, file);
|
|
2374
|
+
if (!ctx.baseline.has(`${ctx.prefix}/${file}`)) continue;
|
|
2290
2375
|
try {
|
|
2376
|
+
const current = readFileSync11(targetFile, "utf8");
|
|
2377
|
+
if (!isHarnessOwned2(ctx, file, current)) backupFile(targetFile, now);
|
|
2291
2378
|
unlinkSync(targetFile);
|
|
2292
2379
|
removed.push(file);
|
|
2293
2380
|
} catch {
|
|
@@ -2299,7 +2386,7 @@ function pruneOrphans(target, source, ext) {
|
|
|
2299
2386
|
function cleanStaleHookRefs(settingsPath, hooksDir) {
|
|
2300
2387
|
let settings;
|
|
2301
2388
|
try {
|
|
2302
|
-
settings = JSON.parse(
|
|
2389
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
|
|
2303
2390
|
} catch {
|
|
2304
2391
|
return [];
|
|
2305
2392
|
}
|
|
@@ -2318,7 +2405,7 @@ function cleanStaleHookRefs(settingsPath, hooksDir) {
|
|
|
2318
2405
|
}
|
|
2319
2406
|
if (removed.length > 0) {
|
|
2320
2407
|
const next = { ...settings, hooks: cleanedHooks };
|
|
2321
|
-
|
|
2408
|
+
writeFileSync6(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
2322
2409
|
`);
|
|
2323
2410
|
}
|
|
2324
2411
|
return removed;
|
|
@@ -2327,7 +2414,7 @@ function keepHookRef(hook, hooksDir, removed) {
|
|
|
2327
2414
|
const refMatch = (hook?.command ?? "").match(/\/\.claude\/hooks\/([^"\s/]+\.sh)/);
|
|
2328
2415
|
if (!refMatch?.[1]) return true;
|
|
2329
2416
|
const fname = refMatch[1];
|
|
2330
|
-
const exists =
|
|
2417
|
+
const exists = existsSync12(join10(hooksDir, fname));
|
|
2331
2418
|
if (!exists && !removed.includes(fname)) removed.push(fname);
|
|
2332
2419
|
return exists;
|
|
2333
2420
|
}
|
|
@@ -2340,11 +2427,11 @@ function runInstall(ctx) {
|
|
|
2340
2427
|
const { harnessRoot, projectDir, spec } = ctx;
|
|
2341
2428
|
const mode = ctx.mode ?? "fresh";
|
|
2342
2429
|
const templatesDir = join11(harnessRoot, "templates");
|
|
2343
|
-
if (!
|
|
2430
|
+
if (!existsSync13(templatesDir)) {
|
|
2344
2431
|
throw new Error(`Templates dir not found: ${templatesDir}`);
|
|
2345
2432
|
}
|
|
2346
2433
|
const claudeDir = join11(projectDir, ".claude");
|
|
2347
|
-
if (mode === "update" && !
|
|
2434
|
+
if (mode === "update" && !existsSync13(claudeDir)) {
|
|
2348
2435
|
throw new Error(`Update mode requires existing .claude/ at ${claudeDir}`);
|
|
2349
2436
|
}
|
|
2350
2437
|
const previousLog = readInstallLog(projectDir);
|
|
@@ -2353,13 +2440,21 @@ function runInstall(ctx) {
|
|
|
2353
2440
|
return runUpdateInstall(ctx, templatesDir, backupPath);
|
|
2354
2441
|
}
|
|
2355
2442
|
const manifestSpec = buildManifestSpec(spec);
|
|
2356
|
-
const
|
|
2443
|
+
const policyBase = new Map((previousLog?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
|
|
2444
|
+
const base = spec.cli.includes("claude") ? installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase) : emptyClaudeBaseline();
|
|
2357
2445
|
const mcpResult = composeAndWriteMcp(harnessRoot, projectDir, spec);
|
|
2358
2446
|
const ciScaffold = isAssetSelected("ci-scaffold", {
|
|
2359
2447
|
tracks: spec.tracks,
|
|
2360
2448
|
options: spec.options,
|
|
2361
2449
|
...spec.userOverride ? { userOverride: spec.userOverride } : {}
|
|
2362
2450
|
}) ? installCiScaffold({ harnessRoot, projectDir, tracks: spec.tracks }) : null;
|
|
2451
|
+
const { externalFiles, externalBackups, ...cliTransforms } = runCliTransforms(
|
|
2452
|
+
spec,
|
|
2453
|
+
harnessRoot,
|
|
2454
|
+
projectDir,
|
|
2455
|
+
manifestSpec.selectedInternalSkills,
|
|
2456
|
+
previousLog?.externalFiles ?? []
|
|
2457
|
+
);
|
|
2363
2458
|
const baseline = {
|
|
2364
2459
|
filesCopied: base.filesCopied,
|
|
2365
2460
|
dirsCopied: base.dirsCopied,
|
|
@@ -2367,20 +2462,23 @@ function runInstall(ctx) {
|
|
|
2367
2462
|
backup: backupPath,
|
|
2368
2463
|
installedTracks: [...spec.tracks].sort(),
|
|
2369
2464
|
mcpServers: Object.keys(mcpResult.mcpServers).sort(),
|
|
2370
|
-
...
|
|
2465
|
+
...cliTransforms,
|
|
2371
2466
|
ciScaffold,
|
|
2372
2467
|
updateMode: null,
|
|
2373
2468
|
mode,
|
|
2374
2469
|
envFiles: writeEnvironmentFiles(projectDir, spec.tracks),
|
|
2375
2470
|
categories: base.categories,
|
|
2376
2471
|
rootClaudeMd: base.rootClaudeMd,
|
|
2377
|
-
|
|
2472
|
+
// 외부 CLI 백업도 같은 줄에 노출한다 — 백업이 화면에 안 보이면 사용자는 자기 편집분이
|
|
2473
|
+
// 어디 갔는지 알 수 없고, 그러면 백업은 있어도 없는 것과 같다 (ADR-046/047 과 같은 이유).
|
|
2474
|
+
backups: [...base.backups, ...externalBackups]
|
|
2378
2475
|
};
|
|
2379
2476
|
ctx.onProgress?.({ type: "baseline-complete", baseline });
|
|
2380
2477
|
const external = runExternalPhase(ctx);
|
|
2381
2478
|
const karpathyHook = wireKarpathyHook(spec, external, harnessRoot, projectDir);
|
|
2382
2479
|
writeInstallLogSafe(
|
|
2383
2480
|
ctx,
|
|
2481
|
+
externalFiles,
|
|
2384
2482
|
external,
|
|
2385
2483
|
base.rootClaudeMdLog,
|
|
2386
2484
|
previousLog,
|
|
@@ -2453,7 +2551,21 @@ function emptyClaudeBaseline() {
|
|
|
2453
2551
|
backups: []
|
|
2454
2552
|
};
|
|
2455
2553
|
}
|
|
2456
|
-
function
|
|
2554
|
+
function backupEditedPolicyFile(entryTarget, target, source, baseline) {
|
|
2555
|
+
const prefix = ".claude/";
|
|
2556
|
+
if (!entryTarget.startsWith(prefix)) return null;
|
|
2557
|
+
const rel = entryTarget.slice(prefix.length);
|
|
2558
|
+
if (!POLICY_DIRS.some(({ dir, ext }) => rel.startsWith(`${dir}/`) && rel.endsWith(ext))) {
|
|
2559
|
+
return null;
|
|
2560
|
+
}
|
|
2561
|
+
if (!existsSync13(target)) return null;
|
|
2562
|
+
const current = readFileSync12(target, "utf-8");
|
|
2563
|
+
if (current === readFileSync12(source, "utf-8")) return null;
|
|
2564
|
+
const recorded = baseline.get(rel);
|
|
2565
|
+
if (recorded !== void 0 && recorded === hashContent(current)) return null;
|
|
2566
|
+
return backupFile(target);
|
|
2567
|
+
}
|
|
2568
|
+
function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase) {
|
|
2457
2569
|
ensureProjectSkeleton(projectDir);
|
|
2458
2570
|
const result = emptyClaudeBaseline();
|
|
2459
2571
|
const manifest = buildManifest(manifestSpec);
|
|
@@ -2463,13 +2575,18 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir) {
|
|
|
2463
2575
|
}
|
|
2464
2576
|
const source = join11(templatesDir, entry.source);
|
|
2465
2577
|
const target = join11(projectDir, entry.target);
|
|
2466
|
-
if (!
|
|
2578
|
+
if (!existsSync13(source)) {
|
|
2467
2579
|
result.skipped += 1;
|
|
2468
2580
|
continue;
|
|
2469
2581
|
}
|
|
2470
2582
|
if (entry.type === "file") {
|
|
2471
2583
|
if (entry.target === ".claude/settings.json") {
|
|
2472
|
-
const backup = backupFileIfChanged(target,
|
|
2584
|
+
const backup = backupFileIfChanged(target, readFileSync12(source, "utf-8"));
|
|
2585
|
+
if (backup) {
|
|
2586
|
+
result.backups.push(backup);
|
|
2587
|
+
}
|
|
2588
|
+
} else {
|
|
2589
|
+
const backup = backupEditedPolicyFile(entry.target, target, source, policyBase);
|
|
2473
2590
|
if (backup) {
|
|
2474
2591
|
result.backups.push(backup);
|
|
2475
2592
|
}
|
|
@@ -2483,7 +2600,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir) {
|
|
|
2483
2600
|
accumulateCategory(result.categories, entry);
|
|
2484
2601
|
}
|
|
2485
2602
|
const hookDir = join11(projectDir, ".claude/hooks");
|
|
2486
|
-
if (
|
|
2603
|
+
if (existsSync13(hookDir)) {
|
|
2487
2604
|
chmodHooksSync(hookDir);
|
|
2488
2605
|
}
|
|
2489
2606
|
writeInstalledTracks(projectDir, manifestSpec.tracks);
|
|
@@ -2504,15 +2621,27 @@ function writeEnvironmentFiles(projectDir, tracks) {
|
|
|
2504
2621
|
gitignoreNpxSkillsAdded: addGitignoreNpxSkillsAgents(projectDir)
|
|
2505
2622
|
};
|
|
2506
2623
|
}
|
|
2507
|
-
function runCliTransforms(spec, harnessRoot, projectDir, selectedInternalSkills) {
|
|
2624
|
+
function runCliTransforms(spec, harnessRoot, projectDir, selectedInternalSkills, previousExternal) {
|
|
2625
|
+
const baseline = new Map(previousExternal.map((f) => [f.path, f.sha256]));
|
|
2626
|
+
const externalFiles = [];
|
|
2627
|
+
const externalBackups = [];
|
|
2628
|
+
const absorb = (report) => {
|
|
2629
|
+
for (const f of report.ownership.files) {
|
|
2630
|
+
baseline.set(f.path, f.sha256);
|
|
2631
|
+
externalFiles.push(f);
|
|
2632
|
+
}
|
|
2633
|
+
externalBackups.push(...report.ownership.backupPaths);
|
|
2634
|
+
};
|
|
2508
2635
|
let codex = null;
|
|
2509
2636
|
let codexOptIn = null;
|
|
2510
2637
|
if (spec.cli.includes("codex")) {
|
|
2511
2638
|
codex = runCodexTransform({
|
|
2512
2639
|
harnessRoot,
|
|
2513
2640
|
projectDir,
|
|
2514
|
-
selectedInternalSkills
|
|
2641
|
+
selectedInternalSkills,
|
|
2642
|
+
baseline
|
|
2515
2643
|
});
|
|
2644
|
+
absorb(codex);
|
|
2516
2645
|
const installScope = spec.scope ?? "project";
|
|
2517
2646
|
if (installScope === "global" && spec.options.withCodexTrust) {
|
|
2518
2647
|
codexOptIn = runCodexOptIn({ projectDir });
|
|
@@ -2520,17 +2649,20 @@ function runCliTransforms(spec, harnessRoot, projectDir, selectedInternalSkills)
|
|
|
2520
2649
|
}
|
|
2521
2650
|
let opencode = null;
|
|
2522
2651
|
if (spec.cli.includes("opencode")) {
|
|
2523
|
-
opencode = runOpencodeTransform({ harnessRoot, projectDir, selectedInternalSkills });
|
|
2652
|
+
opencode = runOpencodeTransform({ harnessRoot, projectDir, selectedInternalSkills, baseline });
|
|
2653
|
+
absorb(opencode);
|
|
2524
2654
|
}
|
|
2525
2655
|
let antigravity = null;
|
|
2526
2656
|
if (spec.cli.includes("antigravity")) {
|
|
2527
2657
|
antigravity = runAntigravityTransform({
|
|
2528
2658
|
harnessRoot,
|
|
2529
2659
|
projectDir,
|
|
2530
|
-
selectedInternalSkills
|
|
2660
|
+
selectedInternalSkills,
|
|
2661
|
+
baseline
|
|
2531
2662
|
});
|
|
2663
|
+
absorb(antigravity);
|
|
2532
2664
|
}
|
|
2533
|
-
return { codex, codexOptIn, opencode, antigravity };
|
|
2665
|
+
return { codex, codexOptIn, opencode, antigravity, externalFiles, externalBackups };
|
|
2534
2666
|
}
|
|
2535
2667
|
function runExternalPhase(ctx) {
|
|
2536
2668
|
if (ctx.runExternal === null) {
|
|
@@ -2568,7 +2700,7 @@ function runExternalPhase(ctx) {
|
|
|
2568
2700
|
ctx.onProgress?.({ type: "external-complete", report: external });
|
|
2569
2701
|
return external;
|
|
2570
2702
|
}
|
|
2571
|
-
function writeInstallLogSafe(ctx, external, rootClaudeMdLog, previousLog, claudeDirMovedAside, rootFiles) {
|
|
2703
|
+
function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, previousLog, claudeDirMovedAside, rootFiles) {
|
|
2572
2704
|
try {
|
|
2573
2705
|
const log = buildInstallLog(
|
|
2574
2706
|
ctx.spec,
|
|
@@ -2580,7 +2712,14 @@ function writeInstallLogSafe(ctx, external, rootClaudeMdLog, previousLog, claude
|
|
|
2580
2712
|
rootFiles
|
|
2581
2713
|
);
|
|
2582
2714
|
const skillFiles = collectSkillHashes(ctx.projectDir);
|
|
2583
|
-
|
|
2715
|
+
const policyFiles = collectPolicyHashes(ctx.projectDir, join11(ctx.harnessRoot, "templates"));
|
|
2716
|
+
const merged = mergeExternalFiles(ctx.projectDir, previousLog?.externalFiles, externalFiles);
|
|
2717
|
+
writeInstallLog(ctx.projectDir, {
|
|
2718
|
+
...log,
|
|
2719
|
+
...skillFiles.length > 0 ? { skillFiles } : {},
|
|
2720
|
+
...policyFiles.length > 0 ? { policyFiles } : {},
|
|
2721
|
+
...merged.length > 0 ? { externalFiles: merged } : {}
|
|
2722
|
+
});
|
|
2584
2723
|
} catch (e2) {
|
|
2585
2724
|
ctx.onProgress?.({
|
|
2586
2725
|
type: "install-log-error",
|
|
@@ -2605,7 +2744,7 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
|
|
|
2605
2744
|
const sourceHook = join11(harnessRoot, "templates/hooks/karpathy-gate.sh");
|
|
2606
2745
|
const targetHook = join11(projectDir, KARPATHY_HOOK_RELPATH);
|
|
2607
2746
|
let hookScriptCopied = false;
|
|
2608
|
-
if (
|
|
2747
|
+
if (existsSync13(sourceHook)) {
|
|
2609
2748
|
copyFile(sourceHook, targetHook);
|
|
2610
2749
|
try {
|
|
2611
2750
|
chmodSync2(targetHook, 493);
|
|
@@ -2615,8 +2754,8 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
|
|
|
2615
2754
|
}
|
|
2616
2755
|
const settingsPath = join11(projectDir, ".claude/settings.json");
|
|
2617
2756
|
let settingsUpdated = false;
|
|
2618
|
-
if (
|
|
2619
|
-
const raw =
|
|
2757
|
+
if (existsSync13(settingsPath)) {
|
|
2758
|
+
const raw = readFileSync12(settingsPath, "utf8");
|
|
2620
2759
|
let before;
|
|
2621
2760
|
try {
|
|
2622
2761
|
before = JSON.parse(raw);
|
|
@@ -2627,7 +2766,7 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
|
|
|
2627
2766
|
const beforeStr = JSON.stringify(before);
|
|
2628
2767
|
const afterStr = JSON.stringify(after);
|
|
2629
2768
|
if (beforeStr !== afterStr) {
|
|
2630
|
-
|
|
2769
|
+
writeFileSync7(settingsPath, `${JSON.stringify(after, null, 2)}
|
|
2631
2770
|
`);
|
|
2632
2771
|
settingsUpdated = true;
|
|
2633
2772
|
}
|
|
@@ -2636,7 +2775,7 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
|
|
|
2636
2775
|
}
|
|
2637
2776
|
function composeAndWriteMcp(harnessRoot, projectDir, spec) {
|
|
2638
2777
|
const mcpPath = join11(projectDir, ".mcp.json");
|
|
2639
|
-
const created = !
|
|
2778
|
+
const created = !existsSync13(mcpPath);
|
|
2640
2779
|
const composed = composeMcpJson({
|
|
2641
2780
|
templateMcpPath: join11(harnessRoot, "templates/mcp.json"),
|
|
2642
2781
|
trackMapPath: join11(harnessRoot, "templates/track-mcp-map.tsv"),
|
|
@@ -2700,16 +2839,16 @@ function accumulateCategory(cats, entry) {
|
|
|
2700
2839
|
}
|
|
2701
2840
|
function writeInstalledTracks(projectDir, tracks) {
|
|
2702
2841
|
const path = join11(projectDir, ".claude/.installed-tracks");
|
|
2703
|
-
|
|
2842
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
2704
2843
|
const sorted = [...new Set(tracks)].sort().join("\n");
|
|
2705
|
-
|
|
2844
|
+
writeFileSync7(path, `${sorted}
|
|
2706
2845
|
`);
|
|
2707
2846
|
}
|
|
2708
2847
|
function writeRootClaudeMd(projectDir, tracks) {
|
|
2709
2848
|
const content = mergeProjectClaude(tracks, { projectName: basename4(projectDir) });
|
|
2710
2849
|
const target = join11(projectDir, "CLAUDE.md");
|
|
2711
2850
|
const backup = backupFileIfChanged(target, content);
|
|
2712
|
-
|
|
2851
|
+
writeFileSync7(target, content);
|
|
2713
2852
|
return { content, backup };
|
|
2714
2853
|
}
|
|
2715
2854
|
function chmodHooksSync(hookDir) {
|
|
@@ -3033,6 +3172,15 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
|
|
|
3033
3172
|
)
|
|
3034
3173
|
);
|
|
3035
3174
|
}
|
|
3175
|
+
if (baseline.updateMode.policyBackedUp.length > 0) {
|
|
3176
|
+
log(
|
|
3177
|
+
assetRow(
|
|
3178
|
+
"skip",
|
|
3179
|
+
"edited policy files",
|
|
3180
|
+
`${baseline.updateMode.policyBackedUp.length} backed up as *.backup-<time>`
|
|
3181
|
+
)
|
|
3182
|
+
);
|
|
3183
|
+
}
|
|
3036
3184
|
if (baseline.updateMode.staleHookRefs.length > 0) {
|
|
3037
3185
|
log(
|
|
3038
3186
|
assetRow(
|
|
@@ -3381,7 +3529,7 @@ function registerInstallCommand(cli2) {
|
|
|
3381
3529
|
|
|
3382
3530
|
// src/commands/list.ts
|
|
3383
3531
|
init_esm_shims();
|
|
3384
|
-
import { existsSync as
|
|
3532
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
3385
3533
|
import { join as join12, resolve as resolve3 } from "path";
|
|
3386
3534
|
function listAction(options = {}, deps = {}) {
|
|
3387
3535
|
const log = deps.log ?? console.log;
|
|
@@ -3446,7 +3594,7 @@ function formatTemplateRows(log, projectDir) {
|
|
|
3446
3594
|
const rootMd = log.templates.rootClaudeMd;
|
|
3447
3595
|
if (rootMd) {
|
|
3448
3596
|
const path = join12(projectDir, rootMd.path);
|
|
3449
|
-
const modified =
|
|
3597
|
+
const modified = existsSync14(path) && hashContent(readFileSync13(path, "utf8")) !== rootMd.sha256;
|
|
3450
3598
|
rows.push(
|
|
3451
3599
|
modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
|
|
3452
3600
|
);
|
|
@@ -3454,7 +3602,7 @@ function formatTemplateRows(log, projectDir) {
|
|
|
3454
3602
|
return rows;
|
|
3455
3603
|
}
|
|
3456
3604
|
function formatRootFileRows(rootFiles, projectDir) {
|
|
3457
|
-
const present = rootFiles.filter((f) =>
|
|
3605
|
+
const present = rootFiles.filter((f) => existsSync14(join12(projectDir, f.path)));
|
|
3458
3606
|
const width = Math.max(0, ...present.map((f) => f.path.length));
|
|
3459
3607
|
return present.map(
|
|
3460
3608
|
(f) => ` ${c.dim(padDisplay(f.path, width))} ${c.dim(f.change === "created" ? "\uC0DD\uC131" : "\uBCD1\uD569")} ${c.dim(f.notes.join(" / "))}`
|
|
@@ -3471,7 +3619,7 @@ function registerListCommand(cli2) {
|
|
|
3471
3619
|
// src/commands/uninstall.ts
|
|
3472
3620
|
init_esm_shims();
|
|
3473
3621
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3474
|
-
import { existsSync as
|
|
3622
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14, rmSync } from "fs";
|
|
3475
3623
|
import { join as join13, resolve as resolve4 } from "path";
|
|
3476
3624
|
|
|
3477
3625
|
// src/uninstall-interactive.ts
|
|
@@ -4757,7 +4905,7 @@ function advisoryLines(plan, targetAssets, projectDir, templatesKept, rootFiles)
|
|
|
4757
4905
|
return lines;
|
|
4758
4906
|
}
|
|
4759
4907
|
function rootFileAdvisoryLines(rootFiles, projectDir) {
|
|
4760
|
-
const present = rootFiles.filter((f) =>
|
|
4908
|
+
const present = rootFiles.filter((f) => existsSync15(join13(projectDir, f.path)));
|
|
4761
4909
|
if (present.length === 0) return [];
|
|
4762
4910
|
return [
|
|
4763
4911
|
"",
|
|
@@ -4830,9 +4978,9 @@ function buildProjectReverseStep(asset, spawn) {
|
|
|
4830
4978
|
}
|
|
4831
4979
|
function settingsHasHookCommand(projectDir, command) {
|
|
4832
4980
|
const path = join13(projectDir, ".claude", "settings.json");
|
|
4833
|
-
if (!
|
|
4981
|
+
if (!existsSync15(path)) return false;
|
|
4834
4982
|
try {
|
|
4835
|
-
const parsed = JSON.parse(
|
|
4983
|
+
const parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
4836
4984
|
return Object.values(parsed.hooks ?? {}).some(
|
|
4837
4985
|
(matchers) => matchers.some((m) => m.hooks?.some((h2) => h2.command === command))
|
|
4838
4986
|
);
|
|
@@ -4857,7 +5005,7 @@ function manualAdvisoryLines(targetAssets, projectDir) {
|
|
|
4857
5005
|
`.claude/settings.json \u2014 hooks.PreToolUse \uC5D0\uC11C \uB2E4\uC74C command \uD56D\uBAA9 \uC0AD\uC81C:
|
|
4858
5006
|
${KARPATHY_HOOK_COMMAND}`
|
|
4859
5007
|
);
|
|
4860
|
-
if (
|
|
5008
|
+
if (existsSync15(join13(projectDir, KARPATHY_HOOK_RELPATH)))
|
|
4861
5009
|
items.push(`\`${KARPATHY_HOOK_RELPATH}\` \u2014 \uC0AD\uC81C`);
|
|
4862
5010
|
if (items.length === 0) return [];
|
|
4863
5011
|
return [
|
|
@@ -4901,8 +5049,8 @@ function rootClaudeMdModified(log, projectDir) {
|
|
|
4901
5049
|
const rootMd = log.templates.rootClaudeMd;
|
|
4902
5050
|
if (!rootMd) return false;
|
|
4903
5051
|
const path = join13(projectDir, rootMd.path);
|
|
4904
|
-
if (!
|
|
4905
|
-
return hashContent(
|
|
5052
|
+
if (!existsSync15(path)) return false;
|
|
5053
|
+
return hashContent(readFileSync14(path, "utf8")) !== rootMd.sha256;
|
|
4906
5054
|
}
|
|
4907
5055
|
function formatTemplateList(log) {
|
|
4908
5056
|
const items = [log.templates.claudeDir];
|
|
@@ -4914,7 +5062,7 @@ function defaultSpawn2(cmd, args) {
|
|
|
4914
5062
|
return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
|
|
4915
5063
|
}
|
|
4916
5064
|
function defaultRm(path) {
|
|
4917
|
-
if (
|
|
5065
|
+
if (existsSync15(path)) {
|
|
4918
5066
|
rmSync(path, { recursive: true, force: true });
|
|
4919
5067
|
}
|
|
4920
5068
|
}
|
|
@@ -4962,7 +5110,7 @@ import { resolve as resolve5 } from "path";
|
|
|
4962
5110
|
|
|
4963
5111
|
// src/state.ts
|
|
4964
5112
|
init_esm_shims();
|
|
4965
|
-
import { existsSync as
|
|
5113
|
+
import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
|
|
4966
5114
|
import { join as join14 } from "path";
|
|
4967
5115
|
var META_FILE = ".claude/.installed-tracks";
|
|
4968
5116
|
var LEGACY_SIGNATURES = [
|
|
@@ -4974,12 +5122,12 @@ var LEGACY_SIGNATURES = [
|
|
|
4974
5122
|
];
|
|
4975
5123
|
function detectInstallState(projectDir) {
|
|
4976
5124
|
const claudeDir = join14(projectDir, ".claude");
|
|
4977
|
-
const hasClaudeDir =
|
|
5125
|
+
const hasClaudeDir = existsSync16(claudeDir);
|
|
4978
5126
|
if (!hasClaudeDir) {
|
|
4979
5127
|
return { state: "new", tracks: [], source: "none", hasClaudeDir: false };
|
|
4980
5128
|
}
|
|
4981
5129
|
const metaPath = join14(projectDir, META_FILE);
|
|
4982
|
-
if (
|
|
5130
|
+
if (existsSync16(metaPath)) {
|
|
4983
5131
|
const tracks2 = readMetafile(metaPath);
|
|
4984
5132
|
return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
|
|
4985
5133
|
}
|
|
@@ -4987,7 +5135,7 @@ function detectInstallState(projectDir) {
|
|
|
4987
5135
|
return { state: "existing", tracks, source: "legacy", hasClaudeDir: true };
|
|
4988
5136
|
}
|
|
4989
5137
|
function readMetafile(path) {
|
|
4990
|
-
const raw =
|
|
5138
|
+
const raw = readFileSync15(path, "utf8");
|
|
4991
5139
|
const seen = /* @__PURE__ */ new Set();
|
|
4992
5140
|
for (const line of raw.split(/\s+/)) {
|
|
4993
5141
|
const trimmed = line.trim();
|
|
@@ -4999,12 +5147,12 @@ function readMetafile(path) {
|
|
|
4999
5147
|
}
|
|
5000
5148
|
function inferFromLegacySignatures(projectDir) {
|
|
5001
5149
|
const rulesDir = join14(projectDir, ".claude/rules");
|
|
5002
|
-
if (!
|
|
5150
|
+
if (!existsSync16(rulesDir)) {
|
|
5003
5151
|
return [];
|
|
5004
5152
|
}
|
|
5005
5153
|
const found = /* @__PURE__ */ new Set();
|
|
5006
5154
|
for (const sig of LEGACY_SIGNATURES) {
|
|
5007
|
-
if (
|
|
5155
|
+
if (existsSync16(join14(rulesDir, sig.rule))) {
|
|
5008
5156
|
found.add(sig.track);
|
|
5009
5157
|
}
|
|
5010
5158
|
}
|