skillgauntlet 1.3.4 → 1.3.6
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/bin/skillgauntlet.js +3 -1
- package/lib/findhost.js +33 -4
- package/lib/i18n.js +3 -0
- package/lib/spawn.js +5 -1
- package/package.json +1 -1
package/bin/skillgauntlet.js
CHANGED
|
@@ -20,7 +20,7 @@ import { renderResult } from '../lib/chart.js';
|
|
|
20
20
|
import { makeProgress } from '../lib/progress.js';
|
|
21
21
|
import { setLang, detectLang, errorText, t } from '../lib/i18n.js';
|
|
22
22
|
import { resolveBin, backend, backendFor, usesOwnDeepseek, setNativeModels, HOSTS, checkModel, draftQueries, draftQuality, runTrial, requireHost, pingHost, setHostBin, binInstalled } from '../lib/trial.js';
|
|
23
|
-
import { HOST_CLI, searchHost, installCommand, installHost, LOGIN, loginCommand } from '../lib/findhost.js';
|
|
23
|
+
import { HOST_CLI, searchHost, installCommand, installHost, userInstallDir, LOGIN, loginCommand } from '../lib/findhost.js';
|
|
24
24
|
import { launchSpec } from '../lib/spawn.js';
|
|
25
25
|
import { ensureTool } from '../lib/github.js';
|
|
26
26
|
import { spawnSync as runLogin } from 'node:child_process';
|
|
@@ -71,6 +71,8 @@ async function fixMissingHost(hostId, err) {
|
|
|
71
71
|
return true;
|
|
72
72
|
}
|
|
73
73
|
if (pick.install) {
|
|
74
|
+
const userDir = userInstallDir();
|
|
75
|
+
if (userDir) say(`\n${t('hostInstallUserDir', { dir: userDir })}`);
|
|
74
76
|
say(`\n${t('hostInstalling', { host: label })}`);
|
|
75
77
|
try { installHost(hostId); } catch (e) {
|
|
76
78
|
say(t('hostInstallFailed', { why: e.message }));
|
package/lib/findhost.js
CHANGED
|
@@ -23,6 +23,10 @@ function npmGlobalDir() {
|
|
|
23
23
|
} catch { return null; }
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function isFileTarget(p) {
|
|
27
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
function listDir(dir) {
|
|
27
31
|
try { return fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
28
32
|
}
|
|
@@ -31,7 +35,9 @@ function listDir(dir) {
|
|
|
31
35
|
function inDir(dir, bin) {
|
|
32
36
|
const out = [];
|
|
33
37
|
for (const e of listDir(dir)) {
|
|
34
|
-
|
|
38
|
+
// npm links a global package's command into <prefix>/bin as a symlink, and a Dirent for a symlink is not a file:
|
|
39
|
+
// following it is what finds a host installed with `--prefix`. A link must lead to a file.
|
|
40
|
+
if (!e.isFile() && !(e.isSymbolicLink() && isFileTarget(path.join(dir, e.name)))) continue;
|
|
35
41
|
const base = e.name.toLowerCase();
|
|
36
42
|
const exact = base === bin || base.startsWith(`${bin}.`);
|
|
37
43
|
// a Windows release asset keeps its platform suffix (codewhale-windows-x64.exe); only a real exe counts for those.
|
|
@@ -73,11 +79,34 @@ export function searchHost(host, dirs = searchDirs()) {
|
|
|
73
79
|
// Codewhale's package has an install script, and npm only runs install scripts it has been told to allow. The
|
|
74
80
|
// allowance is given for this one command (--allow-scripts) rather than written into the user's npm config.
|
|
75
81
|
const INSTALL_FLAGS = { codewhale: ' --allow-scripts=codewhale' };
|
|
76
|
-
|
|
82
|
+
// A system npm (Debian, Ubuntu, Fedora ...) keeps its global packages under /usr, which an ordinary user cannot write,
|
|
83
|
+
// so `npm install -g` ends in EACCES. Where that is so the install goes to the user's own ~/.local (its bin folder is
|
|
84
|
+
// already searched for host CLIs) and needs no sudo and no password. Asked once and remembered.
|
|
85
|
+
let prefixWritable = null;
|
|
86
|
+
export function npmPrefixWritable() {
|
|
87
|
+
if (prefixWritable !== null) return prefixWritable;
|
|
88
|
+
if (process.platform === 'win32') return (prefixWritable = true);
|
|
89
|
+
try {
|
|
90
|
+
const prefix = execSync('npm prefix -g', { encoding: 'utf8', timeout: 15000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
91
|
+
const modules = path.join(prefix, 'lib', 'node_modules');
|
|
92
|
+
fs.accessSync(fs.existsSync(modules) ? modules : prefix, fs.constants.W_OK);
|
|
93
|
+
prefixWritable = true;
|
|
94
|
+
} catch { prefixWritable = false; }
|
|
95
|
+
return prefixWritable;
|
|
96
|
+
}
|
|
97
|
+
export const userPrefix = () => path.join(os.homedir(), '.local');
|
|
98
|
+
// The folder the host will be installed into when the system one is not writable, else null.
|
|
99
|
+
export const userInstallDir = ({ writable = npmPrefixWritable() } = {}) => (writable ? null : userPrefix());
|
|
100
|
+
|
|
101
|
+
export function installCommand(host, { writable = npmPrefixWritable() } = {}) {
|
|
102
|
+
if (!HOST_CLI[host]?.pkg) return null;
|
|
103
|
+
const base = `npm install -g ${HOST_CLI[host].pkg}${INSTALL_FLAGS[host] || ''}`;
|
|
104
|
+
return writable ? base : `${base} --prefix "${userPrefix()}"`;
|
|
105
|
+
}
|
|
77
106
|
|
|
78
107
|
// Runs the npm install with its output on the user's terminal. Throws with a short reason on failure.
|
|
79
|
-
export function installHost(host, run = (cmd) => execSync(cmd, { stdio: 'inherit' })) {
|
|
80
|
-
const cmd = installCommand(host);
|
|
108
|
+
export function installHost(host, run = (cmd) => execSync(cmd, { stdio: 'inherit' }), opts = {}) {
|
|
109
|
+
const cmd = installCommand(host, opts);
|
|
81
110
|
if (!cmd) throw new Error('no installer for this host');
|
|
82
111
|
try { run(cmd); } catch (err) { throw new Error(String(err.message || err).split(/\r?\n/)[0]); }
|
|
83
112
|
}
|
package/lib/i18n.js
CHANGED
|
@@ -167,6 +167,7 @@ const MESSAGES = {
|
|
|
167
167
|
prNeedsReview: '没有自动合并,已转给维护者审核(常见原因:GitHub 账号注册不满 30 天,或今天已提交超过 5 条)。你不需要再做什么:{url}',
|
|
168
168
|
prTimeout: '等了 {m} 分钟还没合并,PR 还在:{url},之后随时在浏览器里看。',
|
|
169
169
|
toolSudo: '这一步要用管理员权限安装:下面可能会让你输入这台电脑的登录密码(输入时不会显示字符,输完按回车)。',
|
|
170
|
+
hostInstallUserDir: '这台电脑的 npm 全局目录你没有写权限(装到系统目录要 sudo),所以会装到你自己的目录:{dir},不需要 sudo 和密码。',
|
|
170
171
|
modelUnknown: '{host} 的模型清单里没有“{model}”。它现在支持:{list}。请重新选一个,或选“默认模型”。',
|
|
171
172
|
updateAvail: '有新版本 {latest}(你现在用的是 {current})。',
|
|
172
173
|
updateQ: '要更新吗?',
|
|
@@ -599,6 +600,7 @@ const MESSAGES = {
|
|
|
599
600
|
prNeedsReview: 'It was not merged automatically and has gone to a maintainer for review (common reasons: the GitHub account is under 30 days old, or more than 5 submissions today). You need do nothing more: {url}',
|
|
600
601
|
prTimeout: 'Waited {m} minutes and it is not merged yet. The pull request is still there: {url}; look any time in the browser.',
|
|
601
602
|
toolSudo: 'This install needs administrator rights: you may be asked for this computer\'s password below (nothing is shown as you type; press enter when done).',
|
|
603
|
+
hostInstallUserDir: 'You cannot write to this computer\'s global npm folder (installing there needs sudo), so it is installed into your own folder instead: {dir}. No sudo or password is needed.',
|
|
602
604
|
modelUnknown: '{host} does not list a model called "{model}". It currently supports: {list}. Pick another, or choose the default model.',
|
|
603
605
|
updateAvail: 'A newer version {latest} is available (you are on {current}).',
|
|
604
606
|
updateQ: 'Update?',
|
|
@@ -1079,6 +1081,7 @@ Layer 0 always runs first: it is free, and it disqualifies most problems before
|
|
|
1079
1081
|
prNeedsReview: '自動マージされず、メンテナーのレビューに回りました(主な理由:GitHub アカウントが作成 30 日未満、または本日 5 件を超える提出)。これ以上の操作は不要です:{url}',
|
|
1080
1082
|
prTimeout: '{m} 分待ちましたがまだマージされていません。プルリクエストは残っています:{url}。いつでもブラウザで確認できます。',
|
|
1081
1083
|
toolSudo: 'このインストールには管理者権限が必要です。このコンピューターのパスワードの入力を求められることがあります(入力中は何も表示されません。入力後に Enter)。',
|
|
1084
|
+
hostInstallUserDir: 'このコンピューターのグローバル npm フォルダには書き込めません(sudo が必要)。代わりにあなた自身のフォルダ {dir} にインストールします。sudo もパスワードも不要です。',
|
|
1082
1085
|
modelUnknown: '{host} のモデル一覧に「{model}」はありません。現在対応:{list}。別のものを選ぶか、既定のモデルを選んでください。',
|
|
1083
1086
|
updateAvail: '新しいバージョン {latest} があります(現在は {current})。',
|
|
1084
1087
|
updateQ: '更新しますか?',
|
package/lib/spawn.js
CHANGED
|
@@ -57,9 +57,13 @@ function findJsEntry(bin, name) {
|
|
|
57
57
|
return candidates.find((p) => { try { return fs.statSync(p).isFile(); } catch { return false; } });
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export function runnerFor(bin, name = path.basename(bin).replace(/\.(cmd|bat|exe)$/i, '')) {
|
|
60
|
+
export function runnerFor(bin, name = path.basename(bin).replace(/\.(cmd|bat|exe)$/i, ''), platform = process.platform) {
|
|
61
61
|
if (looksLikeJs(bin)) return { cmd: process.execPath, prefix: [bin], shell: false };
|
|
62
62
|
if (/\.exe$/i.test(bin)) return { cmd: bin, prefix: [], shell: false };
|
|
63
|
+
// Outside Windows a command is run directly (its shebang picks the interpreter), never through a shell: the prompt
|
|
64
|
+
// holds text from the skill under test, and the cmd.exe-style quoting below means nothing to sh, where $( ) and
|
|
65
|
+
// backticks in that text would run.
|
|
66
|
+
if (platform !== 'win32' && !/\.(cmd|bat)$/i.test(bin)) return { cmd: bin, prefix: [], shell: false };
|
|
63
67
|
const entry = findJsEntry(bin, name);
|
|
64
68
|
if (entry) return { cmd: process.execPath, prefix: [entry], shell: false };
|
|
65
69
|
return { cmd: bin, prefix: [], shell: true };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillgauntlet",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "Check an agent skill before you install it: context cost, permission surface, code risk, instruction hijacking, description quality, trigger rate and gain.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|