@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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/src/api.js +155 -1
- package/dist/cli/src/attach_command.js +217 -0
- package/dist/cli/src/cli.js +154 -8
- package/dist/cli/src/git_remote.js +11 -5
- package/dist/cli/src/service_commands.js +201 -1
- package/dist/desktop-app/src/main/detect.js +55 -4
- package/dist/desktop-app/src/main/tracks.js +153 -6
- package/dist/desktop-app/src/main/upload.js +9 -1
- package/dist/desktop-app/src/main/worktree.js +167 -99
- package/package.json +1 -1
|
@@ -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.
|
|
5
|
+
"version": "0.29.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/api.js
CHANGED
|
@@ -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`, {
|
|
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) => `[2m${value}[0m`;
|
|
32
|
+
const bold = (value) => `[1m${value}[0m`;
|
|
33
|
+
const red = (value) => `[31m${value}[0m`;
|
|
34
|
+
const green = (value) => `[32m${value}[0m`;
|
|
35
|
+
const accent = (value) => `[33m${value}[0m`;
|
|
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
|
+
}
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
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
|
|
1622
|
+
summary: "send the changes as a new save",
|
|
1560
1623
|
usage: 'submit [folder] -m "…"',
|
|
1561
|
-
detail: "Sends everything that changed since the last
|
|
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
|
{
|