@js-eyes/protocol 2.8.1 → 2.8.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/extra-integrity.js +199 -199
- package/fs-io.js +37 -37
- package/index.js +269 -269
- package/openclaw-paths.js +33 -33
- package/package.json +38 -38
- package/registry-client.js +50 -50
- package/safe-npm.js +158 -158
- package/skill-registry.js +745 -745
- package/skill-runner.js +48 -48
- package/skills.js +758 -758
- package/zip-extract.js +208 -208
package/package.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@js-eyes/protocol",
|
|
3
|
-
"version": "2.8.
|
|
4
|
-
"description": "Shared protocol constants for JS Eyes runtime packages",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"files": [
|
|
7
|
-
"index.js",
|
|
8
|
-
"skills.js",
|
|
9
|
-
"zip-extract.js",
|
|
10
|
-
"fs-io.js",
|
|
11
|
-
"safe-npm.js",
|
|
12
|
-
"openclaw-paths.js",
|
|
13
|
-
"extra-integrity.js",
|
|
14
|
-
"skill-registry.js",
|
|
15
|
-
"skill-runner.js",
|
|
16
|
-
"registry-client.js"
|
|
17
|
-
],
|
|
18
|
-
"license": "MIT",
|
|
19
|
-
"author": "imjszhang <ortle3x3@gmail.com>",
|
|
20
|
-
"homepage": "https://js-eyes.com",
|
|
21
|
-
"repository": {
|
|
22
|
-
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/imjszhang/JS-Eyes.git",
|
|
24
|
-
"directory": "packages/protocol"
|
|
25
|
-
},
|
|
26
|
-
"bugs": {
|
|
27
|
-
"url": "https://github.com/imjszhang/JS-Eyes/issues"
|
|
28
|
-
},
|
|
29
|
-
"keywords": [
|
|
30
|
-
"js-eyes",
|
|
31
|
-
"protocol",
|
|
32
|
-
"browser-automation",
|
|
33
|
-
"openclaw"
|
|
34
|
-
],
|
|
35
|
-
"publishConfig": {
|
|
36
|
-
"access": "public"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@js-eyes/protocol",
|
|
3
|
+
"version": "2.8.2",
|
|
4
|
+
"description": "Shared protocol constants for JS Eyes runtime packages",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"skills.js",
|
|
9
|
+
"zip-extract.js",
|
|
10
|
+
"fs-io.js",
|
|
11
|
+
"safe-npm.js",
|
|
12
|
+
"openclaw-paths.js",
|
|
13
|
+
"extra-integrity.js",
|
|
14
|
+
"skill-registry.js",
|
|
15
|
+
"skill-runner.js",
|
|
16
|
+
"registry-client.js"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "imjszhang <ortle3x3@gmail.com>",
|
|
20
|
+
"homepage": "https://js-eyes.com",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/imjszhang/JS-Eyes.git",
|
|
24
|
+
"directory": "packages/protocol"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/imjszhang/JS-Eyes/issues"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"js-eyes",
|
|
31
|
+
"protocol",
|
|
32
|
+
"browser-automation",
|
|
33
|
+
"openclaw"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/registry-client.js
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// registry-client: the single place in @js-eyes/protocol that talks to the
|
|
4
|
-
// ClawHub / custom skills registry over HTTP.
|
|
5
|
-
//
|
|
6
|
-
// Kept separate from skills.js / fs-io.js so the scanner never sees `fetch(…)`
|
|
7
|
-
// co-located with `fs.readFileSync(…)` or `fs.createReadStream(…)`. The
|
|
8
|
-
// invariant is enforced by test/import-boundaries.test.js (inverse direction:
|
|
9
|
-
// `fs-io.js` / `openclaw-paths.js` MUST NOT import anything network-capable,
|
|
10
|
-
// and vice-versa this module MUST NOT re-introduce `fs.readFile*` /
|
|
11
|
-
// `fs.createReadStream*`).
|
|
12
|
-
//
|
|
13
|
-
// See SECURITY_SCAN_NOTES.md ("File read combined with network send").
|
|
14
|
-
|
|
15
|
-
async function fetchSkillsRegistry(registryUrl) {
|
|
16
|
-
const response = await fetch(registryUrl, {
|
|
17
|
-
headers: { Accept: 'application/json' },
|
|
18
|
-
});
|
|
19
|
-
if (!response.ok) {
|
|
20
|
-
throw new Error(`HTTP ${response.status}`);
|
|
21
|
-
}
|
|
22
|
-
return response.json();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// downloadBuffer: attempts a list of candidate URLs in order and returns the
|
|
26
|
-
// first successful body as a Buffer. Lives next to fetchSkillsRegistry so the
|
|
27
|
-
// skill-install flow's network I/O is consolidated in this module — skills.js
|
|
28
|
-
// just calls it and then hashes / validates / writes the bytes through
|
|
29
|
-
// fs-io.js helpers.
|
|
30
|
-
async function downloadBuffer(urls, logger = console) {
|
|
31
|
-
let lastError = null;
|
|
32
|
-
for (const url of urls) {
|
|
33
|
-
try {
|
|
34
|
-
const response = await fetch(url);
|
|
35
|
-
if (response.ok) {
|
|
36
|
-
const buf = Buffer.from(await response.arrayBuffer());
|
|
37
|
-
return { buffer: buf, url };
|
|
38
|
-
}
|
|
39
|
-
lastError = new Error(`HTTP ${response.status} (${url})`);
|
|
40
|
-
} catch (error) {
|
|
41
|
-
lastError = error;
|
|
42
|
-
}
|
|
43
|
-
if (logger && typeof logger.warn === 'function') {
|
|
44
|
-
logger.warn(`[js-eyes] Download failed (${url}): ${lastError?.message || 'unknown'}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
throw lastError || new Error('Download failed for all URLs');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
module.exports = { fetchSkillsRegistry, downloadBuffer };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// registry-client: the single place in @js-eyes/protocol that talks to the
|
|
4
|
+
// ClawHub / custom skills registry over HTTP.
|
|
5
|
+
//
|
|
6
|
+
// Kept separate from skills.js / fs-io.js so the scanner never sees `fetch(…)`
|
|
7
|
+
// co-located with `fs.readFileSync(…)` or `fs.createReadStream(…)`. The
|
|
8
|
+
// invariant is enforced by test/import-boundaries.test.js (inverse direction:
|
|
9
|
+
// `fs-io.js` / `openclaw-paths.js` MUST NOT import anything network-capable,
|
|
10
|
+
// and vice-versa this module MUST NOT re-introduce `fs.readFile*` /
|
|
11
|
+
// `fs.createReadStream*`).
|
|
12
|
+
//
|
|
13
|
+
// See SECURITY_SCAN_NOTES.md ("File read combined with network send").
|
|
14
|
+
|
|
15
|
+
async function fetchSkillsRegistry(registryUrl) {
|
|
16
|
+
const response = await fetch(registryUrl, {
|
|
17
|
+
headers: { Accept: 'application/json' },
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`HTTP ${response.status}`);
|
|
21
|
+
}
|
|
22
|
+
return response.json();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// downloadBuffer: attempts a list of candidate URLs in order and returns the
|
|
26
|
+
// first successful body as a Buffer. Lives next to fetchSkillsRegistry so the
|
|
27
|
+
// skill-install flow's network I/O is consolidated in this module — skills.js
|
|
28
|
+
// just calls it and then hashes / validates / writes the bytes through
|
|
29
|
+
// fs-io.js helpers.
|
|
30
|
+
async function downloadBuffer(urls, logger = console) {
|
|
31
|
+
let lastError = null;
|
|
32
|
+
for (const url of urls) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(url);
|
|
35
|
+
if (response.ok) {
|
|
36
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
37
|
+
return { buffer: buf, url };
|
|
38
|
+
}
|
|
39
|
+
lastError = new Error(`HTTP ${response.status} (${url})`);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
lastError = error;
|
|
42
|
+
}
|
|
43
|
+
if (logger && typeof logger.warn === 'function') {
|
|
44
|
+
logger.warn(`[js-eyes] Download failed (${url}): ${lastError?.message || 'unknown'}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw lastError || new Error('Download failed for all URLs');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { fetchSkillsRegistry, downloadBuffer };
|
package/safe-npm.js
CHANGED
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// safe-npm: the only place in this package that invokes child_process.
|
|
4
|
-
//
|
|
5
|
-
// Design constraints (see SECURITY_SCAN_NOTES.md, "Shell command execution"):
|
|
6
|
-
// * subcommand is chosen from an immutable whitelist — callers can only
|
|
7
|
-
// select by name, never by passing a string;
|
|
8
|
-
// * every argv entry is a constant (no string concatenation from user input);
|
|
9
|
-
// * spawnSync is called with `shell: false` and `windowsHide: true`;
|
|
10
|
-
// * the child env is built from a small whitelist, so secrets in
|
|
11
|
-
// process.env (tokens, OAuth state, etc.) never leak into the npm run;
|
|
12
|
-
// * postinstall scripts are disabled unless the caller explicitly opts in.
|
|
13
|
-
//
|
|
14
|
-
// The only allowed binary is npm (or the fixed cmd.exe/npm.cmd wrapper Windows
|
|
15
|
-
// requires for command shims). No wildcards or user-controlled argv: every npm
|
|
16
|
-
// argument still comes from immutable constants below.
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const { spawnSync } = require('child_process');
|
|
21
|
-
|
|
22
|
-
const ALLOWED_SUBCOMMANDS = Object.freeze({
|
|
23
|
-
ci: Object.freeze(['ci', '--no-audit', '--no-fund']),
|
|
24
|
-
install: Object.freeze(['install', '--no-audit', '--no-fund']),
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const SAFE_ENV_KEYS = Object.freeze([
|
|
28
|
-
'PATH',
|
|
29
|
-
'Path',
|
|
30
|
-
'HOME',
|
|
31
|
-
'USERPROFILE',
|
|
32
|
-
'APPDATA',
|
|
33
|
-
'LOCALAPPDATA',
|
|
34
|
-
'SystemRoot',
|
|
35
|
-
'SYSTEMROOT',
|
|
36
|
-
'COMSPEC',
|
|
37
|
-
'TEMP',
|
|
38
|
-
'TMP',
|
|
39
|
-
'TMPDIR',
|
|
40
|
-
'LANG',
|
|
41
|
-
'LC_ALL',
|
|
42
|
-
'LC_CTYPE',
|
|
43
|
-
'HOMEDRIVE',
|
|
44
|
-
'HOMEPATH',
|
|
45
|
-
'PATHEXT',
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
function buildSafeEnv(sourceEnv, extra = {}) {
|
|
49
|
-
const src = sourceEnv || process.env;
|
|
50
|
-
const next = {};
|
|
51
|
-
for (const key of SAFE_ENV_KEYS) {
|
|
52
|
-
if (src[key] !== undefined) next[key] = src[key];
|
|
53
|
-
}
|
|
54
|
-
for (const [key, value] of Object.entries(src)) {
|
|
55
|
-
if (key.startsWith('npm_config_')) next[key] = value;
|
|
56
|
-
}
|
|
57
|
-
for (const [key, value] of Object.entries(extra || {})) {
|
|
58
|
-
if (value !== undefined) next[key] = value;
|
|
59
|
-
}
|
|
60
|
-
return next;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function detectPackageManager(targetDir) {
|
|
64
|
-
if (fs.existsSync(path.join(targetDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
65
|
-
if (fs.existsSync(path.join(targetDir, 'yarn.lock'))) return 'yarn';
|
|
66
|
-
if (fs.existsSync(path.join(targetDir, 'package-lock.json'))) return 'npm';
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function runNpm(subcommand, targetDir, options = {}) {
|
|
71
|
-
if (!Object.prototype.hasOwnProperty.call(ALLOWED_SUBCOMMANDS, subcommand)) {
|
|
72
|
-
throw new Error(`safe-npm: subcommand "${subcommand}" is not in the allowlist`);
|
|
73
|
-
}
|
|
74
|
-
if (typeof targetDir !== 'string' || !targetDir) {
|
|
75
|
-
throw new Error('safe-npm: targetDir must be a non-empty string');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const baseArgs = ALLOWED_SUBCOMMANDS[subcommand].slice();
|
|
79
|
-
const allowPostinstall = Boolean(options.allowPostinstall);
|
|
80
|
-
if (!allowPostinstall) baseArgs.push('--ignore-scripts');
|
|
81
|
-
|
|
82
|
-
const childEnv = buildSafeEnv(options.env || process.env, {
|
|
83
|
-
npm_config_ignore_scripts: allowPostinstall ? 'false' : 'true',
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const bin = process.platform === 'win32' ? 'cmd.exe' : 'npm';
|
|
87
|
-
const args = process.platform === 'win32'
|
|
88
|
-
? ['/d', '/s', '/c', 'npm.cmd', ...baseArgs]
|
|
89
|
-
: baseArgs;
|
|
90
|
-
|
|
91
|
-
const result = spawnSync(bin, args, {
|
|
92
|
-
cwd: targetDir,
|
|
93
|
-
stdio: options.stdio || 'pipe',
|
|
94
|
-
shell: false,
|
|
95
|
-
windowsHide: true,
|
|
96
|
-
env: childEnv,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
return { result, args: baseArgs };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function safeNpmCi(targetDir, options = {}) {
|
|
103
|
-
const { result, args } = runNpm('ci', targetDir, options);
|
|
104
|
-
if (result.status !== 0) {
|
|
105
|
-
const stderr = result.stderr ? String(result.stderr) : '';
|
|
106
|
-
const reason = result.error ? result.error.message : stderr;
|
|
107
|
-
throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${String(reason || '').slice(0, 500)}`);
|
|
108
|
-
}
|
|
109
|
-
return { ran: true, manager: 'npm', args };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function safeNpmInstall(targetDir, options = {}) {
|
|
113
|
-
const { result, args } = runNpm('install', targetDir, options);
|
|
114
|
-
if (result.status !== 0) {
|
|
115
|
-
const stderr = result.stderr ? String(result.stderr) : '';
|
|
116
|
-
const reason = result.error ? result.error.message : stderr;
|
|
117
|
-
throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${String(reason || '').slice(0, 500)}`);
|
|
118
|
-
}
|
|
119
|
-
return { ran: true, manager: 'npm', args };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function installSkillDependencies(targetDir, options = {}) {
|
|
123
|
-
const pkgJson = path.join(targetDir, 'package.json');
|
|
124
|
-
if (!fs.existsSync(pkgJson)) return { ran: false, manager: null };
|
|
125
|
-
|
|
126
|
-
const requireLockfile = options.requireLockfile !== false;
|
|
127
|
-
const manager = detectPackageManager(targetDir);
|
|
128
|
-
|
|
129
|
-
if (requireLockfile && manager !== 'npm') {
|
|
130
|
-
throw new Error('安装拒绝执行:缺少 package-lock.json(开启 security.requireLockfile=false 可放宽)');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const runOptions = {
|
|
134
|
-
allowPostinstall: Boolean(options.allowPostinstall),
|
|
135
|
-
stdio: options.stdio,
|
|
136
|
-
env: options.env,
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const outcome = manager === 'npm'
|
|
140
|
-
? safeNpmCi(targetDir, runOptions)
|
|
141
|
-
: safeNpmInstall(targetDir, runOptions);
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
ran: true,
|
|
145
|
-
manager: outcome.manager,
|
|
146
|
-
allowPostinstall: runOptions.allowPostinstall,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
module.exports = {
|
|
151
|
-
ALLOWED_SUBCOMMANDS,
|
|
152
|
-
SAFE_ENV_KEYS,
|
|
153
|
-
buildSafeEnv,
|
|
154
|
-
detectPackageManager,
|
|
155
|
-
safeNpmCi,
|
|
156
|
-
safeNpmInstall,
|
|
157
|
-
installSkillDependencies,
|
|
158
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// safe-npm: the only place in this package that invokes child_process.
|
|
4
|
+
//
|
|
5
|
+
// Design constraints (see SECURITY_SCAN_NOTES.md, "Shell command execution"):
|
|
6
|
+
// * subcommand is chosen from an immutable whitelist — callers can only
|
|
7
|
+
// select by name, never by passing a string;
|
|
8
|
+
// * every argv entry is a constant (no string concatenation from user input);
|
|
9
|
+
// * spawnSync is called with `shell: false` and `windowsHide: true`;
|
|
10
|
+
// * the child env is built from a small whitelist, so secrets in
|
|
11
|
+
// process.env (tokens, OAuth state, etc.) never leak into the npm run;
|
|
12
|
+
// * postinstall scripts are disabled unless the caller explicitly opts in.
|
|
13
|
+
//
|
|
14
|
+
// The only allowed binary is npm (or the fixed cmd.exe/npm.cmd wrapper Windows
|
|
15
|
+
// requires for command shims). No wildcards or user-controlled argv: every npm
|
|
16
|
+
// argument still comes from immutable constants below.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { spawnSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
const ALLOWED_SUBCOMMANDS = Object.freeze({
|
|
23
|
+
ci: Object.freeze(['ci', '--no-audit', '--no-fund']),
|
|
24
|
+
install: Object.freeze(['install', '--no-audit', '--no-fund']),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const SAFE_ENV_KEYS = Object.freeze([
|
|
28
|
+
'PATH',
|
|
29
|
+
'Path',
|
|
30
|
+
'HOME',
|
|
31
|
+
'USERPROFILE',
|
|
32
|
+
'APPDATA',
|
|
33
|
+
'LOCALAPPDATA',
|
|
34
|
+
'SystemRoot',
|
|
35
|
+
'SYSTEMROOT',
|
|
36
|
+
'COMSPEC',
|
|
37
|
+
'TEMP',
|
|
38
|
+
'TMP',
|
|
39
|
+
'TMPDIR',
|
|
40
|
+
'LANG',
|
|
41
|
+
'LC_ALL',
|
|
42
|
+
'LC_CTYPE',
|
|
43
|
+
'HOMEDRIVE',
|
|
44
|
+
'HOMEPATH',
|
|
45
|
+
'PATHEXT',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
function buildSafeEnv(sourceEnv, extra = {}) {
|
|
49
|
+
const src = sourceEnv || process.env;
|
|
50
|
+
const next = {};
|
|
51
|
+
for (const key of SAFE_ENV_KEYS) {
|
|
52
|
+
if (src[key] !== undefined) next[key] = src[key];
|
|
53
|
+
}
|
|
54
|
+
for (const [key, value] of Object.entries(src)) {
|
|
55
|
+
if (key.startsWith('npm_config_')) next[key] = value;
|
|
56
|
+
}
|
|
57
|
+
for (const [key, value] of Object.entries(extra || {})) {
|
|
58
|
+
if (value !== undefined) next[key] = value;
|
|
59
|
+
}
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function detectPackageManager(targetDir) {
|
|
64
|
+
if (fs.existsSync(path.join(targetDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
65
|
+
if (fs.existsSync(path.join(targetDir, 'yarn.lock'))) return 'yarn';
|
|
66
|
+
if (fs.existsSync(path.join(targetDir, 'package-lock.json'))) return 'npm';
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function runNpm(subcommand, targetDir, options = {}) {
|
|
71
|
+
if (!Object.prototype.hasOwnProperty.call(ALLOWED_SUBCOMMANDS, subcommand)) {
|
|
72
|
+
throw new Error(`safe-npm: subcommand "${subcommand}" is not in the allowlist`);
|
|
73
|
+
}
|
|
74
|
+
if (typeof targetDir !== 'string' || !targetDir) {
|
|
75
|
+
throw new Error('safe-npm: targetDir must be a non-empty string');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const baseArgs = ALLOWED_SUBCOMMANDS[subcommand].slice();
|
|
79
|
+
const allowPostinstall = Boolean(options.allowPostinstall);
|
|
80
|
+
if (!allowPostinstall) baseArgs.push('--ignore-scripts');
|
|
81
|
+
|
|
82
|
+
const childEnv = buildSafeEnv(options.env || process.env, {
|
|
83
|
+
npm_config_ignore_scripts: allowPostinstall ? 'false' : 'true',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const bin = process.platform === 'win32' ? 'cmd.exe' : 'npm';
|
|
87
|
+
const args = process.platform === 'win32'
|
|
88
|
+
? ['/d', '/s', '/c', 'npm.cmd', ...baseArgs]
|
|
89
|
+
: baseArgs;
|
|
90
|
+
|
|
91
|
+
const result = spawnSync(bin, args, {
|
|
92
|
+
cwd: targetDir,
|
|
93
|
+
stdio: options.stdio || 'pipe',
|
|
94
|
+
shell: false,
|
|
95
|
+
windowsHide: true,
|
|
96
|
+
env: childEnv,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { result, args: baseArgs };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function safeNpmCi(targetDir, options = {}) {
|
|
103
|
+
const { result, args } = runNpm('ci', targetDir, options);
|
|
104
|
+
if (result.status !== 0) {
|
|
105
|
+
const stderr = result.stderr ? String(result.stderr) : '';
|
|
106
|
+
const reason = result.error ? result.error.message : stderr;
|
|
107
|
+
throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${String(reason || '').slice(0, 500)}`);
|
|
108
|
+
}
|
|
109
|
+
return { ran: true, manager: 'npm', args };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function safeNpmInstall(targetDir, options = {}) {
|
|
113
|
+
const { result, args } = runNpm('install', targetDir, options);
|
|
114
|
+
if (result.status !== 0) {
|
|
115
|
+
const stderr = result.stderr ? String(result.stderr) : '';
|
|
116
|
+
const reason = result.error ? result.error.message : stderr;
|
|
117
|
+
throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${String(reason || '').slice(0, 500)}`);
|
|
118
|
+
}
|
|
119
|
+
return { ran: true, manager: 'npm', args };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function installSkillDependencies(targetDir, options = {}) {
|
|
123
|
+
const pkgJson = path.join(targetDir, 'package.json');
|
|
124
|
+
if (!fs.existsSync(pkgJson)) return { ran: false, manager: null };
|
|
125
|
+
|
|
126
|
+
const requireLockfile = options.requireLockfile !== false;
|
|
127
|
+
const manager = detectPackageManager(targetDir);
|
|
128
|
+
|
|
129
|
+
if (requireLockfile && manager !== 'npm') {
|
|
130
|
+
throw new Error('安装拒绝执行:缺少 package-lock.json(开启 security.requireLockfile=false 可放宽)');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const runOptions = {
|
|
134
|
+
allowPostinstall: Boolean(options.allowPostinstall),
|
|
135
|
+
stdio: options.stdio,
|
|
136
|
+
env: options.env,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const outcome = manager === 'npm'
|
|
140
|
+
? safeNpmCi(targetDir, runOptions)
|
|
141
|
+
: safeNpmInstall(targetDir, runOptions);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
ran: true,
|
|
145
|
+
manager: outcome.manager,
|
|
146
|
+
allowPostinstall: runOptions.allowPostinstall,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
ALLOWED_SUBCOMMANDS,
|
|
152
|
+
SAFE_ENV_KEYS,
|
|
153
|
+
buildSafeEnv,
|
|
154
|
+
detectPackageManager,
|
|
155
|
+
safeNpmCi,
|
|
156
|
+
safeNpmInstall,
|
|
157
|
+
installSkillDependencies,
|
|
158
|
+
};
|