@topy-ai/maggie 0.7.0 → 0.7.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.
Files changed (31) hide show
  1. package/README.md +22 -7
  2. package/bin/maggie.js +6 -6
  3. package/bundled-references/maggiedash-dashboard-ui.md +28 -0
  4. package/bundled-skills/maggie-blog/SKILL.md +12 -0
  5. package/bundled-skills/maggie-content-localization/SKILL.md +9 -0
  6. package/bundled-skills/maggie-dash/SKILL.md +21 -0
  7. package/bundled-skills/maggie-deployment/SKILL.md +17 -0
  8. package/bundled-skills/maggie-ops/SKILL.md +27 -0
  9. package/bundled-skills/maggie-seo-geo/SKILL.md +21 -1
  10. package/bundled-skills/maggie-service-booking/SKILL.md +6 -0
  11. package/bundled-templates/maggiedash/README.md +4 -0
  12. package/bundled-templates/maggiedash/dashboard-ui-contract.json +31 -0
  13. package/bundled-tools/clis/maggie_analytics.py +14 -1
  14. package/bundled-tools/clis/maggie_blog.py +6 -0
  15. package/bundled-tools/clis/maggie_dash.py +54 -0
  16. package/bundled-tools/clis/maggie_deployment.py +43 -0
  17. package/bundled-tools/clis/maggie_feedback.py +16 -1
  18. package/bundled-tools/clis/maggie_ops.py +21 -1
  19. package/bundled-tools/clis/maggie_service_booking.py +6 -3
  20. package/bundled-tools/clis/maggie_sitemap.py +16 -2
  21. package/bundled-tools/runtime/analytics_traffic.py +30 -0
  22. package/bundled-tools/runtime/content_localization.py +63 -1
  23. package/bundled-tools/runtime/dependency_lock.py +43 -0
  24. package/bundled-tools/runtime/integration_state.py +17 -0
  25. package/bundled-tools/runtime/maggie_dash_store.py +150 -6
  26. package/bundled-tools/runtime/maggie_dash_ui.py +60 -0
  27. package/bundled-tools/runtime/maggie_sitemap.py +50 -4
  28. package/bundled-tools/runtime/route_imports.py +51 -0
  29. package/bundled-tools/runtime/seed_evidence.py +25 -0
  30. package/package.json +1 -1
  31. package/references/maggiedash-dashboard-ui.md +28 -0
package/README.md CHANGED
@@ -14,9 +14,9 @@ persistent project memory.
14
14
 
15
15
  ## Install
16
16
 
17
- Working-tree additions (not yet released): `site-audit --crawl --save-baseline
18
- FILE --reviewer NAME` records a reviewed site contract; `site-audit --crawl
19
- --baseline FILE` fails on URL, metadata, HTML structure or copy changes.
17
+ `site-audit --crawl --save-baseline FILE --reviewer NAME` records a reviewed
18
+ site contract; `site-audit --crawl --baseline FILE` fails on URL, metadata,
19
+ HTML structure or copy changes.
20
20
  Complete sitemap coverage is required and existing baselines cannot be
21
21
  overwritten. This does not verify browser layout or source-only changes.
22
22
 
@@ -88,7 +88,7 @@ The CLI provides the installer plus durable workflow commands:
88
88
  maggie init | install | update | remove | list | doctor
89
89
  maggie cleanup --project . [--confirm]
90
90
  maggie bootstrap interview | phase ...
91
- maggie dash init | status | migrate
91
+ maggie dash init | status | migrate | cms ...
92
92
  maggie dash transition ... # explicit content approval transition
93
93
  maggie dash variant ... # service variant create/review/preview/publish
94
94
  maggie clone ... # authorized homepage capture
@@ -101,7 +101,7 @@ maggie localization ... # plan, validate, review, publish, stale
101
101
  maggie service ... # import, sync, generate, validate
102
102
  maggie seo performance ... # sampled PageSpeed/CWV report and baseline
103
103
  maggie seo images ... # inventory, variants, confirmation, validate
104
- maggie seo sitemap ... # typed plan, validate, apply, rollback
104
+ maggie seo sitemap ... # typed/semantic plan, agent-files, apply, rollback
105
105
  maggie deployment | migration | release | analytics | schedule
106
106
  maggie deployment canary --asset URL=SHA256 --render-report report.json
107
107
  maggie design icon-inventory --source-dir src --runtime assets/icons.css
@@ -190,10 +190,25 @@ artifact schemas.
190
190
  Recommended upgrade sequence for the current release:
191
191
 
192
192
  ```bash
193
- npx @topy-ai/maggie@0.7.0 update --project . --force
194
- npx @topy-ai/maggie@0.7.0 cleanup --project .
193
+ npx @topy-ai/maggie@0.7.1 update --project . --force
194
+ npx @topy-ai/maggie@0.7.1 cleanup --project .
195
195
  ```
196
196
 
