@coderook/cli 0.25.3 → 0.26.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/.claude-plugin/plugin.json +1 -1
- package/dist/cli/src/api.js +156 -6
- package/dist/cli/src/cli.js +170 -1
- package/dist/cli/src/git_history.js +10 -4
- package/dist/cli/src/git_remote.js +136 -12
- package/dist/cli/src/git_remote_bin.js +33 -3
- package/dist/cli/src/project_commands.js +23 -1
- package/dist/cli/src/publish.js +9 -0
- package/dist/cli/src/service_commands.js +2 -1
- package/dist/cli/src/version_commands.js +493 -0
- package/dist/desktop-app/src/main/secret_patterns.js +75 -0
- package/dist/desktop-app/src/main/tracks.js +58 -0
- package/dist/desktop-app/src/main/upload.js +2 -0
- package/dist/desktop-app/src/main/worktree.js +79 -47
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +24 -0
|
@@ -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.
|
|
5
|
+
"version": "0.26.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/api.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.whoami = whoami;
|
|
4
4
|
exports.projects = projects;
|
|
5
|
+
exports.findPublicProject = findPublicProject;
|
|
5
6
|
exports.findProject = findProject;
|
|
6
7
|
exports.health = health;
|
|
7
8
|
exports.mergeTracks = mergeTracks;
|
|
@@ -27,6 +28,14 @@ exports.runLogs = runLogs;
|
|
|
27
28
|
exports.tokens = tokens;
|
|
28
29
|
exports.revokeToken = revokeToken;
|
|
29
30
|
exports.deleteProject = deleteProject;
|
|
31
|
+
exports.projectLabels = projectLabels;
|
|
32
|
+
exports.createProjectLabel = createProjectLabel;
|
|
33
|
+
exports.deleteProjectLabel = deleteProjectLabel;
|
|
34
|
+
exports.changeVersion = changeVersion;
|
|
35
|
+
exports.reviewVersion = reviewVersion;
|
|
36
|
+
exports.removeVersionContent = removeVersionContent;
|
|
37
|
+
exports.versionAttachments = versionAttachments;
|
|
38
|
+
exports.undoTo = undoTo;
|
|
30
39
|
/** The small part of the API the command-line tool needs directly. */
|
|
31
40
|
const identify_js_1 = require("../../desktop-app/src/main/identify.js");
|
|
32
41
|
const config_js_1 = require("./config.js");
|
|
@@ -76,13 +85,77 @@ async function projects() {
|
|
|
76
85
|
updatedAt: String(row.updatedAt ?? row.createdAt ?? ""),
|
|
77
86
|
}));
|
|
78
87
|
}
|
|
79
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* One public project, by the two names that identify it.
|
|
90
|
+
*
|
|
91
|
+
* Reachable without an account, which is what public means. Only the lookup
|
|
92
|
+
* goes this way: once the project has been named, everything else is fetched
|
|
93
|
+
* through the ordinary repository routes, which already serve a public
|
|
94
|
+
* project to anybody who asks.
|
|
95
|
+
*/
|
|
96
|
+
async function findPublicProject(owner, slug) {
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(`${(0, config_js_1.apiOrigin)()}/v1/public/projects/${encodeURIComponent(owner)}/${encodeURIComponent(slug)}`, { headers: { accept: "application/json", ...(0, identify_js_1.clientHeaders)() } });
|
|
99
|
+
if (!response.ok)
|
|
100
|
+
return null;
|
|
101
|
+
const row = (await response.json());
|
|
102
|
+
if (!row.id)
|
|
103
|
+
return null;
|
|
104
|
+
return {
|
|
105
|
+
id: String(row.id),
|
|
106
|
+
slug: String(row.slug ?? slug),
|
|
107
|
+
name: String(row.displayName || row.slug || slug),
|
|
108
|
+
visibility: String(row.visibility ?? "public"),
|
|
109
|
+
defaultBranch: String(row.defaultBranch || "main"),
|
|
110
|
+
versionCount: Number(row.versionCount ?? 0),
|
|
111
|
+
fileCount: Number(row.fileCount ?? 0),
|
|
112
|
+
storedBytes: Number(row.storedSize ?? 0),
|
|
113
|
+
updatedAt: String(row.updatedAt ?? row.createdAt ?? ""),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* Offline, or no such project. Either way there is nothing to return. */
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Find a project by slug or display name, so either reads naturally.
|
|
123
|
+
*
|
|
124
|
+
* Your own account first, then — for `owner/slug`, or a name that is not
|
|
125
|
+
* yours — the public listing.
|
|
126
|
+
*
|
|
127
|
+
* Without that second step the only projects reachable by name were your
|
|
128
|
+
* own, which made `cbx clone somebody/their-project` answer "No project named
|
|
129
|
+
* … on this account" and `git clone coderook://somebody/their-project` hand
|
|
130
|
+
* back an empty repository. Both are the instruction printed on every public
|
|
131
|
+
* project page, aimed at exactly the people who do not own the thing.
|
|
132
|
+
*/
|
|
80
133
|
async function findProject(reference) {
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
134
|
+
const trimmed = reference.trim();
|
|
135
|
+
const slash = trimmed.lastIndexOf("/");
|
|
136
|
+
const owner = slash > 0 ? trimmed.slice(0, slash) : "";
|
|
137
|
+
const bare = (slash > 0 ? trimmed.slice(slash + 1) : trimmed).toLowerCase();
|
|
138
|
+
/*
|
|
139
|
+
An owner was named, so it is somebody's project rather than a name to
|
|
140
|
+
guess at. Yours is still checked first: naming yourself is allowed, and
|
|
141
|
+
the authenticated listing knows about private projects the public one
|
|
142
|
+
cannot see.
|
|
143
|
+
*/
|
|
144
|
+
const all = await projects().catch(() => []);
|
|
145
|
+
const ownersMatch = (project) => project.slug.toLowerCase() === bare || project.name.toLowerCase() === bare;
|
|
146
|
+
if (!owner) {
|
|
147
|
+
const mine = all.find(ownersMatch);
|
|
148
|
+
if (mine)
|
|
149
|
+
return mine;
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const me = await whoami().catch(() => null);
|
|
153
|
+
if (me && (me.username ?? "").toLowerCase() === owner.toLowerCase()) {
|
|
154
|
+
const mine = all.find(ownersMatch);
|
|
155
|
+
if (mine)
|
|
156
|
+
return mine;
|
|
157
|
+
}
|
|
158
|
+
return findPublicProject(owner, bare);
|
|
86
159
|
}
|
|
87
160
|
/** Whether the service is reachable, and what encodings it accepts. */
|
|
88
161
|
async function health() {
|
|
@@ -177,6 +250,25 @@ async function versions(repositoryId) {
|
|
|
177
250
|
? row.parentVersionIds.map(String)
|
|
178
251
|
: [],
|
|
179
252
|
authorName: String(row.author?.displayName ?? ""),
|
|
253
|
+
name: row.name == null ? null : String(row.name),
|
|
254
|
+
/*
|
|
255
|
+
Falls back to the older field, so this keeps working against a service
|
|
256
|
+
that has not been updated yet rather than reporting everything private.
|
|
257
|
+
*/
|
|
258
|
+
visibility: row.visibility ??
|
|
259
|
+
(row.public === false ? "private" : "public"),
|
|
260
|
+
state: String(row.state ?? "verified"),
|
|
261
|
+
pinned: row.pinned === true,
|
|
262
|
+
labels: Array.isArray(row.labels)
|
|
263
|
+
? row.labels.map((one) => ({
|
|
264
|
+
id: String(one.id ?? ""),
|
|
265
|
+
name: String(one.name ?? ""),
|
|
266
|
+
colour: String(one.colour ?? "#888888"),
|
|
267
|
+
description: one.description == null ? null : String(one.description),
|
|
268
|
+
}))
|
|
269
|
+
: [],
|
|
270
|
+
removedAt: row.removedAt == null ? null : String(row.removedAt),
|
|
271
|
+
head: row.head === true,
|
|
180
272
|
}));
|
|
181
273
|
}
|
|
182
274
|
/**
|
|
@@ -345,3 +437,61 @@ async function deleteProject(repositoryId) {
|
|
|
345
437
|
method: "DELETE",
|
|
346
438
|
});
|
|
347
439
|
}
|
|
440
|
+
async function projectLabels(repositoryId) {
|
|
441
|
+
const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/labels`);
|
|
442
|
+
return (body.labels ?? []).map((row) => ({
|
|
443
|
+
id: String(row.id ?? ""),
|
|
444
|
+
name: String(row.name ?? ""),
|
|
445
|
+
colour: String(row.colour ?? "#888888"),
|
|
446
|
+
description: row.description == null ? null : String(row.description),
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
async function createProjectLabel(repositoryId, name, colour, description) {
|
|
450
|
+
const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/labels`, { method: "POST", body: { name, colour, ...(description ? { description } : {}) } });
|
|
451
|
+
return {
|
|
452
|
+
id: String(body.label?.id ?? ""),
|
|
453
|
+
name: String(body.label?.name ?? name),
|
|
454
|
+
colour: String(body.label?.colour ?? colour),
|
|
455
|
+
description: body.label?.description == null ? null : String(body.label.description),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
async function deleteProjectLabel(repositoryId, labelId) {
|
|
459
|
+
await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/labels/${encodeURIComponent(labelId)}`, { method: "DELETE" });
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Everything about a version, changed in one call.
|
|
463
|
+
*
|
|
464
|
+
* Deliberately one request rather than one per field. The service takes a
|
|
465
|
+
* patch, so a command that sets a name and a colour at once is one round trip
|
|
466
|
+
* and, more to the point, one thing that either happened or did not.
|
|
467
|
+
*/
|
|
468
|
+
async function changeVersion(repositoryId, versionId, patch) {
|
|
469
|
+
await call(`/v1/repositories/${encodeURIComponent(repositoryId)}` +
|
|
470
|
+
`/versions/${encodeURIComponent(versionId)}`, { method: "PATCH", body: patch });
|
|
471
|
+
}
|
|
472
|
+
/** Accept a held version, or turn it down with a reason. */
|
|
473
|
+
async function reviewVersion(repositoryId, versionId, decision, note) {
|
|
474
|
+
const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}` +
|
|
475
|
+
`/versions/${encodeURIComponent(versionId)}/review`, { method: "POST", body: { decision, ...(note ? { note } : {}) } });
|
|
476
|
+
return { state: String(body.state ?? decision) };
|
|
477
|
+
}
|
|
478
|
+
/** Take a version's content down, leaving the version itself as a record. */
|
|
479
|
+
async function removeVersionContent(repositoryId, versionId, reason) {
|
|
480
|
+
const query = reason ? `?reason=${encodeURIComponent(reason)}` : "";
|
|
481
|
+
await call(`/v1/repositories/${encodeURIComponent(repositoryId)}` +
|
|
482
|
+
`/versions/${encodeURIComponent(versionId)}/content${query}`, { method: "DELETE" });
|
|
483
|
+
}
|
|
484
|
+
async function versionAttachments(repositoryId, versionId) {
|
|
485
|
+
const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}` +
|
|
486
|
+
`/versions/${encodeURIComponent(versionId)}/attachments`);
|
|
487
|
+
return (body.attachments ?? []).map((row) => ({
|
|
488
|
+
id: String(row.id ?? ""),
|
|
489
|
+
name: String(row.name ?? ""),
|
|
490
|
+
sizeBytes: Number(row.sizeBytes ?? 0),
|
|
491
|
+
mediaType: String(row.mediaType ?? "application/octet-stream"),
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
/** Put the project back on an earlier commit. */
|
|
495
|
+
async function undoTo(repositoryId, to) {
|
|
496
|
+
return await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/undo`, { method: "POST", body: to ? { to } : {} });
|
|
497
|
+
}
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
|
|
|
37
37
|
const upload_js_1 = require("../../desktop-app/src/main/upload.js");
|
|
38
38
|
const download_js_1 = require("../../desktop-app/src/main/download.js");
|
|
39
39
|
const faults_js_1 = require("../../desktop-app/src/main/faults.js");
|
|
40
|
+
const version_commands_js_1 = require("./version_commands.js");
|
|
40
41
|
const identify_js_1 = require("../../desktop-app/src/main/identify.js");
|
|
41
42
|
const detect_js_1 = require("../../desktop-app/src/main/detect.js");
|
|
42
43
|
const cbx_js_1 = require("../../desktop-app/src/main/cbx.js");
|
|
@@ -637,6 +638,15 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
637
638
|
would reject the publication and the import would be lossy.
|
|
638
639
|
*/
|
|
639
640
|
...(hasFlag(parsed, "allow-ignored") ? { allowIgnored: true } : {}),
|
|
641
|
+
/*
|
|
642
|
+
The same answer, told to the service.
|
|
643
|
+
|
|
644
|
+
The check above is the one that asks a person, and it is the better
|
|
645
|
+
place to ask — it is where the files are. But it is also the part an
|
|
646
|
+
old build or a patched one does not run, so the service asks again and
|
|
647
|
+
refuses unless this says the question was put and answered.
|
|
648
|
+
*/
|
|
649
|
+
...(hasFlag(parsed, "allow-secrets") ? { allowSecrets: true } : {}),
|
|
640
650
|
...(link ? { known: link.local ?? link.manifest ?? {} } : {}),
|
|
641
651
|
});
|
|
642
652
|
let result;
|
|
@@ -925,7 +935,11 @@ async function commandClone(parsed) {
|
|
|
925
935
|
}
|
|
926
936
|
const project = await (0, api_js_2.findProject)(reference);
|
|
927
937
|
if (!project) {
|
|
928
|
-
console.error(red(`No project named ${reference}
|
|
938
|
+
console.error(red(`No project named ${reference}.` +
|
|
939
|
+
(reference.includes("/")
|
|
940
|
+
? " Check the owner and the name, and that it is public."
|
|
941
|
+
: " It is not on your account — for somebody else's, name them" +
|
|
942
|
+
" too: cbx clone <owner>/<project>.")));
|
|
929
943
|
return 1;
|
|
930
944
|
}
|
|
931
945
|
if (!project.versionCount) {
|
|
@@ -1792,6 +1806,161 @@ const SPECS = [
|
|
|
1792
1806
|
usage: "releases [project]",
|
|
1793
1807
|
run: service_commands_js_1.commandReleases,
|
|
1794
1808
|
},
|
|
1809
|
+
{
|
|
1810
|
+
/*
|
|
1811
|
+
The half that hiding never covered.
|
|
1812
|
+
|
|
1813
|
+
Hiding a bad push stops strangers reading it and leaves the project
|
|
1814
|
+
standing on it, so the next person to pull still lands on the mistake.
|
|
1815
|
+
This moves the project.
|
|
1816
|
+
*/
|
|
1817
|
+
name: "undo",
|
|
1818
|
+
group: "Your projects",
|
|
1819
|
+
summary: "put the project back on an earlier save",
|
|
1820
|
+
usage: "undo [n] [project]",
|
|
1821
|
+
detail: "Goes back one save unless you name another to go back to.\n\n" +
|
|
1822
|
+
"Nothing is deleted and nothing is renumbered — the saves that get\n" +
|
|
1823
|
+
"passed over stay in the history, and the next save carries on from\n" +
|
|
1824
|
+
"wherever the project now stands.",
|
|
1825
|
+
examples: ["cbx undo", "cbx undo 41"],
|
|
1826
|
+
run: version_commands_js_1.commandUndo,
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
/*
|
|
1830
|
+
The decision that separates a working history from a published one.
|
|
1831
|
+
|
|
1832
|
+
Every save is a commit and most of them are nobody else's business. This
|
|
1833
|
+
is where one becomes a version — the thing the public side of a project
|
|
1834
|
+
actually offers.
|
|
1835
|
+
*/
|
|
1836
|
+
name: "promote",
|
|
1837
|
+
group: "Your projects",
|
|
1838
|
+
summary: "turn a commit into a version people can fetch",
|
|
1839
|
+
usage: "promote [n] [project]",
|
|
1840
|
+
detail: "Shows the last ten commits and asks which one. Give a number to skip\n" +
|
|
1841
|
+
"the list.\n\n" +
|
|
1842
|
+
"A version is a commit with a name on it. Until a project promotes its\n" +
|
|
1843
|
+
"first one its public page shows everything, as it always did.",
|
|
1844
|
+
options: [
|
|
1845
|
+
{ flags: "--name <text>", description: "what to call it" },
|
|
1846
|
+
{ flags: "--notes <text>", description: "what changed" },
|
|
1847
|
+
],
|
|
1848
|
+
examples: [
|
|
1849
|
+
"cbx promote",
|
|
1850
|
+
"cbx promote 41 --name v2.1",
|
|
1851
|
+
'cbx promote --name "Client build" --notes "Fixes the export dialog"',
|
|
1852
|
+
],
|
|
1853
|
+
run: version_commands_js_1.commandPromote,
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
/*
|
|
1857
|
+
One command for everything a version can be, because the service takes
|
|
1858
|
+
one patch. Three separate commands would be three requests, and three
|
|
1859
|
+
chances for a person to end up with a version that is named but not
|
|
1860
|
+
pinned because the second one failed.
|
|
1861
|
+
*/
|
|
1862
|
+
/*
|
|
1863
|
+
Not called "version".
|
|
1864
|
+
|
|
1865
|
+
`cbx version` has printed the version number since the first release,
|
|
1866
|
+
and quietly changing that when arguments follow is the kind of thing
|
|
1867
|
+
that works until somebody scripts it. This marks a save — pins it,
|
|
1868
|
+
hides it, labels it — which is what it does anyway.
|
|
1869
|
+
*/
|
|
1870
|
+
name: "mark",
|
|
1871
|
+
aliases: ["set"],
|
|
1872
|
+
group: "Your projects",
|
|
1873
|
+
summary: "name, hide, pin or label one save",
|
|
1874
|
+
usage: "mark [n] [project]",
|
|
1875
|
+
detail: "Changes the newest version unless you name another.\n" +
|
|
1876
|
+
"Everything you ask for happens in one request, so it either all\n" +
|
|
1877
|
+
"lands or none of it does.",
|
|
1878
|
+
options: [
|
|
1879
|
+
{ flags: "--name <text>", description: "give it a code name" },
|
|
1880
|
+
{ flags: "--unname", description: "take the name off again" },
|
|
1881
|
+
{ flags: "--notes <text>", description: "what changed" },
|
|
1882
|
+
{ flags: "--hide", description: "nobody outside the project can read it" },
|
|
1883
|
+
{ flags: "--show", description: "anybody can read it" },
|
|
1884
|
+
{
|
|
1885
|
+
flags: "--visibility <who>",
|
|
1886
|
+
description: "private, unlisted (link only) or public",
|
|
1887
|
+
},
|
|
1888
|
+
{ flags: "--pin", description: "keep it at the top of the list" },
|
|
1889
|
+
{ flags: "--unpin", description: "put it back in order" },
|
|
1890
|
+
{ flags: "--labels <a,b>", description: "the labels it should wear" },
|
|
1891
|
+
{ flags: "--clear-labels", description: "take them all off" },
|
|
1892
|
+
],
|
|
1893
|
+
examples: [
|
|
1894
|
+
"cbx mark --name v2.1 --pin",
|
|
1895
|
+
"cbx mark 41 --visibility unlisted",
|
|
1896
|
+
'cbx mark 41 --labels "shipped,client work"',
|
|
1897
|
+
],
|
|
1898
|
+
run: version_commands_js_1.commandVersion,
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
name: "labels",
|
|
1902
|
+
group: "Your projects",
|
|
1903
|
+
summary: "the labels this project puts on versions",
|
|
1904
|
+
usage: "labels [list|add|remove] [name] [colour]",
|
|
1905
|
+
detail: "A label is a name and a colour. Put as many on a version as you like —\n" +
|
|
1906
|
+
"how you group and colour your own history is yours to decide.",
|
|
1907
|
+
examples: [
|
|
1908
|
+
"cbx labels",
|
|
1909
|
+
"cbx labels add shipped green",
|
|
1910
|
+
"cbx labels add urgent #e0574a",
|
|
1911
|
+
"cbx labels remove shipped",
|
|
1912
|
+
],
|
|
1913
|
+
run: version_commands_js_1.commandLabels,
|
|
1914
|
+
},
|
|
1915
|
+
{
|
|
1916
|
+
name: "held",
|
|
1917
|
+
aliases: ["waiting"],
|
|
1918
|
+
group: "Your projects",
|
|
1919
|
+
summary: "versions waiting for somebody to approve them",
|
|
1920
|
+
usage: "held [project]",
|
|
1921
|
+
detail: "A project can be set to hold pushes from anyone below admin until an\n" +
|
|
1922
|
+
"admin accepts them. Those versions are real and stored — they are just\n" +
|
|
1923
|
+
"not the current version, and not on the public page.",
|
|
1924
|
+
run: version_commands_js_1.commandHeld,
|
|
1925
|
+
},
|
|
1926
|
+
{
|
|
1927
|
+
name: "review",
|
|
1928
|
+
group: "Your projects",
|
|
1929
|
+
summary: "accept a held version, or turn it down",
|
|
1930
|
+
usage: "review <n> --approve | --decline",
|
|
1931
|
+
options: [
|
|
1932
|
+
{
|
|
1933
|
+
flags: "--approve",
|
|
1934
|
+
description: "accept it; it becomes the current version",
|
|
1935
|
+
},
|
|
1936
|
+
{ flags: "--decline", description: "turn it down" },
|
|
1937
|
+
{ flags: "--note <text>", description: "why" },
|
|
1938
|
+
],
|
|
1939
|
+
examples: [
|
|
1940
|
+
"cbx review 41 --approve",
|
|
1941
|
+
'cbx review 41 --decline --note "wrong branch"',
|
|
1942
|
+
],
|
|
1943
|
+
run: version_commands_js_1.commandReview,
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
/*
|
|
1947
|
+
Named for what it does rather than "delete", because it does not delete
|
|
1948
|
+
the version. The files go; the version stays in the history as a gap
|
|
1949
|
+
saying what was removed and by whom.
|
|
1950
|
+
*/
|
|
1951
|
+
name: "take-down",
|
|
1952
|
+
group: "Your projects",
|
|
1953
|
+
summary: "remove a version's files, for something published by mistake",
|
|
1954
|
+
usage: "take-down <n> --yes",
|
|
1955
|
+
detail: "The files go and do not come back. The version stays in the history as\n" +
|
|
1956
|
+
"a gap saying what was removed and by whom, so nothing is quietly\n" +
|
|
1957
|
+
"rewritten.",
|
|
1958
|
+
options: [
|
|
1959
|
+
{ flags: "--yes", description: "confirm; without it nothing happens" },
|
|
1960
|
+
{ flags: "--reason <text>", description: "recorded against the gap" },
|
|
1961
|
+
],
|
|
1962
|
+
run: version_commands_js_1.commandTakeDown,
|
|
1963
|
+
},
|
|
1795
1964
|
{
|
|
1796
1965
|
/*
|
|
1797
1966
|
A release is a version with a name on it, so this names one rather than
|
|
@@ -51,14 +51,20 @@ function parseRemoteUrl(url) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
rest = rest.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
/*
|
|
55
|
+
The owner is carried, and what it is used for differs by direction.
|
|
56
|
+
Fetching honours it, because `coderook://somebody/their-project` is the
|
|
57
|
+
line printed on every public project page and it has to reach their
|
|
58
|
+
project rather than look for that name on yours. Pushing still ignores
|
|
59
|
+
it: the token says who you are, and quietly publishing to an account the
|
|
60
|
+
URL named instead would be the worse mistake of the two.
|
|
61
|
+
*/
|
|
57
62
|
const parts = rest.split("/").filter(Boolean);
|
|
58
63
|
const slug = parts[parts.length - 1] ?? "";
|
|
64
|
+
const owner = parts.length > 1 ? parts[parts.length - 2] : "";
|
|
59
65
|
if (!slug)
|
|
60
66
|
throw new Error(`Not a CodeRook remote URL: ${url}`);
|
|
61
|
-
return { slug };
|
|
67
|
+
return { owner, slug };
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* Pull the git commit id back out of a version message, if it carries one.
|
|
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.AlreadyReported = void 0;
|
|
39
40
|
exports.main = main;
|
|
40
41
|
/**
|
|
41
42
|
* `git push coderook` — a git remote helper.
|
|
@@ -110,6 +111,20 @@ const version_js_1 = require("./version.js");
|
|
|
110
111
|
const publish_js_1 = require("./publish.js");
|
|
111
112
|
const git_history_js_1 = require("./git_history.js");
|
|
112
113
|
const api_js_1 = require("./api.js");
|
|
114
|
+
/** A fetch that cannot be served. Carried out so the process can exit non-zero. */
|
|
115
|
+
class ImportFailed extends Error {
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A failure this code has already explained.
|
|
119
|
+
*
|
|
120
|
+
* The entry point prints whatever reaches it, which is right for a surprise
|
|
121
|
+
* and wrong for a refusal that has just been set out in full — the person
|
|
122
|
+
* would read the same paragraph twice, the second time with a program name
|
|
123
|
+
* in front of it.
|
|
124
|
+
*/
|
|
125
|
+
class AlreadyReported extends Error {
|
|
126
|
+
}
|
|
127
|
+
exports.AlreadyReported = AlreadyReported;
|
|
113
128
|
/** Everything the helper writes for a person goes to stderr; stdout is protocol. */
|
|
114
129
|
function say(text) {
|
|
115
130
|
node_process_1.default.stderr.write(`${text}\n`);
|
|
@@ -489,12 +504,19 @@ async function recordPushedCommits(marks) {
|
|
|
489
504
|
* unchanged across a thousand versions is downloaded and sent to git once.
|
|
490
505
|
*/
|
|
491
506
|
async function doImport(refs, url) {
|
|
492
|
-
const { slug } = (0, git_history_js_1.parseRemoteUrl)(url);
|
|
493
|
-
const project = await (0, api_js_1.findProject)(slug);
|
|
507
|
+
const { owner, slug } = (0, git_history_js_1.parseRemoteUrl)(url);
|
|
508
|
+
const project = await (0, api_js_1.findProject)(owner ? `${owner}/${slug}` : slug);
|
|
494
509
|
if (!project) {
|
|
495
|
-
say(`No CodeRook project called "${slug}
|
|
510
|
+
say(`No CodeRook project called "${owner ? `${owner}/${slug}` : slug}",` +
|
|
511
|
+
" or you cannot read it.");
|
|
512
|
+
/*
|
|
513
|
+
`done` closes the stream cleanly, but git reads "no refs" as "an empty
|
|
514
|
+
repository" and reports success — so a clone of something that is not
|
|
515
|
+
there made an empty folder and exited zero. Saying so on the way out is
|
|
516
|
+
what turns that into a failure the person can see.
|
|
517
|
+
*/
|
|
496
518
|
send("done");
|
|
497
|
-
|
|
519
|
+
throw new ImportFailed(`No CodeRook project called "${owner ? `${owner}/${slug}` : slug}", or you cannot read it.`);
|
|
498
520
|
}
|
|
499
521
|
const repositoryId = project.id;
|
|
500
522
|
const state = await remoteState(repositoryId);
|
|
@@ -693,7 +715,34 @@ async function doImport(refs, url) {
|
|
|
693
715
|
}
|
|
694
716
|
}
|
|
695
717
|
async function doPush(requests, url) {
|
|
696
|
-
|
|
718
|
+
/*
|
|
719
|
+
A push never goes to the account named in the URL — it goes to the one the
|
|
720
|
+
token belongs to. Publishing somebody's work to a different account on
|
|
721
|
+
their behalf would be the worse mistake of the two.
|
|
722
|
+
|
|
723
|
+
But it is not enough to ignore the name, now that fetching honours it.
|
|
724
|
+
The same URL would then read from one account and write to another: a
|
|
725
|
+
push to `coderook://somebody/their-project` quietly made a private
|
|
726
|
+
project of that name on your own account and reported success, while git
|
|
727
|
+
printed "To coderook://somebody/their-project". Saying no is the honest
|
|
728
|
+
answer, and it leaves the person a working one.
|
|
729
|
+
*/
|
|
730
|
+
const { owner, slug } = (0, git_history_js_1.parseRemoteUrl)(url);
|
|
731
|
+
if (owner) {
|
|
732
|
+
const me = await (0, api_js_1.whoami)().catch(() => null);
|
|
733
|
+
const mine = (me?.username ?? "").toLowerCase();
|
|
734
|
+
if (mine && owner.toLowerCase() !== mine) {
|
|
735
|
+
say(`This remote names ${owner}, and you are signed in as ${mine}.` +
|
|
736
|
+
` A push goes to your own account, so it would land somewhere the` +
|
|
737
|
+
` URL does not name.`);
|
|
738
|
+
say(`To publish your own copy: git remote set-url origin coderook://${mine}/${slug}`);
|
|
739
|
+
for (const request of requests) {
|
|
740
|
+
send(`error ${request.dst} this remote belongs to ${owner}, not to ${mine}`);
|
|
741
|
+
}
|
|
742
|
+
send("");
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
697
746
|
const marks = await readMarks();
|
|
698
747
|
/*
|
|
699
748
|
Re-read the project before every ref, not once before the batch.
|
|
@@ -1056,6 +1105,40 @@ async function doPush(requests, url) {
|
|
|
1056
1105
|
`);
|
|
1057
1106
|
throw error;
|
|
1058
1107
|
}
|
|
1108
|
+
if (failure?.kind === "credentials") {
|
|
1109
|
+
/*
|
|
1110
|
+
The one refusal a push cannot answer. `cbx submit` can ask and
|
|
1111
|
+
be told yes; git has nowhere to put that question, so the way
|
|
1112
|
+
through is to take the key out — which is the better answer
|
|
1113
|
+
anyway.
|
|
1114
|
+
*/
|
|
1115
|
+
send(`error ${request.dst} this commit carries a credential`);
|
|
1116
|
+
say(`
|
|
1117
|
+
${failure.message}
|
|
1118
|
+
`);
|
|
1119
|
+
/*
|
|
1120
|
+
Deleting the file in a later commit does not help, and saying
|
|
1121
|
+
"take it out and commit again" sends people in circles: a push
|
|
1122
|
+
publishes every commit as its own version, so the commit that
|
|
1123
|
+
introduced the key still carries it however many commits follow.
|
|
1124
|
+
The history has to lose it.
|
|
1125
|
+
*/
|
|
1126
|
+
say(` A later commit that deletes it is not enough — every commit` +
|
|
1127
|
+
` being pushed
|
|
1128
|
+
becomes a version, and the one that added` +
|
|
1129
|
+
` the key still carries it.
|
|
1130
|
+
` +
|
|
1131
|
+
` Rewrite it out (git rebase -i, or git commit --amend if it` +
|
|
1132
|
+
` is the last one),
|
|
1133
|
+
or publish this deliberately with` +
|
|
1134
|
+
` cbx submit --allow-secrets.
|
|
1135
|
+
`);
|
|
1136
|
+
/*
|
|
1137
|
+
Reported already, and in more detail than the wrapper can. The
|
|
1138
|
+
marker stops the bin printing the same paragraph a second time.
|
|
1139
|
+
*/
|
|
1140
|
+
throw new AlreadyReported(failure.message);
|
|
1141
|
+
}
|
|
1059
1142
|
if (failure?.kind === "conflict") {
|
|
1060
1143
|
send(`error ${request.dst} somebody published to "${branch}" while this push was running`);
|
|
1061
1144
|
say(`
|
|
@@ -1124,6 +1207,12 @@ async function main(argv) {
|
|
|
1124
1207
|
(0, identify_js_1.declareClient)("cli", version_js_1.VERSION);
|
|
1125
1208
|
const pending = [];
|
|
1126
1209
|
const importing = [];
|
|
1210
|
+
/*
|
|
1211
|
+
Whether a fetch asked for something that is not there. Git reads "no
|
|
1212
|
+
refs" as "an empty repository" and reports success, so without this a
|
|
1213
|
+
clone of a project you cannot see left an empty folder and exited zero.
|
|
1214
|
+
*/
|
|
1215
|
+
let unservable = false;
|
|
1127
1216
|
for await (const line of lines()) {
|
|
1128
1217
|
const command = line.trim();
|
|
1129
1218
|
if (command === "capabilities") {
|
|
@@ -1144,8 +1233,35 @@ async function main(argv) {
|
|
|
1144
1233
|
}
|
|
1145
1234
|
if (command === "list" || command === "list for-push") {
|
|
1146
1235
|
try {
|
|
1147
|
-
const { slug } = (0, git_history_js_1.parseRemoteUrl)(url);
|
|
1148
|
-
const
|
|
1236
|
+
const { owner, slug } = (0, git_history_js_1.parseRemoteUrl)(url);
|
|
1237
|
+
const named = owner ? `${owner}/${slug}` : slug;
|
|
1238
|
+
const project = await (0, api_js_1.findProject)(named);
|
|
1239
|
+
/*
|
|
1240
|
+
A fetch of something that is not there has to say so here.
|
|
1241
|
+
|
|
1242
|
+
Git asks `list` first and only asks to import the refs it is
|
|
1243
|
+
offered, so a project nobody can read never reaches the import at
|
|
1244
|
+
all — it is simply a listing with nothing in it, which git reports
|
|
1245
|
+
as an empty repository and calls a success. Refusing at this point
|
|
1246
|
+
is the only place the answer can still be "no".
|
|
1247
|
+
|
|
1248
|
+
`list for-push` is exempt: pushing to a name that does not exist
|
|
1249
|
+
yet is how a project gets created.
|
|
1250
|
+
*/
|
|
1251
|
+
if (command === "list" && !project) {
|
|
1252
|
+
say(`No CodeRook project called "${named}", or you cannot read it.`);
|
|
1253
|
+
unservable = true;
|
|
1254
|
+
/*
|
|
1255
|
+
The listing is deliberately left unterminated.
|
|
1256
|
+
|
|
1257
|
+
An empty but well-formed list is a valid answer meaning "an empty
|
|
1258
|
+
repository", and git takes it as one: it reports success, leaves
|
|
1259
|
+
a folder with nothing but .git in it, and ignores whatever the
|
|
1260
|
+
helper exits with. Ending the conversation instead is the only
|
|
1261
|
+
answer git reads as a failure.
|
|
1262
|
+
*/
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1149
1265
|
if (command === "list for-push") {
|
|
1150
1266
|
const known = await remoteRefs(project?.id ?? null);
|
|
1151
1267
|
for (const [ref, sha] of known)
|
|
@@ -1228,10 +1344,18 @@ async function main(argv) {
|
|
|
1228
1344
|
await doImport(batch, url);
|
|
1229
1345
|
}
|
|
1230
1346
|
catch (error) {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1347
|
+
/*
|
|
1348
|
+
A project that could not be found has already said so and closed
|
|
1349
|
+
the stream. Anything else has not, and fast-import would wait on
|
|
1350
|
+
a stream that never ends.
|
|
1351
|
+
*/
|
|
1352
|
+
if (error instanceof ImportFailed) {
|
|
1353
|
+
unservable = true;
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
say(`Import failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1357
|
+
send("done");
|
|
1358
|
+
}
|
|
1235
1359
|
}
|
|
1236
1360
|
continue;
|
|
1237
1361
|
}
|
|
@@ -1254,5 +1378,5 @@ async function main(argv) {
|
|
|
1254
1378
|
continue;
|
|
1255
1379
|
}
|
|
1256
1380
|
}
|
|
1257
|
-
return 0;
|
|
1381
|
+
return unservable ? 1 : 0;
|
|
1258
1382
|
}
|