@uzysjung/agent-harness 26.162.1 → 26.164.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +167 -80
- package/README.md +153 -73
- package/dist/{chunk-3G34MEGZ.js → chunk-X7NSQHXL.js} +1027 -512
- package/dist/chunk-X7NSQHXL.js.map +1 -0
- package/dist/index.js +6372 -4040
- package/dist/index.js.map +1 -1
- package/dist/trust-tier-drift.js +4 -2
- package/dist/trust-tier-drift.js.map +1 -1
- package/package.json +2 -2
- package/templates/codex/README.md +2 -2
- package/templates/codex/config.toml.template +7 -3
- package/templates/hooks/protect-files.sh +8 -3
- package/templates/hooks/session-start.sh +15 -4
- package/templates/scripts/protect-branch.sh +2 -1
- package/templates/scripts/spec-drift-check.sh +19 -2
- package/templates/settings.json +1 -1
- package/templates/track-mcp-map.tsv +3 -2
- package/dist/chunk-3G34MEGZ.js.map +0 -1
|
@@ -103,7 +103,7 @@ function globToRegex(pattern) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// src/external-assets.ts
|
|
106
|
-
var
|
|
106
|
+
var UI_TRACKS = TRACKS.filter((t) => hasUiTrack([t]));
|
|
107
107
|
var CSR_SSR_NEXTJS_FULL = [
|
|
108
108
|
"csr-supabase",
|
|
109
109
|
"csr-fastify",
|
|
@@ -156,6 +156,22 @@ var EXTERNAL_ASSETS = [
|
|
|
156
156
|
// === Repo-bundled internal skills (uzys 1st-party, v26.87.0) ===
|
|
157
157
|
// 2026-08-02 복원 (ADR-062) — 이관(ADR-060) 후 감사에서 본문 열화 104건(dropped 76 ·
|
|
158
158
|
// damaged 28)이 확인돼 9종을 이 리포 번들로 되돌렸다. 아래 10종 전부 `kind: "internal"`.
|
|
159
|
+
// #602 — ui-visual-review: TRACKS.md 가 말하는 트랙 추종 번들 스킬("follows the track, not
|
|
160
|
+
// the catalog"). internal 카탈로그 자산으로 등록해 selectedInternalSkills 흐름에 태우면
|
|
161
|
+
// .agents/skills(비-Claude 자리)에도 깔린다 — 종전의 .claude/ 전용 배선과 어긋나던 것.
|
|
162
|
+
{
|
|
163
|
+
id: "ui-visual-review",
|
|
164
|
+
tier: "official",
|
|
165
|
+
// uzys 자사 스킬
|
|
166
|
+
description: "UI visual review \u2014 screenshot capture, baseline diff, and a review gate for UI changes (ships with UI tracks; ssr-htmx excluded)",
|
|
167
|
+
category: "frontend",
|
|
168
|
+
source: "uzys",
|
|
169
|
+
condition: {
|
|
170
|
+
kind: "any-track",
|
|
171
|
+
tracks: ["csr-supabase", "csr-fastify", "csr-fastapi", "ssr-nextjs", "full"]
|
|
172
|
+
},
|
|
173
|
+
method: { kind: "internal", key: "ui-visual-review" }
|
|
174
|
+
},
|
|
159
175
|
{
|
|
160
176
|
id: "compaction-handoff",
|
|
161
177
|
tier: "official",
|
|
@@ -379,15 +395,34 @@ var EXTERNAL_ASSETS = [
|
|
|
379
395
|
pluginId: "railway@railway-skills"
|
|
380
396
|
}
|
|
381
397
|
},
|
|
398
|
+
// #709 (ADR-101) — Railway MCP 서버의 **선택** 경로. 서버 정의는 트랙 표(`templates/track-mcp-map.tsv`)의 같은 이름
|
|
399
|
+
// 행 하나다(key === 행 이름). 그 행의 패턴이 기본 트랙(csr-* · ssr-htmx · full)을 정하고, 이 항목은 그 밖의 트랙
|
|
400
|
+
// (ssr-nextjs 등)에서 위저드 체크 · `--with` 로 고르게 한다. experimental 이라 조건으로는 미리 체크되지 않는다.
|
|
401
|
+
// 상류 repo 는 archived — Railway 의 현행 경로(`railway mcp`)로 행 명령을 바꾸는 일은 별도 이슈.
|
|
402
|
+
{
|
|
403
|
+
id: "railway-mcp-server",
|
|
404
|
+
tier: "experimental",
|
|
405
|
+
// railwayapp/railway-mcp-server 190★ · archived (월간 trust-tier-drift 가 REPO_OVERRIDE 로 감시)
|
|
406
|
+
description: "Railway MCP server (`npx -y @railway/mcp-server`) in `.mcp.json` \u2014 already on by default on csr-*, ssr-htmx and full; check it to add it on any other track (e.g. ssr-nextjs). Upstream repo is archived; Railway's current route is `railway mcp` (opt-in)",
|
|
407
|
+
category: "backend",
|
|
408
|
+
source: "railwayapp",
|
|
409
|
+
condition: { kind: "opt-in" },
|
|
410
|
+
// #709 리뷰 NOTE 3 — `.mcp.json` · Codex `[mcp_servers]` · `opencode.json` `mcp` 만 받는다. Antigravity 변환에는 MCP 가 없다
|
|
411
|
+
// (internal 기본값 = 4 CLI 는 과대 표기).
|
|
412
|
+
cliSupportOverride: ["claude", "codex", "opencode"],
|
|
413
|
+
method: { kind: "internal", key: "railway-mcp-server" }
|
|
414
|
+
},
|
|
382
415
|
// === csr-supabase|full CLI ===
|
|
383
416
|
{
|
|
384
417
|
id: "vercel-cli",
|
|
385
418
|
tier: "vetted",
|
|
386
419
|
// vercel/vercel 15k
|
|
387
|
-
description: "Vercel CLI \u2014 the real deploy tool as a devDependency (global under --scope global). Only if this project deploys to Vercel",
|
|
420
|
+
description: "Vercel CLI \u2014 the real deploy tool as a devDependency (global under --scope global). Only if this project deploys to Vercel. `vercel mcp` then wires Vercel's official MCP (OAuth) into Claude Code",
|
|
388
421
|
category: "backend",
|
|
389
422
|
source: "vercel",
|
|
390
|
-
// 2026-08-02 사용자 결정: 트랙 기본 → opt-in (ADR-063)
|
|
423
|
+
// 2026-08-02 사용자 결정: 트랙 기본 → opt-in (ADR-063). #709 가 ssr-nextjs 기본으로 바꿨다가 #715 에서 되돌렸다 —
|
|
424
|
+
// 설치자 package.json 에 의존성을 더하고(에이전트는 npx 로 쓸 수 있다) login 없이는 쓸 것이 없으며 핀이 낡는다.
|
|
425
|
+
// Next.js 의 Vercel 기본은 공식 MCP(#711)로 채운다.
|
|
391
426
|
condition: { kind: "opt-in" },
|
|
392
427
|
method: { kind: "npm", pkg: "vercel", version: "54.17.3" }
|
|
393
428
|
},
|
|
@@ -417,6 +452,7 @@ var EXTERNAL_ASSETS = [
|
|
|
417
452
|
// === UI tracks (csr-*|ssr-*|full) ===
|
|
418
453
|
// v26.92.0 — frontend-design (Anthropic official).
|
|
419
454
|
// 사용자 결정: has-dev-track 기본추천 (모든 개발 트랙, executive 제외).
|
|
455
|
+
// #709 (ADR-101) — UI 가 있는 트랙만(`UI_TRACKS`). data · tooling 은 미리 체크하지 않는다.
|
|
420
456
|
// category=frontend (UI 자산, wizard 그룹).
|
|
421
457
|
//
|
|
422
458
|
// 2026-08-26 (#344) — **plugin → skill.** 배달 방식이 도달 CLI 를 정한다(assetCliSupport):
|
|
@@ -432,7 +468,7 @@ var EXTERNAL_ASSETS = [
|
|
|
432
468
|
description: "frontend-design \u2014 visual direction, typography, and anti-generic UI guidance (Anthropic official). For new UI or an explicit redesign; do not let it override an existing design system",
|
|
433
469
|
category: "frontend",
|
|
434
470
|
source: "anthropics",
|
|
435
|
-
condition: { kind: "any-track", tracks:
|
|
471
|
+
condition: { kind: "any-track", tracks: UI_TRACKS },
|
|
436
472
|
method: { kind: "skill", source: "anthropics/skills", skill: "frontend-design" }
|
|
437
473
|
},
|
|
438
474
|
// 2026-08-02 정비 — 프론트엔드 품질 3종 (사용자 지시). 전부 opt-in: frontend-design 이 기본
|
|
@@ -484,9 +520,11 @@ var EXTERNAL_ASSETS = [
|
|
|
484
520
|
// found" 가 됐다(#420) — 지금은 SKILL.md 의 `name:`(`preline-theme-generator`)으로 맞춘다(실측
|
|
485
521
|
// 2026-09-20: 옛 이름 exit 1 · 새 이름 설치 완료). 이름이 바뀌면 월 1회 catalog-verify 가 잡는다.
|
|
486
522
|
//
|
|
487
|
-
// 같은 날 조사한 flowbite 는 **넣지 않는다**: 에이전트용 제공물이 MCP 서버뿐인데, 이 저장소의
|
|
488
|
-
// `.mcp.json` 조립은 트랙 조건만 읽어 opt-in 경로가
|
|
523
|
+
// 같은 날 조사한 flowbite 는 **넣지 않는다**: 에이전트용 제공물이 MCP 서버뿐인데, 그때 이 저장소의
|
|
524
|
+
// `.mcp.json` 조립은 트랙 조건만 읽어 opt-in 경로가 없었다 — 넣으면 해당 트랙 전원에게 항상
|
|
489
525
|
// 켜진다. 안 쓰는 사람에게 MCP 툴 스키마만큼의 상주 비용을 물리는 형태라 기각(사용자 확정).
|
|
526
|
+
// #709 (ADR-101) 부터는 카탈로그 `internal` 항목으로 opt-in 경로가 있다(`railway-mcp-server`). flowbite 는
|
|
527
|
+
// 그 경로로 다시 심사할 수 있다 — 이 변경은 넣지 않는다.
|
|
490
528
|
{
|
|
491
529
|
id: "preline",
|
|
492
530
|
tier: "vetted",
|
|
@@ -870,6 +908,10 @@ var DEV_METHOD_SKILL_IDS = [
|
|
|
870
908
|
];
|
|
871
909
|
var INTERNAL_BUNDLED_SKILL_IDS = [
|
|
872
910
|
...DEV_METHOD_SKILL_IDS,
|
|
911
|
+
// #602 — 트랙을 따라가는 번들 스킬(csr-*·ssr-*·full). selectedInternalSkills 로 흘러야
|
|
912
|
+
// .agents/skills(비-Claude 자리)에도 깔린다 — manifest 의 UI_SKILL_DIRS 배선은 .claude/ 만
|
|
913
|
+
// 알아 TRACKS.md 와 어긋났다. 조건은 buildAssetSpec 이 UI 트랙으로 건다.
|
|
914
|
+
"ui-visual-review",
|
|
873
915
|
"north-star",
|
|
874
916
|
"gh-issue-workflow",
|
|
875
917
|
"objective-brief",
|
|
@@ -975,470 +1017,265 @@ function filterApplicableAssets(assets, ctx) {
|
|
|
975
1017
|
return assets.filter((a) => shouldInstallAsset(a, ctx));
|
|
976
1018
|
}
|
|
977
1019
|
|
|
978
|
-
// src/
|
|
1020
|
+
// src/manifest.ts
|
|
979
1021
|
init_esm_shims();
|
|
980
|
-
import { existsSync, readFileSync } from "fs";
|
|
981
|
-
import { join, resolve } from "path";
|
|
982
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
983
1022
|
|
|
984
|
-
// src/
|
|
1023
|
+
// src/install-log.ts
|
|
985
1024
|
init_esm_shims();
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
var IMPORT_MARKER_END = "<!-- uzys-harness:import:end -->";
|
|
990
|
-
var CONTINUOUS_SKILLS_HEADING = "## Skills that apply continuously";
|
|
991
|
-
function renderContinuousSkillsNote(selectedInternalSkills) {
|
|
992
|
-
const lines = CONTINUOUS_SKILLS.filter((s) => selectedInternalSkills.includes(s.id)).map(
|
|
993
|
-
(s) => `- \`${s.id}\` \u2014 ${s.whenToApply}.`
|
|
994
|
-
);
|
|
995
|
-
if (lines.length === 0) return "";
|
|
996
|
-
return [
|
|
997
|
-
CONTINUOUS_SKILLS_HEADING,
|
|
998
|
-
"",
|
|
999
|
-
"These installed skills apply to every response or delegation; no prompt ever looks like their job, so open them from this list:",
|
|
1000
|
-
"",
|
|
1001
|
-
...lines
|
|
1002
|
-
].join("\n");
|
|
1003
|
-
}
|
|
1004
|
-
function withContinuousSkillsNote(scaffold, selectedInternalSkills) {
|
|
1005
|
-
const note = renderContinuousSkillsNote(selectedInternalSkills);
|
|
1006
|
-
return note ? `${scaffold}
|
|
1025
|
+
import { createHash as createHash2 } from "crypto";
|
|
1026
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, statSync, writeFileSync } from "fs";
|
|
1027
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
1007
1028
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
${body}
|
|
1017
|
-
${IMPORT_MARKER_END}`;
|
|
1018
|
-
}
|
|
1019
|
-
var TRACK_DISPLAY_NAMES = {
|
|
1020
|
-
base: "Base",
|
|
1021
|
-
tooling: "Tooling",
|
|
1022
|
-
"csr-supabase": "CSR Supabase",
|
|
1023
|
-
"csr-fastify": "CSR Fastify",
|
|
1024
|
-
"csr-fastapi": "CSR FastAPI",
|
|
1025
|
-
"ssr-htmx": "SSR HTMX",
|
|
1026
|
-
"ssr-nextjs": "SSR Next.js",
|
|
1027
|
-
data: "Data",
|
|
1028
|
-
executive: "Executive",
|
|
1029
|
-
full: "Full",
|
|
1030
|
-
"project-management": "Project Management",
|
|
1031
|
-
"growth-marketing": "Growth Marketing"
|
|
1032
|
-
};
|
|
1033
|
-
var FULL_EXPANSION = TRACKS.filter((t) => t !== "full");
|
|
1034
|
-
var FILL_SECTIONS = [
|
|
1035
|
-
"identity",
|
|
1036
|
-
"stack",
|
|
1037
|
-
"architecture",
|
|
1038
|
-
"installed-assets",
|
|
1039
|
-
"boundaries",
|
|
1040
|
-
"verify"
|
|
1041
|
-
];
|
|
1042
|
-
var FILL_SPECS = {
|
|
1043
|
-
identity: {
|
|
1044
|
-
title: "Identity & Purpose",
|
|
1045
|
-
prompt: `Replace the H1 title above with this project's real name, then state in 1-2 plain sentences what it does, who uses it, and why it exists. Sources: README.md, the package.json / pyproject.toml "description", docs/. Do NOT describe the harness itself. Delete this comment when done.`,
|
|
1046
|
-
placeholder: "what this project is, who it is for, and why it exists"
|
|
1029
|
+
// package.json
|
|
1030
|
+
var package_default = {
|
|
1031
|
+
name: "@uzysjung/agent-harness",
|
|
1032
|
+
version: "26.164.0",
|
|
1033
|
+
description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
|
|
1034
|
+
type: "module",
|
|
1035
|
+
publishConfig: {
|
|
1036
|
+
access: "public"
|
|
1047
1037
|
},
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
prompt: "Replace the list below with this project's REAL stack. Inspect package.json / pyproject.toml / go.mod / Cargo.toml / Gemfile + lockfiles and any Makefile/justfile/package scripts. List the language(s)+versions, framework(s), package manager, and the exact install/build/test/run commands. Verify each command exists before writing it \u2014 never guess. Delete lines that do not apply, then delete this comment.",
|
|
1051
|
-
placeholder: "languages, runtimes, package manager, and the install/build/test/run commands"
|
|
1038
|
+
engines: {
|
|
1039
|
+
node: ">=20.12.0"
|
|
1052
1040
|
},
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
prompt: "Map this repository. List each top-level source directory and what it holds, the entry point(s), the 3-5 files a newcomer reads first, and how data/requests flow between the main layers. View the real tree (e.g. `git ls-files | sed 's#/.*##' | sort -u`) \u2014 do not assume a framework's conventional layout. Flag any generated/vendored directories that must never be hand-edited. If this is a single small module, say so in one line. Delete this comment when done.",
|
|
1056
|
-
placeholder: "where things live, the entry points, and how data flows between layers"
|
|
1041
|
+
bin: {
|
|
1042
|
+
"agent-harness": "./dist/index.js"
|
|
1057
1043
|
},
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1044
|
+
files: [
|
|
1045
|
+
"dist",
|
|
1046
|
+
"templates",
|
|
1047
|
+
"README.md",
|
|
1048
|
+
"LICENSE"
|
|
1049
|
+
],
|
|
1050
|
+
scripts: {
|
|
1051
|
+
build: "tsup",
|
|
1052
|
+
dev: "tsup --watch",
|
|
1053
|
+
"gen:compat": "npm run build && node scripts/gen-compatibility.mjs",
|
|
1054
|
+
test: "vitest run",
|
|
1055
|
+
"test:watch": "vitest",
|
|
1056
|
+
"test:coverage": "vitest run --coverage",
|
|
1057
|
+
typecheck: "tsc --noEmit",
|
|
1058
|
+
lint: "biome check src tests",
|
|
1059
|
+
"lint:fix": "biome check --write src tests",
|
|
1060
|
+
format: "biome format --write src tests",
|
|
1061
|
+
ci: "npm run typecheck && npm run lint && npm run test:coverage && npm run build",
|
|
1062
|
+
prepare: "[ -d dist ] || npm run build",
|
|
1063
|
+
"cost:report": "npm run build && node scripts/context-cost-report.mjs",
|
|
1064
|
+
"cost:baseline": "npm run build && node scripts/context-cost-baseline.mjs",
|
|
1065
|
+
"assets:history": "bash scripts/asset-history.sh",
|
|
1066
|
+
"release:audit": "node scripts/release-audit.mjs",
|
|
1067
|
+
security: "npm audit --omit=dev --audit-level=high && npx --yes ecc-agentshield@1.4.0 scan --baseline security-baseline.json --gate",
|
|
1068
|
+
"security:baseline": "npx --yes ecc-agentshield@1.4.0 scan --save-baseline security-baseline.json"
|
|
1062
1069
|
},
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1070
|
+
dependencies: {
|
|
1071
|
+
"@clack/prompts": "^1.3.0",
|
|
1072
|
+
cac: "^7.0.0",
|
|
1073
|
+
"smol-toml": "^1.9.0"
|
|
1067
1074
|
},
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1075
|
+
devDependencies: {
|
|
1076
|
+
"@biomejs/biome": "^2.4.13",
|
|
1077
|
+
"@types/node": "^25.6.0",
|
|
1078
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
1079
|
+
tsup: "^8.3.0",
|
|
1080
|
+
typescript: "^5.6.0",
|
|
1081
|
+
vitest: "^4.1.11"
|
|
1082
|
+
},
|
|
1083
|
+
license: "MIT",
|
|
1084
|
+
repository: {
|
|
1085
|
+
type: "git",
|
|
1086
|
+
url: "git+https://github.com/uzysjung/uzys-agent-harness.git"
|
|
1072
1087
|
}
|
|
1073
1088
|
};
|
|
1074
|
-
function anchorLine(surface) {
|
|
1075
|
-
if (surface === "claude") {
|
|
1076
|
-
return `> The working principles live in this project's harness anchor \`${HARNESS_ANCHOR_FILE}\`, imported at the bottom of this file. Everything above that import is **project-specific context only**.`;
|
|
1077
|
-
}
|
|
1078
|
-
const self = surface === "antigravity-rule" ? "`.agents/rules/uzys-harness.md`" : "`AGENTS.md`";
|
|
1079
|
-
return `> **This file (${self}) is the harness anchor** \u2014 it carries the working principles in full, further down. Only the \`## \u2026\` sections immediately below are project-specific context for you to fill in.`;
|
|
1080
|
-
}
|
|
1081
|
-
function scaffoldBanner(surface) {
|
|
1082
|
-
return [
|
|
1083
|
-
"> \u2699\uFE0F **SCAFFOLD \u2014 not filled in yet.** The sections below are a fill-in template for THIS project, not verified facts.",
|
|
1084
|
-
"> To fill: open this file and paste each `<!-- FILL: \u2026 -->` comment's instruction into your coding agent (e.g. Claude Code) \u2014 it will inspect the real repo and write the section. You can also fill them by hand; the comments are the instructions.",
|
|
1085
|
-
anchorLine(surface)
|
|
1086
|
-
].join("\n");
|
|
1087
|
-
}
|
|
1088
|
-
function renderFillScaffold(surface = "claude") {
|
|
1089
|
-
const blocks = FILL_SECTIONS.map((id) => {
|
|
1090
|
-
const spec = FILL_SPECS[id];
|
|
1091
|
-
return `## ${spec.title}
|
|
1092
|
-
|
|
1093
|
-
<!-- FILL:${id} \u2014 ${spec.prompt} -->
|
|
1094
1089
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1090
|
+
// src/adapters/shared-files.ts
|
|
1091
|
+
init_esm_shims();
|
|
1092
|
+
var SHARED_FILES = {
|
|
1093
|
+
"CLAUDE.md": { adapter: "marker-md", prefix: "claude-md:" },
|
|
1094
|
+
"AGENTS.md": { adapter: "marker-md", prefix: "agents-md:" },
|
|
1095
|
+
".claude/settings.json": { adapter: "json-keys", prefix: "settings:" },
|
|
1096
|
+
".mcp.json": { adapter: "json-keys", prefix: "mcp:", strip: "mcpServers." },
|
|
1097
|
+
"opencode.json": { adapter: "json-keys", prefix: "opencode:" },
|
|
1098
|
+
".codex/config.toml": { adapter: "toml-region", prefix: "codex:" },
|
|
1099
|
+
".gitignore": { adapter: "lines", prefix: "gitignore:" }
|
|
1100
|
+
};
|
|
1101
|
+
var KEY_ID_PREFIXES = Object.values(SHARED_FILES).map(
|
|
1102
|
+
(f) => f.prefix
|
|
1103
|
+
);
|
|
1104
|
+
function isKeyId(id) {
|
|
1105
|
+
return KEY_ID_PREFIXES.some((p) => id.startsWith(p));
|
|
1100
1106
|
}
|
|
1101
|
-
function
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
> Active track(s): ${trackList}`;
|
|
1107
|
-
return `${header}
|
|
1108
|
-
|
|
1109
|
-
${opts.projectContext ?? renderFillScaffold()}
|
|
1110
|
-
`;
|
|
1107
|
+
function sharedPathOfKeyId(id) {
|
|
1108
|
+
for (const [path2, file] of Object.entries(SHARED_FILES))
|
|
1109
|
+
if (id.startsWith(file.prefix)) return path2;
|
|
1110
|
+
return null;
|
|
1111
1111
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1112
|
+
|
|
1113
|
+
// src/cli-targets.ts
|
|
1114
|
+
init_esm_shims();
|
|
1115
|
+
var CLI_BASE_SORT_ORDER = {
|
|
1116
|
+
claude: 0,
|
|
1117
|
+
codex: 1,
|
|
1118
|
+
opencode: 2,
|
|
1119
|
+
antigravity: 3
|
|
1120
|
+
};
|
|
1121
|
+
function parseCliTargets(input) {
|
|
1122
|
+
const items = normalizeInput(input);
|
|
1123
|
+
if (items.length === 0) {
|
|
1124
|
+
return { ok: true, targets: ["claude"], warnings: [] };
|
|
1118
1125
|
}
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
if (
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
+
const collected = /* @__PURE__ */ new Set();
|
|
1127
|
+
const warnings = [];
|
|
1128
|
+
for (const item of items) {
|
|
1129
|
+
if (!isCliBase(item)) {
|
|
1130
|
+
let hint = "";
|
|
1131
|
+
if (item === "both") {
|
|
1132
|
+
hint = "\n v0.8.0 removed 'both' alias. Use --cli claude --cli codex.";
|
|
1133
|
+
} else if (item === "all") {
|
|
1134
|
+
hint = "\n v0.8.0 removed 'all' alias. Use --cli claude --cli codex --cli opencode.";
|
|
1135
|
+
} else if (item.includes(",")) {
|
|
1136
|
+
hint = "\n Tip: comma-separated not supported. Use --cli A --cli B for multiple.";
|
|
1137
|
+
}
|
|
1138
|
+
return {
|
|
1139
|
+
ok: false,
|
|
1140
|
+
targets: ["claude"],
|
|
1141
|
+
warnings,
|
|
1142
|
+
error: `Invalid --cli value: ${item}. Must be one of: ${CLI_BASES.join(" | ")}${hint}`
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
collected.add(item);
|
|
1126
1146
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1147
|
+
const targets = [...collected].sort((a, b) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b]);
|
|
1148
|
+
return { ok: true, targets, warnings };
|
|
1149
|
+
}
|
|
1150
|
+
function normalizeInput(input) {
|
|
1151
|
+
if (input === void 0 || input === null) return [];
|
|
1152
|
+
if (typeof input === "string") {
|
|
1153
|
+
const trimmed = input.trim();
|
|
1154
|
+
return trimmed === "" ? [] : [trimmed];
|
|
1129
1155
|
}
|
|
1130
|
-
|
|
1131
|
-
`;
|
|
1132
|
-
return `${body}
|
|
1133
|
-
${block}
|
|
1134
|
-
`;
|
|
1156
|
+
return input.filter((s) => typeof s === "string" && s.trim() !== "").map((s) => s.trim());
|
|
1135
1157
|
}
|
|
1136
|
-
function
|
|
1137
|
-
|
|
1138
|
-
if (start === -1) return null;
|
|
1139
|
-
const endAt = text.indexOf(IMPORT_MARKER_END, start);
|
|
1140
|
-
if (endAt === -1) return null;
|
|
1141
|
-
const before = text.slice(0, start).replace(/\n+$/, "\n");
|
|
1142
|
-
const after = text.slice(endAt + IMPORT_MARKER_END.length).replace(/^\n+/, "");
|
|
1143
|
-
return `${before}${after}`;
|
|
1158
|
+
function targetsInclude(targets, base) {
|
|
1159
|
+
return targets.includes(base);
|
|
1144
1160
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1161
|
+
|
|
1162
|
+
// src/fs-ops.ts
|
|
1163
|
+
init_esm_shims();
|
|
1164
|
+
import { createHash } from "crypto";
|
|
1165
|
+
import {
|
|
1166
|
+
copyFileSync,
|
|
1167
|
+
existsSync,
|
|
1168
|
+
constants as fsConstants,
|
|
1169
|
+
mkdirSync,
|
|
1170
|
+
readdirSync,
|
|
1171
|
+
readFileSync,
|
|
1172
|
+
readlinkSync as readlinkSync2,
|
|
1173
|
+
realpathSync as realpathSync2,
|
|
1174
|
+
renameSync,
|
|
1175
|
+
rmdirSync,
|
|
1176
|
+
rmSync,
|
|
1177
|
+
symlinkSync
|
|
1178
|
+
} from "fs";
|
|
1179
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1180
|
+
|
|
1181
|
+
// src/outside-project.ts
|
|
1182
|
+
init_esm_shims();
|
|
1183
|
+
import { lstatSync, readlinkSync, realpathSync } from "fs";
|
|
1184
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
1185
|
+
function isOutsideProject(projectDir, target) {
|
|
1186
|
+
const root = resolveWriteTarget(projectDir) ?? projectDir;
|
|
1187
|
+
const rel = relative(root, target);
|
|
1188
|
+
return rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
|
1189
|
+
}
|
|
1190
|
+
function resolveWriteTarget(abs) {
|
|
1191
|
+
const tail = [];
|
|
1192
|
+
const hops = /* @__PURE__ */ new Set();
|
|
1193
|
+
let path2 = abs;
|
|
1194
|
+
for (; ; ) {
|
|
1195
|
+
try {
|
|
1196
|
+
return join(realpathSync(path2), ...tail);
|
|
1197
|
+
} catch {
|
|
1198
|
+
}
|
|
1199
|
+
let isLink = false;
|
|
1200
|
+
try {
|
|
1201
|
+
const st = lstatSync(path2, { throwIfNoEntry: false });
|
|
1202
|
+
if (st !== void 0 && !st.isSymbolicLink()) return null;
|
|
1203
|
+
isLink = st !== void 0;
|
|
1204
|
+
} catch {
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
if (isLink) {
|
|
1208
|
+
if (hops.has(path2)) return null;
|
|
1209
|
+
hops.add(path2);
|
|
1210
|
+
path2 = resolve(dirname(path2), readlinkSync(path2));
|
|
1151
1211
|
continue;
|
|
1152
1212
|
}
|
|
1153
|
-
|
|
1213
|
+
const parent = dirname(path2);
|
|
1214
|
+
if (parent === path2) return null;
|
|
1215
|
+
tail.unshift(basename(path2));
|
|
1216
|
+
path2 = parent;
|
|
1154
1217
|
}
|
|
1155
|
-
return false;
|
|
1156
1218
|
}
|
|
1157
|
-
function
|
|
1158
|
-
|
|
1159
|
-
|
|
1219
|
+
function outsideProjectTarget(projectDir, abs) {
|
|
1220
|
+
const target = resolveWriteTarget(abs);
|
|
1221
|
+
if (target === null || !isOutsideProject(projectDir, target)) return null;
|
|
1222
|
+
const rel = relative(projectDir, abs);
|
|
1223
|
+
const parts = rel.split(sep);
|
|
1224
|
+
let link = rel;
|
|
1225
|
+
let linkTarget = target;
|
|
1226
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
1227
|
+
const prefix = join(projectDir, ...parts.slice(0, i));
|
|
1228
|
+
const st = lstatSync(prefix, { throwIfNoEntry: false });
|
|
1229
|
+
if (st?.isSymbolicLink() !== true) continue;
|
|
1230
|
+
const resolved = resolveWriteTarget(prefix);
|
|
1231
|
+
if (resolved === null || !isOutsideProject(projectDir, resolved)) continue;
|
|
1232
|
+
link = parts.slice(0, i).join(sep);
|
|
1233
|
+
linkTarget = resolved;
|
|
1234
|
+
break;
|
|
1160
1235
|
}
|
|
1161
|
-
|
|
1236
|
+
const slash = (p) => p.split(sep).join("/");
|
|
1237
|
+
return { path: slash(rel), target, link: slash(link), linkTarget };
|
|
1162
1238
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
return
|
|
1239
|
+
function outsideLinkAt(projectDir, rel) {
|
|
1240
|
+
const abs = join(projectDir, rel);
|
|
1241
|
+
try {
|
|
1242
|
+
if (!lstatSync(abs, { throwIfNoEntry: false })?.isSymbolicLink()) return null;
|
|
1243
|
+
} catch {
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
return outsideProjectTarget(projectDir, abs);
|
|
1171
1247
|
}
|
|
1172
|
-
function
|
|
1173
|
-
const
|
|
1174
|
-
return
|
|
1248
|
+
function createOutsideGuard(projectDir) {
|
|
1249
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1250
|
+
return {
|
|
1251
|
+
skip(abs) {
|
|
1252
|
+
const hit = outsideProjectTarget(projectDir, abs);
|
|
1253
|
+
if (hit === null) return false;
|
|
1254
|
+
if (!seen.has(hit.path)) seen.set(hit.path, hit);
|
|
1255
|
+
return true;
|
|
1256
|
+
},
|
|
1257
|
+
list: () => [...seen.values()]
|
|
1258
|
+
};
|
|
1175
1259
|
}
|
|
1176
|
-
function
|
|
1177
|
-
const
|
|
1178
|
-
if (!
|
|
1179
|
-
|
|
1180
|
-
if (!existsSync(skillMd)) return null;
|
|
1181
|
-
const fm = extractFrontmatter(readFileSync(skillMd, "utf8"));
|
|
1182
|
-
return fm === null ? null : estimateTokens(fm.length);
|
|
1183
|
-
}
|
|
1184
|
-
function assetBodyTokens(assetId, root = resolveBundleRoot()) {
|
|
1185
|
-
const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
|
|
1186
|
-
if (!asset || asset.method.kind !== "internal") return null;
|
|
1187
|
-
const skillMd = join(root, "templates", "skills", asset.method.key, "SKILL.md");
|
|
1188
|
-
if (!existsSync(skillMd)) return null;
|
|
1189
|
-
const content = readFileSync(skillMd, "utf8");
|
|
1190
|
-
const fm = extractFrontmatter(content);
|
|
1191
|
-
if (fm === null) return estimateTokens(content.length);
|
|
1192
|
-
const close = content.indexOf("\n---", content.indexOf(fm) + fm.length);
|
|
1193
|
-
const body = close === -1 ? "" : content.slice(close + "\n---".length);
|
|
1194
|
-
return estimateTokens(body.trim().length);
|
|
1195
|
-
}
|
|
1196
|
-
function assetCostRows(assetIds, root = resolveBundleRoot()) {
|
|
1197
|
-
return assetIds.map((id) => ({
|
|
1198
|
-
id,
|
|
1199
|
-
descriptorTokens: assetDescriptorTokens(id, root),
|
|
1200
|
-
bodyTokens: assetBodyTokens(id, root)
|
|
1201
|
-
})).sort((a, b) => (b.bodyTokens ?? -1) - (a.bodyTokens ?? -1));
|
|
1202
|
-
}
|
|
1203
|
-
function makeResidentCost(base) {
|
|
1204
|
-
return {
|
|
1205
|
-
...base,
|
|
1206
|
-
perSkillDescriptor: base.perSkillDescriptor ?? {},
|
|
1207
|
-
total: base.rules + base.projectClaudeMd + base.skillDescriptors + base.agentDescriptors,
|
|
1208
|
-
directive: {
|
|
1209
|
-
items: base.items.rules + base.items.claudeMd,
|
|
1210
|
-
tokens: base.rules + base.projectClaudeMd
|
|
1211
|
-
},
|
|
1212
|
-
firing: {
|
|
1213
|
-
items: base.items.skills + base.items.agents,
|
|
1214
|
-
tokens: base.skillDescriptors + base.agentDescriptors
|
|
1215
|
-
}
|
|
1216
|
-
};
|
|
1217
|
-
}
|
|
1218
|
-
function fileTokens(path2) {
|
|
1219
|
-
return existsSync(path2) ? estimateTokens(readFileSync(path2, "utf8").trim().length) : 0;
|
|
1220
|
-
}
|
|
1221
|
-
function descriptorTokens(path2) {
|
|
1222
|
-
if (!existsSync(path2)) return 0;
|
|
1223
|
-
const fm = extractFrontmatter(readFileSync(path2, "utf8"));
|
|
1224
|
-
return fm === null ? 0 : estimateTokens(fm.length);
|
|
1225
|
-
}
|
|
1226
|
-
function landsOnDisk(target, cli) {
|
|
1227
|
-
if (target.startsWith(".claude/agents/")) return cli.includes("claude");
|
|
1228
|
-
return true;
|
|
1229
|
-
}
|
|
1230
|
-
function residentCost(entries, root = resolveBundleRoot()) {
|
|
1231
|
-
const tpl = (source) => join(root, "templates", source);
|
|
1232
|
-
const measured = (e) => e.file ?? tpl(e.source);
|
|
1233
|
-
const perSkill = {};
|
|
1234
|
-
let rules = 0;
|
|
1235
|
-
let skillDescriptors = 0;
|
|
1236
|
-
let agentDescriptors = 0;
|
|
1237
|
-
let ruleItems = 0;
|
|
1238
|
-
let skillItems = 0;
|
|
1239
|
-
let agentItems = 0;
|
|
1240
|
-
for (const e of entries) {
|
|
1241
|
-
if (e.target.startsWith(".claude/rules/")) {
|
|
1242
|
-
rules += fileTokens(measured(e));
|
|
1243
|
-
ruleItems += 1;
|
|
1244
|
-
} else if (e.target.startsWith(".claude/agents/")) {
|
|
1245
|
-
agentDescriptors += descriptorTokens(measured(e));
|
|
1246
|
-
agentItems += 1;
|
|
1247
|
-
} else if (e.target.startsWith(".claude/skills/")) {
|
|
1248
|
-
const t = descriptorTokens(join(measured(e), "SKILL.md"));
|
|
1249
|
-
skillDescriptors += t;
|
|
1250
|
-
skillItems += 1;
|
|
1251
|
-
const id = /\.claude\/skills\/([^/]+)/.exec(e.target)?.[1];
|
|
1252
|
-
if (id !== void 0) perSkill[id] = (perSkill[id] ?? 0) + t;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
const harnessAnchor = fileTokens(join(root, "templates", "CLAUDE.md"));
|
|
1256
|
-
const projectScaffold = estimateTokens(
|
|
1257
|
-
withContinuousSkillsNote(
|
|
1258
|
-
renderFillScaffold(),
|
|
1259
|
-
CONTINUOUS_SKILLS.map((s) => s.id)
|
|
1260
|
-
).trim().length
|
|
1261
|
-
);
|
|
1262
|
-
const projectClaudeMd = harnessAnchor + projectScaffold;
|
|
1263
|
-
const claudeMdItems = (harnessAnchor > 0 ? 1 : 0) + (projectScaffold > 0 ? 1 : 0);
|
|
1264
|
-
return makeResidentCost({
|
|
1265
|
-
rules,
|
|
1266
|
-
projectClaudeMd,
|
|
1267
|
-
skillDescriptors,
|
|
1268
|
-
agentDescriptors,
|
|
1269
|
-
perSkillDescriptor: perSkill,
|
|
1270
|
-
items: {
|
|
1271
|
-
rules: ruleItems,
|
|
1272
|
-
skills: skillItems,
|
|
1273
|
-
agents: agentItems,
|
|
1274
|
-
claudeMd: claudeMdItems,
|
|
1275
|
-
total: ruleItems + skillItems + agentItems + claudeMdItems
|
|
1276
|
-
}
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
function summarizeContextCost(assetIds, root = resolveBundleRoot()) {
|
|
1280
|
-
let measuredTokens = 0;
|
|
1281
|
-
let measuredCount = 0;
|
|
1282
|
-
let unmeasuredCount = 0;
|
|
1283
|
-
for (const id of assetIds) {
|
|
1284
|
-
const tokens = assetDescriptorTokens(id, root);
|
|
1285
|
-
if (tokens === null) unmeasuredCount += 1;
|
|
1286
|
-
else {
|
|
1287
|
-
measuredTokens += tokens;
|
|
1288
|
-
measuredCount += 1;
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
return { measuredTokens, measuredCount, unmeasuredCount };
|
|
1292
|
-
}
|
|
1293
|
-
function formatResidentCostLine(r, unmeasuredCount) {
|
|
1294
|
-
if (r.total === 0) return null;
|
|
1295
|
-
const parts = [
|
|
1296
|
-
`directives ${r.directive.items} ~${r.directive.tokens}`,
|
|
1297
|
-
`[rules ${r.items.rules} ~${r.rules} \xB7 CLAUDE.md ${r.items.claudeMd} ~${r.projectClaudeMd}]`,
|
|
1298
|
-
`triggers ${r.firing.items} ~${r.firing.tokens}`,
|
|
1299
|
-
`[skills ${r.items.skills} ~${r.skillDescriptors} \xB7 agents ${r.items.agents} ~${r.agentDescriptors}]`
|
|
1300
|
-
].join(" \xB7 ");
|
|
1301
|
-
const external = unmeasuredCount > 0 ? ` \xB7 ${unmeasuredCount} external asset${unmeasuredCount === 1 ? "" : "s"} unmeasured` : "";
|
|
1302
|
-
return `session-start context cost: ${r.items.total} items resident \xB7 ~${r.total} tokens/session (${parts}${external})`;
|
|
1303
|
-
}
|
|
1304
|
-
var RESIDENT_ROWS = [
|
|
1305
|
-
{
|
|
1306
|
-
// ADR-083 — 축 소계를 **행으로** 낸다. 소계가 없으면 읽는 사람이 네 줄을 머리로 더해야
|
|
1307
|
-
// 하고, 그러면 "무엇을 줄여야 하나"가 표에서 안 보인다.
|
|
1308
|
-
label: "\uC9C0\uC2DC\uBB38",
|
|
1309
|
-
gap: 13,
|
|
1310
|
-
count: (r) => r.directive.items,
|
|
1311
|
-
tokens: (r) => r.directive.tokens
|
|
1312
|
-
},
|
|
1313
|
-
{ label: " rules", gap: 12, count: (r) => r.items.rules, tokens: (r) => r.rules },
|
|
1314
|
-
{
|
|
1315
|
-
// 한 행에 두 파일(앵커 + 스캐폴드). 개수 열이 2 를 뱉어 그 사실을 드러낸다 — 라벨로
|
|
1316
|
-
// 하나만 이름 붙이면 v26.140.0 이전과 같은 거짓 라벨이 된다.
|
|
1317
|
-
label: " CLAUDE.md",
|
|
1318
|
-
gap: 8,
|
|
1319
|
-
count: (r) => r.items.claudeMd,
|
|
1320
|
-
tokens: (r) => r.projectClaudeMd
|
|
1321
|
-
},
|
|
1322
|
-
{
|
|
1323
|
-
label: "\uBC1C\uD654 \uD45C\uBA74",
|
|
1324
|
-
gap: 10,
|
|
1325
|
-
count: (r) => r.firing.items,
|
|
1326
|
-
tokens: (r) => r.firing.tokens
|
|
1327
|
-
},
|
|
1328
|
-
{
|
|
1329
|
-
label: " skill descriptors",
|
|
1330
|
-
gap: 0,
|
|
1331
|
-
count: (r) => r.items.skills,
|
|
1332
|
-
tokens: (r) => r.skillDescriptors
|
|
1333
|
-
},
|
|
1334
|
-
{
|
|
1335
|
-
label: " agent descriptors",
|
|
1336
|
-
gap: 0,
|
|
1337
|
-
count: (r) => r.items.agents,
|
|
1338
|
-
tokens: (r) => r.agentDescriptors
|
|
1339
|
-
}
|
|
1340
|
-
];
|
|
1341
|
-
function formatResidentCostBlock(r) {
|
|
1342
|
-
const n = (v) => String(v).padStart(3);
|
|
1343
|
-
return [
|
|
1344
|
-
" \uAC1C\uC218 \uD1A0\uD070",
|
|
1345
|
-
...RESIDENT_ROWS.map(
|
|
1346
|
-
(row) => ` ${row.label}${" ".repeat(row.gap)}${n(row.count(r))}\uAC1C ~${row.tokens(r)}`
|
|
1347
|
-
),
|
|
1348
|
-
" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
1349
|
-
` \uC0C1\uC8FC \uD569\uACC4 ${n(r.items.total)}\uAC1C \uC0C1\uC8FC \xB7 ~${r.total} tokens/\uC138\uC158`,
|
|
1350
|
-
"",
|
|
1351
|
-
" \uC9C0\uC2DC\uBB38 = \uC6B0\uB9AC\uAC00 \uD56D\uC0C1 \uC77D\uD788\uAC8C \uB123\uC740 \uAC83. **\uC791\uC744\uC218\uB85D \uC88B\uC740 \uAC12\uC774 \uC544\uB2C8\uB2E4** \u2014 \uD310\uB2E8\uC740",
|
|
1352
|
-
" '\uBD88\uD544\uC694\uD55C \uC9C0\uC2DC\uBB38\uC774 \uC788\uB098'\uB85C \uD55C\uB2E4. ratchet \uC740 \uC870\uC6A9\uD55C \uC99D\uAC00\uB9CC \uB9C9\uB294\uB2E4.",
|
|
1353
|
-
" \uBC1C\uD654 \uD45C\uBA74 = \uC2A4\uD0AC\xB7\uC5D0\uC774\uC804\uD2B8\uAC00 \uBD88\uB9AC\uB824\uACE0 \uC0C1\uC8FC\uC2DC\uD0A4\uB294 descriptor. \uAE4E\uC73C\uBA74 \uC548 \uBD88\uB9B0\uB2E4."
|
|
1354
|
-
];
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// src/manifest.ts
|
|
1358
|
-
init_esm_shims();
|
|
1359
|
-
|
|
1360
|
-
// src/install-log.ts
|
|
1361
|
-
init_esm_shims();
|
|
1362
|
-
import { createHash } from "crypto";
|
|
1363
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
|
|
1364
|
-
import { dirname as dirname2, join as join3 } from "path";
|
|
1365
|
-
|
|
1366
|
-
// src/cli-targets.ts
|
|
1367
|
-
init_esm_shims();
|
|
1368
|
-
var CLI_BASE_SORT_ORDER = {
|
|
1369
|
-
claude: 0,
|
|
1370
|
-
codex: 1,
|
|
1371
|
-
opencode: 2,
|
|
1372
|
-
antigravity: 3
|
|
1373
|
-
};
|
|
1374
|
-
function parseCliTargets(input) {
|
|
1375
|
-
const items = normalizeInput(input);
|
|
1376
|
-
if (items.length === 0) {
|
|
1377
|
-
return { ok: true, targets: ["claude"], warnings: [] };
|
|
1378
|
-
}
|
|
1379
|
-
const collected = /* @__PURE__ */ new Set();
|
|
1380
|
-
const warnings = [];
|
|
1381
|
-
for (const item of items) {
|
|
1382
|
-
if (!isCliBase(item)) {
|
|
1383
|
-
let hint = "";
|
|
1384
|
-
if (item === "both") {
|
|
1385
|
-
hint = "\n v0.8.0 removed 'both' alias. Use --cli claude --cli codex.";
|
|
1386
|
-
} else if (item === "all") {
|
|
1387
|
-
hint = "\n v0.8.0 removed 'all' alias. Use --cli claude --cli codex --cli opencode.";
|
|
1388
|
-
} else if (item.includes(",")) {
|
|
1389
|
-
hint = "\n Tip: comma-separated not supported. Use --cli A --cli B for multiple.";
|
|
1390
|
-
}
|
|
1391
|
-
return {
|
|
1392
|
-
ok: false,
|
|
1393
|
-
targets: ["claude"],
|
|
1394
|
-
warnings,
|
|
1395
|
-
error: `Invalid --cli value: ${item}. Must be one of: ${CLI_BASES.join(" | ")}${hint}`
|
|
1396
|
-
};
|
|
1397
|
-
}
|
|
1398
|
-
collected.add(item);
|
|
1399
|
-
}
|
|
1400
|
-
const targets = [...collected].sort((a, b) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b]);
|
|
1401
|
-
return { ok: true, targets, warnings };
|
|
1402
|
-
}
|
|
1403
|
-
function normalizeInput(input) {
|
|
1404
|
-
if (input === void 0 || input === null) return [];
|
|
1405
|
-
if (typeof input === "string") {
|
|
1406
|
-
const trimmed = input.trim();
|
|
1407
|
-
return trimmed === "" ? [] : [trimmed];
|
|
1408
|
-
}
|
|
1409
|
-
return input.filter((s) => typeof s === "string" && s.trim() !== "").map((s) => s.trim());
|
|
1410
|
-
}
|
|
1411
|
-
function targetsInclude(targets, base) {
|
|
1412
|
-
return targets.includes(base);
|
|
1260
|
+
function mergeOutside(...lists) {
|
|
1261
|
+
const out = /* @__PURE__ */ new Map();
|
|
1262
|
+
for (const list of lists) for (const o of list) if (!out.has(o.path)) out.set(o.path, o);
|
|
1263
|
+
return [...out.values()];
|
|
1413
1264
|
}
|
|
1414
1265
|
|
|
1415
1266
|
// src/fs-ops.ts
|
|
1416
|
-
init_esm_shims();
|
|
1417
|
-
import {
|
|
1418
|
-
copyFileSync,
|
|
1419
|
-
cpSync,
|
|
1420
|
-
existsSync as existsSync2,
|
|
1421
|
-
constants as fsConstants,
|
|
1422
|
-
mkdirSync,
|
|
1423
|
-
readdirSync,
|
|
1424
|
-
readFileSync as readFileSync2,
|
|
1425
|
-
realpathSync,
|
|
1426
|
-
renameSync,
|
|
1427
|
-
rmdirSync
|
|
1428
|
-
} from "fs";
|
|
1429
|
-
import { dirname, join as join2 } from "path";
|
|
1430
1267
|
function ensureDir(path2) {
|
|
1431
1268
|
mkdirSync(path2, { recursive: true });
|
|
1432
1269
|
}
|
|
1433
1270
|
function copyFile(source, target) {
|
|
1434
|
-
if (!
|
|
1271
|
+
if (!existsSync(source)) {
|
|
1435
1272
|
throw new Error(`Source not found: ${source}`);
|
|
1436
1273
|
}
|
|
1437
|
-
mkdirSync(
|
|
1274
|
+
mkdirSync(dirname2(target), { recursive: true });
|
|
1438
1275
|
copyFileSync(source, target);
|
|
1439
1276
|
}
|
|
1440
1277
|
function copyDir(source, target, foreignOf) {
|
|
1441
|
-
if (!
|
|
1278
|
+
if (!existsSync(source)) {
|
|
1442
1279
|
throw new Error(`Source dir not found: ${source}`);
|
|
1443
1280
|
}
|
|
1444
1281
|
mkdirSync(target, { recursive: true });
|
|
@@ -1467,7 +1304,7 @@ function claimBackupPath(base, claim) {
|
|
|
1467
1304
|
throw new Error(`Cannot allocate a backup path for ${base} (${MAX_BACKUP_CLAIMS} taken)`);
|
|
1468
1305
|
}
|
|
1469
1306
|
function backupDir(target, now = /* @__PURE__ */ new Date()) {
|
|
1470
|
-
if (!
|
|
1307
|
+
if (!existsSync(target)) {
|
|
1471
1308
|
return null;
|
|
1472
1309
|
}
|
|
1473
1310
|
const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c) => mkdirSync(c));
|
|
@@ -1476,21 +1313,44 @@ function backupDir(target, now = /* @__PURE__ */ new Date()) {
|
|
|
1476
1313
|
return backup;
|
|
1477
1314
|
}
|
|
1478
1315
|
function copyBackupDir(target, now = /* @__PURE__ */ new Date()) {
|
|
1479
|
-
if (!
|
|
1316
|
+
if (!existsSync(target)) {
|
|
1480
1317
|
return null;
|
|
1481
1318
|
}
|
|
1482
1319
|
const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c) => mkdirSync(c));
|
|
1483
|
-
|
|
1320
|
+
try {
|
|
1321
|
+
copyTreePreservingLinks(realpathSync2(target), backup);
|
|
1322
|
+
} catch (e) {
|
|
1323
|
+
rmSync(backup, { recursive: true, force: true });
|
|
1324
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
1325
|
+
throw new Error(`backup of ${target} failed, partial copy removed \u2014 ${detail}`);
|
|
1326
|
+
}
|
|
1484
1327
|
return backup;
|
|
1485
1328
|
}
|
|
1329
|
+
function copyTreePreservingLinks(source, target) {
|
|
1330
|
+
mkdirSync(target, { recursive: true });
|
|
1331
|
+
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
1332
|
+
const src = join2(source, entry.name);
|
|
1333
|
+
const dst = join2(target, entry.name);
|
|
1334
|
+
if (entry.isSymbolicLink()) {
|
|
1335
|
+
symlinkSync(readlinkSync2(src), dst);
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if (entry.isDirectory()) copyTreePreservingLinks(src, dst);
|
|
1339
|
+
else if (entry.isFile()) copyFile(src, dst);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1486
1342
|
function backupFile(target, now = /* @__PURE__ */ new Date()) {
|
|
1487
1343
|
return claimBackupPath(
|
|
1488
1344
|
`${target}.backup-${formatStamp(now)}`,
|
|
1489
1345
|
(c) => copyFileSync(target, c, fsConstants.COPYFILE_EXCL)
|
|
1490
1346
|
);
|
|
1491
1347
|
}
|
|
1348
|
+
function backupOriginal(backupPath) {
|
|
1349
|
+
const m = /^(.+)\.backup-\d{8}T\d{6}(?:-\d+)?$/.exec(backupPath);
|
|
1350
|
+
return m?.[1] ?? null;
|
|
1351
|
+
}
|
|
1492
1352
|
function listFilesRecursive(dir, prefix = "") {
|
|
1493
|
-
if (!
|
|
1353
|
+
if (!existsSync(dir)) return [];
|
|
1494
1354
|
const out = [];
|
|
1495
1355
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1496
1356
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
@@ -1515,9 +1375,93 @@ function ensureProjectSkeleton(projectDir) {
|
|
|
1515
1375
|
"docs/decisions"
|
|
1516
1376
|
];
|
|
1517
1377
|
for (const d of dirs) {
|
|
1378
|
+
if (outsideProjectTarget(projectDir, join2(projectDir, d)) !== null) continue;
|
|
1518
1379
|
mkdirSync(join2(projectDir, d), { recursive: true });
|
|
1519
1380
|
}
|
|
1520
1381
|
}
|
|
1382
|
+
function dirTreesIdentical(a, b) {
|
|
1383
|
+
if (!existsSync(a) || !existsSync(b)) return false;
|
|
1384
|
+
const signature = (root, rel = "", out = /* @__PURE__ */ new Map()) => {
|
|
1385
|
+
for (const e of readdirSync(join2(root, rel), { withFileTypes: true })) {
|
|
1386
|
+
const r = rel === "" ? e.name : `${rel}/${e.name}`;
|
|
1387
|
+
if (e.isSymbolicLink()) out.set(r, `link:${readlinkSync2(join2(root, r))}`);
|
|
1388
|
+
else if (e.isDirectory()) signature(root, r, out);
|
|
1389
|
+
else if (e.isFile()) {
|
|
1390
|
+
out.set(
|
|
1391
|
+
r,
|
|
1392
|
+
createHash("sha256").update(readFileSync(join2(root, r))).digest("hex")
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return out;
|
|
1397
|
+
};
|
|
1398
|
+
const sa = signature(a);
|
|
1399
|
+
const sb = signature(b);
|
|
1400
|
+
if (sa.size !== sb.size) return false;
|
|
1401
|
+
for (const [rel, sha] of sa) {
|
|
1402
|
+
if (sb.get(rel) !== sha) return false;
|
|
1403
|
+
}
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1406
|
+
function backupIfLossyUtf8(target, now = /* @__PURE__ */ new Date()) {
|
|
1407
|
+
if (!existsSync(target)) return null;
|
|
1408
|
+
let raw;
|
|
1409
|
+
try {
|
|
1410
|
+
raw = readFileSync(target);
|
|
1411
|
+
} catch {
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1414
|
+
if (Buffer.from(raw.toString("utf8")).equals(raw)) return null;
|
|
1415
|
+
const backup = backupFile(target, now);
|
|
1416
|
+
console.error(
|
|
1417
|
+
`\u26A0 ${target} contains bytes that are not valid UTF-8 \u2014 rewriting would alter them permanently. Original bytes saved as ${backup}`
|
|
1418
|
+
);
|
|
1419
|
+
return backup;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/track-heal.ts
|
|
1423
|
+
init_esm_shims();
|
|
1424
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
|
|
1425
|
+
import { join as join3 } from "path";
|
|
1426
|
+
var META_FILE = ".claude/.installed-tracks";
|
|
1427
|
+
var META_KEY = ".installed-tracks";
|
|
1428
|
+
function memoTargets(render) {
|
|
1429
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1430
|
+
return (tracks) => {
|
|
1431
|
+
const key = [...tracks].sort().join(",");
|
|
1432
|
+
const hit = cache.get(key) ?? new Set(render(tracks));
|
|
1433
|
+
cache.set(key, hit);
|
|
1434
|
+
return hit;
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
var claudeTargets = memoTargets((tracks) => {
|
|
1438
|
+
const spec = buildAssetSpec({ tracks, options: DEFAULT_OPTIONS });
|
|
1439
|
+
return buildManifest(spec).filter((e) => e.applies(spec) && (e.type === "file" || e.target.startsWith(".claude/skills/"))).map((e) => e.target);
|
|
1440
|
+
});
|
|
1441
|
+
function claudeRecorded(log) {
|
|
1442
|
+
return /* @__PURE__ */ new Set([
|
|
1443
|
+
...(log.policyFiles ?? []).map((f) => `.claude/${f.path}`),
|
|
1444
|
+
...(log.skillFiles ?? []).map((f) => `.claude/skills/${f.path.split("/")[0] ?? ""}`)
|
|
1445
|
+
]);
|
|
1446
|
+
}
|
|
1447
|
+
function healRecordedTracks(log, projectDir, clis) {
|
|
1448
|
+
if (!clis.includes("claude")) return log;
|
|
1449
|
+
const recorded = log.spec.tracks.filter(isTrack);
|
|
1450
|
+
const added = metafileTracks(log, projectDir).filter((t) => !recorded.includes(t));
|
|
1451
|
+
if (added.length === 0) return log;
|
|
1452
|
+
return {
|
|
1453
|
+
...log,
|
|
1454
|
+
spec: { ...log.spec, tracks: [.../* @__PURE__ */ new Set([...log.spec.tracks, ...added])].sort() }
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
function metafileTracks(log, projectDir) {
|
|
1458
|
+
const abs = join3(projectDir, META_FILE);
|
|
1459
|
+
if (lstatSync2(abs, { throwIfNoEntry: false })?.isFile() !== true) return [];
|
|
1460
|
+
const content = readFileSync2(abs, "utf8");
|
|
1461
|
+
const sha = log.policyFiles?.find((f) => f.path === META_KEY)?.sha256;
|
|
1462
|
+
if (sha === void 0 || hashContent(content) !== sha) return [];
|
|
1463
|
+
return content.split("\n").map((l) => l.trim()).filter(isTrack);
|
|
1464
|
+
}
|
|
1521
1465
|
|
|
1522
1466
|
// src/install-log.ts
|
|
1523
1467
|
var INSTALL_LOG_FILENAME = ".harness-install.json";
|
|
@@ -1546,9 +1490,22 @@ function installedClis(log) {
|
|
|
1546
1490
|
function sortClis(clis) {
|
|
1547
1491
|
return [...new Set(clis)].filter(isCliBase).sort((a, b) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b]);
|
|
1548
1492
|
}
|
|
1549
|
-
function buildAssetEntries(report, scope) {
|
|
1493
|
+
function buildAssetEntries(report, scope, previous) {
|
|
1550
1494
|
if (!report) return [];
|
|
1551
|
-
return report.attempted.filter((r) => r.ok).map((r) =>
|
|
1495
|
+
return report.attempted.filter((r) => r.ok).map((r) => {
|
|
1496
|
+
const entry = assetToLogEntry(r.asset, scope, r.version);
|
|
1497
|
+
if (r.files === void 0) return entry;
|
|
1498
|
+
const recordedBefore = previous?.find((a) => a.id === r.asset.id);
|
|
1499
|
+
if (recordedBefore !== void 0 && recordedBefore.files === void 0) return entry;
|
|
1500
|
+
return { ...entry, files: mergeToolFiles(recordedBefore?.files, r.files) };
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
function mergeToolFiles(prior, run) {
|
|
1504
|
+
const changed = new Map(run.changed.map((f) => [f.path, f.sha256]));
|
|
1505
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1506
|
+
for (const f of prior ?? []) byPath.set(f.path, changed.get(f.path) ?? f.sha256);
|
|
1507
|
+
for (const f of run.created) byPath.set(f.path, f.sha256);
|
|
1508
|
+
return [...byPath].map(([path2, sha256]) => ({ path: path2, sha256 }));
|
|
1552
1509
|
}
|
|
1553
1510
|
function assetToLogEntry(asset, scope, version) {
|
|
1554
1511
|
const detail = methodDetail(asset.method);
|
|
@@ -1577,7 +1534,7 @@ function methodDetail(method) {
|
|
|
1577
1534
|
}
|
|
1578
1535
|
}
|
|
1579
1536
|
var BUNDLED_SKILL_IDS = new Set(INTERNAL_BUNDLED_SKILL_IDS);
|
|
1580
|
-
function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
|
|
1537
|
+
function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = [], codexTrust) {
|
|
1581
1538
|
const skillExclude = (spec.userOverride?.forceExclude ?? []).filter(
|
|
1582
1539
|
(id) => BUNDLED_SKILL_IDS.has(id)
|
|
1583
1540
|
);
|
|
@@ -1588,12 +1545,15 @@ function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDi
|
|
|
1588
1545
|
...rootClaudeMd ? { rootClaudeMd } : {}
|
|
1589
1546
|
};
|
|
1590
1547
|
const clis = sortClis([...installedClis(previous ?? null), ...spec.cli.filter(isCliBase)]);
|
|
1548
|
+
const tracks = [
|
|
1549
|
+
.../* @__PURE__ */ new Set([...(previous?.spec.tracks ?? []).filter(isTrack), ...spec.tracks])
|
|
1550
|
+
].sort();
|
|
1591
1551
|
const log = {
|
|
1592
1552
|
schemaVersion: INSTALL_LOG_VERSION,
|
|
1593
1553
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1594
1554
|
scope,
|
|
1595
1555
|
spec: {
|
|
1596
|
-
tracks
|
|
1556
|
+
tracks,
|
|
1597
1557
|
cli: spec.cli,
|
|
1598
1558
|
clis,
|
|
1599
1559
|
...spec.baselineExclude && spec.baselineExclude.length > 0 ? { baselineExclude: spec.baselineExclude } : {},
|
|
@@ -1604,11 +1564,13 @@ function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDi
|
|
|
1604
1564
|
templates: { ...previous?.templates, ...templates },
|
|
1605
1565
|
assets: mergeAssets(
|
|
1606
1566
|
claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
|
|
1607
|
-
buildAssetEntries(external, scope)
|
|
1567
|
+
buildAssetEntries(external, scope, previous?.assets)
|
|
1608
1568
|
)
|
|
1609
1569
|
};
|
|
1610
1570
|
const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
|
|
1611
1571
|
if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
|
|
1572
|
+
const trust = codexTrust ?? previous?.codexTrust;
|
|
1573
|
+
if (trust) log.codexTrust = trust;
|
|
1612
1574
|
return log;
|
|
1613
1575
|
}
|
|
1614
1576
|
function mergeRootFiles(previous, current) {
|
|
@@ -1657,35 +1619,35 @@ function mergeAssets(previous, current) {
|
|
|
1657
1619
|
];
|
|
1658
1620
|
}
|
|
1659
1621
|
function hashContent(content) {
|
|
1660
|
-
return
|
|
1622
|
+
return createHash2("sha256").update(content, "utf8").digest("hex");
|
|
1661
1623
|
}
|
|
1662
1624
|
function isHarnessOwned(baseline, key, current) {
|
|
1663
1625
|
const recorded = baseline.get(key);
|
|
1664
1626
|
return recorded !== void 0 && recorded === hashContent(current);
|
|
1665
1627
|
}
|
|
1666
|
-
function mergeExternalFiles(
|
|
1628
|
+
function mergeExternalFiles(previous, current) {
|
|
1667
1629
|
const byPath = /* @__PURE__ */ new Map();
|
|
1668
1630
|
for (const file of [...previous ?? [], ...current]) byPath.set(file.path, file);
|
|
1669
|
-
return [...byPath.values()]
|
|
1631
|
+
return [...byPath.values()];
|
|
1670
1632
|
}
|
|
1671
1633
|
function collectSkillHashes(projectDir, templatesDir) {
|
|
1672
|
-
const skillsDir =
|
|
1673
|
-
return listFilesRecursive(skillsDir).filter((rel) =>
|
|
1634
|
+
const skillsDir = join4(projectDir, ".claude/skills");
|
|
1635
|
+
return listFilesRecursive(skillsDir).filter((rel) => existsSync2(join4(templatesDir, "skills", rel))).map((rel) => ({
|
|
1674
1636
|
path: rel,
|
|
1675
|
-
sha256: hashContent(readFileSync3(
|
|
1637
|
+
sha256: hashContent(readFileSync3(join4(skillsDir, rel), "utf8"))
|
|
1676
1638
|
}));
|
|
1677
1639
|
}
|
|
1678
1640
|
function collectPolicyHashes(projectDir, templatesDir) {
|
|
1679
1641
|
const out = [];
|
|
1680
1642
|
for (const { dir, ext } of POLICY_DIRS) {
|
|
1681
|
-
const targetDir =
|
|
1682
|
-
const sourceDir =
|
|
1643
|
+
const targetDir = join4(projectDir, ".claude", dir);
|
|
1644
|
+
const sourceDir = join4(templatesDir, dir);
|
|
1683
1645
|
for (const rel of listFilesRecursive(targetDir)) {
|
|
1684
1646
|
if (!rel.endsWith(ext)) continue;
|
|
1685
|
-
if (!
|
|
1647
|
+
if (!existsSync2(join4(sourceDir, rel))) continue;
|
|
1686
1648
|
out.push({
|
|
1687
1649
|
path: `${dir}/${rel}`,
|
|
1688
|
-
sha256: hashContent(readFileSync3(
|
|
1650
|
+
sha256: hashContent(readFileSync3(join4(targetDir, rel), "utf8"))
|
|
1689
1651
|
});
|
|
1690
1652
|
}
|
|
1691
1653
|
}
|
|
@@ -1693,42 +1655,371 @@ function collectPolicyHashes(projectDir, templatesDir) {
|
|
|
1693
1655
|
}
|
|
1694
1656
|
function writeInstallLog(projectDir, log) {
|
|
1695
1657
|
const path2 = installLogPath(projectDir);
|
|
1696
|
-
mkdirSync2(
|
|
1697
|
-
|
|
1658
|
+
mkdirSync2(dirname3(path2), { recursive: true });
|
|
1659
|
+
const marked = { ...normalizeLogOrder(log), excludedKeysMigrated: true };
|
|
1660
|
+
writeFileSync(path2, `${JSON.stringify(marked, null, 2)}
|
|
1698
1661
|
`, "utf8");
|
|
1699
1662
|
migrateAwayLegacyLog(projectDir);
|
|
1700
1663
|
return path2;
|
|
1701
1664
|
}
|
|
1665
|
+
function normalizeLogOrder(log) {
|
|
1666
|
+
const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
1667
|
+
const byPath = (xs) => [...xs].sort((a, b) => cmp(a.path, b.path));
|
|
1668
|
+
const out = { ...log };
|
|
1669
|
+
if (log.policyFiles !== void 0) out.policyFiles = byPath(log.policyFiles);
|
|
1670
|
+
if (log.skillFiles !== void 0) out.skillFiles = byPath(log.skillFiles);
|
|
1671
|
+
if (log.externalFiles !== void 0) out.externalFiles = byPath(log.externalFiles);
|
|
1672
|
+
if (log.rootFiles !== void 0) out.rootFiles = byPath(log.rootFiles);
|
|
1673
|
+
if (log.portions !== void 0) {
|
|
1674
|
+
out.portions = [...log.portions].sort((a, b) => cmp(a.path, b.path) || cmp(a.key, b.key));
|
|
1675
|
+
}
|
|
1676
|
+
return out;
|
|
1677
|
+
}
|
|
1702
1678
|
function migrateAwayLegacyLog(projectDir) {
|
|
1703
1679
|
const legacy = legacyInstallLogPath(projectDir);
|
|
1704
|
-
if (!
|
|
1680
|
+
if (!existsSync2(legacy)) return;
|
|
1705
1681
|
try {
|
|
1706
|
-
|
|
1682
|
+
rmSync2(legacy);
|
|
1707
1683
|
} catch {
|
|
1708
1684
|
}
|
|
1709
1685
|
}
|
|
1710
1686
|
function readInstallLog(projectDir) {
|
|
1687
|
+
return readInstallLogStatus(projectDir).log;
|
|
1688
|
+
}
|
|
1689
|
+
function readInstallLogStatus(projectDir) {
|
|
1711
1690
|
const path2 = [installLogPath(projectDir), legacyInstallLogPath(projectDir)].find(
|
|
1712
|
-
(p) =>
|
|
1691
|
+
(p) => existsSync2(p)
|
|
1713
1692
|
);
|
|
1714
|
-
if (!path2) return null;
|
|
1693
|
+
if (!path2) return { status: "missing", log: null };
|
|
1694
|
+
let parsed;
|
|
1715
1695
|
try {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1696
|
+
parsed = JSON.parse(readFileSync3(path2, "utf8"));
|
|
1697
|
+
} catch {
|
|
1698
|
+
return { status: "corrupted", log: null };
|
|
1699
|
+
}
|
|
1700
|
+
if (!isValidInstallLogShape(parsed)) return { status: "corrupted", log: null };
|
|
1701
|
+
const log = parsed;
|
|
1702
|
+
if (Array.isArray(log.assets)) {
|
|
1703
|
+
log.assets = log.assets.map(
|
|
1704
|
+
(a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
const migrated = migrateExcluded(log, projectDir);
|
|
1708
|
+
if (migrated.droppedKeys.length > 0) {
|
|
1709
|
+
LEGACY_RELEASED.set(migrated.log, {
|
|
1710
|
+
released: migrated.droppedKeys,
|
|
1711
|
+
catalog: migrated.releasedCatalog
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
return {
|
|
1715
|
+
status: "ok",
|
|
1716
|
+
log: healRecordedTracks(migrated.log, projectDir, installedClis(migrated.log))
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
var LEGACY_RELEASED = /* @__PURE__ */ new WeakMap();
|
|
1720
|
+
function legacyDroppedKeys(log) {
|
|
1721
|
+
return log === null ? [] : LEGACY_RELEASED.get(log)?.released ?? [];
|
|
1722
|
+
}
|
|
1723
|
+
function legacyReleasedCatalog(log) {
|
|
1724
|
+
return log === null ? [] : LEGACY_RELEASED.get(log)?.catalog ?? [];
|
|
1725
|
+
}
|
|
1726
|
+
var SELECTIONS_MAX = 100;
|
|
1727
|
+
var HARNESS_VERSION = package_default.version;
|
|
1728
|
+
function appendSelection(log, ev) {
|
|
1729
|
+
const empty = (a) => a === void 0 || a.length === 0;
|
|
1730
|
+
if (empty(ev.with) && empty(ev.without) && empty(ev.released) && empty(ev.kept)) return log;
|
|
1731
|
+
const clean = {
|
|
1732
|
+
at: ev.at,
|
|
1733
|
+
harness: ev.harness,
|
|
1734
|
+
by: ev.by,
|
|
1735
|
+
...ev.via !== void 0 ? { via: ev.via } : {},
|
|
1736
|
+
...empty(ev.with) ? {} : { with: [...ev.with ?? []] },
|
|
1737
|
+
...empty(ev.without) ? {} : { without: [...ev.without ?? []] },
|
|
1738
|
+
...empty(ev.released) ? {} : { released: [...ev.released ?? []] },
|
|
1739
|
+
...empty(ev.kept) ? {} : { kept: [...ev.kept ?? []] },
|
|
1740
|
+
...ev.interrupted ? { interrupted: true } : {}
|
|
1741
|
+
};
|
|
1742
|
+
return { ...log, selections: [...log.selections ?? [], clean].slice(-SELECTIONS_MAX) };
|
|
1743
|
+
}
|
|
1744
|
+
var LEGACY_REINSTALL_WINDOW_MS = 6e5;
|
|
1745
|
+
function assetFiles(projectDir, asset) {
|
|
1746
|
+
if (asset.files !== void 0) {
|
|
1747
|
+
return asset.files.map((f) => join4(projectDir, f.path)).filter((p) => existsSync2(p));
|
|
1748
|
+
}
|
|
1749
|
+
const dir = asset.detail.skill ?? asset.id;
|
|
1750
|
+
return [".claude/skills", ".agents/skills"].flatMap(
|
|
1751
|
+
(d) => listFilesRecursive(join4(projectDir, d, dir)).map((rel) => join4(projectDir, d, dir, rel))
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
function legacyCatalogReleased(projectDir, log, asset) {
|
|
1755
|
+
if (asset.method !== "skill" || asset.scope === "global") return false;
|
|
1756
|
+
const files = assetFiles(projectDir, asset);
|
|
1757
|
+
if (files.length === 0) return false;
|
|
1758
|
+
const t = Date.parse(log.installedAt);
|
|
1759
|
+
if (Number.isNaN(t)) return false;
|
|
1760
|
+
const m = Math.max(...files.map((f) => statSync(f).mtimeMs));
|
|
1761
|
+
return m <= t && m >= t - LEGACY_REINSTALL_WINDOW_MS;
|
|
1762
|
+
}
|
|
1763
|
+
function migrateExcluded(log, projectDir) {
|
|
1764
|
+
if (log.excludedKeysMigrated === true) return { log, droppedKeys: [], releasedCatalog: [] };
|
|
1765
|
+
const baseline = new Set(log.spec.baselineExclude ?? []);
|
|
1766
|
+
const skills = new Set(log.spec.skillExclude ?? []);
|
|
1767
|
+
const released = [];
|
|
1768
|
+
const releasedCatalog = [];
|
|
1769
|
+
const keptCatalog = [];
|
|
1770
|
+
const kept = [];
|
|
1771
|
+
for (const id of log.excluded ?? []) {
|
|
1772
|
+
if (isKeyId(id)) {
|
|
1773
|
+
released.push(id);
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
if (id.startsWith("baseline:") && !baseline.has(id)) {
|
|
1777
|
+
released.push(id);
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
if (BUNDLED_SKILL_IDS.has(id) && !skills.has(id)) {
|
|
1781
|
+
released.push(id);
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
const asset = log.assets.find((a) => a.id === id);
|
|
1785
|
+
if (asset !== void 0 && !BUNDLED_SKILL_IDS.has(id) && !id.startsWith("baseline:")) {
|
|
1786
|
+
if (projectDir !== void 0 && legacyCatalogReleased(projectDir, log, asset)) {
|
|
1787
|
+
released.push(id);
|
|
1788
|
+
releasedCatalog.push(id);
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
keptCatalog.push(id);
|
|
1792
|
+
}
|
|
1793
|
+
kept.push(id);
|
|
1794
|
+
}
|
|
1795
|
+
let next = { ...log, excludedKeysMigrated: true };
|
|
1796
|
+
if (kept.length > 0) next.excluded = kept;
|
|
1797
|
+
else delete next.excluded;
|
|
1798
|
+
next = appendSelection(next, {
|
|
1799
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1800
|
+
harness: HARNESS_VERSION,
|
|
1801
|
+
by: "migration",
|
|
1802
|
+
released,
|
|
1803
|
+
kept: keptCatalog
|
|
1804
|
+
});
|
|
1805
|
+
return { log: next, droppedKeys: released, releasedCatalog };
|
|
1806
|
+
}
|
|
1807
|
+
function isValidInstallLogShape(value) {
|
|
1808
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1809
|
+
const v = value;
|
|
1810
|
+
const spec = v.spec;
|
|
1811
|
+
if (typeof spec !== "object" || spec === null) return false;
|
|
1812
|
+
if (!Array.isArray(spec.tracks)) return false;
|
|
1813
|
+
if (!Array.isArray(v.assets)) return false;
|
|
1814
|
+
if (typeof v.templates !== "object" || v.templates === null) return false;
|
|
1815
|
+
return true;
|
|
1816
|
+
}
|
|
1817
|
+
function corruptedInstallLogMessage(projectDir) {
|
|
1818
|
+
return `install log is corrupted at ${installLogPath(projectDir)} \u2014 if it holds git conflict markers, take one side whole; otherwise run install --reinstall (it forgets recorded exclusions and external assets)`;
|
|
1819
|
+
}
|
|
1820
|
+
function installLogPath(projectDir) {
|
|
1821
|
+
return join4(projectDir, INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
|
|
1822
|
+
}
|
|
1823
|
+
function legacyInstallLogPath(projectDir) {
|
|
1824
|
+
return join4(projectDir, LEGACY_INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/project-claude-merge.ts
|
|
1828
|
+
init_esm_shims();
|
|
1829
|
+
var HARNESS_ANCHOR_FILE = "CLAUDE-uzys-harness.md";
|
|
1830
|
+
var HARNESS_IMPORT_LINE = `@${HARNESS_ANCHOR_FILE}`;
|
|
1831
|
+
var IMPORT_MARKER_START = "<!-- uzys-harness:import:start -->";
|
|
1832
|
+
var IMPORT_MARKER_END = "<!-- uzys-harness:import:end -->";
|
|
1833
|
+
var CONTINUOUS_SKILLS_HEADING = "## Skills that apply continuously";
|
|
1834
|
+
function renderContinuousSkillsNote(selectedInternalSkills) {
|
|
1835
|
+
const lines = CONTINUOUS_SKILLS.filter((s) => selectedInternalSkills.includes(s.id)).map(
|
|
1836
|
+
(s) => `- \`${s.id}\` \u2014 ${s.whenToApply}.`
|
|
1837
|
+
);
|
|
1838
|
+
if (lines.length === 0) return "";
|
|
1839
|
+
return [
|
|
1840
|
+
CONTINUOUS_SKILLS_HEADING,
|
|
1841
|
+
"",
|
|
1842
|
+
"These installed skills apply to every response or delegation; no prompt ever looks like their job, so open them from this list:",
|
|
1843
|
+
"",
|
|
1844
|
+
...lines
|
|
1845
|
+
].join("\n");
|
|
1846
|
+
}
|
|
1847
|
+
function withContinuousSkillsNote(scaffold, selectedInternalSkills) {
|
|
1848
|
+
const note = renderContinuousSkillsNote(selectedInternalSkills);
|
|
1849
|
+
return note ? `${scaffold}
|
|
1850
|
+
|
|
1851
|
+
${note}` : scaffold;
|
|
1852
|
+
}
|
|
1853
|
+
function importBlock(continuousSkills) {
|
|
1854
|
+
const note = renderContinuousSkillsNote(continuousSkills);
|
|
1855
|
+
const body = note ? `${HARNESS_IMPORT_LINE}
|
|
1856
|
+
|
|
1857
|
+
${note}` : HARNESS_IMPORT_LINE;
|
|
1858
|
+
return `${IMPORT_MARKER_START}
|
|
1859
|
+
${body}
|
|
1860
|
+
${IMPORT_MARKER_END}`;
|
|
1861
|
+
}
|
|
1862
|
+
var TRACK_DISPLAY_NAMES = {
|
|
1863
|
+
base: "Base",
|
|
1864
|
+
tooling: "Tooling",
|
|
1865
|
+
"csr-supabase": "CSR Supabase",
|
|
1866
|
+
"csr-fastify": "CSR Fastify",
|
|
1867
|
+
"csr-fastapi": "CSR FastAPI",
|
|
1868
|
+
"ssr-htmx": "SSR HTMX",
|
|
1869
|
+
"ssr-nextjs": "SSR Next.js",
|
|
1870
|
+
data: "Data",
|
|
1871
|
+
executive: "Executive",
|
|
1872
|
+
full: "Full",
|
|
1873
|
+
"project-management": "Project Management",
|
|
1874
|
+
"growth-marketing": "Growth Marketing"
|
|
1875
|
+
};
|
|
1876
|
+
var FULL_EXPANSION = TRACKS.filter((t) => t !== "full");
|
|
1877
|
+
var FILL_SECTIONS = [
|
|
1878
|
+
"identity",
|
|
1879
|
+
"stack",
|
|
1880
|
+
"architecture",
|
|
1881
|
+
"installed-assets",
|
|
1882
|
+
"boundaries",
|
|
1883
|
+
"verify"
|
|
1884
|
+
];
|
|
1885
|
+
var FILL_SPECS = {
|
|
1886
|
+
identity: {
|
|
1887
|
+
title: "Identity & Purpose",
|
|
1888
|
+
prompt: `Replace the H1 title above with this project's real name, then state in 1-2 plain sentences what it does, who uses it, and why it exists. Sources: README.md, the package.json / pyproject.toml "description", docs/. Do NOT describe the harness itself. Delete this comment when done.`,
|
|
1889
|
+
placeholder: "what this project is, who it is for, and why it exists"
|
|
1890
|
+
},
|
|
1891
|
+
stack: {
|
|
1892
|
+
title: "Stack & Commands",
|
|
1893
|
+
prompt: "Replace the list below with this project's REAL stack. Inspect package.json / pyproject.toml / go.mod / Cargo.toml / Gemfile + lockfiles and any Makefile/justfile/package scripts. List the language(s)+versions, framework(s), package manager, and the exact install/build/test/run commands. Verify each command exists before writing it \u2014 never guess. Delete lines that do not apply, then delete this comment.",
|
|
1894
|
+
placeholder: "languages, runtimes, package manager, and the install/build/test/run commands"
|
|
1895
|
+
},
|
|
1896
|
+
architecture: {
|
|
1897
|
+
title: "Architecture & Layout",
|
|
1898
|
+
prompt: "Map this repository. List each top-level source directory and what it holds, the entry point(s), the 3-5 files a newcomer reads first, and how data/requests flow between the main layers. View the real tree (e.g. `git ls-files | sed 's#/.*##' | sort -u`) \u2014 do not assume a framework's conventional layout. Flag any generated/vendored directories that must never be hand-edited. If this is a single small module, say so in one line. Delete this comment when done.",
|
|
1899
|
+
placeholder: "where things live, the entry points, and how data flows between layers"
|
|
1900
|
+
},
|
|
1901
|
+
"installed-assets": {
|
|
1902
|
+
title: "Installed Harness Assets",
|
|
1903
|
+
prompt: "List the harness assets installed in this project (rules / skills / agents / commands) and add one line each on when to reach for it here. Verify against .claude/rules/*.md, .claude/skills/, .claude/agents/, .claude/commands/ (or this CLI's equivalent) and list ONLY assets whose file exists on disk \u2014 do not invent any. Do NOT restate the harness's working principles (they live in this project's harness anchor and rules layer) \u2014 cross-reference them instead. Delete this comment when done.",
|
|
1904
|
+
placeholder: "the installed rules/skills/agents/commands and when to use each"
|
|
1905
|
+
},
|
|
1906
|
+
boundaries: {
|
|
1907
|
+
title: "Boundaries \u2014 Always / Ask First / Never",
|
|
1908
|
+
prompt: `Fill the Always / Ask First / Never lists with this repo's real red lines. Read the CI config, CODEOWNERS, .gitignore, and release/deploy scripts. Never must include secrets, generated files, force-push, and direct commits to the default branch, plus any repo-specific "do not touch" directories. Every entry must be specific and enforceable here. Delete this comment when done.`,
|
|
1909
|
+
placeholder: "what to always do, ask before doing, and never do in this repo"
|
|
1910
|
+
},
|
|
1911
|
+
verify: {
|
|
1912
|
+
title: "Verification Gate",
|
|
1913
|
+
prompt: 'State the single command (or short sequence) that PROVES a change is safe here \u2014 the test/lint/typecheck/build gate you run before committing \u2014 plus the coverage/CI threshold and what "done" means. Sources: package scripts, CI workflow files, CONTRIBUTING. An agent must be able to self-verify without guessing. Delete this comment when done.',
|
|
1914
|
+
placeholder: "the single command that proves a change is safe, and the done bar"
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
function anchorLine(surface) {
|
|
1918
|
+
if (surface === "claude") {
|
|
1919
|
+
return `> The working principles live in this project's harness anchor \`${HARNESS_ANCHOR_FILE}\`, imported at the bottom of this file. Everything above that import is **project-specific context only**.`;
|
|
1920
|
+
}
|
|
1921
|
+
const self = surface === "antigravity-rule" ? "`.agents/rules/uzys-harness.md`" : "`AGENTS.md`";
|
|
1922
|
+
return `> **This file (${self}) is the harness anchor** \u2014 it carries the working principles in full, further down. Only the \`## \u2026\` sections immediately below are project-specific context for you to fill in.`;
|
|
1923
|
+
}
|
|
1924
|
+
function scaffoldBanner(surface) {
|
|
1925
|
+
return [
|
|
1926
|
+
"> \u2699\uFE0F **SCAFFOLD \u2014 not filled in yet.** The sections below are a fill-in template for THIS project, not verified facts.",
|
|
1927
|
+
"> To fill: open this file and paste each `<!-- FILL: \u2026 -->` comment's instruction into your coding agent (e.g. Claude Code) \u2014 it will inspect the real repo and write the section. You can also fill them by hand; the comments are the instructions.",
|
|
1928
|
+
anchorLine(surface)
|
|
1929
|
+
].join("\n");
|
|
1930
|
+
}
|
|
1931
|
+
function renderFillScaffold(surface = "claude") {
|
|
1932
|
+
const blocks = FILL_SECTIONS.map((id) => {
|
|
1933
|
+
const spec = FILL_SPECS[id];
|
|
1934
|
+
return `## ${spec.title}
|
|
1935
|
+
|
|
1936
|
+
<!-- FILL:${id} \u2014 ${spec.prompt} -->
|
|
1937
|
+
|
|
1938
|
+
_(not filled yet \u2014 ${spec.placeholder})_`;
|
|
1939
|
+
});
|
|
1940
|
+
return `${scaffoldBanner(surface)}
|
|
1941
|
+
|
|
1942
|
+
${blocks.join("\n\n")}`;
|
|
1943
|
+
}
|
|
1944
|
+
function mergeProjectClaude(opts) {
|
|
1945
|
+
const expanded = expandTracks(opts.tracks);
|
|
1946
|
+
const trackList = expanded.map((t) => TRACK_DISPLAY_NAMES[t]).join(", ");
|
|
1947
|
+
const header = `# ${opts.projectName}
|
|
1948
|
+
|
|
1949
|
+
> Active track(s): ${trackList}`;
|
|
1950
|
+
return `${header}
|
|
1951
|
+
|
|
1952
|
+
${opts.projectContext ?? renderFillScaffold()}
|
|
1953
|
+
`;
|
|
1954
|
+
}
|
|
1955
|
+
function upsertHarnessImport(existing, opts) {
|
|
1956
|
+
const block = importBlock(opts.continuousSkills ?? []);
|
|
1957
|
+
if (existing === null) {
|
|
1958
|
+
return `${mergeProjectClaude(opts)}
|
|
1959
|
+
${block}
|
|
1960
|
+
`;
|
|
1961
|
+
}
|
|
1962
|
+
const start = existing.indexOf(IMPORT_MARKER_START);
|
|
1963
|
+
if (start !== -1) {
|
|
1964
|
+
const endAt = existing.indexOf(IMPORT_MARKER_END, start);
|
|
1965
|
+
if (endAt === -1) return existing;
|
|
1966
|
+
const current = existing.slice(start, endAt + IMPORT_MARKER_END.length);
|
|
1967
|
+
if (current === block) return existing;
|
|
1968
|
+
return `${existing.slice(0, start)}${block}${existing.slice(endAt + IMPORT_MARKER_END.length)}`;
|
|
1969
|
+
}
|
|
1970
|
+
if (hasHarnessImport(existing)) {
|
|
1971
|
+
return existing;
|
|
1972
|
+
}
|
|
1973
|
+
const body = existing.endsWith("\n") ? existing : `${existing}
|
|
1974
|
+
`;
|
|
1975
|
+
return `${body}
|
|
1976
|
+
${block}
|
|
1977
|
+
`;
|
|
1978
|
+
}
|
|
1979
|
+
function stripHarnessImport(text) {
|
|
1980
|
+
if (!text.includes(IMPORT_MARKER_START) && !text.includes(IMPORT_MARKER_END)) return null;
|
|
1981
|
+
const endsWithNewline = text.endsWith("\n");
|
|
1982
|
+
const out = [];
|
|
1983
|
+
const lines = text.split("\n");
|
|
1984
|
+
let i = 0;
|
|
1985
|
+
while (i < lines.length) {
|
|
1986
|
+
const line = lines[i] ?? "";
|
|
1987
|
+
if (line.includes(IMPORT_MARKER_START)) {
|
|
1988
|
+
let j = i;
|
|
1989
|
+
while (j < lines.length && !(lines[j] ?? "").includes(IMPORT_MARKER_END)) j++;
|
|
1990
|
+
i = j < lines.length ? j + 1 : i + 1;
|
|
1991
|
+
if (out.length > 0 && out[out.length - 1] === "") out.pop();
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
if (line.includes(IMPORT_MARKER_END) || line.trim() === HARNESS_IMPORT_LINE) {
|
|
1995
|
+
i++;
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
out.push(line);
|
|
1999
|
+
i++;
|
|
2000
|
+
}
|
|
2001
|
+
let result = out.join("\n");
|
|
2002
|
+
if (endsWithNewline && result !== "" && !result.endsWith("\n")) result += "\n";
|
|
2003
|
+
if (!endsWithNewline && result.endsWith("\n")) result = result.slice(0, -1);
|
|
2004
|
+
return result === text ? null : result;
|
|
2005
|
+
}
|
|
2006
|
+
function hasHarnessImport(text) {
|
|
2007
|
+
if (text.includes(IMPORT_MARKER_START)) return true;
|
|
2008
|
+
let inFence = false;
|
|
2009
|
+
for (const line of text.split("\n")) {
|
|
2010
|
+
if (/^\s*(?:```|~~~)/.test(line)) {
|
|
2011
|
+
inFence = !inFence;
|
|
2012
|
+
continue;
|
|
1721
2013
|
}
|
|
1722
|
-
return
|
|
1723
|
-
} catch {
|
|
1724
|
-
return null;
|
|
2014
|
+
if (!inFence && line.trim() === HARNESS_IMPORT_LINE) return true;
|
|
1725
2015
|
}
|
|
2016
|
+
return false;
|
|
1726
2017
|
}
|
|
1727
|
-
function
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
return
|
|
2018
|
+
function expandTracks(tracks) {
|
|
2019
|
+
if (tracks.includes("full")) {
|
|
2020
|
+
return FULL_EXPANSION;
|
|
2021
|
+
}
|
|
2022
|
+
return tracks;
|
|
1732
2023
|
}
|
|
1733
2024
|
|
|
1734
2025
|
// src/manifest.ts
|
|
@@ -1743,7 +2034,6 @@ function buildAssetSpec(ctx) {
|
|
|
1743
2034
|
}
|
|
1744
2035
|
var all = () => true;
|
|
1745
2036
|
var dev = (s) => hasDevTrack(s.tracks);
|
|
1746
|
-
var ui = (s) => hasUiTrack(s.tracks);
|
|
1747
2037
|
var onTracks = (pattern) => (s) => anyTrack(s.tracks, pattern);
|
|
1748
2038
|
var COMMON_RULES = ["git-policy", "change-management", "doc-governance"];
|
|
1749
2039
|
var DEV_RULES = ["test-policy", "ship-checklist"];
|
|
@@ -1856,7 +2146,6 @@ var RETIRED_AGENT_IDS = RETIRED_PATHS.filter(
|
|
|
1856
2146
|
var ALWAYS_HOOKS = ["session-start.sh", "protect-files.sh"];
|
|
1857
2147
|
var COMMON_SKILL_DIRS = [];
|
|
1858
2148
|
var DEV_SKILL_DIRS = [];
|
|
1859
|
-
var UI_SKILL_DIRS = ["ui-visual-review"];
|
|
1860
2149
|
function isCliNeutralTarget(target) {
|
|
1861
2150
|
return target.startsWith(`${INSTALL_LOG_DIR}/`);
|
|
1862
2151
|
}
|
|
@@ -1931,7 +2220,14 @@ function buildManifest(spec) {
|
|
|
1931
2220
|
source: `skills/${sd}`,
|
|
1932
2221
|
target: `.claude/skills/${sd}`,
|
|
1933
2222
|
type: "dir",
|
|
1934
|
-
|
|
2223
|
+
// #602 — selectedInternalSkills 가 SSOT(위저드 해제·--without 반영). `tracks` 만 담긴
|
|
2224
|
+
// 스펙(listBaselineTargets·위저드 1단계 등)에서는 카탈로그 조건으로 판정한다 —
|
|
2225
|
+
// buildAssetSpec 와 같은 isAssetSelected 라 두 번 적는 사본이 아니라 같은 원천이다.
|
|
2226
|
+
applies: (s) => s.selectedInternalSkills !== void 0 ? s.selectedInternalSkills.includes(sd) : (
|
|
2227
|
+
// #602 폴백은 트랙 추종 번들 스킬 한정 — 카탈로그 옵트인(compaction-handoff 등)은
|
|
2228
|
+
// 선택 목록 없이는 깔리지 않는다(위저드 해제·--without 의 SSOT 를 우회하는 금이 된다).
|
|
2229
|
+
sd === "ui-visual-review" && isAssetSelected(sd, { tracks: s.tracks, options: { withCodexTrust: false } })
|
|
2230
|
+
)
|
|
1935
2231
|
});
|
|
1936
2232
|
}
|
|
1937
2233
|
for (const sd of DEV_SKILL_DIRS) {
|
|
@@ -1942,14 +2238,6 @@ function buildManifest(spec) {
|
|
|
1942
2238
|
applies: dev
|
|
1943
2239
|
});
|
|
1944
2240
|
}
|
|
1945
|
-
for (const sd of UI_SKILL_DIRS) {
|
|
1946
|
-
m.push({
|
|
1947
|
-
source: `skills/${sd}`,
|
|
1948
|
-
target: `.claude/skills/${sd}`,
|
|
1949
|
-
type: "dir",
|
|
1950
|
-
applies: ui
|
|
1951
|
-
});
|
|
1952
|
-
}
|
|
1953
2241
|
for (const h of ALWAYS_HOOKS) {
|
|
1954
2242
|
m.push({
|
|
1955
2243
|
source: `hooks/${h}`,
|
|
@@ -1967,6 +2255,212 @@ function buildManifest(spec) {
|
|
|
1967
2255
|
return m;
|
|
1968
2256
|
}
|
|
1969
2257
|
|
|
2258
|
+
// src/context-cost.ts
|
|
2259
|
+
init_esm_shims();
|
|
2260
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
2261
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
2262
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2263
|
+
var CHARS_PER_TOKEN = 4;
|
|
2264
|
+
function estimateTokens(chars) {
|
|
2265
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
2266
|
+
}
|
|
2267
|
+
function resolveBundleRoot() {
|
|
2268
|
+
return resolve2(fileURLToPath2(new URL(".", import.meta.url)), "..");
|
|
2269
|
+
}
|
|
2270
|
+
function extractFrontmatter(content) {
|
|
2271
|
+
const m = content.match(/^?---\r?\n([\s\S]*?)\r?\n---/);
|
|
2272
|
+
return m?.[1] ?? null;
|
|
2273
|
+
}
|
|
2274
|
+
function assetDescriptorTokens(assetId, root = resolveBundleRoot()) {
|
|
2275
|
+
const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
|
|
2276
|
+
if (!asset || asset.method.kind !== "internal") return null;
|
|
2277
|
+
const skillMd = join5(root, "templates", "skills", asset.method.key, "SKILL.md");
|
|
2278
|
+
if (!existsSync3(skillMd)) return null;
|
|
2279
|
+
const fm = extractFrontmatter(readFileSync4(skillMd, "utf8"));
|
|
2280
|
+
return fm === null ? null : estimateTokens(fm.length);
|
|
2281
|
+
}
|
|
2282
|
+
function assetBodyTokens(assetId, root = resolveBundleRoot()) {
|
|
2283
|
+
const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
|
|
2284
|
+
if (!asset || asset.method.kind !== "internal") return null;
|
|
2285
|
+
const skillMd = join5(root, "templates", "skills", asset.method.key, "SKILL.md");
|
|
2286
|
+
if (!existsSync3(skillMd)) return null;
|
|
2287
|
+
const content = readFileSync4(skillMd, "utf8");
|
|
2288
|
+
const fm = extractFrontmatter(content);
|
|
2289
|
+
if (fm === null) return estimateTokens(content.length);
|
|
2290
|
+
const close = content.indexOf("\n---", content.indexOf(fm) + fm.length);
|
|
2291
|
+
const body = close === -1 ? "" : content.slice(close + "\n---".length);
|
|
2292
|
+
return estimateTokens(body.trim().length);
|
|
2293
|
+
}
|
|
2294
|
+
function assetCostRows(assetIds, root = resolveBundleRoot()) {
|
|
2295
|
+
return assetIds.map((id) => ({
|
|
2296
|
+
id,
|
|
2297
|
+
descriptorTokens: assetDescriptorTokens(id, root),
|
|
2298
|
+
bodyTokens: assetBodyTokens(id, root)
|
|
2299
|
+
})).sort((a, b) => (b.bodyTokens ?? -1) - (a.bodyTokens ?? -1));
|
|
2300
|
+
}
|
|
2301
|
+
function makeResidentCost(base) {
|
|
2302
|
+
return {
|
|
2303
|
+
...base,
|
|
2304
|
+
contextFile: base.contextFile ?? "CLAUDE.md",
|
|
2305
|
+
perSkillDescriptor: base.perSkillDescriptor ?? {},
|
|
2306
|
+
total: base.rules + base.projectClaudeMd + base.skillDescriptors + base.agentDescriptors,
|
|
2307
|
+
directive: {
|
|
2308
|
+
items: base.items.rules + base.items.claudeMd,
|
|
2309
|
+
tokens: base.rules + base.projectClaudeMd
|
|
2310
|
+
},
|
|
2311
|
+
firing: {
|
|
2312
|
+
items: base.items.skills + base.items.agents,
|
|
2313
|
+
tokens: base.skillDescriptors + base.agentDescriptors
|
|
2314
|
+
}
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
function fileTokens(path2) {
|
|
2318
|
+
return existsSync3(path2) ? estimateTokens(readFileSync4(path2, "utf8").trim().length) : 0;
|
|
2319
|
+
}
|
|
2320
|
+
function descriptorTokens(path2) {
|
|
2321
|
+
if (!existsSync3(path2)) return 0;
|
|
2322
|
+
const fm = extractFrontmatter(readFileSync4(path2, "utf8"));
|
|
2323
|
+
return fm === null ? 0 : estimateTokens(fm.length);
|
|
2324
|
+
}
|
|
2325
|
+
function anchorSurface(cli) {
|
|
2326
|
+
if (cli.includes("claude") || cli.length === 0) {
|
|
2327
|
+
return { label: "CLAUDE.md", files: 2, scaffold: "claude" };
|
|
2328
|
+
}
|
|
2329
|
+
if (cli.includes("codex") || cli.includes("opencode")) {
|
|
2330
|
+
return { label: "AGENTS.md", files: 1, scaffold: "agents-md" };
|
|
2331
|
+
}
|
|
2332
|
+
return { label: ".agents/rules/uzys-harness.md", files: 1, scaffold: "antigravity-rule" };
|
|
2333
|
+
}
|
|
2334
|
+
function residentCost(entries, root = resolveBundleRoot(), cli = ["claude"], contextFiles) {
|
|
2335
|
+
const tpl = (source) => join5(root, "templates", source);
|
|
2336
|
+
const measured = (e) => e.file ?? tpl(e.source);
|
|
2337
|
+
const perSkill = {};
|
|
2338
|
+
let rules = 0;
|
|
2339
|
+
let skillDescriptors = 0;
|
|
2340
|
+
let agentDescriptors = 0;
|
|
2341
|
+
let ruleItems = 0;
|
|
2342
|
+
let skillItems = 0;
|
|
2343
|
+
let agentItems = 0;
|
|
2344
|
+
for (const e of entries) {
|
|
2345
|
+
if (e.target.startsWith(".claude/rules/")) {
|
|
2346
|
+
rules += fileTokens(measured(e));
|
|
2347
|
+
ruleItems += 1;
|
|
2348
|
+
} else if (e.target.startsWith(".claude/agents/")) {
|
|
2349
|
+
agentDescriptors += descriptorTokens(measured(e));
|
|
2350
|
+
agentItems += 1;
|
|
2351
|
+
} else if (e.target.startsWith(".claude/skills/")) {
|
|
2352
|
+
const t = descriptorTokens(join5(measured(e), "SKILL.md"));
|
|
2353
|
+
skillDescriptors += t;
|
|
2354
|
+
skillItems += 1;
|
|
2355
|
+
const id = /\.claude\/skills\/([^/]+)/.exec(e.target)?.[1];
|
|
2356
|
+
if (id !== void 0) perSkill[id] = (perSkill[id] ?? 0) + t;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
const harnessAnchor = fileTokens(join5(root, "templates", "CLAUDE.md"));
|
|
2360
|
+
const surface = anchorSurface(cli);
|
|
2361
|
+
const projectScaffold = estimateTokens(
|
|
2362
|
+
withContinuousSkillsNote(
|
|
2363
|
+
renderFillScaffold(surface.scaffold),
|
|
2364
|
+
CONTINUOUS_SKILLS.map((s) => s.id)
|
|
2365
|
+
).trim().length
|
|
2366
|
+
);
|
|
2367
|
+
const onDisk = contextFiles?.map(fileTokens).filter((t) => t > 0);
|
|
2368
|
+
const projectClaudeMd = onDisk === void 0 ? harnessAnchor + projectScaffold : onDisk.reduce((a, b) => a + b, 0);
|
|
2369
|
+
const claudeMdItems = onDisk !== void 0 ? onDisk.length : surface.files === 1 ? 1 : (harnessAnchor > 0 ? 1 : 0) + (projectScaffold > 0 ? 1 : 0);
|
|
2370
|
+
return makeResidentCost({
|
|
2371
|
+
rules,
|
|
2372
|
+
projectClaudeMd,
|
|
2373
|
+
skillDescriptors,
|
|
2374
|
+
agentDescriptors,
|
|
2375
|
+
perSkillDescriptor: perSkill,
|
|
2376
|
+
contextFile: surface.label,
|
|
2377
|
+
items: {
|
|
2378
|
+
rules: ruleItems,
|
|
2379
|
+
skills: skillItems,
|
|
2380
|
+
agents: agentItems,
|
|
2381
|
+
claudeMd: claudeMdItems,
|
|
2382
|
+
total: ruleItems + skillItems + agentItems + claudeMdItems
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
function summarizeContextCost(assetIds, root = resolveBundleRoot()) {
|
|
2387
|
+
let measuredTokens = 0;
|
|
2388
|
+
let measuredCount = 0;
|
|
2389
|
+
let unmeasuredCount = 0;
|
|
2390
|
+
for (const id of assetIds) {
|
|
2391
|
+
const tokens = assetDescriptorTokens(id, root);
|
|
2392
|
+
if (tokens === null) unmeasuredCount += 1;
|
|
2393
|
+
else {
|
|
2394
|
+
measuredTokens += tokens;
|
|
2395
|
+
measuredCount += 1;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
return { measuredTokens, measuredCount, unmeasuredCount };
|
|
2399
|
+
}
|
|
2400
|
+
function formatResidentCostLine(r, unmeasuredCount) {
|
|
2401
|
+
if (r.total === 0) return null;
|
|
2402
|
+
const parts = [
|
|
2403
|
+
`directives ${r.directive.items} ~${r.directive.tokens}`,
|
|
2404
|
+
`[rules ${r.items.rules} ~${r.rules} \xB7 ${r.contextFile} ${r.items.claudeMd} ~${r.projectClaudeMd}]`,
|
|
2405
|
+
`triggers ${r.firing.items} ~${r.firing.tokens}`,
|
|
2406
|
+
`[skills ${r.items.skills} ~${r.skillDescriptors} \xB7 agents ${r.items.agents} ~${r.agentDescriptors}]`
|
|
2407
|
+
].join(" \xB7 ");
|
|
2408
|
+
const external = unmeasuredCount > 0 ? ` \xB7 ${unmeasuredCount} external asset${unmeasuredCount === 1 ? "" : "s"} unmeasured` : "";
|
|
2409
|
+
return `session-start context cost: ${r.items.total} items resident \xB7 ~${r.total} tokens/session (${parts}${external})`;
|
|
2410
|
+
}
|
|
2411
|
+
var RESIDENT_ROWS = [
|
|
2412
|
+
{
|
|
2413
|
+
// ADR-083 — 축 소계를 **행으로** 낸다. 소계가 없으면 읽는 사람이 네 줄을 머리로 더해야
|
|
2414
|
+
// 하고, 그러면 "무엇을 줄여야 하나"가 표에서 안 보인다.
|
|
2415
|
+
label: "\uC9C0\uC2DC\uBB38",
|
|
2416
|
+
gap: 13,
|
|
2417
|
+
count: (r) => r.directive.items,
|
|
2418
|
+
tokens: (r) => r.directive.tokens
|
|
2419
|
+
},
|
|
2420
|
+
{ label: " rules", gap: 12, count: (r) => r.items.rules, tokens: (r) => r.rules },
|
|
2421
|
+
{
|
|
2422
|
+
// 한 행에 두 파일(앵커 + 스캐폴드). 개수 열이 2 를 뱉어 그 사실을 드러낸다 — 라벨로
|
|
2423
|
+
// 하나만 이름 붙이면 v26.140.0 이전과 같은 거짓 라벨이 된다.
|
|
2424
|
+
label: " CLAUDE.md",
|
|
2425
|
+
gap: 8,
|
|
2426
|
+
count: (r) => r.items.claudeMd,
|
|
2427
|
+
tokens: (r) => r.projectClaudeMd
|
|
2428
|
+
},
|
|
2429
|
+
{
|
|
2430
|
+
label: "\uBC1C\uD654 \uD45C\uBA74",
|
|
2431
|
+
gap: 10,
|
|
2432
|
+
count: (r) => r.firing.items,
|
|
2433
|
+
tokens: (r) => r.firing.tokens
|
|
2434
|
+
},
|
|
2435
|
+
{
|
|
2436
|
+
label: " skill descriptors",
|
|
2437
|
+
gap: 0,
|
|
2438
|
+
count: (r) => r.items.skills,
|
|
2439
|
+
tokens: (r) => r.skillDescriptors
|
|
2440
|
+
},
|
|
2441
|
+
{
|
|
2442
|
+
label: " agent descriptors",
|
|
2443
|
+
gap: 0,
|
|
2444
|
+
count: (r) => r.items.agents,
|
|
2445
|
+
tokens: (r) => r.agentDescriptors
|
|
2446
|
+
}
|
|
2447
|
+
];
|
|
2448
|
+
function formatResidentCostBlock(r) {
|
|
2449
|
+
const n = (v) => String(v).padStart(3);
|
|
2450
|
+
return [
|
|
2451
|
+
" \uAC1C\uC218 \uD1A0\uD070",
|
|
2452
|
+
...RESIDENT_ROWS.map(
|
|
2453
|
+
(row) => ` ${row.label}${" ".repeat(row.gap)}${n(row.count(r))}\uAC1C ~${row.tokens(r)}`
|
|
2454
|
+
),
|
|
2455
|
+
" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
2456
|
+
` \uC0C1\uC8FC \uD569\uACC4 ${n(r.items.total)}\uAC1C \uC0C1\uC8FC \xB7 ~${r.total} tokens/\uC138\uC158`,
|
|
2457
|
+
"",
|
|
2458
|
+
" \uC9C0\uC2DC\uBB38 = \uC6B0\uB9AC\uAC00 \uD56D\uC0C1 \uC77D\uD788\uAC8C \uB123\uC740 \uAC83. **\uC791\uC744\uC218\uB85D \uC88B\uC740 \uAC12\uC774 \uC544\uB2C8\uB2E4** \u2014 \uD310\uB2E8\uC740",
|
|
2459
|
+
" '\uBD88\uD544\uC694\uD55C \uC9C0\uC2DC\uBB38\uC774 \uC788\uB098'\uB85C \uD55C\uB2E4. ratchet \uC740 \uC870\uC6A9\uD55C \uC99D\uAC00\uB9CC \uB9C9\uB294\uB2E4.",
|
|
2460
|
+
" \uBC1C\uD654 \uD45C\uBA74 = \uC2A4\uD0AC\xB7\uC5D0\uC774\uC804\uD2B8\uAC00 \uBD88\uB9AC\uB824\uACE0 \uC0C1\uC8FC\uC2DC\uD0A4\uB294 descriptor. \uAE4E\uC73C\uBA74 \uC548 \uBD88\uB9B0\uB2E4."
|
|
2461
|
+
];
|
|
2462
|
+
}
|
|
2463
|
+
|
|
1970
2464
|
// src/categories.ts
|
|
1971
2465
|
init_esm_shims();
|
|
1972
2466
|
var CATEGORIES = [
|
|
@@ -1996,8 +2490,10 @@ export {
|
|
|
1996
2490
|
__commonJS,
|
|
1997
2491
|
__toESM,
|
|
1998
2492
|
init_esm_shims,
|
|
1999
|
-
|
|
2000
|
-
|
|
2493
|
+
package_default,
|
|
2494
|
+
SHARED_FILES,
|
|
2495
|
+
isKeyId,
|
|
2496
|
+
sharedPathOfKeyId,
|
|
2001
2497
|
TRACKS,
|
|
2002
2498
|
isTrack,
|
|
2003
2499
|
CLI_BASES,
|
|
@@ -2006,6 +2502,11 @@ export {
|
|
|
2006
2502
|
resolveScope,
|
|
2007
2503
|
DEFAULT_OPTIONS,
|
|
2008
2504
|
UPDATE_GROUPS,
|
|
2505
|
+
CLI_BASE_SORT_ORDER,
|
|
2506
|
+
parseCliTargets,
|
|
2507
|
+
targetsInclude,
|
|
2508
|
+
anyTrack,
|
|
2509
|
+
hasUiTrack,
|
|
2009
2510
|
EXTERNAL_ASSETS,
|
|
2010
2511
|
DEV_METHOD_SKILL_IDS,
|
|
2011
2512
|
INTERNAL_BUNDLED_SKILL_IDS,
|
|
@@ -2019,6 +2520,22 @@ export {
|
|
|
2019
2520
|
assetCliSupport,
|
|
2020
2521
|
assetReachesCli,
|
|
2021
2522
|
filterApplicableAssets,
|
|
2523
|
+
isOutsideProject,
|
|
2524
|
+
outsideProjectTarget,
|
|
2525
|
+
outsideLinkAt,
|
|
2526
|
+
createOutsideGuard,
|
|
2527
|
+
mergeOutside,
|
|
2528
|
+
ensureDir,
|
|
2529
|
+
copyFile,
|
|
2530
|
+
copyDir,
|
|
2531
|
+
backupDir,
|
|
2532
|
+
copyBackupDir,
|
|
2533
|
+
backupFile,
|
|
2534
|
+
backupOriginal,
|
|
2535
|
+
listFilesRecursive,
|
|
2536
|
+
ensureProjectSkeleton,
|
|
2537
|
+
dirTreesIdentical,
|
|
2538
|
+
backupIfLossyUtf8,
|
|
2022
2539
|
HARNESS_ANCHOR_FILE,
|
|
2023
2540
|
HARNESS_IMPORT_LINE,
|
|
2024
2541
|
CONTINUOUS_SKILLS_HEADING,
|
|
@@ -2028,31 +2545,24 @@ export {
|
|
|
2028
2545
|
renderFillScaffold,
|
|
2029
2546
|
upsertHarnessImport,
|
|
2030
2547
|
stripHarnessImport,
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
targetsInclude,
|
|
2043
|
-
ensureDir,
|
|
2044
|
-
copyFile,
|
|
2045
|
-
copyDir,
|
|
2046
|
-
backupDir,
|
|
2047
|
-
copyBackupDir,
|
|
2048
|
-
backupFile,
|
|
2049
|
-
listFilesRecursive,
|
|
2050
|
-
ensureProjectSkeleton,
|
|
2548
|
+
buildAssetSpec,
|
|
2549
|
+
resolveRules,
|
|
2550
|
+
TRACK_AGENTS,
|
|
2551
|
+
RETIRED_AGENTS,
|
|
2552
|
+
RETIRED_PATHS,
|
|
2553
|
+
RETIRED_AGENT_IDS,
|
|
2554
|
+
isCliNeutralTarget,
|
|
2555
|
+
buildManifest,
|
|
2556
|
+
memoTargets,
|
|
2557
|
+
claudeTargets,
|
|
2558
|
+
claudeRecorded,
|
|
2051
2559
|
INSTALL_LOG_FILENAME,
|
|
2052
2560
|
INSTALL_LOG_DIR,
|
|
2561
|
+
LEGACY_INSTALL_LOG_DIR,
|
|
2053
2562
|
POLICY_DIRS,
|
|
2054
2563
|
installedClis,
|
|
2055
2564
|
buildInstallLog,
|
|
2565
|
+
mergeRootFiles,
|
|
2056
2566
|
hashContent,
|
|
2057
2567
|
isHarnessOwned,
|
|
2058
2568
|
mergeExternalFiles,
|
|
@@ -2060,18 +2570,23 @@ export {
|
|
|
2060
2570
|
collectPolicyHashes,
|
|
2061
2571
|
writeInstallLog,
|
|
2062
2572
|
readInstallLog,
|
|
2573
|
+
readInstallLogStatus,
|
|
2574
|
+
legacyDroppedKeys,
|
|
2575
|
+
legacyReleasedCatalog,
|
|
2576
|
+
HARNESS_VERSION,
|
|
2577
|
+
appendSelection,
|
|
2578
|
+
corruptedInstallLogMessage,
|
|
2063
2579
|
installLogPath,
|
|
2064
2580
|
legacyInstallLogPath,
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
buildManifest,
|
|
2581
|
+
estimateTokens,
|
|
2582
|
+
resolveBundleRoot,
|
|
2583
|
+
extractFrontmatter,
|
|
2584
|
+
assetCostRows,
|
|
2585
|
+
residentCost,
|
|
2586
|
+
summarizeContextCost,
|
|
2587
|
+
formatResidentCostLine,
|
|
2588
|
+
formatResidentCostBlock,
|
|
2074
2589
|
CATEGORIES,
|
|
2075
2590
|
CATEGORY_TITLES
|
|
2076
2591
|
};
|
|
2077
|
-
//# sourceMappingURL=chunk-
|
|
2592
|
+
//# sourceMappingURL=chunk-X7NSQHXL.js.map
|