197
+ Maintainers should pass npm credentials through the repository helper, never
198
+ as a command-line argument:
199
+
200
+ ```bash
201
+ node scripts/publish-npm.mjs --maggie-env-file ../.env
202
+ ```
203
+
204
+ The 0.7.1 workflow adds audited MaggieDash CMS operations (`cms revisions`,
205
+ `trash`, `restore`, `schedule`, `duplicate`, `redirect`, and signed
206
+ `preview`), import-authoritative service matching, shared translation indexes,
207
+ sanitized seed manifests, lockfile/analytics traffic checks, semantic sitemap
208
+ validation, locale-aware `llms.txt`/`sitemap.md`/`insights.md` generation,
209
+ explicit integration states, and deployment Origin/infrastructure/data
210
+ rollback gates.
211
+
197
212
  ## MaggieDash lifecycle
198
213
 
199
214
  For a new project, establish the local content and approval foundation before
package/bin/maggie.js CHANGED
@@ -65,7 +65,7 @@ Usage:
65
65
  maggie list
66
66
  maggie doctor [--project PATH]
67
67
  maggie bootstrap interview [project]
68
- maggie dash init|status|migrate|transition|variant --project PATH [options]
68
+ maggie dash init|status|migrate|transition|variant|cms --project PATH [options]
69
69
  maggie dash status --project PATH
70
70
  maggie dash migrate --project PATH --confirm
71
71
  maggie content FILE --source PROVIDER --project PATH --confirm
@@ -92,7 +92,7 @@ Usage:
92
92
  maggie design author --project PATH --route /about --purpose TEXT --audience TEXT --confirm
93
93
  maggie auth reference --project PATH --confirm
94
94
  maggie auth check --project PATH [--production]
95
- maggie blog init|inspect|ingest|validate|publish|sitemap|settings|rollback --project PATH
95
+ maggie blog init|inspect|ingest|validate|publish|sitemap|settings|rollback|integration-state
96
96
  maggie design status <job-id>
97
97
  maggie service import <provider-url> --project PATH
98
98
  maggie service sync <provider-url> --project PATH
@@ -106,18 +106,18 @@ Usage:
106
106
  maggie deployment canary --project PATH --asset URL=SHA256 --render-report report.json --output docs/deployment-canary.json
107
107
  maggie migration --project PATH --environment staging
108
108
  maggie schedule PATH/.maggie/schedule.json --project PATH
109
- maggie analytics --project PATH --environment staging
109
+ maggie analytics [traffic-audit|release-gate] --project PATH --environment staging
110
110
  maggie release PATH --environment staging --target vps-with-cloudflare-dns
111
111
  maggie api lifecycle --project PATH [--execute --allow-quota]
112
112
  maggie memory <init|list|search|context|add|record-error|transition|export> --project PATH
113
113
  maggie localization <extract|plan|generate|preview|validate|review|publish|stale|glossary> [options]
114
- maggie seo performance|images|sitemap [options]
114
+ maggie seo performance|images|sitemap [options] (sitemap supports strict validate and agent-files)
115
115
  maggie feedback <collect|preview|submit|list> [options]
116
116
  maggie site-audit URL [--crawl] [--languages en-GB,es-MX,ja-JP] [--check-hreflang]
117
117
  maggie site-audit URL --crawl --save-baseline FILE --reviewer NAME
118
118
  maggie site-audit URL --crawl --baseline FILE
119
119
  maggie browser-audit URL --browse PATH --output DIR --required SELECTOR [--sticky SELECTOR]
120
- maggie ops audit --project PATH
120
+ maggie ops audit|preflight|verify|lockfiles|seed-manifest --project PATH
121
121
  maggie ops preflight --project PATH --write
122
122
 
123
123
  Examples:
@@ -290,7 +290,7 @@ function service(args) {
290
290
  const root = projectRoot(args);
291
291
  const script = join(root, "tools", "clis", "maggie_service_booking.py");
292
292
  if (!existsSync(script)) throw new Error(`service booking CLI is missing: ${script}`);
293
- const result = spawnSync("python3", [script, ...args], { stdio: "inherit", cwd: root });
293
+ const result = spawnSync("python3", [script, ...args], { stdio: "inherit", cwd: root, env: { ...process.env, MAGGIE_VERSION: PACKAGE_VERSION } });
294
294
  if (result.error) throw result.error;
295
295
  process.exitCode = result.status ?? 1;
296
296
  }
