@uzysjung/agent-harness 26.160.0 → 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 +2334 -1314
- 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();
|
|
@@ -1210,8 +1123,200 @@ function mergeAgentsMd(params) {
|
|
|
1210
1123
|
}
|
|
1211
1124
|
return bodies.size === 0 ? rendered : assemble(rendered, names, bodies);
|
|
1212
1125
|
}
|
|
1126
|
+
function installerLines(body, m) {
|
|
1127
|
+
if (body === null) return [];
|
|
1128
|
+
const at2 = blockRange(body, m);
|
|
1129
|
+
return at2 === null ? [...body] : [...body.slice(0, at2[0]), ...body.slice(at2[1] + 1)];
|
|
1130
|
+
}
|
|
1131
|
+
function compact(lines) {
|
|
1132
|
+
return lines.map((l) => l.replace(/\r$/, "").trimEnd()).filter((l) => l.trim() !== "");
|
|
1133
|
+
}
|
|
1134
|
+
function trimBlankEdges(lines) {
|
|
1135
|
+
const out = trimTrailingBlanks(lines);
|
|
1136
|
+
while (out.length > 0 && (out[0] ?? "").trim() === "") out.shift();
|
|
1137
|
+
return out;
|
|
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
|
+
}
|
|
1144
|
+
function stripHarnessFromAgentsMd(params) {
|
|
1145
|
+
const { existing, template } = params;
|
|
1146
|
+
const lines = existing.split("\n");
|
|
1147
|
+
if (blockRange(lines, ANCHOR_BLOCK) === null && blockRange(lines, SKILLS_BLOCK) === null) {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
const names = sectionNames(template);
|
|
1151
|
+
const context = installerLines(sectionBody(existing, names, CONTEXT_SECTION), SKILLS_BLOCK);
|
|
1152
|
+
const rules = installerLines(sectionBody(existing, names, RULES_SECTION), ANCHOR_BLOCK);
|
|
1153
|
+
const scaffold = compact(renderFillScaffold("agents-md").split("\n"));
|
|
1154
|
+
const contextFilled = compact(context).join("\n") !== scaffold.join("\n");
|
|
1155
|
+
const rulesFilled = compact(rules).length > 0;
|
|
1156
|
+
if (!contextFilled && !rulesFilled) return null;
|
|
1157
|
+
const out = [];
|
|
1158
|
+
const title = (lines[0] ?? "").replace(/\r$/, "");
|
|
1159
|
+
if (/^# /.test(title)) out.push(title, "");
|
|
1160
|
+
out.push(`## ${CONTEXT_SECTION}`, "", ...trimBlankEdges(context), "");
|
|
1161
|
+
if (rulesFilled) out.push(`## ${RULES_SECTION}`, "", ...trimBlankEdges(rules), "");
|
|
1162
|
+
return out.join("\n");
|
|
1163
|
+
}
|
|
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
|
+
}
|
|
1213
1317
|
|
|
1214
1318
|
// src/codex/agents-md.ts
|
|
1319
|
+
init_esm_shims();
|
|
1215
1320
|
function renameSlashes(text) {
|
|
1216
1321
|
return text.replaceAll("/uzys:", "/uzys-");
|
|
1217
1322
|
}
|
|
@@ -1223,8 +1328,8 @@ function renderAgentsMd(params) {
|
|
|
1223
1328
|
|
|
1224
1329
|
// src/codex/skills.ts
|
|
1225
1330
|
init_esm_shims();
|
|
1226
|
-
import { chmodSync, existsSync as
|
|
1227
|
-
import { join as
|
|
1331
|
+
import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
1332
|
+
import { join as join5 } from "path";
|
|
1228
1333
|
function renderBundledSkill(source) {
|
|
1229
1334
|
const trimmed = source.trimEnd();
|
|
1230
1335
|
const lines = trimmed.split(/\r?\n/);
|
|
@@ -1256,14 +1361,14 @@ function writeBundledSkillDirs(params) {
|
|
|
1256
1361
|
const { harnessRoot, projectDir, skillIds, writer } = params;
|
|
1257
1362
|
const written = [];
|
|
1258
1363
|
for (const id of skillIds) {
|
|
1259
|
-
const srcDir =
|
|
1260
|
-
const srcSkill =
|
|
1261
|
-
if (!
|
|
1364
|
+
const srcDir = join5(harnessRoot, "templates/skills", id);
|
|
1365
|
+
const srcSkill = join5(srcDir, "SKILL.md");
|
|
1366
|
+
if (!existsSync4(srcSkill)) {
|
|
1262
1367
|
continue;
|
|
1263
1368
|
}
|
|
1264
|
-
const targetDir =
|
|
1265
|
-
const targetSkill =
|
|
1266
|
-
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")))) {
|
|
1267
1372
|
continue;
|
|
1268
1373
|
}
|
|
1269
1374
|
written.push(targetSkill);
|
|
@@ -1271,10 +1376,10 @@ function writeBundledSkillDirs(params) {
|
|
|
1271
1376
|
if (rel === "SKILL.md") continue;
|
|
1272
1377
|
if (rel.split("/").some((segment) => segment.startsWith("."))) continue;
|
|
1273
1378
|
const segments = rel.split("/");
|
|
1274
|
-
const raw =
|
|
1379
|
+
const raw = readFileSync3(join5(srcDir, ...segments), "utf8");
|
|
1275
1380
|
const ported = rel.endsWith(".md") || rel.endsWith(".sh") ? `${portBody(raw)}
|
|
1276
1381
|
` : raw;
|
|
1277
|
-
const target =
|
|
1382
|
+
const target = join5(targetDir, ...segments);
|
|
1278
1383
|
if (!writer.write(target, ported, { createInRefresh: true })) continue;
|
|
1279
1384
|
if (rel.endsWith(".sh")) chmodSync(target, 493);
|
|
1280
1385
|
written.push(target);
|
|
@@ -1285,30 +1390,40 @@ function writeBundledSkillDirs(params) {
|
|
|
1285
1390
|
|
|
1286
1391
|
// src/owned-write.ts
|
|
1287
1392
|
init_esm_shims();
|
|
1288
|
-
import { existsSync as
|
|
1289
|
-
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";
|
|
1290
1395
|
|
|
1291
1396
|
// src/foreign-slot.ts
|
|
1292
1397
|
init_esm_shims();
|
|
1293
|
-
import { lstatSync } from "fs";
|
|
1294
|
-
import { join as
|
|
1398
|
+
import { lstatSync, realpathSync, statSync } from "fs";
|
|
1399
|
+
import { join as join6 } from "path";
|
|
1295
1400
|
var SKILL_SLOT = /^((?:\.claude|\.agents)\/skills\/[^/]+)(?:\/|$)/;
|
|
1296
1401
|
function occupiedByNonDirectory(path) {
|
|
1297
1402
|
const stat = lstatSync(path, { throwIfNoEntry: false });
|
|
1298
1403
|
return stat !== void 0 && !stat.isDirectory();
|
|
1299
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
|
+
}
|
|
1300
1415
|
function foreignOwnedTarget(projectDir, rel) {
|
|
1301
1416
|
const slot = SKILL_SLOT.exec(rel)?.[1];
|
|
1302
1417
|
if (slot === void 0) return null;
|
|
1303
|
-
if (occupiedByNonDirectory(
|
|
1418
|
+
if (occupiedByNonDirectory(join6(projectDir, slot))) return slot;
|
|
1304
1419
|
if (rel !== slot) {
|
|
1305
1420
|
const parts = rel.slice(slot.length + 1).split("/");
|
|
1306
1421
|
let cursor = slot;
|
|
1307
1422
|
for (const part of parts.slice(0, -1)) {
|
|
1308
1423
|
cursor = `${cursor}/${part}`;
|
|
1309
|
-
if (occupiedByNonDirectory(
|
|
1424
|
+
if (occupiedByNonDirectory(join6(projectDir, cursor))) return cursor;
|
|
1310
1425
|
}
|
|
1311
|
-
const leaf = lstatSync(
|
|
1426
|
+
const leaf = lstatSync(join6(projectDir, rel), { throwIfNoEntry: false });
|
|
1312
1427
|
if (leaf !== void 0 && !leaf.isFile()) return rel;
|
|
1313
1428
|
}
|
|
1314
1429
|
return null;
|
|
@@ -1324,20 +1439,20 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
|
|
|
1324
1439
|
let updated = 0;
|
|
1325
1440
|
return {
|
|
1326
1441
|
write(absPath, content, opts) {
|
|
1327
|
-
const rel = relative(projectDir, absPath).split(
|
|
1442
|
+
const rel = relative(projectDir, absPath).split(sep2).join("/");
|
|
1328
1443
|
const foreign = foreignOwnedTarget(projectDir, rel);
|
|
1329
1444
|
if (foreign !== null) {
|
|
1330
1445
|
if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
|
|
1331
1446
|
return false;
|
|
1332
1447
|
}
|
|
1333
1448
|
const digest = hashContent(content);
|
|
1334
|
-
if (!
|
|
1449
|
+
if (!existsSync5(absPath)) {
|
|
1335
1450
|
if (refreshOnly && !opts?.createInRefresh) return false;
|
|
1336
1451
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
1337
1452
|
writeFileSync(absPath, content);
|
|
1338
1453
|
updated++;
|
|
1339
1454
|
} else {
|
|
1340
|
-
const current =
|
|
1455
|
+
const current = readFileSync4(absPath, "utf8");
|
|
1341
1456
|
if (current !== content) {
|
|
1342
1457
|
if (!isHarnessOwned(baseline, rel, current)) {
|
|
1343
1458
|
backupPaths.push(backupFile(absPath, now));
|
|
@@ -1364,8 +1479,8 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
|
|
|
1364
1479
|
|
|
1365
1480
|
// src/rules-port.ts
|
|
1366
1481
|
init_esm_shims();
|
|
1367
|
-
import { existsSync as
|
|
1368
|
-
import { join as
|
|
1482
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
1483
|
+
import { join as join7 } from "path";
|
|
1369
1484
|
function stripClaudeFrontmatter(source) {
|
|
1370
1485
|
if (!source.startsWith("---\n")) return source.trim();
|
|
1371
1486
|
const end = source.indexOf("\n---", 4);
|
|
@@ -1375,9 +1490,9 @@ function stripClaudeFrontmatter(source) {
|
|
|
1375
1490
|
function portRules(harnessRoot, ruleNames) {
|
|
1376
1491
|
const ported = [];
|
|
1377
1492
|
for (const name of ruleNames) {
|
|
1378
|
-
const src =
|
|
1379
|
-
if (!
|
|
1380
|
-
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")) });
|
|
1381
1496
|
}
|
|
1382
1497
|
return ported;
|
|
1383
1498
|
}
|
|
@@ -1397,11 +1512,17 @@ function runAntigravityTransform(params) {
|
|
|
1397
1512
|
refreshOnly
|
|
1398
1513
|
} = params;
|
|
1399
1514
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1400
|
-
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);
|
|
1401
1522
|
const antigravityInstalled = rulesFile !== null;
|
|
1402
1523
|
const harnessRuleFiles = [];
|
|
1403
1524
|
for (const rule of portRules(harnessRoot, rules)) {
|
|
1404
|
-
const target =
|
|
1525
|
+
const target = join8(projectDir, ".agents", "rules", `${rule.name}.md`);
|
|
1405
1526
|
if (!writer.write(target, `${rule.body}
|
|
1406
1527
|
`, { createInRefresh: antigravityInstalled })) {
|
|
1407
1528
|
continue;
|
|
@@ -1411,8 +1532,8 @@ function runAntigravityTransform(params) {
|
|
|
1411
1532
|
const skillFiles = writeBundledSkillDirs({
|
|
1412
1533
|
harnessRoot,
|
|
1413
1534
|
projectDir,
|
|
1414
|
-
skillIds
|
|
1415
|
-
writer
|
|
1535
|
+
skillIds,
|
|
1536
|
+
writer: skillWriter
|
|
1416
1537
|
});
|
|
1417
1538
|
return {
|
|
1418
1539
|
rulesFile,
|
|
@@ -1422,14 +1543,14 @@ function runAntigravityTransform(params) {
|
|
|
1422
1543
|
};
|
|
1423
1544
|
}
|
|
1424
1545
|
function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
|
|
1425
|
-
const claudeMdPath =
|
|
1426
|
-
const templatePath =
|
|
1427
|
-
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)) {
|
|
1428
1549
|
return null;
|
|
1429
1550
|
}
|
|
1430
|
-
const claudeMd =
|
|
1431
|
-
const template =
|
|
1432
|
-
const target =
|
|
1551
|
+
const claudeMd = readFileSync6(claudeMdPath, "utf8");
|
|
1552
|
+
const template = readFileSync6(templatePath, "utf8");
|
|
1553
|
+
const target = join8(projectDir, ".agents", "rules", "uzys-harness.md");
|
|
1433
1554
|
const rulesOut = renderAgentsMd({
|
|
1434
1555
|
template,
|
|
1435
1556
|
claudeMd,
|
|
@@ -1447,18 +1568,18 @@ function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
|
|
|
1447
1568
|
// src/codex/opt-in.ts
|
|
1448
1569
|
init_esm_shims();
|
|
1449
1570
|
import { homedir } from "os";
|
|
1450
|
-
import { join as
|
|
1571
|
+
import { join as join9 } from "path";
|
|
1451
1572
|
|
|
1452
1573
|
// src/codex/trust-entry.ts
|
|
1453
1574
|
init_esm_shims();
|
|
1454
|
-
import { existsSync as
|
|
1575
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
1455
1576
|
import { dirname as dirname2 } from "path";
|
|
1456
1577
|
var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
|
|
1457
1578
|
function registerTrustEntry(opts) {
|
|
1458
1579
|
const { configPath, projectDir } = opts;
|
|
1459
1580
|
try {
|
|
1460
1581
|
mkdirSync2(dirname2(configPath), { recursive: true });
|
|
1461
|
-
const existing =
|
|
1582
|
+
const existing = existsSync8(configPath) ? readFileSync7(configPath, "utf8") : "";
|
|
1462
1583
|
if (hasTrustEntry(existing, projectDir)) {
|
|
1463
1584
|
return { status: "already-present" };
|
|
1464
1585
|
}
|
|
@@ -1482,8 +1603,8 @@ function hasTrustEntry(configContent, projectDir) {
|
|
|
1482
1603
|
|
|
1483
1604
|
// src/codex/opt-in.ts
|
|
1484
1605
|
function runCodexOptIn(ctx) {
|
|
1485
|
-
const codexHome = ctx.codexHome ??
|
|
1486
|
-
const configPath =
|
|
1606
|
+
const codexHome = ctx.codexHome ?? join9(homedir(), ".codex");
|
|
1607
|
+
const configPath = join9(codexHome, "config.toml");
|
|
1487
1608
|
const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
|
|
1488
1609
|
return {
|
|
1489
1610
|
trustEntry: {
|
|
@@ -1496,8 +1617,8 @@ function runCodexOptIn(ctx) {
|
|
|
1496
1617
|
|
|
1497
1618
|
// src/codex/transform.ts
|
|
1498
1619
|
init_esm_shims();
|
|
1499
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
1500
|
-
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";
|
|
1501
1622
|
|
|
1502
1623
|
// src/codex/config-toml.ts
|
|
1503
1624
|
init_esm_shims();
|
|
@@ -1571,38 +1692,49 @@ function runCodexTransform(params) {
|
|
|
1571
1692
|
selectedInternalSkills = [],
|
|
1572
1693
|
rules = [],
|
|
1573
1694
|
baseline,
|
|
1574
|
-
refreshOnly
|
|
1695
|
+
refreshOnly,
|
|
1696
|
+
writeAgentsMd = true
|
|
1575
1697
|
} = params;
|
|
1576
1698
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1577
|
-
const claudeMd = readRequired(
|
|
1578
|
-
const agentsTemplate = readRequired(
|
|
1579
|
-
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"));
|
|
1580
1702
|
const projectName = basename2(projectDir);
|
|
1581
|
-
const mcp = readOptionalJson(
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
projectName,
|
|
1588
|
-
// ADR-085 — 상시 스킬 안내는 앵커가 아니라 여기(프로젝트 맥락)에, 깔린 것만.
|
|
1589
|
-
// #503 — 그 조각은 설치자 소유 절 안에 사니 마커로 감싼다.
|
|
1590
|
-
projectContext: withMarkedContinuousSkillsNote(
|
|
1591
|
-
renderFillScaffold("agents-md"),
|
|
1592
|
-
selectedInternalSkills
|
|
1593
|
-
),
|
|
1594
|
-
// Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
|
|
1595
|
-
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
|
|
1596
1709
|
});
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
template: agentsTemplate
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
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");
|
|
1606
1738
|
writer.write(
|
|
1607
1739
|
configTomlPath,
|
|
1608
1740
|
renderConfigToml({
|
|
@@ -1612,15 +1744,15 @@ function runCodexTransform(params) {
|
|
|
1612
1744
|
mcp
|
|
1613
1745
|
})
|
|
1614
1746
|
);
|
|
1615
|
-
const hookDir =
|
|
1747
|
+
const hookDir = join10(projectDir, ".codex/hooks");
|
|
1616
1748
|
const hookFiles = [];
|
|
1617
1749
|
for (const hook of HOOK_NAMES) {
|
|
1618
|
-
const src =
|
|
1619
|
-
if (!
|
|
1750
|
+
const src = join10(harnessRoot, "templates/hooks", `${hook}.sh`);
|
|
1751
|
+
if (!existsSync9(src)) {
|
|
1620
1752
|
continue;
|
|
1621
1753
|
}
|
|
1622
|
-
const ported =
|
|
1623
|
-
const target =
|
|
1754
|
+
const ported = readFileSync8(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
|
|
1755
|
+
const target = join10(hookDir, `${hook}.sh`);
|
|
1624
1756
|
if (!writer.write(target, ported)) continue;
|
|
1625
1757
|
chmodSync2(target, 493);
|
|
1626
1758
|
hookFiles.push(target);
|
|
@@ -1628,11 +1760,12 @@ function runCodexTransform(params) {
|
|
|
1628
1760
|
const skillFiles = writeBundledSkillDirs({
|
|
1629
1761
|
harnessRoot,
|
|
1630
1762
|
projectDir,
|
|
1631
|
-
skillIds
|
|
1632
|
-
writer
|
|
1763
|
+
skillIds,
|
|
1764
|
+
writer: skillWriter
|
|
1633
1765
|
});
|
|
1634
1766
|
return {
|
|
1635
1767
|
agentsMdPath,
|
|
1768
|
+
agentsMdSeededFrom: seededContext === null ? null : "CLAUDE.md",
|
|
1636
1769
|
configTomlPath,
|
|
1637
1770
|
hookFiles,
|
|
1638
1771
|
skillFiles,
|
|
@@ -1640,17 +1773,17 @@ function runCodexTransform(params) {
|
|
|
1640
1773
|
};
|
|
1641
1774
|
}
|
|
1642
1775
|
function readRequired(path) {
|
|
1643
|
-
if (!
|
|
1776
|
+
if (!existsSync9(path)) {
|
|
1644
1777
|
throw new Error(`Codex transform: required source missing: ${path}`);
|
|
1645
1778
|
}
|
|
1646
|
-
return
|
|
1779
|
+
return readFileSync8(path, "utf8");
|
|
1647
1780
|
}
|
|
1648
1781
|
function readOptionalJson(path) {
|
|
1649
|
-
if (!
|
|
1782
|
+
if (!existsSync9(path)) {
|
|
1650
1783
|
return null;
|
|
1651
1784
|
}
|
|
1652
1785
|
try {
|
|
1653
|
-
return JSON.parse(
|
|
1786
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
1654
1787
|
} catch {
|
|
1655
1788
|
return null;
|
|
1656
1789
|
}
|
|
@@ -1658,8 +1791,8 @@ function readOptionalJson(path) {
|
|
|
1658
1791
|
|
|
1659
1792
|
// src/opencode/transform.ts
|
|
1660
1793
|
init_esm_shims();
|
|
1661
|
-
import { existsSync as
|
|
1662
|
-
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";
|
|
1663
1796
|
|
|
1664
1797
|
// src/opencode/agents-md.ts
|
|
1665
1798
|
init_esm_shims();
|
|
@@ -1702,24 +1835,33 @@ function runOpencodeTransform(params) {
|
|
|
1702
1835
|
refreshOnly
|
|
1703
1836
|
} = params;
|
|
1704
1837
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1705
|
-
const claudeMd = readRequired2(
|
|
1706
|
-
const agentsTemplate = readRequired2(
|
|
1838
|
+
const claudeMd = readRequired2(join11(harnessRoot, "templates/CLAUDE.md"));
|
|
1839
|
+
const agentsTemplate = readRequired2(join11(harnessRoot, "templates/opencode/AGENTS.md.template"));
|
|
1707
1840
|
const opencodeTemplate = readRequired2(
|
|
1708
|
-
|
|
1841
|
+
join11(harnessRoot, "templates/opencode/opencode.json.template")
|
|
1709
1842
|
);
|
|
1710
1843
|
const projectName = basename3(projectDir);
|
|
1711
|
-
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
|
+
});
|
|
1712
1851
|
ensureDir(projectDir);
|
|
1713
|
-
const agentsMdPath =
|
|
1852
|
+
const agentsMdPath = join11(projectDir, "AGENTS.md");
|
|
1853
|
+
const seededContext = refreshOnly || existsSync10(agentsMdPath) ? null : seedAgentsMdProjectContext(projectDir);
|
|
1714
1854
|
const agentsMdOut = renderAgentsMd2({
|
|
1715
1855
|
template: agentsTemplate,
|
|
1716
1856
|
claudeMd,
|
|
1717
1857
|
projectName,
|
|
1718
1858
|
// ADR-085 — 상시 스킬 안내는 앵커가 아니라 여기(프로젝트 맥락)에, 깔린 것만.
|
|
1719
1859
|
// #503 — 그 조각은 설치자 소유 절 안에 사니 마커로 감싼다 (codex 와 같은 마커).
|
|
1860
|
+
// #531 — `skillIds` 를 쓴다(= 깔린 것 + **이번 실행이 만들 것**). 안 그러면 이번에 새로
|
|
1861
|
+
// 깐 상시 스킬이 다음 update 까지 안내에서 빠져 설치자 에이전트가 영영 안 연다.
|
|
1720
1862
|
projectContext: withMarkedContinuousSkillsNote(
|
|
1721
|
-
renderFillScaffold("agents-md"),
|
|
1722
|
-
|
|
1863
|
+
seededContext ?? renderFillScaffold("agents-md"),
|
|
1864
|
+
skillIds
|
|
1723
1865
|
),
|
|
1724
1866
|
// codex 와 **같은 파일**(프로젝트 루트 `AGENTS.md`)이다. 두 transform 이 서로 다른 본문을
|
|
1725
1867
|
// 쓰면 나중에 도는 쪽이 앞선 쪽을 덮어써, codex+opencode 조합에서 룰이 통째로 사라진다
|
|
@@ -1730,26 +1872,26 @@ function runOpencodeTransform(params) {
|
|
|
1730
1872
|
agentsMdPath,
|
|
1731
1873
|
mergeAgentsMd({
|
|
1732
1874
|
rendered: agentsMdOut,
|
|
1733
|
-
existing:
|
|
1875
|
+
existing: existsSync10(agentsMdPath) ? readFileSync9(agentsMdPath, "utf8") : null,
|
|
1734
1876
|
template: agentsTemplate
|
|
1735
1877
|
})
|
|
1736
1878
|
);
|
|
1737
|
-
const opencodeJsonPath =
|
|
1879
|
+
const opencodeJsonPath = join11(projectDir, "opencode.json");
|
|
1738
1880
|
writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
|
|
1739
1881
|
const skillFiles = writeBundledSkillDirs({
|
|
1740
1882
|
harnessRoot,
|
|
1741
1883
|
projectDir,
|
|
1742
|
-
skillIds
|
|
1743
|
-
writer
|
|
1884
|
+
skillIds,
|
|
1885
|
+
writer: skillWriter
|
|
1744
1886
|
});
|
|
1745
1887
|
const retiredCommands = [];
|
|
1746
|
-
const cmdDir =
|
|
1747
|
-
if (
|
|
1888
|
+
const cmdDir = join11(projectDir, ".opencode/commands");
|
|
1889
|
+
if (existsSync10(cmdDir)) {
|
|
1748
1890
|
for (const entry of readdirSync(cmdDir)) {
|
|
1749
1891
|
if (!entry.endsWith(".md")) continue;
|
|
1750
1892
|
const id = entry.slice(0, -3);
|
|
1751
|
-
if (!
|
|
1752
|
-
const victim =
|
|
1893
|
+
if (!existsSync10(join11(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
|
|
1894
|
+
const victim = join11(cmdDir, entry);
|
|
1753
1895
|
try {
|
|
1754
1896
|
backupFile(victim);
|
|
1755
1897
|
unlinkSync(victim);
|
|
@@ -1760,6 +1902,7 @@ function runOpencodeTransform(params) {
|
|
|
1760
1902
|
}
|
|
1761
1903
|
return {
|
|
1762
1904
|
agentsMdPath,
|
|
1905
|
+
agentsMdSeededFrom: seededContext === null ? null : "CLAUDE.md",
|
|
1763
1906
|
opencodeJsonPath,
|
|
1764
1907
|
skillFiles,
|
|
1765
1908
|
retiredCommands,
|
|
@@ -1767,17 +1910,17 @@ function runOpencodeTransform(params) {
|
|
|
1767
1910
|
};
|
|
1768
1911
|
}
|
|
1769
1912
|
function readRequired2(path) {
|
|
1770
|
-
if (!
|
|
1913
|
+
if (!existsSync10(path)) {
|
|
1771
1914
|
throw new Error(`OpenCode transform: required source missing: ${path}`);
|
|
1772
1915
|
}
|
|
1773
|
-
return
|
|
1916
|
+
return readFileSync9(path, "utf8");
|
|
1774
1917
|
}
|
|
1775
1918
|
function readOptionalJson2(path) {
|
|
1776
|
-
if (!
|
|
1919
|
+
if (!existsSync10(path)) {
|
|
1777
1920
|
return null;
|
|
1778
1921
|
}
|
|
1779
1922
|
try {
|
|
1780
|
-
return JSON.parse(
|
|
1923
|
+
return JSON.parse(readFileSync9(path, "utf8"));
|
|
1781
1924
|
} catch {
|
|
1782
1925
|
return null;
|
|
1783
1926
|
}
|
|
@@ -1823,7 +1966,11 @@ function runCliTransforms(params) {
|
|
|
1823
1966
|
selectedInternalSkills,
|
|
1824
1967
|
rules,
|
|
1825
1968
|
baseline,
|
|
1826
|
-
refreshOnly
|
|
1969
|
+
refreshOnly,
|
|
1970
|
+
// #550 — 같은 `AGENTS.md` 를 한 실행에서 쓰는 쪽은 하나다. opencode 가 뒤에서 자기 템플릿으로
|
|
1971
|
+
// 쓰면 codex 판은 어차피 같은 실행 안에서 덮였고(최종 파일 = opencode 판), 매 실행 codex 가
|
|
1972
|
+
// opencode 판을 자기 판으로 뒤집는 한 번의 쓰기가 설치자 편집분을 백업으로 쌓았다.
|
|
1973
|
+
writeAgentsMd: !cli2.includes("opencode")
|
|
1827
1974
|
});
|
|
1828
1975
|
absorb(codex);
|
|
1829
1976
|
if (codexTrust) {
|
|
@@ -1869,8 +2016,8 @@ function runCliTransforms(params) {
|
|
|
1869
2016
|
|
|
1870
2017
|
// src/env-files.ts
|
|
1871
2018
|
init_esm_shims();
|
|
1872
|
-
import { appendFileSync, existsSync as
|
|
1873
|
-
import { join as
|
|
2019
|
+
import { appendFileSync, existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
|
|
2020
|
+
import { join as join12 } from "path";
|
|
1874
2021
|
var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
|
|
1875
2022
|
# Copy to .env (gitignored) and fill in values: cp .env.example .env
|
|
1876
2023
|
|
|
@@ -1907,24 +2054,24 @@ function writeEnvExample(projectDir, tracks) {
|
|
|
1907
2054
|
if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
|
|
1908
2055
|
return false;
|
|
1909
2056
|
}
|
|
1910
|
-
const path =
|
|
1911
|
-
if (
|
|
2057
|
+
const path = join12(projectDir, ".env.example");
|
|
2058
|
+
if (existsSync11(path)) {
|
|
1912
2059
|
return false;
|
|
1913
2060
|
}
|
|
1914
2061
|
writeFileSync3(path, ENV_EXAMPLE_BODY);
|
|
1915
2062
|
return true;
|
|
1916
2063
|
}
|
|
1917
2064
|
function addGitignoreEnv(projectDir) {
|
|
1918
|
-
const path =
|
|
1919
|
-
if (!
|
|
2065
|
+
const path = join12(projectDir, ".gitignore");
|
|
2066
|
+
if (!existsSync11(path)) {
|
|
1920
2067
|
return false;
|
|
1921
2068
|
}
|
|
1922
|
-
const content =
|
|
2069
|
+
const content = readFileSync10(path, "utf8");
|
|
1923
2070
|
if (GITIGNORE_ENV_PATTERN.test(content)) {
|
|
1924
2071
|
return false;
|
|
1925
2072
|
}
|
|
1926
|
-
const
|
|
1927
|
-
appendFileSync(path, `${
|
|
2073
|
+
const sep4 = content.endsWith("\n") ? "" : "\n";
|
|
2074
|
+
appendFileSync(path, `${sep4}
|
|
1928
2075
|
# Secret env (auto-added by agent-harness install)
|
|
1929
2076
|
.env
|
|
1930
2077
|
`);
|
|
@@ -1933,11 +2080,11 @@ function addGitignoreEnv(projectDir) {
|
|
|
1933
2080
|
var AGENT_ARTIFACT_DIRS = [".factory/", ".goose/", ".uzys-agent-harness/"];
|
|
1934
2081
|
var GITIGNORE_AGENT_ARTIFACT_HEADER = "# agent CLI / harness \uC790\uB3D9 \uC0DD\uC131\uBB3C (auto-added by agent-harness)";
|
|
1935
2082
|
function addGitignoreAgentArtifacts(projectDir) {
|
|
1936
|
-
const path =
|
|
1937
|
-
if (!
|
|
2083
|
+
const path = join12(projectDir, ".gitignore");
|
|
2084
|
+
if (!existsSync11(path)) {
|
|
1938
2085
|
return [];
|
|
1939
2086
|
}
|
|
1940
|
-
const content =
|
|
2087
|
+
const content = readFileSync10(path, "utf8");
|
|
1941
2088
|
const missing = AGENT_ARTIFACT_DIRS.filter((pattern) => {
|
|
1942
2089
|
const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
|
|
1943
2090
|
return !lineRegex.test(content);
|
|
@@ -1945,9 +2092,9 @@ function addGitignoreAgentArtifacts(projectDir) {
|
|
|
1945
2092
|
if (missing.length === 0) {
|
|
1946
2093
|
return [];
|
|
1947
2094
|
}
|
|
1948
|
-
const
|
|
2095
|
+
const sep4 = content.endsWith("\n") ? "" : "\n";
|
|
1949
2096
|
const block = [GITIGNORE_AGENT_ARTIFACT_HEADER, ...missing].join("\n");
|
|
1950
|
-
appendFileSync(path, `${
|
|
2097
|
+
appendFileSync(path, `${sep4}
|
|
1951
2098
|
${block}
|
|
1952
2099
|
`);
|
|
1953
2100
|
return [...missing];
|
|
@@ -1956,9 +2103,9 @@ ${block}
|
|
|
1956
2103
|
// src/external-installer.ts
|
|
1957
2104
|
init_esm_shims();
|
|
1958
2105
|
import { spawnSync } from "child_process";
|
|
1959
|
-
import { existsSync as
|
|
2106
|
+
import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
|
|
1960
2107
|
import { homedir as homedir2 } from "os";
|
|
1961
|
-
import { join as
|
|
2108
|
+
import { join as join13 } from "path";
|
|
1962
2109
|
var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
|
|
1963
2110
|
var NPM_SPAWN_TIMEOUT_MS = 6e5;
|
|
1964
2111
|
function spawnTimeoutFor(kind) {
|
|
@@ -2070,7 +2217,10 @@ function refreshExternalSkills(projectDir, deps = {}) {
|
|
|
2070
2217
|
tracks: log.spec.tracks,
|
|
2071
2218
|
// `forceInclude` 가 조건 판정을 앞지르므로 옵션 값은 결과에 영향을 주지 않는다.
|
|
2072
2219
|
options: DEFAULT_OPTIONS,
|
|
2073
|
-
|
|
2220
|
+
// #528 — 대상 CLI 는 **깔린 집합**(`installedClis`)이다. `spec.cli` 는 마지막 install 의
|
|
2221
|
+
// 요청이라 `uninstall --cli` 뒤에도 뺀 CLI 를 말하고, 반대로 그걸 비우면 `buildSkillArgs` 가
|
|
2222
|
+
// `--agent`·`--copy` 를 못 붙여 Claude 몫이 조용히 빠진다(재리뷰 BLOCKER-3).
|
|
2223
|
+
cli: [...installedClis(log)],
|
|
2074
2224
|
userOverride: { forceInclude: [...installedIds], forceExclude: [] },
|
|
2075
2225
|
scope: log.scope,
|
|
2076
2226
|
projectDir
|
|
@@ -2166,17 +2316,17 @@ function detectVersion(method, spawn) {
|
|
|
2166
2316
|
if (at2 <= 0) return void 0;
|
|
2167
2317
|
const plugin = method.pluginId.slice(0, at2);
|
|
2168
2318
|
const marketplaceShort = method.pluginId.slice(at2 + 1);
|
|
2169
|
-
const cacheBase =
|
|
2170
|
-
if (!
|
|
2319
|
+
const cacheBase = join13(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
|
|
2320
|
+
if (!existsSync12(cacheBase)) return void 0;
|
|
2171
2321
|
const versions = readdirSync2(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
|
|
2172
2322
|
return versions.at(-1);
|
|
2173
2323
|
}
|
|
2174
2324
|
case "npm": {
|
|
2175
2325
|
const npmRoot = getNpmGlobalRoot(spawn);
|
|
2176
2326
|
if (!npmRoot) return void 0;
|
|
2177
|
-
const pkgJson =
|
|
2178
|
-
if (!
|
|
2179
|
-
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"));
|
|
2180
2330
|
return parsed.version;
|
|
2181
2331
|
}
|
|
2182
2332
|
default:
|
|
@@ -2201,9 +2351,30 @@ function getNpmGlobalRoot(spawn) {
|
|
|
2201
2351
|
return void 0;
|
|
2202
2352
|
}
|
|
2203
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
|
+
|
|
2204
2375
|
// src/mcp-merge.ts
|
|
2205
2376
|
init_esm_shims();
|
|
2206
|
-
import { existsSync as
|
|
2377
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
|
|
2207
2378
|
function parseTrackMcpMap(raw) {
|
|
2208
2379
|
const rows = [];
|
|
2209
2380
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -2254,15 +2425,15 @@ function mergeMcpServers(base, rows, tracks) {
|
|
|
2254
2425
|
return out;
|
|
2255
2426
|
}
|
|
2256
2427
|
function composeMcpJson(opts) {
|
|
2257
|
-
const base = JSON.parse(
|
|
2258
|
-
const merged = opts.existingPath &&
|
|
2259
|
-
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") : "";
|
|
2260
2431
|
const rows = parseTrackMcpMap(mapRaw);
|
|
2261
2432
|
return mergeMcpServers(merged, rows, opts.tracks);
|
|
2262
2433
|
}
|
|
2263
2434
|
function mergeUserBase(base, existingPath) {
|
|
2264
2435
|
try {
|
|
2265
|
-
const existing = JSON.parse(
|
|
2436
|
+
const existing = JSON.parse(readFileSync12(existingPath, "utf8"));
|
|
2266
2437
|
return {
|
|
2267
2438
|
...base,
|
|
2268
2439
|
mcpServers: { ...base.mcpServers, ...existing.mcpServers }
|
|
@@ -2280,24 +2451,26 @@ function writeMcpJson(path, mcp) {
|
|
|
2280
2451
|
init_esm_shims();
|
|
2281
2452
|
import {
|
|
2282
2453
|
copyFileSync,
|
|
2283
|
-
existsSync as
|
|
2454
|
+
existsSync as existsSync14,
|
|
2284
2455
|
mkdirSync as mkdirSync3,
|
|
2285
2456
|
readdirSync as readdirSync3,
|
|
2286
|
-
readFileSync as
|
|
2457
|
+
readFileSync as readFileSync13,
|
|
2287
2458
|
rmdirSync,
|
|
2288
|
-
statSync,
|
|
2459
|
+
statSync as statSync2,
|
|
2289
2460
|
unlinkSync as unlinkSync2,
|
|
2290
2461
|
writeFileSync as writeFileSync5
|
|
2291
2462
|
} from "fs";
|
|
2292
|
-
import { basename as basename4, dirname as dirname3, join as
|
|
2463
|
+
import { basename as basename4, dirname as dirname3, join as join15 } from "path";
|
|
2293
2464
|
var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
|
|
2294
2465
|
function buildUpdateSpec(projectDir, tracks, only) {
|
|
2466
|
+
const log = readInstallLog(projectDir);
|
|
2467
|
+
const clis = log === null ? [] : installedClis(log);
|
|
2295
2468
|
return {
|
|
2296
2469
|
tracks: [...tracks],
|
|
2297
2470
|
options: DEFAULT_OPTIONS,
|
|
2298
|
-
cli: ["claude"],
|
|
2471
|
+
cli: clis.length > 0 ? [...clis] : ["claude"],
|
|
2299
2472
|
projectDir,
|
|
2300
|
-
scope:
|
|
2473
|
+
scope: log?.scope ?? "project",
|
|
2301
2474
|
// 전부 골랐으면 "제한 없음"과 같다 — 화면·기록에 제한이 있었던 것처럼 남기지 않는다.
|
|
2302
2475
|
...only !== void 0 && only.length > 0 && only.length < UPDATE_GROUPS.length ? { updateOnly: [...only] } : {}
|
|
2303
2476
|
};
|
|
@@ -2308,7 +2481,7 @@ function parseUpdateOnly(raw) {
|
|
|
2308
2481
|
return { ok: true, groups: [...new Set(raw)] };
|
|
2309
2482
|
}
|
|
2310
2483
|
function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
2311
|
-
const claudeDir =
|
|
2484
|
+
const claudeDir = join15(projectDir, ".claude");
|
|
2312
2485
|
const startedAt = Date.now();
|
|
2313
2486
|
const wants = (g) => only === void 0 || only.includes(g);
|
|
2314
2487
|
const report = {
|
|
@@ -2323,6 +2496,7 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2323
2496
|
rootImportAdded: false,
|
|
2324
2497
|
rootBlockRefreshed: false,
|
|
2325
2498
|
legacyAnchor: null,
|
|
2499
|
+
claudeUnrecorded: null,
|
|
2326
2500
|
skillsBackedUp: [],
|
|
2327
2501
|
skillsSkippedLinks: [],
|
|
2328
2502
|
skillsPruned: [],
|
|
@@ -2345,11 +2519,14 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2345
2519
|
report.installedNew = fresh.installed;
|
|
2346
2520
|
report.restored = fresh.restored;
|
|
2347
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;
|
|
2348
2525
|
const policyBase = policyBaseline(projectDir);
|
|
2349
2526
|
for (const { dir, ext } of POLICY_DIRS) {
|
|
2350
|
-
if (!wants(dir === "hooks" ? "hooks" : "rules")) continue;
|
|
2351
|
-
const target =
|
|
2352
|
-
const source =
|
|
2527
|
+
if (!claudeManaged || !wants(dir === "hooks" ? "hooks" : "rules")) continue;
|
|
2528
|
+
const target = join15(claudeDir, dir);
|
|
2529
|
+
const source = join15(templatesDir, dir);
|
|
2353
2530
|
const label = `.claude/${dir}`;
|
|
2354
2531
|
const ctx = { prefix: dir, baseline: policyBase };
|
|
2355
2532
|
const synced = updateDir(target, source, ext, ctx);
|
|
@@ -2360,10 +2537,11 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2360
2537
|
const syncedDirs = POLICY_DIRS.filter((d2) => wants(d2.dir === "hooks" ? "hooks" : "rules")).map(
|
|
2361
2538
|
(d2) => d2.dir
|
|
2362
2539
|
);
|
|
2363
|
-
if (syncedDirs.length > 0)
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
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"),
|
|
2367
2545
|
skillBaseline(projectDir),
|
|
2368
2546
|
/* @__PURE__ */ new Date(),
|
|
2369
2547
|
(relInSkills) => foreignOwnedTarget(projectDir, `.claude/skills/${relInSkills}`)
|
|
@@ -2372,25 +2550,36 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2372
2550
|
report.skillsBackedUp = skillSync.backedUp;
|
|
2373
2551
|
report.skillsSkippedLinks = skillSync.skippedLinks;
|
|
2374
2552
|
report.skillsPruned = skillSync.pruned;
|
|
2375
|
-
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: [] };
|
|
2376
2554
|
report.installedNew.push(...newSkills.installed);
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
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) {
|
|
2382
2561
|
if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report);
|
|
2383
2562
|
}
|
|
2384
|
-
const settingsPath =
|
|
2385
|
-
if (wants("hooks") &&
|
|
2563
|
+
const settingsPath = join15(claudeDir, "settings.json");
|
|
2564
|
+
if (claudeManaged && wants("hooks") && existsSync14(settingsPath)) {
|
|
2386
2565
|
report.staleHookRefs = cleanStaleHookRefs(settingsPath, claudeDir);
|
|
2387
2566
|
}
|
|
2388
2567
|
if (wants("hooks")) report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
|
|
2389
2568
|
report.retiredAgents = staleAgentFiles(claudeDir);
|
|
2390
2569
|
report.demotedAgents = demotedAgentFiles(claudeDir, installedTracks(projectDir));
|
|
2391
|
-
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
|
+
};
|
|
2392
2578
|
report.externalUpdated = external.externalUpdated;
|
|
2393
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));
|
|
2394
2583
|
const saidBySlotRow = new Set(report.skillsSkippedLinks.map((id) => `.claude/skills/${id}`));
|
|
2395
2584
|
const merged = [];
|
|
2396
2585
|
for (const f of [
|
|
@@ -2402,6 +2591,17 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2402
2591
|
if (!saidBySlotRow.has(f) && !merged.includes(f)) merged.push(f);
|
|
2403
2592
|
}
|
|
2404
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
|
+
}
|
|
2405
2605
|
const skillRefresh = wants("external") ? (deps.refreshSkills ?? refreshExternalSkills)(projectDir) : { attempted: 0, refreshed: 0, failed: [], notInCatalog: [], unknown: false };
|
|
2406
2606
|
report.externalSkillsRefreshed = skillRefresh.refreshed;
|
|
2407
2607
|
report.externalSkillsFailed = skillRefresh.failed;
|
|
@@ -2415,20 +2615,20 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
|
|
|
2415
2615
|
return report;
|
|
2416
2616
|
}
|
|
2417
2617
|
function staleSkillDirs(claudeDir) {
|
|
2418
|
-
const dir =
|
|
2419
|
-
if (!
|
|
2618
|
+
const dir = join15(claudeDir, "skills");
|
|
2619
|
+
if (!existsSync14(dir)) return [];
|
|
2420
2620
|
const stale = (id) => RENAMED_SKILL_IDS[id] !== void 0 || RETIRED_SKILL_IDS.includes(id);
|
|
2421
2621
|
return readdirSync3(dir, { withFileTypes: true }).filter((e2) => e2.isDirectory() && stale(e2.name)).map((e2) => e2.name);
|
|
2422
2622
|
}
|
|
2423
2623
|
function staleAgentFiles(claudeDir) {
|
|
2424
|
-
const dir =
|
|
2425
|
-
if (!
|
|
2624
|
+
const dir = join15(claudeDir, "agents");
|
|
2625
|
+
if (!existsSync14(dir)) return [];
|
|
2426
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));
|
|
2427
2627
|
}
|
|
2428
2628
|
function demotedAgentFiles(claudeDir, tracks) {
|
|
2429
2629
|
if (tracks.length === 0) return [];
|
|
2430
|
-
const dir =
|
|
2431
|
-
if (!
|
|
2630
|
+
const dir = join15(claudeDir, "agents");
|
|
2631
|
+
if (!existsSync14(dir)) return [];
|
|
2432
2632
|
const demoted = TRACK_AGENTS.filter(([, pattern]) => !anyTrack(tracks, pattern)).map(
|
|
2433
2633
|
([id]) => id
|
|
2434
2634
|
);
|
|
@@ -2440,7 +2640,7 @@ function installNewAssets(projectDir, templatesDir, wants = () => true) {
|
|
|
2440
2640
|
const needsReinstall = [];
|
|
2441
2641
|
const foreignOwned = [];
|
|
2442
2642
|
const log = readInstallLog(projectDir);
|
|
2443
|
-
const claudeSelected = log
|
|
2643
|
+
const claudeSelected = installedClis(log).includes("claude");
|
|
2444
2644
|
const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
|
|
2445
2645
|
const priorBaseline = policyBaseline(projectDir);
|
|
2446
2646
|
for (const entry of trackOnlyFileAssets(installedTracks(projectDir))) {
|
|
@@ -2448,15 +2648,15 @@ function installNewAssets(projectDir, templatesDir, wants = () => true) {
|
|
|
2448
2648
|
if (!wants(group)) continue;
|
|
2449
2649
|
if (entry.target === HARNESS_ANCHOR_FILE) continue;
|
|
2450
2650
|
if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
|
|
2451
|
-
const target =
|
|
2651
|
+
const target = join15(projectDir, entry.target);
|
|
2452
2652
|
const foreign = foreignOwnedTarget(projectDir, entry.target);
|
|
2453
2653
|
if (foreign !== null) {
|
|
2454
2654
|
if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
|
|
2455
2655
|
continue;
|
|
2456
2656
|
}
|
|
2457
|
-
if (
|
|
2458
|
-
const source =
|
|
2459
|
-
if (!
|
|
2657
|
+
if (existsSync14(target)) continue;
|
|
2658
|
+
const source = join15(templatesDir, entry.source);
|
|
2659
|
+
if (!existsSync14(source)) continue;
|
|
2460
2660
|
if (entry.target.startsWith(".claude/")) {
|
|
2461
2661
|
if (!claudeSelected) continue;
|
|
2462
2662
|
if (entry.target.startsWith(".claude/hooks/") || entry.target === ".claude/settings.json") {
|
|
@@ -2478,26 +2678,26 @@ function collectRunBackups(projectDir, startedAt) {
|
|
|
2478
2678
|
const out = [];
|
|
2479
2679
|
const consider = (rel) => {
|
|
2480
2680
|
if (!BACKUP_SUFFIX.test(rel)) return;
|
|
2481
|
-
const abs =
|
|
2482
|
-
if (
|
|
2681
|
+
const abs = join15(projectDir, rel);
|
|
2682
|
+
if (statSync2(abs).mtimeMs < startedAt - 1e3) return;
|
|
2483
2683
|
out.push({ path: rel.replace(BACKUP_SUFFIX, ""), backup: rel });
|
|
2484
2684
|
};
|
|
2485
2685
|
for (const e2 of readdirSync3(projectDir, { withFileTypes: true }))
|
|
2486
2686
|
if (e2.isFile()) consider(e2.name);
|
|
2487
2687
|
for (const dir of BACKUP_SCAN_DIRS) {
|
|
2488
|
-
for (const rel of listFilesRecursive(
|
|
2688
|
+
for (const rel of listFilesRecursive(join15(projectDir, dir))) consider(`${dir}/${rel}`);
|
|
2489
2689
|
}
|
|
2490
2690
|
return out.sort((a, b2) => a.path.localeCompare(b2.path));
|
|
2491
2691
|
}
|
|
2492
2692
|
function writeBackupList(projectDir, backups) {
|
|
2493
|
-
const dir =
|
|
2494
|
-
const file =
|
|
2693
|
+
const dir = join15(projectDir, ".uzys-agent-harness");
|
|
2694
|
+
const file = join15(dir, "update-backups.json");
|
|
2495
2695
|
try {
|
|
2496
2696
|
if (backups.length === 0) {
|
|
2497
|
-
if (
|
|
2697
|
+
if (existsSync14(file)) unlinkSync2(file);
|
|
2498
2698
|
return;
|
|
2499
2699
|
}
|
|
2500
|
-
if (!
|
|
2700
|
+
if (!existsSync14(dir)) return;
|
|
2501
2701
|
writeFileSync5(
|
|
2502
2702
|
file,
|
|
2503
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)}
|
|
@@ -2508,29 +2708,33 @@ function writeBackupList(projectDir, backups) {
|
|
|
2508
2708
|
}
|
|
2509
2709
|
function installNewSkillDirs(projectDir, templatesDir, tracks) {
|
|
2510
2710
|
const installed = [];
|
|
2711
|
+
const restored = [];
|
|
2511
2712
|
const foreignOwned = [];
|
|
2512
2713
|
const log = readInstallLog(projectDir);
|
|
2714
|
+
const priorIds = new Set((log?.skillFiles ?? []).map((f) => f.path.split("/")[0] ?? ""));
|
|
2513
2715
|
const excluded = new Set(log?.spec.baselineExclude ?? []);
|
|
2514
2716
|
const skillExcluded = new Set(log?.spec.skillExclude ?? []);
|
|
2515
|
-
if (!(log
|
|
2717
|
+
if (log !== null && !installedClis(log).includes("claude"))
|
|
2718
|
+
return { installed, restored, foreignOwned };
|
|
2516
2719
|
const spec = buildAssetSpec({ tracks, options: DEFAULT_OPTIONS });
|
|
2517
2720
|
for (const entry of buildManifest(spec)) {
|
|
2518
2721
|
if (entry.type !== "dir" || !entry.target.startsWith(".claude/skills/")) continue;
|
|
2519
2722
|
if (!entry.applies(spec) || isBaselineExcluded(entry.target, excluded)) continue;
|
|
2520
2723
|
if (skillExcluded.has(entry.target.slice(".claude/skills/".length))) continue;
|
|
2521
|
-
const target =
|
|
2724
|
+
const target = join15(projectDir, entry.target);
|
|
2522
2725
|
const foreign = foreignOwnedTarget(projectDir, entry.target);
|
|
2523
2726
|
if (foreign !== null) {
|
|
2524
2727
|
if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
|
|
2525
2728
|
continue;
|
|
2526
2729
|
}
|
|
2527
|
-
if (
|
|
2528
|
-
const source =
|
|
2529
|
-
if (!
|
|
2730
|
+
if (existsSync14(target)) continue;
|
|
2731
|
+
const source = join15(templatesDir, entry.source);
|
|
2732
|
+
if (!existsSync14(source)) continue;
|
|
2530
2733
|
copyDir(source, target, (rel) => foreignOwnedTarget(projectDir, `${entry.target}/${rel}`));
|
|
2531
|
-
|
|
2734
|
+
if (priorIds.has(entry.target.slice(".claude/skills/".length))) restored.push(entry.target);
|
|
2735
|
+
else installed.push(entry.target);
|
|
2532
2736
|
}
|
|
2533
|
-
return { installed, foreignOwned };
|
|
2737
|
+
return { installed, restored, foreignOwned };
|
|
2534
2738
|
}
|
|
2535
2739
|
function recordNewSkillBaseline(projectDir, templatesDir, targets) {
|
|
2536
2740
|
const log = readInstallLog(projectDir);
|
|
@@ -2554,16 +2758,16 @@ function trackOnlyFileAssets(tracks) {
|
|
|
2554
2758
|
);
|
|
2555
2759
|
}
|
|
2556
2760
|
function syncHarnessAnchor(projectDir, templatesDir, report, now = /* @__PURE__ */ new Date()) {
|
|
2557
|
-
if (
|
|
2761
|
+
if (existsSync14(join15(projectDir, LEGACY_ANCHOR_FILE))) {
|
|
2558
2762
|
report.legacyAnchor = LEGACY_ANCHOR_FILE;
|
|
2559
2763
|
}
|
|
2560
|
-
const templateMd =
|
|
2561
|
-
if (!
|
|
2562
|
-
const anchor =
|
|
2563
|
-
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);
|
|
2564
2768
|
if (existed) {
|
|
2565
|
-
const current =
|
|
2566
|
-
if (current !==
|
|
2769
|
+
const current = readFileSync13(anchor, "utf8");
|
|
2770
|
+
if (current !== readFileSync13(templateMd, "utf8")) {
|
|
2567
2771
|
const recorded = readInstallLog(projectDir)?.templates.rootClaudeMd;
|
|
2568
2772
|
const owned = recorded?.path === HARNESS_ANCHOR_FILE && hashContent(current) === recorded.sha256;
|
|
2569
2773
|
if (!owned) {
|
|
@@ -2583,15 +2787,15 @@ function syncHarnessAnchor(projectDir, templatesDir, report, now = /* @__PURE__
|
|
|
2583
2787
|
recordAnchorBaseline(projectDir, anchor);
|
|
2584
2788
|
}
|
|
2585
2789
|
function upsertRootImport(projectDir) {
|
|
2586
|
-
const target =
|
|
2587
|
-
const existing =
|
|
2790
|
+
const target = join15(projectDir, "CLAUDE.md");
|
|
2791
|
+
const existing = existsSync14(target) ? readFileSync13(target, "utf8") : null;
|
|
2588
2792
|
const next = upsertHarnessImport(existing, {
|
|
2589
2793
|
projectName: basename4(projectDir),
|
|
2590
2794
|
tracks: installedTracks(projectDir),
|
|
2591
2795
|
// ADR-085 — update 는 선택 목록을 인자로 받지 않는다. `syncSkills` 와 같은 판정을 쓴다:
|
|
2592
2796
|
// `.claude/skills/<id>` 가 있으면 그 스킬은 이 설치가 고른 것이다.
|
|
2593
2797
|
continuousSkills: CONTINUOUS_SKILLS.map((s) => s.id).filter(
|
|
2594
|
-
(id) =>
|
|
2798
|
+
(id) => existsSync14(join15(projectDir, ".claude/skills", id))
|
|
2595
2799
|
)
|
|
2596
2800
|
});
|
|
2597
2801
|
if (next === existing) return false;
|
|
@@ -2611,7 +2815,7 @@ function recordAnchorBaseline(projectDir, anchor) {
|
|
|
2611
2815
|
...log.templates,
|
|
2612
2816
|
rootClaudeMd: {
|
|
2613
2817
|
path: HARNESS_ANCHOR_FILE,
|
|
2614
|
-
sha256: hashContent(
|
|
2818
|
+
sha256: hashContent(readFileSync13(anchor, "utf8"))
|
|
2615
2819
|
}
|
|
2616
2820
|
}
|
|
2617
2821
|
};
|
|
@@ -2622,16 +2826,45 @@ function recordAnchorBaseline(projectDir, anchor) {
|
|
|
2622
2826
|
}
|
|
2623
2827
|
function installedBundledSkills(projectDir) {
|
|
2624
2828
|
return INTERNAL_BUNDLED_SKILL_IDS.filter(
|
|
2625
|
-
(id) =>
|
|
2829
|
+
(id) => existsSync14(join15(projectDir, ".claude/skills", id)) || existsSync14(join15(projectDir, ".agents/skills", id))
|
|
2626
2830
|
);
|
|
2627
2831
|
}
|
|
2832
|
+
function installedCliTargets(log) {
|
|
2833
|
+
if (log === null) return ALL_CLI_TARGETS;
|
|
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"))));
|
|
2859
|
+
}
|
|
2628
2860
|
function refreshExternalCli(projectDir, harnessRoot) {
|
|
2629
2861
|
const log = readInstallLog(projectDir);
|
|
2630
2862
|
const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
|
|
2863
|
+
const presentBefore = sharedSkillIdsOnDisk(projectDir);
|
|
2631
2864
|
const result = runCliTransforms({
|
|
2632
2865
|
harnessRoot,
|
|
2633
2866
|
projectDir,
|
|
2634
|
-
cli:
|
|
2867
|
+
cli: installedCliTargets(log),
|
|
2635
2868
|
// 스킬은 **디스크에 있는 것만** 넘긴다. 스킬 *파일*은 전체 목록을 넘겨도 refreshOnly 가
|
|
2636
2869
|
// 없는 것을 건너뛰지만, `AGENTS.md` 의 상시 스킬 안내(ADR-085)는 이 목록 그대로 렌더돼
|
|
2637
2870
|
// `--without` 으로 뺀 스킬(#505)·opt-in 스킬을 "열어라"고 적었다(실측 2026-09-21). 판정은
|
|
@@ -2651,26 +2884,41 @@ function refreshExternalCli(projectDir, harnessRoot) {
|
|
|
2651
2884
|
} catch {
|
|
2652
2885
|
}
|
|
2653
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
|
+
}
|
|
2654
2899
|
return {
|
|
2655
2900
|
externalUpdated: result.externalUpdated,
|
|
2656
2901
|
externalBackedUp: result.externalBackedUp,
|
|
2657
|
-
externalForeignOwned: result.externalForeignOwned
|
|
2902
|
+
externalForeignOwned: result.externalForeignOwned,
|
|
2903
|
+
written: result.externalFiles.map((f) => f.path),
|
|
2904
|
+
skillsInstalled,
|
|
2905
|
+
skillsRestored
|
|
2658
2906
|
};
|
|
2659
2907
|
}
|
|
2660
2908
|
function isHarnessOwned2(ctx, rel, current) {
|
|
2661
2909
|
return isHarnessOwned(ctx.baseline, `${ctx.prefix}/${rel}`, current);
|
|
2662
2910
|
}
|
|
2663
2911
|
function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
2664
|
-
if (!
|
|
2912
|
+
if (!existsSync14(target) || !existsSync14(source)) return { updated: 0, backedUp: [] };
|
|
2665
2913
|
let updated = 0;
|
|
2666
2914
|
const backedUp = [];
|
|
2667
2915
|
for (const file of readdirSync3(target)) {
|
|
2668
2916
|
if (!file.endsWith(ext)) continue;
|
|
2669
|
-
const targetFile =
|
|
2670
|
-
const sourceFile =
|
|
2671
|
-
if (!
|
|
2672
|
-
const next =
|
|
2673
|
-
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");
|
|
2674
2922
|
if (current === next) continue;
|
|
2675
2923
|
if (!isHarnessOwned2(ctx, file, current)) {
|
|
2676
2924
|
backupFile(targetFile, now);
|
|
@@ -2682,7 +2930,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
|
2682
2930
|
return { updated, backedUp };
|
|
2683
2931
|
}
|
|
2684
2932
|
function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date(), foreignOf) {
|
|
2685
|
-
if (!
|
|
2933
|
+
if (!existsSync14(targetDir) || !existsSync14(sourceDir))
|
|
2686
2934
|
return { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [], pruned: [] };
|
|
2687
2935
|
let updated = 0;
|
|
2688
2936
|
const backedUp = [];
|
|
@@ -2691,27 +2939,27 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
|
|
|
2691
2939
|
const pruned = [];
|
|
2692
2940
|
for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
|
|
2693
2941
|
if (!skill.isDirectory()) continue;
|
|
2694
|
-
const targetSkill =
|
|
2942
|
+
const targetSkill = join15(targetDir, skill.name);
|
|
2695
2943
|
if (occupiedByNonDirectory(targetSkill)) {
|
|
2696
2944
|
skippedLinks.push(skill.name);
|
|
2697
2945
|
continue;
|
|
2698
2946
|
}
|
|
2699
|
-
if (!
|
|
2700
|
-
for (const rel of listFilesRecursive(
|
|
2701
|
-
const targetFile =
|
|
2947
|
+
if (!existsSync14(targetSkill)) continue;
|
|
2948
|
+
for (const rel of listFilesRecursive(join15(sourceDir, skill.name))) {
|
|
2949
|
+
const targetFile = join15(targetSkill, rel);
|
|
2702
2950
|
const foreign = foreignOf(`${skill.name}/${rel}`);
|
|
2703
2951
|
if (foreign !== null) {
|
|
2704
2952
|
if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
|
|
2705
2953
|
continue;
|
|
2706
2954
|
}
|
|
2707
|
-
const next =
|
|
2708
|
-
if (!
|
|
2955
|
+
const next = readFileSync13(join15(sourceDir, skill.name, rel), "utf8");
|
|
2956
|
+
if (!existsSync14(targetFile)) {
|
|
2709
2957
|
mkdirSync3(dirname3(targetFile), { recursive: true });
|
|
2710
2958
|
writeFileSync5(targetFile, next);
|
|
2711
2959
|
updated++;
|
|
2712
2960
|
continue;
|
|
2713
2961
|
}
|
|
2714
|
-
const current =
|
|
2962
|
+
const current = readFileSync13(targetFile, "utf8");
|
|
2715
2963
|
if (current === next) continue;
|
|
2716
2964
|
const recorded = baseline.get(`${skill.name}/${rel}`);
|
|
2717
2965
|
if (recorded === void 0 || hashContent(current) !== recorded) {
|
|
@@ -2721,13 +2969,13 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
|
|
|
2721
2969
|
writeFileSync5(targetFile, next);
|
|
2722
2970
|
updated++;
|
|
2723
2971
|
}
|
|
2724
|
-
const bundled = new Set(listFilesRecursive(
|
|
2972
|
+
const bundled = new Set(listFilesRecursive(join15(sourceDir, skill.name)));
|
|
2725
2973
|
for (const rel of listFilesRecursive(targetSkill)) {
|
|
2726
2974
|
if (bundled.has(rel) || basename4(rel).includes(".backup-")) continue;
|
|
2727
2975
|
if (foreignOf(`${skill.name}/${rel}`) !== null) continue;
|
|
2728
|
-
const targetFile =
|
|
2976
|
+
const targetFile = join15(targetSkill, rel);
|
|
2729
2977
|
const recorded = baseline.get(`${skill.name}/${rel}`);
|
|
2730
|
-
if (recorded === void 0 || hashContent(
|
|
2978
|
+
if (recorded === void 0 || hashContent(readFileSync13(targetFile, "utf8")) !== recorded) {
|
|
2731
2979
|
backupFile(targetFile, now);
|
|
2732
2980
|
backedUp.push(`${skill.name}/${rel}`);
|
|
2733
2981
|
}
|
|
@@ -2776,16 +3024,16 @@ function refreshSkillBaseline(projectDir, templatesDir) {
|
|
|
2776
3024
|
}
|
|
2777
3025
|
}
|
|
2778
3026
|
function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
2779
|
-
if (!
|
|
3027
|
+
if (!existsSync14(target) || !existsSync14(source)) return [];
|
|
2780
3028
|
const removed = [];
|
|
2781
3029
|
for (const file of readdirSync3(target)) {
|
|
2782
3030
|
if (!file.endsWith(ext)) continue;
|
|
2783
|
-
const sourceFile =
|
|
2784
|
-
if (!
|
|
2785
|
-
const targetFile =
|
|
3031
|
+
const sourceFile = join15(source, file);
|
|
3032
|
+
if (!existsSync14(sourceFile)) {
|
|
3033
|
+
const targetFile = join15(target, file);
|
|
2786
3034
|
if (!ctx.baseline.has(`${ctx.prefix}/${file}`)) continue;
|
|
2787
3035
|
try {
|
|
2788
|
-
const current =
|
|
3036
|
+
const current = readFileSync13(targetFile, "utf8");
|
|
2789
3037
|
if (!isHarnessOwned2(ctx, file, current)) backupFile(targetFile, now);
|
|
2790
3038
|
unlinkSync2(targetFile);
|
|
2791
3039
|
removed.push(file);
|
|
@@ -2796,8 +3044,8 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
|
|
|
2796
3044
|
return removed;
|
|
2797
3045
|
}
|
|
2798
3046
|
function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
|
|
2799
|
-
const target =
|
|
2800
|
-
if (!
|
|
3047
|
+
const target = join15(projectDir, ".mcp-allowlist");
|
|
3048
|
+
if (!existsSync14(target)) return null;
|
|
2801
3049
|
try {
|
|
2802
3050
|
const backup = backupFile(target, now);
|
|
2803
3051
|
unlinkSync2(target);
|
|
@@ -2809,7 +3057,7 @@ function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
|
|
|
2809
3057
|
function cleanStaleHookRefs(settingsPath, claudeDir) {
|
|
2810
3058
|
let settings;
|
|
2811
3059
|
try {
|
|
2812
|
-
settings = JSON.parse(
|
|
3060
|
+
settings = JSON.parse(readFileSync13(settingsPath, "utf8"));
|
|
2813
3061
|
} catch {
|
|
2814
3062
|
return [];
|
|
2815
3063
|
}
|
|
@@ -2836,7 +3084,7 @@ function cleanStaleHookRefs(settingsPath, claudeDir) {
|
|
|
2836
3084
|
function keepHookRef(hook, claudeDir, removed) {
|
|
2837
3085
|
const relPath = projectAnchoredRef(hook?.command ?? "", claudeDir);
|
|
2838
3086
|
if (relPath === void 0) return true;
|
|
2839
|
-
const exists =
|
|
3087
|
+
const exists = existsSync14(join15(claudeDir, relPath));
|
|
2840
3088
|
if (!exists && !removed.includes(relPath)) removed.push(relPath);
|
|
2841
3089
|
return exists;
|
|
2842
3090
|
}
|
|
@@ -2858,31 +3106,47 @@ function projectAnchoredRef(command, claudeDir) {
|
|
|
2858
3106
|
}
|
|
2859
3107
|
return void 0;
|
|
2860
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
|
+
}
|
|
2861
3116
|
|
|
2862
3117
|
// src/installer.ts
|
|
2863
3118
|
function runInstall(ctx) {
|
|
2864
3119
|
const { harnessRoot, projectDir, spec } = ctx;
|
|
2865
3120
|
const mode = ctx.mode ?? "fresh";
|
|
2866
|
-
const templatesDir =
|
|
2867
|
-
if (!
|
|
3121
|
+
const templatesDir = join16(harnessRoot, "templates");
|
|
3122
|
+
if (!existsSync15(templatesDir)) {
|
|
2868
3123
|
throw new Error(`Templates dir not found: ${templatesDir}`);
|
|
2869
3124
|
}
|
|
2870
|
-
const claudeDir =
|
|
3125
|
+
const claudeDir = join16(projectDir, ".claude");
|
|
2871
3126
|
const previousLog = readInstallLog(projectDir);
|
|
2872
|
-
const claudeWasSelected = previousLog
|
|
2873
|
-
if (mode === "update" && !
|
|
3127
|
+
const claudeWasSelected = previousLog !== null && installedClis(previousLog).includes("claude");
|
|
3128
|
+
if (mode === "update" && !existsSync15(claudeDir) && (previousLog === null || claudeWasSelected)) {
|
|
2874
3129
|
throw new Error(
|
|
2875
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}`
|
|
2876
3131
|
);
|
|
2877
3132
|
}
|
|
2878
|
-
const backupPath = resolveBackupPath(ctx, mode, claudeDir);
|
|
3133
|
+
const backupPath = resolveBackupPath(ctx, mode, claudeDir, previousLog);
|
|
2879
3134
|
if (mode === "update") {
|
|
2880
3135
|
return runUpdateInstall(ctx, templatesDir, backupPath);
|
|
2881
3136
|
}
|
|
2882
3137
|
const manifestSpec = buildManifestSpec(spec);
|
|
2883
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]));
|
|
2884
3140
|
const baselineExcluded = new Set(spec.baselineExclude ?? []);
|
|
2885
|
-
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
|
+
) : (
|
|
2886
3150
|
// claude 미선택이어도 CLI 중립 자산은 깔린다. manifest 전체가 `.claude/` baseline 안에서만
|
|
2887
3151
|
// 돌던 탓에 이 자산들이 claude 설치에만 도달했는데, **배포 룰 본문이 이 스크립트들을
|
|
2888
3152
|
// 호출 지점으로 지목한다** — 즉 없는 도구를 있다고 안내하고 있었다(#300 과 같은 형태).
|
|
@@ -2914,6 +3178,17 @@ function runInstall(ctx) {
|
|
|
2914
3178
|
previousExternal: previousLog?.externalFiles ?? [],
|
|
2915
3179
|
codexTrust: (spec.scope ?? "project") === "global" && spec.options.withCodexTrust
|
|
2916
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
|
+
});
|
|
2917
3192
|
const baseline = {
|
|
2918
3193
|
filesCopied: base.filesCopied,
|
|
2919
3194
|
dirsCopied: base.dirsCopied,
|
|
@@ -2930,21 +3205,23 @@ function runInstall(ctx) {
|
|
|
2930
3205
|
rootClaudeMd: base.rootClaudeMd,
|
|
2931
3206
|
// 외부 CLI 백업도 같은 줄에 노출한다 — 백업이 화면에 안 보이면 사용자는 자기 편집분이
|
|
2932
3207
|
// 어디 갔는지 알 수 없고, 그러면 백업은 있어도 없는 것과 같다 (ADR-046/047 과 같은 이유).
|
|
2933
|
-
backups: [...base.backups, ...externalBackups],
|
|
3208
|
+
backups: [...base.backups, ...externalBackups, ...linked.backupPaths],
|
|
2934
3209
|
baselineExcluded: base.excluded,
|
|
2935
3210
|
baselineExcludedOnDisk: base.excludedOnDisk,
|
|
2936
3211
|
// `.claude/` baseline 과 외부 CLI 산출물의 같은 판정을 **한 목록으로** 낸다.
|
|
2937
3212
|
baselineForeignOwned: [
|
|
2938
|
-
...base.foreignOwned,
|
|
2939
|
-
|
|
2940
|
-
|
|
3213
|
+
.../* @__PURE__ */ new Set([...base.foreignOwned, ...externalForeignOwned, ...linked.foreignOwned])
|
|
3214
|
+
],
|
|
3215
|
+
baselineLinked: linked.updated,
|
|
3216
|
+
baselineLinkedNotOurs: linked.notOurs
|
|
2941
3217
|
};
|
|
2942
3218
|
ctx.onProgress?.({ type: "baseline-complete", baseline });
|
|
2943
3219
|
const external = runExternalPhase(ctx);
|
|
2944
3220
|
const staleHookRefs = healStaleHookRefs(spec, projectDir);
|
|
2945
3221
|
writeInstallLogSafe(
|
|
2946
3222
|
ctx,
|
|
2947
|
-
|
|
3223
|
+
// 링크 본문의 기준선도 같은 필드다 — 같은 경로면 뒤(이번에 쓴 값)가 이긴다.
|
|
3224
|
+
[...externalFiles, ...linked.files],
|
|
2948
3225
|
external,
|
|
2949
3226
|
base.rootClaudeMdLog,
|
|
2950
3227
|
previousLog,
|
|
@@ -2955,12 +3232,13 @@ function runInstall(ctx) {
|
|
|
2955
3232
|
}
|
|
2956
3233
|
function healStaleHookRefs(spec, projectDir) {
|
|
2957
3234
|
if (!spec.cli.includes("claude")) return [];
|
|
2958
|
-
const settingsPath =
|
|
2959
|
-
if (!
|
|
2960
|
-
return cleanStaleHookRefs(settingsPath,
|
|
3235
|
+
const settingsPath = join16(projectDir, ".claude/settings.json");
|
|
3236
|
+
if (!existsSync15(settingsPath)) return [];
|
|
3237
|
+
return cleanStaleHookRefs(settingsPath, join16(projectDir, ".claude"));
|
|
2961
3238
|
}
|
|
2962
|
-
function resolveBackupPath(ctx, mode, claudeDir) {
|
|
2963
|
-
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);
|
|
2964
3242
|
if (!wantBackup) return null;
|
|
2965
3243
|
return mode === "update" ? copyBackupDir(claudeDir) : backupDir(claudeDir);
|
|
2966
3244
|
}
|
|
@@ -3017,7 +3295,8 @@ function emptyClaudeBaseline() {
|
|
|
3017
3295
|
backups: [],
|
|
3018
3296
|
excluded: [],
|
|
3019
3297
|
excludedOnDisk: [],
|
|
3020
|
-
foreignOwned: []
|
|
3298
|
+
foreignOwned: [],
|
|
3299
|
+
linkedSkills: []
|
|
3021
3300
|
};
|
|
3022
3301
|
}
|
|
3023
3302
|
function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselineExcluded) {
|
|
@@ -3029,12 +3308,12 @@ function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselin
|
|
|
3029
3308
|
continue;
|
|
3030
3309
|
}
|
|
3031
3310
|
if (!isCliNeutralTarget(entry.target)) continue;
|
|
3032
|
-
const source =
|
|
3033
|
-
if (!
|
|
3311
|
+
const source = join16(templatesDir, entry.source);
|
|
3312
|
+
if (!existsSync15(source)) {
|
|
3034
3313
|
result.skipped += 1;
|
|
3035
3314
|
continue;
|
|
3036
3315
|
}
|
|
3037
|
-
copyFile(source,
|
|
3316
|
+
copyFile(source, join16(projectDir, entry.target));
|
|
3038
3317
|
result.filesCopied += 1;
|
|
3039
3318
|
}
|
|
3040
3319
|
return result;
|
|
@@ -3046,14 +3325,28 @@ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
|
|
|
3046
3325
|
if (!POLICY_DIRS.some(({ dir, ext }) => rel.startsWith(`${dir}/`) && rel.endsWith(ext))) {
|
|
3047
3326
|
return null;
|
|
3048
3327
|
}
|
|
3049
|
-
if (!
|
|
3050
|
-
const current =
|
|
3051
|
-
if (current ===
|
|
3328
|
+
if (!existsSync15(target)) return null;
|
|
3329
|
+
const current = readFileSync14(target, "utf-8");
|
|
3330
|
+
if (current === readFileSync14(source, "utf-8")) return null;
|
|
3052
3331
|
const recorded = baseline.get(rel);
|
|
3053
3332
|
if (recorded !== void 0 && recorded === hashContent(current)) return null;
|
|
3054
3333
|
return backupFile(target);
|
|
3055
3334
|
}
|
|
3056
|
-
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) {
|
|
3057
3350
|
ensureProjectSkeleton(projectDir);
|
|
3058
3351
|
const result = emptyClaudeBaseline();
|
|
3059
3352
|
const manifest = buildManifest(manifestSpec);
|
|
@@ -3061,17 +3354,22 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
|
|
|
3061
3354
|
if (!entry.applies(manifestSpec)) {
|
|
3062
3355
|
continue;
|
|
3063
3356
|
}
|
|
3064
|
-
const target =
|
|
3357
|
+
const target = join16(projectDir, entry.target);
|
|
3065
3358
|
if (isBaselineExcluded(entry.target, baselineExcluded)) {
|
|
3066
3359
|
result.excluded.push(entry.target);
|
|
3067
|
-
if (
|
|
3360
|
+
if (existsSync15(target)) result.excludedOnDisk.push(entry.target);
|
|
3068
3361
|
continue;
|
|
3069
3362
|
}
|
|
3070
|
-
const source =
|
|
3071
|
-
if (!
|
|
3363
|
+
const source = join16(templatesDir, entry.source);
|
|
3364
|
+
if (!existsSync15(source)) {
|
|
3072
3365
|
result.skipped += 1;
|
|
3073
3366
|
continue;
|
|
3074
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
|
+
}
|
|
3075
3373
|
const foreign = foreignOwnedTarget(projectDir, entry.target);
|
|
3076
3374
|
if (foreign !== null) {
|
|
3077
3375
|
if (!result.foreignOwned.includes(foreign)) result.foreignOwned.push(foreign);
|
|
@@ -3079,7 +3377,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
|
|
|
3079
3377
|
}
|
|
3080
3378
|
if (entry.type === "file") {
|
|
3081
3379
|
if (entry.target === ".claude/settings.json") {
|
|
3082
|
-
const backup = backupFileIfChanged(target,
|
|
3380
|
+
const backup = backupFileIfChanged(target, readFileSync14(source, "utf-8"));
|
|
3083
3381
|
if (backup) {
|
|
3084
3382
|
result.backups.push(backup);
|
|
3085
3383
|
}
|
|
@@ -3092,6 +3390,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
|
|
|
3092
3390
|
copyFile(source, target);
|
|
3093
3391
|
result.filesCopied += 1;
|
|
3094
3392
|
} else {
|
|
3393
|
+
result.backups.push(...backupEditedSkillFiles(projectDir, entry.target, source, skillBase));
|
|
3095
3394
|
for (const foreign2 of copyDir(
|
|
3096
3395
|
source,
|
|
3097
3396
|
target,
|
|
@@ -3103,24 +3402,29 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
|
|
|
3103
3402
|
}
|
|
3104
3403
|
accumulateCategory(result.categories, entry);
|
|
3105
3404
|
}
|
|
3106
|
-
const hookDir =
|
|
3107
|
-
if (
|
|
3405
|
+
const hookDir = join16(projectDir, ".claude/hooks");
|
|
3406
|
+
if (existsSync15(hookDir)) {
|
|
3108
3407
|
chmodHooksSync(hookDir);
|
|
3109
3408
|
}
|
|
3110
3409
|
writeInstalledTracks(projectDir, manifestSpec.tracks);
|
|
3111
3410
|
const rootClaudeMd = writeRootClaudeMd(
|
|
3112
3411
|
projectDir,
|
|
3113
3412
|
manifestSpec.tracks,
|
|
3114
|
-
manifestSpec.selectedInternalSkills ?? []
|
|
3413
|
+
manifestSpec.selectedInternalSkills ?? [],
|
|
3414
|
+
harnessRoot
|
|
3115
3415
|
);
|
|
3116
|
-
result.rootClaudeMd = {
|
|
3416
|
+
result.rootClaudeMd = {
|
|
3417
|
+
tracks: manifestSpec.tracks,
|
|
3418
|
+
created: rootClaudeMd.created,
|
|
3419
|
+
seededFrom: rootClaudeMd.seededFrom
|
|
3420
|
+
};
|
|
3117
3421
|
result.rootClaudeMdLog = harnessAnchorLog(projectDir);
|
|
3118
3422
|
return result;
|
|
3119
3423
|
}
|
|
3120
3424
|
function harnessAnchorLog(projectDir) {
|
|
3121
|
-
const anchor =
|
|
3122
|
-
if (!
|
|
3123
|
-
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")) };
|
|
3124
3428
|
}
|
|
3125
3429
|
function writeEnvironmentFiles(projectDir, tracks) {
|
|
3126
3430
|
return {
|
|
@@ -3177,8 +3481,9 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
|
|
|
3177
3481
|
claudeDirMovedAside,
|
|
3178
3482
|
rootFiles
|
|
3179
3483
|
);
|
|
3180
|
-
const
|
|
3181
|
-
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")) : [];
|
|
3182
3487
|
const merged = mergeExternalFiles(ctx.projectDir, previousLog?.externalFiles, externalFiles);
|
|
3183
3488
|
writeInstallLog(ctx.projectDir, {
|
|
3184
3489
|
...log,
|
|
@@ -3194,11 +3499,11 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
|
|
|
3194
3499
|
}
|
|
3195
3500
|
}
|
|
3196
3501
|
function composeAndWriteMcp(harnessRoot, projectDir, spec) {
|
|
3197
|
-
const mcpPath =
|
|
3198
|
-
const created = !
|
|
3502
|
+
const mcpPath = join16(projectDir, ".mcp.json");
|
|
3503
|
+
const created = !existsSync15(mcpPath);
|
|
3199
3504
|
const composed = composeMcpJson({
|
|
3200
|
-
templateMcpPath:
|
|
3201
|
-
trackMapPath:
|
|
3505
|
+
templateMcpPath: join16(harnessRoot, "templates/mcp.json"),
|
|
3506
|
+
trackMapPath: join16(harnessRoot, "templates/track-mcp-map.tsv"),
|
|
3202
3507
|
existingPath: mcpPath,
|
|
3203
3508
|
tracks: spec.tracks
|
|
3204
3509
|
});
|
|
@@ -3251,24 +3556,26 @@ function accumulateCategory(cats, entry) {
|
|
|
3251
3556
|
}
|
|
3252
3557
|
}
|
|
3253
3558
|
function writeInstalledTracks(projectDir, tracks) {
|
|
3254
|
-
const path =
|
|
3559
|
+
const path = join16(projectDir, ".claude/.installed-tracks");
|
|
3255
3560
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
3256
3561
|
const sorted = [...new Set(tracks)].sort().join("\n");
|
|
3257
3562
|
writeFileSync6(path, `${sorted}
|
|
3258
3563
|
`);
|
|
3259
3564
|
}
|
|
3260
|
-
function writeRootClaudeMd(projectDir, tracks, continuousSkills) {
|
|
3261
|
-
const target =
|
|
3262
|
-
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;
|
|
3263
3569
|
const content = upsertHarnessImport(existing, {
|
|
3264
3570
|
projectName: basename5(projectDir),
|
|
3265
3571
|
tracks,
|
|
3266
|
-
continuousSkills
|
|
3572
|
+
continuousSkills,
|
|
3573
|
+
...seeded === null ? {} : { projectContext: seeded }
|
|
3267
3574
|
});
|
|
3268
3575
|
if (content !== existing) {
|
|
3269
3576
|
writeFileSync6(target, content);
|
|
3270
3577
|
}
|
|
3271
|
-
return { created: existing === null };
|
|
3578
|
+
return { created: existing === null, seededFrom: seeded === null ? null : "AGENTS.md" };
|
|
3272
3579
|
}
|
|
3273
3580
|
function chmodHooksSync(hookDir) {
|
|
3274
3581
|
for (const file of listHookFiles(hookDir)) {
|
|
@@ -3284,8 +3591,8 @@ function listHookFiles(hookDir) {
|
|
|
3284
3591
|
|
|
3285
3592
|
// src/commands/install-render.ts
|
|
3286
3593
|
init_esm_shims();
|
|
3287
|
-
import { existsSync as
|
|
3288
|
-
import { join as
|
|
3594
|
+
import { existsSync as existsSync16, readdirSync as readdirSync5 } from "fs";
|
|
3595
|
+
import { join as join17 } from "path";
|
|
3289
3596
|
|
|
3290
3597
|
// src/preset-recommend.ts
|
|
3291
3598
|
init_esm_shims();
|
|
@@ -3453,13 +3760,13 @@ function renderUpdateSummary(log, spec, report) {
|
|
|
3453
3760
|
function residentEntriesOnDisk(spec) {
|
|
3454
3761
|
const assetSpec = buildManifestSpec(spec);
|
|
3455
3762
|
const planned = buildManifest(assetSpec).filter((e2) => e2.applies(assetSpec));
|
|
3456
|
-
const agentsDir =
|
|
3457
|
-
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) => ({
|
|
3458
3765
|
source: `agents/${e2.name}`,
|
|
3459
3766
|
target: `.claude/agents/${e2.name}`,
|
|
3460
3767
|
// 배포판이 아니라 **이 프로젝트의 파일**을 잰다 — 사용자가 고친 descriptor 도,
|
|
3461
3768
|
// 배포판에 더는 없는 에이전트도 실제로 상주하는 것은 이쪽이다.
|
|
3462
|
-
file:
|
|
3769
|
+
file: join17(agentsDir, e2.name)
|
|
3463
3770
|
})) : [];
|
|
3464
3771
|
return [...planned.filter((e2) => !e2.target.startsWith(".claude/agents/")), ...onDisk];
|
|
3465
3772
|
}
|
|
@@ -3476,6 +3783,16 @@ function renderCliArtifacts(log, spec, report) {
|
|
|
3476
3783
|
} else if (report.codex || report.opencode) {
|
|
3477
3784
|
log(assetRow("success", "AGENTS.md", `from ${HARNESS_ANCHOR_FILE}`));
|
|
3478
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
|
+
}
|
|
3479
3796
|
if (report.codex) {
|
|
3480
3797
|
log(assetRow("success", ".codex/config.toml", "settings + [mcp_servers.*]"));
|
|
3481
3798
|
log(assetRow("success", ".codex/hooks/", `${report.codex.hookFiles.length} files`));
|
|
@@ -3637,6 +3954,20 @@ function formatAssetMeta(asset, version) {
|
|
|
3637
3954
|
return `internal \xB7 templates (${m.key})`;
|
|
3638
3955
|
}
|
|
3639
3956
|
}
|
|
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
|
+
}
|
|
3640
3971
|
function renderPhase1Rows(log, baseline, verbose = false) {
|
|
3641
3972
|
if (baseline.updateMode) {
|
|
3642
3973
|
if (baseline.backup) {
|
|
@@ -3648,8 +3979,30 @@ function renderPhase1Rows(log, baseline, verbose = false) {
|
|
|
3648
3979
|
for (const path of baseline.updateMode.installedNew) {
|
|
3649
3980
|
log(assetRow("success", path, "added by this release"));
|
|
3650
3981
|
}
|
|
3982
|
+
const withoutArgs = [];
|
|
3651
3983
|
for (const path of baseline.updateMode.restored) {
|
|
3652
|
-
|
|
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
|
+
);
|
|
3653
4006
|
}
|
|
3654
4007
|
if (baseline.updateMode.needsReinstall.length > 0) {
|
|
3655
4008
|
log(
|
|
@@ -3701,6 +4054,15 @@ function renderPhase1Rows(log, baseline, verbose = false) {
|
|
|
3701
4054
|
)
|
|
3702
4055
|
);
|
|
3703
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
|
+
}
|
|
3704
4066
|
if (baseline.updateMode.legacyAnchor) {
|
|
3705
4067
|
log(
|
|
3706
4068
|
assetRow(
|
|
@@ -3728,6 +4090,11 @@ function renderPhase1Rows(log, baseline, verbose = false) {
|
|
|
3728
4090
|
)
|
|
3729
4091
|
);
|
|
3730
4092
|
}
|
|
4093
|
+
linkedSkillRows(
|
|
4094
|
+
log,
|
|
4095
|
+
baseline.updateMode.skillsLinked ?? [],
|
|
4096
|
+
baseline.updateMode.skillsLinkedNotOurs ?? []
|
|
4097
|
+
);
|
|
3731
4098
|
if (baseline.updateMode.skillsSkippedLinks.length > 0) {
|
|
3732
4099
|
log(
|
|
3733
4100
|
assetRow(
|
|
@@ -3934,6 +4301,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
|
|
|
3934
4301
|
)
|
|
3935
4302
|
);
|
|
3936
4303
|
}
|
|
4304
|
+
linkedSkillRows(log, baseline.baselineLinked ?? [], baseline.baselineLinkedNotOurs ?? []);
|
|
3937
4305
|
if (baseline.baselineForeignOwned.length > 0) {
|
|
3938
4306
|
log(
|
|
3939
4307
|
assetRow(
|
|
@@ -3950,7 +4318,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
|
|
|
3950
4318
|
assetRow(
|
|
3951
4319
|
"success",
|
|
3952
4320
|
"CLAUDE.md (root)",
|
|
3953
|
-
baseline.rootClaudeMd
|
|
4321
|
+
rootClaudeMdMeta(baseline.rootClaudeMd, n),
|
|
3954
4322
|
TEMPLATES_COL
|
|
3955
4323
|
)
|
|
3956
4324
|
);
|
|
@@ -4017,6 +4385,14 @@ function shortenPath(p2) {
|
|
|
4017
4385
|
}
|
|
4018
4386
|
return p2;
|
|
4019
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
|
+
}
|
|
4020
4396
|
function formatCliPhaseTitle(targets) {
|
|
4021
4397
|
const labels = [];
|
|
4022
4398
|
if (targets.includes("codex")) labels.push("Codex");
|
|
@@ -4078,6 +4454,18 @@ function installAction(options, deps = {}) {
|
|
|
4078
4454
|
exit(1);
|
|
4079
4455
|
return;
|
|
4080
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) {
|
|
4081
4469
|
const forceInclude = normalizeRepeatable(options.with);
|
|
4082
4470
|
const forceExclude = normalizeRepeatable(options.without);
|
|
4083
4471
|
const baselineIds = new Set(
|
|
@@ -4104,7 +4492,7 @@ function installAction(options, deps = {}) {
|
|
|
4104
4492
|
const filteredInclude = forceInclude.filter((id) => validIds.has(id));
|
|
4105
4493
|
const filteredExclude = forceExclude.filter((id) => validIds.has(id));
|
|
4106
4494
|
const userOverride = filteredInclude.length > 0 || filteredExclude.length > 0 ? { forceInclude: filteredInclude, forceExclude: filteredExclude } : void 0;
|
|
4107
|
-
|
|
4495
|
+
return {
|
|
4108
4496
|
tracks: options.track ?? [],
|
|
4109
4497
|
...userOverride ? { userOverride } : {},
|
|
4110
4498
|
...baselineExclude.length > 0 ? { baselineExclude } : {},
|
|
@@ -4113,18 +4501,21 @@ function installAction(options, deps = {}) {
|
|
|
4113
4501
|
options: {
|
|
4114
4502
|
withCodexTrust: options.withCodexTrust === true
|
|
4115
4503
|
},
|
|
4116
|
-
cli:
|
|
4504
|
+
cli: cli2,
|
|
4117
4505
|
projectDir: resolve2(options.projectDir ?? process.cwd()),
|
|
4118
4506
|
scope: resolveScopeOption(options.scope, err)
|
|
4119
4507
|
};
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
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(" ");
|
|
4128
4519
|
}
|
|
4129
4520
|
function executeSpec(spec, deps = {}) {
|
|
4130
4521
|
const log = deps.log ?? console.log;
|
|
@@ -4206,13 +4597,16 @@ function registerInstallCommand(cli2) {
|
|
|
4206
4597
|
).option(
|
|
4207
4598
|
"--with-codex-trust",
|
|
4208
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"
|
|
4209
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));
|
|
4210
4604
|
}
|
|
4211
4605
|
|
|
4212
4606
|
// src/commands/list.ts
|
|
4213
4607
|
init_esm_shims();
|
|
4214
|
-
import { existsSync as
|
|
4215
|
-
import { join as
|
|
4608
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
|
|
4609
|
+
import { join as join18, resolve as resolve3 } from "path";
|
|
4216
4610
|
function listAction(options = {}, deps = {}) {
|
|
4217
4611
|
const log = deps.log ?? console.log;
|
|
4218
4612
|
const err = deps.err ?? console.error;
|
|
@@ -4231,7 +4625,7 @@ function listAction(options = {}, deps = {}) {
|
|
|
4231
4625
|
log(c.dim(` installed: ${installLog.installedAt}`));
|
|
4232
4626
|
log(c.dim(` scope: ${installLog.scope}`));
|
|
4233
4627
|
log(c.dim(` tracks: ${installLog.spec.tracks.join(", ") || "(none)"}`));
|
|
4234
|
-
log(c.dim(` cli: ${installLog.
|
|
4628
|
+
log(c.dim(` cli: ${installedClis(installLog).join(", ") || "(none)"}`));
|
|
4235
4629
|
log("");
|
|
4236
4630
|
log(c.bold(` Assets (${installLog.assets.length})`));
|
|
4237
4631
|
if (installLog.assets.length === 0) {
|
|
@@ -4275,8 +4669,8 @@ function formatTemplateRows(log, projectDir) {
|
|
|
4275
4669
|
const rows = [` ${c.dim(dirs.join(" "))}`];
|
|
4276
4670
|
const rootMd = log.templates.rootClaudeMd;
|
|
4277
4671
|
if (rootMd) {
|
|
4278
|
-
const path =
|
|
4279
|
-
const modified =
|
|
4672
|
+
const path = join18(projectDir, rootMd.path);
|
|
4673
|
+
const modified = existsSync17(path) && hashContent(readFileSync15(path, "utf8")) !== rootMd.sha256;
|
|
4280
4674
|
rows.push(
|
|
4281
4675
|
modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
|
|
4282
4676
|
);
|
|
@@ -4284,7 +4678,7 @@ function formatTemplateRows(log, projectDir) {
|
|
|
4284
4678
|
return rows;
|
|
4285
4679
|
}
|
|
4286
4680
|
function formatRootFileRows(rootFiles, projectDir) {
|
|
4287
|
-
const present = rootFiles.filter((f) =>
|
|
4681
|
+
const present = rootFiles.filter((f) => existsSync17(join18(projectDir, f.path)));
|
|
4288
4682
|
const width = Math.max(0, ...present.map((f) => f.path.length));
|
|
4289
4683
|
return present.map(
|
|
4290
4684
|
(f) => ` ${c.dim(padDisplay(f.path, width))} ${c.dim(f.change === "created" ? "\uC0DD\uC131" : "\uBCD1\uD569")} ${c.dim(f.notes.join(" / "))}`
|
|
@@ -4302,15 +4696,64 @@ function registerListCommand(cli2) {
|
|
|
4302
4696
|
init_esm_shims();
|
|
4303
4697
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
4304
4698
|
import {
|
|
4305
|
-
existsSync as
|
|
4699
|
+
existsSync as existsSync18,
|
|
4306
4700
|
lstatSync as lstatSync2,
|
|
4307
4701
|
readdirSync as readdirSync6,
|
|
4308
|
-
readFileSync as
|
|
4702
|
+
readFileSync as readFileSync16,
|
|
4309
4703
|
rmdirSync as rmdirSync2,
|
|
4310
4704
|
rmSync,
|
|
4311
4705
|
writeFileSync as writeFileSync7
|
|
4312
4706
|
} from "fs";
|
|
4313
|
-
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
|
+
}
|
|
4314
4757
|
|
|
4315
4758
|
// src/uninstall-interactive.ts
|
|
4316
4759
|
init_esm_shims();
|
|
@@ -5285,109 +5728,605 @@ ${a}
|
|
|
5285
5728
|
};
|
|
5286
5729
|
var jt = `${e("gray", $)} `;
|
|
5287
5730
|
|
|
5288
|
-
// src/
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
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
|
+
};
|
|
5295
5746
|
}
|
|
5296
|
-
function
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
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");
|
|
5311
5764
|
}
|
|
5312
|
-
|
|
5313
|
-
const
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
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);
|
|
5325
5877
|
}
|
|
5326
|
-
|
|
5327
|
-
const
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
`
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
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)]
|
|
5334
5891
|
);
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
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
|
+
});
|
|
5338
5900
|
}
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
return { ok: false, reason: "cancelled" };
|
|
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);
|
|
5345
5906
|
}
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
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);
|
|
5349
5913
|
}
|
|
5350
|
-
const
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
"",
|
|
5355
|
-
|
|
5356
|
-
].join("\n")
|
|
5357
|
-
);
|
|
5358
|
-
if (!ok) {
|
|
5359
|
-
prompts.cancel("Cancelled.");
|
|
5360
|
-
return { ok: false, reason: "cancelled" };
|
|
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
|
+
);
|
|
5361
5920
|
}
|
|
5362
|
-
return { ok: true, options: { projectDir, only: picked.join(",") } };
|
|
5363
5921
|
}
|
|
5364
|
-
|
|
5365
|
-
|
|
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) {
|
|
6279
|
+
prompts.cancel("Cancelled.");
|
|
6280
|
+
return { ok: false, reason: "cancelled" };
|
|
6281
|
+
}
|
|
6282
|
+
return { ok: true, options: { projectDir, only: picked.join(",") } };
|
|
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
|
+
}
|
|
6302
|
+
function defaultUninstallPrompts() {
|
|
6303
|
+
return {
|
|
5366
6304
|
intro: (m) => ge(m),
|
|
5367
6305
|
outro: (m) => ye(m),
|
|
5368
6306
|
cancel: (m) => me(m),
|
|
5369
|
-
selectMode: async (
|
|
6307
|
+
selectMode: async (choices, message) => {
|
|
5370
6308
|
const r = await Ee({
|
|
5371
|
-
message
|
|
5372
|
-
options:
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
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 }))
|
|
5384
6323
|
});
|
|
5385
6324
|
return q(r) ? null : r;
|
|
5386
6325
|
},
|
|
5387
6326
|
selectAssets: async (rows) => {
|
|
5388
6327
|
if (rows.length === 0) return [];
|
|
5389
6328
|
const r = await ve({
|
|
5390
|
-
message: "
|
|
6329
|
+
message: "Assets to remove (Space toggle \xB7 Enter confirm \xB7 ESC cancel)",
|
|
5391
6330
|
options: rows.map((x) => ({ value: x.value, label: x.label, hint: x.hint })),
|
|
5392
6331
|
required: false
|
|
5393
6332
|
});
|
|
@@ -5396,13 +6335,25 @@ function defaultUninstallPrompts() {
|
|
|
5396
6335
|
confirm: async (summary) => {
|
|
5397
6336
|
const r = await ue({ message: `${summary}
|
|
5398
6337
|
|
|
5399
|
-
|
|
6338
|
+
Proceed?`, initialValue: false });
|
|
5400
6339
|
return q(r) ? null : r;
|
|
5401
6340
|
}
|
|
5402
6341
|
};
|
|
5403
6342
|
}
|
|
5404
6343
|
|
|
5405
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
|
+
}
|
|
5406
6357
|
function uninstallAction(options, deps = {}) {
|
|
5407
6358
|
const log = deps.log ?? console.log;
|
|
5408
6359
|
const err = deps.err ?? console.error;
|
|
@@ -5410,6 +6361,8 @@ function uninstallAction(options, deps = {}) {
|
|
|
5410
6361
|
const spawn = deps.spawn ?? defaultSpawn2;
|
|
5411
6362
|
const rm = deps.rm ?? defaultRm;
|
|
5412
6363
|
const writeLog = deps.writeLog ?? writeInstallLog;
|
|
6364
|
+
const moveAside = deps.moveAside ?? backupDir;
|
|
6365
|
+
const harnessRoot = (deps.resolveHarnessRoot ?? defaultHarnessRoot)();
|
|
5413
6366
|
const projectDir = resolve4(options.projectDir ?? process.cwd());
|
|
5414
6367
|
const installLog = readInstallLog(projectDir);
|
|
5415
6368
|
if (!installLog) {
|
|
@@ -5418,6 +6371,13 @@ function uninstallAction(options, deps = {}) {
|
|
|
5418
6371
|
exit(1);
|
|
5419
6372
|
return;
|
|
5420
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
|
+
}
|
|
5421
6381
|
const selectedIds = parseOnly(options.only);
|
|
5422
6382
|
if (options.only !== void 0 && selectedIds === null) {
|
|
5423
6383
|
err(status.failure(c.red("ERROR: --only \uC5D0 \uC790\uC0B0 id \uAC00 \uC5C6\uB2E4")));
|
|
@@ -5438,7 +6398,14 @@ function uninstallAction(options, deps = {}) {
|
|
|
5438
6398
|
const plan = planReverse(targetAssets, spawn);
|
|
5439
6399
|
for (const line of headerLines(installLog, selectedIds, targetAssets.length)) log(line);
|
|
5440
6400
|
if (options.dryRun) {
|
|
5441
|
-
for (const line of dryRunLines(
|
|
6401
|
+
for (const line of dryRunLines(
|
|
6402
|
+
plan,
|
|
6403
|
+
installLog,
|
|
6404
|
+
projectDir,
|
|
6405
|
+
keepTemplates,
|
|
6406
|
+
rootFiles,
|
|
6407
|
+
harnessRoot
|
|
6408
|
+
)) {
|
|
5442
6409
|
log(line);
|
|
5443
6410
|
}
|
|
5444
6411
|
exit(0);
|
|
@@ -5447,12 +6414,16 @@ function uninstallAction(options, deps = {}) {
|
|
|
5447
6414
|
const logSurvives = selectedIds !== null;
|
|
5448
6415
|
const { succeeded, failed, removedIds } = executeReverse(plan, log, logSurvives);
|
|
5449
6416
|
if (!keepTemplates) {
|
|
5450
|
-
const { rootClaudeMdKept, importStripped, external } = removeTemplates(
|
|
6417
|
+
const { rootClaudeMdKept, importStripped, external, moved } = removeTemplates(
|
|
5451
6418
|
installLog,
|
|
5452
6419
|
projectDir,
|
|
5453
|
-
rm
|
|
6420
|
+
{ rm, moveAside },
|
|
6421
|
+
harnessRoot
|
|
5454
6422
|
);
|
|
5455
|
-
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
|
+
}
|
|
5456
6427
|
for (const line of externalRemovalLines(external)) log(line);
|
|
5457
6428
|
if (importStripped) {
|
|
5458
6429
|
log(` ${status.success("CLAUDE.md \u2014 harness @import removed (\uBCF8\uBB38 \uBCF4\uC874)")}`);
|
|
@@ -5489,863 +6460,715 @@ function summarize(r) {
|
|
|
5489
6460
|
if (r.failed > 0)
|
|
5490
6461
|
return {
|
|
5491
6462
|
line: c.yellow(`uninstall finished with ${r.failed} skip(s) (${r.succeeded} ok)`),
|
|
5492
|
-
code: 1
|
|
5493
|
-
};
|
|
5494
|
-
if (r.nothingDone)
|
|
5495
|
-
return {
|
|
5496
|
-
line: c.yellow("\uC544\uBB34\uAC83\uB3C4 \uC790\uB3D9 \uC81C\uAC70\uB418\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uC704 \uC548\uB0B4\uB97C \uB530\uB77C \uC9C1\uC811 \uCC98\uB9AC\uD574\uC57C \uD55C\uB2E4"),
|
|
5497
|
-
code: 1
|
|
5498
|
-
};
|
|
5499
|
-
if (r.logWriteFailed)
|
|
5500
|
-
return {
|
|
5501
|
-
line: c.yellow("\uC790\uC0B0\uC740 \uC81C\uAC70\uB410\uC73C\uB098 install log \uB97C \uAC31\uC2E0\uD558\uC9C0 \uBABB\uD588\uB2E4 (\uAE30\uB85D\uC774 \uC2E4\uC81C\uC640 \uB2E4\uB974\uB2E4)"),
|
|
5502
|
-
code: 1
|
|
5503
|
-
};
|
|
5504
|
-
return { line: status.success(c.green(`uninstall complete (${r.succeeded} asset(s))`)), code: 0 };
|
|
5505
|
-
}
|
|
5506
|
-
function headerLines(installLog, selectedIds, targetCount) {
|
|
5507
|
-
return [
|
|
5508
|
-
"",
|
|
5509
|
-
c.bold("uzys-agent-harness \xB7 uninstall"),
|
|
5510
|
-
"",
|
|
5511
|
-
c.dim(` installed: ${installLog.installedAt}`),
|
|
5512
|
-
c.dim(` scope: ${installLog.scope}`),
|
|
5513
|
-
c.dim(
|
|
5514
|
-
selectedIds ? ` assets: ${targetCount} selected of ${installLog.assets.length} (--only)` : ` assets: ${installLog.assets.length}`
|
|
5515
|
-
),
|
|
5516
|
-
""
|
|
5517
|
-
];
|
|
5518
|
-
}
|
|
5519
|
-
function executeReverse(plan, log, logSurvives) {
|
|
5520
|
-
let succeeded = 0;
|
|
5521
|
-
let failed = 0;
|
|
5522
|
-
const removedIds = [];
|
|
5523
|
-
for (const step of plan.reverseSteps) {
|
|
5524
|
-
const result = step.execute();
|
|
5525
|
-
if (result.ok) {
|
|
5526
|
-
log(` ${status.success(step.label)}`);
|
|
5527
|
-
removedIds.push(step.assetId);
|
|
5528
|
-
succeeded++;
|
|
5529
|
-
} else {
|
|
5530
|
-
log(` ${c.yellow("\u2298")} ${step.label} (${result.message ?? "failed"})`);
|
|
5531
|
-
failed++;
|
|
5532
|
-
}
|
|
5533
|
-
}
|
|
5534
|
-
const tail = logSurvives ? "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0" : "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C";
|
|
5535
|
-
for (const asset of plan.noReversePath) {
|
|
5536
|
-
log(` ${c.yellow("\u2298")} ${asset.id} (${asset.method}) \u2014 ${tail}`);
|
|
5537
|
-
}
|
|
5538
|
-
return { succeeded, failed, removedIds };
|
|
5539
|
-
}
|
|
5540
|
-
function settleLog(ctx, io) {
|
|
5541
|
-
const { installLog, projectDir, selectedIds, removedIds } = ctx;
|
|
5542
|
-
if (selectedIds) {
|
|
5543
|
-
const remaining = installLog.assets.filter((a) => !removedIds.includes(a.id));
|
|
5544
|
-
try {
|
|
5545
|
-
io.writeLog(projectDir, { ...installLog, assets: remaining });
|
|
5546
|
-
io.log(` ${status.success(`install log updated (${remaining.length} asset(s) remain)`)}`);
|
|
5547
|
-
} catch (e2) {
|
|
5548
|
-
io.err(status.failure(c.red(`ERROR: install log \uAC31\uC2E0 \uC2E4\uD328 \u2014 ${installLogPath(projectDir)}`)));
|
|
5549
|
-
io.err(c.dim(` ${e2 instanceof Error ? e2.message : String(e2)}`));
|
|
5550
|
-
io.err(c.dim(` \uC2E4\uC81C\uB85C \uC81C\uAC70\uB41C \uC790\uC0B0: ${removedIds.join(", ") || "(\uC5C6\uC74C)"}`));
|
|
5551
|
-
return true;
|
|
5552
|
-
}
|
|
5553
|
-
} else {
|
|
5554
|
-
io.rm(join16(projectDir, INSTALL_LOG_DIR));
|
|
5555
|
-
io.rm(legacyInstallLogPath(projectDir));
|
|
5556
|
-
io.log(` ${status.success("install log removed")}`);
|
|
5557
|
-
}
|
|
5558
|
-
return false;
|
|
5559
|
-
}
|
|
5560
|
-
function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles) {
|
|
5561
|
-
const lines = [c.yellow("[DRY RUN] reverse list (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
|
|
5562
|
-
if (plan.reverseSteps.length === 0) {
|
|
5563
|
-
lines.push(c.dim(" (no project-scope assets to reverse)"));
|
|
5564
|
-
}
|
|
5565
|
-
lines.push(...plan.reverseSteps.map((s) => ` \u25CB ${s.label}`));
|
|
5566
|
-
lines.push(
|
|
5567
|
-
...plan.noReversePath.map(
|
|
5568
|
-
(a) => c.dim(` \u2298 ${a.id} (${a.method}) \u2014 \uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0`)
|
|
5569
|
-
)
|
|
5570
|
-
);
|
|
5571
|
-
if (!keepTemplates) {
|
|
5572
|
-
lines.push(` \u25CB remove templates: ${formatTemplateList(installLog)}`);
|
|
5573
|
-
const rootMd = installLog.templates.rootClaudeMd;
|
|
5574
|
-
if (rootMd) {
|
|
5575
|
-
lines.push(
|
|
5576
|
-
rootClaudeMdModified(installLog, projectDir) ? ` \u25CB keep ${rootMd.path} (modified since install \u2014 preserved)` : ` \u25CB remove ${rootMd.path}`
|
|
5577
|
-
);
|
|
5578
|
-
}
|
|
5579
|
-
if (hasRootImport(projectDir)) {
|
|
5580
|
-
lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
|
|
5581
|
-
}
|
|
5582
|
-
lines.push(...previewExternalLines(installLog, projectDir));
|
|
5583
|
-
}
|
|
5584
|
-
lines.push(...advisoryLines(plan, projectDir, rootFiles), "");
|
|
5585
|
-
return lines;
|
|
5586
|
-
}
|
|
5587
|
-
function advisoryLines(plan, projectDir, rootFiles) {
|
|
5588
|
-
const lines = [];
|
|
5589
|
-
if (plan.globalAdvisories.length > 0) {
|
|
5590
|
-
lines.push(
|
|
5591
|
-
"",
|
|
5592
|
-
c.yellow(
|
|
5593
|
-
`[GLOBAL] ${plan.globalAdvisories.length} asset(s) at scope=global \u2014 manual removal required (D16):`
|
|
5594
|
-
)
|
|
5595
|
-
);
|
|
5596
|
-
for (const adv of plan.globalAdvisories) {
|
|
5597
|
-
lines.push(c.dim(` \xB7 ${adv.asset.id} (${adv.asset.method})`), c.dim(` ${adv.command}`));
|
|
5598
|
-
}
|
|
5599
|
-
}
|
|
5600
|
-
lines.push(...rootFileAdvisoryLines(rootFiles, projectDir));
|
|
5601
|
-
return lines;
|
|
5602
|
-
}
|
|
5603
|
-
function rootFileAdvisoryLines(rootFiles, projectDir) {
|
|
5604
|
-
const present = rootFiles.filter((f) => existsSync17(join16(projectDir, f.path)));
|
|
5605
|
-
if (present.length === 0) return [];
|
|
5606
|
-
return [
|
|
5607
|
-
"",
|
|
5608
|
-
c.yellow("[ROOT] `.claude/` \uBC16\uC5D0 \uB0A8\uB294 \uAC83 (\uC790\uB3D9\uC73C\uB85C \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4):"),
|
|
5609
|
-
...present.flatMap((f) => [
|
|
5610
|
-
c.dim(
|
|
5611
|
-
` \xB7 ${f.path} \u2014 ${f.change === "created" ? "\uD558\uB124\uC2A4\uAC00 \uC0DD\uC131 (\uC218\uC815\uD55C \uC801 \uC5C6\uC73C\uBA74 \uC0AD\uC81C\uD574\uB3C4 \uC548\uC804)" : "\uAE30\uC874 \uC0AC\uC6A9\uC790 \uD30C\uC77C\uC5D0 \uBCD1\uD569 (\uC9C1\uC811 \uD655\uC778 \uD544\uC694)"}`
|
|
5612
|
-
),
|
|
5613
|
-
c.dim(` ${f.notes.join(" / ")}`)
|
|
5614
|
-
])
|
|
5615
|
-
];
|
|
5616
|
-
}
|
|
5617
|
-
function planReverse(assets, spawn) {
|
|
5618
|
-
const reverseSteps = [];
|
|
5619
|
-
const globalAdvisories = [];
|
|
5620
|
-
const noReversePath = [];
|
|
5621
|
-
for (const asset of assets) {
|
|
5622
|
-
if (asset.scope === "global") {
|
|
5623
|
-
globalAdvisories.push({ asset, command: buildGlobalAdvisoryCmd(asset) });
|
|
5624
|
-
continue;
|
|
5625
|
-
}
|
|
5626
|
-
const step = buildProjectReverseStep(asset, spawn);
|
|
5627
|
-
if (step) reverseSteps.push(step);
|
|
5628
|
-
else noReversePath.push(asset);
|
|
5629
|
-
}
|
|
5630
|
-
return { reverseSteps, globalAdvisories, noReversePath };
|
|
5631
|
-
}
|
|
5632
|
-
function buildProjectReverseStep(asset, spawn) {
|
|
5633
|
-
switch (asset.method) {
|
|
5634
|
-
case "plugin": {
|
|
5635
|
-
const pluginId = asset.detail.pluginId ?? asset.id;
|
|
5636
|
-
return {
|
|
5637
|
-
assetId: asset.id,
|
|
5638
|
-
label: `claude plugin uninstall --scope project ${pluginId}`,
|
|
5639
|
-
execute: () => {
|
|
5640
|
-
const r = spawn("claude", ["plugin", "uninstall", "--scope", "project", pluginId]);
|
|
5641
|
-
return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
|
|
5642
|
-
}
|
|
5643
|
-
};
|
|
5644
|
-
}
|
|
5645
|
-
case "skill": {
|
|
5646
|
-
const source = asset.detail.source ?? asset.id;
|
|
5647
|
-
return {
|
|
5648
|
-
assetId: asset.id,
|
|
5649
|
-
label: `npx skills remove ${source}`,
|
|
5650
|
-
execute: () => {
|
|
5651
|
-
const r = spawn("npx", [skillsCliSpec(), "remove", source, "--yes"]);
|
|
5652
|
-
return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
|
|
5653
|
-
}
|
|
5654
|
-
};
|
|
5655
|
-
}
|
|
5656
|
-
case "npm": {
|
|
5657
|
-
const pkg = asset.detail.pkg ?? asset.id;
|
|
5658
|
-
return {
|
|
5659
|
-
assetId: asset.id,
|
|
5660
|
-
label: `npm uninstall --save-dev ${pkg}`,
|
|
5661
|
-
execute: () => {
|
|
5662
|
-
const r = spawn("npm", ["uninstall", "--save-dev", pkg]);
|
|
5663
|
-
return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
|
|
5664
|
-
}
|
|
5665
|
-
};
|
|
5666
|
-
}
|
|
5667
|
-
case "npx-run":
|
|
5668
|
-
return null;
|
|
5669
|
-
case "shell-script":
|
|
5670
|
-
return null;
|
|
5671
|
-
case "internal":
|
|
5672
|
-
return null;
|
|
5673
|
-
}
|
|
5674
|
-
}
|
|
5675
|
-
function unknownIds(installLog, selectedIds) {
|
|
5676
|
-
const known = new Set(installLog.assets.map((a) => a.id));
|
|
5677
|
-
return selectedIds.filter((id) => !known.has(id));
|
|
5678
|
-
}
|
|
5679
|
-
function parseOnly(only) {
|
|
5680
|
-
if (!only) return null;
|
|
5681
|
-
const ids = only.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
5682
|
-
return ids.length > 0 ? ids : null;
|
|
5683
|
-
}
|
|
5684
|
-
function buildGlobalAdvisoryCmd(asset) {
|
|
5685
|
-
switch (asset.method) {
|
|
5686
|
-
case "plugin": {
|
|
5687
|
-
const pid = asset.detail.pluginId ?? asset.id;
|
|
5688
|
-
return `claude plugin uninstall --scope user ${pid}`;
|
|
5689
|
-
}
|
|
5690
|
-
case "skill": {
|
|
5691
|
-
const s = asset.detail.source ?? asset.id;
|
|
5692
|
-
return `npx skills remove -g ${s}`;
|
|
5693
|
-
}
|
|
5694
|
-
case "npm": {
|
|
5695
|
-
const pkg = asset.detail.pkg ?? asset.id;
|
|
5696
|
-
return `npm uninstall -g ${pkg}`;
|
|
5697
|
-
}
|
|
5698
|
-
case "npx-run":
|
|
5699
|
-
case "shell-script":
|
|
5700
|
-
case "internal":
|
|
5701
|
-
return "(no standard reverse \u2014 manual)";
|
|
5702
|
-
}
|
|
6463
|
+
code: 1
|
|
6464
|
+
};
|
|
6465
|
+
if (r.nothingDone)
|
|
6466
|
+
return {
|
|
6467
|
+
line: c.yellow("\uC544\uBB34\uAC83\uB3C4 \uC790\uB3D9 \uC81C\uAC70\uB418\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uC704 \uC548\uB0B4\uB97C \uB530\uB77C \uC9C1\uC811 \uCC98\uB9AC\uD574\uC57C \uD55C\uB2E4"),
|
|
6468
|
+
code: 1
|
|
6469
|
+
};
|
|
6470
|
+
if (r.logWriteFailed)
|
|
6471
|
+
return {
|
|
6472
|
+
line: c.yellow("\uC790\uC0B0\uC740 \uC81C\uAC70\uB410\uC73C\uB098 install log \uB97C \uAC31\uC2E0\uD558\uC9C0 \uBABB\uD588\uB2E4 (\uAE30\uB85D\uC774 \uC2E4\uC81C\uC640 \uB2E4\uB974\uB2E4)"),
|
|
6473
|
+
code: 1
|
|
6474
|
+
};
|
|
6475
|
+
return { line: status.success(c.green(`uninstall complete (${r.succeeded} asset(s))`)), code: 0 };
|
|
5703
6476
|
}
|
|
5704
|
-
function
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
return { rootClaudeMdKept: false, importStripped, external };
|
|
6477
|
+
function headerLines(installLog, selectedIds, targetCount) {
|
|
6478
|
+
return [
|
|
6479
|
+
"",
|
|
6480
|
+
c.bold("uzys-agent-harness \xB7 uninstall"),
|
|
6481
|
+
"",
|
|
6482
|
+
c.dim(` installed: ${installLog.installedAt}`),
|
|
6483
|
+
c.dim(` scope: ${installLog.scope}`),
|
|
6484
|
+
c.dim(
|
|
6485
|
+
selectedIds ? ` assets: ${targetCount} selected of ${installLog.assets.length} (--only)` : ` assets: ${installLog.assets.length}`
|
|
6486
|
+
),
|
|
6487
|
+
""
|
|
6488
|
+
];
|
|
5717
6489
|
}
|
|
5718
|
-
function
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
if (
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
6490
|
+
function executeReverse(plan, log, logSurvives) {
|
|
6491
|
+
let succeeded = 0;
|
|
6492
|
+
let failed = 0;
|
|
6493
|
+
const removedIds = [];
|
|
6494
|
+
for (const step of plan.reverseSteps) {
|
|
6495
|
+
const result = step.execute();
|
|
6496
|
+
if (result.ok) {
|
|
6497
|
+
log(` ${status.success(step.label)}`);
|
|
6498
|
+
removedIds.push(step.assetId);
|
|
6499
|
+
succeeded++;
|
|
6500
|
+
} else {
|
|
6501
|
+
log(` ${c.yellow("\u2298")} ${step.label} (${result.message ?? "failed"})`);
|
|
6502
|
+
failed++;
|
|
5728
6503
|
}
|
|
5729
|
-
rm(abs);
|
|
5730
|
-
removed.push(path);
|
|
5731
6504
|
}
|
|
5732
|
-
|
|
5733
|
-
|
|
6505
|
+
const tail = logSurvives ? "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0" : "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C";
|
|
6506
|
+
for (const asset of plan.noReversePath) {
|
|
6507
|
+
log(` ${c.yellow("\u2298")} ${asset.id} (${asset.method}) \u2014 ${tail}`);
|
|
6508
|
+
}
|
|
6509
|
+
return { succeeded, failed, removedIds };
|
|
5734
6510
|
}
|
|
5735
|
-
function
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
const
|
|
5739
|
-
if (rel === "" || rel.startsWith(`..${sep2}`) || rel === "..") return;
|
|
6511
|
+
function settleLog(ctx, io) {
|
|
6512
|
+
const { installLog, projectDir, selectedIds, removedIds } = ctx;
|
|
6513
|
+
if (selectedIds) {
|
|
6514
|
+
const remaining = installLog.assets.filter((a) => !removedIds.includes(a.id));
|
|
5740
6515
|
try {
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
} catch {
|
|
5744
|
-
|
|
6516
|
+
io.writeLog(projectDir, { ...installLog, assets: remaining });
|
|
6517
|
+
io.log(` ${status.success(`install log updated (${remaining.length} asset(s) remain)`)}`);
|
|
6518
|
+
} catch (e2) {
|
|
6519
|
+
io.err(status.failure(c.red(`ERROR: install log \uAC31\uC2E0 \uC2E4\uD328 \u2014 ${installLogPath(projectDir)}`)));
|
|
6520
|
+
io.err(c.dim(` ${e2 instanceof Error ? e2.message : String(e2)}`));
|
|
6521
|
+
io.err(c.dim(` \uC2E4\uC81C\uB85C \uC81C\uAC70\uB41C \uC790\uC0B0: ${removedIds.join(", ") || "(\uC5C6\uC74C)"}`));
|
|
6522
|
+
return true;
|
|
5745
6523
|
}
|
|
5746
|
-
|
|
6524
|
+
} else {
|
|
6525
|
+
io.rm(join19(projectDir, INSTALL_LOG_DIR));
|
|
6526
|
+
io.rm(legacyInstallLogPath(projectDir));
|
|
6527
|
+
io.log(` ${status.success("install log removed")}`);
|
|
5747
6528
|
}
|
|
6529
|
+
return false;
|
|
5748
6530
|
}
|
|
5749
|
-
function
|
|
5750
|
-
const
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
try {
|
|
5754
|
-
writeFileSync7(join16(projectDir, "CLAUDE.md"), stripped, "utf8");
|
|
5755
|
-
return true;
|
|
5756
|
-
} catch {
|
|
5757
|
-
return false;
|
|
6531
|
+
function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles, harnessRoot) {
|
|
6532
|
+
const lines = [c.yellow("[DRY RUN] reverse list (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
|
|
6533
|
+
if (plan.reverseSteps.length === 0) {
|
|
6534
|
+
lines.push(c.dim(" (no project-scope assets to reverse)"));
|
|
5758
6535
|
}
|
|
5759
|
-
}
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
6536
|
+
lines.push(...plan.reverseSteps.map((s) => ` \u25CB ${s.label}`));
|
|
6537
|
+
lines.push(
|
|
6538
|
+
...plan.noReversePath.map(
|
|
6539
|
+
(a) => c.dim(` \u2298 ${a.id} (${a.method}) \u2014 \uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0`)
|
|
6540
|
+
)
|
|
6541
|
+
);
|
|
6542
|
+
if (!keepTemplates) {
|
|
6543
|
+
const dirs = recordedTemplateDirs(installLog);
|
|
6544
|
+
for (const rel of dirs) {
|
|
6545
|
+
if (existsSync18(join19(projectDir, rel))) lines.push(MOVE_ASIDE_PREVIEW(rel));
|
|
5769
6546
|
}
|
|
5770
|
-
|
|
6547
|
+
if (dirs.length === 0) lines.push(" \u25CB remove templates: (none)");
|
|
6548
|
+
const rootMd = installLog.templates.rootClaudeMd;
|
|
6549
|
+
if (rootMd) {
|
|
6550
|
+
lines.push(
|
|
6551
|
+
rootClaudeMdModified(installLog, projectDir) ? ` \u25CB keep ${rootMd.path} (modified since install \u2014 preserved)` : ` \u25CB remove ${rootMd.path}`
|
|
6552
|
+
);
|
|
6553
|
+
}
|
|
6554
|
+
if (hasRootImport(projectDir)) {
|
|
6555
|
+
lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
|
|
6556
|
+
}
|
|
6557
|
+
lines.push(...previewExternalLines(installLog, projectDir, harnessRoot, dirs));
|
|
5771
6558
|
}
|
|
5772
|
-
|
|
6559
|
+
lines.push(...advisoryLines(plan, projectDir, rootFiles), "");
|
|
5773
6560
|
return lines;
|
|
5774
6561
|
}
|
|
5775
|
-
function
|
|
6562
|
+
function advisoryLines(plan, projectDir, rootFiles) {
|
|
5776
6563
|
const lines = [];
|
|
5777
|
-
if (
|
|
5778
|
-
lines.push(` ${status.success(`CLI outputs removed: ${external.removed.length} file(s)`)}`);
|
|
5779
|
-
}
|
|
5780
|
-
for (const path of external.kept) {
|
|
6564
|
+
if (plan.globalAdvisories.length > 0) {
|
|
5781
6565
|
lines.push(
|
|
5782
|
-
|
|
6566
|
+
"",
|
|
6567
|
+
c.yellow(
|
|
6568
|
+
`[GLOBAL] ${plan.globalAdvisories.length} asset(s) at scope=global \u2014 manual removal required (D16):`
|
|
6569
|
+
)
|
|
5783
6570
|
);
|
|
6571
|
+
for (const adv of plan.globalAdvisories) {
|
|
6572
|
+
lines.push(c.dim(` \xB7 ${adv.asset.id} (${adv.asset.method})`), c.dim(` ${adv.command}`));
|
|
6573
|
+
}
|
|
5784
6574
|
}
|
|
6575
|
+
lines.push(...rootFileAdvisoryLines(rootFiles, projectDir));
|
|
5785
6576
|
return lines;
|
|
5786
6577
|
}
|
|
5787
|
-
function
|
|
5788
|
-
const
|
|
5789
|
-
|
|
6578
|
+
function rootFileAdvisoryLines(rootFiles, projectDir) {
|
|
6579
|
+
const present = rootFiles.filter((f) => existsSync18(join19(projectDir, f.path)));
|
|
6580
|
+
if (present.length === 0) return [];
|
|
6581
|
+
return [
|
|
6582
|
+
"",
|
|
6583
|
+
c.yellow("[ROOT] `.claude/` \uBC16\uC5D0 \uB0A8\uB294 \uAC83 (\uC790\uB3D9\uC73C\uB85C \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4):"),
|
|
6584
|
+
...present.flatMap((f) => [
|
|
6585
|
+
c.dim(
|
|
6586
|
+
` \xB7 ${f.path} \u2014 ${f.change === "created" ? "\uD558\uB124\uC2A4\uAC00 \uC0DD\uC131 (\uC218\uC815\uD55C \uC801 \uC5C6\uC73C\uBA74 \uC0AD\uC81C\uD574\uB3C4 \uC548\uC804)" : "\uAE30\uC874 \uC0AC\uC6A9\uC790 \uD30C\uC77C\uC5D0 \uBCD1\uD569 (\uC9C1\uC811 \uD655\uC778 \uD544\uC694)"}`
|
|
6587
|
+
),
|
|
6588
|
+
c.dim(` ${f.notes.join(" / ")}`)
|
|
6589
|
+
])
|
|
6590
|
+
];
|
|
5790
6591
|
}
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
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);
|
|
5797
6607
|
return null;
|
|
5798
6608
|
}
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
return hashContent(readFileSync15(path, "utf8")) !== rootMd.sha256;
|
|
5806
|
-
}
|
|
5807
|
-
function formatTemplateList(log) {
|
|
5808
|
-
const items = [log.templates.claudeDir];
|
|
5809
|
-
if (log.templates.codexDir) items.push(log.templates.codexDir);
|
|
5810
|
-
if (log.templates.opencodeDir) items.push(log.templates.opencodeDir);
|
|
5811
|
-
return items.join(", ");
|
|
5812
|
-
}
|
|
5813
|
-
function defaultSpawn2(cmd, args) {
|
|
5814
|
-
return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
|
|
5815
|
-
}
|
|
5816
|
-
function defaultRm(path) {
|
|
5817
|
-
if (existsSync17(path)) {
|
|
5818
|
-
rmSync(path, { recursive: true, force: true });
|
|
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;
|
|
5819
6615
|
}
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
"[Mode] Keep `.claude/`, `.codex/`, `.opencode/` templates (remove only external assets)"
|
|
5827
|
-
).option(
|
|
5828
|
-
"--only <ids>",
|
|
5829
|
-
"[Scope] Remove only these assets (comma-separated ids from `agent-harness list`). Templates untouched"
|
|
5830
|
-
).option("--yes", "[Mode] Skip the interactive picker and remove everything (non-interactive)").action(async (options) => {
|
|
5831
|
-
await dispatchUninstall(options);
|
|
5832
|
-
});
|
|
5833
|
-
}
|
|
5834
|
-
function shouldRunInteractive(options, isTty) {
|
|
5835
|
-
if (!isTty) return false;
|
|
5836
|
-
if (options.yes || options.dryRun) return false;
|
|
5837
|
-
return options.only === void 0;
|
|
5838
|
-
}
|
|
5839
|
-
async function dispatchUninstall(options) {
|
|
5840
|
-
if (!shouldRunInteractive(options, Boolean(process.stdin.isTTY))) {
|
|
5841
|
-
uninstallAction(options);
|
|
5842
|
-
return;
|
|
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;
|
|
5843
6622
|
}
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
status.failure(c.red(`ERROR: install log not found at ${installLogPath(projectDir)}`))
|
|
5850
|
-
);
|
|
5851
|
-
console.error(c.dim(" Nothing installed here by agent-harness."));
|
|
5852
|
-
process.exit(1);
|
|
5853
|
-
}
|
|
5854
|
-
return;
|
|
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;
|
|
5855
6628
|
}
|
|
5856
|
-
|
|
6629
|
+
return target;
|
|
5857
6630
|
}
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
init_esm_shims();
|
|
5861
|
-
import { resolve as resolve5 } from "path";
|
|
5862
|
-
|
|
5863
|
-
// src/state.ts
|
|
5864
|
-
init_esm_shims();
|
|
5865
|
-
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
|
|
5866
|
-
import { join as join17 } from "path";
|
|
5867
|
-
var META_FILE = ".claude/.installed-tracks";
|
|
5868
|
-
var LEGACY_SIGNATURES = [
|
|
5869
|
-
{ rule: "htmx.md", track: "ssr-htmx" },
|
|
5870
|
-
{ rule: "nextjs.md", track: "ssr-nextjs" },
|
|
5871
|
-
{ rule: "data-analysis.md", track: "data" },
|
|
5872
|
-
{ rule: "pyside6.md", track: "data" },
|
|
5873
|
-
{ rule: "cli-development.md", track: "tooling" }
|
|
5874
|
-
];
|
|
5875
|
-
function detectInstallState(projectDir) {
|
|
5876
|
-
const claudeDir = join17(projectDir, ".claude");
|
|
5877
|
-
const hasClaudeDir = existsSync18(claudeDir);
|
|
5878
|
-
if (hasClaudeDir) {
|
|
5879
|
-
const metaPath = join17(projectDir, META_FILE);
|
|
5880
|
-
if (existsSync18(metaPath)) {
|
|
5881
|
-
const tracks2 = readMetafile(metaPath);
|
|
5882
|
-
return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
|
|
5883
|
-
}
|
|
5884
|
-
const tracks = inferFromLegacySignatures(projectDir);
|
|
5885
|
-
return { state: "existing", tracks, source: "legacy", hasClaudeDir: true };
|
|
5886
|
-
}
|
|
5887
|
-
const log = readInstallLog(projectDir);
|
|
5888
|
-
if (log) {
|
|
5889
|
-
return {
|
|
5890
|
-
state: "existing",
|
|
5891
|
-
tracks: tracksFromLog(log.spec.tracks),
|
|
5892
|
-
source: "install-log",
|
|
5893
|
-
hasClaudeDir: false
|
|
5894
|
-
};
|
|
5895
|
-
}
|
|
5896
|
-
return { state: "new", tracks: [], source: "none", hasClaudeDir: false };
|
|
6631
|
+
function underAny(path, owned) {
|
|
6632
|
+
return owned.some((o) => o.endsWith("/") ? path.startsWith(o) : path === o);
|
|
5897
6633
|
}
|
|
5898
|
-
function
|
|
5899
|
-
|
|
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
|
+
};
|
|
5900
6647
|
}
|
|
5901
|
-
function
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
}
|
|
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
|
+
);
|
|
5909
6671
|
}
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
function inferFromLegacySignatures(projectDir) {
|
|
5913
|
-
const rulesDir = join17(projectDir, ".claude/rules");
|
|
5914
|
-
if (!existsSync18(rulesDir)) {
|
|
5915
|
-
return [];
|
|
6672
|
+
if (plan.importBlock && hasRootImport(projectDir)) {
|
|
6673
|
+
lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
|
|
5916
6674
|
}
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
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;
|
|
5922
6687
|
}
|
|
5923
|
-
|
|
6688
|
+
io.rm(join19(projectDir, rootMd.path));
|
|
6689
|
+
io.log(` ${status.success(`${rootMd.path} removed`)}`);
|
|
6690
|
+
return true;
|
|
5924
6691
|
}
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
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);
|
|
5939
6726
|
return;
|
|
5940
6727
|
}
|
|
5941
|
-
const
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
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);
|
|
5947
6752
|
return;
|
|
5948
6753
|
}
|
|
5949
|
-
log(
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
err,
|
|
5953
|
-
exit,
|
|
5954
|
-
mode: "update"
|
|
5955
|
-
});
|
|
5956
|
-
}
|
|
5957
|
-
function registerUpdateCommand(cli2) {
|
|
5958
|
-
cli2.command(
|
|
5959
|
-
"update",
|
|
5960
|
-
"Refresh installed policy files (rules / agents / commands / hooks / skills)"
|
|
5961
|
-
).option("--project-dir <path>", "[Project] Target project directory", {
|
|
5962
|
-
default: process.cwd()
|
|
5963
|
-
}).option(
|
|
5964
|
-
"--only <group>",
|
|
5965
|
-
`[Scope] Update only this group (repeatable): ${UPDATE_GROUPS.join(" | ")}`
|
|
5966
|
-
).action((options) => {
|
|
5967
|
-
updateAction(options);
|
|
5968
|
-
});
|
|
6754
|
+
io.log("");
|
|
6755
|
+
io.log(status.success(c.green(`${target} removed (remaining: ${remaining.join(", ")})`)));
|
|
6756
|
+
io.exit(0);
|
|
5969
6757
|
}
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
init_esm_shims();
|
|
5979
|
-
function buildRouterChoices(state) {
|
|
5980
|
-
const detected = state.tracks.length > 0 ? state.tracks.join(", ") : "(none detected)";
|
|
5981
|
-
return [
|
|
5982
|
-
{
|
|
5983
|
-
value: "add",
|
|
5984
|
-
label: "Add a new Track",
|
|
5985
|
-
hint: `Current: ${detected}`,
|
|
5986
|
-
enabled: true
|
|
5987
|
-
},
|
|
5988
|
-
{
|
|
5989
|
-
value: "update",
|
|
5990
|
-
label: "Update policy files (auto-backup)",
|
|
5991
|
-
// v26.126.0 (R-3a) — skills 가 목록에 들어왔다. 이 문구가 곧 update 의 광고이고
|
|
5992
|
-
// (update 는 위저드로만 도달한다) 실동작과 어긋나면 그 자체로 거짓출하다.
|
|
5993
|
-
hint: "Refresh rules / agents / commands / hooks / skills \u2014 your edits are backed up",
|
|
5994
|
-
enabled: true
|
|
5995
|
-
},
|
|
5996
|
-
{
|
|
5997
|
-
value: "remove",
|
|
5998
|
-
label: "Remove a Track (unsupported)",
|
|
5999
|
-
hint: "Manual edit of .claude/ required \u2014 not automated",
|
|
6000
|
-
enabled: false
|
|
6001
|
-
},
|
|
6002
|
-
{
|
|
6003
|
-
value: "reinstall",
|
|
6004
|
-
label: "Reinstall (backs up current .claude/ first)",
|
|
6005
|
-
hint: "Use when state is corrupted",
|
|
6006
|
-
enabled: true
|
|
6007
|
-
},
|
|
6008
|
-
{
|
|
6009
|
-
value: "exit",
|
|
6010
|
-
label: "Exit",
|
|
6011
|
-
enabled: true
|
|
6758
|
+
function planReverse(assets, spawn) {
|
|
6759
|
+
const reverseSteps = [];
|
|
6760
|
+
const globalAdvisories = [];
|
|
6761
|
+
const noReversePath = [];
|
|
6762
|
+
for (const asset of assets) {
|
|
6763
|
+
if (asset.scope === "global") {
|
|
6764
|
+
globalAdvisories.push({ asset, command: buildGlobalAdvisoryCmd(asset) });
|
|
6765
|
+
continue;
|
|
6012
6766
|
}
|
|
6013
|
-
|
|
6767
|
+
const step = buildProjectReverseStep(asset, spawn);
|
|
6768
|
+
if (step) reverseSteps.push(step);
|
|
6769
|
+
else noReversePath.push(asset);
|
|
6770
|
+
}
|
|
6771
|
+
return { reverseSteps, globalAdvisories, noReversePath };
|
|
6014
6772
|
}
|
|
6015
|
-
function
|
|
6016
|
-
|
|
6017
|
-
|
|
6773
|
+
function buildProjectReverseStep(asset, spawn) {
|
|
6774
|
+
switch (asset.method) {
|
|
6775
|
+
case "plugin": {
|
|
6776
|
+
const pluginId = asset.detail.pluginId ?? asset.id;
|
|
6777
|
+
return {
|
|
6778
|
+
assetId: asset.id,
|
|
6779
|
+
label: `claude plugin uninstall --scope project ${pluginId}`,
|
|
6780
|
+
execute: () => {
|
|
6781
|
+
const r = spawn("claude", ["plugin", "uninstall", "--scope", "project", pluginId]);
|
|
6782
|
+
return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
|
|
6783
|
+
}
|
|
6784
|
+
};
|
|
6785
|
+
}
|
|
6786
|
+
case "skill": {
|
|
6787
|
+
const source = asset.detail.source ?? asset.id;
|
|
6788
|
+
return {
|
|
6789
|
+
assetId: asset.id,
|
|
6790
|
+
label: `npx skills remove ${source}`,
|
|
6791
|
+
execute: () => {
|
|
6792
|
+
const r = spawn("npx", [skillsCliSpec(), "remove", source, "--yes"]);
|
|
6793
|
+
return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
|
|
6794
|
+
}
|
|
6795
|
+
};
|
|
6796
|
+
}
|
|
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
|
+
};
|
|
6807
|
+
}
|
|
6808
|
+
case "npx-run":
|
|
6809
|
+
return null;
|
|
6810
|
+
case "shell-script":
|
|
6811
|
+
return null;
|
|
6812
|
+
case "internal":
|
|
6813
|
+
return null;
|
|
6018
6814
|
}
|
|
6019
|
-
const trackList = state.tracks.length > 0 ? state.tracks.join(", ") : "(no tracks resolved)";
|
|
6020
|
-
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";
|
|
6021
|
-
return `Existing install detected ${sourceLabel}. Tracks: ${trackList}.`;
|
|
6022
6815
|
}
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
var WIZARD_TOTAL = 6;
|
|
6027
|
-
var WIZARD = {
|
|
6028
|
-
TRACKS: { current: 1, total: WIZARD_TOTAL },
|
|
6029
|
-
CLI: { current: 2, total: WIZARD_TOTAL },
|
|
6030
|
-
TARGETS: { current: 3, total: WIZARD_TOTAL },
|
|
6031
|
-
SCOPE: { current: 4, total: WIZARD_TOTAL },
|
|
6032
|
-
CONFIRM: { current: 5, total: WIZARD_TOTAL },
|
|
6033
|
-
INSTALL: { current: 6, total: WIZARD_TOTAL }
|
|
6034
|
-
};
|
|
6035
|
-
function stepLabel(step, suffix) {
|
|
6036
|
-
if (!step) return suffix;
|
|
6037
|
-
return `Step ${step.current}/${step.total} \u2014 ${suffix}`;
|
|
6816
|
+
function unknownIds(installLog, selectedIds) {
|
|
6817
|
+
const known = new Set(installLog.assets.map((a) => a.id));
|
|
6818
|
+
return selectedIds.filter((id) => !known.has(id));
|
|
6038
6819
|
}
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
data: "data \u2014 Python data / DuckDB / PySide6",
|
|
6050
|
-
executive: "executive \u2014 proposals / DD / pitch (no agent-skills)",
|
|
6051
|
-
full: "full \u2014 all dev capabilities",
|
|
6052
|
-
"project-management": "project-management \u2014 PM / Scrum / Jira / Confluence",
|
|
6053
|
-
"growth-marketing": "growth-marketing \u2014 Growth / Marketing / Content"
|
|
6054
|
-
};
|
|
6055
|
-
var VISIBLE_OPTION_DEFS = [];
|
|
6056
|
-
var CLI_BASE_LABELS = {
|
|
6057
|
-
claude: "Claude Code",
|
|
6058
|
-
codex: "Codex (OpenAI)",
|
|
6059
|
-
opencode: "OpenCode (anomalyco)",
|
|
6060
|
-
antigravity: "Antigravity (Google)"
|
|
6061
|
-
};
|
|
6062
|
-
var INSTALL_TARGET_PAGES = [
|
|
6063
|
-
// 2026-08-16 — 트랙이 고르는 자산을 **맨 앞에서** 전부 보여준다. 그전까지 이 항목들은 화면에
|
|
6064
|
-
// 한 번도 안 나온 채 깔렸고, 사용자는 설치가 끝난 뒤 요약에서 처음 봤다.
|
|
6065
|
-
//
|
|
6066
|
-
// 2페이지로 나눈 근거는 실측이다: 최대 트랙(`full`)이 32항목(당시 실측 — rules 6 · agents 9 · hooks 3 ·
|
|
6067
|
-
// skills 14)이라 clack groupMultiselect 의 "페이지당 ≤ ~30행" 제약을 한 페이지로는 못 지킨다.
|
|
6068
|
-
// 가르는 축은 역할이다 — 정책·가드가 한쪽, 실행 능력이 다른 쪽.
|
|
6069
|
-
{ label: "Track baseline \u2014 Rules & Hooks (\uC815\uCC45\xB7\uAC00\uB4DC)", cats: [], baseline: ["rules", "hooks"] },
|
|
6070
|
-
{
|
|
6071
|
-
label: "Track baseline \u2014 Agents & Skills (\uC5D0\uC774\uC804\uD2B8\xB7\uC2A4\uD0AC)",
|
|
6072
|
-
cats: [],
|
|
6073
|
-
baseline: ["agents", "skills"]
|
|
6074
|
-
},
|
|
6075
|
-
{ label: "Dev Core (Frontend \xB7 Backend \xB7 Data)", cats: ["frontend", "backend", "data"] },
|
|
6076
|
-
{ label: "Dev Tools (Security \xB7 Quality \xB7 Understanding)", cats: ["dev-tools", "understanding"] },
|
|
6077
|
-
{ label: "Business (PM \xB7 Executive \xB7 Documents)", cats: ["business"] },
|
|
6078
|
-
// v26.85.0 — 코드-퍼스트 비주얼/미디어 제작 (용도별 섹션, 전부 opt-in).
|
|
6079
|
-
{ label: "Visual & Media (Slides \xB7 Diagrams \xB7 Motion \xB7 Video)", cats: ["visual-media"] },
|
|
6080
|
-
{ label: "Workflow", cats: ["workflow"] }
|
|
6081
|
-
];
|
|
6082
|
-
var BASELINE_TITLES = {
|
|
6083
|
-
rules: "Rules \u2014 \uC0C1\uC2DC \uC0C1\uC8FC\uD558\uB294 \uB300\uC6D0\uCE59 (4 CLI \uC804\uBD80\uC5D0 \uC124\uCE58)",
|
|
6084
|
-
hooks: "Hooks \u2014 \uACB0\uC815\uB860\uC801 \uAC00\uB4DC (\uD574\uC81C\uD558\uBA74 settings.json \uBC30\uC120\uB3C4 \uD568\uAED8 \uBE60\uC9C4\uB2E4)",
|
|
6085
|
-
agents: "Agents \u2014 \uB3C5\uB9BD \uB808\uC778 (\uB9AC\uBDF0\xB7\uAC80\uC99D)",
|
|
6086
|
-
skills: "Skills \u2014 \uD544\uC694\uD560 \uB54C \uC5F4\uB9AC\uB294 \uC791\uC5C5 \uC808\uCC28"
|
|
6087
|
-
};
|
|
6088
|
-
function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Set(), baselineKinds = [], baselineTargets = []) {
|
|
6089
|
-
const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
|
|
6090
|
-
const groups = {};
|
|
6091
|
-
const flatItems = [];
|
|
6092
|
-
for (const kind of baselineKinds) {
|
|
6093
|
-
const items = baselineTargets.filter((t) => t.kind === kind).map((t) => ({
|
|
6094
|
-
value: t.id,
|
|
6095
|
-
label: ` ${t.name}${installedMark(t.id)}`,
|
|
6096
|
-
...t.hint ? { hint: t.hint } : {}
|
|
6097
|
-
}));
|
|
6098
|
-
if (items.length === 0) continue;
|
|
6099
|
-
groups[BASELINE_TITLES[kind]] = items;
|
|
6100
|
-
flatItems.push(...items);
|
|
6101
|
-
}
|
|
6102
|
-
for (const cat of cats) {
|
|
6103
|
-
const items = [];
|
|
6104
|
-
for (const o of VISIBLE_OPTION_DEFS.filter((d2) => d2.category === cat)) {
|
|
6105
|
-
items.push({
|
|
6106
|
-
value: `option:${o.key}`,
|
|
6107
|
-
// v26.62.3 — group header 와 옵션 사이 시각 hierarchy 강화. label prefix 4 space.
|
|
6108
|
-
label: ` ${o.label} [${o.source}]${installedMark(`option:${o.key}`)}`,
|
|
6109
|
-
hint: o.hint
|
|
6110
|
-
});
|
|
6111
|
-
}
|
|
6112
|
-
const tierOrder = { official: 0, vetted: 1, experimental: 2 };
|
|
6113
|
-
const catAssets = [...EXTERNAL_ASSETS.filter((x) => x.category === cat)].sort(
|
|
6114
|
-
(a, b2) => tierOrder[assetTrustTier(a.id)] - tierOrder[assetTrustTier(b2.id)]
|
|
6115
|
-
);
|
|
6116
|
-
for (const a of catAssets) {
|
|
6117
|
-
const tier = assetTrustTier(a.id);
|
|
6118
|
-
const badge = tier === "official" ? " \u2605 official" : tier === "experimental" ? " \u26A0 experimental (opt-in)" : "";
|
|
6119
|
-
items.push({
|
|
6120
|
-
value: `asset:${a.id}`,
|
|
6121
|
-
label: ` ${a.id} [${a.source}]${badge}${installedMark(`asset:${a.id}`)}`,
|
|
6122
|
-
hint: a.description
|
|
6123
|
-
});
|
|
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;
|
|
6824
|
+
}
|
|
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}`;
|
|
6124
6830
|
}
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
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)";
|
|
6130
6843
|
}
|
|
6131
|
-
return { groups, flatItems };
|
|
6132
6844
|
}
|
|
6133
|
-
function
|
|
6134
|
-
const
|
|
6135
|
-
|
|
6136
|
-
|
|
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 }] : [];
|
|
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));
|
|
6137
6858
|
}
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6859
|
+
return { rootClaudeMdKept: false, importStripped, external, moved };
|
|
6860
|
+
}
|
|
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;
|
|
6144
6868
|
}
|
|
6145
6869
|
}
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
return
|
|
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 }) };
|
|
6150
6874
|
}
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
{
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
});
|
|
6178
|
-
if (q(result)) return null;
|
|
6179
|
-
return [...result].sort(
|
|
6180
|
-
(a, b2) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b2]
|
|
6181
|
-
);
|
|
6182
|
-
},
|
|
6183
|
-
selectAction: async (state) => {
|
|
6184
|
-
const result = await Ee({
|
|
6185
|
-
message: summarizeState(state),
|
|
6186
|
-
options: buildRouterChoices(state).map((c3) => {
|
|
6187
|
-
const label = c3.enabled ? c3.label : `${c3.label} [disabled]`;
|
|
6188
|
-
return {
|
|
6189
|
-
value: c3.value,
|
|
6190
|
-
label,
|
|
6191
|
-
...c3.hint ? { hint: c3.hint } : {},
|
|
6192
|
-
...c3.enabled ? {} : { disabled: true }
|
|
6193
|
-
};
|
|
6194
|
-
})
|
|
6195
|
-
});
|
|
6196
|
-
return q(result) ? null : result;
|
|
6197
|
-
},
|
|
6198
|
-
/**
|
|
6199
|
-
* v26.64.0 (ADR-020) — Installation scope select. Default Project (D16 — no global write).
|
|
6200
|
-
* Global 은 사용자 명시 opt-in 시에만.
|
|
6201
|
-
*/
|
|
6202
|
-
selectScope: async (initial = "project", step) => {
|
|
6203
|
-
const result = await Ee({
|
|
6204
|
-
message: stepLabel(step, "Installation scope"),
|
|
6205
|
-
initialValue: initial,
|
|
6206
|
-
options: [
|
|
6207
|
-
{
|
|
6208
|
-
value: "project",
|
|
6209
|
-
label: "Project",
|
|
6210
|
-
hint: "Install in current directory (committed with your project)"
|
|
6211
|
-
},
|
|
6212
|
-
{
|
|
6213
|
-
value: "global",
|
|
6214
|
-
label: "Global",
|
|
6215
|
-
hint: "Write to ~/.claude/, ~/.codex/, npm -g (shared across all projects)"
|
|
6216
|
-
}
|
|
6217
|
-
]
|
|
6218
|
-
});
|
|
6219
|
-
return q(result) ? null : result;
|
|
6220
|
-
},
|
|
6221
|
-
selectUpdateGroups: async () => {
|
|
6222
|
-
const result = await ve({
|
|
6223
|
-
message: "What to update (space = toggle \xB7 your edits are always backed up)",
|
|
6224
|
-
options: [
|
|
6225
|
-
{ value: "skills", label: "Skills", hint: "installed bundled skills \u2192 latest" },
|
|
6226
|
-
{
|
|
6227
|
-
value: "new-skills",
|
|
6228
|
-
label: "New skills",
|
|
6229
|
-
hint: "bundled skills added since your install"
|
|
6230
|
-
},
|
|
6231
|
-
{
|
|
6232
|
-
value: "rules",
|
|
6233
|
-
label: "Rules \xB7 agents \xB7 commands",
|
|
6234
|
-
hint: "your edited files \u2192 *.backup-<time>"
|
|
6235
|
-
},
|
|
6236
|
-
{
|
|
6237
|
-
value: "anchor",
|
|
6238
|
-
label: "CLAUDE-uzys-harness.md anchor",
|
|
6239
|
-
hint: "your edited anchor \u2192 *.backup-<time>"
|
|
6240
|
-
},
|
|
6241
|
-
{
|
|
6242
|
-
value: "hooks",
|
|
6243
|
-
label: "Hooks \xB7 settings.json",
|
|
6244
|
-
hint: "stale hook refs cleaned"
|
|
6245
|
-
},
|
|
6246
|
-
{
|
|
6247
|
-
value: "external",
|
|
6248
|
-
label: "Codex / OpenCode / Antigravity + external skills",
|
|
6249
|
-
hint: "rendered artifacts + npx skills"
|
|
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);
|
|
6250
6901
|
}
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
}
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
},
|
|
6266
|
-
selectInstallTargets: async (initialChecked, step, recap) => {
|
|
6267
|
-
const pages = INSTALL_TARGET_PAGES;
|
|
6268
|
-
const baselineTargets = withBaselineHints(listBaselineTargets({ tracks: recap?.tracks ?? [] }));
|
|
6269
|
-
const initialSet = new Set(initialChecked);
|
|
6270
|
-
const collected = new Set(initialChecked);
|
|
6271
|
-
const recapLine = recap ? `Tracks: ${recap.tracks.join(", ")} \xB7 CLIs: ${recap.cli.join(", ")}` : "";
|
|
6272
|
-
const installedSet = new Set(recap?.installed ?? []);
|
|
6273
|
-
process.stdout.write("\x1B[?1049h");
|
|
6274
|
-
let resultIds = null;
|
|
6275
|
-
let aborted = false;
|
|
6902
|
+
continue;
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
rm(abs);
|
|
6906
|
+
removed.push(path);
|
|
6907
|
+
}
|
|
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;
|
|
6276
6916
|
try {
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
selectableGroups: false
|
|
6315
|
-
};
|
|
6316
|
-
const result = await pe(groupOpts);
|
|
6317
|
-
if (q(result)) {
|
|
6318
|
-
if (pageIdx === 0) {
|
|
6319
|
-
aborted = true;
|
|
6320
|
-
break;
|
|
6321
|
-
}
|
|
6322
|
-
dir = -1;
|
|
6323
|
-
pageIdx--;
|
|
6324
|
-
continue;
|
|
6325
|
-
}
|
|
6326
|
-
for (const it3 of flatItems) collected.delete(it3.value);
|
|
6327
|
-
for (const v2 of result) collected.add(v2);
|
|
6328
|
-
dir = 1;
|
|
6329
|
-
pageIdx++;
|
|
6917
|
+
if (readdirSync6(cursor).length > 0) return;
|
|
6918
|
+
rmdirSync2(cursor);
|
|
6919
|
+
} catch {
|
|
6920
|
+
return;
|
|
6921
|
+
}
|
|
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;
|
|
6948
|
+
}
|
|
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;
|
|
6330
6954
|
}
|
|
6331
|
-
if (
|
|
6332
|
-
|
|
6955
|
+
if (verdict.remainder !== null) {
|
|
6956
|
+
lines.push(` \u25CB strip harness sections from ${path} (\uBCF8\uBB38 \uBCF4\uC874)`);
|
|
6957
|
+
continue;
|
|
6333
6958
|
}
|
|
6334
|
-
} finally {
|
|
6335
|
-
process.stdout.write("\x1B[?1049l");
|
|
6336
6959
|
}
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6960
|
+
removable += 1;
|
|
6961
|
+
}
|
|
6962
|
+
if (removable > 0) lines.unshift(` \u25CB remove ${removable} CLI output file(s)`);
|
|
6963
|
+
return lines;
|
|
6964
|
+
}
|
|
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)`)}`);
|
|
6969
|
+
}
|
|
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.`
|
|
6981
|
+
);
|
|
6982
|
+
}
|
|
6983
|
+
return lines;
|
|
6984
|
+
}
|
|
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)}`))
|
|
6342
7047
|
);
|
|
7048
|
+
console.error(c.dim(" Nothing installed here by agent-harness."));
|
|
7049
|
+
process.exit(1);
|
|
6343
7050
|
}
|
|
6344
|
-
return
|
|
7051
|
+
return;
|
|
6345
7052
|
}
|
|
6346
|
-
|
|
7053
|
+
uninstallAction(picked.options);
|
|
7054
|
+
}
|
|
7055
|
+
|
|
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 };
|
|
7080
|
+
}
|
|
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);
|
|
7105
|
+
}
|
|
7106
|
+
}
|
|
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
|
+
}
|
|
6347
7167
|
|
|
6348
7168
|
// src/interactive.ts
|
|
7169
|
+
init_esm_shims();
|
|
7170
|
+
import { existsSync as existsSync20 } from "fs";
|
|
7171
|
+
import { join as join21 } from "path";
|
|
6349
7172
|
function splitInstallTargets(targets) {
|
|
6350
7173
|
const optionKeys = [];
|
|
6351
7174
|
const assetIds = [];
|
|
@@ -6386,6 +7209,59 @@ function initialTargetSelection(tracks, installedProjectAssetIds) {
|
|
|
6386
7209
|
const baseline = listBaselineTargets({ tracks }).map((t) => t.id);
|
|
6387
7210
|
return [...assets, ...baseline];
|
|
6388
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
|
+
}
|
|
6389
7265
|
async function runInteractive(projectDir, deps = {}) {
|
|
6390
7266
|
const prompts = deps.prompts ?? defaultPrompts;
|
|
6391
7267
|
const detect = deps.detect ?? detectInstallState;
|
|
@@ -6400,10 +7276,10 @@ async function runInteractive(projectDir, deps = {}) {
|
|
|
6400
7276
|
prompts.intro("uzys-agent-harness installer");
|
|
6401
7277
|
const state = detect(projectDir);
|
|
6402
7278
|
const installed = (deps.readInstalled ?? installedTargetState)(projectDir);
|
|
6403
|
-
let initialTracks;
|
|
6404
|
-
let mode = "fresh";
|
|
6405
7279
|
if (state.state === "existing") {
|
|
6406
|
-
const
|
|
7280
|
+
const log = readInstallLog(projectDir);
|
|
7281
|
+
const record = buildInstallRecordView(state, log, existsSync20(join21(projectDir, ".claude")));
|
|
7282
|
+
const action = await prompts.selectAction(state, record);
|
|
6407
7283
|
if (action === null) {
|
|
6408
7284
|
prompts.cancel("Cancelled.");
|
|
6409
7285
|
return { ok: false, reason: "cancelled" };
|
|
@@ -6412,34 +7288,14 @@ async function runInteractive(projectDir, deps = {}) {
|
|
|
6412
7288
|
prompts.outro("Exiting without changes.");
|
|
6413
7289
|
return { ok: false, reason: "exit" };
|
|
6414
7290
|
}
|
|
6415
|
-
if (action === "
|
|
6416
|
-
|
|
6417
|
-
return { ok: false, reason: "disabled-action" };
|
|
6418
|
-
}
|
|
6419
|
-
if (action === "update") {
|
|
6420
|
-
mode = "update";
|
|
6421
|
-
const groups = prompts.selectUpdateGroups ? await prompts.selectUpdateGroups() : null;
|
|
6422
|
-
if (groups === null && prompts.selectUpdateGroups) {
|
|
6423
|
-
prompts.cancel("Cancelled.");
|
|
6424
|
-
return { ok: false, reason: "cancelled" };
|
|
6425
|
-
}
|
|
6426
|
-
const spec = buildUpdateSpec(projectDir, state.tracks, groups ?? void 0);
|
|
6427
|
-
const scopeLine = spec.updateOnly ? `UPDATE ${spec.updateOnly.join(" \xB7 ")} only (untouched: ${UPDATE_GROUPS.filter((g) => !spec.updateOnly?.includes(g)).join(", ")}):` : "UPDATE installed harness files:";
|
|
6428
|
-
const confirmed = await prompts.confirmInstall(`${scopeLine}
|
|
6429
|
-
${formatSummary(spec)}`);
|
|
6430
|
-
if (!confirmed) {
|
|
6431
|
-
prompts.outro("Cancelled.");
|
|
6432
|
-
return { ok: false, reason: "cancelled" };
|
|
6433
|
-
}
|
|
6434
|
-
prompts.outro("Running update mode...");
|
|
6435
|
-
return { ok: true, mode: "update", spec };
|
|
7291
|
+
if (action === "uninstall") {
|
|
7292
|
+
return { ok: true, uninstall: true };
|
|
6436
7293
|
}
|
|
6437
|
-
if (
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
} else if (action === "reinstall") {
|
|
6441
|
-
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 };
|
|
6442
7297
|
}
|
|
7298
|
+
return runUpdateFlow({ projectDir, state, log, prompts, installed });
|
|
6443
7299
|
}
|
|
6444
7300
|
let step = "tracks";
|
|
6445
7301
|
let tracks = null;
|
|
@@ -6448,7 +7304,7 @@ ${formatSummary(spec)}`);
|
|
|
6448
7304
|
let scope = "project";
|
|
6449
7305
|
while (true) {
|
|
6450
7306
|
if (step === "tracks") {
|
|
6451
|
-
const result = await prompts.selectTracks(tracks ??
|
|
7307
|
+
const result = await prompts.selectTracks(tracks ?? void 0, WIZARD.TRACKS);
|
|
6452
7308
|
if (result === null) {
|
|
6453
7309
|
prompts.cancel("Cancelled.");
|
|
6454
7310
|
return { ok: false, reason: "cancelled" };
|
|
@@ -6502,7 +7358,7 @@ ${formatSummary(spec)}`);
|
|
|
6502
7358
|
projectDir,
|
|
6503
7359
|
...userOverride ? { userOverride } : {},
|
|
6504
7360
|
// 해제 목록을 안 넘기면 이 화면이 **제외 유무와 문자열이 완전히 같아진다** — 같은 화면이
|
|
6505
|
-
// 외부 자산 제거는 `-
|
|
7361
|
+
// 외부 자산 제거는 `-Unchecked by you:` 로 이미 보고하므로, 없음은 "아무것도 안 빠졌다"로
|
|
6506
7362
|
// 읽힌다. 상주 비용을 줄이려고 20개를 푼 사용자가 그대로인 숫자를 보게 된다.
|
|
6507
7363
|
...baselineExclude.length > 0 ? { baselineExclude } : {}
|
|
6508
7364
|
})}
|
|
@@ -6529,10 +7385,169 @@ ${summary}`
|
|
|
6529
7385
|
...baselineExclude.length > 0 ? { baselineExclude } : {}
|
|
6530
7386
|
};
|
|
6531
7387
|
prompts.outro(stepLabel(WIZARD.INSTALL, "Installing..."));
|
|
6532
|
-
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;
|
|
6533
7458
|
}
|
|
6534
7459
|
}
|
|
6535
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
|
+
}
|
|
6536
7551
|
function tracksEqual(a, b2) {
|
|
6537
7552
|
if (a.length !== b2.length) return false;
|
|
6538
7553
|
const sortedA = [...a].sort();
|
|
@@ -6585,7 +7600,7 @@ function formatSummary(spec) {
|
|
|
6585
7600
|
lines.push(` +User added: ${spec.userOverride.forceInclude.join(", ")}`);
|
|
6586
7601
|
}
|
|
6587
7602
|
if (spec.userOverride.forceExclude.length > 0) {
|
|
6588
|
-
lines.push(` -
|
|
7603
|
+
lines.push(` -Unchecked by you: ${spec.userOverride.forceExclude.join(", ")}`);
|
|
6589
7604
|
}
|
|
6590
7605
|
}
|
|
6591
7606
|
if (baselineExcluded.size > 0) {
|
|
@@ -6605,6 +7620,11 @@ async function defaultAction(deps = {}) {
|
|
|
6605
7620
|
const run = deps.run ?? ((cwd) => runInteractive(cwd));
|
|
6606
7621
|
const execute = deps.execute ?? executeSpec;
|
|
6607
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
|
+
}
|
|
6608
7628
|
if (!result.ok) {
|
|
6609
7629
|
if (result.message) {
|
|
6610
7630
|
err(result.message);
|