@4-r-c-4-n-4/todo 0.1.2
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/BIBLE.md +212 -0
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/dist/bundle.js +5109 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +36 -0
- package/dist/commands/analyze.d.ts +2 -0
- package/dist/commands/analyze.js +77 -0
- package/dist/commands/close.d.ts +2 -0
- package/dist/commands/close.js +75 -0
- package/dist/commands/dedup.d.ts +2 -0
- package/dist/commands/dedup.js +74 -0
- package/dist/commands/edit.d.ts +2 -0
- package/dist/commands/edit.js +82 -0
- package/dist/commands/export.d.ts +2 -0
- package/dist/commands/export.js +25 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +29 -0
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.js +101 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +91 -0
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.js +157 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +114 -0
- package/dist/commands/show.d.ts +2 -0
- package/dist/commands/show.js +132 -0
- package/dist/commands/transition.d.ts +2 -0
- package/dist/commands/transition.js +66 -0
- package/dist/commands/work.d.ts +2 -0
- package/dist/commands/work.js +128 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +77 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.js +24 -0
- package/dist/dedup.d.ts +8 -0
- package/dist/dedup.js +63 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +26 -0
- package/dist/fingerprint.d.ts +9 -0
- package/dist/fingerprint.js +27 -0
- package/dist/format.d.ts +14 -0
- package/dist/format.js +44 -0
- package/dist/git.d.ts +19 -0
- package/dist/git.js +115 -0
- package/dist/scan.d.ts +7 -0
- package/dist/scan.js +146 -0
- package/dist/state.d.ts +13 -0
- package/dist/state.js +108 -0
- package/dist/ticket.d.ts +23 -0
- package/dist/ticket.js +143 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +3 -0
- package/package.json +51 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerNew = registerNew;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const config_js_1 = require("../config.js");
|
|
6
|
+
const context_js_1 = require("../context.js");
|
|
7
|
+
const fingerprint_js_1 = require("../fingerprint.js");
|
|
8
|
+
const ticket_js_1 = require("../ticket.js");
|
|
9
|
+
const VALID_TYPES = [
|
|
10
|
+
"bug",
|
|
11
|
+
"feature",
|
|
12
|
+
"refactor",
|
|
13
|
+
"chore",
|
|
14
|
+
"debt",
|
|
15
|
+
];
|
|
16
|
+
const VALID_SOURCES = [
|
|
17
|
+
"log",
|
|
18
|
+
"test",
|
|
19
|
+
"agent",
|
|
20
|
+
"human",
|
|
21
|
+
"comment",
|
|
22
|
+
];
|
|
23
|
+
function registerNew(program) {
|
|
24
|
+
program
|
|
25
|
+
.command("new [summary]")
|
|
26
|
+
.description("Create a new ticket")
|
|
27
|
+
.option("-t, --type <type>", "ticket type: bug|feature|refactor|chore|debt", "chore")
|
|
28
|
+
.option("-s, --source <source>", "source type: log|test|agent|human|comment", "human")
|
|
29
|
+
.option("-f, --file <path>", "associate a file path")
|
|
30
|
+
.option("-l, --lines <start,end>", "line range for the file (e.g. 10,20)")
|
|
31
|
+
.option("--tags <tags>", "comma-separated tags")
|
|
32
|
+
.option("--parent <id>", "parent ticket ID")
|
|
33
|
+
.option("--pipe", "read summary from stdin")
|
|
34
|
+
.action((summaryArg, opts) => {
|
|
35
|
+
// Validate type
|
|
36
|
+
const ticketType = opts.type;
|
|
37
|
+
if (!VALID_TYPES.includes(ticketType)) {
|
|
38
|
+
console.error(`Error: invalid type '${ticketType}'. Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
// Validate source
|
|
42
|
+
const sourceType = opts.source;
|
|
43
|
+
if (!VALID_SOURCES.includes(sourceType)) {
|
|
44
|
+
console.error(`Error: invalid source '${sourceType}'. Must be one of: ${VALID_SOURCES.join(", ")}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
// Get context (init required)
|
|
48
|
+
const ctx = (0, context_js_1.getContext)(true);
|
|
49
|
+
const { repoRoot } = ctx;
|
|
50
|
+
// Ensure dirs exist
|
|
51
|
+
(0, config_js_1.ensureTodoDir)(repoRoot);
|
|
52
|
+
// Handle --pipe
|
|
53
|
+
let summary;
|
|
54
|
+
if (opts.pipe) {
|
|
55
|
+
if (process.stdin.isTTY) {
|
|
56
|
+
console.error("Error: --pipe requires piped input");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const stdinContent = (0, node_fs_1.readFileSync)("/dev/stdin", "utf8");
|
|
60
|
+
const lines = stdinContent.split("\n").filter((l) => l.trim() !== "");
|
|
61
|
+
summary = lines[lines.length - 1] ?? "";
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
summary = summaryArg ?? "";
|
|
65
|
+
}
|
|
66
|
+
if (!summary.trim()) {
|
|
67
|
+
console.error("Error: summary is required");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
// Build source object
|
|
71
|
+
const sourceObj = { type: sourceType };
|
|
72
|
+
// Compute traceback fingerprint for log/test sources with piped content
|
|
73
|
+
if (opts.pipe && (sourceType === "log" || sourceType === "test")) {
|
|
74
|
+
// summary was extracted from stdin; we want to fingerprint the full content
|
|
75
|
+
// re-read isn't possible; fingerprint from summary as best-effort
|
|
76
|
+
const fp = (0, fingerprint_js_1.tracebackFingerprint)(summary);
|
|
77
|
+
sourceObj["traceback_fingerprint"] = fp;
|
|
78
|
+
}
|
|
79
|
+
// Build file reference
|
|
80
|
+
let fileRefs;
|
|
81
|
+
if (opts.file) {
|
|
82
|
+
const fileRef = { path: opts.file };
|
|
83
|
+
if (opts.lines) {
|
|
84
|
+
const parts = opts.lines.split(",");
|
|
85
|
+
if (parts.length === 2) {
|
|
86
|
+
const start = parseInt(parts[0], 10);
|
|
87
|
+
const end = parseInt(parts[1], 10);
|
|
88
|
+
if (!isNaN(start) && !isNaN(end)) {
|
|
89
|
+
fileRef.lines = [start, end];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
fileRefs = [fileRef];
|
|
94
|
+
}
|
|
95
|
+
// Parse tags
|
|
96
|
+
let tags;
|
|
97
|
+
if (opts.tags) {
|
|
98
|
+
tags = opts.tags
|
|
99
|
+
.split(",")
|
|
100
|
+
.map((t) => t.trim())
|
|
101
|
+
.filter((t) => t.length > 0);
|
|
102
|
+
}
|
|
103
|
+
const createdAt = new Date().toISOString();
|
|
104
|
+
const rawPayload = summary + (opts.file ?? "");
|
|
105
|
+
const id = (0, ticket_js_1.generateId)(sourceType, rawPayload, createdAt);
|
|
106
|
+
// Dedup check
|
|
107
|
+
try {
|
|
108
|
+
const openTickets = (0, ticket_js_1.listTickets)(repoRoot, "open");
|
|
109
|
+
for (const existing of openTickets) {
|
|
110
|
+
if (existing.summary === summary) {
|
|
111
|
+
console.error(`Warning: possible duplicate of ${existing.id}`);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// ignore listing errors
|
|
118
|
+
}
|
|
119
|
+
// Handle parent
|
|
120
|
+
if (opts.parent) {
|
|
121
|
+
const { readTicketByPrefix } = require("../ticket.js");
|
|
122
|
+
try {
|
|
123
|
+
const parentTicket = readTicketByPrefix(repoRoot, opts.parent);
|
|
124
|
+
if (!parentTicket.relationships)
|
|
125
|
+
parentTicket.relationships = {};
|
|
126
|
+
if (!parentTicket.relationships.children)
|
|
127
|
+
parentTicket.relationships.children = [];
|
|
128
|
+
parentTicket.relationships.children.push(id);
|
|
129
|
+
parentTicket.updated_at = createdAt;
|
|
130
|
+
(0, ticket_js_1.writeTicket)(repoRoot, parentTicket);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
console.error(`Error: parent ticket '${opts.parent}' not found`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Build ticket
|
|
138
|
+
const ticket = {
|
|
139
|
+
id,
|
|
140
|
+
type: ticketType,
|
|
141
|
+
state: "open",
|
|
142
|
+
summary,
|
|
143
|
+
source: sourceObj,
|
|
144
|
+
created_at: createdAt,
|
|
145
|
+
updated_at: createdAt,
|
|
146
|
+
};
|
|
147
|
+
if (fileRefs)
|
|
148
|
+
ticket.files = fileRefs;
|
|
149
|
+
if (tags && tags.length > 0)
|
|
150
|
+
ticket.tags = tags;
|
|
151
|
+
if (opts.parent) {
|
|
152
|
+
ticket.relationships = { parent: opts.parent };
|
|
153
|
+
}
|
|
154
|
+
(0, ticket_js_1.writeTicket)(repoRoot, ticket);
|
|
155
|
+
console.log(ticket.id);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerScan = registerScan;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const config_js_1 = require("../config.js");
|
|
6
|
+
const context_js_1 = require("../context.js");
|
|
7
|
+
const errors_js_1 = require("../errors.js");
|
|
8
|
+
const git_js_1 = require("../git.js");
|
|
9
|
+
const scan_js_1 = require("../scan.js");
|
|
10
|
+
const ticket_js_1 = require("../ticket.js");
|
|
11
|
+
const VALID_TYPES = [
|
|
12
|
+
"bug",
|
|
13
|
+
"feature",
|
|
14
|
+
"refactor",
|
|
15
|
+
"chore",
|
|
16
|
+
"debt",
|
|
17
|
+
];
|
|
18
|
+
function registerScan(program) {
|
|
19
|
+
program
|
|
20
|
+
.command("scan")
|
|
21
|
+
.description("Scan source tree for TODO/FIXME/etc comments and create tickets")
|
|
22
|
+
.option("--dry-run", "print what would be created, do not write")
|
|
23
|
+
.option("--type <type>", "ticket type for created tickets", "chore")
|
|
24
|
+
.action((opts) => {
|
|
25
|
+
try {
|
|
26
|
+
const ticketType = opts.type;
|
|
27
|
+
if (!VALID_TYPES.includes(ticketType)) {
|
|
28
|
+
console.error(`Error: invalid type '${ticketType}'. Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const ctx = (0, context_js_1.getContext)(true);
|
|
32
|
+
const { repoRoot, config } = ctx;
|
|
33
|
+
const scanPatterns = config.intake?.scan_patterns ?? [
|
|
34
|
+
"TODO",
|
|
35
|
+
"FIXME",
|
|
36
|
+
"HACK",
|
|
37
|
+
"XXX",
|
|
38
|
+
];
|
|
39
|
+
const scanExclude = config.intake?.scan_exclude ?? [
|
|
40
|
+
".todo",
|
|
41
|
+
"node_modules",
|
|
42
|
+
".git",
|
|
43
|
+
"dist",
|
|
44
|
+
"build",
|
|
45
|
+
];
|
|
46
|
+
(0, config_js_1.ensureTodoDir)(repoRoot);
|
|
47
|
+
const matches = (0, scan_js_1.scanComments)(repoRoot, scanPatterns, scanExclude);
|
|
48
|
+
// Load all open tickets for dedup
|
|
49
|
+
const openTickets = (0, ticket_js_1.listTickets)(repoRoot, "open");
|
|
50
|
+
let created = 0;
|
|
51
|
+
let skipped = 0;
|
|
52
|
+
for (const match of matches) {
|
|
53
|
+
const normalizedText = match.text.trim().toLowerCase();
|
|
54
|
+
const fingerprint = (0, node_crypto_1.createHash)("sha256")
|
|
55
|
+
.update(normalizedText)
|
|
56
|
+
.digest("hex");
|
|
57
|
+
// Check for existing ticket with same fingerprint
|
|
58
|
+
const exists = openTickets.some((t) => {
|
|
59
|
+
if (t.source.type === "comment" && t.source.raw) {
|
|
60
|
+
const existingFp = (0, node_crypto_1.createHash)("sha256")
|
|
61
|
+
.update(t.source.raw.trim().toLowerCase())
|
|
62
|
+
.digest("hex");
|
|
63
|
+
return existingFp === fingerprint;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
});
|
|
67
|
+
if (exists) {
|
|
68
|
+
console.log(`Skipping existing: ${match.text}`);
|
|
69
|
+
skipped++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (opts.dryRun) {
|
|
73
|
+
console.log(`Would create: ${match.file}:${match.line} [${match.keyword}] ${match.text}`);
|
|
74
|
+
created++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const createdAt = new Date().toISOString();
|
|
78
|
+
const id = (0, ticket_js_1.generateId)("comment", match.text + match.file + match.line, createdAt);
|
|
79
|
+
let lastCommit;
|
|
80
|
+
try {
|
|
81
|
+
const c = (0, git_js_1.getLastCommitForFile)(match.file, repoRoot);
|
|
82
|
+
if (c)
|
|
83
|
+
lastCommit = c;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
lastCommit = undefined;
|
|
87
|
+
}
|
|
88
|
+
const ticket = {
|
|
89
|
+
id,
|
|
90
|
+
type: ticketType,
|
|
91
|
+
state: "open",
|
|
92
|
+
summary: `${match.keyword}: ${match.text}`.slice(0, 120),
|
|
93
|
+
source: { type: "comment", raw: match.text },
|
|
94
|
+
files: [
|
|
95
|
+
{
|
|
96
|
+
path: match.file,
|
|
97
|
+
lines: [match.line, match.line],
|
|
98
|
+
commit: lastCommit,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
created_at: createdAt,
|
|
102
|
+
updated_at: createdAt,
|
|
103
|
+
};
|
|
104
|
+
(0, ticket_js_1.writeTicket)(repoRoot, ticket);
|
|
105
|
+
openTickets.push(ticket); // add to in-memory list for subsequent dedup checks
|
|
106
|
+
created++;
|
|
107
|
+
}
|
|
108
|
+
console.log(`Created ${created} tickets, skipped ${skipped} duplicates`);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
(0, errors_js_1.handleError)(err);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerShow = registerShow;
|
|
4
|
+
const context_js_1 = require("../context.js");
|
|
5
|
+
const errors_js_1 = require("../errors.js");
|
|
6
|
+
const ticket_js_1 = require("../ticket.js");
|
|
7
|
+
function formatDate(iso) {
|
|
8
|
+
return iso.slice(0, 10); // YYYY-MM-DD
|
|
9
|
+
}
|
|
10
|
+
function formatTicketDetail(ticket) {
|
|
11
|
+
const lines = [];
|
|
12
|
+
lines.push(`=== ${ticket.id} [${ticket.type}] ${ticket.state} ===`);
|
|
13
|
+
lines.push(`SUMMARY: ${ticket.summary}`);
|
|
14
|
+
if (ticket.tags && ticket.tags.length > 0) {
|
|
15
|
+
lines.push(`TAGS: ${ticket.tags.join(", ")}`);
|
|
16
|
+
}
|
|
17
|
+
lines.push(`CREATED: ${ticket.created_at}`);
|
|
18
|
+
lines.push(`UPDATED: ${ticket.updated_at}`);
|
|
19
|
+
if (ticket.description) {
|
|
20
|
+
lines.push("");
|
|
21
|
+
lines.push(`DESCRIPTION: ${ticket.description}`);
|
|
22
|
+
}
|
|
23
|
+
// Source
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push(`SOURCE: ${ticket.source.type}`);
|
|
26
|
+
if ("test_file" in ticket.source && ticket.source.test_file) {
|
|
27
|
+
const fn = "test_function" in ticket.source
|
|
28
|
+
? ticket.source.test_function
|
|
29
|
+
: undefined;
|
|
30
|
+
lines.push(` File: ${ticket.source.test_file}${fn ? `::${fn}` : ""}`);
|
|
31
|
+
}
|
|
32
|
+
if ("raw" in ticket.source && ticket.source.raw) {
|
|
33
|
+
lines.push(` Raw: ${ticket.source.raw}`);
|
|
34
|
+
}
|
|
35
|
+
// Files
|
|
36
|
+
if (ticket.files && ticket.files.length > 0) {
|
|
37
|
+
lines.push("");
|
|
38
|
+
lines.push("FILES:");
|
|
39
|
+
for (const f of ticket.files) {
|
|
40
|
+
let desc = ` ${f.path}`;
|
|
41
|
+
if (f.lines)
|
|
42
|
+
desc += `:${f.lines[0]}-${f.lines[1]}`;
|
|
43
|
+
if (f.commit)
|
|
44
|
+
desc += ` (${f.commit.slice(0, 7)})`;
|
|
45
|
+
if (f.note)
|
|
46
|
+
desc += ` — ${f.note}`;
|
|
47
|
+
lines.push(desc);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Analysis
|
|
51
|
+
if (ticket.analysis && ticket.analysis.length > 0) {
|
|
52
|
+
lines.push("");
|
|
53
|
+
lines.push(`ANALYSIS: (${ticket.analysis.length} entries)`);
|
|
54
|
+
for (let i = 0; i < ticket.analysis.length; i++) {
|
|
55
|
+
const a = ticket.analysis[i];
|
|
56
|
+
const conf = a.confidence ? ` [${a.confidence}]` : "";
|
|
57
|
+
lines.push(` [${i}] ${formatDate(a.timestamp)} • ${a.type}${conf}`);
|
|
58
|
+
lines.push(` ${a.content}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Relationships
|
|
62
|
+
if (ticket.relationships) {
|
|
63
|
+
const rel = ticket.relationships;
|
|
64
|
+
const parts = [];
|
|
65
|
+
if (rel.parent)
|
|
66
|
+
parts.push(` parent: ${rel.parent}`);
|
|
67
|
+
if (rel.children && rel.children.length > 0)
|
|
68
|
+
parts.push(` children: ${rel.children.join(", ")}`);
|
|
69
|
+
if (rel.depends_on && rel.depends_on.length > 0)
|
|
70
|
+
parts.push(` depends_on: ${rel.depends_on.join(", ")}`);
|
|
71
|
+
if (rel.blocks && rel.blocks.length > 0)
|
|
72
|
+
parts.push(` blocks: ${rel.blocks.join(", ")}`);
|
|
73
|
+
if (rel.related && rel.related.length > 0)
|
|
74
|
+
parts.push(` related: ${rel.related.join(", ")}`);
|
|
75
|
+
if (rel.duplicates)
|
|
76
|
+
parts.push(` duplicates: ${rel.duplicates}`);
|
|
77
|
+
if (rel.linked_commits && rel.linked_commits.length > 0)
|
|
78
|
+
parts.push(` linked_commits: ${rel.linked_commits.join(", ")}`);
|
|
79
|
+
if (parts.length > 0) {
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push("RELATIONSHIPS:");
|
|
82
|
+
lines.push(...parts);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Work
|
|
86
|
+
if (ticket.work) {
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push("WORK:");
|
|
89
|
+
lines.push(` branch: ${ticket.work.branch}`);
|
|
90
|
+
lines.push(` base: ${ticket.work.base_branch}`);
|
|
91
|
+
lines.push(` started: ${ticket.work.started_at} by ${ticket.work.started_by}`);
|
|
92
|
+
}
|
|
93
|
+
// Resolution
|
|
94
|
+
if (ticket.resolution) {
|
|
95
|
+
lines.push("");
|
|
96
|
+
lines.push("RESOLUTION:");
|
|
97
|
+
lines.push(` commit: ${ticket.resolution.commit}`);
|
|
98
|
+
lines.push(` resolved_at: ${ticket.resolution.resolved_at} by ${ticket.resolution.resolved_by}`);
|
|
99
|
+
if (ticket.resolution.test_file) {
|
|
100
|
+
const fn = ticket.resolution.test_function
|
|
101
|
+
? `::${ticket.resolution.test_function}`
|
|
102
|
+
: "";
|
|
103
|
+
lines.push(` test: ${ticket.resolution.test_file}${fn}`);
|
|
104
|
+
}
|
|
105
|
+
if (ticket.resolution.note) {
|
|
106
|
+
lines.push(` note: ${ticket.resolution.note}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
function registerShow(program) {
|
|
112
|
+
program
|
|
113
|
+
.command("show <id>")
|
|
114
|
+
.description("Show ticket details")
|
|
115
|
+
.option("--raw", "dump raw JSON")
|
|
116
|
+
.action((id, opts) => {
|
|
117
|
+
const ctx = (0, context_js_1.getContext)(true);
|
|
118
|
+
const { repoRoot } = ctx;
|
|
119
|
+
try {
|
|
120
|
+
const ticket = (0, ticket_js_1.readTicketByPrefix)(repoRoot, id);
|
|
121
|
+
if (opts.raw) {
|
|
122
|
+
console.log(JSON.stringify(ticket, null, 2));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.log(formatTicketDetail(ticket));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
(0, errors_js_1.handleError)(err);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerTransition = registerTransition;
|
|
4
|
+
const context_js_1 = require("../context.js");
|
|
5
|
+
const errors_js_1 = require("../errors.js");
|
|
6
|
+
const state_js_1 = require("../state.js");
|
|
7
|
+
const ticket_js_1 = require("../ticket.js");
|
|
8
|
+
const VALID_STATES = [
|
|
9
|
+
"open",
|
|
10
|
+
"active",
|
|
11
|
+
"blocked",
|
|
12
|
+
"done",
|
|
13
|
+
"wontfix",
|
|
14
|
+
"duplicate",
|
|
15
|
+
];
|
|
16
|
+
function registerTransition(program) {
|
|
17
|
+
program
|
|
18
|
+
.command("transition <id> <state>")
|
|
19
|
+
.description("Transition ticket state")
|
|
20
|
+
.option("--commit <sha>", "resolution commit")
|
|
21
|
+
.option("--test <file::func>", "test file and function (colon-separated)")
|
|
22
|
+
.option("--note <text>", "resolution note")
|
|
23
|
+
.option("--depends-on <id>", "set dependency")
|
|
24
|
+
.option("--duplicate-of <id>", "mark as duplicate")
|
|
25
|
+
.action((id, state, opts) => {
|
|
26
|
+
const ctx = (0, context_js_1.getContext)(true);
|
|
27
|
+
const { repoRoot } = ctx;
|
|
28
|
+
if (!VALID_STATES.includes(state)) {
|
|
29
|
+
console.error(`Error: invalid state '${state}'. Must be one of: ${VALID_STATES.join(", ")}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const ticket = (0, ticket_js_1.readTicketByPrefix)(repoRoot, id);
|
|
34
|
+
const fromState = ticket.state;
|
|
35
|
+
// Parse --test as "file::func" or "file"
|
|
36
|
+
let testFile;
|
|
37
|
+
let testFunction;
|
|
38
|
+
if (opts.test) {
|
|
39
|
+
const parts = opts.test.split("::");
|
|
40
|
+
testFile = parts[0];
|
|
41
|
+
testFunction = parts[1];
|
|
42
|
+
}
|
|
43
|
+
const params = {
|
|
44
|
+
commit: opts.commit,
|
|
45
|
+
test_file: testFile,
|
|
46
|
+
test_function: testFunction,
|
|
47
|
+
note: opts.note,
|
|
48
|
+
depends_on: opts.dependsOn,
|
|
49
|
+
duplicate_of: opts.duplicateOf,
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
(0, state_js_1.validateTransition)(ticket, state, params, repoRoot);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error(`Error: ${err.message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const updated = (0, state_js_1.applyTransition)(ticket, state, params, repoRoot);
|
|
59
|
+
(0, ticket_js_1.writeTicket)(repoRoot, updated);
|
|
60
|
+
console.log(`Transitioned ${updated.id}: ${fromState} → ${state}`);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
(0, errors_js_1.handleError)(err);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerWork = registerWork;
|
|
4
|
+
const context_js_1 = require("../context.js");
|
|
5
|
+
const errors_js_1 = require("../errors.js");
|
|
6
|
+
const git_js_1 = require("../git.js");
|
|
7
|
+
const state_js_1 = require("../state.js");
|
|
8
|
+
const ticket_js_1 = require("../ticket.js");
|
|
9
|
+
function registerWork(program) {
|
|
10
|
+
program
|
|
11
|
+
.command("work <id>")
|
|
12
|
+
.description("Start or resume work on a ticket")
|
|
13
|
+
.option("--branch <name>", "override branch name")
|
|
14
|
+
.option("--actor <name>", "override actor (also reads TODO_ACTOR env)")
|
|
15
|
+
.action((id, opts) => {
|
|
16
|
+
const ctx = (0, context_js_1.getContext)(true);
|
|
17
|
+
const { repoRoot } = ctx;
|
|
18
|
+
try {
|
|
19
|
+
const ticket = (0, ticket_js_1.readTicketByPrefix)(repoRoot, id);
|
|
20
|
+
// Check not terminal
|
|
21
|
+
if (ticket_js_1.TERMINAL_STATES.includes(ticket.state)) {
|
|
22
|
+
console.error(`Error: ticket ${ticket.id} is in terminal state '${ticket.state}'. Cannot work on it.`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
// Resolve branch name
|
|
26
|
+
let branch;
|
|
27
|
+
if (opts.branch) {
|
|
28
|
+
branch = opts.branch;
|
|
29
|
+
}
|
|
30
|
+
else if (ticket.relationships?.parent) {
|
|
31
|
+
branch = `todo/${ticket.relationships.parent}`;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
branch = `todo/${ticket.id}`;
|
|
35
|
+
}
|
|
36
|
+
// Resolve actor
|
|
37
|
+
let actor;
|
|
38
|
+
if (opts.actor) {
|
|
39
|
+
actor = opts.actor;
|
|
40
|
+
}
|
|
41
|
+
else if (process.env["TODO_ACTOR"]) {
|
|
42
|
+
actor = process.env["TODO_ACTOR"];
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
try {
|
|
46
|
+
actor = (0, git_js_1.getGitUserName)(repoRoot);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
actor = "unknown";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const defaultBranch = (0, git_js_1.getDefaultBranch)(repoRoot);
|
|
53
|
+
if ((0, git_js_1.branchExists)(branch, repoRoot)) {
|
|
54
|
+
// Resume
|
|
55
|
+
(0, git_js_1.checkoutBranch)(branch, repoRoot);
|
|
56
|
+
// Ensure ticket is active
|
|
57
|
+
if (ticket.state !== "active") {
|
|
58
|
+
const params = { actor };
|
|
59
|
+
try {
|
|
60
|
+
const updated = (0, state_js_1.applyTransition)(ticket, "active", params, repoRoot);
|
|
61
|
+
(0, ticket_js_1.writeTicket)(repoRoot, updated);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// if transition fails (e.g. already done), just proceed
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
let ahead;
|
|
68
|
+
try {
|
|
69
|
+
ahead = (0, git_js_1.getCommitsAhead)(branch, defaultBranch, repoRoot);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
ahead = 0;
|
|
73
|
+
}
|
|
74
|
+
console.log(`Resumed branch ${branch} — ticket ${ticket.id} is active. Branch has ${ahead} commits ahead of ${defaultBranch}.`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// New branch
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
const params = {
|
|
80
|
+
actor,
|
|
81
|
+
};
|
|
82
|
+
let updated;
|
|
83
|
+
try {
|
|
84
|
+
updated = (0, state_js_1.applyTransition)(ticket, "active", params, repoRoot);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error(`Error: ${err.message}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
// Populate work block
|
|
91
|
+
updated.work = {
|
|
92
|
+
branch,
|
|
93
|
+
base_branch: defaultBranch,
|
|
94
|
+
started_at: now,
|
|
95
|
+
started_by: actor,
|
|
96
|
+
};
|
|
97
|
+
updated.updated_at = now;
|
|
98
|
+
(0, ticket_js_1.writeTicket)(repoRoot, updated);
|
|
99
|
+
(0, git_js_1.createBranch)(branch, repoRoot);
|
|
100
|
+
console.log(`Created branch ${branch} — ticket ${ticket.id} is now active.`);
|
|
101
|
+
}
|
|
102
|
+
// Check depends_on: warn if dep commit not ancestor of HEAD
|
|
103
|
+
const deps = ticket.relationships?.depends_on ?? [];
|
|
104
|
+
for (const depId of deps) {
|
|
105
|
+
try {
|
|
106
|
+
const dep = (0, ticket_js_1.readTicket)(repoRoot, depId);
|
|
107
|
+
if (dep.resolution?.commit) {
|
|
108
|
+
const depCommit = dep.resolution.commit;
|
|
109
|
+
try {
|
|
110
|
+
if (!(0, git_js_1.isAncestor)(depCommit, "HEAD", repoRoot)) {
|
|
111
|
+
console.error(`Warning: dependency ${depId} resolved at ${depCommit} is not an ancestor of HEAD`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// ignore ancestor check failures
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// ignore dep lookup failures
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
(0, errors_js_1.handleError)(err);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Config } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_CONFIG: Config;
|
|
3
|
+
export declare function loadConfig(repoRoot: string): Config;
|
|
4
|
+
export declare function getTodoDir(repoRoot: string): string;
|
|
5
|
+
export declare function ensureTodoDir(repoRoot: string): void;
|
|
6
|
+
export declare function getIdLength(config: Config): number;
|
|
7
|
+
export declare function getCommitPrefix(config: Config): string;
|