issy 0.8.0 → 0.9.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.
- package/README.md +17 -2
- package/dist/cli.js +118 -10
- package/dist/main.js +57 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/miketromba/issy/main/assets/issy-
|
|
2
|
+
<img src="https://raw.githubusercontent.com/miketromba/issy/main/assets/issy-banner.png" alt="issy" width="600" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<strong>AI-native issue tracking.</strong><br>
|
|
13
|
-
Tell your coding assistant what to track. It handles the rest
|
|
13
|
+
Tell your coding assistant what to track. It handles the rest.<br>
|
|
14
|
+
<a href="https://issy.sh">issy.sh</a>
|
|
14
15
|
</p>
|
|
15
16
|
|
|
16
17
|
---
|
|
@@ -77,6 +78,7 @@ Once installed globally, you can run commands from your terminal:
|
|
|
77
78
|
```bash
|
|
78
79
|
issy # Start the web UI
|
|
79
80
|
issy list # List open issues (roadmap order)
|
|
81
|
+
issy list --unblocked # List open issues with no open blockers
|
|
80
82
|
issy next # Show next issue to work on
|
|
81
83
|
issy create --title "Bug" # Create an issue
|
|
82
84
|
```
|
|
@@ -123,12 +125,15 @@ Opens a local read-only UI at `http://localhost:1554` for browsing issues.
|
|
|
123
125
|
issy init # Create .issy/issues/ directory
|
|
124
126
|
issy init --seed # Create with a welcome issue
|
|
125
127
|
issy list # List open issues (roadmap order)
|
|
128
|
+
issy list --unblocked # List open issues with no open blockers
|
|
126
129
|
issy next # Show next issue to work on
|
|
127
130
|
issy search "auth" # Fuzzy search
|
|
128
131
|
issy read 0001 # View issue
|
|
129
132
|
issy create --title "Bug" --after 0002 # Create issue after #0002
|
|
133
|
+
issy create --title "Feature" --depends-on 0012,0035 --last # Blocked by issues
|
|
130
134
|
issy create --title "Bug" --body "Details here" --last # Create with body content
|
|
131
135
|
issy update 0001 --before 0003 # Reposition in roadmap
|
|
136
|
+
issy update 0001 --depends-on 0012,0035 # Replace blockers
|
|
132
137
|
issy update 0001 --body "New details" # Replace body content
|
|
133
138
|
issy close 0001 # Close issue
|
|
134
139
|
issy reopen 0001 --after 0004 # Reopen and place in roadmap
|
|
@@ -159,6 +164,14 @@ issy list --sort priority # Sort by priority instead
|
|
|
159
164
|
issy list --sort created # Sort by creation date
|
|
160
165
|
```
|
|
161
166
|
|
|
167
|
+
The `Blk` column in `issy list` shows the count of currently open blockers. `-` means the issue is unblocked. Use `issy list --unblocked` to show only open issues that have no open blockers. Dependency IDs that do not match an existing issue are ignored.
|
|
168
|
+
|
|
169
|
+
Issues can declare blockers with `depends_on`:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
depends_on: 0012, 0035
|
|
173
|
+
```
|
|
174
|
+
|
|
162
175
|
### Hooks
|
|
163
176
|
|
|
164
177
|
issy supports optional hook files in `.issy/` that inject context into stdout after successful operations. The file contents are printed directly, making them visible to AI agents in their command output.
|
|
@@ -210,6 +223,7 @@ scope: medium
|
|
|
210
223
|
type: bug
|
|
211
224
|
status: open
|
|
212
225
|
order: a0
|
|
226
|
+
depends_on: 0012, 0035
|
|
213
227
|
created: 2025-01-15T10:30:00
|
|
214
228
|
---
|
|
215
229
|
|
|
@@ -227,6 +241,7 @@ session isn't established, causing a redirect loop.
|
|
|
227
241
|
| `status` | `open`, `closed` |
|
|
228
242
|
| `labels` | comma-separated (optional) |
|
|
229
243
|
| `order` | fractional index key (managed by issy) |
|
|
244
|
+
| `depends_on` | comma-separated blocking issue IDs (optional) |
|
|
230
245
|
|
|
231
246
|
## Configuration
|
|
232
247
|
|
package/dist/cli.js
CHANGED
|
@@ -355,6 +355,9 @@ function generateFrontmatter(data) {
|
|
|
355
355
|
if (data.order) {
|
|
356
356
|
lines.push(`order: ${data.order}`);
|
|
357
357
|
}
|
|
358
|
+
if (data.depends_on) {
|
|
359
|
+
lines.push(`depends_on: ${data.depends_on}`);
|
|
360
|
+
}
|
|
358
361
|
lines.push(`created: ${data.created}`);
|
|
359
362
|
if (data.updated) {
|
|
360
363
|
lines.push(`updated: ${data.updated}`);
|
|
@@ -363,6 +366,44 @@ function generateFrontmatter(data) {
|
|
|
363
366
|
return lines.join(`
|
|
364
367
|
`);
|
|
365
368
|
}
|
|
369
|
+
function parseDependencyIds(dependsOn) {
|
|
370
|
+
if (!dependsOn)
|
|
371
|
+
return [];
|
|
372
|
+
const seen = new Set;
|
|
373
|
+
const ids = [];
|
|
374
|
+
for (const token of dependsOn.split(/[,\s]+/)) {
|
|
375
|
+
const value = token.trim().replace(/^['"]+|['"]+$/g, "").replace(/^#/, "");
|
|
376
|
+
if (!/^\d+$/.test(value))
|
|
377
|
+
continue;
|
|
378
|
+
const id = value.padStart(4, "0");
|
|
379
|
+
if (seen.has(id))
|
|
380
|
+
continue;
|
|
381
|
+
seen.add(id);
|
|
382
|
+
ids.push(id);
|
|
383
|
+
}
|
|
384
|
+
return ids;
|
|
385
|
+
}
|
|
386
|
+
function filterExistingDependencyIds(dependsOn, issues, excludeId) {
|
|
387
|
+
const existingIds = new Set(issues.map((issue) => issue.id));
|
|
388
|
+
const excludedId = excludeId?.padStart(4, "0");
|
|
389
|
+
return parseDependencyIds(dependsOn).filter((id) => existingIds.has(id) && id !== excludedId);
|
|
390
|
+
}
|
|
391
|
+
function formatExistingDependencyIds(dependsOn, issues, excludeId) {
|
|
392
|
+
return filterExistingDependencyIds(dependsOn, issues, excludeId).join(", ");
|
|
393
|
+
}
|
|
394
|
+
function getDependencyIssues(issue, issues) {
|
|
395
|
+
const dependencyIds = filterExistingDependencyIds(issue.frontmatter.depends_on, issues, issue.id);
|
|
396
|
+
if (dependencyIds.length === 0)
|
|
397
|
+
return [];
|
|
398
|
+
const issueById = new Map(issues.map((i) => [i.id, i]));
|
|
399
|
+
return dependencyIds.map((id) => issueById.get(id)).filter((dependency) => Boolean(dependency));
|
|
400
|
+
}
|
|
401
|
+
function getBlockingIssues(issue, issues) {
|
|
402
|
+
return getDependencyIssues(issue, issues).filter((dependency) => dependency.frontmatter.status === "open");
|
|
403
|
+
}
|
|
404
|
+
function isIssueUnblocked(issue, issues) {
|
|
405
|
+
return issue.frontmatter.status === "open" && getBlockingIssues(issue, issues).length === 0;
|
|
406
|
+
}
|
|
366
407
|
function getIssueIdFromFilename(filename) {
|
|
367
408
|
const match = filename.match(/^(\d+)-/);
|
|
368
409
|
return match ? match[1] : filename.replace(".md", "");
|
|
@@ -496,6 +537,8 @@ async function createIssue(input) {
|
|
|
496
537
|
const issueNumber = await getNextIssueNumber();
|
|
497
538
|
const slug = createSlug(input.title);
|
|
498
539
|
const filename = `${issueNumber}-${slug}.md`;
|
|
540
|
+
const existingIssues = await getAllIssues();
|
|
541
|
+
const dependsOn = formatExistingDependencyIds(input.depends_on, existingIssues, issueNumber);
|
|
499
542
|
const frontmatter = {
|
|
500
543
|
title: input.title,
|
|
501
544
|
priority,
|
|
@@ -504,6 +547,7 @@ async function createIssue(input) {
|
|
|
504
547
|
labels: input.labels || undefined,
|
|
505
548
|
status: "open",
|
|
506
549
|
order: input.order || undefined,
|
|
550
|
+
depends_on: dependsOn || undefined,
|
|
507
551
|
created: formatDate()
|
|
508
552
|
};
|
|
509
553
|
const body = input.body ?? `
|
|
@@ -529,6 +573,8 @@ async function updateIssue(id, input) {
|
|
|
529
573
|
if (!issue) {
|
|
530
574
|
throw new Error(`Issue not found: ${id}`);
|
|
531
575
|
}
|
|
576
|
+
const allIssues = await getAllIssues();
|
|
577
|
+
const dependsOn = input.depends_on !== undefined ? formatExistingDependencyIds(input.depends_on, allIssues, issue.id) : undefined;
|
|
532
578
|
const updatedFrontmatter = {
|
|
533
579
|
...issue.frontmatter,
|
|
534
580
|
...input.title && { title: input.title },
|
|
@@ -540,6 +586,9 @@ async function updateIssue(id, input) {
|
|
|
540
586
|
},
|
|
541
587
|
...input.status && { status: input.status },
|
|
542
588
|
...input.order && { order: input.order },
|
|
589
|
+
...input.depends_on !== undefined && {
|
|
590
|
+
depends_on: dependsOn || undefined
|
|
591
|
+
},
|
|
543
592
|
updated: formatDate()
|
|
544
593
|
};
|
|
545
594
|
const updatedContent = input.body !== undefined ? `
|
|
@@ -2039,6 +2088,14 @@ function filterByQuery(issues, query) {
|
|
|
2039
2088
|
if (issue.frontmatter.status !== statusValue) {
|
|
2040
2089
|
return false;
|
|
2041
2090
|
}
|
|
2091
|
+
} else if (statusValue === "unblocked") {
|
|
2092
|
+
if (!isIssueUnblocked(issue, issues)) {
|
|
2093
|
+
return false;
|
|
2094
|
+
}
|
|
2095
|
+
} else if (statusValue === "blocked") {
|
|
2096
|
+
if (issue.frontmatter.status !== "open" || isIssueUnblocked(issue, issues)) {
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2042
2099
|
}
|
|
2043
2100
|
}
|
|
2044
2101
|
if (parsed.qualifiers.priority) {
|
|
@@ -2126,7 +2183,23 @@ function typeSymbol(type) {
|
|
|
2126
2183
|
}
|
|
2127
2184
|
function formatIssueRow(issue) {
|
|
2128
2185
|
const status = issue.frontmatter.status === "open" ? "OPEN " : "CLOSED";
|
|
2129
|
-
|
|
2186
|
+
const blocked = issue.blockers > 0 ? String(issue.blockers) : "-";
|
|
2187
|
+
return ` ${issue.id} ${prioritySymbol(issue.frontmatter.priority)} ${typeSymbol(issue.frontmatter.type)} ${status} ${blocked.padEnd(3)} ${issue.frontmatter.title}`;
|
|
2188
|
+
}
|
|
2189
|
+
function printDependsOn(issue, issues) {
|
|
2190
|
+
const dependencyIssues = getDependencyIssues(issue, issues);
|
|
2191
|
+
if (dependencyIssues.length === 0)
|
|
2192
|
+
return;
|
|
2193
|
+
console.log(` Depends on: ${dependencyIssues.map((i) => `#${i.id}`).join(", ")}`);
|
|
2194
|
+
}
|
|
2195
|
+
function getDependsOnArg(values) {
|
|
2196
|
+
const value = values["depends-on"];
|
|
2197
|
+
return typeof value === "string" ? value : undefined;
|
|
2198
|
+
}
|
|
2199
|
+
async function resolveDependsOn(value, excludeId) {
|
|
2200
|
+
if (value === undefined)
|
|
2201
|
+
return;
|
|
2202
|
+
return formatExistingDependencyIds(value, await getAllIssues(), excludeId);
|
|
2130
2203
|
}
|
|
2131
2204
|
async function resolvePosition(opts) {
|
|
2132
2205
|
const openIssues = await getOpenIssuesByOrder();
|
|
@@ -2158,7 +2231,7 @@ function hasHelpFlag(commandArgs) {
|
|
|
2158
2231
|
async function listIssues(options) {
|
|
2159
2232
|
const allIssues = await getAllIssues();
|
|
2160
2233
|
const queryParts = [];
|
|
2161
|
-
if (!options.all)
|
|
2234
|
+
if (!options.all && !options.unblocked)
|
|
2162
2235
|
queryParts.push("is:open");
|
|
2163
2236
|
if (options.priority)
|
|
2164
2237
|
queryParts.push(`priority:${options.priority}`);
|
|
@@ -2170,17 +2243,23 @@ async function listIssues(options) {
|
|
|
2170
2243
|
queryParts.push(`sort:${options.sort}`);
|
|
2171
2244
|
if (options.search)
|
|
2172
2245
|
queryParts.push(options.search);
|
|
2173
|
-
const query = queryParts.join(" ")
|
|
2174
|
-
|
|
2246
|
+
const query = queryParts.join(" ");
|
|
2247
|
+
let issues = query ? filterByQuery(allIssues, query) : allIssues;
|
|
2248
|
+
if (options.unblocked) {
|
|
2249
|
+
issues = issues.filter((issue) => isIssueUnblocked(issue, allIssues));
|
|
2250
|
+
}
|
|
2175
2251
|
if (issues.length === 0) {
|
|
2176
2252
|
console.log("No issues found.");
|
|
2177
2253
|
return;
|
|
2178
2254
|
}
|
|
2179
2255
|
console.log(`
|
|
2180
|
-
ID Pri Type Status
|
|
2256
|
+
ID Pri Type Status Blk Title`);
|
|
2181
2257
|
console.log(` ${"-".repeat(100)}`);
|
|
2182
2258
|
for (const issue of issues) {
|
|
2183
|
-
console.log(formatIssueRow(
|
|
2259
|
+
console.log(formatIssueRow({
|
|
2260
|
+
...issue,
|
|
2261
|
+
blockers: getBlockingIssues(issue, allIssues).length
|
|
2262
|
+
}));
|
|
2184
2263
|
}
|
|
2185
2264
|
console.log(`
|
|
2186
2265
|
Total: ${issues.length} issue(s)
|
|
@@ -2188,6 +2267,7 @@ async function listIssues(options) {
|
|
|
2188
2267
|
}
|
|
2189
2268
|
async function readIssue(id) {
|
|
2190
2269
|
const issue = await getIssue(id);
|
|
2270
|
+
const allIssues = await getAllIssues();
|
|
2191
2271
|
if (!issue) {
|
|
2192
2272
|
console.error(`Issue not found: ${id}`);
|
|
2193
2273
|
process.exit(1);
|
|
@@ -2206,6 +2286,7 @@ ${"=".repeat(70)}`);
|
|
|
2206
2286
|
if (issue.frontmatter.labels) {
|
|
2207
2287
|
console.log(` Labels: ${issue.frontmatter.labels}`);
|
|
2208
2288
|
}
|
|
2289
|
+
printDependsOn(issue, allIssues);
|
|
2209
2290
|
if (issue.frontmatter.order) {
|
|
2210
2291
|
console.log(` Order: ${issue.frontmatter.order}`);
|
|
2211
2292
|
}
|
|
@@ -2228,10 +2309,13 @@ async function searchIssuesCommand(query, options) {
|
|
|
2228
2309
|
console.log(`
|
|
2229
2310
|
Search results for "${query}":`);
|
|
2230
2311
|
console.log(`
|
|
2231
|
-
ID Pri Type Status
|
|
2312
|
+
ID Pri Type Status Blk Title`);
|
|
2232
2313
|
console.log(` ${"-".repeat(100)}`);
|
|
2233
2314
|
for (const issue of issues) {
|
|
2234
|
-
console.log(formatIssueRow(
|
|
2315
|
+
console.log(formatIssueRow({
|
|
2316
|
+
...issue,
|
|
2317
|
+
blockers: getBlockingIssues(issue, allIssues).length
|
|
2318
|
+
}));
|
|
2235
2319
|
}
|
|
2236
2320
|
console.log(`
|
|
2237
2321
|
Found: ${issues.length} issue(s)
|
|
@@ -2257,6 +2341,7 @@ async function createIssueCommand(options) {
|
|
|
2257
2341
|
scope: options.scope,
|
|
2258
2342
|
type: options.type,
|
|
2259
2343
|
labels: options.labels,
|
|
2344
|
+
depends_on: await resolveDependsOn(options.dependsOn) || undefined,
|
|
2260
2345
|
order
|
|
2261
2346
|
};
|
|
2262
2347
|
const issue = await createIssue(input);
|
|
@@ -2293,6 +2378,9 @@ async function updateIssueCommand(id, options) {
|
|
|
2293
2378
|
scope: options.scope,
|
|
2294
2379
|
type: options.type,
|
|
2295
2380
|
labels: options.labels,
|
|
2381
|
+
...options.dependsOn !== undefined && {
|
|
2382
|
+
depends_on: await resolveDependsOn(options.dependsOn, id) || ""
|
|
2383
|
+
},
|
|
2296
2384
|
order
|
|
2297
2385
|
});
|
|
2298
2386
|
console.log(`Updated issue: ${issue.filename}`);
|
|
@@ -2341,6 +2429,7 @@ async function reopenIssueCommand(id, options) {
|
|
|
2341
2429
|
}
|
|
2342
2430
|
async function nextIssueCommand() {
|
|
2343
2431
|
const issue = await getNextIssue();
|
|
2432
|
+
const allIssues = await getAllIssues();
|
|
2344
2433
|
if (!issue) {
|
|
2345
2434
|
console.log("No open issues.");
|
|
2346
2435
|
return;
|
|
@@ -2359,6 +2448,7 @@ ${"=".repeat(70)}`);
|
|
|
2359
2448
|
if (issue.frontmatter.labels) {
|
|
2360
2449
|
console.log(` Labels: ${issue.frontmatter.labels}`);
|
|
2361
2450
|
}
|
|
2451
|
+
printDependsOn(issue, allIssues);
|
|
2362
2452
|
if (issue.frontmatter.order) {
|
|
2363
2453
|
console.log(` Order: ${issue.frontmatter.order}`);
|
|
2364
2454
|
}
|
|
@@ -2386,6 +2476,7 @@ Options:
|
|
|
2386
2476
|
Commands:
|
|
2387
2477
|
list List all open issues (roadmap order)
|
|
2388
2478
|
--all, -a Include closed issues
|
|
2479
|
+
--unblocked Only show open issues with no open blockers
|
|
2389
2480
|
--priority, -p <p> Filter by priority (high, medium, low)
|
|
2390
2481
|
--scope <s> Filter by scope (small, medium, large)
|
|
2391
2482
|
--type, -t <t> Filter by type (bug, improvement)
|
|
@@ -2406,6 +2497,7 @@ Commands:
|
|
|
2406
2497
|
--scope <s> Scope (small, medium, large)
|
|
2407
2498
|
--type <t> Type (bug, improvement)
|
|
2408
2499
|
--labels, -l <l> Comma-separated labels
|
|
2500
|
+
--depends-on <ids> Comma-separated blocking issue IDs
|
|
2409
2501
|
--before <id> Insert before this issue in roadmap
|
|
2410
2502
|
--after <id> Insert after this issue in roadmap
|
|
2411
2503
|
--first Insert at the beginning of the roadmap
|
|
@@ -2418,6 +2510,7 @@ Commands:
|
|
|
2418
2510
|
--scope <s> New scope
|
|
2419
2511
|
--type <t> New type
|
|
2420
2512
|
--labels, -l <l> New labels
|
|
2513
|
+
--depends-on <ids> Replace blocking issue IDs (empty clears)
|
|
2421
2514
|
--before <id> Move before this issue in roadmap
|
|
2422
2515
|
--after <id> Move after this issue in roadmap
|
|
2423
2516
|
--first Move to the beginning of the roadmap
|
|
@@ -2435,6 +2528,7 @@ Commands:
|
|
|
2435
2528
|
|
|
2436
2529
|
Examples:
|
|
2437
2530
|
issy list
|
|
2531
|
+
issy list --unblocked
|
|
2438
2532
|
issy list --priority high --type bug
|
|
2439
2533
|
issy next
|
|
2440
2534
|
issy read 0001
|
|
@@ -2456,6 +2550,7 @@ List all open issues in roadmap order.
|
|
|
2456
2550
|
|
|
2457
2551
|
Options:
|
|
2458
2552
|
--all, -a Include closed issues
|
|
2553
|
+
--unblocked Only show open issues with no open blockers
|
|
2459
2554
|
--priority, -p <p> Filter by priority (high, medium, low)
|
|
2460
2555
|
--scope <s> Filter by scope (small, medium, large)
|
|
2461
2556
|
--type, -t <t> Filter by type (bug, improvement)
|
|
@@ -2468,6 +2563,7 @@ Options:
|
|
|
2468
2563
|
args: args.slice(1),
|
|
2469
2564
|
options: {
|
|
2470
2565
|
all: { type: "boolean", short: "a" },
|
|
2566
|
+
unblocked: { type: "boolean" },
|
|
2471
2567
|
priority: { type: "string", short: "p" },
|
|
2472
2568
|
scope: { type: "string" },
|
|
2473
2569
|
type: { type: "string", short: "t" },
|
|
@@ -2545,6 +2641,7 @@ Options:
|
|
|
2545
2641
|
--scope <s> Scope: small, medium, large
|
|
2546
2642
|
--type <t> Type: bug, improvement (default: improvement)
|
|
2547
2643
|
--labels, -l <l> Comma-separated labels
|
|
2644
|
+
--depends-on <ids> Comma-separated blocking issue IDs
|
|
2548
2645
|
--before <id> Insert before this issue in roadmap
|
|
2549
2646
|
--after <id> Insert after this issue in roadmap
|
|
2550
2647
|
--first Insert at the beginning of the roadmap
|
|
@@ -2552,6 +2649,7 @@ Options:
|
|
|
2552
2649
|
|
|
2553
2650
|
Examples:
|
|
2554
2651
|
issy create --title "Fix login bug" --type bug --priority high --after 0002
|
|
2652
|
+
issy create --title "Add export" --depends-on 0001,0002 --last
|
|
2555
2653
|
issy create --title "Add dark mode" --last
|
|
2556
2654
|
`);
|
|
2557
2655
|
return;
|
|
@@ -2565,6 +2663,7 @@ Examples:
|
|
|
2565
2663
|
scope: { type: "string" },
|
|
2566
2664
|
type: { type: "string" },
|
|
2567
2665
|
labels: { type: "string", short: "l" },
|
|
2666
|
+
"depends-on": { type: "string" },
|
|
2568
2667
|
before: { type: "string" },
|
|
2569
2668
|
after: { type: "string" },
|
|
2570
2669
|
first: { type: "boolean" },
|
|
@@ -2572,7 +2671,10 @@ Examples:
|
|
|
2572
2671
|
},
|
|
2573
2672
|
allowPositionals: true
|
|
2574
2673
|
});
|
|
2575
|
-
await createIssueCommand(
|
|
2674
|
+
await createIssueCommand({
|
|
2675
|
+
...values,
|
|
2676
|
+
dependsOn: getDependsOnArg(values)
|
|
2677
|
+
});
|
|
2576
2678
|
break;
|
|
2577
2679
|
}
|
|
2578
2680
|
case "update": {
|
|
@@ -2588,6 +2690,7 @@ Options:
|
|
|
2588
2690
|
--scope <s> New scope: small, medium, large
|
|
2589
2691
|
--type <t> New type: bug, improvement
|
|
2590
2692
|
--labels, -l <l> New labels (comma-separated)
|
|
2693
|
+
--depends-on <ids> Replace blocking issue IDs (empty clears)
|
|
2591
2694
|
--before <id> Move before this issue in roadmap
|
|
2592
2695
|
--after <id> Move after this issue in roadmap
|
|
2593
2696
|
--first Move to the beginning of the roadmap
|
|
@@ -2595,6 +2698,7 @@ Options:
|
|
|
2595
2698
|
|
|
2596
2699
|
Examples:
|
|
2597
2700
|
issy update 0001 --priority low --after 0003
|
|
2701
|
+
issy update 0002 --depends-on 0001,0003
|
|
2598
2702
|
issy update 0002 --title "Renamed issue" --first
|
|
2599
2703
|
`);
|
|
2600
2704
|
return;
|
|
@@ -2613,6 +2717,7 @@ Examples:
|
|
|
2613
2717
|
scope: { type: "string" },
|
|
2614
2718
|
type: { type: "string" },
|
|
2615
2719
|
labels: { type: "string", short: "l" },
|
|
2720
|
+
"depends-on": { type: "string" },
|
|
2616
2721
|
before: { type: "string" },
|
|
2617
2722
|
after: { type: "string" },
|
|
2618
2723
|
first: { type: "boolean" },
|
|
@@ -2620,7 +2725,10 @@ Examples:
|
|
|
2620
2725
|
},
|
|
2621
2726
|
allowPositionals: true
|
|
2622
2727
|
});
|
|
2623
|
-
await updateIssueCommand(id,
|
|
2728
|
+
await updateIssueCommand(id, {
|
|
2729
|
+
...values,
|
|
2730
|
+
dependsOn: getDependsOnArg(values)
|
|
2731
|
+
});
|
|
2624
2732
|
break;
|
|
2625
2733
|
}
|
|
2626
2734
|
case "close": {
|
package/dist/main.js
CHANGED
|
@@ -366,6 +366,9 @@ function generateFrontmatter(data) {
|
|
|
366
366
|
if (data.order) {
|
|
367
367
|
lines.push(`order: ${data.order}`);
|
|
368
368
|
}
|
|
369
|
+
if (data.depends_on) {
|
|
370
|
+
lines.push(`depends_on: ${data.depends_on}`);
|
|
371
|
+
}
|
|
369
372
|
lines.push(`created: ${data.created}`);
|
|
370
373
|
if (data.updated) {
|
|
371
374
|
lines.push(`updated: ${data.updated}`);
|
|
@@ -374,6 +377,44 @@ function generateFrontmatter(data) {
|
|
|
374
377
|
return lines.join(`
|
|
375
378
|
`);
|
|
376
379
|
}
|
|
380
|
+
function parseDependencyIds(dependsOn) {
|
|
381
|
+
if (!dependsOn)
|
|
382
|
+
return [];
|
|
383
|
+
const seen = new Set;
|
|
384
|
+
const ids = [];
|
|
385
|
+
for (const token of dependsOn.split(/[,\s]+/)) {
|
|
386
|
+
const value = token.trim().replace(/^['"]+|['"]+$/g, "").replace(/^#/, "");
|
|
387
|
+
if (!/^\d+$/.test(value))
|
|
388
|
+
continue;
|
|
389
|
+
const id = value.padStart(4, "0");
|
|
390
|
+
if (seen.has(id))
|
|
391
|
+
continue;
|
|
392
|
+
seen.add(id);
|
|
393
|
+
ids.push(id);
|
|
394
|
+
}
|
|
395
|
+
return ids;
|
|
396
|
+
}
|
|
397
|
+
function filterExistingDependencyIds(dependsOn, issues, excludeId) {
|
|
398
|
+
const existingIds = new Set(issues.map((issue) => issue.id));
|
|
399
|
+
const excludedId = excludeId?.padStart(4, "0");
|
|
400
|
+
return parseDependencyIds(dependsOn).filter((id) => existingIds.has(id) && id !== excludedId);
|
|
401
|
+
}
|
|
402
|
+
function formatExistingDependencyIds(dependsOn, issues, excludeId) {
|
|
403
|
+
return filterExistingDependencyIds(dependsOn, issues, excludeId).join(", ");
|
|
404
|
+
}
|
|
405
|
+
function getDependencyIssues(issue, issues) {
|
|
406
|
+
const dependencyIds = filterExistingDependencyIds(issue.frontmatter.depends_on, issues, issue.id);
|
|
407
|
+
if (dependencyIds.length === 0)
|
|
408
|
+
return [];
|
|
409
|
+
const issueById = new Map(issues.map((i) => [i.id, i]));
|
|
410
|
+
return dependencyIds.map((id) => issueById.get(id)).filter((dependency) => Boolean(dependency));
|
|
411
|
+
}
|
|
412
|
+
function getBlockingIssues(issue, issues) {
|
|
413
|
+
return getDependencyIssues(issue, issues).filter((dependency) => dependency.frontmatter.status === "open");
|
|
414
|
+
}
|
|
415
|
+
function isIssueUnblocked(issue, issues) {
|
|
416
|
+
return issue.frontmatter.status === "open" && getBlockingIssues(issue, issues).length === 0;
|
|
417
|
+
}
|
|
377
418
|
function getIssueIdFromFilename(filename) {
|
|
378
419
|
const match = filename.match(/^(\d+)-/);
|
|
379
420
|
return match ? match[1] : filename.replace(".md", "");
|
|
@@ -507,6 +548,8 @@ async function createIssue(input) {
|
|
|
507
548
|
const issueNumber = await getNextIssueNumber();
|
|
508
549
|
const slug = createSlug(input.title);
|
|
509
550
|
const filename = `${issueNumber}-${slug}.md`;
|
|
551
|
+
const existingIssues = await getAllIssues();
|
|
552
|
+
const dependsOn = formatExistingDependencyIds(input.depends_on, existingIssues, issueNumber);
|
|
510
553
|
const frontmatter = {
|
|
511
554
|
title: input.title,
|
|
512
555
|
priority,
|
|
@@ -515,6 +558,7 @@ async function createIssue(input) {
|
|
|
515
558
|
labels: input.labels || undefined,
|
|
516
559
|
status: "open",
|
|
517
560
|
order: input.order || undefined,
|
|
561
|
+
depends_on: dependsOn || undefined,
|
|
518
562
|
created: formatDate()
|
|
519
563
|
};
|
|
520
564
|
const body = input.body ?? `
|
|
@@ -540,6 +584,8 @@ async function updateIssue(id, input) {
|
|
|
540
584
|
if (!issue) {
|
|
541
585
|
throw new Error(`Issue not found: ${id}`);
|
|
542
586
|
}
|
|
587
|
+
const allIssues = await getAllIssues();
|
|
588
|
+
const dependsOn = input.depends_on !== undefined ? formatExistingDependencyIds(input.depends_on, allIssues, issue.id) : undefined;
|
|
543
589
|
const updatedFrontmatter = {
|
|
544
590
|
...issue.frontmatter,
|
|
545
591
|
...input.title && { title: input.title },
|
|
@@ -551,6 +597,9 @@ async function updateIssue(id, input) {
|
|
|
551
597
|
},
|
|
552
598
|
...input.status && { status: input.status },
|
|
553
599
|
...input.order && { order: input.order },
|
|
600
|
+
...input.depends_on !== undefined && {
|
|
601
|
+
depends_on: dependsOn || undefined
|
|
602
|
+
},
|
|
554
603
|
updated: formatDate()
|
|
555
604
|
};
|
|
556
605
|
const updatedContent = input.body !== undefined ? `
|
|
@@ -2050,6 +2099,14 @@ function filterByQuery(issues, query) {
|
|
|
2050
2099
|
if (issue.frontmatter.status !== statusValue) {
|
|
2051
2100
|
return false;
|
|
2052
2101
|
}
|
|
2102
|
+
} else if (statusValue === "unblocked") {
|
|
2103
|
+
if (!isIssueUnblocked(issue, issues)) {
|
|
2104
|
+
return false;
|
|
2105
|
+
}
|
|
2106
|
+
} else if (statusValue === "blocked") {
|
|
2107
|
+
if (issue.frontmatter.status !== "open" || isIssueUnblocked(issue, issues)) {
|
|
2108
|
+
return false;
|
|
2109
|
+
}
|
|
2053
2110
|
}
|
|
2054
2111
|
}
|
|
2055
2112
|
if (parsed.qualifiers.priority) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "issy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "AI-native issue tracking. Markdown files in .issues/, managed by your coding assistant.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"lint": "biome check src bin"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@miketromba/issy-app": "^0.
|
|
39
|
-
"@miketromba/issy-core": "^0.
|
|
38
|
+
"@miketromba/issy-app": "^0.9.0",
|
|
39
|
+
"@miketromba/issy-core": "^0.9.0",
|
|
40
40
|
"update-notifier": "^7.3.1"
|
|
41
41
|
}
|
|
42
42
|
}
|