sentinelmeet-v2 2.0.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.
- sentinelmeet_v2-2.0.0/.gitignore +4 -0
- sentinelmeet_v2-2.0.0/LICENSE +9 -0
- sentinelmeet_v2-2.0.0/PKG-INFO +60 -0
- sentinelmeet_v2-2.0.0/README.md +36 -0
- sentinelmeet_v2-2.0.0/pyproject.toml +39 -0
- sentinelmeet_v2-2.0.0/src/sentinelmeet_installer/__init__.py +2 -0
- sentinelmeet_v2-2.0.0/src/sentinelmeet_installer/cli.py +311 -0
- sentinelmeet_v2-2.0.0/tests/test_cli.py +35 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Apocys
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sentinelmeet-v2
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Installer for SentinelMeet v2: private, on-device meeting transcription, live translation and speaker ID for macOS (Apple Silicon). `pip install sentinelmeet-v2 && sentinelmeet install`.
|
|
5
|
+
Project-URL: Homepage, https://spawnkit.ai/#sentinelmeet
|
|
6
|
+
Project-URL: Releases, https://spawnkit.ai/sentinelmeet/
|
|
7
|
+
Author: Apocys
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: diarization,installer,meetings,sentinelos,transcription,translation,whisper
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: MacOS X
|
|
13
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
19
|
+
Classifier: Topic :: Office/Business
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# sentinelmeet-v2
|
|
26
|
+
|
|
27
|
+
Installer for **SentinelMeet v2**, a private meeting assistant for macOS by [SpawnKit](https://spawnkit.ai/#sentinelmeet).
|
|
28
|
+
Live transcription (Whisper large-v3-turbo), live translation, speaker identification and summaries, processed on your Mac.
|
|
29
|
+
Cloud summaries and CRM publication are optional and off until you turn them on.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install sentinelmeet-v2
|
|
33
|
+
sentinelmeet install --launch
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The command downloads the latest build from `spawnkit.ai/sentinelmeet/`, checks its SHA-256,
|
|
37
|
+
and installs `/Applications/SentinelMeet v2.app`. An existing SentinelMeet v1 install is left untouched.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
| Command | What it does |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `sentinelmeet install [--version v2.0.0] [-y] [--launch]` | Install (or update to) a release |
|
|
44
|
+
| `sentinelmeet status` | Installed version, downloaded models, latest release |
|
|
45
|
+
| `sentinelmeet open` | Launch the app |
|
|
46
|
+
| `sentinelmeet uninstall [--purge]` | Remove the app; `--purge` also deletes meetings, models, settings |
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
- **macOS 13+ on Apple Silicon** (M1 or later). Windows is coming; Linux is not planned yet.
|
|
51
|
+
- Python 3.9 or newer for this installer.
|
|
52
|
+
- About 5 GB free for the speech and summary models, downloaded on first launch.
|
|
53
|
+
- macOS asks for **Microphone** and **Screen Recording** (needed to hear the other side of a call).
|
|
54
|
+
|
|
55
|
+
## Notes
|
|
56
|
+
|
|
57
|
+
- This build is not notarized by Apple yet. The installer removes the download quarantine flag so macOS lets it open.
|
|
58
|
+
- The app bundles FFmpeg 9.0.1 (static arm64 build from martin-riedl.de), licensed under the GPL v3. Source: https://ffmpeg.org/download.html
|
|
59
|
+
|
|
60
|
+
Support: support@spawnkit.ai
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# sentinelmeet-v2
|
|
2
|
+
|
|
3
|
+
Installer for **SentinelMeet v2**, a private meeting assistant for macOS by [SpawnKit](https://spawnkit.ai/#sentinelmeet).
|
|
4
|
+
Live transcription (Whisper large-v3-turbo), live translation, speaker identification and summaries, processed on your Mac.
|
|
5
|
+
Cloud summaries and CRM publication are optional and off until you turn them on.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install sentinelmeet-v2
|
|
9
|
+
sentinelmeet install --launch
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The command downloads the latest build from `spawnkit.ai/sentinelmeet/`, checks its SHA-256,
|
|
13
|
+
and installs `/Applications/SentinelMeet v2.app`. An existing SentinelMeet v1 install is left untouched.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
| Command | What it does |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `sentinelmeet install [--version v2.0.0] [-y] [--launch]` | Install (or update to) a release |
|
|
20
|
+
| `sentinelmeet status` | Installed version, downloaded models, latest release |
|
|
21
|
+
| `sentinelmeet open` | Launch the app |
|
|
22
|
+
| `sentinelmeet uninstall [--purge]` | Remove the app; `--purge` also deletes meetings, models, settings |
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- **macOS 13+ on Apple Silicon** (M1 or later). Windows is coming; Linux is not planned yet.
|
|
27
|
+
- Python 3.9 or newer for this installer.
|
|
28
|
+
- About 5 GB free for the speech and summary models, downloaded on first launch.
|
|
29
|
+
- macOS asks for **Microphone** and **Screen Recording** (needed to hear the other side of a call).
|
|
30
|
+
|
|
31
|
+
## Notes
|
|
32
|
+
|
|
33
|
+
- This build is not notarized by Apple yet. The installer removes the download quarantine flag so macOS lets it open.
|
|
34
|
+
- The app bundles FFmpeg 9.0.1 (static arm64 build from martin-riedl.de), licensed under the GPL v3. Source: https://ffmpeg.org/download.html
|
|
35
|
+
|
|
36
|
+
Support: support@spawnkit.ai
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sentinelmeet-v2"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Installer for SentinelMeet v2: private, on-device meeting transcription, live translation and speaker ID for macOS (Apple Silicon). `pip install sentinelmeet-v2 && sentinelmeet install`."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Apocys" }]
|
|
13
|
+
keywords = ["meetings", "transcription", "whisper", "translation", "diarization", "installer", "sentinelos"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: MacOS X",
|
|
17
|
+
"Environment :: Win32 (MS Windows)",
|
|
18
|
+
"Intended Audience :: End Users/Desktop",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Operating System :: Microsoft :: Windows",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: Office/Business",
|
|
23
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
24
|
+
]
|
|
25
|
+
# stdlib only: urllib, json, subprocess, hashlib, pathlib, tempfile
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://spawnkit.ai/#sentinelmeet"
|
|
30
|
+
Releases = "https://spawnkit.ai/sentinelmeet/"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
sentinelmeet = "sentinelmeet_installer.cli:main"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = ["pytest>=7"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/sentinelmeet_installer"]
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""SentinelMeet installer CLI.
|
|
2
|
+
|
|
3
|
+
sentinelmeet install [--version vX.Y.Z] [--yes]
|
|
4
|
+
sentinelmeet uninstall
|
|
5
|
+
sentinelmeet status
|
|
6
|
+
sentinelmeet open
|
|
7
|
+
|
|
8
|
+
Pure stdlib. Downloads the signed release asset from GitHub Releases, verifies
|
|
9
|
+
its SHA-256 against the release's SHA256SUMS when present, and installs:
|
|
10
|
+
macOS → /Applications/SentinelMeet.app (from the .dmg)
|
|
11
|
+
Windows → runs the NSIS setup.exe
|
|
12
|
+
Linux is not supported by the app yet (system-audio capture is macOS/Windows).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import platform
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import tempfile
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.request
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
REPO = os.environ.get("SENTINELMEET_REPO", "apocys/meeting-speech")
|
|
30
|
+
API = f"https://api.github.com/repos/{REPO}/releases"
|
|
31
|
+
# Public download feed (same shape as a GitHub release: tag_name + assets[]). Set SENTINELMEET_FEED="" to use GitHub.
|
|
32
|
+
FEED = os.environ.get("SENTINELMEET_FEED", "https://spawnkit.ai/sentinelmeet/releases/latest.json")
|
|
33
|
+
UA = {"User-Agent": "sentinelmeet-installer", "Accept": "application/vnd.github+json"}
|
|
34
|
+
APP_NAME = "SentinelMeet v2"
|
|
35
|
+
MAC_APP = Path("/Applications") / f"{APP_NAME}.app"
|
|
36
|
+
MAC_DATA = Path.home() / "Library/Application Support/com.apocys.sentinelmeet-v2"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class InstallError(RuntimeError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ----------------------------------------------------------------------------- github
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_json(url: str) -> dict:
|
|
47
|
+
req = urllib.request.Request(url, headers=UA)
|
|
48
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
49
|
+
return json.loads(r.read().decode())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def fetch_release(version: str | None) -> dict:
|
|
53
|
+
if FEED:
|
|
54
|
+
base = FEED.rsplit("/", 1)[0]
|
|
55
|
+
url = FEED if not version else f"{base}/{version if version.startswith('v') else 'v' + version}.json"
|
|
56
|
+
try:
|
|
57
|
+
return _get_json(url)
|
|
58
|
+
except urllib.error.HTTPError as e:
|
|
59
|
+
if e.code == 404:
|
|
60
|
+
raise InstallError(f"No release found ({'latest' if not version else version}) at {url}") from None
|
|
61
|
+
raise InstallError(f"Download feed error: {e}") from None
|
|
62
|
+
url = f"{API}/latest" if not version else f"{API}/tags/{version if version.startswith('v') else 'v' + version}"
|
|
63
|
+
try:
|
|
64
|
+
return _get_json(url)
|
|
65
|
+
except urllib.error.HTTPError as e:
|
|
66
|
+
if e.code == 404:
|
|
67
|
+
raise InstallError(
|
|
68
|
+
f"No release found ({'latest' if not version else version}) on github.com/{REPO}. "
|
|
69
|
+
"Check https://github.com/%s/releases" % REPO
|
|
70
|
+
) from None
|
|
71
|
+
raise InstallError(f"GitHub API error: {e}") from None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def pick_asset(release: dict, system: str, machine: str) -> dict:
|
|
75
|
+
"""Choose the asset for this OS/arch. Names follow Tauri's bundler."""
|
|
76
|
+
assets = release.get("assets", [])
|
|
77
|
+
names = [a["name"] for a in assets]
|
|
78
|
+
|
|
79
|
+
def find(*needles: str) -> dict | None:
|
|
80
|
+
for a in assets:
|
|
81
|
+
n = a["name"].lower()
|
|
82
|
+
if all(x in n for x in needles):
|
|
83
|
+
return a
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
if system == "Darwin":
|
|
87
|
+
if machine not in ("arm64", "aarch64"):
|
|
88
|
+
raise InstallError("SentinelMeet ships for Apple Silicon only (M1 and later).")
|
|
89
|
+
a = find("aarch64", ".dmg") or find("arm64", ".dmg")
|
|
90
|
+
elif system == "Windows":
|
|
91
|
+
a = find("x64", "setup", ".exe") or find("x64", ".msi")
|
|
92
|
+
else:
|
|
93
|
+
raise InstallError("Linux is not supported yet (system-audio capture is macOS/Windows only).")
|
|
94
|
+
if not a:
|
|
95
|
+
raise InstallError(f"No asset for {system}/{machine} in release {release.get('tag_name')}. Assets: {names}")
|
|
96
|
+
return a
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def fetch_sha256sums(release: dict) -> dict[str, str]:
|
|
100
|
+
for a in release.get("assets", []):
|
|
101
|
+
if a["name"].upper().startswith("SHA256SUMS"):
|
|
102
|
+
with urllib.request.urlopen(urllib.request.Request(a["browser_download_url"], headers=UA), timeout=30) as r:
|
|
103
|
+
out = {}
|
|
104
|
+
for line in r.read().decode().splitlines():
|
|
105
|
+
parts = line.split()
|
|
106
|
+
if len(parts) >= 2:
|
|
107
|
+
out[parts[-1].lstrip("*")] = parts[0].lower()
|
|
108
|
+
return out
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ----------------------------------------------------------------------------- download
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def download(url: str, dest: Path, expected_size: int | None = None) -> None:
|
|
116
|
+
req = urllib.request.Request(url, headers={"User-Agent": UA["User-Agent"]})
|
|
117
|
+
with urllib.request.urlopen(req, timeout=60) as r, open(dest, "wb") as f:
|
|
118
|
+
total = expected_size or int(r.headers.get("Content-Length") or 0)
|
|
119
|
+
done = 0
|
|
120
|
+
last = -1
|
|
121
|
+
while True:
|
|
122
|
+
chunk = r.read(1 << 20)
|
|
123
|
+
if not chunk:
|
|
124
|
+
break
|
|
125
|
+
f.write(chunk)
|
|
126
|
+
done += len(chunk)
|
|
127
|
+
if total:
|
|
128
|
+
pct = done * 100 // total
|
|
129
|
+
if pct != last and pct % 5 == 0:
|
|
130
|
+
sys.stdout.write(f"\r downloading {dest.name} {pct:3d}% ({done // (1 << 20)} / {total // (1 << 20)} MB)")
|
|
131
|
+
sys.stdout.flush()
|
|
132
|
+
last = pct
|
|
133
|
+
sys.stdout.write("\n")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def sha256(path: Path) -> str:
|
|
137
|
+
h = hashlib.sha256()
|
|
138
|
+
with open(path, "rb") as f:
|
|
139
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
140
|
+
h.update(chunk)
|
|
141
|
+
return h.hexdigest()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ----------------------------------------------------------------------------- install
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def install_macos_dmg(dmg: Path, yes: bool) -> Path:
|
|
148
|
+
mnt = Path(tempfile.mkdtemp(prefix="sentinelmeet-dmg-"))
|
|
149
|
+
subprocess.run(["hdiutil", "attach", str(dmg), "-nobrowse", "-quiet", "-mountpoint", str(mnt)], check=True)
|
|
150
|
+
try:
|
|
151
|
+
apps = list(mnt.glob("*.app"))
|
|
152
|
+
if not apps:
|
|
153
|
+
raise InstallError("No .app inside the dmg")
|
|
154
|
+
src = apps[0]
|
|
155
|
+
# Keep the bundle's own name ("SentinelMeet v2.app") so v1 is never overwritten.
|
|
156
|
+
dest = Path("/Applications") / src.name
|
|
157
|
+
if dest.exists():
|
|
158
|
+
if not yes and not _confirm(f"{dest} exists. Replace it?"):
|
|
159
|
+
raise InstallError("Aborted.")
|
|
160
|
+
subprocess.run(["pkill", "-x", "sentinelmeet"], check=False)
|
|
161
|
+
shutil.rmtree(dest)
|
|
162
|
+
shutil.copytree(src, dest, symlinks=True)
|
|
163
|
+
# Unsigned/ad-hoc builds carry the quarantine bit → Gatekeeper refuses.
|
|
164
|
+
subprocess.run(["xattr", "-dr", "com.apple.quarantine", str(dest)], check=False)
|
|
165
|
+
return dest
|
|
166
|
+
finally:
|
|
167
|
+
subprocess.run(["hdiutil", "detach", str(mnt), "-quiet"], check=False)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def install_windows(setup: Path, yes: bool) -> Path:
|
|
171
|
+
args = [str(setup)]
|
|
172
|
+
if yes:
|
|
173
|
+
args.append("/S") # NSIS silent
|
|
174
|
+
subprocess.run(args, check=True)
|
|
175
|
+
return Path(os.environ.get("LOCALAPPDATA", "")) / APP_NAME
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _confirm(q: str) -> bool:
|
|
179
|
+
return input(f"{q} [y/N] ").strip().lower() in ("y", "yes")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ----------------------------------------------------------------------------- commands
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def cmd_install(args: argparse.Namespace) -> int:
|
|
186
|
+
system, machine = platform.system(), platform.machine()
|
|
187
|
+
print(f"SentinelMeet installer — {system} {machine}")
|
|
188
|
+
rel = fetch_release(args.version)
|
|
189
|
+
asset = pick_asset(rel, system, machine)
|
|
190
|
+
tag = rel.get("tag_name")
|
|
191
|
+
print(f" release {tag}: {asset['name']} ({asset['size'] // (1 << 20)} MB)")
|
|
192
|
+
sums = fetch_sha256sums(rel)
|
|
193
|
+
|
|
194
|
+
with tempfile.TemporaryDirectory(prefix="sentinelmeet-") as td:
|
|
195
|
+
path = Path(td) / asset["name"]
|
|
196
|
+
download(asset["browser_download_url"], path, asset.get("size"))
|
|
197
|
+
digest = sha256(path)
|
|
198
|
+
if asset["name"] in sums:
|
|
199
|
+
if sums[asset["name"]] != digest:
|
|
200
|
+
raise InstallError("SHA-256 mismatch — download corrupted or tampered. Aborting.")
|
|
201
|
+
print(" sha256 verified ✓")
|
|
202
|
+
else:
|
|
203
|
+
print(f" sha256 {digest[:16]}… (no SHA256SUMS in this release — not verified)")
|
|
204
|
+
|
|
205
|
+
if system == "Darwin":
|
|
206
|
+
dest = install_macos_dmg(path, args.yes)
|
|
207
|
+
else:
|
|
208
|
+
dest = install_windows(path, args.yes)
|
|
209
|
+
|
|
210
|
+
print(f"\n✅ Installed {APP_NAME} {tag} → {dest}")
|
|
211
|
+
print("First launch downloads the speech models (~1.6 GB Whisper turbo + ~2.7 GB Qwen) — one time.")
|
|
212
|
+
if system == "Darwin":
|
|
213
|
+
print("macOS will ask for Microphone and Screen Recording (system audio) permissions.")
|
|
214
|
+
if args.launch:
|
|
215
|
+
subprocess.run(["open", str(dest)], check=False)
|
|
216
|
+
else:
|
|
217
|
+
print("Run: sentinelmeet open")
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def cmd_uninstall(args: argparse.Namespace) -> int:
|
|
222
|
+
if platform.system() != "Darwin":
|
|
223
|
+
print("On Windows use Settings › Apps › SentinelMeet › Uninstall.")
|
|
224
|
+
return 1
|
|
225
|
+
if not MAC_APP.exists():
|
|
226
|
+
print("Not installed.")
|
|
227
|
+
return 0
|
|
228
|
+
if not args.yes and not _confirm(f"Remove {MAC_APP}?"):
|
|
229
|
+
return 1
|
|
230
|
+
subprocess.run(["pkill", "-x", "sentinelmeet"], check=False)
|
|
231
|
+
shutil.rmtree(MAC_APP)
|
|
232
|
+
print("Removed app.")
|
|
233
|
+
if args.purge:
|
|
234
|
+
if MAC_DATA.exists() and (args.yes or _confirm(f"Also delete meetings, models and settings in {MAC_DATA}?")):
|
|
235
|
+
shutil.rmtree(MAC_DATA)
|
|
236
|
+
print("Removed data.")
|
|
237
|
+
else:
|
|
238
|
+
print(f"Meetings/models kept in {MAC_DATA} (use --purge to delete).")
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def cmd_status(_: argparse.Namespace) -> int:
|
|
243
|
+
system = platform.system()
|
|
244
|
+
if system == "Darwin":
|
|
245
|
+
installed = MAC_APP.exists()
|
|
246
|
+
ver = None
|
|
247
|
+
if installed:
|
|
248
|
+
try:
|
|
249
|
+
out = subprocess.run(
|
|
250
|
+
["defaults", "read", str(MAC_APP / "Contents/Info.plist"), "CFBundleShortVersionString"],
|
|
251
|
+
capture_output=True, text=True, check=True,
|
|
252
|
+
).stdout.strip()
|
|
253
|
+
ver = out
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
256
|
+
print(f"app: {'installed ' + (ver or '') if installed else 'not installed'} ({MAC_APP})")
|
|
257
|
+
models = MAC_DATA / "models"
|
|
258
|
+
if models.exists():
|
|
259
|
+
names = sorted(p.name for p in models.rglob("*") if p.is_file() and p.suffix in (".bin", ".gguf"))
|
|
260
|
+
print(f"models: {', '.join(names) if names else 'none yet'}")
|
|
261
|
+
db = MAC_DATA / "meeting_minutes.sqlite"
|
|
262
|
+
print(f"database: {'present' if db.exists() else 'none'} ({db})")
|
|
263
|
+
else:
|
|
264
|
+
print(f"{system}: use the app's Settings for status.")
|
|
265
|
+
try:
|
|
266
|
+
latest = fetch_release(None).get("tag_name")
|
|
267
|
+
print(f"latest: {latest}")
|
|
268
|
+
except InstallError as e:
|
|
269
|
+
print(f"latest: ? ({e})")
|
|
270
|
+
return 0
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def cmd_open(_: argparse.Namespace) -> int:
|
|
274
|
+
if platform.system() == "Darwin" and MAC_APP.exists():
|
|
275
|
+
subprocess.run(["open", str(MAC_APP)], check=False)
|
|
276
|
+
return 0
|
|
277
|
+
print("App not installed. Run: sentinelmeet install")
|
|
278
|
+
return 1
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def main(argv: list[str] | None = None) -> int:
|
|
282
|
+
p = argparse.ArgumentParser(prog="sentinelmeet", description="Install / manage the SentinelMeet desktop app.")
|
|
283
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
284
|
+
|
|
285
|
+
i = sub.add_parser("install", help="download and install the latest release")
|
|
286
|
+
i.add_argument("--version", help="specific tag, e.g. v0.5.0 (default: latest)")
|
|
287
|
+
i.add_argument("--yes", "-y", action="store_true", help="no prompts; replace existing install")
|
|
288
|
+
i.add_argument("--launch", action="store_true", help="open the app after installing")
|
|
289
|
+
i.set_defaults(fn=cmd_install)
|
|
290
|
+
|
|
291
|
+
u = sub.add_parser("uninstall", help="remove the app (keeps your data unless --purge)")
|
|
292
|
+
u.add_argument("--yes", "-y", action="store_true")
|
|
293
|
+
u.add_argument("--purge", action="store_true", help="also delete meetings, models and settings")
|
|
294
|
+
u.set_defaults(fn=cmd_uninstall)
|
|
295
|
+
|
|
296
|
+
sub.add_parser("status", help="installed version, models, latest release").set_defaults(fn=cmd_status)
|
|
297
|
+
sub.add_parser("open", help="launch the app").set_defaults(fn=cmd_open)
|
|
298
|
+
|
|
299
|
+
args = p.parse_args(argv)
|
|
300
|
+
try:
|
|
301
|
+
return args.fn(args)
|
|
302
|
+
except InstallError as e:
|
|
303
|
+
print(f"✗ {e}", file=sys.stderr)
|
|
304
|
+
return 2
|
|
305
|
+
except KeyboardInterrupt:
|
|
306
|
+
print("\naborted", file=sys.stderr)
|
|
307
|
+
return 130
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == "__main__":
|
|
311
|
+
sys.exit(main())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from sentinelmeet_installer import cli
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def rel(names):
|
|
6
|
+
return {"tag_name": "v0.5.0", "assets": [{"name": n, "size": 1, "browser_download_url": "x"} for n in names]}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_pick_macos_arm():
|
|
10
|
+
r = rel(["SentinelMeet_0.5.0_aarch64.dmg", "SentinelMeet_0.5.0_x64-setup.exe", "SHA256SUMS"])
|
|
11
|
+
assert cli.pick_asset(r, "Darwin", "arm64")["name"].endswith("aarch64.dmg")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_pick_windows():
|
|
15
|
+
r = rel(["SentinelMeet_0.5.0_aarch64.dmg", "SentinelMeet_0.5.0_x64-setup.exe"])
|
|
16
|
+
assert cli.pick_asset(r, "Windows", "AMD64")["name"].endswith("setup.exe")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_intel_mac_refused():
|
|
20
|
+
import pytest
|
|
21
|
+
with pytest.raises(cli.InstallError):
|
|
22
|
+
cli.pick_asset(rel(["SentinelMeet_0.5.0_aarch64.dmg"]), "Darwin", "x86_64")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_linux_refused():
|
|
26
|
+
import pytest
|
|
27
|
+
with pytest.raises(cli.InstallError):
|
|
28
|
+
cli.pick_asset(rel(["a.dmg"]), "Linux", "x86_64")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_cli_help_runs():
|
|
32
|
+
import pytest
|
|
33
|
+
with pytest.raises(SystemExit) as e:
|
|
34
|
+
cli.main(["--help"])
|
|
35
|
+
assert e.value.code == 0
|