@topy-ai/maggie 0.6.1 → 0.6.2
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 +27 -2
- package/bin/maggie.js +4 -0
- package/bundled-contracts/maggie-deployment/canary-report-v1.schema.json +7 -0
- package/bundled-contracts/maggie-design/icon-inventory-v1.schema.json +7 -0
- package/bundled-skills/maggie-deployment/SKILL.md +24 -1
- package/bundled-skills/maggie-design/SKILL.md +20 -1
- package/bundled-tools/clis/maggie_deployment_canary.py +59 -0
- package/bundled-tools/clis/maggie_icon_inventory.py +90 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,8 @@ maggie seo performance ... # sampled PageSpeed/CWV report and baseline
|
|
|
70
70
|
maggie seo images ... # inventory, variants, confirmation, validate
|
|
71
71
|
maggie seo sitemap ... # typed plan, validate, apply, rollback
|
|
72
72
|
maggie deployment | migration | release | analytics | schedule
|
|
73
|
+
maggie deployment canary --asset URL=SHA256 --render-report report.json
|
|
74
|
+
maggie design icon-inventory --source-dir src --runtime assets/icons.css
|
|
73
75
|
maggie api lifecycle | site-audit | ops audit
|
|
74
76
|
```
|
|
75
77
|
|
|
@@ -107,11 +109,34 @@ Read the [performance PRD](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/mai
|
|
|
107
109
|
and [image/sitemap PRD](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/docs/image-sitemap-structure-prd.md)
|
|
108
110
|
for adapter, privacy, apply, rollback, and release gates.
|
|
109
111
|
|
|
112
|
+
### Design and deployment release gates
|
|
113
|
+
|
|
114
|
+
Check source-to-runtime icon coverage before shipping a design:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
maggie design icon-inventory --project . --source-dir src \
|
|
118
|
+
--runtime public/assets/icons.css --output docs/icon-inventory.json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
After deployment, compare an immutable asset fingerprint and validate the
|
|
122
|
+
browser adapter's rendered report:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
maggie deployment canary --project . \
|
|
126
|
+
--asset https://example.com/assets/app.abc123.js=<sha256> \
|
|
127
|
+
--render-report .maggie/rendered-canary.json \
|
|
128
|
+
--output docs/deployment-canary.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The canary report requires a screenshot and zero console errors, missing
|
|
132
|
+
assets, and visual placeholders for every route. It records safe cache headers
|
|
133
|
+
only and never stores response bodies, cookies, or credentials.
|
|
134
|
+
|
|
110
135
|
Recommended upgrade sequence for the current release:
|
|
111
136
|
|
|
112
137
|
```bash
|
|
113
|
-
npx @topy-ai/maggie@0.6.
|
|
114
|
-
npx @topy-ai/maggie@0.6.
|
|
138
|
+
npx @topy-ai/maggie@0.6.2 update --project . --force
|
|
139
|
+
npx @topy-ai/maggie@0.6.2 cleanup --project .
|
|
115
140
|
```
|
|
116
141
|
|
|
117
142
|
## MaggieDash lifecycle
|
package/bin/maggie.js
CHANGED
|
@@ -88,6 +88,7 @@ Usage:
|
|
|
88
88
|
maggie design reference-ui --project PATH --reference PATH --surface blog,service --confirm
|
|
89
89
|
maggie design init --project PATH --surface blog|service --reference-run PATH --confirm
|
|
90
90
|
maggie design validate-ui --project PATH --plan PATH --rendered-dir PATH --confirm
|
|
91
|
+
maggie design icon-inventory --project PATH --source-dir src --runtime assets/icons.css --output docs/icon-inventory.json
|
|
91
92
|
maggie design author --project PATH --route /about --purpose TEXT --audience TEXT --confirm
|
|
92
93
|
maggie auth reference --project PATH --confirm
|
|
93
94
|
maggie auth check --project PATH [--production]
|
|
@@ -102,6 +103,7 @@ Usage:
|
|
|
102
103
|
maggie service convert-page <page-path> --project PATH
|
|
103
104
|
maggie service match-pages --project PATH --pages-dir src/pages
|
|
104
105
|
maggie deployment --project PATH --target vps-with-cloudflare-dns
|
|
106
|
+
maggie deployment canary --project PATH --asset URL=SHA256 --render-report report.json --output docs/deployment-canary.json
|
|
105
107
|
maggie migration --project PATH --environment staging
|
|
106
108
|
maggie schedule PATH/.maggie/schedule.json --project PATH
|
|
107
109
|
maggie analytics --project PATH --environment staging
|
|
@@ -369,12 +371,14 @@ try {
|
|
|
369
371
|
else if (command === "marketplace") marketplace(args);
|
|
370
372
|
else if (command === "clone") workflowCli("maggie_clone.py", args);
|
|
371
373
|
else if (command === "clone-to-template") workflowCli("maggie_clone_to_template.py", args);
|
|
374
|
+
else if (command === "design" && args[0] === "icon-inventory") workflowCli("maggie_icon_inventory.py", args.slice(1));
|
|
372
375
|
else if (command === "design") workflowCli("maggie_design.py", args);
|
|
373
376
|
else if (command === "auth") workflowCli("maggie_auth.py", args);
|
|
374
377
|
else if (command === "blog") workflowCli("maggie_blog.py", args);
|
|
375
378
|
else if (command === "service") service(args);
|
|
376
379
|
else if (command === "seo") seo(args);
|
|
377
380
|
else if (command === "ops") workflowCli("maggie_ops.py", args);
|
|
381
|
+
else if (command === "deployment" && args[0] === "canary") workflowCli("maggie_deployment_canary.py", args.slice(1));
|
|
378
382
|
else if (command === "deployment") workflowCli("maggie_deployment.py", args);
|
|
379
383
|
else if (command === "migration") workflowCli("maggie_migration.py", args);
|
|
380
384
|
else if (command === "schedule") workflowCli("maggie_schedule.py", args);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Maggie edge and rendered deployment canary",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schemaVersion", "assets", "status"],
|
|
6
|
+
"properties": {"schemaVersion": {"const": "maggie-deployment-canary.v1"}, "assets": {"type": "array"}, "rendered": {"type": ["object", "null"]}, "status": {"enum": ["pass", "fail"]}}
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Maggie source-to-runtime icon inventory",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schemaVersion", "source", "runtime", "coverage", "missing", "status"],
|
|
6
|
+
"properties": {"schemaVersion": {"const": "maggie-icon-inventory.v1"}, "source": {"type": "object"}, "runtime": {"type": "object"}, "coverage": {"type": "object"}, "missing": {"type": "array", "items": {"type": "string"}}, "status": {"enum": ["pass", "fail"]}}
|
|
7
|
+
}
|
|
@@ -2,11 +2,34 @@
|
|
|
2
2
|
name: maggie-deployment
|
|
3
3
|
description: Deploy and operate Maggie blog projects with Cloudflare Workers as the default target, while preserving an adapter boundary for VPS, GCP, and AWS.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 1.
|
|
5
|
+
version: 1.1.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Maggie Deployment
|
|
9
9
|
|
|
10
|
+
## Edge and rendered canary
|
|
11
|
+
|
|
12
|
+
After staging or production deployment, verify that the edge serves the
|
|
13
|
+
expected immutable asset bytes and validate a browser-produced rendered report:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
maggie deployment canary --project . \
|
|
17
|
+
--asset https://example.com/assets/app.abc123.js=<sha256> \
|
|
18
|
+
--render-report .maggie/rendered-canary.json \
|
|
19
|
+
--output docs/deployment-canary.json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The asset probe records only safe cache headers and compares the response
|
|
23
|
+
SHA-256 with the supplied release fingerprint. The rendered report must have a
|
|
24
|
+
`routes` array; every route needs a screenshot and zero `consoleErrors`,
|
|
25
|
+
`missingAssets`, and `placeholderCount`. A browser adapter (for example
|
|
26
|
+
Playwright in the host project) produces that report; Maggie validates it and
|
|
27
|
+
never claims a static HTML fetch is a rendered browser check. The command is
|
|
28
|
+
read-only and returns non-zero on stale bytes, missing screenshots, console
|
|
29
|
+
errors, missing assets, or visual placeholders. The result follows
|
|
30
|
+
`maggie-deployment-canary.v1`; never include cookies, authorization headers, or
|
|
31
|
+
response bodies in it.
|
|
32
|
+
|
|
10
33
|
## Automatic memory hook
|
|
11
34
|
|
|
12
35
|
Follow [Maggie Memory Hook](../../references/memory-hook.md) at invocation and completion.
|
|
@@ -2,11 +2,30 @@
|
|
|
2
2
|
name: maggie-design
|
|
3
3
|
description: Design authorized interior pages, review rendered responsive layouts, or explicitly rebrand a packaged homepage/template. Use rebrand only with a named source brand and target brand.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 1.
|
|
5
|
+
version: 1.3.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Maggie Design
|
|
9
9
|
|
|
10
|
+
## Source-to-runtime icon inventory
|
|
11
|
+
|
|
12
|
+
Before release, inventory icon names in source and compare them with the
|
|
13
|
+
runtime CSS/icon map (and any declared font files). The command is read-only
|
|
14
|
+
apart from its explicit report path and fails when a source icon is missing:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
maggie design icon-inventory --project . --source-dir src \
|
|
18
|
+
--runtime public/assets/icons.css --runtime public/assets/icons.woff2 \
|
|
19
|
+
--output docs/icon-inventory.json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `missing`, `unknown`, and `coverage` from the versioned
|
|
23
|
+
`maggie-icon-inventory.v1` report as the release gate. A binary font without a
|
|
24
|
+
name map is reported as evidence only; it must not be treated as proof that a
|
|
25
|
+
glyph exists. Fix the source token or add the runtime definition before
|
|
26
|
+
shipping. Do not paste private source, URLs with credentials, or user data
|
|
27
|
+
into the report.
|
|
28
|
+
|
|
10
29
|
## Automatic memory hook
|
|
11
30
|
|
|
12
31
|
Follow [Maggie Memory Hook](../../references/memory-hook.md) at invocation and completion.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify edge asset fingerprints and a browser-produced rendered canary report."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.request
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def fetch_asset(url: str, expected_sha256: str | None) -> dict:
|
|
16
|
+
if urlparse(url).scheme not in {"http", "https"}:
|
|
17
|
+
raise ValueError("asset URL must use http or https")
|
|
18
|
+
request = urllib.request.Request(url, headers={"User-Agent": "maggie-deployment-canary/1.0"})
|
|
19
|
+
with urllib.request.urlopen(request, timeout=20) as response:
|
|
20
|
+
body = response.read(); headers = {key.lower(): value for key, value in response.headers.items()}
|
|
21
|
+
actual = hashlib.sha256(body).hexdigest()
|
|
22
|
+
return {"url": url, "status": response.status, "sha256": actual, "expectedSha256": expected_sha256, "match": expected_sha256 is None or actual == expected_sha256, "headers": {key: headers[key] for key in ("cache-control", "etag", "age", "cf-cache-status", "cf-ray", "x-cache", "content-type") if key in headers}}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_render_report(path: Path) -> tuple[bool, dict]:
|
|
26
|
+
report = json.loads(path.read_text(encoding="utf-8"))
|
|
27
|
+
routes = report.get("routes")
|
|
28
|
+
if not isinstance(routes, list) or not routes: raise ValueError("render report requires a non-empty routes array")
|
|
29
|
+
failures = []
|
|
30
|
+
for route in routes:
|
|
31
|
+
if route.get("consoleErrors") or route.get("missingAssets") or route.get("placeholderCount", 0): failures.append(route.get("route", "unknown"))
|
|
32
|
+
if not route.get("screenshot"): failures.append(f"{route.get('route', 'unknown')}:screenshot")
|
|
33
|
+
return not failures, {"path": str(path), "routeCount": len(routes), "failures": failures, "report": report}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def canary(project: Path, asset: list[str], render_report: Path | None, output: Path | None) -> int:
|
|
37
|
+
assets = []
|
|
38
|
+
for item in asset:
|
|
39
|
+
if "=" in item: url, expected = item.split("=", 1)
|
|
40
|
+
else: url, expected = item, None
|
|
41
|
+
assets.append(fetch_asset(url, expected or None))
|
|
42
|
+
rendered = None
|
|
43
|
+
if render_report:
|
|
44
|
+
ok, rendered = validate_render_report(render_report.resolve())
|
|
45
|
+
else: ok = True
|
|
46
|
+
result = {"schemaVersion": "maggie-deployment-canary.v1", "workflow": "maggie-deployment", "project": str(project.resolve()), "assets": assets, "rendered": rendered, "status": "pass" if ok and all(a["match"] for a in assets) else "fail", "generatedAt": datetime.now(timezone.utc).isoformat()}
|
|
47
|
+
encoded = json.dumps(result, indent=2, ensure_ascii=False) + "\n"
|
|
48
|
+
if output: destination = (project / output).resolve() if not output.is_absolute() else output.resolve(); destination.parent.mkdir(parents=True, exist_ok=True); destination.write_text(encoded, encoding="utf-8"); result["output"] = str(destination); encoded = json.dumps(result, indent=2, ensure_ascii=False) + "\n"
|
|
49
|
+
print(encoded, end=""); return 0 if result["status"] == "pass" else 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main() -> int:
|
|
53
|
+
parser = argparse.ArgumentParser(description=__doc__); parser.add_argument("--project", type=Path, default=Path.cwd()); parser.add_argument("--asset", action="append", default=[], help="URL or URL=expected-sha256; repeatable"); parser.add_argument("--render-report", type=Path); parser.add_argument("--output", type=Path)
|
|
54
|
+
args = parser.parse_args()
|
|
55
|
+
try: return canary(args.project, args.asset, args.render_report, args.output)
|
|
56
|
+
except (OSError, ValueError, urllib.error.URLError, json.JSONDecodeError) as error: print(f"BLOCKED: maggie-deployment canary: {error}", file=__import__("sys").stderr); return 2
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__": raise SystemExit(main())
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Inventory source icon names and compare them with runtime CSS/font maps."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
SOURCE_EXTENSIONS = {".astro", ".css", ".html", ".jsx", ".js", ".json", ".svelte", ".tsx", ".ts", ".vue"}
|
|
12
|
+
IGNORE_NAMES = {"icon", "icons", "true", "false", "null", "undefined"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _add(found: dict[str, set[str]], name: str, file: Path, evidence: str) -> None:
|
|
16
|
+
name = name.strip().lower()
|
|
17
|
+
if not name or name in IGNORE_NAMES or len(name) > 80 or not re.fullmatch(r"[a-z0-9][a-z0-9._:/-]*", name):
|
|
18
|
+
return
|
|
19
|
+
found.setdefault(name, set()).add(f"{file.as_posix()}:{evidence}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def source_icons(root: Path, source_dir: Path) -> dict[str, set[str]]:
|
|
23
|
+
found: dict[str, set[str]] = {}
|
|
24
|
+
base = source_dir.resolve()
|
|
25
|
+
for file in sorted(base.rglob("*")):
|
|
26
|
+
if not file.is_file() or file.suffix.lower() not in SOURCE_EXTENSIONS:
|
|
27
|
+
continue
|
|
28
|
+
text = file.read_text(encoding="utf-8", errors="replace")
|
|
29
|
+
relative = file.relative_to(root.resolve())
|
|
30
|
+
for match in re.finditer(r"(?:icon|glyph|symbol)\s*[:=]\s*[\"']([^\"']+)", text, re.I):
|
|
31
|
+
_add(found, match.group(1), relative, "named-property")
|
|
32
|
+
for match in re.finditer(r"data-(?:icon|glyph)\s*=\s*[\"']([^\"']+)", text, re.I):
|
|
33
|
+
_add(found, match.group(1), relative, "data-attribute")
|
|
34
|
+
for match in re.finditer(r"(?:ph|fa|lucide|icon)-([a-z0-9][a-z0-9-]*)", text, re.I):
|
|
35
|
+
_add(found, match.group(1), relative, "icon-class")
|
|
36
|
+
for match in re.finditer(r"(?:phosphor|lucide|icon)[:/]([a-z0-9][a-z0-9-]*)", text, re.I):
|
|
37
|
+
_add(found, match.group(1), relative, "icon-token")
|
|
38
|
+
return found
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def runtime_icons(root: Path, runtime_paths: list[Path]) -> tuple[set[str], dict[str, set[str]], list[str]]:
|
|
42
|
+
names: set[str] = set(); evidence: dict[str, set[str]] = {}; font_files: list[str] = []
|
|
43
|
+
for path in runtime_paths:
|
|
44
|
+
path = path.resolve()
|
|
45
|
+
if not path.exists():
|
|
46
|
+
continue
|
|
47
|
+
if path.suffix.lower() in {".woff", ".woff2", ".ttf", ".otf"}:
|
|
48
|
+
font_files.append(str(path.relative_to(root.resolve())) if path.is_relative_to(root.resolve()) else str(path))
|
|
49
|
+
continue
|
|
50
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
51
|
+
relative = path.relative_to(root.resolve()) if path.is_relative_to(root.resolve()) else path
|
|
52
|
+
for match in re.finditer(r"\.(?:ph|fa|lucide|icon)-([a-z0-9][a-z0-9-]*)\b", text, re.I):
|
|
53
|
+
name = match.group(1).lower(); names.add(name); evidence.setdefault(name, set()).add(f"{relative}:css-class")
|
|
54
|
+
for match in re.finditer(r"(?:data-(?:icon|glyph)|icon)\s*=\s*[\"']([^\"']+)", text, re.I):
|
|
55
|
+
name = match.group(1).lower(); names.add(name); evidence.setdefault(name, set()).add(f"{relative}:runtime-map")
|
|
56
|
+
return names, evidence, font_files
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def inventory(project: Path, source_dir: Path, runtime: list[Path], output: Path | None) -> int:
|
|
60
|
+
project = project.resolve(); source = (project / source_dir).resolve() if not source_dir.is_absolute() else source_dir.resolve()
|
|
61
|
+
if not source.is_dir(): raise ValueError(f"source directory not found: {source}")
|
|
62
|
+
runtime_paths = [((project / path) if not path.is_absolute() else path) for path in runtime]
|
|
63
|
+
sources = source_icons(project, source)
|
|
64
|
+
available, runtime_evidence, fonts = runtime_icons(project, runtime_paths)
|
|
65
|
+
source_names = sorted(sources); missing = sorted(set(source_names) - available); matched = sorted(set(source_names) & available)
|
|
66
|
+
report = {
|
|
67
|
+
"schemaVersion": "maggie-icon-inventory.v1", "workflow": "maggie-design",
|
|
68
|
+
"source": {"directory": str(source), "names": [{"name": n, "evidence": sorted(sources[n])} for n in source_names]},
|
|
69
|
+
"runtime": {"paths": [str(p) for p in runtime_paths], "names": [{"name": n, "evidence": sorted(runtime_evidence.get(n, []))} for n in sorted(available)], "fontFiles": fonts},
|
|
70
|
+
"coverage": {"sourceCount": len(source_names), "matchedCount": len(matched), "missingCount": len(missing), "ratio": round(len(matched) / len(source_names), 4) if source_names else 1.0},
|
|
71
|
+
"missing": missing, "unknown": [str(p) for p in runtime_paths if not p.exists()],
|
|
72
|
+
"status": "pass" if not missing and not any(not p.exists() for p in runtime_paths) else "fail",
|
|
73
|
+
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
|
74
|
+
}
|
|
75
|
+
encoded = json.dumps(report, indent=2, ensure_ascii=False) + "\n"
|
|
76
|
+
if output:
|
|
77
|
+
destination = (project / output).resolve() if not output.is_absolute() else output.resolve(); destination.parent.mkdir(parents=True, exist_ok=True); destination.write_text(encoded, encoding="utf-8"); report["output"] = str(destination); encoded = json.dumps(report, indent=2, ensure_ascii=False) + "\n"
|
|
78
|
+
print(encoded, end="")
|
|
79
|
+
return 0 if report["status"] == "pass" else 1
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main() -> int:
|
|
83
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
84
|
+
parser.add_argument("--project", type=Path, default=Path.cwd()); parser.add_argument("--source-dir", type=Path, default=Path("src")); parser.add_argument("--runtime", action="append", type=Path, required=True, help="CSS, icon map, or font file; repeatable"); parser.add_argument("--output", type=Path)
|
|
85
|
+
args = parser.parse_args()
|
|
86
|
+
try: return inventory(args.project, args.source_dir, args.runtime, args.output)
|
|
87
|
+
except (OSError, ValueError, json.JSONDecodeError) as error: print(f"BLOCKED: maggie-design icon-inventory: {error}", file=__import__("sys").stderr); return 2
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__": raise SystemExit(main())
|