@topy-ai/maggie 0.3.0 → 0.5.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.
package/README.md CHANGED
@@ -80,8 +80,8 @@ with `--confirm`, removes only known retired EmDash artifacts. It preserves
80
80
  Recommended upgrade sequence for the current release:
81
81
 
82
82
  ```bash
83
- npx @topy-ai/maggie@0.2.9 update --project . --force
84
- npx @topy-ai/maggie@0.2.9 cleanup --project .
83
+ npx @topy-ai/maggie@0.5.1 update --project . --force
84
+ npx @topy-ai/maggie@0.5.1 cleanup --project .
85
85
  ```
86
86
 
87
87
  ## MaggieDash lifecycle
@@ -267,6 +267,24 @@ python3 tools/clis/maggie_design.py rebrand \
267
267
  | `maggie-auth-reference` | Generate and validate traditional email/password auth with secure server-side sessions |
268
268
  | `maggie-blog` | Run a provider-neutral blog lifecycle with stable identity, topics, feeds, settings, and rollback |
269
269
 
270
+ `maggie-design` can initialize native blog and service UI plans from a local,
271
+ read-only structural reference:
272
+
273
+ ```bash
274
+ maggie design reference-ui --project . --reference /path/to/reference \
275
+ --surface blog,service --confirm
276
+ maggie design init --project . --surface blog \
277
+ --reference-run .maggie/design/content-ui/reference-<id> --confirm
278
+ maggie design init --project . --surface service \
279
+ --reference-run .maggie/design/content-ui/reference-<id> --confirm
280
+ maggie design validate-ui --project . \
281
+ --plan .maggie/design/content-ui/blog/init-<id>/plan.json \
282
+ --rendered-dir .maggie/design/content-ui/blog/init-<id>/screenshots --confirm
283
+ ```
284
+
285
+ This reuses the host project's homepage shell and `DESIGN.md`; it does not
286
+ copy reference branding, source code, private data, or provider facts.
287
+
270
288
  The remaining installable skills are `maggie-blog-bootstrap`, `maggie-dash`,
271
289
  `maggie-clone`, `maggie-clone-to-template`, `maggie-marketplace`,
272
290
  `maggie-template`, `maggie-design`, `maggie-ops`, `maggie-deployment`,
package/bin/maggie.js CHANGED
@@ -85,6 +85,9 @@ Usage:
85
85
  maggie clone run <homepage-url> --run-id <id>
86
86
  maggie clone status --run-id <id>
87
87
  maggie design run <target-url> --clone-run <id>
88
+ maggie design reference-ui --project PATH --reference PATH --surface blog,service --confirm
89
+ maggie design init --project PATH --surface blog|service --reference-run PATH --confirm
90
+ maggie design validate-ui --project PATH --plan PATH --rendered-dir PATH --confirm
88
91
  maggie design author --project PATH --route /about --purpose TEXT --audience TEXT --confirm
89
92
  maggie auth reference --project PATH --confirm
90
93
  maggie auth check --project PATH [--production]