@@ -0,0 +1,28 @@
1
+ # MaggieDash dashboard UI contract
2
+
3
+ MaggieDash workspaces use the lightweight contract in
4
+ `templates/maggiedash/dashboard-ui-contract.json`. It is provider-neutral and
5
+ describes the dashboard chrome, not a framework-specific component library.
6
+
7
+ The reference is the users workspace: one `WorkspaceBar`, one sidebar/content
8
+ navigation relationship, and one `ContentTabs` row. A route must not render a
9
+ second horizontal navigation, duplicate its sidebar, or hide duplicate markup
10
+ with CSS. `WorkspaceCard` owns card framing; page routes own content.
11
+
12
+ `ContentTabs` accepts only the props it renders (`tabs`, `active`, `actions`,
13
+ and `children`). It does not accept a page `title` or `description`. If a page
14
+ needs a heading or description, render a dedicated page header or use
15
+ `WorkspaceBar` explicitly. This keeps component APIs honest and prevents
16
+ dead-copy drift.
17
+
18
+ Validate a host implementation with:
19
+
20
+ ```bash
21
+ maggie dash ui validate --contract templates/maggiedash/dashboard-ui-contract.json \
22
+ --source src/components/WorkspaceBar.tsx \
23
+ --source src/components/WorkspaceCard.tsx \
24
+ --source src/components/ContentTabs.tsx
25
+ ```
26
+
27
+ The check is static evidence. It does not replace an authenticated browser
28
+ review of the rendered dashboard at desktop and mobile sizes.
@@ -60,3 +60,15 @@ maggie design init --project . --surface blog \
60
60
 
61
61
  The blog skill supplies route/data semantics; `maggie-design` supplies the
62
62
  host-native components and responsive visual review.
63
+
64
+ Optional Search Console/AI visibility integrations must report state rather
65
+ than returning an ambiguous empty success payload. Use:
66
+
67
+ ```bash
68
+ maggie blog integration-state # not-configured
69
+ maggie blog integration-state --configured --consent-required # awaiting-consent
70
+ maggie blog integration-state --configured --authorized # ready
71
+ ```
72
+
73
+ Provider adapters should preserve the same `not-configured`,
74
+ `awaiting-consent`, `awaiting-authorization`, `ready`, and `error` semantics.
@@ -116,3 +116,12 @@ styles, SVG, and dynamic expressions. Supplying source and render reports to
116
116
  remains backward-compatible without those artifacts. See
117
117
  [`docs/localization-extraction-render-prd.md`](../../docs/localization-extraction-render-prd.md)
118
118
  for the artifact contract and limitations.
119
+
120
+ Use the shared translation index and route predicates from
121
+ `tools/runtime/content_localization.py`. A host must not keep a second wording
122
+ dictionary beside its translation registry: conflicting `(contentId, locale)`
123
+ records fail closed. Use `is_translated_path()` for both exact routes and
124
+ prefix routes, and do not let locale middleware capture root `.txt`, `.md`,
125
+ `.xml`, or `.json` assets. If a framework rewrites a localized request,
126
+ dedupe instrumentation with `rewrite_once(request_key, seen)` so one request
127
+ does not count twice.
@@ -74,3 +74,24 @@ not part of this skill.
74
74
  Before and after a meaningful run, load and record confirmed project
75
75
  preferences or repaired pitfalls with
76
76
  [the shared memory hook](../../references/memory-hook.md).
77
+
78
+ CMS operations are explicit and auditable:
79
+
80
+ ```bash
81
+ maggie dash cms revisions --project . --project-id local-project --document-id <id> --confirm
82
+ maggie dash cms trash --project . --project-id local-project --document-id <id> --reason "remove from editor" --confirm
83
+ maggie dash cms restore --project . --project-id local-project --document-id <id> --reason "restore" --confirm
84
+ maggie dash cms schedule --project . --project-id local-project --document-id <id> \
85
+ --publish-at 2026-09-20T10:00:00Z --reason "approved release" --confirm
86
+ maggie dash cms duplicate --project . --project-id local-project --document-id <id> \
87
+ --new-id <new-id> --new-slug <new-slug> --reason "create draft" --confirm
88
+ maggie dash cms redirect --project . --project-id local-project --from-path /old --to-path /new \
89
+ --reason "canonical slug change" --confirm
90
+ maggie dash cms preview --project . --project-id local-project --document-id <id> \
91
+ --secret "$MAGGIE_PREVIEW_SECRET" --confirm
92
+ ```
93
+
94
+ Content writes create immutable revision snapshots. Trash is reversible and
95
+ does not destroy the document. Scheduling is accepted only for approved
96
+ content and requires a timezone. Preview tokens are short-lived HMAC-signed
97
+ tokens; never place the secret in source control or generated reports.
@@ -210,3 +210,20 @@ Follow the shared [Maggie Decision Loop](../../references/decision-loop.md) for
210
210
  6. Require explicit final confirmation before any mutation or external write.
211
211
 
212
212
  If a decision is not relevant, record it as `skipped` with a reason. Do not silently assume a missing choice, and do not treat an existing output as permission to skip required work.
213
+
214
+ For scheduled POST jobs, validate the request contract before installation:
215
+
216
+ ```bash
217
+ maggie deployment --validate-request --method POST \
218
+ --origin https://example.test --has-auth
219
+ maggie deployment --verify-infra --service fresha \
220
+ --units-dir .maggie/deployment/schedules
221
+ ```
222
+
223
+ The deployment scheduler must send an explicit Origin and configured auth
224
+ contract; a missing Origin must not be mistaken for an application auth
225
+ failure. Infrastructure verification accepts a declared service/timer pair or
226
+ both units being enabled in systemd. Data-dependent releases additionally
227
+ require `rollback.backupId` and `rollback.restoreCommand` in
228
+ `.maggie/deployment/data-release.json`, because switching code alone does not
229
+ restore incompatible data.
@@ -7,6 +7,19 @@ metadata:
7
7
 
