@topy-ai/maggie 0.7.32 → 0.7.34

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 (138) hide show
  1. package/README-zh-TW.md +2 -2
  2. package/README.md +19 -4
  3. package/bundled-skills/maggie-blog-bootstrap/SKILL.md +8 -0
  4. package/bundled-skills/maggie-clone/SKILL.md +7 -1
  5. package/bundled-templates/astro-blog/README.md +217 -0
  6. package/bundled-templates/astro-blog/examples/blog.config.ts +30 -0
  7. package/bundled-templates/astro-blog/examples/content.config.ts +28 -0
  8. package/bundled-templates/astro-blog/examples/metadata.ts +37 -0
  9. package/bundled-templates/astro-blog/examples/ops-api.ts +31 -0
  10. package/bundled-templates/astro-blog/examples/route-manifest.ts +21 -0
  11. package/bundled-templates/astro-blog/starter/.env.example +36 -0
  12. package/bundled-templates/astro-blog/starter/README.md +168 -0
  13. package/bundled-templates/astro-blog/starter/astro.config.mjs +19 -0
  14. package/bundled-templates/astro-blog/starter/db/schema.sql +508 -0
  15. package/bundled-templates/astro-blog/starter/package-lock.json +8039 -0
  16. package/bundled-templates/astro-blog/starter/package.json +41 -0
  17. package/bundled-templates/astro-blog/starter/scripts/contract-check.mjs +16 -0
  18. package/bundled-templates/astro-blog/starter/scripts/fixture-seed.mjs +11 -0
  19. package/bundled-templates/astro-blog/starter/scripts/schema-check.mjs +13 -0
  20. package/bundled-templates/astro-blog/starter/scripts/seed-db.mjs +20 -0
  21. package/bundled-templates/astro-blog/starter/src/data/demo.ts +52 -0
  22. package/bundled-templates/astro-blog/starter/src/layouts/BaseLayout.astro +53 -0
  23. package/bundled-templates/astro-blog/starter/src/layouts/OpsLayout.astro +73 -0
  24. package/bundled-templates/astro-blog/starter/src/lib/ai-cmo.ts +14 -0
  25. package/bundled-templates/astro-blog/starter/src/lib/config.ts +15 -0
  26. package/bundled-templates/astro-blog/starter/src/lib/db.ts +95 -0
  27. package/bundled-templates/astro-blog/starter/src/lib/google.ts +28 -0
  28. package/bundled-templates/astro-blog/starter/src/lib/media.ts +12 -0
  29. package/bundled-templates/astro-blog/starter/src/lib/migration-validation.ts +11 -0
  30. package/bundled-templates/astro-blog/starter/src/lib/migration.ts +21 -0
  31. package/bundled-templates/astro-blog/starter/src/lib/ops-auth.ts +21 -0
  32. package/bundled-templates/astro-blog/starter/src/lib/providers/email.ts +45 -0
  33. package/bundled-templates/astro-blog/starter/src/lib/providers/media-storage.ts +37 -0
  34. package/bundled-templates/astro-blog/starter/src/lib/report-delivery.ts +26 -0
  35. package/bundled-templates/astro-blog/starter/src/lib/report-pdf.ts +49 -0
  36. package/bundled-templates/astro-blog/starter/src/lib/repository.ts +72 -0
  37. package/bundled-templates/astro-blog/starter/src/lib/seo-audit.ts +72 -0
  38. package/bundled-templates/astro-blog/starter/src/lib/seo-insights.ts +14 -0
  39. package/bundled-templates/astro-blog/starter/src/lib/seo.ts +33 -0
  40. package/bundled-templates/astro-blog/starter/src/lib/types.ts +58 -0
  41. package/bundled-templates/astro-blog/starter/src/lib/wordpress.ts +847 -0
  42. package/bundled-templates/astro-blog/starter/src/middleware.ts +7 -0
  43. package/bundled-templates/astro-blog/starter/src/pages/404.astro +11 -0
  44. package/bundled-templates/astro-blog/starter/src/pages/about.astro +15 -0
  45. package/bundled-templates/astro-blog/starter/src/pages/api/approval/[token].ts +25 -0
  46. package/bundled-templates/astro-blog/starter/src/pages/api/consent.ts +8 -0
  47. package/bundled-templates/astro-blog/starter/src/pages/api/ops/agency.ts +47 -0
  48. package/bundled-templates/astro-blog/starter/src/pages/api/ops/authors.ts +11 -0
  49. package/bundled-templates/astro-blog/starter/src/pages/api/ops/bulk.ts +58 -0
  50. package/bundled-templates/astro-blog/starter/src/pages/api/ops/calendar/publish-due.ts +12 -0
  51. package/bundled-templates/astro-blog/starter/src/pages/api/ops/config/validate.ts +4 -0
  52. package/bundled-templates/astro-blog/starter/src/pages/api/ops/content-tracking/report-state/batch.ts +19 -0
  53. package/bundled-templates/astro-blog/starter/src/pages/api/ops/content-tracking/report-state.ts +15 -0
  54. package/bundled-templates/astro-blog/starter/src/pages/api/ops/ctas.ts +5 -0
  55. package/bundled-templates/astro-blog/starter/src/pages/api/ops/entities.ts +23 -0
  56. package/bundled-templates/astro-blog/starter/src/pages/api/ops/media/optimize.ts +5 -0
  57. package/bundled-templates/astro-blog/starter/src/pages/api/ops/media/upload.ts +30 -0
  58. package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/[id]/resume.ts +28 -0
  59. package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/[id]/validate.ts +6 -0
  60. package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/[id].ts +7 -0
  61. package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/import.ts +458 -0
  62. package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/media.ts +5 -0
  63. package/bundled-templates/astro-blog/starter/src/pages/api/ops/operations.ts +189 -0
  64. package/bundled-templates/astro-blog/starter/src/pages/api/ops/playbooks/[id]/run.ts +55 -0
  65. package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id]/approve.ts +13 -0
  66. package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id]/preview.ts +9 -0
  67. package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id]/publish.ts +13 -0
  68. package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id].ts +18 -0
  69. package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/index.ts +28 -0
  70. package/bundled-templates/astro-blog/starter/src/pages/api/ops/providers.ts +6 -0
  71. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/history.ts +3 -0
  72. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/latest.ts +3 -0
  73. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/posts.ts +3 -0
  74. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/project-context.ts +4 -0
  75. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/sync-updates.ts +43 -0
  76. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/sync.ts +172 -0
  77. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/updates.ts +3 -0
  78. package/bundled-templates/astro-blog/starter/src/pages/api/ops/redirects/check.ts +5 -0
  79. package/bundled-templates/astro-blog/starter/src/pages/api/ops/redirects.ts +32 -0
  80. package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports/deliver-due.ts +12 -0
  81. package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports/deliver.ts +6 -0
  82. package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports/export.ts +17 -0
  83. package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports.ts +31 -0
  84. package/bundled-templates/astro-blog/starter/src/pages/api/ops/rewrite/history/[contentId].ts +3 -0
  85. package/bundled-templates/astro-blog/starter/src/pages/api/ops/rewrite/policy.ts +4 -0
  86. package/bundled-templates/astro-blog/starter/src/pages/api/ops/rewrite/queue.ts +4 -0
  87. package/bundled-templates/astro-blog/starter/src/pages/api/ops/seo/audit.ts +7 -0
  88. package/bundled-templates/astro-blog/starter/src/pages/api/ops/seo/insights.ts +5 -0
  89. package/bundled-templates/astro-blog/starter/src/pages/api/ops/settings/site.ts +7 -0
  90. package/bundled-templates/astro-blog/starter/src/pages/api/ops/settings.ts +19 -0
  91. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/auto-detect.ts +13 -0
  92. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/diff.ts +11 -0
  93. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/match.ts +16 -0
  94. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/matching-history.ts +4 -0
  95. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/runs.ts +4 -0
  96. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap.ts +11 -0
  97. package/bundled-templates/astro-blog/starter/src/pages/api/ops/summary.ts +12 -0
  98. package/bundled-templates/astro-blog/starter/src/pages/api/ops/topics/[id]/faq.ts +12 -0
  99. package/bundled-templates/astro-blog/starter/src/pages/api/ops/topics.ts +11 -0
  100. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/capabilities.ts +5 -0
  101. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migration-plan.ts +21 -0
  102. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations/[id]/resume.ts +5 -0
  103. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations/[id]/validate.ts +6 -0
  104. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations/[id].ts +13 -0
  105. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations.ts +147 -0
  106. package/bundled-templates/astro-blog/starter/src/pages/authors/[slug].astro +12 -0
  107. package/bundled-templates/astro-blog/starter/src/pages/blog/[slug].astro +25 -0
  108. package/bundled-templates/astro-blog/starter/src/pages/blog/index.astro +32 -0
  109. package/bundled-templates/astro-blog/starter/src/pages/blog/page/[page].astro +21 -0
  110. package/bundled-templates/astro-blog/starter/src/pages/ops/index.astro +20 -0
  111. package/bundled-templates/astro-blog/starter/src/pages/ops/operations.astro +33 -0
  112. package/bundled-templates/astro-blog/starter/src/pages/ops/posts/[id]/edit.astro +17 -0
  113. package/bundled-templates/astro-blog/starter/src/pages/ops/posts/[id]/preview.astro +11 -0
  114. package/bundled-templates/astro-blog/starter/src/pages/ops/posts/index.astro +15 -0
  115. package/bundled-templates/astro-blog/starter/src/pages/ops/posts/new.astro +12 -0
  116. package/bundled-templates/astro-blog/starter/src/pages/ops/reports.astro +12 -0
  117. package/bundled-templates/astro-blog/starter/src/pages/ops/rewrite.astro +13 -0
  118. package/bundled-templates/astro-blog/starter/src/pages/ops/settings/integrations.astro +13 -0
  119. package/bundled-templates/astro-blog/starter/src/pages/ops/settings/site.astro +11 -0
  120. package/bundled-templates/astro-blog/starter/src/pages/ops/sitemap.astro +15 -0
  121. package/bundled-templates/astro-blog/starter/src/pages/ops/topics.astro +13 -0
  122. package/bundled-templates/astro-blog/starter/src/pages/ops/wordpress.astro +34 -0
  123. package/bundled-templates/astro-blog/starter/src/pages/review/[token].astro +13 -0
  124. package/bundled-templates/astro-blog/starter/src/pages/robots.txt.ts +4 -0
  125. package/bundled-templates/astro-blog/starter/src/pages/sitemap.xml.ts +12 -0
  126. package/bundled-templates/astro-blog/starter/src/pages/topics/[slug]/page/[page].astro +10 -0
  127. package/bundled-templates/astro-blog/starter/src/pages/topics/[slug].astro +19 -0
  128. package/bundled-templates/astro-blog/starter/src/pages/topics/index.astro +10 -0
  129. package/bundled-templates/astro-blog/starter/src/styles/global.css +4 -0
  130. package/bundled-templates/astro-blog/starter/tsconfig.json +9 -0
  131. package/bundled-templates/astro-blog/starter/wrangler.jsonc +52 -0
  132. package/bundled-tools/clis/maggie_clone.py +42 -15
  133. package/bundled-tools/clis/maggie_dash.py +41 -25
  134. package/bundled-tools/clis/maggie_design.py +2 -0
  135. package/bundled-tools/clis/maggie_design_system.py +7 -1
  136. package/bundled-tools/clis/site_audit.py +25 -2
  137. package/bundled-tools/runtime/maggie_memory.py +6 -1
  138. package/package.json +1 -1
