@uzysjung/agent-harness 26.146.1 → 26.147.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-OEIGA5IL.js → chunk-GPH5AWLX.js} +450 -53
- package/dist/chunk-GPH5AWLX.js.map +1 -0
- package/dist/index.js +604 -529
- package/dist/index.js.map +1 -1
- package/dist/trust-tier-drift.js +1 -1
- package/package.json +1 -1
- package/templates/CLAUDE.md +14 -5
- package/templates/agents/implementer.md +1 -1
- package/templates/codex/AGENTS.md.template +11 -0
- package/templates/opencode/AGENTS.md.template +8 -1
- package/templates/opencode/opencode.json.template +0 -1
- package/templates/rules/cli-development.md +2 -2
- package/templates/rules/doc-governance.md +3 -1
- package/templates/rules/test-policy.md +5 -6
- package/templates/scripts/check-absence.sh +205 -0
- package/templates/settings.json +0 -9
- package/templates/skills/gh-issue-workflow/ISSUE.template.md +111 -35
- package/templates/skills/gh-issue-workflow/SKILL.md +268 -138
- package/templates/skills/gh-issue-workflow/references/hierarchy.md +261 -0
- package/templates/skills/gh-issue-workflow/references/operating-model.md +132 -0
- package/templates/skills/gh-issue-workflow/references/readiness.md +121 -0
- package/templates/skills/ui-visual-review/SKILL.md +17 -3
- package/dist/chunk-OEIGA5IL.js.map +0 -1
- package/templates/commands/ecc/checkpoint.md +0 -32
- package/templates/commands/ecc/e2e.md +0 -105
- package/templates/commands/ecc/eval.md +0 -88
- package/templates/commands/ecc/evolve.md +0 -7
- package/templates/commands/ecc/harness-audit.md +0 -73
- package/templates/commands/ecc/instinct-status.md +0 -8
- package/templates/commands/ecc/promote.md +0 -10
- package/templates/commands/ecc/security-scan.md +0 -10
- package/templates/hooks/mcp-pre-exec.sh +0 -122
- package/templates/mcp-allowlist.example +0 -24
- package/templates/rules/playwright-launch.md +0 -16
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
ALL_RULES,
|
|
3
4
|
CATEGORIES,
|
|
4
5
|
CATEGORY_TITLES,
|
|
5
6
|
CLI_BASES,
|
|
@@ -8,7 +9,9 @@ import {
|
|
|
8
9
|
EXTERNAL_ASSETS,
|
|
9
10
|
HARNESS_ANCHOR_FILE,
|
|
10
11
|
HARNESS_IMPORT_LINE,
|
|
12
|
+
INSTALL_LOG_DIR,
|
|
11
13
|
INTERNAL_BUNDLED_SKILL_IDS,
|
|
14
|
+
POLICY_DIRS,
|
|
12
15
|
TRACKS,
|
|
13
16
|
__commonJS,
|
|
14
17
|
__toESM,
|
|
@@ -16,23 +19,44 @@ import {
|
|
|
16
19
|
assetCliSupport,
|
|
17
20
|
assetReachesCli,
|
|
18
21
|
assetTrustTier,
|
|
22
|
+
backupDir,
|
|
23
|
+
backupFile,
|
|
24
|
+
backupFileIfChanged,
|
|
25
|
+
buildInstallLog,
|
|
19
26
|
buildManifest,
|
|
27
|
+
collectPolicyHashes,
|
|
28
|
+
collectSkillHashes,
|
|
29
|
+
copyBackupDir,
|
|
30
|
+
copyDir,
|
|
31
|
+
copyFile,
|
|
32
|
+
ensureDir,
|
|
33
|
+
ensureProjectSkeleton,
|
|
20
34
|
experimentalOptInCandidates,
|
|
21
35
|
filterApplicableAssets,
|
|
22
36
|
formatResidentCostLine,
|
|
23
37
|
hasUiTrack,
|
|
38
|
+
hashContent,
|
|
24
39
|
init_esm_shims,
|
|
40
|
+
installLogPath,
|
|
25
41
|
isAssetSelected,
|
|
26
42
|
isCliBase,
|
|
43
|
+
isCliNeutralTarget,
|
|
44
|
+
isHarnessOwned,
|
|
27
45
|
isInstallScope,
|
|
28
46
|
isTrack,
|
|
47
|
+
legacyInstallLogPath,
|
|
48
|
+
listFilesRecursive,
|
|
49
|
+
mergeExternalFiles,
|
|
50
|
+
readInstallLog,
|
|
29
51
|
renderFillScaffold,
|
|
30
52
|
residentCost,
|
|
53
|
+
resolveRules,
|
|
31
54
|
resolveScope,
|
|
32
55
|
stripHarnessImport,
|
|
33
56
|
summarizeContextCost,
|
|
34
|
-
upsertHarnessImport
|
|
35
|
-
|
|
57
|
+
upsertHarnessImport,
|
|
58
|
+
writeInstallLog
|
|
59
|
+
} from "./chunk-GPH5AWLX.js";
|
|
36
60
|
|
|
37
61
|
// node_modules/sisteransi/src/index.js
|
|
38
62
|
var require_src = __commonJS({
|
|
@@ -717,7 +741,7 @@ var cac = (name = "") => new CAC(name);
|
|
|
717
741
|
// package.json
|
|
718
742
|
var package_default = {
|
|
719
743
|
name: "@uzysjung/agent-harness",
|
|
720
|
-
version: "26.
|
|
744
|
+
version: "26.147.0",
|
|
721
745
|
description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
|
|
722
746
|
type: "module",
|
|
723
747
|
publishConfig: {
|
|
@@ -778,6 +802,46 @@ init_esm_shims();
|
|
|
778
802
|
import { resolve as resolve2 } from "path";
|
|
779
803
|
import { fileURLToPath } from "url";
|
|
780
804
|
|
|
805
|
+
// src/baseline-targets.ts
|
|
806
|
+
init_esm_shims();
|
|
807
|
+
var BASELINE_KINDS = ["rules", "agents", "hooks", "skills"];
|
|
808
|
+
var BASELINE_PREFIX = "baseline:";
|
|
809
|
+
var PREFIX_BY_KIND = [
|
|
810
|
+
["rules", ".claude/rules/"],
|
|
811
|
+
["agents", ".claude/agents/"],
|
|
812
|
+
["hooks", ".claude/hooks/"],
|
|
813
|
+
["skills", ".claude/skills/"]
|
|
814
|
+
];
|
|
815
|
+
function classifyBaselineTarget(target) {
|
|
816
|
+
for (const [kind, prefix] of PREFIX_BY_KIND) {
|
|
817
|
+
if (!target.startsWith(prefix)) continue;
|
|
818
|
+
const rest = target.slice(prefix.length);
|
|
819
|
+
if (rest === "") return null;
|
|
820
|
+
const first = rest.split("/")[0] ?? "";
|
|
821
|
+
const name = kind === "skills" ? first : first.replace(/\.(md|sh)$/, "");
|
|
822
|
+
if (name === "") return null;
|
|
823
|
+
return { id: `${BASELINE_PREFIX}${kind}/${name}`, kind, name };
|
|
824
|
+
}
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
function listBaselineTargets(spec) {
|
|
828
|
+
const byId = /* @__PURE__ */ new Map();
|
|
829
|
+
for (const entry of buildManifest(spec)) {
|
|
830
|
+
if (!entry.applies(spec)) continue;
|
|
831
|
+
const t = classifyBaselineTarget(entry.target);
|
|
832
|
+
if (t && !byId.has(t.id)) byId.set(t.id, t);
|
|
833
|
+
}
|
|
834
|
+
const order = new Map(BASELINE_KINDS.map((k2, i) => [k2, i]));
|
|
835
|
+
return [...byId.values()].sort(
|
|
836
|
+
(a, b2) => (order.get(a.kind) ?? 0) - (order.get(b2.kind) ?? 0) || a.name.localeCompare(b2.name)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
function isBaselineExcluded(target, excluded) {
|
|
840
|
+
if (excluded.size === 0) return false;
|
|
841
|
+
const t = classifyBaselineTarget(target);
|
|
842
|
+
return t !== null && excluded.has(t.id);
|
|
843
|
+
}
|
|
844
|
+
|
|
781
845
|
// src/cli-targets.ts
|
|
782
846
|
init_esm_shims();
|
|
783
847
|
var CLI_BASE_SORT_ORDER = {
|
|
@@ -915,127 +979,18 @@ init_esm_shims();
|
|
|
915
979
|
import {
|
|
916
980
|
chmodSync as chmodSync2,
|
|
917
981
|
existsSync as existsSync13,
|
|
918
|
-
mkdirSync as
|
|
919
|
-
readdirSync as
|
|
982
|
+
mkdirSync as mkdirSync4,
|
|
983
|
+
readdirSync as readdirSync3,
|
|
920
984
|
readFileSync as readFileSync12,
|
|
921
|
-
|
|
985
|
+
unlinkSync as unlinkSync2,
|
|
986
|
+
writeFileSync as writeFileSync6
|
|
922
987
|
} from "fs";
|
|
923
|
-
import { basename as basename5, dirname as
|
|
988
|
+
import { basename as basename5, dirname as dirname4, join as join11, resolve } from "path";
|
|
924
989
|
|
|
925
990
|
// src/ci-scaffold.ts
|
|
926
991
|
init_esm_shims();
|
|
927
|
-
import { existsSync
|
|
928
|
-
import { join
|
|
929
|
-
|
|
930
|
-
// src/fs-ops.ts
|
|
931
|
-
init_esm_shims();
|
|
932
|
-
import {
|
|
933
|
-
copyFileSync,
|
|
934
|
-
cpSync,
|
|
935
|
-
existsSync,
|
|
936
|
-
constants as fsConstants,
|
|
937
|
-
mkdirSync,
|
|
938
|
-
readdirSync,
|
|
939
|
-
readFileSync,
|
|
940
|
-
realpathSync,
|
|
941
|
-
renameSync,
|
|
942
|
-
rmdirSync
|
|
943
|
-
} from "fs";
|
|
944
|
-
import { dirname, join } from "path";
|
|
945
|
-
function ensureDir(path) {
|
|
946
|
-
mkdirSync(path, { recursive: true });
|
|
947
|
-
}
|
|
948
|
-
function copyFile(source, target) {
|
|
949
|
-
if (!existsSync(source)) {
|
|
950
|
-
throw new Error(`Source not found: ${source}`);
|
|
951
|
-
}
|
|
952
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
953
|
-
copyFileSync(source, target);
|
|
954
|
-
}
|
|
955
|
-
function copyDir(source, target) {
|
|
956
|
-
if (!existsSync(source)) {
|
|
957
|
-
throw new Error(`Source dir not found: ${source}`);
|
|
958
|
-
}
|
|
959
|
-
mkdirSync(target, { recursive: true });
|
|
960
|
-
cpSync(source, target, { recursive: true, force: true });
|
|
961
|
-
}
|
|
962
|
-
var MAX_BACKUP_CLAIMS = 100;
|
|
963
|
-
function claimBackupPath(base, claim) {
|
|
964
|
-
for (let n = 1; n <= MAX_BACKUP_CLAIMS; n++) {
|
|
965
|
-
const candidate = n === 1 ? base : `${base}-${n}`;
|
|
966
|
-
try {
|
|
967
|
-
claim(candidate);
|
|
968
|
-
return candidate;
|
|
969
|
-
} catch (err) {
|
|
970
|
-
if (err.code !== "EEXIST") throw err;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
throw new Error(`Cannot allocate a backup path for ${base} (${MAX_BACKUP_CLAIMS} taken)`);
|
|
974
|
-
}
|
|
975
|
-
function backupDir(target, now = /* @__PURE__ */ new Date()) {
|
|
976
|
-
if (!existsSync(target)) {
|
|
977
|
-
return null;
|
|
978
|
-
}
|
|
979
|
-
const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c3) => mkdirSync(c3));
|
|
980
|
-
rmdirSync(backup);
|
|
981
|
-
renameSync(target, backup);
|
|
982
|
-
return backup;
|
|
983
|
-
}
|
|
984
|
-
function copyBackupDir(target, now = /* @__PURE__ */ new Date()) {
|
|
985
|
-
if (!existsSync(target)) {
|
|
986
|
-
return null;
|
|
987
|
-
}
|
|
988
|
-
const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c3) => mkdirSync(c3));
|
|
989
|
-
cpSync(realpathSync(target), backup, { recursive: true });
|
|
990
|
-
return backup;
|
|
991
|
-
}
|
|
992
|
-
function backupFileIfChanged(target, newContent, now = /* @__PURE__ */ new Date()) {
|
|
993
|
-
if (!existsSync(target)) {
|
|
994
|
-
return null;
|
|
995
|
-
}
|
|
996
|
-
if (readFileSync(target, "utf-8") === newContent) {
|
|
997
|
-
return null;
|
|
998
|
-
}
|
|
999
|
-
return backupFile(target, now);
|
|
1000
|
-
}
|
|
1001
|
-
function backupFile(target, now = /* @__PURE__ */ new Date()) {
|
|
1002
|
-
return claimBackupPath(
|
|
1003
|
-
`${target}.backup-${formatStamp(now)}`,
|
|
1004
|
-
(c3) => copyFileSync(target, c3, fsConstants.COPYFILE_EXCL)
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
function listFilesRecursive(dir, prefix = "") {
|
|
1008
|
-
if (!existsSync(dir)) return [];
|
|
1009
|
-
const out = [];
|
|
1010
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1011
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1012
|
-
if (entry.isDirectory()) {
|
|
1013
|
-
out.push(...listFilesRecursive(join(dir, entry.name), rel));
|
|
1014
|
-
} else if (entry.isFile()) {
|
|
1015
|
-
out.push(rel);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
return out;
|
|
1019
|
-
}
|
|
1020
|
-
function formatStamp(now) {
|
|
1021
|
-
return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").slice(0, 15);
|
|
1022
|
-
}
|
|
1023
|
-
function ensureProjectSkeleton(projectDir) {
|
|
1024
|
-
const dirs = [
|
|
1025
|
-
".claude/commands/uzys",
|
|
1026
|
-
".claude/commands/ecc",
|
|
1027
|
-
".claude/rules",
|
|
1028
|
-
".claude/skills",
|
|
1029
|
-
".claude/agents",
|
|
1030
|
-
".claude/hooks",
|
|
1031
|
-
"docs/decisions"
|
|
1032
|
-
];
|
|
1033
|
-
for (const d2 of dirs) {
|
|
1034
|
-
mkdirSync(join(projectDir, d2), { recursive: true });
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// src/ci-scaffold.ts
|
|
992
|
+
import { existsSync } from "fs";
|
|
993
|
+
import { join } from "path";
|
|
1039
994
|
var PYTHON_TRACKS = "data|csr-fastapi|full";
|
|
1040
995
|
var VARIANTS = [
|
|
1041
996
|
{
|
|
@@ -1059,12 +1014,12 @@ function installCiScaffold(ctx) {
|
|
|
1059
1014
|
for (const v2 of VARIANTS) {
|
|
1060
1015
|
if (!v2.applies(ctx.tracks)) continue;
|
|
1061
1016
|
const relTarget = `.github/workflows/${v2.target}`;
|
|
1062
|
-
const target =
|
|
1063
|
-
if (
|
|
1017
|
+
const target = join(ctx.projectDir, relTarget);
|
|
1018
|
+
if (existsSync(target)) {
|
|
1064
1019
|
report.skippedExisting.push(relTarget);
|
|
1065
1020
|
continue;
|
|
1066
1021
|
}
|
|
1067
|
-
copyFile(
|
|
1022
|
+
copyFile(join(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
|
|
1068
1023
|
report.written.push(relTarget);
|
|
1069
1024
|
}
|
|
1070
1025
|
return report;
|
|
@@ -1075,8 +1030,8 @@ init_esm_shims();
|
|
|
1075
1030
|
|
|
1076
1031
|
// src/antigravity/transform.ts
|
|
1077
1032
|
init_esm_shims();
|
|
1078
|
-
import { existsSync as
|
|
1079
|
-
import { basename, join as
|
|
1033
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
1034
|
+
import { basename, join as join3 } from "path";
|
|
1080
1035
|
|
|
1081
1036
|
// src/codex/agents-md.ts
|
|
1082
1037
|
init_esm_shims();
|
|
@@ -1085,7 +1040,7 @@ function renameSlashes(text) {
|
|
|
1085
1040
|
}
|
|
1086
1041
|
function renderAgentsMd(params) {
|
|
1087
1042
|
const body = params.claudeMd.replace(/^#\s+.*\r?\n/, "").trim();
|
|
1088
|
-
const replaced = params.template.replaceAll("{PROJECT_NAME}", params.projectName).replaceAll("{PROJECT_RULES}", body).replaceAll("{PROJECT_CONTEXT}", params.projectContext);
|
|
1043
|
+
const replaced = params.template.replaceAll("{PROJECT_NAME}", params.projectName).replaceAll("{PROJECT_RULES}", body).replaceAll("{PROJECT_CONTEXT}", params.projectContext).replaceAll("{HARNESS_RULES}", params.harnessRules ?? "");
|
|
1089
1044
|
return renameSlashes(replaced);
|
|
1090
1045
|
}
|
|
1091
1046
|
|
|
@@ -1121,204 +1076,8 @@ function portBody(body) {
|
|
|
1121
1076
|
|
|
1122
1077
|
// src/owned-write.ts
|
|
1123
1078
|
init_esm_shims();
|
|
1124
|
-
import { existsSync as
|
|
1125
|
-
import { dirname
|
|
1126
|
-
|
|
1127
|
-
// src/install-log.ts
|
|
1128
|
-
init_esm_shims();
|
|
1129
|
-
import { createHash } from "crypto";
|
|
1130
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
1131
|
-
import { dirname as dirname2, join as join3 } from "path";
|
|
1132
|
-
var INSTALL_LOG_FILENAME = ".harness-install.json";
|
|
1133
|
-
var INSTALL_LOG_DIR = ".uzys-agent-harness";
|
|
1134
|
-
var LEGACY_INSTALL_LOG_DIR = ".claude";
|
|
1135
|
-
var INSTALL_LOG_VERSION = 1;
|
|
1136
|
-
var POLICY_DIRS = [
|
|
1137
|
-
{ dir: "rules", ext: ".md" },
|
|
1138
|
-
{ dir: "agents", ext: ".md" },
|
|
1139
|
-
{ dir: "commands/uzys", ext: ".md" },
|
|
1140
|
-
{ dir: "hooks", ext: ".sh" }
|
|
1141
|
-
];
|
|
1142
|
-
function buildAssetEntries(report, scope) {
|
|
1143
|
-
if (!report) return [];
|
|
1144
|
-
return report.attempted.filter((r) => r.ok).map((r) => assetToLogEntry(r.asset, scope, r.version));
|
|
1145
|
-
}
|
|
1146
|
-
function assetToLogEntry(asset, scope, version) {
|
|
1147
|
-
const detail = methodDetail(asset.method);
|
|
1148
|
-
const entry = {
|
|
1149
|
-
id: asset.id,
|
|
1150
|
-
category: asset.category,
|
|
1151
|
-
method: asset.method.kind,
|
|
1152
|
-
scope,
|
|
1153
|
-
detail
|
|
1154
|
-
};
|
|
1155
|
-
if (version) entry.version = version;
|
|
1156
|
-
return entry;
|
|
1157
|
-
}
|
|
1158
|
-
function methodDetail(method) {
|
|
1159
|
-
switch (method.kind) {
|
|
1160
|
-
case "plugin":
|
|
1161
|
-
return { marketplace: method.marketplace, pluginId: method.pluginId };
|
|
1162
|
-
case "skill":
|
|
1163
|
-
return { source: method.source, ...method.skill ? { skill: method.skill } : {} };
|
|
1164
|
-
case "npm":
|
|
1165
|
-
return { pkg: method.pkg };
|
|
1166
|
-
case "npx-run":
|
|
1167
|
-
return { cmd: method.cmd, args: (method.args ?? []).join(" ") };
|
|
1168
|
-
case "shell-script":
|
|
1169
|
-
return { script: method.script, args: method.args.join(" ") };
|
|
1170
|
-
case "internal":
|
|
1171
|
-
return { key: method.key };
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
|
|
1175
|
-
const templates = {
|
|
1176
|
-
claudeDir: ".claude/",
|
|
1177
|
-
...spec.cli.includes("codex") ? { codexDir: ".codex/" } : {},
|
|
1178
|
-
...spec.cli.includes("opencode") ? { opencodeDir: ".opencode/" } : {},
|
|
1179
|
-
...rootClaudeMd ? { rootClaudeMd } : {}
|
|
1180
|
-
};
|
|
1181
|
-
const log = {
|
|
1182
|
-
schemaVersion: INSTALL_LOG_VERSION,
|
|
1183
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1184
|
-
scope,
|
|
1185
|
-
spec: {
|
|
1186
|
-
tracks: spec.tracks,
|
|
1187
|
-
cli: spec.cli
|
|
1188
|
-
},
|
|
1189
|
-
// 이번 설치가 만든 항목이 이기고, 이번에 안 만든 항목은 이전 값을 그대로 둔다.
|
|
1190
|
-
// (예: claude 로 깔고 나중에 codex 만 추가 설치해도 root CLAUDE.md 기록이 살아남는다)
|
|
1191
|
-
templates: { ...previous?.templates, ...templates },
|
|
1192
|
-
assets: mergeAssets(
|
|
1193
|
-
claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
|
|
1194
|
-
buildAssetEntries(external, scope)
|
|
1195
|
-
)
|
|
1196
|
-
};
|
|
1197
|
-
const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
|
|
1198
|
-
if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
|
|
1199
|
-
return log;
|
|
1200
|
-
}
|
|
1201
|
-
function mergeRootFiles(previous, current) {
|
|
1202
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
1203
|
-
for (const file of [...previous ?? [], ...current]) {
|
|
1204
|
-
const prior = byPath.get(file.path);
|
|
1205
|
-
byPath.set(
|
|
1206
|
-
file.path,
|
|
1207
|
-
prior ? {
|
|
1208
|
-
path: file.path,
|
|
1209
|
-
change: prior.change === "created" ? "created" : file.change,
|
|
1210
|
-
notes: [.../* @__PURE__ */ new Set([...prior.notes, ...file.notes])]
|
|
1211
|
-
} : file
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
return [...byPath.values()];
|
|
1215
|
-
}
|
|
1216
|
-
function survivesClaudeDirRename(asset) {
|
|
1217
|
-
if (asset.scope === "global") return true;
|
|
1218
|
-
switch (asset.method) {
|
|
1219
|
-
case "plugin":
|
|
1220
|
-
return true;
|
|
1221
|
-
// `~/.claude/plugins/cache` — 프로젝트 밖
|
|
1222
|
-
case "npm":
|
|
1223
|
-
return true;
|
|
1224
|
-
// `node_modules/`
|
|
1225
|
-
case "skill":
|
|
1226
|
-
return false;
|
|
1227
|
-
// `npx skills add` project scope → `.claude/skills/`
|
|
1228
|
-
case "shell-script":
|
|
1229
|
-
return false;
|
|
1230
|
-
// ecc-prune → `.claude/local-plugins/`
|
|
1231
|
-
case "npx-run":
|
|
1232
|
-
return false;
|
|
1233
|
-
case "internal":
|
|
1234
|
-
return false;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
function mergeAssets(previous, current) {
|
|
1238
|
-
if (!previous || previous.length === 0) return [...current];
|
|
1239
|
-
const currentById = new Map(current.map((a) => [a.id, a]));
|
|
1240
|
-
const previousIds = new Set(previous.map((a) => a.id));
|
|
1241
|
-
return [
|
|
1242
|
-
...previous.map((a) => currentById.get(a.id) ?? a),
|
|
1243
|
-
...current.filter((a) => !previousIds.has(a.id))
|
|
1244
|
-
];
|
|
1245
|
-
}
|
|
1246
|
-
function hashContent(content) {
|
|
1247
|
-
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1248
|
-
}
|
|
1249
|
-
function isHarnessOwned(baseline, key, current) {
|
|
1250
|
-
const recorded = baseline.get(key);
|
|
1251
|
-
return recorded !== void 0 && recorded === hashContent(current);
|
|
1252
|
-
}
|
|
1253
|
-
function mergeExternalFiles(projectDir, previous, current) {
|
|
1254
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
1255
|
-
for (const file of [...previous ?? [], ...current]) byPath.set(file.path, file);
|
|
1256
|
-
return [...byPath.values()].filter((f) => existsSync3(join3(projectDir, f.path)));
|
|
1257
|
-
}
|
|
1258
|
-
function collectSkillHashes(projectDir) {
|
|
1259
|
-
const skillsDir = join3(projectDir, ".claude/skills");
|
|
1260
|
-
return listFilesRecursive(skillsDir).map((rel) => ({
|
|
1261
|
-
path: rel,
|
|
1262
|
-
sha256: hashContent(readFileSync2(join3(skillsDir, rel), "utf8"))
|
|
1263
|
-
}));
|
|
1264
|
-
}
|
|
1265
|
-
function collectPolicyHashes(projectDir, templatesDir) {
|
|
1266
|
-
const out = [];
|
|
1267
|
-
for (const { dir, ext } of POLICY_DIRS) {
|
|
1268
|
-
const targetDir = join3(projectDir, ".claude", dir);
|
|
1269
|
-
const sourceDir = join3(templatesDir, dir);
|
|
1270
|
-
for (const rel of listFilesRecursive(targetDir)) {
|
|
1271
|
-
if (!rel.endsWith(ext)) continue;
|
|
1272
|
-
if (!existsSync3(join3(sourceDir, rel))) continue;
|
|
1273
|
-
out.push({
|
|
1274
|
-
path: `${dir}/${rel}`,
|
|
1275
|
-
sha256: hashContent(readFileSync2(join3(targetDir, rel), "utf8"))
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
return out;
|
|
1280
|
-
}
|
|
1281
|
-
function writeInstallLog(projectDir, log) {
|
|
1282
|
-
const path = installLogPath(projectDir);
|
|
1283
|
-
mkdirSync2(dirname2(path), { recursive: true });
|
|
1284
|
-
writeFileSync(path, `${JSON.stringify(log, null, 2)}
|
|
1285
|
-
`, "utf8");
|
|
1286
|
-
migrateAwayLegacyLog(projectDir);
|
|
1287
|
-
return path;
|
|
1288
|
-
}
|
|
1289
|
-
function migrateAwayLegacyLog(projectDir) {
|
|
1290
|
-
const legacy = legacyInstallLogPath(projectDir);
|
|
1291
|
-
if (!existsSync3(legacy)) return;
|
|
1292
|
-
try {
|
|
1293
|
-
rmSync(legacy);
|
|
1294
|
-
} catch {
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
function readInstallLog(projectDir) {
|
|
1298
|
-
const path = [installLogPath(projectDir), legacyInstallLogPath(projectDir)].find(
|
|
1299
|
-
(p2) => existsSync3(p2)
|
|
1300
|
-
);
|
|
1301
|
-
if (!path) return null;
|
|
1302
|
-
try {
|
|
1303
|
-
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1304
|
-
if (Array.isArray(parsed.assets)) {
|
|
1305
|
-
parsed.assets = parsed.assets.map(
|
|
1306
|
-
(a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
|
|
1307
|
-
);
|
|
1308
|
-
}
|
|
1309
|
-
return parsed;
|
|
1310
|
-
} catch {
|
|
1311
|
-
return null;
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
function installLogPath(projectDir) {
|
|
1315
|
-
return join3(projectDir, INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
|
|
1316
|
-
}
|
|
1317
|
-
function legacyInstallLogPath(projectDir) {
|
|
1318
|
-
return join3(projectDir, LEGACY_INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
// src/owned-write.ts
|
|
1079
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1080
|
+
import { dirname, relative, sep } from "path";
|
|
1322
1081
|
function createOwnedWriter(projectDir, baseline, options = {}) {
|
|
1323
1082
|
const { now = /* @__PURE__ */ new Date(), refreshOnly = false } = options;
|
|
1324
1083
|
const written = /* @__PURE__ */ new Map();
|
|
@@ -1326,22 +1085,22 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
|
|
|
1326
1085
|
const backupPaths = [];
|
|
1327
1086
|
let updated = 0;
|
|
1328
1087
|
return {
|
|
1329
|
-
write(absPath, content) {
|
|
1088
|
+
write(absPath, content, opts) {
|
|
1330
1089
|
const rel = relative(projectDir, absPath).split(sep).join("/");
|
|
1331
1090
|
const digest = hashContent(content);
|
|
1332
|
-
if (!
|
|
1333
|
-
if (refreshOnly) return false;
|
|
1334
|
-
|
|
1335
|
-
|
|
1091
|
+
if (!existsSync2(absPath)) {
|
|
1092
|
+
if (refreshOnly && !opts?.createInRefresh) return false;
|
|
1093
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
1094
|
+
writeFileSync(absPath, content);
|
|
1336
1095
|
updated++;
|
|
1337
1096
|
} else {
|
|
1338
|
-
const current =
|
|
1097
|
+
const current = readFileSync(absPath, "utf8");
|
|
1339
1098
|
if (current !== content) {
|
|
1340
1099
|
if (!isHarnessOwned(baseline, rel, current)) {
|
|
1341
1100
|
backupPaths.push(backupFile(absPath, now));
|
|
1342
1101
|
backedUp.push(rel);
|
|
1343
1102
|
}
|
|
1344
|
-
|
|
1103
|
+
writeFileSync(absPath, content);
|
|
1345
1104
|
updated++;
|
|
1346
1105
|
}
|
|
1347
1106
|
}
|
|
@@ -1359,36 +1118,78 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
|
|
|
1359
1118
|
};
|
|
1360
1119
|
}
|
|
1361
1120
|
|
|
1121
|
+
// src/rules-port.ts
|
|
1122
|
+
init_esm_shims();
|
|
1123
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
1124
|
+
import { join as join2 } from "path";
|
|
1125
|
+
function stripClaudeFrontmatter(source) {
|
|
1126
|
+
if (!source.startsWith("---\n")) return source.trim();
|
|
1127
|
+
const end = source.indexOf("\n---", 4);
|
|
1128
|
+
if (end === -1) return source.trim();
|
|
1129
|
+
return source.slice(end + 4).trim();
|
|
1130
|
+
}
|
|
1131
|
+
function portRules(harnessRoot, ruleNames) {
|
|
1132
|
+
const ported = [];
|
|
1133
|
+
for (const name of ruleNames) {
|
|
1134
|
+
const src = join2(harnessRoot, "templates/rules", `${name}.md`);
|
|
1135
|
+
if (!existsSync3(src)) continue;
|
|
1136
|
+
ported.push({ name, body: stripClaudeFrontmatter(readFileSync2(src, "utf8")) });
|
|
1137
|
+
}
|
|
1138
|
+
return ported;
|
|
1139
|
+
}
|
|
1140
|
+
function renderRulesBlock(rules) {
|
|
1141
|
+
if (rules.length === 0) return "";
|
|
1142
|
+
return rules.map((r) => r.body.replace(/^#\s+/, "## ")).join("\n\n---\n\n");
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1362
1145
|
// src/antigravity/transform.ts
|
|
1363
1146
|
function runAntigravityTransform(params) {
|
|
1364
|
-
const {
|
|
1147
|
+
const {
|
|
1148
|
+
harnessRoot,
|
|
1149
|
+
projectDir,
|
|
1150
|
+
selectedInternalSkills = [],
|
|
1151
|
+
rules = [],
|
|
1152
|
+
baseline,
|
|
1153
|
+
refreshOnly
|
|
1154
|
+
} = params;
|
|
1365
1155
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1366
1156
|
const rulesFile = writeRules(harnessRoot, projectDir, writer);
|
|
1157
|
+
const antigravityInstalled = rulesFile !== null;
|
|
1158
|
+
const harnessRuleFiles = [];
|
|
1159
|
+
for (const rule of portRules(harnessRoot, rules)) {
|
|
1160
|
+
const target = join3(projectDir, ".agents", "rules", `${rule.name}.md`);
|
|
1161
|
+
if (!writer.write(target, `${rule.body}
|
|
1162
|
+
`, { createInRefresh: antigravityInstalled })) {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
harnessRuleFiles.push(target);
|
|
1166
|
+
}
|
|
1367
1167
|
const skillFiles = [];
|
|
1368
1168
|
for (const id of selectedInternalSkills) {
|
|
1369
|
-
const src =
|
|
1370
|
-
if (!
|
|
1169
|
+
const src = join3(harnessRoot, "templates/skills", id, "SKILL.md");
|
|
1170
|
+
if (!existsSync4(src)) {
|
|
1371
1171
|
continue;
|
|
1372
1172
|
}
|
|
1373
|
-
const target =
|
|
1374
|
-
if (!writer.write(target, renderBundledSkill(
|
|
1173
|
+
const target = join3(projectDir, ".agents", "skills", id, "SKILL.md");
|
|
1174
|
+
if (!writer.write(target, renderBundledSkill(readFileSync3(src, "utf8")))) continue;
|
|
1375
1175
|
skillFiles.push(target);
|
|
1376
1176
|
}
|
|
1377
1177
|
return {
|
|
1378
1178
|
rulesFile,
|
|
1179
|
+
harnessRuleFiles,
|
|
1379
1180
|
skillFiles,
|
|
1380
1181
|
ownership: writer.result()
|
|
1381
1182
|
};
|
|
1382
1183
|
}
|
|
1383
1184
|
function writeRules(harnessRoot, projectDir, writer) {
|
|
1384
|
-
const claudeMdPath =
|
|
1385
|
-
const templatePath =
|
|
1386
|
-
if (!
|
|
1185
|
+
const claudeMdPath = join3(harnessRoot, "templates/CLAUDE.md");
|
|
1186
|
+
const templatePath = join3(harnessRoot, "templates/antigravity/AGENTS.md.template");
|
|
1187
|
+
if (!existsSync4(claudeMdPath) || !existsSync4(templatePath)) {
|
|
1387
1188
|
return null;
|
|
1388
1189
|
}
|
|
1389
|
-
const claudeMd =
|
|
1390
|
-
const template =
|
|
1391
|
-
const target =
|
|
1190
|
+
const claudeMd = readFileSync3(claudeMdPath, "utf8");
|
|
1191
|
+
const template = readFileSync3(templatePath, "utf8");
|
|
1192
|
+
const target = join3(projectDir, ".agents", "rules", "uzys-harness.md");
|
|
1392
1193
|
const rulesOut = renderAgentsMd({
|
|
1393
1194
|
template,
|
|
1394
1195
|
claudeMd,
|
|
@@ -1401,18 +1202,18 @@ function writeRules(harnessRoot, projectDir, writer) {
|
|
|
1401
1202
|
// src/codex/opt-in.ts
|
|
1402
1203
|
init_esm_shims();
|
|
1403
1204
|
import { homedir } from "os";
|
|
1404
|
-
import { join as
|
|
1205
|
+
import { join as join4 } from "path";
|
|
1405
1206
|
|
|
1406
1207
|
// src/codex/trust-entry.ts
|
|
1407
1208
|
init_esm_shims();
|
|
1408
|
-
import { existsSync as
|
|
1409
|
-
import { dirname as
|
|
1209
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1210
|
+
import { dirname as dirname2 } from "path";
|
|
1410
1211
|
var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
|
|
1411
1212
|
function registerTrustEntry(opts) {
|
|
1412
1213
|
const { configPath, projectDir } = opts;
|
|
1413
1214
|
try {
|
|
1414
|
-
|
|
1415
|
-
const existing =
|
|
1215
|
+
mkdirSync2(dirname2(configPath), { recursive: true });
|
|
1216
|
+
const existing = existsSync5(configPath) ? readFileSync4(configPath, "utf8") : "";
|
|
1416
1217
|
if (hasTrustEntry(existing, projectDir)) {
|
|
1417
1218
|
return { status: "already-present" };
|
|
1418
1219
|
}
|
|
@@ -1420,7 +1221,7 @@ function registerTrustEntry(opts) {
|
|
|
1420
1221
|
[projects."${projectDir}"]
|
|
1421
1222
|
trust_level = "trusted"
|
|
1422
1223
|
`;
|
|
1423
|
-
|
|
1224
|
+
writeFileSync2(configPath, existing + block);
|
|
1424
1225
|
return { status: "registered" };
|
|
1425
1226
|
} catch (e2) {
|
|
1426
1227
|
return {
|
|
@@ -1436,8 +1237,8 @@ function hasTrustEntry(configContent, projectDir) {
|
|
|
1436
1237
|
|
|
1437
1238
|
// src/codex/opt-in.ts
|
|
1438
1239
|
function runCodexOptIn(ctx) {
|
|
1439
|
-
const codexHome = ctx.codexHome ??
|
|
1440
|
-
const configPath =
|
|
1240
|
+
const codexHome = ctx.codexHome ?? join4(homedir(), ".codex");
|
|
1241
|
+
const configPath = join4(codexHome, "config.toml");
|
|
1441
1242
|
const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
|
|
1442
1243
|
return {
|
|
1443
1244
|
trustEntry: {
|
|
@@ -1450,8 +1251,8 @@ function runCodexOptIn(ctx) {
|
|
|
1450
1251
|
|
|
1451
1252
|
// src/codex/transform.ts
|
|
1452
1253
|
init_esm_shims();
|
|
1453
|
-
import { chmodSync, existsSync as
|
|
1454
|
-
import { basename as basename2, join as
|
|
1254
|
+
import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
1255
|
+
import { basename as basename2, join as join5 } from "path";
|
|
1455
1256
|
|
|
1456
1257
|
// src/codex/config-toml.ts
|
|
1457
1258
|
init_esm_shims();
|
|
@@ -1519,23 +1320,32 @@ function quoteIfNeeded(name) {
|
|
|
1519
1320
|
var HOOK_NAMES = ["session-start"];
|
|
1520
1321
|
var ENV_VAR_RENAME = /CLAUDE_PROJECT_DIR/g;
|
|
1521
1322
|
function runCodexTransform(params) {
|
|
1522
|
-
const {
|
|
1323
|
+
const {
|
|
1324
|
+
harnessRoot,
|
|
1325
|
+
projectDir,
|
|
1326
|
+
selectedInternalSkills = [],
|
|
1327
|
+
rules = [],
|
|
1328
|
+
baseline,
|
|
1329
|
+
refreshOnly
|
|
1330
|
+
} = params;
|
|
1523
1331
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1524
|
-
const claudeMd = readRequired(
|
|
1525
|
-
const agentsTemplate = readRequired(
|
|
1526
|
-
const configTemplate = readRequired(
|
|
1332
|
+
const claudeMd = readRequired(join5(harnessRoot, "templates/CLAUDE.md"));
|
|
1333
|
+
const agentsTemplate = readRequired(join5(harnessRoot, "templates/codex/AGENTS.md.template"));
|
|
1334
|
+
const configTemplate = readRequired(join5(harnessRoot, "templates/codex/config.toml.template"));
|
|
1527
1335
|
const projectName = basename2(projectDir);
|
|
1528
|
-
const mcp = readOptionalJson(
|
|
1529
|
-
const agentsMdPath =
|
|
1336
|
+
const mcp = readOptionalJson(join5(harnessRoot, ".mcp.json"));
|
|
1337
|
+
const agentsMdPath = join5(projectDir, "AGENTS.md");
|
|
1530
1338
|
ensureDir(projectDir);
|
|
1531
1339
|
const agentsMdOut = renderAgentsMd({
|
|
1532
1340
|
template: agentsTemplate,
|
|
1533
1341
|
claudeMd,
|
|
1534
1342
|
projectName,
|
|
1535
|
-
projectContext: renderFillScaffold()
|
|
1343
|
+
projectContext: renderFillScaffold(),
|
|
1344
|
+
// Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
|
|
1345
|
+
harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
|
|
1536
1346
|
});
|
|
1537
1347
|
writer.write(agentsMdPath, agentsMdOut);
|
|
1538
|
-
const configTomlPath =
|
|
1348
|
+
const configTomlPath = join5(projectDir, ".codex/config.toml");
|
|
1539
1349
|
writer.write(
|
|
1540
1350
|
configTomlPath,
|
|
1541
1351
|
renderConfigToml({
|
|
@@ -1545,27 +1355,27 @@ function runCodexTransform(params) {
|
|
|
1545
1355
|
mcp
|
|
1546
1356
|
})
|
|
1547
1357
|
);
|
|
1548
|
-
const hookDir =
|
|
1358
|
+
const hookDir = join5(projectDir, ".codex/hooks");
|
|
1549
1359
|
const hookFiles = [];
|
|
1550
1360
|
for (const hook of HOOK_NAMES) {
|
|
1551
|
-
const src =
|
|
1552
|
-
if (!
|
|
1361
|
+
const src = join5(harnessRoot, "templates/hooks", `${hook}.sh`);
|
|
1362
|
+
if (!existsSync6(src)) {
|
|
1553
1363
|
continue;
|
|
1554
1364
|
}
|
|
1555
|
-
const ported =
|
|
1556
|
-
const target =
|
|
1365
|
+
const ported = readFileSync5(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
|
|
1366
|
+
const target = join5(hookDir, `${hook}.sh`);
|
|
1557
1367
|
if (!writer.write(target, ported)) continue;
|
|
1558
1368
|
chmodSync(target, 493);
|
|
1559
1369
|
hookFiles.push(target);
|
|
1560
1370
|
}
|
|
1561
1371
|
const skillFiles = [];
|
|
1562
1372
|
for (const id of selectedInternalSkills) {
|
|
1563
|
-
const src =
|
|
1564
|
-
if (!
|
|
1373
|
+
const src = join5(harnessRoot, "templates/skills", id, "SKILL.md");
|
|
1374
|
+
if (!existsSync6(src)) {
|
|
1565
1375
|
continue;
|
|
1566
1376
|
}
|
|
1567
|
-
const target =
|
|
1568
|
-
if (!writer.write(target, renderBundledSkill(
|
|
1377
|
+
const target = join5(projectDir, ".agents", "skills", id, "SKILL.md");
|
|
1378
|
+
if (!writer.write(target, renderBundledSkill(readFileSync5(src, "utf8")))) continue;
|
|
1569
1379
|
skillFiles.push(target);
|
|
1570
1380
|
}
|
|
1571
1381
|
return {
|
|
@@ -1577,17 +1387,17 @@ function runCodexTransform(params) {
|
|
|
1577
1387
|
};
|
|
1578
1388
|
}
|
|
1579
1389
|
function readRequired(path) {
|
|
1580
|
-
if (!
|
|
1390
|
+
if (!existsSync6(path)) {
|
|
1581
1391
|
throw new Error(`Codex transform: required source missing: ${path}`);
|
|
1582
1392
|
}
|
|
1583
|
-
return
|
|
1393
|
+
return readFileSync5(path, "utf8");
|
|
1584
1394
|
}
|
|
1585
1395
|
function readOptionalJson(path) {
|
|
1586
|
-
if (!
|
|
1396
|
+
if (!existsSync6(path)) {
|
|
1587
1397
|
return null;
|
|
1588
1398
|
}
|
|
1589
1399
|
try {
|
|
1590
|
-
return JSON.parse(
|
|
1400
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1591
1401
|
} catch {
|
|
1592
1402
|
return null;
|
|
1593
1403
|
}
|
|
@@ -1595,8 +1405,8 @@ function readOptionalJson(path) {
|
|
|
1595
1405
|
|
|
1596
1406
|
// src/opencode/transform.ts
|
|
1597
1407
|
init_esm_shims();
|
|
1598
|
-
import { existsSync as
|
|
1599
|
-
import { basename as basename3, join as
|
|
1408
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
1409
|
+
import { basename as basename3, join as join6 } from "path";
|
|
1600
1410
|
|
|
1601
1411
|
// src/opencode/agents-md.ts
|
|
1602
1412
|
init_esm_shims();
|
|
@@ -1605,7 +1415,7 @@ function renameSlashes2(text) {
|
|
|
1605
1415
|
}
|
|
1606
1416
|
function renderAgentsMd2(params) {
|
|
1607
1417
|
const body = params.claudeMd.replace(/^#\s+.*\r?\n/, "").trim();
|
|
1608
|
-
const replaced = params.template.replaceAll("{PROJECT_NAME}", params.projectName).replaceAll("{PROJECT_RULES}", body).replaceAll("{PROJECT_CONTEXT}", params.projectContext);
|
|
1418
|
+
const replaced = params.template.replaceAll("{PROJECT_NAME}", params.projectName).replaceAll("{PROJECT_RULES}", body).replaceAll("{PROJECT_CONTEXT}", params.projectContext).replaceAll("{HARNESS_RULES}", params.harnessRules ?? "");
|
|
1609
1419
|
return renameSlashes2(replaced);
|
|
1610
1420
|
}
|
|
1611
1421
|
|
|
@@ -1696,39 +1506,50 @@ function parseTemplate(template) {
|
|
|
1696
1506
|
|
|
1697
1507
|
// src/opencode/transform.ts
|
|
1698
1508
|
function runOpencodeTransform(params) {
|
|
1699
|
-
const {
|
|
1509
|
+
const {
|
|
1510
|
+
harnessRoot,
|
|
1511
|
+
projectDir,
|
|
1512
|
+
selectedInternalSkills = [],
|
|
1513
|
+
rules = [],
|
|
1514
|
+
baseline,
|
|
1515
|
+
refreshOnly
|
|
1516
|
+
} = params;
|
|
1700
1517
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1701
|
-
const claudeMd = readRequired2(
|
|
1702
|
-
const agentsTemplate = readRequired2(
|
|
1518
|
+
const claudeMd = readRequired2(join6(harnessRoot, "templates/CLAUDE.md"));
|
|
1519
|
+
const agentsTemplate = readRequired2(join6(harnessRoot, "templates/opencode/AGENTS.md.template"));
|
|
1703
1520
|
const opencodeTemplate = readRequired2(
|
|
1704
|
-
|
|
1521
|
+
join6(harnessRoot, "templates/opencode/opencode.json.template")
|
|
1705
1522
|
);
|
|
1706
1523
|
const projectName = basename3(projectDir);
|
|
1707
|
-
const mcp = readOptionalJson2(
|
|
1524
|
+
const mcp = readOptionalJson2(join6(harnessRoot, ".mcp.json"));
|
|
1708
1525
|
ensureDir(projectDir);
|
|
1709
|
-
const agentsMdPath =
|
|
1526
|
+
const agentsMdPath = join6(projectDir, "AGENTS.md");
|
|
1710
1527
|
const agentsMdOut = renderAgentsMd2({
|
|
1711
1528
|
template: agentsTemplate,
|
|
1712
1529
|
claudeMd,
|
|
1713
1530
|
projectName,
|
|
1714
|
-
projectContext: renderFillScaffold()
|
|
1531
|
+
projectContext: renderFillScaffold(),
|
|
1532
|
+
// codex 와 **같은 파일**(프로젝트 루트 `AGENTS.md`)이다. 두 transform 이 서로 다른 본문을
|
|
1533
|
+
// 쓰면 나중에 도는 쪽이 앞선 쪽을 덮어써, codex+opencode 조합에서 룰이 통째로 사라진다
|
|
1534
|
+
// (독립 검증 C-1 실측). 같은 내용을 쓰면 순서가 결과를 바꾸지 않는다.
|
|
1535
|
+
harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
|
|
1715
1536
|
});
|
|
1716
1537
|
writer.write(agentsMdPath, agentsMdOut);
|
|
1717
|
-
const opencodeJsonPath =
|
|
1538
|
+
const opencodeJsonPath = join6(projectDir, "opencode.json");
|
|
1718
1539
|
writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
|
|
1719
|
-
const cmdDir =
|
|
1540
|
+
const cmdDir = join6(projectDir, ".opencode/commands");
|
|
1720
1541
|
if (!refreshOnly) ensureDir(cmdDir);
|
|
1721
1542
|
const commandFiles = [];
|
|
1722
1543
|
for (const id of selectedInternalSkills) {
|
|
1723
|
-
const src =
|
|
1724
|
-
if (!
|
|
1544
|
+
const src = join6(harnessRoot, "templates/skills", id, "SKILL.md");
|
|
1545
|
+
if (!existsSync7(src)) {
|
|
1725
1546
|
continue;
|
|
1726
1547
|
}
|
|
1727
|
-
const target =
|
|
1728
|
-
const shellDependent =
|
|
1548
|
+
const target = join6(cmdDir, `${id}.md`);
|
|
1549
|
+
const shellDependent = existsSync7(join6(harnessRoot, "templates/skills", id, "scripts"));
|
|
1729
1550
|
const wrote = writer.write(
|
|
1730
1551
|
target,
|
|
1731
|
-
renderCommandFromSkill(
|
|
1552
|
+
renderCommandFromSkill(readFileSync6(src, "utf8"), id, { shellDependent })
|
|
1732
1553
|
);
|
|
1733
1554
|
if (!wrote) continue;
|
|
1734
1555
|
commandFiles.push(target);
|
|
@@ -1741,17 +1562,17 @@ function runOpencodeTransform(params) {
|
|
|
1741
1562
|
};
|
|
1742
1563
|
}
|
|
1743
1564
|
function readRequired2(path) {
|
|
1744
|
-
if (!
|
|
1565
|
+
if (!existsSync7(path)) {
|
|
1745
1566
|
throw new Error(`OpenCode transform: required source missing: ${path}`);
|
|
1746
1567
|
}
|
|
1747
|
-
return
|
|
1568
|
+
return readFileSync6(path, "utf8");
|
|
1748
1569
|
}
|
|
1749
1570
|
function readOptionalJson2(path) {
|
|
1750
|
-
if (!
|
|
1571
|
+
if (!existsSync7(path)) {
|
|
1751
1572
|
return null;
|
|
1752
1573
|
}
|
|
1753
1574
|
try {
|
|
1754
|
-
return JSON.parse(
|
|
1575
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1755
1576
|
} catch {
|
|
1756
1577
|
return null;
|
|
1757
1578
|
}
|
|
@@ -1765,6 +1586,7 @@ function runCliTransforms(params) {
|
|
|
1765
1586
|
projectDir,
|
|
1766
1587
|
cli: cli2,
|
|
1767
1588
|
selectedInternalSkills,
|
|
1589
|
+
rules,
|
|
1768
1590
|
previousExternal,
|
|
1769
1591
|
refreshOnly = false,
|
|
1770
1592
|
codexTrust = false
|
|
@@ -1790,6 +1612,7 @@ function runCliTransforms(params) {
|
|
|
1790
1612
|
harnessRoot,
|
|
1791
1613
|
projectDir,
|
|
1792
1614
|
selectedInternalSkills,
|
|
1615
|
+
rules,
|
|
1793
1616
|
baseline,
|
|
1794
1617
|
refreshOnly
|
|
1795
1618
|
});
|
|
@@ -1804,6 +1627,7 @@ function runCliTransforms(params) {
|
|
|
1804
1627
|
harnessRoot,
|
|
1805
1628
|
projectDir,
|
|
1806
1629
|
selectedInternalSkills,
|
|
1630
|
+
rules,
|
|
1807
1631
|
baseline,
|
|
1808
1632
|
refreshOnly
|
|
1809
1633
|
});
|
|
@@ -1815,6 +1639,7 @@ function runCliTransforms(params) {
|
|
|
1815
1639
|
harnessRoot,
|
|
1816
1640
|
projectDir,
|
|
1817
1641
|
selectedInternalSkills,
|
|
1642
|
+
rules,
|
|
1818
1643
|
baseline,
|
|
1819
1644
|
refreshOnly
|
|
1820
1645
|
});
|
|
@@ -1834,8 +1659,8 @@ function runCliTransforms(params) {
|
|
|
1834
1659
|
|
|
1835
1660
|
// src/env-files.ts
|
|
1836
1661
|
init_esm_shims();
|
|
1837
|
-
import { appendFileSync, existsSync as
|
|
1838
|
-
import { join as
|
|
1662
|
+
import { appendFileSync, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
1663
|
+
import { join as join7 } from "path";
|
|
1839
1664
|
var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
|
|
1840
1665
|
# Copy to .env (gitignored) and fill in values: cp .env.example .env
|
|
1841
1666
|
|
|
@@ -1872,19 +1697,19 @@ function writeEnvExample(projectDir, tracks) {
|
|
|
1872
1697
|
if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
|
|
1873
1698
|
return false;
|
|
1874
1699
|
}
|
|
1875
|
-
const path =
|
|
1876
|
-
if (
|
|
1700
|
+
const path = join7(projectDir, ".env.example");
|
|
1701
|
+
if (existsSync8(path)) {
|
|
1877
1702
|
return false;
|
|
1878
1703
|
}
|
|
1879
|
-
|
|
1704
|
+
writeFileSync3(path, ENV_EXAMPLE_BODY);
|
|
1880
1705
|
return true;
|
|
1881
1706
|
}
|
|
1882
1707
|
function addGitignoreEnv(projectDir) {
|
|
1883
|
-
const path =
|
|
1884
|
-
if (!
|
|
1708
|
+
const path = join7(projectDir, ".gitignore");
|
|
1709
|
+
if (!existsSync8(path)) {
|
|
1885
1710
|
return false;
|
|
1886
1711
|
}
|
|
1887
|
-
const content =
|
|
1712
|
+
const content = readFileSync7(path, "utf8");
|
|
1888
1713
|
if (GITIGNORE_ENV_PATTERN.test(content)) {
|
|
1889
1714
|
return false;
|
|
1890
1715
|
}
|
|
@@ -1898,11 +1723,11 @@ function addGitignoreEnv(projectDir) {
|
|
|
1898
1723
|
var AGENT_ARTIFACT_DIRS = [".factory/", ".goose/", ".uzys-agent-harness/"];
|
|
1899
1724
|
var GITIGNORE_AGENT_ARTIFACT_HEADER = "# agent CLI / harness \uC790\uB3D9 \uC0DD\uC131\uBB3C (auto-added by agent-harness)";
|
|
1900
1725
|
function addGitignoreAgentArtifacts(projectDir) {
|
|
1901
|
-
const path =
|
|
1902
|
-
if (!
|
|
1726
|
+
const path = join7(projectDir, ".gitignore");
|
|
1727
|
+
if (!existsSync8(path)) {
|
|
1903
1728
|
return [];
|
|
1904
1729
|
}
|
|
1905
|
-
const content =
|
|
1730
|
+
const content = readFileSync7(path, "utf8");
|
|
1906
1731
|
const missing = AGENT_ARTIFACT_DIRS.filter((pattern) => {
|
|
1907
1732
|
const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
|
|
1908
1733
|
return !lineRegex.test(content);
|
|
@@ -1917,43 +1742,13 @@ ${block}
|
|
|
1917
1742
|
`);
|
|
1918
1743
|
return [...missing];
|
|
1919
1744
|
}
|
|
1920
|
-
function writeMcpAllowlist(projectDir) {
|
|
1921
|
-
const allowlistPath = join8(projectDir, ".mcp-allowlist");
|
|
1922
|
-
if (existsSync9(allowlistPath)) {
|
|
1923
|
-
return null;
|
|
1924
|
-
}
|
|
1925
|
-
const mcpPath = join8(projectDir, ".mcp.json");
|
|
1926
|
-
if (!existsSync9(mcpPath)) {
|
|
1927
|
-
return null;
|
|
1928
|
-
}
|
|
1929
|
-
let names = [];
|
|
1930
|
-
try {
|
|
1931
|
-
const parsed = JSON.parse(readFileSync8(mcpPath, "utf8"));
|
|
1932
|
-
names = Object.keys(parsed.mcpServers ?? {}).sort();
|
|
1933
|
-
} catch {
|
|
1934
|
-
return null;
|
|
1935
|
-
}
|
|
1936
|
-
if (names.length === 0) {
|
|
1937
|
-
return [];
|
|
1938
|
-
}
|
|
1939
|
-
const body = [
|
|
1940
|
-
"# MCP Server Allowlist \u2014 auto-generated by agent-harness install",
|
|
1941
|
-
"# Referenced by mcp-pre-exec.sh hook. Remove or '#' comment any server you want to block.",
|
|
1942
|
-
"# Deleting this file disables the gate (allows all MCP calls).",
|
|
1943
|
-
"",
|
|
1944
|
-
...names,
|
|
1945
|
-
""
|
|
1946
|
-
].join("\n");
|
|
1947
|
-
writeFileSync4(allowlistPath, body);
|
|
1948
|
-
return names;
|
|
1949
|
-
}
|
|
1950
1745
|
|
|
1951
1746
|
// src/external-installer.ts
|
|
1952
1747
|
init_esm_shims();
|
|
1953
1748
|
import { spawnSync } from "child_process";
|
|
1954
|
-
import { existsSync as
|
|
1749
|
+
import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync8 } from "fs";
|
|
1955
1750
|
import { homedir as homedir2 } from "os";
|
|
1956
|
-
import { join as
|
|
1751
|
+
import { join as join8 } from "path";
|
|
1957
1752
|
var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
|
|
1958
1753
|
function selectExternalTargets(assets, ctx) {
|
|
1959
1754
|
const conditionPassed = filterApplicableAssets(assets, ctx).filter(
|
|
@@ -2029,8 +1824,8 @@ function installOne(asset, ctx) {
|
|
|
2029
1824
|
cwd
|
|
2030
1825
|
);
|
|
2031
1826
|
case "shell-script": {
|
|
2032
|
-
const scriptPath =
|
|
2033
|
-
if (!
|
|
1827
|
+
const scriptPath = join8(ctx.harnessRoot, method.script);
|
|
1828
|
+
if (!existsSync9(scriptPath)) {
|
|
2034
1829
|
return {
|
|
2035
1830
|
asset,
|
|
2036
1831
|
ok: false,
|
|
@@ -2120,17 +1915,17 @@ function detectVersion(method, spawn) {
|
|
|
2120
1915
|
if (at2 <= 0) return void 0;
|
|
2121
1916
|
const plugin = method.pluginId.slice(0, at2);
|
|
2122
1917
|
const marketplaceShort = method.pluginId.slice(at2 + 1);
|
|
2123
|
-
const cacheBase =
|
|
2124
|
-
if (!
|
|
2125
|
-
const versions =
|
|
1918
|
+
const cacheBase = join8(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
|
|
1919
|
+
if (!existsSync9(cacheBase)) return void 0;
|
|
1920
|
+
const versions = readdirSync(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
|
|
2126
1921
|
return versions.at(-1);
|
|
2127
1922
|
}
|
|
2128
1923
|
case "npm": {
|
|
2129
1924
|
const npmRoot = getNpmGlobalRoot(spawn);
|
|
2130
1925
|
if (!npmRoot) return void 0;
|
|
2131
|
-
const pkgJson =
|
|
2132
|
-
if (!
|
|
2133
|
-
const parsed = JSON.parse(
|
|
1926
|
+
const pkgJson = join8(npmRoot, method.pkg, "package.json");
|
|
1927
|
+
if (!existsSync9(pkgJson)) return void 0;
|
|
1928
|
+
const parsed = JSON.parse(readFileSync8(pkgJson, "utf8"));
|
|
2134
1929
|
return parsed.version;
|
|
2135
1930
|
}
|
|
2136
1931
|
default:
|
|
@@ -2157,7 +1952,7 @@ function getNpmGlobalRoot(spawn) {
|
|
|
2157
1952
|
|
|
2158
1953
|
// src/mcp-merge.ts
|
|
2159
1954
|
init_esm_shims();
|
|
2160
|
-
import { existsSync as
|
|
1955
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
2161
1956
|
function parseTrackMcpMap(raw) {
|
|
2162
1957
|
const rows = [];
|
|
2163
1958
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -2208,15 +2003,15 @@ function mergeMcpServers(base, rows, tracks) {
|
|
|
2208
2003
|
return out;
|
|
2209
2004
|
}
|
|
2210
2005
|
function composeMcpJson(opts) {
|
|
2211
|
-
const base = JSON.parse(
|
|
2212
|
-
const merged = opts.existingPath &&
|
|
2213
|
-
const mapRaw =
|
|
2006
|
+
const base = JSON.parse(readFileSync9(opts.templateMcpPath, "utf8"));
|
|
2007
|
+
const merged = opts.existingPath && existsSync10(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
|
|
2008
|
+
const mapRaw = existsSync10(opts.trackMapPath) ? readFileSync9(opts.trackMapPath, "utf8") : "";
|
|
2214
2009
|
const rows = parseTrackMcpMap(mapRaw);
|
|
2215
2010
|
return mergeMcpServers(merged, rows, opts.tracks);
|
|
2216
2011
|
}
|
|
2217
2012
|
function mergeUserBase(base, existingPath) {
|
|
2218
2013
|
try {
|
|
2219
|
-
const existing = JSON.parse(
|
|
2014
|
+
const existing = JSON.parse(readFileSync9(existingPath, "utf8"));
|
|
2220
2015
|
return {
|
|
2221
2016
|
...base,
|
|
2222
2017
|
mcpServers: { ...base.mcpServers, ...existing.mcpServers }
|
|
@@ -2226,23 +2021,58 @@ function mergeUserBase(base, existingPath) {
|
|
|
2226
2021
|
}
|
|
2227
2022
|
}
|
|
2228
2023
|
function writeMcpJson(path, mcp) {
|
|
2229
|
-
|
|
2024
|
+
writeFileSync4(path, `${JSON.stringify(mcp, null, 2)}
|
|
2230
2025
|
`);
|
|
2231
2026
|
}
|
|
2232
2027
|
|
|
2028
|
+
// src/superseded.ts
|
|
2029
|
+
init_esm_shims();
|
|
2030
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
2031
|
+
import { join as join9 } from "path";
|
|
2032
|
+
function sameTracks(a, b2) {
|
|
2033
|
+
if (a.length !== b2.length) return false;
|
|
2034
|
+
const sa = [...a].sort();
|
|
2035
|
+
const sb = [...b2].sort();
|
|
2036
|
+
return sa.every((t, i) => t === sb[i]);
|
|
2037
|
+
}
|
|
2038
|
+
function policyKey(claudeRelative) {
|
|
2039
|
+
return POLICY_DIRS.some(
|
|
2040
|
+
({ dir, ext }) => claudeRelative.startsWith(`${dir}/`) && claudeRelative.endsWith(ext)
|
|
2041
|
+
) ? claudeRelative : null;
|
|
2042
|
+
}
|
|
2043
|
+
function findSuperseded(projectDir, spec, previous) {
|
|
2044
|
+
if (!previous) return [];
|
|
2045
|
+
if (!sameTracks(previous.spec.tracks, spec.tracks)) return [];
|
|
2046
|
+
const baseline = new Map((previous.policyFiles ?? []).map((f) => [f.path, f.sha256]));
|
|
2047
|
+
if (baseline.size === 0) return [];
|
|
2048
|
+
const out = [];
|
|
2049
|
+
for (const entry of buildManifest(spec)) {
|
|
2050
|
+
if (entry.applies(spec)) continue;
|
|
2051
|
+
const id = classifyBaselineTarget(entry.target)?.id;
|
|
2052
|
+
if (!id) continue;
|
|
2053
|
+
const key = policyKey(entry.target.slice(".claude/".length));
|
|
2054
|
+
if (!key) continue;
|
|
2055
|
+
const abs = join9(projectDir, entry.target);
|
|
2056
|
+
if (!existsSync11(abs)) continue;
|
|
2057
|
+
if (!isHarnessOwned(baseline, key, readFileSync10(abs, "utf-8"))) continue;
|
|
2058
|
+
out.push({ target: entry.target, id, source: entry.source });
|
|
2059
|
+
}
|
|
2060
|
+
return out;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2233
2063
|
// src/update-mode.ts
|
|
2234
2064
|
init_esm_shims();
|
|
2235
2065
|
import {
|
|
2236
|
-
copyFileSync
|
|
2066
|
+
copyFileSync,
|
|
2237
2067
|
existsSync as existsSync12,
|
|
2238
2068
|
lstatSync,
|
|
2239
|
-
mkdirSync as
|
|
2240
|
-
readdirSync as
|
|
2069
|
+
mkdirSync as mkdirSync3,
|
|
2070
|
+
readdirSync as readdirSync2,
|
|
2241
2071
|
readFileSync as readFileSync11,
|
|
2242
2072
|
unlinkSync,
|
|
2243
|
-
writeFileSync as
|
|
2073
|
+
writeFileSync as writeFileSync5
|
|
2244
2074
|
} from "fs";
|
|
2245
|
-
import { basename as basename4, dirname as
|
|
2075
|
+
import { basename as basename4, dirname as dirname3, join as join10 } from "path";
|
|
2246
2076
|
var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
|
|
2247
2077
|
function buildUpdateSpec(projectDir, tracks) {
|
|
2248
2078
|
return {
|
|
@@ -2269,7 +2099,8 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot) {
|
|
|
2269
2099
|
externalBackedUp: [],
|
|
2270
2100
|
installedNew: [],
|
|
2271
2101
|
restored: [],
|
|
2272
|
-
needsReinstall: []
|
|
2102
|
+
needsReinstall: [],
|
|
2103
|
+
mcpAllowlistRetired: null
|
|
2273
2104
|
};
|
|
2274
2105
|
const fresh = installNewAssets(projectDir, templatesDir);
|
|
2275
2106
|
report.installedNew = fresh.installed;
|
|
@@ -2296,11 +2127,15 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot) {
|
|
|
2296
2127
|
report.skillsBackedUp = skillSync.backedUp;
|
|
2297
2128
|
report.skillsSkippedLinks = skillSync.skippedLinks;
|
|
2298
2129
|
refreshSkillBaseline(projectDir);
|
|
2299
|
-
|
|
2130
|
+
const installLog = readInstallLog(projectDir);
|
|
2131
|
+
if (existsSync12(claudeDir) && (installLog === null || installLog.spec.cli.includes("claude"))) {
|
|
2132
|
+
syncHarnessAnchor(projectDir, templatesDir, report);
|
|
2133
|
+
}
|
|
2300
2134
|
const settingsPath = join10(claudeDir, "settings.json");
|
|
2301
2135
|
if (existsSync12(settingsPath)) {
|
|
2302
2136
|
report.staleHookRefs = cleanStaleHookRefs(settingsPath, claudeDir);
|
|
2303
2137
|
}
|
|
2138
|
+
report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
|
|
2304
2139
|
const external = refreshExternalCli(projectDir, harnessRoot);
|
|
2305
2140
|
report.externalUpdated = external.externalUpdated;
|
|
2306
2141
|
report.externalBackedUp = external.externalBackedUp;
|
|
@@ -2310,10 +2145,13 @@ function installNewAssets(projectDir, templatesDir) {
|
|
|
2310
2145
|
const installed = [];
|
|
2311
2146
|
const restored = [];
|
|
2312
2147
|
const needsReinstall = [];
|
|
2313
|
-
const
|
|
2148
|
+
const log = readInstallLog(projectDir);
|
|
2149
|
+
const claudeSelected = log?.spec.cli.includes("claude") ?? false;
|
|
2150
|
+
const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
|
|
2314
2151
|
const priorBaseline = policyBaseline(projectDir);
|
|
2315
2152
|
for (const entry of trackOnlyFileAssets(installedTracks(projectDir))) {
|
|
2316
2153
|
if (entry.target === HARNESS_ANCHOR_FILE) continue;
|
|
2154
|
+
if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
|
|
2317
2155
|
const target = join10(projectDir, entry.target);
|
|
2318
2156
|
if (existsSync12(target)) continue;
|
|
2319
2157
|
const source = join10(templatesDir, entry.source);
|
|
@@ -2325,8 +2163,8 @@ function installNewAssets(projectDir, templatesDir) {
|
|
|
2325
2163
|
continue;
|
|
2326
2164
|
}
|
|
2327
2165
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2166
|
+
mkdirSync3(dirname3(target), { recursive: true });
|
|
2167
|
+
copyFileSync(source, target);
|
|
2330
2168
|
const recordedAs = entry.target.startsWith(".claude/") ? entry.target.slice(8) : entry.target;
|
|
2331
2169
|
if (priorBaseline.has(recordedAs)) restored.push(entry.target);
|
|
2332
2170
|
else installed.push(entry.target);
|
|
@@ -2349,7 +2187,7 @@ function syncHarnessAnchor(projectDir, templatesDir, report) {
|
|
|
2349
2187
|
if (!existsSync12(templateMd)) return;
|
|
2350
2188
|
const anchor = join10(projectDir, HARNESS_ANCHOR_FILE);
|
|
2351
2189
|
const existed = existsSync12(anchor);
|
|
2352
|
-
|
|
2190
|
+
copyFileSync(templateMd, anchor);
|
|
2353
2191
|
if (existed) {
|
|
2354
2192
|
report.claudeMdUpdated = true;
|
|
2355
2193
|
return;
|
|
@@ -2366,7 +2204,7 @@ function upsertRootImport(projectDir) {
|
|
|
2366
2204
|
tracks: installedTracks(projectDir)
|
|
2367
2205
|
});
|
|
2368
2206
|
if (next === existing) return false;
|
|
2369
|
-
|
|
2207
|
+
writeFileSync5(target, next);
|
|
2370
2208
|
return true;
|
|
2371
2209
|
}
|
|
2372
2210
|
function installedTracks(projectDir) {
|
|
@@ -2393,11 +2231,14 @@ function recordAnchorBaseline(projectDir, anchor) {
|
|
|
2393
2231
|
}
|
|
2394
2232
|
function refreshExternalCli(projectDir, harnessRoot) {
|
|
2395
2233
|
const log = readInstallLog(projectDir);
|
|
2234
|
+
const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
|
|
2396
2235
|
const result = runCliTransforms({
|
|
2397
2236
|
harnessRoot,
|
|
2398
2237
|
projectDir,
|
|
2399
2238
|
cli: ALL_CLI_TARGETS,
|
|
2400
2239
|
selectedInternalSkills: INTERNAL_BUNDLED_SKILL_IDS,
|
|
2240
|
+
// 스킬은 **전부** 넘긴다 — 안 깔린 스킬은 대상 파일이 없어 refreshOnly 가 건너뛴다.
|
|
2241
|
+
rules: ALL_RULES.filter((r) => !isBaselineExcluded(`.claude/rules/${r}.md`, baselineExcluded)),
|
|
2401
2242
|
previousExternal: log?.externalFiles ?? [],
|
|
2402
2243
|
refreshOnly: true
|
|
2403
2244
|
});
|
|
@@ -2423,7 +2264,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
|
2423
2264
|
if (!existsSync12(target) || !existsSync12(source)) return { updated: 0, backedUp: [] };
|
|
2424
2265
|
let updated = 0;
|
|
2425
2266
|
const backedUp = [];
|
|
2426
|
-
for (const file of
|
|
2267
|
+
for (const file of readdirSync2(target)) {
|
|
2427
2268
|
if (!file.endsWith(ext)) continue;
|
|
2428
2269
|
const targetFile = join10(target, file);
|
|
2429
2270
|
const sourceFile = join10(source, file);
|
|
@@ -2435,7 +2276,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
|
2435
2276
|
backupFile(targetFile, now);
|
|
2436
2277
|
backedUp.push(`${ctx.prefix}/${file}`);
|
|
2437
2278
|
}
|
|
2438
|
-
|
|
2279
|
+
writeFileSync5(targetFile, next);
|
|
2439
2280
|
updated++;
|
|
2440
2281
|
}
|
|
2441
2282
|
return { updated, backedUp };
|
|
@@ -2446,7 +2287,7 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
|
|
|
2446
2287
|
let updated = 0;
|
|
2447
2288
|
const backedUp = [];
|
|
2448
2289
|
const skippedLinks = [];
|
|
2449
|
-
for (const skill of
|
|
2290
|
+
for (const skill of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2450
2291
|
if (!skill.isDirectory()) continue;
|
|
2451
2292
|
const targetSkill = join10(targetDir, skill.name);
|
|
2452
2293
|
if (!existsSync12(targetSkill)) continue;
|
|
@@ -2458,8 +2299,8 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
|
|
|
2458
2299
|
const targetFile = join10(targetSkill, rel);
|
|
2459
2300
|
const next = readFileSync11(join10(sourceDir, skill.name, rel), "utf8");
|
|
2460
2301
|
if (!existsSync12(targetFile)) {
|
|
2461
|
-
|
|
2462
|
-
|
|
2302
|
+
mkdirSync3(dirname3(targetFile), { recursive: true });
|
|
2303
|
+
writeFileSync5(targetFile, next);
|
|
2463
2304
|
updated++;
|
|
2464
2305
|
continue;
|
|
2465
2306
|
}
|
|
@@ -2470,7 +2311,7 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
|
|
|
2470
2311
|
backupFile(targetFile, now);
|
|
2471
2312
|
backedUp.push(`${skill.name}/${rel}`);
|
|
2472
2313
|
}
|
|
2473
|
-
|
|
2314
|
+
writeFileSync5(targetFile, next);
|
|
2474
2315
|
updated++;
|
|
2475
2316
|
}
|
|
2476
2317
|
}
|
|
@@ -2511,7 +2352,7 @@ function refreshSkillBaseline(projectDir) {
|
|
|
2511
2352
|
function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
2512
2353
|
if (!existsSync12(target) || !existsSync12(source)) return [];
|
|
2513
2354
|
const removed = [];
|
|
2514
|
-
for (const file of
|
|
2355
|
+
for (const file of readdirSync2(target)) {
|
|
2515
2356
|
if (!file.endsWith(ext)) continue;
|
|
2516
2357
|
const sourceFile = join10(source, file);
|
|
2517
2358
|
if (!existsSync12(sourceFile)) {
|
|
@@ -2528,6 +2369,17 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
|
|
|
2528
2369
|
}
|
|
2529
2370
|
return removed;
|
|
2530
2371
|
}
|
|
2372
|
+
function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
|
|
2373
|
+
const target = join10(projectDir, ".mcp-allowlist");
|
|
2374
|
+
if (!existsSync12(target)) return null;
|
|
2375
|
+
try {
|
|
2376
|
+
const backup = backupFile(target, now);
|
|
2377
|
+
unlinkSync(target);
|
|
2378
|
+
return backup;
|
|
2379
|
+
} catch {
|
|
2380
|
+
return null;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2531
2383
|
function cleanStaleHookRefs(settingsPath, claudeDir) {
|
|
2532
2384
|
let settings;
|
|
2533
2385
|
try {
|
|
@@ -2550,7 +2402,7 @@ function cleanStaleHookRefs(settingsPath, claudeDir) {
|
|
|
2550
2402
|
}
|
|
2551
2403
|
if (removed.length > 0) {
|
|
2552
2404
|
const next = { ...settings, hooks: cleanedHooks };
|
|
2553
|
-
|
|
2405
|
+
writeFileSync5(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
2554
2406
|
`);
|
|
2555
2407
|
}
|
|
2556
2408
|
return removed;
|
|
@@ -2590,17 +2442,27 @@ function runInstall(ctx) {
|
|
|
2590
2442
|
throw new Error(`Templates dir not found: ${templatesDir}`);
|
|
2591
2443
|
}
|
|
2592
2444
|
const claudeDir = join11(projectDir, ".claude");
|
|
2593
|
-
if (mode === "update" && !existsSync13(claudeDir)) {
|
|
2594
|
-
throw new Error(`Update mode requires existing .claude/ at ${claudeDir}`);
|
|
2595
|
-
}
|
|
2596
2445
|
const previousLog = readInstallLog(projectDir);
|
|
2446
|
+
const claudeWasSelected = previousLog?.spec.cli.includes("claude") ?? false;
|
|
2447
|
+
if (mode === "update" && !existsSync13(claudeDir) && (previousLog === null || claudeWasSelected)) {
|
|
2448
|
+
throw new Error(
|
|
2449
|
+
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}`
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2597
2452
|
const backupPath = resolveBackupPath(ctx, mode, claudeDir);
|
|
2598
2453
|
if (mode === "update") {
|
|
2599
2454
|
return runUpdateInstall(ctx, templatesDir, backupPath);
|
|
2600
2455
|
}
|
|
2601
2456
|
const manifestSpec = buildManifestSpec(spec);
|
|
2602
2457
|
const policyBase = new Map((previousLog?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
|
|
2603
|
-
const
|
|
2458
|
+
const baselineExcluded = new Set(spec.baselineExclude ?? []);
|
|
2459
|
+
const supersededFound = findSuperseded(projectDir, manifestSpec, previousLog);
|
|
2460
|
+
const base = spec.cli.includes("claude") ? installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase, baselineExcluded) : (
|
|
2461
|
+
// claude 미선택이어도 CLI 중립 자산은 깔린다. manifest 전체가 `.claude/` baseline 안에서만
|
|
2462
|
+
// 돌던 탓에 이 자산들이 claude 설치에만 도달했는데, **배포 룰 본문이 이 스크립트들을
|
|
2463
|
+
// 호출 지점으로 지목한다** — 즉 없는 도구를 있다고 안내하고 있었다(#300 과 같은 형태).
|
|
2464
|
+
installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselineExcluded)
|
|
2465
|
+
);
|
|
2604
2466
|
const mcpResult = composeAndWriteMcp(harnessRoot, projectDir, spec);
|
|
2605
2467
|
const ciScaffold = isAssetSelected("ci-scaffold", {
|
|
2606
2468
|
tracks: spec.tracks,
|
|
@@ -2618,6 +2480,11 @@ function runInstall(ctx) {
|
|
|
2618
2480
|
projectDir,
|
|
2619
2481
|
cli: spec.cli,
|
|
2620
2482
|
selectedInternalSkills: manifestSpec.selectedInternalSkills,
|
|
2483
|
+
// 룰 목록의 SSOT 는 하나다 — `.claude/rules/` 를 채우는 것과 같은 `resolveRules` 결과가
|
|
2484
|
+
// 나머지 세 CLI 로도 간다. 여기서 다시 고르면 CLI 마다 다른 룰이 깔린다.
|
|
2485
|
+
rules: resolveRules(manifestSpec).filter(
|
|
2486
|
+
(r) => !isBaselineExcluded(`.claude/rules/${r}.md`, baselineExcluded)
|
|
2487
|
+
),
|
|
2621
2488
|
previousExternal: previousLog?.externalFiles ?? [],
|
|
2622
2489
|
codexTrust: (spec.scope ?? "project") === "global" && spec.options.withCodexTrust
|
|
2623
2490
|
});
|
|
@@ -2637,7 +2504,12 @@ function runInstall(ctx) {
|
|
|
2637
2504
|
rootClaudeMd: base.rootClaudeMd,
|
|
2638
2505
|
// 외부 CLI 백업도 같은 줄에 노출한다 — 백업이 화면에 안 보이면 사용자는 자기 편집분이
|
|
2639
2506
|
// 어디 갔는지 알 수 없고, 그러면 백업은 있어도 없는 것과 같다 (ADR-046/047 과 같은 이유).
|
|
2640
|
-
backups: [...base.backups, ...externalBackups]
|
|
2507
|
+
backups: [...base.backups, ...externalBackups],
|
|
2508
|
+
baselineExcluded: base.excluded,
|
|
2509
|
+
baselineExcludedOnDisk: base.excludedOnDisk,
|
|
2510
|
+
// 밀려난 자산 정리는 **manifest 복사가 끝난 뒤**에 한다. 앞에서 지우면 이번 설치가 다시
|
|
2511
|
+
// 깔 수도 있는 파일을 지웠다 되돌리는 셈이라, 무엇이 최종 상태인지 보고가 흐려진다.
|
|
2512
|
+
superseded: applySupersededCleanup(spec, projectDir, supersededFound)
|
|
2641
2513
|
};
|
|
2642
2514
|
ctx.onProgress?.({ type: "baseline-complete", baseline });
|
|
2643
2515
|
const external = runExternalPhase(ctx);
|
|
@@ -2653,6 +2525,21 @@ function runInstall(ctx) {
|
|
|
2653
2525
|
);
|
|
2654
2526
|
return { ...baseline, external, staleHookRefs };
|
|
2655
2527
|
}
|
|
2528
|
+
function applySupersededCleanup(spec, projectDir, found) {
|
|
2529
|
+
if (found.length === 0) return { removed: [], kept: [] };
|
|
2530
|
+
if (!spec.cleanSuperseded) return { removed: [], kept: found.map((f) => f.target) };
|
|
2531
|
+
const removed = [];
|
|
2532
|
+
const kept = [];
|
|
2533
|
+
for (const asset of found) {
|
|
2534
|
+
try {
|
|
2535
|
+
unlinkSync2(join11(projectDir, asset.target));
|
|
2536
|
+
removed.push(asset.target);
|
|
2537
|
+
} catch {
|
|
2538
|
+
kept.push(asset.target);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
return { removed, kept };
|
|
2542
|
+
}
|
|
2656
2543
|
function healStaleHookRefs(spec, projectDir) {
|
|
2657
2544
|
if (!spec.cli.includes("claude")) return [];
|
|
2658
2545
|
const settingsPath = join11(projectDir, ".claude/settings.json");
|
|
@@ -2670,6 +2557,11 @@ function runUpdateInstall(ctx, templatesDir, backupPath) {
|
|
|
2670
2557
|
filesCopied: 0,
|
|
2671
2558
|
dirsCopied: 0,
|
|
2672
2559
|
skipped: 0,
|
|
2560
|
+
baselineExcluded: [],
|
|
2561
|
+
baselineExcludedOnDisk: [],
|
|
2562
|
+
// update 는 밀려난 자산을 다루지 않는다 — 그 판정은 사용자가 옵션을 고르는 설치 경로의
|
|
2563
|
+
// 것이고, update 는 무엇을 골랐는지 다시 묻지 않는다.
|
|
2564
|
+
superseded: { removed: [], kept: [] },
|
|
2673
2565
|
backup: backupPath,
|
|
2674
2566
|
installedTracks: [...ctx.spec.tracks].sort(),
|
|
2675
2567
|
mcpServers: [],
|
|
@@ -2683,7 +2575,6 @@ function runUpdateInstall(ctx, templatesDir, backupPath) {
|
|
|
2683
2575
|
envFiles: {
|
|
2684
2576
|
envExampleCreated: false,
|
|
2685
2577
|
gitignoreEnvAdded: false,
|
|
2686
|
-
mcpAllowlist: null,
|
|
2687
2578
|
gitignoreNpxSkillsAdded: []
|
|
2688
2579
|
},
|
|
2689
2580
|
rootClaudeMd: null
|
|
@@ -2720,9 +2611,30 @@ function emptyClaudeBaseline() {
|
|
|
2720
2611
|
categories: { rules: [], agents: [], hooks: [], commands: 0, skills: [] },
|
|
2721
2612
|
rootClaudeMd: null,
|
|
2722
2613
|
rootClaudeMdLog: null,
|
|
2723
|
-
backups: []
|
|
2614
|
+
backups: [],
|
|
2615
|
+
excluded: [],
|
|
2616
|
+
excludedOnDisk: []
|
|
2724
2617
|
};
|
|
2725
2618
|
}
|
|
2619
|
+
function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselineExcluded) {
|
|
2620
|
+
const result = emptyClaudeBaseline();
|
|
2621
|
+
for (const entry of buildManifest(manifestSpec)) {
|
|
2622
|
+
if (!entry.applies(manifestSpec)) continue;
|
|
2623
|
+
if (entry.target.startsWith(".claude/rules/") && isBaselineExcluded(entry.target, baselineExcluded)) {
|
|
2624
|
+
result.excluded.push(entry.target);
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
if (!isCliNeutralTarget(entry.target)) continue;
|
|
2628
|
+
const source = join11(templatesDir, entry.source);
|
|
2629
|
+
if (!existsSync13(source)) {
|
|
2630
|
+
result.skipped += 1;
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
copyFile(source, join11(projectDir, entry.target));
|
|
2634
|
+
result.filesCopied += 1;
|
|
2635
|
+
}
|
|
2636
|
+
return result;
|
|
2637
|
+
}
|
|
2726
2638
|
function backupEditedPolicyFile(entryTarget, target, source, baseline) {
|
|
2727
2639
|
const prefix = ".claude/";
|
|
2728
2640
|
if (!entryTarget.startsWith(prefix)) return null;
|
|
@@ -2737,7 +2649,7 @@ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
|
|
|
2737
2649
|
if (recorded !== void 0 && recorded === hashContent(current)) return null;
|
|
2738
2650
|
return backupFile(target);
|
|
2739
2651
|
}
|
|
2740
|
-
function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase) {
|
|
2652
|
+
function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase, baselineExcluded) {
|
|
2741
2653
|
ensureProjectSkeleton(projectDir);
|
|
2742
2654
|
const result = emptyClaudeBaseline();
|
|
2743
2655
|
const manifest = buildManifest(manifestSpec);
|
|
@@ -2745,8 +2657,13 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
|
|
|
2745
2657
|
if (!entry.applies(manifestSpec)) {
|
|
2746
2658
|
continue;
|
|
2747
2659
|
}
|
|
2748
|
-
const source = join11(templatesDir, entry.source);
|
|
2749
2660
|
const target = join11(projectDir, entry.target);
|
|
2661
|
+
if (isBaselineExcluded(entry.target, baselineExcluded)) {
|
|
2662
|
+
result.excluded.push(entry.target);
|
|
2663
|
+
if (existsSync13(target)) result.excludedOnDisk.push(entry.target);
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
const source = join11(templatesDir, entry.source);
|
|
2750
2667
|
if (!existsSync13(source)) {
|
|
2751
2668
|
result.skipped += 1;
|
|
2752
2669
|
continue;
|
|
@@ -2790,7 +2707,6 @@ function writeEnvironmentFiles(projectDir, tracks) {
|
|
|
2790
2707
|
return {
|
|
2791
2708
|
envExampleCreated: writeEnvExample(projectDir, tracks),
|
|
2792
2709
|
gitignoreEnvAdded: addGitignoreEnv(projectDir),
|
|
2793
|
-
mcpAllowlist: writeMcpAllowlist(projectDir),
|
|
2794
2710
|
// v0.8.0 — `.factory/`, `.goose/` ignore (npx skills universal install 사용자 #3).
|
|
2795
2711
|
// 2026-08-02 — `.uzys-agent-harness/` 합류 (설치 로그 + 훅 차단 로그).
|
|
2796
2712
|
gitignoreNpxSkillsAdded: addGitignoreAgentArtifacts(projectDir)
|
|
@@ -2882,13 +2798,6 @@ function collectRootFiles(envFiles, ciScaffold, mcpCreated) {
|
|
|
2882
2798
|
if (envFiles.envExampleCreated) {
|
|
2883
2799
|
files.push({ path: ".env.example", change: "created", notes: ["Supabase \uD1A0\uD070 \uAC00\uC774\uB4DC"] });
|
|
2884
2800
|
}
|
|
2885
|
-
if (envFiles.mcpAllowlist && envFiles.mcpAllowlist.length > 0) {
|
|
2886
|
-
files.push({
|
|
2887
|
-
path: ".mcp-allowlist",
|
|
2888
|
-
change: "created",
|
|
2889
|
-
notes: [`MCP allowlist (${envFiles.mcpAllowlist.length} server)`]
|
|
2890
|
-
});
|
|
2891
|
-
}
|
|
2892
2801
|
const gitignoreAdded = [
|
|
2893
2802
|
...envFiles.gitignoreEnvAdded ? [".env"] : [],
|
|
2894
2803
|
...envFiles.gitignoreNpxSkillsAdded
|
|
@@ -2925,9 +2834,9 @@ function accumulateCategory(cats, entry) {
|
|
|
2925
2834
|
}
|
|
2926
2835
|
function writeInstalledTracks(projectDir, tracks) {
|
|
2927
2836
|
const path = join11(projectDir, ".claude/.installed-tracks");
|
|
2928
|
-
|
|
2837
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
2929
2838
|
const sorted = [...new Set(tracks)].sort().join("\n");
|
|
2930
|
-
|
|
2839
|
+
writeFileSync6(path, `${sorted}
|
|
2931
2840
|
`);
|
|
2932
2841
|
}
|
|
2933
2842
|
function writeRootClaudeMd(projectDir, tracks) {
|
|
@@ -2935,7 +2844,7 @@ function writeRootClaudeMd(projectDir, tracks) {
|
|
|
2935
2844
|
const existing = existsSync13(target) ? readFileSync12(target, "utf-8") : null;
|
|
2936
2845
|
const content = upsertHarnessImport(existing, { projectName: basename5(projectDir), tracks });
|
|
2937
2846
|
if (content !== existing) {
|
|
2938
|
-
|
|
2847
|
+
writeFileSync6(target, content);
|
|
2939
2848
|
}
|
|
2940
2849
|
return { created: existing === null };
|
|
2941
2850
|
}
|
|
@@ -2948,7 +2857,7 @@ function chmodHooksSync(hookDir) {
|
|
|
2948
2857
|
}
|
|
2949
2858
|
}
|
|
2950
2859
|
function listHookFiles(hookDir) {
|
|
2951
|
-
return
|
|
2860
|
+
return readdirSync3(hookDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".sh")).map((e2) => resolve(hookDir, e2.name));
|
|
2952
2861
|
}
|
|
2953
2862
|
|
|
2954
2863
|
// src/commands/install-render.ts
|
|
@@ -3301,6 +3210,15 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
|
|
|
3301
3210
|
)
|
|
3302
3211
|
);
|
|
3303
3212
|
}
|
|
3213
|
+
if (baseline.updateMode.mcpAllowlistRetired) {
|
|
3214
|
+
log(
|
|
3215
|
+
assetRow(
|
|
3216
|
+
"skip",
|
|
3217
|
+
".mcp-allowlist",
|
|
3218
|
+
`retired \xB7 the hook that read it is gone \u2014 backed up as ${shortenPath(baseline.updateMode.mcpAllowlistRetired)}`
|
|
3219
|
+
)
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3304
3222
|
if (baseline.updateMode.policyBackedUp.length > 0) {
|
|
3305
3223
|
log(
|
|
3306
3224
|
assetRow(
|
|
@@ -3375,12 +3293,12 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
|
|
|
3375
3293
|
phase1Row(
|
|
3376
3294
|
"hooks",
|
|
3377
3295
|
cats.hooks.length,
|
|
3378
|
-
"session-start \xB7 protect-files \xB7
|
|
3296
|
+
"session-start \xB7 protect-files \xB7 task-brief-nudge",
|
|
3379
3297
|
cats.hooks
|
|
3380
3298
|
);
|
|
3381
3299
|
}
|
|
3382
3300
|
if (cats.commands > 0) {
|
|
3383
|
-
phase1Row("commands", cats.commands, "
|
|
3301
|
+
phase1Row("commands", cats.commands, "slash commands");
|
|
3384
3302
|
}
|
|
3385
3303
|
if (cats.skills.length > 0) {
|
|
3386
3304
|
phase1Row(
|
|
@@ -3394,6 +3312,38 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
|
|
|
3394
3312
|
log(assetRow("success", "rules + hooks + commands + agents", `${baseline.filesCopied} files`));
|
|
3395
3313
|
log(assetRow("success", "skeleton", `${baseline.dirsCopied} dirs`));
|
|
3396
3314
|
}
|
|
3315
|
+
if (baseline.baselineExcluded.length > 0) {
|
|
3316
|
+
const onDisk = new Set(baseline.baselineExcludedOnDisk);
|
|
3317
|
+
const names = baseline.baselineExcluded.map((t) => {
|
|
3318
|
+
const name = t.replace(/^\.claude\//, "");
|
|
3319
|
+
return onDisk.has(t) ? `${name} (still on disk)` : name;
|
|
3320
|
+
});
|
|
3321
|
+
log(
|
|
3322
|
+
assetRow(
|
|
3323
|
+
"skip",
|
|
3324
|
+
"excluded by you",
|
|
3325
|
+
onDisk.size > 0 ? `${names.length} \u2014 ${names.join(", ")} \xB7 "still on disk" = \uC774\uC804 \uC124\uCE58\uBCF8\uC774\uB77C \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4 (\uC81C\uAC70: agent-harness uninstall)` : `${names.length} \u2014 ${names.join(", ")}`
|
|
3326
|
+
)
|
|
3327
|
+
);
|
|
3328
|
+
}
|
|
3329
|
+
if (baseline.superseded.removed.length > 0) {
|
|
3330
|
+
log(
|
|
3331
|
+
assetRow(
|
|
3332
|
+
"success",
|
|
3333
|
+
"superseded \u2014 cleaned",
|
|
3334
|
+
`${baseline.superseded.removed.length} \u2014 ${baseline.superseded.removed.map((t) => t.replace(/^\.claude\//, "")).join(", ")} \xB7 \uB418\uB3CC\uB9AC\uB824\uBA74 \uADF8 \uC790\uC0B0 \uC5C6\uC774 \uC7AC\uC124\uCE58`
|
|
3335
|
+
)
|
|
3336
|
+
);
|
|
3337
|
+
}
|
|
3338
|
+
if (baseline.superseded.kept.length > 0) {
|
|
3339
|
+
log(
|
|
3340
|
+
assetRow(
|
|
3341
|
+
"skip",
|
|
3342
|
+
"superseded \u2014 kept",
|
|
3343
|
+
`${baseline.superseded.kept.length} \u2014 ${baseline.superseded.kept.map((t) => t.replace(/^\.claude\//, "")).join(", ")} \xB7 \uC774\uBC88 \uC120\uD0DD\uC774 \uB300\uCCB4\uD588\uC9C0\uB9CC \uC9C0\uC6B0\uC9C0 \uC54A\uC558\uB2E4 (\uC81C\uAC70: agent-harness uninstall)`
|
|
3344
|
+
)
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3397
3347
|
const TEMPLATES_COL = 28;
|
|
3398
3348
|
if (baseline.rootClaudeMd) {
|
|
3399
3349
|
const n = baseline.rootClaudeMd.tracks.length;
|
|
@@ -3429,16 +3379,6 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
|
|
|
3429
3379
|
}
|
|
3430
3380
|
const mcpList = baseline.mcpServers.join(", ") || "(none)";
|
|
3431
3381
|
log(assetRow("success", ".mcp.json", mcpList, TEMPLATES_COL));
|
|
3432
|
-
if (baseline.envFiles.mcpAllowlist) {
|
|
3433
|
-
log(
|
|
3434
|
-
assetRow(
|
|
3435
|
-
"success",
|
|
3436
|
-
".mcp-allowlist",
|
|
3437
|
-
`${baseline.envFiles.mcpAllowlist.length} servers (D35 opt-in gate)`,
|
|
3438
|
-
TEMPLATES_COL
|
|
3439
|
-
)
|
|
3440
|
-
);
|
|
3441
|
-
}
|
|
3442
3382
|
if (!withEcc && baseline.categories) {
|
|
3443
3383
|
log("");
|
|
3444
3384
|
log(
|
|
@@ -3550,15 +3490,26 @@ function installAction(options, deps = {}) {
|
|
|
3550
3490
|
}
|
|
3551
3491
|
const forceInclude = normalizeRepeatable(options.with);
|
|
3552
3492
|
const forceExclude = normalizeRepeatable(options.without);
|
|
3493
|
+
const baselineIds = new Set(
|
|
3494
|
+
listBaselineTargets({ tracks: options.track ?? [] }).map((t) => t.id)
|
|
3495
|
+
);
|
|
3496
|
+
const baselineExclude = forceExclude.filter((id) => baselineIds.has(id));
|
|
3553
3497
|
const validIds = new Set(EXTERNAL_ASSETS.map((a) => a.id));
|
|
3554
|
-
for (const id of
|
|
3555
|
-
if (
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3498
|
+
for (const id of forceInclude) {
|
|
3499
|
+
if (validIds.has(id)) continue;
|
|
3500
|
+
err(
|
|
3501
|
+
c.yellow(
|
|
3502
|
+
id.startsWith(BASELINE_PREFIX) ? `[WARN] '${id}' cannot be used with --with \u2014 track baseline assets install by default. Use --without to drop one.` : `[WARN] Unknown asset id '${id}' (--with). Skipping. Use one of: ${[...validIds].sort().join(", ")}`
|
|
3503
|
+
)
|
|
3504
|
+
);
|
|
3505
|
+
}
|
|
3506
|
+
for (const id of forceExclude) {
|
|
3507
|
+
if (validIds.has(id) || baselineIds.has(id)) continue;
|
|
3508
|
+
err(
|
|
3509
|
+
c.yellow(
|
|
3510
|
+
id.startsWith(BASELINE_PREFIX) ? `[WARN] '${id}' is not installed by the selected track(s) \u2014 nothing to exclude. Available: ${[...baselineIds].sort().join(", ")}` : `[WARN] Unknown asset id '${id}' (--without). Skipping. Use one of: ${[...validIds].sort().join(", ")}`
|
|
3511
|
+
)
|
|
3512
|
+
);
|
|
3562
3513
|
}
|
|
3563
3514
|
const filteredInclude = forceInclude.filter((id) => validIds.has(id));
|
|
3564
3515
|
const filteredExclude = forceExclude.filter((id) => validIds.has(id));
|
|
@@ -3566,6 +3517,7 @@ function installAction(options, deps = {}) {
|
|
|
3566
3517
|
const spec = {
|
|
3567
3518
|
tracks: options.track ?? [],
|
|
3568
3519
|
...userOverride ? { userOverride } : {},
|
|
3520
|
+
...baselineExclude.length > 0 ? { baselineExclude } : {},
|
|
3569
3521
|
// v26.81.0 (ADR-022, BREAKING) — 자산 1:1 boolean 13종 삭제. 자산 선택은 위
|
|
3570
3522
|
// userOverride(--with <id>)로 일원화. 잔존 = 설치 동작 옵션만.
|
|
3571
3523
|
options: {
|
|
@@ -3763,7 +3715,7 @@ function registerListCommand(cli2) {
|
|
|
3763
3715
|
// src/commands/uninstall.ts
|
|
3764
3716
|
init_esm_shims();
|
|
3765
3717
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3766
|
-
import { existsSync as existsSync15, readFileSync as readFileSync14, rmSync
|
|
3718
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14, rmSync, writeFileSync as writeFileSync7 } from "fs";
|
|
3767
3719
|
import { join as join13, resolve as resolve4 } from "path";
|
|
3768
3720
|
|
|
3769
3721
|
// src/uninstall-interactive.ts
|
|
@@ -5165,7 +5117,7 @@ function stripRootImport(projectDir) {
|
|
|
5165
5117
|
const stripped = current === null ? null : stripHarnessImport(current);
|
|
5166
5118
|
if (stripped === null) return false;
|
|
5167
5119
|
try {
|
|
5168
|
-
|
|
5120
|
+
writeFileSync7(join13(projectDir, "CLAUDE.md"), stripped, "utf8");
|
|
5169
5121
|
return true;
|
|
5170
5122
|
} catch {
|
|
5171
5123
|
return false;
|
|
@@ -5202,7 +5154,7 @@ function defaultSpawn2(cmd, args) {
|
|
|
5202
5154
|
}
|
|
5203
5155
|
function defaultRm(path) {
|
|
5204
5156
|
if (existsSync15(path)) {
|
|
5205
|
-
|
|
5157
|
+
rmSync(path, { recursive: true, force: true });
|
|
5206
5158
|
}
|
|
5207
5159
|
}
|
|
5208
5160
|
function registerUninstallCommand(cli2) {
|
|
@@ -5430,6 +5382,18 @@ var CLI_BASE_LABELS = {
|
|
|
5430
5382
|
antigravity: "Antigravity (Google)"
|
|
5431
5383
|
};
|
|
5432
5384
|
var INSTALL_TARGET_PAGES = [
|
|
5385
|
+
// 2026-08-16 — 트랙이 고르는 자산을 **맨 앞에서** 전부 보여준다. 그전까지 이 항목들은 화면에
|
|
5386
|
+
// 한 번도 안 나온 채 깔렸고, 사용자는 설치가 끝난 뒤 요약에서 처음 봤다.
|
|
5387
|
+
//
|
|
5388
|
+
// 2페이지로 나눈 근거는 실측이다: 최대 트랙(`full`)이 32항목(rules 6 · agents 9 · hooks 3 ·
|
|
5389
|
+
// skills 14)이라 clack groupMultiselect 의 "페이지당 ≤ ~30행" 제약을 한 페이지로는 못 지킨다.
|
|
5390
|
+
// 가르는 축은 역할이다 — 정책·가드가 한쪽, 실행 능력이 다른 쪽.
|
|
5391
|
+
{ label: "Track baseline \u2014 Rules & Hooks (\uC815\uCC45\xB7\uAC00\uB4DC)", cats: [], baseline: ["rules", "hooks"] },
|
|
5392
|
+
{
|
|
5393
|
+
label: "Track baseline \u2014 Agents & Skills (\uC5D0\uC774\uC804\uD2B8\xB7\uC2A4\uD0AC)",
|
|
5394
|
+
cats: [],
|
|
5395
|
+
baseline: ["agents", "skills"]
|
|
5396
|
+
},
|
|
5433
5397
|
{ label: "Dev Core (Frontend \xB7 Backend \xB7 Data)", cats: ["frontend", "backend", "data"] },
|
|
5434
5398
|
{ label: "Dev Tools (Security \xB7 Quality \xB7 Understanding)", cats: ["dev-tools", "understanding"] },
|
|
5435
5399
|
{ label: "Business (PM \xB7 Executive \xB7 Documents)", cats: ["business"] },
|
|
@@ -5450,10 +5414,25 @@ function expandDevMethodBundle(ids) {
|
|
|
5450
5414
|
const rest = ids.filter((v2) => v2 !== DEV_METHOD_BUNDLE_VALUE);
|
|
5451
5415
|
return ids.includes(DEV_METHOD_BUNDLE_VALUE) ? [...rest, ...bundleMemberValues()] : rest;
|
|
5452
5416
|
}
|
|
5453
|
-
|
|
5417
|
+
var BASELINE_TITLES = {
|
|
5418
|
+
rules: "Rules \u2014 \uC0C1\uC2DC \uC0C1\uC8FC\uD558\uB294 \uB300\uC6D0\uCE59 (4 CLI \uC804\uBD80\uC5D0 \uC124\uCE58)",
|
|
5419
|
+
hooks: "Hooks \u2014 \uACB0\uC815\uB860\uC801 \uAC00\uB4DC (\uD574\uC81C\uD558\uBA74 settings.json \uBC30\uC120\uB3C4 \uD568\uAED8 \uBE60\uC9C4\uB2E4)",
|
|
5420
|
+
agents: "Agents \u2014 \uB3C5\uB9BD \uB808\uC778 (\uB9AC\uBDF0\xB7\uAC80\uC99D)",
|
|
5421
|
+
skills: "Skills \u2014 \uD544\uC694\uD560 \uB54C \uC5F4\uB9AC\uB294 \uC791\uC5C5 \uC808\uCC28"
|
|
5422
|
+
};
|
|
5423
|
+
function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Set(), baselineKinds = [], baselineTargets = []) {
|
|
5454
5424
|
const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
|
|
5455
5425
|
const groups = {};
|
|
5456
5426
|
const flatItems = [];
|
|
5427
|
+
for (const kind of baselineKinds) {
|
|
5428
|
+
const items = baselineTargets.filter((t) => t.kind === kind).map((t) => ({
|
|
5429
|
+
value: t.id,
|
|
5430
|
+
label: ` ${t.name}${installedMark(t.id)}`
|
|
5431
|
+
}));
|
|
5432
|
+
if (items.length === 0) continue;
|
|
5433
|
+
groups[BASELINE_TITLES[kind]] = items;
|
|
5434
|
+
flatItems.push(...items);
|
|
5435
|
+
}
|
|
5457
5436
|
for (const cat of cats) {
|
|
5458
5437
|
const items = [];
|
|
5459
5438
|
for (const o of VISIBLE_OPTION_DEFS.filter((d2) => d2.category === cat)) {
|
|
@@ -5588,8 +5567,26 @@ Proceed?`,
|
|
|
5588
5567
|
});
|
|
5589
5568
|
return q(result) ? null : result;
|
|
5590
5569
|
},
|
|
5570
|
+
confirmSupersededCleanup: async (items, context) => {
|
|
5571
|
+
const names = items.map((i) => i.target.replace(/^\.claude\//, "")).join("\n ");
|
|
5572
|
+
const result = await ue({
|
|
5573
|
+
message: [
|
|
5574
|
+
`${context.by.join(" \xB7 ")} \uC744(\uB97C) \uACE8\uB77C\uC11C \uC544\uB798 ${items.length}\uAC1C\uAC00 \uC124\uCE58 \uB300\uC0C1\uC5D0\uC11C \uBE60\uC84C\uC2B5\uB2C8\uB2E4.`,
|
|
5575
|
+
"\uADF8\uB7F0\uB370 \uC774\uC804 \uC124\uCE58\uBCF8\uC774 \uB514\uC2A4\uD06C\uC5D0 \uADF8\uB300\uB85C \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4:",
|
|
5576
|
+
` ${names}`,
|
|
5577
|
+
"",
|
|
5578
|
+
`\uADF8\uB300\uB85C \uB450\uBA74 \uAC19\uC740 \uC77C\uC744 \uD558\uB294 \uC5D0\uC774\uC804\uD2B8\uAC00 \uB450 \uBC8C\uC774\uACE0, \uB9E4 \uC138\uC158 ~${context.tokens} tok \uB97C \uACC4\uC18D \uBB45\uB2C8\uB2E4.`,
|
|
5579
|
+
"(\uD558\uB124\uC2A4\uAC00 \uAE54\uC558\uACE0 \uC218\uC815\uB418\uC9C0 \uC54A\uC740 \uD30C\uC77C\uB9CC \uB300\uC0C1\uC785\uB2C8\uB2E4 \u2014 \uC9C1\uC811 \uACE0\uCE5C \uD30C\uC77C\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4)",
|
|
5580
|
+
"",
|
|
5581
|
+
"\uC815\uB9AC\uD560\uAE4C\uC694?"
|
|
5582
|
+
].join("\n"),
|
|
5583
|
+
initialValue: true
|
|
5584
|
+
});
|
|
5585
|
+
return q(result) ? null : result;
|
|
5586
|
+
},
|
|
5591
5587
|
selectInstallTargets: async (initialChecked, step, recap) => {
|
|
5592
5588
|
const pages = INSTALL_TARGET_PAGES;
|
|
5589
|
+
const baselineTargets = listBaselineTargets({ tracks: recap?.tracks ?? [] });
|
|
5593
5590
|
const displayInitial = collapseDevMethodBundle(initialChecked);
|
|
5594
5591
|
const initialSet = new Set(displayInitial);
|
|
5595
5592
|
const collected = new Set(displayInitial);
|
|
@@ -5602,10 +5599,25 @@ Proceed?`,
|
|
|
5602
5599
|
let aborted = false;
|
|
5603
5600
|
try {
|
|
5604
5601
|
let pageIdx = 0;
|
|
5602
|
+
let dir = 1;
|
|
5605
5603
|
while (pageIdx < pages.length) {
|
|
5606
5604
|
const page = pages[pageIdx];
|
|
5607
5605
|
if (!page) break;
|
|
5608
|
-
const { groups, flatItems } = buildPageGroups(
|
|
5606
|
+
const { groups, flatItems } = buildPageGroups(
|
|
5607
|
+
page.cats,
|
|
5608
|
+
initialSet,
|
|
5609
|
+
installedSet,
|
|
5610
|
+
page.baseline ?? [],
|
|
5611
|
+
baselineTargets
|
|
5612
|
+
);
|
|
5613
|
+
if (flatItems.length === 0) {
|
|
5614
|
+
pageIdx += dir;
|
|
5615
|
+
if (pageIdx < 0) {
|
|
5616
|
+
aborted = true;
|
|
5617
|
+
break;
|
|
5618
|
+
}
|
|
5619
|
+
continue;
|
|
5620
|
+
}
|
|
5609
5621
|
const selectedNow = flatItems.filter((it3) => collected.has(it3.value)).map((it3) => it3.value);
|
|
5610
5622
|
const pageDefault = flatItems.filter((it3) => initialSet.has(it3.value)).length;
|
|
5611
5623
|
const totalSelected = collected.size;
|
|
@@ -5631,11 +5643,13 @@ Proceed?`,
|
|
|
5631
5643
|
aborted = true;
|
|
5632
5644
|
break;
|
|
5633
5645
|
}
|
|
5646
|
+
dir = -1;
|
|
5634
5647
|
pageIdx--;
|
|
5635
5648
|
continue;
|
|
5636
5649
|
}
|
|
5637
5650
|
for (const it3 of flatItems) collected.delete(it3.value);
|
|
5638
5651
|
for (const v2 of result) collected.add(v2);
|
|
5652
|
+
dir = 1;
|
|
5639
5653
|
pageIdx++;
|
|
5640
5654
|
}
|
|
5641
5655
|
if (!aborted) {
|
|
@@ -5659,14 +5673,21 @@ Proceed?`,
|
|
|
5659
5673
|
function splitInstallTargets(targets) {
|
|
5660
5674
|
const optionKeys = [];
|
|
5661
5675
|
const assetIds = [];
|
|
5676
|
+
const baselineIds = [];
|
|
5662
5677
|
for (const t of targets) {
|
|
5663
5678
|
if (t.startsWith("option:")) {
|
|
5664
5679
|
optionKeys.push(t.slice("option:".length));
|
|
5665
5680
|
} else if (t.startsWith("asset:")) {
|
|
5666
5681
|
assetIds.push(t.slice("asset:".length));
|
|
5682
|
+
} else if (t.startsWith(BASELINE_PREFIX)) {
|
|
5683
|
+
baselineIds.push(t);
|
|
5667
5684
|
}
|
|
5668
5685
|
}
|
|
5669
|
-
return { optionKeys, assetIds };
|
|
5686
|
+
return { optionKeys, assetIds, baselineIds };
|
|
5687
|
+
}
|
|
5688
|
+
function baselineExcludeFrom(offered, checked) {
|
|
5689
|
+
const keep = new Set(checked);
|
|
5690
|
+
return offered.filter((t) => !keep.has(t.id)).map((t) => t.id);
|
|
5670
5691
|
}
|
|
5671
5692
|
function toOptionFlags(keys) {
|
|
5672
5693
|
const picked = new Set(keys);
|
|
@@ -5686,7 +5707,9 @@ function installedTargetState(projectDir) {
|
|
|
5686
5707
|
function initialTargetSelection(tracks, installedProjectAssetIds) {
|
|
5687
5708
|
const ids = new Set(recommendedExternalAssets(tracks));
|
|
5688
5709
|
for (const id of installedProjectAssetIds) ids.add(id);
|
|
5689
|
-
|
|
5710
|
+
const assets = [...ids].map((id) => `asset:${id}`);
|
|
5711
|
+
const baseline = listBaselineTargets({ tracks }).map((t) => t.id);
|
|
5712
|
+
return [...assets, ...baseline];
|
|
5690
5713
|
}
|
|
5691
5714
|
async function runInteractive(projectDir, deps = {}) {
|
|
5692
5715
|
const prompts = deps.prompts ?? defaultPrompts;
|
|
@@ -5788,16 +5811,21 @@ ${formatSummary(spec)}`
|
|
|
5788
5811
|
} else {
|
|
5789
5812
|
const finalTracks = tracks;
|
|
5790
5813
|
const finalCli = cli2;
|
|
5791
|
-
const { optionKeys, assetIds } = splitInstallTargets(targetSelections ?? []);
|
|
5814
|
+
const { optionKeys, assetIds, baselineIds } = splitInstallTargets(targetSelections ?? []);
|
|
5792
5815
|
const options = toOptionFlags(optionKeys);
|
|
5793
5816
|
const userOverride = targetSelections === null ? void 0 : computeUserOverride(finalTracks, assetIds);
|
|
5817
|
+
const baselineExclude = targetSelections === null ? [] : baselineExcludeFrom(listBaselineTargets({ tracks: finalTracks }), baselineIds);
|
|
5794
5818
|
const scopeLabel = scope === "global" ? "Global (writes to ~/.claude/, ~/.codex/, npm -g)" : "Project (current directory only)";
|
|
5795
5819
|
const summary = `${formatSummary({
|
|
5796
5820
|
tracks: finalTracks,
|
|
5797
5821
|
options,
|
|
5798
5822
|
cli: finalCli,
|
|
5799
5823
|
projectDir,
|
|
5800
|
-
...userOverride ? { userOverride } : {}
|
|
5824
|
+
...userOverride ? { userOverride } : {},
|
|
5825
|
+
// 해제 목록을 안 넘기면 이 화면이 **제외 유무와 문자열이 완전히 같아진다** — 같은 화면이
|
|
5826
|
+
// 외부 자산 제거는 `-User removed:` 로 이미 보고하므로, 없음은 "아무것도 안 빠졌다"로
|
|
5827
|
+
// 읽힌다. 상주 비용을 줄이려고 20개를 푼 사용자가 그대로인 숫자를 보게 된다.
|
|
5828
|
+
...baselineExclude.length > 0 ? { baselineExclude } : {}
|
|
5801
5829
|
})}
|
|
5802
5830
|
SCOPE ${scopeLabel}`;
|
|
5803
5831
|
const confirmed = await prompts.confirmInstall(
|
|
@@ -5812,22 +5840,59 @@ ${summary}`
|
|
|
5812
5840
|
prompts.outro("Cancelled by user.");
|
|
5813
5841
|
return { ok: false, reason: "cancelled" };
|
|
5814
5842
|
}
|
|
5843
|
+
const spec = {
|
|
5844
|
+
tracks: finalTracks,
|
|
5845
|
+
options,
|
|
5846
|
+
cli: finalCli,
|
|
5847
|
+
projectDir,
|
|
5848
|
+
scope,
|
|
5849
|
+
...userOverride ? { userOverride } : {},
|
|
5850
|
+
...baselineExclude.length > 0 ? { baselineExclude } : {}
|
|
5851
|
+
};
|
|
5852
|
+
const superseded = findSuperseded(
|
|
5853
|
+
projectDir,
|
|
5854
|
+
buildManifestSpec(spec),
|
|
5855
|
+
readInstallLog(projectDir)
|
|
5856
|
+
);
|
|
5857
|
+
let cleanSuperseded = false;
|
|
5858
|
+
if (superseded.length > 0) {
|
|
5859
|
+
const cause = supersededCause(projectDir, spec, readInstallLog(projectDir), superseded);
|
|
5860
|
+
const answer = await prompts.confirmSupersededCleanup(superseded, {
|
|
5861
|
+
by: cause.length > 0 ? cause : ["\uC774\uBC88 \uC120\uD0DD"],
|
|
5862
|
+
tokens: supersededTokens(superseded)
|
|
5863
|
+
});
|
|
5864
|
+
if (answer === null) {
|
|
5865
|
+
step = "confirm";
|
|
5866
|
+
continue;
|
|
5867
|
+
}
|
|
5868
|
+
cleanSuperseded = answer;
|
|
5869
|
+
}
|
|
5815
5870
|
prompts.outro(stepLabel(WIZARD.INSTALL, "Installing..."));
|
|
5816
5871
|
return {
|
|
5817
5872
|
ok: true,
|
|
5818
5873
|
mode,
|
|
5819
|
-
spec: {
|
|
5820
|
-
tracks: finalTracks,
|
|
5821
|
-
options,
|
|
5822
|
-
cli: finalCli,
|
|
5823
|
-
projectDir,
|
|
5824
|
-
scope,
|
|
5825
|
-
...userOverride ? { userOverride } : {}
|
|
5826
|
-
}
|
|
5874
|
+
spec: { ...spec, ...cleanSuperseded ? { cleanSuperseded } : {} }
|
|
5827
5875
|
};
|
|
5828
5876
|
}
|
|
5829
5877
|
}
|
|
5830
5878
|
}
|
|
5879
|
+
function supersededTokens(items) {
|
|
5880
|
+
const r = residentCost(items.map((i) => ({ source: i.source, target: i.target })));
|
|
5881
|
+
return r.rules + r.agentDescriptors + r.skillDescriptors;
|
|
5882
|
+
}
|
|
5883
|
+
function supersededCause(projectDir, spec, log, found) {
|
|
5884
|
+
const selected = spec.userOverride?.forceInclude ?? [];
|
|
5885
|
+
return selected.filter((id) => {
|
|
5886
|
+
const without = {
|
|
5887
|
+
...spec,
|
|
5888
|
+
userOverride: {
|
|
5889
|
+
forceInclude: selected.filter((x) => x !== id),
|
|
5890
|
+
forceExclude: [...spec.userOverride?.forceExclude ?? [], id]
|
|
5891
|
+
}
|
|
5892
|
+
};
|
|
5893
|
+
return findSuperseded(projectDir, buildManifestSpec(without), log).length < found.length;
|
|
5894
|
+
});
|
|
5895
|
+
}
|
|
5831
5896
|
function tracksEqual(a, b2) {
|
|
5832
5897
|
if (a.length !== b2.length) return false;
|
|
5833
5898
|
const sortedA = [...a].sort();
|
|
@@ -5843,6 +5908,7 @@ function computeUserOverride(tracks, assetIds) {
|
|
|
5843
5908
|
return { forceInclude, forceExclude };
|
|
5844
5909
|
}
|
|
5845
5910
|
function formatSummary(spec) {
|
|
5911
|
+
const baselineExcluded = new Set(spec.baselineExclude ?? []);
|
|
5846
5912
|
const opts = Object.keys(spec.options).filter((k2) => spec.options[k2]).map((k2) => k2.replace(/^with/, "").toLowerCase());
|
|
5847
5913
|
const optsLabel = opts.length > 0 ? opts.join(", ") : "(none added)";
|
|
5848
5914
|
const lines = [
|
|
@@ -5864,7 +5930,11 @@ function formatSummary(spec) {
|
|
|
5864
5930
|
lines.push(` \xB7 ${cat}: ${ids.join(", ")}`);
|
|
5865
5931
|
}
|
|
5866
5932
|
const cost = formatResidentCostLine(
|
|
5867
|
-
residentCost(
|
|
5933
|
+
residentCost(
|
|
5934
|
+
buildManifest(spec).filter(
|
|
5935
|
+
(e2) => e2.applies(spec) && !isBaselineExcluded(e2.target, baselineExcluded)
|
|
5936
|
+
)
|
|
5937
|
+
),
|
|
5868
5938
|
summarizeContextCost(finalAssets).unmeasuredCount
|
|
5869
5939
|
);
|
|
5870
5940
|
if (cost) lines.push(` \xB7 ${cost}`);
|
|
@@ -5877,6 +5947,11 @@ function formatSummary(spec) {
|
|
|
5877
5947
|
lines.push(` -User removed: ${spec.userOverride.forceExclude.join(", ")}`);
|
|
5878
5948
|
}
|
|
5879
5949
|
}
|
|
5950
|
+
if (baselineExcluded.size > 0) {
|
|
5951
|
+
lines.push(
|
|
5952
|
+
` -Excluded by you: ${baselineExcluded.size} \u2014 ${[...baselineExcluded].map((id) => id.replace(BASELINE_PREFIX, "")).join(", ")}`
|
|
5953
|
+
);
|
|
5954
|
+
}
|
|
5880
5955
|
return lines.join("\n");
|
|
5881
5956
|
}
|
|
5882
5957
|
|