@zicolasjac-ai/remote-readonly-ssh 1.0.4 → 1.1.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/README.md +28 -5
- package/bin/rrs.js +59 -6
- package/package.json +10 -4
- package/skills/rrs/SKILL.md +115 -0
- package/src/setup.js +88 -0
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# rrs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**CLI and Agent Skill for safe read-only SSH inspection via JumpServer.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
让 AI 助手(opencode / Claude Code / Cursor / Windsurf / Codex 等)通过堡垒机白名单强制地在服务器上做只读查询——技术上零写能力,每次调用(含被拒绝的)写入本地审计日志。
|
|
6
6
|
|
|
7
7
|
## 为什么需要它
|
|
8
8
|
|
|
9
9
|
- **技术强制只读**:命令前缀白名单 + 禁止模式 + 参数防注入 + 目标主机白名单,多层校验。
|
|
10
10
|
- **审计留痕**:每次调用(含被拒绝的记录)写入本地审计日志,可随时审查。
|
|
11
11
|
- **零依赖**:只依赖系统自带 `ssh` 客户端,无 npm 依赖树。
|
|
12
|
+
- **AI Agent 友好**:附带 SKILL.md 可一键安装到 opencode / Claude / Cursor / Windsurf / Codex / OpenHands 等 AI 助手的 skills 目录。
|
|
12
13
|
|
|
13
14
|
## 安装
|
|
14
15
|
|
|
@@ -23,10 +24,12 @@ npm install -g @zicolasjac-ai/remote-readonly-ssh
|
|
|
23
24
|
## 快速开始
|
|
24
25
|
|
|
25
26
|
```bash
|
|
26
|
-
|
|
27
|
+
npm install -g @zicolasjac-ai/remote-readonly-ssh
|
|
28
|
+
rrs init # 生成配置模板 ~/.rrs/config.json
|
|
29
|
+
rrs setup skills # 把 SKILL.md 安装到 opencode / claude / agents 等目录
|
|
27
30
|
# 编辑配置: 填 jump_server / private_key_path / target_hosts
|
|
28
|
-
rrs list-tools
|
|
29
|
-
rrs sys-info
|
|
31
|
+
rrs list-tools
|
|
32
|
+
rrs sys-info --json
|
|
30
33
|
```
|
|
31
34
|
|
|
32
35
|
## 用法
|
|
@@ -67,6 +70,26 @@ rrs <tool> [arg] [--host <ip>] [--config <file>] [--json]
|
|
|
67
70
|
|
|
68
71
|
审计日志:`~/.rrs/audit.log`
|
|
69
72
|
|
|
73
|
+
## Agent Skills
|
|
74
|
+
|
|
75
|
+
本工具附带一份 `SKILL.md`,教 AI 助手何时用、怎么用 `rrs`。安装后用 `rrs setup skills` 一键分发到各 agent 的 skills 目录:
|
|
76
|
+
|
|
77
|
+
| `--agent` | 写入路径 |
|
|
78
|
+
|-----------|----------|
|
|
79
|
+
| `opencode` | `~/.config/opencode/skills/rrs/SKILL.md` |
|
|
80
|
+
| `claude` / `cursor` / `windsurf` / `codex` | `~/.claude/skills/rrs/SKILL.md`(Claude skills 兼容) |
|
|
81
|
+
| `agents` / `openhands` | `~/.agents/skills/rrs/SKILL.md` |
|
|
82
|
+
| `all`(默认) | 上述三类目录都写 |
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
rrs setup skills # 安装到全部三类目录
|
|
86
|
+
rrs setup skills --agent opencode # 只写 opencode
|
|
87
|
+
rrs setup skills --dry-run # 仅预览目标路径
|
|
88
|
+
rrs setup uninstall skills # 从全部目录移除
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
卸载 npm 包时会自动尝试清理这些 skill 文件(`preuninstall` 钩子,失败不影响卸载)。
|
|
92
|
+
|
|
70
93
|
## 开发
|
|
71
94
|
|
|
72
95
|
```bash
|
package/bin/rrs.js
CHANGED
|
@@ -8,6 +8,7 @@ const { JumpShell, cleanAnsi } = require("../src/jumpshell");
|
|
|
8
8
|
const {
|
|
9
9
|
validateCmd, validatePath, validateHost, validateArg, validateBin,
|
|
10
10
|
} = require("../src/whitelist");
|
|
11
|
+
const setupSkill = require("../src/setup");
|
|
11
12
|
|
|
12
13
|
const PACKAGE_ROOT = path.join(__dirname, "..");
|
|
13
14
|
|
|
@@ -22,32 +23,83 @@ function printUsage() {
|
|
|
22
23
|
["dir <path>", "列目录(路径白名单)"],
|
|
23
24
|
["bin-version <bin>", "查二进制版本(二进制白名单)"],
|
|
24
25
|
["exec-only <cmd>", "通用只读命令(前缀白名单+禁止模式)"],
|
|
26
|
+
["setup skills [--agent <n>]", "把 SKILL.md 安装到 opencode/claude/agents 等目录"],
|
|
27
|
+
["setup uninstall skills [--agent <n>]", "从上述目录移除 skill"],
|
|
25
28
|
];
|
|
26
|
-
console.log("rrs —
|
|
27
|
-
console.log("用法: rrs <工具> [参数] [--host <ip>] [--config <file>] [--json]
|
|
29
|
+
console.log("rrs — CLI and Agent Skill for safe read-only SSH inspection\n");
|
|
30
|
+
console.log("用法: rrs <工具> [参数] [--host <ip>] [--config <file>] [--json]");
|
|
31
|
+
console.log(" rrs setup skills [--agent opencode|claude|cursor|windsurf|codex|openhands|agents|all] [--dry-run]\n");
|
|
28
32
|
console.log("选项: --json 以 JSON 输出结果(便于脚本/AI 调用); -v/--version 查看版本\n");
|
|
29
33
|
for (const [k, v] of cmds) {
|
|
30
|
-
console.log(` ${k.padEnd(
|
|
34
|
+
console.log(` ${k.padEnd(40)} ${v}`);
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
function parseArgs(argv) {
|
|
35
|
-
const opts = {
|
|
39
|
+
const opts = {
|
|
40
|
+
mode: "tool",
|
|
41
|
+
tool: null,
|
|
42
|
+
arg: null,
|
|
43
|
+
setupCmd: null,
|
|
44
|
+
agent: "all",
|
|
45
|
+
dryRun: false,
|
|
46
|
+
host: null,
|
|
47
|
+
config: null,
|
|
48
|
+
version: false,
|
|
49
|
+
json: false,
|
|
50
|
+
};
|
|
36
51
|
const rest = [];
|
|
37
52
|
for (let i = 0; i < argv.length; i++) {
|
|
38
53
|
const a = argv[i];
|
|
39
54
|
if (a === "--host") opts.host = argv[++i];
|
|
40
55
|
else if (a === "--config") opts.config = argv[++i];
|
|
41
|
-
else if (a === "
|
|
56
|
+
else if (a === "--agent") opts.agent = argv[++i];
|
|
57
|
+
else if (a === "--dry-run") opts.dryRun = true;
|
|
58
|
+
else if (a === "-h" || a === "--help") { opts.mode = "tool"; opts.tool = "help"; }
|
|
42
59
|
else if (a === "-v" || a === "--version") opts.version = true;
|
|
43
60
|
else if (a === "--json") opts.json = true;
|
|
44
61
|
else rest.push(a);
|
|
45
62
|
}
|
|
63
|
+
if (rest[0] === "setup") {
|
|
64
|
+
opts.mode = "setup";
|
|
65
|
+
if (rest[1] === "uninstall") {
|
|
66
|
+
opts.setupCmd = "uninstall";
|
|
67
|
+
if (rest[2] === "skills") opts.setupTarget = "skills";
|
|
68
|
+
} else if (rest[1] === "skills") {
|
|
69
|
+
opts.setupCmd = "install";
|
|
70
|
+
opts.setupTarget = "skills";
|
|
71
|
+
}
|
|
72
|
+
return opts;
|
|
73
|
+
}
|
|
46
74
|
opts.tool = opts.tool || rest[0] || "help";
|
|
47
75
|
opts.arg = rest[1] || null;
|
|
48
76
|
return opts;
|
|
49
77
|
}
|
|
50
78
|
|
|
79
|
+
function runSetup(opts) {
|
|
80
|
+
const agent = opts.agent || "all";
|
|
81
|
+
if (!opts.setupTarget || opts.setupTarget !== "skills") {
|
|
82
|
+
console.error("用法: rrs setup skills [--agent all|opencode|claude|cursor|windsurf|codex|openhands|agents] [--dry-run]");
|
|
83
|
+
console.error(" rrs setup uninstall skills [--agent ...] [--dry-run]");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
let results;
|
|
87
|
+
try {
|
|
88
|
+
results = opts.setupCmd === "uninstall"
|
|
89
|
+
? setupSkill.uninstall({ agent, dryRun: opts.dryRun })
|
|
90
|
+
: setupSkill.install(PACKAGE_ROOT, { agent, dryRun: opts.dryRun });
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error(`[setup] 错误: ${e.message}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
const verb = opts.setupCmd === "uninstall" ? "卸载" : "安装";
|
|
96
|
+
console.log(`${verb} skill (agent=${agent}):`);
|
|
97
|
+
for (const r of results) {
|
|
98
|
+
const status = r.action.startsWith("would-") ? "将" : "已";
|
|
99
|
+
console.log(` [${r.agent}] ${status}${r.action.replace("would-", "")} ${r.dir}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
51
103
|
function initConfig() {
|
|
52
104
|
const dir = configDir();
|
|
53
105
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -121,6 +173,7 @@ async function main() {
|
|
|
121
173
|
console.log(require(path.join(PACKAGE_ROOT, "package.json")).version);
|
|
122
174
|
return;
|
|
123
175
|
}
|
|
176
|
+
if (opts.mode === "setup") { runSetup(opts); return; }
|
|
124
177
|
if (opts.tool === "help") { printUsage(); return; }
|
|
125
178
|
if (opts.tool === "init") { initConfig(); return; }
|
|
126
179
|
|
|
@@ -165,4 +218,4 @@ if (require.main === module) {
|
|
|
165
218
|
main();
|
|
166
219
|
}
|
|
167
220
|
|
|
168
|
-
module.exports = { parseArgs, buildCmds, printUsage };
|
|
221
|
+
module.exports = { parseArgs, buildCmds, printUsage, runSetup };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zicolasjac-ai/remote-readonly-ssh",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "只读 SSH 运维 CLI
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "只读 SSH 运维 CLI 与 Agent Skill:通过 JumpServer 堡垒机白名单强制执行只读查询;附 SKILL.md 让 opencode / Claude Code / Cursor 等 AI 助手自动发现并正确调用。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
7
|
"rrs": "bin/rrs.js"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
12
12
|
"src/",
|
|
13
|
+
"skills/",
|
|
13
14
|
"config.example.json",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE"
|
|
@@ -26,10 +27,15 @@
|
|
|
26
27
|
"whitelist",
|
|
27
28
|
"audit",
|
|
28
29
|
"security",
|
|
29
|
-
"cli"
|
|
30
|
+
"cli",
|
|
31
|
+
"agent-skill",
|
|
32
|
+
"opencode",
|
|
33
|
+
"claude",
|
|
34
|
+
"ai"
|
|
30
35
|
],
|
|
31
36
|
"dependencies": {},
|
|
32
37
|
"scripts": {
|
|
33
|
-
"test": "node --test"
|
|
38
|
+
"test": "node --test",
|
|
39
|
+
"preuninstall": "node -e \"try { require('./src/setup').uninstall({agent:'all'}) } catch(_) {}\""
|
|
34
40
|
}
|
|
35
41
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rrs
|
|
3
|
+
description: 通过 JumpServer 堡垒机对服务器做白名单强制的只读运维查询(sys-info / proc / ports / dir / read-file / bin-version / exec-only)。技术强制只读、命令前缀白名单 + 禁止模式 + 路径白名单 + 主机白名单多层校验,每次调用(含被拒绝的)都写入本地审计日志。AI Agent 需要做"查看服务器现状"这类操作时调用此 skill;它不会写入任何远程状态。
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: opencode, claude-code, cursor, windsurf, openhands, codex
|
|
6
|
+
metadata:
|
|
7
|
+
audience: ai-agents
|
|
8
|
+
category: ops
|
|
9
|
+
safe: read-only
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# rrs — Read-Only SSH Inspection Skill
|
|
13
|
+
|
|
14
|
+
`rrs`(remote-read-only-ssh)是一个面向 AI Agent 的只读 SSH 运维工具。它通过 JumpServer 风格的堡垒机在白名单目标主机上执行**白名单强制**的只读查询。每次调用(含被拒绝的)都写入本地审计日志。
|
|
15
|
+
|
|
16
|
+
## When to use me
|
|
17
|
+
|
|
18
|
+
当你需要从服务器**获取信息**而不是**修改状态**时调用。典型场景:
|
|
19
|
+
|
|
20
|
+
- 排查时拉主机名 / OS / 内核 / uptime
|
|
21
|
+
- 查某个进程是否在跑(`proc nginx`)
|
|
22
|
+
- 看监听端口(`ports 8080`)
|
|
23
|
+
- 读配置文件(`read-file /etc/nginx/nginx.conf`)
|
|
24
|
+
- 列目录结构(`dir /var/log`)
|
|
25
|
+
- 查二进制版本(`bin-version nginx`)
|
|
26
|
+
- 跑一段自定义只读命令(`exec-only "df -h | head"`)
|
|
27
|
+
|
|
28
|
+
**不要**用它做任何写操作——rm / mv / cp / vi / sed -i / systemctl / 重定向到文件 等一律被拒绝。
|
|
29
|
+
|
|
30
|
+
## Setup(用户一次性)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g @zicolasjac-ai/remote-readonly-ssh
|
|
34
|
+
rrs init # 生成配置模板 ~/.rrs/config.json
|
|
35
|
+
# 编辑配置: jump_server / private_key_path / target_hosts
|
|
36
|
+
rrs setup skills # 把本 skill 安装到 opencode / claude / agents 等目录
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
rrs <tool> [arg] [--host <ip>] [--config <file>] [--json]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**始终加 `--json`**,便于解析。
|
|
46
|
+
|
|
47
|
+
### Tools
|
|
48
|
+
|
|
49
|
+
| 工具 | 用途 | 示例 |
|
|
50
|
+
|------|------|------|
|
|
51
|
+
| `sys-info` | 主机名 / OS / 内核 / uptime | `rrs sys-info --json` |
|
|
52
|
+
| `proc <pattern>` | 进程查询(参数严格校验) | `rrs proc nginx --json` |
|
|
53
|
+
| `ports [filter]` | 监听端口 | `rrs ports 80 --json` |
|
|
54
|
+
| `read-file <path>` | 读文件(路径白名单) | `rrs read-file /etc/nginx/nginx.conf --json` |
|
|
55
|
+
| `dir <path>` | 列目录 | `rrs dir /var/log --json` |
|
|
56
|
+
| `bin-version <bin>` | 查二进制版本/编译参数 | `rrs bin-version nginx --json` |
|
|
57
|
+
| `exec-only <cmd>` | 通用只读命令(白名单校验) | `rrs exec-only "df -h" --json` |
|
|
58
|
+
| `list-tools` | 查看白名单 / 配置 | `rrs list-tools --json` |
|
|
59
|
+
| `init` | 生成配置模板 | `rrs init` |
|
|
60
|
+
|
|
61
|
+
### Options
|
|
62
|
+
|
|
63
|
+
- `--host <ip>`:指定目标主机(必须存在于 `target_hosts` 白名单)
|
|
64
|
+
- `--config <file>`:使用自定义配置文件
|
|
65
|
+
- `--json`:以 JSON 输出
|
|
66
|
+
- `-v` / `--version`:查看版本
|
|
67
|
+
- `-h` / `--help`:查看用法
|
|
68
|
+
|
|
69
|
+
### Return shape
|
|
70
|
+
|
|
71
|
+
成功:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{"ok":true,"tool":"sys-info","host":"10.0.0.1","output":"..."}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
拒绝(参数非法 / 敏感路径 / 命令不在白名单):
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{"ok":false,"tool":"proc","host":"","error":"拒绝: 进程名 含非法字符 [a;b]","kind":"拒绝"}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
错误(连接 / 鉴权 / 超时):
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{"ok":false,"tool":"sys-info","host":"10.0.0.1","error":"等待命令执行完成超时: ...","kind":"错误"}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
退出码:`0`=成功 / `2`=拒绝 / `3`=配置错误 / `1`=其他错误。
|
|
90
|
+
|
|
91
|
+
## Hard rules(不要试图绕过)
|
|
92
|
+
|
|
93
|
+
1. **永远不要**用 `exec-only` 跑写命令(`rm`、`mv`、`sed -i`、`>`、`tee` 等都被白名单拒绝)
|
|
94
|
+
2. **永远不要**用 `read-file` 读 `blocked_paths`(`/etc/passwd`、`/etc/shadow`、`~/.ssh/id_*` 等)
|
|
95
|
+
3. **永远不要**指定未在 `target_hosts` 注册的 IP
|
|
96
|
+
4. **不要**试图把 `;` `&&` `|` `>` `$()` 拼到参数里——所有路径和参数都做严格字符集校验
|
|
97
|
+
|
|
98
|
+
被拒绝的尝试一样会进审计日志。
|
|
99
|
+
|
|
100
|
+
## Audit
|
|
101
|
+
|
|
102
|
+
每次调用(含拒绝)追加写入 `~/.rrs/audit.log`:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
[巡检记录] 2026-08-18 10:30:00
|
|
106
|
+
主机: 10.0.0.1
|
|
107
|
+
巡检项: sys-info
|
|
108
|
+
结果: 成功
|
|
109
|
+
命令明细:
|
|
110
|
+
1. echo ===HOST===; hostname; whoami; id
|
|
111
|
+
读取主机标识与权限
|
|
112
|
+
...
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
如需让审计同时记录命令输出摘要,在 `~/.rrs/config.json` 设置 `"audit": { "include_output": true }`。
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
|
|
7
|
+
const SKILL_NAME = "rrs";
|
|
8
|
+
|
|
9
|
+
const ALIAS_TO_GROUP = {
|
|
10
|
+
opencode: ["opencode"],
|
|
11
|
+
claude: ["claude"],
|
|
12
|
+
agents: ["agents"],
|
|
13
|
+
codex: ["claude"],
|
|
14
|
+
cursor: ["claude"],
|
|
15
|
+
windsurf: ["claude"],
|
|
16
|
+
openhands: ["agents"],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const RELATIVE_DIRS = {
|
|
20
|
+
opencode: [".config", "opencode", "skills"],
|
|
21
|
+
claude: [".claude", "skills"],
|
|
22
|
+
agents: [".agents", "skills"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function listTargets(agent) {
|
|
26
|
+
const a = (agent || "all").toLowerCase();
|
|
27
|
+
if (a === "all") return ["opencode", "claude", "agents"];
|
|
28
|
+
const groups = ALIAS_TO_GROUP[a];
|
|
29
|
+
if (!groups) throw new Error(`未知 --agent [${agent}],支持: all / opencode / claude / cursor / windsurf / codex / openhands / agents`);
|
|
30
|
+
return groups;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function targetFile(agent, home) {
|
|
34
|
+
const segs = RELATIVE_DIRS[agent];
|
|
35
|
+
if (!segs) throw new Error(`内部错误: 未知 agent ${agent}`);
|
|
36
|
+
return path.join(home, ...segs, SKILL_NAME, "SKILL.md");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function skillSrc(packageRoot) {
|
|
40
|
+
return path.join(packageRoot, "skills", SKILL_NAME, "SKILL.md");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function install(packageRoot, opts = {}) {
|
|
44
|
+
const agent = opts.agent || "all";
|
|
45
|
+
const dryRun = !!opts.dryRun;
|
|
46
|
+
const home = opts.home || os.homedir();
|
|
47
|
+
const src = skillSrc(packageRoot);
|
|
48
|
+
if (!fs.existsSync(src)) throw new Error(`缺少 skill 资源: ${src}`);
|
|
49
|
+
const targets = listTargets(agent);
|
|
50
|
+
const results = [];
|
|
51
|
+
for (const t of targets) {
|
|
52
|
+
const dest = targetFile(t, home);
|
|
53
|
+
if (dryRun) {
|
|
54
|
+
results.push({ agent: t, dir: dest, action: "would-write", exists: fs.existsSync(dest) });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
58
|
+
fs.writeFileSync(dest, fs.readFileSync(src, "utf8"), "utf8");
|
|
59
|
+
results.push({ agent: t, dir: dest, action: "written", exists: true });
|
|
60
|
+
}
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function uninstall(opts = {}) {
|
|
65
|
+
const agent = opts.agent || "all";
|
|
66
|
+
const dryRun = !!opts.dryRun;
|
|
67
|
+
const home = opts.home || os.homedir();
|
|
68
|
+
const targets = listTargets(agent);
|
|
69
|
+
const results = [];
|
|
70
|
+
for (const t of targets) {
|
|
71
|
+
const dest = targetFile(t, home);
|
|
72
|
+
const dir = path.dirname(dest);
|
|
73
|
+
if (!fs.existsSync(dest)) {
|
|
74
|
+
results.push({ agent: t, dir: dest, action: "missing", exists: false });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (dryRun) {
|
|
78
|
+
results.push({ agent: t, dir: dest, action: "would-delete", exists: true });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
fs.unlinkSync(dest);
|
|
82
|
+
try { fs.rmdirSync(dir); } catch (_) {}
|
|
83
|
+
results.push({ agent: t, dir: dest, action: "deleted", exists: false });
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { install, uninstall, listTargets, targetFile, skillSrc, ALIAS_TO_GROUP, SKILL_NAME };
|