nvda-addon-kit 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.
Files changed (51) hide show
  1. nvda_addon_kit/__init__.py +5 -0
  2. nvda_addon_kit/build.py +117 -0
  3. nvda_addon_kit/bundle.py +62 -0
  4. nvda_addon_kit/cli.py +377 -0
  5. nvda_addon_kit/config.py +412 -0
  6. nvda_addon_kit/crowdin.py +209 -0
  7. nvda_addon_kit/docs.py +99 -0
  8. nvda_addon_kit/errors.py +40 -0
  9. nvda_addon_kit/install.py +115 -0
  10. nvda_addon_kit/l10n/__init__.py +27 -0
  11. nvda_addon_kit/l10n/catalogs.py +141 -0
  12. nvda_addon_kit/l10n/extract.py +65 -0
  13. nvda_addon_kit/manifest.py +141 -0
  14. nvda_addon_kit/migrate.py +311 -0
  15. nvda_addon_kit/models.py +206 -0
  16. nvda_addon_kit/release.py +199 -0
  17. nvda_addon_kit/scaffold.py +116 -0
  18. nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/01-bug_report.yaml +170 -0
  19. nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/02-feature_request.yaml +44 -0
  20. nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/03-developer_facing_changes.yaml +48 -0
  21. nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/11-advanced_bug_report.md +28 -0
  22. nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/12-advanced_feature_request.md +19 -0
  23. nvda_addon_kit/templates/ci/dot-github/ISSUE_TEMPLATE/config.yml +1 -0
  24. nvda_addon_kit/templates/ci/dot-github/dependabot.yml +14 -0
  25. nvda_addon_kit/templates/ci/dot-github/scripts/checkTranslation.py +146 -0
  26. nvda_addon_kit/templates/ci/dot-github/scripts/crowdinSync.ps1 +227 -0
  27. nvda_addon_kit/templates/ci/dot-github/scripts/languageMappings.json +15 -0
  28. nvda_addon_kit/templates/ci/dot-github/workflows/build_addon.yml +92 -0
  29. nvda_addon_kit/templates/ci/dot-github/workflows/crowdinL10n.yml +60 -0
  30. nvda_addon_kit/templates/core/COPYING.txt +340 -0
  31. nvda_addon_kit/templates/core/addon.toml +107 -0
  32. nvda_addon_kit/templates/core/changelog.md +9 -0
  33. nvda_addon_kit/templates/core/dot-gitattributes +8 -0
  34. nvda_addon_kit/templates/core/dot-gitignore +20 -0
  35. nvda_addon_kit/templates/core/readme.md +41 -0
  36. nvda_addon_kit/templates/core/style.css +26 -0
  37. nvda_addon_kit/templates/plugin/appModule.py +30 -0
  38. nvda_addon_kit/templates/plugin/brailleDisplayDriver.py +48 -0
  39. nvda_addon_kit/templates/plugin/globalPlugin.py +66 -0
  40. nvda_addon_kit/templates/plugin/synthDriver.py +60 -0
  41. nvda_addon_kit/templates/plugin/visionEnhancementProvider.py +45 -0
  42. nvda_addon_kit/templates/tooling/.ruff_cache/.gitignore +2 -0
  43. nvda_addon_kit/templates/tooling/.ruff_cache/CACHEDIR.TAG +1 -0
  44. nvda_addon_kit/templates/tooling/dot-python-version +1 -0
  45. nvda_addon_kit/templates/tooling/pyproject.toml +40 -0
  46. nvda_addon_kit/wizard.py +172 -0
  47. nvda_addon_kit-0.1.0.dist-info/METADATA +122 -0
  48. nvda_addon_kit-0.1.0.dist-info/RECORD +51 -0
  49. nvda_addon_kit-0.1.0.dist-info/WHEEL +4 -0
  50. nvda_addon_kit-0.1.0.dist-info/entry_points.txt +3 -0
  51. nvda_addon_kit-0.1.0.dist-info/licenses/COPYING.txt +340 -0
