@coderook/cli 0.26.0 → 0.28.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 +156 -1
- package/dist/cli/src/attach_command.js +217 -0
- package/dist/cli/src/cli.js +238 -27
- package/dist/cli/src/git_remote.js +100 -15
- package/dist/cli/src/project_commands.js +46 -5
- package/dist/cli/src/service_commands.js +201 -1
- package/dist/cli/src/version_commands.js +163 -4
- package/dist/desktop-app/src/main/download.js +15 -6
- package/dist/desktop-app/src/main/tracks.js +171 -6
- package/dist/desktop-app/src/main/upload.js +4 -0
- package/dist/desktop-app/src/main/worktree.js +26 -8
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -66,6 +124,24 @@ class Tracks {
|
|
|
66
124
|
const body = await this.call(`/v1/repositories/${repositoryId}/versions`);
|
|
67
125
|
return body.versions ?? [];
|
|
68
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* What the account says about this project, as distinct from this folder.
|
|
129
|
+
*
|
|
130
|
+
* The local record knows where a folder saves to and nothing about how the
|
|
131
|
+
* project is published — so the history window could show which saves were
|
|
132
|
+
* public without being able to say whether the project itself was, which is
|
|
133
|
+
* the half that decides whether anybody can reach them.
|
|
134
|
+
*/
|
|
135
|
+
async project(repositoryId) {
|
|
136
|
+
try {
|
|
137
|
+
const body = await this.call(`/v1/repositories/${repositoryId}`);
|
|
138
|
+
return { visibility: body.visibility ?? null };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* Not knowing is not worth failing the window for. */
|
|
142
|
+
return { visibility: null };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
69
145
|
async labels(repositoryId) {
|
|
70
146
|
const body = await this.call(`/v1/repositories/${repositoryId}/labels`);
|
|
71
147
|
return body.labels ?? [];
|
|
@@ -159,17 +235,106 @@ class Tracks {
|
|
|
159
235
|
return [];
|
|
160
236
|
}
|
|
161
237
|
}
|
|
162
|
-
/**
|
|
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
|
+
*/
|
|
163
247
|
async merge(mergeTrackId) {
|
|
164
248
|
const body = await this.call(`/v1/merge-tracks/${mergeTrackId}`);
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
167
332
|
}
|
|
168
|
-
/** Settle one path. */
|
|
169
|
-
async resolve(mergeTrackId, conflictId, resolution) {
|
|
333
|
+
/** Settle one path. An edit also names the object it produced. */
|
|
334
|
+
async resolve(mergeTrackId, conflictId, resolution, objectId) {
|
|
170
335
|
await this.call(`/v1/merge-tracks/${mergeTrackId}/conflicts/${conflictId}`, {
|
|
171
336
|
method: "PUT",
|
|
172
|
-
body: JSON.stringify({ resolution }),
|
|
337
|
+
body: JSON.stringify(objectId ? { resolution, objectId } : { resolution }),
|
|
173
338
|
});
|
|
174
339
|
}
|
|
175
340
|
/**
|
|
@@ -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.
|
|
@@ -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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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;
|