deuk-agent-rule 2.4.6 → 2.5.13

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.
@@ -1,8 +1,19 @@
1
- import { existsSync, readFileSync, writeFileSync } from "fs";
1
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from "fs";
2
2
  import { basename, dirname, join, relative } from "path";
3
3
  import YAML from "yaml";
4
4
 
5
- export const INIT_CONFIG_FILENAME = ".deuk-agent-rule.config.json";
5
+ export const AGENT_ROOT_DIR = ".deuk-agent";
6
+ export const TICKET_SUBDIR = "tickets";
7
+ export const TEMPLATE_SUBDIR = "templates";
8
+ export const RULES_SUBDIR = "rules";
9
+
10
+ export const TICKET_DIR_NAME = `${AGENT_ROOT_DIR}/${TICKET_SUBDIR}`;
11
+ export const TICKET_INDEX_FILENAME = "INDEX.json";
12
+ export const TICKET_LIST_FILENAME = "TICKET_LIST.md";
13
+ export const TICKET_LIST_TEMPLATE_FILENAME = "TICKET_LIST.template.md";
14
+
15
+ export const INIT_CONFIG_FILENAME = `${AGENT_ROOT_DIR}/config.json`;
16
+ export const LEGACY_INIT_CONFIG_FILENAME = ".deuk-agent-rule.config.json";
6
17
  export const INIT_CONFIG_VERSION = 1;
7
18
 
