@topy-ai/maggie 0.7.48 → 0.7.49

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.
Files changed (49) hide show
  1. package/README-zh-TW.md +2 -2
  2. package/README.md +31 -4
  3. package/bin/maggie.js +17 -4
  4. package/bundled-contracts/google-integrations/capability-report-v2.schema.json +2 -1
  5. package/bundled-contracts/maggie-content/email-localization-v1.schema.json +89 -0
  6. package/bundled-contracts/maggie-content/lifecycle-email-v1.schema.json +76 -0
  7. package/bundled-contracts/maggie-deployment/readiness-v1.schema.json +6 -1
  8. package/bundled-contracts/maggie-ops/marketing-run-v1.schema.json +89 -0
  9. package/bundled-contracts/maggie-ops/notification-catalogue-example-v1.json +941 -0
  10. package/bundled-contracts/maggie-ops/notification-catalogue-v1.schema.json +273 -0
  11. package/bundled-contracts/maggie-service-booking/fact-provenance-v1.schema.json +128 -0
  12. package/bundled-contracts/maggie-service-booking/import-stages-v1.schema.json +191 -0
  13. package/bundled-references/google-integrations-runbook.md +54 -0
  14. package/bundled-skills/maggie-content-localization/SKILL.md +17 -1
  15. package/bundled-skills/maggie-content-localization/references/email-and-lifecycle.md +83 -0
  16. package/bundled-skills/maggie-deployment/SKILL.md +89 -7
  17. package/bundled-skills/maggie-deployment/references/vps.md +38 -6
  18. package/bundled-skills/maggie-design/SKILL.md +55 -0
  19. package/bundled-skills/maggie-feedback/SKILL.md +29 -0
  20. package/bundled-skills/maggie-ops/SKILL.md +37 -0
  21. package/bundled-skills/maggie-ops/references/notifications-operations.md +154 -0
  22. package/bundled-skills/maggie-service-booking/SKILL.md +59 -0
  23. package/bundled-skills/maggie-social-share/SKILL.md +19 -2
  24. package/bundled-skills/maggie-social-share/references/marketing-run.md +57 -0
  25. package/bundled-skills/maggie-social-share/references/queue-and-connections.md +51 -0
  26. package/bundled-tools/clis/maggie_deployment.py +58 -17
  27. package/bundled-tools/clis/maggie_deployment_auth_canary.py +216 -0
  28. package/bundled-tools/clis/maggie_deployment_collect.py +225 -0
  29. package/bundled-tools/clis/maggie_deployment_readiness.py +53 -3
  30. package/bundled-tools/clis/maggie_design.py +8 -0
  31. package/bundled-tools/clis/maggie_design_verify.py +359 -0
  32. package/bundled-tools/clis/maggie_docs.py +82 -4
  33. package/bundled-tools/clis/maggie_email_localization.py +382 -0
  34. package/bundled-tools/clis/maggie_feedback.py +9 -2
  35. package/bundled-tools/clis/maggie_icon_glyph_check.py +382 -0
  36. package/bundled-tools/clis/maggie_icon_release_gate.py +28 -1
  37. package/bundled-tools/clis/maggie_localization.py +5 -0
  38. package/bundled-tools/clis/maggie_marketing_run.py +272 -0
  39. package/bundled-tools/clis/maggie_notifications.py +528 -0
  40. package/bundled-tools/clis/maggie_ops.py +87 -0
  41. package/bundled-tools/clis/maggie_schedule.py +12 -2
  42. package/bundled-tools/clis/maggie_service_import.py +520 -0
  43. package/bundled-tools/clis/maggie_social_queue.py +104 -0
  44. package/bundled-tools/clis/maggie_social_share.py +6 -2
  45. package/bundled-tools/runtime/google_ads.py +306 -0
  46. package/bundled-tools/runtime/google_capabilities.py +18 -2
  47. package/bundled-tools/runtime/google_remediation.py +152 -0
  48. package/package.json +1 -1
  49. package/references/google-integrations-runbook.md +54 -0
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ """Collect standard host release evidence and emit one reproducible readiness report.
3
+
4
+ `maggie deployment readiness` validates evidence files but does not produce
5
+ them, so each host used to hand-roll browser, SSH, systemd, and retention
6
+ scripts. This collector runs the standard adapters declared in one reviewed
7
+ config (edge/rendered canary, host health, rollback consistency, remote
8
+ release identity, optional authenticated API canary), writes each adapter's
9
+ evidence, and summarizes everything with the existing
10
+ `maggie-deployment-readiness.v1` report. It never deploys, never uses a
11
+ shell, and never records credentials or response bodies.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import hashlib
18
+ import importlib.util
19
+ import json
20
+ import re
21
+ import subprocess
22
+ import sys
23
+ import urllib.error
24
+ import urllib.request
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+ from typing import Any
28
+ from urllib.parse import urlparse
29
+
30
+ CONFIG_SCHEMA = "maggie-evidence-collector.v1"
31
+ HERE = Path(__file__).resolve().parent
32
+ STANDARD_EVIDENCE = {
33
+ "unitRegression": ".maggie/verification/unit-regression.json",
34
+ "packageSmoke": ".maggie/verification/package-smoke.json",
35
+ "browserEvidence": ".maggie/verification/browser-evidence.json",
36
+ "deploymentPreflight": ".maggie/release-preflight.json",
37
+ }
38
+ SECRET_ARG = re.compile(r"(?:password|passwd|token|secret|apikey|api_key)=|Bearer\s|sk[-_](?:live|test)[-_]|eyJ[A-Za-z0-9_-]{10,}\.", re.IGNORECASE)
39
+ RELEASE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
40
+
41
+
42
+ def load_module(name: str):
43
+ spec = importlib.util.spec_from_file_location(name, HERE / f"{name}.py")
44
+ assert spec and spec.loader
45
+ module = importlib.util.module_from_spec(spec)
46
+ spec.loader.exec_module(module)
47
+ return module
48
+
49
+
50
+ def now() -> str:
51
+ return datetime.now(timezone.utc).isoformat()
52
+
53
+
54
+ def resolve(project: Path, value: str | Path) -> Path:
55
+ path = Path(value).expanduser()
56
+ return path if path.is_absolute() else project / path
57
+
58
+
59
+ def write(path: Path, payload: dict) -> None:
60
+ path.parent.mkdir(parents=True, exist_ok=True)
61
+ path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
62
+
63
+
64
+ def check_argv(argv: object, label: str) -> list[str]:
65
+ if not isinstance(argv, list) or not argv or not all(isinstance(item, str) and item for item in argv):
66
+ raise ValueError(f"{label} command must be a non-empty argv list (no shell string)")
67
+ if any(SECRET_ARG.search(item) for item in argv):
68
+ raise ValueError(f"{label} command contains a literal credential; use host configuration or SSH keys")
69
+ return argv
70
+
71
+
72
+ def http_status(url: str, timeout: float) -> int | None:
73
+ request = urllib.request.Request(url, headers={"User-Agent": "maggie-deployment-collect/1.0"}, method="GET")
74
+ try:
75
+ with urllib.request.urlopen(request, timeout=timeout) as response:
76
+ return response.status
77
+ except urllib.error.HTTPError as error:
78
+ return error.code
79
+ except (urllib.error.URLError, OSError):
80
+ return None
81
+
82
+
83
+ def collect_health(config: dict, timeout: float) -> dict:
84
+ urls = config.get("urls")
85
+ if not isinstance(urls, list) or not urls:
86
+ raise ValueError("health adapter needs a non-empty urls list")
87
+ expect = config.get("expectStatus", [200])
88
+ checks = []
89
+ for url in urls:
90
+ parsed = urlparse(str(url))
91
+ if parsed.scheme not in {"http", "https"} or parsed.query:
92
+ raise ValueError("health URLs must be http(s) without query strings")
93
+ status = http_status(str(url), timeout)
94
+ checks.append({"url": f"{parsed.scheme}://{parsed.netloc}{parsed.path or '/'}", "status": status, "passed": status in expect})
95
+ return {"schemaVersion": "maggie-deployment-health.v1", "generatedAt": now(), "checks": checks, "passed": all(item["passed"] for item in checks)}
96
+
97
+
98
+ def collect_release_identity(config: dict, release: str, timeout: float) -> tuple[dict, dict]:
99
+ run: dict[str, Any] = {}
100
+ observed = ""
101
+ if config.get("command"):
102
+ argv = check_argv(config["command"], "releaseIdentity")
103
+ completed = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
104
+ run = {"argv": argv, "exitCode": completed.returncode}
105
+ raw = completed.stdout.strip().splitlines()[-1].strip() if completed.returncode == 0 and completed.stdout.strip() else ""
106
+ # A `readlink -f <current>` path resolves to .../releases/<release-id>.
107
+ observed = raw.rstrip("/").rsplit("/", 1)[-1] if raw else ""
108
+ elif config.get("url"):
109
+ url = str(config["url"])
110
+ field = str(config.get("field") or "release")
111
+ request = urllib.request.Request(url, headers={"User-Agent": "maggie-deployment-collect/1.0", "Accept": "application/json"})
112
+ try:
113
+ with urllib.request.urlopen(request, timeout=timeout) as response:
114
+ payload = json.loads(response.read(200_000).decode("utf-8"))
115
+ observed = str(payload.get(field, "")) if isinstance(payload, dict) else ""
116
+ run = {"url": url, "field": field}
117
+ except (urllib.error.URLError, OSError, ValueError) as error:
118
+ run = {"url": url, "field": field, "error": type(error).__name__}
119
+ else:
120
+ raise ValueError("releaseIdentity adapter needs command (argv) or url")
121
+ if observed and not RELEASE_ID.fullmatch(observed):
122
+ observed = "<unrecognized>"
123
+ evidence = {"schemaVersion": "maggie-release-identity.v1", "generatedAt": now(), "expected": release, "observed": observed, "passed": bool(observed) and observed == release}
124
+ return evidence, run
125
+
126
+
127
+ def collect_rollback(project: Path, config: dict) -> dict:
128
+ deployment = load_module("maggie_deployment")
129
+ evidence_path = resolve(project, config.get("evidence", "docs/deployment-rollback-smoke.json"))
130
+ keep = None
131
+ if config.get("plan"):
132
+ plan = json.loads(resolve(project, config["plan"]).read_text(encoding="utf-8"))
133
+ keep = (plan.get("retention") or {}).get("keep")
134
+ try:
135
+ result = deployment.validate_rollback_evidence(json.loads(evidence_path.read_text(encoding="utf-8")), keep)
136
+ except (OSError, json.JSONDecodeError) as error:
137
+ result = {"passed": False, "errors": [f"rollback evidence unavailable: {type(error).__name__}"]}
138
+ return {"schemaVersion": "maggie-deployment-rollback-check.v1", "generatedAt": now(), "retentionKeep": keep, "rollbackAvailable": None if keep is None else keep >= 2, "passed": result["passed"], "errors": result["errors"]}
139
+
140
+
141
+ def collect_canary(project: Path, config: dict, destination: Path, timeout: float) -> dict:
142
+ argv = [sys.executable, str(HERE / "maggie_deployment_canary.py"), "--project", str(project), "--output", str(destination)]
143
+ for asset in config.get("assets", []):
144
+ argv += ["--asset", str(asset)]
145
+ if config.get("renderReport"):
146
+ argv += ["--render-report", str(resolve(project, config["renderReport"]))]
147
+ completed = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
148
+ return {"adapter": "maggie_deployment_canary.py", "exitCode": completed.returncode}
149
+
150
+
151
+ def collect(project: Path, config: dict, evidence_dir: Path, timeout: float = 60.0) -> dict:
152
+ if config.get("schemaVersion") != CONFIG_SCHEMA:
153
+ raise ValueError(f"collector config schemaVersion must be {CONFIG_SCHEMA}")
154
+ release = str(config.get("release") or "")
155
+ if not RELEASE_ID.fullmatch(release):
156
+ raise ValueError("collector config needs the expected release id")
157
+ adapters = config.get("adapters") or {}
158
+ unknown = set(adapters) - {"canary", "health", "rollback", "releaseIdentity", "authCanary"}
159
+ if unknown:
160
+ raise ValueError("unsupported adapters: " + ", ".join(sorted(unknown)))
161
+ readiness = load_module("maggie_deployment_readiness")
162
+ paths = {name: resolve(project, (config.get("evidence") or {}).get(name, default)) for name, default in STANDARD_EVIDENCE.items()}
163
+ runs: dict[str, Any] = {}
164
+ canary_path = evidence_dir / "rendered-canary.json"
165
+ if "canary" in adapters:
166
+ runs["canary"] = collect_canary(project, adapters["canary"], canary_path, timeout)
167
+ paths["renderedCanary"] = canary_path
168
+ else:
169
+ paths["renderedCanary"] = resolve(project, (config.get("evidence") or {}).get("renderedCanary", ".maggie/deployment-canary.json"))
170
+ if "health" in adapters:
171
+ write(evidence_dir / "host-health.json", collect_health(adapters["health"], timeout))
172
+ paths["hostHealth"] = evidence_dir / "host-health.json"
173
+ runs["health"] = {"urls": len(adapters["health"].get("urls", []))}
174
+ if "rollback" in adapters:
175
+ write(evidence_dir / "rollback-check.json", collect_rollback(project, adapters["rollback"]))
176
+ paths["rollbackEvidence"] = evidence_dir / "rollback-check.json"
177
+ runs["rollback"] = {"validator": "validate_rollback_evidence"}
178
+ if "releaseIdentity" in adapters:
179
+ evidence, run = collect_release_identity(adapters["releaseIdentity"], release, timeout)
180
+ write(evidence_dir / "release-identity.json", evidence)
181
+ paths["releaseIdentity"] = evidence_dir / "release-identity.json"
182
+ runs["releaseIdentity"] = run
183
+ if "authCanary" in adapters:
184
+ auth = load_module("maggie_deployment_auth_canary")
185
+ spec = adapters["authCanary"]
186
+ plan = json.loads(resolve(project, spec["plan"]).read_text(encoding="utf-8"))
187
+ write(evidence_dir / "auth-canary.json", auth.auth_canary(plan, bool(spec.get("allowMutations")), timeout))
188
+ paths["authenticatedCanary"] = evidence_dir / "auth-canary.json"
189
+ runs["authCanary"] = {"plan": str(spec["plan"]), "allowMutations": bool(spec.get("allowMutations"))}
190
+ report = readiness.readiness_report(project, paths)
191
+ canonical = json.dumps(config, sort_keys=True, ensure_ascii=False).encode("utf-8")
192
+ report["collector"] = {
193
+ "schemaVersion": CONFIG_SCHEMA,
194
+ "configSha256": hashlib.sha256(canonical).hexdigest(),
195
+ "release": release,
196
+ "evidenceDir": readiness.display_path(evidence_dir, project),
197
+ "adapters": runs,
198
+ "mutation": "not executed",
199
+ }
200
+ return report
201
+
202
+
203
+ def main() -> int:
204
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
205
+ parser.add_argument("--project", type=Path, default=Path.cwd())
206
+ parser.add_argument("--config", type=Path, default=Path(".maggie/deployment/collector.json"), help=f"{CONFIG_SCHEMA} config")
207
+ parser.add_argument("--evidence-dir", type=Path, default=Path(".maggie/deployment/collected"))
208
+ parser.add_argument("--timeout", type=float, default=60.0)
209
+ parser.add_argument("--output", type=Path, help="readiness report path (default: <evidence-dir>/readiness.json)")
210
+ args = parser.parse_args()
211
+ project = args.project.resolve()
212
+ evidence_dir = resolve(project, args.evidence_dir)
213
+ try:
214
+ config = json.loads(resolve(project, args.config).read_text(encoding="utf-8"))
215
+ report = collect(project, config, evidence_dir, args.timeout)
216
+ except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
217
+ print(f"BLOCKED: maggie deployment collect: {error}", file=sys.stderr)
218
+ return 2
219
+ write(resolve(project, args.output) if args.output else evidence_dir / "readiness.json", report)
220
+ print(json.dumps(report, indent=2, ensure_ascii=False))
221
+ return 0 if report["status"] == "passed" else 1
222
+
223
+
224
+ if __name__ == "__main__":
225
+ raise SystemExit(main())
@@ -92,6 +92,39 @@ def validate_runtime_parity(payload: dict[str, Any]) -> tuple[bool, str]:
92
92
  return True, "worker/browser runtime parity passed"
93
93
 
94
94
 
95
+ def validate_host_health(payload: dict[str, Any]) -> tuple[bool, str]:
96
+ if payload.get("schemaVersion") != "maggie-deployment-health.v1":
97
+ return False, "host health evidence schemaVersion is unsupported"
98
+ checks = payload.get("checks")
99
+ if payload.get("passed") is not True or not isinstance(checks, list) or not checks:
100
+ return False, "host health evidence must contain passing checks"
101
+ return True, "host health checks passed"
102
+
103
+
104
+ def validate_release_identity(payload: dict[str, Any]) -> tuple[bool, str]:
105
+ if payload.get("schemaVersion") != "maggie-release-identity.v1":
106
+ return False, "release identity evidence schemaVersion is unsupported"
107
+ if payload.get("passed") is not True or not payload.get("expected") or payload.get("observed") != payload.get("expected"):
108
+ return False, "remote release identity does not match the expected release"
109
+ return True, "remote release identity matches"
110
+
111
+
112
+ def validate_rollback_check(payload: dict[str, Any]) -> tuple[bool, str]:
113
+ if payload.get("schemaVersion") != "maggie-deployment-rollback-check.v1":
114
+ return False, "rollback check schemaVersion is unsupported"
115
+ if payload.get("passed") is not True:
116
+ return False, "rollback evidence is inconsistent with the retention policy or not passed"
117
+ return True, "rollback evidence is consistent with the retention policy"
118
+
119
+
120
+ def validate_auth_canary(payload: dict[str, Any]) -> tuple[bool, str]:
121
+ if payload.get("schemaVersion") != "maggie-deployment-auth-canary.v1":
122
+ return False, "authenticated canary schemaVersion is unsupported"
123
+ if payload.get("status") != "passed" or payload.get("passed") is not True:
124
+ return False, "authenticated API canary did not pass"
125
+ return True, "authenticated API canary passed"
126
+
127
+
95
128
  VALIDATORS: dict[str, Callable[[dict[str, Any]], tuple[bool, str]]] = {
96
129
  "unitRegression": validate_unit,
97
130
  "packageSmoke": validate_package,
@@ -99,7 +132,12 @@ VALIDATORS: dict[str, Callable[[dict[str, Any]], tuple[bool, str]]] = {
99
132
  "renderedCanary": validate_canary,
100
133
  "deploymentPreflight": validate_preflight,
101
134
  "runtimeParity": validate_runtime_parity,
135
+ "hostHealth": validate_host_health,
136
+ "releaseIdentity": validate_release_identity,
137
+ "rollbackEvidence": validate_rollback_check,
138
+ "authenticatedCanary": validate_auth_canary,
102
139
  }
140
+ OPTIONAL_CHECKS = ("runtimeParity", "hostHealth", "releaseIdentity", "rollbackEvidence", "authenticatedCanary")
103
141
 
104
142
 
105
143
  def inspect_check(name: str, path: Path, project: Path) -> dict[str, Any]:
@@ -120,7 +158,7 @@ def inspect_check(name: str, path: Path, project: Path) -> dict[str, Any]:
120
158
  return result
121
159
 
122
160
 
123
- def readiness(project: Path, paths: dict[str, Path], output: Path | None = None) -> int:
161
+ def readiness_report(project: Path, paths: dict[str, Path]) -> dict[str, Any]:
124
162
  checks = {name: inspect_check(name, path, project) for name, path in paths.items()}
125
163
  states = {item["state"] for item in checks.values()}
126
164
  status = "failed" if "failed" in states else "inconclusive" if "inconclusive" in states else "passed"
@@ -132,6 +170,12 @@ def readiness(project: Path, paths: dict[str, Path], output: Path | None = None)
132
170
  "checks": checks,
133
171
  "nextAction": "resolve failed evidence before release" if status == "failed" else "collect the missing host adapter evidence" if status == "inconclusive" else "all declared readiness evidence passed",
134
172
  }
173
+ return report
174
+
175
+
176
+ def readiness(project: Path, paths: dict[str, Path], output: Path | None = None) -> int:
177
+ report = readiness_report(project, paths)
178
+ status = report["status"]
135
179
  if output:
136
180
  destination = output if output.is_absolute() else project / output
137
181
  destination.parent.mkdir(parents=True, exist_ok=True)
@@ -150,6 +194,10 @@ def main() -> int:
150
194
  parser.add_argument("--rendered-canary", type=Path, default=Path(".maggie/deployment-canary.json"))
151
195
  parser.add_argument("--deployment-preflight", type=Path, default=Path(".maggie/release-preflight.json"))
152
196
  parser.add_argument("--runtime-parity", type=Path, help="validated worker/browser parity report from maggie deployment parity")
197
+ parser.add_argument("--host-health", type=Path, help="maggie-deployment-health.v1 evidence (e.g. from maggie deployment collect)")
198
+ parser.add_argument("--release-identity", type=Path, help="maggie-release-identity.v1 evidence")
199
+ parser.add_argument("--rollback-check", type=Path, help="maggie-deployment-rollback-check.v1 evidence")
200
+ parser.add_argument("--auth-canary", type=Path, help="maggie-deployment-auth-canary.v1 evidence")
153
201
  parser.add_argument("--output", type=Path)
154
202
  args = parser.parse_args()
155
203
  project = args.project.resolve()
@@ -159,8 +207,10 @@ def main() -> int:
159
207
  name: (getattr(args, option).resolve() if getattr(args, option).is_absolute() else project / getattr(args, option))
160
208
  for name, option in CHECKS
161
209
  }
162
- if args.runtime_parity:
163
- paths["runtimeParity"] = args.runtime_parity.resolve() if args.runtime_parity.is_absolute() else project / args.runtime_parity
210
+ for name, option in (("runtimeParity", "runtime_parity"), ("hostHealth", "host_health"), ("releaseIdentity", "release_identity"), ("rollbackEvidence", "rollback_check"), ("authenticatedCanary", "auth_canary")):
211
+ value = getattr(args, option)
212
+ if value:
213
+ paths[name] = value.resolve() if value.is_absolute() else project / value
164
214
  try:
165
215
  return readiness(project, paths, args.output)
166
216
  except (OSError, ValueError) as error:
@@ -857,12 +857,20 @@ commands:
857
857
  pressable-check flag pressables/buttons nested in another pressable (JSX/TSX)
858
858
  layer-check flag unlayered CSS that overrides Tailwind v4 utilities
859
859
  palette generate or validate a semantic palette role contract
860
+ verify run automatable design-job steps and record evidence
861
+ icon-glyph-check render icons and prove font load and glyph paint
860
862
  reference-ui, init, validate-ui, app-init, app-validate, rebrand, review
861
863
  """)
862
864
  return 0
863
865
  if len(sys.argv) > 1 and sys.argv[1] in {"pressable-check", "layer-check"}:
864
866
  from maggie_design_lint import main as lint_main
865
867
  return lint_main(sys.argv[1:])
868
+ if len(sys.argv) > 1 and sys.argv[1] == "verify":
869
+ from maggie_design_verify import main as verify_main
870
+ return verify_main(sys.argv[2:])
871
+ if len(sys.argv) > 1 and sys.argv[1] == "icon-glyph-check":
872
+ from maggie_icon_glyph_check import main as glyph_main
873
+ return glyph_main(sys.argv[2:])
866
874
  if len(sys.argv) > 1 and sys.argv[1] == "palette":
867
875
  from maggie_palette import main as palette_main
868
876
  return palette_main(sys.argv[2:])