@coderook/cli 0.27.0 → 0.29.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.
@@ -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.27.0",
5
+ "version": "0.29.0",
6
6
  "author": {
7
7
  "name": "ACCA Gaming Productions",
8
8
  "url": "https://coderook.com"
@@ -1,5 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.putObject = putObject;
4
+ exports.beginUpload = beginUpload;
5
+ exports.uploadPart = uploadPart;
6
+ exports.completeUpload = completeUpload;
7
+ exports.attachToVersion = attachToVersion;
3
8
  exports.whoami = whoami;
4
9
  exports.projects = projects;
5
10
  exports.findPublicProject = findPublicProject;
@@ -23,6 +28,7 @@ exports.invite = invite;
23
28
  exports.watch = watch;
24
29
  exports.setWatch = setWatch;
25
30
  exports.workflows = workflows;
31
+ exports.changeWorkflow = changeWorkflow;
26
32
  exports.runs = runs;
27
33
  exports.runLogs = runLogs;
28
34
  exports.tokens = tokens;
@@ -36,6 +42,10 @@ exports.reviewVersion = reviewVersion;
36
42
  exports.removeVersionContent = removeVersionContent;
37
43
  exports.versionAttachments = versionAttachments;
38
44
  exports.undoTo = undoTo;
45
+ exports.webhooks = webhooks;
46
+ exports.addWebhook = addWebhook;
47
+ exports.removeWebhook = removeWebhook;
48
+ exports.collisionCheck = collisionCheck;
39
49
  /** The small part of the API the command-line tool needs directly. */
40
50
  const identify_js_1 = require("../../desktop-app/src/main/identify.js");
41
51
  const config_js_1 = require("./config.js");
@@ -65,6 +75,73 @@ async function call(route, options = {}) {
65
75
  }
66
76
  return body;
67
77
  }
