engly 1.2.0 → 1.4.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 +25 -0
- package/README.md +8 -8
- package/dist/cli.js +585 -178
- package/dist/cli.js.map +1 -1
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -13,131 +13,10 @@ async function configCommand(_option, _value) {
|
|
|
13
13
|
notImplemented("config");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// src/commands/
|
|
17
|
-
|
|
18
|
-
notImplemented("disable");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// src/commands/enable.ts
|
|
22
|
-
async function enableCommand() {
|
|
23
|
-
notImplemented("enable");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// src/commands/init.ts
|
|
27
|
-
import { join as join5 } from "path";
|
|
16
|
+
// src/commands/set-enabled.ts
|
|
17
|
+
import { join as join7 } from "path";
|
|
28
18
|
import * as p from "@clack/prompts";
|
|
29
19
|
|
|
30
|
-
// src/adapters/detect.ts
|
|
31
|
-
import { access } from "fs/promises";
|
|
32
|
-
import { dirname, join } from "path";
|
|
33
|
-
async function anyExists(cwd, paths) {
|
|
34
|
-
for (const path of paths) {
|
|
35
|
-
try {
|
|
36
|
-
await access(join(cwd, path));
|
|
37
|
-
return true;
|
|
38
|
-
} catch {
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
async function anyExistsUpward(cwd, paths) {
|
|
44
|
-
let dir = cwd;
|
|
45
|
-
while (true) {
|
|
46
|
-
if (await anyExists(dir, paths)) return true;
|
|
47
|
-
const parent = dirname(dir);
|
|
48
|
-
if (parent === dir) return false;
|
|
49
|
-
dir = parent;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// src/adapters/claude.ts
|
|
54
|
-
var claude = {
|
|
55
|
-
id: "claude",
|
|
56
|
-
name: "Claude Code",
|
|
57
|
-
delivery: "import",
|
|
58
|
-
detect: (cwd) => anyExists(cwd, ["CLAUDE.md", ".claude"]),
|
|
59
|
-
// Claude Code reads AGENTS.md only when there is no CLAUDE.md, .claude/CLAUDE.md or
|
|
60
|
-
// CLAUDE.local.md in the working directory or any directory above it.
|
|
61
|
-
instructionFile: async (cwd) => {
|
|
62
|
-
if (await anyExistsUpward(cwd, ["CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"])) return "CLAUDE.md";
|
|
63
|
-
return await anyExists(cwd, ["AGENTS.md"]) ? "AGENTS.md" : "CLAUDE.md";
|
|
64
|
-
},
|
|
65
|
-
skillDirs: [".claude/skills"],
|
|
66
|
-
sizeLimit: {
|
|
67
|
-
maxLines: 200,
|
|
68
|
-
warning: "Claude Code follows long instruction files less closely."
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// src/adapters/codex.ts
|
|
73
|
-
var codex = {
|
|
74
|
-
id: "codex",
|
|
75
|
-
name: "Codex",
|
|
76
|
-
delivery: "embedded",
|
|
77
|
-
detect: (cwd) => anyExists(cwd, [".codex", "AGENTS.md"]),
|
|
78
|
-
instructionFile: () => "AGENTS.md",
|
|
79
|
-
skillDirs: [".agents/skills"],
|
|
80
|
-
sizeLimit: {
|
|
81
|
-
// Real limit is 32 KiB for all AGENTS.md files combined (spec §9.6).
|
|
82
|
-
maxBytes: 24 * 1024,
|
|
83
|
-
warning: "Codex may stop reading long instruction files.\nThe Engly block could be cut off."
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// src/adapters/copilot.ts
|
|
88
|
-
var copilot = {
|
|
89
|
-
id: "copilot",
|
|
90
|
-
name: "GitHub Copilot",
|
|
91
|
-
delivery: "embedded",
|
|
92
|
-
detect: (cwd) => anyExists(cwd, [".github/copilot-instructions.md", ".github/instructions"]),
|
|
93
|
-
instructionFile: () => ".github/copilot-instructions.md",
|
|
94
|
-
skillDirs: [".agents/skills", ".claude/skills", ".github/skills"]
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// src/adapters/cursor.ts
|
|
98
|
-
var cursor = {
|
|
99
|
-
id: "cursor",
|
|
100
|
-
name: "Cursor",
|
|
101
|
-
delivery: "own-file",
|
|
102
|
-
detect: (cwd) => anyExists(cwd, [".cursor", ".cursorrules"]),
|
|
103
|
-
instructionFile: () => ".cursor/rules/engly.mdc",
|
|
104
|
-
skillDirs: [".agents/skills", ".cursor/skills", ".claude/skills"]
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// src/adapters/opencode.ts
|
|
108
|
-
var opencode = {
|
|
109
|
-
id: "opencode",
|
|
110
|
-
name: "OpenCode",
|
|
111
|
-
delivery: "embedded",
|
|
112
|
-
detect: (cwd) => anyExists(cwd, ["opencode.json", "opencode.jsonc", ".opencode"]),
|
|
113
|
-
instructionFile: () => "AGENTS.md",
|
|
114
|
-
skillDirs: [".agents/skills", ".claude/skills", ".opencode/skills"]
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// src/adapters/types.ts
|
|
118
|
-
var TOOL_IDS = ["claude", "codex", "opencode", "cursor", "copilot"];
|
|
119
|
-
|
|
120
|
-
// src/adapters/index.ts
|
|
121
|
-
var adapters = { claude, codex, opencode, cursor, copilot };
|
|
122
|
-
var IMPLEMENTED_TOOLS = ["claude"];
|
|
123
|
-
function skillTargets(tools) {
|
|
124
|
-
const chosen = [];
|
|
125
|
-
const byChoices = [...tools].sort((a, b) => adapters[a].skillDirs.length - adapters[b].skillDirs.length);
|
|
126
|
-
for (const tool of byChoices) {
|
|
127
|
-
const dirs = adapters[tool].skillDirs;
|
|
128
|
-
const [preferred] = dirs;
|
|
129
|
-
if (preferred && !dirs.some((dir) => chosen.includes(dir))) chosen.push(preferred);
|
|
130
|
-
}
|
|
131
|
-
return chosen;
|
|
132
|
-
}
|
|
133
|
-
async function detectTools(cwd) {
|
|
134
|
-
const found = [];
|
|
135
|
-
for (const adapter of Object.values(adapters)) {
|
|
136
|
-
if (await adapter.detect(cwd)) found.push(adapter.id);
|
|
137
|
-
}
|
|
138
|
-
return found;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
20
|
// src/core/block.ts
|
|
142
21
|
var START_MARKER_PREFIX = "<!-- engly:start";
|
|
143
22
|
var END_MARKER = "<!-- engly:end -->";
|
|
@@ -176,6 +55,13 @@ function findBlock(text) {
|
|
|
176
55
|
function hasBlock(text) {
|
|
177
56
|
return findBlock(text) !== null;
|
|
178
57
|
}
|
|
58
|
+
function readBlock(text) {
|
|
59
|
+
const range = findBlock(text);
|
|
60
|
+
if (!range) return null;
|
|
61
|
+
const inner = text.slice(range.start, range.end);
|
|
62
|
+
const afterStart = inner.indexOf("-->") + 3;
|
|
63
|
+
return stripOneEol(inner.slice(afterStart, inner.length - END_MARKER.length), "both");
|
|
64
|
+
}
|
|
179
65
|
function buildBlock(content, version, eol = "\n") {
|
|
180
66
|
const body = content.replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
181
67
|
const lines = [startMarker(version), ...body === "" ? [] : body.split("\n"), END_MARKER];
|
|
@@ -197,9 +83,33 @@ function upsertBlock(text, content, options) {
|
|
|
197
83
|
const separator = endsWithEol(text) ? eol : eol + eol;
|
|
198
84
|
return { text: text + separator + block + eol, action: "appended" };
|
|
199
85
|
}
|
|
86
|
+
function removeBlock(text) {
|
|
87
|
+
const range = findBlock(text);
|
|
88
|
+
if (!range) return { text, removed: false };
|
|
89
|
+
let before = text.slice(0, range.start);
|
|
90
|
+
let after = stripOneEol(text.slice(range.end), "start");
|
|
91
|
+
if (before === "") {
|
|
92
|
+
after = stripOneEol(after, "start");
|
|
93
|
+
} else if (/(\r?\n)\r?\n$/.test(before)) {
|
|
94
|
+
before = stripOneEol(before, "end");
|
|
95
|
+
}
|
|
96
|
+
return { text: before + after, removed: true };
|
|
97
|
+
}
|
|
200
98
|
function endsWithEol(text) {
|
|
201
99
|
return text.endsWith("\n");
|
|
202
100
|
}
|
|
101
|
+
function stripOneEol(text, side) {
|
|
102
|
+
let result = text;
|
|
103
|
+
if (side === "start" || side === "both") {
|
|
104
|
+
if (result.startsWith("\r\n")) result = result.slice(2);
|
|
105
|
+
else if (result.startsWith("\n")) result = result.slice(1);
|
|
106
|
+
}
|
|
107
|
+
if (side === "end" || side === "both") {
|
|
108
|
+
if (result.endsWith("\r\n")) result = result.slice(0, -2);
|
|
109
|
+
else if (result.endsWith("\n")) result = result.slice(0, -1);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
203
113
|
function indexesOf(text, search) {
|
|
204
114
|
const result = [];
|
|
205
115
|
let index = text.indexOf(search);
|
|
@@ -212,18 +122,18 @@ function indexesOf(text, search) {
|
|
|
212
122
|
|
|
213
123
|
// src/core/config.ts
|
|
214
124
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
215
|
-
import { dirname as
|
|
125
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
216
126
|
|
|
217
127
|
// src/core/paths.ts
|
|
218
128
|
import { existsSync, readFileSync } from "fs";
|
|
219
|
-
import { dirname
|
|
129
|
+
import { dirname, join } from "path";
|
|
220
130
|
import { fileURLToPath } from "url";
|
|
221
131
|
var cachedRoot;
|
|
222
132
|
function packageRoot() {
|
|
223
133
|
if (cachedRoot) return cachedRoot;
|
|
224
|
-
let dir =
|
|
134
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
225
135
|
while (true) {
|
|
226
|
-
const pkgPath =
|
|
136
|
+
const pkgPath = join(dir, "package.json");
|
|
227
137
|
if (existsSync(pkgPath)) {
|
|
228
138
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
229
139
|
if (pkg.name === "engly") {
|
|
@@ -231,17 +141,23 @@ function packageRoot() {
|
|
|
231
141
|
return dir;
|
|
232
142
|
}
|
|
233
143
|
}
|
|
234
|
-
const parent =
|
|
144
|
+
const parent = dirname(dir);
|
|
235
145
|
if (parent === dir) throw new Error("Could not locate the engly package root.");
|
|
236
146
|
dir = parent;
|
|
237
147
|
}
|
|
238
148
|
}
|
|
239
149
|
function templatesDir() {
|
|
240
|
-
return
|
|
150
|
+
return join(packageRoot(), "templates");
|
|
151
|
+
}
|
|
152
|
+
var cachedInfo;
|
|
153
|
+
function packageInfo() {
|
|
154
|
+
if (cachedInfo) return cachedInfo;
|
|
155
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot(), "package.json"), "utf8"));
|
|
156
|
+
cachedInfo = { name: pkg.name, version: pkg.version };
|
|
157
|
+
return cachedInfo;
|
|
241
158
|
}
|
|
242
159
|
function packageVersion() {
|
|
243
|
-
|
|
244
|
-
return pkg.version;
|
|
160
|
+
return packageInfo().version;
|
|
245
161
|
}
|
|
246
162
|
var ENGLY_DIR = ".engly";
|
|
247
163
|
var CONFIG_FILE = `${ENGLY_DIR}/config.json`;
|
|
@@ -268,7 +184,7 @@ var ConfigError = class extends Error {
|
|
|
268
184
|
}
|
|
269
185
|
};
|
|
270
186
|
function configPath(cwd) {
|
|
271
|
-
return
|
|
187
|
+
return join2(cwd, CONFIG_FILE);
|
|
272
188
|
}
|
|
273
189
|
async function readConfig(cwd, version) {
|
|
274
190
|
let raw;
|
|
@@ -291,7 +207,7 @@ async function readConfig(cwd, version) {
|
|
|
291
207
|
}
|
|
292
208
|
async function writeConfig(cwd, config) {
|
|
293
209
|
const path = configPath(cwd);
|
|
294
|
-
await mkdir(
|
|
210
|
+
await mkdir(dirname2(path), { recursive: true });
|
|
295
211
|
await writeFile(path, `${JSON.stringify(validateConfig(config), null, 2)}
|
|
296
212
|
`, "utf8");
|
|
297
213
|
}
|
|
@@ -315,8 +231,8 @@ function validateConfig(config) {
|
|
|
315
231
|
}
|
|
316
232
|
|
|
317
233
|
// src/core/fs.ts
|
|
318
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
319
|
-
import { dirname as
|
|
234
|
+
import { access, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
235
|
+
import { dirname as dirname3 } from "path";
|
|
320
236
|
async function readTextIfExists(path) {
|
|
321
237
|
try {
|
|
322
238
|
return await readFile2(path, "utf8");
|
|
@@ -326,22 +242,132 @@ async function readTextIfExists(path) {
|
|
|
326
242
|
}
|
|
327
243
|
}
|
|
328
244
|
async function writeText(path, content) {
|
|
329
|
-
await mkdir2(
|
|
245
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
330
246
|
await writeFile2(path, content, "utf8");
|
|
331
247
|
}
|
|
248
|
+
async function pathExists(path) {
|
|
249
|
+
try {
|
|
250
|
+
await access(path);
|
|
251
|
+
return true;
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
332
256
|
|
|
333
|
-
// src/core/
|
|
334
|
-
import {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
257
|
+
// src/core/install.ts
|
|
258
|
+
import { rm, rmdir } from "fs/promises";
|
|
259
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
260
|
+
|
|
261
|
+
// src/adapters/detect.ts
|
|
262
|
+
import { access as access2 } from "fs/promises";
|
|
263
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
264
|
+
async function anyExists(cwd, paths) {
|
|
265
|
+
for (const path of paths) {
|
|
266
|
+
try {
|
|
267
|
+
await access2(join3(cwd, path));
|
|
268
|
+
return true;
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
339
273
|
}
|
|
340
|
-
function
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
274
|
+
async function anyExistsUpward(cwd, paths) {
|
|
275
|
+
let dir = cwd;
|
|
276
|
+
while (true) {
|
|
277
|
+
if (await anyExists(dir, paths)) return true;
|
|
278
|
+
const parent = dirname4(dir);
|
|
279
|
+
if (parent === dir) return false;
|
|
280
|
+
dir = parent;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/adapters/claude.ts
|
|
285
|
+
var claude = {
|
|
286
|
+
id: "claude",
|
|
287
|
+
name: "Claude Code",
|
|
288
|
+
delivery: "import",
|
|
289
|
+
detect: (cwd) => anyExists(cwd, ["CLAUDE.md", ".claude"]),
|
|
290
|
+
// Claude Code reads AGENTS.md only when there is no CLAUDE.md, .claude/CLAUDE.md or
|
|
291
|
+
// CLAUDE.local.md in the working directory or any directory above it.
|
|
292
|
+
instructionFile: async (cwd) => {
|
|
293
|
+
if (await anyExistsUpward(cwd, ["CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"])) return "CLAUDE.md";
|
|
294
|
+
return await anyExists(cwd, ["AGENTS.md"]) ? "AGENTS.md" : "CLAUDE.md";
|
|
295
|
+
},
|
|
296
|
+
instructionFiles: ["CLAUDE.md", "AGENTS.md"],
|
|
297
|
+
skillDirs: [".claude/skills"],
|
|
298
|
+
sizeLimit: {
|
|
299
|
+
maxLines: 200,
|
|
300
|
+
warning: "Claude Code follows long instruction files less closely."
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/adapters/codex.ts
|
|
305
|
+
var codex = {
|
|
306
|
+
id: "codex",
|
|
307
|
+
name: "Codex",
|
|
308
|
+
delivery: "embedded",
|
|
309
|
+
detect: (cwd) => anyExists(cwd, [".codex", "AGENTS.md"]),
|
|
310
|
+
instructionFile: () => "AGENTS.md",
|
|
311
|
+
skillDirs: [".agents/skills"],
|
|
312
|
+
sizeLimit: {
|
|
313
|
+
// Real limit is 32 KiB for all AGENTS.md files combined (spec §9.6).
|
|
314
|
+
maxBytes: 24 * 1024,
|
|
315
|
+
warning: "Codex may stop reading long instruction files.\nThe Engly block could be cut off."
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/adapters/copilot.ts
|
|
320
|
+
var copilot = {
|
|
321
|
+
id: "copilot",
|
|
322
|
+
name: "GitHub Copilot",
|
|
323
|
+
delivery: "embedded",
|
|
324
|
+
detect: (cwd) => anyExists(cwd, [".github/copilot-instructions.md", ".github/instructions"]),
|
|
325
|
+
instructionFile: () => ".github/copilot-instructions.md",
|
|
326
|
+
skillDirs: [".agents/skills", ".claude/skills", ".github/skills"]
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/adapters/cursor.ts
|
|
330
|
+
var cursor = {
|
|
331
|
+
id: "cursor",
|
|
332
|
+
name: "Cursor",
|
|
333
|
+
delivery: "own-file",
|
|
334
|
+
detect: (cwd) => anyExists(cwd, [".cursor", ".cursorrules"]),
|
|
335
|
+
instructionFile: () => ".cursor/rules/engly.mdc",
|
|
336
|
+
skillDirs: [".agents/skills", ".cursor/skills", ".claude/skills"]
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// src/adapters/opencode.ts
|
|
340
|
+
var opencode = {
|
|
341
|
+
id: "opencode",
|
|
342
|
+
name: "OpenCode",
|
|
343
|
+
delivery: "embedded",
|
|
344
|
+
detect: (cwd) => anyExists(cwd, ["opencode.json", "opencode.jsonc", ".opencode"]),
|
|
345
|
+
instructionFile: () => "AGENTS.md",
|
|
346
|
+
skillDirs: [".agents/skills", ".claude/skills", ".opencode/skills"]
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// src/adapters/types.ts
|
|
350
|
+
var TOOL_IDS = ["claude", "codex", "opencode", "cursor", "copilot"];
|
|
351
|
+
|
|
352
|
+
// src/adapters/index.ts
|
|
353
|
+
var adapters = { claude, codex, opencode, cursor, copilot };
|
|
354
|
+
var IMPLEMENTED_TOOLS = ["claude"];
|
|
355
|
+
function skillTargets(tools) {
|
|
356
|
+
const chosen = [];
|
|
357
|
+
const byChoices = [...tools].sort((a, b) => adapters[a].skillDirs.length - adapters[b].skillDirs.length);
|
|
358
|
+
for (const tool of byChoices) {
|
|
359
|
+
const dirs = adapters[tool].skillDirs;
|
|
360
|
+
const [preferred] = dirs;
|
|
361
|
+
if (preferred && !dirs.some((dir) => chosen.includes(dir))) chosen.push(preferred);
|
|
362
|
+
}
|
|
363
|
+
return chosen;
|
|
364
|
+
}
|
|
365
|
+
async function detectTools(cwd) {
|
|
366
|
+
const found = [];
|
|
367
|
+
for (const adapter of Object.values(adapters)) {
|
|
368
|
+
if (await adapter.detect(cwd)) found.push(adapter.id);
|
|
369
|
+
}
|
|
370
|
+
return found;
|
|
345
371
|
}
|
|
346
372
|
|
|
347
373
|
// src/core/instructions.ts
|
|
@@ -363,6 +389,179 @@ async function readSkill(name) {
|
|
|
363
389
|
return readFile3(join4(templatesDir(), "skills", name, "SKILL.md"), "utf8");
|
|
364
390
|
}
|
|
365
391
|
|
|
392
|
+
// src/core/install.ts
|
|
393
|
+
async function writeInstructions(cwd, config) {
|
|
394
|
+
await writeText(join5(cwd, INSTRUCTIONS_FILE), await renderInstructions(config));
|
|
395
|
+
}
|
|
396
|
+
async function installSkills(cwd, tools) {
|
|
397
|
+
for (const dir of skillTargets(tools)) {
|
|
398
|
+
for (const skill of SKILL_NAMES) {
|
|
399
|
+
await writeText(join5(cwd, dir, skill, "SKILL.md"), await readSkill(skill));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function skillStatus(cwd, tools) {
|
|
404
|
+
const folders = [];
|
|
405
|
+
for (const dir of skillTargets(tools)) {
|
|
406
|
+
const missing = [];
|
|
407
|
+
for (const skill of SKILL_NAMES) {
|
|
408
|
+
if (!await pathExists(join5(cwd, dir, skill, "SKILL.md"))) missing.push(skill);
|
|
409
|
+
}
|
|
410
|
+
folders.push({ dir, missing });
|
|
411
|
+
}
|
|
412
|
+
return folders;
|
|
413
|
+
}
|
|
414
|
+
async function removeSkills(cwd) {
|
|
415
|
+
const dirs = new Set(Object.values(adapters).flatMap((adapter) => adapter.skillDirs));
|
|
416
|
+
const removed = [];
|
|
417
|
+
for (const dir of dirs) {
|
|
418
|
+
let removedHere = false;
|
|
419
|
+
for (const skill of SKILL_NAMES) {
|
|
420
|
+
const path = join5(cwd, dir, skill);
|
|
421
|
+
if (!await pathExists(path)) continue;
|
|
422
|
+
await rm(path, { recursive: true, force: true });
|
|
423
|
+
removed.push(join5(dir, skill));
|
|
424
|
+
removedHere = true;
|
|
425
|
+
}
|
|
426
|
+
if (removedHere) await removeEmptyDirs(cwd, dir);
|
|
427
|
+
}
|
|
428
|
+
return removed;
|
|
429
|
+
}
|
|
430
|
+
async function hasInstalledSkills(cwd) {
|
|
431
|
+
const dirs = new Set(Object.values(adapters).flatMap((adapter) => adapter.skillDirs));
|
|
432
|
+
for (const dir of dirs) {
|
|
433
|
+
for (const skill of SKILL_NAMES) {
|
|
434
|
+
if (await pathExists(join5(cwd, dir, skill))) return true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
async function removeEmptyDirs(cwd, relativeDir) {
|
|
440
|
+
let dir = relativeDir;
|
|
441
|
+
for (let level = 0; level < 2 && dir !== "."; level++) {
|
|
442
|
+
try {
|
|
443
|
+
await rmdir(join5(cwd, dir));
|
|
444
|
+
} catch {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
dir = dirname5(dir);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/core/project.ts
|
|
452
|
+
import { join as join6 } from "path";
|
|
453
|
+
function blockContent(enabled) {
|
|
454
|
+
return enabled ? `@${INSTRUCTIONS_FILE}` : "";
|
|
455
|
+
}
|
|
456
|
+
async function inspectAgentFile(cwd, file) {
|
|
457
|
+
const path = join6(cwd, file);
|
|
458
|
+
const text = await readTextIfExists(path);
|
|
459
|
+
if (text === null) return { file, path, text: "", state: "missing" };
|
|
460
|
+
try {
|
|
461
|
+
const content = readBlock(text);
|
|
462
|
+
if (content === null) return { file, path, text, state: "no-block" };
|
|
463
|
+
return { file, path, text, state: content.trim() === "" ? "inactive" : "active" };
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (error instanceof BlockError) return { file, path, text, state: "broken", error: error.message };
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function candidateFiles(cwd, tools) {
|
|
470
|
+
const files = /* @__PURE__ */ new Set();
|
|
471
|
+
for (const tool of tools) {
|
|
472
|
+
if (!IMPLEMENTED_TOOLS.includes(tool)) continue;
|
|
473
|
+
const adapter = adapters[tool];
|
|
474
|
+
for (const file of adapter.instructionFiles ?? [await adapter.instructionFile(cwd)]) files.add(file);
|
|
475
|
+
}
|
|
476
|
+
return [...files];
|
|
477
|
+
}
|
|
478
|
+
async function inspectAgentFiles(cwd, tools) {
|
|
479
|
+
const files = [];
|
|
480
|
+
for (const file of await candidateFiles(cwd, tools)) {
|
|
481
|
+
const inspected = await inspectAgentFile(cwd, file);
|
|
482
|
+
if (inspected.state !== "missing") files.push(inspected);
|
|
483
|
+
}
|
|
484
|
+
return files;
|
|
485
|
+
}
|
|
486
|
+
async function findBlocks(cwd, tools) {
|
|
487
|
+
const files = await inspectAgentFiles(cwd, tools);
|
|
488
|
+
for (const file of files) {
|
|
489
|
+
if (file.state === "broken") throw new BlockError(`${file.file}: ${file.error}`);
|
|
490
|
+
}
|
|
491
|
+
return files.filter((file) => file.state === "active" || file.state === "inactive");
|
|
492
|
+
}
|
|
493
|
+
async function requireConfig(cwd, version) {
|
|
494
|
+
const config = await readConfig(cwd, version);
|
|
495
|
+
if (!config) throw new Error('Engly is not installed here. Run "engly init" first.');
|
|
496
|
+
return config;
|
|
497
|
+
}
|
|
498
|
+
function configuredTools(config) {
|
|
499
|
+
const tools = config?.tools.filter((tool) => IMPLEMENTED_TOOLS.includes(tool)) ?? [];
|
|
500
|
+
return tools.length > 0 ? tools : [...IMPLEMENTED_TOOLS];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/commands/set-enabled.ts
|
|
504
|
+
async function setEnabled(cwd, enabled) {
|
|
505
|
+
const version = packageVersion();
|
|
506
|
+
const word = enabled ? "enabled" : "disabled";
|
|
507
|
+
p.intro(`Engly v${version}`);
|
|
508
|
+
const config = await requireConfig(cwd, version);
|
|
509
|
+
const tools = configuredTools(config);
|
|
510
|
+
const blocks = await findBlocks(cwd, tools);
|
|
511
|
+
if (blocks.length === 0) {
|
|
512
|
+
const files = (await candidateFiles(cwd, tools)).join(" or ");
|
|
513
|
+
p.log.warn(`No Engly block found in ${files}. Run "engly init" to add it.`);
|
|
514
|
+
p.outro("Nothing changed.");
|
|
515
|
+
process.exitCode = 1;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const pending = blocks.filter((block) => block.state === "active" !== enabled);
|
|
519
|
+
if (pending.length === 0 && config.enabled === enabled) {
|
|
520
|
+
p.outro(`Engly is already ${word}.`);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
for (const block of pending) {
|
|
524
|
+
const { text } = upsertBlock(block.text, blockContent(enabled), { version: config.version });
|
|
525
|
+
await writeText(block.path, text);
|
|
526
|
+
}
|
|
527
|
+
if (enabled && !await pathExists(join7(cwd, INSTRUCTIONS_FILE))) {
|
|
528
|
+
await writeInstructions(cwd, config);
|
|
529
|
+
}
|
|
530
|
+
config.enabled = enabled;
|
|
531
|
+
await writeConfig(cwd, config);
|
|
532
|
+
const where = pending.length > 0 ? ` in ${pending.map((block) => block.file).join(" and ")}` : "";
|
|
533
|
+
p.log.success(`\u2713 Engly ${word}${where}`);
|
|
534
|
+
p.outro("Takes effect in your next agent session.");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/commands/disable.ts
|
|
538
|
+
async function disableCommand() {
|
|
539
|
+
await setEnabled(process.cwd(), false);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/commands/enable.ts
|
|
543
|
+
async function enableCommand() {
|
|
544
|
+
await setEnabled(process.cwd(), true);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/commands/init.ts
|
|
548
|
+
import { join as join8 } from "path";
|
|
549
|
+
import * as p2 from "@clack/prompts";
|
|
550
|
+
|
|
551
|
+
// src/core/git.ts
|
|
552
|
+
import { spawnSync } from "child_process";
|
|
553
|
+
function git(cwd, args) {
|
|
554
|
+
const result = spawnSync("git", args, { cwd, stdio: "ignore" });
|
|
555
|
+
if (result.error) return null;
|
|
556
|
+
return result.status;
|
|
557
|
+
}
|
|
558
|
+
function gitStatus(cwd, relativePath) {
|
|
559
|
+
if (git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== 0) return "no-git";
|
|
560
|
+
if (git(cwd, ["ls-files", "--error-unmatch", "--", relativePath]) === 0) return "tracked";
|
|
561
|
+
if (git(cwd, ["check-ignore", "-q", "--", relativePath]) === 0) return "ignored";
|
|
562
|
+
return "untracked";
|
|
563
|
+
}
|
|
564
|
+
|
|
366
565
|
// src/core/size.ts
|
|
367
566
|
function checkSize(text, limit) {
|
|
368
567
|
if (!limit) return null;
|
|
@@ -381,7 +580,7 @@ function checkSize(text, limit) {
|
|
|
381
580
|
var CancelledError = class extends Error {
|
|
382
581
|
};
|
|
383
582
|
function guard(value) {
|
|
384
|
-
if (
|
|
583
|
+
if (p2.isCancel(value)) throw new CancelledError();
|
|
385
584
|
return value;
|
|
386
585
|
}
|
|
387
586
|
async function initCommand(options) {
|
|
@@ -389,21 +588,20 @@ async function initCommand(options) {
|
|
|
389
588
|
await runInit(options);
|
|
390
589
|
} catch (error) {
|
|
391
590
|
if (error instanceof CancelledError) {
|
|
392
|
-
|
|
591
|
+
p2.cancel("Cancelled. Nothing was changed.");
|
|
393
592
|
process.exitCode = 1;
|
|
394
593
|
return;
|
|
395
594
|
}
|
|
396
595
|
throw error;
|
|
397
596
|
}
|
|
398
597
|
}
|
|
399
|
-
async function runInit(options) {
|
|
400
|
-
const cwd = process.cwd();
|
|
598
|
+
async function runInit(options, cwd = process.cwd()) {
|
|
401
599
|
const version = packageVersion();
|
|
402
600
|
const interactive = !options.yes;
|
|
403
601
|
if (interactive && !process.stdin.isTTY) {
|
|
404
602
|
throw new Error('No interactive terminal detected. Run "engly init --yes" to use the defaults.');
|
|
405
603
|
}
|
|
406
|
-
|
|
604
|
+
p2.intro(`Engly v${version} \u2013 your English coach for AI prompts`);
|
|
407
605
|
const config = await readConfig(cwd, version) ?? defaultConfig(version);
|
|
408
606
|
config.version = version;
|
|
409
607
|
const tools = await chooseTools(cwd, options);
|
|
@@ -413,9 +611,9 @@ async function runInit(options) {
|
|
|
413
611
|
config.style = await chooseStyle(options, config.style);
|
|
414
612
|
const adapter = adapters.claude;
|
|
415
613
|
const agentFile = await adapter.instructionFile(cwd);
|
|
416
|
-
const agentPath =
|
|
614
|
+
const agentPath = join8(cwd, agentFile);
|
|
417
615
|
if (agentFile === "AGENTS.md") {
|
|
418
|
-
|
|
616
|
+
p2.log.info("No CLAUDE.md found. Using AGENTS.md, which Claude Code reads when there is no CLAUDE.md.");
|
|
419
617
|
}
|
|
420
618
|
const current = await readTextIfExists(agentPath);
|
|
421
619
|
const blockExists = current !== null && hasBlock(current);
|
|
@@ -426,10 +624,10 @@ async function runInit(options) {
|
|
|
426
624
|
if (placement === "skip") apply = false;
|
|
427
625
|
else position = placement;
|
|
428
626
|
}
|
|
429
|
-
const planned = upsertBlock(current ?? "",
|
|
627
|
+
const planned = upsertBlock(current ?? "", blockContent(true), { version, position });
|
|
430
628
|
if (apply && interactive) {
|
|
431
629
|
const choice = guard(
|
|
432
|
-
await
|
|
630
|
+
await p2.select({
|
|
433
631
|
message: `Add Engly block to:
|
|
434
632
|
${agentFile} (${adapter.name}) \u2192 ${describePlan(current, planned.action)}`,
|
|
435
633
|
options: [
|
|
@@ -444,22 +642,18 @@ ${agentFile} (${adapter.name}) \u2192 ${describePlan(current, planned.action)}
|
|
|
444
642
|
await warnIfInGit(cwd, agentFile, interactive);
|
|
445
643
|
}
|
|
446
644
|
const done = [];
|
|
447
|
-
await
|
|
645
|
+
await writeInstructions(cwd, config);
|
|
448
646
|
if (apply) {
|
|
449
647
|
await writeText(agentPath, planned.text);
|
|
450
648
|
done.push(`\u2713 ${agentFile} ${current === null ? "created" : "updated"}`);
|
|
451
649
|
}
|
|
452
|
-
|
|
453
|
-
for (const skill of SKILL_NAMES) {
|
|
454
|
-
await writeText(join5(cwd, dir, skill, "SKILL.md"), await readSkill(skill));
|
|
455
|
-
}
|
|
456
|
-
}
|
|
650
|
+
await installSkills(cwd, tools);
|
|
457
651
|
done.push(`\u2713 Skills installed: ${SKILL_NAMES.join(", ")}`);
|
|
458
652
|
config.tools = tools;
|
|
459
653
|
config.enabled = true;
|
|
460
654
|
await writeConfig(cwd, config);
|
|
461
|
-
|
|
462
|
-
|
|
655
|
+
p2.log.success(done.join("\n"));
|
|
656
|
+
p2.outro(
|
|
463
657
|
apply ? "Done! Your next prompt will be coached." : `Skills installed. Run "engly init" again to add the Engly block to ${agentFile}.`
|
|
464
658
|
);
|
|
465
659
|
}
|
|
@@ -477,7 +671,7 @@ async function chooseTools(cwd, options) {
|
|
|
477
671
|
const defaults = initial.length > 0 ? initial : [...IMPLEMENTED_TOOLS];
|
|
478
672
|
if (options.yes) return defaults;
|
|
479
673
|
return guard(
|
|
480
|
-
await
|
|
674
|
+
await p2.multiselect({
|
|
481
675
|
message: "Which AI tools do you use?",
|
|
482
676
|
options: TOOL_IDS.map((id) => ({
|
|
483
677
|
value: id,
|
|
@@ -493,7 +687,7 @@ async function chooseTools(cwd, options) {
|
|
|
493
687
|
function keepImplemented(tools) {
|
|
494
688
|
const skipped = tools.filter((tool) => !IMPLEMENTED_TOOLS.includes(tool));
|
|
495
689
|
if (skipped.length > 0) {
|
|
496
|
-
|
|
690
|
+
p2.log.warn(`Not supported yet, skipped: ${skipped.map((tool) => adapters[tool].name).join(", ")}`);
|
|
497
691
|
}
|
|
498
692
|
return tools.filter((tool) => IMPLEMENTED_TOOLS.includes(tool));
|
|
499
693
|
}
|
|
@@ -506,7 +700,7 @@ async function chooseStyle(options, current) {
|
|
|
506
700
|
}
|
|
507
701
|
if (options.yes) return current;
|
|
508
702
|
return guard(
|
|
509
|
-
await
|
|
703
|
+
await p2.select({
|
|
510
704
|
message: "How detailed should the feedback be?",
|
|
511
705
|
initialValue: current,
|
|
512
706
|
options: [
|
|
@@ -526,13 +720,13 @@ async function choosePlacement(file, size, interactive) {
|
|
|
526
720
|
const message = `${file} is ${size.label}
|
|
527
721
|
\u26A0 ${size.warning}`;
|
|
528
722
|
if (!interactive) {
|
|
529
|
-
|
|
723
|
+
p2.log.warn(`${message}
|
|
530
724
|
The Engly block goes near the top.`);
|
|
531
725
|
return "top";
|
|
532
726
|
}
|
|
533
|
-
|
|
727
|
+
p2.log.warn(message);
|
|
534
728
|
return guard(
|
|
535
|
-
await
|
|
729
|
+
await p2.select({
|
|
536
730
|
message: "Where should the block go?",
|
|
537
731
|
options: [
|
|
538
732
|
{ value: "top", label: "Near the top \u2013 recommended" },
|
|
@@ -548,12 +742,12 @@ async function warnIfInGit(cwd, file, interactive) {
|
|
|
548
742
|
const title = status === "tracked" ? `${file} is tracked by git.` : `${file} is not ignored by git.`;
|
|
549
743
|
const warning = "\u26A0 Your teammates will also be coached if you commit it.";
|
|
550
744
|
if (!interactive) {
|
|
551
|
-
|
|
745
|
+
p2.log.warn(`${title}
|
|
552
746
|
${warning}`);
|
|
553
747
|
return;
|
|
554
748
|
}
|
|
555
749
|
const choice = guard(
|
|
556
|
-
await
|
|
750
|
+
await p2.select({
|
|
557
751
|
message: `${title}
|
|
558
752
|
${warning}`,
|
|
559
753
|
options: [
|
|
@@ -566,30 +760,243 @@ ${warning}`,
|
|
|
566
760
|
}
|
|
567
761
|
|
|
568
762
|
// src/commands/remove.ts
|
|
569
|
-
|
|
570
|
-
|
|
763
|
+
import { rm as rm2 } from "fs/promises";
|
|
764
|
+
import { join as join9 } from "path";
|
|
765
|
+
import * as p3 from "@clack/prompts";
|
|
766
|
+
async function removeCommand(options) {
|
|
767
|
+
await runRemove(process.cwd(), options);
|
|
768
|
+
}
|
|
769
|
+
async function runRemove(cwd, options) {
|
|
770
|
+
const version = packageVersion();
|
|
771
|
+
p3.intro(`Engly v${version}`);
|
|
772
|
+
let config = null;
|
|
773
|
+
try {
|
|
774
|
+
config = await readConfig(cwd, version);
|
|
775
|
+
} catch (error) {
|
|
776
|
+
if (!(error instanceof ConfigError)) throw error;
|
|
777
|
+
}
|
|
778
|
+
const blocks = await findBlocks(cwd, config ? configuredTools(config) : [...IMPLEMENTED_TOOLS]);
|
|
779
|
+
const hasEnglyDir = await pathExists(join9(cwd, ENGLY_DIR));
|
|
780
|
+
const hasSkills = await hasInstalledSkills(cwd);
|
|
781
|
+
if (blocks.length === 0 && !hasEnglyDir && !hasSkills) {
|
|
782
|
+
p3.outro("Nothing to remove.");
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const plan = [
|
|
786
|
+
...blocks.map((block) => `The Engly block in ${block.file}`),
|
|
787
|
+
...hasEnglyDir ? [`The ${ENGLY_DIR}/ folder (settings and instructions)`] : [],
|
|
788
|
+
...hasSkills ? [`The skills: ${SKILL_NAMES.join(", ")}`] : []
|
|
789
|
+
];
|
|
790
|
+
if (!options.yes) {
|
|
791
|
+
if (!process.stdin.isTTY) {
|
|
792
|
+
throw new Error('No interactive terminal detected. Run "engly remove --yes" to remove without asking.');
|
|
793
|
+
}
|
|
794
|
+
const confirmed = await p3.confirm({
|
|
795
|
+
message: `Remove Engly from this project?
|
|
796
|
+
${plan.map((line) => ` \u2022 ${line}`).join("\n")}`
|
|
797
|
+
});
|
|
798
|
+
if (p3.isCancel(confirmed) || !confirmed) {
|
|
799
|
+
p3.cancel("Cancelled. Nothing was changed.");
|
|
800
|
+
process.exitCode = 1;
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const done = [];
|
|
805
|
+
for (const block of blocks) {
|
|
806
|
+
await writeText(block.path, removeBlock(block.text).text);
|
|
807
|
+
done.push(`\u2713 Engly block removed from ${block.file}`);
|
|
808
|
+
}
|
|
809
|
+
if (hasSkills) {
|
|
810
|
+
await removeSkills(cwd);
|
|
811
|
+
done.push(`\u2713 Skills removed: ${SKILL_NAMES.join(", ")}`);
|
|
812
|
+
}
|
|
813
|
+
if (hasEnglyDir) {
|
|
814
|
+
await rm2(join9(cwd, ENGLY_DIR), { recursive: true, force: true });
|
|
815
|
+
done.push(`\u2713 ${ENGLY_DIR}/ removed`);
|
|
816
|
+
}
|
|
817
|
+
p3.log.success(done.join("\n"));
|
|
818
|
+
p3.outro("Engly removed. Everything outside the Engly block was left untouched.");
|
|
571
819
|
}
|
|
572
820
|
|
|
573
821
|
// src/commands/status.ts
|
|
822
|
+
import { join as join10 } from "path";
|
|
823
|
+
import * as p4 from "@clack/prompts";
|
|
574
824
|
async function statusCommand() {
|
|
575
|
-
|
|
825
|
+
await runStatus(process.cwd());
|
|
826
|
+
}
|
|
827
|
+
async function runStatus(cwd) {
|
|
828
|
+
const version = packageVersion();
|
|
829
|
+
p4.intro(`Engly v${version}`);
|
|
830
|
+
let config = null;
|
|
831
|
+
let configError = null;
|
|
832
|
+
try {
|
|
833
|
+
config = await readConfig(cwd, version);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (!(error instanceof ConfigError)) throw error;
|
|
836
|
+
configError = error.message;
|
|
837
|
+
}
|
|
838
|
+
const tools = configuredTools(config);
|
|
839
|
+
const files = await inspectAgentFiles(cwd, tools);
|
|
840
|
+
const withBlock = files.filter((file) => file.state === "active" || file.state === "inactive");
|
|
841
|
+
const broken = files.filter((file) => file.state === "broken");
|
|
842
|
+
const hasInstructions = await pathExists(join10(cwd, INSTRUCTIONS_FILE));
|
|
843
|
+
const skills = config ? await skillStatus(cwd, tools) : [];
|
|
844
|
+
const installed = config !== null || configError !== null || withBlock.length > 0 || broken.length > 0 || hasInstructions || await hasInstalledSkills(cwd);
|
|
845
|
+
if (!installed) {
|
|
846
|
+
p4.outro('Engly is not installed in this project. Run "engly init".');
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const active = withBlock.some((file) => file.state === "active");
|
|
850
|
+
const status = withBlock.length === 0 ? "off (no Engly block in the agent files)" : active ? "enabled" : "disabled";
|
|
851
|
+
const rows = [["Status", status]];
|
|
852
|
+
if (config) {
|
|
853
|
+
rows.push(
|
|
854
|
+
["Version", config.version],
|
|
855
|
+
["Style", config.style],
|
|
856
|
+
["Level", config.level],
|
|
857
|
+
["Language", config.language],
|
|
858
|
+
["Tools", tools.map((tool) => adapters[tool].name).join(", ")]
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
rows.push(["Files", fileLines(files, hasInstructions)]);
|
|
862
|
+
if (skills.length > 0) {
|
|
863
|
+
rows.push([
|
|
864
|
+
"Skills",
|
|
865
|
+
skills.map((folder) => `${folder.dir} ${folder.missing.length === 0 ? "\u2713" : "\u2717"}`).join("\n")
|
|
866
|
+
]);
|
|
867
|
+
}
|
|
868
|
+
p4.note(formatRows(rows), "Engly status");
|
|
869
|
+
const warnings = await collectWarnings({ cwd, version, config, configError, tools, withBlock, broken, skills });
|
|
870
|
+
for (const warning of warnings) p4.log.warn(warning);
|
|
871
|
+
p4.outro(warnings.length === 0 ? "All good." : `${warnings.length} warning${warnings.length === 1 ? "" : "s"}.`);
|
|
872
|
+
}
|
|
873
|
+
async function collectWarnings(input) {
|
|
874
|
+
const { cwd, version, config, configError, tools, withBlock, broken, skills } = input;
|
|
875
|
+
const warnings = [];
|
|
876
|
+
if (configError) {
|
|
877
|
+
warnings.push(`${configError} Fix it by hand, or run "engly init" to write a new one.`);
|
|
878
|
+
} else if (!config) {
|
|
879
|
+
warnings.push(`${CONFIG_FILE} is missing. Run "engly init" to write it again.`);
|
|
880
|
+
} else if (config.version !== version) {
|
|
881
|
+
warnings.push(`This project uses Engly v${config.version} and the CLI is v${version}. Run "engly update".`);
|
|
882
|
+
}
|
|
883
|
+
for (const file of broken) warnings.push(`${file.file}: ${file.error}`);
|
|
884
|
+
if (withBlock.length === 0 && broken.length === 0) {
|
|
885
|
+
const names = (await candidateFiles(cwd, tools)).join(" or ");
|
|
886
|
+
warnings.push(`No Engly block found in ${names}, so nothing is coached. Run "engly init" to add it.`);
|
|
887
|
+
}
|
|
888
|
+
const active = withBlock.some((file) => file.state === "active");
|
|
889
|
+
if (config && withBlock.length > 0 && config.enabled !== active) {
|
|
890
|
+
warnings.push(
|
|
891
|
+
`${CONFIG_FILE} says Engly is ${config.enabled ? "enabled" : "disabled"}, but the block is ${active ? "active" : "empty"}. Run "engly ${config.enabled ? "enable" : "disable"}" to sync them.`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
if (active && !await pathExists(join10(cwd, INSTRUCTIONS_FILE))) {
|
|
895
|
+
warnings.push(`${INSTRUCTIONS_FILE} is missing, but the block imports it. Run "engly update".`);
|
|
896
|
+
}
|
|
897
|
+
for (const folder of skills) {
|
|
898
|
+
if (folder.missing.length > 0) {
|
|
899
|
+
warnings.push(`Missing skills in ${folder.dir}: ${folder.missing.join(", ")}. Run "engly update".`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
for (const file of withBlock) {
|
|
903
|
+
const size = sizeWarning(file, tools);
|
|
904
|
+
if (size) warnings.push(size);
|
|
905
|
+
}
|
|
906
|
+
return warnings;
|
|
907
|
+
}
|
|
908
|
+
function sizeWarning(file, tools) {
|
|
909
|
+
for (const tool of tools) {
|
|
910
|
+
const adapter = adapters[tool];
|
|
911
|
+
if (!adapter.instructionFiles?.includes(file.file)) continue;
|
|
912
|
+
const report = checkSize(file.text, adapter.sizeLimit);
|
|
913
|
+
if (report) return `${file.file} is ${report.label}. ${report.warning.replace(/\n/g, " ")}`;
|
|
914
|
+
}
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
function fileLines(files, hasInstructions) {
|
|
918
|
+
const lines = files.map((file) => {
|
|
919
|
+
switch (file.state) {
|
|
920
|
+
case "active":
|
|
921
|
+
return `${file.file} \u2713 (block active)`;
|
|
922
|
+
case "inactive":
|
|
923
|
+
return `${file.file} \u2713 (block empty, coach off)`;
|
|
924
|
+
case "broken":
|
|
925
|
+
return `${file.file} \u2717 (broken markers)`;
|
|
926
|
+
default:
|
|
927
|
+
return `${file.file} \u2013 (no Engly block)`;
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
lines.push(`${INSTRUCTIONS_FILE} ${hasInstructions ? "\u2713" : "\u2717"}`);
|
|
931
|
+
return lines.join("\n");
|
|
932
|
+
}
|
|
933
|
+
function formatRows(rows) {
|
|
934
|
+
const width = Math.max(...rows.map(([label]) => label.length)) + 2;
|
|
935
|
+
return rows.map(
|
|
936
|
+
([label, value]) => value.split("\n").map((line, index) => `${index === 0 ? `${label}:`.padEnd(width) : " ".repeat(width)}${line}`).join("\n")
|
|
937
|
+
).join("\n");
|
|
576
938
|
}
|
|
577
939
|
|
|
578
940
|
// src/commands/update.ts
|
|
941
|
+
import * as p5 from "@clack/prompts";
|
|
579
942
|
async function updateCommand() {
|
|
580
|
-
|
|
943
|
+
await runUpdate(process.cwd());
|
|
944
|
+
}
|
|
945
|
+
async function runUpdate(cwd) {
|
|
946
|
+
const version = packageVersion();
|
|
947
|
+
p5.intro(`Engly v${version}`);
|
|
948
|
+
const config = await requireConfig(cwd, version);
|
|
949
|
+
const previous = config.version;
|
|
950
|
+
const tools = configuredTools(config);
|
|
951
|
+
const blocks = await findBlocks(cwd, tools);
|
|
952
|
+
config.version = version;
|
|
953
|
+
await writeInstructions(cwd, config);
|
|
954
|
+
await installSkills(cwd, tools);
|
|
955
|
+
const done = ["\u2713 .engly/engly.md and skills refreshed"];
|
|
956
|
+
for (const block of blocks) {
|
|
957
|
+
const { text } = upsertBlock(block.text, blockContent(config.enabled), { version });
|
|
958
|
+
if (text === block.text) {
|
|
959
|
+
done.push(`\u2713 ${block.file} block is up to date`);
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
await writeText(block.path, text);
|
|
963
|
+
done.push(`\u2713 ${block.file} block updated`);
|
|
964
|
+
}
|
|
965
|
+
await writeConfig(cwd, config);
|
|
966
|
+
p5.log.success(done.join("\n"));
|
|
967
|
+
if (blocks.length === 0) {
|
|
968
|
+
const files = (await candidateFiles(cwd, tools)).join(" or ");
|
|
969
|
+
p5.log.warn(`No Engly block found in ${files}. Run "engly init" to add it.`);
|
|
970
|
+
}
|
|
971
|
+
if (!config.enabled) {
|
|
972
|
+
p5.log.warn('Engly is disabled. Run "engly enable" to turn it on.');
|
|
973
|
+
}
|
|
974
|
+
p5.log.info(
|
|
975
|
+
"This updates the files in this project. To update the engly CLI itself, run: npm install -g engly@latest"
|
|
976
|
+
);
|
|
977
|
+
p5.outro(previous === version ? `Already on v${version}.` : `Updated from v${previous} to v${version}.`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/core/update-check.ts
|
|
981
|
+
import updateNotifier from "update-notifier";
|
|
982
|
+
function notifyIfUpdateAvailable() {
|
|
983
|
+
try {
|
|
984
|
+
updateNotifier({ pkg: packageInfo() }).notify();
|
|
985
|
+
} catch {
|
|
986
|
+
}
|
|
581
987
|
}
|
|
582
988
|
|
|
583
989
|
// src/cli.ts
|
|
990
|
+
notifyIfUpdateAvailable();
|
|
584
991
|
var program = new Command();
|
|
585
|
-
program.name("engly").description("Your English coach for AI prompts. One command. Every agent. Always on.
|
|
992
|
+
program.name("engly").description("Your English coach for AI prompts. One command. Every agent. Always on.").version(packageVersion());
|
|
586
993
|
program.command("init").description("Install Engly in the current project").option("--tools <tools>", "comma-separated agents: claude,codex,opencode,cursor,copilot").option("--style <style>", "feedback style: light or detailed").option("-y, --yes", "accept all defaults without asking").option("--no-apply", "only install the skills, don't touch the agent files").action(initCommand);
|
|
587
994
|
program.command("update").description("Update the skills and regenerate the Engly block").action(updateCommand);
|
|
588
995
|
program.command("enable").description("Turn the coach on permanently").action(enableCommand);
|
|
589
996
|
program.command("disable").description("Turn the coach off permanently, without uninstalling").action(disableCommand);
|
|
590
997
|
program.command("config").description('Change an option, e.g. "engly config level B2"').argument("<option>", "language, level or style").argument("<value>", "new value").action(configCommand);
|
|
591
998
|
program.command("status").description("Show where Engly is installed and whether it is active").action(statusCommand);
|
|
592
|
-
program.command("remove").description("Remove the Engly block, skills and config").action(removeCommand);
|
|
999
|
+
program.command("remove").description("Remove the Engly block, skills and config").option("-y, --yes", "remove without asking").action(removeCommand);
|
|
593
1000
|
try {
|
|
594
1001
|
await program.parseAsync();
|
|
595
1002
|
} catch (error) {
|