@setzkasten-cms/astro-admin 1.4.0 → 1.4.1

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.
Files changed (153) hide show
  1. package/dist/api-routes/_auth-guard.d.ts +47 -0
  2. package/dist/api-routes/_auth-guard.js +18 -0
  3. package/dist/api-routes/_commit-trailers.d.ts +8 -0
  4. package/dist/api-routes/_commit-trailers.js +8 -0
  5. package/dist/api-routes/_feature-gate.d.ts +23 -0
  6. package/dist/api-routes/_feature-gate.js +7 -0
  7. package/dist/api-routes/_github-cache.d.ts +4 -0
  8. package/dist/api-routes/_github-cache.js +8 -0
  9. package/dist/api-routes/_github-token.d.ts +27 -0
  10. package/dist/api-routes/_github-token.js +8 -0
  11. package/dist/api-routes/_license-tier.d.ts +22 -0
  12. package/dist/api-routes/_license-tier.js +6 -0
  13. package/dist/api-routes/_pages-meta-store.d.ts +32 -0
  14. package/dist/api-routes/_pages-meta-store.js +9 -0
  15. package/dist/api-routes/_role-resolver.d.ts +15 -0
  16. package/dist/api-routes/_role-resolver.js +13 -0
  17. package/dist/api-routes/_session-cookie.d.ts +18 -0
  18. package/dist/api-routes/_session-cookie.js +6 -0
  19. package/dist/api-routes/_storage-config.d.ts +60 -0
  20. package/dist/api-routes/_storage-config.js +10 -0
  21. package/dist/api-routes/_vercel-origin.d.ts +16 -0
  22. package/dist/api-routes/_vercel-origin.js +6 -0
  23. package/dist/api-routes/_webhook-dispatcher.d.ts +13 -0
  24. package/dist/api-routes/_webhook-dispatcher.js +97 -0
  25. package/dist/api-routes/_webhook-signing.d.ts +11 -0
  26. package/dist/api-routes/_webhook-signing.js +6 -0
  27. package/dist/api-routes/_webhook-status-store.d.ts +19 -0
  28. package/dist/api-routes/_webhook-status-store.js +10 -0
  29. package/dist/api-routes/_website-resolver.d.ts +49 -0
  30. package/dist/api-routes/_website-resolver.js +14 -0
  31. package/dist/api-routes/_websites-store.d.ts +30 -0
  32. package/dist/api-routes/_websites-store.js +11 -0
  33. package/dist/api-routes/asset-proxy.d.ts +12 -0
  34. package/dist/api-routes/asset-proxy.js +67 -0
  35. package/dist/api-routes/auth-callback.d.ts +9 -0
  36. package/dist/api-routes/auth-callback.js +68 -0
  37. package/dist/api-routes/auth-login.d.ts +11 -0
  38. package/dist/api-routes/auth-login.js +27 -0
  39. package/dist/api-routes/auth-logout.d.ts +10 -0
  40. package/dist/api-routes/auth-logout.js +13 -0
  41. package/dist/api-routes/auth-session.d.ts +9 -0
  42. package/dist/api-routes/auth-session.js +31 -0
  43. package/dist/api-routes/auth-setzkasten-login.d.ts +18 -0
  44. package/dist/api-routes/auth-setzkasten-login.js +74 -0
  45. package/dist/api-routes/catalog-add.d.ts +14 -0
  46. package/dist/api-routes/catalog-add.js +153 -0
  47. package/dist/api-routes/catalog-export.d.ts +13 -0
  48. package/dist/api-routes/catalog-export.js +71 -0
  49. package/dist/api-routes/catalog-helpers.d.ts +41 -0
  50. package/dist/api-routes/catalog-helpers.js +11 -0
  51. package/dist/api-routes/catalog-list.d.ts +11 -0
  52. package/dist/api-routes/catalog-list.js +12 -0
  53. package/dist/api-routes/config.d.ts +12 -0
  54. package/dist/api-routes/config.js +43 -0
  55. package/dist/api-routes/deploy-hook.d.ts +14 -0
  56. package/dist/api-routes/deploy-hook.js +52 -0
  57. package/dist/api-routes/editors.d.ts +29 -0
  58. package/dist/api-routes/editors.js +18 -0
  59. package/dist/api-routes/github-proxy.d.ts +12 -0
  60. package/dist/api-routes/github-proxy.js +82 -0
  61. package/dist/api-routes/global-config.d.ts +20 -0
  62. package/dist/api-routes/global-config.js +19 -0
  63. package/dist/api-routes/history-rollback.d.ts +22 -0
  64. package/dist/api-routes/history-rollback.js +111 -0
  65. package/dist/api-routes/history-version.d.ts +11 -0
  66. package/dist/api-routes/history-version.js +57 -0
  67. package/dist/api-routes/history.d.ts +13 -0
  68. package/dist/api-routes/history.js +85 -0
  69. package/dist/api-routes/icons-local.d.ts +28 -0
  70. package/dist/api-routes/icons-local.js +115 -0
  71. package/dist/api-routes/init-add-section.d.ts +23 -0
  72. package/dist/api-routes/init-add-section.js +396 -0
  73. package/dist/api-routes/init-apply.d.ts +11 -0
  74. package/dist/api-routes/init-apply.js +266 -0
  75. package/dist/api-routes/init-migrate.d.ts +16 -0
  76. package/dist/api-routes/init-migrate.js +205 -0
  77. package/dist/api-routes/init-scan-page.d.ts +39 -0
  78. package/dist/api-routes/init-scan-page.js +260 -0
  79. package/dist/api-routes/init-scan.d.ts +11 -0
  80. package/dist/api-routes/init-scan.js +128 -0
  81. package/dist/api-routes/migrate-to-multi.d.ts +26 -0
  82. package/dist/api-routes/migrate-to-multi.js +188 -0
  83. package/dist/api-routes/pages.d.ts +39 -0
  84. package/dist/api-routes/pages.js +88 -0
  85. package/dist/api-routes/section-add.d.ts +18 -0
  86. package/dist/api-routes/section-add.js +173 -0
  87. package/dist/api-routes/section-commit-pending.d.ts +18 -0
  88. package/dist/api-routes/section-commit-pending.js +207 -0
  89. package/dist/api-routes/section-delete.d.ts +15 -0
  90. package/dist/api-routes/section-delete.js +149 -0
  91. package/dist/api-routes/section-duplicate.d.ts +15 -0
  92. package/dist/api-routes/section-duplicate.js +143 -0
  93. package/dist/api-routes/section-management.d.ts +41 -0
  94. package/dist/api-routes/section-management.js +14 -0
  95. package/dist/api-routes/section-prepare-copy.d.ts +25 -0
  96. package/dist/api-routes/section-prepare-copy.js +69 -0
  97. package/dist/api-routes/section-prepare.d.ts +18 -0
  98. package/dist/api-routes/section-prepare.js +104 -0
  99. package/dist/api-routes/setup-github-app-bounce.d.ts +13 -0
  100. package/dist/api-routes/setup-github-app-bounce.js +45 -0
  101. package/dist/api-routes/setup-github-app-branches.d.ts +14 -0
  102. package/dist/api-routes/setup-github-app-branches.js +58 -0
  103. package/dist/api-routes/setup-github-app-callback.d.ts +15 -0
  104. package/dist/api-routes/setup-github-app-callback.js +45 -0
  105. package/dist/api-routes/setup-github-app-installed.d.ts +15 -0
  106. package/dist/api-routes/setup-github-app-installed.js +33 -0
  107. package/dist/api-routes/setup-github-app-repos.d.ts +17 -0
  108. package/dist/api-routes/setup-github-app-repos.js +41 -0
  109. package/dist/api-routes/setup-github-app.d.ts +15 -0
  110. package/dist/api-routes/setup-github-app.js +41 -0
  111. package/dist/api-routes/updater-check.d.ts +10 -0
  112. package/dist/api-routes/updater-check.js +37 -0
  113. package/dist/api-routes/updater-register.d.ts +14 -0
  114. package/dist/api-routes/updater-register.js +71 -0
  115. package/dist/api-routes/updater-transfer.d.ts +11 -0
  116. package/dist/api-routes/updater-transfer.js +37 -0
  117. package/dist/api-routes/updater-unbind.d.ts +17 -0
  118. package/dist/api-routes/updater-unbind.js +35 -0
  119. package/dist/api-routes/webhooks-status.d.ts +12 -0
  120. package/dist/api-routes/webhooks-status.js +22 -0
  121. package/dist/api-routes/webhooks-test.d.ts +13 -0
  122. package/dist/api-routes/webhooks-test.js +124 -0
  123. package/dist/api-routes/webhooks.d.ts +6 -0
  124. package/dist/api-routes/webhooks.js +148 -0
  125. package/dist/api-routes/websites-add.d.ts +15 -0
  126. package/dist/api-routes/websites-add.js +92 -0
  127. package/dist/api-routes/websites-list.d.ts +12 -0
  128. package/dist/api-routes/websites-list.js +35 -0
  129. package/dist/api-routes/websites-remove.d.ts +15 -0
  130. package/dist/api-routes/websites-remove.js +69 -0
  131. package/dist/chunk-35S35OIV.js +80 -0
  132. package/dist/chunk-45ARVNT3.js +25 -0
  133. package/dist/chunk-5PIMDP4N.js +25 -0
  134. package/dist/chunk-5ZFTG4BW.js +10 -0
  135. package/dist/chunk-6UIKVKED.js +51 -0
  136. package/dist/chunk-737TIZRU.js +9 -0
  137. package/dist/chunk-AM4DZXXM.js +120 -0
  138. package/dist/chunk-FXNOTESI.js +87 -0
  139. package/dist/chunk-GHNK2GFE.js +48 -0
  140. package/dist/chunk-GRG3LNKH.js +37 -0
  141. package/dist/chunk-INIWFKQ3.js +236 -0
  142. package/dist/chunk-JHY6XTLL.js +24 -0
  143. package/dist/chunk-K22A4ZBS.js +1574 -0
  144. package/dist/chunk-KH22FJO5.js +17 -0
  145. package/dist/chunk-NKDATSPA.js +43 -0
  146. package/dist/chunk-RHJONMLK.js +1267 -0
  147. package/dist/chunk-TJNJKPUL.js +11 -0
  148. package/dist/chunk-V6IMPVF3.js +120 -0
  149. package/dist/chunk-W3QHY5GW.js +19 -0
  150. package/dist/chunk-ZQDGGWJP.js +43 -0
  151. package/package.json +249 -53
  152. package/src/api-routes/__tests__/route-registry.test.ts +7 -1
  153. package/tsconfig.json +0 -9
