trackerctl 0.3.0 → 0.4.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 +24 -0
- package/bin/tracker.mjs +0 -0
- package/dist/tracker.js +344 -5
- package/package.json +1 -1
- package/tracker.config.example.json +1 -0
package/README.md
CHANGED
|
@@ -89,6 +89,7 @@ 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 |
|
|
92
93
|
| `tracker spend <id> <duration>` | Add time spent (`1h30m`, `2d`; `-30m` subtracts) |
|
|
93
94
|
| `tracker estimate <id> <duration>` | Set the time estimate (`0` clears it) |
|
|
94
95
|
| `tracker dep <id> --blocked-by <o> \| --blocks <o>` | Add a dependency edge |
|
|
@@ -96,6 +97,16 @@ Read commands serve from a local sqlite cache that auto-syncs when older than
|
|
|
96
97
|
| `tracker remember <key> <text>` | Store a project memory |
|
|
97
98
|
| `tracker forget <key>` | Hide a memory key |
|
|
98
99
|
| `tracker memories [filter]` | List memories (latest per key wins) |
|
|
100
|
+
| `tracker pr create -t <title> --target <b> …` | Open a PR/MR (`mr` is an alias); `-i 42,43` records Closes trailers |
|
|
101
|
+
| `tracker pr status <id>` | State + provider-neutral CI signal (`none\|pending\|green\|red`) |
|
|
102
|
+
| `tracker pr merge <id> [--close-issues]` | Merge; `--close-issues` closes trailer-referenced issues explicitly |
|
|
103
|
+
| `tracker pr comment/comments/close/reopen` | Discuss, reject (`close -m <reason>`), reopen |
|
|
104
|
+
|
|
105
|
+
Issues and PRs are **separate capability ports**: `provider` selects where issues live,
|
|
106
|
+
`merge_provider` (defaults to `provider`) selects where PRs live — so a Jira + GitHub
|
|
107
|
+
mix needs no core changes, only adapters. Issue closing on merge is always explicit via
|
|
108
|
+
the issues port: GitLab's `Closes #N` magic only fires on default-branch targets, and a
|
|
109
|
+
GitHub PR can never auto-close a Jira issue, so tracker never relies on provider magic.
|
|
99
110
|
|
|
100
111
|
Examples:
|
|
101
112
|
|
|
@@ -106,6 +117,7 @@ tracker search --state closed # state alone is a valid filter
|
|
|
106
117
|
tracker search "payment timeout" --label backend --state open
|
|
107
118
|
tracker comment 42 "blocked on design review"
|
|
108
119
|
tracker comments 42 --json
|
|
120
|
+
tracker attach 42 before.png after.png -m "reference screenshots"
|
|
109
121
|
tracker spend 42 1h30m # durations: w/d/h/m/s, 1d=8h, 1w=5d
|
|
110
122
|
tracker estimate 42 2d
|
|
111
123
|
tracker search checkout --remote --json # fresher, server-side
|
|
@@ -113,6 +125,9 @@ tracker create -t "Ship login" -d "OAuth" --parent 12 --blocked-by 7,9 -l auth,b
|
|
|
113
125
|
tracker claim 42 && do-the-work || echo "someone else got it"
|
|
114
126
|
tracker close 42 --reason "fixed in MR !17"
|
|
115
127
|
tracker remember deploy-cmd "bun run deploy:prod"
|
|
128
|
+
tracker pr create -t "Fix login" --target dev -i 42 --json
|
|
129
|
+
tracker pr status 5 --json # poll for ci green/red
|
|
130
|
+
tracker pr merge 5 --close-issues
|
|
116
131
|
```
|
|
117
132
|
|
|
118
133
|
Exit codes: **0** success · **2** domain failure (lost claim race, refused claim, not
|
|
@@ -151,6 +166,15 @@ domain refusal (e.g. lost a claim race) — pick different work, don't retry.
|
|
|
151
166
|
- Inspect: `tracker show <id> --json`, `tracker children <id> --json`,
|
|
152
167
|
`tracker epic-status <id> --json`, `tracker comments <id> --json`
|
|
153
168
|
- Discuss: `tracker comment <id> "<note for humans or other agents>"`
|
|
169
|
+
- Attach evidence: `tracker attach <id> <files...> -m "<context>" --json` — uploads
|
|
170
|
+
screenshots/files and references them from a comment; reuse the
|
|
171
|
+
returned markdown in descriptions
|
|
172
|
+
- Open a PR/MR: `tracker pr create -t "<title>" --target <branch> -i <issue-ids> --json`
|
|
173
|
+
(source defaults to the current branch)
|
|
174
|
+
- Watch the CI: `tracker pr status <id> --json` — poll until `.ci` is "green" or "red"
|
|
175
|
+
- Land it: `tracker pr merge <id> --close-issues` — also closes the `-i` issues
|
|
176
|
+
with a comment explaining why; never rely on provider auto-close
|
|
177
|
+
|
|
154
178
|
- Track time: `tracker spend <id> 1h30m` after finishing work (estimate vs spent
|
|
155
179
|
feeds later evaluations; durations use 1d=8h, -30m subtracts)
|
|
156
180
|
- 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,8 @@ 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
|
+
attach <id> <file...> [-m <message>]
|
|
1358
|
+
upload files and attach them via a comment
|
|
1204
1359
|
spend <id> <duration> add time spent (1h30m, 45m, 2d; -30m subtracts)
|
|
1205
1360
|
estimate <id> <duration> set the time estimate (0 clears it)
|
|
1206
1361
|
dep <id> --blocked-by <other> | --blocks <other>
|
|
@@ -1208,6 +1363,15 @@ write commands:
|
|
|
1208
1363
|
remember <key> <text> store a project memory (key has no whitespace)
|
|
1209
1364
|
forget <key> hide a memory key
|
|
1210
1365
|
|
|
1366
|
+
pull/merge requests ("pr" and "mr" are the same command):
|
|
1367
|
+
pr create -t <title> --target <branch> [--source <branch>] [-d <desc>]
|
|
1368
|
+
[-i <issue-ids>] [--draft] [--json]
|
|
1369
|
+
pr status <id> [--json] state + provider-neutral ci signal (none|pending|green|red)
|
|
1370
|
+
pr merge <id> [--close-issues]
|
|
1371
|
+
pr comment <id> <text>
|
|
1372
|
+
pr comments <id> [--json]
|
|
1373
|
+
pr close <id> [-m <reason>] | pr reopen <id>
|
|
1374
|
+
|
|
1211
1375
|
search filters:
|
|
1212
1376
|
--assignee <user> --author <user> --label <l> --state open|closed|all
|
|
1213
1377
|
--parent <id> --remote (text query optional when any filter is given)
|
|
@@ -1266,6 +1430,42 @@ Posts a comment on the item. Everything after the id is joined into one
|
|
|
1266
1430
|
comment body, so quoting multi-word text is optional.
|
|
1267
1431
|
|
|
1268
1432
|
example: tracker comment 42 "blocked on the design review, see thread"`,
|
|
1433
|
+
attach: `usage: tracker attach <id> <file...> [-m <message>] [--json]
|
|
1434
|
+
|
|
1435
|
+
Uploads each file to the provider and posts ONE comment on the item containing
|
|
1436
|
+
the optional message plus a markdown reference per file, so the attachments are
|
|
1437
|
+
discoverable from the item itself. Prints each file's markdown snippet (reusable
|
|
1438
|
+
in descriptions); --json emits [{filename, url, markdown}].
|
|
1439
|
+
|
|
1440
|
+
examples:
|
|
1441
|
+
tracker attach 42 before.png after.png -m "reference screenshots"
|
|
1442
|
+
tracker attach 42 design.png --json`,
|
|
1443
|
+
pr: `usage: tracker pr <action> \u2026 (alias: tracker mr)
|
|
1444
|
+
|
|
1445
|
+
actions:
|
|
1446
|
+
create -t <title> --target <branch> [--source <branch>] [-d <desc>]
|
|
1447
|
+
[-i <id1,id2>] [--draft] [--json]
|
|
1448
|
+
open a PR/MR; --source defaults to the current git branch.
|
|
1449
|
+
-i records "Closes #N" trailers so merge --close-issues
|
|
1450
|
+
can close those issues explicitly (no provider magic).
|
|
1451
|
+
status <id> [--json]
|
|
1452
|
+
state (open|merged|closed) + ci signal (none|pending|green|red);
|
|
1453
|
+
poll this to watch a pipeline.
|
|
1454
|
+
merge <id> [--close-issues]
|
|
1455
|
+
merge; --close-issues then closes every trailer-referenced
|
|
1456
|
+
issue via the issue tracker and comments why.
|
|
1457
|
+
comment <id> <text> post a comment
|
|
1458
|
+
comments <id> [--json]
|
|
1459
|
+
list comments oldest-first
|
|
1460
|
+
close <id> [-m <reason>]
|
|
1461
|
+
close without merging ("reject"); reason posts as a comment
|
|
1462
|
+
reopen <id>
|
|
1463
|
+
|
|
1464
|
+
examples:
|
|
1465
|
+
tracker pr create -t "Fix login" --target dev -i 42 --json
|
|
1466
|
+
tracker pr status 5 --json
|
|
1467
|
+
tracker pr merge 5 --close-issues
|
|
1468
|
+
tracker pr close 5 -m "superseded by !6"`,
|
|
1269
1469
|
comments: `usage: tracker comments <id> [--json]
|
|
1270
1470
|
|
|
1271
1471
|
Lists the item's comments oldest-first (system notes are filtered out). Claim
|
|
@@ -1626,6 +1826,128 @@ async function cmdComment(ctx, args) {
|
|
|
1626
1826
|
await ctx.adapter.comment(id, body);
|
|
1627
1827
|
console.log(`commented on #${id}`);
|
|
1628
1828
|
}
|
|
1829
|
+
async function cmdAttach(ctx, args) {
|
|
1830
|
+
const [idArg, ...paths] = args.positionals;
|
|
1831
|
+
const id = normalizeId(idArg);
|
|
1832
|
+
if (paths.length === 0)
|
|
1833
|
+
throw new UsageError(commandHelp("attach"));
|
|
1834
|
+
const files = paths.map((p) => {
|
|
1835
|
+
if (!existsSync4(p))
|
|
1836
|
+
throw new UsageError(`${p}: no such file`);
|
|
1837
|
+
return { filename: basename(p), content: new Uint8Array(readFileSync4(p)) };
|
|
1838
|
+
});
|
|
1839
|
+
const attachments = await ctx.adapter.attach(id, files, str(args, "--message"));
|
|
1840
|
+
if (args.flags.get("--json"))
|
|
1841
|
+
return printJson(attachments);
|
|
1842
|
+
for (const a of attachments)
|
|
1843
|
+
console.log(a.markdown);
|
|
1844
|
+
console.error(`attached ${attachments.length} file(s) to #${id}`);
|
|
1845
|
+
}
|
|
1846
|
+
function normalizePrId(raw) {
|
|
1847
|
+
if (!raw)
|
|
1848
|
+
throw new UsageError("PR id is required");
|
|
1849
|
+
return raw.replace(/^[!#]/, "");
|
|
1850
|
+
}
|
|
1851
|
+
function currentGitBranch() {
|
|
1852
|
+
const proc = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
1853
|
+
const branch = proc.success ? proc.stdout.toString().trim() : "";
|
|
1854
|
+
if (!branch || branch === "HEAD") {
|
|
1855
|
+
throw new UsageError("could not determine the current git branch \u2014 pass --source");
|
|
1856
|
+
}
|
|
1857
|
+
return branch;
|
|
1858
|
+
}
|
|
1859
|
+
async function cmdPr(ctx, args) {
|
|
1860
|
+
const [action, ...positionals] = args.positionals;
|
|
1861
|
+
const json = args.flags.get("--json");
|
|
1862
|
+
switch (action) {
|
|
1863
|
+
case "create": {
|
|
1864
|
+
const title = str(args, "--title");
|
|
1865
|
+
const target = str(args, "--target");
|
|
1866
|
+
if (!title)
|
|
1867
|
+
throw new UsageError(`--title is required
|
|
1868
|
+
|
|
1869
|
+
${commandHelp("pr")}`);
|
|
1870
|
+
if (!target)
|
|
1871
|
+
throw new UsageError(`--target is required
|
|
1872
|
+
|
|
1873
|
+
${commandHelp("pr")}`);
|
|
1874
|
+
const pr = await ctx.adapter.prCreate({
|
|
1875
|
+
title,
|
|
1876
|
+
target,
|
|
1877
|
+
source: str(args, "--source") ?? currentGitBranch(),
|
|
1878
|
+
description: str(args, "--description"),
|
|
1879
|
+
draft: args.flags.get("--draft") === true,
|
|
1880
|
+
issues: (str(args, "--issue") ?? "").split(",").filter(Boolean).map((s) => s.replace(/^#/, ""))
|
|
1881
|
+
});
|
|
1882
|
+
if (json)
|
|
1883
|
+
return printJson(pr);
|
|
1884
|
+
console.log(pr.url);
|
|
1885
|
+
console.error(`created PR !${pr.id}: ${pr.source} \u2192 ${pr.target}`);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
case "status": {
|
|
1889
|
+
const pr = await ctx.adapter.prGet(normalizePrId(positionals[0]));
|
|
1890
|
+
if (json)
|
|
1891
|
+
return printJson(pr);
|
|
1892
|
+
const closes = pr.closesIssues.length ? ` \xB7 closes ${pr.closesIssues.map((i) => `#${i}`).join(",")}` : "";
|
|
1893
|
+
console.log(`!${pr.id} ${pr.state} \xB7 ci ${pr.ci}${pr.draft ? " \xB7 draft" : ""}${closes}`);
|
|
1894
|
+
console.log(pr.url);
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
case "merge": {
|
|
1898
|
+
const id = normalizePrId(positionals[0]);
|
|
1899
|
+
if (args.flags.get("--close-issues")) {
|
|
1900
|
+
const { closed } = await mergeAndCloseIssues(ctx.adapter, ctx.adapter, id);
|
|
1901
|
+
invalidate(ctx);
|
|
1902
|
+
console.log(closed.length ? `merged !${id}, closed ${closed.map((i) => `#${i}`).join(", ")}` : `merged !${id} (no Closes trailers found)`);
|
|
1903
|
+
} else {
|
|
1904
|
+
await ctx.adapter.prMerge(id);
|
|
1905
|
+
console.log(`merged !${id}`);
|
|
1906
|
+
}
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
case "comment": {
|
|
1910
|
+
const id = normalizePrId(positionals[0]);
|
|
1911
|
+
const body = positionals.slice(1).join(" ").trim();
|
|
1912
|
+
if (!body)
|
|
1913
|
+
throw new UsageError(commandHelp("pr"));
|
|
1914
|
+
await ctx.adapter.prComment(id, body);
|
|
1915
|
+
console.log(`commented on !${id}`);
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
case "comments": {
|
|
1919
|
+
const id = normalizePrId(positionals[0]);
|
|
1920
|
+
const comments = await ctx.adapter.prListComments(id);
|
|
1921
|
+
if (json)
|
|
1922
|
+
return printJson(comments);
|
|
1923
|
+
if (comments.length === 0)
|
|
1924
|
+
return console.error(`(no comments on !${id})`);
|
|
1925
|
+
for (const c of comments) {
|
|
1926
|
+
console.log(`@${c.author.username} ${c.createdAt}`);
|
|
1927
|
+
console.log(c.body);
|
|
1928
|
+
console.log("");
|
|
1929
|
+
}
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
case "close": {
|
|
1933
|
+
const id = normalizePrId(positionals[0]);
|
|
1934
|
+
const reason = str(args, "--message");
|
|
1935
|
+
if (reason)
|
|
1936
|
+
await ctx.adapter.prComment(id, reason);
|
|
1937
|
+
await ctx.adapter.prClose(id);
|
|
1938
|
+
console.log(`closed !${id}`);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
case "reopen": {
|
|
1942
|
+
const id = normalizePrId(positionals[0]);
|
|
1943
|
+
await ctx.adapter.prReopen(id);
|
|
1944
|
+
console.log(`reopened !${id}`);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
default:
|
|
1948
|
+
throw new UsageError(commandHelp("pr"));
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1629
1951
|
async function cmdComments(ctx, args) {
|
|
1630
1952
|
const id = normalizeId(args.positionals[0]);
|
|
1631
1953
|
const comments = await ctx.adapter.listComments(id);
|
|
@@ -1841,6 +2163,18 @@ var VALUE_FLAGS = {
|
|
|
1841
2163
|
memories: { "--json": "bool" },
|
|
1842
2164
|
comment: {},
|
|
1843
2165
|
comments: { "--json": "bool" },
|
|
2166
|
+
attach: { "--message": "value", "--json": "bool" },
|
|
2167
|
+
pr: {
|
|
2168
|
+
"--title": "value",
|
|
2169
|
+
"--description": "value",
|
|
2170
|
+
"--source": "value",
|
|
2171
|
+
"--target": "value",
|
|
2172
|
+
"--issue": "value",
|
|
2173
|
+
"--draft": "bool",
|
|
2174
|
+
"--close-issues": "bool",
|
|
2175
|
+
"--message": "value",
|
|
2176
|
+
"--json": "bool"
|
|
2177
|
+
},
|
|
1844
2178
|
spend: {},
|
|
1845
2179
|
estimate: {},
|
|
1846
2180
|
sync: {},
|
|
@@ -1858,10 +2192,13 @@ var ALIASES = {
|
|
|
1858
2192
|
"--labels": "--label",
|
|
1859
2193
|
"-m": "--milestone"
|
|
1860
2194
|
},
|
|
1861
|
-
close: { "-r": "--reason" }
|
|
2195
|
+
close: { "-r": "--reason" },
|
|
2196
|
+
attach: { "-m": "--message" },
|
|
2197
|
+
pr: { "-t": "--title", "-d": "--description", "-m": "--message", "-i": "--issue" }
|
|
1862
2198
|
};
|
|
1863
2199
|
async function run(argv) {
|
|
1864
|
-
const [
|
|
2200
|
+
const [rawCmd, ...rest] = argv;
|
|
2201
|
+
const cmd = rawCmd === "mr" ? "pr" : rawCmd;
|
|
1865
2202
|
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
1866
2203
|
console.log(HELP);
|
|
1867
2204
|
return cmd ? 0 : 1;
|
|
@@ -1902,6 +2239,8 @@ ${HELP}`);
|
|
|
1902
2239
|
memories: cmdMemories,
|
|
1903
2240
|
comment: cmdComment,
|
|
1904
2241
|
comments: cmdComments,
|
|
2242
|
+
attach: cmdAttach,
|
|
2243
|
+
pr: cmdPr,
|
|
1905
2244
|
spend: cmdSpend,
|
|
1906
2245
|
estimate: cmdEstimate,
|
|
1907
2246
|
search: cmdSearch,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackerctl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|