hostinger-api-mcp 3.23.1 → 3.24.0
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/CHANGELOG.md +14 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/headless/SKILL.md +4 -4
- package/skills/headless/run-log.mjs +83 -61
- package/skills/headless/steps/05-design/SKILL.md +2 -2
- package/skills/headless/steps/09-done/SKILL.md +39 -12
- package/src/core/skills.js +12 -12
- package/src/core/skills.ts +12 -12
- package/src/servers/agency-hosting.js +1 -1
- package/src/servers/agency-hosting.ts +1 -1
- package/src/servers/agent-readiness.js +1 -1
- package/src/servers/agent-readiness.ts +1 -1
- package/src/servers/all.js +1 -1
- package/src/servers/all.ts +1 -1
- package/src/servers/billing.js +1 -1
- package/src/servers/billing.ts +1 -1
- package/src/servers/dns.js +1 -1
- package/src/servers/dns.ts +1 -1
- package/src/servers/domains.js +1 -1
- package/src/servers/domains.ts +1 -1
- package/src/servers/ecommerce.js +1 -1
- package/src/servers/ecommerce.ts +1 -1
- package/src/servers/horizons.js +1 -1
- package/src/servers/horizons.ts +1 -1
- package/src/servers/hosting.js +1 -1
- package/src/servers/hosting.ts +1 -1
- package/src/servers/mail.js +1 -1
- package/src/servers/mail.ts +1 -1
- package/src/servers/reach.js +1 -1
- package/src/servers/reach.ts +1 -1
- package/src/servers/vps.js +1 -1
- package/src/servers/vps.ts +1 -1
- package/src/servers/wordpress.js +1 -1
- package/src/servers/wordpress.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ Generated at release time from the diff between the previous and current
|
|
|
4
4
|
build. Tool changes are listed individually; everything else is summarised
|
|
5
5
|
by area.
|
|
6
6
|
|
|
7
|
+
## v3.24.0 — 2026-10-30
|
|
8
|
+
|
|
9
|
+
### Also in this release
|
|
10
|
+
|
|
11
|
+
- Bundled agent skills: `skills/headless/steps/09-done/SKILL.md`
|
|
12
|
+
- Other files: `src/core/skills.js`, `src/core/skills.ts`
|
|
13
|
+
|
|
14
|
+
## v3.23.2 — 2026-10-30
|
|
15
|
+
|
|
16
|
+
### Also in this release
|
|
17
|
+
|
|
18
|
+
- Bundled agent skills: 4 files
|
|
19
|
+
- Other files: `src/core/skills.js`, `src/core/skills.ts`
|
|
20
|
+
|
|
7
21
|
## v3.23.1 — 2026-10-30
|
|
8
22
|
|
|
9
23
|
### Also in this release
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/hostinger/api-mcp-server",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.24.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@hostinger/mcp",
|
|
14
|
-
"version": "3.
|
|
14
|
+
"version": "3.24.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/skills/headless/SKILL.md
CHANGED
|
@@ -198,6 +198,7 @@ took from it.
|
|
|
198
198
|
| File (under `~/.hostinger/<domain>/`) | Written by | Contains |
|
|
199
199
|
|---|---|---|
|
|
200
200
|
| `state.json` | every stage | tool, next stage, Hostinger connector version, timestamps |
|
|
201
|
+
| `run-log.json` | `run-log.mjs` only | the current run, each stage's start and end times, which reports reached Hostinger |
|
|
201
202
|
| `connect.json` | connect | tool, MCP version, sign-in method, plan; shape in `connect/SKILL.md` |
|
|
202
203
|
| `brief.md` | refine | refined brief, audience, tone, pages, must-haves |
|
|
203
204
|
| `site.json` | provision | chosen backend, username, install id, all URLs |
|
|
@@ -234,12 +235,11 @@ provision runs in the background, its status lives in its own `provision` field
|
|
|
234
235
|
prepare step ignores `provision` entirely.
|
|
235
236
|
- On **resume**, `provision.status` is a hint only: re-poll `hosting_websites_list-setups` to learn
|
|
236
237
|
the truth, then update the field.
|
|
237
|
-
- `
|
|
238
|
-
|
|
239
|
-
written **only** by `run-log.mjs`. Leave them alone when you update `state.json` by hand.
|
|
238
|
+
- Write `state.json` however is easiest, rewriting the whole file included: the run log lives in
|
|
239
|
+
`run-log.json`, which only `run-log.mjs` writes.
|
|
240
240
|
- The **onboarding slug** is how hPanel names the coding tool: `claude` for Claude Code, else
|
|
241
241
|
`cursor`, `codex`, `vscode`, `antigravity`, `devin`. Map it from the `<tool>` you pass to
|
|
242
|
-
`run-start --tool` (`
|
|
242
|
+
`run-start --tool` (`run-log.json → run.tool`, e.g. `claude-code` → `claude`).
|
|
243
243
|
|
|
244
244
|
---
|
|
245
245
|
|
|
@@ -76,20 +76,43 @@ function workspace(domain) {
|
|
|
76
76
|
dir,
|
|
77
77
|
domain,
|
|
78
78
|
statePath: join(dir, 'state.json'),
|
|
79
|
+
logPath: join(dir, 'run-log.json'),
|
|
79
80
|
eventsPath: join(dir, 'events.jsonl'),
|
|
80
81
|
reportsDir: join(dir, 'reports'),
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// state.json belongs to the agent, which may rewrite it whole or leave it half-written, so the run
|
|
86
|
+
// log keeps its own file and only reads state.json, treating anything unreadable as empty.
|
|
87
|
+
function readState(ws) {
|
|
88
|
+
try {
|
|
89
|
+
const state = readJson(ws.statePath, {});
|
|
90
|
+
return state && typeof state === 'object' && !Array.isArray(state) ? state : {};
|
|
91
|
+
} catch {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadLog(ws) {
|
|
97
|
+
if (!existsSync(ws.logPath)) migrate(ws);
|
|
98
|
+
return readJson(ws.logPath, {});
|
|
99
|
+
}
|
|
100
|
+
const saveLog = (ws, log) => writeJson(ws.logPath, log);
|
|
101
|
+
|
|
102
|
+
// Builds started before run-log.json existed kept the run log in state.json: move it out once, so
|
|
103
|
+
// those builds keep their timings and the record of reports already sent.
|
|
104
|
+
function migrate(ws) {
|
|
105
|
+
const state = readState(ws);
|
|
106
|
+
const fields = ['run', 'history', 'reports_sent', 'reports_rejected'].filter((key) => key in state);
|
|
107
|
+
if (!fields.length) return;
|
|
108
|
+
saveLog(ws, Object.fromEntries(fields.map((key) => [key, state[key]])));
|
|
109
|
+
for (const key of fields) delete state[key];
|
|
87
110
|
writeJson(ws.statePath, state);
|
|
88
111
|
}
|
|
89
112
|
|
|
90
|
-
function currentRun(
|
|
91
|
-
if (!
|
|
92
|
-
return
|
|
113
|
+
function currentRun(log) {
|
|
114
|
+
if (!log.run?.id) fail('no run started; call run-start first (check-state does this)');
|
|
115
|
+
return log.run;
|
|
93
116
|
}
|
|
94
117
|
|
|
95
118
|
function readEvents(ws) {
|
|
@@ -110,14 +133,14 @@ function readConnect(ws) {
|
|
|
110
133
|
}
|
|
111
134
|
|
|
112
135
|
function runStart(ws, flags) {
|
|
113
|
-
const
|
|
136
|
+
const log = loadLog(ws);
|
|
114
137
|
// A previous run that never reached its report (closed window, crash) is closed out first, so
|
|
115
138
|
// its log still turns into a report.
|
|
116
|
-
if (
|
|
139
|
+
if (log.run?.id && !log.run.reported_at) {
|
|
117
140
|
buildReport(ws, { outcome: 'abandoned' });
|
|
118
|
-
console.log(`closed previous run ${
|
|
141
|
+
console.log(`closed previous run ${log.run.id} as abandoned`);
|
|
119
142
|
}
|
|
120
|
-
const fresh =
|
|
143
|
+
const fresh = loadLog(ws);
|
|
121
144
|
const previous = fresh.run ?? {};
|
|
122
145
|
const startedAt = now();
|
|
123
146
|
const connect = readConnect(ws);
|
|
@@ -135,21 +158,21 @@ function runStart(ws, flags) {
|
|
|
135
158
|
id,
|
|
136
159
|
kind,
|
|
137
160
|
started_at: startedAt,
|
|
138
|
-
tool: flags.tool ?? connect.tool ??
|
|
161
|
+
tool: flags.tool ?? connect.tool ?? readState(ws).tool ?? previous.tool ?? null,
|
|
139
162
|
model: flags.model ?? previous.model ?? null,
|
|
140
163
|
skill: flags.skill ?? previous.skill ?? null,
|
|
141
164
|
};
|
|
142
165
|
fresh.history ??= [];
|
|
143
|
-
|
|
166
|
+
saveLog(ws, fresh);
|
|
144
167
|
console.log(`run ${fresh.run.id} started`);
|
|
145
168
|
}
|
|
146
169
|
|
|
147
170
|
function runUpdate(ws, flags) {
|
|
148
|
-
const
|
|
149
|
-
const run = currentRun(
|
|
171
|
+
const log = loadLog(ws);
|
|
172
|
+
const run = currentRun(log);
|
|
150
173
|
for (const key of ['kind', 'tool', 'model', 'skill']) if (typeof flags[key] === 'string') run[key] = flags[key];
|
|
151
174
|
if (!['build', 'resume', 'redesign', 'iterate'].includes(run.kind)) fail(`unknown kind "${run.kind}"; one of build, resume, redesign, iterate`);
|
|
152
|
-
|
|
175
|
+
saveLog(ws, log);
|
|
153
176
|
console.log(`run ${run.id} updated`);
|
|
154
177
|
}
|
|
155
178
|
|
|
@@ -157,8 +180,8 @@ function runUpdate(ws, flags) {
|
|
|
157
180
|
// to the run it follows: reopen it, so the next `report` rewrites that run's report instead of
|
|
158
181
|
// starting a second one.
|
|
159
182
|
function runResume(ws, flags) {
|
|
160
|
-
const
|
|
161
|
-
const run = currentRun(
|
|
183
|
+
const log = loadLog(ws);
|
|
184
|
+
const run = currentRun(log);
|
|
162
185
|
if (!run.reported_at) {
|
|
163
186
|
console.log(`run ${run.id} is still open; nothing to resume`);
|
|
164
187
|
return;
|
|
@@ -168,42 +191,42 @@ function runResume(ws, flags) {
|
|
|
168
191
|
{ resumed_at: now(), after_report_at: run.reported_at, reason: typeof flags.reason === 'string' ? flags.reason : null },
|
|
169
192
|
];
|
|
170
193
|
delete run.reported_at;
|
|
171
|
-
|
|
194
|
+
saveLog(ws, log);
|
|
172
195
|
console.log(`run ${run.id} resumed (follow-up ${run.follow_ups.length})`);
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
function stageStart(ws, [stage], flags) {
|
|
176
199
|
if (!STAGES.includes(stage)) fail(`unknown stage "${stage}"; one of ${STAGES.join(', ')}`);
|
|
177
|
-
const
|
|
178
|
-
const run = currentRun(
|
|
179
|
-
|
|
200
|
+
const log = loadLog(ws);
|
|
201
|
+
const run = currentRun(log);
|
|
202
|
+
log.history ??= [];
|
|
180
203
|
const phase = flags.phase ?? null;
|
|
181
|
-
const open =
|
|
204
|
+
const open = log.history.find((h) => h.run === run.id && h.stage === stage && h.phase === phase && !h.ended_at);
|
|
182
205
|
if (open) {
|
|
183
206
|
console.log(`${stage}${phase ? `/${phase}` : ''} already started at ${open.started_at}`);
|
|
184
207
|
return;
|
|
185
208
|
}
|
|
186
209
|
const late = lateStart(flags, run);
|
|
187
|
-
|
|
188
|
-
|
|
210
|
+
log.history.push({ run: run.id, stage, phase, started_at: late ?? now(), ended_at: null, result: null, note: null, ...(late ? { start_estimated: true } : {}) });
|
|
211
|
+
saveLog(ws, log);
|
|
189
212
|
console.log(`${stage}${phase ? `/${phase}` : ''} started${late ? ` (estimated start ${late})` : ''}`);
|
|
190
213
|
}
|
|
191
214
|
|
|
192
215
|
function stageEnd(ws, [stage, result], flags) {
|
|
193
216
|
if (!STAGES.includes(stage)) fail(`unknown stage "${stage}"`);
|
|
194
217
|
if (!RESULTS.includes(result)) fail(`unknown result "${result}"; one of ${RESULTS.join(', ')}`);
|
|
195
|
-
const
|
|
196
|
-
const run = currentRun(
|
|
197
|
-
|
|
218
|
+
const log = loadLog(ws);
|
|
219
|
+
const run = currentRun(log);
|
|
220
|
+
log.history ??= [];
|
|
198
221
|
const phase = flags.phase ?? null;
|
|
199
|
-
let entry = [...
|
|
222
|
+
let entry = [...log.history].reverse().find((h) => h.run === run.id && h.stage === stage && h.phase === phase && !h.ended_at);
|
|
200
223
|
if (!entry) {
|
|
201
224
|
// Missed stage-start: use the remembered start if given, else the end of the previous stage; either way estimated.
|
|
202
|
-
const runEntries =
|
|
225
|
+
const runEntries = log.history.filter((h) => h.run === run.id);
|
|
203
226
|
const lastEnd = runEntries.map((h) => h.ended_at).filter(Boolean).sort().at(-1);
|
|
204
227
|
const late = lateStart(flags, run);
|
|
205
228
|
entry = { run: run.id, stage, phase, started_at: late ?? lastEnd ?? run.started_at, ended_at: null, result: null, note: null, start_estimated: true };
|
|
206
|
-
|
|
229
|
+
log.history.push(entry);
|
|
207
230
|
console.log(`${stage}${phase ? `/${phase}` : ''}: stage-start was not logged; start estimated as ${entry.started_at}${late ? '' : ' (end of the previous stage). Pass --started-ago if you know better'}`);
|
|
208
231
|
} else if (flags['started-at'] || flags['started-ago']) {
|
|
209
232
|
entry.started_at = lateStart(flags, run);
|
|
@@ -212,23 +235,23 @@ function stageEnd(ws, [stage, result], flags) {
|
|
|
212
235
|
entry.ended_at = now();
|
|
213
236
|
entry.result = result;
|
|
214
237
|
if (typeof flags.note === 'string') entry.note = flags.note;
|
|
215
|
-
|
|
238
|
+
saveLog(ws, log);
|
|
216
239
|
console.log(`${stage}${phase ? `/${phase}` : ''} ended: ${result}`);
|
|
217
240
|
}
|
|
218
241
|
|
|
219
242
|
function stageFix(ws, [stage], flags) {
|
|
220
243
|
if (!STAGES.includes(stage)) fail(`unknown stage "${stage}"`);
|
|
221
|
-
const
|
|
222
|
-
const run = currentRun(
|
|
244
|
+
const log = loadLog(ws);
|
|
245
|
+
const run = currentRun(log);
|
|
223
246
|
const phase = flags.phase ?? null;
|
|
224
|
-
const entry = [...(
|
|
247
|
+
const entry = [...(log.history ?? [])].reverse().find((h) => h.run === run.id && h.stage === stage && h.phase === phase);
|
|
225
248
|
if (!entry) fail(`${stage}${phase ? `/${phase}` : ''} was never logged in this run; use stage-start with --started-ago instead`);
|
|
226
249
|
const late = lateStart(flags, run);
|
|
227
250
|
if (!late) fail('stage-fix needs --started-ago (e.g. 6m) or --started-at <ISO time>');
|
|
228
251
|
if (entry.ended_at && late > entry.ended_at) fail(`the new start ${late} is after the stage ended (${entry.ended_at})`);
|
|
229
252
|
entry.started_at = late;
|
|
230
253
|
entry.start_estimated = true;
|
|
231
|
-
|
|
254
|
+
saveLog(ws, log);
|
|
232
255
|
console.log(`${stage}${phase ? `/${phase}` : ''} start set to ${late} (estimated)`);
|
|
233
256
|
}
|
|
234
257
|
|
|
@@ -253,18 +276,16 @@ function event(ws, [type, raw]) {
|
|
|
253
276
|
}
|
|
254
277
|
// A bad line in events.jsonl would fail every later report and run-start.
|
|
255
278
|
validate({ ...data, logged_at: now() }, eventSchema(type), `event ${type}`);
|
|
256
|
-
const
|
|
257
|
-
const run = currentRun(
|
|
279
|
+
const log = loadLog(ws);
|
|
280
|
+
const run = currentRun(log);
|
|
258
281
|
appendFileSync(ws.eventsPath, `${JSON.stringify({ ts: now(), run: run.id, type, ...data })}\n`);
|
|
259
282
|
console.log(`${type} logged`);
|
|
260
283
|
}
|
|
261
284
|
|
|
262
285
|
const minutesBetween = (a, b) => (a && b ? Math.round(((Date.parse(b) - Date.parse(a)) / 60000) * 10) / 10 : null);
|
|
263
286
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const state = readJson(join(dir, 'state.json'), {});
|
|
267
|
-
const fe = state.frontend ?? {};
|
|
287
|
+
// Framework is recorded in state.json by the frontend step.
|
|
288
|
+
function frontendStack(fe, dir) {
|
|
268
289
|
const framework = fe.framework ?? fe.stack ?? null;
|
|
269
290
|
if (framework) return fe.rendering ? `${framework} (${fe.rendering})` : framework;
|
|
270
291
|
// Fallback: an older build may have a design.md with a "## Stack" heading.
|
|
@@ -273,12 +294,13 @@ function frontendStack(dir) {
|
|
|
273
294
|
}
|
|
274
295
|
|
|
275
296
|
function buildReport(ws, { outcome, tokens } = {}) {
|
|
276
|
-
const
|
|
277
|
-
const run = currentRun(
|
|
297
|
+
const log = loadLog(ws);
|
|
298
|
+
const run = currentRun(log);
|
|
278
299
|
const site = readJson(join(ws.dir, 'site.json'), {});
|
|
279
300
|
const deploy = readJson(join(ws.dir, 'deploy.json'), {});
|
|
301
|
+
const state = readState(ws);
|
|
280
302
|
const connect = readConnect(ws);
|
|
281
|
-
const history = (
|
|
303
|
+
const history = (log.history ?? []).filter((h) => h.run === run.id);
|
|
282
304
|
// Lines logged before events were checked on write may not match the schema.
|
|
283
305
|
const dropped = [];
|
|
284
306
|
const events = readEvents(ws)
|
|
@@ -316,7 +338,7 @@ function buildReport(ws, { outcome, tokens } = {}) {
|
|
|
316
338
|
const report = {
|
|
317
339
|
schema_version: '1',
|
|
318
340
|
generated_at: now(),
|
|
319
|
-
// Intent, not delivery: delivery is tracked in
|
|
341
|
+
// Intent, not delivery: delivery is tracked in run-log.json.
|
|
320
342
|
consent: { sent: true, note: 'Sent to Hostinger automatically at the end of the run.' },
|
|
321
343
|
run: { id: run.id, kind: run.kind, skill: run.skill, follow_ups: run.follow_ups ?? [] },
|
|
322
344
|
info: {
|
|
@@ -327,9 +349,9 @@ function buildReport(ws, { outcome, tokens } = {}) {
|
|
|
327
349
|
node: process.version,
|
|
328
350
|
mcp: connect.mcp,
|
|
329
351
|
backend: site.backend ?? null,
|
|
330
|
-
frontend: frontendStack(ws.dir),
|
|
352
|
+
frontend: frontendStack(state.frontend ?? {}, ws.dir),
|
|
331
353
|
outcome: resolvedOutcome,
|
|
332
|
-
stopped_at: resolvedOutcome === 'live' ? null : stoppedAt(stages, state),
|
|
354
|
+
stopped_at: resolvedOutcome === 'live' ? null : stoppedAt(stages, state.stage),
|
|
333
355
|
live_url: resolvedOutcome === 'live' ? deploy.urls?.site ?? site.urls?.site ?? null : null,
|
|
334
356
|
admin_url: site.urls?.wp_admin ?? null,
|
|
335
357
|
},
|
|
@@ -357,23 +379,23 @@ function buildReport(ws, { outcome, tokens } = {}) {
|
|
|
357
379
|
writeFileSync(join(ws.reportsDir, `${run.id}.html`), renderHtml(report));
|
|
358
380
|
copyFileSync(join(ws.reportsDir, `${run.id}.html`), join(ws.dir, 'report.html'));
|
|
359
381
|
|
|
360
|
-
const fresh =
|
|
382
|
+
const fresh = loadLog(ws);
|
|
361
383
|
fresh.run.reported_at = now();
|
|
362
|
-
|
|
384
|
+
saveLog(ws, fresh);
|
|
363
385
|
for (const g of gaps) console.log(`warning: ${g.stage}${g.phase ? `/${g.phase}` : ''}: ${g.problem}`);
|
|
364
386
|
return join(ws.dir, 'report.html');
|
|
365
387
|
}
|
|
366
388
|
|
|
367
389
|
// A report counts as sent only while the sent copy's generated_at matches the file.
|
|
368
390
|
function reportFile(ws, runId) {
|
|
369
|
-
const id = runId ?? currentRun(
|
|
391
|
+
const id = runId ?? currentRun(loadLog(ws)).id;
|
|
370
392
|
const file = join(ws.reportsDir, `${id}.json`);
|
|
371
393
|
if (!existsSync(file)) fail(`no report for run ${id} at ${file}`);
|
|
372
394
|
return { id, file, generatedAt: readJson(file, {}).generated_at };
|
|
373
395
|
}
|
|
374
396
|
|
|
375
397
|
function unsent(ws) {
|
|
376
|
-
const
|
|
398
|
+
const log = loadLog(ws);
|
|
377
399
|
const files = existsSync(ws.reportsDir) ? readdirSync(ws.reportsDir).filter((f) => f.endsWith('.json')).sort() : [];
|
|
378
400
|
// Older reports promised "nothing is uploaded".
|
|
379
401
|
const pending = files
|
|
@@ -382,26 +404,26 @@ function unsent(ws) {
|
|
|
382
404
|
return { id: f.replace(/\.json$/, ''), file: join(ws.reportsDir, f), generatedAt: report.generated_at, consent: report.consent };
|
|
383
405
|
})
|
|
384
406
|
.filter(({ consent }) => consent?.sent !== false)
|
|
385
|
-
.filter(({ id, generatedAt }) =>
|
|
407
|
+
.filter(({ id, generatedAt }) => log.reports_sent?.[id] !== generatedAt && log.reports_rejected?.[id] !== generatedAt);
|
|
386
408
|
if (!pending.length) console.log('no unsent reports');
|
|
387
409
|
for (const { id, file } of pending) console.log(`${id} ${file}`);
|
|
388
410
|
}
|
|
389
411
|
|
|
390
412
|
function reportSent(ws, [runId]) {
|
|
391
413
|
const { id, generatedAt } = reportFile(ws, runId);
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
414
|
+
const log = loadLog(ws);
|
|
415
|
+
log.reports_sent = { ...log.reports_sent, [id]: generatedAt };
|
|
416
|
+
saveLog(ws, log);
|
|
395
417
|
console.log(`report ${id} marked sent`);
|
|
396
418
|
}
|
|
397
419
|
|
|
398
420
|
// A 422 is schema drift, so resending won't help.
|
|
399
421
|
function reportRejected(ws, [runId]) {
|
|
400
422
|
const { id, generatedAt } = reportFile(ws, runId);
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const run = currentRun(
|
|
423
|
+
const log = loadLog(ws);
|
|
424
|
+
log.reports_rejected = { ...log.reports_rejected, [id]: generatedAt };
|
|
425
|
+
saveLog(ws, log);
|
|
426
|
+
const run = currentRun(log);
|
|
405
427
|
const symptom = `build report ${id} rejected with 422 by hosting_headless_submit-build-report`;
|
|
406
428
|
appendFileSync(ws.eventsPath, `${JSON.stringify({ ts: now(), run: run.id, type: 'issue', stage: run.reported_at ? 'done' : 'check_state', symptom, severity: 'medium', resolved: false, user_had_to_intervene: false })}\n`);
|
|
407
429
|
console.log(`report ${id} marked rejected; issue logged in run ${run.id}`);
|
|
@@ -430,9 +452,9 @@ function loggingGaps(stages, run, outcome, backend) {
|
|
|
430
452
|
return gaps;
|
|
431
453
|
}
|
|
432
454
|
|
|
433
|
-
function stoppedAt(stages,
|
|
455
|
+
function stoppedAt(stages, fallbackStage) {
|
|
434
456
|
const stuck = [...stages].reverse().find((s) => !s.ended_at || s.result === 'failed' || s.result === 'stopped');
|
|
435
|
-
return stuck?.stage ??
|
|
457
|
+
return stuck?.stage ?? fallbackStage ?? null;
|
|
436
458
|
}
|
|
437
459
|
|
|
438
460
|
// Minimal JSON Schema subset, so reports from different tools share one shape without a dependency.
|
|
@@ -444,8 +444,8 @@ The template's token list is the **minimum**: add what the design needs (`--colo
|
|
|
444
444
|
listed in *Grid, shape & motion* as plain px values.
|
|
445
445
|
|
|
446
446
|
Then update `state.json`: set `"stage": "frontend"` (also for a redesign: restyle, then validate and
|
|
447
|
-
deploy) and `updated_at` to the current UTC time (ISO 8601). Don't touch `provision
|
|
448
|
-
|
|
447
|
+
deploy) and `updated_at` to the current UTC time (ISO 8601). Don't touch `provision` or add
|
|
448
|
+
other keys.
|
|
449
449
|
|
|
450
450
|
If content prepare hasn't sourced images yet, it now follows `design.md` → **Imagery** (see
|
|
451
451
|
`04-content-model/SKILL.md`).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: headless-step-done
|
|
3
|
-
description: "Step 9 (done) of the Hostinger Headless flow. Verifies the live site, then hands over the URLs (hPanel success page, main site, WordPress/store admin, dashboard), opens the success page in the browser on a successful run, and gives a summary of what was built and
|
|
3
|
+
description: "Step 9 (done) of the Hostinger Headless flow. Verifies the live site, then hands over the URLs (hPanel success page, main site, WordPress/store admin, dashboard), opens the success page in the browser on a successful run, and gives a summary of what was built plus explicit next steps — how to log in to wp-admin and reset the password, how to manage content, and how to finish WooCommerce/store setup so it can take orders. Its final report format is also used whenever a run stops early."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# headless-step-done
|
|
@@ -52,14 +52,17 @@ Write it in this order:
|
|
|
52
52
|
- **WordPress admin (one-click auto-login):** for a WordPress backend, **mint and hand over the
|
|
53
53
|
auto-login redirect URL** — `wordpress_login_create-links { username, software: <site.json
|
|
54
54
|
wordpress.install_id> }` returns a temporary link that logs the user straight into wp-admin. Give
|
|
55
|
-
that URL as the admin link (it lands on `/cms/wp-admin`), so the user never needs a password.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
that URL as the admin link (it lands on `/cms/wp-admin`), so the user never needs a password.
|
|
56
|
+
Alongside it, give the two ways back in that don't expire: the **login URL `{urls.wp_admin}`**
|
|
57
|
+
(`https://<domain>/cms/wp-admin` — the headless WordPress lives under `/cms`), and the **WP Admin
|
|
58
|
+
button in hPanel** (hPanel → the website → *WP Admin*). Note how to **reset the password**: on the
|
|
59
|
+
`/cms/wp-admin` login screen use **"Lost your password?"**, and WordPress emails a reset link to the
|
|
60
|
+
**WordPress admin email** (the address set at install, which the owner already has) — see *Next
|
|
61
|
+
steps* for an optional deliverability tip.
|
|
62
|
+
**Never print or store a password.** For a **WordPress + WooCommerce** store, this *same* wp-admin
|
|
63
|
+
login is the store admin (wp-admin → WooCommerce → Products/Orders) — do **not** use the Ecommerce
|
|
64
|
+
`store_admin` path; the store setup the owner still needs is spelled out under *Next steps*. For a
|
|
65
|
+
Hostinger **Ecommerce** store, link the store admin from `urls.store_admin` instead.
|
|
63
66
|
- **hPanel site dashboard:** `https://hpanel.hostinger.com/websites/<domain>` (the headless website
|
|
64
67
|
is installed by now).
|
|
65
68
|
- **Onboarding (resume):** `https://hpanel.hostinger.com/headless-onboarding/<order_id>/<username>/`
|
|
@@ -69,10 +72,34 @@ Write it in this order:
|
|
|
69
72
|
> release. If a built URL 404s, retry the same path without the `/<username>` segment before
|
|
70
73
|
> reporting it as broken.
|
|
71
74
|
|
|
72
|
-
4. **
|
|
75
|
+
4. **Next steps** — what the user does now to actually use the site. Required on a successful run;
|
|
76
|
+
tailor the bullets to the backend and keep them concrete. This is the hand-holding the hPanel
|
|
77
|
+
success page can't give: don't just link wp-admin, tell them what to do once they're in.
|
|
78
|
+
|
|
79
|
+
- **Manage content (WordPress):** in wp-admin (the one-click link, the `/cms/wp-admin` login URL, or
|
|
80
|
+
hPanel → *WP Admin*) → **Posts**, **Pages**, **Media**, **Appearance → Menus**. Say how edits reach
|
|
81
|
+
the live site for *this* build: an **SSR** frontend re-reads the REST API each request, so changes
|
|
82
|
+
show up with no redeploy; an **SSG/static** build must be redeployed to pick up new content (read
|
|
83
|
+
`state.json → frontend.rendering`).
|
|
84
|
+
- **Log in later & reset the password:** repeat the two non-expiring routes (the `/cms/wp-admin` login
|
|
85
|
+
URL and hPanel's *WP Admin* button) and the **"Lost your password?"** reset. The reset link goes to
|
|
86
|
+
the **WordPress admin email** — the address set at install, which the owner already has — so nothing
|
|
87
|
+
extra is needed to receive it. Optionally, for more reliable delivery, they can create a mailbox on
|
|
88
|
+
the domain (hPanel → *Emails*) and set it as the admin address in **wp-admin → Settings → General**.
|
|
89
|
+
- **WordPress + WooCommerce — finish store setup so it can take orders.** The catalog and cart already
|
|
90
|
+
work, but **checkout stays off until the owner configures it in wp-admin**: WooCommerce → Settings →
|
|
91
|
+
**Payments** (enable at least one method) and → **Shipping** (add a zone with a rate), then confirm
|
|
92
|
+
products/prices and place a test order. When `site.json → woocommerce.payment_enabled` is `false`,
|
|
93
|
+
state plainly that this step is **required before any purchase works** — this is the gap owners hit.
|
|
94
|
+
- **Hostinger Ecommerce store:** when `site.json → ecommerce.payment_enabled` is `false`, finish
|
|
95
|
+
payment setup in the store admin (`urls.store_admin`) before the storefront can sell; otherwise it's
|
|
96
|
+
ready to take orders.
|
|
97
|
+
- **Webapp:** manage the app from hPanel → the website (environment variables, deployments, logs).
|
|
98
|
+
|
|
99
|
+
5. **Where state lives** — the workspace path `~/.hostinger/<domain>/`, the `stage` that a
|
|
73
100
|
resumed run will start from, and the build report `~/.hostinger/<domain>/report.html`.
|
|
74
101
|
|
|
75
|
-
|
|
102
|
+
6. **Gaps / follow-ups** (only if any) — a short numbered list of anything skipped, approximated or
|
|
76
103
|
left for the user (e.g. no images generated, default WordPress content left in place).
|
|
77
104
|
|
|
78
105
|
Never include passwords, JWTs or other secrets in the report. For wp-admin access, give the URL and
|
|
@@ -129,7 +156,7 @@ open "$URL" # macOS
|
|
|
129
156
|
## Build report
|
|
130
157
|
|
|
131
158
|
Right after the final report, save the build report for this run. It turns the run log (stage times
|
|
132
|
-
from `
|
|
159
|
+
from `run-log.json`, events from `events.jsonl`) into `reports/<run>.json` and `.html`, and copies the
|
|
133
160
|
HTML to `report.html` in the workspace:
|
|
134
161
|
|
|
135
162
|
```bash
|