@awesomate/hosting-mcp 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,18 +52,17 @@ done
52
52
  echo "--from wp-content looks empty (no themes/ or plugins/). Refusing to --delete against live."; exit 1; }
53
53
  case "$DOMAIN" in *[!a-z0-9.-]*|"") echo "Invalid --domain."; exit 1;; esac
54
54
 
55
- # Read connection details + PAT (node is present wherever the MCP runs).
56
- # Capture first, THEN eval so a no-shell plan (node exits 2) aborts here with
57
- # its own message instead of leaving $U/$P/… unset for `set -u` to trip on later.
58
- if ! CREDS_EVAL="$(node -e '
59
- const c = require(process.env.HOME + "/.awesomate/credentials.json");
60
- if (!c.ssh) { console.error("Your plan does not include shell access (Support Plus+ required)."); process.exit(2); }
61
- const q = (s) => "'"'"'" + String(s == null ? "" : s).replace(/'"'"'/g, "") + "'"'"'";
62
- console.log(`H=${q(c.ssh.host)}; U=${q(c.ssh.user)}; P=${q(c.ssh.port||22)}; K=${q(c.ssh.keyPath)}; API=${q(c.apiBase)}; PAT=${q(c.pat)}`);
63
- ')"; then
55
+ # Resolve the account (folder pin / AWESOMATE_ACCOUNT / sole profile) and its
56
+ # connection details + PAT via the shared resolver (node is present wherever
57
+ # the MCP runs). Capture first, THEN eval so a resolver failure (no-shell
58
+ # plan exits 2, unpinned multi-account exits 1) aborts here with its own
59
+ # message instead of leaving $U/$P/… unset for `set -u` to trip on later.
60
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
61
+ if ! CREDS_EVAL="$(node "$SCRIPT_DIR/resolve-account.mjs" --ssh --api)"; then
64
62
  exit 1
65
63
  fi
66
64
  eval "$CREDS_EVAL"
65
+ echo "→ account: $ACCT"
67
66
 
68
67
  # Harden every credentials-derived field BEFORE it reaches ssh/rsync/scp. rsync
69
68
  # re-tokenizes its -e remote-shell string on whitespace (ignoring shell quotes),
@@ -25,15 +25,13 @@ done
25
25
  [ -n "$DOMAIN" ] && [ -n "$TO" ] || { echo "Usage: pull-live.sh --domain <live-domain> --to <local-dir> [--docroot PATH]"; exit 1; }
26
26
  case "$DOMAIN" in *[!a-z0-9.-]*|"") echo "Invalid --domain."; exit 1;; esac
27
27
 
28
- if ! CREDS_EVAL="$(node -e '
29
- const c = require(process.env.HOME + "/.awesomate/credentials.json");
30
- if (!c.ssh) { console.error("Your plan does not include shell access (Support Plus+ required)."); process.exit(2); }
31
- const q = (s) => "'"'"'" + String(s == null ? "" : s).replace(/'"'"'/g, "") + "'"'"'";
32
- console.log(`H=${q(c.ssh.host)}; U=${q(c.ssh.user)}; P=${q(c.ssh.port||22)}; K=${q(c.ssh.keyPath)}`);
33
- ')"; then
28
+ # Shared resolver: folder pin / AWESOMATE_ACCOUNT / sole profile → ssh block.
29
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30
+ if ! CREDS_EVAL="$(node "$SCRIPT_DIR/resolve-account.mjs" --ssh)"; then
34
31
  exit 1
35
32
  fi
36
33
  eval "$CREDS_EVAL"
34
+ echo "→ account: $ACCT"
37
35
 
38
36
  # Harden credentials-derived fields before they reach rsync/scp/ssh — rsync
