easy-coding-harness 0.4.0 → 0.5.1
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 +15 -0
- package/README.md +30 -4
- package/dist/cli.js +1641 -557
- 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 +10 -0
- package/templates/common/skills/ec-git/SKILL.md +9 -1
- package/templates/common/skills/ec-memory/SKILL.md +17 -0
- package/templates/main-constraint/AGENTS.md.tpl +1 -0
- package/templates/main-constraint/CLAUDE.md.tpl +1 -0
- package/templates/qoder/settings.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -5,12 +5,207 @@ import chalk8 from "chalk";
|
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/add-agent.ts
|
|
8
|
-
import path13 from "path";
|
|
9
8
|
import { outro } from "@clack/prompts";
|
|
10
9
|
import chalk2 from "chalk";
|
|
11
10
|
|
|
12
|
-
// src/
|
|
13
|
-
import
|
|
11
|
+
// src/constants/version.ts
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var packageMetadata = readPackageMetadata();
|
|
16
|
+
var PACKAGE_NAME = packageMetadata.name;
|
|
17
|
+
var VERSION = packageMetadata.version;
|
|
18
|
+
function readPackageMetadata() {
|
|
19
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const candidates = [
|
|
21
|
+
path.resolve(here, "../../package.json"),
|
|
22
|
+
path.resolve(here, "../package.json"),
|
|
23
|
+
path.resolve(process.cwd(), "package.json")
|
|
24
|
+
];
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
28
|
+
if (parsed.name && parsed.version) {
|
|
29
|
+
return { name: parsed.name, version: parsed.version };
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Unable to locate package metadata. Tried: ${candidates.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/ui/banner.ts
|
|
38
|
+
import chalk from "chalk";
|
|
39
|
+
import figlet from "figlet";
|
|
40
|
+
var WIDE_TERMINAL_WIDTH = 88;
|
|
41
|
+
var MEDIUM_TERMINAL_WIDTH = 72;
|
|
42
|
+
function renderBanner() {
|
|
43
|
+
const terminalWidth = process.stdout.columns ?? 120;
|
|
44
|
+
const art = figlet.textSync("Easy Coding", selectBannerPreset(terminalWidth));
|
|
45
|
+
console.log(colorizeBanner(art));
|
|
46
|
+
console.log(chalk.bold.white(` Easy Coding Harness ${chalk.cyan(`v${VERSION}`)}
|
|
47
|
+
`));
|
|
48
|
+
}
|
|
49
|
+
function selectBannerPreset(width) {
|
|
50
|
+
if (width >= WIDE_TERMINAL_WIDTH) {
|
|
51
|
+
return { font: "ANSI Shadow", horizontalLayout: "full" };
|
|
52
|
+
}
|
|
53
|
+
if (width >= MEDIUM_TERMINAL_WIDTH) {
|
|
54
|
+
return { font: "Small Shadow", horizontalLayout: "full" };
|
|
55
|
+
}
|
|
56
|
+
return { font: "Small Slant" };
|
|
57
|
+
}
|
|
58
|
+
function colorizeBanner(art) {
|
|
59
|
+
const colors = [chalk.cyanBright, chalk.cyan, chalk.blueBright, chalk.blue];
|
|
60
|
+
return art.split("\n").map((line, index) => colors[index % colors.length](line)).join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/utils/config-yaml.ts
|
|
64
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
65
|
+
import YAML, { isScalar, isSeq, parseDocument } from "yaml";
|
|
66
|
+
|
|
67
|
+
// src/utils/file-writer.ts
|
|
68
|
+
import { constants } from "fs";
|
|
69
|
+
import { access, cp, writeFile as fsWriteFile, mkdir, readFile, stat } from "fs/promises";
|
|
70
|
+
import path2 from "path";
|
|
71
|
+
async function pathExists(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
await access(filePath, constants.F_OK);
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function ensureDir(dir) {
|
|
80
|
+
await mkdir(dir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
async function writeTextFile(filePath, content) {
|
|
83
|
+
await ensureDir(path2.dirname(filePath));
|
|
84
|
+
await fsWriteFile(filePath, content.endsWith("\n") ? content : `${content}
|
|
85
|
+
`, "utf8");
|
|
86
|
+
}
|
|
87
|
+
async function readTextFile(filePath) {
|
|
88
|
+
return readFile(filePath, "utf8");
|
|
89
|
+
}
|
|
90
|
+
async function readTextIfExists(filePath) {
|
|
91
|
+
if (!await pathExists(filePath)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return readTextFile(filePath);
|
|
95
|
+
}
|
|
96
|
+
async function isDirectory(filePath) {
|
|
97
|
+
try {
|
|
98
|
+
return (await stat(filePath)).isDirectory();
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/utils/config-yaml.ts
|
|
105
|
+
function createDefaultConfig(params) {
|
|
106
|
+
const config = {
|
|
107
|
+
version: 1,
|
|
108
|
+
harness_version: params.harnessVersion,
|
|
109
|
+
agents: params.agents,
|
|
110
|
+
project: {
|
|
111
|
+
name: params.projectName
|
|
112
|
+
},
|
|
113
|
+
memory: {
|
|
114
|
+
short_term_max: 10,
|
|
115
|
+
short_term_keep: 5,
|
|
116
|
+
schema_version: 2
|
|
117
|
+
},
|
|
118
|
+
tasks: {
|
|
119
|
+
auto_archive_days: 30
|
|
120
|
+
},
|
|
121
|
+
behavior: {
|
|
122
|
+
strict_confirm: true,
|
|
123
|
+
auto_mode: false
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
if (params.supermodule) {
|
|
127
|
+
config.supermodule = params.supermodule;
|
|
128
|
+
}
|
|
129
|
+
return config;
|
|
130
|
+
}
|
|
131
|
+
function stringifyConfig(config) {
|
|
132
|
+
return YAML.stringify(config);
|
|
133
|
+
}
|
|
134
|
+
async function writeConfigYaml(filePath, config) {
|
|
135
|
+
await writeTextFile(filePath, stringifyConfig(config));
|
|
136
|
+
}
|
|
137
|
+
async function readConfigYaml(filePath) {
|
|
138
|
+
const content = await readFile2(filePath, "utf8");
|
|
139
|
+
return YAML.parse(content);
|
|
140
|
+
}
|
|
141
|
+
async function updateConfigYaml(filePath, updater) {
|
|
142
|
+
const content = await readFile2(filePath, "utf8");
|
|
143
|
+
const document = parseDocument(content);
|
|
144
|
+
const config = document.toJSON();
|
|
145
|
+
updater(config);
|
|
146
|
+
for (const [key, value] of Object.entries(config)) {
|
|
147
|
+
document.set(key, value);
|
|
148
|
+
}
|
|
149
|
+
await writeTextFile(filePath, document.toString());
|
|
150
|
+
return config;
|
|
151
|
+
}
|
|
152
|
+
async function addAgentsToConfig(filePath, agents) {
|
|
153
|
+
return updateConfigYaml(filePath, (config) => {
|
|
154
|
+
const merged = /* @__PURE__ */ new Set([...config.agents ?? [], ...agents]);
|
|
155
|
+
config.agents = [...merged];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async function updateHarnessVersion(filePath, version) {
|
|
159
|
+
return updateConfigYaml(filePath, (config) => {
|
|
160
|
+
config.harness_version = version;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async function updateSupermoduleConfig(filePath, supermodule) {
|
|
164
|
+
return updateConfigYaml(filePath, (config) => {
|
|
165
|
+
config.supermodule = supermodule;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/utils/gitignore.ts
|
|
170
|
+
import path3 from "path";
|
|
171
|
+
|
|
172
|
+
// src/constants/paths.ts
|
|
173
|
+
var EASY_CODING_DIR = ".easy-coding";
|
|
174
|
+
var CONFIG_FILE = "config.yaml";
|
|
175
|
+
var INSTALL_MANIFEST_FILE = "install-manifest.json";
|
|
176
|
+
var SESSIONS_DIR = "sessions";
|
|
177
|
+
var TASKS_DIR = "tasks";
|
|
178
|
+
var PROJECT_INIT_TASK_ID = "project-init";
|
|
179
|
+
var MEMORY_DIR = "memory";
|
|
180
|
+
var SPEC_DIR = "spec";
|
|
181
|
+
var MAIN_SPEC_DIR = "main";
|
|
182
|
+
var DEV_SPEC_DIR = "dev";
|
|
183
|
+
var TEMPLATES_DIR = "templates";
|
|
184
|
+
var SESSIONS_GITIGNORE_ENTRY = ".easy-coding/sessions/";
|
|
185
|
+
var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
|
|
186
|
+
var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness generated \u2550\u2550\u2550 -->";
|
|
187
|
+
|
|
188
|
+
// src/utils/gitignore.ts
|
|
189
|
+
async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550 easy-coding-harness (auto-generated) \u2550\u2550\u2550\n# Personal runtime state; do not commit") {
|
|
190
|
+
const gitignorePath = path3.join(cwd, ".gitignore");
|
|
191
|
+
const current = await readTextIfExists(gitignorePath) ?? "";
|
|
192
|
+
const lines = current.split(/\r?\n/).map((line) => line.trim());
|
|
193
|
+
if (lines.includes(entry)) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const prefix = current.trimEnd();
|
|
197
|
+
const next = [prefix, heading, entry].filter(Boolean).join("\n");
|
|
198
|
+
await writeTextFile(gitignorePath, next);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
async function ensureEasyCodingSessionsIgnored(cwd) {
|
|
202
|
+
return ensureGitignoreEntry(cwd, SESSIONS_GITIGNORE_ENTRY);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/utils/install-manifest.ts
|
|
206
|
+
import { createHash } from "crypto";
|
|
207
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
208
|
+
import path4 from "path";
|
|
14
209
|
|
|
15
210
|
// src/types/platform.ts
|
|
16
211
|
var pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
@@ -34,7 +229,8 @@ var PLATFORM_META = {
|
|
|
34
229
|
workflow_state_path: ".easy-coding/sessions/",
|
|
35
230
|
main_constraint_file: "CLAUDE.md",
|
|
36
231
|
python_cmd: pythonCmd,
|
|
37
|
-
platform_config_dir: ".claude"
|
|
232
|
+
platform_config_dir: ".claude",
|
|
233
|
+
supermodule_boundary: ""
|
|
38
234
|
}
|
|
39
235
|
},
|
|
40
236
|
codex: {
|
|
@@ -56,7 +252,8 @@ var PLATFORM_META = {
|
|
|
56
252
|
workflow_state_path: ".easy-coding/sessions/",
|
|
57
253
|
main_constraint_file: "AGENTS.md",
|
|
58
254
|
python_cmd: pythonCmd,
|
|
59
|
-
platform_config_dir: ".codex"
|
|
255
|
+
platform_config_dir: ".codex",
|
|
256
|
+
supermodule_boundary: ""
|
|
60
257
|
}
|
|
61
258
|
},
|
|
62
259
|
qoder: {
|
|
@@ -79,7 +276,8 @@ var PLATFORM_META = {
|
|
|
79
276
|
workflow_state_path: ".easy-coding/sessions/",
|
|
80
277
|
main_constraint_file: "AGENTS.md",
|
|
81
278
|
python_cmd: pythonCmd,
|
|
82
|
-
platform_config_dir: ".qoder"
|
|
279
|
+
platform_config_dir: ".qoder",
|
|
280
|
+
supermodule_boundary: ""
|
|
83
281
|
}
|
|
84
282
|
}
|
|
85
283
|
};
|
|
@@ -88,64 +286,6 @@ function isAgentPlatform(value) {
|
|
|
88
286
|
return Object.hasOwn(PLATFORM_META, value);
|
|
89
287
|
}
|
|
90
288
|
|
|
91
|
-
// src/utils/install-manifest.ts
|
|
92
|
-
import { createHash } from "crypto";
|
|
93
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
94
|
-
import path2 from "path";
|
|
95
|
-
|
|
96
|
-
// src/constants/paths.ts
|
|
97
|
-
var EASY_CODING_DIR = ".easy-coding";
|
|
98
|
-
var CONFIG_FILE = "config.yaml";
|
|
99
|
-
var INSTALL_MANIFEST_FILE = "install-manifest.json";
|
|
100
|
-
var SESSIONS_DIR = "sessions";
|
|
101
|
-
var TASKS_DIR = "tasks";
|
|
102
|
-
var PROJECT_INIT_TASK_ID = "project-init";
|
|
103
|
-
var MEMORY_DIR = "memory";
|
|
104
|
-
var SPEC_DIR = "spec";
|
|
105
|
-
var MAIN_SPEC_DIR = "main";
|
|
106
|
-
var DEV_SPEC_DIR = "dev";
|
|
107
|
-
var TEMPLATES_DIR = "templates";
|
|
108
|
-
var SESSIONS_GITIGNORE_ENTRY = ".easy-coding/sessions/";
|
|
109
|
-
var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
|
|
110
|
-
var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness generated \u2550\u2550\u2550 -->";
|
|
111
|
-
|
|
112
|
-
// src/utils/file-writer.ts
|
|
113
|
-
import { constants } from "fs";
|
|
114
|
-
import { access, cp, writeFile as fsWriteFile, mkdir, readFile, stat } from "fs/promises";
|
|
115
|
-
import path from "path";
|
|
116
|
-
async function pathExists(filePath) {
|
|
117
|
-
try {
|
|
118
|
-
await access(filePath, constants.F_OK);
|
|
119
|
-
return true;
|
|
120
|
-
} catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
async function ensureDir(dir) {
|
|
125
|
-
await mkdir(dir, { recursive: true });
|
|
126
|
-
}
|
|
127
|
-
async function writeTextFile(filePath, content) {
|
|
128
|
-
await ensureDir(path.dirname(filePath));
|
|
129
|
-
await fsWriteFile(filePath, content.endsWith("\n") ? content : `${content}
|
|
130
|
-
`, "utf8");
|
|
131
|
-
}
|
|
132
|
-
async function readTextFile(filePath) {
|
|
133
|
-
return readFile(filePath, "utf8");
|
|
134
|
-
}
|
|
135
|
-
async function readTextIfExists(filePath) {
|
|
136
|
-
if (!await pathExists(filePath)) {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
return readTextFile(filePath);
|
|
140
|
-
}
|
|
141
|
-
async function isDirectory(filePath) {
|
|
142
|
-
try {
|
|
143
|
-
return (await stat(filePath)).isDirectory();
|
|
144
|
-
} catch {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
289
|
// src/utils/install-manifest.ts
|
|
150
290
|
function fileArtifact(filePath, kind, platform) {
|
|
151
291
|
return { type: "file", kind, filePath, platform };
|
|
@@ -153,8 +293,8 @@ function fileArtifact(filePath, kind, platform) {
|
|
|
153
293
|
function constraintRegionArtifact(filePath, platform) {
|
|
154
294
|
return { type: "constraint-region", filePath, platform };
|
|
155
295
|
}
|
|
156
|
-
async function hookRegistrationArtifacts(
|
|
157
|
-
const content = await readTextIfExists(
|
|
296
|
+
async function hookRegistrationArtifacts(configPath2, platform) {
|
|
297
|
+
const content = await readTextIfExists(configPath2);
|
|
158
298
|
if (content === null) {
|
|
159
299
|
return [];
|
|
160
300
|
}
|
|
@@ -166,7 +306,7 @@ async function hookRegistrationArtifacts(configPath, platform) {
|
|
|
166
306
|
}
|
|
167
307
|
return collectHookCommands(parsed.hooks).map((command) => ({
|
|
168
308
|
type: "hook-registration",
|
|
169
|
-
configPath,
|
|
309
|
+
configPath: configPath2,
|
|
170
310
|
command,
|
|
171
311
|
hookPath: extractHookPathFromCommand(command),
|
|
172
312
|
platform
|
|
@@ -206,14 +346,14 @@ async function writeInstallManifest(cwd, params) {
|
|
|
206
346
|
continue;
|
|
207
347
|
}
|
|
208
348
|
if (artifact.type === "hook-registration") {
|
|
209
|
-
const
|
|
349
|
+
const configPath2 = toProjectPath(cwd, artifact.configPath);
|
|
210
350
|
const registration = {
|
|
211
|
-
config_path:
|
|
351
|
+
config_path: configPath2,
|
|
212
352
|
command: artifact.command,
|
|
213
|
-
hook_path: artifact.hookPath,
|
|
353
|
+
hook_path: artifact.hookPath ? normalizeHookPath(cwd, artifact.hookPath) : null,
|
|
214
354
|
platform: artifact.platform
|
|
215
355
|
};
|
|
216
|
-
hookRegistrations.set(`${
|
|
356
|
+
hookRegistrations.set(`${configPath2}\0${normalizeCommand(artifact.command)}`, registration);
|
|
217
357
|
continue;
|
|
218
358
|
}
|
|
219
359
|
const relPath = toProjectPath(cwd, artifact.filePath);
|
|
@@ -234,12 +374,12 @@ async function writeInstallManifest(cwd, params) {
|
|
|
234
374
|
constraint_regions: [...constraintRegions.values()].sort(byPath)
|
|
235
375
|
};
|
|
236
376
|
await writeTextFile(
|
|
237
|
-
|
|
377
|
+
path4.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
|
|
238
378
|
JSON.stringify(manifest, null, 2)
|
|
239
379
|
);
|
|
240
380
|
}
|
|
241
381
|
async function readInstallManifest(cwd) {
|
|
242
|
-
const manifestPath2 =
|
|
382
|
+
const manifestPath2 = path4.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
|
|
243
383
|
const content = await readTextIfExists(manifestPath2);
|
|
244
384
|
if (content === null) {
|
|
245
385
|
return null;
|
|
@@ -265,32 +405,32 @@ function manifestPath(cwd, projectPath) {
|
|
|
265
405
|
return resolveProjectPath(cwd, projectPath);
|
|
266
406
|
}
|
|
267
407
|
function toProjectPath(cwd, filePath) {
|
|
268
|
-
const root =
|
|
269
|
-
const resolved =
|
|
408
|
+
const root = path4.resolve(cwd);
|
|
409
|
+
const resolved = path4.resolve(filePath);
|
|
270
410
|
assertPathInsideProject(root, resolved, filePath);
|
|
271
|
-
return
|
|
411
|
+
return path4.relative(root, resolved).split(path4.sep).join("/");
|
|
272
412
|
}
|
|
273
413
|
function normalizeCommand(command) {
|
|
274
414
|
return command.replace(/\\/g, "/").trim().replace(/\s+/g, " ");
|
|
275
415
|
}
|
|
276
416
|
async function sha256File(filePath) {
|
|
277
|
-
const content = await
|
|
417
|
+
const content = await readFile3(filePath);
|
|
278
418
|
return createHash("sha256").update(content).digest("hex");
|
|
279
419
|
}
|
|
280
420
|
function resolveProjectPath(cwd, projectPath) {
|
|
281
421
|
const normalized = projectPath.replace(/\\/g, "/");
|
|
282
422
|
const parts = normalized.split("/");
|
|
283
|
-
if (normalized.trim() === "" ||
|
|
423
|
+
if (normalized.trim() === "" || path4.isAbsolute(projectPath) || path4.posix.isAbsolute(normalized) || path4.win32.isAbsolute(projectPath) || /^[A-Za-z]:/.test(projectPath) || parts.some((part) => part === "..")) {
|
|
284
424
|
throw new Error(`Unsafe install manifest path: ${projectPath}`);
|
|
285
425
|
}
|
|
286
|
-
const root =
|
|
287
|
-
const resolved =
|
|
426
|
+
const root = path4.resolve(cwd);
|
|
427
|
+
const resolved = path4.resolve(root, normalized);
|
|
288
428
|
assertPathInsideProject(root, resolved, projectPath);
|
|
289
429
|
return resolved;
|
|
290
430
|
}
|
|
291
431
|
function assertPathInsideProject(root, resolvedPath, sourcePath) {
|
|
292
|
-
const relative =
|
|
293
|
-
if (!relative || relative.startsWith("..") ||
|
|
432
|
+
const relative = path4.relative(root, resolvedPath);
|
|
433
|
+
if (!relative || relative.startsWith("..") || path4.isAbsolute(relative)) {
|
|
294
434
|
throw new Error(`Unsafe install manifest path: ${sourcePath}`);
|
|
295
435
|
}
|
|
296
436
|
}
|
|
@@ -325,20 +465,250 @@ function collectHookCommands(hooks) {
|
|
|
325
465
|
return commands;
|
|
326
466
|
}
|
|
327
467
|
function extractHookPathFromCommand(command) {
|
|
328
|
-
|
|
329
|
-
return match?.[1] ?? null;
|
|
330
|
-
}
|
|
331
|
-
function byPath(a, b) {
|
|
332
|
-
return a.path.localeCompare(b.path);
|
|
468
|
+
return extractQuotedHookPath(command) ?? extractUnquotedHookPath(command);
|
|
333
469
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
470
|
+
function extractQuotedHookPath(command) {
|
|
471
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
472
|
+
const quote = command[index];
|
|
473
|
+
if (quote !== '"' && quote !== "'") {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (index > 0 && !/\s/.test(command[index - 1])) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const parsed = parseQuotedToken(command, index, quote);
|
|
480
|
+
if (!parsed) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const suffix = readPathSuffix(command, parsed.endIndex);
|
|
484
|
+
for (const candidate of [parsed.value + suffix, parsed.value]) {
|
|
485
|
+
if (isHookPythonPath(candidate)) {
|
|
486
|
+
return candidate;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
index = parsed.endIndex;
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
function parseQuotedToken(command, startIndex, quote) {
|
|
494
|
+
let value = "";
|
|
495
|
+
for (let index = startIndex + 1; index < command.length; index += 1) {
|
|
496
|
+
const char = command[index];
|
|
497
|
+
if (quote === '"' && char === "\\" && index + 1 < command.length) {
|
|
498
|
+
value += command[index + 1];
|
|
499
|
+
index += 1;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (char === quote) {
|
|
503
|
+
return { value, endIndex: index + 1 };
|
|
504
|
+
}
|
|
505
|
+
value += char;
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
function readPathSuffix(command, startIndex) {
|
|
510
|
+
if (command[startIndex] !== "/") {
|
|
511
|
+
return "";
|
|
512
|
+
}
|
|
513
|
+
let suffix = "";
|
|
514
|
+
for (let index = startIndex; index < command.length; index += 1) {
|
|
515
|
+
const char = command[index];
|
|
516
|
+
if (/\s/.test(char)) {
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
suffix += char;
|
|
520
|
+
}
|
|
521
|
+
return suffix;
|
|
522
|
+
}
|
|
523
|
+
function extractUnquotedHookPath(command) {
|
|
524
|
+
const match = command.match(/(?:^|\s)(\S+\/hooks\/\S+\.py)(?:\s|$)/);
|
|
525
|
+
return match?.[1] ?? null;
|
|
526
|
+
}
|
|
527
|
+
function isHookPythonPath(candidate) {
|
|
528
|
+
return /\/hooks\/\S+\.py$/.test(candidate);
|
|
529
|
+
}
|
|
530
|
+
function normalizeHookPath(cwd, hookPath) {
|
|
531
|
+
const normalized = hookPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
532
|
+
if (path4.isAbsolute(hookPath) || path4.posix.isAbsolute(normalized) || path4.win32.isAbsolute(hookPath)) {
|
|
533
|
+
return toProjectPath(cwd, hookPath);
|
|
534
|
+
}
|
|
535
|
+
return normalized;
|
|
536
|
+
}
|
|
537
|
+
function byPath(a, b) {
|
|
538
|
+
return a.path.localeCompare(b.path);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/utils/runtime-scaffold.ts
|
|
542
|
+
import path6 from "path";
|
|
543
|
+
|
|
544
|
+
// src/utils/template-paths.ts
|
|
545
|
+
import { existsSync } from "fs";
|
|
546
|
+
import path5 from "path";
|
|
547
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
548
|
+
function getTemplateRoot() {
|
|
549
|
+
const here = path5.dirname(fileURLToPath2(import.meta.url));
|
|
550
|
+
const candidates = [
|
|
551
|
+
path5.resolve(here, "../templates"),
|
|
552
|
+
path5.resolve(here, "../../templates"),
|
|
553
|
+
path5.resolve(process.cwd(), "src/templates"),
|
|
554
|
+
path5.resolve(process.cwd(), "templates")
|
|
555
|
+
];
|
|
556
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
557
|
+
if (!found) {
|
|
558
|
+
throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
|
|
559
|
+
}
|
|
560
|
+
return found;
|
|
561
|
+
}
|
|
562
|
+
function getTemplatePath(...segments) {
|
|
563
|
+
return path5.join(getTemplateRoot(), ...segments);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/utils/runtime-scaffold.ts
|
|
567
|
+
async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
568
|
+
const easyCodingDir = path6.join(cwd, EASY_CODING_DIR);
|
|
569
|
+
await ensureDir(easyCodingDir);
|
|
570
|
+
const configPath2 = path6.join(easyCodingDir, CONFIG_FILE);
|
|
571
|
+
if (!await pathExists(configPath2)) {
|
|
572
|
+
const projectName = path6.basename(cwd);
|
|
573
|
+
await writeConfigYaml(
|
|
574
|
+
configPath2,
|
|
575
|
+
createDefaultConfig({
|
|
576
|
+
projectName,
|
|
577
|
+
harnessVersion: VERSION,
|
|
578
|
+
agents,
|
|
579
|
+
supermodule: opts.supermodule
|
|
580
|
+
})
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
await ensureDir(path6.join(easyCodingDir, "tasks"));
|
|
584
|
+
await ensureDir(path6.join(easyCodingDir, SESSIONS_DIR));
|
|
585
|
+
await ensureDir(path6.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
|
|
586
|
+
await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
587
|
+
await writeMemoryScaffold(easyCodingDir);
|
|
588
|
+
await writeTemplatesScaffold(easyCodingDir);
|
|
589
|
+
}
|
|
590
|
+
async function writeTemplatesScaffold(easyCodingDir) {
|
|
591
|
+
const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
|
|
592
|
+
await ensureDir(templatesDir);
|
|
593
|
+
const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
|
|
594
|
+
const dest = path6.join(templatesDir, "dev-spec-skeleton.md");
|
|
595
|
+
await writeTextFile(dest, await readTextFile(src));
|
|
596
|
+
}
|
|
597
|
+
async function writeMemoryScaffold(easyCodingDir) {
|
|
598
|
+
const memoryDir = path6.join(easyCodingDir, MEMORY_DIR);
|
|
599
|
+
await ensureDir(path6.join(memoryDir, "short"));
|
|
600
|
+
await ensureDir(path6.join(memoryDir, "long"));
|
|
601
|
+
for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
|
|
602
|
+
const destination = path6.join(memoryDir, "long", file);
|
|
603
|
+
if (await pathExists(destination)) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
const templatePath = getTemplatePath("runtime", "memory", "long", file);
|
|
607
|
+
await writeTextFile(destination, await readTextFile(templatePath));
|
|
608
|
+
}
|
|
609
|
+
const shortTemplateDest = path6.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
|
|
610
|
+
if (!await pathExists(shortTemplateDest)) {
|
|
611
|
+
const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
|
|
612
|
+
await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/utils/task-json.ts
|
|
617
|
+
import { readdir } from "fs/promises";
|
|
618
|
+
import path7 from "path";
|
|
619
|
+
function createProjectInitTask(params) {
|
|
620
|
+
return {
|
|
621
|
+
type: "project-init",
|
|
622
|
+
status: "PENDING",
|
|
623
|
+
created_at: (params.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
624
|
+
created_by: "cli-init",
|
|
625
|
+
last_agent: "cli",
|
|
626
|
+
stage_history: [],
|
|
627
|
+
context: {
|
|
628
|
+
agents_installed: params.agents,
|
|
629
|
+
cli_version: VERSION,
|
|
630
|
+
project_path: params.cwd,
|
|
631
|
+
init_source: params.initSource ?? "fresh",
|
|
632
|
+
...params.legacyAssets ? { legacy_assets: params.legacyAssets } : {},
|
|
633
|
+
...params.legacyMissingHarnessFiles ? { legacy_missing_harness_files: params.legacyMissingHarnessFiles } : {}
|
|
634
|
+
},
|
|
635
|
+
init_log: []
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function getTaskJsonPath(cwd, taskId) {
|
|
639
|
+
return path7.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
|
|
640
|
+
}
|
|
641
|
+
async function writeTaskJson(filePath, task) {
|
|
642
|
+
await writeTextFile(filePath, JSON.stringify(task, null, 2));
|
|
643
|
+
}
|
|
644
|
+
async function readTaskJson(filePath) {
|
|
645
|
+
return JSON.parse(await readTextFile(filePath));
|
|
646
|
+
}
|
|
647
|
+
async function writeProjectInitTask(cwd, agents, options = {}) {
|
|
648
|
+
await writeTaskJson(
|
|
649
|
+
getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID),
|
|
650
|
+
createProjectInitTask({
|
|
651
|
+
cwd,
|
|
652
|
+
agents,
|
|
653
|
+
initSource: options.initSource,
|
|
654
|
+
legacyAssets: options.legacyAssets,
|
|
655
|
+
legacyMissingHarnessFiles: options.legacyMissingHarnessFiles
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
async function setPendingInitSince(cwd, version) {
|
|
660
|
+
const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
|
|
661
|
+
if (!await pathExists(filePath)) return;
|
|
662
|
+
const task = await readTaskJson(filePath);
|
|
663
|
+
if (task.status !== "COMPLETE") return;
|
|
664
|
+
task.pending_init_since = version;
|
|
665
|
+
await writeTaskJson(filePath, task);
|
|
666
|
+
}
|
|
667
|
+
async function listTasks(cwd) {
|
|
668
|
+
const tasksDir = path7.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
669
|
+
if (!await pathExists(tasksDir)) {
|
|
670
|
+
return [];
|
|
671
|
+
}
|
|
672
|
+
const entries = await readdir(tasksDir, { withFileTypes: true });
|
|
673
|
+
const tasks = [];
|
|
674
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
675
|
+
if (!entry.isDirectory()) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const taskPath = getTaskJsonPath(cwd, entry.name);
|
|
679
|
+
if (!await pathExists(taskPath)) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
tasks.push({ id: entry.name, task: await readTaskJson(taskPath) });
|
|
683
|
+
}
|
|
684
|
+
return tasks;
|
|
685
|
+
}
|
|
686
|
+
function summarizeTaskStatuses(tasks) {
|
|
687
|
+
return tasks.reduce(
|
|
688
|
+
(summary, item) => {
|
|
689
|
+
summary[item.task.status] = (summary[item.task.status] ?? 0) + 1;
|
|
690
|
+
return summary;
|
|
691
|
+
},
|
|
692
|
+
{}
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
function isActiveTask(task) {
|
|
696
|
+
return task.status !== "COMPLETE" && task.status !== "CLOSED";
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/commands/install-harness.ts
|
|
700
|
+
import path13 from "path";
|
|
701
|
+
|
|
702
|
+
// src/configurators/claude.ts
|
|
703
|
+
import path9 from "path";
|
|
704
|
+
|
|
705
|
+
// src/configurators/shared.ts
|
|
706
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
707
|
+
import path8 from "path";
|
|
708
|
+
|
|
709
|
+
// src/utils/marked-region.ts
|
|
710
|
+
var MarkedRegionError = class extends Error {
|
|
711
|
+
constructor(message) {
|
|
342
712
|
super(message);
|
|
343
713
|
this.name = "MarkedRegionError";
|
|
344
714
|
}
|
|
@@ -373,28 +743,6 @@ function removeMarkedRegion(content) {
|
|
|
373
743
|
` : "";
|
|
374
744
|
}
|
|
375
745
|
|
|
376
|
-
// src/utils/template-paths.ts
|
|
377
|
-
import { existsSync } from "fs";
|
|
378
|
-
import path3 from "path";
|
|
379
|
-
import { fileURLToPath } from "url";
|
|
380
|
-
function getTemplateRoot() {
|
|
381
|
-
const here = path3.dirname(fileURLToPath(import.meta.url));
|
|
382
|
-
const candidates = [
|
|
383
|
-
path3.resolve(here, "../templates"),
|
|
384
|
-
path3.resolve(here, "../../templates"),
|
|
385
|
-
path3.resolve(process.cwd(), "src/templates"),
|
|
386
|
-
path3.resolve(process.cwd(), "templates")
|
|
387
|
-
];
|
|
388
|
-
const found = candidates.find((candidate) => existsSync(candidate));
|
|
389
|
-
if (!found) {
|
|
390
|
-
throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
|
|
391
|
-
}
|
|
392
|
-
return found;
|
|
393
|
-
}
|
|
394
|
-
function getTemplatePath(...segments) {
|
|
395
|
-
return path3.join(getTemplateRoot(), ...segments);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
746
|
// src/configurators/shared.ts
|
|
399
747
|
var UnresolvedPlaceholderError = class extends Error {
|
|
400
748
|
constructor(contentName, placeholders) {
|
|
@@ -414,16 +762,43 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
|
|
|
414
762
|
}
|
|
415
763
|
return resolved;
|
|
416
764
|
}
|
|
765
|
+
function withInstallPaths(cwd, ctx) {
|
|
766
|
+
const hooksDir = toPosixPath(path8.resolve(cwd, ctx.platform_config_dir, "hooks"));
|
|
767
|
+
return {
|
|
768
|
+
...ctx,
|
|
769
|
+
platform_hooks_dir_abs: hooksDir,
|
|
770
|
+
platform_hooks_dir_shell: jsonStringContent(shellDoubleQuoteArg(hooksDir))
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function renderHookCommand(cwd, ctx, scriptName, platform = process.platform) {
|
|
774
|
+
const hooksDir = toPosixPath(path8.resolve(cwd, ctx.platform_config_dir, "hooks"));
|
|
775
|
+
return `${ctx.python_cmd} ${shellDoubleQuoteArg(hooksDir, platform)}/${scriptName}`;
|
|
776
|
+
}
|
|
777
|
+
function shellDoubleQuoteArg(value, platform = process.platform) {
|
|
778
|
+
if (platform === "win32") {
|
|
779
|
+
if (value.includes('"')) {
|
|
780
|
+
throw new Error("Windows hook paths cannot contain double quotes.");
|
|
781
|
+
}
|
|
782
|
+
return `"${value}"`;
|
|
783
|
+
}
|
|
784
|
+
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
785
|
+
}
|
|
786
|
+
function toPosixPath(filePath) {
|
|
787
|
+
return filePath.split(path8.sep).join("/");
|
|
788
|
+
}
|
|
789
|
+
function jsonStringContent(value) {
|
|
790
|
+
return JSON.stringify(value).slice(1, -1);
|
|
791
|
+
}
|
|
417
792
|
async function resolveSkills(ctx) {
|
|
418
793
|
const skillsRoot = getTemplatePath("common", "skills");
|
|
419
|
-
const entries = await
|
|
794
|
+
const entries = await readdir2(skillsRoot);
|
|
420
795
|
const skills = [];
|
|
421
796
|
for (const entry of entries.sort()) {
|
|
422
|
-
const skillDir =
|
|
797
|
+
const skillDir = path8.join(skillsRoot, entry);
|
|
423
798
|
if (!await isDirectory(skillDir)) {
|
|
424
799
|
continue;
|
|
425
800
|
}
|
|
426
|
-
const content = await readTextFile(
|
|
801
|
+
const content = await readTextFile(path8.join(skillDir, "SKILL.md"));
|
|
427
802
|
skills.push({
|
|
428
803
|
name: entry,
|
|
429
804
|
content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
|
|
@@ -436,10 +811,10 @@ async function resolveBundledSkills(ctx) {
|
|
|
436
811
|
if (!await pathExists(bundledRoot)) {
|
|
437
812
|
return [];
|
|
438
813
|
}
|
|
439
|
-
const entries = await
|
|
814
|
+
const entries = await readdir2(bundledRoot);
|
|
440
815
|
const bundled = [];
|
|
441
816
|
for (const entry of entries.sort()) {
|
|
442
|
-
const sourceDir =
|
|
817
|
+
const sourceDir = path8.join(bundledRoot, entry);
|
|
443
818
|
if (await isDirectory(sourceDir)) {
|
|
444
819
|
bundled.push({ name: entry, sourceDir, context: ctx });
|
|
445
820
|
}
|
|
@@ -450,13 +825,13 @@ async function writeSkills(dir, skills, bundled) {
|
|
|
450
825
|
await ensureDir(dir);
|
|
451
826
|
const written = [];
|
|
452
827
|
for (const skill of skills) {
|
|
453
|
-
const destination =
|
|
828
|
+
const destination = path8.join(dir, skill.name, "SKILL.md");
|
|
454
829
|
await writeTextFile(destination, skill.content);
|
|
455
830
|
written.push(destination);
|
|
456
831
|
}
|
|
457
832
|
for (const skill of bundled) {
|
|
458
833
|
written.push(
|
|
459
|
-
...await copyTemplateDirectory(skill.sourceDir,
|
|
834
|
+
...await copyTemplateDirectory(skill.sourceDir, path8.join(dir, skill.name), skill.context)
|
|
460
835
|
);
|
|
461
836
|
}
|
|
462
837
|
return written;
|
|
@@ -464,14 +839,14 @@ async function writeSkills(dir, skills, bundled) {
|
|
|
464
839
|
async function writeSharedHooks(dir, platform, opts = {}) {
|
|
465
840
|
const hooksRoot = getTemplatePath("shared-hooks");
|
|
466
841
|
const ctx = PLATFORM_META[platform].templateContext;
|
|
467
|
-
const entries = await
|
|
842
|
+
const entries = await readdir2(hooksRoot);
|
|
468
843
|
await ensureDir(dir);
|
|
469
844
|
const written = [];
|
|
470
845
|
for (const entry of entries.sort()) {
|
|
471
846
|
if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
|
|
472
847
|
continue;
|
|
473
848
|
}
|
|
474
|
-
const sourcePath =
|
|
849
|
+
const sourcePath = path8.join(hooksRoot, entry);
|
|
475
850
|
if ((await stat2(sourcePath)).isDirectory()) {
|
|
476
851
|
continue;
|
|
477
852
|
}
|
|
@@ -480,7 +855,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
|
|
|
480
855
|
ctx,
|
|
481
856
|
`shared-hooks/${entry}`
|
|
482
857
|
);
|
|
483
|
-
const destination =
|
|
858
|
+
const destination = path8.join(dir, entry);
|
|
484
859
|
await writeTextFile(destination, content);
|
|
485
860
|
await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
|
|
486
861
|
written.push(destination);
|
|
@@ -491,16 +866,19 @@ async function copyPlatformTemplates(platformTemplateDir, destination, skipDirs,
|
|
|
491
866
|
const source = getTemplatePath(platformTemplateDir);
|
|
492
867
|
return copyTemplateDirectory(source, destination, ctx, new Set(skipDirs));
|
|
493
868
|
}
|
|
494
|
-
async function writeMainConstraint(cwd, platform) {
|
|
869
|
+
async function writeMainConstraint(cwd, platform, opts = {}) {
|
|
495
870
|
const meta = PLATFORM_META[platform];
|
|
496
|
-
const ctx =
|
|
871
|
+
const ctx = {
|
|
872
|
+
...meta.templateContext,
|
|
873
|
+
supermodule_boundary: renderSupermoduleBoundary(opts.supermodule)
|
|
874
|
+
};
|
|
497
875
|
const templateName = `${meta.mainConstraint}.tpl`;
|
|
498
876
|
const template = await readTextFile(getTemplatePath("main-constraint", templateName));
|
|
499
877
|
const generated = resolvePlaceholders(template, ctx, `main-constraint/${templateName}`);
|
|
500
878
|
if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
|
|
501
879
|
throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
|
|
502
880
|
}
|
|
503
|
-
const destination =
|
|
881
|
+
const destination = path8.join(cwd, meta.mainConstraint);
|
|
504
882
|
const current = await readTextIfExists(destination);
|
|
505
883
|
if (current) {
|
|
506
884
|
const markerContent = extractMarkedRegion(generated);
|
|
@@ -513,15 +891,37 @@ async function writeMainConstraint(cwd, platform) {
|
|
|
513
891
|
}
|
|
514
892
|
return destination;
|
|
515
893
|
}
|
|
894
|
+
function renderSupermoduleBoundary(boundary) {
|
|
895
|
+
if (!boundary || boundary.submodulePaths.length === 0) {
|
|
896
|
+
return "";
|
|
897
|
+
}
|
|
898
|
+
const submodules = boundary.submodulePaths.map((submodulePath) => `- \`${submodulePath}\``);
|
|
899
|
+
return [
|
|
900
|
+
"",
|
|
901
|
+
"## Supermodule Boundary",
|
|
902
|
+
"",
|
|
903
|
+
"This directory is a git supermodule root. The following subdirectories are independent",
|
|
904
|
+
"submodule harness roots with their own git boundaries and Easy Coding runtime:",
|
|
905
|
+
"",
|
|
906
|
+
...submodules,
|
|
907
|
+
"",
|
|
908
|
+
"- Cross-repo work launched from this root uses the parent `.easy-coding` task, session, state, and spec as the source of truth.",
|
|
909
|
+
"- Child `.easy-coding` directories are not parent workflow state sources.",
|
|
910
|
+
"- During memory archive, technical memory that belongs to a child repo is written only to that child's `.easy-coding/memory`; do not write child task/session/state files from the parent.",
|
|
911
|
+
"- Commit submodule changes in two steps: push each child repo first, then commit and push the parent gitlink update.",
|
|
912
|
+
"- Parent memory should keep cross-repo context; child technical details should follow the owning child repo.",
|
|
913
|
+
""
|
|
914
|
+
].join("\n");
|
|
915
|
+
}
|
|
516
916
|
async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__PURE__ */ new Set()) {
|
|
517
917
|
await ensureDir(destination);
|
|
518
918
|
const written = [];
|
|
519
|
-
for (const entry of (await
|
|
919
|
+
for (const entry of (await readdir2(source)).sort()) {
|
|
520
920
|
if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
|
|
521
921
|
continue;
|
|
522
922
|
}
|
|
523
|
-
const sourcePath =
|
|
524
|
-
const destinationPath =
|
|
923
|
+
const sourcePath = path8.join(source, entry);
|
|
924
|
+
const destinationPath = path8.join(destination, stripTemplateExtension(entry));
|
|
525
925
|
const sourceStat = await stat2(sourcePath);
|
|
526
926
|
if (sourceStat.isDirectory()) {
|
|
527
927
|
written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
|
|
@@ -541,63 +941,65 @@ function stripTemplateExtension(fileName) {
|
|
|
541
941
|
}
|
|
542
942
|
|
|
543
943
|
// src/configurators/claude.ts
|
|
544
|
-
async function configureClaude(cwd) {
|
|
944
|
+
async function configureClaude(cwd, opts = {}) {
|
|
545
945
|
const platform = "claude-code";
|
|
546
946
|
const meta = PLATFORM_META[platform];
|
|
547
|
-
const ctx = meta.templateContext;
|
|
548
|
-
const dest =
|
|
549
|
-
const hookConfigPath =
|
|
947
|
+
const ctx = withInstallPaths(cwd, meta.templateContext);
|
|
948
|
+
const dest = path9.join(cwd, ".claude");
|
|
949
|
+
const hookConfigPath = path9.join(cwd, meta.hookConfigFile);
|
|
550
950
|
const artifacts = [];
|
|
551
951
|
const platformFiles = await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
|
|
552
952
|
artifacts.push(
|
|
553
953
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
554
954
|
(filePath) => fileArtifact(
|
|
555
955
|
filePath,
|
|
556
|
-
filePath.startsWith(
|
|
956
|
+
filePath.startsWith(path9.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
557
957
|
platform
|
|
558
958
|
)
|
|
559
959
|
)
|
|
560
960
|
);
|
|
561
961
|
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
562
962
|
artifacts.push(
|
|
563
|
-
...(await writeSharedHooks(
|
|
963
|
+
...(await writeSharedHooks(path9.join(dest, "hooks"), platform)).map(
|
|
564
964
|
(filePath) => fileArtifact(filePath, "hook", platform)
|
|
565
965
|
)
|
|
566
966
|
);
|
|
567
967
|
artifacts.push(
|
|
568
968
|
...(await writeSkills(
|
|
569
|
-
|
|
969
|
+
path9.join(cwd, meta.skillsDir),
|
|
570
970
|
await resolveSkills(ctx),
|
|
571
971
|
await resolveBundledSkills(ctx)
|
|
572
972
|
)).map((filePath) => fileArtifact(filePath, "skill", platform))
|
|
573
973
|
);
|
|
574
|
-
artifacts.push(
|
|
974
|
+
artifacts.push(
|
|
975
|
+
constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
|
|
976
|
+
);
|
|
575
977
|
return artifacts;
|
|
576
978
|
}
|
|
577
979
|
|
|
578
980
|
// src/configurators/codex.ts
|
|
579
|
-
import
|
|
580
|
-
async function configureCodex(cwd) {
|
|
981
|
+
import path10 from "path";
|
|
982
|
+
async function configureCodex(cwd, opts = {}) {
|
|
581
983
|
const platform = "codex";
|
|
582
984
|
const meta = PLATFORM_META[platform];
|
|
583
|
-
const ctx = meta.templateContext;
|
|
584
|
-
const hookConfigPath =
|
|
985
|
+
const ctx = withInstallPaths(cwd, meta.templateContext);
|
|
986
|
+
const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
|
|
585
987
|
const artifacts = [];
|
|
586
988
|
artifacts.push(
|
|
587
989
|
...(await writeSkills(
|
|
588
|
-
|
|
990
|
+
path10.join(cwd, meta.skillsDir),
|
|
589
991
|
await resolveSkills(ctx),
|
|
590
992
|
await resolveBundledSkills(ctx)
|
|
591
993
|
)).map((filePath) => fileArtifact(filePath, "skill", platform))
|
|
592
994
|
);
|
|
593
995
|
artifacts.push(
|
|
594
|
-
...(await writeSharedHooks(
|
|
996
|
+
...(await writeSharedHooks(path10.join(cwd, meta.hooksDir), platform, {
|
|
595
997
|
skipSubagentContext: true
|
|
596
998
|
})).map((filePath) => fileArtifact(filePath, "hook", platform))
|
|
597
999
|
);
|
|
598
1000
|
const platformFiles = await copyPlatformTemplates(
|
|
599
1001
|
"codex",
|
|
600
|
-
|
|
1002
|
+
path10.join(cwd, ".codex"),
|
|
601
1003
|
["hooks"],
|
|
602
1004
|
ctx
|
|
603
1005
|
);
|
|
@@ -605,28 +1007,30 @@ async function configureCodex(cwd) {
|
|
|
605
1007
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
606
1008
|
(filePath) => fileArtifact(
|
|
607
1009
|
filePath,
|
|
608
|
-
filePath.startsWith(
|
|
1010
|
+
filePath.startsWith(path10.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
609
1011
|
platform
|
|
610
1012
|
)
|
|
611
1013
|
)
|
|
612
1014
|
);
|
|
613
1015
|
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
614
|
-
artifacts.push(
|
|
1016
|
+
artifacts.push(
|
|
1017
|
+
constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
|
|
1018
|
+
);
|
|
615
1019
|
return artifacts;
|
|
616
1020
|
}
|
|
617
1021
|
|
|
618
1022
|
// src/configurators/qoder.ts
|
|
619
|
-
import { readdir as
|
|
620
|
-
import
|
|
1023
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
1024
|
+
import path12 from "path";
|
|
621
1025
|
|
|
622
1026
|
// src/utils/platform-paths.ts
|
|
623
1027
|
import { existsSync as existsSync2 } from "fs";
|
|
624
|
-
import
|
|
1028
|
+
import path11 from "path";
|
|
625
1029
|
function detectQoderCnVariant(cwd) {
|
|
626
1030
|
if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
|
|
627
1031
|
return true;
|
|
628
1032
|
}
|
|
629
|
-
return existsSync2(
|
|
1033
|
+
return existsSync2(path11.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
|
|
630
1034
|
}
|
|
631
1035
|
function resolvePlatformMeta(cwd, platform) {
|
|
632
1036
|
const meta = PLATFORM_META[platform];
|
|
@@ -657,12 +1061,12 @@ function resolveQoderMetaForBaseDir(baseDir) {
|
|
|
657
1061
|
|
|
658
1062
|
// src/configurators/qoder.ts
|
|
659
1063
|
async function claudeHarnessSkillsExist(cwd) {
|
|
660
|
-
return pathExists(
|
|
1064
|
+
return pathExists(path12.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
|
|
661
1065
|
}
|
|
662
1066
|
async function listFilesRecursive(dir) {
|
|
663
1067
|
let entries;
|
|
664
1068
|
try {
|
|
665
|
-
entries = await
|
|
1069
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
666
1070
|
} catch (error) {
|
|
667
1071
|
if (error.code === "ENOENT") {
|
|
668
1072
|
return [];
|
|
@@ -671,7 +1075,7 @@ async function listFilesRecursive(dir) {
|
|
|
671
1075
|
}
|
|
672
1076
|
const files = [];
|
|
673
1077
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
674
|
-
const entryPath =
|
|
1078
|
+
const entryPath = path12.join(dir, entry.name);
|
|
675
1079
|
if (entry.isDirectory()) {
|
|
676
1080
|
files.push(...await listFilesRecursive(entryPath));
|
|
677
1081
|
continue;
|
|
@@ -685,32 +1089,32 @@ async function listFilesRecursive(dir) {
|
|
|
685
1089
|
async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
|
|
686
1090
|
const artifacts = [];
|
|
687
1091
|
for (const skillName of skillNames) {
|
|
688
|
-
for (const filePath of await listFilesRecursive(
|
|
1092
|
+
for (const filePath of await listFilesRecursive(path12.join(skillsDir, skillName))) {
|
|
689
1093
|
artifacts.push(fileArtifact(filePath, "skill", platform));
|
|
690
1094
|
}
|
|
691
1095
|
}
|
|
692
1096
|
return artifacts;
|
|
693
1097
|
}
|
|
694
|
-
async function configureQoder(cwd) {
|
|
1098
|
+
async function configureQoder(cwd, opts = {}) {
|
|
695
1099
|
const platform = "qoder";
|
|
696
1100
|
const meta = resolvePlatformMeta(cwd, platform);
|
|
697
|
-
const ctx = meta.templateContext;
|
|
698
|
-
const dest =
|
|
699
|
-
const hookConfigPath =
|
|
1101
|
+
const ctx = withInstallPaths(cwd, meta.templateContext);
|
|
1102
|
+
const dest = path12.join(cwd, ctx.platform_config_dir);
|
|
1103
|
+
const hookConfigPath = path12.join(cwd, meta.hookConfigFile);
|
|
700
1104
|
const artifacts = [];
|
|
701
1105
|
const platformFiles = await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
|
|
702
1106
|
artifacts.push(
|
|
703
1107
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
704
1108
|
(filePath) => fileArtifact(
|
|
705
1109
|
filePath,
|
|
706
|
-
filePath.startsWith(
|
|
1110
|
+
filePath.startsWith(path12.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
707
1111
|
platform
|
|
708
1112
|
)
|
|
709
1113
|
)
|
|
710
1114
|
);
|
|
711
1115
|
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
712
1116
|
artifacts.push(
|
|
713
|
-
...(await writeSharedHooks(
|
|
1117
|
+
...(await writeSharedHooks(path12.join(dest, "hooks"), platform)).map(
|
|
714
1118
|
(filePath) => fileArtifact(filePath, "hook", platform)
|
|
715
1119
|
)
|
|
716
1120
|
);
|
|
@@ -718,20 +1122,22 @@ async function configureQoder(cwd) {
|
|
|
718
1122
|
const bundledSkills = await resolveBundledSkills(ctx);
|
|
719
1123
|
if (!await claudeHarnessSkillsExist(cwd)) {
|
|
720
1124
|
artifacts.push(
|
|
721
|
-
...(await writeSkills(
|
|
1125
|
+
...(await writeSkills(path12.join(dest, "skills"), skills, bundledSkills)).map(
|
|
722
1126
|
(filePath) => fileArtifact(filePath, "skill", platform)
|
|
723
1127
|
)
|
|
724
1128
|
);
|
|
725
1129
|
} else {
|
|
726
1130
|
artifacts.push(
|
|
727
1131
|
...await existingManagedSkillArtifacts(
|
|
728
|
-
|
|
1132
|
+
path12.join(dest, "skills"),
|
|
729
1133
|
[...skills.map((skill) => skill.name), ...bundledSkills.map((skill) => skill.name)],
|
|
730
1134
|
platform
|
|
731
1135
|
)
|
|
732
1136
|
);
|
|
733
1137
|
}
|
|
734
|
-
artifacts.push(
|
|
1138
|
+
artifacts.push(
|
|
1139
|
+
constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
|
|
1140
|
+
);
|
|
735
1141
|
return artifacts;
|
|
736
1142
|
}
|
|
737
1143
|
|
|
@@ -742,261 +1148,64 @@ var CONFIGURATORS = {
|
|
|
742
1148
|
qoder: configureQoder
|
|
743
1149
|
};
|
|
744
1150
|
|
|
745
|
-
// src/
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
var PACKAGE_NAME = packageMetadata.name;
|
|
751
|
-
var VERSION = packageMetadata.version;
|
|
752
|
-
function readPackageMetadata() {
|
|
753
|
-
const here = path9.dirname(fileURLToPath2(import.meta.url));
|
|
754
|
-
const candidates = [
|
|
755
|
-
path9.resolve(here, "../../package.json"),
|
|
756
|
-
path9.resolve(here, "../package.json"),
|
|
757
|
-
path9.resolve(process.cwd(), "package.json")
|
|
758
|
-
];
|
|
759
|
-
for (const candidate of candidates) {
|
|
760
|
-
try {
|
|
761
|
-
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
762
|
-
if (parsed.name && parsed.version) {
|
|
763
|
-
return { name: parsed.name, version: parsed.version };
|
|
764
|
-
}
|
|
765
|
-
} catch {
|
|
766
|
-
}
|
|
1151
|
+
// src/commands/install-harness.ts
|
|
1152
|
+
async function configurePlatformsForDir(targetDir, platforms, boundary) {
|
|
1153
|
+
const artifacts = [];
|
|
1154
|
+
for (const platform of platforms) {
|
|
1155
|
+
artifacts.push(...await CONFIGURATORS[platform](targetDir, { supermodule: boundary }));
|
|
767
1156
|
}
|
|
768
|
-
|
|
1157
|
+
return artifacts;
|
|
769
1158
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1159
|
+
async function installHarnessToDir(targetDir, platforms, ctx) {
|
|
1160
|
+
const artifacts = await configurePlatformsForDir(targetDir, platforms, boundaryFromContext(ctx));
|
|
1161
|
+
await writeRuntimeScaffold(targetDir, platforms, {
|
|
1162
|
+
supermodule: supermoduleConfigFromContext(ctx)
|
|
1163
|
+
});
|
|
1164
|
+
await writeInstallManifest(targetDir, {
|
|
1165
|
+
harnessVersion: VERSION,
|
|
1166
|
+
agents: platforms,
|
|
1167
|
+
artifacts
|
|
1168
|
+
});
|
|
1169
|
+
await writeProjectInitTask(targetDir, platforms, {
|
|
1170
|
+
initSource: ctx.initSource,
|
|
1171
|
+
legacyAssets: ctx.legacyAssets,
|
|
1172
|
+
legacyMissingHarnessFiles: ctx.legacyMissingHarnessFiles
|
|
1173
|
+
});
|
|
1174
|
+
await ensureEasyCodingSessionsIgnored(targetDir);
|
|
1175
|
+
return artifacts;
|
|
782
1176
|
}
|
|
783
|
-
function
|
|
784
|
-
if (
|
|
785
|
-
return {
|
|
1177
|
+
function supermoduleConfigFromContext(ctx) {
|
|
1178
|
+
if (ctx.role === "super-parent") {
|
|
1179
|
+
return {
|
|
1180
|
+
role: "super-parent",
|
|
1181
|
+
submodules: ctx.submodulePaths ?? []
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
if (ctx.role === "submodule-child") {
|
|
1185
|
+
return {
|
|
1186
|
+
role: "submodule-child",
|
|
1187
|
+
parent: ctx.parent
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
return { role: "standalone" };
|
|
1191
|
+
}
|
|
1192
|
+
async function refreshSupermoduleParent(targetDir, platforms, submodulePaths) {
|
|
1193
|
+
const supermodule = {
|
|
1194
|
+
role: "super-parent",
|
|
1195
|
+
submodules: submodulePaths
|
|
1196
|
+
};
|
|
1197
|
+
await updateSupermoduleConfig(path13.join(targetDir, EASY_CODING_DIR, CONFIG_FILE), supermodule);
|
|
1198
|
+
for (const platform of platforms) {
|
|
1199
|
+
await writeMainConstraint(targetDir, platform, {
|
|
1200
|
+
supermodule: { submodulePaths }
|
|
1201
|
+
});
|
|
786
1202
|
}
|
|
787
|
-
|
|
788
|
-
|
|
1203
|
+
}
|
|
1204
|
+
function boundaryFromContext(ctx) {
|
|
1205
|
+
if (ctx.role !== "super-parent") {
|
|
1206
|
+
return void 0;
|
|
789
1207
|
}
|
|
790
|
-
return {
|
|
791
|
-
}
|
|
792
|
-
function colorizeBanner(art) {
|
|
793
|
-
const colors = [chalk.cyanBright, chalk.cyan, chalk.blueBright, chalk.blue];
|
|
794
|
-
return art.split("\n").map((line, index) => colors[index % colors.length](line)).join("\n");
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// src/utils/config-yaml.ts
|
|
798
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
799
|
-
import YAML, { isScalar, isSeq, parseDocument } from "yaml";
|
|
800
|
-
function createDefaultConfig(params) {
|
|
801
|
-
return {
|
|
802
|
-
version: 1,
|
|
803
|
-
harness_version: params.harnessVersion,
|
|
804
|
-
agents: params.agents,
|
|
805
|
-
project: {
|
|
806
|
-
name: params.projectName
|
|
807
|
-
},
|
|
808
|
-
memory: {
|
|
809
|
-
short_term_max: 10,
|
|
810
|
-
short_term_keep: 5,
|
|
811
|
-
schema_version: 2
|
|
812
|
-
},
|
|
813
|
-
tasks: {
|
|
814
|
-
auto_archive_days: 30
|
|
815
|
-
},
|
|
816
|
-
behavior: {
|
|
817
|
-
strict_confirm: true,
|
|
818
|
-
auto_mode: false
|
|
819
|
-
}
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
-
function stringifyConfig(config) {
|
|
823
|
-
return YAML.stringify(config);
|
|
824
|
-
}
|
|
825
|
-
async function writeConfigYaml(filePath, config) {
|
|
826
|
-
await writeTextFile(filePath, stringifyConfig(config));
|
|
827
|
-
}
|
|
828
|
-
async function readConfigYaml(filePath) {
|
|
829
|
-
const content = await readFile3(filePath, "utf8");
|
|
830
|
-
return YAML.parse(content);
|
|
831
|
-
}
|
|
832
|
-
async function updateConfigYaml(filePath, updater) {
|
|
833
|
-
const content = await readFile3(filePath, "utf8");
|
|
834
|
-
const document = parseDocument(content);
|
|
835
|
-
const config = document.toJSON();
|
|
836
|
-
updater(config);
|
|
837
|
-
for (const [key, value] of Object.entries(config)) {
|
|
838
|
-
document.set(key, value);
|
|
839
|
-
}
|
|
840
|
-
await writeTextFile(filePath, document.toString());
|
|
841
|
-
return config;
|
|
842
|
-
}
|
|
843
|
-
async function addAgentsToConfig(filePath, agents) {
|
|
844
|
-
return updateConfigYaml(filePath, (config) => {
|
|
845
|
-
const merged = /* @__PURE__ */ new Set([...config.agents ?? [], ...agents]);
|
|
846
|
-
config.agents = [...merged];
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
async function updateHarnessVersion(filePath, version) {
|
|
850
|
-
return updateConfigYaml(filePath, (config) => {
|
|
851
|
-
config.harness_version = version;
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// src/utils/gitignore.ts
|
|
856
|
-
import path10 from "path";
|
|
857
|
-
async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550 easy-coding-harness (auto-generated) \u2550\u2550\u2550\n# Personal runtime state; do not commit") {
|
|
858
|
-
const gitignorePath = path10.join(cwd, ".gitignore");
|
|
859
|
-
const current = await readTextIfExists(gitignorePath) ?? "";
|
|
860
|
-
const lines = current.split(/\r?\n/).map((line) => line.trim());
|
|
861
|
-
if (lines.includes(entry)) {
|
|
862
|
-
return false;
|
|
863
|
-
}
|
|
864
|
-
const prefix = current.trimEnd();
|
|
865
|
-
const next = [prefix, heading, entry].filter(Boolean).join("\n");
|
|
866
|
-
await writeTextFile(gitignorePath, next);
|
|
867
|
-
return true;
|
|
868
|
-
}
|
|
869
|
-
async function ensureEasyCodingSessionsIgnored(cwd) {
|
|
870
|
-
return ensureGitignoreEntry(cwd, SESSIONS_GITIGNORE_ENTRY);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// src/utils/runtime-scaffold.ts
|
|
874
|
-
import path11 from "path";
|
|
875
|
-
async function writeRuntimeScaffold(cwd, agents) {
|
|
876
|
-
const easyCodingDir = path11.join(cwd, EASY_CODING_DIR);
|
|
877
|
-
await ensureDir(easyCodingDir);
|
|
878
|
-
const configPath = path11.join(easyCodingDir, CONFIG_FILE);
|
|
879
|
-
if (!await pathExists(configPath)) {
|
|
880
|
-
const projectName = path11.basename(cwd);
|
|
881
|
-
await writeConfigYaml(
|
|
882
|
-
configPath,
|
|
883
|
-
createDefaultConfig({ projectName, harnessVersion: VERSION, agents })
|
|
884
|
-
);
|
|
885
|
-
}
|
|
886
|
-
await ensureDir(path11.join(easyCodingDir, "tasks"));
|
|
887
|
-
await ensureDir(path11.join(easyCodingDir, SESSIONS_DIR));
|
|
888
|
-
await ensureDir(path11.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
|
|
889
|
-
await ensureDir(path11.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
890
|
-
await writeMemoryScaffold(easyCodingDir);
|
|
891
|
-
await writeTemplatesScaffold(easyCodingDir);
|
|
892
|
-
}
|
|
893
|
-
async function writeTemplatesScaffold(easyCodingDir) {
|
|
894
|
-
const templatesDir = path11.join(easyCodingDir, TEMPLATES_DIR);
|
|
895
|
-
await ensureDir(templatesDir);
|
|
896
|
-
const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
|
|
897
|
-
const dest = path11.join(templatesDir, "dev-spec-skeleton.md");
|
|
898
|
-
await writeTextFile(dest, await readTextFile(src));
|
|
899
|
-
}
|
|
900
|
-
async function writeMemoryScaffold(easyCodingDir) {
|
|
901
|
-
const memoryDir = path11.join(easyCodingDir, MEMORY_DIR);
|
|
902
|
-
await ensureDir(path11.join(memoryDir, "short"));
|
|
903
|
-
await ensureDir(path11.join(memoryDir, "long"));
|
|
904
|
-
for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
|
|
905
|
-
const destination = path11.join(memoryDir, "long", file);
|
|
906
|
-
if (await pathExists(destination)) {
|
|
907
|
-
continue;
|
|
908
|
-
}
|
|
909
|
-
const templatePath = getTemplatePath("runtime", "memory", "long", file);
|
|
910
|
-
await writeTextFile(destination, await readTextFile(templatePath));
|
|
911
|
-
}
|
|
912
|
-
const shortTemplateDest = path11.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
|
|
913
|
-
if (!await pathExists(shortTemplateDest)) {
|
|
914
|
-
const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
|
|
915
|
-
await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// src/utils/task-json.ts
|
|
920
|
-
import { readdir as readdir3 } from "fs/promises";
|
|
921
|
-
import path12 from "path";
|
|
922
|
-
function createProjectInitTask(params) {
|
|
923
|
-
return {
|
|
924
|
-
type: "project-init",
|
|
925
|
-
status: "PENDING",
|
|
926
|
-
created_at: (params.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
927
|
-
created_by: "cli-init",
|
|
928
|
-
last_agent: "cli",
|
|
929
|
-
stage_history: [],
|
|
930
|
-
context: {
|
|
931
|
-
agents_installed: params.agents,
|
|
932
|
-
cli_version: VERSION,
|
|
933
|
-
project_path: params.cwd,
|
|
934
|
-
init_source: params.initSource ?? "fresh",
|
|
935
|
-
...params.legacyAssets ? { legacy_assets: params.legacyAssets } : {},
|
|
936
|
-
...params.legacyMissingHarnessFiles ? { legacy_missing_harness_files: params.legacyMissingHarnessFiles } : {}
|
|
937
|
-
},
|
|
938
|
-
init_log: []
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
function getTaskJsonPath(cwd, taskId) {
|
|
942
|
-
return path12.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
|
|
943
|
-
}
|
|
944
|
-
async function writeTaskJson(filePath, task) {
|
|
945
|
-
await writeTextFile(filePath, JSON.stringify(task, null, 2));
|
|
946
|
-
}
|
|
947
|
-
async function readTaskJson(filePath) {
|
|
948
|
-
return JSON.parse(await readTextFile(filePath));
|
|
949
|
-
}
|
|
950
|
-
async function writeProjectInitTask(cwd, agents, options = {}) {
|
|
951
|
-
await writeTaskJson(
|
|
952
|
-
getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID),
|
|
953
|
-
createProjectInitTask({
|
|
954
|
-
cwd,
|
|
955
|
-
agents,
|
|
956
|
-
initSource: options.initSource,
|
|
957
|
-
legacyAssets: options.legacyAssets,
|
|
958
|
-
legacyMissingHarnessFiles: options.legacyMissingHarnessFiles
|
|
959
|
-
})
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
|
-
async function setPendingInitSince(cwd, version) {
|
|
963
|
-
const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
|
|
964
|
-
if (!await pathExists(filePath)) return;
|
|
965
|
-
const task = await readTaskJson(filePath);
|
|
966
|
-
if (task.status !== "COMPLETE") return;
|
|
967
|
-
task.pending_init_since = version;
|
|
968
|
-
await writeTaskJson(filePath, task);
|
|
969
|
-
}
|
|
970
|
-
async function listTasks(cwd) {
|
|
971
|
-
const tasksDir = path12.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
972
|
-
if (!await pathExists(tasksDir)) {
|
|
973
|
-
return [];
|
|
974
|
-
}
|
|
975
|
-
const entries = await readdir3(tasksDir, { withFileTypes: true });
|
|
976
|
-
const tasks = [];
|
|
977
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
978
|
-
if (!entry.isDirectory()) {
|
|
979
|
-
continue;
|
|
980
|
-
}
|
|
981
|
-
const taskPath = getTaskJsonPath(cwd, entry.name);
|
|
982
|
-
if (!await pathExists(taskPath)) {
|
|
983
|
-
continue;
|
|
984
|
-
}
|
|
985
|
-
tasks.push({ id: entry.name, task: await readTaskJson(taskPath) });
|
|
986
|
-
}
|
|
987
|
-
return tasks;
|
|
988
|
-
}
|
|
989
|
-
function summarizeTaskStatuses(tasks) {
|
|
990
|
-
return tasks.reduce(
|
|
991
|
-
(summary, item) => {
|
|
992
|
-
summary[item.task.status] = (summary[item.task.status] ?? 0) + 1;
|
|
993
|
-
return summary;
|
|
994
|
-
},
|
|
995
|
-
{}
|
|
996
|
-
);
|
|
997
|
-
}
|
|
998
|
-
function isActiveTask(task) {
|
|
999
|
-
return task.status !== "COMPLETE" && task.status !== "CLOSED";
|
|
1208
|
+
return { submodulePaths: ctx.submodulePaths ?? [] };
|
|
1000
1209
|
}
|
|
1001
1210
|
|
|
1002
1211
|
// src/commands/platforms.ts
|
|
@@ -1043,49 +1252,453 @@ async function resolvePlatforms(opts, defaults = ["claude-code"]) {
|
|
|
1043
1252
|
cancel("Platform selection cancelled.");
|
|
1044
1253
|
process.exit(1);
|
|
1045
1254
|
}
|
|
1046
|
-
if (shouldInstall) {
|
|
1047
|
-
return result;
|
|
1255
|
+
if (shouldInstall) {
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
selectedDefaults = result;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
function parseSubmoduleList(submoduleList, available) {
|
|
1262
|
+
const requested = submoduleList.split(",").map((value) => value.trim()).filter(Boolean);
|
|
1263
|
+
if (requested.length === 0) {
|
|
1264
|
+
throw new Error("No submodule specified.");
|
|
1265
|
+
}
|
|
1266
|
+
const byPathOrName = /* @__PURE__ */ new Map();
|
|
1267
|
+
for (const submodule of available) {
|
|
1268
|
+
byPathOrName.set(submodule.path, submodule);
|
|
1269
|
+
byPathOrName.set(submodule.name, submodule);
|
|
1270
|
+
}
|
|
1271
|
+
const selected = [];
|
|
1272
|
+
const invalid = [];
|
|
1273
|
+
for (const value of requested) {
|
|
1274
|
+
const submodule = byPathOrName.get(value);
|
|
1275
|
+
if (!submodule) {
|
|
1276
|
+
invalid.push(value);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (!selected.some((item) => item.path === submodule.path)) {
|
|
1280
|
+
selected.push(submodule);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (invalid.length > 0) {
|
|
1284
|
+
throw new Error(`Unknown or unavailable submodule: ${invalid.join(", ")}`);
|
|
1285
|
+
}
|
|
1286
|
+
return selected.sort((a, b) => a.path.localeCompare(b.path));
|
|
1287
|
+
}
|
|
1288
|
+
async function resolveSubmodules(opts, available, defaultSelection = available) {
|
|
1289
|
+
if (opts.submodules === false) {
|
|
1290
|
+
return [];
|
|
1291
|
+
}
|
|
1292
|
+
if (typeof opts.submodules === "string") {
|
|
1293
|
+
return parseSubmoduleList(opts.submodules, available);
|
|
1294
|
+
}
|
|
1295
|
+
if (available.length === 0) {
|
|
1296
|
+
return [];
|
|
1297
|
+
}
|
|
1298
|
+
if (opts.yes) {
|
|
1299
|
+
return defaultSelection;
|
|
1300
|
+
}
|
|
1301
|
+
const result = await multiselect({
|
|
1302
|
+
message: "Select checked-out submodules to initialize (Space to toggle, Enter to confirm)",
|
|
1303
|
+
options: available.map((submodule) => ({
|
|
1304
|
+
label: `${submodule.path} (${submodule.name})`,
|
|
1305
|
+
value: submodule.path
|
|
1306
|
+
})),
|
|
1307
|
+
initialValues: defaultSelection.map((submodule) => submodule.path),
|
|
1308
|
+
required: false
|
|
1309
|
+
});
|
|
1310
|
+
if (typeof result === "symbol") {
|
|
1311
|
+
cancel("Submodule selection cancelled.");
|
|
1312
|
+
process.exit(1);
|
|
1313
|
+
}
|
|
1314
|
+
const selected = new Set(result);
|
|
1315
|
+
return available.filter((submodule) => selected.has(submodule.path));
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// src/commands/supermodule-targets.ts
|
|
1319
|
+
import path15 from "path";
|
|
1320
|
+
import { cancel as cancel2, multiselect as multiselect2 } from "@clack/prompts";
|
|
1321
|
+
|
|
1322
|
+
// src/utils/gitmodules.ts
|
|
1323
|
+
import { lstat, realpath } from "fs/promises";
|
|
1324
|
+
import path14 from "path";
|
|
1325
|
+
async function parseGitmodules(rootDir) {
|
|
1326
|
+
const content = await readTextIfExists(path14.join(rootDir, ".gitmodules"));
|
|
1327
|
+
if (content === null) {
|
|
1328
|
+
return [];
|
|
1329
|
+
}
|
|
1330
|
+
const entries = [];
|
|
1331
|
+
let current = null;
|
|
1332
|
+
const flushCurrent = () => {
|
|
1333
|
+
if (current?.name && current.path) {
|
|
1334
|
+
entries.push({
|
|
1335
|
+
name: current.name,
|
|
1336
|
+
path: current.path,
|
|
1337
|
+
url: current.url ?? ""
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
1342
|
+
const line = stripGitConfigInlineComment(rawLine).trim();
|
|
1343
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) {
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
const section = line.match(/^\[submodule\s+"(.+)"\]$/i);
|
|
1347
|
+
if (section) {
|
|
1348
|
+
flushCurrent();
|
|
1349
|
+
current = { name: section[1] };
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (!current) {
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
const keyValue = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.*)$/);
|
|
1356
|
+
if (!keyValue) {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
const key = keyValue[1].toLowerCase();
|
|
1360
|
+
const value = unquote(keyValue[2].trim());
|
|
1361
|
+
if (key === "path") {
|
|
1362
|
+
current.path = normalizeSubmodulePath(value);
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
if (key === "url") {
|
|
1366
|
+
current.url = value;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
flushCurrent();
|
|
1370
|
+
return entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
1371
|
+
}
|
|
1372
|
+
async function listInstallableSubmodules(rootDir) {
|
|
1373
|
+
const entries = await parseGitmodules(rootDir);
|
|
1374
|
+
const installable = [];
|
|
1375
|
+
for (const entry of entries) {
|
|
1376
|
+
if (await isSubmoduleWorktree(rootDir, entry.path)) {
|
|
1377
|
+
installable.push(entry);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return installable;
|
|
1381
|
+
}
|
|
1382
|
+
function normalizeSubmodulePath(value) {
|
|
1383
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "").trim();
|
|
1384
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
1385
|
+
if (normalized === "" || path14.posix.isAbsolute(normalized) || path14.win32.isAbsolute(value) || parts.some((part) => part === "..")) {
|
|
1386
|
+
throw new Error(`Unsafe submodule path in .gitmodules: ${value}`);
|
|
1387
|
+
}
|
|
1388
|
+
return parts.join("/");
|
|
1389
|
+
}
|
|
1390
|
+
function unquote(value) {
|
|
1391
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1392
|
+
return value.slice(1, -1);
|
|
1393
|
+
}
|
|
1394
|
+
return value;
|
|
1395
|
+
}
|
|
1396
|
+
function stripGitConfigInlineComment(value) {
|
|
1397
|
+
let inQuotes = false;
|
|
1398
|
+
let escaped = false;
|
|
1399
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1400
|
+
const char = value[index];
|
|
1401
|
+
if (escaped) {
|
|
1402
|
+
escaped = false;
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
if (inQuotes && char === "\\") {
|
|
1406
|
+
escaped = true;
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (char === '"') {
|
|
1410
|
+
inQuotes = !inQuotes;
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
if (!inQuotes && (char === "#" || char === ";") && isCommentStart(value, index)) {
|
|
1414
|
+
return value.slice(0, index);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return value;
|
|
1418
|
+
}
|
|
1419
|
+
function isCommentStart(value, index) {
|
|
1420
|
+
return index === 0 || /\s/.test(value[index - 1]);
|
|
1421
|
+
}
|
|
1422
|
+
async function isSubmoduleWorktree(rootDir, submodulePath) {
|
|
1423
|
+
const dir = path14.join(rootDir, submodulePath);
|
|
1424
|
+
try {
|
|
1425
|
+
if (!await pathHasNoSymlinkSegments(rootDir, submodulePath)) {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
const rootRealPath = await realpath(rootDir);
|
|
1429
|
+
const dirRealPath = await realpath(dir);
|
|
1430
|
+
if (!isInsideDirectory(rootRealPath, dirRealPath)) {
|
|
1431
|
+
return false;
|
|
1432
|
+
}
|
|
1433
|
+
const dirStat = await lstat(dir);
|
|
1434
|
+
if (!dirStat.isDirectory()) {
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
const gitMarkerStat = await lstat(path14.join(dir, ".git"));
|
|
1438
|
+
return !gitMarkerStat.isSymbolicLink() && (gitMarkerStat.isFile() || gitMarkerStat.isDirectory());
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
if (error.code === "ENOENT") {
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
throw error;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
|
|
1447
|
+
let current = rootDir;
|
|
1448
|
+
for (const part of submodulePath.split("/")) {
|
|
1449
|
+
current = path14.join(current, part);
|
|
1450
|
+
const partStat = await lstat(current);
|
|
1451
|
+
if (partStat.isSymbolicLink()) {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return true;
|
|
1456
|
+
}
|
|
1457
|
+
function isInsideDirectory(parent, child) {
|
|
1458
|
+
const relative = path14.relative(parent, child);
|
|
1459
|
+
return Boolean(relative) && !relative.startsWith("..") && !path14.isAbsolute(relative);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// src/commands/supermodule-targets.ts
|
|
1463
|
+
function rejectSubmoduleListWithoutGitmodules(opts) {
|
|
1464
|
+
if (typeof opts.submodules === "string") {
|
|
1465
|
+
throw new Error("--submodules can only be used in a repository with .gitmodules.");
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
async function resolveAddAgentTargets(cwd, opts) {
|
|
1469
|
+
const installedSubmodules = await listInstalledSubmodules(cwd);
|
|
1470
|
+
if ((await parseGitmodules(cwd)).length === 0) {
|
|
1471
|
+
rejectSubmoduleListWithoutGitmodules(opts);
|
|
1472
|
+
return [standaloneTarget(cwd)];
|
|
1473
|
+
}
|
|
1474
|
+
const selected = await resolveSubmodules(opts, installedSubmodules);
|
|
1475
|
+
const parentManagedSubmodulePaths = await listParentManagedSubmodulePaths(cwd);
|
|
1476
|
+
const parentSubmodulePaths = [
|
|
1477
|
+
.../* @__PURE__ */ new Set([...parentManagedSubmodulePaths, ...selected.map((entry) => entry.path)])
|
|
1478
|
+
].sort();
|
|
1479
|
+
return [
|
|
1480
|
+
parentTargetFromPaths(cwd, parentSubmodulePaths),
|
|
1481
|
+
...selected.map((entry) => childTarget(cwd, entry))
|
|
1482
|
+
];
|
|
1483
|
+
}
|
|
1484
|
+
async function resolveUpgradeTargets(cwd) {
|
|
1485
|
+
const installedSubmodules = await listInstalledSubmodules(cwd);
|
|
1486
|
+
if ((await parseGitmodules(cwd)).length === 0) {
|
|
1487
|
+
return [standaloneTarget(cwd)];
|
|
1488
|
+
}
|
|
1489
|
+
return [
|
|
1490
|
+
parentTarget(cwd, installedSubmodules),
|
|
1491
|
+
...installedSubmodules.map((entry) => childTarget(cwd, entry))
|
|
1492
|
+
];
|
|
1493
|
+
}
|
|
1494
|
+
async function resolveInitSubmoduleSelection(cwd, opts) {
|
|
1495
|
+
const installable = await listInstallableSubmodules(cwd);
|
|
1496
|
+
const installed = await listInstalledSubmodules(cwd);
|
|
1497
|
+
const parentManagedSubmodulePaths = await listParentManagedSubmodulePaths(cwd);
|
|
1498
|
+
const defaultSelection = installable;
|
|
1499
|
+
const selected = await resolveSubmodules(opts, installable, defaultSelection);
|
|
1500
|
+
const parentSubmodulePaths = [
|
|
1501
|
+
.../* @__PURE__ */ new Set([...parentManagedSubmodulePaths, ...selected.map((entry) => entry.path)])
|
|
1502
|
+
].sort();
|
|
1503
|
+
return { installable, installed, selected, parentSubmodulePaths };
|
|
1504
|
+
}
|
|
1505
|
+
async function resolveClearTargets(cwd, opts) {
|
|
1506
|
+
if ((await parseGitmodules(cwd)).length === 0) {
|
|
1507
|
+
rejectSubmoduleListWithoutGitmodules(opts);
|
|
1508
|
+
return [standaloneTarget(cwd)];
|
|
1509
|
+
}
|
|
1510
|
+
const installedSubmodules = await listInstalledSubmodules(cwd);
|
|
1511
|
+
const parent = await pathExists(path15.join(cwd, EASY_CODING_DIR)) ? parentTarget(cwd, installedSubmodules) : null;
|
|
1512
|
+
const children = installedSubmodules.map((entry) => childTarget(cwd, entry));
|
|
1513
|
+
if (opts.submodules === false) {
|
|
1514
|
+
return parent ? [parent] : [];
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof opts.submodules === "string") {
|
|
1517
|
+
return [
|
|
1518
|
+
...parent ? [parent] : [],
|
|
1519
|
+
...parseSubmoduleSelection(opts.submodules, installedSubmodules).map(
|
|
1520
|
+
(entry) => childTarget(cwd, entry)
|
|
1521
|
+
)
|
|
1522
|
+
];
|
|
1523
|
+
}
|
|
1524
|
+
if (opts.yes) {
|
|
1525
|
+
return parent ? [parent] : [];
|
|
1526
|
+
}
|
|
1527
|
+
const candidates = [...parent ? [parent] : [], ...children];
|
|
1528
|
+
if (candidates.length === 0) {
|
|
1529
|
+
return [];
|
|
1530
|
+
}
|
|
1531
|
+
const result = await multiselect2({
|
|
1532
|
+
message: "Select Easy Coding harness targets to clear",
|
|
1533
|
+
options: candidates.map((target) => ({
|
|
1534
|
+
label: target.label === "." ? "parent repository" : `submodule: ${target.label}`,
|
|
1535
|
+
value: target.label
|
|
1536
|
+
})),
|
|
1537
|
+
initialValues: parent ? [parent.label] : [],
|
|
1538
|
+
required: true
|
|
1539
|
+
});
|
|
1540
|
+
if (typeof result === "symbol") {
|
|
1541
|
+
cancel2("Clear target selection cancelled.");
|
|
1542
|
+
process.exit(1);
|
|
1543
|
+
}
|
|
1544
|
+
const selected = new Set(result);
|
|
1545
|
+
return candidates.filter((target) => selected.has(target.label));
|
|
1546
|
+
}
|
|
1547
|
+
async function listInstalledSubmodules(cwd) {
|
|
1548
|
+
const entries = await listInstallableSubmodules(cwd);
|
|
1549
|
+
const installed = [];
|
|
1550
|
+
for (const entry of entries) {
|
|
1551
|
+
if (await pathExists(configPath(path15.join(cwd, entry.path)))) {
|
|
1552
|
+
installed.push(entry);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return installed;
|
|
1556
|
+
}
|
|
1557
|
+
async function listParentManagedSubmodulePaths(cwd) {
|
|
1558
|
+
const parentConfigPath = configPath(cwd);
|
|
1559
|
+
if (!await pathExists(parentConfigPath)) {
|
|
1560
|
+
return [];
|
|
1561
|
+
}
|
|
1562
|
+
try {
|
|
1563
|
+
const config = await readConfigYaml(parentConfigPath);
|
|
1564
|
+
if (config.supermodule?.role !== "super-parent" || !Array.isArray(config.supermodule.submodules)) {
|
|
1565
|
+
return [];
|
|
1566
|
+
}
|
|
1567
|
+
return config.supermodule.submodules.filter((submodulePath) => typeof submodulePath === "string").sort();
|
|
1568
|
+
} catch {
|
|
1569
|
+
return [];
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
function standaloneTarget(cwd) {
|
|
1573
|
+
return {
|
|
1574
|
+
dir: cwd,
|
|
1575
|
+
label: ".",
|
|
1576
|
+
configPath: configPath(cwd),
|
|
1577
|
+
supermodule: { role: "standalone" }
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function parentTarget(cwd, installedSubmodules) {
|
|
1581
|
+
const submodulePaths = installedSubmodules.map((entry) => entry.path);
|
|
1582
|
+
return parentTargetFromPaths(cwd, submodulePaths);
|
|
1583
|
+
}
|
|
1584
|
+
function parentTargetFromPaths(cwd, submodulePaths) {
|
|
1585
|
+
return {
|
|
1586
|
+
dir: cwd,
|
|
1587
|
+
label: ".",
|
|
1588
|
+
configPath: configPath(cwd),
|
|
1589
|
+
supermodule: { role: "super-parent", submodules: submodulePaths },
|
|
1590
|
+
boundary: { submodulePaths }
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
function childTarget(cwd, entry) {
|
|
1594
|
+
const dir = path15.join(cwd, entry.path);
|
|
1595
|
+
return {
|
|
1596
|
+
dir,
|
|
1597
|
+
label: entry.path,
|
|
1598
|
+
configPath: configPath(dir),
|
|
1599
|
+
supermodule: { role: "submodule-child", parent: toPosixRelative(dir, cwd) }
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
function parseSubmoduleSelection(submoduleList, available) {
|
|
1603
|
+
const requested = submoduleList.split(",").map((value) => value.trim()).filter(Boolean);
|
|
1604
|
+
if (requested.length === 0) {
|
|
1605
|
+
throw new Error("No submodule specified.");
|
|
1606
|
+
}
|
|
1607
|
+
const byPathOrName = /* @__PURE__ */ new Map();
|
|
1608
|
+
for (const submodule of available) {
|
|
1609
|
+
byPathOrName.set(submodule.path, submodule);
|
|
1610
|
+
byPathOrName.set(submodule.name, submodule);
|
|
1611
|
+
}
|
|
1612
|
+
const selected = [];
|
|
1613
|
+
const invalid = [];
|
|
1614
|
+
for (const value of requested) {
|
|
1615
|
+
const submodule = byPathOrName.get(value);
|
|
1616
|
+
if (!submodule) {
|
|
1617
|
+
invalid.push(value);
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
if (!selected.some((item) => item.path === submodule.path)) {
|
|
1621
|
+
selected.push(submodule);
|
|
1048
1622
|
}
|
|
1049
|
-
selectedDefaults = result;
|
|
1050
1623
|
}
|
|
1624
|
+
if (invalid.length > 0) {
|
|
1625
|
+
throw new Error(`Unknown or unavailable initialized submodule: ${invalid.join(", ")}`);
|
|
1626
|
+
}
|
|
1627
|
+
return selected.sort((a, b) => a.path.localeCompare(b.path));
|
|
1628
|
+
}
|
|
1629
|
+
function configPath(cwd) {
|
|
1630
|
+
return path15.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
1631
|
+
}
|
|
1632
|
+
function toPosixRelative(from, to) {
|
|
1633
|
+
const relative = path15.relative(from, to);
|
|
1634
|
+
return relative ? relative.split(path15.sep).join("/") : ".";
|
|
1051
1635
|
}
|
|
1052
1636
|
|
|
1053
1637
|
// src/commands/add-agent.ts
|
|
1054
1638
|
async function addAgent(opts) {
|
|
1055
1639
|
renderBanner();
|
|
1056
1640
|
const cwd = process.cwd();
|
|
1057
|
-
const
|
|
1058
|
-
if (!await pathExists(configPath)) {
|
|
1641
|
+
const targets = await resolveAddAgentTargets(cwd, opts);
|
|
1642
|
+
if (!await pathExists(targets[0].configPath)) {
|
|
1059
1643
|
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
1060
1644
|
}
|
|
1061
|
-
const config = await readConfigYaml(configPath);
|
|
1062
1645
|
const platforms = await resolvePlatforms(opts, ["claude-code"]);
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1646
|
+
const installedLabels = [];
|
|
1647
|
+
const refreshedLabels = [];
|
|
1648
|
+
for (const target of targets) {
|
|
1649
|
+
if (!await pathExists(target.configPath)) {
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
const config = await readConfigYaml(target.configPath);
|
|
1653
|
+
const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
|
|
1654
|
+
const agents = [...config.agents, ...toInstall];
|
|
1655
|
+
if (toInstall.length > 0) {
|
|
1656
|
+
const artifacts = await configurePlatformsForDir(
|
|
1657
|
+
target.dir,
|
|
1658
|
+
toInstall,
|
|
1659
|
+
target.boundary
|
|
1660
|
+
);
|
|
1661
|
+
await writeRuntimeScaffold(target.dir, agents, {
|
|
1662
|
+
supermodule: target.supermodule
|
|
1663
|
+
});
|
|
1664
|
+
await writeInstallManifest(target.dir, {
|
|
1665
|
+
harnessVersion: VERSION,
|
|
1666
|
+
agents: toInstall,
|
|
1667
|
+
artifacts,
|
|
1668
|
+
mode: "merge"
|
|
1669
|
+
});
|
|
1670
|
+
await ensureEasyCodingSessionsIgnored(target.dir);
|
|
1671
|
+
await addAgentsToConfig(target.configPath, toInstall);
|
|
1672
|
+
await setPendingInitSince(target.dir, VERSION);
|
|
1673
|
+
installedLabels.push(`${target.label}: ${toInstall.join(", ")}`);
|
|
1674
|
+
}
|
|
1675
|
+
if (target.supermodule.role === "super-parent") {
|
|
1676
|
+
await refreshSupermoduleParent(target.dir, agents, target.supermodule.submodules ?? []);
|
|
1677
|
+
refreshedLabels.push(target.label);
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
if (target.supermodule.role === "submodule-child") {
|
|
1681
|
+
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
1682
|
+
refreshedLabels.push(target.label);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (installedLabels.length === 0) {
|
|
1686
|
+
if (refreshedLabels.length > 0) {
|
|
1687
|
+
outro(chalk2.green(`Supermodule topology refreshed:
|
|
1688
|
+
${refreshedLabels.join("\n")}`));
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1065
1691
|
outro(chalk2.yellow("All selected agent platforms are already installed."));
|
|
1066
1692
|
return;
|
|
1067
1693
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
artifacts.push(...await CONFIGURATORS[platform](cwd));
|
|
1071
|
-
}
|
|
1072
|
-
await writeRuntimeScaffold(cwd, [...config.agents, ...toInstall]);
|
|
1073
|
-
await writeInstallManifest(cwd, {
|
|
1074
|
-
harnessVersion: VERSION,
|
|
1075
|
-
agents: toInstall,
|
|
1076
|
-
artifacts,
|
|
1077
|
-
mode: "merge"
|
|
1078
|
-
});
|
|
1079
|
-
await ensureEasyCodingSessionsIgnored(cwd);
|
|
1080
|
-
await addAgentsToConfig(configPath, toInstall);
|
|
1081
|
-
await setPendingInitSince(cwd, VERSION);
|
|
1082
|
-
outro(chalk2.green(`Added agent platforms: ${toInstall.join(", ")}`));
|
|
1694
|
+
outro(chalk2.green(`Added agent platforms:
|
|
1695
|
+
${installedLabels.join("\n")}`));
|
|
1083
1696
|
}
|
|
1084
1697
|
|
|
1085
1698
|
// src/commands/clear.ts
|
|
1086
1699
|
import { readdir as readdir4, rm, writeFile } from "fs/promises";
|
|
1087
|
-
import
|
|
1088
|
-
import { cancel as
|
|
1700
|
+
import path16 from "path";
|
|
1701
|
+
import { cancel as cancel3, confirm as confirm2, outro as outro2 } from "@clack/prompts";
|
|
1089
1702
|
import chalk3 from "chalk";
|
|
1090
1703
|
var PLATFORM_TEMPLATE_DIR = {
|
|
1091
1704
|
"claude-code": "claude",
|
|
@@ -1095,13 +1708,12 @@ var PLATFORM_TEMPLATE_DIR = {
|
|
|
1095
1708
|
async function clear(opts) {
|
|
1096
1709
|
renderBanner();
|
|
1097
1710
|
const cwd = process.cwd();
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1711
|
+
const targets = await resolveClearTargets(cwd, opts);
|
|
1712
|
+
const targetPlans = await buildTargetClearPlans(targets);
|
|
1713
|
+
if (targetPlans.length === 0) {
|
|
1100
1714
|
throw new Error("No .easy-coding directory found in this project \u2014 nothing to clear.");
|
|
1101
1715
|
}
|
|
1102
|
-
|
|
1103
|
-
const plan = await buildClearPlan(cwd, agents);
|
|
1104
|
-
console.log(renderPlan(cwd, agents, plan));
|
|
1716
|
+
console.log(renderTargetPlans(targetPlans));
|
|
1105
1717
|
if (opts.dryRun) {
|
|
1106
1718
|
return;
|
|
1107
1719
|
}
|
|
@@ -1111,18 +1723,54 @@ async function clear(opts) {
|
|
|
1111
1723
|
initialValue: false
|
|
1112
1724
|
});
|
|
1113
1725
|
if (typeof ok === "symbol" || !ok) {
|
|
1114
|
-
|
|
1726
|
+
cancel3("Clear cancelled.");
|
|
1115
1727
|
return;
|
|
1116
1728
|
}
|
|
1117
1729
|
}
|
|
1118
|
-
|
|
1730
|
+
for (const targetPlan of targetPlans) {
|
|
1731
|
+
await executeClearPlan(targetPlan.plan);
|
|
1732
|
+
}
|
|
1733
|
+
await refreshParentAfterChildClear(cwd, targetPlans);
|
|
1119
1734
|
outro2(chalk3.green("Easy Coding harness removed. Run easy-coding init to reinstall."));
|
|
1120
1735
|
}
|
|
1736
|
+
async function buildTargetClearPlans(targets) {
|
|
1737
|
+
const plans = [];
|
|
1738
|
+
for (const target of targets) {
|
|
1739
|
+
const easyCodingDir = path16.join(target.dir, EASY_CODING_DIR);
|
|
1740
|
+
if (!await pathExists(easyCodingDir)) {
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
const agents = await resolveInstalledAgents(target.dir);
|
|
1744
|
+
plans.push({
|
|
1745
|
+
target,
|
|
1746
|
+
agents,
|
|
1747
|
+
plan: await buildClearPlan(target.dir, agents)
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
return plans;
|
|
1751
|
+
}
|
|
1752
|
+
async function refreshParentAfterChildClear(cwd, targetPlans) {
|
|
1753
|
+
if (targetPlans.some((targetPlan) => targetPlan.target.label === ".")) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (!await pathExists(path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const [parent] = await resolveUpgradeTargets(cwd);
|
|
1760
|
+
if (!parent || parent.label !== ".") {
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
const config = await readConfigYaml(parent.configPath);
|
|
1764
|
+
if (!Array.isArray(config.agents) || config.agents.length === 0) {
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
await refreshSupermoduleParent(cwd, config.agents, parent.supermodule.submodules ?? []);
|
|
1768
|
+
}
|
|
1121
1769
|
async function resolveInstalledAgents(cwd) {
|
|
1122
|
-
const
|
|
1123
|
-
if (await pathExists(
|
|
1770
|
+
const configPath2 = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
1771
|
+
if (await pathExists(configPath2)) {
|
|
1124
1772
|
try {
|
|
1125
|
-
const config = await readConfigYaml(
|
|
1773
|
+
const config = await readConfigYaml(configPath2);
|
|
1126
1774
|
const listed = Array.isArray(config.agents) ? config.agents.filter(isAgentPlatform) : [];
|
|
1127
1775
|
if (listed.length > 0) {
|
|
1128
1776
|
return listed;
|
|
@@ -1150,7 +1798,7 @@ async function hasPlatformInstall(cwd, platform) {
|
|
|
1150
1798
|
async function hasInstallMarkers(cwd, meta) {
|
|
1151
1799
|
const markers = [meta.skillsDir, meta.hooksDir, meta.agentsDir, meta.hookConfigFile];
|
|
1152
1800
|
for (const marker of markers) {
|
|
1153
|
-
if (await pathExists(
|
|
1801
|
+
if (await pathExists(path16.join(cwd, marker))) {
|
|
1154
1802
|
return true;
|
|
1155
1803
|
}
|
|
1156
1804
|
}
|
|
@@ -1206,7 +1854,7 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
|
|
|
1206
1854
|
}
|
|
1207
1855
|
if (await manifestFileMatches(filePath, file.sha256)) {
|
|
1208
1856
|
plan.removeFiles.push({ filePath, expectedSha256: file.sha256 });
|
|
1209
|
-
addEmptyDirChain(plan.emptyDirs,
|
|
1857
|
+
addEmptyDirChain(plan.emptyDirs, path16.dirname(filePath), cwd);
|
|
1210
1858
|
} else {
|
|
1211
1859
|
plan.skippedModified.push(filePath);
|
|
1212
1860
|
}
|
|
@@ -1215,11 +1863,12 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
|
|
|
1215
1863
|
if (!agentSet.has(registration.platform)) {
|
|
1216
1864
|
continue;
|
|
1217
1865
|
}
|
|
1866
|
+
const managedHookPaths = registration.hook_path ? managedHookPathsForRegistration(cwd, registration.hook_path) : [];
|
|
1218
1867
|
addHookConfigPrune(
|
|
1219
1868
|
plan.pruneHookConfigs,
|
|
1220
1869
|
plan.pruneHookCommands,
|
|
1221
1870
|
manifestPath(cwd, registration.config_path),
|
|
1222
|
-
|
|
1871
|
+
managedHookPaths,
|
|
1223
1872
|
[registration.command]
|
|
1224
1873
|
);
|
|
1225
1874
|
}
|
|
@@ -1255,28 +1904,28 @@ async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills
|
|
|
1255
1904
|
);
|
|
1256
1905
|
for (const meta of metas) {
|
|
1257
1906
|
for (const name of managedSkills) {
|
|
1258
|
-
plan.remove.add(
|
|
1907
|
+
plan.remove.add(path16.join(cwd, meta.skillsDir, name));
|
|
1259
1908
|
}
|
|
1260
1909
|
for (const name of hookFileNames) {
|
|
1261
|
-
plan.remove.add(
|
|
1910
|
+
plan.remove.add(path16.join(cwd, meta.hooksDir, name));
|
|
1262
1911
|
}
|
|
1263
1912
|
for (const name of agentFileNames) {
|
|
1264
|
-
plan.remove.add(
|
|
1913
|
+
plan.remove.add(path16.join(cwd, meta.agentsDir, name));
|
|
1265
1914
|
}
|
|
1266
1915
|
addHookConfigPrune(
|
|
1267
1916
|
plan.pruneHookConfigs,
|
|
1268
1917
|
plan.pruneHookCommands,
|
|
1269
|
-
|
|
1270
|
-
|
|
1918
|
+
path16.join(cwd, meta.hookConfigFile),
|
|
1919
|
+
managedHookPathsForTemplate(cwd, meta, hookFileNames),
|
|
1271
1920
|
[]
|
|
1272
1921
|
);
|
|
1273
|
-
plan.constraints.add(
|
|
1922
|
+
plan.constraints.add(path16.join(cwd, meta.mainConstraint));
|
|
1274
1923
|
if (platform === "codex") {
|
|
1275
|
-
plan.remove.add(
|
|
1924
|
+
plan.remove.add(path16.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
|
|
1276
1925
|
}
|
|
1277
|
-
plan.emptyDirs.add(
|
|
1278
|
-
plan.emptyDirs.add(
|
|
1279
|
-
plan.emptyDirs.add(
|
|
1926
|
+
plan.emptyDirs.add(path16.join(cwd, meta.skillsDir));
|
|
1927
|
+
plan.emptyDirs.add(path16.join(cwd, meta.hooksDir));
|
|
1928
|
+
plan.emptyDirs.add(path16.join(cwd, meta.agentsDir));
|
|
1280
1929
|
}
|
|
1281
1930
|
}
|
|
1282
1931
|
async function resolveManifestUncoveredQoderMetas(cwd, manifest) {
|
|
@@ -1318,6 +1967,51 @@ function addHookConfigPrune(pruneHookConfigs, pruneHookCommands, filePath, manag
|
|
|
1318
1967
|
}
|
|
1319
1968
|
pruneHookCommands.set(filePath, existingCommands);
|
|
1320
1969
|
}
|
|
1970
|
+
function managedHookPathsForRegistration(cwd, hookPath) {
|
|
1971
|
+
const paths = [hookPath];
|
|
1972
|
+
try {
|
|
1973
|
+
paths.push(...managedHookPathTokens(manifestPath(cwd, hookPath)));
|
|
1974
|
+
} catch {
|
|
1975
|
+
if (path16.isAbsolute(hookPath)) {
|
|
1976
|
+
paths.push(...managedHookPathTokens(hookPath));
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return paths;
|
|
1980
|
+
}
|
|
1981
|
+
function managedHookPathsForTemplate(cwd, meta, hookFileNames) {
|
|
1982
|
+
const relativePaths = hookFileNames.map(
|
|
1983
|
+
(name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`
|
|
1984
|
+
);
|
|
1985
|
+
const absolutePaths = hookFileNames.flatMap(
|
|
1986
|
+
(name) => managedHookPathTokens(path16.join(cwd, meta.hooksDir, name))
|
|
1987
|
+
);
|
|
1988
|
+
return [...relativePaths, ...absolutePaths];
|
|
1989
|
+
}
|
|
1990
|
+
function managedHookPathTokens(filePath) {
|
|
1991
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
1992
|
+
for (const equivalentPath of equivalentAbsolutePaths(filePath)) {
|
|
1993
|
+
const normalized = equivalentPath.replace(/\\/g, "/");
|
|
1994
|
+
const dir = path16.posix.dirname(normalized);
|
|
1995
|
+
const basename = path16.posix.basename(normalized);
|
|
1996
|
+
tokens.add(normalized);
|
|
1997
|
+
tokens.add(shellDoubleQuoteArg2(normalized));
|
|
1998
|
+
tokens.add(`${shellDoubleQuoteArg2(dir)}/${basename}`);
|
|
1999
|
+
}
|
|
2000
|
+
return [...tokens];
|
|
2001
|
+
}
|
|
2002
|
+
function equivalentAbsolutePaths(filePath) {
|
|
2003
|
+
const normalized = path16.resolve(filePath).replace(/\\/g, "/");
|
|
2004
|
+
const equivalents = [normalized];
|
|
2005
|
+
if (normalized.startsWith("/private/var/")) {
|
|
2006
|
+
equivalents.push(normalized.replace(/^\/private\/var\//, "/var/"));
|
|
2007
|
+
} else if (normalized.startsWith("/var/")) {
|
|
2008
|
+
equivalents.push(normalized.replace(/^\/var\//, "/private/var/"));
|
|
2009
|
+
}
|
|
2010
|
+
return equivalents;
|
|
2011
|
+
}
|
|
2012
|
+
function shellDoubleQuoteArg2(value) {
|
|
2013
|
+
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
2014
|
+
}
|
|
1321
2015
|
function createClearPlan() {
|
|
1322
2016
|
return {
|
|
1323
2017
|
remove: /* @__PURE__ */ new Set(),
|
|
@@ -1380,27 +2074,27 @@ function addRuntimeClearEntries(plan, cwd) {
|
|
|
1380
2074
|
plan.remove = [
|
|
1381
2075
|
.../* @__PURE__ */ new Set([
|
|
1382
2076
|
...plan.remove,
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
2077
|
+
path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
|
|
2078
|
+
path16.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
|
|
2079
|
+
path16.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
|
|
2080
|
+
path16.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
|
|
1387
2081
|
])
|
|
1388
2082
|
];
|
|
1389
2083
|
}
|
|
1390
2084
|
function addEmptyDirChain(emptyDirs, startDir, cwd) {
|
|
1391
2085
|
let current = startDir;
|
|
1392
|
-
while (current !== cwd &&
|
|
2086
|
+
while (current !== cwd && isInsideDirectory2(cwd, current)) {
|
|
1393
2087
|
emptyDirs.add(current);
|
|
1394
|
-
const parent =
|
|
2088
|
+
const parent = path16.dirname(current);
|
|
1395
2089
|
if (parent === current) {
|
|
1396
2090
|
break;
|
|
1397
2091
|
}
|
|
1398
2092
|
current = parent;
|
|
1399
2093
|
}
|
|
1400
2094
|
}
|
|
1401
|
-
function
|
|
1402
|
-
const relative =
|
|
1403
|
-
return Boolean(relative) && !relative.startsWith("..") && !
|
|
2095
|
+
function isInsideDirectory2(parent, child) {
|
|
2096
|
+
const relative = path16.relative(parent, child);
|
|
2097
|
+
return Boolean(relative) && !relative.startsWith("..") && !path16.isAbsolute(relative);
|
|
1404
2098
|
}
|
|
1405
2099
|
async function listSharedHookNamesForPlatform(platform) {
|
|
1406
2100
|
const names = await listFileNames(getTemplatePath("shared-hooks"));
|
|
@@ -1502,7 +2196,7 @@ function isManagedHook(hook, managedHookPaths, managedHookCommands) {
|
|
|
1502
2196
|
if (normalizedManagedCommands.includes(normalizeCommand(command))) {
|
|
1503
2197
|
return true;
|
|
1504
2198
|
}
|
|
1505
|
-
const normalizedCommand = command
|
|
2199
|
+
const normalizedCommand = command;
|
|
1506
2200
|
return managedHookPaths.some(
|
|
1507
2201
|
(hookPath) => commandContainsPathToken(normalizedCommand, hookPath.replace(/\\/g, "/"))
|
|
1508
2202
|
);
|
|
@@ -1537,7 +2231,7 @@ function sortDirsDeepestFirst(dirs) {
|
|
|
1537
2231
|
});
|
|
1538
2232
|
}
|
|
1539
2233
|
function pathDepth(dir) {
|
|
1540
|
-
return
|
|
2234
|
+
return path16.normalize(dir).split(path16.sep).filter(Boolean).length;
|
|
1541
2235
|
}
|
|
1542
2236
|
async function listDirNames(dir) {
|
|
1543
2237
|
try {
|
|
@@ -1556,7 +2250,7 @@ async function listFileNames(dir) {
|
|
|
1556
2250
|
}
|
|
1557
2251
|
}
|
|
1558
2252
|
function renderPlan(cwd, agents, plan) {
|
|
1559
|
-
const rel = (target) =>
|
|
2253
|
+
const rel = (target) => path16.relative(cwd, target) || target;
|
|
1560
2254
|
const lines = [];
|
|
1561
2255
|
lines.push(chalk3.bold("easy-coding clear"));
|
|
1562
2256
|
lines.push(`Platforms: ${agents.length > 0 ? agents.join(", ") : "(none detected)"}`);
|
|
@@ -1594,23 +2288,33 @@ function renderPlan(cwd, agents, plan) {
|
|
|
1594
2288
|
);
|
|
1595
2289
|
return lines.join("\n");
|
|
1596
2290
|
}
|
|
2291
|
+
function renderTargetPlans(targetPlans) {
|
|
2292
|
+
if (targetPlans.length === 1) {
|
|
2293
|
+
const [{ target, agents, plan }] = targetPlans;
|
|
2294
|
+
return [`Target: ${target.label}`, renderPlan(target.dir, agents, plan)].join("\n");
|
|
2295
|
+
}
|
|
2296
|
+
return targetPlans.map(
|
|
2297
|
+
({ target, agents, plan }) => [`Target: ${target.label}`, renderPlan(target.dir, agents, plan)].join("\n")
|
|
2298
|
+
).join("\n\n");
|
|
2299
|
+
}
|
|
1597
2300
|
|
|
1598
2301
|
// src/commands/init.ts
|
|
2302
|
+
import path18 from "path";
|
|
1599
2303
|
import { note, outro as outro3 } from "@clack/prompts";
|
|
1600
2304
|
import chalk4 from "chalk";
|
|
1601
2305
|
|
|
1602
2306
|
// src/utils/install-state.ts
|
|
1603
2307
|
import { readdir as readdir5 } from "fs/promises";
|
|
1604
|
-
import
|
|
2308
|
+
import path17 from "path";
|
|
1605
2309
|
var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
|
|
1606
2310
|
async function detectEasyCodingInstallState(cwd) {
|
|
1607
|
-
const easyCodingDir =
|
|
2311
|
+
const easyCodingDir = path17.join(cwd, EASY_CODING_DIR);
|
|
1608
2312
|
if (!await pathExists(easyCodingDir)) {
|
|
1609
2313
|
return { kind: "fresh", easyCodingDir };
|
|
1610
2314
|
}
|
|
1611
|
-
const
|
|
1612
|
-
if (await pathExists(
|
|
1613
|
-
return { kind: "installed", easyCodingDir, configPath };
|
|
2315
|
+
const configPath2 = path17.join(easyCodingDir, CONFIG_FILE);
|
|
2316
|
+
if (await pathExists(configPath2)) {
|
|
2317
|
+
return { kind: "installed", easyCodingDir, configPath: configPath2 };
|
|
1614
2318
|
}
|
|
1615
2319
|
const legacyAssets = await detectLegacyAssets(easyCodingDir);
|
|
1616
2320
|
if (legacyAssets.length === 0) {
|
|
@@ -1626,17 +2330,17 @@ async function detectEasyCodingInstallState(cwd) {
|
|
|
1626
2330
|
async function detectLegacyAssets(easyCodingDir) {
|
|
1627
2331
|
const assets = [];
|
|
1628
2332
|
for (const file of LEGACY_ROOT_FILES) {
|
|
1629
|
-
if (await pathExists(
|
|
2333
|
+
if (await pathExists(path17.join(easyCodingDir, file))) {
|
|
1630
2334
|
assets.push(relativeEasyCodingPath(file));
|
|
1631
2335
|
}
|
|
1632
2336
|
}
|
|
1633
|
-
if (await pathExists(
|
|
2337
|
+
if (await pathExists(path17.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
|
|
1634
2338
|
assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
|
|
1635
2339
|
}
|
|
1636
|
-
const shortMemoryFiles = await listMarkdownFiles(
|
|
2340
|
+
const shortMemoryFiles = await listMarkdownFiles(path17.join(easyCodingDir, "memory", "short"));
|
|
1637
2341
|
assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
|
|
1638
2342
|
for (const dir of ["spec", "prototype"]) {
|
|
1639
|
-
if (await hasAnyDirectoryEntry(
|
|
2343
|
+
if (await hasAnyDirectoryEntry(path17.join(easyCodingDir, dir))) {
|
|
1640
2344
|
assets.push(relativeEasyCodingPath(dir));
|
|
1641
2345
|
}
|
|
1642
2346
|
}
|
|
@@ -1656,19 +2360,54 @@ async function hasAnyDirectoryEntry(dir) {
|
|
|
1656
2360
|
return (await readdir5(dir)).length > 0;
|
|
1657
2361
|
}
|
|
1658
2362
|
function relativeEasyCodingPath(...segments) {
|
|
1659
|
-
return
|
|
2363
|
+
return path17.posix.join(EASY_CODING_DIR, ...segments);
|
|
1660
2364
|
}
|
|
1661
2365
|
function relativeConfigPath() {
|
|
1662
|
-
return
|
|
2366
|
+
return path17.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
1663
2367
|
}
|
|
1664
2368
|
function relativeProjectInitTaskPath() {
|
|
1665
|
-
return
|
|
2369
|
+
return path17.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
|
|
1666
2370
|
}
|
|
1667
2371
|
|
|
1668
2372
|
// src/commands/init.ts
|
|
1669
2373
|
async function init(opts) {
|
|
1670
2374
|
renderBanner();
|
|
1671
2375
|
const cwd = process.cwd();
|
|
2376
|
+
const submodules = await parseGitmodules(cwd);
|
|
2377
|
+
if (submodules.length === 0) {
|
|
2378
|
+
rejectSubmoduleListWithoutGitmodules(opts);
|
|
2379
|
+
}
|
|
2380
|
+
const targets = submodules.length === 0 ? [await standaloneTarget2(cwd)] : await supermoduleTargets(cwd, opts, submodules);
|
|
2381
|
+
const installableTargets = targets.filter((target) => !target.installed);
|
|
2382
|
+
const parentTarget2 = targets.find((target) => target.label === ".");
|
|
2383
|
+
if (installableTargets.length === 0) {
|
|
2384
|
+
await refreshInstalledChildTopologies(targets);
|
|
2385
|
+
if (submodules.length > 0 && parentTarget2) {
|
|
2386
|
+
const platforms2 = await resolveInitPlatforms(cwd, opts, true);
|
|
2387
|
+
await refreshParentTopologyIfNeeded(cwd, parentTarget2, platforms2);
|
|
2388
|
+
}
|
|
2389
|
+
outro3(chalk4.yellow("All selected easy-coding harness targets are already installed."));
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
const platforms = await resolveInitPlatforms(cwd, opts, Boolean(parentTarget2?.installed));
|
|
2393
|
+
for (const target of installableTargets) {
|
|
2394
|
+
await installHarnessToDir(target.dir, platforms, target.context);
|
|
2395
|
+
}
|
|
2396
|
+
await refreshInstalledChildTopologies(targets);
|
|
2397
|
+
if (submodules.length > 0 && parentTarget2) {
|
|
2398
|
+
await refreshParentTopologyIfNeeded(cwd, parentTarget2, platforms);
|
|
2399
|
+
}
|
|
2400
|
+
const triggers = platforms.map(
|
|
2401
|
+
(platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
|
|
2402
|
+
).join("\n");
|
|
2403
|
+
note(triggers, "Next step");
|
|
2404
|
+
outro3(
|
|
2405
|
+
chalk4.green(
|
|
2406
|
+
`easy-coding harness installed in ${installableTargets.map((target) => target.label).join(", ")}. Open your agent and run ec-init.`
|
|
2407
|
+
)
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
async function standaloneTarget2(cwd) {
|
|
1672
2411
|
const installState = await detectEasyCodingInstallState(cwd);
|
|
1673
2412
|
if (installState.kind === "installed") {
|
|
1674
2413
|
throw new Error(
|
|
@@ -1680,36 +2419,121 @@ async function init(opts) {
|
|
|
1680
2419
|
".easy-coding exists but is not recognized as an easy-coding harness or legacy easy-coding skill project. Please inspect it manually before running init."
|
|
1681
2420
|
);
|
|
1682
2421
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2422
|
+
return {
|
|
2423
|
+
dir: cwd,
|
|
2424
|
+
label: ".",
|
|
2425
|
+
context: contextFromState("standalone", installState),
|
|
2426
|
+
installed: false
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
async function supermoduleTargets(cwd, opts, submodules) {
|
|
2430
|
+
const { installable, parentSubmodulePaths } = await resolveInitSubmoduleSelection(cwd, opts);
|
|
2431
|
+
const skipped = submodules.filter(
|
|
2432
|
+
(entry) => !installable.some((installableEntry) => installableEntry.path === entry.path)
|
|
2433
|
+
);
|
|
2434
|
+
if (skipped.length > 0) {
|
|
2435
|
+
note(
|
|
2436
|
+
skipped.map((entry) => `${entry.path} (${entry.name})`).join("\n"),
|
|
2437
|
+
"Skipped unchecked-out submodules"
|
|
2438
|
+
);
|
|
1687
2439
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
2440
|
+
const targets = [
|
|
2441
|
+
await targetFromState(cwd, ".", "super-parent", {
|
|
2442
|
+
submodulePaths: parentSubmodulePaths
|
|
2443
|
+
})
|
|
2444
|
+
];
|
|
2445
|
+
const targetSubmodulePaths = opts.submodules === false ? /* @__PURE__ */ new Set() : new Set(parentSubmodulePaths);
|
|
2446
|
+
for (const entry of installable) {
|
|
2447
|
+
if (!targetSubmodulePaths.has(entry.path)) {
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
const dir = path18.join(cwd, entry.path);
|
|
2451
|
+
targets.push(
|
|
2452
|
+
await targetFromState(dir, entry.path, "submodule-child", {
|
|
2453
|
+
parent: toPosixRelative2(dir, cwd)
|
|
2454
|
+
})
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
return targets;
|
|
2458
|
+
}
|
|
2459
|
+
async function targetFromState(targetDir, label, role, extraContext = {}) {
|
|
2460
|
+
const installState = await detectEasyCodingInstallState(targetDir);
|
|
2461
|
+
if (installState.kind === "unknown") {
|
|
2462
|
+
throw new Error(
|
|
2463
|
+
`${targetDir}/.easy-coding exists but is not recognized as an easy-coding harness or legacy easy-coding skill project. Please inspect it manually before running init.`
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
if (installState.kind === "installed") {
|
|
2467
|
+
return {
|
|
2468
|
+
dir: targetDir,
|
|
2469
|
+
label,
|
|
2470
|
+
installed: true,
|
|
2471
|
+
context: {
|
|
2472
|
+
role,
|
|
2473
|
+
initSource: "fresh",
|
|
2474
|
+
...extraContext
|
|
2475
|
+
}
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
return {
|
|
2479
|
+
dir: targetDir,
|
|
2480
|
+
label,
|
|
2481
|
+
installed: false,
|
|
2482
|
+
context: {
|
|
2483
|
+
...contextFromState(role, installState),
|
|
2484
|
+
...extraContext
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
function contextFromState(role, installState) {
|
|
2489
|
+
return {
|
|
2490
|
+
role,
|
|
1695
2491
|
initSource: installState.kind === "legacy" ? "legacy-easy-coding" : "fresh",
|
|
1696
2492
|
legacyAssets: installState.kind === "legacy" ? installState.legacyAssets : void 0,
|
|
1697
2493
|
legacyMissingHarnessFiles: installState.kind === "legacy" ? installState.missingHarnessFiles : void 0
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
).join("
|
|
1703
|
-
|
|
1704
|
-
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
function toPosixRelative2(from, to) {
|
|
2497
|
+
const relative = path18.relative(from, to);
|
|
2498
|
+
return relative ? relative.split(path18.sep).join("/") : ".";
|
|
2499
|
+
}
|
|
2500
|
+
async function resolveInitPlatforms(cwd, opts, parentInstalled) {
|
|
2501
|
+
if (opts.agent || !parentInstalled) {
|
|
2502
|
+
return resolvePlatforms(opts, ["claude-code"]);
|
|
2503
|
+
}
|
|
2504
|
+
const config = await readConfigYaml(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
|
|
2505
|
+
if (Array.isArray(config.agents) && config.agents.length > 0) {
|
|
2506
|
+
return config.agents;
|
|
2507
|
+
}
|
|
2508
|
+
return resolvePlatforms(opts, ["claude-code"]);
|
|
2509
|
+
}
|
|
2510
|
+
async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
|
|
2511
|
+
if (!await pathExists(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
const config = parentTarget2.installed ? await readConfigYaml(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
|
|
2515
|
+
const platforms = Array.isArray(config.agents) && config.agents.length > 0 ? config.agents : installPlatforms;
|
|
2516
|
+
await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
|
|
2517
|
+
}
|
|
2518
|
+
async function refreshInstalledChildTopologies(targets) {
|
|
2519
|
+
for (const target of targets) {
|
|
2520
|
+
if (!target.installed || target.context.role !== "submodule-child") {
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
const configPath2 = path18.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
|
|
2524
|
+
if (!await pathExists(configPath2)) {
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
await updateSupermoduleConfig(configPath2, supermoduleConfigFromContext(target.context));
|
|
2528
|
+
}
|
|
1705
2529
|
}
|
|
1706
2530
|
|
|
1707
2531
|
// src/commands/status.ts
|
|
1708
|
-
import
|
|
2532
|
+
import path21 from "path";
|
|
1709
2533
|
import chalk5 from "chalk";
|
|
1710
2534
|
|
|
1711
2535
|
// src/utils/compare-versions.ts
|
|
1712
|
-
import
|
|
2536
|
+
import path19 from "path";
|
|
1713
2537
|
function normalize(version) {
|
|
1714
2538
|
const [core] = String(version ?? "").split("-");
|
|
1715
2539
|
return core.split(".").map((part) => {
|
|
@@ -1733,13 +2557,13 @@ function isVersionBehind(installed, current = VERSION) {
|
|
|
1733
2557
|
return compareVersions(installed, current) === -1;
|
|
1734
2558
|
}
|
|
1735
2559
|
async function checkForUpgrade(cwd) {
|
|
1736
|
-
const
|
|
1737
|
-
if (!await pathExists(
|
|
2560
|
+
const configPath2 = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2561
|
+
if (!await pathExists(configPath2)) {
|
|
1738
2562
|
return;
|
|
1739
2563
|
}
|
|
1740
2564
|
let config;
|
|
1741
2565
|
try {
|
|
1742
|
-
config = await readConfigYaml(
|
|
2566
|
+
config = await readConfigYaml(configPath2);
|
|
1743
2567
|
} catch {
|
|
1744
2568
|
return;
|
|
1745
2569
|
}
|
|
@@ -1754,14 +2578,14 @@ async function checkForUpgrade(cwd) {
|
|
|
1754
2578
|
|
|
1755
2579
|
// src/utils/session.ts
|
|
1756
2580
|
import { readdir as readdir6 } from "fs/promises";
|
|
1757
|
-
import
|
|
2581
|
+
import path20 from "path";
|
|
1758
2582
|
var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
1759
2583
|
function getSessionDir(cwd) {
|
|
1760
|
-
return
|
|
2584
|
+
return path20.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
1761
2585
|
}
|
|
1762
2586
|
function getSessionFilePath(cwd, ppid) {
|
|
1763
2587
|
const pid = ppid ?? process.ppid;
|
|
1764
|
-
return
|
|
2588
|
+
return path20.join(getSessionDir(cwd), `${pid}.json`);
|
|
1765
2589
|
}
|
|
1766
2590
|
async function readSessionFile(cwd, ppid) {
|
|
1767
2591
|
const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
|
|
@@ -1775,11 +2599,11 @@ async function readSessionFile(cwd, ppid) {
|
|
|
1775
2599
|
async function status() {
|
|
1776
2600
|
renderBanner();
|
|
1777
2601
|
const cwd = process.cwd();
|
|
1778
|
-
const
|
|
1779
|
-
if (!await pathExists(
|
|
2602
|
+
const configPath2 = path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2603
|
+
if (!await pathExists(configPath2)) {
|
|
1780
2604
|
throw new Error("No easy-coding harness found in this project.");
|
|
1781
2605
|
}
|
|
1782
|
-
const config = await readConfigYaml(
|
|
2606
|
+
const config = await readConfigYaml(configPath2);
|
|
1783
2607
|
const tasks = await listTasks(cwd);
|
|
1784
2608
|
const taskCounts = summarizeTaskStatuses(tasks);
|
|
1785
2609
|
const activeTasks = tasks.filter((item) => isActiveTask(item.task));
|
|
@@ -1829,7 +2653,7 @@ async function status() {
|
|
|
1829
2653
|
|
|
1830
2654
|
// src/commands/update.ts
|
|
1831
2655
|
import { execFileSync } from "child_process";
|
|
1832
|
-
import { cancel as
|
|
2656
|
+
import { cancel as cancel4, confirm as confirm3, outro as outro4 } from "@clack/prompts";
|
|
1833
2657
|
import chalk6 from "chalk";
|
|
1834
2658
|
async function update(opts) {
|
|
1835
2659
|
renderBanner();
|
|
@@ -1848,7 +2672,7 @@ async function update(opts) {
|
|
|
1848
2672
|
initialValue: true
|
|
1849
2673
|
});
|
|
1850
2674
|
if (typeof confirmed === "symbol" || !confirmed) {
|
|
1851
|
-
|
|
2675
|
+
cancel4("Update cancelled.");
|
|
1852
2676
|
return;
|
|
1853
2677
|
}
|
|
1854
2678
|
}
|
|
@@ -1858,37 +2682,62 @@ async function update(opts) {
|
|
|
1858
2682
|
}
|
|
1859
2683
|
|
|
1860
2684
|
// src/commands/upgrade.ts
|
|
1861
|
-
import
|
|
1862
|
-
import { cancel as
|
|
2685
|
+
import path22 from "path";
|
|
2686
|
+
import { cancel as cancel5, confirm as confirm4, outro as outro5 } from "@clack/prompts";
|
|
1863
2687
|
import chalk7 from "chalk";
|
|
2688
|
+
var EXPECTED_HOOK_REGISTRATION_SCRIPTS = {
|
|
2689
|
+
"claude-code": [
|
|
2690
|
+
{ event: "SessionStart", scriptName: "session-start.py" },
|
|
2691
|
+
{ event: "UserPromptSubmit", scriptName: "session-start.py" },
|
|
2692
|
+
{ event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" },
|
|
2693
|
+
{ event: "PreToolUse", scriptName: "inject-subagent-context.py" }
|
|
2694
|
+
],
|
|
2695
|
+
codex: [
|
|
2696
|
+
{ event: "UserPromptSubmit", scriptName: "session-start.py" },
|
|
2697
|
+
{ event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" }
|
|
2698
|
+
],
|
|
2699
|
+
qoder: [
|
|
2700
|
+
{ event: "UserPromptSubmit", scriptName: "session-start.py" },
|
|
2701
|
+
{ event: "UserPromptSubmit", scriptName: "inject-workflow-state.py" },
|
|
2702
|
+
{ event: "PreToolUse", scriptName: "inject-subagent-context.py" }
|
|
2703
|
+
]
|
|
2704
|
+
};
|
|
2705
|
+
var MANAGED_HOOK_SCRIPT_NAMES = {
|
|
2706
|
+
"claude-code": [
|
|
2707
|
+
...new Set(
|
|
2708
|
+
EXPECTED_HOOK_REGISTRATION_SCRIPTS["claude-code"].map(({ scriptName }) => scriptName)
|
|
2709
|
+
)
|
|
2710
|
+
],
|
|
2711
|
+
codex: [...new Set(EXPECTED_HOOK_REGISTRATION_SCRIPTS.codex.map(({ scriptName }) => scriptName))],
|
|
2712
|
+
qoder: [...new Set(EXPECTED_HOOK_REGISTRATION_SCRIPTS.qoder.map(({ scriptName }) => scriptName))]
|
|
2713
|
+
};
|
|
1864
2714
|
async function upgrade(opts) {
|
|
1865
2715
|
renderBanner();
|
|
1866
2716
|
const cwd = process.cwd();
|
|
1867
|
-
const
|
|
1868
|
-
if (!await pathExists(configPath)) {
|
|
2717
|
+
const targets = await resolveUpgradeTargets(cwd);
|
|
2718
|
+
if (!await pathExists(targets[0].configPath)) {
|
|
1869
2719
|
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
1870
2720
|
}
|
|
1871
|
-
const
|
|
1872
|
-
const
|
|
1873
|
-
const
|
|
1874
|
-
if (!
|
|
1875
|
-
throw new Error(
|
|
1876
|
-
"config.yaml is missing required harness fields (harness_version / agents). This project predates the current config layout. Run `easy-coding clear` then `easy-coding init` to migrate \u2014 your tasks, spec, and memory are preserved."
|
|
1877
|
-
);
|
|
1878
|
-
}
|
|
1879
|
-
const relation = compareVersions(installedVersion, VERSION);
|
|
1880
|
-
if (relation === 0) {
|
|
2721
|
+
const pending = await resolvePendingUpgradeTargets(targets);
|
|
2722
|
+
const parentTopologyRefresh = await resolveParentTopologyRefresh(targets, pending);
|
|
2723
|
+
const childTopologyRefreshes = await resolveChildTopologyRefreshes(targets, pending);
|
|
2724
|
+
if (pending.length === 0 && !parentTopologyRefresh && childTopologyRefreshes.length === 0) {
|
|
1881
2725
|
outro5(chalk7.green(`easy-coding harness is already up to date (${VERSION}).`));
|
|
1882
2726
|
return;
|
|
1883
2727
|
}
|
|
1884
|
-
if (relation === 1) {
|
|
1885
|
-
throw new Error(
|
|
1886
|
-
`Project harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
1887
|
-
);
|
|
1888
|
-
}
|
|
1889
2728
|
const summary = [
|
|
1890
|
-
|
|
1891
|
-
|
|
2729
|
+
"Upgrade targets:",
|
|
2730
|
+
...pending.length > 0 ? pending.map(
|
|
2731
|
+
({ target, config }) => `- ${target.label}: ${String(config.harness_version ?? "unknown")} -> ${VERSION}; agents: ${config.agents.join(", ")}`
|
|
2732
|
+
) : ["- (none)"],
|
|
2733
|
+
...parentTopologyRefresh ? [
|
|
2734
|
+
"Supermodule topology refresh:",
|
|
2735
|
+
`- ${parentTopologyRefresh.target.label}: submodules: ${parentTopologyRefresh.submodulePaths.length > 0 ? parentTopologyRefresh.submodulePaths.join(", ") : "(none)"}`
|
|
2736
|
+
] : [],
|
|
2737
|
+
...childTopologyRefreshes.length > 0 ? [
|
|
2738
|
+
parentTopologyRefresh ? "" : "Supermodule topology refresh:",
|
|
2739
|
+
...childTopologyRefreshes.map(({ target }) => `- ${target.label}: role: submodule-child`)
|
|
2740
|
+
].filter(Boolean) : [],
|
|
1892
2741
|
"Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
|
|
1893
2742
|
"Will update project-init task to recommend ec-init re-run for version adaptation.",
|
|
1894
2743
|
"Will not touch memory, state, spec, or project knowledge files."
|
|
@@ -1903,24 +2752,256 @@ async function upgrade(opts) {
|
|
|
1903
2752
|
initialValue: true
|
|
1904
2753
|
});
|
|
1905
2754
|
if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
|
|
1906
|
-
|
|
2755
|
+
cancel5("Upgrade cancelled.");
|
|
1907
2756
|
return;
|
|
1908
2757
|
}
|
|
1909
2758
|
}
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
2759
|
+
for (const { target, config } of pending) {
|
|
2760
|
+
const artifacts = await configurePlatformsForDir(
|
|
2761
|
+
target.dir,
|
|
2762
|
+
config.agents,
|
|
2763
|
+
target.boundary
|
|
2764
|
+
);
|
|
2765
|
+
await writeRuntimeScaffold(target.dir, config.agents, {
|
|
2766
|
+
supermodule: target.supermodule
|
|
2767
|
+
});
|
|
2768
|
+
await writeInstallManifest(target.dir, {
|
|
2769
|
+
harnessVersion: VERSION,
|
|
2770
|
+
agents: config.agents,
|
|
2771
|
+
artifacts
|
|
2772
|
+
});
|
|
2773
|
+
await ensureEasyCodingSessionsIgnored(target.dir);
|
|
2774
|
+
await updateHarnessVersion(target.configPath, VERSION);
|
|
2775
|
+
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
2776
|
+
await setPendingInitSince(target.dir, VERSION);
|
|
2777
|
+
}
|
|
2778
|
+
if (parentTopologyRefresh) {
|
|
2779
|
+
await refreshSupermoduleParent(
|
|
2780
|
+
parentTopologyRefresh.target.dir,
|
|
2781
|
+
parentTopologyRefresh.agents,
|
|
2782
|
+
parentTopologyRefresh.submodulePaths
|
|
2783
|
+
);
|
|
1913
2784
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2785
|
+
for (const { target } of childTopologyRefreshes) {
|
|
2786
|
+
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
2787
|
+
}
|
|
2788
|
+
outro5(
|
|
2789
|
+
chalk7.green(
|
|
2790
|
+
pending.length > 0 ? `easy-coding harness upgraded to ${VERSION}.` : "easy-coding supermodule topology refreshed."
|
|
2791
|
+
)
|
|
2792
|
+
);
|
|
2793
|
+
}
|
|
2794
|
+
async function resolvePendingUpgradeTargets(targets) {
|
|
2795
|
+
const pending = [];
|
|
2796
|
+
for (const target of targets) {
|
|
2797
|
+
if (!await pathExists(target.configPath)) {
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
const config = await readConfigYaml(target.configPath);
|
|
2801
|
+
const installedVersion = String(config.harness_version ?? "");
|
|
2802
|
+
const hasAgents = Array.isArray(config.agents) && config.agents.length > 0;
|
|
2803
|
+
if (!installedVersion || !hasAgents) {
|
|
2804
|
+
throw new Error(
|
|
2805
|
+
`${target.label} config.yaml is missing required harness fields (harness_version / agents). This project predates the current config layout. Run \`easy-coding clear\` then \`easy-coding init\` to migrate \u2014 your tasks, spec, and memory are preserved.`
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
const relation = compareVersions(installedVersion, VERSION);
|
|
2809
|
+
if (relation === 1) {
|
|
2810
|
+
throw new Error(
|
|
2811
|
+
`${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
if (relation === -1 || relation === 0 && await needsHookConfigRefresh(target, config.agents)) {
|
|
2815
|
+
pending.push({ target, config });
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return pending;
|
|
2819
|
+
}
|
|
2820
|
+
async function needsHookConfigRefresh(target, agents) {
|
|
2821
|
+
const manifest = await readInstallManifest(target.dir);
|
|
2822
|
+
for (const agent of agents) {
|
|
2823
|
+
const meta = resolvePlatformMeta(target.dir, agent);
|
|
2824
|
+
const configPath2 = path22.join(target.dir, meta.hookConfigFile);
|
|
2825
|
+
const content = await readTextIfExists(configPath2);
|
|
2826
|
+
if (content === null) {
|
|
2827
|
+
return true;
|
|
2828
|
+
}
|
|
2829
|
+
let parsed;
|
|
2830
|
+
try {
|
|
2831
|
+
parsed = JSON.parse(content);
|
|
2832
|
+
} catch {
|
|
2833
|
+
return true;
|
|
2834
|
+
}
|
|
2835
|
+
const commandsByEvent = collectHookCommandsByEvent(parsed.hooks);
|
|
2836
|
+
const commands = [...commandsByEvent.values()].flat();
|
|
2837
|
+
const expectedRegistrations = expectedHookRegistrations(target.dir, meta, agent);
|
|
2838
|
+
const expectedCommandSet = new Set(
|
|
2839
|
+
expectedRegistrations.map((registration) => registration.command)
|
|
2840
|
+
);
|
|
2841
|
+
if (!hasExpectedHookRegistrations(commandsByEvent, expectedRegistrations)) {
|
|
2842
|
+
return true;
|
|
2843
|
+
}
|
|
2844
|
+
const manifestManagedCommands = new Set(
|
|
2845
|
+
manifest?.hook_registrations.filter((registration) => registration.platform === agent).map((registration) => registration.command) ?? []
|
|
2846
|
+
);
|
|
2847
|
+
if (commands.some(
|
|
2848
|
+
(command) => isUnexpectedManagedHookCommand(
|
|
2849
|
+
command,
|
|
2850
|
+
expectedCommandSet,
|
|
2851
|
+
target.dir,
|
|
2852
|
+
meta,
|
|
2853
|
+
agent,
|
|
2854
|
+
manifestManagedCommands
|
|
2855
|
+
)
|
|
2856
|
+
)) {
|
|
2857
|
+
return true;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
return false;
|
|
2861
|
+
}
|
|
2862
|
+
function expectedHookRegistrations(cwd, meta, platform) {
|
|
2863
|
+
return EXPECTED_HOOK_REGISTRATION_SCRIPTS[platform].map(({ event, scriptName }) => ({
|
|
2864
|
+
event,
|
|
2865
|
+
command: renderHookCommand(cwd, meta.templateContext, scriptName)
|
|
2866
|
+
}));
|
|
2867
|
+
}
|
|
2868
|
+
function hasExpectedHookRegistrations(commandsByEvent, expectedRegistrations) {
|
|
2869
|
+
const actualCounts = countRegistrations(
|
|
2870
|
+
[...commandsByEvent.entries()].flatMap(
|
|
2871
|
+
([event, commands]) => commands.map((command) => ({ event, command }))
|
|
2872
|
+
)
|
|
2873
|
+
);
|
|
2874
|
+
const expectedCounts = countRegistrations(expectedRegistrations);
|
|
2875
|
+
return [...expectedCounts.entries()].every(
|
|
2876
|
+
([registrationKey, expectedCount]) => (actualCounts.get(registrationKey) ?? 0) >= expectedCount
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
function countRegistrations(registrations) {
|
|
2880
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2881
|
+
for (const registration of registrations) {
|
|
2882
|
+
const key = hookRegistrationKey(registration);
|
|
2883
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2884
|
+
}
|
|
2885
|
+
return counts;
|
|
2886
|
+
}
|
|
2887
|
+
function hookRegistrationKey(registration) {
|
|
2888
|
+
return `${registration.event}\0${registration.command}`;
|
|
2889
|
+
}
|
|
2890
|
+
function collectHookCommandsByEvent(hooks) {
|
|
2891
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
2892
|
+
return /* @__PURE__ */ new Map();
|
|
2893
|
+
}
|
|
2894
|
+
const commandsByEvent = /* @__PURE__ */ new Map();
|
|
2895
|
+
for (const [event, value] of Object.entries(hooks)) {
|
|
2896
|
+
if (!Array.isArray(value)) {
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
for (const group of value) {
|
|
2900
|
+
if (!group || typeof group !== "object") {
|
|
2901
|
+
continue;
|
|
2902
|
+
}
|
|
2903
|
+
const hookItems = group.hooks;
|
|
2904
|
+
if (!Array.isArray(hookItems)) {
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
for (const hook of hookItems) {
|
|
2908
|
+
if (!hook || typeof hook !== "object") {
|
|
2909
|
+
continue;
|
|
2910
|
+
}
|
|
2911
|
+
const command = hook.command;
|
|
2912
|
+
if (typeof command === "string") {
|
|
2913
|
+
const eventCommands = commandsByEvent.get(event) ?? [];
|
|
2914
|
+
eventCommands.push(command);
|
|
2915
|
+
commandsByEvent.set(event, eventCommands);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
return commandsByEvent;
|
|
2921
|
+
}
|
|
2922
|
+
function isUnexpectedManagedHookCommand(command, expectedCommands, cwd, meta, platform, manifestManagedCommands) {
|
|
2923
|
+
if (expectedCommands.has(command)) {
|
|
2924
|
+
return false;
|
|
2925
|
+
}
|
|
2926
|
+
if (manifestManagedCommands.has(command)) {
|
|
2927
|
+
return true;
|
|
2928
|
+
}
|
|
2929
|
+
const hookPath = extractHookPathFromCommand(command);
|
|
2930
|
+
return hookPath === null ? false : isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform);
|
|
2931
|
+
}
|
|
2932
|
+
function isCurrentProjectManagedHookPath(cwd, hookPath, meta, platform) {
|
|
2933
|
+
const normalizedHookPath = normalizePathForHookComparison(hookPath);
|
|
2934
|
+
const configDir = normalizePathForHookComparison(meta.templateContext.platform_config_dir);
|
|
2935
|
+
return MANAGED_HOOK_SCRIPT_NAMES[platform].some((scriptName) => {
|
|
2936
|
+
const relativeHookPath = `${configDir}/hooks/${scriptName}`;
|
|
2937
|
+
if (normalizedHookPath === relativeHookPath) {
|
|
2938
|
+
return true;
|
|
2939
|
+
}
|
|
2940
|
+
return pathAliases(
|
|
2941
|
+
normalizePathForHookComparison(path22.resolve(cwd, relativeHookPath))
|
|
2942
|
+
).includes(normalizedHookPath);
|
|
1919
2943
|
});
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2944
|
+
}
|
|
2945
|
+
function normalizePathForHookComparison(value) {
|
|
2946
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2947
|
+
}
|
|
2948
|
+
function pathAliases(value) {
|
|
2949
|
+
const aliases = /* @__PURE__ */ new Set([value]);
|
|
2950
|
+
if (value.startsWith("/private/var/")) {
|
|
2951
|
+
aliases.add(value.replace(/^\/private\/var\//, "/var/"));
|
|
2952
|
+
} else if (value.startsWith("/var/")) {
|
|
2953
|
+
aliases.add(`/private${value}`);
|
|
2954
|
+
}
|
|
2955
|
+
return [...aliases];
|
|
2956
|
+
}
|
|
2957
|
+
async function resolveParentTopologyRefresh(targets, pending) {
|
|
2958
|
+
const parent = targets.find(
|
|
2959
|
+
(target) => target.label === "." && target.supermodule.role === "super-parent"
|
|
2960
|
+
);
|
|
2961
|
+
if (!parent || pending.some(({ target }) => target.label === ".")) {
|
|
2962
|
+
return null;
|
|
2963
|
+
}
|
|
2964
|
+
if (!await pathExists(parent.configPath)) {
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
const config = await readConfigYaml(parent.configPath);
|
|
2968
|
+
if (!Array.isArray(config.agents) || config.agents.length === 0) {
|
|
2969
|
+
return null;
|
|
2970
|
+
}
|
|
2971
|
+
const submodulePaths = parent.supermodule.submodules ?? [];
|
|
2972
|
+
const configuredSubmodules = Array.isArray(config.supermodule?.submodules) ? config.supermodule.submodules : [];
|
|
2973
|
+
const needsRefresh = config.supermodule?.role !== "super-parent" || !sameStringList(configuredSubmodules, submodulePaths);
|
|
2974
|
+
if (!needsRefresh) {
|
|
2975
|
+
return null;
|
|
2976
|
+
}
|
|
2977
|
+
return {
|
|
2978
|
+
target: parent,
|
|
2979
|
+
agents: config.agents,
|
|
2980
|
+
submodulePaths
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
async function resolveChildTopologyRefreshes(targets, pending) {
|
|
2984
|
+
const pendingLabels = new Set(pending.map(({ target }) => target.label));
|
|
2985
|
+
const refreshes = [];
|
|
2986
|
+
for (const target of targets) {
|
|
2987
|
+
if (target.supermodule.role !== "submodule-child" || pendingLabels.has(target.label) || !await pathExists(target.configPath)) {
|
|
2988
|
+
continue;
|
|
2989
|
+
}
|
|
2990
|
+
const config = await readConfigYaml(target.configPath);
|
|
2991
|
+
if (config.supermodule?.role === "submodule-child" && config.supermodule.parent === target.supermodule.parent) {
|
|
2992
|
+
continue;
|
|
2993
|
+
}
|
|
2994
|
+
refreshes.push({ target });
|
|
2995
|
+
}
|
|
2996
|
+
return refreshes;
|
|
2997
|
+
}
|
|
2998
|
+
function sameStringList(left, right) {
|
|
2999
|
+
if (left.length !== right.length) {
|
|
3000
|
+
return false;
|
|
3001
|
+
}
|
|
3002
|
+
const sortedLeft = [...left].sort();
|
|
3003
|
+
const sortedRight = [...right].sort();
|
|
3004
|
+
return sortedLeft.every((value, index) => value === sortedRight[index]);
|
|
1924
3005
|
}
|
|
1925
3006
|
|
|
1926
3007
|
// src/cli.ts
|
|
@@ -1940,11 +3021,14 @@ function withErrorHandling(fn) {
|
|
|
1940
3021
|
await checkForUpgrade(process.cwd());
|
|
1941
3022
|
var program = new Command();
|
|
1942
3023
|
program.name("easy-coding").description(PACKAGE_NAME).version(VERSION, "-v, --version");
|
|
1943
|
-
program.command("init").description("Initialize easy-coding harness in current project").option("--agent <list>", "Comma-separated platforms: claude-code,codex,qoder").option(
|
|
1944
|
-
|
|
3024
|
+
program.command("init").description("Initialize easy-coding harness in current project").option("--agent <list>", "Comma-separated platforms: claude-code,codex,qoder").option(
|
|
3025
|
+
"--submodules <list>",
|
|
3026
|
+
"Comma-separated checked-out submodule paths or names to initialize"
|
|
3027
|
+
).option("--no-submodules", "Initialize only the current directory when .gitmodules exists").option("-y, --yes", "Skip prompts, use defaults").action(withErrorHandling(init));
|
|
3028
|
+
program.command("add-agent").description("Add agent platform support to an existing project").option("--agent <list>", "Comma-separated platforms to add").option("--submodules <list>", "Comma-separated initialized submodule paths or names to update").option("--no-submodules", "Add the agent only to the current directory").action(withErrorHandling(addAgent));
|
|
1945
3029
|
program.command("upgrade").description("Upgrade harness files to current CLI version").option("--dry-run", "Preview changes without applying").option("-y, --yes", "Skip confirmation").action(withErrorHandling(upgrade));
|
|
1946
3030
|
program.command("update").description("Refresh the global CLI to the latest published version").option("--tag <tag>", "npm dist-tag or version to install", "latest").option("--dry-run", "Preview the install command without running it").option("-y, --yes", "Skip confirmation").action(withErrorHandling(update));
|
|
1947
3031
|
program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
|
|
1948
|
-
program.command("clear").description("Remove installed harness files (skills, hooks, config); keep tasks, spec, memory").option("--dry-run", "Preview what would be removed without deleting").option("-y, --yes", "Skip confirmation").action(withErrorHandling(clear));
|
|
3032
|
+
program.command("clear").description("Remove installed harness files (skills, hooks, config); keep tasks, spec, memory").option("--submodules <list>", "Comma-separated initialized submodule paths or names to clear").option("--no-submodules", "Clear only the current directory").option("--dry-run", "Preview what would be removed without deleting").option("-y, --yes", "Skip confirmation").action(withErrorHandling(clear));
|
|
1949
3033
|
program.parse();
|
|
1950
3034
|
//# sourceMappingURL=cli.js.map
|