@@ -0,0 +1,5 @@
1
+ # Copyright (C) 2026 NVDA add-on kit contributors
2
+ # This file is covered by the GNU General Public License.
3
+ # See the file COPYING.txt for more details.
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,117 @@
1
+ # Copyright (C) 2026 NVDA add-on kit contributors
2
+ # This file is covered by the GNU General Public License.
3
+ # See the file COPYING.txt for more details.
4
+
5
+ from copy import replace
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from . import docs, l10n
10
+ from .bundle import createBundle
11
+ from .manifest import writeManifest, writeTranslatedManifest
12
+ from .models import AddonConfig, AddonMetadata, Channel
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class PreparedAddon:
17
+ metadata: AddonMetadata
18
+ languages: list[str]
19
+ documents: list[Path]
20
+
21
+ def defaultBundlePath(self, root: Path) -> Path:
22
+ return root / f"{self.metadata.name}-{self.metadata.version}.nvda-addon"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class BuildResult:
27
+ bundle: Path
28
+ metadata: AddonMetadata
29
+ languages: list[str]
30
+ documents: list[Path]
31
+
32
+
33
+ def resolveMetadata(
34
+ config: AddonConfig,
35
+ *,
36
+ version: str | None = None,
37
+ channel: Channel | None = None,
38
+ ) -> AddonMetadata:
39
+ metadata = config.addon
40
+ if channel is not None:
41
+ metadata = replace(metadata, updateChannel=channel)
42
+ if version:
43
+ metadata = replace(metadata, version=version)
44
+ return metadata
45
+
46
+
47
+ def prepare(config: AddonConfig, metadata: AddonMetadata) -> PreparedAddon:
48
+ writeManifest(config, metadata)
49
+
50
+ languages = l10n.discoverLocales(config)
51
+ translationsByLanguage: dict[str, dict[str, str]] = {}
52
+ for language in languages:
53
+ translations = l10n.loadTranslations(config, language)
54
+ translationsByLanguage[language] = translations
55
+ writeTranslatedManifest(config, language, translations, metadata)
56
+ l10n.compileLocales(config, languages)
57
+
58
+ docs.stageBaseDocuments(config)
59
+ documents = docs.renderAll(config, translationsByLanguage, metadata)
60
+
61
+ return PreparedAddon(metadata=metadata, languages=languages, documents=documents)
62
+
63
+
64
+ def build(
65
+ config: AddonConfig,
66
+ *,
67
+ version: str | None = None,
68
+ channel: Channel | None = None,
69
+ output: Path | None = None,
70
+ ) -> BuildResult:
71
+ metadata = resolveMetadata(config, version=version, channel=channel)
72
+ prepared = prepare(config, metadata)
73
+
74
+ destination = output or prepared.defaultBundlePath(config.root)
75
+ bundle = createBundle(config.addonDir, destination, config.build.excludedFiles)
76
+
77
+ return BuildResult(
78
+ bundle=bundle,
79
+ metadata=prepared.metadata,
80
+ languages=prepared.languages,
81
+ documents=prepared.documents,
82
+ )
83
+
84
+
85
+ def _removeFile(path: Path, removed: list[Path]) -> None:
86
+ if path.is_file():
87
+ path.unlink()
88
+ removed.append(path)
89
+
90
+
91
+ def clean(config: AddonConfig) -> list[Path]:
92
+ removed: list[Path] = []
93
+
94
+ _removeFile(config.addonDir / "manifest.ini", removed)
95
+ _removeFile(config.docsDir / "style.css", removed)
96
+
97
+ for language in l10n.discoverLocales(config):
98
+ _removeFile(l10n.moPath(config, language), removed)
99
+ _removeFile(config.localeDir / language / "manifest.ini", removed)
100
+
101
+ if config.docsDir.is_dir():
102
+ for html in sorted(config.docsDir.glob("*/*.html")):
103
+ _removeFile(html, removed)
104
+ baseDocs = config.docsDir / config.l10n.baseLanguage
105
+ if baseDocs.is_dir():
106
+ for staged in sorted(baseDocs.glob("*.md")):
107
+ _removeFile(staged, removed)
108
+ if not any(baseDocs.iterdir()):
109
+ baseDocs.rmdir()
110
+ removed.append(baseDocs)
111
+
112
+ for bundle in sorted(config.root.glob("*.nvda-addon")):
113
+ _removeFile(bundle, removed)
114
+ for potFile in sorted(config.root.glob("*.pot")):
115
+ _removeFile(potFile, removed)
116
+
117
+ return removed
@@ -0,0 +1,62 @@
1
+ # Copyright (C) 2026 NVDA add-on kit contributors
2
+ # This file is covered by the GNU General Public License.
3
+ # See the file COPYING.txt for more details.
4
+
5
+
6
+ import zipfile
7
+ from collections.abc import Iterable
8
+ from pathlib import Path
9
+
10
+ from .errors import BuildError
11
+
12
+ _ALWAYS_EXCLUDED: tuple[str, ...] = (
13
+ "**/__pycache__/**",
14
+ "**/*.py[co]",
15
+ "**/.git/**",
16
+ "**/.DS_Store",
17
+ "**/*.orig",
18
+ "**/*.rej",
19
+ "**/*.po",
20
+ "**/*.pot",
21
+ "**/*.md",
22
+ )
23
+
24
+
25
+ def _isExcluded(relativePath: Path, patterns: Iterable[str]) -> bool:
26
+ return any(relativePath.full_match(pattern) or relativePath.match(pattern) for pattern in patterns)
27
+
28
+
29
+ def createBundle(
30
+ addonDir: Path,
31
+ destination: Path,
32
+ excludePatterns: Iterable[str] = (),
33
+ ) -> Path:
34
+ if not addonDir.is_dir():
35
+ raise BuildError(f"Add-on directory {addonDir} does not exist.")
36
+
37
+ manifest = addonDir / "manifest.ini"
38
+ if not manifest.is_file():
39
+ raise BuildError(
40
+ f"{manifest} is missing, so the archive would not be a valid add-on. "
41
+ f"This is generated during the build; run `nvaddon build`.",
42
+ )
43
+
44
+ patterns = (*_ALWAYS_EXCLUDED, *excludePatterns)
45
+ base = addonDir.absolute()
46
+ destination.parent.mkdir(parents=True, exist_ok=True)
47
+
48
+ with zipfile.ZipFile(destination, "w", zipfile.ZIP_DEFLATED) as archive:
49
+ for path in sorted(base.rglob("*")):
50
+ if path.is_dir():
51
+ continue
52
+ relativePath = path.relative_to(base)
53
+ if _isExcluded(relativePath, patterns):
54
+ continue
55
+ archive.write(path, relativePath.as_posix())
56
+
57
+ return destination
58
+
59
+
60
+ def listBundle(archivePath: Path) -> list[str]:
61
+ with zipfile.ZipFile(archivePath) as archive:
62
+ return sorted(archive.namelist())
nvda_addon_kit/cli.py ADDED
@@ -0,0 +1,377 @@
1
+ # Copyright (C) 2026 NVDA add-on kit contributors
2
+ # This file is covered by the GNU General Public License.
3
+ # See the file COPYING.txt for more details.
4
+
5
+ import argparse
6
+ import sys
7
+ from collections.abc import Sequence
8
+ from pathlib import Path
9
+
10
+ from . import __version__, l10n
11
+ from . import build as buildModule
12
+ from . import config as configModule
13
+ from .errors import AddonKitError
14
+ from .models import AddonConfig, Channel
15
+
16
+ PLUGIN_KINDS: tuple[str, ...] = (
17
+ "globalPlugin",
18
+ "appModule",
19
+ "synthDriver",
20
+ "brailleDisplayDriver",
21
+ "visionEnhancementProvider",
22
+ "none",
23
+ )
24
+
25
+
26
+ def _loadConfig(args: argparse.Namespace) -> AddonConfig:
27
+ start = Path(args.directory) if getattr(args, "directory", None) else None
28
+ return configModule.load(start)
29
+
30
+
31
+ def _relative(path: Path, root: Path) -> str:
32
+ try:
33
+ return str(path.relative_to(root))
34
+ except ValueError:
35
+ return str(path)
36
+
37
+
38
+ def cmdInit(args: argparse.Namespace) -> int:
39
+ from .scaffold import initialiseAddon
40
+
41
+ destination = Path(args.directory_positional or args.directory or ".").resolve()
42
+ created = initialiseAddon(destination, args)
43
+ print(f"Created {len(created)} files in {destination}")
44
+ print("\nNext steps:")
45
+ print(f" cd {destination.name}")
46
+ print(" nvaddon build")
47
+ return 0
48
+
49
+
50
+ def cmdBuild(args: argparse.Namespace) -> int:
51
+ config = _loadConfig(args)
52
+ result = buildModule.build(
53
+ config,
54
+ version=args.version,
55
+ channel=args.channel,
56
+ output=Path(args.output) if args.output else None,
57
+ )
58
+ if result.languages:
59
+ print(f"Compiled {len(result.languages)} translations: {', '.join(result.languages)}")
60
+ if result.documents:
61
+ print(f"Rendered {len(result.documents)} documentation pages")
62
+ print(f"Built {_relative(result.bundle, config.root)} ({result.bundle.stat().st_size:,} bytes)")
63
+ return 0
64
+
65
+
66
+ def cmdInstall(args: argparse.Namespace) -> int:
67
+ from .install import handOffToNVDA, linkAddon
68
+
69
+ config = _loadConfig(args)
70
+ nvdaPath = Path(args.nvda_path) if args.nvda_path else None
71
+
72
+ if args.link:
73
+ metadata = buildModule.resolveMetadata(config, version=args.version, channel=args.channel)
74
+ _ = buildModule.prepare(config, metadata)
75
+ link = linkAddon(config, nvdaPath)
76
+ print(f"Linked {_relative(config.addonDir, config.root)} to {link}")
77
+ print("Restart NVDA, or reload plugins, to pick up the add-on.")
78
+ return 0
79
+
80
+ result = buildModule.build(config, version=args.version, channel=args.channel)
81
+ handOffToNVDA(result.bundle)
82
+ print(f"Opened {result.bundle.name} with NVDA. Confirm the installation prompt to finish.")
83
+ return 0
84
+
85
+
86
+ def cmdPot(args: argparse.Namespace) -> int:
87
+ config = _loadConfig(args)
88
+ destination = Path(args.output) if args.output else None
89
+ potFile = l10n.writePot(config, merge=args.merge, destination=destination)
90
+ catalog = l10n.buildCatalog(config)
91
+ count = sum(1 for message in catalog if message.id)
92
+ print(f"Wrote {_relative(potFile, config.root)} with {count} messages")
93
+ return 0
94
+
95
+
96
+ def cmdLocaleAdd(args: argparse.Namespace) -> int:
97
+ config = _loadConfig(args)
98
+ created = l10n.addLocales(config, args.languages)
99
+ for path in created:
100
+ print(f"Created {_relative(path, config.root)}")
101
+ return 0
102
+
103
+
104
+ def cmdLocaleUpdate(args: argparse.Namespace) -> int:
105
+ config = _loadConfig(args)
106
+ summary = l10n.updateLocales(config, args.languages or None)
107
+ if not summary:
108
+ print("No translations to update.")
109
+ return 0
110
+ for language, (untranslated, fuzzy) in summary.items():
111
+ details = [f"{untranslated} untranslated"]
112
+ if fuzzy:
113
+ details.append(f"{fuzzy} fuzzy")
114
+ print(f"{language}: {', '.join(details)}")
115
+ return 0
116
+
117
+
118
+ def cmdLocaleCompile(args: argparse.Namespace) -> int:
119
+ config = _loadConfig(args)
120
+ compiled = l10n.compileLocales(config, args.languages or None)
121
+ if not compiled:
122
+ print("No translations to compile.")
123
+ return 0
124
+ for path in compiled:
125
+ print(f"Compiled {_relative(path, config.root)}")
126
+ return 0
127
+
128
+
129
+ def cmdCheck(args: argparse.Namespace) -> int:
130
+ config = _loadConfig(args)
131
+ issues = configModule.validate(config)
132
+ errors = [issue for issue in issues if issue.level == "error"]
133
+ warnings = [issue for issue in issues if issue.level == "warning"]
134
+
135
+ for issue in errors + warnings:
136
+ print(issue, file=sys.stderr if issue.level == "error" else sys.stdout)
137
+
138
+ if not issues:
139
+ print(f"{config.addon.name} {config.addon.version}: no problems found.")
140
+ return 0
141
+
142
+ print(f"\n{len(errors)} error(s), {len(warnings)} warning(s).")
143
+ return 1 if errors else 0
144
+
145
+
146
+ def cmdClean(args: argparse.Namespace) -> int:
147
+ config = _loadConfig(args)
148
+ removed = buildModule.clean(config)
149
+ if not removed:
150
+ print("Nothing to clean.")
151
+ return 0
152
+ for path in removed:
153
+ print(f"Removed {_relative(path, config.root)}")
154
+ print(f"\nRemoved {len(removed)} build outputs.")
155
+ return 0
156
+
157
+
158
+ def cmdMigrate(args: argparse.Namespace) -> int:
159
+ from .migrate import migrateBuildVars
160
+
161
+ start = Path(args.directory) if args.directory else Path.cwd()
162
+ source = Path(args.buildvars) if args.buildvars else start / "buildVars.py"
163
+ destination = Path(args.output) if args.output else start / configModule.CONFIG_FILENAME
164
+
165
+ result = migrateBuildVars(source, destination, force=args.force)
166
+ print(f"Wrote {destination} from {source}")
167
+ for note in result.notes:
168
+ print(f" note: {note}")
169
+ print("\nReview the result, then run `nvaddon check` and `nvaddon build`.")
170
+ print("Once the build output looks right, delete buildVars.py, sconstruct and site_scons/.")
171
+ return 0
172
+
173
+
174
+ def cmdRelease(args: argparse.Namespace) -> int:
175
+ from .release import release
176
+
177
+ config = _loadConfig(args)
178
+ configPath = configModule.findConfig(config.root)
179
+
180
+ plan = release(
181
+ configPath,
182
+ args.version,
183
+ args.channel,
184
+ push=not args.no_push,
185
+ changelog=args.changelog,
186
+ )
187
+
188
+ print(f"{config.addon.version} -> {plan.version} on the {plan.channel} channel")
189
+ print(f"Committed and tagged {plan.tag}")
190
+ if plan.pushed:
191
+ print("Pushed the commit and the tag together.")
192
+ else:
193
+ print("Not pushed. When you are ready:")
194
+ print(f" git push --atomic origin HEAD refs/tags/{plan.tag}")
195
+ return 0
196
+
197
+
198
+ def cmdCrowdin(args: argparse.Namespace) -> int:
199
+ from .crowdin import runCrowdin
200
+
201
+ config = _loadConfig(args)
202
+ return runCrowdin(config, args)
203
+
204
+
205
+ def _addInitArguments(parser: argparse.ArgumentParser) -> None:
206
+ parser.add_argument(
207
+ "directory_positional",
208
+ nargs="?",
209
+ metavar="directory",
210
+ help="where to create the add-on (default: current directory)",
211
+ )
212
+ parser.add_argument("--name", help="add-on name, internal to NVDA (e.g. myAddon)")
213
+ parser.add_argument("--summary", help="user visible name of the add-on")
214
+ parser.add_argument("--description", help="longer description shown in the add-on store")
215
+ parser.add_argument("--author", help='author, as "Name <email@example.com>"')
216
+ parser.add_argument("--addon-version", default=None, help="initial version (default: 1.0.0)")
217
+ parser.add_argument("--url", help="URL of the add-on's documentation or homepage")
218
+ parser.add_argument("--source-url", help="URL of the source repository")
219
+ parser.add_argument("--license", help='licence name (e.g. "GPL v2")')
220
+ parser.add_argument("--license-url", help="URL of the licence text")
221
+ parser.add_argument("--min-nvda", help="minimum supported NVDA version (e.g. 2024.1)")
222
+ parser.add_argument("--last-tested", help="most recent NVDA version tested (e.g. 2025.3)")
223
+ parser.add_argument("--base-language", help="language the add-on is written in (default: en)")
224
+ parser.add_argument("--plugin", choices=PLUGIN_KINDS, help="starter plugin to scaffold")
225
+ parser.add_argument(
226
+ "--no-ci",
227
+ action="store_true",
228
+ help="skip GitHub workflows, issue templates and the Crowdin sync",
229
+ )
230
+ parser.add_argument(
231
+ "--no-tooling",
232
+ action="store_true",
233
+ help="skip the lint configuration and the Python version pin",
234
+ )
235
+ parser.add_argument(
236
+ "-y", "--yes", action="store_true", help="do not prompt; take every answer from flags and defaults"
237
+ )
238
+ parser.add_argument("--force", action="store_true", help="write into a non-empty directory")
239
+
240
+
241
+ def _addVersionArguments(parser: argparse.ArgumentParser) -> None:
242
+ parser.add_argument("--version", metavar="VERSION", help="override the version for this build")
243
+ parser.add_argument(
244
+ "--channel",
245
+ type=Channel,
246
+ choices=list(Channel),
247
+ help="override the update channel for this build",
248
+ )
249
+
250
+
251
+ def _addLanguageArgument(parser: argparse.ArgumentParser, *, required: bool, action: str) -> None:
252
+ parser.add_argument(
253
+ "languages",
254
+ nargs="+" if required else "*",
255
+ metavar="LANG",
256
+ help="language codes, e.g. de pt_BR" if required else f"languages to {action} (default: all)",
257
+ )
258
+
259
+
260
+ def buildParser() -> argparse.ArgumentParser:
261
+ parser = argparse.ArgumentParser(
262
+ prog="nvaddon",
263
+ description="Scaffold, build, translate and install NVDA add-ons.",
264
+ )
265
+ parser.add_argument("--version", action="version", version=f"nvda-addon-kit {__version__}")
266
+ parser.add_argument(
267
+ "-C",
268
+ "--directory",
269
+ metavar="PATH",
270
+ help="run as though started in PATH",
271
+ )
272
+ subparsers = parser.add_subparsers(dest="command", metavar="command")
273
+
274
+ init = subparsers.add_parser("init", help="create a new add-on")
275
+ _addInitArguments(init)
276
+ init.set_defaults(func=cmdInit)
277
+
278
+ build = subparsers.add_parser("build", help="build the .nvda-addon")
279
+ _addVersionArguments(build)
280
+ build.add_argument(
281
+ "-o", "--output", metavar="PATH", help="write the add-on here instead of the default name"
282
+ )
283
+ build.set_defaults(func=cmdBuild)
284
+
285
+ install = subparsers.add_parser("install", help="build and install into NVDA")
286
+ install.add_argument(
287
+ "--link",
288
+ action="store_true",
289
+ help="link the working tree into NVDA's add-ons directory instead of installing a built add-on",
290
+ )
291
+ install.add_argument("--nvda-path", metavar="PATH", help="target a portable NVDA copy at PATH")
292
+ _addVersionArguments(install)
293
+ install.set_defaults(func=cmdInstall)
294
+
295
+ pot = subparsers.add_parser("pot", help="generate the translation template")
296
+ pot.add_argument(
297
+ "--merge",
298
+ action="store_true",
299
+ help="omit the header and source locations, for merging into catalogues",
300
+ )
301
+ pot.add_argument("-o", "--output", metavar="PATH", help="write the template here")
302
+ pot.set_defaults(func=cmdPot)
303
+
304
+ localeAdd = subparsers.add_parser("locale-add", help="start a new translation")
305
+ _addLanguageArgument(localeAdd, required=True, action="add")
306
+ localeAdd.set_defaults(func=cmdLocaleAdd)
307
+
308
+ localeUpdate = subparsers.add_parser(
309
+ "locale-update", help="merge new messages into existing translations"
310
+ )
311
+ _addLanguageArgument(localeUpdate, required=False, action="update")
312
+ localeUpdate.set_defaults(func=cmdLocaleUpdate)
313
+
314
+ localeCompile = subparsers.add_parser("locale-compile", help="compile translations to .mo files")
315
+ _addLanguageArgument(localeCompile, required=False, action="compile")
316
+ localeCompile.set_defaults(func=cmdLocaleCompile)
317
+
318
+ check = subparsers.add_parser("check", help="validate addon.toml without building")
319
+ check.set_defaults(func=cmdCheck)
320
+
321
+ clean = subparsers.add_parser("clean", help="remove generated build outputs")
322
+ clean.set_defaults(func=cmdClean)
323
+
324
+ migrate = subparsers.add_parser("migrate", help="convert buildVars.py to addon.toml")
325
+ migrate.add_argument("--buildvars", metavar="PATH", help="path to buildVars.py")
326
+ migrate.add_argument("-o", "--output", metavar="PATH", help="path to write addon.toml to")
327
+ migrate.add_argument("--force", action="store_true", help="overwrite an existing addon.toml")
328
+ migrate.set_defaults(func=cmdMigrate)
329
+
330
+ release = subparsers.add_parser("release", help="set the version and channel, tag, and push the release")
331
+ release.add_argument("version", help="major, minor, patch, or an explicit version such as 1.2.3")
332
+ release.add_argument(
333
+ "--channel",
334
+ type=Channel,
335
+ choices=list(Channel),
336
+ default=Channel.STABLE,
337
+ help="channel to release on (default: stable); the tag is <version>-<channel>",
338
+ )
339
+ release.add_argument("--no-push", action="store_true", help="commit and tag locally without pushing")
340
+ release.add_argument("--changelog", action="store_true", help="add a stub entry to changelog.md")
341
+ release.set_defaults(func=cmdRelease)
342
+
343
+ crowdin = subparsers.add_parser("crowdin", help="sync translations with Crowdin")
344
+ crowdin.add_argument("action", choices=("upload", "download", "status"), help="what to sync")
345
+ crowdin.add_argument("--project-id", type=int, help="Crowdin project id (default: $CROWDIN_PROJECT_ID)")
346
+ crowdin.add_argument("--token", help="Crowdin API token (default: $CROWDIN_TOKEN)")
347
+ crowdin.add_argument(
348
+ "--min-percent",
349
+ type=int,
350
+ default=50,
351
+ help="minimum translated percentage to download a language (default: 50)",
352
+ )
353
+ crowdin.set_defaults(func=cmdCrowdin)
354
+
355
+ return parser
356
+
357
+
358
+ def main(argv: Sequence[str] | None = None) -> int:
359
+ parser = buildParser()
360
+ args = parser.parse_args(argv)
361
+
362
+ if not getattr(args, "func", None):
363
+ parser.print_help()
364
+ return 2
365
+
366
+ try:
367
+ return args.func(args)
368
+ except AddonKitError as error:
369
+ print(f"error: {error}", file=sys.stderr)
370
+ return 1
371
+ except KeyboardInterrupt:
372
+ print("\nInterrupted.", file=sys.stderr)
373
+ return 130
374
+
375
+
376
+ if __name__ == "__main__":
377
+ raise SystemExit(main())