@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.
- package/README.md +22 -7
- package/bin/maggie.js +6 -6
- package/bundled-references/maggiedash-dashboard-ui.md +28 -0
- package/bundled-skills/maggie-blog/SKILL.md +12 -0
- package/bundled-skills/maggie-content-localization/SKILL.md +9 -0
- package/bundled-skills/maggie-dash/SKILL.md +21 -0
- package/bundled-skills/maggie-deployment/SKILL.md +17 -0
- package/bundled-skills/maggie-ops/SKILL.md +27 -0
- package/bundled-skills/maggie-seo-geo/SKILL.md +21 -1
- package/bundled-skills/maggie-service-booking/SKILL.md +6 -0
- package/bundled-templates/maggiedash/README.md +4 -0
- package/bundled-templates/maggiedash/dashboard-ui-contract.json +31 -0
- package/bundled-tools/clis/maggie_analytics.py +14 -1
- package/bundled-tools/clis/maggie_blog.py +6 -0
- package/bundled-tools/clis/maggie_dash.py +54 -0
- package/bundled-tools/clis/maggie_deployment.py +43 -0
- package/bundled-tools/clis/maggie_feedback.py +16 -1
- package/bundled-tools/clis/maggie_ops.py +21 -1
- package/bundled-tools/clis/maggie_service_booking.py +6 -3
- package/bundled-tools/clis/maggie_sitemap.py +16 -2
- package/bundled-tools/runtime/analytics_traffic.py +30 -0
- package/bundled-tools/runtime/content_localization.py +63 -1
- package/bundled-tools/runtime/dependency_lock.py +43 -0
- package/bundled-tools/runtime/integration_state.py +17 -0
- package/bundled-tools/runtime/maggie_dash_store.py +150 -6
- package/bundled-tools/runtime/maggie_dash_ui.py +60 -0
- package/bundled-tools/runtime/maggie_sitemap.py +50 -4
- package/bundled-tools/runtime/route_imports.py +51 -0
- package/bundled-tools/runtime/seed_evidence.py +25 -0
- package/package.json +1 -1
- 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,
|
|
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.
|
|
194
|
-
npx @topy-ai/maggie@0.7.
|
|
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
|
|
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
|
-
|
|
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":
|
|
86
|
+
"maggieVersion": installed_version(project),
|
|
72
87
|
"type": args.type,
|
|
73
88
|
"skill": skill,
|
|
74
89
|
"runId": run_id,
|