@setzkasten-cms/astro-admin 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-routes/_auth-guard.d.ts +47 -0
- package/dist/api-routes/_auth-guard.js +18 -0
- package/dist/api-routes/_commit-trailers.d.ts +8 -0
- package/dist/api-routes/_commit-trailers.js +8 -0
- package/dist/api-routes/_feature-gate.d.ts +23 -0
- package/dist/api-routes/_feature-gate.js +7 -0
- package/dist/api-routes/_github-cache.d.ts +4 -0
- package/dist/api-routes/_github-cache.js +8 -0
- package/dist/api-routes/_github-token.d.ts +27 -0
- package/dist/api-routes/_github-token.js +8 -0
- package/dist/api-routes/_license-tier.d.ts +22 -0
- package/dist/api-routes/_license-tier.js +6 -0
- package/dist/api-routes/_pages-meta-store.d.ts +32 -0
- package/dist/api-routes/_pages-meta-store.js +9 -0
- package/dist/api-routes/_role-resolver.d.ts +15 -0
- package/dist/api-routes/_role-resolver.js +13 -0
- package/dist/api-routes/_session-cookie.d.ts +18 -0
- package/dist/api-routes/_session-cookie.js +6 -0
- package/dist/api-routes/_storage-config.d.ts +60 -0
- package/dist/api-routes/_storage-config.js +10 -0
- package/dist/api-routes/_vercel-origin.d.ts +16 -0
- package/dist/api-routes/_vercel-origin.js +6 -0
- package/dist/api-routes/_webhook-dispatcher.d.ts +13 -0
- package/dist/api-routes/_webhook-dispatcher.js +97 -0
- package/dist/api-routes/_webhook-signing.d.ts +11 -0
- package/dist/api-routes/_webhook-signing.js +6 -0
- package/dist/api-routes/_webhook-status-store.d.ts +19 -0
- package/dist/api-routes/_webhook-status-store.js +10 -0
- package/dist/api-routes/_website-resolver.d.ts +49 -0
- package/dist/api-routes/_website-resolver.js +14 -0
- package/dist/api-routes/_websites-store.d.ts +30 -0
- package/dist/api-routes/_websites-store.js +11 -0
- package/dist/api-routes/asset-proxy.d.ts +12 -0
- package/dist/api-routes/asset-proxy.js +67 -0
- package/dist/api-routes/auth-callback.d.ts +9 -0
- package/dist/api-routes/auth-callback.js +68 -0
- package/dist/api-routes/auth-login.d.ts +11 -0
- package/dist/api-routes/auth-login.js +27 -0
- package/dist/api-routes/auth-logout.d.ts +10 -0
- package/dist/api-routes/auth-logout.js +13 -0
- package/dist/api-routes/auth-session.d.ts +9 -0
- package/dist/api-routes/auth-session.js +31 -0
- package/dist/api-routes/auth-setzkasten-login.d.ts +18 -0
- package/dist/api-routes/auth-setzkasten-login.js +74 -0
- package/dist/api-routes/catalog-add.d.ts +14 -0
- package/dist/api-routes/catalog-add.js +153 -0
- package/dist/api-routes/catalog-export.d.ts +13 -0
- package/dist/api-routes/catalog-export.js +71 -0
- package/dist/api-routes/catalog-helpers.d.ts +41 -0
- package/dist/api-routes/catalog-helpers.js +11 -0
- package/dist/api-routes/catalog-list.d.ts +11 -0
- package/dist/api-routes/catalog-list.js +12 -0
- package/dist/api-routes/config.d.ts +12 -0
- package/dist/api-routes/config.js +43 -0
- package/dist/api-routes/deploy-hook.d.ts +14 -0
- package/dist/api-routes/deploy-hook.js +52 -0
- package/dist/api-routes/editors.d.ts +29 -0
- package/dist/api-routes/editors.js +18 -0
- package/dist/api-routes/github-proxy.d.ts +12 -0
- package/dist/api-routes/github-proxy.js +82 -0
- package/dist/api-routes/global-config.d.ts +20 -0
- package/dist/api-routes/global-config.js +19 -0
- package/dist/api-routes/history-rollback.d.ts +22 -0
- package/dist/api-routes/history-rollback.js +111 -0
- package/dist/api-routes/history-version.d.ts +11 -0
- package/dist/api-routes/history-version.js +57 -0
- package/dist/api-routes/history.d.ts +13 -0
- package/dist/api-routes/history.js +85 -0
- package/dist/api-routes/icons-local.d.ts +28 -0
- package/dist/api-routes/icons-local.js +115 -0
- package/dist/api-routes/init-add-section.d.ts +23 -0
- package/dist/api-routes/init-add-section.js +396 -0
- package/dist/api-routes/init-apply.d.ts +11 -0
- package/dist/api-routes/init-apply.js +266 -0
- package/dist/api-routes/init-migrate.d.ts +16 -0
- package/dist/api-routes/init-migrate.js +205 -0
- package/dist/api-routes/init-scan-page.d.ts +39 -0
- package/dist/api-routes/init-scan-page.js +260 -0
- package/dist/api-routes/init-scan.d.ts +11 -0
- package/dist/api-routes/init-scan.js +128 -0
- package/dist/api-routes/migrate-to-multi.d.ts +26 -0
- package/dist/api-routes/migrate-to-multi.js +188 -0
- package/dist/api-routes/pages.d.ts +39 -0
- package/dist/api-routes/pages.js +88 -0
- package/dist/api-routes/section-add.d.ts +18 -0
- package/dist/api-routes/section-add.js +173 -0
- package/dist/api-routes/section-commit-pending.d.ts +18 -0
- package/dist/api-routes/section-commit-pending.js +207 -0
- package/dist/api-routes/section-delete.d.ts +15 -0
- package/dist/api-routes/section-delete.js +149 -0
- package/dist/api-routes/section-duplicate.d.ts +15 -0
- package/dist/api-routes/section-duplicate.js +143 -0
- package/dist/api-routes/section-management.d.ts +41 -0
- package/dist/api-routes/section-management.js +14 -0
- package/dist/api-routes/section-prepare-copy.d.ts +25 -0
- package/dist/api-routes/section-prepare-copy.js +69 -0
- package/dist/api-routes/section-prepare.d.ts +18 -0
- package/dist/api-routes/section-prepare.js +104 -0
- package/dist/api-routes/setup-github-app-bounce.d.ts +13 -0
- package/dist/api-routes/setup-github-app-bounce.js +45 -0
- package/dist/api-routes/setup-github-app-branches.d.ts +14 -0
- package/dist/api-routes/setup-github-app-branches.js +58 -0
- package/dist/api-routes/setup-github-app-callback.d.ts +15 -0
- package/dist/api-routes/setup-github-app-callback.js +45 -0
- package/dist/api-routes/setup-github-app-installed.d.ts +15 -0
- package/dist/api-routes/setup-github-app-installed.js +33 -0
- package/dist/api-routes/setup-github-app-repos.d.ts +17 -0
- package/dist/api-routes/setup-github-app-repos.js +41 -0
- package/dist/api-routes/setup-github-app.d.ts +15 -0
- package/dist/api-routes/setup-github-app.js +41 -0
- package/dist/api-routes/updater-check.d.ts +10 -0
- package/dist/api-routes/updater-check.js +37 -0
- package/dist/api-routes/updater-register.d.ts +14 -0
- package/dist/api-routes/updater-register.js +71 -0
- package/dist/api-routes/updater-transfer.d.ts +11 -0
- package/dist/api-routes/updater-transfer.js +37 -0
- package/dist/api-routes/updater-unbind.d.ts +17 -0
- package/dist/api-routes/updater-unbind.js +35 -0
- package/dist/api-routes/webhooks-status.d.ts +12 -0
- package/dist/api-routes/webhooks-status.js +22 -0
- package/dist/api-routes/webhooks-test.d.ts +13 -0
- package/dist/api-routes/webhooks-test.js +124 -0
- package/dist/api-routes/webhooks.d.ts +6 -0
- package/dist/api-routes/webhooks.js +148 -0
- package/dist/api-routes/websites-add.d.ts +15 -0
- package/dist/api-routes/websites-add.js +92 -0
- package/dist/api-routes/websites-list.d.ts +12 -0
- package/dist/api-routes/websites-list.js +35 -0
- package/dist/api-routes/websites-remove.d.ts +15 -0
- package/dist/api-routes/websites-remove.js +69 -0
- package/dist/chunk-35S35OIV.js +80 -0
- package/dist/chunk-45ARVNT3.js +25 -0
- package/dist/chunk-5PIMDP4N.js +25 -0
- package/dist/chunk-5ZFTG4BW.js +10 -0
- package/dist/chunk-6UIKVKED.js +51 -0
- package/dist/chunk-737TIZRU.js +9 -0
- package/dist/chunk-AM4DZXXM.js +120 -0
- package/dist/chunk-FXNOTESI.js +87 -0
- package/dist/chunk-GHNK2GFE.js +48 -0
- package/dist/chunk-GRG3LNKH.js +37 -0
- package/dist/chunk-INIWFKQ3.js +236 -0
- package/dist/chunk-JHY6XTLL.js +24 -0
- package/dist/chunk-K22A4ZBS.js +1574 -0
- package/dist/chunk-KH22FJO5.js +17 -0
- package/dist/chunk-NKDATSPA.js +43 -0
- package/dist/chunk-RHJONMLK.js +1267 -0
- package/dist/chunk-TJNJKPUL.js +11 -0
- package/dist/chunk-V6IMPVF3.js +120 -0
- package/dist/chunk-W3QHY5GW.js +19 -0
- package/dist/chunk-ZQDGGWJP.js +43 -0
- package/package.json +249 -53
- package/src/api-routes/__tests__/route-registry.test.ts +7 -1
- package/tsconfig.json +0 -9
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readPagesMeta
|
|
3
|
+
} from "../chunk-FXNOTESI.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveStorageConfigForRequest
|
|
6
|
+
} from "../chunk-6UIKVKED.js";
|
|
7
|
+
import {
|
|
8
|
+
cachedFetch
|
|
9
|
+
} from "../chunk-45ARVNT3.js";
|
|
10
|
+
import {
|
|
11
|
+
resolveGitHubTokenForRequest
|
|
12
|
+
} from "../chunk-NKDATSPA.js";
|
|
13
|
+
import "../chunk-KH22FJO5.js";
|
|
14
|
+
|
|
15
|
+
// src/api-routes/pages.ts
|
|
16
|
+
function resolvePages() {
|
|
17
|
+
const buildPages = typeof __SETZKASTEN_PAGES__ !== "undefined" ? __SETZKASTEN_PAGES__ : null;
|
|
18
|
+
return buildPages ?? globalThis.__SETZKASTEN_PAGES__ ?? [];
|
|
19
|
+
}
|
|
20
|
+
var GET = async ({ request }) => {
|
|
21
|
+
const isMulti = request.headers.get("x-sk-website") !== null;
|
|
22
|
+
const pages = isMulti ? await fetchPagesFromGitHub(request).catch(() => []) : resolvePages();
|
|
23
|
+
const enriched = await enrichWithLastModified(pages, request).catch(() => pages);
|
|
24
|
+
return new Response(JSON.stringify({ pages: enriched }), {
|
|
25
|
+
status: 200,
|
|
26
|
+
headers: { "Content-Type": "application/json" }
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
async function fetchPagesFromGitHub(request) {
|
|
30
|
+
const storage = await resolveStorageConfigForRequest(request);
|
|
31
|
+
if (!storage) return [];
|
|
32
|
+
const tokenResult = await resolveGitHubTokenForRequest(request);
|
|
33
|
+
if (!tokenResult.ok) return [];
|
|
34
|
+
const { owner, repo, branch } = storage;
|
|
35
|
+
const cacheKey = `pages-list:${owner}/${repo}:${branch}`;
|
|
36
|
+
return cachedFetch(cacheKey, 5 * 6e4, async () => {
|
|
37
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/contents/src/pages?ref=${branch}`;
|
|
38
|
+
const res = await fetch(url, {
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${tokenResult.value}`,
|
|
41
|
+
Accept: "application/vnd.github+json",
|
|
42
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) return [];
|
|
46
|
+
const entries = await res.json();
|
|
47
|
+
if (!Array.isArray(entries)) return [];
|
|
48
|
+
const pages = [];
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (entry.type !== "file") continue;
|
|
51
|
+
if (!entry.name.endsWith(".astro")) continue;
|
|
52
|
+
if (entry.name.startsWith("_") || entry.name.startsWith("[")) continue;
|
|
53
|
+
const pageKey = entry.name.slice(0, -".astro".length);
|
|
54
|
+
pages.push({
|
|
55
|
+
path: entry.path,
|
|
56
|
+
pageKey,
|
|
57
|
+
label: pageKey === "index" ? "Startseite" : pageKey,
|
|
58
|
+
hasConfig: true
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return pages;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function enrichWithLastModified(pages, request) {
|
|
65
|
+
if (pages.length === 0) return pages;
|
|
66
|
+
const storage = await resolveStorageConfigForRequest(request);
|
|
67
|
+
if (!storage) return pages;
|
|
68
|
+
const tokenResult = await resolveGitHubTokenForRequest(request);
|
|
69
|
+
if (!tokenResult.ok) return pages;
|
|
70
|
+
const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
|
|
71
|
+
const target = {
|
|
72
|
+
owner: storage.owner,
|
|
73
|
+
repo: storage.repo,
|
|
74
|
+
branch: storage.branch,
|
|
75
|
+
contentPath: serverConfig?.storage?.contentPath ?? "content",
|
|
76
|
+
token: tokenResult.value
|
|
77
|
+
};
|
|
78
|
+
const meta = await readPagesMeta(target);
|
|
79
|
+
if (!meta.ok) return pages;
|
|
80
|
+
return pages.map((p) => {
|
|
81
|
+
const ts = meta.value.meta.pages[p.pageKey]?.lastModified;
|
|
82
|
+
return ts !== void 0 ? { ...p, lastModified: ts } : p;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
GET,
|
|
87
|
+
resolvePages
|
|
88
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* POST /api/setzkasten/sections/add
|
|
5
|
+
*
|
|
6
|
+
* Adds a new section instance to a page:
|
|
7
|
+
* - Creates content JSON with default values from schema (_sections/{key}.json)
|
|
8
|
+
* - Appends new entry to the page config (pages/_{pageKey}.json)
|
|
9
|
+
* - Key auto-generated from type: hero → hero (or hero--2 if taken)
|
|
10
|
+
*
|
|
11
|
+
* Body: { pageKey, sectionType, sectionKey? (override), sourcePage? (content seed from another page's section), owner?, repo?, branch?, contentPath? }
|
|
12
|
+
*
|
|
13
|
+
* sourcePage: if provided, uses the existing content JSON of that section as seed instead of schema defaults.
|
|
14
|
+
* The source section key is derived from sectionType (or sectionKey if provided) on the sourcePage.
|
|
15
|
+
*/
|
|
16
|
+
declare const POST: APIRoute;
|
|
17
|
+
|
|
18
|
+
export { POST };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addToPageConfig,
|
|
3
|
+
generateAddKey
|
|
4
|
+
} from "../chunk-GHNK2GFE.js";
|
|
5
|
+
import {
|
|
6
|
+
guardPageAccess,
|
|
7
|
+
parseSession
|
|
8
|
+
} from "../chunk-INIWFKQ3.js";
|
|
9
|
+
import {
|
|
10
|
+
resolveStorageConfigForRequest
|
|
11
|
+
} from "../chunk-6UIKVKED.js";
|
|
12
|
+
import "../chunk-5PIMDP4N.js";
|
|
13
|
+
import "../chunk-45ARVNT3.js";
|
|
14
|
+
import {
|
|
15
|
+
resolveGitHubTokenForRequest
|
|
16
|
+
} from "../chunk-NKDATSPA.js";
|
|
17
|
+
import "../chunk-TJNJKPUL.js";
|
|
18
|
+
import {
|
|
19
|
+
withTrailers
|
|
20
|
+
} from "../chunk-KH22FJO5.js";
|
|
21
|
+
|
|
22
|
+
// src/api-routes/section-add.ts
|
|
23
|
+
var POST = async ({ request, cookies }) => {
|
|
24
|
+
const session = cookies.get("setzkasten_session")?.value;
|
|
25
|
+
if (!session) return new Response("Unauthorized", { status: 401 });
|
|
26
|
+
const tokenResult = await resolveGitHubTokenForRequest(request);
|
|
27
|
+
if (!tokenResult.ok) {
|
|
28
|
+
return new Response(tokenResult.error.message, { status: 500 });
|
|
29
|
+
}
|
|
30
|
+
const githubToken = tokenResult.value;
|
|
31
|
+
try {
|
|
32
|
+
const body = await request.json();
|
|
33
|
+
const storage = await resolveStorageConfigForRequest(request, body);
|
|
34
|
+
if (!storage) return Response.json({ error: "Could not resolve owner/repo" }, { status: 400 });
|
|
35
|
+
const { owner, repo, branch, projectPrefix } = storage;
|
|
36
|
+
const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
|
|
37
|
+
const fullConfig = globalThis.__SETZKASTEN_FULL_CONFIG__;
|
|
38
|
+
const contentPath = body.contentPath || serverConfig?.storage?.contentPath || "content";
|
|
39
|
+
const { pageKey, sectionType, sourcePage } = body;
|
|
40
|
+
if (!pageKey || !sectionType) {
|
|
41
|
+
return Response.json({ error: "pageKey and sectionType are required" }, { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
const denied = await guardPageAccess(parseSession(cookies.get("setzkasten_session")?.value), pageKey, fullConfig, request);
|
|
44
|
+
if (denied) return denied;
|
|
45
|
+
const headers = {
|
|
46
|
+
Authorization: `Bearer ${githubToken}`,
|
|
47
|
+
Accept: "application/vnd.github+json",
|
|
48
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
49
|
+
"Content-Type": "application/json"
|
|
50
|
+
};
|
|
51
|
+
const configKey = "_" + pageKey.replace(/\//g, "_");
|
|
52
|
+
const pageConfigPath = `${contentPath}/pages/${configKey}.json`;
|
|
53
|
+
const pageConfigRaw = await fetchFileContent(owner, repo, branch, pageConfigPath, githubToken);
|
|
54
|
+
if (!pageConfigRaw) return Response.json({ error: "Page config not found" }, { status: 404 });
|
|
55
|
+
const pageConfig = JSON.parse(pageConfigRaw);
|
|
56
|
+
const existingKeys = (pageConfig.sections ?? []).map((s) => s.key);
|
|
57
|
+
const newKey = body.sectionKey ?? generateAddKey(existingKeys, sectionType);
|
|
58
|
+
if (existingKeys.includes(newKey)) {
|
|
59
|
+
return Response.json({ error: `Key "${newKey}" already exists on this page` }, { status: 409 });
|
|
60
|
+
}
|
|
61
|
+
let defaultContent = {};
|
|
62
|
+
if (sourcePage) {
|
|
63
|
+
const sourceKey = sectionType;
|
|
64
|
+
const sourceJsonPath = `${contentPath}/_sections/${sourceKey}.json`;
|
|
65
|
+
const sourceContent = await fetchFileContent(owner, repo, branch, sourceJsonPath, githubToken);
|
|
66
|
+
if (sourceContent) {
|
|
67
|
+
try {
|
|
68
|
+
defaultContent = JSON.parse(sourceContent);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (Object.keys(defaultContent).length === 0) {
|
|
74
|
+
const sectionDef = findSectionDef(fullConfig, sectionType);
|
|
75
|
+
defaultContent = sectionDef ? buildDefaultContent(sectionDef.fields ?? {}) : {};
|
|
76
|
+
}
|
|
77
|
+
const updatedConfig = addToPageConfig(pageConfig, newKey, sectionType);
|
|
78
|
+
const sectionJsonPath = `${contentPath}/_sections/${newKey}.json`;
|
|
79
|
+
const commitResult = await batchCommit(
|
|
80
|
+
owner,
|
|
81
|
+
repo,
|
|
82
|
+
branch,
|
|
83
|
+
[
|
|
84
|
+
{ path: pageConfigPath, content: JSON.stringify(updatedConfig, null, 2) },
|
|
85
|
+
{ path: sectionJsonPath, content: JSON.stringify(defaultContent, null, 2) }
|
|
86
|
+
],
|
|
87
|
+
withTrailers(
|
|
88
|
+
`content: add ${sectionType} section "${newKey}" to ${pageKey}`,
|
|
89
|
+
parseSession(cookies.get("setzkasten_session")?.value)?.user?.email
|
|
90
|
+
),
|
|
91
|
+
headers
|
|
92
|
+
);
|
|
93
|
+
if (!commitResult.ok) return Response.json({ error: commitResult.error }, { status: 500 });
|
|
94
|
+
const { recordPageEdit } = await import("./_pages-meta-store.js");
|
|
95
|
+
await recordPageEdit(
|
|
96
|
+
{ owner, repo, branch, contentPath, token: tokenResult.value },
|
|
97
|
+
pageKey
|
|
98
|
+
).catch(() => {
|
|
99
|
+
});
|
|
100
|
+
return Response.json({ success: true, newKey, commitSha: commitResult.sha });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("[setzkasten] section-add error:", error);
|
|
103
|
+
return Response.json(
|
|
104
|
+
{ error: error instanceof Error ? error.message : "Add section failed" },
|
|
105
|
+
{ status: 500 }
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
function findSectionDef(fullConfig, sectionType) {
|
|
110
|
+
if (!fullConfig?.products) return null;
|
|
111
|
+
for (const product of Object.values(fullConfig.products)) {
|
|
112
|
+
if (product?.sections?.[sectionType]) return product.sections[sectionType];
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function buildDefaultContent(fields) {
|
|
117
|
+
const result = {};
|
|
118
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
119
|
+
if (field.defaultValue !== void 0) {
|
|
120
|
+
result[key] = field.defaultValue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
async function fetchFileContent(owner, repo, branch, path, token) {
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch(
|
|
128
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
129
|
+
{ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" } }
|
|
130
|
+
);
|
|
131
|
+
if (!res.ok) return null;
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
return data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function batchCommit(owner, repo, branch, files, message, headers) {
|
|
139
|
+
try {
|
|
140
|
+
const refRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, { headers });
|
|
141
|
+
if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` };
|
|
142
|
+
const { object: { sha: headSha } } = await refRes.json();
|
|
143
|
+
const commitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`, { headers });
|
|
144
|
+
if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` };
|
|
145
|
+
const { tree: { sha: baseSha } } = await commitRes.json();
|
|
146
|
+
const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers,
|
|
149
|
+
body: JSON.stringify({ base_tree: baseSha, tree: files.map((f) => ({ path: f.path, mode: "100644", type: "blob", content: f.content })) })
|
|
150
|
+
});
|
|
151
|
+
if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` };
|
|
152
|
+
const { sha: treeSha } = await treeRes.json();
|
|
153
|
+
const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers,
|
|
156
|
+
body: JSON.stringify({ tree: treeSha, parents: [headSha], message })
|
|
157
|
+
});
|
|
158
|
+
if (!newCommitRes.ok) return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` };
|
|
159
|
+
const { sha: newSha } = await newCommitRes.json();
|
|
160
|
+
const updateRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
|
|
161
|
+
method: "PATCH",
|
|
162
|
+
headers,
|
|
163
|
+
body: JSON.stringify({ sha: newSha })
|
|
164
|
+
});
|
|
165
|
+
if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` };
|
|
166
|
+
return { ok: true, sha: newSha };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return { ok: false, error: error instanceof Error ? error.message : "Commit failed" };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
POST
|
|
173
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* POST /api/setzkasten/sections/commit-pending
|
|
5
|
+
*
|
|
6
|
+
* Batch-commits all pending (optimistically added) sections in ONE GitHub commit.
|
|
7
|
+
* This triggers exactly one Vercel build regardless of how many sections were added.
|
|
8
|
+
*
|
|
9
|
+
* Body: {
|
|
10
|
+
* pageKey: string,
|
|
11
|
+
* pageConfig: object, // full updated page config (already includes pending entries)
|
|
12
|
+
* sections: Array<{ key: string, content: Record<string, unknown> }>,
|
|
13
|
+
* owner?, repo?, branch?, contentPath?
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
declare const POST: APIRoute;
|
|
17
|
+
|
|
18
|
+
export { POST };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
convertToSetHtml
|
|
3
|
+
} from "../chunk-RHJONMLK.js";
|
|
4
|
+
import {
|
|
5
|
+
readPagesMeta
|
|
6
|
+
} from "../chunk-FXNOTESI.js";
|
|
7
|
+
import {
|
|
8
|
+
guardPageAccess,
|
|
9
|
+
parseSession
|
|
10
|
+
} from "../chunk-INIWFKQ3.js";
|
|
11
|
+
import {
|
|
12
|
+
prefixPath,
|
|
13
|
+
resolveStorageConfigForRequest
|
|
14
|
+
} from "../chunk-6UIKVKED.js";
|
|
15
|
+
import "../chunk-5PIMDP4N.js";
|
|
16
|
+
import "../chunk-45ARVNT3.js";
|
|
17
|
+
import {
|
|
18
|
+
resolveGitHubTokenForRequest
|
|
19
|
+
} from "../chunk-NKDATSPA.js";
|
|
20
|
+
import "../chunk-TJNJKPUL.js";
|
|
21
|
+
import {
|
|
22
|
+
withTrailers
|
|
23
|
+
} from "../chunk-KH22FJO5.js";
|
|
24
|
+
|
|
25
|
+
// src/api-routes/section-commit-pending.ts
|
|
26
|
+
import { writeFile } from "fs/promises";
|
|
27
|
+
import { join } from "path";
|
|
28
|
+
import { setPageLastModified } from "@setzkasten-cms/core";
|
|
29
|
+
var POST = async ({ request, cookies }) => {
|
|
30
|
+
const session = cookies.get("setzkasten_session")?.value;
|
|
31
|
+
if (!session) return new Response("Unauthorized", { status: 401 });
|
|
32
|
+
const tokenResult = await resolveGitHubTokenForRequest(request);
|
|
33
|
+
if (!tokenResult.ok) {
|
|
34
|
+
return new Response(tokenResult.error.message, { status: 500 });
|
|
35
|
+
}
|
|
36
|
+
const githubToken = tokenResult.value;
|
|
37
|
+
try {
|
|
38
|
+
const body = await request.json();
|
|
39
|
+
const storage = await resolveStorageConfigForRequest(request, body);
|
|
40
|
+
if (!storage) return Response.json({ error: "Could not resolve owner/repo" }, { status: 400 });
|
|
41
|
+
const { owner, repo, branch } = storage;
|
|
42
|
+
const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
|
|
43
|
+
const fullConfig = globalThis.__SETZKASTEN_FULL_CONFIG__;
|
|
44
|
+
const contentPath = body.contentPath || serverConfig?.storage?.contentPath || "content";
|
|
45
|
+
const { pageKey, pageConfig, sections, edits = [] } = body;
|
|
46
|
+
if (!pageKey || !pageConfig || !Array.isArray(sections)) {
|
|
47
|
+
return Response.json({ error: "pageKey, pageConfig, and sections are required" }, { status: 400 });
|
|
48
|
+
}
|
|
49
|
+
const denied = await guardPageAccess(parseSession(cookies.get("setzkasten_session")?.value), pageKey, fullConfig, request);
|
|
50
|
+
if (denied) return denied;
|
|
51
|
+
const headers = {
|
|
52
|
+
Authorization: `Bearer ${githubToken}`,
|
|
53
|
+
Accept: "application/vnd.github+json",
|
|
54
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
55
|
+
"Content-Type": "application/json"
|
|
56
|
+
};
|
|
57
|
+
const configKey = "_" + pageKey.replace(/\//g, "_");
|
|
58
|
+
const pageConfigPath = `${contentPath}/pages/${configKey}.json`;
|
|
59
|
+
const files = [
|
|
60
|
+
{ path: pageConfigPath, content: JSON.stringify(pageConfig, null, 2) },
|
|
61
|
+
...sections.map((s) => ({
|
|
62
|
+
path: `${contentPath}/_sections/${s.key}.json`,
|
|
63
|
+
content: JSON.stringify(s.content, null, 2)
|
|
64
|
+
})),
|
|
65
|
+
...edits.map((s) => ({
|
|
66
|
+
path: `${contentPath}/_sections/${s.key}.json`,
|
|
67
|
+
content: JSON.stringify(s.content, null, 2)
|
|
68
|
+
}))
|
|
69
|
+
];
|
|
70
|
+
const sectionsWithHtml = [...sections, ...edits].filter((s) => containsHtmlValue(s.content)).map((s) => s.key);
|
|
71
|
+
const projectPrefix = storage.projectPrefix;
|
|
72
|
+
for (const sectionKey of sectionsWithHtml) {
|
|
73
|
+
const componentPath = prefixPath(
|
|
74
|
+
`src/components/sections/${pascalCase(sectionKey)}Section.astro`,
|
|
75
|
+
projectPrefix ?? ""
|
|
76
|
+
);
|
|
77
|
+
if (files.some((f) => f.path === componentPath)) continue;
|
|
78
|
+
const original = await fetchFileContent(owner, repo, branch, componentPath, githubToken);
|
|
79
|
+
if (!original) continue;
|
|
80
|
+
const patched = convertToSetHtml(original);
|
|
81
|
+
if (patched !== original) {
|
|
82
|
+
files.push({ path: componentPath, content: patched });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const metaContentPath = serverConfig?.storage?.contentPath ?? "content";
|
|
86
|
+
const metaTarget = { owner, repo, branch, contentPath: metaContentPath, token: githubToken };
|
|
87
|
+
const metaSnapshot = await readPagesMeta(metaTarget);
|
|
88
|
+
if (metaSnapshot.ok) {
|
|
89
|
+
const nextMeta = setPageLastModified(metaSnapshot.value.meta, pageKey, Date.now());
|
|
90
|
+
files.push({
|
|
91
|
+
path: `${metaContentPath}/_pages-meta.json`,
|
|
92
|
+
content: JSON.stringify(nextMeta, null, 2)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const parts = [];
|
|
96
|
+
if (sections.length > 0) {
|
|
97
|
+
const keys = sections.map((s) => s.key).join(", ");
|
|
98
|
+
parts.push(`add ${sections.length} section${sections.length > 1 ? "s" : ""} (${keys})`);
|
|
99
|
+
}
|
|
100
|
+
if (edits.length > 0) {
|
|
101
|
+
const keys = edits.map((s) => s.key).join(", ");
|
|
102
|
+
parts.push(`update ${edits.length} section${edits.length > 1 ? "s" : ""} (${keys})`);
|
|
103
|
+
}
|
|
104
|
+
const editorEmail = parseSession(cookies.get("setzkasten_session")?.value)?.user?.email;
|
|
105
|
+
const commitMessage = withTrailers(
|
|
106
|
+
`content: ${parts.length > 0 ? parts.join(", ") : "update page config"} on ${pageKey}`,
|
|
107
|
+
editorEmail
|
|
108
|
+
);
|
|
109
|
+
const commitResult = await batchCommit(owner, repo, branch, files, commitMessage, headers);
|
|
110
|
+
if (!commitResult.ok) return Response.json({ error: commitResult.error }, { status: 500 });
|
|
111
|
+
const repoRoot = serverConfig?.repoRoot;
|
|
112
|
+
if (repoRoot) {
|
|
113
|
+
await Promise.all(
|
|
114
|
+
files.map((f) => writeFile(join(repoRoot, f.path), f.content, "utf-8").catch(() => {
|
|
115
|
+
}))
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const { fireWebhooks } = await import("./_webhook-dispatcher.js");
|
|
119
|
+
const parsedSession = parseSession(cookies.get("setzkasten_session")?.value);
|
|
120
|
+
void fireWebhooks(
|
|
121
|
+
"content.save",
|
|
122
|
+
{
|
|
123
|
+
website: { id: owner, repo: `${owner}/${repo}`, branch },
|
|
124
|
+
user: {
|
|
125
|
+
email: parsedSession?.user?.email ?? "unknown",
|
|
126
|
+
name: parsedSession?.user?.name
|
|
127
|
+
},
|
|
128
|
+
commit: { sha: commitResult.sha, message: `Commit on ${pageKey}` },
|
|
129
|
+
files: sections.map((s) => ({
|
|
130
|
+
path: `${metaContentPath}/_sections/${s.key}.json`
|
|
131
|
+
}))
|
|
132
|
+
},
|
|
133
|
+
request
|
|
134
|
+
);
|
|
135
|
+
return Response.json({ success: true, commitSha: commitResult.sha });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("[setzkasten] section-commit-pending error:", error);
|
|
138
|
+
return Response.json(
|
|
139
|
+
{ error: error instanceof Error ? error.message : "Commit failed" },
|
|
140
|
+
{ status: 500 }
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
function containsHtmlValue(value) {
|
|
145
|
+
if (typeof value === "string") return /<\/?[a-z]/i.test(value);
|
|
146
|
+
if (Array.isArray(value)) return value.some(containsHtmlValue);
|
|
147
|
+
if (value && typeof value === "object") return Object.values(value).some(containsHtmlValue);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
function pascalCase(input) {
|
|
151
|
+
return input.split(/[-_\s]+/).filter(Boolean).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
152
|
+
}
|
|
153
|
+
async function fetchFileContent(owner, repo, branch, path, token) {
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(
|
|
156
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
157
|
+
{
|
|
158
|
+
headers: {
|
|
159
|
+
Authorization: `Bearer ${token}`,
|
|
160
|
+
Accept: "application/vnd.github+json",
|
|
161
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
if (!res.ok) return null;
|
|
166
|
+
const data = await res.json();
|
|
167
|
+
return data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function batchCommit(owner, repo, branch, files, message, headers) {
|
|
173
|
+
try {
|
|
174
|
+
const refRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, { headers });
|
|
175
|
+
if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` };
|
|
176
|
+
const { object: { sha: headSha } } = await refRes.json();
|
|
177
|
+
const commitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`, { headers });
|
|
178
|
+
if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` };
|
|
179
|
+
const { tree: { sha: baseSha } } = await commitRes.json();
|
|
180
|
+
const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers,
|
|
183
|
+
body: JSON.stringify({ base_tree: baseSha, tree: files.map((f) => ({ path: f.path, mode: "100644", type: "blob", content: f.content })) })
|
|
184
|
+
});
|
|
185
|
+
if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` };
|
|
186
|
+
const { sha: treeSha } = await treeRes.json();
|
|
187
|
+
const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers,
|
|
190
|
+
body: JSON.stringify({ tree: treeSha, parents: [headSha], message })
|
|
191
|
+
});
|
|
192
|
+
if (!newCommitRes.ok) return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` };
|
|
193
|
+
const { sha: newSha } = await newCommitRes.json();
|
|
194
|
+
const updateRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
|
|
195
|
+
method: "PATCH",
|
|
196
|
+
headers,
|
|
197
|
+
body: JSON.stringify({ sha: newSha })
|
|
198
|
+
});
|
|
199
|
+
if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` };
|
|
200
|
+
return { ok: true, sha: newSha };
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return { ok: false, error: error instanceof Error ? error.message : "Commit failed" };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export {
|
|
206
|
+
POST
|
|
207
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DELETE /api/setzkasten/sections
|
|
5
|
+
*
|
|
6
|
+
* Removes a section from a page:
|
|
7
|
+
* - Deletes the content JSON (_sections/{key}.json)
|
|
8
|
+
* - Removes the entry from the page config (pages/_{pageKey}.json)
|
|
9
|
+
* All changes committed as a single batch.
|
|
10
|
+
*
|
|
11
|
+
* Body: { pageKey, sectionKey, owner?, repo?, branch?, contentPath? }
|
|
12
|
+
*/
|
|
13
|
+
declare const DELETE: APIRoute;
|
|
14
|
+
|
|
15
|
+
export { DELETE };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
removeFromPageConfig
|
|
3
|
+
} from "../chunk-GHNK2GFE.js";
|
|
4
|
+
import {
|
|
5
|
+
guardPageAccess,
|
|
6
|
+
parseSession
|
|
7
|
+
} from "../chunk-INIWFKQ3.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveStorageConfigForRequest
|
|
10
|
+
} from "../chunk-6UIKVKED.js";
|
|
11
|
+
import "../chunk-5PIMDP4N.js";
|
|
12
|
+
import "../chunk-45ARVNT3.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveGitHubTokenForRequest
|
|
15
|
+
} from "../chunk-NKDATSPA.js";
|
|
16
|
+
import "../chunk-TJNJKPUL.js";
|
|
17
|
+
import {
|
|
18
|
+
withTrailers
|
|
19
|
+
} from "../chunk-KH22FJO5.js";
|
|
20
|
+
|
|
21
|
+
// src/api-routes/section-delete.ts
|
|
22
|
+
var DELETE = async ({ request, cookies }) => {
|
|
23
|
+
const session = cookies.get("setzkasten_session")?.value;
|
|
24
|
+
if (!session) return new Response("Unauthorized", { status: 401 });
|
|
25
|
+
const tokenResult = await resolveGitHubTokenForRequest(request);
|
|
26
|
+
if (!tokenResult.ok) {
|
|
27
|
+
return new Response(tokenResult.error.message, { status: 500 });
|
|
28
|
+
}
|
|
29
|
+
const githubToken = tokenResult.value;
|
|
30
|
+
try {
|
|
31
|
+
const body = await request.json();
|
|
32
|
+
const storage = await resolveStorageConfigForRequest(request, body);
|
|
33
|
+
if (!storage) return Response.json({ error: "Could not resolve owner/repo" }, { status: 400 });
|
|
34
|
+
const { owner, repo, branch, projectPrefix } = storage;
|
|
35
|
+
const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
|
|
36
|
+
const fullConfig = globalThis.__SETZKASTEN_FULL_CONFIG__;
|
|
37
|
+
const contentPath = body.contentPath || serverConfig?.storage?.contentPath || "content";
|
|
38
|
+
const { pageKey, sectionKey } = body;
|
|
39
|
+
if (!pageKey || !sectionKey) {
|
|
40
|
+
return Response.json({ error: "pageKey and sectionKey are required" }, { status: 400 });
|
|
41
|
+
}
|
|
42
|
+
const denied = await guardPageAccess(parseSession(cookies.get("setzkasten_session")?.value), pageKey, fullConfig, request);
|
|
43
|
+
if (denied) return denied;
|
|
44
|
+
const headers = {
|
|
45
|
+
Authorization: `Bearer ${githubToken}`,
|
|
46
|
+
Accept: "application/vnd.github+json",
|
|
47
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
48
|
+
"Content-Type": "application/json"
|
|
49
|
+
};
|
|
50
|
+
const configKey = "_" + pageKey.replace(/\//g, "_");
|
|
51
|
+
const pageConfigPath = `${contentPath}/pages/${configKey}.json`;
|
|
52
|
+
const pageConfigRaw = await fetchFileContent(owner, repo, branch, pageConfigPath, githubToken);
|
|
53
|
+
if (!pageConfigRaw) return Response.json({ error: "Page config not found" }, { status: 404 });
|
|
54
|
+
const pageConfig = JSON.parse(pageConfigRaw);
|
|
55
|
+
const updatedConfig = removeFromPageConfig(pageConfig, sectionKey);
|
|
56
|
+
const sectionJsonPath = `${contentPath}/_sections/${sectionKey}.json`;
|
|
57
|
+
const commitResult = await batchCommitWithDeletions(
|
|
58
|
+
owner,
|
|
59
|
+
repo,
|
|
60
|
+
branch,
|
|
61
|
+
[{ path: pageConfigPath, content: JSON.stringify(updatedConfig, null, 2) }],
|
|
62
|
+
[sectionJsonPath],
|
|
63
|
+
withTrailers(
|
|
64
|
+
`content: remove ${sectionKey} section from ${pageKey}`,
|
|
65
|
+
parseSession(cookies.get("setzkasten_session")?.value)?.user?.email
|
|
66
|
+
),
|
|
67
|
+
headers
|
|
68
|
+
);
|
|
69
|
+
if (!commitResult.ok) return Response.json({ error: commitResult.error }, { status: 500 });
|
|
70
|
+
const { recordPageEdit } = await import("./_pages-meta-store.js");
|
|
71
|
+
await recordPageEdit(
|
|
72
|
+
{ owner, repo, branch, contentPath, token: tokenResult.value },
|
|
73
|
+
pageKey
|
|
74
|
+
).catch(() => {
|
|
75
|
+
});
|
|
76
|
+
const { fireWebhooks } = await import("./_webhook-dispatcher.js");
|
|
77
|
+
const session2 = parseSession(cookies.get("setzkasten_session")?.value);
|
|
78
|
+
void fireWebhooks(
|
|
79
|
+
"content.delete",
|
|
80
|
+
{
|
|
81
|
+
website: { id: owner, repo: `${owner}/${repo}`, branch },
|
|
82
|
+
user: { email: session2?.user?.email ?? "unknown", name: session2?.user?.name },
|
|
83
|
+
commit: { sha: commitResult.sha, message: `Delete ${sectionKey} from ${pageKey}` },
|
|
84
|
+
files: [{ path: sectionJsonPath }]
|
|
85
|
+
},
|
|
86
|
+
request
|
|
87
|
+
);
|
|
88
|
+
return Response.json({ success: true, commitSha: commitResult.sha });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("[setzkasten] section-delete error:", error);
|
|
91
|
+
return Response.json(
|
|
92
|
+
{ error: error instanceof Error ? error.message : "Delete failed" },
|
|
93
|
+
{ status: 500 }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
async function fetchFileContent(owner, repo, branch, path, token) {
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(
|
|
100
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
101
|
+
{ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" } }
|
|
102
|
+
);
|
|
103
|
+
if (!res.ok) return null;
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
return data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function batchCommitWithDeletions(owner, repo, branch, upserts, deletions, message, headers) {
|
|
111
|
+
try {
|
|
112
|
+
const refRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, { headers });
|
|
113
|
+
if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` };
|
|
114
|
+
const { object: { sha: headSha } } = await refRes.json();
|
|
115
|
+
const commitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`, { headers });
|
|
116
|
+
if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` };
|
|
117
|
+
const { tree: { sha: baseSha } } = await commitRes.json();
|
|
118
|
+
const tree = [
|
|
119
|
+
...upserts.map((f) => ({ path: f.path, mode: "100644", type: "blob", content: f.content })),
|
|
120
|
+
...deletions.map((path) => ({ path, mode: "100644", type: "blob", sha: null }))
|
|
121
|
+
];
|
|
122
|
+
const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers,
|
|
125
|
+
body: JSON.stringify({ base_tree: baseSha, tree })
|
|
126
|
+
});
|
|
127
|
+
if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` };
|
|
128
|
+
const { sha: treeSha } = await treeRes.json();
|
|
129
|
+
const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers,
|
|
132
|
+
body: JSON.stringify({ tree: treeSha, parents: [headSha], message })
|
|
133
|
+
});
|
|
134
|
+
if (!newCommitRes.ok) return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` };
|
|
135
|
+
const { sha: newSha } = await newCommitRes.json();
|
|
136
|
+
const updateRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
|
|
137
|
+
method: "PATCH",
|
|
138
|
+
headers,
|
|
139
|
+
body: JSON.stringify({ sha: newSha })
|
|
140
|
+
});
|
|
141
|
+
if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` };
|
|
142
|
+
return { ok: true, sha: newSha };
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return { ok: false, error: error instanceof Error ? error.message : "Commit failed" };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export {
|
|
148
|
+
DELETE
|
|
149
|
+
};
|