@servelink/serve 0.7.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 +32 -0
- package/bin/serve.js +29 -0
- package/install.js +184 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @servelink/serve
|
|
2
|
+
|
|
3
|
+
Public tunnel for hosted serve accounts. This package is a thin wrapper: on
|
|
4
|
+
install (postinstall) it downloads the native `serve` CLI binary for your
|
|
5
|
+
platform from the public [servelink-swyftlabs/serve-dist](https://github.com/servelink-swyftlabs/serve-dist)
|
|
6
|
+
releases, verifies its sha256 against `checksums.txt`, and extracts it. The
|
|
7
|
+
binary that runs is exactly the CI-published GoReleaser artifact — no Go code
|
|
8
|
+
is compiled here.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm i -g @servelink/serve
|
|
14
|
+
# or, without a global install:
|
|
15
|
+
npx @servelink/serve
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Use
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
serve login # authorize this device (prints a dashboard URL + code)
|
|
22
|
+
serve # run the MCP server for AI coding agents
|
|
23
|
+
serve mcp # explicit MCP form
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The primary interface is **MCP**: with a bare `serve` registered in your AI
|
|
27
|
+
coding agent, the agent can "serve me this" — a file, a directory, or an
|
|
28
|
+
already-running dev server — and hand back a public HTTPS URL.
|
|
29
|
+
|
|
30
|
+
See the README at the repository root for full onboarding. If your platform is
|
|
31
|
+
unsupported by npm, install via the [Homebrew tap](https://github.com/servelink-swyftlabs/homebrew-tap)
|
|
32
|
+
or a release archive from serve-dist.
|
package/bin/serve.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// @servelink/serve — CLI entrypoint. Finds the native binary installed by
|
|
5
|
+
// install.js and spawns it, forwarding args and inheriting stdio so `serve <port>`
|
|
6
|
+
// and `serve login` behave exactly like the Go CLI.
|
|
7
|
+
|
|
8
|
+
const { spawnSync } = require("node:child_process");
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
|
|
12
|
+
const nativeDir = path.join(__dirname, "..", "native");
|
|
13
|
+
const binName = process.platform === "win32" ? "serve.exe" : "serve";
|
|
14
|
+
const binPath = path.join(nativeDir, binName);
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(binPath)) {
|
|
17
|
+
console.error(
|
|
18
|
+
"serve: native binary not found (expected " + binPath + ").\n" +
|
|
19
|
+
"Reinstall it with: npm rebuild @servelink/serve"
|
|
20
|
+
);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const res = spawnSync(binPath, process.argv.slice(2), { stdio: "inherit" });
|
|
25
|
+
if (res.error) {
|
|
26
|
+
console.error("serve: failed to launch native binary: " + res.error.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
process.exit(res.status === null ? 1 : res.status);
|
package/install.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// @servelink/serve — postinstall.
|
|
5
|
+
// Downloads the matching-platform `serve` client binary from the public
|
|
6
|
+
// servelink-swyftlabs/serve-dist releases repo, verifies it against checksums.txt, and
|
|
7
|
+
// extracts it into ./native so bin/serve.js can invoke it. The binary that runs
|
|
8
|
+
// is exactly the GoReleaser artifact — this package contains no Go code.
|
|
9
|
+
|
|
10
|
+
const crypto = require("node:crypto");
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const https = require("node:https");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
const zlib = require("node:zlib");
|
|
15
|
+
|
|
16
|
+
const REPO = "servelink-swyftlabs/serve-dist";
|
|
17
|
+
const DIST = "https://github.com/" + REPO + "/releases/download";
|
|
18
|
+
|
|
19
|
+
function resolveVersion() {
|
|
20
|
+
const pkg = require("./package.json");
|
|
21
|
+
const v = pkg.version;
|
|
22
|
+
if (!v || v === "0.0.0") {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"@servelink/serve: package version must be set before publish. " +
|
|
25
|
+
"CI publishes with the GoReleaser tag as the npm version (see release.yml)."
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Nothing is ever silently skipped: an unsupported platform fails loudly and
|
|
32
|
+
// points at the manual install paths.
|
|
33
|
+
function resolvePlatform() {
|
|
34
|
+
const osMap = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
35
|
+
const archMap = { x64: "amd64", arm64: "arm64" };
|
|
36
|
+
const os = osMap[process.platform];
|
|
37
|
+
const arch = archMap[process.arch];
|
|
38
|
+
if (!os || !arch) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"@servelink/serve: unsupported platform " + process.platform + "/" + process.arch + ". " +
|
|
41
|
+
"Install via the Homebrew tap or the release archive from " +
|
|
42
|
+
"https://github.com/" + REPO + "/releases"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
// GoReleaser serves client archives for darwin/linux (amd64+arm64) and
|
|
46
|
+
// windows/amd64. Anything else has no published artifact.
|
|
47
|
+
if (os === "windows" && arch !== "amd64") {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"@servelink/serve: no published binary for windows/" + arch + ". " +
|
|
50
|
+
"Install via the release archive from https://github.com/" + REPO + "/releases"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return { os, arch };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parses a GoReleaser checksums.txt (one "<sha256> <filename>" per line) and
|
|
57
|
+
// returns the expected sha256 hex for filename, or null if absent.
|
|
58
|
+
function checksumFor(filename, checksumsText) {
|
|
59
|
+
for (const raw of checksumsText.split(/\r?\n/)) {
|
|
60
|
+
const line = raw.trim();
|
|
61
|
+
if (!line) continue;
|
|
62
|
+
const m = /^([0-9a-fA-F]{64})\s+(\S+)\s*$/.exec(line);
|
|
63
|
+
if (m && m[2] === filename) return m[1].toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function verifySha256(data, expectedHex) {
|
|
69
|
+
return crypto.createHash("sha256").update(data).digest("hex") === expectedHex;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function download(url, redirects) {
|
|
73
|
+
redirects = redirects || 0;
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const req = https.get(url, { headers: { "User-Agent": "@servelink/serve-installer" } }, (res) => {
|
|
76
|
+
const status = res.statusCode;
|
|
77
|
+
if (status >= 300 && status < 400 && res.headers.location && redirects < 5) {
|
|
78
|
+
res.resume();
|
|
79
|
+
download(new URL(res.headers.location, url).toString(), redirects + 1).then(resolve, reject);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (status !== 200) {
|
|
83
|
+
res.resume();
|
|
84
|
+
reject(new Error("HTTP " + status + " while downloading " + url));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const chunks = [];
|
|
88
|
+
res.on("data", (c) => chunks.push(c));
|
|
89
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
90
|
+
res.on("error", reject);
|
|
91
|
+
});
|
|
92
|
+
req.on("error", reject);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Minimal ustar tar reader over an already-gunzipped buffer. Only regular-file
|
|
97
|
+
// entries are kept. GoReleaser archives use short top-level names (serve,
|
|
98
|
+
// LICENSE, README.md), so PAX/GNU long-name extensions are not needed.
|
|
99
|
+
function readTarFiles(buf) {
|
|
100
|
+
const files = new Map();
|
|
101
|
+
let offset = 0;
|
|
102
|
+
const readStr = (o, len) => {
|
|
103
|
+
let end = o;
|
|
104
|
+
while (end < o + len && buf[end] !== 0) end++;
|
|
105
|
+
return buf.toString("utf8", o, end);
|
|
106
|
+
};
|
|
107
|
+
while (offset + 512 <= buf.length) {
|
|
108
|
+
const base = offset; // absolute offset of the current 512-byte header
|
|
109
|
+
const name = readStr(base, 100);
|
|
110
|
+
if (name.length === 0) break; // end-of-archive marker (zero block)
|
|
111
|
+
const size = parseInt(readStr(base + 124, 12).trim(), 8) || 0;
|
|
112
|
+
const typeflag = String.fromCharCode(buf[base + 156]);
|
|
113
|
+
const dataStart = base + 512;
|
|
114
|
+
offset = dataStart + Math.ceil(size / 512) * 512;
|
|
115
|
+
if (typeflag === "0" || typeflag === "" || typeflag === "\u0000") {
|
|
116
|
+
files.set(name, buf.subarray(dataStart, dataStart + size));
|
|
117
|
+
}
|
|
118
|
+
// Directories, symlinks, PAX headers, etc. are intentionally ignored.
|
|
119
|
+
}
|
|
120
|
+
return files;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function run() {
|
|
124
|
+
const version = resolveVersion();
|
|
125
|
+
const { os, arch } = resolvePlatform();
|
|
126
|
+
|
|
127
|
+
const archiveName = "serve_" + version + "_" + os + "_" + arch + ".tar.gz";
|
|
128
|
+
const distDir = path.join(__dirname, "native");
|
|
129
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
// The GitHub release tag on serve-dist is `v<version>` (e.g. v0.4.0), while
|
|
132
|
+
// the GoReleaser archive filename uses {{ .Version }} (the tag with the leading
|
|
133
|
+
// "v" stripped, e.g. serve_0.4.0_darwin_amd64.tar.gz). The npm package version
|
|
134
|
+
// is set to the stripped tag, so the archive name matches directly, but the
|
|
135
|
+
// download URL must re-add the "v" prefix to hit the actual release tag.
|
|
136
|
+
const releaseTag = "v" + version;
|
|
137
|
+
const archiveUrl = DIST + "/" + releaseTag + "/" + archiveName;
|
|
138
|
+
const checksumsUrl = DIST + "/" + releaseTag + "/checksums.txt";
|
|
139
|
+
|
|
140
|
+
const [archive, checksumsText] = await Promise.all([
|
|
141
|
+
download(archiveUrl),
|
|
142
|
+
download(checksumsUrl).then((b) => b.toString("utf8")),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const expected = checksumFor(archiveName, checksumsText);
|
|
146
|
+
if (!expected) {
|
|
147
|
+
throw new Error("@servelink/serve: " + archiveName + " missing from checksums.txt; cannot verify integrity.");
|
|
148
|
+
}
|
|
149
|
+
if (!verifySha256(archive, expected)) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"@servelink/serve: checksum mismatch for " + archiveName +
|
|
152
|
+
". Refusing to install an unverified binary."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const files = readTarFiles(zlib.gunzipSync(archive));
|
|
157
|
+
const entry = files.get("serve");
|
|
158
|
+
if (!entry) {
|
|
159
|
+
throw new Error("@servelink/serve: 'serve' not found in " + archiveName);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const binPath = path.join(distDir, "serve");
|
|
163
|
+
fs.writeFileSync(binPath, entry);
|
|
164
|
+
fs.chmodSync(binPath, 0o755);
|
|
165
|
+
|
|
166
|
+
const { spawnSync } = require("node:child_process");
|
|
167
|
+
const probe = spawnSync(binPath, ["--version"], { encoding: "utf8" });
|
|
168
|
+
if (probe.status !== 0) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"@servelink/serve: installed binary failed --version check: " +
|
|
171
|
+
(probe.stderr || probe.stdout || "exit " + probe.status)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
console.log("@servelink/serve: installed serve " + probe.stdout.trim() + " (" + os + "/" + arch + ")");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (require.main === module) {
|
|
178
|
+
run().catch((err) => {
|
|
179
|
+
console.error(err && err.message ? err.message : String(err));
|
|
180
|
+
process.exit(1);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { resolvePlatform, checksumFor, verifySha256, readTarFiles };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@servelink/serve",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Public tunnel for hosted serve accounts. Thin wrapper that downloads and invokes the native serve CLI.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"serve": "bin/serve.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node install.js",
|
|
10
|
+
"test": "node --test test/install.test.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"install.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"os": [
|
|
21
|
+
"darwin",
|
|
22
|
+
"linux",
|
|
23
|
+
"win32"
|
|
24
|
+
],
|
|
25
|
+
"cpu": [
|
|
26
|
+
"x64",
|
|
27
|
+
"arm64"
|
|
28
|
+
],
|
|
29
|
+
"license": "Proprietary",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"tunnel",
|
|
32
|
+
"ngrok",
|
|
33
|
+
"localhost",
|
|
34
|
+
"reverse-tunnel",
|
|
35
|
+
"mcp",
|
|
36
|
+
"cli"
|
|
37
|
+
]
|
|
38
|
+
}
|