From d898d98a0ea598b7e4de53847c4f3f4dfcccec39 Mon Sep 17 00:00:00 2001
From: hanruo <552455797@qq.com>
Date: Fri, 22 May 2026 14:29:46 +0800
Subject: [PATCH] fix
---
src/app.jsx | 3 +++
src/lib/runner.js | 29 ++++++++++++++++++++++++++
src/lib/tasks.js | 29 ++++++++++++++++++++++++++
src/screens/MainMenu.jsx | 4 ++++
src/screens/SshKeyPrompt.jsx | 40 ++++++++++++++++++++++++++++++++++++
5 files changed, 105 insertions(+)
create mode 100644 src/screens/SshKeyPrompt.jsx
diff --git a/src/app.jsx b/src/app.jsx
index c87b16a..0c34497 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -9,6 +9,7 @@ import ProxyList from "./screens/ProxyList.jsx";
import PlanPreview from "./screens/PlanPreview.jsx";
import RunLog from "./screens/RunLog.jsx";
import FrpConfigForm from "./screens/FrpConfigForm.jsx";
+import SshKeyPrompt from "./screens/SshKeyPrompt.jsx";
function App() {
const { exit } = useApp();
@@ -67,6 +68,8 @@ function renderScreen(screen, nav) {
return ;
case "proxy-list":
return ;
+ case "ssh-key":
+ return ;
case "plan":
return ;
case "run":
diff --git a/src/lib/runner.js b/src/lib/runner.js
index b2fc2b9..c0b6ac3 100644
--- a/src/lib/runner.js
+++ b/src/lib/runner.js
@@ -95,10 +95,36 @@ function runStep(step, index, emitter, state) {
if (step.kind === "zshrc-plugins") {
return updateZshrcPlugins(step, index, emitter);
}
+ if (step.kind === "ssh-authorized-key") {
+ return appendAuthorizedKey(step, index, emitter);
+ }
return Promise.reject(new Error(`Unknown step kind: ${step.kind}`));
}
+function appendAuthorizedKey(step, index, emitter) {
+ try {
+ fs.mkdirSync(step.sshDir, { recursive: true, mode: 0o700 });
+ try { fs.chmodSync(step.sshDir, 0o700); } catch { /* ignore */ }
+
+ const existing = fs.existsSync(step.path) ? fs.readFileSync(step.path, "utf8") : "";
+ const lines = existing.split(/\r?\n/);
+ if (lines.some((line) => line.trim() === step.key)) {
+ emitter.emit("event", { type: "log", index, stream: "stdout", text: `key already present in ${step.path}` });
+ return Promise.resolve();
+ }
+ const next = existing.endsWith("\n") || existing.length === 0
+ ? `${existing}${step.key}\n`
+ : `${existing}\n${step.key}\n`;
+ fs.writeFileSync(step.path, next, { mode: 0o600 });
+ try { fs.chmodSync(step.path, 0o600); } catch { /* ignore */ }
+ emitter.emit("event", { type: "log", index, stream: "stdout", text: `appended key to ${step.path}` });
+ return Promise.resolve();
+ } catch (error) {
+ return Promise.reject(error);
+ }
+}
+
function runCommand(step, index, emitter, state) {
return new Promise((resolve, reject) => {
const child = spawn(step.command, step.args, {
@@ -240,6 +266,9 @@ function formatStep(step) {
if (step.kind === "zshrc-plugins") {
return `update plugins in ${step.path}`;
}
+ if (step.kind === "ssh-authorized-key") {
+ return `append public key to ${step.path}`;
+ }
return step.label || step.kind;
}
diff --git a/src/lib/tasks.js b/src/lib/tasks.js
index f62c1c9..8e508de 100644
--- a/src/lib/tasks.js
+++ b/src/lib/tasks.js
@@ -50,6 +50,7 @@ function zshInstallPlan() {
run("apt update", "sudo", ["apt", "update"]),
run("apt upgrade", "sudo", ["apt", "upgrade", "-y"]),
run("install zsh & friends", "sudo", ["apt", "install", "zsh", "git", "curl", "wget", "-y"]),
+ run("git credential helper = store", "git", ["config", "--global", "credential.helper", "store"]),
run("change shell to zsh", "sudo", ["chsh", "-s", "/bin/zsh", os.userInfo().username]),
run("install oh-my-zsh", "sh", ["-c", OH_MY_ZSH_COMMAND]),
run("install nvm + node LTS", "bash", ["-c", NVM_INSTALL_COMMAND]),
@@ -77,6 +78,33 @@ function gitPluginStep(name, repo, destination) {
return run(`clone ${name}`, "git", ["clone", repo, destination]);
}
+function sshAuthorizedKeyPlan(rawKey) {
+ const key = (rawKey || "").trim();
+ if (!key) {
+ throw new Error("Public key is required.");
+ }
+ if (/\r|\n/.test(key)) {
+ throw new Error("Public key must be a single line.");
+ }
+ if (!/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-\S+|sk-(ssh-ed25519|ecdsa-sha2-nistp256)@openssh\.com)\s+\S+/.test(key)) {
+ throw new Error("That does not look like an OpenSSH public key.");
+ }
+
+ const home = os.homedir();
+ return {
+ title: "Append public key to ~/.ssh/authorized_keys",
+ steps: [
+ {
+ kind: "ssh-authorized-key",
+ label: "append key to authorized_keys",
+ sshDir: path.join(home, ".ssh"),
+ path: path.join(home, ".ssh", "authorized_keys"),
+ key,
+ },
+ ],
+ };
+}
+
function sshInstallPlan() {
return {
title: "Install OpenSSH server",
@@ -318,6 +346,7 @@ function writeFile(label, filePath, content) {
module.exports = {
zshInstallPlan,
sshInstallPlan,
+ sshAuthorizedKeyPlan,
frpInstallPlan,
frpInitConfigPlan,
frpAddProxyPlan,
diff --git a/src/screens/MainMenu.jsx b/src/screens/MainMenu.jsx
index 44ed020..9ea650b 100644
--- a/src/screens/MainMenu.jsx
+++ b/src/screens/MainMenu.jsx
@@ -8,6 +8,7 @@ export default function MainMenu({ nav }) {
const items = [
{ label: "Install zsh + oh-my-zsh + nvm", value: "zsh" },
{ label: "Install OpenSSH server", value: "ssh" },
+ { label: "Add SSH public key to authorized_keys", value: "ssh-key" },
{ label: "FRP setup ▸", value: "frp" },
{ label: "Bootstrap (zsh + ssh + frp)", value: "bootstrap" },
{ label: "Quit", value: "quit" },
@@ -21,6 +22,9 @@ export default function MainMenu({ nav }) {
case "ssh":
nav.push({ name: "plan", props: { plan: sshInstallPlan(), origin: "main" } });
return;
+ case "ssh-key":
+ nav.push({ name: "ssh-key" });
+ return;
case "frp":
nav.push({ name: "frp" });
return;
diff --git a/src/screens/SshKeyPrompt.jsx b/src/screens/SshKeyPrompt.jsx
new file mode 100644
index 0000000..6109a78
--- /dev/null
+++ b/src/screens/SshKeyPrompt.jsx
@@ -0,0 +1,40 @@
+import React, { useState } from "react";
+import { Box, Text, useInput } from "ink";
+import TextInput from "ink-text-input";
+
+import { sshAuthorizedKeyPlan } from "../lib/tasks.js";
+
+export default function SshKeyPrompt({ nav }) {
+ const [key, setKey] = useState("");
+ const [error, setError] = useState(null);
+
+ useInput((input, k) => {
+ if (k.return && key.trim().length > 0) {
+ try {
+ const plan = sshAuthorizedKeyPlan(key);
+ nav.replace({ name: "plan", props: { plan, origin: "main" } });
+ } catch (err) {
+ setError(err.message);
+ }
+ }
+ });
+
+ return (
+
+ Paste the contents of your .pub file (single line):
+ It will be appended to ~/.ssh/authorized_keys (dedup'd).
+
+ key ›
+ { setKey(v); if (error) setError(null); }} />
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+ Press Enter to continue · Esc to cancel
+
+
+ );
+}