@setzkasten-cms/astro-admin 0.6.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/LICENSE +37 -0
- package/package.json +70 -0
- package/src/admin-page.astro +148 -0
- package/src/api-routes/__tests__/add-section-helpers.test.ts +383 -0
- package/src/api-routes/__tests__/catalog-api.test.ts +115 -0
- package/src/api-routes/__tests__/deferred-operations.test.ts +232 -0
- package/src/api-routes/__tests__/deploy-hook.test.ts +134 -0
- package/src/api-routes/__tests__/patch-page-file.test.ts +193 -0
- package/src/api-routes/__tests__/scan-page-helpers.test.ts +162 -0
- package/src/api-routes/__tests__/section-management.test.ts +284 -0
- package/src/api-routes/_storage-config.ts +54 -0
- package/src/api-routes/asset-proxy.ts +76 -0
- package/src/api-routes/auth-callback.ts +105 -0
- package/src/api-routes/auth-login.ts +87 -0
- package/src/api-routes/auth-logout.ts +9 -0
- package/src/api-routes/auth-session.ts +36 -0
- package/src/api-routes/catalog-add.ts +151 -0
- package/src/api-routes/catalog-export.ts +86 -0
- package/src/api-routes/catalog-helpers.ts +83 -0
- package/src/api-routes/catalog-list.ts +12 -0
- package/src/api-routes/config.ts +30 -0
- package/src/api-routes/deploy-hook.ts +69 -0
- package/src/api-routes/github-proxy.ts +111 -0
- package/src/api-routes/init-add-section.ts +511 -0
- package/src/api-routes/init-apply.ts +270 -0
- package/src/api-routes/init-migrate.ts +262 -0
- package/src/api-routes/init-scan-page.ts +336 -0
- package/src/api-routes/init-scan.ts +162 -0
- package/src/api-routes/pages.ts +17 -0
- package/src/api-routes/section-add.ts +189 -0
- package/src/api-routes/section-commit-pending.ts +147 -0
- package/src/api-routes/section-delete.ts +141 -0
- package/src/api-routes/section-duplicate.ts +144 -0
- package/src/api-routes/section-management.ts +95 -0
- package/src/api-routes/section-prepare-copy.ts +93 -0
- package/src/api-routes/section-prepare.ts +121 -0
- package/src/env.d.ts +7 -0
- package/src/init/__tests__/page-level.test.ts +1033 -0
- package/src/init/__tests__/page-list-coverage.test.ts +474 -0
- package/src/init/__tests__/patcher-edge-cases.test.ts +434 -0
- package/src/init/__tests__/patcher-page-mode.test.ts +272 -0
- package/src/init/__tests__/section-pipeline.test.ts +393 -0
- package/src/init/analyzer-types.ts +92 -0
- package/src/init/astro-config-patcher.ts +98 -0
- package/src/init/astro-detector.ts +207 -0
- package/src/init/astro-section-analyzer-v2.ts +1663 -0
- package/src/init/field-label-enricher.ts +72 -0
- package/src/init/template-patcher-v2.ts +1957 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro'
|
|
2
|
+
import { generateConfigFile, type InferredSection, type ConfigGeneratorInput } from '@setzkasten-cms/core/init'
|
|
3
|
+
import { patchAstroConfig } from '../init/astro-config-patcher'
|
|
4
|
+
import { patchTemplateForFields } from '../init/template-patcher-v2'
|
|
5
|
+
|
|
6
|
+
interface ApplyRequest {
|
|
7
|
+
owner: string
|
|
8
|
+
repo: string
|
|
9
|
+
branch?: string
|
|
10
|
+
projectRoot: string
|
|
11
|
+
astroConfigPath: string | null
|
|
12
|
+
sections: InferredSection[]
|
|
13
|
+
pages: Array<{
|
|
14
|
+
pageKey: string
|
|
15
|
+
sectionKeys: string[]
|
|
16
|
+
}>
|
|
17
|
+
contentPath?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FileToCommit {
|
|
21
|
+
path: string
|
|
22
|
+
content: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* POST /api/setzkasten/init/apply
|
|
27
|
+
*
|
|
28
|
+
* Generates config + content files and commits them to the repo.
|
|
29
|
+
* Body: ApplyRequest
|
|
30
|
+
*/
|
|
31
|
+
export const POST: APIRoute = async ({ request, cookies }) => {
|
|
32
|
+
// Verify session
|
|
33
|
+
const session = cookies.get('setzkasten_session')?.value
|
|
34
|
+
if (!session) {
|
|
35
|
+
return new Response('Unauthorized', { status: 401 })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const githubToken = import.meta.env.GITHUB_TOKEN ?? process.env.GITHUB_TOKEN
|
|
39
|
+
if (!githubToken) {
|
|
40
|
+
return new Response('GitHub token not configured', { status: 500 })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const body = await request.json() as ApplyRequest
|
|
45
|
+
const { owner, repo, branch = 'main', projectRoot, astroConfigPath, sections, pages } = body
|
|
46
|
+
const contentPath = body.contentPath ?? 'content'
|
|
47
|
+
|
|
48
|
+
const filesToCommit: FileToCommit[] = []
|
|
49
|
+
|
|
50
|
+
// 1. Generate setzkasten.config.ts
|
|
51
|
+
const configInput: ConfigGeneratorInput = {
|
|
52
|
+
gitRepo: `${owner}/${repo}`,
|
|
53
|
+
productKey: 'website',
|
|
54
|
+
sections,
|
|
55
|
+
}
|
|
56
|
+
const configContent = generateConfigFile(configInput)
|
|
57
|
+
const configPath = projectRoot ? `${projectRoot}/setzkasten.config.ts` : 'setzkasten.config.ts'
|
|
58
|
+
filesToCommit.push({ path: configPath, content: configContent })
|
|
59
|
+
|
|
60
|
+
// 2. Patch astro.config if needed
|
|
61
|
+
if (astroConfigPath) {
|
|
62
|
+
const astroConfigSource = await fetchFileContent(owner, repo, branch, astroConfigPath, githubToken)
|
|
63
|
+
if (astroConfigSource) {
|
|
64
|
+
const patched = patchAstroConfig(astroConfigSource)
|
|
65
|
+
if (patched) {
|
|
66
|
+
filesToCommit.push({ path: astroConfigPath, content: patched })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Generate content JSON for each section
|
|
72
|
+
for (const section of sections) {
|
|
73
|
+
const sectionData: Record<string, unknown> = {}
|
|
74
|
+
for (const field of section.fields) {
|
|
75
|
+
sectionData[field.key] = getDefaultValue(field.type)
|
|
76
|
+
}
|
|
77
|
+
const sectionPath = `${contentPath}/_sections/${section.key}.json`
|
|
78
|
+
filesToCommit.push({
|
|
79
|
+
path: sectionPath,
|
|
80
|
+
content: JSON.stringify(sectionData, null, 2),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 4. Patch component templates — add data-sk-field + CMS variables
|
|
85
|
+
for (const section of sections) {
|
|
86
|
+
if (!section.componentPath) continue
|
|
87
|
+
const componentSource = await fetchFileContent(owner, repo, branch, section.componentPath, githubToken)
|
|
88
|
+
if (!componentSource) continue
|
|
89
|
+
const patched = await patchTemplateForFields(componentSource, section.key, section.fields)
|
|
90
|
+
if (patched !== componentSource) {
|
|
91
|
+
filesToCommit.push({ path: section.componentPath, content: patched })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 5. Generate page configs (renumbered from 4)
|
|
96
|
+
for (const page of pages) {
|
|
97
|
+
const pageConfig = {
|
|
98
|
+
sections: page.sectionKeys.map((key, index) => ({
|
|
99
|
+
key,
|
|
100
|
+
enabled: true,
|
|
101
|
+
order: index,
|
|
102
|
+
})),
|
|
103
|
+
}
|
|
104
|
+
const configKey = '_' + page.pageKey.replace(/\//g, '_')
|
|
105
|
+
const pagePath = `${contentPath}/pages/${configKey}.json`
|
|
106
|
+
filesToCommit.push({
|
|
107
|
+
path: pagePath,
|
|
108
|
+
content: JSON.stringify(pageConfig, null, 2),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 6. Commit all files via Git Trees API
|
|
113
|
+
const commitResult = await batchCommit(
|
|
114
|
+
owner,
|
|
115
|
+
repo,
|
|
116
|
+
branch,
|
|
117
|
+
filesToCommit,
|
|
118
|
+
'feat: initialize Setzkasten CMS',
|
|
119
|
+
githubToken,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if (!commitResult.ok) {
|
|
123
|
+
return Response.json(
|
|
124
|
+
{ error: commitResult.error },
|
|
125
|
+
{ status: 500 },
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return Response.json({
|
|
130
|
+
success: true,
|
|
131
|
+
commitSha: commitResult.sha,
|
|
132
|
+
filesWritten: filesToCommit.map((f) => f.path),
|
|
133
|
+
})
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error('[setzkasten] Init apply error:', error)
|
|
136
|
+
return Response.json(
|
|
137
|
+
{ error: error instanceof Error ? error.message : 'Apply failed' },
|
|
138
|
+
{ status: 500 },
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getDefaultValue(fieldType: string): unknown {
|
|
144
|
+
switch (fieldType) {
|
|
145
|
+
case 'text': return ''
|
|
146
|
+
case 'number': return 0
|
|
147
|
+
case 'boolean': return false
|
|
148
|
+
case 'image': return { path: '', alt: '' }
|
|
149
|
+
case 'array': return []
|
|
150
|
+
case 'color': return '#000000'
|
|
151
|
+
case 'date': return ''
|
|
152
|
+
case 'icon': return ''
|
|
153
|
+
default: return ''
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function fetchFileContent(
|
|
158
|
+
owner: string,
|
|
159
|
+
repo: string,
|
|
160
|
+
branch: string,
|
|
161
|
+
path: string,
|
|
162
|
+
token: string,
|
|
163
|
+
): Promise<string | null> {
|
|
164
|
+
try {
|
|
165
|
+
const response = await fetch(
|
|
166
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
167
|
+
{
|
|
168
|
+
headers: {
|
|
169
|
+
Authorization: `Bearer ${token}`,
|
|
170
|
+
Accept: 'application/vnd.github+json',
|
|
171
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
if (!response.ok) return null
|
|
176
|
+
const data = await response.json() as { content: string; encoding: string }
|
|
177
|
+
return data.encoding === 'base64'
|
|
178
|
+
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
179
|
+
: data.content
|
|
180
|
+
} catch {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function batchCommit(
|
|
186
|
+
owner: string,
|
|
187
|
+
repo: string,
|
|
188
|
+
branch: string,
|
|
189
|
+
files: FileToCommit[],
|
|
190
|
+
message: string,
|
|
191
|
+
token: string,
|
|
192
|
+
): Promise<{ ok: true; sha: string } | { ok: false; error: string }> {
|
|
193
|
+
const headers = {
|
|
194
|
+
Authorization: `Bearer ${token}`,
|
|
195
|
+
Accept: 'application/vnd.github+json',
|
|
196
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
197
|
+
'Content-Type': 'application/json',
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
// 1. Get HEAD ref
|
|
202
|
+
const refRes = await fetch(
|
|
203
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
204
|
+
{ headers },
|
|
205
|
+
)
|
|
206
|
+
if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` }
|
|
207
|
+
const refData = await refRes.json() as { object: { sha: string } }
|
|
208
|
+
const headSha = refData.object.sha
|
|
209
|
+
|
|
210
|
+
// 2. Get base tree
|
|
211
|
+
const commitRes = await fetch(
|
|
212
|
+
`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`,
|
|
213
|
+
{ headers },
|
|
214
|
+
)
|
|
215
|
+
if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` }
|
|
216
|
+
const commitData = await commitRes.json() as { tree: { sha: string } }
|
|
217
|
+
const baseTreeSha = commitData.tree.sha
|
|
218
|
+
|
|
219
|
+
// 3. Create new tree
|
|
220
|
+
const treeRes = await fetch(
|
|
221
|
+
`https://api.github.com/repos/${owner}/${repo}/git/trees`,
|
|
222
|
+
{
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers,
|
|
225
|
+
body: JSON.stringify({
|
|
226
|
+
base_tree: baseTreeSha,
|
|
227
|
+
tree: files.map((f) => ({
|
|
228
|
+
path: f.path,
|
|
229
|
+
mode: '100644',
|
|
230
|
+
type: 'blob',
|
|
231
|
+
content: f.content,
|
|
232
|
+
})),
|
|
233
|
+
}),
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` }
|
|
237
|
+
const treeData = await treeRes.json() as { sha: string }
|
|
238
|
+
|
|
239
|
+
// 4. Create commit
|
|
240
|
+
const newCommitRes = await fetch(
|
|
241
|
+
`https://api.github.com/repos/${owner}/${repo}/git/commits`,
|
|
242
|
+
{
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers,
|
|
245
|
+
body: JSON.stringify({
|
|
246
|
+
tree: treeData.sha,
|
|
247
|
+
parents: [headSha],
|
|
248
|
+
message,
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
if (!newCommitRes.ok) return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
|
|
253
|
+
const newCommitData = await newCommitRes.json() as { sha: string }
|
|
254
|
+
|
|
255
|
+
// 5. Update ref
|
|
256
|
+
const updateRes = await fetch(
|
|
257
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
258
|
+
{
|
|
259
|
+
method: 'PATCH',
|
|
260
|
+
headers,
|
|
261
|
+
body: JSON.stringify({ sha: newCommitData.sha }),
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` }
|
|
265
|
+
|
|
266
|
+
return { ok: true, sha: newCommitData.sha }
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return { ok: false, error: error instanceof Error ? error.message : 'Commit failed' }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro'
|
|
2
|
+
import type { SetzKastenConfig } from '@setzkasten-cms/core'
|
|
3
|
+
import { resolveStorageConfig, prefixPath } from './_storage-config'
|
|
4
|
+
import { patchTemplateForFields } from '../init/template-patcher-v2'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* POST /api/setzkasten/init/migrate
|
|
8
|
+
*
|
|
9
|
+
* Adds `data-sk-field` attributes to an existing Astro section component.
|
|
10
|
+
* Handles two cases:
|
|
11
|
+
* 1. Fields using `getSection()` / `Astro.props` variable patterns
|
|
12
|
+
* 2. Hardcoded text matching content JSON values (for adopted fields without bindings)
|
|
13
|
+
*
|
|
14
|
+
* Body: { owner, repo, branch?, sectionKey, componentPath }
|
|
15
|
+
* Returns: { commitSha, patchedSource, originalSource }
|
|
16
|
+
*/
|
|
17
|
+
export const POST: APIRoute = async ({ request, cookies }) => {
|
|
18
|
+
const session = cookies.get('setzkasten_session')?.value
|
|
19
|
+
if (!session) {
|
|
20
|
+
return new Response('Unauthorized', { status: 401 })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const githubToken = import.meta.env.GITHUB_TOKEN ?? process.env.GITHUB_TOKEN
|
|
24
|
+
if (!githubToken) {
|
|
25
|
+
return new Response('GitHub token not configured', { status: 500 })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const body = await request.json() as {
|
|
30
|
+
owner?: string
|
|
31
|
+
repo?: string
|
|
32
|
+
branch?: string
|
|
33
|
+
sectionKey: string
|
|
34
|
+
componentPath?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const storage = resolveStorageConfig(body)
|
|
38
|
+
if (!storage) {
|
|
39
|
+
return Response.json({ error: 'Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars.' }, { status: 400 })
|
|
40
|
+
}
|
|
41
|
+
const { owner, repo, branch, projectPrefix } = storage
|
|
42
|
+
const { sectionKey } = body
|
|
43
|
+
|
|
44
|
+
// Derive component path from section key if not provided, then prefix for monorepo
|
|
45
|
+
const componentPath = prefixPath(
|
|
46
|
+
body.componentPath || deriveComponentPath(sectionKey),
|
|
47
|
+
projectPrefix,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if (!sectionKey) {
|
|
51
|
+
return Response.json({ error: 'sectionKey is required' }, { status: 400 })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const headers = {
|
|
55
|
+
Authorization: `Bearer ${githubToken}`,
|
|
56
|
+
Accept: 'application/vnd.github+json',
|
|
57
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 1. Fetch component source
|
|
62
|
+
const source = await fetchFileContent(owner, repo, branch, componentPath, githubToken)
|
|
63
|
+
if (!source) {
|
|
64
|
+
return Response.json({ error: 'Could not read component source' }, { status: 404 })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Build fields list from config + content JSON for AST-based patching
|
|
68
|
+
const serverConfig = (globalThis as any).__SETZKASTEN_CONFIG__
|
|
69
|
+
const contentPath = serverConfig?.storage?.contentPath || 'content'
|
|
70
|
+
const sectionJsonPath = `${contentPath}/_sections/${sectionKey}.json`
|
|
71
|
+
const sectionJson = await fetchFileContent(owner, repo, branch, sectionJsonPath, githubToken)
|
|
72
|
+
|
|
73
|
+
const fullConfig = (globalThis as any).__SETZKASTEN_FULL_CONFIG__ as SetzKastenConfig | undefined
|
|
74
|
+
const fieldDefs = getFieldDefs(fullConfig, sectionKey)
|
|
75
|
+
|
|
76
|
+
// Build fields: combine config field types with content JSON values
|
|
77
|
+
const fields: Array<{ key: string; type: string; defaultValue?: unknown }> = []
|
|
78
|
+
let contentData: Record<string, unknown> = {}
|
|
79
|
+
if (sectionJson) {
|
|
80
|
+
try { contentData = JSON.parse(sectionJson) } catch { /* ignore */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add fields from config definitions
|
|
84
|
+
for (const [key, type] of fieldDefs) {
|
|
85
|
+
fields.push({ key, type, defaultValue: contentData[key] })
|
|
86
|
+
}
|
|
87
|
+
// Add any content keys not in config (for CMS-bound field detection)
|
|
88
|
+
for (const [key, value] of Object.entries(contentData)) {
|
|
89
|
+
if (!fieldDefs.has(key)) {
|
|
90
|
+
fields.push({ key, type: 'text', defaultValue: value })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. AST-based patching: adds data-sk-field + CMS variables
|
|
95
|
+
const patched = await patchTemplateForFields(source, sectionKey, fields)
|
|
96
|
+
|
|
97
|
+
if (patched === source) {
|
|
98
|
+
return Response.json({
|
|
99
|
+
success: true,
|
|
100
|
+
noChanges: true,
|
|
101
|
+
message: 'No patchable patterns found in template',
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. Commit the patched file
|
|
106
|
+
const commitResult = await batchCommit(
|
|
107
|
+
owner,
|
|
108
|
+
repo,
|
|
109
|
+
branch,
|
|
110
|
+
[{ path: componentPath, content: patched }],
|
|
111
|
+
`chore: add live-preview bindings to ${sectionKey} section`,
|
|
112
|
+
headers,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if (!commitResult.ok) {
|
|
116
|
+
return Response.json({ error: commitResult.error }, { status: 500 })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Response.json({
|
|
120
|
+
success: true,
|
|
121
|
+
commitSha: commitResult.sha,
|
|
122
|
+
patchedSource: patched,
|
|
123
|
+
originalSource: source,
|
|
124
|
+
})
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('[setzkasten] migrate error:', error)
|
|
127
|
+
return Response.json(
|
|
128
|
+
{ error: error instanceof Error ? error.message : 'Migration failed' },
|
|
129
|
+
{ status: 500 },
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get field definitions for a section from the full config.
|
|
136
|
+
*/
|
|
137
|
+
function getFieldDefs(
|
|
138
|
+
config: SetzKastenConfig | undefined,
|
|
139
|
+
sectionKey: string,
|
|
140
|
+
): Map<string, string> {
|
|
141
|
+
const defs = new Map<string, string>() // field key → field type
|
|
142
|
+
if (!config) return defs
|
|
143
|
+
for (const product of Object.values(config.products)) {
|
|
144
|
+
const section = product.sections[sectionKey]
|
|
145
|
+
if (section) {
|
|
146
|
+
for (const [key, field] of Object.entries(section.fields)) {
|
|
147
|
+
defs.set(key, (field as any).type || 'text')
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return defs
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function deriveComponentPath(sectionKey: string): string {
|
|
155
|
+
const componentName = sectionKey
|
|
156
|
+
.split('-')
|
|
157
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
158
|
+
.join('') + 'Section'
|
|
159
|
+
return `src/components/sections/${componentName}.astro`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function fetchFileContent(
|
|
163
|
+
owner: string,
|
|
164
|
+
repo: string,
|
|
165
|
+
branch: string,
|
|
166
|
+
path: string,
|
|
167
|
+
token: string,
|
|
168
|
+
): Promise<string | null> {
|
|
169
|
+
try {
|
|
170
|
+
const response = await fetch(
|
|
171
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
172
|
+
{
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${token}`,
|
|
175
|
+
Accept: 'application/vnd.github+json',
|
|
176
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
if (!response.ok) return null
|
|
181
|
+
const data = await response.json() as { content: string; encoding: string }
|
|
182
|
+
return data.encoding === 'base64'
|
|
183
|
+
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
184
|
+
: data.content
|
|
185
|
+
} catch {
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function batchCommit(
|
|
191
|
+
owner: string,
|
|
192
|
+
repo: string,
|
|
193
|
+
branch: string,
|
|
194
|
+
files: Array<{ path: string; content: string }>,
|
|
195
|
+
message: string,
|
|
196
|
+
headers: Record<string, string>,
|
|
197
|
+
): Promise<{ ok: true; sha: string } | { ok: false; error: string }> {
|
|
198
|
+
try {
|
|
199
|
+
const refRes = await fetch(
|
|
200
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
201
|
+
{ headers },
|
|
202
|
+
)
|
|
203
|
+
if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` }
|
|
204
|
+
const refData = await refRes.json() as { object: { sha: string } }
|
|
205
|
+
const headSha = refData.object.sha
|
|
206
|
+
|
|
207
|
+
const commitRes = await fetch(
|
|
208
|
+
`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`,
|
|
209
|
+
{ headers },
|
|
210
|
+
)
|
|
211
|
+
if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` }
|
|
212
|
+
const commitData = await commitRes.json() as { tree: { sha: string } }
|
|
213
|
+
|
|
214
|
+
const treeRes = await fetch(
|
|
215
|
+
`https://api.github.com/repos/${owner}/${repo}/git/trees`,
|
|
216
|
+
{
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers,
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
base_tree: commitData.tree.sha,
|
|
221
|
+
tree: files.map((f) => ({
|
|
222
|
+
path: f.path,
|
|
223
|
+
mode: '100644',
|
|
224
|
+
type: 'blob',
|
|
225
|
+
content: f.content,
|
|
226
|
+
})),
|
|
227
|
+
}),
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` }
|
|
231
|
+
const treeData = await treeRes.json() as { sha: string }
|
|
232
|
+
|
|
233
|
+
const newCommitRes = await fetch(
|
|
234
|
+
`https://api.github.com/repos/${owner}/${repo}/git/commits`,
|
|
235
|
+
{
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers,
|
|
238
|
+
body: JSON.stringify({
|
|
239
|
+
tree: treeData.sha,
|
|
240
|
+
parents: [headSha],
|
|
241
|
+
message,
|
|
242
|
+
}),
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
if (!newCommitRes.ok) return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
|
|
246
|
+
const newCommitData = await newCommitRes.json() as { sha: string }
|
|
247
|
+
|
|
248
|
+
const updateRes = await fetch(
|
|
249
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
250
|
+
{
|
|
251
|
+
method: 'PATCH',
|
|
252
|
+
headers,
|
|
253
|
+
body: JSON.stringify({ sha: newCommitData.sha }),
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` }
|
|
257
|
+
|
|
258
|
+
return { ok: true, sha: newCommitData.sha }
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return { ok: false, error: error instanceof Error ? error.message : 'Commit failed' }
|
|
261
|
+
}
|
|
262
|
+
}
|