@topy-ai/maggie 0.6.1 → 0.6.3

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
@@ -1,5 +1,7 @@
1
1
  # @topy-ai/maggie
2
2
 
3
+ **Language:** English · [繁體中文](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/README.zh-TW.md)
4
+
3
5
  [![npm version](https://img.shields.io/npm/v/@topy-ai/maggie)](https://www.npmjs.com/package/@topy-ai/maggie)
4
6
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/LICENSE)
5
7
 
@@ -48,6 +50,26 @@ Maggie keeps the existing project foundation and asks for decisions before
48
50
  shared routes, analytics, or publishing boundaries change. The current
49
51
  package ships 18 installable skills and a local-first MaggieDash foundation.
50
52
 
53
+ For a genuinely empty project, create the host framework first, then bootstrap
54
+ Maggie in this order:
55
+
56
+ ```bash
57
+ python3 tools/clis/maggie.py analyze . --json --save
58
+ python3 tools/clis/maggie.py bootstrap interview .
59
+ python3 tools/clis/maggie_dash.py init --project . --confirm
60
+ python3 tools/clis/maggie_dash.py migrate --project . --confirm
61
+ ```
62
+
63
+ The normal daily loop is:
64
+
65
+ ```text
66
+ doctor → memory context → inspect/plan → confirm → implement → test
67
+ → visual/SEO review → deploy staging → canary → publish with approval
68
+ ```
69
+
70
+ Use `maggie memory record-error` for a repaired, reusable pitfall. Feedback is
71
+ redacted and explicit; it is never promoted to active memory automatically.
72
+
51
73
  ## Command surface
52
74
 
53
75
  The CLI provides the installer plus durable workflow commands:
@@ -70,6 +92,8 @@ maggie seo performance ... # sampled PageSpeed/CWV report and baseline
70
92
  maggie seo images ... # inventory, variants, confirmation, validate
71
93
  maggie seo sitemap ... # typed plan, validate, apply, rollback
72
94
  maggie deployment | migration | release | analytics | schedule
95
+ maggie deployment canary --asset URL=SHA256 --render-report report.json
96
+ maggie design icon-inventory --source-dir src --runtime assets/icons.css
73
97
  maggie api lifecycle | site-audit | ops audit
74
98
  ```
75
99
 
@@ -107,11 +131,34 @@ Read the [performance PRD](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/mai
107
131
  and [image/sitemap PRD](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/docs/image-sitemap-structure-prd.md)
108
132
  for adapter, privacy, apply, rollback, and release gates.
109
133
 
134
+ ### Design and deployment release gates
135
+
136
+ Check source-to-runtime icon coverage before shipping a design:
137
+
138
+ ```bash
139
+ maggie design icon-inventory --project . --source-dir src \
140
+ --runtime public/assets/icons.css --output docs/icon-inventory.json
141
+ ```
142
+
143
+ After deployment, compare an immutable asset fingerprint and validate the
144
+ browser adapter's rendered report:
145
+
146
+ ```bash
147
+ maggie deployment canary --project . \
148
+ --asset https://example.com/assets/app.abc123.js=<sha256> \
149
+ --render-report .maggie/rendered-canary.json \
150
+ --output docs/deployment-canary.json
151
+ ```
152
+
153
+ The canary report requires a screenshot and zero console errors, missing
154
+ assets, and visual placeholders for every route. It records safe cache headers
155
+ only and never stores response bodies, cookies, or credentials.
156
+
110
157
  Recommended upgrade sequence for the current release:
111
158
 
112
159
  ```bash
113
- npx @topy-ai/maggie@0.6.1 update --project . --force
114
- npx @topy-ai/maggie@0.6.1 cleanup --project .
160
+ npx @topy-ai/maggie@0.6.2 update --project . --force
161
+ npx @topy-ai/maggie@0.6.2 cleanup --project .
115
162
  ```
116
163
 
117
164
  ## MaggieDash lifecycle
@@ -0,0 +1,42 @@
1
+ # @topy-ai/maggie(繁體中文)
2
+
3
+ [English README](README.md) · [完整繁體中文 README](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/README.zh-TW.md)
4
+
5
+ `@topy-ai/maggie` 提供 18 個可安裝的 AI website workflow skills,支援
6
+ Codex、Claude Code 與相容的 coding agents。
7
+
8
+ ## 安裝
9
+
10
+ ```bash
11
+ npx @topy-ai/maggie@0.6.2 init --agent all
12
+ npx @topy-ai/maggie doctor --project .
13
+ ```
14
+
15
+ 常用流程:
16
+
17
+ ```bash
18
+ python3 tools/clis/maggie.py analyze . --json --save
19
+ python3 tools/clis/maggie.py bootstrap interview .
20
+ maggie memory context --project . --skill <skill-name>
21
+ maggie doctor --project . --require-bootstrap --strict
22
+ ```
23
+
24
+ 主要 skills 包括 clone、design、template、Blog、SEO/GEO、localization、
25
+ deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
26
+ publish 與 production deployment 需要明確確認。
27
+
28
+ 新版 release gates:
29
+
30
+ ```bash
31
+ maggie design icon-inventory --project . --source-dir src \
32
+ --runtime public/assets/icons.css --output docs/icon-inventory.json
33
+
34
+ maggie deployment canary --project . \
35
+ --asset https://example.com/assets/app.js=<sha256> \
36
+ --render-report .maggie/rendered-canary.json \
37
+ --output docs/deployment-canary.json
38
+ ```
39
+
40
+ 完整中文說明、18 個 skills 清單和 roadmap:
41
+ [繁體中文 README](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/README.zh-TW.md)
42
+ · [Roadmap](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/ROADMAP.md)
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.0.0
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.2.0
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())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,6 +8,7 @@
8
8
  "maggie": "bin/maggie.js"
9
9
  },
10
10
  "files": [
11
+ "README.zh-TW.md",
11
12
  "bin",
12
13
  "references",
13
14
  "bundled-skills",