@coderook/cli 0.24.0 → 0.25.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/README.md +36 -0
- package/dist/cli/src/api.js +14 -0
- package/dist/cli/src/cli.js +126 -5
- package/dist/cli/src/forge_url.js +68 -0
- package/dist/cli/src/git_remote.js +114 -15
- package/dist/cli/src/transfer_command.js +331 -0
- package/package.json +4 -3
|
@@ -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.25.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/README.md
CHANGED
|
@@ -126,6 +126,42 @@ and the desktop application all read one file. A `!` line puts something back.
|
|
|
126
126
|
Personal exclusions that should not reach a collaborator belong in
|
|
127
127
|
`.git/info/exclude`.
|
|
128
128
|
|
|
129
|
+
## Moving a repository here
|
|
130
|
+
|
|
131
|
+
`cbx import` takes a snapshot — the files as they are now, one version, no
|
|
132
|
+
history. To move off a host entirely, with the history intact:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
cbx transfer https://github.com/owner/project
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
GitHub, GitLab, Codeberg and any self-hosted Gitea or Forgejo. Every commit
|
|
139
|
+
becomes a version and every branch a line, so it takes a while on a long
|
|
140
|
+
history — the commit count and an estimate are printed before it starts.
|
|
141
|
+
|
|
142
|
+
**It goes through the public route on purpose.** The history is published by
|
|
143
|
+
running `git push` against the CodeRook remote, exactly as you would by hand.
|
|
144
|
+
A transfer that used a private path would prove only that a private path
|
|
145
|
+
exists.
|
|
146
|
+
|
|
147
|
+
| | |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| Comes across | commits, every branch, tags as releases, the description |
|
|
150
|
+
| With `--issues` and a read token | the open issues |
|
|
151
|
+
| Does not come across | pull requests and reviews, CI configuration, collaborators, stars |
|
|
152
|
+
|
|
153
|
+
The project is created **private** whatever the source was — publishing
|
|
154
|
+
somebody's code as a side effect of moving it is not recoverable. Pass
|
|
155
|
+
`--visibility same` to mirror the source, or `--public`.
|
|
156
|
+
|
|
157
|
+
A private repository needs a read token in the environment — `GITHUB_TOKEN`,
|
|
158
|
+
`GITLAB_TOKEN` or `FORGE_TOKEN`. It is never taken as a flag, never stored,
|
|
159
|
+
and there is no OAuth flow: holding your forge credentials to run a one-off
|
|
160
|
+
migration sits badly beside a promise that your work is not handed to anybody.
|
|
161
|
+
|
|
162
|
+
The report at the end lists what arrived and what did not, rather than leaving
|
|
163
|
+
the absence to be discovered later.
|
|
164
|
+
|
|
129
165
|
## Using git instead
|
|
130
166
|
|
|
131
167
|
Installing the CLI also installs `git-remote-coderook`, so git talks to
|
package/dist/cli/src/api.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.applyMerge = applyMerge;
|
|
|
11
11
|
exports.cancelMerge = cancelMerge;
|
|
12
12
|
exports.aiAccess = aiAccess;
|
|
13
13
|
exports.setAiAccess = setAiAccess;
|
|
14
|
+
exports.updateProject = updateProject;
|
|
14
15
|
exports.versions = versions;
|
|
15
16
|
exports.markRelease = markRelease;
|
|
16
17
|
exports.issues = issues;
|
|
@@ -68,6 +69,7 @@ async function projects() {
|
|
|
68
69
|
slug: String(row.slug ?? ""),
|
|
69
70
|
name: String(row.displayName || row.slug || "Untitled"),
|
|
70
71
|
visibility: String(row.visibility ?? "private"),
|
|
72
|
+
defaultBranch: String(row.defaultBranch || "main"),
|
|
71
73
|
versionCount: Number(row.versionCount ?? 0),
|
|
72
74
|
fileCount: Number(row.fileCount ?? 0),
|
|
73
75
|
storedBytes: Number(row.storedSize ?? 0),
|
|
@@ -149,6 +151,18 @@ async function setAiAccess(repositoryId, change) {
|
|
|
149
151
|
body: change,
|
|
150
152
|
});
|
|
151
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Change what a project says about itself.
|
|
156
|
+
*
|
|
157
|
+
* The same PATCH the AI switches use, which is where these live: a handful of
|
|
158
|
+
* columns on the repository rather than a resource of their own.
|
|
159
|
+
*/
|
|
160
|
+
async function updateProject(repositoryId, change) {
|
|
161
|
+
await call(`/v1/repositories/${encodeURIComponent(repositoryId)}`, {
|
|
162
|
+
method: "PATCH",
|
|
163
|
+
body: change,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
152
166
|
/** Every saved version of a project, newest first. */
|
|
153
167
|
async function versions(repositoryId) {
|
|
154
168
|
const body = await call(`/v1/repositories/${encodeURIComponent(repositoryId)}/versions`);
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -27,6 +27,8 @@ const progress_js_1 = require("./progress.js");
|
|
|
27
27
|
const publish_js_1 = require("./publish.js");
|
|
28
28
|
const project_commands_js_1 = require("./project_commands.js");
|
|
29
29
|
const track_commands_js_1 = require("./track_commands.js");
|
|
30
|
+
const tracks_js_1 = require("../../desktop-app/src/main/tracks.js");
|
|
31
|
+
const transfer_command_js_1 = require("./transfer_command.js");
|
|
30
32
|
const mcp_js_1 = require("./mcp.js");
|
|
31
33
|
const skill_command_js_1 = require("./skill_command.js");
|
|
32
34
|
const licence_commands_js_1 = require("./licence_commands.js");
|
|
@@ -884,7 +886,7 @@ async function commandGet(parsed) {
|
|
|
884
886
|
incoming.has(file.path) &&
|
|
885
887
|
edited.some((one) => one.path === file.path));
|
|
886
888
|
if (!changedInTheGap.length) {
|
|
887
|
-
return fetchInto(link.repositoryId, folder, link.slug, link.local ?? null, "replace");
|
|
889
|
+
return fetchInto(link.repositoryId, folder, link.slug, link.local ?? null, "replace", await (0, track_commands_js_1.trackFor)(folder));
|
|
888
890
|
}
|
|
889
891
|
console.error(red(`${changedInTheGap.length} file${changedInTheGap.length === 1 ? "" : "s"} ` +
|
|
890
892
|
`changed while the interrupted fetch was stopped:`));
|
|
@@ -923,7 +925,9 @@ Save them with ${accent("cbx submit")}, or finish the fetch and ` +
|
|
|
923
925
|
what the version actually changed is written over — unless somebody asks
|
|
924
926
|
for an exact copy, which is how a damaged folder is repaired.
|
|
925
927
|
*/
|
|
926
|
-
return fetchInto(link.repositoryId, folder, link.slug, link.local ?? null, replacing ? "replace" : "reconcile"
|
|
928
|
+
return fetchInto(link.repositoryId, folder, link.slug, link.local ?? null, replacing ? "replace" : "reconcile",
|
|
929
|
+
// The line this folder saves to, so catching up brings that line down.
|
|
930
|
+
await (0, track_commands_js_1.trackFor)(folder));
|
|
927
931
|
}
|
|
928
932
|
async function commandClone(parsed) {
|
|
929
933
|
const reference = parsed.positional[0];
|
|
@@ -941,7 +945,34 @@ async function commandClone(parsed) {
|
|
|
941
945
|
return 1;
|
|
942
946
|
}
|
|
943
947
|
const destination = node_path_1.default.resolve(parsed.positional[1] ?? project.slug);
|
|
944
|
-
|
|
948
|
+
/*
|
|
949
|
+
A clone opens on the project's default line, or on the one named.
|
|
950
|
+
|
|
951
|
+
It used to take whichever version was newest across every line, which is
|
|
952
|
+
only the same thing while a project has one. As soon as somebody pushed to
|
|
953
|
+
a branch, that branch held the newest version — so cloning the project
|
|
954
|
+
handed you their unfinished work under the impression it was the project.
|
|
955
|
+
Nothing said so: the fetch reported a version number and the files looked
|
|
956
|
+
like a project, just not the one anybody asked for.
|
|
957
|
+
|
|
958
|
+
Naming a line here also removes the clone-switch-fetch dance that was the
|
|
959
|
+
only way to get a branch before, which is what exposed the same fault in
|
|
960
|
+
`get`.
|
|
961
|
+
*/
|
|
962
|
+
const wanted = flagText(parsed, "track") ?? project.defaultBranch;
|
|
963
|
+
/*
|
|
964
|
+
Falling back rather than failing when the default line does not exist.
|
|
965
|
+
`defaultBranch` is a property of the project and a line can be renamed out
|
|
966
|
+
from under it; refusing to clone at all in that case would be a worse
|
|
967
|
+
answer than the old behaviour. An explicitly named line is still refused,
|
|
968
|
+
because there the person said which one they meant.
|
|
969
|
+
*/
|
|
970
|
+
const lines = await new tracks_js_1.Tracks(config_js_1.credentials).list(project.id);
|
|
971
|
+
const usable = lines.some((line) => line.kind === "line" && line.name === wanted && line.headVersionId);
|
|
972
|
+
if (!usable && !flagText(parsed, "track")) {
|
|
973
|
+
console.log(dim(`This project has no line called ${wanted}; taking its newest version.`));
|
|
974
|
+
}
|
|
975
|
+
return fetchInto(project.id, destination, project.slug, null, "replace", usable || flagText(parsed, "track") ? wanted : undefined);
|
|
945
976
|
}
|
|
946
977
|
async function fetchInto(repositoryId, destination, slug,
|
|
947
978
|
/*
|
|
@@ -950,9 +981,45 @@ async function fetchInto(repositoryId, destination, slug,
|
|
|
950
981
|
is removed, because "missing from the version" cannot then be told from
|
|
951
982
|
"never came from the version at all".
|
|
952
983
|
*/
|
|
953
|
-
held, mode = "replace"
|
|
984
|
+
held, mode = "replace",
|
|
985
|
+
/*
|
|
986
|
+
The line to fetch. Omitted means the project's newest version, which is
|
|
987
|
+
right for a clone and wrong for a folder that has chosen a line.
|
|
988
|
+
*/
|
|
989
|
+
trackName) {
|
|
954
990
|
const downloader = new download_js_1.Downloader(config_js_1.credentials);
|
|
955
|
-
const
|
|
991
|
+
const all = await downloader.versions(repositoryId);
|
|
992
|
+
/*
|
|
993
|
+
The head of this folder's line, not the newest version in the project.
|
|
994
|
+
|
|
995
|
+
Those are the same thing only while a project has one line. With two, the
|
|
996
|
+
newest version usually belongs to the *other* one — so switching to a line
|
|
997
|
+
and fetching brought down the line you had just left, reported success,
|
|
998
|
+
and left the folder holding the wrong contents while still claiming to
|
|
999
|
+
save to the line you asked for. The next publish would then be computed
|
|
1000
|
+
against a baseline from somewhere else.
|
|
1001
|
+
|
|
1002
|
+
Worse than a wrong answer: `track` ends by telling you to run `get`, so
|
|
1003
|
+
the command that set this up was the one recommending it.
|
|
1004
|
+
*/
|
|
1005
|
+
let latest = all[0];
|
|
1006
|
+
if (trackName) {
|
|
1007
|
+
const chosen = (await new tracks_js_1.Tracks(config_js_1.credentials).list(repositoryId)).find((candidate) => candidate.name === trackName && candidate.kind === "line");
|
|
1008
|
+
if (!chosen) {
|
|
1009
|
+
console.error(red(`This project has no line called ${trackName}.`));
|
|
1010
|
+
return 1;
|
|
1011
|
+
}
|
|
1012
|
+
if (!chosen.headVersionId) {
|
|
1013
|
+
console.error(red(`The line ${trackName} has nothing saved to it yet.`));
|
|
1014
|
+
return 1;
|
|
1015
|
+
}
|
|
1016
|
+
const head = all.find((version) => version.id === chosen.headVersionId);
|
|
1017
|
+
if (!head) {
|
|
1018
|
+
console.error(red(`Could not read the current version of ${trackName}.`));
|
|
1019
|
+
return 1;
|
|
1020
|
+
}
|
|
1021
|
+
latest = head;
|
|
1022
|
+
}
|
|
956
1023
|
if (!latest) {
|
|
957
1024
|
console.error(red("That project has no saved version."));
|
|
958
1025
|
return 1;
|
|
@@ -991,6 +1058,16 @@ held, mode = "replace") {
|
|
|
991
1058
|
// version holds and the two agree again.
|
|
992
1059
|
local: result.manifest,
|
|
993
1060
|
manifest: result.manifest,
|
|
1061
|
+
/*
|
|
1062
|
+
The line this folder is on survives being caught up.
|
|
1063
|
+
|
|
1064
|
+
This record is built fresh rather than merged, so anything not named
|
|
1065
|
+
here is dropped — and `track` was not named. Fetching therefore moved
|
|
1066
|
+
the folder back to `main` every time, silently: the files were right,
|
|
1067
|
+
the folder reported the wrong line, and the next publish went somewhere
|
|
1068
|
+
nobody chose.
|
|
1069
|
+
*/
|
|
1070
|
+
...(trackName ? { track: trackName } : {}),
|
|
994
1071
|
// Reached the end, so there is no longer a fetch in flight.
|
|
995
1072
|
});
|
|
996
1073
|
// The record is written; the run has not reported success yet.
|
|
@@ -1500,6 +1577,43 @@ const SPECS = [
|
|
|
1500
1577
|
],
|
|
1501
1578
|
run: commandImport,
|
|
1502
1579
|
},
|
|
1580
|
+
{
|
|
1581
|
+
/*
|
|
1582
|
+
Distinct from `import`, which takes a snapshot. This moves the history,
|
|
1583
|
+
the branches, the tags and what the project says about itself — the
|
|
1584
|
+
things somebody leaving a host is actually worried about losing.
|
|
1585
|
+
*/
|
|
1586
|
+
name: "transfer",
|
|
1587
|
+
aliases: ["migrate"],
|
|
1588
|
+
group: "Getting started",
|
|
1589
|
+
summary: "move a whole repository here from GitHub, GitLab or Codeberg",
|
|
1590
|
+
usage: "transfer <address>",
|
|
1591
|
+
detail: "Brings the commits, every branch, the tags and the description across.\n" +
|
|
1592
|
+
"Each commit becomes a version and each branch a line, so a long history\n" +
|
|
1593
|
+
"takes a while; the count and an estimate are printed before it starts.\n\n" +
|
|
1594
|
+
"The history is published by running `git push` against the CodeRook\n" +
|
|
1595
|
+
"remote — the same route anybody else uses, deliberately, because the\n" +
|
|
1596
|
+
"claim being made is that git repositories work here.\n\n" +
|
|
1597
|
+
"A private repository needs a read token in the environment:\n" +
|
|
1598
|
+
"GITHUB_TOKEN, GITLAB_TOKEN or FORGE_TOKEN. It is never asked for on the\n" +
|
|
1599
|
+
"command line and never stored.\n\n" +
|
|
1600
|
+
"The project is created private whatever the source was, unless you say\n" +
|
|
1601
|
+
"otherwise. Pull requests, CI configuration and collaborators do not come\n" +
|
|
1602
|
+
"across; the report at the end lists what did and what did not.",
|
|
1603
|
+
options: [
|
|
1604
|
+
{ flags: "--name <name>", description: "call the project this, rather than the repository's name" },
|
|
1605
|
+
{
|
|
1606
|
+
flags: "--visibility <how>",
|
|
1607
|
+
description: "private (default), same as the source, or public",
|
|
1608
|
+
},
|
|
1609
|
+
{ flags: "--issues", description: "also bring the open issues; needs a read token" },
|
|
1610
|
+
],
|
|
1611
|
+
examples: [
|
|
1612
|
+
"cbx transfer https://github.com/owner/project",
|
|
1613
|
+
"cbx transfer https://codeberg.org/owner/project --visibility same",
|
|
1614
|
+
],
|
|
1615
|
+
run: transfer_command_js_1.commandTransfer,
|
|
1616
|
+
},
|
|
1503
1617
|
{
|
|
1504
1618
|
/*
|
|
1505
1619
|
Where the next save goes, which is a property of this folder rather
|
|
@@ -1555,6 +1669,13 @@ const SPECS = [
|
|
|
1555
1669
|
group: "Working with a folder",
|
|
1556
1670
|
summary: "fetch a project into a new folder",
|
|
1557
1671
|
usage: "clone <project> [dir]",
|
|
1672
|
+
options: [
|
|
1673
|
+
{
|
|
1674
|
+
flags: "--track <name>",
|
|
1675
|
+
description: "fetch this line rather than the project's newest version",
|
|
1676
|
+
},
|
|
1677
|
+
],
|
|
1678
|
+
examples: ["cbx clone my-project", "cbx clone my-project --track spike"],
|
|
1558
1679
|
run: commandClone,
|
|
1559
1680
|
},
|
|
1560
1681
|
{
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reading a repository address, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from the transfer itself so a test can exercise it directly:
|
|
6
|
+
* importing the command drags in the API client and the upload engine, and
|
|
7
|
+
* none of that is needed to decide that `gitlab.com/group/subgroup/project`
|
|
8
|
+
* belongs to `group/subgroup`. It is also the piece where a mistake is quiet —
|
|
9
|
+
* a URL parsed into the wrong owner still clones and still pushes, and only
|
|
10
|
+
* the project name afterwards says anything was wrong.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.readForgeUrl = readForgeUrl;
|
|
14
|
+
/**
|
|
15
|
+
* Work out which forge a URL points at.
|
|
16
|
+
*
|
|
17
|
+
* Gitea is the fallback rather than a failure: Codeberg, Forgejo and every
|
|
18
|
+
* self-hosted Gitea share one API shape, so an unknown host is far more likely
|
|
19
|
+
* to be one of those than to be nothing. Guessing wrong costs a failed
|
|
20
|
+
* metadata read, which is reported; refusing outright would cost the transfer.
|
|
21
|
+
*/
|
|
22
|
+
function readForgeUrl(input) {
|
|
23
|
+
let raw = input.trim();
|
|
24
|
+
// `git@host:owner/repo.git` is not a URL; make it one.
|
|
25
|
+
const ssh = raw.match(/^git@([^:]+):(.+)$/);
|
|
26
|
+
if (ssh)
|
|
27
|
+
raw = `https://${ssh[1]}/${ssh[2]}`;
|
|
28
|
+
if (!/^[a-z]+:\/\//i.test(raw))
|
|
29
|
+
raw = `https://${raw}`;
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = new URL(raw);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error(`That does not look like a repository address: ${input}`);
|
|
36
|
+
}
|
|
37
|
+
const parts = parsed.pathname.replace(/\.git$/i, "").split("/").filter(Boolean);
|
|
38
|
+
if (parts.length < 2) {
|
|
39
|
+
throw new Error(`That address names no repository: ${input}\n` +
|
|
40
|
+
` Expected something like https://github.com/owner/project`);
|
|
41
|
+
}
|
|
42
|
+
/*
|
|
43
|
+
GitLab allows groups within groups, so the project is the last segment and
|
|
44
|
+
the owner is everything before it. Taking parts[0] would address the top
|
|
45
|
+
group and miss the project entirely.
|
|
46
|
+
*/
|
|
47
|
+
const repo = parts[parts.length - 1];
|
|
48
|
+
const owner = parts.slice(0, -1).join("/");
|
|
49
|
+
const host = parsed.host;
|
|
50
|
+
const kind = /(^|\.)github\.com$/i.test(host)
|
|
51
|
+
? "github"
|
|
52
|
+
: /(^|\.)gitlab\.com$/i.test(host)
|
|
53
|
+
? "gitlab"
|
|
54
|
+
: "gitea";
|
|
55
|
+
return {
|
|
56
|
+
kind,
|
|
57
|
+
label: kind === "github" ? "GitHub" : kind === "gitlab" ? "GitLab" : `Gitea (${host})`,
|
|
58
|
+
host,
|
|
59
|
+
owner,
|
|
60
|
+
repo,
|
|
61
|
+
clone: `https://${host}/${owner}/${repo}.git`,
|
|
62
|
+
tokenNames: kind === "github"
|
|
63
|
+
? ["GITHUB_TOKEN", "GH_TOKEN"]
|
|
64
|
+
: kind === "gitlab"
|
|
65
|
+
? ["GITLAB_TOKEN"]
|
|
66
|
+
: ["FORGE_TOKEN", "GITEA_TOKEN"],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -253,6 +253,54 @@ async function applyCommit(catFile, scratch, fromCommit, toCommit) {
|
|
|
253
253
|
}
|
|
254
254
|
return { written, deleted, skipped };
|
|
255
255
|
}
|
|
256
|
+
/*
|
|
257
|
+
The service's answer when a line is already there.
|
|
258
|
+
|
|
259
|
+
Matched on the wording as well as the shape because the message is the only
|
|
260
|
+
thing that reaches here: `This project already has a track called main`. The
|
|
261
|
+
previous test looked for "exist", which that sentence does not contain — so a
|
|
262
|
+
branch arriving after the project had been created was reported as a hard
|
|
263
|
+
failure rather than the ordinary "it is already there".
|
|
264
|
+
*/
|
|
265
|
+
const TRACK_EXISTS = /already has a track|track_exists|exists/i;
|
|
266
|
+
/**
|
|
267
|
+
* The order refs are published in, which is not the order git sends them.
|
|
268
|
+
*
|
|
269
|
+
* `git push --all` hands over every branch at once, and on a project that does
|
|
270
|
+
* not exist yet the first one creates it — along with an empty `main`. If a
|
|
271
|
+
* side branch happens to go first it takes the whole history, and `main`
|
|
272
|
+
* arrives to find its own line already made and nothing it can do with it:
|
|
273
|
+
* there is no way to move an existing line's head, only to start one at a
|
|
274
|
+
* version. The result was a project whose default line was permanently empty.
|
|
275
|
+
*
|
|
276
|
+
* Publishing the branch the others fork from first makes every later branch an
|
|
277
|
+
* ordinary fork, which is the case that already worked. Local HEAD wins,
|
|
278
|
+
* because that is what the person is standing on; `main` and `master` are the
|
|
279
|
+
* conventional fallbacks; everything else keeps a stable order so two runs of
|
|
280
|
+
* the same push behave the same way.
|
|
281
|
+
*/
|
|
282
|
+
function ordered(requests) {
|
|
283
|
+
let head = "";
|
|
284
|
+
try {
|
|
285
|
+
head = git(["symbolic-ref", "--short", "HEAD"]).trim();
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// A detached HEAD has no branch to prefer; the fallbacks still apply.
|
|
289
|
+
}
|
|
290
|
+
const rank = (request) => {
|
|
291
|
+
const branch = (0, git_history_js_1.branchOf)(request.dst);
|
|
292
|
+
if (!branch)
|
|
293
|
+
return 4; // tags last: they name versions that must exist
|
|
294
|
+
if (branch === head)
|
|
295
|
+
return 0;
|
|
296
|
+
if (branch === "main")
|
|
297
|
+
return 1;
|
|
298
|
+
if (branch === "master")
|
|
299
|
+
return 2;
|
|
300
|
+
return 3;
|
|
301
|
+
};
|
|
302
|
+
return [...requests].sort((left, right) => rank(left) - rank(right) || left.dst.localeCompare(right.dst));
|
|
303
|
+
}
|
|
256
304
|
/**
|
|
257
305
|
* The refs the remote already holds, as far as we can tell.
|
|
258
306
|
*
|
|
@@ -612,18 +660,44 @@ async function doImport(refs, url) {
|
|
|
612
660
|
}
|
|
613
661
|
async function doPush(requests, url) {
|
|
614
662
|
const { slug } = (0, git_history_js_1.parseRemoteUrl)(url);
|
|
615
|
-
const project = await (0, api_js_1.findProject)(slug);
|
|
616
|
-
const repositoryId = project?.id ?? null;
|
|
617
|
-
const state = repositoryId ? await remoteState(repositoryId) : null;
|
|
618
663
|
const marks = await readMarks();
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
664
|
+
/*
|
|
665
|
+
Re-read the project before every ref, not once before the batch.
|
|
666
|
+
|
|
667
|
+
`git push --all` sends every branch in one batch, and the first of them
|
|
668
|
+
routinely creates the project. Reading the state once meant every later
|
|
669
|
+
ref in that same push still believed the project did not exist: it
|
|
670
|
+
published against a null repository, started its history from nothing, and
|
|
671
|
+
left its line without a head. Pushing two branches to a new project
|
|
672
|
+
produced one working line and one empty one, and reported success for
|
|
673
|
+
both — which is the shape a whole-repository transfer takes, so it failed
|
|
674
|
+
exactly where it mattered most.
|
|
675
|
+
|
|
676
|
+
One extra listing per ref. A push carries a handful of refs and each one
|
|
677
|
+
publishes versions costing seconds, so the cost is not worth the class of
|
|
678
|
+
bug avoiding it creates.
|
|
679
|
+
*/
|
|
680
|
+
let repositoryId = (await (0, api_js_1.findProject)(slug))?.id ?? null;
|
|
681
|
+
let state = repositoryId
|
|
682
|
+
? await remoteState(repositoryId)
|
|
683
|
+
: null;
|
|
684
|
+
const refreshProject = async () => {
|
|
685
|
+
if (!repositoryId)
|
|
686
|
+
repositoryId = (await (0, api_js_1.findProject)(slug))?.id ?? null;
|
|
687
|
+
state = repositoryId ? await remoteState(repositoryId) : null;
|
|
688
|
+
};
|
|
689
|
+
const knownRefs = () => {
|
|
690
|
+
const found = new Map();
|
|
691
|
+
for (const track of state?.tracks ?? []) {
|
|
692
|
+
if (track.kind !== "line" || !track.headVersionId)
|
|
693
|
+
continue;
|
|
694
|
+
const sha = headCommit(track.headVersionId, state, marks);
|
|
695
|
+
if (sha)
|
|
696
|
+
found.set(`refs/heads/${track.name}`, sha);
|
|
697
|
+
}
|
|
698
|
+
return found;
|
|
699
|
+
};
|
|
700
|
+
let known = knownRefs();
|
|
627
701
|
let nextPushMark = 1;
|
|
628
702
|
for (const mark of marks?.sha.keys() ?? []) {
|
|
629
703
|
nextPushMark = Math.max(nextPushMark, mark + 1);
|
|
@@ -633,7 +707,13 @@ async function doPush(requests, url) {
|
|
|
633
707
|
const scratch = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "coderook-push-"));
|
|
634
708
|
let warnedAboutMerges = false;
|
|
635
709
|
try {
|
|
636
|
-
for (const request of requests) {
|
|
710
|
+
for (const request of ordered(requests)) {
|
|
711
|
+
/*
|
|
712
|
+
Whatever the previous ref did, this one starts from what the project
|
|
713
|
+
actually holds now — including a project the previous ref created.
|
|
714
|
+
*/
|
|
715
|
+
await refreshProject();
|
|
716
|
+
known = knownRefs();
|
|
637
717
|
/*
|
|
638
718
|
A tag names a version, which is exactly what a CodeRook release is:
|
|
639
719
|
"a Version with a name on it, not a different kind of object". So a tag
|
|
@@ -808,7 +888,7 @@ async function doPush(requests, url) {
|
|
|
808
888
|
}
|
|
809
889
|
catch (error) {
|
|
810
890
|
const text = error instanceof Error ? error.message : String(error);
|
|
811
|
-
if (
|
|
891
|
+
if (!TRACK_EXISTS.test(text)) {
|
|
812
892
|
send(`error ${request.dst} ${text}`);
|
|
813
893
|
continue;
|
|
814
894
|
}
|
|
@@ -871,7 +951,7 @@ async function doPush(requests, url) {
|
|
|
871
951
|
}
|
|
872
952
|
catch (error) {
|
|
873
953
|
const text = error instanceof Error ? error.message : String(error);
|
|
874
|
-
if (
|
|
954
|
+
if (!TRACK_EXISTS.test(text))
|
|
875
955
|
throw error;
|
|
876
956
|
}
|
|
877
957
|
}
|
|
@@ -1052,7 +1132,26 @@ async function main(argv) {
|
|
|
1052
1132
|
const sha = mark === undefined ? undefined : marks?.sha.get(mark);
|
|
1053
1133
|
send(`${sha ?? "?"} refs/heads/${track.name}`);
|
|
1054
1134
|
}
|
|
1055
|
-
|
|
1135
|
+
/*
|
|
1136
|
+
Which line a clone opens on.
|
|
1137
|
+
|
|
1138
|
+
The project's own default first, then the conventional names, then
|
|
1139
|
+
whatever has content. Looking only for `main` was wrong for a
|
|
1140
|
+
transferred repository: creating a project always makes an empty
|
|
1141
|
+
`main`, so a repository whose default is `master` ends up with a
|
|
1142
|
+
`main` that holds nothing — and `main` is filtered out of `lines`
|
|
1143
|
+
for exactly that reason, leaving HEAD to fall on whichever branch
|
|
1144
|
+
happened to sort first. It landed on `master` by luck rather than
|
|
1145
|
+
because anything chose it.
|
|
1146
|
+
*/
|
|
1147
|
+
const preferred = [
|
|
1148
|
+
project?.defaultBranch,
|
|
1149
|
+
"main",
|
|
1150
|
+
"master",
|
|
1151
|
+
].filter((name) => Boolean(name));
|
|
1152
|
+
const head = preferred
|
|
1153
|
+
.map((name) => lines.find((track) => track.name === name))
|
|
1154
|
+
.find(Boolean) ?? lines[0];
|
|
1056
1155
|
if (head)
|
|
1057
1156
|
send(`@refs/heads/${head.name} HEAD`);
|
|
1058
1157
|
}
|
|
@@ -0,0 +1,331 @@
|
|
|
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.commandTransfer = commandTransfer;
|
|
7
|
+
/**
|
|
8
|
+
* Moving a whole repository off a git host and onto CodeRook.
|
|
9
|
+
*
|
|
10
|
+
* `cbx import` takes a snapshot: the files as they are now, one version, no
|
|
11
|
+
* history. That is the right answer for "get this onto CodeRook quickly" and
|
|
12
|
+
* the wrong one for "move off GitHub", where the history, the branches, the
|
|
13
|
+
* tags and the issues are most of what somebody is worried about losing.
|
|
14
|
+
*
|
|
15
|
+
* ## It uses the public route on purpose
|
|
16
|
+
*
|
|
17
|
+
* The history goes across by running `git push` against the CodeRook remote —
|
|
18
|
+
* the same command anybody else would type, through the same helper. Nothing
|
|
19
|
+
* here reaches past it into a private path.
|
|
20
|
+
*
|
|
21
|
+
* That is deliberate, because the claim being made is interoperability. A
|
|
22
|
+
* transfer that worked through a back door would prove only that a back door
|
|
23
|
+
* exists. This one works because `git push coderook --all` works; if that ever
|
|
24
|
+
* breaks, this breaks with it, loudly, rather than quietly diverging from what
|
|
25
|
+
* users are told to do.
|
|
26
|
+
*
|
|
27
|
+
* ## What crosses, and what cannot
|
|
28
|
+
*
|
|
29
|
+
* Commits, branches, tags and the project's own description come across.
|
|
30
|
+
* Issues come across when a read token is supplied. Pull requests, reviews,
|
|
31
|
+
* CI configuration, stars and collaborator lists do not — some because
|
|
32
|
+
* CodeRook has no such object, and one because it would be wrong: adding
|
|
33
|
+
* somebody to a project is an invitation they have to accept, not a field to
|
|
34
|
+
* copy. The report at the end says which is which rather than leaving the
|
|
35
|
+
* absence to be discovered.
|
|
36
|
+
*
|
|
37
|
+
* ## Tokens
|
|
38
|
+
*
|
|
39
|
+
* Read from the environment, never from a flag and never stored. A read-only
|
|
40
|
+
* token is enough, and no OAuth flow is offered: holding somebody's forge
|
|
41
|
+
* credentials to run a one-off migration sits badly beside a front page that
|
|
42
|
+
* promises their work is not handed to anybody.
|
|
43
|
+
*/
|
|
44
|
+
const node_child_process_1 = require("node:child_process");
|
|
45
|
+
const promises_1 = require("node:fs/promises");
|
|
46
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
47
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
48
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
49
|
+
const api_js_1 = require("./api.js");
|
|
50
|
+
const forge_url_js_1 = require("./forge_url.js");
|
|
51
|
+
const dim = (value) => `[2m${value}[0m`;
|
|
52
|
+
const bold = (value) => `[1m${value}[0m`;
|
|
53
|
+
const red = (value) => `[31m${value}[0m`;
|
|
54
|
+
const green = (value) => `[32m${value}[0m`;
|
|
55
|
+
const accent = (value) => `[33m${value}[0m`;
|
|
56
|
+
/** A read token from the environment, or nothing. Never a flag. */
|
|
57
|
+
function forgeToken(forge) {
|
|
58
|
+
for (const name of forge.tokenNames) {
|
|
59
|
+
const value = node_process_1.default.env[name]?.trim();
|
|
60
|
+
if (value)
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
async function readJson(url, token, forge) {
|
|
66
|
+
const headers = {
|
|
67
|
+
accept: "application/json",
|
|
68
|
+
"user-agent": "cbx-transfer",
|
|
69
|
+
};
|
|
70
|
+
if (token) {
|
|
71
|
+
headers.authorization =
|
|
72
|
+
forge.kind === "gitlab" ? `Bearer ${token}` : `token ${token}`;
|
|
73
|
+
}
|
|
74
|
+
const response = await fetch(url, { headers });
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`${response.status} from ${new URL(url).pathname}`);
|
|
77
|
+
}
|
|
78
|
+
return response.json();
|
|
79
|
+
}
|
|
80
|
+
/** What the forge says about the repository itself. */
|
|
81
|
+
async function describeSource(forge, token) {
|
|
82
|
+
const encoded = encodeURIComponent(`${forge.owner}/${forge.repo}`);
|
|
83
|
+
const url = forge.kind === "github"
|
|
84
|
+
? `https://api.${forge.host}/repos/${forge.owner}/${forge.repo}`
|
|
85
|
+
: forge.kind === "gitlab"
|
|
86
|
+
? `https://${forge.host}/api/v4/projects/${encoded}`
|
|
87
|
+
: `https://${forge.host}/api/v1/repos/${forge.owner}/${forge.repo}`;
|
|
88
|
+
try {
|
|
89
|
+
const body = (await readJson(url, token, forge));
|
|
90
|
+
const isPrivate = body.private === true || body.visibility === "private" || body.internal === true;
|
|
91
|
+
return {
|
|
92
|
+
description: String(body.description ?? "").slice(0, 1000),
|
|
93
|
+
visibility: isPrivate ? "private" : "public",
|
|
94
|
+
defaultBranch: body.default_branch ?? null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Open issues on the source, oldest first so numbering reads sensibly. */
|
|
102
|
+
async function readIssues(forge, token) {
|
|
103
|
+
const encoded = encodeURIComponent(`${forge.owner}/${forge.repo}`);
|
|
104
|
+
const url = forge.kind === "github"
|
|
105
|
+
? `https://api.${forge.host}/repos/${forge.owner}/${forge.repo}/issues?state=open&per_page=100`
|
|
106
|
+
: forge.kind === "gitlab"
|
|
107
|
+
? `https://${forge.host}/api/v4/projects/${encoded}/issues?state=opened&per_page=100`
|
|
108
|
+
: `https://${forge.host}/api/v1/repos/${forge.owner}/${forge.repo}/issues?state=open&limit=100`;
|
|
109
|
+
const body = (await readJson(url, token, forge));
|
|
110
|
+
return body
|
|
111
|
+
/*
|
|
112
|
+
GitHub returns pull requests through the issues endpoint. They are not
|
|
113
|
+
issues, and CodeRook has nothing that a pull request becomes, so copying
|
|
114
|
+
them across as issues would be inventing content rather than moving it.
|
|
115
|
+
*/
|
|
116
|
+
.filter((one) => !one.pull_request)
|
|
117
|
+
.map((one) => ({
|
|
118
|
+
title: String(one.title ?? "").slice(0, 200),
|
|
119
|
+
body: String(one.body ?? one.description ?? "").slice(0, 20_000),
|
|
120
|
+
number: Number(one.number ?? one.iid ?? 0),
|
|
121
|
+
closed: false,
|
|
122
|
+
}))
|
|
123
|
+
.filter((one) => one.title)
|
|
124
|
+
.reverse();
|
|
125
|
+
}
|
|
126
|
+
function run(command, args, cwd) {
|
|
127
|
+
const result = (0, node_child_process_1.spawnSync)(command, args, {
|
|
128
|
+
cwd,
|
|
129
|
+
encoding: "utf8",
|
|
130
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
ok: result.status === 0,
|
|
134
|
+
out: `${result.stdout ?? ""}${result.stderr ?? ""}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function commandTransfer(parsed) {
|
|
138
|
+
const address = parsed.positional[0];
|
|
139
|
+
if (!address) {
|
|
140
|
+
console.error(red("Which repository? Try: cbx transfer https://github.com/owner/project"));
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
let forge;
|
|
144
|
+
try {
|
|
145
|
+
forge = (0, forge_url_js_1.readForgeUrl)(address);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
if (!run("git", ["--version"]).ok) {
|
|
152
|
+
console.error(red("Transferring needs git on this machine."));
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
/*
|
|
156
|
+
The push goes through the remote helper, which git finds on PATH by name.
|
|
157
|
+
Checking now turns a confusing failure halfway through a clone into one
|
|
158
|
+
sentence before anything is downloaded.
|
|
159
|
+
*/
|
|
160
|
+
if (!run("git", ["remote-coderook", "--probe"]).ok) {
|
|
161
|
+
const help = run("git", ["help", "-a"]);
|
|
162
|
+
if (!/remote-coderook/.test(help.out)) {
|
|
163
|
+
console.error(red("git cannot find `git-remote-coderook` on this machine."));
|
|
164
|
+
console.error("It ships with this tool, so this usually means cbx was run from a\n" +
|
|
165
|
+
"checkout rather than installed. Install it globally and try again:\n" +
|
|
166
|
+
` ${accent("npm install --global @coderook/cli")}`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const wanted = (typeof parsed.flags.get("name") === "string"
|
|
171
|
+
? String(parsed.flags.get("name"))
|
|
172
|
+
: forge.repo).toLowerCase();
|
|
173
|
+
const existing = await (0, api_js_1.findProject)(wanted);
|
|
174
|
+
if (existing) {
|
|
175
|
+
console.error(red(`You already have a project called ${existing.slug}.`));
|
|
176
|
+
console.error(`Transfer creates a project; it does not merge into one. Choose another\n` +
|
|
177
|
+
`name with ${accent("--name")}, or delete that project first.`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
const token = forgeToken(forge);
|
|
181
|
+
console.log(`${bold("Transferring")} ${forge.owner}/${forge.repo} ${dim(`from ${forge.label}`)}`);
|
|
182
|
+
console.log(dim(token
|
|
183
|
+
? ` Using the read token in ${forge.tokenNames.find((name) => node_process_1.default.env[name]?.trim())}.`
|
|
184
|
+
: ` No token set. Public repositories work; set ${forge.tokenNames[0]} for a private one or for issues.`));
|
|
185
|
+
const scratch = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "cbx-transfer-"));
|
|
186
|
+
const checkout = node_path_1.default.join(scratch, forge.repo);
|
|
187
|
+
const report = [];
|
|
188
|
+
try {
|
|
189
|
+
/*
|
|
190
|
+
A full clone, not the shallow one `import` uses. The whole point here is
|
|
191
|
+
the history, and `--depth 1` would silently deliver a single commit under
|
|
192
|
+
a command whose name promises everything.
|
|
193
|
+
*/
|
|
194
|
+
console.log(`\n${dim("Fetching the repository with its full history…")}`);
|
|
195
|
+
const cloned = run("git", ["clone", "--no-single-branch", forge.clone, checkout]);
|
|
196
|
+
if (!cloned.ok) {
|
|
197
|
+
console.error(red(`\nCould not clone ${forge.clone}`));
|
|
198
|
+
console.error(cloned.out.trim().split("\n").slice(-4).join("\n"));
|
|
199
|
+
if (!token) {
|
|
200
|
+
console.error(dim(`\nIf it is private, set ${forge.tokenNames[0]} and try again.`));
|
|
201
|
+
}
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
/*
|
|
205
|
+
Every branch, not only the one checked out. A fresh clone has remote
|
|
206
|
+
tracking refs for the rest, and nothing local pointing at them, so
|
|
207
|
+
`--all` would push exactly one branch.
|
|
208
|
+
*/
|
|
209
|
+
const remoteBranches = run("git", [
|
|
210
|
+
"for-each-ref", "--format=%(refname:short)", "refs/remotes/origin",
|
|
211
|
+
], checkout).out
|
|
212
|
+
.split("\n").map((one) => one.trim()).filter(Boolean)
|
|
213
|
+
/*
|
|
214
|
+
The remote's HEAD symref comes back as plain `origin`, not
|
|
215
|
+
`origin/HEAD`, so filtering on the latter leaves a phantom entry that
|
|
216
|
+
inflates the branch count and sends git looking for `origin/origin`.
|
|
217
|
+
*/
|
|
218
|
+
.filter((one) => one.startsWith("origin/"))
|
|
219
|
+
.map((one) => one.slice("origin/".length))
|
|
220
|
+
.filter((one) => one && one !== "HEAD");
|
|
221
|
+
for (const branch of remoteBranches) {
|
|
222
|
+
run("git", ["branch", "--force", branch, `origin/${branch}`], checkout);
|
|
223
|
+
}
|
|
224
|
+
const commits = Number(run("git", ["rev-list", "--all", "--count"], checkout).out.trim() || "0");
|
|
225
|
+
const tags = run("git", ["tag", "-l"], checkout).out.split("\n").filter(Boolean);
|
|
226
|
+
console.log(` ${commits} commit${commits === 1 ? "" : "s"} · ` +
|
|
227
|
+
`${remoteBranches.length} branch${remoteBranches.length === 1 ? "" : "es"} · ` +
|
|
228
|
+
`${tags.length} tag${tags.length === 1 ? "" : "s"}`);
|
|
229
|
+
if (commits > 200) {
|
|
230
|
+
console.log(dim(` Each commit becomes a version, so this will take a while —\n` +
|
|
231
|
+
` roughly ${Math.round((commits * 11) / 60)} minutes. It resumes if interrupted.`));
|
|
232
|
+
}
|
|
233
|
+
run("git", ["remote", "add", "coderook", `coderook://${wanted}`], checkout);
|
|
234
|
+
console.log(`\n${dim("Publishing the history…")}`);
|
|
235
|
+
const pushed = run("git", ["push", "coderook", "--all"], checkout);
|
|
236
|
+
node_process_1.default.stderr.write(pushed.out);
|
|
237
|
+
if (!pushed.ok) {
|
|
238
|
+
console.error(red("\nThe history did not transfer completely."));
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
report.push(`${commits} commits across ${remoteBranches.length} branch(es)`);
|
|
242
|
+
if (tags.length) {
|
|
243
|
+
const pushedTags = run("git", ["push", "coderook", "--tags"], checkout);
|
|
244
|
+
node_process_1.default.stderr.write(pushedTags.out);
|
|
245
|
+
report.push(pushedTags.ok
|
|
246
|
+
? `${tags.length} tag(s) as releases`
|
|
247
|
+
: `tags were refused — see above`);
|
|
248
|
+
}
|
|
249
|
+
const project = await (0, api_js_1.findProject)(wanted);
|
|
250
|
+
if (!project) {
|
|
251
|
+
console.error(red("\nThe project was not created. Nothing else was changed."));
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
// What the repository says about itself.
|
|
255
|
+
const facts = await describeSource(forge, token);
|
|
256
|
+
if (facts) {
|
|
257
|
+
const asked = parsed.flags.get("visibility");
|
|
258
|
+
/*
|
|
259
|
+
Private unless asked otherwise, even when the source is public.
|
|
260
|
+
Mirroring visibility is the faithful thing and publishing somebody's
|
|
261
|
+
code by side effect is the unrecoverable thing, so the safe reading
|
|
262
|
+
wins and the report says what was chosen.
|
|
263
|
+
*/
|
|
264
|
+
const visibility = asked === "same"
|
|
265
|
+
? facts.visibility
|
|
266
|
+
: asked === "public"
|
|
267
|
+
? "public"
|
|
268
|
+
: "private";
|
|
269
|
+
await (0, api_js_1.updateProject)(project.id, {
|
|
270
|
+
...(facts.description ? { description: facts.description } : {}),
|
|
271
|
+
visibility,
|
|
272
|
+
});
|
|
273
|
+
report.push(`description and visibility (${visibility}` +
|
|
274
|
+
`${visibility !== facts.visibility ? `, source was ${facts.visibility}` : ""})`);
|
|
275
|
+
/*
|
|
276
|
+
Said rather than left to be noticed. Creating a project always makes a
|
|
277
|
+
`main` line, so a repository whose default is anything else arrives
|
|
278
|
+
with an empty one beside its real branches — and `cbx tracks` marks it
|
|
279
|
+
as the current line, which reads as though the transfer lost the work.
|
|
280
|
+
*/
|
|
281
|
+
if (facts.defaultBranch && facts.defaultBranch !== "main") {
|
|
282
|
+
report.push(`default line is ${facts.defaultBranch}; the empty "main" beside it ` +
|
|
283
|
+
`was made when the project was created`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
report.push("description could not be read from the source");
|
|
288
|
+
}
|
|
289
|
+
// Issues, only when asked and only with a token.
|
|
290
|
+
if (parsed.flags.has("issues")) {
|
|
291
|
+
if (!token) {
|
|
292
|
+
report.push(`issues skipped — set ${forge.tokenNames[0]} and run with --issues again`);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
try {
|
|
296
|
+
const issues = await readIssues(forge, token);
|
|
297
|
+
let made = 0;
|
|
298
|
+
for (const issue of issues) {
|
|
299
|
+
await (0, api_js_1.createIssue)(project.id, {
|
|
300
|
+
title: issue.title,
|
|
301
|
+
body: `${issue.body}\n\n` +
|
|
302
|
+
`— transferred from ${forge.label} ${forge.owner}/${forge.repo}#${issue.number}`,
|
|
303
|
+
});
|
|
304
|
+
made += 1;
|
|
305
|
+
}
|
|
306
|
+
report.push(`${made} open issue(s)`);
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
report.push(`issues failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
console.log(`\n${green("Transferred")} ${bold(project.slug)}`);
|
|
314
|
+
for (const line of report)
|
|
315
|
+
console.log(` ${green("·")} ${line}`);
|
|
316
|
+
console.log(`\n${dim("Not carried across:")}`);
|
|
317
|
+
console.log(dim(" · pull requests and reviews — CodeRook has no equivalent object"));
|
|
318
|
+
console.log(dim(" · CI configuration — runs are started deliberately, not by a push"));
|
|
319
|
+
console.log(dim(" · collaborators — access is an invitation to accept, not a field to copy"));
|
|
320
|
+
console.log(dim(" · stars, forks and watchers"));
|
|
321
|
+
if (!parsed.flags.has("issues")) {
|
|
322
|
+
console.log(dim(" · issues — pass --issues to bring the open ones"));
|
|
323
|
+
}
|
|
324
|
+
console.log(`\nFetch it anywhere with ${accent(`cbx clone ${project.slug}`)}` +
|
|
325
|
+
`${dim(", or ")}${accent(`git clone coderook://${project.slug}`)}${dim(".")}`);
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
await (0, promises_1.rm)(scratch, { recursive: true, force: true });
|
|
330
|
+
}
|
|
331
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderook/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "CodeRook from the command line, on any operating system",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"homepage": "https://coderook.com",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"test:matrix": "node test/state-matrix.mjs",
|
|
37
37
|
"test:attempt": "node test/attempt-identity.mjs",
|
|
38
38
|
"test:get": "node test/get-safety.mjs",
|
|
39
|
-
"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",
|
|
39
|
+
"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-track.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",
|
|
40
40
|
"test:getedits": "node test/get-protects-edits.mjs",
|
|
41
41
|
"test:upgrade": "node test/upgrade-migration.mjs",
|
|
42
42
|
"test:faultget": "node test/fault-get.mjs",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"test:faultsubmit": "node test/fault-submit.mjs",
|
|
45
45
|
"test:race": "node test/race-attempts.mjs",
|
|
46
46
|
"test:runner": "node test/runner-live.mjs",
|
|
47
|
-
"sync:plugin": "node scripts/sync-plugin-version.mjs"
|
|
47
|
+
"sync:plugin": "node scripts/sync-plugin-version.mjs",
|
|
48
|
+
"test:track": "node test/get-track.mjs"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@types/node": "24.10.1",
|