dsh-db-tool 1.7.0 → 1.7.2
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/dist/adapters/dmdb/index.js +40 -30
- package/dist/adapters/oracle/index.js +37 -30
- package/dist/adapters/sql-shared/pg-like.js +7 -0
- package/dist/adapters/sqlite/index.js +7 -0
- package/dist/adapters/transfer/model.js +19 -4
- package/package.json +1 -1
- package/scripts/fix-dsh-install.ps1 +155 -0
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* - DM 事务隐式开始,无需 SET TRANSACTION。
|
|
10
10
|
*
|
|
11
11
|
* 驱动要点:
|
|
12
|
-
* - db.createPool({ connectString: 'dm://user:pass@host:5236',
|
|
13
|
-
* poolTimeout: 60, queueTimeout: 60000 })
|
|
14
|
-
* @napi-rs/snappy 跨平台坑)。
|
|
12
|
+
* - db.createPool({ connectString: 'dm://user:pass@host:5236', poolAlias: <逐池唯一>,
|
|
13
|
+
* poolMin: 1, poolMax: 10, poolTimeout: 60, queueTimeout: 60000 });不启用压缩
|
|
14
|
+
* (默认 compress=0,规避 @napi-rs/snappy 跨平台坑)。
|
|
15
|
+
* - poolAlias 必须逐池唯一(不传即撞驱动缺省 'default' 并抛 [20006];见 nextPoolAlias 注释)。
|
|
15
16
|
* - execute 必须显式 maxRows(默认 0=无限制);autoCommit 显式传 true 使语义
|
|
16
17
|
* 不依赖驱动默认(驱动连接属性默认已是自动提交:dmdb connection.js
|
|
17
18
|
* conn_prop_autoCommit = !0;d.ts 三级优先链 执行选项 > dmdb.autoCommit > 连接属性)。
|
|
@@ -84,6 +85,39 @@ export function resolveDmConn(conn) {
|
|
|
84
85
|
const password = typeof f.password === 'string' ? f.password : '';
|
|
85
86
|
return { connectString: `dm://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}`, user, password };
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* 连接池别名:dmdb 把池登记在模块级全局 Map(dmdb.pools),key 即 poolAlias。
|
|
90
|
+
* 驱动缺省别名恒为 'default'(driver/pool.js `_poolAlias = "default"`,仅显式传参才覆盖),
|
|
91
|
+
* 且判重无条件执行 → 不传别名时第二个池必抛 [20006] 连接池别名已存在(src/dm.js:236)。
|
|
92
|
+
* 本插件同进程内可并存多个池(连接的 ro/rw 各一份缓存、testConnection/testDraft 的
|
|
93
|
+
* 一次性池、控制台事务会话、数据传输独立池),故必须逐池唯一。
|
|
94
|
+
* 别名同时是 close 的回收键:正常路径 driver/pool.js 会 `pools.delete(alias)`,
|
|
95
|
+
* 但 close 抛错的异常分支会跳过该删除(登记项残留,插件侧无可观测点)。
|
|
96
|
+
*/
|
|
97
|
+
let poolSeq = 0;
|
|
98
|
+
function nextPoolAlias(conn) {
|
|
99
|
+
return `dsh-dm-${conn.meta.id}-${++poolSeq}`;
|
|
100
|
+
}
|
|
101
|
+
/** 建池(连接串解析、池参数与别名单点维护,供适配器与数据传输入口共用) */
|
|
102
|
+
async function createDmPool(conn) {
|
|
103
|
+
const { connectString } = resolveDmConn(conn);
|
|
104
|
+
try {
|
|
105
|
+
// 注意:实参对象不要直接包进 `as unknown as` 断言——变异工具不为该形态生成
|
|
106
|
+
// ObjectLiteral 变异体,「删掉 poolAlias」这类关键退化会在变异报告中不可见(已验证)。
|
|
107
|
+
const pool = await db.createPool({
|
|
108
|
+
connectString,
|
|
109
|
+
poolAlias: nextPoolAlias(conn),
|
|
110
|
+
poolMin: 1,
|
|
111
|
+
poolMax: 10,
|
|
112
|
+
poolTimeout: 60,
|
|
113
|
+
queueTimeout: 60000,
|
|
114
|
+
});
|
|
115
|
+
return pool;
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
throw humanizeDmError(e);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
87
121
|
export async function createDmAdapter(conn, opts) {
|
|
88
122
|
const mode = opts?.mode ?? 'rw';
|
|
89
123
|
let pool;
|
|
@@ -91,19 +125,7 @@ export async function createDmAdapter(conn, opts) {
|
|
|
91
125
|
pool = opts.pool;
|
|
92
126
|
}
|
|
93
127
|
else {
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
pool = (await db.createPool({
|
|
97
|
-
connectString,
|
|
98
|
-
poolMin: 1,
|
|
99
|
-
poolMax: 10,
|
|
100
|
-
poolTimeout: 60,
|
|
101
|
-
queueTimeout: 60000,
|
|
102
|
-
}));
|
|
103
|
-
}
|
|
104
|
-
catch (e) {
|
|
105
|
-
throw humanizeDmError(e);
|
|
106
|
-
}
|
|
128
|
+
pool = await createDmPool(conn);
|
|
107
129
|
}
|
|
108
130
|
/** 默认 execute 选项:显式 maxRows(dmdb 默认 0=无限制,必须覆盖)+ 显式 autoCommit:true
|
|
109
131
|
* (驱动连接属性默认已是 true,显式传递仅让语义不依赖驱动默认;SELECT 立即结束隐式事务,避免 ro 事务悬挂) */
|
|
@@ -335,19 +357,7 @@ export async function createDmAdapter(conn, opts) {
|
|
|
335
357
|
return adapter;
|
|
336
358
|
}
|
|
337
359
|
export const factory = async (conn) => createDmAdapter(conn);
|
|
338
|
-
/**
|
|
360
|
+
/** 数据传输读写端用的独立连接池(别名逐池唯一,可与主连接池并存) */
|
|
339
361
|
export async function openDmPool(conn) {
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
return (await db.createPool({
|
|
343
|
-
connectString,
|
|
344
|
-
poolMin: 1,
|
|
345
|
-
poolMax: 10,
|
|
346
|
-
poolTimeout: 60,
|
|
347
|
-
queueTimeout: 60000,
|
|
348
|
-
}));
|
|
349
|
-
}
|
|
350
|
-
catch (e) {
|
|
351
|
-
throw humanizeDmError(e);
|
|
352
|
-
}
|
|
362
|
+
return await createDmPool(conn);
|
|
353
363
|
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Oracle 适配器(oracledb v7 thin 模式,DB 12.1+)。
|
|
3
3
|
*
|
|
4
4
|
* 设计要点(依据官方文档调研):
|
|
5
|
-
* - oracledb.createPool({ user, password, connectString,
|
|
5
|
+
* - oracledb.createPool({ user, password, connectString, poolAlias: <逐池唯一>,
|
|
6
|
+
* poolMin: 1, poolMax: 10, poolTimeout: 60 })。
|
|
6
7
|
* 禁止 SYSDBA 等特权连接(不透传 privilege)。
|
|
8
|
+
* - poolAlias 显式传唯一值:不传则落到匿名池(不可经 getPool() 检索),见 createOraPool 注释。
|
|
7
9
|
* - CLOB 经全局 fetchAsString 转字符串;BLOB 经全局 fetchAsBuffer 转 Buffer。
|
|
8
10
|
* (fetchInfo 的键是【列名】而非类型名,按类型统一转换只能用全局 fetchXXX 属性——
|
|
9
11
|
* 官方 connection.rst「fetchInfo」节:「Each column is specified by name」)
|
|
@@ -129,6 +131,37 @@ export function resolveOracleConn(conn) {
|
|
|
129
131
|
connectString: `${host}:${port}/${service}`,
|
|
130
132
|
};
|
|
131
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* 连接池别名:oracledb 的规则与 dmdb 不同——只有「显式传入的别名」或
|
|
136
|
+
* 「'default' 尚空闲时取到的缺省值」会登记进模块级 poolCache;两者都被占用时
|
|
137
|
+
* 驱动创建的是 poolAlias === undefined 的匿名池(不报错,也无法经 getPool() 检索),
|
|
138
|
+
* 只有显式重复别名才抛 NJS-046(lib/oracledb.js:640-649、lib/errors.js:352)。
|
|
139
|
+
* 本插件同进程内可并存多个池(连接的 ro/rw 各一份缓存、testConnection/testDraft 的
|
|
140
|
+
* 一次性池、控制台事务会话、数据传输独立池),故显式传唯一别名:每池身份可检索,
|
|
141
|
+
* 且不抢占 getPool() 的缺省键 'default'。回收靠 '_afterPoolClose' 事件(close 抛错则不触发)。
|
|
142
|
+
*/
|
|
143
|
+
let poolSeq = 0;
|
|
144
|
+
function nextPoolAlias(conn) {
|
|
145
|
+
return `dsh-ora-${conn.meta.id}-${++poolSeq}`;
|
|
146
|
+
}
|
|
147
|
+
/** 建池(连接串解析、池参数与别名单点维护,供适配器与数据传输入口共用) */
|
|
148
|
+
async function createOraPool(conn) {
|
|
149
|
+
const { user, password, connectString } = resolveOracleConn(conn);
|
|
150
|
+
try {
|
|
151
|
+
return await oracledb.createPool({
|
|
152
|
+
user,
|
|
153
|
+
password,
|
|
154
|
+
connectString,
|
|
155
|
+
poolAlias: nextPoolAlias(conn),
|
|
156
|
+
poolMin: 1,
|
|
157
|
+
poolMax: 10,
|
|
158
|
+
poolTimeout: 60,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
throw humanizeOraError(e);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
132
165
|
export async function createOracleAdapter(conn, opts) {
|
|
133
166
|
const mode = opts?.mode ?? 'rw';
|
|
134
167
|
let pool;
|
|
@@ -136,20 +169,7 @@ export async function createOracleAdapter(conn, opts) {
|
|
|
136
169
|
pool = opts.pool;
|
|
137
170
|
}
|
|
138
171
|
else {
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
pool = await oracledb.createPool({
|
|
142
|
-
user,
|
|
143
|
-
password,
|
|
144
|
-
connectString,
|
|
145
|
-
poolMin: 1,
|
|
146
|
-
poolMax: 10,
|
|
147
|
-
poolTimeout: 60,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
catch (e) {
|
|
151
|
-
throw humanizeOraError(e);
|
|
152
|
-
}
|
|
172
|
+
pool = await createOraPool(conn);
|
|
153
173
|
}
|
|
154
174
|
function requireRw(action) {
|
|
155
175
|
if (mode === 'ro') {
|
|
@@ -387,20 +407,7 @@ export async function createOracleAdapter(conn, opts) {
|
|
|
387
407
|
return adapter;
|
|
388
408
|
}
|
|
389
409
|
export const factory = async (conn) => createOracleAdapter(conn);
|
|
390
|
-
/**
|
|
410
|
+
/** 数据传输读写端用的独立连接池(别名逐池唯一,可与主连接池并存) */
|
|
391
411
|
export async function openOraclePool(conn) {
|
|
392
|
-
|
|
393
|
-
try {
|
|
394
|
-
return await oracledb.createPool({
|
|
395
|
-
user,
|
|
396
|
-
password,
|
|
397
|
-
connectString,
|
|
398
|
-
poolMin: 1,
|
|
399
|
-
poolMax: 10,
|
|
400
|
-
poolTimeout: 60,
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
catch (e) {
|
|
404
|
-
throw humanizeOraError(e);
|
|
405
|
-
}
|
|
412
|
+
return await createOraPool(conn);
|
|
406
413
|
}
|
|
@@ -202,6 +202,10 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
|
202
202
|
setupPool(pool);
|
|
203
203
|
// 事务激活期间所有 query/execute 路由到同一 client
|
|
204
204
|
let txClient = null;
|
|
205
|
+
/** close 幂等标志:pg-pool 的 end() 二次调用会抛 "Called end on pool more than once",
|
|
206
|
+
* 而 manager 的 console 会话在授权复核失败路径会重复关闭同一适配器(见 manager 的
|
|
207
|
+
* finish('rollback') 与外层 catch)——重复 close 须为 no-op。 */
|
|
208
|
+
let closed = false;
|
|
205
209
|
const run = (sql, params) => txClient ? txClient.query(sql, params) : pool.query(sql, params);
|
|
206
210
|
// 跨库浏览(Navicat 行为):树列出服务器上所有库,展开非连接库时用同一凭据开指向该库的池。
|
|
207
211
|
// 池按库缓存复用;ro 模式下新池同样套会话只读。连接自身的库(fields.database 或 url 库名)直接复用主池。
|
|
@@ -387,6 +391,9 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
|
387
391
|
return toQueryResult(res);
|
|
388
392
|
}),
|
|
389
393
|
close: () => humanize(`${kind} 关闭连接`, async () => {
|
|
394
|
+
if (closed)
|
|
395
|
+
return; // 幂等:重复 close 为 no-op(见 closed 声明处注释)
|
|
396
|
+
closed = true;
|
|
390
397
|
await Promise.all([pool.end(), ...[...dbPools.values()].map((p) => p.end())]);
|
|
391
398
|
}),
|
|
392
399
|
};
|
|
@@ -114,6 +114,10 @@ export function stmtColumns(stmt, raw) {
|
|
|
114
114
|
export async function createSqliteAdapter(conn, opts) {
|
|
115
115
|
const readOnly = opts?.mode === 'ro';
|
|
116
116
|
const { db, driver } = await openRaw(conn, readOnly);
|
|
117
|
+
/** close 幂等:node:sqlite 的 DatabaseSync.close() 二次调用抛 "database is not open",
|
|
118
|
+
* 而 manager 的 runScript 超时路径会对同一会话适配器重复 close
|
|
119
|
+
* (onTimeout 与 finally 各一次)——重复 close 必须为 no-op。 */
|
|
120
|
+
let closed = false;
|
|
117
121
|
return {
|
|
118
122
|
kind: 'sqlite',
|
|
119
123
|
connId: conn.meta.id,
|
|
@@ -175,6 +179,9 @@ export async function createSqliteAdapter(conn, opts) {
|
|
|
175
179
|
return toQueryResult(raw, stmtColumns(stmt, raw));
|
|
176
180
|
}),
|
|
177
181
|
close: () => humanize('sqlite 关闭连接', async () => {
|
|
182
|
+
if (closed)
|
|
183
|
+
return; // 幂等:重复 close 为 no-op(见 closed 声明处注释)
|
|
184
|
+
closed = true;
|
|
178
185
|
db.close();
|
|
179
186
|
}),
|
|
180
187
|
};
|
|
@@ -35,8 +35,16 @@ export async function createTransferReader(rc, loc) {
|
|
|
35
35
|
const pool = kind === 'oracle'
|
|
36
36
|
? (await (await import('../oracle/index.js')).openOraclePool(rc))
|
|
37
37
|
: (await (await import('../dmdb/index.js')).openDmPool(rc));
|
|
38
|
-
// owner = 定位第一层(oracle/dm 单库多 schema
|
|
39
|
-
|
|
38
|
+
// owner = 定位第一层(oracle/dm 单库多 schema 模型),缺省连接用户。
|
|
39
|
+
// owner 非法(超长/含 NUL 换行)或缺失在构造期同步抛错,此时池已建而无人持有 →
|
|
40
|
+
// 登记项永不释放(池泄漏)。故构造期任何抛错都要回收该池。
|
|
41
|
+
try {
|
|
42
|
+
return oralike.createOraLikeTransferReader(kind, pool, loc?.database ?? oralike.ownerOfFromConn(rc));
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
await pool.close().catch(() => { });
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
40
48
|
}
|
|
41
49
|
if (kind === 'mongodb') {
|
|
42
50
|
const { createMongoTransferReaderFromConn } = await import('./kinds/mongo.js');
|
|
@@ -66,8 +74,15 @@ export async function createTransferWriter(rc, writeMode = 'insert', loc) {
|
|
|
66
74
|
const pool = kind === 'oracle'
|
|
67
75
|
? (await (await import('../oracle/index.js')).openOraclePool(rc))
|
|
68
76
|
: (await (await import('../dmdb/index.js')).openDmPool(rc));
|
|
69
|
-
// 写入 owner = 定位第一层,缺省连接用户默认 schema(写端以 owner
|
|
70
|
-
|
|
77
|
+
// 写入 owner = 定位第一层,缺省连接用户默认 schema(写端以 owner 前缀显式限定)。
|
|
78
|
+
// 与读端同理:owner 非法/缺失在构造期同步抛错,池已建而无人持有 → 必须回收。
|
|
79
|
+
try {
|
|
80
|
+
return oralike.createOraLikeTransferWriter(kind, pool, writeMode, loc?.database ?? oralike.ownerOfFromConn(rc));
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
await pool.close().catch(() => { });
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
71
86
|
}
|
|
72
87
|
if (kind === 'mongodb') {
|
|
73
88
|
const { createMongoTransferWriterFromConn } = await import('./kinds/mongo.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
诊断并修复「npm 上已发新版,但本机装不上 / 装到旧版 / 插件链接失效」。
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
三类阻塞:① node_modules\dsh-db-tool 是 link 残留或死链;② minimumReleaseAge
|
|
6
|
+
未豁免目标版本(pnpm 会静默保留旧版并 exit 0);③ allowBuilds 缺 oracledb /
|
|
7
|
+
better-sqlite3(安装报 ERR_PNPM_IGNORED_BUILDS)。默认只诊断,-Apply 才写入。
|
|
8
|
+
.PARAMETER Version
|
|
9
|
+
目标版本;省略时读包内 package.json 的 version。
|
|
10
|
+
.PARAMETER ProfileDir
|
|
11
|
+
profile 目录;默认 %USERPROFILE%\.dsh\profiles\desktop。
|
|
12
|
+
.PARAMETER Apply
|
|
13
|
+
执行修复(默认仅诊断)。写入前自动备份。
|
|
14
|
+
.EXAMPLE
|
|
15
|
+
pwsh -File scripts\fix-dsh-install.ps1
|
|
16
|
+
pwsh -File scripts\fix-dsh-install.ps1 -Apply
|
|
17
|
+
#>
|
|
18
|
+
[CmdletBinding()]
|
|
19
|
+
param(
|
|
20
|
+
[string]$Version,
|
|
21
|
+
[string]$ProfileDir,
|
|
22
|
+
[switch]$Apply
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
$ErrorActionPreference = 'Stop'
|
|
26
|
+
|
|
27
|
+
$prof = if ($ProfileDir) { $ProfileDir } else { Join-Path $env:USERPROFILE '.dsh\profiles\desktop' }
|
|
28
|
+
if (-not (Test-Path -LiteralPath $prof)) { throw "找不到 DSH profile:$prof(DSH 桌面端是否装过并启动过?)" }
|
|
29
|
+
$ws = Join-Path $prof 'pnpm-workspace.yaml'
|
|
30
|
+
if (-not (Test-Path -LiteralPath $ws)) { throw "找不到 $ws(profile 尚未初始化)" }
|
|
31
|
+
|
|
32
|
+
if (-not $Version) {
|
|
33
|
+
$pkg = Join-Path $PSScriptRoot '..\package.json'
|
|
34
|
+
$Version = if (Test-Path -LiteralPath $pkg) { (Get-Content -LiteralPath $pkg -Raw | ConvertFrom-Json).version } else { '0.0.0' }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# 固定编码,不依赖各 PowerShell 版本的默认值:5.1 的 Get-Content/Set-Content 走 ANSI
|
|
38
|
+
# (会把非 ASCII 写坏),且两者默认都丢 BOM。这两个函数只在脚本自己的读写路径上使用。
|
|
39
|
+
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
40
|
+
function Read-Yaml([string]$p) { [System.IO.File]::ReadAllText($p, $utf8NoBom) }
|
|
41
|
+
function Write-Yaml([string]$p, [string]$t) { [System.IO.File]::WriteAllText($p, $t, $utf8NoBom) }
|
|
42
|
+
|
|
43
|
+
# 换行归一化为 LF:CRLF 文件会让 `(?m)$` 锚点全部失效——豁免静默丢失,并写出重复的
|
|
44
|
+
# YAML 段头(pnpm 报 duplicate mapping key,配置直接不可用)。插入统一用 `n。
|
|
45
|
+
$text = (Read-Yaml $ws) -replace "`r`n", "`n"
|
|
46
|
+
|
|
47
|
+
$link = Join-Path $prof 'node_modules\dsh-db-tool'
|
|
48
|
+
$pkgs = @('oracledb', 'better-sqlite3') # 需要执行构建脚本的原生依赖
|
|
49
|
+
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
50
|
+
$fix = @()
|
|
51
|
+
|
|
52
|
+
Write-Host "`n[1/3] 插件链接形态" -ForegroundColor Cyan
|
|
53
|
+
if (Test-Path -LiteralPath $link) {
|
|
54
|
+
$it = Get-Item -LiteralPath $link -Force
|
|
55
|
+
$target = [string]$it.Target
|
|
56
|
+
Write-Host (" node_modules\dsh-db-tool -> {0} : {1}" -f $it.LinkType, $target)
|
|
57
|
+
if ($it.LinkType -eq 'SymbolicLink' -or $it.LinkType -eq 'Junction') {
|
|
58
|
+
# Target 是「as defined」的原样字符串,可能是相对路径;必须按链接所在目录解析,
|
|
59
|
+
# 否则会按当前工作目录误判为死链并删掉一个完好的链接。
|
|
60
|
+
$resolved = if ($target -and -not [System.IO.Path]::IsPathRooted($target)) {
|
|
61
|
+
Join-Path (Split-Path -LiteralPath $link -Parent) $target
|
|
62
|
+
} else { $target }
|
|
63
|
+
if ($resolved -and (Test-Path -LiteralPath $resolved)) {
|
|
64
|
+
Write-Host " [!] 指向真实目录的 link:npm 更新不会生效(加载的是该目录)" -ForegroundColor Yellow
|
|
65
|
+
Write-Host " 如该机不做本地开发,先 remove 再 add 改用 npm 包(见结尾)" -ForegroundColor Yellow
|
|
66
|
+
} else {
|
|
67
|
+
Write-Host " [X] link 目标不存在 -> 安装/加载必定失败" -ForegroundColor Red
|
|
68
|
+
$fix += 'del-dead-link'
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
Write-Host " OK:正常安装目录(非链接)" -ForegroundColor Green
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
Write-Host " 未安装(无 node_modules\dsh-db-tool)"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Write-Host "`n[2/3] minimumReleaseAge 豁免" -ForegroundColor Cyan
|
|
78
|
+
$m = [regex]::Match($text, '(?m)^[ \t]*-[ \t]*[''"]?dsh-db-tool@([^\r\n]*)')
|
|
79
|
+
if (-not $m.Success) {
|
|
80
|
+
Write-Host " [X] 没有 dsh-db-tool 豁免行 -> 新版本会被年龄窗口拦住" -ForegroundColor Red
|
|
81
|
+
$fix += 'add-exclude'
|
|
82
|
+
} else {
|
|
83
|
+
Write-Host (" 现有:dsh-db-tool@" + $m.Groups[1].Value.TrimEnd())
|
|
84
|
+
# 版本边界:否则 1.7.1 会命中 1.7.10
|
|
85
|
+
$bound = '(?<![\d.])' + [regex]::Escape($Version) + '(?![\d.])'
|
|
86
|
+
if ($m.Groups[1].Value -match $bound) {
|
|
87
|
+
Write-Host " OK:已豁免 $Version" -ForegroundColor Green
|
|
88
|
+
} else {
|
|
89
|
+
Write-Host " [X] 未豁免 $Version -> pnpm 会静默装旧版或报 MINIMUM_RELEASE_AGE_VIOLATION" -ForegroundColor Red
|
|
90
|
+
$fix += 'extend-exclude'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Write-Host "`n[3/3] allowBuilds 原生模块白名单" -ForegroundColor Cyan
|
|
95
|
+
$missingAllow = @($pkgs | Where-Object { $text -notmatch ('(?m)^[ \t]*' + [regex]::Escape($_) + ':[ \t]*true') })
|
|
96
|
+
foreach ($n in $pkgs) {
|
|
97
|
+
if ($missingAllow -contains $n) {
|
|
98
|
+
Write-Host " [X] $n 未获允许 -> 安装会以 ERR_PNPM_IGNORED_BUILDS 失败" -ForegroundColor Red
|
|
99
|
+
$fix += "allow-$n"
|
|
100
|
+
} else {
|
|
101
|
+
Write-Host " OK:$n" -ForegroundColor Green
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Write-Host "`n===== 结论 =====" -ForegroundColor Cyan
|
|
106
|
+
if ($fix.Count -eq 0) {
|
|
107
|
+
Write-Host "未发现阻塞项。" -ForegroundColor Green
|
|
108
|
+
} else {
|
|
109
|
+
Write-Host ("待处理 $($fix.Count) 项(见上方 [X])") -ForegroundColor Yellow
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (-not $Apply) {
|
|
113
|
+
Write-Host "`n(诊断模式:未做任何修改。确认后加 -Apply 执行修复)"
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
$new = $text
|
|
118
|
+
if ($fix -contains 'del-dead-link') { Remove-Item -LiteralPath $link -Force }
|
|
119
|
+
|
|
120
|
+
if ($fix -contains 'extend-exclude') {
|
|
121
|
+
# 保留原行的引号与前缀,只追加版本(pnpm 对同名包只认第一条,故必须改原行)
|
|
122
|
+
$new = $new -replace '(?m)^([ \t]*-[ \t]*[''"]?dsh-db-tool@[^\r\n]*?)[ \t]*$', ('$1 || ' + $Version)
|
|
123
|
+
}
|
|
124
|
+
if ($fix -contains 'add-exclude') {
|
|
125
|
+
if ($new -match '(?m)^minimumReleaseAgeExclude:[ \t]*$') {
|
|
126
|
+
$new = $new -replace '(?m)^(minimumReleaseAgeExclude:[ \t]*)$', ('$1' + "`n - dsh-db-tool@$Version")
|
|
127
|
+
} else {
|
|
128
|
+
# 段头缺失:插入无处可落,必须整段追加,否则静默无变更却报告成功
|
|
129
|
+
$new = $new.TrimEnd() + "`nminimumReleaseAgeExclude:`n - dsh-db-tool@$Version`n"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if ($missingAllow.Count -gt 0) {
|
|
133
|
+
# 一次性补齐:逐项插入会各插一次段头,产出重复的 YAML 键
|
|
134
|
+
$block = ($missingAllow | ForEach-Object { ' ' + $_ + ': true' }) -join "`n"
|
|
135
|
+
if ($new -match '(?m)^allowBuilds:[ \t]*$') {
|
|
136
|
+
$new = $new -replace '(?m)^(allowBuilds:[ \t]*)$', ('$1' + "`n" + $block)
|
|
137
|
+
} else {
|
|
138
|
+
$new = $new.TrimEnd() + "`nallowBuilds:`n" + $block + "`n"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if ($new -eq $text) {
|
|
143
|
+
Write-Host "`n无需写入(配置已是目标状态)。" -ForegroundColor Green
|
|
144
|
+
} else {
|
|
145
|
+
Copy-Item -LiteralPath $ws -Destination "$ws.bak-$stamp" -Force
|
|
146
|
+
Write-Yaml $ws $new
|
|
147
|
+
Write-Host "`n已更新 pnpm-workspace.yaml(备份:pnpm-workspace.yaml.bak-$stamp)" -ForegroundColor Green
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Write-Host "`n===== 下一步 =====" -ForegroundColor Cyan
|
|
151
|
+
Write-Host " 1) 完全退出并重启 DSH 桌面端"
|
|
152
|
+
Write-Host " 2) 若上面提示过 link,先移除再安装:"
|
|
153
|
+
Write-Host ' & "<DSH安装目录>\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop remove dsh-db-tool'
|
|
154
|
+
Write-Host " 3) 安装目标版本:"
|
|
155
|
+
Write-Host (' & "<DSH安装目录>\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop add dsh-db-tool@' + $Version + ' --registry=https://registry.npmjs.org/')
|