@awesomate/hosting-mcp
Version:
Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases
101 lines (92 loc) • 4.02 kB
JavaScript
/**
* Shared account resolver for the awesomate-hosting skill scripts.
*
* Mirrors the MCP server's precedence exactly (mcp/src/config.ts):
* AWESOMATE_ACCOUNT env → .awesomate.json pin (walked up from cwd, stopping
* at $HOME) → sole profile → defaultProfile → legacy v1 top-level fields.
* A pin naming a missing profile is a hard error — never a silent fallback.
*
* Prints shell-eval assignments (single-quoted, quote-stripped values — same
* hardening contract the callers' charclass gates assume):
* node resolve-account.mjs --ssh # ACCT + H/U/P/K
* node resolve-account.mjs --api # ACCT + API/PAT
* Flags combine. Exit 1 = no resolvable account, exit 2 = no shell access.
*/
import { existsSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, dirname, resolve } from 'node:path';
function readJson(path) {
try {
const parsed = JSON.parse(readFileSync(path, 'utf8'));
return parsed && typeof parsed === 'object' ? parsed : null;
} catch { return null; }
}
function fail(msg, code = 1) { console.error(msg); process.exit(code); }
const credPath = join(homedir(), '.awesomate', 'credentials.json');
const file = readJson(credPath) ?? {};
const profiles = {};
if (file.profiles && typeof file.profiles === 'object') {
for (const [key, profile] of Object.entries(file.profiles)) {
if (profile && typeof profile.pat === 'string') profiles[key] = profile;
}
} else if (typeof file.pat === 'string' && file.pat) {
profiles[file.slug || 'default'] = file; // legacy v1 single-account file
}
const names = Object.keys(profiles);
function findPin() {
const explicit = process.env.AWESOMATE_PIN_FILE;
if (explicit) return existsSync(explicit) ? explicit : null;
const home = resolve(homedir());
let dir = resolve(process.cwd());
for (;;) {
const candidate = join(dir, '.awesomate.json');
if (existsSync(candidate)) return candidate;
if (dir === home) return null;
const parent = dirname(dir);
if (parent === dir) return null;
dir = parent;
}
}
let key = null;
if (process.env.AWESOMATE_ACCOUNT) {
key = process.env.AWESOMATE_ACCOUNT;
if (!profiles[key]) {
fail(`AWESOMATE_ACCOUNT="${key}" has no matching profile in ${credPath}. Available: ${names.join(', ') || '(none)'}.`);
}
} else {
const pinPath = findPin();
if (pinPath) {
const pin = readJson(pinPath);
key = pin && typeof pin.account === 'string' ? pin.account : null;
if (!key) fail(`${pinPath} exists but has no "account" field. Expected: {"account": "<slug>"}.`);
if (!profiles[key]) {
fail(
`This folder is pinned to account "${key}" (${pinPath}) but no matching profile exists in ${credPath}. ` +
`Available: ${names.join(', ') || '(none)'}. Connect "${key}" from hub.awesomate.ai/claude (logged in as it) or fix the pin.`,
);
}
} else if (names.length === 1) {
key = names[0];
} else if (file.defaultProfile && profiles[file.defaultProfile]) {
key = file.defaultProfile;
} else if (names.length === 0) {
fail('Not connected. Run the Connect Claude Code setup from hub.awesomate.ai/claude.');
} else {
fail(
`Multiple accounts are connected (${names.join(', ')}) and this folder isn't pinned to one. ` +
'Create .awesomate.json {"account": "<slug>"} in the project root or set AWESOMATE_ACCOUNT.',
);
}
}
const profile = profiles[key];
const q = (s) => "'" + String(s == null ? '' : s).replace(/'/g, '') + "'";
const out = [`ACCT=${q(profile.slug ?? key)}`];
if (process.argv.includes('--ssh')) {
if (!profile.ssh) fail('Your plan does not include shell access (Support Plus+ required).', 2);
out.push(`H=${q(profile.ssh.host)}`, `U=${q(profile.ssh.user)}`, `P=${q(profile.ssh.port || 22)}`, `K=${q(profile.ssh.keyPath)}`);
}
if (process.argv.includes('--api')) {
out.push(`API=${q(profile.apiBase || 'https://hub.awesomate.ai')}`, `PAT=${q(profile.pat)}`);
}
console.log(out.join('; '));