trackerctl 0.3.0 → 0.4.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 +25 -0
- package/bin/tracker.mjs +0 -0
- package/dist/tracker.js +375 -5
- package/package.json +1 -1
- package/tracker.config.example.json +1 -0
package/README.md
CHANGED
|
@@ -89,6 +89,8 @@ Read commands serve from a local sqlite cache that auto-syncs when older than
|
|
|
89
89
|
| `tracker release <id>` | Clear assignee + label, tombstone claim tokens |
|
|
90
90
|
| `tracker close <id> [--reason <text>]` | Close (clears assignee + in-progress label) |
|
|
91
91
|
| `tracker comment <id> <text>` | Post a comment on an item |
|
|
92
|
+
| `tracker attach <id> <file...> [-m <msg>]` | Upload files, reference them from one comment; prints markdown |
|
|
93
|
+
| `tracker label <id> [--add a,b] [--remove c,d]` | Add/remove labels without clobbering the rest |
|
|
92
94
|
| `tracker spend <id> <duration>` | Add time spent (`1h30m`, `2d`; `-30m` subtracts) |
|
|
93
95
|
| `tracker estimate <id> <duration>` | Set the time estimate (`0` clears it) |
|
|
94
96
|
| `tracker dep <id> --blocked-by <o> \| --blocks <o>` | Add a dependency edge |
|
|
@@ -96,6 +98,16 @@ Read commands serve from a local sqlite cache that auto-syncs when older than
|
|
|
96
98
|
| `tracker remember <key> <text>` | Store a project memory |
|
|
97
99
|
| `tracker forget <key>` | Hide a memory key |
|
|
98
100
|
| `tracker memories [filter]` | List memories (latest per key wins) |
|
|
101
|
+
| `tracker pr create -t <title> --target <b> …` | Open a PR/MR (`mr` is an alias); `-i 42,43` records Closes trailers |
|
|
102
|
+
| `tracker pr status <id>` | State + provider-neutral CI signal (`none\|pending\|green\|red`) |
|
|
103
|
+
| `tracker pr merge <id> [--close-issues]` | Merge; `--close-issues` closes trailer-referenced issues explicitly |
|
|
104
|
+
| `tracker pr comment/comments/close/reopen` | Discuss, reject (`close -m <reason>`), reopen |
|
|
105
|
+
|
|
106
|
+
Issues and PRs are **separate capability ports**: `provider` selects where issues live,
|
|
107
|
+
`merge_provider` (defaults to `provider`) selects where PRs live — so a Jira + GitHub
|
|
108
|
+
mix needs no core changes, only adapters. Issue closing on merge is always explicit via
|
|
109
|
+
the issues port: GitLab's `Closes #N` magic only fires on default-branch targets, and a
|
|
110
|
+
GitHub PR can never auto-close a Jira issue, so tracker never relies on provider magic.
|
|
99
111
|
|
|
100
112
|
Examples:
|
|
101
113
|
|
|
@@ -106,6 +118,7 @@ tracker search --state closed # state alone is a valid filter
|
|
|
106
118
|
tracker search "payment timeout" --label backend --state open
|
|
107
119
|
tracker comment 42 "blocked on design review"
|
|
108
120
|
tracker comments 42 --json
|
|
121
|
+
tracker attach 42 before.png after.png -m "reference screenshots"
|
|
109
122
|
tracker spend 42 1h30m # durations: w/d/h/m/s, 1d=8h, 1w=5d
|
|
110
123
|
tracker estimate 42 2d
|
|
111
124
|
tracker search checkout --remote --json # fresher, server-side
|
|
@@ -113,6 +126,9 @@ tracker create -t "Ship login" -d "OAuth" --parent 12 --blocked-by 7,9 -l auth,b
|
|
|
113
126
|
tracker claim 42 && do-the-work || echo "someone else got it"
|
|
114
127
|
tracker close 42 --reason "fixed in MR !17"
|
|
115
128
|
tracker remember deploy-cmd "bun run deploy:prod"
|
|
129
|
+
tracker pr create -t "Fix login" --target dev -i 42 --json
|
|
130
|
+
tracker pr status 5 --json # poll for ci green/red
|
|
131
|
+
tracker pr merge 5 --close-issues
|
|
116
132
|
```
|
|
117
133
|
|
|
118
134
|
Exit codes: **0** success · **2** domain failure (lost claim race, refused claim, not
|
|
@@ -151,6 +167,15 @@ domain refusal (e.g. lost a claim race) — pick different work, don't retry.
|
|
|
151
167
|
- Inspect: `tracker show <id> --json`, `tracker children <id> --json`,
|
|
152
168
|
`tracker epic-status <id> --json`, `tracker comments <id> --json`
|
|
153
169
|
- Discuss: `tracker comment <id> "<note for humans or other agents>"`
|
|
170
|
+
- Attach evidence: `tracker attach <id> <files...> -m "<context>" --json` — uploads
|
|
171
|
+
screenshots/files and references them from a comment; reuse the
|
|
172
|
+
returned markdown in descriptions
|
|
173
|
+
- Open a PR/MR: `tracker pr create -t "<title>" --target <branch> -i <issue-ids> --json`
|
|
174
|
+
(source defaults to the current branch)
|
|
175
|
+
- Watch the CI: `tracker pr status <id> --json` — poll until `.ci` is "green" or "red"
|
|
176
|
+
- Land it: `tracker pr merge <id> --close-issues` — also closes the `-i` issues
|
|
177
|
+
with a comment explaining why; never rely on provider auto-close
|
|
178
|
+
|
|
154
179
|
- Track time: `tracker spend <id> 1h30m` after finishing work (estimate vs spent
|
|
155
180
|
feeds later evaluations; durations use 1d=8h, -30m subtracts)
|
|
156
181
|
- Project memory: `tracker remember <key> "<fact>"`, `tracker memories --json`,
|
package/bin/tracker.mjs
CHANGED
|
File without changes
|
package/dist/tracker.js
CHANGED
|
@@ -2,8 +2,38 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/cli/index.ts
|
|
5
|
-
import { existsSync as existsSync4 } from "fs";
|
|
6
|
-
import { resolve as resolve4 } from "path";
|
|
5
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
6
|
+
import { basename, resolve as resolve4 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/core/merge.ts
|
|
9
|
+
function closesTrailers(issues) {
|
|
10
|
+
return issues.map((id) => `Closes #${id}`).join(`
|
|
11
|
+
`);
|
|
12
|
+
}
|
|
13
|
+
function parseClosesIssues(description) {
|
|
14
|
+
const ids = [];
|
|
15
|
+
for (const m of description.matchAll(/^Closes #(\S+)\s*$/gim)) {
|
|
16
|
+
if (!ids.includes(m[1]))
|
|
17
|
+
ids.push(m[1]);
|
|
18
|
+
}
|
|
19
|
+
return ids;
|
|
20
|
+
}
|
|
21
|
+
function withClosesTrailers(description, issues) {
|
|
22
|
+
if (issues.length === 0)
|
|
23
|
+
return description;
|
|
24
|
+
return [description, closesTrailers(issues)].filter(Boolean).join(`
|
|
25
|
+
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
async function mergeAndCloseIssues(merge, issues, prId) {
|
|
29
|
+
const pr = await merge.prGet(prId);
|
|
30
|
+
await merge.prMerge(prId);
|
|
31
|
+
for (const id of pr.closesIssues) {
|
|
32
|
+
await issues.transition(id, "closed");
|
|
33
|
+
await issues.comment(id, `closed by merged PR: ${pr.url}`);
|
|
34
|
+
}
|
|
35
|
+
return { closed: pr.closesIssues };
|
|
36
|
+
}
|
|
7
37
|
|
|
8
38
|
// src/errors.ts
|
|
9
39
|
class DomainError extends Error {
|
|
@@ -52,7 +82,7 @@ class GitLabClient {
|
|
|
52
82
|
...init,
|
|
53
83
|
headers: {
|
|
54
84
|
Authorization: `Bearer ${this.token}`,
|
|
55
|
-
"Content-Type": "application/json",
|
|
85
|
+
...init.body instanceof FormData ? {} : { "Content-Type": "application/json" },
|
|
56
86
|
...init.headers ?? {}
|
|
57
87
|
}
|
|
58
88
|
});
|
|
@@ -101,6 +131,11 @@ class GitLabClient {
|
|
|
101
131
|
}
|
|
102
132
|
return all;
|
|
103
133
|
}
|
|
134
|
+
async upload(path, form) {
|
|
135
|
+
const url = `${this.baseUrl}/api/v4/${path.replace(/^\//, "")}`;
|
|
136
|
+
const res = await this.request(url, { method: "POST", body: form });
|
|
137
|
+
return await res.json();
|
|
138
|
+
}
|
|
104
139
|
async graphql(query, variables = {}) {
|
|
105
140
|
const res = await this.request(`${this.baseUrl}/api/graphql`, {
|
|
106
141
|
method: "POST",
|
|
@@ -207,10 +242,35 @@ mutation trackerWorkItemSetParent($input: WorkItemUpdateInput!) {
|
|
|
207
242
|
workItem { id }
|
|
208
243
|
}
|
|
209
244
|
}`;
|
|
245
|
+
function mapCiStatus(status) {
|
|
246
|
+
if (!status)
|
|
247
|
+
return "none";
|
|
248
|
+
if (status === "success")
|
|
249
|
+
return "green";
|
|
250
|
+
if (status === "failed" || status === "canceled")
|
|
251
|
+
return "red";
|
|
252
|
+
return "pending";
|
|
253
|
+
}
|
|
254
|
+
function mapMr(mr) {
|
|
255
|
+
return {
|
|
256
|
+
id: String(mr.iid),
|
|
257
|
+
title: mr.title,
|
|
258
|
+
state: mr.state === "merged" ? "merged" : mr.state === "opened" ? "open" : "closed",
|
|
259
|
+
source: mr.source_branch,
|
|
260
|
+
target: mr.target_branch,
|
|
261
|
+
draft: mr.draft ?? /^draft:/i.test(mr.title),
|
|
262
|
+
ci: mapCiStatus(mr.head_pipeline?.status),
|
|
263
|
+
closesIssues: parseClosesIssues(mr.description ?? ""),
|
|
264
|
+
url: mr.web_url,
|
|
265
|
+
description: mr.description ?? "",
|
|
266
|
+
updatedAt: mr.updated_at
|
|
267
|
+
};
|
|
268
|
+
}
|
|
210
269
|
|
|
211
270
|
class GitLabAdapter {
|
|
212
271
|
provider = "gitlab";
|
|
213
272
|
client;
|
|
273
|
+
baseUrl;
|
|
214
274
|
projectRef;
|
|
215
275
|
fullPath;
|
|
216
276
|
nativeBlocking;
|
|
@@ -222,6 +282,7 @@ class GitLabAdapter {
|
|
|
222
282
|
token: opts.token,
|
|
223
283
|
fetchImpl: opts.fetchImpl
|
|
224
284
|
});
|
|
285
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
225
286
|
this.projectRef = encodeURIComponent(opts.project);
|
|
226
287
|
this.fullPath = /^\d+$/.test(opts.project) ? null : opts.project;
|
|
227
288
|
this.nativeBlocking = opts.nativeBlocking;
|
|
@@ -477,6 +538,94 @@ class GitLabAdapter {
|
|
|
477
538
|
createdAt: n.created_at
|
|
478
539
|
}));
|
|
479
540
|
}
|
|
541
|
+
async prCreate(draft) {
|
|
542
|
+
const title = draft.draft && !/^draft:/i.test(draft.title) ? `Draft: ${draft.title}` : draft.title;
|
|
543
|
+
const mr = await this.client.rest(`projects/${this.projectRef}/merge_requests`, {
|
|
544
|
+
method: "POST",
|
|
545
|
+
body: {
|
|
546
|
+
title,
|
|
547
|
+
source_branch: draft.source,
|
|
548
|
+
target_branch: draft.target,
|
|
549
|
+
description: withClosesTrailers(draft.description ?? "", draft.issues ?? [])
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
return mapMr(mr);
|
|
553
|
+
}
|
|
554
|
+
async prGet(id) {
|
|
555
|
+
try {
|
|
556
|
+
const mr = await this.client.rest(`projects/${this.projectRef}/merge_requests/${id}`);
|
|
557
|
+
return mapMr(mr);
|
|
558
|
+
} catch (e) {
|
|
559
|
+
if (e instanceof GitLabHttpError && e.status === 404) {
|
|
560
|
+
throw new DomainError(`PR !${id} not found`);
|
|
561
|
+
}
|
|
562
|
+
throw e;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async prMerge(id) {
|
|
566
|
+
try {
|
|
567
|
+
await this.client.rest(`projects/${this.projectRef}/merge_requests/${id}/merge`, {
|
|
568
|
+
method: "PUT"
|
|
569
|
+
});
|
|
570
|
+
} catch (e) {
|
|
571
|
+
if (e instanceof GitLabHttpError && [405, 406, 409, 422].includes(e.status)) {
|
|
572
|
+
throw new DomainError(`GitLab refuses to merge !${id}: ${e.message}`);
|
|
573
|
+
}
|
|
574
|
+
throw e;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async prComment(id, body) {
|
|
578
|
+
await this.client.rest(`projects/${this.projectRef}/merge_requests/${id}/notes`, {
|
|
579
|
+
method: "POST",
|
|
580
|
+
body: { body }
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
async prListComments(id) {
|
|
584
|
+
const notes = await this.client.rest(`projects/${this.projectRef}/merge_requests/${id}/notes`, { query: { sort: "asc", order_by: "created_at" }, paginate: true });
|
|
585
|
+
return notes.filter((n) => !n.system).map((n) => ({
|
|
586
|
+
id: String(n.id),
|
|
587
|
+
body: n.body,
|
|
588
|
+
author: mapUser(n.author),
|
|
589
|
+
createdAt: n.created_at
|
|
590
|
+
}));
|
|
591
|
+
}
|
|
592
|
+
async prStateEvent(id, event) {
|
|
593
|
+
try {
|
|
594
|
+
await this.client.rest(`projects/${this.projectRef}/merge_requests/${id}`, {
|
|
595
|
+
method: "PUT",
|
|
596
|
+
body: { state_event: event }
|
|
597
|
+
});
|
|
598
|
+
} catch (e) {
|
|
599
|
+
if (e instanceof GitLabHttpError && e.status === 422) {
|
|
600
|
+
throw new DomainError(`cannot ${event} PR !${id}: ${e.message}`);
|
|
601
|
+
}
|
|
602
|
+
throw e;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async prClose(id) {
|
|
606
|
+
return this.prStateEvent(id, "close");
|
|
607
|
+
}
|
|
608
|
+
async prReopen(id) {
|
|
609
|
+
return this.prStateEvent(id, "reopen");
|
|
610
|
+
}
|
|
611
|
+
async attach(id, files, message) {
|
|
612
|
+
const attachments = [];
|
|
613
|
+
for (const file of files) {
|
|
614
|
+
const form = new FormData;
|
|
615
|
+
form.set("file", new Blob([file.content]), file.filename);
|
|
616
|
+
const up = await this.client.upload(`projects/${this.projectRef}/uploads`, form);
|
|
617
|
+
attachments.push({
|
|
618
|
+
filename: file.filename,
|
|
619
|
+
url: `${this.baseUrl}${up.full_path ?? up.url}`,
|
|
620
|
+
markdown: up.markdown
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
const body = [message, ...attachments.map((a) => a.markdown)].filter(Boolean).join(`
|
|
624
|
+
|
|
625
|
+
`);
|
|
626
|
+
await this.comment(id, body);
|
|
627
|
+
return attachments;
|
|
628
|
+
}
|
|
480
629
|
async searchRemote(q) {
|
|
481
630
|
const state = q.state ?? "all";
|
|
482
631
|
const issues = await this.client.rest(`projects/${this.projectRef}/issues`, {
|
|
@@ -728,6 +877,9 @@ function parseConfig(json, rootDir) {
|
|
|
728
877
|
if (raw.provider !== "gitlab") {
|
|
729
878
|
throw new UsageError(`unsupported provider "${raw.provider}" (only "gitlab" for now)`);
|
|
730
879
|
}
|
|
880
|
+
if ((raw.merge_provider ?? raw.provider) !== "gitlab") {
|
|
881
|
+
throw new UsageError(`unsupported merge_provider "${raw.merge_provider}" (only "gitlab" for now)`);
|
|
882
|
+
}
|
|
731
883
|
const g = raw.gitlab ?? {};
|
|
732
884
|
if (!g.base_url)
|
|
733
885
|
throw new UsageError("config: gitlab.base_url is required");
|
|
@@ -736,6 +888,7 @@ function parseConfig(json, rootDir) {
|
|
|
736
888
|
const tokenEnv = g.token_env === undefined ? ["TRACKER_GITLAB_TOKEN"] : Array.isArray(g.token_env) ? g.token_env : [g.token_env];
|
|
737
889
|
return {
|
|
738
890
|
provider: "gitlab",
|
|
891
|
+
merge_provider: "gitlab",
|
|
739
892
|
gitlab: {
|
|
740
893
|
base_url: g.base_url.replace(/\/+$/, ""),
|
|
741
894
|
project: String(g.project),
|
|
@@ -1201,6 +1354,10 @@ write commands:
|
|
|
1201
1354
|
release <id> clear assignee/label, tombstone live claim tokens
|
|
1202
1355
|
close <id> [--reason <text>]
|
|
1203
1356
|
comment <id> <text> post a comment on an item
|
|
1357
|
+
label <id> [--add a,b] [--remove c,d]
|
|
1358
|
+
add/remove labels without touching the others
|
|
1359
|
+
attach <id> <file...> [-m <message>]
|
|
1360
|
+
upload files and attach them via a comment
|
|
1204
1361
|
spend <id> <duration> add time spent (1h30m, 45m, 2d; -30m subtracts)
|
|
1205
1362
|
estimate <id> <duration> set the time estimate (0 clears it)
|
|
1206
1363
|
dep <id> --blocked-by <other> | --blocks <other>
|
|
@@ -1208,6 +1365,15 @@ write commands:
|
|
|
1208
1365
|
remember <key> <text> store a project memory (key has no whitespace)
|
|
1209
1366
|
forget <key> hide a memory key
|
|
1210
1367
|
|
|
1368
|
+
pull/merge requests ("pr" and "mr" are the same command):
|
|
1369
|
+
pr create -t <title> --target <branch> [--source <branch>] [-d <desc>]
|
|
1370
|
+
[-i <issue-ids>] [--draft] [--json]
|
|
1371
|
+
pr status <id> [--json] state + provider-neutral ci signal (none|pending|green|red)
|
|
1372
|
+
pr merge <id> [--close-issues]
|
|
1373
|
+
pr comment <id> <text>
|
|
1374
|
+
pr comments <id> [--json]
|
|
1375
|
+
pr close <id> [-m <reason>] | pr reopen <id>
|
|
1376
|
+
|
|
1211
1377
|
search filters:
|
|
1212
1378
|
--assignee <user> --author <user> --label <l> --state open|closed|all
|
|
1213
1379
|
--parent <id> --remote (text query optional when any filter is given)
|
|
@@ -1266,6 +1432,50 @@ Posts a comment on the item. Everything after the id is joined into one
|
|
|
1266
1432
|
comment body, so quoting multi-word text is optional.
|
|
1267
1433
|
|
|
1268
1434
|
example: tracker comment 42 "blocked on the design review, see thread"`,
|
|
1435
|
+
label: `usage: tracker label <id> [--add <a,b>] [--remove <c,d>]
|
|
1436
|
+
|
|
1437
|
+
Adds and/or removes labels (comma-separated) without clobbering the rest.
|
|
1438
|
+
At least one of --add/--remove is required.
|
|
1439
|
+
|
|
1440
|
+
examples:
|
|
1441
|
+
tracker label 42 --add status::agent-blocked
|
|
1442
|
+
tracker label 42 --remove status::agent-blocked --add meta::human-only`,
|
|
1443
|
+
attach: `usage: tracker attach <id> <file...> [-m <message>] [--json]
|
|
1444
|
+
|
|
1445
|
+
Uploads each file to the provider and posts ONE comment on the item containing
|
|
1446
|
+
the optional message plus a markdown reference per file, so the attachments are
|
|
1447
|
+
discoverable from the item itself. Prints each file's markdown snippet (reusable
|
|
1448
|
+
in descriptions); --json emits [{filename, url, markdown}].
|
|
1449
|
+
|
|
1450
|
+
examples:
|
|
1451
|
+
tracker attach 42 before.png after.png -m "reference screenshots"
|
|
1452
|
+
tracker attach 42 design.png --json`,
|
|
1453
|
+
pr: `usage: tracker pr <action> \u2026 (alias: tracker mr)
|
|
1454
|
+
|
|
1455
|
+
actions:
|
|
1456
|
+
create -t <title> --target <branch> [--source <branch>] [-d <desc>]
|
|
1457
|
+
[-i <id1,id2>] [--draft] [--json]
|
|
1458
|
+
open a PR/MR; --source defaults to the current git branch.
|
|
1459
|
+
-i records "Closes #N" trailers so merge --close-issues
|
|
1460
|
+
can close those issues explicitly (no provider magic).
|
|
1461
|
+
status <id> [--json]
|
|
1462
|
+
state (open|merged|closed) + ci signal (none|pending|green|red);
|
|
1463
|
+
poll this to watch a pipeline.
|
|
1464
|
+
merge <id> [--close-issues]
|
|
1465
|
+
merge; --close-issues then closes every trailer-referenced
|
|
1466
|
+
issue via the issue tracker and comments why.
|
|
1467
|
+
comment <id> <text> post a comment
|
|
1468
|
+
comments <id> [--json]
|
|
1469
|
+
list comments oldest-first
|
|
1470
|
+
close <id> [-m <reason>]
|
|
1471
|
+
close without merging ("reject"); reason posts as a comment
|
|
1472
|
+
reopen <id>
|
|
1473
|
+
|
|
1474
|
+
examples:
|
|
1475
|
+
tracker pr create -t "Fix login" --target dev -i 42 --json
|
|
1476
|
+
tracker pr status 5 --json
|
|
1477
|
+
tracker pr merge 5 --close-issues
|
|
1478
|
+
tracker pr close 5 -m "superseded by !6"`,
|
|
1269
1479
|
comments: `usage: tracker comments <id> [--json]
|
|
1270
1480
|
|
|
1271
1481
|
Lists the item's comments oldest-first (system notes are filtered out). Claim
|
|
@@ -1626,6 +1836,147 @@ async function cmdComment(ctx, args) {
|
|
|
1626
1836
|
await ctx.adapter.comment(id, body);
|
|
1627
1837
|
console.log(`commented on #${id}`);
|
|
1628
1838
|
}
|
|
1839
|
+
async function cmdLabel(ctx, args) {
|
|
1840
|
+
const id = normalizeId(args.positionals[0]);
|
|
1841
|
+
const csv = (name) => (str(args, name) ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1842
|
+
const addLabels = csv("--add");
|
|
1843
|
+
const removeLabels = csv("--remove");
|
|
1844
|
+
if (addLabels.length === 0 && removeLabels.length === 0) {
|
|
1845
|
+
throw new UsageError(commandHelp("label"));
|
|
1846
|
+
}
|
|
1847
|
+
await ctx.adapter.update(id, {
|
|
1848
|
+
...addLabels.length ? { addLabels } : {},
|
|
1849
|
+
...removeLabels.length ? { removeLabels } : {}
|
|
1850
|
+
});
|
|
1851
|
+
invalidate(ctx);
|
|
1852
|
+
const parts = [
|
|
1853
|
+
...addLabels.length ? [`+${addLabels.join(" +")}`] : [],
|
|
1854
|
+
...removeLabels.length ? [`-${removeLabels.join(" -")}`] : []
|
|
1855
|
+
];
|
|
1856
|
+
console.log(`#${id} labels: ${parts.join(" ")}`);
|
|
1857
|
+
}
|
|
1858
|
+
async function cmdAttach(ctx, args) {
|
|
1859
|
+
const [idArg, ...paths] = args.positionals;
|
|
1860
|
+
const id = normalizeId(idArg);
|
|
1861
|
+
if (paths.length === 0)
|
|
1862
|
+
throw new UsageError(commandHelp("attach"));
|
|
1863
|
+
const files = paths.map((p) => {
|
|
1864
|
+
if (!existsSync4(p))
|
|
1865
|
+
throw new UsageError(`${p}: no such file`);
|
|
1866
|
+
return { filename: basename(p), content: new Uint8Array(readFileSync4(p)) };
|
|
1867
|
+
});
|
|
1868
|
+
const attachments = await ctx.adapter.attach(id, files, str(args, "--message"));
|
|
1869
|
+
if (args.flags.get("--json"))
|
|
1870
|
+
return printJson(attachments);
|
|
1871
|
+
for (const a of attachments)
|
|
1872
|
+
console.log(a.markdown);
|
|
1873
|
+
console.error(`attached ${attachments.length} file(s) to #${id}`);
|
|
1874
|
+
}
|
|
1875
|
+
function normalizePrId(raw) {
|
|
1876
|
+
if (!raw)
|
|
1877
|
+
throw new UsageError("PR id is required");
|
|
1878
|
+
return raw.replace(/^[!#]/, "");
|
|
1879
|
+
}
|
|
1880
|
+
function currentGitBranch() {
|
|
1881
|
+
const proc = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
1882
|
+
const branch = proc.success ? proc.stdout.toString().trim() : "";
|
|
1883
|
+
if (!branch || branch === "HEAD") {
|
|
1884
|
+
throw new UsageError("could not determine the current git branch \u2014 pass --source");
|
|
1885
|
+
}
|
|
1886
|
+
return branch;
|
|
1887
|
+
}
|
|
1888
|
+
async function cmdPr(ctx, args) {
|
|
1889
|
+
const [action, ...positionals] = args.positionals;
|
|
1890
|
+
const json = args.flags.get("--json");
|
|
1891
|
+
switch (action) {
|
|
1892
|
+
case "create": {
|
|
1893
|
+
const title = str(args, "--title");
|
|
1894
|
+
const target = str(args, "--target");
|
|
1895
|
+
if (!title)
|
|
1896
|
+
throw new UsageError(`--title is required
|
|
1897
|
+
|
|
1898
|
+
${commandHelp("pr")}`);
|
|
1899
|
+
if (!target)
|
|
1900
|
+
throw new UsageError(`--target is required
|
|
1901
|
+
|
|
1902
|
+
${commandHelp("pr")}`);
|
|
1903
|
+
const pr = await ctx.adapter.prCreate({
|
|
1904
|
+
title,
|
|
1905
|
+
target,
|
|
1906
|
+
source: str(args, "--source") ?? currentGitBranch(),
|
|
1907
|
+
description: str(args, "--description"),
|
|
1908
|
+
draft: args.flags.get("--draft") === true,
|
|
1909
|
+
issues: (str(args, "--issue") ?? "").split(",").filter(Boolean).map((s) => s.replace(/^#/, ""))
|
|
1910
|
+
});
|
|
1911
|
+
if (json)
|
|
1912
|
+
return printJson(pr);
|
|
1913
|
+
console.log(pr.url);
|
|
1914
|
+
console.error(`created PR !${pr.id}: ${pr.source} \u2192 ${pr.target}`);
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
case "status": {
|
|
1918
|
+
const pr = await ctx.adapter.prGet(normalizePrId(positionals[0]));
|
|
1919
|
+
if (json)
|
|
1920
|
+
return printJson(pr);
|
|
1921
|
+
const closes = pr.closesIssues.length ? ` \xB7 closes ${pr.closesIssues.map((i) => `#${i}`).join(",")}` : "";
|
|
1922
|
+
console.log(`!${pr.id} ${pr.state} \xB7 ci ${pr.ci}${pr.draft ? " \xB7 draft" : ""}${closes}`);
|
|
1923
|
+
console.log(pr.url);
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
case "merge": {
|
|
1927
|
+
const id = normalizePrId(positionals[0]);
|
|
1928
|
+
if (args.flags.get("--close-issues")) {
|
|
1929
|
+
const { closed } = await mergeAndCloseIssues(ctx.adapter, ctx.adapter, id);
|
|
1930
|
+
invalidate(ctx);
|
|
1931
|
+
console.log(closed.length ? `merged !${id}, closed ${closed.map((i) => `#${i}`).join(", ")}` : `merged !${id} (no Closes trailers found)`);
|
|
1932
|
+
} else {
|
|
1933
|
+
await ctx.adapter.prMerge(id);
|
|
1934
|
+
console.log(`merged !${id}`);
|
|
1935
|
+
}
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
case "comment": {
|
|
1939
|
+
const id = normalizePrId(positionals[0]);
|
|
1940
|
+
const body = positionals.slice(1).join(" ").trim();
|
|
1941
|
+
if (!body)
|
|
1942
|
+
throw new UsageError(commandHelp("pr"));
|
|
1943
|
+
await ctx.adapter.prComment(id, body);
|
|
1944
|
+
console.log(`commented on !${id}`);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
case "comments": {
|
|
1948
|
+
const id = normalizePrId(positionals[0]);
|
|
1949
|
+
const comments = await ctx.adapter.prListComments(id);
|
|
1950
|
+
if (json)
|
|
1951
|
+
return printJson(comments);
|
|
1952
|
+
if (comments.length === 0)
|
|
1953
|
+
return console.error(`(no comments on !${id})`);
|
|
1954
|
+
for (const c of comments) {
|
|
1955
|
+
console.log(`@${c.author.username} ${c.createdAt}`);
|
|
1956
|
+
console.log(c.body);
|
|
1957
|
+
console.log("");
|
|
1958
|
+
}
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
case "close": {
|
|
1962
|
+
const id = normalizePrId(positionals[0]);
|
|
1963
|
+
const reason = str(args, "--message");
|
|
1964
|
+
if (reason)
|
|
1965
|
+
await ctx.adapter.prComment(id, reason);
|
|
1966
|
+
await ctx.adapter.prClose(id);
|
|
1967
|
+
console.log(`closed !${id}`);
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
case "reopen": {
|
|
1971
|
+
const id = normalizePrId(positionals[0]);
|
|
1972
|
+
await ctx.adapter.prReopen(id);
|
|
1973
|
+
console.log(`reopened !${id}`);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
default:
|
|
1977
|
+
throw new UsageError(commandHelp("pr"));
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1629
1980
|
async function cmdComments(ctx, args) {
|
|
1630
1981
|
const id = normalizeId(args.positionals[0]);
|
|
1631
1982
|
const comments = await ctx.adapter.listComments(id);
|
|
@@ -1841,6 +2192,19 @@ var VALUE_FLAGS = {
|
|
|
1841
2192
|
memories: { "--json": "bool" },
|
|
1842
2193
|
comment: {},
|
|
1843
2194
|
comments: { "--json": "bool" },
|
|
2195
|
+
attach: { "--message": "value", "--json": "bool" },
|
|
2196
|
+
label: { "--add": "value", "--remove": "value" },
|
|
2197
|
+
pr: {
|
|
2198
|
+
"--title": "value",
|
|
2199
|
+
"--description": "value",
|
|
2200
|
+
"--source": "value",
|
|
2201
|
+
"--target": "value",
|
|
2202
|
+
"--issue": "value",
|
|
2203
|
+
"--draft": "bool",
|
|
2204
|
+
"--close-issues": "bool",
|
|
2205
|
+
"--message": "value",
|
|
2206
|
+
"--json": "bool"
|
|
2207
|
+
},
|
|
1844
2208
|
spend: {},
|
|
1845
2209
|
estimate: {},
|
|
1846
2210
|
sync: {},
|
|
@@ -1858,10 +2222,13 @@ var ALIASES = {
|
|
|
1858
2222
|
"--labels": "--label",
|
|
1859
2223
|
"-m": "--milestone"
|
|
1860
2224
|
},
|
|
1861
|
-
close: { "-r": "--reason" }
|
|
2225
|
+
close: { "-r": "--reason" },
|
|
2226
|
+
attach: { "-m": "--message" },
|
|
2227
|
+
pr: { "-t": "--title", "-d": "--description", "-m": "--message", "-i": "--issue" }
|
|
1862
2228
|
};
|
|
1863
2229
|
async function run(argv) {
|
|
1864
|
-
const [
|
|
2230
|
+
const [rawCmd, ...rest] = argv;
|
|
2231
|
+
const cmd = rawCmd === "mr" ? "pr" : rawCmd;
|
|
1865
2232
|
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
1866
2233
|
console.log(HELP);
|
|
1867
2234
|
return cmd ? 0 : 1;
|
|
@@ -1902,6 +2269,9 @@ ${HELP}`);
|
|
|
1902
2269
|
memories: cmdMemories,
|
|
1903
2270
|
comment: cmdComment,
|
|
1904
2271
|
comments: cmdComments,
|
|
2272
|
+
attach: cmdAttach,
|
|
2273
|
+
label: cmdLabel,
|
|
2274
|
+
pr: cmdPr,
|
|
1905
2275
|
spend: cmdSpend,
|
|
1906
2276
|
estimate: cmdEstimate,
|
|
1907
2277
|
search: cmdSearch,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackerctl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Multi-backend issue-tracking CLI for humans and AI agents — claim races, dependencies, hierarchy, local FTS search, project memory. GitLab adapter first.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "refo",
|