@sakupa/mcp 0.7.10 → 0.7.11
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/bin.js +71 -25
- package/dist/index.js +71 -25
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -129,7 +129,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
129
129
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
130
130
|
|
|
131
131
|
// ../core/dist/domain/version.js
|
|
132
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
132
|
+
var SAKUPA_MCP_VERSION = "0.7.11";
|
|
133
133
|
|
|
134
134
|
// ../core/dist/domain/errors.js
|
|
135
135
|
var HTTP_STATUS = {
|
|
@@ -686,7 +686,7 @@ var HttpApiClient = class {
|
|
|
686
686
|
|
|
687
687
|
// src/tools/definitions.ts
|
|
688
688
|
import { randomUUID } from "node:crypto";
|
|
689
|
-
import { promises as fs2 } from "node:fs";
|
|
689
|
+
import { existsSync as existsSync3, promises as fs2 } from "node:fs";
|
|
690
690
|
import { join as join4, resolve as resolve3 } from "node:path";
|
|
691
691
|
import { z as z3 } from "zod";
|
|
692
692
|
|
|
@@ -1196,14 +1196,18 @@ function deleteSiteFile(projectDir) {
|
|
|
1196
1196
|
rmSync(path, { force: true });
|
|
1197
1197
|
}
|
|
1198
1198
|
}
|
|
1199
|
-
function
|
|
1200
|
-
let
|
|
1201
|
-
for (; ; ) {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
if (parent
|
|
1205
|
-
|
|
1199
|
+
function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
|
|
1200
|
+
let cursor = startDir;
|
|
1201
|
+
for (let i = 0; i < maxLevels; i += 1) {
|
|
1202
|
+
const parent = dirname(cursor);
|
|
1203
|
+
if (parent === cursor) return null;
|
|
1204
|
+
if (predicate(parent)) return parent;
|
|
1205
|
+
cursor = parent;
|
|
1206
1206
|
}
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
function isInsideGitRepo(projectDir) {
|
|
1210
|
+
return existsSync(join2(projectDir, ".git")) || findAncestor(projectDir, (dir) => existsSync(join2(dir, ".git"))) !== null;
|
|
1207
1211
|
}
|
|
1208
1212
|
function credentialGitReminder(projectDir) {
|
|
1209
1213
|
if (!isInsideGitRepo(projectDir)) return "";
|
|
@@ -1354,7 +1358,7 @@ async function diagnoseBinding(input) {
|
|
|
1354
1358
|
const apexResolves = apexAnswers.length > 0;
|
|
1355
1359
|
const allOk = checks.every((c) => c.state === "ok");
|
|
1356
1360
|
const checklist = checks.map(renderCheck).join("\n") + `
|
|
1357
|
-
[${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet
|
|
1361
|
+
[${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet. Three-step fix, stop at the first that works: (1) try adding a plain record \u2014 type CNAME, host @, value ${input.servingTarget} (most panels and Cloudflare accept this directly; confirm past any MX-conflict warning if the domain sends no email). (2) If rejected, look for ALIAS / ANAME / CNAME-flattening in the record-type list \u2014 same host and value. (3) If the panel has neither, skip the apex: www alone works fine (certificates do not depend on the apex record); optionally add a URL redirect from @ to www.`);
|
|
1358
1362
|
const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Cloudflare validates and issues within minutes once the records above are all OK; Sakupa retries automatically every ~5 minutes)." : "starts after verification.");
|
|
1359
1363
|
return { checks, apexResolves, allOk, checklist, layers };
|
|
1360
1364
|
}
|
|
@@ -1437,7 +1441,7 @@ var LocalGuidanceError = class extends SakupaError {
|
|
|
1437
1441
|
}
|
|
1438
1442
|
};
|
|
1439
1443
|
var projectDirInput = z2.string().optional().describe(
|
|
1440
|
-
"Absolute path of the
|
|
1444
|
+
"Absolute path of the user's PROJECT ROOT \u2014 the folder the user opened/works in (for framework projects: where package.json lives, NEVER the build-output subfolder like dist/out; the analyzer locates the output automatically). .sakupa/site.json lives here, so PASS THE SAME DIRECTORY EVERY TIME for the same project. When omitted the server falls back to its startup directory, which may not be where the user is working now."
|
|
1441
1445
|
);
|
|
1442
1446
|
function withProjectDir(ctx, projectDirArg) {
|
|
1443
1447
|
if (projectDirArg === void 0) return ctx;
|
|
@@ -1661,6 +1665,20 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
|
1661
1665
|
};
|
|
1662
1666
|
}
|
|
1663
1667
|
}
|
|
1668
|
+
function projectRootAbove(projectDir) {
|
|
1669
|
+
if (existsSync3(join4(projectDir, "package.json"))) return null;
|
|
1670
|
+
return findAncestor(projectDir, (dir) => existsSync3(join4(dir, "package.json")), 4);
|
|
1671
|
+
}
|
|
1672
|
+
function findNeighborBinding(projectDir, outputRel) {
|
|
1673
|
+
const bound = (dir) => loadSiteFile(dir).kind !== "absent";
|
|
1674
|
+
const above = findAncestor(projectDir, bound, 3);
|
|
1675
|
+
if (above) return above;
|
|
1676
|
+
if (outputRel && outputRel !== ".") {
|
|
1677
|
+
const outputAbs = resolve3(projectDir, outputRel);
|
|
1678
|
+
if (bound(outputAbs)) return outputAbs;
|
|
1679
|
+
}
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1664
1682
|
function freeSiteCreationBarrier() {
|
|
1665
1683
|
const recent = listRecentCreations(Date.now());
|
|
1666
1684
|
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
@@ -1721,6 +1739,9 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1721
1739
|
publicConfirmed: z3.boolean().optional().describe(
|
|
1722
1740
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1723
1741
|
),
|
|
1742
|
+
subprojectConfirmed: z3.boolean().optional().describe(
|
|
1743
|
+
"Only when creating a NEW site in a subfolder of a package.json project: the user explicitly confirmed this subfolder is an INDEPENDENT site, not the project's build output."
|
|
1744
|
+
),
|
|
1724
1745
|
lang: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1725
1746
|
}
|
|
1726
1747
|
},
|
|
@@ -1749,16 +1770,34 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1749
1770
|
}
|
|
1750
1771
|
const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
1751
1772
|
if (!existing) {
|
|
1773
|
+
const rootAbove = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
|
|
1774
|
+
if (rootAbove) {
|
|
1775
|
+
return text(
|
|
1776
|
+
"not_project_root",
|
|
1777
|
+
`${ctx.projectDir} has no package.json but ${rootAbove} does \u2014 this directory is a SUBFOLDER of that project (typically its build output), and .sakupa must live at the project ROOT. Re-run deploy_site with projectDir: ${rootAbove}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
|
|
1778
|
+
{ projectRoot: rootAbove, confirmationField: "subprojectConfirmed" },
|
|
1779
|
+
"blocked"
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
const neighbor = findNeighborBinding(ctx.projectDir, analysis.recommendedOutputDir);
|
|
1783
|
+
if (neighbor) {
|
|
1784
|
+
return text(
|
|
1785
|
+
"neighbor_binding_found",
|
|
1786
|
+
`No .sakupa binding in ${ctx.projectDir}, but one EXISTS at ${neighbor} \u2014 this looks like the same project addressed at a different directory level. To update that existing site, re-run deploy_site with projectDir: ${neighbor}. Only if the user explicitly wants a SEPARATE new site, move this deploy to a directory outside that project. Nothing was deployed and no site was created.`,
|
|
1787
|
+
{ neighborProjectDir: neighbor },
|
|
1788
|
+
"blocked"
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1752
1791
|
const barrier = freeSiteCreationBarrier();
|
|
1753
1792
|
if (barrier) return barrier;
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1793
|
+
if (args.publicConfirmed !== true) {
|
|
1794
|
+
return text(
|
|
1795
|
+
"public_deployment_confirmation_required",
|
|
1796
|
+
`First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Explain this to the user and obtain explicit confirmation before retrying deploy_site with publicConfirmed: true.`,
|
|
1797
|
+
{ publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
|
|
1798
|
+
"waiting_user"
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1762
1801
|
}
|
|
1763
1802
|
ensureUploadSizeWithinLimits(manifest, !existing);
|
|
1764
1803
|
if (!existing) {
|
|
@@ -2051,17 +2090,19 @@ ${res2.message}
|
|
|
2051
2090
|
"domain_verification_started",
|
|
2052
2091
|
`Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
2053
2092
|
|
|
2054
|
-
|
|
2093
|
+
STEP 1 of 2 \u2014 add ALL THREE records NOW:
|
|
2055
2094
|
|
|
2056
2095
|
1) TXT host: ${txtShort} value: ${res.verificationRecord.value}
|
|
2057
2096
|
2) CNAME host: www value: ${res.servingTarget}
|
|
2058
|
-
3) APEX host: @ -> ${res.servingTarget}
|
|
2097
|
+
3) APEX host: @ -> ${res.servingTarget}. Try a plain CNAME at host @ first (most panels accept it); if rejected use ALIAS / ANAME / CNAME-flattening; if the panel has neither, skip it \u2014 www alone works, and a URL redirect from @ to www covers bare-domain visitors.
|
|
2059
2098
|
|
|
2060
2099
|
Host fields above are the SHORT form: most DNS panels append the domain automatically. After saving, the record list must NOT show ${apex} twice in one name \u2014 that means the full name was pasted into an auto-appending field.
|
|
2061
2100
|
|
|
2062
2101
|
Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
|
|
2063
2102
|
|
|
2064
|
-
|
|
2103
|
+
STEP 2 (after ownership verifies): Cloudflare issues certificate-validation TXT records \u2014 the verification result and bind_domain "status" list them the moment they exist; relay each to the user and add them too. Everything then completes automatically.
|
|
2104
|
+
|
|
2105
|
+
Drive the whole flow with bind_domain "status": it live-checks every record and names the exact fix for anything wrong. Re-check every 5 minutes (up to 10 times). Any later session can resume with action "status" alone; the verificationId is optional.`,
|
|
2065
2106
|
{
|
|
2066
2107
|
verificationId: res.verificationId,
|
|
2067
2108
|
apexDomain: apex,
|
|
@@ -2709,13 +2750,18 @@ Workflow:
|
|
|
2709
2750
|
|
|
2710
2751
|
Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
|
|
2711
2752
|
binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
|
|
2712
|
-
the
|
|
2713
|
-
|
|
2714
|
-
|
|
2753
|
+
the user's PROJECT ROOT, the SAME directory every time for the same project: the folder
|
|
2754
|
+
the user opened (for framework projects, where package.json lives \u2014 never the dist/out
|
|
2755
|
+
build folder; output is auto-detected). Without it the server falls back to its startup
|
|
2756
|
+
directory, which may be a different project than the one the user is looking at. After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
2715
2757
|
Explicit Environment line: TEST vs PRODUCTION). analyze_site, deploy_site, site_status,
|
|
2716
2758
|
refresh_site, delete_site and unbind_domain echo
|
|
2717
2759
|
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2718
2760
|
|
|
2761
|
+
When the same operation fails twice in a row, or the user is clearly stuck or
|
|
2762
|
+
frustrated, proactively offer report_bug: it files the problem into Sakupa's ticket and
|
|
2763
|
+
alert stream, and you should attach your own factual account via agentContext.
|
|
2764
|
+
|
|
2719
2765
|
Safety boundaries:
|
|
2720
2766
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
2721
2767
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|
package/dist/index.js
CHANGED
|
@@ -124,7 +124,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
124
124
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
125
125
|
|
|
126
126
|
// ../core/dist/domain/version.js
|
|
127
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
127
|
+
var SAKUPA_MCP_VERSION = "0.7.11";
|
|
128
128
|
|
|
129
129
|
// ../core/dist/domain/errors.js
|
|
130
130
|
var HTTP_STATUS = {
|
|
@@ -839,14 +839,18 @@ function deleteSiteFile(projectDir) {
|
|
|
839
839
|
rmSync(path, { force: true });
|
|
840
840
|
}
|
|
841
841
|
}
|
|
842
|
-
function
|
|
843
|
-
let
|
|
844
|
-
for (; ; ) {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (parent
|
|
848
|
-
|
|
842
|
+
function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
|
|
843
|
+
let cursor = startDir;
|
|
844
|
+
for (let i = 0; i < maxLevels; i += 1) {
|
|
845
|
+
const parent = dirname(cursor);
|
|
846
|
+
if (parent === cursor) return null;
|
|
847
|
+
if (predicate(parent)) return parent;
|
|
848
|
+
cursor = parent;
|
|
849
849
|
}
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
function isInsideGitRepo(projectDir) {
|
|
853
|
+
return existsSync(join(projectDir, ".git")) || findAncestor(projectDir, (dir) => existsSync(join(dir, ".git"))) !== null;
|
|
850
854
|
}
|
|
851
855
|
function credentialGitReminder(projectDir) {
|
|
852
856
|
if (!isInsideGitRepo(projectDir)) return "";
|
|
@@ -1326,7 +1330,7 @@ var LocalGuidanceError = class extends SakupaError {
|
|
|
1326
1330
|
}
|
|
1327
1331
|
};
|
|
1328
1332
|
var projectDirInput = z2.string().optional().describe(
|
|
1329
|
-
"Absolute path of the
|
|
1333
|
+
"Absolute path of the user's PROJECT ROOT \u2014 the folder the user opened/works in (for framework projects: where package.json lives, NEVER the build-output subfolder like dist/out; the analyzer locates the output automatically). .sakupa/site.json lives here, so PASS THE SAME DIRECTORY EVERY TIME for the same project. When omitted the server falls back to its startup directory, which may not be where the user is working now."
|
|
1330
1334
|
);
|
|
1331
1335
|
function withProjectDir(ctx, projectDirArg) {
|
|
1332
1336
|
if (projectDirArg === void 0) return ctx;
|
|
@@ -1414,7 +1418,7 @@ function toolError(e) {
|
|
|
1414
1418
|
|
|
1415
1419
|
// src/tools/definitions.ts
|
|
1416
1420
|
import { randomUUID } from "node:crypto";
|
|
1417
|
-
import { promises as fs2 } from "node:fs";
|
|
1421
|
+
import { existsSync as existsSync3, promises as fs2 } from "node:fs";
|
|
1418
1422
|
import { join as join4, resolve as resolve3 } from "node:path";
|
|
1419
1423
|
import { z as z3 } from "zod";
|
|
1420
1424
|
|
|
@@ -1562,7 +1566,7 @@ async function diagnoseBinding(input) {
|
|
|
1562
1566
|
const apexResolves = apexAnswers.length > 0;
|
|
1563
1567
|
const allOk = checks.every((c) => c.state === "ok");
|
|
1564
1568
|
const checklist = checks.map(renderCheck).join("\n") + `
|
|
1565
|
-
[${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet
|
|
1569
|
+
[${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet. Three-step fix, stop at the first that works: (1) try adding a plain record \u2014 type CNAME, host @, value ${input.servingTarget} (most panels and Cloudflare accept this directly; confirm past any MX-conflict warning if the domain sends no email). (2) If rejected, look for ALIAS / ANAME / CNAME-flattening in the record-type list \u2014 same host and value. (3) If the panel has neither, skip the apex: www alone works fine (certificates do not depend on the apex record); optionally add a URL redirect from @ to www.`);
|
|
1566
1570
|
const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Cloudflare validates and issues within minutes once the records above are all OK; Sakupa retries automatically every ~5 minutes)." : "starts after verification.");
|
|
1567
1571
|
return { checks, apexResolves, allOk, checklist, layers };
|
|
1568
1572
|
}
|
|
@@ -1725,6 +1729,20 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
|
1725
1729
|
};
|
|
1726
1730
|
}
|
|
1727
1731
|
}
|
|
1732
|
+
function projectRootAbove(projectDir) {
|
|
1733
|
+
if (existsSync3(join4(projectDir, "package.json"))) return null;
|
|
1734
|
+
return findAncestor(projectDir, (dir) => existsSync3(join4(dir, "package.json")), 4);
|
|
1735
|
+
}
|
|
1736
|
+
function findNeighborBinding(projectDir, outputRel) {
|
|
1737
|
+
const bound = (dir) => loadSiteFile(dir).kind !== "absent";
|
|
1738
|
+
const above = findAncestor(projectDir, bound, 3);
|
|
1739
|
+
if (above) return above;
|
|
1740
|
+
if (outputRel && outputRel !== ".") {
|
|
1741
|
+
const outputAbs = resolve3(projectDir, outputRel);
|
|
1742
|
+
if (bound(outputAbs)) return outputAbs;
|
|
1743
|
+
}
|
|
1744
|
+
return null;
|
|
1745
|
+
}
|
|
1728
1746
|
function freeSiteCreationBarrier() {
|
|
1729
1747
|
const recent = listRecentCreations(Date.now());
|
|
1730
1748
|
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
@@ -1785,6 +1803,9 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1785
1803
|
publicConfirmed: z3.boolean().optional().describe(
|
|
1786
1804
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1787
1805
|
),
|
|
1806
|
+
subprojectConfirmed: z3.boolean().optional().describe(
|
|
1807
|
+
"Only when creating a NEW site in a subfolder of a package.json project: the user explicitly confirmed this subfolder is an INDEPENDENT site, not the project's build output."
|
|
1808
|
+
),
|
|
1788
1809
|
lang: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1789
1810
|
}
|
|
1790
1811
|
},
|
|
@@ -1813,16 +1834,34 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1813
1834
|
}
|
|
1814
1835
|
const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
1815
1836
|
if (!existing) {
|
|
1837
|
+
const rootAbove = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
|
|
1838
|
+
if (rootAbove) {
|
|
1839
|
+
return text(
|
|
1840
|
+
"not_project_root",
|
|
1841
|
+
`${ctx.projectDir} has no package.json but ${rootAbove} does \u2014 this directory is a SUBFOLDER of that project (typically its build output), and .sakupa must live at the project ROOT. Re-run deploy_site with projectDir: ${rootAbove}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
|
|
1842
|
+
{ projectRoot: rootAbove, confirmationField: "subprojectConfirmed" },
|
|
1843
|
+
"blocked"
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
const neighbor = findNeighborBinding(ctx.projectDir, analysis.recommendedOutputDir);
|
|
1847
|
+
if (neighbor) {
|
|
1848
|
+
return text(
|
|
1849
|
+
"neighbor_binding_found",
|
|
1850
|
+
`No .sakupa binding in ${ctx.projectDir}, but one EXISTS at ${neighbor} \u2014 this looks like the same project addressed at a different directory level. To update that existing site, re-run deploy_site with projectDir: ${neighbor}. Only if the user explicitly wants a SEPARATE new site, move this deploy to a directory outside that project. Nothing was deployed and no site was created.`,
|
|
1851
|
+
{ neighborProjectDir: neighbor },
|
|
1852
|
+
"blocked"
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1816
1855
|
const barrier = freeSiteCreationBarrier();
|
|
1817
1856
|
if (barrier) return barrier;
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1857
|
+
if (args.publicConfirmed !== true) {
|
|
1858
|
+
return text(
|
|
1859
|
+
"public_deployment_confirmation_required",
|
|
1860
|
+
`First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Explain this to the user and obtain explicit confirmation before retrying deploy_site with publicConfirmed: true.`,
|
|
1861
|
+
{ publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
|
|
1862
|
+
"waiting_user"
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1826
1865
|
}
|
|
1827
1866
|
ensureUploadSizeWithinLimits(manifest, !existing);
|
|
1828
1867
|
if (!existing) {
|
|
@@ -2115,17 +2154,19 @@ ${res2.message}
|
|
|
2115
2154
|
"domain_verification_started",
|
|
2116
2155
|
`Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
2117
2156
|
|
|
2118
|
-
|
|
2157
|
+
STEP 1 of 2 \u2014 add ALL THREE records NOW:
|
|
2119
2158
|
|
|
2120
2159
|
1) TXT host: ${txtShort} value: ${res.verificationRecord.value}
|
|
2121
2160
|
2) CNAME host: www value: ${res.servingTarget}
|
|
2122
|
-
3) APEX host: @ -> ${res.servingTarget}
|
|
2161
|
+
3) APEX host: @ -> ${res.servingTarget}. Try a plain CNAME at host @ first (most panels accept it); if rejected use ALIAS / ANAME / CNAME-flattening; if the panel has neither, skip it \u2014 www alone works, and a URL redirect from @ to www covers bare-domain visitors.
|
|
2123
2162
|
|
|
2124
2163
|
Host fields above are the SHORT form: most DNS panels append the domain automatically. After saving, the record list must NOT show ${apex} twice in one name \u2014 that means the full name was pasted into an auto-appending field.
|
|
2125
2164
|
|
|
2126
2165
|
Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
|
|
2127
2166
|
|
|
2128
|
-
|
|
2167
|
+
STEP 2 (after ownership verifies): Cloudflare issues certificate-validation TXT records \u2014 the verification result and bind_domain "status" list them the moment they exist; relay each to the user and add them too. Everything then completes automatically.
|
|
2168
|
+
|
|
2169
|
+
Drive the whole flow with bind_domain "status": it live-checks every record and names the exact fix for anything wrong. Re-check every 5 minutes (up to 10 times). Any later session can resume with action "status" alone; the verificationId is optional.`,
|
|
2129
2170
|
{
|
|
2130
2171
|
verificationId: res.verificationId,
|
|
2131
2172
|
apexDomain: apex,
|
|
@@ -2704,13 +2745,18 @@ Workflow:
|
|
|
2704
2745
|
|
|
2705
2746
|
Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
|
|
2706
2747
|
binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
|
|
2707
|
-
the
|
|
2708
|
-
|
|
2709
|
-
|
|
2748
|
+
the user's PROJECT ROOT, the SAME directory every time for the same project: the folder
|
|
2749
|
+
the user opened (for framework projects, where package.json lives \u2014 never the dist/out
|
|
2750
|
+
build folder; output is auto-detected). Without it the server falls back to its startup
|
|
2751
|
+
directory, which may be a different project than the one the user is looking at. After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
2710
2752
|
Explicit Environment line: TEST vs PRODUCTION). analyze_site, deploy_site, site_status,
|
|
2711
2753
|
refresh_site, delete_site and unbind_domain echo
|
|
2712
2754
|
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2713
2755
|
|
|
2756
|
+
When the same operation fails twice in a row, or the user is clearly stuck or
|
|
2757
|
+
frustrated, proactively offer report_bug: it files the problem into Sakupa's ticket and
|
|
2758
|
+
alert stream, and you should attach your own factual account via agentContext.
|
|
2759
|
+
|
|
2714
2760
|
Safety boundaries:
|
|
2715
2761
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
2716
2762
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|