286 lines
No EOL
9.1 KiB
JavaScript
Executable file
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
|