pwsh-guide 0.2.1 → 0.3.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 +27 -13
- package/bin/pwsh-guide.js +51 -23
- package/package.json +1 -1
- package/src/detect.js +12 -3
- package/src/meta.js +124 -0
- package/src/probe.js +90 -24
- package/src/render.js +117 -6
- package/templates/SKILL.powershell.md +31 -11
- package/templates/SKILL.unix.md +25 -8
package/README.md
CHANGED
|
@@ -1,32 +1,43 @@
|
|
|
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
|
+
|
|
9
11
|
- Windows 下生成 PowerShell 指南;macOS/Linux 下生成 bash/zsh 指南(只生成当前环境可用内容,省 token)
|
|
10
12
|
- 平台无关探测全部用 Node 原生实现(`os` / `process.env` / `fs`),不依赖特定 shell
|
|
13
|
+
- 工具版本并行探测;项目上下文以 git 根为基准,并按 lock 文件推断推荐包管理器与项目命令
|
|
11
14
|
|
|
12
15
|
## 安装与使用
|
|
13
16
|
|
|
14
17
|
```bash
|
|
15
18
|
# 项目根目录运行(npx 方式,无需全局安装)
|
|
16
19
|
npx pwsh-guide init
|
|
17
|
-
#
|
|
20
|
+
# 环境变化后更新(重新生成 SKILL.md 与 references,两份输出)
|
|
18
21
|
npx pwsh-guide refresh
|
|
19
|
-
#
|
|
22
|
+
# 检测环境漂移(退出码 0=一致,1=有漂移,2=无快照)
|
|
23
|
+
npx pwsh-guide check
|
|
24
|
+
# 只输出探测 JSON,不写任何文件(stdout 为纯 JSON)
|
|
20
25
|
npx pwsh-guide init --dry-run
|
|
21
26
|
# 查看版本
|
|
22
27
|
npx pwsh-guide --version
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
生成的 skill
|
|
30
|
+
生成的 skill 有两份(内容一致,分别被不同代理发现):
|
|
31
|
+
|
|
32
|
+
- `.agents/skills/pwsh-guide/`(Codex)
|
|
33
|
+
- `.claude/skills/pwsh-guide/`(Claude Code)
|
|
34
|
+
|
|
35
|
+
每份包含:
|
|
26
36
|
|
|
27
|
-
- `SKILL.md
|
|
37
|
+
- `SKILL.md`:命令执行铁律、输入/输出编码处理、快速自检、代码库检索指引、失败诊断、项目命令
|
|
28
38
|
- `references/environment.md`:探测到的本机环境事实(shell 版本/编码/locale/工具/项目上下文)
|
|
29
39
|
- `references/commands.md`:按探测到的工具动态裁剪的命令模板
|
|
40
|
+
- `.meta.json`:生成快照(`check` 漂移检测的基准)
|
|
30
41
|
|
|
31
42
|
## 探测内容(只读)
|
|
32
43
|
|
|
@@ -34,25 +45,28 @@ npx pwsh-guide --version
|
|
|
34
45
|
- shell(按平台深度探测):
|
|
35
46
|
- Windows:PowerShell 版本 / PSEdition / 控制台输出编码 / `$OutputEncoding` / chcp / 执行策略 / pwsh 7 可用性
|
|
36
47
|
- 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
|
-
-
|
|
48
|
+
- 工具:git、node、npm、npx、pnpm、yarn、bun、python、py、uv、pip、docker、make、rg、gh、pwsh、cargo、go 的可用性与版本(PATH 遍历 + `--version`,并行探测,超时/缺失记为 null)
|
|
49
|
+
- 项目(以 git 根为基准):标记文件、venv、package.json scripts、推荐包管理器(lock 文件推断)、README / Makefile 目标 / Docker Compose、项目根第一层子目录
|
|
39
50
|
|
|
40
51
|
## 开发
|
|
41
52
|
|
|
42
53
|
```text
|
|
43
|
-
bin/pwsh-guide.js CLI 入口(init / refresh / --dry-run)
|
|
44
|
-
src/probe.js 跨平台主探测(Node
|
|
45
|
-
src/detect.js
|
|
54
|
+
bin/pwsh-guide.js CLI 入口(init / refresh / check / --dry-run)
|
|
55
|
+
src/probe.js 跨平台主探测(Node 原生;并行工具探测;项目根基准)
|
|
56
|
+
src/detect.js 仓库根判定与双输出目录解析
|
|
46
57
|
src/render.js 按 shell 族渲染 SKILL.md / environment.md / commands.md
|
|
58
|
+
src/meta.js 生成快照与漂移对比(check 用)
|
|
47
59
|
scripts/probe.windows.ps1 Windows 深度探测(PS 5.1 兼容,输出 UTF-8 JSON)
|
|
48
60
|
scripts/probe.unix.sh Unix 深度探测(POSIX sh 兼容,输出 UTF-8 JSON)
|
|
49
61
|
templates/SKILL.powershell.md PowerShell 族 SKILL.md 模板({{占位符}} 由 render.js 填充)
|
|
50
62
|
templates/SKILL.unix.md Unix 族 SKILL.md 模板
|
|
63
|
+
test/probe.test.js 单元测试(node --test)
|
|
64
|
+
.github/workflows/ci.yml 三平台(windows/ubuntu/macos)测试与集成验证
|
|
51
65
|
```
|
|
52
66
|
|
|
53
|
-
变更记录由 OpenSpec 管理(`openspec
|
|
67
|
+
变更记录由 OpenSpec 管理(`openspec/`)。
|
|
54
68
|
|
|
55
69
|
## 验证范围
|
|
56
70
|
|
|
57
|
-
-
|
|
58
|
-
-
|
|
71
|
+
- CI:windows-latest / ubuntu-latest / macos-latest 三平台自动运行单元测试与 `init`/`check` 集成验证
|
|
72
|
+
- 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
|
|
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,29 +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 });
|
|
87
|
+
|
|
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
|
+
}
|
|
78
96
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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);
|
|
82
103
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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);
|
|
88
116
|
}
|
|
89
117
|
|
|
90
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.3.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": {
|
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,124 @@
|
|
|
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
|
+
psVersion: shell.psVersion || null,
|
|
28
|
+
psEdition: shell.psEdition || null,
|
|
29
|
+
chcp: shell.chcp ?? null,
|
|
30
|
+
consoleOutputEncoding: shell.consoleOutputEncoding || null,
|
|
31
|
+
outputEncoding: shell.outputEncoding || null,
|
|
32
|
+
locale: (shell.locale && { ...shell.locale }) || null,
|
|
33
|
+
},
|
|
34
|
+
tools,
|
|
35
|
+
project: {
|
|
36
|
+
gitRoot: project.gitRoot || null,
|
|
37
|
+
markers: (project.markers || []).slice().sort(),
|
|
38
|
+
venv: project.venv || null,
|
|
39
|
+
packageManager: project.packageManager || null,
|
|
40
|
+
packageScripts: Object.keys(pkgScripts).length ? { ...pkgScripts } : null,
|
|
41
|
+
cwdPackageScripts: Object.keys(cwdScripts).length ? { ...cwdScripts } : null,
|
|
42
|
+
makefileTargets: (project.makefile && project.makefile.targets) || [],
|
|
43
|
+
compose: (project.compose && project.compose.file) || null,
|
|
44
|
+
readme: (project.readme && project.readme.file) || null,
|
|
45
|
+
subdirs: (project.subdirs || []).slice().sort(),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function writeMeta(dir, env, generatorVersion) {
|
|
51
|
+
const meta = buildMeta(env, generatorVersion);
|
|
52
|
+
fs.writeFileSync(path.join(dir, META_FILENAME), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
|
|
53
|
+
return meta;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function readMeta(dir) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(fs.readFileSync(path.join(dir, META_FILENAME), 'utf8'));
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function eq(a, b) {
|
|
65
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fmt(value) {
|
|
69
|
+
if (value === null || value === undefined) return '无';
|
|
70
|
+
if (Array.isArray(value)) return value.length ? value.join(', ') : '无';
|
|
71
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
72
|
+
return String(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 对比当前环境与快照,返回差异项数组(空数组 = 无漂移)
|
|
76
|
+
export function compareEnv(env, meta) {
|
|
77
|
+
const drift = [];
|
|
78
|
+
const shell = env.shell || {};
|
|
79
|
+
const project = env.project || {};
|
|
80
|
+
const tools = {};
|
|
81
|
+
for (const t of env.tools || []) tools[t.name] = t.version;
|
|
82
|
+
|
|
83
|
+
const expectShell = meta.shell || {};
|
|
84
|
+
const shellChecks = [
|
|
85
|
+
['平台', env.platform, meta.platform],
|
|
86
|
+
['OS 版本', (env.os && env.os.release) || null, meta.osRelease || null],
|
|
87
|
+
['架构', (env.os && env.os.arch) || null, meta.arch || null],
|
|
88
|
+
['默认 shell', shell.defaultShell || null, expectShell.defaultShell || null],
|
|
89
|
+
['PowerShell 版本', shell.psVersion || null, expectShell.psVersion || null],
|
|
90
|
+
['chcp', shell.chcp ?? null, expectShell.chcp ?? null],
|
|
91
|
+
['控制台输出编码', shell.consoleOutputEncoding || null, expectShell.consoleOutputEncoding || null],
|
|
92
|
+
['$OutputEncoding', shell.outputEncoding || null, expectShell.outputEncoding || null],
|
|
93
|
+
];
|
|
94
|
+
for (const [label, cur, old] of shellChecks) {
|
|
95
|
+
if (!eq(cur, old)) drift.push(`${label}: ${fmt(old)} → ${fmt(cur)}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const curLocale = shell.locale || {};
|
|
99
|
+
const oldLocale = expectShell.locale || {};
|
|
100
|
+
if (!eq(curLocale, oldLocale)) drift.push(`locale: ${fmt(oldLocale)} → ${fmt(curLocale)}`);
|
|
101
|
+
|
|
102
|
+
const toolNames = new Set([...Object.keys(meta.tools || {}), ...Object.keys(tools)]);
|
|
103
|
+
for (const name of [...toolNames].sort()) {
|
|
104
|
+
const oldVersion = (meta.tools || {})[name] || null;
|
|
105
|
+
const curVersion = tools[name] || null;
|
|
106
|
+
if (!eq(curVersion, oldVersion)) drift.push(`工具 ${name}: ${fmt(oldVersion)} → ${fmt(curVersion)}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const oldProject = meta.project || {};
|
|
110
|
+
const projectChecks = [
|
|
111
|
+
['git 根', project.gitRoot || null, oldProject.gitRoot || null],
|
|
112
|
+
['标记文件', (project.markers || []).slice().sort(), (oldProject.markers || []).slice().sort()],
|
|
113
|
+
['venv', project.venv || null, oldProject.venv || null],
|
|
114
|
+
['包管理器', project.packageManager || null, oldProject.packageManager || null],
|
|
115
|
+
['package.json scripts', Object.keys(project.packageScripts || {}).length ? project.packageScripts : null, oldProject.packageScripts || null],
|
|
116
|
+
['Makefile 目标', (project.makefile && project.makefile.targets) || [], oldProject.makefileTargets || []],
|
|
117
|
+
['Docker Compose', (project.compose && project.compose.file) || null, oldProject.compose || null],
|
|
118
|
+
];
|
|
119
|
+
for (const [label, cur, old] of projectChecks) {
|
|
120
|
+
if (!eq(cur, old)) drift.push(`${label}: ${fmt(old)} → ${fmt(cur)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return drift;
|
|
124
|
+
}
|
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
|
}
|
|
@@ -131,20 +148,22 @@ function runDeepScript(file, args, cwd) {
|
|
|
131
148
|
});
|
|
132
149
|
}
|
|
133
150
|
|
|
134
|
-
// 工具探测:PATH 遍历 +
|
|
151
|
+
// 工具探测:PATH 遍历 + 版本命令,全部容错;版本探测并行执行,整体耗时受单工具超时上限约束
|
|
135
152
|
async function probeTools() {
|
|
136
|
-
const
|
|
153
|
+
const paths = new Map();
|
|
137
154
|
for (const name of TOOL_NAMES) {
|
|
138
155
|
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 });
|
|
156
|
+
if (toolPath) paths.set(name, toolPath);
|
|
146
157
|
}
|
|
147
|
-
|
|
158
|
+
const entries = await Promise.all(
|
|
159
|
+
[...paths.entries()].map(async ([name, toolPath]) => {
|
|
160
|
+
const rawVersion = await runVersionCommand(toolPath);
|
|
161
|
+
return { name, version: isPlausibleVersion(rawVersion) ? rawVersion : null, source: toolPath };
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
165
|
+
// 保持 TOOL_NAMES 顺序输出,缺失工具 version 为 null
|
|
166
|
+
return TOOL_NAMES.map((name) => byName.get(name) ?? { name, version: null, source: null });
|
|
148
167
|
}
|
|
149
168
|
|
|
150
169
|
function findToolPath(name) {
|
|
@@ -196,16 +215,30 @@ function runVersionCommand(toolPath) {
|
|
|
196
215
|
});
|
|
197
216
|
}
|
|
198
217
|
|
|
199
|
-
// 项目上下文:全部 Node fs
|
|
200
|
-
|
|
218
|
+
// 项目上下文:全部 Node fs 实现;以项目根(git 根 ?? 当前目录)为基准
|
|
219
|
+
// 可选参数 projectRoot 仅供测试注入项目根
|
|
220
|
+
export function probeProject(cwd, projectRoot) {
|
|
221
|
+
const root = projectRoot ?? findProjectRoot(cwd);
|
|
222
|
+
const rootSubdirs = readSubdirs(root);
|
|
201
223
|
const subdirs = readSubdirs(cwd);
|
|
224
|
+
const venv = findVenv(root) ?? findVenv(cwd);
|
|
225
|
+
const packageScripts = readPackageScripts(root);
|
|
226
|
+
const cwdScripts = readPackageScripts(cwd);
|
|
227
|
+
const cwdHasOwn = cwd !== root && Object.keys(cwdScripts).length > 0;
|
|
228
|
+
const makefile = readMakefile(root);
|
|
202
229
|
return {
|
|
203
230
|
cwd,
|
|
231
|
+
projectRoot: root,
|
|
204
232
|
gitRoot: findGitRoot(cwd),
|
|
205
|
-
markers: findMarkers(
|
|
206
|
-
venv
|
|
207
|
-
packageScripts
|
|
208
|
-
|
|
233
|
+
markers: findMarkers(root, rootSubdirs),
|
|
234
|
+
venv,
|
|
235
|
+
packageScripts,
|
|
236
|
+
cwdPackageScripts: cwdHasOwn ? cwdScripts : {},
|
|
237
|
+
packageManager: inferPackageManager(root),
|
|
238
|
+
makefile: makefile ? { file: makefile.file, targets: makefile.targets } : null,
|
|
239
|
+
compose: findFirst(root, COMPOSE_NAMES) ? { file: findFirst(root, COMPOSE_NAMES) } : null,
|
|
240
|
+
readme: findFirst(root, README_NAMES) ? { file: findFirst(root, README_NAMES) } : null,
|
|
241
|
+
subdirs: rootSubdirs.length ? rootSubdirs : subdirs,
|
|
209
242
|
};
|
|
210
243
|
}
|
|
211
244
|
|
|
@@ -220,24 +253,24 @@ function readSubdirs(cwd) {
|
|
|
220
253
|
}
|
|
221
254
|
}
|
|
222
255
|
|
|
223
|
-
function findMarkers(
|
|
256
|
+
function findMarkers(root, subdirs) {
|
|
224
257
|
const found = [];
|
|
225
258
|
const scope = ['.', ...subdirs];
|
|
226
259
|
for (const marker of MARKER_NAMES) {
|
|
227
260
|
for (const dir of scope) {
|
|
228
|
-
const target = dir === '.' ? path.join(
|
|
261
|
+
const target = dir === '.' ? path.join(root, marker) : path.join(root, dir, marker);
|
|
229
262
|
if (fs.existsSync(target)) {
|
|
230
263
|
found.push(dir === '.' ? marker : path.join(dir, marker));
|
|
231
|
-
break; // 每个 marker
|
|
264
|
+
break; // 每个 marker 只记录一次(优先项目根)
|
|
232
265
|
}
|
|
233
266
|
}
|
|
234
267
|
}
|
|
235
268
|
return found;
|
|
236
269
|
}
|
|
237
270
|
|
|
238
|
-
function findVenv(
|
|
239
|
-
for (const
|
|
240
|
-
if (fs.existsSync(path.join(
|
|
271
|
+
function findVenv(dir) {
|
|
272
|
+
for (const name of ['.venv', 'venv']) {
|
|
273
|
+
if (fs.existsSync(path.join(dir, name, 'pyvenv.cfg'))) return name;
|
|
241
274
|
}
|
|
242
275
|
return null;
|
|
243
276
|
}
|
|
@@ -250,4 +283,37 @@ function readPackageScripts(cwd) {
|
|
|
250
283
|
} catch {
|
|
251
284
|
return {};
|
|
252
285
|
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 按 lock 文件推断推荐包管理器;多锁文件时按 LOCK_PRIORITY 取第一个命中的
|
|
289
|
+
export function inferPackageManager(projectRoot) {
|
|
290
|
+
for (const [lock, pm] of LOCK_PRIORITY) {
|
|
291
|
+
if (fs.existsSync(path.join(projectRoot, lock))) return pm;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function readMakefile(dir) {
|
|
297
|
+
for (const name of MAKEFILE_NAMES) {
|
|
298
|
+
const file = path.join(dir, name);
|
|
299
|
+
if (!fs.existsSync(file)) continue;
|
|
300
|
+
const targets = [];
|
|
301
|
+
try {
|
|
302
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
303
|
+
for (const line of text.split(/\r?\n/)) {
|
|
304
|
+
const match = line.match(/^([A-Za-z0-9_.-]+)\s*:/);
|
|
305
|
+
if (match && !MAKEFILE_PSEUDO.has(match[1])) targets.push(match[1]);
|
|
306
|
+
if (targets.length >= MAKEFILE_MAX_TARGETS) break;
|
|
307
|
+
}
|
|
308
|
+
} catch { /* 读取失败时返回空目标 */ }
|
|
309
|
+
return { file: name, targets };
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function findFirst(dir, names) {
|
|
315
|
+
for (const name of names) {
|
|
316
|
+
if (fs.existsSync(path.join(dir, name))) return name;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
253
319
|
}
|
package/src/render.js
CHANGED
|
@@ -13,6 +13,8 @@ const SKILL_TEMPLATES = {
|
|
|
13
13
|
unix: path.join(TEMPLATES_DIR, 'SKILL.unix.md'),
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
17
|
+
|
|
16
18
|
const templateCache = new Map();
|
|
17
19
|
|
|
18
20
|
function loadTemplate(key) {
|
|
@@ -60,10 +62,16 @@ function localeSummary(env) {
|
|
|
60
62
|
return parts.length ? parts.join(', ') : '未获取';
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
function langValue(env) {
|
|
66
|
+
const l = (env.shell && env.shell.locale) || {};
|
|
67
|
+
return l.LANG || l.LC_ALL || '未获取';
|
|
68
|
+
}
|
|
69
|
+
|
|
63
70
|
function projectContextBlock(env) {
|
|
64
71
|
const p = env.project || {};
|
|
65
72
|
const lines = [];
|
|
66
73
|
lines.push(`- 当前目录:\`${p.cwd || '未知'}\``);
|
|
74
|
+
lines.push(`- 项目根:\`${p.projectRoot || '未知'}\``);
|
|
67
75
|
lines.push(`- git 仓库根:${p.gitRoot ? `\`${p.gitRoot}\`` : '未发现 .git(已用当前目录作为根)'}`);
|
|
68
76
|
lines.push(`- venv:${p.venv ? `\`${p.venv}\`` : '未发现'}`);
|
|
69
77
|
const markers = p.markers && p.markers.length ? p.markers.join(', ') : '未发现';
|
|
@@ -72,8 +80,97 @@ function projectContextBlock(env) {
|
|
|
72
80
|
? Object.entries(p.packageScripts).map(([k, v]) => `${k}: ${v}`).join(';')
|
|
73
81
|
: '未发现 package.json scripts';
|
|
74
82
|
lines.push(`- package.json scripts:${scripts}`);
|
|
83
|
+
lines.push(`- 推荐包管理器:${p.packageManager || '未推断'}`);
|
|
84
|
+
const extras = [];
|
|
85
|
+
if (p.readme) extras.push(`README: ${p.readme.file}`);
|
|
86
|
+
if (p.makefile) extras.push(`Makefile: ${p.makefile.file}(${p.makefile.targets.length ? p.makefile.targets.join(', ') : '无目标'})`);
|
|
87
|
+
if (p.compose) extras.push(`Compose: ${p.compose.file}`);
|
|
88
|
+
if (extras.length) lines.push(`- 其它:${extras.join(';')}`);
|
|
75
89
|
const subdirs = p.subdirs && p.subdirs.length ? p.subdirs.join(', ') : '(空)';
|
|
76
|
-
lines.push(`-
|
|
90
|
+
lines.push(`- 项目根第一层子目录:${subdirs}`);
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 项目命令:直接给出本项目"怎么装、怎么跑、怎么测"的推荐命令
|
|
95
|
+
function renderProjectCommands(env) {
|
|
96
|
+
const p = env.project || {};
|
|
97
|
+
const pm = p.packageManager;
|
|
98
|
+
const scripts = p.packageScripts || {};
|
|
99
|
+
const scriptNames = Object.keys(scripts);
|
|
100
|
+
const lines = [];
|
|
101
|
+
const push = (s) => lines.push(`- ${s}`);
|
|
102
|
+
|
|
103
|
+
if (pm === 'uv') {
|
|
104
|
+
push(`推荐包管理器:\`uv\`(检测到 uv.lock)`);
|
|
105
|
+
push(`安装依赖:\`uv sync\``);
|
|
106
|
+
push(`在项目环境中运行:\`uv run <cmd>\``);
|
|
107
|
+
push(`添加依赖:\`uv add <pkg>\``);
|
|
108
|
+
} else if (pm === 'pnpm') {
|
|
109
|
+
push(`推荐包管理器:\`pnpm\`(检测到 pnpm-lock.yaml)`);
|
|
110
|
+
push(`安装依赖:\`pnpm install\``);
|
|
111
|
+
} else if (pm === 'yarn') {
|
|
112
|
+
push(`推荐包管理器:\`yarn\`(检测到 yarn.lock)`);
|
|
113
|
+
push(`安装依赖:\`yarn install\``);
|
|
114
|
+
} else if (pm === 'bun') {
|
|
115
|
+
push(`推荐包管理器:\`bun\`(检测到 bun.lockb / bun.lock)`);
|
|
116
|
+
push(`安装依赖:\`bun install\``);
|
|
117
|
+
} else if (pm === 'npm' || (scriptNames.length > 0 && !pm)) {
|
|
118
|
+
push(`包管理器:\`npm\``);
|
|
119
|
+
push(`安装依赖:\`npm install\``);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (scriptNames.length > 0) {
|
|
123
|
+
const runner = ['pnpm', 'yarn', 'bun'].includes(pm) ? pm : 'npm';
|
|
124
|
+
const labels = { dev: '开发', test: '测试', build: '构建', start: '启动', lint: 'Lint', typecheck: '类型检查' };
|
|
125
|
+
const interesting = scriptNames.filter((name) => ['dev', 'test', 'build', 'start', 'lint', 'typecheck'].includes(name));
|
|
126
|
+
const names = interesting.length > 0 ? interesting : scriptNames.slice(0, 6);
|
|
127
|
+
for (const name of names) {
|
|
128
|
+
const label = labels[name] || '运行';
|
|
129
|
+
const cmd = name === 'test' ? `${runner} test` : `${runner} run ${name}`;
|
|
130
|
+
push(`${label}:\`${cmd}\``);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (p.venv) {
|
|
135
|
+
const sep = p.venv === 'venv' ? 'venv' : '.venv';
|
|
136
|
+
push(`Python venv:\`${p.venv}\`;Windows 激活 \`.\\${sep}\\Scripts\\Activate.ps1\`,Unix 激活 \`source ${sep}/bin/activate\``);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (p.makefile && p.makefile.targets.length > 0) {
|
|
140
|
+
push(`Makefile 目标(${p.makefile.file}):${p.makefile.targets.join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (p.compose) {
|
|
144
|
+
push(`Docker Compose:\`docker compose -f ${p.compose.file} up -d\``);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (p.readme) {
|
|
148
|
+
push(`动手前先读 \`${p.readme.file}\` 了解结构与约定`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (lines.length === 0) {
|
|
152
|
+
lines.push('未探测到可推导的项目命令;先读 README / AGENTS.md 确认项目结构再操作。');
|
|
153
|
+
}
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 代码库检索指引:rg 可用时优先 rg,否则用平台原生搜索
|
|
158
|
+
function renderSearchGuide(env) {
|
|
159
|
+
const family = resolveFamily(env);
|
|
160
|
+
const rg = getTool(env, 'rg');
|
|
161
|
+
const lines = [];
|
|
162
|
+
if (rg && rg.version) {
|
|
163
|
+
lines.push('- 定位定义/引用:`rg -n \'<pattern>\' <dir>`;列文件:`rg --files <dir>`');
|
|
164
|
+
lines.push('- 排除噪音目录:`rg --glob \'!node_modules/**\' --glob \'!.git/**\' \'<pattern>\'`');
|
|
165
|
+
} else if (family === 'powershell') {
|
|
166
|
+
lines.push('- 文本搜索:`Get-ChildItem -Recurse -File | Select-String -Pattern \'<pattern>\'`(范围大时先限定 `-Path` 子目录)');
|
|
167
|
+
lines.push('- 列文件:`Get-ChildItem -Recurse -File | Where-Object { $_.Name -like \'<pattern>\' }`');
|
|
168
|
+
} else {
|
|
169
|
+
lines.push('- 文本搜索:`grep -rn \'<pattern>\' <dir>`;排除噪音:`grep -rn --exclude-dir=node_modules --exclude-dir=.git \'<pattern>\' .`');
|
|
170
|
+
}
|
|
171
|
+
lines.push(family === 'powershell'
|
|
172
|
+
? '- 先读文件再修改:`Get-Content -Path \'<file>\' -Encoding UTF8`'
|
|
173
|
+
: '- 先读文件再修改:`cat <file>`');
|
|
77
174
|
return lines.join('\n');
|
|
78
175
|
}
|
|
79
176
|
|
|
@@ -83,6 +180,7 @@ export function renderSkill(env) {
|
|
|
83
180
|
const os = env.os || {};
|
|
84
181
|
const values = {
|
|
85
182
|
generatedAt: new Date().toISOString(),
|
|
183
|
+
generatorVersion: pkg.version,
|
|
86
184
|
osVersion: `${os.platform || ''} ${os.release || ''}`.trim() || '未知',
|
|
87
185
|
shellName: shell.defaultShellName || shell.defaultShell || (family === 'powershell' ? 'powershell' : 'sh'),
|
|
88
186
|
psVersion: shell.psVersion || '未知',
|
|
@@ -91,11 +189,14 @@ export function renderSkill(env) {
|
|
|
91
189
|
executionPolicySummary: executionPolicySummary(env),
|
|
92
190
|
pwsh7Note: shell.pwsh7Available ? '已检测到 pwsh 7,需要时可优先使用 `pwsh`(语法兼容、性能更好)。' : '',
|
|
93
191
|
localeSummary: localeSummary(env),
|
|
192
|
+
langValue: langValue(env),
|
|
94
193
|
fishNote: shell.defaultShellName === 'fish' ? '注意:默认 shell 是 fish,语法与 bash/zsh 不同(本文以 bash/zsh 为主)。' : '',
|
|
95
194
|
gitRoot: (env.project && env.project.gitRoot) || '未发现 .git',
|
|
96
195
|
cwd: (env.project && env.project.cwd) || process.cwd(),
|
|
97
196
|
toolsSummary: toolsSummary(env),
|
|
98
197
|
projectContext: projectContextBlock(env),
|
|
198
|
+
projectCommands: renderProjectCommands(env),
|
|
199
|
+
searchGuide: renderSearchGuide(env),
|
|
99
200
|
};
|
|
100
201
|
return fill(loadTemplate(family), values);
|
|
101
202
|
}
|
|
@@ -108,7 +209,7 @@ function renderEnvironment(env) {
|
|
|
108
209
|
const lines = [];
|
|
109
210
|
lines.push('# 环境事实(pwsh-guide 探测结果)');
|
|
110
211
|
lines.push('');
|
|
111
|
-
lines.push('> 本文件由 pwsh-guide 生成;环境变化后运行 `pwsh-guide refresh`
|
|
212
|
+
lines.push('> 本文件由 pwsh-guide 生成;环境变化后运行 `pwsh-guide refresh` 更新,运行 `pwsh-guide check` 检测漂移。');
|
|
112
213
|
lines.push('');
|
|
113
214
|
lines.push('## Shell 环境');
|
|
114
215
|
lines.push('');
|
|
@@ -155,21 +256,30 @@ function renderEnvironment(env) {
|
|
|
155
256
|
lines.push('| 项 | 值 |');
|
|
156
257
|
lines.push('| --- | --- |');
|
|
157
258
|
lines.push(`| 当前目录 | ${p.cwd ?? '-'} |`);
|
|
259
|
+
lines.push(`| 项目根 | ${p.projectRoot ?? '-'} |`);
|
|
158
260
|
lines.push(`| git 仓库根 | ${p.gitRoot ?? '未发现 .git'} |`);
|
|
159
261
|
lines.push(`| venv | ${p.venv ?? '未发现'} |`);
|
|
160
262
|
lines.push(`| 标记文件 | ${p.markers && p.markers.length ? p.markers.join(', ') : '未发现'} |`);
|
|
263
|
+
lines.push(`| 推荐包管理器 | ${p.packageManager ?? '未推断'} |`);
|
|
161
264
|
const scriptsText = p.packageScripts && Object.keys(p.packageScripts).length
|
|
162
265
|
? Object.entries(p.packageScripts).map(([k, v]) => `${k}: ${v}`).join('<br>')
|
|
163
266
|
: '未发现 package.json scripts';
|
|
164
267
|
lines.push(`| package.json scripts | ${scriptsText} |`);
|
|
165
|
-
|
|
268
|
+
const cwdScriptsText = p.cwdPackageScripts && Object.keys(p.cwdPackageScripts).length
|
|
269
|
+
? Object.entries(p.cwdPackageScripts).map(([k, v]) => `${k}: ${v}`).join('<br>')
|
|
270
|
+
: '-';
|
|
271
|
+
lines.push(`| 当前目录 scripts | ${cwdScriptsText} |`);
|
|
272
|
+
lines.push(`| README | ${p.readme ? p.readme.file : '未发现'} |`);
|
|
273
|
+
lines.push(`| Makefile | ${p.makefile ? `${p.makefile.file}(目标:${p.makefile.targets.join(', ')})` : '未发现'} |`);
|
|
274
|
+
lines.push(`| Docker Compose | ${p.compose ? p.compose.file : '未发现'} |`);
|
|
275
|
+
lines.push(`| 项目根第一层子目录 | ${p.subdirs && p.subdirs.length ? p.subdirs.join(', ') : '-'} |`);
|
|
166
276
|
return `${lines.join('\n')}\n`;
|
|
167
277
|
}
|
|
168
278
|
|
|
169
279
|
const COMMON_COMMANDS_POWERSHELL = [
|
|
170
280
|
'- 列出目录:`Get-ChildItem -Path . -Force`',
|
|
171
281
|
'- 递归查找文件:`Get-ChildItem -Recurse -Filter \'*.py\'`',
|
|
172
|
-
'- 文本搜索:`
|
|
282
|
+
'- 文本搜索:`Get-ChildItem -Recurse -Filter \'*.py\' | Select-String -Pattern \'关键词\'`',
|
|
173
283
|
'- 读文件:`Get-Content -Path \'<file>\' -Encoding UTF8`',
|
|
174
284
|
'- 写文件:`Set-Content -Path \'<file>\' -Value \'<text>\' -Encoding UTF8`',
|
|
175
285
|
'- 判断路径存在:`Test-Path \'<path>\'`',
|
|
@@ -253,7 +363,7 @@ const TOOL_COMMANDS = {
|
|
|
253
363
|
'- 容器列表:`docker ps`',
|
|
254
364
|
],
|
|
255
365
|
make: [
|
|
256
|
-
'-
|
|
366
|
+
'- 查看目标:`make help`(若定义)或读取 Makefile',
|
|
257
367
|
'- 执行目标:`make <target>`',
|
|
258
368
|
],
|
|
259
369
|
rg: [
|
|
@@ -265,7 +375,7 @@ const TOOL_COMMANDS = {
|
|
|
265
375
|
'- 创建 PR:`gh pr create`',
|
|
266
376
|
],
|
|
267
377
|
pwsh: [
|
|
268
|
-
'- 版本:`pwsh
|
|
378
|
+
'- 版本:`pwsh --version`',
|
|
269
379
|
'- 运行脚本:`pwsh -File <script>.ps1`',
|
|
270
380
|
],
|
|
271
381
|
cargo: [
|
|
@@ -285,6 +395,7 @@ function renderCommands(env) {
|
|
|
285
395
|
lines.push('# 命令模板(按工具)');
|
|
286
396
|
lines.push('');
|
|
287
397
|
lines.push('> 仅列出探测到可用的工具;`<...>` 为占位参数,执行前替换。');
|
|
398
|
+
lines.push('> 本文件为通用模板,未逐条在本机实测;复杂场景执行前先 `--help` 确认参数,破坏性操作先 `-WhatIf` 或展示计划。');
|
|
288
399
|
lines.push('');
|
|
289
400
|
lines.push(family === 'unix' ? '## 通用操作(bash/zsh)' : '## 通用操作(PowerShell 原生,始终可用)');
|
|
290
401
|
for (const c of common) lines.push(c);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pwsh-guide
|
|
3
|
-
description: 本项目的 PowerShell 命令执行指南。需要执行 shell 命令、遇到 PowerShell
|
|
3
|
+
description: 本项目的 PowerShell 命令执行指南。需要执行 shell 命令、遇到 PowerShell 报错、中文乱码或不确定命令语法时使用;含本机环境事实、命令模板、快速自检与失败诊断。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# PowerShell 操作指南(本环境)
|
|
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,7 @@ 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 节设置输入编码。
|
|
27
28
|
|
|
28
29
|
## 3. 常用命令模板
|
|
29
30
|
|
|
@@ -33,8 +34,10 @@ description: 本项目的 PowerShell 命令执行指南。需要执行 shell 命
|
|
|
33
34
|
|
|
34
35
|
## 4. 编码与乱码处理
|
|
35
36
|
|
|
36
|
-
-
|
|
37
|
-
-
|
|
37
|
+
- 输出乱码:`[Console]::OutputEncoding = [System.Text.Encoding]::UTF8`;切换代码页 `chcp 65001`。
|
|
38
|
+
- **输入乱码(中文参数)**:`$OutputEncoding` 默认是 us-ascii,原生命令收到的中文参数会乱码。含中文参数的命令须在同一命令内先设置:
|
|
39
|
+
`[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; $OutputEncoding = [System.Text.Encoding]::UTF8; <cmd>`
|
|
40
|
+
- 写 `.ps1` 时在文件头部设置一次上述两行。
|
|
38
41
|
- 写文件指定编码:`Set-Content -Path <file> -Value <text> -Encoding UTF8`(PS 5.1 会写 BOM)。
|
|
39
42
|
- 读文件乱码:`Get-Content -Path <file> -Encoding UTF8`。
|
|
40
43
|
- 重定向中文输出优先 `Out-File -Encoding UTF8`,避免 `>` 按控制台编码写出。
|
|
@@ -44,19 +47,36 @@ description: 本项目的 PowerShell 命令执行指南。需要执行 shell 命
|
|
|
44
47
|
| 症状 | 常见原因 | 处理 |
|
|
45
48
|
| --- | --- | --- |
|
|
46
49
|
| 命令找不到 | PATH 缺失 / 拼写错 | `Get-Command <name>` 确认;查看 `references/environment.md` 工具表 |
|
|
47
|
-
| 中文乱码 | 代码页与输出编码不匹配 | 按第 4 节切换 UTF-8 |
|
|
50
|
+
| 中文乱码 | 代码页与输出编码不匹配 | 按第 4 节切换 UTF-8(含 $OutputEncoding 输入侧) |
|
|
48
51
|
| 引号/转义报错 | 内联 `-Command` 引号地狱 | 改用 `-File` 脚本或参数数组 |
|
|
49
52
|
| 权限拒绝 | 执行策略 / ACL | `Get-ExecutionPolicy -List`;脚本加 `-ExecutionPolicy Bypass` |
|
|
50
53
|
| 路径找不到 | 空格/中文/反斜杠 | 单引号包裹;先 `Test-Path` |
|
|
51
54
|
| 程序假死 | 交互式命令等待输入 | 加 `-NonInteractive`,或 `echo y | <cmd>` |
|
|
55
|
+
| npm/网络报错 | registry/代理/锁文件不匹配 | `npm config get registry`、`npm ping`;确认包管理器与锁文件一致 |
|
|
56
|
+
| 端口被占用 | 端口冲突 | `Get-NetTCPConnection -LocalPort <port>` 找 PID,确认后 `Stop-Process -Id <pid> -Force` |
|
|
57
|
+
| git 凭据弹窗 | 需要认证 | 先确认凭据;必要时 `$env:GIT_TERMINAL_PROMPT = '0'` 避免挂起 |
|
|
52
58
|
|
|
53
|
-
## 6.
|
|
59
|
+
## 6. 快速自检(环境是否已变化)
|
|
60
|
+
|
|
61
|
+
- 复核 shell:`$PSVersionTable.PSVersion.ToString()` 应为 `{{psVersion}}`;`chcp` 应为 `{{chcp}}`。
|
|
62
|
+
- 复核工具:逐一运行第 3 节每个工具的 `--version`,输出应与列出的版本一致。
|
|
63
|
+
- 规则:任一自检项与期望不符,或同一命令连续失败两次,先运行 `npx pwsh-guide refresh` 并重读本文件,不要继续猜测。
|
|
64
|
+
|
|
65
|
+
## 7. 代码库检索指引
|
|
66
|
+
|
|
67
|
+
{{searchGuide}}
|
|
68
|
+
|
|
69
|
+
## 8. 项目上下文(来自探测)
|
|
70
|
+
|
|
71
|
+
{{projectContext}}
|
|
72
|
+
|
|
73
|
+
## 9. 项目命令(推荐)
|
|
74
|
+
|
|
75
|
+
{{projectCommands}}
|
|
76
|
+
|
|
77
|
+
## 10. 禁止事项
|
|
54
78
|
|
|
55
79
|
- 禁止 bash 语法:`ls -la`、`grep`、`cat`、`rm -rf`、`export`、`&&`/`||`。
|
|
56
80
|
- 删除一律用 `Remove-Item -Path <path> -Recurse -Force`,且先确认路径再执行。
|
|
57
81
|
- 未读文件内容不得修改;不得批量格式化无关代码。
|
|
58
|
-
- 仓库 AGENTS.md 规则优先于本文件。
|
|
59
|
-
|
|
60
|
-
## 7. 项目上下文(来自探测)
|
|
61
|
-
|
|
62
|
-
{{projectContext}}
|
|
82
|
+
- 仓库 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 规则优先于本文件。
|