8
8
  # Maggie Ops
9
9
 
10
+ Before release, run the lockfile guard when a project has more than one package
11
+ manager:
12
+
13
+ ```bash
14
+ maggie ops lockfiles --project .
15
+ ```
16
+
17
+ It compares direct package and optional dependency names in `package.json`
18
+ with `package-lock.json`, reports the presence of `pnpm-lock.yaml`, and fails
19
+ when npm CI cannot install a declared package. It does not rewrite either
20
+ lockfile; regenerate and commit both through the project's chosen package
21
+ manager workflow.
22
+
10
23
  ## Automatic memory hook
11
24
 
12
25
  Follow [Maggie Memory Hook](../../references/memory-hook.md) at invocation and completion.
@@ -195,3 +208,17 @@ Follow the shared [Maggie Decision Loop](../../references/decision-loop.md) for
195
208
  6. Require explicit final confirmation before any mutation or external write.
196
209
 
197
210
  If a decision is not relevant, record it as `skipped` with a reason. Do not silently assume a missing choice, and do not treat an existing output as permission to skip required work.
211
+
212
+ Before data-dependent checks, validate an explicit sanitized fixture manifest:
213
+
214
+ ```bash
215
+ maggie ops seed-manifest --project . --manifest .maggie/seed-manifest.json
216
+ maggie ops lockfiles --project .
217
+ ```
218
+
219
+ The seed manifest must be opt-in, sanitized, and contain non-empty fixtures;
220
+ an empty dev database is not evidence that an operational check passed.
221
+ Known Maggie/toolchain traffic can be measured without polluting first-party
222
+ analytics using `maggie analytics traffic-audit --events events.json`. The
223
+ classifier excludes only explicit tool/test markers and never infers identity
224
+ from IP or private fields.
@@ -33,6 +33,11 @@ lastmod, optional JSON object mapping locale tags to alternate URLs, such as
33
33
  It emits `lastmod` and `xhtml:link rel="alternate"`. Supply truthful content
34
34
  change dates; omit unknown dates. Alternates must currently use the approved
35
35
  origin. Reciprocal locale coverage and date semantics require separate review.
36
+ `lastmod` must come from a recorded content-change event or source revision;
37
+ never derive it from `updated_at`, pull time, sync time, or deployment time.
38
+ If the change date is unknown, omit `lastmod`. An empty content-type does not
39
+ need a sitemap chunk in the sitemap index; serving an empty endpoint and
40
+ advertising it are separate decisions.
36
41
 
37
42
  ## Freeze and compare a reviewed site
38
43
 
@@ -122,7 +127,7 @@ maggie seo sitemap rollback --backup-manifest .maggie-sitemap-backups/<plan>/bac
122
127
 
123
128
  Only confirmed image variants may enter `srcset`; `apply` requires an explicit
124
129
  confirmation and host adapter. Sitemap plans keep content types separate,
125
- emit chunk 1 for empty enabled types, enforce absolute same-origin URLs, and
130
+ omit empty chunks from the sitemap index, enforce absolute same-origin URLs, and
126
131
  record redirects for removed sitemap files. Read the [image and sitemap PRD](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/docs/image-sitemap-structure-prd.md)
127
132
  for adapter and rollback rules.
128
133
 
@@ -207,3 +212,18 @@ Follow the shared [Maggie Decision Loop](../../references/decision-loop.md) for
207
212
  6. Require explicit final confirmation before any mutation or external write.
208
213
 
209
214
  If a decision is not relevant, record it as `skipped` with a reason. Do not silently assume a missing choice, and do not treat an existing output as permission to skip required work.
215
+
216
+ Use semantic validation when route evidence is available:
217
+
218
+ ```bash
219
+ maggie seo sitemap validate --plan .maggie/sitemap-plan.json --strict-semantic
220
+ maggie seo sitemap agent-files --origin https://example.test \
221
+ --routes-file .maggie/routes.tsv --locale fr-FR --output-dir public/fr
222
+ ```
223
+
224
+ Strict validation checks searchable content evidence, indexability, self
225
+ canonical ownership and truthful `lastmod` provenance. `lastmod` may only be
226
+ backed by a content-change/source-revision event; operational sync, pull,
227
+ deploy, or `updated_at` timestamps are rejected. The agent-file command emits
228
+ locale-aware `llms.txt`, `sitemap.md`, and `insights.md` from the same route
229
+ inventory; non-indexable routes are omitted.
@@ -7,6 +7,12 @@ metadata:
7
7
 
8
8
  # Maggie Service Booking
9
9
 
10
+ Route matching evidence must come from the route source's actual import
11
+ statements. The shared resolver in `tools/runtime/route_imports.py` resolves
12
+ relative imports and records a component fingerprint; it never selects a
13
+ same-basename sibling as a fallback. Missing imports are evidence requiring
14
+ review, not permission to guess.
15
+
10
16
  ## Reviewed page relationships
