@huangjunsen/vault-cli
Version:
macOS vault CLI: hide/unhide directories with .noindex and manage encrypted sparsebundles
129 lines (114 loc) • 4.15 kB
JavaScript
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execSync, spawnSync } = require('child_process');
function isMacOS() {
return process.platform === 'darwin';
}
function printInfo(msg) {
console.log(`[i] ${msg}`);
}
function printSuccess(msg) {
console.log(`[✓] ${msg}`);
}
function printError(msg) {
console.error(`[x] ${msg}`);
}
function loadConfig(configPath) {
if (!fs.existsSync(configPath)) {
throw new Error(`Config not found: ${configPath}\nTwo supported formats:\n1) Simple array of paths (recommended):\n[\n "/Users/you/Projects/SecretA",\n "/Users/you/Projects/SecretB"\n]\n2) Advanced targets array (back-compat):\n{\n "targets": [\n { "name": "company", "path": "/Users/you/Company", "mode": "rename_noindex" },\n { "name": "vault", "image": "/Users/you/SecureVault.sparsebundle", "mountpoint": "/Volumes/SecureVault", "mode": "sparsebundle" }\n ]\n}`);
}
const raw = fs.readFileSync(configPath, 'utf8');
let cfg;
try {
cfg = JSON.parse(raw);
} catch (e) {
throw new Error(`Invalid JSON in config: ${configPath}`);
}
// If config is a plain array of strings -> treat as paths
if (Array.isArray(cfg)) {
const targets = cfg.map((p, idx) => {
if (typeof p !== 'string') throw new Error('Path entries must be strings.');
const base = path.basename(p);
const name = base.replace(/[^a-zA-Z0-9_.-]/g, '_') || `t${idx+1}`;
return { name, path: p, mode: 'rename_noindex' };
});
return { targets };
}
// Advanced object form with targets
if (!cfg.targets || !Array.isArray(cfg.targets)) {
throw new Error('Config must be an array of paths OR an object with "targets" array.');
}
for (const t of cfg.targets) {
if (!t.name) throw new Error('Each target must have a "name".');
if (!t.mode) throw new Error(`Target ${t.name} missing "mode".`);
if (t.group && typeof t.group !== 'string') throw new Error(`Target ${t.name} has invalid group; expected string.`);
if (t.mode === 'rename_noindex') {
if (!t.path) throw new Error(`Target ${t.name} needs "path" for rename_noindex mode.`);
} else if (t.mode === 'sparsebundle') {
if (!t.image || !t.mountpoint) {
throw new Error(`Target ${t.name} needs "image" and "mountpoint" for sparsebundle mode.`);
}
} else {
throw new Error(`Unknown mode for target ${t.name}: ${t.mode}`);
}
}
return cfg;
}
function findTarget(targets, nameOrPath) {
const list = targets || [];
const foundByName = list.find(t => t.name === nameOrPath);
if (foundByName) return foundByName;
// Match by exact path if provided
const norm = nameOrPath && path.resolve(nameOrPath);
const foundByPath = list.find(t => t.path && path.resolve(t.path) === norm);
return foundByPath;
}
function filterTargets(targets, { names, group, all }) {
let list = targets || [];
if (all) return list;
if (group) list = list.filter(t => t.group === group);
if (names && names.length) list = list.filter(t => names.includes(t.name));
return list;
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function run(cmd, args = [], opts = {}) {
const res = spawnSync(cmd, args, { stdio: 'pipe', encoding: 'utf8', ...opts });
if (res.error) throw res.error;
if (res.status !== 0) {
const msg = res.stderr || res.stdout || `Command failed: ${cmd} ${args.join(' ')}`;
const err = new Error(msg.trim());
err.code = res.status;
throw err;
}
return res.stdout.trim();
}
function runInherit(cmd, args = [], opts = {}) {
const res = spawnSync(cmd, args, { stdio: 'inherit', ...opts });
if (res.error) throw res.error;
if (res.status !== 0) {
const err = new Error(`Command failed (${res.status}): ${cmd} ${args.join(' ')}`);
err.code = res.status;
throw err;
}
}
function pathExists(p) {
try { fs.accessSync(p); return true; } catch { return false; }
}
module.exports = {
isMacOS,
printInfo,
printSuccess,
printError,
loadConfig,
findTarget,
filterTargets,
ensureDir,
run,
runInherit,
pathExists,
};