@dhruv2mars/mdv 0.0.13 → 0.0.14
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 +13 -0
- package/bin/install-lib.js +45 -0
- package/bin/install.js +104 -18
- package/bin/mdv-lib.js +37 -0
- package/bin/mdv.js +12 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ npm i -g @dhruv2mars/mdv
|
|
|
10
10
|
|
|
11
11
|
First run downloads native `mdv` binary.
|
|
12
12
|
Assets are resolved from GitHub Releases for your platform/arch.
|
|
13
|
+
Installer keeps verified cache under `~/.mdv/cache`.
|
|
13
14
|
|
|
14
15
|
## Usage
|
|
15
16
|
|
|
@@ -17,6 +18,12 @@ Assets are resolved from GitHub Releases for your platform/arch.
|
|
|
17
18
|
mdv README.md
|
|
18
19
|
```
|
|
19
20
|
|
|
21
|
+
Manual upgrade:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
mdv update
|
|
25
|
+
```
|
|
26
|
+
|
|
20
27
|
Stream stdin:
|
|
21
28
|
|
|
22
29
|
```bash
|
|
@@ -37,3 +44,9 @@ tail -f notes.md | mdv --stream
|
|
|
37
44
|
- `--no-watch` disable file watch
|
|
38
45
|
- `--stream` read markdown from stdin
|
|
39
46
|
- `--perf` show perf stats
|
|
47
|
+
|
|
48
|
+
## Installer Env
|
|
49
|
+
|
|
50
|
+
- `MDV_INSTALL_DEBUG=1` local installer debug logs
|
|
51
|
+
- `MDV_INSTALL_TIMEOUT_MS` request timeout (default `15000`)
|
|
52
|
+
- `MDV_INSTALL_RETRY_ATTEMPTS` retries (default `3`)
|
package/bin/install-lib.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
1
3
|
export function assetNameFor(platform = process.platform, arch = process.arch) {
|
|
2
4
|
const ext = platform === 'win32' ? '.exe' : '';
|
|
3
5
|
return `mdv-${platform}-${arch}${ext}`;
|
|
@@ -14,6 +16,49 @@ export function checksumsAssetNameFromBinaryAsset(asset) {
|
|
|
14
16
|
return checksumsAssetNameFor(m[1], m[2]);
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
function parseIntEnv(value, fallback, min, max) {
|
|
20
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
21
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
22
|
+
if (parsed < min) return min;
|
|
23
|
+
if (parsed > max) return max;
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function installTuningFromEnv(env = process.env) {
|
|
28
|
+
return {
|
|
29
|
+
retryAttempts: parseIntEnv(env.MDV_INSTALL_RETRY_ATTEMPTS, 3, 1, 10),
|
|
30
|
+
timeoutMs: parseIntEnv(env.MDV_INSTALL_TIMEOUT_MS, 15000, 1000, 120000),
|
|
31
|
+
backoffMs: parseIntEnv(env.MDV_INSTALL_BACKOFF_MS, 250, 50, 5000),
|
|
32
|
+
backoffJitterMs: parseIntEnv(env.MDV_INSTALL_BACKOFF_JITTER_MS, 100, 0, 2000)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function computeBackoffDelay(attempt, backoffMs, backoffJitterMs, rand = Math.random) {
|
|
37
|
+
const scaled = Math.max(1, attempt) * Math.max(0, backoffMs);
|
|
38
|
+
if (backoffJitterMs <= 0) return scaled;
|
|
39
|
+
const jitter = Math.floor(Math.max(0, rand()) * (backoffJitterMs + 1));
|
|
40
|
+
return scaled + jitter;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function cachePathsFor(installRoot, version, asset, checksumsAsset) {
|
|
44
|
+
const root = join(installRoot, 'cache', `v${version}`);
|
|
45
|
+
return {
|
|
46
|
+
cacheDir: root,
|
|
47
|
+
cacheBinary: join(root, asset),
|
|
48
|
+
cacheChecksums: join(root, checksumsAsset)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildChecksumMismatchHelp({ asset, expected, actual, cachePath }) {
|
|
53
|
+
const shortExpected = String(expected || '').slice(0, 12);
|
|
54
|
+
const shortActual = String(actual || '').slice(0, 12);
|
|
55
|
+
return [
|
|
56
|
+
`checksum mismatch for ${asset}`,
|
|
57
|
+
`expected=${shortExpected}... actual=${shortActual}...`,
|
|
58
|
+
`clear cache and retry: rm -rf ${cachePath}`
|
|
59
|
+
].join('; ');
|
|
60
|
+
}
|
|
61
|
+
|
|
17
62
|
export function findAssetUrl(release, asset) {
|
|
18
63
|
if (!release || !Array.isArray(release.assets)) return null;
|
|
19
64
|
for (const item of release.assets) {
|
package/bin/install.js
CHANGED
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
mkdirSync,
|
|
9
9
|
readFileSync,
|
|
10
10
|
rmSync,
|
|
11
|
-
renameSync
|
|
11
|
+
renameSync,
|
|
12
|
+
writeFileSync
|
|
12
13
|
} from 'node:fs';
|
|
13
14
|
import { createHash } from 'node:crypto';
|
|
14
15
|
import { homedir } from 'node:os';
|
|
@@ -18,7 +19,11 @@ import { spawnSync } from 'node:child_process';
|
|
|
18
19
|
import { fileURLToPath } from 'node:url';
|
|
19
20
|
import {
|
|
20
21
|
assetNameFor,
|
|
22
|
+
buildChecksumMismatchHelp,
|
|
23
|
+
cachePathsFor,
|
|
21
24
|
checksumsAssetNameFor,
|
|
25
|
+
computeBackoffDelay,
|
|
26
|
+
installTuningFromEnv,
|
|
22
27
|
parseChecksumForAsset,
|
|
23
28
|
resolveReleaseAssetBundle,
|
|
24
29
|
shouldUseFallbackUrl
|
|
@@ -30,9 +35,13 @@ const installRoot = process.env.MDV_INSTALL_ROOT || join(homedir(), '.mdv');
|
|
|
30
35
|
const binDir = join(installRoot, 'bin');
|
|
31
36
|
const binName = process.platform === 'win32' ? 'mdv.exe' : 'mdv';
|
|
32
37
|
const dest = join(binDir, binName);
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
38
|
+
const tuning = installTuningFromEnv(process.env);
|
|
39
|
+
const retryAttempts = tuning.retryAttempts;
|
|
40
|
+
const timeoutMs = tuning.timeoutMs;
|
|
41
|
+
const backoffMs = tuning.backoffMs;
|
|
42
|
+
const backoffJitterMs = tuning.backoffJitterMs;
|
|
43
|
+
const debugEnabled = process.env.MDV_INSTALL_DEBUG === '1';
|
|
44
|
+
const installStartedAt = Date.now();
|
|
36
45
|
|
|
37
46
|
if (process.env.MDV_SKIP_DOWNLOAD === '1') process.exit(0);
|
|
38
47
|
if (existsSync(dest)) process.exit(0);
|
|
@@ -42,26 +51,47 @@ mkdirSync(binDir, { recursive: true });
|
|
|
42
51
|
const version = pkgVersion();
|
|
43
52
|
const asset = assetNameFor();
|
|
44
53
|
const checksumsAsset = checksumsAssetNameFor();
|
|
54
|
+
const cachePaths = cachePathsFor(installRoot, version, asset, checksumsAsset);
|
|
45
55
|
const url = `https://github.com/${REPO}/releases/download/v${version}/${asset}`;
|
|
46
56
|
const checksumsUrl = `https://github.com/${REPO}/releases/download/v${version}/${checksumsAsset}`;
|
|
47
57
|
const tmp = `${dest}.tmp-${Date.now()}`;
|
|
58
|
+
mkdirSync(cachePaths.cacheDir, { recursive: true });
|
|
48
59
|
|
|
49
60
|
try {
|
|
61
|
+
trace(`start retry=${retryAttempts} timeout=${timeoutMs}ms backoff=${backoffMs}ms jitter=${backoffJitterMs}ms`);
|
|
50
62
|
console.error(`mdv: download ${asset} v${version}`);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
let checksumsText = null;
|
|
64
|
+
const restoredFromCache = await installFromCache(cachePaths, asset, tmp);
|
|
65
|
+
if (restoredFromCache) {
|
|
66
|
+
trace('cache-hit');
|
|
67
|
+
} else {
|
|
68
|
+
trace('cache-miss');
|
|
69
|
+
const result = await downloadWithFallback(
|
|
70
|
+
{ binaryUrl: url, checksumsUrl },
|
|
71
|
+
version,
|
|
72
|
+
asset,
|
|
73
|
+
checksumsAsset,
|
|
74
|
+
tmp
|
|
75
|
+
);
|
|
76
|
+
if (result.source === 'primary') {
|
|
77
|
+
checksumsText = result.checksumsText;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
58
80
|
if (process.platform !== 'win32') chmodSync(tmp, 0o755);
|
|
59
81
|
renameSync(tmp, dest);
|
|
82
|
+
if (checksumsText) {
|
|
83
|
+
persistCache(cachePaths, checksumsText, dest);
|
|
84
|
+
trace('cache-store');
|
|
85
|
+
}
|
|
86
|
+
trace('success');
|
|
60
87
|
process.exit(0);
|
|
61
88
|
} catch (err) {
|
|
62
89
|
try { rmSync(tmp, { force: true }); } catch {}
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
if (err && err.code === 'MDV_CHECKSUM_MISMATCH') {
|
|
91
|
+
console.error(`mdv: ${err.message}`);
|
|
92
|
+
} else {
|
|
93
|
+
console.error(`mdv: download failed (${String(err)})`);
|
|
94
|
+
}
|
|
65
95
|
|
|
66
96
|
if (process.env.MDV_ALLOW_CARGO_FALLBACK === '1') {
|
|
67
97
|
if (cargoInstallFallback()) {
|
|
@@ -170,7 +200,7 @@ async function withRetry(label, fn) {
|
|
|
170
200
|
} catch (err) {
|
|
171
201
|
lastErr = err;
|
|
172
202
|
if (attempt >= retryAttempts) break;
|
|
173
|
-
await sleep(backoffMs
|
|
203
|
+
await sleep(computeBackoffDelay(attempt, backoffMs, backoffJitterMs));
|
|
174
204
|
console.error(`mdv: retry ${label} (${attempt + 1}/${retryAttempts})`);
|
|
175
205
|
}
|
|
176
206
|
}
|
|
@@ -201,14 +231,15 @@ async function downloadAndVerify({ binaryUrl, checksumsUrl }, asset, outPath) {
|
|
|
201
231
|
|
|
202
232
|
const actual = await sha256File(outPath);
|
|
203
233
|
if (actual !== expected) {
|
|
204
|
-
throw
|
|
234
|
+
throw checksumMismatchError(asset, expected, actual);
|
|
205
235
|
}
|
|
236
|
+
return { checksumsText };
|
|
206
237
|
}
|
|
207
238
|
|
|
208
239
|
async function downloadWithFallback(primary, version, asset, checksumsAsset, outPath) {
|
|
209
240
|
try {
|
|
210
|
-
await downloadAndVerify(primary, asset, outPath);
|
|
211
|
-
return;
|
|
241
|
+
const primaryVerified = await downloadAndVerify(primary, asset, outPath);
|
|
242
|
+
return { ...primaryVerified, source: 'primary' };
|
|
212
243
|
} catch (primaryErr) {
|
|
213
244
|
const fallback = await resolveReleaseAssetBundle({
|
|
214
245
|
version,
|
|
@@ -222,7 +253,8 @@ async function downloadWithFallback(primary, version, asset, checksumsAsset, out
|
|
|
222
253
|
}
|
|
223
254
|
|
|
224
255
|
console.error(`mdv: fallback download ${fallback.binaryUrl}`);
|
|
225
|
-
await downloadAndVerify(fallback, asset, outPath);
|
|
256
|
+
const fallbackVerified = await downloadAndVerify(fallback, asset, outPath);
|
|
257
|
+
return { ...fallbackVerified, source: 'fallback' };
|
|
226
258
|
}
|
|
227
259
|
}
|
|
228
260
|
|
|
@@ -270,6 +302,60 @@ function requestJson(url) {
|
|
|
270
302
|
});
|
|
271
303
|
}
|
|
272
304
|
|
|
305
|
+
function trace(msg) {
|
|
306
|
+
if (!debugEnabled) return;
|
|
307
|
+
const elapsed = Date.now() - installStartedAt;
|
|
308
|
+
console.error(`mdv:debug +${elapsed}ms ${msg}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function installFromCache(paths, asset, outPath) {
|
|
312
|
+
if (!existsSync(paths.cacheBinary) || !existsSync(paths.cacheChecksums)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const checksumsText = readFileSync(paths.cacheChecksums, 'utf8');
|
|
318
|
+
const expected = parseChecksumForAsset(checksumsText, asset);
|
|
319
|
+
if (!expected) {
|
|
320
|
+
trace('cache-invalid-missing-checksum-entry');
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
const actual = await sha256File(paths.cacheBinary);
|
|
324
|
+
if (actual !== expected) {
|
|
325
|
+
trace('cache-invalid-checksum-mismatch');
|
|
326
|
+
try { rmSync(paths.cacheBinary, { force: true }); } catch {}
|
|
327
|
+
try { rmSync(paths.cacheChecksums, { force: true }); } catch {}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
copyFileSync(paths.cacheBinary, outPath);
|
|
331
|
+
return true;
|
|
332
|
+
} catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function persistCache(paths, checksumsText, sourceBinaryPath) {
|
|
338
|
+
try {
|
|
339
|
+
copyFileSync(sourceBinaryPath, paths.cacheBinary);
|
|
340
|
+
writeFileSync(paths.cacheChecksums, checksumsText, 'utf8');
|
|
341
|
+
} catch {
|
|
342
|
+
trace('cache-store-failed');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function checksumMismatchError(asset, expected, actual) {
|
|
347
|
+
const err = new Error(
|
|
348
|
+
buildChecksumMismatchHelp({
|
|
349
|
+
asset,
|
|
350
|
+
expected,
|
|
351
|
+
actual,
|
|
352
|
+
cachePath: cachePaths.cacheDir
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
err.code = 'MDV_CHECKSUM_MISMATCH';
|
|
356
|
+
return err;
|
|
357
|
+
}
|
|
358
|
+
|
|
273
359
|
function cargoInstallFallback() {
|
|
274
360
|
const probe = spawnSync('cargo', ['--version'], { stdio: 'ignore' });
|
|
275
361
|
if (probe.status !== 0) return false;
|
package/bin/mdv-lib.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const PACKAGE_NAME = '@dhruv2mars/mdv@latest';
|
|
5
|
+
|
|
6
|
+
export function binNameForPlatform(platform = process.platform) {
|
|
7
|
+
return platform === 'win32' ? 'mdv.exe' : 'mdv';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resolveInstallRoot(env = process.env, home = homedir()) {
|
|
11
|
+
return env.MDV_INSTALL_ROOT || join(home, '.mdv');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveInstalledBin(env = process.env, platform = process.platform, home = homedir()) {
|
|
15
|
+
const installRoot = resolveInstallRoot(env, home);
|
|
16
|
+
const binName = binNameForPlatform(platform);
|
|
17
|
+
return join(installRoot, 'bin', binName);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function shouldRunUpdateCommand(args) {
|
|
21
|
+
return Array.isArray(args) && args.length > 0 && args[0] === 'update';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveUpdateCommand(env = process.env) {
|
|
25
|
+
const npmExecPath = env.npm_execpath;
|
|
26
|
+
if (typeof npmExecPath === 'string' && npmExecPath.endsWith('.js')) {
|
|
27
|
+
return {
|
|
28
|
+
command: process.execPath,
|
|
29
|
+
args: [npmExecPath, 'install', '-g', PACKAGE_NAME]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
command: 'npm',
|
|
35
|
+
args: ['install', '-g', PACKAGE_NAME]
|
|
36
|
+
};
|
|
37
|
+
}
|
package/bin/mdv.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
3
|
import { spawnSync } from 'node:child_process';
|
|
6
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
resolveInstalledBin,
|
|
7
|
+
resolveUpdateCommand,
|
|
8
|
+
shouldRunUpdateCommand
|
|
9
|
+
} from './mdv-lib.js';
|
|
7
10
|
|
|
8
11
|
const args = process.argv.slice(2);
|
|
9
12
|
|
|
13
|
+
if (shouldRunUpdateCommand(args)) {
|
|
14
|
+
const update = resolveUpdateCommand(process.env);
|
|
15
|
+
const res = spawnSync(update.command, update.args, { stdio: 'inherit', env: process.env });
|
|
16
|
+
process.exit(res.status ?? 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
const envBin = process.env.MDV_BIN;
|
|
11
20
|
if (envBin) run(envBin, args);
|
|
12
21
|
|
|
13
|
-
const
|
|
14
|
-
const binName = process.platform === 'win32' ? 'mdv.exe' : 'mdv';
|
|
15
|
-
const installedBin = join(installRoot, 'bin', binName);
|
|
22
|
+
const installedBin = resolveInstalledBin(process.env, process.platform);
|
|
16
23
|
|
|
17
24
|
if (!existsSync(installedBin)) {
|
|
18
25
|
const here = fileURLToPath(new URL('.', import.meta.url));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhruv2mars/mdv",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "Terminal-first markdown visualizer/editor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"mdv": "node bin/mdv.js",
|
|
14
14
|
"postinstall": "node bin/install.js",
|
|
15
15
|
"selftest": "node bin/selftest.js",
|
|
16
|
-
"lint": "node --check bin/mdv.js && node --check bin/install.js && node --check bin/install-lib.js && node --check bin/selftest.js",
|
|
16
|
+
"lint": "node --check bin/mdv.js && node --check bin/mdv-lib.js && node --check bin/install.js && node --check bin/install-lib.js && node --check bin/selftest.js",
|
|
17
17
|
"test": "node scripts/test.js",
|
|
18
18
|
"coverage": "c8 --all --include bin/install-lib.js --reporter text-summary --check-coverage --lines 95 --functions 95 --branches 80 --statements 95 node scripts/test.js"
|
|
19
19
|
},
|