typst-pkg-sync 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ """Local package manager for Typst."""
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
typst_pkg_sync/cli.py ADDED
@@ -0,0 +1,564 @@
1
+ """
2
+ typst-pkg-sync — Local Package Manager for Typst
3
+
4
+ Downloads tagged source files from GitHub, GitLab, and Codeberg
5
+ and installs them into the local Typst package directory structure:
6
+
7
+ <base_dir>/<namespace>/<package-name>/<version>/
8
+
9
+ The package name is read from the typst.toml file inside the
10
+ downloaded archive. Version tags may carry a 'v' prefix (e.g. "v1.0.0"),
11
+ which is stripped for the installation directory.
12
+
13
+ Requires Python 3.14+ (uses tomllib from the standard library).
14
+ No third-party dependencies.
15
+ """
16
+
17
+ import argparse
18
+ import http.client
19
+ import json
20
+ import os
21
+ import re
22
+ import shutil
23
+ import sys
24
+ import tarfile
25
+ import tempfile
26
+ import tomllib
27
+ import urllib.error
28
+ import urllib.parse
29
+ import urllib.request
30
+ import zipfile
31
+ from pathlib import Path
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Constants
35
+ # ---------------------------------------------------------------------------
36
+
37
+ CONFIG_DIR = Path.home() / ".config" / "lpmt"
38
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
39
+ DEFAULT_BASE_DIR = Path.home() / ".local/share/typst/packages"
40
+
41
+ # Version strings: optional 'v' prefix, then digits and dots,
42
+ # at least major.minor. Capture group 1 = version without prefix.
43
+ SEMVER_RE = re.compile(r"^v?(\d+\.\d+(?:\.\d+)?)$")
44
+
45
+ PLATFORMS = {
46
+ "github": {
47
+ "tags_api": "https://api.github.com/repos/{owner}/{repo}/tags",
48
+ "archive": "https://github.com/{owner}/{repo}/archive/refs/tags/{tag}.tar.gz",
49
+ "archive_format": "tar",
50
+ "auth_env": "GITHUB_TOKEN",
51
+ "auth_header": "Authorization",
52
+ "auth_prefix": "token ",
53
+ },
54
+ "gitlab": {
55
+ "base_url": "https://gitlab.com",
56
+ "tags_api": "{base_url}/api/v4/projects/{project_path}/repository/tags",
57
+ "archive": "{base_url}/api/v4/projects/{project_path}/repository/archive.tar.gz?sha={tag}",
58
+ "archive_format": "tar",
59
+ "auth_env": "GITLAB_TOKEN",
60
+ "auth_header": "PRIVATE-TOKEN",
61
+ "auth_prefix": "",
62
+ },
63
+ "codeberg": {
64
+ "tags_api": "https://codeberg.org/api/v1/repos/{owner}/{repo}/tags",
65
+ "archive": "https://codeberg.org/{owner}/{repo}/archive/{tag}.tar.gz",
66
+ "archive_format": "tar",
67
+ "auth_env": "CODEBERG_TOKEN",
68
+ "auth_header": "Authorization",
69
+ "auth_prefix": "token ",
70
+ },
71
+ }
72
+
73
+ DEFAULT_CONFIG = """\
74
+ # typst-pkg-sync — Local Package Manager for Typst
75
+ #
76
+ # Packages are installed to:
77
+ # <base_dir>/<namespace>/<package-name>/<version>/
78
+ #
79
+ # The <package-name> is read from the typst.toml file inside the
80
+ # downloaded archive, NOT from the repository name.
81
+ #
82
+ # Tags may be written with or without a 'v' prefix (e.g. "v1.0.0"
83
+ # or "1.0.0"). The prefix is stripped for the install directory.
84
+ #
85
+ # GitLab repositories may set base_url and token_env per repository.
86
+ # base_url defaults to https://gitlab.com; token_env defaults to GITLAB_TOKEN.
87
+
88
+ [settings]
89
+ base_dir = "~/.local/share/typst/packages"
90
+ namespace = "jp-packages"
91
+ skip_existing = true
92
+ verbose = true
93
+
94
+ [[repositories]]
95
+ platform = "gitlab"
96
+ base_url = "https://gitlab.gwdg.de"
97
+ owner = "ressler"
98
+ repo = "jps-university-assignments"
99
+ tags = ["0.2.0"]
100
+
101
+ [[repositories]]
102
+ platform = "codeberg"
103
+ owner = "ressler"
104
+ repo = "protokoll-template"
105
+ tags = "all"
106
+ """
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Logging
110
+ # ---------------------------------------------------------------------------
111
+
112
+
113
+ def log_info(msg: str) -> None:
114
+ print(f"\033[32m[INFO]\033[0m {msg}")
115
+
116
+
117
+ def log_warn(msg: str) -> None:
118
+ print(f"\033[33m[WARN]\033[0m {msg}")
119
+
120
+
121
+ def log_error(msg: str) -> None:
122
+ print(f"\033[31m[ERROR]\033[0m {msg}")
123
+
124
+
125
+ def log_debug(msg: str) -> None:
126
+ print(f"\033[34m[DEBUG]\033[0m {msg}")
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Tag handling
131
+ # ---------------------------------------------------------------------------
132
+
133
+
134
+ def normalize_tag(tag: str) -> str | None:
135
+ """
136
+ Normalize a tag string.
137
+
138
+ Accepts an optional 'v' prefix followed by digits and dots
139
+ (at least major.minor). Returns the version without the prefix,
140
+ or None if the tag does not match the pattern.
141
+ """
142
+ m = SEMVER_RE.match(tag)
143
+ return m.group(1) if m else None
144
+
145
+
146
+ def validate_tags(tags: object, repo: str) -> list[str] | str:
147
+ """
148
+ Validate the tags field from the config.
149
+
150
+ Allowed:
151
+ - the string "all"
152
+ - an array of version strings, with optional 'v' prefix
153
+ (e.g. "0.2.0", "v1.0.0", "1.0")
154
+
155
+ Returns either "all" or a list of normalized versions
156
+ (prefix stripped). Exits on invalid input.
157
+ """
158
+ if isinstance(tags, str):
159
+ if tags == "all":
160
+ return "all"
161
+ log_error(f'[{repo}] tags must be an array of versions or the string "all", got: "{tags}"')
162
+ sys.exit(1)
163
+
164
+ if isinstance(tags, list):
165
+ if not tags:
166
+ log_error(f"[{repo}] tags array is empty")
167
+ sys.exit(1)
168
+ normalized = []
169
+ for t in tags:
170
+ version = normalize_tag(t) if isinstance(t, str) else None
171
+ if version is None:
172
+ log_error(
173
+ f"[{repo}] invalid tag {t!r}: tags must consist of an "
174
+ f"optional 'v' prefix followed by digits and dots "
175
+ f'(e.g. "0.2.0" or "v1.0.0")'
176
+ )
177
+ sys.exit(1)
178
+ normalized.append(version)
179
+ return normalized
180
+
181
+ log_error(f'[{repo}] tags must be an array of versions or "all"')
182
+ sys.exit(1)
183
+
184
+
185
+ def filter_remote_tags(raw_tags: list[str], owner: str, repo: str) -> list[str]:
186
+ """
187
+ Filter remote tag names against the version pattern.
188
+
189
+ Tags that don't match (e.g. "release-candidate", "nightly")
190
+ are skipped with a warning. Matching tags are normalized
191
+ ('v' prefix stripped) and deduplicated while preserving order.
192
+ """
193
+ versions: list[str] = []
194
+ seen: set[str] = set()
195
+
196
+ for raw in raw_tags:
197
+ version = normalize_tag(raw)
198
+ if version is None:
199
+ log_warn(f"[{owner}/{repo}] skipping non-version tag: {raw!r}")
200
+ continue
201
+ if version in seen:
202
+ log_warn(
203
+ f"[{owner}/{repo}] tag {raw!r} maps to version {version!r}, "
204
+ f"which was already seen — skipping duplicate"
205
+ )
206
+ continue
207
+ seen.add(version)
208
+ versions.append(version)
209
+
210
+ return versions
211
+
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # Config handling
215
+ # ---------------------------------------------------------------------------
216
+
217
+
218
+ def create_default_config(config_file: Path) -> None:
219
+ config_file.parent.mkdir(parents=True, exist_ok=True)
220
+ config_file.write_text(DEFAULT_CONFIG)
221
+ log_info(f"Created default config at {config_file}")
222
+ log_info("Edit it to add your repositories, then re-run.")
223
+ sys.exit(0)
224
+
225
+
226
+ def load_config(config_file: Path = CONFIG_FILE) -> dict:
227
+ if not config_file.exists():
228
+ create_default_config(config_file)
229
+ try:
230
+ with open(config_file, "rb") as f:
231
+ return tomllib.load(f)
232
+ except tomllib.TOMLDecodeError as e:
233
+ log_error(f"Invalid TOML in {config_file}: {e}")
234
+ sys.exit(1)
235
+
236
+
237
+ def resolve_platform_config(platform: str, repo_cfg: dict) -> dict | None:
238
+ """Apply per-repository overrides to a platform's request settings."""
239
+ plat = dict(PLATFORMS[platform])
240
+ plat["name"] = platform
241
+
242
+ token_env = repo_cfg.get("token_env", plat["auth_env"])
243
+ if not isinstance(token_env, str) or not token_env:
244
+ log_error(f"Invalid token_env for repository '{repo_cfg.get('repo', '')}'")
245
+ return None
246
+ plat["auth_env"] = token_env
247
+
248
+ if "base_url" in repo_cfg:
249
+ if platform != "gitlab":
250
+ log_error("The 'base_url' option is only supported for GitLab repositories")
251
+ return None
252
+ base_url = repo_cfg["base_url"]
253
+ if not isinstance(base_url, str) or not base_url or base_url != base_url.strip():
254
+ log_error(f"Invalid GitLab base_url: {base_url!r}")
255
+ return None
256
+ try:
257
+ parsed = urllib.parse.urlsplit(base_url)
258
+ _ = parsed.port
259
+ except ValueError:
260
+ log_error(f"Invalid GitLab base_url: {base_url!r}")
261
+ return None
262
+ if (
263
+ parsed.scheme not in {"http", "https"}
264
+ or not parsed.hostname
265
+ or parsed.username is not None
266
+ or parsed.password is not None
267
+ or parsed.query
268
+ or parsed.fragment
269
+ ):
270
+ log_error(f"Invalid GitLab base_url: {base_url!r}")
271
+ return None
272
+ plat["base_url"] = base_url.rstrip("/")
273
+
274
+ return plat
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # HTTP helpers (stdlib only)
279
+ # ---------------------------------------------------------------------------
280
+
281
+
282
+ def make_request(url: str, plat: dict) -> bytes:
283
+ headers = {"User-Agent": "typst-pkg-sync/1.0"}
284
+
285
+ token = os.environ.get(plat["auth_env"], "")
286
+ if token:
287
+ headers[plat["auth_header"]] = plat["auth_prefix"] + token
288
+
289
+ req = urllib.request.Request(url, headers=headers)
290
+ with urllib.request.urlopen(req, timeout=60) as resp:
291
+ return resp.read()
292
+
293
+
294
+ def fetch_tags(plat: dict, owner: str, repo: str) -> list[str]:
295
+ """Fetch all tag names from the remote repository."""
296
+ url = plat["tags_api"].format(
297
+ base_url=plat.get("base_url", ""),
298
+ owner=owner,
299
+ repo=repo,
300
+ project_path=urllib.parse.quote(f"{owner}/{repo}", safe=""),
301
+ )
302
+ try:
303
+ data = json.loads(make_request(url, plat))
304
+ return [tag["name"] for tag in data]
305
+ except (http.client.HTTPException, OSError, ValueError, KeyError, TypeError) as e:
306
+ log_warn(f"Could not fetch tags for {owner}/{repo}: {e}")
307
+ return []
308
+
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # Archive handling
312
+ # ---------------------------------------------------------------------------
313
+
314
+
315
+ def extract_archive(archive_bytes: bytes, dest: Path, fmt: str) -> None:
316
+ """Extract a tar.gz or zip archive into dest, flattening the top dir."""
317
+ with tempfile.TemporaryDirectory() as tmp:
318
+ tmp_path = Path(tmp) / ("archive.tar.gz" if fmt == "tar" else "archive.zip")
319
+ tmp_path.write_bytes(archive_bytes)
320
+
321
+ extract_dir = Path(tmp) / "extracted"
322
+ extract_dir.mkdir()
323
+
324
+ if fmt == "tar":
325
+ with tarfile.open(tmp_path, "r:gz") as tf:
326
+ tf.extractall(extract_dir, filter="data")
327
+ else:
328
+ with zipfile.ZipFile(tmp_path) as zf:
329
+ zf.extractall(extract_dir)
330
+
331
+ entries = list(extract_dir.iterdir())
332
+ source = entries[0] if (len(entries) == 1 and entries[0].is_dir()) else extract_dir
333
+
334
+ for item in source.iterdir():
335
+ shutil.move(str(item), str(dest / item.name))
336
+
337
+
338
+ def read_package_name(package_dir: Path) -> str | None:
339
+ """
340
+ Read the package name from typst.toml in the extracted package.
341
+
342
+ Expected structure in typst.toml:
343
+ [package]
344
+ name = "..."
345
+ """
346
+ typst_toml = package_dir / "typst.toml"
347
+ if not typst_toml.exists():
348
+ return None
349
+ try:
350
+ with open(typst_toml, "rb") as f:
351
+ data = tomllib.load(f)
352
+ name = data.get("package", {}).get("name")
353
+ return name if isinstance(name, str) and name else None
354
+ except (tomllib.TOMLDecodeError, OSError) as e:
355
+ log_warn(f"Could not parse typst.toml: {e}")
356
+ return None
357
+
358
+
359
+ # ---------------------------------------------------------------------------
360
+ # Download & install
361
+ # ---------------------------------------------------------------------------
362
+
363
+
364
+ def download_and_install(
365
+ plat: dict,
366
+ owner: str,
367
+ repo: str,
368
+ tag: str,
369
+ base_dir: Path,
370
+ namespace: str,
371
+ skip_existing: bool,
372
+ ) -> bool:
373
+ """
374
+ Download one tag of one repository and install it.
375
+
376
+ 'tag' must already be normalized (no 'v' prefix) — it is used both
377
+ to build the archive URL (with prefix re-added if the remote uses
378
+ one) and as the version directory name.
379
+
380
+ The package name is read from typst.toml inside the archive.
381
+ Returns True on success (including skip), False on failure.
382
+ """
383
+ platform = plat["name"]
384
+
385
+ # Try the plain tag first; if it 404s, retry with a 'v' prefix.
386
+ # This handles remotes that tag as "v1.0.0" while the config
387
+ # (or the filtered remote list) uses the normalized form.
388
+ candidates = [tag]
389
+ if not tag.startswith("v"):
390
+ candidates.append(f"v{tag}")
391
+
392
+ archive_bytes: bytes | None = None
393
+ used_tag: str | None = None
394
+
395
+ for candidate in candidates:
396
+ url = plat["archive"].format(
397
+ base_url=plat.get("base_url", ""),
398
+ owner=owner,
399
+ project_path=urllib.parse.quote(f"{owner}/{repo}", safe=""),
400
+ repo=urllib.parse.quote(repo, safe=""),
401
+ tag=urllib.parse.quote(candidate, safe=""),
402
+ )
403
+ try:
404
+ log_info(f"⬇ Downloading {owner}/{repo}@{candidate} from {platform}...")
405
+ archive_bytes = make_request(url, plat)
406
+ used_tag = candidate
407
+ break
408
+ except urllib.error.HTTPError as e:
409
+ if e.code == 404 and candidate != candidates[-1]:
410
+ log_debug(f"Tag '{candidate}' not found, trying next candidate...")
411
+ continue
412
+ log_error(f"HTTP {e.code} for {url}")
413
+ return False
414
+ except (http.client.HTTPException, OSError, ValueError) as e:
415
+ log_error(f"Download failed for {owner}/{repo}@{candidate}: {e}")
416
+ return False
417
+
418
+ if archive_bytes is None or used_tag is None:
419
+ log_error(f"Could not download {owner}/{repo}@{tag}")
420
+ return False
421
+
422
+ # Extract to a staging dir first so we can read typst.toml
423
+ with tempfile.TemporaryDirectory() as tmp:
424
+ staging = Path(tmp) / "staging"
425
+ staging.mkdir()
426
+
427
+ try:
428
+ extract_archive(archive_bytes, staging, plat["archive_format"])
429
+ except (
430
+ tarfile.TarError,
431
+ zipfile.BadZipFile,
432
+ OSError,
433
+ EOFError,
434
+ ) as e:
435
+ log_error(f"Extraction failed for {owner}/{repo}@{used_tag}: {e}")
436
+ return False
437
+
438
+ # Read the package name from typst.toml
439
+ package_name = read_package_name(staging)
440
+ if package_name is None:
441
+ log_error(
442
+ f"No valid typst.toml with [package] name found in "
443
+ f"{owner}/{repo}@{used_tag} — cannot determine package name"
444
+ )
445
+ return False
446
+
447
+ if package_name != repo:
448
+ log_debug(f"Package name '{package_name}' differs from repo '{repo}'")
449
+
450
+ # Final destination: normalized tag (no 'v' prefix) as version dir
451
+ dest = base_dir / namespace / package_name / tag
452
+
453
+ if dest.exists():
454
+ if skip_existing:
455
+ log_warn(f"⊘ Already exists: {dest}")
456
+ return True
457
+ log_warn(f"Overwriting existing: {dest}")
458
+ shutil.rmtree(dest)
459
+
460
+ dest.mkdir(parents=True, exist_ok=True)
461
+
462
+ try:
463
+ for item in staging.iterdir():
464
+ shutil.move(str(item), str(dest / item.name))
465
+ except OSError as e:
466
+ log_error(f"Install failed: {e}")
467
+ shutil.rmtree(dest, ignore_errors=True)
468
+ return False
469
+
470
+ log_info(f"✅ Installed {package_name}@{tag} → {dest}")
471
+ return True
472
+
473
+
474
+ # ---------------------------------------------------------------------------
475
+ # Sync logic
476
+ # ---------------------------------------------------------------------------
477
+
478
+
479
+ def sync_repository(repo_cfg: dict, settings: dict) -> bool:
480
+ platform = repo_cfg.get("platform", "").lower()
481
+ owner = repo_cfg.get("owner", "")
482
+ repo = repo_cfg.get("repo", "")
483
+ raw_tags = repo_cfg.get("tags")
484
+
485
+ if platform not in PLATFORMS:
486
+ log_error(f"Unknown platform '{platform}' for repo '{repo}'")
487
+ return False
488
+ if not owner or not repo:
489
+ log_error(f"Missing 'owner' or 'repo' in config entry: {repo_cfg}")
490
+ return False
491
+
492
+ plat = resolve_platform_config(platform, repo_cfg)
493
+ if plat is None:
494
+ return False
495
+
496
+ tags = validate_tags(raw_tags, repo)
497
+
498
+ # "all": query remote tags and filter them for the version pattern
499
+ if tags == "all":
500
+ raw_remote = fetch_tags(plat, owner, repo)
501
+ if not raw_remote:
502
+ log_error(f"No tags found for {owner}/{repo}")
503
+ return False
504
+ tags = filter_remote_tags(raw_remote, owner, repo)
505
+ if not tags:
506
+ log_error(f"No tags matching the version pattern found for {owner}/{repo}")
507
+ return False
508
+ log_info(f'[{owner}/{repo}] tags = "all" → {len(tags)} version(s): {tags}')
509
+
510
+ base_dir = Path(settings.get("base_dir", str(DEFAULT_BASE_DIR))).expanduser()
511
+ namespace = settings.get("namespace", "preview")
512
+ skip_existing = settings.get("skip_existing", True)
513
+
514
+ ok = 0
515
+ for tag in tags:
516
+ if download_and_install(plat, owner, repo, tag, base_dir, namespace, skip_existing):
517
+ ok += 1
518
+
519
+ log_info(f"── {owner}/{repo}: {ok}/{len(tags)} version(s) synced")
520
+ return ok == len(tags)
521
+
522
+
523
+ def main(argv: list[str] | None = None) -> None:
524
+ parser = argparse.ArgumentParser(description="Sync Typst packages from Git repositories")
525
+ parser.add_argument(
526
+ "--config",
527
+ type=Path,
528
+ default=CONFIG_FILE,
529
+ help=f"config file path (default: {CONFIG_FILE})",
530
+ )
531
+ args = parser.parse_args(argv)
532
+ config_file = args.config.expanduser()
533
+
534
+ config = load_config(config_file)
535
+ settings = config.get("settings", {})
536
+ repos = config.get("repositories", [])
537
+
538
+ if not repos:
539
+ log_warn("No [[repositories]] defined in config.")
540
+ log_info(f"Edit {config_file} to add some.")
541
+ sys.exit(0)
542
+
543
+ namespace = settings.get("namespace", "preview")
544
+ base_dir = Path(settings.get("base_dir", str(DEFAULT_BASE_DIR))).expanduser()
545
+
546
+ log_info("📦 typst-pkg-sync — Typst Package Sync")
547
+ log_info(f"📁 Base: {base_dir}")
548
+ log_info(f"🏷 Namespace: {namespace}")
549
+ print()
550
+
551
+ failures = 0
552
+ for repo_cfg in repos:
553
+ if not sync_repository(repo_cfg, settings):
554
+ failures += 1
555
+
556
+ print()
557
+ if failures:
558
+ log_error(f"{failures} repository(ies) had errors")
559
+ sys.exit(1)
560
+ log_info("🎉 All done!")
561
+
562
+
563
+ if __name__ == "__main__":
564
+ main()
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: typst-pkg-sync
3
+ Version: 0.1.0
4
+ Summary: Sync tagged Typst packages from GitHub, GitLab, and Codeberg
5
+ Author: Jan-Philipp Reßler
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.14
9
+ Project-URL: Repository, https://codeberg.org/ressler/lpmt
10
+ Project-URL: Issues, https://codeberg.org/ressler/lpmt/issues
11
+ Description-Content-Type: text/markdown
12
+
13
+ # typst-pkg-sync
14
+
15
+ typst-pkg-sync downloads tagged Typst packages from GitHub, GitLab (including
16
+ self-hosted instances), and Codeberg, then installs them in the local Typst
17
+ package directory.
18
+
19
+ Requires Python 3.14 or newer. typst-pkg-sync has no runtime third-party dependencies.
20
+
21
+ ## Install
22
+
23
+ After typst-pkg-sync is published to PyPI, install it as a user tool with:
24
+
25
+ ```sh
26
+ uv tool install typst-pkg-sync
27
+ ```
28
+
29
+ To install from a local checkout while developing typst-pkg-sync:
30
+
31
+ ```sh
32
+ uv tool install .
33
+ ```
34
+
35
+ ## Configure
36
+
37
+ On its first run, typst-pkg-sync creates `~/.config/lpmt/config.toml` and exits.
38
+ The `lpmt` config directory is retained for compatibility. Edit the file to add
39
+ repositories, then run `typst-pkg-sync` again. Alternatively, select a config
40
+ file explicitly:
41
+
42
+ ```sh
43
+ typst-pkg-sync --config /path/to/config.toml
44
+ ```
45
+
46
+ See the [example configuration](https://codeberg.org/ressler/lpmt/src/branch/main/config_example.toml)
47
+ for examples, including a self-hosted GitLab repository. GitLab entries can
48
+ set `base_url` and an optional `token_env` per repository.
49
+
50
+ Packages are installed under:
51
+
52
+ ```text
53
+ <base_dir>/<namespace>/<package-name>/<version>/
54
+ ```
55
+
56
+ The package name is read from `typst.toml` in the downloaded archive. A `v`
57
+ prefix on a tag is removed from the installed version directory.
58
+
59
+ ## Development
60
+
61
+ Run the sample configuration with `just run` and the code-quality checks with
62
+ `just check`.
63
+
64
+ typst-pkg-sync is licensed under MIT; see [LICENSE](LICENSE).
@@ -0,0 +1,8 @@
1
+ typst_pkg_sync/__init__.py,sha256=ELTFeRb4DDajqZyzxzkPGXFMqwHpO_xc0yFXdW_6lC4,39
2
+ typst_pkg_sync/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
3
+ typst_pkg_sync/cli.py,sha256=CeK39OZ3Ynt3tBuJxXoeRPlYs6cAt66x8E--c0h8gyM,18566
4
+ typst_pkg_sync-0.1.0.dist-info/licenses/LICENSE,sha256=Og_MGJ76gr6KQjWSzcS6AVwFM2suJj6syvXrlYd-ytc,1076
5
+ typst_pkg_sync-0.1.0.dist-info/WHEEL,sha256=cmC5s21ojypbVslldL7IJq3hjZH-tINy4rziKePFsG0,81
6
+ typst_pkg_sync-0.1.0.dist-info/entry_points.txt,sha256=khquF574jW8TPJyO0xjed1MZVPYkVjxgLHL0AjFQxAc,60
7
+ typst_pkg_sync-0.1.0.dist-info/METADATA,sha256=6jAnsW7lzpnGbw3VDmsG501SeWEDyns8x6WCnI-ZQTM,1866
8
+ typst_pkg_sync-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.23
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ typst-pkg-sync = typst_pkg_sync.cli:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jan-Philipp Reßler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.