easy-coding-harness 0.5.0 → 0.5.2-beta.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/CHANGELOG.md +7 -0
- package/dist/cli.js +422 -25
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/settings.json +4 -4
- package/templates/codex/hooks.json +2 -2
- package/templates/common/bundled-skills/ec-init/SKILL.md +12 -0
- package/templates/qoder/settings.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
+
## 0.5.2-beta.0
|
|
10
|
+
|
|
11
|
+
- 修复 Claude Code hook 命令使用直接相对路径导致在子目录 cwd 下找不到 `.claude/hooks/*.py` 的问题;Claude、Codex、Qoder 的 hook 配置现在使用可共享的 portable relative launcher,并绑定 `.easy-coding/config.yaml` 的 `project.id`,避免把本机仓库绝对路径写入可提交配置,同时防止 supermodule 父仓 hook 被子仓 cwd 错路由。
|
|
12
|
+
- `easy-coding upgrade` 可直接刷新存量 0.5.0 项目和已安装 0.5.1 beta 绝对路径配置;同版本也会修复缺失、重复、事件错位或 stale 的托管 hook 注册。
|
|
13
|
+
- `easy-coding clear` 和 `install-manifest.json` 兼容 portable launcher、旧直接相对命令和旧绝对 hook 命令,清理时仍按项目相对 hook 路径识别托管注册项。
|
|
14
|
+
- npm `0.5.1` 已发布不可覆盖,本版本作为 beta 修复包发布,用于替代原 `0.5.1` beta。
|
|
15
|
+
|
|
9
16
|
## 0.5.0
|
|
10
17
|
|
|
11
18
|
- 新增 supermodule 支持:`easy-coding init` 会识别 `.gitmodules`,父仓必装,已检出的一级子仓可按交互或参数选择分层安装。
|
package/dist/cli.js
CHANGED
|
@@ -61,6 +61,7 @@ function colorizeBanner(art) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// src/utils/config-yaml.ts
|
|
64
|
+
import { randomUUID } from "crypto";
|
|
64
65
|
import { readFile as readFile2 } from "fs/promises";
|
|
65
66
|
import YAML, { isScalar, isSeq, parseDocument } from "yaml";
|
|
66
67
|
|
|
@@ -108,6 +109,7 @@ function createDefaultConfig(params) {
|
|
|
108
109
|
harness_version: params.harnessVersion,
|
|
109
110
|
agents: params.agents,
|
|
110
111
|
project: {
|
|
112
|
+
id: params.projectId ?? createProjectId(),
|
|
111
113
|
name: params.projectName
|
|
112
114
|
},
|
|
113
115
|
memory: {
|
|
@@ -138,6 +140,14 @@ async function readConfigYaml(filePath) {
|
|
|
138
140
|
const content = await readFile2(filePath, "utf8");
|
|
139
141
|
return YAML.parse(content);
|
|
140
142
|
}
|
|
143
|
+
async function readProjectIdIfExists(filePath) {
|
|
144
|
+
try {
|
|
145
|
+
const config = await readConfigYaml(filePath);
|
|
146
|
+
return typeof config.project?.id === "string" && config.project.id.trim() ? config.project.id : null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
141
151
|
async function updateConfigYaml(filePath, updater) {
|
|
142
152
|
const content = await readFile2(filePath, "utf8");
|
|
143
153
|
const document = parseDocument(content);
|
|
@@ -160,11 +170,27 @@ async function updateHarnessVersion(filePath, version) {
|
|
|
160
170
|
config.harness_version = version;
|
|
161
171
|
});
|
|
162
172
|
}
|
|
173
|
+
async function ensureProjectId(filePath) {
|
|
174
|
+
let projectId = "";
|
|
175
|
+
await updateConfigYaml(filePath, (config) => {
|
|
176
|
+
if (!config.project || typeof config.project !== "object") {
|
|
177
|
+
config.project = { id: createProjectId(), name: "" };
|
|
178
|
+
}
|
|
179
|
+
if (typeof config.project.id !== "string" || !config.project.id.trim()) {
|
|
180
|
+
config.project.id = createProjectId();
|
|
181
|
+
}
|
|
182
|
+
projectId = config.project.id;
|
|
183
|
+
});
|
|
184
|
+
return projectId;
|
|
185
|
+
}
|
|
163
186
|
async function updateSupermoduleConfig(filePath, supermodule) {
|
|
164
187
|
return updateConfigYaml(filePath, (config) => {
|
|
165
188
|
config.supermodule = supermodule;
|
|
166
189
|
});
|
|
167
190
|
}
|
|
191
|
+
function createProjectId() {
|
|
192
|
+
return `ec-${randomUUID()}`;
|
|
193
|
+
}
|
|
168
194
|
|
|
169
195
|
// src/utils/gitignore.ts
|
|
170
196
|
import path3 from "path";
|
|
@@ -350,7 +376,7 @@ async function writeInstallManifest(cwd, params) {
|
|
|
350
376
|
const registration = {
|
|
351
377
|
config_path: configPath2,
|
|
352
378
|
command: artifact.command,
|
|
353
|
-
hook_path: artifact.hookPath,
|
|
379
|
+
hook_path: artifact.hookPath ? normalizeHookPath(cwd, artifact.hookPath) : null,
|
|
354
380
|
platform: artifact.platform
|
|
355
381
|
};
|
|
356
382
|
hookRegistrations.set(`${configPath2}\0${normalizeCommand(artifact.command)}`, registration);
|
|
@@ -465,9 +491,75 @@ function collectHookCommands(hooks) {
|
|
|
465
491
|
return commands;
|
|
466
492
|
}
|
|
467
493
|
function extractHookPathFromCommand(command) {
|
|
468
|
-
|
|
494
|
+
return extractQuotedHookPath(command) ?? extractUnquotedHookPath(command);
|
|
495
|
+
}
|
|
496
|
+
function extractQuotedHookPath(command) {
|
|
497
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
498
|
+
const quote = command[index];
|
|
499
|
+
if (quote !== '"' && quote !== "'") {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (index > 0 && !/\s/.test(command[index - 1])) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const parsed = parseQuotedToken(command, index, quote);
|
|
506
|
+
if (!parsed) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const suffix = readPathSuffix(command, parsed.endIndex);
|
|
510
|
+
for (const candidate of [parsed.value + suffix, parsed.value]) {
|
|
511
|
+
if (isHookPythonPath(candidate)) {
|
|
512
|
+
return candidate;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
index = parsed.endIndex;
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
function parseQuotedToken(command, startIndex, quote) {
|
|
520
|
+
let value = "";
|
|
521
|
+
for (let index = startIndex + 1; index < command.length; index += 1) {
|
|
522
|
+
const char = command[index];
|
|
523
|
+
if (quote === '"' && char === "\\" && index + 1 < command.length) {
|
|
524
|
+
value += command[index + 1];
|
|
525
|
+
index += 1;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (char === quote) {
|
|
529
|
+
return { value, endIndex: index + 1 };
|
|
530
|
+
}
|
|
531
|
+
value += char;
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
function readPathSuffix(command, startIndex) {
|
|
536
|
+
if (command[startIndex] !== "/") {
|
|
537
|
+
return "";
|
|
538
|
+
}
|
|
539
|
+
let suffix = "";
|
|
540
|
+
for (let index = startIndex; index < command.length; index += 1) {
|
|
541
|
+
const char = command[index];
|
|
542
|
+
if (/\s/.test(char)) {
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
suffix += char;
|
|
546
|
+
}
|
|
547
|
+
return suffix;
|
|
548
|
+
}
|
|
549
|
+
function extractUnquotedHookPath(command) {
|
|
550
|
+
const match = command.match(/(?:^|\s)(\S+\/hooks\/\S+\.py)(?:\s|$)/);
|
|
469
551
|
return match?.[1] ?? null;
|
|
470
552
|
}
|
|
553
|
+
function isHookPythonPath(candidate) {
|
|
554
|
+
return /\/hooks\/\S+\.py$/.test(candidate);
|
|
555
|
+
}
|
|
556
|
+
function normalizeHookPath(cwd, hookPath) {
|
|
557
|
+
const normalized = hookPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
558
|
+
if (path4.isAbsolute(hookPath) || path4.posix.isAbsolute(normalized) || path4.win32.isAbsolute(hookPath)) {
|
|
559
|
+
return toProjectPath(cwd, hookPath);
|
|
560
|
+
}
|
|
561
|
+
return normalized;
|
|
562
|
+
}
|
|
471
563
|
function byPath(a, b) {
|
|
472
564
|
return a.path.localeCompare(b.path);
|
|
473
565
|
}
|
|
@@ -502,17 +594,21 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
|
502
594
|
const easyCodingDir = path6.join(cwd, EASY_CODING_DIR);
|
|
503
595
|
await ensureDir(easyCodingDir);
|
|
504
596
|
const configPath2 = path6.join(easyCodingDir, CONFIG_FILE);
|
|
597
|
+
let projectId = opts.projectId ?? createProjectId();
|
|
505
598
|
if (!await pathExists(configPath2)) {
|
|
506
599
|
const projectName = path6.basename(cwd);
|
|
507
600
|
await writeConfigYaml(
|
|
508
601
|
configPath2,
|
|
509
602
|
createDefaultConfig({
|
|
510
603
|
projectName,
|
|
604
|
+
projectId,
|
|
511
605
|
harnessVersion: VERSION,
|
|
512
606
|
agents,
|
|
513
607
|
supermodule: opts.supermodule
|
|
514
608
|
})
|
|
515
609
|
);
|
|
610
|
+
} else {
|
|
611
|
+
projectId = await ensureProjectId(configPath2);
|
|
516
612
|
}
|
|
517
613
|
await ensureDir(path6.join(easyCodingDir, "tasks"));
|
|
518
614
|
await ensureDir(path6.join(easyCodingDir, SESSIONS_DIR));
|
|
@@ -520,6 +616,7 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
|
520
616
|
await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
521
617
|
await writeMemoryScaffold(easyCodingDir);
|
|
522
618
|
await writeTemplatesScaffold(easyCodingDir);
|
|
619
|
+
return projectId;
|
|
523
620
|
}
|
|
524
621
|
async function writeTemplatesScaffold(easyCodingDir) {
|
|
525
622
|
const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
|
|
@@ -696,6 +793,89 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
|
|
|
696
793
|
}
|
|
697
794
|
return resolved;
|
|
698
795
|
}
|
|
796
|
+
async function withProjectInstallPaths(cwd, ctx, projectId) {
|
|
797
|
+
const resolvedProjectId = projectId ?? await readProjectIdIfExists(path8.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
|
|
798
|
+
return withInstallPaths(cwd, ctx, resolvedProjectId ?? void 0);
|
|
799
|
+
}
|
|
800
|
+
function withInstallPaths(cwd, ctx, projectId) {
|
|
801
|
+
return {
|
|
802
|
+
...ctx,
|
|
803
|
+
platform_hook_session_start_command: jsonStringContent(
|
|
804
|
+
renderHookCommand(cwd, ctx, "session-start.py", process.platform, projectId)
|
|
805
|
+
),
|
|
806
|
+
platform_hook_inject_workflow_state_command: jsonStringContent(
|
|
807
|
+
renderHookCommand(cwd, ctx, "inject-workflow-state.py", process.platform, projectId)
|
|
808
|
+
),
|
|
809
|
+
platform_hook_inject_subagent_context_command: jsonStringContent(
|
|
810
|
+
renderHookCommand(cwd, ctx, "inject-subagent-context.py", process.platform, projectId)
|
|
811
|
+
)
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function renderHookCommand(_cwd, ctx, scriptName, platform = process.platform, projectId) {
|
|
815
|
+
const launcher = `import base64;exec(base64.b64decode('${Buffer.from(HOOK_LAUNCHER_PYTHON).toString("base64")}').decode())`;
|
|
816
|
+
const rootIdArg = projectId ? ` ${shellDoubleQuoteArg(projectId, platform)}` : "";
|
|
817
|
+
return `${ctx.python_cmd} -c ${shellDoubleQuoteArg(launcher, platform)}${rootIdArg} ${ctx.platform_config_dir}/hooks/${scriptName}`;
|
|
818
|
+
}
|
|
819
|
+
function shellDoubleQuoteArg(value, platform = process.platform) {
|
|
820
|
+
if (platform === "win32") {
|
|
821
|
+
if (value.includes('"')) {
|
|
822
|
+
throw new Error("Windows hook paths cannot contain double quotes.");
|
|
823
|
+
}
|
|
824
|
+
return `"${value}"`;
|
|
825
|
+
}
|
|
826
|
+
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
827
|
+
}
|
|
828
|
+
function jsonStringContent(value) {
|
|
829
|
+
return JSON.stringify(value).slice(1, -1);
|
|
830
|
+
}
|
|
831
|
+
var HOOK_LAUNCHER_PYTHON = [
|
|
832
|
+
"import io,json,os,re,runpy,sys",
|
|
833
|
+
"from pathlib import Path",
|
|
834
|
+
"payload=sys.stdin.read()",
|
|
835
|
+
"try:",
|
|
836
|
+
" data=json.loads(payload or '{}')",
|
|
837
|
+
"except Exception:",
|
|
838
|
+
" data={}",
|
|
839
|
+
"expected_id=sys.argv[1] if len(sys.argv) > 2 else None",
|
|
840
|
+
"script_arg=sys.argv[2] if len(sys.argv) > 2 else (sys.argv[1] if len(sys.argv) > 1 else None)",
|
|
841
|
+
"def project_id(root):",
|
|
842
|
+
" try:",
|
|
843
|
+
" lines=(root / '.easy-coding' / 'config.yaml').read_text(encoding='utf-8').splitlines()",
|
|
844
|
+
" except Exception:",
|
|
845
|
+
" return None",
|
|
846
|
+
" in_project=False",
|
|
847
|
+
" for line in lines:",
|
|
848
|
+
" if not in_project:",
|
|
849
|
+
" if re.match(r'^project\\s*:\\s*(?:#.*)?$', line):",
|
|
850
|
+
" in_project=True",
|
|
851
|
+
" continue",
|
|
852
|
+
" if not line.strip() or line.lstrip().startswith('#'):",
|
|
853
|
+
" continue",
|
|
854
|
+
" if not line.startswith((' ', '\\t')):",
|
|
855
|
+
" return None",
|
|
856
|
+
` match=re.match(r"\\s+id\\s*:\\s*['\\"]?([^'\\"#\\s]+)", line)`,
|
|
857
|
+
" if match:",
|
|
858
|
+
" return match.group(1)",
|
|
859
|
+
" return None",
|
|
860
|
+
"def roots_from(raw):",
|
|
861
|
+
" if not raw:",
|
|
862
|
+
" return []",
|
|
863
|
+
" start=Path(raw).resolve()",
|
|
864
|
+
" return [candidate for candidate in (start, *start.parents) if (candidate / '.easy-coding').is_dir()]",
|
|
865
|
+
"sources=[os.environ.get('CLAUDE_PROJECT_DIR'), os.environ.get('QODER_PROJECT_DIR'), data.get('cwd'), os.getcwd()]",
|
|
866
|
+
"root=None",
|
|
867
|
+
"if expected_id:",
|
|
868
|
+
" root=next((candidate for raw in sources for candidate in roots_from(raw) if project_id(candidate) == expected_id), None)",
|
|
869
|
+
"else:",
|
|
870
|
+
" root=next((candidate for raw in sources for candidate in roots_from(raw)), None)",
|
|
871
|
+
"target=(root / script_arg) if root and script_arg else None",
|
|
872
|
+
"if target and target.is_file():",
|
|
873
|
+
" os.chdir(root)",
|
|
874
|
+
" data['cwd']=str(root)",
|
|
875
|
+
" sys.stdin=io.StringIO(json.dumps(data))",
|
|
876
|
+
" sys.path.insert(0, str(target.parent))",
|
|
877
|
+
" runpy.run_path(str(target), run_name='__main__')"
|
|
878
|
+
].join("\n");
|
|
699
879
|
async function resolveSkills(ctx) {
|
|
700
880
|
const skillsRoot = getTemplatePath("common", "skills");
|
|
701
881
|
const entries = await readdir2(skillsRoot);
|
|
@@ -851,7 +1031,7 @@ function stripTemplateExtension(fileName) {
|
|
|
851
1031
|
async function configureClaude(cwd, opts = {}) {
|
|
852
1032
|
const platform = "claude-code";
|
|
853
1033
|
const meta = PLATFORM_META[platform];
|
|
854
|
-
const ctx = meta.templateContext;
|
|
1034
|
+
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
855
1035
|
const dest = path9.join(cwd, ".claude");
|
|
856
1036
|
const hookConfigPath = path9.join(cwd, meta.hookConfigFile);
|
|
857
1037
|
const artifacts = [];
|
|
@@ -889,7 +1069,7 @@ import path10 from "path";
|
|
|
889
1069
|
async function configureCodex(cwd, opts = {}) {
|
|
890
1070
|
const platform = "codex";
|
|
891
1071
|
const meta = PLATFORM_META[platform];
|
|
892
|
-
const ctx = meta.templateContext;
|
|
1072
|
+
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
893
1073
|
const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
|
|
894
1074
|
const artifacts = [];
|
|
895
1075
|
artifacts.push(
|
|
@@ -1005,7 +1185,7 @@ async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
|
|
|
1005
1185
|
async function configureQoder(cwd, opts = {}) {
|
|
1006
1186
|
const platform = "qoder";
|
|
1007
1187
|
const meta = resolvePlatformMeta(cwd, platform);
|
|
1008
|
-
const ctx = meta.templateContext;
|
|
1188
|
+
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
1009
1189
|
const dest = path12.join(cwd, ctx.platform_config_dir);
|
|
1010
1190
|
const hookConfigPath = path12.join(cwd, meta.hookConfigFile);
|
|
1011
1191
|
const artifacts = [];
|
|
@@ -1056,17 +1236,22 @@ var CONFIGURATORS = {
|
|
|
1056
1236
|
};
|
|
1057
1237
|
|
|
1058
1238
|
// src/commands/install-harness.ts
|
|
1059
|
-
async function configurePlatformsForDir(targetDir, platforms,
|
|
1239
|
+
async function configurePlatformsForDir(targetDir, platforms, opts = {}) {
|
|
1060
1240
|
const artifacts = [];
|
|
1061
1241
|
for (const platform of platforms) {
|
|
1062
|
-
artifacts.push(...await CONFIGURATORS[platform](targetDir,
|
|
1242
|
+
artifacts.push(...await CONFIGURATORS[platform](targetDir, opts));
|
|
1063
1243
|
}
|
|
1064
1244
|
return artifacts;
|
|
1065
1245
|
}
|
|
1066
1246
|
async function installHarnessToDir(targetDir, platforms, ctx) {
|
|
1067
|
-
const
|
|
1247
|
+
const projectId = createProjectId();
|
|
1248
|
+
const artifacts = await configurePlatformsForDir(targetDir, platforms, {
|
|
1249
|
+
supermodule: boundaryFromContext(ctx),
|
|
1250
|
+
projectId
|
|
1251
|
+
});
|
|
1068
1252
|
await writeRuntimeScaffold(targetDir, platforms, {
|
|
1069
|
-
supermodule: supermoduleConfigFromContext(ctx)
|
|
1253
|
+
supermodule: supermoduleConfigFromContext(ctx),
|
|
1254
|
+
projectId
|
|
1070
1255
|
});
|
|
1071
1256
|
await writeInstallManifest(targetDir, {
|
|
1072
1257
|
harnessVersion: VERSION,
|
|
@@ -1560,14 +1745,13 @@ async function addAgent(opts) {
|
|
|
1560
1745
|
const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
|
|
1561
1746
|
const agents = [...config.agents, ...toInstall];
|
|
1562
1747
|
if (toInstall.length > 0) {
|
|
1563
|
-
const
|
|
1564
|
-
target.dir,
|
|
1565
|
-
toInstall,
|
|
1566
|
-
target.boundary
|
|
1567
|
-
);
|
|
1568
|
-
await writeRuntimeScaffold(target.dir, agents, {
|
|
1748
|
+
const projectId = await writeRuntimeScaffold(target.dir, agents, {
|
|
1569
1749
|
supermodule: target.supermodule
|
|
1570
1750
|
});
|
|
1751
|
+
const artifacts = await configurePlatformsForDir(target.dir, toInstall, {
|
|
1752
|
+
supermodule: target.boundary,
|
|
1753
|
+
projectId
|
|
1754
|
+
});
|
|
1571
1755
|
await writeInstallManifest(target.dir, {
|
|
1572
1756
|
harnessVersion: VERSION,
|
|
1573
1757
|
agents: toInstall,
|
|
@@ -1770,11 +1954,12 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
|
|
|
1770
1954
|
if (!agentSet.has(registration.platform)) {
|
|
1771
1955
|
continue;
|
|
1772
1956
|
}
|
|
1957
|
+
const managedHookPaths = registration.hook_path ? managedHookPathsForRegistration(cwd, registration.hook_path) : [];
|
|
1773
1958
|
addHookConfigPrune(
|
|
1774
1959
|
plan.pruneHookConfigs,
|
|
1775
1960
|
plan.pruneHookCommands,
|
|
1776
1961
|
manifestPath(cwd, registration.config_path),
|
|
1777
|
-
|
|
1962
|
+
managedHookPaths,
|
|
1778
1963
|
[registration.command]
|
|
1779
1964
|
);
|
|
1780
1965
|
}
|
|
@@ -1822,7 +2007,7 @@ async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills
|
|
|
1822
2007
|
plan.pruneHookConfigs,
|
|
1823
2008
|
plan.pruneHookCommands,
|
|
1824
2009
|
path16.join(cwd, meta.hookConfigFile),
|
|
1825
|
-
|
|
2010
|
+
managedHookPathsForTemplate(cwd, meta, hookFileNames),
|
|
1826
2011
|
[]
|
|
1827
2012
|
);
|
|
1828
2013
|
plan.constraints.add(path16.join(cwd, meta.mainConstraint));
|
|
@@ -1873,6 +2058,51 @@ function addHookConfigPrune(pruneHookConfigs, pruneHookCommands, filePath, manag
|
|
|
1873
2058
|
}
|
|
1874
2059
|
pruneHookCommands.set(filePath, existingCommands);
|
|
1875
2060
|
}
|
|
2061
|
+
function managedHookPathsForRegistration(cwd, hookPath) {
|
|
2062
|
+
const paths = [hookPath];
|
|
2063
|
+
try {
|
|
2064
|
+
paths.push(...managedHookPathTokens(manifestPath(cwd, hookPath)));
|
|
2065
|
+
} catch {
|
|
2066
|
+
if (path16.isAbsolute(hookPath)) {
|
|
2067
|
+
paths.push(...managedHookPathTokens(hookPath));
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return paths;
|
|
2071
|
+
}
|
|
2072
|
+
function managedHookPathsForTemplate(cwd, meta, hookFileNames) {
|
|
2073
|
+
const relativePaths = hookFileNames.map(
|
|
2074
|
+
(name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`
|
|
2075
|
+
);
|
|
2076
|
+
const absolutePaths = hookFileNames.flatMap(
|
|
2077
|
+
(name) => managedHookPathTokens(path16.join(cwd, meta.hooksDir, name))
|
|
2078
|
+
);
|
|
2079
|
+
return [...relativePaths, ...absolutePaths];
|
|
2080
|
+
}
|
|
2081
|
+
function managedHookPathTokens(filePath) {
|
|
2082
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
2083
|
+
for (const equivalentPath of equivalentAbsolutePaths(filePath)) {
|
|
2084
|
+
const normalized = equivalentPath.replace(/\\/g, "/");
|
|
2085
|
+
const dir = path16.posix.dirname(normalized);
|
|
2086
|
+
const basename = path16.posix.basename(normalized);
|
|
2087
|
+
tokens.add(normalized);
|
|
2088
|
+
tokens.add(shellDoubleQuoteArg2(normalized));
|
|
2089
|
+
tokens.add(`${shellDoubleQuoteArg2(dir)}/${basename}`);
|
|
2090
|
+
}
|
|
2091
|
+
return [...tokens];
|
|
2092
|
+
}
|
|
2093
|
+
function equivalentAbsolutePaths(filePath) {
|
|
2094
|
+
const normalized = path16.resolve(filePath).replace(/\\/g, "/");
|
|
2095
|
+
const equivalents = [normalized];
|
|
2096
|
+
if (normalized.startsWith("/private/var/")) {
|
|
2097
|
+
equivalents.push(normalized.replace(/^\/private\/var\//, "/var/"));
|
|
2098
|
+
} else if (normalized.startsWith("/var/")) {
|
|
2099
|
+
equivalents.push(normalized.replace(/^\/var\//, "/private/var/"));
|
|
2100
|
+
}
|
|
2101
|
+
return equivalents;
|
|
2102
|
+
}
|
|
2103
|
+
function shellDoubleQuoteArg2(value) {
|
|
2104
|
+
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
2105
|
+
}
|
|
1876
2106
|
function createClearPlan() {
|
|
1877
2107
|
return {
|
|
1878
2108
|
remove: /* @__PURE__ */ new Set(),
|
|
@@ -2057,7 +2287,7 @@ function isManagedHook(hook, managedHookPaths, managedHookCommands) {
|
|
|
2057
2287
|
if (normalizedManagedCommands.includes(normalizeCommand(command))) {
|
|
2058
2288
|
return true;
|
|
2059
2289
|
}
|
|
2060
|
-
const normalizedCommand = command
|
|
2290
|
+
const normalizedCommand = command;
|
|
2061
2291
|
return managedHookPaths.some(
|
|
2062
2292
|
(hookPath) => commandContainsPathToken(normalizedCommand, hookPath.replace(/\\/g, "/"))
|
|
2063
2293
|
);
|
|
@@ -2543,8 +2773,35 @@ async function update(opts) {
|
|
|
2543
2773
|
}
|
|
2544
2774
|
|
|
2545
2775
|
// src/commands/upgrade.ts
|
|
2776
|
+
import path22 from "path";
|
|
2546
2777
|
import { cancel as cancel5, confirm as confirm4, outro as outro5 } from "@clack/prompts";
|
|
2547
2778
|
import chalk7 from "chalk";
|
|
2779
|
+
var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
|
|
2780
|
+
"claude-code": [
|
|
2781
|
+
{ event: "SessionStart", scriptName: "session-start.py" },
|
|
2782
|
+
{ event: "UserPromptSubmit", scriptName: "session-start.py" },
|
|
2783
|
+
{ event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" },
|
|
2784
|
+
{ event: "PreToolUse", scriptName: "inject-subagent-context.py" }
|
|
2785
|
+
],
|
|
2786
|
+
codex: [
|
|
2787
|
+
{ event: "UserPromptSubmit", scriptName: "session-start.py" },
|
|
2788
|
+
{ event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" }
|
|
2789
|
+
],
|
|
2790
|
+
qoder: [
|
|
2791
|
+
{ event: "UserPromptSubmit", scriptName: "session-start.py" },
|
|
2792
|
+
{ event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" },
|
|
2793
|
+
{ event: "PreToolUse", scriptName: "inject-subagent-context.py" }
|
|
2794
|
+
]
|
|
2795
|
+
};
|
|
2796
|
+
var MANAGED_HOOK_SCRIPT_NAMES = {
|
|
2797
|
+
"claude-code": [
|
|
2798
|
+
...new Set(
|
|
2799
|
+
EXPECTED_HOOK_REGISTRATION_SCRIPTS["claude-code"].map(({ scriptName }) => scriptName)
|
|
2800
|
+
)
|
|
2801
|
+
],
|
|
2802
|
+
codex: [...new Set(EXPECTED_HOOK_REGISTRATION_SCRIPTS.codex.map(({ scriptName }) => scriptName))],
|
|
2803
|
+
qoder: [...new Set(EXPECTED_HOOK_REGISTRATION_SCRIPTS.qoder.map(({ scriptName }) => scriptName))]
|
|
2804
|
+
};
|
|
2548
2805
|
async function upgrade(opts) {
|
|
2549
2806
|
renderBanner();
|
|
2550
2807
|
const cwd = process.cwd();
|
|
@@ -2591,14 +2848,13 @@ async function upgrade(opts) {
|
|
|
2591
2848
|
}
|
|
2592
2849
|
}
|
|
2593
2850
|
for (const { target, config } of pending) {
|
|
2594
|
-
const
|
|
2595
|
-
target.dir,
|
|
2596
|
-
config.agents,
|
|
2597
|
-
target.boundary
|
|
2598
|
-
);
|
|
2599
|
-
await writeRuntimeScaffold(target.dir, config.agents, {
|
|
2851
|
+
const projectId = await writeRuntimeScaffold(target.dir, config.agents, {
|
|
2600
2852
|
supermodule: target.supermodule
|
|
2601
2853
|
});
|
|
2854
|
+
const artifacts = await configurePlatformsForDir(target.dir, config.agents, {
|
|
2855
|
+
supermodule: target.boundary,
|
|
2856
|
+
projectId
|
|
2857
|
+
});
|
|
2602
2858
|
await writeInstallManifest(target.dir, {
|
|
2603
2859
|
harnessVersion: VERSION,
|
|
2604
2860
|
agents: config.agents,
|
|
@@ -2645,12 +2901,153 @@ async function resolvePendingUpgradeTargets(targets) {
|
|
|
2645
2901
|
`${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
2646
2902
|
);
|
|
2647
2903
|
}
|
|
2648
|
-
if (relation === -1) {
|
|
2904
|
+
if (relation === -1 || relation === 0 && await needsHookConfigRefresh(target, config)) {
|
|
2649
2905
|
pending.push({ target, config });
|
|
2650
2906
|
}
|
|
2651
2907
|
}
|
|
2652
2908
|
return pending;
|
|
2653
2909
|
}
|
|
2910
|
+
async function needsHookConfigRefresh(target, config) {
|
|
2911
|
+
const projectId = typeof config.project?.id === "string" ? config.project.id.trim() : "";
|
|
2912
|
+
if (!projectId) {
|
|
2913
|
+
return true;
|
|
2914
|
+
}
|
|
2915
|
+
const manifest = await readInstallManifest(target.dir);
|
|
2916
|
+
for (const agent of config.agents) {
|
|
2917
|
+
const meta = resolvePlatformMeta(target.dir, agent);
|
|
2918
|
+
const configPath2 = path22.join(target.dir, meta.hookConfigFile);
|
|
2919
|
+
const content = await readTextIfExists(configPath2);
|
|
2920
|
+
if (content === null) {
|
|
2921
|
+
return true;
|
|
2922
|
+
}
|
|
2923
|
+
let parsed;
|
|
2924
|
+
try {
|
|
2925
|
+
parsed = JSON.parse(content);
|
|
2926
|
+
} catch {
|
|
2927
|
+
return true;
|
|
2928
|
+
}
|
|
2929
|
+
const commandsByEvent = collectHookCommandsByEvent(parsed.hooks);
|
|
2930
|
+
const commands = [...commandsByEvent.values()].flat();
|
|
2931
|
+
const expectedRegistrations = expectedHookRegistrations(target.dir, meta, agent, projectId);
|
|
2932
|
+
const expectedCommandSet = new Set(
|
|
2933
|
+
expectedRegistrations.map((registration) => registration.command)
|
|
2934
|
+
);
|
|
2935
|
+
if (!hasExpectedHookRegistrations(commandsByEvent, expectedRegistrations)) {
|
|
2936
|
+
return true;
|
|
2937
|
+
}
|
|
2938
|
+
const manifestManagedCommands = new Set(
|
|
2939
|
+
manifest?.hook_registrations.filter((registration) => registration.platform === agent).map((registration) => registration.command) ?? []
|
|
2940
|
+
);
|
|
2941
|
+
if (commands.some(
|
|
2942
|
+
(command) => isUnexpectedManagedHookCommand(
|
|
2943
|
+
command,
|
|
2944
|
+
expectedCommandSet,
|
|
2945
|
+
target.dir,
|
|
2946
|
+
meta,
|
|
2947
|
+
agent,
|
|
2948
|
+
manifestManagedCommands
|
|
2949
|
+
)
|
|
2950
|
+
)) {
|
|
2951
|
+
return true;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
return false;
|
|
2955
|
+
}
|
|
2956
|
+
function expectedHookRegistrations(cwd, meta, platform, projectId) {
|
|
2957
|
+
return EXPECTED_HOOK_REGISTRATION_SCRIPTS[platform].map(({ event, scriptName }) => ({
|
|
2958
|
+
event,
|
|
2959
|
+
command: renderHookCommand(cwd, meta.templateContext, scriptName, process.platform, projectId)
|
|
2960
|
+
}));
|
|
2961
|
+
}
|
|
2962
|
+
function hasExpectedHookRegistrations(commandsByEvent, expectedRegistrations) {
|
|
2963
|
+
const actualCounts = countRegistrations(
|
|
2964
|
+
[...commandsByEvent.entries()].flatMap(
|
|
2965
|
+
([event, commands]) => commands.map((command) => ({ event, command }))
|
|
2966
|
+
)
|
|
2967
|
+
);
|
|
2968
|
+
const expectedCounts = countRegistrations(expectedRegistrations);
|
|
2969
|
+
return [...expectedCounts.entries()].every(
|
|
2970
|
+
([registrationKey, expectedCount]) => (actualCounts.get(registrationKey) ?? 0) >= expectedCount
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
function countRegistrations(registrations) {
|
|
2974
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2975
|
+
for (const registration of registrations) {
|
|
2976
|
+
const key = hookRegistrationKey(registration);
|
|
2977
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2978
|
+
}
|
|
2979
|
+
return counts;
|
|
2980
|
+
}
|
|
2981
|
+
function hookRegistrationKey(registration) {
|
|
2982
|
+
return `${registration.event}\0${registration.command}`;
|
|
2983
|
+
}
|
|
2984
|
+
function collectHookCommandsByEvent(hooks) {
|
|
2985
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
2986
|
+
return /* @__PURE__ */ new Map();
|
|
2987
|
+
}
|
|
2988
|
+
const commandsByEvent = /* @__PURE__ */ new Map();
|
|
2989
|
+
for (const [event, value] of Object.entries(hooks)) {
|
|
2990
|
+
if (!Array.isArray(value)) {
|
|
2991
|
+
continue;
|
|
2992
|
+
}
|
|
2993
|
+
for (const group of value) {
|
|
2994
|
+
if (!group || typeof group !== "object") {
|
|
2995
|
+
continue;
|
|
2996
|
+
}
|
|
2997
|
+
const hookItems = group.hooks;
|
|
2998
|
+
if (!Array.isArray(hookItems)) {
|
|
2999
|
+
continue;
|
|
3000
|
+
}
|
|
3001
|
+
for (const hook of hookItems) {
|
|
3002
|
+
if (!hook || typeof hook !== "object") {
|
|
3003
|
+
continue;
|
|
3004
|
+
}
|
|
3005
|
+
const command = hook.command;
|
|
3006
|
+
if (typeof command === "string") {
|
|
3007
|
+
const eventCommands = commandsByEvent.get(event) ?? [];
|
|
3008
|
+
eventCommands.push(command);
|
|
3009
|
+
commandsByEvent.set(event, eventCommands);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
return commandsByEvent;
|
|
3015
|
+
}
|
|
3016
|
+
function isUnexpectedManagedHookCommand(command, expectedCommands, cwd, meta, platform, manifestManagedCommands) {
|
|
3017
|
+
if (expectedCommands.has(command)) {
|
|
3018
|
+
return false;
|
|
3019
|
+
}
|
|
3020
|
+
if (manifestManagedCommands.has(command)) {
|
|
3021
|
+
return true;
|
|
3022
|
+
}
|
|
3023
|
+
const hookPath = extractHookPathFromCommand(command);
|
|
3024
|
+
return hookPath === null ? false : isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform);
|
|
3025
|
+
}
|
|
3026
|
+
function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
|
|
3027
|
+
const normalizedHookPath = normalizePathForHookComparison(hookPath);
|
|
3028
|
+
const configDir = normalizePathForHookComparison(meta.templateContext.platform_config_dir);
|
|
3029
|
+
return MANAGED_HOOK_SCRIPT_NAMES[platform].some((scriptName) => {
|
|
3030
|
+
const relativeHookPath = `${configDir}/hooks/${scriptName}`;
|
|
3031
|
+
if (normalizedHookPath === relativeHookPath) {
|
|
3032
|
+
return true;
|
|
3033
|
+
}
|
|
3034
|
+
return pathAliases(
|
|
3035
|
+
normalizePathForHookComparison(path22.resolve(cwd, relativeHookPath))
|
|
3036
|
+
).includes(normalizedHookPath);
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
function normalizePathForHookComparison(value) {
|
|
3040
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
3041
|
+
}
|
|
3042
|
+
function pathAliases(value) {
|
|
3043
|
+
const aliases = /* @__PURE__ */ new Set([value]);
|
|
3044
|
+
if (value.startsWith("/private/var/")) {
|
|
3045
|
+
aliases.add(value.replace(/^\/private\/var\//, "/var/"));
|
|
3046
|
+
} else if (value.startsWith("/var/")) {
|
|
3047
|
+
aliases.add(`/private${value}`);
|
|
3048
|
+
}
|
|
3049
|
+
return [...aliases];
|
|
3050
|
+
}
|
|
2654
3051
|
async function resolveParentTopologyRefresh(targets, pending) {
|
|
2655
3052
|
const parent = targets.find(
|
|
2656
3053
|
(target) => target.label === "." && target.supermodule.role === "super-parent"
|