@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.
@@ -1045,12 +1045,18 @@ async function doPush(requests, url) {
1045
1045
  continue;
1046
1046
  }
1047
1047
  /*
1048
- Said before the work starts, not after. Each commit is a published
1049
- version and versions are not free to make, so somebody pushing years
1048
+ Said before the work starts, not after. Each commit becomes a save on
1049
+ the project and saves are not free to make, so somebody pushing years
1050
1050
  of history deserves the chance to stop and import a snapshot instead.
1051
+
1052
+ "to publish as versions" is what this used to say, and it stopped
1053
+ being true when commits and versions separated: a pushed commit is a
1054
+ save that nobody outside the project can see. Publishing one is what
1055
+ pushing a *tag* does, because a tag is a name and a named save is a
1056
+ version.
1051
1057
  */
1052
1058
  const estimate = Math.round((commits.length * 11) / 60);
1053
- say(` ${commits.length} commit${commits.length === 1 ? "" : "s"} to publish as version${commits.length === 1 ? "" : "s"}` +
1059
+ say(` ${commits.length} commit${commits.length === 1 ? "" : "s"} to send as save${commits.length === 1 ? "" : "s"}` +
1054
1060
  (commits.length > 20 ? ` — roughly ${estimate} minute${estimate === 1 ? "" : "s"}` : ""));
1055
1061
  /*
1056
1062
  A push that continues a branch has to start from where the branch
@@ -1114,8 +1120,8 @@ async function doPush(requests, url) {
1114
1120
  .slice(1);
1115
1121
  if (parents.length > 1 && !warnedAboutMerges) {
1116
1122
  warnedAboutMerges = true;
1117
- say(` note: merge commits are published as one version holding the ` +
1118
- `merged tree.\n The contents are exact; the branch shape is not kept.`);
1123
+ say(` note: a merge commit arrives as one save holding the merged ` +
1124
+ `tree.\n The contents are exact; the branch shape is not kept.`);
1119
1125
  }
1120
1126
  const changes = await applyCommit(catFile, scratch, previous, sha);
1121
1127
  previous = sha;
@@ -21,6 +21,7 @@ exports.commandRuns = commandRuns;
21
21
  exports.commandLogs = commandLogs;
22
22
  exports.commandTokens = commandTokens;
23
23
  exports.commandDelete = commandDelete;
24
+ exports.commandHooks = commandHooks;
24
25
  const node_process_1 = __importDefault(require("node:process"));
25
26
  const promises_1 = require("node:readline/promises");
26
27
  const api_js_1 = require("./api.js");
@@ -51,11 +52,47 @@ async function commandIssues(parsed) {
51
52
  }
52
53
  if (typeof title === "string" && title.trim()) {
53
54
  const body = parsed.flags.get("body");
55
+ /*
56
+ Labels at the moment it is opened, rather than in a second command
57
+ afterwards. The service has always taken them here; nothing had ever
58
+ sent any, so every issue arrived unlabelled and the label filter on the
59
+ website had nothing to filter by until somebody went back and tidied.
60
+ */
61
+ const named = parsed.flags.get("labels");
62
+ const labels = typeof named === "string"
63
+ ? named
64
+ .split(",")
65
+ .map((one) => one.trim())
66
+ .filter(Boolean)
67
+ : [];
68
+ /*
69
+ And which save it is about, when it is about one. "It broke" and "it
70
+ broke in v41" are different reports, and only the first could be
71
+ written down.
72
+ */
73
+ const about = parsed.flags.get("version");
74
+ let versionId;
75
+ if (typeof about === "string" && about.trim()) {
76
+ const wanted = about.trim().replace(/^v/i, "");
77
+ const all = await (0, api_js_1.versions)(project.id);
78
+ const found = all.find((one) => String(one.sequence) === wanted);
79
+ if (!found) {
80
+ console.error(red(`${project.name} has no version ${about.trim()}.`));
81
+ return 1;
82
+ }
83
+ versionId = found.id;
84
+ }
54
85
  const created = await (0, api_js_1.createIssue)(project.id, {
55
86
  title: title.trim(),
56
87
  body: typeof body === "string" ? body : "",
88
+ labels,
89
+ ...(versionId ? { versionId } : {}),
57
90
  });
58
91
  console.log(green(`Opened issue #${created.number} on ${project.name}.`));
92
+ if (labels.length)
93
+ console.log(dim(` labelled ${labels.join(", ")}`));
94
+ if (versionId)
95
+ console.log(dim(` about v${about}`));
59
96
  return 0;
60
97
  }
