restic-setup-server/dist/cli.js
2026-01-21 11:37:40 -08:00

258 lines
No EOL
8.1 KiB
JavaScript
Executable file

#!/usr/bin/env node
import { parseArgs } from 'util';
import { execSync } from 'child_process';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
function generatePassword() {
try {
const password = execSync(
"openssl rand -base64 32 | tr -d '/+=' | head -c 32",
{ encoding: "utf8" }
).trim();
if (password.length !== 32) {
throw new Error(`Generated password length is ${password.length}, expected 32`);
}
return password;
} catch (error) {
throw new Error(`Failed to generate password: ${error instanceof Error ? error.message : String(error)}`);
}
}
// src/lib/deploy.ts
var __dirname$1 = dirname(fileURLToPath(import.meta.url));
var TEMPLATES_DIR = join(__dirname$1, "../templates");
function execSsh(host, command, user = "lilith") {
try {
return execSync(`ssh ${user}@${host} "${command}"`, {
encoding: "utf8",
stdio: "pipe"
}).trim();
} catch (error) {
throw new Error(`SSH command failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
function scpFile(localPath, remotePath, host, user = "lilith") {
try {
execSync(`scp ${localPath} ${user}@${host}:${remotePath}`, {
encoding: "utf8",
stdio: "pipe"
});
} catch (error) {
throw new Error(`SCP failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function deployServer(config) {
const {
host,
dataPath = "/bigdisk/restic-backups",
port = 8e3,
password: providedPassword,
dockerPath = "/bigdisk/restic",
sshUser = "lilith"
} = config;
try {
if (!host) {
throw new Error("Host is required");
}
console.log(`[restic-setup-server] Deploying to ${host}...`);
const password = providedPassword || generatePassword();
console.log(`[restic-setup-server] Using password: ${password.substring(0, 8)}...`);
console.log(`[restic-setup-server] Creating directories...`);
execSsh(
host,
`sudo mkdir -p ${dockerPath} ${dataPath} && sudo chown -R ${sshUser}:${sshUser} ${dockerPath} ${dataPath}`,
sshUser
);
console.log(`[restic-setup-server] Deploying docker-compose.yml...`);
const dockerComposeTemplate = join(TEMPLATES_DIR, "docker-compose.yml");
scpFile(dockerComposeTemplate, `${dockerPath}/docker-compose.yml`, host, sshUser);
console.log(`[restic-setup-server] Starting container...`);
execSsh(host, `cd ${dockerPath} && docker-compose up -d`, sshUser);
const serverUrl = `http://${host}:${port}`;
console.log(`[restic-setup-server] \u2705 Deployment successful: ${serverUrl}`);
return {
success: true,
password,
serverUrl
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[restic-setup-server] \u274C Deployment failed: ${errorMessage}`);
return {
success: false,
password: providedPassword || "",
serverUrl: `http://${host}:${port || 8e3}`,
error: errorMessage
};
}
}
async function verifyServer(host, port = 8e3) {
const serverUrl = `http://${host}:${port}`;
try {
console.log(`[restic-setup-server] Verifying server at ${serverUrl}...`);
try {
const containerStatus = execSync(
`ssh lilith@${host} "docker ps --filter name=restic-rest-server --format '{{.Status}}'"`,
{ encoding: "utf8", stdio: "pipe" }
).trim();
if (!containerStatus || !containerStatus.includes("Up")) {
return {
healthy: false,
serverUrl,
error: "Container is not running"
};
}
console.log(`[restic-setup-server] Container status: ${containerStatus}`);
} catch (error) {
return {
healthy: false,
serverUrl,
error: `Failed to check container: ${error instanceof Error ? error.message : String(error)}`
};
}
try {
execSync(`ssh lilith@${host} "ss -tlnp | grep :${port}"`, {
encoding: "utf8",
stdio: "pipe"
});
console.log(`[restic-setup-server] \u2705 Server is healthy at ${serverUrl}`);
return {
healthy: true,
serverUrl
};
} catch (error) {
return {
healthy: false,
serverUrl,
error: `Port ${port} is not listening`
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[restic-setup-server] \u274C Verification failed: ${errorMessage}`);
return {
healthy: false,
serverUrl,
error: errorMessage
};
}
}
// src/cli.ts
async function main() {
const { values, positionals } = parseArgs({
options: {
host: { type: "string", short: "h" },
port: { type: "string", short: "p" },
password: { type: "string" },
"data-path": { type: "string" },
"docker-path": { type: "string" },
"ssh-user": { type: "string" },
"generate-password": { type: "boolean" },
verify: { type: "boolean" },
help: { type: "boolean" }
},
allowPositionals: true
});
const command = positionals[0] || "deploy";
if (values.help || command === "help") {
console.log(`
Usage: restic-setup-server [command] [options]
Commands:
deploy Deploy restic REST server (default)
verify Verify server is running
generate-password Generate a secure password
Deploy Options:
-h, --host <host> Target host (required)
-p, --port <port> REST API port (default: 8000)
--password <password> Restic password (generated if not provided)
--data-path <path> Data storage path (default: /bigdisk/restic-backups)
--docker-path <path> Docker compose path (default: /bigdisk/restic)
--ssh-user <user> SSH user (default: lilith)
Verify Options:
-h, --host <host> Target host (required)
-p, --port <port> REST API port (default: 8000)
Examples:
# Deploy server
restic-setup-server deploy --host 10.0.0.11
# Deploy with custom password
restic-setup-server deploy --host 10.0.0.11 --password mypassword
# Verify server
restic-setup-server verify --host 10.0.0.11
# Generate password
restic-setup-server generate-password
`);
process.exit(0);
}
try {
switch (command) {
case "deploy": {
if (!values.host) {
console.error("Error: --host is required for deploy command");
process.exit(1);
}
const result = await deployServer({
host: values.host,
port: values.port ? parseInt(values.port, 10) : void 0,
password: values.password,
dataPath: values["data-path"],
dockerPath: values["docker-path"],
sshUser: values["ssh-user"]
});
if (result.success) {
console.log("\n\u2705 Deployment successful!");
console.log(`Server URL: ${result.serverUrl}`);
console.log(`Password: ${result.password}`);
console.log("\nStore the password in your vault for workstation setup.");
process.exit(0);
} else {
console.error(`
\u274C Deployment failed: ${result.error}`);
process.exit(1);
}
}
case "verify": {
if (!values.host) {
console.error("Error: --host is required for verify command");
process.exit(1);
}
const result = await verifyServer(
values.host,
values.port ? parseInt(values.port, 10) : void 0
);
if (result.healthy) {
console.log(`
\u2705 Server is healthy: ${result.serverUrl}`);
process.exit(0);
} else {
console.error(`
\u274C Server is unhealthy: ${result.error}`);
process.exit(1);
}
}
case "generate-password": {
const password = generatePassword();
console.log(password);
process.exit(0);
}
default:
console.error(`Unknown command: ${command}`);
console.error('Run "restic-setup-server help" for usage information');
process.exit(1);
}
} catch (error) {
console.error("\n\u274C Error:", error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
main();
//# sourceMappingURL=cli.js.map
//# sourceMappingURL=cli.js.map