@coderook/cli 0.33.0 → 0.34.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 +4 -3
- package/README.md +64 -9
- package/dist/cbx/src/core/upload.js +4 -0
- package/dist/cbx/src/local/remote.js +1 -0
- package/dist/cli/src/build_command.js +808 -0
- package/dist/cli/src/cli.js +278 -74
- package/dist/cli/src/config.js +6 -0
- package/dist/cli/src/folder_match.js +62 -0
- package/dist/cli/src/git_remote.js +76 -11
- package/dist/cli/src/local_commands.js +23 -2
- package/dist/cli/src/mcp.js +39 -3
- package/dist/cli/src/project_choice.js +122 -0
- package/dist/cli/src/publish.js +1 -0
- package/package.json +1 -1
- package/skills/coderook/SKILL.md +66 -2
package/dist/cli/src/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ const help_js_1 = require("./help.js");
|
|
|
28
28
|
const progress_js_1 = require("./progress.js");
|
|
29
29
|
const publish_js_1 = require("./publish.js");
|
|
30
30
|
const attach_command_js_1 = require("./attach_command.js");
|
|
31
|
+
const build_command_js_1 = require("./build_command.js");
|
|
31
32
|
const pages_command_js_1 = require("./pages_command.js");
|
|
32
33
|
const project_commands_js_1 = require("./project_commands.js");
|
|
33
34
|
const track_commands_js_1 = require("./track_commands.js");
|
|
@@ -52,6 +53,8 @@ const profile_js_1 = require("../../cbx/src/core/profile.js");
|
|
|
52
53
|
const detect_js_1 = require("../../cbx/src/core/detect.js");
|
|
53
54
|
const cbx_js_1 = require("../../cbx/src/core/cbx.js");
|
|
54
55
|
const api_js_2 = require("./api.js");
|
|
56
|
+
const folder_match_js_1 = require("./folder_match.js");
|
|
57
|
+
const project_choice_js_1 = require("./project_choice.js");
|
|
55
58
|
const import_command_js_1 = require("./import_command.js");
|
|
56
59
|
const runner_js_1 = require("./runner.js");
|
|
57
60
|
const config_js_1 = require("./config.js");
|
|
@@ -203,11 +206,22 @@ const folderFor = (parsed, index = 0) => node_path_1.default.resolve(parsed.posi
|
|
|
203
206
|
*/
|
|
204
207
|
async function reconcile(folder, options = {}) {
|
|
205
208
|
const link = await (0, config_js_1.readLink)(folder);
|
|
206
|
-
|
|
209
|
+
/*
|
|
210
|
+
A folder that is not linked is not linked.
|
|
211
|
+
|
|
212
|
+
This used to look the folder's name up and link it to whatever answered,
|
|
213
|
+
from any command — `status` included, and straight after `cbx unlink`.
|
|
214
|
+
That is how an inner folder named like its parent's project was saved into
|
|
215
|
+
it, laying a second copy of the app over the project's root. Which project
|
|
216
|
+
a folder belongs to is a question for the person, asked where it matters.
|
|
217
|
+
*/
|
|
218
|
+
if (!link && !options.adopt) {
|
|
219
|
+
return { link: null, baseline: null, behind: null, member: true };
|
|
220
|
+
}
|
|
207
221
|
let member = true;
|
|
208
222
|
let project = link
|
|
209
223
|
? (await (0, api_js_2.projects)()).find((candidate) => candidate.id === link.repositoryId)
|
|
210
|
-
:
|
|
224
|
+
: options.adopt;
|
|
211
225
|
/*
|
|
212
226
|
Not on the account's own list, which holds only projects it is in. A
|
|
213
227
|
folder cloned from somebody else's public project is exactly that, and
|
|
@@ -291,9 +305,11 @@ async function reconcile(folder, options = {}) {
|
|
|
291
305
|
local: Object.fromEntries(baseline),
|
|
292
306
|
manifest: Object.fromEntries(baseline),
|
|
293
307
|
};
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
308
|
+
if (options.persist !== false) {
|
|
309
|
+
await (0, config_js_1.writeLink)(folder, fresh);
|
|
310
|
+
if (!options.quiet && !link) {
|
|
311
|
+
console.log(dim(`Linked this folder to ${project.slug}.`));
|
|
312
|
+
}
|
|
297
313
|
}
|
|
298
314
|
return { link: fresh, baseline, behind: null, member };
|
|
299
315
|
}
|
|
@@ -358,7 +374,35 @@ async function commandUnlink(parsed) {
|
|
|
358
374
|
console.log(`Detached ${bold(node_path_1.default.basename(folder))}` +
|
|
359
375
|
(link?.slug ? dim(` from ${link.slug}`) : "") +
|
|
360
376
|
".");
|
|
361
|
-
console.log(dim("Nothing was deleted.
|
|
377
|
+
console.log(dim("Nothing was deleted. The next submit from here asks which project it is."));
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
async function commandLink(parsed) {
|
|
381
|
+
const reference = parsed.positional[0];
|
|
382
|
+
if (!reference) {
|
|
383
|
+
console.error(red("Say which project: cbx link <project> [folder]"));
|
|
384
|
+
return 1;
|
|
385
|
+
}
|
|
386
|
+
const folder = folderFor(parsed, 1);
|
|
387
|
+
const already = await (0, config_js_1.readLink)(folder);
|
|
388
|
+
if (already) {
|
|
389
|
+
console.error(red(`This folder is already linked to ${already.slug}.`));
|
|
390
|
+
console.error(dim(` Run ${accent("cbx unlink")} first to link it somewhere else.`));
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
393
|
+
const project = await (0, api_js_2.findProject)(reference);
|
|
394
|
+
if (!project) {
|
|
395
|
+
console.error(red(`No project matching "${reference}" on your account.`));
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
const { link } = await reconcile(folder, { adopt: project, quiet: true });
|
|
399
|
+
if (!link) {
|
|
400
|
+
console.error(red(`${project.slug} has nothing saved yet, so there is nothing to link to.`));
|
|
401
|
+
console.error(dim(` Save into it with ${accent(`cbx submit --into ${project.slug}`)}.`));
|
|
402
|
+
return 1;
|
|
403
|
+
}
|
|
404
|
+
console.log(`Linked ${bold(node_path_1.default.basename(folder))} to ${accent(project.slug)}` + dim(` at v${link.sequence}`) + ".");
|
|
405
|
+
console.log(dim(` ${accent("cbx status")} shows how this folder differs from it; nothing was sent or fetched.`));
|
|
362
406
|
return 0;
|
|
363
407
|
}
|
|
364
408
|
async function commandStatus(parsed) {
|
|
@@ -395,7 +439,21 @@ async function commandStatus(parsed) {
|
|
|
395
439
|
console.log(bold(node_path_1.default.basename(folder)) + dim(` ${folder}`));
|
|
396
440
|
console.log(link
|
|
397
441
|
? `Account holds ${accent(`v${link.sequence}`)} · ${Object.keys(link.manifest).length} files ${dim(`(${link.slug})`)}`
|
|
398
|
-
: dim("
|
|
442
|
+
: dim("This folder is not linked to a project."));
|
|
443
|
+
if (!link) {
|
|
444
|
+
const likes = (0, folder_match_js_1.similarProjects)(await (0, api_js_2.projects)().catch(() => []), node_path_1.default.basename(folder));
|
|
445
|
+
if (likes.length) {
|
|
446
|
+
const width = Math.max(...likes.map((like) => like.project.slug.length));
|
|
447
|
+
console.log(dim("Projects on your account like it:"));
|
|
448
|
+
for (const like of likes)
|
|
449
|
+
console.log(` ${await (0, project_choice_js_1.likenessLine)(folder, like, width)}`);
|
|
450
|
+
console.log(dim("Link it with ") + accent(`cbx link ${likes[0].project.slug}`) +
|
|
451
|
+
dim(", or start a separate project with ") + accent('cbx submit --new -m "…"') + dim("."));
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
console.log(dim("Submitting will create a project for it."));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
399
457
|
/*
|
|
400
458
|
A folder can have nothing to send and still not hold the whole version:
|
|
401
459
|
when the service merges somebody else's save into yours, their files
|
|
@@ -592,6 +650,9 @@ async function commandImport(parsed) {
|
|
|
592
650
|
*/
|
|
593
651
|
if (!submitFlags.has("name"))
|
|
594
652
|
submitFlags.set("name", plan.name);
|
|
653
|
+
/* An import is a new project; one already called this is said, not joined. */
|
|
654
|
+
if (!submitFlags.has("into"))
|
|
655
|
+
submitFlags.set("new", true);
|
|
595
656
|
if (!submitFlags.has("m") && !submitFlags.has("message")) {
|
|
596
657
|
submitFlags.set("message", `Imported from ${url}`);
|
|
597
658
|
}
|
|
@@ -606,6 +667,28 @@ async function commandImport(parsed) {
|
|
|
606
667
|
}
|
|
607
668
|
return code;
|
|
608
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* Say so when this folder is a git repository with no CodeRook remote.
|
|
672
|
+
*
|
|
673
|
+
* Every save from one is a snapshot that forgets the commits it holds, and an
|
|
674
|
+
* assistant told only about `cbx submit` never learns there is another way:
|
|
675
|
+
* the remote helper, which saves each commit with its message. One line,
|
|
676
|
+
* after the save, and only until the remote exists.
|
|
677
|
+
*/
|
|
678
|
+
function noteGitRoute(folder, slug) {
|
|
679
|
+
try {
|
|
680
|
+
const config = (0, node_fs_1.readFileSync)(node_path_1.default.join(folder, ".git", "config"), "utf8");
|
|
681
|
+
if (/coderook:/.test(config))
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const name = slug ?? "<project>";
|
|
688
|
+
console.log(dim("This is a git repository. To save each commit with its message instead of snapshots: ") +
|
|
689
|
+
accent(`git remote add coderook coderook://${name}`) + dim(" then ") +
|
|
690
|
+
accent("git push -o adopt coderook main") + dim(" once, and plain git push after."));
|
|
691
|
+
}
|
|
609
692
|
/** A file's SHA-256, streamed; null when it cannot be read. */
|
|
610
693
|
async function digestOfFile(full) {
|
|
611
694
|
try {
|
|
@@ -651,7 +734,38 @@ async function commandSubmit(parsed) {
|
|
|
651
734
|
return 1;
|
|
652
735
|
}
|
|
653
736
|
const dryRun = hasFlag(parsed, "dry-run", "n");
|
|
654
|
-
|
|
737
|
+
/*
|
|
738
|
+
Which project this goes into is settled before anything is scanned, and
|
|
739
|
+
by the person: a folder that is not linked is never joined to a project
|
|
740
|
+
because their names agree, nor quietly made into a second copy of one.
|
|
741
|
+
*/
|
|
742
|
+
const already = await (0, config_js_1.readLink)(folder);
|
|
743
|
+
let adopt;
|
|
744
|
+
const intoFlag = flagText(parsed, "into");
|
|
745
|
+
if (already && (hasFlag(parsed, "new") || (intoFlag && (0, folder_match_js_1.slugFor)(intoFlag.split("/").pop()) !== already.slug))) {
|
|
746
|
+
console.error(red(`This folder is already linked to ${already.slug}.`));
|
|
747
|
+
console.error(dim(` Run ${accent("cbx unlink")} first to save it somewhere else.`));
|
|
748
|
+
return 1;
|
|
749
|
+
}
|
|
750
|
+
if (!already) {
|
|
751
|
+
const chosen = await (0, project_choice_js_1.chooseProject)({
|
|
752
|
+
folder,
|
|
753
|
+
name: flagText(parsed, "name") ?? node_path_1.default.basename(folder),
|
|
754
|
+
...(intoFlag ? { into: intoFlag } : {}),
|
|
755
|
+
startNew: hasFlag(parsed, "new"),
|
|
756
|
+
command: "cbx submit",
|
|
757
|
+
});
|
|
758
|
+
if (!chosen)
|
|
759
|
+
return 1;
|
|
760
|
+
if ("id" in chosen)
|
|
761
|
+
adopt = chosen;
|
|
762
|
+
else
|
|
763
|
+
parsed.flags.set("name", chosen.name);
|
|
764
|
+
}
|
|
765
|
+
const { link, baseline, behind, member } = await reconcile(folder, {
|
|
766
|
+
...(adopt ? { adopt } : {}),
|
|
767
|
+
persist: !dryRun,
|
|
768
|
+
});
|
|
655
769
|
/*
|
|
656
770
|
Somebody else's project, cloned from its public page. Reading and
|
|
657
771
|
fetching are fine; saving into it is not this account's to do, and the
|
|
@@ -680,8 +794,8 @@ async function commandSubmit(parsed) {
|
|
|
680
794
|
A dry run changes nothing, and that includes writing a LICENSE: it says
|
|
681
795
|
what would be added instead.
|
|
682
796
|
*/
|
|
683
|
-
const wouldLicence = dryRun && !link && !hasFlag(parsed, "no-licence") && !(0, node_fs_1.existsSync)(node_path_1.default.join(folder, "LICENSE"));
|
|
684
|
-
const added = link || dryRun
|
|
797
|
+
const wouldLicence = dryRun && !link && !adopt && !hasFlag(parsed, "no-licence") && !(0, node_fs_1.existsSync)(node_path_1.default.join(folder, "LICENSE"));
|
|
798
|
+
const added = link || adopt || dryRun
|
|
685
799
|
? null
|
|
686
800
|
: await (0, licence_commands_js_1.licenceNewProject)(folder,
|
|
687
801
|
/*
|
|
@@ -899,7 +1013,7 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
899
1013
|
console.log("");
|
|
900
1014
|
}
|
|
901
1015
|
const line = progressLine();
|
|
902
|
-
|
|
1016
|
+
let uploader = new upload_js_1.Uploader(config_js_1.credentials);
|
|
903
1017
|
/*
|
|
904
1018
|
Built by the shared description, so this and the git remote helper cannot
|
|
905
1019
|
drift. They already had: the helper listed deletions in `deletions` alone
|
|
@@ -928,8 +1042,13 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
928
1042
|
wins: renaming somebody's project because they passed a flag would be
|
|
929
1043
|
a surprise, and the flag exists for projects that do not exist yet.
|
|
930
1044
|
*/
|
|
931
|
-
projectName: link?.slug ?? flagText(parsed, "name") ?? node_path_1.default.basename(folder),
|
|
932
|
-
repositoryId: link?.repositoryId ?? null,
|
|
1045
|
+
projectName: link?.slug ?? adopt?.slug ?? flagText(parsed, "name") ?? node_path_1.default.basename(folder),
|
|
1046
|
+
repositoryId: link?.repositoryId ?? adopt?.id ?? null,
|
|
1047
|
+
/*
|
|
1048
|
+
Settled above, so a project that appears under this name between the
|
|
1049
|
+
question and the save is a collision to report, not one to join.
|
|
1050
|
+
*/
|
|
1051
|
+
joinExisting: false,
|
|
933
1052
|
// Every current CLI publish states its ancestry. A brand-new project is
|
|
934
1053
|
// explicitly based on an empty Track; a linked folder names the immutable
|
|
935
1054
|
// Version it was last reconciled with.
|
|
@@ -1001,71 +1120,99 @@ Pass ${accent("--allow-secrets")} if these are not real keys.`);
|
|
|
1001
1120
|
}
|
|
1002
1121
|
}
|
|
1003
1122
|
let result;
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1123
|
+
/*
|
|
1124
|
+
How long to wait before picking a dropped upload up again by itself.
|
|
1125
|
+
|
|
1126
|
+
Each request is already retried for a few seconds, but a Wi-Fi drop or a
|
|
1127
|
+
router reconnecting outlasts that, and the upload ended with "run the same
|
|
1128
|
+
command again" for somebody to notice and act on. Running it again was
|
|
1129
|
+
always safe and cheap (pieces already stored are recognised and skipped,
|
|
1130
|
+
and the save itself is named so it can never be made twice), so the
|
|
1131
|
+
command now does it itself, a few times, before giving up.
|
|
1132
|
+
*/
|
|
1133
|
+
const resumeAfter = [10, 30, 60];
|
|
1134
|
+
for (let resumed = 0;; resumed += 1) {
|
|
1135
|
+
try {
|
|
1136
|
+
const plan = await uploader.plan(uploadRequest, (progress) => {
|
|
1137
|
+
line(` ${track(progress.percent)} ${String(progress.percent).padStart(3)}% ` +
|
|
1138
|
+
`${"plan".padEnd(7)} ${progress.files}/${progress.totalFiles} ` +
|
|
1139
|
+
dim(progress.path.slice(-40)));
|
|
1140
|
+
});
|
|
1141
|
+
done(line);
|
|
1142
|
+
console.log(`${accent("Upload plan")} ${bytes(plan.sourceBytes)} selected → ` +
|
|
1143
|
+
`${bytes(plan.compactedBytes)} compacted; ${bytes(plan.chargeableBytes)} new storage.`);
|
|
1144
|
+
console.log(plan.allowance.exempt
|
|
1145
|
+
? dim("Annual plan: the monthly staged upload allowance does not apply.")
|
|
1146
|
+
: dim(`Monthly stage ${plan.allowance.stage}: ${plan.allowance.unlockedPercent}% unlocked, ` +
|
|
1147
|
+
`${bytes(plan.allowance.remainingBytes)} remains after this reservation.`));
|
|
1148
|
+
if (dryRun) {
|
|
1149
|
+
await uploader.cancelPlan(plan, true);
|
|
1150
|
+
const going = files.filter((file) => !file.deleted);
|
|
1151
|
+
const removing = files.filter((file) => file.deleted);
|
|
1152
|
+
console.log(`${going.length} file${going.length === 1 ? "" : "s"} would be sent or reused:`);
|
|
1153
|
+
for (const file of going)
|
|
1154
|
+
console.log(` ${file.path}`);
|
|
1155
|
+
if (removing.length) {
|
|
1156
|
+
console.log(`${removing.length} file${removing.length === 1 ? "" : "s"} would be removed from the save:`);
|
|
1157
|
+
for (const file of removing)
|
|
1158
|
+
console.log(` ${red("deleted")} ${file.path}`);
|
|
1159
|
+
}
|
|
1160
|
+
noteKeptDeletions(folder, baseline, hasFlag(parsed, "sync"));
|
|
1161
|
+
console.log("Nothing was uploaded and no Version was created.");
|
|
1162
|
+
return 0;
|
|
1028
1163
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1164
|
+
result = await uploader.execute(uploadRequest, plan, (progress) => {
|
|
1165
|
+
line(` ${track(progress.percent)} ${String(progress.percent).padStart(3)}% ` +
|
|
1166
|
+
`${progress.stage.padEnd(7)} ${progress.files}/${progress.totalFiles} ` +
|
|
1167
|
+
dim(progress.path.slice(-40)));
|
|
1168
|
+
});
|
|
1169
|
+
break;
|
|
1032
1170
|
}
|
|
1033
|
-
|
|
1034
|
-
line
|
|
1035
|
-
`${progress.stage.padEnd(7)} ${progress.files}/${progress.totalFiles} ` +
|
|
1036
|
-
dim(progress.path.slice(-40)));
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
catch (error) {
|
|
1040
|
-
done(line);
|
|
1041
|
-
/*
|
|
1042
|
-
Classified by the shared description rather than by a private regular
|
|
1043
|
-
expression, so a push and a submit disagree about nothing — including
|
|
1044
|
-
what counts as "the connection failed" and what that means for whether
|
|
1045
|
-
the work was saved.
|
|
1046
|
-
*/
|
|
1047
|
-
const failure = (0, publish_js_1.classifyPublishFailure)(error);
|
|
1048
|
-
const text = (0, network_js_1.describeError)(error);
|
|
1049
|
-
if (failure?.kind === "interrupted") {
|
|
1050
|
-
console.error(red(`
|
|
1051
|
-
The connection failed: ${text}`));
|
|
1052
|
-
console.error(`Your work may already have been saved. Run the same command again —` +
|
|
1053
|
-
` it will not create a second version.`);
|
|
1054
|
-
return 1;
|
|
1055
|
-
}
|
|
1056
|
-
if (failure?.kind === "conflict") {
|
|
1171
|
+
catch (error) {
|
|
1172
|
+
done(line);
|
|
1057
1173
|
/*
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1174
|
+
Classified by the shared description rather than by a private regular
|
|
1175
|
+
expression, so a push and a submit disagree about nothing — including
|
|
1176
|
+
what counts as "the connection failed" and what that means for whether
|
|
1177
|
+
the work was saved.
|
|
1062
1178
|
*/
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1179
|
+
const failure = (0, publish_js_1.classifyPublishFailure)(error);
|
|
1180
|
+
const text = (0, network_js_1.describeError)(error);
|
|
1181
|
+
if (failure?.kind === "interrupted" && !dryRun && resumed < resumeAfter.length) {
|
|
1182
|
+
const wait = resumeAfter[resumed];
|
|
1183
|
+
console.error(dim(`The connection dropped (${text})`));
|
|
1184
|
+
console.error(dim(`Picking the upload up again in ${wait}s (try ${resumed + 2} of ${resumeAfter.length + 1}).` +
|
|
1185
|
+
` Pieces already sent are kept; only the rest goes.`));
|
|
1186
|
+
/*
|
|
1187
|
+
The failed attempt's other lanes are still retrying; stopped first, so
|
|
1188
|
+
they do not carry on sending beside the attempt that replaces them.
|
|
1189
|
+
*/
|
|
1190
|
+
uploader.cancel();
|
|
1191
|
+
await (0, network_js_1.pause)(wait * 1000);
|
|
1192
|
+
uploader = new upload_js_1.Uploader(config_js_1.credentials);
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (failure?.kind === "interrupted") {
|
|
1196
|
+
console.error(red(`
|
|
1197
|
+
The connection failed: ${text}`));
|
|
1198
|
+
console.error(`Your work may already have been saved. Run the same command again —` +
|
|
1199
|
+
` it will not create a second version.`);
|
|
1200
|
+
return 1;
|
|
1201
|
+
}
|
|
1202
|
+
if (failure?.kind === "conflict") {
|
|
1203
|
+
/*
|
|
1204
|
+
Somebody else saved to this project first. Everything that did not
|
|
1205
|
+
overlap has already been combined by the service; what is left is a
|
|
1206
|
+
genuine decision, so the answer is to bring their work down and look
|
|
1207
|
+
at it rather than to try again harder.
|
|
1208
|
+
*/
|
|
1209
|
+
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
1210
|
+
console.error(`\nRun ${accent("cbx get")} to bring the latest version down, then` +
|
|
1211
|
+
` submit again. Nothing was changed on your account.`);
|
|
1212
|
+
return 1;
|
|
1213
|
+
}
|
|
1214
|
+
throw error;
|
|
1067
1215
|
}
|
|
1068
|
-
throw error;
|
|
1069
1216
|
}
|
|
1070
1217
|
done(line);
|
|
1071
1218
|
if (result.mergeTrack) {
|
|
@@ -1167,6 +1314,7 @@ The connection failed: ${text}`));
|
|
|
1167
1314
|
*/
|
|
1168
1315
|
` · the save holds ${bytes(result.sourceBytes)}`);
|
|
1169
1316
|
noteKeptDeletions(folder, baseline, hasFlag(parsed, "sync"));
|
|
1317
|
+
noteGitRoute(folder, link?.slug ?? adopt?.slug ?? (0, folder_match_js_1.slugFor)(flagText(parsed, "name") ?? node_path_1.default.basename(folder)));
|
|
1170
1318
|
/*
|
|
1171
1319
|
Where the time went, when asked for.
|
|
1172
1320
|
|
|
@@ -1198,6 +1346,8 @@ async function commandGet(parsed) {
|
|
|
1198
1346
|
const { link } = await reconcile(folder);
|
|
1199
1347
|
if (!link) {
|
|
1200
1348
|
console.error(red("This folder is not linked to a project on your account."));
|
|
1349
|
+
console.error(dim(" Link it with ") + accent("cbx link <project>") +
|
|
1350
|
+
dim(", or fetch a project into a new folder with ") + accent("cbx clone <project>") + dim("."));
|
|
1201
1351
|
return 1;
|
|
1202
1352
|
}
|
|
1203
1353
|
/*
|
|
@@ -2065,7 +2215,10 @@ const SPECS = [
|
|
|
2065
2215
|
usage: 'submit [folder] -m "…"',
|
|
2066
2216
|
detail: "Sends everything that changed since the last save. If the folder is\n" +
|
|
2067
2217
|
"not linked to a project yet, one is created on your account, named\n" +
|
|
2068
|
-
"after the folder and private to begin with
|
|
2218
|
+
"after the folder and private to begin with — unless your account\n" +
|
|
2219
|
+
"already has a project with a name like it. Then you are asked which\n" +
|
|
2220
|
+
"you mean, and a script or assistant must say: --into <project> saves\n" +
|
|
2221
|
+
"into it, --new starts a separate one.\n\n" +
|
|
2069
2222
|
"A save is not a version. Nobody outside the project can see one until\n" +
|
|
2070
2223
|
"it is named, which is what `cbx release` does — so submitting is as\n" +
|
|
2071
2224
|
"cheap and as private as you want it to be.",
|
|
@@ -2076,6 +2229,14 @@ const SPECS = [
|
|
|
2076
2229
|
flags: "--name <name>",
|
|
2077
2230
|
description: "name a new project this, instead of after the folder",
|
|
2078
2231
|
},
|
|
2232
|
+
{
|
|
2233
|
+
flags: "--into <project>",
|
|
2234
|
+
description: "first save from this folder: save into this project and link the folder to it",
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
flags: "--new",
|
|
2238
|
+
description: "first save from this folder: start a new project even if one has a similar name",
|
|
2239
|
+
},
|
|
2079
2240
|
{
|
|
2080
2241
|
flags: "--allow-secrets",
|
|
2081
2242
|
description: "send files that look like credentials, and files with keys inside",
|
|
@@ -2095,6 +2256,7 @@ const SPECS = [
|
|
|
2095
2256
|
],
|
|
2096
2257
|
examples: [
|
|
2097
2258
|
'cbx submit -m "Fix the export dialog"',
|
|
2259
|
+
'cbx submit --into voxai-coder -m "First save from the Linux machine"',
|
|
2098
2260
|
'cbx submit --track spike -m "Try the other encoder"',
|
|
2099
2261
|
],
|
|
2100
2262
|
run: commandSubmit,
|
|
@@ -2211,6 +2373,20 @@ const SPECS = [
|
|
|
2211
2373
|
examples: ["cbx switch", "cbx switch -c spike", "cbx switch main"],
|
|
2212
2374
|
run: (0, local_commands_js_1.localFirst)(local_commands_js_1.localSwitch, track_commands_js_1.commandTrack),
|
|
2213
2375
|
},
|
|
2376
|
+
{
|
|
2377
|
+
name: "link",
|
|
2378
|
+
group: "Working with a folder",
|
|
2379
|
+
summary: "connect this folder to a project already on your account",
|
|
2380
|
+
usage: "link <project> [folder]",
|
|
2381
|
+
detail: "Says which project this folder is a copy of, without sending or\n" +
|
|
2382
|
+
"fetching anything. Use it for a folder that holds a project you saved\n" +
|
|
2383
|
+
"from somewhere else — another machine, another operating system, or a\n" +
|
|
2384
|
+
"copy made by hand — so its next save goes into that project instead\n" +
|
|
2385
|
+
"of starting a new one. Files that differ here are what the next save\n" +
|
|
2386
|
+
"sends; `cbx status` shows them first.",
|
|
2387
|
+
examples: ["cbx link voxai-coder", "cbx link my-project ./copy"],
|
|
2388
|
+
run: commandLink,
|
|
2389
|
+
},
|
|
2214
2390
|
{
|
|
2215
2391
|
name: "unlink",
|
|
2216
2392
|
group: "Working with a folder",
|
|
@@ -3126,6 +3302,34 @@ function localTwin(name, original, local, row, extra) {
|
|
|
3126
3302
|
run: (0, local_commands_js_1.localFirst)(local, spec.run),
|
|
3127
3303
|
};
|
|
3128
3304
|
}
|
|
3305
|
+
SPECS.push({
|
|
3306
|
+
name: "build",
|
|
3307
|
+
group: "Actions",
|
|
3308
|
+
summary: "build executables for Windows, Linux and macOS on this machine",
|
|
3309
|
+
usage: "build [folder] [--target win,linux,mac]",
|
|
3310
|
+
detail: "Builds the project in this folder for each target, using whatever this\n" +
|
|
3311
|
+
"machine has: Go, Bun, Deno and .NET build every platform from any one;\n" +
|
|
3312
|
+
"Rust does with cargo-zigbuild; Electron builds Windows and Linux in\n" +
|
|
3313
|
+
"Docker; PyInstaller builds Linux in Docker or WSL. A target this machine\n" +
|
|
3314
|
+
"cannot build is named, with what would build it. Nothing is uploaded\n" +
|
|
3315
|
+
"unless --ship is given. Output goes to .coderook/build/<target>/.\n\n" +
|
|
3316
|
+
"The toolchain is recognised from the files here. To fix it, or to give\n" +
|
|
3317
|
+
"a command of your own, describe the build in coderook.build.json\n" +
|
|
3318
|
+
"(cbx build init writes one), which is saved with the project.",
|
|
3319
|
+
options: [
|
|
3320
|
+
{ flags: "-t, --target <list>", description: "windows-x64, linux-arm64, macos-arm64 … or win, linux, mac, all" },
|
|
3321
|
+
{ flags: "--plan", description: "say how each target would be built, build nothing" },
|
|
3322
|
+
{ flags: "--preset <name>", description: "go, bun, deno, rust, dotnet, electron, pyinstaller or custom" },
|
|
3323
|
+
{ flags: "--ship", description: "attach what was built to the project's newest save" },
|
|
3324
|
+
],
|
|
3325
|
+
examples: [
|
|
3326
|
+
"cbx build --plan",
|
|
3327
|
+
"cbx build --target win,linux,mac",
|
|
3328
|
+
"cbx build --target all --ship",
|
|
3329
|
+
"cbx build init",
|
|
3330
|
+
],
|
|
3331
|
+
run: build_command_js_1.commandBuild,
|
|
3332
|
+
});
|
|
3129
3333
|
SPECS.push(localTwin("push", "submit", local_commands_js_1.localPush, {
|
|
3130
3334
|
summary: "send this line's saves to CodeRook",
|
|
3131
3335
|
usage: "push",
|
package/dist/cli/src/config.js
CHANGED
|
@@ -402,6 +402,12 @@ async function forgetLink(localPath) {
|
|
|
402
402
|
await writePrivate(storedFileFor(key), JSON.stringify(forgotten));
|
|
403
403
|
}
|
|
404
404
|
await (0, promises_1.rm)(folderLinkFile(localPath), { force: true });
|
|
405
|
+
/* The folder CodeRook made for the link goes too, unless it holds anything else. */
|
|
406
|
+
const directory = node_path_1.default.dirname(folderLinkFile(localPath));
|
|
407
|
+
const left = await (0, promises_1.readdir)(directory).catch(() => null);
|
|
408
|
+
if (left && left.length === 1 && left[0] === ".gitignore") {
|
|
409
|
+
await (0, promises_1.rm)(directory, { recursive: true, force: true });
|
|
410
|
+
}
|
|
405
411
|
return true;
|
|
406
412
|
}
|
|
407
413
|
async function writeLink(localPath, link) {
|
|
@@ -0,0 +1,62 @@
|
|
|
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.slugFor = slugFor;
|
|
7
|
+
exports.similarProjects = similarProjects;
|
|
8
|
+
exports.pathsPresent = pathsPresent;
|
|
9
|
+
/*
|
|
10
|
+
Which of the account's projects a folder that is not linked might be.
|
|
11
|
+
|
|
12
|
+
A save from an unlinked folder used to decide this alone: a project with the
|
|
13
|
+
folder's name was joined without a word, and anything else started a new
|
|
14
|
+
project without one either. Both went wrong on a real account. An inner
|
|
15
|
+
folder called `voxai-coder` was joined to the project of the same name and
|
|
16
|
+
laid a second copy of the app over its root for thirteen versions; a Linux
|
|
17
|
+
machine that did not know the folder was linked started `voxai-coder-linux`
|
|
18
|
+
beside it. Neither was a choice anybody made.
|
|
19
|
+
|
|
20
|
+
So this only finds the candidates. Choosing between them is the person's.
|
|
21
|
+
*/
|
|
22
|
+
const node_fs_1 = require("node:fs");
|
|
23
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
24
|
+
/** The slug the service gives a name: what a new project would be called. */
|
|
25
|
+
function slugFor(name) {
|
|
26
|
+
return (name
|
|
27
|
+
.trim()
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
30
|
+
.replace(/^-|-$/g, "") || "project");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Projects whose name is this one, or this one with a word added or taken
|
|
34
|
+
* away: `voxai-coder` finds `voxai-coder-linux`, and the other way round.
|
|
35
|
+
*
|
|
36
|
+
* Words, not letters. `app` is not like `apple`, and a project somebody
|
|
37
|
+
* named `wallpaper-picker-backend` on purpose is offered, never assumed.
|
|
38
|
+
*/
|
|
39
|
+
function similarProjects(all, name) {
|
|
40
|
+
const wanted = slugFor(name);
|
|
41
|
+
const related = (slug) => slug.startsWith(`${wanted}-`) || wanted.startsWith(`${slug}-`);
|
|
42
|
+
return all
|
|
43
|
+
.filter((project) => project.slug === wanted || related(project.slug))
|
|
44
|
+
.map((project) => ({ project, sameName: project.slug === wanted }))
|
|
45
|
+
.sort((left, right) => Number(right.sameName) - Number(left.sameName))
|
|
46
|
+
.slice(0, 5);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* How many of a project's files are in this folder at the same path.
|
|
50
|
+
*
|
|
51
|
+
* Paths only, and only counted: reading every file to compare digests would
|
|
52
|
+
* make the question cost a full scan, and a folder that holds most of a
|
|
53
|
+
* project's paths is already the answer somebody needs to choose.
|
|
54
|
+
*/
|
|
55
|
+
function pathsPresent(folder, paths) {
|
|
56
|
+
let present = 0;
|
|
57
|
+
for (const file of paths) {
|
|
58
|
+
if ((0, node_fs_1.existsSync)(node_path_1.default.join(folder, ...file.split("/"))))
|
|
59
|
+
present += 1;
|
|
60
|
+
}
|
|
61
|
+
return present;
|
|
62
|
+
}
|