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 CHANGED
@@ -1,47 +1,56 @@
1
1
  # pwsh-guide
2
2
 
3
- 生成项目级 Agent Skill:探测当前 Windows/PowerShell 环境与本项目上下文,产出可靠的命令使用指南(`SKILL.md` + `references/`),供 Codex、Claude Code 等代理自动读取,减少命令失败、乱码与反复试错。
3
+ 生成项目级 Agent Skill:跨平台探测当前环境(Windows PowerShell / macOS / Linux shell)与本项目上下文,产出可靠的命令使用指南(`SKILL.md` + `references/`),供 Codex、Claude Code 等代理自动读取,减少命令失败、乱码与反复试错。
4
4
 
5
5
  ## 原理
6
6
 
7
- AI 代理在 Windows + PowerShell 下执行命令失败,本质是"生成即采样":模型先验以 bash 为主、缺少本机环境事实。`pwsh-guide` 把环境探测结果固化进 skill 的 references(惰性加载、不占上下文),并用命令铁律 + 失败诊断约束生成空间。
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
- ```powershell
12
- npm link
13
- ```
14
-
15
- ## 用法
12
+ ## 安装与使用
16
13
 
17
- ```powershell
18
- # 在任意项目根目录运行
19
- pwsh-guide init # 探测并生成 .agents/skills/pwsh-guide/
20
- pwsh-guide refresh # 环境变化后重新探测,更新 references
21
- pwsh-guide init --dry-run # 只输出探测 JSON,不写文件
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`:探测到的本机环境事实(PS 版本/编码/执行策略/工具/项目上下文)
26
+ - `references/environment.md`:探测到的本机环境事实(shell 版本/编码/locale/工具/项目上下文)
28
27
  - `references/commands.md`:按探测到的工具动态裁剪的命令模板
29
28
 
30
29
  ## 探测内容(只读)
31
30
 
32
- - shell:PowerShell 版本/PSEdition/编码/代码页/执行策略/OS/用户与临时目录
33
- - 工具:git、node、npm、npx、pnpm、yarn、bun、python、py、uv、pip、docker、make、rg、gh、pwsh、cargo、go 的可用性与版本
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 CLI 入口(init / refresh / --dry-run)
40
- src/probe.js spawn powershell.exe 执行探测并解析 JSON
41
- src/detect.js 仓库根判定与目标目录解析
42
- src/render.js 渲染 SKILL.md / environment.md / commands.md
43
- scripts/probe.ps1 只读探测脚本(PS 5.1 兼容,输出 UTF-8 JSON)
44
- templates/SKILL.md SKILL.md 模板({{占位符}} render.js 填充)
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/`),当前变更:`add-pwsh-guide`。
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 - 生成项目级 PowerShell/cmd 操作指南 Agent Skill
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.1.0",
4
- "description": "Generate a project-level Agent Skill: probe the current Windows/PowerShell environment and produce a reliable command guide (SKILL.md + references) for agents such as Codex and Claude Code.",
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 findProjectRoot(startDir) {
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 start; // 找不到 .git,降级为当前目录
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 - 调用 scripts/probe.ps1 探测环境并解析 JSON
1
+ // probe.js - 跨平台环境主探测(Node 原生,零 shell 依赖)
2
2
  // 用法:probeEnvironment(cwd) -> Promise<object>
3
- import { spawn } from 'node:child_process';
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 PROBE_SCRIPT = path.resolve(__dirname, '..', 'scripts', 'probe.ps1');
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
- return new Promise((resolve, reject) => {
12
- const args = [
13
- '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
14
- '-File', PROBE_SCRIPT,
15
- ];
16
- const child = spawn('powershell.exe', args, { cwd, shell: false, windowsHide: true });
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) => reject(new Error(`无法启动 powershell.exe: ${err.message}`)));
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
- reject(new Error(`probe.ps1 退出码 ${code}: ${stderr.trim()}`));
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
- reject(new Error(`探测输出不含 JSON:\n${stdout}\n${stderr}`));
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
- reject(new Error(`解析探测 JSON 失败: ${err.message}`));
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 TEMPLATE = fs.readFileSync(path.resolve(__dirname, '..', 'templates', 'SKILL.md'), 'utf8');
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
- for (const [key, value] of Object.entries(values)) {
13
- out = out.split(`{{${key}}}`).join(String(value));
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
- return fill(TEMPLATE, {
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
- lines.push(`| PowerShell 版本 | ${shell.psVersion ?? '-'} |`);
84
- lines.push(`| PSEdition | ${shell.psEdition ?? '-'} |`);
85
- lines.push(`| 主机 | ${shell.hostName ?? '-'} |`);
86
- lines.push(`| pwsh 7 可用 | ${shell.pwsh7Available ? '' : '否'} |`);
87
- lines.push(`| OS | ${shell.osVersion ?? '-'} |`);
88
- lines.push(`| 控制台输出编码 | ${shell.consoleOutputEncoding ?? '-'} |`);
89
- lines.push(`| $OutputEncoding | ${shell.outputEncoding ?? '-'} |`);
90
- lines.push(`| 代码页 chcp | ${shell.chcp ?? '-'} |`);
91
- lines.push(`| 执行策略 | ${executionPolicySummary(env)} |`);
92
- lines.push(`| 用户目录 | ${shell.userProfile ?? '-'} |`);
93
- lines.push(`| 临时目录 | ${shell.tempDir ?? '-'} |`);
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 COMMON_COMMANDS = [
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 COMMON_COMMANDS) lines.push(c);
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 `${lines.join('\n')}`;
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: 本项目的 Windows PowerShell/cmd 命令执行指南。需要执行 shell 命令、遇到 PowerShell 报错、中文乱码或不确定命令语法时使用;含本机环境事实、命令模板与失败诊断。
3
+ description: 本项目的 PowerShell 命令执行指南。需要执行 shell 命令、遇到 PowerShell 报错、中文乱码或不确定命令语法时使用;含本机环境事实、命令模板与失败诊断。
4
4
  ---
5
5
 
6
- # PowerShell/cmd 操作指南(本环境)
6
+ # PowerShell 操作指南(本环境)
7
7
 
8
8
  > 由 `pwsh-guide` 生成于 {{generatedAt}}。环境变化后运行 `pwsh-guide refresh` 更新。
9
9
 
10
10
  ## 1. 执行命令前先确认
11
11
 
12
- - Shell 是 `powershell.exe`(PS {{psVersion}}),**不是 bash**:禁止 `ls -la`、`grep`、`cat`、`rm -rf`、`export`。
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