@coderook/cli 0.31.10 → 0.31.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "name": "coderook",
3
3
  "displayName": "CodeRook",
4
4
  "description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, from Claude Code.",
5
- "version": "0.31.10",
5
+ "version": "0.31.12",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -7,6 +7,7 @@ exports.completeUpload = completeUpload;
7
7
  exports.attachToVersion = attachToVersion;
8
8
  exports.whoami = whoami;
9
9
  exports.projects = projects;
10
+ exports.projectById = projectById;
10
11
  exports.findPublicProject = findPublicProject;
11
12
  exports.findProject = findProject;
12
13
  exports.health = health;
@@ -184,6 +185,37 @@ async function projects() {
184
185
  updatedAt: String(row.updatedAt ?? row.createdAt ?? ""),
185
186
  }));
186
187
  }
188
+ /**
189
+ * One project by its id, including somebody else's public or unlisted one.
190
+ *
191
+ * The account's own list holds only projects it is in, so a folder cloned
192
+ * from somebody else's public project had no way back to it: every command
193
+ * after the clone said the project was "no longer on your account". The
194
+ * service answers this route for anybody signed in when the project is open,
195
+ * and says whether the caller is one of its people.
196
+ */
197
+ async function projectById(repositoryId) {
198
+ try {
199
+ const row = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}`);
200
+ if (!row.id)
201
+ return null;
202
+ return {
203
+ id: String(row.id),
204
+ slug: String(row.slug ?? ""),
205
+ name: String(row.displayName || row.slug || "Untitled"),
206
+ visibility: String(row.visibility ?? "public"),
207
+ defaultBranch: String(row.defaultBranch || "main"),
208
+ versionCount: Number(row.versionCount ?? 0),
209
+ fileCount: Number(row.fileCount ?? 0),
210
+ storedBytes: Number(row.storedSize ?? 0),
211
+ updatedAt: String(row.updatedAt ?? row.createdAt ?? ""),
212
+ member: row.member !== false,
213
+ };
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ }
187
219
  /**
188
220
  * One public project, by the two names that identify it.
189
221
  *
@@ -195,9 +195,23 @@ const folderFor = (parsed, index = 0) => node_path_1.default.resolve(parsed.posi
195
195
  async function reconcile(folder, options = {}) {
196
196
  const link = await (0, config_js_1.readLink)(folder);
197
197
  const reference = link?.slug ?? node_path_1.default.basename(folder);
198
- const project = link
198
+ let member = true;
199
+ let project = link
199
200
  ? (await (0, api_js_2.projects)()).find((candidate) => candidate.id === link.repositoryId)
200
201
  : await (0, api_js_2.findProject)(reference);
202
+ /*
203
+ Not on the account's own list, which holds only projects it is in. A
204
+ folder cloned from somebody else's public project is exactly that, and
205
+ it was told the project had gone. Asked by id instead, the service still
206
+ answers for an open project and says the caller is a visitor.
207
+ */
208
+ if (link && !project) {
209
+ const visited = await (0, api_js_2.projectById)(link.repositoryId);
210
+ if (visited) {
211
+ project = visited;
212
+ member = visited.member;
213
+ }
214
+ }
201
215
  /*
202
216
  A link that has outlived the project it points at.
203
217
 
@@ -217,12 +231,12 @@ async function reconcile(folder, options = {}) {
217
231
  }
218
232
  if (!project?.versionCount) {
219
233
  // Nothing saved on the account, so everything here is outstanding.
220
- return { link: link ?? null, baseline: null, behind: null };
234
+ return { link: link ?? null, baseline: null, behind: null, member };
221
235
  }
222
236
  const downloader = new download_js_1.Downloader(config_js_1.credentials);
223
237
  const latest = (await downloader.versions(project.id))[0];
224
238
  if (!latest)
225
- return { link: link ?? null, baseline: null, behind: null };
239
+ return { link: link ?? null, baseline: null, behind: null, member };
226
240
  /*
227
241
  A folder that already knows which version it stands on keeps it. Only a
228
242
  folder that has never been reconciled — or was linked before versions
@@ -250,6 +264,7 @@ async function reconcile(folder, options = {}) {
250
264
  */
251
265
  baseline: new Map(Object.entries(known.local ?? known.manifest)),
252
266
  behind,
267
+ member,
253
268
  };
254
269
  }
255
270
  const baseline = new Map();
@@ -271,7 +286,7 @@ async function reconcile(folder, options = {}) {
271
286
  if (!options.quiet && !link) {
272
287
  console.log(dim(`Linked this folder to ${project.slug}.`));
273
288
  }
274
- return { link: fresh, baseline, behind: null };
289
+ return { link: fresh, baseline, behind: null, member };
275
290
  }
276
291
  // ── commands ────────────────────────────────────────────────────────────────
277
292
  async function commandSignIn(parsed) {
@@ -585,7 +600,19 @@ async function commandImport(parsed) {
585
600
  async function commandSubmit(parsed) {
586
601
  const folder = folderFor(parsed);
587
602
  const message = flagText(parsed, "m", "message") ?? "";
588
- const { link, baseline, behind } = await reconcile(folder);
603
+ const { link, baseline, behind, member } = await reconcile(folder);
604
+ /*
605
+ Somebody else's project, cloned from its public page. Reading and
606
+ fetching are fine; saving into it is not this account's to do, and the
607
+ service would refuse it anyway, less clearly.
608
+ */
609
+ if (link && !member) {
610
+ console.error(red(`Access denied: you are not a collaborator on ${link.slug}.`) +
611
+ "\n" +
612
+ dim(` Ask its owner to invite you, or run ${accent("cbx unlink")} to start a project of ` +
613
+ "your own from these files."));
614
+ return 1;
615
+ }
589
616
  /*
590
617
  A first save gets a licence unless it is turned down.
591
618
 
@@ -2282,9 +2309,8 @@ const SPECS = [
2282
2309
  "calls link only, keeps it off your profile and out of search. Public\n" +
2283
2310
  "lists it on your profile, and its page offers the saves somebody named\n" +
2284
2311
  "and did not hide.\n\n" +
2285
- "Opening a project up asks first and says what becomes readable: any\n" +
2286
- "account signed in to CodeRook that has its link can open every save\n" +
2287
- "through the API, not only the named versions. Going private asks\n" +
2312
+ "Opening a project up asks first and says what becomes readable: the\n" +
2313
+ "named versions nobody hid, and nothing else. Going private asks\n" +
2288
2314
  "nothing.",
2289
2315
  options: [
2290
2316
  { flags: "-y, --yes", description: "open it up without asking, for scripts" },
@@ -288,16 +288,6 @@ async function commandVisibility(parsed) {
288
288
  else {
289
289
  console.log(`Making ${bold(project.name)} link only keeps it off your profile and out of search.`);
290
290
  }
291
- /*
292
- Said because it is true today, not because it is the design. The public
293
- page follows the named-versions rule; the signed-in routes let any
294
- account read an unlisted or public project whole, commits and hidden
295
- versions included. Somebody deciding whether to open a project up
296
- should be deciding about that.
297
- */
298
- console.log(dim(" Any account signed in to CodeRook that has its link can open every save\n" +
299
- " through the API, not only the named versions. Check the history holds\n" +
300
- " nothing you would not share."));
301
291
  if (!(await agreed(`Make ${project.name} ${wordFor(target)}? [y/N] `))) {
302
292
  console.log(dim(`Nothing changed. ${project.name} is still ${wordFor(current)}.`));
303
293
  return 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.31.10",
3
+ "version": "0.31.12",
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": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",