easy-coding-harness 0.1.4
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/README.md +112 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +979 -0
- package/dist/cli.js.map +1 -0
- package/package.json +53 -0
- package/templates/claude/agents/ec-implementer.md +26 -0
- package/templates/claude/agents/ec-reviewer.md +30 -0
- package/templates/claude/agents/ec-verifier.md +30 -0
- package/templates/claude/settings.json +40 -0
- package/templates/codex/agents/ec-implementer.toml +25 -0
- package/templates/codex/agents/ec-reviewer.toml +28 -0
- package/templates/codex/agents/ec-verifier.toml +26 -0
- package/templates/codex/config.toml +9 -0
- package/templates/codex/hooks.json +24 -0
- package/templates/common/bundled-skills/ec-meta/SKILL.md +56 -0
- package/templates/common/bundled-skills/ec-meta/references/customize-local/README.md +54 -0
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +76 -0
- package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +52 -0
- package/templates/common/skills/ec-analysis/SKILL.md +113 -0
- package/templates/common/skills/ec-brainstorming/SKILL.md +70 -0
- package/templates/common/skills/ec-git/SKILL.md +47 -0
- package/templates/common/skills/ec-implementing/SKILL.md +89 -0
- package/templates/common/skills/ec-init/SKILL.md +96 -0
- package/templates/common/skills/ec-memory/SKILL.md +69 -0
- package/templates/common/skills/ec-reviewing/SKILL.md +61 -0
- package/templates/common/skills/ec-task-close/SKILL.md +35 -0
- package/templates/common/skills/ec-task-management/SKILL.md +45 -0
- package/templates/common/skills/ec-verification/SKILL.md +78 -0
- package/templates/common/skills/ec-workflow/SKILL.md +120 -0
- package/templates/main-constraint/AGENTS.md.tpl +58 -0
- package/templates/main-constraint/CLAUDE.md.tpl +56 -0
- package/templates/qoder/agents/ec-implementer.md +28 -0
- package/templates/qoder/agents/ec-reviewer.md +32 -0
- package/templates/qoder/agents/ec-verifier.md +32 -0
- package/templates/qoder/settings.json +36 -0
- package/templates/runtime/memory/long/BUSINESS.md +14 -0
- package/templates/runtime/memory/long/MEMORY.md +3 -0
- package/templates/runtime/memory/long/TECHNICAL.md +3 -0
- package/templates/shared-hooks/easy_coding_status.py +73 -0
- package/templates/shared-hooks/inject-subagent-context.py +80 -0
- package/templates/shared-hooks/inject-workflow-state.py +92 -0
- package/templates/shared-hooks/session-start.py +111 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import chalk6 from "chalk";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/commands/add-agent.ts
|
|
8
|
+
import path8 from "path";
|
|
9
|
+
import { outro } from "@clack/prompts";
|
|
10
|
+
import chalk2 from "chalk";
|
|
11
|
+
|
|
12
|
+
// src/configurators/claude.ts
|
|
13
|
+
import path4 from "path";
|
|
14
|
+
|
|
15
|
+
// src/types/platform.ts
|
|
16
|
+
var pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
17
|
+
var PLATFORM_META = {
|
|
18
|
+
"claude-code": {
|
|
19
|
+
label: "Claude Code",
|
|
20
|
+
skillsDir: ".claude/skills",
|
|
21
|
+
hooksDir: ".claude/hooks",
|
|
22
|
+
hookConfigFile: ".claude/settings.json",
|
|
23
|
+
agentsDir: ".claude/agents",
|
|
24
|
+
agentFileExt: ".md",
|
|
25
|
+
mainConstraint: "CLAUDE.md",
|
|
26
|
+
skillTrigger: "/",
|
|
27
|
+
hookEvents: ["SessionStart", "PreToolUse", "UserPromptSubmit", "Stop"],
|
|
28
|
+
stateInjectEvent: ["SessionStart", "UserPromptSubmit"],
|
|
29
|
+
hasSubagentContext: true,
|
|
30
|
+
templateContext: {
|
|
31
|
+
sub_agent_dispatch: "Agent tool",
|
|
32
|
+
platform_spawn_instruction: 'Use the Agent tool with run_in_background when useful; use isolation: "worktree" for parallel file edits.',
|
|
33
|
+
skill_trigger: "/",
|
|
34
|
+
workflow_state_path: ".easy-coding/state.json",
|
|
35
|
+
main_constraint_file: "CLAUDE.md",
|
|
36
|
+
python_cmd: pythonCmd,
|
|
37
|
+
platform_config_dir: ".claude"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
codex: {
|
|
41
|
+
label: "Codex",
|
|
42
|
+
skillsDir: ".agents/skills",
|
|
43
|
+
hooksDir: ".codex/hooks",
|
|
44
|
+
hookConfigFile: ".codex/hooks.json",
|
|
45
|
+
agentsDir: ".codex/agents",
|
|
46
|
+
agentFileExt: ".toml",
|
|
47
|
+
mainConstraint: "AGENTS.md",
|
|
48
|
+
skillTrigger: "$",
|
|
49
|
+
hookEvents: ["UserPromptSubmit"],
|
|
50
|
+
stateInjectEvent: ["UserPromptSubmit"],
|
|
51
|
+
hasSubagentContext: false,
|
|
52
|
+
templateContext: {
|
|
53
|
+
sub_agent_dispatch: "Codex sub-agent dispatch",
|
|
54
|
+
platform_spawn_instruction: "Use Codex sub-agent delegation where available; pass the full task card in the prompt.",
|
|
55
|
+
skill_trigger: "$",
|
|
56
|
+
workflow_state_path: ".easy-coding/state.json",
|
|
57
|
+
main_constraint_file: "AGENTS.md",
|
|
58
|
+
python_cmd: pythonCmd,
|
|
59
|
+
platform_config_dir: ".codex"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
qoder: {
|
|
63
|
+
label: "Qoder",
|
|
64
|
+
skillsDir: ".qoder/skills",
|
|
65
|
+
hooksDir: ".qoder/hooks",
|
|
66
|
+
hookConfigFile: ".qoder/settings.json",
|
|
67
|
+
agentsDir: ".qoder/agents",
|
|
68
|
+
agentFileExt: ".md",
|
|
69
|
+
mainConstraint: "AGENTS.md",
|
|
70
|
+
skillTrigger: "/",
|
|
71
|
+
hookEvents: ["UserPromptSubmit", "PreToolUse", "Stop"],
|
|
72
|
+
stateInjectEvent: ["UserPromptSubmit"],
|
|
73
|
+
hasSubagentContext: true,
|
|
74
|
+
cnVariant: ".qodercn",
|
|
75
|
+
templateContext: {
|
|
76
|
+
sub_agent_dispatch: "Agent tool",
|
|
77
|
+
platform_spawn_instruction: "Use the Agent tool with worktree isolation for parallel file edits.",
|
|
78
|
+
skill_trigger: "/",
|
|
79
|
+
workflow_state_path: ".easy-coding/state.json",
|
|
80
|
+
main_constraint_file: "AGENTS.md",
|
|
81
|
+
python_cmd: pythonCmd,
|
|
82
|
+
platform_config_dir: ".qoder"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var AGENT_PLATFORMS = Object.keys(PLATFORM_META);
|
|
87
|
+
function isAgentPlatform(value) {
|
|
88
|
+
return Object.hasOwn(PLATFORM_META, value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/configurators/shared.ts
|
|
92
|
+
import { readdir, stat as stat2 } from "fs/promises";
|
|
93
|
+
import path3 from "path";
|
|
94
|
+
|
|
95
|
+
// src/constants/paths.ts
|
|
96
|
+
var EASY_CODING_DIR = ".easy-coding";
|
|
97
|
+
var CONFIG_FILE = "config.yaml";
|
|
98
|
+
var STATE_FILE = "state.json";
|
|
99
|
+
var TASKS_DIR = "tasks";
|
|
100
|
+
var PROJECT_INIT_TASK_ID = "project-init";
|
|
101
|
+
var MEMORY_DIR = "memory";
|
|
102
|
+
var SPEC_DIR = "spec";
|
|
103
|
+
var MAIN_SPEC_DIR = "main";
|
|
104
|
+
var DEV_SPEC_DIR = "dev";
|
|
105
|
+
var STATE_GITIGNORE_ENTRY = ".easy-coding/state.json";
|
|
106
|
+
var GENERATED_REGION_START = "<!-- \u2550\u2550\u2550 easy-coding-harness generated (DO NOT EDIT BETWEEN MARKERS) \u2550\u2550\u2550 -->";
|
|
107
|
+
var GENERATED_REGION_END = "<!-- \u2550\u2550\u2550 end easy-coding-harness generated \u2550\u2550\u2550 -->";
|
|
108
|
+
|
|
109
|
+
// src/utils/file-writer.ts
|
|
110
|
+
import { constants } from "fs";
|
|
111
|
+
import { access, cp, writeFile as fsWriteFile, mkdir, readFile, stat } from "fs/promises";
|
|
112
|
+
import path from "path";
|
|
113
|
+
async function pathExists(filePath) {
|
|
114
|
+
try {
|
|
115
|
+
await access(filePath, constants.F_OK);
|
|
116
|
+
return true;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function ensureDir(dir) {
|
|
122
|
+
await mkdir(dir, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
async function writeTextFile(filePath, content) {
|
|
125
|
+
await ensureDir(path.dirname(filePath));
|
|
126
|
+
await fsWriteFile(filePath, content.endsWith("\n") ? content : `${content}
|
|
127
|
+
`, "utf8");
|
|
128
|
+
}
|
|
129
|
+
async function readTextFile(filePath) {
|
|
130
|
+
return readFile(filePath, "utf8");
|
|
131
|
+
}
|
|
132
|
+
async function readTextIfExists(filePath) {
|
|
133
|
+
if (!await pathExists(filePath)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return readTextFile(filePath);
|
|
137
|
+
}
|
|
138
|
+
async function isDirectory(filePath) {
|
|
139
|
+
try {
|
|
140
|
+
return (await stat(filePath)).isDirectory();
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/utils/marked-region.ts
|
|
147
|
+
var MarkedRegionError = class extends Error {
|
|
148
|
+
constructor(message) {
|
|
149
|
+
super(message);
|
|
150
|
+
this.name = "MarkedRegionError";
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
function extractMarkedRegion(content) {
|
|
154
|
+
const start = content.indexOf(GENERATED_REGION_START);
|
|
155
|
+
const end = content.indexOf(GENERATED_REGION_END);
|
|
156
|
+
if (start === -1 && end === -1) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (start === -1 || end === -1 || end < start) {
|
|
160
|
+
throw new MarkedRegionError("Generated region markers are incomplete or out of order.");
|
|
161
|
+
}
|
|
162
|
+
return content.slice(start, end + GENERATED_REGION_END.length);
|
|
163
|
+
}
|
|
164
|
+
function replaceMarkedRegion(existing, generatedRegion) {
|
|
165
|
+
const currentRegion = extractMarkedRegion(existing);
|
|
166
|
+
if (!currentRegion) {
|
|
167
|
+
return `${generatedRegion.trim()}
|
|
168
|
+
|
|
169
|
+
${existing.trimStart()}`.trimEnd();
|
|
170
|
+
}
|
|
171
|
+
return existing.replace(currentRegion, generatedRegion.trim());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/utils/template-paths.ts
|
|
175
|
+
import { existsSync } from "fs";
|
|
176
|
+
import path2 from "path";
|
|
177
|
+
import { fileURLToPath } from "url";
|
|
178
|
+
function getTemplateRoot() {
|
|
179
|
+
const here = path2.dirname(fileURLToPath(import.meta.url));
|
|
180
|
+
const candidates = [
|
|
181
|
+
path2.resolve(here, "../templates"),
|
|
182
|
+
path2.resolve(here, "../../templates"),
|
|
183
|
+
path2.resolve(process.cwd(), "src/templates"),
|
|
184
|
+
path2.resolve(process.cwd(), "templates")
|
|
185
|
+
];
|
|
186
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
187
|
+
if (!found) {
|
|
188
|
+
throw new Error(`Unable to locate templates directory. Tried: ${candidates.join(", ")}`);
|
|
189
|
+
}
|
|
190
|
+
return found;
|
|
191
|
+
}
|
|
192
|
+
function getTemplatePath(...segments) {
|
|
193
|
+
return path2.join(getTemplateRoot(), ...segments);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/configurators/shared.ts
|
|
197
|
+
var UnresolvedPlaceholderError = class extends Error {
|
|
198
|
+
constructor(contentName, placeholders) {
|
|
199
|
+
super(`Unresolved placeholders in ${contentName}: ${placeholders.join(", ")}`);
|
|
200
|
+
this.name = "UnresolvedPlaceholderError";
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
function resolvePlaceholders(content, ctx, contentName = "template") {
|
|
204
|
+
const withPython = content.replace(/\{\{PYTHON_CMD\}\}/g, ctx.python_cmd);
|
|
205
|
+
const resolved = withPython.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
206
|
+
const value = ctx[key];
|
|
207
|
+
return value === void 0 ? match : String(value);
|
|
208
|
+
});
|
|
209
|
+
const unresolved = [...resolved.matchAll(/\{\{[^}]+\}\}/g)].map((match) => match[0]);
|
|
210
|
+
if (unresolved.length > 0) {
|
|
211
|
+
throw new UnresolvedPlaceholderError(contentName, unresolved);
|
|
212
|
+
}
|
|
213
|
+
return resolved;
|
|
214
|
+
}
|
|
215
|
+
async function resolveSkills(ctx) {
|
|
216
|
+
const skillsRoot = getTemplatePath("common", "skills");
|
|
217
|
+
const entries = await readdir(skillsRoot);
|
|
218
|
+
const skills = [];
|
|
219
|
+
for (const entry of entries.sort()) {
|
|
220
|
+
const skillDir = path3.join(skillsRoot, entry);
|
|
221
|
+
if (!await isDirectory(skillDir)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const content = await readTextFile(path3.join(skillDir, "SKILL.md"));
|
|
225
|
+
skills.push({
|
|
226
|
+
name: entry,
|
|
227
|
+
content: resolvePlaceholders(content, ctx, `common/skills/${entry}/SKILL.md`)
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return skills;
|
|
231
|
+
}
|
|
232
|
+
async function resolveBundledSkills(ctx) {
|
|
233
|
+
const bundledRoot = getTemplatePath("common", "bundled-skills");
|
|
234
|
+
if (!await pathExists(bundledRoot)) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const entries = await readdir(bundledRoot);
|
|
238
|
+
const bundled = [];
|
|
239
|
+
for (const entry of entries.sort()) {
|
|
240
|
+
const sourceDir = path3.join(bundledRoot, entry);
|
|
241
|
+
if (await isDirectory(sourceDir)) {
|
|
242
|
+
bundled.push({ name: entry, sourceDir, context: ctx });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return bundled;
|
|
246
|
+
}
|
|
247
|
+
async function writeSkills(dir, skills, bundled) {
|
|
248
|
+
await ensureDir(dir);
|
|
249
|
+
for (const skill of skills) {
|
|
250
|
+
await writeTextFile(path3.join(dir, skill.name, "SKILL.md"), skill.content);
|
|
251
|
+
}
|
|
252
|
+
for (const skill of bundled) {
|
|
253
|
+
await copyTemplateDirectory(skill.sourceDir, path3.join(dir, skill.name), skill.context);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function writeSharedHooks(dir, platform, opts = {}) {
|
|
257
|
+
const hooksRoot = getTemplatePath("shared-hooks");
|
|
258
|
+
const ctx = PLATFORM_META[platform].templateContext;
|
|
259
|
+
const entries = await readdir(hooksRoot);
|
|
260
|
+
await ensureDir(dir);
|
|
261
|
+
for (const entry of entries.sort()) {
|
|
262
|
+
if (opts.skipSubagentContext && entry === "inject-subagent-context.py") {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const sourcePath = path3.join(hooksRoot, entry);
|
|
266
|
+
if ((await stat2(sourcePath)).isDirectory()) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const content = resolvePlaceholders(
|
|
270
|
+
await readTextFile(sourcePath),
|
|
271
|
+
ctx,
|
|
272
|
+
`shared-hooks/${entry}`
|
|
273
|
+
);
|
|
274
|
+
const destination = path3.join(dir, entry);
|
|
275
|
+
await writeTextFile(destination, content);
|
|
276
|
+
await import("fs/promises").then(({ chmod }) => chmod(destination, 493));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function copyPlatformTemplates(platformTemplateDir, destination, skipDirs, ctx) {
|
|
280
|
+
const source = getTemplatePath(platformTemplateDir);
|
|
281
|
+
await copyTemplateDirectory(source, destination, ctx, new Set(skipDirs));
|
|
282
|
+
}
|
|
283
|
+
async function writeMainConstraint(cwd, platform) {
|
|
284
|
+
const meta = PLATFORM_META[platform];
|
|
285
|
+
const ctx = meta.templateContext;
|
|
286
|
+
const templateName = `${meta.mainConstraint}.tpl`;
|
|
287
|
+
const template = await readTextFile(getTemplatePath("main-constraint", templateName));
|
|
288
|
+
const generated = resolvePlaceholders(template, ctx, `main-constraint/${templateName}`);
|
|
289
|
+
if (!generated.includes(GENERATED_REGION_START) || !generated.includes(GENERATED_REGION_END)) {
|
|
290
|
+
throw new Error(`Main constraint template ${templateName} does not contain generated markers.`);
|
|
291
|
+
}
|
|
292
|
+
const destination = path3.join(cwd, meta.mainConstraint);
|
|
293
|
+
const current = await readTextIfExists(destination);
|
|
294
|
+
const next = current ? replaceMarkedRegion(current, generated) : generated;
|
|
295
|
+
await writeTextFile(destination, next);
|
|
296
|
+
}
|
|
297
|
+
async function copyTemplateDirectory(source, destination, ctx, skipDirs = /* @__PURE__ */ new Set()) {
|
|
298
|
+
await ensureDir(destination);
|
|
299
|
+
for (const entry of (await readdir(source)).sort()) {
|
|
300
|
+
if (skipDirs.has(entry) || shouldSkipTemplateEntry(entry)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const sourcePath = path3.join(source, entry);
|
|
304
|
+
const destinationPath = path3.join(destination, stripTemplateExtension(entry));
|
|
305
|
+
const sourceStat = await stat2(sourcePath);
|
|
306
|
+
if (sourceStat.isDirectory()) {
|
|
307
|
+
await copyTemplateDirectory(sourcePath, destinationPath, ctx, skipDirs);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const content = resolvePlaceholders(await readTextFile(sourcePath), ctx, sourcePath);
|
|
311
|
+
await writeTextFile(destinationPath, content);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function shouldSkipTemplateEntry(entry) {
|
|
315
|
+
return entry === ".DS_Store" || entry === "__pycache__" || entry.endsWith(".ts") || entry.endsWith(".js") || entry.endsWith(".map");
|
|
316
|
+
}
|
|
317
|
+
function stripTemplateExtension(fileName) {
|
|
318
|
+
return fileName.endsWith(".tpl") ? fileName.slice(0, -4) : fileName;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/configurators/claude.ts
|
|
322
|
+
async function configureClaude(cwd) {
|
|
323
|
+
const platform = "claude-code";
|
|
324
|
+
const meta = PLATFORM_META[platform];
|
|
325
|
+
const ctx = meta.templateContext;
|
|
326
|
+
const dest = path4.join(cwd, ".claude");
|
|
327
|
+
await copyPlatformTemplates("claude", dest, ["hooks"], ctx);
|
|
328
|
+
await writeSharedHooks(path4.join(dest, "hooks"), platform);
|
|
329
|
+
await writeSkills(
|
|
330
|
+
path4.join(cwd, meta.skillsDir),
|
|
331
|
+
await resolveSkills(ctx),
|
|
332
|
+
await resolveBundledSkills(ctx)
|
|
333
|
+
);
|
|
334
|
+
await writeMainConstraint(cwd, platform);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/configurators/codex.ts
|
|
338
|
+
import path5 from "path";
|
|
339
|
+
async function configureCodex(cwd) {
|
|
340
|
+
const platform = "codex";
|
|
341
|
+
const meta = PLATFORM_META[platform];
|
|
342
|
+
const ctx = meta.templateContext;
|
|
343
|
+
await writeSkills(
|
|
344
|
+
path5.join(cwd, meta.skillsDir),
|
|
345
|
+
await resolveSkills(ctx),
|
|
346
|
+
await resolveBundledSkills(ctx)
|
|
347
|
+
);
|
|
348
|
+
await writeSharedHooks(path5.join(cwd, meta.hooksDir), platform, { skipSubagentContext: true });
|
|
349
|
+
await copyPlatformTemplates("codex", path5.join(cwd, ".codex"), ["hooks"], ctx);
|
|
350
|
+
await writeMainConstraint(cwd, platform);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/configurators/qoder.ts
|
|
354
|
+
import { existsSync as existsSync2 } from "fs";
|
|
355
|
+
import path6 from "path";
|
|
356
|
+
async function configureQoder(cwd) {
|
|
357
|
+
const platform = "qoder";
|
|
358
|
+
const baseDir = detectQoderCnVariant(cwd) ? ".qodercn" : ".qoder";
|
|
359
|
+
const ctx = {
|
|
360
|
+
...PLATFORM_META[platform].templateContext,
|
|
361
|
+
platform_config_dir: baseDir
|
|
362
|
+
};
|
|
363
|
+
const dest = path6.join(cwd, baseDir);
|
|
364
|
+
await copyPlatformTemplates("qoder", dest, ["hooks"], ctx);
|
|
365
|
+
await writeSharedHooks(path6.join(dest, "hooks"), platform);
|
|
366
|
+
await writeSkills(
|
|
367
|
+
path6.join(dest, "skills"),
|
|
368
|
+
await resolveSkills(ctx),
|
|
369
|
+
await resolveBundledSkills(ctx)
|
|
370
|
+
);
|
|
371
|
+
await writeMainConstraint(cwd, platform);
|
|
372
|
+
}
|
|
373
|
+
function detectQoderCnVariant(cwd) {
|
|
374
|
+
if (process.env.EC_QODER_VARIANT === "cn" || process.env.QODER_VARIANT === "cn") {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
return existsSync2(path6.join(cwd, ".qodercn"));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/configurators/index.ts
|
|
381
|
+
var CONFIGURATORS = {
|
|
382
|
+
"claude-code": configureClaude,
|
|
383
|
+
codex: configureCodex,
|
|
384
|
+
qoder: configureQoder
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/ui/banner.ts
|
|
388
|
+
import chalk from "chalk";
|
|
389
|
+
import figlet from "figlet";
|
|
390
|
+
|
|
391
|
+
// src/constants/version.ts
|
|
392
|
+
import { readFileSync } from "fs";
|
|
393
|
+
import path7 from "path";
|
|
394
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
395
|
+
var packageMetadata = readPackageMetadata();
|
|
396
|
+
var PACKAGE_NAME = packageMetadata.name;
|
|
397
|
+
var VERSION = packageMetadata.version;
|
|
398
|
+
function readPackageMetadata() {
|
|
399
|
+
const here = path7.dirname(fileURLToPath2(import.meta.url));
|
|
400
|
+
const candidates = [
|
|
401
|
+
path7.resolve(here, "../../package.json"),
|
|
402
|
+
path7.resolve(here, "../package.json"),
|
|
403
|
+
path7.resolve(process.cwd(), "package.json")
|
|
404
|
+
];
|
|
405
|
+
for (const candidate of candidates) {
|
|
406
|
+
try {
|
|
407
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
408
|
+
if (parsed.name && parsed.version) {
|
|
409
|
+
return { name: parsed.name, version: parsed.version };
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
throw new Error(`Unable to locate package metadata. Tried: ${candidates.join(", ")}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/ui/banner.ts
|
|
418
|
+
var WIDE_TERMINAL_WIDTH = 88;
|
|
419
|
+
var MEDIUM_TERMINAL_WIDTH = 72;
|
|
420
|
+
function renderBanner() {
|
|
421
|
+
const terminalWidth = process.stdout.columns ?? 120;
|
|
422
|
+
const art = figlet.textSync("Easy Coding", selectBannerPreset(terminalWidth));
|
|
423
|
+
console.log(colorizeBanner(art));
|
|
424
|
+
console.log(chalk.bold.white(` Easy Coding Harness ${chalk.cyan(`v${VERSION}`)}
|
|
425
|
+
`));
|
|
426
|
+
}
|
|
427
|
+
function selectBannerPreset(width) {
|
|
428
|
+
if (width >= WIDE_TERMINAL_WIDTH) {
|
|
429
|
+
return { font: "ANSI Shadow", horizontalLayout: "full" };
|
|
430
|
+
}
|
|
431
|
+
if (width >= MEDIUM_TERMINAL_WIDTH) {
|
|
432
|
+
return { font: "Small Shadow", horizontalLayout: "full" };
|
|
433
|
+
}
|
|
434
|
+
return { font: "Small Slant" };
|
|
435
|
+
}
|
|
436
|
+
function colorizeBanner(art) {
|
|
437
|
+
const colors = [chalk.cyanBright, chalk.cyan, chalk.blueBright, chalk.blue];
|
|
438
|
+
return art.split("\n").map((line, index) => colors[index % colors.length](line)).join("\n");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/utils/config-yaml.ts
|
|
442
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
443
|
+
import YAML, { isScalar, isSeq, parseDocument } from "yaml";
|
|
444
|
+
function createDefaultConfig(params) {
|
|
445
|
+
return {
|
|
446
|
+
version: 1,
|
|
447
|
+
harness_version: params.harnessVersion,
|
|
448
|
+
agents: params.agents,
|
|
449
|
+
project: {
|
|
450
|
+
name: params.projectName,
|
|
451
|
+
mode: "auto"
|
|
452
|
+
},
|
|
453
|
+
memory: {
|
|
454
|
+
short_term_max: 10,
|
|
455
|
+
short_term_keep: 5,
|
|
456
|
+
schema_version: 2
|
|
457
|
+
},
|
|
458
|
+
test: {
|
|
459
|
+
framework: "auto",
|
|
460
|
+
command: ""
|
|
461
|
+
},
|
|
462
|
+
tasks: {
|
|
463
|
+
auto_archive_days: 30
|
|
464
|
+
},
|
|
465
|
+
behavior: {
|
|
466
|
+
strict_confirm: true,
|
|
467
|
+
auto_mode: false
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function stringifyConfig(config) {
|
|
472
|
+
return YAML.stringify(config);
|
|
473
|
+
}
|
|
474
|
+
async function writeConfigYaml(filePath, config) {
|
|
475
|
+
await writeTextFile(filePath, stringifyConfig(config));
|
|
476
|
+
}
|
|
477
|
+
async function readConfigYaml(filePath) {
|
|
478
|
+
const content = await readFile2(filePath, "utf8");
|
|
479
|
+
return YAML.parse(content);
|
|
480
|
+
}
|
|
481
|
+
async function updateConfigYaml(filePath, updater) {
|
|
482
|
+
const content = await readFile2(filePath, "utf8");
|
|
483
|
+
const document = parseDocument(content);
|
|
484
|
+
const config = document.toJSON();
|
|
485
|
+
updater(config);
|
|
486
|
+
for (const [key, value] of Object.entries(config)) {
|
|
487
|
+
document.set(key, value);
|
|
488
|
+
}
|
|
489
|
+
await writeTextFile(filePath, document.toString());
|
|
490
|
+
return config;
|
|
491
|
+
}
|
|
492
|
+
async function addAgentsToConfig(filePath, agents) {
|
|
493
|
+
return updateConfigYaml(filePath, (config) => {
|
|
494
|
+
const merged = /* @__PURE__ */ new Set([...config.agents ?? [], ...agents]);
|
|
495
|
+
config.agents = [...merged];
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async function updateHarnessVersion(filePath, version) {
|
|
499
|
+
return updateConfigYaml(filePath, (config) => {
|
|
500
|
+
config.harness_version = version;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/commands/platforms.ts
|
|
505
|
+
import { cancel, confirm, multiselect } from "@clack/prompts";
|
|
506
|
+
function parseAgentList(agentList) {
|
|
507
|
+
const values = agentList.split(",").map((value) => value.trim()).filter(Boolean);
|
|
508
|
+
if (values.length === 0) {
|
|
509
|
+
throw new Error("No agent platform specified.");
|
|
510
|
+
}
|
|
511
|
+
const invalid = values.filter((value) => !isAgentPlatform(value));
|
|
512
|
+
if (invalid.length > 0) {
|
|
513
|
+
throw new Error(`Unknown agent platform: ${invalid.join(", ")}`);
|
|
514
|
+
}
|
|
515
|
+
return [...new Set(values)];
|
|
516
|
+
}
|
|
517
|
+
async function resolvePlatforms(opts, defaults = ["claude-code"]) {
|
|
518
|
+
if (opts.agent) {
|
|
519
|
+
return parseAgentList(opts.agent);
|
|
520
|
+
}
|
|
521
|
+
if (opts.yes) {
|
|
522
|
+
return defaults;
|
|
523
|
+
}
|
|
524
|
+
let selectedDefaults = defaults;
|
|
525
|
+
while (true) {
|
|
526
|
+
const result = await multiselect({
|
|
527
|
+
message: "Select agent platforms (Space to toggle, Enter to review)",
|
|
528
|
+
options: AGENT_PLATFORMS.map((platform) => ({
|
|
529
|
+
label: PLATFORM_META[platform].label,
|
|
530
|
+
value: platform
|
|
531
|
+
})),
|
|
532
|
+
initialValues: selectedDefaults,
|
|
533
|
+
required: true
|
|
534
|
+
});
|
|
535
|
+
if (typeof result === "symbol") {
|
|
536
|
+
cancel("Platform selection cancelled.");
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
const labels = result.map((platform) => PLATFORM_META[platform].label).join(", ");
|
|
540
|
+
const shouldInstall = await confirm({
|
|
541
|
+
message: `Install Easy Coding for: ${labels}?`,
|
|
542
|
+
initialValue: true
|
|
543
|
+
});
|
|
544
|
+
if (typeof shouldInstall === "symbol") {
|
|
545
|
+
cancel("Platform selection cancelled.");
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
if (shouldInstall) {
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
selectedDefaults = result;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/commands/add-agent.ts
|
|
556
|
+
async function addAgent(opts) {
|
|
557
|
+
renderBanner();
|
|
558
|
+
const cwd = process.cwd();
|
|
559
|
+
const configPath = path8.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
560
|
+
if (!await pathExists(configPath)) {
|
|
561
|
+
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
562
|
+
}
|
|
563
|
+
const config = await readConfigYaml(configPath);
|
|
564
|
+
const platforms = await resolvePlatforms(opts, ["claude-code"]);
|
|
565
|
+
const toInstall = platforms.filter((platform) => !config.agents.includes(platform));
|
|
566
|
+
if (toInstall.length === 0) {
|
|
567
|
+
outro(chalk2.yellow("All selected agent platforms are already installed."));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
for (const platform of toInstall) {
|
|
571
|
+
await CONFIGURATORS[platform](cwd);
|
|
572
|
+
}
|
|
573
|
+
await addAgentsToConfig(configPath, toInstall);
|
|
574
|
+
outro(chalk2.green(`Added agent platforms: ${toInstall.join(", ")}`));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/commands/init.ts
|
|
578
|
+
import { note, outro as outro2 } from "@clack/prompts";
|
|
579
|
+
import chalk3 from "chalk";
|
|
580
|
+
|
|
581
|
+
// src/utils/gitignore.ts
|
|
582
|
+
import path9 from "path";
|
|
583
|
+
async function ensureGitignoreEntry(cwd, entry, heading = "# \u2550\u2550\u2550 easy-coding-harness (auto-generated) \u2550\u2550\u2550\n# Personal runtime state; do not commit") {
|
|
584
|
+
const gitignorePath = path9.join(cwd, ".gitignore");
|
|
585
|
+
const current = await readTextIfExists(gitignorePath) ?? "";
|
|
586
|
+
const lines = current.split(/\r?\n/).map((line) => line.trim());
|
|
587
|
+
if (lines.includes(entry)) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
const prefix = current.trimEnd();
|
|
591
|
+
const next = [prefix, heading, entry].filter(Boolean).join("\n");
|
|
592
|
+
await writeTextFile(gitignorePath, next);
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
async function ensureEasyCodingStateIgnored(cwd) {
|
|
596
|
+
return ensureGitignoreEntry(cwd, STATE_GITIGNORE_ENTRY);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/utils/install-state.ts
|
|
600
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
601
|
+
import path10 from "path";
|
|
602
|
+
var LEGACY_ROOT_FILES = ["SOUL.md", "RULES.md", "ABSTRACT.md"];
|
|
603
|
+
async function detectEasyCodingInstallState(cwd) {
|
|
604
|
+
const easyCodingDir = path10.join(cwd, EASY_CODING_DIR);
|
|
605
|
+
if (!await pathExists(easyCodingDir)) {
|
|
606
|
+
return { kind: "fresh", easyCodingDir };
|
|
607
|
+
}
|
|
608
|
+
const configPath = path10.join(easyCodingDir, CONFIG_FILE);
|
|
609
|
+
if (await pathExists(configPath)) {
|
|
610
|
+
return { kind: "installed", easyCodingDir, configPath };
|
|
611
|
+
}
|
|
612
|
+
const legacyAssets = await detectLegacyAssets(easyCodingDir);
|
|
613
|
+
if (legacyAssets.length === 0) {
|
|
614
|
+
return { kind: "unknown", easyCodingDir };
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
kind: "legacy",
|
|
618
|
+
easyCodingDir,
|
|
619
|
+
legacyAssets,
|
|
620
|
+
missingHarnessFiles: [relativeConfigPath(), relativeProjectInitTaskPath()]
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
async function detectLegacyAssets(easyCodingDir) {
|
|
624
|
+
const assets = [];
|
|
625
|
+
for (const file of LEGACY_ROOT_FILES) {
|
|
626
|
+
if (await pathExists(path10.join(easyCodingDir, file))) {
|
|
627
|
+
assets.push(relativeEasyCodingPath(file));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (await pathExists(path10.join(easyCodingDir, "memory", "long", "MEMORY.md"))) {
|
|
631
|
+
assets.push(relativeEasyCodingPath("memory", "long", "MEMORY.md"));
|
|
632
|
+
}
|
|
633
|
+
const shortMemoryFiles = await listMarkdownFiles(path10.join(easyCodingDir, "memory", "short"));
|
|
634
|
+
assets.push(...shortMemoryFiles.map((file) => relativeEasyCodingPath("memory", "short", file)));
|
|
635
|
+
for (const dir of ["spec", "prototype"]) {
|
|
636
|
+
if (await hasAnyDirectoryEntry(path10.join(easyCodingDir, dir))) {
|
|
637
|
+
assets.push(relativeEasyCodingPath(dir));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return assets.sort();
|
|
641
|
+
}
|
|
642
|
+
async function listMarkdownFiles(dir) {
|
|
643
|
+
if (!await isDirectory(dir)) {
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
647
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
|
|
648
|
+
}
|
|
649
|
+
async function hasAnyDirectoryEntry(dir) {
|
|
650
|
+
if (!await isDirectory(dir)) {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
return (await readdir2(dir)).length > 0;
|
|
654
|
+
}
|
|
655
|
+
function relativeEasyCodingPath(...segments) {
|
|
656
|
+
return path10.posix.join(EASY_CODING_DIR, ...segments);
|
|
657
|
+
}
|
|
658
|
+
function relativeConfigPath() {
|
|
659
|
+
return path10.posix.join(EASY_CODING_DIR, CONFIG_FILE);
|
|
660
|
+
}
|
|
661
|
+
function relativeProjectInitTaskPath() {
|
|
662
|
+
return path10.posix.join(EASY_CODING_DIR, TASKS_DIR, PROJECT_INIT_TASK_ID, "task.json");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/utils/runtime-scaffold.ts
|
|
666
|
+
import path11 from "path";
|
|
667
|
+
async function writeRuntimeScaffold(cwd, agents) {
|
|
668
|
+
const easyCodingDir = path11.join(cwd, EASY_CODING_DIR);
|
|
669
|
+
await ensureDir(easyCodingDir);
|
|
670
|
+
const configPath = path11.join(easyCodingDir, CONFIG_FILE);
|
|
671
|
+
if (!await pathExists(configPath)) {
|
|
672
|
+
const projectName = path11.basename(cwd);
|
|
673
|
+
await writeConfigYaml(
|
|
674
|
+
configPath,
|
|
675
|
+
createDefaultConfig({ projectName, harnessVersion: VERSION, agents })
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
await ensureDir(path11.join(easyCodingDir, "tasks"));
|
|
679
|
+
await ensureDir(path11.join(easyCodingDir, SPEC_DIR, MAIN_SPEC_DIR));
|
|
680
|
+
await ensureDir(path11.join(easyCodingDir, SPEC_DIR, DEV_SPEC_DIR));
|
|
681
|
+
await writeMemoryScaffold(easyCodingDir);
|
|
682
|
+
}
|
|
683
|
+
async function writeMemoryScaffold(easyCodingDir) {
|
|
684
|
+
const memoryDir = path11.join(easyCodingDir, MEMORY_DIR);
|
|
685
|
+
await ensureDir(path11.join(memoryDir, "short"));
|
|
686
|
+
await ensureDir(path11.join(memoryDir, "long"));
|
|
687
|
+
for (const file of ["MEMORY.md", "BUSINESS.md", "TECHNICAL.md"]) {
|
|
688
|
+
const destination = path11.join(memoryDir, "long", file);
|
|
689
|
+
if (await pathExists(destination)) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const templatePath = getTemplatePath("runtime", "memory", "long", file);
|
|
693
|
+
await writeTextFile(destination, await readTextFile(templatePath));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/utils/task-json.ts
|
|
698
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
699
|
+
import path12 from "path";
|
|
700
|
+
function createProjectInitTask(params) {
|
|
701
|
+
return {
|
|
702
|
+
type: "project-init",
|
|
703
|
+
status: "PENDING",
|
|
704
|
+
created_at: (params.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
705
|
+
created_by: "cli-init",
|
|
706
|
+
context: {
|
|
707
|
+
agents_installed: params.agents,
|
|
708
|
+
cli_version: VERSION,
|
|
709
|
+
project_path: params.cwd,
|
|
710
|
+
init_source: params.initSource ?? "fresh",
|
|
711
|
+
...params.legacyAssets ? { legacy_assets: params.legacyAssets } : {},
|
|
712
|
+
...params.legacyMissingHarnessFiles ? { legacy_missing_harness_files: params.legacyMissingHarnessFiles } : {}
|
|
713
|
+
},
|
|
714
|
+
init_log: []
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function getTaskJsonPath(cwd, taskId) {
|
|
718
|
+
return path12.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json");
|
|
719
|
+
}
|
|
720
|
+
async function writeTaskJson(filePath, task) {
|
|
721
|
+
await writeTextFile(filePath, JSON.stringify(task, null, 2));
|
|
722
|
+
}
|
|
723
|
+
async function readTaskJson(filePath) {
|
|
724
|
+
return JSON.parse(await readTextFile(filePath));
|
|
725
|
+
}
|
|
726
|
+
async function writeProjectInitTask(cwd, agents, options = {}) {
|
|
727
|
+
await writeTaskJson(
|
|
728
|
+
getTaskJsonPath(cwd, PROJECT_INIT_TASK_ID),
|
|
729
|
+
createProjectInitTask({
|
|
730
|
+
cwd,
|
|
731
|
+
agents,
|
|
732
|
+
initSource: options.initSource,
|
|
733
|
+
legacyAssets: options.legacyAssets,
|
|
734
|
+
legacyMissingHarnessFiles: options.legacyMissingHarnessFiles
|
|
735
|
+
})
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
async function listTasks(cwd) {
|
|
739
|
+
const tasksDir = path12.join(cwd, EASY_CODING_DIR, TASKS_DIR);
|
|
740
|
+
if (!await pathExists(tasksDir)) {
|
|
741
|
+
return [];
|
|
742
|
+
}
|
|
743
|
+
const entries = await readdir3(tasksDir, { withFileTypes: true });
|
|
744
|
+
const tasks = [];
|
|
745
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
746
|
+
if (!entry.isDirectory()) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
const taskPath = getTaskJsonPath(cwd, entry.name);
|
|
750
|
+
if (!await pathExists(taskPath)) {
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
tasks.push({ id: entry.name, task: await readTaskJson(taskPath) });
|
|
754
|
+
}
|
|
755
|
+
return tasks;
|
|
756
|
+
}
|
|
757
|
+
function summarizeTaskStatuses(tasks) {
|
|
758
|
+
return tasks.reduce(
|
|
759
|
+
(summary, item) => {
|
|
760
|
+
summary[item.task.status] = (summary[item.task.status] ?? 0) + 1;
|
|
761
|
+
return summary;
|
|
762
|
+
},
|
|
763
|
+
{}
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
function isActiveTask(task) {
|
|
767
|
+
return task.status !== "COMPLETE" && task.status !== "CLOSED";
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/commands/init.ts
|
|
771
|
+
async function init(opts) {
|
|
772
|
+
renderBanner();
|
|
773
|
+
const cwd = process.cwd();
|
|
774
|
+
const installState = await detectEasyCodingInstallState(cwd);
|
|
775
|
+
if (installState.kind === "installed") {
|
|
776
|
+
throw new Error(
|
|
777
|
+
".easy-coding/config.yaml already exists. Use easy-coding add-agent or easy-coding upgrade."
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
if (installState.kind === "unknown") {
|
|
781
|
+
throw new Error(
|
|
782
|
+
".easy-coding exists but is not recognized as an easy-coding harness or legacy easy-coding skill project. Please inspect it manually before running init."
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const platforms = await resolvePlatforms(opts, ["claude-code"]);
|
|
786
|
+
for (const platform of platforms) {
|
|
787
|
+
await CONFIGURATORS[platform](cwd);
|
|
788
|
+
}
|
|
789
|
+
await writeRuntimeScaffold(cwd, platforms);
|
|
790
|
+
await writeProjectInitTask(cwd, platforms, {
|
|
791
|
+
initSource: installState.kind === "legacy" ? "legacy-easy-coding" : "fresh",
|
|
792
|
+
legacyAssets: installState.kind === "legacy" ? installState.legacyAssets : void 0,
|
|
793
|
+
legacyMissingHarnessFiles: installState.kind === "legacy" ? installState.missingHarnessFiles : void 0
|
|
794
|
+
});
|
|
795
|
+
await ensureEasyCodingStateIgnored(cwd);
|
|
796
|
+
const triggers = platforms.map(
|
|
797
|
+
(platform) => `${PLATFORM_META[platform].label}: ${PLATFORM_META[platform].skillTrigger}ec-init`
|
|
798
|
+
).join("\n");
|
|
799
|
+
note(triggers, "Next step");
|
|
800
|
+
outro2(chalk3.green("easy-coding harness installed. Open your agent and run ec-init."));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/commands/status.ts
|
|
804
|
+
import path15 from "path";
|
|
805
|
+
import chalk4 from "chalk";
|
|
806
|
+
|
|
807
|
+
// src/utils/compare-versions.ts
|
|
808
|
+
import path13 from "path";
|
|
809
|
+
function normalize(version) {
|
|
810
|
+
const [core] = version.split("-");
|
|
811
|
+
return core.split(".").map((part) => {
|
|
812
|
+
const parsed = Number.parseInt(part, 10);
|
|
813
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function compareVersions(a, b) {
|
|
817
|
+
const left = normalize(a);
|
|
818
|
+
const right = normalize(b);
|
|
819
|
+
const length = Math.max(left.length, right.length);
|
|
820
|
+
for (let index = 0; index < length; index += 1) {
|
|
821
|
+
const leftPart = left[index] ?? 0;
|
|
822
|
+
const rightPart = right[index] ?? 0;
|
|
823
|
+
if (leftPart < rightPart) return -1;
|
|
824
|
+
if (leftPart > rightPart) return 1;
|
|
825
|
+
}
|
|
826
|
+
return 0;
|
|
827
|
+
}
|
|
828
|
+
function isVersionBehind(installed, current = VERSION) {
|
|
829
|
+
return compareVersions(installed, current) === -1;
|
|
830
|
+
}
|
|
831
|
+
async function checkForUpgrade(cwd) {
|
|
832
|
+
const configPath = path13.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
833
|
+
if (!await pathExists(configPath)) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const config = await readConfigYaml(configPath);
|
|
837
|
+
const installed = String(config.harness_version ?? "");
|
|
838
|
+
if (installed && isVersionBehind(installed)) {
|
|
839
|
+
process.stderr.write(
|
|
840
|
+
`easy-coding harness ${installed} is older than CLI ${VERSION}. Run easy-coding upgrade when ready.
|
|
841
|
+
`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/utils/state-json.ts
|
|
847
|
+
import path14 from "path";
|
|
848
|
+
function getStateJsonPath(cwd) {
|
|
849
|
+
return path14.join(cwd, EASY_CODING_DIR, STATE_FILE);
|
|
850
|
+
}
|
|
851
|
+
async function readStateJson(cwd) {
|
|
852
|
+
const content = await readTextIfExists(getStateJsonPath(cwd));
|
|
853
|
+
if (!content) {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
return JSON.parse(content);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/commands/status.ts
|
|
860
|
+
async function status() {
|
|
861
|
+
renderBanner();
|
|
862
|
+
const cwd = process.cwd();
|
|
863
|
+
const configPath = path15.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
864
|
+
if (!await pathExists(configPath)) {
|
|
865
|
+
throw new Error("No easy-coding harness found in this project.");
|
|
866
|
+
}
|
|
867
|
+
const config = await readConfigYaml(configPath);
|
|
868
|
+
const tasks = await listTasks(cwd);
|
|
869
|
+
const taskCounts = summarizeTaskStatuses(tasks);
|
|
870
|
+
const activeTasks = tasks.filter((item) => isActiveTask(item.task));
|
|
871
|
+
const state = await readStateJson(cwd);
|
|
872
|
+
const versionRelation = compareVersions(config.harness_version, VERSION);
|
|
873
|
+
console.log(chalk4.bold("Harness"));
|
|
874
|
+
console.log(` version: ${config.harness_version}`);
|
|
875
|
+
console.log(` cli: ${VERSION}`);
|
|
876
|
+
if (versionRelation === -1) {
|
|
877
|
+
console.log(chalk4.yellow(" upgrade: available"));
|
|
878
|
+
} else if (versionRelation === 1) {
|
|
879
|
+
console.log(chalk4.red(" upgrade: CLI is older than project harness"));
|
|
880
|
+
} else {
|
|
881
|
+
console.log(" upgrade: up to date");
|
|
882
|
+
}
|
|
883
|
+
console.log(` agents: ${config.agents.join(", ") || "(none)"}`);
|
|
884
|
+
console.log(` project: ${config.project.name} (${config.project.mode})`);
|
|
885
|
+
console.log("");
|
|
886
|
+
console.log(chalk4.bold("State"));
|
|
887
|
+
if (state) {
|
|
888
|
+
console.log(` current_stage: ${state.current_stage}`);
|
|
889
|
+
console.log(` current_task: ${state.current_task ?? "(none)"}`);
|
|
890
|
+
console.log(` last_agent: ${state.last_agent}`);
|
|
891
|
+
} else {
|
|
892
|
+
console.log(" state.json: not created yet");
|
|
893
|
+
}
|
|
894
|
+
console.log("");
|
|
895
|
+
console.log(chalk4.bold("Tasks"));
|
|
896
|
+
console.log(` total: ${tasks.length}`);
|
|
897
|
+
for (const [status2, count] of Object.entries(taskCounts)) {
|
|
898
|
+
console.log(` ${status2}: ${count}`);
|
|
899
|
+
}
|
|
900
|
+
if (activeTasks.length > 0) {
|
|
901
|
+
console.log(" active:");
|
|
902
|
+
for (const item of activeTasks) {
|
|
903
|
+
console.log(` - ${item.id}: ${item.task.status}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/commands/upgrade.ts
|
|
909
|
+
import path16 from "path";
|
|
910
|
+
import { cancel as cancel2, confirm as confirm2, outro as outro3 } from "@clack/prompts";
|
|
911
|
+
import chalk5 from "chalk";
|
|
912
|
+
async function upgrade(opts) {
|
|
913
|
+
renderBanner();
|
|
914
|
+
const cwd = process.cwd();
|
|
915
|
+
const configPath = path16.join(cwd, EASY_CODING_DIR, CONFIG_FILE);
|
|
916
|
+
if (!await pathExists(configPath)) {
|
|
917
|
+
throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
|
|
918
|
+
}
|
|
919
|
+
const config = await readConfigYaml(configPath);
|
|
920
|
+
const relation = compareVersions(config.harness_version, VERSION);
|
|
921
|
+
if (relation === 0) {
|
|
922
|
+
outro3(chalk5.green(`easy-coding harness is already up to date (${VERSION}).`));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (relation === 1) {
|
|
926
|
+
throw new Error(
|
|
927
|
+
`Project harness version ${config.harness_version} is newer than CLI ${VERSION}. Update the CLI first.`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const summary = [
|
|
931
|
+
`Upgrade: ${config.harness_version} -> ${VERSION}`,
|
|
932
|
+
`Installed agents: ${config.agents.join(", ")}`,
|
|
933
|
+
"Will overwrite managed skills, hooks, agents, and generated main-constraint regions.",
|
|
934
|
+
"Will not touch tasks, memory, state, spec, or project knowledge files."
|
|
935
|
+
].join("\n");
|
|
936
|
+
if (opts.dryRun) {
|
|
937
|
+
console.log(summary);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (!opts.yes) {
|
|
941
|
+
const shouldUpgrade = await confirm2({
|
|
942
|
+
message: "Apply this harness upgrade?",
|
|
943
|
+
initialValue: true
|
|
944
|
+
});
|
|
945
|
+
if (typeof shouldUpgrade === "symbol" || !shouldUpgrade) {
|
|
946
|
+
cancel2("Upgrade cancelled.");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
for (const platform of config.agents) {
|
|
951
|
+
await CONFIGURATORS[platform](cwd);
|
|
952
|
+
}
|
|
953
|
+
await updateHarnessVersion(configPath, VERSION);
|
|
954
|
+
outro3(chalk5.green(`easy-coding harness upgraded to ${VERSION}.`));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/cli.ts
|
|
958
|
+
function withErrorHandling(fn) {
|
|
959
|
+
return async (opts) => {
|
|
960
|
+
try {
|
|
961
|
+
await fn(opts);
|
|
962
|
+
} catch (error) {
|
|
963
|
+
console.error(chalk6.red("Error:"), error instanceof Error ? error.message : error);
|
|
964
|
+
if (process.env.EC_DEBUG && error instanceof Error) {
|
|
965
|
+
console.error(error.stack);
|
|
966
|
+
}
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
await checkForUpgrade(process.cwd());
|
|
972
|
+
var program = new Command();
|
|
973
|
+
program.name("easy-coding").description(PACKAGE_NAME).version(VERSION, "-v, --version");
|
|
974
|
+
program.command("init").description("Initialize easy-coding harness in current project").option("--agent <list>", "Comma-separated platforms: claude-code,codex,qoder").option("-y, --yes", "Skip prompts, use defaults").action(withErrorHandling(init));
|
|
975
|
+
program.command("add-agent").description("Add agent platform support to an existing project").option("--agent <list>", "Comma-separated platforms to add").action(withErrorHandling(addAgent));
|
|
976
|
+
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));
|
|
977
|
+
program.command("status").description("Show installed agents, version, and tasks").action(withErrorHandling(status));
|
|
978
|
+
program.parse();
|
|
979
|
+
//# sourceMappingURL=cli.js.map
|