@zicolasjac-ai/remote-readonly-ssh 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,19 +19,28 @@ npm install -g @zicolasjac-ai/remote-readonly-ssh
19
19
 
20
20
  要求 Node.js >= 18,系统已装 OpenSSH 客户端(Windows 10+ / Linux / macOS 自带)。
21
21
 
22
+ > **npx 一行命令**:`npx -y @zicolasjac-ai/remote-readonly-ssh init` 也可直接启动初始化向导(无需先 `npm install`)。
23
+ >
22
24
  > 从旧包名 `remote-readonly-ssh`(v1.0.0)升级的用户,请先 `npm uninstall -g remote-readonly-ssh` 再安装,否则会报 `EEXIST: file already exists`。
23
25
 
26
+ > **postinstall 自动引导**:全局安装完成后,TTY 环境下会自动进入初始化向导;非交互式安装(如 CI)会打印三行下一步提示,不会阻塞安装。
27
+
24
28
  ## 快速开始
25
29
 
26
30
  ```bash
27
- npm install -g @zicolasjac-ai/remote-readonly-ssh
28
- rrs init # 生成配置模板 ~/.rrs/config.json
29
- rrs setup skills # 把 SKILL.md 安装到 opencode / claude / agents 等目录
30
- # 编辑配置: 填 jump_server / private_key_path / target_hosts
31
- rrs list-tools
32
- rrs sys-info --json
31
+ npm install -g @zicolasjac-ai/remote-readonly-ssh # 装完自动进向导
32
+ # 或 npx -y @zicolasjac-ai/remote-readonly-ssh init
33
33
  ```
34
34
 
35
+ 向导会依次询问:
36
+
37
+ 1. 是否覆盖已有 `~/.rrs/config.json`(首次安装跳过此步)
38
+ 2. 堡垒机主机 / 端口 / 用户名 / SSH 私钥路径
39
+ 3. 目标主机白名单(每行 `ip=堡垒机搜索关键字`)
40
+ 4. 是否把 SKILL.md 安装到 opencode / Claude / Cursor 等 AI 助手
41
+
42
+ 向导非交互式环境(CI、管道)会静默生成配置模板,不阻塞安装。
43
+
35
44
  ## 用法
36
45
 
