vault-setup-client/dist/cli.js
2026-01-21 11:37:55 -08:00

286 lines
No EOL
9.1 KiB
JavaScript
Executable file

#!/usr/bin/env node
import { parseArgs } from 'util';
import { existsSync, lstatSync, readlinkSync, accessSync, constants, unlinkSync, symlinkSync } from 'fs';
import { join, resolve } from 'path';
import { platform, homedir } from 'os';
import { execSync } from 'child_process';
async function setupVaultSymlink(projectPath) {
const symlinkPath = join(homedir(), ".vault");
const targetPath = resolve(projectPath, "vault");
try {
console.log(`[vault-setup-client] Setting up vault symlink...`);
console.log(`[vault-setup-client] Symlink: ${symlinkPath}`);
console.log(`[vault-setup-client] Target: ${targetPath}`);
if (!existsSync(targetPath)) {
throw new Error(`Target vault does not exist: ${targetPath}`);
}
if (existsSync(symlinkPath)) {
const stats = lstatSync(symlinkPath);
if (stats.isSymbolicLink()) {
const currentTarget = readlinkSync(symlinkPath);
if (resolve(currentTarget) === targetPath) {
console.log(`[vault-setup-client] \u2705 Symlink already exists and points to correct target`);
return {
success: true,
symlinkPath,
targetPath
};
}
console.log(`[vault-setup-client] Removing old symlink (points to ${currentTarget})`);
unlinkSync(symlinkPath);
} else {
throw new Error(`${symlinkPath} exists but is not a symlink`);
}
}
symlinkSync(targetPath, symlinkPath);
console.log(`[vault-setup-client] \u2705 Symlink created successfully`);
return {
success: true,
symlinkPath,
targetPath
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[vault-setup-client] \u274C Symlink setup failed: ${errorMessage}`);
return {
success: false,
symlinkPath,
targetPath,
error: errorMessage
};
}
}
async function verifyVaultAccess(vaultPath = join(homedir(), ".vault")) {
try {
if (!existsSync(vaultPath)) {
return {
accessible: false,
vaultPath,
isSymlink: false,
error: "Vault does not exist"
};
}
const stats = lstatSync(vaultPath);
const isSymlink = stats.isSymbolicLink();
const targetPath = isSymlink ? readlinkSync(vaultPath) : void 0;
try {
accessSync(vaultPath, constants.R_OK);
} catch {
return {
accessible: false,
vaultPath,
isSymlink,
targetPath,
error: "No read access to vault"
};
}
console.log(`[vault-setup-client] \u2705 Vault is accessible`);
if (isSymlink && targetPath) {
console.log(`[vault-setup-client] Symlink target: ${targetPath}`);
}
return {
accessible: true,
vaultPath,
isSymlink,
targetPath
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
accessible: false,
vaultPath,
isSymlink: false,
error: errorMessage
};
}
}
async function storeInKeychain(secret) {
const { service, account, password } = secret;
try {
if (platform() !== "darwin") {
throw new Error("Keychain is only available on macOS");
}
console.log(`[vault-setup-client] Storing secret in Keychain...`);
console.log(`[vault-setup-client] Service: ${service}`);
console.log(`[vault-setup-client] Account: ${account}`);
execSync(
'security add-generic-password -s "' + service + '" -a "' + account + '" -w "' + password + '" -U',
{ encoding: "utf8", stdio: "pipe" }
);
console.log(`[vault-setup-client] \u2705 Secret stored in Keychain`);
return {
success: true
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[vault-setup-client] \u274C Failed to store in Keychain: ${errorMessage}`);
return {
success: false,
error: errorMessage
};
}
}
async function retrieveFromKeychain(service, account) {
try {
if (platform() !== "darwin") {
throw new Error("Keychain is only available on macOS");
}
console.log(`[vault-setup-client] Retrieving secret from Keychain...`);
console.log(`[vault-setup-client] Service: ${service}`);
console.log(`[vault-setup-client] Account: ${account}`);
const password = execSync(
'security find-generic-password -s "' + service + '" -a "' + account + '" -w',
{ encoding: "utf8", stdio: "pipe" }
).trim();
console.log(`[vault-setup-client] \u2705 Secret retrieved from Keychain`);
return {
success: true,
password
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[vault-setup-client] \u274C Failed to retrieve from Keychain: ${errorMessage}`);
return {
success: false,
error: errorMessage
};
}
}
// src/cli.ts
async function main() {
const { values, positionals } = parseArgs({
options: {
project: { type: "string", short: "p" },
service: { type: "string", short: "s" },
account: { type: "string", short: "a" },
password: { type: "string" },
help: { type: "boolean", short: "h" }
},
allowPositionals: true
});
const command = positionals[0] || "link";
if (values.help || command === "help") {
console.log(`
Usage: vault-setup-client [command] [options]
Commands:
link Create ~/.vault symlink to project vault (default)
verify Verify vault is accessible
keychain-store Store secret in macOS Keychain
keychain-get Retrieve secret from macOS Keychain
Link Options:
-p, --project <path> Path to project containing vault/ (required)
Keychain Store Options:
-s, --service <name> Service name (required)
-a, --account <name> Account name (required)
--password <password> Password to store (required)
Keychain Get Options:
-s, --service <name> Service name (required)
-a, --account <name> Account name (required)
Examples:
# Create vault symlink
vault-setup-client link --project ~/Code/@applications/@lilith/lilith-platform
# Verify vault access
vault-setup-client verify
# Store in Keychain
vault-setup-client keychain-store \\
--service restic-backup \\
--account lilith-platform-workstations \\
--password CWPVvKALTwyJfbdVE3oIq7L8Wc7MH4Pz
# Retrieve from Keychain
vault-setup-client keychain-get \\
--service restic-backup \\
--account lilith-platform-workstations
`);
process.exit(0);
}
try {
switch (command) {
case "link": {
if (!values.project) {
console.error("Error: --project is required for link command");
process.exit(1);
}
const result = await setupVaultSymlink(values.project);
if (result.success) {
console.log("\n\u2705 Vault symlink created successfully!");
console.log(`Symlink: ${result.symlinkPath}`);
console.log(`Target: ${result.targetPath}`);
process.exit(0);
} else {
console.error(`
\u274C Symlink setup failed: ${result.error}`);
process.exit(1);
}
}
case "verify": {
const result = await verifyVaultAccess();
if (result.accessible) {
console.log("\n\u2705 Vault is accessible!");
console.log(`Path: ${result.vaultPath}`);
if (result.isSymlink && result.targetPath) {
console.log(`Symlink target: ${result.targetPath}`);
}
process.exit(0);
} else {
console.error(`
\u274C Vault is not accessible: ${result.error}`);
process.exit(1);
}
}
case "keychain-store": {
if (!values.service || !values.account || !values.password) {
console.error("Error: --service, --account, and --password are required");
process.exit(1);
}
const result = await storeInKeychain({
service: values.service,
account: values.account,
password: values.password
});
if (result.success) {
console.log("\n\u2705 Secret stored in Keychain!");
process.exit(0);
} else {
console.error(`
\u274C Failed to store in Keychain: ${result.error}`);
process.exit(1);
}
}
case "keychain-get": {
if (!values.service || !values.account) {
console.error("Error: --service and --account are required");
process.exit(1);
}
const result = await retrieveFromKeychain(values.service, values.account);
if (result.success && result.password) {
console.log(result.password);
process.exit(0);
} else {
console.error(`
\u274C Failed to retrieve from Keychain: ${result.error}`);
process.exit(1);
}
}
default:
console.error(`Unknown command: ${command}`);
console.error('Run "vault-setup-client 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