easy-coding-harness 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/README.md +10 -65
- package/dist/cli.js +1227 -299
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
- package/templates/claude/agents/ec-fixer.md +36 -0
- package/templates/codex/agents/ec-fixer.toml +25 -0
- package/templates/common/bundled-skills/ec-init/SKILL.md +174 -0
- package/templates/common/bundled-skills/ec-init/references/memory-migration.md +87 -0
- package/templates/common/bundled-skills/ec-meta/SKILL.md +5 -2
- package/templates/common/bundled-skills/ec-meta/references/customize-local/README.md +2 -1
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +14 -11
- package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +8 -4
- package/templates/common/skills/ec-analysis/SKILL.md +104 -126
- package/templates/common/skills/ec-brainstorming/SKILL.md +1 -1
- package/templates/common/skills/ec-git/SKILL.md +10 -10
- package/templates/common/skills/ec-implementing/SKILL.md +26 -3
- package/templates/common/skills/ec-memory/SKILL.md +56 -19
- package/templates/common/skills/ec-reviewing/SKILL.md +39 -10
- package/templates/common/skills/ec-task-close/SKILL.md +4 -3
- package/templates/common/skills/ec-task-management/SKILL.md +9 -24
- package/templates/common/skills/ec-verification/SKILL.md +22 -11
- package/templates/common/skills/ec-workflow/SKILL.md +51 -21
- package/templates/main-constraint/AGENTS.md.tpl +16 -3
- package/templates/main-constraint/CLAUDE.md.tpl +16 -3
- package/templates/qoder/agents/ec-fixer.md +36 -0
- package/templates/runtime/memory/SHORT_MEMORY_TEMPLATE.md +75 -0
- package/templates/runtime/memory/long/BUSINESS.md +42 -9
- package/templates/runtime/memory/long/MEMORY.md +37 -2
- package/templates/runtime/memory/long/TECHNICAL.md +45 -1
- package/templates/runtime/templates/dev-spec-skeleton.md +80 -0
- package/templates/shared-hooks/easy_coding_state.py +516 -0
- package/templates/shared-hooks/easy_coding_status.py +77 -38
- package/templates/shared-hooks/inject-subagent-context.py +4 -11
- package/templates/shared-hooks/inject-workflow-state.py +5 -14
- package/templates/shared-hooks/session-start.py +90 -16
- package/templates/common/skills/ec-init/SKILL.md +0 -96
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk7 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/add-agent.ts
|
|
8
|
-
import
|
|
8
|
+
import path13 from "path";
|
|
9
9
|
import { outro } from "@clack/prompts";
|
|
10
10
|
import chalk2 from "chalk";
|
|
11
11
|
|
|
12
12
|
// src/configurators/claude.ts
|
|
13
|
-
import
|
|
13
|
+
import path5 from "path";
|
|
14
14
|
|
|
15
15
|
// src/types/platform.ts
|
|
16
16
|
var pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
@@ -31,7 +31,7 @@ var PLATFORM_META = {
|
|
|
31
31
|
sub_agent_dispatch: "Agent tool",
|
|
32
32
|
platform_spawn_instruction: 'Use the Agent tool with run_in_background when useful; use isolation: "worktree" for parallel file edits.',
|
|
33
33
|
skill_trigger: "/",
|
|
34
|
-
workflow_state_path: ".easy-coding/
|
|
34
|
+
workflow_state_path: ".easy-coding/sessions/",
|
|
35
35
|
main_constraint_file: "CLAUDE.md",
|
|
36
36
|
python_cmd: pythonCmd,
|
|
37
37
|
platform_config_dir: ".claude"
|
|
@@ -53,7 +53,7 @@ var PLATFORM_META = {
|
|
|
53
53
|
sub_agent_dispatch: "Codex sub-agent dispatch",
|
|
54
54
|
platform_spawn_instruction: "Use Codex sub-agent delegation where available; pass the full task card in the prompt.",
|
|
55
55
|
skill_trigger: "$",
|
|
56
|
-
workflow_state_path: ".easy-coding/
|
|
56
|
+
workflow_state_path: ".easy-coding/sessions/",
|
|
57
57
|
main_constraint_file: "AGENTS.md",
|
|
58
58
|
python_cmd: pythonCmd,
|
|
59
59
|
platform_config_dir: ".codex"
|
|
@@ -76,7 +76,7 @@ var PLATFORM_META = {
|
|
|
76
76
|
sub_agent_dispatch: "Agent tool",
|
|
77
77
|
platform_spawn_instruction: "Use the Agent tool with worktree isolation for parallel file edits.",
|
|
78
78
|
skill_trigger: "/",
|
|
79
|
-
workflow_state_path: ".easy-coding/
|
|
79
|
+
workflow_state_path: ".easy-coding/sessions/",
|
|
80
80
|
main_constraint_file: "AGENTS.md",
|
|
81
81
|
python_cmd: pythonCmd,
|
|
82
82
|
platform_config_dir: ".qoder"
|
|
@@ -88,21 +88,24 @@ function isAgentPlatform(value) {
|
|
|
88
88
|
return Object.hasOwn(PLATFORM_META, value);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// src/
|
|
92
|
-
import {
|
|
93
|
-
import
|
|
91
|
+
// src/utils/install-manifest.ts
|
|
92
|
+
import { createHash } from "crypto";
|
|
93
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
94
|
+
import path2 from "path";
|
|
94
95
|
|
|
95
96
|
// src/constants/paths.ts
|
|
96
97
|
var EASY_CODING_DIR = ".easy-coding";
|
|
97
98
|
var CONFIG_FILE = "config.yaml";
|
|
98
|
-
var
|
|
99
|
+
var INSTALL_MANIFEST_FILE = "install-manifest.json";
|
|
100
|
+
var SESSIONS_DIR = "sessions";
|
|
99
101
|
var TASKS_DIR = "tasks";
|
|
100
102
|
var PROJECT_INIT_TASK_ID = "project-init";
|
|
101
103
|
var MEMORY_DIR = "memory";
|
|
102
104
|
var SPEC_DIR = "spec";
|
|
103
105
|
var MAIN_SPEC_DIR = "main";
|
|
104
106
|
var DEV_SPEC_DIR = "dev";
|
|
105
|
-
var
|
|
107
|
+
var TEMPLATES_DIR = "templates";
|
|
108
|
+
var SESSIONS_GITIGNORE_ENTRY = ".easy-coding/sessions/";
|
|
106
109
|
var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
|
|
107
110
|
var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness generated \u2550\u2550\u2550 -->";
|
|
108
111
|
|
|
@@ -143,6 +146,196 @@ async function isDirectory(filePath) {
|
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
148
|
|
|
149
|
+
// src/utils/install-manifest.ts
|
|
150
|
+
function fileArtifact(filePath, kind, platform) {
|
|
151
|
+
return { type: "file", kind, filePath, platform };
|
|
152
|
+
}
|
|
153
|
+
function constraintRegionArtifact(filePath, platform) {
|
|
154
|
+
return { type: "constraint-region", filePath, platform };
|
|
155
|
+
}
|
|
156
|
+
async function hookRegistrationArtifacts(configPath, platform) {
|
|
157
|
+
const content = await readTextIfExists(configPath);
|
|
158
|
+
if (content === null) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(content);
|
|
164
|
+
} catch {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
return collectHookCommands(parsed.hooks).map((command) => ({
|
|
168
|
+
type: "hook-registration",
|
|
169
|
+
configPath,
|
|
170
|
+
command,
|
|
171
|
+
hookPath: extractHookPathFromCommand(command),
|
|
172
|
+
platform
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
async function writeInstallManifest(cwd, params) {
|
|
176
|
+
const existing = params.mode === "merge" ? await readInstallManifest(cwd) : null;
|
|
177
|
+
const files = /* @__PURE__ */ new Map();
|
|
178
|
+
const hookRegistrations = /* @__PURE__ */ new Map();
|
|
179
|
+
const constraintRegions = /* @__PURE__ */ new Map();
|
|
180
|
+
if (existing) {
|
|
181
|
+
for (const file of existing.files) {
|
|
182
|
+
files.set(file.path, file);
|
|
183
|
+
}
|
|
184
|
+
for (const registration of existing.hook_registrations) {
|
|
185
|
+
hookRegistrations.set(
|
|
186
|
+
`${registration.config_path}\0${normalizeCommand(registration.command)}`,
|
|
187
|
+
registration
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
for (const region of existing.constraint_regions) {
|
|
191
|
+
constraintRegions.set(region.path, region);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const artifact of params.artifacts) {
|
|
195
|
+
if (artifact.type === "file") {
|
|
196
|
+
if (!await pathExists(artifact.filePath)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const relPath2 = toProjectPath(cwd, artifact.filePath);
|
|
200
|
+
files.set(relPath2, {
|
|
201
|
+
path: relPath2,
|
|
202
|
+
kind: artifact.kind,
|
|
203
|
+
platform: artifact.platform,
|
|
204
|
+
sha256: await sha256File(artifact.filePath)
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (artifact.type === "hook-registration") {
|
|
209
|
+
const configPath = toProjectPath(cwd, artifact.configPath);
|
|
210
|
+
const registration = {
|
|
211
|
+
config_path: configPath,
|
|
212
|
+
command: artifact.command,
|
|
213
|
+
hook_path: artifact.hookPath,
|
|
214
|
+
platform: artifact.platform
|
|
215
|
+
};
|
|
216
|
+
hookRegistrations.set(`${configPath}\0${normalizeCommand(artifact.command)}`, registration);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const relPath = toProjectPath(cwd, artifact.filePath);
|
|
220
|
+
constraintRegions.set(relPath, {
|
|
221
|
+
path: relPath,
|
|
222
|
+
platform: artifact.platform
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const manifest = {
|
|
226
|
+
schema_version: 1,
|
|
227
|
+
harness_version: params.harnessVersion,
|
|
228
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
229
|
+
agents: [.../* @__PURE__ */ new Set([...existing?.agents ?? [], ...params.agents])],
|
|
230
|
+
files: [...files.values()].sort(byPath),
|
|
231
|
+
hook_registrations: [...hookRegistrations.values()].sort(
|
|
232
|
+
(a, b) => `${a.config_path}\0${a.command}`.localeCompare(`${b.config_path}\0${b.command}`)
|
|
233
|
+
),
|
|
234
|
+
constraint_regions: [...constraintRegions.values()].sort(byPath)
|
|
235
|
+
};
|
|
236
|
+
await writeTextFile(
|
|
237
|
+
path2.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE),
|
|
238
|
+
JSON.stringify(manifest, null, 2)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
async function readInstallManifest(cwd) {
|
|
242
|
+
const manifestPath2 = path2.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE);
|
|
243
|
+
const content = await readTextIfExists(manifestPath2);
|
|
244
|
+
if (content === null) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const parsed = JSON.parse(content);
|
|
249
|
+
if (parsed.schema_version !== 1 || !Array.isArray(parsed.files) || !Array.isArray(parsed.hook_registrations) || !Array.isArray(parsed.constraint_regions)) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const agents = Array.isArray(parsed.agents) ? parsed.agents.filter((agent) => isAgentPlatform(agent)) : [];
|
|
253
|
+
return { ...parsed, agents };
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function manifestFileMatches(filePath, sha256) {
|
|
259
|
+
if (!await pathExists(filePath)) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return await sha256File(filePath) === sha256;
|
|
263
|
+
}
|
|
264
|
+
function manifestPath(cwd, projectPath) {
|
|
265
|
+
return resolveProjectPath(cwd, projectPath);
|
|
266
|
+
}
|
|
267
|
+
function toProjectPath(cwd, filePath) {
|
|
268
|
+
const root = path2.resolve(cwd);
|
|
269
|
+
const resolved = path2.resolve(filePath);
|
|
270
|
+
assertPathInsideProject(root, resolved, filePath);
|
|
271
|
+
return path2.relative(root, resolved).split(path2.sep).join("/");
|
|
272
|
+
}
|
|
273
|
+
function normalizeCommand(command) {
|
|
274
|
+
return command.replace(/\\/g, "/").trim().replace(/\s+/g, " ");
|
|
275
|
+
}
|
|
276
|
+
async function sha256File(filePath) {
|
|
277
|
+
const content = await readFile2(filePath);
|
|
278
|
+
return createHash("sha256").update(content).digest("hex");
|
|
279
|
+
}
|
|
280
|
+
function resolveProjectPath(cwd, projectPath) {
|
|
281
|
+
const normalized = projectPath.replace(/\\/g, "/");
|
|
282
|
+
const parts = normalized.split("/");
|
|
283
|
+
if (normalized.trim() === "" || path2.isAbsolute(projectPath) || path2.posix.isAbsolute(normalized) || path2.win32.isAbsolute(projectPath) || /^[A-Za-z]:/.test(projectPath) || parts.some((part) => part === "..")) {
|
|
284
|
+
throw new Error(`Unsafe install manifest path: ${projectPath}`);
|
|
285
|
+
}
|
|
286
|
+
const root = path2.resolve(cwd);
|
|
287
|
+
const resolved = path2.resolve(root, normalized);
|
|
288
|
+
assertPathInsideProject(root, resolved, projectPath);
|
|
289
|
+
return resolved;
|
|
290
|
+
}
|
|
291
|
+
function assertPathInsideProject(root, resolvedPath, sourcePath) {
|
|
292
|
+
const relative = path2.relative(root, resolvedPath);
|
|
293
|
+
if (!relative || relative.startsWith("..") || path2.isAbsolute(relative)) {
|
|
294
|
+
throw new Error(`Unsafe install manifest path: ${sourcePath}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function collectHookCommands(hooks) {
|
|
298
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
const commands = [];
|
|
302
|
+
for (const value of Object.values(hooks)) {
|
|
303
|
+
if (!Array.isArray(value)) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
for (const group of value) {
|
|
307
|
+
if (!group || typeof group !== "object") {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const hookItems = group.hooks;
|
|
311
|
+
if (!Array.isArray(hookItems)) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
for (const hook of hookItems) {
|
|
315
|
+
if (!hook || typeof hook !== "object") {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const command = hook.command;
|
|
319
|
+
if (typeof command === "string") {
|
|
320
|
+
commands.push(command);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return commands;
|
|
326
|
+
}
|
|
327
|
+
function extractHookPathFromCommand(command) {
|
|
328
|
+
const match = normalizeCommand(command).match(/(?:^|\s)(\S+\/hooks\/\S+\.py)(?:\s|$)/);
|
|
329
|
+
return match?.[1] ?? null;
|
|
330
|
+
}
|
|
331
|
+
function byPath(a, b) {
|
|
332
|
+
return a.path.localeCompare(b.path);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/configurators/shared.ts
|
|
336
|
+
import { readdir, stat as stat2 } from "fs/promises";
|
|
337
|
+
import path4 from "path";
|
|
338
|
+
|
|
146
339
|
// src/utils/marked-region.ts
|
|
147
340
|
var MarkedRegionError = class extends Error {
|
|
148
341
|
constructor(message) {
|
|
@@ -170,18 +363,27 @@ ${existing.trimStart()}`.trimEnd();
|
|
|
170
363
|
}
|
|
171
364
|
return existing.replace(currentRegion, generatedRegion.trim());
|
|
172
365
|
}
|
|
366
|
+
function removeMarkedRegion(content) {
|
|
367
|
+
const region = extractMarkedRegion(content);
|
|
368
|
+
if (!region) {
|
|
369
|
+
return content;
|
|
370
|
+
}
|
|
371
|
+
const remaining = content.replace(region, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
372
|
+
return remaining ? `${remaining}
|
|
373
|
+
` : "";
|
|
374
|
+
}
|
|
173
375
|
|
|
174
376
|
// src/utils/template-paths.ts
|
|
175
377
|
import { existsSync } from "fs";
|
|
176
|
-
import
|
|
378
|
+
import path3 from "path";
|
|
177
379
|
import { fileURLToPath } from "url";
|
|
178
380
|
function getTemplateRoot() {
|
|
179
|
-
const here =
|
|
381
|
+
const here = path3.dirname(fileURLToPath(import.meta.url));
|
|
180
382
|
const candidates = [
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
383
|
+
path3.resolve(here, "../templates"),
|
|
384
|
+
path3.resolve(here, "../../templates"),
|
|
385
|
+
path3.resolve(process.cwd(), "src/templates"),
|
|
386
|
+
path3.resolve(process.cwd(), "templates")
|
|
185
387
|
];
|
|
186
388
|
const found = candidates.find((candidate) => existsSync(candidate));
|
|
187
389
|
if (!found) {
|
|
@@ -190,7 +392,7 @@ function getTemplateRoot() {
|
|
|
190
392
|
return found;
|
|
191
393
|
}
|
|
192
394
|
function getTemplatePath(...segments) {
|
|
193
|
-
return
|
|
395
|
+
return path3.join(getTemplateRoot(), ...segments);
|
|
194
396
|
}
|
|
195
397
|
|
|
196
398
|
// src/configurators/shared.ts
|
|
@@ -217,11 +419,11 @@ async function resolveSkills(ctx) {
|
|
|
217
419
|
const entries = await readdir(skillsRoot);
|
|
218
420
|
const skills = [];
|
|
219
421
|
for (const entry of entries.sort()) {
|
|
220
|
-
const skillDir =
|
|
422
|
+
const skillDir = path4.join(skillsRoot, entry);
|
|
221
423
|
if (!await isDirectory(skillDir)) {
|
|
222
424
|
continue;
|
|
223
425
|
}
|
|
224
|
-
const content = await readTextFile(
|
|
426
|
+
const content = await readTextFile(path4.join(skillDir, "SKILL.md"));
|
|
225
427
|
skills.push({
|
|
226
428
|
name: entry,
|
|
227
429
|
content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
|
|
@@ -237,7 +439,7 @@ async function resolveBundledSkills(ctx) {
|
|
|
237
439
|
const entries = await readdir(bundledRoot);
|
|
238
440
|
const bundled = [];
|
|
239
441
|
for (const entry of entries.sort()) {
|
|
240
|
-
const sourceDir =
|
|
442
|
+
const sourceDir = path4.join(bundledRoot, entry);
|
|
241
443
|
if (await isDirectory(sourceDir)) {
|
|
242
444
|
bundled.push({ name: entry, sourceDir, context: ctx });
|
|
243
445
|
}
|
|
@@ -246,23 +448,30 @@ async function resolveBundledSkills(ctx) {
|
|
|
246
448
|
}
|
|
247
449
|
async function writeSkills(dir, skills, bundled) {
|
|
248
450
|
await ensureDir(dir);
|
|
451
|
+
const written = [];
|
|
249
452
|
for (const skill of skills) {
|
|
250
|
-
|
|
453
|
+
const destination = path4.join(dir, skill.name, "SKILL.md");
|
|
454
|
+
await writeTextFile(destination, skill.content);
|
|
455
|
+
written.push(destination);
|
|
251
456
|
}
|
|
252
457
|
for (const skill of bundled) {
|
|
253
|
-
|
|
458
|
+
written.push(
|
|
459
|
+
...await copyTemplateDirectory(skill.sourceDir, path4.join(dir, skill.name), skill.context)
|
|
460
|
+
);
|
|
254
461
|
}
|
|
462
|
+
return written;
|
|
255
463
|
}
|
|
256
464
|
async function writeSharedHooks(dir, platform, opts = {}) {
|
|
257
465
|
const hooksRoot = getTemplatePath("shared-hooks");
|
|
258
466
|
const ctx = PLATFORM_META[platform].templateContext;
|
|
259
467
|
const entries = await readdir(hooksRoot);
|
|
260
468
|
await ensureDir(dir);
|
|
469
|
+
const written = [];
|
|
261
470
|
for (const entry of entries.sort()) {
|
|
262
471
|
if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
|
|
263
472
|
continue;
|
|
264
473
|
}
|
|
265
|
-
const sourcePath =
|
|
474
|
+
const sourcePath = path4.join(hooksRoot, entry);
|
|
266
475
|
if ((await stat2(sourcePath)).isDirectory()) {
|
|
267
476
|
continue;
|
|
268
477
|
}
|
|
@@ -271,14 +480,16 @@ async function writeSharedHooks(dir, platform, opts = {}) {
|
|
|
271
480
|
ctx,
|
|
272
481
|
`shared-hooks/${entry}`
|
|
273
482
|
);
|
|
274
|
-
const destination =
|
|
483
|
+
const destination = path4.join(dir, entry);
|
|
275
484
|
await writeTextFile(destination, content);
|
|
276
485
|
await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
|
|
486
|
+
written.push(destination);
|
|
277
487
|
}
|
|
488
|
+
return written;
|
|
278
489
|
}
|
|
279
490
|
async function copyPlatformTemplates(platformTemplateDir, destination, skipDirs, ctx) {
|
|
280
491
|
const source = getTemplatePath(platformTemplateDir);
|
|
281
|
-
|
|
492
|
+
return copyTemplateDirectory(source, destination, ctx, new Set(skipDirs));
|
|
282
493
|
}
|
|
283
494
|
async function writeMainConstraint(cwd, platform) {
|
|
284
495
|
const meta = PLATFORM_META[platform];
|
|
@@ -289,27 +500,38 @@ async function writeMainConstraint(cwd, platform) {
|
|
|
289
500
|
if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
|
|
290
501
|
throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
|
|
291
502
|
}
|
|
292
|
-
const destination =
|
|
503
|
+
const destination = path4.join(cwd, meta.mainConstraint);
|
|
293
504
|
const current = await readTextIfExists(destination);
|
|
294
|
-
|
|
295
|
-
|
|
505
|
+
if (current) {
|
|
506
|
+
const markerContent = extractMarkedRegion(generated);
|
|
507
|
+
if (!markerContent) {
|
|
508
|
+
throw new Error(`Main constraint template ${templateName} generated content lacks markers.`);
|
|
509
|
+
}
|
|
510
|
+
await writeTextFile(destination, replaceMarkedRegion(current, markerContent));
|
|
511
|
+
} else {
|
|
512
|
+
await writeTextFile(destination, generated);
|
|
513
|
+
}
|
|
514
|
+
return destination;
|
|
296
515
|
}
|
|
297
516
|
async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__PURE__ */ new Set()) {
|
|
298
517
|
await ensureDir(destination);
|
|
518
|
+
const written = [];
|
|
299
519
|
for (const entry of (await readdir(source)).sort()) {
|
|
300
520
|
if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
|
|
301
521
|
continue;
|
|
302
522
|
}
|
|
303
|
-
const sourcePath =
|
|
304
|
-
const destinationPath =
|
|
523
|
+
const sourcePath = path4.join(source, entry);
|
|
524
|
+
const destinationPath = path4.join(destination, stripTemplateExtension(entry));
|
|
305
525
|
const sourceStat = await stat2(sourcePath);
|
|
306
526
|
if (sourceStat.isDirectory()) {
|
|
307
|
-
await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs);
|
|
527
|
+
written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
|
|
308
528
|
continue;
|
|
309
529
|
}
|
|
310
530
|
const content = resolvePlaceholders(await readTextFile(sourcePath), ctx, sourcePath);
|
|
311
531
|
await writeTextFile(destinationPath, content);
|
|
532
|
+
written.push(destinationPath);
|
|
312
533
|
}
|
|
534
|
+
return written;
|
|
313
535
|
}
|
|
314
536
|
function shouldSkipTemplateEntry(entry) {
|
|
315
537
|
return entry === ".DS_Store" || entry === "__pycache__" || entry.endsWith(".ts") || entry.endsWith(".js") || entry.endsWith(".map");
|
|
@@ -323,69 +545,194 @@ async function configureClaude(cwd) {
|
|
|
323
545
|
const platform = "claude-code";
|
|
324
546
|
const meta = PLATFORM_META[platform];
|
|
325
547
|
const ctx = meta.templateContext;
|
|
326
|
-
const dest =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
await
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
548
|
+
const dest = path5.join(cwd, ".claude");
|
|
549
|
+
const hookConfigPath = path5.join(cwd, meta.hookConfigFile);
|
|
550
|
+
const artifacts = [];
|
|
551
|
+
const platformFiles = await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
|
|
552
|
+
artifacts.push(
|
|
553
|
+
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
554
|
+
(filePath) => fileArtifact(
|
|
555
|
+
filePath,
|
|
556
|
+
filePath.startsWith(path5.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
557
|
+
platform
|
|
558
|
+
)
|
|
559
|
+
)
|
|
560
|
+
);
|
|
561
|
+
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
562
|
+
artifacts.push(
|
|
563
|
+
...(await writeSharedHooks(path5.join(dest, "hooks"), platform)).map(
|
|
564
|
+
(filePath) => fileArtifact(filePath, "hook", platform)
|
|
565
|
+
)
|
|
333
566
|
);
|
|
334
|
-
|
|
567
|
+
artifacts.push(
|
|
568
|
+
...(await writeSkills(
|
|
569
|
+
path5.join(cwd, meta.skillsDir),
|
|
570
|
+
await resolveSkills(ctx),
|
|
571
|
+
await resolveBundledSkills(ctx)
|
|
572
|
+
)).map((filePath) => fileArtifact(filePath, "skill", platform))
|
|
573
|
+
);
|
|
574
|
+
artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
|
|
575
|
+
return artifacts;
|
|
335
576
|
}
|
|
336
577
|
|
|
337
578
|
// src/configurators/codex.ts
|
|
338
|
-
import
|
|
579
|
+
import path6 from "path";
|
|
339
580
|
async function configureCodex(cwd) {
|
|
340
581
|
const platform = "codex";
|
|
341
582
|
const meta = PLATFORM_META[platform];
|
|
342
583
|
const ctx = meta.templateContext;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
await
|
|
584
|
+
const hookConfigPath = path6.join(cwd, meta.hookConfigFile);
|
|
585
|
+
const artifacts = [];
|
|
586
|
+
artifacts.push(
|
|
587
|
+
...(await writeSkills(
|
|
588
|
+
path6.join(cwd, meta.skillsDir),
|
|
589
|
+
await resolveSkills(ctx),
|
|
590
|
+
await resolveBundledSkills(ctx)
|
|
591
|
+
)).map((filePath) => fileArtifact(filePath, "skill", platform))
|
|
592
|
+
);
|
|
593
|
+
artifacts.push(
|
|
594
|
+
...(await writeSharedHooks(path6.join(cwd, meta.hooksDir), platform, {
|
|
595
|
+
skipSubagentContext: true
|
|
596
|
+
})).map((filePath) => fileArtifact(filePath, "hook", platform))
|
|
347
597
|
);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
598
|
+
const platformFiles = await copyPlatformTemplates(
|
|
599
|
+
"codex",
|
|
600
|
+
path6.join(cwd, ".codex"),
|
|
601
|
+
["hooks"],
|
|
602
|
+
ctx
|
|
603
|
+
);
|
|
604
|
+
artifacts.push(
|
|
605
|
+
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
606
|
+
(filePath) => fileArtifact(
|
|
607
|
+
filePath,
|
|
608
|
+
filePath.startsWith(path6.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
609
|
+
platform
|
|
610
|
+
)
|
|
611
|
+
)
|
|
612
|
+
);
|
|
613
|
+
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
614
|
+
artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
|
|
615
|
+
return artifacts;
|
|
351
616
|
}
|
|
352
617
|
|
|
353
618
|
// src/configurators/qoder.ts
|
|
354
|
-
import {
|
|
355
|
-
import
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
619
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
620
|
+
import path8 from "path";
|
|
621
|
+
|
|
622
|
+
// src/utils/platform-paths.ts
|
|
623
|
+
import { existsSync as existsSync2 } from "fs";
|
|
624
|
+
import path7 from "path";
|
|
625
|
+
function detectQoderCnVariant(cwd) {
|
|
626
|
+
if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
return existsSync2(path7.join(cwd, PLATFORM_META.qoder.cnVariant ?? ".qodercn"));
|
|
630
|
+
}
|
|
631
|
+
function resolvePlatformMeta(cwd, platform) {
|
|
632
|
+
const meta = PLATFORM_META[platform];
|
|
633
|
+
if (platform !== "qoder" || !detectQoderCnVariant(cwd)) {
|
|
634
|
+
return meta;
|
|
635
|
+
}
|
|
636
|
+
return resolveQoderMetaForBaseDir(meta.cnVariant ?? ".qodercn");
|
|
637
|
+
}
|
|
638
|
+
function resolveQoderVariantMetas() {
|
|
639
|
+
const standardDir = PLATFORM_META.qoder.templateContext.platform_config_dir;
|
|
640
|
+
const cnDir = PLATFORM_META.qoder.cnVariant ?? ".qodercn";
|
|
641
|
+
return [.../* @__PURE__ */ new Set([standardDir, cnDir])].map(resolveQoderMetaForBaseDir);
|
|
642
|
+
}
|
|
643
|
+
function resolveQoderMetaForBaseDir(baseDir) {
|
|
644
|
+
const meta = PLATFORM_META.qoder;
|
|
645
|
+
return {
|
|
646
|
+
...meta,
|
|
647
|
+
skillsDir: `${baseDir}/skills`,
|
|
648
|
+
hooksDir: `${baseDir}/hooks`,
|
|
649
|
+
hookConfigFile: `${baseDir}/settings.json`,
|
|
650
|
+
agentsDir: `${baseDir}/agents`,
|
|
651
|
+
templateContext: {
|
|
652
|
+
...meta.templateContext,
|
|
653
|
+
platform_config_dir: baseDir
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/configurators/qoder.ts
|
|
659
|
+
async function claudeHarnessSkillsExist(cwd) {
|
|
660
|
+
return pathExists(path8.join(cwd, ".claude", "skills", "ec-workflow", "SKILL.md"));
|
|
661
|
+
}
|
|
662
|
+
async function listFilesRecursive(dir) {
|
|
663
|
+
let entries;
|
|
359
664
|
try {
|
|
360
|
-
|
|
361
|
-
} catch {
|
|
362
|
-
|
|
665
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
666
|
+
} catch (error) {
|
|
667
|
+
if (error.code === "ENOENT") {
|
|
668
|
+
return [];
|
|
669
|
+
}
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
672
|
+
const files = [];
|
|
673
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
674
|
+
const entryPath = path8.join(dir, entry.name);
|
|
675
|
+
if (entry.isDirectory()) {
|
|
676
|
+
files.push(...await listFilesRecursive(entryPath));
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (entry.isFile()) {
|
|
680
|
+
files.push(entryPath);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return files;
|
|
684
|
+
}
|
|
685
|
+
async function existingManagedSkillArtifacts(skillsDir, skillNames, platform) {
|
|
686
|
+
const artifacts = [];
|
|
687
|
+
for (const skillName of skillNames) {
|
|
688
|
+
for (const filePath of await listFilesRecursive(path8.join(skillsDir, skillName))) {
|
|
689
|
+
artifacts.push(fileArtifact(filePath, "skill", platform));
|
|
690
|
+
}
|
|
363
691
|
}
|
|
692
|
+
return artifacts;
|
|
364
693
|
}
|
|
365
694
|
async function configureQoder(cwd) {
|
|
366
695
|
const platform = "qoder";
|
|
367
|
-
const
|
|
368
|
-
const ctx =
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
696
|
+
const meta = resolvePlatformMeta(cwd, platform);
|
|
697
|
+
const ctx = meta.templateContext;
|
|
698
|
+
const dest = path8.join(cwd, ctx.platform_config_dir);
|
|
699
|
+
const hookConfigPath = path8.join(cwd, meta.hookConfigFile);
|
|
700
|
+
const artifacts = [];
|
|
701
|
+
const platformFiles = await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
|
|
702
|
+
artifacts.push(
|
|
703
|
+
...platformFiles.filter((filePath) => filePath !== hookConfigPath).map(
|
|
704
|
+
(filePath) => fileArtifact(
|
|
705
|
+
filePath,
|
|
706
|
+
filePath.startsWith(path8.join(cwd, meta.agentsDir)) ? "agent" : "platform-config",
|
|
707
|
+
platform
|
|
708
|
+
)
|
|
709
|
+
)
|
|
710
|
+
);
|
|
711
|
+
artifacts.push(...await hookRegistrationArtifacts(hookConfigPath, platform));
|
|
712
|
+
artifacts.push(
|
|
713
|
+
...(await writeSharedHooks(path8.join(dest, "hooks"), platform)).map(
|
|
714
|
+
(filePath) => fileArtifact(filePath, "hook", platform)
|
|
715
|
+
)
|
|
716
|
+
);
|
|
717
|
+
const skills = await resolveSkills(ctx);
|
|
718
|
+
const bundledSkills = await resolveBundledSkills(ctx);
|
|
719
|
+
if (!await claudeHarnessSkillsExist(cwd)) {
|
|
720
|
+
artifacts.push(
|
|
721
|
+
...(await writeSkills(path8.join(dest, "skills"), skills, bundledSkills)).map(
|
|
722
|
+
(filePath) => fileArtifact(filePath, "skill", platform)
|
|
723
|
+
)
|
|
724
|
+
);
|
|
725
|
+
} else {
|
|
726
|
+
artifacts.push(
|
|
727
|
+
...await existingManagedSkillArtifacts(
|
|
728
|
+
path8.join(dest, "skills"),
|
|
729
|
+
[...skills.map((skill) => skill.name), ...bundledSkills.map((skill) => skill.name)],
|
|
730
|
+
platform
|
|
731
|
+
)
|
|
380
732
|
);
|
|
381
733
|
}
|
|
382
|
-
await writeMainConstraint(cwd, platform);
|
|
383
|
-
|
|
384
|
-
function detectQoderCnVariant(cwd) {
|
|
385
|
-
if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
|
|
386
|
-
return true;
|
|
387
|
-
}
|
|
388
|
-
return existsSync2(path6.join(cwd, ".qodercn"));
|
|
734
|
+
artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
|
|
735
|
+
return artifacts;
|
|
389
736
|
}
|
|
390
737
|
|
|
391
738
|
// src/configurators/index.ts
|
|
@@ -395,23 +742,19 @@ var CONFIGURATORS = {
|
|
|
395
742
|
qoder: configureQoder
|
|
396
743
|
};
|
|
397
744
|
|
|
398
|
-
// src/ui/banner.ts
|
|
399
|
-
import chalk from "chalk";
|
|
400
|
-
import figlet from "figlet";
|
|
401
|
-
|
|
402
745
|
// src/constants/version.ts
|
|
403
746
|
import { readFileSync } from "fs";
|
|
404
|
-
import
|
|
747
|
+
import path9 from "path";
|
|
405
748
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
406
749
|
var packageMetadata = readPackageMetadata();
|
|
407
750
|
var PACKAGE_NAME = packageMetadata.name;
|
|
408
751
|
var VERSION = packageMetadata.version;
|
|
409
752
|
function readPackageMetadata() {
|
|
410
|
-
const here =
|
|
753
|
+
const here = path9.dirname(fileURLToPath2(import.meta.url));
|
|
411
754
|
const candidates = [
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
755
|
+
path9.resolve(here, "../../package.json"),
|
|
756
|
+
path9.resolve(here, "../package.json"),
|
|
757
|
+
path9.resolve(process.cwd(), "package.json")
|
|
415
758
|
];
|
|
416
759
|
for (const candidate of candidates) {
|
|
417
760
|
try {
|
|
@@ -426,6 +769,8 @@ function readPackageMetadata() {
|
|
|
426
769
|
}
|
|
427
770
|
|
|
428
771
|
// src/ui/banner.ts
|
|
772
|
+
import chalk from "chalk";
|
|
773
|
+
import figlet from "figlet";
|
|
429
774
|
var WIDE_TERMINAL_WIDTH = 88;
|
|
430
775
|
var MEDIUM_TERMINAL_WIDTH = 72;
|
|
431
776
|
function renderBanner() {
|
|
@@ -450,7 +795,7 @@ function colorizeBanner(art) {
|
|
|
450
795
|
}
|
|
451
796
|
|
|
452
797
|
// src/utils/config-yaml.ts
|
|
453
|
-
import { readFile as
|
|
798
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
454
799
|
import YAML, { isScalar, isSeq, parseDocument } from "yaml";
|
|
455
800
|
function createDefaultConfig(params) {
|
|
456
801
|
return {
|
|
@@ -458,18 +803,13 @@ function createDefaultConfig(params) {
|
|
|
458
803
|
harness_version: params.harnessVersion,
|
|
459
804
|
agents: params.agents,
|
|
460
805
|
project: {
|
|
461
|
-
name: params.projectName
|
|
462
|
-
mode: "auto"
|
|
806
|
+
name: params.projectName
|
|
463
807
|
},
|
|
464
808
|
memory: {
|
|
465
809
|
short_term_max: 10,
|
|
466
810
|
short_term_keep: 5,
|
|
467
811
|
schema_version: 2
|
|
468
812
|
},
|
|
469
|
-
test: {
|
|
470
|
-
framework: "auto",
|
|
471
|
-
command: ""
|
|
472
|
-
},
|
|
473
813
|
tasks: {
|
|
474
814
|
auto_archive_days: 30
|
|
475
815
|
},
|
|
@@ -486,11 +826,11 @@ async function writeConfigYaml(filePath, config) {
|
|
|
486
826
|
await writeTextFile(filePath, stringifyConfig(config));
|
|
487
827
|
}
|
|
488
828
|
async function readConfigYaml(filePath) {
|
|
489
|
-
const content = await
|
|
829
|
+
const content = await readFile3(filePath, "utf8");
|
|
490
830
|
return YAML.parse(content);
|
|
491
831
|
}
|
|
492
832
|
async function updateConfigYaml(filePath, updater) {
|
|
493
|
-
const content = await
|
|
833
|
+
const content = await readFile3(filePath, "utf8");
|
|
494
834
|
const document = parseDocument(content);
|
|
495
835
|
const config = document.toJSON();
|
|
496
836
|
updater(config);
|
|
@@ -512,87 +852,10 @@ async function updateHarnessVersion(filePath, version) {
|
|
|
512
852
|
});
|
|
513
853
|
}
|
|
514
854
|
|
|
515
|
-
// src/commands/platforms.ts
|
|
516
|
-
import { cancel, confirm, multiselect } from "@clack/prompts";
|
|
517
|
-
function parseAgentList(agentList) {
|
|
518
|
-
const values = agentList.split(",").map((value) => value.trim()).filter(Boolean);
|
|
519
|
-
if (values.length === 0) {
|
|
520
|
-
throw new Error("No agent platform specified.");
|
|
521
|
-
}
|
|
522
|
-
const invalid = values.filter((value) => !isAgentPlatform(value));
|
|
523
|
-
if (invalid.length > 0) {
|
|
524
|
-
throw new Error(`Unknown agent platform: ${invalid.join(", ")}`);
|
|
525
|
-
}
|
|
526
|
-
return [...new Set(values)];
|
|
527
|
-
}
|
|
528
|
-
async function resolvePlatforms(opts, defaults = ["claude-code"]) {
|
|
529
|
-
if (opts.agent) {
|
|
530
|
-
return parseAgentList(opts.agent);
|
|
531
|
-
}
|
|
532
|
-
if (opts.yes) {
|
|
533
|
-
return defaults;
|
|
534
|
-
}
|
|
535
|
-
let selectedDefaults = defaults;
|
|
536
|
-
while (true) {
|
|
537
|
-
const result = await multiselect({
|
|
538
|
-
message: "Select agent platforms (Space to toggle, Enter to review)",
|
|
539
|
-
options: AGENT_PLATFORMS.map((platform) => ({
|
|
540
|
-
label: PLATFORM_META[platform].label,
|
|
541
|
-
value: platform
|
|
542
|
-
})),
|
|
543
|
-
initialValues: selectedDefaults,
|
|
544
|
-
required: true
|
|
545
|
-
});
|
|
546
|
-
if (typeof result === "symbol") {
|
|
547
|
-
cancel("Platform selection cancelled.");
|
|
548
|
-
process.exit(1);
|
|
549
|
-
}
|
|
550
|
-
const labels = result.map((platform) => PLATFORM_META[platform].label).join(", ");
|
|
551
|
-
const shouldInstall = await confirm({
|
|
552
|
-
message: `Install Easy Coding for: ${labels}?`,
|
|
553
|
-
initialValue: true
|
|
554
|
-
});
|
|
555
|
-
if (typeof shouldInstall === "symbol") {
|
|
556
|
-
cancel("Platform selection cancelled.");
|
|
557
|
-
process.exit(1);
|
|
558
|
-
}
|
|
559
|
-
if (shouldInstall) {
|
|
560
|
-
return result;
|
|
561
|
-
}
|
|
562
|
-
selectedDefaults = result;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// src/commands/add-agent.ts
|
|
567
|
-
async function addAgent(opts) {
|
|
568
|
-
renderBanner();
|
|
569
|
-
const cwd = process.cwd();
|
|
570
|
-
const configPath = path8.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
571
|
-
if (!await pathExists(configPath)) {
|
|
572
|
-
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
573
|
-
}
|
|
574
|
-
const config = await readConfigYaml(configPath);
|
|
575
|
-
const platforms = await resolvePlatforms(opts, ["claude-code"]);
|
|
576
|
-
const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
|
|
577
|
-
if (toInstall.length === 0) {
|
|
578
|
-
outro(chalk2.yellow("All selected agent platforms are already installed."));
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
for (const platform of toInstall) {
|
|
582
|
-
await CONFIGURATORS[platform](cwd);
|
|
583
|
-
}
|
|
584
|
-
await addAgentsToConfig(configPath, toInstall);
|
|
585
|
-
outro(chalk2.green(`Added agent platforms: ${toInstall.join(", ")}`));
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// src/commands/init.ts
|
|
589
|
-
import { note, outro as outro2 } from "@clack/prompts";
|
|
590
|
-
import chalk3 from "chalk";
|
|
591
|
-
|
|
592
855
|
// src/utils/gitignore.ts
|
|
593
|
-
import
|
|
856
|
+
import path10 from "path";
|
|
594
857
|
async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550 easy-coding-harness (auto-generated) \u2550\u2550\u2550\n# Personal runtime state; do not commit") {
|
|
595
|
-
const gitignorePath =
|
|
858
|
+
const gitignorePath = path10.join(cwd, ".gitignore");
|
|
596
859
|
const current = await readTextIfExists(gitignorePath) ?? "";
|
|
597
860
|
const lines = current.split(/\r?\n/).map((line) => line.trim());
|
|
598
861
|
if (lines.includes(entry)) {
|
|
@@ -603,93 +866,36 @@ async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550
|
|
|
603
866
|
await writeTextFile(gitignorePath, next);
|
|
604
867
|
return true;
|
|
605
868
|
}
|
|
606
|
-
async function
|
|
607
|
-
return ensureGitignoreEntry(cwd,
|
|
869
|
+
async function ensureEasyCodingSessionsIgnored(cwd) {
|
|
870
|
+
return ensureGitignoreEntry(cwd, SESSIONS_GITIGNORE_ENTRY);
|
|
608
871
|
}
|
|
609
872
|
|
|
610
|
-
// src/utils/
|
|
611
|
-
import
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
if (!await pathExists(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
const legacyAssets = await detectLegacyAssets(easyCodingDir);
|
|
624
|
-
if (legacyAssets.length === 0) {
|
|
625
|
-
return { kind: "unknown", easyCodingDir };
|
|
626
|
-
}
|
|
627
|
-
return {
|
|
628
|
-
kind: "legacy",
|
|
629
|
-
easyCodingDir,
|
|
630
|
-
legacyAssets,
|
|
631
|
-
missingHarnessFiles: [relativeConfigPath(), relativeProjectInitTaskPath()]
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
async function detectLegacyAssets(easyCodingDir) {
|
|
635
|
-
const assets = [];
|
|
636
|
-
for (const file of LEGACY_ROOT_FILES) {
|
|
637
|
-
if (await pathExists(path10.join(easyCodingDir, file))) {
|
|
638
|
-
assets.push(relativeEasyCodingPath(file));
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
if (await pathExists(path10.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
|
|
642
|
-
assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
|
|
643
|
-
}
|
|
644
|
-
const shortMemoryFiles = await listMarkdownFiles(path10.join(easyCodingDir, "memory", "short"));
|
|
645
|
-
assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
|
|
646
|
-
for (const dir of ["spec", "prototype"]) {
|
|
647
|
-
if (await hasAnyDirectoryEntry(path10.join(easyCodingDir, dir))) {
|
|
648
|
-
assets.push(relativeEasyCodingPath(dir));
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
return assets.sort();
|
|
652
|
-
}
|
|
653
|
-
async function listMarkdownFiles(dir) {
|
|
654
|
-
if (!await isDirectory(dir)) {
|
|
655
|
-
return [];
|
|
656
|
-
}
|
|
657
|
-
const entries = await readdir2(dir, { withFileTypes: true });
|
|
658
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
|
|
659
|
-
}
|
|
660
|
-
async function hasAnyDirectoryEntry(dir) {
|
|
661
|
-
if (!await isDirectory(dir)) {
|
|
662
|
-
return false;
|
|
663
|
-
}
|
|
664
|
-
return (await readdir2(dir)).length > 0;
|
|
665
|
-
}
|
|
666
|
-
function relativeEasyCodingPath(...segments) {
|
|
667
|
-
return path10.posix.join(EASY_CODING_DIR, ...segments);
|
|
668
|
-
}
|
|
669
|
-
function relativeConfigPath() {
|
|
670
|
-
return path10.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
671
|
-
}
|
|
672
|
-
function relativeProjectInitTaskPath() {
|
|
673
|
-
return path10.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// src/utils/runtime-scaffold.ts
|
|
677
|
-
import path11 from "path";
|
|
678
|
-
async function writeRuntimeScaffold(cwd, agents) {
|
|
679
|
-
const easyCodingDir = path11.join(cwd, EASY_CODING_DIR);
|
|
680
|
-
await ensureDir(easyCodingDir);
|
|
681
|
-
const configPath = path11.join(easyCodingDir, CONFIG_FILE);
|
|
682
|
-
if (!await pathExists(configPath)) {
|
|
683
|
-
const projectName = path11.basename(cwd);
|
|
684
|
-
await writeConfigYaml(
|
|
685
|
-
configPath,
|
|
686
|
-
createDefaultConfig({ projectName, harnessVersion: VERSION, agents })
|
|
687
|
-
);
|
|
873
|
+
// src/utils/runtime-scaffold.ts
|
|
874
|
+
import path11 from "path";
|
|
875
|
+
async function writeRuntimeScaffold(cwd, agents) {
|
|
876
|
+
const easyCodingDir = path11.join(cwd, EASY_CODING_DIR);
|
|
877
|
+
await ensureDir(easyCodingDir);
|
|
878
|
+
const configPath = path11.join(easyCodingDir, CONFIG_FILE);
|
|
879
|
+
if (!await pathExists(configPath)) {
|
|
880
|
+
const projectName = path11.basename(cwd);
|
|
881
|
+
await writeConfigYaml(
|
|
882
|
+
configPath,
|
|
883
|
+
createDefaultConfig({ projectName, harnessVersion: VERSION, agents })
|
|
884
|
+
);
|
|
688
885
|
}
|
|
689
886
|
await ensureDir(path11.join(easyCodingDir, "tasks"));
|
|
887
|
+
await ensureDir(path11.join(easyCodingDir, SESSIONS_DIR));
|
|
690
888
|
await ensureDir(path11.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
|
|
691
889
|
await ensureDir(path11.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
692
890
|
await writeMemoryScaffold(easyCodingDir);
|
|
891
|
+
await writeTemplatesScaffold(easyCodingDir);
|
|
892
|
+
}
|
|
893
|
+
async function writeTemplatesScaffold(easyCodingDir) {
|
|
894
|
+
const templatesDir = path11.join(easyCodingDir, TEMPLATES_DIR);
|
|
895
|
+
await ensureDir(templatesDir);
|
|
896
|
+
const src = getTemplatePath("runtime", "templates", "dev-spec-skeleton.md");
|
|
897
|
+
const dest = path11.join(templatesDir, "dev-spec-skeleton.md");
|
|
898
|
+
await writeTextFile(dest, await readTextFile(src));
|
|
693
899
|
}
|
|
694
900
|
async function writeMemoryScaffold(easyCodingDir) {
|
|
695
901
|
const memoryDir = path11.join(easyCodingDir, MEMORY_DIR);
|
|
@@ -703,6 +909,11 @@ async function writeMemoryScaffold(easyCodingDir) {
|
|
|
703
909
|
const templatePath = getTemplatePath("runtime", "memory", "long", file);
|
|
704
910
|
await writeTextFile(destination, await readTextFile(templatePath));
|
|
705
911
|
}
|
|
912
|
+
const shortTemplateDest = path11.join(memoryDir, "SHORT_MEMORY_TEMPLATE.md");
|
|
913
|
+
if (!await pathExists(shortTemplateDest)) {
|
|
914
|
+
const templatePath = getTemplatePath("runtime", "memory", "SHORT_MEMORY_TEMPLATE.md");
|
|
915
|
+
await writeTextFile(shortTemplateDest, await readTextFile(templatePath));
|
|
916
|
+
}
|
|
706
917
|
}
|
|
707
918
|
|
|
708
919
|
// src/utils/task-json.ts
|
|
@@ -714,6 +925,8 @@ function createProjectInitTask(params) {
|
|
|
714
925
|
status: "PENDING",
|
|
715
926
|
created_at: (params.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
716
927
|
created_by: "cli-init",
|
|
928
|
+
last_agent: "cli",
|
|
929
|
+
stage_history: [],
|
|
717
930
|
context: {
|
|
718
931
|
agents_installed: params.agents,
|
|
719
932
|
cli_version: VERSION,
|
|
@@ -746,6 +959,14 @@ async function writeProjectInitTask(cwd, agents, options = {}) {
|
|
|
746
959
|
})
|
|
747
960
|
);
|
|
748
961
|
}
|
|
962
|
+
async function setPendingInitSince(cwd, version) {
|
|
963
|
+
const filePath = getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID);
|
|
964
|
+
if (!await pathExists(filePath)) return;
|
|
965
|
+
const task = await readTaskJson(filePath);
|
|
966
|
+
if (task.status !== "COMPLETE") return;
|
|
967
|
+
task.pending_init_since = version;
|
|
968
|
+
await writeTaskJson(filePath, task);
|
|
969
|
+
}
|
|
749
970
|
async function listTasks(cwd) {
|
|
750
971
|
const tasksDir = path12.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
751
972
|
if (!await pathExists(tasksDir)) {
|
|
@@ -778,6 +999,672 @@ function isActiveTask(task) {
|
|
|
778
999
|
return task.status !== "COMPLETE" && task.status !== "CLOSED";
|
|
779
1000
|
}
|
|
780
1001
|
|
|
1002
|
+
// src/commands/platforms.ts
|
|
1003
|
+
import { cancel, confirm, multiselect } from "@clack/prompts";
|
|
1004
|
+
function parseAgentList(agentList) {
|
|
1005
|
+
const values = agentList.split(",").map((value) => value.trim()).filter(Boolean);
|
|
1006
|
+
if (values.length === 0) {
|
|
1007
|
+
throw new Error("No agent platform specified.");
|
|
1008
|
+
}
|
|
1009
|
+
const invalid = values.filter((value) => !isAgentPlatform(value));
|
|
1010
|
+
if (invalid.length > 0) {
|
|
1011
|
+
throw new Error(`Unknown agent platform: ${invalid.join(", ")}`);
|
|
1012
|
+
}
|
|
1013
|
+
return [...new Set(values)];
|
|
1014
|
+
}
|
|
1015
|
+
async function resolvePlatforms(opts, defaults = ["claude-code"]) {
|
|
1016
|
+
if (opts.agent) {
|
|
1017
|
+
return parseAgentList(opts.agent);
|
|
1018
|
+
}
|
|
1019
|
+
if (opts.yes) {
|
|
1020
|
+
return defaults;
|
|
1021
|
+
}
|
|
1022
|
+
let selectedDefaults = defaults;
|
|
1023
|
+
while (true) {
|
|
1024
|
+
const result = await multiselect({
|
|
1025
|
+
message: "Select agent platforms (Space to toggle, Enter to review)",
|
|
1026
|
+
options: AGENT_PLATFORMS.map((platform) => ({
|
|
1027
|
+
label: PLATFORM_META[platform].label,
|
|
1028
|
+
value: platform
|
|
1029
|
+
})),
|
|
1030
|
+
initialValues: selectedDefaults,
|
|
1031
|
+
required: true
|
|
1032
|
+
});
|
|
1033
|
+
if (typeof result === "symbol") {
|
|
1034
|
+
cancel("Platform selection cancelled.");
|
|
1035
|
+
process.exit(1);
|
|
1036
|
+
}
|
|
1037
|
+
const labels = result.map((platform) => PLATFORM_META[platform].label).join(", ");
|
|
1038
|
+
const shouldInstall = await confirm({
|
|
1039
|
+
message: `Install Easy Coding for: ${labels}?`,
|
|
1040
|
+
initialValue: true
|
|
1041
|
+
});
|
|
1042
|
+
if (typeof shouldInstall === "symbol") {
|
|
1043
|
+
cancel("Platform selection cancelled.");
|
|
1044
|
+
process.exit(1);
|
|
1045
|
+
}
|
|
1046
|
+
if (shouldInstall) {
|
|
1047
|
+
return result;
|
|
1048
|
+
}
|
|
1049
|
+
selectedDefaults = result;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/commands/add-agent.ts
|
|
1054
|
+
async function addAgent(opts) {
|
|
1055
|
+
renderBanner();
|
|
1056
|
+
const cwd = process.cwd();
|
|
1057
|
+
const configPath = path13.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
1058
|
+
if (!await pathExists(configPath)) {
|
|
1059
|
+
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
1060
|
+
}
|
|
1061
|
+
const config = await readConfigYaml(configPath);
|
|
1062
|
+
const platforms = await resolvePlatforms(opts, ["claude-code"]);
|
|
1063
|
+
const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
|
|
1064
|
+
if (toInstall.length === 0) {
|
|
1065
|
+
outro(chalk2.yellow("All selected agent platforms are already installed."));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const artifacts = [];
|
|
1069
|
+
for (const platform of toInstall) {
|
|
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(", ")}`));
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/commands/clear.ts
|
|
1086
|
+
import { readdir as readdir4, rm, writeFile } from "fs/promises";
|
|
1087
|
+
import path14 from "path";
|
|
1088
|
+
import { cancel as cancel2, confirm as confirm2, outro as outro2 } from "@clack/prompts";
|
|
1089
|
+
import chalk3 from "chalk";
|
|
1090
|
+
var PLATFORM_TEMPLATE_DIR = {
|
|
1091
|
+
"claude-code": "claude",
|
|
1092
|
+
codex: "codex",
|
|
1093
|
+
qoder: "qoder"
|
|
1094
|
+
};
|
|
1095
|
+
async function clear(opts) {
|
|
1096
|
+
renderBanner();
|
|
1097
|
+
const cwd = process.cwd();
|
|
1098
|
+
const easyCodingDir = path14.join(cwd, EASY_CODING_DIR);
|
|
1099
|
+
if (!await pathExists(easyCodingDir)) {
|
|
1100
|
+
throw new Error("No .easy-coding directory found in this project \u2014 nothing to clear.");
|
|
1101
|
+
}
|
|
1102
|
+
const agents = await resolveInstalledAgents(cwd);
|
|
1103
|
+
const plan = await buildClearPlan(cwd, agents);
|
|
1104
|
+
console.log(renderPlan(cwd, agents, plan));
|
|
1105
|
+
if (opts.dryRun) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (!opts.yes) {
|
|
1109
|
+
const ok = await confirm2({
|
|
1110
|
+
message: "Remove these harness files? Tasks, spec, memory, and knowledge files are kept.",
|
|
1111
|
+
initialValue: false
|
|
1112
|
+
});
|
|
1113
|
+
if (typeof ok === "symbol" || !ok) {
|
|
1114
|
+
cancel2("Clear cancelled.");
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
await executeClearPlan(plan);
|
|
1119
|
+
outro2(chalk3.green("Easy Coding harness removed. Run easy-coding init to reinstall."));
|
|
1120
|
+
}
|
|
1121
|
+
async function resolveInstalledAgents(cwd) {
|
|
1122
|
+
const configPath = path14.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
1123
|
+
if (await pathExists(configPath)) {
|
|
1124
|
+
try {
|
|
1125
|
+
const config = await readConfigYaml(configPath);
|
|
1126
|
+
const listed = Array.isArray(config.agents) ? config.agents.filter(isAgentPlatform) : [];
|
|
1127
|
+
if (listed.length > 0) {
|
|
1128
|
+
return listed;
|
|
1129
|
+
}
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const detected = [];
|
|
1134
|
+
for (const platform of AGENT_PLATFORMS) {
|
|
1135
|
+
if (await hasPlatformInstall(cwd, platform)) {
|
|
1136
|
+
detected.push(platform);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return detected;
|
|
1140
|
+
}
|
|
1141
|
+
async function hasPlatformInstall(cwd, platform) {
|
|
1142
|
+
const metas = platform === "qoder" ? resolveQoderVariantMetas() : [resolvePlatformMeta(cwd, platform)];
|
|
1143
|
+
for (const meta of metas) {
|
|
1144
|
+
if (await hasInstallMarkers(cwd, meta)) {
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
async function hasInstallMarkers(cwd, meta) {
|
|
1151
|
+
const markers = [meta.skillsDir, meta.hooksDir, meta.agentsDir, meta.hookConfigFile];
|
|
1152
|
+
for (const marker of markers) {
|
|
1153
|
+
if (await pathExists(path14.join(cwd, marker))) {
|
|
1154
|
+
return true;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
async function resolveClearPlatformMetas(cwd, platform) {
|
|
1160
|
+
if (platform !== "qoder") {
|
|
1161
|
+
return [resolvePlatformMeta(cwd, platform)];
|
|
1162
|
+
}
|
|
1163
|
+
const installed = [];
|
|
1164
|
+
for (const meta of resolveQoderVariantMetas()) {
|
|
1165
|
+
if (await hasInstallMarkers(cwd, meta)) {
|
|
1166
|
+
installed.push(meta);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return installed.length > 0 ? installed : [resolvePlatformMeta(cwd, platform)];
|
|
1170
|
+
}
|
|
1171
|
+
async function buildClearPlan(cwd, agents) {
|
|
1172
|
+
const manifest = await readInstallManifest(cwd);
|
|
1173
|
+
if (manifest) {
|
|
1174
|
+
const plan2 = await buildManifestClearPlan(cwd, agents, manifest);
|
|
1175
|
+
const manifestAgents = new Set(manifest.agents.filter(isAgentPlatform));
|
|
1176
|
+
const missingAgents = agents.filter((agent) => !manifestAgents.has(agent));
|
|
1177
|
+
if (missingAgents.length > 0) {
|
|
1178
|
+
mergeClearPlan(plan2, await buildTemplateClearPlan(cwd, missingAgents));
|
|
1179
|
+
}
|
|
1180
|
+
if (manifestAgents.has("qoder") && agents.includes("qoder")) {
|
|
1181
|
+
const uncoveredQoderMetas = await resolveManifestUncoveredQoderMetas(cwd, manifest);
|
|
1182
|
+
if (uncoveredQoderMetas.length > 0) {
|
|
1183
|
+
mergeClearPlan(
|
|
1184
|
+
plan2,
|
|
1185
|
+
await buildTemplateClearPlanForMetas(cwd, "qoder", uncoveredQoderMetas)
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
addRuntimeClearEntries(plan2, cwd);
|
|
1190
|
+
return plan2;
|
|
1191
|
+
}
|
|
1192
|
+
const plan = await buildTemplateClearPlan(cwd, agents);
|
|
1193
|
+
addRuntimeClearEntries(plan, cwd);
|
|
1194
|
+
return plan;
|
|
1195
|
+
}
|
|
1196
|
+
async function buildManifestClearPlan(cwd, agents, manifest) {
|
|
1197
|
+
const plan = createClearPlan();
|
|
1198
|
+
const agentSet = new Set(agents);
|
|
1199
|
+
for (const file of manifest.files) {
|
|
1200
|
+
if (!agentSet.has(file.platform)) {
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
const filePath = manifestPath(cwd, file.path);
|
|
1204
|
+
if (!await pathExists(filePath)) {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
if (await manifestFileMatches(filePath, file.sha256)) {
|
|
1208
|
+
plan.removeFiles.push({ filePath, expectedSha256: file.sha256 });
|
|
1209
|
+
addEmptyDirChain(plan.emptyDirs, path14.dirname(filePath), cwd);
|
|
1210
|
+
} else {
|
|
1211
|
+
plan.skippedModified.push(filePath);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
for (const registration of manifest.hook_registrations) {
|
|
1215
|
+
if (!agentSet.has(registration.platform)) {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
addHookConfigPrune(
|
|
1219
|
+
plan.pruneHookConfigs,
|
|
1220
|
+
plan.pruneHookCommands,
|
|
1221
|
+
manifestPath(cwd, registration.config_path),
|
|
1222
|
+
registration.hook_path ? [registration.hook_path] : [],
|
|
1223
|
+
[registration.command]
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
for (const region of manifest.constraint_regions) {
|
|
1227
|
+
if (!agentSet.has(region.platform)) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
plan.constraints.add(manifestPath(cwd, region.path));
|
|
1231
|
+
}
|
|
1232
|
+
return finalizeClearPlan(plan);
|
|
1233
|
+
}
|
|
1234
|
+
async function buildTemplateClearPlan(cwd, agents) {
|
|
1235
|
+
const plan = createClearPlan();
|
|
1236
|
+
const managedSkills = await listDirNames(getTemplatePath("common", "skills"));
|
|
1237
|
+
managedSkills.push(...await listDirNames(getTemplatePath("common", "bundled-skills")));
|
|
1238
|
+
for (const platform of agents) {
|
|
1239
|
+
const metas = await resolveClearPlatformMetas(cwd, platform);
|
|
1240
|
+
await addTemplateClearEntries(plan, cwd, platform, metas, managedSkills);
|
|
1241
|
+
}
|
|
1242
|
+
return finalizeClearPlan(plan);
|
|
1243
|
+
}
|
|
1244
|
+
async function buildTemplateClearPlanForMetas(cwd, platform, metas) {
|
|
1245
|
+
const plan = createClearPlan();
|
|
1246
|
+
const managedSkills = await listDirNames(getTemplatePath("common", "skills"));
|
|
1247
|
+
managedSkills.push(...await listDirNames(getTemplatePath("common", "bundled-skills")));
|
|
1248
|
+
await addTemplateClearEntries(plan, cwd, platform, metas, managedSkills);
|
|
1249
|
+
return finalizeClearPlan(plan);
|
|
1250
|
+
}
|
|
1251
|
+
async function addTemplateClearEntries(plan, cwd, platform, metas, managedSkills) {
|
|
1252
|
+
const hookFileNames = await listSharedHookNamesForPlatform(platform);
|
|
1253
|
+
const agentFileNames = await listFileNames(
|
|
1254
|
+
getTemplatePath(PLATFORM_TEMPLATE_DIR[platform], "agents")
|
|
1255
|
+
);
|
|
1256
|
+
for (const meta of metas) {
|
|
1257
|
+
for (const name of managedSkills) {
|
|
1258
|
+
plan.remove.add(path14.join(cwd, meta.skillsDir, name));
|
|
1259
|
+
}
|
|
1260
|
+
for (const name of hookFileNames) {
|
|
1261
|
+
plan.remove.add(path14.join(cwd, meta.hooksDir, name));
|
|
1262
|
+
}
|
|
1263
|
+
for (const name of agentFileNames) {
|
|
1264
|
+
plan.remove.add(path14.join(cwd, meta.agentsDir, name));
|
|
1265
|
+
}
|
|
1266
|
+
addHookConfigPrune(
|
|
1267
|
+
plan.pruneHookConfigs,
|
|
1268
|
+
plan.pruneHookCommands,
|
|
1269
|
+
path14.join(cwd, meta.hookConfigFile),
|
|
1270
|
+
hookFileNames.map((name) => `${meta.templateContext.platform_config_dir}/hooks/${name}`),
|
|
1271
|
+
[]
|
|
1272
|
+
);
|
|
1273
|
+
plan.constraints.add(path14.join(cwd, meta.mainConstraint));
|
|
1274
|
+
if (platform === "codex") {
|
|
1275
|
+
plan.remove.add(path14.join(cwd, meta.templateContext.platform_config_dir, "config.toml"));
|
|
1276
|
+
}
|
|
1277
|
+
plan.emptyDirs.add(path14.join(cwd, meta.skillsDir));
|
|
1278
|
+
plan.emptyDirs.add(path14.join(cwd, meta.hooksDir));
|
|
1279
|
+
plan.emptyDirs.add(path14.join(cwd, meta.agentsDir));
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
async function resolveManifestUncoveredQoderMetas(cwd, manifest) {
|
|
1283
|
+
const coveredBaseDirs = manifestCoveredQoderBaseDirs(manifest);
|
|
1284
|
+
const metas = [];
|
|
1285
|
+
for (const meta of resolveQoderVariantMetas()) {
|
|
1286
|
+
const baseDir = meta.templateContext.platform_config_dir;
|
|
1287
|
+
if (coveredBaseDirs.has(baseDir)) {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
if (await hasInstallMarkers(cwd, meta)) {
|
|
1291
|
+
metas.push(meta);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return metas;
|
|
1295
|
+
}
|
|
1296
|
+
function manifestCoveredQoderBaseDirs(manifest) {
|
|
1297
|
+
const covered = /* @__PURE__ */ new Set();
|
|
1298
|
+
for (const meta of resolveQoderVariantMetas()) {
|
|
1299
|
+
const baseDir = meta.templateContext.platform_config_dir.replace(/\\/g, "/");
|
|
1300
|
+
const prefix = `${baseDir}/`;
|
|
1301
|
+
if (manifest.files.some((file) => file.platform === "qoder" && file.path.startsWith(prefix)) || manifest.hook_registrations.some(
|
|
1302
|
+
(registration) => registration.platform === "qoder" && (registration.config_path === meta.hookConfigFile || registration.config_path.startsWith(prefix) || Boolean(registration.hook_path?.startsWith(prefix)))
|
|
1303
|
+
)) {
|
|
1304
|
+
covered.add(baseDir);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return covered;
|
|
1308
|
+
}
|
|
1309
|
+
function addHookConfigPrune(pruneHookConfigs, pruneHookCommands, filePath, managedHookPaths, managedHookCommands) {
|
|
1310
|
+
const existingPaths = pruneHookConfigs.get(filePath) ?? /* @__PURE__ */ new Set();
|
|
1311
|
+
for (const managedHookPath of managedHookPaths) {
|
|
1312
|
+
existingPaths.add(managedHookPath);
|
|
1313
|
+
}
|
|
1314
|
+
pruneHookConfigs.set(filePath, existingPaths);
|
|
1315
|
+
const existingCommands = pruneHookCommands.get(filePath) ?? /* @__PURE__ */ new Set();
|
|
1316
|
+
for (const managedHookCommand of managedHookCommands) {
|
|
1317
|
+
existingCommands.add(managedHookCommand);
|
|
1318
|
+
}
|
|
1319
|
+
pruneHookCommands.set(filePath, existingCommands);
|
|
1320
|
+
}
|
|
1321
|
+
function createClearPlan() {
|
|
1322
|
+
return {
|
|
1323
|
+
remove: /* @__PURE__ */ new Set(),
|
|
1324
|
+
removeFiles: [],
|
|
1325
|
+
skippedModified: [],
|
|
1326
|
+
pruneHookConfigs: /* @__PURE__ */ new Map(),
|
|
1327
|
+
pruneHookCommands: /* @__PURE__ */ new Map(),
|
|
1328
|
+
constraints: /* @__PURE__ */ new Set(),
|
|
1329
|
+
emptyDirs: /* @__PURE__ */ new Set()
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function finalizeClearPlan(plan) {
|
|
1333
|
+
return {
|
|
1334
|
+
remove: [...plan.remove],
|
|
1335
|
+
removeFiles: plan.removeFiles,
|
|
1336
|
+
skippedModified: plan.skippedModified,
|
|
1337
|
+
pruneHookConfigs: mergeHookConfigPrunes(plan.pruneHookConfigs, plan.pruneHookCommands),
|
|
1338
|
+
constraints: [...plan.constraints],
|
|
1339
|
+
emptyDirs: [...plan.emptyDirs]
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
function mergeHookConfigPrunes(pruneHookConfigs, pruneHookCommands) {
|
|
1343
|
+
const filePaths = /* @__PURE__ */ new Set([...pruneHookConfigs.keys(), ...pruneHookCommands.keys()]);
|
|
1344
|
+
return [...filePaths].map((filePath) => ({
|
|
1345
|
+
filePath,
|
|
1346
|
+
managedHookPaths: [...pruneHookConfigs.get(filePath) ?? /* @__PURE__ */ new Set()],
|
|
1347
|
+
managedHookCommands: [...pruneHookCommands.get(filePath) ?? /* @__PURE__ */ new Set()]
|
|
1348
|
+
}));
|
|
1349
|
+
}
|
|
1350
|
+
function mergeClearPlan(target, source) {
|
|
1351
|
+
target.remove = [.../* @__PURE__ */ new Set([...target.remove, ...source.remove])];
|
|
1352
|
+
target.removeFiles = dedupeFileRemovals([...target.removeFiles, ...source.removeFiles]);
|
|
1353
|
+
target.skippedModified = [.../* @__PURE__ */ new Set([...target.skippedModified, ...source.skippedModified])];
|
|
1354
|
+
target.constraints = [.../* @__PURE__ */ new Set([...target.constraints, ...source.constraints])];
|
|
1355
|
+
target.emptyDirs = [.../* @__PURE__ */ new Set([...target.emptyDirs, ...source.emptyDirs])];
|
|
1356
|
+
for (const sourcePrune of source.pruneHookConfigs) {
|
|
1357
|
+
const existing = target.pruneHookConfigs.find(
|
|
1358
|
+
(prune) => prune.filePath === sourcePrune.filePath
|
|
1359
|
+
);
|
|
1360
|
+
if (!existing) {
|
|
1361
|
+
target.pruneHookConfigs.push(sourcePrune);
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
existing.managedHookPaths = [
|
|
1365
|
+
.../* @__PURE__ */ new Set([...existing.managedHookPaths, ...sourcePrune.managedHookPaths])
|
|
1366
|
+
];
|
|
1367
|
+
existing.managedHookCommands = [
|
|
1368
|
+
.../* @__PURE__ */ new Set([...existing.managedHookCommands, ...sourcePrune.managedHookCommands])
|
|
1369
|
+
];
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function dedupeFileRemovals(removals) {
|
|
1373
|
+
const byPath2 = /* @__PURE__ */ new Map();
|
|
1374
|
+
for (const removal of removals) {
|
|
1375
|
+
byPath2.set(removal.filePath, removal);
|
|
1376
|
+
}
|
|
1377
|
+
return [...byPath2.values()];
|
|
1378
|
+
}
|
|
1379
|
+
function addRuntimeClearEntries(plan, cwd) {
|
|
1380
|
+
plan.remove = [
|
|
1381
|
+
.../* @__PURE__ */ new Set([
|
|
1382
|
+
...plan.remove,
|
|
1383
|
+
path14.join(cwd, EASY_CODING_DIR, CONFIG_FILE),
|
|
1384
|
+
path14.join(cwd, EASY_CODING_DIR, SESSIONS_DIR),
|
|
1385
|
+
path14.join(cwd, EASY_CODING_DIR, TEMPLATES_DIR),
|
|
1386
|
+
path14.join(cwd, EASY_CODING_DIR, INSTALL_MANIFEST_FILE)
|
|
1387
|
+
])
|
|
1388
|
+
];
|
|
1389
|
+
}
|
|
1390
|
+
function addEmptyDirChain(emptyDirs, startDir, cwd) {
|
|
1391
|
+
let current = startDir;
|
|
1392
|
+
while (current !== cwd && isInsideDirectory(cwd, current)) {
|
|
1393
|
+
emptyDirs.add(current);
|
|
1394
|
+
const parent = path14.dirname(current);
|
|
1395
|
+
if (parent === current) {
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
current = parent;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function isInsideDirectory(parent, child) {
|
|
1402
|
+
const relative = path14.relative(parent, child);
|
|
1403
|
+
return Boolean(relative) && !relative.startsWith("..") && !path14.isAbsolute(relative);
|
|
1404
|
+
}
|
|
1405
|
+
async function listSharedHookNamesForPlatform(platform) {
|
|
1406
|
+
const names = await listFileNames(getTemplatePath("shared-hooks"));
|
|
1407
|
+
if (PLATFORM_META[platform].hasSubagentContext) {
|
|
1408
|
+
return names;
|
|
1409
|
+
}
|
|
1410
|
+
return names.filter((name) => name !== "inject-subagent-context.py");
|
|
1411
|
+
}
|
|
1412
|
+
async function executeClearPlan(plan) {
|
|
1413
|
+
for (const target of plan.removeFiles) {
|
|
1414
|
+
if (await manifestFileMatches(target.filePath, target.expectedSha256)) {
|
|
1415
|
+
await rm(target.filePath, { force: true });
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
for (const target of plan.remove) {
|
|
1419
|
+
await rm(target, { recursive: true, force: true });
|
|
1420
|
+
}
|
|
1421
|
+
for (const config of plan.pruneHookConfigs) {
|
|
1422
|
+
await pruneHookConfig(config);
|
|
1423
|
+
}
|
|
1424
|
+
for (const constraint of plan.constraints) {
|
|
1425
|
+
await stripConstraintRegion(constraint);
|
|
1426
|
+
}
|
|
1427
|
+
for (const dir of sortDirsDeepestFirst(plan.emptyDirs)) {
|
|
1428
|
+
await removeIfEmpty(dir);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async function pruneHookConfig(config) {
|
|
1432
|
+
const { filePath, managedHookCommands, managedHookPaths } = config;
|
|
1433
|
+
const content = await readTextIfExists(filePath);
|
|
1434
|
+
if (content === null) {
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
let parsed;
|
|
1438
|
+
try {
|
|
1439
|
+
parsed = JSON.parse(content);
|
|
1440
|
+
} catch {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
const hooks = parsed.hooks;
|
|
1444
|
+
if (hooks && typeof hooks === "object" && !Array.isArray(hooks)) {
|
|
1445
|
+
const cleaned = pruneHookEvents(
|
|
1446
|
+
hooks,
|
|
1447
|
+
managedHookPaths,
|
|
1448
|
+
managedHookCommands
|
|
1449
|
+
);
|
|
1450
|
+
if (cleaned === null) {
|
|
1451
|
+
parsed = Object.fromEntries(Object.entries(parsed).filter(([key]) => key !== "hooks"));
|
|
1452
|
+
} else {
|
|
1453
|
+
parsed.hooks = cleaned;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (Object.keys(parsed).length === 0) {
|
|
1457
|
+
await rm(filePath, { force: true });
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
await writeFile(filePath, `${JSON.stringify(parsed, null, 2)}
|
|
1461
|
+
`, "utf8");
|
|
1462
|
+
}
|
|
1463
|
+
function pruneHookEvents(hooks, managedHookPaths, managedHookCommands) {
|
|
1464
|
+
const result = {};
|
|
1465
|
+
for (const [event, value] of Object.entries(hooks)) {
|
|
1466
|
+
if (!Array.isArray(value)) {
|
|
1467
|
+
result[event] = value;
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
const keptGroups = value.map((group) => pruneHookGroup(group, managedHookPaths, managedHookCommands)).filter((group) => group !== null);
|
|
1471
|
+
if (keptGroups.length > 0) {
|
|
1472
|
+
result[event] = keptGroups;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return Object.keys(result).length === 0 ? null : result;
|
|
1476
|
+
}
|
|
1477
|
+
function pruneHookGroup(group, managedHookPaths, managedHookCommands) {
|
|
1478
|
+
if (!group || typeof group !== "object") {
|
|
1479
|
+
return group;
|
|
1480
|
+
}
|
|
1481
|
+
const entry = group;
|
|
1482
|
+
if (!Array.isArray(entry.hooks)) {
|
|
1483
|
+
return group;
|
|
1484
|
+
}
|
|
1485
|
+
const keptHooks = entry.hooks.filter(
|
|
1486
|
+
(hook) => !isManagedHook(hook, managedHookPaths, managedHookCommands)
|
|
1487
|
+
);
|
|
1488
|
+
if (keptHooks.length === 0) {
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
return { ...group, hooks: keptHooks };
|
|
1492
|
+
}
|
|
1493
|
+
function isManagedHook(hook, managedHookPaths, managedHookCommands) {
|
|
1494
|
+
if (!hook || typeof hook !== "object") {
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
const command = hook.command;
|
|
1498
|
+
if (typeof command !== "string") {
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
const normalizedManagedCommands = managedHookCommands.map(normalizeCommand);
|
|
1502
|
+
if (normalizedManagedCommands.includes(normalizeCommand(command))) {
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
const normalizedCommand = command.replace(/\\/g, "/");
|
|
1506
|
+
return managedHookPaths.some(
|
|
1507
|
+
(hookPath) => commandContainsPathToken(normalizedCommand, hookPath.replace(/\\/g, "/"))
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
function commandContainsPathToken(command, targetPath) {
|
|
1511
|
+
const escaped = targetPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1512
|
+
return new RegExp(`(^|\\s|["'])${escaped}($|\\s|["'])`).test(command);
|
|
1513
|
+
}
|
|
1514
|
+
async function stripConstraintRegion(filePath) {
|
|
1515
|
+
const content = await readTextIfExists(filePath);
|
|
1516
|
+
if (content === null) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const stripped = removeMarkedRegion(content);
|
|
1520
|
+
if (stripped !== content) {
|
|
1521
|
+
await writeFile(filePath, stripped, "utf8");
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
async function removeIfEmpty(dir) {
|
|
1525
|
+
try {
|
|
1526
|
+
const entries = await readdir4(dir);
|
|
1527
|
+
if (entries.length === 0) {
|
|
1528
|
+
await rm(dir, { recursive: true, force: true });
|
|
1529
|
+
}
|
|
1530
|
+
} catch {
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
function sortDirsDeepestFirst(dirs) {
|
|
1534
|
+
return [...dirs].sort((left, right) => {
|
|
1535
|
+
const depthDiff = pathDepth(right) - pathDepth(left);
|
|
1536
|
+
return depthDiff === 0 ? left.localeCompare(right) : depthDiff;
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
function pathDepth(dir) {
|
|
1540
|
+
return path14.normalize(dir).split(path14.sep).filter(Boolean).length;
|
|
1541
|
+
}
|
|
1542
|
+
async function listDirNames(dir) {
|
|
1543
|
+
try {
|
|
1544
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
1545
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
1546
|
+
} catch {
|
|
1547
|
+
return [];
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
async function listFileNames(dir) {
|
|
1551
|
+
try {
|
|
1552
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
1553
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
1554
|
+
} catch {
|
|
1555
|
+
return [];
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
function renderPlan(cwd, agents, plan) {
|
|
1559
|
+
const rel = (target) => path14.relative(cwd, target) || target;
|
|
1560
|
+
const lines = [];
|
|
1561
|
+
lines.push(chalk3.bold("easy-coding clear"));
|
|
1562
|
+
lines.push(`Platforms: ${agents.length > 0 ? agents.join(", ") : "(none detected)"}`);
|
|
1563
|
+
lines.push("");
|
|
1564
|
+
lines.push("Will remove:");
|
|
1565
|
+
for (const target of plan.removeFiles) {
|
|
1566
|
+
lines.push(` - ${rel(target.filePath)}`);
|
|
1567
|
+
}
|
|
1568
|
+
for (const target of plan.remove) {
|
|
1569
|
+
lines.push(` - ${rel(target)}`);
|
|
1570
|
+
}
|
|
1571
|
+
if (plan.skippedModified.length > 0) {
|
|
1572
|
+
lines.push("Will keep modified manifest files:");
|
|
1573
|
+
for (const target of plan.skippedModified) {
|
|
1574
|
+
lines.push(` - ${rel(target)}`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (plan.constraints.length > 0) {
|
|
1578
|
+
lines.push("Will strip the generated region from (file kept):");
|
|
1579
|
+
for (const target of plan.constraints) {
|
|
1580
|
+
lines.push(` - ${rel(target)}`);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
if (plan.pruneHookConfigs.length > 0) {
|
|
1584
|
+
lines.push("Will prune harness hooks from (file kept if other config remains):");
|
|
1585
|
+
for (const target of plan.pruneHookConfigs) {
|
|
1586
|
+
lines.push(` - ${rel(target.filePath)}`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
lines.push("");
|
|
1590
|
+
lines.push(
|
|
1591
|
+
chalk3.dim(
|
|
1592
|
+
"Kept: .easy-coding/tasks, spec, memory, project.yaml, and knowledge files (SOUL/RULES/ABSTRACT/TEST_STRATEGY)."
|
|
1593
|
+
)
|
|
1594
|
+
);
|
|
1595
|
+
return lines.join("\n");
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/commands/init.ts
|
|
1599
|
+
import { note, outro as outro3 } from "@clack/prompts";
|
|
1600
|
+
import chalk4 from "chalk";
|
|
1601
|
+
|
|
1602
|
+
// src/utils/install-state.ts
|
|
1603
|
+
import { readdir as readdir5 } from "fs/promises";
|
|
1604
|
+
import path15 from "path";
|
|
1605
|
+
var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
|
|
1606
|
+
async function detectEasyCodingInstallState(cwd) {
|
|
1607
|
+
const easyCodingDir = path15.join(cwd, EASY_CODING_DIR);
|
|
1608
|
+
if (!await pathExists(easyCodingDir)) {
|
|
1609
|
+
return { kind: "fresh", easyCodingDir };
|
|
1610
|
+
}
|
|
1611
|
+
const configPath = path15.join(easyCodingDir, CONFIG_FILE);
|
|
1612
|
+
if (await pathExists(configPath)) {
|
|
1613
|
+
return { kind: "installed", easyCodingDir, configPath };
|
|
1614
|
+
}
|
|
1615
|
+
const legacyAssets = await detectLegacyAssets(easyCodingDir);
|
|
1616
|
+
if (legacyAssets.length === 0) {
|
|
1617
|
+
return { kind: "unknown", easyCodingDir };
|
|
1618
|
+
}
|
|
1619
|
+
return {
|
|
1620
|
+
kind: "legacy",
|
|
1621
|
+
easyCodingDir,
|
|
1622
|
+
legacyAssets,
|
|
1623
|
+
missingHarnessFiles: [relativeConfigPath(), relativeProjectInitTaskPath()]
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
async function detectLegacyAssets(easyCodingDir) {
|
|
1627
|
+
const assets = [];
|
|
1628
|
+
for (const file of LEGACY_ROOT_FILES) {
|
|
1629
|
+
if (await pathExists(path15.join(easyCodingDir, file))) {
|
|
1630
|
+
assets.push(relativeEasyCodingPath(file));
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
if (await pathExists(path15.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
|
|
1634
|
+
assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
|
|
1635
|
+
}
|
|
1636
|
+
const shortMemoryFiles = await listMarkdownFiles(path15.join(easyCodingDir, "memory", "short"));
|
|
1637
|
+
assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
|
|
1638
|
+
for (const dir of ["spec", "prototype"]) {
|
|
1639
|
+
if (await hasAnyDirectoryEntry(path15.join(easyCodingDir, dir))) {
|
|
1640
|
+
assets.push(relativeEasyCodingPath(dir));
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return assets.sort();
|
|
1644
|
+
}
|
|
1645
|
+
async function listMarkdownFiles(dir) {
|
|
1646
|
+
if (!await isDirectory(dir)) {
|
|
1647
|
+
return [];
|
|
1648
|
+
}
|
|
1649
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
1650
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
|
|
1651
|
+
}
|
|
1652
|
+
async function hasAnyDirectoryEntry(dir) {
|
|
1653
|
+
if (!await isDirectory(dir)) {
|
|
1654
|
+
return false;
|
|
1655
|
+
}
|
|
1656
|
+
return (await readdir5(dir)).length > 0;
|
|
1657
|
+
}
|
|
1658
|
+
function relativeEasyCodingPath(...segments) {
|
|
1659
|
+
return path15.posix.join(EASY_CODING_DIR, ...segments);
|
|
1660
|
+
}
|
|
1661
|
+
function relativeConfigPath() {
|
|
1662
|
+
return path15.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
1663
|
+
}
|
|
1664
|
+
function relativeProjectInitTaskPath() {
|
|
1665
|
+
return path15.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
|
|
1666
|
+
}
|
|
1667
|
+
|
|
781
1668
|
// src/commands/init.ts
|
|
782
1669
|
async function init(opts) {
|
|
783
1670
|
renderBanner();
|
|
@@ -794,31 +1681,37 @@ async function init(opts) {
|
|
|
794
1681
|
);
|
|
795
1682
|
}
|
|
796
1683
|
const platforms = await resolvePlatforms(opts, ["claude-code"]);
|
|
1684
|
+
const artifacts = [];
|
|
797
1685
|
for (const platform of platforms) {
|
|
798
|
-
await CONFIGURATORS[platform](cwd);
|
|
1686
|
+
artifacts.push(...await CONFIGURATORS[platform](cwd));
|
|
799
1687
|
}
|
|
800
1688
|
await writeRuntimeScaffold(cwd, platforms);
|
|
1689
|
+
await writeInstallManifest(cwd, {
|
|
1690
|
+
harnessVersion: VERSION,
|
|
1691
|
+
agents: platforms,
|
|
1692
|
+
artifacts
|
|
1693
|
+
});
|
|
801
1694
|
await writeProjectInitTask(cwd, platforms, {
|
|
802
1695
|
initSource: installState.kind === "legacy" ? "legacy-easy-coding" : "fresh",
|
|
803
1696
|
legacyAssets: installState.kind === "legacy" ? installState.legacyAssets : void 0,
|
|
804
1697
|
legacyMissingHarnessFiles: installState.kind === "legacy" ? installState.missingHarnessFiles : void 0
|
|
805
1698
|
});
|
|
806
|
-
await
|
|
1699
|
+
await ensureEasyCodingSessionsIgnored(cwd);
|
|
807
1700
|
const triggers = platforms.map(
|
|
808
1701
|
(platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
|
|
809
1702
|
).join("\n");
|
|
810
1703
|
note(triggers, "Next step");
|
|
811
|
-
|
|
1704
|
+
outro3(chalk4.green("easy-coding harness installed. Open your agent and run ec-init."));
|
|
812
1705
|
}
|
|
813
1706
|
|
|
814
1707
|
// src/commands/status.ts
|
|
815
|
-
import
|
|
816
|
-
import
|
|
1708
|
+
import path18 from "path";
|
|
1709
|
+
import chalk5 from "chalk";
|
|
817
1710
|
|
|
818
1711
|
// src/utils/compare-versions.ts
|
|
819
|
-
import
|
|
1712
|
+
import path16 from "path";
|
|
820
1713
|
function normalize(version) {
|
|
821
|
-
const [core] = version.split("-");
|
|
1714
|
+
const [core] = String(version ?? "").split("-");
|
|
822
1715
|
return core.split(".").map((part) => {
|
|
823
1716
|
const parsed = Number.parseInt(part, 10);
|
|
824
1717
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
@@ -840,11 +1733,16 @@ function isVersionBehind(installed, current = VERSION) {
|
|
|
840
1733
|
return compareVersions(installed, current) === -1;
|
|
841
1734
|
}
|
|
842
1735
|
async function checkForUpgrade(cwd) {
|
|
843
|
-
const configPath =
|
|
1736
|
+
const configPath = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
844
1737
|
if (!await pathExists(configPath)) {
|
|
845
1738
|
return;
|
|
846
1739
|
}
|
|
847
|
-
|
|
1740
|
+
let config;
|
|
1741
|
+
try {
|
|
1742
|
+
config = await readConfigYaml(configPath);
|
|
1743
|
+
} catch {
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
848
1746
|
const installed = String(config.harness_version ?? "");
|
|
849
1747
|
if (installed && isVersionBehind(installed)) {
|
|
850
1748
|
process.stderr.write(
|
|
@@ -854,13 +1752,19 @@ async function checkForUpgrade(cwd) {
|
|
|
854
1752
|
}
|
|
855
1753
|
}
|
|
856
1754
|
|
|
857
|
-
// src/utils/
|
|
858
|
-
import
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1755
|
+
// src/utils/session.ts
|
|
1756
|
+
import { readdir as readdir6 } from "fs/promises";
|
|
1757
|
+
import path17 from "path";
|
|
1758
|
+
var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
1759
|
+
function getSessionDir(cwd) {
|
|
1760
|
+
return path17.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
1761
|
+
}
|
|
1762
|
+
function getSessionFilePath(cwd, ppid) {
|
|
1763
|
+
const pid = ppid ?? process.ppid;
|
|
1764
|
+
return path17.join(getSessionDir(cwd), `${pid}.json`);
|
|
1765
|
+
}
|
|
1766
|
+
async function readSessionFile(cwd, ppid) {
|
|
1767
|
+
const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
|
|
864
1768
|
if (!content) {
|
|
865
1769
|
return null;
|
|
866
1770
|
}
|
|
@@ -871,7 +1775,7 @@ async function readStateJson(cwd) {
|
|
|
871
1775
|
async function status() {
|
|
872
1776
|
renderBanner();
|
|
873
1777
|
const cwd = process.cwd();
|
|
874
|
-
const configPath =
|
|
1778
|
+
const configPath = path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
875
1779
|
if (!await pathExists(configPath)) {
|
|
876
1780
|
throw new Error("No easy-coding harness found in this project.");
|
|
877
1781
|
}
|
|
@@ -879,34 +1783,40 @@ async function status() {
|
|
|
879
1783
|
const tasks = await listTasks(cwd);
|
|
880
1784
|
const taskCounts = summarizeTaskStatuses(tasks);
|
|
881
1785
|
const activeTasks = tasks.filter((item) => isActiveTask(item.task));
|
|
882
|
-
const
|
|
1786
|
+
const session = await readSessionFile(cwd);
|
|
883
1787
|
const versionRelation = compareVersions(config.harness_version, VERSION);
|
|
884
|
-
console.log(
|
|
1788
|
+
console.log(chalk5.bold("Harness"));
|
|
885
1789
|
console.log(` version: ${config.harness_version}`);
|
|
886
1790
|
console.log(` cli: ${VERSION}`);
|
|
887
1791
|
if (versionRelation === -1) {
|
|
888
|
-
console.log(
|
|
1792
|
+
console.log(chalk5.yellow(" upgrade: available"));
|
|
889
1793
|
} else if (versionRelation === 1) {
|
|
890
|
-
console.log(
|
|
1794
|
+
console.log(chalk5.red(" upgrade: CLI is older than project harness"));
|
|
891
1795
|
} else {
|
|
892
1796
|
console.log(" upgrade: up to date");
|
|
893
1797
|
}
|
|
894
1798
|
console.log(` agents: ${config.agents.join(", ") || "(none)"}`);
|
|
895
|
-
console.log(` project: ${config.project.name}
|
|
1799
|
+
console.log(` project: ${config.project.name}`);
|
|
896
1800
|
console.log("");
|
|
897
|
-
console.log(
|
|
898
|
-
if (
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1801
|
+
console.log(chalk5.bold("Session"));
|
|
1802
|
+
if (session?.current_task) {
|
|
1803
|
+
const taskPath = getTaskJsonPath(cwd, session.current_task);
|
|
1804
|
+
if (await pathExists(taskPath)) {
|
|
1805
|
+
const task = await readTaskJson(taskPath);
|
|
1806
|
+
console.log(` current_task: ${session.current_task}`);
|
|
1807
|
+
console.log(` current_stage: ${task.status}`);
|
|
1808
|
+
console.log(` last_agent: ${task.last_agent}`);
|
|
1809
|
+
} else {
|
|
1810
|
+
console.log(` current_task: ${session.current_task} (task.json missing)`);
|
|
1811
|
+
}
|
|
902
1812
|
} else {
|
|
903
|
-
console.log("
|
|
1813
|
+
console.log(" no active session");
|
|
904
1814
|
}
|
|
905
1815
|
console.log("");
|
|
906
|
-
console.log(
|
|
1816
|
+
console.log(chalk5.bold("Tasks"));
|
|
907
1817
|
console.log(` total: ${tasks.length}`);
|
|
908
|
-
for (const [
|
|
909
|
-
console.log(` ${
|
|
1818
|
+
for (const [taskStatus, count] of Object.entries(taskCounts)) {
|
|
1819
|
+
console.log(` ${taskStatus}: ${count}`);
|
|
910
1820
|
}
|
|
911
1821
|
if (activeTasks.length > 0) {
|
|
912
1822
|
console.log(" active:");
|
|
@@ -918,52 +1828,69 @@ async function status() {
|
|
|
918
1828
|
}
|
|
919
1829
|
|
|
920
1830
|
// src/commands/upgrade.ts
|
|
921
|
-
import
|
|
922
|
-
import { cancel as
|
|
923
|
-
import
|
|
1831
|
+
import path19 from "path";
|
|
1832
|
+
import { cancel as cancel3, confirm as confirm3, outro as outro4 } from "@clack/prompts";
|
|
1833
|
+
import chalk6 from "chalk";
|
|
924
1834
|
async function upgrade(opts) {
|
|
925
1835
|
renderBanner();
|
|
926
1836
|
const cwd = process.cwd();
|
|
927
|
-
const configPath =
|
|
1837
|
+
const configPath = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
928
1838
|
if (!await pathExists(configPath)) {
|
|
929
1839
|
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
930
1840
|
}
|
|
931
1841
|
const config = await readConfigYaml(configPath);
|
|
932
|
-
const
|
|
1842
|
+
const installedVersion = String(config.harness_version ?? "");
|
|
1843
|
+
const hasAgents = Array.isArray(config.agents) && config.agents.length > 0;
|
|
1844
|
+
if (!installedVersion || !hasAgents) {
|
|
1845
|
+
throw new Error(
|
|
1846
|
+
"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."
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
const relation = compareVersions(installedVersion, VERSION);
|
|
933
1850
|
if (relation === 0) {
|
|
934
|
-
|
|
1851
|
+
outro4(chalk6.green(`easy-coding harness is already up to date (${VERSION}).`));
|
|
935
1852
|
return;
|
|
936
1853
|
}
|
|
937
1854
|
if (relation === 1) {
|
|
938
1855
|
throw new Error(
|
|
939
|
-
`Project harness version ${
|
|
1856
|
+
`Project harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
940
1857
|
);
|
|
941
1858
|
}
|
|
942
1859
|
const summary = [
|
|
943
|
-
`Upgrade: ${
|
|
1860
|
+
`Upgrade: ${installedVersion || "unknown"} -> ${VERSION}`,
|
|
944
1861
|
`Installed agents: ${config.agents.join(", ")}`,
|
|
945
|
-
"Will overwrite managed skills, hooks, agents, and generated main-constraint regions.",
|
|
946
|
-
"Will
|
|
1862
|
+
"Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
|
|
1863
|
+
"Will update project-init task to recommend ec-init re-run for version adaptation.",
|
|
1864
|
+
"Will not touch memory, state, spec, or project knowledge files."
|
|
947
1865
|
].join("\n");
|
|
948
1866
|
if (opts.dryRun) {
|
|
949
1867
|
console.log(summary);
|
|
950
1868
|
return;
|
|
951
1869
|
}
|
|
952
1870
|
if (!opts.yes) {
|
|
953
|
-
const shouldUpgrade = await
|
|
1871
|
+
const shouldUpgrade = await confirm3({
|
|
954
1872
|
message: "Apply this harness upgrade?",
|
|
955
1873
|
initialValue: true
|
|
956
1874
|
});
|
|
957
1875
|
if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
|
|
958
|
-
|
|
1876
|
+
cancel3("Upgrade cancelled.");
|
|
959
1877
|
return;
|
|
960
1878
|
}
|
|
961
1879
|
}
|
|
1880
|
+
const artifacts = [];
|
|
962
1881
|
for (const platform of config.agents) {
|
|
963
|
-
await CONFIGURATORS[platform](cwd);
|
|
1882
|
+
artifacts.push(...await CONFIGURATORS[platform](cwd));
|
|
964
1883
|
}
|
|
1884
|
+
await writeRuntimeScaffold(cwd, config.agents);
|
|
1885
|
+
await writeInstallManifest(cwd, {
|
|
1886
|
+
harnessVersion: VERSION,
|
|
1887
|
+
agents: config.agents,
|
|
1888
|
+
artifacts
|
|
1889
|
+
});
|
|
1890
|
+
await ensureEasyCodingSessionsIgnored(cwd);
|
|
965
1891
|
await updateHarnessVersion(configPath, VERSION);
|
|
966
|
-
|
|
1892
|
+
await setPendingInitSince(cwd, VERSION);
|
|
1893
|
+
outro4(chalk6.green(`easy-coding harness upgraded to ${VERSION}.`));
|
|
967
1894
|
}
|
|
968
1895
|
|
|
969
1896
|
// src/cli.ts
|
|
@@ -972,7 +1899,7 @@ function withErrorHandling(fn) {
|
|
|
972
1899
|
try {
|
|
973
1900
|
await fn(opts);
|
|
974
1901
|
} catch (error) {
|
|
975
|
-
console.error(
|
|
1902
|
+
console.error(chalk7.red("Error:"), error instanceof Error ? error.message : error);
|
|
976
1903
|
if (process.env.EC_DEBUG && error instanceof Error) {
|
|
977
1904
|
console.error(error.stack);
|
|
978
1905
|
}
|
|
@@ -987,5 +1914,6 @@ program.command("init").description("Initialize easy-coding harness in current p
|
|
|
987
1914
|
program.command("add-agent").description("Add agent platform support to an existing project").option("--agent <list>", "Comma-separated platforms to add").action(withErrorHandling(addAgent));
|
|
988
1915
|
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));
|
|
989
1916
|
program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
|
|
1917
|
+
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));
|
|
990
1918
|
program.parse();
|
|
991
1919
|
//# sourceMappingURL=cli.js.map
|