@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.
@@ -229,6 +229,7 @@ class Downloader {
229
229
  ...(row.objectId ? { objectId: String(row.objectId) } : {}),
230
230
  sha256: String(row.sha256 ?? ""),
231
231
  sourceSize: Number(row.sourceSize ?? 0),
232
+ ...(row.sealed === true ? { sealed: true } : {}),
232
233
  ...(packed?.objectId
233
234
  ? {
234
235
  pack: {
@@ -298,7 +299,7 @@ class Downloader {
298
299
  const packed = new Map();
299
300
  for (const file of files) {
300
301
  const id = file.pack?.objectId;
301
- if (!id)
302
+ if (!id || file.sealed)
302
303
  continue;
303
304
  const group = packed.get(id) ?? [];
304
305
  group.push(file);
@@ -323,7 +324,7 @@ class Downloader {
323
324
  for (const file of files) {
324
325
  this.check();
325
326
  progress(file.path);
326
- const packId = file.pack?.objectId;
327
+ const packId = file.sealed ? undefined : file.pack?.objectId;
327
328
  if (packId && restoredPacks.has(packId))
328
329
  continue;
329
330
  if (packId) {
@@ -372,7 +373,14 @@ class Downloader {
372
373
  }
373
374
  async fetchInto(repositoryId, versionId, file, target) {
374
375
  const whole = (0, node_crypto_1.createHash)("sha256");
375
- const pieces = file.chunks ?? [];
376
+ /*
377
+ A sealed file is read whole, by path, whatever shape it is stored in.
378
+
379
+ Its pieces and its object are the covered bytes; only the per-path route
380
+ puts the real value back. Reading it any other way fetches something
381
+ that cannot match the digest this file was listed with.
382
+ */
383
+ const pieces = file.sealed ? [] : (file.chunks ?? []);
376
384
  // Captured so the generators below do not need `this` rebound.
377
385
  const request = (route) => this.request(route);
378
386
  const check = () => this.check();
@@ -399,7 +407,7 @@ class Downloader {
399
407
  }
400
408
  })()
401
409
  : (async function* () {
402
- const reply = await request(file.objectId
410
+ const reply = await request(file.objectId && !file.sealed
403
411
  ? `/v1/repositories/${repositoryId}/objects/${file.objectId}`
404
412
  : `/v1/repositories/${repositoryId}/versions/${versionId}/file` +
405
413
  `?path=${encodeURIComponent(file.path)}`);
@@ -456,7 +464,8 @@ class Downloader {
456
464
  let bytes = 0;
457
465
  const packed = new Map();
458
466
  for (const file of files) {
459
- const id = file.pack?.objectId;
467
+ /* Sealed members never come out of the pack — see `sealed` above. */
468
+ const id = file.sealed ? undefined : file.pack?.objectId;
460
469
  if (!id)
461
470
  continue;
462
471
  const group = packed.get(id) ?? [];
@@ -479,7 +488,7 @@ class Downloader {
479
488
  if (parts.some((part) => part === ".." || part.includes("\0"))) {
480
489
  throw new Error(`That version contains an unsafe path: ${file.path}`);
481
490
  }
482
- const packId = file.pack?.objectId;
491
+ const packId = file.sealed ? undefined : file.pack?.objectId;
483
492
  if (packId && restoredPacks.has(packId))
484
493
  continue;
485
494
  if (packId) {
@@ -367,6 +367,7 @@ const READABLE = new Set([
367
367
  ".groovy",
368
368
  ".h",
369
369
  ".hpp",
370
+ ".hcl",
370
371
  ".hs",
371
372
  ".htm",
372
373
  ".html",
@@ -54,6 +54,82 @@ class Tracks {
54
54
  }
55
55
  return (text ? JSON.parse(text) : {});
56
56
  }
57
+ /**
58
+ * A project's saves, as the owner sees them.
59
+ *
60
+ * Every one of them, including the ones held for review and the ones whose
61
+ * files were taken down — this is the working history, not the published
62
+ * one, and the whole point of showing it here is deciding what becomes
63
+ * published.
64
+ */
65
+ async versions(repositoryId) {
66
+ const body = await this.call(`/v1/repositories/${repositoryId}/versions`);
67
+ return body.versions ?? [];
68
+ }
69
+ /**
70
+ * What the account says about this project, as distinct from this folder.
71
+ *
72
+ * The local record knows where a folder saves to and nothing about how the
73
+ * project is published — so the history window could show which saves were
74
+ * public without being able to say whether the project itself was, which is
75
+ * the half that decides whether anybody can reach them.
76
+ */
77
+ async project(repositoryId) {
78
+ try {
79
+ const body = await this.call(`/v1/repositories/${repositoryId}`);
80
+ return { visibility: body.visibility ?? null };
81
+ }
82
+ catch {
83
+ /* Not knowing is not worth failing the window for. */
84
+ return { visibility: null };
85
+ }
86
+ }
87
+ async labels(repositoryId) {
88
+ const body = await this.call(`/v1/repositories/${repositoryId}/labels`);
89
+ return body.labels ?? [];
90
+ }
91
+ async createLabel(repositoryId, name, colour) {
92
+ const body = await this.call(`/v1/repositories/${repositoryId}/labels`, { method: "POST", body: JSON.stringify({ name, colour }) });
93
+ return body.label;
94
+ }
95
+ async reviewVersion(repositoryId, versionId, decision, note) {
96
+ await this.call(`/v1/repositories/${repositoryId}/versions/${versionId}/review`, {
97
+ method: "POST",
98
+ body: JSON.stringify({ decision, ...(note ? { note } : {}) }),
99
+ });
100
+ }
101
+ async takeVersionDown(repositoryId, versionId, reason) {
102
+ const query = reason ? `?reason=${encodeURIComponent(reason)}` : "";
103
+ await this.call(`/v1/repositories/${repositoryId}/versions/${versionId}/content${query}`, { method: "DELETE" });
104
+ }
105
+ /** Put the project back on an earlier save. Nothing is deleted. */
106
+ async undo(repositoryId, to) {
107
+ return await this.call(`/v1/repositories/${repositoryId}/undo`, {
108
+ method: "POST",
109
+ body: JSON.stringify(to ? { to } : {}),
110
+ });
111
+ }
112
+ /** Publish an older save's content again, as a new save. */
113
+ async restoreVersion(repositoryId, versionId, message) {
114
+ await this.call(`/v1/repositories/${repositoryId}/versions/${versionId}/restore`, {
115
+ method: "POST",
116
+ body: JSON.stringify({ message }),
117
+ });
118
+ }
119
+ /**
120
+ * Everything about a version, changed in one call.
121
+ *
122
+ * Named, hidden, pinned or labelled from the desktop app for the first time.
123
+ * All three clients call the same route with the same patch, which is what
124
+ * stops the next capability being reachable from one of them and not the
125
+ * others.
126
+ */
127
+ async changeVersion(repositoryId, versionId, patch) {
128
+ await this.call(`/v1/repositories/${repositoryId}/versions/${versionId}`, {
129
+ method: "PATCH",
130
+ body: JSON.stringify(patch),
131
+ });
132
+ }
57
133
  /**
58
134
  * Every line and merge this project has.
59
135
  *
@@ -1163,6 +1163,10 @@ class Uploader {
1163
1163
  ...(request.track ? { track: request.track } : {}),
1164
1164
  ...(request.allowIgnored ? { allowIgnored: true } : {}),
1165
1165
  ...(request.allowSecrets ? { allowSecrets: true } : {}),
1166
+ ...(request.linesAdded === undefined ? {} : { linesAdded: request.linesAdded }),
1167
+ ...(request.linesRemoved === undefined
1168
+ ? {}
1169
+ : { linesRemoved: request.linesRemoved }),
1166
1170
  /*
1167
1171
  Names this attempt so a retry after a lost connection is answered
1168
1172
  with the version already made, rather than making a second one.
@@ -1233,6 +1237,7 @@ class Uploader {
1233
1237
  repositoryId,
1234
1238
  versionId: completed.version.id,
1235
1239
  sequence: completed.version.sequence,
1240
+ ...(completed.version.state === "held" ? { held: true } : {}),
1236
1241
  ...(completed.mergeTrack ? { mergeTrack: completed.mergeTrack } : {}),
1237
1242
  ...(completed.repeated ? { repeated: true } : {}),
1238
1243
  sourceBytes: completed.version.sourceSize ?? sourceBytes,
@@ -999,9 +999,26 @@ async function uploadConcerns(root, include) {
999
999
  * matches no service's format and so is invisible to the pattern scan — the
1000
1000
  * only thing that identifies it is the name of the file it is sitting in.
1001
1001
  */
1002
+ /**
1003
+ * The `.env.<something>` files that exist to be committed.
1004
+ *
1005
+ * `.env.example` is the file a project is *supposed* to publish — it is the
1006
+ * documentation of which variables exist, with the values left empty. Refusing
1007
+ * to send it, and then advising it be added to .gitignore, is advice that
1008
+ * breaks the project for the next person who clones it.
1009
+ *
1010
+ * The same list the service uses when it decides what to cover, so a file is
1011
+ * not a template in one half of the system and a credential in the other.
1012
+ */
1013
+ const TEMPLATE_SUFFIXES = [".example", ".sample", ".template", ".dist", ".defaults"];
1014
+ function isTemplateName(name) {
1015
+ return TEMPLATE_SUFFIXES.some((suffix) => name.endsWith(suffix));
1016
+ }
1002
1017
  function isCredentialByName(relativePath) {
1003
1018
  const parts = relativePath.split("/");
1004
1019
  const name = (parts[parts.length - 1] ?? "").toLowerCase();
1020
+ if (isTemplateName(name))
1021
+ return false;
1005
1022
  return (SECRET_NAMES.has(name) ||
1006
1023
  name.startsWith(".env.") ||
1007
1024
  SECRET_SUFFIXES.some((suffix) => name.endsWith(suffix)) ||
@@ -1032,14 +1049,15 @@ async function detectSecrets(root) {
1032
1049
  continue;
1033
1050
  }
1034
1051
  seen += 1;
1035
- const name = entry.name.toLowerCase();
1036
- const isSecret = SECRET_NAMES.has(name) ||
1037
- name.startsWith(".env.") ||
1038
- SECRET_SUFFIXES.some((suffix) => name.endsWith(suffix)) ||
1039
- node_path_1.default.relative(root, directory).split(node_path_1.default.sep).includes("secrets");
1040
- if (isSecret) {
1041
- found.push(node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/"));
1042
- }
1052
+ /*
1053
+ Asked of the one function rather than repeated here. These were two
1054
+ copies of the same rule, which is how `.env.example` came to be
1055
+ refused by the command line long after the service had learned that
1056
+ templates are not credentials.
1057
+ */
1058
+ const relative = node_path_1.default.relative(root, full).split(node_path_1.default.sep).join("/");
1059
+ if (isCredentialByName(relative))
1060
+ found.push(relative);
1043
1061
  }
1044
1062
  }
1045
1063
  return found;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderook/cli",
3
- "version": "0.25.4",
3
+ "version": "0.27.0",
4
4
  "description": "CodeRook from the command line, on any operating system",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "homepage": "https://coderook.com",
@@ -81,6 +81,28 @@ cbx submit --track spike -m "…"
81
81
  Switching says where the next save goes and nothing else — no files move.
82
82
  Run `cbx get` afterwards to bring that line's files in.
83
83
 
84
+ ## Versions, and going back
85
+
86
+ Every save is a commit. A commit becomes a *version* — the thing the public
87
+ side of a project offers, and the thing a collaborator pulls — only when
88
+ somebody promotes it.
89
+
90
+ ```bash
91
+ cbx promote # pick from the last ten and name one
92
+ cbx mark 41 --pin # pin, hide, rename or label one save
93
+ cbx labels add shipped green # the labels a project can wear
94
+ cbx undo # put the project back on the save before this one
95
+ ```
96
+
97
+ `undo` moves where the project stands. Nothing is deleted and nothing is
98
+ renumbered — the saves passed over stay in the history, and the next save
99
+ carries on from wherever it now stands. It is the answer to "I pushed the
100
+ wrong thing"; hiding a version stops strangers reading it but leaves the
101
+ project standing on it, so the next person to pull still lands on the mistake.
102
+
103
+ **Ask before `undo`, `promote` and `take-down`.** They change what other
104
+ people see. `cbx versions` first, so the person can say which save they mean.
105
+
84
106
  ## When two saves collide
85
107
 
86
108
  If somebody saved while this folder was behind, the second save becomes a merge
@@ -121,6 +143,8 @@ cbx unbundle project.cbx ./restored
121
143
  again and gains nothing.
122
144
  - Do not guess a project name. `cbx projects` lists them; a folder that is
123
145
  already linked needs no name at all.
146
+ - Do not run `cbx take-down`. It destroys the files in a version and they do
147
+ not come back. Say it exists and let its owner run it.
124
148
 
125
149
  ## If something refuses
126
150