deuk-agent-rule 1.0.13 → 2.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/scripts/cli.mjs CHANGED
@@ -1,441 +1,110 @@
1
- #!/usr/bin/env node
2
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
- import { createInterface } from "readline";
4
- import { dirname, join } from "path";
5
- import { fileURLToPath } from "url";
6
- import {
7
- applyAgents,
8
- applyRules,
9
- readBundleAgents,
10
- resolveMarkers,
11
- } from "./merge-logic.mjs";
12
-
13
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- const pkgRoot = join(__dirname, "..");
15
- const bundleRoot = join(pkgRoot, "bundle");
16
-
17
- /** Default directory for persisted handoffs; `init` adds it to `.gitignore`. */
18
- const HANDOFF_DIR_NAME = ".deuk-agent-handoff";
19
- const GITIGNORE_HANDOFF_MARKER = "# deuk-agent-rule: handoff directory (local, not committed by default)";
20
-
21
- function printHandoffTip() {
22
- console.log(
23
- "tip: Persist multi-session specs under " +
24
- HANDOFF_DIR_NAME +
25
- "/ (see README § Handoffs). Optional: mirror the same body to .cursor/plans/deuk-handoff.plan.md for the Plans panel.",
26
- );
27
- }
28
-
29
- function ensureHandoffDirAndGitignore(opts) {
30
- const handoffPath = join(opts.cwd, HANDOFF_DIR_NAME);
31
- const gitignorePath = join(opts.cwd, ".gitignore");
32
- const ignoreLine = HANDOFF_DIR_NAME + "/";
33
-
34
- if (opts.dryRun) {
35
- console.log("handoff: would mkdir " + HANDOFF_DIR_NAME + "/ and ensure .gitignore ignores it");
36
- printHandoffTip();
37
- return;
38
- }
39
-
40
- mkdirSync(handoffPath, { recursive: true });
41
- console.log("handoff: " + HANDOFF_DIR_NAME + "/");
42
-
43
- let gi = "";
44
- if (existsSync(gitignorePath)) {
45
- gi = readFileSync(gitignorePath, "utf8");
46
- const lines = gi.split(/\r?\n/).map((l) => l.trim());
47
- const already =
48
- gi.includes(ignoreLine) ||
49
- lines.some((t) => t === HANDOFF_DIR_NAME || t === ignoreLine.replace(/\/$/, ""));
50
- if (already) {
51
- console.log(".gitignore: already ignores " + HANDOFF_DIR_NAME);
52
- printHandoffTip();
53
- return;
54
- }
55
- const block = "\n" + GITIGNORE_HANDOFF_MARKER + "\n" + ignoreLine + "\n";
56
- appendFileSync(gitignorePath, block, "utf8");
57
- console.log(".gitignore: appended " + ignoreLine.trim());
58
- printHandoffTip();
59
- } else {
60
- writeFileSync(gitignorePath, GITIGNORE_HANDOFF_MARKER + "\n" + ignoreLine + "\n", "utf8");
61
- console.log(".gitignore: created with " + ignoreLine.trim());
62
- printHandoffTip();
63
- }
64
- }
65
-
66
- // ---------------------------------------------------------------------------
67
- // Interactive prompt helpers (no external deps)
68
- // ---------------------------------------------------------------------------
69
-
70
- function isNonInteractive(opts) {
71
- return opts.nonInteractive || process.env.CI || !process.stdin.isTTY;
72
- }
73
-
74
- async function ask(rl, question) {
75
- return new Promise((resolve) => rl.question(question, resolve));
76
- }
77
-
78
- async function selectOne(rl, prompt, choices) {
79
- console.log("\n" + prompt);
80
- choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
81
- while (true) {
82
- const ans = (await ask(rl, ` Choice [1-${choices.length}]: `)).trim();
83
- const idx = parseInt(ans, 10) - 1;
84
- if (idx >= 0 && idx < choices.length) return choices[idx].value;
85
- console.log(" Please enter a number between 1 and " + choices.length);
86
- }
87
- }
88
-
89
- async function selectMany(rl, prompt, choices) {
90
- console.log("\n" + prompt + " (comma-separated numbers, or 'all')");
91
- choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
92
- while (true) {
93
- const ans = (await ask(rl, ` Choices: `)).trim().toLowerCase();
94
- if (ans === "all" || ans === "") return choices.map((c) => c.value);
95
- const parts = ans.split(/[,\s]+/).map((s) => parseInt(s, 10) - 1);
96
- if (parts.every((i) => i >= 0 && i < choices.length)) {
97
- return parts.map((i) => choices[i].value);
98
- }
99
- console.log(" Invalid selection, try again.");
100
- }
101
- }
102
-
103
- const STACKS = [
104
- { label: "Unity / C#", value: "unity" },
105
- { label: "Next.js + C#", value: "nextjs-dotnet" },
106
- { label: "Web (React / Vue / general)", value: "web" },
107
- { label: "Java / Spring Boot", value: "java" },
108
- { label: "Other / skip", value: "other" },
109
- ];
110
-
111
- /** Survey only today; merge/init does not branch on these yet (future tool-specific templates). */
112
- const AGENT_TOOLS = [
113
- { label: "Cursor", value: "cursor" },
114
- { label: "GitHub Copilot", value: "copilot" },
115
- { label: "Gemini / Antigravity", value: "gemini" },
116
- { label: "Claude (Cursor / Claude Code)", value: "claude" },
117
- { label: "Windsurf", value: "windsurf" },
118
- { label: "JetBrains AI Assistant", value: "jetbrains" },
119
- { label: "All of the above", value: "all" },
120
- { label: "Other / skip", value: "other" },
121
- ];
122
-
123
- /** Written after first interactive init; reused on later inits unless --interactive or schema mismatch. */
124
- const INIT_CONFIG_VERSION = 1;
125
- const INIT_CONFIG_FILENAME = ".deuk-agent-rule.config.json";
126
-
127
- function loadInitConfig(cwd) {
128
- const p = join(cwd, INIT_CONFIG_FILENAME);
129
- if (!existsSync(p)) return null;
130
- try {
131
- const j = JSON.parse(readFileSync(p, "utf8"));
132
- if (j.version !== INIT_CONFIG_VERSION) return null;
133
- const allowedStack = new Set(STACKS.map((s) => s.value));
134
- if (!allowedStack.has(j.stack)) return null;
135
- const allowedTools = new Set(AGENT_TOOLS.map((t) => t.value));
136
- if (!Array.isArray(j.agentTools) || !j.agentTools.every((t) => allowedTools.has(t))) return null;
137
- if (!["inject", "skip", "overwrite"].includes(j.agentsMode)) return null;
138
- return {
139
- stack: j.stack,
140
- agentTools: j.agentTools,
141
- agentsMode: j.agentsMode,
142
- };
143
- } catch {
144
- return null;
145
- }
146
- }
147
-
148
- function writeInitConfig(cwd, opts) {
149
- const p = join(cwd, INIT_CONFIG_FILENAME);
150
- const body = {
151
- version: INIT_CONFIG_VERSION,
152
- stack: opts.stack,
153
- agentTools: opts.agentTools,
154
- agentsMode: opts.agents ?? "inject",
155
- updatedAt: new Date().toISOString(),
156
- };
157
- writeFileSync(p, JSON.stringify(body, null, 2) + "\n", "utf8");
158
- console.log("saved: " + INIT_CONFIG_FILENAME);
159
- }
160
-
161
- async function runInteractive(opts) {
162
- const rl = createInterface({ input: process.stdin, output: process.stdout });
163
- try {
164
- console.log("\nDeukAgentRules init — let's configure your workspace.\n");
165
-
166
- const stack = await selectOne(rl, "What is your primary tech stack?", STACKS);
167
- const tools = await selectMany(rl, "Which agent tools do you use?", AGENT_TOOLS);
168
-
169
- const targetAgents = join(opts.cwd, "AGENTS.md");
170
- let agentsDefault = "inject";
171
- if (!existsSync(targetAgents)) {
172
- agentsDefault = "inject"; // will append markers
173
- console.log("\n No AGENTS.md found — will create with markers.");
174
- } else {
175
- const content = readFileSync(targetAgents, "utf8");
176
- const hasMarkers = content.includes("deuk-agent-rule:begin");
177
- if (!hasMarkers) {
178
- const choice = await selectOne(rl, "AGENTS.md exists but has no markers. How to apply?", [
179
- { label: "Append managed block at the end (safe)", value: "inject" },
180
- { label: "Overwrite entire AGENTS.md", value: "overwrite" },
181
- { label: "Skip AGENTS.md", value: "skip" },
182
- ]);
183
- agentsDefault = choice;
184
- }
185
- }
186
-
187
- opts.agents = opts.agents ?? agentsDefault;
188
- opts.stack = stack;
189
- opts.agentTools = tools;
190
-
191
- console.log("\n Stack : " + stack);
192
- console.log(" Tools : " + (tools.join(", ") || "none"));
193
- console.log(" AGENTS: " + opts.agents + "\n");
194
- } finally {
195
- rl.close();
196
- }
197
- }
198
-
199
- // ---------------------------------------------------------------------------
200
- // Help / arg parsing
201
- // ---------------------------------------------------------------------------
202
-
203
- function printHelp() {
204
- console.log(
205
- `DeukAgentRules (npm: deuk-agent-rule) — AGENTS.md + .cursor/rules templates
206
-
207
- Usage:
208
- npx deuk-agent-rule init [options] # interactive by default
209
- npx deuk-agent-rule merge [options]
210
-
211
- Options:
212
- --cwd <path> Target repo root (default: current directory)
213
- --dry-run Print actions; do not write files
214
- --non-interactive CI/scripts: no prompts; use --agents/--rules (no saved config read)
215
- --interactive Ask questions even if .deuk-agent-rule.config.json exists
216
- --tag <id> Marker id (default: deuk-agent-rule)
217
- --agents <mode> inject | skip | overwrite
218
- --rules <mode> prefix | skip | overwrite
219
- --backup Write *.bak before overwrite
220
- --append-if-no-markers
221
- --marker-begin / --marker-end Custom marker strings (both required)
222
-
223
- init also creates .deuk-agent-handoff/ and appends it to .gitignore (local handoffs).
224
- After npm update, run init again: deuk-agent-rule-*.mdc rules refresh from the bundle (no separate merge needed).
225
-
226
- Korean: package README.ko.md
227
- `,
228
- );
229
- }
230
-
231
- function parseArgs(argv) {
232
- const out = {
233
- cwd: process.cwd(),
234
- dryRun: false,
235
- backup: false,
236
- tag: undefined,
237
- markerBegin: undefined,
238
- markerEnd: undefined,
239
- agents: undefined,
240
- rules: undefined,
241
- appendIfNoMarkers: false,
242
- nonInteractive: false,
243
- interactive: false,
244
- };
245
- for (let i = 0; i < argv.length; i++) {
246
- const a = argv[i];
247
- if (a === "--cwd") {
248
- out.cwd = argv[++i];
249
- if (!out.cwd) throw new Error("--cwd requires a path");
250
- } else if (a === "--dry-run") out.dryRun = true;
251
- else if (a === "--backup") out.backup = true;
252
- else if (a === "--non-interactive") out.nonInteractive = true;
253
- else if (a === "--interactive") out.interactive = true;
254
- else if (a === "--tag") {
255
- out.tag = argv[++i];
256
- if (out.tag == null) throw new Error("--tag requires a value");
257
- } else if (a === "--marker-begin") {
258
- out.markerBegin = argv[++i];
259
- if (out.markerBegin == null) throw new Error("--marker-begin requires a value");
260
- } else if (a === "--marker-end") {
261
- out.markerEnd = argv[++i];
262
- if (out.markerEnd == null) throw new Error("--marker-end requires a value");
263
- } else if (a === "--agents") {
264
- out.agents = argv[++i];
265
- if (!out.agents) throw new Error("--agents requires skip|overwrite|inject");
266
- } else if (a === "--rules") {
267
- out.rules = argv[++i];
268
- if (!out.rules) throw new Error("--rules requires skip|overwrite|prefix");
269
- } else if (a === "--append-if-no-markers") out.appendIfNoMarkers = true;
270
- else if (a === "-h" || a === "--help") {
271
- printHelp();
272
- process.exit(0);
273
- } else {
274
- throw new Error("Unknown argument: " + a);
275
- }
276
- }
277
- return out;
278
- }
279
-
280
- function validateMode(name, v, allowed) {
281
- if (!allowed.includes(v)) {
282
- throw new Error("Invalid " + name + ": " + v + " (allowed: " + allowed.join(", ") + ")");
283
- }
284
- }
285
-
286
- // ---------------------------------------------------------------------------
287
- // init / merge runners
288
- // ---------------------------------------------------------------------------
289
-
290
- function runInit(opts) {
291
- const markers = resolveMarkers({
292
- tag: opts.tag,
293
- markerBegin: opts.markerBegin,
294
- markerEnd: opts.markerEnd,
295
- });
296
- const agentsMode = opts.agents ?? "inject";
297
- validateMode("agents", agentsMode, ["skip", "overwrite", "inject"]);
298
-
299
- const rulesMode = opts.rules ?? "prefix";
300
- validateMode("rules", rulesMode, ["skip", "overwrite", "prefix"]);
301
-
302
- const bundleContent = readBundleAgents(bundleRoot);
303
- const targetAgents = join(opts.cwd, "AGENTS.md");
304
- const targetRules = join(opts.cwd, ".cursor", "rules");
305
-
306
- const agentsResult = applyAgents({
307
- targetPath: targetAgents,
308
- bundleContent,
309
- markers,
310
- flavor: "init",
311
- appendIfNoMarkers: opts.appendIfNoMarkers,
312
- dryRun: opts.dryRun,
313
- backup: opts.backup,
314
- agentsMode,
315
- });
316
- console.log("AGENTS.md: " + agentsResult.action + (agentsResult.mode ? " (" + agentsResult.mode + ")" : ""));
317
-
318
- const ruleActions = applyRules({
319
- bundleRulesDir: join(bundleRoot, "rules"),
320
- targetRulesDir: targetRules,
321
- rulesMode,
322
- dryRun: opts.dryRun,
323
- backup: opts.backup,
324
- });
325
- for (const r of ruleActions) {
326
- console.log("rule " + r.action + ": " + (r.dest || r.src) + (r.reason ? " (" + r.reason + ")" : ""));
327
- }
328
-
329
- ensureHandoffDirAndGitignore(opts);
330
- }
331
-
332
- function runMerge(opts) {
333
- const markers = resolveMarkers({
334
- tag: opts.tag,
335
- markerBegin: opts.markerBegin,
336
- markerEnd: opts.markerEnd,
337
- });
338
- const agentsMode = opts.agents ?? "inject";
339
- validateMode("agents", agentsMode, ["skip", "overwrite", "inject"]);
340
-
341
- const rulesMode = opts.rules ?? "skip";
342
- validateMode("rules", rulesMode, ["skip", "overwrite", "prefix"]);
343
-
344
- const bundleContent = readBundleAgents(bundleRoot);
345
- const targetAgents = join(opts.cwd, "AGENTS.md");
346
- const targetRules = join(opts.cwd, ".cursor", "rules");
347
-
348
- const agentsResult = applyAgents({
349
- targetPath: targetAgents,
350
- bundleContent,
351
- markers,
352
- flavor: "merge",
353
- appendIfNoMarkers: opts.appendIfNoMarkers,
354
- dryRun: opts.dryRun,
355
- backup: opts.backup,
356
- agentsMode,
357
- });
358
- console.log("AGENTS.md: " + agentsResult.action + (agentsResult.mode ? " (" + agentsResult.mode + ")" : ""));
359
-
360
- const ruleActions = applyRules({
361
- bundleRulesDir: join(bundleRoot, "rules"),
362
- targetRulesDir: targetRules,
363
- rulesMode,
364
- dryRun: opts.dryRun,
365
- backup: opts.backup,
366
- });
367
- for (const r of ruleActions) {
368
- console.log("rule " + r.action + ": " + (r.dest || r.src) + (r.reason ? " (" + r.reason + ")" : ""));
369
- }
370
- }
371
-
372
- // ---------------------------------------------------------------------------
373
- // Entry point
374
- // ---------------------------------------------------------------------------
375
-
376
- async function main() {
377
- const argv = process.argv.slice(2);
378
- const sub = argv[0];
379
- if (!sub || sub === "-h" || sub === "--help") {
380
- printHelp();
381
- process.exit(0);
382
- }
383
-
384
- const rest = argv.slice(1);
385
- if (sub === "help") {
386
- printHelp();
387
- process.exit(0);
388
- }
389
-
390
- let opts;
391
- try {
392
- opts = parseArgs(rest);
393
- } catch (e) {
394
- console.error(e.message || e);
395
- printHelp();
396
- process.exit(1);
397
- }
398
-
399
- if (!existsSync(bundleRoot)) {
400
- console.error(
401
- "Missing bundle/ (run from published package or run npm run sync in DeukAgentRules when developing).",
402
- );
403
- process.exit(1);
404
- }
405
-
406
- try {
407
- if (sub === "init") {
408
- if (!isNonInteractive(opts)) {
409
- const saved = loadInitConfig(opts.cwd);
410
- if (saved && !opts.interactive) {
411
- opts.agents = opts.agents !== undefined ? opts.agents : saved.agentsMode;
412
- opts.stack = saved.stack;
413
- opts.agentTools = saved.agentTools;
414
- const stackL = STACKS.find((s) => s.value === saved.stack)?.label || saved.stack;
415
- console.log("\nDeukAgentRules init — using saved choices from " + INIT_CONFIG_FILENAME);
416
- console.log(" Stack : " + saved.stack + " (" + stackL + ")");
417
- console.log(" Tools : " + (saved.agentTools.join(", ") || "none"));
418
- console.log(" AGENTS: " + opts.agents);
419
- console.log(" (`--interactive` to change, or edit/delete " + INIT_CONFIG_FILENAME + ")\n");
420
- } else {
421
- await runInteractive(opts);
422
- if (!opts.dryRun) {
423
- writeInitConfig(opts.cwd, opts);
424
- }
425
- }
426
- }
427
- runInit(opts);
428
- } else if (sub === "merge") {
429
- runMerge(opts);
430
- } else {
431
- console.error("Unknown command: " + sub);
432
- printHelp();
433
- process.exit(1);
434
- }
435
- } catch (e) {
436
- console.error(e.message || e);
437
- process.exit(1);
438
- }
439
- }
440
-
441
- main();
1
+ #!/usr/bin/env node
2
+ import { existsSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { parseArgs, parseTicketArgs } from "./cli-args.mjs";
6
+ import { runInit, runMerge } from "./cli-init-commands.mjs";
7
+ import { runTicketCreate, runTicketList, runTicketUse, runTicketClose } from "./cli-ticket-commands.mjs";
8
+ import { loadInitConfig, writeInitConfig } from "./cli-prompts.mjs";
9
+ import { runInteractive } from "./cli-prompts.mjs";
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const pkgRoot = join(__dirname, "..");
13
+ const bundleRoot = join(pkgRoot, "bundle");
14
+ async function main() {
15
+ const argv = process.argv.slice(2);
16
+ const sub = argv[0];
17
+ if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
18
+ printHelp();
19
+ return;
20
+ }
21
+
22
+ const rest = argv.slice(1);
23
+
24
+ if (sub === "ticket") {
25
+ const action = rest[0];
26
+ const opts = parseTicketArgs(rest.slice(1));
27
+ if (action === "create") await runTicketCreate(opts);
28
+ else if (action === "list") await runTicketList(opts);
29
+ else if (action === "use") await runTicketUse(opts);
30
+ else if (action === "close") await runTicketClose(opts);
31
+ else if (action === "migrate") await runTicketMigrate(opts);
32
+ else {
33
+ console.error("Unknown ticket action: " + action);
34
+ printHelp();
35
+ }
36
+ return;
37
+ }
38
+
39
+ if (sub === "init" || sub === "merge") {
40
+ const opts = parseArgs(rest);
41
+ if (opts.help) {
42
+ printHelp();
43
+ return;
44
+ }
45
+
46
+ const saved = loadInitConfig(opts.cwd);
47
+ if (saved && !opts.interactive) {
48
+ // CLI flags (opts) take precedence over saved config
49
+ for (const key in saved) {
50
+ if (opts[key] === undefined) opts[key] = saved[key];
51
+ }
52
+ console.log(`Using saved config from .deuk-agent-rule.config.json (CLI overrides applied)`);
53
+ }
54
+
55
+ if (sub === "init") {
56
+ await handleInit(opts);
57
+ } else {
58
+ runMerge(opts, bundleRoot);
59
+ }
60
+ return;
61
+ }
62
+
63
+ console.error("Unknown command: " + sub);
64
+ printHelp();
65
+ }
66
+
67
+ import { runTicketMigrate } from "./cli-ticket-commands.mjs";
68
+
69
+ async function handleInit(opts) {
70
+ if (!opts.interactive && !opts.nonInteractive && !loadInitConfig(opts.cwd)) {
71
+ // If no config and not interactive, prompt unless non-interactive
72
+ await runInteractive(opts);
73
+ if (!opts.dryRun) writeInitConfig(opts.cwd, opts);
74
+ } else if (opts.interactive) {
75
+ await runInteractive(opts);
76
+ if (!opts.dryRun) writeInitConfig(opts.cwd, opts);
77
+ }
78
+ await runInit(opts, bundleRoot);
79
+ }
80
+
81
+ function printHelp() {
82
+ console.log(`DeukAgentRules CLI - Generalization Rules & Ticket Management
83
+
84
+ Usage:
85
+ npx deuk-agent-rule init [options]
86
+ npx deuk-agent-rule merge [options]
87
+ npx deuk-agent-rule ticket <create|list|use|close|migrate> [options]
88
+
89
+ Options:
90
+ --cwd <path> Target repo root
91
+ --dry-run Print actions without writing
92
+ --non-interactive CI/scripts mode: no prompts
93
+ --tag <id> Custom marker ID (default: deuk-agent-rule)
94
+ --agents <mode> inject | skip | overwrite
95
+ --rules <mode> prefix | skip | overwrite
96
+ --cursorrules <mode> inject | skip | overwrite
97
+
98
+ Ticket Options:
99
+ --topic <name> Ticket topic slug
100
+ --group <name> Ticket group (sub|main|discussion)
101
+ --project <name> Project filter (DeukUI|DeukAgentRules)
102
+ --latest Use most recent ticket
103
+ --path-only Print only the file path
104
+ `);
105
+ }
106
+
107
+ main().catch(err => {
108
+ console.error(err.message || err);
109
+ process.exit(1);
110
+ });