78
+ /**
79
+ * Send raw bytes, rather than JSON.
80
+ *
81
+ * `call` is built for a JSON request and a JSON reply; an object upload is a
82
+ * body of bytes with the digest in the path, so it needs its own door rather
83
+ * than a flag threaded through that one.
84
+ */
85
+ async function send(route, method, bytes, contentType) {
86
+ const token = await (0, config_js_1.loadToken)();
87
+ if (!token)
88
+ throw new Error("Not signed in. Run: cbx sign-in");
89
+ const response = await fetch(`${(0, config_js_1.apiOrigin)()}${route}`, {
90
+ method,
91
+ headers: {
92
+ accept: "application/json",
93
+ authorization: `Bearer ${token}`,
94
+ "user-agent": "CodeRook-CLI/0.1",
95
+ ...(0, identify_js_1.clientHeaders)(),
96
+ "content-type": contentType,
97
+ "content-length": String(bytes.byteLength),
98
+ },
99
+ body: bytes,
100
+ });
101
+ const text = await response.text();
102
+ const body = text ? JSON.parse(text) : {};
103
+ if (!response.ok) {
104
+ throw new Error(body?.error?.message ??
105
+ `${route} failed (${response.status})`);
106
+ }
107
+ return body;
108
+ }
109
+ /** One object, small enough for a single request. Returns its id. */
110
+ async function putObject(repositoryId, sha256, bytes, mediaType) {
111
+ const body = await send(`/v1/repositories/${encodeURIComponent(repositoryId)}/objects/${sha256}` +
112
+ `?kind=chunk&role=chunk&mediaType=${encodeURIComponent(mediaType)}`, "PUT", bytes, "application/octet-stream");
113
+ return String(body.objectId ?? "");
114
+ }
115
+ /** Start a multipart upload, for anything the direct route will not take. */
116
+ async function beginUpload(repositoryId, definition) {
117
+ const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/uploads`, {
118
+ method: "POST",
119
+ body: definition,
120
+ });
121
+ const id = body.uploadSessionId ?? body.id;
122
+ if (!id)
123
+ throw new Error("The service did not return an upload session");
124
+ return {
125
+ uploadSessionId: String(id),
126
+ maximumPartBytes: Number(body.maximumPartBytes ?? 95 * 1024 * 1024),
127
+ };
128
+ }
129
+ /** One numbered part of a multipart upload. */
130
+ async function uploadPart(uploadSessionId, partNumber, bytes) {
131
+ await send(`/v1/uploads/${encodeURIComponent(uploadSessionId)}/parts/${partNumber}`, "PUT", bytes, "application/octet-stream");
132
+ }
133
+ /** Close a multipart upload and take the object it became. */
134
+ async function completeUpload(uploadSessionId) {
135
+ const body = await call(`/v1/uploads/${encodeURIComponent(uploadSessionId)}/complete`, { method: "POST" });
136
+ if (!body.objectId)
137
+ throw new Error("The upload did not produce an object");
138
+ return String(body.objectId);
139
+ }
140
+ /** Make an object already on the project a download on one of its versions. */
141
+ async function attachToVersion(repositoryId, versionId, name, objectId) {
142
+ await call(`/v1/repositories/${encodeURIComponent(repositoryId)}` +
143
+ `/versions/${encodeURIComponent(versionId)}/attachments`, { method: "POST", body: { name, objectId } });
144
+ }
68
145
  async function whoami() {
69
146
  const body = await call("/v1/auth/session");
70
147
  if (!body.user)
@@ -313,7 +390,15 @@ async function createIssue(repositoryId, input) {
313
390
  itself, and exactly the kind of small wrongness that makes somebody stop
314
391
  trusting the rest of the output.
315
392
  */
316
- const created = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/issues`, { method: "POST", body: { title: input.title, body: input.body ?? "" } });
393
+ const created = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/issues`, {
394
+ method: "POST",
395
+ body: {
396
+ title: input.title,
397
+ body: input.body ?? "",
398
+ ...(input.labels?.length ? { labels: input.labels } : {}),
399
+ ...(input.versionId ? { versionId: input.versionId } : {}),
400
+ },
401
+ });
317
402
  return { number: Number(created.number ?? 0) };
318
403
  }
319
404
  async function releases(repositoryId) {
@@ -367,15 +452,29 @@ async function setWatch(repositoryId, change) {
367
452
  async function workflows(repositoryId) {
368
453
  const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/workflows`);
369
454
  return (body.workflows ?? []).map((row) => ({
455
+ /*
456
+ The id was dropped here for as long as this only ever printed a list.
457
+ Anything that acts on one workflow needs it, and a list that has thrown
458
+ away the identity of its rows can only be re-fetched.
459
+ */
460
+ id: String(row.id ?? ""),
370
461
  name: String(row.name ?? ""),
371
462
  trigger: String(row.trigger ?? ""),
372
463
  command: String(row.command ?? ""),
373
464
  runsOn: String(row.runsOn ?? "hosted"),
465
+ artifactPaths: Array.isArray(row.artifactPaths)
466
+ ? row.artifactPaths.map((one) => String(one))
467
+ : [],
468
+ attachArtifacts: row.attachArtifacts === true,
374
469
  runs: Number(row.runs ?? 0),
375
470
  passing: Number(row.passing ?? 0),
376
471
  archivedAt: row.archivedAt ? String(row.archivedAt) : null,
377
472
  }));
378
473
  }
474
+ /** Change a workflow. Only what is named is touched. */
475
+ async function changeWorkflow(repositoryId, workflowId, change) {
476
+ await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/workflows/${encodeURIComponent(workflowId)}`, { method: "PATCH", body: change });
477
+ }
379
478
  /**
380
479
  * What has run on a project.
381
480
  *
@@ -496,3 +595,58 @@ async function versionAttachments(repositoryId, versionId) {
496
595
  async function undoTo(repositoryId, to) {
497
596
  return await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/undo`, { method: "POST", body: to ? { to } : {} });
498
597
  }
