applepy-cli 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,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: applepy-cli
3
+ Version: 0.1.0
4
+ Summary: Build tool for ApplePy — scaffold, build, and publish Swift-powered Python packages
5
+ Author: Jagtesh Chadha
6
+ License: BSD-3-Clause
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Operating System :: MacOS
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Software Development :: Build Tools
11
+ Requires-Python: >=3.10
@@ -0,0 +1,6 @@
1
+ applepy_cli.py,sha256=21zSV1Ltqv9GuWzYtPAsLptag1c2qVaJF_RnwAFFIrQ,19363
2
+ applepy_cli-0.1.0.dist-info/METADATA,sha256=4-plOScXJSGZLlsZihQob-9OX0mBYWaqnFEccLZN0EY,406
3
+ applepy_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
4
+ applepy_cli-0.1.0.dist-info/entry_points.txt,sha256=-5A4rmwvK-i-weVRVJnHRVEoqocFEipprb5ub4GE_fk,45
5
+ applepy_cli-0.1.0.dist-info/top_level.txt,sha256=XdQ29T_Z6WVJ7zKv3nk-I4QvRxKtJRz81PYMYx13q1g,12
6
+ applepy_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ applepy = applepy_cli:main
@@ -0,0 +1 @@
1
+ applepy_cli
applepy_cli.py ADDED
@@ -0,0 +1,647 @@
1
+ #!/usr/bin/env python3
2
+ """applepy — Build tool for ApplePy projects.
3
+
4
+ Equivalent to maturin for PyO3. Scaffolds, builds, and publishes
5
+ Swift-powered Python extension modules.
6
+
7
+ Commands:
8
+ applepy new <name> Create a new project
9
+ applepy develop Build and install into current env
10
+ applepy build Build a distributable wheel
11
+ applepy publish Publish to PyPI
12
+ """
13
+ import argparse
14
+ import os
15
+ import platform
16
+ import re
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ import sysconfig
21
+ from pathlib import Path
22
+ from textwrap import dedent
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ # ApplePy Swift package — used when generating Package.swift for new projects
27
+ APPLEPY_GITHUB_URL = "https://github.com/jagtesh/ApplePy.git"
28
+ APPLEPY_MIN_VERSION = "1.0.0"
29
+
30
+
31
+ # ── Templates ───────────────────────────────────────────────
32
+
33
+ PYPROJECT_TEMPLATE = '''\
34
+ [build-system]
35
+ requires = ["setuptools>=68.0"]
36
+ build-backend = "setuptools.build_meta"
37
+
38
+ [project]
39
+ name = "{name}"
40
+ version = "0.1.0"
41
+ description = "{description}"
42
+ readme = "README.md"
43
+ license = {{text = "BSD-3-Clause"}}
44
+ requires-python = ">=3.10"
45
+ classifiers = [
46
+ "Development Status :: 3 - Alpha",
47
+ "Operating System :: MacOS",
48
+ "Programming Language :: Python :: 3",
49
+ "Programming Language :: Swift",
50
+ ]
51
+
52
+ [tool.setuptools.packages.find]
53
+ include = ["{name}*"]
54
+ '''
55
+
56
+ SETUP_PY_TEMPLATE = '''\
57
+ """Build — compiles Swift source into a Python-loadable .so"""
58
+ import os
59
+ import subprocess
60
+ import sys
61
+ import sysconfig
62
+ from pathlib import Path
63
+
64
+ from setuptools import setup
65
+ from setuptools.command.build_ext import build_ext
66
+
67
+
68
+ class SwiftBuildExt(build_ext):
69
+ """Custom build_ext that calls `swift build` to compile the Swift extension."""
70
+
71
+ def run(self):
72
+ if sys.platform != "darwin":
73
+ raise RuntimeError("{name} only supports macOS")
74
+
75
+ swift_dir = Path(__file__).parent / "swift"
76
+ pkg_config_path = sysconfig.get_config_var("LIBPC") or ""
77
+
78
+ env = os.environ.copy()
79
+ env["PKG_CONFIG_PATH"] = pkg_config_path
80
+
81
+ print("🔨 Building Swift extension...")
82
+ subprocess.check_call(
83
+ ["swift", "build"],
84
+ cwd=swift_dir,
85
+ env=env,
86
+ )
87
+
88
+ build_dir = swift_dir / ".build" / "debug"
89
+ dylib = build_dir / "lib{swift_target}.dylib"
90
+ if not dylib.exists():
91
+ raise RuntimeError(f"Build succeeded but {{dylib}} not found")
92
+
93
+ dest = Path(__file__).parent / "{name}" / "{name}.so"
94
+ print(f"📦 Installing {{dylib.name}} → {{dest}}")
95
+ import shutil
96
+ shutil.copy2(dylib, dest)
97
+
98
+ def get_ext_filename(self, ext_name):
99
+ return ext_name + ".so"
100
+
101
+
102
+ setup(cmdclass={{"build_ext": SwiftBuildExt}})
103
+ '''
104
+
105
+ INIT_PY_TEMPLATE = '''\
106
+ """{name} — {description}
107
+
108
+ Powered by Swift & ApplePy.
109
+ """
110
+ import importlib
111
+ import os
112
+ import sys
113
+
114
+ if sys.platform != "darwin":
115
+ raise ImportError("{name} only supports macOS")
116
+
117
+
118
+ def _load_native():
119
+ """Load the compiled Swift extension module."""
120
+ pkg_dir = os.path.dirname(os.path.abspath(__file__))
121
+ so_path = os.path.join(pkg_dir, "{name}.so")
122
+
123
+ if not os.path.exists(so_path):
124
+ raise ImportError(
125
+ "Native extension not found. Build it first:\\n"
126
+ " applepy develop\\n"
127
+ " # or: pip install -e ."
128
+ )
129
+
130
+ spec = importlib.util.spec_from_file_location("{name}", so_path)
131
+ mod = importlib.util.module_from_spec(spec)
132
+ spec.loader.exec_module(mod)
133
+ return mod
134
+
135
+
136
+ _native = _load_native()
137
+
138
+ # Re-export all public attributes from the native module
139
+ for _attr in dir(_native):
140
+ if not _attr.startswith("_"):
141
+ globals()[_attr] = getattr(_native, _attr)
142
+
143
+ __version__ = "0.1.0"
144
+ '''
145
+
146
+ # Two Package.swift templates: one for GitHub (default), one for local dev
147
+ PACKAGE_SWIFT_TEMPLATE_GITHUB = '''\
148
+ // swift-tools-version: 6.0
149
+ import PackageDescription
150
+
151
+ let package = Package(
152
+ name: "{swift_target}",
153
+ platforms: [.macOS(.v14)],
154
+ products: [
155
+ .library(name: "{swift_target}", type: .dynamic, targets: ["{swift_target}"]),
156
+ ],
157
+ dependencies: [
158
+ .package(url: "{applepy_url}", from: "{applepy_version}"),
159
+ ],
160
+ targets: [
161
+ .target(
162
+ name: "{swift_target}",
163
+ dependencies: [
164
+ .product(name: "ApplePy", package: "ApplePy"),
165
+ .product(name: "ApplePyClient", package: "ApplePy"),
166
+ ]
167
+ ),
168
+ ]
169
+ )
170
+ '''
171
+
172
+ PACKAGE_SWIFT_TEMPLATE_LOCAL = '''\
173
+ // swift-tools-version: 6.0
174
+ import PackageDescription
175
+
176
+ let package = Package(
177
+ name: "{swift_target}",
178
+ platforms: [.macOS(.v14)],
179
+ products: [
180
+ .library(name: "{swift_target}", type: .dynamic, targets: ["{swift_target}"]),
181
+ ],
182
+ dependencies: [
183
+ .package(path: "{applepy_path}"),
184
+ ],
185
+ targets: [
186
+ .target(
187
+ name: "{swift_target}",
188
+ dependencies: [
189
+ .product(name: "ApplePy", package: "ApplePy"),
190
+ .product(name: "ApplePyClient", package: "ApplePy"),
191
+ ]
192
+ ),
193
+ ]
194
+ )
195
+ '''
196
+
197
+ SWIFT_SOURCE_TEMPLATE = '''\
198
+ // {swift_target} — Powered by ApplePy
199
+ //
200
+ // Usage from Python:
201
+ // import {name}
202
+ // print({name}.hello("world"))
203
+
204
+ import ApplePy
205
+ @preconcurrency import ApplePyFFI
206
+
207
+ // MARK: - Functions
208
+
209
+ @PyFunction
210
+ func hello(name: String = "World") -> String {{
211
+ return "Hello, \\(name)! 🍎"
212
+ }}
213
+
214
+ // MARK: - Module Entry Point
215
+
216
+ @PyModule("{name}", functions: [
217
+ hello,
218
+ ])
219
+ func {name}() {{}}
220
+ '''
221
+
222
+ DEMO_PY_TEMPLATE = '''\
223
+ #!/usr/bin/env python3
224
+ """{swift_target} — Example Usage"""
225
+ import {name}
226
+
227
+ print({name}.hello("World"))
228
+ print({name}.hello("ApplePy"))
229
+ '''
230
+
231
+ README_TEMPLATE = '''\
232
+ # {swift_target}
233
+
234
+ {description}
235
+
236
+ > **macOS only** — requires Swift 6.0+ and ApplePy
237
+
238
+ ## Install
239
+
240
+ ```bash
241
+ applepy develop
242
+ # or: pip install -e .
243
+ ```
244
+
245
+ ## Usage
246
+
247
+ ```python
248
+ import {name}
249
+
250
+ print({name}.hello("World")) # Hello, World! 🍎
251
+ ```
252
+
253
+ ## Development
254
+
255
+ ```bash
256
+ applepy develop # Build Swift + install
257
+ applepy build # Build wheel
258
+ applepy publish # Publish to PyPI
259
+ ```
260
+ '''
261
+
262
+ GITIGNORE_TEMPLATE = '''\
263
+ *.so
264
+ swift/.build/
265
+ *.egg-info/
266
+ dist/
267
+ build/
268
+ __pycache__/
269
+ .DS_Store
270
+ '''
271
+
272
+
273
+ # ── Helpers ─────────────────────────────────────────────────
274
+
275
+ def _get_platform_tag():
276
+ """Get the wheel platform tag for the current macOS + arch."""
277
+ # e.g., macosx_14_0_arm64
278
+ mac_ver = platform.mac_ver()[0] # "14.3.1"
279
+ parts = mac_ver.split(".")
280
+ major = parts[0] if parts else "14"
281
+ minor = parts[1] if len(parts) > 1 else "0"
282
+ arch = platform.machine() # "arm64" or "x86_64"
283
+ return f"macosx_{major}_{minor}_{arch}"
284
+
285
+
286
+ def _find_applepy():
287
+ """Try to find a local ApplePy package."""
288
+ cli_dir = Path(__file__).resolve().parent
289
+ candidates = [
290
+ cli_dir.parent.parent, # Tools/applepy-cli -> ApplePy
291
+ cli_dir.parent.parent.parent / "ApplePy", # sibling in workspace
292
+ Path.cwd().parent / "ApplePy",
293
+ ]
294
+ for p in candidates:
295
+ if (p / "Package.swift").exists() and (p / "Sources").exists():
296
+ return p
297
+ return None
298
+
299
+
300
+ def _load_project(project_dir: Path) -> dict:
301
+ """Load project configuration from pyproject.toml."""
302
+ pyproject = project_dir / "pyproject.toml"
303
+ if not pyproject.exists():
304
+ print(f"❌ No pyproject.toml found. Are you in a project directory?")
305
+ sys.exit(1)
306
+
307
+ name = None
308
+ content = pyproject.read_text()
309
+ for line in content.splitlines():
310
+ line = line.strip()
311
+ if line.startswith("name") and "=" in line:
312
+ val = line.split("=", 1)[1].strip().strip('"').strip("'")
313
+ name = val
314
+ break
315
+
316
+ if not name:
317
+ print(f"❌ Could not find project name in pyproject.toml")
318
+ sys.exit(1)
319
+
320
+ # Derive swift target from name
321
+ swift_target = "".join(word.capitalize() for word in name.split("_")) or name.capitalize()
322
+
323
+ # Check if swift/Package.swift has a different target name
324
+ pkg_swift = project_dir / "swift" / "Package.swift"
325
+ if pkg_swift.exists():
326
+ pkg_content = pkg_swift.read_text()
327
+ match = re.search(r'Package\(\s*name:\s*"([^"]+)"', pkg_content)
328
+ if match:
329
+ swift_target = match.group(1)
330
+
331
+ return {"name": name, "swift_target": swift_target}
332
+
333
+
334
+ def _swift_build(swift_dir: Path, swift_target: str):
335
+ """Run swift build and return the path to the built dylib."""
336
+ pkg_config_path = sysconfig.get_config_var("LIBPC") or ""
337
+ env = os.environ.copy()
338
+ env["PKG_CONFIG_PATH"] = pkg_config_path
339
+
340
+ print(f"🔨 Building {swift_target}...")
341
+ try:
342
+ subprocess.check_call(["swift", "build"], cwd=swift_dir, env=env)
343
+ except subprocess.CalledProcessError:
344
+ print(f"❌ Swift build failed")
345
+ sys.exit(1)
346
+
347
+ dylib = swift_dir / ".build" / "debug" / f"lib{swift_target}.dylib"
348
+ if not dylib.exists():
349
+ print(f"❌ Expected {dylib} not found")
350
+ sys.exit(1)
351
+
352
+ return dylib
353
+
354
+
355
+ # ── Commands ────────────────────────────────────────────────
356
+
357
+ def cmd_new(args):
358
+ """Scaffold a new ApplePy project."""
359
+ name = args.name.lower().replace("-", "_").replace(" ", "_")
360
+ swift_target = "".join(word.capitalize() for word in name.split("_")) or name.capitalize()
361
+ project_dir = Path(args.name)
362
+ description = args.description or "A Swift-powered Python package"
363
+
364
+ if project_dir.exists():
365
+ print(f"❌ Directory '{args.name}' already exists")
366
+ sys.exit(1)
367
+
368
+ print(f"🍎 Creating new ApplePy project: {name}")
369
+ print(f" Swift target: {swift_target}")
370
+
371
+ # Determine ApplePy dependency mode
372
+ use_local = args.local
373
+ if use_local:
374
+ applepy_path = args.applepy_path or _find_applepy()
375
+ if not applepy_path:
376
+ print(f" ⚠ Could not auto-detect local ApplePy. Using default path.")
377
+ applepy_path = Path("../../ApplePy")
378
+ else:
379
+ applepy_path = Path(applepy_path).resolve()
380
+ # Compute relative path from swift/ to ApplePy
381
+ swift_dir = project_dir.resolve() / "swift"
382
+ try:
383
+ rel_applepy = os.path.relpath(applepy_path, swift_dir)
384
+ except ValueError:
385
+ rel_applepy = str(applepy_path)
386
+ print(f" ApplePy: local ({rel_applepy})")
387
+ else:
388
+ applepy_url = args.applepy_url or APPLEPY_GITHUB_URL
389
+ applepy_version = args.applepy_version or APPLEPY_MIN_VERSION
390
+ print(f" ApplePy: {applepy_url} (>= {applepy_version})")
391
+ print()
392
+
393
+ # Create directory structure
394
+ dirs = [
395
+ project_dir,
396
+ project_dir / name,
397
+ project_dir / name / "examples",
398
+ project_dir / "swift",
399
+ project_dir / "swift" / "Sources" / swift_target,
400
+ ]
401
+ for d in dirs:
402
+ d.mkdir(parents=True, exist_ok=True)
403
+
404
+ # Build context for templates
405
+ ctx = {
406
+ "name": name,
407
+ "swift_target": swift_target,
408
+ "description": description,
409
+ }
410
+
411
+ # Choose Package.swift template
412
+ if use_local:
413
+ ctx["applepy_path"] = rel_applepy
414
+ pkg_swift_template = PACKAGE_SWIFT_TEMPLATE_LOCAL
415
+ else:
416
+ ctx["applepy_url"] = applepy_url
417
+ ctx["applepy_version"] = applepy_version
418
+ pkg_swift_template = PACKAGE_SWIFT_TEMPLATE_GITHUB
419
+
420
+ files = {
421
+ project_dir / "pyproject.toml": PYPROJECT_TEMPLATE,
422
+ project_dir / "setup.py": SETUP_PY_TEMPLATE,
423
+ project_dir / "README.md": README_TEMPLATE,
424
+ project_dir / ".gitignore": GITIGNORE_TEMPLATE,
425
+ project_dir / name / "__init__.py": INIT_PY_TEMPLATE,
426
+ project_dir / name / "examples" / "demo.py": DEMO_PY_TEMPLATE,
427
+ project_dir / "swift" / "Package.swift": pkg_swift_template,
428
+ project_dir / "swift" / "Sources" / swift_target / f"{swift_target}.swift": SWIFT_SOURCE_TEMPLATE,
429
+ }
430
+
431
+ for filepath, template in files.items():
432
+ content = template.format(**ctx)
433
+ filepath.write_text(content)
434
+ rel = filepath.relative_to(project_dir)
435
+ print(f" ✓ {rel}")
436
+
437
+ # Init git
438
+ if shutil.which("git"):
439
+ subprocess.run(["git", "init", "-q"], cwd=project_dir)
440
+ subprocess.run(["git", "add", "-A"], cwd=project_dir)
441
+ subprocess.run(
442
+ ["git", "commit", "-q", "-m", f"Initial ApplePy project: {name}"],
443
+ cwd=project_dir,
444
+ )
445
+ print(f" ✓ git initialized")
446
+
447
+ print()
448
+ print(f"🎉 Project created! Next steps:")
449
+ print(f" cd {args.name}")
450
+ print(f" applepy develop")
451
+ print(f" python {name}/examples/demo.py")
452
+
453
+
454
+ def cmd_develop(args):
455
+ """Build Swift extension and install into current environment."""
456
+ project_dir = Path.cwd()
457
+ config = _load_project(project_dir)
458
+ name = config["name"]
459
+ swift_target = config["swift_target"]
460
+
461
+ swift_dir = project_dir / "swift"
462
+ if not swift_dir.exists():
463
+ print(f"❌ No swift/ directory found. Are you in an ApplePy project?")
464
+ sys.exit(1)
465
+
466
+ dylib = _swift_build(swift_dir, swift_target)
467
+
468
+ dest = project_dir / name / f"{name}.so"
469
+ shutil.copy2(dylib, dest)
470
+ print(f"📦 Installed {dylib.name} → {dest.relative_to(project_dir)}")
471
+
472
+ print(f"📥 Installing {name} into current environment...")
473
+ subprocess.check_call(
474
+ [sys.executable, "-m", "pip", "install", "-e", ".", "-q"],
475
+ cwd=project_dir,
476
+ )
477
+
478
+ print(f"✅ Done! Try: python -c \"import {name}; print(dir({name}))\"")
479
+
480
+
481
+ def cmd_build(args):
482
+ """Build a distributable wheel with the native .so baked in."""
483
+ project_dir = Path.cwd()
484
+ config = _load_project(project_dir)
485
+ name = config["name"]
486
+ swift_target = config["swift_target"]
487
+
488
+ swift_dir = project_dir / "swift"
489
+ dylib = _swift_build(swift_dir, swift_target)
490
+
491
+ dest = project_dir / name / f"{name}.so"
492
+ shutil.copy2(dylib, dest)
493
+ print(f"📦 Installed {dylib.name} → {dest.relative_to(project_dir)}")
494
+
495
+ # Build wheel with correct platform tag
496
+ dist_dir = project_dir / "dist"
497
+ dist_dir.mkdir(exist_ok=True)
498
+
499
+ plat_tag = _get_platform_tag()
500
+ print(f"📦 Building wheel (platform: {plat_tag})...")
501
+ subprocess.check_call(
502
+ [
503
+ sys.executable, "-m", "pip", "wheel", ".",
504
+ "-w", "dist", "-q", "--no-deps",
505
+ ],
506
+ cwd=project_dir,
507
+ )
508
+
509
+ # Rename the wheel to use the correct platform tag
510
+ # setuptools generates py3-none-any, we need macosx_XX_X_arch
511
+ for whl in dist_dir.glob(f"{name}-*-py3-none-any.whl"):
512
+ correct_name = whl.name.replace("py3-none-any", f"py3-none-{plat_tag}")
513
+ correct_path = whl.parent / correct_name
514
+ whl.rename(correct_path)
515
+ size_kb = correct_path.stat().st_size / 1024
516
+ print(f"✅ Built: {correct_name} ({size_kb:.0f} KB)")
517
+ return
518
+
519
+ # If already has correct tag, just report
520
+ wheels = list(dist_dir.glob(f"{name}-*.whl"))
521
+ for w in sorted(wheels, key=lambda x: x.stat().st_mtime, reverse=True)[:1]:
522
+ size_kb = w.stat().st_size / 1024
523
+ print(f"✅ Built: {w.name} ({size_kb:.0f} KB)")
524
+
525
+
526
+ def cmd_publish(args):
527
+ """Publish to PyPI (or TestPyPI with --test)."""
528
+ project_dir = Path.cwd()
529
+ config = _load_project(project_dir)
530
+ name = config["name"]
531
+
532
+ dist_dir = project_dir / "dist"
533
+ wheels = sorted(dist_dir.glob("*.whl"), key=lambda x: x.stat().st_mtime, reverse=True)
534
+
535
+ if not wheels:
536
+ print(f"❌ No wheels found in dist/. Run `applepy build` first.")
537
+ sys.exit(1)
538
+
539
+ # Show most recent wheel
540
+ latest = wheels[0]
541
+ print(f"📤 Publishing {name} to {'TestPyPI' if args.test else 'PyPI'}...")
542
+ print(f" Wheel: {latest.name}")
543
+
544
+ if args.test:
545
+ repo_url = "https://test.pypi.org/legacy/"
546
+ else:
547
+ repo_url = None
548
+
549
+ if not args.yes:
550
+ confirm = input("\n Proceed? [y/N] ")
551
+ if confirm.lower() not in ("y", "yes"):
552
+ print(" Cancelled.")
553
+ return
554
+
555
+ cmd = [sys.executable, "-m", "twine", "upload"]
556
+ if repo_url:
557
+ cmd += ["--repository-url", repo_url]
558
+ cmd.append(str(latest))
559
+
560
+ try:
561
+ subprocess.check_call(cmd)
562
+ print(f"\n✅ Published {name}!")
563
+ if args.test:
564
+ print(f" pip install -i https://test.pypi.org/simple/ {name}")
565
+ else:
566
+ print(f" pip install {name}")
567
+ except FileNotFoundError:
568
+ print(f"❌ twine not found. Install it: pip install twine")
569
+ sys.exit(1)
570
+ except subprocess.CalledProcessError:
571
+ print(f"❌ Upload failed")
572
+ sys.exit(1)
573
+
574
+
575
+ # ── Main ────────────────────────────────────────────────────
576
+
577
+ def main():
578
+ parser = argparse.ArgumentParser(
579
+ prog="applepy",
580
+ description="🍎 ApplePy — Build tool for Swift-powered Python packages",
581
+ formatter_class=argparse.RawDescriptionHelpFormatter,
582
+ epilog=dedent("""\
583
+ Examples:
584
+ applepy new myproject Create project (GitHub ApplePy)
585
+ applepy new myproject --local Create project (local ApplePy)
586
+ cd myproject && applepy develop Build and install
587
+ applepy build Build a wheel
588
+ applepy publish --test Publish to TestPyPI
589
+ """),
590
+ )
591
+ parser.add_argument("--version", action="version", version=f"applepy {__version__}")
592
+ sub = parser.add_subparsers(dest="command", help="Command to run")
593
+
594
+ # new
595
+ p_new = sub.add_parser("new", help="Create a new ApplePy project")
596
+ p_new.add_argument("name", help="Project name (e.g. myproject)")
597
+ p_new.add_argument("-d", "--description", help="Project description")
598
+ p_new.add_argument(
599
+ "--local", action="store_true",
600
+ help="Use local ApplePy path instead of GitHub URL (for development)",
601
+ )
602
+ p_new.add_argument(
603
+ "--applepy-path",
604
+ help="Local path to ApplePy package (implies --local, auto-detected if not set)",
605
+ )
606
+ p_new.add_argument(
607
+ "--applepy-url",
608
+ help=f"GitHub URL for ApplePy (default: {APPLEPY_GITHUB_URL})",
609
+ )
610
+ p_new.add_argument(
611
+ "--applepy-version",
612
+ help=f"Minimum ApplePy version (default: {APPLEPY_MIN_VERSION})",
613
+ )
614
+
615
+ # develop
616
+ sub.add_parser("develop", help="Build Swift and install into current env")
617
+
618
+ # build
619
+ sub.add_parser("build", help="Build a distributable wheel")
620
+
621
+ # publish
622
+ p_pub = sub.add_parser("publish", help="Publish to PyPI")
623
+ p_pub.add_argument("--test", action="store_true", help="Publish to TestPyPI instead")
624
+ p_pub.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
625
+
626
+ args = parser.parse_args()
627
+
628
+ if not args.command:
629
+ parser.print_help()
630
+ sys.exit(0)
631
+
632
+ # --applepy-path implies --local
633
+ if args.command == "new" and args.applepy_path:
634
+ args.local = True
635
+
636
+ commands = {
637
+ "new": cmd_new,
638
+ "develop": cmd_develop,
639
+ "build": cmd_build,
640
+ "publish": cmd_publish,
641
+ }
642
+
643
+ commands[args.command](args)
644
+
645
+
646
+ if __name__ == "__main__":
647
+ main()