11
17
 
12
18
  Reviewed page selection preserves existing `supporting` relations, including
@@ -7,3 +7,7 @@ own framework conventions for rendering.
7
7
  The package includes only these lightweight contracts. Large marketplace
8
8
  previews and template media remain separately distributable through the
9
9
  marketplace repository.
10
+
11
+ Dashboard routes should adopt `dashboard-ui-contract.json` and validate their
12
+ actual host components with `maggie dash ui validate`. The contract provides
13
+ the shared workspace chrome and rejects components that declare dead props.
@@ -0,0 +1,31 @@
1
+ {
2
+ "schemaVersion": "maggiedash-dashboard-ui.v1",
3
+ "shell": {
4
+ "reference": "users-workspace",
5
+ "navigation": "sidebar-plus-content-tabs",
6
+ "rules": [
7
+ "render one workspace bar per route",
8
+ "render one content tab bar per route",
9
+ "do not duplicate sidebar navigation in page content",
10
+ "do not hide duplicate navigation with CSS"
11
+ ]
12
+ },
13
+ "components": [
14
+ {
15
+ "name": "WorkspaceBar",
16
+ "props": ["workspaceName", "activeSection", "actions", "children"],
17
+ "forbiddenProps": []
18
+ },
19
+ {
20
+ "name": "WorkspaceCard",
21
+ "props": ["title", "children"],
22
+ "forbiddenProps": []
23
+ },
24
+ {
25
+ "name": "ContentTabs",
26
+ "props": ["tabs", "active", "actions", "children"],
27
+ "forbiddenProps": ["title", "description"]
28
+ }
29
+ ],
30
+ "accessibility": ["active tab exposes aria-selected", "actions have accessible names", "workspace navigation has a landmark"]
31
+ }
@@ -10,6 +10,9 @@ import re
10
10
  import subprocess
11
11
  from pathlib import Path
12
12
  from urllib.parse import urlsplit
13
+ import sys
14
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
15
+ from analytics_traffic import audit_events # noqa: E402
13
16
 
14
17
 
15
18
  GA4_ID = re.compile(r"^G-[A-Z0-9]+$", re.I)
@@ -107,7 +110,7 @@ def release_gate(args: argparse.Namespace) -> int:
107
110
 
108
111
  def main() -> int:
109
112
  parser = argparse.ArgumentParser()
110
- parser.add_argument("command", nargs="?", choices=("release-gate",))
113
+ parser.add_argument("command", nargs="?", choices=("release-gate", "traffic-audit"))
111
114
  parser.add_argument("--project", default=".")
112
115
  parser.add_argument("--environment", choices=("development", "staging", "production"), default="staging")
113
116
  parser.add_argument("--env-file", help="optional env file; values are never printed")
@@ -117,12 +120,22 @@ def main() -> int:
117
120
  parser.add_argument("--network-report", help="redacted network evidence for release-gate")
118
121
  parser.add_argument("--provider-report", help="read-only provider evidence for release-gate")
119
122
  parser.add_argument("--smoke-report", help="production smoke evidence for release-gate")
123
+ parser.add_argument("--events", help="redacted JSON array of analytics events for traffic-audit")
120
124
  args = parser.parse_args()
121
125
  if args.command == "release-gate":
122
126
  required = ("contract", "render_report", "network_report", "provider_report", "smoke_report")
123
127
  if any(not getattr(args, name) for name in required):
124
128
  parser.error("release-gate requires --contract, --render-report, --network-report, --provider-report, and --smoke-report")
125
129
  return release_gate(args)
130
+ if args.command == "traffic-audit":
131
+ if not args.events:
132
+ parser.error("traffic-audit requires --events")
133
+ value = json.loads(Path(args.events).read_text(encoding="utf-8"))
134
+ if not isinstance(value, list):
135
+ parser.error("--events must contain a JSON array")
136
+ result = audit_events(value)
137
+ print(json.dumps(result, indent=2, ensure_ascii=False))
138
+ return 0
126
139
  project = Path(args.project).resolve()
127
140
  env = parse_env(Path(args.env_file).resolve()) if args.env_file else parse_env(project / ".env.example")
128
141
  env.update({key: value for key, value in os.environ.items() if key in {"PUBLIC_GA4_MEASUREMENT_ID", "PUBLIC_ANALYTICS_ENABLED", "PUBLIC_ANALYTICS_CONSENT_REQUIRED", "GSC_SITE_URL", "GSC_VERIFICATION_TOKEN"}})
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
12
12
  from maggie_blog import BlogStore # noqa: E402
13
13
  from localization_runner import process_adapter
14
+ from integration_state import integration_state # noqa: E402
14
15
 
15
16
 
16
17
  def main() -> int:
