lanesh 0.0.0-stage → 0.1.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.
- package/README.md +16 -2
- package/bin/lanesh.js +34 -0
- package/lib/launcher.js +170 -0
- package/package.json +40 -4
package/README.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lanesh
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Share skills and shells across your machines through one MCP endpoint.
|
|
4
|
+
|
|
5
|
+
This package installs the `lanesh` command. On first run it downloads the
|
|
6
|
+
native binary for your platform from
|
|
7
|
+
[GitHub Releases](https://github.com/jtsang4/lanesh/releases), verifies its
|
|
8
|
+
SHA-256 checksum, and stores it in `~/.lanesh/bin` (override with
|
|
9
|
+
`LANESH_HOME`).
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx lanesh --help
|
|
13
|
+
# or
|
|
14
|
+
npm install -g lanesh
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
See the [project README](https://github.com/jtsang4/lanesh#readme) for details.
|
package/bin/lanesh.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
import { ensureBinary } from '../lib/launcher.js';
|
|
6
|
+
|
|
7
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
8
|
+
|
|
9
|
+
let bin;
|
|
10
|
+
try {
|
|
11
|
+
bin = await ensureBinary(version);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
process.stderr.write(`lanesh: ${err.message}\n`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: 'inherit', windowsHide: true });
|
|
18
|
+
|
|
19
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
20
|
+
process.on(signal, () => child.kill(signal));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
child.on('error', (err) => {
|
|
24
|
+
process.stderr.write(`lanesh: failed to start ${bin}: ${err.message}\n`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
child.on('exit', (code, signal) => {
|
|
29
|
+
if (signal) {
|
|
30
|
+
process.kill(process.pid, signal);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
process.exit(code ?? 1);
|
|
34
|
+
});
|
package/lib/launcher.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Resolves, downloads and verifies the native lanesh binary.
|
|
2
|
+
//
|
|
3
|
+
// The npm package ships no binary. On first run it downloads the release
|
|
4
|
+
// binary that matches this package's version into a Lanesh-managed directory
|
|
5
|
+
// (~/.lanesh/bin by default), so that the daemon can later update itself
|
|
6
|
+
// without fighting npm over files in node_modules.
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { basename, dirname, join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_DOWNLOAD_BASE = 'https://github.com/jtsang4/lanesh/releases/download';
|
|
15
|
+
|
|
16
|
+
const OS_MAP = { darwin: 'darwin', linux: 'linux', win32: 'windows' };
|
|
17
|
+
const ARCH_MAP = { x64: 'amd64', arm64: 'arm64' };
|
|
18
|
+
|
|
19
|
+
/** Maps Node's platform/arch to Go's GOOS/GOARCH, or throws if unsupported. */
|
|
20
|
+
export function resolveTarget(platform = process.platform, arch = process.arch) {
|
|
21
|
+
const os = OS_MAP[platform];
|
|
22
|
+
const goarch = ARCH_MAP[arch];
|
|
23
|
+
if (!os || !goarch) {
|
|
24
|
+
throw new Error(`lanesh does not provide a binary for ${platform}/${arch}`);
|
|
25
|
+
}
|
|
26
|
+
return { os, arch: goarch, exe: os === 'windows' ? '.exe' : '' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Name of the raw binary release asset, matching .goreleaser.yaml. */
|
|
30
|
+
export function assetName(version, target) {
|
|
31
|
+
return `lanesh_${version}_${target.os}_${target.arch}${target.exe}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function laneshHome(env = process.env) {
|
|
35
|
+
return env.LANESH_HOME || join(homedir(), '.lanesh');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function binaryPath(env = process.env, target = resolveTarget()) {
|
|
39
|
+
return join(laneshHome(env), 'bin', `lanesh${target.exe}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseVersion(v) {
|
|
43
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(String(v).trim());
|
|
44
|
+
if (!m) return null;
|
|
45
|
+
return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? '' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Compares two semantic versions. Unparseable versions sort lowest. */
|
|
49
|
+
export function compareVersions(a, b) {
|
|
50
|
+
const pa = parseVersion(a);
|
|
51
|
+
const pb = parseVersion(b);
|
|
52
|
+
if (!pa || !pb) return pa ? 1 : pb ? -1 : 0;
|
|
53
|
+
for (let i = 0; i < 3; i++) {
|
|
54
|
+
if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] < pb.nums[i] ? -1 : 1;
|
|
55
|
+
}
|
|
56
|
+
return comparePrerelease(pa.pre, pb.pre);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Orders prerelease tags per semver (numeric identifiers compare numerically).
|
|
60
|
+
function comparePrerelease(a, b) {
|
|
61
|
+
if (a === b) return 0;
|
|
62
|
+
if (!a) return 1;
|
|
63
|
+
if (!b) return -1;
|
|
64
|
+
const xa = a.split('.');
|
|
65
|
+
const xb = b.split('.');
|
|
66
|
+
for (let i = 0; i < Math.min(xa.length, xb.length); i++) {
|
|
67
|
+
const na = /^\d+$/.test(xa[i]);
|
|
68
|
+
const nb = /^\d+$/.test(xb[i]);
|
|
69
|
+
if (na && nb) {
|
|
70
|
+
const d = Number(xa[i]) - Number(xb[i]);
|
|
71
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
72
|
+
} else if (na !== nb) {
|
|
73
|
+
return na ? -1 : 1;
|
|
74
|
+
} else if (xa[i] !== xb[i]) {
|
|
75
|
+
return xa[i] < xb[i] ? -1 : 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return xa.length === xb.length ? 0 : xa.length < xb.length ? -1 : 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Returns true for versions that have no published release (local builds). */
|
|
82
|
+
export function isDevelopmentVersion(v) {
|
|
83
|
+
return !parseVersion(v) || String(v).includes('development');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Extracts the sha256 for `name` from a goreleaser checksums.txt body. */
|
|
87
|
+
export function findChecksum(checksums, name) {
|
|
88
|
+
for (const line of checksums.split('\n')) {
|
|
89
|
+
const [sum, file] = line.trim().split(/\s+/);
|
|
90
|
+
if (file === name) return sum.toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function installedVersion(bin) {
|
|
96
|
+
if (!existsSync(bin)) return null;
|
|
97
|
+
try {
|
|
98
|
+
return execFileSync(bin, ['version', '--short'], { encoding: 'utf8', timeout: 10_000 }).trim();
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function fetchOk(url) {
|
|
105
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
106
|
+
if (!res.ok) throw new Error(`download failed: ${url} (${res.status} ${res.statusText})`);
|
|
107
|
+
return res;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Downloads, verifies and atomically installs the binary for `version`. */
|
|
111
|
+
export async function install(version, { env = process.env, target = resolveTarget() } = {}) {
|
|
112
|
+
const base = (env.LANESH_DOWNLOAD_BASE || DEFAULT_DOWNLOAD_BASE).replace(/\/+$/, '');
|
|
113
|
+
const name = assetName(version, target);
|
|
114
|
+
const dir = `${base}/v${version}`;
|
|
115
|
+
|
|
116
|
+
const [checksums, binary] = await Promise.all([
|
|
117
|
+
fetchOk(`${dir}/checksums.txt`).then((r) => r.text()),
|
|
118
|
+
fetchOk(`${dir}/${name}`).then(async (r) => Buffer.from(await r.arrayBuffer()))
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
const expected = findChecksum(checksums, name);
|
|
122
|
+
if (!expected) throw new Error(`no checksum for ${name} in checksums.txt`);
|
|
123
|
+
const actual = createHash('sha256').update(binary).digest('hex');
|
|
124
|
+
if (actual !== expected) {
|
|
125
|
+
throw new Error(`checksum mismatch for ${name}: expected ${expected}, got ${actual}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const bin = binaryPath(env, target);
|
|
129
|
+
mkdirSync(join(laneshHome(env), 'bin'), { recursive: true });
|
|
130
|
+
const tmp = `${bin}.download`;
|
|
131
|
+
writeFileSync(tmp, binary, { mode: 0o755 });
|
|
132
|
+
chmodSync(tmp, 0o755);
|
|
133
|
+
|
|
134
|
+
if (target.os === 'windows' && existsSync(bin)) {
|
|
135
|
+
// A running .exe cannot be overwritten or deleted, but it can be renamed.
|
|
136
|
+
for (const f of readdirSync(dirname(bin))) {
|
|
137
|
+
if (f.startsWith(`${basename(bin)}.old`)) {
|
|
138
|
+
try {
|
|
139
|
+
rmSync(join(dirname(bin), f), { force: true });
|
|
140
|
+
} catch {
|
|
141
|
+
// still running
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
renameSync(bin, `${bin}.old-${Date.now()}`);
|
|
146
|
+
}
|
|
147
|
+
renameSync(tmp, bin);
|
|
148
|
+
return bin;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Returns the path of a binary at least as new as `wanted`, downloading it if
|
|
153
|
+
* needed. A newer managed binary (for example after a self-update) is kept.
|
|
154
|
+
*/
|
|
155
|
+
export async function ensureBinary(wanted, { env = process.env, target = resolveTarget() } = {}) {
|
|
156
|
+
const bin = binaryPath(env, target);
|
|
157
|
+
const current = installedVersion(bin);
|
|
158
|
+
|
|
159
|
+
if (isDevelopmentVersion(wanted)) {
|
|
160
|
+
if (current) return bin;
|
|
161
|
+
throw new Error(
|
|
162
|
+
`this is a development build of the lanesh npm package; install a release or place a binary at ${bin}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (current && !isDevelopmentVersion(current) && compareVersions(current, wanted) >= 0) {
|
|
166
|
+
return bin;
|
|
167
|
+
}
|
|
168
|
+
process.stderr.write(`lanesh: installing v${wanted} to ${bin}\n`);
|
|
169
|
+
return install(wanted, { env, target });
|
|
170
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lanesh",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Share skills and shells across your machines through one MCP endpoint.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"agent",
|
|
9
|
+
"skills",
|
|
10
|
+
"remote-execution",
|
|
11
|
+
"cli"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/jtsang4/lanesh#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/jtsang4/lanesh/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/jtsang4/lanesh.git",
|
|
20
|
+
"directory": "build/npm"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "jtsang4",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"lanesh": "bin/lanesh.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"lib"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node --test"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"provenance": true
|
|
41
|
+
}
|
|
42
|
+
}
|