@coderook/cli 0.34.8 → 0.34.10
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/README.md +26 -0
- package/dist/cbx/src/core/upload.js +253 -29
- package/dist/cli/src/api.js +15 -0
- package/dist/cli/src/cli.js +13 -1
- package/dist/cli/src/service_commands.js +54 -10
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +12 -2
|
@@ -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, with cbx or plain git, from Claude Code.",
|
|
5
|
-
"version": "0.34.
|
|
5
|
+
"version": "0.34.10",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/README.md
CHANGED
|
@@ -436,6 +436,32 @@ artifacts or with `cbx attach <file>`, so a release made by pushing a tag
|
|
|
436
436
|
simply has none until something adds them — which is what a release without a
|
|
437
437
|
build has always looked like.
|
|
438
438
|
|
|
439
|
+
## Releases and app downloads
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
cbx release 1.4.0 name the newest save as a release
|
|
443
|
+
cbx attach ./dist/MyApp-Setup-1.4.0.exe put the installer on it
|
|
444
|
+
cbx release --downloads public offer its downloads, keep the source private
|
|
445
|
+
cbx releases each release, its files, and who can take them
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
In a public project, `--downloads public` offers a release's notes,
|
|
449
|
+
attachments and update feed to anybody while the version's own files keep
|
|
450
|
+
their visibility, so one project holds the code and ships the app. There is
|
|
451
|
+
no need for a second project that only holds releases. `--downloads private`
|
|
452
|
+
takes them back. Without a release name it changes an existing release and
|
|
453
|
+
leaves its notes alone.
|
|
454
|
+
|
|
455
|
+
`cbx releases` marks each one `public`, `downloads public` or `private`, and
|
|
456
|
+
for a public project prints the addresses an app or a script reads, which
|
|
457
|
+
need no account:
|
|
458
|
+
|
|
459
|
+
```
|
|
460
|
+
https://api.coderook.com/v1/public/projects/OWNER/SLUG/releases/latest
|
|
461
|
+
https://api.coderook.com/v1/public/projects/OWNER/SLUG/releases
|
|
462
|
+
https://api.coderook.com/v1/public/projects/OWNER/SLUG/updates electron-updater's generic provider
|
|
463
|
+
```
|
|
464
|
+
|
|
439
465
|
## Building for every platform
|
|
440
466
|
|
|
441
467
|
```
|
|
@@ -113,6 +113,24 @@ class UploadCancelled extends Error {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
exports.UploadCancelled = UploadCancelled;
|
|
116
|
+
/*
|
|
117
|
+
Version listings already read by this process, by repository and version.
|
|
118
|
+
|
|
119
|
+
A save read the version it started from twice — once to plan, once to
|
|
120
|
+
send — and then read the version it had just made, each time the whole
|
|
121
|
+
list: three downloads of 6,628 rows to change one line of one file. A
|
|
122
|
+
version never changes once it exists, so a listing read once is the
|
|
123
|
+
listing for good.
|
|
124
|
+
*/
|
|
125
|
+
const versionListings = new Map();
|
|
126
|
+
/*
|
|
127
|
+
Delta signatures, for the same reason: one describes a file of a version
|
|
128
|
+
that will never change, and the planning pass and the sending pass each
|
|
129
|
+
asked for it.
|
|
130
|
+
*/
|
|
131
|
+
const signatures = new Map();
|
|
132
|
+
/** How many version listings a folder keeps in `.coderook/versions`. */
|
|
133
|
+
const KEPT_LISTINGS = 3;
|
|
116
134
|
class Uploader {
|
|
117
135
|
credentials;
|
|
118
136
|
aborted = false;
|
|
@@ -127,6 +145,7 @@ class Uploader {
|
|
|
127
145
|
deltaProtocol = null;
|
|
128
146
|
/** Batch delta is separate so V2 single-file support remains compatible. */
|
|
129
147
|
deltaBatchSupported = null;
|
|
148
|
+
versionDeltaSupported = null;
|
|
130
149
|
microchunkSupported = null;
|
|
131
150
|
/** One shared preflight, so parallel upload lanes cannot race /health. */
|
|
132
151
|
capabilityRequest = null;
|
|
@@ -524,7 +543,7 @@ class Uploader {
|
|
|
524
543
|
if (!ticked.size)
|
|
525
544
|
throw new Error("Nothing is selected to upload");
|
|
526
545
|
const prior = request.repositoryId
|
|
527
|
-
? await this.versionFiles(request.repositoryId, request.baseVersionId)
|
|
546
|
+
? await this.versionFiles(request.repositoryId, request.baseVersionId, request.localPath)
|
|
528
547
|
: new Map();
|
|
529
548
|
// Ticked files are sent; everything else the project still has keeps the
|
|
530
549
|
// copy already stored, and a file that is new but unticked is left out.
|
|
@@ -1013,7 +1032,56 @@ class Uploader {
|
|
|
1013
1032
|
: null;
|
|
1014
1033
|
const linesAdded = counted ? counted.added : request.linesAdded;
|
|
1015
1034
|
const linesRemoved = counted ? counted.removed : request.linesRemoved;
|
|
1016
|
-
|
|
1035
|
+
/*
|
|
1036
|
+
The version's whole file list, as the service will hold it.
|
|
1037
|
+
*/
|
|
1038
|
+
const wire = contents.map((item) => ({
|
|
1039
|
+
path: item.path,
|
|
1040
|
+
...(item.pack
|
|
1041
|
+
? {
|
|
1042
|
+
pack: {
|
|
1043
|
+
objectId: item.pack.packObjectId,
|
|
1044
|
+
offset: item.pack.offset,
|
|
1045
|
+
length: item.pack.length,
|
|
1046
|
+
},
|
|
1047
|
+
sha256: item.sha256,
|
|
1048
|
+
}
|
|
1049
|
+
: item.chunks
|
|
1050
|
+
? { chunks: item.chunks, sha256: item.sha256 }
|
|
1051
|
+
: { objectId: item.objectId }),
|
|
1052
|
+
sourceSize: item.sourceSize,
|
|
1053
|
+
storedSize: item.storedSize,
|
|
1054
|
+
mediaType: item.mediaType,
|
|
1055
|
+
executable: item.executable === true,
|
|
1056
|
+
}));
|
|
1057
|
+
/*
|
|
1058
|
+
Or only what changed (version-delta-v1).
|
|
1059
|
+
|
|
1060
|
+
The list above names every file in the project, so a save of one line
|
|
1061
|
+
in a 6,628-file project sent 2.3 MB describing 6,627 files the service
|
|
1062
|
+
already held exactly that way. When the service can take it, the save
|
|
1063
|
+
names the version it started from, the paths that went, and only the
|
|
1064
|
+
entries that differ from that version; the service fills in the rest
|
|
1065
|
+
from its own record before it checks anything.
|
|
1066
|
+
*/
|
|
1067
|
+
const startedFrom = typeof request.baseVersionId === "string" &&
|
|
1068
|
+
prior.size &&
|
|
1069
|
+
(await this.versionDeltaAllowed())
|
|
1070
|
+
? request.baseVersionId
|
|
1071
|
+
: null;
|
|
1072
|
+
const changesOnly = startedFrom
|
|
1073
|
+
? {
|
|
1074
|
+
files: wire.filter((entry) => {
|
|
1075
|
+
const before = prior.get(entry.path);
|
|
1076
|
+
return !before || !sameEntry(entry, wireFromPrior(entry.path, before));
|
|
1077
|
+
}),
|
|
1078
|
+
filesFrom: {
|
|
1079
|
+
versionId: startedFrom,
|
|
1080
|
+
removed: [...prior.keys()].filter((held) => !named.has(held)),
|
|
1081
|
+
},
|
|
1082
|
+
}
|
|
1083
|
+
: null;
|
|
1084
|
+
const send = (asChanges) => (0, profile_js_1.timed)("publish the version", () => this.call(`/v1/repositories/${repositoryId}/versions`, {
|
|
1017
1085
|
method: "POST",
|
|
1018
1086
|
contentType: "application/json",
|
|
1019
1087
|
body: JSON.stringify({
|
|
@@ -1057,27 +1125,21 @@ class Uploader {
|
|
|
1057
1125
|
One shape or the other, never both — the service refuses a file that
|
|
1058
1126
|
names an object and a chunk list, and so does its database.
|
|
1059
1127
|
*/
|
|
1060
|
-
files:
|
|
1061
|
-
path: item.path,
|
|
1062
|
-
...(item.pack
|
|
1063
|
-
? {
|
|
1064
|
-
pack: {
|
|
1065
|
-
objectId: item.pack.packObjectId,
|
|
1066
|
-
offset: item.pack.offset,
|
|
1067
|
-
length: item.pack.length,
|
|
1068
|
-
},
|
|
1069
|
-
sha256: item.sha256,
|
|
1070
|
-
}
|
|
1071
|
-
: item.chunks
|
|
1072
|
-
? { chunks: item.chunks, sha256: item.sha256 }
|
|
1073
|
-
: { objectId: item.objectId }),
|
|
1074
|
-
sourceSize: item.sourceSize,
|
|
1075
|
-
storedSize: item.storedSize,
|
|
1076
|
-
mediaType: item.mediaType,
|
|
1077
|
-
executable: item.executable === true,
|
|
1078
|
-
})),
|
|
1128
|
+
...(asChanges && changesOnly ? changesOnly : { files: wire }),
|
|
1079
1129
|
}),
|
|
1080
1130
|
}));
|
|
1131
|
+
/*
|
|
1132
|
+
A service that cannot build on the version named (it was taken down,
|
|
1133
|
+
say) answers files_from_*; the whole list is then sent instead, which
|
|
1134
|
+
every service accepts.
|
|
1135
|
+
*/
|
|
1136
|
+
const completed = await send(true).catch((error) => {
|
|
1137
|
+
const code = error.code;
|
|
1138
|
+
if (changesOnly && typeof code === "string" && code.startsWith("files_from_")) {
|
|
1139
|
+
return send(false);
|
|
1140
|
+
}
|
|
1141
|
+
throw error;
|
|
1142
|
+
});
|
|
1081
1143
|
/*
|
|
1082
1144
|
A clean stale-base save may contain a server-side merge. The candidate
|
|
1083
1145
|
manifest built above is then not the Version that landed: it lacks the
|
|
@@ -1086,8 +1148,21 @@ class Uploader {
|
|
|
1086
1148
|
Version once rather than guessing.
|
|
1087
1149
|
*/
|
|
1088
1150
|
let acceptedManifest = (0, publication_js_1.acceptedManifestDigests)(completed.manifest?.files);
|
|
1089
|
-
|
|
1090
|
-
|
|
1151
|
+
/*
|
|
1152
|
+
A service that lists version-delta-v1 also says when it combined this
|
|
1153
|
+
save with somebody else's (`combined`). Without that, and for a repeated
|
|
1154
|
+
attempt, the version that landed may not be the list sent here, so it is
|
|
1155
|
+
read back. Otherwise it is exactly that list, and reading 6,628 rows to
|
|
1156
|
+
learn what was just sent is the third download this used to cost.
|
|
1157
|
+
*/
|
|
1158
|
+
const landedAsSent = !completed.mergeTrack &&
|
|
1159
|
+
!completed.repeated &&
|
|
1160
|
+
!completed.combined &&
|
|
1161
|
+
/* A key published on purpose is stored as its covered copy, not as sent. */
|
|
1162
|
+
!request.allowSecrets &&
|
|
1163
|
+
(await this.versionDeltaAllowed());
|
|
1164
|
+
if (!completed.mergeTrack && !landedAsSent && !Object.keys(acceptedManifest).length) {
|
|
1165
|
+
acceptedManifest = Object.fromEntries([...(await this.versionFiles(repositoryId, completed.version.id, request.localPath))].map(([file, descriptor]) => [file, descriptor.sha256]));
|
|
1091
1166
|
}
|
|
1092
1167
|
const manifest = Object.keys(acceptedManifest).length
|
|
1093
1168
|
? acceptedManifest
|
|
@@ -1146,6 +1221,19 @@ class Uploader {
|
|
|
1146
1221
|
if (held)
|
|
1147
1222
|
local[path] = held;
|
|
1148
1223
|
}
|
|
1224
|
+
/*
|
|
1225
|
+
The next save starts from this version, so its listing is kept in the
|
|
1226
|
+
folder now, from the list just sent — every entry with its digest — and
|
|
1227
|
+
that save does not have to download it.
|
|
1228
|
+
*/
|
|
1229
|
+
if (landedAsSent && request.localPath) {
|
|
1230
|
+
const rows = wire.map((entry) => ({ ...entry, sha256: manifest[entry.path] }));
|
|
1231
|
+
if (rows.every((row) => typeof row.sha256 === "string" && row.sha256.length === 64)) {
|
|
1232
|
+
const key = `${repositoryId}/${completed.version.id}`;
|
|
1233
|
+
versionListings.set(key, Promise.resolve(rows));
|
|
1234
|
+
await writeListing(request.localPath, repositoryId, completed.version.id, rows);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1149
1237
|
report({
|
|
1150
1238
|
stage: "done",
|
|
1151
1239
|
files: declarations.length,
|
|
@@ -2000,7 +2088,9 @@ class Uploader {
|
|
|
2000
2088
|
});
|
|
2001
2089
|
}
|
|
2002
2090
|
/** What the newest version holds, so unchanged files can point at it. */
|
|
2003
|
-
async versionFiles(repositoryId, versionId
|
|
2091
|
+
async versionFiles(repositoryId, versionId,
|
|
2092
|
+
/** The project folder, whose `.coderook/versions` keeps listings between runs. */
|
|
2093
|
+
localPath) {
|
|
2004
2094
|
const held = new Map();
|
|
2005
2095
|
try {
|
|
2006
2096
|
// Explicit null is a genuinely empty base. Undefined belongs only to
|
|
@@ -2014,10 +2104,8 @@ class Uploader {
|
|
|
2014
2104
|
}
|
|
2015
2105
|
if (!wanted)
|
|
2016
2106
|
return held;
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
});
|
|
2020
|
-
for (const row of body.files ?? []) {
|
|
2107
|
+
const rows = await this.listing(repositoryId, wanted, localPath);
|
|
2108
|
+
for (const row of rows) {
|
|
2021
2109
|
const pieces = Array.isArray(row.chunks)
|
|
2022
2110
|
? row.chunks.map((chunk) => ({
|
|
2023
2111
|
objectId: String(chunk.objectId ?? ""),
|
|
@@ -2052,6 +2140,37 @@ class Uploader {
|
|
|
2052
2140
|
}
|
|
2053
2141
|
return held;
|
|
2054
2142
|
}
|
|
2143
|
+
/**
|
|
2144
|
+
* A version's files, as the service lists them: from this process's memo,
|
|
2145
|
+
* then the folder's own cache, and only then from the service.
|
|
2146
|
+
*
|
|
2147
|
+
* The folder cache is what makes the next save cheap: the version a save
|
|
2148
|
+
* starts from is nearly always the one the previous save made, and that
|
|
2149
|
+
* one is written here by the save that made it (rememberListing).
|
|
2150
|
+
*/
|
|
2151
|
+
async listing(repositoryId, versionId, localPath) {
|
|
2152
|
+
const key = `${repositoryId}/${versionId}`;
|
|
2153
|
+
const known = versionListings.get(key);
|
|
2154
|
+
if (known)
|
|
2155
|
+
return known;
|
|
2156
|
+
const reading = (async () => {
|
|
2157
|
+
const cached = localPath
|
|
2158
|
+
? await readListing(localPath, repositoryId, versionId)
|
|
2159
|
+
: null;
|
|
2160
|
+
if (cached)
|
|
2161
|
+
return cached;
|
|
2162
|
+
const body = await this.call(`/v1/repositories/${repositoryId}/versions/${versionId}/files`, {
|
|
2163
|
+
method: "GET",
|
|
2164
|
+
});
|
|
2165
|
+
const rows = body.files ?? [];
|
|
2166
|
+
if (localPath)
|
|
2167
|
+
await writeListing(localPath, repositoryId, versionId, rows);
|
|
2168
|
+
return rows;
|
|
2169
|
+
})();
|
|
2170
|
+
versionListings.set(key, reading);
|
|
2171
|
+
reading.catch(() => versionListings.delete(key));
|
|
2172
|
+
return reading;
|
|
2173
|
+
}
|
|
2055
2174
|
/** The account's repository with this slug, if it has one. */
|
|
2056
2175
|
async findBySlug(slug) {
|
|
2057
2176
|
try {
|
|
@@ -2372,7 +2491,17 @@ class Uploader {
|
|
|
2372
2491
|
return null;
|
|
2373
2492
|
}
|
|
2374
2493
|
}
|
|
2375
|
-
|
|
2494
|
+
deltaSignature(repositoryId, baseVersionId, filePath, baseSha256, version) {
|
|
2495
|
+
const key = [repositoryId, baseVersionId, filePath, baseSha256, version].join("\u0000");
|
|
2496
|
+
const known = signatures.get(key);
|
|
2497
|
+
if (known)
|
|
2498
|
+
return known;
|
|
2499
|
+
const asking = this.fetchDeltaSignature(repositoryId, baseVersionId, filePath, baseSha256, version);
|
|
2500
|
+
signatures.set(key, asking);
|
|
2501
|
+
asking.catch(() => signatures.delete(key));
|
|
2502
|
+
return asking;
|
|
2503
|
+
}
|
|
2504
|
+
async fetchDeltaSignature(repositoryId, baseVersionId, filePath, baseSha256, version) {
|
|
2376
2505
|
const token = await this.credentials.token();
|
|
2377
2506
|
if (!token)
|
|
2378
2507
|
throw new Error("Sign in again before uploading");
|
|
@@ -2841,6 +2970,20 @@ class Uploader {
|
|
|
2841
2970
|
: 0;
|
|
2842
2971
|
return this.deltaProtocol;
|
|
2843
2972
|
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Whether a save may name only what changed (`filesFrom`).
|
|
2975
|
+
*
|
|
2976
|
+
* Asked, never assumed, and for a stronger reason than the others: a
|
|
2977
|
+
* service that does not know the field would drop it and publish the
|
|
2978
|
+
* changed files alone as the whole project. Only a service that lists
|
|
2979
|
+
* `version-delta-v1` gets anything but the full list.
|
|
2980
|
+
*/
|
|
2981
|
+
async versionDeltaAllowed() {
|
|
2982
|
+
if (this.versionDeltaSupported !== null)
|
|
2983
|
+
return this.versionDeltaSupported;
|
|
2984
|
+
this.versionDeltaSupported = (await this.serviceFeatures()).includes("version-delta-v1");
|
|
2985
|
+
return this.versionDeltaSupported;
|
|
2986
|
+
}
|
|
2844
2987
|
async deltaBatchAllowed() {
|
|
2845
2988
|
if (this.deltaBatchSupported !== null)
|
|
2846
2989
|
return this.deltaBatchSupported;
|
|
@@ -2975,3 +3118,84 @@ async function executableFor(request, relative, before) {
|
|
|
2975
3118
|
return before;
|
|
2976
3119
|
}
|
|
2977
3120
|
}
|
|
3121
|
+
/*
|
|
3122
|
+
The folder's cache of version listings: `.coderook/versions/<id>.json`.
|
|
3123
|
+
|
|
3124
|
+
`.coderook` is never uploaded and ignores itself, so this never reaches a
|
|
3125
|
+
project. A version never changes, so an entry is either right or for a
|
|
3126
|
+
different version; it is checked against both names it was written under,
|
|
3127
|
+
and anything unreadable is simply not used.
|
|
3128
|
+
*/
|
|
3129
|
+
async function readListing(localPath, repositoryId, versionId) {
|
|
3130
|
+
try {
|
|
3131
|
+
const kept = JSON.parse(await (0, promises_1.readFile)(node_path_1.default.join(localPath, ".coderook", "versions", `${versionId}.json`), "utf8"));
|
|
3132
|
+
if (kept.repositoryId !== repositoryId ||
|
|
3133
|
+
kept.versionId !== versionId ||
|
|
3134
|
+
!Array.isArray(kept.files)) {
|
|
3135
|
+
return null;
|
|
3136
|
+
}
|
|
3137
|
+
return kept.files;
|
|
3138
|
+
}
|
|
3139
|
+
catch {
|
|
3140
|
+
return null;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
async function writeListing(localPath, repositoryId, versionId, files) {
|
|
3144
|
+
const folder = node_path_1.default.join(localPath, ".coderook", "versions");
|
|
3145
|
+
try {
|
|
3146
|
+
await (0, promises_1.mkdir)(folder, { recursive: true });
|
|
3147
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(folder, `${versionId}.json`), JSON.stringify({ repositoryId, versionId, files }));
|
|
3148
|
+
/* Only the newest few: the next save needs one, a retry another. */
|
|
3149
|
+
const entries = await Promise.all((await (0, promises_1.readdir)(folder))
|
|
3150
|
+
.filter((name) => name.endsWith(".json"))
|
|
3151
|
+
.map(async (name) => ({
|
|
3152
|
+
name,
|
|
3153
|
+
at: (await (0, promises_1.stat)(node_path_1.default.join(folder, name))).mtimeMs,
|
|
3154
|
+
})));
|
|
3155
|
+
entries.sort((left, right) => right.at - left.at);
|
|
3156
|
+
for (const old of entries.slice(KEPT_LISTINGS)) {
|
|
3157
|
+
await (0, promises_1.rm)(node_path_1.default.join(folder, old.name), { force: true });
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
catch {
|
|
3161
|
+
/* A cache that cannot be written costs one download next time, nothing more. */
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
/** A file the previous version holds, in the shape a save names it. */
|
|
3165
|
+
function wireFromPrior(filePath, held) {
|
|
3166
|
+
return {
|
|
3167
|
+
path: filePath,
|
|
3168
|
+
...(held.pack
|
|
3169
|
+
? {
|
|
3170
|
+
pack: {
|
|
3171
|
+
objectId: held.pack.objectId,
|
|
3172
|
+
offset: held.pack.offset,
|
|
3173
|
+
length: held.pack.length,
|
|
3174
|
+
},
|
|
3175
|
+
sha256: held.sha256,
|
|
3176
|
+
}
|
|
3177
|
+
: held.chunks?.length
|
|
3178
|
+
? { chunks: held.chunks, sha256: held.sha256 }
|
|
3179
|
+
: { objectId: held.objectId }),
|
|
3180
|
+
sourceSize: held.sourceSize,
|
|
3181
|
+
storedSize: held.storedSize,
|
|
3182
|
+
mediaType: held.mediaType,
|
|
3183
|
+
executable: held.executable === true,
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
/** Whether two entries name the same file stored the same way. */
|
|
3187
|
+
function sameEntry(left, right) {
|
|
3188
|
+
return canonical(left) === canonical(right);
|
|
3189
|
+
}
|
|
3190
|
+
function canonical(value) {
|
|
3191
|
+
if (Array.isArray(value))
|
|
3192
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
3193
|
+
if (value && typeof value === "object") {
|
|
3194
|
+
return `{${Object.keys(value)
|
|
3195
|
+
.filter((key) => value[key] !== undefined)
|
|
3196
|
+
.sort()
|
|
3197
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
3198
|
+
.join(",")}}`;
|
|
3199
|
+
}
|
|
3200
|
+
return JSON.stringify(value);
|
|
3201
|
+
}
|
package/dist/cli/src/api.js
CHANGED
|
@@ -31,6 +31,7 @@ exports.compareVersions = compareVersions;
|
|
|
31
31
|
exports.fileTextAt = fileTextAt;
|
|
32
32
|
exports.versions = versions;
|
|
33
33
|
exports.markRelease = markRelease;
|
|
34
|
+
exports.setReleaseDownloadsPublic = setReleaseDownloadsPublic;
|
|
34
35
|
exports.issues = issues;
|
|
35
36
|
exports.createIssue = createIssue;
|
|
36
37
|
exports.releases = releases;
|
|
@@ -628,6 +629,17 @@ async function markRelease(repositoryId, versionId, name, notes) {
|
|
|
628
629
|
versionId: String(body.release?.versionId ?? versionId),
|
|
629
630
|
};
|
|
630
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Make a release's downloads public, or private again, apart from its source.
|
|
634
|
+
*
|
|
635
|
+
* What is attached to the release, its notes and the update feed are offered
|
|
636
|
+
* to anybody while the version's files keep their own visibility, so one
|
|
637
|
+
* project can ship its installers without a second public project for them.
|
|
638
|
+
*/
|
|
639
|
+
async function setReleaseDownloadsPublic(repositoryId, versionId, value) {
|
|
640
|
+
await call(`/v1/repositories/${encodeURIComponent(repositoryId)}` +
|
|
641
|
+
`/versions/${encodeURIComponent(versionId)}/release/downloads`, { method: "PUT", body: { public: value } });
|
|
642
|
+
}
|
|
631
643
|
async function issues(repositoryId) {
|
|
632
644
|
const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/issues`);
|
|
633
645
|
return {
|
|
@@ -671,6 +683,9 @@ async function releases(repositoryId) {
|
|
|
671
683
|
releasedAt: String(row.releasedAt ?? ""),
|
|
672
684
|
releasedBy: String(row.releasedBy ?? ""),
|
|
673
685
|
assets: Array.isArray(row.assets) ? row.assets : [],
|
|
686
|
+
attachments: Array.isArray(row.attachments) ? row.attachments : [],
|
|
687
|
+
downloadsPublic: row.downloadsPublic === true,
|
|
688
|
+
sourcePublic: row.sourcePublic === true,
|
|
674
689
|
}));
|
|
675
690
|
}
|
|
676
691
|
async function collaborators(repositoryId) {
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -2801,6 +2801,10 @@ const SPECS = [
|
|
|
2801
2801
|
group: "Your projects",
|
|
2802
2802
|
summary: "what has been released, and its files",
|
|
2803
2803
|
usage: "releases [project]",
|
|
2804
|
+
detail: "Each release with its files and who can take them: public (source and\n" +
|
|
2805
|
+
"downloads), downloads public (the source stays private) or private.\n" +
|
|
2806
|
+
"For a public project it also prints the addresses an app or a script\n" +
|
|
2807
|
+
"reads them from, which need no account.",
|
|
2804
2808
|
run: service_commands_js_1.commandReleases,
|
|
2805
2809
|
},
|
|
2806
2810
|
{
|
|
@@ -3060,14 +3064,22 @@ const SPECS = [
|
|
|
3060
3064
|
"Nothing is uploaded: a release is a version with a name on it, so the\n" +
|
|
3061
3065
|
"version has to exist already.\n\n" +
|
|
3062
3066
|
"The same thing happens when a git tag is pushed to a CodeRook remote —\n" +
|
|
3063
|
-
"`git push cbx v1.0` and `cbx release v1.0` are one action
|
|
3067
|
+
"`git push cbx v1.0` and `cbx release v1.0` are one action.\n\n" +
|
|
3068
|
+
"Put the installers on it with `cbx attach`. In a public project,\n" +
|
|
3069
|
+
"--downloads public offers the release's notes, attachments and update\n" +
|
|
3070
|
+
"feed to anybody while the version's source stays private, so one\n" +
|
|
3071
|
+
"project can ship its own app without a second, public project for the\n" +
|
|
3072
|
+
"installers. Without a name, --downloads changes an existing release.",
|
|
3064
3073
|
options: [
|
|
3065
3074
|
{ flags: "--version <n>", description: "name this version instead of the newest" },
|
|
3066
3075
|
{ flags: "--notes <text>", description: "what changed, for the release page" },
|
|
3076
|
+
{ flags: "--downloads <public|private>", description: "who can take what is attached, apart from the source" },
|
|
3067
3077
|
],
|
|
3068
3078
|
examples: [
|
|
3069
3079
|
"cbx release v1.0",
|
|
3070
3080
|
'cbx release v1.2 --version 41 --notes "Fixes the export dialog"',
|
|
3081
|
+
"cbx release 1.4.0 --downloads public",
|
|
3082
|
+
"cbx release --downloads private --version 41",
|
|
3071
3083
|
],
|
|
3072
3084
|
run: service_commands_js_1.commandRelease,
|
|
3073
3085
|
},
|
|
@@ -119,12 +119,22 @@ async function commandIssues(parsed) {
|
|
|
119
119
|
* would be a feature that exists twice and agrees by luck.
|
|
120
120
|
*/
|
|
121
121
|
async function commandRelease(parsed) {
|
|
122
|
+
/*
|
|
123
|
+
`--downloads public|private` decides who can take what is attached,
|
|
124
|
+
apart from the source. Given without a name it changes an existing
|
|
125
|
+
release and nothing else: naming it again would rewrite its notes.
|
|
126
|
+
*/
|
|
127
|
+
const downloads = parsed.flags.get("downloads");
|
|
128
|
+
if (downloads !== undefined && downloads !== "public" && downloads !== "private") {
|
|
129
|
+
console.error((0, colour_js_1.red)("--downloads is public or private: cbx release v1.0 --downloads public"));
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
122
132
|
const name = parsed.positional[0];
|
|
123
|
-
if (!name) {
|
|
133
|
+
if (!name && downloads === undefined) {
|
|
124
134
|
console.error((0, colour_js_1.red)("Name the release: cbx release v1.0"));
|
|
125
135
|
return 1;
|
|
126
136
|
}
|
|
127
|
-
const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[1]);
|
|
137
|
+
const project = await (0, project_commands_js_1.resolveProject)(name ? parsed.positional[1] : parsed.positional[0]);
|
|
128
138
|
if (!project)
|
|
129
139
|
return 1;
|
|
130
140
|
const all = await (0, api_js_1.versions)(project.id);
|
|
@@ -161,15 +171,32 @@ async function commandRelease(parsed) {
|
|
|
161
171
|
return 1;
|
|
162
172
|
}
|
|
163
173
|
const notes = parsed.flags.get("notes");
|
|
164
|
-
|
|
165
|
-
|
|
174
|
+
if (name) {
|
|
175
|
+
try {
|
|
176
|
+
await (0, api_js_1.markRelease)(project.id, target.id, name, typeof notes === "string" ? notes : null);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error((0, colour_js_1.red)(error instanceof Error ? error.message : String(error)));
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
console.log(`${(0, colour_js_1.green)("Released")} ${(0, colour_js_1.bold)(name)} ${(0, colour_js_1.dim)("at")} v${target.sequence}` +
|
|
183
|
+
`${(0, colour_js_1.dim)(" — ")}${target.message.split("\n")[0].slice(0, 48)}`);
|
|
166
184
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
if (downloads !== undefined) {
|
|
186
|
+
try {
|
|
187
|
+
await (0, api_js_1.setReleaseDownloadsPublic)(project.id, target.id, downloads === "public");
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
console.error((0, colour_js_1.red)(error instanceof Error ? error.message : String(error)));
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
193
|
+
console.log(downloads === "public"
|
|
194
|
+
? `${(0, colour_js_1.green)("Downloads public")} ${(0, colour_js_1.dim)("on")} v${target.sequence}${(0, colour_js_1.dim)(": anybody can take what is attached; the source keeps its own visibility.")}`
|
|
195
|
+
: `${(0, colour_js_1.green)("Downloads private")} ${(0, colour_js_1.dim)("on")} v${target.sequence}${(0, colour_js_1.dim)(": only people on this project can take them now.")}`);
|
|
196
|
+
if (downloads === "public" && project.visibility !== "public") {
|
|
197
|
+
console.log((0, colour_js_1.dim)(" The project itself is not public, so nobody outside it can reach them yet: cbx visibility public."));
|
|
198
|
+
}
|
|
170
199
|
}
|
|
171
|
-
console.log(`${(0, colour_js_1.green)("Released")} ${(0, colour_js_1.bold)(name)} ${(0, colour_js_1.dim)("at")} v${target.sequence}` +
|
|
172
|
-
`${(0, colour_js_1.dim)(" — ")}${target.message.split("\n")[0].slice(0, 48)}`);
|
|
173
200
|
return 0;
|
|
174
201
|
}
|
|
175
202
|
async function commandReleases(parsed) {
|
|
@@ -186,13 +213,30 @@ async function commandReleases(parsed) {
|
|
|
186
213
|
const when = release.releasedAt
|
|
187
214
|
? new Date(release.releasedAt).toLocaleDateString()
|
|
188
215
|
: "";
|
|
216
|
+
const who = release.sourcePublic
|
|
217
|
+
? "public"
|
|
218
|
+
: release.downloadsPublic
|
|
219
|
+
? "downloads public"
|
|
220
|
+
: "private";
|
|
189
221
|
console.log(` ${(0, colour_js_1.accent)(`v${release.sequence}`).padEnd(16)} ` +
|
|
190
222
|
`${(release.name || "—").padEnd(28)} ${when.padEnd(14)} ` +
|
|
191
|
-
`${release.assets.length} files`);
|
|
223
|
+
`${`${release.assets.length + release.attachments.length} files`.padEnd(10)} ${(0, colour_js_1.dim)(who)}`);
|
|
192
224
|
const firstLine = release.notes.split("\n")[0];
|
|
193
225
|
if (firstLine)
|
|
194
226
|
console.log(` ${(0, colour_js_1.dim)(firstLine)}`);
|
|
195
227
|
}
|
|
228
|
+
/* Where an app or a script reads them, for a project anybody can reach. */
|
|
229
|
+
const owner = project.visibility === "public" && published.some((one) => one.downloadsPublic || one.sourcePublic)
|
|
230
|
+
? await (0, api_js_1.whoami)().then((me) => me.username).catch(() => null)
|
|
231
|
+
: null;
|
|
232
|
+
if (owner) {
|
|
233
|
+
const base = `https://api.coderook.com/v1/public/projects/${encodeURIComponent(owner)}/${encodeURIComponent(project.slug)}`;
|
|
234
|
+
console.log("");
|
|
235
|
+
console.log((0, colour_js_1.dim)(" From code, no account needed:"));
|
|
236
|
+
console.log(` ${(0, colour_js_1.dim)("latest release")} ${base}/releases/latest`);
|
|
237
|
+
console.log(` ${(0, colour_js_1.dim)("all releases")} ${base}/releases`);
|
|
238
|
+
console.log(` ${(0, colour_js_1.dim)("update feed")} ${base}/updates`);
|
|
239
|
+
}
|
|
196
240
|
return 0;
|
|
197
241
|
}
|
|
198
242
|
/** Who can reach a project, and who has been asked. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderook/cli",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.10",
|
|
4
4
|
"description": "The command line for CodeRook (coderook.com): version control and project hosting for projects of any size. Every save is a compressed snapshot, private or public, never used to train AI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://coderook.com",
|
package/skills/coderook/SKILL.md
CHANGED
|
@@ -246,11 +246,13 @@ line's source to find these — this is the whole of it:
|
|
|
246
246
|
GET /projects?q= public projects, newest first
|
|
247
247
|
GET /projects/{owner}/{slug} one project
|
|
248
248
|
GET /projects/{owner}/{slug}/versions the versions it offers
|
|
249
|
+
GET /projects/{owner}/{slug}/releases every release, with its downloads
|
|
249
250
|
GET /projects/{owner}/{slug}/releases/latest newest release and its files
|
|
250
251
|
GET /projects/{owner}/{slug}/updates/latest.yml electron-updater feed
|
|
251
252
|
GET /projects/{owner}/{slug}/versions/{v}/files every file, with digests
|
|
252
253
|
GET /projects/{owner}/{slug}/versions/{v}/file?path= one file
|
|
253
254
|
GET /projects/{owner}/{slug}/versions/{v}/archive the version as a zip
|
|
255
|
+
GET /projects/{owner}/{slug}/versions/{v}/last-changes?path= which version last changed each entry
|
|
254
256
|
GET /projects/{owner}/{slug}/versions/{v}/attachments files attached to it
|
|
255
257
|
GET /projects/{owner}/{slug}/versions/{v}/attachments/{id} download one
|
|
256
258
|
```
|
|
@@ -279,8 +281,16 @@ The feed serves `latest.yml`, `latest-mac.yml` and `latest-linux.yml` with
|
|
|
279
281
|
each file's size and SHA-512, and redirects file names to their downloads.
|
|
280
282
|
Anything else can read `releases/latest` as JSON: version, notes, and every
|
|
281
283
|
file with its size, SHA-256 and `url`. Updaters have no account, so the
|
|
282
|
-
project they read must be public
|
|
283
|
-
|
|
284
|
+
project they read must be public, but its source need not be:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
cbx release 1.4.0 --downloads public # notes, attachments and feed public; files stay private
|
|
288
|
+
cbx releases # public / downloads public / private, and the addresses
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Ask before `--downloads public`**: it publishes the attached files to anyone.
|
|
292
|
+
One project holds the code and ships the app; a second "releases" project is
|
|
293
|
+
no longer needed.
|
|
284
294
|
|
|
285
295
|
## Building executables for every platform
|
|
286
296
|
|