61
98
  const found = await (0, api_js_1.issues)(project.id);
@@ -216,10 +253,67 @@ async function commandWatch(parsed) {
216
253
  }
217
254
  /** The automations a project has, and how they have been going. */
218
255
  async function commandWorkflows(parsed) {
219
- const project = await (0, project_commands_js_1.resolveProject)(parsed.positional[0]);
256
+ const project = await (0, project_commands_js_1.resolveProject)(typeof parsed.flags.get("project") === "string"
257
+ ? String(parsed.flags.get("project"))
258
+ : parsed.positional[0]);
220
259
  if (!project)
221
260
  return 1;
222
261
  const found = (await (0, api_js_1.workflows)(project.id)).filter((workflow) => !workflow.archivedAt);
262
+ /*
263
+ Setting, before printing. `cbx actions --ship Build` reads as a sentence
264
+ and then shows the list it has just changed, which is the same shape
265
+ `cbx notes` uses: one command, print by default, flags to write.
266
+ */
267
+ const ship = parsed.flags.get("ship");
268
+ const stop = parsed.flags.get("no-ship");
269
+ if (ship !== undefined || stop !== undefined) {
270
+ const wanted = ship !== undefined;
271
+ const named = wanted ? ship : stop;
272
+ if (typeof named !== "string" || !named.trim()) {
273
+ console.error(red("Which action?") + dim(` cbx actions ${wanted ? "--ship" : "--no-ship"} "Build"`));
274
+ return 1;
275
+ }
276
+ const target = found.find((workflow) => workflow.name.toLowerCase() === named.trim().toLowerCase());
277
+ if (!target) {
278
+ console.error(red(`${project.name} has no action called ${named.trim()}.`));
279
+ if (found.length) {
280
+ console.error(dim(` It has: ${found.map((one) => one.name).join(", ")}`));
281
+ }
282
+ return 1;
283
+ }
284
+ if (wanted && !target.artifactPaths.length) {
285
+ /*
286
+ Refused rather than set. Shipping what a run collected means nothing
287
+ when the run collects nothing, and a flag that reported success and
288
+ then never produced a download would be indistinguishable from the
289
+ feature being broken.
290
+ */
291
+ console.error(red(`${target.name} keeps no files, so it has nothing to ship.`));
292
+ console.error(dim(" Give it something to keep on the Actions screen first, e.g. dist/*.exe"));
293
+ return 1;
294
+ }
295
+ try {
296
+ await (0, api_js_1.changeWorkflow)(project.id, target.id, { attachArtifacts: wanted });
297
+ }
298
+ catch (error) {
299
+ console.error(red(error instanceof Error ? error.message : String(error)));
300
+ return 1;
301
+ }
302
+ target.attachArtifacts = wanted;
303
+ console.log(wanted
304
+ ? green(`${target.name} will attach what it builds to the version it ran against.`)
305
+ : green(`${target.name} will keep its files without attaching them.`));
306
+ if (wanted) {
307
+ /*
308
+ Said plainly, because attaching is publishing. An attachment on a
309
+ public version is a download anybody can take, and somebody turning
310
+ this on for a workflow that builds an internal tool deserves to hear
311
+ that before the next run rather than after it.
312
+ */
313
+ console.log(dim(" Anything it attaches to a public version is public the moment it lands."));
314
+ }
315
+ console.log("");
316
+ }
223
317
  if (!found.length) {
224
318
  console.log(dim("No actions on this project."));
225
319
  return 0;
@@ -232,6 +326,12 @@ async function commandWorkflows(parsed) {
232
326
  console.log(` ${workflow.name.padEnd(24)} ${dim(workflow.trigger).padEnd(20)} ` +
233
327
  `${dim(workflow.runsOn).padEnd(18)} ${health}`);
234
328
  console.log(` ${dim(workflow.command)}`);
329
+ if (workflow.artifactPaths.length) {
330
+ console.log(` ${dim("keeps")} ${dim(workflow.artifactPaths.join(", "))}` +
331
+ (workflow.attachArtifacts
332
+ ? ` ${accent("→ downloads on the version")}`
333
+ : ""));
334
+ }
235
335
  }
236
336
  return 0;
237
337
  }
@@ -399,3 +499,103 @@ async function commandDelete(parsed) {
399
499
  console.log(green(`Deleted ${project.name}.`));
400
500
  return 0;
401
501
  }
502
+ /**
503
+ * Where a project tells something else that something happened.
504
+ *
505
+ * The service has been able to do this since Merge Tracks shipped — queue an
506
+ * event, sign it, deliver it after the request that caused it — and nothing
507
+ * could reach it. A project could not be told to notify a build box, a chat
508
+ * room or a status page, because no client asked.
509
+ */
510
+ async function commandHooks(parsed) {
511
+ const project = await (0, project_commands_js_1.resolveProject)(typeof parsed.flags.get("project") === "string"
512
+ ? String(parsed.flags.get("project"))
513
+ : parsed.positional[0]);
514
+ if (!project)
515
+ return 1;
516
+ const adding = parsed.flags.get("add");
517
+ if (adding !== undefined) {
518
+ if (typeof adding !== "string" || !adding.trim()) {
519
+ console.error(red("Which address?") + dim(" cbx hooks --add https://example.com/hook"));
520
+ return 1;
521
+ }
522
+ const url = adding.trim();
523
+ if (!/^https:\/\//i.test(url)) {
524
+ /*
525
+ Refused here as well as by the service. A webhook carries a signed
526
+ payload about a private project, and http would put it on the wire in
527
+ the clear — worth saying before the round trip rather than after.
528
+ */
529
+ console.error(red("A webhook address has to be https."));
530
+ return 1;
531
+ }
532
+ const named = parsed.flags.get("events");
533
+ const events = typeof named === "string"
534
+ ? named.split(",").map((one) => one.trim()).filter(Boolean)
535
+ : [];
536
+ try {
537
+ const made = await (0, api_js_1.addWebhook)(project.id, url, events);
538
+ console.log(green(`${project.name} will notify ${url}.`));
539
+ console.log("");
540
+ /*
541
+ Printed once because it exists once. The service generates it and no
542
+ route ever hands it back, so this is the only moment anybody can
543
+ write it down — and saying so is the difference between a person
544
+ copying it and a person losing it.
545
+ */
546
+ console.log(` ${bold("Signing secret")} ${made.secret}`);
547
+ console.log(dim(" Written once and never again. Use it to check the signature on"));
548
+ console.log(dim(" what arrives, so nobody else can post to your endpoint."));
549
+ if (events.length)
550
+ console.log(dim(` Sending: ${events.join(", ")}`));
551
+ else
552
+ console.log(dim(" Sending: everything this project announces"));
553
+ }
554
+ catch (error) {
555
+ console.error(red(error instanceof Error ? error.message : String(error)));
556
+ return 1;
557
+ }
558
+ return 0;
559
+ }
560
+ const removing = parsed.flags.get("remove");
561
+ if (removing !== undefined) {
562
+ if (typeof removing !== "string" || !removing.trim()) {
563
+ console.error(red("Which one?") + dim(" cbx hooks --remove <id>"));
564
+ return 1;
565
+ }
566
+ const wanted = removing.trim();
567
+ const found = (await (0, api_js_1.webhooks)(project.id)).find((one) => one.id === wanted || one.url === wanted);
568
+ if (!found) {
569
+ console.error(red(`${project.name} has no webhook called ${wanted}.`));
570
+ return 1;
571
+ }
572
+ await (0, api_js_1.removeWebhook)(project.id, found.id);
573
+ console.log(green(`${found.url} will not be notified any more.`));
574
+ return 0;
575
+ }
576
+ const found = await (0, api_js_1.webhooks)(project.id);
577
+ if (!found.length) {
578
+ console.log(dim("This project notifies nothing."));
579
+ console.log(dim(" cbx hooks --add https://example.com/coderook"));
580
+ return 0;
581
+ }
582
+ console.log(bold(project.name));
583
+ for (const hook of found) {
584
+ /*
585
+ A run of failures shown rather than hidden. A webhook that has been
586
+ failing for a week looks exactly like one that works until somebody
587
+ checks the receiving end, which is the wrong place to find out.
588
+ */
589
+ const health = !hook.active
590
+ ? red("off")
591
+ : hook.consecutiveFailures
592
+ ? red(`${hook.consecutiveFailures} failed in a row`)
593
+ : hook.lastDeliveredAt
594
+ ? green("delivering")
595
+ : dim("nothing sent yet");
596
+ console.log(` ${hook.url}`);
597
+ console.log(` ${dim(hook.id)} ${health}` +
598
+ (hook.events.length ? ` ${dim(hook.events.join(", "))}` : ` ${dim("everything")}`));
599
+ }
600
+ return 0;
601
+ }
@@ -39,6 +39,40 @@ const RULES = [
39
39
  { directory: ".next", reason: "Next.js build output", recommended: true },
40
40
  { directory: ".gradle", reason: "Gradle build state", recommended: true },
41
41
  { directory: ".terraform", reason: "Downloaded Terraform providers", recommended: true },
42
+ /*
43
+ Game engines, which is where the size actually is.
44
+
45
+ A Unity project is mostly not the project: `Library/` alone is routinely
46
+ thousands of times the size of `Assets/`, and every byte of it is rebuilt
47
+ from `Assets/` and `Packages/` the next time the editor opens. Until these
48
+ existed a Unity folder produced no suggestions at all — the one shape of
49
+ project where the question "what should I leave out?" has the largest
50
+ possible answer was the one the detector had nothing to say about.
51
+ */
52
+ { directory: "Library", reason: "Unity's import cache — rebuilt when the project next opens", recommended: true },
53
+ { directory: "Temp", reason: "Unity's scratch folder for the running editor", recommended: true },
54
+ { directory: "MemoryCaptures", reason: "Unity memory snapshots — large, and not the project", recommended: true },
55
+ { directory: "Recordings", reason: "Unity recorder output", recommended: true },
56
+ { directory: "UserSettings", reason: "Your own editor layout — not part of the project", recommended: true },
57
+ { directory: "DerivedDataCache", reason: "Unreal's derived data cache — rebuilt on demand", recommended: true },
58
+ { directory: "Intermediate", reason: "Unreal build intermediates — rebuilt when you build", recommended: true },
59
+ { directory: "Binaries", reason: "Unreal compiled output — rebuilt when you build", recommended: true },
60
+ /*
61
+ Named like the work, and it is not: Unreal keeps logs, crash reports and
62
+ autosaves here. Offered rather than ticked, because a folder called
63
+ "Saved" is the one nobody should have excluded on our say-so.
64
+ */
65
+ { directory: "Saved", reason: "Unreal logs and autosaves — check before excluding", recommended: false },
66
+ { directory: ".godot", reason: "Godot's import cache — rebuilt when the project next opens", recommended: true },
67
+ { directory: ".import", reason: "Godot's import cache — rebuilt when the project next opens", recommended: true },
68
+ { directory: "Logs", reason: "Editor logs", recommended: true },
69
+ { directory: "obj", reason: "Compiler intermediates — rebuilt when you build", recommended: true },
70
+ { directory: ".vs", reason: "Visual Studio's local cache", recommended: true },
71
+ /*
72
+ Offered, not ticked. JetBrains keeps run configurations in here that some
73
+ projects deliberately share, so this is a judgement rather than a fact.
74
+ */
75
+ { directory: ".idea", reason: "JetBrains editor state — check before excluding", recommended: false },
42
76
  /*
43
77
  A Chromium or Electron profile left in the project folder. Worth its own
44
78
  reason because the consequence is not only size: these hold lock files
@@ -59,6 +93,7 @@ const RULES = [
59
93
  { directory: "dist", reason: "Usually build output — check before excluding", recommended: false },
60
94
  { directory: "build", reason: "Usually build output — check before excluding", recommended: false },
61
95
  { directory: "out", reason: "Usually build output — check before excluding", recommended: false },
96
+ { directory: "bin", reason: "Usually build output — check before excluding", recommended: false },
62
97
  { extension: ".safetensors", reason: "Model weights — large, and usually downloadable", recommended: false },
63
98
  { extension: ".ckpt", reason: "Model weights — large, and usually downloadable", recommended: false },
64
99
  { extension: ".pt", reason: "Model weights — large, and usually downloadable", recommended: false },
@@ -86,13 +121,29 @@ function suggestExclusions(files) {
86
121
  const extension = node_path_1.default.extname(name).toLowerCase();
87
122
  for (const rule of RULES) {
88
123
  let pattern = null;
89
- if (rule.directory && segments.slice(0, -1).includes(rule.directory)) {
90
- pattern = `${rule.directory}/`;
124
+ if (rule.directory) {
125
+ /*
126
+ Matched without regard to case, and written back with the case the
127
+ folder actually has.
128
+
129
+ Every engine disagrees about capitals — Unity ships `Library` and
130
+ `obj` in the same project, Unreal `Binaries`, Godot `.godot` — and
131
+ an exact-name match meant the detector recognised whichever spelling
132
+ happened to be written here and silently missed the rest. Emitting
133
+ the observed name rather than the rule's own keeps the line that
134
+ gets written matching the folder that is there.
135
+ */
136
+ const wanted = rule.directory.toLowerCase();
137
+ const found = segments
138
+ .slice(0, -1)
139
+ .find((segment) => segment.toLowerCase() === wanted);
140
+ if (found)
141
+ pattern = `${found}/`;
91
142
  }
92
- else if (rule.file && name === rule.file) {
143
+ if (!pattern && rule.file && name === rule.file) {
93
144
  pattern = rule.file;
94
145
  }
95
- else if (rule.extension && extension === rule.extension) {
146
+ if (!pattern && rule.extension && extension === rule.extension) {
96
147
  pattern = `*${rule.extension}`;
97
148
  }
98
149
  if (!pattern)
@@ -1,8 +1,66 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Tracks = void 0;
4
+ /**
5
+ * The lines a project is saved on, and the merges waiting on a person.
6
+ *
7
+ * The service has had all of this since Tracks existed: a project has one or
8
+ * more lines of versions, and a publish that diverges from what the account
9
+ * holds is put on a Merge Track of its own rather than laid over work the
10
+ * uploader had not seen. That is the right behaviour and it already happens.
11
+ *
12
+ * What did not exist was any way to know. The app never asked for a track, so
13
+ * a save that diverted reported "done" and said nothing about where the work
14
+ * went — the version was safe, on a track nobody could see, and the only way
15
+ * to find it was the website. This is the half that was missing.
16
+ */
17
+ const node_crypto_1 = require("node:crypto");
4
18
  const identify_js_1 = require("./identify.js");
5
19
  const retry_js_1 = require("./retry.js");
20
+ /** A media type from the name, so an edited file is stored as what it is. */
21
+ function mediaTypeForPath(path) {
22
+ const known = {
23
+ css: "text/css",
24
+ html: "text/html",
25
+ js: "text/javascript",
26
+ json: "application/json",
27
+ md: "text/markdown",
28
+ py: "text/x-python",
29
+ ts: "text/typescript",
30
+ tsx: "text/typescript",
31
+ xml: "application/xml",
32
+ yaml: "application/yaml",
33
+ yml: "application/yaml",
34
+ };
35
+ return known[path.split(".").pop()?.toLowerCase() ?? ""] ?? "text/plain";
36
+ }
37
+ /**
38
+ * The error to raise for a failed request, with a refusal said properly.
39
+ *
40
+ * A build refused for being too old is not a network failure and must not
41
+ * read like one: the person can fix it, and only if they are told how. The
42
+ * two requests below build their own fetches rather than going through
43
+ * `call`, so without this a refused desktop was told only "(426)".
44
+ */
45
+ function refusalOrError(body, status, fallback) {
46
+ const tooOld = (0, identify_js_1.readRefusal)(status, body);
47
+ if (tooOld) {
48
+ return new Error(`${tooOld.message} You are running ${tooOld.yourVersion ?? "an older build"}; ` +
49
+ `update from ${tooOld.upgradeUrl}.`);
50
+ }
51
+ let message = `${fallback} (${status})`;
52
+ if (body.trimStart().startsWith("{")) {
53
+ try {
54
+ message =
55
+ JSON.parse(body).error?.message ??
56
+ message;
57
+ }
58
+ catch {
59
+ /* keep the generic message */
60
+ }
61
+ }
62
+ return Object.assign(new Error(message), { status });
63
+ }
6
64
  class Tracks {
7
65
  credentials;
8
66
  constructor(credentials) {
@@ -177,17 +235,106 @@ class Tracks {
177
235
  return [];
178
236
  }
179
237
  }
180
- /** One merge and every path it is waiting on. */
238
+ /**
239
+ * One merge and every path it is waiting on.
240
+ *
241
+ * The merge itself is nested under `mergeTrack`, which this used to spread
242
+ * flat — so `track.reference` was undefined and the window's title fell
243
+ * back to the word "Merge" on every merge there has ever been. The rest of
244
+ * the reply was thrown away with it, including the two version ids that
245
+ * make showing the conflict possible at all.
246
+ */
181
247
  async merge(mergeTrackId) {
182
248
  const body = await this.call(`/v1/merge-tracks/${mergeTrackId}`);
183
- const { conflicts = [], ...track } = body;
184
- return { track: track, conflicts };
249
+ return {
250
+ track: body.mergeTrack,
251
+ conflicts: body.conflicts ?? [],
252
+ repositoryId: body.mergeTrack.repositoryId,
253
+ candidateVersionId: body.mergeTrack.candidateVersionId ?? null,
254
+ currentTargetVersionId: body.currentTargetVersionId ?? null,
255
+ checks: body.checks ?? [],
256
+ ready: body.provisional?.ready ?? false,
257
+ unresolvedPaths: body.provisional?.unresolvedPaths ?? [],
258
+ blockedByChecks: body.provisional?.blockedByChecks ?? [],
259
+ };
260
+ }
261
+ /**
262
+ * One side of a conflict, as text.
263
+ *
264
+ * Both sides of a merge are versions, so this is the ordinary file route.
265
+ * Null means there is genuinely no file on that side, which is half of
266
+ * these conflicts: one person edited what another deleted.
267
+ */
268
+ async fileAt(repositoryId, versionId, path) {
269
+ if (!versionId)
270
+ return null;
271
+ const token = await this.credentials.token();
272
+ if (!token)
273
+ throw new Error("Sign in again");
274
+ const response = await fetch(`${this.credentials.origin()}/v1/repositories/${repositoryId}` +
275
+ `/versions/${versionId}/file?path=${encodeURIComponent(path)}`, {
276
+ headers: {
277
+ authorization: `Bearer ${token}`,
278
+ "user-agent": "CodeRook/0.1",
279
+ ...(0, identify_js_1.clientHeaders)(),
280
+ },
281
+ });
282
+ if (response.status === 404)
283
+ return null;
284
+ if (!response.ok) {
285
+ throw refusalOrError(await response.text().catch(() => ""), response.status, `Could not read ${path}`);
286
+ }
287
+ const bytes = new Uint8Array(await response.arrayBuffer());
288
+ /*
289
+ Decoded strictly, so a file that is not UTF-8 reports itself as binary
290
+ rather than arriving as a screenful of replacement characters somebody
291
+ might then save over the real thing.
292
+ */
293
+ let text = null;
294
+ try {
295
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
296
+ if (text.includes("\u0000"))
297
+ text = null;
298
+ }
299
+ catch {
300
+ text = null;
301
+ }
302
+ return { text, bytes: bytes.byteLength };
303
+ }
304
+ /**
305
+ * Send a file somebody wrote, and answer with the object it became.
306
+ *
307
+ * The other half of an edited resolution: the service takes an object id,
308
+ * and something has to put the bytes there first.
309
+ */
310
+ async putText(repositoryId, path, text) {
311
+ const token = await this.credentials.token();
312
+ if (!token)
313
+ throw new Error("Sign in again");
314
+ const bytes = new TextEncoder().encode(text);
315
+ const digest = (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex");
316
+ const response = await fetch(`${this.credentials.origin()}/v1/repositories/${repositoryId}` +
317
+ `/objects/${digest}?kind=chunk&role=chunk&logicalSize=${bytes.byteLength}` +
318
+ `&mediaType=${encodeURIComponent(mediaTypeForPath(path))}`, {
319
+ method: "PUT",
320
+ headers: {
321
+ authorization: `Bearer ${token}`,
322
+ "content-type": "application/octet-stream",
323
+ "user-agent": "CodeRook/0.1",
324
+ ...(0, identify_js_1.clientHeaders)(),
325
+ },
326
+ body: bytes,
327
+ });
328
+ const body = await response.text();
329
+ if (!response.ok)
330
+ throw refusalOrError(body, response.status, "Could not send the file");
331
+ return JSON.parse(body).objectId;
185
332
  }
186
- /** Settle one path. */
187
- async resolve(mergeTrackId, conflictId, resolution) {
333
+ /** Settle one path. An edit also names the object it produced. */
334
+ async resolve(mergeTrackId, conflictId, resolution, objectId) {
188
335
  await this.call(`/v1/merge-tracks/${mergeTrackId}/conflicts/${conflictId}`, {
189
336
  method: "PUT",
190
- body: JSON.stringify({ resolution }),
337
+ body: JSON.stringify(objectId ? { resolution, objectId } : { resolution }),
191
338
  });
192
339
  }
193
340
  /**
@@ -354,7 +354,15 @@ class Uploader {
354
354
  */
355
355
  const everything = await (0, profile_js_1.timed)("survey the project", () => (0, worktree_js_1.surveyFiles)(request.localPath, rules));
356
356
  (0, profile_js_1.counted)("files surveyed", everything.length);
357
- const ticked = new Set(request.include);
357
+ const unticked = new Set(request.excluded ?? []);
358
+ const ticked = request.excluded
359
+ ? new Set([
360
+ ...everything
361
+ .map((file) => file.path)
362
+ .filter((path) => !unticked.has(path)),
363
+ ...(request.deletions ?? []),
364
+ ])
365
+ : new Set(request.include);
358
366
  if (!ticked.size)
359
367
  throw new Error("Nothing is selected to upload");
360
368
  const prior = request.repositoryId