issy 0.10.0 → 0.10.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/README.md +14 -0
- package/dist/cli.js +118 -10
- package/dist/main.js +81 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ Once installed globally, you can run commands from your terminal:
|
|
|
90
90
|
```bash
|
|
91
91
|
issy # Start the web UI
|
|
92
92
|
issy list # List open issues (roadmap order)
|
|
93
|
+
issy list --unblocked # List open issues with no open blockers
|
|
93
94
|
issy next # Show next issue to work on
|
|
94
95
|
issy create --title "Bug" # Create an issue
|
|
95
96
|
issy learn # Print AI-agent instructions
|
|
@@ -137,12 +138,15 @@ Opens a local read-only UI at `http://localhost:1554` for browsing issues.
|
|
|
137
138
|
issy init # Create .issy/issues/ directory
|
|
138
139
|
issy init --seed # Create with a welcome issue
|
|
139
140
|
issy list # List open issues (roadmap order)
|
|
141
|
+
issy list --unblocked # List open issues with no open blockers
|
|
140
142
|
issy next # Show next issue to work on
|
|
141
143
|
issy search "auth" # Fuzzy search
|
|
142
144
|
issy read 0001 # View issue
|
|
143
145
|
issy create --title "Bug" --after 0002 # Create issue after #0002
|
|
146
|
+
issy create --title "Feature" --depends-on 0012,0035 --last # Blocked by issues
|
|
144
147
|
issy create --title "Bug" --body "Details here" --last # Create with body content
|
|
145
148
|
issy update 0001 --before 0003 # Reposition in roadmap
|
|
149
|
+
issy update 0001 --depends-on 0012,0035 # Replace blockers
|
|
146
150
|
issy update 0001 --body "New details" # Replace body content
|
|
147
151
|
issy close 0001 # Close issue
|
|
148
152
|
issy reopen 0001 --after 0004 # Reopen and place in roadmap
|
|
@@ -175,6 +179,14 @@ issy list --sort priority # Sort by priority instead
|
|
|
175
179
|
issy list --sort created # Sort by creation date
|
|
176
180
|
```
|
|
177
181
|
|
|
182
|
+
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.
|
|
183
|
+
|
|
184
|
+
Issues can declare blockers with `depends_on`:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
depends_on: 0012, 0035
|
|
188
|
+
```
|
|
189
|
+
|
|
178
190
|
### Hooks
|
|
179
191
|
|
|
180
192
|
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.
|
|
@@ -226,6 +238,7 @@ scope: medium
|
|
|
226
238
|
type: bug
|
|
227
239
|
status: open
|
|
228
240
|
order: a0
|
|
241
|
+
depends_on: 0012, 0035
|
|
229
242
|
created: 2025-01-15T10:30:00
|
|
230
243
|
---
|
|
231
244
|
|
|
@@ -243,6 +256,7 @@ session isn't established, causing a redirect loop.
|
|
|
243
256
|
| `status` | `open`, `closed` |
|
|
244
257
|
| `labels` | comma-separated (optional) |
|
|
245
258
|
| `order` | fractional index key (managed by issy) |
|
|
259
|
+
| `depends_on` | comma-separated blocking issue IDs (optional) |
|
|
246
260
|
|
|
247
261
|
## Configuration
|
|
248
262
|
|
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)
|
|
@@ -2410,6 +2501,7 @@ Commands:
|
|
|
2410
2501
|
--scope <s> Scope (small, medium, large)
|
|
2411
2502
|
--type <t> Type (bug, improvement)
|
|
2412
2503
|
--labels, -l <l> Comma-separated labels
|
|
2504
|
+
--depends-on <ids> Comma-separated blocking issue IDs
|
|
2413
2505
|
--before <id> Insert before this issue in roadmap
|
|
2414
2506
|
--after <id> Insert after this issue in roadmap
|
|
2415
2507
|
--first Insert at the beginning of the roadmap
|
|
@@ -2422,6 +2514,7 @@ Commands:
|
|
|
2422
2514
|
--scope <s> New scope
|
|
2423
2515
|
--type <t> New type
|
|
2424
2516
|
--labels, -l <l> New labels
|
|
2517
|
+
--depends-on <ids> Replace blocking issue IDs (empty clears)
|
|
2425
2518
|
--before <id> Move before this issue in roadmap
|
|
2426
2519
|
--after <id> Move after this issue in roadmap
|
|
2427
2520
|
--first Move to the beginning of the roadmap
|
|
@@ -2439,6 +2532,7 @@ Commands:
|
|
|
2439
2532
|
|
|
2440
2533
|
Examples:
|
|
2441
2534
|
issy list
|
|
2535
|
+
issy list --unblocked
|
|
2442
2536
|
issy list --priority high --type bug
|
|
2443
2537
|
issy next
|
|
2444
2538
|
issy learn
|
|
@@ -2462,6 +2556,7 @@ List all open issues in roadmap order.
|
|
|
2462
2556
|
|
|
2463
2557
|
Options:
|
|
2464
2558
|
--all, -a Include closed issues
|
|
2559
|
+
--unblocked Only show open issues with no open blockers
|
|
2465
2560
|
--priority, -p <p> Filter by priority (high, medium, low)
|
|
2466
2561
|
--scope <s> Filter by scope (small, medium, large)
|
|
2467
2562
|
--type, -t <t> Filter by type (bug, improvement)
|
|
@@ -2474,6 +2569,7 @@ Options:
|
|
|
2474
2569
|
args: args.slice(1),
|
|
2475
2570
|
options: {
|
|
2476
2571
|
all: { type: "boolean", short: "a" },
|
|
2572
|
+
unblocked: { type: "boolean" },
|
|
2477
2573
|
priority: { type: "string", short: "p" },
|
|
2478
2574
|
scope: { type: "string" },
|
|
2479
2575
|
type: { type: "string", short: "t" },
|
|
@@ -2551,6 +2647,7 @@ Options:
|
|
|
2551
2647
|
--scope <s> Scope: small, medium, large
|
|
2552
2648
|
--type <t> Type: bug, improvement (default: improvement)
|
|
2553
2649
|
--labels, -l <l> Comma-separated labels
|
|
2650
|
+
--depends-on <ids> Comma-separated blocking issue IDs
|
|
2554
2651
|
--before <id> Insert before this issue in roadmap
|
|
2555
2652
|
--after <id> Insert after this issue in roadmap
|
|
2556
2653
|
--first Insert at the beginning of the roadmap
|
|
@@ -2558,6 +2655,7 @@ Options:
|
|
|
2558
2655
|
|
|
2559
2656
|
Examples:
|
|
2560
2657
|
issy create --title "Fix login bug" --type bug --priority high --after 0002
|
|
2658
|
+
issy create --title "Add export" --depends-on 0001,0002 --last
|
|
2561
2659
|
issy create --title "Add dark mode" --last
|
|
2562
2660
|
`);
|
|
2563
2661
|
return;
|
|
@@ -2571,6 +2669,7 @@ Examples:
|
|
|
2571
2669
|
scope: { type: "string" },
|
|
2572
2670
|
type: { type: "string" },
|
|
2573
2671
|
labels: { type: "string", short: "l" },
|
|
2672
|
+
"depends-on": { type: "string" },
|
|
2574
2673
|
before: { type: "string" },
|
|
2575
2674
|
after: { type: "string" },
|
|
2576
2675
|
first: { type: "boolean" },
|
|
@@ -2578,7 +2677,10 @@ Examples:
|
|
|
2578
2677
|
},
|
|
2579
2678
|
allowPositionals: true
|
|
2580
2679
|
});
|
|
2581
|
-
await createIssueCommand(
|
|
2680
|
+
await createIssueCommand({
|
|
2681
|
+
...values,
|
|
2682
|
+
dependsOn: getDependsOnArg(values)
|
|
2683
|
+
});
|
|
2582
2684
|
break;
|
|
2583
2685
|
}
|
|
2584
2686
|
case "update": {
|
|
@@ -2594,6 +2696,7 @@ Options:
|
|
|
2594
2696
|
--scope <s> New scope: small, medium, large
|
|
2595
2697
|
--type <t> New type: bug, improvement
|
|
2596
2698
|
--labels, -l <l> New labels (comma-separated)
|
|
2699
|
+
--depends-on <ids> Replace blocking issue IDs (empty clears)
|
|
2597
2700
|
--before <id> Move before this issue in roadmap
|
|
2598
2701
|
--after <id> Move after this issue in roadmap
|
|
2599
2702
|
--first Move to the beginning of the roadmap
|
|
@@ -2601,6 +2704,7 @@ Options:
|
|
|
2601
2704
|
|
|
2602
2705
|
Examples:
|
|
2603
2706
|
issy update 0001 --priority low --after 0003
|
|
2707
|
+
issy update 0002 --depends-on 0001,0003
|
|
2604
2708
|
issy update 0002 --title "Renamed issue" --first
|
|
2605
2709
|
`);
|
|
2606
2710
|
return;
|
|
@@ -2619,6 +2723,7 @@ Examples:
|
|
|
2619
2723
|
scope: { type: "string" },
|
|
2620
2724
|
type: { type: "string" },
|
|
2621
2725
|
labels: { type: "string", short: "l" },
|
|
2726
|
+
"depends-on": { type: "string" },
|
|
2622
2727
|
before: { type: "string" },
|
|
2623
2728
|
after: { type: "string" },
|
|
2624
2729
|
first: { type: "boolean" },
|
|
@@ -2626,7 +2731,10 @@ Examples:
|
|
|
2626
2731
|
},
|
|
2627
2732
|
allowPositionals: true
|
|
2628
2733
|
});
|
|
2629
|
-
await updateIssueCommand(id,
|
|
2734
|
+
await updateIssueCommand(id, {
|
|
2735
|
+
...values,
|
|
2736
|
+
dependsOn: getDependsOnArg(values)
|
|
2737
|
+
});
|
|
2630
2738
|
break;
|
|
2631
2739
|
}
|
|
2632
2740
|
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) {
|
|
@@ -2137,12 +2194,13 @@ var compact = markdown([
|
|
|
2137
2194
|
"",
|
|
2138
2195
|
"## Essential workflow",
|
|
2139
2196
|
"",
|
|
2140
|
-
"- List work: `issy list` or `issy list --
|
|
2197
|
+
"- List work: `issy list`, `issy list --all`, or `issy list --unblocked`.",
|
|
2141
2198
|
'- Search work: `issy search "query"` or `issy search "query" --all`.',
|
|
2142
2199
|
"- Read before changing: `issy read <id>`.",
|
|
2143
2200
|
"- Pick next work: `issy next`.",
|
|
2144
2201
|
'- Create work: `issy create --title "..." --type bug|improvement --priority high|medium|low <position>`.',
|
|
2145
|
-
|
|
2202
|
+
'- Create blocked work: `issy create --title "..." --depends-on 0001,0002 <position>`.',
|
|
2203
|
+
"- Update work: `issy update <id> [options]`, including `--depends-on <ids>` to replace blockers.",
|
|
2146
2204
|
"- Close work: `issy close <id>` after the work is complete and verified.",
|
|
2147
2205
|
"- Reopen work: `issy reopen <id> <position>`.",
|
|
2148
2206
|
"",
|
|
@@ -2159,6 +2217,9 @@ var compact = markdown([
|
|
|
2159
2217
|
"- When creating an issue and open issues already exist, include exactly one position flag: `--before <id>`, `--after <id>`, `--first`, or `--last`.",
|
|
2160
2218
|
"- When reopening an issue and other open issues exist, include exactly one position flag.",
|
|
2161
2219
|
"- Use dependency order: prerequisites first, dependent/user-facing work later. Use `--last` when placement is unclear.",
|
|
2220
|
+
"- Use `depends_on` / `--depends-on` when an issue cannot start until specific blocking issues are closed.",
|
|
2221
|
+
"- `issy list` shows a `Blk` column with open blocker counts; `-` means unblocked.",
|
|
2222
|
+
"- `issy list --unblocked` shows only open issues with no open blockers.",
|
|
2162
2223
|
"",
|
|
2163
2224
|
"## Closing",
|
|
2164
2225
|
"",
|
|
@@ -2242,6 +2303,16 @@ var topics = [
|
|
|
2242
2303
|
"- Updating an issue: position flags are optional and reposition the issue when provided.",
|
|
2243
2304
|
"- Never provide more than one position flag.",
|
|
2244
2305
|
"",
|
|
2306
|
+
"## Explicit blockers",
|
|
2307
|
+
"",
|
|
2308
|
+
"- Use `depends_on` when an issue cannot start until specific blocking issues are closed.",
|
|
2309
|
+
'- Create blocked work with `issy create --title "..." --depends-on 0001,0002 --last`.',
|
|
2310
|
+
"- Replace blockers with `issy update <id> --depends-on 0001,0003`.",
|
|
2311
|
+
'- Clear blockers with `issy update <id> --depends-on ""`.',
|
|
2312
|
+
"- Missing or malformed dependency IDs are ignored.",
|
|
2313
|
+
"- `issy list` shows a compact `Blk` column. `-` means unblocked; otherwise the value is the number of open blockers.",
|
|
2314
|
+
"- `issy list --unblocked` shows only open issues with no open blockers.",
|
|
2315
|
+
"",
|
|
2245
2316
|
"## Choosing placement",
|
|
2246
2317
|
"",
|
|
2247
2318
|
"- Place prerequisites before dependent issues.",
|
|
@@ -2253,9 +2324,12 @@ var topics = [
|
|
|
2253
2324
|
"## Useful commands",
|
|
2254
2325
|
"",
|
|
2255
2326
|
"- `issy list` shows open issues in roadmap order.",
|
|
2327
|
+
"- `issy list --unblocked` shows open issues with no open blockers.",
|
|
2256
2328
|
"- `issy next` shows the first open issue in roadmap order.",
|
|
2257
2329
|
'- `issy create --title "..." --last` appends a new issue.',
|
|
2330
|
+
'- `issy create --title "..." --depends-on 0001,0002 --last` creates an issue blocked by other issues.',
|
|
2258
2331
|
"- `issy update <id> --before <other-id>` repositions an issue.",
|
|
2332
|
+
"- `issy update <id> --depends-on 0001,0003` replaces blockers.",
|
|
2259
2333
|
"- `issy reopen <id> --after <other-id>` reopens and places a closed issue."
|
|
2260
2334
|
])
|
|
2261
2335
|
},
|
|
@@ -2272,6 +2346,7 @@ var topics = [
|
|
|
2272
2346
|
"",
|
|
2273
2347
|
"- `issy list`: list open issues in roadmap order.",
|
|
2274
2348
|
"- `issy list --all`: include closed issues.",
|
|
2349
|
+
"- `issy list --unblocked`: list open issues with no open blockers.",
|
|
2275
2350
|
"- `issy list --priority high|medium|low`: filter by priority.",
|
|
2276
2351
|
"- `issy list --scope small|medium|large`: filter by scope.",
|
|
2277
2352
|
"- `issy list --type bug|improvement`: filter by type.",
|
|
@@ -2289,10 +2364,11 @@ var topics = [
|
|
|
2289
2364
|
"",
|
|
2290
2365
|
'- `issy create --title "Fix login bug" --type bug --priority high --after 0002`.',
|
|
2291
2366
|
'- `issy create --title "Add dark mode" --type improvement --last --labels "ui, frontend"`.',
|
|
2367
|
+
'- `issy create --title "Add export" --depends-on 0001,0002 --last`.',
|
|
2292
2368
|
'- `issy create --title "Urgent fix" --first`.',
|
|
2293
2369
|
'- `issy create --title "Fix crash" --body "## Problem\\n\\nApp crashes on startup." --last`.',
|
|
2294
2370
|
"",
|
|
2295
|
-
"Create options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--before`, `--after`, `--first`, `--last`.",
|
|
2371
|
+
"Create options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--depends-on`, `--before`, `--after`, `--first`, `--last`.",
|
|
2296
2372
|
"",
|
|
2297
2373
|
"## Update",
|
|
2298
2374
|
"",
|
|
@@ -2300,9 +2376,10 @@ var topics = [
|
|
|
2300
2376
|
"- `issy update <id> --after 0003`.",
|
|
2301
2377
|
"- `issy update <id> --first`.",
|
|
2302
2378
|
'- `issy update <id> --labels "api, backend"`.',
|
|
2379
|
+
"- `issy update <id> --depends-on 0001,0003`.",
|
|
2303
2380
|
'- `issy update <id> --body "## Problem\\n\\nUpdated description."`.',
|
|
2304
2381
|
"",
|
|
2305
|
-
"Update options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--before`, `--after`, `--first`, `--last`.",
|
|
2382
|
+
"Update options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--depends-on`, `--before`, `--after`, `--first`, `--last`.",
|
|
2306
2383
|
"",
|
|
2307
2384
|
"## Close and reopen",
|
|
2308
2385
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "issy",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
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.10.
|
|
39
|
-
"@miketromba/issy-core": "^0.10.
|
|
38
|
+
"@miketromba/issy-app": "^0.10.1",
|
|
39
|
+
"@miketromba/issy-core": "^0.10.1",
|
|
40
40
|
"update-notifier": "^7.3.1"
|
|
41
41
|
}
|
|
42
42
|
}
|