@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 +49 -2
- package/README.zh-TW.md +42 -0
- 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 +2 -1
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
|
[](https://www.npmjs.com/package/@topy-ai/maggie)
|
|
4
6
|
[](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.
|
|
114
|
-
npx @topy-ai/maggie@0.6.
|
|
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
|
package/README.zh-TW.md
ADDED
|
@@ -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.
|
|
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())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topy-ai/maggie",
|
|
3
|
-
"version": "0.6.
|
|
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",
|