jsql-neo 6.5.24 → 6.5.25
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/LICENSE +1 -1
- package/README.md +4 -3
- package/bin/jsql +120 -1
- package/index.js +1 -1
- package/lib/uninstaller.js +216 -0
- package/lib/updater.js +365 -0
- package/package.json +1 -1
- package/postinstall.js +179 -44
- package/test/cli-maintenance.test.js +112 -0
package/LICENSE
CHANGED
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
same "printed page" as the copyright notice for easier
|
|
188
188
|
identification within third-party archives.
|
|
189
189
|
|
|
190
|
-
Copyright
|
|
190
|
+
Copyright 2026 Vexify
|
|
191
191
|
|
|
192
192
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
193
|
you may not use this file except in compliance with the License.
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
4
|
> MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
|
|
5
5
|
|
|
6
|
-
> **v6.5.
|
|
6
|
+
> **v6.5.25** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|

|
|
22
22
|
|
|
23
|
-
A real `jsql tui` session: joins, NULL semantics,
|
|
24
|
-
savepoint rollback.
|
|
23
|
+
A real `jsql tui` session (v6.5.25): 64-bit integer precision, joins, NULL semantics,
|
|
24
|
+
and savepoint rollback — all in one in-memory engine, rendered on a dark background.
|
|
25
25
|
|
|
26
26
|
[MP4 version](doc/demo.mp4) · [asciinema recording](doc/demo.cast)
|
|
27
27
|
|
|
@@ -227,5 +227,6 @@ Apache-2.0 License — contributions welcome!
|
|
|
227
227
|
- **Issues:** [Open an issue](https://github.com/vexify-org/JSQL-neo/issues)
|
|
228
228
|
- **PRs:** Fork → branch → PR — thank you!
|
|
229
229
|
- **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
230
|
+
- **Dev container:** [jsql-neo/devbox](https://github.com/jsql-neo/devbox) — reproducible dev environment (Docker/devcontainer)
|
|
230
231
|
- **Code of Conduct:** [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
231
232
|
- **Security:** [SECURITY.md](SECURITY.md) — please report vulnerabilities privately
|
package/bin/jsql
CHANGED
|
@@ -13,7 +13,7 @@ const { nag, setStarred, readConfig } = require('../lib/star-nag');
|
|
|
13
13
|
|
|
14
14
|
// star 提示:任何异常都不该挡住命令本身
|
|
15
15
|
// version / --version 只回版本号、star 本身就是干这件事的 —— 这些不提示,其余全部提示
|
|
16
|
-
const NO_NAG_ARGS = new Set(['version', '--version', '-v', 'star']);
|
|
16
|
+
const NO_NAG_ARGS = new Set(['version', '--version', '-v', 'star', 'update', 'uninstall', 'remove']);
|
|
17
17
|
if (!process.env.JSQL_NO_STAR_NAG && !NO_NAG_ARGS.has(process.argv[2])) {
|
|
18
18
|
try { nag(); } catch (e) { /* 提示失败与命令无关 */ }
|
|
19
19
|
}
|
|
@@ -34,6 +34,122 @@ const manager = new ModuleManager();
|
|
|
34
34
|
const RAW_ARGV = process.argv.slice(2);
|
|
35
35
|
|
|
36
36
|
const _pkg = require('../package.json');
|
|
37
|
+
|
|
38
|
+
// ── update / uninstall 命令的实现 ─────────────────────────────
|
|
39
|
+
|
|
40
|
+
function updateOptions(sub) {
|
|
41
|
+
sub.option('check', { type: 'boolean', description: 'Only check for a newer version' });
|
|
42
|
+
sub.option('force', { type: 'boolean', description: 'Reinstall even if already up to date' });
|
|
43
|
+
sub.option('dry-run', { type: 'boolean', description: 'Print the install command without running it' });
|
|
44
|
+
sub.option('json', { type: 'boolean', description: 'Output as JSON' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function updateHandler(argv) {
|
|
48
|
+
const updater = require('../lib/updater');
|
|
49
|
+
try {
|
|
50
|
+
const result = await updater.runUpdate({
|
|
51
|
+
checkOnly: argv.check === true,
|
|
52
|
+
force: argv.force === true,
|
|
53
|
+
dryRun: argv['dry-run'] === true,
|
|
54
|
+
});
|
|
55
|
+
if (argv.json) console.log(JSON.stringify(result, null, 2));
|
|
56
|
+
else console.log(updater.formatResult(result));
|
|
57
|
+
if (!result.ok) process.exitCode = 1;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(`Error: ${e.message}`);
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function uninstallOptions(sub) {
|
|
65
|
+
sub.option('yes', { alias: ['y'], type: 'boolean', description: 'Skip the confirmation prompt' });
|
|
66
|
+
sub.option('keep-data', { type: 'boolean', description: 'Keep user data (plugins/config/cache)' });
|
|
67
|
+
sub.option('global', { type: 'boolean', description: 'Only uninstall the global npm package' });
|
|
68
|
+
sub.option('all', { type: 'boolean', description: 'Remove user data and the global npm package' });
|
|
69
|
+
sub.option('dry-run', { type: 'boolean', description: 'Show what would be removed' });
|
|
70
|
+
sub.option('json', { type: 'boolean', description: 'Output as JSON' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function uninstallHandler(argv) {
|
|
74
|
+
const un = require('../lib/uninstaller');
|
|
75
|
+
const fail = (e) => { console.error(`Error: ${e.message}`); process.exitCode = 1; };
|
|
76
|
+
try {
|
|
77
|
+
const yes = argv.yes === true || argv.y === true;
|
|
78
|
+
const dryRun = argv['dry-run'] === true;
|
|
79
|
+
const doGlobal = argv.global === true || argv.all === true;
|
|
80
|
+
const keepData = argv['keep-data'] === true || (argv.global === true && argv.all !== true);
|
|
81
|
+
const doData = !keepData;
|
|
82
|
+
|
|
83
|
+
if (!doData && !doGlobal) {
|
|
84
|
+
console.log('Nothing to do (data kept, global package not requested).');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const targets = doData ? un.inspectTargets() : [];
|
|
89
|
+
const existing = targets.filter((t) => t.exists);
|
|
90
|
+
const globalInstalled = doGlobal ? un.isGloballyInstalled() : false;
|
|
91
|
+
|
|
92
|
+
if (!dryRun) {
|
|
93
|
+
if (doData && existing.length === 0 && !doGlobal) {
|
|
94
|
+
console.log('No jsql user data found. Nothing to remove.');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!yes) {
|
|
98
|
+
if (!process.stdin.isTTY) {
|
|
99
|
+
console.error('Refusing to delete user data without confirmation. Re-run with --yes (or --dry-run to preview).');
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (doData && existing.length) {
|
|
104
|
+
console.log('The following user data will be removed:');
|
|
105
|
+
for (const t of existing) console.log(` - ${t.path} (${t.label})`);
|
|
106
|
+
}
|
|
107
|
+
if (doGlobal) console.log('Will also run: npm uninstall -g ' + un.PKG_NAME);
|
|
108
|
+
const ok = await un.confirm('Proceed? [y/N] ');
|
|
109
|
+
if (!ok) {
|
|
110
|
+
console.log('Aborted. No changes were made.');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const dataResult = doData ? un.removeData({ dryRun }) : { removed: [], skipped: [], failed: [] };
|
|
117
|
+
let globalResult = null;
|
|
118
|
+
if (doGlobal) {
|
|
119
|
+
if (dryRun) globalResult = { ok: true, dryRun: true, command: `npm uninstall -g ${un.PKG_NAME}`, error: null };
|
|
120
|
+
else if (!globalInstalled) globalResult = { ok: true, skipped: true, command: `npm uninstall -g ${un.PKG_NAME}`, error: null };
|
|
121
|
+
else globalResult = un.uninstallGlobal(false);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (argv.json) {
|
|
125
|
+
console.log(JSON.stringify({ data: dataResult, global: globalResult, dryRun }, null, 2));
|
|
126
|
+
} else {
|
|
127
|
+
const verb = dryRun ? 'Would remove' : 'Removed';
|
|
128
|
+
if (dataResult.removed.length) {
|
|
129
|
+
console.log(`${verb} ${dataResult.removed.length} path(s):`);
|
|
130
|
+
for (const p of dataResult.removed) console.log(` - ${p}`);
|
|
131
|
+
} else if (doData) {
|
|
132
|
+
console.log('No jsql user data to remove.');
|
|
133
|
+
}
|
|
134
|
+
for (const f of dataResult.failed) console.error(`Failed to remove ${f.path}: ${f.error}`);
|
|
135
|
+
if (doGlobal) {
|
|
136
|
+
if (globalResult.dryRun) console.log(`Would run: ${globalResult.command}`);
|
|
137
|
+
else if (globalResult.skipped) console.log('Global jsql-neo package is not installed; skipped npm uninstall.');
|
|
138
|
+
else if (globalResult.ok) console.log('Uninstalled global package (npm uninstall -g jsql-neo).');
|
|
139
|
+
else console.error(`Global uninstall failed: ${globalResult.error}`);
|
|
140
|
+
}
|
|
141
|
+
if (!doGlobal) {
|
|
142
|
+
console.log('To remove the package itself, run: npm uninstall -g jsql-neo');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (dataResult.failed.length || (globalResult && !globalResult.ok && !globalResult.skipped)) {
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
}
|
|
148
|
+
} catch (e) {
|
|
149
|
+
fail(e);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
37
153
|
const cli = (yaggs || require('./yaggs-fallback').buildYaggsFallback(_pkg))({
|
|
38
154
|
pkg: _pkg,
|
|
39
155
|
})
|
|
@@ -412,6 +528,9 @@ const cli = (yaggs || require('./yaggs-fallback').buildYaggsFallback(_pkg))({
|
|
|
412
528
|
fail(e);
|
|
413
529
|
}
|
|
414
530
|
})
|
|
531
|
+
.command('update', 'Update jsql-neo to the latest version', updateOptions, updateHandler)
|
|
532
|
+
.command('uninstall', 'Remove jsql user data and/or the global package', uninstallOptions, uninstallHandler)
|
|
533
|
+
.command('remove', 'Alias of uninstall', uninstallOptions, uninstallHandler)
|
|
415
534
|
.command('version', 'Show version', null, () => {
|
|
416
535
|
console.log(require('../package.json').version);
|
|
417
536
|
})
|
package/index.js
CHANGED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* jsql uninstall —— 清理 jsql-neo 的用户级数据(可选卸载全局包)。
|
|
4
|
+
*
|
|
5
|
+
* 红线:
|
|
6
|
+
* - 只允许删除用户 home 目录下、且属于 jsql 的固定路径;
|
|
7
|
+
* - 拒绝删除 home 本身、文件系统根、当前工作目录、包目录(仓库)及其祖先/子目录;
|
|
8
|
+
* - 绝不静默删除:非 TTY 且没有 --yes 时直接拒绝;TTY 下要求交互确认。
|
|
9
|
+
*
|
|
10
|
+
* 覆盖的数据位置(与代码里的实际路径保持一致):
|
|
11
|
+
* ~/.jsql/ 插件注册表安装目录(lib/database.js / src/cli/plugin.js)
|
|
12
|
+
* ~/.config/jsql/ 模块配置 mod.config(lib/mod.js)
|
|
13
|
+
* ~/.jsqlmr star 提示状态(lib/star-nag.js)
|
|
14
|
+
* ~/.cache/jsql/ 可能的缓存(存在才删)
|
|
15
|
+
* 支持 JSQL_PLUGIN_DIR / JSQL_MOD_CONFIG / JSQL_MR_PATH / JSQL_CACHE_DIR 覆盖。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const readline = require('readline');
|
|
22
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const PKG_NAME = 'jsql-neo';
|
|
25
|
+
|
|
26
|
+
function packageDir() {
|
|
27
|
+
return path.resolve(__dirname, '..');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function npmCommand() {
|
|
31
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isInside(child, parent) {
|
|
35
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
36
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 列出所有清理目标。
|
|
41
|
+
* @param {{keepData?: boolean}} [opts]
|
|
42
|
+
* @returns {Array<{path: string, kind: 'dir'|'file', label: string}>}
|
|
43
|
+
*/
|
|
44
|
+
function collectTargets(opts = {}) {
|
|
45
|
+
if (opts.keepData) return [];
|
|
46
|
+
const home = os.homedir();
|
|
47
|
+
const targets = [];
|
|
48
|
+
const add = (p, kind, label) => {
|
|
49
|
+
if (!p) return;
|
|
50
|
+
const resolved = path.resolve(p);
|
|
51
|
+
if (targets.some((t) => t.path === resolved)) return;
|
|
52
|
+
targets.push({ path: resolved, kind, label });
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
add(process.env.JSQL_PLUGIN_DIR || path.join(home, '.jsql'), 'dir', '插件/用户数据 (~/.jsql)');
|
|
56
|
+
|
|
57
|
+
if (process.env.JSQL_MOD_CONFIG) {
|
|
58
|
+
add(process.env.JSQL_MOD_CONFIG, 'file', '模块配置 (JSQL_MOD_CONFIG)');
|
|
59
|
+
} else {
|
|
60
|
+
add(path.join(home, '.config', 'jsql'), 'dir', '模块配置 (~/.config/jsql)');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
add(process.env.JSQL_MR_PATH || path.join(home, '.jsqlmr'), 'file', 'star 状态 (~/.jsqlmr)');
|
|
64
|
+
add(process.env.JSQL_CACHE_DIR || path.join(home, '.cache', 'jsql'), 'dir', '缓存 (~/.cache/jsql)');
|
|
65
|
+
|
|
66
|
+
return targets;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 安全校验:任何目标都必须落在 home 下、且不能碰到仓库/工作目录。
|
|
71
|
+
* @throws {Error} 校验不通过
|
|
72
|
+
*/
|
|
73
|
+
function assertSafeTarget(target) {
|
|
74
|
+
const resolved = path.resolve(target.path || target);
|
|
75
|
+
const home = path.resolve(os.homedir());
|
|
76
|
+
const pkg = packageDir();
|
|
77
|
+
const cwd = path.resolve(process.cwd());
|
|
78
|
+
const root = path.parse(resolved).root;
|
|
79
|
+
|
|
80
|
+
if (resolved === root) {
|
|
81
|
+
throw new Error(`refusing to remove filesystem root: ${resolved}`);
|
|
82
|
+
}
|
|
83
|
+
if (resolved === home) {
|
|
84
|
+
throw new Error(`refusing to remove home directory: ${resolved}`);
|
|
85
|
+
}
|
|
86
|
+
if (!isInside(resolved, home)) {
|
|
87
|
+
throw new Error(`refusing to remove path outside home (${home}): ${resolved}`);
|
|
88
|
+
}
|
|
89
|
+
if (isInside(pkg, resolved) || isInside(resolved, pkg)) {
|
|
90
|
+
throw new Error(`refusing to remove path overlapping the jsql-neo package: ${resolved}`);
|
|
91
|
+
}
|
|
92
|
+
// 目标不能是当前工作目录的祖先(否则会连工作目录一起删掉)
|
|
93
|
+
if (isInside(cwd, resolved)) {
|
|
94
|
+
throw new Error(`refusing to remove an ancestor of the current working directory: ${resolved}`);
|
|
95
|
+
}
|
|
96
|
+
// 目标必须是 jsql 命名的目录/文件,杜绝误删
|
|
97
|
+
const base = path.basename(resolved).toLowerCase();
|
|
98
|
+
const looksLikeJsql = base === '.jsqlmr' || base.startsWith('jsql') || base === '.jsql';
|
|
99
|
+
const parentIsConfig = path.basename(path.dirname(resolved)) === '.config'
|
|
100
|
+
|| path.basename(path.dirname(resolved)) === '.cache';
|
|
101
|
+
if (!looksLikeJsql && !parentIsConfig) {
|
|
102
|
+
throw new Error(`refusing to remove unexpected path: ${resolved}`);
|
|
103
|
+
}
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 检查目标是否存在,并给出可删除列表(不实际删除)。
|
|
109
|
+
* @param {{keepData?: boolean}} [opts]
|
|
110
|
+
*/
|
|
111
|
+
function inspectTargets(opts = {}) {
|
|
112
|
+
return collectTargets(opts).map((t) => {
|
|
113
|
+
let exists = false;
|
|
114
|
+
let isSymlink = false;
|
|
115
|
+
try {
|
|
116
|
+
const st = fs.lstatSync(t.path);
|
|
117
|
+
exists = true;
|
|
118
|
+
isSymlink = st.isSymbolicLink();
|
|
119
|
+
} catch (_) { /* 不存在 */ }
|
|
120
|
+
return { ...t, exists, isSymlink };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 删除数据目标。
|
|
126
|
+
* @param {{keepData?: boolean, dryRun?: boolean}} [opts]
|
|
127
|
+
* @returns {{removed: string[], skipped: string[], failed: Array<{path: string, error: string}>}}
|
|
128
|
+
*/
|
|
129
|
+
function removeData(opts = {}) {
|
|
130
|
+
const result = { removed: [], skipped: [], failed: [] };
|
|
131
|
+
const targets = inspectTargets(opts);
|
|
132
|
+
for (const t of targets) {
|
|
133
|
+
if (!t.exists) {
|
|
134
|
+
result.skipped.push(t.path);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
let safe;
|
|
138
|
+
try {
|
|
139
|
+
safe = assertSafeTarget(t);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
result.failed.push({ path: t.path, error: e.message });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (opts.dryRun) {
|
|
145
|
+
result.removed.push(safe);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
// 符号链接只删链接本身,绝不跟随
|
|
150
|
+
if (t.isSymlink) fs.unlinkSync(safe);
|
|
151
|
+
else fs.rmSync(safe, { recursive: true, force: true });
|
|
152
|
+
result.removed.push(safe);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
result.failed.push({ path: safe, error: e.message });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 卸载全局 npm 包;失败时返回错误信息而不是抛异常 */
|
|
161
|
+
function uninstallGlobal(dryRun) {
|
|
162
|
+
const args = ['uninstall', '-g', PKG_NAME];
|
|
163
|
+
const command = `${npmCommand()} ${args.join(' ')}`;
|
|
164
|
+
if (dryRun) return { ok: true, dryRun: true, command, error: null };
|
|
165
|
+
|
|
166
|
+
const r = spawnSync(npmCommand(), args, {
|
|
167
|
+
stdio: 'inherit',
|
|
168
|
+
timeout: 5 * 60 * 1000,
|
|
169
|
+
});
|
|
170
|
+
if (r.error) return { ok: false, command, error: r.error.message };
|
|
171
|
+
if (r.status !== 0) return { ok: false, command, error: `npm uninstall exited with code ${r.status}` };
|
|
172
|
+
return { ok: true, command, error: null };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 全局安装是否还存在(用 npm root -g 判断) */
|
|
176
|
+
function isGloballyInstalled() {
|
|
177
|
+
try {
|
|
178
|
+
const root = execFileSync(npmCommand(), ['root', '-g'], {
|
|
179
|
+
encoding: 'utf8',
|
|
180
|
+
timeout: 15000,
|
|
181
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
182
|
+
}).trim();
|
|
183
|
+
if (!root) return false;
|
|
184
|
+
return fs.existsSync(path.join(root, PKG_NAME, 'package.json'));
|
|
185
|
+
} catch (_) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 交互确认;非 TTY 返回 false(调用方应要求 --yes) */
|
|
191
|
+
function confirm(question) {
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
if (!process.stdin.isTTY) {
|
|
194
|
+
resolve(false);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
198
|
+
rl.question(question, (answer) => {
|
|
199
|
+
rl.close();
|
|
200
|
+
resolve(/^y(es)?$/i.test(String(answer || '').trim()));
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
PKG_NAME,
|
|
207
|
+
collectTargets,
|
|
208
|
+
assertSafeTarget,
|
|
209
|
+
inspectTargets,
|
|
210
|
+
removeData,
|
|
211
|
+
uninstallGlobal,
|
|
212
|
+
isGloballyInstalled,
|
|
213
|
+
confirm,
|
|
214
|
+
packageDir,
|
|
215
|
+
isInside,
|
|
216
|
+
};
|
package/lib/updater.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* jsql update —— 检查并安装 jsql-neo 的新版本。
|
|
4
|
+
*
|
|
5
|
+
* 设计取舍:
|
|
6
|
+
* - 版本来源优先 npm registry(https://registry.npmjs.org/jsql-neo/latest),
|
|
7
|
+
* 失败时退回 `npm view jsql-neo version`;两者都失败就优雅报错,不 crash。
|
|
8
|
+
* - 只有「全局安装」才自动执行 `npm install -g jsql-neo@latest`;
|
|
9
|
+
* 本地依赖安装绝不自动改用户的 package.json,只打印清晰指令。
|
|
10
|
+
* - `--check` 只看不装;`--dry-run` 打印将要执行的命令;`--force` 即使已是最新也重装。
|
|
11
|
+
*
|
|
12
|
+
* 不引入任何第三方依赖(与项目其它部分保持一致)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const https = require('https');
|
|
17
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const PKG_NAME = 'jsql-neo';
|
|
20
|
+
const REGISTRY_LATEST = `https://registry.npmjs.org/${PKG_NAME}/latest`;
|
|
21
|
+
const FETCH_TIMEOUT_MS = 8000;
|
|
22
|
+
const MAX_BODY_BYTES = 512 * 1024;
|
|
23
|
+
|
|
24
|
+
function packageDir() {
|
|
25
|
+
return path.resolve(__dirname, '..');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function currentVersion() {
|
|
29
|
+
return require('../package.json').version;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── 版本比较 ────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/** 解析 semver(宽松):v1.2.3 / 1.2 / 1.2.3-beta.1 → { nums, pre } */
|
|
35
|
+
function parseVersion(v) {
|
|
36
|
+
let s = String(v == null ? '' : v).trim();
|
|
37
|
+
if (s[0] === 'v' || s[0] === 'V') s = s.slice(1);
|
|
38
|
+
const plus = s.indexOf('+');
|
|
39
|
+
if (plus !== -1) s = s.slice(0, plus);
|
|
40
|
+
const dash = s.indexOf('-');
|
|
41
|
+
const core = dash === -1 ? s : s.slice(0, dash);
|
|
42
|
+
const pre = dash === -1 ? '' : s.slice(dash + 1);
|
|
43
|
+
const nums = core.split('.').map((n) => {
|
|
44
|
+
const x = parseInt(n, 10);
|
|
45
|
+
return Number.isFinite(x) ? x : 0;
|
|
46
|
+
});
|
|
47
|
+
while (nums.length < 3) nums.push(0);
|
|
48
|
+
return { nums, pre };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** semver-ish 比较:a < b → -1,相等 → 0,a > b → 1(prerelease 视为更低) */
|
|
52
|
+
function compareVersions(a, b) {
|
|
53
|
+
const A = parseVersion(a);
|
|
54
|
+
const B = parseVersion(b);
|
|
55
|
+
for (let i = 0; i < 3; i++) {
|
|
56
|
+
if (A.nums[i] !== B.nums[i]) return A.nums[i] > B.nums[i] ? 1 : -1;
|
|
57
|
+
}
|
|
58
|
+
if (A.pre === B.pre) return 0;
|
|
59
|
+
if (!A.pre) return 1;
|
|
60
|
+
if (!B.pre) return -1;
|
|
61
|
+
return A.pre > B.pre ? 1 : -1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isNewer(latest, current) {
|
|
65
|
+
return compareVersions(latest, current) > 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── 网络 ───────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function httpGetText(url, redirects = 0) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
if (redirects > 5) {
|
|
73
|
+
reject(new Error(`too many redirects for ${url}`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let req;
|
|
77
|
+
try {
|
|
78
|
+
req = https.get(url, {
|
|
79
|
+
headers: {
|
|
80
|
+
'User-Agent': `jsql-neo/${currentVersion()} (+https://github.com/vexify-org/JSQL-neo)`,
|
|
81
|
+
Accept: 'application/vnd.npm.install-v1+json, application/json;q=0.9, */*;q=0.1',
|
|
82
|
+
},
|
|
83
|
+
}, (res) => {
|
|
84
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
85
|
+
res.resume();
|
|
86
|
+
const next = new URL(res.headers.location, url).toString();
|
|
87
|
+
if (!/^https?:/i.test(next)) {
|
|
88
|
+
reject(new Error(`invalid redirect target for ${url}`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
httpGetText(next, redirects + 1).then(resolve, reject);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (res.statusCode !== 200) {
|
|
95
|
+
res.resume();
|
|
96
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const chunks = [];
|
|
100
|
+
let received = 0;
|
|
101
|
+
res.on('data', (chunk) => {
|
|
102
|
+
received += chunk.length;
|
|
103
|
+
if (received > MAX_BODY_BYTES) {
|
|
104
|
+
res.destroy();
|
|
105
|
+
reject(new Error(`response too large for ${url}`));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
chunks.push(chunk);
|
|
109
|
+
});
|
|
110
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
111
|
+
res.on('error', reject);
|
|
112
|
+
});
|
|
113
|
+
} catch (e) {
|
|
114
|
+
reject(e);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
req.on('error', reject);
|
|
118
|
+
req.setTimeout(FETCH_TIMEOUT_MS, () => {
|
|
119
|
+
try { req.destroy(); } catch (_) { /* ignore */ }
|
|
120
|
+
reject(new Error(`request timed out after ${FETCH_TIMEOUT_MS}ms: ${url}`));
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 查询 npm 上的最新版本号。
|
|
127
|
+
* @param {{ fetchImpl?: Function, npmView?: boolean }} [opts] 测试可注入
|
|
128
|
+
* @returns {Promise<{version: string, source: string}>}
|
|
129
|
+
*/
|
|
130
|
+
async function fetchLatestVersion(opts = {}) {
|
|
131
|
+
const fetchImpl = opts.fetchImpl || httpGetText;
|
|
132
|
+
const errors = [];
|
|
133
|
+
|
|
134
|
+
// 1) registry HTTP(快,且不需要 npm 进程)
|
|
135
|
+
try {
|
|
136
|
+
const body = await fetchImpl(REGISTRY_LATEST);
|
|
137
|
+
const data = JSON.parse(body);
|
|
138
|
+
if (data && typeof data.version === 'string' && data.version.trim()) {
|
|
139
|
+
return { version: data.version.trim(), source: 'registry' };
|
|
140
|
+
}
|
|
141
|
+
throw new Error('registry response has no version field');
|
|
142
|
+
} catch (e) {
|
|
143
|
+
errors.push(`registry: ${e.message}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2) npm CLI 兜底(有些环境走私有 registry 配置,curl 直连反而不通)
|
|
147
|
+
if (opts.npmView !== false) {
|
|
148
|
+
try {
|
|
149
|
+
const out = execFileSync(npmCommand(), ['view', PKG_NAME, 'version'], {
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
timeout: 15000,
|
|
152
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
153
|
+
}).trim();
|
|
154
|
+
// `npm view` 在旧版本里可能输出多行/带引号
|
|
155
|
+
const m = out.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
|
|
156
|
+
if (m) return { version: m[0], source: 'npm-view' };
|
|
157
|
+
throw new Error(`unexpected npm view output: ${out.slice(0, 120)}`);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
errors.push(`npm view: ${e.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const err = new Error(`could not determine the latest version (${errors.join('; ')})`);
|
|
164
|
+
err.causes = errors;
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── 安装形态判断 ───────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
function npmCommand() {
|
|
171
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isInside(child, parent) {
|
|
175
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
176
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 收集可能的全局 node_modules 根目录 */
|
|
180
|
+
function globalRoots() {
|
|
181
|
+
const roots = [];
|
|
182
|
+
try {
|
|
183
|
+
const out = execFileSync(npmCommand(), ['root', '-g'], {
|
|
184
|
+
encoding: 'utf8',
|
|
185
|
+
timeout: 15000,
|
|
186
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
187
|
+
}).trim();
|
|
188
|
+
if (out) roots.push(out);
|
|
189
|
+
} catch (_) { /* npm 不可用就靠下面的启发式 */ }
|
|
190
|
+
|
|
191
|
+
if (process.env.npm_config_prefix) {
|
|
192
|
+
roots.push(path.join(process.env.npm_config_prefix, 'lib', 'node_modules'));
|
|
193
|
+
if (process.platform === 'win32') {
|
|
194
|
+
roots.push(path.join(process.env.npm_config_prefix, 'node_modules'));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
198
|
+
roots.push(path.join(process.env.APPDATA, 'npm', 'node_modules'));
|
|
199
|
+
}
|
|
200
|
+
return Array.from(new Set(roots.filter(Boolean)));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 判断当前这份 jsql-neo 是否来自全局安装。
|
|
205
|
+
* @param {string} [dir] 包目录,默认是 lib/..
|
|
206
|
+
*/
|
|
207
|
+
function isGlobalInstall(dir) {
|
|
208
|
+
const pkgDir = path.resolve(dir || packageDir());
|
|
209
|
+
return globalRoots().some((root) => isInside(pkgDir, root));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 当前包是否位于某个项目的 node_modules 下(本地依赖安装) */
|
|
213
|
+
function isLocalDependency(dir) {
|
|
214
|
+
const pkgDir = path.resolve(dir || packageDir());
|
|
215
|
+
return path.basename(path.dirname(pkgDir)) === 'node_modules';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── 更新流程 ───────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
function installCommand(version) {
|
|
221
|
+
return `${npmCommand()} install -g ${PKG_NAME}@${version}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 执行更新检查 / 安装。
|
|
226
|
+
* @param {{checkOnly?: boolean, force?: boolean, dryRun?: boolean}} [opts]
|
|
227
|
+
*/
|
|
228
|
+
async function runUpdate(opts = {}) {
|
|
229
|
+
const current = currentVersion();
|
|
230
|
+
let latestInfo;
|
|
231
|
+
try {
|
|
232
|
+
latestInfo = await fetchLatestVersion({ fetchImpl: opts.fetchImpl, npmView: opts.npmView });
|
|
233
|
+
} catch (e) {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
current,
|
|
237
|
+
latest: null,
|
|
238
|
+
error: e.message,
|
|
239
|
+
hints: [
|
|
240
|
+
'检查网络/registry 配置,或手动执行: ' + installCommand('latest'),
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const latest = latestInfo.version;
|
|
246
|
+
const updateAvailable = isNewer(latest, current);
|
|
247
|
+
const global = typeof opts.global === 'boolean' ? opts.global : isGlobalInstall();
|
|
248
|
+
const localDep = typeof opts.localDependency === 'boolean' ? opts.localDependency : isLocalDependency();
|
|
249
|
+
const result = {
|
|
250
|
+
ok: true,
|
|
251
|
+
current,
|
|
252
|
+
latest,
|
|
253
|
+
latestSource: latestInfo.source,
|
|
254
|
+
updateAvailable,
|
|
255
|
+
installType: global ? 'global' : (localDep ? 'local-dependency' : 'unknown'),
|
|
256
|
+
action: 'none',
|
|
257
|
+
command: null,
|
|
258
|
+
installed: false,
|
|
259
|
+
warnings: [],
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (!updateAvailable && !opts.force) {
|
|
263
|
+
result.action = opts.checkOnly ? 'checked' : 'up-to-date';
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (opts.checkOnly) {
|
|
268
|
+
result.action = 'checked';
|
|
269
|
+
result.command = global ? installCommand(latest) : installCommand(latest);
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (opts.dryRun) {
|
|
274
|
+
result.action = 'would-install';
|
|
275
|
+
result.command = installCommand(latest);
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 非全局安装:不自动改动用户项目依赖,只给清晰指令
|
|
280
|
+
if (!global) {
|
|
281
|
+
result.action = 'instructions';
|
|
282
|
+
result.command = installCommand(latest);
|
|
283
|
+
result.localHint = `npm install ${PKG_NAME}@${latest}`;
|
|
284
|
+
result.warnings.push(
|
|
285
|
+
'jsql-neo 不是全局安装(当前位于项目 node_modules 或开发目录),未自动安装。'
|
|
286
|
+
);
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const args = ['install', '-g', `${PKG_NAME}@${latest}`];
|
|
291
|
+
const r = spawnSync(npmCommand(), args, {
|
|
292
|
+
stdio: 'inherit',
|
|
293
|
+
timeout: 10 * 60 * 1000,
|
|
294
|
+
});
|
|
295
|
+
result.command = `${npmCommand()} ${args.join(' ')}`;
|
|
296
|
+
if (r.error) {
|
|
297
|
+
result.ok = false;
|
|
298
|
+
result.error = r.error.message;
|
|
299
|
+
result.action = 'install-failed';
|
|
300
|
+
result.warnings.push('可尝试手动执行并检查权限: ' + result.command);
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
if (r.status !== 0) {
|
|
304
|
+
result.ok = false;
|
|
305
|
+
result.error = `npm install exited with code ${r.status}`;
|
|
306
|
+
result.action = 'install-failed';
|
|
307
|
+
result.warnings.push(
|
|
308
|
+
'如果是权限问题,尝试: sudo ' + result.command,
|
|
309
|
+
);
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
result.installed = true;
|
|
314
|
+
result.action = 'installed';
|
|
315
|
+
result.after = latest;
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** 把 runUpdate 的结果渲染成人读文本 */
|
|
320
|
+
function formatResult(r) {
|
|
321
|
+
const lines = [];
|
|
322
|
+
if (!r.ok) {
|
|
323
|
+
lines.push(`更新失败: ${r.error}`);
|
|
324
|
+
for (const h of r.hints || []) lines.push(` ${h}`);
|
|
325
|
+
return lines.join('\n');
|
|
326
|
+
}
|
|
327
|
+
lines.push(`当前版本: ${r.current}`);
|
|
328
|
+
lines.push(`最新版本: ${r.latest}${r.latestSource === 'npm-view' ? ' (via npm view)' : ''}`);
|
|
329
|
+
|
|
330
|
+
if (r.action === 'up-to-date') {
|
|
331
|
+
lines.push('已是最新版本。');
|
|
332
|
+
} else if (r.action === 'checked') {
|
|
333
|
+
lines.push(r.updateAvailable ? '有新版本可用。' : '已是最新版本。');
|
|
334
|
+
if (r.updateAvailable) lines.push(`运行 \`jsql update\` 安装,或手动执行: ${r.command}`);
|
|
335
|
+
} else if (r.action === 'would-install') {
|
|
336
|
+
lines.push(`将执行: ${r.command}`);
|
|
337
|
+
lines.push('(--dry-run,未做任何修改)');
|
|
338
|
+
} else if (r.action === 'installed') {
|
|
339
|
+
lines.push(`已更新到 ${r.latest}。重新打开终端后 \`jsql version\` 生效。`);
|
|
340
|
+
} else if (r.action === 'instructions') {
|
|
341
|
+
lines.push('检测到非全局安装,未自动更新(避免改动你的项目依赖)。');
|
|
342
|
+
lines.push(`全局安装: ${r.command}`);
|
|
343
|
+
if (r.localHint) lines.push(`项目内升级: ${r.localHint}`);
|
|
344
|
+
} else if (r.action === 'install-failed') {
|
|
345
|
+
lines.push(`安装失败: ${r.error}`);
|
|
346
|
+
}
|
|
347
|
+
for (const w of r.warnings || []) lines.push(`注意: ${w}`);
|
|
348
|
+
return lines.join('\n');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = {
|
|
352
|
+
PKG_NAME,
|
|
353
|
+
REGISTRY_LATEST,
|
|
354
|
+
parseVersion,
|
|
355
|
+
compareVersions,
|
|
356
|
+
isNewer,
|
|
357
|
+
currentVersion,
|
|
358
|
+
fetchLatestVersion,
|
|
359
|
+
isGlobalInstall,
|
|
360
|
+
isLocalDependency,
|
|
361
|
+
globalRoots,
|
|
362
|
+
runUpdate,
|
|
363
|
+
formatResult,
|
|
364
|
+
installCommand,
|
|
365
|
+
};
|
package/package.json
CHANGED
package/postinstall.js
CHANGED
|
@@ -30,13 +30,28 @@ const GHPROXY = process.env.JSQL_GHPROXY !== undefined
|
|
|
30
30
|
? process.env.JSQL_GHPROXY
|
|
31
31
|
: '';
|
|
32
32
|
const GHPROXY_TOKEN = process.env.JSQL_GHPROXY_TOKEN || '';
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
|
|
34
|
+
// ── 原生二进制获取策略 ────────────────────────────────────────────────
|
|
35
|
+
// v6.5.25 起默认【强制从远端拉取最新版】:安装时总是从 GitHub Release 重新
|
|
36
|
+
// 下载与当前包版本匹配的原生模块,校验 SHA256 后覆盖本地同名文件(旧缓存、
|
|
37
|
+
// 手工编译产物都不会被直接信任)。下载失败时保留本地可用模块,最后才回退
|
|
38
|
+
// 源码编译 —— 即「远端优先 / 强制覆盖 / 失败降级」。
|
|
39
|
+
//
|
|
40
|
+
// 环境变量:
|
|
41
|
+
// JSQL_NO_NATIVE_DOWNLOAD=1 完全关闭下载(旧 opt-in 行为,local → 源码编译)
|
|
42
|
+
// JSQL_NATIVE_DOWNLOAD=0 同上(=0 语义)
|
|
43
|
+
// JSQL_AUTO_DOWNLOAD_NATIVE=0 同上(=0 语义)
|
|
44
|
+
// JSQL_NATIVE_FORCE_REFRESH=0 仍下载,但本地模块可用时跳过(旧「缓存优先」)
|
|
45
|
+
// JSQL_NATIVE_TAG=vX.Y.Z|latest 指定远端 tag;默认 v<package.version>
|
|
46
|
+
// JSQL_AUTO_DOWNLOAD_NATIVE=1 / JSQL_NATIVE_DOWNLOAD=1 仍然兼容(显式开)
|
|
47
|
+
//
|
|
48
|
+
// 供应链安全(issue #25):不内置任何加速站/token,代理仍需显式 JSQL_GHPROXY,
|
|
49
|
+
// 且下载必须通过 SHA256SUMS 校验(除非用户显式 JSQL_ALLOW_UNVERIFIED_NATIVE=1)。
|
|
50
|
+
const DOWNLOAD_DISABLED = process.env.JSQL_NO_NATIVE_DOWNLOAD === '1'
|
|
51
|
+
|| process.env.JSQL_NATIVE_DOWNLOAD === '0'
|
|
52
|
+
|| process.env.JSQL_AUTO_DOWNLOAD_NATIVE === '0';
|
|
53
|
+
const FORCE_REFRESH = process.env.JSQL_NATIVE_FORCE_REFRESH !== '0';
|
|
54
|
+
const TAG = process.env.JSQL_NATIVE_TAG || `v${pkg.version}`;
|
|
40
55
|
|
|
41
56
|
// 原生模块体积上限(防代理/镜像返回超大文件打爆磁盘/内存)
|
|
42
57
|
const MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024;
|
|
@@ -76,6 +91,52 @@ function sameOrigin(a, b) {
|
|
|
76
91
|
try { return new URL(a).origin === new URL(b).origin; } catch (_) { return false; }
|
|
77
92
|
}
|
|
78
93
|
|
|
94
|
+
/**
|
|
95
|
+
* 解析 JSQL_NATIVE_TAG=latest:跟随 GitHub `/releases/latest` 的 302 跳转,
|
|
96
|
+
* 从 Location 里取出真实 tag(vX.Y.Z)。失败返回 null —— 调用方回退到当前版本 tag。
|
|
97
|
+
*/
|
|
98
|
+
function resolveLatestTag() {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const url = `https://github.com/${REPO}/releases/latest`;
|
|
101
|
+
let settled = false;
|
|
102
|
+
const done = (v) => { if (!settled) { settled = true; resolve(v); } };
|
|
103
|
+
let req;
|
|
104
|
+
try {
|
|
105
|
+
req = https.get(url, { headers: { 'User-Agent': 'jsql-neo-postinstall' } }, (res) => {
|
|
106
|
+
const loc = res.headers.location || '';
|
|
107
|
+
res.resume();
|
|
108
|
+
const m = String(loc).match(/\/releases\/tag\/([^/?#]+)/);
|
|
109
|
+
done(m ? decodeURIComponent(m[1]) : null);
|
|
110
|
+
});
|
|
111
|
+
} catch (_) {
|
|
112
|
+
done(null);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
req.on('error', () => done(null));
|
|
116
|
+
req.setTimeout(8000, () => { try { req.destroy(); } catch (_) {} done(null); });
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 取得本次要拉取的 release tag。
|
|
122
|
+
* 默认 = v<package.version>(二进制必须与包版本匹配,避免 ABI/API 错配);
|
|
123
|
+
* 只有用户显式 JSQL_NATIVE_TAG=latest 时才去解析最新 release。
|
|
124
|
+
*/
|
|
125
|
+
async function releaseTag() {
|
|
126
|
+
if (TAG.toLowerCase() !== 'latest') return TAG;
|
|
127
|
+
const resolved = await resolveLatestTag();
|
|
128
|
+
if (resolved) {
|
|
129
|
+
console.log(`[jsql-neo] JSQL_NATIVE_TAG=latest resolved to ${resolved}`);
|
|
130
|
+
return resolved;
|
|
131
|
+
}
|
|
132
|
+
console.warn(`[jsql-neo] cannot resolve latest release tag; using v${pkg.version}`);
|
|
133
|
+
return `v${pkg.version}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function withUserAgent(headers) {
|
|
137
|
+
return Object.assign({ 'User-Agent': 'jsql-neo-postinstall' }, headers || {});
|
|
138
|
+
}
|
|
139
|
+
|
|
79
140
|
function safeRedirect(next) {
|
|
80
141
|
return /^https?:/i.test(String(next || ''));
|
|
81
142
|
}
|
|
@@ -87,7 +148,7 @@ function download(url, dest, headers, redirects = 0) {
|
|
|
87
148
|
return;
|
|
88
149
|
}
|
|
89
150
|
const mod = url.startsWith('https:') ? https : http;
|
|
90
|
-
const req = mod.get(url, { headers: headers
|
|
151
|
+
const req = mod.get(url, { headers: withUserAgent(headers) }, (res) => {
|
|
91
152
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
92
153
|
res.resume();
|
|
93
154
|
const next = new URL(res.headers.location, url).toString();
|
|
@@ -137,6 +198,10 @@ function download(url, dest, headers, redirects = 0) {
|
|
|
137
198
|
});
|
|
138
199
|
});
|
|
139
200
|
req.on('error', reject);
|
|
201
|
+
req.setTimeout(60000, () => {
|
|
202
|
+
try { req.destroy(); } catch (_) { /* ignore */ }
|
|
203
|
+
reject(new Error(`download timed out after 60s: ${url}`));
|
|
204
|
+
});
|
|
140
205
|
});
|
|
141
206
|
}
|
|
142
207
|
|
|
@@ -147,7 +212,7 @@ function fetchText(url, headers, redirects = 0) {
|
|
|
147
212
|
return;
|
|
148
213
|
}
|
|
149
214
|
const mod = url.startsWith('https:') ? https : http;
|
|
150
|
-
const req = mod.get(url, { headers: headers
|
|
215
|
+
const req = mod.get(url, { headers: withUserAgent(headers) }, (res) => {
|
|
151
216
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
152
217
|
res.resume();
|
|
153
218
|
const next = new URL(res.headers.location, url).toString();
|
|
@@ -178,6 +243,10 @@ function fetchText(url, headers, redirects = 0) {
|
|
|
178
243
|
res.on('error', reject);
|
|
179
244
|
});
|
|
180
245
|
req.on('error', reject);
|
|
246
|
+
req.setTimeout(20000, () => {
|
|
247
|
+
try { req.destroy(); } catch (_) { /* ignore */ }
|
|
248
|
+
reject(new Error(`request timed out after 20s: ${url}`));
|
|
249
|
+
});
|
|
181
250
|
});
|
|
182
251
|
}
|
|
183
252
|
|
|
@@ -213,7 +282,7 @@ function expectedHashFromManifest(text, asset) {
|
|
|
213
282
|
* 2) 从 GitHub Release 的 SHA256SUMS 资产下载并解析
|
|
214
283
|
* 拿不到就返回 null —— 调用方会拒绝加载未校验的二进制(fail closed)。
|
|
215
284
|
*/
|
|
216
|
-
async function fetchExpectedHash(asset) {
|
|
285
|
+
async function fetchExpectedHash(asset, tag) {
|
|
217
286
|
const explicit = process.env.JSQL_NATIVE_SHA256;
|
|
218
287
|
if (explicit) {
|
|
219
288
|
if (!/^[a-fA-F0-9]{64}$/.test(explicit.trim())) {
|
|
@@ -221,7 +290,7 @@ async function fetchExpectedHash(asset) {
|
|
|
221
290
|
}
|
|
222
291
|
return explicit.trim().toLowerCase();
|
|
223
292
|
}
|
|
224
|
-
const raw = `https://github.com/${REPO}/releases/download/${
|
|
293
|
+
const raw = `https://github.com/${REPO}/releases/download/${tag}/${CHECKSUM_ASSET}`;
|
|
225
294
|
for (const cand of proxyCandidates(raw)) {
|
|
226
295
|
try {
|
|
227
296
|
const text = await fetchText(cand.url, cand.viaProxy ? proxyHeaders() : undefined);
|
|
@@ -271,7 +340,43 @@ function tryLoadNative() {
|
|
|
271
340
|
}
|
|
272
341
|
}
|
|
273
342
|
|
|
274
|
-
|
|
343
|
+
/**
|
|
344
|
+
* 把已经校验过的临时文件安装到 NATIVE_PATH:
|
|
345
|
+
* 先备份现有模块(若存在),再原子替换。返回备份路径供失败回滚。
|
|
346
|
+
*/
|
|
347
|
+
function installDownloadedModule(tmp) {
|
|
348
|
+
fs.mkdirSync(path.dirname(NATIVE_PATH), { recursive: true });
|
|
349
|
+
const backup = `${NATIVE_PATH}.previous`;
|
|
350
|
+
let hadBackup = false;
|
|
351
|
+
if (fs.existsSync(NATIVE_PATH)) {
|
|
352
|
+
try {
|
|
353
|
+
fs.copyFileSync(NATIVE_PATH, backup);
|
|
354
|
+
hadBackup = true;
|
|
355
|
+
} catch (_) { /* 备份失败不阻塞覆盖,但会失去回滚能力 */ }
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
fs.renameSync(tmp, NATIVE_PATH);
|
|
359
|
+
} catch (e) {
|
|
360
|
+
// Windows 上 rename 覆盖已存在文件会 EEXIST/EPERM
|
|
361
|
+
if (e.code === 'EEXIST' || e.code === 'EPERM' || e.code === 'EACCES') {
|
|
362
|
+
try { fs.unlinkSync(NATIVE_PATH); } catch (_) { /* ignore */ }
|
|
363
|
+
fs.renameSync(tmp, NATIVE_PATH);
|
|
364
|
+
} else {
|
|
365
|
+
throw e;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
fs.chmodSync(NATIVE_PATH, 0o755);
|
|
369
|
+
return { backup: hadBackup ? backup : null };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 强制从远端拉取原生模块(默认路径)。
|
|
374
|
+
* 关键点:不因本地已有同名文件而跳过 —— 每次都重新下载、校验、覆盖安装。
|
|
375
|
+
*
|
|
376
|
+
* @param {{ tag?: string }} [opts]
|
|
377
|
+
* @returns {Promise<boolean>} 是否成功安装并加载
|
|
378
|
+
*/
|
|
379
|
+
async function downloadAndVerify(opts = {}) {
|
|
275
380
|
if (!supportsNativeNapi()) {
|
|
276
381
|
console.warn(
|
|
277
382
|
`[jsql-neo] Node ${process.versions.node} provides N-API ${nativeNapiVersion() || 'unknown'}, ` +
|
|
@@ -279,13 +384,14 @@ async function downloadAndVerify() {
|
|
|
279
384
|
);
|
|
280
385
|
return false;
|
|
281
386
|
}
|
|
387
|
+
const tag = opts.tag || await releaseTag();
|
|
282
388
|
const libc = detectLibc();
|
|
283
389
|
const asset = process.env.JSQL_NATIVE_ASSET || `jsql-neo-native.${nativeTargetSuffix(process.platform, process.arch, libc)}.node`;
|
|
284
|
-
const raw = `https://github.com/${REPO}/releases/download/${
|
|
390
|
+
const raw = `https://github.com/${REPO}/releases/download/${tag}/${asset}`;
|
|
285
391
|
|
|
286
392
|
let expected = null;
|
|
287
393
|
try {
|
|
288
|
-
expected = await fetchExpectedHash(asset);
|
|
394
|
+
expected = await fetchExpectedHash(asset, tag);
|
|
289
395
|
} catch (e) {
|
|
290
396
|
console.warn('[jsql-neo] failed to resolve native checksum:', e.message);
|
|
291
397
|
}
|
|
@@ -294,13 +400,13 @@ async function downloadAndVerify() {
|
|
|
294
400
|
if (!expected && !allowUnverified) {
|
|
295
401
|
if (libc === 'musl') {
|
|
296
402
|
console.warn(
|
|
297
|
-
`[jsql-neo] musl libc detected but
|
|
403
|
+
`[jsql-neo] musl libc detected but release ${tag} has no native asset for ${asset}; ` +
|
|
298
404
|
'falling back to the WASM/JS engine. Set JSQL_NATIVE_ASSET to a compatible musl build if available.'
|
|
299
405
|
);
|
|
300
406
|
} else {
|
|
301
407
|
// fail closed:拿不到 SHA256SUMS 就不下载,绝不 require 未经校验的二进制
|
|
302
408
|
console.warn(
|
|
303
|
-
|
|
409
|
+
`[jsql-neo] no SHA256SUMS available for release ${tag}; refusing to download an unverified native module.\n` +
|
|
304
410
|
'[jsql-neo] set JSQL_NATIVE_SHA256=<64-hex> to pin a checksum, or JSQL_ALLOW_UNVERIFIED_NATIVE=1 to override (unsafe).'
|
|
305
411
|
);
|
|
306
412
|
}
|
|
@@ -312,7 +418,7 @@ async function downloadAndVerify() {
|
|
|
312
418
|
|
|
313
419
|
// 依次尝试:环境变量指定的代理 → 内置加速站 → 直连
|
|
314
420
|
for (const cand of proxyCandidates(raw)) {
|
|
315
|
-
console.log(`[jsql-neo]
|
|
421
|
+
console.log(`[jsql-neo] fetching native module (${tag}): ${cand.url}`);
|
|
316
422
|
const tmp = `${NATIVE_PATH}.${process.pid}.download`;
|
|
317
423
|
try {
|
|
318
424
|
// 加速站鉴权走 Authorization 头,直连时不带
|
|
@@ -325,13 +431,23 @@ async function downloadAndVerify() {
|
|
|
325
431
|
}
|
|
326
432
|
console.log('[jsql-neo] checksum verified:', actual);
|
|
327
433
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
434
|
+
|
|
435
|
+
// 强制覆盖:即便本地已有同名文件也替换(旧模块先备份,加载失败可回滚)
|
|
436
|
+
const installed = installDownloadedModule(tmp);
|
|
331
437
|
if (tryLoadNative()) {
|
|
332
|
-
|
|
438
|
+
if (installed.backup) {
|
|
439
|
+
try { fs.unlinkSync(installed.backup); } catch (_) { /* ignore */ }
|
|
440
|
+
}
|
|
441
|
+
console.log(`[jsql-neo] native module updated: native/jsql-neo-native.node (${tag})`);
|
|
333
442
|
return true;
|
|
334
443
|
}
|
|
444
|
+
// 新模块无法加载 → 回滚到之前的可用模块
|
|
445
|
+
if (installed.backup) {
|
|
446
|
+
try {
|
|
447
|
+
fs.renameSync(installed.backup, NATIVE_PATH);
|
|
448
|
+
console.warn('[jsql-neo] rolled back to the previous native module.');
|
|
449
|
+
} catch (_) { /* ignore */ }
|
|
450
|
+
}
|
|
335
451
|
console.warn('[jsql-neo] downloaded module cannot be loaded; trying next source.');
|
|
336
452
|
} catch (e) {
|
|
337
453
|
console.warn('[jsql-neo] download failed:', e.message);
|
|
@@ -342,22 +458,13 @@ async function downloadAndVerify() {
|
|
|
342
458
|
return false;
|
|
343
459
|
}
|
|
344
460
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (process.env.CI === 'true' || process.env.CI === '1') return true;
|
|
352
|
-
return false;
|
|
353
|
-
}
|
|
354
|
-
|
|
461
|
+
/**
|
|
462
|
+
* 确保原生模块可用。
|
|
463
|
+
*
|
|
464
|
+
* 默认行为(v6.5.25+):总是从远端 release 强制拉取并覆盖本地模块;
|
|
465
|
+
* 失败时按「本地已有模块 → 源码编译 → WASM/JS」逐级降级,绝不让安装失败。
|
|
466
|
+
*/
|
|
355
467
|
async function ensureNativeNode() {
|
|
356
|
-
// 已存在且能加载 → 直接用;否则默认不再自动下载(opt-in),只回退源码编译。
|
|
357
|
-
if (tryLoadNative()) {
|
|
358
|
-
console.log('[jsql-neo] native module ready: native/jsql-neo-native.node');
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
468
|
if (!supportsNativeNapi()) {
|
|
362
469
|
console.warn(
|
|
363
470
|
`[jsql-neo] Node ${process.versions.node} provides N-API ${nativeNapiVersion() || 'unknown'}, ` +
|
|
@@ -365,14 +472,35 @@ async function ensureNativeNode() {
|
|
|
365
472
|
);
|
|
366
473
|
return;
|
|
367
474
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
'[jsql-neo] native module
|
|
373
|
-
|
|
374
|
-
|
|
475
|
+
|
|
476
|
+
if (DOWNLOAD_DISABLED) {
|
|
477
|
+
console.warn('[jsql-neo] native download disabled by env (JSQL_NO_NATIVE_DOWNLOAD=1 / JSQL_NATIVE_DOWNLOAD=0); using local module or source build.');
|
|
478
|
+
if (tryLoadNative()) {
|
|
479
|
+
console.log('[jsql-neo] native module ready: native/jsql-neo-native.node');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
buildNativeFromSource();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const localReady = tryLoadNative();
|
|
487
|
+
if (localReady && !FORCE_REFRESH) {
|
|
488
|
+
console.log('[jsql-neo] native module ready (JSQL_NATIVE_FORCE_REFRESH=0, remote refresh skipped): native/jsql-neo-native.node');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
console.log(localReady
|
|
493
|
+
? '[jsql-neo] local native module found; forcing refresh from remote release (set JSQL_NATIVE_FORCE_REFRESH=0 to keep the local copy)'
|
|
494
|
+
: '[jsql-neo] fetching native module from remote release...');
|
|
495
|
+
|
|
496
|
+
if (await downloadAndVerify()) return;
|
|
497
|
+
|
|
498
|
+
// 拉取失败:本地可用就继续用本地 —— 网络问题不该把已经能跑的安装搞坏
|
|
499
|
+
if (tryLoadNative()) {
|
|
500
|
+
console.warn('[jsql-neo] remote refresh failed; keeping the existing local native module.');
|
|
501
|
+
return;
|
|
375
502
|
}
|
|
503
|
+
console.warn('[jsql-neo] could not fetch a verified native module; falling back to source build / WASM.');
|
|
376
504
|
buildNativeFromSource();
|
|
377
505
|
}
|
|
378
506
|
|
|
@@ -416,4 +544,11 @@ if (require.main === module) {
|
|
|
416
544
|
main();
|
|
417
545
|
}
|
|
418
546
|
|
|
419
|
-
module.exports = {
|
|
547
|
+
module.exports = {
|
|
548
|
+
buildNativeFromSource,
|
|
549
|
+
downloadAndVerify,
|
|
550
|
+
ensureNativeNode,
|
|
551
|
+
releaseTag,
|
|
552
|
+
resolveLatestTag,
|
|
553
|
+
installDownloadedModule,
|
|
554
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* v6.5.25: `jsql update` / `jsql uninstall` helpers — offline unit tests. */
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const updater = require('../lib/updater');
|
|
9
|
+
const uninstaller = require('../lib/uninstaller');
|
|
10
|
+
|
|
11
|
+
let pass = 0, fail = 0;
|
|
12
|
+
function ok(name, cond) {
|
|
13
|
+
if (cond) { pass++; console.log(`[OK] ${name}`); }
|
|
14
|
+
else { fail++; console.log(`[FAIL] ${name}`); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fakeFetch(version) {
|
|
18
|
+
return async () => JSON.stringify({ name: 'jsql-neo', version });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
(async () => {
|
|
22
|
+
// ── version comparison ─────────────────────────────────
|
|
23
|
+
ok('compareVersions 6.5.24 < 6.5.25', updater.compareVersions('6.5.24', '6.5.25') === -1);
|
|
24
|
+
ok('compareVersions v6.5.4 > 6.5.3', updater.compareVersions('v6.5.4', '6.5.3') === 1);
|
|
25
|
+
ok('compareVersions equal', updater.compareVersions('6.5.24', 'v6.5.24') === 0);
|
|
26
|
+
ok('prerelease is lower', updater.compareVersions('6.5.25-beta.1', '6.5.25') === -1);
|
|
27
|
+
ok('isNewer true', updater.isNewer('6.5.25', '6.5.24'));
|
|
28
|
+
ok('isNewer false', !updater.isNewer('6.5.24', '6.5.24'));
|
|
29
|
+
|
|
30
|
+
// ── fetchLatestVersion (injected fetch) ────────────────
|
|
31
|
+
const info = await updater.fetchLatestVersion({ fetchImpl: fakeFetch('9.9.9'), npmView: false });
|
|
32
|
+
ok('fetchLatestVersion parses registry json', info.version === '9.9.9' && info.source === 'registry');
|
|
33
|
+
|
|
34
|
+
let threw = false;
|
|
35
|
+
try {
|
|
36
|
+
await updater.fetchLatestVersion({ fetchImpl: async () => { throw new Error('offline'); }, npmView: false });
|
|
37
|
+
} catch (e) { threw = true; }
|
|
38
|
+
ok('fetchLatestVersion fails gracefully', threw);
|
|
39
|
+
|
|
40
|
+
// ── runUpdate branches ─────────────────────────────────
|
|
41
|
+
const upToDate = await updater.runUpdate({
|
|
42
|
+
fetchImpl: fakeFetch(updater.currentVersion()), npmView: false, global: false,
|
|
43
|
+
});
|
|
44
|
+
ok('update: up-to-date', upToDate.ok && upToDate.action === 'up-to-date' && !upToDate.updateAvailable);
|
|
45
|
+
|
|
46
|
+
const check = await updater.runUpdate({
|
|
47
|
+
fetchImpl: fakeFetch('99.0.0'), npmView: false, global: false, checkOnly: true,
|
|
48
|
+
});
|
|
49
|
+
ok('update: check only', check.ok && check.action === 'checked' && check.updateAvailable);
|
|
50
|
+
|
|
51
|
+
const local = await updater.runUpdate({
|
|
52
|
+
fetchImpl: fakeFetch('99.0.0'), npmView: false, global: false,
|
|
53
|
+
});
|
|
54
|
+
ok('update: non-global gives instructions', local.action === 'instructions' && !!local.command);
|
|
55
|
+
ok('update: never auto-installs locally', local.installed === false);
|
|
56
|
+
|
|
57
|
+
const dry = await updater.runUpdate({
|
|
58
|
+
fetchImpl: fakeFetch('99.0.0'), npmView: false, global: true, dryRun: true,
|
|
59
|
+
});
|
|
60
|
+
ok('update: global dry-run', dry.action === 'would-install' && /install -g/.test(dry.command));
|
|
61
|
+
|
|
62
|
+
ok('formatResult renders versions', /当前版本/.test(updater.formatResult(upToDate)));
|
|
63
|
+
|
|
64
|
+
// ── uninstall: path collection + safety ────────────────
|
|
65
|
+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-uninstall-'));
|
|
66
|
+
const oldHome = process.env.HOME;
|
|
67
|
+
const oldMod = process.env.JSQL_MOD_CONFIG;
|
|
68
|
+
const oldMr = process.env.JSQL_MR_PATH;
|
|
69
|
+
const oldCache = process.env.JSQL_CACHE_DIR;
|
|
70
|
+
try {
|
|
71
|
+
process.env.HOME = tmpHome;
|
|
72
|
+
delete process.env.JSQL_MOD_CONFIG;
|
|
73
|
+
delete process.env.JSQL_MR_PATH;
|
|
74
|
+
delete process.env.JSQL_CACHE_DIR;
|
|
75
|
+
|
|
76
|
+
fs.mkdirSync(path.join(tmpHome, '.jsql', 'plugins'), { recursive: true });
|
|
77
|
+
fs.writeFileSync(path.join(tmpHome, '.jsql', 'plugins', 'p.json'), '{}');
|
|
78
|
+
fs.mkdirSync(path.join(tmpHome, '.config', 'jsql'), { recursive: true });
|
|
79
|
+
fs.writeFileSync(path.join(tmpHome, '.config', 'jsql', 'mod.config'), '{}');
|
|
80
|
+
fs.writeFileSync(path.join(tmpHome, '.jsqlmr'), '{}');
|
|
81
|
+
|
|
82
|
+
const targets = uninstaller.collectTargets();
|
|
83
|
+
ok('uninstall: collects user data paths', targets.length >= 3);
|
|
84
|
+
ok('uninstall: keep-data collects nothing', uninstaller.collectTargets({ keepData: true }).length === 0);
|
|
85
|
+
|
|
86
|
+
const inspected = uninstaller.inspectTargets();
|
|
87
|
+
ok('uninstall: detects existing paths', inspected.filter(t => t.exists).length === 3);
|
|
88
|
+
|
|
89
|
+
const dryRun = uninstaller.removeData({ dryRun: true });
|
|
90
|
+
ok('uninstall: dry-run deletes nothing', fs.existsSync(path.join(tmpHome, '.jsql')) && dryRun.removed.length === 3);
|
|
91
|
+
|
|
92
|
+
const removed = uninstaller.removeData();
|
|
93
|
+
ok('uninstall: removes data', removed.removed.length === 3 && removed.failed.length === 0);
|
|
94
|
+
ok('uninstall: .jsql gone', !fs.existsSync(path.join(tmpHome, '.jsql')));
|
|
95
|
+
ok('uninstall: .jsqlmr gone', !fs.existsSync(path.join(tmpHome, '.jsqlmr')));
|
|
96
|
+
ok('uninstall: home still exists', fs.existsSync(tmpHome));
|
|
97
|
+
|
|
98
|
+
// malicious override must be refused
|
|
99
|
+
process.env.JSQL_MR_PATH = path.join(uninstaller.packageDir(), 'README.md');
|
|
100
|
+
const bad = uninstaller.removeData();
|
|
101
|
+
ok('uninstall: refuses path inside package', bad.failed.length === 1 && fs.existsSync(path.join(uninstaller.packageDir(), 'README.md')));
|
|
102
|
+
} finally {
|
|
103
|
+
process.env.HOME = oldHome;
|
|
104
|
+
if (oldMod === undefined) delete process.env.JSQL_MOD_CONFIG; else process.env.JSQL_MOD_CONFIG = oldMod;
|
|
105
|
+
if (oldMr === undefined) delete process.env.JSQL_MR_PATH; else process.env.JSQL_MR_PATH = oldMr;
|
|
106
|
+
if (oldCache === undefined) delete process.env.JSQL_CACHE_DIR; else process.env.JSQL_CACHE_DIR = oldCache;
|
|
107
|
+
try { fs.rmSync(tmpHome, { recursive: true, force: true }); } catch (_) { /* ignore */ }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(`\n=== pass ${pass} fail ${fail} ===`);
|
|
111
|
+
process.exit(fail ? 1 : 0);
|
|
112
|
+
})();
|