@@ -205,7 +208,11 @@ function install(args) {
205
208
  const tools = join(root, "tools");
206
209
  if (existsSync(TOOLS_ROOT)) {
207
210
  for (const group of ["clis", "runtime", "integrations"]) {
208
- if (existsSync(join(TOOLS_ROOT, group))) copyIfMissing(join(TOOLS_ROOT, group), join(tools, group), force);
211
+ if (existsSync(join(TOOLS_ROOT, group))) {
212
+ const target = join(tools, group);
213
+ if (existsSync(target)) syncTree(join(TOOLS_ROOT, group), target, force);
214
+ else copyIfMissing(join(TOOLS_ROOT, group), target, force);
215
+ }
209
216
  }
210
217
  }
211
218
  if (existsSync(join(DESIGN_ROOT, "SPA-DESIGN.md"))) {
@@ -241,7 +248,10 @@ function update(args) {
241
248
  if (existsSync(REFERENCES_ROOT) && existsSync(join(agentRoot, "references"))) updated += syncTree(REFERENCES_ROOT, join(agentRoot, "references"), force);
242
249
  }
243
250
  const tools = join(root, "tools");
244
- if (existsSync(TOOLS_ROOT)) for (const group of ["clis", "integrations"]) if (existsSync(join(tools, group))) updated += syncTree(join(TOOLS_ROOT, group), join(tools, group), force);
251
+ if (existsSync(TOOLS_ROOT)) for (const group of ["clis", "runtime", "integrations"]) if (existsSync(join(TOOLS_ROOT, group))) {
252
+ const target = join(tools, group);
253
+ if (existsSync(target)) updated += syncTree(join(TOOLS_ROOT, group), target, force);
254
+ }
245
255
  if (existsSync(join(DESIGN_ROOT, "SPA-DESIGN.md")) && existsSync(join(root, ".maggie", "design-reference"))) {
246
256
  updated += syncTree(DESIGN_ROOT, join(root, ".maggie", "design-reference"), force);
247
257
  }
@@ -0,0 +1,6 @@
1
+ # Maggie Content UI Contracts
2
+
3
+ These contracts describe sanitized structural evidence captured from a local
4
+ UI reference and the native implementation plan consumed by `maggie-design`.
5
+ They intentionally exclude source-brand content, private data, credentials,
6
+ raw assets, and copied source code.
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "maggie-content-ui-plan.v1",
4
+ "type": "object",
5
+ "required": ["schemaVersion", "id", "workflow", "mode", "surface", "phase", "shell", "components", "routes", "dataAdapter", "publicWrite"],
6
+ "properties": {
7
+ "schemaVersion": { "const": "maggie-content-ui-plan.v1" },
8
+ "id": { "type": "string", "pattern": "^init-[a-f0-9]+$" },
9
+ "workflow": { "const": "maggie-design" },
10
+ "mode": { "const": "content-ui-init" },
11
+ "surface": { "enum": ["blog", "service"] },
12
+ "phase": { "const": "ready" },
13
+ "shell": { "type": "object", "required": ["sourceOfTruth", "reuse", "replaceAllowed"] },
14
+ "components": { "type": "array", "minItems": 1 },
15
+ "routes": { "type": "array", "minItems": 1 },
16
+ "dataAdapter": { "enum": ["maggie-blog", "maggie-service-booking"] },
17
+ "publicWrite": { "const": false }
18
+ }
19
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "maggie-content-ui-reference.v1",
4
+ "type": "object",
5
+ "required": ["schemaVersion", "id", "workflow", "mode", "surfaces", "sourcePolicy", "privateSourceCopied"],
6
+ "properties": {
7
+ "schemaVersion": { "const": "maggie-content-ui-reference.v1" },
8
+ "id": { "type": "string", "pattern": "^reference-[a-f0-9]+$" },
9
+ "workflow": { "const": "maggie-design" },
10
+ "mode": { "const": "reference-ui" },
11
+ "surfaces": { "type": "object", "minProperties": 1 },
12
+ "sourcePolicy": { "const": "read-only-structural-guideline" },
13
+ "privateSourceCopied": { "const": false }
14
+ }
15
+ }
@@ -32,3 +32,15 @@ published slug must not change during a rewrite. Search/sort views are not
32
32
  indexable; drafts never appear in public routes, RSS, or sitemap output.
33
33
  Provider keys remain server-side. Public publication and migrations always
34
34
  require explicit confirmation.
35
+
36
+ To initialize native front-end pages from the approved local UI guideline,
37
+ run `maggie-design`:
38
+
39
+ ```bash
40
+ maggie design init --project . --surface blog \
41
+ --reference-run .maggie/design/content-ui/reference-<id> \
42
+ --base-path /our-blogs --confirm
43
+ ```
44
+
45
+ The blog skill supplies route/data semantics; `maggie-design` supplies the
46
+ host-native components and responsive visual review.
@@ -90,6 +90,57 @@ unconfirmed writes. Generated copy and media must be marked authored rather
90
90
  than source-observed. Implementation must still capture responsive screenshots
91
91
  and run accessibility/build validation before publication.
92
92
 
93
+ ## Content UI initialization
94
+
95
+ Use content UI initialization when `maggie-blog` or
96
+ `maggie-service-booking` needs native front-end layouts based on a local UI
97
+ reference. The reference is structural evidence only. It must not replace the
98
+ project's `DESIGN.md`, homepage shell, brand, copy, assets, or provider facts.
99
+
100
+ First record a sanitized, read-only reference manifest:
101
+
102
+ ```bash
103
+ maggie design reference-ui \
104
+ --project . \
105
+ --reference /home/balalior/Dev/clients/spachevychase.org \
106
+ --surface blog,service --confirm
107
+ ```
108
+
109
+ Then initialize one surface at a time from that manifest:
110
+
111
+ ```bash
112
+ maggie design init --project . --surface blog \
113
+ --reference-run .maggie/design/content-ui/reference-<id> \
114
+ --base-path /our-blogs --confirm
115
+
116
+ maggie design init --project . --surface service \
117
+ --reference-run .maggie/design/content-ui/reference-<id> \
118
+ --route-pattern '/services/[slug]/' --confirm
119
+ ```
120
+
121
+ The initializer creates a reviewable component/route/responsive/provenance
122
+ plan under `.maggie/design/content-ui/`. It does not write public components
123
+ or publish routes. The implementation phase must call the matching data skill,
124
+ reuse the homepage shell, capture desktop/tablet/mobile screenshots, and pass
125
+ accessibility, metadata, build, and visual review gates before approval.
126
+
127
+ Validate the rendered plan before approval:
128
+
129
+ ```bash
130
+ maggie design validate-ui --project . \
131
+ --plan .maggie/design/content-ui/blog/init-<id>/plan.json \
132
+ --rendered-dir .maggie/design/content-ui/blog/init-<id>/screenshots \
133
+ --confirm
134
+ ```
135
+
136
+ This gate requires valid PNG evidence for desktop, tablet, and mobile and keeps
137
+ `publicWrite: false` until a separate approval step.
138
+
139
+ For blog UI, preserve archive, topic, post/slug, pagination, RSS, and sitemap
140
+ semantics from `maggie-blog`. For service UI, preserve provider identity,
141
+ variants, booking URLs, duplicate canonical/noindex decisions, and service
142
+ sitemap semantics from `maggie-service-booking`.
143
+
93
144
  ## Explicit homepage rebrand mode
94
145
 
95
146
  The homepage `review` mode only compares screenshots. It does not rebrand a
@@ -30,7 +30,10 @@ interpretation, and content creation remain agent-reviewed decisions.
30
30
  With `--crawl`, it resolves sitemap indexes and audits every listed HTML route
31
31
  for HTTP 200, one H1, title/description, locale, canonical, Open Graph/Twitter
32
32
  metadata, valid entity JSON-LD, and image alt text. Non-HTML endpoints such as
33
- RSS must not be included in the HTML page sitemap.
33
+ RSS must not be included in the HTML page sitemap. Sitemap `<loc>` values must
34
+ be absolute HTTP(S) URLs; the audit reports relative values as a failure even
35
+ when it can resolve them for continued crawling, so one malformed entry cannot
36
+ hide a protocol violation.
34
37
  Use `--output <project>/docs/site-audit.json` to persist the evidence used by
35
38
  the release review.
36
39
 
@@ -207,6 +207,19 @@ and `DESIGN.md`. Use the homepage header, footer, fonts, tokens, navigation,
207
207
  analytics boundary, accessibility behaviour, and responsive breakpoints.
208
208
  Only the service content region varies.
209
209
 
210
+ When initializing a new service UI from the approved reference guideline, use
211
+ the shared `maggie-design` initializer:
212
+
213
+ ```bash
214
+ maggie design init --project . --surface service \
215
+ --reference-run .maggie/design/content-ui/reference-<id> \
216
+ --route-pattern '/services/[slug]/' --confirm
217
+ ```
218
+
219
+ This creates a reviewable native component/route plan. It does not copy the
220
+ reference project's brand, source code, assets, or provider facts, and it does
221
+ not publish service pages without the existing service fact/copy gates.
222
+
210
223
  Each service page must expose the service title, category breadcrumbs, factual
211
224
  description, all active variants, duration, currency-formatted price, booking
212
225
  CTA, and payment CTA only if available. Preserve the provider booking URL as
@@ -34,6 +34,32 @@ DESIGN_HEADINGS = (
34
34
 
35
35
  REVIEW_VIEWPORTS = {"desktop": (1440, 900), "tablet": (768, 900), "mobile": (390, 844)}
36
36
 
37
+ REFERENCE_SURFACES = {
38
+ "blog": {
39
+ "files": [
40
+ "src/layouts/BlogLayout.astro", "src/components/blog/BlogArchive.astro",
41
+ "src/components/blog/PostCard.astro", "src/components/blog/Pagination.astro",
42
+ "src/components/blog/PostSidebar.astro", "src/pages/our-blogs/index.astro",
43
+ "src/pages/our-blogs/[slug].astro", "src/pages/our-blogs/topic/[slug].astro",
44
+ "src/pages/rss.xml.ts", "src/pages/sitemap.xml.ts",
45
+ ],
46
+ "components": ["BlogLayout", "BlogArchive", "PostCard", "Pagination", "PostSidebar", "ShareMenu"],
47
+ "routes": ["{base}/", "{base}/page/{n}/", "{base}/{slug}/", "{base}/topic/{slug}/", "{base}/topic/{slug}/page/{n}/", "/rss.xml", "/sitemap.xml"],
48
+ "patterns": ["compact image-backed archive hero", "URL-backed search/topic/sort controls", "responsive post grid", "sticky article sidebar", "related articles", "crawlable topic links"],
49
+ },
50
+ "service": {
51
+ "files": [
52
+ "src/pages/services/index.astro", "src/pages/services/[slug].astro",
53
+ "src/components/ServicePage.astro", "src/components/CategoryServiceHub.astro",
54
+ "src/components/SupportingServicePage.astro", "src/components/ServiceContextNarrative.astro",
55
+ "src/pages/sitemap-services.xml.ts", "src/pages/sitemap-service-categories.xml.ts",
56
+ ],
57
+ "components": ["ServiceIndex", "CategoryServiceHub", "ServicePage", "ServiceFacts", "RelatedServices", "ServiceFaq"],
58
+ "routes": ["/services/", "/services/{slug}/", "/service-categories/{slug}/", "/sitemap-services.xml", "/sitemap-service-categories.xml"],
59
+ "patterns": ["category breadcrumb hero", "variant facts and booking card", "service narrative with image", "related treatment grid", "FAQ accordion", "category navigation"],
60
+ },
61
+ }
62
+
37
63
 
38
64
  def rendered_asset_preflight(template: Path) -> list[str]:
39
65
  """Catch package CSS failures before visual comparison is trusted."""
@@ -56,6 +82,81 @@ def rendered_asset_preflight(template: Path) -> list[str]:
56
82
  return errors
57
83
 
58
84
 
85
+ def _surface_list(value: str) -> list[str]:
86
+ surfaces = [item.strip().lower() for item in value.split(",") if item.strip()]
87
+ invalid = sorted(set(surfaces) - set(REFERENCE_SURFACES))
88
+ if invalid or not surfaces:
89
+ raise ValueError("surface must contain blog and/or service")
90
+ return list(dict.fromkeys(surfaces))
91
+
92
+
93
+ def reference_ui(project: Path, reference: Path, surfaces: list[str], screenshots_dir: Path | None, confirm: bool) -> int:
94
+ """Record sanitized structural evidence from a read-only reference project."""
95
+ project = project.resolve(); reference = reference.resolve()
96
+ if not reference.is_dir(): raise ValueError(f"reference project not found: {reference}")
97
+ if not confirm: print("CONFIRMATION_REQUIRED: rerun with --confirm", file=sys.stderr); return 2
98
+ missing = {surface: [item for item in REFERENCE_SURFACES[surface]["files"] if not (reference / item).exists()] for surface in surfaces}
99
+ missing = {key: value for key, value in missing.items() if value}
100
+ if missing: raise ValueError("reference files missing: " + json.dumps(missing, ensure_ascii=False))
101
+ digest = hashlib.sha256((str(reference) + "\n" + ",".join(surfaces)).encode()).hexdigest()[:12]
102
+ output_dir = project / ".maggie" / "design" / "content-ui" / f"reference-{digest}"; output_dir.mkdir(parents=True, exist_ok=True)
103
+ evidence = {}
104
+ for surface in surfaces:
105
+ config = REFERENCE_SURFACES[surface]
106
+ evidence[surface] = {"sourceFiles": config["files"], "components": config["components"], "routes": config["routes"], "patterns": config["patterns"], "observations": {pattern: True for pattern in config["patterns"]}}
107
+ screenshots = {name: "pending" for name in REVIEW_VIEWPORTS}
108
+ if screenshots_dir:
109
+ screenshots = {name: str((screenshots_dir / f"{name}.png").resolve()) if (screenshots_dir / f"{name}.png").is_file() else "missing" for name in REVIEW_VIEWPORTS}
110
+ manifest = {"schemaVersion": "maggie-content-ui-reference.v1", "id": f"reference-{digest}", "workflow": "maggie-design", "mode": "reference-ui", "referenceProject": reference.name, "surfaces": evidence, "screenshots": screenshots, "sourcePolicy": "read-only-structural-guideline", "privateSourceCopied": False, "createdAt": datetime.now(timezone.utc).isoformat()}
111
+ path = output_dir / "reference-manifest.json"; path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
112
+ print(json.dumps({"manifest": str(path), "id": manifest["id"], "surfaces": surfaces, "screenshotStatus": screenshots}, indent=2, ensure_ascii=False)); return 0
113
+
114
+
115
+ def init_content_ui(project: Path, surface: str, reference_run: Path, base_path: str, route_pattern: str, confirm: bool) -> int:
116
+ """Create the reviewable plan that Maggie Design will implement natively."""
117
+ project = project.resolve(); surface = surface.strip().lower()
118
+ if surface not in REFERENCE_SURFACES: raise ValueError("surface must be blog or service")
119
+ if not confirm: print("CONFIRMATION_REQUIRED: rerun with --confirm", file=sys.stderr); return 2
120
+ contract = require_design_contract(project)
121
+ manifest_path = reference_run.resolve()
122
+ if manifest_path.is_dir(): manifest_path /= "reference-manifest.json"
123
+ if not manifest_path.exists(): raise ValueError(f"reference manifest required: {manifest_path}")
124
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
125
+ if surface not in manifest.get("surfaces", {}): raise ValueError(f"reference manifest does not contain surface: {surface}")
126
+ digest = hashlib.sha256((str(project) + "\n" + surface + "\n" + str(manifest_path)).encode()).hexdigest()[:12]
127
+ output_dir = project / ".maggie" / "design" / "content-ui" / surface / f"init-{digest}"; output_dir.mkdir(parents=True, exist_ok=True)
128
+ config = REFERENCE_SURFACES[surface]
129
+ routes = config["routes"]
130
+ if surface == "blog": routes = [route.replace("{base}", base_path.rstrip("/") or "/our-blogs") for route in routes]
131
+ elif route_pattern: routes = [route_pattern, "/sitemap-services.xml", "/sitemap-service-categories.xml"]
132
+ plan = {"schemaVersion": "maggie-content-ui-plan.v1", "id": f"init-{digest}", "workflow": "maggie-design", "mode": "content-ui-init", "surface": surface, "phase": "ready", "referenceManifest": str(manifest_path), "designContract": contract, "shell": {"sourceOfTruth": "existing homepage shell", "reuse": ["header", "footer", "tokens", "typography", "icons", "breakpoints", "accessibility", "analytics"], "replaceAllowed": False}, "components": config["components"], "routes": routes, "dataAdapter": "maggie-blog" if surface == "blog" else "maggie-service-booking", "provenance": {"referenceObserved": True, "projectPreserved": True, "agentAuthored": True, "providerFact": surface == "service"}, "requiredSteps": ["inspect-host-shell", "create-native-components", "bind-data-adapter", "capture-desktop-tablet-mobile", "accessibility-check", "metadata-and-route-check", "build-check", "explicit-approval"], "publicWrite": False, "createdAt": datetime.now(timezone.utc).isoformat()}
133
+ files = {"reference-manifest.json": manifest, "shell-reuse.json": plan["shell"], "component-plan.json": {"components": config["components"], "patterns": config["patterns"]}, "route-plan.json": {"routes": routes, "dataAdapter": plan["dataAdapter"]}, "responsive-plan.json": {"viewports": REVIEW_VIEWPORTS, "mobileStickySidebar": False if surface == "blog" else None}, "provenance.json": plan["provenance"], "validation.json": {"status": "pending", "required": plan["requiredSteps"]}}
134
+ for filename, value in files.items(): (output_dir / filename).write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
135
+ (output_dir / "screenshots").mkdir(exist_ok=True)
136
+ (output_dir / "plan.json").write_text(json.dumps(plan, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
137
+ print(json.dumps({"plan": str(output_dir / "plan.json"), "surface": surface, "phase": "ready", "publicWrite": False}, indent=2)); return 0
138
+
139
+
140
+ def validate_content_ui(project: Path, plan_path: Path, rendered_dir: Path | None, confirm: bool) -> int:
141
+ """Validate a generated content UI plan before host publication."""
142
+ project = project.resolve(); plan_path = plan_path.resolve()
143
+ if not plan_path.exists(): raise ValueError(f"content UI plan not found: {plan_path}")
144
+ plan = json.loads(plan_path.read_text(encoding="utf-8")); errors = []
145
+ if plan.get("workflow") != "maggie-design" or plan.get("mode") != "content-ui-init": errors.append("invalid design workflow plan")
146
+ if plan.get("publicWrite") is not False: errors.append("publicWrite must remain false before approval")
147
+ try: require_design_contract(project)
148
+ except ValueError as error: errors.append(str(error))
149
+ screenshot_status = {}
150
+ for name in REVIEW_VIEWPORTS:
151
+ path = (rendered_dir / f"{name}.png") if rendered_dir else plan_path.parent / "screenshots" / f"{name}.png"
152
+ valid = path.is_file() and path.read_bytes()[:8] == b"\x89PNG\r\n\x1a\n"
153
+ screenshot_status[name] = {"path": str(path), "validPng": valid}
154
+ if not valid: errors.append(f"missing valid {name} screenshot")
155
+ result = {"schemaVersion": "maggie-content-ui-validation.v1", "plan": str(plan_path), "surface": plan.get("surface"), "screenshots": screenshot_status, "checks": {"shellReuse": plan.get("shell", {}).get("replaceAllowed") is False, "publicWriteBlocked": plan.get("publicWrite") is False, "designContract": not any("DESIGN.md" in error for error in errors)}, "status": "passed" if not errors else "failed", "errors": errors, "validatedAt": datetime.now(timezone.utc).isoformat()}
156
+ if not confirm: print(json.dumps(result, indent=2, ensure_ascii=False)); return 0 if not errors else 1
157
+ output = plan_path.parent / "validation.json"; output.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"); print(json.dumps(result, indent=2, ensure_ascii=False)); return 0 if not errors else 1
158
+
159
+
59
160
  def rebrand_template(args: argparse.Namespace) -> int:
60
161
  """Apply an explicit brand identity to a packaged homepage template."""
61
162
  template = args.template.resolve()
@@ -479,6 +580,42 @@ def author_job(project: Path, route: str, purpose: str, audience: str, brief_fil
479
580
 
480
581
 
481
582
  def main() -> int:
583
+ if len(sys.argv) > 1 and sys.argv[1] == "reference-ui":
584
+ command = argparse.ArgumentParser(description="Record sanitized blog/service UI evidence from a read-only reference project.")
585
+ command.add_argument("--project", type=Path, default=Path.cwd())
586
+ command.add_argument("--reference", type=Path, required=True)
587
+ command.add_argument("--surface", default="blog,service")
588
+ command.add_argument("--screenshots-dir", type=Path)
589
+ command.add_argument("--confirm", action="store_true")
590
+ args = command.parse_args(sys.argv[2:])
591
+ try:
592
+ return reference_ui(args.project, args.reference, _surface_list(args.surface), args.screenshots_dir, args.confirm)
593
+ except (OSError, ValueError, json.JSONDecodeError) as error:
594
+ print(f"BLOCKED: maggie-design reference-ui: {error}", file=sys.stderr); return 1
595
+ if len(sys.argv) > 1 and sys.argv[1] == "init":
596
+ command = argparse.ArgumentParser(description="Initialize a native blog or service UI plan from a sanitized reference manifest.")
597
+ command.add_argument("--project", type=Path, default=Path.cwd())
598
+ command.add_argument("--surface", required=True, choices=["blog", "service"])
599
+ command.add_argument("--reference-run", type=Path, required=True)
600
+ command.add_argument("--base-path", default="/our-blogs")
601
+ command.add_argument("--route-pattern", default="/services/[slug]/")
602
+ command.add_argument("--confirm", action="store_true")
603
+ args = command.parse_args(sys.argv[2:])
604
+ try:
605
+ return init_content_ui(args.project, args.surface, args.reference_run, args.base_path, args.route_pattern, args.confirm)
606
+ except (OSError, ValueError, json.JSONDecodeError) as error:
607
+ print(f"BLOCKED: maggie-design init: {error}", file=sys.stderr); return 1
608
+ if len(sys.argv) > 1 and sys.argv[1] == "validate-ui":
609
+ command = argparse.ArgumentParser(description="Validate content UI screenshots and design/public-write gates.")
610
+ command.add_argument("--project", type=Path, default=Path.cwd())
611
+ command.add_argument("--plan", type=Path, required=True)
612
+ command.add_argument("--rendered-dir", type=Path)
613
+ command.add_argument("--confirm", action="store_true")
614
+ args = command.parse_args(sys.argv[2:])
615
+ try:
616
+ return validate_content_ui(args.project, args.plan, args.rendered_dir, args.confirm)
617
+ except (OSError, ValueError, json.JSONDecodeError) as error:
618
+ print(f"BLOCKED: maggie-design validate-ui: {error}", file=sys.stderr); return 1
482
619
  if len(sys.argv) > 1 and sys.argv[1] == "rebrand":
483
620
  rebrand = argparse.ArgumentParser(description="Apply an explicit brand identity to a packaged marketplace template.")
484
621
  rebrand.add_argument("--template", type=Path, required=True)
@@ -134,10 +134,11 @@ def hreflang_check(url: str, page: PageParser, expected: set[str]) -> dict:
134
134
  return {"ok": not missing and not reciprocal_errors, "missing": missing, "reciprocal_errors": reciprocal_errors, "links": links}
135
135
 
136
136
 
137
- def sitemap_urls(base: str) -> list[str]:
138
- """Resolve a sitemap index and return its public page URLs."""
137
+ def sitemap_urls(base: str) -> tuple[list[str], list[dict[str, str]]]:
138
+ """Resolve a sitemap index and return page URLs plus raw loc violations."""
139
139
  pending = [urljoin(base + "/", "sitemap.xml")]
140
140
  pages = []
141
+ violations = []
141
142
  seen = set()
142
143
  while pending:
143
144
  sitemap_url = pending.pop(0)
@@ -145,12 +146,16 @@ def sitemap_urls(base: str) -> list[str]:
145
146
  continue
146
147
  seen.add(sitemap_url)
147
148
  _, _, body = fetch(sitemap_url)
148
- locs = [urljoin(base + "/", value.strip()) for value in re.findall(r"<loc>\s*(.*?)\s*</loc>", body, re.I | re.S)]
149
+ raw_locs = [value.strip() for value in re.findall(r"<loc>\s*(.*?)\s*</loc>", body, re.I | re.S)]
150
+ for value in raw_locs:
151
+ if not re.match(r"^https?://[^\s]+$", value, re.I):
152
+ violations.append({"sitemap": sitemap_url, "loc": value, "reason": "sitemap loc must be an absolute HTTP(S) URL"})
153
+ locs = [urljoin(base + "/", value) for value in raw_locs]
149
154
  if re.search(r"<sitemapindex\b", body, re.I):
150
155
  pending.extend(locs)
151
156
  else:
152
157
  pages.extend(locs)
153
- return list(dict.fromkeys(pages))
158
+ return list(dict.fromkeys(pages)), violations
154
159
 
155
160
 
156
161
  def main() -> int:
@@ -215,7 +220,8 @@ def main() -> int:
215
220
  crawl = {"enabled": args.crawl, "passed": True, "pages": []}
216
221
  if args.crawl:
217
222
  try:
218
- urls = sitemap_urls(base)[: args.max_pages]
223
+ urls, sitemap_violations = sitemap_urls(base)
224
+ urls = urls[: args.max_pages]
219
225
  for page_url in urls:
220
226
  try:
221
227
  status, content_type, body = fetch(page_url)
@@ -232,7 +238,8 @@ def main() -> int:
232
238
  except Exception as exc:
233
239
  page_checks = {"url": page_url, "passed": False, "error": type(exc).__name__}
234
240
  crawl["pages"].append(page_checks)
235
- crawl["passed"] = bool(urls) and len(crawl["pages"]) == len(urls) and all(item["passed"] for item in crawl["pages"])
241
+ crawl["sitemap_loc_violations"] = sitemap_violations
242
+ crawl["passed"] = not sitemap_violations and bool(urls) and len(crawl["pages"]) == len(urls) and all(item["passed"] for item in crawl["pages"])
236
243
  crawl["url_count"] = len(urls)
237
244
  except Exception as exc:
238
245
  crawl = {"enabled": True, "passed": False, "error": type(exc).__name__, "pages": []}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",