8
19
  export const STACKS = [
@@ -22,9 +33,13 @@ export const AGENT_TOOLS = [
22
33
 
23
34
  export function loadInitConfig(cwd) {
24
35
  const p = join(cwd, INIT_CONFIG_FILENAME);
25
- if (!existsSync(p)) return null;
36
+ const legacyP = join(cwd, LEGACY_INIT_CONFIG_FILENAME);
37
+
38
+ let target = existsSync(p) ? p : (existsSync(legacyP) ? legacyP : null);
39
+ if (!target) return null;
40
+
26
41
  try {
27
- const j = JSON.parse(readFileSync(p, "utf8"));
42
+ const j = JSON.parse(readFileSync(target, "utf8"));
28
43
  if (j.version !== INIT_CONFIG_VERSION) return null;
29
44
  return j;
30
45
  } catch {
@@ -42,6 +57,7 @@ export function writeInitConfig(cwd, opts) {
42
57
  shareTickets: !!opts.shareTickets,
43
58
  remoteSync: !!opts.remoteSync,
44
59
  pipelineUrl: opts.pipelineUrl,
60
+ ignoreDirs: opts.ignoreDirs || DEFAULT_IGNORE_DIRS,
45
61
  updatedAt: new Date().toISOString(),
46
62
  };
47
63
  writeFileSync(p, JSON.stringify(data, null, 2), "utf8");
@@ -76,6 +92,21 @@ export function makeEntryId() {
76
92
  return `000-fallback-${Date.now().toString(36)}`;
77
93
  }
78
94
 
95
+ export function findFileRecursively(dir, fileName) {
96
+ if (!existsSync(dir)) return null;
97
+ const entries = readdirSync(dir, { withFileTypes: true });
98
+ for (const entry of entries) {
99
+ const res = join(dir, entry.name);
100
+ if (entry.isDirectory()) {
101
+ const found = findFileRecursively(res, fileName);
102
+ if (found) return found;
103
+ } else if (entry.name === fileName) {
104
+ return res;
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+
79
110
  export function detectProjectFromBody(body) {
80
111
  const content = String(body || "");
81
112
  const metaMatch = content.match(/^project:\s*(.+)$/mi);
@@ -144,7 +175,10 @@ export function stringifyFrontMatter(meta, content) {
144
175
  return `---\n${yamlStr}\n---\n\n${content.trim()}\n`;
145
176
  }
146
177
 
147
- export function semverGt(a, b) {
178
+ /**
179
+ * Returns true if semver a is less than b
180
+ */
181
+ export function semverLt(a, b) {
148
182
  const pa = String(a || "0").replace(/[^0-9.]/g, "").split(".").map(Number);
149
183
  const pb = String(b || "0").replace(/[^0-9.]/g, "").split(".").map(Number);
150
184
  for (let i = 0; i < 3; i++) {
@@ -166,7 +200,7 @@ export async function checkUpdateNotifier() {
166
200
  if (res.ok) {
167
201
  const data = await res.json();
168
202
  // Only notify when registry version is strictly newer than local (handles local dev symlink case)
169
- if (data.version && semverGt(currentVersion, data.version)) {
203
+ if (data.version && semverLt(currentVersion, data.version)) {
170
204
  console.warn(`\n\x1b[33m💡 Update available! ${currentVersion} → ${data.version}\x1b[0m`);
171
205
  console.warn(`\x1b[36mRun 'npm install -g deuk-agent-rule' to update.\x1b[0m\n`);
172
206
  }
@@ -175,3 +209,33 @@ export async function checkUpdateNotifier() {
175
209
  // Ignore timeout or network errors silently
176
210
  }
177
211
  }
212
+
213
+ export const DEFAULT_IGNORE_DIRS = ["node_modules", ".git", ".deuk-agent", "tmp", "temp", ".tmp", ".cache"];
214
+
215
+ /**
216
+ * Recursively finds all directories containing deuk-agent ticket structures.
217
+ */
218
+ export function discoverAllSubmodules(baseCwd, ignoreDirs = DEFAULT_IGNORE_DIRS, out = new Set()) {
219
+ if (!existsSync(baseCwd)) return Array.from(out);
220
+
221
+ const hasLegacy1 = existsSync(join(baseCwd, ".deuk-agent-ticket"));
222
+ const hasLegacy2 = existsSync(join(baseCwd, ".deuk-agent-tickets"));
223
+ const hasNew = existsSync(join(baseCwd, AGENT_ROOT_DIR, TICKET_SUBDIR));
224
+
225
+ if (hasLegacy1 || hasLegacy2 || hasNew) {
226
+ out.add(baseCwd);
227
+ }
228
+
229
+ try {
230
+ const entries = readdirSync(baseCwd, { withFileTypes: true });
231
+ for (const ent of entries) {
232
+ if (!ent.isDirectory()) continue;
233
+ // Skip system or noisy directories based on ignoreDirs configuration
234
+ if (ignoreDirs.includes(ent.name) || ent.name.startsWith(".deuk-agent")) continue;
235
+ discoverAllSubmodules(join(baseCwd, ent.name), ignoreDirs, out);
236
+ }
237
+ } catch {
238
+ // Ignore permission errors on specific subfolders
239
+ }
240
+ return Array.from(out);
241
+ }
package/scripts/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from "fs";
2
+ import { existsSync, rmSync } from "fs";
3
3
  import { dirname, join } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { parseArgs, parseTicketArgs } from "./cli-args.mjs";
@@ -77,6 +77,14 @@ async function main() {
77
77
  // Removed legacy migration runTicketMigrate
78
78
 
79
79
  async function handleInit(opts) {
80
+ if (opts.clean && !opts.dryRun) {
81
+ console.log(`[CLEAN] Removing legacy templates and config...`);
82
+ const templatesDir = join(opts.cwd, ".deuk-agent-templates");
83
+ const configFile = join(opts.cwd, ".deuk-agent-rule.config.json");
84
+ if (existsSync(templatesDir)) rmSync(templatesDir, { recursive: true, force: true });
85
+ if (existsSync(configFile)) rmSync(configFile, { force: true });
86
+ }
87
+
80
88
  if (!opts.interactive && !opts.nonInteractive && !loadInitConfig(opts.cwd)) {
81
89
  // If no config and not interactive, prompt unless non-interactive
82
90
  await runInteractive(opts);
@@ -266,34 +266,6 @@ export function removeTaggedBlock(content, begin, end) {
266
266
  return { ok: true, content: next };
267
267
  }
268
268
 
269
- /**
270
- * Strip `deuk-agent-rule-cursorrules` (or custom markers) region from `.cursorrules`.
271
- * @param {{ cwd: string, markers: { begin: string, end: string }, dryRun?: boolean, backup?: boolean }} opts
272
- */
273
- export function removeCursorrules(opts) {
274
- const { cwd, markers, dryRun, backup } = opts;
275
- const targetPath = join(cwd, ".cursorrules");
276
- if (!existsSync(targetPath)) {
277
- return { action: "skip", reason: "no file" };
278
- }
279
- const existing = readFileSync(targetPath, "utf8");
280
- const result = removeTaggedBlock(existing, markers.begin, markers.end);
281
- if (!result.ok) {
282
- return { action: "skip", path: targetPath, reason: result.reason };
283
- }
284
- if (dryRun) {
285
- return { action: "would-write", path: targetPath, mode: "remove-tagged" };
286
- }
287
- if (backup && existsSync(targetPath)) {
288
- copyFileSync(targetPath, targetPath + ".bak");
289
- }
290
- if (result.content === "") {
291
- unlinkSync(targetPath);
292
- return { action: "delete", path: targetPath, mode: "remove-tagged" };
293
- }
294
- writeFileSync(targetPath, result.content, "utf8");
295
- return { action: "write", path: targetPath, mode: "remove-tagged" };
296
- }
297
269
 
298
270
  /**
299
271
  * @param {{
@@ -21,6 +21,8 @@ const agentsSrc = join(publishDir, "AGENTS.md");
21
21
  const agentsDest = join(pkgRoot, "bundle", "AGENTS.md");
22
22
  const cursorrulesSrc = join(publishDir, ".cursorrules");
23
23
  const cursorrulesDest = join(pkgRoot, "bundle", ".cursorrules");
24
+ const geminiSrc = join(publishDir, "gemini.md");
25
+ const geminiDest = join(pkgRoot, "bundle", "gemini.md");
24
26
 
25
27
  if (!existsSync(publishDir)) {
26
28
  throw new Error("Missing publish template dir: " + publishDir);
@@ -56,7 +58,20 @@ if (existsSync(templatesSrc)) {
56
58
  }
57
59
  }
58
60
 
61
+ const dynamicRulesSrc = join(publishDir, "rules.d");
62
+ const dynamicRulesDest = join(pkgRoot, "bundle", "rules.d");
63
+ if (existsSync(dynamicRulesSrc)) {
64
+ if (existsSync(dynamicRulesDest)) {
65
+ rmSync(dynamicRulesDest, { recursive: true });
66
+ }
67
+ mkdirSync(dynamicRulesDest, { recursive: true });
68
+ for (const name of readdirSync(dynamicRulesSrc)) {
69
+ copyFileSync(join(dynamicRulesSrc, name), join(dynamicRulesDest, name));
70
+ }
71
+ }
72
+
59
73
  const agentsBody = readFileSync(agentsSrc, "utf8");
60
74
  writeFileSync(agentsDest, agentsBody, "utf8");
61
75
  copyFileSync(cursorrulesSrc, cursorrulesDest);
76
+ if (existsSync(geminiSrc)) copyFileSync(geminiSrc, geminiDest);
62
77
  console.log("deuk-agent-rule: synced bundle from publish/ template.");