39
37
  # word-splits its -e remote-shell string, so whitespace here could inject ssh
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared account resolver for the awesomate-hosting skill scripts.
4
+ *
5
+ * Mirrors the MCP server's precedence exactly (mcp/src/config.ts):
6
+ * AWESOMATE_ACCOUNT env → .awesomate.json pin (walked up from cwd, stopping
7
+ * at $HOME) → sole profile → defaultProfile → legacy v1 top-level fields.
8
+ * A pin naming a missing profile is a hard error — never a silent fallback.
9
+ *
10
+ * Prints shell-eval assignments (single-quoted, quote-stripped values — same
11
+ * hardening contract the callers' charclass gates assume):
12
+ * node resolve-account.mjs --ssh # ACCT + H/U/P/K
13
+ * node resolve-account.mjs --api # ACCT + API/PAT
14
+ * Flags combine. Exit 1 = no resolvable account, exit 2 = no shell access.
15
+ */
16
+
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { homedir } from 'node:os';
19
+ import { join, dirname, resolve } from 'node:path';
20
+
21
+ function readJson(path) {
22
+ try {
23
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
24
+ return parsed && typeof parsed === 'object' ? parsed : null;
25
+ } catch { return null; }
26
+ }
27
+
28
+ function fail(msg, code = 1) { console.error(msg); process.exit(code); }
29
+
30
+ const credPath = join(homedir(), '.awesomate', 'credentials.json');
31
+ const file = readJson(credPath) ?? {};
32
+
33
+ const profiles = {};
34
+ if (file.profiles && typeof file.profiles === 'object') {
35
+ for (const [key, profile] of Object.entries(file.profiles)) {
36
+ if (profile && typeof profile.pat === 'string') profiles[key] = profile;
37
+ }
38
+ } else if (typeof file.pat === 'string' && file.pat) {
39
+ profiles[file.slug || 'default'] = file; // legacy v1 single-account file
40
+ }
41
+ const names = Object.keys(profiles);
42
+
43
+ function findPin() {
44
+ const explicit = process.env.AWESOMATE_PIN_FILE;
45
+ if (explicit) return existsSync(explicit) ? explicit : null;
46
+ const home = resolve(homedir());
47
+ let dir = resolve(process.cwd());
48
+ for (;;) {
49
+ const candidate = join(dir, '.awesomate.json');
50
+ if (existsSync(candidate)) return candidate;
51
+ if (dir === home) return null;
52
+ const parent = dirname(dir);
53
+ if (parent === dir) return null;
54
+ dir = parent;
55
+ }
56
+ }
57
+
58
+ let key = null;
59
+ if (process.env.AWESOMATE_ACCOUNT) {
60
+ key = process.env.AWESOMATE_ACCOUNT;
61
+ if (!profiles[key]) {
62
+ fail(`AWESOMATE_ACCOUNT="${key}" has no matching profile in ${credPath}. Available: ${names.join(', ') || '(none)'}.`);
63
+ }
64
+ } else {
65
+ const pinPath = findPin();
66
+ if (pinPath) {
67
+ const pin = readJson(pinPath);
68
+ key = pin && typeof pin.account === 'string' ? pin.account : null;
69
+ if (!key) fail(`${pinPath} exists but has no "account" field. Expected: {"account": "<slug>"}.`);
70
+ if (!profiles[key]) {
71
+ fail(
72
+ `This folder is pinned to account "${key}" (${pinPath}) but no matching profile exists in ${credPath}. ` +
73
+ `Available: ${names.join(', ') || '(none)'}. Connect "${key}" from hub.awesomate.ai/sites (logged in as it) or fix the pin.`,
74
+ );
75
+ }
76
+ } else if (names.length === 1) {
77
+ key = names[0];
78
+ } else if (file.defaultProfile && profiles[file.defaultProfile]) {
79
+ key = file.defaultProfile;
80
+ } else if (names.length === 0) {
81
+ fail('Not connected. Run the Connect Claude Code setup from hub.awesomate.ai/sites.');
82
+ } else {
83
+ fail(
84
+ `Multiple accounts are connected (${names.join(', ')}) and this folder isn't pinned to one. ` +
85
+ 'Create .awesomate.json {"account": "<slug>"} in the project root or set AWESOMATE_ACCOUNT.',
86
+ );
87
+ }
88
+ }
89
+
90
+ const profile = profiles[key];
91
+ const q = (s) => "'" + String(s == null ? '' : s).replace(/'/g, '') + "'";
92
+ const out = [`ACCT=${q(profile.slug ?? key)}`];
93
+ if (process.argv.includes('--ssh')) {
94
+ if (!profile.ssh) fail('Your plan does not include shell access (Support Plus+ required).', 2);
95
+ out.push(`H=${q(profile.ssh.host)}`, `U=${q(profile.ssh.user)}`, `P=${q(profile.ssh.port || 22)}`, `K=${q(profile.ssh.keyPath)}`);
96
+ }
97
+ if (process.argv.includes('--api')) {
98
+ out.push(`API=${q(profile.apiBase || 'https://hub.awesomate.ai')}`, `PAT=${q(profile.pat)}`);
99
+ }
100
+ console.log(out.join('; '));
@@ -8,12 +8,12 @@ set -euo pipefail
8
8
  CRED="$HOME/.awesomate/credentials.json"
9
9
  [ -f "$CRED" ] || { echo "Not connected. Run bootstrap first (Connect Claude Code on hub.awesomate.ai/sites)."; exit 1; }
10
10
 
11
- # Parse the ssh block with node (present wherever the MCP runs via npx).
12
- eval "$(node -e '
13
- const c = require(process.env.HOME + "/.awesomate/credentials.json");
14
- if (!c.ssh) { console.error("Your plan does not include shell access (Support Plus+ required)."); process.exit(2); }
15
- const q = (s) => "'"'"'" + String(s).replace(/'"'"'/g, "") + "'"'"'";
16
- console.log(`H=${q(c.ssh.host)}; U=${q(c.ssh.user)}; P=${q(c.ssh.port||22)}; K=${q(c.ssh.keyPath)}`);
17
- ')"
11
+ # Resolve WHICH account (folder pin / AWESOMATE_ACCOUNT / sole profile) and its
12
+ # ssh block via the shared resolver. Capture then eval so a resolver failure
13
+ # aborts with its own message instead of tripping set -u later.
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ if ! CREDS_EVAL="$(node "$SCRIPT_DIR/resolve-account.mjs" --ssh)"; then exit 1; fi
16
+ eval "$CREDS_EVAL"
17
+ echo "→ account: $ACCT" >&2
18
18
 
19
19
  exec ssh -i "$K" -p "$P" -o StrictHostKeyChecking=accept-new "$U@$H" "$@"
@@ -20,7 +20,13 @@ promote back, and delete drafts — via the `awesomate_n8n_deploy` and
20
20
 
21
21
  ## 0. First run (every session)
22
22
 
23
- Call `awesomate_n8n_context` once before any n8n work and cache the result:
23
+ Run `awesomate_whoami` first n8n tools act on whichever ACCOUNT this folder
24
+ resolves to (folder `.awesomate.json` pin / `AWESOMATE_ACCOUNT` / sole
25
+ profile; see the awesomate-hosting skill's multi-account section). If the slug
26
+ isn't the instance the user means, stop and fix the pin/connection before any
27
+ n8n work. Every tool response is stamped `account: <slug>` — watch it.
28
+
29
+ Then call `awesomate_n8n_context` once before any n8n work and cache the result:
24
30
 
25
31
  - `consented: false` → give the user the `settingsUrl` link (Settings →
26
32
  Privacy → "Allow Claude Code to Build n8n Workflows"), wait for them to
@@ -40,8 +46,11 @@ $env, `$json.body`, task-runner limits, activation semantics).
40
46
 
41
47
  ## 1. Reads
42
48
 
43
- PAT as `Authorization: Bearer <pat>` (from `~/.awesomate/credentials.json`)
44
- against `apiBase`. Never echo the PAT into the conversation.
49
+ PAT as `Authorization: Bearer <pat>` against `apiBase`. Resolve the ACTIVE
50
+ profile's pat `node ~/.claude/skills/awesomate-hosting/scripts/resolve-account.mjs --api`
51
+ — never read `credentials.json`'s top-level `pat` directly when a `profiles`
52
+ map exists (it mirrors the default profile, not necessarily this folder's
53
+ account). Never echo the PAT into the conversation.
45
54
 
46
55
  | What | Endpoint |
47
56
  |---|---|