@@ -0,0 +1,458 @@
1
+ import type { APIRoute } from "astro";
2
+ import { createHash } from "node:crypto";
3
+ import { audit, db, id, now } from "../../../../lib/db";
4
+ import { json } from "../../../../lib/ops-auth";
5
+ import {
6
+ checksum,
7
+ parseMigration,
8
+ type MigrationFormat,
9
+ type MigrationRecord,
10
+ } from "../../../../lib/migration";
11
+
12
+ function format(value: unknown): MigrationFormat | undefined {
13
+ return ["json", "csv", "wxr", "sitemap", "media-archive"].includes(
14
+ String(value),
15
+ )
16
+ ? (String(value) as MigrationFormat)
17
+ : undefined;
18
+ }
19
+ function source(value: unknown): string | undefined {
20
+ try {
21
+ const url = new URL(String(value));
22
+ return /^https?:$/.test(url.protocol) ? url.toString() : undefined;
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ function sourceChecksum(records: MigrationRecord[]): string {
28
+ return createHash("sha256")
29
+ .update(
30
+ records
31
+ .map((record) => checksum(record))
32
+ .sort()
33
+ .join("\n"),
34
+ )
35
+ .digest("hex");
36
+ }
37
+ function metadata(record: MigrationRecord): string {
38
+ const raw = record.seo?.custom_metadata_json || "{}";
39
+ try {
40
+ const parsed = JSON.parse(raw);
41
+ return JSON.stringify(parsed && typeof parsed === "object" ? parsed : {});
42
+ } catch {
43
+ return "{}";
44
+ }
45
+ }
46
+
47
+ export const POST: APIRoute = async ({ request }) => {
48
+ const body = (await request.json().catch(() => null)) as any;
49
+ const kind = format(body?.format);
50
+ if (!kind)
51
+ return json(
52
+ { error: "format must be json, csv, wxr, sitemap, or media-archive" },
53
+ 400,
54
+ );
55
+ let input = typeof body?.input === "string" ? body.input : "";
56
+ const location = source(body?.sourceUrl);
57
+ if (!input && location) {
58
+ const response = await fetch(location, {
59
+ signal: AbortSignal.timeout(30000),
60
+ });
61
+ if (!response.ok)
62
+ return json({ error: `source returned ${response.status}` }, 502);
63
+ input = await response.text();
64
+ }
65
+ if (!input) return json({ error: "input or sourceUrl is required" }, 400);
66
+ try {
67
+ const records = parseMigration(input, kind, location);
68
+ const migrationId = id("migration");
69
+ const timestamp = now();
70
+ const mode = body?.mode === "apply" ? "apply" : "preview";
71
+ const aggregateChecksum = sourceChecksum(records);
72
+ const syncMode = ["all", "new", "modified"].includes(body?.syncMode)
73
+ ? body.syncMode
74
+ : body?.incremental === true
75
+ ? "modified"
76
+ : "all";
77
+ const conflict = ["source-wins", "target-wins", "manual"].includes(
78
+ body?.conflict,
79
+ )
80
+ ? body.conflict
81
+ : "manual";
82
+ const options = {
83
+ format: kind,
84
+ incremental: syncMode !== "all",
85
+ syncMode,
86
+ detectDeleted: body?.detectDeleted === true,
87
+ conflict,
88
+ };
89
+ db()
90
+ .prepare(
91
+ "INSERT INTO wp_migrations (id,source_url,mode,status,options_json,counts_json,started_at,source_checksum) VALUES (?,?,?,?,?,?,?,?)",
92
+ )
93
+ .run(
94
+ migrationId,
95
+ location || "inline",
96
+ mode,
97
+ "running",
98
+ JSON.stringify(options),
99
+ "{}",
100
+ timestamp,
101
+ aggregateChecksum,
102
+ );
103
+ db()
104
+ .prepare(
105
+ "INSERT INTO migration_sources (id,kind,location,options_json,checksum,created_at,updated_at) VALUES (?,?,?,?,?,?,?)",
106
+ )
107
+ .run(
108
+ id("migration_source"),
109
+ kind,
110
+ location || "inline",
111
+ JSON.stringify(body || {}),
112
+ aggregateChecksum,
113
+ timestamp,
114
+ timestamp,
115
+ );
116
+ if (mode === "preview") {
117
+ const preview = records
118
+ .slice(0, 20)
119
+ .map((record) => ({ ...record, checksum: checksum(record) }));
120
+ db()
121
+ .prepare(
122
+ "UPDATE wp_migrations SET status='completed',counts_json=?,completed_at=?,checkpoint_json=? WHERE id=?",
123
+ )
124
+ .run(
125
+ JSON.stringify({
126
+ total: records.length,
127
+ previewed: preview.length,
128
+ sourceChecksum: aggregateChecksum,
129
+ syncMode,
130
+ detectDeleted: options.detectDeleted,
131
+ }),
132
+ now(),
133
+ JSON.stringify({
134
+ processed: 0,
135
+ total: records.length,
136
+ completed: false,
137
+ }),
138
+ migrationId,
139
+ );
140
+ return json({
141
+ id: migrationId,
142
+ mode,
143
+ total: records.length,
144
+ sourceChecksum: aggregateChecksum,
145
+ syncMode,
146
+ items: preview,
147
+ });
148
+ }
149
+ const authorId = "author-imported";
150
+ db()
151
+ .prepare(
152
+ "INSERT OR IGNORE INTO authors (id,slug,name,credentials_json,created_at,updated_at) VALUES (?,?,?,?,?,?)",
153
+ )
154
+ .run(authorId, "imported", "Imported author", "[]", timestamp, timestamp);
155
+ const write = db().transaction(() => {
156
+ let created = 0;
157
+ let updated = 0;
158
+ let skipped = 0;
159
+ let conflicts = 0;
160
+ let deleted = 0;
161
+ const seen = new Set<string>();
162
+ const writeItem = db().prepare(
163
+ "INSERT INTO wp_import_items (id,migration_id,entity_type,source_id,target_id,action,error,created_at) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(migration_id,entity_type,source_id) DO UPDATE SET target_id=excluded.target_id,action=excluded.action,error=excluded.error",
164
+ );
165
+ const conflictInsert = db().prepare(
166
+ "INSERT INTO migration_conflicts (id,migration_id,source_id,target_id,resolution,details_json,created_at) VALUES (?,?,?,?,?,?,?)",
167
+ );
168
+ for (const record of records) {
169
+ const sourceId = `${kind}:${record.sourceId}`;
170
+ seen.add(sourceId);
171
+ if (kind === "media-archive") {
172
+ const media = record.seo?.media_archive
173
+ ? JSON.parse(record.seo.media_archive)
174
+ : {};
175
+ const mediaId = id("media");
176
+ db()
177
+ .prepare(
178
+ "INSERT INTO media_assets (id,source,source_id,source_url,local_url,mime_type,alt_text,caption,width,height,metadata_json,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(source,source_id) DO UPDATE SET local_url=excluded.local_url,mime_type=excluded.mime_type,alt_text=excluded.alt_text,caption=excluded.caption,width=excluded.width,height=excluded.height,metadata_json=excluded.metadata_json,updated_at=excluded.updated_at",
179
+ )
180
+ .run(
181
+ mediaId,
182
+ "media-archive",
183
+ String(record.sourceId),
184
+ media.sourceUrl || record.canonicalUrl || "",
185
+ media.localUrl || null,
186
+ media.mimeType || null,
187
+ media.altText || record.title,
188
+ media.caption || "",
189
+ media.width || null,
190
+ media.height || null,
191
+ JSON.stringify(media),
192
+ timestamp,
193
+ timestamp,
194
+ );
195
+ writeItem.run(
196
+ id("import_item"),
197
+ migrationId,
198
+ "media",
199
+ sourceId,
200
+ mediaId,
201
+ "updated",
202
+ null,
203
+ timestamp,
204
+ );
205
+ updated++;
206
+ continue;
207
+ }
208
+ const existing = db()
209
+ .prepare("SELECT * FROM posts WHERE source='cms' AND source_id=?")
210
+ .get(sourceId) as any;
211
+ const recordChecksum = checksum(record);
212
+ if (
213
+ existing?.checksum === recordChecksum ||
214
+ (syncMode === "new" && existing) ||
215
+ (syncMode === "modified" &&
216
+ existing &&
217
+ record.sourceModified &&
218
+ existing.source_modified_at === record.sourceModified)
219
+ ) {
220
+ skipped++;
221
+ writeItem.run(
222
+ id("import_item"),
223
+ migrationId,
224
+ "post",
225
+ sourceId,
226
+ existing?.id || null,
227
+ "skipped",
228
+ "unchanged or excluded by sync mode",
229
+ timestamp,
230
+ );
231
+ continue;
232
+ }
233
+ if (
234
+ existing &&
235
+ conflict !== "source-wins" &&
236
+ existing.updated_at > (record.sourceModified || timestamp)
237
+ ) {
238
+ conflicts++;
239
+ conflictInsert.run(
240
+ id("conflict"),
241
+ migrationId,
242
+ sourceId,
243
+ existing.id,
244
+ conflict,
245
+ JSON.stringify({
246
+ sourceModified: record.sourceModified || null,
247
+ targetUpdated: existing.updated_at,
248
+ }),
249
+ timestamp,
250
+ );
251
+ if (conflict === "manual" || conflict === "target-wins") {
252
+ skipped++;
253
+ writeItem.run(
254
+ id("import_item"),
255
+ migrationId,
256
+ "post",
257
+ sourceId,
258
+ existing.id,
259
+ "skipped",
260
+ `${conflict} conflict requires review`,
261
+ timestamp,
262
+ );
263
+ continue;
264
+ }
265
+ }
266
+ const owner = db()
267
+ .prepare("SELECT id FROM posts WHERE slug=?")
268
+ .get(record.slug) as { id: string } | undefined;
269
+ const targetSlug =
270
+ owner && owner.id !== existing?.id
271
+ ? `${record.slug}-${record.sourceId}`
272
+ : record.slug;
273
+ const canonical =
274
+ record.canonicalUrl ||
275
+ new URL(
276
+ `/blog/${targetSlug}`,
277
+ import.meta.env.PUBLIC_SITE_URL || "https://example.com",
278
+ ).toString();
279
+ const seo = record.seo || {};
280
+ const values = {
281
+ slug: targetSlug,
282
+ title: record.title,
283
+ excerpt: record.excerpt,
284
+ html: record.html,
285
+ canonical,
286
+ status: record.publishedAt ? "published" : "draft",
287
+ publishedAt: record.publishedAt || null,
288
+ sourceModified: record.sourceModified || null,
289
+ seoTitle: seo.seo_title || null,
290
+ metaDescription: seo.meta_description || null,
291
+ focusKeyword: seo.focus_keyword || null,
292
+ canonicalOverride: seo.canonical_override || null,
293
+ robotsIndex: seo.robots_index || "index",
294
+ robotsFollow: seo.robots_follow || "follow",
295
+ ogImageUrl: seo.og_image_url || null,
296
+ schemaType: seo.schema_type || "Article",
297
+ customMetadata: metadata(record),
298
+ checksum: recordChecksum,
299
+ };
300
+ let targetId = existing?.id || id("post");
301
+ if (existing) {
302
+ const version =
303
+ Number(
304
+ (
305
+ db()
306
+ .prepare(
307
+ "SELECT MAX(version) version FROM post_revisions WHERE post_id=?",
308
+ )
309
+ .get(targetId) as any
310
+ )?.version || 0,
311
+ ) + 1;
312
+ db()
313
+ .prepare(
314
+ "INSERT INTO post_revisions (id,post_id,version,snapshot_json,actor_id,reason,created_at) VALUES (?,?,?,?,?,?,?)",
315
+ )
316
+ .run(
317
+ id("revision"),
318
+ targetId,
319
+ version,
320
+ JSON.stringify(existing),
321
+ "generic-migration",
322
+ "Generic migration",
323
+ );
324
+ db()
325
+ .prepare(
326
+ "UPDATE posts SET slug=?,title=?,excerpt=?,content_html=?,canonical_url=?,status=?,published_at=?,updated_at=?,source_modified_at=?,seo_title=?,meta_description=?,focus_keyword=?,canonical_override=?,robots_index=?,robots_follow=?,og_image_url=?,schema_type=?,custom_metadata_json=?,checksum=? WHERE id=?",
327
+ )
328
+ .run(
329
+ values.slug,
330
+ values.title,
331
+ values.excerpt,
332
+ values.html,
333
+ values.canonical,
334
+ values.status,
335
+ values.publishedAt,
336
+ timestamp,
337
+ values.sourceModified,
338
+ values.seoTitle,
339
+ values.metaDescription,
340
+ values.focusKeyword,
341
+ values.canonicalOverride,
342
+ values.robotsIndex,
343
+ values.robotsFollow,
344
+ values.ogImageUrl,
345
+ values.schemaType,
346
+ values.customMetadata,
347
+ values.checksum,
348
+ targetId,
349
+ );
350
+ updated++;
351
+ } else {
352
+ db()
353
+ .prepare(
354
+ "INSERT INTO posts (id,source,source_id,slug,title,excerpt,content_html,canonical_url,author_id,status,published_at,updated_at,created_at,source_modified_at,seo_title,meta_description,focus_keyword,canonical_override,robots_index,robots_follow,og_image_url,schema_type,custom_metadata_json,checksum) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
355
+ )
356
+ .run(
357
+ targetId,
358
+ "cms",
359
+ sourceId,
360
+ values.slug,
361
+ values.title,
362
+ values.excerpt,
363
+ values.html,
364
+ values.canonical,
365
+ authorId,
366
+ values.status,
367
+ values.publishedAt,
368
+ timestamp,
369
+ timestamp,
370
+ values.sourceModified,
371
+ values.seoTitle,
372
+ values.metaDescription,
373
+ values.focusKeyword,
374
+ values.canonicalOverride,
375
+ values.robotsIndex,
376
+ values.robotsFollow,
377
+ values.ogImageUrl,
378
+ values.schemaType,
379
+ values.customMetadata,
380
+ values.checksum,
381
+ );
382
+ created++;
383
+ }
384
+ writeItem.run(
385
+ id("import_item"),
386
+ migrationId,
387
+ "post",
388
+ sourceId,
389
+ targetId,
390
+ existing ? "updated" : "created",
391
+ null,
392
+ timestamp,
393
+ );
394
+ }
395
+ if (options.detectDeleted && kind !== "media-archive") {
396
+ const candidates = db()
397
+ .prepare(
398
+ "SELECT id,source_id FROM posts WHERE source='cms' AND source_id LIKE ?",
399
+ )
400
+ .all(`${kind}:%`) as Array<{ id: string; source_id: string }>;
401
+ for (const candidate of candidates)
402
+ if (!seen.has(candidate.source_id)) {
403
+ db()
404
+ .prepare(
405
+ "UPDATE posts SET status='archived',updated_at=? WHERE id=?",
406
+ )
407
+ .run(timestamp, candidate.id);
408
+ writeItem.run(
409
+ id("import_item"),
410
+ migrationId,
411
+ "post",
412
+ candidate.source_id,
413
+ candidate.id,
414
+ "updated",
415
+ "source deleted; archived locally",
416
+ timestamp,
417
+ );
418
+ deleted++;
419
+ }
420
+ }
421
+ return {
422
+ created,
423
+ updated,
424
+ skipped,
425
+ conflicts,
426
+ deleted,
427
+ total: records.length,
428
+ };
429
+ });
430
+ const counts = write();
431
+ db()
432
+ .prepare(
433
+ "UPDATE wp_migrations SET status='completed',counts_json=?,checkpoint_json=?,deleted_count=?,conflict_count=?,completed_at=? WHERE id=?",
434
+ )
435
+ .run(
436
+ JSON.stringify({ ...counts, sourceChecksum: aggregateChecksum }),
437
+ JSON.stringify({
438
+ processed: records.length,
439
+ total: records.length,
440
+ completed: true,
441
+ }),
442
+ counts.deleted,
443
+ counts.conflicts,
444
+ now(),
445
+ migrationId,
446
+ );
447
+ audit("migration.applied", "migration", migrationId, null, counts);
448
+ return json(
449
+ { id: migrationId, mode, counts, sourceChecksum: aggregateChecksum },
450
+ 201,
451
+ );
452
+ } catch (error) {
453
+ return json(
454
+ { error: error instanceof Error ? error.message : "migration failed" },
455
+ 422,
456
+ );
457
+ }
458
+ };
@@ -0,0 +1,5 @@
1
+ import type { APIRoute } from "astro";
2
+ import { db, id, now } from "../../../../lib/db";
3
+ import { json } from "../../../../lib/ops-auth";
4
+
5
+ export const POST: APIRoute = async ({ request }) => { const body = await request.json().catch(() => null) as any; const items = Array.isArray(body?.items) ? body.items : []; if (!items.length) return json({ error: "items is required" }, 400); const timestamp = now(); let created = 0; const tx = db().transaction(() => { for (const item of items) { if (!item.sourceUrl) continue; db().prepare("INSERT INTO media_assets (id,source,source_id,source_url,local_url,mime_type,alt_text,caption,width,height,metadata_json,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(source,source_id) DO UPDATE SET local_url=excluded.local_url,mime_type=excluded.mime_type,alt_text=excluded.alt_text,width=excluded.width,height=excluded.height,updated_at=excluded.updated_at").run(id("media"), "media-archive", String(item.sourceId || item.sourceUrl), item.sourceUrl, item.localUrl || null, item.mimeType || null, item.altText || "", item.caption || "", item.width || null, item.height || null, JSON.stringify({ archive: body.archive || null }), timestamp, timestamp); created++; } }); tx(); return json({ imported: created, status: "registered" }, 201); };