@andrestobelem/clone 0.1.3 → 0.2.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/bin/clone.js CHANGED
@@ -1,232 +1,13 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
2
 
4
- const crypto = require("node:crypto");
5
- const fs = require("node:fs");
6
- const os = require("node:os");
7
- const path = require("node:path");
8
- const { spawn, spawnSync } = require("node:child_process");
9
- const packageMetadata = require("../package.json");
10
-
11
- const RELEASE_ROOT = "https://github.com/andrestobelem/clone/releases/download";
12
- const TARGETS = new Map([
13
- ["darwin-arm64", "clone-darwin-arm64"],
14
- ["darwin-x64", "clone-darwin-x64"],
15
- ["linux-arm64", "clone-linux-arm64"],
16
- ["linux-x64", "clone-linux-x64"],
17
- ["win32-x64", "clone-win32-x64.exe"],
18
- ]);
19
- const BLOCKED_PORTS = new Set([
20
- 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95,
21
- 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179,
22
- 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601,
23
- 636, 989, 990, 993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566,
24
- 6665, 6666, 6667, 6668, 6669, 6697, 10080,
25
- ]);
26
-
27
- function target(platform = process.platform, architecture = process.arch) {
28
- const value = TARGETS.get(`${platform}-${architecture}`);
29
- if (!value) throw new Error(`Clone does not support ${platform}/${architecture}.`);
30
- return value;
31
- }
32
-
33
- function cacheDirectory(platform = process.platform, environment = process.env) {
34
- if (environment.CLONE_CACHE_DIR) return path.resolve(environment.CLONE_CACHE_DIR);
35
- if (platform === "darwin") return path.join(os.homedir(), "Library", "Caches", "Clone");
36
- if (platform === "win32") return path.join(environment.LOCALAPPDATA || os.homedir(), "Clone", "cache");
37
- return path.join(environment.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"), "clone");
38
- }
39
-
40
- function dataDirectory(platform = process.platform, environment = process.env, override = null) {
41
- if (override) return path.resolve(override);
42
- if (environment.CLONE_DATA_DIR) return path.resolve(environment.CLONE_DATA_DIR);
43
- if (platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", "Clone");
44
- if (platform === "win32") return path.join(environment.APPDATA || os.homedir(), "Clone");
45
- return path.join(environment.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"), "clone");
46
- }
47
-
48
- function parseLauncherArguments(arguments_, workingDirectory = process.cwd()) {
49
- let storage = null;
50
- const forwarded = [];
51
- for (let index = 0; index < arguments_.length; index += 1) {
52
- const value = arguments_[index];
53
- if (value === "--local") {
54
- if (storage) throw new Error("Use only one data directory option.");
55
- storage = path.join(workingDirectory, ".clone");
56
- } else if (value === "--data-dir") {
57
- if (storage) throw new Error("Use only one data directory option.");
58
- const directory = arguments_[++index];
59
- if (!directory) throw new Error("--data-dir requires a path.");
60
- storage = path.resolve(workingDirectory, directory);
61
- } else if (value.startsWith("--data-dir=")) {
62
- if (storage) throw new Error("Use only one data directory option.");
63
- const directory = value.slice(11);
64
- if (!directory) throw new Error("--data-dir requires a path.");
65
- storage = path.resolve(workingDirectory, directory);
66
- } else {
67
- forwarded.push(value);
68
- }
69
- }
70
- return { arguments: forwarded, storage };
71
- }
72
-
73
- async function download(url, destination) {
74
- const response = await fetch(url, { headers: { "user-agent": `clone/${packageMetadata.version}` } });
75
- if (!response.ok) throw new Error(`Download failed (${response.status}): ${url}`);
76
- const temporary = `${destination}.download`;
77
- fs.writeFileSync(temporary, Buffer.from(await response.arrayBuffer()), { mode: 0o755 });
78
- fs.renameSync(temporary, destination);
79
- }
80
-
81
- function checksum(file) {
82
- return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
83
- }
84
-
85
- async function binaryPath() {
86
- if (process.env.CLONE_BINARY_PATH) return path.resolve(process.env.CLONE_BINARY_PATH);
87
- const filename = target();
88
- const directory = path.join(cacheDirectory(), packageMetadata.version);
89
- const binary = path.join(directory, filename);
90
- const checksums = path.join(directory, "checksums.txt");
91
- fs.mkdirSync(directory, { recursive: true });
92
- const release = `${RELEASE_ROOT}/v${packageMetadata.version}`;
93
- if (!fs.existsSync(checksums)) await download(`${release}/checksums.txt`, checksums);
94
- const expectedLine = fs.readFileSync(checksums, "utf8").split(/\r?\n/).find((line) => line.endsWith(` ${filename}`));
95
- if (!expectedLine) throw new Error(`The release has no checksum for ${filename}.`);
96
- const expected = expectedLine.split(/\s+/)[0];
97
- if (!fs.existsSync(binary) || checksum(binary) !== expected) {
98
- fs.rmSync(binary, { force: true });
99
- await download(`${release}/${filename}`, binary);
100
- }
101
- if (checksum(binary) !== expected) {
102
- fs.rmSync(binary, { force: true });
103
- throw new Error(`Checksum verification failed for ${filename}.`);
104
- }
105
- if (process.platform !== "win32") fs.chmodSync(binary, 0o755);
106
- return binary;
107
- }
108
-
109
- function openBrowser(url) {
110
- const command = process.platform === "darwin" ? ["open", [url]]
111
- : process.platform === "win32" ? ["cmd", ["/d", "/s", "/c", "start", "", url]]
112
- : ["xdg-open", [url]];
113
- const child = spawn(command[0], command[1], { detached: true, stdio: "ignore" });
114
- child.unref();
115
- }
116
-
117
- async function waitForServer(url, child, errorState) {
118
- for (let attempt = 0; attempt < 80; attempt += 1) {
119
- if (errorState.value) throw errorState.value;
120
- if (child.exitCode !== null) throw new Error(`Clone stopped with exit code ${child.exitCode}.`);
121
- try {
122
- const response = await fetch(`${url}/api/health`);
123
- if (response.ok) return;
124
- } catch {}
125
- await new Promise((resolve) => setTimeout(resolve, 250));
126
- }
127
- throw new Error(`Clone did not start at ${url}.`);
128
- }
129
-
130
- function parseStartArguments(arguments_) {
131
- let port = process.env.PORT || "4173";
132
- let open = true;
133
- for (let index = 0; index < arguments_.length; index += 1) {
134
- const value = arguments_[index];
135
- if (value === "--no-open") open = false;
136
- else if (value === "--port") port = arguments_[++index];
137
- else if (value.startsWith("--port=")) port = value.slice(7);
138
- else throw new Error(`Unknown start option: ${value}`);
139
- }
140
- if (!/^\d+$/.test(String(port)) || Number(port) < 1 || Number(port) > 65535) throw new Error("Port must be between 1 and 65535.");
141
- if (BLOCKED_PORTS.has(Number(port))) throw new Error(`Port ${port} is blocked by web browsers.`);
142
- return { port: String(port), open };
143
- }
144
-
145
- function initialize(binary, database, storage) {
146
- if (fs.existsSync(database) || fs.existsSync(path.join(storage, "snapshot"))) return;
147
- fs.mkdirSync(storage, { recursive: true });
148
- const result = spawnSync(binary, ["--db", database, "init"], { encoding: "utf8" });
149
- if (result.error) throw result.error;
150
- if (result.status !== 0) {
151
- if (result.stdout) process.stdout.write(result.stdout);
152
- if (result.stderr) process.stderr.write(result.stderr);
153
- throw new Error(`Clone initialization failed with exit code ${result.status}.`);
154
- }
155
- }
156
-
157
- async function start(binary, arguments_, environment = process.env) {
158
- const options = parseStartArguments(arguments_);
159
- const storage = dataDirectory(process.platform, environment);
160
- const database = path.join(storage, "workspace.sqlite3");
161
- initialize(binary, database, storage);
162
- const url = `http://127.0.0.1:${options.port}`;
163
- const child = spawn(binary, ["--db", database, "serve"], {
164
- env: { ...environment, CLONE_HOST: "127.0.0.1", PORT: options.port },
165
- stdio: "inherit",
166
- });
167
- const errorState = { value: null };
168
- child.once("error", (error) => { errorState.value = error; });
169
- for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => child.kill(signal));
170
- try {
171
- await waitForServer(url, child, errorState);
172
- } catch (error) {
173
- child.kill("SIGTERM");
174
- throw error;
175
- }
176
- console.log(`Open ${url}. Stop with Ctrl+C.`);
177
- if (options.open) openBrowser(url);
178
- return new Promise((resolve) => {
179
- child.once("exit", (code, signal) => resolve(signal ? 1 : (code || 0)));
180
- });
181
- }
182
-
183
- function help(storage = null) {
184
- console.log(`Clone ${packageMetadata.version}
185
-
186
- Usage:
187
- clone [--local | --data-dir PATH] [start] [--port PORT] [--no-open]
188
- clone [--local | --data-dir PATH] <CLI command> [options]
189
-
190
- Examples:
191
- npx @andrestobelem/clone
192
- npx @andrestobelem/clone --local
193
- npx @andrestobelem/clone --data-dir ./project-data
194
- npx @andrestobelem/clone start --port 5000
195
- npx @andrestobelem/clone teams list --json
196
-
197
- Data: ${dataDirectory(process.platform, process.env, storage)}`);
198
- }
199
-
200
- async function main(arguments_ = process.argv.slice(2)) {
201
- const launcher = parseLauncherArguments(arguments_);
202
- arguments_ = launcher.arguments;
203
- if (arguments_.includes("--version") || arguments_.includes("-V")) {
204
- console.log(packageMetadata.version);
205
- return 0;
206
- }
207
- if (arguments_.includes("--help") || arguments_.includes("-h")) {
208
- help(launcher.storage);
209
- return 0;
210
- }
211
- const binary = await binaryPath();
212
- const command = arguments_[0];
213
- if (!command || command === "start") {
214
- const environment = launcher.storage ? { ...process.env, CLONE_DATA_DIR: launcher.storage } : process.env;
215
- return start(binary, command ? arguments_.slice(1) : arguments_, environment);
216
- }
217
- const storage = dataDirectory(process.platform, process.env, launcher.storage);
218
- const database = path.join(storage, "workspace.sqlite3");
219
- initialize(binary, database, storage);
220
- const result = spawnSync(binary, ["--db", database, ...arguments_], { stdio: "inherit" });
221
- if (result.error) throw result.error;
222
- return result.status || 0;
223
- }
224
-
225
- if (require.main === module) {
226
- main().then((code) => { process.exitCode = code; }).catch((error) => {
227
- console.error(`Clone: ${error.message}`);
228
- process.exitCode = 1;
229
- });
230
- }
231
-
232
- module.exports = { cacheDirectory, dataDirectory, parseLauncherArguments, parseStartArguments, target };
3
+ import("../dist/cli.js")
4
+ .then(({ main }) => main(process.argv.slice(2)))
5
+ .then(
6
+ (code) => {
7
+ process.exitCode = code;
8
+ },
9
+ (error) => {
10
+ console.error(error instanceof Error ? error.message : String(error));
11
+ process.exitCode = 4;
12
+ },
13
+ );