@@ -29,8 +30,13 @@ def main() -> int:
29
30
  publish = sub.add_parser("publish"); publish.add_argument("--project", type=Path, default=Path.cwd()); publish.add_argument("--slug", required=True); publish.add_argument("--actor", required=True); publish.add_argument("--reason", required=True); publish.add_argument("--confirm", action="store_true")
30
31
  sitemap = sub.add_parser("sitemap"); sitemap.add_argument("--project", type=Path, default=Path.cwd())
31
32
  settings = sub.add_parser("settings"); settings.add_argument("--project", type=Path, default=Path.cwd())
33
+ state = sub.add_parser("integration-state"); state.add_argument("--configured", action="store_true"); state.add_argument("--consent-required", action="store_true"); state.add_argument("--consent", action="store_true"); state.add_argument("--authorized", action="store_true"); state.add_argument("--error")
32
34
  rollback = sub.add_parser("rollback"); rollback.add_argument("--project", type=Path, default=Path.cwd()); rollback.add_argument("--backup"); rollback.add_argument("--confirm", action="store_true")
33
35
  args = parser.parse_args()
36
+ if args.command == "integration-state":
37
+ result = integration_state(configured=args.configured, consent_required=args.consent_required, consent=args.consent, authorized=args.authorized, error=args.error)
38
+ print(json.dumps(result, indent=2, ensure_ascii=False))
39
+ return 0 if result["status"] != "error" else 1
34
40
  store = BlogStore(args.project.resolve())
35
41
  if args.command in {"init", "ingest", "publish", "rollback", "translate-pending"} and not args.confirm:
36
42
  print("CONFIRMATION_REQUIRED: rerun with --confirm", file=sys.stderr); return 2
@@ -17,6 +17,7 @@ from pathlib import Path
17
17
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
18
18
  from maggie_dash_store import MaggieDashStore # noqa: E402
19
19
  from service_variants import ServiceVariantStore # noqa: E402
20
+ from maggie_dash_ui import load_and_validate # noqa: E402
20
21
 
21
22
 
22
23
  def project_root(args: argparse.Namespace) -> Path:
@@ -93,6 +94,38 @@ def command_transition(args: argparse.Namespace) -> int:
93
94
  store.close()
94
95
 
95
96
 
97
+ def command_cms(args: argparse.Namespace) -> int:
98
+ require_confirm(args)
99
+ store = store_for(project_root(args))
100
+ try:
101
+ if args.cms_command == "revisions":
102
+ result = store.list_revisions(args.project_id, args.document_id)
103
+ elif args.cms_command == "trash":
104
+ result = store.trash(args.project_id, args.document_id, args.actor, args.reason)
105
+ elif args.cms_command == "restore":
106
+ result = store.restore(args.project_id, args.document_id, args.actor, args.reason)
107
+ elif args.cms_command == "schedule":
108
+ result = store.schedule_publish(args.project_id, args.document_id, args.publish_at, args.actor, args.reason)
109
+ elif args.cms_command == "duplicate":
110
+ result = store.duplicate(args.project_id, args.document_id, args.new_id, args.new_slug, args.actor, args.reason)
111
+ elif args.cms_command == "redirect":
112
+ result = store.add_redirect(args.project_id, args.from_path, args.to_path, args.actor, args.reason, args.status_code)
113
+ elif args.cms_command == "preview":
114
+ result = store.issue_preview(args.project_id, args.document_id, args.secret, args.ttl)
115
+ else:
116
+ raise ValueError(f"unknown CMS command: {args.cms_command}")
117
+ emit(result)
118
+ return 0
119
+ finally:
120
+ store.close()
121
+
122
+
123
+ def command_ui(args: argparse.Namespace) -> int:
124
+ result = load_and_validate(Path(args.contract).resolve(), [Path(value).resolve() for value in args.source])
125
+ emit(result)
126
+ return 0 if result["passed"] else 1
127
+
128
+
96
129
  def variant_store(args: argparse.Namespace) -> ServiceVariantStore:
97
130
  return ServiceVariantStore(project_root(args) / ".maggie" / "service-variants.json")
98
131
 
@@ -155,6 +188,27 @@ def parser() -> argparse.ArgumentParser:
155
188
  transition.add_argument("--reason", required=True)
156
189
  transition.add_argument("--confirm", action="store_true")
157
190
  transition.set_defaults(func=command_transition)
