@peerbit/shared-fs-cli 0.0.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.
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import chalk from "chalk";
4
+ import { installNativeAdapter } from "./native-adapter.js";
5
+
6
+ const takeValue = (args: string[], name: string) => {
7
+ const index = args.indexOf(name);
8
+ if (index === -1) {
9
+ return undefined;
10
+ }
11
+ return args[index + 1];
12
+ };
13
+
14
+ const hasFlag = (args: string[], name: string) => args.includes(name);
15
+
16
+ const args = process.argv.slice(2);
17
+
18
+ installNativeAdapter({
19
+ installDir: takeValue(args, "--prefix"),
20
+ version: takeValue(args, "--version"),
21
+ baseUrl: takeValue(args, "--base-url"),
22
+ force: hasFlag(args, "--force"),
23
+ ifNeeded: hasFlag(args, "--if-needed"),
24
+ })
25
+ .then((result) => {
26
+ if (hasFlag(args, "--print-path")) {
27
+ console.log(result.binaryPath);
28
+ return;
29
+ }
30
+ if (hasFlag(args, "--quiet")) {
31
+ return;
32
+ }
33
+ if (result.installed) {
34
+ console.log(
35
+ chalk.green(`Installed native adapter at ${result.binaryPath}`)
36
+ );
37
+ return;
38
+ }
39
+ console.log(
40
+ chalk.gray(
41
+ `Native adapter already installed at ${result.binaryPath}`
42
+ )
43
+ );
44
+ })
45
+ .catch((error) => {
46
+ console.error(error instanceof Error ? error.message : String(error));
47
+ process.exitCode = 1;
48
+ });
@@ -0,0 +1,434 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import fsp from "node:fs/promises";
4
+ import http from "node:http";
5
+ import https from "node:https";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { pipeline } from "node:stream/promises";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const CLI_PACKAGE_NAME = "@peerbit/shared-fs-cli";
12
+ const DEFAULT_RELEASE_REPOSITORY = "dao-xyz/peerbit-examples";
13
+ const DEFAULT_PATH_COMMAND = "peerbit-shared-fs-native";
14
+
15
+ export type NativeAdapterTarget = {
16
+ id: string;
17
+ platform: NodeJS.Platform;
18
+ arch: NodeJS.Architecture;
19
+ archiveExtension: "tar.gz" | "zip";
20
+ binaryName: string;
21
+ };
22
+
23
+ export type ResolveNativeAdapterOptions = {
24
+ env?: NodeJS.ProcessEnv;
25
+ installDir?: string;
26
+ platform?: NodeJS.Platform;
27
+ commandExists?: (command: string) => Promise<boolean>;
28
+ };
29
+
30
+ export type InstallNativeAdapterOptions = {
31
+ installDir?: string;
32
+ platform?: NodeJS.Platform;
33
+ arch?: NodeJS.Architecture;
34
+ version?: string;
35
+ baseUrl?: string;
36
+ force?: boolean;
37
+ ifNeeded?: boolean;
38
+ };
39
+
40
+ export type InstallNativeAdapterResult = {
41
+ binaryPath: string;
42
+ installed: boolean;
43
+ skippedReason?: "already-installed";
44
+ target: NativeAdapterTarget;
45
+ assetName: string;
46
+ url: string;
47
+ };
48
+
49
+ export class NativeAdapterInstallError extends Error {
50
+ constructor(message: string) {
51
+ super(message);
52
+ this.name = "NativeAdapterInstallError";
53
+ }
54
+ }
55
+
56
+ export const nativeAdapterBinaryName = (
57
+ platform: NodeJS.Platform = process.platform
58
+ ) =>
59
+ platform === "win32" ? `${DEFAULT_PATH_COMMAND}.exe` : DEFAULT_PATH_COMMAND;
60
+
61
+ export const getNativeAdapterTarget = (
62
+ platform: NodeJS.Platform = process.platform,
63
+ arch: NodeJS.Architecture = process.arch
64
+ ): NativeAdapterTarget => {
65
+ if (platform !== "linux" && platform !== "darwin" && platform !== "win32") {
66
+ throw new NativeAdapterInstallError(
67
+ `No prebuilt native adapter target for platform ${platform}.`
68
+ );
69
+ }
70
+ if (arch !== "x64" && arch !== "arm64") {
71
+ throw new NativeAdapterInstallError(
72
+ `No prebuilt native adapter target for architecture ${arch}.`
73
+ );
74
+ }
75
+
76
+ return {
77
+ id: `${platform}-${arch}`,
78
+ platform,
79
+ arch,
80
+ archiveExtension: platform === "win32" ? "zip" : "tar.gz",
81
+ binaryName: nativeAdapterBinaryName(platform),
82
+ };
83
+ };
84
+
85
+ export const nativeAdapterAssetName = (target: NativeAdapterTarget) =>
86
+ `peerbit-shared-fs-native-${target.id}.${target.archiveExtension}`;
87
+
88
+ export const nativeAdapterReleaseTag = (version: string) =>
89
+ version.startsWith("shared-fs-native-v")
90
+ ? version
91
+ : `shared-fs-native-v${version.replace(/^v/, "")}`;
92
+
93
+ export const nativeAdapterDownloadBaseUrl = (tag: string) =>
94
+ `https://github.com/${DEFAULT_RELEASE_REPOSITORY}/releases/download/${tag}`;
95
+
96
+ export const nativeAdapterDownloadUrl = (options: {
97
+ assetName: string;
98
+ baseUrl?: string;
99
+ tag: string;
100
+ }) => {
101
+ const baseUrl =
102
+ options.baseUrl ?? nativeAdapterDownloadBaseUrl(options.tag);
103
+ return `${baseUrl.replace(/\/$/, "")}/${options.assetName}`;
104
+ };
105
+
106
+ export const defaultNativeAdapterInstallDir = (
107
+ env: NodeJS.ProcessEnv = process.env
108
+ ) =>
109
+ env.PEERBIT_SHARED_FS_NATIVE_INSTALL_DIR ||
110
+ path.join(os.homedir(), ".peerbit", "shared-fs", "bin");
111
+
112
+ export const defaultNativeAdapterPath = (
113
+ options: {
114
+ env?: NodeJS.ProcessEnv;
115
+ installDir?: string;
116
+ platform?: NodeJS.Platform;
117
+ } = {}
118
+ ) =>
119
+ path.join(
120
+ options.installDir ??
121
+ defaultNativeAdapterInstallDir(options.env ?? process.env),
122
+ nativeAdapterBinaryName(options.platform ?? process.platform)
123
+ );
124
+
125
+ const pathExists = async (candidate: string) => {
126
+ try {
127
+ await fsp.access(candidate, fs.constants.F_OK);
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ };
133
+
134
+ const executablePathExists = async (candidate: string) => {
135
+ try {
136
+ await fsp.access(candidate, fs.constants.X_OK);
137
+ return true;
138
+ } catch {
139
+ return process.platform === "win32" && (await pathExists(candidate));
140
+ }
141
+ };
142
+
143
+ const isPathLikeCommand = (command: string) =>
144
+ path.isAbsolute(command) || command.includes("/") || command.includes("\\");
145
+
146
+ export const commandExistsOnPath = async (
147
+ command: string,
148
+ options: {
149
+ env?: NodeJS.ProcessEnv;
150
+ platform?: NodeJS.Platform;
151
+ } = {}
152
+ ) => {
153
+ const env = options.env ?? process.env;
154
+ const platform = options.platform ?? process.platform;
155
+ if (isPathLikeCommand(command)) {
156
+ return executablePathExists(command);
157
+ }
158
+
159
+ const pathValue = env.PATH;
160
+ if (!pathValue) {
161
+ return false;
162
+ }
163
+
164
+ const extensions =
165
+ platform === "win32"
166
+ ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)
167
+ : [""];
168
+ const commandNames =
169
+ platform === "win32" && path.extname(command) === ""
170
+ ? [
171
+ command,
172
+ ...extensions.map((extension) => `${command}${extension}`),
173
+ ]
174
+ : [command];
175
+
176
+ for (const directory of pathValue.split(path.delimiter)) {
177
+ for (const commandName of commandNames) {
178
+ if (await executablePathExists(path.join(directory, commandName))) {
179
+ return true;
180
+ }
181
+ }
182
+ }
183
+ return false;
184
+ };
185
+
186
+ export const resolveExternalNativeAdapter = async (
187
+ explicitCommand?: string,
188
+ options: ResolveNativeAdapterOptions = {}
189
+ ) => {
190
+ const env = options.env ?? process.env;
191
+ const platform = options.platform ?? process.platform;
192
+ const commandExists =
193
+ options.commandExists ??
194
+ ((command: string) => commandExistsOnPath(command, { env, platform }));
195
+
196
+ if (explicitCommand?.trim()) {
197
+ return explicitCommand;
198
+ }
199
+
200
+ if (env.PEERBIT_SHARED_FS_NATIVE_ADAPTER?.trim()) {
201
+ return env.PEERBIT_SHARED_FS_NATIVE_ADAPTER;
202
+ }
203
+
204
+ const managedPath = defaultNativeAdapterPath({
205
+ env,
206
+ installDir: options.installDir,
207
+ platform,
208
+ });
209
+ if (await pathExists(managedPath)) {
210
+ return managedPath;
211
+ }
212
+
213
+ if (await commandExists(DEFAULT_PATH_COMMAND)) {
214
+ return DEFAULT_PATH_COMMAND;
215
+ }
216
+
217
+ return undefined;
218
+ };
219
+
220
+ const readCliPackageVersion = async () => {
221
+ let directory = path.dirname(fileURLToPath(import.meta.url));
222
+ while (true) {
223
+ const packagePath = path.join(directory, "package.json");
224
+ try {
225
+ const parsed = JSON.parse(
226
+ await fsp.readFile(packagePath, "utf8")
227
+ ) as {
228
+ name?: string;
229
+ version?: string;
230
+ };
231
+ if (parsed.name === CLI_PACKAGE_NAME && parsed.version) {
232
+ return parsed.version;
233
+ }
234
+ } catch {}
235
+
236
+ const parent = path.dirname(directory);
237
+ if (parent === directory) {
238
+ throw new NativeAdapterInstallError(
239
+ `Unable to find ${CLI_PACKAGE_NAME} package version.`
240
+ );
241
+ }
242
+ directory = parent;
243
+ }
244
+ };
245
+
246
+ const downloadFile = async (
247
+ url: string,
248
+ destination: string,
249
+ redirectBudget = 5
250
+ ): Promise<void> => {
251
+ const parsedUrl = new URL(url);
252
+ const client = parsedUrl.protocol === "http:" ? http : https;
253
+ await new Promise<void>((resolve, reject) => {
254
+ const request = client.get(
255
+ parsedUrl,
256
+ {
257
+ headers: {
258
+ "user-agent": `${CLI_PACKAGE_NAME} native-adapter-installer`,
259
+ },
260
+ },
261
+ (response) => {
262
+ const statusCode = response.statusCode ?? 0;
263
+ const location = response.headers.location;
264
+ if (
265
+ statusCode >= 300 &&
266
+ statusCode < 400 &&
267
+ location &&
268
+ redirectBudget > 0
269
+ ) {
270
+ response.resume();
271
+ downloadFile(
272
+ new URL(location, parsedUrl).toString(),
273
+ destination,
274
+ redirectBudget - 1
275
+ ).then(resolve, reject);
276
+ return;
277
+ }
278
+
279
+ if (statusCode !== 200) {
280
+ response.resume();
281
+ reject(
282
+ new NativeAdapterInstallError(
283
+ `Download failed with HTTP ${statusCode}: ${url}`
284
+ )
285
+ );
286
+ return;
287
+ }
288
+
289
+ pipeline(response, fs.createWriteStream(destination)).then(
290
+ resolve,
291
+ reject
292
+ );
293
+ }
294
+ );
295
+ request.on("error", reject);
296
+ });
297
+ };
298
+
299
+ const runProcess = async (command: string, args: string[]) => {
300
+ await new Promise<void>((resolve, reject) => {
301
+ const child = spawn(command, args, {
302
+ stdio: ["ignore", "ignore", "pipe"],
303
+ });
304
+ let stderr = "";
305
+ child.stderr.on("data", (chunk) => {
306
+ stderr += chunk.toString("utf8");
307
+ });
308
+ child.once("error", reject);
309
+ child.once("exit", (code, signal) => {
310
+ if (code === 0) {
311
+ resolve();
312
+ return;
313
+ }
314
+ reject(
315
+ new NativeAdapterInstallError(
316
+ `${command} ${args.join(" ")} failed with code=${code} signal=${signal}: ${stderr.trim()}`
317
+ )
318
+ );
319
+ });
320
+ });
321
+ };
322
+
323
+ const extractArchive = async (
324
+ archivePath: string,
325
+ outputDirectory: string,
326
+ target: NativeAdapterTarget
327
+ ) => {
328
+ if (target.archiveExtension === "zip") {
329
+ const quotePowerShell = (value: string) =>
330
+ `'${value.replace(/'/g, "''")}'`;
331
+ await runProcess("powershell.exe", [
332
+ "-NoProfile",
333
+ "-ExecutionPolicy",
334
+ "Bypass",
335
+ "-Command",
336
+ `Expand-Archive -LiteralPath ${quotePowerShell(
337
+ archivePath
338
+ )} -DestinationPath ${quotePowerShell(outputDirectory)} -Force`,
339
+ ]);
340
+ return;
341
+ }
342
+ await runProcess("tar", ["-xzf", archivePath, "-C", outputDirectory]);
343
+ };
344
+
345
+ const findExtractedBinary = async (
346
+ directory: string,
347
+ binaryName: string
348
+ ): Promise<string | undefined> => {
349
+ const entries = await fsp.readdir(directory, { withFileTypes: true });
350
+ for (const entry of entries) {
351
+ const entryPath = path.join(directory, entry.name);
352
+ if (
353
+ entry.isFile() &&
354
+ entry.name.toLowerCase() === binaryName.toLowerCase()
355
+ ) {
356
+ return entryPath;
357
+ }
358
+ if (entry.isDirectory()) {
359
+ const found = await findExtractedBinary(entryPath, binaryName);
360
+ if (found) {
361
+ return found;
362
+ }
363
+ }
364
+ }
365
+ return undefined;
366
+ };
367
+
368
+ export const installNativeAdapter = async (
369
+ options: InstallNativeAdapterOptions = {}
370
+ ): Promise<InstallNativeAdapterResult> => {
371
+ const target = getNativeAdapterTarget(options.platform, options.arch);
372
+ const installDir =
373
+ options.installDir ?? defaultNativeAdapterInstallDir(process.env);
374
+ const binaryPath = path.join(installDir, target.binaryName);
375
+ const existing = await pathExists(binaryPath);
376
+ const version =
377
+ options.version ??
378
+ process.env.PEERBIT_SHARED_FS_NATIVE_VERSION ??
379
+ (await readCliPackageVersion());
380
+ const tag = nativeAdapterReleaseTag(version);
381
+ const assetName = nativeAdapterAssetName(target);
382
+ const url = nativeAdapterDownloadUrl({
383
+ assetName,
384
+ baseUrl:
385
+ options.baseUrl ??
386
+ process.env.PEERBIT_SHARED_FS_NATIVE_RELEASE_BASE_URL,
387
+ tag,
388
+ });
389
+
390
+ if (existing && (options.ifNeeded || !options.force)) {
391
+ return {
392
+ binaryPath,
393
+ installed: false,
394
+ skippedReason: "already-installed",
395
+ target,
396
+ assetName,
397
+ url,
398
+ };
399
+ }
400
+
401
+ const tempDirectory = await fsp.mkdtemp(
402
+ path.join(os.tmpdir(), "peerbit-shared-fs-native-")
403
+ );
404
+ try {
405
+ const archivePath = path.join(tempDirectory, assetName);
406
+ await downloadFile(url, archivePath);
407
+ await extractArchive(archivePath, tempDirectory, target);
408
+ const extractedBinary = await findExtractedBinary(
409
+ tempDirectory,
410
+ target.binaryName
411
+ );
412
+ if (!extractedBinary) {
413
+ throw new NativeAdapterInstallError(
414
+ `Archive ${assetName} did not contain ${target.binaryName}.`
415
+ );
416
+ }
417
+
418
+ await fsp.mkdir(installDir, { recursive: true });
419
+ await fsp.copyFile(extractedBinary, binaryPath);
420
+ if (target.platform !== "win32") {
421
+ await fsp.chmod(binaryPath, 0o755);
422
+ }
423
+
424
+ return {
425
+ binaryPath,
426
+ installed: true,
427
+ target,
428
+ assetName,
429
+ url,
430
+ };
431
+ } finally {
432
+ await fsp.rm(tempDirectory, { recursive: true, force: true });
433
+ }
434
+ };