@uzysjung/agent-harness 26.146.0 → 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 +607 -530
- package/dist/index.js.map +1 -1
- package/dist/trust-tier-drift.js +1 -1
- package/package.json +4 -2
- 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: {
|
|
@@ -750,7 +774,9 @@ var package_default = {
|
|
|
750
774
|
ci: "npm run typecheck && npm run lint && npm run test:coverage && npm run build",
|
|
751
775
|
prepare: "[ -d dist ] || npm run build",
|
|
752
776
|
"cost:report": "npm run build && node scripts/context-cost-report.mjs",
|
|
753
|
-
"cost:baseline": "npm run build && node scripts/context-cost-baseline.mjs"
|
|
777
|
+
"cost:baseline": "npm run build && node scripts/context-cost-baseline.mjs",
|
|
778
|
+
security: "npm audit --omit=dev --audit-level=high && npx --yes ecc-agentshield@1.4.0 scan --baseline security-baseline.json --gate",
|
|
779
|
+
"security:baseline": "npx --yes ecc-agentshield@1.4.0 scan --save-baseline security-baseline.json"
|
|
754
780
|
},
|
|
755
781
|
dependencies: {
|
|
756
782
|
"@clack/prompts": "^1.3.0",
|
|
@@ -776,6 +802,46 @@ init_esm_shims();
|
|
|
776
802
|
import { resolve as resolve2 } from "path";
|
|
777
803
|
import { fileURLToPath } from "url";
|
|
778
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
|
+
|
|
779
845
|
// src/cli-targets.ts
|
|
780
846
|
init_esm_shims();
|
|
781
847
|
var CLI_BASE_SORT_ORDER = {
|
|
@@ -913,127 +979,18 @@ init_esm_shims();
|
|
|
913
979
|
import {
|
|
914
980
|
chmodSync as chmodSync2,
|
|
915
981
|
existsSync as existsSync13,
|
|
916
|
-
mkdirSync as
|
|
917
|
-
readdirSync as
|
|
982
|
+
mkdirSync as mkdirSync4,
|
|
983
|
+
readdirSync as readdirSync3,
|
|
918
984
|
readFileSync as readFileSync12,
|
|
919
|
-
|
|
985
|
+
unlinkSync as unlinkSync2,
|
|
986
|
+
writeFileSync as writeFileSync6
|
|
920
987
|
} from "fs";
|
|
921
|
-
import { basename as basename5, dirname as
|
|
988
|
+
import { basename as basename5, dirname as dirname4, join as join11, resolve } from "path";
|
|
922
989
|
|
|
923
990
|
// src/ci-scaffold.ts
|
|
924
991
|
init_esm_shims();
|
|
925
|
-
import { existsSync
|
|
926
|
-
import { join
|
|
927
|
-
|
|
928
|
-
// src/fs-ops.ts
|
|
929
|
-
init_esm_shims();
|
|
930
|
-
import {
|
|
931
|
-
copyFileSync,
|
|
932
|
-
cpSync,
|
|
933
|
-
existsSync,
|
|
934
|
-
constants as fsConstants,
|
|
935
|
-
mkdirSync,
|
|
936
|
-
readdirSync,
|
|
937
|
-
readFileSync,
|
|
938
|
-
realpathSync,
|
|
939
|
-
renameSync,
|
|
940
|
-
rmdirSync
|
|
941
|
-
} from "fs";
|
|
942
|
-
import { dirname, join } from "path";
|
|
943
|
-
function ensureDir(path) {
|
|
944
|
-
mkdirSync(path, { recursive: true });
|
|
945
|
-
}
|
|
946
|
-
function copyFile(source, target) {
|
|
947
|
-
if (!existsSync(source)) {
|
|
948
|
-
throw new Error(`Source not found: ${source}`);
|
|
949
|
-
}
|
|
950
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
951
|
-
copyFileSync(source, target);
|
|
952
|
-
}
|
|
953
|
-
function copyDir(source, target) {
|
|
954
|
-
if (!existsSync(source)) {
|
|
955
|
-
throw new Error(`Source dir not found: ${source}`);
|
|
956
|
-
}
|
|
957
|
-
mkdirSync(target, { recursive: true });
|
|
958
|
-
cpSync(source, target, { recursive: true, force: true });
|
|
959
|
-
}
|
|
960
|
-
var MAX_BACKUP_CLAIMS = 100;
|
|
961
|
-
function claimBackupPath(base, claim) {
|
|
962
|
-
for (let n = 1; n <= MAX_BACKUP_CLAIMS; n++) {
|
|
963
|
-
const candidate = n === 1 ? base : `${base}-${n}`;
|
|
964
|
-
try {
|
|
965
|
-
claim(candidate);
|
|
966
|
-
return candidate;
|
|
967
|
-
} catch (err) {
|
|
968
|
-
if (err.code !== "EEXIST") throw err;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
throw new Error(`Cannot allocate a backup path for ${base} (${MAX_BACKUP_CLAIMS} taken)`);
|
|
972
|
-
}
|
|
973
|
-
function backupDir(target, now = /* @__PURE__ */ new Date()) {
|
|
974
|
-
if (!existsSync(target)) {
|
|
975
|
-
return null;
|
|
976
|
-
}
|
|
977
|
-
const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c3) => mkdirSync(c3));
|
|
978
|
-
rmdirSync(backup);
|
|
979
|
-
renameSync(target, backup);
|
|
980
|
-
return backup;
|
|
981
|
-
}
|
|
982
|
-
function copyBackupDir(target, now = /* @__PURE__ */ new Date()) {
|
|
983
|
-
if (!existsSync(target)) {
|
|
984
|
-
return null;
|
|
985
|
-
}
|
|
986
|
-
const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c3) => mkdirSync(c3));
|
|
987
|
-
cpSync(realpathSync(target), backup, { recursive: true });
|
|
988
|
-
return backup;
|
|
989
|
-
}
|
|
990
|
-
function backupFileIfChanged(target, newContent, now = /* @__PURE__ */ new Date()) {
|
|
991
|
-
if (!existsSync(target)) {
|
|
992
|
-
return null;
|
|
993
|
-
}
|
|
994
|
-
if (readFileSync(target, "utf-8") === newContent) {
|
|
995
|
-
return null;
|
|
996
|
-
}
|
|
997
|
-
return backupFile(target, now);
|
|
998
|
-
}
|
|
999
|
-
function backupFile(target, now = /* @__PURE__ */ new Date()) {
|
|
1000
|
-
return claimBackupPath(
|
|
1001
|
-
`${target}.backup-${formatStamp(now)}`,
|
|
1002
|
-
(c3) => copyFileSync(target, c3, fsConstants.COPYFILE_EXCL)
|
|
1003
|
-
);
|
|
1004
|
-
}
|
|
1005
|
-
function listFilesRecursive(dir, prefix = "") {
|
|
1006
|
-
if (!existsSync(dir)) return [];
|
|
1007
|
-
const out = [];
|
|
1008
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1009
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1010
|
-
if (entry.isDirectory()) {
|
|
1011
|
-
out.push(...listFilesRecursive(join(dir, entry.name), rel));
|
|
1012
|
-
} else if (entry.isFile()) {
|
|
1013
|
-
out.push(rel);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
return out;
|
|
1017
|
-
}
|
|
1018
|
-
function formatStamp(now) {
|
|
1019
|
-
return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").slice(0, 15);
|
|
1020
|
-
}
|
|
1021
|
-
function ensureProjectSkeleton(projectDir) {
|
|
1022
|
-
const dirs = [
|
|
1023
|
-
".claude/commands/uzys",
|
|
1024
|
-
".claude/commands/ecc",
|
|
1025
|
-
".claude/rules",
|
|
1026
|
-
".claude/skills",
|
|
1027
|
-
".claude/agents",
|
|
1028
|
-
".claude/hooks",
|
|
1029
|
-
"docs/decisions"
|
|
1030
|
-
];
|
|
1031
|
-
for (const d2 of dirs) {
|
|
1032
|
-
mkdirSync(join(projectDir, d2), { recursive: true });
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// src/ci-scaffold.ts
|
|
992
|
+
import { existsSync } from "fs";
|
|
993
|
+
import { join } from "path";
|
|
1037
994
|
var PYTHON_TRACKS = "data|csr-fastapi|full";
|
|
1038
995
|
var VARIANTS = [
|
|
1039
996
|
{
|
|
@@ -1057,12 +1014,12 @@ function installCiScaffold(ctx) {
|
|
|
1057
1014
|
for (const v2 of VARIANTS) {
|
|
1058
1015
|
if (!v2.applies(ctx.tracks)) continue;
|
|
1059
1016
|
const relTarget = `.github/workflows/${v2.target}`;
|
|
1060
|
-
const target =
|
|
1061
|
-
if (
|
|
1017
|
+
const target = join(ctx.projectDir, relTarget);
|
|
1018
|
+
if (existsSync(target)) {
|
|
1062
1019
|
report.skippedExisting.push(relTarget);
|
|
1063
1020
|
continue;
|
|
1064
1021
|
}
|
|
1065
|
-
copyFile(
|
|
1022
|
+
copyFile(join(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
|
|
1066
1023
|
report.written.push(relTarget);
|
|
1067
1024
|
}
|
|
1068
1025
|
return report;
|
|
@@ -1073,8 +1030,8 @@ init_esm_shims();
|
|
|
1073
1030
|
|
|
1074
1031
|
// src/antigravity/transform.ts
|
|
1075
1032
|
init_esm_shims();
|
|
1076
|
-
import { existsSync as
|
|
1077
|
-
import { basename, join as
|
|
1033
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
1034
|
+
import { basename, join as join3 } from "path";
|
|
1078
1035
|
|
|
1079
1036
|
// src/codex/agents-md.ts
|
|
1080
1037
|
init_esm_shims();
|
|
@@ -1083,7 +1040,7 @@ function renameSlashes(text) {
|
|
|
1083
1040
|
}
|
|
1084
1041
|
function renderAgentsMd(params) {
|
|
1085
1042
|
const body = params.claudeMd.replace(/^#\s+.*\r?\n/, "").trim();
|
|
1086
|
-
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 ?? "");
|
|
1087
1044
|
return renameSlashes(replaced);
|
|
1088
1045
|
}
|
|
1089
1046
|
|
|
@@ -1119,204 +1076,8 @@ function portBody(body) {
|
|
|
1119
1076
|
|
|
1120
1077
|
// src/owned-write.ts
|
|
1121
1078
|
init_esm_shims();
|
|
1122
|
-
import { existsSync as
|
|
1123
|
-
import { dirname
|
|
1124
|
-
|
|
1125
|
-
// src/install-log.ts
|
|
1126
|
-
init_esm_shims();
|
|
1127
|
-
import { createHash } from "crypto";
|
|
1128
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
1129
|
-
import { dirname as dirname2, join as join3 } from "path";
|
|
1130
|
-
var INSTALL_LOG_FILENAME = ".harness-install.json";
|
|
1131
|
-
var INSTALL_LOG_DIR = ".uzys-agent-harness";
|
|
1132
|
-
var LEGACY_INSTALL_LOG_DIR = ".claude";
|
|
1133
|
-
var INSTALL_LOG_VERSION = 1;
|
|
1134
|
-
var POLICY_DIRS = [
|
|
1135
|
-
{ dir: "rules", ext: ".md" },
|
|
1136
|
-
{ dir: "agents", ext: ".md" },
|
|
1137
|
-
{ dir: "commands/uzys", ext: ".md" },
|
|
1138
|
-
{ dir: "hooks", ext: ".sh" }
|
|
1139
|
-
];
|
|
1140
|
-
function buildAssetEntries(report, scope) {
|
|
1141
|
-
if (!report) return [];
|
|
1142
|
-
return report.attempted.filter((r) => r.ok).map((r) => assetToLogEntry(r.asset, scope, r.version));
|
|
1143
|
-
}
|
|
1144
|
-
function assetToLogEntry(asset, scope, version) {
|
|
1145
|
-
const detail = methodDetail(asset.method);
|
|
1146
|
-
const entry = {
|
|
1147
|
-
id: asset.id,
|
|
1148
|
-
category: asset.category,
|
|
1149
|
-
method: asset.method.kind,
|
|
1150
|
-
scope,
|
|
1151
|
-
detail
|
|
1152
|
-
};
|
|
1153
|
-
if (version) entry.version = version;
|
|
1154
|
-
return entry;
|
|
1155
|
-
}
|
|
1156
|
-
function methodDetail(method) {
|
|
1157
|
-
switch (method.kind) {
|
|
1158
|
-
case "plugin":
|
|
1159
|
-
return { marketplace: method.marketplace, pluginId: method.pluginId };
|
|
1160
|
-
case "skill":
|
|
1161
|
-
return { source: method.source, ...method.skill ? { skill: method.skill } : {} };
|
|
1162
|
-
case "npm":
|
|
1163
|
-
return { pkg: method.pkg };
|
|
1164
|
-
case "npx-run":
|
|
1165
|
-
return { cmd: method.cmd, args: (method.args ?? []).join(" ") };
|
|
1166
|
-
case "shell-script":
|
|
1167
|
-
return { script: method.script, args: method.args.join(" ") };
|
|
1168
|
-
case "internal":
|
|
1169
|
-
return { key: method.key };
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
|
|
1173
|
-
const templates = {
|
|
1174
|
-
claudeDir: ".claude/",
|
|
1175
|
-
...spec.cli.includes("codex") ? { codexDir: ".codex/" } : {},
|
|
1176
|
-
...spec.cli.includes("opencode") ? { opencodeDir: ".opencode/" } : {},
|
|
1177
|
-
...rootClaudeMd ? { rootClaudeMd } : {}
|
|
1178
|
-
};
|
|
1179
|
-
const log = {
|
|
1180
|
-
schemaVersion: INSTALL_LOG_VERSION,
|
|
1181
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1182
|
-
scope,
|
|
1183
|
-
spec: {
|
|
1184
|
-
tracks: spec.tracks,
|
|
1185
|
-
cli: spec.cli
|
|
1186
|
-
},
|
|
1187
|
-
// 이번 설치가 만든 항목이 이기고, 이번에 안 만든 항목은 이전 값을 그대로 둔다.
|
|
1188
|
-
// (예: claude 로 깔고 나중에 codex 만 추가 설치해도 root CLAUDE.md 기록이 살아남는다)
|
|
1189
|
-
templates: { ...previous?.templates, ...templates },
|
|
1190
|
-
assets: mergeAssets(
|
|
1191
|
-
claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
|
|
1192
|
-
buildAssetEntries(external, scope)
|
|
1193
|
-
)
|
|
1194
|
-
};
|
|
1195
|
-
const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
|
|
1196
|
-
if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
|
|
1197
|
-
return log;
|
|
1198
|
-
}
|
|
1199
|
-
function mergeRootFiles(previous, current) {
|
|
1200
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
1201
|
-
for (const file of [...previous ?? [], ...current]) {
|
|
1202
|
-
const prior = byPath.get(file.path);
|
|
1203
|
-
byPath.set(
|
|
1204
|
-
file.path,
|
|
1205
|
-
prior ? {
|
|
1206
|
-
path: file.path,
|
|
1207
|
-
change: prior.change === "created" ? "created" : file.change,
|
|
1208
|
-
notes: [.../* @__PURE__ */ new Set([...prior.notes, ...file.notes])]
|
|
1209
|
-
} : file
|
|
1210
|
-
);
|
|
1211
|
-
}
|
|
1212
|
-
return [...byPath.values()];
|
|
1213
|
-
}
|
|
1214
|
-
function survivesClaudeDirRename(asset) {
|
|
1215
|
-
if (asset.scope === "global") return true;
|
|
1216
|
-
switch (asset.method) {
|
|
1217
|
-
case "plugin":
|
|
1218
|
-
return true;
|
|
1219
|
-
// `~/.claude/plugins/cache` — 프로젝트 밖
|
|
1220
|
-
case "npm":
|
|
1221
|
-
return true;
|
|
1222
|
-
// `node_modules/`
|
|
1223
|
-
case "skill":
|
|
1224
|
-
return false;
|
|
1225
|
-
// `npx skills add` project scope → `.claude/skills/`
|
|
1226
|
-
case "shell-script":
|
|
1227
|
-
return false;
|
|
1228
|
-
// ecc-prune → `.claude/local-plugins/`
|
|
1229
|
-
case "npx-run":
|
|
1230
|
-
return false;
|
|
1231
|
-
case "internal":
|
|
1232
|
-
return false;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
function mergeAssets(previous, current) {
|
|
1236
|
-
if (!previous || previous.length === 0) return [...current];
|
|
1237
|
-
const currentById = new Map(current.map((a) => [a.id, a]));
|
|
1238
|
-
const previousIds = new Set(previous.map((a) => a.id));
|
|
1239
|
-
return [
|
|
1240
|
-
...previous.map((a) => currentById.get(a.id) ?? a),
|
|
1241
|
-
...current.filter((a) => !previousIds.has(a.id))
|
|
1242
|
-
];
|
|
1243
|
-
}
|
|
1244
|
-
function hashContent(content) {
|
|
1245
|
-
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1246
|
-
}
|
|
1247
|
-
function isHarnessOwned(baseline, key, current) {
|
|
1248
|
-
const recorded = baseline.get(key);
|
|
1249
|
-
return recorded !== void 0 && recorded === hashContent(current);
|
|
1250
|
-
}
|
|
1251
|
-
function mergeExternalFiles(projectDir, previous, current) {
|
|
1252
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
1253
|
-
for (const file of [...previous ?? [], ...current]) byPath.set(file.path, file);
|
|
1254
|
-
return [...byPath.values()].filter((f) => existsSync3(join3(projectDir, f.path)));
|
|
1255
|
-
}
|
|
1256
|
-
function collectSkillHashes(projectDir) {
|
|
1257
|
-
const skillsDir = join3(projectDir, ".claude/skills");
|
|
1258
|
-
return listFilesRecursive(skillsDir).map((rel) => ({
|
|
1259
|
-
path: rel,
|
|
1260
|
-
sha256: hashContent(readFileSync2(join3(skillsDir, rel), "utf8"))
|
|
1261
|
-
}));
|
|
1262
|
-
}
|
|
1263
|
-
function collectPolicyHashes(projectDir, templatesDir) {
|
|
1264
|
-
const out = [];
|
|
1265
|
-
for (const { dir, ext } of POLICY_DIRS) {
|
|
1266
|
-
const targetDir = join3(projectDir, ".claude", dir);
|
|
1267
|
-
const sourceDir = join3(templatesDir, dir);
|
|
1268
|
-
for (const rel of listFilesRecursive(targetDir)) {
|
|
1269
|
-
if (!rel.endsWith(ext)) continue;
|
|
1270
|
-
if (!existsSync3(join3(sourceDir, rel))) continue;
|
|
1271
|
-
out.push({
|
|
1272
|
-
path: `${dir}/${rel}`,
|
|
1273
|
-
sha256: hashContent(readFileSync2(join3(targetDir, rel), "utf8"))
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
return out;
|
|
1278
|
-
}
|
|
1279
|
-
function writeInstallLog(projectDir, log) {
|
|
1280
|
-
const path = installLogPath(projectDir);
|
|
1281
|
-
mkdirSync2(dirname2(path), { recursive: true });
|
|
1282
|
-
writeFileSync(path, `${JSON.stringify(log, null, 2)}
|
|
1283
|
-
`, "utf8");
|
|
1284
|
-
migrateAwayLegacyLog(projectDir);
|
|
1285
|
-
return path;
|
|
1286
|
-
}
|
|
1287
|
-
function migrateAwayLegacyLog(projectDir) {
|
|
1288
|
-
const legacy = legacyInstallLogPath(projectDir);
|
|
1289
|
-
if (!existsSync3(legacy)) return;
|
|
1290
|
-
try {
|
|
1291
|
-
rmSync(legacy);
|
|
1292
|
-
} catch {
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
function readInstallLog(projectDir) {
|
|
1296
|
-
const path = [installLogPath(projectDir), legacyInstallLogPath(projectDir)].find(
|
|
1297
|
-
(p2) => existsSync3(p2)
|
|
1298
|
-
);
|
|
1299
|
-
if (!path) return null;
|
|
1300
|
-
try {
|
|
1301
|
-
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1302
|
-
if (Array.isArray(parsed.assets)) {
|
|
1303
|
-
parsed.assets = parsed.assets.map(
|
|
1304
|
-
(a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
return parsed;
|
|
1308
|
-
} catch {
|
|
1309
|
-
return null;
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
function installLogPath(projectDir) {
|
|
1313
|
-
return join3(projectDir, INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
|
|
1314
|
-
}
|
|
1315
|
-
function legacyInstallLogPath(projectDir) {
|
|
1316
|
-
return join3(projectDir, LEGACY_INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
// src/owned-write.ts
|
|
1079
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1080
|
+
import { dirname, relative, sep } from "path";
|
|
1320
1081
|
function createOwnedWriter(projectDir, baseline, options = {}) {
|
|
1321
1082
|
const { now = /* @__PURE__ */ new Date(), refreshOnly = false } = options;
|
|
1322
1083
|
const written = /* @__PURE__ */ new Map();
|
|
@@ -1324,22 +1085,22 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
|
|
|
1324
1085
|
const backupPaths = [];
|
|
1325
1086
|
let updated = 0;
|
|
1326
1087
|
return {
|
|
1327
|
-
write(absPath, content) {
|
|
1088
|
+
write(absPath, content, opts) {
|
|
1328
1089
|
const rel = relative(projectDir, absPath).split(sep).join("/");
|
|
1329
1090
|
const digest = hashContent(content);
|
|
1330
|
-
if (!
|
|
1331
|
-
if (refreshOnly) return false;
|
|
1332
|
-
|
|
1333
|
-
|
|
1091
|
+
if (!existsSync2(absPath)) {
|
|
1092
|
+
if (refreshOnly && !opts?.createInRefresh) return false;
|
|
1093
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
1094
|
+
writeFileSync(absPath, content);
|
|
1334
1095
|
updated++;
|
|
1335
1096
|
} else {
|
|
1336
|
-
const current =
|
|
1097
|
+
const current = readFileSync(absPath, "utf8");
|
|
1337
1098
|
if (current !== content) {
|
|
1338
1099
|
if (!isHarnessOwned(baseline, rel, current)) {
|
|
1339
1100
|
backupPaths.push(backupFile(absPath, now));
|
|
1340
1101
|
backedUp.push(rel);
|
|
1341
1102
|
}
|
|
1342
|
-
|
|
1103
|
+
writeFileSync(absPath, content);
|
|
1343
1104
|
updated++;
|
|
1344
1105
|
}
|
|
1345
1106
|
}
|
|
@@ -1357,36 +1118,78 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
|
|
|
1357
1118
|
};
|
|
1358
1119
|
}
|
|
1359
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
|
+
|
|
1360
1145
|
// src/antigravity/transform.ts
|
|
1361
1146
|
function runAntigravityTransform(params) {
|
|
1362
|
-
const {
|
|
1147
|
+
const {
|
|
1148
|
+
harnessRoot,
|
|
1149
|
+
projectDir,
|
|
1150
|
+
selectedInternalSkills = [],
|
|
1151
|
+
rules = [],
|
|
1152
|
+
baseline,
|
|
1153
|
+
refreshOnly
|
|
1154
|
+
} = params;
|
|
1363
1155
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1364
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
|
+
}
|
|
1365
1167
|
const skillFiles = [];
|
|
1366
1168
|
for (const id of selectedInternalSkills) {
|
|
1367
|
-
const src =
|
|
1368
|
-
if (!
|
|
1169
|
+
const src = join3(harnessRoot, "templates/skills", id, "SKILL.md");
|
|
1170
|
+
if (!existsSync4(src)) {
|
|
1369
1171
|
continue;
|
|
1370
1172
|
}
|
|
1371
|
-
const target =
|
|
1372
|
-
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;
|
|
1373
1175
|
skillFiles.push(target);
|
|
1374
1176
|
}
|
|
1375
1177
|
return {
|
|
1376
1178
|
rulesFile,
|
|
1179
|
+
harnessRuleFiles,
|
|
1377
1180
|
skillFiles,
|
|
1378
1181
|
ownership: writer.result()
|
|
1379
1182
|
};
|
|
1380
1183
|
}
|
|
1381
1184
|
function writeRules(harnessRoot, projectDir, writer) {
|
|
1382
|
-
const claudeMdPath =
|
|
1383
|
-
const templatePath =
|
|
1384
|
-
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)) {
|
|
1385
1188
|
return null;
|
|
1386
1189
|
}
|
|
1387
|
-
const claudeMd =
|
|
1388
|
-
const template =
|
|
1389
|
-
const target =
|
|
1190
|
+
const claudeMd = readFileSync3(claudeMdPath, "utf8");
|
|
1191
|
+
const template = readFileSync3(templatePath, "utf8");
|
|
1192
|
+
const target = join3(projectDir, ".agents", "rules", "uzys-harness.md");
|
|
1390
1193
|
const rulesOut = renderAgentsMd({
|
|
1391
1194
|
template,
|
|
1392
1195
|
claudeMd,
|
|
@@ -1399,18 +1202,18 @@ function writeRules(harnessRoot, projectDir, writer) {
|
|
|
1399
1202
|
// src/codex/opt-in.ts
|
|
1400
1203
|
init_esm_shims();
|
|
1401
1204
|
import { homedir } from "os";
|
|
1402
|
-
import { join as
|
|
1205
|
+
import { join as join4 } from "path";
|
|
1403
1206
|
|
|
1404
1207
|
// src/codex/trust-entry.ts
|
|
1405
1208
|
init_esm_shims();
|
|
1406
|
-
import { existsSync as
|
|
1407
|
-
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";
|
|
1408
1211
|
var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
|
|
1409
1212
|
function registerTrustEntry(opts) {
|
|
1410
1213
|
const { configPath, projectDir } = opts;
|
|
1411
1214
|
try {
|
|
1412
|
-
|
|
1413
|
-
const existing =
|
|
1215
|
+
mkdirSync2(dirname2(configPath), { recursive: true });
|
|
1216
|
+
const existing = existsSync5(configPath) ? readFileSync4(configPath, "utf8") : "";
|
|
1414
1217
|
if (hasTrustEntry(existing, projectDir)) {
|
|
1415
1218
|
return { status: "already-present" };
|
|
1416
1219
|
}
|
|
@@ -1418,7 +1221,7 @@ function registerTrustEntry(opts) {
|
|
|
1418
1221
|
[projects."${projectDir}"]
|
|
1419
1222
|
trust_level = "trusted"
|
|
1420
1223
|
`;
|
|
1421
|
-
|
|
1224
|
+
writeFileSync2(configPath, existing + block);
|
|
1422
1225
|
return { status: "registered" };
|
|
1423
1226
|
} catch (e2) {
|
|
1424
1227
|
return {
|
|
@@ -1434,8 +1237,8 @@ function hasTrustEntry(configContent, projectDir) {
|
|
|
1434
1237
|
|
|
1435
1238
|
// src/codex/opt-in.ts
|
|
1436
1239
|
function runCodexOptIn(ctx) {
|
|
1437
|
-
const codexHome = ctx.codexHome ??
|
|
1438
|
-
const configPath =
|
|
1240
|
+
const codexHome = ctx.codexHome ?? join4(homedir(), ".codex");
|
|
1241
|
+
const configPath = join4(codexHome, "config.toml");
|
|
1439
1242
|
const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
|
|
1440
1243
|
return {
|
|
1441
1244
|
trustEntry: {
|
|
@@ -1448,8 +1251,8 @@ function runCodexOptIn(ctx) {
|
|
|
1448
1251
|
|
|
1449
1252
|
// src/codex/transform.ts
|
|
1450
1253
|
init_esm_shims();
|
|
1451
|
-
import { chmodSync, existsSync as
|
|
1452
|
-
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";
|
|
1453
1256
|
|
|
1454
1257
|
// src/codex/config-toml.ts
|
|
1455
1258
|
init_esm_shims();
|
|
@@ -1517,23 +1320,32 @@ function quoteIfNeeded(name) {
|
|
|
1517
1320
|
var HOOK_NAMES = ["session-start"];
|
|
1518
1321
|
var ENV_VAR_RENAME = /CLAUDE_PROJECT_DIR/g;
|
|
1519
1322
|
function runCodexTransform(params) {
|
|
1520
|
-
const {
|
|
1323
|
+
const {
|
|
1324
|
+
harnessRoot,
|
|
1325
|
+
projectDir,
|
|
1326
|
+
selectedInternalSkills = [],
|
|
1327
|
+
rules = [],
|
|
1328
|
+
baseline,
|
|
1329
|
+
refreshOnly
|
|
1330
|
+
} = params;
|
|
1521
1331
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1522
|
-
const claudeMd = readRequired(
|
|
1523
|
-
const agentsTemplate = readRequired(
|
|
1524
|
-
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"));
|
|
1525
1335
|
const projectName = basename2(projectDir);
|
|
1526
|
-
const mcp = readOptionalJson(
|
|
1527
|
-
const agentsMdPath =
|
|
1336
|
+
const mcp = readOptionalJson(join5(harnessRoot, ".mcp.json"));
|
|
1337
|
+
const agentsMdPath = join5(projectDir, "AGENTS.md");
|
|
1528
1338
|
ensureDir(projectDir);
|
|
1529
1339
|
const agentsMdOut = renderAgentsMd({
|
|
1530
1340
|
template: agentsTemplate,
|
|
1531
1341
|
claudeMd,
|
|
1532
1342
|
projectName,
|
|
1533
|
-
projectContext: renderFillScaffold()
|
|
1343
|
+
projectContext: renderFillScaffold(),
|
|
1344
|
+
// Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
|
|
1345
|
+
harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
|
|
1534
1346
|
});
|
|
1535
1347
|
writer.write(agentsMdPath, agentsMdOut);
|
|
1536
|
-
const configTomlPath =
|
|
1348
|
+
const configTomlPath = join5(projectDir, ".codex/config.toml");
|
|
1537
1349
|
writer.write(
|
|
1538
1350
|
configTomlPath,
|
|
1539
1351
|
renderConfigToml({
|
|
@@ -1543,27 +1355,27 @@ function runCodexTransform(params) {
|
|
|
1543
1355
|
mcp
|
|
1544
1356
|
})
|
|
1545
1357
|
);
|
|
1546
|
-
const hookDir =
|
|
1358
|
+
const hookDir = join5(projectDir, ".codex/hooks");
|
|
1547
1359
|
const hookFiles = [];
|
|
1548
1360
|
for (const hook of HOOK_NAMES) {
|
|
1549
|
-
const src =
|
|
1550
|
-
if (!
|
|
1361
|
+
const src = join5(harnessRoot, "templates/hooks", `${hook}.sh`);
|
|
1362
|
+
if (!existsSync6(src)) {
|
|
1551
1363
|
continue;
|
|
1552
1364
|
}
|
|
1553
|
-
const ported =
|
|
1554
|
-
const target =
|
|
1365
|
+
const ported = readFileSync5(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
|
|
1366
|
+
const target = join5(hookDir, `${hook}.sh`);
|
|
1555
1367
|
if (!writer.write(target, ported)) continue;
|
|
1556
1368
|
chmodSync(target, 493);
|
|
1557
1369
|
hookFiles.push(target);
|
|
1558
1370
|
}
|
|
1559
1371
|
const skillFiles = [];
|
|
1560
1372
|
for (const id of selectedInternalSkills) {
|
|
1561
|
-
const src =
|
|
1562
|
-
if (!
|
|
1373
|
+
const src = join5(harnessRoot, "templates/skills", id, "SKILL.md");
|
|
1374
|
+
if (!existsSync6(src)) {
|
|
1563
1375
|
continue;
|
|
1564
1376
|
}
|
|
1565
|
-
const target =
|
|
1566
|
-
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;
|
|
1567
1379
|
skillFiles.push(target);
|
|
1568
1380
|
}
|
|
1569
1381
|
return {
|
|
@@ -1575,17 +1387,17 @@ function runCodexTransform(params) {
|
|
|
1575
1387
|
};
|
|
1576
1388
|
}
|
|
1577
1389
|
function readRequired(path) {
|
|
1578
|
-
if (!
|
|
1390
|
+
if (!existsSync6(path)) {
|
|
1579
1391
|
throw new Error(`Codex transform: required source missing: ${path}`);
|
|
1580
1392
|
}
|
|
1581
|
-
return
|
|
1393
|
+
return readFileSync5(path, "utf8");
|
|
1582
1394
|
}
|
|
1583
1395
|
function readOptionalJson(path) {
|
|
1584
|
-
if (!
|
|
1396
|
+
if (!existsSync6(path)) {
|
|
1585
1397
|
return null;
|
|
1586
1398
|
}
|
|
1587
1399
|
try {
|
|
1588
|
-
return JSON.parse(
|
|
1400
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1589
1401
|
} catch {
|
|
1590
1402
|
return null;
|
|
1591
1403
|
}
|
|
@@ -1593,8 +1405,8 @@ function readOptionalJson(path) {
|
|
|
1593
1405
|
|
|
1594
1406
|
// src/opencode/transform.ts
|
|
1595
1407
|
init_esm_shims();
|
|
1596
|
-
import { existsSync as
|
|
1597
|
-
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";
|
|
1598
1410
|
|
|
1599
1411
|
// src/opencode/agents-md.ts
|
|
1600
1412
|
init_esm_shims();
|
|
@@ -1603,7 +1415,7 @@ function renameSlashes2(text) {
|
|
|
1603
1415
|
}
|
|
1604
1416
|
function renderAgentsMd2(params) {
|
|
1605
1417
|
const body = params.claudeMd.replace(/^#\s+.*\r?\n/, "").trim();
|
|
1606
|
-
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 ?? "");
|
|
1607
1419
|
return renameSlashes2(replaced);
|
|
1608
1420
|
}
|
|
1609
1421
|
|
|
@@ -1694,39 +1506,50 @@ function parseTemplate(template) {
|
|
|
1694
1506
|
|
|
1695
1507
|
// src/opencode/transform.ts
|
|
1696
1508
|
function runOpencodeTransform(params) {
|
|
1697
|
-
const {
|
|
1509
|
+
const {
|
|
1510
|
+
harnessRoot,
|
|
1511
|
+
projectDir,
|
|
1512
|
+
selectedInternalSkills = [],
|
|
1513
|
+
rules = [],
|
|
1514
|
+
baseline,
|
|
1515
|
+
refreshOnly
|
|
1516
|
+
} = params;
|
|
1698
1517
|
const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
|
|
1699
|
-
const claudeMd = readRequired2(
|
|
1700
|
-
const agentsTemplate = readRequired2(
|
|
1518
|
+
const claudeMd = readRequired2(join6(harnessRoot, "templates/CLAUDE.md"));
|
|
1519
|
+
const agentsTemplate = readRequired2(join6(harnessRoot, "templates/opencode/AGENTS.md.template"));
|
|
1701
1520
|
const opencodeTemplate = readRequired2(
|
|
1702
|
-
|
|
1521
|
+
join6(harnessRoot, "templates/opencode/opencode.json.template")
|
|
1703
1522
|
);
|
|
1704
1523
|
const projectName = basename3(projectDir);
|
|
1705
|
-
const mcp = readOptionalJson2(
|
|
1524
|
+
const mcp = readOptionalJson2(join6(harnessRoot, ".mcp.json"));
|
|
1706
1525
|
ensureDir(projectDir);
|
|
1707
|
-
const agentsMdPath =
|
|
1526
|
+
const agentsMdPath = join6(projectDir, "AGENTS.md");
|
|
1708
1527
|
const agentsMdOut = renderAgentsMd2({
|
|
1709
1528
|
template: agentsTemplate,
|
|
1710
1529
|
claudeMd,
|
|
1711
1530
|
projectName,
|
|
1712
|
-
projectContext: renderFillScaffold()
|
|
1531
|
+
projectContext: renderFillScaffold(),
|
|
1532
|
+
// codex 와 **같은 파일**(프로젝트 루트 `AGENTS.md`)이다. 두 transform 이 서로 다른 본문을
|
|
1533
|
+
// 쓰면 나중에 도는 쪽이 앞선 쪽을 덮어써, codex+opencode 조합에서 룰이 통째로 사라진다
|
|
1534
|
+
// (독립 검증 C-1 실측). 같은 내용을 쓰면 순서가 결과를 바꾸지 않는다.
|
|
1535
|
+
harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
|
|
1713
1536
|
});
|
|
1714
1537
|
writer.write(agentsMdPath, agentsMdOut);
|
|
1715
|
-
const opencodeJsonPath =
|
|
1538
|
+
const opencodeJsonPath = join6(projectDir, "opencode.json");
|
|
1716
1539
|
writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
|
|
1717
|
-
const cmdDir =
|
|
1540
|
+
const cmdDir = join6(projectDir, ".opencode/commands");
|
|
1718
1541
|
if (!refreshOnly) ensureDir(cmdDir);
|
|
1719
1542
|
const commandFiles = [];
|
|
1720
1543
|
for (const id of selectedInternalSkills) {
|
|
1721
|
-
const src =
|
|
1722
|
-
if (!
|
|
1544
|
+
const src = join6(harnessRoot, "templates/skills", id, "SKILL.md");
|
|
1545
|
+
if (!existsSync7(src)) {
|
|
1723
1546
|
continue;
|
|
1724
1547
|
}
|
|
1725
|
-
const target =
|
|
1726
|
-
const shellDependent =
|
|
1548
|
+
const target = join6(cmdDir, `${id}.md`);
|
|
1549
|
+
const shellDependent = existsSync7(join6(harnessRoot, "templates/skills", id, "scripts"));
|
|
1727
1550
|
const wrote = writer.write(
|
|
1728
1551
|
target,
|
|
1729
|
-
renderCommandFromSkill(
|
|
1552
|
+
renderCommandFromSkill(readFileSync6(src, "utf8"), id, { shellDependent })
|
|
1730
1553
|
);
|
|
1731
1554
|
if (!wrote) continue;
|
|
1732
1555
|
commandFiles.push(target);
|
|
@@ -1739,17 +1562,17 @@ function runOpencodeTransform(params) {
|
|
|
1739
1562
|
};
|
|
1740
1563
|
}
|
|
1741
1564
|
function readRequired2(path) {
|
|
1742
|
-
if (!
|
|
1565
|
+
if (!existsSync7(path)) {
|
|
1743
1566
|
throw new Error(`OpenCode transform: required source missing: ${path}`);
|
|
1744
1567
|
}
|
|
1745
|
-
return
|
|
1568
|
+
return readFileSync6(path, "utf8");
|
|
1746
1569
|
}
|
|
1747
1570
|
function readOptionalJson2(path) {
|
|
1748
|
-
if (!
|
|
1571
|
+
if (!existsSync7(path)) {
|
|
1749
1572
|
return null;
|
|
1750
1573
|
}
|
|
1751
1574
|
try {
|
|
1752
|
-
return JSON.parse(
|
|
1575
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1753
1576
|
} catch {
|
|
1754
1577
|
return null;
|
|
1755
1578
|
}
|
|
@@ -1763,6 +1586,7 @@ function runCliTransforms(params) {
|
|
|
1763
1586
|
projectDir,
|
|
1764
1587
|
cli: cli2,
|
|
1765
1588
|
selectedInternalSkills,
|
|
1589
|
+
rules,
|
|
1766
1590
|
previousExternal,
|
|
1767
1591
|
refreshOnly = false,
|
|
1768
1592
|
codexTrust = false
|
|
@@ -1788,6 +1612,7 @@ function runCliTransforms(params) {
|
|
|
1788
1612
|
harnessRoot,
|
|
1789
1613
|
projectDir,
|
|
1790
1614
|
selectedInternalSkills,
|
|
1615
|
+
rules,
|
|
1791
1616
|
baseline,
|
|
1792
1617
|
refreshOnly
|
|
1793
1618
|
});
|
|
@@ -1802,6 +1627,7 @@ function runCliTransforms(params) {
|
|
|
1802
1627
|
harnessRoot,
|
|
1803
1628
|
projectDir,
|
|
1804
1629
|
selectedInternalSkills,
|
|
1630
|
+
rules,
|
|
1805
1631
|
baseline,
|
|
1806
1632
|
refreshOnly
|
|
1807
1633
|
});
|
|
@@ -1813,6 +1639,7 @@ function runCliTransforms(params) {
|
|
|
1813
1639
|
harnessRoot,
|
|
1814
1640
|
projectDir,
|
|
1815
1641
|
selectedInternalSkills,
|
|
1642
|
+
rules,
|
|
1816
1643
|
baseline,
|
|
1817
1644
|
refreshOnly
|
|
1818
1645
|
});
|
|
@@ -1832,8 +1659,8 @@ function runCliTransforms(params) {
|
|
|
1832
1659
|
|
|
1833
1660
|
// src/env-files.ts
|
|
1834
1661
|
init_esm_shims();
|
|
1835
|
-
import { appendFileSync, existsSync as
|
|
1836
|
-
import { join as
|
|
1662
|
+
import { appendFileSync, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
1663
|
+
import { join as join7 } from "path";
|
|
1837
1664
|
var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
|
|
1838
1665
|
# Copy to .env (gitignored) and fill in values: cp .env.example .env
|
|
1839
1666
|
|
|
@@ -1870,19 +1697,19 @@ function writeEnvExample(projectDir, tracks) {
|
|
|
1870
1697
|
if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
|
|
1871
1698
|
return false;
|
|
1872
1699
|
}
|
|
1873
|
-
const path =
|
|
1874
|
-
if (
|
|
1700
|
+
const path = join7(projectDir, ".env.example");
|
|
1701
|
+
if (existsSync8(path)) {
|
|
1875
1702
|
return false;
|
|
1876
1703
|
}
|
|
1877
|
-
|
|
1704
|
+
writeFileSync3(path, ENV_EXAMPLE_BODY);
|
|
1878
1705
|
return true;
|
|
1879
1706
|
}
|
|
1880
1707
|
function addGitignoreEnv(projectDir) {
|
|
1881
|
-
const path =
|
|
1882
|
-
if (!
|
|
1708
|
+
const path = join7(projectDir, ".gitignore");
|
|
1709
|
+
if (!existsSync8(path)) {
|
|
1883
1710
|
return false;
|
|
1884
1711
|
}
|
|
1885
|
-
const content =
|
|
1712
|
+
const content = readFileSync7(path, "utf8");
|
|
1886
1713
|
if (GITIGNORE_ENV_PATTERN.test(content)) {
|
|
1887
1714
|
return false;
|
|
1888
1715
|
}
|
|
@@ -1896,11 +1723,11 @@ function addGitignoreEnv(projectDir) {
|
|
|
1896
1723
|
var AGENT_ARTIFACT_DIRS = [".factory/", ".goose/", ".uzys-agent-harness/"];
|
|
1897
1724
|
var GITIGNORE_AGENT_ARTIFACT_HEADER = "# agent CLI / harness \uC790\uB3D9 \uC0DD\uC131\uBB3C (auto-added by agent-harness)";
|
|
1898
1725
|
function addGitignoreAgentArtifacts(projectDir) {
|
|
1899
|
-
const path =
|
|
1900
|
-
if (!
|
|
1726
|
+
const path = join7(projectDir, ".gitignore");
|
|
1727
|
+
if (!existsSync8(path)) {
|
|
1901
1728
|
return [];
|
|
1902
1729
|
}
|
|
1903
|
-
const content =
|
|
1730
|
+
const content = readFileSync7(path, "utf8");
|
|
1904
1731
|
const missing = AGENT_ARTIFACT_DIRS.filter((pattern) => {
|
|
1905
1732
|
const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
|
|
1906
1733
|
return !lineRegex.test(content);
|
|
@@ -1915,43 +1742,13 @@ ${block}
|
|
|
1915
1742
|
`);
|
|
1916
1743
|
return [...missing];
|
|
1917
1744
|
}
|
|
1918
|
-
function writeMcpAllowlist(projectDir) {
|
|
1919
|
-
const allowlistPath = join8(projectDir, ".mcp-allowlist");
|
|
1920
|
-
if (existsSync9(allowlistPath)) {
|
|
1921
|
-
return null;
|
|
1922
|
-
}
|
|
1923
|
-
const mcpPath = join8(projectDir, ".mcp.json");
|
|
1924
|
-
if (!existsSync9(mcpPath)) {
|
|
1925
|
-
return null;
|
|
1926
|
-
}
|
|
1927
|
-
let names = [];
|
|
1928
|
-
try {
|
|
1929
|
-
const parsed = JSON.parse(readFileSync8(mcpPath, "utf8"));
|
|
1930
|
-
names = Object.keys(parsed.mcpServers ?? {}).sort();
|
|
1931
|
-
} catch {
|
|
1932
|
-
return null;
|
|
1933
|
-
}
|
|
1934
|
-
if (names.length === 0) {
|
|
1935
|
-
return [];
|
|
1936
|
-
}
|
|
1937
|
-
const body = [
|
|
1938
|
-
"# MCP Server Allowlist \u2014 auto-generated by agent-harness install",
|
|
1939
|
-
"# Referenced by mcp-pre-exec.sh hook. Remove or '#' comment any server you want to block.",
|
|
1940
|
-
"# Deleting this file disables the gate (allows all MCP calls).",
|
|
1941
|
-
"",
|
|
1942
|
-
...names,
|
|
1943
|
-
""
|
|
1944
|
-
].join("\n");
|
|
1945
|
-
writeFileSync4(allowlistPath, body);
|
|
1946
|
-
return names;
|
|
1947
|
-
}
|
|
1948
1745
|
|
|
1949
1746
|
// src/external-installer.ts
|
|
1950
1747
|
init_esm_shims();
|
|
1951
1748
|
import { spawnSync } from "child_process";
|
|
1952
|
-
import { existsSync as
|
|
1749
|
+
import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync8 } from "fs";
|
|
1953
1750
|
import { homedir as homedir2 } from "os";
|
|
1954
|
-
import { join as
|
|
1751
|
+
import { join as join8 } from "path";
|
|
1955
1752
|
var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
|
|
1956
1753
|
function selectExternalTargets(assets, ctx) {
|
|
1957
1754
|
const conditionPassed = filterApplicableAssets(assets, ctx).filter(
|
|
@@ -2027,8 +1824,8 @@ function installOne(asset, ctx) {
|
|
|
2027
1824
|
cwd
|
|
2028
1825
|
);
|
|
2029
1826
|
case "shell-script": {
|
|
2030
|
-
const scriptPath =
|
|
2031
|
-
if (!
|
|
1827
|
+
const scriptPath = join8(ctx.harnessRoot, method.script);
|
|
1828
|
+
if (!existsSync9(scriptPath)) {
|
|
2032
1829
|
return {
|
|
2033
1830
|
asset,
|
|
2034
1831
|
ok: false,
|
|
@@ -2118,17 +1915,17 @@ function detectVersion(method, spawn) {
|
|
|
2118
1915
|
if (at2 <= 0) return void 0;
|
|
2119
1916
|
const plugin = method.pluginId.slice(0, at2);
|
|
2120
1917
|
const marketplaceShort = method.pluginId.slice(at2 + 1);
|
|
2121
|
-
const cacheBase =
|
|
2122
|
-
if (!
|
|
2123
|
-
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();
|
|
2124
1921
|
return versions.at(-1);
|
|
2125
1922
|
}
|
|
2126
1923
|
case "npm": {
|
|
2127
1924
|
const npmRoot = getNpmGlobalRoot(spawn);
|
|
2128
1925
|
if (!npmRoot) return void 0;
|
|
2129
|
-
const pkgJson =
|
|
2130
|
-
if (!
|
|
2131
|
-
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"));
|
|
2132
1929
|
return parsed.version;
|
|
2133
1930
|
}
|
|
2134
1931
|
default:
|
|
@@ -2155,7 +1952,7 @@ function getNpmGlobalRoot(spawn) {
|
|
|
2155
1952
|
|
|
2156
1953
|
// src/mcp-merge.ts
|
|
2157
1954
|
init_esm_shims();
|
|
2158
|
-
import { existsSync as
|
|
1955
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
2159
1956
|
function parseTrackMcpMap(raw) {
|
|
2160
1957
|
const rows = [];
|
|
2161
1958
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -2206,15 +2003,15 @@ function mergeMcpServers(base, rows, tracks) {
|
|
|
2206
2003
|
return out;
|
|
2207
2004
|
}
|
|
2208
2005
|
function composeMcpJson(opts) {
|
|
2209
|
-
const base = JSON.parse(
|
|
2210
|
-
const merged = opts.existingPath &&
|
|
2211
|
-
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") : "";
|
|
2212
2009
|
const rows = parseTrackMcpMap(mapRaw);
|
|
2213
2010
|
return mergeMcpServers(merged, rows, opts.tracks);
|
|
2214
2011
|
}
|
|
2215
2012
|
function mergeUserBase(base, existingPath) {
|
|
2216
2013
|
try {
|
|
2217
|
-
const existing = JSON.parse(
|
|
2014
|
+
const existing = JSON.parse(readFileSync9(existingPath, "utf8"));
|
|
2218
2015
|
return {
|
|
2219
2016
|
...base,
|
|
2220
2017
|
mcpServers: { ...base.mcpServers, ...existing.mcpServers }
|
|
@@ -2224,23 +2021,58 @@ function mergeUserBase(base, existingPath) {
|
|
|
2224
2021
|
}
|
|
2225
2022
|
}
|
|
2226
2023
|
function writeMcpJson(path, mcp) {
|
|
2227
|
-
|
|
2024
|
+
writeFileSync4(path, `${JSON.stringify(mcp, null, 2)}
|
|
2228
2025
|
`);
|
|
2229
2026
|
}
|
|
2230
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
|
+
|
|
2231
2063
|
// src/update-mode.ts
|
|
2232
2064
|
init_esm_shims();
|
|
2233
2065
|
import {
|
|
2234
|
-
copyFileSync
|
|
2066
|
+
copyFileSync,
|
|
2235
2067
|
existsSync as existsSync12,
|
|
2236
2068
|
lstatSync,
|
|
2237
|
-
mkdirSync as
|
|
2238
|
-
readdirSync as
|
|
2069
|
+
mkdirSync as mkdirSync3,
|
|
2070
|
+
readdirSync as readdirSync2,
|
|
2239
2071
|
readFileSync as readFileSync11,
|
|
2240
2072
|
unlinkSync,
|
|
2241
|
-
writeFileSync as
|
|
2073
|
+
writeFileSync as writeFileSync5
|
|
2242
2074
|
} from "fs";
|
|
2243
|
-
import { basename as basename4, dirname as
|
|
2075
|
+
import { basename as basename4, dirname as dirname3, join as join10 } from "path";
|
|
2244
2076
|
var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
|
|
2245
2077
|
function buildUpdateSpec(projectDir, tracks) {
|
|
2246
2078
|
return {
|
|
@@ -2267,7 +2099,8 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot) {
|
|
|
2267
2099
|
externalBackedUp: [],
|
|
2268
2100
|
installedNew: [],
|
|
2269
2101
|
restored: [],
|
|
2270
|
-
needsReinstall: []
|
|
2102
|
+
needsReinstall: [],
|
|
2103
|
+
mcpAllowlistRetired: null
|
|
2271
2104
|
};
|
|
2272
2105
|
const fresh = installNewAssets(projectDir, templatesDir);
|
|
2273
2106
|
report.installedNew = fresh.installed;
|
|
@@ -2294,11 +2127,15 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot) {
|
|
|
2294
2127
|
report.skillsBackedUp = skillSync.backedUp;
|
|
2295
2128
|
report.skillsSkippedLinks = skillSync.skippedLinks;
|
|
2296
2129
|
refreshSkillBaseline(projectDir);
|
|
2297
|
-
|
|
2130
|
+
const installLog = readInstallLog(projectDir);
|
|
2131
|
+
if (existsSync12(claudeDir) && (installLog === null || installLog.spec.cli.includes("claude"))) {
|
|
2132
|
+
syncHarnessAnchor(projectDir, templatesDir, report);
|
|
2133
|
+
}
|
|
2298
2134
|
const settingsPath = join10(claudeDir, "settings.json");
|
|
2299
2135
|
if (existsSync12(settingsPath)) {
|
|
2300
2136
|
report.staleHookRefs = cleanStaleHookRefs(settingsPath, claudeDir);
|
|
2301
2137
|
}
|
|
2138
|
+
report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
|
|
2302
2139
|
const external = refreshExternalCli(projectDir, harnessRoot);
|
|
2303
2140
|
report.externalUpdated = external.externalUpdated;
|
|
2304
2141
|
report.externalBackedUp = external.externalBackedUp;
|
|
@@ -2308,10 +2145,13 @@ function installNewAssets(projectDir, templatesDir) {
|
|
|
2308
2145
|
const installed = [];
|
|
2309
2146
|
const restored = [];
|
|
2310
2147
|
const needsReinstall = [];
|
|
2311
|
-
const
|
|
2148
|
+
const log = readInstallLog(projectDir);
|
|
2149
|
+
const claudeSelected = log?.spec.cli.includes("claude") ?? false;
|
|
2150
|
+
const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
|
|
2312
2151
|
const priorBaseline = policyBaseline(projectDir);
|
|
2313
2152
|
for (const entry of trackOnlyFileAssets(installedTracks(projectDir))) {
|
|
2314
2153
|
if (entry.target === HARNESS_ANCHOR_FILE) continue;
|
|
2154
|
+
if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
|
|
2315
2155
|
const target = join10(projectDir, entry.target);
|
|
2316
2156
|
if (existsSync12(target)) continue;
|
|
2317
2157
|
const source = join10(templatesDir, entry.source);
|
|
@@ -2323,8 +2163,8 @@ function installNewAssets(projectDir, templatesDir) {
|
|
|
2323
2163
|
continue;
|
|
2324
2164
|
}
|
|
2325
2165
|
}
|
|
2326
|
-
|
|
2327
|
-
|
|
2166
|
+
mkdirSync3(dirname3(target), { recursive: true });
|
|
2167
|
+
copyFileSync(source, target);
|
|
2328
2168
|
const recordedAs = entry.target.startsWith(".claude/") ? entry.target.slice(8) : entry.target;
|
|
2329
2169
|
if (priorBaseline.has(recordedAs)) restored.push(entry.target);
|
|
2330
2170
|
else installed.push(entry.target);
|
|
@@ -2347,7 +2187,7 @@ function syncHarnessAnchor(projectDir, templatesDir, report) {
|
|
|
2347
2187
|
if (!existsSync12(templateMd)) return;
|
|
2348
2188
|
const anchor = join10(projectDir, HARNESS_ANCHOR_FILE);
|
|
2349
2189
|
const existed = existsSync12(anchor);
|
|
2350
|
-
|
|
2190
|
+
copyFileSync(templateMd, anchor);
|
|
2351
2191
|
if (existed) {
|
|
2352
2192
|
report.claudeMdUpdated = true;
|
|
2353
2193
|
return;
|
|
@@ -2364,7 +2204,7 @@ function upsertRootImport(projectDir) {
|
|
|
2364
2204
|
tracks: installedTracks(projectDir)
|
|
2365
2205
|
});
|
|
2366
2206
|
if (next === existing) return false;
|
|
2367
|
-
|
|
2207
|
+
writeFileSync5(target, next);
|
|
2368
2208
|
return true;
|
|
2369
2209
|
}
|
|
2370
2210
|
function installedTracks(projectDir) {
|
|
@@ -2391,11 +2231,14 @@ function recordAnchorBaseline(projectDir, anchor) {
|
|
|
2391
2231
|
}
|
|
2392
2232
|
function refreshExternalCli(projectDir, harnessRoot) {
|
|
2393
2233
|
const log = readInstallLog(projectDir);
|
|
2234
|
+
const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
|
|
2394
2235
|
const result = runCliTransforms({
|
|
2395
2236
|
harnessRoot,
|
|
2396
2237
|
projectDir,
|
|
2397
2238
|
cli: ALL_CLI_TARGETS,
|
|
2398
2239
|
selectedInternalSkills: INTERNAL_BUNDLED_SKILL_IDS,
|
|
2240
|
+
// 스킬은 **전부** 넘긴다 — 안 깔린 스킬은 대상 파일이 없어 refreshOnly 가 건너뛴다.
|
|
2241
|
+
rules: ALL_RULES.filter((r) => !isBaselineExcluded(`.claude/rules/${r}.md`, baselineExcluded)),
|
|
2399
2242
|
previousExternal: log?.externalFiles ?? [],
|
|
2400
2243
|
refreshOnly: true
|
|
2401
2244
|
});
|
|
@@ -2421,7 +2264,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
|
2421
2264
|
if (!existsSync12(target) || !existsSync12(source)) return { updated: 0, backedUp: [] };
|
|
2422
2265
|
let updated = 0;
|
|
2423
2266
|
const backedUp = [];
|
|
2424
|
-
for (const file of
|
|
2267
|
+
for (const file of readdirSync2(target)) {
|
|
2425
2268
|
if (!file.endsWith(ext)) continue;
|
|
2426
2269
|
const targetFile = join10(target, file);
|
|
2427
2270
|
const sourceFile = join10(source, file);
|
|
@@ -2433,7 +2276,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
|
2433
2276
|
backupFile(targetFile, now);
|
|
2434
2277
|
backedUp.push(`${ctx.prefix}/${file}`);
|
|
2435
2278
|
}
|
|
2436
|
-
|
|
2279
|
+
writeFileSync5(targetFile, next);
|
|
2437
2280
|
updated++;
|
|
2438
2281
|
}
|
|
2439
2282
|
return { updated, backedUp };
|
|
@@ -2444,7 +2287,7 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
|
|
|
2444
2287
|
let updated = 0;
|
|
2445
2288
|
const backedUp = [];
|
|
2446
2289
|
const skippedLinks = [];
|
|
2447
|
-
for (const skill of
|
|
2290
|
+
for (const skill of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2448
2291
|
if (!skill.isDirectory()) continue;
|
|
2449
2292
|
const targetSkill = join10(targetDir, skill.name);
|
|
2450
2293
|
if (!existsSync12(targetSkill)) continue;
|
|
@@ -2456,8 +2299,8 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
|
|
|
2456
2299
|
const targetFile = join10(targetSkill, rel);
|
|
2457
2300
|
const next = readFileSync11(join10(sourceDir, skill.name, rel), "utf8");
|
|
2458
2301
|
if (!existsSync12(targetFile)) {
|
|
2459
|
-
|
|
2460
|
-
|
|
2302
|
+
mkdirSync3(dirname3(targetFile), { recursive: true });
|
|
2303
|
+
writeFileSync5(targetFile, next);
|
|
2461
2304
|
updated++;
|
|
2462
2305
|
continue;
|
|
2463
2306
|
}
|
|
@@ -2468,7 +2311,7 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
|
|
|
2468
2311
|
backupFile(targetFile, now);
|
|
2469
2312
|
backedUp.push(`${skill.name}/${rel}`);
|
|
2470
2313
|
}
|
|
2471
|
-
|
|
2314
|
+
writeFileSync5(targetFile, next);
|
|
2472
2315
|
updated++;
|
|
2473
2316
|
}
|
|
2474
2317
|
}
|
|
@@ -2509,7 +2352,7 @@ function refreshSkillBaseline(projectDir) {
|
|
|
2509
2352
|
function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
|
|
2510
2353
|
if (!existsSync12(target) || !existsSync12(source)) return [];
|
|
2511
2354
|
const removed = [];
|
|
2512
|
-
for (const file of
|
|
2355
|
+
for (const file of readdirSync2(target)) {
|
|
2513
2356
|
if (!file.endsWith(ext)) continue;
|
|
2514
2357
|
const sourceFile = join10(source, file);
|
|
2515
2358
|
if (!existsSync12(sourceFile)) {
|
|
@@ -2526,6 +2369,17 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
|
|
|
2526
2369
|
}
|
|
2527
2370
|
return removed;
|
|
2528
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
|
+
}
|
|
2529
2383
|
function cleanStaleHookRefs(settingsPath, claudeDir) {
|
|
2530
2384
|
let settings;
|
|
2531
2385
|
try {
|
|
@@ -2548,7 +2402,7 @@ function cleanStaleHookRefs(settingsPath, claudeDir) {
|
|
|
2548
2402
|
}
|
|
2549
2403
|
if (removed.length > 0) {
|
|
2550
2404
|
const next = { ...settings, hooks: cleanedHooks };
|
|
2551
|
-
|
|
2405
|
+
writeFileSync5(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
2552
2406
|
`);
|
|
2553
2407
|
}
|
|
2554
2408
|
return removed;
|
|
@@ -2588,17 +2442,27 @@ function runInstall(ctx) {
|
|
|
2588
2442
|
throw new Error(`Templates dir not found: ${templatesDir}`);
|
|
2589
2443
|
}
|
|
2590
2444
|
const claudeDir = join11(projectDir, ".claude");
|
|
2591
|
-
if (mode === "update" && !existsSync13(claudeDir)) {
|
|
2592
|
-
throw new Error(`Update mode requires existing .claude/ at ${claudeDir}`);
|
|
2593
|
-
}
|
|
2594
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
|
+
}
|
|
2595
2452
|
const backupPath = resolveBackupPath(ctx, mode, claudeDir);
|
|
2596
2453
|
if (mode === "update") {
|
|
2597
2454
|
return runUpdateInstall(ctx, templatesDir, backupPath);
|
|
2598
2455
|
}
|
|
2599
2456
|
const manifestSpec = buildManifestSpec(spec);
|
|
2600
2457
|
const policyBase = new Map((previousLog?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
|
|
2601
|
-
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
|
+
);
|
|
2602
2466
|
const mcpResult = composeAndWriteMcp(harnessRoot, projectDir, spec);
|
|
2603
2467
|
const ciScaffold = isAssetSelected("ci-scaffold", {
|
|
2604
2468
|
tracks: spec.tracks,
|
|
@@ -2616,6 +2480,11 @@ function runInstall(ctx) {
|
|
|
2616
2480
|
projectDir,
|
|
2617
2481
|
cli: spec.cli,
|
|
2618
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
|
+
),
|
|
2619
2488
|
previousExternal: previousLog?.externalFiles ?? [],
|
|
2620
2489
|
codexTrust: (spec.scope ?? "project") === "global" && spec.options.withCodexTrust
|
|
2621
2490
|
});
|
|
@@ -2635,7 +2504,12 @@ function runInstall(ctx) {
|
|
|
2635
2504
|
rootClaudeMd: base.rootClaudeMd,
|
|
2636
2505
|
// 외부 CLI 백업도 같은 줄에 노출한다 — 백업이 화면에 안 보이면 사용자는 자기 편집분이
|
|
2637
2506
|
// 어디 갔는지 알 수 없고, 그러면 백업은 있어도 없는 것과 같다 (ADR-046/047 과 같은 이유).
|
|
2638
|
-
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)
|
|
2639
2513
|
};
|
|
2640
2514
|
ctx.onProgress?.({ type: "baseline-complete", baseline });
|
|
2641
2515
|
const external = runExternalPhase(ctx);
|
|
@@ -2651,6 +2525,21 @@ function runInstall(ctx) {
|
|
|
2651
2525
|
);
|
|
2652
2526
|
return { ...baseline, external, staleHookRefs };
|
|
2653
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
|
+
}
|
|
2654
2543
|
function healStaleHookRefs(spec, projectDir) {
|
|
2655
2544
|
if (!spec.cli.includes("claude")) return [];
|
|
2656
2545
|
const settingsPath = join11(projectDir, ".claude/settings.json");
|
|
@@ -2668,6 +2557,11 @@ function runUpdateInstall(ctx, templatesDir, backupPath) {
|
|
|
2668
2557
|
filesCopied: 0,
|
|
2669
2558
|
dirsCopied: 0,
|
|
2670
2559
|
skipped: 0,
|
|
2560
|
+
baselineExcluded: [],
|
|
2561
|
+
baselineExcludedOnDisk: [],
|
|
2562
|
+
// update 는 밀려난 자산을 다루지 않는다 — 그 판정은 사용자가 옵션을 고르는 설치 경로의
|
|
2563
|
+
// 것이고, update 는 무엇을 골랐는지 다시 묻지 않는다.
|
|
2564
|
+
superseded: { removed: [], kept: [] },
|
|
2671
2565
|
backup: backupPath,
|
|
2672
2566
|
installedTracks: [...ctx.spec.tracks].sort(),
|
|
2673
2567
|
mcpServers: [],
|
|
@@ -2681,7 +2575,6 @@ function runUpdateInstall(ctx, templatesDir, backupPath) {
|
|
|
2681
2575
|
envFiles: {
|
|
2682
2576
|
envExampleCreated: false,
|
|
2683
2577
|
gitignoreEnvAdded: false,
|
|
2684
|
-
mcpAllowlist: null,
|
|
2685
2578
|
gitignoreNpxSkillsAdded: []
|
|
2686
2579
|
},
|
|
2687
2580
|
rootClaudeMd: null
|
|
@@ -2718,9 +2611,30 @@ function emptyClaudeBaseline() {
|
|
|
2718
2611
|
categories: { rules: [], agents: [], hooks: [], commands: 0, skills: [] },
|
|
2719
2612
|
rootClaudeMd: null,
|
|
2720
2613
|
rootClaudeMdLog: null,
|
|
2721
|
-
backups: []
|
|
2614
|
+
backups: [],
|
|
2615
|
+
excluded: [],
|
|
2616
|
+
excludedOnDisk: []
|
|
2722
2617
|
};
|
|
2723
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
|
+
}
|
|
2724
2638
|
function backupEditedPolicyFile(entryTarget, target, source, baseline) {
|
|
2725
2639
|
const prefix = ".claude/";
|
|
2726
2640
|
if (!entryTarget.startsWith(prefix)) return null;
|
|
@@ -2735,7 +2649,7 @@ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
|
|
|
2735
2649
|
if (recorded !== void 0 && recorded === hashContent(current)) return null;
|
|
2736
2650
|
return backupFile(target);
|
|
2737
2651
|
}
|
|
2738
|
-
function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase) {
|
|
2652
|
+
function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase, baselineExcluded) {
|
|
2739
2653
|
ensureProjectSkeleton(projectDir);
|
|
2740
2654
|
const result = emptyClaudeBaseline();
|
|
2741
2655
|
const manifest = buildManifest(manifestSpec);
|
|
@@ -2743,8 +2657,13 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
|
|
|
2743
2657
|
if (!entry.applies(manifestSpec)) {
|
|
2744
2658
|
continue;
|
|
2745
2659
|
}
|
|
2746
|
-
const source = join11(templatesDir, entry.source);
|
|
2747
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);
|
|
2748
2667
|
if (!existsSync13(source)) {
|
|
2749
2668
|
result.skipped += 1;
|
|
2750
2669
|
continue;
|
|
@@ -2788,7 +2707,6 @@ function writeEnvironmentFiles(projectDir, tracks) {
|
|
|
2788
2707
|
return {
|
|
2789
2708
|
envExampleCreated: writeEnvExample(projectDir, tracks),
|
|
2790
2709
|
gitignoreEnvAdded: addGitignoreEnv(projectDir),
|
|
2791
|
-
mcpAllowlist: writeMcpAllowlist(projectDir),
|
|
2792
2710
|
// v0.8.0 — `.factory/`, `.goose/` ignore (npx skills universal install 사용자 #3).
|
|
2793
2711
|
// 2026-08-02 — `.uzys-agent-harness/` 합류 (설치 로그 + 훅 차단 로그).
|
|
2794
2712
|
gitignoreNpxSkillsAdded: addGitignoreAgentArtifacts(projectDir)
|
|
@@ -2880,13 +2798,6 @@ function collectRootFiles(envFiles, ciScaffold, mcpCreated) {
|
|
|
2880
2798
|
if (envFiles.envExampleCreated) {
|
|
2881
2799
|
files.push({ path: ".env.example", change: "created", notes: ["Supabase \uD1A0\uD070 \uAC00\uC774\uB4DC"] });
|
|
2882
2800
|
}
|
|
2883
|
-
if (envFiles.mcpAllowlist && envFiles.mcpAllowlist.length > 0) {
|
|
2884
|
-
files.push({
|
|
2885
|
-
path: ".mcp-allowlist",
|
|
2886
|
-
change: "created",
|
|
2887
|
-
notes: [`MCP allowlist (${envFiles.mcpAllowlist.length} server)`]
|
|
2888
|
-
});
|
|
2889
|
-
}
|
|
2890
2801
|
const gitignoreAdded = [
|
|
2891
2802
|
...envFiles.gitignoreEnvAdded ? [".env"] : [],
|
|
2892
2803
|
...envFiles.gitignoreNpxSkillsAdded
|
|
@@ -2923,9 +2834,9 @@ function accumulateCategory(cats, entry) {
|
|
|
2923
2834
|
}
|
|
2924
2835
|
function writeInstalledTracks(projectDir, tracks) {
|
|
2925
2836
|
const path = join11(projectDir, ".claude/.installed-tracks");
|
|
2926
|
-
|
|
2837
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
2927
2838
|
const sorted = [...new Set(tracks)].sort().join("\n");
|
|
2928
|
-
|
|
2839
|
+
writeFileSync6(path, `${sorted}
|
|
2929
2840
|
`);
|
|
2930
2841
|
}
|
|
2931
2842
|
function writeRootClaudeMd(projectDir, tracks) {
|
|
@@ -2933,7 +2844,7 @@ function writeRootClaudeMd(projectDir, tracks) {
|
|
|
2933
2844
|
const existing = existsSync13(target) ? readFileSync12(target, "utf-8") : null;
|
|
2934
2845
|
const content = upsertHarnessImport(existing, { projectName: basename5(projectDir), tracks });
|
|
2935
2846
|
if (content !== existing) {
|
|
2936
|
-
|
|
2847
|
+
writeFileSync6(target, content);
|
|
2937
2848
|
}
|
|
2938
2849
|
return { created: existing === null };
|
|
2939
2850
|
}
|
|
@@ -2946,7 +2857,7 @@ function chmodHooksSync(hookDir) {
|
|
|
2946
2857
|
}
|
|
2947
2858
|
}
|
|
2948
2859
|
function listHookFiles(hookDir) {
|
|
2949
|
-
return
|
|
2860
|
+
return readdirSync3(hookDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".sh")).map((e2) => resolve(hookDir, e2.name));
|
|
2950
2861
|
}
|
|
2951
2862
|
|
|
2952
2863
|
// src/commands/install-render.ts
|
|
@@ -3299,6 +3210,15 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
|
|
|
3299
3210
|
)
|
|
3300
3211
|
);
|
|
3301
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
|
+
}
|
|
3302
3222
|
if (baseline.updateMode.policyBackedUp.length > 0) {
|
|
3303
3223
|
log(
|
|
3304
3224
|
assetRow(
|
|
@@ -3373,12 +3293,12 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
|
|
|
3373
3293
|
phase1Row(
|
|
3374
3294
|
"hooks",
|
|
3375
3295
|
cats.hooks.length,
|
|
3376
|
-
"session-start \xB7 protect-files \xB7
|
|
3296
|
+
"session-start \xB7 protect-files \xB7 task-brief-nudge",
|
|
3377
3297
|
cats.hooks
|
|
3378
3298
|
);
|
|
3379
3299
|
}
|
|
3380
3300
|
if (cats.commands > 0) {
|
|
3381
|
-
phase1Row("commands", cats.commands, "
|
|
3301
|
+
phase1Row("commands", cats.commands, "slash commands");
|
|
3382
3302
|
}
|
|
3383
3303
|
if (cats.skills.length > 0) {
|
|
3384
3304
|
phase1Row(
|
|
@@ -3392,6 +3312,38 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
|
|
|
3392
3312
|
log(assetRow("success", "rules + hooks + commands + agents", `${baseline.filesCopied} files`));
|
|
3393
3313
|
log(assetRow("success", "skeleton", `${baseline.dirsCopied} dirs`));
|
|
3394
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
|
+
}
|
|
3395
3347
|
const TEMPLATES_COL = 28;
|
|
3396
3348
|
if (baseline.rootClaudeMd) {
|
|
3397
3349
|
const n = baseline.rootClaudeMd.tracks.length;
|
|
@@ -3427,16 +3379,6 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
|
|
|
3427
3379
|
}
|
|
3428
3380
|
const mcpList = baseline.mcpServers.join(", ") || "(none)";
|
|
3429
3381
|
log(assetRow("success", ".mcp.json", mcpList, TEMPLATES_COL));
|
|
3430
|
-
if (baseline.envFiles.mcpAllowlist) {
|
|
3431
|
-
log(
|
|
3432
|
-
assetRow(
|
|
3433
|
-
"success",
|
|
3434
|
-
".mcp-allowlist",
|
|
3435
|
-
`${baseline.envFiles.mcpAllowlist.length} servers (D35 opt-in gate)`,
|
|
3436
|
-
TEMPLATES_COL
|
|
3437
|
-
)
|
|
3438
|
-
);
|
|
3439
|
-
}
|
|
3440
3382
|
if (!withEcc && baseline.categories) {
|
|
3441
3383
|
log("");
|
|
3442
3384
|
log(
|
|
@@ -3548,15 +3490,26 @@ function installAction(options, deps = {}) {
|
|
|
3548
3490
|
}
|
|
3549
3491
|
const forceInclude = normalizeRepeatable(options.with);
|
|
3550
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));
|
|
3551
3497
|
const validIds = new Set(EXTERNAL_ASSETS.map((a) => a.id));
|
|
3552
|
-
for (const id of
|
|
3553
|
-
if (
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
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
|
+
);
|
|
3560
3513
|
}
|
|
3561
3514
|
const filteredInclude = forceInclude.filter((id) => validIds.has(id));
|
|
3562
3515
|
const filteredExclude = forceExclude.filter((id) => validIds.has(id));
|
|
@@ -3564,6 +3517,7 @@ function installAction(options, deps = {}) {
|
|
|
3564
3517
|
const spec = {
|
|
3565
3518
|
tracks: options.track ?? [],
|
|
3566
3519
|
...userOverride ? { userOverride } : {},
|
|
3520
|
+
...baselineExclude.length > 0 ? { baselineExclude } : {},
|
|
3567
3521
|
// v26.81.0 (ADR-022, BREAKING) — 자산 1:1 boolean 13종 삭제. 자산 선택은 위
|
|
3568
3522
|
// userOverride(--with <id>)로 일원화. 잔존 = 설치 동작 옵션만.
|
|
3569
3523
|
options: {
|
|
@@ -3761,7 +3715,7 @@ function registerListCommand(cli2) {
|
|
|
3761
3715
|
// src/commands/uninstall.ts
|
|
3762
3716
|
init_esm_shims();
|
|
3763
3717
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3764
|
-
import { existsSync as existsSync15, readFileSync as readFileSync14, rmSync
|
|
3718
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14, rmSync, writeFileSync as writeFileSync7 } from "fs";
|
|
3765
3719
|
import { join as join13, resolve as resolve4 } from "path";
|
|
3766
3720
|
|
|
3767
3721
|
// src/uninstall-interactive.ts
|
|
@@ -5163,7 +5117,7 @@ function stripRootImport(projectDir) {
|
|
|
5163
5117
|
const stripped = current === null ? null : stripHarnessImport(current);
|
|
5164
5118
|
if (stripped === null) return false;
|
|
5165
5119
|
try {
|
|
5166
|
-
|
|
5120
|
+
writeFileSync7(join13(projectDir, "CLAUDE.md"), stripped, "utf8");
|
|
5167
5121
|
return true;
|
|
5168
5122
|
} catch {
|
|
5169
5123
|
return false;
|
|
@@ -5200,7 +5154,7 @@ function defaultSpawn2(cmd, args) {
|
|
|
5200
5154
|
}
|
|
5201
5155
|
function defaultRm(path) {
|
|
5202
5156
|
if (existsSync15(path)) {
|
|
5203
|
-
|
|
5157
|
+
rmSync(path, { recursive: true, force: true });
|
|
5204
5158
|
}
|
|
5205
5159
|
}
|
|
5206
5160
|
function registerUninstallCommand(cli2) {
|
|
@@ -5428,6 +5382,18 @@ var CLI_BASE_LABELS = {
|
|
|
5428
5382
|
antigravity: "Antigravity (Google)"
|
|
5429
5383
|
};
|
|
5430
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
|
+
},
|
|
5431
5397
|
{ label: "Dev Core (Frontend \xB7 Backend \xB7 Data)", cats: ["frontend", "backend", "data"] },
|
|
5432
5398
|
{ label: "Dev Tools (Security \xB7 Quality \xB7 Understanding)", cats: ["dev-tools", "understanding"] },
|
|
5433
5399
|
{ label: "Business (PM \xB7 Executive \xB7 Documents)", cats: ["business"] },
|
|
@@ -5448,10 +5414,25 @@ function expandDevMethodBundle(ids) {
|
|
|
5448
5414
|
const rest = ids.filter((v2) => v2 !== DEV_METHOD_BUNDLE_VALUE);
|
|
5449
5415
|
return ids.includes(DEV_METHOD_BUNDLE_VALUE) ? [...rest, ...bundleMemberValues()] : rest;
|
|
5450
5416
|
}
|
|
5451
|
-
|
|
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 = []) {
|
|
5452
5424
|
const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
|
|
5453
5425
|
const groups = {};
|
|
5454
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
|
+
}
|
|
5455
5436
|
for (const cat of cats) {
|
|
5456
5437
|
const items = [];
|
|
5457
5438
|
for (const o of VISIBLE_OPTION_DEFS.filter((d2) => d2.category === cat)) {
|
|
@@ -5586,8 +5567,26 @@ Proceed?`,
|
|
|
5586
5567
|
});
|
|
5587
5568
|
return q(result) ? null : result;
|
|
5588
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
|
+
},
|
|
5589
5587
|
selectInstallTargets: async (initialChecked, step, recap) => {
|
|
5590
5588
|
const pages = INSTALL_TARGET_PAGES;
|
|
5589
|
+
const baselineTargets = listBaselineTargets({ tracks: recap?.tracks ?? [] });
|
|
5591
5590
|
const displayInitial = collapseDevMethodBundle(initialChecked);
|
|
5592
5591
|
const initialSet = new Set(displayInitial);
|
|
5593
5592
|
const collected = new Set(displayInitial);
|
|
@@ -5600,10 +5599,25 @@ Proceed?`,
|
|
|
5600
5599
|
let aborted = false;
|
|
5601
5600
|
try {
|
|
5602
5601
|
let pageIdx = 0;
|
|
5602
|
+
let dir = 1;
|
|
5603
5603
|
while (pageIdx < pages.length) {
|
|
5604
5604
|
const page = pages[pageIdx];
|
|
5605
5605
|
if (!page) break;
|
|
5606
|
-
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
|
+
}
|
|
5607
5621
|
const selectedNow = flatItems.filter((it3) => collected.has(it3.value)).map((it3) => it3.value);
|
|
5608
5622
|
const pageDefault = flatItems.filter((it3) => initialSet.has(it3.value)).length;
|
|
5609
5623
|
const totalSelected = collected.size;
|
|
@@ -5629,11 +5643,13 @@ Proceed?`,
|
|
|
5629
5643
|
aborted = true;
|
|
5630
5644
|
break;
|
|
5631
5645
|
}
|
|
5646
|
+
dir = -1;
|
|
5632
5647
|
pageIdx--;
|
|
5633
5648
|
continue;
|
|
5634
5649
|
}
|
|
5635
5650
|
for (const it3 of flatItems) collected.delete(it3.value);
|
|
5636
5651
|
for (const v2 of result) collected.add(v2);
|
|
5652
|
+
dir = 1;
|
|
5637
5653
|
pageIdx++;
|
|
5638
5654
|
}
|
|
5639
5655
|
if (!aborted) {
|
|
@@ -5657,14 +5673,21 @@ Proceed?`,
|
|
|
5657
5673
|
function splitInstallTargets(targets) {
|
|
5658
5674
|
const optionKeys = [];
|
|
5659
5675
|
const assetIds = [];
|
|
5676
|
+
const baselineIds = [];
|
|
5660
5677
|
for (const t of targets) {
|
|
5661
5678
|
if (t.startsWith("option:")) {
|
|
5662
5679
|
optionKeys.push(t.slice("option:".length));
|
|
5663
5680
|
} else if (t.startsWith("asset:")) {
|
|
5664
5681
|
assetIds.push(t.slice("asset:".length));
|
|
5682
|
+
} else if (t.startsWith(BASELINE_PREFIX)) {
|
|
5683
|
+
baselineIds.push(t);
|
|
5665
5684
|
}
|
|
5666
5685
|
}
|
|
5667
|
-
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);
|
|
5668
5691
|
}
|
|
5669
5692
|
function toOptionFlags(keys) {
|
|
5670
5693
|
const picked = new Set(keys);
|
|
@@ -5684,7 +5707,9 @@ function installedTargetState(projectDir) {
|
|
|
5684
5707
|
function initialTargetSelection(tracks, installedProjectAssetIds) {
|
|
5685
5708
|
const ids = new Set(recommendedExternalAssets(tracks));
|
|
5686
5709
|
for (const id of installedProjectAssetIds) ids.add(id);
|
|
5687
|
-
|
|
5710
|
+
const assets = [...ids].map((id) => `asset:${id}`);
|
|
5711
|
+
const baseline = listBaselineTargets({ tracks }).map((t) => t.id);
|
|
5712
|
+
return [...assets, ...baseline];
|
|
5688
5713
|
}
|
|
5689
5714
|
async function runInteractive(projectDir, deps = {}) {
|
|
5690
5715
|
const prompts = deps.prompts ?? defaultPrompts;
|
|
@@ -5786,16 +5811,21 @@ ${formatSummary(spec)}`
|
|
|
5786
5811
|
} else {
|
|
5787
5812
|
const finalTracks = tracks;
|
|
5788
5813
|
const finalCli = cli2;
|
|
5789
|
-
const { optionKeys, assetIds } = splitInstallTargets(targetSelections ?? []);
|
|
5814
|
+
const { optionKeys, assetIds, baselineIds } = splitInstallTargets(targetSelections ?? []);
|
|
5790
5815
|
const options = toOptionFlags(optionKeys);
|
|
5791
5816
|
const userOverride = targetSelections === null ? void 0 : computeUserOverride(finalTracks, assetIds);
|
|
5817
|
+
const baselineExclude = targetSelections === null ? [] : baselineExcludeFrom(listBaselineTargets({ tracks: finalTracks }), baselineIds);
|
|
5792
5818
|
const scopeLabel = scope === "global" ? "Global (writes to ~/.claude/, ~/.codex/, npm -g)" : "Project (current directory only)";
|
|
5793
5819
|
const summary = `${formatSummary({
|
|
5794
5820
|
tracks: finalTracks,
|
|
5795
5821
|
options,
|
|
5796
5822
|
cli: finalCli,
|
|
5797
5823
|
projectDir,
|
|
5798
|
-
...userOverride ? { userOverride } : {}
|
|
5824
|
+
...userOverride ? { userOverride } : {},
|
|
5825
|
+
// 해제 목록을 안 넘기면 이 화면이 **제외 유무와 문자열이 완전히 같아진다** — 같은 화면이
|
|
5826
|
+
// 외부 자산 제거는 `-User removed:` 로 이미 보고하므로, 없음은 "아무것도 안 빠졌다"로
|
|
5827
|
+
// 읽힌다. 상주 비용을 줄이려고 20개를 푼 사용자가 그대로인 숫자를 보게 된다.
|
|
5828
|
+
...baselineExclude.length > 0 ? { baselineExclude } : {}
|
|
5799
5829
|
})}
|
|
5800
5830
|
SCOPE ${scopeLabel}`;
|
|
5801
5831
|
const confirmed = await prompts.confirmInstall(
|
|
@@ -5810,22 +5840,59 @@ ${summary}`
|
|
|
5810
5840
|
prompts.outro("Cancelled by user.");
|
|
5811
5841
|
return { ok: false, reason: "cancelled" };
|
|
5812
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
|
+
}
|
|
5813
5870
|
prompts.outro(stepLabel(WIZARD.INSTALL, "Installing..."));
|
|
5814
5871
|
return {
|
|
5815
5872
|
ok: true,
|
|
5816
5873
|
mode,
|
|
5817
|
-
spec: {
|
|
5818
|
-
tracks: finalTracks,
|
|
5819
|
-
options,
|
|
5820
|
-
cli: finalCli,
|
|
5821
|
-
projectDir,
|
|
5822
|
-
scope,
|
|
5823
|
-
...userOverride ? { userOverride } : {}
|
|
5824
|
-
}
|
|
5874
|
+
spec: { ...spec, ...cleanSuperseded ? { cleanSuperseded } : {} }
|
|
5825
5875
|
};
|
|
5826
5876
|
}
|
|
5827
5877
|
}
|
|
5828
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
|
+
}
|
|
5829
5896
|
function tracksEqual(a, b2) {
|
|
5830
5897
|
if (a.length !== b2.length) return false;
|
|
5831
5898
|
const sortedA = [...a].sort();
|
|
@@ -5841,6 +5908,7 @@ function computeUserOverride(tracks, assetIds) {
|
|
|
5841
5908
|
return { forceInclude, forceExclude };
|
|
5842
5909
|
}
|
|
5843
5910
|
function formatSummary(spec) {
|
|
5911
|
+
const baselineExcluded = new Set(spec.baselineExclude ?? []);
|
|
5844
5912
|
const opts = Object.keys(spec.options).filter((k2) => spec.options[k2]).map((k2) => k2.replace(/^with/, "").toLowerCase());
|
|
5845
5913
|
const optsLabel = opts.length > 0 ? opts.join(", ") : "(none added)";
|
|
5846
5914
|
const lines = [
|
|
@@ -5862,7 +5930,11 @@ function formatSummary(spec) {
|
|
|
5862
5930
|
lines.push(` \xB7 ${cat}: ${ids.join(", ")}`);
|
|
5863
5931
|
}
|
|
5864
5932
|
const cost = formatResidentCostLine(
|
|
5865
|
-
residentCost(
|
|
5933
|
+
residentCost(
|
|
5934
|
+
buildManifest(spec).filter(
|
|
5935
|
+
(e2) => e2.applies(spec) && !isBaselineExcluded(e2.target, baselineExcluded)
|
|
5936
|
+
)
|
|
5937
|
+
),
|
|
5866
5938
|
summarizeContextCost(finalAssets).unmeasuredCount
|
|
5867
5939
|
);
|
|
5868
5940
|
if (cost) lines.push(` \xB7 ${cost}`);
|
|
@@ -5875,6 +5947,11 @@ function formatSummary(spec) {
|
|
|
5875
5947
|
lines.push(` -User removed: ${spec.userOverride.forceExclude.join(", ")}`);
|
|
5876
5948
|
}
|
|
5877
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
|
+
}
|
|
5878
5955
|
return lines.join("\n");
|
|
5879
5956
|
}
|
|
5880
5957
|
|