loki-mode 6.75.2 → 6.76.0
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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +672 -0
- package/autonomy/run.sh +1 -1
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/mcp/magic_tools.py +471 -0
- package/mcp/server.py +13 -0
- package/package.json +1 -1
- package/references/magic-modules-patterns.md +634 -0
- package/skills/00-index.md +1 -0
- package/skills/magic-modules.md +205 -0
- package/web-app/dist/assets/{AdminPage-D4QSV6Zi.js → AdminPage-DwVUK4v9.js} +1 -1
- package/web-app/dist/assets/{Avatar-88MlpLO5.js → Avatar-B7gqhcg3.js} +1 -1
- package/web-app/dist/assets/{Badge-DbGjLr4i.js → Badge-DA3xNJAS.js} +1 -1
- package/web-app/dist/assets/{Button-sp_FVGZj.js → Button-BPXURLaK.js} +1 -1
- package/web-app/dist/assets/{ComparePage-p2ENnfa7.js → ComparePage-B0JQMhKG.js} +1 -1
- package/web-app/dist/assets/GitHubIssuesPanel-D38-fy29.js +12 -0
- package/web-app/dist/assets/{GitHubPRsPanel-Bi_yrcAE.js → GitHubPRsPanel-DLPcW3N0.js} +2 -2
- package/web-app/dist/assets/{HomePage-BB83YPiX.js → HomePage-CzeoS2V_.js} +3 -3
- package/web-app/dist/assets/{LoginPage-BXUudCJ9.js → LoginPage-DqCzxsfx.js} +1 -1
- package/web-app/dist/assets/MagicPage-CBLqpa55.js +31 -0
- package/web-app/dist/assets/{MetricsPage-CX0Ahy-_.js → MetricsPage-CPYQR0zr.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-C4JqatEk.js → NotFoundPage-B62u4iCs.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-t5J2XAJT.js → ProjectPage-DNujSl6j.js} +67 -72
- package/web-app/dist/assets/{ProjectsPage-Bzpz1clk.js → ProjectsPage-uHG7kxB-.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-y_yl8FvH.js → SettingsPage-BaQJbOgL.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-B7d6pzMq.js → ShowcasePage-DQR_e-kg.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-C4tR33KU.js → SystemSettingsPage-C_Q_1WK4.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-DIOCfZIP.js → TeamsPage-DOFErDqX.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DlKyapXX.js → TemplatesPage-Ty72hILN.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Czg-ZC2k.js → TerminalOutput-DqOVnR1p.js} +7 -12
- package/web-app/dist/assets/{activity-h1wU9a0L.js → activity-BgBZ4s4c.js} +1 -1
- package/web-app/dist/assets/{bell-Bu8lsWOp.js → bell-C-UezVWi.js} +1 -1
- package/web-app/dist/assets/{bot-rWO7KjkQ.js → bot-D70fEnm5.js} +1 -1
- package/web-app/dist/assets/{check-BWp8L5Cy.js → check-CBohulxQ.js} +1 -1
- package/web-app/dist/assets/{chevron-left-Bw4I1yGm.js → chevron-left-C-emzUhB.js} +1 -1
- package/web-app/dist/assets/{circle-alert-C37PKXiC.js → circle-alert-8SRY0_GX.js} +1 -1
- package/web-app/dist/assets/{clock-DDScLol4.js → clock-mfq4XnPQ.js} +1 -1
- package/web-app/dist/assets/{cloud-DaYKPLaM.js → cloud-DpRM7T8t.js} +1 -1
- package/web-app/dist/assets/code-xml-1N2Ui-4c.js +6 -0
- package/web-app/dist/assets/{copy-DKIRv0VK.js → copy-LXquTgzI.js} +1 -1
- package/web-app/dist/assets/{database-CYZBHz51.js → database-S1dyXnuT.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CydJu0kl.js → dollar-sign-CRqk0dW5.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DqZ9gpdv.js → file-code-corner-B99CwY_6.js} +1 -1
- package/web-app/dist/assets/{file-plus-CzeFJWp3.js → file-plus-DZ5qnz5b.js} +1 -1
- package/web-app/dist/assets/{folder-open-4YWk08dP.js → folder-open-DBCm7yuF.js} +1 -1
- package/web-app/dist/assets/{git-commit-horizontal-wbqFPNID.js → git-commit-horizontal-DM1ERuNd.js} +1 -1
- package/web-app/dist/assets/{globe-Cby-g5Yb.js → globe-B7xEJSL_.js} +1 -1
- package/web-app/dist/assets/{hammer-BNScgGdp.js → hammer-Cgi3LTuS.js} +1 -1
- package/web-app/dist/assets/{index-6Z4B0I6r.js → index-BN52-GQT.js} +22 -17
- package/web-app/dist/assets/index-BfZSDej1.css +1 -0
- package/web-app/dist/assets/{layers-XfssQc5V.js → layers-Bi8RPIBC.js} +1 -1
- package/web-app/dist/assets/{lightbulb-EhnzRw7M.js → lightbulb-Doc_n8JX.js} +1 -1
- package/web-app/dist/assets/{loader-circle-BA0QIVGA.js → loader-circle-BB932A7A.js} +1 -1
- package/web-app/dist/assets/{lock-BABtHe6K.js → lock-Bt6gpMrs.js} +1 -1
- package/web-app/dist/assets/{mail-Dokiey5S.js → mail-BuzAu1IP.js} +1 -1
- package/web-app/dist/assets/{package-DbJyS1Ft.js → package-BE5FHxQ8.js} +1 -1
- package/web-app/dist/assets/{plus-BcAN8Kaj.js → plus-CNqABexN.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-B3dG1-Sb.js → refresh-cw-34B13ztx.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-Cs1Phctm.js → rotate-ccw-CrD2QB29.js} +1 -1
- package/web-app/dist/assets/{save-DsrNCZrP.js → save-DsJcqdnI.js} +1 -1
- package/web-app/dist/assets/{server-CpN2GX4G.js → server-BcgRMArA.js} +1 -1
- package/web-app/dist/assets/{shield-alert-CKJ1pzCz.js → shield-alert-DLYLdVJ0.js} +1 -1
- package/web-app/dist/assets/{trash-2-C9vZqTqw.js → trash-2-Cc-VTvzt.js} +1 -1
- package/web-app/dist/assets/{trending-down-BNLTrF5P.js → trending-down-CrDpO2a_.js} +1 -1
- package/web-app/dist/assets/{trending-up-DmFIdVOc.js → trending-up-CNVsmM3G.js} +1 -1
- package/web-app/dist/assets/upload-LuDuB7Wc.js +6 -0
- package/web-app/dist/assets/{usePolling-vUlY-o6P.js → usePolling-C8rvc-CG.js} +1 -1
- package/web-app/dist/assets/{user-Dh00W8De.js → user-BT79cI-o.js} +1 -1
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +120 -0
- package/web-app/dist/assets/GitHubIssuesPanel-DBbBTG9w.js +0 -17
- package/web-app/dist/assets/index-CVM4A1Fw.css +0 -1
package/autonomy/loki
CHANGED
|
@@ -450,6 +450,7 @@ show_help() {
|
|
|
450
450
|
echo " onboard [path] Analyze a repo and generate CLAUDE.md (structure, conventions, commands)"
|
|
451
451
|
echo ' explain [path] Analyze any codebase and explain its architecture in plain English'
|
|
452
452
|
echo " docs [cmd] Generate, update, check project documentation"
|
|
453
|
+
echo " magic [cmd] Spec-driven component generation (MagicModules + MoMoA)"
|
|
453
454
|
echo " plan <PRD> Dry-run PRD analysis: complexity, cost, and execution plan"
|
|
454
455
|
echo " ci [opts] CI/CD quality gate integration (--pr, --report, --github-comment)"
|
|
455
456
|
echo " test [opts] AI-powered test generation (--file, --dir, --changed, --dry-run)"
|
|
@@ -540,6 +541,7 @@ cmd_start() {
|
|
|
540
541
|
echo " --complex Force complex complexity tier (8 phases)"
|
|
541
542
|
echo " --github Enable GitHub issue import"
|
|
542
543
|
echo " --no-dashboard Disable web dashboard"
|
|
544
|
+
echo " --api Start dashboard API server alongside the build"
|
|
543
545
|
echo " --sandbox Run in Docker sandbox"
|
|
544
546
|
echo " --skip-memory Skip loading memory context at startup"
|
|
545
547
|
echo " --compliance PRESET Enable compliance mode (default|healthcare|fintech|government)"
|
|
@@ -654,6 +656,10 @@ cmd_start() {
|
|
|
654
656
|
export LOKI_DASHBOARD=false
|
|
655
657
|
shift
|
|
656
658
|
;;
|
|
659
|
+
--api)
|
|
660
|
+
export LOKI_START_API=true
|
|
661
|
+
shift
|
|
662
|
+
;;
|
|
657
663
|
--sandbox)
|
|
658
664
|
export LOKI_SANDBOX_MODE=true
|
|
659
665
|
shift
|
|
@@ -1112,6 +1118,14 @@ cmd_start() {
|
|
|
1112
1118
|
exit 1
|
|
1113
1119
|
fi
|
|
1114
1120
|
|
|
1121
|
+
# --api flag: start the dashboard API server in background before the build
|
|
1122
|
+
if [ "${LOKI_START_API:-false}" = "true" ]; then
|
|
1123
|
+
local dash_port="${LOKI_DASHBOARD_PORT:-57374}"
|
|
1124
|
+
echo -e "${GREEN}Starting dashboard API on port $dash_port...${NC}"
|
|
1125
|
+
cmd_dashboard_start 2>/dev/null &
|
|
1126
|
+
sleep 2
|
|
1127
|
+
fi
|
|
1128
|
+
|
|
1115
1129
|
exec "$RUN_SH" "${args[@]}"
|
|
1116
1130
|
}
|
|
1117
1131
|
|
|
@@ -11199,6 +11213,9 @@ main() {
|
|
|
11199
11213
|
docs)
|
|
11200
11214
|
cmd_docs "$@"
|
|
11201
11215
|
;;
|
|
11216
|
+
magic)
|
|
11217
|
+
cmd_magic "$@"
|
|
11218
|
+
;;
|
|
11202
11219
|
ci)
|
|
11203
11220
|
cmd_ci "$@"
|
|
11204
11221
|
;;
|
|
@@ -19317,6 +19334,661 @@ _docs_human_size() {
|
|
|
19317
19334
|
fi
|
|
19318
19335
|
}
|
|
19319
19336
|
|
|
19337
|
+
#===============================================================================
|
|
19338
|
+
# Magic: Spec-driven component generation (MagicModules + MoMoA)
|
|
19339
|
+
#
|
|
19340
|
+
# Storage layout:
|
|
19341
|
+
# .loki/magic/specs/<name>.md - Canonical component specs
|
|
19342
|
+
# .loki/magic/generated/react/<name>.tsx - React output
|
|
19343
|
+
# .loki/magic/generated/webcomponent/<name>.js - Web Component output
|
|
19344
|
+
# .loki/magic/generated/tests/<name>.test.tsx - Generated tests
|
|
19345
|
+
# .loki/magic/registry.json - Component registry
|
|
19346
|
+
#===============================================================================
|
|
19347
|
+
|
|
19348
|
+
_magic_help() {
|
|
19349
|
+
echo -e "${BOLD}loki magic${NC} - Spec-driven component generation"
|
|
19350
|
+
echo ""
|
|
19351
|
+
echo "Usage: loki magic <command> [options]"
|
|
19352
|
+
echo ""
|
|
19353
|
+
echo "Commands:"
|
|
19354
|
+
echo " generate <name> Generate a component from a spec or screenshot"
|
|
19355
|
+
echo " update Regenerate components from their specs"
|
|
19356
|
+
echo " list List registered components"
|
|
19357
|
+
echo " remove <name> Remove a component and its artifacts"
|
|
19358
|
+
echo " registry <op> Registry operations (show|prune|stats)"
|
|
19359
|
+
echo " debate <name> Run multi-persona debate on an existing component"
|
|
19360
|
+
echo " help Show this help"
|
|
19361
|
+
echo ""
|
|
19362
|
+
echo "Options for 'generate':"
|
|
19363
|
+
echo " --from-spec PATH Generate from existing spec file"
|
|
19364
|
+
echo " --from-screenshot PATH Generate from a screenshot (vision-based)"
|
|
19365
|
+
echo " --target TARGET Output target: react|webcomponent|both (default: react)"
|
|
19366
|
+
echo " --placement PATH Where to place final component in project"
|
|
19367
|
+
echo ""
|
|
19368
|
+
echo "Options for 'update':"
|
|
19369
|
+
echo " --name NAME Update a specific component (default: all)"
|
|
19370
|
+
echo " --force Overwrite local edits if any"
|
|
19371
|
+
echo ""
|
|
19372
|
+
echo "Options for 'list':"
|
|
19373
|
+
echo " --format FMT Output format: json|table (default: table)"
|
|
19374
|
+
echo ""
|
|
19375
|
+
echo "Options for 'remove':"
|
|
19376
|
+
echo " --force Skip confirmation"
|
|
19377
|
+
echo ""
|
|
19378
|
+
echo "Options for 'debate':"
|
|
19379
|
+
echo " --rounds N Number of debate rounds (default: 3)"
|
|
19380
|
+
echo ""
|
|
19381
|
+
echo "Examples:"
|
|
19382
|
+
echo " loki magic generate LoginForm --from-spec specs/login.md"
|
|
19383
|
+
echo " loki magic generate Navbar --from-screenshot shots/nav.png --target both"
|
|
19384
|
+
echo " loki magic update --name LoginForm"
|
|
19385
|
+
echo " loki magic list --format json"
|
|
19386
|
+
echo " loki magic registry stats"
|
|
19387
|
+
echo " loki magic debate LoginForm --rounds 5"
|
|
19388
|
+
}
|
|
19389
|
+
|
|
19390
|
+
_magic_ensure_dirs() {
|
|
19391
|
+
mkdir -p ".loki/magic/specs" \
|
|
19392
|
+
".loki/magic/generated/react" \
|
|
19393
|
+
".loki/magic/generated/webcomponent" \
|
|
19394
|
+
".loki/magic/generated/tests" 2>/dev/null || true
|
|
19395
|
+
if [ ! -f ".loki/magic/registry.json" ]; then
|
|
19396
|
+
# Schema matches magic/registry/schema.json: components as array
|
|
19397
|
+
echo '{"version": "1", "updated_at": "", "components": []}' > ".loki/magic/registry.json"
|
|
19398
|
+
fi
|
|
19399
|
+
}
|
|
19400
|
+
|
|
19401
|
+
_magic_python() {
|
|
19402
|
+
# Prefer python3.12 if present (matches the rest of the project), fall back to python3
|
|
19403
|
+
if [ -x "/opt/homebrew/bin/python3.12" ]; then
|
|
19404
|
+
echo "/opt/homebrew/bin/python3.12"
|
|
19405
|
+
elif command -v python3.12 &>/dev/null; then
|
|
19406
|
+
command -v python3.12
|
|
19407
|
+
else
|
|
19408
|
+
echo "python3"
|
|
19409
|
+
fi
|
|
19410
|
+
}
|
|
19411
|
+
|
|
19412
|
+
# Export PYTHONPATH so magic/* modules shipped with Loki are importable
|
|
19413
|
+
# no matter which project directory the user runs from.
|
|
19414
|
+
_magic_pypath() {
|
|
19415
|
+
local sk="${SKILL_DIR:-}"
|
|
19416
|
+
if [ -z "$sk" ]; then
|
|
19417
|
+
sk="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")"
|
|
19418
|
+
fi
|
|
19419
|
+
# Preserve any existing PYTHONPATH
|
|
19420
|
+
if [ -n "${PYTHONPATH:-}" ]; then
|
|
19421
|
+
echo "${sk}:${PYTHONPATH}"
|
|
19422
|
+
else
|
|
19423
|
+
echo "$sk"
|
|
19424
|
+
fi
|
|
19425
|
+
}
|
|
19426
|
+
|
|
19427
|
+
_magic_valid_name() {
|
|
19428
|
+
local name="$1"
|
|
19429
|
+
[[ "$name" =~ ^[A-Za-z][A-Za-z0-9_-]*$ ]]
|
|
19430
|
+
}
|
|
19431
|
+
|
|
19432
|
+
_magic_generate() {
|
|
19433
|
+
local name=""
|
|
19434
|
+
local from_spec=""
|
|
19435
|
+
local from_screenshot=""
|
|
19436
|
+
local target="react"
|
|
19437
|
+
local placement=""
|
|
19438
|
+
local description=""
|
|
19439
|
+
local tags=""
|
|
19440
|
+
|
|
19441
|
+
while [[ $# -gt 0 ]]; do
|
|
19442
|
+
case "$1" in
|
|
19443
|
+
--from-spec)
|
|
19444
|
+
from_spec="${2:-}"
|
|
19445
|
+
shift 2
|
|
19446
|
+
;;
|
|
19447
|
+
--from-screenshot)
|
|
19448
|
+
from_screenshot="${2:-}"
|
|
19449
|
+
shift 2
|
|
19450
|
+
;;
|
|
19451
|
+
--target)
|
|
19452
|
+
target="${2:-}"
|
|
19453
|
+
shift 2
|
|
19454
|
+
;;
|
|
19455
|
+
--placement)
|
|
19456
|
+
placement="${2:-}"
|
|
19457
|
+
shift 2
|
|
19458
|
+
;;
|
|
19459
|
+
--description|--desc)
|
|
19460
|
+
description="${2:-}"
|
|
19461
|
+
shift 2
|
|
19462
|
+
;;
|
|
19463
|
+
--tags)
|
|
19464
|
+
tags="${2:-}"
|
|
19465
|
+
shift 2
|
|
19466
|
+
;;
|
|
19467
|
+
--help|-h)
|
|
19468
|
+
_magic_help
|
|
19469
|
+
return 0
|
|
19470
|
+
;;
|
|
19471
|
+
-*)
|
|
19472
|
+
log_error "Unknown option: $1"
|
|
19473
|
+
return 1
|
|
19474
|
+
;;
|
|
19475
|
+
*)
|
|
19476
|
+
if [ -z "$name" ]; then
|
|
19477
|
+
name="$1"
|
|
19478
|
+
fi
|
|
19479
|
+
shift
|
|
19480
|
+
;;
|
|
19481
|
+
esac
|
|
19482
|
+
done
|
|
19483
|
+
|
|
19484
|
+
if [ -z "$name" ]; then
|
|
19485
|
+
log_error "Component name is required"
|
|
19486
|
+
echo "Usage: loki magic generate <name> [--from-spec PATH] [--from-screenshot PATH] [--target react|webcomponent|both] [--placement PATH]"
|
|
19487
|
+
return 1
|
|
19488
|
+
fi
|
|
19489
|
+
|
|
19490
|
+
if ! _magic_valid_name "$name"; then
|
|
19491
|
+
log_error "Invalid component name: '$name' (must start with a letter, contain only letters, digits, _ or -)"
|
|
19492
|
+
return 1
|
|
19493
|
+
fi
|
|
19494
|
+
|
|
19495
|
+
case "$target" in
|
|
19496
|
+
react|webcomponent|both) ;;
|
|
19497
|
+
*)
|
|
19498
|
+
log_error "Invalid target: '$target' (must be react, webcomponent, or both)"
|
|
19499
|
+
return 1
|
|
19500
|
+
;;
|
|
19501
|
+
esac
|
|
19502
|
+
|
|
19503
|
+
if [ -n "$from_spec" ] && [ ! -f "$from_spec" ]; then
|
|
19504
|
+
log_error "Spec file not found: $from_spec"
|
|
19505
|
+
return 1
|
|
19506
|
+
fi
|
|
19507
|
+
if [ -n "$from_screenshot" ] && [ ! -f "$from_screenshot" ]; then
|
|
19508
|
+
log_error "Screenshot file not found: $from_screenshot"
|
|
19509
|
+
return 1
|
|
19510
|
+
fi
|
|
19511
|
+
|
|
19512
|
+
_magic_ensure_dirs
|
|
19513
|
+
local py
|
|
19514
|
+
py=$(_magic_python)
|
|
19515
|
+
|
|
19516
|
+
log_info "Generating component '$name' (target: $target)"
|
|
19517
|
+
|
|
19518
|
+
# 1. Build/load the canonical spec
|
|
19519
|
+
local spec_path=".loki/magic/specs/${name}.md"
|
|
19520
|
+
if [ -n "$from_spec" ]; then
|
|
19521
|
+
cp "$from_spec" "$spec_path"
|
|
19522
|
+
log_info "Copied spec from $from_spec"
|
|
19523
|
+
else
|
|
19524
|
+
log_info "Synthesizing spec via magic.core.spec"
|
|
19525
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19526
|
+
import sys
|
|
19527
|
+
try:
|
|
19528
|
+
from magic.core.spec import generate_spec
|
|
19529
|
+
except Exception as exc:
|
|
19530
|
+
sys.stderr.write(f'[ERROR] magic.core.spec not available: {exc}\n')
|
|
19531
|
+
sys.exit(2)
|
|
19532
|
+
generate_spec(
|
|
19533
|
+
name='$name',
|
|
19534
|
+
out_path='$spec_path',
|
|
19535
|
+
from_spec='${from_spec}' or None,
|
|
19536
|
+
from_screenshot='${from_screenshot}' or None,
|
|
19537
|
+
)
|
|
19538
|
+
" || {
|
|
19539
|
+
log_error "Spec generation failed (module magic.core.spec missing or errored)"
|
|
19540
|
+
return 1
|
|
19541
|
+
}
|
|
19542
|
+
fi
|
|
19543
|
+
|
|
19544
|
+
# 2. Load design tokens (optional; module may not yet exist)
|
|
19545
|
+
log_info "Loading design tokens"
|
|
19546
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19547
|
+
try:
|
|
19548
|
+
from magic.core.design_tokens import load_tokens
|
|
19549
|
+
load_tokens()
|
|
19550
|
+
except Exception as exc:
|
|
19551
|
+
import sys
|
|
19552
|
+
sys.stderr.write(f'[WARN] design tokens unavailable: {exc}\n')
|
|
19553
|
+
" 2>&1 | grep -v '^$' || true
|
|
19554
|
+
|
|
19555
|
+
# 3. Generate component(s)
|
|
19556
|
+
log_info "Generating component via magic.core.generator"
|
|
19557
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19558
|
+
import sys
|
|
19559
|
+
try:
|
|
19560
|
+
from magic.core.generator import generate_component
|
|
19561
|
+
except Exception as exc:
|
|
19562
|
+
sys.stderr.write(f'[ERROR] magic.core.generator not available: {exc}\n')
|
|
19563
|
+
sys.exit(2)
|
|
19564
|
+
generate_component(
|
|
19565
|
+
name='$name',
|
|
19566
|
+
spec_path='$spec_path',
|
|
19567
|
+
target='$target',
|
|
19568
|
+
react_out='.loki/magic/generated/react/${name}.tsx',
|
|
19569
|
+
wc_out='.loki/magic/generated/webcomponent/${name}.js',
|
|
19570
|
+
test_out='.loki/magic/generated/tests/${name}.test.tsx',
|
|
19571
|
+
placement='${placement}' or None,
|
|
19572
|
+
)
|
|
19573
|
+
" || {
|
|
19574
|
+
log_error "Component generation failed"
|
|
19575
|
+
return 1
|
|
19576
|
+
}
|
|
19577
|
+
|
|
19578
|
+
# 4. Register in registry
|
|
19579
|
+
log_info "Registering component"
|
|
19580
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19581
|
+
import sys
|
|
19582
|
+
try:
|
|
19583
|
+
from magic.core.registry import register_component
|
|
19584
|
+
except Exception as exc:
|
|
19585
|
+
sys.stderr.write(f'[ERROR] magic.core.registry not available: {exc}\n')
|
|
19586
|
+
sys.exit(2)
|
|
19587
|
+
register_component(
|
|
19588
|
+
registry_path='.loki/magic/registry.json',
|
|
19589
|
+
name='$name',
|
|
19590
|
+
spec_path='$spec_path',
|
|
19591
|
+
target='$target',
|
|
19592
|
+
react_path='.loki/magic/generated/react/${name}.tsx' if '$target' in ('react','both') else '',
|
|
19593
|
+
webcomponent_path='.loki/magic/generated/webcomponent/${name}.js' if '$target' in ('webcomponent','both') else '',
|
|
19594
|
+
test_path='.loki/magic/generated/tests/${name}.test.tsx',
|
|
19595
|
+
description='''$description'''.strip(),
|
|
19596
|
+
tags=[t.strip() for t in '''$tags'''.split(',') if t.strip()],
|
|
19597
|
+
placement='${placement}' or None,
|
|
19598
|
+
)
|
|
19599
|
+
" || {
|
|
19600
|
+
log_warn "Registry update failed; component files are still present"
|
|
19601
|
+
}
|
|
19602
|
+
|
|
19603
|
+
log_info "Component '$name' generated successfully"
|
|
19604
|
+
echo " Spec: $spec_path"
|
|
19605
|
+
case "$target" in
|
|
19606
|
+
react) echo " React: .loki/magic/generated/react/${name}.tsx" ;;
|
|
19607
|
+
webcomponent) echo " WC: .loki/magic/generated/webcomponent/${name}.js" ;;
|
|
19608
|
+
both)
|
|
19609
|
+
echo " React: .loki/magic/generated/react/${name}.tsx"
|
|
19610
|
+
echo " WC: .loki/magic/generated/webcomponent/${name}.js"
|
|
19611
|
+
;;
|
|
19612
|
+
esac
|
|
19613
|
+
echo " Tests: .loki/magic/generated/tests/${name}.test.tsx"
|
|
19614
|
+
}
|
|
19615
|
+
|
|
19616
|
+
_magic_update() {
|
|
19617
|
+
local name=""
|
|
19618
|
+
local force="false"
|
|
19619
|
+
|
|
19620
|
+
while [[ $# -gt 0 ]]; do
|
|
19621
|
+
case "$1" in
|
|
19622
|
+
--name)
|
|
19623
|
+
name="${2:-}"
|
|
19624
|
+
shift 2
|
|
19625
|
+
;;
|
|
19626
|
+
--force)
|
|
19627
|
+
force="true"
|
|
19628
|
+
shift
|
|
19629
|
+
;;
|
|
19630
|
+
--help|-h)
|
|
19631
|
+
_magic_help
|
|
19632
|
+
return 0
|
|
19633
|
+
;;
|
|
19634
|
+
*)
|
|
19635
|
+
log_error "Unknown option: $1"
|
|
19636
|
+
return 1
|
|
19637
|
+
;;
|
|
19638
|
+
esac
|
|
19639
|
+
done
|
|
19640
|
+
|
|
19641
|
+
_magic_ensure_dirs
|
|
19642
|
+
local py
|
|
19643
|
+
py=$(_magic_python)
|
|
19644
|
+
|
|
19645
|
+
log_info "Updating components from specs (name=${name:-<all>}, force=$force)"
|
|
19646
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19647
|
+
import sys
|
|
19648
|
+
try:
|
|
19649
|
+
from magic.core.generator import update_components
|
|
19650
|
+
except Exception as exc:
|
|
19651
|
+
sys.stderr.write(f'[ERROR] magic.core.generator not available: {exc}\n')
|
|
19652
|
+
sys.exit(2)
|
|
19653
|
+
update_components(
|
|
19654
|
+
registry_path='.loki/magic/registry.json',
|
|
19655
|
+
name='${name}' or None,
|
|
19656
|
+
force=$([ "$force" = "true" ] && echo True || echo False),
|
|
19657
|
+
)
|
|
19658
|
+
" || {
|
|
19659
|
+
log_error "Update failed"
|
|
19660
|
+
return 1
|
|
19661
|
+
}
|
|
19662
|
+
|
|
19663
|
+
log_info "Update complete"
|
|
19664
|
+
}
|
|
19665
|
+
|
|
19666
|
+
_magic_list() {
|
|
19667
|
+
local format="table"
|
|
19668
|
+
|
|
19669
|
+
while [[ $# -gt 0 ]]; do
|
|
19670
|
+
case "$1" in
|
|
19671
|
+
--format)
|
|
19672
|
+
format="${2:-}"
|
|
19673
|
+
shift 2
|
|
19674
|
+
;;
|
|
19675
|
+
--help|-h)
|
|
19676
|
+
_magic_help
|
|
19677
|
+
return 0
|
|
19678
|
+
;;
|
|
19679
|
+
*)
|
|
19680
|
+
log_error "Unknown option: $1"
|
|
19681
|
+
return 1
|
|
19682
|
+
;;
|
|
19683
|
+
esac
|
|
19684
|
+
done
|
|
19685
|
+
|
|
19686
|
+
case "$format" in
|
|
19687
|
+
json|table) ;;
|
|
19688
|
+
*)
|
|
19689
|
+
log_error "Invalid format: '$format' (must be json or table)"
|
|
19690
|
+
return 1
|
|
19691
|
+
;;
|
|
19692
|
+
esac
|
|
19693
|
+
|
|
19694
|
+
_magic_ensure_dirs
|
|
19695
|
+
local py
|
|
19696
|
+
py=$(_magic_python)
|
|
19697
|
+
|
|
19698
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19699
|
+
import json, sys
|
|
19700
|
+
try:
|
|
19701
|
+
with open('.loki/magic/registry.json') as fh:
|
|
19702
|
+
data = json.load(fh)
|
|
19703
|
+
except Exception as exc:
|
|
19704
|
+
sys.stderr.write(f'[ERROR] could not read registry: {exc}\n')
|
|
19705
|
+
sys.exit(1)
|
|
19706
|
+
|
|
19707
|
+
components_raw = data.get('components', [])
|
|
19708
|
+
# Normalize: registry schema uses list of entries; legacy format was dict
|
|
19709
|
+
if isinstance(components_raw, dict):
|
|
19710
|
+
components = [{'name': k, **(v or {})} for k, v in components_raw.items()]
|
|
19711
|
+
else:
|
|
19712
|
+
components = components_raw
|
|
19713
|
+
|
|
19714
|
+
fmt = '$format'
|
|
19715
|
+
|
|
19716
|
+
if fmt == 'json':
|
|
19717
|
+
print(json.dumps(components, indent=2, sort_keys=True))
|
|
19718
|
+
else:
|
|
19719
|
+
if not components:
|
|
19720
|
+
print('No components registered. Use: loki magic generate <name>')
|
|
19721
|
+
sys.exit(0)
|
|
19722
|
+
print(f'{\"NAME\":<24} {\"VERSION\":<10} {\"TARGETS\":<24} {\"TAGS\"}')
|
|
19723
|
+
print('-' * 100)
|
|
19724
|
+
for entry in sorted(components, key=lambda e: e.get('name', '')):
|
|
19725
|
+
name = entry.get('name', '-')
|
|
19726
|
+
version = entry.get('version', '-')
|
|
19727
|
+
tgts = entry.get('targets', [])
|
|
19728
|
+
targets = ','.join(tgts) if isinstance(tgts, list) else str(tgts)
|
|
19729
|
+
tags = entry.get('tags', [])
|
|
19730
|
+
tags_str = ','.join(tags) if isinstance(tags, list) else str(tags)
|
|
19731
|
+
print(f'{name:<24} {version:<10} {targets:<24} {tags_str}')
|
|
19732
|
+
"
|
|
19733
|
+
}
|
|
19734
|
+
|
|
19735
|
+
_magic_remove() {
|
|
19736
|
+
local name=""
|
|
19737
|
+
local force="false"
|
|
19738
|
+
|
|
19739
|
+
while [[ $# -gt 0 ]]; do
|
|
19740
|
+
case "$1" in
|
|
19741
|
+
--force)
|
|
19742
|
+
force="true"
|
|
19743
|
+
shift
|
|
19744
|
+
;;
|
|
19745
|
+
--help|-h)
|
|
19746
|
+
_magic_help
|
|
19747
|
+
return 0
|
|
19748
|
+
;;
|
|
19749
|
+
-*)
|
|
19750
|
+
log_error "Unknown option: $1"
|
|
19751
|
+
return 1
|
|
19752
|
+
;;
|
|
19753
|
+
*)
|
|
19754
|
+
if [ -z "$name" ]; then
|
|
19755
|
+
name="$1"
|
|
19756
|
+
fi
|
|
19757
|
+
shift
|
|
19758
|
+
;;
|
|
19759
|
+
esac
|
|
19760
|
+
done
|
|
19761
|
+
|
|
19762
|
+
if [ -z "$name" ]; then
|
|
19763
|
+
log_error "Component name is required"
|
|
19764
|
+
echo "Usage: loki magic remove <name> [--force]"
|
|
19765
|
+
return 1
|
|
19766
|
+
fi
|
|
19767
|
+
|
|
19768
|
+
if ! _magic_valid_name "$name"; then
|
|
19769
|
+
log_error "Invalid component name: '$name'"
|
|
19770
|
+
return 1
|
|
19771
|
+
fi
|
|
19772
|
+
|
|
19773
|
+
_magic_ensure_dirs
|
|
19774
|
+
|
|
19775
|
+
if [ "$force" != "true" ]; then
|
|
19776
|
+
echo -n "Remove component '$name' and all its artifacts? [y/N] "
|
|
19777
|
+
local reply
|
|
19778
|
+
read -r reply || reply=""
|
|
19779
|
+
case "$reply" in
|
|
19780
|
+
y|Y|yes|YES) ;;
|
|
19781
|
+
*)
|
|
19782
|
+
log_info "Aborted"
|
|
19783
|
+
return 0
|
|
19784
|
+
;;
|
|
19785
|
+
esac
|
|
19786
|
+
fi
|
|
19787
|
+
|
|
19788
|
+
rm -f ".loki/magic/specs/${name}.md" \
|
|
19789
|
+
".loki/magic/generated/react/${name}.tsx" \
|
|
19790
|
+
".loki/magic/generated/webcomponent/${name}.js" \
|
|
19791
|
+
".loki/magic/generated/tests/${name}.test.tsx" 2>/dev/null || true
|
|
19792
|
+
|
|
19793
|
+
local py
|
|
19794
|
+
py=$(_magic_python)
|
|
19795
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19796
|
+
import json, sys
|
|
19797
|
+
try:
|
|
19798
|
+
with open('.loki/magic/registry.json') as fh:
|
|
19799
|
+
data = json.load(fh)
|
|
19800
|
+
except Exception:
|
|
19801
|
+
data = {'components': {}, 'version': 1}
|
|
19802
|
+
components = data.setdefault('components', {})
|
|
19803
|
+
if '$name' in components:
|
|
19804
|
+
del components['$name']
|
|
19805
|
+
with open('.loki/magic/registry.json', 'w') as fh:
|
|
19806
|
+
json.dump(data, fh, indent=2, sort_keys=True)
|
|
19807
|
+
" || log_warn "Could not update registry.json"
|
|
19808
|
+
|
|
19809
|
+
log_info "Removed component '$name'"
|
|
19810
|
+
}
|
|
19811
|
+
|
|
19812
|
+
_magic_registry() {
|
|
19813
|
+
local op="${1:-show}"
|
|
19814
|
+
shift 2>/dev/null || true
|
|
19815
|
+
|
|
19816
|
+
case "$op" in
|
|
19817
|
+
--help|-h|help)
|
|
19818
|
+
_magic_help
|
|
19819
|
+
return 0
|
|
19820
|
+
;;
|
|
19821
|
+
esac
|
|
19822
|
+
|
|
19823
|
+
_magic_ensure_dirs
|
|
19824
|
+
local py
|
|
19825
|
+
py=$(_magic_python)
|
|
19826
|
+
|
|
19827
|
+
case "$op" in
|
|
19828
|
+
show)
|
|
19829
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19830
|
+
import json
|
|
19831
|
+
with open('.loki/magic/registry.json') as fh:
|
|
19832
|
+
data = json.load(fh)
|
|
19833
|
+
print(json.dumps(data, indent=2, sort_keys=True))
|
|
19834
|
+
"
|
|
19835
|
+
;;
|
|
19836
|
+
prune)
|
|
19837
|
+
log_info "Pruning registry entries with missing artifacts"
|
|
19838
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19839
|
+
import json, os, sys
|
|
19840
|
+
try:
|
|
19841
|
+
from magic.core.registry import prune_registry
|
|
19842
|
+
removed = prune_registry('.loki/magic/registry.json')
|
|
19843
|
+
print(f'Pruned {removed} stale entries')
|
|
19844
|
+
except Exception as exc:
|
|
19845
|
+
# Fallback prune: drop entries whose spec file is missing
|
|
19846
|
+
with open('.loki/magic/registry.json') as fh:
|
|
19847
|
+
data = json.load(fh)
|
|
19848
|
+
comps = data.get('components', {})
|
|
19849
|
+
removed = 0
|
|
19850
|
+
for name in list(comps.keys()):
|
|
19851
|
+
spec = (comps[name] or {}).get('spec_path')
|
|
19852
|
+
if not spec or not os.path.isfile(spec):
|
|
19853
|
+
del comps[name]
|
|
19854
|
+
removed += 1
|
|
19855
|
+
with open('.loki/magic/registry.json', 'w') as fh:
|
|
19856
|
+
json.dump(data, fh, indent=2, sort_keys=True)
|
|
19857
|
+
print(f'Pruned {removed} stale entries (fallback)')
|
|
19858
|
+
"
|
|
19859
|
+
;;
|
|
19860
|
+
stats)
|
|
19861
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19862
|
+
import json, os
|
|
19863
|
+
with open('.loki/magic/registry.json') as fh:
|
|
19864
|
+
data = json.load(fh)
|
|
19865
|
+
raw = data.get('components', []) or []
|
|
19866
|
+
# Accept both array (new) and dict (legacy) shapes
|
|
19867
|
+
if isinstance(raw, dict):
|
|
19868
|
+
comps = [{'name': k, **(v or {})} for k, v in raw.items()]
|
|
19869
|
+
else:
|
|
19870
|
+
comps = raw
|
|
19871
|
+
total = len(comps)
|
|
19872
|
+
targets = {}
|
|
19873
|
+
missing = 0
|
|
19874
|
+
for entry in comps:
|
|
19875
|
+
entry = entry or {}
|
|
19876
|
+
for t in entry.get('targets', [entry.get('target', 'unknown')]):
|
|
19877
|
+
targets[t] = targets.get(t, 0) + 1
|
|
19878
|
+
spec = entry.get('spec_path')
|
|
19879
|
+
if not spec or not os.path.isfile(spec):
|
|
19880
|
+
missing += 1
|
|
19881
|
+
print(f'Components: {total}')
|
|
19882
|
+
print(f'Missing specs: {missing}')
|
|
19883
|
+
print('By target:')
|
|
19884
|
+
for t in sorted(targets):
|
|
19885
|
+
print(f' {t:<14} {targets[t]}')
|
|
19886
|
+
"
|
|
19887
|
+
;;
|
|
19888
|
+
*)
|
|
19889
|
+
log_error "Unknown registry op: $op"
|
|
19890
|
+
echo "Available: show, prune, stats"
|
|
19891
|
+
return 1
|
|
19892
|
+
;;
|
|
19893
|
+
esac
|
|
19894
|
+
}
|
|
19895
|
+
|
|
19896
|
+
_magic_debate() {
|
|
19897
|
+
local name=""
|
|
19898
|
+
local rounds="3"
|
|
19899
|
+
|
|
19900
|
+
while [[ $# -gt 0 ]]; do
|
|
19901
|
+
case "$1" in
|
|
19902
|
+
--rounds)
|
|
19903
|
+
rounds="${2:-}"
|
|
19904
|
+
shift 2
|
|
19905
|
+
;;
|
|
19906
|
+
--help|-h)
|
|
19907
|
+
_magic_help
|
|
19908
|
+
return 0
|
|
19909
|
+
;;
|
|
19910
|
+
-*)
|
|
19911
|
+
log_error "Unknown option: $1"
|
|
19912
|
+
return 1
|
|
19913
|
+
;;
|
|
19914
|
+
*)
|
|
19915
|
+
if [ -z "$name" ]; then
|
|
19916
|
+
name="$1"
|
|
19917
|
+
fi
|
|
19918
|
+
shift
|
|
19919
|
+
;;
|
|
19920
|
+
esac
|
|
19921
|
+
done
|
|
19922
|
+
|
|
19923
|
+
if [ -z "$name" ]; then
|
|
19924
|
+
log_error "Component name is required"
|
|
19925
|
+
echo "Usage: loki magic debate <name> --rounds N"
|
|
19926
|
+
return 1
|
|
19927
|
+
fi
|
|
19928
|
+
|
|
19929
|
+
if ! _magic_valid_name "$name"; then
|
|
19930
|
+
log_error "Invalid component name: '$name'"
|
|
19931
|
+
return 1
|
|
19932
|
+
fi
|
|
19933
|
+
|
|
19934
|
+
if ! [[ "$rounds" =~ ^[0-9]+$ ]] || [ "$rounds" -lt 1 ]; then
|
|
19935
|
+
log_error "--rounds must be a positive integer"
|
|
19936
|
+
return 1
|
|
19937
|
+
fi
|
|
19938
|
+
|
|
19939
|
+
_magic_ensure_dirs
|
|
19940
|
+
local spec_path=".loki/magic/specs/${name}.md"
|
|
19941
|
+
if [ ! -f "$spec_path" ]; then
|
|
19942
|
+
log_error "No spec found for '$name' at $spec_path"
|
|
19943
|
+
return 1
|
|
19944
|
+
fi
|
|
19945
|
+
|
|
19946
|
+
local py
|
|
19947
|
+
py=$(_magic_python)
|
|
19948
|
+
|
|
19949
|
+
log_info "Running debate on '$name' ($rounds rounds)"
|
|
19950
|
+
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
19951
|
+
import sys
|
|
19952
|
+
try:
|
|
19953
|
+
from magic.core.debate import run_debate
|
|
19954
|
+
except Exception as exc:
|
|
19955
|
+
sys.stderr.write(f'[ERROR] magic.core.debate not available: {exc}\n')
|
|
19956
|
+
sys.exit(2)
|
|
19957
|
+
run_debate(
|
|
19958
|
+
name='$name',
|
|
19959
|
+
spec_path='$spec_path',
|
|
19960
|
+
rounds=$rounds,
|
|
19961
|
+
react_path='.loki/magic/generated/react/${name}.tsx',
|
|
19962
|
+
wc_path='.loki/magic/generated/webcomponent/${name}.js',
|
|
19963
|
+
)
|
|
19964
|
+
" || {
|
|
19965
|
+
log_error "Debate failed"
|
|
19966
|
+
return 1
|
|
19967
|
+
}
|
|
19968
|
+
|
|
19969
|
+
log_info "Debate complete"
|
|
19970
|
+
}
|
|
19971
|
+
|
|
19972
|
+
cmd_magic() {
|
|
19973
|
+
local subcmd="${1:-help}"
|
|
19974
|
+
shift 2>/dev/null || true
|
|
19975
|
+
|
|
19976
|
+
case "$subcmd" in
|
|
19977
|
+
generate|gen) _magic_generate "$@" ;;
|
|
19978
|
+
update|up) _magic_update "$@" ;;
|
|
19979
|
+
list|ls) _magic_list "$@" ;;
|
|
19980
|
+
remove|rm) _magic_remove "$@" ;;
|
|
19981
|
+
registry|reg) _magic_registry "$@" ;;
|
|
19982
|
+
debate) _magic_debate "$@" ;;
|
|
19983
|
+
help|--help|-h) _magic_help ;;
|
|
19984
|
+
*)
|
|
19985
|
+
log_error "Unknown magic subcommand: $subcmd"
|
|
19986
|
+
_magic_help
|
|
19987
|
+
return 1
|
|
19988
|
+
;;
|
|
19989
|
+
esac
|
|
19990
|
+
}
|
|
19991
|
+
|
|
19320
19992
|
# CI/CD quality gate integration (v6.22.0)
|
|
19321
19993
|
cmd_ci() {
|
|
19322
19994
|
local ci_pr=false
|