magmascript 3.2.4__tar.gz → 3.3.0__tar.gz
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.
- {magmascript-3.2.4 → magmascript-3.3.0}/PKG-INFO +21 -23
- {magmascript-3.2.4 → magmascript-3.3.0}/README.md +19 -19
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mc1/client.py +103 -14
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript.egg-info/PKG-INFO +21 -23
- {magmascript-3.2.4 → magmascript-3.3.0}/pyproject.toml +14 -4
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_mc1.py +122 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/LICENSE +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/cli.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/codegen/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/codegen/c.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/cache.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/command_registry.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/commands.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/config.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/exceptions.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/github.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/output.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/registry.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/rpc.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/core/runner.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/archive/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/archive/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/gh/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/gh/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/gh/tools.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/lastfm/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/lastfm/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mac/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mac/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mac/tools.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mb/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mb/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mc1/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mc1/tools.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mcp/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mcp/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/mcp/tools.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/media/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/media/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/media/providers/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/media/providers/archive.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/media/providers/met_museum.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/media/providers/openverse.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/media/providers/pexels.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/media/providers/pixabay.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/media/providers/smithsonian.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/media/tools.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/pi/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/pi/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/pi/tools.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/rights/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/rights/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/rights/tools.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/scores/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/scores/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/scores/tools.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/search/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/domains/search/client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/ast_nodes.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/astheno/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/astheno/arena.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/astheno/dump.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/astheno/floorplan.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/astheno/numeric.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/builtins.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/domain_bridge.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/environment.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/hypnagogia.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/interpreter.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/lexer.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/parser.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/pretty.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/pygments_mgs.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/tokens.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/util.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/lang/visitor.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/repl.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript/scaffold/__init__.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript.egg-info/SOURCES.txt +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript.egg-info/dependency_links.txt +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript.egg-info/entry_points.txt +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript.egg-info/requires.txt +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/magmascript.egg-info/top_level.txt +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/setup.cfg +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_astheno.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_cache.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_classes.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_cli.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_cli_regressions.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_commands.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_domain_bridge.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_import.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_lang.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_mac.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_mcp_client.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_rpc.py +0 -0
- {magmascript-3.2.4 → magmascript-3.3.0}/tests/test_try_catch.py +0 -0
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: magmascript
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Scripting toolkit with domain-first subcommands
|
|
5
5
|
Author: Jake McCoy
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://magmacrunch.com/ware/magmascript/
|
|
8
|
-
Project-URL: Documentation, https://
|
|
9
|
-
Project-URL: Repository, https://github.com/magmacrunch-media/magmascript
|
|
10
|
-
Project-URL: Changelog, https://github.com/magmacrunch-media/magmascript/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Documentation, https://magmacrunch.com/ware/magmascript/
|
|
11
9
|
Project-URL: Funding, https://github.com/sponsors/magmacrunch-media
|
|
12
10
|
Keywords: mcp,scripting,cli,magmacrunch
|
|
13
11
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -421,7 +419,7 @@ this ground was scorched at line 3 (garrisoned at line 1)
|
|
|
421
419
|
|
|
422
420
|
`floorplan` is the only new reserved word; everything else is a shadowable
|
|
423
421
|
builtin, so existing scripts are unaffected. Full reference on the
|
|
424
|
-
[Asthenosphere wiki page](
|
|
422
|
+
[Asthenosphere wiki page](wiki/Asthenosphere.md).
|
|
425
423
|
|
|
426
424
|
### REPL
|
|
427
425
|
|
|
@@ -678,24 +676,24 @@ gh_workflows # GitHub commands
|
|
|
678
676
|
|
|
679
677
|
## Documentation
|
|
680
678
|
|
|
681
|
-
Full documentation on the [Wiki](
|
|
682
|
-
- [MagmaScript Language](
|
|
683
|
-
- [Configuration](
|
|
684
|
-
- [MCP Domain](
|
|
685
|
-
- [Pi Domain](
|
|
686
|
-
- [MC1 Domain](
|
|
687
|
-
- [Mac Domain](
|
|
688
|
-
- [GitHub Domain](
|
|
689
|
-
- [Scores Domain](
|
|
690
|
-
- [Rights Domain](
|
|
691
|
-
- [Archive Domain](
|
|
692
|
-
- [Last.fm Domain](
|
|
693
|
-
- [MusicBrainz Domain](
|
|
694
|
-
- [Search Domain](
|
|
695
|
-
- [Media Domain](
|
|
696
|
-
- [Example Scripts](
|
|
697
|
-
- [Asthenosphere](
|
|
698
|
-
- [Architecture](
|
|
679
|
+
Full documentation on the [Wiki](wiki/):
|
|
680
|
+
- [MagmaScript Language](wiki/MagmaScript-Language.md)
|
|
681
|
+
- [Configuration](wiki/Configuration.md)
|
|
682
|
+
- [MCP Domain](wiki/MCP-Domain.md)
|
|
683
|
+
- [Pi Domain](wiki/Pi-Domain.md)
|
|
684
|
+
- [MC1 Domain](wiki/MC1-Domain.md)
|
|
685
|
+
- [Mac Domain](wiki/Mac-Domain.md)
|
|
686
|
+
- [GitHub Domain](wiki/GitHub-Domain.md)
|
|
687
|
+
- [Scores Domain](wiki/Scores-Domain.md)
|
|
688
|
+
- [Rights Domain](wiki/Rights-Domain.md)
|
|
689
|
+
- [Archive Domain](wiki/Archive-Domain.md)
|
|
690
|
+
- [Last.fm Domain](wiki/Last.fm-Domain.md)
|
|
691
|
+
- [MusicBrainz Domain](wiki/MusicBrainz-Domain.md)
|
|
692
|
+
- [Search Domain](wiki/Search-Domain.md)
|
|
693
|
+
- [Media Domain](wiki/Media-Domain.md)
|
|
694
|
+
- [Example Scripts](wiki/Example-Scripts.md)
|
|
695
|
+
- [Asthenosphere](wiki/Asthenosphere.md)
|
|
696
|
+
- [Architecture](wiki/Architecture.md)
|
|
699
697
|
|
|
700
698
|
## Support This Project
|
|
701
699
|
|
|
@@ -383,7 +383,7 @@ this ground was scorched at line 3 (garrisoned at line 1)
|
|
|
383
383
|
|
|
384
384
|
`floorplan` is the only new reserved word; everything else is a shadowable
|
|
385
385
|
builtin, so existing scripts are unaffected. Full reference on the
|
|
386
|
-
[Asthenosphere wiki page](
|
|
386
|
+
[Asthenosphere wiki page](wiki/Asthenosphere.md).
|
|
387
387
|
|
|
388
388
|
### REPL
|
|
389
389
|
|
|
@@ -640,24 +640,24 @@ gh_workflows # GitHub commands
|
|
|
640
640
|
|
|
641
641
|
## Documentation
|
|
642
642
|
|
|
643
|
-
Full documentation on the [Wiki](
|
|
644
|
-
- [MagmaScript Language](
|
|
645
|
-
- [Configuration](
|
|
646
|
-
- [MCP Domain](
|
|
647
|
-
- [Pi Domain](
|
|
648
|
-
- [MC1 Domain](
|
|
649
|
-
- [Mac Domain](
|
|
650
|
-
- [GitHub Domain](
|
|
651
|
-
- [Scores Domain](
|
|
652
|
-
- [Rights Domain](
|
|
653
|
-
- [Archive Domain](
|
|
654
|
-
- [Last.fm Domain](
|
|
655
|
-
- [MusicBrainz Domain](
|
|
656
|
-
- [Search Domain](
|
|
657
|
-
- [Media Domain](
|
|
658
|
-
- [Example Scripts](
|
|
659
|
-
- [Asthenosphere](
|
|
660
|
-
- [Architecture](
|
|
643
|
+
Full documentation on the [Wiki](wiki/):
|
|
644
|
+
- [MagmaScript Language](wiki/MagmaScript-Language.md)
|
|
645
|
+
- [Configuration](wiki/Configuration.md)
|
|
646
|
+
- [MCP Domain](wiki/MCP-Domain.md)
|
|
647
|
+
- [Pi Domain](wiki/Pi-Domain.md)
|
|
648
|
+
- [MC1 Domain](wiki/MC1-Domain.md)
|
|
649
|
+
- [Mac Domain](wiki/Mac-Domain.md)
|
|
650
|
+
- [GitHub Domain](wiki/GitHub-Domain.md)
|
|
651
|
+
- [Scores Domain](wiki/Scores-Domain.md)
|
|
652
|
+
- [Rights Domain](wiki/Rights-Domain.md)
|
|
653
|
+
- [Archive Domain](wiki/Archive-Domain.md)
|
|
654
|
+
- [Last.fm Domain](wiki/Last.fm-Domain.md)
|
|
655
|
+
- [MusicBrainz Domain](wiki/MusicBrainz-Domain.md)
|
|
656
|
+
- [Search Domain](wiki/Search-Domain.md)
|
|
657
|
+
- [Media Domain](wiki/Media-Domain.md)
|
|
658
|
+
- [Example Scripts](wiki/Example-Scripts.md)
|
|
659
|
+
- [Asthenosphere](wiki/Asthenosphere.md)
|
|
660
|
+
- [Architecture](wiki/Architecture.md)
|
|
661
661
|
|
|
662
662
|
## Support This Project
|
|
663
663
|
|
|
@@ -5,9 +5,11 @@ Supports SSH (remote) execution mode for managing MC1.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import re
|
|
8
9
|
import subprocess
|
|
9
10
|
|
|
10
11
|
from magmascript.core.config import Config, get_config
|
|
12
|
+
from magmascript.core.exceptions import MagmascriptError, SSHError
|
|
11
13
|
from magmascript.core.runner import CommandRunner
|
|
12
14
|
from magmascript.domains.mc1.tools import (
|
|
13
15
|
MC1PowerSettings,
|
|
@@ -18,6 +20,8 @@ from magmascript.domains.mc1.tools import (
|
|
|
18
20
|
parse_system_info,
|
|
19
21
|
)
|
|
20
22
|
|
|
23
|
+
_MAC_RE = re.compile(r"^[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5}$")
|
|
24
|
+
|
|
21
25
|
|
|
22
26
|
class MC1Client:
|
|
23
27
|
"""Direct client for MC1 Windows PC management.
|
|
@@ -32,6 +36,10 @@ class MC1Client:
|
|
|
32
36
|
self._user = cfg.mc1.user
|
|
33
37
|
self._local = local
|
|
34
38
|
self._runner = CommandRunner(cfg.mc1.host, cfg.mc1.user, local=local)
|
|
39
|
+
# Wake-on-LAN relays through the Pi, which shares MC1's broadcast
|
|
40
|
+
# domain. See wake_on_lan for why that is not optional.
|
|
41
|
+
self._pi_host = cfg.pi.host
|
|
42
|
+
self._pi_user = cfg.pi.user
|
|
35
43
|
|
|
36
44
|
# ------------------------------------------------------------------
|
|
37
45
|
# Service management
|
|
@@ -141,26 +149,107 @@ class MC1Client:
|
|
|
141
149
|
# Wake-on-LAN
|
|
142
150
|
# ------------------------------------------------------------------
|
|
143
151
|
|
|
144
|
-
def wake_on_lan(self, mac_address: str) -> str:
|
|
145
|
-
"""Send Wake-on-LAN magic
|
|
152
|
+
def wake_on_lan(self, mac_address: str, *, relay: bool = True) -> str:
|
|
153
|
+
"""Send Wake-on-LAN magic packets to MC1 by every available route.
|
|
154
|
+
|
|
155
|
+
A magic packet is addressed to 255.255.255.255:9, which is **never
|
|
156
|
+
routed**, so a direct send reaches MC1 only from MC1's own subnet and
|
|
157
|
+
silently reaches nothing from anywhere else. The Pi is always on that
|
|
158
|
+
subnet and is reachable over the tailnet from anywhere, so it is used as
|
|
159
|
+
a relay as well. Both are sent rather than guessing which network this
|
|
160
|
+
machine is on, because the loser costs about a second and there is no
|
|
161
|
+
location test to get wrong.
|
|
162
|
+
|
|
163
|
+
This is the same fix the Mac's ~/bin/wake-mc1 got on 2026-10-09, after
|
|
164
|
+
being direct-only meant waking MC1 from away did nothing while the same
|
|
165
|
+
command worked at home.
|
|
146
166
|
|
|
147
|
-
|
|
148
|
-
|
|
167
|
+
Note `wakeonlan` exits 0 whenever the datagram left the host, which says
|
|
168
|
+
nothing about where it went. So this reports the routes that ACCEPTED a
|
|
169
|
+
packet and never claims MC1 is awake. It does not poll; nothing here can
|
|
170
|
+
confirm a wake.
|
|
149
171
|
|
|
150
172
|
Args:
|
|
151
|
-
mac_address: Ethernet MAC
|
|
173
|
+
mac_address: Ethernet MAC, ":" or "-" separated
|
|
174
|
+
(e.g. "A0:AD:9F:A4:72:97" or "A0-AD-9F-A4-72-97").
|
|
175
|
+
relay: Set False to send direct only and skip the Pi.
|
|
152
176
|
|
|
153
177
|
Returns:
|
|
154
|
-
|
|
178
|
+
A message naming each route tried and whether it took the packet.
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
ValueError: if mac_address is not six hex octets.
|
|
182
|
+
MagmascriptError: if no route accepted the packet.
|
|
155
183
|
"""
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
# wakeonlan accepts both separators, so normalise before validating.
|
|
185
|
+
mac = mac_address.replace("-", ":")
|
|
186
|
+
if not _MAC_RE.match(mac):
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"not a MAC address: {mac_address!r} "
|
|
189
|
+
"(expected six hex octets, e.g. A0:AD:9F:A4:72:97)"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
lines: list[str] = []
|
|
193
|
+
accepted = 0
|
|
194
|
+
|
|
195
|
+
if self._wol_direct(mac):
|
|
196
|
+
lines.append(" direct broadcast sent (reaches MC1 only from its own subnet)")
|
|
197
|
+
accepted += 1
|
|
198
|
+
else:
|
|
199
|
+
lines.append(" direct broadcast FAILED (is wakeonlan installed here?)")
|
|
200
|
+
|
|
201
|
+
if relay:
|
|
202
|
+
if not self._pi_host:
|
|
203
|
+
lines.append(
|
|
204
|
+
" relay SKIPPED: no [pi] host configured, so this is "
|
|
205
|
+
"home-only. Set pi.host in ~/.config/magmascript/config.toml"
|
|
206
|
+
)
|
|
207
|
+
elif self._wol_relay(mac):
|
|
208
|
+
lines.append(
|
|
209
|
+
f" relayed via {self._pi_host} (reaches MC1 from any network)"
|
|
210
|
+
)
|
|
211
|
+
accepted += 1
|
|
212
|
+
else:
|
|
213
|
+
lines.append(f" relay via {self._pi_host} FAILED")
|
|
214
|
+
|
|
215
|
+
report = "\n".join(lines)
|
|
216
|
+
if not accepted:
|
|
217
|
+
raise MagmascriptError(
|
|
218
|
+
f"no route accepted a Wake-on-LAN packet for {mac}:\n{report}"
|
|
219
|
+
)
|
|
220
|
+
return f"Wake-on-LAN packet for {mac}:\n{report}"
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _wol_direct(mac: str) -> bool:
|
|
224
|
+
"""Broadcast on this machine's own subnet.
|
|
225
|
+
|
|
226
|
+
False rather than raising when `wakeonlan` is missing, which it is on
|
|
227
|
+
Windows, so the relay can still carry the packet.
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
result = subprocess.run(
|
|
231
|
+
["wakeonlan", mac],
|
|
232
|
+
capture_output=True,
|
|
233
|
+
timeout=10,
|
|
234
|
+
check=False, # the exit status is the return value, not an error
|
|
235
|
+
)
|
|
236
|
+
except (OSError, subprocess.SubprocessError):
|
|
237
|
+
return False
|
|
238
|
+
return result.returncode == 0
|
|
239
|
+
|
|
240
|
+
def _wol_relay(self, mac: str) -> bool:
|
|
241
|
+
"""Run `wakeonlan` on the Pi, which shares MC1's broadcast domain.
|
|
242
|
+
|
|
243
|
+
`mac` is interpolated into a command a remote shell parses, so it must
|
|
244
|
+
already have passed _MAC_RE.
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
CommandRunner(self._pi_host, self._pi_user).run(
|
|
248
|
+
f"wakeonlan {mac}", timeout=15
|
|
249
|
+
)
|
|
250
|
+
except SSHError:
|
|
251
|
+
return False
|
|
252
|
+
return True
|
|
164
253
|
|
|
165
254
|
# ------------------------------------------------------------------
|
|
166
255
|
# Power management
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: magmascript
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Scripting toolkit with domain-first subcommands
|
|
5
5
|
Author: Jake McCoy
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://magmacrunch.com/ware/magmascript/
|
|
8
|
-
Project-URL: Documentation, https://
|
|
9
|
-
Project-URL: Repository, https://github.com/magmacrunch-media/magmascript
|
|
10
|
-
Project-URL: Changelog, https://github.com/magmacrunch-media/magmascript/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Documentation, https://magmacrunch.com/ware/magmascript/
|
|
11
9
|
Project-URL: Funding, https://github.com/sponsors/magmacrunch-media
|
|
12
10
|
Keywords: mcp,scripting,cli,magmacrunch
|
|
13
11
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -421,7 +419,7 @@ this ground was scorched at line 3 (garrisoned at line 1)
|
|
|
421
419
|
|
|
422
420
|
`floorplan` is the only new reserved word; everything else is a shadowable
|
|
423
421
|
builtin, so existing scripts are unaffected. Full reference on the
|
|
424
|
-
[Asthenosphere wiki page](
|
|
422
|
+
[Asthenosphere wiki page](wiki/Asthenosphere.md).
|
|
425
423
|
|
|
426
424
|
### REPL
|
|
427
425
|
|
|
@@ -678,24 +676,24 @@ gh_workflows # GitHub commands
|
|
|
678
676
|
|
|
679
677
|
## Documentation
|
|
680
678
|
|
|
681
|
-
Full documentation on the [Wiki](
|
|
682
|
-
- [MagmaScript Language](
|
|
683
|
-
- [Configuration](
|
|
684
|
-
- [MCP Domain](
|
|
685
|
-
- [Pi Domain](
|
|
686
|
-
- [MC1 Domain](
|
|
687
|
-
- [Mac Domain](
|
|
688
|
-
- [GitHub Domain](
|
|
689
|
-
- [Scores Domain](
|
|
690
|
-
- [Rights Domain](
|
|
691
|
-
- [Archive Domain](
|
|
692
|
-
- [Last.fm Domain](
|
|
693
|
-
- [MusicBrainz Domain](
|
|
694
|
-
- [Search Domain](
|
|
695
|
-
- [Media Domain](
|
|
696
|
-
- [Example Scripts](
|
|
697
|
-
- [Asthenosphere](
|
|
698
|
-
- [Architecture](
|
|
679
|
+
Full documentation on the [Wiki](wiki/):
|
|
680
|
+
- [MagmaScript Language](wiki/MagmaScript-Language.md)
|
|
681
|
+
- [Configuration](wiki/Configuration.md)
|
|
682
|
+
- [MCP Domain](wiki/MCP-Domain.md)
|
|
683
|
+
- [Pi Domain](wiki/Pi-Domain.md)
|
|
684
|
+
- [MC1 Domain](wiki/MC1-Domain.md)
|
|
685
|
+
- [Mac Domain](wiki/Mac-Domain.md)
|
|
686
|
+
- [GitHub Domain](wiki/GitHub-Domain.md)
|
|
687
|
+
- [Scores Domain](wiki/Scores-Domain.md)
|
|
688
|
+
- [Rights Domain](wiki/Rights-Domain.md)
|
|
689
|
+
- [Archive Domain](wiki/Archive-Domain.md)
|
|
690
|
+
- [Last.fm Domain](wiki/Last.fm-Domain.md)
|
|
691
|
+
- [MusicBrainz Domain](wiki/MusicBrainz-Domain.md)
|
|
692
|
+
- [Search Domain](wiki/Search-Domain.md)
|
|
693
|
+
- [Media Domain](wiki/Media-Domain.md)
|
|
694
|
+
- [Example Scripts](wiki/Example-Scripts.md)
|
|
695
|
+
- [Asthenosphere](wiki/Asthenosphere.md)
|
|
696
|
+
- [Architecture](wiki/Architecture.md)
|
|
699
697
|
|
|
700
698
|
## Support This Project
|
|
701
699
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "magmascript"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.3.0"
|
|
8
8
|
description = "Scripting toolkit with domain-first subcommands"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -48,10 +48,20 @@ all = [
|
|
|
48
48
|
magmascript = "magmascript.cli:main"
|
|
49
49
|
|
|
50
50
|
[project.urls]
|
|
51
|
+
# EVERY LINK HERE HAS TO BE PUBLIC, because this is what PyPI puts on the
|
|
52
|
+
# project page for anyone who finds the package. The repository went private on
|
|
53
|
+
# 2026-10-03, which 404'd three of the five: Documentation (it pointed at the
|
|
54
|
+
# wiki, and a private repo's wiki is turned off entirely), Repository, and
|
|
55
|
+
# Changelog, which was a blob link into the repo.
|
|
56
|
+
#
|
|
57
|
+
# They are removed rather than repointed. There is no public repository or
|
|
58
|
+
# changelog to send anyone to, and an absent link is better than one that 404s
|
|
59
|
+
# from the package's own front page.
|
|
60
|
+
#
|
|
61
|
+
# NOTE a published release's metadata is frozen. Fixing this file only affects
|
|
62
|
+
# the NEXT release; the pages for 3.2.4 and earlier keep the dead links.
|
|
51
63
|
Homepage = "https://magmacrunch.com/ware/magmascript/"
|
|
52
|
-
Documentation = "https://
|
|
53
|
-
Repository = "https://github.com/magmacrunch-media/magmascript"
|
|
54
|
-
Changelog = "https://github.com/magmacrunch-media/magmascript/blob/main/CHANGELOG.md"
|
|
64
|
+
Documentation = "https://magmacrunch.com/ware/magmascript/"
|
|
55
65
|
Funding = "https://github.com/sponsors/magmacrunch-media"
|
|
56
66
|
|
|
57
67
|
[tool.setuptools.packages.find]
|
|
@@ -156,3 +156,125 @@ class TestRegistration:
|
|
|
156
156
|
from magmascript.core.config import load_config
|
|
157
157
|
|
|
158
158
|
assert load_config().mc1.host == "9.9.9.9"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TestWakeOnLan:
|
|
162
|
+
"""`mc1 wake` sends by BOTH routes.
|
|
163
|
+
|
|
164
|
+
A magic packet goes to 255.255.255.255:9, which is never routed, so a
|
|
165
|
+
direct send reaches MC1 only from MC1's own subnet. Direct-only is what
|
|
166
|
+
made the Mac's wake-mc1 silently do nothing from off that subnet, and
|
|
167
|
+
these are the checks that would have caught it.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def _client(self, *, pi_host="10.0.0.9", pi_user="jake"):
|
|
171
|
+
from magmascript.core.config import Config, MC1Config, PIConfig
|
|
172
|
+
from magmascript.domains.mc1 import MC1Client
|
|
173
|
+
|
|
174
|
+
return MC1Client(
|
|
175
|
+
Config(
|
|
176
|
+
mc1=MC1Config(host="1.2.3.4", user="someone"),
|
|
177
|
+
pi=PIConfig(host=pi_host, user=pi_user),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _stub(self, monkeypatch, client, *, direct, relay):
|
|
182
|
+
"""Record calls and report the given per-route outcomes."""
|
|
183
|
+
calls = []
|
|
184
|
+
monkeypatch.setattr(
|
|
185
|
+
type(client),
|
|
186
|
+
"_wol_direct",
|
|
187
|
+
staticmethod(lambda mac: (calls.append(("direct", mac)), direct)[1]),
|
|
188
|
+
)
|
|
189
|
+
monkeypatch.setattr(
|
|
190
|
+
type(client),
|
|
191
|
+
"_wol_relay",
|
|
192
|
+
lambda self, mac: (calls.append(("relay", mac)), relay)[1],
|
|
193
|
+
)
|
|
194
|
+
return calls
|
|
195
|
+
|
|
196
|
+
def test_sends_both_routes(self, monkeypatch):
|
|
197
|
+
client = self._client()
|
|
198
|
+
calls = self._stub(monkeypatch, client, direct=True, relay=True)
|
|
199
|
+
out = client.wake_on_lan("A0:AD:9F:A4:72:97")
|
|
200
|
+
assert [c[0] for c in calls] == ["direct", "relay"]
|
|
201
|
+
assert "direct broadcast sent" in out
|
|
202
|
+
assert "relayed via 10.0.0.9" in out
|
|
203
|
+
|
|
204
|
+
def test_relay_carries_it_when_direct_fails(self, monkeypatch):
|
|
205
|
+
"""The off-LAN case: no local wakeonlan, Pi still delivers."""
|
|
206
|
+
client = self._client()
|
|
207
|
+
self._stub(monkeypatch, client, direct=False, relay=True)
|
|
208
|
+
out = client.wake_on_lan("A0:AD:9F:A4:72:97")
|
|
209
|
+
assert "direct broadcast FAILED" in out
|
|
210
|
+
assert "relayed via 10.0.0.9" in out
|
|
211
|
+
|
|
212
|
+
def test_direct_carries_it_when_relay_fails(self, monkeypatch):
|
|
213
|
+
client = self._client()
|
|
214
|
+
self._stub(monkeypatch, client, direct=True, relay=False)
|
|
215
|
+
out = client.wake_on_lan("A0:AD:9F:A4:72:97")
|
|
216
|
+
assert "direct broadcast sent" in out
|
|
217
|
+
assert "relay via 10.0.0.9 FAILED" in out
|
|
218
|
+
|
|
219
|
+
def test_raises_when_no_route_accepts(self, monkeypatch):
|
|
220
|
+
from magmascript.core.exceptions import MagmascriptError
|
|
221
|
+
|
|
222
|
+
client = self._client()
|
|
223
|
+
self._stub(monkeypatch, client, direct=False, relay=False)
|
|
224
|
+
try:
|
|
225
|
+
client.wake_on_lan("A0:AD:9F:A4:72:97")
|
|
226
|
+
except MagmascriptError as e:
|
|
227
|
+
assert "no route accepted" in str(e)
|
|
228
|
+
else:
|
|
229
|
+
raise AssertionError("expected MagmascriptError when both routes fail")
|
|
230
|
+
|
|
231
|
+
def test_unconfigured_pi_is_named_not_silently_skipped(self, monkeypatch):
|
|
232
|
+
"""A missing [pi] host makes this home-only, so it must say so."""
|
|
233
|
+
client = self._client(pi_host="", pi_user="")
|
|
234
|
+
calls = self._stub(monkeypatch, client, direct=True, relay=True)
|
|
235
|
+
out = client.wake_on_lan("A0:AD:9F:A4:72:97")
|
|
236
|
+
assert [c[0] for c in calls] == ["direct"]
|
|
237
|
+
assert "relay SKIPPED" in out
|
|
238
|
+
assert "home-only" in out
|
|
239
|
+
|
|
240
|
+
def test_relay_false_skips_the_pi(self, monkeypatch):
|
|
241
|
+
client = self._client()
|
|
242
|
+
calls = self._stub(monkeypatch, client, direct=True, relay=True)
|
|
243
|
+
client.wake_on_lan("A0:AD:9F:A4:72:97", relay=False)
|
|
244
|
+
assert [c[0] for c in calls] == ["direct"]
|
|
245
|
+
|
|
246
|
+
def test_hyphens_are_normalised(self, monkeypatch):
|
|
247
|
+
client = self._client()
|
|
248
|
+
calls = self._stub(monkeypatch, client, direct=True, relay=True)
|
|
249
|
+
client.wake_on_lan("A0-AD-9F-A4-72-97")
|
|
250
|
+
assert {c[1] for c in calls} == {"A0:AD:9F:A4:72:97"}
|
|
251
|
+
|
|
252
|
+
def test_rejects_a_non_mac(self, monkeypatch):
|
|
253
|
+
client = self._client()
|
|
254
|
+
calls = self._stub(monkeypatch, client, direct=True, relay=True)
|
|
255
|
+
for bad in ("", "nope", "A0:AD:9F:A4:72", "A0:AD:9F:A4:72:97:FF", "ZZ:..."):
|
|
256
|
+
try:
|
|
257
|
+
client.wake_on_lan(bad)
|
|
258
|
+
except ValueError:
|
|
259
|
+
pass
|
|
260
|
+
else:
|
|
261
|
+
raise AssertionError(f"expected ValueError for {bad!r}")
|
|
262
|
+
assert calls == [], "nothing may be sent for a malformed MAC"
|
|
263
|
+
|
|
264
|
+
def test_rejects_shell_injection_before_sending(self, monkeypatch):
|
|
265
|
+
"""The relay interpolates the MAC into a command a remote shell parses."""
|
|
266
|
+
client = self._client()
|
|
267
|
+
calls = self._stub(monkeypatch, client, direct=True, relay=True)
|
|
268
|
+
for bad in (
|
|
269
|
+
"A0:AD:9F:A4:72:97; touch /tmp/pwned",
|
|
270
|
+
"A0:AD:9F:A4:72:97 && reboot",
|
|
271
|
+
"$(reboot)",
|
|
272
|
+
"`reboot`",
|
|
273
|
+
):
|
|
274
|
+
try:
|
|
275
|
+
client.wake_on_lan(bad)
|
|
276
|
+
except ValueError:
|
|
277
|
+
pass
|
|
278
|
+
else:
|
|
279
|
+
raise AssertionError(f"expected ValueError for {bad!r}")
|
|
280
|
+
assert calls == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|