From d4ed356035c606c52cd1612c9aff25fbf936ff0a Mon Sep 17 00:00:00 2001 From: "hostingerbot-public[bot]" <348809394+hostingerbot-public[bot]@users.noreply.github.com> Date: Sun, 15 Nov 2026 06:13:46 +0000 Subject: [PATCH] chore: sync with @hostinger/mcp 4.0.1 --- .cursor-plugin/plugin.json | 2 +- CHANGELOG.md | 6 +++ mcp.json | 20 ++++----- scripts/mcp-tools.json | 29 +++++++----- skills/deploy-to-hosting/SKILL.md | 2 +- skills/hostinger-headless/SKILL.md | 12 ++--- skills/hostinger-headless/VERSION | 6 +-- skills/hostinger-headless/connect/SKILL.md | 44 ++++++++++++++----- .../hostinger-headless/connect/bootstrap.mjs | 39 ++++++++++------ skills/hostinger-headless/connect/launch.mjs | 42 ++++++++++++++++++ skills/hostinger-headless/connect/launch.sh | 23 ++++++++++ .../connect/tools/antigravity.md | 10 ++--- .../connect/tools/claude-code.md | 6 +-- .../hostinger-headless/connect/tools/codex.md | 14 +++--- .../connect/tools/cursor.md | 18 ++++---- .../hostinger-headless/connect/tools/devin.md | 10 ++--- .../connect/tools/vscode.md | 14 +++--- skills/hostinger-headless/hostinger-mcp.mjs | 13 ++---- .../steps/03-provision/ecommerce/SKILL.md | 4 +- .../steps/03-provision/webapp/SKILL.md | 2 +- .../steps/03-provision/wordpress/SKILL.md | 4 +- .../steps/04-content-model/SKILL.md | 36 +++++++++------ .../steps/04-content-model/wordpress/SKILL.md | 2 +- .../steps/06-frontend/SKILL.md | 16 ++++++- .../steps/07-validate/SKILL.md | 13 +++--- .../steps/08-deploy/SKILL.md | 23 +++++++--- skills/hostinger-headless/wait.mjs | 19 +++++--- skills/maintain-wordpress/SKILL.md | 2 +- skills/troubleshoot-website/SKILL.md | 2 +- 29 files changed, 292 insertions(+), 141 deletions(-) create mode 100644 skills/hostinger-headless/connect/launch.mjs create mode 100644 skills/hostinger-headless/connect/launch.sh diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 56eed86..256b36c 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "hostinger-connector", "displayName": "Hostinger Connector", - "version": "0.3.3", + "version": "0.3.4", "description": "MCP plugin to deploy and manage Hostinger websites, WordPress, domains, DNS, VPS, email, and subscriptions from inside Cursor.", "author": "Hostinger", "license": "MIT", diff --git a/CHANGELOG.md b/CHANGELOG.md index d7cf64b..c379d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.3.4] - 2026-11-15 + +### Changed + +- Synced with `@hostinger/mcp` 4.0.1: deploy-to-hosting, hostinger-headless, maintain-wordpress, operation catalog, troubleshoot-website + ## [0.3.3] - 2026-11-12 ### Fixed diff --git a/mcp.json b/mcp.json index ee73e2b..f9c2f3a 100644 --- a/mcp.json +++ b/mcp.json @@ -3,52 +3,52 @@ "hostinger-hosting": { "command": "npx", "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-hosting-mcp"], - "env": { "USER_AGENT": "plugin;cursor;0.3.3" } + "env": { "USER_AGENT": "plugin;cursor;0.3.4" } }, "hostinger-wordpress": { "command": "npx", "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-wordpress-mcp"], - "env": { "USER_AGENT": "plugin;cursor;0.3.3" } + "env": { "USER_AGENT": "plugin;cursor;0.3.4" } }, "hostinger-agency-hosting": { "command": "npx", "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-agency-hosting-mcp"], - "env": { "USER_AGENT": "plugin;cursor;0.3.3" } + "env": { "USER_AGENT": "plugin;cursor;0.3.4" } }, "hostinger-domains": { "command": "npx", "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-domains-mcp"], - "env": { "USER_AGENT": "plugin;cursor;0.3.3" } + "env": { "USER_AGENT": "plugin;cursor;0.3.4" } }, "hostinger-dns": { "command": "npx", "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-dns-mcp"], - "env": { "USER_AGENT": "plugin;cursor;0.3.3" } + "env": { "USER_AGENT": "plugin;cursor;0.3.4" } }, "hostinger-billing": { "command": "npx", "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-billing-mcp"], - "env": { "USER_AGENT": "plugin;cursor;0.3.3" } + "env": { "USER_AGENT": "plugin;cursor;0.3.4" } }, "hostinger-reach": { "command": "npx", "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-reach-mcp"], - "env": { "USER_AGENT": "plugin;cursor;0.3.3" } + "env": { "USER_AGENT": "plugin;cursor;0.3.4" } }, "hostinger-ecommerce": { "command": "npx", "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-ecommerce-mcp"], - "env": { "USER_AGENT": "plugin;cursor;0.3.3" } + "env": { "USER_AGENT": "plugin;cursor;0.3.4" } }, "hostinger-vps": { "command": "npx", "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-vps-mcp"], - "env": { "USER_AGENT": "plugin;cursor;0.3.3" } + "env": { "USER_AGENT": "plugin;cursor;0.3.4" } }, "hostinger-mail": { "command": "npx", "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-mail-mcp"], - "env": { "USER_AGENT": "plugin;cursor;0.3.3" } + "env": { "USER_AGENT": "plugin;cursor;0.3.4" } } } } diff --git a/scripts/mcp-tools.json b/scripts/mcp-tools.json index fc66e44..695fb2e 100644 --- a/scripts/mcp-tools.json +++ b/scripts/mcp-tools.json @@ -1,7 +1,7 @@ { "package": "@hostinger/mcp", - "version": "3.29.0", - "total": 468, + "version": "4.0.1", + "total": 477, "groups": { "agency-hosting": [ "agency-hosting_cache_clear-website", @@ -159,7 +159,15 @@ "gpu_catalog_pool", "gpu_catalog_pool-groups", "gpu_catalog_pool-instance-types", - "gpu_catalog_pool-os-templates" + "gpu_catalog_pool-os-templates", + "gpu_exposed-services_create", + "gpu_exposed-services_delete", + "gpu_exposed-services_list", + "gpu_instances_create", + "gpu_instances_delete", + "gpu_instances_get", + "gpu_instances_list", + "gpu_instances_metrics" ], "horizons": [ "changelog_list-entries", @@ -197,10 +205,6 @@ "hosting_databases_setup-website", "hosting_datacenters_list", "hosting_deploy", - "hosting_deploy-js-application", - "hosting_deploy-static-website", - "hosting_deploy-wordpress-plugin", - "hosting_deploy-wordpress-theme", "hosting_domains_create-website-parked", "hosting_domains_create-website-subdomain", "hosting_domains_delete-website-parked", @@ -223,8 +227,6 @@ "hosting_git_ssh-public-key", "hosting_git_update-auto-deployment-settings", "hosting_headless_submit-build-report", - "hosting_import-wordpress-website", - "hosting_list-js-deployments", "hosting_nodejs_analyse-failed-build", "hosting_nodejs_build", "hosting_nodejs_build-logs", @@ -245,13 +247,18 @@ "hosting_php_get", "hosting_php_info", "hosting_php_reset-extensions", + "hosting_php_update", "hosting_php_update-extensions", "hosting_php_update-options", "hosting_php_update-version", + "hosting_pocketbase_backend", + "hosting_pocketbase_create-backend", + "hosting_pocketbase_delete-backend", + "hosting_pocketbase_list-backends", + "hosting_pocketbase_set-superuser", "hosting_redirects_create-website", "hosting_redirects_delete-website", "hosting_redirects_list-website", - "hosting_show-js-deployment-logs", "hosting_ssl_install", "hosting_ssl_status", "hosting_ssl_toggle-https-redirect", @@ -320,6 +327,8 @@ "reach_campaigns_get", "reach_campaigns_list", "reach_campaigns_performance", + "reach_campaigns_send", + "reach_campaigns_update", "reach_contact-fields_create", "reach_contact-fields_delete", "reach_contact-fields_list", diff --git a/skills/deploy-to-hosting/SKILL.md b/skills/deploy-to-hosting/SKILL.md index dcfec44..4536b11 100644 --- a/skills/deploy-to-hosting/SKILL.md +++ b/skills/deploy-to-hosting/SKILL.md @@ -112,7 +112,7 @@ Agency Plan sites have no Git operations; deploy archives. 1. `hosting_nodejs_list-environment-variables` for the current keys. Values come back as `********`; never send those back. 2. Build the full set: every current key plus the new ones, with real values from the project's `.env` or the user. When the real value of an existing key is unknown, ask — do not drop it. -3. Send it once; the app restarts. Values baked in at build time (Next.js `NEXT_PUBLIC_*`, Vite `VITE_*`) need a new `hosting_nodejs_start-build`. +3. Send it once; the app restarts. Values baked in at build time (Next.js `NEXT_PUBLIC_*`, Vite `VITE_*`) need a new build: deploy again with `hosting_deploy`; for local files uploaded to the hosted server, run `hosting_nodejs_start-build` again as in step 2. Keys use uppercase letters, digits and underscores. Never commit `.env` files or repeat secret values back to the user. diff --git a/skills/hostinger-headless/SKILL.md b/skills/hostinger-headless/SKILL.md index bd7de71..5016cf0 100644 --- a/skills/hostinger-headless/SKILL.md +++ b/skills/hostinger-headless/SKILL.md @@ -192,13 +192,15 @@ has usually outlasted the job, so it returns at once. The single rule that makes ### Waiting on Hostinger: `wait.mjs` `wait.mjs` (next to this file) waits for a slow Hostinger job in one command, so you don't spend a turn -on every check: +on every check. Run it, like `push-content.mjs`, from the home folder (`cd "$HOME" && …`): both +start the Hostinger MCP server with the user's sign-in, and a version manager's `node` (asdf, mise) +runs whatever Node the current project's `.tool-versions` names: ```bash -node /wait.mjs setup # a fresh site's setup → completed -node /wait.mjs cms # a shell swap's WordPress install at /cms → valid -node /wait.mjs build # the deploy build → completed, then the root serves 200 -node /wait.mjs live # the root serves 200 +cd "$HOME" && node /wait.mjs setup # a fresh site's setup → completed +cd "$HOME" && node /wait.mjs cms # a shell swap's WordPress install at /cms → valid +cd "$HOME" && node /wait.mjs build # the deploy build → completed, then the root serves 200 (http:// while SSL is pending) +cd "$HOME" && node /wait.mjs live # the root serves 200 ``` Run it in the foreground, with a `--timeout` just under your tool's command limit (e.g. `--timeout 9m` diff --git a/skills/hostinger-headless/VERSION b/skills/hostinger-headless/VERSION index 96ce396..f4e13f8 100644 --- a/skills/hostinger-headless/VERSION +++ b/skills/hostinger-headless/VERSION @@ -1,3 +1,3 @@ -version=3.29.0 -commit=c4c3f98adbe2 -built=2026-11-05 +version=4.0.1 +commit=9de0d1c40059 +built=2026-11-13 diff --git a/skills/hostinger-headless/connect/SKILL.md b/skills/hostinger-headless/connect/SKILL.md index 1bbf184..82b2230 100644 --- a/skills/hostinger-headless/connect/SKILL.md +++ b/skills/hostinger-headless/connect/SKILL.md @@ -77,10 +77,10 @@ Read `/VERSION`: its line 1 is the skills version this run uses, which go Run the bundled script. It verifies the MCP server and handles sign-in, emitting one JSON event per line on stdout. Run it as a background/streaming process. Show the user only the sign-in URL (`awaiting_user`) or a failure's `detail`, and handle the other events silently (table below). -The script is safe and inspectable: it only checks the MCP server via `npx` and drives the OAuth sign-in (a browser flow with a localhost callback) — no other network calls, no filesystem writes. +The script is safe and inspectable: it copies the launcher to `~/.hostinger/mcp/launch.mjs`, checks the MCP server through it and drives the OAuth sign-in (a browser flow with a localhost callback) — no other network calls, no other filesystem writes. ```bash -node /connect/bootstrap.mjs +cd "$HOME" && node /connect/bootstrap.mjs ``` ### Relay these events @@ -89,11 +89,12 @@ The script emits one JSON object per line: | Event | What to do | | --- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `launcher` (`command`, `args`) | How to start the MCP server: use these for every registration (Phase 2). | | `mcp_ok` | MCP server reachable — continue. | | `logged_in` | Already authenticated (env token or a previous sign-in) — continue. | | `awaiting_user` (`verificationUri`) | A browser window should have opened on the user's machine. Show the URL in plain prose and wait for the user to finish signing in. The OAuth callback lands on a localhost port, so the sign-in must be completed on this same machine. | | `success` | Bootstrap done — continue to Phase 2. | -| `mcp_unreachable` / `login_failed` (with `detail`) | Stop and show the user the `detail`. Do not improvise a parallel setup by hand. | +| `launcher_failed` / `mcp_unreachable` / `login_failed` (with `detail`) | Stop and show the user the `detail`. Do not improvise a parallel setup by hand. | If the environment has a `HOSTINGER_API_TOKEN` set (for example CI), the bootstrap reports `logged_in` immediately — no browser needed. @@ -107,8 +108,8 @@ hostinger-api-mcp --login hostinger-api-mcp --logout # Without a global install: -npx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --login -npx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout +cd "$HOME" && node "$HOME/.hostinger/mcp/launch.mjs" --login +cd "$HOME" && node "$HOME/.hostinger/mcp/launch.mjs" --logout ``` Credentials live in `~/.config/hostinger-mcp/credentials.json` (macOS/Linux) or `%APPDATA%\hostinger-mcp\credentials.json` (Windows) and are shared by every Hostinger MCP binary. OAuth works only when the tool runs the server over its default local (stdio) transport, which is what every registration below uses; `--http` needs `HOSTINGER_API_TOKEN`. @@ -127,8 +128,8 @@ one group says nothing about the other: | Group | Operations | Comes from | Needed by | |---|---|---|---| -| **Hosting** | `hosting_*` (websites, deploy, build report), `wordpress_*` | `hostinger` (`npx -y @hostinger/mcp@latest`), or the scoped servers such as `hostinger-hosting` + `hostinger-wordpress` | every build | -| **Ecommerce** | `ecommerce_*` (stores, products, shipping, payments) | **only** `hostinger-ecommerce` (`npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp`), or the Ecommerce MCP in the Hostinger Connector. Neither `hostinger` nor the scoped hosting servers include it | Ecommerce backends (chosen later, in refine) | +| **Hosting** | `hosting_*` (websites, deploy, build report), `wordpress_*` | `hostinger` (` `), or the scoped servers such as `hostinger-hosting` + `hostinger-wordpress` | every build | +| **Ecommerce** | `ecommerce_*` (stores, products, shipping, payments) | **only** `hostinger-ecommerce` (` hostinger-ecommerce-mcp`), or the Ecommerce MCP in the Hostinger Connector. Neither `hostinger` nor the scoped hosting servers include it | Ecommerce backends (chosen later, in refine) | Check each group on its own, right here at connect, before anything else: @@ -148,8 +149,8 @@ Most coding tools load MCP servers only when they start, so a server registered until the tool restarts. When either tool group above is missing: 1. **Register the missing servers** in the tool's MCP config, as the tool route below shows: `hostinger` - (`npx -y @hostinger/mcp@latest`) if the hosting group is missing, and `hostinger-ecommerce` - (`npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp`) if the ecommerce group is missing. + (` `) if the hosting group is missing, and `hostinger-ecommerce` + (` hostinger-ecommerce-mcp`) if the ecommerce group is missing. Skip a server that is already registered under its name. When neither group is there, register both: the backend isn't chosen until refine, and one restart then covers every backend. 2. **Write `connect.json`** (Phase 3) before you stop. The skill folder from Phase 1 stays in place, @@ -163,6 +164,29 @@ until the tool restarts. When either tool group above is missing: If a tool loads new servers without a restart (VS Code), use them right away and skip steps 3–4. +**Every registration and every command that starts the MCP server goes through the launcher** +(`~/.hostinger/mcp/launch.sh` and `launch.mjs`, which the bootstrap writes): never `npx` directly, with or without +`--prefix`. The tool starts the server, and you run commands, in the user's project, often a cloned +repository or template. npx run there puts the project's `node_modules/.bin` first on PATH and reads +its `.npmrc`, so whoever wrote the project runs their own `node` with the user's Hostinger sign-in. +The launcher runs npx from the home folder instead. A registration runs exactly the bootstrap's +`launcher` event, never a bare `node`: a version manager (asdf, mise) picks the Node that the project's +own `.tool-versions` names. On macOS and Linux the event is `/bin/sh` with `launch.sh`, which finds +Node only after moving to the home folder, so Node upgrades don't break it; on Windows it is the +absolute path of `node.exe` with `launch.mjs`. Replace `` and `` with that event's +`command` and `args[0]`, in shell commands and in JSON or TOML configs alike, and never leave a +placeholder in place. In JSON and TOML configs, double every backslash of a Windows path +(`C:\\Program Files\\nodejs\\node.exe`); a single one is an escape there (`\n` in `\nodejs` becomes a +newline). Commands you run yourself start in the home folder +(`cd "$HOME" && node …`) for the same reason. + +**An existing Hostinger registration is "already registered" only when it runs exactly the +bootstrap's `launcher` event** (same `command`, `args[0]` the launcher, plus `hostinger-ecommerce-mcp` +for that server). Before you trust one (both groups present, or a name the tool lists), check its +command (Claude Code: `claude mcp get `; otherwise the tool's MCP config). Anything else, such as +`npx`, a bare `node` or `hostinger-api-mcp`, or any other command or path than the event's: +remove it and register it again through the launcher, then follow *register, restart, resume*. + | Tool | Route | |-------------|---------------------------------| | Claude Code | `connect/tools/claude-code.md` | @@ -173,7 +197,7 @@ If a tool loads new servers without a restart (VS Code), use them right away and | Devin | `connect/tools/devin.md` | If the user's tool isn't listed yet, connect it the generic way: register both Hostinger MCP servers -in that tool's MCP config (command `npx`, args as above), then follow *register, restart, resume*. +in that tool's MCP config (command ``, args `` as above), then follow *register, restart, resume*. ### The skills come from Phase 1 only diff --git a/skills/hostinger-headless/connect/bootstrap.mjs b/skills/hostinger-headless/connect/bootstrap.mjs index 2840ab9..ffaa2cf 100644 --- a/skills/hostinger-headless/connect/bootstrap.mjs +++ b/skills/hostinger-headless/connect/bootstrap.mjs @@ -2,10 +2,10 @@ // Hostinger Headless — bootstrap. // Verifies the Hostinger MCP server is reachable and drives the OAuth sign-in, // emitting one JSON event per line on stdout so a coding agent can relay -// progress to the user. No filesystem writes besides what the MCP itself -// stores (OAuth credentials); no network calls of its own. +// progress to the user. Its only writes are the launcher (~/.hostinger/mcp/launch.mjs, which +// coding tools register) and what the MCP itself stores (OAuth credentials); no network calls of its own. import { spawn, spawnSync } from 'node:child_process'; -import { existsSync, readFileSync } from 'node:fs'; +import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; import os from 'node:os'; @@ -17,19 +17,30 @@ const fail = (event, extra = {}) => { process.exit(1); }; -// ── platform-safe binary names (npm/npx are .cmd on Windows) ───────────────── +// ── the MCP server, always through the launcher ────────────────────────────── +// launch.mjs runs the latest published package via npx from the home folder (see its header for why +// npx must never run in the project). Coding tools register the home copy, which every run refreshes, +// so a registration keeps working when the skill folder moves to a new version. const isWin = process.platform === 'win32'; -const bin = (name) => (isWin ? `${name}.cmd` : name); -// Run the MCP via npx — no global install, always the latest published version. -// Use --package so npx resolves the published package, then invoke the unified binary. -const MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api-mcp']; -// Node >= 18.20.2 / 20.12.2 refuses to spawn a .cmd/.bat without a shell -// (CVE-2024-27980), so npx.cmd needs shell:true on Windows. Our args are all -// fixed literals, so enabling the shell introduces no injection risk. -const spawnShell = isWin; +const LAUNCHER = path.join(os.homedir(), '.hostinger', 'mcp', 'launch.mjs'); +try { + mkdirSync(path.dirname(LAUNCHER), { recursive: true }); + copyFileSync(new URL('./launch.mjs', import.meta.url), LAUNCHER); + copyFileSync(new URL('./launch.sh', import.meta.url), LAUNCHER.replace(/\.mjs$/, '.sh')); +} catch (e) { + fail('launcher_failed', { detail: String(e) }); +} +const MCP = [process.execPath, LAUNCHER]; +// A registration never names a bare `node`: Windows looks it up in the working directory (the project) +// first, and a version manager's shim (asdf, mise) runs the Node the project's .tool-versions names. +// macOS/Linux register /bin/sh with launch.sh, which finds node only after moving to home, so Node +// upgrades keep working. Windows names this Node by absolute path (this script runs from home). +emit('launcher', isWin + ? { command: process.execPath, args: [LAUNCHER] } + : { command: '/bin/sh', args: [LAUNCHER.replace(/\.mjs$/, '.sh')] }); function capture(cmd, args) { - const r = spawnSync(cmd, args, { encoding: 'utf8', shell: spawnShell, cwd: os.homedir() }); + const r = spawnSync(cmd, args, { encoding: 'utf8', cwd: os.homedir() }); return { status: r.status ?? 1, out: `${r.stdout || ''}${r.stderr || ''}`, error: r.error }; } @@ -72,7 +83,7 @@ function existingAuth() { // prefix — forward it so the agent can show it if the browser didn't open. function login() { return new Promise((resolve) => { - const child = spawn(MCP[0], [...MCP.slice(1), '--login'], { shell: spawnShell, cwd: os.homedir() }); + const child = spawn(MCP[0], [...MCP.slice(1), '--login'], { cwd: os.homedir() }); let diag = ''; let urlSent = false; const collect = (chunk) => { diff --git a/skills/hostinger-headless/connect/launch.mjs b/skills/hostinger-headless/connect/launch.mjs new file mode 100644 index 0000000..c50629f --- /dev/null +++ b/skills/hostinger-headless/connect/launch.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env node +// Hostinger Headless — starts the latest Hostinger MCP server for a coding tool or a script. +// node launch.mjs [hostinger--mcp] [--login | --logout | --version] +// +// Coding tools start stdio servers in the user's project, often a cloned repository or template. +// npx run there puts /node_modules/.bin, and the same folder of every parent, first on PATH +// (no npm flag turns that off) and reads the project's .npmrc, so the project's own `node` would run +// the server with the user's Hostinger credentials. This launcher runs npx from the home folder, with +// no relative or node_modules entries left on PATH, so nothing in the project is ever looked up. +import { spawn } from 'node:child_process'; +import { homedir } from 'node:os'; +import path from 'node:path'; + +const [PACKAGE, DEFAULT_BIN] = ['--package=@hostinger/mcp@latest', 'hostinger-api-mcp']; +const isWin = process.platform === 'win32'; +const home = homedir(); + +const args = process.argv.slice(2); +// npx.cmd needs a shell on Windows (CVE-2024-27980), so every argument must be shell-inert. +if (args.some((a) => !/^[\w@.=:/-]+$/.test(a))) { + process.stderr.write(`launch.mjs: unexpected argument in ${JSON.stringify(args)}\n`); + process.exit(2); +} +const bin = /^[a-z0-9-]+-mcp$/.test(args[0] ?? '') ? args.shift() : DEFAULT_BIN; + +// Windows may spell it Path; a relative entry or a node_modules folder can come from the project. +// The server returns to the project once started, so relative paths in tool arguments still work +// (launch.sh has already moved to home and names the project itself). +const env = { ...process.env, MCP_PROJECT_DIR: process.env.MCP_PROJECT_DIR || process.cwd() }; +for (const key of Object.keys(env).filter((k) => k.toUpperCase() === 'PATH')) { + env[key] = env[key].split(path.delimiter) + .filter((p) => path.isAbsolute(p) && !p.split(/[\\/]/).includes('node_modules')) + .join(path.delimiter); +} + +const child = spawn(isWin ? 'npx.cmd' : 'npx', ['-y', PACKAGE, bin, ...args], { cwd: home, env, stdio: 'inherit', shell: isWin }); +for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => child.kill(signal)); +child.on('error', (e) => { + process.stderr.write(`launch.mjs: could not start npx: ${e.message}\n`); + process.exit(1); +}); +child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0))); diff --git a/skills/hostinger-headless/connect/launch.sh b/skills/hostinger-headless/connect/launch.sh new file mode 100644 index 0000000..d616c62 --- /dev/null +++ b/skills/hostinger-headless/connect/launch.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Hostinger Headless — what coding tools register on macOS and Linux: /bin/sh /.hostinger/mcp/launch.sh +# /bin/sh never moves, and node is looked up only here, after moving to the home folder, so a Node +# upgrade (Homebrew, nvm, asdf) keeps the registration working, and a version manager's shim picks the +# user's own Node, never the one a project's .tool-versions names. Relative and node_modules entries +# are dropped from PATH first: either could hold a node the project put there. +# The server returns to the project once started (relative paths in tool arguments), so name it first. +MCP_PROJECT_DIR=$(pwd) +export MCP_PROJECT_DIR +cd || exit 1 +set -f +safe= +IFS=: +for dir in $PATH; do + case $dir in + /*node_modules*) ;; + /*) safe=${safe:+$safe:}$dir ;; + esac +done +unset IFS +PATH=$safe +export PATH +exec node "${0%/*}/launch.mjs" "$@" diff --git a/skills/hostinger-headless/connect/tools/antigravity.md b/skills/hostinger-headless/connect/tools/antigravity.md index a71a9e5..9ce7160 100644 --- a/skills/hostinger-headless/connect/tools/antigravity.md +++ b/skills/hostinger-headless/connect/tools/antigravity.md @@ -12,7 +12,7 @@ description: "Specific Antigravity Hostinger MCP Server connection instructions The later stages drive the Hostinger connector through Antigravity's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md` says: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both -respond → use them and move on. +respond and both registrations run the launcher (*An existing Hostinger registration* in `connect/SKILL.md`) → use them and move on. If either is missing, add the missing server(s) to `~/.gemini/antigravity/mcp_config.json` (Windows: `C:\Users\\.gemini\antigravity\mcp_config.json`), which you can reach from the Agent panel's @@ -25,12 +25,12 @@ no tokens to handle: { "mcpServers": { "hostinger": { - "command": "npx", - "args": ["-y", "@hostinger/mcp@latest"] + "command": "", + "args": [""] }, "hostinger-ecommerce": { - "command": "npx", - "args": ["--package=@hostinger/mcp@latest", "hostinger-ecommerce-mcp"] + "command": "", + "args": ["", "hostinger-ecommerce-mcp"] } } } diff --git a/skills/hostinger-headless/connect/tools/claude-code.md b/skills/hostinger-headless/connect/tools/claude-code.md index 0929867..aeea2f9 100644 --- a/skills/hostinger-headless/connect/tools/claude-code.md +++ b/skills/hostinger-headless/connect/tools/claude-code.md @@ -19,14 +19,14 @@ groups as *Check both tool groups* in `connect/SKILL.md` says: `hostinger-wordpress`, `hostinger-domains`, …) never include it, so a session full of `mcp__hostinger-*__*` tools can still be missing it. -Both present → use them and move on. Either missing → register the missing server(s) at user scope +Both present and both registrations run the launcher (*An existing Hostinger registration* in `connect/SKILL.md`) → use them and move on. Either missing → register the missing server(s) at user scope now (`claude mcp list` shows what's already registered; skip those). The bootstrap already signed you in, so they reuse the cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no tokens to handle: ```bash -claude mcp add hostinger --scope user -- npx -y @hostinger/mcp@latest # hosting group missing -claude mcp add hostinger-ecommerce --scope user -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp # ecommerce group missing +claude mcp add hostinger --scope user -- "" "" # hosting group missing +claude mcp add hostinger-ecommerce --scope user -- "" "" hostinger-ecommerce-mcp # ecommerce group missing ``` Claude Code loads MCP servers only at startup, so follow *register, restart, resume* in diff --git a/skills/hostinger-headless/connect/tools/codex.md b/skills/hostinger-headless/connect/tools/codex.md index 4a9e08e..7ed0fe0 100644 --- a/skills/hostinger-headless/connect/tools/codex.md +++ b/skills/hostinger-headless/connect/tools/codex.md @@ -12,27 +12,27 @@ description: "Specific Codex Hostinger MCP Server connection instructions for th The later stages drive the Hostinger connector through Codex's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md` says: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both -respond → use them and move on. +respond and both registrations run the launcher (*An existing Hostinger registration* in `connect/SKILL.md`) → use them and move on. If either is missing, register the missing server(s); skip one already registered. The bootstrap already signed you in, so they reuse the cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no tokens to handle: ```bash -codex mcp add hostinger -- npx -y @hostinger/mcp@latest -codex mcp add hostinger-ecommerce -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp +codex mcp add hostinger -- "" "" +codex mcp add hostinger-ecommerce -- "" "" hostinger-ecommerce-mcp ``` or add them to `~/.codex/config.toml`: ```toml [mcp_servers.hostinger] -command = "npx" -args = ["-y", "@hostinger/mcp@latest"] +command = "" +args = [""] [mcp_servers.hostinger-ecommerce] -command = "npx" -args = ["--package=@hostinger/mcp@latest", "hostinger-ecommerce-mcp"] +command = "" +args = ["", "hostinger-ecommerce-mcp"] ``` Codex loads MCP servers only when it starts, so follow *register, restart, resume* in diff --git a/skills/hostinger-headless/connect/tools/cursor.md b/skills/hostinger-headless/connect/tools/cursor.md index 5ccb28e..99bd08a 100644 --- a/skills/hostinger-headless/connect/tools/cursor.md +++ b/skills/hostinger-headless/connect/tools/cursor.md @@ -13,13 +13,13 @@ description: "Specific Cursor Hostinger MCP Server connection instructions for t **Prerequisite:** Node.js 20 or newer on `PATH` (`node -v`). The published package engines require `>=20`; use Node 24 if you follow the package README. -The Hostinger MCP is the npm package `@hostinger/mcp`. You do **not** need a global install — Cursor (and this session) can run it through `npx`. Prefer the unified binary `hostinger-api-mcp` (every operation). Scoped binaries such as `hostinger-hosting-mcp` are optional. +The Hostinger MCP is the npm package `@hostinger/mcp`. You do **not** need a global install — Cursor (and this session) run it through the launcher (`~/.hostinger/mcp/`, written by the connect bootstrap), which starts the latest release with `npx` from your home folder. Never run `npx` for it directly: from the project, npx would run the project's own `node`. `` and `` below come from the bootstrap's `launcher` event (see `connect/SKILL.md`). Prefer the unified binary `hostinger-api-mcp` (every operation). Scoped binaries such as `hostinger-hosting-mcp` are optional. ### Option A — no global install (recommended) ```bash # Verify the package/binary is reachable -npx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --version +cd "$HOME" && node "$HOME/.hostinger/mcp/launch.mjs" --version ``` ### Option B — global install @@ -37,7 +37,7 @@ hostinger-api-mcp --version Check **both** tool groups, as *Check both tool groups* in `connect/SKILL.md` says: hosting (`mcp__hostinger__*` or `mcp__hostinger-hosting__*`) **and** ecommerce (`mcp__hostinger-ecommerce__*`). The scoped `mcp__hostinger-*__*` servers never include ecommerce, so they alone don't count. Both -present → skip registration: go to Step 2 only if you still need sign-in, otherwise go to Step 3. +present and both registrations run the launcher (*An existing Hostinger registration* in `connect/SKILL.md`) → skip registration: go to Step 2 only if you still need sign-in, otherwise go to Step 3. If either is missing, register the missing server(s) in `~/.cursor/mcp.json` (global, so every project and every later run has them). Merge them into any existing `mcpServers` rather than replacing the file. @@ -48,12 +48,12 @@ OAuth can run: { "mcpServers": { "hostinger": { - "command": "npx", - "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-api-mcp"] + "command": "", + "args": [""] }, "hostinger-ecommerce": { - "command": "npx", - "args": ["--yes", "--package=@hostinger/mcp@latest", "hostinger-ecommerce-mcp"] + "command": "", + "args": ["", "hostinger-ecommerce-mcp"] } } } @@ -74,7 +74,7 @@ If the connect bootstrap already emitted `logged_in` / `success`, credentials ar Otherwise run the login command. It registers a dynamic OAuth client (once per machine), **opens the user's browser** to Hostinger's authorization page, listens on a localhost callback, stores tokens, and exits: ```bash -npx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --login +cd "$HOME" && node "$HOME/.hostinger/mcp/launch.mjs" --login ``` (With a global install: `hostinger-api-mcp --login`.) @@ -95,7 +95,7 @@ They are shared across every Hostinger MCP binary. Expired access tokens refresh To sign out later: ```bash -npx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout +cd "$HOME" && node "$HOME/.hostinger/mcp/launch.mjs" --logout ``` **API token (optional, CI / scripts):** set `HOSTINGER_API_TOKEN` in the environment before launching Cursor. When present it always wins and no OAuth code runs. Do not commit the token. diff --git a/skills/hostinger-headless/connect/tools/devin.md b/skills/hostinger-headless/connect/tools/devin.md index e4f834f..7499ebe 100644 --- a/skills/hostinger-headless/connect/tools/devin.md +++ b/skills/hostinger-headless/connect/tools/devin.md @@ -12,7 +12,7 @@ description: "Specific Devin Hostinger MCP Server connection instructions for th The later stages drive the Hostinger connector through Devin's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md` says: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both -respond → use them and move on. +respond and both registrations run the launcher (*An existing Hostinger registration* in `connect/SKILL.md`) → use them and move on. If either is missing, register the missing server(s) for Devin; skip one already registered. The bootstrap already signed you in, so they reuse the cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no @@ -21,18 +21,18 @@ tokens to handle. - **Devin CLI:** ```bash - devin mcp add hostinger -- npx -y @hostinger/mcp@latest - devin mcp add hostinger-ecommerce -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp + devin mcp add hostinger -- "" "" + devin mcp add hostinger-ecommerce -- "" "" hostinger-ecommerce-mcp ``` - **Devin app:** Settings → MCP Marketplace → **Add Your Own**, transport **STDIO**, once per server: ```json - { "command": "npx", "args": ["-y", "@hostinger/mcp@latest"] } + { "command": "", "args": [""] } ``` ```json - { "command": "npx", "args": ["--package=@hostinger/mcp@latest", "hostinger-ecommerce-mcp"] } + { "command": "", "args": ["", "hostinger-ecommerce-mcp"] } ``` If the tools still aren't in this session, follow *register, restart, resume* in `connect/SKILL.md`: diff --git a/skills/hostinger-headless/connect/tools/vscode.md b/skills/hostinger-headless/connect/tools/vscode.md index fa8512f..068db26 100644 --- a/skills/hostinger-headless/connect/tools/vscode.md +++ b/skills/hostinger-headless/connect/tools/vscode.md @@ -12,7 +12,7 @@ description: "Specific VS Code Hostinger MCP Server connection instructions for The later stages drive the Hostinger connector through VS Code's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md` says: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both -respond → use them and move on. +respond and both registrations run the launcher (*An existing Hostinger registration* in `connect/SKILL.md`) → use them and move on. If either is missing, register the missing server(s) for VS Code; skip one already registered. The bootstrap already signed you in, so they reuse the cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are @@ -25,13 +25,13 @@ no tokens to handle. Add them to `.vscode/mcp.json` (workspace) or your user `se "servers": { "hostinger": { "type": "stdio", - "command": "npx", - "args": ["-y", "@hostinger/mcp@latest"] + "command": "", + "args": [""] }, "hostinger-ecommerce": { "type": "stdio", - "command": "npx", - "args": ["--package=@hostinger/mcp@latest", "hostinger-ecommerce-mcp"] + "command": "", + "args": ["", "hostinger-ecommerce-mcp"] } } } @@ -40,8 +40,8 @@ no tokens to handle. Add them to `.vscode/mcp.json` (workspace) or your user `se Or add them in two commands: ```bash -code --add-mcp '{"name":"hostinger","command":"npx","args":["-y","@hostinger/mcp@latest"]}' -code --add-mcp '{"name":"hostinger-ecommerce","command":"npx","args":["--package=@hostinger/mcp@latest","hostinger-ecommerce-mcp"]}' +code --add-mcp '{"name":"hostinger","command":"","args":[""]}' +code --add-mcp '{"name":"hostinger-ecommerce","command":"","args":["","hostinger-ecommerce-mcp"]}' ``` VS Code picks up new servers and their tools become available right away, with no editor restart. diff --git a/skills/hostinger-headless/hostinger-mcp.mjs b/skills/hostinger-headless/hostinger-mcp.mjs index 171b3a1..b24e03e 100644 --- a/skills/hostinger-headless/hostinger-mcp.mjs +++ b/skills/hostinger-headless/hostinger-mcp.mjs @@ -3,20 +3,15 @@ import { spawn } from 'node:child_process'; import { homedir } from 'node:os'; +import { fileURLToPath } from 'node:url'; -// ── platform-safe binary names (npm/npx are .cmd on Windows), as in connect/bootstrap.mjs ────────── -const isWin = process.platform === 'win32'; -const bin = (name) => (isWin ? `${name}.cmd` : name); -const MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api-mcp']; -// Node >= 18.20.2 / 20.12.2 refuses to spawn a .cmd/.bat without a shell -// (CVE-2024-27980), so npx.cmd needs shell:true on Windows. Our args are all -// fixed literals, so enabling the shell introduces no injection risk. -const spawnShell = isWin; +// ── always through the launcher, which runs npx from the home folder, never in the project ───────── +const MCP = [process.execPath, fileURLToPath(new URL('./connect/launch.mjs', import.meta.url))]; // One server per script run. Once it dies every pending and later call rejects, so the caller ends with // exit 2 rather than hanging. export function connect(clientName) { - const child = spawn(MCP[0], MCP.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], shell: spawnShell, cwd: homedir() }); + const child = spawn(MCP[0], MCP.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], cwd: homedir() }); const pending = new Map(); let buffer = ''; let diag = ''; diff --git a/skills/hostinger-headless/steps/03-provision/ecommerce/SKILL.md b/skills/hostinger-headless/steps/03-provision/ecommerce/SKILL.md index 3064fb8..5366e71 100644 --- a/skills/hostinger-headless/steps/03-provision/ecommerce/SKILL.md +++ b/skills/hostinger-headless/steps/03-provision/ecommerce/SKILL.md @@ -22,7 +22,7 @@ Provision a **Hostinger Ecommerce** backend for `` and prove it is ready > aren't (for example, a tool connected before connect registered both servers), register it now. The > command for each tool is in its `connect/tools/.md`; for Claude Code: > ```bash -> claude mcp add hostinger-ecommerce --scope user -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp +> claude mcp add hostinger-ecommerce --scope user -- "" "" hostinger-ecommerce-mcp > ``` > (Or enable the **Ecommerce** MCP in the Hostinger Connector.) A newly added MCP server only loads > after a restart, so pause the run with *register, restart, resume* from `connect/SKILL.md`: save @@ -49,7 +49,7 @@ Same lifecycle as `../wordpress/SKILL.md` Steps 1–2, but `type: "headless_ecom call a separate subdomain op first — there's no need in this flow). The response carries the generated domain; if it differs from the workspace dir, move `~/.hostinger//` → `/` (if `/` already exists, stop and ask instead of moving - into it) and update `brief.md`. Don't wait here. At the join, `node /wait.mjs setup ` + into it) and update `brief.md`. Don't wait here. At the join, `cd "$HOME" && node /wait.mjs setup ` returns once the setup is `completed` (see *Waiting on Hostinger* in the top-level `SKILL.md`). `website_type` will be `headless`. diff --git a/skills/hostinger-headless/steps/03-provision/webapp/SKILL.md b/skills/hostinger-headless/steps/03-provision/webapp/SKILL.md index 815c21e..42edbee 100644 --- a/skills/hostinger-headless/steps/03-provision/webapp/SKILL.md +++ b/skills/hostinger-headless/steps/03-provision/webapp/SKILL.md @@ -57,7 +57,7 @@ All website/database operations below go through the Hostinger connector (`execu `~/.hostinger//` (if `/` already exists, stop and ask instead of moving into it), update the domain in `brief.md`, and tell the user the new domain. -3. Don't wait here. At the join, `node /wait.mjs setup ` (see *Waiting on Hostinger* in +3. Don't wait here. At the join, `cd "$HOME" && node /wait.mjs setup ` (see *Waiting on Hostinger* in the top-level `SKILL.md`) returns once the setup is `completed`, usually at once by then. A `failed` setup → stop, point at hPanel. diff --git a/skills/hostinger-headless/steps/03-provision/wordpress/SKILL.md b/skills/hostinger-headless/steps/03-provision/wordpress/SKILL.md index 86efef4..e766cac 100644 --- a/skills/hostinger-headless/steps/03-provision/wordpress/SKILL.md +++ b/skills/hostinger-headless/steps/03-provision/wordpress/SKILL.md @@ -64,7 +64,7 @@ All operations below go through the Hostinger connector (`execute`). (if `/` already exists, stop and ask instead of moving into it), update the domain in `brief.md`, and tell the user the new domain. Admin credentials are generated by the platform; never ask the user for a password. -3. Don't wait here. At the join, `node /wait.mjs setup ` (see *Waiting on Hostinger* in +3. Don't wait here. At the join, `cd "$HOME" && node /wait.mjs setup ` (see *Waiting on Hostinger* in the top-level `SKILL.md`) returns once the setup is `completed`, usually at once by then. A `failed` setup → stop, point at hPanel. @@ -95,7 +95,7 @@ install WordPress straight into its `/cms` directory. This is the onboarding han generated database counts against the plan's limit. 3. The call is **asynchronous** — a success response only means the install job was **queued**. This is the *fire*: record `provision.in_progress` and return to prepare work. At the join, - `node /wait.mjs cms ` returns the `/cms` install row (`directory: "cms"`, + `cd "$HOME" && node /wait.mjs cms ` returns the `/cms` install row (`directory: "cms"`, valid) once it exists, usually within 1–2 minutes of the install. ## Step 3 — Verify (all must pass) diff --git a/skills/hostinger-headless/steps/04-content-model/SKILL.md b/skills/hostinger-headless/steps/04-content-model/SKILL.md index d57cbc5..3b0127a 100644 --- a/skills/hostinger-headless/steps/04-content-model/SKILL.md +++ b/skills/hostinger-headless/steps/04-content-model/SKILL.md @@ -170,30 +170,38 @@ Source every image in a few batched calls, not a search and a fetch per photo: "sourdough bread", "bakery interior", "Lisbon street"). - With an `UNSPLASH_ACCESS_KEY` in the environment, use the official API, one call per theme: `GET https://api.unsplash.com/search/photos?query=&orientation=landscape&per_page=20` with - header `Authorization: Client-ID `. Each result's `urls.raw` is the file, `alt_description` - what it shows and `user.name` the photographer; for each photo you keep, also `GET` its + header `Authorization: Client-ID `. Each result's `urls.raw` is the file (its `photo-…` + part, before `?`, is the ``), `id` the ``, `alt_description` what it shows and + `user.name` the photographer; for each photo you keep, also `GET` its `links.download_location` (same header), as the API terms require. - Without a key, open the theme's search page `https://unsplash.com/s/photos/` - with your **web-fetch tool**, once per theme, and ask it to list every free photo on the page with - its `images.unsplash.com/photo-<…>` URL quoted exactly (the part before `?`), the photo page - link, the alt text and the photographer. One fetch returns 10–20 candidates. Skip the - `plus.unsplash.com/premium_photo-…` results: they are Unsplash+ and not free. Fetch the themes in - parallel when your tool allows several calls at once. Fall back to a web search limited to - `unsplash.com` plus a fetch of one photo page (`https://unsplash.com/photos/<…>`) only for a - subject no search page covers. + with your **web-fetch tool**, once per theme, with this prompt (a short fixed answer keeps the + fetch fast): + + ```text + List up to 12 free photos on this page, one line each, in this exact format and nothing else: + | | | + is the photo-… part of its images.unsplash.com URL, copied exactly, before any "?". + is the part after unsplash.com/photos/ in its photo page link. + Skip plus.unsplash.com/premium_photo-… results: they are Unsplash+ and not free. + ``` + + Fetch the themes in parallel when your tool allows several calls at once. Fall back to a web + search limited to `unsplash.com` plus a fetch of one photo page (`https://unsplash.com/photos/<…>`) + only for a subject no search page covers. - Every other `unsplash.com` request (curl on any page, `/photos//download`, `/napi/…`, `/oembed`) hits a bot challenge: a `307` to `/.within.website` (Anubis) or a 401. If your fetch tool gets the challenge page too, stop: no user-agent spoofing or other workarounds. On a WordPress backend where Option A hasn't already failed, offer it; otherwise ask the user for an `UNSPLASH_ACCESS_KEY` or record the gap (step 5). -2. **Check them all in one command.** Pick a candidate per image brief from the alt text, then in one +2. **Check them all in one command.** Pick a candidate per image brief from its alt text, then in one shell command request each web-size URL - (`https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max`, query string replaced) and + (`https://images.unsplash.com/?w=1600&q=80&fm=jpg&fit=max`, query string replaced) and confirm 200 `image/jpeg`, and save a small preview (`?w=500`) of each into one folder, e.g.: ```bash mkdir -p ~/.hostinger//media/previews && cd ~/.hostinger//media/previews && \ - for id in photo- photo-; do + for id in ; do curl -s -o /dev/null -w "$id %{http_code} %{content_type}\n" "https://images.unsplash.com/$id?w=1600&q=80&fm=jpg&fit=max" curl -s -o "$id.jpg" "https://images.unsplash.com/$id?w=500&q=60&fm=jpg&fit=max" done @@ -211,8 +219,8 @@ Source every image in a few batched calls, not a search and a fetch per photo: `content.json` instead: `url`, `alt`, `filename`, and the credit as `caption`): ```markdown - Image: https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max - Image credit: — https://unsplash.com/photos/ (Unsplash License) + Image: https://images.unsplash.com/?w=1600&q=80&fm=jpg&fit=max + Image credit: — https://unsplash.com/photos/ (Unsplash License) Image alt: Image file: -.jpg ``` diff --git a/skills/hostinger-headless/steps/04-content-model/wordpress/SKILL.md b/skills/hostinger-headless/steps/04-content-model/wordpress/SKILL.md index 2819355..d61c63d 100644 --- a/skills/hostinger-headless/steps/04-content-model/wordpress/SKILL.md +++ b/skills/hostinger-headless/steps/04-content-model/wordpress/SKILL.md @@ -78,7 +78,7 @@ else is refused, with the field named. ## Step 2 — Run the push ```bash -node /push-content.mjs +cd "$HOME" && node /push-content.mjs ``` It mints the token itself (never printed), removes WordPress's "Hello world!" post and "Sample Page", diff --git a/skills/hostinger-headless/steps/06-frontend/SKILL.md b/skills/hostinger-headless/steps/06-frontend/SKILL.md index 4830637..3f96df8 100644 --- a/skills/hostinger-headless/steps/06-frontend/SKILL.md +++ b/skills/hostinger-headless/steps/06-frontend/SKILL.md @@ -182,11 +182,23 @@ Guidelines (framework-agnostic): ## Step 4 — Hand off -Confirm the project builds clean locally against the live backend, no placeholder/fixture content -remains, and `state.json → frontend` matches what you built. Update `state.json` (`stage: "validate"`). +Confirm the project builds clean locally against the live backend, looks right at desktop and phone +width (below), no placeholder/fixture content remains, and `state.json → frontend` matches what you +built. Update `state.json` (`stage: "validate"`). Validate (Step 7) owns the clean rebuild + user preview; deploy (Step 8) ships it with the `app_type` and output directory that match the chosen framework + rendering. +**Visual check.** With the site running locally against the live backend, look at the home page and one +page of each template (e.g. a post, a product, the contact page) at desktop and phone width. Take all the +screenshots first, then open them all in one turn where your tool can open several images at once, since +each separate look costs a model turn. Don't tile several pages onto one image or take one tall +full-page shot: the viewer shrinks big images, and a shrunk page hides small bugs like a wrapped line. +After a fix, re-shoot only what changed. Phone width is where most layout bugs show. Headless Chrome +won't size a window below ~500 px, so a phone-width window comes back cropped: set a 390 px viewport +(e.g. Playwright), or put one route in 1280 px and 390 px iframes side by side on a local page to get +both widths in one image. The live site runs this same build, so it needs no screenshots; deploy checks +that it serves. + > **Webapp:** just confirm the app builds clean and `state.json → frontend` is right — there's no > `content-model.md` or headless backend to check. A DB-backed app can't reach its database locally > (`127.0.0.1` is server-only), so its data layer is verified **after** deploy, not here. diff --git a/skills/hostinger-headless/steps/07-validate/SKILL.md b/skills/hostinger-headless/steps/07-validate/SKILL.md index 1ce085a..ed30cef 100644 --- a/skills/hostinger-headless/steps/07-validate/SKILL.md +++ b/skills/hostinger-headless/steps/07-validate/SKILL.md @@ -6,9 +6,9 @@ description: "Step 7 (validate) of the Hostinger Headless flow. OPTIONAL quick g # headless-step-validate > **Optional / fast.** Every stage before this one already verifies its own work (provision checks the -> backend, content-model push checks the REST counts, frontend builds clean), and **deploy verifies the -> live result**. So validate is **not** a re-run of all that — it's a quick confirmation, plus the -> optional human preview/sign-off. **Skip Steps 3–4 (rebuild + approval) entirely** on an autonomous or +> backend, content-model push checks the REST counts, frontend builds clean and looks right at desktop +> and phone width), and **deploy verifies the live result**. So validate is **not** a re-run of all +> that — it's a quick confirmation, plus the optional human preview/sign-off. **Skip Steps 3–4 (rebuild + approval) entirely** on an autonomous or > temp-domain run where no one is signing off; just do the two 10-second checks below and move on. Spend > time here only when a person wants to see it before it goes live. @@ -61,9 +61,10 @@ fixtures, placeholder copy or `.htaccess` in the output). ## 4 — Preview & approval (optional — only when a person is signing off) When someone wants to see it first: start the local preview (Astro: `npm run preview`; Next.js: -`npx next start`), check desktop and mobile widths, navigation and a couple of key routes, and present -it for **explicit approval**. If preview tooling is unavailable, give the user the command and URL. -On an autonomous or temp-domain run, skip this — deploy will verify the live result anyway. +`npx next start`), show the user a couple of key routes at desktop and phone width (capture phone width +as in frontend's *Visual check*; frontend already caught the layout bugs, so this is for the user, not a +re-review) and navigation, and present it for **explicit approval**. If preview tooling is unavailable, +give the user the command and URL. On an autonomous or temp-domain run, skip this — deploy will verify the live result anyway. Then (after approval, or immediately if 3–4 were skipped): `state.json` → `stage: "deploy"`, fresh `updated_at`; leave `provision` unchanged. diff --git a/skills/hostinger-headless/steps/08-deploy/SKILL.md b/skills/hostinger-headless/steps/08-deploy/SKILL.md index 2a498ab..b1214c5 100644 --- a/skills/hostinger-headless/steps/08-deploy/SKILL.md +++ b/skills/hostinger-headless/steps/08-deploy/SKILL.md @@ -15,13 +15,13 @@ This is an **instance-barrier** step: run it only after `provision.status == "co > **Connected to the hosted server (`mcp.hostinger.com`)?** Its `hosting_deploy` deploys Git > repositories only and cannot read a local `path`. Run this step through the local > `hostinger-api-mcp` server instead (see the connect step: -> `npx --yes --package=@hostinger/mcp@latest hostinger-api-mcp`). +> ` `). > **With a shell and the Hostinger CLI installed**, the same deploy is one command, with the build > settings passed exactly as the table below says. Run it from the project, since it links the current directory: > `cd ~/.hostinger//src && hostinger up --domain --type nodejs --app-type --output-directory --build-script build --node-version 22 --package-manager npm --yes --format json`. > It uploads, builds, waits and returns `data.buildUuid` and `data.buildState`; add `--no-wait` to -> return at once and wait with `node /wait.mjs build `. Then continue at +> return at once and wait with `cd "$HOME" && node /wait.mjs build `. Then continue at > "Verify" below. Recent CLI versions see that the site is headless (`data.headless: true`) and > deploy only its Node.js frontend, leaving `/cms` alone; still pass the settings above. @@ -96,6 +96,9 @@ Zip the project **source** with `package.json` at the archive root. **Include** `public/`, the framework config (`next.config.mjs` / `astro.config.mjs`), `tsconfig.json`, `package.json`, `package-lock.json`, `.nvmrc`. **Exclude** `node_modules/`, the build output (`.next/`, `dist/`, `out/`), `.git/`, and any `.htaccess` — the server installs and builds from source. +**Never archive secrets:** the commands below also leave out `.env*` files, `.npmrc` and key/certificate +files (`*.pem`, `*.key`, `*.p12`, `*.pfx`). A brought-in project often has a `.env.local`; its values go in +as environment variables (Step 1), never in the archive. > ## ⚠️ Write the archive outside `~/.hostinger/` (the deploy refuses it there) > `hosting_deploy` refuses any `path` under a hidden folder at the top of the home directory @@ -110,7 +113,8 @@ Zip the project **source** with `package.json` at the archive root. **Include** # macOS/Linux — the OS temp dir is outside the home directory cd ~/.hostinger//src DEPLOY_DIR=$(mktemp -d) -zip -r -X "$DEPLOY_DIR/app.zip" . -x 'node_modules/*' '.next/*' 'dist/*' 'out/*' '.git/*' '**/.htaccess' +zip -r -X "$DEPLOY_DIR/app.zip" . -x 'node_modules/*' '.next/*' 'dist/*' 'out/*' '.git/*' '**/.htaccess' \ + '.env*' '*/.env*' '.npmrc' '*/.npmrc' '*.pem' '*.key' '*.p12' '*.pfx' echo "$DEPLOY_DIR/app.zip" # the absolute path to pass as `path` in Step 3 ``` @@ -119,7 +123,8 @@ echo "$DEPLOY_DIR/app.zip" # the absolute path to pass as `path` in Step 3 # profile, which only you can write to (never a folder under C:\, which every local account can write into) cd $HOME\.hostinger\\src $DeployDir = Join-Path $HOME "hostinger-deploy\"; New-Item -ItemType Directory -Force $DeployDir | Out-Null -tar -a -cf "$DeployDir\app.zip" --exclude=node_modules --exclude=.next --exclude=dist --exclude=out --exclude=.git --exclude=.htaccess . +tar -a -cf "$DeployDir\app.zip" --exclude=node_modules --exclude=.next --exclude=dist --exclude=out --exclude=.git --exclude=.htaccess ` + --exclude=.env* --exclude=.npmrc --exclude=*.pem --exclude=*.key --exclude=*.p12 --exclude=*.pfx . "$DeployDir\app.zip" # the absolute path to pass as `path` in Step 3 ``` @@ -156,13 +161,17 @@ keeps the root from serving 403 (see the rule above). Wait for it in one command (see *Waiting on Hostinger* in the top-level `SKILL.md`): ```bash -node /wait.mjs build +cd "$HOME" && node /wait.mjs build ``` -It waits for `hosting_get-deployment` to report the build `completed`, then for `https:///` to -serve 200. What its result means here: +It waits for `hosting_get-deployment` to report the build `completed`, then up to 5 minutes for +`https:///` (or `http://` while the certificate is pending) to serve 200. What its result means here: - `ok: true`: the build is done and the root serves 200. Go on to Step 5. +- `ok: true` with `https_pending: true`: the build is done and `http:///` serves 200, but the + new domain's SSL certificate is still installing (this can take the better part of an hour). The + deploy worked: tell the user HTTPS will follow on its own, run Step 5 over `http://`, and don't + wait for the certificate. - `ok: false` with `build.state: failed`: read `log_tail` (the end of the build log) and `analysis`, fix locally, re-archive and redeploy (pass `fromLine` to `hosting_get-deployment` to read only new log output). diff --git a/skills/hostinger-headless/wait.mjs b/skills/hostinger-headless/wait.mjs index f3bdadc..428f482 100644 --- a/skills/hostinger-headless/wait.mjs +++ b/skills/hostinger-headless/wait.mjs @@ -4,8 +4,8 @@ // // node wait.mjs setup # hosting_websites_list-setups row → completed (fresh site) // node wait.mjs cms # wordpress_installations_list → /cms row is_valid (shell swap) -// node wait.mjs build # hosting_get-deployment → build completed, then the root serves 200 -// node wait.mjs live # https:/// serves 200 +// node wait.mjs build # hosting_get-deployment → build completed, then the root serves 200 (http:// while SSL is pending) +// node wait.mjs live # https:/// serves 200 (http:// while SSL is pending) // any of them: [--timeout 10m] // // It talks to the local Hostinger MCP server, which reuses the sign-in from the connect step. Progress @@ -78,12 +78,20 @@ const isTerminal = ({ error, status }) => (status >= 400 && status < 500 && ![40 const rowsFor = (data, domain) => (Array.isArray(data) ? data : data?.data ?? []).filter((r) => r.domain?.toLowerCase() === domain.toLowerCase()); +// A new domain's SSL certificate can take far longer than the build, and until then HTTPS fails at the +// TLS handshake. When plain HTTP serves the app, the deploy is done; HTTPS follows on its own. const waitLive = (domain, timeoutMs) => poll('live', timeoutMs, async () => { const status = await httpStatus(`https://${domain}/`); - return status === 200 ? { done: { ok: true, live: `https://${domain}/` } } : { status: `root ${status || 'unreachable'}` }; + if (status === 200) return { done: { ok: true, live: `https://${domain}/` } }; + if (status !== 0) return { status: `root ${status}` }; + const plain = await httpStatus(`http://${domain}/`); + if (plain === 200) return { done: { ok: true, live: `http://${domain}/`, https_pending: true } }; + return { status: `root unreachable over https, http ${plain || 'unreachable'}` }; }); +const LIVE_TIMEOUT = '5m'; + const KINDS = { setup: { args: [], @@ -140,14 +148,15 @@ const KINDS = { // Settings that didn't take make the root 403 for good, so don't wait for it to go live. const missing = ['app_type', 'output_directory', 'node_version'].filter((k) => build.buildSettings?.[k] == null); if (missing.length) return { ...build, ok: false, error: `build settings missing: ${missing.join(', ')}; pass them on hosting_deploy and redeploy` }; - const live = await waitLive(domain, timeoutMs - (Date.now() - started)); + const left = timeoutMs - (Date.now() - started); + const live = await waitLive(domain, Math.min(left, duration(LIVE_TIMEOUT))); return { ...build, ...live, waited_s: Math.round((Date.now() - started) / 1000) }; }, }, live: { args: [], - timeout: '5m', + timeout: LIVE_TIMEOUT, run: (_mcp, { domain }, timeoutMs) => waitLive(domain, timeoutMs), }, }; diff --git a/skills/maintain-wordpress/SKILL.md b/skills/maintain-wordpress/SKILL.md index cfa18f3..a41274a 100644 --- a/skills/maintain-wordpress/SKILL.md +++ b/skills/maintain-wordpress/SKILL.md @@ -65,7 +65,7 @@ When a site breaks: stop the run, deactivate the plugin updated last with `wordp - Page cache: `wordpress_litespeed-cache_show-lite-speed-status`; purge after design or content changes. - Object cache: `wordpress_object-cache_show-memcached-status`, then `wordpress_object-cache_toggle-memcached` with `enabled: true` — the usual quick speed-up. -- PHP version: `hosting_php_get` is large (around 25 KB), so read it only when the user asks about PHP. PHP 8.x is markedly faster than 7.x; confirm plugin compatibility before `hosting_php_update-version`. +- PHP version: `hosting_php_get` is large (around 25 KB), so read it only when the user asks about PHP. PHP 8.x is markedly faster than 7.x; confirm plugin compatibility before switching with `hosting_php_update` (`version`). The switch resets extensions and options to their defaults, so pass the custom ones from `hosting_php_get` in the same call. - wp-admin: `wordpress_login_create-links` returns temporary one-click login links. Give them to the user only; never store or reuse them. - Hostinger's own plugins update with `wordpress_plugins_update-hostinger` (`slug`). diff --git a/skills/troubleshoot-website/SKILL.md b/skills/troubleshoot-website/SKILL.md index f27ffe8..aac01f4 100644 --- a/skills/troubleshoot-website/SKILL.md +++ b/skills/troubleshoot-website/SKILL.md @@ -68,7 +68,7 @@ Batch `hosting_get-deployment` (the latest build with its log, failure analysis ### PHP -`hosting_php_get` returns every extension and option (around 25 KB), so call it only for PHP problems. `hosting_php_update-version` switches versions. `hosting_php_update-options` silently caps values at the plan maximum (`max` in `hosting_php_get`), so read the applied value back. Agency: `agency-hosting_php_list-options-for-website`, then `agency-hosting_php_replace-website-options` — a full replace, so send every custom option that should stay. +`hosting_php_get` returns every extension and option (around 25 KB), so call it only for PHP problems. `hosting_php_update` changes the version, extensions and options in one call. Switching the version resets the extensions and options to their defaults, so pass the ones to keep in the same call. Option values are silently capped at the plan maximum (`max` in `hosting_php_get`), so read the applied value back. Agency: `agency-hosting_php_list-options-for-website`, then `agency-hosting_php_replace-website-options` — a full replace, so send every custom option that should stay. ### Agency Plan sites