191
+ cms = sub.add_parser("cms", help="manage revisions, trash, scheduling, redirects and previews")
192
+ cms_sub = cms.add_subparsers(dest="cms_command", required=True)
193
+ for name in ("revisions", "trash", "restore"):
194
+ command = cms_sub.add_parser(name)
195
+ command.add_argument("--project", default="."); command.add_argument("--project-id", default="local-project")
196
+ command.add_argument("--document-id", required=True); command.add_argument("--actor", default="cli"); command.add_argument("--reason", default="CMS operation"); command.add_argument("--confirm", action="store_true")
197
+ schedule = cms_sub.add_parser("schedule")
198
+ schedule.add_argument("--project", default="."); schedule.add_argument("--project-id", default="local-project"); schedule.add_argument("--document-id", required=True); schedule.add_argument("--publish-at", required=True); schedule.add_argument("--actor", default="cli"); schedule.add_argument("--reason", required=True); schedule.add_argument("--confirm", action="store_true")
199
+ duplicate = cms_sub.add_parser("duplicate")
200
+ duplicate.add_argument("--project", default="."); duplicate.add_argument("--project-id", default="local-project"); duplicate.add_argument("--document-id", required=True); duplicate.add_argument("--new-id", required=True); duplicate.add_argument("--new-slug", required=True); duplicate.add_argument("--actor", default="cli"); duplicate.add_argument("--reason", required=True); duplicate.add_argument("--confirm", action="store_true")
201
+ redirect = cms_sub.add_parser("redirect")
202
+ redirect.add_argument("--project", default="."); redirect.add_argument("--project-id", default="local-project"); redirect.add_argument("--from-path", required=True); redirect.add_argument("--to-path", required=True); redirect.add_argument("--status-code", type=int, default=301); redirect.add_argument("--actor", default="cli"); redirect.add_argument("--reason", required=True); redirect.add_argument("--confirm", action="store_true")
203
+ preview = cms_sub.add_parser("preview")
204
+ preview.add_argument("--project", default="."); preview.add_argument("--project-id", default="local-project"); preview.add_argument("--document-id", required=True); preview.add_argument("--secret", required=True); preview.add_argument("--ttl", type=int, default=900); preview.add_argument("--confirm", action="store_true")
205
+ cms.set_defaults(func=command_cms)
206
+ ui = sub.add_parser("ui", help="validate MaggieDash dashboard chrome")
207
+ ui_sub = ui.add_subparsers(dest="ui_command", required=True)
208
+ ui_validate = ui_sub.add_parser("validate")
209
+ ui_validate.add_argument("--contract", required=True)
210
+ ui_validate.add_argument("--source", action="append", default=[])
211
+ ui_validate.set_defaults(func=command_ui)
158
212
  variant = sub.add_parser("variant", help="manage service variant lifecycle")
159
213
  variant_sub = variant.add_subparsers(dest="variant_command", required=True)
160
214
  create = variant_sub.add_parser("create"); create.add_argument("--project", default="."); create.add_argument("--service-id", required=True); create.add_argument("--variant-id", required=True); create.add_argument("--variant-type", required=True); create.add_argument("--locale", required=True); create.add_argument("--market", required=True); create.add_argument("--slug", required=True); create.add_argument("--title", required=True); create.add_argument("--facts", required=True); create.add_argument("--source-revision", required=True); create.add_argument("--canonical-variant-id"); create.add_argument("--cluster-link", action="append", default=[]); create.add_argument("--layout-family", default="service-default"); create.add_argument("--confirm", action="store_true")
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import json
7
7
  import re
8
+ import subprocess
8
9
  from datetime import datetime, timezone
9
10
  from pathlib import Path
10
11
 
@@ -106,6 +107,7 @@ def vps_plan(domain: str, service: str, release_root: str, node_port: int) -> di
106
107
  "restart": f"systemctl restart {service}",
107
108
  "verify": f"systemctl is-active {service} && curl -fsS https://{domain}/robots.txt && curl -fsS https://{domain}/sitemap.xml",
108
109
  "rollback": f"ln -sfn {root}/releases/<previous-release> {current} && systemctl restart {service}",
110
+ "data_rollback": "restore the matching data checkpoint before restarting the previous code release",
109
111
  },
