@coderook/cli 0.33.1 → 0.34.1
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 +66 -10
- package/dist/cbx/src/core/cbx.js +9 -0
- 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 +932 -0
- package/dist/cli/src/cli.js +197 -15
- 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/git_remote_bin.js +3 -1
- package/dist/cli/src/local_commands.js +23 -2
- package/dist/cli/src/mcp.js +39 -3
- package/dist/cli/src/node_check.js +26 -0
- package/dist/cli/src/portable_python.js +370 -0
- package/dist/cli/src/project_choice.js +122 -0
- package/dist/cli/src/publish.js +1 -0
- package/package.json +2 -2
- package/skills/coderook/SKILL.md +66 -2
package/dist/cli/src/cli.js
CHANGED
|
@@ -28,6 +28,8 @@ 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");
|
|
32
|
+
const node_check_js_1 = require("./node_check.js");
|
|
31
33
|
const pages_command_js_1 = require("./pages_command.js");
|
|
32
34
|
const project_commands_js_1 = require("./project_commands.js");
|
|
33
35
|
const track_commands_js_1 = require("./track_commands.js");
|
|
@@ -52,6 +54,8 @@ const profile_js_1 = require("../../cbx/src/core/profile.js");
|
|
|
52
54
|
const detect_js_1 = require("../../cbx/src/core/detect.js");
|
|
53
55
|
const cbx_js_1 = require("../../cbx/src/core/cbx.js");
|
|
54
56
|
const api_js_2 = require("./api.js");
|
|
57
|
+
const folder_match_js_1 = require("./folder_match.js");
|
|
58
|
+
const project_choice_js_1 = require("./project_choice.js");
|
|
55
59
|
const import_command_js_1 = require("./import_command.js");
|
|
56
60
|
const runner_js_1 = require("./runner.js");
|
|
57
61
|
const config_js_1 = require("./config.js");
|
|
@@ -203,11 +207,22 @@ const folderFor = (parsed, index = 0) => node_path_1.default.resolve(parsed.posi
|
|
|
203
207
|
*/
|
|
204
208
|
async function reconcile(folder, options = {}) {
|
|
205
209
|
const link = await (0, config_js_1.readLink)(folder);
|
|
206
|
-
|
|
210
|
+
/*
|
|
211
|
+
A folder that is not linked is not linked.
|
|
212
|
+
|
|
213
|
+
This used to look the folder's name up and link it to whatever answered,
|
|
214
|
+
from any command — `status` included, and straight after `cbx unlink`.
|
|
215
|
+
That is how an inner folder named like its parent's project was saved into
|
|
216
|
+
it, laying a second copy of the app over the project's root. Which project
|
|
217
|
+
a folder belongs to is a question for the person, asked where it matters.
|
|
218
|
+
*/
|
|
219
|
+
if (!link && !options.adopt) {
|
|
220
|
+
return { link: null, baseline: null, behind: null, member: true };
|
|
221
|
+
}
|
|
207
222
|
let member = true;
|
|
208
223
|
let project = link
|
|
209
224
|
? (await (0, api_js_2.projects)()).find((candidate) => candidate.id === link.repositoryId)
|
|
210
|
-
:
|
|
225
|
+
: options.adopt;
|
|
211
226
|
/*
|
|
212
227
|
Not on the account's own list, which holds only projects it is in. A
|
|
213
228
|
folder cloned from somebody else's public project is exactly that, and
|
|
@@ -291,9 +306,11 @@ async function reconcile(folder, options = {}) {
|
|
|
291
306
|
local: Object.fromEntries(baseline),
|
|
292
307
|
manifest: Object.fromEntries(baseline),
|
|
293
308
|
};
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
309
|
+
if (options.persist !== false) {
|
|
310
|
+
await (0, config_js_1.writeLink)(folder, fresh);
|
|
311
|
+
if (!options.quiet && !link) {
|
|
312
|
+
console.log(dim(`Linked this folder to ${project.slug}.`));
|
|
313
|
+
}
|
|
297
314
|
}
|
|
298
315
|
return { link: fresh, baseline, behind: null, member };
|
|
299
316
|
}
|
|
@@ -355,10 +372,37 @@ async function commandUnlink(parsed) {
|
|
|
355
372
|
console.log("This folder was not linked to a project.");
|
|
356
373
|
return 0;
|
|
357
374
|
}
|
|
358
|
-
console.log(`
|
|
359
|
-
(link?.slug ? dim(` from ${link.slug}`) : "") +
|
|
375
|
+
console.log((link?.slug ? `Unlinked this folder from ${bold(link.slug)}` : `Unlinked ${bold(node_path_1.default.basename(folder))}`) +
|
|
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
|
/*
|
|
@@ -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.
|
|
@@ -1195,6 +1314,7 @@ The connection failed: ${text}`));
|
|
|
1195
1314
|
*/
|
|
1196
1315
|
` · the save holds ${bytes(result.sourceBytes)}`);
|
|
1197
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)));
|
|
1198
1318
|
/*
|
|
1199
1319
|
Where the time went, when asked for.
|
|
1200
1320
|
|
|
@@ -1226,6 +1346,8 @@ async function commandGet(parsed) {
|
|
|
1226
1346
|
const { link } = await reconcile(folder);
|
|
1227
1347
|
if (!link) {
|
|
1228
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("."));
|
|
1229
1351
|
return 1;
|
|
1230
1352
|
}
|
|
1231
1353
|
/*
|
|
@@ -2093,7 +2215,10 @@ const SPECS = [
|
|
|
2093
2215
|
usage: 'submit [folder] -m "…"',
|
|
2094
2216
|
detail: "Sends everything that changed since the last save. If the folder is\n" +
|
|
2095
2217
|
"not linked to a project yet, one is created on your account, named\n" +
|
|
2096
|
-
"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" +
|
|
2097
2222
|
"A save is not a version. Nobody outside the project can see one until\n" +
|
|
2098
2223
|
"it is named, which is what `cbx release` does — so submitting is as\n" +
|
|
2099
2224
|
"cheap and as private as you want it to be.",
|
|
@@ -2104,6 +2229,14 @@ const SPECS = [
|
|
|
2104
2229
|
flags: "--name <name>",
|
|
2105
2230
|
description: "name a new project this, instead of after the folder",
|
|
2106
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
|
+
},
|
|
2107
2240
|
{
|
|
2108
2241
|
flags: "--allow-secrets",
|
|
2109
2242
|
description: "send files that look like credentials, and files with keys inside",
|
|
@@ -2123,6 +2256,7 @@ const SPECS = [
|
|
|
2123
2256
|
],
|
|
2124
2257
|
examples: [
|
|
2125
2258
|
'cbx submit -m "Fix the export dialog"',
|
|
2259
|
+
'cbx submit --into voxai-coder -m "First save from the Linux machine"',
|
|
2126
2260
|
'cbx submit --track spike -m "Try the other encoder"',
|
|
2127
2261
|
],
|
|
2128
2262
|
run: commandSubmit,
|
|
@@ -2239,6 +2373,20 @@ const SPECS = [
|
|
|
2239
2373
|
examples: ["cbx switch", "cbx switch -c spike", "cbx switch main"],
|
|
2240
2374
|
run: (0, local_commands_js_1.localFirst)(local_commands_js_1.localSwitch, track_commands_js_1.commandTrack),
|
|
2241
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
|
+
},
|
|
2242
2390
|
{
|
|
2243
2391
|
name: "unlink",
|
|
2244
2392
|
group: "Working with a folder",
|
|
@@ -3154,6 +3302,35 @@ function localTwin(name, original, local, row, extra) {
|
|
|
3154
3302
|
run: (0, local_commands_js_1.localFirst)(local, spec.run),
|
|
3155
3303
|
};
|
|
3156
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 Linux from Windows with\n" +
|
|
3313
|
+
"nothing extra; Python ships the target's own interpreter and wheels.\n" +
|
|
3314
|
+
"Linux never needs Docker. A target this machine\n" +
|
|
3315
|
+
"cannot build is named, with what would build it. Nothing is uploaded\n" +
|
|
3316
|
+
"unless --ship is given. Output goes to .coderook/build/<target>/.\n\n" +
|
|
3317
|
+
"The toolchain is recognised from the files here. To fix it, or to give\n" +
|
|
3318
|
+
"a command of your own, describe the build in coderook.build.json\n" +
|
|
3319
|
+
"(cbx build init writes one), which is saved with the project.",
|
|
3320
|
+
options: [
|
|
3321
|
+
{ flags: "-t, --target <list>", description: "windows-x64, linux-arm64, macos-arm64 … or win, linux, mac, all" },
|
|
3322
|
+
{ flags: "--plan", description: "say how each target would be built, build nothing" },
|
|
3323
|
+
{ flags: "--preset <name>", description: "go, bun, deno, rust, dotnet, electron, pyinstaller, python or custom" },
|
|
3324
|
+
{ flags: "--ship", description: "attach what was built to the project's newest save" },
|
|
3325
|
+
],
|
|
3326
|
+
examples: [
|
|
3327
|
+
"cbx build --plan",
|
|
3328
|
+
"cbx build --target win,linux,mac",
|
|
3329
|
+
"cbx build --target all --ship",
|
|
3330
|
+
"cbx build init",
|
|
3331
|
+
],
|
|
3332
|
+
run: build_command_js_1.commandBuild,
|
|
3333
|
+
});
|
|
3157
3334
|
SPECS.push(localTwin("push", "submit", local_commands_js_1.localPush, {
|
|
3158
3335
|
summary: "send this line's saves to CodeRook",
|
|
3159
3336
|
usage: "push",
|
|
@@ -3179,6 +3356,11 @@ SPECS.push(localTwin("push", "submit", local_commands_js_1.localPush, {
|
|
|
3179
3356
|
}, [{ flags: "-f, --force", description: "discard unsaved changes that are in the way" }]));
|
|
3180
3357
|
const REGISTRY = (0, registry_js_1.buildRegistry)(SPECS);
|
|
3181
3358
|
async function main(argv) {
|
|
3359
|
+
const old = (0, node_check_js_1.nodeTooOld)();
|
|
3360
|
+
if (old) {
|
|
3361
|
+
console.error(red(old));
|
|
3362
|
+
return 1;
|
|
3363
|
+
}
|
|
3182
3364
|
const [name, ...rest] = argv;
|
|
3183
3365
|
if (!name || name === "--help" || name === "-h") {
|
|
3184
3366
|
console.log((0, help_js_1.renderHelp)(REGISTRY, version_js_1.VERSION));
|
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
|
+
}
|
|
@@ -934,7 +934,7 @@ depth = null) {
|
|
|
934
934
|
await (0, promises_1.rm)(scratch, { recursive: true, force: true });
|
|
935
935
|
}
|
|
936
936
|
}
|
|
937
|
-
async function doPush(requests, url) {
|
|
937
|
+
async function doPush(requests, url, options = {}) {
|
|
938
938
|
/*
|
|
939
939
|
A push never goes to the account named in the URL — it goes to the one the
|
|
940
940
|
token belongs to. Publishing somebody's work to a different account on
|
|
@@ -988,6 +988,19 @@ async function doPush(requests, url) {
|
|
|
988
988
|
if (!repositoryId)
|
|
989
989
|
repositoryId = (await (0, api_js_1.findProject)(slug))?.id ?? null;
|
|
990
990
|
state = repositoryId ? await remoteState(repositoryId) : null;
|
|
991
|
+
/*
|
|
992
|
+
A clone knows commits the messages do not name: the ones it built when
|
|
993
|
+
it imported each version. Counted as published, so a branch started in
|
|
994
|
+
a clone forks from the version it was cloned at — and a clone of a
|
|
995
|
+
project saved with `cbx submit` is not mistaken for unrelated history.
|
|
996
|
+
*/
|
|
997
|
+
if (state && marks) {
|
|
998
|
+
for (const [versionId, mark] of marks.ofVersion) {
|
|
999
|
+
const sha = marks.sha.get(mark);
|
|
1000
|
+
if (sha && !state.versionOfSha.has(sha))
|
|
1001
|
+
state.versionOfSha.set(sha, versionId);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
991
1004
|
};
|
|
992
1005
|
const knownRefs = () => {
|
|
993
1006
|
const found = new Map();
|
|
@@ -1154,11 +1167,36 @@ async function doPush(requests, url) {
|
|
|
1154
1167
|
the ordinary act of pushing a second branch to a project git already
|
|
1155
1168
|
owns, which is the common case and not a conflict of any kind.
|
|
1156
1169
|
*/
|
|
1170
|
+
/*
|
|
1171
|
+
Unless the person says this repository *is* that project.
|
|
1172
|
+
|
|
1173
|
+
The usual way here is a working repository whose CodeRook project was
|
|
1174
|
+
started with `cbx submit` or the desktop app: same files, different
|
|
1175
|
+
history. Refusing outright sent people to make a second project, which
|
|
1176
|
+
is the duplicate this exists to prevent. `-o adopt` takes the middle
|
|
1177
|
+
road: the project's saves stay as they are, this commit's tree is saved
|
|
1178
|
+
on top as one save that names the commit, and from then on the project
|
|
1179
|
+
and the repository agree, so later pushes add one save per commit.
|
|
1180
|
+
*/
|
|
1181
|
+
let adopting = null;
|
|
1157
1182
|
if (state && !already && state.versionCount > 0 && state.versionOfSha.size === 0) {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1183
|
+
const line = state.tracks.find((candidate) => candidate.name === branch && candidate.kind === "line");
|
|
1184
|
+
if (options.adopt && line?.headVersionId) {
|
|
1185
|
+
adopting = { headVersionId: line.headVersionId };
|
|
1186
|
+
say(` adopting "${slug}": its ${state.versionCount} save${state.versionCount === 1 ? "" : "s"} stay as they are;` +
|
|
1187
|
+
` ${tip.slice(0, 8)} is saved on top as one save,\n and later pushes add one save per commit.`);
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
send(`error ${request.dst} the project "${slug}" already has ${state.versionCount} version${state.versionCount === 1 ? "" : "s"} that did not come from git; pushing would publish this history a second time`);
|
|
1191
|
+
say(options.adopt
|
|
1192
|
+
? `\n The project has no line called "${branch}" to adopt. Push the branch it saves on, usually main.\n`
|
|
1193
|
+
: `\n If this repository holds that project, save it on top and link the two:\n` +
|
|
1194
|
+
` git push -o adopt coderook ${branch}\n` +
|
|
1195
|
+
` Your git history is not replayed; the commit you push becomes one save,\n` +
|
|
1196
|
+
` and every push after it sends one save per commit.\n\n` +
|
|
1197
|
+
` Or start from the project instead: git clone coderook://${slug}\n`);
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1162
1200
|
}
|
|
1163
1201
|
/*
|
|
1164
1202
|
A branch the project has never seen starts at the newest commit it
|
|
@@ -1173,10 +1211,12 @@ async function doPush(requests, url) {
|
|
|
1173
1211
|
}
|
|
1174
1212
|
const from = already ?? fork?.sha ?? null;
|
|
1175
1213
|
const range = from ? `${from}..${tip}` : tip;
|
|
1176
|
-
const commits =
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1214
|
+
const commits = adopting
|
|
1215
|
+
? [tip]
|
|
1216
|
+
: git(["rev-list", "--reverse", "--topo-order", range])
|
|
1217
|
+
.split("\n")
|
|
1218
|
+
.map((line) => line.trim())
|
|
1219
|
+
.filter(Boolean);
|
|
1180
1220
|
if (!commits.length) {
|
|
1181
1221
|
/*
|
|
1182
1222
|
Nothing to publish does not mean nothing to do. A branch whose tip
|
|
@@ -1236,7 +1276,14 @@ async function doPush(requests, url) {
|
|
|
1236
1276
|
let previous = git_history_js_1.EMPTY_TREE;
|
|
1237
1277
|
let currentRepositoryId = repositoryId;
|
|
1238
1278
|
let published = 0;
|
|
1239
|
-
if (
|
|
1279
|
+
if (adopting && repositoryId) {
|
|
1280
|
+
baseVersionId = adopting.headVersionId;
|
|
1281
|
+
const { Downloader } = await Promise.resolve().then(() => __importStar(require("../../cbx/src/core/download.js")));
|
|
1282
|
+
const held = await new Downloader(config_js_1.credentials).files(repositoryId, baseVersionId);
|
|
1283
|
+
manifest = Object.fromEntries(held.map((file) => [file.path, file.sha256]));
|
|
1284
|
+
local = { ...manifest };
|
|
1285
|
+
}
|
|
1286
|
+
else if (from && repositoryId && state) {
|
|
1240
1287
|
previous = from;
|
|
1241
1288
|
if (already) {
|
|
1242
1289
|
const track = state.tracks.find((candidate) => candidate.name === branch && candidate.kind === "line");
|
|
@@ -1281,6 +1328,17 @@ async function doPush(requests, url) {
|
|
|
1281
1328
|
}
|
|
1282
1329
|
const changes = await applyCommit(catFile, scratch, previous, sha);
|
|
1283
1330
|
previous = sha;
|
|
1331
|
+
if (adopting) {
|
|
1332
|
+
/*
|
|
1333
|
+
The commit's tree is the whole truth: what the project holds and
|
|
1334
|
+
the commit does not is removed, exactly as a later push would.
|
|
1335
|
+
*/
|
|
1336
|
+
const inTree = new Set(git(["ls-tree", "-r", "-z", "--name-only", sha]).split("\0").filter(Boolean));
|
|
1337
|
+
for (const file of Object.keys(manifest)) {
|
|
1338
|
+
if (!inTree.has(file) && !changes.deleted.includes(file))
|
|
1339
|
+
changes.deleted.push(file);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1284
1342
|
if (changes.skipped.length) {
|
|
1285
1343
|
say(` skipped ${changes.skipped.length} submodule path(s) in ${sha.slice(0, 8)}`);
|
|
1286
1344
|
}
|
|
@@ -1463,6 +1521,8 @@ async function main(argv) {
|
|
|
1463
1521
|
let unservable = false;
|
|
1464
1522
|
/** `git clone --depth N`, when git passed one. */
|
|
1465
1523
|
let depth = null;
|
|
1524
|
+
/** `git push -o adopt`. */
|
|
1525
|
+
let adopt = false;
|
|
1466
1526
|
for await (const line of lines()) {
|
|
1467
1527
|
const command = line.trim();
|
|
1468
1528
|
if (command === "capabilities") {
|
|
@@ -1486,6 +1546,11 @@ async function main(argv) {
|
|
|
1486
1546
|
depth = Number(value);
|
|
1487
1547
|
send("ok");
|
|
1488
1548
|
}
|
|
1549
|
+
else if (name === "push-option" && value === "adopt") {
|
|
1550
|
+
/* `git push -o adopt`: see the adopting note in doPush. */
|
|
1551
|
+
adopt = true;
|
|
1552
|
+
send("ok");
|
|
1553
|
+
}
|
|
1489
1554
|
else {
|
|
1490
1555
|
send("unsupported");
|
|
1491
1556
|
}
|
|
@@ -1633,7 +1698,7 @@ async function main(argv) {
|
|
|
1633
1698
|
send("");
|
|
1634
1699
|
continue;
|
|
1635
1700
|
}
|
|
1636
|
-
await doPush(batch, url);
|
|
1701
|
+
await doPush(batch, url, { adopt });
|
|
1637
1702
|
}
|
|
1638
1703
|
continue;
|
|
1639
1704
|
}
|
|
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
14
14
|
*/
|
|
15
15
|
const node_process_1 = __importDefault(require("node:process"));
|
|
16
16
|
const git_remote_js_1 = require("./git_remote.js");
|
|
17
|
+
const node_check_js_1 = require("./node_check.js");
|
|
17
18
|
/**
|
|
18
19
|
* Finish, without cutting the pipe to git mid-sentence.
|
|
19
20
|
*
|
|
@@ -36,7 +37,8 @@ function finish(code) {
|
|
|
36
37
|
node_process_1.default.stdin.pause();
|
|
37
38
|
setTimeout(() => node_process_1.default.exit(code), 5000).unref();
|
|
38
39
|
}
|
|
39
|
-
(0,
|
|
40
|
+
const old = (0, node_check_js_1.nodeTooOld)();
|
|
41
|
+
(old ? Promise.reject(new Error(old)) : (0, git_remote_js_1.main)(node_process_1.default.argv.slice(2)))
|
|
40
42
|
.then(finish)
|
|
41
43
|
.catch((error) => {
|
|
42
44
|
/*
|
|
@@ -32,6 +32,7 @@ const repository_js_1 = require("../../cbx/src/local/repository.js");
|
|
|
32
32
|
const remote_js_1 = require("../../cbx/src/local/remote.js");
|
|
33
33
|
const api_js_1 = require("./api.js");
|
|
34
34
|
const config_js_1 = require("./config.js");
|
|
35
|
+
const project_choice_js_1 = require("./project_choice.js");
|
|
35
36
|
const project_commands_js_1 = require("./project_commands.js");
|
|
36
37
|
const publish_js_1 = require("./publish.js");
|
|
37
38
|
const ESC = String.fromCharCode(27);
|
|
@@ -363,11 +364,31 @@ async function recordLink(repository, line, repositoryId, head, manifest) {
|
|
|
363
364
|
async function localPush(parsed, repository) {
|
|
364
365
|
return reporting(async () => {
|
|
365
366
|
const link = await (0, config_js_1.readLink)(repository.root);
|
|
367
|
+
/*
|
|
368
|
+
A history that has never been pushed, in a folder linked to nothing,
|
|
369
|
+
is asked which project it belongs to, as `cbx submit` asks: naming it
|
|
370
|
+
after the folder joined any project that shared the name.
|
|
371
|
+
*/
|
|
372
|
+
let chosen = null;
|
|
373
|
+
if (!link && !(await (0, remote_js_1.readRemote)(repository)).repositoryId) {
|
|
374
|
+
const into = parsed.flags.get("into");
|
|
375
|
+
const named = parsed.flags.get("name");
|
|
376
|
+
chosen = await (0, project_choice_js_1.chooseProject)({
|
|
377
|
+
folder: repository.root,
|
|
378
|
+
name: typeof named === "string" ? named : node_path_1.default.basename(repository.root),
|
|
379
|
+
...(typeof into === "string" ? { into } : {}),
|
|
380
|
+
startNew: has(parsed, "new"),
|
|
381
|
+
command: "cbx push",
|
|
382
|
+
});
|
|
383
|
+
if (!chosen)
|
|
384
|
+
return 1;
|
|
385
|
+
}
|
|
366
386
|
const interactive = node_process_1.default.stdout.isTTY;
|
|
367
387
|
const result = await (0, remote_js_1.push)(repository, {
|
|
368
388
|
credentials: config_js_1.credentials,
|
|
369
|
-
projectName: link?.slug ?? node_path_1.default.basename(repository.root),
|
|
370
|
-
repositoryId: link?.repositoryId ?? null,
|
|
389
|
+
projectName: link?.slug ?? (chosen ? ("id" in chosen ? chosen.slug : chosen.name) : node_path_1.default.basename(repository.root)),
|
|
390
|
+
repositoryId: link?.repositoryId ?? (chosen && "id" in chosen ? chosen.id : null),
|
|
391
|
+
joinExisting: false,
|
|
371
392
|
allowSecrets: has(parsed, "allow-secrets"),
|
|
372
393
|
acknowledged: has(parsed, "yes", "y"),
|
|
373
394
|
report: (event) => {
|