deuk-agent-rule 1.0.13 → 2.2.0

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.
@@ -4,12 +4,24 @@ import {
4
4
  mkdirSync,
5
5
  readFileSync,
6
6
  readdirSync,
7
+ unlinkSync,
7
8
  writeFileSync,
8
9
  } from "fs";
9
10
  import { join } from "path";
10
11
 
11
12
  export const DEFAULT_TAG = "deuk-agent-rule";
12
13
 
14
+ /** HTML-style markers in `.cursorrules` (separate from AGENTS.md markers). */
15
+ export const CURSORRULES_TAG = "deuk-agent-rule-cursorrules";
16
+
17
+ export function resolveCursorrulesMarkers(o = {}) {
18
+ return resolveMarkers({
19
+ tag: o.tag && String(o.tag).trim() ? String(o.tag).trim() : CURSORRULES_TAG,
20
+ markerBegin: o.markerBegin,
21
+ markerEnd: o.markerEnd,
22
+ });
23
+ }
24
+
13
25
  export function resolveMarkers(o) {
14
26
  const hasBegin = o.markerBegin != null && o.markerBegin !== "";
15
27
  const hasEnd = o.markerEnd != null && o.markerEnd !== "";
@@ -35,7 +47,8 @@ export function findMarkerRegion(content, begin, end) {
35
47
  const j = content.indexOf(end, i + begin.length);
36
48
  if (j === -1) {
37
49
  throw new Error(
38
- "Found begin marker but no matching end marker after it.\n begin: " + begin + "\n end: " + end,
50
+ `[MARKER ERROR] Found begin marker "${begin}" but no matching end marker "${end}" after it.\n` +
51
+ ` This usually happens if one marker was deleted or renamed manually. Please verify the target file.`
39
52
  );
40
53
  }
41
54
  const innerStart = i + begin.length;
@@ -44,60 +57,85 @@ export function findMarkerRegion(content, begin, end) {
44
57
  }
45
58
 
46
59
  export function applyAgents(opts) {
47
- const {
48
- targetPath,
49
- bundleContent,
50
- markers,
51
- flavor,
52
- appendIfNoMarkers,
53
- dryRun,
54
- backup,
55
- agentsMode,
56
- } = opts;
60
+ const { agentsMode, targetPath } = opts;
57
61
 
58
62
  if (agentsMode === "skip") {
59
63
  return {
60
64
  action: "skip",
61
- reason: existsSync(targetPath)
62
- ? "agents mode skip (file exists)"
63
- : "agents mode skip",
65
+ reason: existsSync(targetPath) ? "agents mode skip (file exists)" : "agents mode skip",
64
66
  };
65
67
  }
66
68
 
67
69
  if (agentsMode === "overwrite") {
68
- const prev = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
69
- if (dryRun) {
70
- return { action: "would-write", path: targetPath, mode: "overwrite" };
71
- }
72
- if (backup && existsSync(targetPath)) {
73
- copyFileSync(targetPath, targetPath + ".bak");
74
- }
75
- writeFileSync(targetPath, bundleContent, "utf8");
76
- return { action: "write", path: targetPath, mode: "overwrite", hadPrevious: !!prev };
70
+ return handleAgentOverwrite(opts);
77
71
  }
78
72
 
79
73
  const existing = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
80
- const region = findMarkerRegion(existing, markers.begin, markers.end);
81
74
 
82
- if (region) {
83
- const inner = bundleContent.trimEnd() + "\n";
84
- const next =
85
- existing.slice(0, region.innerStart) +
86
- "\n" +
87
- inner +
88
- existing.slice(region.innerEnd);
89
- if (dryRun) {
90
- return { action: "would-write", path: targetPath, mode: "inject-region" };
91
- }
92
- if (backup && existsSync(targetPath)) {
93
- copyFileSync(targetPath, targetPath + ".bak");
75
+ // Workflow Safety Check: warn about different tags and error on case-mismatch
76
+ const markerRegex = /<!--\s*([a-zA-Z0-9_-]+):begin\s*-->/gi;
77
+ const currentTagMatch = opts.markers.begin.match(/<!--\s*([a-zA-Z0-9_-]+):begin\s*-->/i);
78
+ const currentTag = currentTagMatch ? currentTagMatch[1] : null;
79
+
80
+ const foundTags = [];
81
+ let m;
82
+ while ((m = markerRegex.exec(existing)) !== null) {
83
+ if (currentTag && m[1].toLowerCase() === currentTag.toLowerCase()) {
84
+ if (m[1] !== currentTag) {
85
+ throw new Error(
86
+ `[CRITICAL ERROR] Case mismatch for tag "${currentTag}". Found "${m[1]}" in ${targetPath}.\n` +
87
+ ` Please unify casing to avoid duplicate managed blocks.`
88
+ );
89
+ }
90
+ } else {
91
+ foundTags.push(m[1]);
94
92
  }
95
- writeFileSync(targetPath, next, "utf8");
96
- return { action: "write", path: targetPath, mode: "inject-region" };
97
93
  }
98
94
 
99
- const allowAppend =
100
- appendIfNoMarkers || flavor === "init";
95
+ if (foundTags.length > 0) {
96
+ console.warn(`[WARNING] Foreign markers in ${targetPath}: ${[...new Set(foundTags)].join(", ")}`);
97
+ console.warn(` Current tag is "${currentTag}". This might lead to duplicate managed blocks.`);
98
+ }
99
+
100
+ const region = findMarkerRegion(existing, opts.markers.begin, opts.markers.end);
101
+
102
+ if (region) {
103
+ return handleAgentInject(opts, existing, region);
104
+ }
105
+
106
+ return handleAgentAppend(opts, existing);
107
+ }
108
+
109
+ function handleAgentOverwrite(opts) {
110
+ const { targetPath, bundleContent, dryRun, backup } = opts;
111
+ const prev = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
112
+ if (dryRun) {
113
+ return { action: "would-write", path: targetPath, mode: "overwrite" };
114
+ }
115
+ if (backup && existsSync(targetPath)) {
116
+ copyFileSync(targetPath, targetPath + ".bak");
117
+ }
118
+ writeFileSync(targetPath, bundleContent, "utf8");
119
+ return { action: "write", path: targetPath, mode: "overwrite", hadPrevious: !!prev };
120
+ }
121
+
122
+ function handleAgentInject(opts, existing, region) {
123
+ const { targetPath, bundleContent, dryRun, backup } = opts;
124
+ const inner = bundleContent.trimEnd() + "\n";
125
+ const next = existing.slice(0, region.innerStart) + "\n" + inner + existing.slice(region.innerEnd);
126
+ if (dryRun) {
127
+ return { action: "would-write", path: targetPath, mode: "inject-region" };
128
+ }
129
+ if (backup && existsSync(targetPath)) {
130
+ copyFileSync(targetPath, targetPath + ".bak");
131
+ }
132
+ writeFileSync(targetPath, next, "utf8");
133
+ return { action: "write", path: targetPath, mode: "inject-region" };
134
+ }
135
+
136
+ function handleAgentAppend(opts, existing) {
137
+ const { targetPath, bundleContent, markers, flavor, appendIfNoMarkers, dryRun, backup } = opts;
138
+ const allowAppend = appendIfNoMarkers || flavor === "init";
101
139
 
102
140
  if (!allowAppend) {
103
141
  const hint = [
@@ -193,3 +231,135 @@ export function readBundleAgents(bundleRoot) {
193
231
  }
194
232
  return readFileSync(p, "utf8");
195
233
  }
234
+
235
+ /**
236
+ * Optional bundle file: Cursor root rules pointer to AGENTS.md.
237
+ * @returns {string|null} file contents, or null if missing
238
+ */
239
+ export function readBundleCursorrules(bundleRoot) {
240
+ const p = join(bundleRoot, ".cursorrules");
241
+ if (!existsSync(p)) {
242
+ return null;
243
+ }
244
+ return readFileSync(p, "utf8");
245
+ }
246
+
247
+ /**
248
+ * Remove one tagged block (markers inclusive). Returns ok:false if begin marker not found.
249
+ * @returns {{ ok: true, content: string } | { ok: false, reason: string }}
250
+ */
251
+ export function removeTaggedBlock(content, begin, end) {
252
+ const i = content.indexOf(begin);
253
+ if (i === -1) {
254
+ return { ok: false, reason: "begin not found" };
255
+ }
256
+ const j = content.indexOf(end, i + begin.length);
257
+ if (j === -1) {
258
+ return { ok: false, reason: "end not found" };
259
+ }
260
+ const afterEnd = j + end.length;
261
+ let next = content.slice(0, i) + content.slice(afterEnd);
262
+ next = next.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "").trimEnd();
263
+ if (next.length) {
264
+ next += "\n";
265
+ }
266
+ return { ok: true, content: next };
267
+ }
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
+
298
+ /**
299
+ * @param {{
300
+ * bundleRoot: string,
301
+ * cwd: string,
302
+ * markers: { begin: string, end: string },
303
+ * cursorrulesMode: "skip" | "inject" | "overwrite",
304
+ * dryRun?: boolean,
305
+ * backup?: boolean,
306
+ * }} opts
307
+ */
308
+ export function applyCursorrules(opts) {
309
+ const { bundleRoot, cwd, markers, cursorrulesMode, dryRun, backup } = opts;
310
+ const raw = readBundleCursorrules(bundleRoot);
311
+ if (!raw) {
312
+ return { action: "skip", reason: "bundle .cursorrules missing" };
313
+ }
314
+ const inner = raw.trimEnd();
315
+ const targetPath = join(cwd, ".cursorrules");
316
+
317
+ if (cursorrulesMode === "skip") {
318
+ return { action: "skip", reason: "cursorrules mode skip" };
319
+ }
320
+
321
+ const writeTaggedOnly = (bodyInner) =>
322
+ markers.begin + "\n\n" + bodyInner + "\n\n" + markers.end + "\n";
323
+
324
+ if (cursorrulesMode === "overwrite") {
325
+ const next = writeTaggedOnly(inner);
326
+ if (dryRun) {
327
+ return { action: "would-write", path: targetPath, mode: "overwrite" };
328
+ }
329
+ if (backup && existsSync(targetPath)) {
330
+ copyFileSync(targetPath, targetPath + ".bak");
331
+ }
332
+ writeFileSync(targetPath, next, "utf8");
333
+ return { action: "write", path: targetPath, mode: "overwrite" };
334
+ }
335
+
336
+ // inject: update tagged region, or prepend tagged block above existing content
337
+ const existing = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
338
+ const region = findMarkerRegion(existing, markers.begin, markers.end);
339
+
340
+ if (region) {
341
+ const next =
342
+ existing.slice(0, region.innerStart) + "\n" + inner + "\n" + existing.slice(region.innerEnd);
343
+ if (dryRun) {
344
+ return { action: "would-write", path: targetPath, mode: "inject-region" };
345
+ }
346
+ if (backup && existsSync(targetPath)) {
347
+ copyFileSync(targetPath, targetPath + ".bak");
348
+ }
349
+ writeFileSync(targetPath, next, "utf8");
350
+ return { action: "write", path: targetPath, mode: "inject-region" };
351
+ }
352
+
353
+ const block = writeTaggedOnly(inner) + "\n";
354
+ const rest = existing.replace(/^\uFEFF/, "").trimStart();
355
+ const next = rest.length ? block + rest : block.trimEnd() + "\n";
356
+
357
+ if (dryRun) {
358
+ return { action: "would-write", path: targetPath, mode: "prepend-tagged" };
359
+ }
360
+ if (backup && existsSync(targetPath)) {
361
+ copyFileSync(targetPath, targetPath + ".bak");
362
+ }
363
+ writeFileSync(targetPath, next, "utf8");
364
+ return { action: "write", path: targetPath, mode: "prepend-tagged" };
365
+ }
@@ -13,12 +13,14 @@ import { fileURLToPath } from "url";
13
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
14
  const pkgRoot = join(__dirname, "..");
15
15
 
16
- /** Public npm bundle: generic template only (no monorepo paths or internal handoffs). */
16
+ /** Copy publish/ templates into bundle/ for npm packaging. */
17
17
  const publishDir = join(pkgRoot, "publish");
18
18
  const publishRulesDir = join(publishDir, "rules");
19
19
  const rulesDest = join(pkgRoot, "bundle", "rules");
20
20
  const agentsSrc = join(publishDir, "AGENTS.md");
21
21
  const agentsDest = join(pkgRoot, "bundle", "AGENTS.md");
22
+ const cursorrulesSrc = join(publishDir, ".cursorrules");
23
+ const cursorrulesDest = join(pkgRoot, "bundle", ".cursorrules");
22
24
 
23
25
  if (!existsSync(publishDir)) {
24
26
  throw new Error("Missing publish template dir: " + publishDir);
@@ -29,6 +31,9 @@ if (!existsSync(publishRulesDir)) {
29
31
  if (!existsSync(agentsSrc)) {
30
32
  throw new Error("Missing publish/AGENTS.md: " + agentsSrc);
31
33
  }
34
+ if (!existsSync(cursorrulesSrc)) {
35
+ throw new Error("Missing publish/.cursorrules: " + cursorrulesSrc);
36
+ }
32
37
 
33
38
  if (existsSync(rulesDest)) {
34
39
  rmSync(rulesDest, { recursive: true });
@@ -41,4 +46,5 @@ for (const name of readdirSync(publishRulesDir)) {
41
46
 
42
47
  const agentsBody = readFileSync(agentsSrc, "utf8");
43
48
  writeFileSync(agentsDest, agentsBody, "utf8");
49
+ copyFileSync(cursorrulesSrc, cursorrulesDest);
44
50
  console.log("deuk-agent-rule: synced bundle from publish/ template.");
@@ -1,22 +0,0 @@
1
- /**
2
- * Post-process CHANGELOG.md after commit-and-tag-version: drop internal "sync" wording
3
- * from user-facing bullets (OSS mirror / sync-oss / release automation noise).
4
- */
5
- import { readFileSync, writeFileSync } from "fs";
6
- import { dirname, join } from "path";
7
- import { fileURLToPath } from "url";
8
-
9
- const root = join(dirname(fileURLToPath(import.meta.url)), "..");
10
- const file = join(root, "CHANGELOG.md");
11
- let s = readFileSync(file, "utf8");
12
-
13
- s = s.replace(/,\s*and OSS sync\b/g, "");
14
- s = s.replace(/\s+and OSS sync\b/g, "");
15
- s = s.replace(
16
- /- `sync-oss` copies (`package-lock\.json`) for reproducible installs\./g,
17
- "- Release packaging includes $1 for reproducible installs.",
18
- );
19
- s = s.replace(/\bCLI and publish AGENTS sync\b/gi, "CLI and publish AGENTS updates");
20
- s = s.replace(/\bpublish AGENTS sync\b/gi, "publish AGENTS alignment");
21
-
22
- writeFileSync(file, s, "utf8");
@@ -1,128 +0,0 @@
1
- /**
2
- * Populates ../DeukAgentRulesOSS for the public GitHub repo.
3
- * Run: cd deuk-agent-rule && npm run sync:oss
4
- */
5
- import { cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
6
- import { dirname, join } from "path";
7
- import { fileURLToPath } from "url";
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
- const pkgRoot = join(__dirname, "..");
11
- const repoRoot = join(pkgRoot, "..");
12
- const ossRoot = join(repoRoot, "DeukAgentRulesOSS");
13
- const ossPublic = join(pkgRoot, "oss-public");
14
-
15
- /** Set DEUK_AGENT_RULES_OSS_REPO to override, e.g. https://github.com/you/DeukAgentRulesOSS */
16
- const OSS_REPO =
17
- process.env.DEUK_AGENT_RULES_OSS_REPO || "https://github.com/joygram/DeukAgentRules";
18
-
19
- function gitBase() {
20
- let u = OSS_REPO.trim().replace(/\.git$/i, "").replace(/\/$/, "");
21
- if (!u.startsWith("http")) return u;
22
- return u;
23
- }
24
-
25
- const base = gitBase();
26
- const gitUrl = base.startsWith("http") ? "git+" + base + ".git" : base;
27
-
28
- /** Public mirror: no prebump/postchangelog (internal sync / polish hooks). */
29
- function stripOssVersionrcScripts(ossVersionrcPath) {
30
- let t = readFileSync(ossVersionrcPath, "utf8").replace(/\r\n/g, "\n");
31
- t = t.replace(/\n scripts:\s*\{\n[\s\S]*?\n \},\s*\n/, "\n");
32
- writeFileSync(ossVersionrcPath, t, "utf8");
33
- }
34
-
35
- mkdirSync(join(ossRoot, "scripts"), { recursive: true });
36
- mkdirSync(join(ossRoot, "publish"), { recursive: true });
37
-
38
- cpSync(join(pkgRoot, "publish"), join(ossRoot, "publish"), { recursive: true, force: true });
39
- if (existsSync(join(pkgRoot, ".github"))) {
40
- cpSync(join(pkgRoot, ".github"), join(ossRoot, ".github"), { recursive: true, force: true });
41
- }
42
- cpSync(join(pkgRoot, "scripts", "cli.mjs"), join(ossRoot, "scripts", "cli.mjs"), { force: true });
43
- cpSync(join(pkgRoot, "scripts", "merge-logic.mjs"), join(ossRoot, "scripts", "merge-logic.mjs"), {
44
- force: true,
45
- });
46
- cpSync(join(pkgRoot, "scripts", "sync-bundle.mjs"), join(ossRoot, "scripts", "sync-bundle.mjs"), {
47
- force: true,
48
- });
49
-
50
- if (!existsSync(ossPublic)) {
51
- throw new Error("Missing oss-public/: " + ossPublic);
52
- }
53
- cpSync(join(pkgRoot, "README.md"), join(ossRoot, "README.md"), { force: true });
54
- cpSync(join(pkgRoot, "README.ko.md"), join(ossRoot, "README.ko.md"), { force: true });
55
- if (existsSync(join(pkgRoot, "CHANGELOG.md"))) {
56
- cpSync(join(pkgRoot, "CHANGELOG.md"), join(ossRoot, "CHANGELOG.md"), { force: true });
57
- }
58
- if (existsSync(join(pkgRoot, "package-lock.json"))) {
59
- cpSync(join(pkgRoot, "package-lock.json"), join(ossRoot, "package-lock.json"), { force: true });
60
- }
61
- if (existsSync(join(pkgRoot, "LICENSE"))) {
62
- cpSync(join(pkgRoot, "LICENSE"), join(ossRoot, "LICENSE"), { force: true });
63
- }
64
- if (existsSync(join(pkgRoot, ".npmrc"))) {
65
- cpSync(join(pkgRoot, ".npmrc"), join(ossRoot, ".npmrc"), { force: true });
66
- }
67
- if (existsSync(join(pkgRoot, ".versionrc.cjs"))) {
68
- cpSync(join(pkgRoot, ".versionrc.cjs"), join(ossRoot, ".versionrc.cjs"), { force: true });
69
- stripOssVersionrcScripts(join(ossRoot, ".versionrc.cjs"));
70
- }
71
- const changelogTemplates = join(pkgRoot, "changelog-templates");
72
- if (existsSync(changelogTemplates)) {
73
- cpSync(changelogTemplates, join(ossRoot, "changelog-templates"), { recursive: true, force: true });
74
- }
75
- cpSync(join(ossPublic, "RELEASING.md"), join(ossRoot, "RELEASING.md"), { force: true });
76
- cpSync(join(ossPublic, "RELEASING.ko.md"), join(ossRoot, "RELEASING.ko.md"), { force: true });
77
- cpSync(join(ossPublic, "GITHUB_DESCRIPTION.md"), join(ossRoot, "GITHUB_DESCRIPTION.md"), {
78
- force: true,
79
- });
80
-
81
- const srcPkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
82
- const outPkg = {
83
- ...srcPkg,
84
- license: srcPkg.license || "Apache-2.0",
85
- repository: {
86
- type: "git",
87
- url: gitUrl,
88
- },
89
- bugs: {
90
- url: base.startsWith("http") ? base + "/issues" : base,
91
- },
92
- homepage: base.startsWith("http") ? base + "#readme" : base,
93
- files: [
94
- "LICENSE",
95
- "bundle/**/*",
96
- "scripts/**/*.mjs",
97
- "README.md",
98
- "README.ko.md",
99
- "CHANGELOG.md",
100
- ],
101
- };
102
- delete outPkg.private;
103
- if (outPkg.scripts && outPkg.scripts["merge:dry"]) {
104
- const { "merge:dry": _md, ...r2 } = outPkg.scripts;
105
- outPkg.scripts = r2;
106
- }
107
- if (outPkg.scripts && outPkg.scripts["sync:oss"]) {
108
- const { "sync:oss": _drop, ...rest } = outPkg.scripts;
109
- outPkg.scripts = rest;
110
- }
111
- /** Mirror is not a publish/version source: no bump scripts or release devDependencies. */
112
- for (const k of ["bump", "bump:patch", "bump:minor", "bump:major"]) {
113
- if (outPkg.scripts && outPkg.scripts[k]) {
114
- const { [k]: _drop, ...rest } = outPkg.scripts;
115
- outPkg.scripts = rest;
116
- }
117
- }
118
- delete outPkg.devDependencies;
119
-
120
- writeFileSync(join(ossRoot, "package.json"), JSON.stringify(outPkg, null, 2) + "\n", "utf8");
121
-
122
- const ossPolish = join(ossRoot, "scripts", "changelog-polish.mjs");
123
- if (existsSync(ossPolish)) {
124
- unlinkSync(ossPolish);
125
- }
126
-
127
- console.log("deuk-agent-rule: synced OSS tree at " + ossRoot);
128
- console.log(" Override repo URL: DEUK_AGENT_RULES_OSS_REPO=https://github.com/org/DeukAgentRulesOSS");