@@ -0,0 +1,260 @@
1
+ import {
2
+ analyzeAstroSection,
3
+ extractLayoutImport,
4
+ extractSectionImports
5
+ } from "../chunk-K22A4ZBS.js";
6
+ import {
7
+ prefixPath,
8
+ resolveStorageConfigForRequest
9
+ } from "../chunk-6UIKVKED.js";
10
+ import {
11
+ resolveGitHubTokenForRequest
12
+ } from "../chunk-NKDATSPA.js";
13
+
14
+ // src/api-routes/init-scan-page.ts
15
+ function resolveFullConfig() {
16
+ const buildConfig = typeof __SETZKASTEN_FULL_CONFIG__ !== "undefined" ? __SETZKASTEN_FULL_CONFIG__ : null;
17
+ return buildConfig ?? globalThis.__SETZKASTEN_FULL_CONFIG__;
18
+ }
19
+ var POST = async ({ request, cookies }) => {
20
+ const session = cookies.get("setzkasten_session")?.value;
21
+ if (!session) {
22
+ return new Response("Unauthorized", { status: 401 });
23
+ }
24
+ const tokenResult = await resolveGitHubTokenForRequest(request);
25
+ if (!tokenResult.ok) {
26
+ return new Response(tokenResult.error.message, { status: 500 });
27
+ }
28
+ const githubToken = tokenResult.value;
29
+ try {
30
+ const body = await request.json();
31
+ const storage = await resolveStorageConfigForRequest(request, body);
32
+ if (!storage) {
33
+ return Response.json({ error: "Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars." }, { status: 400 });
34
+ }
35
+ const { owner, repo, branch, projectPrefix } = storage;
36
+ const { pagePath } = body;
37
+ if (!pagePath) {
38
+ return Response.json({ error: "pagePath is required" }, { status: 400 });
39
+ }
40
+ const config = resolveFullConfig();
41
+ const managedSections = /* @__PURE__ */ new Map();
42
+ if (config) {
43
+ for (const product of Object.values(config.products)) {
44
+ for (const [key, section] of Object.entries(product.sections)) {
45
+ managedSections.set(key, new Set(Object.keys(section.fields)));
46
+ }
47
+ }
48
+ }
49
+ const treeResponse = await githubFetch(
50
+ `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`,
51
+ githubToken
52
+ );
53
+ if (!treeResponse.ok) {
54
+ return Response.json(
55
+ { error: `Failed to fetch repo tree: ${treeResponse.status}` },
56
+ { status: treeResponse.status }
57
+ );
58
+ }
59
+ const treeData = await treeResponse.json();
60
+ const files = treeData.tree.map((item) => ({
61
+ path: item.path,
62
+ type: item.type
63
+ }));
64
+ let fullPagePath = prefixPath(pagePath, projectPrefix);
65
+ let pageSource = await fetchFileContent(owner, repo, branch, fullPagePath, githubToken);
66
+ if (!pageSource && pagePath.endsWith(".astro")) {
67
+ const indexPath = pagePath.replace(/\.astro$/, "/index.astro");
68
+ fullPagePath = prefixPath(indexPath, projectPrefix);
69
+ pageSource = await fetchFileContent(owner, repo, branch, fullPagePath, githubToken);
70
+ }
71
+ if (!pageSource) {
72
+ return Response.json({ error: `Could not read page source: ${fullPagePath}` }, { status: 404 });
73
+ }
74
+ const imports = extractSectionImports(pageSource, fullPagePath, files, projectPrefix);
75
+ const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
76
+ const contentPath = serverConfig?.storage?.contentPath || "content";
77
+ const unmanagedSections = [];
78
+ const managedUpdates = [];
79
+ for (const imp of imports) {
80
+ if (!imp.resolvedPath) continue;
81
+ if (!managedSections.has(imp.sectionKey)) {
82
+ const sectionSource = await fetchFileContent(owner, repo, branch, imp.resolvedPath, githubToken);
83
+ if (!sectionSource) continue;
84
+ const section = await analyzeAstroSection(
85
+ sectionSource,
86
+ imp.sectionKey,
87
+ imp.componentName,
88
+ imp.resolvedPath
89
+ );
90
+ unmanagedSections.push(section);
91
+ } else {
92
+ const existingFieldKeys = managedSections.get(imp.sectionKey);
93
+ const sectionSource = await fetchFileContent(owner, repo, branch, imp.resolvedPath, githubToken);
94
+ if (!sectionSource) continue;
95
+ const inferred = await analyzeAstroSection(
96
+ sectionSource,
97
+ imp.sectionKey,
98
+ imp.componentName,
99
+ imp.resolvedPath
100
+ );
101
+ const sectionJsonPath = `${contentPath}/_sections/${imp.sectionKey}.json`;
102
+ const sectionJson = await fetchFileContent(owner, repo, branch, sectionJsonPath, githubToken);
103
+ const existingValues = /* @__PURE__ */ new Set();
104
+ if (sectionJson) {
105
+ try {
106
+ const data = JSON.parse(sectionJson);
107
+ for (const val of Object.values(data)) {
108
+ if (typeof val === "string" && val.length >= 2) existingValues.add(val);
109
+ }
110
+ } catch {
111
+ }
112
+ }
113
+ const missingFields = inferred.fields.filter((f) => {
114
+ if (existingFieldKeys.has(f.key)) return false;
115
+ if (typeof f.defaultValue === "string" && existingValues.has(f.defaultValue)) return false;
116
+ return true;
117
+ });
118
+ if (missingFields.length > 0) {
119
+ managedUpdates.push({
120
+ key: imp.sectionKey,
121
+ componentName: imp.componentName,
122
+ componentPath: imp.resolvedPath,
123
+ missingFields,
124
+ allFields: inferred.fields
125
+ });
126
+ }
127
+ }
128
+ }
129
+ {
130
+ const pageKeyNorm = pagePath.replace(/^src\/pages\//, "").replace(/\/(index)?\.astro$/, "").replace(/\.astro$/, "") || "index";
131
+ const sectionKey = "_page_" + pageKeyNorm.replace(/\//g, "_");
132
+ const pageSection = await analyzeAstroSection(
133
+ pageSource,
134
+ sectionKey,
135
+ pageKeyNorm.replace(/\//g, "_") + "Page",
136
+ fullPagePath,
137
+ { mode: "page" }
138
+ );
139
+ pageSection.isPageLevel = true;
140
+ if (!managedSections.has(sectionKey)) {
141
+ if (pageSection.fields.length > 0) {
142
+ unmanagedSections.push(pageSection);
143
+ }
144
+ } else {
145
+ const existingFieldKeys = managedSections.get(sectionKey);
146
+ const sectionJsonPath = `${contentPath}/_sections/${sectionKey}.json`;
147
+ const sectionJson = await fetchFileContent(owner, repo, branch, sectionJsonPath, githubToken);
148
+ const existingValues = /* @__PURE__ */ new Set();
149
+ if (sectionJson) {
150
+ try {
151
+ const data = JSON.parse(sectionJson);
152
+ for (const val of Object.values(data)) {
153
+ if (typeof val === "string" && val.length >= 2) existingValues.add(val);
154
+ }
155
+ } catch {
156
+ }
157
+ }
158
+ const missingFields = pageSection.fields.filter((f) => {
159
+ if (existingFieldKeys.has(f.key)) return false;
160
+ if (typeof f.defaultValue === "string" && existingValues.has(f.defaultValue)) return false;
161
+ return true;
162
+ });
163
+ if (missingFields.length > 0) {
164
+ managedUpdates.push({
165
+ key: sectionKey,
166
+ componentName: pageKeyNorm.replace(/\//g, "_") + "Page",
167
+ componentPath: fullPagePath,
168
+ missingFields,
169
+ allFields: pageSection.fields
170
+ });
171
+ }
172
+ }
173
+ }
174
+ {
175
+ const layoutImport = extractLayoutImport(pageSource, fullPagePath, files, projectPrefix);
176
+ if (layoutImport?.resolvedPath) {
177
+ const layoutSource = await fetchFileContent(owner, repo, branch, layoutImport.resolvedPath, githubToken);
178
+ if (layoutSource) {
179
+ for (const region of extractLayoutRegions(layoutSource)) {
180
+ const sectionKey = `_layout_${region.name}`;
181
+ if (managedSections.has(sectionKey)) continue;
182
+ const regionSource = `---
183
+ ---
184
+
185
+ ${region.html}`;
186
+ const section = await analyzeAstroSection(
187
+ regionSource,
188
+ sectionKey,
189
+ region.name,
190
+ layoutImport.resolvedPath,
191
+ { mode: "page" }
192
+ );
193
+ section.isPageLevel = true;
194
+ section.isLayoutRegion = true;
195
+ section.layoutPath = layoutImport.resolvedPath;
196
+ section.regionTag = region.tag;
197
+ if (section.fields.length > 0) {
198
+ unmanagedSections.push(section);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+ return Response.json({ unmanagedSections, managedUpdates });
205
+ } catch (error) {
206
+ console.error("[setzkasten] scan-page error:", error);
207
+ return Response.json(
208
+ { error: error instanceof Error ? error.message : "Scan failed" },
209
+ { status: 500 }
210
+ );
211
+ }
212
+ };
213
+ async function githubFetch(url, token) {
214
+ return fetch(url, {
215
+ headers: {
216
+ Authorization: `Bearer ${token}`,
217
+ Accept: "application/vnd.github+json",
218
+ "X-GitHub-Api-Version": "2022-11-28"
219
+ }
220
+ });
221
+ }
222
+ function extractLayoutRegions(layoutSource) {
223
+ let template = layoutSource;
224
+ if (layoutSource.startsWith("---")) {
225
+ const endIdx = layoutSource.indexOf("\n---", 3);
226
+ if (endIdx !== -1) {
227
+ template = layoutSource.slice(endIdx + 4);
228
+ }
229
+ }
230
+ const regions = [];
231
+ for (const tag of ["header", "footer"]) {
232
+ const openTag = new RegExp(`<${tag}(\\s[^>]*)?>`, "i");
233
+ const openMatch = openTag.exec(template);
234
+ if (!openMatch) continue;
235
+ const closeTag = `</${tag}>`;
236
+ const closeIdx = template.indexOf(closeTag, openMatch.index);
237
+ if (closeIdx === -1) continue;
238
+ const html = template.slice(openMatch.index, closeIdx + closeTag.length);
239
+ regions.push({ name: tag, tag, html });
240
+ }
241
+ return regions;
242
+ }
243
+ async function fetchFileContent(owner, repo, branch, path, token) {
244
+ try {
245
+ const response = await githubFetch(
246
+ `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
247
+ token
248
+ );
249
+ if (!response.ok) return null;
250
+ const data = await response.json();
251
+ return data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ export {
257
+ POST,
258
+ extractLayoutRegions,
259
+ resolveFullConfig
260
+ };
@@ -0,0 +1,11 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ /**
4
+ * POST /api/setzkasten/init/scan
5
+ *
6
+ * Scans a GitHub repo and returns project analysis + detected sections.
7
+ * Body: { owner: string, repo: string, branch?: string }
8
+ */
9
+ declare const POST: APIRoute;
10
+
11
+ export { POST };
@@ -0,0 +1,128 @@
1
+ import {
2
+ analyzeAstroSection,
3
+ extractSectionImports,
4
+ findAstroPages
5
+ } from "../chunk-K22A4ZBS.js";
6
+ import {
7
+ resolveGitHubTokenForRequest
8
+ } from "../chunk-NKDATSPA.js";
9
+
10
+ // src/api-routes/init-scan.ts
11
+ import { analyzeProject } from "@setzkasten-cms/core/init";
12
+ var POST = async ({ request, cookies }) => {
13
+ const session = cookies.get("setzkasten_session")?.value;
14
+ if (!session) {
15
+ return new Response("Unauthorized", { status: 401 });
16
+ }
17
+ const tokenResult = await resolveGitHubTokenForRequest(request);
18
+ if (!tokenResult.ok) {
19
+ return new Response(tokenResult.error.message, { status: 500 });
20
+ }
21
+ const githubToken = tokenResult.value;
22
+ try {
23
+ const body = await request.json();
24
+ const { owner, repo, branch = "main" } = body;
25
+ if (!owner || !repo) {
26
+ return Response.json({ error: "owner and repo are required" }, { status: 400 });
27
+ }
28
+ const treeResponse = await githubFetch(
29
+ `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`,
30
+ githubToken
31
+ );
32
+ if (!treeResponse.ok) {
33
+ return Response.json(
34
+ { error: `Failed to fetch repo tree: ${treeResponse.status}` },
35
+ { status: treeResponse.status }
36
+ );
37
+ }
38
+ const treeData = await treeResponse.json();
39
+ const files = treeData.tree.map((item) => ({
40
+ path: item.path,
41
+ type: item.type
42
+ }));
43
+ const analysis = analyzeProject(files);
44
+ const astroRoot = analysis.projectRoots.find((r) => r.framework === "astro");
45
+ if (!astroRoot) {
46
+ return Response.json({
47
+ analysis,
48
+ pages: [],
49
+ sections: [],
50
+ astroConfigPath: null,
51
+ message: "Kein Astro-Projekt gefunden"
52
+ });
53
+ }
54
+ const pages = findAstroPages(files, astroRoot.path);
55
+ const allSections = /* @__PURE__ */ new Map();
56
+ const pageSections = {};
57
+ for (const page of pages) {
58
+ const pageSource = await fetchFileContent(owner, repo, branch, page.filePath, githubToken);
59
+ if (!pageSource) continue;
60
+ const imports = extractSectionImports(pageSource, page.filePath, files, astroRoot.path);
61
+ pageSections[page.pageKey] = imports.map((i) => i.sectionKey);
62
+ for (const imp of imports) {
63
+ if (allSections.has(imp.sectionKey)) continue;
64
+ if (!imp.resolvedPath) continue;
65
+ const sectionSource = await fetchFileContent(owner, repo, branch, imp.resolvedPath, githubToken);
66
+ if (!sectionSource) continue;
67
+ const section = await analyzeAstroSection(
68
+ sectionSource,
69
+ imp.sectionKey,
70
+ imp.componentName,
71
+ imp.resolvedPath
72
+ );
73
+ allSections.set(imp.sectionKey, section);
74
+ }
75
+ }
76
+ const astroConfigCandidates = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
77
+ let astroConfigPath = null;
78
+ for (const candidate of astroConfigCandidates) {
79
+ const path = astroRoot.path ? `${astroRoot.path}/${candidate}` : candidate;
80
+ if (files.some((f) => f.path === path)) {
81
+ astroConfigPath = path;
82
+ break;
83
+ }
84
+ }
85
+ return Response.json({
86
+ analysis,
87
+ projectRoot: astroRoot.path,
88
+ pages,
89
+ pageSections,
90
+ sections: Object.fromEntries(allSections),
91
+ astroConfigPath
92
+ });
93
+ } catch (error) {
94
+ console.error("[setzkasten] Init scan error:", error);
95
+ return Response.json(
96
+ { error: error instanceof Error ? error.message : "Scan failed" },
97
+ { status: 500 }
98
+ );
99
+ }
100
+ };
101
+ async function githubFetch(url, token) {
102
+ return fetch(url, {
103
+ headers: {
104
+ Authorization: `Bearer ${token}`,
105
+ Accept: "application/vnd.github+json",
106
+ "X-GitHub-Api-Version": "2022-11-28"
107
+ }
108
+ });
109
+ }
110
+ async function fetchFileContent(owner, repo, branch, path, token) {
111
+ try {
112
+ const response = await githubFetch(
113
+ `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
114
+ token
115
+ );
116
+ if (!response.ok) return null;
117
+ const data = await response.json();
118
+ if (data.encoding === "base64") {
119
+ return Buffer.from(data.content, "base64").toString("utf-8");
120
+ }
121
+ return data.content;
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+ export {
127
+ POST
128
+ };
@@ -0,0 +1,26 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ /**
4
+ * POST /api/setzkasten/migrate/to-multi
5
+ *
6
+ * Body: { configRepo: 'owner/repo', configInstallationId: string,
7
+ * previewOrigin?: string }
8
+ *
9
+ * Admin-only. Expects the deployer to have already (1) created the
10
+ * config repo and (2) installed the GitHub App on it. The endpoint then:
11
+ *
12
+ * 1. Reads `_editors.json` and `_global_config.json` from the current
13
+ * single-mode website repo.
14
+ * 2. Writes both files into `<config-repo>/content/`.
15
+ * 3. Initialises `<config-repo>/websites.json` with a single entry that
16
+ * snapshots the current single-mode setup (repo, branch, preview
17
+ * origin, App-Installation-ID).
18
+ *
19
+ * After a 200 response the deployer still has to update
20
+ * `setzkasten.config.ts` (kind: 'single' → 'multi') and the ENV
21
+ * variables and redeploy. The wizard surfaces a copy-ready diff for
22
+ * those manual steps.
23
+ */
24
+ declare const POST: APIRoute;
25
+
26
+ export { POST };
@@ -0,0 +1,188 @@
1
+ import {
2
+ parseSession
3
+ } from "../chunk-INIWFKQ3.js";
4
+ import {
5
+ resolveStorageConfig
6
+ } from "../chunk-6UIKVKED.js";
7
+ import "../chunk-5PIMDP4N.js";
8
+ import "../chunk-45ARVNT3.js";
9
+ import {
10
+ resolveConfigRepoToken
11
+ } from "../chunk-NKDATSPA.js";
12
+ import {
13
+ resolveLicenseTier
14
+ } from "../chunk-TJNJKPUL.js";
15
+ import {
16
+ withTrailers
17
+ } from "../chunk-KH22FJO5.js";
18
+
19
+ // src/api-routes/migrate-to-multi.ts
20
+ import { canAddWebsite, isMultiModeAvailable } from "@setzkasten-cms/core";
21
+ var POST = async ({ request, cookies }) => {
22
+ const session = parseSession(cookies.get("setzkasten_session")?.value);
23
+ if (!session) return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
24
+ if (session.user.role !== "admin")
25
+ return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 });
26
+ const tier = resolveLicenseTier();
27
+ if (!isMultiModeAvailable(tier)) {
28
+ return new Response(
29
+ JSON.stringify({
30
+ error: "Multi-Mode ist nur mit Pro- oder Enterprise-Lizenz verf\xFCgbar.",
31
+ tier
32
+ }),
33
+ { status: 402, headers: { "Content-Type": "application/json" } }
34
+ );
35
+ }
36
+ let body = {};
37
+ try {
38
+ body = await request.json();
39
+ } catch {
40
+ return new Response(JSON.stringify({ error: "Invalid JSON body" }), { status: 400 });
41
+ }
42
+ if (!body.configRepo || typeof body.configRepo !== "string" || !body.configRepo.includes("/")) {
43
+ return new Response(JSON.stringify({ error: "configRepo (owner/repo) ist erforderlich" }), {
44
+ status: 400
45
+ });
46
+ }
47
+ if (!body.configInstallationId || typeof body.configInstallationId !== "string") {
48
+ return new Response(JSON.stringify({ error: "configInstallationId ist erforderlich" }), {
49
+ status: 400
50
+ });
51
+ }
52
+ const fullConfig = globalThis.__SETZKASTEN_FULL_CONFIG__;
53
+ if (fullConfig?.storage?.kind && fullConfig.storage.kind !== "single" && fullConfig.storage.kind !== "github-app") {
54
+ return new Response(
55
+ JSON.stringify({
56
+ error: `Migration nur aus dem Single-Mode m\xF6glich. Aktueller storage.kind: ${fullConfig.storage.kind ?? "unbekannt"}`
57
+ }),
58
+ { status: 400 }
59
+ );
60
+ }
61
+ const allowed = canAddWebsite(tier, 0);
62
+ if (!allowed.ok) {
63
+ return new Response(JSON.stringify({ error: allowed.reason }), { status: 402 });
64
+ }
65
+ const sourceStorage = resolveStorageConfig();
66
+ if (!sourceStorage) {
67
+ return new Response(
68
+ JSON.stringify({ error: "Single-Mode Storage konnte nicht aufgel\xF6st werden" }),
69
+ { status: 500 }
70
+ );
71
+ }
72
+ const tokenResult = await resolveConfigRepoToken();
73
+ if (!tokenResult.ok) {
74
+ return new Response(JSON.stringify({ error: tokenResult.error.message }), { status: 500 });
75
+ }
76
+ const token = tokenResult.value;
77
+ const [configOwner, configRepo] = body.configRepo.split("/");
78
+ if (!configOwner || !configRepo) {
79
+ return new Response(JSON.stringify({ error: 'configRepo muss "owner/repo" sein' }), {
80
+ status: 400
81
+ });
82
+ }
83
+ const sourceContentPath = globalThis.__SETZKASTEN_CONFIG__?.storage?.contentPath ?? "content";
84
+ const headers = {
85
+ Authorization: `Bearer ${token}`,
86
+ Accept: "application/vnd.github+json",
87
+ "X-GitHub-Api-Version": "2022-11-28",
88
+ "Content-Type": "application/json"
89
+ };
90
+ const ghBase = (owner, repo, path) => `https://api.github.com/repos/${owner}/${repo}/contents/${path}`;
91
+ const sourceEditors = await readOptional(
92
+ ghBase(sourceStorage.owner, sourceStorage.repo, `${sourceContentPath}/_editors.json`),
93
+ `?ref=${sourceStorage.branch}`,
94
+ headers
95
+ );
96
+ const sourceGlobal = await readOptional(
97
+ ghBase(sourceStorage.owner, sourceStorage.repo, `${sourceContentPath}/_global_config.json`),
98
+ `?ref=${sourceStorage.branch}`,
99
+ headers
100
+ );
101
+ const configBranch = "main";
102
+ const editorsCommit = sourceEditors ? await putFile(
103
+ ghBase(configOwner, configRepo, "content/_editors.json"),
104
+ sourceEditors,
105
+ configBranch,
106
+ "chore(migrate): copy editors from website repo",
107
+ headers
108
+ ) : true;
109
+ const globalCommit = sourceGlobal ? await putFile(
110
+ ghBase(configOwner, configRepo, "content/_global_config.json"),
111
+ sourceGlobal,
112
+ configBranch,
113
+ "chore(migrate): copy global config from website repo",
114
+ headers
115
+ ) : true;
116
+ const previewOrigin = body.previewOrigin ?? process.env.PUBLIC_SITE_URL ?? "http://localhost:4321";
117
+ const initialEntry = {
118
+ id: "main",
119
+ name: sourceStorage.repo,
120
+ repo: `${sourceStorage.owner}/${sourceStorage.repo}`,
121
+ branch: sourceStorage.branch,
122
+ previewOrigin,
123
+ githubApp: {
124
+ appId: process.env.GITHUB_APP_ID ?? "",
125
+ installationId: process.env.GITHUB_APP_INSTALLATION_ID ?? ""
126
+ }
127
+ };
128
+ const websitesContent = JSON.stringify({ websites: [initialEntry] }, null, 2);
129
+ const websitesCommit = await putFile(
130
+ ghBase(configOwner, configRepo, "websites.json"),
131
+ websitesContent,
132
+ configBranch,
133
+ "feat(migrate): initialise websites.json with current single-mode setup",
134
+ headers
135
+ );
136
+ return new Response(
137
+ JSON.stringify({
138
+ ok: true,
139
+ committed: {
140
+ editors: editorsCommit,
141
+ globalConfig: globalCommit,
142
+ websites: websitesCommit
143
+ },
144
+ // Echo back the values the user still has to set themselves.
145
+ manual: {
146
+ configRepo: body.configRepo,
147
+ configInstallationId: body.configInstallationId,
148
+ configBranch,
149
+ envChanges: {
150
+ add: {
151
+ SETZKASTEN_CONFIG_REPO: body.configRepo,
152
+ SETZKASTEN_CONFIG_BRANCH: configBranch,
153
+ GITHUB_APP_CONFIG_INSTALLATION_ID: body.configInstallationId
154
+ }
155
+ }
156
+ }
157
+ }),
158
+ { status: 200, headers: { "Content-Type": "application/json" } }
159
+ );
160
+ };
161
+ async function readOptional(url, qs, headers) {
162
+ const res = await fetch(url + qs, { headers });
163
+ if (!res.ok) return null;
164
+ const data = await res.json();
165
+ return data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
166
+ }
167
+ async function putFile(url, content, branch, message, headers) {
168
+ let sha;
169
+ try {
170
+ const existing = await fetch(`${url}?ref=${branch}`, { headers });
171
+ if (existing.ok) {
172
+ const data = await existing.json();
173
+ sha = data.sha;
174
+ }
175
+ } catch {
176
+ }
177
+ const body = {
178
+ message: withTrailers(message),
179
+ content: Buffer.from(content).toString("base64"),
180
+ branch
181
+ };
182
+ if (sha) body.sha = sha;
183
+ const res = await fetch(url, { method: "PUT", headers, body: JSON.stringify(body) });
184
+ return res.ok;
185
+ }
186
+ export {
187
+ POST
188
+ };
@@ -0,0 +1,39 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ interface PageInfo {
4
+ path: string;
5
+ pageKey: string;
6
+ label: string;
7
+ hasConfig: boolean;
8
+ /** Unix-ms timestamp of the page's last Setzkasten-driven commit, when
9
+ * `_pages-meta.json` knows about the page. */
10
+ lastModified?: number;
11
+ }
12
+ /**
13
+ * Returns the list of pages scanned at build time.
14
+ * Reads the Vite build-time constant first; falls back to globalThis for
15
+ * local dev / test environments where the define is not applied.
16
+ *
17
+ * Only valid in single-mode where the admin and the website share the same
18
+ * Astro project. Multi-mode has to fetch the page list per website at
19
+ * runtime (see {@link fetchPagesFromGitHub}).
20
+ */
21
+ declare function resolvePages(): PageInfo[];
22
+ /**
23
+ * GET /api/setzkasten/pages
24
+ *
25
+ * Single-mode: returns the build-time scan from the admin's own Astro
26
+ * project, enriched with `_pages-meta.json` timestamps from the
27
+ * (single) repo.
28
+ *
29
+ * Multi-mode: the X-SK-Website header selects one of the registered
30
+ * websites. The admin doesn't have build-time access to that website's
31
+ * `src/pages/` directory, so we fetch it via the GitHub Contents API
32
+ * (cached for 5 min), then enrich with the per-website
33
+ * `_pages-meta.json`. Without this branch, every website in Multi-Mode
34
+ * would see the admin's own page list — typically a single "index"
35
+ * stub — instead of its real pages.
36
+ */
37
+ declare const GET: APIRoute;
38
+
39
+ export { GET, resolvePages };