runspec-windows 0.1.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.
- runspec_windows-0.1.0/.gitignore +58 -0
- runspec_windows-0.1.0/CHANGELOG.md +35 -0
- runspec_windows-0.1.0/PKG-INFO +15 -0
- runspec_windows-0.1.0/README.md +119 -0
- runspec_windows-0.1.0/pyproject.toml +93 -0
- runspec_windows-0.1.0/runspec_windows/__init__.py +14 -0
- runspec_windows-0.1.0/runspec_windows/_platform.py +65 -0
- runspec_windows-0.1.0/runspec_windows/eventlog.py +87 -0
- runspec_windows-0.1.0/runspec_windows/graph/__init__.py +24 -0
- runspec_windows-0.1.0/runspec_windows/graph/_emit.py +20 -0
- runspec_windows-0.1.0/runspec_windows/graph/account.py +48 -0
- runspec_windows-0.1.0/runspec_windows/graph/auth.py +127 -0
- runspec_windows-0.1.0/runspec_windows/graph/calendar.py +77 -0
- runspec_windows-0.1.0/runspec_windows/graph/client.py +43 -0
- runspec_windows-0.1.0/runspec_windows/graph/files.py +51 -0
- runspec_windows-0.1.0/runspec_windows/graph/outlook.py +79 -0
- runspec_windows-0.1.0/runspec_windows/graph/teams.py +62 -0
- runspec_windows-0.1.0/runspec_windows/network.py +160 -0
- runspec_windows-0.1.0/runspec_windows/processes.py +95 -0
- runspec_windows-0.1.0/runspec_windows/runspec.toml +425 -0
- runspec_windows-0.1.0/runspec_windows/services.py +97 -0
- runspec_windows-0.1.0/runspec_windows/sessions.py +83 -0
- runspec_windows-0.1.0/runspec_windows/software.py +76 -0
- runspec_windows-0.1.0/runspec_windows/system_info.py +138 -0
- runspec_windows-0.1.0/runspec_windows/tasks.py +59 -0
- runspec_windows-0.1.0/tests/__init__.py +0 -0
- runspec_windows-0.1.0/tests/test_graph.py +236 -0
- runspec_windows-0.1.0/tests/test_parsers.py +119 -0
- runspec_windows-0.1.0/tests/test_platform_guard.py +37 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
.Python
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.eggs/
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
.env
|
|
16
|
+
pip-wheel-metadata/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
htmlcov/
|
|
21
|
+
.coverage
|
|
22
|
+
coverage.xml
|
|
23
|
+
*.cover
|
|
24
|
+
|
|
25
|
+
# Node
|
|
26
|
+
node_modules/
|
|
27
|
+
dist/
|
|
28
|
+
*.js.map
|
|
29
|
+
.npm
|
|
30
|
+
|
|
31
|
+
# Go
|
|
32
|
+
*.exe
|
|
33
|
+
*.test
|
|
34
|
+
*.out
|
|
35
|
+
vendor/
|
|
36
|
+
|
|
37
|
+
# IDE
|
|
38
|
+
.idea/
|
|
39
|
+
.vscode/
|
|
40
|
+
*.iml
|
|
41
|
+
*.iws
|
|
42
|
+
*.ipr
|
|
43
|
+
.DS_Store
|
|
44
|
+
Thumbs.db
|
|
45
|
+
|
|
46
|
+
# Docs
|
|
47
|
+
site/
|
|
48
|
+
|
|
49
|
+
# Misc
|
|
50
|
+
*.log
|
|
51
|
+
*.tmp
|
|
52
|
+
|
|
53
|
+
# External reference repos (cloned locally, not committed)
|
|
54
|
+
chainlit-docs/
|
|
55
|
+
.chainlit/
|
|
56
|
+
|
|
57
|
+
# Claude Code local config (machine-specific)
|
|
58
|
+
.claude/launch.json
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# runspec-windows Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] — 2026-06-03
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
35 runnables covering Windows system administration and Microsoft 365:
|
|
8
|
+
|
|
9
|
+
**System monitoring** — `system-info`, `disk-usage`, `check-memory`, `uptime`
|
|
10
|
+
|
|
11
|
+
**Processes** — `list-processes`, `process-info`, `kill-process`
|
|
12
|
+
|
|
13
|
+
**Services** — `list-services`, `check-service`, `restart-service`, `stop-service`
|
|
14
|
+
|
|
15
|
+
**Network** — `ping-host`, `check-port`, `show-connections`, `list-adapters`, `flush-dns`
|
|
16
|
+
|
|
17
|
+
**Event log** — `query-eventlog`, `recent-errors`
|
|
18
|
+
|
|
19
|
+
**Scheduled tasks** — `list-scheduled-tasks`
|
|
20
|
+
|
|
21
|
+
**Installed software** — `installed-software`
|
|
22
|
+
|
|
23
|
+
**Sessions** — `who`, `current-user`
|
|
24
|
+
|
|
25
|
+
**Microsoft Graph (Outlook + Teams + Calendar + OneDrive)** — `graph-login`, `graph-whoami`, `outlook-unread`, `outlook-search`, `outlook-folders`, `teams-list-chats`, `teams-read-chat`, `calendar-upcoming`, `calendar-today`, `next-meeting`, `onedrive-recent`, `onedrive-search`, `onedrive-list`
|
|
26
|
+
|
|
27
|
+
Read-only runnables are `autonomy = "autonomous"`; state-changing ones
|
|
28
|
+
(`kill-process`, `restart-service`, `stop-service`, `flush-dns`, `graph-login`)
|
|
29
|
+
are `autonomy = "confirm"`. Windows-only runnables return a structured JSON
|
|
30
|
+
error (not a traceback) when run on a non-Windows host.
|
|
31
|
+
|
|
32
|
+
Microsoft Graph runnables use delegated **device-code** auth via MSAL — no
|
|
33
|
+
client secret. The optional `msal`/`httpx` dependencies live behind the
|
|
34
|
+
`[graph]` extra. The package also exports `graph_get()` and `get_token()` as a
|
|
35
|
+
public Python API for building further Graph-backed wrapper runnables.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runspec-windows
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Windows system admin + Microsoft 365 (Outlook, Teams) runnables for runspec
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: runspec>=0.23.0
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
9
|
+
Requires-Dist: msal>=1.28; extra == 'dev'
|
|
10
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
13
|
+
Provides-Extra: graph
|
|
14
|
+
Requires-Dist: httpx>=0.27; extra == 'graph'
|
|
15
|
+
Requires-Dist: msal>=1.28; extra == 'graph'
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# runspec-windows
|
|
2
|
+
|
|
3
|
+
Windows system-administration and Microsoft 365 (Outlook + Teams) runnables for
|
|
4
|
+
[runspec](https://pypi.org/project/runspec/). The Windows counterpart to
|
|
5
|
+
[`runspec-linux`](../runspec-linux): `pip install` it into a venv and the
|
|
6
|
+
runnables are discoverable by `runspec local`, `runspec serve` (MCP), and
|
|
7
|
+
runspec-console.
|
|
8
|
+
|
|
9
|
+
All runnables emit JSON on stdout. Read-only runnables are `autonomy =
|
|
10
|
+
"autonomous"`; state-changing ones (`kill-process`, `restart-service`,
|
|
11
|
+
`stop-service`, `flush-dns`, `graph-login`) are `autonomy = "confirm"`.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pip install runspec-windows # system utilities only
|
|
17
|
+
pip install "runspec-windows[graph]" # + Microsoft 365 (Outlook/Teams) runnables
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Runnables
|
|
21
|
+
|
|
22
|
+
| Group | Runnables |
|
|
23
|
+
|---|---|
|
|
24
|
+
| System | `system-info`, `disk-usage`, `check-memory`, `uptime` |
|
|
25
|
+
| Processes | `list-processes`, `process-info`, `kill-process` |
|
|
26
|
+
| Services | `list-services`, `check-service`, `restart-service`, `stop-service` |
|
|
27
|
+
| Network | `ping-host`, `check-port`, `show-connections`, `list-adapters`, `flush-dns` |
|
|
28
|
+
| Event log | `query-eventlog`, `recent-errors` |
|
|
29
|
+
| Scheduled tasks | `list-scheduled-tasks` |
|
|
30
|
+
| Installed software | `installed-software` |
|
|
31
|
+
| Sessions | `who`, `current-user` |
|
|
32
|
+
| Microsoft 365 | `graph-login`, `graph-whoami`, `outlook-unread`, `outlook-search`, `outlook-folders`, `teams-list-chats`, `teams-read-chat`, `calendar-upcoming`, `calendar-today`, `next-meeting`, `onedrive-recent`, `onedrive-search`, `onedrive-list` |
|
|
33
|
+
|
|
34
|
+
The system runnables are Windows-only; run one on macOS/Linux and it returns a
|
|
35
|
+
clean JSON error rather than a traceback. The Microsoft Graph runnables are pure
|
|
36
|
+
HTTP and run anywhere — they only need a token.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
system-info
|
|
40
|
+
list-processes --filter chrome --limit 10
|
|
41
|
+
query-eventlog --log System --level error --count 20
|
|
42
|
+
installed-software --filter "Microsoft"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Microsoft 365 (Outlook + Teams) — one-time setup
|
|
46
|
+
|
|
47
|
+
The Graph runnables authenticate as the signed-in user via the OAuth
|
|
48
|
+
**device-code** flow (no client secret, no admin app password). You need a
|
|
49
|
+
public-client **app registration** in your Entra ID (Azure AD) tenant:
|
|
50
|
+
|
|
51
|
+
1. **Entra admin center → App registrations → New registration.** Give it a
|
|
52
|
+
name; under *Supported account types* pick whatever matches your tenant.
|
|
53
|
+
2. **Authentication → Advanced settings → Allow public client flows → Yes.**
|
|
54
|
+
(Device-code flow requires this.)
|
|
55
|
+
3. **API permissions → Add → Microsoft Graph → Delegated**, add
|
|
56
|
+
`User.Read`, `Mail.Read`, `Chat.Read`, `Calendars.Read`, `Files.Read.All`,
|
|
57
|
+
then *Grant admin consent* if your tenant requires it.
|
|
58
|
+
4. Copy the **Application (client) ID**.
|
|
59
|
+
|
|
60
|
+
Then point the package at it (the client id is **not** a secret):
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
set RUNSPEC_GRAPH_CLIENT_ID=<application-client-id>
|
|
64
|
+
set RUNSPEC_GRAPH_TENANT=<tenant-id-or-domain> # optional; default "organizations"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
…or create `%APPDATA%\runspec-windows\graph.toml`:
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
client_id = "00000000-0000-0000-0000-000000000000"
|
|
71
|
+
tenant = "contoso.onmicrosoft.com"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Sign in once (prints a code + URL to visit; the token is cached under
|
|
75
|
+
`%APPDATA%\runspec-windows\msal_cache.bin`):
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
graph-login
|
|
79
|
+
graph-whoami
|
|
80
|
+
outlook-unread --count 10
|
|
81
|
+
outlook-search --query "invoice"
|
|
82
|
+
teams-list-chats
|
|
83
|
+
teams-read-chat --chat-id 19:abc...@thread.v2
|
|
84
|
+
calendar-upcoming --days 7
|
|
85
|
+
calendar-today
|
|
86
|
+
next-meeting
|
|
87
|
+
onedrive-recent
|
|
88
|
+
onedrive-search --query "budget"
|
|
89
|
+
onedrive-list --path "/Documents"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Tokens are cached and refreshed silently; rerun `graph-login` if the cache is
|
|
93
|
+
cleared or consent changes.
|
|
94
|
+
|
|
95
|
+
## Public Python API
|
|
96
|
+
|
|
97
|
+
Mirroring `runspec-linux`'s `nc_send`, this package exports the authenticated
|
|
98
|
+
Graph client so you can build your own wrapper runnables:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from runspec_windows import graph_get, get_token
|
|
102
|
+
|
|
103
|
+
me = graph_get("/me")
|
|
104
|
+
events = graph_get("/me/events", params={"$top": 5})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
python -m venv .venv && . .venv/bin/activate # or .venv\Scripts\activate
|
|
111
|
+
pip install -e ".[dev,graph]"
|
|
112
|
+
ruff check . && ruff format --check .
|
|
113
|
+
pytest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The OS-specific work sits behind thin helpers in `_platform.py`; the pure output
|
|
117
|
+
parsers (`tasklist`/`netstat`/`ipconfig`/`schtasks`/`query user`) and the Graph
|
|
118
|
+
formatters are unit-tested on any platform. Real end-to-end runs of the
|
|
119
|
+
Windows-only runnables happen on a Windows host / the `windows-latest` CI job.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runspec-windows"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
description = "Windows system admin + Microsoft 365 (Outlook, Teams) runnables for runspec"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"runspec>=0.23.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
# Microsoft Graph runnables (Outlook + Teams). Pure HTTP — no pywin32 needed.
|
|
16
|
+
graph = [
|
|
17
|
+
"msal>=1.28",
|
|
18
|
+
"httpx>=0.27",
|
|
19
|
+
]
|
|
20
|
+
dev = [
|
|
21
|
+
"ruff",
|
|
22
|
+
"mypy",
|
|
23
|
+
"pytest>=8.0",
|
|
24
|
+
"msal>=1.28",
|
|
25
|
+
"httpx>=0.27",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
# System monitoring
|
|
30
|
+
system-info = "runspec_windows.system_info:main_system_info"
|
|
31
|
+
disk-usage = "runspec_windows.system_info:main_disk_usage"
|
|
32
|
+
check-memory = "runspec_windows.system_info:main_check_memory"
|
|
33
|
+
uptime = "runspec_windows.system_info:main_uptime"
|
|
34
|
+
# Processes
|
|
35
|
+
list-processes = "runspec_windows.processes:main_list_processes"
|
|
36
|
+
process-info = "runspec_windows.processes:main_process_info"
|
|
37
|
+
kill-process = "runspec_windows.processes:main_kill_process"
|
|
38
|
+
# Services
|
|
39
|
+
list-services = "runspec_windows.services:main_list_services"
|
|
40
|
+
check-service = "runspec_windows.services:main_check_service"
|
|
41
|
+
restart-service = "runspec_windows.services:main_restart_service"
|
|
42
|
+
stop-service = "runspec_windows.services:main_stop_service"
|
|
43
|
+
# Network
|
|
44
|
+
ping-host = "runspec_windows.network:main_ping_host"
|
|
45
|
+
check-port = "runspec_windows.network:main_check_port"
|
|
46
|
+
show-connections = "runspec_windows.network:main_show_connections"
|
|
47
|
+
list-adapters = "runspec_windows.network:main_list_adapters"
|
|
48
|
+
flush-dns = "runspec_windows.network:main_flush_dns"
|
|
49
|
+
# Event log
|
|
50
|
+
query-eventlog = "runspec_windows.eventlog:main_query_eventlog"
|
|
51
|
+
recent-errors = "runspec_windows.eventlog:main_recent_errors"
|
|
52
|
+
# Scheduled tasks
|
|
53
|
+
list-scheduled-tasks = "runspec_windows.tasks:main_list_scheduled_tasks"
|
|
54
|
+
# Installed software
|
|
55
|
+
installed-software = "runspec_windows.software:main_installed_software"
|
|
56
|
+
# Sessions
|
|
57
|
+
who = "runspec_windows.sessions:main_who"
|
|
58
|
+
current-user = "runspec_windows.sessions:main_current_user"
|
|
59
|
+
# Microsoft Graph — account
|
|
60
|
+
graph-login = "runspec_windows.graph.account:main_graph_login"
|
|
61
|
+
graph-whoami = "runspec_windows.graph.account:main_graph_whoami"
|
|
62
|
+
# Microsoft Graph — Outlook
|
|
63
|
+
outlook-unread = "runspec_windows.graph.outlook:main_outlook_unread"
|
|
64
|
+
outlook-search = "runspec_windows.graph.outlook:main_outlook_search"
|
|
65
|
+
outlook-folders = "runspec_windows.graph.outlook:main_outlook_folders"
|
|
66
|
+
# Microsoft Graph — Calendar
|
|
67
|
+
calendar-upcoming = "runspec_windows.graph.calendar:main_calendar_upcoming"
|
|
68
|
+
calendar-today = "runspec_windows.graph.calendar:main_calendar_today"
|
|
69
|
+
next-meeting = "runspec_windows.graph.calendar:main_next_meeting"
|
|
70
|
+
# Microsoft Graph — OneDrive / Files
|
|
71
|
+
onedrive-recent = "runspec_windows.graph.files:main_onedrive_recent"
|
|
72
|
+
onedrive-search = "runspec_windows.graph.files:main_onedrive_search"
|
|
73
|
+
onedrive-list = "runspec_windows.graph.files:main_onedrive_list"
|
|
74
|
+
# Microsoft Graph — Teams
|
|
75
|
+
teams-list-chats = "runspec_windows.graph.teams:main_teams_list_chats"
|
|
76
|
+
teams-read-chat = "runspec_windows.graph.teams:main_teams_read_chat"
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
testpaths = ["tests"]
|
|
80
|
+
|
|
81
|
+
[tool.mypy]
|
|
82
|
+
python_version = "3.10"
|
|
83
|
+
|
|
84
|
+
[[tool.mypy.overrides]]
|
|
85
|
+
module = ["msal.*"]
|
|
86
|
+
ignore_missing_imports = true
|
|
87
|
+
|
|
88
|
+
[tool.ruff]
|
|
89
|
+
line-length = 200
|
|
90
|
+
target-version = "py310"
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint]
|
|
93
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""runspec-windows — Windows system admin + Microsoft 365 runnables for runspec.
|
|
2
|
+
|
|
3
|
+
Public API (parallels runspec-linux's ``nc_send``): ``graph_get`` and
|
|
4
|
+
``get_token`` let other wrapper runnables reuse the authenticated Microsoft
|
|
5
|
+
Graph client without re-implementing the device-code flow.
|
|
6
|
+
|
|
7
|
+
Importing this package never requires the optional ``[graph]`` dependencies —
|
|
8
|
+
``msal``/``httpx`` are imported lazily and only when a Graph call is made.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from runspec_windows.graph.auth import get_token
|
|
12
|
+
from runspec_windows.graph.client import graph_get
|
|
13
|
+
|
|
14
|
+
__all__ = ["graph_get", "get_token"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""_platform.py — Windows execution helpers shared by the system runnables.
|
|
2
|
+
|
|
3
|
+
The OS-specific work (shelling out to ``powershell``/``sc``/``netstat`` etc.)
|
|
4
|
+
lives behind these thin helpers so the pure parsers in each module stay
|
|
5
|
+
import-clean and unit-testable on any platform. Every Windows-only runnable
|
|
6
|
+
calls :func:`ensure_windows` first, so running one on a non-Windows host returns
|
|
7
|
+
a structured JSON error instead of a traceback.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
IS_WINDOWS = sys.platform == "win32"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ensure_windows() -> None:
|
|
20
|
+
"""Emit a JSON error and exit if not running on Windows.
|
|
21
|
+
|
|
22
|
+
Keeps non-Windows behaviour predictable (clean error, exit 1) rather than
|
|
23
|
+
crashing on a missing ``ipconfig``/``sc``/Win32 API.
|
|
24
|
+
"""
|
|
25
|
+
if not IS_WINDOWS:
|
|
26
|
+
print(json.dumps({"error": "This runnable requires Windows", "platform": sys.platform}))
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fail(message: str, **extra: object) -> None:
|
|
31
|
+
"""Print ``{"error": message, **extra}`` as JSON and exit non-zero."""
|
|
32
|
+
print(json.dumps({"error": message, **extra}))
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_powershell(script: str, timeout: int = 30) -> str:
|
|
37
|
+
"""Run a PowerShell snippet non-interactively and return its stdout.
|
|
38
|
+
|
|
39
|
+
Raises ``RuntimeError`` with stderr on a non-zero exit so callers can turn
|
|
40
|
+
it into a JSON error.
|
|
41
|
+
"""
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
timeout=timeout,
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
errors="replace",
|
|
49
|
+
)
|
|
50
|
+
if result.returncode != 0:
|
|
51
|
+
raise RuntimeError(result.stderr.strip() or f"powershell exited {result.returncode}")
|
|
52
|
+
return result.stdout
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run_cmd(args: list[str], timeout: int = 30, check: bool = False) -> subprocess.CompletedProcess[str]:
|
|
56
|
+
"""Run a console command and return the completed process (text mode)."""
|
|
57
|
+
return subprocess.run(
|
|
58
|
+
args,
|
|
59
|
+
capture_output=True,
|
|
60
|
+
text=True,
|
|
61
|
+
timeout=timeout,
|
|
62
|
+
check=check,
|
|
63
|
+
encoding="utf-8",
|
|
64
|
+
errors="replace",
|
|
65
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Event log runnables: query-eventlog, recent-errors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import runspec as rs
|
|
8
|
+
|
|
9
|
+
from runspec_windows import _platform
|
|
10
|
+
|
|
11
|
+
# Get-WinEvent numeric levels.
|
|
12
|
+
_LEVELS = {"critical": 1, "error": 2, "warning": 3, "information": 4}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_events(data: object) -> list[dict]:
|
|
16
|
+
"""Reshape parsed ``Get-WinEvent | ConvertTo-Json`` output into rows."""
|
|
17
|
+
if isinstance(data, dict):
|
|
18
|
+
items = [data]
|
|
19
|
+
elif isinstance(data, list):
|
|
20
|
+
items = [d for d in data if isinstance(d, dict)]
|
|
21
|
+
else:
|
|
22
|
+
items = []
|
|
23
|
+
rows = []
|
|
24
|
+
for item in items:
|
|
25
|
+
message = item.get("message") or ""
|
|
26
|
+
if isinstance(message, str) and len(message) > 500:
|
|
27
|
+
message = message[:500] + "…"
|
|
28
|
+
rows.append(
|
|
29
|
+
{
|
|
30
|
+
"time": item.get("time"),
|
|
31
|
+
"level": item.get("level"),
|
|
32
|
+
"id": item.get("Id"),
|
|
33
|
+
"provider": item.get("ProviderName"),
|
|
34
|
+
"message": message,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
return rows
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _select() -> str:
|
|
41
|
+
return "Select-Object @{N='time';E={$_.TimeCreated.ToString('s')}},@{N='level';E={$_.LevelDisplayName}},Id,ProviderName,@{N='message';E={$_.Message}}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _query(log: str, count: int, level: str | None) -> list[dict]:
|
|
45
|
+
filt = f"LogName='{log}'"
|
|
46
|
+
if level and level != "all":
|
|
47
|
+
filt += f"; Level={_LEVELS[level]}"
|
|
48
|
+
script = f"Get-WinEvent -FilterHashtable @{{{filt}}} -MaxEvents {count} -ErrorAction Stop | {_select()} | ConvertTo-Json -Compress"
|
|
49
|
+
out = _platform.run_powershell(script).strip()
|
|
50
|
+
return normalize_events(json.loads(out)) if out else []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main_query_eventlog() -> None:
|
|
54
|
+
spec = rs.parse("query-eventlog")
|
|
55
|
+
_platform.ensure_windows()
|
|
56
|
+
log = str(spec.log)
|
|
57
|
+
level = str(spec.level)
|
|
58
|
+
count = int(spec.count)
|
|
59
|
+
try:
|
|
60
|
+
print(json.dumps({"log": log, "level": level, "events": _query(log, count, level)}))
|
|
61
|
+
except RuntimeError as e:
|
|
62
|
+
# Get-WinEvent raises "No events were found" when the filter matches nothing.
|
|
63
|
+
if "No events were found" in str(e):
|
|
64
|
+
print(json.dumps({"log": log, "level": level, "events": []}))
|
|
65
|
+
else:
|
|
66
|
+
_platform.fail(str(e), log=log)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
_platform.fail(str(e), log=log)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main_recent_errors() -> None:
|
|
72
|
+
spec = rs.parse("recent-errors")
|
|
73
|
+
_platform.ensure_windows()
|
|
74
|
+
count = int(spec.count)
|
|
75
|
+
result: dict[str, list[dict]] = {}
|
|
76
|
+
try:
|
|
77
|
+
for log in ("System", "Application"):
|
|
78
|
+
try:
|
|
79
|
+
result[log] = _query(log, count, "error")
|
|
80
|
+
except RuntimeError as e:
|
|
81
|
+
if "No events were found" in str(e):
|
|
82
|
+
result[log] = []
|
|
83
|
+
else:
|
|
84
|
+
raise
|
|
85
|
+
print(json.dumps(result))
|
|
86
|
+
except Exception as e:
|
|
87
|
+
_platform.fail(str(e))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Microsoft Graph integration for runspec-windows (Outlook + Teams).
|
|
2
|
+
|
|
3
|
+
Pure HTTP via ``httpx`` + delegated auth via ``msal`` device-code flow — no
|
|
4
|
+
pywin32 and no client secret. The optional dependencies live behind the
|
|
5
|
+
``[graph]`` extra; importing this package never requires them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GraphError(Exception):
|
|
12
|
+
"""Base error for Graph runnables (rendered as a JSON ``error`` payload)."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GraphDepError(GraphError):
|
|
16
|
+
"""The optional ``[graph]`` dependencies (msal/httpx) are not installed."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GraphConfigError(GraphError):
|
|
20
|
+
"""No Azure client id configured (RUNSPEC_GRAPH_CLIENT_ID / graph.toml)."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GraphAuthError(GraphError):
|
|
24
|
+
"""No cached credentials — the user must run ``graph-login`` first."""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""_emit.py — shared JSON output + error handling for Graph runnables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from runspec_windows import _platform
|
|
10
|
+
from runspec_windows.graph import GraphError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_graph(produce: Callable[[], Any]) -> None:
|
|
14
|
+
"""Print ``produce()`` as JSON, turning Graph errors into a JSON error payload."""
|
|
15
|
+
try:
|
|
16
|
+
print(json.dumps(produce()))
|
|
17
|
+
except GraphError as e:
|
|
18
|
+
_platform.fail(str(e), kind=type(e).__name__)
|
|
19
|
+
except Exception as e: # network/JSON/etc.
|
|
20
|
+
_platform.fail(str(e))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Account runnables (Microsoft Graph): graph-login, graph-whoami."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import runspec as rs
|
|
8
|
+
|
|
9
|
+
from runspec_windows import _platform
|
|
10
|
+
from runspec_windows.graph import GraphError
|
|
11
|
+
from runspec_windows.graph._emit import run_graph
|
|
12
|
+
from runspec_windows.graph.auth import device_login
|
|
13
|
+
from runspec_windows.graph.client import graph_get
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main_graph_login() -> None:
|
|
17
|
+
rs.parse("graph-login")
|
|
18
|
+
try:
|
|
19
|
+
result = device_login()
|
|
20
|
+
claims = result.get("id_token_claims", {})
|
|
21
|
+
print(
|
|
22
|
+
json.dumps(
|
|
23
|
+
{
|
|
24
|
+
"signed_in": True,
|
|
25
|
+
"user": claims.get("preferred_username") or claims.get("name"),
|
|
26
|
+
"scopes": result.get("scope"),
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
except GraphError as e:
|
|
31
|
+
_platform.fail(str(e), kind=type(e).__name__)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
_platform.fail(str(e))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main_graph_whoami() -> None:
|
|
37
|
+
rs.parse("graph-whoami")
|
|
38
|
+
run_graph(lambda: _whoami(graph_get("/me", params={"$select": "displayName,userPrincipalName,mail,jobTitle,id"})))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _whoami(me: dict) -> dict:
|
|
42
|
+
return {
|
|
43
|
+
"display_name": me.get("displayName"),
|
|
44
|
+
"user_principal_name": me.get("userPrincipalName"),
|
|
45
|
+
"mail": me.get("mail"),
|
|
46
|
+
"job_title": me.get("jobTitle"),
|
|
47
|
+
"id": me.get("id"),
|
|
48
|
+
}
|