@coderook/cli 0.22.2 → 0.23.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/cli.js +182 -18
- package/dist/cli/src/import_command.js +150 -0
- package/dist/desktop-app/src/main/upload.js +11 -1
- package/package.json +51 -51
|
@@ -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.23.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
*/
|
|
18
18
|
const promises_1 = require("node:readline/promises");
|
|
19
19
|
const node_os_1 = __importDefault(require("node:os"));
|
|
20
|
+
const promises_2 = require("node:fs/promises");
|
|
20
21
|
const node_path_1 = __importDefault(require("node:path"));
|
|
21
22
|
const node_process_1 = __importDefault(require("node:process"));
|
|
22
23
|
const api_js_1 = require("./api.js");
|
|
@@ -37,6 +38,7 @@ const identify_js_1 = require("../../desktop-app/src/main/identify.js");
|
|
|
37
38
|
const detect_js_1 = require("../../desktop-app/src/main/detect.js");
|
|
38
39
|
const cbx_js_1 = require("../../desktop-app/src/main/cbx.js");
|
|
39
40
|
const api_js_2 = require("./api.js");
|
|
41
|
+
const import_command_js_1 = require("./import_command.js");
|
|
40
42
|
const runner_js_1 = require("./runner.js");
|
|
41
43
|
const config_js_1 = require("./config.js");
|
|
42
44
|
/*
|
|
@@ -313,6 +315,103 @@ async function commandStatus(parsed) {
|
|
|
313
315
|
console.log(dim(` …and ${files.length - 50} more`));
|
|
314
316
|
return 0;
|
|
315
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Bring an existing repository in from another host.
|
|
320
|
+
*
|
|
321
|
+
* Snapshot only, and it says so. The files arrive, the history does not —
|
|
322
|
+
* see `import_command.ts` for why that is a limit rather than an omission.
|
|
323
|
+
* Everything after the fetch is the ordinary save path, so an import
|
|
324
|
+
* produces exactly the Version that saving the same folder would.
|
|
325
|
+
*/
|
|
326
|
+
async function commandImport(parsed) {
|
|
327
|
+
const url = parsed.positional[0];
|
|
328
|
+
if (!url) {
|
|
329
|
+
console.error(red("Nothing to import from."));
|
|
330
|
+
console.error("Give the address of a repository, for example:");
|
|
331
|
+
console.error(` ${accent("coderook import https://github.com/owner/project")}`);
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
334
|
+
if (!(0, import_command_js_1.looksLikeRepositoryUrl)(url)) {
|
|
335
|
+
console.error(red(`${url} does not look like a repository address.`));
|
|
336
|
+
console.error("Expected something like https://github.com/owner/project or\n" +
|
|
337
|
+
"git@github.com:owner/project.git");
|
|
338
|
+
return 1;
|
|
339
|
+
}
|
|
340
|
+
if (!(await (0, import_command_js_1.gitAvailable)())) {
|
|
341
|
+
console.error(red("Import needs git on this machine, and it was not found."));
|
|
342
|
+
console.error("Git is used to fetch the files once; nothing about your project\n" +
|
|
343
|
+
"afterwards depends on it.");
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
const destination = parsed.positional[1] ?? null;
|
|
347
|
+
let plan;
|
|
348
|
+
try {
|
|
349
|
+
plan = await (0, import_command_js_1.fetchSnapshot)(url, destination, (line) => console.log(dim(line)));
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
console.error(red("Could not fetch that repository."));
|
|
353
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
354
|
+
/*
|
|
355
|
+
git puts the useful sentence last and a stack of its own noise first.
|
|
356
|
+
A private repository with no credentials is by far the most common
|
|
357
|
+
failure, so it is named rather than left to be inferred.
|
|
358
|
+
*/
|
|
359
|
+
console.error(detail.split("\n").filter(Boolean).slice(-3).join("\n"));
|
|
360
|
+
if (/authentication|denied|not found|could not read/i.test(detail)) {
|
|
361
|
+
console.error("\nIf it is private, sign in to it with git first — CodeRook uses\n" +
|
|
362
|
+
"the credentials git already has and never asks for a token.");
|
|
363
|
+
}
|
|
364
|
+
return 1;
|
|
365
|
+
}
|
|
366
|
+
const { files, bytes } = await (0, import_command_js_1.measure)(plan.folder);
|
|
367
|
+
if (!files) {
|
|
368
|
+
console.error(red("That repository has no files in it."));
|
|
369
|
+
if (plan.temporary)
|
|
370
|
+
await (0, promises_2.rm)(node_path_1.default.dirname(plan.folder), { recursive: true, force: true });
|
|
371
|
+
return 1;
|
|
372
|
+
}
|
|
373
|
+
console.log(`Fetched ${files} file${files === 1 ? "" : "s"}, ${(0, import_command_js_1.humanBytes)(bytes)}, ` +
|
|
374
|
+
`into ${plan.folder}`);
|
|
375
|
+
/*
|
|
376
|
+
Said before the upload rather than after it fails. The window is a share
|
|
377
|
+
of the plan allowance per week, so a large import is a thing somebody
|
|
378
|
+
should know about while they can still choose a smaller repository.
|
|
379
|
+
*/
|
|
380
|
+
if (bytes > 1024 ** 3) {
|
|
381
|
+
console.log(dim("This is a large import. Uploads are limited to a share of your\n" +
|
|
382
|
+
"allowance each week, so a project this size may need more than one\n" +
|
|
383
|
+
"sitting; the save resumes rather than starting over."));
|
|
384
|
+
}
|
|
385
|
+
console.log(dim("History is not imported — this becomes the first version."));
|
|
386
|
+
/*
|
|
387
|
+
Hand the fetched folder to the ordinary save path. --allow-ignored is set
|
|
388
|
+
because git tracks files committed before the rule that excludes them,
|
|
389
|
+
and dropping those would make the import quietly lossy.
|
|
390
|
+
*/
|
|
391
|
+
const submitFlags = new Map(parsed.flags);
|
|
392
|
+
submitFlags.set("allow-ignored", true);
|
|
393
|
+
/*
|
|
394
|
+
Name the project after the repository, not after wherever the files were
|
|
395
|
+
put. Without this an import into a temporary folder produces a project
|
|
396
|
+
called something like `tmp-4f21`, and the name is the thing somebody
|
|
397
|
+
types afterwards to fetch it.
|
|
398
|
+
*/
|
|
399
|
+
if (!submitFlags.has("name"))
|
|
400
|
+
submitFlags.set("name", plan.name);
|
|
401
|
+
if (!submitFlags.has("m") && !submitFlags.has("message")) {
|
|
402
|
+
submitFlags.set("message", `Imported from ${url}`);
|
|
403
|
+
}
|
|
404
|
+
const code = await commandSubmit({
|
|
405
|
+
positional: [plan.folder],
|
|
406
|
+
flags: submitFlags,
|
|
407
|
+
});
|
|
408
|
+
if (code === 0 && plan.temporary) {
|
|
409
|
+
console.log(dim(`The working copy stays at ${plan.folder} until you remove it.
|
|
410
|
+
` +
|
|
411
|
+
`Run ${accent("coderook get " + plan.name)} anywhere to fetch it fresh.`));
|
|
412
|
+
}
|
|
413
|
+
return code;
|
|
414
|
+
}
|
|
316
415
|
async function commandSubmit(parsed) {
|
|
317
416
|
const folder = folderFor(parsed);
|
|
318
417
|
const message = flagText(parsed, "m", "message") ?? "";
|
|
@@ -340,7 +439,22 @@ async function commandSubmit(parsed) {
|
|
|
340
439
|
await (0, api_js_2.whoami)()
|
|
341
440
|
.then((account) => account.displayName || account.username || "")
|
|
342
441
|
.catch(() => ""), hasFlag(parsed, "no-licence"));
|
|
343
|
-
|
|
442
|
+
/*
|
|
443
|
+
Import sends what it fetched, filtering nothing.
|
|
444
|
+
|
|
445
|
+
The ignore rules exist to keep build output and local mess out of a
|
|
446
|
+
working folder, and applying them to a fresh clone gets the wrong answer
|
|
447
|
+
twice over: a clone holds exactly the files the source repository tracked
|
|
448
|
+
and nothing else, so there is no mess to exclude — while a repository
|
|
449
|
+
that tracks something its own `.gitignore` now names (which git does,
|
|
450
|
+
for anything committed before the rule) would have those files dropped.
|
|
451
|
+
Dropping them makes an import quietly lossy, which is worse than
|
|
452
|
+
refusing to import at all.
|
|
453
|
+
*/
|
|
454
|
+
const importing = hasFlag(parsed, "allow-ignored");
|
|
455
|
+
const rules = importing
|
|
456
|
+
? { shared: "", local: "" }
|
|
457
|
+
: await (0, worktree_js_1.readRules)(folder);
|
|
344
458
|
const files = await (0, worktree_js_1.changedFiles)(folder, rules, baseline);
|
|
345
459
|
/*
|
|
346
460
|
An upgraded folder with no materialisation record and something that
|
|
@@ -404,17 +518,17 @@ async function commandSubmit(parsed) {
|
|
|
404
518
|
*/
|
|
405
519
|
const shielded = (await (0, worktree_js_1.detectPrivateDirectories)(folder)).filter((finding) => [...sending].some((file) => file === finding.path || file.startsWith(`${finding.path}/`)));
|
|
406
520
|
if (shielded.length && !hasFlag(parsed, "allow-private")) {
|
|
407
|
-
console.log(red(`
|
|
521
|
+
console.log(red(`
|
|
408
522
|
${shielded.length} folder${shielded.length === 1 ? "" : "s"} here belong${shielded.length === 1 ? "s" : ""} to a program, not to your project:`));
|
|
409
523
|
for (const finding of shielded) {
|
|
410
524
|
console.log(` ${finding.path} ${dim(`— ${finding.because}`)}`);
|
|
411
525
|
}
|
|
412
|
-
console.log(`
|
|
526
|
+
console.log(`
|
|
413
527
|
Nothing was sent. To leave them behind:`);
|
|
414
528
|
for (const finding of shielded) {
|
|
415
529
|
console.log(` ${accent(`echo "${finding.rule}" >> .gitignore`)}`);
|
|
416
530
|
}
|
|
417
|
-
console.error(`
|
|
531
|
+
console.error(`
|
|
418
532
|
Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
|
|
419
533
|
return 1;
|
|
420
534
|
}
|
|
@@ -449,7 +563,7 @@ Or pass ${accent("--allow-private")} if they genuinely belong in the project.`);
|
|
|
449
563
|
const pasted = await (0, worktree_js_1.detectPastedCredentials)(folder, files.map((file) => file.path));
|
|
450
564
|
const stillPasted = pasted.filter((finding) => !exposed.includes(finding.path));
|
|
451
565
|
if (stillPasted.length) {
|
|
452
|
-
console.log(red(`
|
|
566
|
+
console.log(red(`
|
|
453
567
|
${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s have credentials"} inside:`));
|
|
454
568
|
for (const finding of stillPasted.slice(0, 20)) {
|
|
455
569
|
console.log(` ${finding.path}`);
|
|
@@ -464,11 +578,11 @@ ${stillPasted.length} file${stillPasted.length === 1 ? " has a credential" : "s
|
|
|
464
578
|
console.log(dim(` …and ${stillPasted.length - 20} more`));
|
|
465
579
|
}
|
|
466
580
|
if (!hasFlag(parsed, "allow-secrets")) {
|
|
467
|
-
console.error(`
|
|
581
|
+
console.error(`
|
|
468
582
|
Nothing was sent. Move the key into an environment variable, and if` +
|
|
469
583
|
` it has ever been published, replace it at the service that issued` +
|
|
470
584
|
` it — a key that has leaked stays leaked.` +
|
|
471
|
-
`
|
|
585
|
+
`
|
|
472
586
|
Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
473
587
|
return 1;
|
|
474
588
|
}
|
|
@@ -490,13 +604,27 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
490
604
|
include: files.map((file) => file.path),
|
|
491
605
|
deletions: files.filter((file) => file.deleted).map((file) => file.path),
|
|
492
606
|
message,
|
|
493
|
-
|
|
607
|
+
/*
|
|
608
|
+
A folder's name is the right default and the wrong answer for import,
|
|
609
|
+
where the folder is somewhere temporary and the project should carry
|
|
610
|
+
the name it had at the place it came from. An existing link always
|
|
611
|
+
wins: renaming somebody's project because they passed a flag would be
|
|
612
|
+
a surprise, and the flag exists for projects that do not exist yet.
|
|
613
|
+
*/
|
|
614
|
+
projectName: link?.slug ?? flagText(parsed, "name") ?? node_path_1.default.basename(folder),
|
|
494
615
|
repositoryId: link?.repositoryId ?? null,
|
|
495
616
|
// Every current CLI publish states its ancestry. A brand-new project is
|
|
496
617
|
// explicitly based on an empty Track; a linked folder names the immutable
|
|
497
618
|
// Version it was last reconciled with.
|
|
498
619
|
baseVersionId: link?.baseVersionId ?? null,
|
|
499
620
|
track: flagText(parsed, "track") ?? (await (0, track_commands_js_1.trackFor)(folder)),
|
|
621
|
+
/*
|
|
622
|
+
Only import sets this. Git keeps tracking files committed before the
|
|
623
|
+
rule that excludes them, so a faithful import carries paths the
|
|
624
|
+
project's own ignore rules now refuse; without the override the server
|
|
625
|
+
would reject the publication and the import would be lossy.
|
|
626
|
+
*/
|
|
627
|
+
...(hasFlag(parsed, "allow-ignored") ? { allowIgnored: true } : {}),
|
|
500
628
|
...(link?.baseVersionId
|
|
501
629
|
? { expectedHeadVersionId: link.baseVersionId }
|
|
502
630
|
: {}),
|
|
@@ -542,7 +670,7 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
542
670
|
difference between a person retrying and a person wondering.
|
|
543
671
|
*/
|
|
544
672
|
if (!code && /fetch failed|ECONNRESET|socket hang up|network|ETIMEDOUT/i.test(text)) {
|
|
545
|
-
console.error(red(`
|
|
673
|
+
console.error(red(`
|
|
546
674
|
The connection failed: ${text}`));
|
|
547
675
|
console.error(`Your work may already have been saved. Run the same command again —` +
|
|
548
676
|
` it will not create a second version.`);
|
|
@@ -724,7 +852,7 @@ async function commandGet(parsed) {
|
|
|
724
852
|
`changed while the interrupted fetch was stopped:`));
|
|
725
853
|
for (const file of changedInTheGap.slice(0, 20))
|
|
726
854
|
console.error(` ${file.path}`);
|
|
727
|
-
console.error(`
|
|
855
|
+
console.error(`
|
|
728
856
|
Save them with ${accent("coderook submit")}, or finish the fetch and ` +
|
|
729
857
|
`discard them with ${accent("coderook get --replace")}.`);
|
|
730
858
|
return 1;
|
|
@@ -877,8 +1005,8 @@ async function suggestRules(folder, apply) {
|
|
|
877
1005
|
const addition = (0, detect_js_1.rulesFromSuggestions)(recommended);
|
|
878
1006
|
const shared = rules.shared.trimEnd();
|
|
879
1007
|
await (0, worktree_js_1.writeRules)(folder, {
|
|
880
|
-
shared: shared ? `${shared}
|
|
881
|
-
|
|
1008
|
+
shared: shared ? `${shared}
|
|
1009
|
+
|
|
882
1010
|
${addition}` : addition,
|
|
883
1011
|
local: rules.local,
|
|
884
1012
|
});
|
|
@@ -1005,7 +1133,7 @@ async function commandMerges(parsed) {
|
|
|
1005
1133
|
const counts = merge.conflicts;
|
|
1006
1134
|
console.log(`${accent(merge.reference)} ${counts ? `${counts.unresolved} of ${counts.total} still to decide` : ""} ${dim(new Date(merge.createdAt).toLocaleString())}`);
|
|
1007
1135
|
}
|
|
1008
|
-
console.log(dim(`
|
|
1136
|
+
console.log(dim(`
|
|
1009
1137
|
Run coderook merge <reference> to look at one.`));
|
|
1010
1138
|
return 0;
|
|
1011
1139
|
}
|
|
@@ -1071,7 +1199,7 @@ async function commandMerge(parsed) {
|
|
|
1071
1199
|
}
|
|
1072
1200
|
}
|
|
1073
1201
|
const now = await (0, api_js_1.mergeTrack)(summary.id);
|
|
1074
|
-
console.log(`
|
|
1202
|
+
console.log(`
|
|
1075
1203
|
${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
|
|
1076
1204
|
(now.provisional.ready
|
|
1077
1205
|
? "ready to apply"
|
|
@@ -1086,19 +1214,19 @@ ${accent(now.mergeTrack.reference)} · ${now.provisional.fileCount} files · ` +
|
|
|
1086
1214
|
return 1;
|
|
1087
1215
|
}
|
|
1088
1216
|
const applied = await (0, api_js_1.applyMerge)(summary.id);
|
|
1089
|
-
console.log(`
|
|
1217
|
+
console.log(`
|
|
1090
1218
|
Applied as ${accent(`v${applied.version.sequence}`)}.`);
|
|
1091
1219
|
console.log(dim("Run coderook get to bring it down to this folder."));
|
|
1092
1220
|
return 0;
|
|
1093
1221
|
}
|
|
1094
1222
|
if (!decision) {
|
|
1095
|
-
console.log(dim(`
|
|
1223
|
+
console.log(dim(`
|
|
1096
1224
|
--mine keeps yours, --theirs keeps what was already saved,` +
|
|
1097
|
-
` --drop removes the file.
|
|
1225
|
+
` --drop removes the file.
|
|
1098
1226
|
Add --path <file> for one file, then --apply when ready.`));
|
|
1099
1227
|
}
|
|
1100
1228
|
else if (now.provisional.ready) {
|
|
1101
|
-
console.log(dim(`
|
|
1229
|
+
console.log(dim(`
|
|
1102
1230
|
Run coderook merge ${now.mergeTrack.reference} --apply to publish it.`));
|
|
1103
1231
|
}
|
|
1104
1232
|
return 0;
|
|
@@ -1275,6 +1403,10 @@ const SPECS = [
|
|
|
1275
1403
|
options: [
|
|
1276
1404
|
{ flags: "-m, --message <text>", description: "what changed, in a sentence" },
|
|
1277
1405
|
{ flags: "-n, --dry-run", description: "show what would be sent, send nothing" },
|
|
1406
|
+
{
|
|
1407
|
+
flags: "--name <name>",
|
|
1408
|
+
description: "name a new project this, instead of after the folder",
|
|
1409
|
+
},
|
|
1278
1410
|
{
|
|
1279
1411
|
flags: "--allow-secrets",
|
|
1280
1412
|
description: "send files that look like credentials, and files with keys inside",
|
|
@@ -1294,6 +1426,38 @@ const SPECS = [
|
|
|
1294
1426
|
],
|
|
1295
1427
|
run: commandSubmit,
|
|
1296
1428
|
},
|
|
1429
|
+
{
|
|
1430
|
+
name: "import",
|
|
1431
|
+
group: "Getting started",
|
|
1432
|
+
summary: "bring a project in from another host",
|
|
1433
|
+
usage: "import <address> [folder]",
|
|
1434
|
+
detail: "Fetches a repository from GitHub, GitLab or anywhere else git can\n" +
|
|
1435
|
+
"reach, and saves it as the first version of a CodeRook project.\n\n" +
|
|
1436
|
+
"The files come across; the history does not. Every past commit would\n" +
|
|
1437
|
+
"have to be published as its own version, which on a large project\n" +
|
|
1438
|
+
"takes days — so this takes an honest snapshot rather than leaving a\n" +
|
|
1439
|
+
"half-finished import behind. What arrives is the current state of the\n" +
|
|
1440
|
+
"default branch.\n\n" +
|
|
1441
|
+
"A public repository needs nothing. A private one uses the credentials\n" +
|
|
1442
|
+
"git already has on this machine; CodeRook never asks for, stores or\n" +
|
|
1443
|
+
"forwards a token.\n\n" +
|
|
1444
|
+
"Files that the project's own ignore rules exclude are sent anyway,\n" +
|
|
1445
|
+
"because git keeps tracking anything committed before the rule that\n" +
|
|
1446
|
+
"excludes it, and leaving them out would lose files the source has.",
|
|
1447
|
+
options: [
|
|
1448
|
+
{ flags: "-m, --message <text>", description: "the first version's message" },
|
|
1449
|
+
{ flags: "--track <name>", description: "save onto this line" },
|
|
1450
|
+
{
|
|
1451
|
+
flags: "--no-licence",
|
|
1452
|
+
description: "do not add a licence to the new project",
|
|
1453
|
+
},
|
|
1454
|
+
],
|
|
1455
|
+
examples: [
|
|
1456
|
+
"coderook import https://github.com/owner/project",
|
|
1457
|
+
"coderook import git@github.com:owner/project.git ./project",
|
|
1458
|
+
],
|
|
1459
|
+
run: commandImport,
|
|
1460
|
+
},
|
|
1297
1461
|
{
|
|
1298
1462
|
/*
|
|
1299
1463
|
Where the next save goes, which is a property of this folder rather
|
|
@@ -0,0 +1,150 @@
|
|
|
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.projectNameFromUrl = projectNameFromUrl;
|
|
7
|
+
exports.looksLikeRepositoryUrl = looksLikeRepositoryUrl;
|
|
8
|
+
exports.gitAvailable = gitAvailable;
|
|
9
|
+
exports.fetchSnapshot = fetchSnapshot;
|
|
10
|
+
exports.measure = measure;
|
|
11
|
+
exports.humanBytes = humanBytes;
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const promises_1 = require("node:fs/promises");
|
|
14
|
+
const node_os_1 = require("node:os");
|
|
15
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
+
const node_util_1 = require("node:util");
|
|
17
|
+
const run = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
18
|
+
/**
|
|
19
|
+
* The repository's own name, from the last path segment.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately not the host: an address with no path at all is not a
|
|
22
|
+
* repository, and naming somebody's project `github.com` because the URL was
|
|
23
|
+
* incomplete is the sort of thing they would only notice later.
|
|
24
|
+
*/
|
|
25
|
+
function projectNameFromUrl(url) {
|
|
26
|
+
const trimmed = url.trim().replace(/\/+$/, "");
|
|
27
|
+
/* Drop the scheme and authority so only path segments remain. */
|
|
28
|
+
const withoutScheme = trimmed.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
|
|
29
|
+
const afterHost = /^[^/]*:/.test(withoutScheme)
|
|
30
|
+
? /* scp-style git@host:owner/name */
|
|
31
|
+
withoutScheme.slice(withoutScheme.indexOf(":") + 1)
|
|
32
|
+
: withoutScheme.slice(withoutScheme.indexOf("/") + 1);
|
|
33
|
+
const hasPath = withoutScheme.includes("/") || /^[^/]*:/.test(withoutScheme);
|
|
34
|
+
if (!hasPath)
|
|
35
|
+
return "imported-project";
|
|
36
|
+
const tail = afterHost.split("/").filter(Boolean).pop() ?? "";
|
|
37
|
+
const name = tail.replace(/\.git$/i, "").trim();
|
|
38
|
+
return name || "imported-project";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Refuse anything that is not a repository location.
|
|
42
|
+
*
|
|
43
|
+
* `git clone` will happily treat a local path as a source, and a URL typed
|
|
44
|
+
* with a scheme this does not expect is more likely a mistake than an
|
|
45
|
+
* intention. Being narrow here keeps the command from doing something
|
|
46
|
+
* surprising with an argument that was meant for something else.
|
|
47
|
+
*/
|
|
48
|
+
function looksLikeRepositoryUrl(url) {
|
|
49
|
+
const value = url.trim();
|
|
50
|
+
if (/^(https?|git|ssh):\/\//i.test(value))
|
|
51
|
+
return true;
|
|
52
|
+
/* scp-style: git@host:owner/name.git */
|
|
53
|
+
if (/^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:[^\s]+$/.test(value))
|
|
54
|
+
return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
async function exists(target) {
|
|
58
|
+
try {
|
|
59
|
+
await (0, promises_1.access)(target);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function isEmptyDirectory(target) {
|
|
67
|
+
try {
|
|
68
|
+
const entries = await (0, promises_1.readdir)(target);
|
|
69
|
+
return entries.length === 0;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function gitAvailable() {
|
|
76
|
+
try {
|
|
77
|
+
await run("git", ["--version"], { windowsHide: true });
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Shallow-clone into a working folder and strip the Git metadata.
|
|
86
|
+
*
|
|
87
|
+
* Returns where the files landed. The caller publishes from there with the
|
|
88
|
+
* ordinary save path, so an import produces exactly the Version an ordinary
|
|
89
|
+
* save of the same files would.
|
|
90
|
+
*/
|
|
91
|
+
async function fetchSnapshot(url, into, log = () => { }) {
|
|
92
|
+
const name = projectNameFromUrl(url);
|
|
93
|
+
let folder;
|
|
94
|
+
let temporary = false;
|
|
95
|
+
if (into) {
|
|
96
|
+
folder = node_path_1.default.resolve(into);
|
|
97
|
+
if ((await exists(folder)) && !(await isEmptyDirectory(folder))) {
|
|
98
|
+
throw new Error(`${folder} already has files in it. Import needs an empty folder, ` +
|
|
99
|
+
`so that nothing here is overwritten by what arrives.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const base = await (0, promises_1.mkdtemp)(node_path_1.default.join((0, node_os_1.tmpdir)(), "coderook-import-"));
|
|
104
|
+
folder = node_path_1.default.join(base, name);
|
|
105
|
+
temporary = true;
|
|
106
|
+
}
|
|
107
|
+
log(`Fetching ${url}`);
|
|
108
|
+
/*
|
|
109
|
+
--depth 1 for the reason in the header. --single-branch keeps it to the
|
|
110
|
+
default branch: an import takes a snapshot of one line of work, and
|
|
111
|
+
fetching every branch's tip would cost time to produce content this
|
|
112
|
+
command then discards.
|
|
113
|
+
*/
|
|
114
|
+
await run("git", ["clone", "--depth", "1", "--single-branch", url, folder], { windowsHide: true, maxBuffer: 32 * 1024 * 1024 });
|
|
115
|
+
const gitDirectory = node_path_1.default.join(folder, ".git");
|
|
116
|
+
if (await exists(gitDirectory)) {
|
|
117
|
+
await (0, promises_1.rm)(gitDirectory, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
return { folder, name, temporary };
|
|
120
|
+
}
|
|
121
|
+
/** Total bytes and file count of the fetched tree, for the summary line. */
|
|
122
|
+
async function measure(folder) {
|
|
123
|
+
let files = 0;
|
|
124
|
+
let bytes = 0;
|
|
125
|
+
const walk = async (directory) => {
|
|
126
|
+
for (const entry of await (0, promises_1.readdir)(directory, { withFileTypes: true })) {
|
|
127
|
+
const full = node_path_1.default.join(directory, entry.name);
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
await walk(full);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!entry.isFile())
|
|
133
|
+
continue;
|
|
134
|
+
files += 1;
|
|
135
|
+
bytes += (await (0, promises_1.stat)(full)).size;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
await walk(folder);
|
|
139
|
+
return { files, bytes };
|
|
140
|
+
}
|
|
141
|
+
function humanBytes(value) {
|
|
142
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
143
|
+
let size = value;
|
|
144
|
+
let unit = 0;
|
|
145
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
146
|
+
size /= 1024;
|
|
147
|
+
unit += 1;
|
|
148
|
+
}
|
|
149
|
+
return `${unit === 0 ? size : size.toFixed(size < 10 ? 2 : 1)} ${units[unit]}`;
|
|
150
|
+
}
|
|
@@ -327,7 +327,16 @@ class Uploader {
|
|
|
327
327
|
// A version is a snapshot, not a delta, so it has to name every file in
|
|
328
328
|
// the project — not merely the ones being sent this time. Anything
|
|
329
329
|
// unchanged keeps the object the previous version already pointed at.
|
|
330
|
-
|
|
330
|
+
/*
|
|
331
|
+
An import filters nothing. The rules keep local mess out of a working
|
|
332
|
+
folder, but an imported tree is exactly what the source repository
|
|
333
|
+
tracked — there is no mess in it, and anything the source tracked in
|
|
334
|
+
spite of its own rules (which git does, for files committed before the
|
|
335
|
+
rule) would otherwise be dropped without being mentioned.
|
|
336
|
+
*/
|
|
337
|
+
const rules = request.allowIgnored
|
|
338
|
+
? { shared: "", local: "" }
|
|
339
|
+
: await (0, worktree_js_1.readRules)(request.localPath);
|
|
331
340
|
/*
|
|
332
341
|
Surveyed, not listed.
|
|
333
342
|
|
|
@@ -1152,6 +1161,7 @@ class Uploader {
|
|
|
1152
1161
|
? {}
|
|
1153
1162
|
: { expectedHeadVersionId: request.expectedHeadVersionId }),
|
|
1154
1163
|
...(request.track ? { track: request.track } : {}),
|
|
1164
|
+
...(request.allowIgnored ? { allowIgnored: true } : {}),
|
|
1155
1165
|
/*
|
|
1156
1166
|
Names this attempt so a retry after a lost connection is answered
|
|
1157
1167
|
with the version already made, rather than making a second one.
|
package/package.json
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
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
|
-
".claude-plugin",
|
|
24
|
-
"dist",
|
|
25
|
-
"skills"
|
|
26
|
-
],
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "tsc -p tsconfig.json",
|
|
29
|
-
"check": "tsc -p tsconfig.json --noEmit",
|
|
30
|
-
"test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
|
|
31
|
-
"start": "node dist/cli/src/cli.js",
|
|
32
|
-
"prepublishOnly": "npm run sync:plugin && npm run build",
|
|
33
|
-
"test:e2e": "node test/e2e.mjs",
|
|
34
|
-
"test:matrix": "node test/state-matrix.mjs",
|
|
35
|
-
"test:attempt": "node test/attempt-identity.mjs",
|
|
36
|
-
"test:get": "node test/get-safety.mjs",
|
|
37
|
-
"test:live": "node test/e2e.mjs --allow-production && node test/state-matrix.mjs --allow-production && node test/get-safety.mjs --allow-production && node test/get-protects-edits.mjs --allow-production && node test/upgrade-migration.mjs --allow-production && node test/fault-get.mjs --allow-production && node test/version-floor.mjs --allow-production && node test/fault-submit.mjs --allow-production && node test/race-attempts.mjs --allow-production && node test/attempt-identity.mjs --allow-production",
|
|
38
|
-
"test:getedits": "node test/get-protects-edits.mjs",
|
|
39
|
-
"test:upgrade": "node test/upgrade-migration.mjs",
|
|
40
|
-
"test:faultget": "node test/fault-get.mjs",
|
|
41
|
-
"test:floor": "node test/version-floor.mjs",
|
|
42
|
-
"test:faultsubmit": "node test/fault-submit.mjs",
|
|
43
|
-
"test:race": "node test/race-attempts.mjs",
|
|
44
|
-
"test:runner": "node test/runner-live.mjs",
|
|
45
|
-
"sync:plugin": "node scripts/sync-plugin-version.mjs"
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@types/node": "24.10.1",
|
|
49
|
-
"typescript": "5.9.3"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@coderook/cli",
|
|
3
|
+
"version": "0.23.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
|
+
".claude-plugin",
|
|
24
|
+
"dist",
|
|
25
|
+
"skills"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"test": "tsc -p tsconfig.json && node --test --experimental-strip-types test/*.test.ts",
|
|
31
|
+
"start": "node dist/cli/src/cli.js",
|
|
32
|
+
"prepublishOnly": "npm run sync:plugin && npm run build",
|
|
33
|
+
"test:e2e": "node test/e2e.mjs",
|
|
34
|
+
"test:matrix": "node test/state-matrix.mjs",
|
|
35
|
+
"test:attempt": "node test/attempt-identity.mjs",
|
|
36
|
+
"test:get": "node test/get-safety.mjs",
|
|
37
|
+
"test:live": "node test/e2e.mjs --allow-production && node test/state-matrix.mjs --allow-production && node test/get-safety.mjs --allow-production && node test/get-protects-edits.mjs --allow-production && node test/upgrade-migration.mjs --allow-production && node test/fault-get.mjs --allow-production && node test/version-floor.mjs --allow-production && node test/fault-submit.mjs --allow-production && node test/race-attempts.mjs --allow-production && node test/attempt-identity.mjs --allow-production",
|
|
38
|
+
"test:getedits": "node test/get-protects-edits.mjs",
|
|
39
|
+
"test:upgrade": "node test/upgrade-migration.mjs",
|
|
40
|
+
"test:faultget": "node test/fault-get.mjs",
|
|
41
|
+
"test:floor": "node test/version-floor.mjs",
|
|
42
|
+
"test:faultsubmit": "node test/fault-submit.mjs",
|
|
43
|
+
"test:race": "node test/race-attempts.mjs",
|
|
44
|
+
"test:runner": "node test/runner-live.mjs",
|
|
45
|
+
"sync:plugin": "node scripts/sync-plugin-version.mjs"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "24.10.1",
|
|
49
|
+
"typescript": "5.9.3"
|
|
50
|
+
}
|
|
51
|
+
}
|