37
46
  ```
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const initWizard = require("../src/init");
5
+ const setupSkill = require("../src/setup");
6
+ const path = require("path");
7
+
8
+ const PACKAGE_ROOT = path.join(__dirname, "..");
9
+
10
+ (async () => {
11
+ const tty = process.stdin.isTTY && process.stdout.isTTY;
12
+ if (tty) {
13
+ try {
14
+ await initWizard.runInteractive({ packageRoot: PACKAGE_ROOT });
15
+ } catch (e) {
16
+ console.error(`[rrs postinstall] 向导失败: ${e.message}`);
17
+ console.error("可稍后手动执行: rrs init");
18
+ }
19
+ } else {
20
+ console.log("");
21
+ console.log("rrs 安装完成。下一步:");
22
+ console.log(" rrs init # 交互式初始化 + 安装 AI skill(推荐)");
23
+ console.log(" rrs setup skills # 仅安装 AI skill 到 opencode / claude 等");
24
+ console.log(" rrs list-tools # 查看白名单与配置");
25
+ console.log("");
26
+ }
27
+ })();
package/bin/rrs.js CHANGED
@@ -9,12 +9,13 @@ const {
9
9
  validateCmd, validatePath, validateHost, validateArg, validateBin,
10
10
  } = require("../src/whitelist");
11
11
  const setupSkill = require("../src/setup");
12
+ const initWizard = require("../src/init");
12
13
 
13
14
  const PACKAGE_ROOT = path.join(__dirname, "..");
14
15
 
15
16
  function printUsage() {
16
17
  const cmds = [
17
- ["init", "生成配置模板到 ~/.rrs/config.json"],
18
+ ["init", "交互式初始化向导(TTY 下)或生成配置模板"],
18
19
  ["list-tools", "查看工具、白名单、配置"],
19
20
  ["sys-info", "主机名/OS/内核/uptime"],
20
21
  ["proc <pattern>", "进程查询(防注入参数校验)"],
@@ -101,8 +102,10 @@ function runSetup(opts) {
101
102
  }
102
103
 
103
104
  function initConfig() {
105
+ if (process.stdin.isTTY && process.stdout.isTTY) {
106
+ return initWizard.runInteractive();
107
+ }
104
108
  const dir = configDir();
105
- fs.mkdirSync(dir, { recursive: true });
106
109
  const dest = path.join(dir, "config.json");
107
110
  if (fs.existsSync(dest)) {
108
111
  console.log(`配置已存在: ${dest}(未覆盖,如需重置请手动删除)`);
@@ -218,4 +221,4 @@ if (require.main === module) {
218
221
  main();
219
222
  }
220
223
 
221
- module.exports = { parseArgs, buildCmds, printUsage, runSetup };
224
+ module.exports = { parseArgs, buildCmds, printUsage, runSetup, initConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zicolasjac-ai/remote-readonly-ssh",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "只读 SSH 运维 CLI 与 Agent Skill:通过 JumpServer 堡垒机白名单强制执行只读查询;附 SKILL.md 让 opencode / Claude Code / Cursor 等 AI 助手自动发现并正确调用。",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -36,6 +36,7 @@
36
36
  "dependencies": {},
37
37
  "scripts": {
38
38
  "test": "node --test",
39
+ "postinstall": "node bin/postinstall.js",
39
40
  "preuninstall": "node -e \"try { require('./src/setup').uninstall({agent:'all'}) } catch(_) {}\""
40
41
  }
41
42
  }
package/src/init.js ADDED
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const readline = require("node:readline/promises");
7
+ const setupSkill = require("./setup");
8
+
9
+ const PACKAGE_ROOT = path.join(__dirname, "..");
10
+
11
+ function templatePath() {
12
+ return path.join(PACKAGE_ROOT, "config.example.json");
13
+ }
14
+
15
+ function readTemplate() {
16
+ return JSON.parse(fs.readFileSync(templatePath(), "utf8"));
17
+ }
18
+
19
+ function writeTemplate(destDir) {
20
+ fs.mkdirSync(destDir, { recursive: true });
21
+ const dest = path.join(destDir, "config.json");
22
+ if (fs.existsSync(dest)) return { dest, existed: true };
23
+ fs.writeFileSync(dest, JSON.stringify(readTemplate(), null, 2), "utf8");
24
+ return { dest, existed: false };
25
+ }
26
+
27
+ function parseTargets(text) {
28
+ const out = {};
29
+ for (const line of String(text || "").split(/\r?\n/)) {
30
+ const t = line.trim();
31
+ if (!t || t.startsWith("#")) continue;
32
+ const eq = t.indexOf("=");
33
+ if (eq <= 0) continue;
34
+ const ip = t.slice(0, eq).trim();
35
+ const kw = t.slice(eq + 1).trim();
36
+ if (ip && kw) out[ip] = kw;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function applyAnswers(cfg, answers) {
42
+ const a = answers || {};
43
+ if (a.jumpHost) cfg.jump_server.host = a.jumpHost;
44
+ if (a.jumpPort) cfg.jump_server.port = Number(a.jumpPort) || cfg.jump_server.port;
45
+ if (a.jumpUser) cfg.jump_server.username = a.jumpUser;
46
+ if (a.privateKeyPath) cfg.private_key_path = a.privateKeyPath;
47
+ if (a.targetHosts && Object.keys(a.targetHosts).length) cfg.target_hosts = a.targetHosts;
48
+ return cfg;
49
+ }
50
+
51
+ function defaultAnswersFor(cfg) {
52
+ return {
53
+ jumpHost: cfg.jump_server && cfg.jump_server.host ? "" : "",
54
+ jumpPort: cfg.jump_server && cfg.jump_server.port ? String(cfg.jump_server.port) : "22",
55
+ jumpUser: cfg.jump_server && cfg.jump_server.username ? cfg.jump_server.username : "",
56
+ privateKeyPath: cfg.private_key_path && !cfg.private_key_path.includes("YOURNAME") ? cfg.private_key_path : "",
57
+ targetHosts: cfg.target_hosts || {},
58
+ };
59
+ }
60
+
61
+ async function collectAnswers(rl, cfg, prompt) {
62
+ const ask = async (q, def) => {
63
+ const label = def ? `${q} [${def}]` : q;
64
+ const ans = (await rl.question(`${label}: `)).trim();
65
+ return ans || def || "";
66
+ };
67
+ const usePrompt = prompt && typeof prompt === "object";
68
+ const ans = {};
69
+ ans.jumpHost = usePrompt && prompt.jumpHost
70
+ ? await prompt.jumpHost(ask, cfg)
71
+ : await ask("堡垒机主机 IP / 域名", "");
72
+ if (!ans.jumpHost) {
73
+ throw new Error("跳过:未填写堡垒机主机。请稍后编辑 " + path.join(os.homedir(), ".rrs", "config.json"));
74
+ }
75
+ ans.jumpPort = usePrompt && prompt.jumpPort
76
+ ? await prompt.jumpPort(ask, cfg)
77
+ : await ask("堡垒机端口", "22");
78
+ ans.jumpUser = usePrompt && prompt.jumpUser
79
+ ? await prompt.jumpUser(ask, cfg)
80
+ : await ask("堡垒机用户名", "");
81
+ ans.privateKeyPath = usePrompt && prompt.privateKeyPath
82
+ ? await prompt.privateKeyPath(ask, cfg)
83
+ : await ask("SSH 私钥绝对路径", path.join(os.homedir(), ".ssh", "id_rsa"));
84
+ const targetText = usePrompt && prompt.targetHosts
85
+ ? await prompt.targetHosts(ask, cfg)
86
+ : await ask("目标主机(每行 ip=堡垒机搜索关键字,空行结束)", "");
87
+ ans.targetHosts = parseTargets(targetText);
88
+ return ans;
89
+ }
90
+
91
+ async function runInteractive({ input = process.stdin, output = process.stdout, packageRoot = PACKAGE_ROOT, prompt, installSkills = true, rlFactory } = {}) {
92
+ const destDir = path.join(os.homedir(), ".rrs");
93
+ const dest = path.join(destDir, "config.json");
94
+ const existed = fs.existsSync(dest);
95
+
96
+ const rl = rlFactory
97
+ ? rlFactory()
98
+ : readline.createInterface({ input, output });
99
+ try {
100
+ let overwrite = "n";
101
+ if (existed) {
102
+ const ans = (await rl.question(`检测到已存在配置 ${dest}\n (n) 跳过配置,仅安装 skill (b) 备份后重写 (y) 直接覆盖 [n]: `)).trim().toLowerCase();
103
+ overwrite = ans || "n";
104
+ }
105
+
106
+ if (!existed || overwrite === "y" || overwrite === "b") {
107
+ if (existed && overwrite === "b") {
108
+ fs.copyFileSync(dest, dest + ".bak");
109
+ }
110
+ fs.mkdirSync(destDir, { recursive: true });
111
+ const cfg = readTemplate();
112
+ const answers = await collectAnswers(rl, defaultAnswersFor(cfg), prompt);
113
+ const filled = applyAnswers(cfg, answers);
114
+ fs.writeFileSync(dest, JSON.stringify(filled, null, 2), "utf8");
115
+ output.write(`已写入配置: ${dest}\n`);
116
+ } else {
117
+ output.write(`保留现有配置: ${dest}\n`);
118
+ }
119
+
120
+ let installedSkills = false;
121
+ if (installSkills) {
122
+ const ans = (await rl.question("安装 AI skill 到 opencode / claude / agents 目录? [Y/n]: ")).trim().toLowerCase();
123
+ const install = ans || "y";
124
+ if (install !== "n") {
125
+ const results = setupSkill.install(packageRoot, { agent: "all" });
126
+ for (const r of results) {
127
+ output.write(` [${r.agent}] ${r.action === "written" ? "已安装" : r.action} ${r.dir}\n`);
128
+ }
129
+ installedSkills = true;
130
+ }
131
+ }
132
+
133
+ output.write("\n下一步:\n");
134
+ output.write(" rrs list-tools # 查看白名单与配置\n");
135
+ output.write(" rrs sys-info --json # 试跑一次只读巡检\n");
136
+ return { dest, overwrite, installedSkills };
137
+ } finally {
138
+ if (!rlFactory) rl.close();
139
+ }
140
+ }
141
+
142
+ module.exports = { runInteractive, writeTemplate, readTemplate, parseTargets, applyAnswers, defaultAnswersFor, templatePath };