@awesomate/hosting-mcp 0.7.2 → 0.8.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/dist/index.js +124 -0
- package/package.json +3 -3
- package/skill/awesomate-app-builder/SKILL.md +104 -0
- package/skill/awesomate-app-builder/references/stack-decision.md +52 -0
- package/skill/awesomate-credentials/SKILL.md +62 -0
- package/skill/awesomate-credentials/references/where-secrets-go.md +26 -0
- package/skill/awesomate-credentials/scripts/capture-secret.mjs +41 -0
- package/skill/awesomate-github/SKILL.md +71 -0
- package/skill/awesomate-github/references/github-basics.md +12 -0
- package/skill/awesomate-github/scripts/github-setup.mjs +126 -0
- package/skill/awesomate-hosting/SKILL.md +33 -0
- package/skill/awesomate-hosting/scripts/bootstrap.mjs +33 -0
- package/skill/awesomate-hosting/scripts/support-report.mjs +261 -0
- package/skill/awesomate-seo/SKILL.md +71 -0
- package/skill/awesomate-seo/references/seo-checklist.md +72 -0
package/dist/index.js
CHANGED
|
@@ -40179,6 +40179,130 @@ server.registerTool(
|
|
|
40179
40179
|
}
|
|
40180
40180
|
}
|
|
40181
40181
|
);
|
|
40182
|
+
readTool(
|
|
40183
|
+
"awesomate_app_context",
|
|
40184
|
+
"Call FIRST before any app-builder work. Returns the connected account's plan, whether the app builder is unlocked (capability.appBuilder \u2014 Support Plus+), whether they have a cPanel hosting account yet, their existing apps, firstRun, and capability-aware next-step suggestions to offer the user. If appBuilder is false, relay the upgrade suggestion rather than attempting to build.",
|
|
40185
|
+
"/api/my-apps/context"
|
|
40186
|
+
);
|
|
40187
|
+
readTool(
|
|
40188
|
+
"awesomate_app_list",
|
|
40189
|
+
"List the client\u2019s apps (id, slug, kind, status, primary_domain, github repo). Use to find an appId before get/deploy.",
|
|
40190
|
+
"/api/my-apps/apps"
|
|
40191
|
+
);
|
|
40192
|
+
server.registerTool(
|
|
40193
|
+
"awesomate_app_get",
|
|
40194
|
+
{
|
|
40195
|
+
description: "Get one app plus its environments (subdomains, ports, db engine/name, deploy + health state). Poll this after awesomate_app_create \u2014 status goes provisioning \u2192 active | failed (read provision_error on failure).",
|
|
40196
|
+
inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_list / _create") }
|
|
40197
|
+
},
|
|
40198
|
+
async ({ appId }) => {
|
|
40199
|
+
try {
|
|
40200
|
+
return textResult(await hubGet(requireConfig(), `/api/my-apps/apps/${appId}`));
|
|
40201
|
+
} catch (err) {
|
|
40202
|
+
return errorResult(err);
|
|
40203
|
+
}
|
|
40204
|
+
}
|
|
40205
|
+
);
|
|
40206
|
+
server.registerTool(
|
|
40207
|
+
"awesomate_app_create",
|
|
40208
|
+
{
|
|
40209
|
+
description: "Provision a new app on the client's cPanel account. Choose the stack deliberately (see the awesomate-app-builder skill): kind 'static' = a single fast landing/lead page served straight from the docroot (no DB, no server process \u2014 pick this for brochure/landing/lead-capture); kind 'node' = a dynamic app with a backend + database (logins, custom logic, an API). For node, dbEngine 'postgres' suits relational/JSON/AI-heavy data, 'mysql' is the simple default. Returns 202 immediately with an appId + subdomain(s); poll awesomate_app_get until status=active. Requires apps:write (Support Plus+) \u2014 a 403 means offer an upgrade. Get the user's confirmation on the stack + name before calling.",
|
|
40210
|
+
inputSchema: {
|
|
40211
|
+
appSlug: external_exports.string().regex(/^[a-z][a-z0-9]{1,15}$/).describe("2-16 chars, starts with a letter, lowercase alphanumeric \u2014 used for subdomains + db names"),
|
|
40212
|
+
kind: external_exports.enum(["node", "static"]).default("node").describe("'static' for a landing/lead page (no DB); 'node' for a dynamic app"),
|
|
40213
|
+
dbEngine: external_exports.enum(["mysql", "postgres"]).default("mysql").describe("node only \u2014 the database engine to provision"),
|
|
40214
|
+
template: external_exports.string().optional().describe("Starter template (defaults: node \u2192 'node-auth-sync', static \u2192 'static-landing')")
|
|
40215
|
+
}
|
|
40216
|
+
},
|
|
40217
|
+
async ({ appSlug, kind, dbEngine, template }) => {
|
|
40218
|
+
try {
|
|
40219
|
+
return textResult(
|
|
40220
|
+
await hubPost(requireConfig(), "/api/my-apps/apps", { appSlug, kind, dbEngine, template })
|
|
40221
|
+
);
|
|
40222
|
+
} catch (err) {
|
|
40223
|
+
return errorResult(err);
|
|
40224
|
+
}
|
|
40225
|
+
}
|
|
40226
|
+
);
|
|
40227
|
+
server.registerTool(
|
|
40228
|
+
"awesomate_app_deploy",
|
|
40229
|
+
{
|
|
40230
|
+
description: "Report how to deploy an app and its per-env targets. Node apps deploy via git push (dev/staging/main \u2192 GitHub Actions \u2192 cPanel); this returns the branch\u2192env map and subdomains. Use the awesomate-github skill to wire push-to-deploy the first time.",
|
|
40231
|
+
inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
|
|
40232
|
+
},
|
|
40233
|
+
async ({ appId }) => {
|
|
40234
|
+
try {
|
|
40235
|
+
return textResult(await hubPost(requireConfig(), `/api/my-apps/apps/${appId}/deploy`, {}));
|
|
40236
|
+
} catch (err) {
|
|
40237
|
+
return errorResult(err);
|
|
40238
|
+
}
|
|
40239
|
+
}
|
|
40240
|
+
);
|
|
40241
|
+
server.registerTool(
|
|
40242
|
+
"awesomate_app_set_env",
|
|
40243
|
+
{
|
|
40244
|
+
description: "Store an API key / secret for a Node app environment: it's encrypted in the hub AND injected into the app's .env (0600) then the app restarts. ALWAYS use this instead of putting a secret in code, a committed file, or leaving it in the chat. NEVER echo the value back \u2014 confirm with the key name only. If the user pastes a key in chat, store it here and tell them (per the awesomate-credentials skill) it should be rotated since it passed through the transcript. Node apps only (static sites have no server env).",
|
|
40245
|
+
inputSchema: {
|
|
40246
|
+
appId: external_exports.number().int().positive().describe("The app id"),
|
|
40247
|
+
key: external_exports.string().regex(/^[A-Z][A-Z0-9_]{0,63}$/).describe("Env var name, e.g. OPENAI_API_KEY"),
|
|
40248
|
+
value: external_exports.string().min(1).describe("The secret value (stored encrypted; never echoed)"),
|
|
40249
|
+
env: external_exports.enum(["dev", "staging", "prod"]).default("dev").describe("Which environment to set it on")
|
|
40250
|
+
}
|
|
40251
|
+
},
|
|
40252
|
+
async ({ appId, key, value, env }) => {
|
|
40253
|
+
try {
|
|
40254
|
+
return textResult(await hubPost(requireConfig(), `/api/my-apps/apps/${appId}/env`, { key, value, env }));
|
|
40255
|
+
} catch (err) {
|
|
40256
|
+
return errorResult(err);
|
|
40257
|
+
}
|
|
40258
|
+
}
|
|
40259
|
+
);
|
|
40260
|
+
server.registerTool(
|
|
40261
|
+
"awesomate_n8n_attach_to_app",
|
|
40262
|
+
{
|
|
40263
|
+
description: "Wire an app to call the user's own n8n as a backend. Give it the app + a webhook URL you already created/tested/promoted on their n8n (via awesomate_n8n_deploy \u2014 create_draft returns the webhook URL, and promote preserves it). It stores N8N_WEBHOOK_URL + a generated N8N_WEBHOOK_SECRET on the app (encrypted + injected) and RETURNS the secret so you can add a matching `X-Awesomate-Webhook-Secret` header check to the n8n workflow (so the webhook isn't world-callable). The app then calls it via src/lib/n8n.ts callWorkflow(). Node apps only. Only reach for this when the job needs an external app/credential or AI the user already has in n8n \u2014 not for pure in-app logic.",
|
|
40264
|
+
inputSchema: {
|
|
40265
|
+
appId: external_exports.number().int().positive().describe("The app id"),
|
|
40266
|
+
webhookUrl: external_exports.string().url().describe("The n8n workflow's production webhook URL (from awesomate_n8n_deploy)"),
|
|
40267
|
+
env: external_exports.enum(["dev", "staging", "prod"]).default("dev").describe("Which app environment to wire")
|
|
40268
|
+
}
|
|
40269
|
+
},
|
|
40270
|
+
async ({ appId, webhookUrl, env }) => {
|
|
40271
|
+
try {
|
|
40272
|
+
return textResult(await hubPost(requireConfig(), `/api/my-apps/apps/${appId}/attach-n8n`, { webhookUrl, env }));
|
|
40273
|
+
} catch (err) {
|
|
40274
|
+
return errorResult(err);
|
|
40275
|
+
}
|
|
40276
|
+
}
|
|
40277
|
+
);
|
|
40278
|
+
server.registerTool(
|
|
40279
|
+
"awesomate_app_health",
|
|
40280
|
+
{
|
|
40281
|
+
description: "Probe an app's environments live and report health. Node envs hit /api/ready (200 = deployed + DB up + migrations applied); static hits the root. An env that isn't deployed yet reports unreachable \u2014 expected, not a failure. Use after a deploy to confirm it came up, or when the user says something's down.",
|
|
40282
|
+
inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
|
|
40283
|
+
},
|
|
40284
|
+
async ({ appId }) => {
|
|
40285
|
+
try {
|
|
40286
|
+
return textResult(await hubPost(requireConfig(), `/api/my-apps/apps/${appId}/health`, {}));
|
|
40287
|
+
} catch (err) {
|
|
40288
|
+
return errorResult(err);
|
|
40289
|
+
}
|
|
40290
|
+
}
|
|
40291
|
+
);
|
|
40292
|
+
server.registerTool(
|
|
40293
|
+
"awesomate_n8n_provision_pg",
|
|
40294
|
+
{
|
|
40295
|
+
description: "Provision a NEW Postgres database on the user's own Awesomate cPanel hosting and wire it into their n8n as a ready-to-use `postgres` credential their workflows can select. This is a WRITE that creates real infrastructure \u2014 get explicit user approval first. Requires: their n8n builder access (Support Plus+, allow_client_cli_builds) AND the allow_pg_writes consent (403 consent_required \u2192 send them to settingsUrl); AND an Awesomate cPanel hosting account (409 hosting_required otherwise). Limited to 2/day. Returns the new credential id + name, the database/user, and the password ONCE (it lives in the n8n credential, never stored by Awesomate \u2014 tell the user to note or rotate it). After this, build the workflow selecting the returned Postgres credential.",
|
|
40296
|
+
inputSchema: {}
|
|
40297
|
+
},
|
|
40298
|
+
async () => {
|
|
40299
|
+
try {
|
|
40300
|
+
return textResult(await hubPost(requireConfig(), "/api/my-n8n/machine/pg-credentials", {}));
|
|
40301
|
+
} catch (err) {
|
|
40302
|
+
return errorResult(err);
|
|
40303
|
+
}
|
|
40304
|
+
}
|
|
40305
|
+
);
|
|
40182
40306
|
async function main() {
|
|
40183
40307
|
try {
|
|
40184
40308
|
config2 = loadConfig();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, and
|
|
3
|
+
"version": "0.8.2",
|
|
4
|
+
"description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"publishConfig": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --external:node:*",
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
|
-
"test": "esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && node --test test
|
|
22
|
+
"test": "esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && node --test test/*.test.mjs",
|
|
23
23
|
"prepublishOnly": "npm run typecheck && npm run test && npm run build"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: awesomate-app-builder
|
|
3
|
+
description: Build real things on the user's Awesomate hosting from plain English — a website, a landing page, or a web app with a database. Use when the user says "get started", "what can you do", "build me an app", "make a website / landing page", "I need a form / signup / dashboard", "help", or mentions their Awesomate hosting and wants something built. The first-run greeter for non-technical users. Companion to awesomate-hosting, awesomate-credentials, awesomate-github and awesomate-seo — same connection, same PAT.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Awesomate App Builder — build real things from plain English
|
|
7
|
+
|
|
8
|
+
The user is likely **non-technical**. Your job: understand what they actually
|
|
9
|
+
want in plain words, pick the simplest stack that fits, and build + ship it on
|
|
10
|
+
their Awesomate hosting — explaining just enough, never jargon-dumping. Ask
|
|
11
|
+
short questions one or two at a time. Provisioning is consent-free (it's their
|
|
12
|
+
own hosting) but you should always confirm the **stack + name** before creating.
|
|
13
|
+
|
|
14
|
+
## 0. First run (every session)
|
|
15
|
+
|
|
16
|
+
1. Run `awesomate_whoami` — everything acts on whichever ACCOUNT this folder
|
|
17
|
+
resolves to. Every tool response is stamped `account: <slug>`; if it isn't
|
|
18
|
+
the account the user means, fix the pin/connection first (see the
|
|
19
|
+
awesomate-hosting skill's multi-account section).
|
|
20
|
+
2. Call `awesomate_app_context` once and cache it. It returns:
|
|
21
|
+
- `capability.appBuilder` — if **false**, building is Support Plus+. Relay
|
|
22
|
+
the upgrade suggestion (don't attempt to build); reads still work.
|
|
23
|
+
- `hasCpanelAccount` — if **false**, they need hosting set up first (hand
|
|
24
|
+
off to the awesomate-hosting skill).
|
|
25
|
+
- `firstRun` + `suggestions[]` — on first run, greet warmly and offer the
|
|
26
|
+
suggestions as a short menu ("Here's what I can do for you… what would
|
|
27
|
+
you like?"). Don't dump all tools.
|
|
28
|
+
- `apps[]` — their existing apps; offer to continue one instead of starting
|
|
29
|
+
over.
|
|
30
|
+
|
|
31
|
+
## 1. Decide the stack (do this before building)
|
|
32
|
+
|
|
33
|
+
Read [references/stack-decision.md](references/stack-decision.md) — it's the
|
|
34
|
+
decision brain. The short version, matched to what they say:
|
|
35
|
+
|
|
36
|
+
- **"I have a WordPress site" / blog / regular content / needs to rank on
|
|
37
|
+
Google** → **WordPress** (hand to the awesomate-hosting skill). Then use
|
|
38
|
+
**awesomate-seo** so it's findable.
|
|
39
|
+
- **Shop / sell products** → **WordPress + WooCommerce**.
|
|
40
|
+
- **Landing page / capture leads / one-pager / "just needs to look good and
|
|
41
|
+
be found"** → **static site** (`kind: 'static'`). Fastest possible load,
|
|
42
|
+
no database. This is the right default for marketing pages — don't reach for
|
|
43
|
+
a full app.
|
|
44
|
+
- **App with logins / custom logic / a dashboard / an API** → **Node app**
|
|
45
|
+
(`kind: 'node'`). Pick **postgres** when the data is relational / needs
|
|
46
|
+
concurrency / JSON / analytics; **mysql** for simple cases.
|
|
47
|
+
|
|
48
|
+
**Never default to WordPress.** Match the tech to the job; prefer the least
|
|
49
|
+
machinery that does it.
|
|
50
|
+
|
|
51
|
+
### When to use their n8n instead of coding a backend
|
|
52
|
+
If the app needs to **send an email, look something up in their apps (Sheets,
|
|
53
|
+
CRM, a database), or use AI** — their **own n8n** is often the fastest path,
|
|
54
|
+
because their credentials are already connected there. Check the credential
|
|
55
|
+
inventory (`GET /api/my-n8n/machine/credentials`) and say e.g. *"you already
|
|
56
|
+
have Gmail connected in n8n — want the form to email you through that?"* Use
|
|
57
|
+
the **awesomate-n8n** skill to build the workflow, then store its webhook URL
|
|
58
|
+
in the app (see the awesomate-credentials skill) and call it from the app.
|
|
59
|
+
Don't reach for n8n for pure in-app logic with no external app/credential — a
|
|
60
|
+
webhook round-trip adds latency and a failure point for nothing.
|
|
61
|
+
|
|
62
|
+
## 2. The build loop
|
|
63
|
+
|
|
64
|
+
Work in short phases, keeping the user in plain language. **Dev is the default
|
|
65
|
+
target; never touch prod without an explicit ask.**
|
|
66
|
+
|
|
67
|
+
1. **Discovery** — one or two plain questions until you know the job + the
|
|
68
|
+
stack (§1). Don't over-interview.
|
|
69
|
+
2. **Design** — say back, in a sentence or two, what you'll build and the stack
|
|
70
|
+
you recommend. Get a yes.
|
|
71
|
+
3. **Confirmation gate** — confirm the **name** (2–16 lowercase letters/digits,
|
|
72
|
+
starts with a letter — becomes the subdomain + db names) and the **stack**,
|
|
73
|
+
then call `awesomate_app_create` (`appSlug`, `kind`, and for node `dbEngine`).
|
|
74
|
+
It returns **202** with an `appId` + subdomain(s); poll `awesomate_app_get`
|
|
75
|
+
until `status` is `active` (or `failed` — read `provision_error`, explain
|
|
76
|
+
plainly).
|
|
77
|
+
4. **Implementation** — build the actual code/content locally against the
|
|
78
|
+
template. Handle **secrets** only via the **awesomate-credentials** skill
|
|
79
|
+
(encrypted + injected, never committed/echoed). Wire **version control**
|
|
80
|
+
from the first commit via **awesomate-github**. If it needs email/lookup/AI,
|
|
81
|
+
attach n8n (§"when to use n8n").
|
|
82
|
+
5. **Testing** — deploy to **dev** (`git push` to `dev`), then
|
|
83
|
+
`awesomate_app_health` to confirm it came up (Node: `/api/ready`). Show the
|
|
84
|
+
user the live dev URL and let them try it. Iterate on dev until they're happy.
|
|
85
|
+
6. **Wrap-up / promote** — only when the user approves, promote **dev → staging
|
|
86
|
+
→ main** (merge + push per branch). `awesomate_app_deploy` reports the
|
|
87
|
+
branch→env map. Prod is `main`.
|
|
88
|
+
|
|
89
|
+
**Promote / rollback (git-based):** promoting is merging the tested branch
|
|
90
|
+
forward (`dev`→`staging`→`main`); rolling back is `git revert` of the bad
|
|
91
|
+
commit + push (redeploys the previous good state), or redeploy an earlier good
|
|
92
|
+
SHA. Always `awesomate_app_health` after a promote. Static sites ship their
|
|
93
|
+
files directly (no build step).
|
|
94
|
+
|
|
95
|
+
## 3. Hard rules
|
|
96
|
+
|
|
97
|
+
- Non-technical user: explain in plain words, one or two short questions at a
|
|
98
|
+
time, no unexplained jargon. Recommend; don't quiz them on architecture.
|
|
99
|
+
- Confirm the stack + name before `awesomate_app_create`. Default the target
|
|
100
|
+
to **dev**; never push to prod without an explicit ask.
|
|
101
|
+
- A `403` on a write = plan gate (Support Plus+). Offer an upgrade, don't retry.
|
|
102
|
+
- Never invent that a capability exists — read `awesomate_app_context` first.
|
|
103
|
+
- Never print a secret or commit a `.env` (the credentials + github skills
|
|
104
|
+
enforce this).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Stack decision — match the tech to the job
|
|
2
|
+
|
|
3
|
+
Bias toward whatever **loads fastest and is the least machinery** for the need.
|
|
4
|
+
Ask at most 2–3 plain questions, then recommend a stack in one sentence and
|
|
5
|
+
confirm before building. **Never default to WordPress.**
|
|
6
|
+
|
|
7
|
+
## The map
|
|
8
|
+
|
|
9
|
+
| What the user is really trying to do | Stack | Why / how |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| "I already have a WordPress site" / a blog / publishes content regularly / **wants to rank on Google & AI** | **WordPress** | Best CMS for indexable content, sitemaps, schema. Hand to the awesomate-hosting skill to provision, then run **awesomate-seo**. |
|
|
12
|
+
| Sell products / online shop | **WordPress + WooCommerce** | Reuse the `woo-agent-storefront` (AI-shopping feeds + `llms.txt`) and `woo-catalog-perfection` skills. |
|
|
13
|
+
| A landing page / capture leads / a one-pager / "just needs to look good and be found" | **static site** (`kind: 'static'`) | Served straight from the cPanel docroot — no database, no server process, fastest load. Add SEO + a lead form (below). The right default for marketing pages. |
|
|
14
|
+
| A tool/app with **logins, custom logic, a dashboard, or an API** | **Node app** (`kind: 'node'`) | A real backend + database. |
|
|
15
|
+
|
|
16
|
+
### Within a Node app: which database?
|
|
17
|
+
- **postgres** — relational data with relationships, concurrency, JSON columns,
|
|
18
|
+
or analytics/AI features. The modern default for a real app.
|
|
19
|
+
- **mysql** — simple/legacy needs, or when they already know MySQL.
|
|
20
|
+
- Provisioned automatically by `awesomate_app_create`; the app's `.env` gets
|
|
21
|
+
`DB_ENGINE`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS`, and a
|
|
22
|
+
ready-to-use `DATABASE_URL`.
|
|
23
|
+
|
|
24
|
+
## Questions that resolve the choice fast
|
|
25
|
+
1. "Is this mainly a **page people read** (marketing, blog, shop) or a **tool
|
|
26
|
+
people use** (log in, do things, see their own data)?" → read → WP/static;
|
|
27
|
+
tool → Node.
|
|
28
|
+
2. If read: "Do you need to **publish/edit content often** (blog, many pages)?"
|
|
29
|
+
→ yes → WordPress; no → static.
|
|
30
|
+
3. If tool: "Will people **sign in** or does it store data per user?" → yes →
|
|
31
|
+
Node + postgres.
|
|
32
|
+
|
|
33
|
+
## Does it need a backend job? Prefer their n8n.
|
|
34
|
+
If the ask includes **email someone, look something up in their apps, save a
|
|
35
|
+
lead somewhere, or use AI**, that's usually a job for their **own n8n** — their
|
|
36
|
+
credentials are already connected there, so there's nothing new to set up.
|
|
37
|
+
|
|
38
|
+
- Check `GET /api/my-n8n/machine/credentials` (names/types only) and offer what
|
|
39
|
+
they already have: *"You've got Gmail + Google Sheets connected — want the
|
|
40
|
+
form to email you and add the lead to a sheet?"*
|
|
41
|
+
- Build the workflow with the **awesomate-n8n** skill (webhook trigger →
|
|
42
|
+
action), take its **webhook URL**, and store it in the app as
|
|
43
|
+
`N8N_WEBHOOK_URL` via the **awesomate-credentials** skill. The static
|
|
44
|
+
template's lead form and the Node template's `src/lib/n8n.ts` both POST there.
|
|
45
|
+
- Skip n8n for pure in-app logic with no external app/credential/AI — a webhook
|
|
46
|
+
round-trip just adds latency and a failure point.
|
|
47
|
+
|
|
48
|
+
## Always, for any public site
|
|
49
|
+
Offer **awesomate-seo** (meta/OG tags, `sitemap.xml`, `robots.txt`, JSON-LD, a
|
|
50
|
+
general `llms.txt`) so it's findable by search engines *and* AI assistants — and
|
|
51
|
+
**awesomate-github** so their work is version-controlled and deploys on push
|
|
52
|
+
from day one.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: awesomate-credentials
|
|
3
|
+
description: Safely capture and store API keys, tokens, database URLs and other secrets for the user's Awesomate apps — encrypted in the hub and injected into the app's .env, never committed to git, never echoed back. Use whenever the user pastes something that looks like a secret, says "here's my API key", "add this key", "store this secret / token / password", "save my credentials", or drops a key into a file. Companion to awesomate-app-builder.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Awesomate Credentials — handle secrets safely
|
|
7
|
+
|
|
8
|
+
Non-technical users will hand you secrets the easy way: pasted into chat or
|
|
9
|
+
dropped in a file. Your job is to get them into the right place **safely** and
|
|
10
|
+
never leak them.
|
|
11
|
+
|
|
12
|
+
## The rules (non-negotiable)
|
|
13
|
+
|
|
14
|
+
1. **Never echo a secret value back** — not in a summary, not in a code block,
|
|
15
|
+
not "just to confirm". Confirm with the **key name + a redacted preview
|
|
16
|
+
only** (e.g. `OPENAI_API_KEY = sk-…redacted`).
|
|
17
|
+
2. **Never commit a secret.** App secrets live in `.env` (gitignored) or the
|
|
18
|
+
hub's encrypted store — never in tracked files. If you see a secret in code
|
|
19
|
+
about to be committed, stop and move it.
|
|
20
|
+
3. **A secret pasted in chat is exposed.** Store it, then tell the user plainly:
|
|
21
|
+
*"Because that key was pasted into the chat, treat it as exposed — rotate it
|
|
22
|
+
when convenient, and next time drop it in the inbox file (below) instead."*
|
|
23
|
+
4. **Prefer the file-drop path** for anything sensitive (below).
|
|
24
|
+
|
|
25
|
+
## Detect and offer
|
|
26
|
+
|
|
27
|
+
If the user's message contains something shaped like a secret — `sk-…`,
|
|
28
|
+
`ghp_`/`gho_…`, `AKIA…`, `xox[bp]-…`, a bearer token, a `postgres://…` /
|
|
29
|
+
`mysql://…` URL, or they say "here's my key" — don't just carry on. Offer to
|
|
30
|
+
store it safely: *"I'll store that as an encrypted secret for your app and keep
|
|
31
|
+
it out of the code — what's it for?"* Then store it (below) and redact it from
|
|
32
|
+
your own responses.
|
|
33
|
+
|
|
34
|
+
## Where a secret goes
|
|
35
|
+
|
|
36
|
+
Read [references/where-secrets-go.md](references/where-secrets-go.md) for the
|
|
37
|
+
full map. Short version:
|
|
38
|
+
|
|
39
|
+
- **A running app needs it** (API key the backend calls, a DB URL) → store it on
|
|
40
|
+
the app with **`awesomate_app_set_env`** (tool). It's encrypted in the hub AND
|
|
41
|
+
written to the app's `.env` (0600), then the app restarts. Ask which env
|
|
42
|
+
(default `dev`). Node apps only — static sites have no server env.
|
|
43
|
+
- **A personal key just to keep** (not an app runtime secret) → your local
|
|
44
|
+
`~/.config/api-keys.env` (chmod 600), with a note if it was pasted in chat.
|
|
45
|
+
- **Never** a tracked file, a commit, or the chat.
|
|
46
|
+
|
|
47
|
+
## The safe file-drop flow (preferred)
|
|
48
|
+
|
|
49
|
+
For anything sensitive, steer the user away from pasting in chat:
|
|
50
|
+
|
|
51
|
+
1. Tell them: *"Create a file `.awesomate/secret-inbox.txt` in your project and
|
|
52
|
+
paste the value there — I'll grab it and wipe the file."* (`.awesomate/` is
|
|
53
|
+
gitignored by the github skill.)
|
|
54
|
+
2. Read the file to capture the value, call `awesomate_app_set_env` to store it,
|
|
55
|
+
then **scrub the file**:
|
|
56
|
+
`node ~/.claude/skills/awesomate-credentials/scripts/capture-secret.mjs .awesomate/secret-inbox.txt`
|
|
57
|
+
— this securely overwrites and removes it so the plaintext doesn't linger.
|
|
58
|
+
3. Confirm with the key name only.
|
|
59
|
+
|
|
60
|
+
## After storing
|
|
61
|
+
Tell the user what you set (key + env), that it's encrypted and injected, and —
|
|
62
|
+
if it came through the chat — the one-line rotation nudge. Never restate the value.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Where a secret goes
|
|
2
|
+
|
|
3
|
+
| The secret is… | Put it… | How |
|
|
4
|
+
|---|---|---|
|
|
5
|
+
| An app runtime secret (API key the backend calls, a third-party token, a DB URL) | The app's `.env` **and** the hub's encrypted store | `awesomate_app_set_env` (tool) — encrypts hub-side + injects into `.env` (0600) + restarts the app |
|
|
6
|
+
| An n8n webhook URL the app calls (`N8N_WEBHOOK_URL`) | Same as above | `awesomate_app_set_env` — it's not secret-secret, but it belongs in `.env` with the rest |
|
|
7
|
+
| A personal key the user just wants kept (not used by a running app) | `~/.config/api-keys.env` (chmod 600) | Append `KEY=value`; add a `# pasted in chat <date> — rotate` note if it came through chat |
|
|
8
|
+
| Anything, ever | **NOT** in code, a tracked file, a commit, or the chat transcript | — |
|
|
9
|
+
|
|
10
|
+
## Why two places for app secrets
|
|
11
|
+
The hub's encrypted `hosted_app_env_secrets` row is the **durable source of
|
|
12
|
+
truth** (survives redeploys, is auditable, uses the same AES-256-GCM as the DB
|
|
13
|
+
password). The host `.env` is what the **running process actually reads**
|
|
14
|
+
(`dotenv/config`). `awesomate_app_set_env` writes both in one step.
|
|
15
|
+
|
|
16
|
+
## Redaction pattern
|
|
17
|
+
When you confirm, show at most a short prefix:
|
|
18
|
+
- `sk-proj-abc…` → `OPENAI_API_KEY = sk-proj-…redacted`
|
|
19
|
+
- a DB URL → `DATABASE_URL = postgresql://…redacted`
|
|
20
|
+
Never the full value.
|
|
21
|
+
|
|
22
|
+
## Rotation note (chat-pasted keys)
|
|
23
|
+
A value pasted into the conversation is in the transcript — treat it as exposed.
|
|
24
|
+
After storing it, tell the user once: *"rotate this when convenient, and next
|
|
25
|
+
time use the `.awesomate/secret-inbox.txt` drop so it stays out of the chat."*
|
|
26
|
+
High-value keys (payment, production DB, cloud root) — recommend rotating now.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Securely scrub a secret-inbox file after its value has been captured and
|
|
4
|
+
* stored (via awesomate_app_set_env). Overwrites the file contents with random
|
|
5
|
+
* bytes, then removes it, so the plaintext secret doesn't linger on disk.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node capture-secret.mjs <path-to-inbox-file>
|
|
8
|
+
*
|
|
9
|
+
* Prints only a non-sensitive confirmation — never the file contents. The
|
|
10
|
+
* capturing step (reading the value + calling awesomate_app_set_env) is done by
|
|
11
|
+
* Claude with the MCP tool; this script's single job is the safe wipe.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { statSync, writeFileSync, rmSync } from 'node:fs';
|
|
15
|
+
import { randomBytes } from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
const target = process.argv[2];
|
|
18
|
+
if (!target) {
|
|
19
|
+
console.error('usage: node capture-secret.mjs <path-to-inbox-file>');
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let size = 0;
|
|
24
|
+
try {
|
|
25
|
+
size = statSync(target).size;
|
|
26
|
+
} catch {
|
|
27
|
+
console.log(`nothing to scrub: ${target} does not exist`);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Overwrite with random bytes of the same length, then delete. Best-effort
|
|
33
|
+
// (a single pass is enough on modern storage for this threat model — the
|
|
34
|
+
// point is not leaving the plaintext readable at the known path).
|
|
35
|
+
if (size > 0) writeFileSync(target, randomBytes(size), { flag: 'w' });
|
|
36
|
+
rmSync(target, { force: true });
|
|
37
|
+
console.log(`scrubbed ${target} (${size} bytes overwritten + removed)`);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(`failed to scrub ${target}: ${err instanceof Error ? err.message : String(err)}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: awesomate-github
|
|
3
|
+
description: Set up GitHub version control for the user's app with zero Git knowledge required — connect their GitHub once, then create the repo, .gitignore, first commit and push-to-deploy for them. Use when the user says "connect GitHub", "back up my app", "save my work", "set up version control", "put this on GitHub", or after building an app when it should be version-controlled. Companion to awesomate-app-builder.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Awesomate GitHub — version control for people who don't know Git
|
|
7
|
+
|
|
8
|
+
Assume the user knows **nothing** about Git or GitHub. Never make them run raw
|
|
9
|
+
git commands or understand branches/remotes. You drive everything through the
|
|
10
|
+
`gh` CLI and the bundled script; you ask only simple, plain questions.
|
|
11
|
+
|
|
12
|
+
## 0. First run
|
|
13
|
+
|
|
14
|
+
1. **Is `gh` installed?** Run `gh --version`. If missing, give the one-line
|
|
15
|
+
install for their OS and stop until it's there:
|
|
16
|
+
- macOS: `brew install gh`
|
|
17
|
+
- Debian/Ubuntu: `sudo apt install gh` (or https://cli.github.com)
|
|
18
|
+
- Windows: `winget install --id GitHub.cli`
|
|
19
|
+
2. **Are they connected to GitHub?** Run `gh auth status`. If not:
|
|
20
|
+
- Say plainly: *"I'll connect you to GitHub — I'll show a link and a short
|
|
21
|
+
code. Open the link, paste the code, and click approve. No password, no
|
|
22
|
+
tokens to copy."*
|
|
23
|
+
- Have them run `gh auth login` (choose GitHub.com → HTTPS → "Login with a
|
|
24
|
+
web browser"). It prints a one-time code + opens the device-flow page.
|
|
25
|
+
Wait for them to finish, then re-check `gh auth status`.
|
|
26
|
+
- Never ask them for a token or paste one yourself.
|
|
27
|
+
|
|
28
|
+
## 1. Set up a project (the whole thing, via one script)
|
|
29
|
+
|
|
30
|
+
Once `gh auth status` is good, run the scaffolder from the project folder:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
node ~/.claude/skills/awesomate-github/scripts/github-setup.mjs <repo-name> [--public]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
It does, in order (all idempotent — safe to re-run):
|
|
37
|
+
1. **Secret guard** — refuses if a `.env` or an obvious secret (key/token
|
|
38
|
+
shapes) is about to be tracked. Fix that first (use the
|
|
39
|
+
awesomate-credentials skill) — never commit secrets.
|
|
40
|
+
2. Ensures `.gitignore` exists and excludes `.env` (creates/appends if needed).
|
|
41
|
+
3. `git init` (if needed), stages everything, makes the first commit.
|
|
42
|
+
4. `gh repo create <name> --private --source=. --push` (private by default;
|
|
43
|
+
`--public` to override). Personal accounts work — no org needed.
|
|
44
|
+
5. Prints the repo URL.
|
|
45
|
+
|
|
46
|
+
Ask only: **"What should we call this project?"** (repo name) and, if relevant,
|
|
47
|
+
**"Keep it private?"** (default yes). Derive a sensible kebab-case repo name
|
|
48
|
+
from what they tell you.
|
|
49
|
+
|
|
50
|
+
## 2. Push-to-deploy (for apps built with awesomate-app-builder)
|
|
51
|
+
|
|
52
|
+
If this is a Node app from the app builder, wire the deploy secrets so a push
|
|
53
|
+
ships it (GitHub Actions → cPanel). The app builder / hub provides the SSH
|
|
54
|
+
deploy values; set them as repo secrets with `gh secret set` (the script does
|
|
55
|
+
this when run with `--deploy-secrets` and the values are available in the
|
|
56
|
+
environment). Then: **push to `dev` → deploys dev; `main` → deploys prod.**
|
|
57
|
+
Explain it as *"save your work = it goes live"* — don't lecture on branches.
|
|
58
|
+
|
|
59
|
+
## 3. Everyday use (plain language)
|
|
60
|
+
|
|
61
|
+
- "Save my work" → stage, commit with a short message you write for them, push.
|
|
62
|
+
- "Put it live" → push to the right branch (dev/staging/main) and tell them the URL.
|
|
63
|
+
- Always write clear commit messages **for** them; don't ask them to.
|
|
64
|
+
|
|
65
|
+
## Hard rules
|
|
66
|
+
- Never commit a secret or a `.env`. The script's secret guard is a backstop,
|
|
67
|
+
not a licence to skip checking. If you spot a secret, route it through the
|
|
68
|
+
awesomate-credentials skill instead.
|
|
69
|
+
- Never ask the user for a GitHub token or password; the device-flow is the
|
|
70
|
+
only auth path.
|
|
71
|
+
- Default repos to **private**.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# GitHub, in four sentences (only surface if the user asks)
|
|
2
|
+
|
|
3
|
+
- **A repository ("repo")** is just a folder of your project that GitHub keeps a
|
|
4
|
+
safe, versioned copy of — every save is remembered, so you can always go back.
|
|
5
|
+
- **A commit** is one saved snapshot with a short note about what changed.
|
|
6
|
+
- **Pushing** sends your latest commits up to GitHub (the backup + the thing
|
|
7
|
+
that triggers deploys).
|
|
8
|
+
- **Branches** are parallel copies; here they map to environments — `dev` is
|
|
9
|
+
your workspace, `main` is what's live — so "push to main" means "make it live".
|
|
10
|
+
|
|
11
|
+
You never need to run Git commands yourself — just tell me "save my work" or
|
|
12
|
+
"put it live" and I'll do the right thing.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Zero-knowledge GitHub scaffolder. Idempotent — safe to re-run.
|
|
4
|
+
*
|
|
5
|
+
* node github-setup.mjs <repo-name> [--public] [--path <dir>] [--deploy-secrets]
|
|
6
|
+
*
|
|
7
|
+
* Steps: verify gh auth → ensure .gitignore excludes .env → SECRET GUARD
|
|
8
|
+
* (refuse to commit a .env or an obvious key/token) → git init + first commit →
|
|
9
|
+
* `gh repo create --source=. --push` (private by default; personal accounts OK)
|
|
10
|
+
* → optionally seed deploy Actions secrets from the environment.
|
|
11
|
+
*
|
|
12
|
+
* Deploy secrets are read from env vars (SSH_HOST/SSH_USER/SSH_PRIVATE_KEY), not
|
|
13
|
+
* argv, so they never appear in `ps`. Prints only non-sensitive output.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, statSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const flags = new Set(args.filter((a) => a.startsWith('--')));
|
|
22
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
23
|
+
const repoName = positional[0];
|
|
24
|
+
const pathIdx = args.indexOf('--path');
|
|
25
|
+
const cwd = pathIdx >= 0 && args[pathIdx + 1] ? args[pathIdx + 1] : process.cwd();
|
|
26
|
+
const isPublic = flags.has('--public');
|
|
27
|
+
const wantDeploySecrets = flags.has('--deploy-secrets');
|
|
28
|
+
|
|
29
|
+
if (!repoName || !/^[a-z0-9][a-z0-9._-]{0,99}$/i.test(repoName)) {
|
|
30
|
+
console.error('usage: node github-setup.mjs <repo-name> [--public] [--path <dir>] [--deploy-secrets]');
|
|
31
|
+
console.error('repo-name: letters/digits/._- , starts alphanumeric');
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function run(cmd, cmdArgs, opts = {}) {
|
|
36
|
+
return spawnSync(cmd, cmdArgs, { cwd, encoding: 'utf8', ...opts });
|
|
37
|
+
}
|
|
38
|
+
function die(msg) {
|
|
39
|
+
console.error(`\n✗ ${msg}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- 0. gh present + authenticated ----------------------------------------
|
|
44
|
+
if (run('gh', ['--version']).status !== 0) {
|
|
45
|
+
die("GitHub CLI 'gh' isn't installed. Install it (macOS: brew install gh) then re-run.");
|
|
46
|
+
}
|
|
47
|
+
if (run('gh', ['auth', 'status']).status !== 0) {
|
|
48
|
+
die("You're not connected to GitHub yet. Run: gh auth login (GitHub.com → HTTPS → Login with a web browser), then re-run this.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- 1. .gitignore excludes .env ------------------------------------------
|
|
52
|
+
const giPath = join(cwd, '.gitignore');
|
|
53
|
+
const needed = ['.env', '.env.local', '*.log', '.DS_Store', 'node_modules/'];
|
|
54
|
+
let gi = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
|
|
55
|
+
const have = new Set(gi.split(/\r?\n/).map((l) => l.trim()));
|
|
56
|
+
const toAdd = needed.filter((n) => !have.has(n));
|
|
57
|
+
if (toAdd.length) {
|
|
58
|
+
gi = (gi.endsWith('\n') || gi === '' ? gi : gi + '\n') + toAdd.join('\n') + '\n';
|
|
59
|
+
writeFileSync(giPath, gi);
|
|
60
|
+
console.log(`• .gitignore: added ${toAdd.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- init early so the secret guard can inspect the staged set ------------
|
|
64
|
+
if (!existsSync(join(cwd, '.git'))) {
|
|
65
|
+
if (run('git', ['init', '-b', 'main']).status !== 0) die('git init failed.');
|
|
66
|
+
console.log('• initialised a git repo (branch: main)');
|
|
67
|
+
}
|
|
68
|
+
if (run('git', ['add', '-A']).status !== 0) die('git add failed.');
|
|
69
|
+
|
|
70
|
+
// --- 2. SECRET GUARD -------------------------------------------------------
|
|
71
|
+
const staged = run('git', ['diff', '--cached', '--name-only']).stdout.split(/\r?\n/).filter(Boolean);
|
|
72
|
+
const envLeak = staged.find((f) => /(^|\/)\.env(\.|$)/.test(f) && !/\.env\.example$/.test(f));
|
|
73
|
+
if (envLeak) {
|
|
74
|
+
die(`Refusing to commit '${envLeak}' — that's a secrets file. It's now gitignored; run \`git rm --cached ${envLeak}\` and re-run. Store secrets via the awesomate-credentials skill, never in the repo.`);
|
|
75
|
+
}
|
|
76
|
+
const SECRET_RE = /(sk-[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[bp]-[A-Za-z0-9-]{10,}|-----BEGIN [A-Z ]*PRIVATE KEY-----)/;
|
|
77
|
+
for (const f of staged) {
|
|
78
|
+
const abs = join(cwd, f);
|
|
79
|
+
try {
|
|
80
|
+
if (statSync(abs).size > 512 * 1024) continue; // skip large/binary
|
|
81
|
+
if (SECRET_RE.test(readFileSync(abs, 'utf8'))) {
|
|
82
|
+
die(`Refusing to commit '${f}' — it contains something shaped like a secret (API key / token / private key). Move it out (use the awesomate-credentials skill) and re-run.`);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
/* unreadable/binary — skip */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- 3. first commit (only if there's anything to commit) -----------------
|
|
90
|
+
if (run('git', ['diff', '--cached', '--quiet']).status !== 0) {
|
|
91
|
+
if (run('git', ['commit', '-m', 'Initial commit']).status !== 0) die('git commit failed.');
|
|
92
|
+
console.log('• made the first commit');
|
|
93
|
+
} else {
|
|
94
|
+
console.log('• nothing new to commit');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- 4. create the GitHub repo + push -------------------------------------
|
|
98
|
+
const already = run('git', ['remote', 'get-url', 'origin']);
|
|
99
|
+
if (already.status === 0) {
|
|
100
|
+
console.log(`• remote already set: ${already.stdout.trim()}`);
|
|
101
|
+
run('git', ['push', '-u', 'origin', 'HEAD']);
|
|
102
|
+
} else {
|
|
103
|
+
const vis = isPublic ? '--public' : '--private';
|
|
104
|
+
const create = run('gh', ['repo', 'create', repoName, vis, '--source=.', '--remote=origin', '--push']);
|
|
105
|
+
if (create.status !== 0) {
|
|
106
|
+
die(`gh repo create failed: ${(create.stderr || create.stdout || '').trim()}`);
|
|
107
|
+
}
|
|
108
|
+
console.log(`• created ${isPublic ? 'public' : 'private'} repo and pushed`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- 5. deploy secrets (values from env, never argv) ----------------------
|
|
112
|
+
if (wantDeploySecrets) {
|
|
113
|
+
for (const name of ['SSH_HOST', 'SSH_USER', 'SSH_PRIVATE_KEY']) {
|
|
114
|
+
const val = process.env[name];
|
|
115
|
+
if (!val) {
|
|
116
|
+
console.log(`• skip deploy secret ${name} (not in environment)`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const set = run('gh', ['secret', 'set', name], { input: val });
|
|
120
|
+
console.log(set.status === 0 ? `• set deploy secret ${name}` : `• FAILED to set ${name}: ${(set.stderr || '').trim()}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- done ------------------------------------------------------------------
|
|
125
|
+
const url = run('gh', ['repo', 'view', '--json', 'url', '-q', '.url']).stdout.trim();
|
|
126
|
+
console.log(`\n✓ Done. Your project is on GitHub${url ? `: ${url}` : ''}.`);
|
|
@@ -97,6 +97,33 @@ fallback** — that's deliberate.
|
|
|
97
97
|
`GET {apiBase}/api/hosting-access/context` (401 without a token still proves
|
|
98
98
|
reachability).
|
|
99
99
|
|
|
100
|
+
### When you're stuck: generate a support report
|
|
101
|
+
|
|
102
|
+
If a connect or tool failure survives the documented fixes above (wrong
|
|
103
|
+
account, proxy, resume, restart, legacy registrations), don't keep guessing —
|
|
104
|
+
hand Awesomate a diagnostic they can act on:
|
|
105
|
+
|
|
106
|
+
1. Run `node ~/.claude/skills/awesomate-hosting/scripts/support-report.mjs
|
|
107
|
+
--note "<one line: what the user was doing and what happened>"`.
|
|
108
|
+
It writes a **fully redacted** report to
|
|
109
|
+
`~/.awesomate/support-report-<timestamp>.md` — tokens reduced to
|
|
110
|
+
prefix+last4, no key material, proxy credentials stripped. It includes
|
|
111
|
+
versions, profiles/pin/registration state, live connectivity probes, and
|
|
112
|
+
the last bootstrap log automatically.
|
|
113
|
+
2. Show the user the file path and the headline findings (the ACTIVE line,
|
|
114
|
+
any ⚠ LEGACY registration flags, and the probe results).
|
|
115
|
+
3. **Ask the user before submitting.** With their OK, re-run with `--submit`
|
|
116
|
+
— it POSTs the report to Awesomate and returns a reference like
|
|
117
|
+
`ASR-XXXXXXXX`. Tell the user to quote that reference to
|
|
118
|
+
support@awesomate.ai or their Awesomate contact; the report is already
|
|
119
|
+
attached to it server-side. Submission works even when the token is
|
|
120
|
+
broken (that's usually why you're here).
|
|
121
|
+
4. If `--submit` fails too (fully offline), the user emails the file itself —
|
|
122
|
+
it's safe to send as-is.
|
|
123
|
+
|
|
124
|
+
Never edit the report to add raw tokens, codes, or keys, and never submit
|
|
125
|
+
without the user's explicit go-ahead.
|
|
126
|
+
|
|
100
127
|
If this skill is loaded but **no `awesomate_*` tools exist in the session at
|
|
101
128
|
all**, the MCP server was registered after Claude Code started (the bootstrap
|
|
102
129
|
just ran). Don't investigate settings files or reinstall anything — and don't
|
|
@@ -222,6 +249,12 @@ Git skills drive the Git workflow; this skill just deploys the result.
|
|
|
222
249
|
- `wp.sh` — run a WP-CLI command against a live site over that SSH.
|
|
223
250
|
- `deploy.sh` — snapshot-first Studio→live deploy (files, optional DB).
|
|
224
251
|
- `pull-live.sh` — clone a live site down to a local folder (read-only on live).
|
|
252
|
+
- `resolve-account.mjs` — shared account resolver (pin/env/profile precedence)
|
|
253
|
+
used by the shell scripts and the REST fallback; `--api` emits API/PAT/ACCT,
|
|
254
|
+
`--ssh` emits the ssh block.
|
|
255
|
+
- `support-report.mjs` — redacted diagnostic bundle for Awesomate support
|
|
256
|
+
(see "When you're stuck" above); `--submit` delivers it and returns a
|
|
257
|
+
reference ID.
|
|
225
258
|
|
|
226
259
|
All scripts read `~/.awesomate/credentials.json`; none take secrets on the
|
|
227
260
|
command line.
|
|
@@ -61,6 +61,37 @@ const credPath = join(dir, 'credentials.json');
|
|
|
61
61
|
const keyPath = join(keysDir, 'id_ed25519');
|
|
62
62
|
const PIN_FILENAME = '.awesomate.json';
|
|
63
63
|
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Connect log: every run's output (redacted) lands in ~/.awesomate/
|
|
66
|
+
// last-connect.log so a failure is diagnosable AFTER the fact — the
|
|
67
|
+
// support-report script attaches it automatically. Tokens/codes never reach
|
|
68
|
+
// the log: bootstrap output doesn't print them and the argv line is redacted.
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
const CONNECT_LOG = [`--- awesomate connect ${new Date().toISOString()} ---`];
|
|
71
|
+
function redactSecrets(s) {
|
|
72
|
+
return String(s)
|
|
73
|
+
.replace(/amt_pat_[A-Za-z0-9_-]{8,}/g, (t) => `${t.slice(0, 12)}…${t.slice(-4)}`)
|
|
74
|
+
.replace(/amt_bs_[A-Za-z0-9_-]{4,}/g, 'amt_bs_[redacted]');
|
|
75
|
+
}
|
|
76
|
+
CONNECT_LOG.push(`argv: ${process.argv.slice(2).map(redactSecrets).join(' ')}`);
|
|
77
|
+
for (const method of ['log', 'error']) {
|
|
78
|
+
const original = console[method].bind(console);
|
|
79
|
+
console[method] = (...args) => {
|
|
80
|
+
CONNECT_LOG.push(redactSecrets(args.join(' ')));
|
|
81
|
+
original(...args);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
process.on('exit', (code) => {
|
|
85
|
+
try {
|
|
86
|
+
CONNECT_LOG.push(`exit: ${code}`);
|
|
87
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
88
|
+
writeFileSync(join(dir, 'last-connect.log'), `${CONNECT_LOG.join('\n')}\n`, { mode: 0o600 });
|
|
89
|
+
} catch { /* logging must never block exit */ }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const SUPPORT_HINT =
|
|
93
|
+
'Stuck? Generate a support report: node ~/.claude/skills/awesomate-hosting/scripts/support-report.mjs --note "<what happened>" (add --submit to send it to Awesomate and get a reference ID).';
|
|
94
|
+
|
|
64
95
|
// ---------------------------------------------------------------------------
|
|
65
96
|
// Proxy-aware fetch. Node's built-in fetch does NOT honor HTTP(S)_PROXY —
|
|
66
97
|
// in sandboxes that force egress through a proxy, requests bypass it and the
|
|
@@ -502,6 +533,7 @@ async function main() {
|
|
|
502
533
|
console.log('Setup complete. Next: run awesomate_whoami to confirm the account, then ask me to read your plan and list your sites.');
|
|
503
534
|
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
535
|
if (issues.length) {
|
|
536
|
+
console.log(SUPPORT_HINT);
|
|
505
537
|
console.log(`AWESOMATE CONNECT: PARTIAL account=${slug} pin=${pinPath ?? 'none'} issues=${issues.join(',')}`);
|
|
506
538
|
process.exit(1);
|
|
507
539
|
}
|
|
@@ -513,6 +545,7 @@ main().catch((err) => {
|
|
|
513
545
|
if (err.isApiError && err.status === 401) {
|
|
514
546
|
console.error('Grab a fresh code from hub.awesomate.ai/sites — codes last 10 minutes (and stay re-runnable within that window).');
|
|
515
547
|
}
|
|
548
|
+
console.error(SUPPORT_HINT);
|
|
516
549
|
console.error('AWESOMATE CONNECT: FAILED reason=error');
|
|
517
550
|
process.exit(1);
|
|
518
551
|
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Awesomate support report — turn a broken setup into something support can
|
|
4
|
+
* actually act on.
|
|
5
|
+
*
|
|
6
|
+
* Collects a REDACTED diagnostic snapshot of this machine's Awesomate
|
|
7
|
+
* connection state (versions, profiles, pins, MCP registrations, proxy env,
|
|
8
|
+
* live connectivity probes, ssh tooling, the last bootstrap log) and writes
|
|
9
|
+
* it to ~/.awesomate/support-report-<timestamp>.md.
|
|
10
|
+
*
|
|
11
|
+
* node support-report.mjs [--note "what went wrong"] [--category connect|tools|ssh|deploy|n8n|other]
|
|
12
|
+
* node support-report.mjs --submit # also POST it to Awesomate (returns a reference ID)
|
|
13
|
+
*
|
|
14
|
+
* Redaction guarantees (safe to email or submit):
|
|
15
|
+
* - access tokens → first 12 chars + … + last 4 (identifies the row, useless as a credential)
|
|
16
|
+
* - setup codes → amt_bs_[redacted]
|
|
17
|
+
* - ssh → fingerprint + paths only, never key material
|
|
18
|
+
* - proxy URLs → user:pass@ stripped
|
|
19
|
+
*
|
|
20
|
+
* Node 18+ built-ins only.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
|
|
25
|
+
import { homedir, hostname, platform, release, arch } from 'node:os';
|
|
26
|
+
import { join, dirname, resolve } from 'node:path';
|
|
27
|
+
|
|
28
|
+
function arg(name, fallback) {
|
|
29
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
30
|
+
return i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith('--') ? process.argv[i + 1] : fallback;
|
|
31
|
+
}
|
|
32
|
+
const SUBMIT = process.argv.includes('--submit');
|
|
33
|
+
const NOTE = arg('note', null);
|
|
34
|
+
const CATEGORY = arg('category', 'connect');
|
|
35
|
+
|
|
36
|
+
const HOME = homedir();
|
|
37
|
+
const AWM_DIR = join(HOME, '.awesomate');
|
|
38
|
+
const CRED_PATH = join(AWM_DIR, 'credentials.json');
|
|
39
|
+
const PIN_FILENAME = '.awesomate.json';
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Redaction — applied to individual values AND as a final pass over the whole
|
|
43
|
+
// document, so nothing secret can leak through a path we forgot.
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
function redact(s) {
|
|
46
|
+
return String(s)
|
|
47
|
+
.replace(/amt_pat_[A-Za-z0-9_-]{8,}/g, (t) => `${t.slice(0, 12)}…${t.slice(-4)}`)
|
|
48
|
+
.replace(/amt_bs_[A-Za-z0-9_-]{4,}/g, 'amt_bs_[redacted]')
|
|
49
|
+
.replace(/(https?:\/\/)[^/@\s]+@/g, '$1[credentials-redacted]@')
|
|
50
|
+
.replace(/-----BEGIN[\s\S]*?-----END [A-Z ]*KEY-----/g, '[key-material-redacted]');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJson(path) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
56
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
57
|
+
} catch { return null; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function tryExec(cmd, args) {
|
|
61
|
+
try {
|
|
62
|
+
return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 8000 }).trim();
|
|
63
|
+
} catch { return null; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Account resolution — same precedence as the MCP server / resolve-account.mjs
|
|
68
|
+
// (duplicated minimally here so the report works even when those are broken).
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
function findPin() {
|
|
71
|
+
const home = resolve(HOME);
|
|
72
|
+
let dir = resolve(process.cwd());
|
|
73
|
+
for (;;) {
|
|
74
|
+
const candidate = join(dir, PIN_FILENAME);
|
|
75
|
+
if (existsSync(candidate)) return candidate;
|
|
76
|
+
if (dir === home) return null;
|
|
77
|
+
const parent = dirname(dir);
|
|
78
|
+
if (parent === dir) return null;
|
|
79
|
+
dir = parent;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function profileSummary(key, p) {
|
|
84
|
+
return {
|
|
85
|
+
profile: key,
|
|
86
|
+
slug: p.slug ?? null,
|
|
87
|
+
contactId: p.contactId ?? null,
|
|
88
|
+
email: p.email ?? null,
|
|
89
|
+
plan: p.plan ?? null,
|
|
90
|
+
apiBase: p.apiBase ?? null,
|
|
91
|
+
tokenRedacted: p.pat ? redact(p.pat) : null,
|
|
92
|
+
tokenExpiresAt: p.expiresAt ?? null,
|
|
93
|
+
ssh: p.ssh ? { host: p.ssh.host, user: p.ssh.user, port: p.ssh.port ?? 22, fingerprint: p.ssh.fingerprint ?? null, keyPath: p.ssh.keyPath ?? null } : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
const lines = [];
|
|
99
|
+
const push = (s = '') => lines.push(s);
|
|
100
|
+
|
|
101
|
+
// ── Environment ──────────────────────────────────────────────────────────
|
|
102
|
+
push('# Awesomate support report');
|
|
103
|
+
push('');
|
|
104
|
+
push(`- generated: ${new Date().toISOString()}`);
|
|
105
|
+
push(`- host: ${hostname()} (${platform()} ${release()}, ${arch()})`);
|
|
106
|
+
push(`- node: ${process.version}`);
|
|
107
|
+
push(`- claude CLI: ${tryExec('claude', ['--version']) ?? 'not found on PATH'}`);
|
|
108
|
+
const installedVersion = (() => {
|
|
109
|
+
try { return readFileSync(join(HOME, '.claude', 'skills', 'awesomate-hosting', '.installed-version'), 'utf8').trim(); } catch { return null; }
|
|
110
|
+
})();
|
|
111
|
+
push(`- installed skill version: ${installedVersion ?? 'no marker (pre-0.6 install?)'}`);
|
|
112
|
+
push(`- cwd: ${process.cwd()}`);
|
|
113
|
+
if (NOTE) { push(''); push(`## What the user reports`); push(''); push(NOTE); }
|
|
114
|
+
|
|
115
|
+
// ── Proxy environment ────────────────────────────────────────────────────
|
|
116
|
+
push(''); push('## Proxy environment'); push('');
|
|
117
|
+
const proxyVars = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'NO_PROXY', 'no_proxy', 'NODE_USE_ENV_PROXY'];
|
|
118
|
+
const setVars = proxyVars.filter((v) => process.env[v]);
|
|
119
|
+
if (setVars.length === 0) push('- none set');
|
|
120
|
+
for (const v of setVars) push(`- ${v}=${redact(process.env[v])}`);
|
|
121
|
+
|
|
122
|
+
// ── Credentials + account resolution ─────────────────────────────────────
|
|
123
|
+
push(''); push('## Credentials & account resolution'); push('');
|
|
124
|
+
const creds = readJson(CRED_PATH);
|
|
125
|
+
let profiles = {};
|
|
126
|
+
let activeApiBase = 'https://hub.awesomate.ai';
|
|
127
|
+
let activePat = null;
|
|
128
|
+
let activeSlug = null;
|
|
129
|
+
if (!creds) {
|
|
130
|
+
push(`- ${CRED_PATH}: ${existsSync(CRED_PATH) ? 'EXISTS BUT UNPARSEABLE' : 'missing (never connected on this machine?)'}`);
|
|
131
|
+
} else {
|
|
132
|
+
if (creds.profiles && typeof creds.profiles === 'object') profiles = creds.profiles;
|
|
133
|
+
else if (creds.pat) profiles = { [creds.slug || 'default']: creds };
|
|
134
|
+
push(`- credentials file: v${creds.version ?? 1}, defaultProfile: ${creds.defaultProfile ?? '(none)'}`);
|
|
135
|
+
for (const [key, p] of Object.entries(profiles)) {
|
|
136
|
+
push(`- profile ${JSON.stringify(profileSummary(key, p))}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const pinPath = findPin();
|
|
140
|
+
const pin = pinPath ? readJson(pinPath) : null;
|
|
141
|
+
push(`- folder pin: ${pinPath ? `${pinPath} → ${JSON.stringify(pin)}` : 'none found (walk-up from cwd)'}`);
|
|
142
|
+
const names = Object.keys(profiles);
|
|
143
|
+
if (process.env.AWESOMATE_PAT) { activePat = process.env.AWESOMATE_PAT; activeSlug = '(env PAT)'; push('- ACTIVE: env AWESOMATE_PAT override'); }
|
|
144
|
+
else if (process.env.AWESOMATE_ACCOUNT && profiles[process.env.AWESOMATE_ACCOUNT]) { const p = profiles[process.env.AWESOMATE_ACCOUNT]; activePat = p.pat; activeSlug = p.slug; activeApiBase = p.apiBase ?? activeApiBase; push(`- ACTIVE: env AWESOMATE_ACCOUNT=${process.env.AWESOMATE_ACCOUNT}`); }
|
|
145
|
+
else if (pin?.account && profiles[pin.account]) { const p = profiles[pin.account]; activePat = p.pat; activeSlug = p.slug; activeApiBase = p.apiBase ?? activeApiBase; push(`- ACTIVE: pin → ${pin.account}`); }
|
|
146
|
+
else if (pin?.account && !profiles[pin.account]) { push(`- ACTIVE: NONE — pin names "${pin.account}" but no such profile exists (this is a hard error by design)`); }
|
|
147
|
+
else if (names.length === 1) { const p = profiles[names[0]]; activePat = p.pat; activeSlug = p.slug; activeApiBase = p.apiBase ?? activeApiBase; push(`- ACTIVE: sole profile ${names[0]}`); }
|
|
148
|
+
else if (creds?.defaultProfile && profiles[creds.defaultProfile]) { const p = profiles[creds.defaultProfile]; activePat = p.pat; activeSlug = p.slug; activeApiBase = p.apiBase ?? activeApiBase; push(`- ACTIVE: defaultProfile ${creds.defaultProfile}`); }
|
|
149
|
+
else { push(`- ACTIVE: NONE (${names.length} profiles, no pin/default)`); }
|
|
150
|
+
|
|
151
|
+
// ── MCP registrations ────────────────────────────────────────────────────
|
|
152
|
+
push(''); push('## MCP registrations'); push('');
|
|
153
|
+
const claudeJson = readJson(join(HOME, '.claude.json'));
|
|
154
|
+
const userEntry = claudeJson?.mcpServers?.['awesomate-hosting'];
|
|
155
|
+
push(`- user scope (~/.claude.json): ${userEntry ? JSON.stringify({ command: userEntry.command, args: userEntry.args, envKeys: Object.keys(userEntry.env ?? {}) }) : 'none'}`);
|
|
156
|
+
if (userEntry?.env?.AWESOMATE_PAT) push(' ⚠ LEGACY: token baked into env — this pins the registration to one stale account. Re-run Connect to fix.');
|
|
157
|
+
for (const [projectDir, project] of Object.entries(claudeJson?.projects ?? {})) {
|
|
158
|
+
const e = project?.mcpServers?.['awesomate-hosting'];
|
|
159
|
+
if (e) push(`- local scope (${projectDir}): envKeys=${JSON.stringify(Object.keys(e.env ?? {}))}${e.env?.AWESOMATE_PAT ? ' ⚠ LEGACY env token' : ''}`);
|
|
160
|
+
}
|
|
161
|
+
const mcpJson = readJson(join(process.cwd(), '.mcp.json'));
|
|
162
|
+
const projEntry = mcpJson?.mcpServers?.['awesomate-hosting'];
|
|
163
|
+
if (projEntry) push(`- project scope (cwd/.mcp.json): envKeys=${JSON.stringify(Object.keys(projEntry.env ?? {}))}${projEntry.env?.AWESOMATE_PAT ? ' ⚠ LEGACY env token' : ''}`);
|
|
164
|
+
|
|
165
|
+
// ── Connectivity probes (proxy-aware) ────────────────────────────────────
|
|
166
|
+
push(''); push('## Connectivity probes'); push('');
|
|
167
|
+
let doFetch = fetch;
|
|
168
|
+
if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy) {
|
|
169
|
+
try {
|
|
170
|
+
const undici = await import('undici');
|
|
171
|
+
const dispatcher = new undici.EnvHttpProxyAgent();
|
|
172
|
+
doFetch = (url, opts = {}) => undici.fetch(url, { ...opts, dispatcher });
|
|
173
|
+
push('- probe transport: undici EnvHttpProxyAgent (proxy honored)');
|
|
174
|
+
} catch {
|
|
175
|
+
push('- probe transport: built-in fetch (undici unavailable — proxy NOT honored unless NODE_USE_ENV_PROXY=1)');
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
push('- probe transport: built-in fetch (no proxy configured)');
|
|
179
|
+
}
|
|
180
|
+
async function probe(label, url, headers) {
|
|
181
|
+
try {
|
|
182
|
+
const res = await doFetch(url, { headers, signal: AbortSignal.timeout(10_000) });
|
|
183
|
+
const body = await res.text();
|
|
184
|
+
const hasApiBody = (() => { try { const j = JSON.parse(body); return j && typeof j === 'object'; } catch { return false; } })();
|
|
185
|
+
push(`- ${label}: HTTP ${res.status} (${hasApiBody ? 'API response body' : 'NON-API body — likely a gateway/proxy in the way'})`);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
push(`- ${label}: FAILED (${err?.cause?.code ?? err?.name ?? err?.message})`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await probe('GET /api/hosting-access/context (no auth — 401 proves reachability)', `${activeApiBase}/api/hosting-access/context`);
|
|
191
|
+
if (activePat) {
|
|
192
|
+
await probe(`GET /api/hosting-access/context (as ${activeSlug})`, `${activeApiBase}/api/hosting-access/context`, { Authorization: `Bearer ${activePat}` });
|
|
193
|
+
} else {
|
|
194
|
+
push('- authenticated probe skipped: no active profile resolved');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── SSH tooling ──────────────────────────────────────────────────────────
|
|
198
|
+
push(''); push('## SSH tooling'); push('');
|
|
199
|
+
push(`- ssh: ${tryExec('ssh', ['-V']) ?? tryExec('sh', ['-c', 'ssh -V 2>&1']) ?? 'not found'}`);
|
|
200
|
+
push(`- ssh-keygen: ${tryExec('sh', ['-c', 'command -v ssh-keygen']) ?? 'not found'}`);
|
|
201
|
+
const keyPath = join(AWM_DIR, 'keys', 'id_ed25519');
|
|
202
|
+
push(`- key file: ${existsSync(keyPath) ? `present — ${tryExec('ssh-keygen', ['-lf', keyPath]) ?? 'fingerprint unavailable'}` : 'absent'}`);
|
|
203
|
+
|
|
204
|
+
// ── Last bootstrap log ───────────────────────────────────────────────────
|
|
205
|
+
push(''); push('## Last connect attempt (~/.awesomate/last-connect.log)'); push('');
|
|
206
|
+
const logPath = join(AWM_DIR, 'last-connect.log');
|
|
207
|
+
if (existsSync(logPath)) {
|
|
208
|
+
push('```');
|
|
209
|
+
push(redact(readFileSync(logPath, 'utf8')).trim().split('\n').slice(-60).join('\n'));
|
|
210
|
+
push('```');
|
|
211
|
+
} else {
|
|
212
|
+
push('- no log (bootstrap predates 0.7.3, or never ran on this machine)');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Write + optionally submit ────────────────────────────────────────────
|
|
216
|
+
const report = redact(lines.join('\n')) + '\n';
|
|
217
|
+
mkdirSync(AWM_DIR, { recursive: true, mode: 0o700 });
|
|
218
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
219
|
+
const outPath = join(AWM_DIR, `support-report-${stamp}.md`);
|
|
220
|
+
writeFileSync(outPath, report);
|
|
221
|
+
chmodSync(outPath, 0o600);
|
|
222
|
+
console.log(`✓ Support report written to ${outPath} (all tokens redacted — safe to send).`);
|
|
223
|
+
|
|
224
|
+
if (!SUBMIT) {
|
|
225
|
+
console.log('Send it to support@awesomate.ai, or re-run with --submit to deliver it directly and get a reference ID.');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const submitBody = {
|
|
230
|
+
report,
|
|
231
|
+
summary: NOTE ? NOTE.slice(0, 255) : `Diagnostic from ${activeSlug ?? 'unconnected machine'} (${hostname()})`,
|
|
232
|
+
slug: activeSlug && activeSlug !== '(env PAT)' ? activeSlug : null,
|
|
233
|
+
email: null,
|
|
234
|
+
category: CATEGORY,
|
|
235
|
+
clientVersion: installedVersion,
|
|
236
|
+
};
|
|
237
|
+
try {
|
|
238
|
+
const res = await doFetch(`${activeApiBase}/api/hosting-access/support-report`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: { 'Content-Type': 'application/json', ...(activePat ? { Authorization: `Bearer ${activePat}` } : {}) },
|
|
241
|
+
body: JSON.stringify(submitBody),
|
|
242
|
+
signal: AbortSignal.timeout(15_000),
|
|
243
|
+
});
|
|
244
|
+
const json = await res.json().catch(() => null);
|
|
245
|
+
if (res.ok && json?.reference) {
|
|
246
|
+
console.log(`✓ Submitted to Awesomate support. Reference: ${json.reference}`);
|
|
247
|
+
console.log(` ${json.next}`);
|
|
248
|
+
} else {
|
|
249
|
+
console.error(`Submit failed (HTTP ${res.status}${json?.error ? `: ${json.error}` : ''}) — email the file above to support@awesomate.ai instead.`);
|
|
250
|
+
process.exitCode = 1;
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.error(`Submit failed (${err?.cause?.code ?? err?.message}) — email the file above to support@awesomate.ai instead.`);
|
|
254
|
+
process.exitCode = 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main().catch((err) => {
|
|
259
|
+
console.error('support-report failed:', err?.stack ?? err?.message ?? String(err));
|
|
260
|
+
process.exit(1);
|
|
261
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: awesomate-seo
|
|
3
|
+
description: Make the user's site findable — by Google AND by AI assistants. Adds title/meta/Open Graph tags, sitemap.xml, robots.txt, canonical tags, JSON-LD structured data, and a general llms.txt. Works for static sites, Node apps, and WordPress. Use when the user says "SEO", "get found on Google", "rank higher", "make my site discoverable", "sitemap", "meta tags", "schema", or after building/changing a public site. Companion to awesomate-app-builder.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Awesomate SEO — get found on Google and AI
|
|
7
|
+
|
|
8
|
+
Search engines and AI assistants both need machine-readable signals. This skill
|
|
9
|
+
adds them to any site the user has. Treat SEO as **ongoing**, not one-shot:
|
|
10
|
+
bake it in when a page is built, and re-run the relevant parts whenever content
|
|
11
|
+
or structure changes.
|
|
12
|
+
|
|
13
|
+
Be honest about limits: these are the signals that *let* you rank and be cited
|
|
14
|
+
— they don't *guarantee* placement. Never promise rankings.
|
|
15
|
+
|
|
16
|
+
## 0. What kind of site is this?
|
|
17
|
+
|
|
18
|
+
- **Static site or Node app** (from awesomate-app-builder) → you write the files
|
|
19
|
+
directly into the project (below), commit, and deploy.
|
|
20
|
+
- **WordPress** → drive it through WP-CLI via the awesomate-hosting skill
|
|
21
|
+
(`wp` passthrough). Install a free SEO plugin + let it generate the sitemap
|
|
22
|
+
rather than hand-writing files (see §WordPress).
|
|
23
|
+
- **A shop (WooCommerce)** → also use the `woo-agent-storefront` skill for
|
|
24
|
+
AI-shopping feeds + a store `llms.txt`, and `woo-catalog-perfection` for
|
|
25
|
+
product-data quality. This skill covers the site-level SEO around it.
|
|
26
|
+
|
|
27
|
+
## 1. Static / Node — write these
|
|
28
|
+
|
|
29
|
+
Follow [references/seo-checklist.md](references/seo-checklist.md) for exact
|
|
30
|
+
formats + length limits. In short, per site:
|
|
31
|
+
|
|
32
|
+
1. **`<head>` tags** on every page: unique `<title>` (≤60 chars), meta
|
|
33
|
+
description (≤155), canonical `<link>`, Open Graph (`og:title/description/
|
|
34
|
+
image/url/type`) and `twitter:card`. The static-landing template ships
|
|
35
|
+
`{{...}}` placeholders — fill them with real values.
|
|
36
|
+
2. **`robots.txt`** at the site root — allow crawling, point to the sitemap.
|
|
37
|
+
3. **`sitemap.xml`** at the root — list every real URL with `<loc>` (+ `<lastmod>`).
|
|
38
|
+
4. **JSON-LD** (`<script type="application/ld+json">`) — the right schema.org
|
|
39
|
+
type (`Organization` / `WebSite` / `Product` / `Article` / `LocalBusiness`).
|
|
40
|
+
5. **`llms.txt`** at the root — a short, plain-markdown summary of what the
|
|
41
|
+
site/business is and its key pages, so AI assistants can understand and cite
|
|
42
|
+
it. (This is the general, non-shop counterpart to the Woo `llms.txt`.)
|
|
43
|
+
|
|
44
|
+
Then commit + deploy (awesomate-github). On content changes, update the
|
|
45
|
+
affected `<head>` tags, `sitemap.xml` `<lastmod>`, and `llms.txt`.
|
|
46
|
+
|
|
47
|
+
## 2. WordPress
|
|
48
|
+
|
|
49
|
+
Through the awesomate-hosting skill's `wp` passthrough:
|
|
50
|
+
- Install + activate a free SEO plugin (e.g. `wp plugin install wordpress-seo
|
|
51
|
+
--activate` for Yoast, or the user's preference). It manages titles/meta,
|
|
52
|
+
Open Graph, and the XML sitemap.
|
|
53
|
+
- Confirm the sitemap is live (`/sitemap_index.xml` or `/sitemap.xml`) and
|
|
54
|
+
`robots.txt` references it.
|
|
55
|
+
- Add an `llms.txt` at the site root for AI discoverability (the plugin won't).
|
|
56
|
+
- WordPress is the recommended stack precisely when SEO/organic reach matters —
|
|
57
|
+
lean into it here.
|
|
58
|
+
|
|
59
|
+
## 3. Verify
|
|
60
|
+
- `robots.txt` and `sitemap.xml` (or the WP sitemap) return 200 at the root.
|
|
61
|
+
- Each key page has a unique title + description and valid JSON-LD (no syntax
|
|
62
|
+
errors — a broken `application/ld+json` block is worse than none).
|
|
63
|
+
- `llms.txt` exists and is accurate.
|
|
64
|
+
Tell the user the next step is submitting the sitemap in Google Search Console
|
|
65
|
+
(you can't do that for them) — offer a one-paragraph how-to if they want it.
|
|
66
|
+
|
|
67
|
+
## Hard rules
|
|
68
|
+
- Never fabricate content, reviews, or business facts to "help SEO".
|
|
69
|
+
- Never claim these guarantee a ranking — they make the site *eligible* and
|
|
70
|
+
*legible*, to search engines and AI both.
|
|
71
|
+
- Invalid structured data is a bug — validate the JSON before shipping.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# SEO checklist — exact formats
|
|
2
|
+
|
|
3
|
+
## `<head>` (every page, unique per page)
|
|
4
|
+
```html
|
|
5
|
+
<title>Primary keyword — Brand</title> <!-- ≤60 chars -->
|
|
6
|
+
<meta name="description" content="…"> <!-- ≤155 chars, compelling -->
|
|
7
|
+
<link rel="canonical" href="https://DOMAIN/PATH">
|
|
8
|
+
<meta property="og:title" content="…">
|
|
9
|
+
<meta property="og:description" content="…">
|
|
10
|
+
<meta property="og:type" content="website"> <!-- or article/product -->
|
|
11
|
+
<meta property="og:url" content="https://DOMAIN/PATH">
|
|
12
|
+
<meta property="og:image" content="https://DOMAIN/og.png"> <!-- 1200×630 ideally -->
|
|
13
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## `robots.txt` (site root)
|
|
17
|
+
```
|
|
18
|
+
User-agent: *
|
|
19
|
+
Allow: /
|
|
20
|
+
Sitemap: https://DOMAIN/sitemap.xml
|
|
21
|
+
```
|
|
22
|
+
Only `Disallow:` paths that genuinely shouldn't be indexed (admin, cart,
|
|
23
|
+
thank-you). Don't block CSS/JS.
|
|
24
|
+
|
|
25
|
+
## `sitemap.xml` (site root)
|
|
26
|
+
```xml
|
|
27
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
28
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
29
|
+
<url><loc>https://DOMAIN/</loc><lastmod>YYYY-MM-DD</lastmod></url>
|
|
30
|
+
<url><loc>https://DOMAIN/about</loc><lastmod>YYYY-MM-DD</lastmod></url>
|
|
31
|
+
</urlset>
|
|
32
|
+
```
|
|
33
|
+
List only real, canonical, indexable URLs (no dupes, no query-string variants).
|
|
34
|
+
|
|
35
|
+
## JSON-LD (in `<head>` or end of `<body>`)
|
|
36
|
+
Pick the type that fits; validate before shipping.
|
|
37
|
+
```html
|
|
38
|
+
<script type="application/ld+json">
|
|
39
|
+
{
|
|
40
|
+
"@context": "https://schema.org",
|
|
41
|
+
"@type": "Organization",
|
|
42
|
+
"name": "Brand",
|
|
43
|
+
"url": "https://DOMAIN/",
|
|
44
|
+
"logo": "https://DOMAIN/logo.png"
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
```
|
|
48
|
+
Common types: `WebSite` (+ `SearchAction`), `Organization` / `LocalBusiness`
|
|
49
|
+
(add `address`, `telephone`, `openingHours`), `Product` (`offers`, `brand`,
|
|
50
|
+
`gtin`), `Article` (`headline`, `datePublished`, `author`), `FAQPage`.
|
|
51
|
+
|
|
52
|
+
## `llms.txt` (site root — AI discoverability)
|
|
53
|
+
Plain markdown so assistants can read + cite the site. Keep it current.
|
|
54
|
+
```
|
|
55
|
+
# Brand — what we do
|
|
56
|
+
|
|
57
|
+
One-paragraph plain-English description of the business/product and who it's for.
|
|
58
|
+
|
|
59
|
+
## Key pages
|
|
60
|
+
- [Home](https://DOMAIN/): …
|
|
61
|
+
- [Pricing](https://DOMAIN/pricing): …
|
|
62
|
+
- [Contact](https://DOMAIN/contact): …
|
|
63
|
+
|
|
64
|
+
## Facts
|
|
65
|
+
- Location, hours, contact — only true, verifiable facts.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Length + quality rules
|
|
69
|
+
- Titles ≤60, descriptions ≤155 chars, each unique across the site.
|
|
70
|
+
- One `<h1>` per page; descriptive; not keyword-stuffed.
|
|
71
|
+
- Alt text on meaningful images.
|
|
72
|
+
- Every claim in `llms.txt`/JSON-LD must be true — never invent facts or reviews.
|