easy-coding-harness 0.5.1 → 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 +5 -4
- package/dist/cli.js +129 -35
- 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 +6 -4
- package/templates/qoder/settings.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
- `y`:常规功能升级
|
|
7
7
|
- `z`:日常 bug 修复
|
|
8
8
|
|
|
9
|
-
## 0.5.
|
|
9
|
+
## 0.5.2-beta.0
|
|
10
10
|
|
|
11
|
-
- 修复 Claude Code hook
|
|
12
|
-
- `easy-coding upgrade` 可直接刷新存量 0.5.0
|
|
13
|
-
- `easy-coding clear` 和 `install-manifest.json`
|
|
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。
|
|
14
15
|
|
|
15
16
|
## 0.5.0
|
|
16
17
|
|
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";
|
|
@@ -568,17 +594,21 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
|
568
594
|
const easyCodingDir = path6.join(cwd, EASY_CODING_DIR);
|
|
569
595
|
await ensureDir(easyCodingDir);
|
|
570
596
|
const configPath2 = path6.join(easyCodingDir, CONFIG_FILE);
|
|
597
|
+
let projectId = opts.projectId ?? createProjectId();
|
|
571
598
|
if (!await pathExists(configPath2)) {
|
|
572
599
|
const projectName = path6.basename(cwd);
|
|
573
600
|
await writeConfigYaml(
|
|
574
601
|
configPath2,
|
|
575
602
|
createDefaultConfig({
|
|
576
603
|
projectName,
|
|
604
|
+
projectId,
|
|
577
605
|
harnessVersion: VERSION,
|
|
578
606
|
agents,
|
|
579
607
|
supermodule: opts.supermodule
|
|
580
608
|
})
|
|
581
609
|
);
|
|
610
|
+
} else {
|
|
611
|
+
projectId = await ensureProjectId(configPath2);
|
|
582
612
|
}
|
|
583
613
|
await ensureDir(path6.join(easyCodingDir, "tasks"));
|
|
584
614
|
await ensureDir(path6.join(easyCodingDir, SESSIONS_DIR));
|
|
@@ -586,6 +616,7 @@ async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
|
586
616
|
await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
587
617
|
await writeMemoryScaffold(easyCodingDir);
|
|
588
618
|
await writeTemplatesScaffold(easyCodingDir);
|
|
619
|
+
return projectId;
|
|
589
620
|
}
|
|
590
621
|
async function writeTemplatesScaffold(easyCodingDir) {
|
|
591
622
|
const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
|
|
@@ -762,17 +793,28 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
|
|
|
762
793
|
}
|
|
763
794
|
return resolved;
|
|
764
795
|
}
|
|
765
|
-
function
|
|
766
|
-
const
|
|
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) {
|
|
767
801
|
return {
|
|
768
802
|
...ctx,
|
|
769
|
-
|
|
770
|
-
|
|
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
|
+
)
|
|
771
812
|
};
|
|
772
813
|
}
|
|
773
|
-
function renderHookCommand(
|
|
774
|
-
const
|
|
775
|
-
|
|
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}`;
|
|
776
818
|
}
|
|
777
819
|
function shellDoubleQuoteArg(value, platform = process.platform) {
|
|
778
820
|
if (platform === "win32") {
|
|
@@ -783,12 +825,57 @@ function shellDoubleQuoteArg(value, platform = process.platform) {
|
|
|
783
825
|
}
|
|
784
826
|
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
785
827
|
}
|
|
786
|
-
function toPosixPath(filePath) {
|
|
787
|
-
return filePath.split(path8.sep).join("/");
|
|
788
|
-
}
|
|
789
828
|
function jsonStringContent(value) {
|
|
790
829
|
return JSON.stringify(value).slice(1, -1);
|
|
791
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");
|
|
792
879
|
async function resolveSkills(ctx) {
|
|
793
880
|
const skillsRoot = getTemplatePath("common", "skills");
|
|
794
881
|
const entries = await readdir2(skillsRoot);
|
|
@@ -944,7 +1031,7 @@ function stripTemplateExtension(fileName) {
|
|
|
944
1031
|
async function configureClaude(cwd, opts = {}) {
|
|
945
1032
|
const platform = "claude-code";
|
|
946
1033
|
const meta = PLATFORM_META[platform];
|
|
947
|
-
const ctx =
|
|
1034
|
+
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
948
1035
|
const dest = path9.join(cwd, ".claude");
|
|
949
1036
|
const hookConfigPath = path9.join(cwd, meta.hookConfigFile);
|
|
950
1037
|
const artifacts = [];
|
|
@@ -982,7 +1069,7 @@ import path10 from "path";
|
|
|
982
1069
|
async function configureCodex(cwd, opts = {}) {
|
|
983
1070
|
const platform = "codex";
|
|
984
1071
|
const meta = PLATFORM_META[platform];
|
|
985
|
-
const ctx =
|
|
1072
|
+
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
986
1073
|
const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
|
|
987
1074
|
const artifacts = [];
|
|
988
1075
|
artifacts.push(
|
|
@@ -1098,7 +1185,7 @@ async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
|
|
|
1098
1185
|
async function configureQoder(cwd, opts = {}) {
|
|
1099
1186
|
const platform = "qoder";
|
|
1100
1187
|
const meta = resolvePlatformMeta(cwd, platform);
|
|
1101
|
-
const ctx =
|
|
1188
|
+
const ctx = await withProjectInstallPaths(cwd, meta.templateContext, opts.projectId);
|
|
1102
1189
|
const dest = path12.join(cwd, ctx.platform_config_dir);
|
|
1103
1190
|
const hookConfigPath = path12.join(cwd, meta.hookConfigFile);
|
|
1104
1191
|
const artifacts = [];
|
|
@@ -1149,17 +1236,22 @@ var CONFIGURATORS = {
|
|
|
1149
1236
|
};
|
|
1150
1237
|
|
|
1151
1238
|
// src/commands/install-harness.ts
|
|
1152
|
-
async function configurePlatformsForDir(targetDir, platforms,
|
|
1239
|
+
async function configurePlatformsForDir(targetDir, platforms, opts = {}) {
|
|
1153
1240
|
const artifacts = [];
|
|
1154
1241
|
for (const platform of platforms) {
|
|
1155
|
-
artifacts.push(...await CONFIGURATORS[platform](targetDir,
|
|
1242
|
+
artifacts.push(...await CONFIGURATORS[platform](targetDir, opts));
|
|
1156
1243
|
}
|
|
1157
1244
|
return artifacts;
|
|
1158
1245
|
}
|
|
1159
1246
|
async function installHarnessToDir(targetDir, platforms, ctx) {
|
|
1160
|
-
const
|
|
1247
|
+
const projectId = createProjectId();
|
|
1248
|
+
const artifacts = await configurePlatformsForDir(targetDir, platforms, {
|
|
1249
|
+
supermodule: boundaryFromContext(ctx),
|
|
1250
|
+
projectId
|
|
1251
|
+
});
|
|
1161
1252
|
await writeRuntimeScaffold(targetDir, platforms, {
|
|
1162
|
-
supermodule: supermoduleConfigFromContext(ctx)
|
|
1253
|
+
supermodule: supermoduleConfigFromContext(ctx),
|
|
1254
|
+
projectId
|
|
1163
1255
|
});
|
|
1164
1256
|
await writeInstallManifest(targetDir, {
|
|
1165
1257
|
harnessVersion: VERSION,
|
|
@@ -1653,14 +1745,13 @@ async function addAgent(opts) {
|
|
|
1653
1745
|
const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
|
|
1654
1746
|
const agents = [...config.agents, ...toInstall];
|
|
1655
1747
|
if (toInstall.length > 0) {
|
|
1656
|
-
const
|
|
1657
|
-
target.dir,
|
|
1658
|
-
toInstall,
|
|
1659
|
-
target.boundary
|
|
1660
|
-
);
|
|
1661
|
-
await writeRuntimeScaffold(target.dir, agents, {
|
|
1748
|
+
const projectId = await writeRuntimeScaffold(target.dir, agents, {
|
|
1662
1749
|
supermodule: target.supermodule
|
|
1663
1750
|
});
|
|
1751
|
+
const artifacts = await configurePlatformsForDir(target.dir, toInstall, {
|
|
1752
|
+
supermodule: target.boundary,
|
|
1753
|
+
projectId
|
|
1754
|
+
});
|
|
1664
1755
|
await writeInstallManifest(target.dir, {
|
|
1665
1756
|
harnessVersion: VERSION,
|
|
1666
1757
|
agents: toInstall,
|
|
@@ -2757,14 +2848,13 @@ async function upgrade(opts) {
|
|
|
2757
2848
|
}
|
|
2758
2849
|
}
|
|
2759
2850
|
for (const { target, config } of pending) {
|
|
2760
|
-
const
|
|
2761
|
-
target.dir,
|
|
2762
|
-
config.agents,
|
|
2763
|
-
target.boundary
|
|
2764
|
-
);
|
|
2765
|
-
await writeRuntimeScaffold(target.dir, config.agents, {
|
|
2851
|
+
const projectId = await writeRuntimeScaffold(target.dir, config.agents, {
|
|
2766
2852
|
supermodule: target.supermodule
|
|
2767
2853
|
});
|
|
2854
|
+
const artifacts = await configurePlatformsForDir(target.dir, config.agents, {
|
|
2855
|
+
supermodule: target.boundary,
|
|
2856
|
+
projectId
|
|
2857
|
+
});
|
|
2768
2858
|
await writeInstallManifest(target.dir, {
|
|
2769
2859
|
harnessVersion: VERSION,
|
|
2770
2860
|
agents: config.agents,
|
|
@@ -2811,15 +2901,19 @@ async function resolvePendingUpgradeTargets(targets) {
|
|
|
2811
2901
|
`${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
2812
2902
|
);
|
|
2813
2903
|
}
|
|
2814
|
-
if (relation === -1 || relation === 0 && await needsHookConfigRefresh(target, config
|
|
2904
|
+
if (relation === -1 || relation === 0 && await needsHookConfigRefresh(target, config)) {
|
|
2815
2905
|
pending.push({ target, config });
|
|
2816
2906
|
}
|
|
2817
2907
|
}
|
|
2818
2908
|
return pending;
|
|
2819
2909
|
}
|
|
2820
|
-
async function needsHookConfigRefresh(target,
|
|
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
|
+
}
|
|
2821
2915
|
const manifest = await readInstallManifest(target.dir);
|
|
2822
|
-
for (const agent of agents) {
|
|
2916
|
+
for (const agent of config.agents) {
|
|
2823
2917
|
const meta = resolvePlatformMeta(target.dir, agent);
|
|
2824
2918
|
const configPath2 = path22.join(target.dir, meta.hookConfigFile);
|
|
2825
2919
|
const content = await readTextIfExists(configPath2);
|
|
@@ -2834,7 +2928,7 @@ async function needsHookConfigRefresh(target, agents) {
|
|
|
2834
2928
|
}
|
|
2835
2929
|
const commandsByEvent = collectHookCommandsByEvent(parsed.hooks);
|
|
2836
2930
|
const commands = [...commandsByEvent.values()].flat();
|
|
2837
|
-
const expectedRegistrations = expectedHookRegistrations(target.dir, meta, agent);
|
|
2931
|
+
const expectedRegistrations = expectedHookRegistrations(target.dir, meta, agent, projectId);
|
|
2838
2932
|
const expectedCommandSet = new Set(
|
|
2839
2933
|
expectedRegistrations.map((registration) => registration.command)
|
|
2840
2934
|
);
|
|
@@ -2859,10 +2953,10 @@ async function needsHookConfigRefresh(target, agents) {
|
|
|
2859
2953
|
}
|
|
2860
2954
|
return false;
|
|
2861
2955
|
}
|
|
2862
|
-
function expectedHookRegistrations(cwd, meta, platform) {
|
|
2956
|
+
function expectedHookRegistrations(cwd, meta, platform, projectId) {
|
|
2863
2957
|
return EXPECTED_HOOK_REGISTRATION_SCRIPTS[platform].map(({ event, scriptName }) => ({
|
|
2864
2958
|
event,
|
|
2865
|
-
command: renderHookCommand(cwd, meta.templateContext, scriptName)
|
|
2959
|
+
command: renderHookCommand(cwd, meta.templateContext, scriptName, process.platform, projectId)
|
|
2866
2960
|
}));
|
|
2867
2961
|
}
|
|
2868
2962
|
function hasExpectedHookRegistrations(commandsByEvent, expectedRegistrations) {
|