@uzysjung/agent-harness 26.160.1 → 26.161.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +74 -66
- package/README.md +61 -53
- package/dist/{chunk-ZXWOY5VO.js → chunk-MBRLCU5O.js} +76 -3
- package/dist/chunk-MBRLCU5O.js.map +1 -0
- package/dist/index.js +2158 -1241
- package/dist/index.js.map +1 -1
- package/dist/trust-tier-drift.js +1 -1
- package/package.json +3 -3
- package/templates/skills/audit-harness-fit/README.md +1 -0
- package/templates/skills/audit-harness-fit/SKILL.md +3 -1
- package/templates/skills/audit-harness-fit/evals/scenarios.yaml +18 -0
- package/templates/skills/audit-harness-fit/references/audit.md +37 -0
- package/templates/skills/audit-service-gaps/SKILL.md +7 -12
- package/templates/skills/compaction-handoff/SKILL.md +5 -6
- package/templates/skills/external-model-consult/SKILL.md +9 -13
- package/templates/skills/gh-issue-workflow/SKILL.md +7 -12
- package/templates/skills/model-orchestration/SKILL.md +57 -17
- package/templates/skills/multi-persona-review/SKILL.md +6 -10
- package/templates/skills/north-star/SKILL.md +7 -13
- package/templates/skills/objective-brief/SKILL.md +7 -12
- package/templates/skills/recurrence-prevention/SKILL.md +7 -11
- package/templates/skills/self-hosted-github-runner/SKILL.md +6 -7
- package/templates/skills/ui-visual-review/SKILL.md +7 -1
- package/dist/chunk-ZXWOY5VO.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
CATEGORIES,
|
|
5
5
|
CATEGORY_TITLES,
|
|
6
6
|
CLI_BASES,
|
|
7
|
+
CLI_BASE_SORT_ORDER,
|
|
7
8
|
CONTINUOUS_SKILLS,
|
|
8
9
|
CONTINUOUS_SKILLS_HEADING,
|
|
9
10
|
DEFAULT_OPTIONS,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
HARNESS_ANCHOR_FILE,
|
|
12
13
|
HARNESS_IMPORT_LINE,
|
|
13
14
|
INSTALL_LOG_DIR,
|
|
15
|
+
INSTALL_LOG_FILENAME,
|
|
14
16
|
INTERNAL_BUNDLED_SKILL_IDS,
|
|
15
17
|
POLICY_DIRS,
|
|
16
18
|
RENAMED_SKILL_IDS,
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
hashContent,
|
|
48
50
|
init_esm_shims,
|
|
49
51
|
installLogPath,
|
|
52
|
+
installedClis,
|
|
50
53
|
isAssetSelected,
|
|
51
54
|
isCliBase,
|
|
52
55
|
isCliNeutralTarget,
|
|
@@ -57,6 +60,7 @@ import {
|
|
|
57
60
|
legacyInstallLogPath,
|
|
58
61
|
listFilesRecursive,
|
|
59
62
|
mergeExternalFiles,
|
|
63
|
+
parseCliTargets,
|
|
60
64
|
readInstallLog,
|
|
61
65
|
renderContinuousSkillsNote,
|
|
62
66
|
renderFillScaffold,
|
|
@@ -64,12 +68,14 @@ import {
|
|
|
64
68
|
resolveBundleRoot,
|
|
65
69
|
resolveRules,
|
|
66
70
|
resolveScope,
|
|
71
|
+
scaffoldBanner,
|
|
67
72
|
stripHarnessImport,
|
|
68
73
|
summarizeContextCost,
|
|
74
|
+
targetsInclude,
|
|
69
75
|
upsertHarnessImport,
|
|
70
76
|
withContinuousSkillsNote,
|
|
71
77
|
writeInstallLog
|
|
72
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-MBRLCU5O.js";
|
|
73
79
|
|
|
74
80
|
// node_modules/sisteransi/src/index.js
|
|
75
81
|
var require_src = __commonJS({
|
|
@@ -754,7 +760,7 @@ var cac = (name = "") => new CAC(name);
|
|
|
754
760
|
// package.json
|
|
755
761
|
var package_default = {
|
|
756
762
|
name: "@uzysjung/agent-harness",
|
|
757
|
-
version: "26.
|
|
763
|
+
version: "26.161.0",
|
|
758
764
|
description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
|
|
759
765
|
type: "module",
|
|
760
766
|
publishConfig: {
|
|
@@ -799,10 +805,10 @@ var package_default = {
|
|
|
799
805
|
devDependencies: {
|
|
800
806
|
"@biomejs/biome": "^2.4.13",
|
|
801
807
|
"@types/node": "^25.6.0",
|
|
802
|
-
"@vitest/coverage-v8": "^4.1.
|
|
808
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
803
809
|
tsup: "^8.3.0",
|
|
804
810
|
typescript: "^5.6.0",
|
|
805
|
-
vitest: "^4.1.
|
|
811
|
+
vitest: "^4.1.11"
|
|
806
812
|
},
|
|
807
813
|
license: "MIT",
|
|
808
814
|
repository: {
|
|
@@ -894,55 +900,6 @@ function withBaselineHints(targets, templatesDir) {
|
|
|
894
900
|
});
|
|
895
901
|
}
|
|
896
902
|
|
|
897
|
-
// src/cli-targets.ts
|
|
898
|
-
init_esm_shims();
|
|
899
|
-
var CLI_BASE_SORT_ORDER = {
|
|
900
|
-
claude: 0,
|
|
901
|
-
codex: 1,
|
|
902
|
-
opencode: 2,
|
|
903
|
-
antigravity: 3
|
|
904
|
-
};
|
|
905
|
-
function parseCliTargets(input) {
|
|
906
|
-
const items = normalizeInput(input);
|
|
907
|
-
if (items.length === 0) {
|
|
908
|
-
return { ok: true, targets: ["claude"], warnings: [] };
|
|
909
|
-
}
|
|
910
|
-
const collected = /* @__PURE__ */ new Set();
|
|
911
|
-
const warnings = [];
|
|
912
|
-
for (const item of items) {
|
|
913
|
-
if (!isCliBase(item)) {
|
|
914
|
-
let hint = "";
|
|
915
|
-
if (item === "both") {
|
|
916
|
-
hint = "\n v0.8.0 removed 'both' alias. Use --cli claude --cli codex.";
|
|
917
|
-
} else if (item === "all") {
|
|
918
|
-
hint = "\n v0.8.0 removed 'all' alias. Use --cli claude --cli codex --cli opencode.";
|
|
919
|
-
} else if (item.includes(",")) {
|
|
920
|
-
hint = "\n Tip: comma-separated not supported. Use --cli A --cli B for multiple.";
|
|
921
|
-
}
|
|
922
|
-
return {
|
|
923
|
-
ok: false,
|
|
924
|
-
targets: ["claude"],
|
|
925
|
-
warnings,
|
|
926
|
-
error: `Invalid --cli value: ${item}. Must be one of: ${CLI_BASES.join(" | ")}${hint}`
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
collected.add(item);
|
|
930
|
-
}
|
|
931
|
-
const targets = [...collected].sort((a, b2) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b2]);
|
|
932
|
-
return { ok: true, targets, warnings };
|
|
933
|
-
}
|
|
934
|
-
function normalizeInput(input) {
|
|
935
|
-
if (input === void 0 || input === null) return [];
|
|
936
|
-
if (typeof input === "string") {
|
|
937
|
-
const trimmed = input.trim();
|
|
938
|
-
return trimmed === "" ? [] : [trimmed];
|
|
939
|
-
}
|
|
940
|
-
return input.filter((s) => typeof s === "string" && s.trim() !== "").map((s) => s.trim());
|
|
941
|
-
}
|
|
942
|
-
function targetsInclude(targets, base) {
|
|
943
|
-
return targets.includes(base);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
903
|
// src/design.ts
|
|
947
904
|
init_esm_shims();
|
|
948
905
|
var isColorEnabled = (() => {
|
|
@@ -1030,62 +987,18 @@ function padDisplay(s, width) {
|
|
|
1030
987
|
init_esm_shims();
|
|
1031
988
|
import {
|
|
1032
989
|
chmodSync as chmodSync3,
|
|
1033
|
-
existsSync as
|
|
990
|
+
existsSync as existsSync15,
|
|
1034
991
|
mkdirSync as mkdirSync4,
|
|
1035
992
|
readdirSync as readdirSync4,
|
|
1036
|
-
readFileSync as
|
|
993
|
+
readFileSync as readFileSync14,
|
|
1037
994
|
writeFileSync as writeFileSync6
|
|
1038
995
|
} from "fs";
|
|
1039
|
-
import { basename as basename5, dirname as dirname4, join as
|
|
996
|
+
import { basename as basename5, dirname as dirname4, join as join16, resolve } from "path";
|
|
1040
997
|
|
|
1041
|
-
// src/
|
|
998
|
+
// src/anchor-seed.ts
|
|
1042
999
|
init_esm_shims();
|
|
1043
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1000
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1044
1001
|
import { join as join2 } from "path";
|
|
1045
|
-
var PYTHON_TRACKS = "data|csr-fastapi|full";
|
|
1046
|
-
var VARIANTS = [
|
|
1047
|
-
{
|
|
1048
|
-
source: "ci-node.yml",
|
|
1049
|
-
target: "ci.yml",
|
|
1050
|
-
applies: (tracks) => anyTrack(tracks, "csr-*|ssr-*|tooling|full") || !anyTrack(tracks, PYTHON_TRACKS)
|
|
1051
|
-
},
|
|
1052
|
-
{
|
|
1053
|
-
source: "ci-python.yml",
|
|
1054
|
-
target: "ci-python.yml",
|
|
1055
|
-
applies: (tracks) => anyTrack(tracks, PYTHON_TRACKS)
|
|
1056
|
-
},
|
|
1057
|
-
{
|
|
1058
|
-
source: "e2e.yml",
|
|
1059
|
-
target: "e2e.yml",
|
|
1060
|
-
applies: (tracks) => hasUiTrack(tracks)
|
|
1061
|
-
}
|
|
1062
|
-
];
|
|
1063
|
-
function installCiScaffold(ctx) {
|
|
1064
|
-
const report = { written: [], skippedExisting: [] };
|
|
1065
|
-
for (const v2 of VARIANTS) {
|
|
1066
|
-
if (!v2.applies(ctx.tracks)) continue;
|
|
1067
|
-
const relTarget = `.github/workflows/${v2.target}`;
|
|
1068
|
-
const target = join2(ctx.projectDir, relTarget);
|
|
1069
|
-
if (existsSync2(target)) {
|
|
1070
|
-
report.skippedExisting.push(relTarget);
|
|
1071
|
-
continue;
|
|
1072
|
-
}
|
|
1073
|
-
copyFile(join2(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
|
|
1074
|
-
report.written.push(relTarget);
|
|
1075
|
-
}
|
|
1076
|
-
return report;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// src/cli-transforms.ts
|
|
1080
|
-
init_esm_shims();
|
|
1081
|
-
|
|
1082
|
-
// src/antigravity/transform.ts
|
|
1083
|
-
init_esm_shims();
|
|
1084
|
-
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
1085
|
-
import { basename, join as join6 } from "path";
|
|
1086
|
-
|
|
1087
|
-
// src/codex/agents-md.ts
|
|
1088
|
-
init_esm_shims();
|
|
1089
1002
|
|
|
1090
1003
|
// src/agents-md-merge.ts
|
|
1091
1004
|
init_esm_shims();
|
|
@@ -1223,6 +1136,11 @@ function trimBlankEdges(lines) {
|
|
|
1223
1136
|
while (out.length > 0 && (out[0] ?? "").trim() === "") out.shift();
|
|
1224
1137
|
return out;
|
|
1225
1138
|
}
|
|
1139
|
+
function projectContextInstallerLines(params) {
|
|
1140
|
+
const { existing, template } = params;
|
|
1141
|
+
const body = sectionBody(existing, sectionNames(template), CONTEXT_SECTION);
|
|
1142
|
+
return body === null ? null : installerLines(body, SKILLS_BLOCK);
|
|
1143
|
+
}
|
|
1226
1144
|
function stripHarnessFromAgentsMd(params) {
|
|
1227
1145
|
const { existing, template } = params;
|
|
1228
1146
|
const lines = existing.split("\n");
|
|
@@ -1244,7 +1162,161 @@ function stripHarnessFromAgentsMd(params) {
|
|
|
1244
1162
|
return out.join("\n");
|
|
1245
1163
|
}
|
|
1246
1164
|
|
|
1165
|
+
// src/anchor-seed.ts
|
|
1166
|
+
var SCAFFOLD_MARK = "SCAFFOLD \u2014 not filled in yet";
|
|
1167
|
+
var TRACK_NOTE_MARK = "Active track(s):";
|
|
1168
|
+
function readIfExists(path) {
|
|
1169
|
+
if (!existsSync2(path)) return null;
|
|
1170
|
+
try {
|
|
1171
|
+
return readFileSync2(path, "utf8");
|
|
1172
|
+
} catch {
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function dropHarnessQuoteBlocks(lines) {
|
|
1177
|
+
const out = [];
|
|
1178
|
+
let i = 0;
|
|
1179
|
+
while (i < lines.length) {
|
|
1180
|
+
if (!(lines[i] ?? "").trim().startsWith(">")) {
|
|
1181
|
+
out.push(lines[i] ?? "");
|
|
1182
|
+
i += 1;
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
let end = i;
|
|
1186
|
+
while (end < lines.length && (lines[end] ?? "").trim().startsWith(">")) end++;
|
|
1187
|
+
const run = lines.slice(i, end);
|
|
1188
|
+
const text = run.join("\n");
|
|
1189
|
+
if (!text.includes(SCAFFOLD_MARK) && !text.includes(TRACK_NOTE_MARK)) out.push(...run);
|
|
1190
|
+
i = end;
|
|
1191
|
+
}
|
|
1192
|
+
return out;
|
|
1193
|
+
}
|
|
1194
|
+
function dropTitle(lines) {
|
|
1195
|
+
const out = [...lines];
|
|
1196
|
+
while (out.length > 0 && (out[0] ?? "").trim() === "") out.shift();
|
|
1197
|
+
if (out.length > 0 && /^#\s/.test(out[0] ?? "")) out.shift();
|
|
1198
|
+
return out;
|
|
1199
|
+
}
|
|
1200
|
+
function compact2(lines) {
|
|
1201
|
+
return lines.map((l) => l.replace(/\r$/, "").trimEnd()).filter((l) => l.trim() !== "");
|
|
1202
|
+
}
|
|
1203
|
+
function isPristineScaffold(body, surface) {
|
|
1204
|
+
const pristine = compact2(dropHarnessQuoteBlocks(renderFillScaffold(surface).split("\n")));
|
|
1205
|
+
return compact2(body).join("\n") === pristine.join("\n");
|
|
1206
|
+
}
|
|
1207
|
+
function compose(body, into) {
|
|
1208
|
+
const trimmed = [...body];
|
|
1209
|
+
while (trimmed.length > 0 && (trimmed.at(-1) ?? "").trim() === "") trimmed.pop();
|
|
1210
|
+
while (trimmed.length > 0 && (trimmed[0] ?? "").trim() === "") trimmed.shift();
|
|
1211
|
+
if (trimmed.length === 0) return null;
|
|
1212
|
+
return `${scaffoldBanner(into)}
|
|
1213
|
+
|
|
1214
|
+
${trimmed.join("\n")}`;
|
|
1215
|
+
}
|
|
1216
|
+
function seedAgentsMdProjectContext(projectDir) {
|
|
1217
|
+
const root = readIfExists(join2(projectDir, "CLAUDE.md"));
|
|
1218
|
+
if (root === null) return null;
|
|
1219
|
+
const withoutImport = stripHarnessImport(root) ?? root;
|
|
1220
|
+
const body = dropHarnessQuoteBlocks(dropTitle(withoutImport.split("\n")));
|
|
1221
|
+
if (isPristineScaffold(body, "claude")) return null;
|
|
1222
|
+
return compose(body, "agents-md");
|
|
1223
|
+
}
|
|
1224
|
+
function seedRootClaudeProjectContext(projectDir, harnessRoot) {
|
|
1225
|
+
const agents = readIfExists(join2(projectDir, "AGENTS.md"));
|
|
1226
|
+
if (agents === null) return null;
|
|
1227
|
+
const template = readIfExists(join2(harnessRoot, "templates/codex/AGENTS.md.template")) ?? readIfExists(join2(harnessRoot, "templates/opencode/AGENTS.md.template"));
|
|
1228
|
+
if (template === null) return null;
|
|
1229
|
+
const installer = projectContextInstallerLines({ existing: agents, template });
|
|
1230
|
+
if (installer === null) return null;
|
|
1231
|
+
const body = dropHarnessQuoteBlocks(installer);
|
|
1232
|
+
if (isPristineScaffold(body, "agents-md")) return null;
|
|
1233
|
+
return compose(body, "claude");
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/ci-scaffold.ts
|
|
1237
|
+
init_esm_shims();
|
|
1238
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1239
|
+
import { join as join3 } from "path";
|
|
1240
|
+
var PYTHON_TRACKS = "data|csr-fastapi|full";
|
|
1241
|
+
var VARIANTS = [
|
|
1242
|
+
{
|
|
1243
|
+
source: "ci-node.yml",
|
|
1244
|
+
target: "ci.yml",
|
|
1245
|
+
applies: (tracks) => anyTrack(tracks, "csr-*|ssr-*|tooling|full") || !anyTrack(tracks, PYTHON_TRACKS)
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
source: "ci-python.yml",
|
|
1249
|
+
target: "ci-python.yml",
|
|
1250
|
+
applies: (tracks) => anyTrack(tracks, PYTHON_TRACKS)
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
source: "e2e.yml",
|
|
1254
|
+
target: "e2e.yml",
|
|
1255
|
+
applies: (tracks) => hasUiTrack(tracks)
|
|
1256
|
+
}
|
|
1257
|
+
];
|
|
1258
|
+
function installCiScaffold(ctx) {
|
|
1259
|
+
const report = { written: [], skippedExisting: [] };
|
|
1260
|
+
for (const v2 of VARIANTS) {
|
|
1261
|
+
if (!v2.applies(ctx.tracks)) continue;
|
|
1262
|
+
const relTarget = `.github/workflows/${v2.target}`;
|
|
1263
|
+
const target = join3(ctx.projectDir, relTarget);
|
|
1264
|
+
if (existsSync3(target)) {
|
|
1265
|
+
report.skippedExisting.push(relTarget);
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
copyFile(join3(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
|
|
1269
|
+
report.written.push(relTarget);
|
|
1270
|
+
}
|
|
1271
|
+
return report;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/cli-transforms.ts
|
|
1275
|
+
init_esm_shims();
|
|
1276
|
+
|
|
1277
|
+
// src/antigravity/transform.ts
|
|
1278
|
+
init_esm_shims();
|
|
1279
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
1280
|
+
import { basename, join as join8 } from "path";
|
|
1281
|
+
|
|
1282
|
+
// src/agents-skill-targets.ts
|
|
1283
|
+
init_esm_shims();
|
|
1284
|
+
import { join as join4, sep } from "path";
|
|
1285
|
+
function bundledSkillTargets(projectDir) {
|
|
1286
|
+
const log = readInstallLog(projectDir);
|
|
1287
|
+
if (log === null) return [];
|
|
1288
|
+
const tracks = log.spec.tracks.filter(
|
|
1289
|
+
(t) => TRACKS.includes(t)
|
|
1290
|
+
);
|
|
1291
|
+
if (tracks.length === 0) return [];
|
|
1292
|
+
const skillExcluded = new Set(log.spec.skillExclude ?? []);
|
|
1293
|
+
const baselineExcluded = new Set(log.spec.baselineExclude ?? []);
|
|
1294
|
+
return buildAssetSpec({ tracks, options: DEFAULT_OPTIONS }).selectedInternalSkills.filter(
|
|
1295
|
+
(id) => !skillExcluded.has(id) && !isBaselineExcluded(`.claude/skills/${id}`, baselineExcluded)
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
function allowSkillCreation(writer, projectDir, ids) {
|
|
1299
|
+
if (ids.length === 0) return writer;
|
|
1300
|
+
const roots = ids.map((id) => `${join4(projectDir, ".agents", "skills", id)}${sep}`);
|
|
1301
|
+
return {
|
|
1302
|
+
write(absPath, content, opts) {
|
|
1303
|
+
const inTarget = roots.some((root) => absPath.startsWith(root));
|
|
1304
|
+
return writer.write(absPath, content, inTarget ? { ...opts, createInRefresh: true } : opts);
|
|
1305
|
+
},
|
|
1306
|
+
result: () => writer.result()
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function agentsSkillSlot(params) {
|
|
1310
|
+
const { projectDir, selectedInternalSkills, refreshOnly, writer } = params;
|
|
1311
|
+
const creatable = refreshOnly ? bundledSkillTargets(projectDir) : [];
|
|
1312
|
+
return {
|
|
1313
|
+
skillIds: creatable.length === 0 ? selectedInternalSkills : [.../* @__PURE__ */ new Set([...selectedInternalSkills, ...creatable])],
|
|
1314
|
+
skillWriter: allowSkillCreation(writer, projectDir, creatable)
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1247
1318
|
// src/codex/agents-md.ts
|
|
1319
|
+
init_esm_shims();
|
|
1248
1320
|
function renameSlashes(text) {
|
|
1249
1321
|
return text.replaceAll("/uzys:", "/uzys-");
|
|
1250
1322
|
}
|
|
@@ -1256,8 +1328,8 @@ function renderAgentsMd(params) {
|
|
|
1256
1328
|
|
|
1257
1329
|
// src/codex/skills.ts
|
|
1258
1330
|
init_esm_shims();
|
|
1259
|
-
import { chmodSync, existsSync as
|
|
1260
|
-
import { join as
|
|
1331
|
+
import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
1332
|
+
import { join as join5 } from "path";
|
|
1261
1333
|
function renderBundledSkill(source) {
|
|
1262
1334
|
const trimmed = source.trimEnd();
|
|
1263
1335
|
const lines = trimmed.split(/\r?\n/);
|
|
@@ -1289,14 +1361,14 @@ function writeBundledSkillDirs(params) {
|
|
|
1289
1361
|
const { harnessRoot, projectDir, skillIds, writer } = params;
|
|
1290
1362
|
const written = [];
|
|
1291
1363
|
for (const id of skillIds) {
|
|
1292
|
-
const srcDir =
|
|
1293
|
-
const srcSkill =
|
|
1294
|
-
if (!
|
|
1364
|
+
const srcDir = join5(harnessRoot, "templates/skills", id);
|
|
1365
|
+
const srcSkill = join5(srcDir, "SKILL.md");
|
|
1366
|
+
if (!existsSync4(srcSkill)) {
|
|
1295
1367
|
continue;
|
|
1296
1368
|
}
|
|
1297
|
-
const targetDir =
|
|
1298
|
-
const targetSkill =
|
|
1299
|
-
if (!writer.write(targetSkill, renderBundledSkill(
|
|
1369
|
+
const targetDir = join5(projectDir, ".agents", "skills", id);
|
|
1370
|
+
const targetSkill = join5(targetDir, "SKILL.md");
|
|
1371
|
+
if (!writer.write(targetSkill, renderBundledSkill(readFileSync3(srcSkill, "utf8")))) {
|
|
1300
1372
|
continue;
|
|
1301
1373
|
}
|
|
1302
1374
|
written.push(targetSkill);
|
|
@@ -1304,10 +1376,10 @@ function writeBundledSkillDirs(params) {
|
|
|
1304
1376
|
if (rel === "SKILL.md") continue;
|
|
1305
1377
|
if (rel.split("/").some((segment) => segment.startsWith("."))) continue;
|
|
1306
1378
|
const segments = rel.split("/");
|
|
1307
|
-
const raw =
|
|
1379
|
+
const raw = readFileSync3(join5(srcDir, ...segments), "utf8");
|
|
1308
1380
|
const ported = rel.endsWith(".md") || rel.endsWith(".sh") ? `${portBody(raw)}
|
|
1309
1381
|
` : raw;
|
|
1310
|
-
const target =
|
|
1382
|
+
const target = join5(targetDir, ...segments);
|
|
1311
1383
|
if (!writer.write(target, ported, { createInRefresh: true })) continue;
|
|
1312
1384
|
if (rel.endsWith(".sh")) chmodSync(target, 493);
|
|
1313
1385
|
written.push(target);
|
|
@@ -1318,30 +1390,40 @@ function writeBundledSkillDirs(params) {
|
|
|
1318
1390
|
|
|
1319
1391
|
// src/owned-write.ts
|
|
1320
1392
|
init_esm_shims();
|
|
1321
|
-
import { existsSync as
|
|
1322
|
-
import { dirname, relative, sep } from "path";
|
|
1393
|
+
import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
1394
|
+
import { dirname, relative, sep as sep2 } from "path";
|
|
1323
1395
|
|
|
1324
1396
|
// src/foreign-slot.ts
|
|
1325
1397
|
init_esm_shims();
|
|
1326
|
-
import { lstatSync } from "fs";
|
|
1327
|
-
import { join as
|
|
1398
|
+
import { lstatSync, realpathSync, statSync } from "fs";
|
|
1399
|
+
import { join as join6 } from "path";
|
|
1328
1400
|
var SKILL_SLOT = /^((?:\.claude|\.agents)\/skills\/[^/]+)(?:\/|$)/;
|
|
1329
1401
|
function occupiedByNonDirectory(path) {
|
|
1330
1402
|
const stat = lstatSync(path, { throwIfNoEntry: false });
|
|
1331
1403
|
return stat !== void 0 && !stat.isDirectory();
|
|
1332
1404
|
}
|
|
1405
|
+
function linksToProjectSharedSkill(projectDir, id) {
|
|
1406
|
+
const slot = join6(projectDir, ".claude/skills", id);
|
|
1407
|
+
if (lstatSync(slot, { throwIfNoEntry: false })?.isSymbolicLink() !== true) return false;
|
|
1408
|
+
try {
|
|
1409
|
+
const real = realpathSync(slot);
|
|
1410
|
+
return real === join6(realpathSync(projectDir), ".agents/skills", id) && statSync(real).isDirectory();
|
|
1411
|
+
} catch {
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1333
1415
|
function foreignOwnedTarget(projectDir, rel) {
|
|
1334
1416
|
const slot = SKILL_SLOT.exec(rel)?.[1];
|
|
1335
1417
|
if (slot === void 0) return null;
|
|
1336
|
-
if (occupiedByNonDirectory(
|
|
1418
|
+
if (occupiedByNonDirectory(join6(projectDir, slot))) return slot;
|
|
1337
1419
|
if (rel !== slot) {
|
|
1338
1420
|
const parts = rel.slice(slot.length + 1).split("/");
|
|
1339
1421
|
let cursor = slot;
|
|
1340
1422
|
for (const part of parts.slice(0, -1)) {
|
|
1341
1423
|
cursor = `${cursor}/${part}`;
|
|
1342
|
-
if (occupiedByNonDirectory(
|
|
1424
|
+
if (occupiedByNonDirectory(join6(projectDir, cursor))) return cursor;
|
|
1343
1425
|
}
|
|
1344
|
-
const leaf = lstatSync(
|
|
1426
|
+
const leaf = lstatSync(join6(projectDir, rel), { throwIfNoEntry: false });
|
|
1345
1427
|
if (leaf !== void 0 && !leaf.isFile()) return rel;
|
|
1346
1428
|
}
|
|
1347
1429
|
return null;
|
|
@@ -1357,20 +1439,20 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
|
|
|
1357
1439
|
let updated = 0;
|
|
1358
1440
|
return {
|
|
1359
1441
|
write(absPath, content, opts) {
|
|
1360
|
-
const rel = relative(projectDir, absPath).split(
|
|
1442
|
+
const rel = relative(projectDir, absPath).split(sep2).join("/");
|
|
1361
1443
|
const foreign = foreignOwnedTarget(projectDir, rel);
|
|
1362
1444
|
if (foreign !== null) {
|
|
1363
1445
|
if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
|
|
1364
1446
|
return false;
|
|
1365
1447
|
}
|
|
1366
1448
|
const digest = hashContent(content);
|
|
1367
|
-
if (!
|
|
1449
|
+
if (!existsSync5(absPath)) {
|
|
1368
1450
|
if (refreshOnly && !opts?.createInRefresh) return false;
|
|
1369
1451
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
1370
1452
|
writeFileSync(absPath, content);
|
|
1371
1453
|
updated++;
|
|
1372
1454
|
} else {
|
|
1373
|
-
const current =
|
|
1455
|
+
const current = readFileSync4(absPath, "utf8");
|
|
1374
1456
|
if (current !== content) {
|
|
1375
1457
|
if (!isHarnessOwned(baseline, rel, current)) {
|
|
1376
1458
|
backupPaths.push(backupFile(absPath, now));
|
|
@@ -1397,8 +1479,8 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
|
|
|
1397
1479
|
|
|
1398
1480
|
// src/rules-port.ts
|
|
1399
1481
|
init_esm_shims();
|
|
1400
|
-
import { existsSync as
|
|
1401
|
-
import { join as
|
|
1482
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
1483
|
+
import { join as join7 } from "path";
|
|
1402
1484
|
function stripClaudeFrontmatter(source) {
|
|
1403
1485
|
if (!source.startsWith("---\n")) return source.trim();
|
|
1404
1486
|
const end = source.indexOf("\n---", 4);
|
|
@@ -1408,9 +1490,9 @@ function stripClaudeFrontmatter(source) {
|
|
|
1408
1490
|
function portRules(harnessRoot, ruleNames) {
|
|
1409
1491
|
const ported = [];
|
|
1410
1492
|
for (const name of ruleNames) {
|
|
1411
|
-
const src =
|
|
1412
|
-
if (!
|
|
1413
|
-
ported.push({ name, body: stripClaudeFrontmatter(
|
|
1493
|
+
const src = join7(harnessRoot, "templates/rules", `${name}.md`);
|
|
1494
|
+
if (!existsSync6(src)) continue;
|
|
1495
|
+
ported.push({ name, body: stripClaudeFrontmatter(readFileSync5(src, "utf8")) });
|
|
1414
1496
|
}
|
|
1415
1497
|
return ported;
|
|
1416
1498
|
}
|
|
@@ -1430,11 +1512,17 @@ function runAntigravityTransform(params) {
|
|
|
1430
1512
|
refreshOnly
|
|
1431
1513
|
} = params;
|
|
1432
1514
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1433
|
-
const
|
|
1515
|
+
const { skillIds, skillWriter } = agentsSkillSlot({
|
|
1516
|
+
projectDir,
|
|
1517
|
+
selectedInternalSkills,
|
|
1518
|
+
refreshOnly: refreshOnly ?? false,
|
|
1519
|
+
writer
|
|
1520
|
+
});
|
|
1521
|
+
const rulesFile = writeRules(harnessRoot, projectDir, writer, skillIds);
|
|
1434
1522
|
const antigravityInstalled = rulesFile !== null;
|
|
1435
1523
|
const harnessRuleFiles = [];
|
|
1436
1524
|
for (const rule of portRules(harnessRoot, rules)) {
|
|
1437
|
-
const target =
|
|
1525
|
+
const target = join8(projectDir, ".agents", "rules", `${rule.name}.md`);
|
|
1438
1526
|
if (!writer.write(target, `${rule.body}
|
|
1439
1527
|
`, { createInRefresh: antigravityInstalled })) {
|
|
1440
1528
|
continue;
|
|
@@ -1444,8 +1532,8 @@ function runAntigravityTransform(params) {
|
|
|
1444
1532
|
const skillFiles = writeBundledSkillDirs({
|
|
1445
1533
|
harnessRoot,
|
|
1446
1534
|
projectDir,
|
|
1447
|
-
skillIds
|
|
1448
|
-
writer
|
|
1535
|
+
skillIds,
|
|
1536
|
+
writer: skillWriter
|
|
1449
1537
|
});
|
|
1450
1538
|
return {
|
|
1451
1539
|
rulesFile,
|
|
@@ -1455,14 +1543,14 @@ function runAntigravityTransform(params) {
|
|
|
1455
1543
|
};
|
|
1456
1544
|
}
|
|
1457
1545
|
function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
|
|
1458
|
-
const claudeMdPath =
|
|
1459
|
-
const templatePath =
|
|
1460
|
-
if (!
|
|
1546
|
+
const claudeMdPath = join8(harnessRoot, "templates/CLAUDE.md");
|
|
1547
|
+
const templatePath = join8(harnessRoot, "templates/antigravity/AGENTS.md.template");
|
|
1548
|
+
if (!existsSync7(claudeMdPath) || !existsSync7(templatePath)) {
|
|
1461
1549
|
return null;
|
|
1462
1550
|
}
|
|
1463
|
-
const claudeMd =
|
|
1464
|
-
const template =
|
|
1465
|
-
const target =
|
|
1551
|
+
const claudeMd = readFileSync6(claudeMdPath, "utf8");
|
|
1552
|
+
const template = readFileSync6(templatePath, "utf8");
|
|
1553
|
+
const target = join8(projectDir, ".agents", "rules", "uzys-harness.md");
|
|
1466
1554
|
const rulesOut = renderAgentsMd({
|
|
1467
1555
|
template,
|
|
1468
1556
|
claudeMd,
|
|
@@ -1480,18 +1568,18 @@ function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
|
|
|
1480
1568
|
// src/codex/opt-in.ts
|
|
1481
1569
|
init_esm_shims();
|
|
1482
1570
|
import { homedir } from "os";
|
|
1483
|
-
import { join as
|
|
1571
|
+
import { join as join9 } from "path";
|
|
1484
1572
|
|
|
1485
1573
|
// src/codex/trust-entry.ts
|
|
1486
1574
|
init_esm_shims();
|
|
1487
|
-
import { existsSync as
|
|
1575
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
1488
1576
|
import { dirname as dirname2 } from "path";
|
|
1489
1577
|
var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
|
|
1490
1578
|
function registerTrustEntry(opts) {
|
|
1491
1579
|
const { configPath, projectDir } = opts;
|
|
1492
1580
|
try {
|
|
1493
1581
|
mkdirSync2(dirname2(configPath), { recursive: true });
|
|
1494
|
-
const existing =
|
|
1582
|
+
const existing = existsSync8(configPath) ? readFileSync7(configPath, "utf8") : "";
|
|
1495
1583
|
if (hasTrustEntry(existing, projectDir)) {
|
|
1496
1584
|
return { status: "already-present" };
|
|
1497
1585
|
}
|
|
@@ -1515,8 +1603,8 @@ function hasTrustEntry(configContent, projectDir) {
|
|
|
1515
1603
|
|
|
1516
1604
|
// src/codex/opt-in.ts
|
|
1517
1605
|
function runCodexOptIn(ctx) {
|
|
1518
|
-
const codexHome = ctx.codexHome ??
|
|
1519
|
-
const configPath =
|
|
1606
|
+
const codexHome = ctx.codexHome ?? join9(homedir(), ".codex");
|
|
1607
|
+
const configPath = join9(codexHome, "config.toml");
|
|
1520
1608
|
const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
|
|
1521
1609
|
return {
|
|
1522
1610
|
trustEntry: {
|
|
@@ -1529,8 +1617,8 @@ function runCodexOptIn(ctx) {
|
|
|
1529
1617
|
|
|
1530
1618
|
// src/codex/transform.ts
|
|
1531
1619
|
init_esm_shims();
|
|
1532
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
1533
|
-
import { basename as basename2, join as
|
|
1620
|
+
import { chmodSync as chmodSync2, existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
1621
|
+
import { basename as basename2, join as join10 } from "path";
|
|
1534
1622
|
|
|
1535
1623
|
// src/codex/config-toml.ts
|
|
1536
1624
|
init_esm_shims();
|
|
@@ -1604,38 +1692,49 @@ function runCodexTransform(params) {
|
|
|
1604
1692
|
selectedInternalSkills = [],
|
|
1605
1693
|
rules = [],
|
|
1606
1694
|
baseline,
|
|
1607
|
-
refreshOnly
|
|
1695
|
+
refreshOnly,
|
|
1696
|
+
writeAgentsMd = true
|
|
1608
1697
|
} = params;
|
|
1609
1698
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1610
|
-
const claudeMd = readRequired(
|
|
1611
|
-
const agentsTemplate = readRequired(
|
|
1612
|
-
const configTemplate = readRequired(
|
|
1699
|
+
const claudeMd = readRequired(join10(harnessRoot, "templates/CLAUDE.md"));
|
|
1700
|
+
const agentsTemplate = readRequired(join10(harnessRoot, "templates/codex/AGENTS.md.template"));
|
|
1701
|
+
const configTemplate = readRequired(join10(harnessRoot, "templates/codex/config.toml.template"));
|
|
1613
1702
|
const projectName = basename2(projectDir);
|
|
1614
|
-
const mcp = readOptionalJson(
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
projectName,
|
|
1621
|
-
// ADR-085 — 상시 스킬 안내는 앵커가 아니라 여기(프로젝트 맥락)에, 깔린 것만.
|
|
1622
|
-
// #503 — 그 조각은 설치자 소유 절 안에 사니 마커로 감싼다.
|
|
1623
|
-
projectContext: withMarkedContinuousSkillsNote(
|
|
1624
|
-
renderFillScaffold("agents-md"),
|
|
1625
|
-
selectedInternalSkills
|
|
1626
|
-
),
|
|
1627
|
-
// Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
|
|
1628
|
-
harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
|
|
1703
|
+
const mcp = readOptionalJson(join10(harnessRoot, ".mcp.json"));
|
|
1704
|
+
const { skillIds, skillWriter } = agentsSkillSlot({
|
|
1705
|
+
projectDir,
|
|
1706
|
+
selectedInternalSkills,
|
|
1707
|
+
refreshOnly: refreshOnly ?? false,
|
|
1708
|
+
writer
|
|
1629
1709
|
});
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
template: agentsTemplate
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1710
|
+
const agentsMdPath = join10(projectDir, "AGENTS.md");
|
|
1711
|
+
const seededContext = !writeAgentsMd || refreshOnly || existsSync9(agentsMdPath) ? null : seedAgentsMdProjectContext(projectDir);
|
|
1712
|
+
ensureDir(projectDir);
|
|
1713
|
+
if (writeAgentsMd) {
|
|
1714
|
+
const agentsMdOut = renderAgentsMd({
|
|
1715
|
+
template: agentsTemplate,
|
|
1716
|
+
claudeMd,
|
|
1717
|
+
projectName,
|
|
1718
|
+
// ADR-085 — 상시 스킬 안내는 앵커가 아니라 여기(프로젝트 맥락)에, 깔린 것만.
|
|
1719
|
+
// #503 — 그 조각은 설치자 소유 절 안에 사니 마커로 감싼다.
|
|
1720
|
+
// #530 — `skillIds` 를 쓴다(= 깔린 것 + **이번 실행이 만들 것**).
|
|
1721
|
+
projectContext: withMarkedContinuousSkillsNote(
|
|
1722
|
+
seededContext ?? renderFillScaffold("agents-md"),
|
|
1723
|
+
skillIds
|
|
1724
|
+
),
|
|
1725
|
+
// Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
|
|
1726
|
+
harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
|
|
1727
|
+
});
|
|
1728
|
+
writer.write(
|
|
1729
|
+
agentsMdPath,
|
|
1730
|
+
mergeAgentsMd({
|
|
1731
|
+
rendered: agentsMdOut,
|
|
1732
|
+
existing: existsSync9(agentsMdPath) ? readFileSync8(agentsMdPath, "utf8") : null,
|
|
1733
|
+
template: agentsTemplate
|
|
1734
|
+
})
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
const configTomlPath = join10(projectDir, ".codex/config.toml");
|
|
1639
1738
|
writer.write(
|
|
1640
1739
|
configTomlPath,
|
|
1641
1740
|
renderConfigToml({
|
|
@@ -1645,15 +1744,15 @@ function runCodexTransform(params) {
|
|
|
1645
1744
|
mcp
|
|
1646
1745
|
})
|
|
1647
1746
|
);
|
|
1648
|
-
const hookDir =
|
|
1747
|
+
const hookDir = join10(projectDir, ".codex/hooks");
|
|
1649
1748
|
const hookFiles = [];
|
|
1650
1749
|
for (const hook of HOOK_NAMES) {
|
|
1651
|
-
const src =
|
|
1652
|
-
if (!
|
|
1750
|
+
const src = join10(harnessRoot, "templates/hooks", `${hook}.sh`);
|
|
1751
|
+
if (!existsSync9(src)) {
|
|
1653
1752
|
continue;
|
|
1654
1753
|
}
|
|
1655
|
-
const ported =
|
|
1656
|
-
const target =
|
|
1754
|
+
const ported = readFileSync8(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
|
|
1755
|
+
const target = join10(hookDir, `${hook}.sh`);
|
|
1657
1756
|
if (!writer.write(target, ported)) continue;
|
|
1658
1757
|
chmodSync2(target, 493);
|
|
1659
1758
|
hookFiles.push(target);
|
|
@@ -1661,11 +1760,12 @@ function runCodexTransform(params) {
|
|
|
1661
1760
|
const skillFiles = writeBundledSkillDirs({
|
|
1662
1761
|
harnessRoot,
|
|
1663
1762
|
projectDir,
|
|
1664
|
-
skillIds
|
|
1665
|
-
writer
|
|
1763
|
+
skillIds,
|
|
1764
|
+
writer: skillWriter
|
|
1666
1765
|
});
|
|
1667
1766
|
return {
|
|
1668
1767
|
agentsMdPath,
|
|
1768
|
+
agentsMdSeededFrom: seededContext === null ? null : "CLAUDE.md",
|
|
1669
1769
|
configTomlPath,
|
|
1670
1770
|
hookFiles,
|
|
1671
1771
|
skillFiles,
|
|
@@ -1673,17 +1773,17 @@ function runCodexTransform(params) {
|
|
|
1673
1773
|
};
|
|
1674
1774
|
}
|
|
1675
1775
|
function readRequired(path) {
|
|
1676
|
-
if (!
|
|
1776
|
+
if (!existsSync9(path)) {
|
|
1677
1777
|
throw new Error(`Codex transform: required source missing: ${path}`);
|
|
1678
1778
|
}
|
|
1679
|
-
return
|
|
1779
|
+
return readFileSync8(path, "utf8");
|
|
1680
1780
|
}
|
|
1681
1781
|
function readOptionalJson(path) {
|
|
1682
|
-
if (!
|
|
1782
|
+
if (!existsSync9(path)) {
|
|
1683
1783
|
return null;
|
|
1684
1784
|
}
|
|
1685
1785
|
try {
|
|
1686
|
-
return JSON.parse(
|
|
1786
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
1687
1787
|
} catch {
|
|
1688
1788
|
return null;
|
|
1689
1789
|
}
|
|
@@ -1691,8 +1791,8 @@ function readOptionalJson(path) {
|
|
|
1691
1791
|
|
|
1692
1792
|
// src/opencode/transform.ts
|
|
1693
1793
|
init_esm_shims();
|
|
1694
|
-
import { existsSync as
|
|
1695
|
-
import { basename as basename3, join as
|
|
1794
|
+
import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync9, unlinkSync } from "fs";
|
|
1795
|
+
import { basename as basename3, join as join11 } from "path";
|
|
1696
1796
|
|
|
1697
1797
|
// src/opencode/agents-md.ts
|
|
1698
1798
|
init_esm_shims();
|
|
@@ -1735,24 +1835,33 @@ function runOpencodeTransform(params) {
|
|
|
1735
1835
|
refreshOnly
|
|
1736
1836
|
} = params;
|
|
1737
1837
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1738
|
-
const claudeMd = readRequired2(
|
|
1739
|
-
const agentsTemplate = readRequired2(
|
|
1838
|
+
const claudeMd = readRequired2(join11(harnessRoot, "templates/CLAUDE.md"));
|
|
1839
|
+
const agentsTemplate = readRequired2(join11(harnessRoot, "templates/opencode/AGENTS.md.template"));
|
|
1740
1840
|
const opencodeTemplate = readRequired2(
|
|
1741
|
-
|
|
1841
|
+
join11(harnessRoot, "templates/opencode/opencode.json.template")
|
|
1742
1842
|
);
|
|
1743
1843
|
const projectName = basename3(projectDir);
|
|
1744
|
-
const mcp = readOptionalJson2(
|
|
1844
|
+
const mcp = readOptionalJson2(join11(harnessRoot, ".mcp.json"));
|
|
1845
|
+
const { skillIds, skillWriter } = agentsSkillSlot({
|
|
1846
|
+
projectDir,
|
|
1847
|
+
selectedInternalSkills,
|
|
1848
|
+
refreshOnly: refreshOnly ?? false,
|
|
1849
|
+
writer
|
|
1850
|
+
});
|
|
1745
1851
|
ensureDir(projectDir);
|
|
1746
|
-
const agentsMdPath =
|
|
1852
|
+
const agentsMdPath = join11(projectDir, "AGENTS.md");
|
|
1853
|
+
const seededContext = refreshOnly || existsSync10(agentsMdPath) ? null : seedAgentsMdProjectContext(projectDir);
|
|
1747
1854
|
const agentsMdOut = renderAgentsMd2({
|
|
1748
1855
|
template: agentsTemplate,
|
|
1749
1856
|
claudeMd,
|
|
1750
1857
|
projectName,
|
|
1751
1858
|
// ADR-085 — 상시 스킬 안내는 앵커가 아니라 여기(프로젝트 맥락)에, 깔린 것만.
|
|
1752
1859
|
// #503 — 그 조각은 설치자 소유 절 안에 사니 마커로 감싼다 (codex 와 같은 마커).
|
|
1860
|
+
// #531 — `skillIds` 를 쓴다(= 깔린 것 + **이번 실행이 만들 것**). 안 그러면 이번에 새로
|
|
1861
|
+
// 깐 상시 스킬이 다음 update 까지 안내에서 빠져 설치자 에이전트가 영영 안 연다.
|
|
1753
1862
|
projectContext: withMarkedContinuousSkillsNote(
|
|
1754
|
-
renderFillScaffold("agents-md"),
|
|
1755
|
-
|
|
1863
|
+
seededContext ?? renderFillScaffold("agents-md"),
|
|
1864
|
+
skillIds
|
|
1756
1865
|
),
|
|
1757
1866
|
// codex 와 **같은 파일**(프로젝트 루트 `AGENTS.md`)이다. 두 transform 이 서로 다른 본문을
|
|
1758
1867
|
// 쓰면 나중에 도는 쪽이 앞선 쪽을 덮어써, codex+opencode 조합에서 룰이 통째로 사라진다
|
|
@@ -1763,26 +1872,26 @@ function runOpencodeTransform(params) {
|
|
|
1763
1872
|
agentsMdPath,
|
|
1764
1873
|
mergeAgentsMd({
|
|
1765
1874
|
rendered: agentsMdOut,
|
|
1766
|
-
existing:
|
|
1875
|
+
existing: existsSync10(agentsMdPath) ? readFileSync9(agentsMdPath, "utf8") : null,
|
|
1767
1876
|
template: agentsTemplate
|
|
1768
1877
|
})
|
|
1769
1878
|
);
|
|
1770
|
-
const opencodeJsonPath =
|
|
1879
|
+
const opencodeJsonPath = join11(projectDir, "opencode.json");
|
|
1771
1880
|
writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
|
|
1772
1881
|
const skillFiles = writeBundledSkillDirs({
|
|
1773
1882
|
harnessRoot,
|
|
1774
1883
|
projectDir,
|
|
1775
|
-
skillIds
|
|
1776
|
-
writer
|
|
1884
|
+
skillIds,
|
|
1885
|
+
writer: skillWriter
|
|
1777
1886
|
});
|
|
1778
1887
|
const retiredCommands = [];
|
|
1779
|
-
const cmdDir =
|
|
1780
|
-
if (
|
|
1888
|
+
const cmdDir = join11(projectDir, ".opencode/commands");
|
|
1889
|
+
if (existsSync10(cmdDir)) {
|
|
1781
1890
|
for (const entry of readdirSync(cmdDir)) {
|
|
1782
1891
|
if (!entry.endsWith(".md")) continue;
|
|
1783
1892
|
const id = entry.slice(0, -3);
|
|
1784
|
-
if (!
|
|
1785
|
-
const victim =
|
|
1893
|
+
if (!existsSync10(join11(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
|
|
1894
|
+
const victim = join11(cmdDir, entry);
|
|
1786
1895
|
try {
|
|
1787
1896
|
backupFile(victim);
|
|
1788
1897
|
unlinkSync(victim);
|
|
@@ -1793,6 +1902,7 @@ function runOpencodeTransform(params) {
|
|
|
1793
1902
|
}
|
|
1794
1903
|
return {
|
|
1795
1904
|
agentsMdPath,
|
|
1905
|
+
agentsMdSeededFrom: seededContext === null ? null : "CLAUDE.md",
|
|
1796
1906
|
opencodeJsonPath,
|
|
1797
1907
|
skillFiles,
|
|
1798
1908
|
retiredCommands,
|
|
@@ -1800,17 +1910,17 @@ function runOpencodeTransform(params) {
|
|
|
1800
1910
|
};
|
|
1801
1911
|
}
|
|
1802
1912
|
function readRequired2(path) {
|
|
1803
|
-
if (!
|
|
1913
|
+
if (!existsSync10(path)) {
|
|
1804
1914
|
throw new Error(`OpenCode transform: required source missing: ${path}`);
|
|
1805
1915
|
}
|
|
1806
|
-
return
|
|
1916
|
+
return readFileSync9(path, "utf8");
|
|
1807
1917
|
}
|
|
1808
1918
|
function readOptionalJson2(path) {
|
|
1809
|
-
if (!
|
|
1919
|
+
if (!existsSync10(path)) {
|
|
1810
1920
|
return null;
|
|
1811
1921
|
}
|
|
1812
1922
|
try {
|
|
1813
|
-
return JSON.parse(
|
|
1923
|
+
return JSON.parse(readFileSync9(path, "utf8"));
|
|
1814
1924
|
} catch {
|
|
1815
1925
|
return null;
|
|
1816
1926
|
}
|
|
@@ -1856,7 +1966,11 @@ function runCliTransforms(params) {
|
|
|
1856
1966
|
selectedInternalSkills,
|
|
1857
1967
|
rules,
|
|
1858
1968
|
baseline,
|
|
1859
|
-
refreshOnly
|
|
1969
|
+
refreshOnly,
|
|
1970
|
+
// #550 — 같은 `AGENTS.md` 를 한 실행에서 쓰는 쪽은 하나다. opencode 가 뒤에서 자기 템플릿으로
|
|
1971
|
+
// 쓰면 codex 판은 어차피 같은 실행 안에서 덮였고(최종 파일 = opencode 판), 매 실행 codex 가
|
|
1972
|
+
// opencode 판을 자기 판으로 뒤집는 한 번의 쓰기가 설치자 편집분을 백업으로 쌓았다.
|
|
1973
|
+
writeAgentsMd: !cli2.includes("opencode")
|
|
1860
1974
|
});
|
|
1861
1975
|
absorb(codex);
|
|
1862
1976
|
if (codexTrust) {
|
|
@@ -1902,8 +2016,8 @@ function runCliTransforms(params) {
|
|
|
1902
2016
|
|
|
1903
2017
|
// src/env-files.ts
|
|
1904
2018
|
init_esm_shims();
|
|
1905
|
-
import { appendFileSync, existsSync as
|
|
1906
|
-
import { join as
|
|
2019
|
+
import { appendFileSync, existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
|
|
2020
|
+
import { join as join12 } from "path";
|
|
1907
2021
|
var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
|
|
1908
2022
|
# Copy to .env (gitignored) and fill in values: cp .env.example .env
|
|
1909
2023
|
|
|
@@ -1940,24 +2054,24 @@ function writeEnvExample(projectDir, tracks) {
|
|
|
1940
2054
|
if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
|
|
1941
2055
|
return false;
|
|
1942
2056
|
}
|
|
1943
|
-
const path =
|
|
1944
|
-
if (
|
|
2057
|
+
const path = join12(projectDir, ".env.example");
|
|
2058
|
+
if (existsSync11(path)) {
|
|
1945
2059
|
return false;
|
|
1946
2060
|
}
|
|
1947
2061
|
writeFileSync3(path, ENV_EXAMPLE_BODY);
|
|
1948
2062
|
return true;
|
|
1949
2063
|
}
|
|
1950
2064
|
function addGitignoreEnv(projectDir) {
|
|
1951
|
-
const path =
|
|
1952
|
-
if (!
|
|
2065
|
+
const path = join12(projectDir, ".gitignore");
|
|
2066
|
+
if (!existsSync11(path)) {
|
|
1953
2067
|
return false;
|
|
1954
2068
|
}
|
|
1955
|
-
const content =
|
|
2069
|
+
const content = readFileSync10(path, "utf8");
|
|
1956
2070
|
if (GITIGNORE_ENV_PATTERN.test(content)) {
|
|
1957
2071
|
return false;
|
|
1958
2072
|
}
|
|
1959
|
-
const
|
|
1960
|
-
appendFileSync(path, `${
|
|
2073
|
+
const sep4 = content.endsWith("\n") ? "" : "\n";
|
|
2074
|
+
appendFileSync(path, `${sep4}
|
|
1961
2075
|
# Secret env (auto-added by agent-harness install)
|
|
1962
2076
|
.env
|
|
1963
2077
|
`);
|
|
@@ -1966,11 +2080,11 @@ function addGitignoreEnv(projectDir) {
|
|
|
1966
2080
|
var AGENT_ARTIFACT_DIRS = [".factory/", ".goose/", ".uzys-agent-harness/"];
|
|
1967
2081
|
var GITIGNORE_AGENT_ARTIFACT_HEADER = "# agent CLI / harness \uC790\uB3D9 \uC0DD\uC131\uBB3C (auto-added by agent-harness)";
|
|
1968
2082
|
function addGitignoreAgentArtifacts(projectDir) {
|
|
1969
|
-
const path =
|
|
1970
|
-
if (!
|
|
2083
|
+
const path = join12(projectDir, ".gitignore");
|
|
2084
|
+
if (!existsSync11(path)) {
|
|
1971
2085
|
return [];
|
|
1972
2086
|
}
|
|
1973
|
-
const content =
|
|
2087
|
+
const content = readFileSync10(path, "utf8");
|
|
1974
2088
|
const missing = AGENT_ARTIFACT_DIRS.filter((pattern) => {
|
|
1975
2089
|
const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
|
|
1976
2090
|
return !lineRegex.test(content);
|
|
@@ -1978,9 +2092,9 @@ function addGitignoreAgentArtifacts(projectDir) {
|
|
|
1978
2092
|
if (missing.length === 0) {
|
|
1979
2093
|
return [];
|
|
1980
2094
|
}
|
|
1981
|
-
const
|
|
2095
|
+
const sep4 = content.endsWith("\n") ? "" : "\n";
|
|
1982
2096
|
const block = [GITIGNORE_AGENT_ARTIFACT_HEADER, ...missing].join("\n");
|
|
1983
|
-
appendFileSync(path, `${
|
|
2097
|
+
appendFileSync(path, `${sep4}
|
|
1984
2098
|
${block}
|
|
1985
2099
|
`);
|
|
1986
2100
|
return [...missing];
|
|
@@ -1989,9 +2103,9 @@ ${block}
|
|
|
1989
2103
|
// src/external-installer.ts
|
|
1990
2104
|
init_esm_shims();
|
|
1991
2105
|
import { spawnSync } from "child_process";
|
|
1992
|
-
import { existsSync as
|
|
2106
|
+
import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
|
|
1993
2107
|
import { homedir as homedir2 } from "os";
|
|
1994
|
-
import { join as
|
|
2108
|
+
import { join as join13 } from "path";
|
|
1995
2109
|
var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
|
|
1996
2110
|
var NPM_SPAWN_TIMEOUT_MS = 6e5;
|
|
1997
2111
|
function spawnTimeoutFor(kind) {
|
|
@@ -2103,7 +2217,10 @@ function refreshExternalSkills(projectDir, deps = {}) {
|
|
|
2103
2217
|
tracks: log.spec.tracks,
|
|
2104
2218
|
// `forceInclude` 가 조건 판정을 앞지르므로 옵션 값은 결과에 영향을 주지 않는다.
|
|
2105
2219
|
options: DEFAULT_OPTIONS,
|
|
2106
|
-
|
|
2220
|
+
// #528 — 대상 CLI 는 **깔린 집합**(`installedClis`)이다. `spec.cli` 는 마지막 install 의
|
|
2221
|
+
// 요청이라 `uninstall --cli` 뒤에도 뺀 CLI 를 말하고, 반대로 그걸 비우면 `buildSkillArgs` 가
|
|
2222
|
+
// `--agent`·`--copy` 를 못 붙여 Claude 몫이 조용히 빠진다(재리뷰 BLOCKER-3).
|
|
2223
|
+
cli: [...installedClis(log)],
|
|
2107
2224
|
userOverride: { forceInclude: [...installedIds], forceExclude: [] },
|
|
2108
2225
|
scope: log.scope,
|
|
2109
2226
|
projectDir
|
|
@@ -2199,17 +2316,17 @@ function detectVersion(method, spawn) {
|
|
|
2199
2316
|
if (at2 <= 0) return void 0;
|
|
2200
2317
|
const plugin = method.pluginId.slice(0, at2);
|
|
2201
2318
|
const marketplaceShort = method.pluginId.slice(at2 + 1);
|
|
2202
|
-
const cacheBase =
|
|
2203
|
-
if (!
|
|
2319
|
+
const cacheBase = join13(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
|
|
2320
|
+
if (!existsSync12(cacheBase)) return void 0;
|
|
2204
2321
|
const versions = readdirSync2(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
|
|
2205
2322
|
return versions.at(-1);
|
|
2206
2323
|
}
|
|
2207
2324
|
case "npm": {
|
|
2208
2325
|
const npmRoot = getNpmGlobalRoot(spawn);
|
|
2209
2326
|
if (!npmRoot) return void 0;
|
|
2210
|
-
const pkgJson =
|
|
2211
|
-
if (!
|
|
2212
|
-
const parsed = JSON.parse(
|
|
2327
|
+
const pkgJson = join13(npmRoot, method.pkg, "package.json");
|
|
2328
|
+
if (!existsSync12(pkgJson)) return void 0;
|
|
2329
|
+
const parsed = JSON.parse(readFileSync11(pkgJson, "utf8"));
|
|
2213
2330
|
return parsed.version;
|
|
2214
2331
|
}
|
|
2215
2332
|
default:
|
|
@@ -2234,9 +2351,30 @@ function getNpmGlobalRoot(spawn) {
|
|
|
2234
2351
|
return void 0;
|
|
2235
2352
|
}
|
|
2236
2353
|
|
|
2354
|
+
// src/linked-skill-bodies.ts
|
|
2355
|
+
init_esm_shims();
|
|
2356
|
+
import { join as join14 } from "path";
|
|
2357
|
+
function refreshLinkedSkillBodies(params) {
|
|
2358
|
+
const { harnessRoot, projectDir, ids, baseline } = params;
|
|
2359
|
+
const bodyKey = (id) => `.agents/skills/${id}/SKILL.md`;
|
|
2360
|
+
const ours = ids.filter((id) => baseline.has(bodyKey(id)));
|
|
2361
|
+
const writer = createOwnedWriter(projectDir, baseline);
|
|
2362
|
+
const written = new Set(
|
|
2363
|
+
writeBundledSkillDirs({ harnessRoot, projectDir, skillIds: ours, writer })
|
|
2364
|
+
);
|
|
2365
|
+
const result = writer.result();
|
|
2366
|
+
return {
|
|
2367
|
+
updated: ours.filter((id) => written.has(join14(projectDir, bodyKey(id)))),
|
|
2368
|
+
notOurs: ids.filter((id) => !baseline.has(bodyKey(id))),
|
|
2369
|
+
files: result.files,
|
|
2370
|
+
backupPaths: result.backupPaths,
|
|
2371
|
+
foreignOwned: result.foreignOwned
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2237
2375
|
// src/mcp-merge.ts
|
|
2238
2376
|
init_esm_shims();
|
|
2239
|
-
import { existsSync as
|
|
2377
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
|
|
2240
2378
|
function parseTrackMcpMap(raw) {
|
|
2241
2379
|
const rows = [];
|
|
2242
2380
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -2287,15 +2425,15 @@ function mergeMcpServers(base, rows, tracks) {
|
|
|
2287
2425
|
return out;
|
|
2288
2426
|
}
|
|
2289
2427
|
function composeMcpJson(opts) {
|
|
2290
|
-
const base = JSON.parse(
|
|
2291
|
-
const merged = opts.existingPath &&
|
|
2292
|
-
const mapRaw =
|
|
2428
|
+
const base = JSON.parse(readFileSync12(opts.templateMcpPath, "utf8"));
|
|
2429
|
+
const merged = opts.existingPath && existsSync13(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
|
|
2430
|
+
const mapRaw = existsSync13(opts.trackMapPath) ? readFileSync12(opts.trackMapPath, "utf8") : "";
|
|
2293
2431
|
const rows = parseTrackMcpMap(mapRaw);
|
|
2294
2432
|
return mergeMcpServers(merged, rows, opts.tracks);
|
|
2295
2433
|
}
|
|
2296
2434
|
function mergeUserBase(base, existingPath) {
|
|
2297
2435
|
try {
|
|
2298
|
-
const existing = JSON.parse(
|
|
2436
|
+
const existing = JSON.parse(readFileSync12(existingPath, "utf8"));
|
|
2299
2437
|
return {
|
|
2300
2438
|
...base,
|
|
2301
2439
|
mcpServers: { ...base.mcpServers, ...existing.mcpServers }
|
|
@@ -2313,24 +2451,26 @@ function writeMcpJson(path, mcp) {
|
|
|
2313
2451
|
init_esm_shims();
|
|
2314
2452
|
import {
|
|
2315
2453
|
copyFileSync,
|
|
2316
|
-
existsSync as
|
|
2454
|
+
existsSync as existsSync14,
|
|
2317
2455
|
mkdirSync as mkdirSync3,
|
|
2318
2456
|
readdirSync as readdirSync3,
|
|
2319
|
-
readFileSync as
|
|
2457
|
+
readFileSync as readFileSync13,
|
|
2320
2458
|
rmdirSync,
|
|
2321
|
-
statSync,
|
|
2459
|
+
statSync as statSync2,
|
|
2322
2460
|
unlinkSync as unlinkSync2,
|
|
2323
2461
|
writeFileSync as writeFileSync5
|
|
2324
2462
|
} from "fs";
|
|
2325
|
-
import { basename as basename4, dirname as dirname3, join as
|
|
2463
|
+
import { basename as basename4, dirname as dirname3, join as join15 } from "path";
|
|
2326
2464
|
var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
|
|
2327
2465
|
function buildUpdateSpec(projectDir, tracks, only) {
|
|
2466
|
+
const log = readInstallLog(projectDir);
|
|
2467
|
+
const clis = log === null ? [] : installedClis(log);
|
|
2328
2468
|
return {
|
|
2329
2469
|
tracks: [...tracks],
|
|
2330
2470
|
options: DEFAULT_OPTIONS,
|
|
2331
|
-
cli: ["claude"],
|
|
2471
|
+
cli: clis.length > 0 ? [...clis] : ["claude"],
|
|
2332
2472
|
projectDir,
|
|
2333
|
-
scope:
|
|
2473
|
+
scope: log?.scope ?? "project",
|
|
2334
2474
|
// 전부 골랐으면 "제한 없음"과 같다 — 화면·기록에 제한이 있었던 것처럼 남기지 않는다.
|
|
2335
2475
|
...only !== void 0 && only.length > 0 && only.length < UPDATE_GROUPS.length ? { updateOnly: [...only] } : {}
|
|
2336
2476
|
};
|
|
@@ -2341,7 +2481,7 @@ function parseUpdateOnly(raw) {
|
|
|
2341
2481
|
return { ok: true, groups: [...new Set(raw)] };
|
|
2342
2482
|
}
|
|
2343
2483
|
function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
2344
|
-
const claudeDir =
|
|
2484
|
+
const claudeDir = join15(projectDir, ".claude");
|
|
2345
2485
|
const startedAt = Date.now();
|
|
2346
2486
|
const wants = (g) => only === void 0 || only.includes(g);
|
|
2347
2487
|
const report = {
|
|
@@ -2356,6 +2496,7 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2356
2496
|
rootImportAdded: false,
|
|
2357
2497
|
rootBlockRefreshed: false,
|
|
2358
2498
|
legacyAnchor: null,
|
|
2499
|
+
claudeUnrecorded: null,
|
|
2359
2500
|
skillsBackedUp: [],
|
|
2360
2501
|
skillsSkippedLinks: [],
|
|
2361
2502
|
skillsPruned: [],
|
|
@@ -2378,11 +2519,14 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2378
2519
|
report.installedNew = fresh.installed;
|
|
2379
2520
|
report.restored = fresh.restored;
|
|
2380
2521
|
report.needsReinstall = fresh.needsReinstall;
|
|
2522
|
+
const logAtStart = readInstallLog(projectDir);
|
|
2523
|
+
const claudeManaged = logAtStart === null || installedClis(logAtStart).includes("claude");
|
|
2524
|
+
report.claudeUnrecorded = !claudeManaged && logAtStart !== null && existsSync14(claudeDir) ? recordClaudeCommand(logAtStart, installedTracks(projectDir)) : null;
|
|
2381
2525
|
const policyBase = policyBaseline(projectDir);
|
|
2382
2526
|
for (const { dir, ext } of POLICY_DIRS) {
|
|
2383
|
-
if (!wants(dir === "hooks" ? "hooks" : "rules")) continue;
|
|
2384
|
-
const target =
|
|
2385
|
-
const source =
|
|
2527
|
+
if (!claudeManaged || !wants(dir === "hooks" ? "hooks" : "rules")) continue;
|
|
2528
|
+
const target = join15(claudeDir, dir);
|
|
2529
|
+
const source = join15(templatesDir, dir);
|
|
2386
2530
|
const label = `.claude/${dir}`;
|
|
2387
2531
|
const ctx = { prefix: dir, baseline: policyBase };
|
|
2388
2532
|
const synced = updateDir(target, source, ext, ctx);
|
|
@@ -2393,10 +2537,11 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2393
2537
|
const syncedDirs = POLICY_DIRS.filter((d2) => wants(d2.dir === "hooks" ? "hooks" : "rules")).map(
|
|
2394
2538
|
(d2) => d2.dir
|
|
2395
2539
|
);
|
|
2396
|
-
if (syncedDirs.length > 0)
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2540
|
+
if (claudeManaged && syncedDirs.length > 0)
|
|
2541
|
+
refreshPolicyBaseline(projectDir, templatesDir, syncedDirs);
|
|
2542
|
+
const skillSync = claudeManaged && wants("skills") ? syncSkills(
|
|
2543
|
+
join15(claudeDir, "skills"),
|
|
2544
|
+
join15(templatesDir, "skills"),
|
|
2400
2545
|
skillBaseline(projectDir),
|
|
2401
2546
|
/* @__PURE__ */ new Date(),
|
|
2402
2547
|
(relInSkills) => foreignOwnedTarget(projectDir, `.claude/skills/${relInSkills}`)
|
|
@@ -2405,25 +2550,36 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2405
2550
|
report.skillsBackedUp = skillSync.backedUp;
|
|
2406
2551
|
report.skillsSkippedLinks = skillSync.skippedLinks;
|
|
2407
2552
|
report.skillsPruned = skillSync.pruned;
|
|
2408
|
-
const newSkills = wants("new-skills") ? installNewSkillDirs(projectDir, templatesDir, installedTracks(projectDir)) : { installed: [], foreignOwned: [] };
|
|
2553
|
+
const newSkills = claudeManaged && wants("new-skills") ? installNewSkillDirs(projectDir, templatesDir, installedTracks(projectDir)) : { installed: [], restored: [], foreignOwned: [] };
|
|
2409
2554
|
report.installedNew.push(...newSkills.installed);
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2555
|
+
report.restored.push(...newSkills.restored);
|
|
2556
|
+
const copiedSkills = [...newSkills.installed, ...newSkills.restored];
|
|
2557
|
+
if (claudeManaged && wants("skills")) refreshSkillBaseline(projectDir, templatesDir);
|
|
2558
|
+
else if (claudeManaged && copiedSkills.length > 0)
|
|
2559
|
+
recordNewSkillBaseline(projectDir, templatesDir, copiedSkills);
|
|
2560
|
+
if (existsSync14(claudeDir) && claudeManaged) {
|
|
2415
2561
|
if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report);
|
|
2416
2562
|
}
|
|
2417
|
-
const settingsPath =
|
|
2418
|
-
if (wants("hooks") &&
|
|
2563
|
+
const settingsPath = join15(claudeDir, "settings.json");
|
|
2564
|
+
if (claudeManaged && wants("hooks") && existsSync14(settingsPath)) {
|
|
2419
2565
|
report.staleHookRefs = cleanStaleHookRefs(settingsPath, claudeDir);
|
|
2420
2566
|
}
|
|
2421
2567
|
if (wants("hooks")) report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
|
|
2422
2568
|
report.retiredAgents = staleAgentFiles(claudeDir);
|
|
2423
2569
|
report.demotedAgents = demotedAgentFiles(claudeDir, installedTracks(projectDir));
|
|
2424
|
-
const external = wants("external") ? refreshExternalCli(projectDir, harnessRoot) : {
|
|
2570
|
+
const external = wants("external") ? refreshExternalCli(projectDir, harnessRoot) : {
|
|
2571
|
+
externalUpdated: 0,
|
|
2572
|
+
externalBackedUp: [],
|
|
2573
|
+
externalForeignOwned: [],
|
|
2574
|
+
written: [],
|
|
2575
|
+
skillsInstalled: [],
|
|
2576
|
+
skillsRestored: []
|
|
2577
|
+
};
|
|
2425
2578
|
report.externalUpdated = external.externalUpdated;
|
|
2426
2579
|
report.externalBackedUp = external.externalBackedUp;
|
|
2580
|
+
report.installedNew.push(...external.skillsInstalled);
|
|
2581
|
+
report.restored.push(...external.skillsRestored);
|
|
2582
|
+
report.restoredWithout = restoredWithoutArgs(report.restored, installedTracks(projectDir));
|
|
2427
2583
|
const saidBySlotRow = new Set(report.skillsSkippedLinks.map((id) => `.claude/skills/${id}`));
|
|
2428
2584
|
const merged = [];
|
|
2429
2585
|
for (const f of [
|
|
@@ -2435,6 +2591,17 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2435
2591
|
if (!saidBySlotRow.has(f) && !merged.includes(f)) merged.push(f);
|
|
2436
2592
|
}
|
|
2437
2593
|
report.foreignOwned = merged;
|
|
2594
|
+
if (wants("external")) {
|
|
2595
|
+
const linked = report.skillsSkippedLinks.filter(
|
|
2596
|
+
(id) => linksToProjectSharedSkill(projectDir, id)
|
|
2597
|
+
);
|
|
2598
|
+
const refreshed = linked.filter(
|
|
2599
|
+
(id) => external.written.includes(`.agents/skills/${id}/SKILL.md`)
|
|
2600
|
+
);
|
|
2601
|
+
report.skillsLinked = refreshed;
|
|
2602
|
+
report.skillsLinkedNotOurs = linked.filter((id) => !refreshed.includes(id));
|
|
2603
|
+
report.skillsSkippedLinks = report.skillsSkippedLinks.filter((id) => !linked.includes(id));
|
|
2604
|
+
}
|
|
2438
2605
|
const skillRefresh = wants("external") ? (deps.refreshSkills ?? refreshExternalSkills)(projectDir) : { attempted: 0, refreshed: 0, failed: [], notInCatalog: [], unknown: false };
|
|
2439
2606
|
report.externalSkillsRefreshed = skillRefresh.refreshed;
|
|
2440
2607
|
report.externalSkillsFailed = skillRefresh.failed;
|
|
@@ -2448,20 +2615,20 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2448
2615
|
return report;
|
|
2449
2616
|
}
|
|
2450
2617
|
function staleSkillDirs(claudeDir) {
|
|
2451
|
-
const dir =
|
|
2452
|
-
if (!
|
|
2618
|
+
const dir = join15(claudeDir, "skills");
|
|
2619
|
+
if (!existsSync14(dir)) return [];
|
|
2453
2620
|
const stale = (id) => RENAMED_SKILL_IDS[id] !== void 0 || RETIRED_SKILL_IDS.includes(id);
|
|
2454
2621
|
return readdirSync3(dir, { withFileTypes: true }).filter((e2) => e2.isDirectory() && stale(e2.name)).map((e2) => e2.name);
|
|
2455
2622
|
}
|
|
2456
2623
|
function staleAgentFiles(claudeDir) {
|
|
2457
|
-
const dir =
|
|
2458
|
-
if (!
|
|
2624
|
+
const dir = join15(claudeDir, "agents");
|
|
2625
|
+
if (!existsSync14(dir)) return [];
|
|
2459
2626
|
return readdirSync3(dir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => basename4(e2.name, ".md")).filter((id) => RETIRED_AGENT_IDS.includes(id));
|
|
2460
2627
|
}
|
|
2461
2628
|
function demotedAgentFiles(claudeDir, tracks) {
|
|
2462
2629
|
if (tracks.length === 0) return [];
|
|
2463
|
-
const dir =
|
|
2464
|
-
if (!
|
|
2630
|
+
const dir = join15(claudeDir, "agents");
|
|
2631
|
+
if (!existsSync14(dir)) return [];
|
|
2465
2632
|
const demoted = TRACK_AGENTS.filter(([, pattern]) => !anyTrack(tracks, pattern)).map(
|
|
2466
2633
|
([id]) => id
|
|
2467
2634
|
);
|
|
@@ -2473,7 +2640,7 @@ function installNewAssets(projectDir, templatesDir, wants = () => true) {
|
|
|
2473
2640
|
const needsReinstall = [];
|
|
2474
2641
|
const foreignOwned = [];
|
|
2475
2642
|
const log = readInstallLog(projectDir);
|
|
2476
|
-
const claudeSelected = log
|
|
2643
|
+
const claudeSelected = installedClis(log).includes("claude");
|
|
2477
2644
|
const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
|
|
2478
2645
|
const priorBaseline = policyBaseline(projectDir);
|
|
2479
2646
|
for (const entry of trackOnlyFileAssets(installedTracks(projectDir))) {
|
|
@@ -2481,15 +2648,15 @@ function installNewAssets(projectDir, templatesDir, wants = () => true) {
|
|
|
2481
2648
|
if (!wants(group)) continue;
|
|
2482
2649
|
if (entry.target === HARNESS_ANCHOR_FILE) continue;
|
|
2483
2650
|
if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
|
|
2484
|
-
const target =
|
|
2651
|
+
const target = join15(projectDir, entry.target);
|
|
2485
2652
|
const foreign = foreignOwnedTarget(projectDir, entry.target);
|
|
2486
2653
|
if (foreign !== null) {
|
|
2487
2654
|
if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
|
|
2488
2655
|
continue;
|
|
2489
2656
|
}
|
|
2490
|
-
if (
|
|
2491
|
-
const source =
|
|
2492
|
-
if (!
|
|
2657
|
+
if (existsSync14(target)) continue;
|
|
2658
|
+
const source = join15(templatesDir, entry.source);
|
|
2659
|
+
if (!existsSync14(source)) continue;
|
|
2493
2660
|
if (entry.target.startsWith(".claude/")) {
|
|
2494
2661
|
if (!claudeSelected) continue;
|
|
2495
2662
|
if (entry.target.startsWith(".claude/hooks/") || entry.target === ".claude/settings.json") {
|
|
@@ -2511,26 +2678,26 @@ function collectRunBackups(projectDir, startedAt) {
|
|
|
2511
2678
|
const out = [];
|
|
2512
2679
|
const consider = (rel) => {
|
|
2513
2680
|
if (!BACKUP_SUFFIX.test(rel)) return;
|
|
2514
|
-
const abs =
|
|
2515
|
-
if (
|
|
2681
|
+
const abs = join15(projectDir, rel);
|
|
2682
|
+
if (statSync2(abs).mtimeMs < startedAt - 1e3) return;
|
|
2516
2683
|
out.push({ path: rel.replace(BACKUP_SUFFIX, ""), backup: rel });
|
|
2517
2684
|
};
|
|
2518
2685
|
for (const e2 of readdirSync3(projectDir, { withFileTypes: true }))
|
|
2519
2686
|
if (e2.isFile()) consider(e2.name);
|
|
2520
2687
|
for (const dir of BACKUP_SCAN_DIRS) {
|
|
2521
|
-
for (const rel of listFilesRecursive(
|
|
2688
|
+
for (const rel of listFilesRecursive(join15(projectDir, dir))) consider(`${dir}/${rel}`);
|
|
2522
2689
|
}
|
|
2523
2690
|
return out.sort((a, b2) => a.path.localeCompare(b2.path));
|
|
2524
2691
|
}
|
|
2525
2692
|
function writeBackupList(projectDir, backups) {
|
|
2526
|
-
const dir =
|
|
2527
|
-
const file =
|
|
2693
|
+
const dir = join15(projectDir, ".uzys-agent-harness");
|
|
2694
|
+
const file = join15(dir, "update-backups.json");
|
|
2528
2695
|
try {
|
|
2529
2696
|
if (backups.length === 0) {
|
|
2530
|
-
if (
|
|
2697
|
+
if (existsSync14(file)) unlinkSync2(file);
|
|
2531
2698
|
return;
|
|
2532
2699
|
}
|
|
2533
|
-
if (!
|
|
2700
|
+
if (!existsSync14(dir)) return;
|
|
2534
2701
|
writeFileSync5(
|
|
2535
2702
|
file,
|
|
2536
2703
|
`${JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString(), note: "update \uAC00 \uB0A8\uAE34 \uBC31\uC5C5 \uC30D. \uC6D0\uBCF8(path)\uC740 \uCD5C\uC2E0 \uBC88\uB4E4\uD310, \uBC31\uC5C5(backup)\uC740 \uB2F9\uC2E0\uC758 \uD3B8\uC9D1\uBD84. \uD3B8\uC9D1\uC744 \uC0C8 \uD310\uC5D0 \uB2E4\uC2DC \uC5B9\uC73C\uB824\uBA74 \uB450 \uD30C\uC77C\uC758 diff \uB97C \uBCF4\uB77C.", backups }, null, 2)}
|
|
@@ -2541,29 +2708,33 @@ function writeBackupList(projectDir, backups) {
|
|
|
2541
2708
|
}
|
|
2542
2709
|
function installNewSkillDirs(projectDir, templatesDir, tracks) {
|
|
2543
2710
|
const installed = [];
|
|
2711
|
+
const restored = [];
|
|
2544
2712
|
const foreignOwned = [];
|
|
2545
2713
|
const log = readInstallLog(projectDir);
|
|
2714
|
+
const priorIds = new Set((log?.skillFiles ?? []).map((f) => f.path.split("/")[0] ?? ""));
|
|
2546
2715
|
const excluded = new Set(log?.spec.baselineExclude ?? []);
|
|
2547
2716
|
const skillExcluded = new Set(log?.spec.skillExclude ?? []);
|
|
2548
|
-
if (!(log
|
|
2717
|
+
if (log !== null && !installedClis(log).includes("claude"))
|
|
2718
|
+
return { installed, restored, foreignOwned };
|
|
2549
2719
|
const spec = buildAssetSpec({ tracks, options: DEFAULT_OPTIONS });
|
|
2550
2720
|
for (const entry of buildManifest(spec)) {
|
|
2551
2721
|
if (entry.type !== "dir" || !entry.target.startsWith(".claude/skills/")) continue;
|
|
2552
2722
|
if (!entry.applies(spec) || isBaselineExcluded(entry.target, excluded)) continue;
|
|
2553
2723
|
if (skillExcluded.has(entry.target.slice(".claude/skills/".length))) continue;
|
|
2554
|
-
const target =
|
|
2724
|
+
const target = join15(projectDir, entry.target);
|
|
2555
2725
|
const foreign = foreignOwnedTarget(projectDir, entry.target);
|
|
2556
2726
|
if (foreign !== null) {
|
|
2557
2727
|
if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
|
|
2558
2728
|
continue;
|
|
2559
2729
|
}
|
|
2560
|
-
if (
|
|
2561
|
-
const source =
|
|
2562
|
-
if (!
|
|
2730
|
+
if (existsSync14(target)) continue;
|
|
2731
|
+
const source = join15(templatesDir, entry.source);
|
|
2732
|
+
if (!existsSync14(source)) continue;
|
|
2563
2733
|
copyDir(source, target, (rel) => foreignOwnedTarget(projectDir, `${entry.target}/${rel}`));
|
|
2564
|
-
|
|
2734
|
+
if (priorIds.has(entry.target.slice(".claude/skills/".length))) restored.push(entry.target);
|
|
2735
|
+
else installed.push(entry.target);
|
|
2565
2736
|
}
|
|
2566
|
-
return { installed, foreignOwned };
|
|
2737
|
+
return { installed, restored, foreignOwned };
|
|
2567
2738
|
}
|
|
2568
2739
|
function recordNewSkillBaseline(projectDir, templatesDir, targets) {
|
|
2569
2740
|
const log = readInstallLog(projectDir);
|
|
@@ -2587,16 +2758,16 @@ function trackOnlyFileAssets(tracks) {
|
|
|
2587
2758
|
);
|
|
2588
2759
|
}
|
|
2589
2760
|
function syncHarnessAnchor(projectDir, templatesDir, report, now = /* @__PURE__ */ new Date()) {
|
|
2590
|
-
if (
|
|
2761
|
+
if (existsSync14(join15(projectDir, LEGACY_ANCHOR_FILE))) {
|
|
2591
2762
|
report.legacyAnchor = LEGACY_ANCHOR_FILE;
|
|
2592
2763
|
}
|
|
2593
|
-
const templateMd =
|
|
2594
|
-
if (!
|
|
2595
|
-
const anchor =
|
|
2596
|
-
const existed =
|
|
2764
|
+
const templateMd = join15(templatesDir, "CLAUDE.md");
|
|
2765
|
+
if (!existsSync14(templateMd)) return;
|
|
2766
|
+
const anchor = join15(projectDir, HARNESS_ANCHOR_FILE);
|
|
2767
|
+
const existed = existsSync14(anchor);
|
|
2597
2768
|
if (existed) {
|
|
2598
|
-
const current =
|
|
2599
|
-
if (current !==
|
|
2769
|
+
const current = readFileSync13(anchor, "utf8");
|
|
2770
|
+
if (current !== readFileSync13(templateMd, "utf8")) {
|
|
2600
2771
|
const recorded = readInstallLog(projectDir)?.templates.rootClaudeMd;
|
|
2601
2772
|
const owned = recorded?.path === HARNESS_ANCHOR_FILE && hashContent(current) === recorded.sha256;
|
|
2602
2773
|
if (!owned) {
|
|
@@ -2616,15 +2787,15 @@ function syncHarnessAnchor(projectDir, templatesDir, report, now = /* @__PURE__
|
|
|
2616
2787
|
recordAnchorBaseline(projectDir, anchor);
|
|
2617
2788
|
}
|
|
2618
2789
|
function upsertRootImport(projectDir) {
|
|
2619
|
-
const target =
|
|
2620
|
-
const existing =
|
|
2790
|
+
const target = join15(projectDir, "CLAUDE.md");
|
|
2791
|
+
const existing = existsSync14(target) ? readFileSync13(target, "utf8") : null;
|
|
2621
2792
|
const next = upsertHarnessImport(existing, {
|
|
2622
2793
|
projectName: basename4(projectDir),
|
|
2623
2794
|
tracks: installedTracks(projectDir),
|
|
2624
2795
|
// ADR-085 — update 는 선택 목록을 인자로 받지 않는다. `syncSkills` 와 같은 판정을 쓴다:
|
|
2625
2796
|
// `.claude/skills/<id>` 가 있으면 그 스킬은 이 설치가 고른 것이다.
|
|
2626
2797
|
continuousSkills: CONTINUOUS_SKILLS.map((s) => s.id).filter(
|
|
2627
|
-
(id) =>
|
|
2798
|
+
(id) => existsSync14(join15(projectDir, ".claude/skills", id))
|
|
2628
2799
|
)
|
|
2629
2800
|
});
|
|
2630
2801
|
if (next === existing) return false;
|
|
@@ -2644,7 +2815,7 @@ function recordAnchorBaseline(projectDir, anchor) {
|
|
|
2644
2815
|
...log.templates,
|
|
2645
2816
|
rootClaudeMd: {
|
|
2646
2817
|
path: HARNESS_ANCHOR_FILE,
|
|
2647
|
-
sha256: hashContent(
|
|
2818
|
+
sha256: hashContent(readFileSync13(anchor, "utf8"))
|
|
2648
2819
|
}
|
|
2649
2820
|
}
|
|
2650
2821
|
};
|
|
@@ -2655,20 +2826,41 @@ function recordAnchorBaseline(projectDir, anchor) {
|
|
|
2655
2826
|
}
|
|
2656
2827
|
function installedBundledSkills(projectDir) {
|
|
2657
2828
|
return INTERNAL_BUNDLED_SKILL_IDS.filter(
|
|
2658
|
-
(id) =>
|
|
2829
|
+
(id) => existsSync14(join15(projectDir, ".claude/skills", id)) || existsSync14(join15(projectDir, ".agents/skills", id))
|
|
2659
2830
|
);
|
|
2660
2831
|
}
|
|
2661
2832
|
function installedCliTargets(log) {
|
|
2662
2833
|
if (log === null) return ALL_CLI_TARGETS;
|
|
2663
|
-
return
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2834
|
+
return installedClis(log);
|
|
2835
|
+
}
|
|
2836
|
+
var RESTORED_SKILL_SLOT = /^\.(?:claude|agents)\/skills\/([^/]+)$/;
|
|
2837
|
+
function restoredWithoutArgs(paths, tracks) {
|
|
2838
|
+
const catalog = new Set(EXTERNAL_ASSETS.map((a) => a.id));
|
|
2839
|
+
const baselineIds = new Set(listBaselineTargets({ tracks }).map((t) => t.id));
|
|
2840
|
+
const out = {};
|
|
2841
|
+
for (const path of paths) {
|
|
2842
|
+
const skill = RESTORED_SKILL_SLOT.exec(path)?.[1];
|
|
2843
|
+
if (skill !== void 0 && catalog.has(skill) && INTERNAL_BUNDLED_SKILL_IDS.includes(skill)) {
|
|
2844
|
+
out[path] = skill;
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
const target = classifyBaselineTarget(path);
|
|
2848
|
+
if (target !== null && baselineIds.has(target.id)) out[path] = target.id;
|
|
2849
|
+
}
|
|
2850
|
+
return out;
|
|
2851
|
+
}
|
|
2852
|
+
var SHARED_SKILLS_DIR = ".agents/skills";
|
|
2853
|
+
var SHARED_SKILL_FILE = /^\.agents\/skills\/([^/]+)\//;
|
|
2854
|
+
var SHARED_SKILL_MD = /^\.agents\/skills\/([^/]+)\/SKILL\.md$/;
|
|
2855
|
+
function sharedSkillIdsOnDisk(projectDir) {
|
|
2856
|
+
const root = join15(projectDir, SHARED_SKILLS_DIR);
|
|
2857
|
+
if (!existsSync14(root)) return /* @__PURE__ */ new Set();
|
|
2858
|
+
return new Set(readdirSync3(root).filter((id) => existsSync14(join15(root, id, "SKILL.md"))));
|
|
2668
2859
|
}
|
|
2669
2860
|
function refreshExternalCli(projectDir, harnessRoot) {
|
|
2670
2861
|
const log = readInstallLog(projectDir);
|
|
2671
2862
|
const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
|
|
2863
|
+
const presentBefore = sharedSkillIdsOnDisk(projectDir);
|
|
2672
2864
|
const result = runCliTransforms({
|
|
2673
2865
|
harnessRoot,
|
|
2674
2866
|
projectDir,
|
|
@@ -2692,26 +2884,41 @@ function refreshExternalCli(projectDir, harnessRoot) {
|
|
|
2692
2884
|
} catch {
|
|
2693
2885
|
}
|
|
2694
2886
|
}
|
|
2887
|
+
const priorIds = new Set(
|
|
2888
|
+
(log?.externalFiles ?? []).map((f) => SHARED_SKILL_FILE.exec(f.path)?.[1]).filter(Boolean)
|
|
2889
|
+
);
|
|
2890
|
+
const skillsInstalled = [];
|
|
2891
|
+
const skillsRestored = [];
|
|
2892
|
+
for (const f of result.externalFiles) {
|
|
2893
|
+
const id = SHARED_SKILL_MD.exec(f.path)?.[1];
|
|
2894
|
+
if (id === void 0 || presentBefore.has(id)) continue;
|
|
2895
|
+
const slot = `${SHARED_SKILLS_DIR}/${id}`;
|
|
2896
|
+
const into = priorIds.has(id) ? skillsRestored : skillsInstalled;
|
|
2897
|
+
if (!into.includes(slot)) into.push(slot);
|
|
2898
|
+
}
|
|
2695
2899
|
return {
|
|
2696
2900
|
externalUpdated: result.externalUpdated,
|
|
2697
2901
|
externalBackedUp: result.externalBackedUp,
|
|
2698
|
-
externalForeignOwned: result.externalForeignOwned
|
|
2902
|
+
externalForeignOwned: result.externalForeignOwned,
|
|
2903
|
+
written: result.externalFiles.map((f) => f.path),
|
|
2904
|
+
skillsInstalled,
|
|
2905
|
+
skillsRestored
|
|
2699
2906
|
};
|
|
2700
2907
|
}
|
|
2701
2908
|
function isHarnessOwned2(ctx, rel, current) {
|
|
2702
2909
|
return isHarnessOwned(ctx.baseline, `${ctx.prefix}/${rel}`, current);
|
|
2703
2910
|
}
|
|
2704
2911
|
function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
2705
|
-
if (!
|
|
2912
|
+
if (!existsSync14(target) || !existsSync14(source)) return { updated: 0, backedUp: [] };
|
|
2706
2913
|
let updated = 0;
|
|
2707
2914
|
const backedUp = [];
|
|
2708
2915
|
for (const file of readdirSync3(target)) {
|
|
2709
2916
|
if (!file.endsWith(ext)) continue;
|
|
2710
|
-
const targetFile =
|
|
2711
|
-
const sourceFile =
|
|
2712
|
-
if (!
|
|
2713
|
-
const next =
|
|
2714
|
-
const current =
|
|
2917
|
+
const targetFile = join15(target, file);
|
|
2918
|
+
const sourceFile = join15(source, file);
|
|
2919
|
+
if (!existsSync14(sourceFile)) continue;
|
|
2920
|
+
const next = readFileSync13(sourceFile, "utf8");
|
|
2921
|
+
const current = readFileSync13(targetFile, "utf8");
|
|
2715
2922
|
if (current === next) continue;
|
|
2716
2923
|
if (!isHarnessOwned2(ctx, file, current)) {
|
|
2717
2924
|
backupFile(targetFile, now);
|
|
@@ -2723,7 +2930,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
|
2723
2930
|
return { updated, backedUp };
|
|
2724
2931
|
}
|
|
2725
2932
|
function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date(), foreignOf) {
|
|
2726
|
-
if (!
|
|
2933
|
+
if (!existsSync14(targetDir) || !existsSync14(sourceDir))
|
|
2727
2934
|
return { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [], pruned: [] };
|
|
2728
2935
|
let updated = 0;
|
|
2729
2936
|
const backedUp = [];
|
|
@@ -2732,27 +2939,27 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
|
|
|
2732
2939
|
const pruned = [];
|
|
2733
2940
|
for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
|
|
2734
2941
|
if (!skill.isDirectory()) continue;
|
|
2735
|
-
const targetSkill =
|
|
2942
|
+
const targetSkill = join15(targetDir, skill.name);
|
|
2736
2943
|
if (occupiedByNonDirectory(targetSkill)) {
|
|
2737
2944
|
skippedLinks.push(skill.name);
|
|
2738
2945
|
continue;
|
|
2739
2946
|
}
|
|
2740
|
-
if (!
|
|
2741
|
-
for (const rel of listFilesRecursive(
|
|
2742
|
-
const targetFile =
|
|
2947
|
+
if (!existsSync14(targetSkill)) continue;
|
|
2948
|
+
for (const rel of listFilesRecursive(join15(sourceDir, skill.name))) {
|
|
2949
|
+
const targetFile = join15(targetSkill, rel);
|
|
2743
2950
|
const foreign = foreignOf(`${skill.name}/${rel}`);
|
|
2744
2951
|
if (foreign !== null) {
|
|
2745
2952
|
if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
|
|
2746
2953
|
continue;
|
|
2747
2954
|
}
|
|
2748
|
-
const next =
|
|
2749
|
-
if (!
|
|
2955
|
+
const next = readFileSync13(join15(sourceDir, skill.name, rel), "utf8");
|
|
2956
|
+
if (!existsSync14(targetFile)) {
|
|
2750
2957
|
mkdirSync3(dirname3(targetFile), { recursive: true });
|
|
2751
2958
|
writeFileSync5(targetFile, next);
|
|
2752
2959
|
updated++;
|
|
2753
2960
|
continue;
|
|
2754
2961
|
}
|
|
2755
|
-
const current =
|
|
2962
|
+
const current = readFileSync13(targetFile, "utf8");
|
|
2756
2963
|
if (current === next) continue;
|
|
2757
2964
|
const recorded = baseline.get(`${skill.name}/${rel}`);
|
|
2758
2965
|
if (recorded === void 0 || hashContent(current) !== recorded) {
|
|
@@ -2762,13 +2969,13 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
|
|
|
2762
2969
|
writeFileSync5(targetFile, next);
|
|
2763
2970
|
updated++;
|
|
2764
2971
|
}
|
|
2765
|
-
const bundled = new Set(listFilesRecursive(
|
|
2972
|
+
const bundled = new Set(listFilesRecursive(join15(sourceDir, skill.name)));
|
|
2766
2973
|
for (const rel of listFilesRecursive(targetSkill)) {
|
|
2767
2974
|
if (bundled.has(rel) || basename4(rel).includes(".backup-")) continue;
|
|
2768
2975
|
if (foreignOf(`${skill.name}/${rel}`) !== null) continue;
|
|
2769
|
-
const targetFile =
|
|
2976
|
+
const targetFile = join15(targetSkill, rel);
|
|
2770
2977
|
const recorded = baseline.get(`${skill.name}/${rel}`);
|
|
2771
|
-
if (recorded === void 0 || hashContent(
|
|
2978
|
+
if (recorded === void 0 || hashContent(readFileSync13(targetFile, "utf8")) !== recorded) {
|
|
2772
2979
|
backupFile(targetFile, now);
|
|
2773
2980
|
backedUp.push(`${skill.name}/${rel}`);
|
|
2774
2981
|
}
|
|
@@ -2817,16 +3024,16 @@ function refreshSkillBaseline(projectDir, templatesDir) {
|
|
|
2817
3024
|
}
|
|
2818
3025
|
}
|
|
2819
3026
|
function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
2820
|
-
if (!
|
|
3027
|
+
if (!existsSync14(target) || !existsSync14(source)) return [];
|
|
2821
3028
|
const removed = [];
|
|
2822
3029
|
for (const file of readdirSync3(target)) {
|
|
2823
3030
|
if (!file.endsWith(ext)) continue;
|
|
2824
|
-
const sourceFile =
|
|
2825
|
-
if (!
|
|
2826
|
-
const targetFile =
|
|
3031
|
+
const sourceFile = join15(source, file);
|
|
3032
|
+
if (!existsSync14(sourceFile)) {
|
|
3033
|
+
const targetFile = join15(target, file);
|
|
2827
3034
|
if (!ctx.baseline.has(`${ctx.prefix}/${file}`)) continue;
|
|
2828
3035
|
try {
|
|
2829
|
-
const current =
|
|
3036
|
+
const current = readFileSync13(targetFile, "utf8");
|
|
2830
3037
|
if (!isHarnessOwned2(ctx, file, current)) backupFile(targetFile, now);
|
|
2831
3038
|
unlinkSync2(targetFile);
|
|
2832
3039
|
removed.push(file);
|
|
@@ -2837,8 +3044,8 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
|
|
|
2837
3044
|
return removed;
|
|
2838
3045
|
}
|
|
2839
3046
|
function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
|
|
2840
|
-
const target =
|
|
2841
|
-
if (!
|
|
3047
|
+
const target = join15(projectDir, ".mcp-allowlist");
|
|
3048
|
+
if (!existsSync14(target)) return null;
|
|
2842
3049
|
try {
|
|
2843
3050
|
const backup = backupFile(target, now);
|
|
2844
3051
|
unlinkSync2(target);
|
|
@@ -2850,7 +3057,7 @@ function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
|
|
|
2850
3057
|
function cleanStaleHookRefs(settingsPath, claudeDir) {
|
|
2851
3058
|
let settings;
|
|
2852
3059
|
try {
|
|
2853
|
-
settings = JSON.parse(
|
|
3060
|
+
settings = JSON.parse(readFileSync13(settingsPath, "utf8"));
|
|
2854
3061
|
} catch {
|
|
2855
3062
|
return [];
|
|
2856
3063
|
}
|
|
@@ -2877,7 +3084,7 @@ function cleanStaleHookRefs(settingsPath, claudeDir) {
|
|
|
2877
3084
|
function keepHookRef(hook, claudeDir, removed) {
|
|
2878
3085
|
const relPath = projectAnchoredRef(hook?.command ?? "", claudeDir);
|
|
2879
3086
|
if (relPath === void 0) return true;
|
|
2880
|
-
const exists =
|
|
3087
|
+
const exists = existsSync14(join15(claudeDir, relPath));
|
|
2881
3088
|
if (!exists && !removed.includes(relPath)) removed.push(relPath);
|
|
2882
3089
|
return exists;
|
|
2883
3090
|
}
|
|
@@ -2899,31 +3106,47 @@ function projectAnchoredRef(command, claudeDir) {
|
|
|
2899
3106
|
}
|
|
2900
3107
|
return void 0;
|
|
2901
3108
|
}
|
|
3109
|
+
function recordClaudeCommand(log, tracks) {
|
|
3110
|
+
const trackArgs = (tracks.length > 0 ? tracks : log.spec.tracks).map((t) => `--track ${t}`);
|
|
3111
|
+
return `agent-harness install ${trackArgs.join(" ")} --cli claude --scope ${log.scope}`.replace(
|
|
3112
|
+
/\s+/g,
|
|
3113
|
+
" "
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
2902
3116
|
|
|
2903
3117
|
// src/installer.ts
|
|
2904
3118
|
function runInstall(ctx) {
|
|
2905
3119
|
const { harnessRoot, projectDir, spec } = ctx;
|
|
2906
3120
|
const mode = ctx.mode ?? "fresh";
|
|
2907
|
-
const templatesDir =
|
|
2908
|
-
if (!
|
|
3121
|
+
const templatesDir = join16(harnessRoot, "templates");
|
|
3122
|
+
if (!existsSync15(templatesDir)) {
|
|
2909
3123
|
throw new Error(`Templates dir not found: ${templatesDir}`);
|
|
2910
3124
|
}
|
|
2911
|
-
const claudeDir =
|
|
3125
|
+
const claudeDir = join16(projectDir, ".claude");
|
|
2912
3126
|
const previousLog = readInstallLog(projectDir);
|
|
2913
|
-
const claudeWasSelected = previousLog
|
|
2914
|
-
if (mode === "update" && !
|
|
3127
|
+
const claudeWasSelected = previousLog !== null && installedClis(previousLog).includes("claude");
|
|
3128
|
+
if (mode === "update" && !existsSync15(claudeDir) && (previousLog === null || claudeWasSelected)) {
|
|
2915
3129
|
throw new Error(
|
|
2916
3130
|
claudeWasSelected ? `Update mode found a broken install at ${projectDir} \u2014 this project installed Claude Code assets but \`.claude/\` is gone. Reinstall instead: agent-harness install --track <name>` : `Update mode requires an existing install at ${projectDir}`
|
|
2917
3131
|
);
|
|
2918
3132
|
}
|
|
2919
|
-
const backupPath = resolveBackupPath(ctx, mode, claudeDir);
|
|
3133
|
+
const backupPath = resolveBackupPath(ctx, mode, claudeDir, previousLog);
|
|
2920
3134
|
if (mode === "update") {
|
|
2921
3135
|
return runUpdateInstall(ctx, templatesDir, backupPath);
|
|
2922
3136
|
}
|
|
2923
3137
|
const manifestSpec = buildManifestSpec(spec);
|
|
2924
3138
|
const policyBase = new Map((previousLog?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
|
|
3139
|
+
const skillBase = new Map((previousLog?.skillFiles ?? []).map((f) => [f.path, f.sha256]));
|
|
2925
3140
|
const baselineExcluded = new Set(spec.baselineExclude ?? []);
|
|
2926
|
-
const base = spec.cli.includes("claude") ? installClaudeBaseline(
|
|
3141
|
+
const base = spec.cli.includes("claude") ? installClaudeBaseline(
|
|
3142
|
+
manifestSpec,
|
|
3143
|
+
projectDir,
|
|
3144
|
+
templatesDir,
|
|
3145
|
+
policyBase,
|
|
3146
|
+
skillBase,
|
|
3147
|
+
baselineExcluded,
|
|
3148
|
+
harnessRoot
|
|
3149
|
+
) : (
|
|
2927
3150
|
// claude 미선택이어도 CLI 중립 자산은 깔린다. manifest 전체가 `.claude/` baseline 안에서만
|
|
2928
3151
|
// 돌던 탓에 이 자산들이 claude 설치에만 도달했는데, **배포 룰 본문이 이 스크립트들을
|
|
2929
3152
|
// 호출 지점으로 지목한다** — 즉 없는 도구를 있다고 안내하고 있었다(#300 과 같은 형태).
|
|
@@ -2955,6 +3178,17 @@ function runInstall(ctx) {
|
|
|
2955
3178
|
previousExternal: previousLog?.externalFiles ?? [],
|
|
2956
3179
|
codexTrust: (spec.scope ?? "project") === "global" && spec.options.withCodexTrust
|
|
2957
3180
|
});
|
|
3181
|
+
const linked = refreshLinkedSkillBodies({
|
|
3182
|
+
harnessRoot,
|
|
3183
|
+
projectDir,
|
|
3184
|
+
ids: base.linkedSkills,
|
|
3185
|
+
baseline: new Map(
|
|
3186
|
+
mergeExternalFiles(projectDir, previousLog?.externalFiles, externalFiles).map((f) => [
|
|
3187
|
+
f.path,
|
|
3188
|
+
f.sha256
|
|
3189
|
+
])
|
|
3190
|
+
)
|
|
3191
|
+
});
|
|
2958
3192
|
const baseline = {
|
|
2959
3193
|
filesCopied: base.filesCopied,
|
|
2960
3194
|
dirsCopied: base.dirsCopied,
|
|
@@ -2971,21 +3205,23 @@ function runInstall(ctx) {
|
|
|
2971
3205
|
rootClaudeMd: base.rootClaudeMd,
|
|
2972
3206
|
// 외부 CLI 백업도 같은 줄에 노출한다 — 백업이 화면에 안 보이면 사용자는 자기 편집분이
|
|
2973
3207
|
// 어디 갔는지 알 수 없고, 그러면 백업은 있어도 없는 것과 같다 (ADR-046/047 과 같은 이유).
|
|
2974
|
-
backups: [...base.backups, ...externalBackups],
|
|
3208
|
+
backups: [...base.backups, ...externalBackups, ...linked.backupPaths],
|
|
2975
3209
|
baselineExcluded: base.excluded,
|
|
2976
3210
|
baselineExcludedOnDisk: base.excludedOnDisk,
|
|
2977
3211
|
// `.claude/` baseline 과 외부 CLI 산출물의 같은 판정을 **한 목록으로** 낸다.
|
|
2978
3212
|
baselineForeignOwned: [
|
|
2979
|
-
...base.foreignOwned,
|
|
2980
|
-
|
|
2981
|
-
|
|
3213
|
+
.../* @__PURE__ */ new Set([...base.foreignOwned, ...externalForeignOwned, ...linked.foreignOwned])
|
|
3214
|
+
],
|
|
3215
|
+
baselineLinked: linked.updated,
|
|
3216
|
+
baselineLinkedNotOurs: linked.notOurs
|
|
2982
3217
|
};
|
|
2983
3218
|
ctx.onProgress?.({ type: "baseline-complete", baseline });
|
|
2984
3219
|
const external = runExternalPhase(ctx);
|
|
2985
3220
|
const staleHookRefs = healStaleHookRefs(spec, projectDir);
|
|
2986
3221
|
writeInstallLogSafe(
|
|
2987
3222
|
ctx,
|
|
2988
|
-
|
|
3223
|
+
// 링크 본문의 기준선도 같은 필드다 — 같은 경로면 뒤(이번에 쓴 값)가 이긴다.
|
|
3224
|
+
[...externalFiles, ...linked.files],
|
|
2989
3225
|
external,
|
|
2990
3226
|
base.rootClaudeMdLog,
|
|
2991
3227
|
previousLog,
|
|
@@ -2996,12 +3232,13 @@ function runInstall(ctx) {
|
|
|
2996
3232
|
}
|
|
2997
3233
|
function healStaleHookRefs(spec, projectDir) {
|
|
2998
3234
|
if (!spec.cli.includes("claude")) return [];
|
|
2999
|
-
const settingsPath =
|
|
3000
|
-
if (!
|
|
3001
|
-
return cleanStaleHookRefs(settingsPath,
|
|
3235
|
+
const settingsPath = join16(projectDir, ".claude/settings.json");
|
|
3236
|
+
if (!existsSync15(settingsPath)) return [];
|
|
3237
|
+
return cleanStaleHookRefs(settingsPath, join16(projectDir, ".claude"));
|
|
3002
3238
|
}
|
|
3003
|
-
function resolveBackupPath(ctx, mode, claudeDir) {
|
|
3004
|
-
const
|
|
3239
|
+
function resolveBackupPath(ctx, mode, claudeDir, previousLog) {
|
|
3240
|
+
const claudeUntouched = previousLog !== null && !installedClis(previousLog).includes("claude");
|
|
3241
|
+
const wantBackup = ctx.backup ?? (mode === "reinstall" || mode === "update" && !claudeUntouched);
|
|
3005
3242
|
if (!wantBackup) return null;
|
|
3006
3243
|
return mode === "update" ? copyBackupDir(claudeDir) : backupDir(claudeDir);
|
|
3007
3244
|
}
|
|
@@ -3058,7 +3295,8 @@ function emptyClaudeBaseline() {
|
|
|
3058
3295
|
backups: [],
|
|
3059
3296
|
excluded: [],
|
|
3060
3297
|
excludedOnDisk: [],
|
|
3061
|
-
foreignOwned: []
|
|
3298
|
+
foreignOwned: [],
|
|
3299
|
+
linkedSkills: []
|
|
3062
3300
|
};
|
|
3063
3301
|
}
|
|
3064
3302
|
function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselineExcluded) {
|
|
@@ -3070,12 +3308,12 @@ function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselin
|
|
|
3070
3308
|
continue;
|
|
3071
3309
|
}
|
|
3072
3310
|
if (!isCliNeutralTarget(entry.target)) continue;
|
|
3073
|
-
const source =
|
|
3074
|
-
if (!
|
|
3311
|
+
const source = join16(templatesDir, entry.source);
|
|
3312
|
+
if (!existsSync15(source)) {
|
|
3075
3313
|
result.skipped += 1;
|
|
3076
3314
|
continue;
|
|
3077
3315
|
}
|
|
3078
|
-
copyFile(source,
|
|
3316
|
+
copyFile(source, join16(projectDir, entry.target));
|
|
3079
3317
|
result.filesCopied += 1;
|
|
3080
3318
|
}
|
|
3081
3319
|
return result;
|
|
@@ -3087,14 +3325,28 @@ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
|
|
|
3087
3325
|
if (!POLICY_DIRS.some(({ dir, ext }) => rel.startsWith(`${dir}/`) && rel.endsWith(ext))) {
|
|
3088
3326
|
return null;
|
|
3089
3327
|
}
|
|
3090
|
-
if (!
|
|
3091
|
-
const current =
|
|
3092
|
-
if (current ===
|
|
3328
|
+
if (!existsSync15(target)) return null;
|
|
3329
|
+
const current = readFileSync14(target, "utf-8");
|
|
3330
|
+
if (current === readFileSync14(source, "utf-8")) return null;
|
|
3093
3331
|
const recorded = baseline.get(rel);
|
|
3094
3332
|
if (recorded !== void 0 && recorded === hashContent(current)) return null;
|
|
3095
3333
|
return backupFile(target);
|
|
3096
3334
|
}
|
|
3097
|
-
function
|
|
3335
|
+
function backupEditedSkillFiles(projectDir, entryTarget, source, baseline) {
|
|
3336
|
+
const id = entryTarget.slice(".claude/skills/".length);
|
|
3337
|
+
const backups = [];
|
|
3338
|
+
for (const rel of listFilesRecursive(source)) {
|
|
3339
|
+
if (foreignOwnedTarget(projectDir, `${entryTarget}/${rel}`) !== null) continue;
|
|
3340
|
+
const target = join16(projectDir, entryTarget, rel);
|
|
3341
|
+
if (!existsSync15(target)) continue;
|
|
3342
|
+
const current = readFileSync14(target, "utf-8");
|
|
3343
|
+
if (current === readFileSync14(join16(source, rel), "utf-8")) continue;
|
|
3344
|
+
if (isHarnessOwned(baseline, `${id}/${rel}`, current)) continue;
|
|
3345
|
+
backups.push(backupFile(target));
|
|
3346
|
+
}
|
|
3347
|
+
return backups;
|
|
3348
|
+
}
|
|
3349
|
+
function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase, skillBase, baselineExcluded, harnessRoot) {
|
|
3098
3350
|
ensureProjectSkeleton(projectDir);
|
|
3099
3351
|
const result = emptyClaudeBaseline();
|
|
3100
3352
|
const manifest = buildManifest(manifestSpec);
|
|
@@ -3102,17 +3354,22 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
|
|
|
3102
3354
|
if (!entry.applies(manifestSpec)) {
|
|
3103
3355
|
continue;
|
|
3104
3356
|
}
|
|
3105
|
-
const target =
|
|
3357
|
+
const target = join16(projectDir, entry.target);
|
|
3106
3358
|
if (isBaselineExcluded(entry.target, baselineExcluded)) {
|
|
3107
3359
|
result.excluded.push(entry.target);
|
|
3108
|
-
if (
|
|
3360
|
+
if (existsSync15(target)) result.excludedOnDisk.push(entry.target);
|
|
3109
3361
|
continue;
|
|
3110
3362
|
}
|
|
3111
|
-
const source =
|
|
3112
|
-
if (!
|
|
3363
|
+
const source = join16(templatesDir, entry.source);
|
|
3364
|
+
if (!existsSync15(source)) {
|
|
3113
3365
|
result.skipped += 1;
|
|
3114
3366
|
continue;
|
|
3115
3367
|
}
|
|
3368
|
+
const skillId = entry.target.slice(".claude/skills/".length);
|
|
3369
|
+
if (entry.type === "dir" && linksToProjectSharedSkill(projectDir, skillId)) {
|
|
3370
|
+
result.linkedSkills.push(skillId);
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
3116
3373
|
const foreign = foreignOwnedTarget(projectDir, entry.target);
|
|
3117
3374
|
if (foreign !== null) {
|
|
3118
3375
|
if (!result.foreignOwned.includes(foreign)) result.foreignOwned.push(foreign);
|
|
@@ -3120,7 +3377,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
|
|
|
3120
3377
|
}
|
|
3121
3378
|
if (entry.type === "file") {
|
|
3122
3379
|
if (entry.target === ".claude/settings.json") {
|
|
3123
|
-
const backup = backupFileIfChanged(target,
|
|
3380
|
+
const backup = backupFileIfChanged(target, readFileSync14(source, "utf-8"));
|
|
3124
3381
|
if (backup) {
|
|
3125
3382
|
result.backups.push(backup);
|
|
3126
3383
|
}
|
|
@@ -3133,6 +3390,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
|
|
|
3133
3390
|
copyFile(source, target);
|
|
3134
3391
|
result.filesCopied += 1;
|
|
3135
3392
|
} else {
|
|
3393
|
+
result.backups.push(...backupEditedSkillFiles(projectDir, entry.target, source, skillBase));
|
|
3136
3394
|
for (const foreign2 of copyDir(
|
|
3137
3395
|
source,
|
|
3138
3396
|
target,
|
|
@@ -3144,24 +3402,29 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
|
|
|
3144
3402
|
}
|
|
3145
3403
|
accumulateCategory(result.categories, entry);
|
|
3146
3404
|
}
|
|
3147
|
-
const hookDir =
|
|
3148
|
-
if (
|
|
3405
|
+
const hookDir = join16(projectDir, ".claude/hooks");
|
|
3406
|
+
if (existsSync15(hookDir)) {
|
|
3149
3407
|
chmodHooksSync(hookDir);
|
|
3150
3408
|
}
|
|
3151
3409
|
writeInstalledTracks(projectDir, manifestSpec.tracks);
|
|
3152
3410
|
const rootClaudeMd = writeRootClaudeMd(
|
|
3153
3411
|
projectDir,
|
|
3154
3412
|
manifestSpec.tracks,
|
|
3155
|
-
manifestSpec.selectedInternalSkills ?? []
|
|
3413
|
+
manifestSpec.selectedInternalSkills ?? [],
|
|
3414
|
+
harnessRoot
|
|
3156
3415
|
);
|
|
3157
|
-
result.rootClaudeMd = {
|
|
3416
|
+
result.rootClaudeMd = {
|
|
3417
|
+
tracks: manifestSpec.tracks,
|
|
3418
|
+
created: rootClaudeMd.created,
|
|
3419
|
+
seededFrom: rootClaudeMd.seededFrom
|
|
3420
|
+
};
|
|
3158
3421
|
result.rootClaudeMdLog = harnessAnchorLog(projectDir);
|
|
3159
3422
|
return result;
|
|
3160
3423
|
}
|
|
3161
3424
|
function harnessAnchorLog(projectDir) {
|
|
3162
|
-
const anchor =
|
|
3163
|
-
if (!
|
|
3164
|
-
return { path: HARNESS_ANCHOR_FILE, sha256: hashContent(
|
|
3425
|
+
const anchor = join16(projectDir, HARNESS_ANCHOR_FILE);
|
|
3426
|
+
if (!existsSync15(anchor)) return null;
|
|
3427
|
+
return { path: HARNESS_ANCHOR_FILE, sha256: hashContent(readFileSync14(anchor, "utf-8")) };
|
|
3165
3428
|
}
|
|
3166
3429
|
function writeEnvironmentFiles(projectDir, tracks) {
|
|
3167
3430
|
return {
|
|
@@ -3218,8 +3481,9 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
|
|
|
3218
3481
|
claudeDirMovedAside,
|
|
3219
3482
|
rootFiles
|
|
3220
3483
|
);
|
|
3221
|
-
const
|
|
3222
|
-
const
|
|
3484
|
+
const claudeInstalled = (log.spec.clis ?? []).includes("claude");
|
|
3485
|
+
const skillFiles = claudeInstalled ? collectSkillHashes(ctx.projectDir, join16(ctx.harnessRoot, "templates")) : [];
|
|
3486
|
+
const policyFiles = claudeInstalled ? collectPolicyHashes(ctx.projectDir, join16(ctx.harnessRoot, "templates")) : [];
|
|
3223
3487
|
const merged = mergeExternalFiles(ctx.projectDir, previousLog?.externalFiles, externalFiles);
|
|
3224
3488
|
writeInstallLog(ctx.projectDir, {
|
|
3225
3489
|
...log,
|
|
@@ -3235,11 +3499,11 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
|
|
|
3235
3499
|
}
|
|
3236
3500
|
}
|
|
3237
3501
|
function composeAndWriteMcp(harnessRoot, projectDir, spec) {
|
|
3238
|
-
const mcpPath =
|
|
3239
|
-
const created = !
|
|
3502
|
+
const mcpPath = join16(projectDir, ".mcp.json");
|
|
3503
|
+
const created = !existsSync15(mcpPath);
|
|
3240
3504
|
const composed = composeMcpJson({
|
|
3241
|
-
templateMcpPath:
|
|
3242
|
-
trackMapPath:
|
|
3505
|
+
templateMcpPath: join16(harnessRoot, "templates/mcp.json"),
|
|
3506
|
+
trackMapPath: join16(harnessRoot, "templates/track-mcp-map.tsv"),
|
|
3243
3507
|
existingPath: mcpPath,
|
|
3244
3508
|
tracks: spec.tracks
|
|
3245
3509
|
});
|
|
@@ -3292,24 +3556,26 @@ function accumulateCategory(cats, entry) {
|
|
|
3292
3556
|
}
|
|
3293
3557
|
}
|
|
3294
3558
|
function writeInstalledTracks(projectDir, tracks) {
|
|
3295
|
-
const path =
|
|
3559
|
+
const path = join16(projectDir, ".claude/.installed-tracks");
|
|
3296
3560
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
3297
3561
|
const sorted = [...new Set(tracks)].sort().join("\n");
|
|
3298
3562
|
writeFileSync6(path, `${sorted}
|
|
3299
3563
|
`);
|
|
3300
3564
|
}
|
|
3301
|
-
function writeRootClaudeMd(projectDir, tracks, continuousSkills) {
|
|
3302
|
-
const target =
|
|
3303
|
-
const existing =
|
|
3565
|
+
function writeRootClaudeMd(projectDir, tracks, continuousSkills, harnessRoot) {
|
|
3566
|
+
const target = join16(projectDir, "CLAUDE.md");
|
|
3567
|
+
const existing = existsSync15(target) ? readFileSync14(target, "utf-8") : null;
|
|
3568
|
+
const seeded = existing === null ? seedRootClaudeProjectContext(projectDir, harnessRoot) : null;
|
|
3304
3569
|
const content = upsertHarnessImport(existing, {
|
|
3305
3570
|
projectName: basename5(projectDir),
|
|
3306
3571
|
tracks,
|
|
3307
|
-
continuousSkills
|
|
3572
|
+
continuousSkills,
|
|
3573
|
+
...seeded === null ? {} : { projectContext: seeded }
|
|
3308
3574
|
});
|
|
3309
3575
|
if (content !== existing) {
|
|
3310
3576
|
writeFileSync6(target, content);
|
|
3311
3577
|
}
|
|
3312
|
-
return { created: existing === null };
|
|
3578
|
+
return { created: existing === null, seededFrom: seeded === null ? null : "AGENTS.md" };
|
|
3313
3579
|
}
|
|
3314
3580
|
function chmodHooksSync(hookDir) {
|
|
3315
3581
|
for (const file of listHookFiles(hookDir)) {
|
|
@@ -3325,8 +3591,8 @@ function listHookFiles(hookDir) {
|
|
|
3325
3591
|
|
|
3326
3592
|
// src/commands/install-render.ts
|
|
3327
3593
|
init_esm_shims();
|
|
3328
|
-
import { existsSync as
|
|
3329
|
-
import { join as
|
|
3594
|
+
import { existsSync as existsSync16, readdirSync as readdirSync5 } from "fs";
|
|
3595
|
+
import { join as join17 } from "path";
|
|
3330
3596
|
|
|
3331
3597
|
// src/preset-recommend.ts
|
|
3332
3598
|
init_esm_shims();
|
|
@@ -3494,13 +3760,13 @@ function renderUpdateSummary(log, spec, report) {
|
|
|
3494
3760
|
function residentEntriesOnDisk(spec) {
|
|
3495
3761
|
const assetSpec = buildManifestSpec(spec);
|
|
3496
3762
|
const planned = buildManifest(assetSpec).filter((e2) => e2.applies(assetSpec));
|
|
3497
|
-
const agentsDir =
|
|
3498
|
-
const onDisk =
|
|
3763
|
+
const agentsDir = join17(spec.projectDir, ".claude", "agents");
|
|
3764
|
+
const onDisk = existsSync16(agentsDir) ? readdirSync5(agentsDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => ({
|
|
3499
3765
|
source: `agents/${e2.name}`,
|
|
3500
3766
|
target: `.claude/agents/${e2.name}`,
|
|
3501
3767
|
// 배포판이 아니라 **이 프로젝트의 파일**을 잰다 — 사용자가 고친 descriptor 도,
|
|
3502
3768
|
// 배포판에 더는 없는 에이전트도 실제로 상주하는 것은 이쪽이다.
|
|
3503
|
-
file:
|
|
3769
|
+
file: join17(agentsDir, e2.name)
|
|
3504
3770
|
})) : [];
|
|
3505
3771
|
return [...planned.filter((e2) => !e2.target.startsWith(".claude/agents/")), ...onDisk];
|
|
3506
3772
|
}
|
|
@@ -3517,6 +3783,16 @@ function renderCliArtifacts(log, spec, report) {
|
|
|
3517
3783
|
} else if (report.codex || report.opencode) {
|
|
3518
3784
|
log(assetRow("success", "AGENTS.md", `from ${HARNESS_ANCHOR_FILE}`));
|
|
3519
3785
|
}
|
|
3786
|
+
const agentsSeededFrom = report.codex?.agentsMdSeededFrom ?? report.opencode?.agentsMdSeededFrom ?? null;
|
|
3787
|
+
if (agentsSeededFrom) {
|
|
3788
|
+
log(
|
|
3789
|
+
assetRow(
|
|
3790
|
+
"success",
|
|
3791
|
+
"AGENTS.md",
|
|
3792
|
+
`Project Context seeded from ${agentsSeededFrom} \xB7 CLI \uACE0\uC720 \uD45C\uD604\uC740 audit-harness-fit \uC73C\uB85C \uB9DE\uCD98\uB2E4`
|
|
3793
|
+
)
|
|
3794
|
+
);
|
|
3795
|
+
}
|
|
3520
3796
|
if (report.codex) {
|
|
3521
3797
|
log(assetRow("success", ".codex/config.toml", "settings + [mcp_servers.*]"));
|
|
3522
3798
|
log(assetRow("success", ".codex/hooks/", `${report.codex.hookFiles.length} files`));
|
|
@@ -3678,19 +3954,55 @@ function formatAssetMeta(asset, version) {
|
|
|
3678
3954
|
return `internal \xB7 templates (${m.key})`;
|
|
3679
3955
|
}
|
|
3680
3956
|
}
|
|
3681
|
-
function
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3957
|
+
function linkedSkillRows(log, updated, notOurs) {
|
|
3958
|
+
for (const id of updated) {
|
|
3959
|
+
log(assetRow("success", `.claude/skills/${id}`, `linked \xB7 updated via .agents/skills/${id}`));
|
|
3960
|
+
}
|
|
3961
|
+
for (const id of notOurs) {
|
|
3962
|
+
log(
|
|
3963
|
+
assetRow(
|
|
3964
|
+
"skip",
|
|
3965
|
+
`.claude/skills/${id}`,
|
|
3966
|
+
`linked \xB7 not ours \u2014 .agents/skills/${id} \uB294 \uC774 \uD558\uB124\uC2A4\uAC00 \uC4F4 \uAE30\uB85D\uC774 \uC5C6\uC5B4 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC558\uB2E4`
|
|
3967
|
+
)
|
|
3968
|
+
);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
function renderPhase1Rows(log, baseline, verbose = false) {
|
|
3972
|
+
if (baseline.updateMode) {
|
|
3973
|
+
if (baseline.backup) {
|
|
3974
|
+
log(assetRow("success", "backup", shortenPath(baseline.backup)));
|
|
3975
|
+
}
|
|
3976
|
+
for (const [dir, count] of Object.entries(baseline.updateMode.updated)) {
|
|
3687
3977
|
if (count > 0) log(assetRow("success", dir, `${count} files updated`));
|
|
3688
3978
|
}
|
|
3689
3979
|
for (const path of baseline.updateMode.installedNew) {
|
|
3690
3980
|
log(assetRow("success", path, "added by this release"));
|
|
3691
3981
|
}
|
|
3982
|
+
const withoutArgs = [];
|
|
3692
3983
|
for (const path of baseline.updateMode.restored) {
|
|
3693
|
-
|
|
3984
|
+
const arg = baseline.updateMode.restoredWithout?.[path];
|
|
3985
|
+
if (arg === void 0) {
|
|
3986
|
+
log(
|
|
3987
|
+
assetRow(
|
|
3988
|
+
"success",
|
|
3989
|
+
path,
|
|
3990
|
+
"was missing \u2014 reinstalled (harness-managed \u2014 update always restores it)"
|
|
3991
|
+
)
|
|
3992
|
+
);
|
|
3993
|
+
continue;
|
|
3994
|
+
}
|
|
3995
|
+
log(assetRow("success", path, "was missing \u2014 reinstalled"));
|
|
3996
|
+
if (!withoutArgs.includes(arg)) withoutArgs.push(arg);
|
|
3997
|
+
}
|
|
3998
|
+
if (withoutArgs.length > 0) {
|
|
3999
|
+
log(
|
|
4000
|
+
assetRow(
|
|
4001
|
+
"skip",
|
|
4002
|
+
"to keep it out",
|
|
4003
|
+
`deleting is undone by the next update \u2014 re-run \`agent-harness install\` with your usual flags plus ${withoutArgs.map((a) => `--without ${a}`).join(" ")}, then delete it again`
|
|
4004
|
+
)
|
|
4005
|
+
);
|
|
3694
4006
|
}
|
|
3695
4007
|
if (baseline.updateMode.needsReinstall.length > 0) {
|
|
3696
4008
|
log(
|
|
@@ -3742,6 +4054,15 @@ function renderPhase1Rows(log, baseline, verbose = false) {
|
|
|
3742
4054
|
)
|
|
3743
4055
|
);
|
|
3744
4056
|
}
|
|
4057
|
+
if (baseline.updateMode.claudeUnrecorded) {
|
|
4058
|
+
log(
|
|
4059
|
+
assetRow(
|
|
4060
|
+
"skip",
|
|
4061
|
+
".claude/",
|
|
4062
|
+
`present but not recorded as installed \u2014 left untouched. If its rules/ \xB7 agents/ \xB7 hooks/ \xB7 skills/ came from this tool (you did not write them), run \`${baseline.updateMode.claudeUnrecorded}\` once to re-record them and updates resume; otherwise this directory is yours`
|
|
4063
|
+
)
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
3745
4066
|
if (baseline.updateMode.legacyAnchor) {
|
|
3746
4067
|
log(
|
|
3747
4068
|
assetRow(
|
|
@@ -3769,6 +4090,11 @@ function renderPhase1Rows(log, baseline, verbose = false) {
|
|
|
3769
4090
|
)
|
|
3770
4091
|
);
|
|
3771
4092
|
}
|
|
4093
|
+
linkedSkillRows(
|
|
4094
|
+
log,
|
|
4095
|
+
baseline.updateMode.skillsLinked ?? [],
|
|
4096
|
+
baseline.updateMode.skillsLinkedNotOurs ?? []
|
|
4097
|
+
);
|
|
3772
4098
|
if (baseline.updateMode.skillsSkippedLinks.length > 0) {
|
|
3773
4099
|
log(
|
|
3774
4100
|
assetRow(
|
|
@@ -3975,6 +4301,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
|
|
|
3975
4301
|
)
|
|
3976
4302
|
);
|
|
3977
4303
|
}
|
|
4304
|
+
linkedSkillRows(log, baseline.baselineLinked ?? [], baseline.baselineLinkedNotOurs ?? []);
|
|
3978
4305
|
if (baseline.baselineForeignOwned.length > 0) {
|
|
3979
4306
|
log(
|
|
3980
4307
|
assetRow(
|
|
@@ -3991,7 +4318,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
|
|
|
3991
4318
|
assetRow(
|
|
3992
4319
|
"success",
|
|
3993
4320
|
"CLAUDE.md (root)",
|
|
3994
|
-
baseline.rootClaudeMd
|
|
4321
|
+
rootClaudeMdMeta(baseline.rootClaudeMd, n),
|
|
3995
4322
|
TEMPLATES_COL
|
|
3996
4323
|
)
|
|
3997
4324
|
);
|
|
@@ -4058,6 +4385,14 @@ function shortenPath(p2) {
|
|
|
4058
4385
|
}
|
|
4059
4386
|
return p2;
|
|
4060
4387
|
}
|
|
4388
|
+
function rootClaudeMdMeta(root, trackCount) {
|
|
4389
|
+
const tracks = `${trackCount} track${trackCount > 1 ? "s" : ""} noted`;
|
|
4390
|
+
if (!root.created) return `@import ${HARNESS_ANCHOR_FILE} (body preserved)`;
|
|
4391
|
+
if (root.seededFrom) {
|
|
4392
|
+
return `Project Context seeded from ${root.seededFrom} + @import \xB7 ${tracks} \xB7 CLI \uACE0\uC720 \uD45C\uD604\uC740 audit-harness-fit \uC73C\uB85C \uB9DE\uCD98\uB2E4`;
|
|
4393
|
+
}
|
|
4394
|
+
return `fill-in scaffold + @import \xB7 ${tracks}`;
|
|
4395
|
+
}
|
|
4061
4396
|
function formatCliPhaseTitle(targets) {
|
|
4062
4397
|
const labels = [];
|
|
4063
4398
|
if (targets.includes("codex")) labels.push("Codex");
|
|
@@ -4119,6 +4454,18 @@ function installAction(options, deps = {}) {
|
|
|
4119
4454
|
exit(1);
|
|
4120
4455
|
return;
|
|
4121
4456
|
}
|
|
4457
|
+
const spec = installSpecFromOptions(options, validated.cli, err);
|
|
4458
|
+
executeSpec(spec, {
|
|
4459
|
+
log,
|
|
4460
|
+
err,
|
|
4461
|
+
exit,
|
|
4462
|
+
runPipeline,
|
|
4463
|
+
resolveHarnessRoot,
|
|
4464
|
+
verbose: options.verbose === true,
|
|
4465
|
+
...options.reinstall === true ? { mode: "reinstall" } : {}
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
function installSpecFromOptions(options, cli2, err) {
|
|
4122
4469
|
const forceInclude = normalizeRepeatable(options.with);
|
|
4123
4470
|
const forceExclude = normalizeRepeatable(options.without);
|
|
4124
4471
|
const baselineIds = new Set(
|
|
@@ -4145,7 +4492,7 @@ function installAction(options, deps = {}) {
|
|
|
4145
4492
|
const filteredInclude = forceInclude.filter((id) => validIds.has(id));
|
|
4146
4493
|
const filteredExclude = forceExclude.filter((id) => validIds.has(id));
|
|
4147
4494
|
const userOverride = filteredInclude.length > 0 || filteredExclude.length > 0 ? { forceInclude: filteredInclude, forceExclude: filteredExclude } : void 0;
|
|
4148
|
-
|
|
4495
|
+
return {
|
|
4149
4496
|
tracks: options.track ?? [],
|
|
4150
4497
|
...userOverride ? { userOverride } : {},
|
|
4151
4498
|
...baselineExclude.length > 0 ? { baselineExclude } : {},
|
|
@@ -4154,18 +4501,21 @@ function installAction(options, deps = {}) {
|
|
|
4154
4501
|
options: {
|
|
4155
4502
|
withCodexTrust: options.withCodexTrust === true
|
|
4156
4503
|
},
|
|
4157
|
-
cli:
|
|
4504
|
+
cli: cli2,
|
|
4158
4505
|
projectDir: resolve2(options.projectDir ?? process.cwd()),
|
|
4159
4506
|
scope: resolveScopeOption(options.scope, err)
|
|
4160
4507
|
};
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4508
|
+
}
|
|
4509
|
+
function installCommandLine(options) {
|
|
4510
|
+
const repeat = (flag, value) => normalizeRepeatable(value).map((v2) => `${flag} ${v2}`);
|
|
4511
|
+
return [
|
|
4512
|
+
"agent-harness install",
|
|
4513
|
+
...repeat("--track", options.track),
|
|
4514
|
+
...repeat("--cli", options.cli),
|
|
4515
|
+
...options.scope ? [`--scope ${options.scope}`] : [],
|
|
4516
|
+
...repeat("--with", options.with),
|
|
4517
|
+
...repeat("--without", options.without)
|
|
4518
|
+
].join(" ");
|
|
4169
4519
|
}
|
|
4170
4520
|
function executeSpec(spec, deps = {}) {
|
|
4171
4521
|
const log = deps.log ?? console.log;
|
|
@@ -4247,13 +4597,16 @@ function registerInstallCommand(cli2) {
|
|
|
4247
4597
|
).option(
|
|
4248
4598
|
"--with-codex-trust",
|
|
4249
4599
|
"[Codex] Codex global opt-in: register trust entry in ~/.codex/config.toml"
|
|
4600
|
+
).option(
|
|
4601
|
+
"--reinstall",
|
|
4602
|
+
"[Mode] Move .claude/ aside as .claude.backup-<ts> and rebuild it \u2014 use when .claude/ is damaged or missing"
|
|
4250
4603
|
).option("--verbose", "[Misc] Show installed file lists per category (default: counts only)").example("install --track tooling --with marketingskills").example("install --track csr-supabase --cli claude --cli codex").example("install --track csr-supabase --without netlify-cli --with railway-skills").action((options) => installAction(options));
|
|
4251
4604
|
}
|
|
4252
4605
|
|
|
4253
4606
|
// src/commands/list.ts
|
|
4254
4607
|
init_esm_shims();
|
|
4255
|
-
import { existsSync as
|
|
4256
|
-
import { join as
|
|
4608
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
|
|
4609
|
+
import { join as join18, resolve as resolve3 } from "path";
|
|
4257
4610
|
function listAction(options = {}, deps = {}) {
|
|
4258
4611
|
const log = deps.log ?? console.log;
|
|
4259
4612
|
const err = deps.err ?? console.error;
|
|
@@ -4272,7 +4625,7 @@ function listAction(options = {}, deps = {}) {
|
|
|
4272
4625
|
log(c.dim(` installed: ${installLog.installedAt}`));
|
|
4273
4626
|
log(c.dim(` scope: ${installLog.scope}`));
|
|
4274
4627
|
log(c.dim(` tracks: ${installLog.spec.tracks.join(", ") || "(none)"}`));
|
|
4275
|
-
log(c.dim(` cli: ${installLog.
|
|
4628
|
+
log(c.dim(` cli: ${installedClis(installLog).join(", ") || "(none)"}`));
|
|
4276
4629
|
log("");
|
|
4277
4630
|
log(c.bold(` Assets (${installLog.assets.length})`));
|
|
4278
4631
|
if (installLog.assets.length === 0) {
|
|
@@ -4316,8 +4669,8 @@ function formatTemplateRows(log, projectDir) {
|
|
|
4316
4669
|
const rows = [` ${c.dim(dirs.join(" "))}`];
|
|
4317
4670
|
const rootMd = log.templates.rootClaudeMd;
|
|
4318
4671
|
if (rootMd) {
|
|
4319
|
-
const path =
|
|
4320
|
-
const modified =
|
|
4672
|
+
const path = join18(projectDir, rootMd.path);
|
|
4673
|
+
const modified = existsSync17(path) && hashContent(readFileSync15(path, "utf8")) !== rootMd.sha256;
|
|
4321
4674
|
rows.push(
|
|
4322
4675
|
modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
|
|
4323
4676
|
);
|
|
@@ -4325,7 +4678,7 @@ function formatTemplateRows(log, projectDir) {
|
|
|
4325
4678
|
return rows;
|
|
4326
4679
|
}
|
|
4327
4680
|
function formatRootFileRows(rootFiles, projectDir) {
|
|
4328
|
-
const present = rootFiles.filter((f) =>
|
|
4681
|
+
const present = rootFiles.filter((f) => existsSync17(join18(projectDir, f.path)));
|
|
4329
4682
|
const width = Math.max(0, ...present.map((f) => f.path.length));
|
|
4330
4683
|
return present.map(
|
|
4331
4684
|
(f) => ` ${c.dim(padDisplay(f.path, width))} ${c.dim(f.change === "created" ? "\uC0DD\uC131" : "\uBCD1\uD569")} ${c.dim(f.notes.join(" / "))}`
|
|
@@ -4343,15 +4696,64 @@ function registerListCommand(cli2) {
|
|
|
4343
4696
|
init_esm_shims();
|
|
4344
4697
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
4345
4698
|
import {
|
|
4346
|
-
existsSync as
|
|
4699
|
+
existsSync as existsSync18,
|
|
4347
4700
|
lstatSync as lstatSync2,
|
|
4348
4701
|
readdirSync as readdirSync6,
|
|
4349
|
-
readFileSync as
|
|
4702
|
+
readFileSync as readFileSync16,
|
|
4350
4703
|
rmdirSync as rmdirSync2,
|
|
4351
4704
|
rmSync,
|
|
4352
4705
|
writeFileSync as writeFileSync7
|
|
4353
4706
|
} from "fs";
|
|
4354
|
-
import { dirname as dirname5, join as
|
|
4707
|
+
import { dirname as dirname5, join as join19, relative as relative2, resolve as resolve4, sep as sep3 } from "path";
|
|
4708
|
+
|
|
4709
|
+
// src/cli-ownership.ts
|
|
4710
|
+
init_esm_shims();
|
|
4711
|
+
var SHARED_BY_ALL = (self) => [
|
|
4712
|
+
{ path: ".mcp.json", kind: "keep", sharedWith: CLI_BASES.filter((c3) => c3 !== self) },
|
|
4713
|
+
{
|
|
4714
|
+
path: `${INSTALL_LOG_DIR}/`,
|
|
4715
|
+
kind: "keep",
|
|
4716
|
+
sharedWith: CLI_BASES.filter((c3) => c3 !== self)
|
|
4717
|
+
}
|
|
4718
|
+
];
|
|
4719
|
+
var CLI_OWNERSHIP = {
|
|
4720
|
+
claude: [
|
|
4721
|
+
{ path: ".claude/", kind: "dir", sharedWith: [] },
|
|
4722
|
+
{ path: HARNESS_ANCHOR_FILE, kind: "anchor", sharedWith: [] },
|
|
4723
|
+
{ path: "CLAUDE.md", kind: "import-block", sharedWith: [] },
|
|
4724
|
+
...SHARED_BY_ALL("claude")
|
|
4725
|
+
],
|
|
4726
|
+
codex: [
|
|
4727
|
+
{ path: ".codex/", kind: "dir", sharedWith: [] },
|
|
4728
|
+
{ path: "AGENTS.md", kind: "recorded", sharedWith: ["opencode"] },
|
|
4729
|
+
{ path: ".agents/skills/", kind: "recorded", sharedWith: ["opencode", "antigravity"] },
|
|
4730
|
+
...SHARED_BY_ALL("codex")
|
|
4731
|
+
],
|
|
4732
|
+
opencode: [
|
|
4733
|
+
{ path: ".opencode/", kind: "dir", sharedWith: [] },
|
|
4734
|
+
{ path: "opencode.json", kind: "recorded", sharedWith: [] },
|
|
4735
|
+
{ path: "AGENTS.md", kind: "recorded", sharedWith: ["codex"] },
|
|
4736
|
+
{ path: ".agents/skills/", kind: "recorded", sharedWith: ["codex", "antigravity"] },
|
|
4737
|
+
...SHARED_BY_ALL("opencode")
|
|
4738
|
+
],
|
|
4739
|
+
antigravity: [
|
|
4740
|
+
{ path: ".agents/rules/uzys-harness.md", kind: "recorded", sharedWith: [] },
|
|
4741
|
+
{ path: ".agents/skills/", kind: "recorded", sharedWith: ["codex", "opencode"] },
|
|
4742
|
+
...SHARED_BY_ALL("antigravity")
|
|
4743
|
+
]
|
|
4744
|
+
};
|
|
4745
|
+
function removableFor(cli2, remaining) {
|
|
4746
|
+
const others = remaining.filter((c3) => c3 !== cli2);
|
|
4747
|
+
const stillUsed = new Set(others.flatMap((c3) => CLI_OWNERSHIP[c3].map((p2) => p2.path)));
|
|
4748
|
+
const exclusive = [];
|
|
4749
|
+
const sharedNowUnowned = [];
|
|
4750
|
+
for (const owned of CLI_OWNERSHIP[cli2]) {
|
|
4751
|
+
if (owned.kind === "keep") continue;
|
|
4752
|
+
if (owned.sharedWith.length === 0) exclusive.push(owned);
|
|
4753
|
+
else if (!stillUsed.has(owned.path)) sharedNowUnowned.push(owned);
|
|
4754
|
+
}
|
|
4755
|
+
return { exclusive, sharedNowUnowned };
|
|
4756
|
+
}
|
|
4355
4757
|
|
|
4356
4758
|
// src/uninstall-interactive.ts
|
|
4357
4759
|
init_esm_shims();
|
|
@@ -5326,109 +5728,605 @@ ${a}
|
|
|
5326
5728
|
};
|
|
5327
5729
|
var jt = `${e("gray", $)} `;
|
|
5328
5730
|
|
|
5329
|
-
// src/
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5731
|
+
// src/prompts.ts
|
|
5732
|
+
init_esm_shims();
|
|
5733
|
+
|
|
5734
|
+
// src/router.ts
|
|
5735
|
+
init_esm_shims();
|
|
5736
|
+
var NO_RECORD = { clis: null, scope: null, repair: null };
|
|
5737
|
+
function buildInstallRecordView(state, log, claudeDirExists) {
|
|
5738
|
+
if (log === null) return NO_RECORD;
|
|
5739
|
+
const clis = installedClis(log);
|
|
5740
|
+
const broken = clis.includes("claude") && !claudeDirExists;
|
|
5741
|
+
return {
|
|
5742
|
+
clis,
|
|
5743
|
+
scope: log.scope,
|
|
5744
|
+
repair: broken ? repairCommand(log, state.tracks) : null
|
|
5745
|
+
};
|
|
5336
5746
|
}
|
|
5337
|
-
function
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5747
|
+
function repairCommand(log, detected) {
|
|
5748
|
+
const recorded = log.spec.tracks.filter(isTrack);
|
|
5749
|
+
const tracks = recorded.length > 0 ? recorded : detected;
|
|
5750
|
+
const trackArgs = tracks.map((t) => `--track ${t}`).join(" ");
|
|
5751
|
+
return `agent-harness install --reinstall ${trackArgs} --cli claude --scope ${log.scope}`;
|
|
5752
|
+
}
|
|
5753
|
+
function describeInstall(state, record = NO_RECORD) {
|
|
5754
|
+
const tracks = state.tracks.length > 0 ? state.tracks.join(", ") : "(none detected)";
|
|
5755
|
+
if (record.clis === null) return `Installed here (no record): tracks ${tracks} \xB7 CLI unknown`;
|
|
5756
|
+
const clis = record.clis.length > 0 ? record.clis.join(", ") : "(none)";
|
|
5757
|
+
const head = `Installed here: tracks ${tracks} \xB7 CLI ${clis} \xB7 scope ${record.scope} (record: ${INSTALL_LOG_DIR}/${INSTALL_LOG_FILENAME})`;
|
|
5758
|
+
if (record.repair === null) return head;
|
|
5759
|
+
return [
|
|
5760
|
+
head,
|
|
5761
|
+
"\u26A0 .claude/ is missing but the record says Claude Code is installed. Update cannot rebuild it.",
|
|
5762
|
+
` Repair: ${record.repair}`
|
|
5763
|
+
].join("\n");
|
|
5352
5764
|
}
|
|
5353
|
-
|
|
5354
|
-
const
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5765
|
+
function buildRouterChoices(_state, record = NO_RECORD) {
|
|
5766
|
+
const broken = typeof record.repair === "string";
|
|
5767
|
+
return [
|
|
5768
|
+
{
|
|
5769
|
+
value: "update",
|
|
5770
|
+
label: "Update",
|
|
5771
|
+
// 이 hint 가 update 동작의 광고다 — 갱신 대상(rules/agents/commands/hooks/skills)과 편집분
|
|
5772
|
+
// 백업을 빠뜨리면 거짓출하가 된다(R-3a · `tests/render-hint-parity.test.ts` 가 derive 로 문다).
|
|
5773
|
+
hint: broken ? ".claude/ missing \u2014 run the repair command above" : "bring installed rules / agents / commands / hooks / skills to this release; add tracks, CLIs or assets on the way \u2014 nothing is removed here, your edits are backed up as *.backup-<time>",
|
|
5774
|
+
enabled: !broken
|
|
5775
|
+
},
|
|
5776
|
+
{
|
|
5777
|
+
value: "uninstall",
|
|
5778
|
+
label: "Uninstall",
|
|
5779
|
+
hint: "remove everything, one CLI, or single assets",
|
|
5780
|
+
enabled: true
|
|
5781
|
+
},
|
|
5782
|
+
{
|
|
5783
|
+
value: "exit",
|
|
5784
|
+
label: "Exit",
|
|
5785
|
+
enabled: true
|
|
5786
|
+
}
|
|
5787
|
+
];
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
// src/wizard-steps.ts
|
|
5791
|
+
init_esm_shims();
|
|
5792
|
+
var WIZARD_TOTAL = 6;
|
|
5793
|
+
var WIZARD = {
|
|
5794
|
+
TRACKS: { current: 1, total: WIZARD_TOTAL },
|
|
5795
|
+
CLI: { current: 2, total: WIZARD_TOTAL },
|
|
5796
|
+
TARGETS: { current: 3, total: WIZARD_TOTAL },
|
|
5797
|
+
SCOPE: { current: 4, total: WIZARD_TOTAL },
|
|
5798
|
+
CONFIRM: { current: 5, total: WIZARD_TOTAL },
|
|
5799
|
+
INSTALL: { current: 6, total: WIZARD_TOTAL }
|
|
5800
|
+
};
|
|
5801
|
+
var UPDATE_WIZARD_TOTAL = 5;
|
|
5802
|
+
var UPDATE_WIZARD = {
|
|
5803
|
+
TRACKS: { current: 1, total: UPDATE_WIZARD_TOTAL },
|
|
5804
|
+
CLI: { current: 2, total: UPDATE_WIZARD_TOTAL },
|
|
5805
|
+
TARGETS: { current: 3, total: UPDATE_WIZARD_TOTAL },
|
|
5806
|
+
CONFIRM: { current: 4, total: UPDATE_WIZARD_TOTAL },
|
|
5807
|
+
RUN: { current: 5, total: UPDATE_WIZARD_TOTAL }
|
|
5808
|
+
};
|
|
5809
|
+
function stepLabel(step, suffix) {
|
|
5810
|
+
if (!step) return suffix;
|
|
5811
|
+
return `Step ${step.current}/${step.total} \u2014 ${suffix}`;
|
|
5812
|
+
}
|
|
5813
|
+
|
|
5814
|
+
// src/prompts.ts
|
|
5815
|
+
var TRACK_LABELS = {
|
|
5816
|
+
// #533 — base 는 모든 dev 트랙에 이미 들어 있다(dev 트랙 8종 ∪ base = 추가 파일 0, 설계 §5 실측).
|
|
5817
|
+
// 함께 골라도 해는 없어 막지 않고(사용자 결정 2026-09-27), 그 사실만 라벨로 알린다.
|
|
5818
|
+
base: "base \u2014 no stack yet \u2014 every dev track already includes it",
|
|
5819
|
+
tooling: "tooling \u2014 Bash + Markdown meta-project",
|
|
5820
|
+
"csr-supabase": "csr-supabase \u2014 Vite + React + Supabase",
|
|
5821
|
+
"csr-fastify": "csr-fastify \u2014 Vite + React + Fastify",
|
|
5822
|
+
"csr-fastapi": "csr-fastapi \u2014 Vite + React + FastAPI",
|
|
5823
|
+
"ssr-htmx": "ssr-htmx \u2014 htmx + FastAPI",
|
|
5824
|
+
"ssr-nextjs": "ssr-nextjs \u2014 Next.js (App Router)",
|
|
5825
|
+
data: "data \u2014 Python data / DuckDB / PySide6",
|
|
5826
|
+
executive: "executive \u2014 proposals / DD / pitch (no agent-skills)",
|
|
5827
|
+
full: "full \u2014 all dev capabilities",
|
|
5828
|
+
"project-management": "project-management \u2014 PM / Scrum / Jira / Confluence",
|
|
5829
|
+
"growth-marketing": "growth-marketing \u2014 Growth / Marketing / Content"
|
|
5830
|
+
};
|
|
5831
|
+
var VISIBLE_OPTION_DEFS = [];
|
|
5832
|
+
var CLI_BASE_LABELS = {
|
|
5833
|
+
claude: "Claude Code",
|
|
5834
|
+
codex: "Codex (OpenAI)",
|
|
5835
|
+
opencode: "OpenCode (anomalyco)",
|
|
5836
|
+
antigravity: "Antigravity (Google)"
|
|
5837
|
+
};
|
|
5838
|
+
var INSTALL_TARGET_PAGES = [
|
|
5839
|
+
// 2026-08-16 — 트랙이 고르는 자산을 **맨 앞에서** 전부 보여준다. 그전까지 이 항목들은 화면에
|
|
5840
|
+
// 한 번도 안 나온 채 깔렸고, 사용자는 설치가 끝난 뒤 요약에서 처음 봤다.
|
|
5841
|
+
//
|
|
5842
|
+
// 2페이지로 나눈 근거는 실측이다: 최대 트랙(`full`)이 32항목(당시 실측 — rules 6 · agents 9 · hooks 3 ·
|
|
5843
|
+
// skills 14)이라 clack groupMultiselect 의 "페이지당 ≤ ~30행" 제약을 한 페이지로는 못 지킨다.
|
|
5844
|
+
// 가르는 축은 역할이다 — 정책·가드가 한쪽, 실행 능력이 다른 쪽.
|
|
5845
|
+
{ label: "Track baseline \u2014 Rules & Hooks (\uC815\uCC45\xB7\uAC00\uB4DC)", cats: [], baseline: ["rules", "hooks"] },
|
|
5846
|
+
{
|
|
5847
|
+
label: "Track baseline \u2014 Agents & Skills (\uC5D0\uC774\uC804\uD2B8\xB7\uC2A4\uD0AC)",
|
|
5848
|
+
cats: [],
|
|
5849
|
+
baseline: ["agents", "skills"]
|
|
5850
|
+
},
|
|
5851
|
+
{ label: "Dev Core (Frontend \xB7 Backend \xB7 Data)", cats: ["frontend", "backend", "data"] },
|
|
5852
|
+
{ label: "Dev Tools (Security \xB7 Quality \xB7 Understanding)", cats: ["dev-tools", "understanding"] },
|
|
5853
|
+
{ label: "Business (PM \xB7 Executive \xB7 Documents)", cats: ["business"] },
|
|
5854
|
+
// v26.85.0 — 코드-퍼스트 비주얼/미디어 제작 (용도별 섹션, 전부 opt-in).
|
|
5855
|
+
{ label: "Visual & Media (Slides \xB7 Diagrams \xB7 Motion \xB7 Video)", cats: ["visual-media"] },
|
|
5856
|
+
{ label: "Workflow", cats: ["workflow"] }
|
|
5857
|
+
];
|
|
5858
|
+
var BASELINE_TITLES = {
|
|
5859
|
+
rules: "Rules \u2014 \uC0C1\uC2DC \uC0C1\uC8FC\uD558\uB294 \uB300\uC6D0\uCE59 (4 CLI \uC804\uBD80\uC5D0 \uC124\uCE58)",
|
|
5860
|
+
hooks: "Hooks \u2014 \uACB0\uC815\uB860\uC801 \uAC00\uB4DC (\uD574\uC81C\uD558\uBA74 settings.json \uBC30\uC120\uB3C4 \uD568\uAED8 \uBE60\uC9C4\uB2E4)",
|
|
5861
|
+
agents: "Agents \u2014 \uB3C5\uB9BD \uB808\uC778 (\uB9AC\uBDF0\xB7\uAC80\uC99D)",
|
|
5862
|
+
skills: "Skills \u2014 \uD544\uC694\uD560 \uB54C \uC5F4\uB9AC\uB294 \uC791\uC5C5 \uC808\uCC28"
|
|
5863
|
+
};
|
|
5864
|
+
function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Set(), baselineKinds = [], baselineTargets = []) {
|
|
5865
|
+
const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
|
|
5866
|
+
const groups = {};
|
|
5867
|
+
const flatItems = [];
|
|
5868
|
+
for (const kind of baselineKinds) {
|
|
5869
|
+
const items = baselineTargets.filter((t) => t.kind === kind).map((t) => ({
|
|
5870
|
+
value: t.id,
|
|
5871
|
+
label: ` ${t.name}${installedMark(t.id)}`,
|
|
5872
|
+
...t.hint ? { hint: t.hint } : {}
|
|
5873
|
+
}));
|
|
5874
|
+
if (items.length === 0) continue;
|
|
5875
|
+
groups[BASELINE_TITLES[kind]] = items;
|
|
5876
|
+
flatItems.push(...items);
|
|
5366
5877
|
}
|
|
5367
|
-
|
|
5368
|
-
const
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
`
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5878
|
+
for (const cat of cats) {
|
|
5879
|
+
const items = [];
|
|
5880
|
+
for (const o of VISIBLE_OPTION_DEFS.filter((d2) => d2.category === cat)) {
|
|
5881
|
+
items.push({
|
|
5882
|
+
value: `option:${o.key}`,
|
|
5883
|
+
// v26.62.3 — group header 와 옵션 사이 시각 hierarchy 강화. label prefix 4 space.
|
|
5884
|
+
label: ` ${o.label} [${o.source}]${installedMark(`option:${o.key}`)}`,
|
|
5885
|
+
hint: o.hint
|
|
5886
|
+
});
|
|
5887
|
+
}
|
|
5888
|
+
const tierOrder = { official: 0, vetted: 1, experimental: 2 };
|
|
5889
|
+
const catAssets = [...EXTERNAL_ASSETS.filter((x) => x.category === cat)].sort(
|
|
5890
|
+
(a, b2) => tierOrder[assetTrustTier(a.id)] - tierOrder[assetTrustTier(b2.id)]
|
|
5375
5891
|
);
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5892
|
+
for (const a of catAssets) {
|
|
5893
|
+
const tier = assetTrustTier(a.id);
|
|
5894
|
+
const badge = tier === "official" ? " \u2605 official" : tier === "experimental" ? " \u26A0 experimental (opt-in)" : "";
|
|
5895
|
+
items.push({
|
|
5896
|
+
value: `asset:${a.id}`,
|
|
5897
|
+
label: ` ${a.id} [${a.source}]${badge}${installedMark(`asset:${a.id}`)}`,
|
|
5898
|
+
hint: a.description
|
|
5899
|
+
});
|
|
5379
5900
|
}
|
|
5380
|
-
|
|
5901
|
+
if (items.length === 0) continue;
|
|
5902
|
+
const selectedInCat = items.filter((it3) => initialSet.has(it3.value)).length;
|
|
5903
|
+
const header = `${CATEGORY_TITLES[cat]} [${selectedInCat}/${items.length} \u2713 default]`;
|
|
5904
|
+
groups[header] = items;
|
|
5905
|
+
flatItems.push(...items);
|
|
5381
5906
|
}
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5907
|
+
return { groups, flatItems };
|
|
5908
|
+
}
|
|
5909
|
+
function assertPagesCoverAllCategories(pages) {
|
|
5910
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5911
|
+
for (const page of pages) {
|
|
5912
|
+
for (const cat of page.cats) counts.set(cat, (counts.get(cat) ?? 0) + 1);
|
|
5386
5913
|
}
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5914
|
+
const missing = CATEGORIES.filter((c3) => !counts.has(c3));
|
|
5915
|
+
const duplicated = CATEGORIES.filter((c3) => (counts.get(c3) ?? 0) > 1);
|
|
5916
|
+
if (missing.length > 0 || duplicated.length > 0) {
|
|
5917
|
+
throw new Error(
|
|
5918
|
+
`INSTALL_TARGET_PAGES drift vs CATEGORIES \u2014 missing=[${missing.join(", ")}] duplicated=[${duplicated.join(", ")}]. \uBAA8\uB4E0 \uCE74\uD14C\uACE0\uB9AC\uB294 \uC815\uD655\uD788 \uD55C \uD398\uC774\uC9C0\uC5D0 \uBC30\uCE58\uD574\uC57C \uD55C\uB2E4 (no-false-ship: wizard \uBBF8\uB178\uCD9C = \uAC70\uC9D3 \uAD11\uACE0).`
|
|
5919
|
+
);
|
|
5390
5920
|
}
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
)
|
|
5399
|
-
|
|
5921
|
+
}
|
|
5922
|
+
assertPagesCoverAllCategories(INSTALL_TARGET_PAGES);
|
|
5923
|
+
function viewportItems(itemCount) {
|
|
5924
|
+
const rows = process.stdout.rows ?? 24;
|
|
5925
|
+
return Math.max(8, Math.min(itemCount, rows - 10));
|
|
5926
|
+
}
|
|
5927
|
+
var defaultPrompts = {
|
|
5928
|
+
intro: (msg) => ge(msg),
|
|
5929
|
+
outro: (msg) => ye(msg),
|
|
5930
|
+
cancel: (msg) => me(msg),
|
|
5931
|
+
selectTracks: async (initial, step, installed = []) => {
|
|
5932
|
+
const locked = new Set(installed);
|
|
5933
|
+
const result = await ve({
|
|
5934
|
+
message: locked.size > 0 ? `${stepLabel(step, "Tracks")} (\u25CF installed = already here, locked \xB7 check a new one to add its rules \xB7 agents \xB7 skills)` : stepLabel(step, "Select Track(s)"),
|
|
5935
|
+
options: TRACKS.map((t) => ({
|
|
5936
|
+
value: t,
|
|
5937
|
+
label: locked.has(t) ? `${TRACK_LABELS[t]} \u25CF installed` : TRACK_LABELS[t]
|
|
5938
|
+
})),
|
|
5939
|
+
...initial ? { initialValues: initial } : {},
|
|
5940
|
+
maxItems: viewportItems(11),
|
|
5941
|
+
required: true
|
|
5942
|
+
});
|
|
5943
|
+
return q(result) ? null : result;
|
|
5944
|
+
},
|
|
5945
|
+
selectCli: async (initial, step, installed = []) => {
|
|
5946
|
+
const initialValues = initial && initial.length > 0 ? [...initial] : ["claude"];
|
|
5947
|
+
const locked = new Set(installed);
|
|
5948
|
+
const label = (c3) => locked.has(c3) ? `${CLI_BASE_LABELS[c3]} \u25CF installed` : CLI_BASE_LABELS[c3];
|
|
5949
|
+
const result = await ve({
|
|
5950
|
+
message: locked.size > 0 ? `${stepLabel(step, "CLIs")} (\u25CF installed = locked \u2014 remove one with Uninstall \xB7 check another to install its files)` : stepLabel(step, "Target CLI(s)"),
|
|
5951
|
+
options: [
|
|
5952
|
+
{ value: "claude", label: label("claude") },
|
|
5953
|
+
{ value: "codex", label: label("codex") },
|
|
5954
|
+
{ value: "opencode", label: label("opencode") },
|
|
5955
|
+
{ value: "antigravity", label: label("antigravity") }
|
|
5956
|
+
],
|
|
5957
|
+
initialValues,
|
|
5958
|
+
required: true
|
|
5959
|
+
});
|
|
5960
|
+
if (q(result)) return null;
|
|
5961
|
+
return [...result].sort(
|
|
5962
|
+
(a, b2) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b2]
|
|
5963
|
+
);
|
|
5964
|
+
},
|
|
5965
|
+
selectAction: async (state, record) => {
|
|
5966
|
+
const result = await Ee({
|
|
5967
|
+
message: describeInstall(state, record),
|
|
5968
|
+
options: buildRouterChoices(state, record).map((c3) => {
|
|
5969
|
+
const label = c3.enabled ? c3.label : `${c3.label} [disabled]`;
|
|
5970
|
+
return {
|
|
5971
|
+
value: c3.value,
|
|
5972
|
+
label,
|
|
5973
|
+
...c3.hint ? { hint: c3.hint } : {},
|
|
5974
|
+
...c3.enabled ? {} : { disabled: true }
|
|
5975
|
+
};
|
|
5976
|
+
})
|
|
5977
|
+
});
|
|
5978
|
+
return q(result) ? null : result;
|
|
5979
|
+
},
|
|
5980
|
+
/**
|
|
5981
|
+
* v26.64.0 (ADR-020) — Installation scope select. Default Project (D16 — no global write).
|
|
5982
|
+
* Global 은 사용자 명시 opt-in 시에만.
|
|
5983
|
+
*/
|
|
5984
|
+
selectScope: async (initial = "project", step) => {
|
|
5985
|
+
const result = await Ee({
|
|
5986
|
+
message: stepLabel(step, "Installation scope"),
|
|
5987
|
+
initialValue: initial,
|
|
5988
|
+
options: [
|
|
5989
|
+
{
|
|
5990
|
+
value: "project",
|
|
5991
|
+
label: "Project",
|
|
5992
|
+
hint: "Install in current directory (committed with your project)"
|
|
5993
|
+
},
|
|
5994
|
+
{
|
|
5995
|
+
value: "global",
|
|
5996
|
+
label: "Global",
|
|
5997
|
+
hint: "Write to ~/.claude/, ~/.codex/, npm -g (shared across all projects)"
|
|
5998
|
+
}
|
|
5999
|
+
]
|
|
6000
|
+
});
|
|
6001
|
+
return q(result) ? null : result;
|
|
6002
|
+
},
|
|
6003
|
+
selectUpdateGroups: async () => {
|
|
6004
|
+
const result = await ve({
|
|
6005
|
+
message: "What to update (space = toggle \xB7 your edits are always backed up)",
|
|
6006
|
+
options: [
|
|
6007
|
+
{ value: "skills", label: "Skills", hint: "installed bundled skills \u2192 latest" },
|
|
6008
|
+
{
|
|
6009
|
+
value: "new-skills",
|
|
6010
|
+
label: "New skills",
|
|
6011
|
+
hint: "bundled skills added since your install"
|
|
6012
|
+
},
|
|
6013
|
+
{
|
|
6014
|
+
value: "rules",
|
|
6015
|
+
label: "Rules \xB7 agents \xB7 commands",
|
|
6016
|
+
hint: "your edited files \u2192 *.backup-<time>"
|
|
6017
|
+
},
|
|
6018
|
+
{
|
|
6019
|
+
value: "anchor",
|
|
6020
|
+
label: "CLAUDE-uzys-harness.md anchor",
|
|
6021
|
+
hint: "your edited anchor \u2192 *.backup-<time>"
|
|
6022
|
+
},
|
|
6023
|
+
{
|
|
6024
|
+
value: "hooks",
|
|
6025
|
+
label: "Hooks \xB7 settings.json",
|
|
6026
|
+
hint: "stale hook refs cleaned"
|
|
6027
|
+
},
|
|
6028
|
+
{
|
|
6029
|
+
value: "external",
|
|
6030
|
+
label: "Codex / OpenCode / Antigravity + external skills",
|
|
6031
|
+
hint: "rendered artifacts + npx skills"
|
|
6032
|
+
}
|
|
6033
|
+
],
|
|
6034
|
+
initialValues: [...UPDATE_GROUPS],
|
|
6035
|
+
required: true
|
|
6036
|
+
});
|
|
6037
|
+
return q(result) ? null : result;
|
|
6038
|
+
},
|
|
6039
|
+
confirmInstall: async (summary) => {
|
|
6040
|
+
const result = await ue({
|
|
6041
|
+
message: `${summary}
|
|
6042
|
+
|
|
6043
|
+
Proceed?`,
|
|
6044
|
+
initialValue: true
|
|
6045
|
+
});
|
|
6046
|
+
return q(result) ? null : result;
|
|
6047
|
+
},
|
|
6048
|
+
selectInstallTargets: async (initialChecked, step, recap) => {
|
|
6049
|
+
const pages = INSTALL_TARGET_PAGES;
|
|
6050
|
+
const baselineTargets = withBaselineHints(listBaselineTargets({ tracks: recap?.tracks ?? [] }));
|
|
6051
|
+
const initialSet = new Set(initialChecked);
|
|
6052
|
+
const collected = new Set(initialChecked);
|
|
6053
|
+
const recapLine = recap ? `Tracks: ${recap.tracks.join(", ")} \xB7 CLIs: ${recap.cli.join(", ")}` : "";
|
|
6054
|
+
const installedSet = new Set(recap?.installed ?? []);
|
|
6055
|
+
process.stdout.write("\x1B[?1049h");
|
|
6056
|
+
let resultIds = null;
|
|
6057
|
+
let aborted = false;
|
|
6058
|
+
try {
|
|
6059
|
+
let pageIdx = 0;
|
|
6060
|
+
let dir = 1;
|
|
6061
|
+
while (pageIdx < pages.length) {
|
|
6062
|
+
const page = pages[pageIdx];
|
|
6063
|
+
if (!page) break;
|
|
6064
|
+
const { groups, flatItems } = buildPageGroups(
|
|
6065
|
+
page.cats,
|
|
6066
|
+
initialSet,
|
|
6067
|
+
installedSet,
|
|
6068
|
+
page.baseline ?? [],
|
|
6069
|
+
baselineTargets
|
|
6070
|
+
);
|
|
6071
|
+
if (flatItems.length === 0) {
|
|
6072
|
+
pageIdx += dir;
|
|
6073
|
+
if (pageIdx < 0) {
|
|
6074
|
+
aborted = true;
|
|
6075
|
+
break;
|
|
6076
|
+
}
|
|
6077
|
+
continue;
|
|
6078
|
+
}
|
|
6079
|
+
const selectedNow = flatItems.filter((it3) => collected.has(it3.value)).map((it3) => it3.value);
|
|
6080
|
+
const pageDefault = flatItems.filter((it3) => initialSet.has(it3.value)).length;
|
|
6081
|
+
const totalSelected = collected.size;
|
|
6082
|
+
const message = [
|
|
6083
|
+
`Step ${step.current}/${step.total} \xB7 Page ${pageIdx + 1}/${pages.length} \xB7 ${page.label}`,
|
|
6084
|
+
recapLine ? ` ${recapLine}` : "",
|
|
6085
|
+
` Selected so far: ${totalSelected} items \xB7 This page default \u2713 ${pageDefault}/${flatItems.length}`,
|
|
6086
|
+
// 마커의 뜻을 화면에서 바로 알려준다 — 체크 해제를 제거로 오해하는 것이 이 화면의
|
|
6087
|
+
// 원래 문제였으므로, 마커가 보일 때는 제거 경로를 같은 자리에서 말한다.
|
|
6088
|
+
installedSet.size > 0 ? " \u25CF installed = \uC774\uBBF8 \uC124\uCE58\uB428 \xB7 \uCCB4\uD06C \uD574\uC81C\uD574\uB3C4 \uC81C\uAC70\uB418\uC9C0 \uC54A\uB294\uB2E4 (\uC81C\uAC70: Uninstall \uBA54\uB274 \uB610\uB294 agent-harness uninstall)" : "",
|
|
6089
|
+
" Space toggle \xB7 Enter \u2192 next \xB7 ESC \u2192 prev"
|
|
6090
|
+
].filter(Boolean).join("\n");
|
|
6091
|
+
const groupOpts = {
|
|
6092
|
+
message,
|
|
6093
|
+
options: groups,
|
|
6094
|
+
initialValues: selectedNow,
|
|
6095
|
+
required: false,
|
|
6096
|
+
selectableGroups: false
|
|
6097
|
+
};
|
|
6098
|
+
const result = await pe(groupOpts);
|
|
6099
|
+
if (q(result)) {
|
|
6100
|
+
if (pageIdx === 0) {
|
|
6101
|
+
aborted = true;
|
|
6102
|
+
break;
|
|
6103
|
+
}
|
|
6104
|
+
dir = -1;
|
|
6105
|
+
pageIdx--;
|
|
6106
|
+
continue;
|
|
6107
|
+
}
|
|
6108
|
+
for (const it3 of flatItems) collected.delete(it3.value);
|
|
6109
|
+
for (const v2 of result) collected.add(v2);
|
|
6110
|
+
dir = 1;
|
|
6111
|
+
pageIdx++;
|
|
6112
|
+
}
|
|
6113
|
+
if (!aborted) {
|
|
6114
|
+
resultIds = [...collected];
|
|
6115
|
+
}
|
|
6116
|
+
} finally {
|
|
6117
|
+
process.stdout.write("\x1B[?1049l");
|
|
6118
|
+
}
|
|
6119
|
+
if (resultIds !== null) {
|
|
6120
|
+
process.stdout.write(
|
|
6121
|
+
`\u25C7 Step ${step.current}/${step.total} \u2014 Install targets \xB7 ${resultIds.length} selected
|
|
6122
|
+
\u2502
|
|
6123
|
+
`
|
|
6124
|
+
);
|
|
6125
|
+
}
|
|
6126
|
+
return resultIds;
|
|
6127
|
+
}
|
|
6128
|
+
};
|
|
6129
|
+
|
|
6130
|
+
// src/uninstall-interactive.ts
|
|
6131
|
+
function buildRemovableRows(assets) {
|
|
6132
|
+
return assets.map((a) => ({
|
|
6133
|
+
value: a.id,
|
|
6134
|
+
label: `${a.id} [${a.method}]${a.version ? ` v${a.version}` : ""}`,
|
|
6135
|
+
hint: hintFor(a)
|
|
6136
|
+
}));
|
|
6137
|
+
}
|
|
6138
|
+
function hintFor(asset) {
|
|
6139
|
+
if (asset.scope === "global") {
|
|
6140
|
+
return "global scope \u2014 not removed automatically; prints the manual command to run";
|
|
6141
|
+
}
|
|
6142
|
+
switch (asset.method) {
|
|
6143
|
+
case "plugin":
|
|
6144
|
+
return "claude plugin uninstall --scope project";
|
|
6145
|
+
case "skill":
|
|
6146
|
+
return "npx skills remove";
|
|
6147
|
+
case "npm":
|
|
6148
|
+
return "npm uninstall --save-dev";
|
|
6149
|
+
case "npx-run":
|
|
6150
|
+
// shell-script = #492 에서 은퇴한 legacy kind. 옛 로그가 그대로 들어온다.
|
|
6151
|
+
case "shell-script":
|
|
6152
|
+
case "internal":
|
|
6153
|
+
return 'no automatic reverse path \u2014 only "Remove everything" clears it';
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
function buildUninstallModeChoices(clis, assetCount) {
|
|
6157
|
+
return [
|
|
6158
|
+
{
|
|
6159
|
+
value: "cli",
|
|
6160
|
+
label: "Remove one CLI",
|
|
6161
|
+
hint: clis.length > 1 ? "that CLI's own files go; files shared with a remaining CLI stay; your text stays" : 'only one CLI here \u2014 use "Remove everything"',
|
|
6162
|
+
enabled: clis.length > 1
|
|
6163
|
+
},
|
|
6164
|
+
{
|
|
6165
|
+
value: "selected",
|
|
6166
|
+
label: "Remove selected assets",
|
|
6167
|
+
hint: assetCount > 0 ? `pick from the ${assetCount} external assets; templates (.claude/ etc.) stay` : "no external assets recorded",
|
|
6168
|
+
enabled: assetCount > 0
|
|
6169
|
+
},
|
|
6170
|
+
{
|
|
6171
|
+
value: "all",
|
|
6172
|
+
label: "Remove everything",
|
|
6173
|
+
hint: "assets + templates + the install record \u2014 CLI folders (.claude/ \xB7 .codex/ \xB7 .opencode/, whichever exist) are moved aside as <dir>.backup-<time>",
|
|
6174
|
+
enabled: true
|
|
6175
|
+
}
|
|
6176
|
+
];
|
|
6177
|
+
}
|
|
6178
|
+
function describeOwned(p2) {
|
|
6179
|
+
if (p2.kind === "dir") {
|
|
6180
|
+
return `${p2.path} \u2192 moved aside as ${p2.path.replace(/\/+$/, "")}.backup-<time> (your own files there stay in it)`;
|
|
6181
|
+
}
|
|
6182
|
+
if (p2.kind === "import-block") return `the import block in ${p2.path} (your text stays)`;
|
|
6183
|
+
if (p2.path === "AGENTS.md") return "AGENTS.md harness sections (your ## Project Context stays)";
|
|
6184
|
+
if (p2.path === ".agents/skills/") return ".agents/skills/<harness skills>";
|
|
6185
|
+
return p2.path;
|
|
6186
|
+
}
|
|
6187
|
+
function removalSummary(cli2, installed) {
|
|
6188
|
+
const remaining = installed.filter((c3) => c3 !== cli2);
|
|
6189
|
+
const { exclusive, sharedNowUnowned } = removableFor(cli2, remaining);
|
|
6190
|
+
const lines = [];
|
|
6191
|
+
if (exclusive.length > 0) {
|
|
6192
|
+
lines.push(
|
|
6193
|
+
` \xB7 ${exclusive.map(describeOwned).join(" \xB7 ")} (only ${CLI_BASE_LABELS[cli2]} uses them)`
|
|
6194
|
+
);
|
|
6195
|
+
}
|
|
6196
|
+
if (sharedNowUnowned.length > 0) {
|
|
6197
|
+
lines.push(
|
|
6198
|
+
` \xB7 ${sharedNowUnowned.map(describeOwned).join(" \xB7 ")} (${CLI_BASE_LABELS[cli2]} was the last user)`
|
|
6199
|
+
);
|
|
6200
|
+
}
|
|
6201
|
+
const kept = [
|
|
6202
|
+
...new Set(
|
|
6203
|
+
remaining.flatMap(
|
|
6204
|
+
(c3) => CLI_OWNERSHIP[c3].filter((p2) => p2.kind !== "keep").map((p2) => p2.path)
|
|
6205
|
+
)
|
|
6206
|
+
)
|
|
6207
|
+
];
|
|
6208
|
+
lines.push(
|
|
6209
|
+
` \xB7 kept: ${[...kept, ".mcp.json", `install record (clis: ${remaining.join(", ")})`].join(" \xB7 ")}`
|
|
6210
|
+
);
|
|
6211
|
+
return lines;
|
|
6212
|
+
}
|
|
6213
|
+
function buildCliRows(installed) {
|
|
6214
|
+
return installed.map((cli2) => {
|
|
6215
|
+
const { exclusive, sharedNowUnowned } = removableFor(
|
|
6216
|
+
cli2,
|
|
6217
|
+
installed.filter((c3) => c3 !== cli2)
|
|
6218
|
+
);
|
|
6219
|
+
return {
|
|
6220
|
+
value: cli2,
|
|
6221
|
+
label: CLI_BASE_LABELS[cli2],
|
|
6222
|
+
hint: `removes ${[...exclusive, ...sharedNowUnowned].map(describeOwned).join(" \xB7 ")}`
|
|
6223
|
+
};
|
|
6224
|
+
});
|
|
6225
|
+
}
|
|
6226
|
+
async function runInteractiveUninstall(projectDir, deps = {}) {
|
|
6227
|
+
const prompts = deps.prompts ?? defaultUninstallPrompts();
|
|
6228
|
+
const isTty = deps.isTty ?? (() => Boolean(process.stdin.isTTY));
|
|
6229
|
+
const readLog = deps.readLog ?? readInstallLog;
|
|
6230
|
+
if (!isTty()) return { ok: false, reason: "no-tty" };
|
|
6231
|
+
const log = readLog(projectDir);
|
|
6232
|
+
if (!log) return { ok: false, reason: "no-log" };
|
|
6233
|
+
if (!deps.embedded) prompts.intro("uzys-agent-harness \xB7 uninstall");
|
|
6234
|
+
const rows = buildRemovableRows(log.assets);
|
|
6235
|
+
const clis = installedClis(log);
|
|
6236
|
+
const mode = await prompts.selectMode(
|
|
6237
|
+
buildUninstallModeChoices(clis, rows.length),
|
|
6238
|
+
`uzys-agent-harness \xB7 uninstall installed: ${clis.join(", ") || "(none)"} \xB7 assets ${rows.length}`
|
|
6239
|
+
);
|
|
6240
|
+
if (mode === null) {
|
|
6241
|
+
prompts.cancel("Cancelled.");
|
|
6242
|
+
return { ok: false, reason: "cancelled" };
|
|
6243
|
+
}
|
|
6244
|
+
if (mode === "cli") return pickCli(projectDir, clis, prompts);
|
|
6245
|
+
if (mode === "all") {
|
|
6246
|
+
const ok2 = await prompts.confirm(
|
|
6247
|
+
[
|
|
6248
|
+
"Remove everything? This is the same as: agent-harness uninstall --yes",
|
|
6249
|
+
` \xB7 ${log.assets.length} recorded asset(s) \u2014 only those with an automatic reverse path are removed`,
|
|
6250
|
+
" \xB7 templates: CLI folders (.claude/ \xB7 .codex/ \xB7 .opencode/, whichever exist) are moved aside as <dir>.backup-<time> (your own files there stay in them)",
|
|
6251
|
+
" \xB7 the install record goes too",
|
|
6252
|
+
" \xB7 files outside (.mcp.json etc.) are not deleted \u2014 you get instructions instead"
|
|
6253
|
+
].join("\n")
|
|
6254
|
+
);
|
|
6255
|
+
if (!ok2) {
|
|
6256
|
+
prompts.cancel("Cancelled.");
|
|
6257
|
+
return { ok: false, reason: "cancelled" };
|
|
6258
|
+
}
|
|
6259
|
+
return { ok: true, options: { projectDir } };
|
|
6260
|
+
}
|
|
6261
|
+
const picked = await prompts.selectAssets(rows);
|
|
6262
|
+
if (picked === null) {
|
|
6263
|
+
prompts.cancel("Cancelled.");
|
|
6264
|
+
return { ok: false, reason: "cancelled" };
|
|
6265
|
+
}
|
|
6266
|
+
if (picked.length === 0) {
|
|
6267
|
+
prompts.outro("Nothing selected \u2014 no changes.");
|
|
6268
|
+
return { ok: false, reason: "nothing-selected" };
|
|
6269
|
+
}
|
|
6270
|
+
const ok = await prompts.confirm(
|
|
6271
|
+
[
|
|
6272
|
+
`Remove ${picked.length} selected asset(s)? This is the same as: agent-harness uninstall --only ${picked.join(",")}`,
|
|
6273
|
+
...picked.map((id) => ` \xB7 ${id}`),
|
|
6274
|
+
"",
|
|
6275
|
+
"Templates (.claude/ etc.) stay."
|
|
6276
|
+
].join("\n")
|
|
6277
|
+
);
|
|
6278
|
+
if (!ok) {
|
|
5400
6279
|
prompts.cancel("Cancelled.");
|
|
5401
6280
|
return { ok: false, reason: "cancelled" };
|
|
5402
6281
|
}
|
|
5403
6282
|
return { ok: true, options: { projectDir, only: picked.join(",") } };
|
|
5404
6283
|
}
|
|
6284
|
+
async function pickCli(projectDir, clis, prompts) {
|
|
6285
|
+
const cli2 = await prompts.selectCli(buildCliRows(clis));
|
|
6286
|
+
if (cli2 === null) {
|
|
6287
|
+
prompts.cancel("Cancelled.");
|
|
6288
|
+
return { ok: false, reason: "cancelled" };
|
|
6289
|
+
}
|
|
6290
|
+
const ok = await prompts.confirm(
|
|
6291
|
+
[
|
|
6292
|
+
`Remove ${cli2}? This is the same as: agent-harness uninstall --cli ${cli2}`,
|
|
6293
|
+
...removalSummary(cli2, clis)
|
|
6294
|
+
].join("\n")
|
|
6295
|
+
);
|
|
6296
|
+
if (!ok) {
|
|
6297
|
+
prompts.cancel("Cancelled.");
|
|
6298
|
+
return { ok: false, reason: "cancelled" };
|
|
6299
|
+
}
|
|
6300
|
+
return { ok: true, options: { projectDir, cli: cli2 } };
|
|
6301
|
+
}
|
|
5405
6302
|
function defaultUninstallPrompts() {
|
|
5406
6303
|
return {
|
|
5407
6304
|
intro: (m) => ge(m),
|
|
5408
6305
|
outro: (m) => ye(m),
|
|
5409
6306
|
cancel: (m) => me(m),
|
|
5410
|
-
selectMode: async (
|
|
6307
|
+
selectMode: async (choices, message) => {
|
|
5411
6308
|
const r = await Ee({
|
|
5412
|
-
message
|
|
5413
|
-
options:
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
6309
|
+
message,
|
|
6310
|
+
options: choices.map((c3) => ({
|
|
6311
|
+
value: c3.value,
|
|
6312
|
+
label: c3.enabled ? c3.label : `${c3.label} [disabled]`,
|
|
6313
|
+
hint: c3.hint,
|
|
6314
|
+
...c3.enabled ? {} : { disabled: true }
|
|
6315
|
+
}))
|
|
6316
|
+
});
|
|
6317
|
+
return q(r) ? null : r;
|
|
6318
|
+
},
|
|
6319
|
+
selectCli: async (rows) => {
|
|
6320
|
+
const r = await Ee({
|
|
6321
|
+
message: 'Which CLI? (the last remaining CLI cannot be removed this way \u2014 use "Remove everything")',
|
|
6322
|
+
options: rows.map((x) => ({ value: x.value, label: x.label, hint: x.hint }))
|
|
5425
6323
|
});
|
|
5426
6324
|
return q(r) ? null : r;
|
|
5427
6325
|
},
|
|
5428
6326
|
selectAssets: async (rows) => {
|
|
5429
6327
|
if (rows.length === 0) return [];
|
|
5430
6328
|
const r = await ve({
|
|
5431
|
-
message: "
|
|
6329
|
+
message: "Assets to remove (Space toggle \xB7 Enter confirm \xB7 ESC cancel)",
|
|
5432
6330
|
options: rows.map((x) => ({ value: x.value, label: x.label, hint: x.hint })),
|
|
5433
6331
|
required: false
|
|
5434
6332
|
});
|
|
@@ -5437,13 +6335,25 @@ function defaultUninstallPrompts() {
|
|
|
5437
6335
|
confirm: async (summary) => {
|
|
5438
6336
|
const r = await ue({ message: `${summary}
|
|
5439
6337
|
|
|
5440
|
-
|
|
6338
|
+
Proceed?`, initialValue: false });
|
|
5441
6339
|
return q(r) ? null : r;
|
|
5442
6340
|
}
|
|
5443
6341
|
};
|
|
5444
6342
|
}
|
|
5445
6343
|
|
|
5446
6344
|
// src/commands/uninstall.ts
|
|
6345
|
+
function templateDirPath(projectDir, rel) {
|
|
6346
|
+
return join19(projectDir, rel.replace(/\/+$/, ""));
|
|
6347
|
+
}
|
|
6348
|
+
function movedAsideLine(projectDir, rel, backup) {
|
|
6349
|
+
return ` ${status.success(`${rel} moved aside \u2192 ${relative2(projectDir, backup)} (your own files there are kept)`)}`;
|
|
6350
|
+
}
|
|
6351
|
+
var MOVE_ASIDE_PREVIEW = (rel) => ` \u25CB move ${rel} aside \u2192 ${rel.replace(/\/+$/, "")}.backup-<time> (your own files there stay in the backup)`;
|
|
6352
|
+
function recordedTemplateDirs(log) {
|
|
6353
|
+
return [log.templates.claudeDir, log.templates.codexDir, log.templates.opencodeDir].filter(
|
|
6354
|
+
(d2) => d2 !== void 0
|
|
6355
|
+
);
|
|
6356
|
+
}
|
|
5447
6357
|
function uninstallAction(options, deps = {}) {
|
|
5448
6358
|
const log = deps.log ?? console.log;
|
|
5449
6359
|
const err = deps.err ?? console.error;
|
|
@@ -5451,6 +6361,7 @@ function uninstallAction(options, deps = {}) {
|
|
|
5451
6361
|
const spawn = deps.spawn ?? defaultSpawn2;
|
|
5452
6362
|
const rm = deps.rm ?? defaultRm;
|
|
5453
6363
|
const writeLog = deps.writeLog ?? writeInstallLog;
|
|
6364
|
+
const moveAside = deps.moveAside ?? backupDir;
|
|
5454
6365
|
const harnessRoot = (deps.resolveHarnessRoot ?? defaultHarnessRoot)();
|
|
5455
6366
|
const projectDir = resolve4(options.projectDir ?? process.cwd());
|
|
5456
6367
|
const installLog = readInstallLog(projectDir);
|
|
@@ -5460,6 +6371,13 @@ function uninstallAction(options, deps = {}) {
|
|
|
5460
6371
|
exit(1);
|
|
5461
6372
|
return;
|
|
5462
6373
|
}
|
|
6374
|
+
if (options.cli !== void 0) {
|
|
6375
|
+
removeCliAction(
|
|
6376
|
+
{ options, installLog, projectDir, harnessRoot },
|
|
6377
|
+
{ log, err, exit, rm, writeLog, moveAside }
|
|
6378
|
+
);
|
|
6379
|
+
return;
|
|
6380
|
+
}
|
|
5463
6381
|
const selectedIds = parseOnly(options.only);
|
|
5464
6382
|
if (options.only !== void 0 && selectedIds === null) {
|
|
5465
6383
|
err(status.failure(c.red("ERROR: --only \uC5D0 \uC790\uC0B0 id \uAC00 \uC5C6\uB2E4")));
|
|
@@ -5496,13 +6414,16 @@ function uninstallAction(options, deps = {}) {
|
|
|
5496
6414
|
const logSurvives = selectedIds !== null;
|
|
5497
6415
|
const { succeeded, failed, removedIds } = executeReverse(plan, log, logSurvives);
|
|
5498
6416
|
if (!keepTemplates) {
|
|
5499
|
-
const { rootClaudeMdKept, importStripped, external } = removeTemplates(
|
|
6417
|
+
const { rootClaudeMdKept, importStripped, external, moved } = removeTemplates(
|
|
5500
6418
|
installLog,
|
|
5501
6419
|
projectDir,
|
|
5502
|
-
rm,
|
|
6420
|
+
{ rm, moveAside },
|
|
5503
6421
|
harnessRoot
|
|
5504
6422
|
);
|
|
5505
|
-
log(
|
|
6423
|
+
for (const m of moved) log(movedAsideLine(projectDir, m.rel, m.backup));
|
|
6424
|
+
if (recordedTemplateDirs(installLog).length === 0) {
|
|
6425
|
+
log(` ${status.success("templates removed: (none)")}`);
|
|
6426
|
+
}
|
|
5506
6427
|
for (const line of externalRemovalLines(external)) log(line);
|
|
5507
6428
|
if (importStripped) {
|
|
5508
6429
|
log(` ${status.success("CLAUDE.md \u2014 harness @import removed (\uBCF8\uBB38 \uBCF4\uC874)")}`);
|
|
@@ -5601,7 +6522,7 @@ function settleLog(ctx, io) {
|
|
|
5601
6522
|
return true;
|
|
5602
6523
|
}
|
|
5603
6524
|
} else {
|
|
5604
|
-
io.rm(
|
|
6525
|
+
io.rm(join19(projectDir, INSTALL_LOG_DIR));
|
|
5605
6526
|
io.rm(legacyInstallLogPath(projectDir));
|
|
5606
6527
|
io.log(` ${status.success("install log removed")}`);
|
|
5607
6528
|
}
|
|
@@ -5619,7 +6540,11 @@ function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles, har
|
|
|
5619
6540
|
)
|
|
5620
6541
|
);
|
|
5621
6542
|
if (!keepTemplates) {
|
|
5622
|
-
|
|
6543
|
+
const dirs = recordedTemplateDirs(installLog);
|
|
6544
|
+
for (const rel of dirs) {
|
|
6545
|
+
if (existsSync18(join19(projectDir, rel))) lines.push(MOVE_ASIDE_PREVIEW(rel));
|
|
6546
|
+
}
|
|
6547
|
+
if (dirs.length === 0) lines.push(" \u25CB remove templates: (none)");
|
|
5623
6548
|
const rootMd = installLog.templates.rootClaudeMd;
|
|
5624
6549
|
if (rootMd) {
|
|
5625
6550
|
lines.push(
|
|
@@ -5629,7 +6554,7 @@ function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles, har
|
|
|
5629
6554
|
if (hasRootImport(projectDir)) {
|
|
5630
6555
|
lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
|
|
5631
6556
|
}
|
|
5632
|
-
lines.push(...previewExternalLines(installLog, projectDir, harnessRoot));
|
|
6557
|
+
lines.push(...previewExternalLines(installLog, projectDir, harnessRoot, dirs));
|
|
5633
6558
|
}
|
|
5634
6559
|
lines.push(...advisoryLines(plan, projectDir, rootFiles), "");
|
|
5635
6560
|
return lines;
|
|
@@ -5651,7 +6576,7 @@ function advisoryLines(plan, projectDir, rootFiles) {
|
|
|
5651
6576
|
return lines;
|
|
5652
6577
|
}
|
|
5653
6578
|
function rootFileAdvisoryLines(rootFiles, projectDir) {
|
|
5654
|
-
const present = rootFiles.filter((f) =>
|
|
6579
|
+
const present = rootFiles.filter((f) => existsSync18(join19(projectDir, f.path)));
|
|
5655
6580
|
if (present.length === 0) return [];
|
|
5656
6581
|
return [
|
|
5657
6582
|
"",
|
|
@@ -5664,6 +6589,172 @@ function rootFileAdvisoryLines(rootFiles, projectDir) {
|
|
|
5664
6589
|
])
|
|
5665
6590
|
];
|
|
5666
6591
|
}
|
|
6592
|
+
var TEMPLATE_DIR_FIELD = {
|
|
6593
|
+
claude: "claudeDir",
|
|
6594
|
+
codex: "codexDir",
|
|
6595
|
+
opencode: "opencodeDir"
|
|
6596
|
+
};
|
|
6597
|
+
function resolveCliTarget(ctx, io) {
|
|
6598
|
+
const { options, installLog } = ctx;
|
|
6599
|
+
if (options.only !== void 0 || options.keepTemplates) {
|
|
6600
|
+
io.err(status.failure(c.red("ERROR: --cli \uB294 --only \xB7 --keep-templates \uC640 \uD568\uAED8 \uC4F8 \uC218 \uC5C6\uB2E4")));
|
|
6601
|
+
io.err(
|
|
6602
|
+
c.dim(
|
|
6603
|
+
" CLI \uD558\uB098 \uC81C\uAC70: uninstall --cli <name> / \uC790\uC0B0 \uD558\uB098 \uC81C\uAC70: uninstall --only <id>"
|
|
6604
|
+
)
|
|
6605
|
+
);
|
|
6606
|
+
io.exit(1);
|
|
6607
|
+
return null;
|
|
6608
|
+
}
|
|
6609
|
+
const target = (options.cli ?? "").trim();
|
|
6610
|
+
if (!isCliBase(target)) {
|
|
6611
|
+
io.err(status.failure(c.red(`ERROR: Invalid --cli value: ${target || "(empty)"}`)));
|
|
6612
|
+
io.err(c.dim(` Must be one of: ${CLI_BASES.join(" | ")}`));
|
|
6613
|
+
io.exit(1);
|
|
6614
|
+
return null;
|
|
6615
|
+
}
|
|
6616
|
+
const installed = installedClis(installLog);
|
|
6617
|
+
if (!installed.includes(target)) {
|
|
6618
|
+
io.err(status.failure(c.red(`ERROR: ${target} is not installed in this project`)));
|
|
6619
|
+
io.err(c.dim(` installed: ${installed.join(", ") || "(none)"}`));
|
|
6620
|
+
io.exit(1);
|
|
6621
|
+
return null;
|
|
6622
|
+
}
|
|
6623
|
+
if (installed.length <= 1) {
|
|
6624
|
+
io.err(status.failure(c.red(`ERROR: ${target} \uB294 \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uB9C8\uC9C0\uB9C9 CLI \uB2E4`)));
|
|
6625
|
+
io.err(c.dim(" \uC804\uB7C9 \uC81C\uAC70\uB294 `agent-harness uninstall` \uC744 \uC4F4\uB2E4 (\uC790\uC0B0\xB7\uC124\uCE58 \uAE30\uB85D\uAE4C\uC9C0 \uD568\uAED8)"));
|
|
6626
|
+
io.exit(1);
|
|
6627
|
+
return null;
|
|
6628
|
+
}
|
|
6629
|
+
return target;
|
|
6630
|
+
}
|
|
6631
|
+
function underAny(path, owned) {
|
|
6632
|
+
return owned.some((o) => o.endsWith("/") ? path.startsWith(o) : path === o);
|
|
6633
|
+
}
|
|
6634
|
+
function planCliRemoval(target, remaining, installLog) {
|
|
6635
|
+
const { exclusive, sharedNowUnowned } = removableFor(target, remaining);
|
|
6636
|
+
const owned = [...exclusive, ...sharedNowUnowned];
|
|
6637
|
+
const recorded = owned.filter((o) => o.kind === "recorded").map((o) => o.path);
|
|
6638
|
+
const scopedFiles = (installLog.externalFiles ?? []).filter(
|
|
6639
|
+
(f) => underAny(f.path, recorded)
|
|
6640
|
+
);
|
|
6641
|
+
return {
|
|
6642
|
+
dirs: owned.filter((o) => o.kind === "dir").map((o) => o.path),
|
|
6643
|
+
scoped: { ...installLog, externalFiles: scopedFiles },
|
|
6644
|
+
anchor: owned.some((o) => o.kind === "anchor"),
|
|
6645
|
+
importBlock: owned.some((o) => o.kind === "import-block")
|
|
6646
|
+
};
|
|
6647
|
+
}
|
|
6648
|
+
function removeCliHeader(target, installed, remaining) {
|
|
6649
|
+
return [
|
|
6650
|
+
"",
|
|
6651
|
+
c.bold(`uzys-agent-harness \xB7 uninstall --cli ${target}`),
|
|
6652
|
+
"",
|
|
6653
|
+
c.dim(` installed: ${installed.join(", ")}`),
|
|
6654
|
+
c.dim(` removing: ${target}`),
|
|
6655
|
+
c.dim(` remaining: ${remaining.join(", ")}`),
|
|
6656
|
+
""
|
|
6657
|
+
];
|
|
6658
|
+
}
|
|
6659
|
+
function removeCliDryRunLines(plan, ctx, remaining) {
|
|
6660
|
+
const { installLog, projectDir, harnessRoot } = ctx;
|
|
6661
|
+
const lines = [c.yellow("[DRY RUN] CLI \uC81C\uAC70 \uBBF8\uB9AC\uBCF4\uAE30 (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
|
|
6662
|
+
for (const dir of plan.dirs) {
|
|
6663
|
+
if (existsSync18(join19(projectDir, dir))) lines.push(MOVE_ASIDE_PREVIEW(dir));
|
|
6664
|
+
}
|
|
6665
|
+
lines.push(...previewExternalLines(plan.scoped, projectDir, harnessRoot));
|
|
6666
|
+
const rootMd = plan.anchor ? installLog.templates.rootClaudeMd : void 0;
|
|
6667
|
+
if (rootMd && existsSync18(join19(projectDir, rootMd.path))) {
|
|
6668
|
+
lines.push(
|
|
6669
|
+
rootClaudeMdModified(installLog, projectDir) ? ` \u25CB keep ${rootMd.path} (modified since install \u2014 preserved)` : ` \u25CB remove ${rootMd.path}`
|
|
6670
|
+
);
|
|
6671
|
+
}
|
|
6672
|
+
if (plan.importBlock && hasRootImport(projectDir)) {
|
|
6673
|
+
lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
|
|
6674
|
+
}
|
|
6675
|
+
lines.push(` \u25CB install log updated (clis: ${remaining.join(", ")})`, "");
|
|
6676
|
+
return lines;
|
|
6677
|
+
}
|
|
6678
|
+
function removeHarnessAnchor(ctx, io) {
|
|
6679
|
+
const { installLog, projectDir } = ctx;
|
|
6680
|
+
const rootMd = installLog.templates.rootClaudeMd;
|
|
6681
|
+
if (!rootMd || !existsSync18(join19(projectDir, rootMd.path))) return false;
|
|
6682
|
+
if (rootClaudeMdModified(installLog, projectDir)) {
|
|
6683
|
+
io.log(
|
|
6684
|
+
` ${c.yellow("\u2298")} ${rootMd.path} kept \u2014 modified since install. Remove manually if intended.`
|
|
6685
|
+
);
|
|
6686
|
+
return false;
|
|
6687
|
+
}
|
|
6688
|
+
io.rm(join19(projectDir, rootMd.path));
|
|
6689
|
+
io.log(` ${status.success(`${rootMd.path} removed`)}`);
|
|
6690
|
+
return true;
|
|
6691
|
+
}
|
|
6692
|
+
function settleCliLog(ctx, target, remaining, recovered, anchorRemoved, removedDirs) {
|
|
6693
|
+
const { installLog } = ctx;
|
|
6694
|
+
const next = {
|
|
6695
|
+
...installLog,
|
|
6696
|
+
spec: {
|
|
6697
|
+
...installLog.spec,
|
|
6698
|
+
// `spec.cli` 는 손대지 않는다 — "마지막 install 이 요청한 CLI" 라는 뜻이고, 빼면 빈 배열이
|
|
6699
|
+
// 된다(재리뷰 BLOCKER-3: 외부 스킬 refresh 가 그 값으로 `--agent`·`--copy` 를 조립하다
|
|
6700
|
+
// 빈 배열이면 둘 다 빠져 Claude 몫이 조용히 빠졌다). 깔린 집합은 `clis` 하나가 말한다.
|
|
6701
|
+
clis: [...remaining]
|
|
6702
|
+
},
|
|
6703
|
+
templates: { ...installLog.templates }
|
|
6704
|
+
};
|
|
6705
|
+
const field = TEMPLATE_DIR_FIELD[target];
|
|
6706
|
+
if (field) delete next.templates[field];
|
|
6707
|
+
if (anchorRemoved) delete next.templates.rootClaudeMd;
|
|
6708
|
+
const survivors = (installLog.externalFiles ?? []).filter(
|
|
6709
|
+
(f) => !recovered.has(f.path) && !underAny(f.path, removedDirs)
|
|
6710
|
+
);
|
|
6711
|
+
if (survivors.length > 0) next.externalFiles = survivors;
|
|
6712
|
+
else delete next.externalFiles;
|
|
6713
|
+
return next;
|
|
6714
|
+
}
|
|
6715
|
+
function removeCliAction(ctx, io) {
|
|
6716
|
+
const target = resolveCliTarget(ctx, io);
|
|
6717
|
+
if (target === null) return;
|
|
6718
|
+
const { options, installLog, projectDir, harnessRoot } = ctx;
|
|
6719
|
+
const installed = installedClis(installLog);
|
|
6720
|
+
const remaining = installed.filter((cli2) => cli2 !== target);
|
|
6721
|
+
const plan = planCliRemoval(target, remaining, installLog);
|
|
6722
|
+
for (const line of removeCliHeader(target, installed, remaining)) io.log(line);
|
|
6723
|
+
if (options.dryRun) {
|
|
6724
|
+
for (const line of removeCliDryRunLines(plan, ctx, remaining)) io.log(line);
|
|
6725
|
+
io.exit(0);
|
|
6726
|
+
return;
|
|
6727
|
+
}
|
|
6728
|
+
for (const dir of plan.dirs) {
|
|
6729
|
+
if (!existsSync18(join19(projectDir, dir))) continue;
|
|
6730
|
+
const backup = io.moveAside(templateDirPath(projectDir, dir));
|
|
6731
|
+
if (backup) io.log(movedAsideLine(projectDir, dir, backup));
|
|
6732
|
+
}
|
|
6733
|
+
const external = removeExternalFiles(plan.scoped, projectDir, io.rm, harnessRoot);
|
|
6734
|
+
for (const line of externalRemovalLines(external)) io.log(line);
|
|
6735
|
+
if (plan.importBlock && stripRootImport(projectDir)) {
|
|
6736
|
+
io.log(` ${status.success("CLAUDE.md \u2014 harness @import removed (\uBCF8\uBB38 \uBCF4\uC874)")}`);
|
|
6737
|
+
}
|
|
6738
|
+
const anchorRemoved = plan.anchor ? removeHarnessAnchor(ctx, io) : false;
|
|
6739
|
+
const recovered = /* @__PURE__ */ new Set([...external.removed, ...external.stripped]);
|
|
6740
|
+
const next = settleCliLog(ctx, target, remaining, recovered, anchorRemoved, plan.dirs);
|
|
6741
|
+
try {
|
|
6742
|
+
io.writeLog(projectDir, next);
|
|
6743
|
+
io.log(` ${status.success(`install log updated (clis: ${remaining.join(", ")})`)}`);
|
|
6744
|
+
} catch (e2) {
|
|
6745
|
+
io.err(status.failure(c.red(`ERROR: install log \uAC31\uC2E0 \uC2E4\uD328 \u2014 ${installLogPath(projectDir)}`)));
|
|
6746
|
+
io.err(c.dim(` ${e2 instanceof Error ? e2.message : String(e2)}`));
|
|
6747
|
+
io.log("");
|
|
6748
|
+
io.log(
|
|
6749
|
+
c.yellow(`${target} \uB294 \uC81C\uAC70\uB410\uC73C\uB098 install log \uB97C \uAC31\uC2E0\uD558\uC9C0 \uBABB\uD588\uB2E4 (\uAE30\uB85D\uC774 \uC2E4\uC81C\uC640 \uB2E4\uB974\uB2E4)`)
|
|
6750
|
+
);
|
|
6751
|
+
io.exit(1);
|
|
6752
|
+
return;
|
|
6753
|
+
}
|
|
6754
|
+
io.log("");
|
|
6755
|
+
io.log(status.success(c.green(`${target} removed (remaining: ${remaining.join(", ")})`)));
|
|
6756
|
+
io.exit(0);
|
|
6757
|
+
}
|
|
5667
6758
|
function planReverse(assets, spawn) {
|
|
5668
6759
|
const reverseSteps = [];
|
|
5669
6760
|
const globalAdvisories = [];
|
|
@@ -5703,752 +6794,381 @@ function buildProjectReverseStep(asset, spawn) {
|
|
|
5703
6794
|
}
|
|
5704
6795
|
};
|
|
5705
6796
|
}
|
|
5706
|
-
case "npm": {
|
|
5707
|
-
const pkg = asset.detail.pkg ?? asset.id;
|
|
5708
|
-
return {
|
|
5709
|
-
assetId: asset.id,
|
|
5710
|
-
label: `npm uninstall --save-dev ${pkg}`,
|
|
5711
|
-
execute: () => {
|
|
5712
|
-
const r = spawn("npm", ["uninstall", "--save-dev", pkg]);
|
|
5713
|
-
return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
|
|
5714
|
-
}
|
|
5715
|
-
};
|
|
5716
|
-
}
|
|
5717
|
-
case "npx-run":
|
|
5718
|
-
return null;
|
|
5719
|
-
case "shell-script":
|
|
5720
|
-
return null;
|
|
5721
|
-
case "internal":
|
|
5722
|
-
return null;
|
|
5723
|
-
}
|
|
5724
|
-
}
|
|
5725
|
-
function unknownIds(installLog, selectedIds) {
|
|
5726
|
-
const known = new Set(installLog.assets.map((a) => a.id));
|
|
5727
|
-
return selectedIds.filter((id) => !known.has(id));
|
|
5728
|
-
}
|
|
5729
|
-
function parseOnly(only) {
|
|
5730
|
-
if (!only) return null;
|
|
5731
|
-
const ids = only.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
5732
|
-
return ids.length > 0 ? ids : null;
|
|
5733
|
-
}
|
|
5734
|
-
function buildGlobalAdvisoryCmd(asset) {
|
|
5735
|
-
switch (asset.method) {
|
|
5736
|
-
case "plugin": {
|
|
5737
|
-
const pid = asset.detail.pluginId ?? asset.id;
|
|
5738
|
-
return `claude plugin uninstall --scope user ${pid}`;
|
|
5739
|
-
}
|
|
5740
|
-
case "skill": {
|
|
5741
|
-
const s = asset.detail.source ?? asset.id;
|
|
5742
|
-
return `npx skills remove -g ${s}`;
|
|
5743
|
-
}
|
|
5744
|
-
case "npm": {
|
|
5745
|
-
const pkg = asset.detail.pkg ?? asset.id;
|
|
5746
|
-
return `npm uninstall -g ${pkg}`;
|
|
5747
|
-
}
|
|
5748
|
-
case "npx-run":
|
|
5749
|
-
case "shell-script":
|
|
5750
|
-
case "internal":
|
|
5751
|
-
return "(no standard reverse \u2014 manual)";
|
|
5752
|
-
}
|
|
5753
|
-
}
|
|
5754
|
-
function removeTemplates(log, projectDir, rm, harnessRoot) {
|
|
5755
|
-
rm(join16(projectDir, log.templates.claudeDir));
|
|
5756
|
-
if (log.templates.codexDir) rm(join16(projectDir, log.templates.codexDir));
|
|
5757
|
-
if (log.templates.opencodeDir) rm(join16(projectDir, log.templates.opencodeDir));
|
|
5758
|
-
const external = removeExternalFiles(log, projectDir, rm, harnessRoot);
|
|
5759
|
-
const importStripped = stripRootImport(projectDir);
|
|
5760
|
-
const rootMd = log.templates.rootClaudeMd;
|
|
5761
|
-
if (rootMd) {
|
|
5762
|
-
if (rootClaudeMdModified(log, projectDir))
|
|
5763
|
-
return { rootClaudeMdKept: true, importStripped, external };
|
|
5764
|
-
rm(join16(projectDir, rootMd.path));
|
|
5765
|
-
}
|
|
5766
|
-
return { rootClaudeMdKept: false, importStripped, external };
|
|
5767
|
-
}
|
|
5768
|
-
var AGENTS_MD = "AGENTS.md";
|
|
5769
|
-
function readAgentsMdTemplate(harnessRoot, log) {
|
|
5770
|
-
const flavor = log.templates.opencodeDir ? "opencode" : "codex";
|
|
5771
|
-
try {
|
|
5772
|
-
return readFileSync15(join16(harnessRoot, "templates", flavor, "AGENTS.md.template"), "utf8");
|
|
5773
|
-
} catch {
|
|
5774
|
-
return null;
|
|
5775
|
-
}
|
|
5776
|
-
}
|
|
5777
|
-
function agentsMdRemainder(current, harnessRoot, log) {
|
|
5778
|
-
const template = readAgentsMdTemplate(harnessRoot, log);
|
|
5779
|
-
if (template === null) return { ok: false };
|
|
5780
|
-
return { ok: true, remainder: stripHarnessFromAgentsMd({ existing: current, template }) };
|
|
5781
|
-
}
|
|
5782
|
-
function removeExternalFiles(log, projectDir, rm, harnessRoot) {
|
|
5783
|
-
const removed = [];
|
|
5784
|
-
const kept = [];
|
|
5785
|
-
const stripped = [];
|
|
5786
|
-
const unjudged = [];
|
|
5787
|
-
for (const { path, sha256 } of log.externalFiles ?? []) {
|
|
5788
|
-
const abs = join16(projectDir, path);
|
|
5789
|
-
if (!existsSync17(abs)) continue;
|
|
5790
|
-
if (!lstatSync2(abs).isFile()) continue;
|
|
5791
|
-
const current = readFileSync15(abs, "utf8");
|
|
5792
|
-
if (hashContent(current) !== sha256) {
|
|
5793
|
-
kept.push(path);
|
|
5794
|
-
continue;
|
|
5795
|
-
}
|
|
5796
|
-
if (path === AGENTS_MD) {
|
|
5797
|
-
const verdict = agentsMdRemainder(current, harnessRoot, log);
|
|
5798
|
-
if (!verdict.ok) {
|
|
5799
|
-
unjudged.push(path);
|
|
5800
|
-
continue;
|
|
5801
|
-
}
|
|
5802
|
-
if (verdict.remainder !== null) {
|
|
5803
|
-
try {
|
|
5804
|
-
writeFileSync7(abs, verdict.remainder, "utf8");
|
|
5805
|
-
stripped.push(path);
|
|
5806
|
-
} catch {
|
|
5807
|
-
unjudged.push(path);
|
|
5808
|
-
}
|
|
5809
|
-
continue;
|
|
5810
|
-
}
|
|
5811
|
-
}
|
|
5812
|
-
rm(abs);
|
|
5813
|
-
removed.push(path);
|
|
5814
|
-
}
|
|
5815
|
-
for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname5(join16(projectDir, path)));
|
|
5816
|
-
return { removed, kept, stripped, unjudged };
|
|
5817
|
-
}
|
|
5818
|
-
function pruneEmptyDirsUpward(projectDir, startDir) {
|
|
5819
|
-
let cursor = startDir;
|
|
5820
|
-
while (true) {
|
|
5821
|
-
const rel = relative2(projectDir, cursor);
|
|
5822
|
-
if (rel === "" || rel.startsWith(`..${sep2}`) || rel === "..") return;
|
|
5823
|
-
try {
|
|
5824
|
-
if (readdirSync6(cursor).length > 0) return;
|
|
5825
|
-
rmdirSync2(cursor);
|
|
5826
|
-
} catch {
|
|
5827
|
-
return;
|
|
5828
|
-
}
|
|
5829
|
-
cursor = dirname5(cursor);
|
|
5830
|
-
}
|
|
5831
|
-
}
|
|
5832
|
-
function stripRootImport(projectDir) {
|
|
5833
|
-
const current = readRootClaudeMd(projectDir);
|
|
5834
|
-
const stripped = current === null ? null : stripHarnessImport(current);
|
|
5835
|
-
if (stripped === null) return false;
|
|
5836
|
-
try {
|
|
5837
|
-
writeFileSync7(join16(projectDir, "CLAUDE.md"), stripped, "utf8");
|
|
5838
|
-
return true;
|
|
5839
|
-
} catch {
|
|
5840
|
-
return false;
|
|
5841
|
-
}
|
|
5842
|
-
}
|
|
5843
|
-
function previewExternalLines(installLog, projectDir, harnessRoot) {
|
|
5844
|
-
const lines = [];
|
|
5845
|
-
let removable = 0;
|
|
5846
|
-
for (const { path, sha256 } of installLog.externalFiles ?? []) {
|
|
5847
|
-
const abs = join16(projectDir, path);
|
|
5848
|
-
if (!existsSync17(abs)) continue;
|
|
5849
|
-
const current = readFileSync15(abs, "utf8");
|
|
5850
|
-
if (hashContent(current) !== sha256) {
|
|
5851
|
-
lines.push(` \u25CB keep ${path} (modified since install \u2014 preserved)`);
|
|
5852
|
-
continue;
|
|
5853
|
-
}
|
|
5854
|
-
if (path === AGENTS_MD) {
|
|
5855
|
-
const verdict = agentsMdRemainder(current, harnessRoot, installLog);
|
|
5856
|
-
if (!verdict.ok) {
|
|
5857
|
-
lines.push(` \u25CB keep ${path} (harness sections could not be separated \u2014 preserved)`);
|
|
5858
|
-
continue;
|
|
5859
|
-
}
|
|
5860
|
-
if (verdict.remainder !== null) {
|
|
5861
|
-
lines.push(` \u25CB strip harness sections from ${path} (\uBCF8\uBB38 \uBCF4\uC874)`);
|
|
5862
|
-
continue;
|
|
5863
|
-
}
|
|
5864
|
-
}
|
|
5865
|
-
removable += 1;
|
|
5866
|
-
}
|
|
5867
|
-
if (removable > 0) lines.unshift(` \u25CB remove ${removable} CLI output file(s)`);
|
|
5868
|
-
return lines;
|
|
5869
|
-
}
|
|
5870
|
-
function externalRemovalLines(external) {
|
|
5871
|
-
const lines = [];
|
|
5872
|
-
if (external.removed.length > 0) {
|
|
5873
|
-
lines.push(` ${status.success(`CLI outputs removed: ${external.removed.length} file(s)`)}`);
|
|
5874
|
-
}
|
|
5875
|
-
for (const path of external.stripped) {
|
|
5876
|
-
lines.push(` ${status.success(`${path} \u2014 harness sections removed (\uBCF8\uBB38 \uBCF4\uC874)`)}`);
|
|
5877
|
-
}
|
|
5878
|
-
for (const path of external.unjudged) {
|
|
5879
|
-
lines.push(
|
|
5880
|
-
` ${c.yellow("\u2298")} ${path} kept \u2014 harness sections could not be separated (template unreadable or write failed). Remove manually if intended.`
|
|
5881
|
-
);
|
|
5882
|
-
}
|
|
5883
|
-
for (const path of external.kept) {
|
|
5884
|
-
lines.push(
|
|
5885
|
-
` ${c.yellow("\u2298")} ${path} kept \u2014 modified since install. Remove manually if intended.`
|
|
5886
|
-
);
|
|
5887
|
-
}
|
|
5888
|
-
return lines;
|
|
5889
|
-
}
|
|
5890
|
-
function hasRootImport(projectDir) {
|
|
5891
|
-
const current = readRootClaudeMd(projectDir);
|
|
5892
|
-
return current !== null && stripHarnessImport(current) !== null;
|
|
5893
|
-
}
|
|
5894
|
-
function readRootClaudeMd(projectDir) {
|
|
5895
|
-
const path = join16(projectDir, "CLAUDE.md");
|
|
5896
|
-
if (!existsSync17(path)) return null;
|
|
5897
|
-
try {
|
|
5898
|
-
return readFileSync15(path, "utf8");
|
|
5899
|
-
} catch {
|
|
5900
|
-
return null;
|
|
5901
|
-
}
|
|
5902
|
-
}
|
|
5903
|
-
function rootClaudeMdModified(log, projectDir) {
|
|
5904
|
-
const rootMd = log.templates.rootClaudeMd;
|
|
5905
|
-
if (!rootMd) return false;
|
|
5906
|
-
const path = join16(projectDir, rootMd.path);
|
|
5907
|
-
if (!existsSync17(path)) return false;
|
|
5908
|
-
return hashContent(readFileSync15(path, "utf8")) !== rootMd.sha256;
|
|
5909
|
-
}
|
|
5910
|
-
function formatTemplateList(log) {
|
|
5911
|
-
const items = [log.templates.claudeDir];
|
|
5912
|
-
if (log.templates.codexDir) items.push(log.templates.codexDir);
|
|
5913
|
-
if (log.templates.opencodeDir) items.push(log.templates.opencodeDir);
|
|
5914
|
-
return items.join(", ");
|
|
5915
|
-
}
|
|
5916
|
-
function defaultSpawn2(cmd, args) {
|
|
5917
|
-
return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
|
|
5918
|
-
}
|
|
5919
|
-
function defaultRm(path) {
|
|
5920
|
-
if (existsSync17(path)) {
|
|
5921
|
-
rmSync(path, { recursive: true, force: true });
|
|
5922
|
-
}
|
|
5923
|
-
}
|
|
5924
|
-
function registerUninstallCommand(cli2) {
|
|
5925
|
-
cli2.command("uninstall", "Uninstall harness assets (log-based reverse)").option("--project-dir <path>", "[Project] Target project directory", {
|
|
5926
|
-
default: process.cwd()
|
|
5927
|
-
}).option("--dry-run", "[Mode] List reverse steps without executing").option(
|
|
5928
|
-
"--keep-templates",
|
|
5929
|
-
"[Mode] Keep `.claude/`, `.codex/`, `.opencode/` templates (remove only external assets)"
|
|
5930
|
-
).option(
|
|
5931
|
-
"--only <ids>",
|
|
5932
|
-
"[Scope] Remove only these assets (comma-separated ids from `agent-harness list`). Templates untouched"
|
|
5933
|
-
).option("--yes", "[Mode] Skip the interactive picker and remove everything (non-interactive)").action(async (options) => {
|
|
5934
|
-
await dispatchUninstall(options);
|
|
5935
|
-
});
|
|
5936
|
-
}
|
|
5937
|
-
function shouldRunInteractive(options, isTty) {
|
|
5938
|
-
if (!isTty) return false;
|
|
5939
|
-
if (options.yes || options.dryRun) return false;
|
|
5940
|
-
return options.only === void 0;
|
|
5941
|
-
}
|
|
5942
|
-
async function dispatchUninstall(options) {
|
|
5943
|
-
if (!shouldRunInteractive(options, Boolean(process.stdin.isTTY))) {
|
|
5944
|
-
uninstallAction(options);
|
|
5945
|
-
return;
|
|
5946
|
-
}
|
|
5947
|
-
const projectDir = resolve4(options.projectDir ?? process.cwd());
|
|
5948
|
-
const picked = await runInteractiveUninstall(projectDir);
|
|
5949
|
-
if (!picked.ok || !picked.options) {
|
|
5950
|
-
if (picked.reason === "no-log") {
|
|
5951
|
-
console.error(
|
|
5952
|
-
status.failure(c.red(`ERROR: install log not found at ${installLogPath(projectDir)}`))
|
|
5953
|
-
);
|
|
5954
|
-
console.error(c.dim(" Nothing installed here by agent-harness."));
|
|
5955
|
-
process.exit(1);
|
|
5956
|
-
}
|
|
5957
|
-
return;
|
|
5958
|
-
}
|
|
5959
|
-
uninstallAction(picked.options);
|
|
5960
|
-
}
|
|
5961
|
-
|
|
5962
|
-
// src/commands/update.ts
|
|
5963
|
-
init_esm_shims();
|
|
5964
|
-
import { resolve as resolve5 } from "path";
|
|
5965
|
-
|
|
5966
|
-
// src/state.ts
|
|
5967
|
-
init_esm_shims();
|
|
5968
|
-
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
|
|
5969
|
-
import { join as join17 } from "path";
|
|
5970
|
-
var META_FILE = ".claude/.installed-tracks";
|
|
5971
|
-
var LEGACY_SIGNATURES = [
|
|
5972
|
-
{ rule: "htmx.md", track: "ssr-htmx" },
|
|
5973
|
-
{ rule: "nextjs.md", track: "ssr-nextjs" },
|
|
5974
|
-
{ rule: "data-analysis.md", track: "data" },
|
|
5975
|
-
{ rule: "pyside6.md", track: "data" },
|
|
5976
|
-
{ rule: "cli-development.md", track: "tooling" }
|
|
5977
|
-
];
|
|
5978
|
-
function detectInstallState(projectDir) {
|
|
5979
|
-
const claudeDir = join17(projectDir, ".claude");
|
|
5980
|
-
const hasClaudeDir = existsSync18(claudeDir);
|
|
5981
|
-
if (hasClaudeDir) {
|
|
5982
|
-
const metaPath = join17(projectDir, META_FILE);
|
|
5983
|
-
if (existsSync18(metaPath)) {
|
|
5984
|
-
const tracks2 = readMetafile(metaPath);
|
|
5985
|
-
return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
|
|
6797
|
+
case "npm": {
|
|
6798
|
+
const pkg = asset.detail.pkg ?? asset.id;
|
|
6799
|
+
return {
|
|
6800
|
+
assetId: asset.id,
|
|
6801
|
+
label: `npm uninstall --save-dev ${pkg}`,
|
|
6802
|
+
execute: () => {
|
|
6803
|
+
const r = spawn("npm", ["uninstall", "--save-dev", pkg]);
|
|
6804
|
+
return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
|
|
6805
|
+
}
|
|
6806
|
+
};
|
|
5986
6807
|
}
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
state: "existing",
|
|
5994
|
-
tracks: tracksFromLog(log.spec.tracks),
|
|
5995
|
-
source: "install-log",
|
|
5996
|
-
hasClaudeDir: false
|
|
5997
|
-
};
|
|
6808
|
+
case "npx-run":
|
|
6809
|
+
return null;
|
|
6810
|
+
case "shell-script":
|
|
6811
|
+
return null;
|
|
6812
|
+
case "internal":
|
|
6813
|
+
return null;
|
|
5998
6814
|
}
|
|
5999
|
-
return { state: "new", tracks: [], source: "none", hasClaudeDir: false };
|
|
6000
6815
|
}
|
|
6001
|
-
function
|
|
6002
|
-
|
|
6816
|
+
function unknownIds(installLog, selectedIds) {
|
|
6817
|
+
const known = new Set(installLog.assets.map((a) => a.id));
|
|
6818
|
+
return selectedIds.filter((id) => !known.has(id));
|
|
6003
6819
|
}
|
|
6004
|
-
function
|
|
6005
|
-
|
|
6006
|
-
const
|
|
6007
|
-
|
|
6008
|
-
const trimmed = line.trim();
|
|
6009
|
-
if (isTrack(trimmed)) {
|
|
6010
|
-
seen.add(trimmed);
|
|
6011
|
-
}
|
|
6012
|
-
}
|
|
6013
|
-
return [...seen].sort();
|
|
6820
|
+
function parseOnly(only) {
|
|
6821
|
+
if (!only) return null;
|
|
6822
|
+
const ids = only.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6823
|
+
return ids.length > 0 ? ids : null;
|
|
6014
6824
|
}
|
|
6015
|
-
function
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
const found = /* @__PURE__ */ new Set();
|
|
6021
|
-
for (const sig of LEGACY_SIGNATURES) {
|
|
6022
|
-
if (existsSync18(join17(rulesDir, sig.rule))) {
|
|
6023
|
-
found.add(sig.track);
|
|
6825
|
+
function buildGlobalAdvisoryCmd(asset) {
|
|
6826
|
+
switch (asset.method) {
|
|
6827
|
+
case "plugin": {
|
|
6828
|
+
const pid = asset.detail.pluginId ?? asset.id;
|
|
6829
|
+
return `claude plugin uninstall --scope user ${pid}`;
|
|
6024
6830
|
}
|
|
6831
|
+
case "skill": {
|
|
6832
|
+
const s = asset.detail.source ?? asset.id;
|
|
6833
|
+
return `npx skills remove -g ${s}`;
|
|
6834
|
+
}
|
|
6835
|
+
case "npm": {
|
|
6836
|
+
const pkg = asset.detail.pkg ?? asset.id;
|
|
6837
|
+
return `npm uninstall -g ${pkg}`;
|
|
6838
|
+
}
|
|
6839
|
+
case "npx-run":
|
|
6840
|
+
case "shell-script":
|
|
6841
|
+
case "internal":
|
|
6842
|
+
return "(no standard reverse \u2014 manual)";
|
|
6025
6843
|
}
|
|
6026
|
-
return [...found].sort();
|
|
6027
|
-
}
|
|
6028
|
-
|
|
6029
|
-
// src/commands/update.ts
|
|
6030
|
-
function updateAction(options = {}, deps = {}) {
|
|
6031
|
-
const log = deps.log ?? console.log;
|
|
6032
|
-
const err = deps.err ?? console.error;
|
|
6033
|
-
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
6034
|
-
const detect = deps.detect ?? detectInstallState;
|
|
6035
|
-
const execute = deps.execute ?? executeSpec;
|
|
6036
|
-
const projectDir = resolve5(options.projectDir ?? process.cwd());
|
|
6037
|
-
const state = detect(projectDir);
|
|
6038
|
-
if (state.state === "new") {
|
|
6039
|
-
err(status.failure(c.red(`No harness install found at ${projectDir}`)));
|
|
6040
|
-
err(c.dim(" Run `agent-harness install --track <name>` first."));
|
|
6041
|
-
exit(1);
|
|
6042
|
-
return;
|
|
6043
|
-
}
|
|
6044
|
-
const rawOnly = options.only === void 0 ? [] : [options.only].flat().map((v2) => typeof v2 === "string" ? v2 : "(missing value)");
|
|
6045
|
-
const parsed = parseUpdateOnly(rawOnly);
|
|
6046
|
-
if (!parsed.ok) {
|
|
6047
|
-
err(status.failure(c.red(`Unknown --only value: ${parsed.invalid.join(", ")}`)));
|
|
6048
|
-
err(c.dim(` Valid groups: ${UPDATE_GROUPS.join(" | ")}`));
|
|
6049
|
-
exit(1);
|
|
6050
|
-
return;
|
|
6051
|
-
}
|
|
6052
|
-
log(c.dim(`Updating installed harness files in ${projectDir}`));
|
|
6053
|
-
execute(buildUpdateSpec(projectDir, state.tracks, parsed.groups), {
|
|
6054
|
-
log,
|
|
6055
|
-
err,
|
|
6056
|
-
exit,
|
|
6057
|
-
mode: "update"
|
|
6058
|
-
});
|
|
6059
6844
|
}
|
|
6060
|
-
function
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
default: process.cwd()
|
|
6066
|
-
}).option(
|
|
6067
|
-
"--only <group>",
|
|
6068
|
-
`[Scope] Update only this group (repeatable): ${UPDATE_GROUPS.join(" | ")}`
|
|
6069
|
-
).action((options) => {
|
|
6070
|
-
updateAction(options);
|
|
6845
|
+
function removeTemplates(log, projectDir, io, harnessRoot) {
|
|
6846
|
+
const { rm } = io;
|
|
6847
|
+
const moved = recordedTemplateDirs(log).flatMap((rel) => {
|
|
6848
|
+
const backup = io.moveAside(templateDirPath(projectDir, rel));
|
|
6849
|
+
return backup ? [{ rel, backup }] : [];
|
|
6071
6850
|
});
|
|
6851
|
+
const external = removeExternalFiles(log, projectDir, rm, harnessRoot);
|
|
6852
|
+
const importStripped = stripRootImport(projectDir);
|
|
6853
|
+
const rootMd = log.templates.rootClaudeMd;
|
|
6854
|
+
if (rootMd) {
|
|
6855
|
+
if (rootClaudeMdModified(log, projectDir))
|
|
6856
|
+
return { rootClaudeMdKept: true, importStripped, external, moved };
|
|
6857
|
+
rm(join19(projectDir, rootMd.path));
|
|
6858
|
+
}
|
|
6859
|
+
return { rootClaudeMdKept: false, importStripped, external, moved };
|
|
6072
6860
|
}
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
// src/router.ts
|
|
6081
|
-
init_esm_shims();
|
|
6082
|
-
function buildRouterChoices(state) {
|
|
6083
|
-
const detected = state.tracks.length > 0 ? state.tracks.join(", ") : "(none detected)";
|
|
6084
|
-
return [
|
|
6085
|
-
{
|
|
6086
|
-
value: "add",
|
|
6087
|
-
label: "Add a new Track",
|
|
6088
|
-
hint: `Current: ${detected}`,
|
|
6089
|
-
enabled: true
|
|
6090
|
-
},
|
|
6091
|
-
{
|
|
6092
|
-
value: "update",
|
|
6093
|
-
label: "Update policy files (auto-backup)",
|
|
6094
|
-
// v26.126.0 (R-3a) — skills 가 목록에 들어왔다. 이 문구가 곧 update 의 광고이고
|
|
6095
|
-
// (update 는 위저드로만 도달한다) 실동작과 어긋나면 그 자체로 거짓출하다.
|
|
6096
|
-
hint: "Refresh rules / agents / commands / hooks / skills \u2014 your edits are backed up",
|
|
6097
|
-
enabled: true
|
|
6098
|
-
},
|
|
6099
|
-
{
|
|
6100
|
-
value: "remove",
|
|
6101
|
-
label: "Remove a Track (unsupported)",
|
|
6102
|
-
hint: "Manual edit of .claude/ required \u2014 not automated",
|
|
6103
|
-
enabled: false
|
|
6104
|
-
},
|
|
6105
|
-
{
|
|
6106
|
-
value: "reinstall",
|
|
6107
|
-
label: "Reinstall (backs up current .claude/ first)",
|
|
6108
|
-
hint: "Use when state is corrupted",
|
|
6109
|
-
enabled: true
|
|
6110
|
-
},
|
|
6111
|
-
{
|
|
6112
|
-
value: "exit",
|
|
6113
|
-
label: "Exit",
|
|
6114
|
-
enabled: true
|
|
6115
|
-
}
|
|
6116
|
-
];
|
|
6117
|
-
}
|
|
6118
|
-
function summarizeState(state) {
|
|
6119
|
-
if (state.state === "new") {
|
|
6120
|
-
return "No prior install detected \u2014 new install flow.";
|
|
6861
|
+
var AGENTS_MD = "AGENTS.md";
|
|
6862
|
+
function readAgentsMdTemplate(harnessRoot, log) {
|
|
6863
|
+
const flavor = log.templates.opencodeDir ? "opencode" : "codex";
|
|
6864
|
+
try {
|
|
6865
|
+
return readFileSync16(join19(harnessRoot, "templates", flavor, "AGENTS.md.template"), "utf8");
|
|
6866
|
+
} catch {
|
|
6867
|
+
return null;
|
|
6121
6868
|
}
|
|
6122
|
-
const trackList = state.tracks.length > 0 ? state.tracks.join(", ") : "(no tracks resolved)";
|
|
6123
|
-
const sourceLabel = state.source === "metafile" ? "via .claude/.installed-tracks" : state.source === "legacy" ? "via legacy rules/*.md heuristic" : state.source === "install-log" ? "via .uzys-agent-harness/ install log" : "via no source";
|
|
6124
|
-
return `Existing install detected ${sourceLabel}. Tracks: ${trackList}.`;
|
|
6125
6869
|
}
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
var WIZARD = {
|
|
6131
|
-
TRACKS: { current: 1, total: WIZARD_TOTAL },
|
|
6132
|
-
CLI: { current: 2, total: WIZARD_TOTAL },
|
|
6133
|
-
TARGETS: { current: 3, total: WIZARD_TOTAL },
|
|
6134
|
-
SCOPE: { current: 4, total: WIZARD_TOTAL },
|
|
6135
|
-
CONFIRM: { current: 5, total: WIZARD_TOTAL },
|
|
6136
|
-
INSTALL: { current: 6, total: WIZARD_TOTAL }
|
|
6137
|
-
};
|
|
6138
|
-
function stepLabel(step, suffix) {
|
|
6139
|
-
if (!step) return suffix;
|
|
6140
|
-
return `Step ${step.current}/${step.total} \u2014 ${suffix}`;
|
|
6870
|
+
function agentsMdRemainder(current, harnessRoot, log) {
|
|
6871
|
+
const template = readAgentsMdTemplate(harnessRoot, log);
|
|
6872
|
+
if (template === null) return { ok: false };
|
|
6873
|
+
return { ok: true, remainder: stripHarnessFromAgentsMd({ existing: current, template }) };
|
|
6141
6874
|
}
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
label: "Track baseline \u2014 Agents & Skills (\uC5D0\uC774\uC804\uD2B8\xB7\uC2A4\uD0AC)",
|
|
6175
|
-
cats: [],
|
|
6176
|
-
baseline: ["agents", "skills"]
|
|
6177
|
-
},
|
|
6178
|
-
{ label: "Dev Core (Frontend \xB7 Backend \xB7 Data)", cats: ["frontend", "backend", "data"] },
|
|
6179
|
-
{ label: "Dev Tools (Security \xB7 Quality \xB7 Understanding)", cats: ["dev-tools", "understanding"] },
|
|
6180
|
-
{ label: "Business (PM \xB7 Executive \xB7 Documents)", cats: ["business"] },
|
|
6181
|
-
// v26.85.0 — 코드-퍼스트 비주얼/미디어 제작 (용도별 섹션, 전부 opt-in).
|
|
6182
|
-
{ label: "Visual & Media (Slides \xB7 Diagrams \xB7 Motion \xB7 Video)", cats: ["visual-media"] },
|
|
6183
|
-
{ label: "Workflow", cats: ["workflow"] }
|
|
6184
|
-
];
|
|
6185
|
-
var BASELINE_TITLES = {
|
|
6186
|
-
rules: "Rules \u2014 \uC0C1\uC2DC \uC0C1\uC8FC\uD558\uB294 \uB300\uC6D0\uCE59 (4 CLI \uC804\uBD80\uC5D0 \uC124\uCE58)",
|
|
6187
|
-
hooks: "Hooks \u2014 \uACB0\uC815\uB860\uC801 \uAC00\uB4DC (\uD574\uC81C\uD558\uBA74 settings.json \uBC30\uC120\uB3C4 \uD568\uAED8 \uBE60\uC9C4\uB2E4)",
|
|
6188
|
-
agents: "Agents \u2014 \uB3C5\uB9BD \uB808\uC778 (\uB9AC\uBDF0\xB7\uAC80\uC99D)",
|
|
6189
|
-
skills: "Skills \u2014 \uD544\uC694\uD560 \uB54C \uC5F4\uB9AC\uB294 \uC791\uC5C5 \uC808\uCC28"
|
|
6190
|
-
};
|
|
6191
|
-
function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Set(), baselineKinds = [], baselineTargets = []) {
|
|
6192
|
-
const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
|
|
6193
|
-
const groups = {};
|
|
6194
|
-
const flatItems = [];
|
|
6195
|
-
for (const kind of baselineKinds) {
|
|
6196
|
-
const items = baselineTargets.filter((t) => t.kind === kind).map((t) => ({
|
|
6197
|
-
value: t.id,
|
|
6198
|
-
label: ` ${t.name}${installedMark(t.id)}`,
|
|
6199
|
-
...t.hint ? { hint: t.hint } : {}
|
|
6200
|
-
}));
|
|
6201
|
-
if (items.length === 0) continue;
|
|
6202
|
-
groups[BASELINE_TITLES[kind]] = items;
|
|
6203
|
-
flatItems.push(...items);
|
|
6875
|
+
function removeExternalFiles(log, projectDir, rm, harnessRoot) {
|
|
6876
|
+
const removed = [];
|
|
6877
|
+
const kept = [];
|
|
6878
|
+
const stripped = [];
|
|
6879
|
+
const unjudged = [];
|
|
6880
|
+
for (const { path, sha256 } of log.externalFiles ?? []) {
|
|
6881
|
+
const abs = join19(projectDir, path);
|
|
6882
|
+
if (!existsSync18(abs)) continue;
|
|
6883
|
+
if (!lstatSync2(abs).isFile()) continue;
|
|
6884
|
+
const current = readFileSync16(abs, "utf8");
|
|
6885
|
+
if (hashContent(current) !== sha256) {
|
|
6886
|
+
kept.push(path);
|
|
6887
|
+
continue;
|
|
6888
|
+
}
|
|
6889
|
+
if (path === AGENTS_MD) {
|
|
6890
|
+
const verdict = agentsMdRemainder(current, harnessRoot, log);
|
|
6891
|
+
if (!verdict.ok) {
|
|
6892
|
+
unjudged.push(path);
|
|
6893
|
+
continue;
|
|
6894
|
+
}
|
|
6895
|
+
if (verdict.remainder !== null) {
|
|
6896
|
+
try {
|
|
6897
|
+
writeFileSync7(abs, verdict.remainder, "utf8");
|
|
6898
|
+
stripped.push(path);
|
|
6899
|
+
} catch {
|
|
6900
|
+
unjudged.push(path);
|
|
6901
|
+
}
|
|
6902
|
+
continue;
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
rm(abs);
|
|
6906
|
+
removed.push(path);
|
|
6204
6907
|
}
|
|
6205
|
-
for (const
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6908
|
+
for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname5(join19(projectDir, path)));
|
|
6909
|
+
return { removed, kept, stripped, unjudged };
|
|
6910
|
+
}
|
|
6911
|
+
function pruneEmptyDirsUpward(projectDir, startDir) {
|
|
6912
|
+
let cursor = startDir;
|
|
6913
|
+
while (true) {
|
|
6914
|
+
const rel = relative2(projectDir, cursor);
|
|
6915
|
+
if (rel === "" || rel.startsWith(`..${sep3}`) || rel === "..") return;
|
|
6916
|
+
try {
|
|
6917
|
+
if (readdirSync6(cursor).length > 0) return;
|
|
6918
|
+
rmdirSync2(cursor);
|
|
6919
|
+
} catch {
|
|
6920
|
+
return;
|
|
6214
6921
|
}
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6922
|
+
cursor = dirname5(cursor);
|
|
6923
|
+
}
|
|
6924
|
+
}
|
|
6925
|
+
function stripRootImport(projectDir) {
|
|
6926
|
+
const current = readRootClaudeMd(projectDir);
|
|
6927
|
+
const stripped = current === null ? null : stripHarnessImport(current);
|
|
6928
|
+
if (stripped === null) return false;
|
|
6929
|
+
try {
|
|
6930
|
+
writeFileSync7(join19(projectDir, "CLAUDE.md"), stripped, "utf8");
|
|
6931
|
+
return true;
|
|
6932
|
+
} catch {
|
|
6933
|
+
return false;
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
function previewExternalLines(installLog, projectDir, harnessRoot, movedDirs = []) {
|
|
6937
|
+
const lines = [];
|
|
6938
|
+
let removable = 0;
|
|
6939
|
+
for (const { path, sha256 } of installLog.externalFiles ?? []) {
|
|
6940
|
+
const abs = join19(projectDir, path);
|
|
6941
|
+
if (underAny(path, movedDirs)) continue;
|
|
6942
|
+
if (!existsSync18(abs)) continue;
|
|
6943
|
+
if (!lstatSync2(abs).isFile()) continue;
|
|
6944
|
+
const current = readFileSync16(abs, "utf8");
|
|
6945
|
+
if (hashContent(current) !== sha256) {
|
|
6946
|
+
lines.push(` \u25CB keep ${path} (modified since install \u2014 preserved)`);
|
|
6947
|
+
continue;
|
|
6227
6948
|
}
|
|
6228
|
-
if (
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6949
|
+
if (path === AGENTS_MD) {
|
|
6950
|
+
const verdict = agentsMdRemainder(current, harnessRoot, installLog);
|
|
6951
|
+
if (!verdict.ok) {
|
|
6952
|
+
lines.push(` \u25CB keep ${path} (harness sections could not be separated \u2014 preserved)`);
|
|
6953
|
+
continue;
|
|
6954
|
+
}
|
|
6955
|
+
if (verdict.remainder !== null) {
|
|
6956
|
+
lines.push(` \u25CB strip harness sections from ${path} (\uBCF8\uBB38 \uBCF4\uC874)`);
|
|
6957
|
+
continue;
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6960
|
+
removable += 1;
|
|
6233
6961
|
}
|
|
6234
|
-
|
|
6962
|
+
if (removable > 0) lines.unshift(` \u25CB remove ${removable} CLI output file(s)`);
|
|
6963
|
+
return lines;
|
|
6235
6964
|
}
|
|
6236
|
-
function
|
|
6237
|
-
const
|
|
6238
|
-
|
|
6239
|
-
|
|
6965
|
+
function externalRemovalLines(external) {
|
|
6966
|
+
const lines = [];
|
|
6967
|
+
if (external.removed.length > 0) {
|
|
6968
|
+
lines.push(` ${status.success(`CLI outputs removed: ${external.removed.length} file(s)`)}`);
|
|
6240
6969
|
}
|
|
6241
|
-
const
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6970
|
+
for (const path of external.stripped) {
|
|
6971
|
+
lines.push(` ${status.success(`${path} \u2014 harness sections removed (\uBCF8\uBB38 \uBCF4\uC874)`)}`);
|
|
6972
|
+
}
|
|
6973
|
+
for (const path of external.unjudged) {
|
|
6974
|
+
lines.push(
|
|
6975
|
+
` ${c.yellow("\u2298")} ${path} kept \u2014 harness sections could not be separated (template unreadable or write failed). Remove manually if intended.`
|
|
6976
|
+
);
|
|
6977
|
+
}
|
|
6978
|
+
for (const path of external.kept) {
|
|
6979
|
+
lines.push(
|
|
6980
|
+
` ${c.yellow("\u2298")} ${path} kept \u2014 modified since install. Remove manually if intended.`
|
|
6246
6981
|
);
|
|
6247
6982
|
}
|
|
6983
|
+
return lines;
|
|
6248
6984
|
}
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6985
|
+
function hasRootImport(projectDir) {
|
|
6986
|
+
const current = readRootClaudeMd(projectDir);
|
|
6987
|
+
return current !== null && stripHarnessImport(current) !== null;
|
|
6988
|
+
}
|
|
6989
|
+
function readRootClaudeMd(projectDir) {
|
|
6990
|
+
const path = join19(projectDir, "CLAUDE.md");
|
|
6991
|
+
if (!existsSync18(path)) return null;
|
|
6992
|
+
try {
|
|
6993
|
+
return readFileSync16(path, "utf8");
|
|
6994
|
+
} catch {
|
|
6995
|
+
return null;
|
|
6996
|
+
}
|
|
6997
|
+
}
|
|
6998
|
+
function rootClaudeMdModified(log, projectDir) {
|
|
6999
|
+
const rootMd = log.templates.rootClaudeMd;
|
|
7000
|
+
if (!rootMd) return false;
|
|
7001
|
+
const path = join19(projectDir, rootMd.path);
|
|
7002
|
+
if (!existsSync18(path)) return false;
|
|
7003
|
+
return hashContent(readFileSync16(path, "utf8")) !== rootMd.sha256;
|
|
7004
|
+
}
|
|
7005
|
+
function defaultSpawn2(cmd, args) {
|
|
7006
|
+
return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
|
|
7007
|
+
}
|
|
7008
|
+
function defaultRm(path) {
|
|
7009
|
+
if (existsSync18(path)) {
|
|
7010
|
+
rmSync(path, { recursive: true, force: true });
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
function registerUninstallCommand(cli2) {
|
|
7014
|
+
cli2.command("uninstall", "Uninstall harness assets (log-based reverse)").option("--project-dir <path>", "[Project] Target project directory", {
|
|
7015
|
+
default: process.cwd()
|
|
7016
|
+
}).option("--dry-run", "[Mode] List reverse steps without executing").option(
|
|
7017
|
+
"--keep-templates",
|
|
7018
|
+
"[Mode] Keep `.claude/`, `.codex/`, `.opencode/` templates (remove only external assets)"
|
|
7019
|
+
).option(
|
|
7020
|
+
"--only <ids>",
|
|
7021
|
+
"[Scope] Remove only these assets (comma-separated ids from `agent-harness list`). Templates untouched"
|
|
7022
|
+
).option(
|
|
7023
|
+
"--cli <name>",
|
|
7024
|
+
`[Scope] Remove one CLI only (${CLI_BASES.join(" | ")}). Shared files stay until the last user leaves`
|
|
7025
|
+
).option("--yes", "[Mode] Skip the interactive picker and remove everything (non-interactive)").action(async (options) => {
|
|
7026
|
+
await dispatchUninstall(options);
|
|
7027
|
+
});
|
|
7028
|
+
}
|
|
7029
|
+
function shouldRunInteractive(options, isTty) {
|
|
7030
|
+
if (!isTty) return false;
|
|
7031
|
+
if (options.yes || options.dryRun) return false;
|
|
7032
|
+
return options.only === void 0 && options.cli === void 0;
|
|
7033
|
+
}
|
|
7034
|
+
async function dispatchUninstall(options) {
|
|
7035
|
+
if (!shouldRunInteractive(options, Boolean(process.stdin.isTTY))) {
|
|
7036
|
+
uninstallAction(options);
|
|
7037
|
+
return;
|
|
7038
|
+
}
|
|
7039
|
+
await runUninstallScreen(resolve4(options.projectDir ?? process.cwd()));
|
|
7040
|
+
}
|
|
7041
|
+
async function runUninstallScreen(projectDir, opts = {}) {
|
|
7042
|
+
const picked = await runInteractiveUninstall(projectDir, opts);
|
|
7043
|
+
if (!picked.ok || !picked.options) {
|
|
7044
|
+
if (picked.reason === "no-log") {
|
|
7045
|
+
console.error(
|
|
7046
|
+
status.failure(c.red(`ERROR: install log not found at ${installLogPath(projectDir)}`))
|
|
7047
|
+
);
|
|
7048
|
+
console.error(c.dim(" Nothing installed here by agent-harness."));
|
|
7049
|
+
process.exit(1);
|
|
7050
|
+
}
|
|
7051
|
+
return;
|
|
7052
|
+
}
|
|
7053
|
+
uninstallAction(picked.options);
|
|
6253
7054
|
}
|
|
6254
|
-
var defaultPrompts = {
|
|
6255
|
-
intro: (msg) => ge(msg),
|
|
6256
|
-
outro: (msg) => ye(msg),
|
|
6257
|
-
cancel: (msg) => me(msg),
|
|
6258
|
-
selectTracks: async (initial, step) => {
|
|
6259
|
-
const result = await ve({
|
|
6260
|
-
message: stepLabel(step, "Select Track(s)"),
|
|
6261
|
-
options: TRACKS.map((t) => ({ value: t, label: TRACK_LABELS[t] })),
|
|
6262
|
-
...initial ? { initialValues: initial } : {},
|
|
6263
|
-
maxItems: viewportItems(11),
|
|
6264
|
-
required: true
|
|
6265
|
-
});
|
|
6266
|
-
return q(result) ? null : result;
|
|
6267
|
-
},
|
|
6268
|
-
selectCli: async (initial, step) => {
|
|
6269
|
-
const initialValues = initial && initial.length > 0 ? [...initial] : ["claude"];
|
|
6270
|
-
const result = await ve({
|
|
6271
|
-
message: stepLabel(step, "Target CLI(s)"),
|
|
6272
|
-
options: [
|
|
6273
|
-
{ value: "claude", label: CLI_BASE_LABELS.claude },
|
|
6274
|
-
{ value: "codex", label: CLI_BASE_LABELS.codex },
|
|
6275
|
-
{ value: "opencode", label: CLI_BASE_LABELS.opencode },
|
|
6276
|
-
{ value: "antigravity", label: CLI_BASE_LABELS.antigravity }
|
|
6277
|
-
],
|
|
6278
|
-
initialValues,
|
|
6279
|
-
required: true
|
|
6280
|
-
});
|
|
6281
|
-
if (q(result)) return null;
|
|
6282
|
-
return [...result].sort(
|
|
6283
|
-
(a, b2) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b2]
|
|
6284
|
-
);
|
|
6285
|
-
},
|
|
6286
|
-
selectAction: async (state) => {
|
|
6287
|
-
const result = await Ee({
|
|
6288
|
-
message: summarizeState(state),
|
|
6289
|
-
options: buildRouterChoices(state).map((c3) => {
|
|
6290
|
-
const label = c3.enabled ? c3.label : `${c3.label} [disabled]`;
|
|
6291
|
-
return {
|
|
6292
|
-
value: c3.value,
|
|
6293
|
-
label,
|
|
6294
|
-
...c3.hint ? { hint: c3.hint } : {},
|
|
6295
|
-
...c3.enabled ? {} : { disabled: true }
|
|
6296
|
-
};
|
|
6297
|
-
})
|
|
6298
|
-
});
|
|
6299
|
-
return q(result) ? null : result;
|
|
6300
|
-
},
|
|
6301
|
-
/**
|
|
6302
|
-
* v26.64.0 (ADR-020) — Installation scope select. Default Project (D16 — no global write).
|
|
6303
|
-
* Global 은 사용자 명시 opt-in 시에만.
|
|
6304
|
-
*/
|
|
6305
|
-
selectScope: async (initial = "project", step) => {
|
|
6306
|
-
const result = await Ee({
|
|
6307
|
-
message: stepLabel(step, "Installation scope"),
|
|
6308
|
-
initialValue: initial,
|
|
6309
|
-
options: [
|
|
6310
|
-
{
|
|
6311
|
-
value: "project",
|
|
6312
|
-
label: "Project",
|
|
6313
|
-
hint: "Install in current directory (committed with your project)"
|
|
6314
|
-
},
|
|
6315
|
-
{
|
|
6316
|
-
value: "global",
|
|
6317
|
-
label: "Global",
|
|
6318
|
-
hint: "Write to ~/.claude/, ~/.codex/, npm -g (shared across all projects)"
|
|
6319
|
-
}
|
|
6320
|
-
]
|
|
6321
|
-
});
|
|
6322
|
-
return q(result) ? null : result;
|
|
6323
|
-
},
|
|
6324
|
-
selectUpdateGroups: async () => {
|
|
6325
|
-
const result = await ve({
|
|
6326
|
-
message: "What to update (space = toggle \xB7 your edits are always backed up)",
|
|
6327
|
-
options: [
|
|
6328
|
-
{ value: "skills", label: "Skills", hint: "installed bundled skills \u2192 latest" },
|
|
6329
|
-
{
|
|
6330
|
-
value: "new-skills",
|
|
6331
|
-
label: "New skills",
|
|
6332
|
-
hint: "bundled skills added since your install"
|
|
6333
|
-
},
|
|
6334
|
-
{
|
|
6335
|
-
value: "rules",
|
|
6336
|
-
label: "Rules \xB7 agents \xB7 commands",
|
|
6337
|
-
hint: "your edited files \u2192 *.backup-<time>"
|
|
6338
|
-
},
|
|
6339
|
-
{
|
|
6340
|
-
value: "anchor",
|
|
6341
|
-
label: "CLAUDE-uzys-harness.md anchor",
|
|
6342
|
-
hint: "your edited anchor \u2192 *.backup-<time>"
|
|
6343
|
-
},
|
|
6344
|
-
{
|
|
6345
|
-
value: "hooks",
|
|
6346
|
-
label: "Hooks \xB7 settings.json",
|
|
6347
|
-
hint: "stale hook refs cleaned"
|
|
6348
|
-
},
|
|
6349
|
-
{
|
|
6350
|
-
value: "external",
|
|
6351
|
-
label: "Codex / OpenCode / Antigravity + external skills",
|
|
6352
|
-
hint: "rendered artifacts + npx skills"
|
|
6353
|
-
}
|
|
6354
|
-
],
|
|
6355
|
-
initialValues: [...UPDATE_GROUPS],
|
|
6356
|
-
required: true
|
|
6357
|
-
});
|
|
6358
|
-
return q(result) ? null : result;
|
|
6359
|
-
},
|
|
6360
|
-
confirmInstall: async (summary) => {
|
|
6361
|
-
const result = await ue({
|
|
6362
|
-
message: `${summary}
|
|
6363
7055
|
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
installedSet,
|
|
6389
|
-
page.baseline ?? [],
|
|
6390
|
-
baselineTargets
|
|
6391
|
-
);
|
|
6392
|
-
if (flatItems.length === 0) {
|
|
6393
|
-
pageIdx += dir;
|
|
6394
|
-
if (pageIdx < 0) {
|
|
6395
|
-
aborted = true;
|
|
6396
|
-
break;
|
|
6397
|
-
}
|
|
6398
|
-
continue;
|
|
6399
|
-
}
|
|
6400
|
-
const selectedNow = flatItems.filter((it3) => collected.has(it3.value)).map((it3) => it3.value);
|
|
6401
|
-
const pageDefault = flatItems.filter((it3) => initialSet.has(it3.value)).length;
|
|
6402
|
-
const totalSelected = collected.size;
|
|
6403
|
-
const message = [
|
|
6404
|
-
`Step ${step.current}/${step.total} \xB7 Page ${pageIdx + 1}/${pages.length} \xB7 ${page.label}`,
|
|
6405
|
-
recapLine ? ` ${recapLine}` : "",
|
|
6406
|
-
` Selected so far: ${totalSelected} items \xB7 This page default \u2713 ${pageDefault}/${flatItems.length}`,
|
|
6407
|
-
// 마커의 뜻을 화면에서 바로 알려준다 — 체크 해제를 제거로 오해하는 것이 이 화면의
|
|
6408
|
-
// 원래 문제였으므로, 마커가 보일 때는 제거 경로를 같은 자리에서 말한다.
|
|
6409
|
-
installedSet.size > 0 ? " \u25CF installed = \uC774\uBBF8 \uC124\uCE58\uB428 \xB7 \uCCB4\uD06C \uD574\uC81C\uD574\uB3C4 \uC81C\uAC70\uB418\uC9C0 \uC54A\uB294\uB2E4 (\uC81C\uAC70: agent-harness uninstall)" : "",
|
|
6410
|
-
" Space toggle \xB7 Enter \u2192 next \xB7 ESC \u2192 prev"
|
|
6411
|
-
].filter(Boolean).join("\n");
|
|
6412
|
-
const groupOpts = {
|
|
6413
|
-
message,
|
|
6414
|
-
options: groups,
|
|
6415
|
-
initialValues: selectedNow,
|
|
6416
|
-
required: false,
|
|
6417
|
-
selectableGroups: false
|
|
6418
|
-
};
|
|
6419
|
-
const result = await pe(groupOpts);
|
|
6420
|
-
if (q(result)) {
|
|
6421
|
-
if (pageIdx === 0) {
|
|
6422
|
-
aborted = true;
|
|
6423
|
-
break;
|
|
6424
|
-
}
|
|
6425
|
-
dir = -1;
|
|
6426
|
-
pageIdx--;
|
|
6427
|
-
continue;
|
|
6428
|
-
}
|
|
6429
|
-
for (const it3 of flatItems) collected.delete(it3.value);
|
|
6430
|
-
for (const v2 of result) collected.add(v2);
|
|
6431
|
-
dir = 1;
|
|
6432
|
-
pageIdx++;
|
|
6433
|
-
}
|
|
6434
|
-
if (!aborted) {
|
|
6435
|
-
resultIds = [...collected];
|
|
6436
|
-
}
|
|
6437
|
-
} finally {
|
|
6438
|
-
process.stdout.write("\x1B[?1049l");
|
|
7056
|
+
// src/commands/update.ts
|
|
7057
|
+
init_esm_shims();
|
|
7058
|
+
import { resolve as resolve5 } from "path";
|
|
7059
|
+
|
|
7060
|
+
// src/state.ts
|
|
7061
|
+
init_esm_shims();
|
|
7062
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
|
|
7063
|
+
import { join as join20 } from "path";
|
|
7064
|
+
var META_FILE = ".claude/.installed-tracks";
|
|
7065
|
+
var LEGACY_SIGNATURES = [
|
|
7066
|
+
{ rule: "htmx.md", track: "ssr-htmx" },
|
|
7067
|
+
{ rule: "nextjs.md", track: "ssr-nextjs" },
|
|
7068
|
+
{ rule: "data-analysis.md", track: "data" },
|
|
7069
|
+
{ rule: "pyside6.md", track: "data" },
|
|
7070
|
+
{ rule: "cli-development.md", track: "tooling" }
|
|
7071
|
+
];
|
|
7072
|
+
function detectInstallState(projectDir) {
|
|
7073
|
+
const claudeDir = join20(projectDir, ".claude");
|
|
7074
|
+
const hasClaudeDir = existsSync19(claudeDir);
|
|
7075
|
+
if (hasClaudeDir) {
|
|
7076
|
+
const metaPath = join20(projectDir, META_FILE);
|
|
7077
|
+
if (existsSync19(metaPath)) {
|
|
7078
|
+
const tracks2 = readMetafile(metaPath);
|
|
7079
|
+
return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
|
|
6439
7080
|
}
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
7081
|
+
const tracks = inferFromLegacySignatures(projectDir);
|
|
7082
|
+
return { state: "existing", tracks, source: "legacy", hasClaudeDir: true };
|
|
7083
|
+
}
|
|
7084
|
+
const log = readInstallLog(projectDir);
|
|
7085
|
+
if (log) {
|
|
7086
|
+
return {
|
|
7087
|
+
state: "existing",
|
|
7088
|
+
tracks: tracksFromLog(log.spec.tracks),
|
|
7089
|
+
source: "install-log",
|
|
7090
|
+
hasClaudeDir: false
|
|
7091
|
+
};
|
|
7092
|
+
}
|
|
7093
|
+
return { state: "new", tracks: [], source: "none", hasClaudeDir: false };
|
|
7094
|
+
}
|
|
7095
|
+
function tracksFromLog(tracks) {
|
|
7096
|
+
return [...new Set(tracks.filter(isTrack))].sort();
|
|
7097
|
+
}
|
|
7098
|
+
function readMetafile(path) {
|
|
7099
|
+
const raw = readFileSync17(path, "utf8");
|
|
7100
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7101
|
+
for (const line of raw.split(/\s+/)) {
|
|
7102
|
+
const trimmed = line.trim();
|
|
7103
|
+
if (isTrack(trimmed)) {
|
|
7104
|
+
seen.add(trimmed);
|
|
6446
7105
|
}
|
|
6447
|
-
return resultIds;
|
|
6448
7106
|
}
|
|
6449
|
-
|
|
7107
|
+
return [...seen].sort();
|
|
7108
|
+
}
|
|
7109
|
+
function inferFromLegacySignatures(projectDir) {
|
|
7110
|
+
const rulesDir = join20(projectDir, ".claude/rules");
|
|
7111
|
+
if (!existsSync19(rulesDir)) {
|
|
7112
|
+
return [];
|
|
7113
|
+
}
|
|
7114
|
+
const found = /* @__PURE__ */ new Set();
|
|
7115
|
+
for (const sig of LEGACY_SIGNATURES) {
|
|
7116
|
+
if (existsSync19(join20(rulesDir, sig.rule))) {
|
|
7117
|
+
found.add(sig.track);
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
7120
|
+
return [...found].sort();
|
|
7121
|
+
}
|
|
7122
|
+
|
|
7123
|
+
// src/commands/update.ts
|
|
7124
|
+
function updateAction(options = {}, deps = {}) {
|
|
7125
|
+
const log = deps.log ?? console.log;
|
|
7126
|
+
const err = deps.err ?? console.error;
|
|
7127
|
+
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
7128
|
+
const detect = deps.detect ?? detectInstallState;
|
|
7129
|
+
const execute = deps.execute ?? executeSpec;
|
|
7130
|
+
const projectDir = resolve5(options.projectDir ?? process.cwd());
|
|
7131
|
+
const state = detect(projectDir);
|
|
7132
|
+
if (state.state === "new") {
|
|
7133
|
+
err(status.failure(c.red(`No harness install found at ${projectDir}`)));
|
|
7134
|
+
err(c.dim(" Run `agent-harness install --track <name>` first."));
|
|
7135
|
+
exit(1);
|
|
7136
|
+
return;
|
|
7137
|
+
}
|
|
7138
|
+
const rawOnly = options.only === void 0 ? [] : [options.only].flat().map((v2) => typeof v2 === "string" ? v2 : "(missing value)");
|
|
7139
|
+
const parsed = parseUpdateOnly(rawOnly);
|
|
7140
|
+
if (!parsed.ok) {
|
|
7141
|
+
err(status.failure(c.red(`Unknown --only value: ${parsed.invalid.join(", ")}`)));
|
|
7142
|
+
err(c.dim(` Valid groups: ${UPDATE_GROUPS.join(" | ")}`));
|
|
7143
|
+
exit(1);
|
|
7144
|
+
return;
|
|
7145
|
+
}
|
|
7146
|
+
log(c.dim(`Updating installed harness files in ${projectDir}`));
|
|
7147
|
+
execute(buildUpdateSpec(projectDir, state.tracks, parsed.groups), {
|
|
7148
|
+
log,
|
|
7149
|
+
err,
|
|
7150
|
+
exit,
|
|
7151
|
+
mode: "update"
|
|
7152
|
+
});
|
|
7153
|
+
}
|
|
7154
|
+
function registerUpdateCommand(cli2) {
|
|
7155
|
+
cli2.command(
|
|
7156
|
+
"update",
|
|
7157
|
+
"Refresh installed policy files (rules / agents / commands / hooks / skills)"
|
|
7158
|
+
).option("--project-dir <path>", "[Project] Target project directory", {
|
|
7159
|
+
default: process.cwd()
|
|
7160
|
+
}).option(
|
|
7161
|
+
"--only <group>",
|
|
7162
|
+
`[Scope] Update only this group (repeatable): ${UPDATE_GROUPS.join(" | ")}`
|
|
7163
|
+
).action((options) => {
|
|
7164
|
+
updateAction(options);
|
|
7165
|
+
});
|
|
7166
|
+
}
|
|
6450
7167
|
|
|
6451
7168
|
// src/interactive.ts
|
|
7169
|
+
init_esm_shims();
|
|
7170
|
+
import { existsSync as existsSync20 } from "fs";
|
|
7171
|
+
import { join as join21 } from "path";
|
|
6452
7172
|
function splitInstallTargets(targets) {
|
|
6453
7173
|
const optionKeys = [];
|
|
6454
7174
|
const assetIds = [];
|
|
@@ -6489,6 +7209,59 @@ function initialTargetSelection(tracks, installedProjectAssetIds) {
|
|
|
6489
7209
|
const baseline = listBaselineTargets({ tracks }).map((t) => t.id);
|
|
6490
7210
|
return [...assets, ...baseline];
|
|
6491
7211
|
}
|
|
7212
|
+
function lockTracks(picked, installed) {
|
|
7213
|
+
return [.../* @__PURE__ */ new Set([...installed, ...picked])].sort();
|
|
7214
|
+
}
|
|
7215
|
+
function lockClis(picked, installed) {
|
|
7216
|
+
return [.../* @__PURE__ */ new Set([...installed, ...picked])].sort(
|
|
7217
|
+
(a, b2) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b2]
|
|
7218
|
+
);
|
|
7219
|
+
}
|
|
7220
|
+
var BUNDLED_SKILLS = new Set(INTERNAL_BUNDLED_SKILL_IDS);
|
|
7221
|
+
function classifyUpdateIntent(log, confirmed, legacyTracks = []) {
|
|
7222
|
+
const recordTracks = log ? log.spec.tracks : legacyTracks;
|
|
7223
|
+
const recordClis = recordedClis(log);
|
|
7224
|
+
if (!sameSet(confirmed.tracks, recordTracks)) return "add";
|
|
7225
|
+
if (!sameSet(confirmed.cli, recordClis)) return "add";
|
|
7226
|
+
const recorded = log?.assets ?? [];
|
|
7227
|
+
const covered = coveredByRecord(log, legacyTracks);
|
|
7228
|
+
const picked = new Set(confirmed.assetIds);
|
|
7229
|
+
if (confirmed.assetIds.some((id) => !covered.has(id))) return "add";
|
|
7230
|
+
if (recorded.some((a) => a.scope !== "global" && !picked.has(a.id))) return "add";
|
|
7231
|
+
if (!sameSet(confirmed.baselineExclude, log?.spec.baselineExclude ?? [])) return "add";
|
|
7232
|
+
if (!sameSet(confirmed.skillExclude, log?.spec.skillExclude ?? [])) return "add";
|
|
7233
|
+
return "refresh";
|
|
7234
|
+
}
|
|
7235
|
+
function recordedClis(log) {
|
|
7236
|
+
return log ? [...installedClis(log)] : ["claude"];
|
|
7237
|
+
}
|
|
7238
|
+
function coveredByRecord(log, legacyTracks) {
|
|
7239
|
+
const recordTracks = (log ? log.spec.tracks : legacyTracks).filter(isTrack);
|
|
7240
|
+
const clis = recordedClis(log);
|
|
7241
|
+
const covered = new Set((log?.assets ?? []).map((a) => a.id));
|
|
7242
|
+
for (const id of recommendedExternalAssets(recordTracks)) {
|
|
7243
|
+
const asset = EXTERNAL_ASSETS.find((a) => a.id === id);
|
|
7244
|
+
if (!asset || asset.method.kind === "internal" || !assetReachesCli(asset, clis)) {
|
|
7245
|
+
covered.add(id);
|
|
7246
|
+
}
|
|
7247
|
+
}
|
|
7248
|
+
return covered;
|
|
7249
|
+
}
|
|
7250
|
+
function sameSet(a, b2) {
|
|
7251
|
+
const left = new Set(a);
|
|
7252
|
+
const right = new Set(b2);
|
|
7253
|
+
return left.size === right.size && [...left].every((x) => right.has(x));
|
|
7254
|
+
}
|
|
7255
|
+
function updateInitialSelection(tracks, installedProjectAssetIds, log, legacyTracks = []) {
|
|
7256
|
+
const covered = coveredByRecord(log, legacyTracks);
|
|
7257
|
+
const recordTracks = (log ? log.spec.tracks : legacyTracks).filter(isTrack);
|
|
7258
|
+
const excluded = /* @__PURE__ */ new Set([
|
|
7259
|
+
...log?.spec.baselineExclude ?? [],
|
|
7260
|
+
...(log?.spec.skillExclude ?? []).map((id) => `asset:${id}`),
|
|
7261
|
+
...recommendedExternalAssets(recordTracks).filter((id) => !covered.has(id)).map((id) => `asset:${id}`)
|
|
7262
|
+
]);
|
|
7263
|
+
return initialTargetSelection(tracks, installedProjectAssetIds).filter((id) => !excluded.has(id));
|
|
7264
|
+
}
|
|
6492
7265
|
async function runInteractive(projectDir, deps = {}) {
|
|
6493
7266
|
const prompts = deps.prompts ?? defaultPrompts;
|
|
6494
7267
|
const detect = deps.detect ?? detectInstallState;
|
|
@@ -6503,10 +7276,10 @@ async function runInteractive(projectDir, deps = {}) {
|
|
|
6503
7276
|
prompts.intro("uzys-agent-harness installer");
|
|
6504
7277
|
const state = detect(projectDir);
|
|
6505
7278
|
const installed = (deps.readInstalled ?? installedTargetState)(projectDir);
|
|
6506
|
-
let initialTracks;
|
|
6507
|
-
let mode = "fresh";
|
|
6508
7279
|
if (state.state === "existing") {
|
|
6509
|
-
const
|
|
7280
|
+
const log = readInstallLog(projectDir);
|
|
7281
|
+
const record = buildInstallRecordView(state, log, existsSync20(join21(projectDir, ".claude")));
|
|
7282
|
+
const action = await prompts.selectAction(state, record);
|
|
6510
7283
|
if (action === null) {
|
|
6511
7284
|
prompts.cancel("Cancelled.");
|
|
6512
7285
|
return { ok: false, reason: "cancelled" };
|
|
@@ -6515,34 +7288,14 @@ async function runInteractive(projectDir, deps = {}) {
|
|
|
6515
7288
|
prompts.outro("Exiting without changes.");
|
|
6516
7289
|
return { ok: false, reason: "exit" };
|
|
6517
7290
|
}
|
|
6518
|
-
if (action === "
|
|
6519
|
-
|
|
6520
|
-
return { ok: false, reason: "disabled-action" };
|
|
6521
|
-
}
|
|
6522
|
-
if (action === "update") {
|
|
6523
|
-
mode = "update";
|
|
6524
|
-
const groups = prompts.selectUpdateGroups ? await prompts.selectUpdateGroups() : null;
|
|
6525
|
-
if (groups === null && prompts.selectUpdateGroups) {
|
|
6526
|
-
prompts.cancel("Cancelled.");
|
|
6527
|
-
return { ok: false, reason: "cancelled" };
|
|
6528
|
-
}
|
|
6529
|
-
const spec = buildUpdateSpec(projectDir, state.tracks, groups ?? void 0);
|
|
6530
|
-
const scopeLine = spec.updateOnly ? `UPDATE ${spec.updateOnly.join(" \xB7 ")} only (untouched: ${UPDATE_GROUPS.filter((g) => !spec.updateOnly?.includes(g)).join(", ")}):` : "UPDATE installed harness files:";
|
|
6531
|
-
const confirmed = await prompts.confirmInstall(`${scopeLine}
|
|
6532
|
-
${formatSummary(spec)}`);
|
|
6533
|
-
if (!confirmed) {
|
|
6534
|
-
prompts.outro("Cancelled.");
|
|
6535
|
-
return { ok: false, reason: "cancelled" };
|
|
6536
|
-
}
|
|
6537
|
-
prompts.outro("Running update mode...");
|
|
6538
|
-
return { ok: true, mode: "update", spec };
|
|
7291
|
+
if (action === "uninstall") {
|
|
7292
|
+
return { ok: true, uninstall: true };
|
|
6539
7293
|
}
|
|
6540
|
-
if (
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
} else if (action === "reinstall") {
|
|
6544
|
-
mode = "reinstall";
|
|
7294
|
+
if (record.repair !== null) {
|
|
7295
|
+
prompts.cancel(`.claude/ is missing \u2014 repair first: ${record.repair}`);
|
|
7296
|
+
return { ok: false, reason: "disabled-action", message: record.repair };
|
|
6545
7297
|
}
|
|
7298
|
+
return runUpdateFlow({ projectDir, state, log, prompts, installed });
|
|
6546
7299
|
}
|
|
6547
7300
|
let step = "tracks";
|
|
6548
7301
|
let tracks = null;
|
|
@@ -6551,7 +7304,7 @@ ${formatSummary(spec)}`);
|
|
|
6551
7304
|
let scope = "project";
|
|
6552
7305
|
while (true) {
|
|
6553
7306
|
if (step === "tracks") {
|
|
6554
|
-
const result = await prompts.selectTracks(tracks ??
|
|
7307
|
+
const result = await prompts.selectTracks(tracks ?? void 0, WIZARD.TRACKS);
|
|
6555
7308
|
if (result === null) {
|
|
6556
7309
|
prompts.cancel("Cancelled.");
|
|
6557
7310
|
return { ok: false, reason: "cancelled" };
|
|
@@ -6605,7 +7358,7 @@ ${formatSummary(spec)}`);
|
|
|
6605
7358
|
projectDir,
|
|
6606
7359
|
...userOverride ? { userOverride } : {},
|
|
6607
7360
|
// 해제 목록을 안 넘기면 이 화면이 **제외 유무와 문자열이 완전히 같아진다** — 같은 화면이
|
|
6608
|
-
// 외부 자산 제거는 `-
|
|
7361
|
+
// 외부 자산 제거는 `-Unchecked by you:` 로 이미 보고하므로, 없음은 "아무것도 안 빠졌다"로
|
|
6609
7362
|
// 읽힌다. 상주 비용을 줄이려고 20개를 푼 사용자가 그대로인 숫자를 보게 된다.
|
|
6610
7363
|
...baselineExclude.length > 0 ? { baselineExclude } : {}
|
|
6611
7364
|
})}
|
|
@@ -6632,10 +7385,169 @@ ${summary}`
|
|
|
6632
7385
|
...baselineExclude.length > 0 ? { baselineExclude } : {}
|
|
6633
7386
|
};
|
|
6634
7387
|
prompts.outro(stepLabel(WIZARD.INSTALL, "Installing..."));
|
|
6635
|
-
return { ok: true, mode, spec };
|
|
7388
|
+
return { ok: true, mode: "fresh", spec };
|
|
7389
|
+
}
|
|
7390
|
+
}
|
|
7391
|
+
}
|
|
7392
|
+
async function runUpdateFlow(ctx) {
|
|
7393
|
+
const { projectDir, state, log, prompts, installed } = ctx;
|
|
7394
|
+
const lockedTracks = lockTracks(log ? log.spec.tracks.filter(isTrack) : [], state.tracks);
|
|
7395
|
+
const lockedClis = installedClis(log);
|
|
7396
|
+
const scope = log?.scope ?? "project";
|
|
7397
|
+
let step = "tracks";
|
|
7398
|
+
let tracks = null;
|
|
7399
|
+
let cli2 = null;
|
|
7400
|
+
let targetSelections = null;
|
|
7401
|
+
while (true) {
|
|
7402
|
+
if (step === "tracks") {
|
|
7403
|
+
const result = await prompts.selectTracks(
|
|
7404
|
+
tracks ?? lockedTracks,
|
|
7405
|
+
UPDATE_WIZARD.TRACKS,
|
|
7406
|
+
lockedTracks
|
|
7407
|
+
);
|
|
7408
|
+
if (result === null) {
|
|
7409
|
+
prompts.cancel("Cancelled.");
|
|
7410
|
+
return { ok: false, reason: "cancelled" };
|
|
7411
|
+
}
|
|
7412
|
+
const next = lockTracks(result, lockedTracks);
|
|
7413
|
+
if (tracks !== null && !tracksEqual(tracks, next)) targetSelections = null;
|
|
7414
|
+
tracks = next;
|
|
7415
|
+
step = "cli";
|
|
7416
|
+
} else if (step === "cli") {
|
|
7417
|
+
const initial = cli2 ?? (lockedClis.length > 0 ? [...lockedClis] : ["claude"]);
|
|
7418
|
+
const result = await prompts.selectCli(initial, UPDATE_WIZARD.CLI, lockedClis);
|
|
7419
|
+
if (result === null) {
|
|
7420
|
+
step = "tracks";
|
|
7421
|
+
continue;
|
|
7422
|
+
}
|
|
7423
|
+
cli2 = lockClis(result, lockedClis);
|
|
7424
|
+
step = "targets";
|
|
7425
|
+
} else if (step === "targets") {
|
|
7426
|
+
const current = tracks ?? lockedTracks;
|
|
7427
|
+
const initial = targetSelections !== null ? [...targetSelections] : updateInitialSelection(current, installed.projectScoped, log, state.tracks);
|
|
7428
|
+
const result = await prompts.selectInstallTargets(initial, UPDATE_WIZARD.TARGETS, {
|
|
7429
|
+
tracks: current,
|
|
7430
|
+
cli: cli2 ?? [...lockedClis],
|
|
7431
|
+
installed: installed.installed.map((id) => `asset:${id}`)
|
|
7432
|
+
});
|
|
7433
|
+
if (result === null) {
|
|
7434
|
+
step = "cli";
|
|
7435
|
+
continue;
|
|
7436
|
+
}
|
|
7437
|
+
targetSelections = result;
|
|
7438
|
+
step = "confirm";
|
|
7439
|
+
} else {
|
|
7440
|
+
const outcome = await confirmUpdate({
|
|
7441
|
+
projectDir,
|
|
7442
|
+
state,
|
|
7443
|
+
log,
|
|
7444
|
+
prompts,
|
|
7445
|
+
scope,
|
|
7446
|
+
lockedClis,
|
|
7447
|
+
// biome-ignore lint/style/noNonNullAssertion: confirm 도달 = 이전 step 완료 보장
|
|
7448
|
+
tracks,
|
|
7449
|
+
// biome-ignore lint/style/noNonNullAssertion: same as above
|
|
7450
|
+
cli: cli2,
|
|
7451
|
+
targetSelections: targetSelections ?? []
|
|
7452
|
+
});
|
|
7453
|
+
if (outcome === "back") {
|
|
7454
|
+
step = "targets";
|
|
7455
|
+
continue;
|
|
7456
|
+
}
|
|
7457
|
+
return outcome;
|
|
6636
7458
|
}
|
|
6637
7459
|
}
|
|
6638
7460
|
}
|
|
7461
|
+
async function confirmUpdate(input) {
|
|
7462
|
+
const { projectDir, state, log, prompts, scope, tracks, cli: cli2 } = input;
|
|
7463
|
+
const { assetIds, baselineIds } = splitInstallTargets(input.targetSelections);
|
|
7464
|
+
const userOverride = computeUserOverride(tracks, assetIds);
|
|
7465
|
+
const baselineExclude = baselineExcludeFrom(listBaselineTargets({ tracks }), baselineIds);
|
|
7466
|
+
const skillExclude = (userOverride?.forceExclude ?? []).filter((id) => BUNDLED_SKILLS.has(id));
|
|
7467
|
+
const intent = classifyUpdateIntent(
|
|
7468
|
+
log,
|
|
7469
|
+
{ tracks, cli: cli2, assetIds, baselineExclude, skillExclude },
|
|
7470
|
+
state.tracks
|
|
7471
|
+
);
|
|
7472
|
+
const header = stepLabel(UPDATE_WIZARD.CONFIRM, "Confirm");
|
|
7473
|
+
const scopeLine = ` SCOPE ${scope === "global" ? "Global" : "Project"} (${log ? "from your install record \u2014 not changed here" : "no install record \u2014 default"})`;
|
|
7474
|
+
if (intent === "refresh") {
|
|
7475
|
+
const groups = prompts.selectUpdateGroups ? await prompts.selectUpdateGroups() : void 0;
|
|
7476
|
+
if (groups === null) return "back";
|
|
7477
|
+
const spec2 = buildUpdateSpec(projectDir, state.tracks, groups);
|
|
7478
|
+
const only = spec2.updateOnly ?? [];
|
|
7479
|
+
const summary2 = [
|
|
7480
|
+
header,
|
|
7481
|
+
only.length > 0 ? `UPDATE ${only.join(" \xB7 ")} only (untouched: ${UPDATE_GROUPS.filter((g) => !only.includes(g)).join(", ")}):` : "UPDATE installed harness files:",
|
|
7482
|
+
// 요약은 Step 3 에서 체크된 것을 센다 — spec(추천 전체)으로 세면 체크 안 된 채 둔 자산까지
|
|
7483
|
+
// "selected" 로 적는데 refresh 는 그것을 깔지 않는다(리뷰 B2). 실행 spec 은 그대로다.
|
|
7484
|
+
annotate(formatSummary({ ...spec2, ...userOverride ? { userOverride } : {} }), {
|
|
7485
|
+
Tracks: "no change",
|
|
7486
|
+
CLI: "no change",
|
|
7487
|
+
Assets: "no change"
|
|
7488
|
+
}),
|
|
7489
|
+
scopeLine,
|
|
7490
|
+
` RUNS AS agent-harness update${only.map((g) => ` --only ${g}`).join("")}`
|
|
7491
|
+
].join("\n");
|
|
7492
|
+
const confirmed2 = await prompts.confirmInstall(summary2);
|
|
7493
|
+
if (confirmed2 === null) return "back";
|
|
7494
|
+
if (!confirmed2) {
|
|
7495
|
+
prompts.outro("Cancelled.");
|
|
7496
|
+
return { ok: false, reason: "cancelled" };
|
|
7497
|
+
}
|
|
7498
|
+
prompts.outro("Running update mode...");
|
|
7499
|
+
return { ok: true, mode: "update", spec: spec2 };
|
|
7500
|
+
}
|
|
7501
|
+
const without = [...userOverride?.forceExclude ?? [], ...baselineExclude];
|
|
7502
|
+
const options = {
|
|
7503
|
+
track: [...tracks],
|
|
7504
|
+
cli: [...cli2],
|
|
7505
|
+
scope,
|
|
7506
|
+
projectDir,
|
|
7507
|
+
...userOverride && userOverride.forceInclude.length > 0 ? { with: [...userOverride.forceInclude] } : {},
|
|
7508
|
+
...without.length > 0 ? { without } : {}
|
|
7509
|
+
};
|
|
7510
|
+
const spec = installSpecFromOptions(options, [...cli2], () => {
|
|
7511
|
+
});
|
|
7512
|
+
const recordTracks = new Set(log ? log.spec.tracks : state.tracks);
|
|
7513
|
+
const addedTracks = tracks.filter((t) => !recordTracks.has(t));
|
|
7514
|
+
const addedClis = cli2.filter((c3) => !input.lockedClis.includes(c3));
|
|
7515
|
+
const covered = coveredByRecord(log, state.tracks);
|
|
7516
|
+
const addedAssets = assetIds.filter((id) => !covered.has(id));
|
|
7517
|
+
const locked = input.lockedClis.join(", ");
|
|
7518
|
+
const summary = [
|
|
7519
|
+
header,
|
|
7520
|
+
annotate(formatSummary(spec), {
|
|
7521
|
+
Tracks: addedTracks.length > 0 ? `+${addedTracks.join(", +")}` : "no change",
|
|
7522
|
+
CLI: [
|
|
7523
|
+
addedClis.length > 0 ? `+${addedClis.join(", +")}` : "",
|
|
7524
|
+
locked ? `${locked} ${input.lockedClis.length > 1 ? "stay" : "stays"} \u2014 locked` : ""
|
|
7525
|
+
].filter(Boolean).join(" \xB7 "),
|
|
7526
|
+
Assets: addedAssets.length > 0 ? `+${addedAssets.length} new: ${addedAssets.join(", ")}` : "no new assets"
|
|
7527
|
+
}),
|
|
7528
|
+
scopeLine,
|
|
7529
|
+
` RUNS AS ${installCommandLine(options)}`,
|
|
7530
|
+
" (adds the new track's files and the new CLI's files \xB7 refreshes what is installed \xB7 edited files \u2192 *.backup-<time>)",
|
|
7531
|
+
" (retired-file cleanup runs on your next `agent-harness update`)"
|
|
7532
|
+
].join("\n");
|
|
7533
|
+
const confirmed = await prompts.confirmInstall(summary);
|
|
7534
|
+
if (confirmed === null) return "back";
|
|
7535
|
+
if (!confirmed) {
|
|
7536
|
+
prompts.outro("Cancelled by user.");
|
|
7537
|
+
return { ok: false, reason: "cancelled" };
|
|
7538
|
+
}
|
|
7539
|
+
prompts.outro(stepLabel(UPDATE_WIZARD.RUN, "Installing..."));
|
|
7540
|
+
return { ok: true, mode: "add", spec };
|
|
7541
|
+
}
|
|
7542
|
+
function annotate(summary, notes) {
|
|
7543
|
+
return summary.split("\n").map((line) => {
|
|
7544
|
+
const key = Object.keys(notes).find(
|
|
7545
|
+
(k2) => line.startsWith(`${k2}:`)
|
|
7546
|
+
);
|
|
7547
|
+
const note = key ? notes[key] : "";
|
|
7548
|
+
return note ? `${line.padEnd(40)} (${note})` : line;
|
|
7549
|
+
}).join("\n");
|
|
7550
|
+
}
|
|
6639
7551
|
function tracksEqual(a, b2) {
|
|
6640
7552
|
if (a.length !== b2.length) return false;
|
|
6641
7553
|
const sortedA = [...a].sort();
|
|
@@ -6688,7 +7600,7 @@ function formatSummary(spec) {
|
|
|
6688
7600
|
lines.push(` +User added: ${spec.userOverride.forceInclude.join(", ")}`);
|
|
6689
7601
|
}
|
|
6690
7602
|
if (spec.userOverride.forceExclude.length > 0) {
|
|
6691
|
-
lines.push(` -
|
|
7603
|
+
lines.push(` -Unchecked by you: ${spec.userOverride.forceExclude.join(", ")}`);
|
|
6692
7604
|
}
|
|
6693
7605
|
}
|
|
6694
7606
|
if (baselineExcluded.size > 0) {
|
|
@@ -6708,6 +7620,11 @@ async function defaultAction(deps = {}) {
|
|
|
6708
7620
|
const run = deps.run ?? ((cwd) => runInteractive(cwd));
|
|
6709
7621
|
const execute = deps.execute ?? executeSpec;
|
|
6710
7622
|
const result = await run(process.cwd());
|
|
7623
|
+
if (result.ok && result.uninstall) {
|
|
7624
|
+
const uninstall = deps.uninstall ?? ((cwd) => runUninstallScreen(cwd, { embedded: true }));
|
|
7625
|
+
await uninstall(process.cwd());
|
|
7626
|
+
return;
|
|
7627
|
+
}
|
|
6711
7628
|
if (!result.ok) {
|
|
6712
7629
|
if (result.message) {
|
|
6713
7630
|
err(result.message);
|