fix
This commit is contained in:
@@ -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 <ProxyForm nav={nav} {...screen.props} />;
|
||||
case "proxy-list":
|
||||
return <ProxyList nav={nav} {...screen.props} />;
|
||||
case "ssh-key":
|
||||
return <SshKeyPrompt nav={nav} {...screen.props} />;
|
||||
case "plan":
|
||||
return <PlanPreview nav={nav} {...screen.props} />;
|
||||
case "run":
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
40
src/screens/SshKeyPrompt.jsx
Normal file
40
src/screens/SshKeyPrompt.jsx
Normal file
@@ -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 (
|
||||
<Box flexDirection="column">
|
||||
<Text>Paste the contents of your <Text bold>.pub</Text> file (single line):</Text>
|
||||
<Text dimColor>It will be appended to ~/.ssh/authorized_keys (dedup'd).</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text>key › </Text>
|
||||
<TextInput value={key} onChange={(v) => { setKey(v); if (error) setError(null); }} />
|
||||
</Box>
|
||||
{error ? (
|
||||
<Box marginTop={1}>
|
||||
<Text color="red">{error}</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Press Enter to continue · Esc to cancel</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user