@topy-ai/maggie 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -66,8 +66,9 @@ Maggie in this order:
|
|
|
66
66
|
```bash
|
|
67
67
|
python3 tools/clis/maggie.py analyze . --json --save
|
|
68
68
|
python3 tools/clis/maggie.py bootstrap interview .
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
maggie dash install --project . --confirm
|
|
70
|
+
maggie dash init --project . --confirm
|
|
71
|
+
maggie dash migrate --project . --confirm
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
The normal daily loop is:
|
|
@@ -88,7 +89,7 @@ The CLI provides the installer plus durable workflow commands:
|
|
|
88
89
|
maggie init | install | update | remove | list | doctor
|
|
89
90
|
maggie cleanup --project . [--confirm]
|
|
90
91
|
maggie bootstrap interview | phase ...
|
|
91
|
-
maggie dash init | status | migrate | cms ...
|
|
92
|
+
maggie dash install | init | status | migrate | cms ...
|
|
92
93
|
maggie dash transition ... # explicit content approval transition
|
|
93
94
|
maggie dash variant ... # service variant create/review/preview/publish
|
|
94
95
|
maggie clone ... # authorized homepage capture
|
|
@@ -190,8 +191,8 @@ artifact schemas.
|
|
|
190
191
|
Recommended upgrade sequence for the current release:
|
|
191
192
|
|
|
192
193
|
```bash
|
|
193
|
-
npx @topy-ai/maggie@0.7.
|
|
194
|
-
npx @topy-ai/maggie@0.7.
|
|
194
|
+
npx @topy-ai/maggie@0.7.2 update --project . --force
|
|
195
|
+
npx @topy-ai/maggie@0.7.2 cleanup --project .
|
|
195
196
|
```
|
|
196
197
|
|
|
197
198
|
Maintainers should pass npm credentials through the repository helper, never
|
|
@@ -201,7 +202,8 @@ as a command-line argument:
|
|
|
201
202
|
node scripts/publish-npm.mjs --maggie-env-file ../.env
|
|
202
203
|
```
|
|
203
204
|
|
|
204
|
-
The 0.7.
|
|
205
|
+
The 0.7.2 workflow adds the installable MaggieDash admin distribution and
|
|
206
|
+
audited CMS operations (`cms revisions`,
|
|
205
207
|
`trash`, `restore`, `schedule`, `duplicate`, `redirect`, and signed
|
|
206
208
|
`preview`), import-authoritative service matching, shared translation indexes,
|
|
207
209
|
sanitized seed manifests, lockfile/analytics traffic checks, semantic sitemap
|
|
@@ -215,11 +217,20 @@ For a new project, establish the local content and approval foundation before
|
|
|
215
217
|
connecting providers or publishing:
|
|
216
218
|
|
|
217
219
|
```bash
|
|
220
|
+
maggie dash install --project . --confirm
|
|
218
221
|
maggie dash init --project . --confirm
|
|
219
222
|
maggie dash status --project .
|
|
220
223
|
maggie doctor --project .
|
|
221
224
|
```
|
|
222
225
|
|
|
226
|
+
`maggie dash install` installs the first-party private MaggieDash dashboard
|
|
227
|
+
source into `./_maggie/admin`, preserving local files unless `--force` is used.
|
|
228
|
+
It records the resolved source revision in `.maggie/dash-install.json`. During
|
|
229
|
+
bootstrap this is enabled by default; use `--dash-source /path/to/MaggieDash`
|
|
230
|
+
for local development or a configured private Git URL. The host project keeps
|
|
231
|
+
ownership of routes, email/password auth, database, provider credentials, and
|
|
232
|
+
the `/api/maggie/*` adapter.
|
|
233
|
+
|
|
223
234
|
Content remains draft-first and external writes remain explicit. Use the
|
|
224
235
|
project-local memory workflow for confirmed preferences and reusable lessons;
|
|
225
236
|
feedback drafts are never promoted to active memory automatically.
|
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|cms --project PATH [options]
|
|
68
|
+
maggie dash init|install|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
|
|
@@ -5,8 +5,30 @@ description: Manage the MaggieDash project foundation, local content store, and
|
|
|
5
5
|
|
|
6
6
|
# MaggieDash
|
|
7
7
|
|
|
8
|
-
Use the provider-neutral MaggieDash contracts and CLI for project setup
|
|
9
|
-
content operations.
|
|
8
|
+
Use the provider-neutral MaggieDash contracts and CLI for project setup,
|
|
9
|
+
installation of the first-party admin workspace, and content operations.
|
|
10
|
+
|
|
11
|
+
## Install the admin workspace
|
|
12
|
+
|
|
13
|
+
During bootstrap, the recommended decision is to install MaggieDash into
|
|
14
|
+
`./_maggie/admin`. The dashboard source lives in the private first-party
|
|
15
|
+
`TOPY-AI-LTD/MaggieDash` repository during the MVP phase; Git/gh credentials
|
|
16
|
+
must already be configured for the project owner.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
maggie dash install --project . --confirm
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The command is idempotent, preserves existing files unless `--force` is
|
|
23
|
+
explicitly supplied, and records the resolved revision in
|
|
24
|
+
`.maggie/dash-install.json`. It never accepts or stores a GitHub token as a
|
|
25
|
+
CLI argument. For local development or a pinned checkout, use
|
|
26
|
+
`--source /path/to/MaggieDash` or `--source URL --ref REF`.
|
|
27
|
+
|
|
28
|
+
The installed dashboard owns the React workspace UI. The host project still
|
|
29
|
+
owns the framework route, email/password session middleware, database, media
|
|
30
|
+
storage, provider credentials, and `/api/maggie/*` adapter endpoints. Read the
|
|
31
|
+
MaggieDash host adapter contract before adding a new framework adapter.
|
|
10
32
|
|
|
11
33
|
## Workflow
|
|
12
34
|
|
|
@@ -11,3 +11,14 @@ marketplace repository.
|
|
|
11
11
|
Dashboard routes should adopt `dashboard-ui-contract.json` and validate their
|
|
12
12
|
actual host components with `maggie dash ui validate`. The contract provides
|
|
13
13
|
the shared workspace chrome and rejects components that declare dead props.
|
|
14
|
+
|
|
15
|
+
The first-party dashboard distribution is installed separately into
|
|
16
|
+
`./_maggie/admin`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
maggie dash install --project . --confirm
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
It records the source revision at `.maggie/dash-install.json`; host routes,
|
|
23
|
+
email/password authentication, and `/api/maggie/*` adapters remain owned by the
|
|
24
|
+
website project.
|
|
@@ -8,6 +8,7 @@ import hashlib
|
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
10
|
import re
|
|
11
|
+
import subprocess
|
|
11
12
|
import sys
|
|
12
13
|
import time
|
|
13
14
|
from datetime import datetime, timezone
|
|
@@ -561,6 +562,11 @@ def command_bootstrap_phase(args: argparse.Namespace) -> int:
|
|
|
561
562
|
if analysis.get("maggiedash", {}).get("status") not in {"Detected", "Planned"}:
|
|
562
563
|
print("GATE_BLOCKED: MaggieDash project contract is unavailable.", file=sys.stderr)
|
|
563
564
|
return 2
|
|
565
|
+
bootstrap = load_state(root) or {}
|
|
566
|
+
decisions = bootstrap.get("decisions", {})
|
|
567
|
+
if decisions.get("maggiedash_admin") and not (root / STATE_DIR / "dash-install.json").exists():
|
|
568
|
+
print("GATE_BLOCKED: MaggieDash admin is enabled but not installed; run `maggie dash install --project . --confirm`.", file=sys.stderr)
|
|
569
|
+
return 2
|
|
564
570
|
phase["status"] = "passed"
|
|
565
571
|
phase["validation"] = args.validation
|
|
566
572
|
phase["validated_at"] = utc_now()
|
|
@@ -589,7 +595,7 @@ BOOTSTRAP_QUESTIONS = [
|
|
|
589
595
|
("content_source", "Where should posts be stored?", ["preserve", "local-content", "database", "api-pull"]),
|
|
590
596
|
("posts_per_page", "How many posts should appear on each blog page?", ["9", "12", "6", "custom"]),
|
|
591
597
|
("grid_columns", "How many columns should the default post grid use?", ["3", "2", "4", "custom"]),
|
|
592
|
-
("
|
|
598
|
+
("maggiedash_admin", "Should bootstrap install the private MaggieDash admin workspace into ./_maggie/admin?", ["enabled", "disabled"]),
|
|
593
599
|
]
|
|
594
600
|
|
|
595
601
|
|
|
@@ -604,7 +610,7 @@ def question_default(key: str, analysis: dict) -> str:
|
|
|
604
610
|
detected = analysis.get(key, {}).get("value") if isinstance(analysis.get(key), dict) else None
|
|
605
611
|
if key == "posts_per_page": return "9"
|
|
606
612
|
if key == "grid_columns": return "3"
|
|
607
|
-
if key == "
|
|
613
|
+
if key == "maggiedash_admin": return "enabled"
|
|
608
614
|
return detected or {"ui_system": "preserve-or-tailwind", "icon_set": "phosphor", "font": "preserve-or-system", "database": "preserve-or-sqlite"}.get(key, "detected")
|
|
609
615
|
|
|
610
616
|
|
|
@@ -641,6 +647,8 @@ def command_bootstrap_interview(args: argparse.Namespace) -> int:
|
|
|
641
647
|
raise RuntimeError(f"invalid answers file: {args.answers_file}") from error
|
|
642
648
|
if not isinstance(loaded, dict): raise RuntimeError("answers file must contain a JSON object")
|
|
643
649
|
answers.update({str(key): str(value) for key, value in loaded.items()})
|
|
650
|
+
if "maggiedash_admin" not in answers and "ops_dashboard" in answers:
|
|
651
|
+
answers["maggiedash_admin"] = answers["ops_dashboard"]
|
|
644
652
|
for key, question, choices in BOOTSTRAP_QUESTIONS:
|
|
645
653
|
value = answers.get(key, question_default(key, analysis))
|
|
646
654
|
if not args.non_interactive and key not in answers:
|
|
@@ -665,8 +673,10 @@ def command_bootstrap_interview(args: argparse.Namespace) -> int:
|
|
|
665
673
|
project=str(root), confirm=["foundation", "experience", "data", "publishing"],
|
|
666
674
|
framework=answers["framework"], language=answers["language"], content_language=answers["content_language"],
|
|
667
675
|
ui_system=answers["ui_system"], icon_set=answers["icon_set"], font=answers["font"], database=answers["database"], content_source=answers["content_source"],
|
|
668
|
-
posts_per_page=int(answers["posts_per_page"]), grid_columns=int(answers["grid_columns"]),
|
|
676
|
+
posts_per_page=int(answers["posts_per_page"]), grid_columns=int(answers["grid_columns"]),
|
|
677
|
+
ops_dashboard=answers["maggiedash_admin"] == "enabled", maggiedash_admin=answers["maggiedash_admin"] == "enabled",
|
|
669
678
|
site_domain=answers["site_domain"], api_key_present=answers["api_key_present"], foundation_source=answers["foundation_source"], maggiedash_path=None,
|
|
679
|
+
dash_source=getattr(args, "dash_source", None),
|
|
670
680
|
)
|
|
671
681
|
return command_complete(complete_args)
|
|
672
682
|
|
|
@@ -685,6 +695,17 @@ def command_complete(args: argparse.Namespace) -> int:
|
|
|
685
695
|
if foundation_analysis.get("maggiedash", {}).get("status") != "Detected":
|
|
686
696
|
print("FOUNDATION_REQUIRED: use-existing-maggiedash was selected but no MaggieDash contract was detected.", file=sys.stderr)
|
|
687
697
|
return 2
|
|
698
|
+
install_admin = bool(getattr(args, "maggiedash_admin", False))
|
|
699
|
+
if install_admin:
|
|
700
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
701
|
+
command = [sys.executable, str(Path(__file__).with_name("maggie_dash.py")), "install", "--project", str(root), "--confirm"]
|
|
702
|
+
dash_source = getattr(args, "dash_source", None)
|
|
703
|
+
if dash_source:
|
|
704
|
+
command.extend(["--source", dash_source])
|
|
705
|
+
result = subprocess.run(command, cwd=root, check=False)
|
|
706
|
+
if result.returncode:
|
|
707
|
+
print("MAGGIEDASH_INSTALL_FAILED: bootstrap was not completed; resolve the private MaggieDash source and retry.", file=sys.stderr)
|
|
708
|
+
return result.returncode
|
|
688
709
|
state = {
|
|
689
710
|
"schema_version": "1.1",
|
|
690
711
|
"status": "completed",
|
|
@@ -707,6 +728,7 @@ def command_complete(args: argparse.Namespace) -> int:
|
|
|
707
728
|
"posts_per_page": args.posts_per_page,
|
|
708
729
|
"grid_columns": args.grid_columns,
|
|
709
730
|
"ops_dashboard": args.ops_dashboard,
|
|
731
|
+
"maggiedash_admin": install_admin,
|
|
710
732
|
},
|
|
711
733
|
}
|
|
712
734
|
path = state_path(root)
|
|
@@ -714,7 +736,7 @@ def command_complete(args: argparse.Namespace) -> int:
|
|
|
714
736
|
path.write_text(json.dumps(state, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
715
737
|
contracts = {
|
|
716
738
|
"project.json": {"schema_version": "1.0", "framework": args.framework, "language": args.language, "database": args.database, "content_source": args.content_source, "site_domain": getattr(args, "site_domain", "none"), "context_source": state["decisions"]["context_source"], "foundation_source": foundation_source},
|
|
717
|
-
"maggiedash.json": {"schema_version": "1.0", "source": foundation_source, "path": getattr(args, "maggiedash_path", None), "scaffold_command": "maggie dash init --project ." if foundation_source == "create-maggiedash" else None, "capabilities": ["content", "admin", "auth", "media", "plugins", "api", "mcp"], "status": "detected" if foundation_source == "use-existing-maggiedash" else "planned" if foundation_source == "create-maggiedash" else "not-selected"},
|
|
739
|
+
"maggiedash.json": {"schema_version": "1.0", "source": foundation_source, "path": getattr(args, "maggiedash_path", None), "scaffold_command": "maggie dash init --project ." if foundation_source == "create-maggiedash" else None, "admin_install": {"enabled": install_admin, "target": "./_maggie/admin", "state": ".maggie/dash-install.json" if install_admin else None}, "capabilities": ["content", "admin", "auth", "media", "plugins", "api", "mcp"], "status": "detected" if foundation_source == "use-existing-maggiedash" else "planned" if foundation_source == "create-maggiedash" else "not-selected"},
|
|
718
740
|
"decisions.json": state["decisions"],
|
|
719
741
|
"schema.json": {"post_required": ["id", "slug", "title", "content", "status", "canonicalUrl", "publishedAt", "updatedAt", "author"], "public_status": "published", "posts_per_page": args.posts_per_page, "grid_columns": args.grid_columns, "grid_default": f"{args.grid_columns}x{args.posts_per_page // args.grid_columns}"},
|
|
720
742
|
"routes.json": {"public": ["/about", "/blog", "/blog/page/[page]", "/blog/[slug]", "/topics", "/topics/[slug]", "/authors/[slug]", "/sitemap.xml", "/robots.txt"], "ops": ["/ops", "/ops/posts", "/ops/posts/new", "/ops/posts/[id]/edit", "/ops/posts/[id]/preview", "/ops/topics", "/ops/sitemap", "/ops/reports", "/ops/settings/site", "/ops/settings/integrations", "/ops/operations", "/ops/wordpress", "/api/ops/summary", "/api/ops/posts", "/api/ops/topics", "/api/ops/bulk", "/api/ops/agency", "/api/ops/entities", "/api/ops/migrations/import", "/api/ops/migrations/[id]/resume", "/api/ops/wordpress/migration-plan", "/api/ops/pull/project-context", "/api/ops/pull/sync", "/api/ops/pull/sync-updates", "/api/ops/sitemap/matching-history", "/api/ops/sitemap/match", "/api/ops/sitemap/auto-detect", "/api/ops/rewrite/queue", "/api/ops/rewrite/history", "/api/ops/rewrite/policy", "/api/ops/content-tracking/report-state", "/api/ops/content-tracking/report-state/batch", "/api/ops/reports", "/api/ops/playbooks/[id]/run"]},
|
|
@@ -903,6 +925,7 @@ def parser() -> argparse.ArgumentParser:
|
|
|
903
925
|
interview.add_argument("--answers-file", help="JSON answers for headless/CI use")
|
|
904
926
|
interview.add_argument("--non-interactive", action="store_true", help="do not prompt; requires --answers-file and --confirm")
|
|
905
927
|
interview.add_argument("--confirm", action="store_true", help="apply the reviewed proposal")
|
|
928
|
+
interview.add_argument("--dash-source", help="local checkout or Git URL for the MaggieDash admin source")
|
|
906
929
|
interview.set_defaults(func=command_bootstrap_interview)
|
|
907
930
|
phase = bootstrap_sub.add_parser("phase", help="start/pass/fail one ordered workflow phase")
|
|
908
931
|
phase.add_argument("action", choices=["status", "start", "pass", "fail"])
|
|
@@ -915,11 +938,13 @@ def parser() -> argparse.ArgumentParser:
|
|
|
915
938
|
complete.add_argument("--api-key-present", choices=["yes", "no"], default="no")
|
|
916
939
|
complete.add_argument("--foundation-source", choices=["use-existing-maggiedash", "create-maggiedash", "preserve-existing-project"], default="preserve-existing-project")
|
|
917
940
|
complete.add_argument("--maggiedash-path", default=None, help="path to an existing MaggieDash project contract")
|
|
941
|
+
complete.add_argument("--dash-source", help="local checkout or Git URL for the MaggieDash admin source")
|
|
918
942
|
for name, default in (("framework", "detected"), ("language", "detected"), ("content-language", "confirmed"), ("ui-system", "preserve-or-tailwind"), ("icon-set", "phosphor"), ("font", "preserve-or-system"), ("database", "preserve-or-sqlite"), ("content-source", "confirmed")):
|
|
919
943
|
complete.add_argument("--" + name, dest=name.replace("-", "_"), default=default)
|
|
920
944
|
complete.add_argument("--posts-per-page", type=int, default=9)
|
|
921
945
|
complete.add_argument("--grid-columns", type=int, default=3)
|
|
922
946
|
complete.add_argument("--ops-dashboard", action=argparse.BooleanOptionalAction, default=False)
|
|
947
|
+
complete.add_argument("--maggiedash-admin", action=argparse.BooleanOptionalAction, default=False)
|
|
923
948
|
complete.set_defaults(func=command_complete)
|
|
924
949
|
generate = sub.add_parser("generate", help="generate stable contract or fixture files")
|
|
925
950
|
generate.add_argument("target", choices=["contract", "fixture", "post", "topic", "author", "migration", "seo", "ops-page"])
|
|
@@ -11,7 +11,12 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
13
|
import json
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
14
17
|
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from datetime import datetime, timezone
|
|
15
20
|
from pathlib import Path
|
|
16
21
|
|
|
17
22
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
|
|
@@ -38,6 +43,118 @@ def require_confirm(args: argparse.Namespace) -> None:
|
|
|
38
43
|
raise SystemExit(2)
|
|
39
44
|
|
|
40
45
|
|
|
46
|
+
DEFAULT_DASH_SOURCE = "https://github.com/TOPY-AI-LTD/MaggieDash.git"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def utc_now() -> str:
|
|
50
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def git_revision(path: Path) -> str | None:
|
|
54
|
+
try:
|
|
55
|
+
result = subprocess.run(["git", "-C", str(path), "rev-parse", "HEAD"], capture_output=True, text=True, check=True)
|
|
56
|
+
except (OSError, subprocess.CalledProcessError):
|
|
57
|
+
return None
|
|
58
|
+
return result.stdout.strip() or None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def copy_dashboard_tree(source: Path, target: Path, force: bool) -> tuple[list[str], list[str]]:
|
|
62
|
+
installed: list[str] = []
|
|
63
|
+
preserved: list[str] = []
|
|
64
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
for path in sorted(source.rglob("*")):
|
|
66
|
+
relative = path.relative_to(source)
|
|
67
|
+
destination = target / relative
|
|
68
|
+
if path.is_dir():
|
|
69
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
continue
|
|
71
|
+
if destination.exists() and not force:
|
|
72
|
+
preserved.append(str(destination))
|
|
73
|
+
continue
|
|
74
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
shutil.copy2(path, destination)
|
|
76
|
+
installed.append(str(destination))
|
|
77
|
+
return installed, preserved
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def command_install(args: argparse.Namespace) -> int:
|
|
81
|
+
require_confirm(args)
|
|
82
|
+
root = project_root(args)
|
|
83
|
+
source_value = args.source or os.environ.get("MAGGIE_DASH_SOURCE") or DEFAULT_DASH_SOURCE
|
|
84
|
+
ref = args.ref
|
|
85
|
+
target = root / args.target
|
|
86
|
+
temporary: tempfile.TemporaryDirectory[str] | None = None
|
|
87
|
+
source = Path(source_value).expanduser().resolve() if not source_value.startswith(("https://", "http://", "git@")) else None
|
|
88
|
+
try:
|
|
89
|
+
if source is None:
|
|
90
|
+
temporary = tempfile.TemporaryDirectory(prefix="maggiedash-install-")
|
|
91
|
+
clone = ["git", "clone", "--depth", "1"]
|
|
92
|
+
if ref:
|
|
93
|
+
clone.extend(["--branch", ref])
|
|
94
|
+
clone.extend([source_value, temporary.name])
|
|
95
|
+
result = subprocess.run(clone, capture_output=True, text=True)
|
|
96
|
+
if result.returncode:
|
|
97
|
+
detail = (result.stderr or result.stdout).strip().splitlines()[-1:]
|
|
98
|
+
raise RuntimeError("MaggieDash source could not be cloned" + (f": {detail[0]}" if detail else ""))
|
|
99
|
+
source = Path(temporary.name)
|
|
100
|
+
if not source.is_dir():
|
|
101
|
+
raise RuntimeError(f"MaggieDash source directory not found: {source}")
|
|
102
|
+
manifest_path = source / "maggiedash.json"
|
|
103
|
+
if not manifest_path.exists():
|
|
104
|
+
raise RuntimeError("MaggieDash source has no maggiedash.json manifest")
|
|
105
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
106
|
+
if manifest.get("schemaVersion") != "maggiedash-distribution.v1":
|
|
107
|
+
raise RuntimeError("unsupported MaggieDash distribution schema")
|
|
108
|
+
if manifest.get("target") != "_maggie/admin" and args.target == "_maggie/admin":
|
|
109
|
+
raise RuntimeError("MaggieDash manifest target is not ./_maggie/admin")
|
|
110
|
+
installed: list[str] = []
|
|
111
|
+
preserved: list[str] = []
|
|
112
|
+
for item in manifest.get("files", []):
|
|
113
|
+
relative = Path(item)
|
|
114
|
+
if relative.is_absolute() or ".." in relative.parts:
|
|
115
|
+
raise RuntimeError(f"unsafe MaggieDash manifest path: {item}")
|
|
116
|
+
source_item = source / relative
|
|
117
|
+
if not source_item.exists():
|
|
118
|
+
raise RuntimeError(f"MaggieDash manifest file is missing: {item}")
|
|
119
|
+
if relative.parts and relative.parts[0] == "admin":
|
|
120
|
+
destination = target / Path(*relative.parts[1:])
|
|
121
|
+
else:
|
|
122
|
+
destination = target / relative
|
|
123
|
+
if source_item.is_dir():
|
|
124
|
+
current_installed, current_preserved = copy_dashboard_tree(source_item, destination, args.force)
|
|
125
|
+
else:
|
|
126
|
+
current_installed = []
|
|
127
|
+
current_preserved = []
|
|
128
|
+
if destination.exists() and not args.force:
|
|
129
|
+
current_preserved = [str(destination)]
|
|
130
|
+
else:
|
|
131
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
shutil.copy2(source_item, destination)
|
|
133
|
+
current_installed = [str(destination)]
|
|
134
|
+
installed.extend(current_installed)
|
|
135
|
+
preserved.extend(current_preserved)
|
|
136
|
+
state = {
|
|
137
|
+
"schemaVersion": "maggiedash-install.v1",
|
|
138
|
+
"status": "installed",
|
|
139
|
+
"source": source_value,
|
|
140
|
+
"ref": ref,
|
|
141
|
+
"revision": git_revision(source),
|
|
142
|
+
"version": manifest.get("version"),
|
|
143
|
+
"target": str(target),
|
|
144
|
+
"installed": sorted(set(installed)),
|
|
145
|
+
"preserved": sorted(set(preserved)),
|
|
146
|
+
"updatedAt": utc_now(),
|
|
147
|
+
}
|
|
148
|
+
state_path = root / ".maggie" / "dash-install.json"
|
|
149
|
+
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
state_path.write_text(json.dumps(state, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
151
|
+
emit({"status": "installed", "version": manifest.get("version"), "target": str(target), "installed": len(set(installed)), "preserved": len(set(preserved)), "state": str(state_path)})
|
|
152
|
+
return 0
|
|
153
|
+
finally:
|
|
154
|
+
if temporary is not None:
|
|
155
|
+
temporary.cleanup()
|
|
156
|
+
|
|
157
|
+
|
|
41
158
|
def command_init(args: argparse.Namespace) -> int:
|
|
42
159
|
require_confirm(args)
|
|
43
160
|
root = project_root(args)
|
|
@@ -171,6 +288,14 @@ def parser() -> argparse.ArgumentParser:
|
|
|
171
288
|
init.add_argument("--locale", default="en-GB")
|
|
172
289
|
init.add_argument("--confirm", action="store_true")
|
|
173
290
|
init.set_defaults(func=command_init)
|
|
291
|
+
install = sub.add_parser("install", help="install the MaggieDash admin workspace")
|
|
292
|
+
install.add_argument("--project", default=".")
|
|
293
|
+
install.add_argument("--source", help="local checkout or Git URL; defaults to MaggieDash repository")
|
|
294
|
+
install.add_argument("--ref", default="main", help="Git ref when installing from a remote source")
|
|
295
|
+
install.add_argument("--target", default="_maggie/admin", help="installation target relative to the project")
|
|
296
|
+
install.add_argument("--force", action="store_true", help="replace existing dashboard files")
|
|
297
|
+
install.add_argument("--confirm", action="store_true")
|
|
298
|
+
install.set_defaults(func=command_install)
|
|
174
299
|
status = sub.add_parser("status", help="inspect project and documents")
|
|
175
300
|
status.add_argument("--project", default=".")
|
|
176
301
|
status.add_argument("--project-id", default="local-project")
|