@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.
- package/README-zh-TW.md +2 -2
- package/README.md +19 -4
- package/bundled-skills/maggie-blog-bootstrap/SKILL.md +8 -0
- package/bundled-skills/maggie-clone/SKILL.md +7 -1
- package/bundled-templates/astro-blog/README.md +217 -0
- package/bundled-templates/astro-blog/examples/blog.config.ts +30 -0
- package/bundled-templates/astro-blog/examples/content.config.ts +28 -0
- package/bundled-templates/astro-blog/examples/metadata.ts +37 -0
- package/bundled-templates/astro-blog/examples/ops-api.ts +31 -0
- package/bundled-templates/astro-blog/examples/route-manifest.ts +21 -0
- package/bundled-templates/astro-blog/starter/.env.example +36 -0
- package/bundled-templates/astro-blog/starter/README.md +168 -0
- package/bundled-templates/astro-blog/starter/astro.config.mjs +19 -0
- package/bundled-templates/astro-blog/starter/db/schema.sql +508 -0
- package/bundled-templates/astro-blog/starter/package-lock.json +8039 -0
- package/bundled-templates/astro-blog/starter/package.json +41 -0
- package/bundled-templates/astro-blog/starter/scripts/contract-check.mjs +16 -0
- package/bundled-templates/astro-blog/starter/scripts/fixture-seed.mjs +11 -0
- package/bundled-templates/astro-blog/starter/scripts/schema-check.mjs +13 -0
- package/bundled-templates/astro-blog/starter/scripts/seed-db.mjs +20 -0
- package/bundled-templates/astro-blog/starter/src/data/demo.ts +52 -0
- package/bundled-templates/astro-blog/starter/src/layouts/BaseLayout.astro +53 -0
- package/bundled-templates/astro-blog/starter/src/layouts/OpsLayout.astro +73 -0
- package/bundled-templates/astro-blog/starter/src/lib/ai-cmo.ts +14 -0
- package/bundled-templates/astro-blog/starter/src/lib/config.ts +15 -0
- package/bundled-templates/astro-blog/starter/src/lib/db.ts +95 -0
- package/bundled-templates/astro-blog/starter/src/lib/google.ts +28 -0
- package/bundled-templates/astro-blog/starter/src/lib/media.ts +12 -0
- package/bundled-templates/astro-blog/starter/src/lib/migration-validation.ts +11 -0
- package/bundled-templates/astro-blog/starter/src/lib/migration.ts +21 -0
- package/bundled-templates/astro-blog/starter/src/lib/ops-auth.ts +21 -0
- package/bundled-templates/astro-blog/starter/src/lib/providers/email.ts +45 -0
- package/bundled-templates/astro-blog/starter/src/lib/providers/media-storage.ts +37 -0
- package/bundled-templates/astro-blog/starter/src/lib/report-delivery.ts +26 -0
- package/bundled-templates/astro-blog/starter/src/lib/report-pdf.ts +49 -0
- package/bundled-templates/astro-blog/starter/src/lib/repository.ts +72 -0
- package/bundled-templates/astro-blog/starter/src/lib/seo-audit.ts +72 -0
- package/bundled-templates/astro-blog/starter/src/lib/seo-insights.ts +14 -0
- package/bundled-templates/astro-blog/starter/src/lib/seo.ts +33 -0
- package/bundled-templates/astro-blog/starter/src/lib/types.ts +58 -0
- package/bundled-templates/astro-blog/starter/src/lib/wordpress.ts +847 -0
- package/bundled-templates/astro-blog/starter/src/middleware.ts +7 -0
- package/bundled-templates/astro-blog/starter/src/pages/404.astro +11 -0
- package/bundled-templates/astro-blog/starter/src/pages/about.astro +15 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/approval/[token].ts +25 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/consent.ts +8 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/agency.ts +47 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/authors.ts +11 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/bulk.ts +58 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/calendar/publish-due.ts +12 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/config/validate.ts +4 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/content-tracking/report-state/batch.ts +19 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/content-tracking/report-state.ts +15 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/ctas.ts +5 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/entities.ts +23 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/media/optimize.ts +5 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/media/upload.ts +30 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/[id]/resume.ts +28 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/[id]/validate.ts +6 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/[id].ts +7 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/import.ts +458 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/media.ts +5 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/operations.ts +189 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/playbooks/[id]/run.ts +55 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id]/approve.ts +13 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id]/preview.ts +9 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id]/publish.ts +13 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id].ts +18 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/index.ts +28 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/providers.ts +6 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/history.ts +3 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/latest.ts +3 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/posts.ts +3 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/project-context.ts +4 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/sync-updates.ts +43 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/sync.ts +172 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/updates.ts +3 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/redirects/check.ts +5 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/redirects.ts +32 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports/deliver-due.ts +12 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports/deliver.ts +6 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports/export.ts +17 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports.ts +31 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/rewrite/history/[contentId].ts +3 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/rewrite/policy.ts +4 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/rewrite/queue.ts +4 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/seo/audit.ts +7 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/seo/insights.ts +5 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/settings/site.ts +7 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/settings.ts +19 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/auto-detect.ts +13 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/diff.ts +11 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/match.ts +16 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/matching-history.ts +4 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/runs.ts +4 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap.ts +11 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/summary.ts +12 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/topics/[id]/faq.ts +12 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/topics.ts +11 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/capabilities.ts +5 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migration-plan.ts +21 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations/[id]/resume.ts +5 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations/[id]/validate.ts +6 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations/[id].ts +13 -0
- package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations.ts +147 -0
- package/bundled-templates/astro-blog/starter/src/pages/authors/[slug].astro +12 -0
- package/bundled-templates/astro-blog/starter/src/pages/blog/[slug].astro +25 -0
- package/bundled-templates/astro-blog/starter/src/pages/blog/index.astro +32 -0
- package/bundled-templates/astro-blog/starter/src/pages/blog/page/[page].astro +21 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/index.astro +20 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/operations.astro +33 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/posts/[id]/edit.astro +17 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/posts/[id]/preview.astro +11 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/posts/index.astro +15 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/posts/new.astro +12 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/reports.astro +12 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/rewrite.astro +13 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/settings/integrations.astro +13 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/settings/site.astro +11 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/sitemap.astro +15 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/topics.astro +13 -0
- package/bundled-templates/astro-blog/starter/src/pages/ops/wordpress.astro +34 -0
- package/bundled-templates/astro-blog/starter/src/pages/review/[token].astro +13 -0
- package/bundled-templates/astro-blog/starter/src/pages/robots.txt.ts +4 -0
- package/bundled-templates/astro-blog/starter/src/pages/sitemap.xml.ts +12 -0
- package/bundled-templates/astro-blog/starter/src/pages/topics/[slug]/page/[page].astro +10 -0
- package/bundled-templates/astro-blog/starter/src/pages/topics/[slug].astro +19 -0
- package/bundled-templates/astro-blog/starter/src/pages/topics/index.astro +10 -0
- package/bundled-templates/astro-blog/starter/src/styles/global.css +4 -0
- package/bundled-templates/astro-blog/starter/tsconfig.json +9 -0
- package/bundled-templates/astro-blog/starter/wrangler.jsonc +52 -0
- package/bundled-tools/clis/maggie_clone.py +42 -15
- package/bundled-tools/clis/maggie_dash.py +41 -25
- package/bundled-tools/clis/maggie_design.py +2 -0
- package/bundled-tools/clis/maggie_design_system.py +7 -1
- package/bundled-tools/clis/site_audit.py +25 -2
- package/bundled-tools/runtime/maggie_memory.py +6 -1
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
3
|
+
import { postsForTopic, siteUrl, topicBySlug } from "../../lib/repository";
|
|
4
|
+
import { topicJsonLd } from "../../lib/seo";
|
|
5
|
+
import type { Topic } from "../../lib/types";
|
|
6
|
+
|
|
7
|
+
const topic = topicBySlug(Astro.params.slug || "") as Topic | undefined;
|
|
8
|
+
if (!topic) { Astro.response.status = 404; return Astro.rewrite("/404"); }
|
|
9
|
+
const topicPosts = postsForTopic(topic.slug);
|
|
10
|
+
const pageSize = 12;
|
|
11
|
+
---
|
|
12
|
+
<BaseLayout title={topic.title} description={topic.description} canonical={siteUrl(`/topics/${topic.slug}`)} jsonLd={topicJsonLd(topic, siteUrl(`/topics/${topic.slug}`))}>
|
|
13
|
+
<h1>{topic.title}</h1>
|
|
14
|
+
<p>{topic.description}</p>
|
|
15
|
+
<section><h2>Articles</h2>{topicPosts.slice(0, pageSize).map((post) => <article><h3><a href={`/blog/${post.slug}`}>{post.title}</a></h3><p>{post.excerpt}</p></article>)}</section>
|
|
16
|
+
{topicPosts.length > pageSize && <nav aria-label="Topic pagination"><a href={`/topics/${topic.slug}/page/2`}>Next</a></nav>}
|
|
17
|
+
<aside><h2>Keep learning</h2><a href="/get-started">Get started</a></aside>
|
|
18
|
+
{topic.faq.length > 0 && <section><h2>FAQs</h2>{topic.faq.map((faq) => <details><summary>{faq.question}</summary><p>{faq.answer}</p></details>)}</section>}
|
|
19
|
+
</BaseLayout>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
3
|
+
import { allTopics, siteUrl } from "../../lib/repository";
|
|
4
|
+
const topics = allTopics();
|
|
5
|
+
const publishedTopics = topics.filter((topic) => topic.status === "published");
|
|
6
|
+
---
|
|
7
|
+
<BaseLayout title="Topics" description="Browse topics." canonical={siteUrl("/topics")} robots={publishedTopics.length ? "index,follow" : "noindex,follow"}>
|
|
8
|
+
<h1>Topics</h1>
|
|
9
|
+
<div class="topic-grid">{publishedTopics.map((topic) => <article><h2><a href={`/topics/${topic.slug}`}>{topic.title}</a></h2><p>{topic.description}</p></article>)}</div>
|
|
10
|
+
</BaseLayout>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "maggie-blog",
|
|
4
|
+
"compatibility_date": "2026-08-27",
|
|
5
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
6
|
+
"workers_dev": true,
|
|
7
|
+
"assets": {
|
|
8
|
+
"binding": "ASSETS",
|
|
9
|
+
"directory": "./dist"
|
|
10
|
+
},
|
|
11
|
+
"d1_databases": [
|
|
12
|
+
{
|
|
13
|
+
"binding": "DB",
|
|
14
|
+
"database_name": "maggie-blog"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"r2_buckets": [
|
|
18
|
+
{
|
|
19
|
+
"binding": "MEDIA",
|
|
20
|
+
"bucket_name": "maggie-blog-media"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"kv_namespaces": [
|
|
24
|
+
{
|
|
25
|
+
"binding": "CACHE"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"env": {
|
|
29
|
+
"staging": {
|
|
30
|
+
"name": "maggie-blog-staging",
|
|
31
|
+
"workers_dev": true,
|
|
32
|
+
"vars": {
|
|
33
|
+
"PUBLIC_SITE_ENV": "staging",
|
|
34
|
+
"MAGGIE_DEPLOY_PROVIDER": "cloudflare"
|
|
35
|
+
},
|
|
36
|
+
"d1_databases": [{ "binding": "DB", "database_name": "maggie-blog-staging" }],
|
|
37
|
+
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "maggie-blog-media-staging" }],
|
|
38
|
+
"kv_namespaces": [{ "binding": "CACHE" }]
|
|
39
|
+
},
|
|
40
|
+
"production": {
|
|
41
|
+
"name": "maggie-blog-production",
|
|
42
|
+
"workers_dev": false,
|
|
43
|
+
"vars": {
|
|
44
|
+
"PUBLIC_SITE_ENV": "production",
|
|
45
|
+
"MAGGIE_DEPLOY_PROVIDER": "cloudflare"
|
|
46
|
+
},
|
|
47
|
+
"d1_databases": [{ "binding": "DB", "database_name": "maggie-blog" }],
|
|
48
|
+
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "maggie-blog-media" }],
|
|
49
|
+
"kv_namespaces": [{ "binding": "CACHE" }]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -57,6 +57,12 @@ def normalized(url: str) -> str:
|
|
|
57
57
|
def key_part(value, fallback):
|
|
58
58
|
return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")[:48] or fallback
|
|
59
59
|
|
|
60
|
+
def css_urls(value: str) -> list[str]:
|
|
61
|
+
return [html.unescape(match).strip().strip("\"'") for match in re.findall(r"url\(\s*([^)]*?)\s*\)", value or "", re.I)]
|
|
62
|
+
|
|
63
|
+
def srcset_urls(value: str) -> list[str]:
|
|
64
|
+
return [part.strip().split()[0] for part in (value or "").split(",") if part.strip()]
|
|
65
|
+
|
|
60
66
|
def plan(urls, project):
|
|
61
67
|
result, seen = [], set()
|
|
62
68
|
for raw in urls:
|
|
@@ -97,22 +103,26 @@ def rendered_quality(raw, parsed):
|
|
|
97
103
|
|
|
98
104
|
class PageParser(HTMLParser):
|
|
99
105
|
def __init__(self):
|
|
100
|
-
super().__init__(); self.title = ""; self._title = False; self._heading = None; self._text = []; self.headings = []; self.links = []; self.images = []; self.stylesheets = []; self.metas = {}
|
|
106
|
+
super().__init__(); self.title = ""; self._title = False; self._heading = None; self._text = []; self._style = False; self._style_text = []; self.headings = []; self.links = []; self.images = []; self.backgrounds = []; self.stylesheets = []; self.metas = {}
|
|
101
107
|
def handle_starttag(self, tag, attrs):
|
|
102
108
|
values = dict(attrs)
|
|
103
109
|
if tag == "title": self._title = True
|
|
110
|
+
elif tag == "style": self._style = True; self._style_text = []
|
|
104
111
|
elif tag in {"h1", "h2", "h3", "h4", "h5", "h6"}: self._heading, self._text = tag, []
|
|
105
112
|
elif tag == "a" and values.get("href"): self.links.append({"href": values["href"], "text": ""})
|
|
106
|
-
elif tag
|
|
113
|
+
elif tag in {"img", "source"} and (values.get("src") or values.get("srcset")): self.images.append({"src": values.get("src", ""), "srcset": values.get("srcset", ""), "alt": values.get("alt", "") or ""})
|
|
107
114
|
elif tag == "link" and values.get("rel", "").lower() == "stylesheet" and values.get("href"): self.stylesheets.append(values["href"])
|
|
108
115
|
elif tag == "meta":
|
|
109
116
|
key = values.get("name") or values.get("property")
|
|
110
117
|
if key and values.get("content") is not None: self.metas[key] = values["content"]
|
|
118
|
+
if values.get("style"): self.backgrounds.extend(css_urls(values["style"]))
|
|
111
119
|
def handle_endtag(self, tag):
|
|
112
120
|
if tag == "title": self._title = False
|
|
121
|
+
elif tag == "style": self.backgrounds.extend(css_urls("".join(self._style_text))); self._style = False; self._style_text = []
|
|
113
122
|
elif tag == self._heading: self.headings.append({"tag": tag, "text": " ".join(self._text).strip()}); self._heading, self._text = None, []
|
|
114
123
|
def handle_data(self, data):
|
|
115
124
|
value = " ".join(data.split())
|
|
125
|
+
if self._style: self._style_text.append(data)
|
|
116
126
|
if self._title: self.title += value
|
|
117
127
|
if self._heading: self._text.append(value)
|
|
118
128
|
if self.links and not self._heading: self.links[-1]["text"] = (self.links[-1]["text"] + " " + value).strip()
|
|
@@ -221,12 +231,17 @@ def command_extract(args):
|
|
|
221
231
|
slug, pages = target["page_key"], []
|
|
222
232
|
quality = []
|
|
223
233
|
for name in VIEWPORTS:
|
|
224
|
-
raw = (root / "html/source" / slug / f"{name}.html").read_text(encoding="utf-8"); parser = PageParser(); parser.feed(raw); pages.append({"viewport": name, "title": parser.title, "headings": parser.headings, "links": parser.links, "images": parser.images, "stylesheets": parser.stylesheets, "meta": parser.metas, "html_bytes": len(raw.encode())})
|
|
234
|
+
raw = (root / "html/source" / slug / f"{name}.html").read_text(encoding="utf-8"); parser = PageParser(); parser.feed(raw); pages.append({"viewport": name, "title": parser.title, "headings": parser.headings, "links": parser.links, "images": parser.images, "backgrounds": parser.backgrounds, "stylesheets": parser.stylesheets, "meta": parser.metas, "html_bytes": len(raw.encode())})
|
|
225
235
|
quality.append({"viewport": name, **rendered_quality(raw, parser)})
|
|
226
236
|
if not all(item["rendered"] for item in quality):
|
|
227
237
|
raise RuntimeError(f"preview did not render content for {target['source_url']}; captured a loading/empty shell at one or more viewports")
|
|
228
238
|
save_json(root / "extracted" / slug / "page.json", {"source_url": target["source_url"], "pages": pages})
|
|
229
|
-
|
|
239
|
+
asset_records = []
|
|
240
|
+
for page in pages:
|
|
241
|
+
asset_records.extend({"src": image.get("src", ""), "kind": "image"} for image in page["images"] if image.get("src"))
|
|
242
|
+
asset_records.extend({"src": value, "kind": "responsive-image"} for image in page["images"] for value in srcset_urls(image.get("srcset", "")))
|
|
243
|
+
asset_records.extend({"src": value, "kind": "background"} for value in page.get("backgrounds", []))
|
|
244
|
+
assets = [json.loads(x) for x in sorted({json.dumps(x, sort_keys=True) for x in asset_records})]
|
|
230
245
|
save_json(root / "extracted" / slug / "ASSET_MANIFEST.json", {"source_url": target["source_url"], "assets": assets})
|
|
231
246
|
lines = [f"# {target['source_url']}", "", "## Headings", ""] + [f"- `{x['tag']}` {x['text']}" for x in pages[0]["headings"]] + ["", "## Evidence", "", "- DOM and screenshots are stored in the clone run.", "- Computed CSS and interaction states require browser evidence; do not infer them from classes."]
|
|
232
247
|
(root / "extracted" / slug / "PAGE_TOPOLOGY.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
@@ -240,19 +255,31 @@ def command_assets(args):
|
|
|
240
255
|
root, manifest = read_manifest(args.project.resolve(), args.run_id); require_phase(manifest, "extracted"); report = []
|
|
241
256
|
for target in manifest["targets"]:
|
|
242
257
|
slug = target["page_key"]; data = json.loads((root / "extracted" / slug / "page.json").read_text(encoding="utf-8")); seen = set()
|
|
258
|
+
references = []
|
|
243
259
|
for page in data["pages"]:
|
|
244
|
-
for
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
260
|
+
references.extend({"src": image.get("src", ""), "alt": image.get("alt", "")} for image in page["images"] if image.get("src"))
|
|
261
|
+
references.extend({"src": value, "alt": ""} for image in page["images"] for value in srcset_urls(image.get("srcset", "")))
|
|
262
|
+
references.extend({"src": value, "alt": ""} for value in page.get("backgrounds", []))
|
|
263
|
+
for stylesheet in page.get("stylesheets", []):
|
|
264
|
+
stylesheet_url = urllib.parse.urljoin(target.get("capture_url", target["source_url"]), html.unescape(stylesheet))
|
|
248
265
|
try:
|
|
249
|
-
with urllib.request.urlopen(urllib.request.Request(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
266
|
+
with urllib.request.urlopen(urllib.request.Request(stylesheet_url, headers={"User-Agent": "MaggieClone/1.0"}), timeout=30) as response:
|
|
267
|
+
css = response.read(args.max_bytes).decode(response.headers.get_content_charset() or "utf-8", "replace")
|
|
268
|
+
references.extend({"src": value, "alt": ""} for value in css_urls(css))
|
|
269
|
+
except (OSError, urllib.error.URLError):
|
|
270
|
+
pass
|
|
271
|
+
for asset in references:
|
|
272
|
+
absolute = urllib.parse.urljoin(target.get("capture_url", target["source_url"]), html.unescape(asset["src"]))
|
|
273
|
+
if absolute in seen: continue
|
|
274
|
+
seen.add(absolute); item = {"url": absolute, "alt": asset.get("alt", ""), "status": "discovered"}
|
|
275
|
+
try:
|
|
276
|
+
with urllib.request.urlopen(urllib.request.Request(absolute, headers={"User-Agent": "MaggieClone/1.0"}), timeout=30) as response:
|
|
277
|
+
body = response.read(args.max_bytes + 1); item.update({"status": "downloaded", "content_type": response.headers.get_content_type(), "bytes": len(body)})
|
|
278
|
+
if len(body) <= args.max_bytes:
|
|
279
|
+
suffix = Path(urllib.parse.urlsplit(absolute).path).suffix[:8]; destination = root / "assets" / slug / (hashlib.sha256(absolute.encode()).hexdigest()[:16] + suffix); destination.parent.mkdir(parents=True, exist_ok=True); destination.write_bytes(body); item["local_path"] = str(destination.relative_to(root))
|
|
280
|
+
project_asset = Path(target["asset_root"]) / destination.name; project_asset.parent.mkdir(parents=True, exist_ok=True); shutil.copy2(destination, project_asset); item["project_path"] = str(project_asset)
|
|
281
|
+
except (OSError, urllib.error.URLError) as error: item.update({"status": "failed", "error": str(error)})
|
|
282
|
+
report.append(item)
|
|
256
283
|
save_json(root / "assets" / "manifest.json", {"created_at": now(), "items": report}); set_phase(root, manifest, "assets"); print(f"asset audit: {len(report)} unique image(s), {sum(x['status'] == 'downloaded' for x in report)} downloaded"); return 0
|
|
257
284
|
|
|
258
285
|
def command_compare(args):
|
|
@@ -40,6 +40,19 @@ def store_for(root: Path) -> MaggieDashStore:
|
|
|
40
40
|
return MaggieDashStore(root / ".maggie" / "maggiedash.sqlite")
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def resolve_project_id(args: argparse.Namespace) -> str:
|
|
44
|
+
"""Use the persisted project identity when the CLI flag is omitted."""
|
|
45
|
+
explicit = getattr(args, "project_id", None)
|
|
46
|
+
if explicit:
|
|
47
|
+
return explicit
|
|
48
|
+
contract = project_root(args) / ".maggie" / "maggiedash.json"
|
|
49
|
+
try:
|
|
50
|
+
value = json.loads(contract.read_text(encoding="utf-8")).get("id")
|
|
51
|
+
except (OSError, json.JSONDecodeError, AttributeError):
|
|
52
|
+
value = None
|
|
53
|
+
return str(value).strip() if value else "local-project"
|
|
54
|
+
|
|
55
|
+
|
|
43
56
|
def emit(value: object) -> None:
|
|
44
57
|
print(json.dumps(value, indent=2, ensure_ascii=False))
|
|
45
58
|
|
|
@@ -296,10 +309,11 @@ def command_init(args: argparse.Namespace) -> int:
|
|
|
296
309
|
|
|
297
310
|
def command_status(args: argparse.Namespace) -> int:
|
|
298
311
|
root = project_root(args)
|
|
312
|
+
project_id = resolve_project_id(args)
|
|
299
313
|
store = store_for(root)
|
|
300
314
|
try:
|
|
301
|
-
project = store.connection.execute("SELECT * FROM maggiedash_projects WHERE id=?", (
|
|
302
|
-
documents = store.connection.execute("SELECT id, kind, title, slug, status, updated_at FROM maggiedash_documents WHERE project_id=? ORDER BY updated_at DESC", (
|
|
315
|
+
project = store.connection.execute("SELECT * FROM maggiedash_projects WHERE id=?", (project_id,)).fetchone()
|
|
316
|
+
documents = store.connection.execute("SELECT id, kind, title, slug, status, updated_at FROM maggiedash_documents WHERE project_id=? ORDER BY updated_at DESC", (project_id,)).fetchall()
|
|
303
317
|
emit({"status": "ready" if project else "not_initialized", "project": dict(project) if project else None, "documents": [dict(row) for row in documents], "storage": str(store.path)})
|
|
304
318
|
return 0 if project else 1
|
|
305
319
|
finally:
|
|
@@ -321,9 +335,10 @@ def command_migrate(args: argparse.Namespace) -> int:
|
|
|
321
335
|
|
|
322
336
|
def command_transition(args: argparse.Namespace) -> int:
|
|
323
337
|
require_confirm(args)
|
|
338
|
+
project_id = resolve_project_id(args)
|
|
324
339
|
store = store_for(project_root(args))
|
|
325
340
|
try:
|
|
326
|
-
emit(store.transition(
|
|
341
|
+
emit(store.transition(project_id, args.document_id, args.status, args.actor, args.reason))
|
|
327
342
|
return 0
|
|
328
343
|
finally:
|
|
329
344
|
store.close()
|
|
@@ -331,31 +346,32 @@ def command_transition(args: argparse.Namespace) -> int:
|
|
|
331
346
|
|
|
332
347
|
def command_cms(args: argparse.Namespace) -> int:
|
|
333
348
|
require_confirm(args)
|
|
349
|
+
project_id = resolve_project_id(args)
|
|
334
350
|
store = store_for(project_root(args))
|
|
335
351
|
try:
|
|
336
352
|
if args.cms_command == "revisions":
|
|
337
|
-
result = store.list_revisions(
|
|
353
|
+
result = store.list_revisions(project_id, args.document_id)
|
|
338
354
|
elif args.cms_command == "trash":
|
|
339
|
-
result = store.trash(
|
|
355
|
+
result = store.trash(project_id, args.document_id, args.actor, args.reason)
|
|
340
356
|
elif args.cms_command == "restore":
|
|
341
|
-
result = store.restore(
|
|
357
|
+
result = store.restore(project_id, args.document_id, args.actor, args.reason)
|
|
342
358
|
elif args.cms_command == "schedule":
|
|
343
|
-
result = store.schedule_publish(
|
|
359
|
+
result = store.schedule_publish(project_id, args.document_id, args.publish_at, args.actor, args.reason)
|
|
344
360
|
elif args.cms_command == "publish-due":
|
|
345
|
-
result = store.publish_due(
|
|
361
|
+
result = store.publish_due(project_id, args.now, args.actor, args.reason)
|
|
346
362
|
elif args.cms_command == "duplicate":
|
|
347
|
-
result = store.duplicate(
|
|
363
|
+
result = store.duplicate(project_id, args.document_id, args.new_id, args.new_slug, args.actor, args.reason)
|
|
348
364
|
elif args.cms_command == "redirect":
|
|
349
|
-
result = store.add_redirect(
|
|
365
|
+
result = store.add_redirect(project_id, args.from_path, args.to_path, args.actor, args.reason, args.status_code)
|
|
350
366
|
elif args.cms_command == "preview":
|
|
351
|
-
result = store.issue_preview(
|
|
367
|
+
result = store.issue_preview(project_id, args.document_id, args.secret, args.ttl)
|
|
352
368
|
elif args.cms_command == "preset-save":
|
|
353
369
|
sections = json.loads(Path(args.sections_file).resolve().read_text(encoding="utf-8"))
|
|
354
|
-
result = store.save_section_preset(
|
|
370
|
+
result = store.save_section_preset(project_id, args.preset_id, args.name, sections, args.actor, args.description)
|
|
355
371
|
elif args.cms_command == "preset-list":
|
|
356
|
-
result = store.list_section_presets(
|
|
372
|
+
result = store.list_section_presets(project_id)
|
|
357
373
|
elif args.cms_command == "preset-insert":
|
|
358
|
-
result = store.insert_section_preset(
|
|
374
|
+
result = store.insert_section_preset(project_id, args.preset_id, args.existing_id, args.actor, args.reason)
|
|
359
375
|
else:
|
|
360
376
|
raise ValueError(f"unknown CMS command: {args.cms_command}")
|
|
361
377
|
emit(result)
|
|
@@ -622,7 +638,7 @@ def parser() -> argparse.ArgumentParser:
|
|
|
622
638
|
install.set_defaults(func=command_install)
|
|
623
639
|
status = sub.add_parser("status", help="inspect project and documents")
|
|
624
640
|
status.add_argument("--project", default=".")
|
|
625
|
-
status.add_argument("--project-id"
|
|
641
|
+
status.add_argument("--project-id")
|
|
626
642
|
status.set_defaults(func=command_status)
|
|
627
643
|
migrate = sub.add_parser("migrate", help="apply the local MaggieDash schema")
|
|
628
644
|
migrate.add_argument("--project", default=".")
|
|
@@ -630,7 +646,7 @@ def parser() -> argparse.ArgumentParser:
|
|
|
630
646
|
migrate.set_defaults(func=command_migrate)
|
|
631
647
|
transition = sub.add_parser("transition", help="move a document through its approval lifecycle")
|
|
632
648
|
transition.add_argument("--project", default=".")
|
|
633
|
-
transition.add_argument("--project-id"
|
|
649
|
+
transition.add_argument("--project-id")
|
|
634
650
|
transition.add_argument("--document-id", required=True)
|
|
635
651
|
transition.add_argument("--status", required=True)
|
|
636
652
|
transition.add_argument("--actor", default="cli")
|
|
@@ -641,24 +657,24 @@ def parser() -> argparse.ArgumentParser:
|
|
|
641
657
|
cms_sub = cms.add_subparsers(dest="cms_command", required=True)
|
|
642
658
|
for name in ("revisions", "trash", "restore"):
|
|
643
659
|
command = cms_sub.add_parser(name)
|
|
644
|
-
command.add_argument("--project", default="."); command.add_argument("--project-id"
|
|
660
|
+
command.add_argument("--project", default="."); command.add_argument("--project-id")
|
|
645
661
|
command.add_argument("--document-id", required=True); command.add_argument("--actor", default="cli"); command.add_argument("--reason", default="CMS operation"); command.add_argument("--confirm", action="store_true")
|
|
646
662
|
schedule = cms_sub.add_parser("schedule")
|
|
647
|
-
schedule.add_argument("--project", default="."); schedule.add_argument("--project-id"
|
|
663
|
+
schedule.add_argument("--project", default="."); schedule.add_argument("--project-id"); schedule.add_argument("--document-id", required=True); schedule.add_argument("--publish-at", required=True); schedule.add_argument("--actor", default="cli"); schedule.add_argument("--reason", required=True); schedule.add_argument("--confirm", action="store_true")
|
|
648
664
|
publish_due = cms_sub.add_parser("publish-due", help="publish approved scheduled content whose time has arrived")
|
|
649
|
-
publish_due.add_argument("--project", default="."); publish_due.add_argument("--project-id"
|
|
665
|
+
publish_due.add_argument("--project", default="."); publish_due.add_argument("--project-id"); publish_due.add_argument("--now"); publish_due.add_argument("--actor", default="scheduler"); publish_due.add_argument("--reason", default="scheduled publish tick"); publish_due.add_argument("--confirm", action="store_true")
|
|
650
666
|
duplicate = cms_sub.add_parser("duplicate")
|
|
651
|
-
duplicate.add_argument("--project", default="."); duplicate.add_argument("--project-id"
|
|
667
|
+
duplicate.add_argument("--project", default="."); duplicate.add_argument("--project-id"); duplicate.add_argument("--document-id", required=True); duplicate.add_argument("--new-id", required=True); duplicate.add_argument("--new-slug", required=True); duplicate.add_argument("--actor", default="cli"); duplicate.add_argument("--reason", required=True); duplicate.add_argument("--confirm", action="store_true")
|
|
652
668
|
redirect = cms_sub.add_parser("redirect")
|
|
653
|
-
redirect.add_argument("--project", default="."); redirect.add_argument("--project-id"
|
|
669
|
+
redirect.add_argument("--project", default="."); redirect.add_argument("--project-id"); redirect.add_argument("--from-path", required=True); redirect.add_argument("--to-path", required=True); redirect.add_argument("--status-code", type=int, default=301); redirect.add_argument("--actor", default="cli"); redirect.add_argument("--reason", required=True); redirect.add_argument("--confirm", action="store_true")
|
|
654
670
|
preview = cms_sub.add_parser("preview")
|
|
655
|
-
preview.add_argument("--project", default="."); preview.add_argument("--project-id"
|
|
671
|
+
preview.add_argument("--project", default="."); preview.add_argument("--project-id"); preview.add_argument("--document-id", required=True); preview.add_argument("--secret", required=True); preview.add_argument("--ttl", type=int, default=900); preview.add_argument("--confirm", action="store_true")
|
|
656
672
|
preset_save = cms_sub.add_parser("preset-save", help="save a reusable ordered section arrangement")
|
|
657
|
-
preset_save.add_argument("--project", default="."); preset_save.add_argument("--project-id"
|
|
673
|
+
preset_save.add_argument("--project", default="."); preset_save.add_argument("--project-id"); preset_save.add_argument("--preset-id", required=True); preset_save.add_argument("--name", required=True); preset_save.add_argument("--sections-file", required=True); preset_save.add_argument("--description", default=""); preset_save.add_argument("--actor", default="cli"); preset_save.add_argument("--confirm", action="store_true")
|
|
658
674
|
preset_list = cms_sub.add_parser("preset-list", help="list reusable section arrangements")
|
|
659
|
-
preset_list.add_argument("--project", default="."); preset_list.add_argument("--project-id"
|
|
675
|
+
preset_list.add_argument("--project", default="."); preset_list.add_argument("--project-id"); preset_list.add_argument("--confirm", action="store_true")
|
|
660
676
|
preset_insert = cms_sub.add_parser("preset-insert", help="expand an arrangement with fresh section IDs")
|
|
661
|
-
preset_insert.add_argument("--project", default="."); preset_insert.add_argument("--project-id"
|
|
677
|
+
preset_insert.add_argument("--project", default="."); preset_insert.add_argument("--project-id"); preset_insert.add_argument("--preset-id", required=True); preset_insert.add_argument("--existing-id", action="append", default=[]); preset_insert.add_argument("--actor", default="cli"); preset_insert.add_argument("--reason", required=True); preset_insert.add_argument("--confirm", action="store_true")
|
|
662
678
|
cms.set_defaults(func=command_cms)
|
|
663
679
|
ui = sub.add_parser("ui", help="validate MaggieDash dashboard chrome")
|
|
664
680
|
ui_sub = ui.add_subparsers(dest="ui_command", required=True)
|
|
@@ -655,6 +655,8 @@ def in_place_job(project: Path, routes: list[str], force: bool = False, content_
|
|
|
655
655
|
project / relative,
|
|
656
656
|
project / "src" / "pages" / relative,
|
|
657
657
|
project / "src" / "app" / relative,
|
|
658
|
+
project / "src" / "pages" / relative / "index.astro",
|
|
659
|
+
project / "src" / "pages" / relative / "index.html",
|
|
658
660
|
project / "src" / "pages" / f"{relative}.astro",
|
|
659
661
|
project / "src" / "pages" / f"{relative}.html",
|
|
660
662
|
project / "src" / "app" / relative / "page.tsx",
|
|
@@ -91,7 +91,13 @@ def load_run(project: Path, run_id: str) -> tuple[Path, dict, list[dict]]:
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
def sample_body(sample: Path) -> str:
|
|
94
|
-
|
|
94
|
+
try:
|
|
95
|
+
text = sample.read_text(encoding="utf-8")
|
|
96
|
+
except UnicodeDecodeError as error:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"design sample must be a UTF-8 DESIGN.md/text file, not a binary image: {sample}; "
|
|
99
|
+
"use text metadata or convert the image into approved design tokens first"
|
|
100
|
+
) from error
|
|
95
101
|
return text.split("---", 2)[2].strip() if text.startswith("---") and text.count("---") >= 2 else text.strip()
|
|
96
102
|
|
|
97
103
|
|
|
@@ -18,6 +18,29 @@ from content_localization import MARKETS, parse_locale, valid_locale # noqa: E4
|
|
|
18
18
|
import site_baseline
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def stable_structure_reference(key: str, value: str) -> str:
|
|
22
|
+
"""Ignore cache/content hashes in static asset references only."""
|
|
23
|
+
if key not in {"src", "href"} or not value:
|
|
24
|
+
return value
|
|
25
|
+
parsed = urlparse(value)
|
|
26
|
+
path = parsed.path
|
|
27
|
+
if not re.search(r"\.(?:css|js|mjs|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf)$", path, re.I):
|
|
28
|
+
return value
|
|
29
|
+
path = re.sub(r"(?:[-_.])[0-9a-f]{8,64}(?=\.[^.]+$)", "-asset", path, flags=re.I)
|
|
30
|
+
return urlunparse((parsed.scheme, parsed.netloc, path, parsed.params, "", ""))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def stable_structure(structure: list[list[object]]) -> list[list[object]]:
|
|
34
|
+
normalized = []
|
|
35
|
+
for entry in structure:
|
|
36
|
+
if len(entry) != 3 or not isinstance(entry[2], dict):
|
|
37
|
+
normalized.append(entry)
|
|
38
|
+
continue
|
|
39
|
+
attrs = {key: stable_structure_reference(key, str(value)) for key, value in entry[2].items()}
|
|
40
|
+
normalized.append([entry[0], entry[1], attrs])
|
|
41
|
+
return normalized
|
|
42
|
+
|
|
43
|
+
|
|
21
44
|
class PageParser(HTMLParser):
|
|
22
45
|
def __init__(self) -> None:
|
|
23
46
|
super().__init__()
|
|
@@ -148,7 +171,7 @@ def audit_page(url: str, html: str, status: int, content_type: str, expected_lan
|
|
|
148
171
|
"twitter_card": page.meta.get("twitter:card") in {"summary", "summary_large_image"},
|
|
149
172
|
"jsonld": page.jsonld > 0 and all(item is not None for item in page.jsonld_values),
|
|
150
173
|
"entity_jsonld": any(isinstance(item, dict) and item.get("@type") and (item.get("url") or item.get("@id")) for item in page.jsonld_values),
|
|
151
|
-
"images": all(image["src"] and image["alt"] for image in page.images),
|
|
174
|
+
"images": all(image["src"] and image["alt"] is not None for image in page.images),
|
|
152
175
|
"canonical_value": canonical,
|
|
153
176
|
"image_count": len(page.images),
|
|
154
177
|
"language": parse_locale(page.lang)[0],
|
|
@@ -161,7 +184,7 @@ def audit_page(url: str, html: str, status: int, content_type: str, expected_lan
|
|
|
161
184
|
"title": page.title, "meta": page.meta, "canonical": canonical,
|
|
162
185
|
"lang": page.lang, "hreflang": page.hreflang, "robots": robots,
|
|
163
186
|
"jsonld": page.jsonld_values, "images": page.images,
|
|
164
|
-
"structureHash": hashlib.sha256(json.dumps(page.structure, sort_keys=True).encode()).hexdigest(),
|
|
187
|
+
"structureHash": hashlib.sha256(json.dumps(stable_structure(page.structure), sort_keys=True).encode()).hexdigest(),
|
|
165
188
|
"textHash": hashlib.sha256(" ".join(page.visible_text).encode()).hexdigest(),
|
|
166
189
|
},
|
|
167
190
|
"contentContract": {
|
|
@@ -21,7 +21,12 @@ FILES = {
|
|
|
21
21
|
KINDS = set(FILES)
|
|
22
22
|
SCOPES = {"user", "workspace", "project"}
|
|
23
23
|
STATUSES = {"candidate", "active", "superseded", "archived"}
|
|
24
|
-
|
|
24
|
+
# Reject credential-shaped assignments, not ordinary prose that happens to
|
|
25
|
+
# mention a security word (for example, "use design tokens").
|
|
26
|
+
SECRET_PATTERN = re.compile(
|
|
27
|
+
r"(?:api[_-]?key|secret|token|password|private[_-]?key|oauth)\s*(?:=|:)\s*[^\s,;]+",
|
|
28
|
+
re.I,
|
|
29
|
+
)
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
def now() -> str:
|