@coderook/cli 0.34.9 → 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.
@@ -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.9",
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
  ```
@@ -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) {
@@ -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
- try {
165
- await (0, api_js_1.markRelease)(project.id, target.id, name, typeof notes === "string" ? notes : null);
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
- catch (error) {
168
- console.error((0, colour_js_1.red)(error instanceof Error ? error.message : String(error)));
169
- return 1;
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.9",
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",
@@ -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; keep private code in a separate project and
283
- ship releases from a public one.
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