fix
This commit is contained in:
@@ -9,6 +9,7 @@ import ProxyList from "./screens/ProxyList.jsx";
|
|||||||
import PlanPreview from "./screens/PlanPreview.jsx";
|
import PlanPreview from "./screens/PlanPreview.jsx";
|
||||||
import RunLog from "./screens/RunLog.jsx";
|
import RunLog from "./screens/RunLog.jsx";
|
||||||
import FrpConfigForm from "./screens/FrpConfigForm.jsx";
|
import FrpConfigForm from "./screens/FrpConfigForm.jsx";
|
||||||
|
import SshKeyPrompt from "./screens/SshKeyPrompt.jsx";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { exit } = useApp();
|
const { exit } = useApp();
|
||||||
@@ -67,6 +68,8 @@ function renderScreen(screen, nav) {
|
|||||||
return <ProxyForm nav={nav} {...screen.props} />;
|
return <ProxyForm nav={nav} {...screen.props} />;
|
||||||
case "proxy-list":
|
case "proxy-list":
|
||||||
return <ProxyList nav={nav} {...screen.props} />;
|
return <ProxyList nav={nav} {...screen.props} />;
|
||||||
|
case "ssh-key":
|
||||||
|
return <SshKeyPrompt nav={nav} {...screen.props} />;
|
||||||
case "plan":
|
case "plan":
|
||||||
return <PlanPreview nav={nav} {...screen.props} />;
|
return <PlanPreview nav={nav} {...screen.props} />;
|
||||||
case "run":
|
case "run":
|
||||||
|
|||||||
@@ -95,10 +95,36 @@ function runStep(step, index, emitter, state) {
|
|||||||
if (step.kind === "zshrc-plugins") {
|
if (step.kind === "zshrc-plugins") {
|
||||||
return updateZshrcPlugins(step, index, emitter);
|
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}`));
|
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) {
|
function runCommand(step, index, emitter, state) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(step.command, step.args, {
|
const child = spawn(step.command, step.args, {
|
||||||
@@ -240,6 +266,9 @@ function formatStep(step) {
|
|||||||
if (step.kind === "zshrc-plugins") {
|
if (step.kind === "zshrc-plugins") {
|
||||||
return `update plugins in ${step.path}`;
|
return `update plugins in ${step.path}`;
|
||||||
}
|
}
|
||||||
|
if (step.kind === "ssh-authorized-key") {
|
||||||
|
return `append public key to ${step.path}`;
|
||||||
|
}
|
||||||
return step.label || step.kind;
|
return step.label || step.kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ function zshInstallPlan() {
|
|||||||
run("apt update", "sudo", ["apt", "update"]),
|
run("apt update", "sudo", ["apt", "update"]),
|
||||||
run("apt upgrade", "sudo", ["apt", "upgrade", "-y"]),
|
run("apt upgrade", "sudo", ["apt", "upgrade", "-y"]),
|
||||||
run("install zsh & friends", "sudo", ["apt", "install", "zsh", "git", "curl", "wget", "-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("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 oh-my-zsh", "sh", ["-c", OH_MY_ZSH_COMMAND]),
|
||||||
run("install nvm + node LTS", "bash", ["-c", NVM_INSTALL_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]);
|
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() {
|
function sshInstallPlan() {
|
||||||
return {
|
return {
|
||||||
title: "Install OpenSSH server",
|
title: "Install OpenSSH server",
|
||||||
@@ -318,6 +346,7 @@ function writeFile(label, filePath, content) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
zshInstallPlan,
|
zshInstallPlan,
|
||||||
sshInstallPlan,
|
sshInstallPlan,
|
||||||
|
sshAuthorizedKeyPlan,
|
||||||
frpInstallPlan,
|
frpInstallPlan,
|
||||||
frpInitConfigPlan,
|
frpInitConfigPlan,
|
||||||
frpAddProxyPlan,
|
frpAddProxyPlan,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default function MainMenu({ nav }) {
|
|||||||
const items = [
|
const items = [
|
||||||
{ label: "Install zsh + oh-my-zsh + nvm", value: "zsh" },
|
{ label: "Install zsh + oh-my-zsh + nvm", value: "zsh" },
|
||||||
{ label: "Install OpenSSH server", value: "ssh" },
|
{ label: "Install OpenSSH server", value: "ssh" },
|
||||||
|
{ label: "Add SSH public key to authorized_keys", value: "ssh-key" },
|
||||||
{ label: "FRP setup ▸", value: "frp" },
|
{ label: "FRP setup ▸", value: "frp" },
|
||||||
{ label: "Bootstrap (zsh + ssh + frp)", value: "bootstrap" },
|
{ label: "Bootstrap (zsh + ssh + frp)", value: "bootstrap" },
|
||||||
{ label: "Quit", value: "quit" },
|
{ label: "Quit", value: "quit" },
|
||||||
@@ -21,6 +22,9 @@ export default function MainMenu({ nav }) {
|
|||||||
case "ssh":
|
case "ssh":
|
||||||
nav.push({ name: "plan", props: { plan: sshInstallPlan(), origin: "main" } });
|
nav.push({ name: "plan", props: { plan: sshInstallPlan(), origin: "main" } });
|
||||||
return;
|
return;
|
||||||
|
case "ssh-key":
|
||||||
|
nav.push({ name: "ssh-key" });
|
||||||
|
return;
|
||||||
case "frp":
|
case "frp":
|
||||||
nav.push({ name: "frp" });
|
nav.push({ name: "frp" });
|
||||||
return;
|
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