pwsh-guide 0.1.0 → 0.2.0
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.md +32 -23
- package/bin/pwsh-guide.js +2 -2
- package/package.json +7 -3
- package/scripts/probe.unix.sh +56 -0
- package/scripts/probe.windows.ps1 +33 -0
- package/src/detect.js +7 -3
- package/src/probe.js +208 -13
- package/src/render.js +85 -26
- package/templates/{SKILL.md → SKILL.powershell.md} +6 -5
- package/templates/SKILL.unix.md +58 -0
- package/scripts/probe.ps1 +0 -107
package/README.md
CHANGED
|
@@ -1,47 +1,56 @@
|
|
|
1
1
|
# pwsh-guide
|
|
2
2
|
|
|
3
|
-
生成项目级 Agent Skill
|
|
3
|
+
生成项目级 Agent Skill:跨平台探测当前环境(Windows PowerShell / macOS / Linux shell)与本项目上下文,产出可靠的命令使用指南(`SKILL.md` + `references/`),供 Codex、Claude Code 等代理自动读取,减少命令失败、乱码与反复试错。
|
|
4
4
|
|
|
5
5
|
## 原理
|
|
6
6
|
|
|
7
|
-
AI
|
|
7
|
+
AI 代理执行命令失败,本质是"生成即采样":模型先验以 bash 为主、缺少本机环境事实。`pwsh-guide` 把环境探测结果固化进 skill 的 references(惰性加载、不占上下文),并用命令铁律 + 失败诊断约束生成空间。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- Windows 下生成 PowerShell 指南;macOS/Linux 下生成 bash/zsh 指南(只生成当前环境可用内容,省 token)
|
|
10
|
+
- 平台无关探测全部用 Node 原生实现(`os` / `process.env` / `fs`),不依赖特定 shell
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
npm link
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## 用法
|
|
12
|
+
## 安装与使用
|
|
16
13
|
|
|
17
|
-
```
|
|
18
|
-
#
|
|
19
|
-
pwsh-guide init
|
|
20
|
-
|
|
21
|
-
pwsh-guide
|
|
14
|
+
```bash
|
|
15
|
+
# 项目根目录运行(npx 方式,无需全局安装)
|
|
16
|
+
npx pwsh-guide init
|
|
17
|
+
# 环境变化后更新
|
|
18
|
+
npx pwsh-guide refresh
|
|
19
|
+
# 只输出探测 JSON,不写文件
|
|
20
|
+
npx pwsh-guide init --dry-run
|
|
22
21
|
```
|
|
23
22
|
|
|
24
23
|
生成的 skill 位于 `.agents/skills/pwsh-guide/`:
|
|
25
24
|
|
|
26
25
|
- `SKILL.md`:命令执行铁律、编码与乱码处理、失败分类诊断、禁止事项
|
|
27
|
-
- `references/environment.md`:探测到的本机环境事实(
|
|
26
|
+
- `references/environment.md`:探测到的本机环境事实(shell 版本/编码/locale/工具/项目上下文)
|
|
28
27
|
- `references/commands.md`:按探测到的工具动态裁剪的命令模板
|
|
29
28
|
|
|
30
29
|
## 探测内容(只读)
|
|
31
30
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
31
|
+
- 平台与 OS:`os.platform()` / `os.release()` / `os.arch()` / 主机名 / 用户与临时目录
|
|
32
|
+
- shell(按平台深度探测):
|
|
33
|
+
- Windows:PowerShell 版本 / PSEdition / 控制台输出编码 / `$OutputEncoding` / chcp / 执行策略 / pwsh 7 可用性
|
|
34
|
+
- macOS/Linux:默认 shell(`$SHELL`)/ 可用 shell(bash / zsh / fish)/ locale(LANG / LC_ALL / LC_CTYPE)/ WSL 标注
|
|
35
|
+
- 工具:git、node、npm、npx、pnpm、yarn、bun、python、py、uv、pip、docker、make、rg、gh、pwsh、cargo、go 的可用性与版本(PATH 遍历 + `--version`,超时/缺失记为 null)
|
|
34
36
|
- 项目:git 根(向上查找,无则用当前目录)、标记文件、venv、package.json scripts、第一层子目录
|
|
35
37
|
|
|
36
38
|
## 开发
|
|
37
39
|
|
|
38
40
|
```text
|
|
39
|
-
bin/pwsh-guide.js
|
|
40
|
-
src/probe.js
|
|
41
|
-
src/detect.js
|
|
42
|
-
src/render.js
|
|
43
|
-
scripts/probe.ps1
|
|
44
|
-
|
|
41
|
+
bin/pwsh-guide.js CLI 入口(init / refresh / --dry-run)
|
|
42
|
+
src/probe.js 跨平台主探测(Node 原生;按平台分发深度脚本)
|
|
43
|
+
src/detect.js 仓库根判定与目标目录解析
|
|
44
|
+
src/render.js 按 shell 族渲染 SKILL.md / environment.md / commands.md
|
|
45
|
+
scripts/probe.windows.ps1 Windows 深度探测(PS 5.1 兼容,输出 UTF-8 JSON)
|
|
46
|
+
scripts/probe.unix.sh Unix 深度探测(POSIX sh 兼容,输出 UTF-8 JSON)
|
|
47
|
+
templates/SKILL.powershell.md PowerShell 族 SKILL.md 模板({{占位符}} 由 render.js 填充)
|
|
48
|
+
templates/SKILL.unix.md Unix 族 SKILL.md 模板
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
变更记录由 OpenSpec 管理(`openspec
|
|
51
|
+
变更记录由 OpenSpec 管理(`openspec/`):当前变更 `add-pwsh-guide`(已发布 v0.1.0)、`cross-platform-0.2`(进行中)。
|
|
52
|
+
|
|
53
|
+
## 验证范围
|
|
54
|
+
|
|
55
|
+
- Windows:本机实测(探测 JSON / init / refresh)
|
|
56
|
+
- macOS/Linux:静态审查(POSIX sh 脚本与 Node 分支),尚未真机运行
|
package/bin/pwsh-guide.js
CHANGED
|
@@ -8,7 +8,7 @@ import { probeEnvironment } from '../src/probe.js';
|
|
|
8
8
|
import { resolveSkillDir } from '../src/detect.js';
|
|
9
9
|
import { renderSkill, renderEnvironment, renderCommands } from '../src/render.js';
|
|
10
10
|
|
|
11
|
-
const USAGE = `pwsh-guide - 生成项目级
|
|
11
|
+
const USAGE = `pwsh-guide - 生成项目级 Shell 操作指南 Agent Skill(跨平台:Windows PowerShell / macOS / Linux)
|
|
12
12
|
|
|
13
13
|
用法:
|
|
14
14
|
pwsh-guide init 探测环境并在 .agents/skills/pwsh-guide/ 生成 skill
|
|
@@ -79,4 +79,4 @@ async function main() {
|
|
|
79
79
|
main().catch((err) => {
|
|
80
80
|
console.error(`[pwsh-guide] 失败: ${err.message}`);
|
|
81
81
|
process.exit(1);
|
|
82
|
-
});
|
|
82
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pwsh-guide",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Generate a project-level Agent Skill: probe the current Windows
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Generate a project-level Agent Skill: probe the current environment (Windows PowerShell / macOS / Linux shell) and produce a reliable command guide (SKILL.md + references) for agents such as Codex and Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"pwsh-guide": "bin/pwsh-guide.js"
|
|
@@ -21,9 +21,13 @@
|
|
|
21
21
|
"keywords": [
|
|
22
22
|
"agent-skills",
|
|
23
23
|
"powershell",
|
|
24
|
+
"bash",
|
|
25
|
+
"shell",
|
|
24
26
|
"windows",
|
|
27
|
+
"macos",
|
|
28
|
+
"linux",
|
|
25
29
|
"codex",
|
|
26
30
|
"claude-code"
|
|
27
31
|
],
|
|
28
32
|
"license": "MIT"
|
|
29
|
-
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# probe.unix.sh - pwsh-guide Unix 深度探测脚本(只读,不修改任何文件或系统设置)
|
|
3
|
+
# 用法:sh probe.unix.sh
|
|
4
|
+
# 输出:UTF-8 JSON 到 stdout(shell 深度字段,供 src/probe.js 合并)
|
|
5
|
+
# 兼容:POSIX sh(macOS / Linux)
|
|
6
|
+
|
|
7
|
+
# JSON 字符串转义(最小实现)
|
|
8
|
+
json_escape() {
|
|
9
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# locale 单项取值:优先 locale 命令,缺失时回退环境变量
|
|
13
|
+
get_locale() {
|
|
14
|
+
key="$1"
|
|
15
|
+
value=$(locale 2>/dev/null | sed -n "s/^$key=//p" | tr -d '"')
|
|
16
|
+
if [ -z "$value" ]; then
|
|
17
|
+
case "$key" in
|
|
18
|
+
LANG) value="$LANG" ;;
|
|
19
|
+
LC_ALL) value="$LC_ALL" ;;
|
|
20
|
+
LC_CTYPE) value="$LC_CTYPE" ;;
|
|
21
|
+
esac
|
|
22
|
+
fi
|
|
23
|
+
printf '%s' "$value"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# 查找 shell 路径;找不到输出空
|
|
27
|
+
shell_path() {
|
|
28
|
+
command -v "$1" 2>/dev/null || printf ''
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
default_shell="${SHELL:-/bin/sh}"
|
|
32
|
+
temp_dir="${TMPDIR:-/tmp}"
|
|
33
|
+
|
|
34
|
+
# WSL 标注:uname -r 含 microsoft
|
|
35
|
+
wsl=no
|
|
36
|
+
uname_r=$(uname -r 2>/dev/null || printf '')
|
|
37
|
+
case "$uname_r" in
|
|
38
|
+
*[Mm]icrosoft*) wsl=yes ;;
|
|
39
|
+
esac
|
|
40
|
+
|
|
41
|
+
cat <<EOF
|
|
42
|
+
{
|
|
43
|
+
"defaultShell": "$(json_escape "$default_shell")",
|
|
44
|
+
"bash": "$(shell_path bash)",
|
|
45
|
+
"zsh": "$(shell_path zsh)",
|
|
46
|
+
"fish": "$(shell_path fish)",
|
|
47
|
+
"locale": {
|
|
48
|
+
"LANG": "$(json_escape "$(get_locale LANG)")",
|
|
49
|
+
"LC_ALL": "$(json_escape "$(get_locale LC_ALL)")",
|
|
50
|
+
"LC_CTYPE": "$(json_escape "$(get_locale LC_CTYPE)")"
|
|
51
|
+
},
|
|
52
|
+
"userProfile": "$(json_escape "$HOME")",
|
|
53
|
+
"tempDir": "$(json_escape "$temp_dir")",
|
|
54
|
+
"wsl": "$wsl"
|
|
55
|
+
}
|
|
56
|
+
EOF
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# probe.windows.ps1 - pwsh-guide Windows 深度探测脚本(只读,不修改任何文件或系统设置)
|
|
2
|
+
# 用法:powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File probe.windows.ps1
|
|
3
|
+
# 输出:UTF-8 JSON 到 stdout(shell 深度字段,供 src/probe.js 合并)
|
|
4
|
+
# 兼容:Windows PowerShell 5.1+(不使用 PS7 新语法:??、&&、三元等)
|
|
5
|
+
|
|
6
|
+
$originalConsoleEncoding = [Console]::OutputEncoding.WebName
|
|
7
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
8
|
+
$ErrorActionPreference = 'Continue'
|
|
9
|
+
|
|
10
|
+
$executionPolicy = @{}
|
|
11
|
+
Get-ExecutionPolicy -List | ForEach-Object { $executionPolicy[[string]$_.Scope] = [string]$_.ExecutionPolicy }
|
|
12
|
+
|
|
13
|
+
$chcp = $null
|
|
14
|
+
try {
|
|
15
|
+
$chcpText = (& chcp 2>$null | Select-Object -First 1)
|
|
16
|
+
if ($chcpText -match '(\d+)') { $chcp = [int]$Matches[1] }
|
|
17
|
+
} catch { $chcp = $null }
|
|
18
|
+
|
|
19
|
+
$result = [pscustomobject]@{
|
|
20
|
+
defaultShell = 'powershell.exe'
|
|
21
|
+
psVersion = $PSVersionTable.PSVersion.ToString()
|
|
22
|
+
psEdition = $PSVersionTable.PSEdition
|
|
23
|
+
hostName = $Host.Name
|
|
24
|
+
pwsh7Available = ($null -ne (Get-Command pwsh -ErrorAction SilentlyContinue))
|
|
25
|
+
osVersion = [System.Environment]::OSVersion.VersionString
|
|
26
|
+
consoleOutputEncoding = $originalConsoleEncoding
|
|
27
|
+
outputEncoding = $OutputEncoding.WebName
|
|
28
|
+
chcp = $chcp
|
|
29
|
+
executionPolicy = $executionPolicy
|
|
30
|
+
userProfile = $env:USERPROFILE
|
|
31
|
+
tempDir = $env:TEMP
|
|
32
|
+
}
|
|
33
|
+
$result | ConvertTo-Json -Depth 5
|
package/src/detect.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
// detect.js - 仓库根判定与 skill 目标目录解析
|
|
2
|
-
// 用法:findProjectRoot(cwd) -> string;resolveSkillDir(cwd) -> string
|
|
2
|
+
// 用法:findGitRoot(cwd) -> string|null;findProjectRoot(cwd) -> string;resolveSkillDir(cwd) -> string
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
-
export function
|
|
6
|
+
export function findGitRoot(startDir) {
|
|
7
7
|
const start = path.resolve(startDir);
|
|
8
8
|
let dir = start;
|
|
9
9
|
for (;;) {
|
|
10
10
|
if (fs.existsSync(path.join(dir, '.git'))) return dir;
|
|
11
11
|
const parent = path.dirname(dir);
|
|
12
|
-
if (parent === dir) return
|
|
12
|
+
if (parent === dir) return null; // 找不到 .git
|
|
13
13
|
dir = parent;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export function findProjectRoot(startDir) {
|
|
18
|
+
return findGitRoot(startDir) ?? path.resolve(startDir); // 找不到 .git,降级为当前目录
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
export function resolveSkillDir(cwd) {
|
|
18
22
|
return path.join(findProjectRoot(cwd), '.agents', 'skills', 'pwsh-guide');
|
|
19
23
|
}
|
package/src/probe.js
CHANGED
|
@@ -1,41 +1,236 @@
|
|
|
1
|
-
// probe.js -
|
|
1
|
+
// probe.js - 跨平台环境主探测(Node 原生,零 shell 依赖)
|
|
2
2
|
// 用法:probeEnvironment(cwd) -> Promise<object>
|
|
3
|
-
|
|
3
|
+
// 平台无关信息(platform/os/default shell/tools/project)由 Node 原生探测;
|
|
4
|
+
// 平台深度信息(PS 版本表、locale 等)由分平台脚本补充(Windows powershell.exe / Unix /bin/sh)。
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import fs from 'node:fs';
|
|
4
7
|
import path from 'node:path';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
5
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { findGitRoot } from './detect.js';
|
|
6
11
|
|
|
7
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const
|
|
13
|
+
const SCRIPTS_DIR = path.resolve(__dirname, '..', 'scripts');
|
|
14
|
+
|
|
15
|
+
const DEEP_SCRIPTS = {
|
|
16
|
+
win32: path.join(SCRIPTS_DIR, 'probe.windows.ps1'),
|
|
17
|
+
unix: path.join(SCRIPTS_DIR, 'probe.unix.sh'),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const TOOL_NAMES = [
|
|
21
|
+
'git', 'node', 'npm', 'npx', 'pnpm', 'yarn', 'bun',
|
|
22
|
+
'python', 'py', 'uv', 'pip', 'docker', 'make', 'rg', 'gh', 'pwsh', 'cargo', 'go',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const MARKER_NAMES = [
|
|
26
|
+
'package.json', 'pyproject.toml', 'requirements.txt', 'uv.lock',
|
|
27
|
+
'go.mod', 'Cargo.toml', 'pnpm-lock.yaml', 'yarn.lock', 'Dockerfile',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const IGNORED_SUBDIRS = new Set(['.git', '.venv', 'venv', 'node_modules']);
|
|
31
|
+
const VERSION_TIMEOUT_MS = 5000;
|
|
9
32
|
|
|
10
33
|
export async function probeEnvironment(cwd) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
const platform = process.platform; // 'win32' | 'darwin' | 'linux'(其它平台按 unix 处理)
|
|
35
|
+
const family = platform === 'win32' ? 'powershell' : 'unix';
|
|
36
|
+
|
|
37
|
+
const [deepShell, tools, project] = await Promise.all([
|
|
38
|
+
runDeepProbe(platform, cwd),
|
|
39
|
+
probeTools(),
|
|
40
|
+
probeProject(cwd),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const defaultShell = resolveDefaultShell(platform, deepShell);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
schemaVersion: 2,
|
|
47
|
+
platform,
|
|
48
|
+
family,
|
|
49
|
+
os: {
|
|
50
|
+
platform,
|
|
51
|
+
release: os.release(),
|
|
52
|
+
arch: os.arch(),
|
|
53
|
+
hostname: os.hostname(),
|
|
54
|
+
homeDir: os.homedir(),
|
|
55
|
+
tmpDir: os.tmpdir(),
|
|
56
|
+
},
|
|
57
|
+
shell: {
|
|
58
|
+
family,
|
|
59
|
+
defaultShell,
|
|
60
|
+
defaultShellName: defaultShell.split(/[\\/]/).pop().toLowerCase(),
|
|
61
|
+
...deepShell,
|
|
62
|
+
},
|
|
63
|
+
tools,
|
|
64
|
+
project,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveDefaultShell(platform, deepShell) {
|
|
69
|
+
if (deepShell && deepShell.defaultShell) return deepShell.defaultShell;
|
|
70
|
+
if (platform === 'win32') return 'powershell.exe';
|
|
71
|
+
return process.env.SHELL || '/bin/sh';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 分平台深度探测:失败时降级为空对象,不中断主探测
|
|
75
|
+
function runDeepProbe(platform, cwd) {
|
|
76
|
+
const script = DEEP_SCRIPTS[platform] || DEEP_SCRIPTS.unix;
|
|
77
|
+
if (platform === 'win32') {
|
|
78
|
+
return runDeepScript('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', script], cwd);
|
|
79
|
+
}
|
|
80
|
+
return runDeepScript('/bin/sh', [script], cwd);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runDeepScript(file, args, cwd) {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const child = spawn(file, args, { cwd, shell: false, windowsHide: true });
|
|
17
86
|
let stdout = '';
|
|
18
87
|
let stderr = '';
|
|
19
88
|
child.stdout.setEncoding('utf8');
|
|
20
89
|
child.stderr.setEncoding('utf8');
|
|
21
90
|
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
22
91
|
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
23
|
-
child.on('error', (err) =>
|
|
92
|
+
child.on('error', (err) => {
|
|
93
|
+
console.warn(`[pwsh-guide] 深度探测脚本无法启动(${file}):${err.message}`);
|
|
94
|
+
resolve({});
|
|
95
|
+
});
|
|
24
96
|
child.on('close', (code) => {
|
|
25
97
|
if (code !== 0) {
|
|
26
|
-
|
|
98
|
+
console.warn(`[pwsh-guide] 深度探测脚本退出码 ${code}:${stderr.trim() || stdout.trim()}`);
|
|
99
|
+
resolve({});
|
|
27
100
|
return;
|
|
28
101
|
}
|
|
29
102
|
const jsonStart = stdout.indexOf('{');
|
|
30
103
|
if (jsonStart === -1) {
|
|
31
|
-
|
|
104
|
+
console.warn('[pwsh-guide] 深度探测脚本未输出 JSON,已降级');
|
|
105
|
+
resolve({});
|
|
32
106
|
return;
|
|
33
107
|
}
|
|
34
108
|
try {
|
|
35
109
|
resolve(JSON.parse(stdout.slice(jsonStart)));
|
|
36
110
|
} catch (err) {
|
|
37
|
-
|
|
111
|
+
console.warn(`[pwsh-guide] 深度探测 JSON 解析失败:${err.message}`);
|
|
112
|
+
resolve({});
|
|
38
113
|
}
|
|
39
114
|
});
|
|
40
115
|
});
|
|
41
116
|
}
|
|
117
|
+
|
|
118
|
+
// 工具探测:PATH 遍历 + 版本命令,全部容错
|
|
119
|
+
async function probeTools() {
|
|
120
|
+
const results = [];
|
|
121
|
+
for (const name of TOOL_NAMES) {
|
|
122
|
+
const toolPath = findToolPath(name);
|
|
123
|
+
if (!toolPath) {
|
|
124
|
+
results.push({ name, version: null, source: null });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const version = await runVersionCommand(toolPath);
|
|
128
|
+
results.push({ name, version, source: toolPath });
|
|
129
|
+
}
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findToolPath(name) {
|
|
134
|
+
const exts = toolExtensions();
|
|
135
|
+
const dirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
136
|
+
for (const dir of dirs) {
|
|
137
|
+
for (const ext of exts) {
|
|
138
|
+
const candidate = path.join(dir, name + ext);
|
|
139
|
+
try {
|
|
140
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
141
|
+
} catch { /* 继续查找 */ }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function toolExtensions() {
|
|
148
|
+
if (process.platform !== 'win32') return [''];
|
|
149
|
+
return (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
150
|
+
.split(';').map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function runVersionCommand(toolPath) {
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
let file;
|
|
156
|
+
let args;
|
|
157
|
+
let options = { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true };
|
|
158
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(toolPath)) {
|
|
159
|
+
// .cmd/.bat 需经 cmd.exe 执行;shell:true 时命令串内的引号不被 Node 转义,可正确处理带空格/中文路径
|
|
160
|
+
file = `"${toolPath}" --version`;
|
|
161
|
+
options = { ...options, shell: true };
|
|
162
|
+
} else {
|
|
163
|
+
file = toolPath;
|
|
164
|
+
args = ['--version'];
|
|
165
|
+
}
|
|
166
|
+
const child = spawn(file, args, options);
|
|
167
|
+
let out = '';
|
|
168
|
+
child.stdout.setEncoding('utf8');
|
|
169
|
+
child.stderr.setEncoding('utf8');
|
|
170
|
+
child.stdout.on('data', (chunk) => { out += chunk; });
|
|
171
|
+
child.stderr.on('data', (chunk) => { out += chunk; });
|
|
172
|
+
const timer = setTimeout(() => { child.kill(); resolve(null); }, VERSION_TIMEOUT_MS);
|
|
173
|
+
child.on('error', () => { clearTimeout(timer); resolve(null); });
|
|
174
|
+
child.on('close', () => {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
const line = out.split(/\r?\n/).map((s) => s.trim()).find((s) => s.length > 0);
|
|
177
|
+
resolve(line ?? null);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 项目上下文:全部 Node fs 实现
|
|
183
|
+
function probeProject(cwd) {
|
|
184
|
+
const subdirs = readSubdirs(cwd);
|
|
185
|
+
return {
|
|
186
|
+
cwd,
|
|
187
|
+
gitRoot: findGitRoot(cwd),
|
|
188
|
+
markers: findMarkers(cwd, subdirs),
|
|
189
|
+
venv: findVenv(cwd),
|
|
190
|
+
packageScripts: readPackageScripts(cwd),
|
|
191
|
+
subdirs,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readSubdirs(cwd) {
|
|
196
|
+
try {
|
|
197
|
+
return fs.readdirSync(cwd, { withFileTypes: true })
|
|
198
|
+
.filter((entry) => entry.isDirectory() && !IGNORED_SUBDIRS.has(entry.name))
|
|
199
|
+
.map((entry) => entry.name)
|
|
200
|
+
.sort();
|
|
201
|
+
} catch {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function findMarkers(cwd, subdirs) {
|
|
207
|
+
const found = [];
|
|
208
|
+
const scope = ['.', ...subdirs];
|
|
209
|
+
for (const marker of MARKER_NAMES) {
|
|
210
|
+
for (const dir of scope) {
|
|
211
|
+
const target = dir === '.' ? path.join(cwd, marker) : path.join(cwd, dir, marker);
|
|
212
|
+
if (fs.existsSync(target)) {
|
|
213
|
+
found.push(dir === '.' ? marker : path.join(dir, marker));
|
|
214
|
+
break; // 每个 marker 只记录一次(优先当前目录)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return found;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function findVenv(cwd) {
|
|
222
|
+
for (const dir of ['.venv', 'venv']) {
|
|
223
|
+
if (fs.existsSync(path.join(cwd, dir, 'pyvenv.cfg'))) return dir;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readPackageScripts(cwd) {
|
|
229
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
230
|
+
try {
|
|
231
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
232
|
+
return pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
233
|
+
} catch {
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
}
|
package/src/render.js
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
// render.js - 将探测 JSON 渲染为 SKILL.md / references/environment.md / references/commands.md
|
|
2
2
|
// 用法:renderSkill(env)、renderEnvironment(env)、renderCommands(env)
|
|
3
|
+
// env.family 与 env.shell.family:'powershell' | 'unix'(由 src/probe.js 探测)
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const
|
|
9
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates');
|
|
10
|
+
|
|
11
|
+
const SKILL_TEMPLATES = {
|
|
12
|
+
powershell: path.join(TEMPLATES_DIR, 'SKILL.powershell.md'),
|
|
13
|
+
unix: path.join(TEMPLATES_DIR, 'SKILL.unix.md'),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const templateCache = new Map();
|
|
17
|
+
|
|
18
|
+
function loadTemplate(key) {
|
|
19
|
+
if (!templateCache.has(key)) {
|
|
20
|
+
templateCache.set(key, fs.readFileSync(SKILL_TEMPLATES[key], 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
return templateCache.get(key);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveFamily(env) {
|
|
26
|
+
return env.family === 'unix' ? 'unix' : 'powershell';
|
|
27
|
+
}
|
|
9
28
|
|
|
10
29
|
function fill(template, values) {
|
|
11
30
|
let out = template;
|
|
12
|
-
|
|
13
|
-
|
|
31
|
+
const placeholderRe = /\{\{([a-zA-Z0-9_]+)\}\}/g;
|
|
32
|
+
for (const match of template.matchAll(placeholderRe)) {
|
|
33
|
+
out = out.split(`{{${match[1]}}}`).join(String(values[match[1]] ?? ''));
|
|
14
34
|
}
|
|
15
35
|
return out;
|
|
16
36
|
}
|
|
@@ -19,11 +39,6 @@ function getTool(env, name) {
|
|
|
19
39
|
return (env.tools || []).find((t) => t.name === name);
|
|
20
40
|
}
|
|
21
41
|
|
|
22
|
-
function toolVersion(env, name) {
|
|
23
|
-
const t = getTool(env, name);
|
|
24
|
-
return t && t.version ? String(t.version) : null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
42
|
function toolsSummary(env) {
|
|
28
43
|
const tools = (env.tools || []).filter((t) => t.version);
|
|
29
44
|
if (tools.length === 0) return '- 未探测到常用工具,请检查 PATH 后运行 pwsh-guide refresh。';
|
|
@@ -36,6 +51,15 @@ function executionPolicySummary(env) {
|
|
|
36
51
|
return parts.length ? parts.join(', ') : '未获取';
|
|
37
52
|
}
|
|
38
53
|
|
|
54
|
+
function localeSummary(env) {
|
|
55
|
+
const l = (env.shell && env.shell.locale) || {};
|
|
56
|
+
const parts = [];
|
|
57
|
+
for (const key of ['LANG', 'LC_ALL', 'LC_CTYPE']) {
|
|
58
|
+
if (l[key]) parts.push(`${key}=${l[key]}`);
|
|
59
|
+
}
|
|
60
|
+
return parts.length ? parts.join(', ') : '未获取';
|
|
61
|
+
}
|
|
62
|
+
|
|
39
63
|
function projectContextBlock(env) {
|
|
40
64
|
const p = env.project || {};
|
|
41
65
|
const lines = [];
|
|
@@ -54,22 +78,32 @@ function projectContextBlock(env) {
|
|
|
54
78
|
}
|
|
55
79
|
|
|
56
80
|
export function renderSkill(env) {
|
|
81
|
+
const family = resolveFamily(env);
|
|
57
82
|
const shell = env.shell || {};
|
|
58
|
-
|
|
83
|
+
const os = env.os || {};
|
|
84
|
+
const values = {
|
|
59
85
|
generatedAt: new Date().toISOString(),
|
|
86
|
+
osVersion: `${os.platform || ''} ${os.release || ''}`.trim() || '未知',
|
|
87
|
+
shellName: shell.defaultShellName || shell.defaultShell || (family === 'powershell' ? 'powershell' : 'sh'),
|
|
60
88
|
psVersion: shell.psVersion || '未知',
|
|
61
89
|
consoleOutputEncoding: shell.consoleOutputEncoding || '未知',
|
|
62
90
|
chcp: shell.chcp ?? '未知',
|
|
63
91
|
executionPolicySummary: executionPolicySummary(env),
|
|
92
|
+
pwsh7Note: shell.pwsh7Available ? '已检测到 pwsh 7,需要时可优先使用 `pwsh`(语法兼容、性能更好)。' : '',
|
|
93
|
+
localeSummary: localeSummary(env),
|
|
94
|
+
fishNote: shell.defaultShellName === 'fish' ? '注意:默认 shell 是 fish,语法与 bash/zsh 不同(本文以 bash/zsh 为主)。' : '',
|
|
64
95
|
gitRoot: (env.project && env.project.gitRoot) || '未发现 .git',
|
|
65
96
|
cwd: (env.project && env.project.cwd) || process.cwd(),
|
|
66
97
|
toolsSummary: toolsSummary(env),
|
|
67
98
|
projectContext: projectContextBlock(env),
|
|
68
|
-
}
|
|
99
|
+
};
|
|
100
|
+
return fill(loadTemplate(family), values);
|
|
69
101
|
}
|
|
70
102
|
|
|
71
103
|
function renderEnvironment(env) {
|
|
104
|
+
const family = resolveFamily(env);
|
|
72
105
|
const shell = env.shell || {};
|
|
106
|
+
const os = env.os || {};
|
|
73
107
|
const p = env.project || {};
|
|
74
108
|
const lines = [];
|
|
75
109
|
lines.push('# 环境事实(pwsh-guide 探测结果)');
|
|
@@ -80,17 +114,28 @@ function renderEnvironment(env) {
|
|
|
80
114
|
lines.push('');
|
|
81
115
|
lines.push('| 项 | 值 |');
|
|
82
116
|
lines.push('| --- | --- |');
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
117
|
+
if (family === 'powershell') {
|
|
118
|
+
lines.push(`| OS | ${os.release ?? '-'} |`);
|
|
119
|
+
lines.push(`| 默认 shell | ${shell.defaultShell ?? '-'} |`);
|
|
120
|
+
lines.push(`| PowerShell 版本 | ${shell.psVersion ?? '-'} |`);
|
|
121
|
+
lines.push(`| PSEdition | ${shell.psEdition ?? '-'} |`);
|
|
122
|
+
lines.push(`| 主机 | ${shell.hostName ?? '-'} |`);
|
|
123
|
+
lines.push(`| pwsh 7 可用 | ${shell.pwsh7Available ? '是' : '否'} |`);
|
|
124
|
+
lines.push(`| 控制台输出编码 | ${shell.consoleOutputEncoding ?? '-'} |`);
|
|
125
|
+
lines.push(`| $OutputEncoding | ${shell.outputEncoding ?? '-'} |`);
|
|
126
|
+
lines.push(`| 代码页 chcp | ${shell.chcp ?? '-'} |`);
|
|
127
|
+
lines.push(`| 执行策略 | ${executionPolicySummary(env)} |`);
|
|
128
|
+
} else {
|
|
129
|
+
lines.push(`| OS | ${os.release ?? '-'} |`);
|
|
130
|
+
lines.push(`| 默认 shell | ${shell.defaultShell ?? '-'} |`);
|
|
131
|
+
lines.push(`| bash | ${shell.bash ? `\`${shell.bash}\`` : '-'} |`);
|
|
132
|
+
lines.push(`| zsh | ${shell.zsh ? `\`${shell.zsh}\`` : '-'} |`);
|
|
133
|
+
lines.push(`| fish | ${shell.fish ? `\`${shell.fish}\`` : '-'} |`);
|
|
134
|
+
lines.push(`| locale | ${localeSummary(env)} |`);
|
|
135
|
+
lines.push(`| WSL | ${String(shell.wsl).toLowerCase() === 'yes' ? '是' : '否'} |`);
|
|
136
|
+
}
|
|
137
|
+
lines.push(`| 用户目录 | ${shell.userProfile || os.homeDir || '-'} |`);
|
|
138
|
+
lines.push(`| 临时目录 | ${shell.tempDir || os.tmpDir || '-'} |`);
|
|
94
139
|
lines.push('');
|
|
95
140
|
lines.push('## 可用工具');
|
|
96
141
|
lines.push('');
|
|
@@ -121,7 +166,7 @@ function renderEnvironment(env) {
|
|
|
121
166
|
return `${lines.join('\n')}\n`;
|
|
122
167
|
}
|
|
123
168
|
|
|
124
|
-
const
|
|
169
|
+
const COMMON_COMMANDS_POWERSHELL = [
|
|
125
170
|
'- 列出目录:`Get-ChildItem -Path . -Force`',
|
|
126
171
|
'- 递归查找文件:`Get-ChildItem -Recurse -Filter \'*.py\'`',
|
|
127
172
|
'- 文本搜索:`Select-String -Path \'**\\*.py\' -Pattern \'关键词\'`',
|
|
@@ -133,6 +178,18 @@ const COMMON_COMMANDS = [
|
|
|
133
178
|
'- 端口占用:`Get-NetTCPConnection -LocalPort <port>`',
|
|
134
179
|
];
|
|
135
180
|
|
|
181
|
+
const COMMON_COMMANDS_UNIX = [
|
|
182
|
+
'- 列出目录:`ls -la`;目录树:`find . -maxdepth 2 -type d`',
|
|
183
|
+
'- 递归查找文件:`find . -type f -name \'*.py\'`',
|
|
184
|
+
'- 文本搜索:`rg \'<pattern>\' <path>`(rg 可用时),否则 `grep -rn \'<pattern>\' <path>`',
|
|
185
|
+
'- 读文件:`cat <file>`;分页:`less <file>`',
|
|
186
|
+
'- 写文件:`cat > <file> <<\'EOF\'` 多行写入;追加:`cat >> <file> <<\'EOF\'`',
|
|
187
|
+
'- 判断路径存在:`test -e <path>`;文件判断:`[ -f <path> ]`',
|
|
188
|
+
'- 环境变量读:`echo $NAME`;写:`export NAME=\'value\'`(仅当前会话)',
|
|
189
|
+
'- 进程列表:`ps aux | grep <name>`;结束:`kill <pid>`',
|
|
190
|
+
'- 端口占用:`lsof -i :<port>`(macOS/Linux)或 `ss -tlnp | grep <port>`',
|
|
191
|
+
];
|
|
192
|
+
|
|
136
193
|
const TOOL_ORDER = [
|
|
137
194
|
'git', 'node', 'npm', 'npx', 'pnpm', 'yarn', 'bun',
|
|
138
195
|
'python', 'py', 'uv', 'pip', 'docker', 'make', 'rg', 'gh', 'pwsh', 'cargo', 'go',
|
|
@@ -222,13 +279,15 @@ const TOOL_COMMANDS = {
|
|
|
222
279
|
};
|
|
223
280
|
|
|
224
281
|
function renderCommands(env) {
|
|
282
|
+
const family = resolveFamily(env);
|
|
283
|
+
const common = family === 'unix' ? COMMON_COMMANDS_UNIX : COMMON_COMMANDS_POWERSHELL;
|
|
225
284
|
const lines = [];
|
|
226
285
|
lines.push('# 命令模板(按工具)');
|
|
227
286
|
lines.push('');
|
|
228
287
|
lines.push('> 仅列出探测到可用的工具;`<...>` 为占位参数,执行前替换。');
|
|
229
288
|
lines.push('');
|
|
230
|
-
lines.push('## 通用操作(PowerShell 原生,始终可用)');
|
|
231
|
-
for (const c of
|
|
289
|
+
lines.push(family === 'unix' ? '## 通用操作(bash/zsh)' : '## 通用操作(PowerShell 原生,始终可用)');
|
|
290
|
+
for (const c of common) lines.push(c);
|
|
232
291
|
lines.push('');
|
|
233
292
|
for (const tool of TOOL_ORDER) {
|
|
234
293
|
const info = getTool(env, tool);
|
|
@@ -239,7 +298,7 @@ function renderCommands(env) {
|
|
|
239
298
|
for (const c of cmds) lines.push(c);
|
|
240
299
|
lines.push('');
|
|
241
300
|
}
|
|
242
|
-
return
|
|
301
|
+
return lines.join('\n');
|
|
243
302
|
}
|
|
244
303
|
|
|
245
|
-
export { renderEnvironment, renderCommands };
|
|
304
|
+
export { renderEnvironment, renderCommands };
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pwsh-guide
|
|
3
|
-
description: 本项目的
|
|
3
|
+
description: 本项目的 PowerShell 命令执行指南。需要执行 shell 命令、遇到 PowerShell 报错、中文乱码或不确定命令语法时使用;含本机环境事实、命令模板与失败诊断。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# PowerShell
|
|
6
|
+
# PowerShell 操作指南(本环境)
|
|
7
7
|
|
|
8
8
|
> 由 `pwsh-guide` 生成于 {{generatedAt}}。环境变化后运行 `pwsh-guide refresh` 更新。
|
|
9
9
|
|
|
10
10
|
## 1. 执行命令前先确认
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- 默认 shell 是 `{{shellName}}`(PS {{psVersion}}),**不是 bash/cmd**:禁止 `ls -la`、`grep`、`cat`、`rm -rf`、`export`、`dir`、`copy`。
|
|
13
|
+
- OS:{{osVersion}}。{{pwsh7Note}}
|
|
13
14
|
- 控制台输出编码:{{consoleOutputEncoding}}(代码页 chcp {{chcp}})。
|
|
14
15
|
- 执行策略:{{executionPolicySummary}}。
|
|
15
16
|
- 当前目录:`{{cwd}}`;git 根:`{{gitRoot}}`。
|
|
@@ -17,7 +18,7 @@ description: 本项目的 Windows PowerShell/cmd 命令执行指南。需要执
|
|
|
17
18
|
|
|
18
19
|
## 2. 命令执行铁律
|
|
19
20
|
|
|
20
|
-
1. 一条命令一次执行;禁止 `&&`、`||`、`;` 链式(PowerShell 5.1 不支持 `&&`)。
|
|
21
|
+
1. 一条命令一次执行;禁止 `&&`、`||`、`;` 链式(Windows PowerShell 5.1 不支持 `&&`)。
|
|
21
22
|
2. 执行后检查退出状态:外部命令看 `$LASTEXITCODE`,cmdlet 看 `$?`,并查看 stderr。
|
|
22
23
|
3. 路径含空格或中文时用单引号包裹:`Get-Content 'E:\path with 中文\file.txt'`。
|
|
23
24
|
4. 环境变量读取 `$env:NAME`,写入 `$env:NAME = 'value'`(仅当前会话)。
|
|
@@ -58,4 +59,4 @@ description: 本项目的 Windows PowerShell/cmd 命令执行指南。需要执
|
|
|
58
59
|
|
|
59
60
|
## 7. 项目上下文(来自探测)
|
|
60
61
|
|
|
61
|
-
{{projectContext}}
|
|
62
|
+
{{projectContext}}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pwsh-guide
|
|
3
|
+
description: 本项目的 Unix shell(bash/zsh)命令执行指南。需要执行 shell 命令、遇到 shell 报错、编码问题或不确定命令语法时使用;含本机环境事实、命令模板与失败诊断。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Shell 操作指南(本环境)
|
|
7
|
+
|
|
8
|
+
> 由 `pwsh-guide` 生成于 {{generatedAt}}。环境变化后运行 `pwsh-guide refresh` 更新。
|
|
9
|
+
|
|
10
|
+
## 1. 执行命令前先确认
|
|
11
|
+
|
|
12
|
+
- 默认 shell 是 `{{shellName}}`,**不是 PowerShell/cmd**:禁止 `Get-ChildItem`、`Get-Content`、`Set-Content`、`dir`、`copy`、`$env:`。
|
|
13
|
+
- OS:{{osVersion}}。{{fishNote}}
|
|
14
|
+
- locale:{{localeSummary}}(非 UTF-8 时中文可能乱码)。
|
|
15
|
+
- 当前目录:`{{cwd}}`;git 根:`{{gitRoot}}`。
|
|
16
|
+
- 完整环境事实见 `references/environment.md`;可用命令模板见 `references/commands.md`。
|
|
17
|
+
|
|
18
|
+
## 2. 命令执行铁律
|
|
19
|
+
|
|
20
|
+
1. 一条命令一次执行;避免 `&&`/`||` 链式(出错时难以定位)。
|
|
21
|
+
2. 执行后检查退出码 `$?` 与 stderr。
|
|
22
|
+
3. 路径含空格时用引号包裹:`cat 'path with space/file.txt'`。
|
|
23
|
+
4. 环境变量读取 `echo $NAME`,写入 `export NAME='value'`(仅当前会话)。
|
|
24
|
+
5. 只读操作优先;删除等危险操作先展示计划再执行,`rm` 谨慎使用。
|
|
25
|
+
6. 长命令优先写成脚本文件(`.sh`)再 `bash <script>` 执行,避免内联引号地狱。
|
|
26
|
+
|
|
27
|
+
## 3. 常用命令模板
|
|
28
|
+
|
|
29
|
+
可用工具与命令见 `references/commands.md`。本机探测到的工具:
|
|
30
|
+
|
|
31
|
+
{{toolsSummary}}
|
|
32
|
+
|
|
33
|
+
## 4. 编码处理
|
|
34
|
+
|
|
35
|
+
- 确认 locale:`locale`;乱码排查:`echo $LANG` 应含 `UTF-8`。
|
|
36
|
+
- 文件编码转换:`iconv -f GBK -t UTF-8 <input> > <output>`。
|
|
37
|
+
- 终端乱码时先切 UTF-8:`export LANG=C.UTF-8`(仅当前会话)。
|
|
38
|
+
|
|
39
|
+
## 5. 失败分类诊断
|
|
40
|
+
|
|
41
|
+
| 症状 | 常见原因 | 处理 |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| command not found | PATH 缺失 / 拼写错 | `command -v <name>` 确认;查看 `references/environment.md` 工具表 |
|
|
44
|
+
| Permission denied | 无执行权限 | `chmod +x <file>`;需要 sudo 时先说明原因 |
|
|
45
|
+
| 引号/转义报错 | 嵌套引号 | 用单引号或写成脚本文件 |
|
|
46
|
+
| 中文乱码 | locale 非 UTF-8 | 按第 4 节处理 |
|
|
47
|
+
| 程序假死 | 交互式命令等待输入 | 加非交互参数,或 `< /dev/null` |
|
|
48
|
+
|
|
49
|
+
## 6. 禁止事项
|
|
50
|
+
|
|
51
|
+
- 禁止 PowerShell/cmd 语法:`Get-ChildItem`、`Get-Content`、`Set-Content`、`dir`、`copy`、`$env:`。
|
|
52
|
+
- `rm -rf` 前必须确认路径;删除操作先展示计划。
|
|
53
|
+
- 未读文件内容不得修改;不得批量格式化无关代码。
|
|
54
|
+
- 仓库 AGENTS.md 规则优先于本文件。
|
|
55
|
+
|
|
56
|
+
## 7. 项目上下文(来自探测)
|
|
57
|
+
|
|
58
|
+
{{projectContext}}
|
package/scripts/probe.ps1
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# probe.ps1 - pwsh-guide 环境探测脚本(只读,不修改任何文件或系统设置)
|
|
2
|
-
# 用法:powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File probe.ps1
|
|
3
|
-
# 输出:UTF-8 JSON 到 stdout(shell / tools / project 三部分)
|
|
4
|
-
# 兼容:Windows PowerShell 5.1+(不使用 PS7 新语法:??、&&、三元等)
|
|
5
|
-
|
|
6
|
-
$originalConsoleEncoding = [Console]::OutputEncoding.WebName
|
|
7
|
-
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
8
|
-
$ErrorActionPreference = 'Continue'
|
|
9
|
-
|
|
10
|
-
function Get-ToolInfo {
|
|
11
|
-
param([string]$Name)
|
|
12
|
-
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
|
13
|
-
if ($null -eq $cmd) { return $null }
|
|
14
|
-
$version = $null
|
|
15
|
-
try {
|
|
16
|
-
$raw = & $Name --version 2>$null | Select-Object -First 1
|
|
17
|
-
if ($null -ne $raw -and -not [string]::IsNullOrWhiteSpace([string]$raw)) { $version = [string]$raw }
|
|
18
|
-
} catch { $version = $null }
|
|
19
|
-
if ($null -eq $version -and $cmd.Source) {
|
|
20
|
-
try {
|
|
21
|
-
$v = (Get-Item $cmd.Source -ErrorAction Stop).VersionInfo.FileVersion
|
|
22
|
-
if ($null -ne $v -and -not [string]::IsNullOrWhiteSpace($v) -and $v -ne '0.0.0.0') { $version = $v }
|
|
23
|
-
} catch { $version = $null }
|
|
24
|
-
}
|
|
25
|
-
return [pscustomobject]@{ name = $Name; version = $version; source = $cmd.Source }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
$executionPolicy = @{}
|
|
29
|
-
Get-ExecutionPolicy -List | ForEach-Object { $executionPolicy[[string]$_.Scope] = [string]$_.ExecutionPolicy }
|
|
30
|
-
|
|
31
|
-
$chcp = $null
|
|
32
|
-
try {
|
|
33
|
-
$chcpText = (& chcp 2>$null | Select-Object -First 1)
|
|
34
|
-
if ($chcpText -match '(\d+)') { $chcp = [int]$Matches[1] }
|
|
35
|
-
} catch { $chcp = $null }
|
|
36
|
-
|
|
37
|
-
$toolNames = @('git','node','npm','npx','pnpm','yarn','bun','python','py','uv','pip','docker','make','rg','gh','pwsh','cargo','go')
|
|
38
|
-
$tools = @()
|
|
39
|
-
foreach ($n in $toolNames) {
|
|
40
|
-
$info = Get-ToolInfo $n
|
|
41
|
-
if ($null -ne $info) { $tools += $info }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
$cwd = (Get-Location).Path
|
|
45
|
-
|
|
46
|
-
$gitRoot = $null
|
|
47
|
-
if (Get-Command git -ErrorAction SilentlyContinue) {
|
|
48
|
-
try {
|
|
49
|
-
$line = git rev-parse --show-toplevel 2>$null | Select-Object -First 1
|
|
50
|
-
if ($line -and (Test-Path $line)) { $gitRoot = $line }
|
|
51
|
-
} catch { $gitRoot = $null }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
$markerNames = @('package.json','pyproject.toml','requirements.txt','uv.lock','go.mod','Cargo.toml','pnpm-lock.yaml','yarn.lock','Dockerfile')
|
|
55
|
-
$subdirs = @(Get-ChildItem -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notin @('.git','.venv','venv','node_modules') } | Select-Object -ExpandProperty Name)
|
|
56
|
-
$foundMarkers = @()
|
|
57
|
-
foreach ($m in $markerNames) {
|
|
58
|
-
foreach ($d in (@('.') + $subdirs)) {
|
|
59
|
-
$p = Join-Path $cwd (Join-Path $d $m)
|
|
60
|
-
if (Test-Path $p) {
|
|
61
|
-
if ($d -eq '.') { $rel = $m } else { $rel = "$d\$m" }
|
|
62
|
-
$foundMarkers += $rel
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
$venv = $null
|
|
68
|
-
foreach ($d in @('.venv','venv')) {
|
|
69
|
-
if (Test-Path (Join-Path $cwd (Join-Path $d 'pyvenv.cfg'))) { $venv = $d; break }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
$packageScripts = @{}
|
|
73
|
-
$pkgPath = Join-Path $cwd 'package.json'
|
|
74
|
-
if (Test-Path $pkgPath) {
|
|
75
|
-
try {
|
|
76
|
-
$pkg = Get-Content -Raw $pkgPath | ConvertFrom-Json
|
|
77
|
-
if ($null -ne $pkg.scripts) {
|
|
78
|
-
$pkg.scripts.PSObject.Properties | ForEach-Object { $packageScripts[$_.Name] = [string]$_.Value }
|
|
79
|
-
}
|
|
80
|
-
} catch { $packageScripts = @{} }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
$result = [pscustomobject]@{
|
|
84
|
-
shell = [pscustomobject]@{
|
|
85
|
-
psVersion = $PSVersionTable.PSVersion.ToString()
|
|
86
|
-
psEdition = $PSVersionTable.PSEdition
|
|
87
|
-
hostName = $Host.Name
|
|
88
|
-
pwsh7Available = ($null -ne (Get-Command pwsh -ErrorAction SilentlyContinue))
|
|
89
|
-
osVersion = [System.Environment]::OSVersion.VersionString
|
|
90
|
-
consoleOutputEncoding = $originalConsoleEncoding
|
|
91
|
-
outputEncoding = $OutputEncoding.WebName
|
|
92
|
-
chcp = $chcp
|
|
93
|
-
executionPolicy = $executionPolicy
|
|
94
|
-
userProfile = $env:USERPROFILE
|
|
95
|
-
tempDir = $env:TEMP
|
|
96
|
-
}
|
|
97
|
-
tools = $tools
|
|
98
|
-
project = [pscustomobject]@{
|
|
99
|
-
cwd = $cwd
|
|
100
|
-
gitRoot = $gitRoot
|
|
101
|
-
markers = $foundMarkers
|
|
102
|
-
venv = $venv
|
|
103
|
-
packageScripts = $packageScripts
|
|
104
|
-
subdirs = $subdirs
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
$result | ConvertTo-Json -Depth 5
|