110
112
  "files": {
111
113
  "systemd": f"{service}.service",
@@ -130,10 +132,36 @@ def validate_data_checkpoint(path: Path) -> dict:
130
132
  if isinstance(required, list) and isinstance(current, dict):
131
133
  missing = [table for table in required if table not in current]
132
134
  if missing: errors.append("currentCounts is missing required tables")
135
+ empty = [table for table in required if table in current and (not isinstance(current[table], int) or current[table] <= 0)]
136
+ if empty: errors.append("currentCounts has empty required tables: " + ", ".join(empty))
133
137
  if not value.get("release") or not value.get("checkedAt"): errors.append("release and checkedAt are required")
138
+ rollback = value.get("rollback")
139
+ if not isinstance(rollback, dict) or not rollback.get("backupId") or not rollback.get("restoreCommand"):
140
+ errors.append("rollback.backupId and rollback.restoreCommand are required for data rollback")
134
141
  return {"passed": not errors, "declared": True, "errors": errors}
135
142
 
136
143
 
144
+ def validate_scheduler_request(method: str, origin: str | None, has_auth: bool) -> dict:
145
+ errors = []
146
+ if method.upper() == "POST":
147
+ if not origin or not re.fullmatch(r"https?://[^/\s]+", origin): errors.append("POST scheduler requests require an explicit Origin header")
148
+ if not has_auth: errors.append("POST scheduler requests require the configured auth contract")
149
+ return {"schemaVersion": "maggie-deployment-request.v1", "method": method.upper(), "originPresent": bool(origin), "authPresent": bool(has_auth), "passed": not errors, "errors": errors}
150
+
151
+
152
+ def verify_infrastructure(units_dir: Path | None, service: str, timer: str | None = None) -> dict:
153
+ timer_name = timer or f"maggie-{service}.timer"
154
+ service_name = f"maggie-{service}.service" if not service.endswith(".service") else service
155
+ declared = bool(units_dir and (units_dir / service_name).is_file() and (units_dir / timer_name).is_file())
156
+ systemd = {"service": False, "timer": False}
157
+ for unit, key in ((service_name, "service"), (timer_name, "timer")):
158
+ try:
159
+ systemd[key] = subprocess.run(["systemctl", "is-enabled", unit], capture_output=True, text=True, check=False, timeout=5).returncode == 0
160
+ except (OSError, subprocess.SubprocessError):
161
+ systemd[key] = False
162
+ return {"schemaVersion": "maggie-deployment-infra.v1", "service": service_name, "timer": timer_name, "declaredArtifacts": declared, "systemdEnabled": systemd, "passed": declared or all(systemd.values()), "errors": [] if declared or all(systemd.values()) else ["systemd service/timer are not declared or enabled"]}
163
+
164
+
137
165
  def retention_plan(release_root: str, current_link: str, keep: int) -> dict:
138
166
  if keep < 2 or keep > 5:
139
167
  raise ValueError("keep must be between 2 and 5")
@@ -206,6 +234,13 @@ def main() -> int:
206
234
  parser.add_argument("--retention-plan", action="store_true", help="create a read-only release prune candidate plan")
207
235
  parser.add_argument("--current-link", help="current symlink for --retention-plan")
208
236
  parser.add_argument("--keep-releases", type=int, default=2)
237
+ parser.add_argument("--verify-infra", action="store_true", help="verify declared or installed systemd service/timer")
238
+ parser.add_argument("--units-dir", help="directory containing generated .service and .timer units")
239
+ parser.add_argument("--timer", help="systemd timer unit name")
240
+ parser.add_argument("--validate-request", action="store_true", help="validate a scheduler request contract")
241
+ parser.add_argument("--method", default="POST")
242
+ parser.add_argument("--origin")
243
+ parser.add_argument("--has-auth", action="store_true")
209
244
  args = parser.parse_args()
210
245
  try:
211
246
  if args.vps_plan:
@@ -226,6 +261,14 @@ def main() -> int:
226
261
  output.write_text(json.dumps(plan, indent=2) + "\n", encoding="utf-8")
227
262
  print(json.dumps(plan, indent=2, ensure_ascii=False))
228
263
  return 0
264
+ if args.validate_request:
265
+ result = validate_scheduler_request(args.method, args.origin, args.has_auth)
266
+ print(json.dumps(result, indent=2, ensure_ascii=False))
267
+ return 0 if result["passed"] else 1
268
+ if args.verify_infra:
269
+ result = verify_infrastructure(Path(args.units_dir).resolve() if args.units_dir else None, args.service, args.timer)
270
+ print(json.dumps(result, indent=2, ensure_ascii=False))
271
+ return 0 if result["passed"] else 1
229
272
  result = preflight(Path(args.project).resolve(), args.target, args.environment)
230
273
  if args.output:
231
274
  Path(args.output).parent.mkdir(parents=True, exist_ok=True)
@@ -34,6 +34,21 @@ def project_fingerprint(project: Path) -> str:
34
34
  return hashlib.sha256(str(project.resolve()).encode()).hexdigest()[:16]
35
35
 
36
36
 
37
+ def installed_version(project: Path) -> str:
38
+ """Resolve the version from the runtime wrapper or project install state."""
39
+ if os.environ.get("MAGGIE_VERSION"):
40
+ return safe_text(os.environ["MAGGIE_VERSION"])
41
+ for path in (project / ".maggie" / "install.json", project / "VERSION"):
42
+ try:
43
+ value = json.loads(path.read_text(encoding="utf-8")) if path.suffix == ".json" else path.read_text(encoding="utf-8").strip()
44
+ if isinstance(value, dict): value = value.get("version", "")
45
+ if re.fullmatch(r"\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?", str(value)):
46
+ return str(value)
47
+ except (OSError, ValueError, json.JSONDecodeError):
48
+ continue
49
+ return "unknown"
50
+
51
+
37
52
  def feedback_id(project: Path) -> str:
38
53
  timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
39
54
  return f"fb-{timestamp}-{project_fingerprint(project)[:6]}-{secrets.token_hex(3)}"
@@ -68,7 +83,7 @@ def collect(args: argparse.Namespace) -> int:
68
83
  "feedbackId": feedback_id(project),
69
84
  "createdAt": now(),
70
85
  "source": "maggie-cli",
71
- "maggieVersion": os.environ.get("MAGGIE_VERSION", "unknown"),
86
+ "maggieVersion": installed_version(project),
72
87
  "type": args.type,
73
88
  "skill": skill,
74
89
  "runId": run_id,