@coderook/cli 0.1.0 → 0.3.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/dist/cli/src/api.js +24 -0
- package/dist/cli/src/cli.js +268 -23
- package/dist/desktop-app/src/main/upload.js +14 -1
- package/package.json +36 -36
package/dist/cli/src/api.js
CHANGED
|
@@ -4,6 +4,11 @@ exports.whoami = whoami;
|
|
|
4
4
|
exports.projects = projects;
|
|
5
5
|
exports.findProject = findProject;
|
|
6
6
|
exports.health = health;
|
|
7
|
+
exports.mergeTracks = mergeTracks;
|
|
8
|
+
exports.mergeTrack = mergeTrack;
|
|
9
|
+
exports.resolveMergeConflict = resolveMergeConflict;
|
|
10
|
+
exports.applyMerge = applyMerge;
|
|
11
|
+
exports.cancelMerge = cancelMerge;
|
|
7
12
|
/** The small part of the API the command-line tool needs directly. */
|
|
8
13
|
const config_js_1 = require("./config.js");
|
|
9
14
|
async function call(route, options = {}) {
|
|
@@ -65,3 +70,22 @@ async function health() {
|
|
|
65
70
|
});
|
|
66
71
|
return (await response.json());
|
|
67
72
|
}
|
|
73
|
+
async function mergeTracks(repositoryId) {
|
|
74
|
+
const body = await call(`/v1/repositories/${repositoryId}/merge-tracks`);
|
|
75
|
+
return body.mergeTracks;
|
|
76
|
+
}
|
|
77
|
+
async function mergeTrack(id) {
|
|
78
|
+
return call(`/v1/merge-tracks/${id}`);
|
|
79
|
+
}
|
|
80
|
+
async function resolveMergeConflict(mergeTrackId, conflictId, resolution) {
|
|
81
|
+
await call(`/v1/merge-tracks/${mergeTrackId}/conflicts/${conflictId}`, {
|
|
82
|
+
method: "PUT",
|
|
83
|
+
body: { resolution },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async function applyMerge(mergeTrackId) {
|
|
87
|
+
return call(`/v1/merge-tracks/${mergeTrackId}/apply`, { method: "POST" });
|
|
88
|
+
}
|
|
89
|
+
async function cancelMerge(mergeTrackId) {
|
|
90
|
+
await call(`/v1/merge-tracks/${mergeTrackId}/cancel`, { method: "POST" });
|
|
91
|
+
}
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -18,13 +18,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
const promises_1 = require("node:readline/promises");
|
|
19
19
|
const node_path_1 = __importDefault(require("node:path"));
|
|
20
20
|
const node_process_1 = __importDefault(require("node:process"));
|
|
21
|
+
const api_js_1 = require("./api.js");
|
|
21
22
|
const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
|
|
22
23
|
const upload_js_1 = require("../../desktop-app/src/main/upload.js");
|
|
23
24
|
const download_js_1 = require("../../desktop-app/src/main/download.js");
|
|
24
25
|
const cbx_js_1 = require("../../desktop-app/src/main/cbx.js");
|
|
25
|
-
const
|
|
26
|
+
const api_js_2 = require("./api.js");
|
|
26
27
|
const config_js_1 = require("./config.js");
|
|
27
|
-
|
|
28
|
+
/*
|
|
29
|
+
Read from the package rather than written twice. A hardcoded copy had
|
|
30
|
+
already drifted from the published version, which makes `coderook doctor`
|
|
31
|
+
worse than useless when working out what somebody is actually running.
|
|
32
|
+
*/
|
|
33
|
+
const VERSION = (() => {
|
|
34
|
+
try {
|
|
35
|
+
return (require("../../../package.json").version ?? "0.0.0");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "0.0.0";
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
28
41
|
// ── presentation ────────────────────────────────────────────────────────────
|
|
29
42
|
const colour = node_process_1.default.stdout.isTTY && !node_process_1.default.env.NO_COLOR;
|
|
30
43
|
const dim = (text) => (colour ? `[2m${text}[0m` : text);
|
|
@@ -94,20 +107,51 @@ const folderFor = (parsed, index = 0) => node_path_1.default.resolve(parsed.posi
|
|
|
94
107
|
* what that repository already holds. The account is the authority, exactly
|
|
95
108
|
* as it is in the desktop application.
|
|
96
109
|
*/
|
|
110
|
+
/**
|
|
111
|
+
* Work out what this folder is based on, and whether the account has moved.
|
|
112
|
+
*
|
|
113
|
+
* The base is the version this folder was last brought level with — not
|
|
114
|
+
* whatever happens to be newest. Silently re-basing on the newest is what
|
|
115
|
+
* makes a folder look up to date when somebody else has published: the
|
|
116
|
+
* changes are then measured against their work, and saving quietly lays
|
|
117
|
+
* this machine's copy over theirs.
|
|
118
|
+
*
|
|
119
|
+
* So a folder that has fallen behind keeps its own base and is told, which
|
|
120
|
+
* is what lets the service merge the two rather than pick a winner.
|
|
121
|
+
*/
|
|
97
122
|
async function reconcile(folder, options = {}) {
|
|
98
123
|
const link = await (0, config_js_1.readLink)(folder);
|
|
99
124
|
const reference = link?.slug ?? node_path_1.default.basename(folder);
|
|
100
125
|
const project = link
|
|
101
|
-
? (await (0,
|
|
102
|
-
: await (0,
|
|
126
|
+
? (await (0, api_js_2.projects)()).find((candidate) => candidate.id === link.repositoryId)
|
|
127
|
+
: await (0, api_js_2.findProject)(reference);
|
|
103
128
|
if (!project?.versionCount) {
|
|
104
129
|
// Nothing saved on the account, so everything here is outstanding.
|
|
105
|
-
return { link: link ?? null, baseline: null };
|
|
130
|
+
return { link: link ?? null, baseline: null, behind: null };
|
|
106
131
|
}
|
|
107
132
|
const downloader = new download_js_1.Downloader(config_js_1.credentials);
|
|
108
133
|
const latest = (await downloader.versions(project.id))[0];
|
|
109
134
|
if (!latest)
|
|
110
|
-
return { link: link ?? null, baseline: null };
|
|
135
|
+
return { link: link ?? null, baseline: null, behind: null };
|
|
136
|
+
/*
|
|
137
|
+
A folder that already knows which version it stands on keeps it. Only a
|
|
138
|
+
folder that has never been reconciled — or was linked before versions
|
|
139
|
+
were recorded — adopts the newest as its starting point.
|
|
140
|
+
*/
|
|
141
|
+
const known = link?.versionId && link.manifest ? link : null;
|
|
142
|
+
const behind = known && known.versionId !== latest.id
|
|
143
|
+
? { sequence: latest.sequence, id: latest.id }
|
|
144
|
+
: null;
|
|
145
|
+
if (known) {
|
|
146
|
+
if (behind && !options.quiet) {
|
|
147
|
+
console.log(dim(`The account is on v${behind.sequence}; this folder is based on v${known.sequence}.`));
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
link: known,
|
|
151
|
+
baseline: new Map(Object.entries(known.manifest)),
|
|
152
|
+
behind,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
111
155
|
const baseline = new Map();
|
|
112
156
|
for (const file of await downloader.files(project.id, latest.id)) {
|
|
113
157
|
baseline.set(file.path, file.sha256);
|
|
@@ -116,13 +160,14 @@ async function reconcile(folder, options = {}) {
|
|
|
116
160
|
repositoryId: project.id,
|
|
117
161
|
slug: project.slug,
|
|
118
162
|
sequence: latest.sequence,
|
|
163
|
+
versionId: latest.id,
|
|
119
164
|
manifest: Object.fromEntries(baseline),
|
|
120
165
|
};
|
|
121
166
|
await (0, config_js_1.writeLink)(folder, fresh);
|
|
122
167
|
if (!options.quiet && !link) {
|
|
123
168
|
console.log(dim(`Linked this folder to ${project.slug}.`));
|
|
124
169
|
}
|
|
125
|
-
return { link: fresh, baseline };
|
|
170
|
+
return { link: fresh, baseline, behind: null };
|
|
126
171
|
}
|
|
127
172
|
// ── commands ────────────────────────────────────────────────────────────────
|
|
128
173
|
async function commandSignIn(parsed) {
|
|
@@ -140,7 +185,7 @@ async function commandSignIn(parsed) {
|
|
|
140
185
|
}
|
|
141
186
|
await (0, config_js_1.storeToken)(token);
|
|
142
187
|
try {
|
|
143
|
-
const account = await (0,
|
|
188
|
+
const account = await (0, api_js_2.whoami)();
|
|
144
189
|
console.log(`Signed in as ${bold(account.displayName)} ${dim(`<${account.email}>`)} · ${account.plan}`);
|
|
145
190
|
console.log(dim(`Token stored in ${(0, config_js_1.configDirectory)()}`));
|
|
146
191
|
return 0;
|
|
@@ -157,12 +202,12 @@ async function commandSignOut() {
|
|
|
157
202
|
return 0;
|
|
158
203
|
}
|
|
159
204
|
async function commandWhoami() {
|
|
160
|
-
const account = await (0,
|
|
205
|
+
const account = await (0, api_js_2.whoami)();
|
|
161
206
|
console.log(`${bold(account.displayName)} <${account.email}> · ${account.plan}`);
|
|
162
207
|
return 0;
|
|
163
208
|
}
|
|
164
209
|
async function commandProjects() {
|
|
165
|
-
const all = await (0,
|
|
210
|
+
const all = await (0, api_js_2.projects)();
|
|
166
211
|
if (!all.length) {
|
|
167
212
|
console.log("No projects on this account yet.");
|
|
168
213
|
return 0;
|
|
@@ -206,6 +251,31 @@ async function commandSubmit(parsed) {
|
|
|
206
251
|
console.log("Nothing to submit; this folder matches the saved version.");
|
|
207
252
|
return 0;
|
|
208
253
|
}
|
|
254
|
+
/*
|
|
255
|
+
The policy is that credentials are never quietly dropped and never
|
|
256
|
+
quietly sent — they are asked about. The desktop application has always
|
|
257
|
+
put this question in front of the first upload; the command line was
|
|
258
|
+
sending them without a word, which is the worse half of the two
|
|
259
|
+
behaviours the policy exists to prevent.
|
|
260
|
+
*/
|
|
261
|
+
const secrets = await (0, worktree_js_1.detectSecrets)(folder);
|
|
262
|
+
const sending = new Set(files.map((file) => file.path));
|
|
263
|
+
const exposed = secrets.filter((secret) => sending.has(secret));
|
|
264
|
+
if (exposed.length) {
|
|
265
|
+
console.log(red(`\n${exposed.length} file${exposed.length === 1 ? " looks like a credential" : "s look like credentials"}:`));
|
|
266
|
+
for (const secret of exposed.slice(0, 20))
|
|
267
|
+
console.log(` ${secret}`);
|
|
268
|
+
if (exposed.length > 20) {
|
|
269
|
+
console.log(dim(` …and ${exposed.length - 20} more`));
|
|
270
|
+
}
|
|
271
|
+
if (!hasFlag(parsed, "allow-secrets")) {
|
|
272
|
+
console.error(`\nNothing was sent. Add them to ${accent(".gitignore")} to leave them` +
|
|
273
|
+
` behind, or pass ${accent("--allow-secrets")} if they genuinely belong` +
|
|
274
|
+
` in the project.`);
|
|
275
|
+
return 1;
|
|
276
|
+
}
|
|
277
|
+
console.log(dim("Sending them anyway, because --allow-secrets was given."));
|
|
278
|
+
}
|
|
209
279
|
if (hasFlag(parsed, "dry-run", "n")) {
|
|
210
280
|
console.log(`${files.length} file${files.length === 1 ? "" : "s"} would be sent:`);
|
|
211
281
|
for (const file of files)
|
|
@@ -214,21 +284,66 @@ async function commandSubmit(parsed) {
|
|
|
214
284
|
}
|
|
215
285
|
const line = progressLine();
|
|
216
286
|
const uploader = new upload_js_1.Uploader(config_js_1.credentials);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
287
|
+
let result;
|
|
288
|
+
try {
|
|
289
|
+
result = await uploader.run({
|
|
290
|
+
localPath: folder,
|
|
291
|
+
include: files.map((file) => file.path),
|
|
292
|
+
message,
|
|
293
|
+
projectName: link?.slug ?? node_path_1.default.basename(folder),
|
|
294
|
+
repositoryId: link?.repositoryId ?? null,
|
|
295
|
+
/*
|
|
296
|
+
Only claimed when this folder has actually been reconciled with a
|
|
297
|
+
known version. A folder linked before versions were recorded says
|
|
298
|
+
nothing rather than guessing, and publishes as it always did.
|
|
299
|
+
*/
|
|
300
|
+
...(link?.versionId ? { expectedHeadVersionId: link.versionId } : {}),
|
|
301
|
+
}, (progress) => {
|
|
302
|
+
line(` ${String(progress.percent).padStart(3)}% ${progress.stage.padEnd(7)} ` +
|
|
303
|
+
`${progress.files}/${progress.totalFiles} ${progress.path.slice(-48)}`);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
done(line);
|
|
308
|
+
const code = error.code;
|
|
309
|
+
if (code === "merge_required" || code === "track_moved") {
|
|
310
|
+
/*
|
|
311
|
+
Somebody else saved to this project first. Everything that did not
|
|
312
|
+
overlap has already been combined by the service; what is left is a
|
|
313
|
+
genuine decision, so the answer is to bring their work down and look
|
|
314
|
+
at it rather than to try again harder.
|
|
315
|
+
*/
|
|
316
|
+
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
317
|
+
console.error(`\nRun ${accent("coderook get")} to bring the latest version down, then` +
|
|
318
|
+
` submit again. Nothing was changed on your account.`);
|
|
319
|
+
return 1;
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
227
323
|
done(line);
|
|
324
|
+
if (result.mergeTrack) {
|
|
325
|
+
/*
|
|
326
|
+
The upload is complete and kept, but the project has not changed —
|
|
327
|
+
calling this "saved" would be false, and the base is deliberately left
|
|
328
|
+
alone so the folder still knows what it was working from.
|
|
329
|
+
*/
|
|
330
|
+
console.log(`\nSomebody else saved first, and ${result.mergeTrack.conflicts.length === 1
|
|
331
|
+
? "one file overlaps"
|
|
332
|
+
: `${result.mergeTrack.conflicts.length} files overlap`}.`);
|
|
333
|
+
console.log(`Your work is complete and kept as ${accent(result.mergeTrack.reference)}:`);
|
|
334
|
+
for (const conflict of result.mergeTrack.conflicts.slice(0, 20)) {
|
|
335
|
+
console.log(` ${red(conflict.path)} ${dim(conflict.kind)}`);
|
|
336
|
+
}
|
|
337
|
+
console.log(`\nRun ${accent(`coderook merge ${result.mergeTrack.reference}`)} to decide,` +
|
|
338
|
+
` or ${accent("coderook merges")} to see everything waiting.`);
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
228
341
|
await (0, config_js_1.writeLink)(folder, {
|
|
229
342
|
repositoryId: result.repositoryId,
|
|
230
343
|
slug: link?.slug ?? node_path_1.default.basename(folder),
|
|
231
344
|
sequence: result.sequence,
|
|
345
|
+
// What this folder is now working from, so the next submit can say so.
|
|
346
|
+
versionId: result.versionId,
|
|
232
347
|
manifest: result.manifest,
|
|
233
348
|
});
|
|
234
349
|
console.log(`Saved ${accent(`v${result.sequence}`)} · sent ${bytes(result.sentBytes)} in ` +
|
|
@@ -252,7 +367,7 @@ async function commandClone(parsed) {
|
|
|
252
367
|
console.error(red("Which project? Try: coderook clone <project>"));
|
|
253
368
|
return 1;
|
|
254
369
|
}
|
|
255
|
-
const project = await (0,
|
|
370
|
+
const project = await (0, api_js_2.findProject)(reference);
|
|
256
371
|
if (!project) {
|
|
257
372
|
console.error(red(`No project named ${reference} on this account.`));
|
|
258
373
|
return 1;
|
|
@@ -277,10 +392,12 @@ async function fetchInto(repositoryId, destination, slug) {
|
|
|
277
392
|
` ${progress.path.slice(-48)}`);
|
|
278
393
|
});
|
|
279
394
|
done(line);
|
|
395
|
+
// Fetching is how a folder catches up, so this is what moves its base.
|
|
280
396
|
await (0, config_js_1.writeLink)(destination, {
|
|
281
397
|
repositoryId,
|
|
282
398
|
slug,
|
|
283
399
|
sequence: latest.sequence,
|
|
400
|
+
versionId: latest.id,
|
|
284
401
|
manifest: result.manifest,
|
|
285
402
|
});
|
|
286
403
|
console.log(`${accent(`v${latest.sequence}`)} · ${result.files} files · ${bytes(result.bytes)} into ${destination}`);
|
|
@@ -355,7 +472,7 @@ async function commandInspect(parsed) {
|
|
|
355
472
|
return 0;
|
|
356
473
|
}
|
|
357
474
|
async function commandDoctor() {
|
|
358
|
-
const service = await (0,
|
|
475
|
+
const service = await (0, api_js_2.health)().catch(() => null);
|
|
359
476
|
console.log(`CodeRook CLI ${VERSION} · Node ${node_process_1.default.versions.node} · ${node_process_1.default.platform}`);
|
|
360
477
|
console.log(`Config: ${(0, config_js_1.configDirectory)()}`);
|
|
361
478
|
console.log(service
|
|
@@ -368,7 +485,7 @@ async function commandDoctor() {
|
|
|
368
485
|
return 0;
|
|
369
486
|
}
|
|
370
487
|
try {
|
|
371
|
-
const account = await (0,
|
|
488
|
+
const account = await (0, api_js_2.whoami)();
|
|
372
489
|
console.log(`Account: ${account.email} · ${account.plan}`);
|
|
373
490
|
}
|
|
374
491
|
catch (error) {
|
|
@@ -390,6 +507,10 @@ ${bold("Working with a folder")}
|
|
|
390
507
|
coderook clone <project> [dir] Fetch a project into a new folder
|
|
391
508
|
coderook rules [folder] Show the ignore rules (--init to start one)
|
|
392
509
|
|
|
510
|
+
${bold("When somebody saved first")}
|
|
511
|
+
coderook merges Uploads of yours waiting on a decision
|
|
512
|
+
coderook merge <ref> Look at one (--mine --theirs --both --drop)
|
|
513
|
+
|
|
393
514
|
${bold("Bundles")}
|
|
394
515
|
coderook bundle [folder] [out] Pack the project as a .cbx
|
|
395
516
|
coderook unbundle <file> [dir] Extract a .cbx
|
|
@@ -402,10 +523,132 @@ ${bold("Other")}
|
|
|
402
523
|
${bold("Options")}
|
|
403
524
|
-m, --message The version message for submit
|
|
404
525
|
-n, --dry-run Show what submit would send, without sending
|
|
526
|
+
--allow-secrets Send files that look like credentials anyway
|
|
405
527
|
--token Supply the token to sign-in instead of being asked
|
|
406
528
|
|
|
407
529
|
${dim("The environment variable CODEROOK_TOKEN is used when set, so automated")}
|
|
408
530
|
${dim("runs need nothing on disk. CODEROOK_API_URL points at another service.")}`;
|
|
531
|
+
/**
|
|
532
|
+
* Everything waiting on a decision, for one folder's project.
|
|
533
|
+
*
|
|
534
|
+
* A diverted upload is easy to forget about — it is not an error and the
|
|
535
|
+
* project looks untouched — so this is the way to find out that work of
|
|
536
|
+
* yours is sitting somewhere, still complete, waiting.
|
|
537
|
+
*/
|
|
538
|
+
async function commandMerges(parsed) {
|
|
539
|
+
const folder = folderFor(parsed);
|
|
540
|
+
const link = await (0, config_js_1.readLink)(folder);
|
|
541
|
+
if (!link) {
|
|
542
|
+
console.error(red("This folder is not linked to a project on your account."));
|
|
543
|
+
return 1;
|
|
544
|
+
}
|
|
545
|
+
const waiting = (await (0, api_js_1.mergeTracks)(link.repositoryId)).filter((merge) => merge.state === "open");
|
|
546
|
+
if (!waiting.length) {
|
|
547
|
+
console.log("Nothing is waiting to be merged.");
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
for (const merge of waiting) {
|
|
551
|
+
const counts = merge.conflicts;
|
|
552
|
+
console.log(`${accent(merge.reference)} ${counts ? `${counts.unresolved} of ${counts.total} still to decide` : ""} ${dim(new Date(merge.createdAt).toLocaleString())}`);
|
|
553
|
+
}
|
|
554
|
+
console.log(dim(`
|
|
555
|
+
Run coderook merge <reference> to look at one.`));
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
/** Find a merge by the reference a person would type, such as M-2. */
|
|
559
|
+
async function findMerge(folder, reference) {
|
|
560
|
+
const link = await (0, config_js_1.readLink)(folder);
|
|
561
|
+
if (!link)
|
|
562
|
+
throw new Error("This folder is not linked to a project on your account.");
|
|
563
|
+
const all = await (0, api_js_1.mergeTracks)(link.repositoryId);
|
|
564
|
+
const found = all.find((merge) => merge.reference.toLowerCase() === reference.toLowerCase());
|
|
565
|
+
if (!found)
|
|
566
|
+
throw new Error(`No merge called ${reference} on this project.`);
|
|
567
|
+
return found;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Look at one merge, and optionally finish it.
|
|
571
|
+
*
|
|
572
|
+
* Deciding happens here rather than in a command of its own because the
|
|
573
|
+
* decision only means anything next to the thing it is about.
|
|
574
|
+
*/
|
|
575
|
+
async function commandMerge(parsed) {
|
|
576
|
+
const reference = parsed.positional[0];
|
|
577
|
+
if (!reference) {
|
|
578
|
+
console.error(red("Which merge? Try: coderook merge M-1"));
|
|
579
|
+
return 1;
|
|
580
|
+
}
|
|
581
|
+
const folder = folderFor({ ...parsed, positional: parsed.positional.slice(1) });
|
|
582
|
+
const summary = await findMerge(folder, reference);
|
|
583
|
+
const detail = await (0, api_js_1.mergeTrack)(summary.id);
|
|
584
|
+
if (hasFlag(parsed, "cancel")) {
|
|
585
|
+
await (0, api_js_1.cancelMerge)(summary.id);
|
|
586
|
+
console.log(`${summary.reference} cancelled. Your upload is still stored and nothing published was touched.`);
|
|
587
|
+
return 0;
|
|
588
|
+
}
|
|
589
|
+
const outstanding = detail.conflicts.filter((conflict) => !conflict.resolvedAt);
|
|
590
|
+
const decision = hasFlag(parsed, "mine")
|
|
591
|
+
? "take_candidate"
|
|
592
|
+
: hasFlag(parsed, "theirs")
|
|
593
|
+
? "take_target"
|
|
594
|
+
: hasFlag(parsed, "drop")
|
|
595
|
+
? "delete"
|
|
596
|
+
: hasFlag(parsed, "both", "keep-both")
|
|
597
|
+
? "keep_both"
|
|
598
|
+
: null;
|
|
599
|
+
if (decision) {
|
|
600
|
+
const only = flagText(parsed, "path");
|
|
601
|
+
const chosen = only
|
|
602
|
+
? outstanding.filter((conflict) => conflict.path === only)
|
|
603
|
+
: outstanding;
|
|
604
|
+
if (!chosen.length) {
|
|
605
|
+
console.error(red(only ? `${only} has no outstanding decision.` : "Nothing left to decide."));
|
|
606
|
+
return 1;
|
|
607
|
+
}
|
|
608
|
+
for (const conflict of chosen) {
|
|
609
|
+
await (0, api_js_1.resolveMergeConflict)(summary.id, conflict.id, decision);
|
|
610
|
+
console.log(` ${conflict.path} → ${decision === "take_candidate"
|
|
611
|
+
? "yours"
|
|
612
|
+
: decision === "take_target"
|
|
613
|
+
? "theirs"
|
|
614
|
+
: decision === "keep_both"
|
|
615
|
+
? `both, yours saved beside it`
|
|
616
|
+
: "removed"}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const now = await (0, api_js_1.mergeTrack)(summary.id);
|
|
620
|
+
console.log(`
|
|
621
|
+
${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
|
|
622
|
+
(now.provisional.ready
|
|
623
|
+
? "ready to apply"
|
|
624
|
+
: `${now.provisional.unresolvedPaths.length} still to decide`));
|
|
625
|
+
for (const conflict of now.conflicts) {
|
|
626
|
+
console.log(` ${conflict.resolvedAt ? accent("decided") : red("waiting")} ${conflict.path}` +
|
|
627
|
+
` ${dim(conflict.kind)}${conflict.resolution ? dim(` (${conflict.resolution})`) : ""}`);
|
|
628
|
+
}
|
|
629
|
+
if (hasFlag(parsed, "apply")) {
|
|
630
|
+
if (!now.provisional.ready) {
|
|
631
|
+
console.error(red("\nStill undecided files; nothing was applied."));
|
|
632
|
+
return 1;
|
|
633
|
+
}
|
|
634
|
+
const applied = await (0, api_js_1.applyMerge)(summary.id);
|
|
635
|
+
console.log(`
|
|
636
|
+
Applied as ${accent(`v${applied.version.sequence}`)}.`);
|
|
637
|
+
console.log(dim("Run coderook get to bring it down to this folder."));
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
if (!decision) {
|
|
641
|
+
console.log(dim(`
|
|
642
|
+
--mine keeps yours, --theirs keeps what was already saved,` +
|
|
643
|
+
` --drop removes the file.
|
|
644
|
+
Add --path <file> for one file, then --apply when ready.`));
|
|
645
|
+
}
|
|
646
|
+
else if (now.provisional.ready) {
|
|
647
|
+
console.log(dim(`
|
|
648
|
+
Run coderook merge ${now.mergeTrack.reference} --apply to publish it.`));
|
|
649
|
+
}
|
|
650
|
+
return 0;
|
|
651
|
+
}
|
|
409
652
|
const COMMANDS = {
|
|
410
653
|
"sign-in": commandSignIn,
|
|
411
654
|
login: commandSignIn,
|
|
@@ -422,6 +665,8 @@ const COMMANDS = {
|
|
|
422
665
|
bundle: commandBundle,
|
|
423
666
|
unbundle: commandUnbundle,
|
|
424
667
|
inspect: commandInspect,
|
|
668
|
+
merges: commandMerges,
|
|
669
|
+
merge: commandMerge,
|
|
425
670
|
doctor: () => commandDoctor(),
|
|
426
671
|
};
|
|
427
672
|
async function main(argv) {
|
|
@@ -95,7 +95,16 @@ class Uploader {
|
|
|
95
95
|
const body = text ? JSON.parse(text) : {};
|
|
96
96
|
if (!response.ok) {
|
|
97
97
|
const message = body?.error?.message ?? `${route} failed (${response.status})`;
|
|
98
|
-
|
|
98
|
+
/*
|
|
99
|
+
The service says what kind of failure this is, and callers need that
|
|
100
|
+
to say anything useful — "someone else saved first" deserves a
|
|
101
|
+
different suggestion from "the disk is full". Carrying the code on
|
|
102
|
+
the error keeps them from having to match on wording.
|
|
103
|
+
*/
|
|
104
|
+
throw Object.assign(new Error(message), {
|
|
105
|
+
code: typeof body?.error?.code === "string" ? body.error.code : "",
|
|
106
|
+
status: response.status,
|
|
107
|
+
});
|
|
99
108
|
}
|
|
100
109
|
return body;
|
|
101
110
|
}
|
|
@@ -288,6 +297,9 @@ class Uploader {
|
|
|
288
297
|
contentType: "application/json",
|
|
289
298
|
body: JSON.stringify({
|
|
290
299
|
message: request.message.trim() || "Saved from the CodeRook desktop app",
|
|
300
|
+
...(request.expectedHeadVersionId === undefined
|
|
301
|
+
? {}
|
|
302
|
+
: { expectedHeadVersionId: request.expectedHeadVersionId }),
|
|
291
303
|
sourceSize: sourceBytes,
|
|
292
304
|
storedSize: storedBytes,
|
|
293
305
|
files: contents.map((item) => ({
|
|
@@ -314,6 +326,7 @@ class Uploader {
|
|
|
314
326
|
repositoryId,
|
|
315
327
|
versionId: completed.version.id,
|
|
316
328
|
sequence: completed.version.sequence,
|
|
329
|
+
...(completed.mergeTrack ? { mergeTrack: completed.mergeTrack } : {}),
|
|
317
330
|
sourceBytes,
|
|
318
331
|
storedBytes,
|
|
319
332
|
sentBytes,
|
package/package.json
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@coderook/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CodeRook from the command line, on any operating system",
|
|
5
|
-
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
-
"homepage": "https://coderook.com",
|
|
7
|
-
"bugs": {
|
|
8
|
-
"url": "https://coderook.com/contact"
|
|
9
|
-
},
|
|
10
|
-
"keywords": [
|
|
11
|
-
"coderook",
|
|
12
|
-
"versioning",
|
|
13
|
-
"backup",
|
|
14
|
-
"cbx"
|
|
15
|
-
],
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=20.11.0"
|
|
18
|
-
},
|
|
19
|
-
"bin": {
|
|
20
|
-
"coderook": "dist/cli/src/cli.js"
|
|
21
|
-
},
|
|
22
|
-
"files": [
|
|
23
|
-
"dist"
|
|
24
|
-
],
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "tsc -p tsconfig.json",
|
|
27
|
-
"check": "tsc -p tsconfig.json --noEmit",
|
|
28
|
-
"test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
|
|
29
|
-
"start": "node dist/cli/src/cli.js",
|
|
30
|
-
"prepublishOnly": "npm run build"
|
|
31
|
-
},
|
|
32
|
-
"devDependencies": {
|
|
33
|
-
"@types/node": "24.10.1",
|
|
34
|
-
"typescript": "5.9.3"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@coderook/cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "CodeRook from the command line, on any operating system",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
+
"homepage": "https://coderook.com",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://coderook.com/contact"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"coderook",
|
|
12
|
+
"versioning",
|
|
13
|
+
"backup",
|
|
14
|
+
"cbx"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20.11.0"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"coderook": "dist/cli/src/cli.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
28
|
+
"test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
|
|
29
|
+
"start": "node dist/cli/src/cli.js",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "24.10.1",
|
|
34
|
+
"typescript": "5.9.3"
|
|
35
|
+
}
|
|
36
|
+
}
|