easy-coding-harness 0.3.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +30 -4
- package/dist/cli.js +1340 -559
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +5 -0
- package/templates/common/skills/ec-git/SKILL.md +9 -1
- package/templates/common/skills/ec-memory/SKILL.md +17 -0
- package/templates/common/skills/ec-task-management/SKILL.md +44 -10
- package/templates/common/skills/ec-workflow/SKILL.md +45 -39
- package/templates/main-constraint/AGENTS.md.tpl +1 -0
- package/templates/main-constraint/CLAUDE.md.tpl +1 -0
- package/templates/shared-hooks/easy_coding_state.py +149 -4
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
353
|
hook_path: artifact.hookPath,
|
|
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
|
}
|
|
@@ -332,17 +472,181 @@ function byPath(a, b) {
|
|
|
332
472
|
return a.path.localeCompare(b.path);
|
|
333
473
|
}
|
|
334
474
|
|
|
335
|
-
// src/
|
|
336
|
-
import
|
|
337
|
-
import path4 from "path";
|
|
475
|
+
// src/utils/runtime-scaffold.ts
|
|
476
|
+
import path6 from "path";
|
|
338
477
|
|
|
339
|
-
// src/utils/
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
478
|
+
// src/utils/template-paths.ts
|
|
479
|
+
import { existsSync } from "fs";
|
|
480
|
+
import path5 from "path";
|
|
481
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
482
|
+
function getTemplateRoot() {
|
|
483
|
+
const here = path5.dirname(fileURLToPath2(import.meta.url));
|
|
484
|
+
const candidates = [
|
|
485
|
+
path5.resolve(here, "../templates"),
|
|
486
|
+
path5.resolve(here, "../../templates"),
|
|
487
|
+
path5.resolve(process.cwd(), "src/templates"),
|
|
488
|
+
path5.resolve(process.cwd(), "templates")
|
|
489
|
+
];
|
|
490
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
491
|
+
if (!found) {
|
|
492
|
+
throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
|
|
493
|
+
}
|
|
494
|
+
return found;
|
|
495
|
+
}
|
|
496
|
+
function getTemplatePath(...segments) {
|
|
497
|
+
return path5.join(getTemplateRoot(), ...segments);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/utils/runtime-scaffold.ts
|
|
501
|
+
async function writeRuntimeScaffold(cwd, agents, opts = {}) {
|
|
502
|
+
const easyCodingDir = path6.join(cwd, EASY_CODING_DIR);
|
|
503
|
+
await ensureDir(easyCodingDir);
|
|
504
|
+
const configPath2 = path6.join(easyCodingDir, CONFIG_FILE);
|
|
505
|
+
if (!await pathExists(configPath2)) {
|
|
506
|
+
const projectName = path6.basename(cwd);
|
|
507
|
+
await writeConfigYaml(
|
|
508
|
+
configPath2,
|
|
509
|
+
createDefaultConfig({
|
|
510
|
+
projectName,
|
|
511
|
+
harnessVersion: VERSION,
|
|
512
|
+
agents,
|
|
513
|
+
supermodule: opts.supermodule
|
|
514
|
+
})
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
await ensureDir(path6.join(easyCodingDir, "tasks"));
|
|
518
|
+
await ensureDir(path6.join(easyCodingDir, SESSIONS_DIR));
|
|
519
|
+
await ensureDir(path6.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
|
|
520
|
+
await ensureDir(path6.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
521
|
+
await writeMemoryScaffold(easyCodingDir);
|
|
522
|
+
await writeTemplatesScaffold(easyCodingDir);
|
|
523
|
+
}
|
|
524
|
+
async function writeTemplatesScaffold(easyCodingDir) {
|
|
525
|
+
const templatesDir = path6.join(easyCodingDir, TEMPLATES_DIR);
|
|
526
|
+
await ensureDir(templatesDir);
|
|
527
|
+
const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
|
|
528
|
+
const dest = path6.join(templatesDir, "dev-spec-skeleton.md");
|
|
529
|
+
await writeTextFile(dest, await readTextFile(src));
|
|
530
|
+
}
|
|
531
|
+
async function writeMemoryScaffold(easyCodingDir) {
|
|
532
|
+
const memoryDir = path6.join(easyCodingDir, MEMORY_DIR);
|
|
533
|
+
await ensureDir(path6.join(memoryDir, "short"));
|
|
534
|
+
await ensureDir(path6.join(memoryDir, "long"));
|
|
535
|
+
for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
|
|
536
|
+
const destination = path6.join(memoryDir, "long", file);
|
|
537
|
+
if (await pathExists(destination)) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const templatePath = getTemplatePath("runtime", "memory", "long", file);
|
|
541
|
+
await writeTextFile(destination, await readTextFile(templatePath));
|
|
542
|
+
}
|
|
543
|
+
const shortTemplateDest = path6.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
|
|
544
|
+
if (!await pathExists(shortTemplateDest)) {
|
|
545
|
+
const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
|
|
546
|
+
await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/utils/task-json.ts
|
|
551
|
+
import { readdir } from "fs/promises";
|
|
552
|
+
import path7 from "path";
|
|
553
|
+
function createProjectInitTask(params) {
|
|
554
|
+
return {
|
|
555
|
+
type: "project-init",
|
|
556
|
+
status: "PENDING",
|
|
557
|
+
created_at: (params.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
558
|
+
created_by: "cli-init",
|
|
559
|
+
last_agent: "cli",
|
|
560
|
+
stage_history: [],
|
|
561
|
+
context: {
|
|
562
|
+
agents_installed: params.agents,
|
|
563
|
+
cli_version: VERSION,
|
|
564
|
+
project_path: params.cwd,
|
|
565
|
+
init_source: params.initSource ?? "fresh",
|
|
566
|
+
...params.legacyAssets ? { legacy_assets: params.legacyAssets } : {},
|
|
567
|
+
...params.legacyMissingHarnessFiles ? { legacy_missing_harness_files: params.legacyMissingHarnessFiles } : {}
|
|
568
|
+
},
|
|
569
|
+
init_log: []
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function getTaskJsonPath(cwd, taskId) {
|
|
573
|
+
return path7.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
|
|
574
|
+
}
|
|
575
|
+
async function writeTaskJson(filePath, task) {
|
|
576
|
+
await writeTextFile(filePath, JSON.stringify(task, null, 2));
|
|
577
|
+
}
|
|
578
|
+
async function readTaskJson(filePath) {
|
|
579
|
+
return JSON.parse(await readTextFile(filePath));
|
|
580
|
+
}
|
|
581
|
+
async function writeProjectInitTask(cwd, agents, options = {}) {
|
|
582
|
+
await writeTaskJson(
|
|
583
|
+
getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID),
|
|
584
|
+
createProjectInitTask({
|
|
585
|
+
cwd,
|
|
586
|
+
agents,
|
|
587
|
+
initSource: options.initSource,
|
|
588
|
+
legacyAssets: options.legacyAssets,
|
|
589
|
+
legacyMissingHarnessFiles: options.legacyMissingHarnessFiles
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
async function setPendingInitSince(cwd, version) {
|
|
594
|
+
const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
|
|
595
|
+
if (!await pathExists(filePath)) return;
|
|
596
|
+
const task = await readTaskJson(filePath);
|
|
597
|
+
if (task.status !== "COMPLETE") return;
|
|
598
|
+
task.pending_init_since = version;
|
|
599
|
+
await writeTaskJson(filePath, task);
|
|
600
|
+
}
|
|
601
|
+
async function listTasks(cwd) {
|
|
602
|
+
const tasksDir = path7.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
603
|
+
if (!await pathExists(tasksDir)) {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
const entries = await readdir(tasksDir, { withFileTypes: true });
|
|
607
|
+
const tasks = [];
|
|
608
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
609
|
+
if (!entry.isDirectory()) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const taskPath = getTaskJsonPath(cwd, entry.name);
|
|
613
|
+
if (!await pathExists(taskPath)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
tasks.push({ id: entry.name, task: await readTaskJson(taskPath) });
|
|
617
|
+
}
|
|
618
|
+
return tasks;
|
|
619
|
+
}
|
|
620
|
+
function summarizeTaskStatuses(tasks) {
|
|
621
|
+
return tasks.reduce(
|
|
622
|
+
(summary, item) => {
|
|
623
|
+
summary[item.task.status] = (summary[item.task.status] ?? 0) + 1;
|
|
624
|
+
return summary;
|
|
625
|
+
},
|
|
626
|
+
{}
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
function isActiveTask(task) {
|
|
630
|
+
return task.status !== "COMPLETE" && task.status !== "CLOSED";
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/commands/install-harness.ts
|
|
634
|
+
import path13 from "path";
|
|
635
|
+
|
|
636
|
+
// src/configurators/claude.ts
|
|
637
|
+
import path9 from "path";
|
|
638
|
+
|
|
639
|
+
// src/configurators/shared.ts
|
|
640
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
641
|
+
import path8 from "path";
|
|
642
|
+
|
|
643
|
+
// src/utils/marked-region.ts
|
|
644
|
+
var MarkedRegionError = class extends Error {
|
|
645
|
+
constructor(message) {
|
|
646
|
+
super(message);
|
|
647
|
+
this.name = "MarkedRegionError";
|
|
648
|
+
}
|
|
649
|
+
};
|
|
346
650
|
function extractMarkedRegion(content) {
|
|
347
651
|
const start = content.indexOf(GENERATED_REGION_START);
|
|
348
652
|
const end = content.indexOf(GENERATED_REGION_END);
|
|
@@ -373,28 +677,6 @@ function removeMarkedRegion(content) {
|
|
|
373
677
|
` : "";
|
|
374
678
|
}
|
|
375
679
|
|
|
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
680
|
// src/configurators/shared.ts
|
|
399
681
|
var UnresolvedPlaceholderError = class extends Error {
|
|
400
682
|
constructor(contentName, placeholders) {
|
|
@@ -416,14 +698,14 @@ function resolvePlaceholders(content, ctx, contentName = "template") {
|
|
|
416
698
|
}
|
|
417
699
|
async function resolveSkills(ctx) {
|
|
418
700
|
const skillsRoot = getTemplatePath("common", "skills");
|
|
419
|
-
const entries = await
|
|
701
|
+
const entries = await readdir2(skillsRoot);
|
|
420
702
|
const skills = [];
|
|
421
703
|
for (const entry of entries.sort()) {
|
|
422
|
-
const skillDir =
|
|
704
|
+
const skillDir = path8.join(skillsRoot, entry);
|
|
423
705
|
if (!await isDirectory(skillDir)) {
|
|
424
706
|
continue;
|
|
425
707
|
}
|
|
426
|
-
const content = await readTextFile(
|
|
708
|
+
const content = await readTextFile(path8.join(skillDir, "SKILL.md"));
|
|
427
709
|
skills.push({
|
|
428
710
|
name: entry,
|
|
429
711
|
content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
|
|
@@ -436,10 +718,10 @@ async function resolveBundledSkills(ctx) {
|
|
|
436
718
|
if (!await pathExists(bundledRoot)) {
|
|
437
719
|
return [];
|
|
438
720
|
}
|
|
439
|
-
const entries = await
|
|
721
|
+
const entries = await readdir2(bundledRoot);
|
|
440
722
|
const bundled = [];
|
|
441
723
|
for (const entry of entries.sort()) {
|
|
442
|
-
const sourceDir =
|
|
724
|
+
const sourceDir = path8.join(bundledRoot, entry);
|
|
443
725
|
if (await isDirectory(sourceDir)) {
|
|
444
726
|
bundled.push({ name: entry, sourceDir, context: ctx });
|
|
445
727
|
}
|
|
@@ -450,13 +732,13 @@ async function writeSkills(dir, skills, bundled) {
|
|
|
450
732
|
await ensureDir(dir);
|
|
451
733
|
const written = [];
|
|
452
734
|
for (const skill of skills) {
|
|
453
|
-
const destination =
|
|
735
|
+
const destination = path8.join(dir, skill.name, "SKILL.md");
|
|
454
736
|
await writeTextFile(destination, skill.content);
|
|
455
737
|
written.push(destination);
|
|
456
738
|
}
|
|
457
739
|
for (const skill of bundled) {
|
|
458
740
|
written.push(
|
|
459
|
-
...await copyTemplateDirectory(skill.sourceDir,
|
|
741
|
+
...await copyTemplateDirectory(skill.sourceDir, path8.join(dir, skill.name), skill.context)
|
|
460
742
|
);
|
|
461
743
|
}
|
|
462
744
|
return written;
|
|
@@ -464,14 +746,14 @@ async function writeSkills(dir, skills, bundled) {
|
|
|
464
746
|
async function writeSharedHooks(dir, platform, opts = {}) {
|
|
465
747
|
const hooksRoot = getTemplatePath("shared-hooks");
|
|
466
748
|
const ctx = PLATFORM_META[platform].templateContext;
|
|
467
|
-
const entries = await
|
|
749
|
+
const entries = await readdir2(hooksRoot);
|
|
468
750
|
await ensureDir(dir);
|
|
469
751
|
const written = [];
|
|
470
752
|
for (const entry of entries.sort()) {
|
|
471
753
|
if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
|
|
472
754
|
continue;
|
|
473
755
|
}
|
|
474
|
-
const sourcePath =
|
|
756
|
+
const sourcePath = path8.join(hooksRoot, entry);
|
|
475
757
|
if ((await stat2(sourcePath)).isDirectory()) {
|
|
476
758
|
continue;
|
|
477
759
|
}
|
|
@@ -480,7 +762,7 @@ async function writeSharedHooks(dir, platform, opts = {}) {
|
|
|
480
762
|
ctx,
|
|
481
763
|
`shared-hooks/${entry}`
|
|
482
764
|
);
|
|
483
|
-
const destination =
|
|
765
|
+
const destination = path8.join(dir, entry);
|
|
484
766
|
await writeTextFile(destination, content);
|
|
485
767
|
await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
|
|
486
768
|
written.push(destination);
|
|
@@ -491,16 +773,19 @@ async function copyPlatformTemplates(platformTemplateDir, destination, skipDirs,
|
|
|
491
773
|
const source = getTemplatePath(platformTemplateDir);
|
|
492
774
|
return copyTemplateDirectory(source, destination, ctx, new Set(skipDirs));
|
|
493
775
|
}
|
|
494
|
-
async function writeMainConstraint(cwd, platform) {
|
|
776
|
+
async function writeMainConstraint(cwd, platform, opts = {}) {
|
|
495
777
|
const meta = PLATFORM_META[platform];
|
|
496
|
-
const ctx =
|
|
778
|
+
const ctx = {
|
|
779
|
+
...meta.templateContext,
|
|
780
|
+
supermodule_boundary: renderSupermoduleBoundary(opts.supermodule)
|
|
781
|
+
};
|
|
497
782
|
const templateName = `${meta.mainConstraint}.tpl`;
|
|
498
783
|
const template = await readTextFile(getTemplatePath("main-constraint", templateName));
|
|
499
784
|
const generated = resolvePlaceholders(template, ctx, `main-constraint/${templateName}`);
|
|
500
785
|
if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
|
|
501
786
|
throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
|
|
502
787
|
}
|
|
503
|
-
const destination =
|
|
788
|
+
const destination = path8.join(cwd, meta.mainConstraint);
|
|
504
789
|
const current = await readTextIfExists(destination);
|
|
505
790
|
if (current) {
|
|
506
791
|
const markerContent = extractMarkedRegion(generated);
|
|
@@ -513,15 +798,37 @@ async function writeMainConstraint(cwd, platform) {
|
|
|
513
798
|
}
|
|
514
799
|
return destination;
|
|
515
800
|
}
|
|
801
|
+
function renderSupermoduleBoundary(boundary) {
|
|
802
|
+
if (!boundary || boundary.submodulePaths.length === 0) {
|
|
803
|
+
return "";
|
|
804
|
+
}
|
|
805
|
+
const submodules = boundary.submodulePaths.map((submodulePath) => `- \`${submodulePath}\``);
|
|
806
|
+
return [
|
|
807
|
+
"",
|
|
808
|
+
"## Supermodule Boundary",
|
|
809
|
+
"",
|
|
810
|
+
"This directory is a git supermodule root. The following subdirectories are independent",
|
|
811
|
+
"submodule harness roots with their own git boundaries and Easy Coding runtime:",
|
|
812
|
+
"",
|
|
813
|
+
...submodules,
|
|
814
|
+
"",
|
|
815
|
+
"- Cross-repo work launched from this root uses the parent `.easy-coding` task, session, state, and spec as the source of truth.",
|
|
816
|
+
"- Child `.easy-coding` directories are not parent workflow state sources.",
|
|
817
|
+
"- 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.",
|
|
818
|
+
"- Commit submodule changes in two steps: push each child repo first, then commit and push the parent gitlink update.",
|
|
819
|
+
"- Parent memory should keep cross-repo context; child technical details should follow the owning child repo.",
|
|
820
|
+
""
|
|
821
|
+
].join("\n");
|
|
822
|
+
}
|
|
516
823
|
async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__PURE__ */ new Set()) {
|
|
517
824
|
await ensureDir(destination);
|
|
518
825
|
const written = [];
|
|
519
|
-
for (const entry of (await
|
|
826
|
+
for (const entry of (await readdir2(source)).sort()) {
|
|
520
827
|
if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
|
|
521
828
|
continue;
|
|
522
829
|
}
|
|
523
|
-
const sourcePath =
|
|
524
|
-
const destinationPath =
|
|
830
|
+
const sourcePath = path8.join(source, entry);
|
|
831
|
+
const destinationPath = path8.join(destination, stripTemplateExtension(entry));
|
|
525
832
|
const sourceStat = await stat2(sourcePath);
|
|
526
833
|
if (sourceStat.isDirectory()) {
|
|
527
834
|
written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
|
|
@@ -541,63 +848,65 @@ function stripTemplateExtension(fileName) {
|
|
|
541
848
|
}
|
|
542
849
|
|
|
543
850
|
// src/configurators/claude.ts
|
|
544
|
-
async function configureClaude(cwd) {
|
|
851
|
+
async function configureClaude(cwd, opts = {}) {
|
|
545
852
|
const platform = "claude-code";
|
|
546
853
|
const meta = PLATFORM_META[platform];
|
|
547
854
|
const ctx = meta.templateContext;
|
|
548
|
-
const dest =
|
|
549
|
-
const hookConfigPath =
|
|
855
|
+
const dest = path9.join(cwd, ".claude");
|
|
856
|
+
const hookConfigPath = path9.join(cwd, meta.hookConfigFile);
|
|
550
857
|
const artifacts = [];
|
|
551
858
|
const platformFiles = await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
|
|
552
859
|
artifacts.push(
|
|
553
860
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
554
861
|
(filePath) => fileArtifact(
|
|
555
862
|
filePath,
|
|
556
|
-
filePath.startsWith(
|
|
863
|
+
filePath.startsWith(path9.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
557
864
|
platform
|
|
558
865
|
)
|
|
559
866
|
)
|
|
560
867
|
);
|
|
561
868
|
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
562
869
|
artifacts.push(
|
|
563
|
-
...(await writeSharedHooks(
|
|
870
|
+
...(await writeSharedHooks(path9.join(dest, "hooks"), platform)).map(
|
|
564
871
|
(filePath) => fileArtifact(filePath, "hook", platform)
|
|
565
872
|
)
|
|
566
873
|
);
|
|
567
874
|
artifacts.push(
|
|
568
875
|
...(await writeSkills(
|
|
569
|
-
|
|
876
|
+
path9.join(cwd, meta.skillsDir),
|
|
570
877
|
await resolveSkills(ctx),
|
|
571
878
|
await resolveBundledSkills(ctx)
|
|
572
879
|
)).map((filePath) => fileArtifact(filePath, "skill", platform))
|
|
573
880
|
);
|
|
574
|
-
artifacts.push(
|
|
881
|
+
artifacts.push(
|
|
882
|
+
constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
|
|
883
|
+
);
|
|
575
884
|
return artifacts;
|
|
576
885
|
}
|
|
577
886
|
|
|
578
887
|
// src/configurators/codex.ts
|
|
579
|
-
import
|
|
580
|
-
async function configureCodex(cwd) {
|
|
888
|
+
import path10 from "path";
|
|
889
|
+
async function configureCodex(cwd, opts = {}) {
|
|
581
890
|
const platform = "codex";
|
|
582
891
|
const meta = PLATFORM_META[platform];
|
|
583
892
|
const ctx = meta.templateContext;
|
|
584
|
-
const hookConfigPath =
|
|
893
|
+
const hookConfigPath = path10.join(cwd, meta.hookConfigFile);
|
|
585
894
|
const artifacts = [];
|
|
586
895
|
artifacts.push(
|
|
587
896
|
...(await writeSkills(
|
|
588
|
-
|
|
897
|
+
path10.join(cwd, meta.skillsDir),
|
|
589
898
|
await resolveSkills(ctx),
|
|
590
899
|
await resolveBundledSkills(ctx)
|
|
591
900
|
)).map((filePath) => fileArtifact(filePath, "skill", platform))
|
|
592
901
|
);
|
|
593
902
|
artifacts.push(
|
|
594
|
-
...(await writeSharedHooks(
|
|
903
|
+
...(await writeSharedHooks(path10.join(cwd, meta.hooksDir), platform, {
|
|
595
904
|
skipSubagentContext: true
|
|
596
905
|
})).map((filePath) => fileArtifact(filePath, "hook", platform))
|
|
597
906
|
);
|
|
598
907
|
const platformFiles = await copyPlatformTemplates(
|
|
599
908
|
"codex",
|
|
600
|
-
|
|
909
|
+
path10.join(cwd, ".codex"),
|
|
601
910
|
["hooks"],
|
|
602
911
|
ctx
|
|
603
912
|
);
|
|
@@ -605,28 +914,30 @@ async function configureCodex(cwd) {
|
|
|
605
914
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
606
915
|
(filePath) => fileArtifact(
|
|
607
916
|
filePath,
|
|
608
|
-
filePath.startsWith(
|
|
917
|
+
filePath.startsWith(path10.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
609
918
|
platform
|
|
610
919
|
)
|
|
611
920
|
)
|
|
612
921
|
);
|
|
613
922
|
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
614
|
-
artifacts.push(
|
|
923
|
+
artifacts.push(
|
|
924
|
+
constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
|
|
925
|
+
);
|
|
615
926
|
return artifacts;
|
|
616
927
|
}
|
|
617
928
|
|
|
618
929
|
// src/configurators/qoder.ts
|
|
619
|
-
import { readdir as
|
|
620
|
-
import
|
|
930
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
931
|
+
import path12 from "path";
|
|
621
932
|
|
|
622
933
|
// src/utils/platform-paths.ts
|
|
623
934
|
import { existsSync as existsSync2 } from "fs";
|
|
624
|
-
import
|
|
935
|
+
import path11 from "path";
|
|
625
936
|
function detectQoderCnVariant(cwd) {
|
|
626
937
|
if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
|
|
627
938
|
return true;
|
|
628
939
|
}
|
|
629
|
-
return existsSync2(
|
|
940
|
+
return existsSync2(path11.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
|
|
630
941
|
}
|
|
631
942
|
function resolvePlatformMeta(cwd, platform) {
|
|
632
943
|
const meta = PLATFORM_META[platform];
|
|
@@ -657,12 +968,12 @@ function resolveQoderMetaForBaseDir(baseDir) {
|
|
|
657
968
|
|
|
658
969
|
// src/configurators/qoder.ts
|
|
659
970
|
async function claudeHarnessSkillsExist(cwd) {
|
|
660
|
-
return pathExists(
|
|
971
|
+
return pathExists(path12.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
|
|
661
972
|
}
|
|
662
973
|
async function listFilesRecursive(dir) {
|
|
663
974
|
let entries;
|
|
664
975
|
try {
|
|
665
|
-
entries = await
|
|
976
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
666
977
|
} catch (error) {
|
|
667
978
|
if (error.code === "ENOENT") {
|
|
668
979
|
return [];
|
|
@@ -671,7 +982,7 @@ async function listFilesRecursive(dir) {
|
|
|
671
982
|
}
|
|
672
983
|
const files = [];
|
|
673
984
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
674
|
-
const entryPath =
|
|
985
|
+
const entryPath = path12.join(dir, entry.name);
|
|
675
986
|
if (entry.isDirectory()) {
|
|
676
987
|
files.push(...await listFilesRecursive(entryPath));
|
|
677
988
|
continue;
|
|
@@ -685,32 +996,32 @@ async function listFilesRecursive(dir) {
|
|
|
685
996
|
async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
|
|
686
997
|
const artifacts = [];
|
|
687
998
|
for (const skillName of skillNames) {
|
|
688
|
-
for (const filePath of await listFilesRecursive(
|
|
999
|
+
for (const filePath of await listFilesRecursive(path12.join(skillsDir, skillName))) {
|
|
689
1000
|
artifacts.push(fileArtifact(filePath, "skill", platform));
|
|
690
1001
|
}
|
|
691
1002
|
}
|
|
692
1003
|
return artifacts;
|
|
693
1004
|
}
|
|
694
|
-
async function configureQoder(cwd) {
|
|
1005
|
+
async function configureQoder(cwd, opts = {}) {
|
|
695
1006
|
const platform = "qoder";
|
|
696
1007
|
const meta = resolvePlatformMeta(cwd, platform);
|
|
697
1008
|
const ctx = meta.templateContext;
|
|
698
|
-
const dest =
|
|
699
|
-
const hookConfigPath =
|
|
1009
|
+
const dest = path12.join(cwd, ctx.platform_config_dir);
|
|
1010
|
+
const hookConfigPath = path12.join(cwd, meta.hookConfigFile);
|
|
700
1011
|
const artifacts = [];
|
|
701
1012
|
const platformFiles = await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
|
|
702
1013
|
artifacts.push(
|
|
703
1014
|
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
704
1015
|
(filePath) => fileArtifact(
|
|
705
1016
|
filePath,
|
|
706
|
-
filePath.startsWith(
|
|
1017
|
+
filePath.startsWith(path12.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
707
1018
|
platform
|
|
708
1019
|
)
|
|
709
1020
|
)
|
|
710
1021
|
);
|
|
711
1022
|
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
712
1023
|
artifacts.push(
|
|
713
|
-
...(await writeSharedHooks(
|
|
1024
|
+
...(await writeSharedHooks(path12.join(dest, "hooks"), platform)).map(
|
|
714
1025
|
(filePath) => fileArtifact(filePath, "hook", platform)
|
|
715
1026
|
)
|
|
716
1027
|
);
|
|
@@ -718,20 +1029,22 @@ async function configureQoder(cwd) {
|
|
|
718
1029
|
const bundledSkills = await resolveBundledSkills(ctx);
|
|
719
1030
|
if (!await claudeHarnessSkillsExist(cwd)) {
|
|
720
1031
|
artifacts.push(
|
|
721
|
-
...(await writeSkills(
|
|
1032
|
+
...(await writeSkills(path12.join(dest, "skills"), skills, bundledSkills)).map(
|
|
722
1033
|
(filePath) => fileArtifact(filePath, "skill", platform)
|
|
723
1034
|
)
|
|
724
1035
|
);
|
|
725
1036
|
} else {
|
|
726
1037
|
artifacts.push(
|
|
727
1038
|
...await existingManagedSkillArtifacts(
|
|
728
|
-
|
|
1039
|
+
path12.join(dest, "skills"),
|
|
729
1040
|
[...skills.map((skill) => skill.name), ...bundledSkills.map((skill) => skill.name)],
|
|
730
1041
|
platform
|
|
731
1042
|
)
|
|
732
1043
|
);
|
|
733
1044
|
}
|
|
734
|
-
artifacts.push(
|
|
1045
|
+
artifacts.push(
|
|
1046
|
+
constraintRegionArtifact(await writeMainConstraint(cwd, platform, opts), platform)
|
|
1047
|
+
);
|
|
735
1048
|
return artifacts;
|
|
736
1049
|
}
|
|
737
1050
|
|
|
@@ -742,350 +1055,557 @@ var CONFIGURATORS = {
|
|
|
742
1055
|
qoder: configureQoder
|
|
743
1056
|
};
|
|
744
1057
|
|
|
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
|
-
}
|
|
767
|
-
}
|
|
768
|
-
throw new Error(`Unable to locate package metadata. Tried: ${candidates.join(", ")}`);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// src/ui/banner.ts
|
|
772
|
-
import chalk from "chalk";
|
|
773
|
-
import figlet from "figlet";
|
|
774
|
-
var WIDE_TERMINAL_WIDTH = 88;
|
|
775
|
-
var MEDIUM_TERMINAL_WIDTH = 72;
|
|
776
|
-
function renderBanner() {
|
|
777
|
-
const terminalWidth = process.stdout.columns ?? 120;
|
|
778
|
-
const art = figlet.textSync("Easy Coding", selectBannerPreset(terminalWidth));
|
|
779
|
-
console.log(colorizeBanner(art));
|
|
780
|
-
console.log(chalk.bold.white(` Easy Coding Harness ${chalk.cyan(`v${VERSION}`)}
|
|
781
|
-
`));
|
|
782
|
-
}
|
|
783
|
-
function selectBannerPreset(width) {
|
|
784
|
-
if (width >= WIDE_TERMINAL_WIDTH) {
|
|
785
|
-
return { font: "ANSI Shadow", horizontalLayout: "full" };
|
|
786
|
-
}
|
|
787
|
-
if (width >= MEDIUM_TERMINAL_WIDTH) {
|
|
788
|
-
return { font: "Small Shadow", horizontalLayout: "full" };
|
|
789
|
-
}
|
|
790
|
-
return { font: "Small Slant" };
|
|
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);
|
|
1058
|
+
// src/commands/install-harness.ts
|
|
1059
|
+
async function configurePlatformsForDir(targetDir, platforms, boundary) {
|
|
1060
|
+
const artifacts = [];
|
|
1061
|
+
for (const platform of platforms) {
|
|
1062
|
+
artifacts.push(...await CONFIGURATORS[platform](targetDir, { supermodule: boundary }));
|
|
839
1063
|
}
|
|
840
|
-
|
|
841
|
-
return config;
|
|
1064
|
+
return artifacts;
|
|
842
1065
|
}
|
|
843
|
-
async function
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1066
|
+
async function installHarnessToDir(targetDir, platforms, ctx) {
|
|
1067
|
+
const artifacts = await configurePlatformsForDir(targetDir, platforms, boundaryFromContext(ctx));
|
|
1068
|
+
await writeRuntimeScaffold(targetDir, platforms, {
|
|
1069
|
+
supermodule: supermoduleConfigFromContext(ctx)
|
|
847
1070
|
});
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1071
|
+
await writeInstallManifest(targetDir, {
|
|
1072
|
+
harnessVersion: VERSION,
|
|
1073
|
+
agents: platforms,
|
|
1074
|
+
artifacts
|
|
852
1075
|
});
|
|
1076
|
+
await writeProjectInitTask(targetDir, platforms, {
|
|
1077
|
+
initSource: ctx.initSource,
|
|
1078
|
+
legacyAssets: ctx.legacyAssets,
|
|
1079
|
+
legacyMissingHarnessFiles: ctx.legacyMissingHarnessFiles
|
|
1080
|
+
});
|
|
1081
|
+
await ensureEasyCodingSessionsIgnored(targetDir);
|
|
1082
|
+
return artifacts;
|
|
853
1083
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
if (
|
|
862
|
-
return
|
|
1084
|
+
function supermoduleConfigFromContext(ctx) {
|
|
1085
|
+
if (ctx.role === "super-parent") {
|
|
1086
|
+
return {
|
|
1087
|
+
role: "super-parent",
|
|
1088
|
+
submodules: ctx.submodulePaths ?? []
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
if (ctx.role === "submodule-child") {
|
|
1092
|
+
return {
|
|
1093
|
+
role: "submodule-child",
|
|
1094
|
+
parent: ctx.parent
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
return { role: "standalone" };
|
|
1098
|
+
}
|
|
1099
|
+
async function refreshSupermoduleParent(targetDir, platforms, submodulePaths) {
|
|
1100
|
+
const supermodule = {
|
|
1101
|
+
role: "super-parent",
|
|
1102
|
+
submodules: submodulePaths
|
|
1103
|
+
};
|
|
1104
|
+
await updateSupermoduleConfig(path13.join(targetDir, EASY_CODING_DIR, CONFIG_FILE), supermodule);
|
|
1105
|
+
for (const platform of platforms) {
|
|
1106
|
+
await writeMainConstraint(targetDir, platform, {
|
|
1107
|
+
supermodule: { submodulePaths }
|
|
1108
|
+
});
|
|
863
1109
|
}
|
|
864
|
-
const prefix = current.trimEnd();
|
|
865
|
-
const next = [prefix, heading, entry].filter(Boolean).join("\n");
|
|
866
|
-
await writeTextFile(gitignorePath, next);
|
|
867
|
-
return true;
|
|
868
1110
|
}
|
|
869
|
-
|
|
870
|
-
|
|
1111
|
+
function boundaryFromContext(ctx) {
|
|
1112
|
+
if (ctx.role !== "super-parent") {
|
|
1113
|
+
return void 0;
|
|
1114
|
+
}
|
|
1115
|
+
return { submodulePaths: ctx.submodulePaths ?? [] };
|
|
871
1116
|
}
|
|
872
1117
|
|
|
873
|
-
// src/
|
|
874
|
-
import
|
|
875
|
-
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
if (!await pathExists(configPath)) {
|
|
880
|
-
const projectName = path11.basename(cwd);
|
|
881
|
-
await writeConfigYaml(
|
|
882
|
-
configPath,
|
|
883
|
-
createDefaultConfig({ projectName, harnessVersion: VERSION, agents })
|
|
884
|
-
);
|
|
1118
|
+
// src/commands/platforms.ts
|
|
1119
|
+
import { cancel, confirm, multiselect } from "@clack/prompts";
|
|
1120
|
+
function parseAgentList(agentList) {
|
|
1121
|
+
const values = agentList.split(",").map((value) => value.trim()).filter(Boolean);
|
|
1122
|
+
if (values.length === 0) {
|
|
1123
|
+
throw new Error("No agent platform specified.");
|
|
885
1124
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
await writeTemplatesScaffold(easyCodingDir);
|
|
1125
|
+
const invalid = values.filter((value) => !isAgentPlatform(value));
|
|
1126
|
+
if (invalid.length > 0) {
|
|
1127
|
+
throw new Error(`Unknown agent platform: ${invalid.join(", ")}`);
|
|
1128
|
+
}
|
|
1129
|
+
return [...new Set(values)];
|
|
892
1130
|
}
|
|
893
|
-
async function
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1131
|
+
async function resolvePlatforms(opts, defaults = ["claude-code"]) {
|
|
1132
|
+
if (opts.agent) {
|
|
1133
|
+
return parseAgentList(opts.agent);
|
|
1134
|
+
}
|
|
1135
|
+
if (opts.yes) {
|
|
1136
|
+
return defaults;
|
|
1137
|
+
}
|
|
1138
|
+
let selectedDefaults = defaults;
|
|
1139
|
+
while (true) {
|
|
1140
|
+
const result = await multiselect({
|
|
1141
|
+
message: "Select agent platforms (Space to toggle, Enter to review)",
|
|
1142
|
+
options: AGENT_PLATFORMS.map((platform) => ({
|
|
1143
|
+
label: PLATFORM_META[platform].label,
|
|
1144
|
+
value: platform
|
|
1145
|
+
})),
|
|
1146
|
+
initialValues: selectedDefaults,
|
|
1147
|
+
required: true
|
|
1148
|
+
});
|
|
1149
|
+
if (typeof result === "symbol") {
|
|
1150
|
+
cancel("Platform selection cancelled.");
|
|
1151
|
+
process.exit(1);
|
|
1152
|
+
}
|
|
1153
|
+
const labels = result.map((platform) => PLATFORM_META[platform].label).join(", ");
|
|
1154
|
+
const shouldInstall = await confirm({
|
|
1155
|
+
message: `Install Easy Coding for: ${labels}?`,
|
|
1156
|
+
initialValue: true
|
|
1157
|
+
});
|
|
1158
|
+
if (typeof shouldInstall === "symbol") {
|
|
1159
|
+
cancel("Platform selection cancelled.");
|
|
1160
|
+
process.exit(1);
|
|
1161
|
+
}
|
|
1162
|
+
if (shouldInstall) {
|
|
1163
|
+
return result;
|
|
1164
|
+
}
|
|
1165
|
+
selectedDefaults = result;
|
|
1166
|
+
}
|
|
899
1167
|
}
|
|
900
|
-
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1168
|
+
function parseSubmoduleList(submoduleList, available) {
|
|
1169
|
+
const requested = submoduleList.split(",").map((value) => value.trim()).filter(Boolean);
|
|
1170
|
+
if (requested.length === 0) {
|
|
1171
|
+
throw new Error("No submodule specified.");
|
|
1172
|
+
}
|
|
1173
|
+
const byPathOrName = /* @__PURE__ */ new Map();
|
|
1174
|
+
for (const submodule of available) {
|
|
1175
|
+
byPathOrName.set(submodule.path, submodule);
|
|
1176
|
+
byPathOrName.set(submodule.name, submodule);
|
|
1177
|
+
}
|
|
1178
|
+
const selected = [];
|
|
1179
|
+
const invalid = [];
|
|
1180
|
+
for (const value of requested) {
|
|
1181
|
+
const submodule = byPathOrName.get(value);
|
|
1182
|
+
if (!submodule) {
|
|
1183
|
+
invalid.push(value);
|
|
907
1184
|
continue;
|
|
908
1185
|
}
|
|
909
|
-
|
|
910
|
-
|
|
1186
|
+
if (!selected.some((item) => item.path === submodule.path)) {
|
|
1187
|
+
selected.push(submodule);
|
|
1188
|
+
}
|
|
911
1189
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1190
|
+
if (invalid.length > 0) {
|
|
1191
|
+
throw new Error(`Unknown or unavailable submodule: ${invalid.join(", ")}`);
|
|
1192
|
+
}
|
|
1193
|
+
return selected.sort((a, b) => a.path.localeCompare(b.path));
|
|
1194
|
+
}
|
|
1195
|
+
async function resolveSubmodules(opts, available, defaultSelection = available) {
|
|
1196
|
+
if (opts.submodules === false) {
|
|
1197
|
+
return [];
|
|
916
1198
|
}
|
|
1199
|
+
if (typeof opts.submodules === "string") {
|
|
1200
|
+
return parseSubmoduleList(opts.submodules, available);
|
|
1201
|
+
}
|
|
1202
|
+
if (available.length === 0) {
|
|
1203
|
+
return [];
|
|
1204
|
+
}
|
|
1205
|
+
if (opts.yes) {
|
|
1206
|
+
return defaultSelection;
|
|
1207
|
+
}
|
|
1208
|
+
const result = await multiselect({
|
|
1209
|
+
message: "Select checked-out submodules to initialize (Space to toggle, Enter to confirm)",
|
|
1210
|
+
options: available.map((submodule) => ({
|
|
1211
|
+
label: `${submodule.path} (${submodule.name})`,
|
|
1212
|
+
value: submodule.path
|
|
1213
|
+
})),
|
|
1214
|
+
initialValues: defaultSelection.map((submodule) => submodule.path),
|
|
1215
|
+
required: false
|
|
1216
|
+
});
|
|
1217
|
+
if (typeof result === "symbol") {
|
|
1218
|
+
cancel("Submodule selection cancelled.");
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
const selected = new Set(result);
|
|
1222
|
+
return available.filter((submodule) => selected.has(submodule.path));
|
|
917
1223
|
}
|
|
918
1224
|
|
|
919
|
-
// src/
|
|
920
|
-
import
|
|
921
|
-
import
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1225
|
+
// src/commands/supermodule-targets.ts
|
|
1226
|
+
import path15 from "path";
|
|
1227
|
+
import { cancel as cancel2, multiselect as multiselect2 } from "@clack/prompts";
|
|
1228
|
+
|
|
1229
|
+
// src/utils/gitmodules.ts
|
|
1230
|
+
import { lstat, realpath } from "fs/promises";
|
|
1231
|
+
import path14 from "path";
|
|
1232
|
+
async function parseGitmodules(rootDir) {
|
|
1233
|
+
const content = await readTextIfExists(path14.join(rootDir, ".gitmodules"));
|
|
1234
|
+
if (content === null) {
|
|
1235
|
+
return [];
|
|
1236
|
+
}
|
|
1237
|
+
const entries = [];
|
|
1238
|
+
let current = null;
|
|
1239
|
+
const flushCurrent = () => {
|
|
1240
|
+
if (current?.name && current.path) {
|
|
1241
|
+
entries.push({
|
|
1242
|
+
name: current.name,
|
|
1243
|
+
path: current.path,
|
|
1244
|
+
url: current.url ?? ""
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
939
1247
|
};
|
|
1248
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
1249
|
+
const line = stripGitConfigInlineComment(rawLine).trim();
|
|
1250
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) {
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
const section = line.match(/^\[submodule\s+"(.+)"\]$/i);
|
|
1254
|
+
if (section) {
|
|
1255
|
+
flushCurrent();
|
|
1256
|
+
current = { name: section[1] };
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
if (!current) {
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
const keyValue = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.*)$/);
|
|
1263
|
+
if (!keyValue) {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const key = keyValue[1].toLowerCase();
|
|
1267
|
+
const value = unquote(keyValue[2].trim());
|
|
1268
|
+
if (key === "path") {
|
|
1269
|
+
current.path = normalizeSubmodulePath(value);
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
if (key === "url") {
|
|
1273
|
+
current.url = value;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
flushCurrent();
|
|
1277
|
+
return entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
940
1278
|
}
|
|
941
|
-
function
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
-
);
|
|
1279
|
+
async function listInstallableSubmodules(rootDir) {
|
|
1280
|
+
const entries = await parseGitmodules(rootDir);
|
|
1281
|
+
const installable = [];
|
|
1282
|
+
for (const entry of entries) {
|
|
1283
|
+
if (await isSubmoduleWorktree(rootDir, entry.path)) {
|
|
1284
|
+
installable.push(entry);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return installable;
|
|
961
1288
|
}
|
|
962
|
-
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1289
|
+
function normalizeSubmodulePath(value) {
|
|
1290
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "").trim();
|
|
1291
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
1292
|
+
if (normalized === "" || path14.posix.isAbsolute(normalized) || path14.win32.isAbsolute(value) || parts.some((part) => part === "..")) {
|
|
1293
|
+
throw new Error(`Unsafe submodule path in .gitmodules: ${value}`);
|
|
1294
|
+
}
|
|
1295
|
+
return parts.join("/");
|
|
969
1296
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
return [];
|
|
1297
|
+
function unquote(value) {
|
|
1298
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1299
|
+
return value.slice(1, -1);
|
|
974
1300
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1301
|
+
return value;
|
|
1302
|
+
}
|
|
1303
|
+
function stripGitConfigInlineComment(value) {
|
|
1304
|
+
let inQuotes = false;
|
|
1305
|
+
let escaped = false;
|
|
1306
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1307
|
+
const char = value[index];
|
|
1308
|
+
if (escaped) {
|
|
1309
|
+
escaped = false;
|
|
979
1310
|
continue;
|
|
980
1311
|
}
|
|
981
|
-
|
|
982
|
-
|
|
1312
|
+
if (inQuotes && char === "\\") {
|
|
1313
|
+
escaped = true;
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
if (char === '"') {
|
|
1317
|
+
inQuotes = !inQuotes;
|
|
983
1318
|
continue;
|
|
984
1319
|
}
|
|
985
|
-
|
|
1320
|
+
if (!inQuotes && (char === "#" || char === ";") && isCommentStart(value, index)) {
|
|
1321
|
+
return value.slice(0, index);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return value;
|
|
1325
|
+
}
|
|
1326
|
+
function isCommentStart(value, index) {
|
|
1327
|
+
return index === 0 || /\s/.test(value[index - 1]);
|
|
1328
|
+
}
|
|
1329
|
+
async function isSubmoduleWorktree(rootDir, submodulePath) {
|
|
1330
|
+
const dir = path14.join(rootDir, submodulePath);
|
|
1331
|
+
try {
|
|
1332
|
+
if (!await pathHasNoSymlinkSegments(rootDir, submodulePath)) {
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
const rootRealPath = await realpath(rootDir);
|
|
1336
|
+
const dirRealPath = await realpath(dir);
|
|
1337
|
+
if (!isInsideDirectory(rootRealPath, dirRealPath)) {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
const dirStat = await lstat(dir);
|
|
1341
|
+
if (!dirStat.isDirectory()) {
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
1344
|
+
const gitMarkerStat = await lstat(path14.join(dir, ".git"));
|
|
1345
|
+
return !gitMarkerStat.isSymbolicLink() && (gitMarkerStat.isFile() || gitMarkerStat.isDirectory());
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
if (error.code === "ENOENT") {
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
throw error;
|
|
986
1351
|
}
|
|
987
|
-
return tasks;
|
|
988
1352
|
}
|
|
989
|
-
function
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1353
|
+
async function pathHasNoSymlinkSegments(rootDir, submodulePath) {
|
|
1354
|
+
let current = rootDir;
|
|
1355
|
+
for (const part of submodulePath.split("/")) {
|
|
1356
|
+
current = path14.join(current, part);
|
|
1357
|
+
const partStat = await lstat(current);
|
|
1358
|
+
if (partStat.isSymbolicLink()) {
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return true;
|
|
997
1363
|
}
|
|
998
|
-
function
|
|
999
|
-
|
|
1364
|
+
function isInsideDirectory(parent, child) {
|
|
1365
|
+
const relative = path14.relative(parent, child);
|
|
1366
|
+
return Boolean(relative) && !relative.startsWith("..") && !path14.isAbsolute(relative);
|
|
1000
1367
|
}
|
|
1001
1368
|
|
|
1002
|
-
// src/commands/
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
if (
|
|
1011
|
-
|
|
1369
|
+
// src/commands/supermodule-targets.ts
|
|
1370
|
+
function rejectSubmoduleListWithoutGitmodules(opts) {
|
|
1371
|
+
if (typeof opts.submodules === "string") {
|
|
1372
|
+
throw new Error("--submodules can only be used in a repository with .gitmodules.");
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
async function resolveAddAgentTargets(cwd, opts) {
|
|
1376
|
+
const installedSubmodules = await listInstalledSubmodules(cwd);
|
|
1377
|
+
if ((await parseGitmodules(cwd)).length === 0) {
|
|
1378
|
+
rejectSubmoduleListWithoutGitmodules(opts);
|
|
1379
|
+
return [standaloneTarget(cwd)];
|
|
1380
|
+
}
|
|
1381
|
+
const selected = await resolveSubmodules(opts, installedSubmodules);
|
|
1382
|
+
const parentManagedSubmodulePaths = await listParentManagedSubmodulePaths(cwd);
|
|
1383
|
+
const parentSubmodulePaths = [
|
|
1384
|
+
.../* @__PURE__ */ new Set([...parentManagedSubmodulePaths, ...selected.map((entry) => entry.path)])
|
|
1385
|
+
].sort();
|
|
1386
|
+
return [
|
|
1387
|
+
parentTargetFromPaths(cwd, parentSubmodulePaths),
|
|
1388
|
+
...selected.map((entry) => childTarget(cwd, entry))
|
|
1389
|
+
];
|
|
1390
|
+
}
|
|
1391
|
+
async function resolveUpgradeTargets(cwd) {
|
|
1392
|
+
const installedSubmodules = await listInstalledSubmodules(cwd);
|
|
1393
|
+
if ((await parseGitmodules(cwd)).length === 0) {
|
|
1394
|
+
return [standaloneTarget(cwd)];
|
|
1012
1395
|
}
|
|
1013
|
-
return [
|
|
1396
|
+
return [
|
|
1397
|
+
parentTarget(cwd, installedSubmodules),
|
|
1398
|
+
...installedSubmodules.map((entry) => childTarget(cwd, entry))
|
|
1399
|
+
];
|
|
1014
1400
|
}
|
|
1015
|
-
async function
|
|
1016
|
-
|
|
1017
|
-
|
|
1401
|
+
async function resolveInitSubmoduleSelection(cwd, opts) {
|
|
1402
|
+
const installable = await listInstallableSubmodules(cwd);
|
|
1403
|
+
const installed = await listInstalledSubmodules(cwd);
|
|
1404
|
+
const parentManagedSubmodulePaths = await listParentManagedSubmodulePaths(cwd);
|
|
1405
|
+
const defaultSelection = installable;
|
|
1406
|
+
const selected = await resolveSubmodules(opts, installable, defaultSelection);
|
|
1407
|
+
const parentSubmodulePaths = [
|
|
1408
|
+
.../* @__PURE__ */ new Set([...parentManagedSubmodulePaths, ...selected.map((entry) => entry.path)])
|
|
1409
|
+
].sort();
|
|
1410
|
+
return { installable, installed, selected, parentSubmodulePaths };
|
|
1411
|
+
}
|
|
1412
|
+
async function resolveClearTargets(cwd, opts) {
|
|
1413
|
+
if ((await parseGitmodules(cwd)).length === 0) {
|
|
1414
|
+
rejectSubmoduleListWithoutGitmodules(opts);
|
|
1415
|
+
return [standaloneTarget(cwd)];
|
|
1416
|
+
}
|
|
1417
|
+
const installedSubmodules = await listInstalledSubmodules(cwd);
|
|
1418
|
+
const parent = await pathExists(path15.join(cwd, EASY_CODING_DIR)) ? parentTarget(cwd, installedSubmodules) : null;
|
|
1419
|
+
const children = installedSubmodules.map((entry) => childTarget(cwd, entry));
|
|
1420
|
+
if (opts.submodules === false) {
|
|
1421
|
+
return parent ? [parent] : [];
|
|
1422
|
+
}
|
|
1423
|
+
if (typeof opts.submodules === "string") {
|
|
1424
|
+
return [
|
|
1425
|
+
...parent ? [parent] : [],
|
|
1426
|
+
...parseSubmoduleSelection(opts.submodules, installedSubmodules).map(
|
|
1427
|
+
(entry) => childTarget(cwd, entry)
|
|
1428
|
+
)
|
|
1429
|
+
];
|
|
1018
1430
|
}
|
|
1019
1431
|
if (opts.yes) {
|
|
1020
|
-
return
|
|
1432
|
+
return parent ? [parent] : [];
|
|
1021
1433
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1434
|
+
const candidates = [...parent ? [parent] : [], ...children];
|
|
1435
|
+
if (candidates.length === 0) {
|
|
1436
|
+
return [];
|
|
1437
|
+
}
|
|
1438
|
+
const result = await multiselect2({
|
|
1439
|
+
message: "Select Easy Coding harness targets to clear",
|
|
1440
|
+
options: candidates.map((target) => ({
|
|
1441
|
+
label: target.label === "." ? "parent repository" : `submodule: ${target.label}`,
|
|
1442
|
+
value: target.label
|
|
1443
|
+
})),
|
|
1444
|
+
initialValues: parent ? [parent.label] : [],
|
|
1445
|
+
required: true
|
|
1446
|
+
});
|
|
1447
|
+
if (typeof result === "symbol") {
|
|
1448
|
+
cancel2("Clear target selection cancelled.");
|
|
1449
|
+
process.exit(1);
|
|
1450
|
+
}
|
|
1451
|
+
const selected = new Set(result);
|
|
1452
|
+
return candidates.filter((target) => selected.has(target.label));
|
|
1453
|
+
}
|
|
1454
|
+
async function listInstalledSubmodules(cwd) {
|
|
1455
|
+
const entries = await listInstallableSubmodules(cwd);
|
|
1456
|
+
const installed = [];
|
|
1457
|
+
for (const entry of entries) {
|
|
1458
|
+
if (await pathExists(configPath(path15.join(cwd, entry.path)))) {
|
|
1459
|
+
installed.push(entry);
|
|
1036
1460
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1461
|
+
}
|
|
1462
|
+
return installed;
|
|
1463
|
+
}
|
|
1464
|
+
async function listParentManagedSubmodulePaths(cwd) {
|
|
1465
|
+
const parentConfigPath = configPath(cwd);
|
|
1466
|
+
if (!await pathExists(parentConfigPath)) {
|
|
1467
|
+
return [];
|
|
1468
|
+
}
|
|
1469
|
+
try {
|
|
1470
|
+
const config = await readConfigYaml(parentConfigPath);
|
|
1471
|
+
if (config.supermodule?.role !== "super-parent" || !Array.isArray(config.supermodule.submodules)) {
|
|
1472
|
+
return [];
|
|
1045
1473
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1474
|
+
return config.supermodule.submodules.filter((submodulePath) => typeof submodulePath === "string").sort();
|
|
1475
|
+
} catch {
|
|
1476
|
+
return [];
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function standaloneTarget(cwd) {
|
|
1480
|
+
return {
|
|
1481
|
+
dir: cwd,
|
|
1482
|
+
label: ".",
|
|
1483
|
+
configPath: configPath(cwd),
|
|
1484
|
+
supermodule: { role: "standalone" }
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
function parentTarget(cwd, installedSubmodules) {
|
|
1488
|
+
const submodulePaths = installedSubmodules.map((entry) => entry.path);
|
|
1489
|
+
return parentTargetFromPaths(cwd, submodulePaths);
|
|
1490
|
+
}
|
|
1491
|
+
function parentTargetFromPaths(cwd, submodulePaths) {
|
|
1492
|
+
return {
|
|
1493
|
+
dir: cwd,
|
|
1494
|
+
label: ".",
|
|
1495
|
+
configPath: configPath(cwd),
|
|
1496
|
+
supermodule: { role: "super-parent", submodules: submodulePaths },
|
|
1497
|
+
boundary: { submodulePaths }
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function childTarget(cwd, entry) {
|
|
1501
|
+
const dir = path15.join(cwd, entry.path);
|
|
1502
|
+
return {
|
|
1503
|
+
dir,
|
|
1504
|
+
label: entry.path,
|
|
1505
|
+
configPath: configPath(dir),
|
|
1506
|
+
supermodule: { role: "submodule-child", parent: toPosixRelative(dir, cwd) }
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
function parseSubmoduleSelection(submoduleList, available) {
|
|
1510
|
+
const requested = submoduleList.split(",").map((value) => value.trim()).filter(Boolean);
|
|
1511
|
+
if (requested.length === 0) {
|
|
1512
|
+
throw new Error("No submodule specified.");
|
|
1513
|
+
}
|
|
1514
|
+
const byPathOrName = /* @__PURE__ */ new Map();
|
|
1515
|
+
for (const submodule of available) {
|
|
1516
|
+
byPathOrName.set(submodule.path, submodule);
|
|
1517
|
+
byPathOrName.set(submodule.name, submodule);
|
|
1518
|
+
}
|
|
1519
|
+
const selected = [];
|
|
1520
|
+
const invalid = [];
|
|
1521
|
+
for (const value of requested) {
|
|
1522
|
+
const submodule = byPathOrName.get(value);
|
|
1523
|
+
if (!submodule) {
|
|
1524
|
+
invalid.push(value);
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
if (!selected.some((item) => item.path === submodule.path)) {
|
|
1528
|
+
selected.push(submodule);
|
|
1048
1529
|
}
|
|
1049
|
-
selectedDefaults = result;
|
|
1050
1530
|
}
|
|
1531
|
+
if (invalid.length > 0) {
|
|
1532
|
+
throw new Error(`Unknown or unavailable initialized submodule: ${invalid.join(", ")}`);
|
|
1533
|
+
}
|
|
1534
|
+
return selected.sort((a, b) => a.path.localeCompare(b.path));
|
|
1535
|
+
}
|
|
1536
|
+
function configPath(cwd) {
|
|
1537
|
+
return path15.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
1538
|
+
}
|
|
1539
|
+
function toPosixRelative(from, to) {
|
|
1540
|
+
const relative = path15.relative(from, to);
|
|
1541
|
+
return relative ? relative.split(path15.sep).join("/") : ".";
|
|
1051
1542
|
}
|
|
1052
1543
|
|
|
1053
1544
|
// src/commands/add-agent.ts
|
|
1054
1545
|
async function addAgent(opts) {
|
|
1055
1546
|
renderBanner();
|
|
1056
1547
|
const cwd = process.cwd();
|
|
1057
|
-
const
|
|
1058
|
-
if (!await pathExists(configPath)) {
|
|
1548
|
+
const targets = await resolveAddAgentTargets(cwd, opts);
|
|
1549
|
+
if (!await pathExists(targets[0].configPath)) {
|
|
1059
1550
|
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
1060
1551
|
}
|
|
1061
|
-
const config = await readConfigYaml(configPath);
|
|
1062
1552
|
const platforms = await resolvePlatforms(opts, ["claude-code"]);
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1553
|
+
const installedLabels = [];
|
|
1554
|
+
const refreshedLabels = [];
|
|
1555
|
+
for (const target of targets) {
|
|
1556
|
+
if (!await pathExists(target.configPath)) {
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
const config = await readConfigYaml(target.configPath);
|
|
1560
|
+
const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
|
|
1561
|
+
const agents = [...config.agents, ...toInstall];
|
|
1562
|
+
if (toInstall.length > 0) {
|
|
1563
|
+
const artifacts = await configurePlatformsForDir(
|
|
1564
|
+
target.dir,
|
|
1565
|
+
toInstall,
|
|
1566
|
+
target.boundary
|
|
1567
|
+
);
|
|
1568
|
+
await writeRuntimeScaffold(target.dir, agents, {
|
|
1569
|
+
supermodule: target.supermodule
|
|
1570
|
+
});
|
|
1571
|
+
await writeInstallManifest(target.dir, {
|
|
1572
|
+
harnessVersion: VERSION,
|
|
1573
|
+
agents: toInstall,
|
|
1574
|
+
artifacts,
|
|
1575
|
+
mode: "merge"
|
|
1576
|
+
});
|
|
1577
|
+
await ensureEasyCodingSessionsIgnored(target.dir);
|
|
1578
|
+
await addAgentsToConfig(target.configPath, toInstall);
|
|
1579
|
+
await setPendingInitSince(target.dir, VERSION);
|
|
1580
|
+
installedLabels.push(`${target.label}: ${toInstall.join(", ")}`);
|
|
1581
|
+
}
|
|
1582
|
+
if (target.supermodule.role === "super-parent") {
|
|
1583
|
+
await refreshSupermoduleParent(target.dir, agents, target.supermodule.submodules ?? []);
|
|
1584
|
+
refreshedLabels.push(target.label);
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
if (target.supermodule.role === "submodule-child") {
|
|
1588
|
+
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
1589
|
+
refreshedLabels.push(target.label);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
if (installedLabels.length === 0) {
|
|
1593
|
+
if (refreshedLabels.length > 0) {
|
|
1594
|
+
outro(chalk2.green(`Supermodule topology refreshed:
|
|
1595
|
+
${refreshedLabels.join("\n")}`));
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1065
1598
|
outro(chalk2.yellow("All selected agent platforms are already installed."));
|
|
1066
1599
|
return;
|
|
1067
1600
|
}
|
|
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(", ")}`));
|
|
1601
|
+
outro(chalk2.green(`Added agent platforms:
|
|
1602
|
+
${installedLabels.join("\n")}`));
|
|
1083
1603
|
}
|
|
1084
1604
|
|
|
1085
1605
|
// src/commands/clear.ts
|
|
1086
1606
|
import { readdir as readdir4, rm, writeFile } from "fs/promises";
|
|
1087
|
-
import
|
|
1088
|
-
import { cancel as
|
|
1607
|
+
import path16 from "path";
|
|
1608
|
+
import { cancel as cancel3, confirm as confirm2, outro as outro2 } from "@clack/prompts";
|
|
1089
1609
|
import chalk3 from "chalk";
|
|
1090
1610
|
var PLATFORM_TEMPLATE_DIR = {
|
|
1091
1611
|
"claude-code": "claude",
|
|
@@ -1095,13 +1615,12 @@ var PLATFORM_TEMPLATE_DIR = {
|
|
|
1095
1615
|
async function clear(opts) {
|
|
1096
1616
|
renderBanner();
|
|
1097
1617
|
const cwd = process.cwd();
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1618
|
+
const targets = await resolveClearTargets(cwd, opts);
|
|
1619
|
+
const targetPlans = await buildTargetClearPlans(targets);
|
|
1620
|
+
if (targetPlans.length === 0) {
|
|
1100
1621
|
throw new Error("No .easy-coding directory found in this project \u2014 nothing to clear.");
|
|
1101
1622
|
}
|
|
1102
|
-
|
|
1103
|
-
const plan = await buildClearPlan(cwd, agents);
|
|
1104
|
-
console.log(renderPlan(cwd, agents, plan));
|
|
1623
|
+
console.log(renderTargetPlans(targetPlans));
|
|
1105
1624
|
if (opts.dryRun) {
|
|
1106
1625
|
return;
|
|
1107
1626
|
}
|
|
@@ -1111,18 +1630,54 @@ async function clear(opts) {
|
|
|
1111
1630
|
initialValue: false
|
|
1112
1631
|
});
|
|
1113
1632
|
if (typeof ok === "symbol" || !ok) {
|
|
1114
|
-
|
|
1633
|
+
cancel3("Clear cancelled.");
|
|
1115
1634
|
return;
|
|
1116
1635
|
}
|
|
1117
1636
|
}
|
|
1118
|
-
|
|
1637
|
+
for (const targetPlan of targetPlans) {
|
|
1638
|
+
await executeClearPlan(targetPlan.plan);
|
|
1639
|
+
}
|
|
1640
|
+
await refreshParentAfterChildClear(cwd, targetPlans);
|
|
1119
1641
|
outro2(chalk3.green("Easy Coding harness removed. Run easy-coding init to reinstall."));
|
|
1120
1642
|
}
|
|
1643
|
+
async function buildTargetClearPlans(targets) {
|
|
1644
|
+
const plans = [];
|
|
1645
|
+
for (const target of targets) {
|
|
1646
|
+
const easyCodingDir = path16.join(target.dir, EASY_CODING_DIR);
|
|
1647
|
+
if (!await pathExists(easyCodingDir)) {
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const agents = await resolveInstalledAgents(target.dir);
|
|
1651
|
+
plans.push({
|
|
1652
|
+
target,
|
|
1653
|
+
agents,
|
|
1654
|
+
plan: await buildClearPlan(target.dir, agents)
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
return plans;
|
|
1658
|
+
}
|
|
1659
|
+
async function refreshParentAfterChildClear(cwd, targetPlans) {
|
|
1660
|
+
if (targetPlans.some((targetPlan) => targetPlan.target.label === ".")) {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (!await pathExists(path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const [parent] = await resolveUpgradeTargets(cwd);
|
|
1667
|
+
if (!parent || parent.label !== ".") {
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
const config = await readConfigYaml(parent.configPath);
|
|
1671
|
+
if (!Array.isArray(config.agents) || config.agents.length === 0) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
await refreshSupermoduleParent(cwd, config.agents, parent.supermodule.submodules ?? []);
|
|
1675
|
+
}
|
|
1121
1676
|
async function resolveInstalledAgents(cwd) {
|
|
1122
|
-
const
|
|
1123
|
-
if (await pathExists(
|
|
1677
|
+
const configPath2 = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
1678
|
+
if (await pathExists(configPath2)) {
|
|
1124
1679
|
try {
|
|
1125
|
-
const config = await readConfigYaml(
|
|
1680
|
+
const config = await readConfigYaml(configPath2);
|
|
1126
1681
|
const listed = Array.isArray(config.agents) ? config.agents.filter(isAgentPlatform) : [];
|
|
1127
1682
|
if (listed.length > 0) {
|
|
1128
1683
|
return listed;
|
|
@@ -1150,7 +1705,7 @@ async function hasPlatformInstall(cwd, platform) {
|
|
|
1150
1705
|
async function hasInstallMarkers(cwd, meta) {
|
|
1151
1706
|
const markers = [meta.skillsDir, meta.hooksDir, meta.agentsDir, meta.hookConfigFile];
|
|
1152
1707
|
for (const marker of markers) {
|
|
1153
|
-
if (await pathExists(
|
|
1708
|
+
if (await pathExists(path16.join(cwd, marker))) {
|
|
1154
1709
|
return true;
|
|
1155
1710
|
}
|
|
1156
1711
|
}
|
|
@@ -1206,7 +1761,7 @@ async function buildManifestClearPlan(cwd, agents, manifest) {
|
|
|
1206
1761
|
}
|
|
1207
1762
|
if (await manifestFileMatches(filePath, file.sha256)) {
|
|
1208
1763
|
plan.removeFiles.push({ filePath, expectedSha256: file.sha256 });
|
|
1209
|
-
addEmptyDirChain(plan.emptyDirs,
|
|
1764
|
+
addEmptyDirChain(plan.emptyDirs, path16.dirname(filePath), cwd);
|
|
1210
1765
|
} else {
|
|
1211
1766
|
plan.skippedModified.push(filePath);
|
|
1212
1767
|
}
|
|
@@ -1255,28 +1810,28 @@ async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills
|
|
|
1255
1810
|
);
|
|
1256
1811
|
for (const meta of metas) {
|
|
1257
1812
|
for (const name of managedSkills) {
|
|
1258
|
-
plan.remove.add(
|
|
1813
|
+
plan.remove.add(path16.join(cwd, meta.skillsDir, name));
|
|
1259
1814
|
}
|
|
1260
1815
|
for (const name of hookFileNames) {
|
|
1261
|
-
plan.remove.add(
|
|
1816
|
+
plan.remove.add(path16.join(cwd, meta.hooksDir, name));
|
|
1262
1817
|
}
|
|
1263
1818
|
for (const name of agentFileNames) {
|
|
1264
|
-
plan.remove.add(
|
|
1819
|
+
plan.remove.add(path16.join(cwd, meta.agentsDir, name));
|
|
1265
1820
|
}
|
|
1266
1821
|
addHookConfigPrune(
|
|
1267
1822
|
plan.pruneHookConfigs,
|
|
1268
1823
|
plan.pruneHookCommands,
|
|
1269
|
-
|
|
1824
|
+
path16.join(cwd, meta.hookConfigFile),
|
|
1270
1825
|
hookFileNames.map((name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`),
|
|
1271
1826
|
[]
|
|
1272
1827
|
);
|
|
1273
|
-
plan.constraints.add(
|
|
1828
|
+
plan.constraints.add(path16.join(cwd, meta.mainConstraint));
|
|
1274
1829
|
if (platform === "codex") {
|
|
1275
|
-
plan.remove.add(
|
|
1830
|
+
plan.remove.add(path16.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
|
|
1276
1831
|
}
|
|
1277
|
-
plan.emptyDirs.add(
|
|
1278
|
-
plan.emptyDirs.add(
|
|
1279
|
-
plan.emptyDirs.add(
|
|
1832
|
+
plan.emptyDirs.add(path16.join(cwd, meta.skillsDir));
|
|
1833
|
+
plan.emptyDirs.add(path16.join(cwd, meta.hooksDir));
|
|
1834
|
+
plan.emptyDirs.add(path16.join(cwd, meta.agentsDir));
|
|
1280
1835
|
}
|
|
1281
1836
|
}
|
|
1282
1837
|
async function resolveManifestUncoveredQoderMetas(cwd, manifest) {
|
|
@@ -1380,27 +1935,27 @@ function addRuntimeClearEntries(plan, cwd) {
|
|
|
1380
1935
|
plan.remove = [
|
|
1381
1936
|
.../* @__PURE__ */ new Set([
|
|
1382
1937
|
...plan.remove,
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1938
|
+
path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
|
|
1939
|
+
path16.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
|
|
1940
|
+
path16.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
|
|
1941
|
+
path16.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
|
|
1387
1942
|
])
|
|
1388
1943
|
];
|
|
1389
1944
|
}
|
|
1390
1945
|
function addEmptyDirChain(emptyDirs, startDir, cwd) {
|
|
1391
1946
|
let current = startDir;
|
|
1392
|
-
while (current !== cwd &&
|
|
1947
|
+
while (current !== cwd && isInsideDirectory2(cwd, current)) {
|
|
1393
1948
|
emptyDirs.add(current);
|
|
1394
|
-
const parent =
|
|
1949
|
+
const parent = path16.dirname(current);
|
|
1395
1950
|
if (parent === current) {
|
|
1396
1951
|
break;
|
|
1397
1952
|
}
|
|
1398
1953
|
current = parent;
|
|
1399
1954
|
}
|
|
1400
1955
|
}
|
|
1401
|
-
function
|
|
1402
|
-
const relative =
|
|
1403
|
-
return Boolean(relative) && !relative.startsWith("..") && !
|
|
1956
|
+
function isInsideDirectory2(parent, child) {
|
|
1957
|
+
const relative = path16.relative(parent, child);
|
|
1958
|
+
return Boolean(relative) && !relative.startsWith("..") && !path16.isAbsolute(relative);
|
|
1404
1959
|
}
|
|
1405
1960
|
async function listSharedHookNamesForPlatform(platform) {
|
|
1406
1961
|
const names = await listFileNames(getTemplatePath("shared-hooks"));
|
|
@@ -1537,7 +2092,7 @@ function sortDirsDeepestFirst(dirs) {
|
|
|
1537
2092
|
});
|
|
1538
2093
|
}
|
|
1539
2094
|
function pathDepth(dir) {
|
|
1540
|
-
return
|
|
2095
|
+
return path16.normalize(dir).split(path16.sep).filter(Boolean).length;
|
|
1541
2096
|
}
|
|
1542
2097
|
async function listDirNames(dir) {
|
|
1543
2098
|
try {
|
|
@@ -1556,7 +2111,7 @@ async function listFileNames(dir) {
|
|
|
1556
2111
|
}
|
|
1557
2112
|
}
|
|
1558
2113
|
function renderPlan(cwd, agents, plan) {
|
|
1559
|
-
const rel = (target) =>
|
|
2114
|
+
const rel = (target) => path16.relative(cwd, target) || target;
|
|
1560
2115
|
const lines = [];
|
|
1561
2116
|
lines.push(chalk3.bold("easy-coding clear"));
|
|
1562
2117
|
lines.push(`Platforms: ${agents.length > 0 ? agents.join(", ") : "(none detected)"}`);
|
|
@@ -1594,23 +2149,33 @@ function renderPlan(cwd, agents, plan) {
|
|
|
1594
2149
|
);
|
|
1595
2150
|
return lines.join("\n");
|
|
1596
2151
|
}
|
|
2152
|
+
function renderTargetPlans(targetPlans) {
|
|
2153
|
+
if (targetPlans.length === 1) {
|
|
2154
|
+
const [{ target, agents, plan }] = targetPlans;
|
|
2155
|
+
return [`Target: ${target.label}`, renderPlan(target.dir, agents, plan)].join("\n");
|
|
2156
|
+
}
|
|
2157
|
+
return targetPlans.map(
|
|
2158
|
+
({ target, agents, plan }) => [`Target: ${target.label}`, renderPlan(target.dir, agents, plan)].join("\n")
|
|
2159
|
+
).join("\n\n");
|
|
2160
|
+
}
|
|
1597
2161
|
|
|
1598
2162
|
// src/commands/init.ts
|
|
2163
|
+
import path18 from "path";
|
|
1599
2164
|
import { note, outro as outro3 } from "@clack/prompts";
|
|
1600
2165
|
import chalk4 from "chalk";
|
|
1601
2166
|
|
|
1602
2167
|
// src/utils/install-state.ts
|
|
1603
2168
|
import { readdir as readdir5 } from "fs/promises";
|
|
1604
|
-
import
|
|
2169
|
+
import path17 from "path";
|
|
1605
2170
|
var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
|
|
1606
2171
|
async function detectEasyCodingInstallState(cwd) {
|
|
1607
|
-
const easyCodingDir =
|
|
2172
|
+
const easyCodingDir = path17.join(cwd, EASY_CODING_DIR);
|
|
1608
2173
|
if (!await pathExists(easyCodingDir)) {
|
|
1609
2174
|
return { kind: "fresh", easyCodingDir };
|
|
1610
2175
|
}
|
|
1611
|
-
const
|
|
1612
|
-
if (await pathExists(
|
|
1613
|
-
return { kind: "installed", easyCodingDir, configPath };
|
|
2176
|
+
const configPath2 = path17.join(easyCodingDir, CONFIG_FILE);
|
|
2177
|
+
if (await pathExists(configPath2)) {
|
|
2178
|
+
return { kind: "installed", easyCodingDir, configPath: configPath2 };
|
|
1614
2179
|
}
|
|
1615
2180
|
const legacyAssets = await detectLegacyAssets(easyCodingDir);
|
|
1616
2181
|
if (legacyAssets.length === 0) {
|
|
@@ -1626,17 +2191,17 @@ async function detectEasyCodingInstallState(cwd) {
|
|
|
1626
2191
|
async function detectLegacyAssets(easyCodingDir) {
|
|
1627
2192
|
const assets = [];
|
|
1628
2193
|
for (const file of LEGACY_ROOT_FILES) {
|
|
1629
|
-
if (await pathExists(
|
|
2194
|
+
if (await pathExists(path17.join(easyCodingDir, file))) {
|
|
1630
2195
|
assets.push(relativeEasyCodingPath(file));
|
|
1631
2196
|
}
|
|
1632
2197
|
}
|
|
1633
|
-
if (await pathExists(
|
|
2198
|
+
if (await pathExists(path17.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
|
|
1634
2199
|
assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
|
|
1635
2200
|
}
|
|
1636
|
-
const shortMemoryFiles = await listMarkdownFiles(
|
|
2201
|
+
const shortMemoryFiles = await listMarkdownFiles(path17.join(easyCodingDir, "memory", "short"));
|
|
1637
2202
|
assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
|
|
1638
2203
|
for (const dir of ["spec", "prototype"]) {
|
|
1639
|
-
if (await hasAnyDirectoryEntry(
|
|
2204
|
+
if (await hasAnyDirectoryEntry(path17.join(easyCodingDir, dir))) {
|
|
1640
2205
|
assets.push(relativeEasyCodingPath(dir));
|
|
1641
2206
|
}
|
|
1642
2207
|
}
|
|
@@ -1656,19 +2221,54 @@ async function hasAnyDirectoryEntry(dir) {
|
|
|
1656
2221
|
return (await readdir5(dir)).length > 0;
|
|
1657
2222
|
}
|
|
1658
2223
|
function relativeEasyCodingPath(...segments) {
|
|
1659
|
-
return
|
|
2224
|
+
return path17.posix.join(EASY_CODING_DIR, ...segments);
|
|
1660
2225
|
}
|
|
1661
2226
|
function relativeConfigPath() {
|
|
1662
|
-
return
|
|
2227
|
+
return path17.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
1663
2228
|
}
|
|
1664
2229
|
function relativeProjectInitTaskPath() {
|
|
1665
|
-
return
|
|
2230
|
+
return path17.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
|
|
1666
2231
|
}
|
|
1667
2232
|
|
|
1668
2233
|
// src/commands/init.ts
|
|
1669
2234
|
async function init(opts) {
|
|
1670
2235
|
renderBanner();
|
|
1671
2236
|
const cwd = process.cwd();
|
|
2237
|
+
const submodules = await parseGitmodules(cwd);
|
|
2238
|
+
if (submodules.length === 0) {
|
|
2239
|
+
rejectSubmoduleListWithoutGitmodules(opts);
|
|
2240
|
+
}
|
|
2241
|
+
const targets = submodules.length === 0 ? [await standaloneTarget2(cwd)] : await supermoduleTargets(cwd, opts, submodules);
|
|
2242
|
+
const installableTargets = targets.filter((target) => !target.installed);
|
|
2243
|
+
const parentTarget2 = targets.find((target) => target.label === ".");
|
|
2244
|
+
if (installableTargets.length === 0) {
|
|
2245
|
+
await refreshInstalledChildTopologies(targets);
|
|
2246
|
+
if (submodules.length > 0 && parentTarget2) {
|
|
2247
|
+
const platforms2 = await resolveInitPlatforms(cwd, opts, true);
|
|
2248
|
+
await refreshParentTopologyIfNeeded(cwd, parentTarget2, platforms2);
|
|
2249
|
+
}
|
|
2250
|
+
outro3(chalk4.yellow("All selected easy-coding harness targets are already installed."));
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
const platforms = await resolveInitPlatforms(cwd, opts, Boolean(parentTarget2?.installed));
|
|
2254
|
+
for (const target of installableTargets) {
|
|
2255
|
+
await installHarnessToDir(target.dir, platforms, target.context);
|
|
2256
|
+
}
|
|
2257
|
+
await refreshInstalledChildTopologies(targets);
|
|
2258
|
+
if (submodules.length > 0 && parentTarget2) {
|
|
2259
|
+
await refreshParentTopologyIfNeeded(cwd, parentTarget2, platforms);
|
|
2260
|
+
}
|
|
2261
|
+
const triggers = platforms.map(
|
|
2262
|
+
(platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
|
|
2263
|
+
).join("\n");
|
|
2264
|
+
note(triggers, "Next step");
|
|
2265
|
+
outro3(
|
|
2266
|
+
chalk4.green(
|
|
2267
|
+
`easy-coding harness installed in ${installableTargets.map((target) => target.label).join(", ")}. Open your agent and run ec-init.`
|
|
2268
|
+
)
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
async function standaloneTarget2(cwd) {
|
|
1672
2272
|
const installState = await detectEasyCodingInstallState(cwd);
|
|
1673
2273
|
if (installState.kind === "installed") {
|
|
1674
2274
|
throw new Error(
|
|
@@ -1680,36 +2280,121 @@ async function init(opts) {
|
|
|
1680
2280
|
".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
2281
|
);
|
|
1682
2282
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2283
|
+
return {
|
|
2284
|
+
dir: cwd,
|
|
2285
|
+
label: ".",
|
|
2286
|
+
context: contextFromState("standalone", installState),
|
|
2287
|
+
installed: false
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
async function supermoduleTargets(cwd, opts, submodules) {
|
|
2291
|
+
const { installable, parentSubmodulePaths } = await resolveInitSubmoduleSelection(cwd, opts);
|
|
2292
|
+
const skipped = submodules.filter(
|
|
2293
|
+
(entry) => !installable.some((installableEntry) => installableEntry.path === entry.path)
|
|
2294
|
+
);
|
|
2295
|
+
if (skipped.length > 0) {
|
|
2296
|
+
note(
|
|
2297
|
+
skipped.map((entry) => `${entry.path} (${entry.name})`).join("\n"),
|
|
2298
|
+
"Skipped unchecked-out submodules"
|
|
2299
|
+
);
|
|
1687
2300
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
2301
|
+
const targets = [
|
|
2302
|
+
await targetFromState(cwd, ".", "super-parent", {
|
|
2303
|
+
submodulePaths: parentSubmodulePaths
|
|
2304
|
+
})
|
|
2305
|
+
];
|
|
2306
|
+
const targetSubmodulePaths = opts.submodules === false ? /* @__PURE__ */ new Set() : new Set(parentSubmodulePaths);
|
|
2307
|
+
for (const entry of installable) {
|
|
2308
|
+
if (!targetSubmodulePaths.has(entry.path)) {
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
const dir = path18.join(cwd, entry.path);
|
|
2312
|
+
targets.push(
|
|
2313
|
+
await targetFromState(dir, entry.path, "submodule-child", {
|
|
2314
|
+
parent: toPosixRelative2(dir, cwd)
|
|
2315
|
+
})
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
return targets;
|
|
2319
|
+
}
|
|
2320
|
+
async function targetFromState(targetDir, label, role, extraContext = {}) {
|
|
2321
|
+
const installState = await detectEasyCodingInstallState(targetDir);
|
|
2322
|
+
if (installState.kind === "unknown") {
|
|
2323
|
+
throw new Error(
|
|
2324
|
+
`${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.`
|
|
2325
|
+
);
|
|
2326
|
+
}
|
|
2327
|
+
if (installState.kind === "installed") {
|
|
2328
|
+
return {
|
|
2329
|
+
dir: targetDir,
|
|
2330
|
+
label,
|
|
2331
|
+
installed: true,
|
|
2332
|
+
context: {
|
|
2333
|
+
role,
|
|
2334
|
+
initSource: "fresh",
|
|
2335
|
+
...extraContext
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
return {
|
|
2340
|
+
dir: targetDir,
|
|
2341
|
+
label,
|
|
2342
|
+
installed: false,
|
|
2343
|
+
context: {
|
|
2344
|
+
...contextFromState(role, installState),
|
|
2345
|
+
...extraContext
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
function contextFromState(role, installState) {
|
|
2350
|
+
return {
|
|
2351
|
+
role,
|
|
1695
2352
|
initSource: installState.kind === "legacy" ? "legacy-easy-coding" : "fresh",
|
|
1696
2353
|
legacyAssets: installState.kind === "legacy" ? installState.legacyAssets : void 0,
|
|
1697
2354
|
legacyMissingHarnessFiles: installState.kind === "legacy" ? installState.missingHarnessFiles : void 0
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
).join("
|
|
1703
|
-
|
|
1704
|
-
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
function toPosixRelative2(from, to) {
|
|
2358
|
+
const relative = path18.relative(from, to);
|
|
2359
|
+
return relative ? relative.split(path18.sep).join("/") : ".";
|
|
2360
|
+
}
|
|
2361
|
+
async function resolveInitPlatforms(cwd, opts, parentInstalled) {
|
|
2362
|
+
if (opts.agent || !parentInstalled) {
|
|
2363
|
+
return resolvePlatforms(opts, ["claude-code"]);
|
|
2364
|
+
}
|
|
2365
|
+
const config = await readConfigYaml(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE));
|
|
2366
|
+
if (Array.isArray(config.agents) && config.agents.length > 0) {
|
|
2367
|
+
return config.agents;
|
|
2368
|
+
}
|
|
2369
|
+
return resolvePlatforms(opts, ["claude-code"]);
|
|
2370
|
+
}
|
|
2371
|
+
async function refreshParentTopologyIfNeeded(cwd, parentTarget2, installPlatforms) {
|
|
2372
|
+
if (!await pathExists(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE))) {
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
const config = parentTarget2.installed ? await readConfigYaml(path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE)) : { agents: installPlatforms };
|
|
2376
|
+
const platforms = Array.isArray(config.agents) && config.agents.length > 0 ? config.agents : installPlatforms;
|
|
2377
|
+
await refreshSupermoduleParent(cwd, platforms, parentTarget2.context.submodulePaths ?? []);
|
|
2378
|
+
}
|
|
2379
|
+
async function refreshInstalledChildTopologies(targets) {
|
|
2380
|
+
for (const target of targets) {
|
|
2381
|
+
if (!target.installed || target.context.role !== "submodule-child") {
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
const configPath2 = path18.join(target.dir, EASY_CODING_DIR, CONFIG_FILE);
|
|
2385
|
+
if (!await pathExists(configPath2)) {
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
await updateSupermoduleConfig(configPath2, supermoduleConfigFromContext(target.context));
|
|
2389
|
+
}
|
|
1705
2390
|
}
|
|
1706
2391
|
|
|
1707
2392
|
// src/commands/status.ts
|
|
1708
|
-
import
|
|
2393
|
+
import path21 from "path";
|
|
1709
2394
|
import chalk5 from "chalk";
|
|
1710
2395
|
|
|
1711
2396
|
// src/utils/compare-versions.ts
|
|
1712
|
-
import
|
|
2397
|
+
import path19 from "path";
|
|
1713
2398
|
function normalize(version) {
|
|
1714
2399
|
const [core] = String(version ?? "").split("-");
|
|
1715
2400
|
return core.split(".").map((part) => {
|
|
@@ -1733,13 +2418,13 @@ function isVersionBehind(installed, current = VERSION) {
|
|
|
1733
2418
|
return compareVersions(installed, current) === -1;
|
|
1734
2419
|
}
|
|
1735
2420
|
async function checkForUpgrade(cwd) {
|
|
1736
|
-
const
|
|
1737
|
-
if (!await pathExists(
|
|
2421
|
+
const configPath2 = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2422
|
+
if (!await pathExists(configPath2)) {
|
|
1738
2423
|
return;
|
|
1739
2424
|
}
|
|
1740
2425
|
let config;
|
|
1741
2426
|
try {
|
|
1742
|
-
config = await readConfigYaml(
|
|
2427
|
+
config = await readConfigYaml(configPath2);
|
|
1743
2428
|
} catch {
|
|
1744
2429
|
return;
|
|
1745
2430
|
}
|
|
@@ -1754,14 +2439,14 @@ async function checkForUpgrade(cwd) {
|
|
|
1754
2439
|
|
|
1755
2440
|
// src/utils/session.ts
|
|
1756
2441
|
import { readdir as readdir6 } from "fs/promises";
|
|
1757
|
-
import
|
|
2442
|
+
import path20 from "path";
|
|
1758
2443
|
var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
1759
2444
|
function getSessionDir(cwd) {
|
|
1760
|
-
return
|
|
2445
|
+
return path20.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
1761
2446
|
}
|
|
1762
2447
|
function getSessionFilePath(cwd, ppid) {
|
|
1763
2448
|
const pid = ppid ?? process.ppid;
|
|
1764
|
-
return
|
|
2449
|
+
return path20.join(getSessionDir(cwd), `${pid}.json`);
|
|
1765
2450
|
}
|
|
1766
2451
|
async function readSessionFile(cwd, ppid) {
|
|
1767
2452
|
const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
|
|
@@ -1775,11 +2460,11 @@ async function readSessionFile(cwd, ppid) {
|
|
|
1775
2460
|
async function status() {
|
|
1776
2461
|
renderBanner();
|
|
1777
2462
|
const cwd = process.cwd();
|
|
1778
|
-
const
|
|
1779
|
-
if (!await pathExists(
|
|
2463
|
+
const configPath2 = path21.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
2464
|
+
if (!await pathExists(configPath2)) {
|
|
1780
2465
|
throw new Error("No easy-coding harness found in this project.");
|
|
1781
2466
|
}
|
|
1782
|
-
const config = await readConfigYaml(
|
|
2467
|
+
const config = await readConfigYaml(configPath2);
|
|
1783
2468
|
const tasks = await listTasks(cwd);
|
|
1784
2469
|
const taskCounts = summarizeTaskStatuses(tasks);
|
|
1785
2470
|
const activeTasks = tasks.filter((item) => isActiveTask(item.task));
|
|
@@ -1829,7 +2514,7 @@ async function status() {
|
|
|
1829
2514
|
|
|
1830
2515
|
// src/commands/update.ts
|
|
1831
2516
|
import { execFileSync } from "child_process";
|
|
1832
|
-
import { cancel as
|
|
2517
|
+
import { cancel as cancel4, confirm as confirm3, outro as outro4 } from "@clack/prompts";
|
|
1833
2518
|
import chalk6 from "chalk";
|
|
1834
2519
|
async function update(opts) {
|
|
1835
2520
|
renderBanner();
|
|
@@ -1848,7 +2533,7 @@ async function update(opts) {
|
|
|
1848
2533
|
initialValue: true
|
|
1849
2534
|
});
|
|
1850
2535
|
if (typeof confirmed === "symbol" || !confirmed) {
|
|
1851
|
-
|
|
2536
|
+
cancel4("Update cancelled.");
|
|
1852
2537
|
return;
|
|
1853
2538
|
}
|
|
1854
2539
|
}
|
|
@@ -1858,37 +2543,35 @@ async function update(opts) {
|
|
|
1858
2543
|
}
|
|
1859
2544
|
|
|
1860
2545
|
// src/commands/upgrade.ts
|
|
1861
|
-
import
|
|
1862
|
-
import { cancel as cancel4, confirm as confirm4, outro as outro5 } from "@clack/prompts";
|
|
2546
|
+
import { cancel as cancel5, confirm as confirm4, outro as outro5 } from "@clack/prompts";
|
|
1863
2547
|
import chalk7 from "chalk";
|
|
1864
2548
|
async function upgrade(opts) {
|
|
1865
2549
|
renderBanner();
|
|
1866
2550
|
const cwd = process.cwd();
|
|
1867
|
-
const
|
|
1868
|
-
if (!await pathExists(configPath)) {
|
|
2551
|
+
const targets = await resolveUpgradeTargets(cwd);
|
|
2552
|
+
if (!await pathExists(targets[0].configPath)) {
|
|
1869
2553
|
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
1870
2554
|
}
|
|
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) {
|
|
2555
|
+
const pending = await resolvePendingUpgradeTargets(targets);
|
|
2556
|
+
const parentTopologyRefresh = await resolveParentTopologyRefresh(targets, pending);
|
|
2557
|
+
const childTopologyRefreshes = await resolveChildTopologyRefreshes(targets, pending);
|
|
2558
|
+
if (pending.length === 0 && !parentTopologyRefresh && childTopologyRefreshes.length === 0) {
|
|
1881
2559
|
outro5(chalk7.green(`easy-coding harness is already up to date (${VERSION}).`));
|
|
1882
2560
|
return;
|
|
1883
2561
|
}
|
|
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
2562
|
const summary = [
|
|
1890
|
-
|
|
1891
|
-
|
|
2563
|
+
"Upgrade targets:",
|
|
2564
|
+
...pending.length > 0 ? pending.map(
|
|
2565
|
+
({ target, config }) => `- ${target.label}: ${String(config.harness_version ?? "unknown")} -> ${VERSION}; agents: ${config.agents.join(", ")}`
|
|
2566
|
+
) : ["- (none)"],
|
|
2567
|
+
...parentTopologyRefresh ? [
|
|
2568
|
+
"Supermodule topology refresh:",
|
|
2569
|
+
`- ${parentTopologyRefresh.target.label}: submodules: ${parentTopologyRefresh.submodulePaths.length > 0 ? parentTopologyRefresh.submodulePaths.join(", ") : "(none)"}`
|
|
2570
|
+
] : [],
|
|
2571
|
+
...childTopologyRefreshes.length > 0 ? [
|
|
2572
|
+
parentTopologyRefresh ? "" : "Supermodule topology refresh:",
|
|
2573
|
+
...childTopologyRefreshes.map(({ target }) => `- ${target.label}: role: submodule-child`)
|
|
2574
|
+
].filter(Boolean) : [],
|
|
1892
2575
|
"Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
|
|
1893
2576
|
"Will update project-init task to recommend ec-init re-run for version adaptation.",
|
|
1894
2577
|
"Will not touch memory, state, spec, or project knowledge files."
|
|
@@ -1903,24 +2586,119 @@ async function upgrade(opts) {
|
|
|
1903
2586
|
initialValue: true
|
|
1904
2587
|
});
|
|
1905
2588
|
if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
|
|
1906
|
-
|
|
2589
|
+
cancel5("Upgrade cancelled.");
|
|
1907
2590
|
return;
|
|
1908
2591
|
}
|
|
1909
2592
|
}
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
2593
|
+
for (const { target, config } of pending) {
|
|
2594
|
+
const artifacts = await configurePlatformsForDir(
|
|
2595
|
+
target.dir,
|
|
2596
|
+
config.agents,
|
|
2597
|
+
target.boundary
|
|
2598
|
+
);
|
|
2599
|
+
await writeRuntimeScaffold(target.dir, config.agents, {
|
|
2600
|
+
supermodule: target.supermodule
|
|
2601
|
+
});
|
|
2602
|
+
await writeInstallManifest(target.dir, {
|
|
2603
|
+
harnessVersion: VERSION,
|
|
2604
|
+
agents: config.agents,
|
|
2605
|
+
artifacts
|
|
2606
|
+
});
|
|
2607
|
+
await ensureEasyCodingSessionsIgnored(target.dir);
|
|
2608
|
+
await updateHarnessVersion(target.configPath, VERSION);
|
|
2609
|
+
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
2610
|
+
await setPendingInitSince(target.dir, VERSION);
|
|
2611
|
+
}
|
|
2612
|
+
if (parentTopologyRefresh) {
|
|
2613
|
+
await refreshSupermoduleParent(
|
|
2614
|
+
parentTopologyRefresh.target.dir,
|
|
2615
|
+
parentTopologyRefresh.agents,
|
|
2616
|
+
parentTopologyRefresh.submodulePaths
|
|
2617
|
+
);
|
|
1913
2618
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2619
|
+
for (const { target } of childTopologyRefreshes) {
|
|
2620
|
+
await updateSupermoduleConfig(target.configPath, target.supermodule);
|
|
2621
|
+
}
|
|
2622
|
+
outro5(
|
|
2623
|
+
chalk7.green(
|
|
2624
|
+
pending.length > 0 ? `easy-coding harness upgraded to ${VERSION}.` : "easy-coding supermodule topology refreshed."
|
|
2625
|
+
)
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
async function resolvePendingUpgradeTargets(targets) {
|
|
2629
|
+
const pending = [];
|
|
2630
|
+
for (const target of targets) {
|
|
2631
|
+
if (!await pathExists(target.configPath)) {
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
const config = await readConfigYaml(target.configPath);
|
|
2635
|
+
const installedVersion = String(config.harness_version ?? "");
|
|
2636
|
+
const hasAgents = Array.isArray(config.agents) && config.agents.length > 0;
|
|
2637
|
+
if (!installedVersion || !hasAgents) {
|
|
2638
|
+
throw new Error(
|
|
2639
|
+
`${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.`
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
const relation = compareVersions(installedVersion, VERSION);
|
|
2643
|
+
if (relation === 1) {
|
|
2644
|
+
throw new Error(
|
|
2645
|
+
`${target.label} harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
if (relation === -1) {
|
|
2649
|
+
pending.push({ target, config });
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
return pending;
|
|
2653
|
+
}
|
|
2654
|
+
async function resolveParentTopologyRefresh(targets, pending) {
|
|
2655
|
+
const parent = targets.find(
|
|
2656
|
+
(target) => target.label === "." && target.supermodule.role === "super-parent"
|
|
2657
|
+
);
|
|
2658
|
+
if (!parent || pending.some(({ target }) => target.label === ".")) {
|
|
2659
|
+
return null;
|
|
2660
|
+
}
|
|
2661
|
+
if (!await pathExists(parent.configPath)) {
|
|
2662
|
+
return null;
|
|
2663
|
+
}
|
|
2664
|
+
const config = await readConfigYaml(parent.configPath);
|
|
2665
|
+
if (!Array.isArray(config.agents) || config.agents.length === 0) {
|
|
2666
|
+
return null;
|
|
2667
|
+
}
|
|
2668
|
+
const submodulePaths = parent.supermodule.submodules ?? [];
|
|
2669
|
+
const configuredSubmodules = Array.isArray(config.supermodule?.submodules) ? config.supermodule.submodules : [];
|
|
2670
|
+
const needsRefresh = config.supermodule?.role !== "super-parent" || !sameStringList(configuredSubmodules, submodulePaths);
|
|
2671
|
+
if (!needsRefresh) {
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2674
|
+
return {
|
|
2675
|
+
target: parent,
|
|
1917
2676
|
agents: config.agents,
|
|
1918
|
-
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2677
|
+
submodulePaths
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
async function resolveChildTopologyRefreshes(targets, pending) {
|
|
2681
|
+
const pendingLabels = new Set(pending.map(({ target }) => target.label));
|
|
2682
|
+
const refreshes = [];
|
|
2683
|
+
for (const target of targets) {
|
|
2684
|
+
if (target.supermodule.role !== "submodule-child" || pendingLabels.has(target.label) || !await pathExists(target.configPath)) {
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
const config = await readConfigYaml(target.configPath);
|
|
2688
|
+
if (config.supermodule?.role === "submodule-child" && config.supermodule.parent === target.supermodule.parent) {
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
refreshes.push({ target });
|
|
2692
|
+
}
|
|
2693
|
+
return refreshes;
|
|
2694
|
+
}
|
|
2695
|
+
function sameStringList(left, right) {
|
|
2696
|
+
if (left.length !== right.length) {
|
|
2697
|
+
return false;
|
|
2698
|
+
}
|
|
2699
|
+
const sortedLeft = [...left].sort();
|
|
2700
|
+
const sortedRight = [...right].sort();
|
|
2701
|
+
return sortedLeft.every((value, index) => value === sortedRight[index]);
|
|
1924
2702
|
}
|
|
1925
2703
|
|
|
1926
2704
|
// src/cli.ts
|
|
@@ -1940,11 +2718,14 @@ function withErrorHandling(fn) {
|
|
|
1940
2718
|
await checkForUpgrade(process.cwd());
|
|
1941
2719
|
var program = new Command();
|
|
1942
2720
|
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
|
-
|
|
2721
|
+
program.command("init").description("Initialize easy-coding harness in current project").option("--agent <list>", "Comma-separated platforms: claude-code,codex,qoder").option(
|
|
2722
|
+
"--submodules <list>",
|
|
2723
|
+
"Comma-separated checked-out submodule paths or names to initialize"
|
|
2724
|
+
).option("--no-submodules", "Initialize only the current directory when .gitmodules exists").option("-y, --yes", "Skip prompts, use defaults").action(withErrorHandling(init));
|
|
2725
|
+
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
2726
|
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
2727
|
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
2728
|
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));
|
|
2729
|
+
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
2730
|
program.parse();
|
|
1950
2731
|
//# sourceMappingURL=cli.js.map
|