@coderook/cli 0.1.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/LICENSE.txt +46 -0
- package/README.md +99 -0
- package/dist/cli/src/api.js +67 -0
- package/dist/cli/src/cli.js +461 -0
- package/dist/cli/src/config.js +96 -0
- package/dist/desktop-app/src/main/cbx.js +455 -0
- package/dist/desktop-app/src/main/credentials.js +2 -0
- package/dist/desktop-app/src/main/download.js +147 -0
- package/dist/desktop-app/src/main/rules.js +172 -0
- package/dist/desktop-app/src/main/upload.js +433 -0
- package/dist/desktop-app/src/main/worktree.js +626 -0
- package/dist/desktop-app/src/shared/types.js +3 -0
- package/package.json +36 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
CodeRook Desktop - Terms of Use
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CodeRook. All rights reserved.
|
|
4
|
+
|
|
5
|
+
1. Licence
|
|
6
|
+
|
|
7
|
+
CodeRook grants you a personal, non-exclusive, non-transferable licence to
|
|
8
|
+
install and use CodeRook Desktop on computers you own or control, for the
|
|
9
|
+
purpose of working with your own CodeRook projects.
|
|
10
|
+
|
|
11
|
+
2. What this software does
|
|
12
|
+
|
|
13
|
+
CodeRook Desktop reads folders you choose on this computer and uploads the
|
|
14
|
+
files you select to your CodeRook account. It shows you what will be sent
|
|
15
|
+
before anything leaves the machine, and it never uploads a folder you have
|
|
16
|
+
not opened.
|
|
17
|
+
|
|
18
|
+
3. Your content
|
|
19
|
+
|
|
20
|
+
Your files remain yours. CodeRook stores them so you can retrieve them, and
|
|
21
|
+
does not claim ownership of anything you upload.
|
|
22
|
+
|
|
23
|
+
4. Credentials
|
|
24
|
+
|
|
25
|
+
Your access token is stored using the operating system's credential vault. It
|
|
26
|
+
is not written to disk in plain text. Signing out removes it from this
|
|
27
|
+
computer.
|
|
28
|
+
|
|
29
|
+
5. No warranty
|
|
30
|
+
|
|
31
|
+
This software is provided "as is", without warranty of any kind, express or
|
|
32
|
+
implied, including but not limited to the warranties of merchantability,
|
|
33
|
+
fitness for a particular purpose and non-infringement. Keep your own backups
|
|
34
|
+
of anything you cannot afford to lose.
|
|
35
|
+
|
|
36
|
+
6. Limitation of liability
|
|
37
|
+
|
|
38
|
+
In no event shall CodeRook be liable for any claim, damages or other
|
|
39
|
+
liability, whether in an action of contract, tort or otherwise, arising from,
|
|
40
|
+
out of or in connection with the software or the use or other dealings in the
|
|
41
|
+
software.
|
|
42
|
+
|
|
43
|
+
7. Privacy and terms
|
|
44
|
+
|
|
45
|
+
Use of your CodeRook account is additionally governed by the terms and privacy
|
|
46
|
+
policy published at https://coderook.com/terms and https://coderook.com/privacy.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# CodeRook CLI
|
|
2
|
+
|
|
3
|
+
CodeRook from the terminal, on Windows, macOS and Linux alike. It uses the
|
|
4
|
+
same engines as the desktop application — the same scanner, the same
|
|
5
|
+
`.gitignore` rules, the same upload, download and `.cbx` bundling — so a
|
|
6
|
+
project moved by one is understood by the other.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Requires Node 20.11 or later, and nothing else.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npm install --global @coderook/cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or from this repository:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
cd cli
|
|
20
|
+
npm install
|
|
21
|
+
npm run build
|
|
22
|
+
npm link
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Sign in
|
|
26
|
+
|
|
27
|
+
Create a personal access token in **Settings** on coderook.com, then:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
coderook sign-in
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The token is written to a file only your account can read:
|
|
34
|
+
|
|
35
|
+
| Platform | Location |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| Windows | `%APPDATA%\CodeRook` |
|
|
38
|
+
| macOS | `~/Library/Application Support/CodeRook` |
|
|
39
|
+
| Linux | `$XDG_CONFIG_HOME/coderook`, or `~/.config/coderook` |
|
|
40
|
+
|
|
41
|
+
For automation, set `CODEROOK_TOKEN` instead and nothing is written to disk.
|
|
42
|
+
`CODEROOK_API_URL` points the tool at a different service.
|
|
43
|
+
|
|
44
|
+
## Everyday use
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
coderook status what is here that is not saved yet
|
|
48
|
+
coderook submit -m "Fixed the parser"
|
|
49
|
+
coderook get fetch the latest version over this folder
|
|
50
|
+
coderook clone my-project fetch a project into a new folder
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`status` and `submit` act on the current directory unless you name another.
|
|
54
|
+
The first time either runs in a folder, it looks for a project on your account
|
|
55
|
+
whose name matches and links the two, so a folder you already uploaded from
|
|
56
|
+
the desktop is recognised rather than treated as new.
|
|
57
|
+
|
|
58
|
+
`submit` sends only what changed. Files the service already holds are not
|
|
59
|
+
uploaded again, and the version it records still names every file in the
|
|
60
|
+
project — a version is a snapshot, not a difference.
|
|
61
|
+
|
|
62
|
+
## Ignore rules
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
coderook rules show the rules in force
|
|
66
|
+
coderook rules --init write a starter .gitignore
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Rules live in the project's own `.gitignore`, so CodeRook, git, the website
|
|
70
|
+
and the desktop application all read one file. A `!` line puts something back.
|
|
71
|
+
Personal exclusions that should not reach a collaborator belong in
|
|
72
|
+
`.git/info/exclude`.
|
|
73
|
+
|
|
74
|
+
## Bundles
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
coderook bundle pack this project as <name>.cbx
|
|
78
|
+
coderook unbundle backup.cbx extract one
|
|
79
|
+
coderook inspect backup.cbx see what it holds (--files to list them)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A `.cbx` is the CodeBox bundle format: content-defined chunks, Zstandard where
|
|
83
|
+
it helps, raw where it does not, and a SHA-256 for every chunk and file.
|
|
84
|
+
Extraction is verified and atomic — a damaged bundle fails rather than leaving
|
|
85
|
+
a half-written tree.
|
|
86
|
+
|
|
87
|
+
## Checking things
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
coderook doctor
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Reports the tool's version, where its configuration lives, whether the service
|
|
94
|
+
is reachable, and who this machine is signed in as.
|
|
95
|
+
|
|
96
|
+
## Exit codes
|
|
97
|
+
|
|
98
|
+
`0` on success, `1` on failure. Every command prints why it failed on standard
|
|
99
|
+
error, so a script can branch on the status and log the reason.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.whoami = whoami;
|
|
4
|
+
exports.projects = projects;
|
|
5
|
+
exports.findProject = findProject;
|
|
6
|
+
exports.health = health;
|
|
7
|
+
/** The small part of the API the command-line tool needs directly. */
|
|
8
|
+
const config_js_1 = require("./config.js");
|
|
9
|
+
async function call(route, options = {}) {
|
|
10
|
+
const token = await (0, config_js_1.loadToken)();
|
|
11
|
+
if (!token) {
|
|
12
|
+
throw new Error("Not signed in. Run: coderook sign-in");
|
|
13
|
+
}
|
|
14
|
+
const response = await fetch(`${(0, config_js_1.apiOrigin)()}${route}`, {
|
|
15
|
+
method: options.method ?? "GET",
|
|
16
|
+
headers: {
|
|
17
|
+
accept: "application/json",
|
|
18
|
+
authorization: `Bearer ${token}`,
|
|
19
|
+
"user-agent": "CodeRook-CLI/0.1",
|
|
20
|
+
...(options.body ? { "content-type": "application/json" } : {}),
|
|
21
|
+
},
|
|
22
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
23
|
+
});
|
|
24
|
+
const text = await response.text();
|
|
25
|
+
const body = text ? JSON.parse(text) : {};
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
if (response.status === 401) {
|
|
28
|
+
throw new Error("That token was not accepted. Run: coderook sign-in");
|
|
29
|
+
}
|
|
30
|
+
throw new Error(body?.error?.message ?? `${route} failed (${response.status})`);
|
|
31
|
+
}
|
|
32
|
+
return body;
|
|
33
|
+
}
|
|
34
|
+
async function whoami() {
|
|
35
|
+
const body = await call("/v1/auth/session");
|
|
36
|
+
if (!body.user)
|
|
37
|
+
throw new Error("That token does not belong to an account");
|
|
38
|
+
return body.user;
|
|
39
|
+
}
|
|
40
|
+
async function projects() {
|
|
41
|
+
const body = await call("/v1/repositories");
|
|
42
|
+
return (body.repositories ?? []).map((row) => ({
|
|
43
|
+
id: String(row.id ?? ""),
|
|
44
|
+
slug: String(row.slug ?? ""),
|
|
45
|
+
name: String(row.displayName || row.slug || "Untitled"),
|
|
46
|
+
visibility: String(row.visibility ?? "private"),
|
|
47
|
+
versionCount: Number(row.versionCount ?? 0),
|
|
48
|
+
fileCount: Number(row.fileCount ?? 0),
|
|
49
|
+
storedBytes: Number(row.storedSize ?? 0),
|
|
50
|
+
updatedAt: String(row.updatedAt ?? row.createdAt ?? ""),
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/** Find a project by slug or display name, so either reads naturally. */
|
|
54
|
+
async function findProject(reference) {
|
|
55
|
+
const wanted = reference.trim().toLowerCase();
|
|
56
|
+
const all = await projects();
|
|
57
|
+
return (all.find((project) => project.slug.toLowerCase() === wanted) ??
|
|
58
|
+
all.find((project) => project.name.toLowerCase() === wanted) ??
|
|
59
|
+
null);
|
|
60
|
+
}
|
|
61
|
+
/** Whether the service is reachable, and what encodings it accepts. */
|
|
62
|
+
async function health() {
|
|
63
|
+
const response = await fetch(`${(0, config_js_1.apiOrigin)()}/health?t=${Date.now()}`, {
|
|
64
|
+
headers: { "cache-control": "no-cache" },
|
|
65
|
+
});
|
|
66
|
+
return (await response.json());
|
|
67
|
+
}
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
/**
|
|
8
|
+
* CodeRook from the command line.
|
|
9
|
+
*
|
|
10
|
+
* The same engines the desktop application uses — the same scanner, the same
|
|
11
|
+
* ignore rules, the same upload and download — with a terminal in front of
|
|
12
|
+
* them instead of a window. Nothing here is Windows-specific, so it runs
|
|
13
|
+
* wherever Node does.
|
|
14
|
+
*
|
|
15
|
+
* The vocabulary follows docs/REPOSITORY_MODEL.md: get latest, save a
|
|
16
|
+
* version, submit it.
|
|
17
|
+
*/
|
|
18
|
+
const promises_1 = require("node:readline/promises");
|
|
19
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
20
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
21
|
+
const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
|
|
22
|
+
const upload_js_1 = require("../../desktop-app/src/main/upload.js");
|
|
23
|
+
const download_js_1 = require("../../desktop-app/src/main/download.js");
|
|
24
|
+
const cbx_js_1 = require("../../desktop-app/src/main/cbx.js");
|
|
25
|
+
const api_js_1 = require("./api.js");
|
|
26
|
+
const config_js_1 = require("./config.js");
|
|
27
|
+
const VERSION = "0.1.0";
|
|
28
|
+
// ── presentation ────────────────────────────────────────────────────────────
|
|
29
|
+
const colour = node_process_1.default.stdout.isTTY && !node_process_1.default.env.NO_COLOR;
|
|
30
|
+
const dim = (text) => (colour ? `[2m${text}[0m` : text);
|
|
31
|
+
const bold = (text) => (colour ? `[1m${text}[0m` : text);
|
|
32
|
+
const accent = (text) => (colour ? `[33m${text}[0m` : text);
|
|
33
|
+
const red = (text) => (colour ? `[31m${text}[0m` : text);
|
|
34
|
+
function bytes(value) {
|
|
35
|
+
if (value >= 1024 ** 3)
|
|
36
|
+
return `${(value / 1024 ** 3).toFixed(2)} GB`;
|
|
37
|
+
if (value >= 1024 ** 2)
|
|
38
|
+
return `${(value / 1024 ** 2).toFixed(1)} MB`;
|
|
39
|
+
if (value >= 1024)
|
|
40
|
+
return `${(value / 1024).toFixed(1)} KB`;
|
|
41
|
+
return `${value} B`;
|
|
42
|
+
}
|
|
43
|
+
/** One line that rewrites itself, so progress does not scroll the terminal. */
|
|
44
|
+
function progressLine() {
|
|
45
|
+
let last = 0;
|
|
46
|
+
return (text) => {
|
|
47
|
+
if (!node_process_1.default.stdout.isTTY)
|
|
48
|
+
return;
|
|
49
|
+
const padded = text.padEnd(last);
|
|
50
|
+
last = text.length;
|
|
51
|
+
node_process_1.default.stdout.write(`\r${padded}`);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const done = (line) => {
|
|
55
|
+
if (node_process_1.default.stdout.isTTY) {
|
|
56
|
+
line("");
|
|
57
|
+
node_process_1.default.stdout.write("\r");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
function parse(argv) {
|
|
61
|
+
const positional = [];
|
|
62
|
+
const flags = new Map();
|
|
63
|
+
for (let at = 0; at < argv.length; at += 1) {
|
|
64
|
+
const item = argv[at];
|
|
65
|
+
if (!item.startsWith("-")) {
|
|
66
|
+
positional.push(item);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const name = item.replace(/^--?/, "");
|
|
70
|
+
const next = argv[at + 1];
|
|
71
|
+
if (next && !next.startsWith("-")) {
|
|
72
|
+
flags.set(name, next);
|
|
73
|
+
at += 1;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
flags.set(name, true);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { positional, flags };
|
|
80
|
+
}
|
|
81
|
+
const flagText = (parsed, ...names) => {
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
const value = parsed.flags.get(name);
|
|
84
|
+
if (typeof value === "string")
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
};
|
|
89
|
+
const hasFlag = (parsed, ...names) => names.some((name) => parsed.flags.has(name));
|
|
90
|
+
const folderFor = (parsed, index = 0) => node_path_1.default.resolve(parsed.positional[index] ?? node_process_1.default.cwd());
|
|
91
|
+
// ── shared behaviour ────────────────────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Reconcile a folder with the account: which repository it belongs to and
|
|
94
|
+
* what that repository already holds. The account is the authority, exactly
|
|
95
|
+
* as it is in the desktop application.
|
|
96
|
+
*/
|
|
97
|
+
async function reconcile(folder, options = {}) {
|
|
98
|
+
const link = await (0, config_js_1.readLink)(folder);
|
|
99
|
+
const reference = link?.slug ?? node_path_1.default.basename(folder);
|
|
100
|
+
const project = link
|
|
101
|
+
? (await (0, api_js_1.projects)()).find((candidate) => candidate.id === link.repositoryId)
|
|
102
|
+
: await (0, api_js_1.findProject)(reference);
|
|
103
|
+
if (!project?.versionCount) {
|
|
104
|
+
// Nothing saved on the account, so everything here is outstanding.
|
|
105
|
+
return { link: link ?? null, baseline: null };
|
|
106
|
+
}
|
|
107
|
+
const downloader = new download_js_1.Downloader(config_js_1.credentials);
|
|
108
|
+
const latest = (await downloader.versions(project.id))[0];
|
|
109
|
+
if (!latest)
|
|
110
|
+
return { link: link ?? null, baseline: null };
|
|
111
|
+
const baseline = new Map();
|
|
112
|
+
for (const file of await downloader.files(project.id, latest.id)) {
|
|
113
|
+
baseline.set(file.path, file.sha256);
|
|
114
|
+
}
|
|
115
|
+
const fresh = {
|
|
116
|
+
repositoryId: project.id,
|
|
117
|
+
slug: project.slug,
|
|
118
|
+
sequence: latest.sequence,
|
|
119
|
+
manifest: Object.fromEntries(baseline),
|
|
120
|
+
};
|
|
121
|
+
await (0, config_js_1.writeLink)(folder, fresh);
|
|
122
|
+
if (!options.quiet && !link) {
|
|
123
|
+
console.log(dim(`Linked this folder to ${project.slug}.`));
|
|
124
|
+
}
|
|
125
|
+
return { link: fresh, baseline };
|
|
126
|
+
}
|
|
127
|
+
// ── commands ────────────────────────────────────────────────────────────────
|
|
128
|
+
async function commandSignIn(parsed) {
|
|
129
|
+
const supplied = flagText(parsed, "token");
|
|
130
|
+
let token = supplied ?? "";
|
|
131
|
+
if (!token) {
|
|
132
|
+
console.log(`Create a personal access token at ${dim("https://coderook.com/?screen=settings")}`);
|
|
133
|
+
const reader = (0, promises_1.createInterface)({ input: node_process_1.default.stdin, output: node_process_1.default.stdout });
|
|
134
|
+
token = (await reader.question("Access token: ")).trim();
|
|
135
|
+
reader.close();
|
|
136
|
+
}
|
|
137
|
+
if (!token) {
|
|
138
|
+
console.error(red("No token given."));
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
await (0, config_js_1.storeToken)(token);
|
|
142
|
+
try {
|
|
143
|
+
const account = await (0, api_js_1.whoami)();
|
|
144
|
+
console.log(`Signed in as ${bold(account.displayName)} ${dim(`<${account.email}>`)} · ${account.plan}`);
|
|
145
|
+
console.log(dim(`Token stored in ${(0, config_js_1.configDirectory)()}`));
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
await (0, config_js_1.clearToken)();
|
|
150
|
+
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function commandSignOut() {
|
|
155
|
+
await (0, config_js_1.clearToken)();
|
|
156
|
+
console.log("Signed out on this machine.");
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
async function commandWhoami() {
|
|
160
|
+
const account = await (0, api_js_1.whoami)();
|
|
161
|
+
console.log(`${bold(account.displayName)} <${account.email}> · ${account.plan}`);
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
async function commandProjects() {
|
|
165
|
+
const all = await (0, api_js_1.projects)();
|
|
166
|
+
if (!all.length) {
|
|
167
|
+
console.log("No projects on this account yet.");
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
const width = Math.max(...all.map((project) => project.slug.length));
|
|
171
|
+
for (const project of all) {
|
|
172
|
+
console.log(`${accent(project.slug.padEnd(width))} v${String(project.versionCount).padEnd(4)}` +
|
|
173
|
+
`${String(project.fileCount).padStart(6)} files ${bytes(project.storedBytes).padStart(9)}` +
|
|
174
|
+
` ${dim(project.visibility)}`);
|
|
175
|
+
}
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
async function commandStatus(parsed) {
|
|
179
|
+
const folder = folderFor(parsed);
|
|
180
|
+
const { link, baseline } = await reconcile(folder);
|
|
181
|
+
const rules = await (0, worktree_js_1.readRules)(folder);
|
|
182
|
+
const files = await (0, worktree_js_1.changedFiles)(folder, rules, baseline);
|
|
183
|
+
console.log(bold(node_path_1.default.basename(folder)) + dim(` ${folder}`));
|
|
184
|
+
console.log(link
|
|
185
|
+
? `Account holds ${accent(`v${link.sequence}`)} · ${Object.keys(link.manifest).length} files ${dim(`(${link.slug})`)}`
|
|
186
|
+
: dim("Not on your account yet — submitting will create it."));
|
|
187
|
+
if (!files.length) {
|
|
188
|
+
console.log("Nothing to submit; this folder matches the saved version.");
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
console.log(`\n${files.length} file${files.length === 1 ? "" : "s"} to submit:`);
|
|
192
|
+
for (const file of files.slice(0, 50)) {
|
|
193
|
+
console.log(` ${file.deleted ? red("deleted") : accent("changed")} ${file.path}`);
|
|
194
|
+
}
|
|
195
|
+
if (files.length > 50)
|
|
196
|
+
console.log(dim(` …and ${files.length - 50} more`));
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
async function commandSubmit(parsed) {
|
|
200
|
+
const folder = folderFor(parsed);
|
|
201
|
+
const message = flagText(parsed, "m", "message") ?? "";
|
|
202
|
+
const { link, baseline } = await reconcile(folder);
|
|
203
|
+
const rules = await (0, worktree_js_1.readRules)(folder);
|
|
204
|
+
const files = await (0, worktree_js_1.changedFiles)(folder, rules, baseline);
|
|
205
|
+
if (!files.length) {
|
|
206
|
+
console.log("Nothing to submit; this folder matches the saved version.");
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
if (hasFlag(parsed, "dry-run", "n")) {
|
|
210
|
+
console.log(`${files.length} file${files.length === 1 ? "" : "s"} would be sent:`);
|
|
211
|
+
for (const file of files)
|
|
212
|
+
console.log(` ${file.path}`);
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
const line = progressLine();
|
|
216
|
+
const uploader = new upload_js_1.Uploader(config_js_1.credentials);
|
|
217
|
+
const result = await uploader.run({
|
|
218
|
+
localPath: folder,
|
|
219
|
+
include: files.map((file) => file.path),
|
|
220
|
+
message,
|
|
221
|
+
projectName: link?.slug ?? node_path_1.default.basename(folder),
|
|
222
|
+
repositoryId: link?.repositoryId ?? null,
|
|
223
|
+
}, (progress) => {
|
|
224
|
+
line(` ${String(progress.percent).padStart(3)}% ${progress.stage.padEnd(7)} ` +
|
|
225
|
+
`${progress.files}/${progress.totalFiles} ${progress.path.slice(-48)}`);
|
|
226
|
+
});
|
|
227
|
+
done(line);
|
|
228
|
+
await (0, config_js_1.writeLink)(folder, {
|
|
229
|
+
repositoryId: result.repositoryId,
|
|
230
|
+
slug: link?.slug ?? node_path_1.default.basename(folder),
|
|
231
|
+
sequence: result.sequence,
|
|
232
|
+
manifest: result.manifest,
|
|
233
|
+
});
|
|
234
|
+
console.log(`Saved ${accent(`v${result.sequence}`)} · sent ${bytes(result.sentBytes)} in ` +
|
|
235
|
+
`${result.sentFiles} file${result.sentFiles === 1 ? "" : "s"}` +
|
|
236
|
+
(result.reusedFiles ? `, ${result.reusedFiles} already stored` : "") +
|
|
237
|
+
` · version holds ${bytes(result.sourceBytes)}`);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
async function commandGet(parsed) {
|
|
241
|
+
const folder = folderFor(parsed);
|
|
242
|
+
const { link } = await reconcile(folder);
|
|
243
|
+
if (!link) {
|
|
244
|
+
console.error(red("This folder is not linked to a project on your account."));
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
return fetchInto(link.repositoryId, folder, link.slug);
|
|
248
|
+
}
|
|
249
|
+
async function commandClone(parsed) {
|
|
250
|
+
const reference = parsed.positional[0];
|
|
251
|
+
if (!reference) {
|
|
252
|
+
console.error(red("Which project? Try: coderook clone <project>"));
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
const project = await (0, api_js_1.findProject)(reference);
|
|
256
|
+
if (!project) {
|
|
257
|
+
console.error(red(`No project named ${reference} on this account.`));
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
if (!project.versionCount) {
|
|
261
|
+
console.error(red(`${project.slug} has no saved version to fetch.`));
|
|
262
|
+
return 1;
|
|
263
|
+
}
|
|
264
|
+
const destination = node_path_1.default.resolve(parsed.positional[1] ?? project.slug);
|
|
265
|
+
return fetchInto(project.id, destination, project.slug);
|
|
266
|
+
}
|
|
267
|
+
async function fetchInto(repositoryId, destination, slug) {
|
|
268
|
+
const downloader = new download_js_1.Downloader(config_js_1.credentials);
|
|
269
|
+
const latest = (await downloader.versions(repositoryId))[0];
|
|
270
|
+
if (!latest) {
|
|
271
|
+
console.error(red("That project has no saved version."));
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
const line = progressLine();
|
|
275
|
+
const result = await downloader.run(repositoryId, latest.id, destination, (progress) => {
|
|
276
|
+
line(` ${String(progress.percent).padStart(3)}% ${progress.files}/${progress.totalFiles}` +
|
|
277
|
+
` ${progress.path.slice(-48)}`);
|
|
278
|
+
});
|
|
279
|
+
done(line);
|
|
280
|
+
await (0, config_js_1.writeLink)(destination, {
|
|
281
|
+
repositoryId,
|
|
282
|
+
slug,
|
|
283
|
+
sequence: latest.sequence,
|
|
284
|
+
manifest: result.manifest,
|
|
285
|
+
});
|
|
286
|
+
console.log(`${accent(`v${latest.sequence}`)} · ${result.files} files · ${bytes(result.bytes)} into ${destination}`);
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
async function commandRules(parsed) {
|
|
290
|
+
const folder = folderFor(parsed);
|
|
291
|
+
if (hasFlag(parsed, "init")) {
|
|
292
|
+
const existing = await (0, worktree_js_1.readRules)(folder);
|
|
293
|
+
await (0, worktree_js_1.writeRules)(folder, {
|
|
294
|
+
shared: existing.shared.trim() ? existing.shared : worktree_js_1.STARTER_IGNORE,
|
|
295
|
+
local: existing.local,
|
|
296
|
+
});
|
|
297
|
+
console.log(`Wrote ${node_path_1.default.join(folder, ".gitignore")}`);
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
const rules = await (0, worktree_js_1.readRules)(folder);
|
|
301
|
+
node_process_1.default.stdout.write(rules.shared);
|
|
302
|
+
if (rules.local.trim()) {
|
|
303
|
+
console.log(dim("\n# --- only on this machine (.git/info/exclude) ---"));
|
|
304
|
+
node_process_1.default.stdout.write(rules.local);
|
|
305
|
+
}
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
async function commandBundle(parsed) {
|
|
309
|
+
const folder = folderFor(parsed, 0);
|
|
310
|
+
const target = node_path_1.default.resolve(parsed.positional[1] ?? `${node_path_1.default.basename(folder)}.cbx`);
|
|
311
|
+
const rules = await (0, worktree_js_1.readRules)(folder);
|
|
312
|
+
const entries = (await (0, worktree_js_1.changedFiles)(folder, rules, null)).map((file) => file.path);
|
|
313
|
+
const line = progressLine();
|
|
314
|
+
const result = await (0, cbx_js_1.packBundle)({ root: folder, target, entries, sourceName: node_path_1.default.basename(folder) }, (progress) => {
|
|
315
|
+
line(` ${String(progress.percent).padStart(3)}% ${progress.path.slice(-48)}`);
|
|
316
|
+
});
|
|
317
|
+
done(line);
|
|
318
|
+
const saved = Math.max(0, 100 - Math.ceil((result.archiveBytes / Math.max(result.sourceBytes, 1)) * 100));
|
|
319
|
+
console.log(`${target} · ${bytes(result.archiveBytes)} from ${bytes(result.sourceBytes)} ` +
|
|
320
|
+
`(${saved}% smaller) · ${result.uniqueChunks} chunks`);
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
async function commandUnbundle(parsed) {
|
|
324
|
+
const source = parsed.positional[0];
|
|
325
|
+
if (!source) {
|
|
326
|
+
console.error(red("Which bundle? Try: coderook unbundle <file.cbx> [folder]"));
|
|
327
|
+
return 1;
|
|
328
|
+
}
|
|
329
|
+
const manifest = await (0, cbx_js_1.readManifest)(node_path_1.default.resolve(source));
|
|
330
|
+
const destination = node_path_1.default.resolve(parsed.positional[1] ?? manifest.source_name);
|
|
331
|
+
const line = progressLine();
|
|
332
|
+
const result = await (0, cbx_js_1.unpackBundle)(node_path_1.default.resolve(source), destination, (progress) => {
|
|
333
|
+
line(` ${String(progress.percent).padStart(3)}% ${progress.path.slice(-48)}`);
|
|
334
|
+
});
|
|
335
|
+
done(line);
|
|
336
|
+
console.log(`${result.files} files · ${bytes(result.bytes)} into ${destination}`);
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
async function commandInspect(parsed) {
|
|
340
|
+
const source = parsed.positional[0];
|
|
341
|
+
if (!source) {
|
|
342
|
+
console.error(red("Which bundle? Try: coderook inspect <file.cbx>"));
|
|
343
|
+
return 1;
|
|
344
|
+
}
|
|
345
|
+
const manifest = await (0, cbx_js_1.readManifest)(node_path_1.default.resolve(source));
|
|
346
|
+
const total = manifest.files.reduce((sum, file) => sum + file.size, 0);
|
|
347
|
+
console.log(`${bold(manifest.source_name)} · format ${manifest.format} v${manifest.format_version}`);
|
|
348
|
+
console.log(`${manifest.files.length} files · ${bytes(total)} · ` +
|
|
349
|
+
`${Object.keys(manifest.chunks).length} chunks`);
|
|
350
|
+
if (hasFlag(parsed, "files")) {
|
|
351
|
+
for (const file of manifest.files) {
|
|
352
|
+
console.log(` ${bytes(file.size).padStart(9)} ${file.path}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
async function commandDoctor() {
|
|
358
|
+
const service = await (0, api_js_1.health)().catch(() => null);
|
|
359
|
+
console.log(`CodeRook CLI ${VERSION} · Node ${node_process_1.default.versions.node} · ${node_process_1.default.platform}`);
|
|
360
|
+
console.log(`Config: ${(0, config_js_1.configDirectory)()}`);
|
|
361
|
+
console.log(service
|
|
362
|
+
? `Service: ${service.status} · schema ${service.schemaVersion}` +
|
|
363
|
+
(service.contentEncodings ? ` · accepts ${service.contentEncodings.join(", ")}` : "")
|
|
364
|
+
: red("Service: unreachable"));
|
|
365
|
+
const token = await (0, config_js_1.loadToken)();
|
|
366
|
+
if (!token) {
|
|
367
|
+
console.log("Account: not signed in");
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const account = await (0, api_js_1.whoami)();
|
|
372
|
+
console.log(`Account: ${account.email} · ${account.plan}`);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
console.log(red(`Account: ${error instanceof Error ? error.message : error}`));
|
|
376
|
+
}
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
const USAGE = `${bold("coderook")} — CodeRook from the command line
|
|
380
|
+
|
|
381
|
+
${bold("Getting started")}
|
|
382
|
+
coderook sign-in Store a personal access token
|
|
383
|
+
coderook whoami Who this machine is signed in as
|
|
384
|
+
coderook projects Every project on your account
|
|
385
|
+
|
|
386
|
+
${bold("Working with a folder")}
|
|
387
|
+
coderook status [folder] What is here that is not saved yet
|
|
388
|
+
coderook submit [folder] -m "…" Send the changes as a new version
|
|
389
|
+
coderook get [folder] Fetch the latest version over this folder
|
|
390
|
+
coderook clone <project> [dir] Fetch a project into a new folder
|
|
391
|
+
coderook rules [folder] Show the ignore rules (--init to start one)
|
|
392
|
+
|
|
393
|
+
${bold("Bundles")}
|
|
394
|
+
coderook bundle [folder] [out] Pack the project as a .cbx
|
|
395
|
+
coderook unbundle <file> [dir] Extract a .cbx
|
|
396
|
+
coderook inspect <file> What a .cbx contains (--files to list them)
|
|
397
|
+
|
|
398
|
+
${bold("Other")}
|
|
399
|
+
coderook doctor Check the service, config and sign-in
|
|
400
|
+
coderook sign-out Forget the token on this machine
|
|
401
|
+
|
|
402
|
+
${bold("Options")}
|
|
403
|
+
-m, --message The version message for submit
|
|
404
|
+
-n, --dry-run Show what submit would send, without sending
|
|
405
|
+
--token Supply the token to sign-in instead of being asked
|
|
406
|
+
|
|
407
|
+
${dim("The environment variable CODEROOK_TOKEN is used when set, so automated")}
|
|
408
|
+
${dim("runs need nothing on disk. CODEROOK_API_URL points at another service.")}`;
|
|
409
|
+
const COMMANDS = {
|
|
410
|
+
"sign-in": commandSignIn,
|
|
411
|
+
login: commandSignIn,
|
|
412
|
+
"sign-out": commandSignOut,
|
|
413
|
+
logout: commandSignOut,
|
|
414
|
+
whoami: () => commandWhoami(),
|
|
415
|
+
projects: () => commandProjects(),
|
|
416
|
+
status: commandStatus,
|
|
417
|
+
submit: commandSubmit,
|
|
418
|
+
publish: commandSubmit,
|
|
419
|
+
get: commandGet,
|
|
420
|
+
clone: commandClone,
|
|
421
|
+
rules: commandRules,
|
|
422
|
+
bundle: commandBundle,
|
|
423
|
+
unbundle: commandUnbundle,
|
|
424
|
+
inspect: commandInspect,
|
|
425
|
+
doctor: () => commandDoctor(),
|
|
426
|
+
};
|
|
427
|
+
async function main(argv) {
|
|
428
|
+
const [name, ...rest] = argv;
|
|
429
|
+
if (!name || name === "help" || name === "--help" || name === "-h") {
|
|
430
|
+
console.log(USAGE);
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
if (name === "--version" || name === "-v" || name === "version") {
|
|
434
|
+
console.log(VERSION);
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
const command = COMMANDS[name];
|
|
438
|
+
if (!command) {
|
|
439
|
+
console.error(red(`Unknown command: ${name}`));
|
|
440
|
+
console.error(dim("Run coderook help to see what there is."));
|
|
441
|
+
return 1;
|
|
442
|
+
}
|
|
443
|
+
return command(parse(rest));
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Set the status and let Node wind down on its own.
|
|
447
|
+
*
|
|
448
|
+
* Calling process.exit here would race the sockets `fetch` keeps alive, and
|
|
449
|
+
* on Windows that trips a libuv assertion which prints alarming noise after
|
|
450
|
+
* a perfectly successful command. Unreferencing standard input is what lets
|
|
451
|
+
* the process finish promptly once the work is done.
|
|
452
|
+
*/
|
|
453
|
+
function leave(code) {
|
|
454
|
+
node_process_1.default.exitCode = code;
|
|
455
|
+
}
|
|
456
|
+
main(node_process_1.default.argv.slice(2))
|
|
457
|
+
.then(leave)
|
|
458
|
+
.catch((error) => {
|
|
459
|
+
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
460
|
+
leave(1);
|
|
461
|
+
});
|