@sglwsjxh/ffix 1.0.1
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/.github/workflows/build_and_publish.yml +44 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/config/config.example.json +5 -0
- package/dist/main.js +114 -0
- package/package.json +21 -0
- package/src/config.ts +49 -0
- package/src/context.ts +30 -0
- package/src/llm.ts +106 -0
- package/src/main.ts +155 -0
- package/src/shell.ts +184 -0
- package/src/types.ts +26 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Build and Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
packages: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
name: Build and publish
|
|
16
|
+
runs-on: windows-latest
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout repository
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Setup Node.js for npm
|
|
23
|
+
uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: "22"
|
|
26
|
+
registry-url: "https://registry.npmjs.org/"
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: npm install --no-package-lock
|
|
30
|
+
|
|
31
|
+
- name: Build package
|
|
32
|
+
run: npm run build
|
|
33
|
+
|
|
34
|
+
- name: Create GitHub Release
|
|
35
|
+
uses: softprops/action-gh-release@v2
|
|
36
|
+
with:
|
|
37
|
+
generate_release_notes: true
|
|
38
|
+
env:
|
|
39
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
40
|
+
|
|
41
|
+
- name: Publish to npm
|
|
42
|
+
run: npm publish --access public
|
|
43
|
+
env:
|
|
44
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 清木殇
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ffix ? fuck fix !!
|
|
2
|
+
|
|
3
|
+
在 powershell 里敲错命令了? **fuck !!**
|
|
4
|
+
|
|
5
|
+
```powershell
|
|
6
|
+
PS C:\Users\mark3> got branch
|
|
7
|
+
got: 我不认识这个命令
|
|
8
|
+
PS C:\Users\mark3> fuck
|
|
9
|
+
→ 建议执行:git branch
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
按 Enter 就执行了,就是这么简单
|
|
13
|
+
|
|
14
|
+
## 它干了什么
|
|
15
|
+
|
|
16
|
+
你敲错命令 → fuck 拿到报错 → 丢给 AI → 返回修复命令 → 你确认后执行
|
|
17
|
+
|
|
18
|
+
## 安装方法
|
|
19
|
+
|
|
20
|
+
```powershell
|
|
21
|
+
npm i -g @sglwsjxh/ffix@latest
|
|
22
|
+
fuck install
|
|
23
|
+
pwsh
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 使用方法
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
# 敲错命令了
|
|
30
|
+
git brnch
|
|
31
|
+
|
|
32
|
+
# 让 AI 修一下
|
|
33
|
+
fuck
|
|
34
|
+
# → 建议执行:git branch
|
|
35
|
+
# 按 Enter 执行,Ctrl+C 取消
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 运行环境
|
|
39
|
+
|
|
40
|
+
- PowerShell 7
|
|
41
|
+
- Node.js 18+
|
|
42
|
+
- OpenAI 兼容的 api
|
|
43
|
+
|
|
44
|
+
## 从源码开发
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
npm run build # 编译
|
|
48
|
+
npx tsx src/main.ts # 直接跑源码
|
|
49
|
+
```
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{readFile as _,unlink as L}from"fs/promises";import{readFile as $,writeFile as I,mkdir as M}from"fs/promises";import{join as h}from"path";import{homedir as N}from"os";var P=h(N(),".ffix"),c=h(P,"config.json"),p={baseUrl:"",apiKey:"",model:""},R={stderrTailLines:20,timeoutMs:15e3,tempFilePath:"%TEMP%\\fuck_ctx.json"};async function w(){try{await $(c,"utf-8")}catch{await M(P,{recursive:!0}),await I(c,JSON.stringify(p,null,4),"utf-8"),console.log(`\u9996\u6B21\u4F7F\u7528\uFF0C\u8BF7\u914D\u7F6E API \u4FE1\u606F\uFF1A${c}`)}}async function y(){let t=await $(c,"utf-8"),e=JSON.parse(t);if(!e.apiKey)throw new Error(`\u8BF7\u5728 ${c} \u4E2D\u914D\u7F6E apiKey`);return{baseUrl:e.baseUrl||p.baseUrl,apiKey:e.apiKey,model:e.model||p.model}}async function m(){return R}function v(t){let e=process.env.TEMP??process.env.TMPDIR??"";return t.replace(/%TEMP%/g,e)}async function b(){let t=await m(),e=v(t.tempFilePath);return U(e)}async function U(t){try{let e=await _(t,"utf-8"),o=JSON.parse(e);return typeof o.lastCommand!="string"||o.lastCommand.length===0||typeof o.exitCode!="number"?null:o}catch{return null}finally{try{await L(t)}catch{}}}var D=`\u4F60\u662F\u4E00\u4E2A PowerShell 7 \u547D\u4EE4\u4FEE\u590D\u52A9\u624B\uFF0C\u5206\u6790\u5931\u8D25\u547D\u4EE4\u5E76\u7ED9\u51FA\u4FEE\u590D\u547D\u4EE4
|
|
3
|
+
|
|
4
|
+
\u4E25\u683C\u8981\u6C42\uFF1A
|
|
5
|
+
1. \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u591A\u4F59\u6587\u5B57
|
|
6
|
+
2. JSON \u683C\u5F0F\u4E25\u683C\u56FA\u5B9A\u4E3A {"command": "\u4FEE\u590D\u547D\u4EE4", "confidence": "high|medium|low"}
|
|
7
|
+
3. command \u5FC5\u987B\u662F PowerShell 7 \u53EF\u76F4\u63A5\u6267\u884C\u7684\u4E00\u6761\u547D\u4EE4
|
|
8
|
+
4. \u65E0\u6CD5\u4FEE\u590D\u65F6\u8FD4\u56DE {"command": "", "confidence": "low"}
|
|
9
|
+
5. \u4E0D\u8981\u751F\u6210 bash/zsh \u547D\u4EE4
|
|
10
|
+
6. \u4E0D\u8981\u5047\u8BBE\u7528\u6237\u60F3\u5B89\u88C5\u8F6F\u4EF6`;function G(t){return`\u4E0A\u4E00\u6761\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF0C\u4EE5\u4E0B\u662F\u4E0A\u4E0B\u6587\uFF1A
|
|
11
|
+
|
|
12
|
+
\u547D\u4EE4\uFF1A${t.lastCommand}
|
|
13
|
+
\u9000\u51FA\u7801\uFF1A${t.exitCode}
|
|
14
|
+
\u9519\u8BEF\u4FE1\u606F\uFF1A${t.errorOutput}
|
|
15
|
+
\u5F53\u524D\u76EE\u5F55\uFF1A${t.cwd}
|
|
16
|
+
\u64CD\u4F5C\u7CFB\u7EDF\uFF1A${t.os}
|
|
17
|
+
Shell\uFF1A${t.shell}`}async function F(t){try{let[e,o]=await Promise.all([y(),m()]),r=`${e.baseUrl.replace(/\/+$/,"")}/chat/completions`,i=JSON.stringify({model:e.model,messages:[{role:"system",content:D},{role:"user",content:G(t)}],temperature:.2,max_tokens:1024}),s=new AbortController,u=setTimeout(()=>s.abort(),o.timeoutMs),n;try{n=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},body:i,signal:s.signal})}finally{clearTimeout(u)}if(!n.ok){let j=await n.text();return console.error(`[llm] API returned status ${n.status}: ${j}`),null}let d=(await n.json())?.choices?.[0]?.message?.content;if(!d)return console.error("[llm] No content in response"),null;let a;try{a=JSON.parse(d)}catch{return console.error("[llm] Failed to parse LLM response as JSON:",d),null}let f=a.command??a.recommended_command??"";if(!f)return null;let l=String(a.confidence??"");return l&&!["high","medium","low"].includes(l)?{command:f}:{command:f,...l?{confidence:l}:{}}}catch(e){return e instanceof DOMException&&e.name==="AbortError"?console.error("[llm] Request timed out"):console.error("[llm] Request failed:",e),null}}import{readFile as S,writeFile as g,mkdir as J}from"fs/promises";import{join as k}from"path";import{homedir as O}from"os";function E(){return k(O(),"Documents","PowerShell","Microsoft.PowerShell_profile.ps1")}function H(){return`# >>> fuck init >>>
|
|
18
|
+
|
|
19
|
+
# CLI \u5165\u53E3\u8DEF\u5F84\uFF08\u901A\u8FC7 npm root -g \u52A8\u6001\u83B7\u53D6\uFF0C\u4E0D\u5199\u6B7B\u8DEF\u5F84\uFF09
|
|
20
|
+
$Fuck_NodeCli = "$(npm root -g)\\@sglwsjxh\\ffix\\dist\\main.js"
|
|
21
|
+
|
|
22
|
+
# \u91C7\u96C6\u5931\u8D25\u547D\u4EE4\u7684\u4E0A\u4E0B\u6587\u5230\u4E34\u65F6\u6587\u4EF6
|
|
23
|
+
function Write-FuckContext {
|
|
24
|
+
# \u51FD\u6570\u5F00\u5934\u7ACB\u523B\u4FDD\u5B58\u5173\u952E\u72B6\u6001\uFF0C\u907F\u514D\u540E\u7EED\u547D\u4EE4\u8986\u76D6
|
|
25
|
+
$lastSuccess = $?
|
|
26
|
+
$exitCode = $global:LASTEXITCODE
|
|
27
|
+
|
|
28
|
+
# Channel A: \u4F1A\u8BDD\u5386\u53F2\uFF08\u5FEB\u901F\u8DEF\u5F84\uFF09
|
|
29
|
+
$lastCmd = Get-History -Count 1 | Select-Object -ExpandProperty CommandLine -ErrorAction SilentlyContinue
|
|
30
|
+
|
|
31
|
+
# Channel B: PSReadLine \u5386\u53F2\u6587\u4EF6\u56DE\u9000\uFF08\u89E3\u51B3 PS7 \u5F02\u6B65\u5386\u53F2\u5BFC\u81F4 Get-History \u8FD4\u56DE null \u7684\u95EE\u9898\uFF09
|
|
32
|
+
if (-not $lastCmd) {
|
|
33
|
+
try {
|
|
34
|
+
$option = Get-PSReadLineOption -ErrorAction Stop
|
|
35
|
+
$historyPath = $option.HistorySavePath
|
|
36
|
+
if ($historyPath -and (Test-Path $historyPath)) {
|
|
37
|
+
$lines = Get-Content $historyPath -Tail 20 -ErrorAction Stop
|
|
38
|
+
$lastCmd = $lines |
|
|
39
|
+
Where-Object { $_ -and ($_ -notmatch '^s*fuck(s|$)') } |
|
|
40
|
+
Select-Object -Last 1
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
# PSReadLine \u4E0D\u53EF\u7528\u65F6\u9759\u9ED8\u8DF3\u8FC7
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (-not $lastCmd) { return }
|
|
48
|
+
|
|
49
|
+
if ($exitCode -ne 0 -or -not $lastSuccess) {
|
|
50
|
+
$effectiveExitCode = if ($exitCode -ne 0) { $exitCode } else { 1 }
|
|
51
|
+
$errorMsg = if ($Error[0]) { $Error[0].Exception.Message } else { '' }
|
|
52
|
+
$ctx = @{
|
|
53
|
+
lastCommand = $lastCmd
|
|
54
|
+
exitCode = $effectiveExitCode
|
|
55
|
+
errorOutput = $errorMsg
|
|
56
|
+
cwd = (Get-Location).Path
|
|
57
|
+
shell = 'powershell-7'
|
|
58
|
+
os = 'win32'
|
|
59
|
+
timestamp = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ss.fffZ')
|
|
60
|
+
}
|
|
61
|
+
$ctx | ConvertTo-Json -Compress | Out-File -FilePath "$env:TEMP\\fuck_ctx.json" -Encoding utf8
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# \u4FDD\u5B58\u539F\u59CB prompt \u51FD\u6570\uFF0C\u786E\u4FDD\u4E0D\u7834\u574F\u7528\u6237\u81EA\u5B9A\u4E49\u7684 prompt
|
|
66
|
+
$Fuck_OriginalPrompt = \${function:prompt}
|
|
67
|
+
|
|
68
|
+
# \u91CD\u5199 prompt \u51FD\u6570\uFF1A\u5728\u6BCF\u6B21\u663E\u793A\u63D0\u793A\u7B26\u524D\u91C7\u96C6\u4E0A\u4E0B\u6587\uFF0C\u7136\u540E\u6062\u590D\u539F\u59CB\u884C\u4E3A
|
|
69
|
+
function prompt {
|
|
70
|
+
Write-FuckContext
|
|
71
|
+
& $Fuck_OriginalPrompt
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# fuck \u547D\u4EE4\uFF1A\u8BFB\u53D6\u4E0A\u4E0B\u6587 \u2192 \u8C03\u7528 CLI\uFF08\u5E26\u786E\u8BA4\uFF09\u2192 \u6355\u83B7 stdout \u2192 iex \u6267\u884C
|
|
75
|
+
function fuck {
|
|
76
|
+
$ctxPath = "$env:TEMP\\fuck_ctx.json"
|
|
77
|
+
if (-not (Test-Path $ctxPath)) {
|
|
78
|
+
Write-Host "\u6CA1\u6709\u627E\u5230\u4E0A\u4E00\u6761\u547D\u4EE4\u7684\u4E0A\u4E0B\u6587"
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$ctx = Get-Content $ctxPath -Raw | ConvertFrom-Json
|
|
83
|
+
if (-not $ctx) {
|
|
84
|
+
Write-Host "\u6CA1\u6709\u627E\u5230\u4E0A\u4E00\u6761\u547D\u4EE4\u7684\u4E0A\u4E0B\u6587"
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# \u8BFB\u53D6\u540E\u7ACB\u5373\u5220\u9664\uFF0C\u907F\u514D\u88AB\u4E0B\u4E00\u8F6E prompt hook \u8986\u76D6
|
|
89
|
+
Remove-Item $ctxPath -Force -ErrorAction SilentlyContinue
|
|
90
|
+
|
|
91
|
+
$command = & node "$Fuck_NodeCli" --cmd "$($ctx.lastCommand)" --exit-code $ctx.exitCode --error-output "$($ctx.errorOutput)" --cwd "$($ctx.cwd)" --confirm
|
|
92
|
+
|
|
93
|
+
if (-not [string]::IsNullOrWhiteSpace($command)) {
|
|
94
|
+
iex "$command"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
[Console]::ResetColor()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# <<< fuck init <<<`}async function A(){let t=E();await J(k(O(),"Documents","PowerShell"),{recursive:!0});let e="";try{e=await S(t,"utf-8")}catch{}if(e.includes("# >>> fuck init >>>")){console.log("fuck \u5DF2\u7ECF\u5B89\u88C5\u5230 $PROFILE\uFF0C\u8DF3\u8FC7");return}await g(t+".bak",e,"utf-8");let o=H();await g(t,e+(e?`
|
|
101
|
+
`:"")+o,"utf-8"),console.log(`\u5DF2\u5B89\u88C5\u5230 ${t}`)}async function T(){let t=E(),e;try{e=await S(t,"utf-8")}catch{console.log("\u6CA1\u6709\u627E\u5230 $PROFILE");return}let o="# >>> fuck init >>>",r="# <<< fuck init <<<",i=e.indexOf(o);if(i===-1){console.log("\u6CA1\u6709\u627E\u5230 fuck \u6CE8\u5165\u5185\u5BB9");return}let s=e.indexOf(r,i);if(s===-1){console.log("\u9519\u8BEF\uFF1A\u6CE8\u5165\u6807\u8BB0\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u624B\u52A8\u68C0\u67E5 $PROFILE");return}let u=e.substring(0,i),n=e.substring(s+r.length),C=n.startsWith(`\r
|
|
102
|
+
`)?n.substring(2):n.startsWith(`
|
|
103
|
+
`)?n.substring(1):n;await g(t,u+C,"utf-8"),console.log("\u5DF2\u4ECE $PROFILE \u5378\u8F7D")}var x=t=>process.stderr.write(t);function W(){return new Promise(t=>{process.stdin.setRawMode(!0),process.stdin.resume(),process.stdin.once("data",e=>{process.stdin.pause(),process.stdin.setRawMode(!1),t(e.toString())})})}function q(t){let e={json:!1,quiet:!1,confirm:!1};for(let o=0;o<t.length;o++)switch(t[o]){case"install":e.subcommand="install";break;case"uninstall":e.subcommand="uninstall";break;case"--cmd":e.cmd=t[++o];break;case"--exit-code":e.exitCode=Number(t[++o]);break;case"--error-output":e.errorOutput=t[++o];break;case"--cwd":e.cwd=t[++o];break;case"--json":e.json=!0;break;case"--quiet":e.quiet=!0;break;case"--confirm":e.confirm=!0;break}return e}function K(t){return{lastCommand:t.cmd,exitCode:t.exitCode,errorOutput:t.errorOutput??"",cwd:t.cwd??process.cwd(),shell:"powershell-7",os:"win32",timestamp:new Date().toISOString()}}async function B(){let t=q(process.argv.slice(2));if(t.subcommand==="install"){await A();return}if(t.subcommand==="uninstall"){await T();return}await w();let e=null;t.cmd&&t.exitCode!==void 0&&!Number.isNaN(t.exitCode)?e=K(t):e=await b(),e||(console.error("\u6CA1\u6709\u627E\u5230\u4E0A\u4E00\u6761\u547D\u4EE4\u7684\u4E0A\u4E0B\u6587"),process.exit(1));let o=await F(e);if((!o||!o.command)&&(console.error("\u6CA1\u80FD\u627E\u5230\u4FEE\u590D\u65B9\u6848"),process.exit(1)),t.confirm){x(`\u4E0A\u4E00\u6761\u547D\u4EE4\uFF1A${e.lastCommand}
|
|
104
|
+
|
|
105
|
+
`),x(`\x1B[32m\u2726 \u5EFA\u8BAE\u6267\u884C\uFF1A${o.command}\x1B[0m
|
|
106
|
+
|
|
107
|
+
`),x("Enter = \u6267\u884C Ctrl+C = \u53D6\u6D88");try{let r=await W();(r==="\r"||r===`
|
|
108
|
+
`)&&(process.stderr.write(`
|
|
109
|
+
|
|
110
|
+
\x1B[32m> ${o.command}\x1B[0m
|
|
111
|
+
`),process.stdout.write(o.command),process.exit(0)),process.stderr.write(`
|
|
112
|
+
|
|
113
|
+
\u5DF2\u53D6\u6D88
|
|
114
|
+
`),process.exit(1)}catch{process.exit(1)}}t.json?console.log(JSON.stringify({command:o.command,confidence:o.confidence??"medium"})):(t.quiet,console.log(o.command)),process.exit(0)}B().catch(t=>{console.error(t),process.exit(1)});
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sglwsjxh/ffix",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"fuck": "./dist/main.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "tsx src/main.ts",
|
|
10
|
+
"build": "tsup src/main.ts --clean --minify --format esm",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"prepublish": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "*",
|
|
17
|
+
"tsup": "*",
|
|
18
|
+
"tsx": "*",
|
|
19
|
+
"@types/node": "*"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
|
|
5
|
+
import type { UserConfig, AppConfig } from './types.js'
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = join(homedir(), '.ffix')
|
|
8
|
+
const CONFIG_PATH = join(CONFIG_DIR, 'config.json')
|
|
9
|
+
|
|
10
|
+
const DEFAULT_USER_CONFIG: UserConfig = {
|
|
11
|
+
baseUrl: '',
|
|
12
|
+
apiKey: '',
|
|
13
|
+
model: '',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_APP_CONFIG: AppConfig = {
|
|
17
|
+
stderrTailLines: 20,
|
|
18
|
+
timeoutMs: 15_000,
|
|
19
|
+
tempFilePath: '%TEMP%\\fuck_ctx.json',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function ensureConfig(): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
await readFile(CONFIG_PATH, 'utf-8')
|
|
25
|
+
} catch {
|
|
26
|
+
await mkdir(CONFIG_DIR, { recursive: true })
|
|
27
|
+
await writeFile(CONFIG_PATH, JSON.stringify(DEFAULT_USER_CONFIG, null, 4), 'utf-8')
|
|
28
|
+
console.log(`首次使用,请配置 API 信息:${CONFIG_PATH}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function loadUserConfig(): Promise<UserConfig> {
|
|
33
|
+
const raw = await readFile(CONFIG_PATH, 'utf-8')
|
|
34
|
+
const config: UserConfig = JSON.parse(raw)
|
|
35
|
+
|
|
36
|
+
if (!config.apiKey) {
|
|
37
|
+
throw new Error(`请在 ${CONFIG_PATH} 中配置 apiKey`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
baseUrl: config.baseUrl || DEFAULT_USER_CONFIG.baseUrl,
|
|
42
|
+
apiKey: config.apiKey,
|
|
43
|
+
model: config.model || DEFAULT_USER_CONFIG.model,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function loadAppConfig(): Promise<AppConfig> {
|
|
48
|
+
return DEFAULT_APP_CONFIG
|
|
49
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FixContext } from './types.js'
|
|
2
|
+
import { readFile, unlink } from 'node:fs/promises'
|
|
3
|
+
import { loadAppConfig } from './config.js'
|
|
4
|
+
|
|
5
|
+
function resolveTempPath(template: string): string {
|
|
6
|
+
const tempDir = process.env.TEMP ?? process.env.TMPDIR ?? ''
|
|
7
|
+
return template.replace(/%TEMP%/g, tempDir)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function readContext(): Promise<FixContext | null> {
|
|
11
|
+
const appConfig = await loadAppConfig()
|
|
12
|
+
const filePath = resolveTempPath(appConfig.tempFilePath)
|
|
13
|
+
return readContextFromPath(filePath)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function readContextFromPath(filePath: string): Promise<FixContext | null> {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readFile(filePath, 'utf-8')
|
|
19
|
+
const ctx: FixContext = JSON.parse(raw)
|
|
20
|
+
|
|
21
|
+
if (typeof ctx.lastCommand !== 'string' || ctx.lastCommand.length === 0) return null
|
|
22
|
+
if (typeof ctx.exitCode !== 'number') return null
|
|
23
|
+
|
|
24
|
+
return ctx
|
|
25
|
+
} catch {
|
|
26
|
+
return null
|
|
27
|
+
} finally {
|
|
28
|
+
try { await unlink(filePath) } catch { }
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/llm.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { FixContext, FixSuggestion } from './types.js'
|
|
2
|
+
import { loadUserConfig, loadAppConfig } from './config.js'
|
|
3
|
+
|
|
4
|
+
const SYSTEM_PROMPT = `你是一个 PowerShell 7 命令修复助手,分析失败命令并给出修复命令
|
|
5
|
+
|
|
6
|
+
严格要求:
|
|
7
|
+
1. 只返回 JSON,不要多余文字
|
|
8
|
+
2. JSON 格式严格固定为 {"command": "修复命令", "confidence": "high|medium|low"}
|
|
9
|
+
3. command 必须是 PowerShell 7 可直接执行的一条命令
|
|
10
|
+
4. 无法修复时返回 {"command": "", "confidence": "low"}
|
|
11
|
+
5. 不要生成 bash/zsh 命令
|
|
12
|
+
6. 不要假设用户想安装软件`
|
|
13
|
+
|
|
14
|
+
function buildUserPrompt(context: FixContext): string {
|
|
15
|
+
return `上一条命令执行失败,以下是上下文:
|
|
16
|
+
|
|
17
|
+
命令:${context.lastCommand}
|
|
18
|
+
退出码:${context.exitCode}
|
|
19
|
+
错误信息:${context.errorOutput}
|
|
20
|
+
当前目录:${context.cwd}
|
|
21
|
+
操作系统:${context.os}
|
|
22
|
+
Shell:${context.shell}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getFixSuggestion(context: FixContext): Promise<FixSuggestion | null> {
|
|
26
|
+
try {
|
|
27
|
+
const [userConfig, appConfig] = await Promise.all([
|
|
28
|
+
loadUserConfig(),
|
|
29
|
+
loadAppConfig(),
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
const url = `${userConfig.baseUrl.replace(/\/+$/, '')}/chat/completions`
|
|
33
|
+
|
|
34
|
+
const body = JSON.stringify({
|
|
35
|
+
model: userConfig.model,
|
|
36
|
+
messages: [
|
|
37
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
38
|
+
{ role: 'user', content: buildUserPrompt(context) },
|
|
39
|
+
],
|
|
40
|
+
temperature: 0.2,
|
|
41
|
+
max_tokens: 1024,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
const timeoutId = setTimeout(() => controller.abort(), appConfig.timeoutMs)
|
|
46
|
+
|
|
47
|
+
let response: Response
|
|
48
|
+
try {
|
|
49
|
+
response = await fetch(url, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'Authorization': `Bearer ${userConfig.apiKey}`,
|
|
54
|
+
},
|
|
55
|
+
body,
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
})
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timeoutId)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const responseBody = await response.text()
|
|
64
|
+
console.error(`[llm] API returned status ${response.status}: ${responseBody}`)
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const json = await response.json() as { choices?: Array<{ message?: { content?: string } }> }
|
|
69
|
+
|
|
70
|
+
const content = json?.choices?.[0]?.message?.content
|
|
71
|
+
if (!content) {
|
|
72
|
+
console.error('[llm] No content in response')
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let parsed: Record<string, unknown>
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(content)
|
|
79
|
+
} catch {
|
|
80
|
+
console.error('[llm] Failed to parse LLM response as JSON:', content)
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const cmd = (parsed.command ?? parsed.recommended_command ?? '') as string
|
|
85
|
+
if (!cmd) {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const confidence = String(parsed.confidence ?? '') as FixSuggestion['confidence']
|
|
90
|
+
if (confidence && !['high', 'medium', 'low'].includes(confidence)) {
|
|
91
|
+
return { command: cmd }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
command: cmd,
|
|
96
|
+
...(confidence ? { confidence } : {}),
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
100
|
+
console.error('[llm] Request timed out')
|
|
101
|
+
} else {
|
|
102
|
+
console.error('[llm] Request failed:', err)
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readContext } from './context.js'
|
|
4
|
+
import { getFixSuggestion } from './llm.js'
|
|
5
|
+
import { install, uninstall } from './shell.js'
|
|
6
|
+
import { ensureConfig } from './config.js'
|
|
7
|
+
import type { FixContext } from './types.js'
|
|
8
|
+
|
|
9
|
+
const w = (s: string) => process.stderr.write(s)
|
|
10
|
+
|
|
11
|
+
function keyPress(): Promise<string> {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
process.stdin.setRawMode(true)
|
|
14
|
+
process.stdin.resume()
|
|
15
|
+
process.stdin.once('data', (data) => {
|
|
16
|
+
process.stdin.pause()
|
|
17
|
+
process.stdin.setRawMode(false)
|
|
18
|
+
resolve(data.toString())
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CliArgs {
|
|
24
|
+
subcommand?: 'install' | 'uninstall'
|
|
25
|
+
cmd?: string
|
|
26
|
+
exitCode?: number
|
|
27
|
+
errorOutput?: string
|
|
28
|
+
cwd?: string
|
|
29
|
+
json: boolean
|
|
30
|
+
quiet: boolean
|
|
31
|
+
confirm: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv: string[]): CliArgs {
|
|
35
|
+
const args: CliArgs = { json: false, quiet: false, confirm: false }
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < argv.length; i++) {
|
|
38
|
+
switch (argv[i]) {
|
|
39
|
+
case 'install':
|
|
40
|
+
args.subcommand = 'install'
|
|
41
|
+
break
|
|
42
|
+
case 'uninstall':
|
|
43
|
+
args.subcommand = 'uninstall'
|
|
44
|
+
break
|
|
45
|
+
case '--cmd':
|
|
46
|
+
args.cmd = argv[++i]
|
|
47
|
+
break
|
|
48
|
+
case '--exit-code':
|
|
49
|
+
args.exitCode = Number(argv[++i])
|
|
50
|
+
break
|
|
51
|
+
case '--error-output':
|
|
52
|
+
args.errorOutput = argv[++i]
|
|
53
|
+
break
|
|
54
|
+
case '--cwd':
|
|
55
|
+
args.cwd = argv[++i]
|
|
56
|
+
break
|
|
57
|
+
case '--json':
|
|
58
|
+
args.json = true
|
|
59
|
+
break
|
|
60
|
+
case '--quiet':
|
|
61
|
+
args.quiet = true
|
|
62
|
+
break
|
|
63
|
+
case '--confirm':
|
|
64
|
+
args.confirm = true
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return args
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildContextFromArgs(args: CliArgs): FixContext {
|
|
73
|
+
return {
|
|
74
|
+
lastCommand: args.cmd!,
|
|
75
|
+
exitCode: args.exitCode!,
|
|
76
|
+
errorOutput: args.errorOutput ?? '',
|
|
77
|
+
cwd: args.cwd ?? process.cwd(),
|
|
78
|
+
shell: 'powershell-7',
|
|
79
|
+
os: 'win32',
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function main(): Promise<void> {
|
|
85
|
+
const args = parseArgs(process.argv.slice(2))
|
|
86
|
+
|
|
87
|
+
if (args.subcommand === 'install') {
|
|
88
|
+
await install()
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (args.subcommand === 'uninstall') {
|
|
93
|
+
await uninstall()
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await ensureConfig()
|
|
98
|
+
|
|
99
|
+
let context: FixContext | null = null
|
|
100
|
+
|
|
101
|
+
if (args.cmd && args.exitCode !== undefined && !Number.isNaN(args.exitCode)) {
|
|
102
|
+
context = buildContextFromArgs(args)
|
|
103
|
+
} else {
|
|
104
|
+
context = await readContext()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!context) {
|
|
108
|
+
console.error('没有找到上一条命令的上下文')
|
|
109
|
+
process.exit(1)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const suggestion = await getFixSuggestion(context)
|
|
113
|
+
|
|
114
|
+
if (!suggestion || !suggestion.command) {
|
|
115
|
+
console.error('没能找到修复方案')
|
|
116
|
+
process.exit(1)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (args.confirm) {
|
|
120
|
+
w(`上一条命令:${context.lastCommand}\n\n`)
|
|
121
|
+
w(`\x1b[32m✦ 建议执行:${suggestion.command}\x1b[0m\n\n`)
|
|
122
|
+
w('Enter = 执行 Ctrl+C = 取消')
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const key = await keyPress()
|
|
126
|
+
if (key === '\r' || key === '\n') {
|
|
127
|
+
process.stderr.write(`\n\n\x1b[32m> ${suggestion.command}\x1b[0m\n`)
|
|
128
|
+
process.stdout.write(suggestion.command)
|
|
129
|
+
process.exit(0)
|
|
130
|
+
}
|
|
131
|
+
process.stderr.write('\n\n已取消\n')
|
|
132
|
+
process.exit(1)
|
|
133
|
+
} catch {
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (args.json) {
|
|
139
|
+
console.log(JSON.stringify({
|
|
140
|
+
command: suggestion.command,
|
|
141
|
+
confidence: suggestion.confidence ?? 'medium',
|
|
142
|
+
}))
|
|
143
|
+
} else if (args.quiet) {
|
|
144
|
+
console.log(suggestion.command)
|
|
145
|
+
} else {
|
|
146
|
+
console.log(suggestion.command)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
process.exit(0)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
main().catch((err: unknown) => {
|
|
153
|
+
console.error(err)
|
|
154
|
+
process.exit(1)
|
|
155
|
+
})
|
package/src/shell.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
|
|
5
|
+
function getProfilePath(): string {
|
|
6
|
+
return join(homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 检测当前是否在 PowerShell 7 环境中运行
|
|
11
|
+
* 通过检查 PSModulePath 环境变量是否包含 "PowerShell\" 来判断
|
|
12
|
+
*/
|
|
13
|
+
export async function detectPs7(): Promise<boolean> {
|
|
14
|
+
return (process.env.PSModulePath ?? '').includes('PowerShell\\')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 生成要注入到 $PROFILE 的 PowerShell 脚本
|
|
19
|
+
* 内容包含:
|
|
20
|
+
* - 标记块 # >>> fuck init >>> / # <<< fuck init <<<
|
|
21
|
+
* - $Fuck_NodeCli 变量(指向 CLI 入口路径)
|
|
22
|
+
* - Write-FuckContext 函数(采集失败命令上下文)
|
|
23
|
+
* - prompt 函数重写(渲染前调用 Write-FuckContext)
|
|
24
|
+
* - fuck 命令(读取上下文 → 调用 LLM → 展示建议 → 确认执行)
|
|
25
|
+
*/
|
|
26
|
+
export function generateProfileScript(): string {
|
|
27
|
+
return `# >>> fuck init >>>
|
|
28
|
+
|
|
29
|
+
# CLI 入口路径(通过 npm root -g 动态获取,不写死路径)
|
|
30
|
+
$Fuck_NodeCli = "$(npm root -g)\\@sglwsjxh\\ffix\\dist\\main.js"
|
|
31
|
+
|
|
32
|
+
# 采集失败命令的上下文到临时文件
|
|
33
|
+
function Write-FuckContext {
|
|
34
|
+
# 函数开头立刻保存关键状态,避免后续命令覆盖
|
|
35
|
+
$lastSuccess = $?
|
|
36
|
+
$exitCode = $global:LASTEXITCODE
|
|
37
|
+
|
|
38
|
+
# Channel A: 会话历史(快速路径)
|
|
39
|
+
$lastCmd = Get-History -Count 1 | Select-Object -ExpandProperty CommandLine -ErrorAction SilentlyContinue
|
|
40
|
+
|
|
41
|
+
# Channel B: PSReadLine 历史文件回退(解决 PS7 异步历史导致 Get-History 返回 null 的问题)
|
|
42
|
+
if (-not $lastCmd) {
|
|
43
|
+
try {
|
|
44
|
+
$option = Get-PSReadLineOption -ErrorAction Stop
|
|
45
|
+
$historyPath = $option.HistorySavePath
|
|
46
|
+
if ($historyPath -and (Test-Path $historyPath)) {
|
|
47
|
+
$lines = Get-Content $historyPath -Tail 20 -ErrorAction Stop
|
|
48
|
+
$lastCmd = $lines |
|
|
49
|
+
Where-Object { $_ -and ($_ -notmatch '^\s*fuck(\s|$)') } |
|
|
50
|
+
Select-Object -Last 1
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
# PSReadLine 不可用时静默跳过
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (-not $lastCmd) { return }
|
|
58
|
+
|
|
59
|
+
if ($exitCode -ne 0 -or -not $lastSuccess) {
|
|
60
|
+
$effectiveExitCode = if ($exitCode -ne 0) { $exitCode } else { 1 }
|
|
61
|
+
$errorMsg = if ($Error[0]) { $Error[0].Exception.Message } else { '' }
|
|
62
|
+
$ctx = @{
|
|
63
|
+
lastCommand = $lastCmd
|
|
64
|
+
exitCode = $effectiveExitCode
|
|
65
|
+
errorOutput = $errorMsg
|
|
66
|
+
cwd = (Get-Location).Path
|
|
67
|
+
shell = 'powershell-7'
|
|
68
|
+
os = 'win32'
|
|
69
|
+
timestamp = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ss.fffZ')
|
|
70
|
+
}
|
|
71
|
+
$ctx | ConvertTo-Json -Compress | Out-File -FilePath "$env:TEMP\\fuck_ctx.json" -Encoding utf8
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# 保存原始 prompt 函数,确保不破坏用户自定义的 prompt
|
|
76
|
+
$Fuck_OriginalPrompt = \${function:prompt}
|
|
77
|
+
|
|
78
|
+
# 重写 prompt 函数:在每次显示提示符前采集上下文,然后恢复原始行为
|
|
79
|
+
function prompt {
|
|
80
|
+
Write-FuckContext
|
|
81
|
+
& $Fuck_OriginalPrompt
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# fuck 命令:读取上下文 → 调用 CLI(带确认)→ 捕获 stdout → iex 执行
|
|
85
|
+
function fuck {
|
|
86
|
+
$ctxPath = "$env:TEMP\\fuck_ctx.json"
|
|
87
|
+
if (-not (Test-Path $ctxPath)) {
|
|
88
|
+
Write-Host "没有找到上一条命令的上下文"
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
$ctx = Get-Content $ctxPath -Raw | ConvertFrom-Json
|
|
93
|
+
if (-not $ctx) {
|
|
94
|
+
Write-Host "没有找到上一条命令的上下文"
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# 读取后立即删除,避免被下一轮 prompt hook 覆盖
|
|
99
|
+
Remove-Item $ctxPath -Force -ErrorAction SilentlyContinue
|
|
100
|
+
|
|
101
|
+
$command = & node "$Fuck_NodeCli" --cmd "$($ctx.lastCommand)" --exit-code $ctx.exitCode --error-output "$($ctx.errorOutput)" --cwd "$($ctx.cwd)" --confirm
|
|
102
|
+
|
|
103
|
+
if (-not [string]::IsNullOrWhiteSpace($command)) {
|
|
104
|
+
iex "$command"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
[Console]::ResetColor()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# <<< fuck init <<<`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 将 fuck 注入到当前用户的 $PROFILE 中
|
|
115
|
+
* 先备份原文件,再追加注入脚本
|
|
116
|
+
* 如果已经注入过则跳过
|
|
117
|
+
*/
|
|
118
|
+
export async function install(): Promise<void> {
|
|
119
|
+
const profilePath = getProfilePath()
|
|
120
|
+
|
|
121
|
+
await mkdir(join(homedir(), 'Documents', 'PowerShell'), { recursive: true })
|
|
122
|
+
|
|
123
|
+
let content = ''
|
|
124
|
+
try {
|
|
125
|
+
content = await readFile(profilePath, 'utf-8')
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (content.includes('# >>> fuck init >>>')) {
|
|
130
|
+
console.log('fuck 已经安装到 $PROFILE,跳过')
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await writeFile(profilePath + '.bak', content, 'utf-8')
|
|
135
|
+
|
|
136
|
+
const script = generateProfileScript()
|
|
137
|
+
const separator = content ? '\n' : ''
|
|
138
|
+
await writeFile(profilePath, content + separator + script, 'utf-8')
|
|
139
|
+
|
|
140
|
+
console.log(`已安装到 ${profilePath}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 从 $PROFILE 中卸载 fuck 注入内容
|
|
145
|
+
* 只删除标记块之间的内容,不影响用户其他配置
|
|
146
|
+
*/
|
|
147
|
+
export async function uninstall(): Promise<void> {
|
|
148
|
+
const profilePath = getProfilePath()
|
|
149
|
+
|
|
150
|
+
let content: string
|
|
151
|
+
try {
|
|
152
|
+
content = await readFile(profilePath, 'utf-8')
|
|
153
|
+
} catch {
|
|
154
|
+
console.log('没有找到 $PROFILE')
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const startTag = '# >>> fuck init >>>'
|
|
159
|
+
const endTag = '# <<< fuck init <<<'
|
|
160
|
+
|
|
161
|
+
const startIdx = content.indexOf(startTag)
|
|
162
|
+
if (startIdx === -1) {
|
|
163
|
+
console.log('没有找到 fuck 注入内容')
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const endIdx = content.indexOf(endTag, startIdx)
|
|
168
|
+
if (endIdx === -1) {
|
|
169
|
+
console.log('错误:注入标记不完整,请手动检查 $PROFILE')
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 提取标记块前后的内容,合并回文件
|
|
174
|
+
const before = content.substring(0, startIdx)
|
|
175
|
+
const after = content.substring(endIdx + endTag.length)
|
|
176
|
+
const trimmed = after.startsWith('\r\n')
|
|
177
|
+
? after.substring(2)
|
|
178
|
+
: after.startsWith('\n')
|
|
179
|
+
? after.substring(1)
|
|
180
|
+
: after
|
|
181
|
+
|
|
182
|
+
await writeFile(profilePath, before + trimmed, 'utf-8')
|
|
183
|
+
console.log('已从 $PROFILE 卸载')
|
|
184
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface UserConfig {
|
|
2
|
+
baseUrl: string
|
|
3
|
+
apiKey: string
|
|
4
|
+
model: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface AppConfig {
|
|
8
|
+
stderrTailLines: number
|
|
9
|
+
timeoutMs: number
|
|
10
|
+
tempFilePath: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FixContext {
|
|
14
|
+
lastCommand: string
|
|
15
|
+
exitCode: number
|
|
16
|
+
errorOutput: string
|
|
17
|
+
cwd: string
|
|
18
|
+
shell: 'powershell-7'
|
|
19
|
+
os: 'win32'
|
|
20
|
+
timestamp: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FixSuggestion {
|
|
24
|
+
command: string
|
|
25
|
+
confidence?: 'high' | 'medium' | 'low'
|
|
26
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"types": ["node"],
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|