@fastmoss/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/README.md +72 -0
- package/bin/fastmoss.js +12 -0
- package/lib/runtime.js +274 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @fastmoss/cli
|
|
2
|
+
|
|
3
|
+
FastMoss CLI launcher for npm and npx.
|
|
4
|
+
|
|
5
|
+
This package does not bundle the Go binary itself. On first run it downloads the matching `fastmoss` binary from GitHub Releases, stores it in a local cache directory, and then forwards all CLI arguments to that binary.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Run without installing:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @fastmoss/cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install globally:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @fastmoss/cli
|
|
19
|
+
fastmoss
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Common examples:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
fastmoss tools
|
|
26
|
+
fastmoss call --tool creator_search --args '{"keywords":"beauty","region":"US","page":1,"pagesize":10}'
|
|
27
|
+
fastmoss stdio
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Supported Platforms
|
|
31
|
+
|
|
32
|
+
- macOS `amd64`
|
|
33
|
+
- macOS `arm64`
|
|
34
|
+
- Linux `amd64`
|
|
35
|
+
- Linux `arm64`
|
|
36
|
+
- Windows `amd64`
|
|
37
|
+
|
|
38
|
+
## Cache Directory
|
|
39
|
+
|
|
40
|
+
The downloaded binary is cached here by default:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
~/.fastmoss/bin/<version>/<platform>/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can override the cache directory:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
FASTMOSS_CACHE_DIR=/custom/cache/dir npx @fastmoss/cli
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Download Source
|
|
53
|
+
|
|
54
|
+
By default the wrapper downloads binaries from the public GitHub release repository configured in `package.json`.
|
|
55
|
+
|
|
56
|
+
For internal debugging or private release mirrors, you can override the base URL:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
FASTMOSS_DOWNLOAD_BASE_URL=https://downloads.example.com/releases npx @fastmoss/cli
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The wrapper will request one of these asset names depending on platform:
|
|
63
|
+
|
|
64
|
+
- `fastmoss-darwin-amd64`
|
|
65
|
+
- `fastmoss-darwin-arm64`
|
|
66
|
+
- `fastmoss-linux-amd64`
|
|
67
|
+
- `fastmoss-linux-arm64`
|
|
68
|
+
- `fastmoss-windows-amd64.exe`
|
|
69
|
+
|
|
70
|
+
## Release Workflow
|
|
71
|
+
|
|
72
|
+
The private source repository prepares this package and the matching binaries. The public GitHub release repository then publishes the package and hosts the release assets.
|
package/bin/fastmoss.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const packageJSON = require("../package.json");
|
|
4
|
+
const { runCLI } = require("../lib/runtime");
|
|
5
|
+
|
|
6
|
+
runCLI({
|
|
7
|
+
version: packageJSON.version,
|
|
8
|
+
configuredDownloadBaseURL: packageJSON.fastmoss?.downloadBaseURL,
|
|
9
|
+
}).catch((error) => {
|
|
10
|
+
process.stderr.write(`fastmoss wrapper error: ${error.message}\n`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/lib/runtime.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const http = require("node:http");
|
|
5
|
+
const https = require("node:https");
|
|
6
|
+
const { pipeline } = require("node:stream/promises");
|
|
7
|
+
const { spawn } = require("node:child_process");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_DOWNLOAD_BASE_URL =
|
|
10
|
+
"https://github.com/FastMoss/cli/releases/download";
|
|
11
|
+
|
|
12
|
+
const PLATFORM_TARGETS = {
|
|
13
|
+
"darwin:x64": {
|
|
14
|
+
assetName: "fastmoss-darwin-amd64",
|
|
15
|
+
binaryName: "fastmoss",
|
|
16
|
+
cacheKey: "darwin-amd64",
|
|
17
|
+
},
|
|
18
|
+
"darwin:arm64": {
|
|
19
|
+
assetName: "fastmoss-darwin-arm64",
|
|
20
|
+
binaryName: "fastmoss",
|
|
21
|
+
cacheKey: "darwin-arm64",
|
|
22
|
+
},
|
|
23
|
+
"linux:x64": {
|
|
24
|
+
assetName: "fastmoss-linux-amd64",
|
|
25
|
+
binaryName: "fastmoss",
|
|
26
|
+
cacheKey: "linux-amd64",
|
|
27
|
+
},
|
|
28
|
+
"linux:arm64": {
|
|
29
|
+
assetName: "fastmoss-linux-arm64",
|
|
30
|
+
binaryName: "fastmoss",
|
|
31
|
+
cacheKey: "linux-arm64",
|
|
32
|
+
},
|
|
33
|
+
"win32:x64": {
|
|
34
|
+
assetName: "fastmoss-windows-amd64.exe",
|
|
35
|
+
binaryName: "fastmoss.exe",
|
|
36
|
+
cacheKey: "windows-amd64",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function resolvePlatformTarget({
|
|
41
|
+
platform = process.platform,
|
|
42
|
+
arch = process.arch,
|
|
43
|
+
} = {}) {
|
|
44
|
+
const key = `${platform}:${arch}`;
|
|
45
|
+
const target = PLATFORM_TARGETS[key];
|
|
46
|
+
if (!target) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Unsupported platform: ${platform}/${arch}. Supported targets: ${Object.keys(
|
|
49
|
+
PLATFORM_TARGETS,
|
|
50
|
+
)
|
|
51
|
+
.map((value) => value.replace(":", "/"))
|
|
52
|
+
.join(", ")}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return target;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveCacheRoot({
|
|
59
|
+
env = process.env,
|
|
60
|
+
homeDir = os.homedir(),
|
|
61
|
+
} = {}) {
|
|
62
|
+
const override = String(env.FASTMOSS_CACHE_DIR || "").trim();
|
|
63
|
+
if (override !== "") {
|
|
64
|
+
return override;
|
|
65
|
+
}
|
|
66
|
+
return path.join(homeDir, ".fastmoss", "bin");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveBinaryPath({ cacheRoot, version, target }) {
|
|
70
|
+
return path.join(cacheRoot, version, target.cacheKey, target.binaryName);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveDownloadBaseURL({
|
|
74
|
+
env = process.env,
|
|
75
|
+
configuredDownloadBaseURL = "",
|
|
76
|
+
} = {}) {
|
|
77
|
+
const envOverride = String(env.FASTMOSS_DOWNLOAD_BASE_URL || "").trim();
|
|
78
|
+
if (envOverride !== "") {
|
|
79
|
+
return envOverride.replace(/\/+$/, "");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const configured = String(configuredDownloadBaseURL || "").trim();
|
|
83
|
+
if (configured !== "") {
|
|
84
|
+
return configured.replace(/\/+$/, "");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return DEFAULT_DOWNLOAD_BASE_URL;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildDownloadURL({
|
|
91
|
+
version,
|
|
92
|
+
target,
|
|
93
|
+
env = process.env,
|
|
94
|
+
configuredDownloadBaseURL = "",
|
|
95
|
+
}) {
|
|
96
|
+
const baseURL = resolveDownloadBaseURL({ env, configuredDownloadBaseURL });
|
|
97
|
+
return `${baseURL}/v${version}/${target.assetName}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function ensureBinary({
|
|
101
|
+
version,
|
|
102
|
+
env = process.env,
|
|
103
|
+
platform = process.platform,
|
|
104
|
+
arch = process.arch,
|
|
105
|
+
homeDir = os.homedir(),
|
|
106
|
+
configuredDownloadBaseURL = "",
|
|
107
|
+
}) {
|
|
108
|
+
const target = resolvePlatformTarget({ platform, arch });
|
|
109
|
+
const cacheRoot = resolveCacheRoot({ env, homeDir });
|
|
110
|
+
const binaryPath = resolveBinaryPath({ cacheRoot, version, target });
|
|
111
|
+
|
|
112
|
+
if (await fileExists(binaryPath)) {
|
|
113
|
+
return { binaryPath, downloaded: false };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await fs.promises.mkdir(path.dirname(binaryPath), { recursive: true });
|
|
117
|
+
const downloadURL = buildDownloadURL({
|
|
118
|
+
version,
|
|
119
|
+
target,
|
|
120
|
+
env,
|
|
121
|
+
configuredDownloadBaseURL,
|
|
122
|
+
});
|
|
123
|
+
const tempPath = `${binaryPath}.download`;
|
|
124
|
+
|
|
125
|
+
await downloadFile(downloadURL, tempPath);
|
|
126
|
+
|
|
127
|
+
if (platform !== "win32") {
|
|
128
|
+
await fs.promises.chmod(tempPath, 0o755);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await fs.promises.rename(tempPath, binaryPath);
|
|
132
|
+
return { binaryPath, downloaded: true, downloadURL };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function fileExists(filePath) {
|
|
136
|
+
try {
|
|
137
|
+
const stat = await fs.promises.stat(filePath);
|
|
138
|
+
return stat.isFile() && stat.size > 0;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error && error.code === "ENOENT") {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function downloadFile(url, destination) {
|
|
148
|
+
try {
|
|
149
|
+
await downloadWithRedirects(url, destination, 0);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
await fs.promises.rm(destination, { force: true }).catch(() => {});
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function downloadWithRedirects(url, destination, redirectCount) {
|
|
157
|
+
if (redirectCount > 5) {
|
|
158
|
+
return Promise.reject(new Error("Too many redirects while downloading fastmoss"));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const client = url.startsWith("https://") ? https : http;
|
|
163
|
+
const request = client.get(
|
|
164
|
+
url,
|
|
165
|
+
{
|
|
166
|
+
headers: {
|
|
167
|
+
"User-Agent": "fastmoss-npm-wrapper",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
async (response) => {
|
|
171
|
+
const statusCode = response.statusCode || 0;
|
|
172
|
+
|
|
173
|
+
if (
|
|
174
|
+
statusCode >= 300 &&
|
|
175
|
+
statusCode < 400 &&
|
|
176
|
+
response.headers.location
|
|
177
|
+
) {
|
|
178
|
+
response.resume();
|
|
179
|
+
try {
|
|
180
|
+
await downloadWithRedirects(
|
|
181
|
+
response.headers.location,
|
|
182
|
+
destination,
|
|
183
|
+
redirectCount + 1,
|
|
184
|
+
);
|
|
185
|
+
resolve();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
reject(error);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (statusCode !== 200) {
|
|
193
|
+
response.resume();
|
|
194
|
+
reject(
|
|
195
|
+
new Error(
|
|
196
|
+
`Failed to download fastmoss binary: HTTP ${statusCode} from ${url}`,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const output = fs.createWriteStream(destination, { mode: 0o755 });
|
|
203
|
+
try {
|
|
204
|
+
await pipeline(response, output);
|
|
205
|
+
resolve();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
reject(error);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
request.on("error", reject);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function runCLI({
|
|
217
|
+
version,
|
|
218
|
+
args = process.argv.slice(2),
|
|
219
|
+
env = process.env,
|
|
220
|
+
platform = process.platform,
|
|
221
|
+
arch = process.arch,
|
|
222
|
+
homeDir = os.homedir(),
|
|
223
|
+
stdout = process.stdout,
|
|
224
|
+
stderr = process.stderr,
|
|
225
|
+
configuredDownloadBaseURL = "",
|
|
226
|
+
} = {}) {
|
|
227
|
+
if (
|
|
228
|
+
args.length === 1 &&
|
|
229
|
+
["--version", "-v", "version"].includes(args[0])
|
|
230
|
+
) {
|
|
231
|
+
stdout.write(`${version}\n`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const { binaryPath, downloaded, downloadURL } = await ensureBinary({
|
|
236
|
+
version,
|
|
237
|
+
env,
|
|
238
|
+
platform,
|
|
239
|
+
arch,
|
|
240
|
+
homeDir,
|
|
241
|
+
configuredDownloadBaseURL,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (downloaded) {
|
|
245
|
+
stderr.write(`Downloading fastmoss ${version} from ${downloadURL}\n`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await new Promise((resolve, reject) => {
|
|
249
|
+
const child = spawn(binaryPath, args, {
|
|
250
|
+
stdio: ["inherit", stdout, stderr],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
child.on("error", reject);
|
|
254
|
+
child.on("exit", (code, signal) => {
|
|
255
|
+
if (signal) {
|
|
256
|
+
reject(new Error(`fastmoss exited with signal ${signal}`));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
process.exitCode = code || 0;
|
|
260
|
+
resolve();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
DEFAULT_DOWNLOAD_BASE_URL,
|
|
267
|
+
buildDownloadURL,
|
|
268
|
+
ensureBinary,
|
|
269
|
+
resolveBinaryPath,
|
|
270
|
+
resolveCacheRoot,
|
|
271
|
+
resolveDownloadBaseURL,
|
|
272
|
+
resolvePlatformTarget,
|
|
273
|
+
runCLI,
|
|
274
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fastmoss/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "FastMoss CLI launcher for npm and npx",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"private": false,
|
|
7
|
+
"bin": {
|
|
8
|
+
"fastmoss": "./bin/fastmoss.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"lib",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test ./test/*.test.js"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"fastmoss": {
|
|
22
|
+
"downloadBaseURL": "https://github.com/FastMoss/cli/releases/download"
|
|
23
|
+
}
|
|
24
|
+
}
|