@awesomate/hosting-mcp 0.7.0 → 0.7.2
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.
- package/README.md +16 -6
- package/dist/index.js +18685 -107
- package/package.json +2 -1
- package/skill/awesomate-hosting/SKILL.md +31 -3
- package/skill/awesomate-hosting/scripts/bootstrap.mjs +260 -113
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, and n8n automations",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
+
"undici": "^6.21.0",
|
|
27
28
|
"zod": "^3.24.1"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
@@ -68,8 +68,34 @@ fallback** — that's deliberate.
|
|
|
68
68
|
- Connecting a second account never disconnects the first; re-pairing an
|
|
69
69
|
account just refreshes its profile.
|
|
70
70
|
- The bootstrap prints a final `AWESOMATE CONNECT: SUCCESS account=<slug>` (or
|
|
71
|
-
`FAILED reason=…`) sentinel — when a connect ran as a
|
|
72
|
-
for that line instead of assuming success from
|
|
71
|
+
`PARTIAL … issues=…` / `FAILED reason=…`) sentinel — when a connect ran as a
|
|
72
|
+
`!` bang command, check for that line instead of assuming success from
|
|
73
|
+
silence. `PARTIAL` means the token/pin are stored but a step (listed in
|
|
74
|
+
`issues=`) needs the printed manual fix.
|
|
75
|
+
|
|
76
|
+
### Sandboxed / ephemeral environments (cloud agent containers)
|
|
77
|
+
|
|
78
|
+
- **Egress proxy**: if `HTTPS_PROXY`/`HTTP_PROXY` is set, the environment
|
|
79
|
+
forces traffic through a proxy that Node's built-in fetch ignores — requests
|
|
80
|
+
bypass it and the gateway answers 403, which looks like a bad code/token but
|
|
81
|
+
is a transport failure. The bootstrap and MCP server handle this themselves
|
|
82
|
+
(undici env-proxy agent); if an older version is in play, prefix commands
|
|
83
|
+
with `NODE_USE_ENV_PROXY=1`. Diagnostic: `curl` works but `node -e "fetch(…)"`
|
|
84
|
+
403s → proxy bypass, not credentials.
|
|
85
|
+
- **Interrupted connect?** Setup codes are re-runnable for their whole
|
|
86
|
+
10-minute life (each run mints a fresh token), and `--pat amt_pat_…` resumes
|
|
87
|
+
with an already-redeemed token — never tell the user their code is spent
|
|
88
|
+
unless the API literally said so.
|
|
89
|
+
- **Nothing persists**: `~/.awesomate`, the skill copy, and the MCP
|
|
90
|
+
registration all vanish with the container. Treat a container connect as a
|
|
91
|
+
diagnosis/test, and tell the user a real workstation is needed for a lasting
|
|
92
|
+
install.
|
|
93
|
+
- **No ssh client**: containers usually lack `ssh-keygen`/`ssh`, so deploy and
|
|
94
|
+
shell are unavailable — reads and management calls still work. The bootstrap
|
|
95
|
+
skips SSH with a note rather than failing.
|
|
96
|
+
- Never probe connectivity with a POST to `/redeem` — it's state-changing. Use
|
|
97
|
+
`GET {apiBase}/api/hosting-access/context` (401 without a token still proves
|
|
98
|
+
reachability).
|
|
73
99
|
|
|
74
100
|
If this skill is loaded but **no `awesomate_*` tools exist in the session at
|
|
75
101
|
all**, the MCP server was registered after Claude Code started (the bootstrap
|
|
@@ -189,7 +215,9 @@ Git skills drive the Git workflow; this skill just deploys the result.
|
|
|
189
215
|
effort) generates an SSH keypair and registers the public key. If the SSH
|
|
190
216
|
step fails (e.g. hosting not fully provisioned), the token + skill + MCP are
|
|
191
217
|
still set up. The Connect-Claude-Code prompt from the hub runs this via
|
|
192
|
-
`npx -
|
|
218
|
+
`npx -y --package=@awesomate/hosting-mcp awesomate-hosting-bootstrap`.
|
|
219
|
+
(Always the `--package=` form — a bare `-p` breaks the claude CLI's argv
|
|
220
|
+
pre-scan if the command is ever wrapped in `claude mcp add`.)
|
|
193
221
|
- `ssh-connect.sh` — open a jailed SSH session (or run one remote command).
|
|
194
222
|
- `wp.sh` — run a WP-CLI command against a live site over that SSH.
|
|
195
223
|
- `deploy.sh` — snapshot-first Studio→live deploy (files, optional DB).
|
|
@@ -2,27 +2,33 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Awesomate Hosting — one-time bootstrap.
|
|
4
4
|
*
|
|
5
|
-
* Redeems the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* Redeems the setup code from the hub's "Connect Claude Code" prompt for a
|
|
6
|
+
* durable, scoped access token; stores it as a PROFILE (multi-account safe);
|
|
7
|
+
* pins the current folder to that account; and (on Support Plus+) generates an
|
|
8
|
+
* SSH keypair and authorizes the PUBLIC key on the client's own cPanel
|
|
9
|
+
* account. The private key never leaves this machine; the setup code is the
|
|
10
|
+
* only secret that ever crossed the clipboard, and it's short-lived. The
|
|
11
|
+
* access token itself is never printed.
|
|
12
12
|
*
|
|
13
13
|
* Usage (Claude runs this — the user doesn't touch a terminal):
|
|
14
|
-
* node bootstrap.mjs --code amt_bs_XXXX [--account <slug>] [--
|
|
14
|
+
* node bootstrap.mjs --code amt_bs_XXXX [--account <slug>] [--pin-dir <dir>]
|
|
15
|
+
* node bootstrap.mjs --pat amt_pat_XXXX [--account <slug>] # resume: skip redeem
|
|
15
16
|
*
|
|
16
|
-
* --account asserts WHICH account
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* --account asserts WHICH account this connect is for — mismatch aborts before
|
|
18
|
+
* anything is written. --pat resumes a half-finished bootstrap with an
|
|
19
|
+
* already-redeemed token (no fresh code needed). Setup codes are replayable
|
|
20
|
+
* for their whole 10-minute life, so re-running the same command is also safe.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* Proxied environments (cloud agent sandboxes): Node's built-in fetch ignores
|
|
23
|
+
* HTTP(S)_PROXY, so this script routes through undici's EnvHttpProxyAgent when
|
|
24
|
+
* a proxy is configured (falling back to a one-shot re-exec with
|
|
25
|
+
* NODE_USE_ENV_PROXY=1 when undici isn't resolvable).
|
|
26
|
+
*
|
|
27
|
+
* Node 18+ built-ins only, plus undici when running from the npm package.
|
|
22
28
|
*/
|
|
23
29
|
|
|
24
|
-
import { execFileSync } from 'node:child_process';
|
|
25
|
-
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync, cpSync, readdirSync } from 'node:fs';
|
|
30
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
31
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync, cpSync, readdirSync, renameSync, statSync } from 'node:fs';
|
|
26
32
|
import { homedir, hostname } from 'node:os';
|
|
27
33
|
import { join, dirname, resolve } from 'node:path';
|
|
28
34
|
import { fileURLToPath } from 'node:url';
|
|
@@ -33,13 +39,21 @@ function arg(name, fallback) {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
const code = arg('code');
|
|
42
|
+
const resumePat = arg('pat');
|
|
36
43
|
const expectedAccount = arg('account', null);
|
|
44
|
+
const pinDirArg = arg('pin-dir', null);
|
|
37
45
|
const apiBase = (arg('api', 'https://hub.awesomate.ai')).replace(/\/$/, '');
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
|
|
47
|
+
if (!resumePat && (!code || !code.startsWith('amt_bs_'))) {
|
|
48
|
+
console.error('Missing/invalid --code. Copy a fresh setup prompt from hub.awesomate.ai/sites (or resume with --pat if you already hold a token).');
|
|
40
49
|
console.error('AWESOMATE CONNECT: FAILED reason=bad-code');
|
|
41
50
|
process.exit(1);
|
|
42
51
|
}
|
|
52
|
+
if (resumePat && !resumePat.startsWith('amt_pat_')) {
|
|
53
|
+
console.error('--pat does not look like an Awesomate access token (expected amt_pat_ prefix).');
|
|
54
|
+
console.error('AWESOMATE CONNECT: FAILED reason=bad-pat');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
43
57
|
|
|
44
58
|
const dir = join(homedir(), '.awesomate');
|
|
45
59
|
const keysDir = join(dir, 'keys');
|
|
@@ -47,20 +61,81 @@ const credPath = join(dir, 'credentials.json');
|
|
|
47
61
|
const keyPath = join(keysDir, 'id_ed25519');
|
|
48
62
|
const PIN_FILENAME = '.awesomate.json';
|
|
49
63
|
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Proxy-aware fetch. Node's built-in fetch does NOT honor HTTP(S)_PROXY —
|
|
66
|
+
// in sandboxes that force egress through a proxy, requests bypass it and the
|
|
67
|
+
// gateway answers 403, which looks exactly like a credential failure. Route
|
|
68
|
+
// through undici's EnvHttpProxyAgent when a proxy env var is set.
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
const PROXY_URL =
|
|
71
|
+
process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || '';
|
|
72
|
+
let doFetch = fetch;
|
|
73
|
+
|
|
74
|
+
async function initProxySupport() {
|
|
75
|
+
if (!PROXY_URL) return;
|
|
76
|
+
// undici tags EnvHttpProxyAgent experimental and warns on construction —
|
|
77
|
+
// noise in the middle of connect output. Keep every other warning.
|
|
78
|
+
process.removeAllListeners('warning');
|
|
79
|
+
process.on('warning', (w) => {
|
|
80
|
+
if (!/EnvHttpProxyAgent/.test(String(w.message))) console.error(w.stack ?? String(w.message));
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
// Resolves when running from the npm package (undici is a dependency).
|
|
84
|
+
const undici = await import('undici');
|
|
85
|
+
const dispatcher = new undici.EnvHttpProxyAgent();
|
|
86
|
+
doFetch = (url, opts = {}) => undici.fetch(url, { ...opts, dispatcher });
|
|
87
|
+
console.log(`• Egress proxy detected — routing hub requests through ${PROXY_URL}.`);
|
|
88
|
+
} catch {
|
|
89
|
+
// Running from an installed skill copy with no node_modules. Re-exec once
|
|
90
|
+
// with NODE_USE_ENV_PROXY=1 (built-in fetch honors proxies on newer Node;
|
|
91
|
+
// the env var is ignored where unsupported, so this is at worst a no-op).
|
|
92
|
+
if (process.env.AWESOMATE_PROXY_REEXEC !== '1') {
|
|
93
|
+
const r = spawnSync(process.execPath, process.argv.slice(1), {
|
|
94
|
+
stdio: 'inherit',
|
|
95
|
+
env: { ...process.env, NODE_USE_ENV_PROXY: '1', AWESOMATE_PROXY_REEXEC: '1' },
|
|
96
|
+
});
|
|
97
|
+
process.exit(r.status ?? 1);
|
|
98
|
+
}
|
|
99
|
+
console.log(`• Egress proxy detected (${PROXY_URL}) but proxy support is unavailable in this runtime — requests may be blocked by the gateway.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const proxyAdvice = () =>
|
|
104
|
+
PROXY_URL
|
|
105
|
+
? ` An egress proxy is configured (${PROXY_URL}); if this persists, re-run the command prefixed with NODE_USE_ENV_PROXY=1.`
|
|
106
|
+
: '';
|
|
107
|
+
|
|
50
108
|
async function request(method, path, { body, token } = {}) {
|
|
51
109
|
const headers = { 'Content-Type': 'application/json' };
|
|
52
110
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
111
|
+
let res;
|
|
112
|
+
try {
|
|
113
|
+
res = await doFetch(`${apiBase}${path}`, {
|
|
114
|
+
method,
|
|
115
|
+
headers,
|
|
116
|
+
body: method === 'GET' ? undefined : JSON.stringify(body ?? {}),
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
const detail = err?.cause?.code ?? err?.code ?? err?.message ?? 'network error';
|
|
120
|
+
throw new Error(`Could not reach ${apiBase} (${detail}). This is a NETWORK problem, not a code/token problem.${proxyAdvice()}`);
|
|
121
|
+
}
|
|
58
122
|
const text = await res.text();
|
|
59
123
|
let json;
|
|
60
124
|
try { json = text ? JSON.parse(text) : null; } catch { json = text; }
|
|
125
|
+
const apiError = json && typeof json === 'object' && typeof json.error === 'string' ? json.error : null;
|
|
61
126
|
if (!res.ok) {
|
|
62
|
-
|
|
63
|
-
|
|
127
|
+
if (apiError) {
|
|
128
|
+
const e = new Error(`${path} failed: ${apiError}`);
|
|
129
|
+
e.isApiError = true;
|
|
130
|
+
e.status = res.status;
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
// No structured error body → the response almost certainly came from an
|
|
134
|
+
// egress gateway / firewall, not from the hub. Say so instead of blaming
|
|
135
|
+
// the code — that misdirection is expensive.
|
|
136
|
+
throw new Error(
|
|
137
|
+
`${path} returned HTTP ${res.status} without an API error body — the request likely never reached hub.awesomate.ai (egress proxy or firewall in the way).${proxyAdvice()}`,
|
|
138
|
+
);
|
|
64
139
|
}
|
|
65
140
|
return json;
|
|
66
141
|
}
|
|
@@ -74,6 +149,13 @@ function readJsonFile(path) {
|
|
|
74
149
|
} catch { return null; }
|
|
75
150
|
}
|
|
76
151
|
|
|
152
|
+
/** Atomic JSON write (tmp + rename) so a crash can't leave a torn file. */
|
|
153
|
+
function writeJsonFile(path, value) {
|
|
154
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
155
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
156
|
+
renameSync(tmp, path);
|
|
157
|
+
}
|
|
158
|
+
|
|
77
159
|
/**
|
|
78
160
|
* Merge one account profile into credentials.json (v2 multi-profile shape).
|
|
79
161
|
* Existing profiles for OTHER accounts are preserved — connecting a second
|
|
@@ -122,25 +204,33 @@ function mergeProfile(profile) {
|
|
|
122
204
|
|
|
123
205
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
124
206
|
chmodSync(dir, 0o700);
|
|
125
|
-
|
|
207
|
+
writeJsonFile(credPath, creds);
|
|
126
208
|
chmodSync(credPath, 0o600);
|
|
127
209
|
return creds;
|
|
128
210
|
}
|
|
129
211
|
|
|
130
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Pin a folder to this account. --pin-dir overrides the implicit cwd (useful
|
|
214
|
+
* when the bootstrap is run from somewhere other than the project root).
|
|
215
|
+
* Never pins $HOME.
|
|
216
|
+
*/
|
|
131
217
|
function writePin(slug) {
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
console.log(`•
|
|
218
|
+
const target = resolve(pinDirArg || process.cwd());
|
|
219
|
+
if (pinDirArg && !(existsSync(target) && statSync(target).isDirectory())) {
|
|
220
|
+
console.log(`• --pin-dir ${pinDirArg} is not an existing directory — skipping the pin. Create it and add ${PIN_FILENAME} with {"account": "${slug}"}.`);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (target === resolve(homedir())) {
|
|
224
|
+
console.log(`• Target is the home directory — not writing a ${PIN_FILENAME} pin. Re-run from the project folder (or pass --pin-dir) to pin it.`);
|
|
135
225
|
return null;
|
|
136
226
|
}
|
|
137
|
-
const pinPath = join(
|
|
227
|
+
const pinPath = join(target, PIN_FILENAME);
|
|
138
228
|
const existing = readJsonFile(pinPath);
|
|
139
229
|
if (existing?.account && existing.account !== slug) {
|
|
140
|
-
console.log(`• Re-pinning
|
|
230
|
+
console.log(`• Re-pinning ${target}: ${existing.account} → ${slug}.`);
|
|
141
231
|
}
|
|
142
|
-
|
|
143
|
-
console.log(`✓ Pinned ${
|
|
232
|
+
writeJsonFile(pinPath, { version: 1, account: slug });
|
|
233
|
+
console.log(`✓ Pinned ${target} to account "${slug}" (${PIN_FILENAME}).`);
|
|
144
234
|
return pinPath;
|
|
145
235
|
}
|
|
146
236
|
|
|
@@ -152,12 +242,6 @@ function writePin(slug) {
|
|
|
152
242
|
* failure here never aborts the rest of setup.
|
|
153
243
|
*/
|
|
154
244
|
function installSkill() {
|
|
155
|
-
// bootstrap.mjs lives at <skill>/scripts/bootstrap.mjs → the skill dir is
|
|
156
|
-
// `..` and every sibling of it under the package's skill/ dir is another
|
|
157
|
-
// bundled skill (e.g. awesomate-n8n). Install them all. When bootstrap is
|
|
158
|
-
// re-run from an already-installed copy (~/.claude/skills/...), the parent
|
|
159
|
-
// is the user's whole skills folder — in that case only reinstall this one
|
|
160
|
-
// skill, never its unrelated siblings.
|
|
161
245
|
const hostingSkill = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
162
246
|
const skillRoot = dirname(hostingSkill);
|
|
163
247
|
// Only the package layout (skill/<name>/) has bundled siblings; from an
|
|
@@ -203,100 +287,153 @@ function installSkill() {
|
|
|
203
287
|
return true;
|
|
204
288
|
}
|
|
205
289
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
290
|
+
// --package= (with the =) instead of -p: the claude CLI pre-scans the FULL
|
|
291
|
+
// argv for its own global -p/--print flag — even after the `--` separator —
|
|
292
|
+
// so any `claude mcp add … -- npx -y -p pkg bin` flips it into print mode and
|
|
293
|
+
// it rejects the scope flag as "unknown option" (marlins bug report,
|
|
294
|
+
// 2026-07-28). npx treats both forms identically at runtime; the = form keeps
|
|
295
|
+
// the value glued to the token so these args are safe to copy anywhere.
|
|
296
|
+
// The explicit package must stay: this package ships two bins.
|
|
297
|
+
const CLEAN_ENTRY = {
|
|
298
|
+
command: 'npx',
|
|
299
|
+
args: ['-y', '--package=@awesomate/hosting-mcp', 'awesomate-hosting-mcp'],
|
|
300
|
+
};
|
|
301
|
+
const MANUAL_ADD_JSON = JSON.stringify({ mcpServers: { 'awesomate-hosting': CLEAN_ENTRY } }, null, 2);
|
|
212
302
|
|
|
213
303
|
/**
|
|
214
|
-
* Converge on exactly one env-free, user-scope MCP registration
|
|
304
|
+
* Converge on exactly one env-free, user-scope MCP registration by editing the
|
|
305
|
+
* config files DIRECTLY — `claude mcp add` flag syntax has drifted between CLI
|
|
306
|
+
* versions (--scope/-s rejected on some), and a registration is too important
|
|
307
|
+
* to hang off a moving target.
|
|
215
308
|
*
|
|
216
309
|
* Legacy bootstraps baked AWESOMATE_PAT into the registration env — env beats
|
|
217
310
|
* credentials.json inside the server, so those registrations keep serving a
|
|
218
|
-
* STALE account forever after a re-pair. Remove every
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* scopes can't be removed from here — those are reported for manual cleanup.
|
|
222
|
-
*
|
|
223
|
-
* Throws on failure: a wrong registration is exactly the silent-wrong-account
|
|
224
|
-
* bug this bootstrap exists to prevent.
|
|
311
|
+
* STALE account forever after a re-pair. Remove every entry that carries the
|
|
312
|
+
* env var (user scope, this project's local scope + .mcp.json); entries in
|
|
313
|
+
* OTHER projects are reported for manual cleanup, not touched.
|
|
225
314
|
*/
|
|
226
315
|
function ensureMcpRegistration() {
|
|
227
316
|
const cwd = resolve(process.cwd());
|
|
228
|
-
const
|
|
229
|
-
const warnings = [];
|
|
230
|
-
|
|
231
|
-
const scopesToClean = [];
|
|
232
|
-
if (claudeConfig.mcpServers?.['awesomate-hosting']?.env?.AWESOMATE_PAT) scopesToClean.push('user');
|
|
233
|
-
if (readJsonFile(join(cwd, '.mcp.json'))?.mcpServers?.['awesomate-hosting']?.env?.AWESOMATE_PAT) scopesToClean.push('project');
|
|
234
|
-
for (const [projectDir, project] of Object.entries(claudeConfig.projects ?? {})) {
|
|
235
|
-
if (!project?.mcpServers?.['awesomate-hosting']?.env?.AWESOMATE_PAT) continue;
|
|
236
|
-
if (resolve(projectDir) === cwd) scopesToClean.push('local');
|
|
237
|
-
else warnings.push(`cd ${projectDir} && claude mcp remove awesomate-hosting -s local`);
|
|
238
|
-
}
|
|
317
|
+
const claudeJsonPath = join(homedir(), '.claude.json');
|
|
239
318
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
319
|
+
let cfg = {};
|
|
320
|
+
if (existsSync(claudeJsonPath)) {
|
|
321
|
+
cfg = readJsonFile(claudeJsonPath);
|
|
322
|
+
if (!cfg) {
|
|
323
|
+
throw new Error(`${claudeJsonPath} exists but is not valid JSON — refusing to rewrite it. Fix the file, then add:\n${MANUAL_ADD_JSON}`);
|
|
324
|
+
}
|
|
243
325
|
}
|
|
244
326
|
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
327
|
+
const entry = cfg.mcpServers?.['awesomate-hosting'];
|
|
328
|
+
const entryIsClean =
|
|
329
|
+
entry && !entry.env?.AWESOMATE_PAT && JSON.stringify(entry.args ?? []).includes('awesomate-hosting-mcp');
|
|
330
|
+
|
|
331
|
+
let changed = false;
|
|
332
|
+
if (!entryIsClean) {
|
|
333
|
+
cfg.mcpServers = { ...(cfg.mcpServers ?? {}), 'awesomate-hosting': CLEAN_ENTRY };
|
|
334
|
+
changed = true;
|
|
335
|
+
console.log(
|
|
336
|
+
entry?.env?.AWESOMATE_PAT
|
|
337
|
+
? '✓ Replaced the legacy user-scope registration (had a token baked into its env) with an env-free one.'
|
|
338
|
+
: '✓ Registered the Awesomate Hosting MCP server (user scope, env-free — the folder pin picks the account).',
|
|
339
|
+
);
|
|
251
340
|
} else {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
341
|
+
console.log('✓ MCP server already registered (user scope, env-free) — account selection comes from the folder pin.');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// This project's local-scope entry (lives under projects[cwd] in ~/.claude.json).
|
|
345
|
+
const localEntry = cfg.projects?.[cwd]?.mcpServers?.['awesomate-hosting'];
|
|
346
|
+
if (localEntry?.env?.AWESOMATE_PAT) {
|
|
347
|
+
delete cfg.projects[cwd].mcpServers['awesomate-hosting'];
|
|
348
|
+
changed = true;
|
|
349
|
+
console.log("✓ Removed this project's legacy local-scope registration (token baked into env).");
|
|
350
|
+
}
|
|
351
|
+
// Other projects: report, don't touch.
|
|
352
|
+
for (const [projectDir, project] of Object.entries(cfg.projects ?? {})) {
|
|
353
|
+
if (resolve(projectDir) === cwd) continue;
|
|
354
|
+
if (project?.mcpServers?.['awesomate-hosting']?.env?.AWESOMATE_PAT) {
|
|
355
|
+
console.log(`• ${projectDir} still has a legacy registration with a baked-in token — from that folder run the Connect command again, or remove the awesomate-hosting entry from its local MCP config.`);
|
|
255
356
|
}
|
|
256
|
-
claudeCli(['mcp', 'add', '--scope', 'user', 'awesomate-hosting', '--',
|
|
257
|
-
'npx', '-y', '-p', '@awesomate/hosting-mcp', 'awesomate-hosting-mcp']);
|
|
258
|
-
console.log('✓ Registered the Awesomate Hosting MCP server (user scope, env-free — the folder pin picks the account).');
|
|
259
357
|
}
|
|
260
358
|
|
|
261
|
-
|
|
262
|
-
|
|
359
|
+
if (changed) {
|
|
360
|
+
writeJsonFile(claudeJsonPath, cfg);
|
|
361
|
+
// Read back and verify — never trust a write (or a CLI exit code) alone.
|
|
362
|
+
const verify = readJsonFile(claudeJsonPath);
|
|
363
|
+
const written = verify?.mcpServers?.['awesomate-hosting'];
|
|
364
|
+
if (!written || written.command !== CLEAN_ENTRY.command || !JSON.stringify(written.args ?? []).includes('awesomate-hosting-mcp')) {
|
|
365
|
+
throw new Error(`wrote ${claudeJsonPath} but the awesomate-hosting entry did not verify on read-back`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Project-scope .mcp.json in cwd.
|
|
370
|
+
const mcpJsonPath = join(cwd, '.mcp.json');
|
|
371
|
+
const mcpJson = readJsonFile(mcpJsonPath);
|
|
372
|
+
if (mcpJson?.mcpServers?.['awesomate-hosting']?.env?.AWESOMATE_PAT) {
|
|
373
|
+
delete mcpJson.mcpServers['awesomate-hosting'];
|
|
374
|
+
writeJsonFile(mcpJsonPath, mcpJson);
|
|
375
|
+
console.log('✓ Removed the legacy project-scope (.mcp.json) registration (token baked into env).');
|
|
263
376
|
}
|
|
264
377
|
}
|
|
265
378
|
|
|
266
379
|
async function main() {
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
380
|
+
await initProxySupport();
|
|
381
|
+
const issues = [];
|
|
382
|
+
|
|
383
|
+
// ── Identity: redeem a code, or verify a resumed PAT ─────────────────────
|
|
384
|
+
let redeemed;
|
|
385
|
+
if (resumePat) {
|
|
386
|
+
console.log(`Resuming with an existing access token — verifying against ${apiBase} …`);
|
|
387
|
+
const ctx = await get('/api/hosting-access/context', { token: resumePat });
|
|
388
|
+
redeemed = {
|
|
389
|
+
pat: resumePat,
|
|
390
|
+
contactId: ctx.contactId,
|
|
391
|
+
slug: ctx.slug ?? null,
|
|
392
|
+
email: ctx.email ?? null,
|
|
393
|
+
plan: ctx.plan,
|
|
394
|
+
shellAccess: Boolean(ctx.capabilities?.shell),
|
|
395
|
+
cpanel: ctx.cpanel ?? null,
|
|
396
|
+
expiresAt: ctx.patExpiresAt ?? null,
|
|
397
|
+
};
|
|
398
|
+
} else {
|
|
399
|
+
console.log(`Redeeming setup code against ${apiBase} …`);
|
|
400
|
+
redeemed = await post('/api/hosting-access/redeem', {
|
|
401
|
+
body: { code, machineLabel: `Claude Code on ${hostname()}` },
|
|
402
|
+
});
|
|
403
|
+
}
|
|
271
404
|
|
|
272
405
|
const slug = redeemed.slug ?? null;
|
|
273
406
|
if (!slug) {
|
|
274
|
-
console.error('The hub did not return an account slug
|
|
407
|
+
console.error('The hub did not return an account slug — cannot safely store a profile. Contact Awesomate support.');
|
|
275
408
|
console.error('AWESOMATE CONNECT: FAILED reason=no-slug');
|
|
276
409
|
process.exit(1);
|
|
277
410
|
}
|
|
278
411
|
// Wrong-account guard: abort BEFORE writing anything.
|
|
279
412
|
if (expectedAccount && expectedAccount !== slug) {
|
|
280
|
-
console.error(`This setup code belongs to account "${slug}" but this connect was for "${expectedAccount}".`);
|
|
413
|
+
console.error(`This ${resumePat ? 'token' : 'setup code'} belongs to account "${slug}" but this connect was for "${expectedAccount}".`);
|
|
281
414
|
console.error(`You are probably logged into the wrong hub account — log into ${expectedAccount}'s hub account and copy a fresh Connect prompt.`);
|
|
282
415
|
console.error(`AWESOMATE CONNECT: FAILED reason=account-mismatch expected=${expectedAccount} got=${slug}`);
|
|
283
416
|
process.exit(1);
|
|
284
417
|
}
|
|
285
418
|
|
|
286
|
-
// Server-side identity check with the NEW token before trusting it
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
context =
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
419
|
+
// Server-side identity check with the NEW token before trusting it (the
|
|
420
|
+
// resume path already did exactly this to build `redeemed`).
|
|
421
|
+
if (!resumePat) {
|
|
422
|
+
let context = null;
|
|
423
|
+
try {
|
|
424
|
+
context = await get('/api/hosting-access/context', { token: redeemed.pat });
|
|
425
|
+
} catch (err) {
|
|
426
|
+
console.error(`Token verification failed (${err.message}) — not storing it. Re-run the same command (setup codes stay valid for 10 minutes).`);
|
|
427
|
+
console.error('AWESOMATE CONNECT: FAILED reason=verify-failed');
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
const ctxSlug = context?.slug ?? null;
|
|
431
|
+
if (ctxSlug && ctxSlug !== slug) {
|
|
432
|
+
console.error(`Identity mismatch: redeem said "${slug}" but the token resolves to "${ctxSlug}". Not storing it — contact Awesomate support.`);
|
|
433
|
+
console.error('AWESOMATE CONNECT: FAILED reason=identity-mismatch');
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
redeemed.email = redeemed.email ?? context?.email ?? null;
|
|
300
437
|
}
|
|
301
438
|
|
|
302
439
|
const profile = {
|
|
@@ -304,7 +441,7 @@ async function main() {
|
|
|
304
441
|
pat: redeemed.pat,
|
|
305
442
|
contactId: redeemed.contactId,
|
|
306
443
|
slug,
|
|
307
|
-
email: redeemed.email ??
|
|
444
|
+
email: redeemed.email ?? null,
|
|
308
445
|
plan: redeemed.plan,
|
|
309
446
|
cpanel: redeemed.cpanel ?? null,
|
|
310
447
|
expiresAt: redeemed.expiresAt ?? null,
|
|
@@ -315,8 +452,8 @@ async function main() {
|
|
|
315
452
|
const pinPath = writePin(slug);
|
|
316
453
|
|
|
317
454
|
// Install the skill + register the MCP server FIRST — these always work and
|
|
318
|
-
// don't depend on hosting being fully provisioned.
|
|
319
|
-
//
|
|
455
|
+
// don't depend on hosting being fully provisioned. Neither is allowed to
|
|
456
|
+
// strand the other or SSH: every step from here is collected, not fatal.
|
|
320
457
|
try { installSkill(); } catch (err) {
|
|
321
458
|
console.log(`• Could not auto-install the skill (${err.message}) — continuing; you can copy skill/ manually.`);
|
|
322
459
|
}
|
|
@@ -324,15 +461,15 @@ async function main() {
|
|
|
324
461
|
try {
|
|
325
462
|
ensureMcpRegistration();
|
|
326
463
|
} catch (err) {
|
|
327
|
-
|
|
328
|
-
console.error(
|
|
329
|
-
console.error(`
|
|
330
|
-
process.exit(1);
|
|
464
|
+
issues.push('mcp-registration');
|
|
465
|
+
console.error(`• MCP registration failed: ${err.message}`);
|
|
466
|
+
console.error(` Add it manually (no token goes in the config — the folder pin picks the account):\n${MANUAL_ADD_JSON}`);
|
|
331
467
|
}
|
|
332
468
|
|
|
333
469
|
// SSH key (Support Plus+): best-effort. If it fails (e.g. hosting not fully
|
|
334
|
-
// provisioned yet
|
|
335
|
-
// ready until it's registered, which
|
|
470
|
+
// provisioned yet, or no ssh tooling in this environment), keep the token +
|
|
471
|
+
// skill + MCP — deploy/shell just aren't ready until it's registered, which
|
|
472
|
+
// the skill can retry later.
|
|
336
473
|
if (redeemed.shellAccess) {
|
|
337
474
|
try {
|
|
338
475
|
if (!existsSync(keyPath)) {
|
|
@@ -349,8 +486,12 @@ async function main() {
|
|
|
349
486
|
mergeProfile(profile);
|
|
350
487
|
console.log(`✓ SSH key authorized on ${reg.sshUser}@${reg.sshHost} (jailed to your account).`);
|
|
351
488
|
} catch (err) {
|
|
352
|
-
|
|
353
|
-
|
|
489
|
+
if (err?.code === 'ENOENT') {
|
|
490
|
+
console.log('• No ssh-keygen in this environment — skipping SSH setup. Deploy and shell need a real workstation; reads and management calls work fine here.');
|
|
491
|
+
} else {
|
|
492
|
+
console.log(`• SSH key not registered yet: ${err.message}`);
|
|
493
|
+
console.log(' Everything else is set up. This usually means hosting isn\'t fully provisioned — contact Awesomate support, then ask me to "retry SSH setup".');
|
|
494
|
+
}
|
|
354
495
|
}
|
|
355
496
|
} else {
|
|
356
497
|
console.log('• Plan has no shell access — read/status mode. Upgrade to Support Plus for SSH + deploy.');
|
|
@@ -360,12 +501,18 @@ async function main() {
|
|
|
360
501
|
console.log(`\n✓ Connected: ${who}${pinPath ? ` — pinned ${dirname(pinPath)}` : ''}`);
|
|
361
502
|
console.log('Setup complete. Next: run awesomate_whoami to confirm the account, then ask me to read your plan and list your sites.');
|
|
362
503
|
console.log('(The skill works in your current Claude session right away via its REST fallback — restart Claude Code when convenient to load the MCP tools.)');
|
|
504
|
+
if (issues.length) {
|
|
505
|
+
console.log(`AWESOMATE CONNECT: PARTIAL account=${slug} pin=${pinPath ?? 'none'} issues=${issues.join(',')}`);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
363
508
|
console.log(`AWESOMATE CONNECT: SUCCESS account=${slug} pin=${pinPath ?? 'none'}`);
|
|
364
509
|
}
|
|
365
510
|
|
|
366
511
|
main().catch((err) => {
|
|
367
512
|
console.error('Bootstrap failed:', err.message);
|
|
368
|
-
|
|
513
|
+
if (err.isApiError && err.status === 401) {
|
|
514
|
+
console.error('Grab a fresh code from hub.awesomate.ai/sites — codes last 10 minutes (and stay re-runnable within that window).');
|
|
515
|
+
}
|
|
369
516
|
console.error('AWESOMATE CONNECT: FAILED reason=error');
|
|
370
517
|
process.exit(1);
|
|
371
518
|
});
|