pwsh-guide 0.2.2 → 0.4.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 +33 -16
- package/bin/pwsh-guide.js +52 -22
- package/package.json +1 -1
- package/scripts/probe.windows.ps1 +2 -1
- package/src/detect.js +12 -3
- package/src/meta.js +126 -0
- package/src/probe.js +115 -28
- package/src/render.js +139 -8
- package/templates/SKILL.powershell.md +37 -13
- package/templates/SKILL.pwsh.md +91 -0
- package/templates/SKILL.unix.md +25 -8
package/README.md
CHANGED
|
@@ -1,58 +1,75 @@
|
|
|
1
1
|
# pwsh-guide
|
|
2
2
|
|
|
3
|
-
生成项目级 Agent Skill:跨平台探测当前环境(Windows PowerShell / macOS / Linux shell)与本项目上下文,产出可靠的命令使用指南(`SKILL.md` + `references/`),供 Codex
|
|
3
|
+
生成项目级 Agent Skill:跨平台探测当前环境(Windows PowerShell / macOS / Linux shell)与本项目上下文,产出可靠的命令使用指南(`SKILL.md` + `references/`),供 Codex(`.agents/skills/`)与 Claude Code(`.claude/skills/`)自动读取,减少命令失败、乱码与反复试错。
|
|
4
4
|
|
|
5
5
|
## 原理
|
|
6
6
|
|
|
7
7
|
AI 代理执行命令失败,本质是"生成即采样":模型先验以 bash 为主、缺少本机环境事实。`pwsh-guide` 把环境探测结果固化进 skill 的 references(惰性加载、不占上下文),并用命令铁律 + 失败诊断约束生成空间。
|
|
8
8
|
|
|
9
|
+
v0.3 起补上"验证闭环":`init`/`refresh` 生成快照(`.meta.json`)→ `check` 检测环境漂移 → SKILL.md 内嵌快速自检指令;环境变化后 AI 先 refresh 再继续,不再用过期的指南。
|
|
10
|
+
|
|
11
|
+
v0.4 起按"默认 shell 变体"渲染:Windows 上区分 `pwsh`(PowerShell 7)与 `powershell.exe`(Windows PowerShell 5.1)——pwsh 指南默认 UTF-8、无需输入侧编码 hack,并包含 `-Parallel`/`??`/三元等特性;5.1 指南保留输入侧 hack。两个实测参数陷阱(内联 splatting、特殊字符内联拼接)已固化进两变体模板。
|
|
12
|
+
|
|
9
13
|
- Windows 下生成 PowerShell 指南;macOS/Linux 下生成 bash/zsh 指南(只生成当前环境可用内容,省 token)
|
|
10
14
|
- 平台无关探测全部用 Node 原生实现(`os` / `process.env` / `fs`),不依赖特定 shell
|
|
15
|
+
- 工具版本并行探测;项目上下文以 git 根为基准,并按 lock 文件推断推荐包管理器与项目命令
|
|
11
16
|
|
|
12
17
|
## 安装与使用
|
|
13
18
|
|
|
14
19
|
```bash
|
|
15
20
|
# 项目根目录运行(npx 方式,无需全局安装)
|
|
16
21
|
npx pwsh-guide init
|
|
17
|
-
#
|
|
22
|
+
# 环境变化后更新(重新生成 SKILL.md 与 references,两份输出)
|
|
18
23
|
npx pwsh-guide refresh
|
|
19
|
-
#
|
|
24
|
+
# 检测环境漂移(退出码 0=一致,1=有漂移,2=无快照)
|
|
25
|
+
npx pwsh-guide check
|
|
26
|
+
# 只输出探测 JSON,不写任何文件(stdout 为纯 JSON)
|
|
20
27
|
npx pwsh-guide init --dry-run
|
|
21
28
|
# 查看版本
|
|
22
29
|
npx pwsh-guide --version
|
|
23
30
|
```
|
|
24
31
|
|
|
25
|
-
生成的 skill
|
|
32
|
+
生成的 skill 有两份(内容一致,分别被不同代理发现):
|
|
33
|
+
|
|
34
|
+
- `.agents/skills/pwsh-guide/`(Codex)
|
|
35
|
+
- `.claude/skills/pwsh-guide/`(Claude Code)
|
|
36
|
+
|
|
37
|
+
每份包含:
|
|
26
38
|
|
|
27
|
-
- `SKILL.md
|
|
39
|
+
- `SKILL.md`:命令执行铁律、输入/输出编码处理、快速自检、代码库检索指引、失败诊断、项目命令
|
|
28
40
|
- `references/environment.md`:探测到的本机环境事实(shell 版本/编码/locale/工具/项目上下文)
|
|
29
41
|
- `references/commands.md`:按探测到的工具动态裁剪的命令模板
|
|
42
|
+
- `.meta.json`:生成快照(`check` 漂移检测的基准)
|
|
30
43
|
|
|
31
44
|
## 探测内容(只读)
|
|
32
45
|
|
|
33
46
|
- 平台与 OS:`os.platform()` / `os.release()` / `os.arch()` / 主机名 / 用户与临时目录
|
|
34
47
|
- shell(按平台深度探测):
|
|
35
|
-
- Windows:PowerShell 版本 / PSEdition / 控制台输出编码 / `$OutputEncoding` / chcp / 执行策略 / pwsh 7
|
|
48
|
+
- Windows:PowerShell 版本 / PSEdition / shell 变体(`pwsh` 或 `powershell`,按 `$env:SHELL` 判定,可用 `PWSH_GUIDE_SHELL=pwsh` 强制)/ 控制台输出编码 / `$OutputEncoding` / chcp / 执行策略 / pwsh 7 可用性;powershell.exe 探测失败时自动用 pwsh 重试
|
|
36
49
|
- macOS/Linux:默认 shell(`$SHELL`)/ 可用 shell(bash / zsh / fish)/ locale(LANG / LC_ALL / LC_CTYPE)/ WSL 标注
|
|
37
|
-
- 工具:git、node、npm、npx、pnpm、yarn、bun、python、py、uv、pip、docker、make、rg、gh、pwsh、cargo、go 的可用性与版本(PATH 遍历 + `--version
|
|
38
|
-
-
|
|
50
|
+
- 工具:git、node、npm、npx、pnpm、yarn、bun、python、py、uv、pip、docker、make、rg、gh、pwsh、cargo、go 的可用性与版本(PATH 遍历 + `--version`,并行探测,超时/缺失记为 null)
|
|
51
|
+
- 项目(以 git 根为基准):标记文件、venv、package.json scripts、推荐包管理器(lock 文件推断)、README / Makefile 目标 / Docker Compose、项目根第一层子目录
|
|
39
52
|
|
|
40
53
|
## 开发
|
|
41
54
|
|
|
42
55
|
```text
|
|
43
|
-
bin/pwsh-guide.js CLI 入口(init / refresh / --dry-run)
|
|
44
|
-
src/probe.js 跨平台主探测(Node
|
|
45
|
-
src/detect.js
|
|
46
|
-
src/render.js 按 shell
|
|
56
|
+
bin/pwsh-guide.js CLI 入口(init / refresh / check / --dry-run)
|
|
57
|
+
src/probe.js 跨平台主探测(Node 原生;并行工具探测;项目根基准)
|
|
58
|
+
src/detect.js 仓库根判定与双输出目录解析
|
|
59
|
+
src/render.js 按 shell 族与变体渲染 SKILL.md / environment.md / commands.md
|
|
60
|
+
src/meta.js 生成快照与漂移对比(check 用)
|
|
47
61
|
scripts/probe.windows.ps1 Windows 深度探测(PS 5.1 兼容,输出 UTF-8 JSON)
|
|
48
62
|
scripts/probe.unix.sh Unix 深度探测(POSIX sh 兼容,输出 UTF-8 JSON)
|
|
49
|
-
templates/SKILL.powershell.md PowerShell
|
|
63
|
+
templates/SKILL.powershell.md PowerShell 5.1 变体 SKILL.md 模板({{占位符}} 由 render.js 填充)
|
|
64
|
+
templates/SKILL.pwsh.md pwsh 7 变体 SKILL.md 模板
|
|
50
65
|
templates/SKILL.unix.md Unix 族 SKILL.md 模板
|
|
66
|
+
test/probe.test.js 单元测试(node --test)
|
|
67
|
+
.github/workflows/ci.yml 三平台(windows/ubuntu/macos)测试与集成验证
|
|
51
68
|
```
|
|
52
69
|
|
|
53
|
-
变更记录由 OpenSpec 管理(`openspec
|
|
70
|
+
变更记录由 OpenSpec 管理(`openspec/`)。
|
|
54
71
|
|
|
55
72
|
## 验证范围
|
|
56
73
|
|
|
57
|
-
-
|
|
58
|
-
-
|
|
74
|
+
- CI:windows-latest / ubuntu-latest / macos-latest 三平台自动运行单元测试与 `init`/`check` 集成验证
|
|
75
|
+
- Windows:本机实测(探测 JSON / init / refresh / check)
|
package/bin/pwsh-guide.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// pwsh-guide CLI 入口:init / refresh / --dry-run
|
|
3
|
-
// 用法:pwsh-guide init [--dry-run] | pwsh-guide refresh | pwsh-guide help
|
|
2
|
+
// pwsh-guide CLI 入口:init / refresh / check / --dry-run
|
|
3
|
+
// 用法:pwsh-guide init [--dry-run] | pwsh-guide refresh | pwsh-guide check | pwsh-guide help
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { probeEnvironment } from '../src/probe.js';
|
|
9
|
-
import {
|
|
9
|
+
import { resolveSkillDirs } from '../src/detect.js';
|
|
10
10
|
import { renderSkill, renderEnvironment, renderCommands } from '../src/render.js';
|
|
11
|
+
import { writeMeta, readMeta, compareEnv } from '../src/meta.js';
|
|
11
12
|
|
|
12
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -15,8 +16,9 @@ const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.js
|
|
|
15
16
|
const USAGE = `pwsh-guide - 生成项目级 Shell 操作指南 Agent Skill(跨平台:Windows PowerShell / macOS / Linux)
|
|
16
17
|
|
|
17
18
|
用法:
|
|
18
|
-
pwsh-guide init 探测环境并在 .agents/skills/
|
|
19
|
-
pwsh-guide refresh 重新探测并重新生成 SKILL.md 与 references
|
|
19
|
+
pwsh-guide init 探测环境并在 .agents/skills/ 与 .claude/skills/ 生成 skill
|
|
20
|
+
pwsh-guide refresh 重新探测并重新生成 SKILL.md 与 references(两份)
|
|
21
|
+
pwsh-guide check 重新探测并与上次生成快照对比,报告环境漂移(退出码 0=一致,1=有漂移,2=无快照)
|
|
20
22
|
pwsh-guide init --dry-run 仅输出探测结果,不写任何文件
|
|
21
23
|
|
|
22
24
|
选项:
|
|
@@ -45,16 +47,22 @@ async function main() {
|
|
|
45
47
|
console.log(USAGE);
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
48
|
-
if (command !== 'init' && command !== 'refresh') {
|
|
50
|
+
if (command !== 'init' && command !== 'refresh' && command !== 'check') {
|
|
49
51
|
console.error(`[pwsh-guide] 未知命令: ${command}\n`);
|
|
50
52
|
console.error(USAGE);
|
|
51
53
|
process.exit(1);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
const cwd = process.cwd();
|
|
57
|
+
|
|
58
|
+
if (command === 'check') {
|
|
59
|
+
await runCheck(cwd);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
const dryRun = values['dry-run'];
|
|
56
64
|
|
|
57
|
-
console.
|
|
65
|
+
console.error('[pwsh-guide] 正在探测环境(只读,不修改任何文件)...');
|
|
58
66
|
const env = await probeEnvironment(cwd);
|
|
59
67
|
|
|
60
68
|
if (dryRun) {
|
|
@@ -62,27 +70,49 @@ async function main() {
|
|
|
62
70
|
return;
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
if (command === 'init' &&
|
|
68
|
-
console.error(`[pwsh-guide] ${
|
|
73
|
+
const skillDirs = resolveSkillDirs(cwd);
|
|
74
|
+
const existing = skillDirs.filter((dir) => fs.existsSync(dir));
|
|
75
|
+
if (command === 'init' && existing.length > 0) {
|
|
76
|
+
console.error(`[pwsh-guide] ${existing.map((dir) => path.relative(cwd, dir)).join('、')} 已存在,请改用 "pwsh-guide refresh" 更新,或先删除该目录后再 init。`);
|
|
69
77
|
process.exit(1);
|
|
70
78
|
}
|
|
71
|
-
if (command === 'refresh' &&
|
|
72
|
-
console.error(`[pwsh-guide] ${
|
|
79
|
+
if (command === 'refresh' && existing.length === 0) {
|
|
80
|
+
console.error(`[pwsh-guide] 未找到 ${skillDirs.map((dir) => path.relative(cwd, dir)).join('、')},请先运行 "pwsh-guide init"。`);
|
|
73
81
|
process.exit(1);
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
const
|
|
77
|
-
|
|
84
|
+
for (const dir of skillDirs) {
|
|
85
|
+
const refsDir = path.join(dir, 'references');
|
|
86
|
+
fs.mkdirSync(refsDir, { recursive: true });
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
console.log('\n完成。新会话中 Codex/Claude Code
|
|
88
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), renderSkill(env), 'utf8');
|
|
89
|
+
fs.writeFileSync(path.join(refsDir, 'environment.md'), renderEnvironment(env), 'utf8');
|
|
90
|
+
fs.writeFileSync(path.join(refsDir, 'commands.md'), renderCommands(env), 'utf8');
|
|
91
|
+
writeMeta(dir, env, pkg.version);
|
|
92
|
+
console.log(`[pwsh-guide] 已更新 ${dir}`);
|
|
93
|
+
}
|
|
94
|
+
console.log('\n完成。新会话中 Codex(.agents/skills/)与 Claude Code(.claude/skills/)会自动发现该 skill。');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function runCheck(cwd) {
|
|
98
|
+
const skillDirs = resolveSkillDirs(cwd);
|
|
99
|
+
const withMeta = skillDirs.filter((dir) => readMeta(dir));
|
|
100
|
+
if (withMeta.length === 0) {
|
|
101
|
+
console.error('[pwsh-guide] 未找到 .meta.json 快照,请先运行 "pwsh-guide init" 或 "pwsh-guide refresh"。');
|
|
102
|
+
process.exit(2);
|
|
103
|
+
}
|
|
104
|
+
console.error('[pwsh-guide] 正在重新探测(只读)...');
|
|
105
|
+
const env = await probeEnvironment(cwd);
|
|
106
|
+
const meta = readMeta(withMeta[0]);
|
|
107
|
+
const drift = compareEnv(env, meta);
|
|
108
|
+
if (drift.length === 0) {
|
|
109
|
+
console.log(`[pwsh-guide] check:无漂移(快照 ${meta.generatedBy} @ ${meta.generatedAt})。`);
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
console.log(`[pwsh-guide] check:发现 ${drift.length} 项漂移(相对 ${meta.generatedBy} @ ${meta.generatedAt}):`);
|
|
113
|
+
for (const item of drift) console.log(` - ${item}`);
|
|
114
|
+
console.log('运行 "pwsh-guide refresh" 更新 skill 后重读。');
|
|
115
|
+
process.exit(1);
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
main().catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pwsh-guide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
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": {
|
|
@@ -17,11 +17,12 @@ try {
|
|
|
17
17
|
} catch { $chcp = $null }
|
|
18
18
|
|
|
19
19
|
$result = [pscustomobject]@{
|
|
20
|
-
defaultShell = 'powershell.exe'
|
|
20
|
+
defaultShell = if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh' } else { 'powershell.exe' }
|
|
21
21
|
psVersion = $PSVersionTable.PSVersion.ToString()
|
|
22
22
|
psEdition = $PSVersionTable.PSEdition
|
|
23
23
|
hostName = $Host.Name
|
|
24
24
|
pwsh7Available = ($null -ne (Get-Command pwsh -ErrorAction SilentlyContinue))
|
|
25
|
+
ps51Available = ($null -ne (Get-Command powershell.exe -ErrorAction SilentlyContinue))
|
|
25
26
|
osVersion = [System.Environment]::OSVersion.VersionString
|
|
26
27
|
consoleOutputEncoding = $originalConsoleEncoding
|
|
27
28
|
outputEncoding = $OutputEncoding.WebName
|
package/src/detect.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// detect.js - 仓库根判定与 skill 目标目录解析
|
|
2
|
-
// 用法:findGitRoot(cwd) -> string|null;findProjectRoot(cwd) -> string;
|
|
2
|
+
// 用法:findGitRoot(cwd) -> string|null;findProjectRoot(cwd) -> string;resolveSkillDirs(cwd) -> string[]
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
@@ -18,6 +18,15 @@ export function findProjectRoot(startDir) {
|
|
|
18
18
|
return findGitRoot(startDir) ?? path.resolve(startDir); // 找不到 .git,降级为当前目录
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// 双输出:Codex 读取 .agents/skills/,Claude Code 读取 .claude/skills/
|
|
22
|
+
export function resolveSkillDirs(cwd) {
|
|
23
|
+
const root = findProjectRoot(cwd);
|
|
24
|
+
return [
|
|
25
|
+
path.join(root, '.agents', 'skills', 'pwsh-guide'),
|
|
26
|
+
path.join(root, '.claude', 'skills', 'pwsh-guide'),
|
|
27
|
+
];
|
|
23
28
|
}
|
|
29
|
+
|
|
30
|
+
export function resolveSkillDir(cwd) {
|
|
31
|
+
return resolveSkillDirs(cwd)[0];
|
|
32
|
+
}
|
package/src/meta.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// meta.js - 生成快照与漂移对比
|
|
2
|
+
// 用法:writeMeta(dir, env, generatorVersion);readMeta(dir);compareEnv(env, meta) -> string[]
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const META_SCHEMA_VERSION = 1;
|
|
7
|
+
export const META_FILENAME = '.meta.json';
|
|
8
|
+
|
|
9
|
+
// 快照只取"信号"字段,排除 cwd、hostname 等易变噪音,避免 check 误报
|
|
10
|
+
export function buildMeta(env, generatorVersion) {
|
|
11
|
+
const tools = {};
|
|
12
|
+
for (const t of env.tools || []) tools[t.name] = t.version;
|
|
13
|
+
const shell = env.shell || {};
|
|
14
|
+
const project = env.project || {};
|
|
15
|
+
const pkgScripts = project.packageScripts || {};
|
|
16
|
+
const cwdScripts = project.cwdPackageScripts || {};
|
|
17
|
+
return {
|
|
18
|
+
schemaVersion: META_SCHEMA_VERSION,
|
|
19
|
+
generatedBy: generatorVersion,
|
|
20
|
+
generatedAt: new Date().toISOString(),
|
|
21
|
+
platform: env.platform,
|
|
22
|
+
family: env.family,
|
|
23
|
+
osRelease: (env.os && env.os.release) || null,
|
|
24
|
+
arch: (env.os && env.os.arch) || null,
|
|
25
|
+
shell: {
|
|
26
|
+
defaultShell: shell.defaultShell || null,
|
|
27
|
+
variant: shell.variant || null,
|
|
28
|
+
psVersion: shell.psVersion || null,
|
|
29
|
+
psEdition: shell.psEdition || null,
|
|
30
|
+
chcp: shell.chcp ?? null,
|
|
31
|
+
consoleOutputEncoding: shell.consoleOutputEncoding || null,
|
|
32
|
+
outputEncoding: shell.outputEncoding || null,
|
|
33
|
+
locale: (shell.locale && { ...shell.locale }) || null,
|
|
34
|
+
},
|
|
35
|
+
tools,
|
|
36
|
+
project: {
|
|
37
|
+
gitRoot: project.gitRoot || null,
|
|
38
|
+
markers: (project.markers || []).slice().sort(),
|
|
39
|
+
venv: project.venv || null,
|
|
40
|
+
packageManager: project.packageManager || null,
|
|
41
|
+
packageScripts: Object.keys(pkgScripts).length ? { ...pkgScripts } : null,
|
|
42
|
+
cwdPackageScripts: Object.keys(cwdScripts).length ? { ...cwdScripts } : null,
|
|
43
|
+
makefileTargets: (project.makefile && project.makefile.targets) || [],
|
|
44
|
+
compose: (project.compose && project.compose.file) || null,
|
|
45
|
+
readme: (project.readme && project.readme.file) || null,
|
|
46
|
+
subdirs: (project.subdirs || []).slice().sort(),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writeMeta(dir, env, generatorVersion) {
|
|
52
|
+
const meta = buildMeta(env, generatorVersion);
|
|
53
|
+
fs.writeFileSync(path.join(dir, META_FILENAME), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
|
|
54
|
+
return meta;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readMeta(dir) {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(fs.readFileSync(path.join(dir, META_FILENAME), 'utf8'));
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function eq(a, b) {
|
|
66
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fmt(value) {
|
|
70
|
+
if (value === null || value === undefined) return '无';
|
|
71
|
+
if (Array.isArray(value)) return value.length ? value.join(', ') : '无';
|
|
72
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
73
|
+
return String(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 对比当前环境与快照,返回差异项数组(空数组 = 无漂移)
|
|
77
|
+
export function compareEnv(env, meta) {
|
|
78
|
+
const drift = [];
|
|
79
|
+
const shell = env.shell || {};
|
|
80
|
+
const project = env.project || {};
|
|
81
|
+
const tools = {};
|
|
82
|
+
for (const t of env.tools || []) tools[t.name] = t.version;
|
|
83
|
+
|
|
84
|
+
const expectShell = meta.shell || {};
|
|
85
|
+
const shellChecks = [
|
|
86
|
+
['平台', env.platform, meta.platform],
|
|
87
|
+
['OS 版本', (env.os && env.os.release) || null, meta.osRelease || null],
|
|
88
|
+
['架构', (env.os && env.os.arch) || null, meta.arch || null],
|
|
89
|
+
['默认 shell', shell.defaultShell || null, expectShell.defaultShell || null],
|
|
90
|
+
['shell 变体', shell.variant || null, expectShell.variant || null],
|
|
91
|
+
['PowerShell 版本', shell.psVersion || null, expectShell.psVersion || null],
|
|
92
|
+
['chcp', shell.chcp ?? null, expectShell.chcp ?? null],
|
|
93
|
+
['控制台输出编码', shell.consoleOutputEncoding || null, expectShell.consoleOutputEncoding || null],
|
|
94
|
+
['$OutputEncoding', shell.outputEncoding || null, expectShell.outputEncoding || null],
|
|
95
|
+
];
|
|
96
|
+
for (const [label, cur, old] of shellChecks) {
|
|
97
|
+
if (!eq(cur, old)) drift.push(`${label}: ${fmt(old)} → ${fmt(cur)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const curLocale = shell.locale || {};
|
|
101
|
+
const oldLocale = expectShell.locale || {};
|
|
102
|
+
if (!eq(curLocale, oldLocale)) drift.push(`locale: ${fmt(oldLocale)} → ${fmt(curLocale)}`);
|
|
103
|
+
|
|
104
|
+
const toolNames = new Set([...Object.keys(meta.tools || {}), ...Object.keys(tools)]);
|
|
105
|
+
for (const name of [...toolNames].sort()) {
|
|
106
|
+
const oldVersion = (meta.tools || {})[name] || null;
|
|
107
|
+
const curVersion = tools[name] || null;
|
|
108
|
+
if (!eq(curVersion, oldVersion)) drift.push(`工具 ${name}: ${fmt(oldVersion)} → ${fmt(curVersion)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const oldProject = meta.project || {};
|
|
112
|
+
const projectChecks = [
|
|
113
|
+
['git 根', project.gitRoot || null, oldProject.gitRoot || null],
|
|
114
|
+
['标记文件', (project.markers || []).slice().sort(), (oldProject.markers || []).slice().sort()],
|
|
115
|
+
['venv', project.venv || null, oldProject.venv || null],
|
|
116
|
+
['包管理器', project.packageManager || null, oldProject.packageManager || null],
|
|
117
|
+
['package.json scripts', Object.keys(project.packageScripts || {}).length ? project.packageScripts : null, oldProject.packageScripts || null],
|
|
118
|
+
['Makefile 目标', (project.makefile && project.makefile.targets) || [], oldProject.makefileTargets || []],
|
|
119
|
+
['Docker Compose', (project.compose && project.compose.file) || null, oldProject.compose || null],
|
|
120
|
+
];
|
|
121
|
+
for (const [label, cur, old] of projectChecks) {
|
|
122
|
+
if (!eq(cur, old)) drift.push(`${label}: ${fmt(old)} → ${fmt(cur)}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return drift;
|
|
126
|
+
}
|
package/src/probe.js
CHANGED
|
@@ -7,7 +7,7 @@ import fs from 'node:fs';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { findGitRoot } from './detect.js';
|
|
10
|
+
import { findGitRoot, findProjectRoot } from './detect.js';
|
|
11
11
|
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const SCRIPTS_DIR = path.resolve(__dirname, '..', 'scripts');
|
|
@@ -27,6 +27,23 @@ const MARKER_NAMES = [
|
|
|
27
27
|
'go.mod', 'Cargo.toml', 'pnpm-lock.yaml', 'yarn.lock', 'Dockerfile',
|
|
28
28
|
];
|
|
29
29
|
|
|
30
|
+
// lock 文件 -> 推荐包管理器(优先级从高到低,多锁文件时取第一个命中的)
|
|
31
|
+
const LOCK_PRIORITY = [
|
|
32
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
33
|
+
['bun.lockb', 'bun'],
|
|
34
|
+
['bun.lock', 'bun'],
|
|
35
|
+
['yarn.lock', 'yarn'],
|
|
36
|
+
['package-lock.json', 'npm'],
|
|
37
|
+
['uv.lock', 'uv'],
|
|
38
|
+
['poetry.lock', 'poetry'],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const README_NAMES = ['README.md', 'readme.md', 'README', 'Readme.md'];
|
|
42
|
+
const COMPOSE_NAMES = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml'];
|
|
43
|
+
const MAKEFILE_NAMES = ['Makefile', 'makefile', 'GNUmakefile'];
|
|
44
|
+
const MAKEFILE_PSEUDO = new Set(['.PHONY', '.DEFAULT', '.SUFFIXES', '.EXPORT', '.ONESHELL', '.SILENT', '.NOTPARALLEL']);
|
|
45
|
+
const MAKEFILE_MAX_TARGETS = 20;
|
|
46
|
+
|
|
30
47
|
const IGNORED_SUBDIRS = new Set(['.git', '.venv', 'venv', 'node_modules']);
|
|
31
48
|
const VERSION_TIMEOUT_MS = 5000;
|
|
32
49
|
|
|
@@ -41,7 +58,7 @@ const VERSION_ERROR_PATTERNS = [
|
|
|
41
58
|
/找不到/,
|
|
42
59
|
];
|
|
43
60
|
|
|
44
|
-
function isPlausibleVersion(text) {
|
|
61
|
+
export function isPlausibleVersion(text) {
|
|
45
62
|
if (!text) return false;
|
|
46
63
|
return !VERSION_ERROR_PATTERNS.some((re) => re.test(text));
|
|
47
64
|
}
|
|
@@ -75,6 +92,7 @@ export async function probeEnvironment(cwd) {
|
|
|
75
92
|
defaultShell,
|
|
76
93
|
defaultShellName: defaultShell.split(/[\\/]/).pop().toLowerCase(),
|
|
77
94
|
...deepShell,
|
|
95
|
+
variant: resolveShellVariant(platform, process.env, deepShell),
|
|
78
96
|
},
|
|
79
97
|
tools,
|
|
80
98
|
project,
|
|
@@ -86,14 +104,34 @@ function resolveDefaultShell(platform, deepShell) {
|
|
|
86
104
|
if (platform === 'win32') return 'powershell.exe';
|
|
87
105
|
return process.env.SHELL || '/bin/sh';
|
|
88
106
|
}
|
|
107
|
+
// Windows shell 变体判定(可测纯函数):'pwsh' | 'powershell' | null(非 Windows)
|
|
108
|
+
// 优先级:PWSH_GUIDE_SHELL 覆盖 > $env:SHELL 指向 > 深度探测实际运行 shell > 默认 powershell
|
|
109
|
+
export function resolveShellVariant(platform, env, deepShell) {
|
|
110
|
+
if (platform !== 'win32') return null;
|
|
111
|
+
const override = String(env.PWSH_GUIDE_SHELL || '').trim().toLowerCase();
|
|
112
|
+
if (override) return override.includes('pwsh') ? 'pwsh' : 'powershell';
|
|
113
|
+
const shellPath = String(env.SHELL || '').replace(/\\/g, '/').toLowerCase();
|
|
114
|
+
if (shellPath && /(^|\/)pwsh(\.exe)?$/.test(shellPath)) return 'pwsh';
|
|
115
|
+
if (shellPath && /(^|\/)powershell(\.exe)?$/.test(shellPath)) return 'powershell';
|
|
116
|
+
const deep = String((deepShell && deepShell.defaultShell) || '').toLowerCase();
|
|
117
|
+
if (deep.includes('pwsh')) return 'pwsh';
|
|
118
|
+
return 'powershell';
|
|
119
|
+
}
|
|
89
120
|
|
|
90
121
|
// 分平台深度探测:失败时降级为空对象,不中断主探测
|
|
91
122
|
function runDeepProbe(platform, cwd) {
|
|
92
123
|
const script = DEEP_SCRIPTS[platform] || DEEP_SCRIPTS.unix;
|
|
93
|
-
if (platform
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
if (platform !== 'win32') return runDeepScript('/bin/sh', [script], cwd);
|
|
125
|
+
const args = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', script];
|
|
126
|
+
const pwshPath = findToolPath('pwsh');
|
|
127
|
+
const preferred = resolveShellVariant(platform, process.env, {}) === 'pwsh' && pwshPath ? pwshPath : 'powershell.exe';
|
|
128
|
+
return runDeepScript(preferred, args, cwd).then((result) => {
|
|
129
|
+
if (Object.keys(result).length > 0) return result;
|
|
130
|
+
// 首选失败:若有 pwsh 且首选不是 pwsh,用 pwsh 重试一次;仍失败则降级
|
|
131
|
+
if (!pwshPath || preferred === pwshPath) return result;
|
|
132
|
+
console.warn('[pwsh-guide] powershell.exe 深度探测失败,改用 pwsh 重试...');
|
|
133
|
+
return runDeepScript(pwshPath, args, cwd);
|
|
134
|
+
});
|
|
97
135
|
}
|
|
98
136
|
|
|
99
137
|
function runDeepScript(file, args, cwd) {
|
|
@@ -131,20 +169,22 @@ function runDeepScript(file, args, cwd) {
|
|
|
131
169
|
});
|
|
132
170
|
}
|
|
133
171
|
|
|
134
|
-
// 工具探测:PATH 遍历 +
|
|
172
|
+
// 工具探测:PATH 遍历 + 版本命令,全部容错;版本探测并行执行,整体耗时受单工具超时上限约束
|
|
135
173
|
async function probeTools() {
|
|
136
|
-
const
|
|
174
|
+
const paths = new Map();
|
|
137
175
|
for (const name of TOOL_NAMES) {
|
|
138
176
|
const toolPath = findToolPath(name);
|
|
139
|
-
if (
|
|
140
|
-
results.push({ name, version: null, source: null });
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const rawVersion = await runVersionCommand(toolPath);
|
|
144
|
-
const version = isPlausibleVersion(rawVersion) ? rawVersion : null;
|
|
145
|
-
results.push({ name, version, source: toolPath });
|
|
177
|
+
if (toolPath) paths.set(name, toolPath);
|
|
146
178
|
}
|
|
147
|
-
|
|
179
|
+
const entries = await Promise.all(
|
|
180
|
+
[...paths.entries()].map(async ([name, toolPath]) => {
|
|
181
|
+
const rawVersion = await runVersionCommand(toolPath);
|
|
182
|
+
return { name, version: isPlausibleVersion(rawVersion) ? rawVersion : null, source: toolPath };
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
186
|
+
// 保持 TOOL_NAMES 顺序输出,缺失工具 version 为 null
|
|
187
|
+
return TOOL_NAMES.map((name) => byName.get(name) ?? { name, version: null, source: null });
|
|
148
188
|
}
|
|
149
189
|
|
|
150
190
|
function findToolPath(name) {
|
|
@@ -196,16 +236,30 @@ function runVersionCommand(toolPath) {
|
|
|
196
236
|
});
|
|
197
237
|
}
|
|
198
238
|
|
|
199
|
-
// 项目上下文:全部 Node fs
|
|
200
|
-
|
|
239
|
+
// 项目上下文:全部 Node fs 实现;以项目根(git 根 ?? 当前目录)为基准
|
|
240
|
+
// 可选参数 projectRoot 仅供测试注入项目根
|
|
241
|
+
export function probeProject(cwd, projectRoot) {
|
|
242
|
+
const root = projectRoot ?? findProjectRoot(cwd);
|
|
243
|
+
const rootSubdirs = readSubdirs(root);
|
|
201
244
|
const subdirs = readSubdirs(cwd);
|
|
245
|
+
const venv = findVenv(root) ?? findVenv(cwd);
|
|
246
|
+
const packageScripts = readPackageScripts(root);
|
|
247
|
+
const cwdScripts = readPackageScripts(cwd);
|
|
248
|
+
const cwdHasOwn = cwd !== root && Object.keys(cwdScripts).length > 0;
|
|
249
|
+
const makefile = readMakefile(root);
|
|
202
250
|
return {
|
|
203
251
|
cwd,
|
|
252
|
+
projectRoot: root,
|
|
204
253
|
gitRoot: findGitRoot(cwd),
|
|
205
|
-
markers: findMarkers(
|
|
206
|
-
venv
|
|
207
|
-
packageScripts
|
|
208
|
-
|
|
254
|
+
markers: findMarkers(root, rootSubdirs),
|
|
255
|
+
venv,
|
|
256
|
+
packageScripts,
|
|
257
|
+
cwdPackageScripts: cwdHasOwn ? cwdScripts : {},
|
|
258
|
+
packageManager: inferPackageManager(root),
|
|
259
|
+
makefile: makefile ? { file: makefile.file, targets: makefile.targets } : null,
|
|
260
|
+
compose: findFirst(root, COMPOSE_NAMES) ? { file: findFirst(root, COMPOSE_NAMES) } : null,
|
|
261
|
+
readme: findFirst(root, README_NAMES) ? { file: findFirst(root, README_NAMES) } : null,
|
|
262
|
+
subdirs: rootSubdirs.length ? rootSubdirs : subdirs,
|
|
209
263
|
};
|
|
210
264
|
}
|
|
211
265
|
|
|
@@ -220,24 +274,24 @@ function readSubdirs(cwd) {
|
|
|
220
274
|
}
|
|
221
275
|
}
|
|
222
276
|
|
|
223
|
-
function findMarkers(
|
|
277
|
+
function findMarkers(root, subdirs) {
|
|
224
278
|
const found = [];
|
|
225
279
|
const scope = ['.', ...subdirs];
|
|
226
280
|
for (const marker of MARKER_NAMES) {
|
|
227
281
|
for (const dir of scope) {
|
|
228
|
-
const target = dir === '.' ? path.join(
|
|
282
|
+
const target = dir === '.' ? path.join(root, marker) : path.join(root, dir, marker);
|
|
229
283
|
if (fs.existsSync(target)) {
|
|
230
284
|
found.push(dir === '.' ? marker : path.join(dir, marker));
|
|
231
|
-
break; // 每个 marker
|
|
285
|
+
break; // 每个 marker 只记录一次(优先项目根)
|
|
232
286
|
}
|
|
233
287
|
}
|
|
234
288
|
}
|
|
235
289
|
return found;
|
|
236
290
|
}
|
|
237
291
|
|
|
238
|
-
function findVenv(
|
|
239
|
-
for (const
|
|
240
|
-
if (fs.existsSync(path.join(
|
|
292
|
+
function findVenv(dir) {
|
|
293
|
+
for (const name of ['.venv', 'venv']) {
|
|
294
|
+
if (fs.existsSync(path.join(dir, name, 'pyvenv.cfg'))) return name;
|
|
241
295
|
}
|
|
242
296
|
return null;
|
|
243
297
|
}
|
|
@@ -250,4 +304,37 @@ function readPackageScripts(cwd) {
|
|
|
250
304
|
} catch {
|
|
251
305
|
return {};
|
|
252
306
|
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 按 lock 文件推断推荐包管理器;多锁文件时按 LOCK_PRIORITY 取第一个命中的
|
|
310
|
+
export function inferPackageManager(projectRoot) {
|
|
311
|
+
for (const [lock, pm] of LOCK_PRIORITY) {
|
|
312
|
+
if (fs.existsSync(path.join(projectRoot, lock))) return pm;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function readMakefile(dir) {
|
|
318
|
+
for (const name of MAKEFILE_NAMES) {
|
|
319
|
+
const file = path.join(dir, name);
|
|
320
|
+
if (!fs.existsSync(file)) continue;
|
|
321
|
+
const targets = [];
|
|
322
|
+
try {
|
|
323
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
324
|
+
for (const line of text.split(/\r?\n/)) {
|
|
325
|
+
const match = line.match(/^([A-Za-z0-9_.-]+)\s*:/);
|
|
326
|
+
if (match && !MAKEFILE_PSEUDO.has(match[1])) targets.push(match[1]);
|
|
327
|
+
if (targets.length >= MAKEFILE_MAX_TARGETS) break;
|
|
328
|
+
}
|
|
329
|
+
} catch { /* 读取失败时返回空目标 */ }
|
|
330
|
+
return { file: name, targets };
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function findFirst(dir, names) {
|
|
336
|
+
for (const name of names) {
|
|
337
|
+
if (fs.existsSync(path.join(dir, name))) return name;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
253
340
|
}
|
package/src/render.js
CHANGED
|
@@ -10,9 +10,12 @@ const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates');
|
|
|
10
10
|
|
|
11
11
|
const SKILL_TEMPLATES = {
|
|
12
12
|
powershell: path.join(TEMPLATES_DIR, 'SKILL.powershell.md'),
|
|
13
|
+
pwsh: path.join(TEMPLATES_DIR, 'SKILL.pwsh.md'),
|
|
13
14
|
unix: path.join(TEMPLATES_DIR, 'SKILL.unix.md'),
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
18
|
+
|
|
16
19
|
const templateCache = new Map();
|
|
17
20
|
|
|
18
21
|
function loadTemplate(key) {
|
|
@@ -26,6 +29,12 @@ function resolveFamily(env) {
|
|
|
26
29
|
return env.family === 'unix' ? 'unix' : 'powershell';
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
// Windows 上按 shell 变体选模板:pwsh -> SKILL.pwsh.md,否则 -> SKILL.powershell.md
|
|
33
|
+
function resolveTemplateKey(env) {
|
|
34
|
+
if (env.family === 'unix') return 'unix';
|
|
35
|
+
return (env.shell && env.shell.variant) === 'pwsh' ? 'pwsh' : 'powershell';
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
function fill(template, values) {
|
|
30
39
|
let out = template;
|
|
31
40
|
const placeholderRe = /\{\{([a-zA-Z0-9_]+)\}\}/g;
|
|
@@ -60,10 +69,16 @@ function localeSummary(env) {
|
|
|
60
69
|
return parts.length ? parts.join(', ') : '未获取';
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
function langValue(env) {
|
|
73
|
+
const l = (env.shell && env.shell.locale) || {};
|
|
74
|
+
return l.LANG || l.LC_ALL || '未获取';
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
function projectContextBlock(env) {
|
|
64
78
|
const p = env.project || {};
|
|
65
79
|
const lines = [];
|
|
66
80
|
lines.push(`- 当前目录:\`${p.cwd || '未知'}\``);
|
|
81
|
+
lines.push(`- 项目根:\`${p.projectRoot || '未知'}\``);
|
|
67
82
|
lines.push(`- git 仓库根:${p.gitRoot ? `\`${p.gitRoot}\`` : '未发现 .git(已用当前目录作为根)'}`);
|
|
68
83
|
lines.push(`- venv:${p.venv ? `\`${p.venv}\`` : '未发现'}`);
|
|
69
84
|
const markers = p.markers && p.markers.length ? p.markers.join(', ') : '未发现';
|
|
@@ -72,32 +87,135 @@ function projectContextBlock(env) {
|
|
|
72
87
|
? Object.entries(p.packageScripts).map(([k, v]) => `${k}: ${v}`).join(';')
|
|
73
88
|
: '未发现 package.json scripts';
|
|
74
89
|
lines.push(`- package.json scripts:${scripts}`);
|
|
90
|
+
lines.push(`- 推荐包管理器:${p.packageManager || '未推断'}`);
|
|
91
|
+
const extras = [];
|
|
92
|
+
if (p.readme) extras.push(`README: ${p.readme.file}`);
|
|
93
|
+
if (p.makefile) extras.push(`Makefile: ${p.makefile.file}(${p.makefile.targets.length ? p.makefile.targets.join(', ') : '无目标'})`);
|
|
94
|
+
if (p.compose) extras.push(`Compose: ${p.compose.file}`);
|
|
95
|
+
if (extras.length) lines.push(`- 其它:${extras.join(';')}`);
|
|
75
96
|
const subdirs = p.subdirs && p.subdirs.length ? p.subdirs.join(', ') : '(空)';
|
|
76
|
-
lines.push(`-
|
|
97
|
+
lines.push(`- 项目根第一层子目录:${subdirs}`);
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 项目命令:直接给出本项目"怎么装、怎么跑、怎么测"的推荐命令
|
|
102
|
+
function renderProjectCommands(env) {
|
|
103
|
+
const p = env.project || {};
|
|
104
|
+
const pm = p.packageManager;
|
|
105
|
+
const scripts = p.packageScripts || {};
|
|
106
|
+
const scriptNames = Object.keys(scripts);
|
|
107
|
+
const lines = [];
|
|
108
|
+
const push = (s) => lines.push(`- ${s}`);
|
|
109
|
+
|
|
110
|
+
if (pm === 'uv') {
|
|
111
|
+
push(`推荐包管理器:\`uv\`(检测到 uv.lock)`);
|
|
112
|
+
push(`安装依赖:\`uv sync\``);
|
|
113
|
+
push(`在项目环境中运行:\`uv run <cmd>\``);
|
|
114
|
+
push(`添加依赖:\`uv add <pkg>\``);
|
|
115
|
+
} else if (pm === 'pnpm') {
|
|
116
|
+
push(`推荐包管理器:\`pnpm\`(检测到 pnpm-lock.yaml)`);
|
|
117
|
+
push(`安装依赖:\`pnpm install\``);
|
|
118
|
+
} else if (pm === 'yarn') {
|
|
119
|
+
push(`推荐包管理器:\`yarn\`(检测到 yarn.lock)`);
|
|
120
|
+
push(`安装依赖:\`yarn install\``);
|
|
121
|
+
} else if (pm === 'bun') {
|
|
122
|
+
push(`推荐包管理器:\`bun\`(检测到 bun.lockb / bun.lock)`);
|
|
123
|
+
push(`安装依赖:\`bun install\``);
|
|
124
|
+
} else if (pm === 'npm' || (scriptNames.length > 0 && !pm)) {
|
|
125
|
+
push(`包管理器:\`npm\``);
|
|
126
|
+
push(`安装依赖:\`npm install\``);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (scriptNames.length > 0) {
|
|
130
|
+
const runner = ['pnpm', 'yarn', 'bun'].includes(pm) ? pm : 'npm';
|
|
131
|
+
const labels = { dev: '开发', test: '测试', build: '构建', start: '启动', lint: 'Lint', typecheck: '类型检查' };
|
|
132
|
+
const interesting = scriptNames.filter((name) => ['dev', 'test', 'build', 'start', 'lint', 'typecheck'].includes(name));
|
|
133
|
+
const names = interesting.length > 0 ? interesting : scriptNames.slice(0, 6);
|
|
134
|
+
for (const name of names) {
|
|
135
|
+
const label = labels[name] || '运行';
|
|
136
|
+
const cmd = name === 'test' ? `${runner} test` : `${runner} run ${name}`;
|
|
137
|
+
push(`${label}:\`${cmd}\``);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (p.venv) {
|
|
142
|
+
const sep = p.venv === 'venv' ? 'venv' : '.venv';
|
|
143
|
+
push(`Python venv:\`${p.venv}\`;Windows 激活 \`.\\${sep}\\Scripts\\Activate.ps1\`,Unix 激活 \`source ${sep}/bin/activate\``);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (p.makefile && p.makefile.targets.length > 0) {
|
|
147
|
+
push(`Makefile 目标(${p.makefile.file}):${p.makefile.targets.join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (p.compose) {
|
|
151
|
+
push(`Docker Compose:\`docker compose -f ${p.compose.file} up -d\``);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (p.readme) {
|
|
155
|
+
push(`动手前先读 \`${p.readme.file}\` 了解结构与约定`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (lines.length === 0) {
|
|
159
|
+
lines.push('未探测到可推导的项目命令;先读 README / AGENTS.md 确认项目结构再操作。');
|
|
160
|
+
}
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 代码库检索指引:rg 可用时优先 rg,否则用平台原生搜索
|
|
165
|
+
function renderSearchGuide(env) {
|
|
166
|
+
const family = resolveFamily(env);
|
|
167
|
+
const rg = getTool(env, 'rg');
|
|
168
|
+
const lines = [];
|
|
169
|
+
if (rg && rg.version) {
|
|
170
|
+
lines.push('- 定位定义/引用:`rg -n \'<pattern>\' <dir>`;列文件:`rg --files <dir>`');
|
|
171
|
+
lines.push('- 排除噪音目录:`rg --glob \'!node_modules/**\' --glob \'!.git/**\' \'<pattern>\'`');
|
|
172
|
+
} else if (family === 'powershell') {
|
|
173
|
+
lines.push('- 文本搜索:`Get-ChildItem -Recurse -File | Select-String -Pattern \'<pattern>\'`(范围大时先限定 `-Path` 子目录)');
|
|
174
|
+
lines.push('- 列文件:`Get-ChildItem -Recurse -File | Where-Object { $_.Name -like \'<pattern>\' }`');
|
|
175
|
+
} else {
|
|
176
|
+
lines.push('- 文本搜索:`grep -rn \'<pattern>\' <dir>`;排除噪音:`grep -rn --exclude-dir=node_modules --exclude-dir=.git \'<pattern>\' .`');
|
|
177
|
+
}
|
|
178
|
+
lines.push(family === 'powershell'
|
|
179
|
+
? '- 先读文件再修改:`Get-Content -Path \'<file>\' -Encoding UTF8`'
|
|
180
|
+
: '- 先读文件再修改:`cat <file>`');
|
|
77
181
|
return lines.join('\n');
|
|
78
182
|
}
|
|
79
183
|
|
|
80
184
|
export function renderSkill(env) {
|
|
185
|
+
const tplKey = resolveTemplateKey(env);
|
|
81
186
|
const family = resolveFamily(env);
|
|
82
187
|
const shell = env.shell || {};
|
|
83
188
|
const os = env.os || {};
|
|
189
|
+
const pwshTool = getTool(env, 'pwsh');
|
|
190
|
+
const pwshVersion = (pwshTool && pwshTool.version) || '未知';
|
|
191
|
+
const ps51Installed = Boolean(shell.psVersion && /^5\./.test(shell.psVersion));
|
|
84
192
|
const values = {
|
|
85
193
|
generatedAt: new Date().toISOString(),
|
|
194
|
+
generatorVersion: pkg.version,
|
|
86
195
|
osVersion: `${os.platform || ''} ${os.release || ''}`.trim() || '未知',
|
|
87
196
|
shellName: shell.defaultShellName || shell.defaultShell || (family === 'powershell' ? 'powershell' : 'sh'),
|
|
88
197
|
psVersion: shell.psVersion || '未知',
|
|
198
|
+
pwshVersion,
|
|
89
199
|
consoleOutputEncoding: shell.consoleOutputEncoding || '未知',
|
|
90
200
|
chcp: shell.chcp ?? '未知',
|
|
91
201
|
executionPolicySummary: executionPolicySummary(env),
|
|
92
|
-
pwsh7Note: shell.pwsh7Available
|
|
202
|
+
pwsh7Note: shell.pwsh7Available
|
|
203
|
+
? `本机也装有 pwsh ${pwshVersion === '未知' ? '7' : pwshVersion}(PowerShell 7)。如实际执行环境是 pwsh:\`&&\`/\`??\` 等新语法可用、中文参数默认 UTF-8 不乱码,可优先使用 \`pwsh\`。`
|
|
204
|
+
: '',
|
|
205
|
+
ps51Note: (shell.ps51Available || ps51Installed)
|
|
206
|
+
? '本机也装有 Windows PowerShell 5.1(powershell.exe)。若实际执行环境是 powershell.exe:`&&`/`??` 等新语法不可用、`$OutputEncoding` 默认非 UTF-8(中文参数需先设置)。'
|
|
207
|
+
: '',
|
|
93
208
|
localeSummary: localeSummary(env),
|
|
209
|
+
langValue: langValue(env),
|
|
94
210
|
fishNote: shell.defaultShellName === 'fish' ? '注意:默认 shell 是 fish,语法与 bash/zsh 不同(本文以 bash/zsh 为主)。' : '',
|
|
95
211
|
gitRoot: (env.project && env.project.gitRoot) || '未发现 .git',
|
|
96
212
|
cwd: (env.project && env.project.cwd) || process.cwd(),
|
|
97
213
|
toolsSummary: toolsSummary(env),
|
|
98
214
|
projectContext: projectContextBlock(env),
|
|
215
|
+
projectCommands: renderProjectCommands(env),
|
|
216
|
+
searchGuide: renderSearchGuide(env),
|
|
99
217
|
};
|
|
100
|
-
return fill(loadTemplate(
|
|
218
|
+
return fill(loadTemplate(tplKey), values);
|
|
101
219
|
}
|
|
102
220
|
|
|
103
221
|
function renderEnvironment(env) {
|
|
@@ -105,10 +223,11 @@ function renderEnvironment(env) {
|
|
|
105
223
|
const shell = env.shell || {};
|
|
106
224
|
const os = env.os || {};
|
|
107
225
|
const p = env.project || {};
|
|
226
|
+
const pwshTool = getTool(env, 'pwsh');
|
|
108
227
|
const lines = [];
|
|
109
228
|
lines.push('# 环境事实(pwsh-guide 探测结果)');
|
|
110
229
|
lines.push('');
|
|
111
|
-
lines.push('> 本文件由 pwsh-guide 生成;环境变化后运行 `pwsh-guide refresh`
|
|
230
|
+
lines.push('> 本文件由 pwsh-guide 生成;环境变化后运行 `pwsh-guide refresh` 更新,运行 `pwsh-guide check` 检测漂移。');
|
|
112
231
|
lines.push('');
|
|
113
232
|
lines.push('## Shell 环境');
|
|
114
233
|
lines.push('');
|
|
@@ -117,10 +236,12 @@ function renderEnvironment(env) {
|
|
|
117
236
|
if (family === 'powershell') {
|
|
118
237
|
lines.push(`| OS | ${os.release ?? '-'} |`);
|
|
119
238
|
lines.push(`| 默认 shell | ${shell.defaultShell ?? '-'} |`);
|
|
239
|
+
lines.push(`| shell 变体 | ${shell.variant === 'pwsh' ? 'pwsh(PowerShell 7)' : shell.variant === 'powershell' ? 'powershell.exe(Windows PowerShell 5.1)' : '-'} |`);
|
|
120
240
|
lines.push(`| PowerShell 版本 | ${shell.psVersion ?? '-'} |`);
|
|
121
241
|
lines.push(`| PSEdition | ${shell.psEdition ?? '-'} |`);
|
|
122
242
|
lines.push(`| 主机 | ${shell.hostName ?? '-'} |`);
|
|
123
243
|
lines.push(`| pwsh 7 可用 | ${shell.pwsh7Available ? '是' : '否'} |`);
|
|
244
|
+
lines.push(`| pwsh 7 版本 | ${(pwshTool && pwshTool.version) ?? '-'} |`);
|
|
124
245
|
lines.push(`| 控制台输出编码 | ${shell.consoleOutputEncoding ?? '-'} |`);
|
|
125
246
|
lines.push(`| $OutputEncoding | ${shell.outputEncoding ?? '-'} |`);
|
|
126
247
|
lines.push(`| 代码页 chcp | ${shell.chcp ?? '-'} |`);
|
|
@@ -155,21 +276,30 @@ function renderEnvironment(env) {
|
|
|
155
276
|
lines.push('| 项 | 值 |');
|
|
156
277
|
lines.push('| --- | --- |');
|
|
157
278
|
lines.push(`| 当前目录 | ${p.cwd ?? '-'} |`);
|
|
279
|
+
lines.push(`| 项目根 | ${p.projectRoot ?? '-'} |`);
|
|
158
280
|
lines.push(`| git 仓库根 | ${p.gitRoot ?? '未发现 .git'} |`);
|
|
159
281
|
lines.push(`| venv | ${p.venv ?? '未发现'} |`);
|
|
160
282
|
lines.push(`| 标记文件 | ${p.markers && p.markers.length ? p.markers.join(', ') : '未发现'} |`);
|
|
283
|
+
lines.push(`| 推荐包管理器 | ${p.packageManager ?? '未推断'} |`);
|
|
161
284
|
const scriptsText = p.packageScripts && Object.keys(p.packageScripts).length
|
|
162
285
|
? Object.entries(p.packageScripts).map(([k, v]) => `${k}: ${v}`).join('<br>')
|
|
163
286
|
: '未发现 package.json scripts';
|
|
164
287
|
lines.push(`| package.json scripts | ${scriptsText} |`);
|
|
165
|
-
|
|
288
|
+
const cwdScriptsText = p.cwdPackageScripts && Object.keys(p.cwdPackageScripts).length
|
|
289
|
+
? Object.entries(p.cwdPackageScripts).map(([k, v]) => `${k}: ${v}`).join('<br>')
|
|
290
|
+
: '-';
|
|
291
|
+
lines.push(`| 当前目录 scripts | ${cwdScriptsText} |`);
|
|
292
|
+
lines.push(`| README | ${p.readme ? p.readme.file : '未发现'} |`);
|
|
293
|
+
lines.push(`| Makefile | ${p.makefile ? `${p.makefile.file}(目标:${p.makefile.targets.join(', ')})` : '未发现'} |`);
|
|
294
|
+
lines.push(`| Docker Compose | ${p.compose ? p.compose.file : '未发现'} |`);
|
|
295
|
+
lines.push(`| 项目根第一层子目录 | ${p.subdirs && p.subdirs.length ? p.subdirs.join(', ') : '-'} |`);
|
|
166
296
|
return `${lines.join('\n')}\n`;
|
|
167
297
|
}
|
|
168
298
|
|
|
169
299
|
const COMMON_COMMANDS_POWERSHELL = [
|
|
170
300
|
'- 列出目录:`Get-ChildItem -Path . -Force`',
|
|
171
301
|
'- 递归查找文件:`Get-ChildItem -Recurse -Filter \'*.py\'`',
|
|
172
|
-
'- 文本搜索:`
|
|
302
|
+
'- 文本搜索:`Get-ChildItem -Recurse -Filter \'*.py\' | Select-String -Pattern \'关键词\'`',
|
|
173
303
|
'- 读文件:`Get-Content -Path \'<file>\' -Encoding UTF8`',
|
|
174
304
|
'- 写文件:`Set-Content -Path \'<file>\' -Value \'<text>\' -Encoding UTF8`',
|
|
175
305
|
'- 判断路径存在:`Test-Path \'<path>\'`',
|
|
@@ -253,7 +383,7 @@ const TOOL_COMMANDS = {
|
|
|
253
383
|
'- 容器列表:`docker ps`',
|
|
254
384
|
],
|
|
255
385
|
make: [
|
|
256
|
-
'-
|
|
386
|
+
'- 查看目标:`make help`(若定义)或读取 Makefile',
|
|
257
387
|
'- 执行目标:`make <target>`',
|
|
258
388
|
],
|
|
259
389
|
rg: [
|
|
@@ -265,7 +395,7 @@ const TOOL_COMMANDS = {
|
|
|
265
395
|
'- 创建 PR:`gh pr create`',
|
|
266
396
|
],
|
|
267
397
|
pwsh: [
|
|
268
|
-
'- 版本:`pwsh
|
|
398
|
+
'- 版本:`pwsh --version`',
|
|
269
399
|
'- 运行脚本:`pwsh -File <script>.ps1`',
|
|
270
400
|
],
|
|
271
401
|
cargo: [
|
|
@@ -285,6 +415,7 @@ function renderCommands(env) {
|
|
|
285
415
|
lines.push('# 命令模板(按工具)');
|
|
286
416
|
lines.push('');
|
|
287
417
|
lines.push('> 仅列出探测到可用的工具;`<...>` 为占位参数,执行前替换。');
|
|
418
|
+
lines.push('> 本文件为通用模板,未逐条在本机实测;复杂场景执行前先 `--help` 确认参数,破坏性操作先 `-WhatIf` 或展示计划。');
|
|
288
419
|
lines.push('');
|
|
289
420
|
lines.push(family === 'unix' ? '## 通用操作(bash/zsh)' : '## 通用操作(PowerShell 原生,始终可用)');
|
|
290
421
|
for (const c of common) lines.push(c);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pwsh-guide
|
|
3
|
-
description: 本项目的 PowerShell
|
|
3
|
+
description: 本项目的 PowerShell(Windows PowerShell 5.1)命令执行指南。需要执行 shell 命令、遇到 PowerShell 报错、中文乱码或不确定命令语法时使用;含本机环境事实、命令模板、快速自检与失败诊断。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# PowerShell
|
|
6
|
+
# PowerShell 操作指南(本环境 · Windows PowerShell 5.1)
|
|
7
7
|
|
|
8
|
-
> 由 `pwsh-guide` 生成于 {{generatedAt}}。环境变化后运行 `pwsh-guide refresh`
|
|
8
|
+
> 由 `pwsh-guide v{{generatorVersion}}` 生成于 {{generatedAt}}。环境变化后运行 `pwsh-guide refresh` 更新;`pwsh-guide check` 可检测环境漂移。
|
|
9
9
|
|
|
10
10
|
## 1. 执行命令前先确认
|
|
11
11
|
|
|
@@ -24,6 +24,9 @@ description: 本项目的 PowerShell 命令执行指南。需要执行 shell 命
|
|
|
24
24
|
4. 环境变量读取 `$env:NAME`,写入 `$env:NAME = 'value'`(仅当前会话)。
|
|
25
25
|
5. 只读操作优先;删除等危险操作先 `-WhatIf` 或展示计划再执行。
|
|
26
26
|
6. 长命令优先写成 `.ps1` 再以 `-File` 执行,避免内联引号地狱。
|
|
27
|
+
7. 含中文参数的原生命令(git/node/npm 等)先按第 4 节设置输入编码。
|
|
28
|
+
8. 传参用具名参数或变量 splatting:先 `$h = @{ ... }` 再 `命令 @h`;**禁止 `命令 @{ ... }`**(内联 splatting 不生效,会把 hashtable 当普通参数)。
|
|
29
|
+
9. 含特殊字符(引号/逗号/中文)的参数:禁止内联拼接进 `-Command "..."` 字符串(会被拆碎);改用 `.ps1` 脚本 `param()`、环境变量或参数数组传递。
|
|
27
30
|
|
|
28
31
|
## 3. 常用命令模板
|
|
29
32
|
|
|
@@ -33,8 +36,10 @@ description: 本项目的 PowerShell 命令执行指南。需要执行 shell 命
|
|
|
33
36
|
|
|
34
37
|
## 4. 编码与乱码处理
|
|
35
38
|
|
|
36
|
-
-
|
|
37
|
-
-
|
|
39
|
+
- 输出乱码:`[Console]::OutputEncoding = [System.Text.Encoding]::UTF8`;切换代码页 `chcp 65001`。
|
|
40
|
+
- **输入乱码(中文参数)**:`$OutputEncoding` 默认是 us-ascii,原生命令收到的中文参数会乱码。含中文参数的命令须在同一命令内先设置:
|
|
41
|
+
`[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; $OutputEncoding = [System.Text.Encoding]::UTF8; <cmd>`
|
|
42
|
+
- 写 `.ps1` 时在文件头部设置一次上述两行。
|
|
38
43
|
- 写文件指定编码:`Set-Content -Path <file> -Value <text> -Encoding UTF8`(PS 5.1 会写 BOM)。
|
|
39
44
|
- 读文件乱码:`Get-Content -Path <file> -Encoding UTF8`。
|
|
40
45
|
- 重定向中文输出优先 `Out-File -Encoding UTF8`,避免 `>` 按控制台编码写出。
|
|
@@ -44,19 +49,38 @@ description: 本项目的 PowerShell 命令执行指南。需要执行 shell 命
|
|
|
44
49
|
| 症状 | 常见原因 | 处理 |
|
|
45
50
|
| --- | --- | --- |
|
|
46
51
|
| 命令找不到 | PATH 缺失 / 拼写错 | `Get-Command <name>` 确认;查看 `references/environment.md` 工具表 |
|
|
47
|
-
| 中文乱码 | 代码页与输出编码不匹配 | 按第 4 节切换 UTF-8 |
|
|
52
|
+
| 中文乱码 | 代码页与输出编码不匹配 | 按第 4 节切换 UTF-8(含 $OutputEncoding 输入侧) |
|
|
48
53
|
| 引号/转义报错 | 内联 `-Command` 引号地狱 | 改用 `-File` 脚本或参数数组 |
|
|
54
|
+
| 参数被拆分/丢失 | 特殊字符内联拼接进 `-Command` | 改用 `.ps1` 脚本 `param()` / 环境变量 / 参数数组 |
|
|
55
|
+
| 参数收到 Hashtable | 误用内联 splatting `命令 @{...}` | 先存变量 `$h = @{...}` 再 `命令 @h` |
|
|
49
56
|
| 权限拒绝 | 执行策略 / ACL | `Get-ExecutionPolicy -List`;脚本加 `-ExecutionPolicy Bypass` |
|
|
50
57
|
| 路径找不到 | 空格/中文/反斜杠 | 单引号包裹;先 `Test-Path` |
|
|
51
58
|
| 程序假死 | 交互式命令等待输入 | 加 `-NonInteractive`,或 `echo y | <cmd>` |
|
|
59
|
+
| npm/网络报错 | registry/代理/锁文件不匹配 | `npm config get registry`、`npm ping`;确认包管理器与锁文件一致 |
|
|
60
|
+
| 端口被占用 | 端口冲突 | `Get-NetTCPConnection -LocalPort <port>` 找 PID,确认后 `Stop-Process -Id <pid> -Force` |
|
|
61
|
+
| git 凭据弹窗 | 需要认证 | 先确认凭据;必要时 `$env:GIT_TERMINAL_PROMPT = '0'` 避免挂起 |
|
|
52
62
|
|
|
53
|
-
## 6.
|
|
63
|
+
## 6. 快速自检(环境是否已变化)
|
|
54
64
|
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
|
|
65
|
+
- 复核 shell:`$PSVersionTable.PSVersion.ToString()` 应为 `{{psVersion}}`;`chcp` 应为 `{{chcp}}`。
|
|
66
|
+
- 复核工具:逐一运行第 3 节每个工具的 `--version`,输出应与列出的版本一致。
|
|
67
|
+
- 规则:任一自检项与期望不符,或同一命令连续失败两次,先运行 `npx pwsh-guide refresh` 并重读本文件,不要继续猜测。
|
|
68
|
+
|
|
69
|
+
## 7. 代码库检索指引
|
|
70
|
+
|
|
71
|
+
{{searchGuide}}
|
|
72
|
+
|
|
73
|
+
## 8. 项目上下文(来自探测)
|
|
59
74
|
|
|
60
|
-
|
|
75
|
+
{{projectContext}}
|
|
61
76
|
|
|
62
|
-
|
|
77
|
+
## 9. 项目命令(推荐)
|
|
78
|
+
|
|
79
|
+
{{projectCommands}}
|
|
80
|
+
|
|
81
|
+
## 10. 禁止事项
|
|
82
|
+
|
|
83
|
+
- 禁止 bash 语法:`ls -la`、`grep`、`cat`、`rm -rf`、`export`、`dir`、`copy`。
|
|
84
|
+
- 删除一律用 `Remove-Item -Path <path> -Recurse -Force`,且先确认路径再执行。
|
|
85
|
+
- 未读文件内容不得修改;不得批量格式化无关代码。
|
|
86
|
+
- 仓库 AGENTS.md 规则优先于本文件。
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pwsh-guide
|
|
3
|
+
description: 本项目的 PowerShell(pwsh 7)命令执行指南。需要执行 shell 命令、遇到 PowerShell 报错、中文乱码或不确定命令语法时使用;含本机环境事实、命令模板、快速自检与失败诊断。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PowerShell 操作指南(本环境 · pwsh 7)
|
|
7
|
+
|
|
8
|
+
> 由 `pwsh-guide v{{generatorVersion}}` 生成于 {{generatedAt}}。环境变化后运行 `pwsh-guide refresh` 更新;`pwsh-guide check` 可检测环境漂移。
|
|
9
|
+
|
|
10
|
+
## 1. 执行命令前先确认
|
|
11
|
+
|
|
12
|
+
- 默认 shell 是 `{{shellName}}`(pwsh {{psVersion}}),**不是 bash/cmd**:禁止 `ls -la`、`grep`、`cat`、`rm -rf`、`export`、`dir`、`copy`。
|
|
13
|
+
- OS:{{osVersion}}。{{ps51Note}}
|
|
14
|
+
- 编码:`$OutputEncoding` 与 `[Console]::OutputEncoding` 均为 UTF-8,中文参数/输出一般不会乱码(代码页 chcp {{chcp}})。
|
|
15
|
+
- 执行策略:{{executionPolicySummary}}。
|
|
16
|
+
- 当前目录:`{{cwd}}`;git 根:`{{gitRoot}}`。
|
|
17
|
+
- 完整环境事实见 `references/environment.md`;可用命令模板见 `references/commands.md`。
|
|
18
|
+
|
|
19
|
+
## 2. 命令执行铁律
|
|
20
|
+
|
|
21
|
+
1. 一条命令一次执行;pwsh 7 支持 `&&`/`||`,但链式出错时难以定位,仍建议一次一条。
|
|
22
|
+
2. 执行后检查退出状态:外部命令看 `$LASTEXITCODE`,cmdlet 看 `$?`,并查看 stderr。
|
|
23
|
+
3. 路径含空格或中文时用单引号包裹:`Get-Content 'E:\path with 中文\file.txt'`。
|
|
24
|
+
4. 环境变量读取 `$env:NAME`,写入 `$env:NAME = 'value'`(仅当前会话)。
|
|
25
|
+
5. 只读操作优先;删除等危险操作先 `-WhatIf` 或展示计划再执行。
|
|
26
|
+
6. 长命令优先写成 `.ps1` 再以 `pwsh -File` 执行,避免内联引号地狱。
|
|
27
|
+
7. 含特殊字符(引号/逗号/中文)的参数:禁止内联拼接进 `-Command "..."` 字符串(会被拆碎);改用 `.ps1` 脚本 `param()`、环境变量或参数数组传递。
|
|
28
|
+
8. 传参用具名参数或变量 splatting:先 `$h = @{ ... }` 再 `命令 @h`;**禁止 `命令 @{ ... }`**(内联 splatting 不生效,会把 hashtable 当普通参数)。
|
|
29
|
+
|
|
30
|
+
## 3. 常用命令模板
|
|
31
|
+
|
|
32
|
+
可用工具与命令见 `references/commands.md`。本机探测到的工具:
|
|
33
|
+
|
|
34
|
+
{{toolsSummary}}
|
|
35
|
+
|
|
36
|
+
## 4. 编码与乱码处理
|
|
37
|
+
|
|
38
|
+
- pwsh 7 默认 UTF-8:`$OutputEncoding` 与 `[Console]::OutputEncoding` 均为 UTF-8,中文一般不会乱码。
|
|
39
|
+
- 写文件指定编码:`Set-Content -Path <file> -Value <text> -Encoding utf8`(pwsh 7 写 UTF-8 无 BOM);`Out-File -Encoding utf8` 同理。
|
|
40
|
+
- 读文件乱码:`Get-Content -Path <file> -Encoding UTF8`。
|
|
41
|
+
- 读 GBK 旧文件:`[System.IO.File]::ReadAllText('<file>', [System.Text.Encoding]::GetEncoding('GBK'))`。
|
|
42
|
+
- 仍乱码时:确认 `[Console]::OutputEncoding.WebName` 为 `utf-8`;必要时 `chcp 65001`。
|
|
43
|
+
|
|
44
|
+
## 5. 失败分类诊断
|
|
45
|
+
|
|
46
|
+
| 症状 | 常见原因 | 处理 |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| 命令找不到 | PATH 缺失 / 拼写错 | `Get-Command <name>` 确认;查看 `references/environment.md` 工具表 |
|
|
49
|
+
| 中文乱码 | 代码页与输出编码不匹配 | 按第 4 节处理(pwsh 默认 UTF-8,一般不会出现) |
|
|
50
|
+
| 引号/转义报错 | 内联 `-Command` 引号地狱 | 改用 `-File` 脚本或参数数组 |
|
|
51
|
+
| 参数被拆分/丢失 | 特殊字符内联拼接进 `-Command` | 改用 `.ps1` 脚本 `param()` / 环境变量 / 参数数组 |
|
|
52
|
+
| 参数收到 Hashtable | 误用内联 splatting `命令 @{...}` | 先存变量 `$h = @{...}` 再 `命令 @h` |
|
|
53
|
+
| 新语法报错 | 实际 shell 是 powershell.exe 5.1 | `&&`/`??`/三元仅在 pwsh 7 可用;确认实际执行 shell |
|
|
54
|
+
| 权限拒绝 | 执行策略 / ACL | `Get-ExecutionPolicy -List`;脚本加 `-ExecutionPolicy Bypass` |
|
|
55
|
+
| 路径找不到 | 空格/中文/反斜杠 | 单引号包裹;先 `Test-Path` |
|
|
56
|
+
| 程序假死 | 交互式命令等待输入 | 加 `-NonInteractive`,或 `echo y | <cmd>` |
|
|
57
|
+
| npm/网络报错 | registry/代理/锁文件不匹配 | `npm config get registry`、`npm ping`;确认包管理器与锁文件一致 |
|
|
58
|
+
| 端口被占用 | 端口冲突 | `Get-NetTCPConnection -LocalPort <port>` 找 PID,确认后 `Stop-Process -Id <pid> -Force` |
|
|
59
|
+
| git 凭据弹窗 | 需要认证 | 先确认凭据;必要时 `$env:GIT_TERMINAL_PROMPT = '0'` 避免挂起 |
|
|
60
|
+
|
|
61
|
+
## 6. 快速自检(环境是否已变化)
|
|
62
|
+
|
|
63
|
+
- 复核 shell:`pwsh --version` 应为 `{{pwshVersion}}`;`$OutputEncoding.WebName` 应为 `utf-8`。
|
|
64
|
+
- 复核工具:逐一运行第 3 节每个工具的 `--version`,输出应与列出的版本一致。
|
|
65
|
+
- 规则:任一自检项与期望不符,或同一命令连续失败两次,先运行 `npx pwsh-guide refresh` 并重读本文件,不要继续猜测。
|
|
66
|
+
|
|
67
|
+
## 7. 代码库检索指引
|
|
68
|
+
|
|
69
|
+
{{searchGuide}}
|
|
70
|
+
|
|
71
|
+
## 8. 项目上下文(来自探测)
|
|
72
|
+
|
|
73
|
+
{{projectContext}}
|
|
74
|
+
|
|
75
|
+
## 9. 项目命令(推荐)
|
|
76
|
+
|
|
77
|
+
{{projectCommands}}
|
|
78
|
+
|
|
79
|
+
## 10. pwsh 7 特性(与 powershell.exe 5.1 的差异)
|
|
80
|
+
|
|
81
|
+
- `&&`/`||`、空合并 `??`、三元 `?:`、`Get-Content -Raw` 等新语法仅 pwsh 7 可用(5.1 会报错)。
|
|
82
|
+
- 并行:`1..10 | ForEach-Object -Parallel { <块> } -ThrottleLimit 4`;块内用 `$using:变量` 读外部变量,输出顺序不保证。
|
|
83
|
+
- JSON:`Get-Content -Raw -Encoding utf8 | ConvertFrom-Json -AsHashtable`(保留键大小写)。
|
|
84
|
+
- 原生命令非 0 退出触发错误处理:`$PSNativeCommandUseErrorActionPreference = $true`(谨慎使用,会影响所有原生命令)。
|
|
85
|
+
|
|
86
|
+
## 11. 禁止事项
|
|
87
|
+
|
|
88
|
+
- 禁止 bash 语法:`ls -la`、`grep`、`cat`、`rm -rf`、`export`、`dir`、`copy`。
|
|
89
|
+
- 删除一律用 `Remove-Item -Path <path> -Recurse -Force`,且先确认路径再执行。
|
|
90
|
+
- 未读文件内容不得修改;不得批量格式化无关代码。
|
|
91
|
+
- 仓库 AGENTS.md 规则优先于本文件。
|
package/templates/SKILL.unix.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pwsh-guide
|
|
3
|
-
description: 本项目的 Unix shell(bash/zsh)命令执行指南。需要执行 shell 命令、遇到 shell
|
|
3
|
+
description: 本项目的 Unix shell(bash/zsh)命令执行指南。需要执行 shell 命令、遇到 shell 报错、编码问题或不确定命令语法时使用;含本机环境事实、命令模板、快速自检与失败诊断。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Shell 操作指南(本环境)
|
|
7
7
|
|
|
8
|
-
> 由 `pwsh-guide` 生成于 {{generatedAt}}。环境变化后运行 `pwsh-guide refresh`
|
|
8
|
+
> 由 `pwsh-guide v{{generatorVersion}}` 生成于 {{generatedAt}}。环境变化后运行 `pwsh-guide refresh` 更新;`pwsh-guide check` 可检测环境漂移。
|
|
9
9
|
|
|
10
10
|
## 1. 执行命令前先确认
|
|
11
11
|
|
|
@@ -45,14 +45,31 @@ description: 本项目的 Unix shell(bash/zsh)命令执行指南。需要执
|
|
|
45
45
|
| 引号/转义报错 | 嵌套引号 | 用单引号或写成脚本文件 |
|
|
46
46
|
| 中文乱码 | locale 非 UTF-8 | 按第 4 节处理 |
|
|
47
47
|
| 程序假死 | 交互式命令等待输入 | 加非交互参数,或 `< /dev/null` |
|
|
48
|
+
| npm/网络报错 | registry/代理/锁文件不匹配 | `npm config get registry`、`npm ping`;确认包管理器与锁文件一致 |
|
|
49
|
+
| 端口被占用 | 端口冲突 | `lsof -i :<port>` 找 PID,确认后 `kill <pid>` |
|
|
50
|
+
| git 凭据弹窗 | 需要认证 | 先确认凭据;必要时 `GIT_TERMINAL_PROMPT=0` 避免挂起 |
|
|
48
51
|
|
|
49
|
-
## 6.
|
|
52
|
+
## 6. 快速自检(环境是否已变化)
|
|
53
|
+
|
|
54
|
+
- 复核 shell:`echo $SHELL` 应为 `{{shellName}}`;`echo $LANG` 应为 `{{langValue}}`。
|
|
55
|
+
- 复核工具:逐一运行第 3 节每个工具的 `--version`,输出应与列出的版本一致。
|
|
56
|
+
- 规则:任一自检项与期望不符,或同一命令连续失败两次,先运行 `npx pwsh-guide refresh` 并重读本文件,不要继续猜测。
|
|
57
|
+
|
|
58
|
+
## 7. 代码库检索指引
|
|
59
|
+
|
|
60
|
+
{{searchGuide}}
|
|
61
|
+
|
|
62
|
+
## 8. 项目上下文(来自探测)
|
|
63
|
+
|
|
64
|
+
{{projectContext}}
|
|
65
|
+
|
|
66
|
+
## 9. 项目命令(推荐)
|
|
67
|
+
|
|
68
|
+
{{projectCommands}}
|
|
69
|
+
|
|
70
|
+
## 10. 禁止事项
|
|
50
71
|
|
|
51
72
|
- 禁止 PowerShell/cmd 语法:`Get-ChildItem`、`Get-Content`、`Set-Content`、`dir`、`copy`、`$env:`。
|
|
52
73
|
- `rm -rf` 前必须确认路径;删除操作先展示计划。
|
|
53
74
|
- 未读文件内容不得修改;不得批量格式化无关代码。
|
|
54
|
-
- 仓库 AGENTS.md 规则优先于本文件。
|
|
55
|
-
|
|
56
|
-
## 7. 项目上下文(来自探测)
|
|
57
|
-
|
|
58
|
-
{{projectContext}}
|
|
75
|
+
- 仓库 AGENTS.md 规则优先于本文件。
|