@coderook/cli 0.31.4 → 0.31.6
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.
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "coderook",
|
|
3
3
|
"displayName": "CodeRook",
|
|
4
4
|
"description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, from Claude Code.",
|
|
5
|
-
"version": "0.31.
|
|
5
|
+
"version": "0.31.6",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/api.js
CHANGED
|
@@ -10,6 +10,9 @@ exports.projects = projects;
|
|
|
10
10
|
exports.findPublicProject = findPublicProject;
|
|
11
11
|
exports.findProject = findProject;
|
|
12
12
|
exports.health = health;
|
|
13
|
+
exports.requestReviews = requestReviews;
|
|
14
|
+
exports.openProposal = openProposal;
|
|
15
|
+
exports.proposals = proposals;
|
|
13
16
|
exports.mergeTracks = mergeTracks;
|
|
14
17
|
exports.mergeTrack = mergeTrack;
|
|
15
18
|
exports.resolveMergeConflict = resolveMergeConflict;
|
|
@@ -265,6 +268,22 @@ async function health() {
|
|
|
265
268
|
});
|
|
266
269
|
return (await response.json());
|
|
267
270
|
}
|
|
271
|
+
/** Ask people, by username, to review a merge. */
|
|
272
|
+
async function requestReviews(mergeTrackId, usernames) {
|
|
273
|
+
const body = await call(`/v1/merge-tracks/${mergeTrackId}/review-requests`, { method: "POST", body: { reviewers: usernames } });
|
|
274
|
+
return body.asked;
|
|
275
|
+
}
|
|
276
|
+
/** Offer a line of work to another, usually main. */
|
|
277
|
+
async function openProposal(repositoryId, input) {
|
|
278
|
+
return call(`/v1/repositories/${repositoryId}/proposals`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
body: input,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function proposals(repositoryId, all = false) {
|
|
284
|
+
const body = await call(`/v1/repositories/${repositoryId}/proposals${all ? "?state=all" : ""}`);
|
|
285
|
+
return body.proposals;
|
|
286
|
+
}
|
|
268
287
|
async function mergeTracks(repositoryId) {
|
|
269
288
|
const body = await call(`/v1/repositories/${repositoryId}/merge-tracks`);
|
|
270
289
|
return body.mergeTracks;
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -1026,6 +1026,17 @@ The connection failed: ${text}`));
|
|
|
1026
1026
|
observedHeadVersionId: result.versionId,
|
|
1027
1027
|
local: result.local,
|
|
1028
1028
|
manifest: result.manifest,
|
|
1029
|
+
/*
|
|
1030
|
+
The line this folder is on survives a save onto it.
|
|
1031
|
+
|
|
1032
|
+
The same fault `get` had and lost: this record is built fresh, and
|
|
1033
|
+
without naming `track` every save onto a line moved the folder back to
|
|
1034
|
+
main — so the second save on a line went to main, and `cbx propose`
|
|
1035
|
+
found the folder "on main" straight after `cbx track spike --new` and a
|
|
1036
|
+
save. A one-off `--track` does not move it either: that flag means
|
|
1037
|
+
"just this once", so the folder keeps the line it had.
|
|
1038
|
+
*/
|
|
1039
|
+
...(link?.track ? { track: link.track } : {}),
|
|
1029
1040
|
});
|
|
1030
1041
|
/*
|
|
1031
1042
|
What this save hashed, kept for the next one.
|
|
@@ -1547,6 +1558,69 @@ async function commandDoctor() {
|
|
|
1547
1558
|
* project looks untouched — so this is the way to find out that work of
|
|
1548
1559
|
* yours is sitting somewhere, still complete, waiting.
|
|
1549
1560
|
*/
|
|
1561
|
+
/**
|
|
1562
|
+
* Offer this folder's line of work to another line, usually main.
|
|
1563
|
+
*
|
|
1564
|
+
* The folder's line is the one proposed, because it is the one being worked
|
|
1565
|
+
* on; proposing main is refused rather than guessed at, since the answer to
|
|
1566
|
+
* "propose what?" is then a line nobody named.
|
|
1567
|
+
*/
|
|
1568
|
+
async function commandPropose(parsed) {
|
|
1569
|
+
const folder = folderFor(parsed);
|
|
1570
|
+
const link = await (0, config_js_1.readLink)(folder);
|
|
1571
|
+
if (!link) {
|
|
1572
|
+
console.error(red("This folder is not linked to a project on your account."));
|
|
1573
|
+
return 1;
|
|
1574
|
+
}
|
|
1575
|
+
const from = flagText(parsed, "from") ?? (await (0, track_commands_js_1.trackFor)(folder));
|
|
1576
|
+
const into = flagText(parsed, "into") ?? "main";
|
|
1577
|
+
if (from === into) {
|
|
1578
|
+
console.error(red(`This folder is on ${from}. Start a line with cbx track <name> --new, save onto it, then propose it.`));
|
|
1579
|
+
return 1;
|
|
1580
|
+
}
|
|
1581
|
+
const title = flagText(parsed, "message", "m", "title");
|
|
1582
|
+
if (!title) {
|
|
1583
|
+
console.error(red('Give it a title: cbx propose -m "What this changes"'));
|
|
1584
|
+
return 1;
|
|
1585
|
+
}
|
|
1586
|
+
const opened = await (0, api_js_1.openProposal)(link.repositoryId, {
|
|
1587
|
+
from,
|
|
1588
|
+
into,
|
|
1589
|
+
title,
|
|
1590
|
+
...(flagText(parsed, "body") ? { body: flagText(parsed, "body") } : {}),
|
|
1591
|
+
});
|
|
1592
|
+
const reference = opened.mergeTrack.reference;
|
|
1593
|
+
console.log(`${accent(reference)} ${from} → ${into} ${title}`);
|
|
1594
|
+
if (opened.conflicts.settled) {
|
|
1595
|
+
console.log(dim(` ${opened.conflicts.settled} file(s) both sides changed were combined line by line.`));
|
|
1596
|
+
}
|
|
1597
|
+
console.log(opened.conflicts.unresolved
|
|
1598
|
+
? ` ${opened.conflicts.unresolved} file(s) need a decision. Run cbx merge ${reference} to look.`
|
|
1599
|
+
: dim(` Nothing to decide. Run cbx merge ${reference} --apply when it has been reviewed.`));
|
|
1600
|
+
return 0;
|
|
1601
|
+
}
|
|
1602
|
+
async function commandProposals(parsed) {
|
|
1603
|
+
const folder = folderFor(parsed);
|
|
1604
|
+
const link = await (0, config_js_1.readLink)(folder);
|
|
1605
|
+
if (!link) {
|
|
1606
|
+
console.error(red("This folder is not linked to a project on your account."));
|
|
1607
|
+
return 1;
|
|
1608
|
+
}
|
|
1609
|
+
const listed = await (0, api_js_1.proposals)(link.repositoryId, hasFlag(parsed, "all"));
|
|
1610
|
+
if (!listed.length) {
|
|
1611
|
+
console.log(hasFlag(parsed, "all") ? "No proposals yet." : "No open proposals.");
|
|
1612
|
+
return 0;
|
|
1613
|
+
}
|
|
1614
|
+
for (const proposal of listed) {
|
|
1615
|
+
const counts = proposal.conflicts;
|
|
1616
|
+
const waiting = counts && counts.unresolved ? red(`${counts.unresolved} to decide`) : "";
|
|
1617
|
+
console.log(`${accent(proposal.reference)} ${proposal.proposal?.from ?? "?"} → ${proposal.proposal?.into ?? "?"} ` +
|
|
1618
|
+
`${proposal.proposal?.title ?? ""} ${waiting}` +
|
|
1619
|
+
(proposal.state === "open" ? "" : dim(` ${proposal.state}`)));
|
|
1620
|
+
}
|
|
1621
|
+
console.log(dim("\nRun cbx merge <reference> to look at one."));
|
|
1622
|
+
return 0;
|
|
1623
|
+
}
|
|
1550
1624
|
async function commandMerges(parsed) {
|
|
1551
1625
|
const folder = folderFor(parsed);
|
|
1552
1626
|
const link = await (0, config_js_1.readLink)(folder);
|
|
@@ -1593,6 +1667,17 @@ async function commandMerge(parsed) {
|
|
|
1593
1667
|
const folder = folderFor({ ...parsed, positional: parsed.positional.slice(1) });
|
|
1594
1668
|
const summary = await findMerge(folder, reference);
|
|
1595
1669
|
const detail = await (0, api_js_1.mergeTrack)(summary.id);
|
|
1670
|
+
/*
|
|
1671
|
+
Asking people to look. Named the way people name each other, and said
|
|
1672
|
+
back by name, so a typo in a username is visible rather than silent.
|
|
1673
|
+
*/
|
|
1674
|
+
const ask = flagText(parsed, "ask");
|
|
1675
|
+
if (ask) {
|
|
1676
|
+
const asked = await (0, api_js_1.requestReviews)(summary.id, ask.split(",").map((name) => name.trim()).filter(Boolean));
|
|
1677
|
+
console.log(asked.length
|
|
1678
|
+
? `Asked ${asked.map((person) => person.displayName).join(", ")} to review ${summary.reference}.`
|
|
1679
|
+
: "Nobody new was asked.");
|
|
1680
|
+
}
|
|
1596
1681
|
if (hasFlag(parsed, "cancel")) {
|
|
1597
1682
|
await (0, api_js_1.cancelMerge)(summary.id);
|
|
1598
1683
|
console.log(`${summary.reference} cancelled. Your upload is still stored and nothing published was touched.`);
|
|
@@ -1629,6 +1714,32 @@ async function commandMerge(parsed) {
|
|
|
1629
1714
|
}
|
|
1630
1715
|
}
|
|
1631
1716
|
const now = await (0, api_js_1.mergeTrack)(summary.id);
|
|
1717
|
+
if (now.proposal) {
|
|
1718
|
+
console.log(`\n${accent(now.mergeTrack.reference)} ${now.proposal.from} → ${now.proposal.into}` +
|
|
1719
|
+
(now.changeRequest ? ` ${now.changeRequest.title}` : ""));
|
|
1720
|
+
const approvals = now.changeRequest?.approvals;
|
|
1721
|
+
if (approvals && approvals.required) {
|
|
1722
|
+
console.log(` ${approvals.satisfied ? accent("approved") : red("waiting for review")}` +
|
|
1723
|
+
dim(` · ${approvals.approvals} of ${approvals.required} approvals`));
|
|
1724
|
+
}
|
|
1725
|
+
for (const blocked of now.provisional.blockedByChecks ?? []) {
|
|
1726
|
+
console.log(` ${red("check")} ${blocked}`);
|
|
1727
|
+
}
|
|
1728
|
+
for (const one of now.changeRequest?.reviews ?? []) {
|
|
1729
|
+
if (one.verdict === "comment" && !one.body)
|
|
1730
|
+
continue;
|
|
1731
|
+
const said = one.verdict === "approve"
|
|
1732
|
+
? accent("approved")
|
|
1733
|
+
: one.verdict === "request_changes"
|
|
1734
|
+
? red("asked for changes")
|
|
1735
|
+
: dim("commented");
|
|
1736
|
+
console.log(` ${one.reviewerName ?? "A reviewer"} ${said}${one.body ? dim(`: ${one.body}`) : ""}`);
|
|
1737
|
+
}
|
|
1738
|
+
const waiting = (now.changeRequest?.requested ?? []).filter((person) => !person.reviewed);
|
|
1739
|
+
if (waiting.length) {
|
|
1740
|
+
console.log(dim(` waiting on ${waiting.map((person) => person.displayName).join(", ")}`));
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1632
1743
|
console.log(`
|
|
1633
1744
|
${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
|
|
1634
1745
|
(now.provisional.ready
|
|
@@ -1646,10 +1757,14 @@ ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
|
|
|
1646
1757
|
const applied = await (0, api_js_1.applyMerge)(summary.id);
|
|
1647
1758
|
console.log(`
|
|
1648
1759
|
Applied as ${accent(`v${applied.version.sequence}`)}.`);
|
|
1760
|
+
if (applied.closedIssues?.length) {
|
|
1761
|
+
console.log(`Closed ${applied.closedIssues.map((number) => `#${number}`).join(", ")}.`);
|
|
1762
|
+
}
|
|
1649
1763
|
console.log(dim("Run cbx get to bring it down to this folder."));
|
|
1650
1764
|
return 0;
|
|
1651
1765
|
}
|
|
1652
|
-
|
|
1766
|
+
const stillOpen = now.conflicts.some((conflict) => !conflict.resolvedAt);
|
|
1767
|
+
if (!decision && stillOpen) {
|
|
1653
1768
|
console.log(dim(`
|
|
1654
1769
|
--mine keeps yours, --theirs keeps what was already saved,` +
|
|
1655
1770
|
` --drop removes the file.
|
|
@@ -2197,7 +2312,7 @@ const SPECS = [
|
|
|
2197
2312
|
detail: "A signed POST to an address of yours when something happens here.\n" +
|
|
2198
2313
|
"The signing secret is generated by the service and shown once, when\n" +
|
|
2199
2314
|
"the hook is added — no route hands it back afterwards.\n\n" +
|
|
2200
|
-
"Events: version.published, merge.opened, merge.applied,\n" +
|
|
2315
|
+
"Events: version.published, merge.opened, merge.applied, merge.cancelled,\n" +
|
|
2201
2316
|
"check.reported, change_request.opened, change_request.reviewed.\n" +
|
|
2202
2317
|
"Naming none of them sends all of them.",
|
|
2203
2318
|
options: [
|
|
@@ -2466,12 +2581,16 @@ const SPECS = [
|
|
|
2466
2581
|
options: [
|
|
2467
2582
|
{ flags: "--ship <action>", description: "its builds become downloads on the version" },
|
|
2468
2583
|
{ flags: "--no-ship <action>", description: "keep its files without attaching them" },
|
|
2584
|
+
{ flags: "--on-save <action>", description: "run it on every save" },
|
|
2585
|
+
{ flags: "--on-proposal <action>", description: "run it on proposals; its result becomes a check" },
|
|
2586
|
+
{ flags: "--by-hand <action>", description: "run it only when somebody starts it" },
|
|
2469
2587
|
{ flags: "--project <name>", description: "which project" },
|
|
2470
2588
|
],
|
|
2471
2589
|
examples: [
|
|
2472
2590
|
"cbx actions",
|
|
2473
2591
|
'cbx actions --ship "Windows installer"',
|
|
2474
2592
|
'cbx actions --no-ship "Windows installer"',
|
|
2593
|
+
'cbx actions --on-proposal Tests',
|
|
2475
2594
|
],
|
|
2476
2595
|
run: service_commands_js_1.commandWorkflows,
|
|
2477
2596
|
},
|
|
@@ -2552,6 +2671,31 @@ const SPECS = [
|
|
|
2552
2671
|
options: [{ flags: "--revoke <id>", description: "stop one working now" }],
|
|
2553
2672
|
run: service_commands_js_1.commandTokens,
|
|
2554
2673
|
},
|
|
2674
|
+
{
|
|
2675
|
+
name: "propose",
|
|
2676
|
+
group: "Working with a folder",
|
|
2677
|
+
summary: "offer this folder's line to main",
|
|
2678
|
+
usage: 'propose -m "title" [--into main] [--body text] [folder]',
|
|
2679
|
+
options: [
|
|
2680
|
+
{ flags: "-m, --message <text>", description: "the proposal's title" },
|
|
2681
|
+
{ flags: "--into <line>", description: "the line to propose into (default main)" },
|
|
2682
|
+
{ flags: "--from <line>", description: "propose this line instead of the folder's" },
|
|
2683
|
+
{ flags: "--body <text>", description: "a longer description" },
|
|
2684
|
+
],
|
|
2685
|
+
detail: "Opens a proposal from the line this folder is on into main (or --into).\n" +
|
|
2686
|
+
"Files both sides changed are combined line by line where they can be;\n" +
|
|
2687
|
+
"the rest wait for a decision in cbx merge. \"Fixes #12\" in the title or\n" +
|
|
2688
|
+
"body closes issue 12 when the proposal lands.",
|
|
2689
|
+
run: commandPropose,
|
|
2690
|
+
},
|
|
2691
|
+
{
|
|
2692
|
+
name: "proposals",
|
|
2693
|
+
group: "Working with a folder",
|
|
2694
|
+
summary: "open proposals on this project",
|
|
2695
|
+
usage: "proposals [--all] [folder]",
|
|
2696
|
+
options: [{ flags: "--all", description: "include merged and cancelled ones" }],
|
|
2697
|
+
run: commandProposals,
|
|
2698
|
+
},
|
|
2555
2699
|
{
|
|
2556
2700
|
name: "merges",
|
|
2557
2701
|
group: "When somebody saved first",
|
|
@@ -2571,6 +2715,7 @@ const SPECS = [
|
|
|
2571
2715
|
{ flags: "--theirs", description: "keep theirs" },
|
|
2572
2716
|
{ flags: "--both", description: "keep both" },
|
|
2573
2717
|
{ flags: "--drop", description: "abandon the upload" },
|
|
2718
|
+
{ flags: "--ask <usernames>", description: "ask people to review it, comma separated" },
|
|
2574
2719
|
],
|
|
2575
2720
|
run: commandMerge,
|
|
2576
2721
|
},
|
|
@@ -314,6 +314,46 @@ async function commandWorkflows(parsed) {
|
|
|
314
314
|
}
|
|
315
315
|
console.log("");
|
|
316
316
|
}
|
|
317
|
+
/*
|
|
318
|
+
When an action runs. A trigger used to be a label nothing acted on; now a
|
|
319
|
+
save or a proposal really does start the actions that asked for it, so
|
|
320
|
+
choosing is worth a flag.
|
|
321
|
+
*/
|
|
322
|
+
const WHEN = [
|
|
323
|
+
["on-save", "version_saved", "runs on every save"],
|
|
324
|
+
["on-proposal", "merge_opened", "runs when a line is proposed, and again on each save to it"],
|
|
325
|
+
["by-hand", "manual", "runs when somebody starts it"],
|
|
326
|
+
];
|
|
327
|
+
for (const [flag, trigger, meaning] of WHEN) {
|
|
328
|
+
const named = parsed.flags.get(flag);
|
|
329
|
+
if (named === undefined)
|
|
330
|
+
continue;
|
|
331
|
+
if (typeof named !== "string" || !named.trim()) {
|
|
332
|
+
console.error(red("Which action?") + dim(` cbx actions --${flag} "Tests"`));
|
|
333
|
+
return 1;
|
|
334
|
+
}
|
|
335
|
+
const target = found.find((workflow) => workflow.name.toLowerCase() === named.trim().toLowerCase());
|
|
336
|
+
if (!target) {
|
|
337
|
+
console.error(red(`${project.name} has no action called ${named.trim()}.`));
|
|
338
|
+
if (found.length) {
|
|
339
|
+
console.error(dim(` It has: ${found.map((one) => one.name).join(", ")}`));
|
|
340
|
+
}
|
|
341
|
+
return 1;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
await (0, api_js_1.changeWorkflow)(project.id, target.id, { trigger });
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
348
|
+
return 1;
|
|
349
|
+
}
|
|
350
|
+
target.trigger = trigger;
|
|
351
|
+
console.log(green(`${target.name} ${meaning}.`));
|
|
352
|
+
if (trigger === "merge_opened") {
|
|
353
|
+
console.log(dim(" Its result becomes a required check on the proposal."));
|
|
354
|
+
}
|
|
355
|
+
console.log("");
|
|
356
|
+
}
|
|
317
357
|
if (!found.length) {
|
|
318
358
|
console.log(dim("No actions on this project."));
|
|
319
359
|
return 0;
|
|
@@ -256,8 +256,36 @@ class Tracks {
|
|
|
256
256
|
ready: body.provisional?.ready ?? false,
|
|
257
257
|
unresolvedPaths: body.provisional?.unresolvedPaths ?? [],
|
|
258
258
|
blockedByChecks: body.provisional?.blockedByChecks ?? [],
|
|
259
|
+
proposal: body.proposal ?? null,
|
|
260
|
+
request: body.changeRequest
|
|
261
|
+
? { ...body.changeRequest, requested: body.changeRequest.requested ?? [] }
|
|
262
|
+
: null,
|
|
259
263
|
};
|
|
260
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Offer a line to another, usually main.
|
|
267
|
+
*
|
|
268
|
+
* The service reads both lines, combines what it can, and answers with the
|
|
269
|
+
* merge it opened — which the window then shows like any other.
|
|
270
|
+
*/
|
|
271
|
+
async propose(repositoryId, input) {
|
|
272
|
+
const body = await this.call(`/v1/repositories/${repositoryId}/proposals`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
body: JSON.stringify(input.body ? input : { from: input.from, into: input.into, title: input.title }),
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
mergeTrackId: body.mergeTrack.id,
|
|
278
|
+
reference: body.mergeTrack.reference,
|
|
279
|
+
unresolved: body.conflicts.unresolved,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/** Say where you stand on a proposal. */
|
|
283
|
+
async review(mergeTrackId, verdict, note) {
|
|
284
|
+
await this.call(`/v1/merge-tracks/${mergeTrackId}/review`, {
|
|
285
|
+
method: "PUT",
|
|
286
|
+
body: JSON.stringify(note ? { verdict, body: note } : { verdict }),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
261
289
|
/**
|
|
262
290
|
* One side of a conflict, as text.
|
|
263
291
|
*
|