@appsoftwareltd/etherpk-mcp 0.14.1 → 0.15.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/README.md +23 -3
- package/dist/main.js +793 -196
- package/dist/main.js.map +1 -1
- package/package.json +5 -5
package/dist/main.js
CHANGED
|
@@ -717,7 +717,7 @@ function looksLikeAgentSetupCode(value) {
|
|
|
717
717
|
}
|
|
718
718
|
var package_default = {
|
|
719
719
|
name: "@appsoftwareltd/etherpk-mcp",
|
|
720
|
-
version: "0.
|
|
720
|
+
version: "0.15.0",
|
|
721
721
|
license: "Elastic-2.0",
|
|
722
722
|
description: "EtherPK Headless Client: an MCP server over a synced knowledge graph, run beside the agent on the user's own machine.",
|
|
723
723
|
type: "module",
|
|
@@ -4201,8 +4201,8 @@ function parseBlocks(markdown) {
|
|
|
4201
4201
|
* does.
|
|
4202
4202
|
*/
|
|
4203
4203
|
/** An extension's id or a name under it: lowercase letters, digits and hyphens, from a letter. */
|
|
4204
|
-
var NAME = "[a-z][a-z0-9-]*";
|
|
4205
|
-
var EXTENSION_FENCE_INFO = new RegExp(`^${NAME}\\.${NAME}$`);
|
|
4204
|
+
var NAME$1 = "[a-z][a-z0-9-]*";
|
|
4205
|
+
var EXTENSION_FENCE_INFO = new RegExp(`^${NAME$1}\\.${NAME$1}$`);
|
|
4206
4206
|
/** Whether an info word names an Extension Fence: an extension's id, a dot and a name. */
|
|
4207
4207
|
function isExtensionFenceInfo(info) {
|
|
4208
4208
|
return EXTENSION_FENCE_INFO.test(info);
|
|
@@ -11258,8 +11258,10 @@ async function hostRequest(endpoint, request, options = {}) {
|
|
|
11258
11258
|
/**
|
|
11259
11259
|
* The settings a publish reads, which the command line decides even when the graph's host runs
|
|
11260
11260
|
* the publish: where the publish folders are remembered, which browser draws diagrams and maps,
|
|
11261
|
-
*
|
|
11262
|
-
*
|
|
11261
|
+
* which style a map's picture is drawn over, and the GitHub token and API a [[Publish
|
|
11262
|
+
* Repository]] is committed with (ADR 0152). The host was started from another environment (an
|
|
11263
|
+
* agent's), and a scheduled publish may name its own browser, map host or token. The token goes
|
|
11264
|
+
* no further than this request: the host uses it for this publish and keeps nothing of it.
|
|
11263
11265
|
*/
|
|
11264
11266
|
var PUBLISH_SETTINGS = [
|
|
11265
11267
|
"ETHERPK_MCP_PUBLISH_CONFIG",
|
|
@@ -11267,7 +11269,9 @@ var PUBLISH_SETTINGS = [
|
|
|
11267
11269
|
"ETHERPK_CHROMIUM",
|
|
11268
11270
|
"PLAYWRIGHT_BROWSERS_PATH",
|
|
11269
11271
|
"PLAYWRIGHT_CHROMIUM_EXECUTABLE",
|
|
11270
|
-
"ETHERPK_MAP_STYLE_URL"
|
|
11272
|
+
"ETHERPK_MAP_STYLE_URL",
|
|
11273
|
+
"ETHERPK_GITHUB_TOKEN",
|
|
11274
|
+
"ETHERPK_GITHUB_API_URL"
|
|
11271
11275
|
];
|
|
11272
11276
|
/** The publish settings in `env`, for a `publish` request. */
|
|
11273
11277
|
function publishSettings(env) {
|
|
@@ -11625,6 +11629,13 @@ function sameBytes(a, b) {
|
|
|
11625
11629
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
11626
11630
|
return true;
|
|
11627
11631
|
}
|
|
11632
|
+
/** Whether the folder already holds `content` at `path`: compared where it can, read back where it cannot. */
|
|
11633
|
+
async function holds$1(folder, path, content) {
|
|
11634
|
+
if (folder.sameContent) return folder.sameContent(path, content);
|
|
11635
|
+
if (typeof content === "string") return await folder.readText(path) === content;
|
|
11636
|
+
const existing = await folder.readBytes(path);
|
|
11637
|
+
return existing !== null && sameBytes(existing, content);
|
|
11638
|
+
}
|
|
11628
11639
|
/** Write a site into a folder under the rules above, whatever stands behind the folder. */
|
|
11629
11640
|
async function writeSite(folder, bundle, options) {
|
|
11630
11641
|
const result = {
|
|
@@ -11636,17 +11647,9 @@ async function writeSite(folder, bundle, options) {
|
|
|
11636
11647
|
let done = 0;
|
|
11637
11648
|
for (const [path, content] of bundle) {
|
|
11638
11649
|
options.onProgress?.(done++, total);
|
|
11639
|
-
if (
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
continue;
|
|
11643
|
-
}
|
|
11644
|
-
} else {
|
|
11645
|
-
const existing = await folder.readBytes(path);
|
|
11646
|
-
if (existing && sameBytes(existing, content)) {
|
|
11647
|
-
result.unchanged++;
|
|
11648
|
-
continue;
|
|
11649
|
-
}
|
|
11650
|
+
if (await holds$1(folder, path, content)) {
|
|
11651
|
+
result.unchanged++;
|
|
11652
|
+
continue;
|
|
11650
11653
|
}
|
|
11651
11654
|
await folder.writeFile(path, content);
|
|
11652
11655
|
result.written++;
|
|
@@ -11672,6 +11675,570 @@ async function writeSite(folder, bundle, options) {
|
|
|
11672
11675
|
options.onProgress?.(total, total);
|
|
11673
11676
|
return result;
|
|
11674
11677
|
}
|
|
11678
|
+
var GitHubError = class extends Error {
|
|
11679
|
+
code;
|
|
11680
|
+
status;
|
|
11681
|
+
detail;
|
|
11682
|
+
constructor(code, message, status, detail) {
|
|
11683
|
+
super(message);
|
|
11684
|
+
this.code = code;
|
|
11685
|
+
this.status = status;
|
|
11686
|
+
this.detail = detail;
|
|
11687
|
+
this.name = "GitHubError";
|
|
11688
|
+
}
|
|
11689
|
+
};
|
|
11690
|
+
/** Writes GitHub asks to be spaced (its "best practices for using the REST API"). */
|
|
11691
|
+
var WRITE_SPACING_MS = 1e3;
|
|
11692
|
+
/** How many times a request GitHub failed on (5xx) is tried again. Creating git objects is idempotent. */
|
|
11693
|
+
var SERVER_ERROR_RETRIES = 2;
|
|
11694
|
+
var realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
11695
|
+
/** True for a 403 or 429 that GitHub sends as a rate limit, rather than a refusal. */
|
|
11696
|
+
function isRateLimit(status, headers, message) {
|
|
11697
|
+
if (status === 429) return true;
|
|
11698
|
+
if (status !== 403) return false;
|
|
11699
|
+
return headers.has("retry-after") || headers.get("x-ratelimit-remaining") === "0" || /rate limit/i.test(message);
|
|
11700
|
+
}
|
|
11701
|
+
/** How long to wait before trying again, in GitHub's order: retry-after, the reset time, a doubling minute. */
|
|
11702
|
+
function waitFor(headers, attempt, now) {
|
|
11703
|
+
const retryAfter = Number(headers.get("retry-after"));
|
|
11704
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) return retryAfter * 1e3;
|
|
11705
|
+
const reset = Number(headers.get("x-ratelimit-reset"));
|
|
11706
|
+
if (headers.get("x-ratelimit-remaining") === "0" && Number.isFinite(reset) && reset * 1e3 > now) return reset * 1e3 - now;
|
|
11707
|
+
return 6e4 * 2 ** attempt;
|
|
11708
|
+
}
|
|
11709
|
+
/** The code for a refusal GitHub sent, from its status and message. */
|
|
11710
|
+
function refusalCode(status, message) {
|
|
11711
|
+
if (status === 401) return "bad-token";
|
|
11712
|
+
if (/not a fast.?forward/i.test(message)) return "branch-moved";
|
|
11713
|
+
if (/protected branch|pull request|GH006|signed/i.test(message)) return "branch-protected";
|
|
11714
|
+
if (status === 409 && /empty/i.test(message)) return "empty-repository";
|
|
11715
|
+
if (status === 403) return "read-only";
|
|
11716
|
+
if (status === 404) return "no-access";
|
|
11717
|
+
return "unexpected";
|
|
11718
|
+
}
|
|
11719
|
+
function createGitHubApi(options) {
|
|
11720
|
+
const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
11721
|
+
const baseUrl = (options.baseUrl ?? "https://api.github.com").replace(/\/+$/, "");
|
|
11722
|
+
const sleep = options.sleep ?? realSleep;
|
|
11723
|
+
const now = options.now ?? Date.now;
|
|
11724
|
+
let lastWrite = Number.NEGATIVE_INFINITY;
|
|
11725
|
+
let writeQueue = Promise.resolve();
|
|
11726
|
+
async function send(method, path, body) {
|
|
11727
|
+
try {
|
|
11728
|
+
return await doFetch(`${baseUrl}${path}`, {
|
|
11729
|
+
method,
|
|
11730
|
+
headers: {
|
|
11731
|
+
Accept: "application/vnd.github+json",
|
|
11732
|
+
Authorization: `Bearer ${options.token}`,
|
|
11733
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
11734
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
11735
|
+
},
|
|
11736
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
11737
|
+
credentials: "omit"
|
|
11738
|
+
});
|
|
11739
|
+
} catch (error) {
|
|
11740
|
+
throw new GitHubError("offline", `GitHub could not be reached: ${error instanceof Error ? error.message : String(error)}`);
|
|
11741
|
+
}
|
|
11742
|
+
}
|
|
11743
|
+
async function attempt(method, path, body, write) {
|
|
11744
|
+
let rateLimited = 0;
|
|
11745
|
+
let failed = 0;
|
|
11746
|
+
for (;;) {
|
|
11747
|
+
if (write) {
|
|
11748
|
+
const wait = lastWrite + WRITE_SPACING_MS - now();
|
|
11749
|
+
if (wait > 0) await sleep(wait);
|
|
11750
|
+
lastWrite = now();
|
|
11751
|
+
}
|
|
11752
|
+
const response = await send(method, path, body);
|
|
11753
|
+
if (response.ok) {
|
|
11754
|
+
const text = await response.text();
|
|
11755
|
+
return text === "" ? null : JSON.parse(text);
|
|
11756
|
+
}
|
|
11757
|
+
const message = await response.json().then((json) => typeof json?.message === "string" ? json.message : "").catch(() => "");
|
|
11758
|
+
if (isRateLimit(response.status, response.headers, message)) {
|
|
11759
|
+
const wait = waitFor(response.headers, rateLimited, now());
|
|
11760
|
+
if (rateLimited >= 4) throw new GitHubError("rate-limited", `GitHub asked for a wait: ${message}`, response.status, { retryAt: now() + wait });
|
|
11761
|
+
rateLimited++;
|
|
11762
|
+
options.onWait?.(now() + wait);
|
|
11763
|
+
await sleep(wait);
|
|
11764
|
+
continue;
|
|
11765
|
+
}
|
|
11766
|
+
if (response.status >= 500 && failed < SERVER_ERROR_RETRIES) {
|
|
11767
|
+
failed++;
|
|
11768
|
+
await sleep(2e3 * failed);
|
|
11769
|
+
continue;
|
|
11770
|
+
}
|
|
11771
|
+
throw new GitHubError(refusalCode(response.status, message), message || `${response.status} ${response.statusText}`, response.status);
|
|
11772
|
+
}
|
|
11773
|
+
}
|
|
11774
|
+
return { request(method, path, body) {
|
|
11775
|
+
if (method === "GET") return attempt(method, path, body, false);
|
|
11776
|
+
const run = writeQueue.then(() => attempt(method, path, body, true));
|
|
11777
|
+
writeQueue = run.catch(() => void 0);
|
|
11778
|
+
return run;
|
|
11779
|
+
} };
|
|
11780
|
+
}
|
|
11781
|
+
/** `/repos/{owner}/{name}`, each part escaped. */
|
|
11782
|
+
function repoPath(repo) {
|
|
11783
|
+
return `/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}`;
|
|
11784
|
+
}
|
|
11785
|
+
/** A branch name in a ref path: a branch may hold `/`, which stays a separator. */
|
|
11786
|
+
function branchPath(branch) {
|
|
11787
|
+
return branch.split("/").map(encodeURIComponent).join("/");
|
|
11788
|
+
}
|
|
11789
|
+
/** Standard base64, as GitHub's blob and contents endpoints take and give it. */
|
|
11790
|
+
function bytesToBase64(bytes) {
|
|
11791
|
+
let binary = "";
|
|
11792
|
+
for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
11793
|
+
return btoa(binary);
|
|
11794
|
+
}
|
|
11795
|
+
/** GitHub's base64 wraps lines; the line breaks are not part of it. */
|
|
11796
|
+
function base64ToBytes(text) {
|
|
11797
|
+
const binary = atob(text.replace(/\s+/g, ""));
|
|
11798
|
+
const bytes = new Uint8Array(binary.length);
|
|
11799
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
11800
|
+
return bytes;
|
|
11801
|
+
}
|
|
11802
|
+
//#endregion
|
|
11803
|
+
//#region ../client/src/lib/document/publish/host/github-repository.ts
|
|
11804
|
+
/**
|
|
11805
|
+
* A [[Publish Repository]] before any publish: reading what a person typed as a repository,
|
|
11806
|
+
* checking that their GitHub token can reach it, and starting the branch when they ask (ADR 0152).
|
|
11807
|
+
*
|
|
11808
|
+
* The check runs when the repository is saved in the Publish tab and before the first publish to
|
|
11809
|
+
* one that was never checked, rather than behind a button of its own. Write access is proven by
|
|
11810
|
+
* writing: a fine-grained token's own permissions are not in any answer GitHub gives (a
|
|
11811
|
+
* repository's `permissions` are the person's role), so the check uploads a tiny blob that no
|
|
11812
|
+
* commit refers to, which GitHub discards.
|
|
11813
|
+
*
|
|
11814
|
+
* A publish never creates a branch: a branch name mistyped would publish where no host looks.
|
|
11815
|
+
* The person asks for it (`createBranch`), after the check has said the branch is not there.
|
|
11816
|
+
*/
|
|
11817
|
+
var OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
11818
|
+
var NAME = /^[A-Za-z0-9._-]{1,100}$/;
|
|
11819
|
+
/**
|
|
11820
|
+
* `owner/name` from what a person typed or pasted: `owner/name`, a github.com address, a clone
|
|
11821
|
+
* url or an ssh address. Null when it is none of those.
|
|
11822
|
+
*/
|
|
11823
|
+
function parseRepository(input) {
|
|
11824
|
+
const parts = input.trim().replace(/^git@github\.com:/i, "").replace(/^(?:https?:\/\/)?(?:www\.)?github\.com\//i, "").replace(/\/+$/, "").replace(/\.git$/i, "").split("/");
|
|
11825
|
+
if (parts.length !== 2) return null;
|
|
11826
|
+
const [owner, name] = parts;
|
|
11827
|
+
if (!OWNER.test(owner) || !NAME.test(name) || name === "." || name === "..") return null;
|
|
11828
|
+
return {
|
|
11829
|
+
owner,
|
|
11830
|
+
name
|
|
11831
|
+
};
|
|
11832
|
+
}
|
|
11833
|
+
/** Whether git takes `name` as a branch name (the rules of `git check-ref-format` a person can break by typing). */
|
|
11834
|
+
function isBranchName(name) {
|
|
11835
|
+
if (name === "" || name.startsWith("/") || name.endsWith("/") || name.endsWith(".") || name.endsWith(".lock")) return false;
|
|
11836
|
+
if (name.includes("..") || name.includes("//") || name.includes("@{")) return false;
|
|
11837
|
+
if ([...name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127)) return false;
|
|
11838
|
+
return !/[\s~^:?*[\\]/.test(name);
|
|
11839
|
+
}
|
|
11840
|
+
function megabytes(bytes) {
|
|
11841
|
+
return `${Math.round(bytes / 1048576)} MB`;
|
|
11842
|
+
}
|
|
11843
|
+
function clock(ms) {
|
|
11844
|
+
return new Date(ms).toLocaleTimeString([], {
|
|
11845
|
+
hour: "2-digit",
|
|
11846
|
+
minute: "2-digit"
|
|
11847
|
+
});
|
|
11848
|
+
}
|
|
11849
|
+
/**
|
|
11850
|
+
* An error from a check or a publish, worded for the person: what happened, why, and what to do.
|
|
11851
|
+
* `replaceToken` says where a new token goes: the Publish tab's GitHub section by default, an
|
|
11852
|
+
* environment variable at the Headless Client's command line.
|
|
11853
|
+
*/
|
|
11854
|
+
function describeGitHubError(error, repo, options = {}) {
|
|
11855
|
+
if (!(error instanceof GitHubError)) return error instanceof Error ? error.message : String(error);
|
|
11856
|
+
const where = `${repo.owner}/${repo.name}`;
|
|
11857
|
+
switch (error.code) {
|
|
11858
|
+
case "offline": return "GitHub could not be reached. Check the connection and try again.";
|
|
11859
|
+
case "bad-token": return `GitHub refused the token. It has expired, been revoked or was mistyped. ${options.replaceToken ?? "Make a new one and replace it under GitHub above."}`;
|
|
11860
|
+
case "no-access": return `GitHub found no repository ${where} for this token. Check the name, and that the token's Repository access includes it.`;
|
|
11861
|
+
case "read-only": return `The token can read ${where} but not write to it. On GitHub, give the token the Contents permission with Read and write.`;
|
|
11862
|
+
case "branch-missing": return `${where} has no branch "${repo.branch}". Check the name, or create the branch.`;
|
|
11863
|
+
case "empty-repository": return `${where} is empty. Create the branch to make its first commit.`;
|
|
11864
|
+
case "branch-moved": return `The branch "${repo.branch}" changed twice while EtherPK was publishing to it. Publish again.`;
|
|
11865
|
+
case "branch-protected": return `A protection rule on "${repo.branch}" refused the commit, as it asks for a pull request or a signed commit. Publish to a branch of its own, such as gh-pages, and serve the site from that branch.`;
|
|
11866
|
+
case "rate-limited": return `GitHub asked EtherPK to wait for longer than it will. Nothing was committed. Publish again after ${clock(error.detail?.retryAt ?? Date.now())}.`;
|
|
11867
|
+
case "too-large": return `${error.detail?.path ?? "A file"} is ${megabytes(error.detail?.size ?? 0)}, and GitHub takes files up to 100 MB. Nothing was committed. Make the file smaller or link to it from elsewhere.`;
|
|
11868
|
+
case "truncated": return `"${repo.branch}" holds more files than GitHub lists at once, so EtherPK cannot see which old files to remove. Publish to a branch of its own.`;
|
|
11869
|
+
case "unexpected": return `GitHub refused the request${error.status ? ` (${error.status})` : ""}: ${error.message}`;
|
|
11870
|
+
}
|
|
11871
|
+
}
|
|
11872
|
+
//#endregion
|
|
11873
|
+
//#region ../client/src/lib/document/publish/seeded.ts
|
|
11874
|
+
var PAGES_WORKFLOW_PATH = ".github/workflows/pages.yaml";
|
|
11875
|
+
var WRANGLER_PATH = "wrangler.jsonc";
|
|
11876
|
+
var ASSETS_IGNORE_PATH = ".assetsignore";
|
|
11877
|
+
var README_PATH = "README.md";
|
|
11878
|
+
var GITIGNORE_PATH = ".gitignore";
|
|
11879
|
+
/**
|
|
11880
|
+
* Files written when absent and left alone afterwards. A Publish Repository gets no Pages
|
|
11881
|
+
* workflow: GitHub refuses a commit that adds one unless the token may also change workflows,
|
|
11882
|
+
* and Pages serves a branch as it is without one ("Deploy from a branch"), so its README says
|
|
11883
|
+
* that instead.
|
|
11884
|
+
*/
|
|
11885
|
+
function seededFiles(publication, destination = "folder") {
|
|
11886
|
+
const files = /* @__PURE__ */ new Map([
|
|
11887
|
+
[PAGES_WORKFLOW_PATH, `# Publishes this folder to GitHub Pages as it is: the site is already built by EtherPK,
|
|
11888
|
+
# so there is no build step. In the repository's Settings → Pages, set the source to
|
|
11889
|
+
# "GitHub Actions" once - every push to main then deploys.
|
|
11890
|
+
name: Deploy to GitHub Pages
|
|
11891
|
+
on:
|
|
11892
|
+
push:
|
|
11893
|
+
branches: [main]
|
|
11894
|
+
workflow_dispatch:
|
|
11895
|
+
permissions:
|
|
11896
|
+
contents: read
|
|
11897
|
+
pages: write
|
|
11898
|
+
id-token: write
|
|
11899
|
+
concurrency:
|
|
11900
|
+
group: pages
|
|
11901
|
+
cancel-in-progress: false
|
|
11902
|
+
jobs:
|
|
11903
|
+
deploy:
|
|
11904
|
+
runs-on: ubuntu-latest
|
|
11905
|
+
environment:
|
|
11906
|
+
name: github-pages
|
|
11907
|
+
url: \${{ steps.deployment.outputs.page_url }}
|
|
11908
|
+
steps:
|
|
11909
|
+
- uses: actions/checkout@v4
|
|
11910
|
+
- uses: actions/configure-pages@v5
|
|
11911
|
+
- uses: actions/upload-pages-artifact@v3
|
|
11912
|
+
with:
|
|
11913
|
+
path: .
|
|
11914
|
+
- id: deployment
|
|
11915
|
+
uses: actions/deploy-pages@v4
|
|
11916
|
+
`],
|
|
11917
|
+
[WRANGLER_PATH, `// Cloudflare Workers with static assets (Cloudflare's successor to Pages for new sites).
|
|
11918
|
+
// Deploy with \`npx wrangler deploy\`, or connect the repository in the dashboard under
|
|
11919
|
+
// Workers → Create → Import a repository and leave the build command empty. The Worker is
|
|
11920
|
+
// named after the publication - Cloudflare warns when this name and the Worker's differ.
|
|
11921
|
+
{
|
|
11922
|
+
"name": "${publication.id}",
|
|
11923
|
+
"compatibility_date": "2026-01-01",
|
|
11924
|
+
"assets": {
|
|
11925
|
+
"directory": ".",
|
|
11926
|
+
"html_handling": "auto-trailing-slash",
|
|
11927
|
+
"not_found_handling": "404-page"
|
|
11928
|
+
}
|
|
11929
|
+
}
|
|
11930
|
+
`],
|
|
11931
|
+
[README_PATH, `# ${publication.name}
|
|
11932
|
+
|
|
11933
|
+
This folder is a website published by [EtherPK](https://etherpk.com) from the publication
|
|
11934
|
+
\`${publication.id}\`. Every file in it except \`CNAME\` and \`robots.txt\` is regenerated on each
|
|
11935
|
+
publish, so edit the notes, the publication page, its include pages and its theme in EtherPK,
|
|
11936
|
+
not here.
|
|
11937
|
+
|
|
11938
|
+
- Preview: open \`index.html\` in a browser - search works from disk too.
|
|
11939
|
+
${destination === "repository" ? `- GitHub Pages: in the repository's Settings → Pages, set Source to "Deploy from a branch" and
|
|
11940
|
+
choose this branch and \`/ (root)\`. Every publish then deploys.` : `- GitHub Pages: push this folder to a repository and set Settings → Pages → Source to
|
|
11941
|
+
"GitHub Actions" - \`.github/workflows/pages.yaml\` deploys every push to \`main\`.`}
|
|
11942
|
+
- Cloudflare: \`npx wrangler deploy\` uses \`wrangler.jsonc\`, or connect the repository in the
|
|
11943
|
+
Workers dashboard with no build command.
|
|
11944
|
+
- Netlify, GitLab Pages, Vercel, S3 or your own server: the folder is plain files, so any of
|
|
11945
|
+
them serves it as it is. Step by step for each: https://docs.etherpk.com/hosting-a-published-site
|
|
11946
|
+
`],
|
|
11947
|
+
[GITIGNORE_PATH, `.DS_Store\nThumbs.db\n`],
|
|
11948
|
+
[ASSETS_IGNORE_PATH, `# Files in this folder that are not part of the website, kept out of the Worker's assets.
|
|
11949
|
+
# Written once by EtherPK - yours to edit.
|
|
11950
|
+
.git
|
|
11951
|
+
.github
|
|
11952
|
+
.gitignore
|
|
11953
|
+
.assetsignore
|
|
11954
|
+
.nojekyll
|
|
11955
|
+
wrangler.jsonc
|
|
11956
|
+
AGENTS.md
|
|
11957
|
+
README.md
|
|
11958
|
+
`]
|
|
11959
|
+
]);
|
|
11960
|
+
if (destination === "repository") files.delete(PAGES_WORKFLOW_PATH);
|
|
11961
|
+
return files;
|
|
11962
|
+
}
|
|
11963
|
+
var AGENTS_START = "<!-- etherpk:start -->";
|
|
11964
|
+
var AGENTS_END = "<!-- etherpk:end -->";
|
|
11965
|
+
function managedSection(publication) {
|
|
11966
|
+
return `${AGENTS_START}
|
|
11967
|
+
## EtherPK published site
|
|
11968
|
+
|
|
11969
|
+
This folder is a website generated by [EtherPK](https://etherpk.com) from the publication
|
|
11970
|
+
\`${publication.id}\` ("${publication.name}"). It is **output**: every file except \`CNAME\`,
|
|
11971
|
+
\`robots.txt\` and the text outside this section of \`AGENTS.md\` is rewritten on each publish
|
|
11972
|
+
and files that are no longer produced are deleted.
|
|
11973
|
+
|
|
11974
|
+
- Do not edit the HTML, the assets or \`theme/\` here - the change is lost on the next publish.
|
|
11975
|
+
- Content changes are made to the documents in EtherPK. A document is published when its
|
|
11976
|
+
frontmatter says \`public: true\` and names this publication in \`publications: [${publication.id}]\`
|
|
11977
|
+
(unless the publication takes every public document).
|
|
11978
|
+
- The navigation is the outline of the publication page in EtherPK - the theme and the include
|
|
11979
|
+
slots (head, favicon, logo, header, footer, before-content, after-content, scripts, styles) are chosen
|
|
11980
|
+
and edited there too, in the Publish settings and the Theme editor.
|
|
11981
|
+
- EtherPK publishes this site into a folder, or straight to its repository on GitHub, from any
|
|
11982
|
+
device. A change made here to a file it writes is lost on the next publish, wherever that runs.
|
|
11983
|
+
- \`index.html\` opens from disk for a preview. On GitHub Pages the site deploys through
|
|
11984
|
+
\`.github/workflows/pages.yaml\` where there is one, or with Pages set to "Deploy from a branch".
|
|
11985
|
+
\`wrangler.jsonc\` deploys it to Cloudflare Workers, and any static host serves it as it is
|
|
11986
|
+
(https://docs.etherpk.com/hosting-a-published-site).
|
|
11987
|
+
|
|
11988
|
+
This section is rewritten by EtherPK on every publish. Put your own notes above or below it.
|
|
11989
|
+
${AGENTS_END}
|
|
11990
|
+
`;
|
|
11991
|
+
}
|
|
11992
|
+
/**
|
|
11993
|
+
* The publication whose site a folder's AGENTS.md says the folder holds, or null when it carries
|
|
11994
|
+
* no published-site section. A first publish to a [[Publish Repository]] asks before it replaces
|
|
11995
|
+
* files on a branch that does not say it holds this publication's site.
|
|
11996
|
+
*/
|
|
11997
|
+
function publishedPublicationOf(agentsMd) {
|
|
11998
|
+
if (agentsMd === null) return null;
|
|
11999
|
+
const start = agentsMd.indexOf(AGENTS_START);
|
|
12000
|
+
const end = agentsMd.indexOf(AGENTS_END);
|
|
12001
|
+
if (start === -1 || end <= start) return null;
|
|
12002
|
+
const section = agentsMd.slice(start, end);
|
|
12003
|
+
if (!section.includes("## EtherPK published site")) return null;
|
|
12004
|
+
return /from the publication\s+`([a-z0-9-]+)`/.exec(section)?.[1] ?? null;
|
|
12005
|
+
}
|
|
12006
|
+
/** The folder's AGENTS.md with the managed section replaced or added, the user's text kept. */
|
|
12007
|
+
function agentsMdWithManagedSection(existing, publication) {
|
|
12008
|
+
const section = managedSection(publication);
|
|
12009
|
+
if (existing === null || existing.trim() === "") return section;
|
|
12010
|
+
const start = existing.indexOf(AGENTS_START);
|
|
12011
|
+
const end = existing.indexOf(AGENTS_END);
|
|
12012
|
+
if (start !== -1 && end !== -1 && end > start) return existing.slice(0, start) + section.trimEnd() + existing.slice(end + 20);
|
|
12013
|
+
return `${existing.trimEnd()}\n\n${section}`;
|
|
12014
|
+
}
|
|
12015
|
+
//#endregion
|
|
12016
|
+
//#region ../client/src/lib/document/publish/host/git-blob-sha.ts
|
|
12017
|
+
/**
|
|
12018
|
+
* The name git gives a file's content: the SHA-1 of `blob <byte length>\0` followed by the bytes,
|
|
12019
|
+
* as `git hash-object` prints it. A [[Publish Repository]] compares it with the names in the
|
|
12020
|
+
* branch's tree to find what a publish changed without downloading a single file, and an
|
|
12021
|
+
* unchanged image keeps its name, so a later publish never sends it again.
|
|
12022
|
+
*
|
|
12023
|
+
* WebCrypto, which the browser and Node both have, so the Client and the Headless Client share it.
|
|
12024
|
+
* SHA-1 here is git's object name, not a security measure.
|
|
12025
|
+
*/
|
|
12026
|
+
var encoder$1 = new TextEncoder();
|
|
12027
|
+
async function gitBlobSha(content) {
|
|
12028
|
+
const bytes = typeof content === "string" ? encoder$1.encode(content) : content;
|
|
12029
|
+
const header = encoder$1.encode(`blob ${bytes.byteLength}\0`);
|
|
12030
|
+
const object = new Uint8Array(header.byteLength + bytes.byteLength);
|
|
12031
|
+
object.set(header, 0);
|
|
12032
|
+
object.set(bytes, header.byteLength);
|
|
12033
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-1", object));
|
|
12034
|
+
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
12035
|
+
}
|
|
12036
|
+
/** Tree entries sent in one request, and inline text in one request, before the next batch starts. */
|
|
12037
|
+
var TREE_BATCH_ENTRIES = 300;
|
|
12038
|
+
var TREE_BATCH_BYTES = 4194304;
|
|
12039
|
+
var decoder = new TextDecoder();
|
|
12040
|
+
var encoder = new TextEncoder();
|
|
12041
|
+
/** Read the branch: its head commit and every file in its tree. */
|
|
12042
|
+
async function openRepositoryBranch(api, repo) {
|
|
12043
|
+
const base = repoPath(repo);
|
|
12044
|
+
let head;
|
|
12045
|
+
try {
|
|
12046
|
+
head = (await api.request("GET", `${base}/git/ref/heads/${branchPath(repo.branch)}`)).object.sha;
|
|
12047
|
+
} catch (error) {
|
|
12048
|
+
if (error instanceof GitHubError && error.code === "no-access") {
|
|
12049
|
+
await api.request("GET", base);
|
|
12050
|
+
throw new GitHubError("branch-missing", error.message, error.status);
|
|
12051
|
+
}
|
|
12052
|
+
throw error;
|
|
12053
|
+
}
|
|
12054
|
+
const treeSha = (await api.request("GET", `${base}/git/commits/${head}`)).tree.sha;
|
|
12055
|
+
const listing = await api.request("GET", `${base}/git/trees/${treeSha}?recursive=1`);
|
|
12056
|
+
if (listing.truncated) throw new GitHubError("truncated", "GitHub listed the branch only in part.");
|
|
12057
|
+
const tree = /* @__PURE__ */ new Map();
|
|
12058
|
+
for (const entry of listing.tree) if (entry.type === "blob") tree.set(entry.path, {
|
|
12059
|
+
mode: entry.mode,
|
|
12060
|
+
sha: entry.sha
|
|
12061
|
+
});
|
|
12062
|
+
/** Path → new content, or null for a removal. */
|
|
12063
|
+
const staged = /* @__PURE__ */ new Map();
|
|
12064
|
+
const downloaded = /* @__PURE__ */ new Map();
|
|
12065
|
+
async function bytesAt(path) {
|
|
12066
|
+
const change = staged.get(path);
|
|
12067
|
+
if (change !== void 0) return change === null ? null : typeof change === "string" ? encoder.encode(change) : change;
|
|
12068
|
+
const entry = tree.get(path);
|
|
12069
|
+
if (!entry) return null;
|
|
12070
|
+
let bytes = downloaded.get(entry.sha);
|
|
12071
|
+
if (!bytes) {
|
|
12072
|
+
bytes = base64ToBytes((await api.request("GET", `${base}/git/blobs/${entry.sha}`)).content);
|
|
12073
|
+
downloaded.set(entry.sha, bytes);
|
|
12074
|
+
}
|
|
12075
|
+
return bytes;
|
|
12076
|
+
}
|
|
12077
|
+
return {
|
|
12078
|
+
head,
|
|
12079
|
+
async readText(path) {
|
|
12080
|
+
const bytes = await bytesAt(path);
|
|
12081
|
+
return bytes === null ? null : decoder.decode(bytes);
|
|
12082
|
+
},
|
|
12083
|
+
readBytes: bytesAt,
|
|
12084
|
+
async sameContent(path, content) {
|
|
12085
|
+
const entry = tree.get(path);
|
|
12086
|
+
return entry !== void 0 && entry.sha === await gitBlobSha(content);
|
|
12087
|
+
},
|
|
12088
|
+
async writeFile(path, content) {
|
|
12089
|
+
staged.set(path, content);
|
|
12090
|
+
},
|
|
12091
|
+
async remove(path) {
|
|
12092
|
+
if (tree.has(path)) staged.set(path, null);
|
|
12093
|
+
},
|
|
12094
|
+
async listFiles() {
|
|
12095
|
+
return [...tree.keys()];
|
|
12096
|
+
},
|
|
12097
|
+
replaced() {
|
|
12098
|
+
return [...staged.keys()].filter((path) => tree.has(path) && path !== "AGENTS.md").sort();
|
|
12099
|
+
},
|
|
12100
|
+
pendingChanges() {
|
|
12101
|
+
return staged.size;
|
|
12102
|
+
},
|
|
12103
|
+
async commit(message, options = {}) {
|
|
12104
|
+
if (staged.size === 0) return null;
|
|
12105
|
+
const report = options.onProgress ?? (() => {});
|
|
12106
|
+
const maxFileBytes = options.maxFileBytes ?? 104857600;
|
|
12107
|
+
for (const [path, content] of staged) {
|
|
12108
|
+
if (content === null) continue;
|
|
12109
|
+
const size = typeof content === "string" ? encoder.encode(content).byteLength : content.byteLength;
|
|
12110
|
+
if (size > maxFileBytes) throw new GitHubError("too-large", `${path} is over GitHub's file size limit.`, void 0, {
|
|
12111
|
+
path,
|
|
12112
|
+
size
|
|
12113
|
+
});
|
|
12114
|
+
}
|
|
12115
|
+
const known = new Set([...tree.values()].map((entry) => entry.sha));
|
|
12116
|
+
const binaries = [...staged].filter((change) => change[1] instanceof Uint8Array);
|
|
12117
|
+
const blobShas = /* @__PURE__ */ new Map();
|
|
12118
|
+
report("uploading", 0, binaries.length);
|
|
12119
|
+
for (const [index, [path, bytes]] of binaries.entries()) {
|
|
12120
|
+
let sha = await gitBlobSha(bytes);
|
|
12121
|
+
if (!known.has(sha)) {
|
|
12122
|
+
sha = (await api.request("POST", `${base}/git/blobs`, {
|
|
12123
|
+
content: bytesToBase64(bytes),
|
|
12124
|
+
encoding: "base64"
|
|
12125
|
+
})).sha;
|
|
12126
|
+
known.add(sha);
|
|
12127
|
+
}
|
|
12128
|
+
blobShas.set(path, sha);
|
|
12129
|
+
report("uploading", index + 1, binaries.length);
|
|
12130
|
+
}
|
|
12131
|
+
const batches = batch([...staged].map(([path, content]) => {
|
|
12132
|
+
const mode = tree.get(path)?.mode ?? "100644";
|
|
12133
|
+
if (content === null) return {
|
|
12134
|
+
path,
|
|
12135
|
+
mode,
|
|
12136
|
+
type: "blob",
|
|
12137
|
+
sha: null
|
|
12138
|
+
};
|
|
12139
|
+
if (typeof content === "string") return {
|
|
12140
|
+
path,
|
|
12141
|
+
mode,
|
|
12142
|
+
type: "blob",
|
|
12143
|
+
content
|
|
12144
|
+
};
|
|
12145
|
+
return {
|
|
12146
|
+
path,
|
|
12147
|
+
mode,
|
|
12148
|
+
type: "blob",
|
|
12149
|
+
sha: blobShas.get(path)
|
|
12150
|
+
};
|
|
12151
|
+
}), options.treeBatch ?? TREE_BATCH_ENTRIES);
|
|
12152
|
+
let root = treeSha;
|
|
12153
|
+
for (const [index, entriesOfBatch] of batches.entries()) {
|
|
12154
|
+
report("committing", index, batches.length + 2);
|
|
12155
|
+
root = (await api.request("POST", `${base}/git/trees`, {
|
|
12156
|
+
base_tree: root,
|
|
12157
|
+
tree: entriesOfBatch
|
|
12158
|
+
})).sha;
|
|
12159
|
+
}
|
|
12160
|
+
report("committing", batches.length, batches.length + 2);
|
|
12161
|
+
const commit = await api.request("POST", `${base}/git/commits`, {
|
|
12162
|
+
message,
|
|
12163
|
+
tree: root,
|
|
12164
|
+
parents: [head]
|
|
12165
|
+
});
|
|
12166
|
+
report("committing", batches.length + 1, batches.length + 2);
|
|
12167
|
+
await api.request("PATCH", `${base}/git/refs/heads/${branchPath(repo.branch)}`, {
|
|
12168
|
+
sha: commit.sha,
|
|
12169
|
+
force: false
|
|
12170
|
+
});
|
|
12171
|
+
report("committing", batches.length + 2, batches.length + 2);
|
|
12172
|
+
return {
|
|
12173
|
+
sha: commit.sha,
|
|
12174
|
+
url: commit.html_url
|
|
12175
|
+
};
|
|
12176
|
+
}
|
|
12177
|
+
};
|
|
12178
|
+
}
|
|
12179
|
+
/** Tree entries in batches small enough for one request: by count, and by the text they carry. */
|
|
12180
|
+
function batch(entries, size) {
|
|
12181
|
+
const out = [];
|
|
12182
|
+
let current = [];
|
|
12183
|
+
let bytes = 0;
|
|
12184
|
+
for (const entry of entries) {
|
|
12185
|
+
const weight = entry.content?.length ?? 0;
|
|
12186
|
+
if (current.length > 0 && (current.length >= size || bytes + weight > TREE_BATCH_BYTES)) {
|
|
12187
|
+
out.push(current);
|
|
12188
|
+
current = [];
|
|
12189
|
+
bytes = 0;
|
|
12190
|
+
}
|
|
12191
|
+
current.push(entry);
|
|
12192
|
+
bytes += weight;
|
|
12193
|
+
}
|
|
12194
|
+
if (current.length > 0) out.push(current);
|
|
12195
|
+
return out;
|
|
12196
|
+
}
|
|
12197
|
+
/** The graph's own AGENTS.md section (`storage/fs/agents-md-section.md`). */
|
|
12198
|
+
var GRAPH_AGENTS_HEADING = /^## EtherPK knowledge graph\s*$/m;
|
|
12199
|
+
/** True when the branch looks like a [[Graph Folder]], which a publish would gut. */
|
|
12200
|
+
async function holdsGraph(branch) {
|
|
12201
|
+
const files = await branch.listFiles();
|
|
12202
|
+
if (files.some((path) => path.startsWith("pages/")) && files.some((path) => path.startsWith("journals/"))) return true;
|
|
12203
|
+
const agents = files.includes("AGENTS.md") ? await branch.readText("AGENTS.md") : null;
|
|
12204
|
+
return agents !== null && GRAPH_AGENTS_HEADING.test(agents);
|
|
12205
|
+
}
|
|
12206
|
+
/** Publish a rendered site to the branch as one commit, under the rules at the top of this module. */
|
|
12207
|
+
async function publishToRepository(api, repo, input, options = {}) {
|
|
12208
|
+
for (let attempt = 0;; attempt++) {
|
|
12209
|
+
options.onProgress?.("reading", 0, 0);
|
|
12210
|
+
const branch = await openRepositoryBranch(api, repo);
|
|
12211
|
+
if (await holdsGraph(branch)) throw new Error(`${repo.owner}/${repo.name} holds a knowledge graph on "${repo.branch}", and a publish there would delete its files. Choose a repository or a branch of its own for the site.`);
|
|
12212
|
+
const publishedHere = publishedPublicationOf(await branch.readText("AGENTS.md")) === input.publicationId;
|
|
12213
|
+
const written = await writeSite(branch, input.bundle, {
|
|
12214
|
+
seeded: input.seeded,
|
|
12215
|
+
agentsMd: input.agentsMd
|
|
12216
|
+
});
|
|
12217
|
+
if (!options.confirmed && !publishedHere) {
|
|
12218
|
+
const replaced = branch.replaced();
|
|
12219
|
+
if (replaced.length > 0) return {
|
|
12220
|
+
state: "needs-confirmation",
|
|
12221
|
+
replaced
|
|
12222
|
+
};
|
|
12223
|
+
}
|
|
12224
|
+
if (branch.pendingChanges() === 0) return {
|
|
12225
|
+
state: "unchanged",
|
|
12226
|
+
written
|
|
12227
|
+
};
|
|
12228
|
+
const message = `Publish ${input.publicationId}: ${written.written} written, ${written.deleted.length} removed`;
|
|
12229
|
+
try {
|
|
12230
|
+
return {
|
|
12231
|
+
state: "committed",
|
|
12232
|
+
written,
|
|
12233
|
+
commit: await branch.commit(message, options),
|
|
12234
|
+
retried: attempt > 0
|
|
12235
|
+
};
|
|
12236
|
+
} catch (error) {
|
|
12237
|
+
if (error instanceof GitHubError && error.code === "branch-moved" && attempt === 0) continue;
|
|
12238
|
+
throw error;
|
|
12239
|
+
}
|
|
12240
|
+
}
|
|
12241
|
+
}
|
|
11675
12242
|
//#endregion
|
|
11676
12243
|
//#region ../../packages/themes/src/index.ts
|
|
11677
12244
|
var files = /* #__PURE__ */ Object.assign({
|
|
@@ -12046,124 +12613,6 @@ function withPublishing(text, patch, options = {}) {
|
|
|
12046
12613
|
}, options);
|
|
12047
12614
|
}
|
|
12048
12615
|
//#endregion
|
|
12049
|
-
//#region ../client/src/lib/document/publish/seeded.ts
|
|
12050
|
-
var PAGES_WORKFLOW_PATH = ".github/workflows/pages.yaml";
|
|
12051
|
-
var WRANGLER_PATH = "wrangler.jsonc";
|
|
12052
|
-
var ASSETS_IGNORE_PATH = ".assetsignore";
|
|
12053
|
-
var README_PATH = "README.md";
|
|
12054
|
-
var GITIGNORE_PATH = ".gitignore";
|
|
12055
|
-
/** Files written when absent and left alone afterwards. */
|
|
12056
|
-
function seededFiles(publication) {
|
|
12057
|
-
return /* @__PURE__ */ new Map([
|
|
12058
|
-
[PAGES_WORKFLOW_PATH, `# Publishes this folder to GitHub Pages as it is: the site is already built by EtherPK,
|
|
12059
|
-
# so there is no build step. In the repository's Settings → Pages, set the source to
|
|
12060
|
-
# "GitHub Actions" once - every push to main then deploys.
|
|
12061
|
-
name: Deploy to GitHub Pages
|
|
12062
|
-
on:
|
|
12063
|
-
push:
|
|
12064
|
-
branches: [main]
|
|
12065
|
-
workflow_dispatch:
|
|
12066
|
-
permissions:
|
|
12067
|
-
contents: read
|
|
12068
|
-
pages: write
|
|
12069
|
-
id-token: write
|
|
12070
|
-
concurrency:
|
|
12071
|
-
group: pages
|
|
12072
|
-
cancel-in-progress: false
|
|
12073
|
-
jobs:
|
|
12074
|
-
deploy:
|
|
12075
|
-
runs-on: ubuntu-latest
|
|
12076
|
-
environment:
|
|
12077
|
-
name: github-pages
|
|
12078
|
-
url: \${{ steps.deployment.outputs.page_url }}
|
|
12079
|
-
steps:
|
|
12080
|
-
- uses: actions/checkout@v4
|
|
12081
|
-
- uses: actions/configure-pages@v5
|
|
12082
|
-
- uses: actions/upload-pages-artifact@v3
|
|
12083
|
-
with:
|
|
12084
|
-
path: .
|
|
12085
|
-
- id: deployment
|
|
12086
|
-
uses: actions/deploy-pages@v4
|
|
12087
|
-
`],
|
|
12088
|
-
[WRANGLER_PATH, `// Cloudflare Workers with static assets (Cloudflare's successor to Pages for new sites).
|
|
12089
|
-
// Deploy with \`npx wrangler deploy\`, or connect the repository in the dashboard under
|
|
12090
|
-
// Workers → Create → Import a repository and leave the build command empty. The Worker is
|
|
12091
|
-
// named after the publication - Cloudflare warns when this name and the Worker's differ.
|
|
12092
|
-
{
|
|
12093
|
-
"name": "${publication.id}",
|
|
12094
|
-
"compatibility_date": "2026-01-01",
|
|
12095
|
-
"assets": {
|
|
12096
|
-
"directory": ".",
|
|
12097
|
-
"html_handling": "auto-trailing-slash",
|
|
12098
|
-
"not_found_handling": "404-page"
|
|
12099
|
-
}
|
|
12100
|
-
}
|
|
12101
|
-
`],
|
|
12102
|
-
[README_PATH, `# ${publication.name}
|
|
12103
|
-
|
|
12104
|
-
This folder is a website published by [EtherPK](https://etherpk.com) from the publication
|
|
12105
|
-
\`${publication.id}\`. Every file in it except \`CNAME\` and \`robots.txt\` is regenerated on each
|
|
12106
|
-
publish, so edit the notes, the publication page, its include pages and its theme in EtherPK,
|
|
12107
|
-
not here.
|
|
12108
|
-
|
|
12109
|
-
- Preview: open \`index.html\` in a browser - search works from disk too.
|
|
12110
|
-
- GitHub Pages: push this folder to a repository and set Settings → Pages → Source to
|
|
12111
|
-
"GitHub Actions" - \`.github/workflows/pages.yaml\` deploys every push to \`main\`.
|
|
12112
|
-
- Cloudflare: \`npx wrangler deploy\` uses \`wrangler.jsonc\`, or connect the repository in the
|
|
12113
|
-
Workers dashboard with no build command.
|
|
12114
|
-
- Netlify, GitLab Pages, Vercel, S3 or your own server: the folder is plain files, so any of
|
|
12115
|
-
them serves it as it is. Step by step for each: https://docs.etherpk.com/hosting-a-published-site
|
|
12116
|
-
`],
|
|
12117
|
-
[GITIGNORE_PATH, `.DS_Store\nThumbs.db\n`],
|
|
12118
|
-
[ASSETS_IGNORE_PATH, `# Files in this folder that are not part of the website, kept out of the Worker's assets.
|
|
12119
|
-
# Written once by EtherPK - yours to edit.
|
|
12120
|
-
.git
|
|
12121
|
-
.github
|
|
12122
|
-
.gitignore
|
|
12123
|
-
.assetsignore
|
|
12124
|
-
.nojekyll
|
|
12125
|
-
wrangler.jsonc
|
|
12126
|
-
AGENTS.md
|
|
12127
|
-
README.md
|
|
12128
|
-
`]
|
|
12129
|
-
]);
|
|
12130
|
-
}
|
|
12131
|
-
var AGENTS_START = "<!-- etherpk:start -->";
|
|
12132
|
-
var AGENTS_END = "<!-- etherpk:end -->";
|
|
12133
|
-
function managedSection(publication) {
|
|
12134
|
-
return `${AGENTS_START}
|
|
12135
|
-
## EtherPK published site
|
|
12136
|
-
|
|
12137
|
-
This folder is a website generated by [EtherPK](https://etherpk.com) from the publication
|
|
12138
|
-
\`${publication.id}\` ("${publication.name}"). It is **output**: every file except \`CNAME\`,
|
|
12139
|
-
\`robots.txt\` and the text outside this section of \`AGENTS.md\` is rewritten on each publish
|
|
12140
|
-
and files that are no longer produced are deleted.
|
|
12141
|
-
|
|
12142
|
-
- Do not edit the HTML, the assets or \`theme/\` here - the change is lost on the next publish.
|
|
12143
|
-
- Content changes are made to the documents in EtherPK. A document is published when its
|
|
12144
|
-
frontmatter says \`public: true\` and names this publication in \`publications: [${publication.id}]\`
|
|
12145
|
-
(unless the publication takes every public document).
|
|
12146
|
-
- The navigation is the outline of the publication page in EtherPK - the theme and the include
|
|
12147
|
-
slots (head, favicon, logo, header, footer, before-content, after-content, scripts, styles) are chosen
|
|
12148
|
-
and edited there too, in the Publish settings and the Theme editor.
|
|
12149
|
-
- \`index.html\` opens from disk for a preview. \`.github/workflows/pages.yaml\` deploys the folder
|
|
12150
|
-
to GitHub Pages, \`wrangler.jsonc\` deploys it to Cloudflare Workers, and any static host serves it
|
|
12151
|
-
as it is (https://docs.etherpk.com/hosting-a-published-site).
|
|
12152
|
-
|
|
12153
|
-
This section is rewritten by EtherPK on every publish. Put your own notes above or below it.
|
|
12154
|
-
${AGENTS_END}
|
|
12155
|
-
`;
|
|
12156
|
-
}
|
|
12157
|
-
/** The folder's AGENTS.md with the managed section replaced or added, the user's text kept. */
|
|
12158
|
-
function agentsMdWithManagedSection(existing, publication) {
|
|
12159
|
-
const section = managedSection(publication);
|
|
12160
|
-
if (existing === null || existing.trim() === "") return section;
|
|
12161
|
-
const start = existing.indexOf(AGENTS_START);
|
|
12162
|
-
const end = existing.indexOf(AGENTS_END);
|
|
12163
|
-
if (start !== -1 && end !== -1 && end > start) return existing.slice(0, start) + section.trimEnd() + existing.slice(end + 20);
|
|
12164
|
-
return `${existing.trimEnd()}\n\n${section}`;
|
|
12165
|
-
}
|
|
12166
|
-
//#endregion
|
|
12167
12616
|
//#region ../client/src/lib/document/fence-answer.ts
|
|
12168
12617
|
var PICTURE_TYPES = /* @__PURE__ */ new Set([
|
|
12169
12618
|
"image/svg+xml",
|
|
@@ -14464,7 +14913,7 @@ async function runPublish(publication, deps) {
|
|
|
14464
14913
|
return {
|
|
14465
14914
|
report,
|
|
14466
14915
|
bundle,
|
|
14467
|
-
seeded: seededFiles(publication),
|
|
14916
|
+
seeded: seededFiles(publication, deps.destination),
|
|
14468
14917
|
agentsMd: (existing) => agentsMdWithManagedSection(existing, publication)
|
|
14469
14918
|
};
|
|
14470
14919
|
}
|
|
@@ -14987,14 +15436,35 @@ function nodeSiteFolder(root) {
|
|
|
14987
15436
|
//#endregion
|
|
14988
15437
|
//#region src/publish-folders.ts
|
|
14989
15438
|
/**
|
|
14990
|
-
* Where each [[Publication]]
|
|
14991
|
-
*
|
|
14992
|
-
*
|
|
14993
|
-
*
|
|
14994
|
-
* the
|
|
14995
|
-
*
|
|
14996
|
-
*
|
|
14997
|
-
|
|
15439
|
+
* Where each [[Publication]] publishes from this machine (ADR 0086, ADR 0152): its [[Publish
|
|
15440
|
+
* Folder]], set with `etherpk-mcp publish … --out <dir>`, or its [[Publish Repository]], set with
|
|
15441
|
+
* `--github owner/name[#branch]`. One or the other: setting one forgets the other, as in the
|
|
15442
|
+
* Client. Read by the `publish` tool and by the command run without either. A file of its own
|
|
15443
|
+
* beside the logins (`publish.json`), because it is not a secret and outlives a login (the GitHub
|
|
15444
|
+
* token is never in it: it comes from `ETHERPK_GITHUB_TOKEN`); keyed by the graph the way its
|
|
15445
|
+
* cache directory is keyed - the server's host and graph id for a synced graph, the folder's path
|
|
15446
|
+
* hash for a served folder - so two graphs with a publication both called `blog` never share a
|
|
15447
|
+
* destination. Never written into a graph folder, which is the user's content (ADR 0079).
|
|
15448
|
+
*/
|
|
15449
|
+
/**
|
|
15450
|
+
* `owner/name` or `owner/name#branch` (a github.com address works too), as `--github` takes it.
|
|
15451
|
+
* Null when it is not a repository, or the branch is not a name git takes.
|
|
15452
|
+
*/
|
|
15453
|
+
function parseRepositoryArgument(text) {
|
|
15454
|
+
const [repository, branch, ...rest] = text.trim().split("#");
|
|
15455
|
+
if (rest.length > 0) return null;
|
|
15456
|
+
const parsed = parseRepository(repository);
|
|
15457
|
+
const name = branch?.trim() || "main";
|
|
15458
|
+
if (!parsed || !isBranchName(name)) return null;
|
|
15459
|
+
return {
|
|
15460
|
+
...parsed,
|
|
15461
|
+
branch: name
|
|
15462
|
+
};
|
|
15463
|
+
}
|
|
15464
|
+
/** How a repository is written for a person: `owner/name#branch`. */
|
|
15465
|
+
function repositoryLabel(repo) {
|
|
15466
|
+
return `${repo.owner}/${repo.name}#${repo.branch}`;
|
|
15467
|
+
}
|
|
14998
15468
|
function defaultPublishFoldersPath(env = process.env) {
|
|
14999
15469
|
const override = env.ETHERPK_MCP_PUBLISH_CONFIG?.trim();
|
|
15000
15470
|
if (override) return override;
|
|
@@ -15014,14 +15484,25 @@ function parsePublishFolders(raw) {
|
|
|
15014
15484
|
} catch {
|
|
15015
15485
|
return out;
|
|
15016
15486
|
}
|
|
15487
|
+
const isMap = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15017
15488
|
const folders = parsed?.folders;
|
|
15018
|
-
if (
|
|
15019
|
-
|
|
15020
|
-
if (typeof byPublication !== "object" || byPublication === null || Array.isArray(byPublication)) continue;
|
|
15489
|
+
if (isMap(folders)) for (const [graphKey, byPublication] of Object.entries(folders)) {
|
|
15490
|
+
if (!isMap(byPublication)) continue;
|
|
15021
15491
|
const kept = {};
|
|
15022
15492
|
for (const [publication, path] of Object.entries(byPublication)) if (typeof path === "string" && isAbsolute(path)) kept[publication] = path;
|
|
15023
15493
|
if (Object.keys(kept).length > 0) out.folders[graphKey] = kept;
|
|
15024
15494
|
}
|
|
15495
|
+
const repositories = parsed?.repositories;
|
|
15496
|
+
if (isMap(repositories)) for (const [graphKey, byPublication] of Object.entries(repositories)) {
|
|
15497
|
+
if (!isMap(byPublication)) continue;
|
|
15498
|
+
const kept = {};
|
|
15499
|
+
for (const [publication, repo] of Object.entries(byPublication)) {
|
|
15500
|
+
if (!isMap(repo) || typeof repo.owner !== "string" || typeof repo.name !== "string" || typeof repo.branch !== "string") continue;
|
|
15501
|
+
const checked = parseRepositoryArgument(`${repo.owner}/${repo.name}#${repo.branch}`);
|
|
15502
|
+
if (checked) kept[publication] = checked;
|
|
15503
|
+
}
|
|
15504
|
+
if (Object.keys(kept).length > 0) (out.repositories ??= {})[graphKey] = kept;
|
|
15505
|
+
}
|
|
15025
15506
|
return out;
|
|
15026
15507
|
}
|
|
15027
15508
|
async function readPublishFolders(path) {
|
|
@@ -15043,6 +15524,33 @@ async function writePublishFolders(path, folders) {
|
|
|
15043
15524
|
function publishFolderOf(folders, graphKey, publicationId) {
|
|
15044
15525
|
return folders.folders[graphKey]?.[publicationId] ?? null;
|
|
15045
15526
|
}
|
|
15527
|
+
/** The repository set for a publication on this machine, or null. */
|
|
15528
|
+
function publishRepositoryOf(folders, graphKey, publicationId) {
|
|
15529
|
+
return folders.repositories?.[graphKey]?.[publicationId] ?? null;
|
|
15530
|
+
}
|
|
15531
|
+
/** `publications` with one entry dropped, and the graph's map dropped when it empties. */
|
|
15532
|
+
function without(byGraph, graphKey, publicationId) {
|
|
15533
|
+
const out = { ...byGraph ?? {} };
|
|
15534
|
+
const rest = { ...out[graphKey] ?? {} };
|
|
15535
|
+
delete rest[publicationId];
|
|
15536
|
+
if (Object.keys(rest).length > 0) out[graphKey] = rest;
|
|
15537
|
+
else delete out[graphKey];
|
|
15538
|
+
return out;
|
|
15539
|
+
}
|
|
15540
|
+
/** The same file with one publication's repository set, in place of any folder - the caller writes it. */
|
|
15541
|
+
function withPublishRepository(folders, graphKey, publicationId, repo) {
|
|
15542
|
+
const repositories = folders.repositories ?? {};
|
|
15543
|
+
return {
|
|
15544
|
+
folders: without(folders.folders, graphKey, publicationId),
|
|
15545
|
+
repositories: {
|
|
15546
|
+
...repositories,
|
|
15547
|
+
[graphKey]: {
|
|
15548
|
+
...repositories[graphKey] ?? {},
|
|
15549
|
+
[publicationId]: repo
|
|
15550
|
+
}
|
|
15551
|
+
}
|
|
15552
|
+
};
|
|
15553
|
+
}
|
|
15046
15554
|
/**
|
|
15047
15555
|
* How two absolute folder paths relate, by the platform's own `relative` rather than by splitting
|
|
15048
15556
|
* on `/` (Windows paths use `\`, and a shared prefix such as `notes` and `notes-site` is not a
|
|
@@ -15082,15 +15590,19 @@ async function canonicalFolder(folder) {
|
|
|
15082
15590
|
async function publishFolderOverlap(graphFolder, publishFolder) {
|
|
15083
15591
|
return relateFolders(await canonicalFolder(graphFolder), await canonicalFolder(publishFolder));
|
|
15084
15592
|
}
|
|
15085
|
-
/** The same file with one publication's folder set (made absolute) - the caller writes it. */
|
|
15593
|
+
/** The same file with one publication's folder set (made absolute), in place of any repository - the caller writes it. */
|
|
15086
15594
|
function withPublishFolder(folders, graphKey, publicationId, folder) {
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
...folders.folders
|
|
15091
|
-
[
|
|
15092
|
-
|
|
15093
|
-
|
|
15595
|
+
const repositories = without(folders.repositories, graphKey, publicationId);
|
|
15596
|
+
return {
|
|
15597
|
+
folders: {
|
|
15598
|
+
...folders.folders,
|
|
15599
|
+
[graphKey]: {
|
|
15600
|
+
...folders.folders[graphKey] ?? {},
|
|
15601
|
+
[publicationId]: resolve(folder)
|
|
15602
|
+
}
|
|
15603
|
+
},
|
|
15604
|
+
...Object.keys(repositories).length > 0 ? { repositories } : {}
|
|
15605
|
+
};
|
|
15094
15606
|
}
|
|
15095
15607
|
//#endregion
|
|
15096
15608
|
//#region ../client/src/lib/document/protection/cipher-fence.ts
|
|
@@ -18107,9 +18619,12 @@ async function graphInfo(graph) {
|
|
|
18107
18619
|
* which the Settings tab uses too, so a publication page an agent creates is the one a person
|
|
18108
18620
|
* would have created.
|
|
18109
18621
|
*
|
|
18110
|
-
*
|
|
18622
|
+
* Three things are this host's own. The [[Publish Folder]] is configuration a person set on the
|
|
18111
18623
|
* command line, never a tool argument: the site writer deletes strays from the paths it owns,
|
|
18112
|
-
* and a folder an agent inferred could lose real files to that rule.
|
|
18624
|
+
* and a folder an agent inferred could lose real files to that rule. A [[Publish Repository]] is
|
|
18625
|
+
* set the same way, and only the person's own `etherpk-mcp publish` commits to it, with the
|
|
18626
|
+
* token in `ETHERPK_GITHUB_TOKEN`: a commit there can deploy the site, so the agent's tool checks
|
|
18627
|
+
* such a publication (a dry run) and never publishes it (ADR 0152). And a publish whose pages
|
|
18113
18628
|
* hold Mermaid needs the browser `diagrams setup` installed; without one it refuses rather than
|
|
18114
18629
|
* publishing a site with its diagrams turned to text.
|
|
18115
18630
|
*/
|
|
@@ -18172,8 +18687,11 @@ async function settle$1(graph) {
|
|
|
18172
18687
|
const result = await graph.settle();
|
|
18173
18688
|
if (!result.settled) throw new ToolError("not_settled", result.message);
|
|
18174
18689
|
}
|
|
18175
|
-
/**
|
|
18176
|
-
|
|
18690
|
+
/**
|
|
18691
|
+
* How a publication reads to an agent: its page's settings, and where this machine publishes it.
|
|
18692
|
+
* A repository shows as `owner/name#branch`; it is not the agent's to publish to.
|
|
18693
|
+
*/
|
|
18694
|
+
function publicationView(publication, destination) {
|
|
18177
18695
|
return {
|
|
18178
18696
|
id: publication.id,
|
|
18179
18697
|
name: publication.name,
|
|
@@ -18185,13 +18703,17 @@ function publicationView(publication, folder) {
|
|
|
18185
18703
|
theme: publication.theme,
|
|
18186
18704
|
recent: publication.recent,
|
|
18187
18705
|
includes: publication.includes,
|
|
18188
|
-
publishFolder: folder
|
|
18706
|
+
publishFolder: destination.folder,
|
|
18707
|
+
publishRepository: destination.repository ? repositoryLabel(destination.repository) : null
|
|
18189
18708
|
};
|
|
18190
18709
|
}
|
|
18191
|
-
async function
|
|
18710
|
+
async function destinationsFor(graph, env) {
|
|
18192
18711
|
const folders = await readPublishFolders(defaultPublishFoldersPath(env));
|
|
18193
18712
|
const key = publishGraphKey(graph.backend, graph.graphId);
|
|
18194
|
-
return (publicationId) =>
|
|
18713
|
+
return (publicationId) => ({
|
|
18714
|
+
folder: publishFolderOf(folders, key, publicationId),
|
|
18715
|
+
repository: publishRepositoryOf(folders, key, publicationId)
|
|
18716
|
+
});
|
|
18195
18717
|
}
|
|
18196
18718
|
/** The publications the graph defines, the issues in their pages, and the public documents none takes. */
|
|
18197
18719
|
async function listPublications(graph, host) {
|
|
@@ -18199,9 +18721,9 @@ async function listPublications(graph, host) {
|
|
|
18199
18721
|
await graph.store.refresh();
|
|
18200
18722
|
const { source, unsettled } = await graph.publishing.readSource();
|
|
18201
18723
|
const summary = summarisePublishing(source);
|
|
18202
|
-
const
|
|
18724
|
+
const destinationOf = await destinationsFor(graph, env);
|
|
18203
18725
|
return {
|
|
18204
|
-
publications: summary.publications.map((p) => publicationView(p,
|
|
18726
|
+
publications: summary.publications.map((p) => publicationView(p, destinationOf(p.id))),
|
|
18205
18727
|
issues: summary.issues,
|
|
18206
18728
|
publicInNoPublication: summary.publicInNoPublication,
|
|
18207
18729
|
...unsettled.length > 0 ? { unsettled } : {}
|
|
@@ -18247,10 +18769,10 @@ async function createPublication(graph, args, host) {
|
|
|
18247
18769
|
await settle$1(graph);
|
|
18248
18770
|
}
|
|
18249
18771
|
const after = summarisePublishing((await graph.publishing.readSource()).source).publications.find((p) => p.id === id);
|
|
18250
|
-
const
|
|
18772
|
+
const destinationOf = await destinationsFor(graph, env);
|
|
18251
18773
|
return {
|
|
18252
18774
|
created: true,
|
|
18253
|
-
publication: after ? publicationView(after,
|
|
18775
|
+
publication: after ? publicationView(after, destinationOf(id)) : {
|
|
18254
18776
|
id,
|
|
18255
18777
|
page: concept
|
|
18256
18778
|
}
|
|
@@ -18272,8 +18794,14 @@ async function updatePublication(graph, args, host) {
|
|
|
18272
18794
|
await updatePublicationPage(frontmatterStore(graph), current.concept, current, changes);
|
|
18273
18795
|
await settle$1(graph);
|
|
18274
18796
|
const after = summarisePublishing((await graph.publishing.readSource()).source).publications.find((p) => p.id === current.id);
|
|
18275
|
-
const
|
|
18276
|
-
return { publication: publicationView(after ?? current,
|
|
18797
|
+
const destinationOf = await destinationsFor(graph, env);
|
|
18798
|
+
return { publication: publicationView(after ?? current, destinationOf(current.id)) };
|
|
18799
|
+
}
|
|
18800
|
+
/** A GitHub refusal or a refused branch, as this host words it: the token is an environment variable here. */
|
|
18801
|
+
function repositoryRefusal(error, repo) {
|
|
18802
|
+
if (error instanceof GitHubError) return new ToolError("github_refused", `${describeGitHubError(error, repo, { replaceToken: "Make a new one and set ETHERPK_GITHUB_TOKEN to it." })} Nothing was published.`);
|
|
18803
|
+
if (error instanceof Error && /knowledge graph/.test(error.message)) return new ToolError("repository_holds_graph", `${error.message} Nothing was published.`);
|
|
18804
|
+
return error;
|
|
18277
18805
|
}
|
|
18278
18806
|
/** How many items fall under each key. */
|
|
18279
18807
|
function countBy(items, keyOf) {
|
|
@@ -18307,12 +18835,13 @@ async function publish(graph, args, host) {
|
|
|
18307
18835
|
const summary = summarisePublishing(source);
|
|
18308
18836
|
const publication = summary.publications.find((p) => p.id === args.id.trim());
|
|
18309
18837
|
if (!publication) throw publicationNotFound(args.id, summary.publications, via);
|
|
18310
|
-
const folder = (await
|
|
18838
|
+
const { folder, repository } = (await destinationsFor(graph, env))(publication.id);
|
|
18311
18839
|
const dryRun = args.dryRun === true;
|
|
18312
|
-
|
|
18313
|
-
|
|
18314
|
-
|
|
18315
|
-
}
|
|
18840
|
+
const where = graph.backend.kind === "folder" ? `--folder "${graph.backend.path}"` : `--graph "${graph.name}"`;
|
|
18841
|
+
if (repository && !dryRun && via === "agent") throw new ToolError("publishes_to_repository", `"${publication.name}" publishes to the GitHub repository ${repositoryLabel(repository)}, and an agent's publish never commits to a repository, because a commit there can deploy the site. Use dryRun to check it. To publish it, ask the user to run: ${cmd} publish ${where} --publication ${publication.id}`);
|
|
18842
|
+
const token = env.ETHERPK_GITHUB_TOKEN?.trim();
|
|
18843
|
+
if (repository && !dryRun && !token) throw new ToolError("no_github_token", `"${publication.name}" publishes to ${repositoryLabel(repository)}, and ETHERPK_GITHUB_TOKEN is not set. Set it to a GitHub token that may read and write the repository's Contents. Nothing was published.`);
|
|
18844
|
+
if (!folder && !repository && !dryRun) throw new ToolError("no_publish_folder", via === "cli" ? `No publish folder or repository is set for "${publication.name}" on this machine. Run: ${cmd} publish ${where} --publication ${publication.id} --out <dir>, or --github <owner/name> - either is remembered.` : `No publish folder is set for "${publication.name}" on this machine. Ask the user to run: ${cmd} publish ${where} --publication ${publication.id} --out <dir> - that publishes once and remembers the folder for this tool.`);
|
|
18316
18845
|
if (folder && !dryRun) await checkPublishFolder(graph, publication.id, folder, host);
|
|
18317
18846
|
const selected = selectDocuments(source.documents, publication, summary.publications);
|
|
18318
18847
|
const includeTexts = Object.values(publication.includes).map((concept) => source.documents.find((d) => d.concept.toLowerCase() === concept.toLowerCase())?.text ?? "");
|
|
@@ -18342,12 +18871,33 @@ async function publish(graph, args, host) {
|
|
|
18342
18871
|
renderer,
|
|
18343
18872
|
maps
|
|
18344
18873
|
}),
|
|
18345
|
-
unsettled
|
|
18874
|
+
unsettled,
|
|
18875
|
+
destination: repository ? "repository" : "folder"
|
|
18346
18876
|
});
|
|
18877
|
+
let committed = null;
|
|
18878
|
+
if (run.report.ok && repository && !dryRun) {
|
|
18879
|
+
try {
|
|
18880
|
+
committed = await publishToRepository(createGitHubApi({
|
|
18881
|
+
token,
|
|
18882
|
+
baseUrl: env.ETHERPK_GITHUB_API_URL?.trim() || void 0
|
|
18883
|
+
}), repository, {
|
|
18884
|
+
publicationId: publication.id,
|
|
18885
|
+
bundle: run.bundle,
|
|
18886
|
+
seeded: run.seeded,
|
|
18887
|
+
agentsMd: run.agentsMd
|
|
18888
|
+
}, { confirmed: args.replace === true });
|
|
18889
|
+
} catch (error) {
|
|
18890
|
+
throw repositoryRefusal(error, repository);
|
|
18891
|
+
}
|
|
18892
|
+
if (committed.state === "needs-confirmation") {
|
|
18893
|
+
const listed = committed.replaced.slice(0, 10).join(", ") + (committed.replaced.length > 10 ? ` and ${committed.replaced.length - 10} more` : "");
|
|
18894
|
+
throw new ToolError("repository_needs_confirmation", `${repositoryLabel(repository)} does not hold this site yet, and publishing would replace or delete ${committed.replaced.length} file${committed.replaced.length === 1 ? "" : "s"} there that it did not write: ${listed}. Nothing was committed. Run the command again with --replace to publish anyway. The old versions stay in the branch's history.`);
|
|
18895
|
+
}
|
|
18896
|
+
}
|
|
18347
18897
|
const written = run.report.ok && folder && !dryRun ? await writeSite(nodeSiteFolder(folder), run.bundle, {
|
|
18348
18898
|
seeded: run.seeded,
|
|
18349
18899
|
agentsMd: run.agentsMd
|
|
18350
|
-
}) : null;
|
|
18900
|
+
}) : committed?.written ?? null;
|
|
18351
18901
|
const excludedByReason = {};
|
|
18352
18902
|
for (const e of run.report.excluded) excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
|
|
18353
18903
|
return {
|
|
@@ -18355,6 +18905,10 @@ async function publish(graph, args, host) {
|
|
|
18355
18905
|
...dryRun ? { dryRun: true } : {},
|
|
18356
18906
|
publication: run.report.publication,
|
|
18357
18907
|
folder,
|
|
18908
|
+
...repository ? {
|
|
18909
|
+
repository: repositoryLabel(repository),
|
|
18910
|
+
commit: committed?.state === "committed" ? committed.commit.url : null
|
|
18911
|
+
} : {},
|
|
18358
18912
|
written: written ? {
|
|
18359
18913
|
written: written.written,
|
|
18360
18914
|
unchanged: written.unchanged,
|
|
@@ -18413,15 +18967,19 @@ function cliPublishOutput(result) {
|
|
|
18413
18967
|
async function publishingInfo(graph, host) {
|
|
18414
18968
|
const { env, cmd } = hostOf$1(host);
|
|
18415
18969
|
const { source } = await graph.publishing.readSource();
|
|
18416
|
-
const
|
|
18970
|
+
const destinationOf = await destinationsFor(graph, env);
|
|
18417
18971
|
const chromium = await chromiumStatus(env, cmd);
|
|
18418
18972
|
return {
|
|
18419
|
-
publications: summarisePublishing(source).publications.map((p) =>
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18973
|
+
publications: summarisePublishing(source).publications.map((p) => {
|
|
18974
|
+
const { folder, repository } = destinationOf(p.id);
|
|
18975
|
+
return {
|
|
18976
|
+
id: p.id,
|
|
18977
|
+
name: p.name,
|
|
18978
|
+
kind: p.kind,
|
|
18979
|
+
publishFolder: folder,
|
|
18980
|
+
publishRepository: repository ? repositoryLabel(repository) : null
|
|
18981
|
+
};
|
|
18982
|
+
}),
|
|
18425
18983
|
mermaid: {
|
|
18426
18984
|
available: chromium.executable !== null,
|
|
18427
18985
|
...chromium.executable ? {} : { setup: chromium.setupCommand }
|
|
@@ -19784,6 +20342,7 @@ async function runGraphHost(deps) {
|
|
|
19784
20342
|
return startSession(socket, request, reply);
|
|
19785
20343
|
case "publish":
|
|
19786
20344
|
case "publish-check":
|
|
20345
|
+
case "publish-github":
|
|
19787
20346
|
if (stopping) return answer({ etherpk: "stopping" });
|
|
19788
20347
|
return runPublish(request, answer);
|
|
19789
20348
|
default: return answer({
|
|
@@ -25818,7 +26377,7 @@ function bindServeLifetime(deps) {
|
|
|
25818
26377
|
* etherpk-mcp stop [--graph <id or name> [--sync-server <url>] | --folder <path> | --all]
|
|
25819
26378
|
* etherpk-mcp logout [--sync-server <url> | --all]
|
|
25820
26379
|
* etherpk-mcp semantic setup | status | remove
|
|
25821
|
-
* etherpk-mcp publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir> | --dry-run]
|
|
26380
|
+
* etherpk-mcp publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir> | --github <owner/name[#branch]> | --dry-run] [--replace]
|
|
25822
26381
|
* etherpk-mcp diagrams setup | status
|
|
25823
26382
|
*
|
|
25824
26383
|
* One config file holds a login per Sync Server (ADR 0075). `--sync-server` names the one a
|
|
@@ -25889,8 +26448,9 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
|
|
|
25889
26448
|
to a service to be embedded.
|
|
25890
26449
|
${CMD} semantic status | remove
|
|
25891
26450
|
Whether it is set up here, or delete it (logout --all deletes it too).
|
|
25892
|
-
${CMD} publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir> | --dry-run]
|
|
25893
|
-
Publish one publication of a graph to a folder on this machine
|
|
26451
|
+
${CMD} publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir> | --github <owner/name[#branch]> | --dry-run] [--replace]
|
|
26452
|
+
Publish one publication of a graph to a folder on this machine, or to a branch of a
|
|
26453
|
+
GitHub repository, and print the report.
|
|
25894
26454
|
--out sets the publish folder for that graph and publication and is remembered: from
|
|
25895
26455
|
then on this command without --out, and the agent's publish tool, write to the same
|
|
25896
26456
|
folder (the agent cannot choose one). It must be a folder of its own, outside the
|
|
@@ -25898,6 +26458,13 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
|
|
|
25898
26458
|
and feed files) are rewritten and their strays removed - everything else in it is left
|
|
25899
26459
|
alone. Publishing writes files - deploying them (a git push, say) is yours. Run it from
|
|
25900
26460
|
cron for a site that republishes itself.
|
|
26461
|
+
--github commits the site to the repository's branch (main unless #branch says) as one
|
|
26462
|
+
commit, with the token in ETHERPK_GITHUB_TOKEN, and is remembered in place of a folder.
|
|
26463
|
+
The host watching the branch then deploys it. The same files are owned and the rest of
|
|
26464
|
+
the branch is left alone. A publish never creates the branch. The agent's publish tool
|
|
26465
|
+
never commits to a repository: this command is how a publication there is published.
|
|
26466
|
+
--replace lets a first publish replace files on a branch that does not hold this site
|
|
26467
|
+
yet, which it otherwise lists and refuses.
|
|
25901
26468
|
--dry-run renders and checks the publication and prints the whole report, with the
|
|
25902
26469
|
names of the pages it mentions, but writes nothing and needs no folder. Keep its output
|
|
25903
26470
|
out of public logs.
|
|
@@ -26308,19 +26875,32 @@ async function publishCommand(args) {
|
|
|
26308
26875
|
if (!publication) fail("publish needs --publication <id>. The agent's list_publications tool, or Settings → Publish in EtherPK, shows the ids.");
|
|
26309
26876
|
const dryRun = args["dry-run"] === true;
|
|
26310
26877
|
const out = args.out?.trim();
|
|
26311
|
-
|
|
26878
|
+
const githubArg = args.github?.trim();
|
|
26879
|
+
if (dryRun && (out || githubArg)) fail("publish --dry-run writes nothing, so it takes no --out or --github. Leave them off, or drop --dry-run to publish.");
|
|
26880
|
+
if (out && githubArg) fail("publish takes --out or --github, not both: a publication publishes to one place on this machine.");
|
|
26881
|
+
const github = githubArg ? parseRepositoryArgument(githubArg) : null;
|
|
26882
|
+
if (githubArg && !github) fail(`--github takes owner/name, or owner/name#branch, such as you/garden-site#main ("${githubArg}" is not one).`);
|
|
26312
26883
|
const found = await findGraph(request).catch((error) => fail(`etherpk-mcp: ${describeError(error)}`));
|
|
26313
26884
|
const env = publishSettings(process.env);
|
|
26885
|
+
const replace = args.replace === true ? { replace: true } : {};
|
|
26314
26886
|
const { reply, socket } = await requestHost(launcherFor(found), dryRun ? {
|
|
26315
26887
|
etherpk: "publish-check",
|
|
26316
26888
|
protocol: 1,
|
|
26317
26889
|
publication,
|
|
26318
26890
|
env
|
|
26891
|
+
} : github ? {
|
|
26892
|
+
etherpk: "publish-github",
|
|
26893
|
+
protocol: 1,
|
|
26894
|
+
publication,
|
|
26895
|
+
github,
|
|
26896
|
+
...replace,
|
|
26897
|
+
env
|
|
26319
26898
|
} : {
|
|
26320
26899
|
etherpk: "publish",
|
|
26321
26900
|
protocol: 1,
|
|
26322
26901
|
publication,
|
|
26323
26902
|
...out ? { out: resolve(out) } : {},
|
|
26903
|
+
...replace,
|
|
26324
26904
|
env
|
|
26325
26905
|
}).catch((error) => fail(`etherpk-mcp: ${describeError(error)}`));
|
|
26326
26906
|
socket.destroy();
|
|
@@ -26332,7 +26912,7 @@ async function publishCommand(args) {
|
|
|
26332
26912
|
return;
|
|
26333
26913
|
case "refused": return fail(reply.message);
|
|
26334
26914
|
case "failed":
|
|
26335
|
-
if (dryRun && reply.message === "The request was not understood.") return fail(`etherpk-mcp: this graph is served on this computer by an older etherpk-mcp, which cannot run a dry run. Run: ${CMD} stop, then try again.`);
|
|
26915
|
+
if ((dryRun || github) && reply.message === "The request was not understood.") return fail(`etherpk-mcp: this graph is served on this computer by an older etherpk-mcp, which cannot ${dryRun ? "run a dry run" : "publish to a repository"}. Run: ${CMD} stop, then try again.`);
|
|
26336
26916
|
return fail(`etherpk-mcp: ${reply.message}`);
|
|
26337
26917
|
case "unsupported": return fail(`etherpk-mcp: this graph is served on this computer by etherpk-mcp ${reply.version}, which cannot run a publish for this version (${VERSION}). Run: ${CMD} stop, then publish again.`);
|
|
26338
26918
|
default: return fail(`etherpk-mcp: the graph's background process gave an answer this version does not know ("${reply.etherpk}").`);
|
|
@@ -26340,9 +26920,10 @@ async function publishCommand(args) {
|
|
|
26340
26920
|
}
|
|
26341
26921
|
/**
|
|
26342
26922
|
* A command-line publish, as the graph's host runs it: the publication first, so a mistyped id is
|
|
26343
|
-
* never remembered as a
|
|
26344
|
-
* remembered, then the publish the agent's tool runs
|
|
26345
|
-
*
|
|
26923
|
+
* never remembered as a destination, then `--out` checked against the graph's folder, or
|
|
26924
|
+
* `--github` checked as a repository, and remembered, then the publish the agent's tool runs, as
|
|
26925
|
+
* the person (`via: 'cli'`), which may commit to a repository. The answer is what the command
|
|
26926
|
+
* prints, in its own words.
|
|
26346
26927
|
*/
|
|
26347
26928
|
async function publishInHost(graph, request) {
|
|
26348
26929
|
const env = withPublishSettings(process.env, request.env ?? {});
|
|
@@ -26368,15 +26949,29 @@ async function publishInHost(graph, request) {
|
|
|
26368
26949
|
await findPublication(graph, request.publication, host);
|
|
26369
26950
|
const configPath = defaultPublishFoldersPath(env);
|
|
26370
26951
|
const key = publishGraphKey(graph.backend, graph.graphId);
|
|
26371
|
-
if (request.
|
|
26952
|
+
if (request.etherpk === "publish-github") {
|
|
26953
|
+
const repo = parseRepositoryArgument(`${request.github.owner}/${request.github.name}#${request.github.branch}`);
|
|
26954
|
+
if (!repo) return {
|
|
26955
|
+
etherpk: "refused",
|
|
26956
|
+
message: "etherpk-mcp: --github takes owner/name, or owner/name#branch."
|
|
26957
|
+
};
|
|
26958
|
+
await writePublishFolders(configPath, withPublishRepository(await readPublishFolders(configPath), key, request.publication, repo));
|
|
26959
|
+
notes.push(`etherpk-mcp: "${request.publication}" of "${graph.name}" now publishes to ${repositoryLabel(repo)} (remembered in ${configPath}, in place of any folder).`);
|
|
26960
|
+
} else if (request.out) {
|
|
26372
26961
|
await checkPublishFolder(graph, request.publication, request.out, host);
|
|
26373
26962
|
await writePublishFolders(configPath, withPublishFolder(await readPublishFolders(configPath), key, request.publication, request.out));
|
|
26374
26963
|
notes.push(`etherpk-mcp: publish folder for "${request.publication}" of "${graph.name}" set to ${request.out} (remembered in ${configPath}).`);
|
|
26375
|
-
} else
|
|
26376
|
-
|
|
26377
|
-
|
|
26378
|
-
|
|
26379
|
-
|
|
26964
|
+
} else {
|
|
26965
|
+
const remembered = await readPublishFolders(configPath);
|
|
26966
|
+
if (!publishFolderOf(remembered, key, request.publication) && !publishRepositoryOf(remembered, key, request.publication)) return {
|
|
26967
|
+
etherpk: "refused",
|
|
26968
|
+
message: `No publish folder or repository is set for "${request.publication}" of "${graph.name}" on this machine. Pass --out <dir> or --github <owner/name> once - it is remembered.`
|
|
26969
|
+
};
|
|
26970
|
+
}
|
|
26971
|
+
const result = await publish(graph, {
|
|
26972
|
+
id: request.publication,
|
|
26973
|
+
replace: request.replace === true
|
|
26974
|
+
}, host);
|
|
26380
26975
|
return {
|
|
26381
26976
|
etherpk: "published",
|
|
26382
26977
|
output: cliPublishOutput(result),
|
|
@@ -26892,6 +27487,8 @@ async function main() {
|
|
|
26892
27487
|
"no-semantic": { type: "boolean" },
|
|
26893
27488
|
publication: { type: "string" },
|
|
26894
27489
|
out: { type: "string" },
|
|
27490
|
+
github: { type: "string" },
|
|
27491
|
+
replace: { type: "boolean" },
|
|
26895
27492
|
"dry-run": { type: "boolean" },
|
|
26896
27493
|
all: { type: "boolean" },
|
|
26897
27494
|
name: { type: "string" },
|