@topy-ai/maggie 0.7.36 → 0.7.38
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-zh-TW.md +52 -2
- package/README.md +110 -3
- package/bin/maggie.js +2 -0
- package/bundled-contracts/maggiedash/README.md +4 -1
- package/bundled-contracts/maggiedash/booking-access-v1.json +29 -0
- package/bundled-contracts/maggiedash/booking-customer-surface-v1.json +46 -0
- package/bundled-contracts/maggiedash/booking-email-templates-v1.json +38 -0
- package/bundled-contracts/maggiedash/booking-host-adapter-v1.json +78 -0
- package/bundled-contracts/maggiedash/booking-ops-evidence-v1.json +19 -0
- package/bundled-contracts/maggiedash/execution-board.json +1887 -0
- package/bundled-contracts/maggiedash/stripe-booking-capabilities-v1.json +68 -0
- package/bundled-skills/README.md +1 -0
- package/bundled-skills/catalog.json +4 -0
- package/bundled-skills/maggie-booking/SKILL.md +464 -0
- package/bundled-tools/clis/maggie.py +10 -0
- package/bundled-tools/clis/maggie_booking.py +1145 -0
- package/bundled-tools/clis/maggie_dash.py +187 -5
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
import argparse
|
|
13
13
|
import json
|
|
14
14
|
import os
|
|
15
|
+
import re
|
|
15
16
|
import shutil
|
|
16
17
|
import subprocess
|
|
17
18
|
import sys
|
|
@@ -145,9 +146,137 @@ def manifest_root_file_pairs(source: Path, manifest: dict[str, object], root: Pa
|
|
|
145
146
|
return pairs
|
|
146
147
|
|
|
147
148
|
|
|
148
|
-
def
|
|
149
|
+
def manifest_workspace_file_pairs(source: Path, manifest: dict[str, object], root: Path) -> list[tuple[Path, Path, Path]]:
|
|
150
|
+
"""Return additional installable workspaces such as ./_maggie/booking."""
|
|
151
|
+
pairs: list[tuple[Path, Path, Path]] = []
|
|
152
|
+
for item in manifest.get("workspaces", []):
|
|
153
|
+
if not isinstance(item, dict):
|
|
154
|
+
raise RuntimeError("MaggieDash workspaces entries must be objects")
|
|
155
|
+
source_relative = Path(str(item.get("source", "")))
|
|
156
|
+
target_relative = Path(str(item.get("target", "")))
|
|
157
|
+
if not source_relative.parts or not target_relative.parts:
|
|
158
|
+
raise RuntimeError("MaggieDash workspaces entries require source and target")
|
|
159
|
+
if source_relative.is_absolute() or target_relative.is_absolute() or ".." in source_relative.parts or ".." in target_relative.parts:
|
|
160
|
+
raise RuntimeError("unsafe MaggieDash workspace path")
|
|
161
|
+
source_item = source / source_relative
|
|
162
|
+
if not source_item.is_dir():
|
|
163
|
+
raise RuntimeError(f"MaggieDash workspace source is missing: {source_relative}")
|
|
164
|
+
for path in sorted(source_item.rglob("*")):
|
|
165
|
+
if path.is_file():
|
|
166
|
+
child = path.relative_to(source_item)
|
|
167
|
+
pairs.append((path, root / target_relative / child, source_relative / child))
|
|
168
|
+
return pairs
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def detect_host_framework(root: Path, requested: str) -> str | None:
|
|
172
|
+
"""Resolve the optional host scaffold without guessing unsupported apps."""
|
|
173
|
+
if requested == "none":
|
|
174
|
+
return None
|
|
175
|
+
if requested != "auto":
|
|
176
|
+
return requested
|
|
177
|
+
package = root / "package.json"
|
|
178
|
+
try:
|
|
179
|
+
value = json.loads(package.read_text(encoding="utf-8"))
|
|
180
|
+
except (OSError, json.JSONDecodeError):
|
|
181
|
+
value = {}
|
|
182
|
+
dependencies = set((value.get("dependencies") or {})) | set((value.get("devDependencies") or {}))
|
|
183
|
+
if "astro" in dependencies or any(root.glob("src/**/*.astro")):
|
|
184
|
+
return "astro"
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
ASTRO_MIDDLEWARE_MARKER = "maggie-auto-rewrite-v1"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def merge_astro_middleware(path: Path) -> dict[str, object]:
|
|
192
|
+
"""Add the stable Maggie paths to an existing conventional Astro middleware.
|
|
193
|
+
|
|
194
|
+
The installer must preserve host-owned auth/redirect logic. This narrow
|
|
195
|
+
merge only handles the common `onRequest = defineMiddleware((context,
|
|
196
|
+
next) => { ... })` shape and is idempotent. Unknown middleware shapes are
|
|
197
|
+
reported to the caller instead of being rewritten heuristically.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
content = path.read_text(encoding="utf-8")
|
|
201
|
+
except OSError as error:
|
|
202
|
+
return {"status": "manual", "reason": f"could not read {path}: {error}"}
|
|
203
|
+
if ASTRO_MIDDLEWARE_MARKER in content:
|
|
204
|
+
return {"status": "unchanged", "reason": "Maggie rewrite block already present"}
|
|
205
|
+
required_patterns = ("/_maggie/booking", "/_maggie/login", "/_maggie/register", "/_maggie/reset-password")
|
|
206
|
+
if all(pattern in content for pattern in required_patterns) and "context.rewrite" in content:
|
|
207
|
+
return {"status": "unchanged", "reason": "existing middleware already exposes Maggie paths"}
|
|
208
|
+
match = re.search(
|
|
209
|
+
r"export\s+const\s+onRequest\s*=\s*defineMiddleware\(\s*\(\s*context\s*,\s*next\s*\)\s*=>\s*\{",
|
|
210
|
+
content,
|
|
211
|
+
)
|
|
212
|
+
if not match:
|
|
213
|
+
return {"status": "manual", "reason": "middleware is not the supported defineMiddleware((context, next) => {}) shape"}
|
|
214
|
+
block = r'''
|
|
215
|
+
// maggie-auto-rewrite-v1: generated by `maggie booking install`; keep host auth below.
|
|
216
|
+
const maggieRewrites: Array<[RegExp, (path: string) => string]> = [
|
|
217
|
+
[/^\/_maggie\/login\/?$/, () => "/maggie/login"],
|
|
218
|
+
[/^\/_maggie\/register\/?$/, () => "/maggie/register"],
|
|
219
|
+
[/^\/_maggie\/reset-password\/?$/, () => "/maggie/reset-password"],
|
|
220
|
+
[/^\/_maggie\/booking\/book\/?$/, () => "/maggie/booking/book"],
|
|
221
|
+
[/^\/_maggie\/booking(\/.*)?$/, (path) => "/maggie/booking" + path.replace(/^\/_maggie\/booking/, "")],
|
|
222
|
+
];
|
|
223
|
+
for (const [pattern, target] of maggieRewrites) {
|
|
224
|
+
if (pattern.test(context.url.pathname)) {
|
|
225
|
+
const destination = new URL(target(context.url.pathname), context.url);
|
|
226
|
+
destination.search = context.url.search;
|
|
227
|
+
return context.rewrite(destination.pathname + destination.search);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
'''
|
|
231
|
+
merged = content[:match.end()] + block + content[match.end():]
|
|
232
|
+
try:
|
|
233
|
+
path.write_text(merged, encoding="utf-8")
|
|
234
|
+
except OSError as error:
|
|
235
|
+
return {"status": "manual", "reason": f"could not update {path}: {error}"}
|
|
236
|
+
return {"status": "updated", "reason": "inserted idempotent Maggie rewrite block"}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def manifest_host_file_pairs(source: Path, manifest: dict[str, object], root: Path, framework: str | None) -> list[tuple[Path, Path, Path]]:
|
|
240
|
+
"""Return non-destructive host files for a declared framework adapter."""
|
|
241
|
+
if not framework:
|
|
242
|
+
return []
|
|
243
|
+
bootstrap = manifest.get("hostBootstrap", {})
|
|
244
|
+
if not isinstance(bootstrap, dict):
|
|
245
|
+
return []
|
|
246
|
+
spec = bootstrap.get(framework)
|
|
247
|
+
if not isinstance(spec, dict):
|
|
248
|
+
raise RuntimeError(
|
|
249
|
+
f"MaggieDash distribution at {source} does not provide a {framework} host bootstrap; "
|
|
250
|
+
f"release a source with manifest.hostBootstrap.{framework} or pass --source to a released checkout"
|
|
251
|
+
)
|
|
252
|
+
source_relative = Path(str(spec.get("source", "")))
|
|
253
|
+
target_relative = Path(str(spec.get("target", ".")))
|
|
254
|
+
if not source_relative.parts or source_relative.is_absolute() or ".." in source_relative.parts:
|
|
255
|
+
raise RuntimeError("unsafe MaggieDash host bootstrap source")
|
|
256
|
+
if target_relative.as_posix() not in {".", ""} and (target_relative.is_absolute() or ".." in target_relative.parts):
|
|
257
|
+
raise RuntimeError("unsafe MaggieDash host bootstrap target")
|
|
258
|
+
source_item = source / source_relative
|
|
259
|
+
if not source_item.is_dir():
|
|
260
|
+
raise RuntimeError(f"MaggieDash host bootstrap source is missing: {source_relative}")
|
|
261
|
+
pairs: list[tuple[Path, Path, Path]] = []
|
|
262
|
+
for path in sorted(source_item.rglob("*")):
|
|
263
|
+
if path.is_file():
|
|
264
|
+
child = path.relative_to(source_item)
|
|
265
|
+
# A deployment config is host-owned. The default Node config is
|
|
266
|
+
# useful for a blank Astro project, but adding a second config to
|
|
267
|
+
# a host that already has astro.config.ts/cloudflare/etc. can make
|
|
268
|
+
# Astro ambiguous or change its deployment target.
|
|
269
|
+
if framework == "astro" and child.name == "astro.config.mjs" and any(root.glob("astro.config.*")):
|
|
270
|
+
continue
|
|
271
|
+
pairs.append((path, root / target_relative / child, source_relative / child))
|
|
272
|
+
return pairs
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def command_install_diff(source: Path, manifest: dict[str, object], target: Path, compare_dir: Path | None, force: bool, host_pairs: list[tuple[Path, Path, Path]] | None = None, host_framework: str | None = None) -> dict[str, object]:
|
|
149
276
|
pairs = manifest_file_pairs(source, manifest, target)
|
|
150
277
|
root_pairs = manifest_root_file_pairs(source, manifest, target.parents[1])
|
|
278
|
+
workspace_pairs = manifest_workspace_file_pairs(source, manifest, target.parents[1])
|
|
279
|
+
host_pairs = host_pairs or []
|
|
151
280
|
files: list[dict[str, str]] = []
|
|
152
281
|
source_relative = {relative for _, _, relative in pairs}
|
|
153
282
|
for source_file, destination, relative in pairs:
|
|
@@ -172,6 +301,24 @@ def command_install_diff(source: Path, manifest: dict[str, object], target: Path
|
|
|
172
301
|
else:
|
|
173
302
|
action = "update" if force else "preserve"
|
|
174
303
|
files.append({"path": str(relative), "action": action, "comparePath": str(compare_file)})
|
|
304
|
+
for source_file, destination, relative in workspace_pairs:
|
|
305
|
+
compare_file = destination
|
|
306
|
+
if not compare_file.exists():
|
|
307
|
+
action = "add"
|
|
308
|
+
elif compare_file.read_bytes() == source_file.read_bytes():
|
|
309
|
+
action = "unchanged"
|
|
310
|
+
else:
|
|
311
|
+
action = "update" if force else "preserve"
|
|
312
|
+
files.append({"path": str(relative), "action": action, "comparePath": str(compare_file)})
|
|
313
|
+
for source_file, destination, relative in host_pairs:
|
|
314
|
+
compare_file = destination
|
|
315
|
+
if not compare_file.exists():
|
|
316
|
+
action = "add"
|
|
317
|
+
elif compare_file.read_bytes() == source_file.read_bytes():
|
|
318
|
+
action = "unchanged"
|
|
319
|
+
else:
|
|
320
|
+
action = "update" if force else "preserve"
|
|
321
|
+
files.append({"path": f"host/{host_framework}/{relative}", "action": action, "comparePath": str(compare_file)})
|
|
175
322
|
if compare_dir and compare_dir.exists():
|
|
176
323
|
for existing in sorted(compare_dir.rglob("*")):
|
|
177
324
|
if not existing.is_file():
|
|
@@ -183,13 +330,14 @@ def command_install_diff(source: Path, manifest: dict[str, object], target: Path
|
|
|
183
330
|
summary: dict[str, int] = {}
|
|
184
331
|
for item in files:
|
|
185
332
|
summary[item["action"]] = summary.get(item["action"], 0) + 1
|
|
186
|
-
return {"status": "dry-run", "version": manifest.get("version"), "target": str(target), "compareDir": str(compare_dir) if compare_dir else None, "summary": summary, "files": files}
|
|
333
|
+
return {"status": "dry-run", "version": manifest.get("version"), "target": str(target), "hostFramework": host_framework, "compareDir": str(compare_dir) if compare_dir else None, "summary": summary, "files": files}
|
|
187
334
|
|
|
188
335
|
|
|
189
336
|
def command_install(args: argparse.Namespace) -> int:
|
|
190
337
|
if not args.dry_run and not args.diff:
|
|
191
338
|
require_confirm(args)
|
|
192
339
|
root = project_root(args)
|
|
340
|
+
host_framework = detect_host_framework(root, args.host)
|
|
193
341
|
source_value = args.source or os.environ.get("MAGGIE_DASH_SOURCE") or DEFAULT_DASH_SOURCE
|
|
194
342
|
ref = args.ref
|
|
195
343
|
target_relative = Path(args.target)
|
|
@@ -222,6 +370,7 @@ def command_install(args: argparse.Namespace) -> int:
|
|
|
222
370
|
raise RuntimeError("unsupported MaggieDash distribution schema")
|
|
223
371
|
if manifest.get("target") != "_maggie/admin" and args.target == "_maggie/admin":
|
|
224
372
|
raise RuntimeError("MaggieDash manifest target is not ./_maggie/admin")
|
|
373
|
+
host_pairs = manifest_host_file_pairs(source, manifest, root, host_framework)
|
|
225
374
|
compare_dir = None
|
|
226
375
|
if args.existing_dir:
|
|
227
376
|
compare_dir = Path(args.existing_dir).expanduser()
|
|
@@ -229,7 +378,7 @@ def command_install(args: argparse.Namespace) -> int:
|
|
|
229
378
|
compare_dir = root / compare_dir
|
|
230
379
|
compare_dir = compare_dir.resolve()
|
|
231
380
|
if args.dry_run or args.diff:
|
|
232
|
-
emit(command_install_diff(source, manifest, target, compare_dir, args.force))
|
|
381
|
+
emit(command_install_diff(source, manifest, target, compare_dir, args.force, host_pairs, host_framework))
|
|
233
382
|
return 0
|
|
234
383
|
installed: list[str] = []
|
|
235
384
|
preserved: list[str] = []
|
|
@@ -264,6 +413,32 @@ def command_install(args: argparse.Namespace) -> int:
|
|
|
264
413
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
265
414
|
shutil.copy2(source_file, destination)
|
|
266
415
|
installed.append(str(destination))
|
|
416
|
+
for source_file, destination, _ in manifest_workspace_file_pairs(source, manifest, root):
|
|
417
|
+
if destination.exists() and not args.force:
|
|
418
|
+
preserved.append(str(destination))
|
|
419
|
+
else:
|
|
420
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
421
|
+
shutil.copy2(source_file, destination)
|
|
422
|
+
installed.append(str(destination))
|
|
423
|
+
host_installed: list[str] = []
|
|
424
|
+
host_preserved: list[str] = []
|
|
425
|
+
host_updated: list[str] = []
|
|
426
|
+
host_warnings: list[str] = []
|
|
427
|
+
for source_file, destination, _ in host_pairs:
|
|
428
|
+
if destination.exists() and not args.force:
|
|
429
|
+
host_preserved.append(str(destination))
|
|
430
|
+
else:
|
|
431
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
432
|
+
shutil.copy2(source_file, destination)
|
|
433
|
+
host_installed.append(str(destination))
|
|
434
|
+
if host_framework == "astro":
|
|
435
|
+
middleware = root / "src" / "middleware.ts"
|
|
436
|
+
if middleware.exists():
|
|
437
|
+
merge_result = merge_astro_middleware(middleware)
|
|
438
|
+
if merge_result.get("status") == "updated":
|
|
439
|
+
host_updated.append(str(middleware))
|
|
440
|
+
elif merge_result.get("status") == "manual":
|
|
441
|
+
host_warnings.append(str(merge_result.get("reason")))
|
|
267
442
|
state = {
|
|
268
443
|
"schemaVersion": "maggiedash-install.v1",
|
|
269
444
|
"status": "installed",
|
|
@@ -274,13 +449,19 @@ def command_install(args: argparse.Namespace) -> int:
|
|
|
274
449
|
"target": str(target),
|
|
275
450
|
"installed": sorted(set(installed)),
|
|
276
451
|
"preserved": sorted(set(preserved)),
|
|
452
|
+
"hostFramework": host_framework,
|
|
453
|
+
"hostBootstrap": "installed" if host_framework else "skipped",
|
|
454
|
+
"hostInstalled": sorted(set(host_installed)),
|
|
455
|
+
"hostPreserved": sorted(set(host_preserved)),
|
|
456
|
+
"hostUpdated": sorted(set(host_updated)),
|
|
457
|
+
"hostWarnings": sorted(set(host_warnings)),
|
|
277
458
|
"updatedAt": utc_now(),
|
|
278
459
|
}
|
|
279
460
|
state_path = root / ".maggie" / "dash-install.json"
|
|
280
461
|
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
281
462
|
state_path.write_text(json.dumps(state, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
282
|
-
emit({"status": "installed", "version": manifest.get("version"), "target": str(target), "installed": len(set(installed)), "preserved": len(set(preserved)), "state": str(state_path)})
|
|
283
|
-
return 0
|
|
463
|
+
emit({"status": "needs-attention" if host_warnings else "installed", "version": manifest.get("version"), "target": str(target), "hostFramework": host_framework, "hostInstalled": len(set(host_installed)), "hostPreserved": len(set(host_preserved)), "hostUpdated": len(set(host_updated)), "hostWarnings": host_warnings, "installed": len(set(installed)), "preserved": len(set(preserved)), "state": str(state_path)})
|
|
464
|
+
return 1 if host_warnings else 0
|
|
284
465
|
finally:
|
|
285
466
|
if temporary is not None:
|
|
286
467
|
temporary.cleanup()
|
|
@@ -630,6 +811,7 @@ def parser() -> argparse.ArgumentParser:
|
|
|
630
811
|
install.add_argument("--source", help="local checkout or Git URL; defaults to MaggieDash repository")
|
|
631
812
|
install.add_argument("--ref", default="main", help="Git ref when installing from a remote source")
|
|
632
813
|
install.add_argument("--target", default="_maggie/admin", help="installation target relative to the project")
|
|
814
|
+
install.add_argument("--host", choices=["auto", "astro", "none"], default="auto", help="install the matching non-destructive host scaffold (default: auto-detect Astro)")
|
|
633
815
|
install.add_argument("--force", action="store_true", help="replace existing dashboard files")
|
|
634
816
|
install.add_argument("--dry-run", action="store_true", help="show the install plan without writing files")
|
|
635
817
|
install.add_argument("--diff", action="store_true", help="show the install plan and compare with an existing workspace")
|