dsh-code-server-app 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +521 -519
- package/README.md +505 -503
- package/lib/client.js +3 -3
- package/lib/index.js +24 -1
- package/lib/launcher.mjs +433 -362
- package/lib/serve-dsh.mjs +162 -162
- package/package.json +4 -2
- package/vendor/VENDOR.json +1 -1
package/lib/serve-dsh.mjs
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/serve-dsh.mjs — 把 VS Code server 挂到 DSH 自身 HTTP 端口上的两段式注册(serve=dsh)。
|
|
3
|
-
*
|
|
4
|
-
* ctx.webServer.register({ kind: 'prefix', path: '/code-server', handler }) ← HTTP
|
|
5
|
-
* ctx.webServer.registerUpgrade({ path: '/code-server/<productPath>', handler }) ← WebSocket(精确匹配)
|
|
6
|
-
*
|
|
7
|
-
* 两条路径都必须先过 ctx.connection.requestRejection():DSH 的 webServer 本身不做任何校验,
|
|
8
|
-
* 防护是逐路由的(Host/Origin fence + 浏览器 cookie 认证),缺了它 IDE 会成为源站上唯一的无认证面。
|
|
9
|
-
*
|
|
10
|
-
* 转发策略(Phase 0 实测):**剥掉挂载前缀**后转给 launcher 的命名管道 —— VS Code 侧保持根挂载,
|
|
11
|
-
* 与今天已验证的配置完全一致;客户端渲染出的资源引用全是相对路径,浏览器按 /code-server/ 解析,
|
|
12
|
-
* 因此同一套 prefix 路由就能覆盖 HTML、静态资源、vscode-remote-resource 与 /_static/*。
|
|
13
|
-
*
|
|
14
|
-
* 已知退化:转发端口(/proxy/:port)的 WebSocket 无法用精确升级路由覆盖(端口号在路径里),
|
|
15
|
-
* 需要 DSH 上游提供 prefix upgrade;HTTP 转发端口不受影响。
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { request as httpRequest } from 'node:http';
|
|
19
|
-
import { connect } from 'node:net';
|
|
20
|
-
|
|
21
|
-
export const MOUNT_PATH = '/code-server';
|
|
22
|
-
|
|
23
|
-
/** 前缀匹配(与 DSH webServer 的 prefix 语义一致:命中 p 与 p/<anything>)。 */
|
|
24
|
-
function matchesPrefix(pathname, prefix) {
|
|
25
|
-
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 把 /code-server/xxx 转成上游要的 /xxx(空则 '/'),保留 query。 */
|
|
29
|
-
function stripPrefix(url, prefix) {
|
|
30
|
-
const rest = url.slice(prefix.length);
|
|
31
|
-
return rest === '' ? '/' : rest;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function rejectionOf(connection, req) {
|
|
35
|
-
try {
|
|
36
|
-
return connection.requestRejection({ headers: req.headers }) ?? undefined;
|
|
37
|
-
} catch (error) {
|
|
38
|
-
return { error };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 转发目标的两种形态:命名管道(kind:'pipe')或回环 TCP(kind:'tcp')。
|
|
43
|
-
* 注意 http.request 用 socketPath、net.connect 用 path —— 两者键名不同,必须在这里翻译。 */
|
|
44
|
-
function httpOptions(target, extra) {
|
|
45
|
-
return target.kind === 'pipe'
|
|
46
|
-
? { socketPath: target.pipe, ...extra }
|
|
47
|
-
: { host: target.host, port: target.port, ...extra };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function connectTo(target, onConnect) {
|
|
51
|
-
return target.kind === 'pipe'
|
|
52
|
-
? connect({ path: target.pipe }, onConnect)
|
|
53
|
-
: connect({ host: target.host, port: target.port }, onConnect);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 注册 IDE 的同源挂载点。
|
|
58
|
-
* @param {object} options
|
|
59
|
-
* @param {object} options.webServer - DSH webServer 服务(register / registerUpgrade)
|
|
60
|
-
* @param {object} options.connection - DSH connection 服务(requestRejection)
|
|
61
|
-
* @param {() => ({socketPath: string} | {host: string, port: number} | null)} options.getTarget
|
|
62
|
-
* 当前 launcher 的转发目标(命名管道为主;loopback TCP 用于测试/回退)
|
|
63
|
-
* @param {string} options.productPath - VS Code 客户端路径(quality-commit)
|
|
64
|
-
* @param {string} [options.mount] - 挂载前缀,默认 /code-server
|
|
65
|
-
* @param {(msg: string) => void} [options.log]
|
|
66
|
-
* @returns {{ disposers: (() => void)[], upgradePath: string }}
|
|
67
|
-
*/
|
|
68
|
-
export function mountOnWebServer({ webServer, connection, getTarget, productPath, mount = MOUNT_PATH, log = () => {} }) {
|
|
69
|
-
const upgradePath = `${mount}/${productPath}`;
|
|
70
|
-
const disposers = [];
|
|
71
|
-
|
|
72
|
-
/** 请求被 fence 拒绝时按 DSH 的约定回 401/403(upgrade 走裸 socket)。 */
|
|
73
|
-
function fence(req) {
|
|
74
|
-
const rejection = rejectionOf(connection, req);
|
|
75
|
-
if (rejection === undefined) return null;
|
|
76
|
-
if (typeof rejection === 'object' && rejection.error !== undefined) {
|
|
77
|
-
log(`requestRejection 抛错,按 403 处理: ${rejection.error?.message ?? rejection.error}`);
|
|
78
|
-
return 403;
|
|
79
|
-
}
|
|
80
|
-
return rejection;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
disposers.push(webServer.register({
|
|
84
|
-
kind: 'prefix',
|
|
85
|
-
path: mount,
|
|
86
|
-
handler: (req, res) => {
|
|
87
|
-
try {
|
|
88
|
-
const pathname = new URL(req.url ?? '/', 'http://x').pathname;
|
|
89
|
-
if (!matchesPrefix(pathname, mount)) { res.writeHead(404); res.end(); return; }
|
|
90
|
-
const status = fence(req);
|
|
91
|
-
if (status !== null) {
|
|
92
|
-
res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
|
|
93
|
-
res.end(status === 401 ? 'unauthorized\n' : 'forbidden\n');
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const target = getTarget();
|
|
97
|
-
if (target === null) {
|
|
98
|
-
res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
|
|
99
|
-
res.end('code-server 未启动\n');
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const upstream = httpRequest(httpOptions(target, {
|
|
103
|
-
method: req.method,
|
|
104
|
-
path: stripPrefix(req.url ?? '/', mount),
|
|
105
|
-
headers: req.headers,
|
|
106
|
-
}), (upstreamRes) => {
|
|
107
|
-
res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
|
|
108
|
-
upstreamRes.pipe(res);
|
|
109
|
-
});
|
|
110
|
-
upstream.on('error', (error) => {
|
|
111
|
-
log(`HTTP 转发失败(${req.method} ${req.url}): ${error.message}`);
|
|
112
|
-
if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
113
|
-
if (!res.writableEnded) res.end('code-server 不可达\n');
|
|
114
|
-
});
|
|
115
|
-
req.on('aborted', () => upstream.destroy());
|
|
116
|
-
req.pipe(upstream);
|
|
117
|
-
} catch (error) {
|
|
118
|
-
// 路由回调里的异常绝不能冒泡到 DSH 的 webServer(会变成未捕获异常)
|
|
119
|
-
log(`HTTP 挂载点异常(${req.method} ${req.url}): ${error && error.stack ? error.stack : error}`);
|
|
120
|
-
if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
121
|
-
if (!res.writableEnded) res.end('code-server 挂载点异常\n');
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
}));
|
|
125
|
-
|
|
126
|
-
disposers.push(webServer.registerUpgrade({
|
|
127
|
-
path: upgradePath,
|
|
128
|
-
handler: (req, socket, head) => {
|
|
129
|
-
try {
|
|
130
|
-
const status = fence(req);
|
|
131
|
-
if (status !== null) {
|
|
132
|
-
socket.end(`HTTP/1.1 ${status} ${status === 401 ? 'Unauthorized' : 'Forbidden'}\r\nConnection: close\r\n\r\n`);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
const target = getTarget();
|
|
136
|
-
if (target === null) { socket.destroy(); return; }
|
|
137
|
-
const upstream = connectTo(target, () => {
|
|
138
|
-
const lines = [`GET ${stripPrefix(req.url ?? '/', mount)} HTTP/1.1`];
|
|
139
|
-
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
140
|
-
lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
|
|
141
|
-
}
|
|
142
|
-
upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
|
|
143
|
-
if (head !== undefined && head.length > 0) upstream.write(head);
|
|
144
|
-
upstream.pipe(socket);
|
|
145
|
-
socket.pipe(upstream);
|
|
146
|
-
});
|
|
147
|
-
upstream.on('error', (error) => { log(`WS 转发失败(${req.url}): ${error.message}`); socket.destroy(); });
|
|
148
|
-
socket.on('error', () => upstream.destroy());
|
|
149
|
-
socket.on('close', () => upstream.destroy());
|
|
150
|
-
} catch (error) {
|
|
151
|
-
log(`WS 挂载点异常(${req.url}): ${error && error.stack ? error.stack : error}`);
|
|
152
|
-
socket.destroy();
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
}));
|
|
156
|
-
|
|
157
|
-
log(`已挂载 ${mount}/ (HTTP prefix)+ ${upgradePath} (WS exact)`);
|
|
158
|
-
return {
|
|
159
|
-
upgradePath,
|
|
160
|
-
disposers,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* lib/serve-dsh.mjs — 把 VS Code server 挂到 DSH 自身 HTTP 端口上的两段式注册(serve=dsh)。
|
|
3
|
+
*
|
|
4
|
+
* ctx.webServer.register({ kind: 'prefix', path: '/code-server', handler }) ← HTTP
|
|
5
|
+
* ctx.webServer.registerUpgrade({ path: '/code-server/<productPath>', handler }) ← WebSocket(精确匹配)
|
|
6
|
+
*
|
|
7
|
+
* 两条路径都必须先过 ctx.connection.requestRejection():DSH 的 webServer 本身不做任何校验,
|
|
8
|
+
* 防护是逐路由的(Host/Origin fence + 浏览器 cookie 认证),缺了它 IDE 会成为源站上唯一的无认证面。
|
|
9
|
+
*
|
|
10
|
+
* 转发策略(Phase 0 实测):**剥掉挂载前缀**后转给 launcher 的命名管道 —— VS Code 侧保持根挂载,
|
|
11
|
+
* 与今天已验证的配置完全一致;客户端渲染出的资源引用全是相对路径,浏览器按 /code-server/ 解析,
|
|
12
|
+
* 因此同一套 prefix 路由就能覆盖 HTML、静态资源、vscode-remote-resource 与 /_static/*。
|
|
13
|
+
*
|
|
14
|
+
* 已知退化:转发端口(/proxy/:port)的 WebSocket 无法用精确升级路由覆盖(端口号在路径里),
|
|
15
|
+
* 需要 DSH 上游提供 prefix upgrade;HTTP 转发端口不受影响。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { request as httpRequest } from 'node:http';
|
|
19
|
+
import { connect } from 'node:net';
|
|
20
|
+
|
|
21
|
+
export const MOUNT_PATH = '/code-server';
|
|
22
|
+
|
|
23
|
+
/** 前缀匹配(与 DSH webServer 的 prefix 语义一致:命中 p 与 p/<anything>)。 */
|
|
24
|
+
function matchesPrefix(pathname, prefix) {
|
|
25
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 把 /code-server/xxx 转成上游要的 /xxx(空则 '/'),保留 query。 */
|
|
29
|
+
function stripPrefix(url, prefix) {
|
|
30
|
+
const rest = url.slice(prefix.length);
|
|
31
|
+
return rest === '' ? '/' : rest;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rejectionOf(connection, req) {
|
|
35
|
+
try {
|
|
36
|
+
return connection.requestRejection({ headers: req.headers }) ?? undefined;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return { error };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 转发目标的两种形态:命名管道(kind:'pipe')或回环 TCP(kind:'tcp')。
|
|
43
|
+
* 注意 http.request 用 socketPath、net.connect 用 path —— 两者键名不同,必须在这里翻译。 */
|
|
44
|
+
function httpOptions(target, extra) {
|
|
45
|
+
return target.kind === 'pipe'
|
|
46
|
+
? { socketPath: target.pipe, ...extra }
|
|
47
|
+
: { host: target.host, port: target.port, ...extra };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function connectTo(target, onConnect) {
|
|
51
|
+
return target.kind === 'pipe'
|
|
52
|
+
? connect({ path: target.pipe }, onConnect)
|
|
53
|
+
: connect({ host: target.host, port: target.port }, onConnect);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 注册 IDE 的同源挂载点。
|
|
58
|
+
* @param {object} options
|
|
59
|
+
* @param {object} options.webServer - DSH webServer 服务(register / registerUpgrade)
|
|
60
|
+
* @param {object} options.connection - DSH connection 服务(requestRejection)
|
|
61
|
+
* @param {() => ({socketPath: string} | {host: string, port: number} | null)} options.getTarget
|
|
62
|
+
* 当前 launcher 的转发目标(命名管道为主;loopback TCP 用于测试/回退)
|
|
63
|
+
* @param {string} options.productPath - VS Code 客户端路径(quality-commit)
|
|
64
|
+
* @param {string} [options.mount] - 挂载前缀,默认 /code-server
|
|
65
|
+
* @param {(msg: string) => void} [options.log]
|
|
66
|
+
* @returns {{ disposers: (() => void)[], upgradePath: string }}
|
|
67
|
+
*/
|
|
68
|
+
export function mountOnWebServer({ webServer, connection, getTarget, productPath, mount = MOUNT_PATH, log = () => {} }) {
|
|
69
|
+
const upgradePath = `${mount}/${productPath}`;
|
|
70
|
+
const disposers = [];
|
|
71
|
+
|
|
72
|
+
/** 请求被 fence 拒绝时按 DSH 的约定回 401/403(upgrade 走裸 socket)。 */
|
|
73
|
+
function fence(req) {
|
|
74
|
+
const rejection = rejectionOf(connection, req);
|
|
75
|
+
if (rejection === undefined) return null;
|
|
76
|
+
if (typeof rejection === 'object' && rejection.error !== undefined) {
|
|
77
|
+
log(`requestRejection 抛错,按 403 处理: ${rejection.error?.message ?? rejection.error}`);
|
|
78
|
+
return 403;
|
|
79
|
+
}
|
|
80
|
+
return rejection;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
disposers.push(webServer.register({
|
|
84
|
+
kind: 'prefix',
|
|
85
|
+
path: mount,
|
|
86
|
+
handler: (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const pathname = new URL(req.url ?? '/', 'http://x').pathname;
|
|
89
|
+
if (!matchesPrefix(pathname, mount)) { res.writeHead(404); res.end(); return; }
|
|
90
|
+
const status = fence(req);
|
|
91
|
+
if (status !== null) {
|
|
92
|
+
res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
|
|
93
|
+
res.end(status === 401 ? 'unauthorized\n' : 'forbidden\n');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const target = getTarget();
|
|
97
|
+
if (target === null) {
|
|
98
|
+
res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
|
|
99
|
+
res.end('code-server 未启动\n');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const upstream = httpRequest(httpOptions(target, {
|
|
103
|
+
method: req.method,
|
|
104
|
+
path: stripPrefix(req.url ?? '/', mount),
|
|
105
|
+
headers: req.headers,
|
|
106
|
+
}), (upstreamRes) => {
|
|
107
|
+
res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
|
|
108
|
+
upstreamRes.pipe(res);
|
|
109
|
+
});
|
|
110
|
+
upstream.on('error', (error) => {
|
|
111
|
+
log(`HTTP 转发失败(${req.method} ${req.url}): ${error.message}`);
|
|
112
|
+
if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
113
|
+
if (!res.writableEnded) res.end('code-server 不可达\n');
|
|
114
|
+
});
|
|
115
|
+
req.on('aborted', () => upstream.destroy());
|
|
116
|
+
req.pipe(upstream);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// 路由回调里的异常绝不能冒泡到 DSH 的 webServer(会变成未捕获异常)
|
|
119
|
+
log(`HTTP 挂载点异常(${req.method} ${req.url}): ${error && error.stack ? error.stack : error}`);
|
|
120
|
+
if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
121
|
+
if (!res.writableEnded) res.end('code-server 挂载点异常\n');
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
disposers.push(webServer.registerUpgrade({
|
|
127
|
+
path: upgradePath,
|
|
128
|
+
handler: (req, socket, head) => {
|
|
129
|
+
try {
|
|
130
|
+
const status = fence(req);
|
|
131
|
+
if (status !== null) {
|
|
132
|
+
socket.end(`HTTP/1.1 ${status} ${status === 401 ? 'Unauthorized' : 'Forbidden'}\r\nConnection: close\r\n\r\n`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const target = getTarget();
|
|
136
|
+
if (target === null) { socket.destroy(); return; }
|
|
137
|
+
const upstream = connectTo(target, () => {
|
|
138
|
+
const lines = [`GET ${stripPrefix(req.url ?? '/', mount)} HTTP/1.1`];
|
|
139
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
140
|
+
lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
|
|
141
|
+
}
|
|
142
|
+
upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
|
|
143
|
+
if (head !== undefined && head.length > 0) upstream.write(head);
|
|
144
|
+
upstream.pipe(socket);
|
|
145
|
+
socket.pipe(upstream);
|
|
146
|
+
});
|
|
147
|
+
upstream.on('error', (error) => { log(`WS 转发失败(${req.url}): ${error.message}`); socket.destroy(); });
|
|
148
|
+
socket.on('error', () => upstream.destroy());
|
|
149
|
+
socket.on('close', () => upstream.destroy());
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log(`WS 挂载点异常(${req.url}): ${error && error.stack ? error.stack : error}`);
|
|
152
|
+
socket.destroy();
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
log(`已挂载 ${mount}/ (HTTP prefix)+ ${upgradePath} (WS exact)`);
|
|
158
|
+
return {
|
|
159
|
+
upgradePath,
|
|
160
|
+
disposers,
|
|
161
|
+
};
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-code-server-app",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "VS Code (from a code-server release) inside DSH: a right-sidebar tab driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs). The tab claims DSH file addresses (dsh-resource://file/**), so the product's own produced-file chips, delivered-file previews and inline prose mentions open in the workbench. The IDE is a resident surface moved with Element.moveBefore instead of being remounted, so switching sidebar tabs no longer reloads it. Requires a DSH with the right-sidebar services (sidebarRightTabs/sidebarRight, >= 0.1.5-alpha.1); older DSH versions get a single upgrade notice on the settings page and no other UI. Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). No code-server Node layer, no argon2, no C++ toolchain.",
|
|
5
5
|
"homepage": "https://github.com/jinsiyu/dsh-code-server-app",
|
|
6
6
|
"repository": {
|
|
@@ -111,6 +111,8 @@
|
|
|
111
111
|
"publish:repacks": "node scripts/publish-repacks.mjs",
|
|
112
112
|
"publish:plugin": "node scripts/publish-plugin.mjs",
|
|
113
113
|
"promote": "node scripts/promote.mjs",
|
|
114
|
-
"build:client": "node scripts/build-client.mjs"
|
|
114
|
+
"build:client": "node scripts/build-client.mjs",
|
|
115
|
+
"test:apply": "node scripts/test-plugin-apply.mjs",
|
|
116
|
+
"test:launcher-routes": "node scripts/test-launcher-routes.mjs"
|
|
115
117
|
}
|
|
116
118
|
}
|
package/vendor/VENDOR.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"vscodeVersion": "1.136.1",
|
|
4
4
|
"productPath": "stable-8d5f383f301ca20681f5b6606b8207d9dc87bdd8",
|
|
5
5
|
"layout": "vscode-only",
|
|
6
|
-
"preparedAt": "2026-09-
|
|
6
|
+
"preparedAt": "2026-09-11T10:59:59.005Z",
|
|
7
7
|
"source": "registry",
|
|
8
8
|
"node": "v24.13.1",
|
|
9
9
|
"platform": "win32",
|