598
+ /** Where this project tells somebody else that something happened. */
599
+ async function webhooks(repositoryId) {
600
+ const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/webhooks`);
601
+ return (body.webhooks ?? []).map((row) => ({
602
+ id: String(row.id ?? ""),
603
+ url: String(row.url ?? ""),
604
+ events: Array.isArray(row.events) ? row.events.map((one) => String(one)) : [],
605
+ active: row.active !== false,
606
+ lastDeliveredAt: row.last_delivered_at
607
+ ? String(row.last_delivered_at)
608
+ : null,
609
+ consecutiveFailures: Number(row.consecutive_failures ?? 0),
610
+ }));
611
+ }
612
+ /**
613
+ * Add one, and answer with the signing secret.
614
+ *
615
+ * The secret is generated by the service and returned exactly once — it is
616
+ * not readable from any route afterwards — so whatever prints this is the
617
+ * only chance anybody gets to write it down.
618
+ */
619
+ async function addWebhook(repositoryId, url, events) {
620
+ const created = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/webhooks`, { method: "POST", body: events.length ? { url, events } : { url } });
621
+ return { id: String(created.id ?? ""), secret: String(created.secret ?? "") };
622
+ }
623
+ async function removeWebhook(repositoryId, webhookId) {
624
+ await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/webhooks/${encodeURIComponent(webhookId)}`, { method: "DELETE" });
625
+ }
626
+ /**
627
+ * What would collide if this folder published right now.
628
+ *
629
+ * Advice, asked before anything is sent. The service has offered this since
630
+ * Merge Tracks shipped and no client asked, so the first anybody heard that
631
+ * their upload was walking into a merge was after it had finished — which on
632
+ * a slow line is the worst possible moment to find out.
633
+ *
634
+ * The answer can be stale by the time the publish arrives; the publish path
635
+ * decides for real. That is why this warns rather than refuses.
636
+ */
637
+ async function collisionCheck(repositoryId, baseVersionId, paths) {
638
+ const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/collision-check`, {
639
+ method: "POST",
640
+ /*
641
+ Capped, because this is advice and a folder can hold a hundred thousand
642
+ paths. The colliding ones are what matter and a long list of them is
643
+ already a merge nobody wants to read on a terminal.
644
+ */
645
+ body: { baseVersionId, paths: paths.slice(0, 5000) },
646
+ });
647
+ return {
648
+ behind: body.behind === true,
649
+ movedPaths: body.movedPaths ?? [],
650
+ collidingPaths: body.collidingPaths ?? [],
651
+ };
652
+ }
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.commandAttach = commandAttach;
7
+ /**
8
+ * `cbx attach` — put a file people can download on a version.
9
+ *
10
+ * A version could already hold downloads and there was no way to add one. The
11
+ * service accepted an attachment naming any object already on the project,
12
+ * and the only thing that ever put an object there through this door was a
13
+ * workflow run — so shipping a build made anywhere else meant a person
14
+ * clicking through the website, and shipping one built on this machine meant
15
+ * nothing at all.
16
+ *
17
+ * The size is the interesting part. A single PUT is capped at 95 MiB by the
18
+ * service, and the installer this was written for is 95.5 MB — so the
19
+ * comfortable path is the one that does not work for the thing people most
20
+ * want to attach. Anything above the cap goes up in parts instead, which is
21
+ * the same route the desktop uploader uses.
22
+ */
23
+ const node_crypto_1 = require("node:crypto");
24
+ const node_fs_1 = require("node:fs");
25
+ const promises_1 = require("node:fs/promises");
26
+ const node_path_1 = __importDefault(require("node:path"));
27
+ const api_js_1 = require("./api.js");
28
+ const project_commands_js_1 = require("./project_commands.js");
29
+ const version_commands_js_1 = require("./version_commands.js");
30
+ const api_js_2 = require("./api.js");
31
+ const dim = (value) => `${value}`;
32
+ const bold = (value) => `${value}`;
33
+ const red = (value) => `${value}`;
34
+ const green = (value) => `${value}`;
35
+ const accent = (value) => `${value}`;
36
+ /** What the service will take in one request. */
37
+ const DIRECT_LIMIT = 95 * 1024 * 1024;
38
+ /** Comfortably under the cap, so one slow part does not hold the rest up. */
39
+ const PART_SIZE = 32 * 1024 * 1024;
40
+ function readable(bytes) {
41
+ const units = ["B", "KB", "MB", "GB"];
42
+ let value = bytes;
43
+ let unit = 0;
44
+ while (value >= 1024 && unit < units.length - 1) {
45
+ value /= 1024;
46
+ unit += 1;
47
+ }
48
+ return `${value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
49
+ }
50
+ /**
51
+ * The digest, read a piece at a time.
52
+ *
53
+ * A ninety-five megabyte installer read into memory to be hashed and then
54
+ * read again to be sent is two hundred megabytes of a small machine's memory
55
+ * spent saying something a stream can say for nothing.
56
+ */
57
+ async function digestOf(file) {
58
+ const hash = (0, node_crypto_1.createHash)("sha256");
59
+ await new Promise((resolve, reject) => {
60
+ const stream = (0, node_fs_1.createReadStream)(file);
61
+ stream.on("data", (piece) => hash.update(piece));
62
+ stream.on("error", reject);
63
+ stream.on("end", () => resolve());
64
+ });
65
+ return hash.digest("hex");
66
+ }
67
+ /** A media type from the name, because the service records one either way. */
68
+ function mediaTypeOf(name) {
69
+ const known = {
70
+ ".exe": "application/vnd.microsoft.portable-executable",
71
+ ".dmg": "application/x-apple-diskimage",
72
+ ".deb": "application/vnd.debian.binary-package",
73
+ ".rpm": "application/x-rpm",
74
+ ".appimage": "application/x-executable",
75
+ ".zip": "application/zip",
76
+ ".gz": "application/gzip",
77
+ ".tgz": "application/gzip",
78
+ ".7z": "application/x-7z-compressed",
79
+ ".pdf": "application/pdf",
80
+ ".md": "text/markdown",
81
+ ".txt": "text/plain",
82
+ ".json": "application/json",
83
+ };
84
+ return known[node_path_1.default.extname(name).toLowerCase()] ?? "application/octet-stream";
85
+ }
86
+ /**
87
+ * Send the bytes and return the object they became.
88
+ *
89
+ * Two routes, one answer. Which one is used is a property of the size and not
90
+ * something a caller should have to think about, so it is decided here.
91
+ */
92
+ async function sendFile(repositoryId, file, size, sha256, mediaType, say) {
93
+ if (size <= DIRECT_LIMIT) {
94
+ const handle = await (0, promises_1.open)(file, "r");
95
+ try {
96
+ const bytes = await handle.readFile();
97
+ return await (0, api_js_1.putObject)(repositoryId, sha256, bytes, mediaType);
98
+ }
99
+ finally {
100
+ await handle.close();
101
+ }
102
+ }
103
+ /*
104
+ Parts, because the service will not take this in one request. Sent one at
105
+ a time rather than at once: the point of this path is a file too big to
106
+ hold comfortably, and holding four of its pieces to go faster gives that
107
+ back.
108
+ */
109
+ const session = await (0, api_js_1.beginUpload)(repositoryId, {
110
+ sha256,
111
+ size,
112
+ mediaType,
113
+ kind: "chunk",
114
+ repositoryRole: "chunk",
115
+ });
116
+ const parts = Math.ceil(size / PART_SIZE);
117
+ say(dim(` ${readable(size)} in ${parts} parts`));
118
+ const handle = await (0, promises_1.open)(file, "r");
119
+ try {
120
+ for (let index = 0; index < parts; index += 1) {
121
+ const start = index * PART_SIZE;
122
+ const length = Math.min(PART_SIZE, size - start);
123
+ const buffer = Buffer.alloc(length);
124
+ await handle.read(buffer, 0, length, start);
125
+ await (0, api_js_1.uploadPart)(session.uploadSessionId, index + 1, buffer);
126
+ say(dim(` part ${index + 1}/${parts}`));
127
+ }
128
+ }
129
+ finally {
130
+ await handle.close();
131
+ }
132
+ return await (0, api_js_1.completeUpload)(session.uploadSessionId);
133
+ }
134
+ async function commandAttach(parsed) {
135
+ const file = parsed.positional[0];
136
+ if (!file) {
137
+ console.error(red("Which file?") + dim(" cbx attach ./release/Installer.exe"));
138
+ return 1;
139
+ }
140
+ let size;
141
+ try {
142
+ const info = await (0, promises_1.stat)(file);
143
+ if (!info.isFile()) {
144
+ console.error(red(`${file} is not a file.`));
145
+ return 1;
146
+ }
147
+ size = info.size;
148
+ }
149
+ catch {
150
+ console.error(red(`There is no file at ${file}.`));
151
+ return 1;
152
+ }
153
+ if (size === 0) {
154
+ console.error(red("That file is empty."));
155
+ return 1;
156
+ }
157
+ /*
158
+ The version is the second positional, so `cbx attach build.exe 41` reads
159
+ the way the other version commands do. Without one it is the newest save,
160
+ which is almost always the one just built against.
161
+ */
162
+ const which = (0, version_commands_js_1.split)({
163
+ ...parsed,
164
+ positional: parsed.positional.slice(1),
165
+ });
166
+ const project = await (0, project_commands_js_1.resolveProject)(which.project);
167
+ if (!project)
168
+ return 1;
169
+ const all = await (0, api_js_2.versions)(project.id);
170
+ if (!all.length) {
171
+ console.error(red("This project has no versions yet."));
172
+ return 1;
173
+ }
174
+ const target = which.n
175
+ ? all.find((one) => String(one.sequence) === which.n.replace(/^v/i, ""))
176
+ : all.reduce((newest, one) => (one.sequence > newest.sequence ? one : newest));
177
+ if (!target) {
178
+ console.error(red(`This project has no version ${which.n}.`));
179
+ return 1;
180
+ }
181
+ const named = parsed.flags.get("name");
182
+ const name = typeof named === "string" && named.trim() ? named.trim() : node_path_1.default.basename(file);
183
+ console.log(`${bold(name)} ${dim(`(${readable(size)})`)} → ${accent(`v${target.sequence}`)} of ${project.name}`);
184
+ let objectId;
185
+ try {
186
+ const sha256 = await digestOf(file);
187
+ objectId = await sendFile(project.id, file, size, sha256, mediaTypeOf(name), (text) => console.log(text));
188
+ }
189
+ catch (error) {
190
+ console.error(red(error instanceof Error ? error.message : String(error)));
191
+ return 1;
192
+ }
193
+ try {
194
+ await (0, api_js_1.attachToVersion)(project.id, target.id, name, objectId);
195
+ }
196
+ catch (error) {
197
+ const message = error instanceof Error ? error.message : String(error);
198
+ console.error(red(message));
199
+ if (/already has something called/i.test(message)) {
200
+ console.error(dim(" Use --name to give this one a different name on the version."));
201
+ }
202
+ return 1;
203
+ }
204
+ console.log(green(`Attached. It is a download on v${target.sequence}.`));
205
+ if (!target.name) {
206
+ /*
207
+ Said because it is the difference between attaching and publishing. A
208
+ commit is not offered to anybody, so the download exists and nobody
209
+ outside the project can reach it yet.
210
+ */
211
+ console.log(dim(" v") +
212
+ dim(String(target.sequence)) +
213
+ dim(" is a commit, so nobody outside the project can take it yet."));
214
+ console.log(dim(` Make it a version: cbx mark ${target.sequence} --name v1.0`));
215
+ }
216
+ return 0;
217
+ }
@@ -25,6 +25,7 @@ const registry_js_1 = require("./registry.js");
25
25
  const help_js_1 = require("./help.js");
26
26
  const progress_js_1 = require("./progress.js");
27
27
  const publish_js_1 = require("./publish.js");
28
+ const attach_command_js_1 = require("./attach_command.js");
28
29
  const project_commands_js_1 = require("./project_commands.js");
29
30
  const track_commands_js_1 = require("./track_commands.js");
30
31
  const tracks_js_1 = require("../../desktop-app/src/main/tracks.js");
@@ -693,6 +694,53 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
693
694
  ...(hasFlag(parsed, "allow-secrets") ? { allowSecrets: true } : {}),
694
695
  ...(link ? { known: link.local ?? link.manifest ?? {} } : {}),
695
696
  });
697
+ /*
698
+ Whether this is walking into a merge, asked before anything is sent.
699
+
700
+ The service has offered this since Merge Tracks shipped and nothing ever
701
+ asked, so the first anybody heard that their upload would be diverted was
702
+ after it had finished — which on a slow line is the worst possible moment
703
+ to find out. It is advice: the answer can be stale by the time the
704
+ publish lands and the publish path decides for real, so this says what is
705
+ coming and gets out of the way.
706
+ */
707
+ if (link?.repositoryId && link.baseVersionId) {
708
+ try {
709
+ const ahead = await (0, api_js_1.collisionCheck)(link.repositoryId, link.baseVersionId, files.filter((file) => !file.deleted).map((file) => file.path));
710
+ if (ahead.collidingPaths.length) {
711
+ /*
712
+ "1 of your file is" — the plural was on the wrong noun. It is one
713
+ of *your files*, however many of them collided, so only the verb
714
+ changes with the count.
715
+ */
716
+ const many = ahead.collidingPaths.length !== 1;
717
+ console.log(accent("Heads up") +
718
+ ` somebody has saved since you last caught up, and ` +
719
+ `${ahead.collidingPaths.length} of your files ` +
720
+ `${many ? "are" : "is"} among what they changed.`);
721
+ for (const path of ahead.collidingPaths.slice(0, 5)) {
722
+ console.log(dim(` ${path}`));
723
+ }
724
+ if (ahead.collidingPaths.length > 5) {
725
+ console.log(dim(` and ${ahead.collidingPaths.length - 5} more`));
726
+ }
727
+ console.log(dim(" Your upload will be kept whole and put on a merge for you to settle."));
728
+ console.log(dim(" Run cbx get first if you would rather build on theirs."));
729
+ console.log("");
730
+ }
731
+ else if (ahead.behind) {
732
+ console.log(dim("Somebody has saved since you last caught up, but not to any file " +
733
+ "you changed — this will combine on its own."));
734
+ console.log("");
735
+ }
736
+ }
737
+ catch {
738
+ /*
739
+ Advice that cannot be fetched is not a reason to refuse a publish.
740
+ The service checks properly at the point it matters.
741
+ */
742
+ }
743
+ }
696
744
  let result;
697
745
  try {
698
746
  const plan = await uploader.plan(uploadRequest, (progress) => {
@@ -824,7 +872,13 @@ The connection failed: ${text}`));
824
872
  ? `, ${result.alreadyStoredFiles} already on the account`
