jsql-neo 6.5.24 → 6.5.26
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/sql.js +25 -0
- package/lib/uninstaller.js +216 -0
- package/lib/updater.js +365 -0
- package/package.json +3 -3
- package/postinstall.js +179 -44
- package/test/cli-maintenance.test.js +112 -0
- package/test/result-raw.test.js +93 -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.26** — 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
package/lib/sql.js
CHANGED
|
@@ -8263,6 +8263,31 @@ function buildResultEnvelope(stmt, r, durationMs) {
|
|
|
8263
8263
|
: (typeof out.affectedRows === 'number' ? out.affectedRows : 0);
|
|
8264
8264
|
}
|
|
8265
8265
|
if (out.warnings === undefined) out.warnings = [];
|
|
8266
|
+
/*
|
|
8267
|
+
* result.raw 的原始契约:与 columns / rows 指向**同一份**结果,只是对象形式。
|
|
8268
|
+
*
|
|
8269
|
+
* 证据在 lib/prepared.js 的 get():raw 缺失时它会用 columns + rows[0]
|
|
8270
|
+
* 现场拼出等价对象 —— 说明 raw 本就该是「columns × rows 的 zip」。
|
|
8271
|
+
*
|
|
8272
|
+
* 但 executeSelect 的多个分支直接把「投影前的源行」塞进 raw
|
|
8273
|
+
* (rows: mapped, raw: rows)。于是聚合 / 计算列 / 无 FROM 查询里,
|
|
8274
|
+
* raw 变成 undefined、{_virtual:true},或是缺了计算列的源行对象
|
|
8275
|
+
* (GROUP BY 还会泄露内部 _group)。见 issue #43。
|
|
8276
|
+
*
|
|
8277
|
+
* 与其逐个分支改,不如在统一出口兑现契约:只要结果是 SELECT 且有
|
|
8278
|
+
* columns + rows,就从投影后的元组重建对象形式。非元组行保持原样。
|
|
8279
|
+
*/
|
|
8280
|
+
if (t === 'select' && Array.isArray(out.columns) && Array.isArray(out.rows)) {
|
|
8281
|
+
const cols = out.columns;
|
|
8282
|
+
out.raw = out.rows.map((row, i) => {
|
|
8283
|
+
if (!Array.isArray(row)) {
|
|
8284
|
+
return (Array.isArray(out.raw) && out.raw[i] !== undefined) ? out.raw[i] : row;
|
|
8285
|
+
}
|
|
8286
|
+
const obj = {};
|
|
8287
|
+
for (let j = 0; j < cols.length; j++) obj[cols[j]] = row[j];
|
|
8288
|
+
return obj;
|
|
8289
|
+
});
|
|
8290
|
+
}
|
|
8266
8291
|
if (out.durationMs === undefined) out.durationMs = durationMs;
|
|
8267
8292
|
if (out.message === undefined) {
|
|
8268
8293
|
out.message = t === 'select'
|
|
@@ -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
|
+
};
|