@coderook/cli 0.25.4 → 0.27.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 +86 -0
- package/dist/cli/src/cli.js +267 -23
- package/dist/cli/src/git_remote.js +100 -15
- package/dist/cli/src/project_commands.js +67 -4
- package/dist/cli/src/service_commands.js +2 -1
- package/dist/cli/src/version_commands.js +652 -0
- package/dist/desktop-app/src/main/download.js +15 -6
- package/dist/desktop-app/src/main/secret_patterns.js +1 -0
- package/dist/desktop-app/src/main/tracks.js +76 -0
- package/dist/desktop-app/src/main/upload.js +5 -0
- package/dist/desktop-app/src/main/worktree.js +26 -8
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +24 -0
|
@@ -47,9 +47,19 @@ exports.main = main;
|
|
|
47
47
|
* `coderook://` URLs, which is what lets every editor's built-in git panel work
|
|
48
48
|
* with CodeRook without any per-editor work.
|
|
49
49
|
*
|
|
50
|
-
* Both directions are implemented: `push` publishes
|
|
51
|
-
* `import` rebuilds a git history from
|
|
52
|
-
* work.
|
|
50
|
+
* Both directions are implemented: `push` publishes each commit as a save, and
|
|
51
|
+
* `import` rebuilds a git history from those saves so `git clone` and `git
|
|
52
|
+
* fetch` work.
|
|
53
|
+
*
|
|
54
|
+
* The vocabulary matters here, because the two products do not agree by
|
|
55
|
+
* accident. A pushed commit becomes a *commit* in CodeRook — a save, inside
|
|
56
|
+
* the project, that nobody outside can see. Tagging it names it, and a named
|
|
57
|
+
* save is a *version*, which is the only thing the public side offers. That
|
|
58
|
+
* is the same distinction git already draws between a commit and a tag, so
|
|
59
|
+
* the mapping is one-to-one rather than an approximation.
|
|
60
|
+
*
|
|
61
|
+
* A save also carries a description of its own, separate from its message.
|
|
62
|
+
* That travels as a git note on `refs/notes/coderook` in both directions.
|
|
53
63
|
*
|
|
54
64
|
* ## Why the `push` capability rather than `export`
|
|
55
65
|
*
|
|
@@ -143,6 +153,26 @@ function git(args) {
|
|
|
143
153
|
}
|
|
144
154
|
return result.stdout;
|
|
145
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Where CodeRook keeps a save's description inside a git repository.
|
|
158
|
+
*
|
|
159
|
+
* `refs/notes/coderook` rather than git's default `refs/notes/commits`,
|
|
160
|
+
* because the default is shared ground — anything else attaching notes writes
|
|
161
|
+
* there too, and a fetch that overwrote somebody's own notes with ours would
|
|
162
|
+
* be taking something that was not offered.
|
|
163
|
+
*/
|
|
164
|
+
const NOTES_REF = "refs/notes/coderook";
|
|
165
|
+
/**
|
|
166
|
+
* The description attached to a commit, if there is one.
|
|
167
|
+
*
|
|
168
|
+
* Empty rather than throwing for every ordinary reason it can be missing: no
|
|
169
|
+
* notes ref at all, no note on this commit, or a repository where notes have
|
|
170
|
+
* never been used. None of those is a problem worth stopping a push for.
|
|
171
|
+
*/
|
|
172
|
+
function noteOn(sha) {
|
|
173
|
+
const result = (0, node_child_process_1.spawnSync)("git", ["notes", `--ref=${NOTES_REF}`, "show", sha], { encoding: "utf8", maxBuffer: 1024 * 1024 * 8 });
|
|
174
|
+
return result.status === 0 ? (result.stdout ?? "").trim() : "";
|
|
175
|
+
}
|
|
146
176
|
/** Run git for its exit status alone. False rather than throwing. */
|
|
147
177
|
function gitOk(args) {
|
|
148
178
|
return ((0, node_child_process_1.spawnSync)("git", args, { encoding: "utf8", stdio: "ignore" }).status === 0);
|
|
@@ -665,6 +695,40 @@ async function doImport(refs, url) {
|
|
|
665
695
|
say(` ${done}/${fresh.length} versions`);
|
|
666
696
|
}
|
|
667
697
|
}
|
|
698
|
+
/*
|
|
699
|
+
What each save says about itself, as git notes.
|
|
700
|
+
|
|
701
|
+
A CodeRook save carries a description separate from its message — the
|
|
702
|
+
thing the website shows as release info and `cbx notes` writes — and a
|
|
703
|
+
fetch used to drop all of it except on the few versions that were also
|
|
704
|
+
releases. Git's answer for text attached to a commit without changing
|
|
705
|
+
the commit is a note, so that is where it goes: `git log
|
|
706
|
+
--notes=coderook` shows them, and pushing them back returns them.
|
|
707
|
+
|
|
708
|
+
Written as one commit on its own ref after every version commit exists,
|
|
709
|
+
because a note has to name a commit that is already in the stream.
|
|
710
|
+
*/
|
|
711
|
+
const described = fresh.filter((version) => (version.notes ?? "").trim());
|
|
712
|
+
if (described.length) {
|
|
713
|
+
send(`commit ${(0, git_history_js_1.importRef)(NOTES_REF)}`);
|
|
714
|
+
send(`committer CodeRook <noreply@coderook.com> ` +
|
|
715
|
+
`${(0, git_history_js_1.stamp)(new Date().toISOString())} +0000`);
|
|
716
|
+
const why = Buffer.from("What each save says about itself\n", "utf8");
|
|
717
|
+
send(`data ${why.length}`);
|
|
718
|
+
node_process_1.default.stdout.write(why);
|
|
719
|
+
node_process_1.default.stdout.write("\n");
|
|
720
|
+
for (const version of described) {
|
|
721
|
+
const mark = commitMark.get(version.id);
|
|
722
|
+
if (mark === undefined)
|
|
723
|
+
continue;
|
|
724
|
+
const body = Buffer.from(`${(version.notes ?? "").trim()}\n`, "utf8");
|
|
725
|
+
send(`N inline :${mark}`);
|
|
726
|
+
send(`data ${body.length}`);
|
|
727
|
+
node_process_1.default.stdout.write(body);
|
|
728
|
+
node_process_1.default.stdout.write("\n");
|
|
729
|
+
}
|
|
730
|
+
say(` ${described.length} description${described.length === 1 ? "" : "s"} as git notes`);
|
|
731
|
+
}
|
|
668
732
|
/* Every ref git asked for, named outright now the commits exist. */
|
|
669
733
|
settleRefs();
|
|
670
734
|
/*
|
|
@@ -856,7 +920,7 @@ async function doPush(requests, url) {
|
|
|
856
920
|
const existing = await releases(repositoryId).catch(() => []);
|
|
857
921
|
const already = existing.find((release) => release.versionId === versionId && release.name !== tag);
|
|
858
922
|
if (already) {
|
|
859
|
-
say(` note: v${already.sequence} was already released as "${already.name}".
|
|
923
|
+
say(` note: v${already.sequence} was already released as "${already.name}".
|
|
860
924
|
` +
|
|
861
925
|
` A version carries one name, so it is now "${tag}".`);
|
|
862
926
|
}
|
|
@@ -1098,10 +1162,10 @@ async function doPush(requests, url) {
|
|
|
1098
1162
|
happened and go looking for a way to undo it.
|
|
1099
1163
|
*/
|
|
1100
1164
|
send(`error ${request.dst} the connection failed part way through`);
|
|
1101
|
-
say(`
|
|
1102
|
-
${published} of ${commits.length} commits were published.
|
|
1165
|
+
say(`
|
|
1166
|
+
${published} of ${commits.length} commits were published.
|
|
1103
1167
|
` +
|
|
1104
|
-
` Push again — commits already published are not sent twice.
|
|
1168
|
+
` Push again — commits already published are not sent twice.
|
|
1105
1169
|
`);
|
|
1106
1170
|
throw error;
|
|
1107
1171
|
}
|
|
@@ -1113,8 +1177,8 @@ async function doPush(requests, url) {
|
|
|
1113
1177
|
anyway.
|
|
1114
1178
|
*/
|
|
1115
1179
|
send(`error ${request.dst} this commit carries a credential`);
|
|
1116
|
-
say(`
|
|
1117
|
-
${failure.message}
|
|
1180
|
+
say(`
|
|
1181
|
+
${failure.message}
|
|
1118
1182
|
`);
|
|
1119
1183
|
/*
|
|
1120
1184
|
Deleting the file in a later commit does not help, and saying
|
|
@@ -1124,14 +1188,14 @@ async function doPush(requests, url) {
|
|
|
1124
1188
|
The history has to lose it.
|
|
1125
1189
|
*/
|
|
1126
1190
|
say(` A later commit that deletes it is not enough — every commit` +
|
|
1127
|
-
` being pushed
|
|
1191
|
+
` being pushed
|
|
1128
1192
|
becomes a version, and the one that added` +
|
|
1129
|
-
` the key still carries it.
|
|
1193
|
+
` the key still carries it.
|
|
1130
1194
|
` +
|
|
1131
1195
|
` Rewrite it out (git rebase -i, or git commit --amend if it` +
|
|
1132
|
-
` is the last one),
|
|
1196
|
+
` is the last one),
|
|
1133
1197
|
or publish this deliberately with` +
|
|
1134
|
-
` cbx submit --allow-secrets.
|
|
1198
|
+
` cbx submit --allow-secrets.
|
|
1135
1199
|
`);
|
|
1136
1200
|
/*
|
|
1137
1201
|
Reported already, and in more detail than the wrapper can. The
|
|
@@ -1141,8 +1205,8 @@ async function doPush(requests, url) {
|
|
|
1141
1205
|
}
|
|
1142
1206
|
if (failure?.kind === "conflict") {
|
|
1143
1207
|
send(`error ${request.dst} somebody published to "${branch}" while this push was running`);
|
|
1144
|
-
say(`
|
|
1145
|
-
git fetch, then push again.
|
|
1208
|
+
say(`
|
|
1209
|
+
git fetch, then push again.
|
|
1146
1210
|
`);
|
|
1147
1211
|
throw error;
|
|
1148
1212
|
}
|
|
@@ -1154,6 +1218,27 @@ async function doPush(requests, url) {
|
|
|
1154
1218
|
}
|
|
1155
1219
|
currentRepositoryId = result.repositoryId;
|
|
1156
1220
|
baseVersionId = result.versionId;
|
|
1221
|
+
/*
|
|
1222
|
+
A note on the commit becomes what the version says about itself.
|
|
1223
|
+
|
|
1224
|
+
CodeRook lets a save be described before anybody decides to publish
|
|
1225
|
+
it, and git has exactly one place for text attached to a commit
|
|
1226
|
+
without altering it. Sent after the publish rather than with it,
|
|
1227
|
+
because a note is not part of what was saved and a failure to
|
|
1228
|
+
record one must not fail the push.
|
|
1229
|
+
*/
|
|
1230
|
+
const note = noteOn(sha);
|
|
1231
|
+
if (note) {
|
|
1232
|
+
try {
|
|
1233
|
+
const { changeVersion } = await Promise.resolve().then(() => __importStar(require("./api.js")));
|
|
1234
|
+
await changeVersion(result.repositoryId, result.versionId, {
|
|
1235
|
+
notes: note,
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
say(` note: the description on ${sha.slice(0, 8)} could not be saved`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1157
1242
|
/*
|
|
1158
1243
|
The version and the commit it came from are the same thing in this
|
|
1159
1244
|
repository from here on. Recorded so a later fetch does not rebuild
|
|
@@ -131,20 +131,83 @@ async function commandVersions(parsed) {
|
|
|
131
131
|
console.log(dim("Nothing has been saved to this project yet."));
|
|
132
132
|
return 0;
|
|
133
133
|
}
|
|
134
|
+
/*
|
|
135
|
+
The same two lanes the website shows.
|
|
136
|
+
|
|
137
|
+
A version is a commit somebody named, so the lanes are a split of one
|
|
138
|
+
history rather than two histories — asking for commits gives the saves
|
|
139
|
+
still waiting on that decision, and asking for versions gives the ones
|
|
140
|
+
the public side can see. Neither contains the other; with no flag you get
|
|
141
|
+
both, which is what this always did.
|
|
142
|
+
*/
|
|
143
|
+
const onlyVersions = parsed.flags.get("versions") === true;
|
|
144
|
+
const onlyCommits = parsed.flags.get("commits") === true;
|
|
145
|
+
if (onlyVersions && onlyCommits) {
|
|
146
|
+
console.error(red("Pick one: --versions or --commits, or neither for both."));
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
const lane = saved.filter((version) => {
|
|
150
|
+
if (onlyVersions)
|
|
151
|
+
return Boolean(version.name);
|
|
152
|
+
if (onlyCommits)
|
|
153
|
+
return !version.name;
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
if (!lane.length) {
|
|
157
|
+
console.log(dim(onlyVersions
|
|
158
|
+
? "Nothing has been made a version yet."
|
|
159
|
+
: "Every save here has been made a version."));
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
const withNotes = parsed.flags.get("notes") === true;
|
|
134
163
|
const asked = Number(parsed.flags.get("limit") ?? 20);
|
|
135
164
|
const limit = Number.isFinite(asked) && asked > 0 ? asked : 20;
|
|
136
165
|
console.log(bold(project.name));
|
|
137
|
-
for (const version of
|
|
166
|
+
for (const version of lane.slice(0, limit)) {
|
|
138
167
|
const when = version.createdAt
|
|
139
168
|
? new Date(version.createdAt).toLocaleString()
|
|
140
169
|
: "";
|
|
170
|
+
/*
|
|
171
|
+
What the project has made of this version, said before the numbers.
|
|
172
|
+
|
|
173
|
+
A name, a pin and a set of labels are how somebody finds the version
|
|
174
|
+
they meant among forty; the file count is how they check it once they
|
|
175
|
+
have. Putting the arrangement first is the difference between a log and
|
|
176
|
+
a list they organised.
|
|
177
|
+
*/
|
|
178
|
+
const marks = [
|
|
179
|
+
/* Which one the project is standing on, which is not the same as which
|
|
180
|
+
one is at the top once somebody has pinned something. */
|
|
181
|
+
version.head ? green("current") : "",
|
|
182
|
+
version.pinned ? accent("pinned") : "",
|
|
183
|
+
version.name ? bold(version.name) : "",
|
|
184
|
+
version.state === "held" ? red("held") : "",
|
|
185
|
+
version.state === "declined" ? dim("declined") : "",
|
|
186
|
+
version.removedAt ? dim("taken down") : "",
|
|
187
|
+
/*
|
|
188
|
+
Said only of a version, because only a version is offered to anybody.
|
|
189
|
+
A commit nobody has promoted is not hidden — it was never on offer.
|
|
190
|
+
*/
|
|
191
|
+
version.name && version.visibility === "private" ? dim("hidden") : "",
|
|
192
|
+
...version.labels.map((label) => accent(label.name)),
|
|
193
|
+
].filter(Boolean);
|
|
141
194
|
console.log(` ${accent(`v${version.sequence}`).padEnd(16)} ${when.padEnd(22)}` +
|
|
142
|
-
`${String(version.fileCount).padStart(5)} files ${bytes(version.storedSize)}`
|
|
195
|
+
`${String(version.fileCount).padStart(5)} files ${bytes(version.storedSize)}` +
|
|
196
|
+
(marks.length ? ` ${marks.join(" ")}` : ""));
|
|
143
197
|
if (version.message)
|
|
144
198
|
console.log(` ${dim(version.message)}`);
|
|
199
|
+
/*
|
|
200
|
+
Indented under the save it belongs to rather than printed flat, so a
|
|
201
|
+
note of several paragraphs still reads as part of one row.
|
|
202
|
+
*/
|
|
203
|
+
if (withNotes && version.notes?.trim()) {
|
|
204
|
+
for (const line of version.notes.trim().split(/\r?\n/)) {
|
|
205
|
+
console.log(` ${line}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
145
208
|
}
|
|
146
|
-
if (
|
|
147
|
-
console.log(dim(` … ${
|
|
209
|
+
if (lane.length > limit) {
|
|
210
|
+
console.log(dim(` … ${lane.length - limit} older. Use --limit to see more.`));
|
|
148
211
|
}
|
|
149
212
|
return 0;
|
|
150
213
|
}
|
|
@@ -105,7 +105,8 @@ async function commandRelease(parsed) {
|
|
|
105
105
|
const wanted = parsed.flags.get("version");
|
|
106
106
|
const target = typeof wanted === "string"
|
|
107
107
|
? all.find((version) => String(version.sequence) === wanted.replace(/^v/i, ""))
|
|
108
|
-
|
|
108
|
+
/* The newest by number rather than by position in the list. */
|
|
109
|
+
: all.reduce((newest, one) => (one.sequence > newest.sequence ? one : newest));
|
|
109
110
|
if (!target) {
|
|
110
111
|
console.error(red(`This project has no version ${String(wanted)}.`));
|
|
111
112
|
return 1;
|