easy-coding-harness 0.1.8 → 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 -78
- package/dist/cli.js +1187 -282
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
- 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-memory/SKILL.md +25 -8
- 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 +10 -4
- package/templates/common/skills/ec-workflow/SKILL.md +43 -16
- package/templates/main-constraint/AGENTS.md.tpl +16 -3
- package/templates/main-constraint/CLAUDE.md.tpl +16 -3
- 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 +72 -81
- package/templates/shared-hooks/inject-subagent-context.py +5 -12
- package/templates/shared-hooks/inject-workflow-state.py +2 -1
- package/templates/shared-hooks/session-start.py +2 -1
- 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";
|
|
@@ -88,13 +88,15 @@ 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";
|
|
99
|
+
var INSTALL_MANIFEST_FILE = "install-manifest.json";
|
|
98
100
|
var SESSIONS_DIR = "sessions";
|
|
99
101
|
var TASKS_DIR = "tasks";
|
|
100
102
|
var PROJECT_INIT_TASK_ID = "project-init";
|
|
@@ -102,6 +104,7 @@ var MEMORY_DIR = "memory";
|
|
|
102
104
|
var SPEC_DIR = "spec";
|
|
103
105
|
var MAIN_SPEC_DIR = "main";
|
|
104
106
|
var DEV_SPEC_DIR = "dev";
|
|
107
|
+
var TEMPLATES_DIR = "templates";
|
|
105
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 -->";
|
|
@@ -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,7 +500,7 @@ 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
505
|
if (current) {
|
|
295
506
|
const markerContent = extractMarkedRegion(generated);
|
|
@@ -300,23 +511,27 @@ async function writeMainConstraint(cwd, platform) {
|
|
|
300
511
|
} else {
|
|
301
512
|
await writeTextFile(destination, generated);
|
|
302
513
|
}
|
|
514
|
+
return destination;
|
|
303
515
|
}
|
|
304
516
|
async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__PURE__ */ new Set()) {
|
|
305
517
|
await ensureDir(destination);
|
|
518
|
+
const written = [];
|
|
306
519
|
for (const entry of (await readdir(source)).sort()) {
|
|
307
520
|
if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
|
|
308
521
|
continue;
|
|
309
522
|
}
|
|
310
|
-
const sourcePath =
|
|
311
|
-
const destinationPath =
|
|
523
|
+
const sourcePath = path4.join(source, entry);
|
|
524
|
+
const destinationPath = path4.join(destination, stripTemplateExtension(entry));
|
|
312
525
|
const sourceStat = await stat2(sourcePath);
|
|
313
526
|
if (sourceStat.isDirectory()) {
|
|
314
|
-
await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs);
|
|
527
|
+
written.push(...await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs));
|
|
315
528
|
continue;
|
|
316
529
|
}
|
|
317
530
|
const content = resolvePlaceholders(await readTextFile(sourcePath), ctx, sourcePath);
|
|
318
531
|
await writeTextFile(destinationPath, content);
|
|
532
|
+
written.push(destinationPath);
|
|
319
533
|
}
|
|
534
|
+
return written;
|
|
320
535
|
}
|
|
321
536
|
function shouldSkipTemplateEntry(entry) {
|
|
322
537
|
return entry === ".DS_Store" || entry === "__pycache__" || entry.endsWith(".ts") || entry.endsWith(".js") || entry.endsWith(".map");
|
|
@@ -330,69 +545,194 @@ async function configureClaude(cwd) {
|
|
|
330
545
|
const platform = "claude-code";
|
|
331
546
|
const meta = PLATFORM_META[platform];
|
|
332
547
|
const ctx = meta.templateContext;
|
|
333
|
-
const dest =
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
await
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
+
)
|
|
566
|
+
);
|
|
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))
|
|
340
573
|
);
|
|
341
|
-
await writeMainConstraint(cwd, platform);
|
|
574
|
+
artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
|
|
575
|
+
return artifacts;
|
|
342
576
|
}
|
|
343
577
|
|
|
344
578
|
// src/configurators/codex.ts
|
|
345
|
-
import
|
|
579
|
+
import path6 from "path";
|
|
346
580
|
async function configureCodex(cwd) {
|
|
347
581
|
const platform = "codex";
|
|
348
582
|
const meta = PLATFORM_META[platform];
|
|
349
583
|
const ctx = meta.templateContext;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
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))
|
|
354
592
|
);
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
593
|
+
artifacts.push(
|
|
594
|
+
...(await writeSharedHooks(path6.join(cwd, meta.hooksDir), platform, {
|
|
595
|
+
skipSubagentContext: true
|
|
596
|
+
})).map((filePath) => fileArtifact(filePath, "hook", platform))
|
|
597
|
+
);
|
|
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;
|
|
358
616
|
}
|
|
359
617
|
|
|
360
618
|
// src/configurators/qoder.ts
|
|
361
|
-
import {
|
|
362
|
-
import
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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;
|
|
366
664
|
try {
|
|
367
|
-
|
|
368
|
-
} catch {
|
|
369
|
-
|
|
665
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
666
|
+
} catch (error) {
|
|
667
|
+
if (error.code === "ENOENT") {
|
|
668
|
+
return [];
|
|
669
|
+
}
|
|
670
|
+
throw error;
|
|
370
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
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return artifacts;
|
|
371
693
|
}
|
|
372
694
|
async function configureQoder(cwd) {
|
|
373
695
|
const platform = "qoder";
|
|
374
|
-
const
|
|
375
|
-
const ctx =
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
)
|
|
387
732
|
);
|
|
388
733
|
}
|
|
389
|
-
await writeMainConstraint(cwd, platform);
|
|
390
|
-
|
|
391
|
-
function detectQoderCnVariant(cwd) {
|
|
392
|
-
if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
|
|
393
|
-
return true;
|
|
394
|
-
}
|
|
395
|
-
return existsSync2(path6.join(cwd, ".qodercn"));
|
|
734
|
+
artifacts.push(constraintRegionArtifact(await writeMainConstraint(cwd, platform), platform));
|
|
735
|
+
return artifacts;
|
|
396
736
|
}
|
|
397
737
|
|
|
398
738
|
// src/configurators/index.ts
|
|
@@ -402,23 +742,19 @@ var CONFIGURATORS = {
|
|
|
402
742
|
qoder: configureQoder
|
|
403
743
|
};
|
|
404
744
|
|
|
405
|
-
// src/ui/banner.ts
|
|
406
|
-
import chalk from "chalk";
|
|
407
|
-
import figlet from "figlet";
|
|
408
|
-
|
|
409
745
|
// src/constants/version.ts
|
|
410
746
|
import { readFileSync } from "fs";
|
|
411
|
-
import
|
|
747
|
+
import path9 from "path";
|
|
412
748
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
413
749
|
var packageMetadata = readPackageMetadata();
|
|
414
750
|
var PACKAGE_NAME = packageMetadata.name;
|
|
415
751
|
var VERSION = packageMetadata.version;
|
|
416
752
|
function readPackageMetadata() {
|
|
417
|
-
const here =
|
|
753
|
+
const here = path9.dirname(fileURLToPath2(import.meta.url));
|
|
418
754
|
const candidates = [
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
755
|
+
path9.resolve(here, "../../package.json"),
|
|
756
|
+
path9.resolve(here, "../package.json"),
|
|
757
|
+
path9.resolve(process.cwd(), "package.json")
|
|
422
758
|
];
|
|
423
759
|
for (const candidate of candidates) {
|
|
424
760
|
try {
|
|
@@ -433,6 +769,8 @@ function readPackageMetadata() {
|
|
|
433
769
|
}
|
|
434
770
|
|
|
435
771
|
// src/ui/banner.ts
|
|
772
|
+
import chalk from "chalk";
|
|
773
|
+
import figlet from "figlet";
|
|
436
774
|
var WIDE_TERMINAL_WIDTH = 88;
|
|
437
775
|
var MEDIUM_TERMINAL_WIDTH = 72;
|
|
438
776
|
function renderBanner() {
|
|
@@ -457,7 +795,7 @@ function colorizeBanner(art) {
|
|
|
457
795
|
}
|
|
458
796
|
|
|
459
797
|
// src/utils/config-yaml.ts
|
|
460
|
-
import { readFile as
|
|
798
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
461
799
|
import YAML, { isScalar, isSeq, parseDocument } from "yaml";
|
|
462
800
|
function createDefaultConfig(params) {
|
|
463
801
|
return {
|
|
@@ -465,18 +803,13 @@ function createDefaultConfig(params) {
|
|
|
465
803
|
harness_version: params.harnessVersion,
|
|
466
804
|
agents: params.agents,
|
|
467
805
|
project: {
|
|
468
|
-
name: params.projectName
|
|
469
|
-
mode: "auto"
|
|
806
|
+
name: params.projectName
|
|
470
807
|
},
|
|
471
808
|
memory: {
|
|
472
809
|
short_term_max: 10,
|
|
473
810
|
short_term_keep: 5,
|
|
474
811
|
schema_version: 2
|
|
475
812
|
},
|
|
476
|
-
test: {
|
|
477
|
-
framework: "auto",
|
|
478
|
-
command: ""
|
|
479
|
-
},
|
|
480
813
|
tasks: {
|
|
481
814
|
auto_archive_days: 30
|
|
482
815
|
},
|
|
@@ -493,11 +826,11 @@ async function writeConfigYaml(filePath, config) {
|
|
|
493
826
|
await writeTextFile(filePath, stringifyConfig(config));
|
|
494
827
|
}
|
|
495
828
|
async function readConfigYaml(filePath) {
|
|
496
|
-
const content = await
|
|
829
|
+
const content = await readFile3(filePath, "utf8");
|
|
497
830
|
return YAML.parse(content);
|
|
498
831
|
}
|
|
499
832
|
async function updateConfigYaml(filePath, updater) {
|
|
500
|
-
const content = await
|
|
833
|
+
const content = await readFile3(filePath, "utf8");
|
|
501
834
|
const document = parseDocument(content);
|
|
502
835
|
const config = document.toJSON();
|
|
503
836
|
updater(config);
|
|
@@ -519,87 +852,10 @@ async function updateHarnessVersion(filePath, version) {
|
|
|
519
852
|
});
|
|
520
853
|
}
|
|
521
854
|
|
|
522
|
-
// src/commands/platforms.ts
|
|
523
|
-
import { cancel, confirm, multiselect } from "@clack/prompts";
|
|
524
|
-
function parseAgentList(agentList) {
|
|
525
|
-
const values = agentList.split(",").map((value) => value.trim()).filter(Boolean);
|
|
526
|
-
if (values.length === 0) {
|
|
527
|
-
throw new Error("No agent platform specified.");
|
|
528
|
-
}
|
|
529
|
-
const invalid = values.filter((value) => !isAgentPlatform(value));
|
|
530
|
-
if (invalid.length > 0) {
|
|
531
|
-
throw new Error(`Unknown agent platform: ${invalid.join(", ")}`);
|
|
532
|
-
}
|
|
533
|
-
return [...new Set(values)];
|
|
534
|
-
}
|
|
535
|
-
async function resolvePlatforms(opts, defaults = ["claude-code"]) {
|
|
536
|
-
if (opts.agent) {
|
|
537
|
-
return parseAgentList(opts.agent);
|
|
538
|
-
}
|
|
539
|
-
if (opts.yes) {
|
|
540
|
-
return defaults;
|
|
541
|
-
}
|
|
542
|
-
let selectedDefaults = defaults;
|
|
543
|
-
while (true) {
|
|
544
|
-
const result = await multiselect({
|
|
545
|
-
message: "Select agent platforms (Space to toggle, Enter to review)",
|
|
546
|
-
options: AGENT_PLATFORMS.map((platform) => ({
|
|
547
|
-
label: PLATFORM_META[platform].label,
|
|
548
|
-
value: platform
|
|
549
|
-
})),
|
|
550
|
-
initialValues: selectedDefaults,
|
|
551
|
-
required: true
|
|
552
|
-
});
|
|
553
|
-
if (typeof result === "symbol") {
|
|
554
|
-
cancel("Platform selection cancelled.");
|
|
555
|
-
process.exit(1);
|
|
556
|
-
}
|
|
557
|
-
const labels = result.map((platform) => PLATFORM_META[platform].label).join(", ");
|
|
558
|
-
const shouldInstall = await confirm({
|
|
559
|
-
message: `Install Easy Coding for: ${labels}?`,
|
|
560
|
-
initialValue: true
|
|
561
|
-
});
|
|
562
|
-
if (typeof shouldInstall === "symbol") {
|
|
563
|
-
cancel("Platform selection cancelled.");
|
|
564
|
-
process.exit(1);
|
|
565
|
-
}
|
|
566
|
-
if (shouldInstall) {
|
|
567
|
-
return result;
|
|
568
|
-
}
|
|
569
|
-
selectedDefaults = result;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// src/commands/add-agent.ts
|
|
574
|
-
async function addAgent(opts) {
|
|
575
|
-
renderBanner();
|
|
576
|
-
const cwd = process.cwd();
|
|
577
|
-
const configPath = path8.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
578
|
-
if (!await pathExists(configPath)) {
|
|
579
|
-
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
580
|
-
}
|
|
581
|
-
const config = await readConfigYaml(configPath);
|
|
582
|
-
const platforms = await resolvePlatforms(opts, ["claude-code"]);
|
|
583
|
-
const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
|
|
584
|
-
if (toInstall.length === 0) {
|
|
585
|
-
outro(chalk2.yellow("All selected agent platforms are already installed."));
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
for (const platform of toInstall) {
|
|
589
|
-
await CONFIGURATORS[platform](cwd);
|
|
590
|
-
}
|
|
591
|
-
await addAgentsToConfig(configPath, toInstall);
|
|
592
|
-
outro(chalk2.green(`Added agent platforms: ${toInstall.join(", ")}`));
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// src/commands/init.ts
|
|
596
|
-
import { note, outro as outro2 } from "@clack/prompts";
|
|
597
|
-
import chalk3 from "chalk";
|
|
598
|
-
|
|
599
855
|
// src/utils/gitignore.ts
|
|
600
|
-
import
|
|
856
|
+
import path10 from "path";
|
|
601
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") {
|
|
602
|
-
const gitignorePath =
|
|
858
|
+
const gitignorePath = path10.join(cwd, ".gitignore");
|
|
603
859
|
const current = await readTextIfExists(gitignorePath) ?? "";
|
|
604
860
|
const lines = current.split(/\r?\n/).map((line) => line.trim());
|
|
605
861
|
if (lines.includes(entry)) {
|
|
@@ -614,90 +870,32 @@ async function ensureEasyCodingSessionsIgnored(cwd) {
|
|
|
614
870
|
return ensureGitignoreEntry(cwd, SESSIONS_GITIGNORE_ENTRY);
|
|
615
871
|
}
|
|
616
872
|
|
|
617
|
-
// src/utils/
|
|
618
|
-
import
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
if (!await pathExists(
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
const legacyAssets = await detectLegacyAssets(easyCodingDir);
|
|
631
|
-
if (legacyAssets.length === 0) {
|
|
632
|
-
return { kind: "unknown", easyCodingDir };
|
|
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
|
+
);
|
|
633
885
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
886
|
+
await ensureDir(path11.join(easyCodingDir, "tasks"));
|
|
887
|
+
await ensureDir(path11.join(easyCodingDir, SESSIONS_DIR));
|
|
888
|
+
await ensureDir(path11.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
|
|
889
|
+
await ensureDir(path11.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
890
|
+
await writeMemoryScaffold(easyCodingDir);
|
|
891
|
+
await writeTemplatesScaffold(easyCodingDir);
|
|
640
892
|
}
|
|
641
|
-
async function
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
if (await pathExists(path10.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
|
|
649
|
-
assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
|
|
650
|
-
}
|
|
651
|
-
const shortMemoryFiles = await listMarkdownFiles(path10.join(easyCodingDir, "memory", "short"));
|
|
652
|
-
assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
|
|
653
|
-
for (const dir of ["spec", "prototype"]) {
|
|
654
|
-
if (await hasAnyDirectoryEntry(path10.join(easyCodingDir, dir))) {
|
|
655
|
-
assets.push(relativeEasyCodingPath(dir));
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
return assets.sort();
|
|
659
|
-
}
|
|
660
|
-
async function listMarkdownFiles(dir) {
|
|
661
|
-
if (!await isDirectory(dir)) {
|
|
662
|
-
return [];
|
|
663
|
-
}
|
|
664
|
-
const entries = await readdir2(dir, { withFileTypes: true });
|
|
665
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
|
|
666
|
-
}
|
|
667
|
-
async function hasAnyDirectoryEntry(dir) {
|
|
668
|
-
if (!await isDirectory(dir)) {
|
|
669
|
-
return false;
|
|
670
|
-
}
|
|
671
|
-
return (await readdir2(dir)).length > 0;
|
|
672
|
-
}
|
|
673
|
-
function relativeEasyCodingPath(...segments) {
|
|
674
|
-
return path10.posix.join(EASY_CODING_DIR, ...segments);
|
|
675
|
-
}
|
|
676
|
-
function relativeConfigPath() {
|
|
677
|
-
return path10.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
678
|
-
}
|
|
679
|
-
function relativeProjectInitTaskPath() {
|
|
680
|
-
return path10.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// src/utils/runtime-scaffold.ts
|
|
684
|
-
import path11 from "path";
|
|
685
|
-
async function writeRuntimeScaffold(cwd, agents) {
|
|
686
|
-
const easyCodingDir = path11.join(cwd, EASY_CODING_DIR);
|
|
687
|
-
await ensureDir(easyCodingDir);
|
|
688
|
-
const configPath = path11.join(easyCodingDir, CONFIG_FILE);
|
|
689
|
-
if (!await pathExists(configPath)) {
|
|
690
|
-
const projectName = path11.basename(cwd);
|
|
691
|
-
await writeConfigYaml(
|
|
692
|
-
configPath,
|
|
693
|
-
createDefaultConfig({ projectName, harnessVersion: VERSION, agents })
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
await ensureDir(path11.join(easyCodingDir, "tasks"));
|
|
697
|
-
await ensureDir(path11.join(easyCodingDir, SESSIONS_DIR));
|
|
698
|
-
await ensureDir(path11.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
|
|
699
|
-
await ensureDir(path11.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
700
|
-
await writeMemoryScaffold(easyCodingDir);
|
|
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));
|
|
701
899
|
}
|
|
702
900
|
async function writeMemoryScaffold(easyCodingDir) {
|
|
703
901
|
const memoryDir = path11.join(easyCodingDir, MEMORY_DIR);
|
|
@@ -711,6 +909,11 @@ async function writeMemoryScaffold(easyCodingDir) {
|
|
|
711
909
|
const templatePath = getTemplatePath("runtime", "memory", "long", file);
|
|
712
910
|
await writeTextFile(destination, await readTextFile(templatePath));
|
|
713
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
|
+
}
|
|
714
917
|
}
|
|
715
918
|
|
|
716
919
|
// src/utils/task-json.ts
|
|
@@ -756,6 +959,14 @@ async function writeProjectInitTask(cwd, agents, options = {}) {
|
|
|
756
959
|
})
|
|
757
960
|
);
|
|
758
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
|
+
}
|
|
759
970
|
async function listTasks(cwd) {
|
|
760
971
|
const tasksDir = path12.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
761
972
|
if (!await pathExists(tasksDir)) {
|
|
@@ -788,6 +999,672 @@ function isActiveTask(task) {
|
|
|
788
999
|
return task.status !== "COMPLETE" && task.status !== "CLOSED";
|
|
789
1000
|
}
|
|
790
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
|
+
|
|
791
1668
|
// src/commands/init.ts
|
|
792
1669
|
async function init(opts) {
|
|
793
1670
|
renderBanner();
|
|
@@ -804,10 +1681,16 @@ async function init(opts) {
|
|
|
804
1681
|
);
|
|
805
1682
|
}
|
|
806
1683
|
const platforms = await resolvePlatforms(opts, ["claude-code"]);
|
|
1684
|
+
const artifacts = [];
|
|
807
1685
|
for (const platform of platforms) {
|
|
808
|
-
await CONFIGURATORS[platform](cwd);
|
|
1686
|
+
artifacts.push(...await CONFIGURATORS[platform](cwd));
|
|
809
1687
|
}
|
|
810
1688
|
await writeRuntimeScaffold(cwd, platforms);
|
|
1689
|
+
await writeInstallManifest(cwd, {
|
|
1690
|
+
harnessVersion: VERSION,
|
|
1691
|
+
agents: platforms,
|
|
1692
|
+
artifacts
|
|
1693
|
+
});
|
|
811
1694
|
await writeProjectInitTask(cwd, platforms, {
|
|
812
1695
|
initSource: installState.kind === "legacy" ? "legacy-easy-coding" : "fresh",
|
|
813
1696
|
legacyAssets: installState.kind === "legacy" ? installState.legacyAssets : void 0,
|
|
@@ -818,17 +1701,17 @@ async function init(opts) {
|
|
|
818
1701
|
(platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
|
|
819
1702
|
).join("\n");
|
|
820
1703
|
note(triggers, "Next step");
|
|
821
|
-
|
|
1704
|
+
outro3(chalk4.green("easy-coding harness installed. Open your agent and run ec-init."));
|
|
822
1705
|
}
|
|
823
1706
|
|
|
824
1707
|
// src/commands/status.ts
|
|
825
|
-
import
|
|
826
|
-
import
|
|
1708
|
+
import path18 from "path";
|
|
1709
|
+
import chalk5 from "chalk";
|
|
827
1710
|
|
|
828
1711
|
// src/utils/compare-versions.ts
|
|
829
|
-
import
|
|
1712
|
+
import path16 from "path";
|
|
830
1713
|
function normalize(version) {
|
|
831
|
-
const [core] = version.split("-");
|
|
1714
|
+
const [core] = String(version ?? "").split("-");
|
|
832
1715
|
return core.split(".").map((part) => {
|
|
833
1716
|
const parsed = Number.parseInt(part, 10);
|
|
834
1717
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
@@ -850,11 +1733,16 @@ function isVersionBehind(installed, current = VERSION) {
|
|
|
850
1733
|
return compareVersions(installed, current) === -1;
|
|
851
1734
|
}
|
|
852
1735
|
async function checkForUpgrade(cwd) {
|
|
853
|
-
const configPath =
|
|
1736
|
+
const configPath = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
854
1737
|
if (!await pathExists(configPath)) {
|
|
855
1738
|
return;
|
|
856
1739
|
}
|
|
857
|
-
|
|
1740
|
+
let config;
|
|
1741
|
+
try {
|
|
1742
|
+
config = await readConfigYaml(configPath);
|
|
1743
|
+
} catch {
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
858
1746
|
const installed = String(config.harness_version ?? "");
|
|
859
1747
|
if (installed && isVersionBehind(installed)) {
|
|
860
1748
|
process.stderr.write(
|
|
@@ -865,15 +1753,15 @@ async function checkForUpgrade(cwd) {
|
|
|
865
1753
|
}
|
|
866
1754
|
|
|
867
1755
|
// src/utils/session.ts
|
|
868
|
-
import { readdir as
|
|
869
|
-
import
|
|
1756
|
+
import { readdir as readdir6 } from "fs/promises";
|
|
1757
|
+
import path17 from "path";
|
|
870
1758
|
var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
871
1759
|
function getSessionDir(cwd) {
|
|
872
|
-
return
|
|
1760
|
+
return path17.join(cwd, EASY_CODING_DIR, SESSIONS_DIR);
|
|
873
1761
|
}
|
|
874
1762
|
function getSessionFilePath(cwd, ppid) {
|
|
875
1763
|
const pid = ppid ?? process.ppid;
|
|
876
|
-
return
|
|
1764
|
+
return path17.join(getSessionDir(cwd), `${pid}.json`);
|
|
877
1765
|
}
|
|
878
1766
|
async function readSessionFile(cwd, ppid) {
|
|
879
1767
|
const content = await readTextIfExists(getSessionFilePath(cwd, ppid));
|
|
@@ -887,7 +1775,7 @@ async function readSessionFile(cwd, ppid) {
|
|
|
887
1775
|
async function status() {
|
|
888
1776
|
renderBanner();
|
|
889
1777
|
const cwd = process.cwd();
|
|
890
|
-
const configPath =
|
|
1778
|
+
const configPath = path18.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
891
1779
|
if (!await pathExists(configPath)) {
|
|
892
1780
|
throw new Error("No easy-coding harness found in this project.");
|
|
893
1781
|
}
|
|
@@ -897,20 +1785,20 @@ async function status() {
|
|
|
897
1785
|
const activeTasks = tasks.filter((item) => isActiveTask(item.task));
|
|
898
1786
|
const session = await readSessionFile(cwd);
|
|
899
1787
|
const versionRelation = compareVersions(config.harness_version, VERSION);
|
|
900
|
-
console.log(
|
|
1788
|
+
console.log(chalk5.bold("Harness"));
|
|
901
1789
|
console.log(` version: ${config.harness_version}`);
|
|
902
1790
|
console.log(` cli: ${VERSION}`);
|
|
903
1791
|
if (versionRelation === -1) {
|
|
904
|
-
console.log(
|
|
1792
|
+
console.log(chalk5.yellow(" upgrade: available"));
|
|
905
1793
|
} else if (versionRelation === 1) {
|
|
906
|
-
console.log(
|
|
1794
|
+
console.log(chalk5.red(" upgrade: CLI is older than project harness"));
|
|
907
1795
|
} else {
|
|
908
1796
|
console.log(" upgrade: up to date");
|
|
909
1797
|
}
|
|
910
1798
|
console.log(` agents: ${config.agents.join(", ") || "(none)"}`);
|
|
911
|
-
console.log(` project: ${config.project.name}
|
|
1799
|
+
console.log(` project: ${config.project.name}`);
|
|
912
1800
|
console.log("");
|
|
913
|
-
console.log(
|
|
1801
|
+
console.log(chalk5.bold("Session"));
|
|
914
1802
|
if (session?.current_task) {
|
|
915
1803
|
const taskPath = getTaskJsonPath(cwd, session.current_task);
|
|
916
1804
|
if (await pathExists(taskPath)) {
|
|
@@ -925,7 +1813,7 @@ async function status() {
|
|
|
925
1813
|
console.log(" no active session");
|
|
926
1814
|
}
|
|
927
1815
|
console.log("");
|
|
928
|
-
console.log(
|
|
1816
|
+
console.log(chalk5.bold("Tasks"));
|
|
929
1817
|
console.log(` total: ${tasks.length}`);
|
|
930
1818
|
for (const [taskStatus, count] of Object.entries(taskCounts)) {
|
|
931
1819
|
console.log(` ${taskStatus}: ${count}`);
|
|
@@ -940,53 +1828,69 @@ async function status() {
|
|
|
940
1828
|
}
|
|
941
1829
|
|
|
942
1830
|
// src/commands/upgrade.ts
|
|
943
|
-
import
|
|
944
|
-
import { cancel as
|
|
945
|
-
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";
|
|
946
1834
|
async function upgrade(opts) {
|
|
947
1835
|
renderBanner();
|
|
948
1836
|
const cwd = process.cwd();
|
|
949
|
-
const configPath =
|
|
1837
|
+
const configPath = path19.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
950
1838
|
if (!await pathExists(configPath)) {
|
|
951
1839
|
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
952
1840
|
}
|
|
953
1841
|
const config = await readConfigYaml(configPath);
|
|
954
|
-
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);
|
|
955
1850
|
if (relation === 0) {
|
|
956
|
-
|
|
1851
|
+
outro4(chalk6.green(`easy-coding harness is already up to date (${VERSION}).`));
|
|
957
1852
|
return;
|
|
958
1853
|
}
|
|
959
1854
|
if (relation === 1) {
|
|
960
1855
|
throw new Error(
|
|
961
|
-
`Project harness version ${
|
|
1856
|
+
`Project harness version ${installedVersion} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
962
1857
|
);
|
|
963
1858
|
}
|
|
964
1859
|
const summary = [
|
|
965
|
-
`Upgrade: ${
|
|
1860
|
+
`Upgrade: ${installedVersion || "unknown"} -> ${VERSION}`,
|
|
966
1861
|
`Installed agents: ${config.agents.join(", ")}`,
|
|
967
|
-
"Will overwrite managed skills, hooks, agents, and generated main-constraint regions.",
|
|
968
|
-
"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."
|
|
969
1865
|
].join("\n");
|
|
970
1866
|
if (opts.dryRun) {
|
|
971
1867
|
console.log(summary);
|
|
972
1868
|
return;
|
|
973
1869
|
}
|
|
974
1870
|
if (!opts.yes) {
|
|
975
|
-
const shouldUpgrade = await
|
|
1871
|
+
const shouldUpgrade = await confirm3({
|
|
976
1872
|
message: "Apply this harness upgrade?",
|
|
977
1873
|
initialValue: true
|
|
978
1874
|
});
|
|
979
1875
|
if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
|
|
980
|
-
|
|
1876
|
+
cancel3("Upgrade cancelled.");
|
|
981
1877
|
return;
|
|
982
1878
|
}
|
|
983
1879
|
}
|
|
1880
|
+
const artifacts = [];
|
|
984
1881
|
for (const platform of config.agents) {
|
|
985
|
-
await CONFIGURATORS[platform](cwd);
|
|
1882
|
+
artifacts.push(...await CONFIGURATORS[platform](cwd));
|
|
986
1883
|
}
|
|
1884
|
+
await writeRuntimeScaffold(cwd, config.agents);
|
|
1885
|
+
await writeInstallManifest(cwd, {
|
|
1886
|
+
harnessVersion: VERSION,
|
|
1887
|
+
agents: config.agents,
|
|
1888
|
+
artifacts
|
|
1889
|
+
});
|
|
987
1890
|
await ensureEasyCodingSessionsIgnored(cwd);
|
|
988
1891
|
await updateHarnessVersion(configPath, VERSION);
|
|
989
|
-
|
|
1892
|
+
await setPendingInitSince(cwd, VERSION);
|
|
1893
|
+
outro4(chalk6.green(`easy-coding harness upgraded to ${VERSION}.`));
|
|
990
1894
|
}
|
|
991
1895
|
|
|
992
1896
|
// src/cli.ts
|
|
@@ -995,7 +1899,7 @@ function withErrorHandling(fn) {
|
|
|
995
1899
|
try {
|
|
996
1900
|
await fn(opts);
|
|
997
1901
|
} catch (error) {
|
|
998
|
-
console.error(
|
|
1902
|
+
console.error(chalk7.red("Error:"), error instanceof Error ? error.message : error);
|
|
999
1903
|
if (process.env.EC_DEBUG && error instanceof Error) {
|
|
1000
1904
|
console.error(error.stack);
|
|
1001
1905
|
}
|
|
@@ -1010,5 +1914,6 @@ program.command("init").description("Initialize easy-coding harness in current p
|
|
|
1010
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));
|
|
1011
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));
|
|
1012
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));
|
|
1013
1918
|
program.parse();
|
|
1014
1919
|
//# sourceMappingURL=cli.js.map
|