deuk-agent-rule 2.2.0 → 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/CHANGELOG.md +139 -132
- package/README.ko.md +125 -125
- package/README.ko.pdf +0 -0
- package/README.md +123 -123
- package/bundle/AGENTS.md +87 -87
- package/bundle/rules/delivery-and-parallel-work.mdc +26 -26
- package/bundle/rules/git-commit.mdc +18 -18
- package/bundle/rules/multi-ai-workflow.mdc +105 -105
- package/package.json +12 -4
- package/scripts/changelog-polish.mjs +22 -0
- package/scripts/cli-args.mjs +43 -43
- package/scripts/cli-init-commands.mjs +65 -65
- package/scripts/cli-init-logic.mjs +21 -21
- package/scripts/cli-prompts.mjs +123 -123
- package/scripts/cli-ticket-commands.mjs +159 -159
- package/scripts/cli-ticket-logic.mjs +229 -229
- package/scripts/cli-utils.mjs +82 -82
- package/scripts/cli.mjs +110 -110
- package/scripts/merge-logic.mjs +365 -365
- package/scripts/sync-bundle.mjs +50 -50
- package/scripts/sync-oss.mjs +129 -0
package/scripts/cli-prompts.mjs
CHANGED
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
import { createInterface } from "readline";
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
|
|
5
|
-
const INIT_CONFIG_FILENAME = ".deuk-agent-rule.config.json";
|
|
6
|
-
const INIT_CONFIG_VERSION = 1;
|
|
7
|
-
|
|
8
|
-
export async function ask(rl, question) {
|
|
9
|
-
return new Promise((resolve) => rl.question(question, resolve));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function askYesNo(question, defaultYes = true) {
|
|
13
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
-
try {
|
|
15
|
-
const ans = (await ask(rl, question + (defaultYes ? " [Y/n]: " : " [y/N]: "))).trim().toLowerCase();
|
|
16
|
-
if (!ans) return defaultYes;
|
|
17
|
-
return ans === "y" || ans === "yes";
|
|
18
|
-
} finally {
|
|
19
|
-
rl.close();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function selectOne(rl, prompt, choices) {
|
|
24
|
-
console.log("\n" + prompt);
|
|
25
|
-
choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
|
|
26
|
-
while (true) {
|
|
27
|
-
const ans = (await ask(rl, ` Choice [1-${choices.length}]: `)).trim();
|
|
28
|
-
const idx = parseInt(ans, 10) - 1;
|
|
29
|
-
if (idx >= 0 && idx < choices.length) return choices[idx].value;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function selectMany(rl, prompt, choices) {
|
|
34
|
-
console.log("\n" + prompt + " (comma-separated numbers, or 'all')");
|
|
35
|
-
choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
|
|
36
|
-
while (true) {
|
|
37
|
-
const ans = (await ask(rl, ` Choices: `)).trim().toLowerCase();
|
|
38
|
-
if (ans === "all" || ans === "") return choices.map((c) => c.value);
|
|
39
|
-
const parts = ans.split(/[,\s]+/).map((s) => parseInt(s, 10) - 1);
|
|
40
|
-
if (parts.every((i) => i >= 0 && i < choices.length)) return parts.map((i) => choices[i].value);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function loadInitConfig(cwd) {
|
|
45
|
-
const p = join(cwd, INIT_CONFIG_FILENAME);
|
|
46
|
-
if (!existsSync(p)) return null;
|
|
47
|
-
try {
|
|
48
|
-
const j = JSON.parse(readFileSync(p, "utf8"));
|
|
49
|
-
if (j.version !== INIT_CONFIG_VERSION) return null;
|
|
50
|
-
return j;
|
|
51
|
-
} catch {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function writeInitConfig(cwd, opts) {
|
|
57
|
-
const p = join(cwd, INIT_CONFIG_FILENAME);
|
|
58
|
-
const body = {
|
|
59
|
-
version: INIT_CONFIG_VERSION,
|
|
60
|
-
stack: opts.stack,
|
|
61
|
-
agentTools: opts.agentTools,
|
|
62
|
-
agentsMode: opts.agents ?? "inject",
|
|
63
|
-
updatedAt: new Date().toISOString(),
|
|
64
|
-
};
|
|
65
|
-
writeFileSync(p, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const STACKS = [
|
|
69
|
-
{ label: "Unity / C#", value: "unity" },
|
|
70
|
-
{ label: "Next.js + C#", value: "nextjs-dotnet" },
|
|
71
|
-
{ label: "Web (React / Vue / general)", value: "web" },
|
|
72
|
-
{ label: "Java / Spring Boot", value: "java" },
|
|
73
|
-
{ label: "Other / skip", value: "other" },
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
export const AGENT_TOOLS = [
|
|
77
|
-
{ label: "Cursor", value: "cursor" },
|
|
78
|
-
{ label: "GitHub Copilot", value: "copilot" },
|
|
79
|
-
{ label: "Gemini / Antigravity", value: "gemini" },
|
|
80
|
-
{ label: "Claude (Cursor / Claude Code)", value: "claude" },
|
|
81
|
-
{ label: "Windsurf", value: "windsurf" },
|
|
82
|
-
{ label: "JetBrains AI Assistant", value: "jetbrains" },
|
|
83
|
-
{ label: "All of the above", value: "all" },
|
|
84
|
-
{ label: "Other / skip", value: "other" },
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
export async function runInteractive(opts) {
|
|
88
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
89
|
-
try {
|
|
90
|
-
console.log("\nDeukAgentRules init — let's configure your workspace.\n");
|
|
91
|
-
|
|
92
|
-
const stack = await selectOne(rl, "What is your primary tech stack?", STACKS);
|
|
93
|
-
const tools = await selectMany(rl, "Which agent tools do you use?", AGENT_TOOLS);
|
|
94
|
-
|
|
95
|
-
const targetAgents = join(opts.cwd, "AGENTS.md");
|
|
96
|
-
let agentsDefault = "inject";
|
|
97
|
-
if (!existsSync(targetAgents)) {
|
|
98
|
-
agentsDefault = "inject"; // will append markers
|
|
99
|
-
console.log("\n No AGENTS.md found — will create with markers.");
|
|
100
|
-
} else {
|
|
101
|
-
const content = readFileSync(targetAgents, "utf8");
|
|
102
|
-
const hasMarkers = content.includes("deuk-agent-rule:begin");
|
|
103
|
-
if (!hasMarkers) {
|
|
104
|
-
const choice = await selectOne(rl, "AGENTS.md exists but has no markers. How to apply?", [
|
|
105
|
-
{ label: "Append managed block at the end (safe)", value: "inject" },
|
|
106
|
-
{ label: "Overwrite entire AGENTS.md", value: "overwrite" },
|
|
107
|
-
{ label: "Skip AGENTS.md", value: "skip" },
|
|
108
|
-
]);
|
|
109
|
-
agentsDefault = choice;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
opts.agents = opts.agents ?? agentsDefault;
|
|
114
|
-
opts.stack = stack;
|
|
115
|
-
opts.agentTools = tools;
|
|
116
|
-
|
|
117
|
-
console.log("\n Stack : " + stack);
|
|
118
|
-
console.log(" Tools : " + (tools.join(", ") || "none"));
|
|
119
|
-
console.log(" AGENTS: " + opts.agents + "\n");
|
|
120
|
-
} finally {
|
|
121
|
-
rl.close();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
const INIT_CONFIG_FILENAME = ".deuk-agent-rule.config.json";
|
|
6
|
+
const INIT_CONFIG_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
export async function ask(rl, question) {
|
|
9
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function askYesNo(question, defaultYes = true) {
|
|
13
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
try {
|
|
15
|
+
const ans = (await ask(rl, question + (defaultYes ? " [Y/n]: " : " [y/N]: "))).trim().toLowerCase();
|
|
16
|
+
if (!ans) return defaultYes;
|
|
17
|
+
return ans === "y" || ans === "yes";
|
|
18
|
+
} finally {
|
|
19
|
+
rl.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function selectOne(rl, prompt, choices) {
|
|
24
|
+
console.log("\n" + prompt);
|
|
25
|
+
choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
|
|
26
|
+
while (true) {
|
|
27
|
+
const ans = (await ask(rl, ` Choice [1-${choices.length}]: `)).trim();
|
|
28
|
+
const idx = parseInt(ans, 10) - 1;
|
|
29
|
+
if (idx >= 0 && idx < choices.length) return choices[idx].value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function selectMany(rl, prompt, choices) {
|
|
34
|
+
console.log("\n" + prompt + " (comma-separated numbers, or 'all')");
|
|
35
|
+
choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
|
|
36
|
+
while (true) {
|
|
37
|
+
const ans = (await ask(rl, ` Choices: `)).trim().toLowerCase();
|
|
38
|
+
if (ans === "all" || ans === "") return choices.map((c) => c.value);
|
|
39
|
+
const parts = ans.split(/[,\s]+/).map((s) => parseInt(s, 10) - 1);
|
|
40
|
+
if (parts.every((i) => i >= 0 && i < choices.length)) return parts.map((i) => choices[i].value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadInitConfig(cwd) {
|
|
45
|
+
const p = join(cwd, INIT_CONFIG_FILENAME);
|
|
46
|
+
if (!existsSync(p)) return null;
|
|
47
|
+
try {
|
|
48
|
+
const j = JSON.parse(readFileSync(p, "utf8"));
|
|
49
|
+
if (j.version !== INIT_CONFIG_VERSION) return null;
|
|
50
|
+
return j;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function writeInitConfig(cwd, opts) {
|
|
57
|
+
const p = join(cwd, INIT_CONFIG_FILENAME);
|
|
58
|
+
const body = {
|
|
59
|
+
version: INIT_CONFIG_VERSION,
|
|
60
|
+
stack: opts.stack,
|
|
61
|
+
agentTools: opts.agentTools,
|
|
62
|
+
agentsMode: opts.agents ?? "inject",
|
|
63
|
+
updatedAt: new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
writeFileSync(p, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const STACKS = [
|
|
69
|
+
{ label: "Unity / C#", value: "unity" },
|
|
70
|
+
{ label: "Next.js + C#", value: "nextjs-dotnet" },
|
|
71
|
+
{ label: "Web (React / Vue / general)", value: "web" },
|
|
72
|
+
{ label: "Java / Spring Boot", value: "java" },
|
|
73
|
+
{ label: "Other / skip", value: "other" },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
export const AGENT_TOOLS = [
|
|
77
|
+
{ label: "Cursor", value: "cursor" },
|
|
78
|
+
{ label: "GitHub Copilot", value: "copilot" },
|
|
79
|
+
{ label: "Gemini / Antigravity", value: "gemini" },
|
|
80
|
+
{ label: "Claude (Cursor / Claude Code)", value: "claude" },
|
|
81
|
+
{ label: "Windsurf", value: "windsurf" },
|
|
82
|
+
{ label: "JetBrains AI Assistant", value: "jetbrains" },
|
|
83
|
+
{ label: "All of the above", value: "all" },
|
|
84
|
+
{ label: "Other / skip", value: "other" },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
export async function runInteractive(opts) {
|
|
88
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
89
|
+
try {
|
|
90
|
+
console.log("\nDeukAgentRules init — let's configure your workspace.\n");
|
|
91
|
+
|
|
92
|
+
const stack = await selectOne(rl, "What is your primary tech stack?", STACKS);
|
|
93
|
+
const tools = await selectMany(rl, "Which agent tools do you use?", AGENT_TOOLS);
|
|
94
|
+
|
|
95
|
+
const targetAgents = join(opts.cwd, "AGENTS.md");
|
|
96
|
+
let agentsDefault = "inject";
|
|
97
|
+
if (!existsSync(targetAgents)) {
|
|
98
|
+
agentsDefault = "inject"; // will append markers
|
|
99
|
+
console.log("\n No AGENTS.md found — will create with markers.");
|
|
100
|
+
} else {
|
|
101
|
+
const content = readFileSync(targetAgents, "utf8");
|
|
102
|
+
const hasMarkers = content.includes("deuk-agent-rule:begin");
|
|
103
|
+
if (!hasMarkers) {
|
|
104
|
+
const choice = await selectOne(rl, "AGENTS.md exists but has no markers. How to apply?", [
|
|
105
|
+
{ label: "Append managed block at the end (safe)", value: "inject" },
|
|
106
|
+
{ label: "Overwrite entire AGENTS.md", value: "overwrite" },
|
|
107
|
+
{ label: "Skip AGENTS.md", value: "skip" },
|
|
108
|
+
]);
|
|
109
|
+
agentsDefault = choice;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
opts.agents = opts.agents ?? agentsDefault;
|
|
114
|
+
opts.stack = stack;
|
|
115
|
+
opts.agentTools = tools;
|
|
116
|
+
|
|
117
|
+
console.log("\n Stack : " + stack);
|
|
118
|
+
console.log(" Tools : " + (tools.join(", ") || "none"));
|
|
119
|
+
console.log(" AGENTS: " + opts.agents + "\n");
|
|
120
|
+
} finally {
|
|
121
|
+
rl.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
2
|
-
import { basename, join } from "path";
|
|
3
|
-
import { toSlug, toRepoRelativePath, inferRefTitleAndTopic, resolveReferencedTicketPath } from "./cli-utils.mjs";
|
|
4
|
-
import { TICKET_DIR_NAME, appendTicketEntry, rebuildTicketIndexFromTopicFilesIfNeeded, detectConsumerTicketDir } from "./cli-ticket-logic.mjs";
|
|
5
|
-
|
|
6
|
-
import { createInterface } from "readline";
|
|
7
|
-
import { selectOne } from "./cli-prompts.mjs";
|
|
8
|
-
|
|
9
|
-
export async function runTicketCreate(opts) {
|
|
10
|
-
if (!opts.topic && !opts.ref) throw new Error("ticket create requires --topic or --ref");
|
|
11
|
-
|
|
12
|
-
const inferred = opts.ref ? inferRefTitleAndTopic(opts) : null;
|
|
13
|
-
const topic = toSlug(opts.topic || inferred?.topic || "ticket");
|
|
14
|
-
const title = opts.topic || inferred?.title || "ticket";
|
|
15
|
-
const group = toSlug(opts.group || "sub");
|
|
16
|
-
|
|
17
|
-
let path, source;
|
|
18
|
-
if (opts.ref) {
|
|
19
|
-
path = resolveReferencedTicketPath(opts);
|
|
20
|
-
source = "ticket-reference";
|
|
21
|
-
} else {
|
|
22
|
-
let body = opts.content ? String(opts.content).replace(/\\n/g, '\n') : "";
|
|
23
|
-
if (!body && opts.from) body = readFileSync(join(opts.cwd, opts.from), "utf8");
|
|
24
|
-
const abs = join(opts.cwd, TICKET_DIR_NAME, group, `${topic}-${Date.now()}.md`);
|
|
25
|
-
mkdirSync(join(opts.cwd, TICKET_DIR_NAME, group), { recursive: true });
|
|
26
|
-
path = toRepoRelativePath(opts.cwd, abs);
|
|
27
|
-
const marker = `\n\n<!-- Ticket (repo-relative): ${path} -->\n`;
|
|
28
|
-
writeFileSync(abs, body.trimEnd() + marker, "utf8");
|
|
29
|
-
source = "ticket-create";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
appendTicketEntry(opts.cwd, {
|
|
33
|
-
id: `ticket_${Date.now()}`,
|
|
34
|
-
title, topic, group, project: opts.project || "global",
|
|
35
|
-
createdAt: new Date().toISOString(), path, source
|
|
36
|
-
}, opts);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function runTicketList(opts) {
|
|
40
|
-
const ticketDir = detectConsumerTicketDir(opts.cwd);
|
|
41
|
-
if (!ticketDir) {
|
|
42
|
-
throw new Error("No ticket system found. Please run 'npx deuk-agent-rule init' first.");
|
|
43
|
-
}
|
|
44
|
-
const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
|
|
45
|
-
let rows = index.entries;
|
|
46
|
-
|
|
47
|
-
if (!opts.all) {
|
|
48
|
-
const targetStatus = opts.status || "open";
|
|
49
|
-
rows = rows.filter(e => e.status === targetStatus);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (opts.group) rows = rows.filter(e => e.group === opts.group);
|
|
53
|
-
if (opts.project) rows = rows.filter(e => e.project === opts.project);
|
|
54
|
-
|
|
55
|
-
console.log("# STATUS GROUP PROJECT CREATED TITLE");
|
|
56
|
-
rows.slice(0, opts.limit).forEach((e, idx) => {
|
|
57
|
-
const stat = (e.status === "closed" ? "[x]" : "[ ]").padEnd(7);
|
|
58
|
-
const safeTitle = String(e.title || e.topic || "").replace(/(\n|\\n)+/g, " ").slice(0, 50);
|
|
59
|
-
console.log(`${String(idx+1).padEnd(2)} ${stat} ${e.group.padEnd(10)} ${e.project.padEnd(11)} ${e.createdAt.padEnd(24)} ${safeTitle}`);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
import { updateTicketEntryStatus } from "./cli-ticket-logic.mjs";
|
|
64
|
-
|
|
65
|
-
export async function runTicketClose(opts) {
|
|
66
|
-
if (!opts.topic && !opts.latest) {
|
|
67
|
-
if (process.stdout.isTTY) {
|
|
68
|
-
const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
|
|
69
|
-
const choices = index.entries
|
|
70
|
-
.filter(e => e.status !== "closed")
|
|
71
|
-
.map(e => ({ label: `[${e.group}] ${e.title}`, value: e.topic }));
|
|
72
|
-
if (choices.length > 0) {
|
|
73
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
74
|
-
try {
|
|
75
|
-
opts.topic = await selectOne(rl, "Choose a ticket to close:", choices);
|
|
76
|
-
} finally {
|
|
77
|
-
rl.close();
|
|
78
|
-
}
|
|
79
|
-
} else {
|
|
80
|
-
throw new Error("No open tickets found to close.");
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
throw new Error("ticket close requires --topic or --latest");
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
opts.status = "closed";
|
|
87
|
-
const entry = updateTicketEntryStatus(opts.cwd, opts);
|
|
88
|
-
console.log(`ticket: closed -> ${entry.topic} (${entry.path})`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export async function runTicketUse(opts) {
|
|
92
|
-
const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
|
|
93
|
-
|
|
94
|
-
let targetTopic = opts.topic;
|
|
95
|
-
if (!targetTopic && !opts.latest) {
|
|
96
|
-
if (process.stdout.isTTY) {
|
|
97
|
-
const choices = index.entries
|
|
98
|
-
.map(e => ({ label: `${e.status === 'closed' ? '✓ ' : ''}[${e.group}] ${e.title}`, value: e.topic }));
|
|
99
|
-
if (choices.length > 0) {
|
|
100
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
101
|
-
try {
|
|
102
|
-
targetTopic = await selectOne(rl, "Choose a ticket to use:", choices);
|
|
103
|
-
} finally {
|
|
104
|
-
rl.close();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const found = opts.latest ? index.entries[0] : index.entries.find(e => e.topic.includes(targetTopic));
|
|
111
|
-
if (!found) throw new Error("No matching ticket found");
|
|
112
|
-
|
|
113
|
-
if (opts.pathOnly) console.log(found.path);
|
|
114
|
-
else {
|
|
115
|
-
console.log(`Path: ${found.path}`);
|
|
116
|
-
if (opts.printContent) console.log("\n" + readFileSync(join(opts.cwd, found.path), "utf8"));
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
import { getLegacyMigrationCandidate, parseLegacyTicketMeta } from "./cli-ticket-logic.mjs";
|
|
121
|
-
import { dirname } from "path";
|
|
122
|
-
|
|
123
|
-
export async function runTicketMigrate(opts) {
|
|
124
|
-
const candidate = getLegacyMigrationCandidate(opts.cwd);
|
|
125
|
-
if (!candidate) {
|
|
126
|
-
console.log("ticket: no legacy LATEST.md migration candidate found");
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const { title, group, project } = parseLegacyTicketMeta(candidate.body);
|
|
131
|
-
const topic = toSlug(title);
|
|
132
|
-
const stamp = Date.now();
|
|
133
|
-
const relPath = join(TICKET_DIR_NAME, group, `${topic}-${stamp}.md`);
|
|
134
|
-
const absPath = join(opts.cwd, relPath);
|
|
135
|
-
|
|
136
|
-
if (opts.dryRun) {
|
|
137
|
-
console.log("ticket: would migrate -> " + relPath);
|
|
138
|
-
} else {
|
|
139
|
-
mkdirSync(dirname(absPath), { recursive: true });
|
|
140
|
-
const marker = `\n\n<!-- Ticket (repo-relative): ${relPath} -->\n`;
|
|
141
|
-
writeFileSync(absPath, candidate.body.trimEnd() + marker, "utf8");
|
|
142
|
-
console.log("ticket: migrated body -> " + relPath);
|
|
143
|
-
|
|
144
|
-
appendTicketEntry(opts.cwd, {
|
|
145
|
-
id: `ticket_migrated_${stamp}`,
|
|
146
|
-
title,
|
|
147
|
-
topic,
|
|
148
|
-
group,
|
|
149
|
-
project,
|
|
150
|
-
createdAt: new Date().toISOString(),
|
|
151
|
-
path: relPath,
|
|
152
|
-
source: "ticket-migrate",
|
|
153
|
-
}, opts);
|
|
154
|
-
if (existsSync(candidate.latestPath)) {
|
|
155
|
-
unlinkSync(candidate.latestPath);
|
|
156
|
-
console.log("ticket: deleted legacy LATEST.md");
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
2
|
+
import { basename, join } from "path";
|
|
3
|
+
import { toSlug, toRepoRelativePath, inferRefTitleAndTopic, resolveReferencedTicketPath } from "./cli-utils.mjs";
|
|
4
|
+
import { TICKET_DIR_NAME, appendTicketEntry, rebuildTicketIndexFromTopicFilesIfNeeded, detectConsumerTicketDir } from "./cli-ticket-logic.mjs";
|
|
5
|
+
|
|
6
|
+
import { createInterface } from "readline";
|
|
7
|
+
import { selectOne } from "./cli-prompts.mjs";
|
|
8
|
+
|
|
9
|
+
export async function runTicketCreate(opts) {
|
|
10
|
+
if (!opts.topic && !opts.ref) throw new Error("ticket create requires --topic or --ref");
|
|
11
|
+
|
|
12
|
+
const inferred = opts.ref ? inferRefTitleAndTopic(opts) : null;
|
|
13
|
+
const topic = toSlug(opts.topic || inferred?.topic || "ticket");
|
|
14
|
+
const title = opts.topic || inferred?.title || "ticket";
|
|
15
|
+
const group = toSlug(opts.group || "sub");
|
|
16
|
+
|
|
17
|
+
let path, source;
|
|
18
|
+
if (opts.ref) {
|
|
19
|
+
path = resolveReferencedTicketPath(opts);
|
|
20
|
+
source = "ticket-reference";
|
|
21
|
+
} else {
|
|
22
|
+
let body = opts.content ? String(opts.content).replace(/\\n/g, '\n') : "";
|
|
23
|
+
if (!body && opts.from) body = readFileSync(join(opts.cwd, opts.from), "utf8");
|
|
24
|
+
const abs = join(opts.cwd, TICKET_DIR_NAME, group, `${topic}-${Date.now()}.md`);
|
|
25
|
+
mkdirSync(join(opts.cwd, TICKET_DIR_NAME, group), { recursive: true });
|
|
26
|
+
path = toRepoRelativePath(opts.cwd, abs);
|
|
27
|
+
const marker = `\n\n<!-- Ticket (repo-relative): ${path} -->\n`;
|
|
28
|
+
writeFileSync(abs, body.trimEnd() + marker, "utf8");
|
|
29
|
+
source = "ticket-create";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
appendTicketEntry(opts.cwd, {
|
|
33
|
+
id: `ticket_${Date.now()}`,
|
|
34
|
+
title, topic, group, project: opts.project || "global",
|
|
35
|
+
createdAt: new Date().toISOString(), path, source
|
|
36
|
+
}, opts);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runTicketList(opts) {
|
|
40
|
+
const ticketDir = detectConsumerTicketDir(opts.cwd);
|
|
41
|
+
if (!ticketDir) {
|
|
42
|
+
throw new Error("No ticket system found. Please run 'npx deuk-agent-rule init' first.");
|
|
43
|
+
}
|
|
44
|
+
const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
|
|
45
|
+
let rows = index.entries;
|
|
46
|
+
|
|
47
|
+
if (!opts.all) {
|
|
48
|
+
const targetStatus = opts.status || "open";
|
|
49
|
+
rows = rows.filter(e => e.status === targetStatus);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (opts.group) rows = rows.filter(e => e.group === opts.group);
|
|
53
|
+
if (opts.project) rows = rows.filter(e => e.project === opts.project);
|
|
54
|
+
|
|
55
|
+
console.log("# STATUS GROUP PROJECT CREATED TITLE");
|
|
56
|
+
rows.slice(0, opts.limit).forEach((e, idx) => {
|
|
57
|
+
const stat = (e.status === "closed" ? "[x]" : "[ ]").padEnd(7);
|
|
58
|
+
const safeTitle = String(e.title || e.topic || "").replace(/(\n|\\n)+/g, " ").slice(0, 50);
|
|
59
|
+
console.log(`${String(idx+1).padEnd(2)} ${stat} ${e.group.padEnd(10)} ${e.project.padEnd(11)} ${e.createdAt.padEnd(24)} ${safeTitle}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
import { updateTicketEntryStatus } from "./cli-ticket-logic.mjs";
|
|
64
|
+
|
|
65
|
+
export async function runTicketClose(opts) {
|
|
66
|
+
if (!opts.topic && !opts.latest) {
|
|
67
|
+
if (process.stdout.isTTY) {
|
|
68
|
+
const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
|
|
69
|
+
const choices = index.entries
|
|
70
|
+
.filter(e => e.status !== "closed")
|
|
71
|
+
.map(e => ({ label: `[${e.group}] ${e.title}`, value: e.topic }));
|
|
72
|
+
if (choices.length > 0) {
|
|
73
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
74
|
+
try {
|
|
75
|
+
opts.topic = await selectOne(rl, "Choose a ticket to close:", choices);
|
|
76
|
+
} finally {
|
|
77
|
+
rl.close();
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error("No open tickets found to close.");
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
throw new Error("ticket close requires --topic or --latest");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
opts.status = "closed";
|
|
87
|
+
const entry = updateTicketEntryStatus(opts.cwd, opts);
|
|
88
|
+
console.log(`ticket: closed -> ${entry.topic} (${entry.path})`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function runTicketUse(opts) {
|
|
92
|
+
const index = rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, opts);
|
|
93
|
+
|
|
94
|
+
let targetTopic = opts.topic;
|
|
95
|
+
if (!targetTopic && !opts.latest) {
|
|
96
|
+
if (process.stdout.isTTY) {
|
|
97
|
+
const choices = index.entries
|
|
98
|
+
.map(e => ({ label: `${e.status === 'closed' ? '✓ ' : ''}[${e.group}] ${e.title}`, value: e.topic }));
|
|
99
|
+
if (choices.length > 0) {
|
|
100
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
101
|
+
try {
|
|
102
|
+
targetTopic = await selectOne(rl, "Choose a ticket to use:", choices);
|
|
103
|
+
} finally {
|
|
104
|
+
rl.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const found = opts.latest ? index.entries[0] : index.entries.find(e => e.topic.includes(targetTopic));
|
|
111
|
+
if (!found) throw new Error("No matching ticket found");
|
|
112
|
+
|
|
113
|
+
if (opts.pathOnly) console.log(found.path);
|
|
114
|
+
else {
|
|
115
|
+
console.log(`Path: ${found.path}`);
|
|
116
|
+
if (opts.printContent) console.log("\n" + readFileSync(join(opts.cwd, found.path), "utf8"));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
import { getLegacyMigrationCandidate, parseLegacyTicketMeta } from "./cli-ticket-logic.mjs";
|
|
121
|
+
import { dirname } from "path";
|
|
122
|
+
|
|
123
|
+
export async function runTicketMigrate(opts) {
|
|
124
|
+
const candidate = getLegacyMigrationCandidate(opts.cwd);
|
|
125
|
+
if (!candidate) {
|
|
126
|
+
console.log("ticket: no legacy LATEST.md migration candidate found");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { title, group, project } = parseLegacyTicketMeta(candidate.body);
|
|
131
|
+
const topic = toSlug(title);
|
|
132
|
+
const stamp = Date.now();
|
|
133
|
+
const relPath = join(TICKET_DIR_NAME, group, `${topic}-${stamp}.md`);
|
|
134
|
+
const absPath = join(opts.cwd, relPath);
|
|
135
|
+
|
|
136
|
+
if (opts.dryRun) {
|
|
137
|
+
console.log("ticket: would migrate -> " + relPath);
|
|
138
|
+
} else {
|
|
139
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
140
|
+
const marker = `\n\n<!-- Ticket (repo-relative): ${relPath} -->\n`;
|
|
141
|
+
writeFileSync(absPath, candidate.body.trimEnd() + marker, "utf8");
|
|
142
|
+
console.log("ticket: migrated body -> " + relPath);
|
|
143
|
+
|
|
144
|
+
appendTicketEntry(opts.cwd, {
|
|
145
|
+
id: `ticket_migrated_${stamp}`,
|
|
146
|
+
title,
|
|
147
|
+
topic,
|
|
148
|
+
group,
|
|
149
|
+
project,
|
|
150
|
+
createdAt: new Date().toISOString(),
|
|
151
|
+
path: relPath,
|
|
152
|
+
source: "ticket-migrate",
|
|
153
|
+
}, opts);
|
|
154
|
+
if (existsSync(candidate.latestPath)) {
|
|
155
|
+
unlinkSync(candidate.latestPath);
|
|
156
|
+
console.log("ticket: deleted legacy LATEST.md");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|