825
873
  : "") +
826
874
  (result.reusedFiles ? `, ${result.reusedFiles} unchanged` : "") +
827
- ` · version holds ${bytes(result.sourceBytes)}`);
875
+ /*
876
+ "version holds" was the old fused vocabulary. `cbx submit` makes a
877
+ save; a save becomes a version when somebody names it, which is what
878
+ `cbx release` does. Saying "version" here promised something this
879
+ command does not do.
880
+ */
881
+ ` · the save holds ${bytes(result.sourceBytes)}`);
828
882
  return 0;
829
883
  }
830
884
  /**
@@ -1136,8 +1190,17 @@ trackName) {
1136
1190
  */
1137
1191
  async function suggestRules(folder, apply) {
1138
1192
  const rules = await (0, worktree_js_1.readRules)(folder);
1139
- const files = await (0, worktree_js_1.changedFiles)(folder, rules, null);
1140
- const suggestions = (0, detect_js_1.suggestExclusions)(await (0, worktree_js_1.fileSizes)(folder, files));
1193
+ /*
1194
+ Sizes for the whole folder, not for the listed rows.
1195
+
1196
+ This used to measure `changedFiles`, which stops at twenty thousand — so
1197
+ on the projects with the most to leave out, the sizes here were a fraction
1198
+ of the truth and the biggest item could be missing from the list outright.
1199
+ The scan carries a size for every file it walks past now, capped or not.
1200
+ */
1201
+ const listing = { listed: 0, total: 0, sizes: [] };
1202
+ await (0, worktree_js_1.changedFiles)(folder, rules, null, "add-and-update", undefined, listing);
1203
+ const suggestions = (0, detect_js_1.suggestExclusions)(listing.sizes);
1141
1204
  if (!suggestions.length) {
1142
1205
  console.log(dim("Nothing here looks like it should be left out."));
1143
1206
  return 0;
@@ -1556,11 +1619,14 @@ const SPECS = [
1556
1619
  name: "submit",
1557
1620
  aliases: ["publish", "push"],
1558
1621
  group: "Working with a folder",
1559
- summary: "send the changes as a new version",
1622
+ summary: "send the changes as a new save",
1560
1623
  usage: 'submit [folder] -m "…"',
1561
- detail: "Sends everything that changed since the last version. If the folder is\n" +
1624
+ detail: "Sends everything that changed since the last save. If the folder is\n" +
1562
1625
  "not linked to a project yet, one is created on your account, named\n" +
1563
- "after the folder and private to begin with.",
1626
+ "after the folder and private to begin with.\n\n" +
1627
+ "A save is not a version. Nobody outside the project can see one until\n" +
1628
+ "it is named, which is what `cbx release` does — so submitting is as\n" +
1629
+ "cheap and as private as you want it to be.",
1564
1630
  options: [
1565
1631
  { flags: "-m, --message <text>", description: "what changed, in a sentence" },
1566
1632
  { flags: "-n, --dry-run", description: "show what would be sent, send nothing" },
@@ -1847,11 +1913,21 @@ const SPECS = [
1847
1913
  group: "Your projects",
1848
1914
  summary: "issues on a project, or open one",
1849
1915
  usage: "issues [project]",
1916
+ detail: "Labels are given when the issue is opened rather than added\n" +
1917
+ "afterwards, and any name that does not exist yet is created.\n\n" +
1918
+ "--version says which save it is about. \"It broke\" and \"it broke in\n" +
1919
+ "v41\" are different reports, and the second is the one somebody can\n" +
1920
+ "act on.",
1850
1921
  options: [
1851
1922
  { flags: '--new "<title>"', description: "open a new issue" },
1852
1923
  { flags: "--body <text>", description: "the description for a new one" },
1924
+ { flags: "--labels <a,b>", description: "labels for a new one, comma separated" },
1925
+ { flags: "--version <n>", description: "which save it is about" },
1926
+ ],
1927
+ examples: [
1928
+ 'cbx issues my-project --new "Crash on export"',
1929
+ 'cbx issues --new "Installer will not run" --labels bug,windows --version 41',
1853
1930
  ],
1854
- examples: ['cbx issues my-project --new "Crash on export"'],
1855
1931
  run: service_commands_js_1.commandIssues,
1856
1932
  },
1857
1933
  {
@@ -1862,6 +1938,37 @@ const SPECS = [
1862
1938
  usage: "releases [project]",
1863
1939
  run: service_commands_js_1.commandReleases,
1864
1940
  },
1941
+ {
1942
+ /*
1943
+ The service has been able to notify something else since Merge Tracks
1944
+ shipped — queue an event, sign it, deliver it after the request that
1945
+ caused it — and nothing could reach it, so no project could be told to
1946
+ tell a build box or a status page anything.
1947
+ */
1948
+ name: "hooks",
1949
+ aliases: ["webhooks"],
1950
+ group: "Your projects",
1951
+ summary: "where this project tells something else what happened",
1952
+ usage: "hooks [project]",
1953
+ detail: "A signed POST to an address of yours when something happens here.\n" +
1954
+ "The signing secret is generated by the service and shown once, when\n" +
1955
+ "the hook is added — no route hands it back afterwards.\n\n" +
1956
+ "Events: version.published, merge.opened, merge.applied,\n" +
1957
+ "check.reported, change_request.opened, change_request.reviewed.\n" +
1958
+ "Naming none of them sends all of them.",
1959
+ options: [
1960
+ { flags: "--add <url>", description: "notify this address; https only" },
1961
+ { flags: "--events <a,b>", description: "only these, comma separated" },
1962
+ { flags: "--remove <id>", description: "stop notifying it" },
1963
+ { flags: "--project <name>", description: "which project" },
1964
+ ],
1965
+ examples: [
1966
+ "cbx hooks",
1967
+ "cbx hooks --add https://example.com/coderook",
1968
+ "cbx hooks --add https://example.com/builds --events version.published",
1969
+ ],
1970
+ run: service_commands_js_1.commandHooks,
1971
+ },
1865
1972
  {
1866
1973
  /*
1867
1974
  The half that hiding never covered.
@@ -1925,6 +2032,30 @@ const SPECS = [
1925
2032
  that works until somebody scripts it. This marks a save — pins it,
1926
2033
  hides it, labels it — which is what it does anyway.
1927
2034
  */
2035
+ name: "attach",
2036
+ group: "Your projects",
2037
+ summary: "put a file people can download on a version",
2038
+ usage: "attach <file> [n] [project]",
2039
+ detail: "A version can carry downloads — an installer, a build, a changelog —\n" +
2040
+ "and until now the only thing that could put one there was a workflow\n" +
2041
+ "run. This attaches a file from this machine.\n\n" +
2042
+ "Attaching is not publishing. A download on a commit is reachable by\n" +
2043
+ "nobody until that commit is made a version; on a version already\n" +
2044
+ "offered it is public the moment it lands.\n\n" +
2045
+ "Large files go up in parts, so a 95 MB installer works the same way a\n" +
2046
+ "text file does.",
2047
+ options: [
2048
+ { flags: "--name <text>", description: "call it something else on the version" },
2049
+ { flags: "--project <name>", description: "which project" },
2050
+ ],
2051
+ examples: [
2052
+ "cbx attach ./release/Installer-1.0.exe",
2053
+ "cbx attach ./release/app.dmg 41",
2054
+ 'cbx attach ./notes.pdf --name "Release notes.pdf"',
2055
+ ],
2056
+ run: attach_command_js_1.commandAttach,
2057
+ },
2058
+ {
1928
2059
  name: "notes",
1929
2060
  aliases: ["release-notes"],
1930
2061
  group: "Your projects",
@@ -2082,7 +2213,22 @@ const SPECS = [
2082
2213
  summary: "the automations a project has",
2083
2214
  usage: "actions [project]",
2084
2215
  detail: "Shows what each action runs, what it runs on, and how many of its\n" +
2085
- "runs have passed. Use `cbx runner` to execute them on this machine.",
2216
+ "runs have passed. Use `cbx runner` to execute them on this machine.\n\n" +
2217
+ "An action that keeps files can hand them straight to the version it\n" +
2218
+ "ran against, so a passing build becomes a download without anybody\n" +
2219
+ "opening the website. --ship turns that on for one action.\n\n" +
2220
+ "Attaching is publishing: what an action ships onto a public version\n" +
2221
+ "is a download anybody can take, from the moment the run passes.",
2222
+ options: [
2223
+ { flags: "--ship <action>", description: "its builds become downloads on the version" },
2224
+ { flags: "--no-ship <action>", description: "keep its files without attaching them" },
2225
+ { flags: "--project <name>", description: "which project" },
2226
+ ],
2227
+ examples: [
2228
+ "cbx actions",
2229
+ 'cbx actions --ship "Windows installer"',
2230
+ 'cbx actions --no-ship "Windows installer"',
2231
+ ],
2086
2232
  run: service_commands_js_1.commandWorkflows,
2087
2233
  },
2088
2234
  {