sima-cli 2.1.6__tar.gz → 2.1.7__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.
Files changed (138) hide show
  1. {sima_cli-2.1.6/sima_cli.egg-info → sima_cli-2.1.7}/PKG-INFO +1 -1
  2. {sima_cli-2.1.6 → sima_cli-2.1.7}/pyproject.toml +1 -1
  3. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/__version__.py +1 -1
  4. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/cli.py +50 -5
  5. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/metadata_installer.py +4 -0
  6. sima_cli-2.1.7/sima_cli/install/package_builder.py +304 -0
  7. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/registry.py +49 -0
  8. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/commands.py +1 -1
  9. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/install.py +181 -0
  10. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/neat.py +37 -6
  11. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/preinstall.py +10 -3
  12. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/utils.py +2 -1
  13. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/netboot.py +126 -24
  14. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/updater.py +11 -16
  15. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/vulcan/artifacts.py +126 -6
  16. sima_cli-2.1.7/sima_cli/vulcan/commands.py +226 -0
  17. {sima_cli-2.1.6 → sima_cli-2.1.7/sima_cli.egg-info}/PKG-INFO +1 -1
  18. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli.egg-info/SOURCES.txt +4 -0
  19. sima_cli-2.1.7/tests/unit/test_firmware.py +60 -0
  20. sima_cli-2.1.7/tests/unit/test_metadata_installer.py +21 -0
  21. sima_cli-2.1.7/tests/unit/test_netboot.py +98 -0
  22. sima_cli-2.1.7/tests/unit/test_package_builder.py +314 -0
  23. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_sdk_image_detection.py +211 -0
  24. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_sdk_preinstall.py +14 -0
  25. sima_cli-2.1.7/tests/unit/test_vulcan.py +445 -0
  26. sima_cli-2.1.6/sima_cli/vulcan/commands.py +0 -115
  27. sima_cli-2.1.6/tests/unit/test_firmware.py +0 -0
  28. sima_cli-2.1.6/tests/unit/test_vulcan.py +0 -180
  29. {sima_cli-2.1.6 → sima_cli-2.1.7}/LICENSE +0 -0
  30. {sima_cli-2.1.6 → sima_cli-2.1.7}/MANIFEST.in +0 -0
  31. {sima_cli-2.1.6 → sima_cli-2.1.7}/README.md +0 -0
  32. {sima_cli-2.1.6 → sima_cli-2.1.7}/requirements.txt +0 -0
  33. {sima_cli-2.1.6 → sima_cli-2.1.7}/setup.cfg +0 -0
  34. {sima_cli-2.1.6 → sima_cli-2.1.7}/setup.py +0 -0
  35. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/__init__.py +0 -0
  36. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/__main__.py +0 -0
  37. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/app_zoo/__init__.py +0 -0
  38. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/app_zoo/app.py +0 -0
  39. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/app_zoo/commands.py +0 -0
  40. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/auth/__init__.py +0 -0
  41. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/auth/auth0.py +0 -0
  42. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/auth/devportal.py +0 -0
  43. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/auth/login.py +0 -0
  44. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/auth/oauth.py +0 -0
  45. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/data/__init__.py +0 -0
  46. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/data/resources_internal.yaml +0 -0
  47. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/data/resources_public.yaml +0 -0
  48. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/deploy_only/__init__.py +0 -0
  49. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/deploy_only/device/__init__.py +0 -0
  50. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/deploy_only/device/commands.py +0 -0
  51. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/deploy_only/mpk/__init__.py +0 -0
  52. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/deploy_only/mpk/commands.py +0 -0
  53. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/discover/__init__.py +0 -0
  54. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/discover/discover.py +0 -0
  55. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/discover/linuxll.py +0 -0
  56. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/download/__init__.py +0 -0
  57. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/download/downloader.py +0 -0
  58. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/__init__.py +0 -0
  59. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/github_assets.py +0 -0
  60. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/hostdriver.py +0 -0
  61. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/metadata_info.py +0 -0
  62. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/metadata_validator.py +0 -0
  63. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/optiview.py +0 -0
  64. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/install/palette.py +0 -0
  65. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/mla/__init__.py +0 -0
  66. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/mla/meminfo.py +0 -0
  67. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/model_zoo/__init__.py +0 -0
  68. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/model_zoo/model.py +0 -0
  69. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/network/__init__.py +0 -0
  70. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/network/network.py +0 -0
  71. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/playbooks/__init__.py +0 -0
  72. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/playbooks/commands.py +0 -0
  73. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/playbooks/manager.py +0 -0
  74. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/__init__.py +0 -0
  75. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/cmdexec.py +0 -0
  76. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/config.py +0 -0
  77. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/linux_shared_network.py +0 -0
  78. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/requirements.json +0 -0
  79. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/script.py +0 -0
  80. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/stop.py +0 -0
  81. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/sdk/uninstall.py +0 -0
  82. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/serial/__init__.py +0 -0
  83. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/serial/serial.py +0 -0
  84. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/storage/__init__.py +0 -0
  85. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/storage/nvme.py +0 -0
  86. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/storage/sdcard.py +0 -0
  87. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/__init__.py +0 -0
  88. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/bmaptool.py +0 -0
  89. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/bootimg.py +0 -0
  90. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/cleanlog.py +0 -0
  91. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/elxr.py +0 -0
  92. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/local.py +0 -0
  93. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/query.py +0 -0
  94. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/update/remote.py +0 -0
  95. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/upgrade/__init__.py +0 -0
  96. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/upgrade/selfupdate.py +0 -0
  97. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/__init__.py +0 -0
  98. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/api_common.py +0 -0
  99. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/artifactory.py +0 -0
  100. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/common.py +0 -0
  101. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/config.py +0 -0
  102. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/config_loader.py +0 -0
  103. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/container_registries.py +0 -0
  104. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/device_api.py +0 -0
  105. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/disk.py +0 -0
  106. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/docker.py +0 -0
  107. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/env.py +0 -0
  108. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/errors.py +0 -0
  109. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/mpk_api.py +0 -0
  110. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/net.py +0 -0
  111. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/network.py +0 -0
  112. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/pcie.py +0 -0
  113. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/pkg_update_check.py +0 -0
  114. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/serializers.py +0 -0
  115. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/services.py +0 -0
  116. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/utils/tag.py +0 -0
  117. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli/vulcan/__init__.py +0 -0
  118. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli.egg-info/dependency_links.txt +0 -0
  119. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli.egg-info/entry_points.txt +0 -0
  120. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli.egg-info/requires.txt +0 -0
  121. {sima_cli-2.1.6 → sima_cli-2.1.7}/sima_cli.egg-info/top_level.txt +0 -0
  122. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/__init__.py +0 -0
  123. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/e2e/__init__.py +0 -0
  124. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/__init__.py +0 -0
  125. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_app_zoo.py +0 -0
  126. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_auth.py +0 -0
  127. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_cli.py +0 -0
  128. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_cli_stdio.py +0 -0
  129. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_download.py +0 -0
  130. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_elxr_update.py +0 -0
  131. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_install_stub.py +0 -0
  132. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_model_zoo.py +0 -0
  133. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_pkg_update_check.py +0 -0
  134. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_sdk_uninstall.py +0 -0
  135. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_selfupdate.py +0 -0
  136. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_skills_commands.py +0 -0
  137. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_skills_manager.py +0 -0
  138. {sima_cli-2.1.6 → sima_cli-2.1.7}/tests/unit/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 2.1.6
3
+ Version: 2.1.7
4
4
  Summary: CLI tool for SiMa Developer Portal to download models, firmware, and apps.
5
5
  Home-page: https://developer.sima.ai/
6
6
  Author: SiMa.ai
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sima-cli"
7
- version = "2.1.6"
7
+ version = "2.1.7"
8
8
  description = "CLI tool for SiMa Developer Portal to download models, firmware, and apps."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "2.1.6"
2
+ __version__ = "2.1.7"
@@ -30,6 +30,7 @@ from sima_cli.install.registry import register_packages_commands
30
30
  from sima_cli.upgrade.selfupdate import register_selfupdate_command
31
31
  from sima_cli.playbooks import register_playbook_commands
32
32
  from sima_cli.vulcan import register_vulcan_commands
33
+ from sima_cli.vulcan.commands import ENV_BASE_URLS, install_vulcan_package
33
34
 
34
35
  def _configure_stdio_errors() -> None:
35
36
  for stream in (getattr(sys, "stdout", None), getattr(sys, "stderr", None)):
@@ -504,7 +505,31 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
504
505
  @click.argument("component", required=False)
505
506
  @click.option("-v", "--version", help="SDK version (required for SDK-dependent components unless --metadata is provided)")
506
507
  @click.option("-m", "--mirror", help="URL to a metadata.json file for generic installation")
507
- @click.option("-t", "--tag", help="Tag of the package (optional)")
508
+ @click.option("-t", "--tag", help="Tag of the package. With --vulcan, metadata variant type such as minimum.")
509
+ @click.option("--vulcan", "use_vulcan", is_flag=True, help="Install from Vulcan artifacts using the Vulcan package resolver.")
510
+ @click.option(
511
+ "--env",
512
+ "vulcan_environment",
513
+ type=click.Choice(sorted(ENV_BASE_URLS), case_sensitive=False),
514
+ default=None,
515
+ help="Vulcan artifact environment. Used with --vulcan. Defaults to production.",
516
+ )
517
+ @click.option(
518
+ "--base-url",
519
+ "vulcan_base_url",
520
+ default=None,
521
+ envvar="SIMA_VULCAN_BASE_URL",
522
+ help="Override the Vulcan artifact base URL. Used with --vulcan.",
523
+ )
524
+ @click.option(
525
+ "-d",
526
+ "--install-dir",
527
+ default=".",
528
+ show_default=True,
529
+ type=click.Path(file_okay=False, dir_okay=True, path_type=str),
530
+ help="Directory where Vulcan package resources are downloaded and installed. Used with --vulcan.",
531
+ )
532
+ @click.option("--json", "json_output", is_flag=True, help="With --vulcan, print resolved metadata URL and exit.")
508
533
  @click.option(
509
534
  "-f",
510
535
  "--force",
@@ -513,7 +538,7 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
513
538
  help="Force installation even if compatibility checks fail.",
514
539
  )
515
540
  @click.pass_context
516
- def install_cmd(ctx, component, version, mirror, tag, force):
541
+ def install_cmd(ctx, component, version, mirror, tag, use_vulcan, vulcan_environment, vulcan_base_url, install_dir, json_output, force):
517
542
  """
518
543
  Install SiMa packages.
519
544
 
@@ -548,12 +573,29 @@ def install_cmd(ctx, component, version, mirror, tag, force):
548
573
  """
549
574
  internal = ctx.obj.get("internal", False)
550
575
 
576
+ if use_vulcan:
577
+ if mirror:
578
+ raise click.ClickException("--mirror cannot be used with --vulcan.")
579
+ if not component:
580
+ raise click.ClickException("You must specify a Vulcan target when using --vulcan.")
581
+ install_vulcan_package(
582
+ target=component,
583
+ environment=vulcan_environment or "production",
584
+ base_url=vulcan_base_url,
585
+ package_type=tag,
586
+ install_dir=install_dir,
587
+ force=force,
588
+ json_output=json_output,
589
+ )
590
+ return None
591
+
551
592
  # Metadata-based installation path
552
593
  if mirror:
553
594
  if component:
554
595
  click.echo(f"⚠️ Component '{component}' is ignored when using --metadata. Proceeding with metadata-based installation.")
555
596
  click.echo(f"🔧 Installing generic component from metadata URL: {mirror}")
556
- return install_from_metadata(metadata_url=mirror, internal=internal, force=force)
597
+ install_from_metadata(metadata_url=mirror, internal=internal, force=force)
598
+ return None
557
599
 
558
600
  # No component and no metadata: error
559
601
  if not component:
@@ -562,11 +604,13 @@ def install_cmd(ctx, component, version, mirror, tag, force):
562
604
 
563
605
  # if user specified gh: as component, treat it the same as -m
564
606
  if component.startswith("gh:"):
565
- return install_from_metadata(metadata_url=component, internal=False, force=force)
607
+ install_from_metadata(metadata_url=component, internal=False, force=force)
608
+ return None
566
609
 
567
610
  # if the user specified cr: or ghcr: as component, install from container registry
568
611
  if component.startswith("cr:") or component.startswith("ghcr:"):
569
- return install_from_cr(resource_spec=component, internal=internal)
612
+ install_from_cr(resource_spec=component, internal=internal)
613
+ return None
570
614
 
571
615
  version = resolve_version(version)
572
616
 
@@ -601,6 +645,7 @@ def install_cmd(ctx, component, version, mirror, tag, force):
601
645
  ctx.exit(1)
602
646
 
603
647
  click.echo("✅ Installation complete.")
648
+ return None
604
649
 
605
650
 
606
651
  # ----------------------
@@ -936,6 +936,10 @@ def _is_platform_compatible(metadata: dict, force: bool = False) -> bool:
936
936
  platforms = metadata.get("platforms", [])
937
937
  board_ver, _ = get_sima_build_version()
938
938
 
939
+ if not platforms:
940
+ click.echo("ℹ️ No platform restrictions specified; treating package as compatible with all platforms.")
941
+ return True
942
+
939
943
  # Detect current OS and version
940
944
  os_name = platform.system().lower() # 'darwin', 'windows', 'linux'
941
945
  os_version = "Unknown"
@@ -0,0 +1,304 @@
1
+ import datetime
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import re
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional, Sequence, Tuple
9
+
10
+ import requests
11
+
12
+
13
+ METADATA_FILENAME = "metadata.json"
14
+ DEFAULT_POST_MESSAGE = "[bold]Package installed successfully.[/bold]\n"
15
+
16
+
17
+ def _run_git(args: List[str], cwd: Path) -> Optional[str]:
18
+ try:
19
+ result = subprocess.run(
20
+ ["git"] + args,
21
+ cwd=str(cwd),
22
+ check=True,
23
+ stdout=subprocess.PIPE,
24
+ stderr=subprocess.DEVNULL,
25
+ text=True,
26
+ )
27
+ except (OSError, subprocess.CalledProcessError):
28
+ return None
29
+ return result.stdout.strip()
30
+
31
+
32
+ def _git_root(search_dir: Path) -> Optional[Path]:
33
+ output = _run_git(["rev-parse", "--show-toplevel"], search_dir)
34
+ if not output:
35
+ return None
36
+ return Path(output).resolve()
37
+
38
+
39
+ def _github_repo_from_remote(remote_url: str) -> Optional[Tuple[str, str]]:
40
+ value = remote_url.strip()
41
+ if value.endswith(".git"):
42
+ value = value[:-4]
43
+
44
+ if value.startswith("git@github.com:"):
45
+ value = value[len("git@github.com:"):]
46
+ elif value.startswith("https://github.com/"):
47
+ value = value[len("https://github.com/"):]
48
+ elif value.startswith("http://github.com/"):
49
+ value = value[len("http://github.com/"):]
50
+ else:
51
+ return None
52
+
53
+ parts = value.split("/")
54
+ if len(parts) != 2 or not parts[0] or not parts[1]:
55
+ return None
56
+ return parts[0], parts[1]
57
+
58
+
59
+ def resolve_git_context(artifacts_folder: Path) -> Tuple[Optional[Path], Optional[Tuple[str, str]]]:
60
+ for search_dir in (Path.cwd(), artifacts_folder):
61
+ root = _git_root(search_dir)
62
+ if not root:
63
+ continue
64
+ remote = _run_git(["remote", "get-url", "origin"], root) or ""
65
+ return root, _github_repo_from_remote(remote)
66
+ return None, None
67
+
68
+
69
+ def default_package_name(artifacts_folder: Path) -> str:
70
+ git_root, github_repo = resolve_git_context(artifacts_folder)
71
+ if github_repo:
72
+ owner, repo = github_repo
73
+ return "gh:{}/{}".format(owner, repo)
74
+ if git_root:
75
+ return git_root.name
76
+ return Path.cwd().name
77
+
78
+
79
+ def default_version(artifacts_folder: Path) -> str:
80
+ git_root, _github_repo = resolve_git_context(artifacts_folder)
81
+ if git_root:
82
+ tag = _run_git(["describe", "--tags", "--exact-match"], git_root)
83
+ if tag:
84
+ return tag
85
+ commit = _run_git(["rev-parse", "--short", "HEAD"], git_root)
86
+ if commit:
87
+ return commit
88
+ return datetime.datetime.now().strftime("%Y%m%d%H%M%S")
89
+
90
+
91
+ def github_repo_description(artifacts_folder: Path) -> str:
92
+ _git_root, github_repo = resolve_git_context(artifacts_folder)
93
+ if not github_repo:
94
+ return ""
95
+
96
+ owner, repo = github_repo
97
+ headers = {"Accept": "application/vnd.github+json"}
98
+ token = os.getenv("GITHUB_TOKEN")
99
+ if token:
100
+ headers["Authorization"] = "Bearer {}".format(token)
101
+
102
+ try:
103
+ response = requests.get(
104
+ "https://api.github.com/repos/{}/{}".format(owner, repo),
105
+ headers=headers,
106
+ timeout=5,
107
+ )
108
+ response.raise_for_status()
109
+ description = response.json().get("description")
110
+ except Exception:
111
+ return ""
112
+ return description if isinstance(description, str) else ""
113
+
114
+
115
+ def _sha256_file(path: Path) -> str:
116
+ hasher = hashlib.sha256()
117
+ with path.open("rb") as file_obj:
118
+ for chunk in iter(lambda: file_obj.read(1024 * 1024), b""):
119
+ hasher.update(chunk)
120
+ return hasher.hexdigest()
121
+
122
+
123
+ def _format_size(total_bytes: int) -> str:
124
+ units = (("GB", 1024 ** 3), ("MB", 1024 ** 2), ("KB", 1024))
125
+ if total_bytes <= 0:
126
+ return "0KB"
127
+ for suffix, factor in units:
128
+ if total_bytes >= factor:
129
+ value = total_bytes / factor
130
+ text = "{:.1f}".format(value).rstrip("0").rstrip(".")
131
+ return "{}{}".format(text, suffix)
132
+ return "1KB"
133
+
134
+
135
+ def _matches_exclude_pattern(resource: str, patterns: Sequence[str]) -> bool:
136
+ filename = Path(resource).name
137
+ return any(pattern and (pattern in resource or pattern in filename) for pattern in patterns)
138
+
139
+
140
+ def collect_artifact_resources(
141
+ artifacts_folder: Path,
142
+ exclude_patterns: Optional[Sequence[str]] = None,
143
+ ) -> Tuple[List[str], Dict[str, Path], int]:
144
+ resources = []
145
+ resource_paths = {}
146
+ total_size = 0
147
+ normalized_excludes = [pattern.strip() for pattern in (exclude_patterns or []) if pattern.strip()]
148
+
149
+ for path in sorted(artifacts_folder.rglob("*")):
150
+ if not path.is_file():
151
+ continue
152
+ rel = path.relative_to(artifacts_folder).as_posix()
153
+ if rel == METADATA_FILENAME or re.fullmatch(r"metadata-[A-Za-z0-9_.-]+\.json", rel):
154
+ continue
155
+ if _matches_exclude_pattern(rel, normalized_excludes):
156
+ continue
157
+ resources.append(rel)
158
+ resource_paths[rel] = path
159
+ total_size += path.stat().st_size
160
+
161
+ return resources, resource_paths, total_size
162
+
163
+
164
+ def parse_selectables(selectables: Optional[str]) -> List[Tuple[str, str]]:
165
+ if not selectables:
166
+ return []
167
+
168
+ parsed = []
169
+ for raw_item in selectables.split(";"):
170
+ item = raw_item.strip()
171
+ if not item:
172
+ continue
173
+ if ":" not in item:
174
+ raise ValueError("selectables entries must use 'name:file' format")
175
+ name, resource = item.split(":", 1)
176
+ name = name.strip()
177
+ resource = resource.strip()
178
+ if not name or not resource:
179
+ raise ValueError("selectables entries must include both name and file")
180
+ parsed.append((name, resource))
181
+ return parsed
182
+
183
+
184
+ def build_selectable_resources(
185
+ selectable_specs: List[Tuple[str, str]],
186
+ resource_paths: Dict[str, Path],
187
+ ) -> List[Dict[str, str]]:
188
+ selectable_resources = []
189
+ seen_resources = set()
190
+
191
+ for name, resource in selectable_specs:
192
+ if resource not in resource_paths:
193
+ raise ValueError("selectable resource is not in artifacts-folder: {}".format(resource))
194
+ if resource in seen_resources:
195
+ raise ValueError("duplicate selectable resource: {}".format(resource))
196
+ seen_resources.add(resource)
197
+ selectable_resources.append({
198
+ "name": name,
199
+ "url": "",
200
+ "resource": resource,
201
+ "checksum": _sha256_file(resource_paths[resource]),
202
+ })
203
+
204
+ return selectable_resources
205
+
206
+
207
+ def resolve_install_script(artifacts_folder: Path, install_script: str) -> str:
208
+ script = install_script.strip()
209
+ if not script:
210
+ raise ValueError("install-script cannot be empty")
211
+
212
+ candidate_paths = []
213
+ raw_path = Path(script).expanduser()
214
+ if raw_path.is_absolute():
215
+ candidate_paths.append(raw_path)
216
+ else:
217
+ candidate_paths.append(artifacts_folder / raw_path)
218
+ candidate_paths.append(Path.cwd() / raw_path)
219
+
220
+ artifacts_root = artifacts_folder.resolve()
221
+ for candidate in candidate_paths:
222
+ try:
223
+ resolved = candidate.resolve()
224
+ except OSError:
225
+ continue
226
+ if not resolved.is_file():
227
+ continue
228
+ try:
229
+ rel = resolved.relative_to(artifacts_root).as_posix()
230
+ except ValueError:
231
+ continue
232
+ return "./{}".format(rel)
233
+
234
+ return script
235
+
236
+
237
+ def build_metadata(
238
+ artifacts_folder: Path,
239
+ name: Optional[str] = None,
240
+ version: Optional[str] = None,
241
+ description: Optional[str] = None,
242
+ install_script: str = "",
243
+ selectables: Optional[str] = None,
244
+ exclude: Optional[Sequence[str]] = None,
245
+ ) -> Dict:
246
+ artifacts_folder = artifacts_folder.expanduser().resolve()
247
+ if not artifacts_folder.is_dir():
248
+ raise ValueError("artifacts-folder is not a directory: {}".format(artifacts_folder))
249
+
250
+ resources, resource_paths, total_size = collect_artifact_resources(artifacts_folder, exclude_patterns=exclude)
251
+ if not resources:
252
+ raise ValueError("artifacts-folder does not contain any artifact files")
253
+
254
+ selectable_specs = parse_selectables(selectables)
255
+ selectable_resources = build_selectable_resources(selectable_specs, resource_paths)
256
+ selectable_resource_names = {entry["resource"] for entry in selectable_resources}
257
+ resources = [resource for resource in resources if resource not in selectable_resource_names]
258
+ checksums = {
259
+ resource: _sha256_file(path)
260
+ for resource, path in resource_paths.items()
261
+ if resource not in selectable_resource_names
262
+ }
263
+
264
+ resolved_description = description
265
+ if resolved_description is None:
266
+ resolved_description = github_repo_description(artifacts_folder)
267
+
268
+ size_text = _format_size(total_size)
269
+ return {
270
+ "name": name or default_package_name(artifacts_folder),
271
+ "version": version or default_version(artifacts_folder),
272
+ "release": "",
273
+ "description": resolved_description or "",
274
+ "platforms": [],
275
+ "resources": resources,
276
+ "resources-checksum": checksums,
277
+ "selectable-resources": selectable_resources,
278
+ "size": {
279
+ "download": size_text,
280
+ "install": size_text,
281
+ },
282
+ "installation": {
283
+ "script": resolve_install_script(artifacts_folder, install_script),
284
+ "post-message": DEFAULT_POST_MESSAGE,
285
+ },
286
+ }
287
+
288
+
289
+ def metadata_filename(variant: Optional[str] = None) -> str:
290
+ if variant is None:
291
+ return METADATA_FILENAME
292
+
293
+ normalized = variant.strip()
294
+ if not normalized:
295
+ return METADATA_FILENAME
296
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", normalized):
297
+ raise ValueError("variant must contain only letters, numbers, dots, underscores, or hyphens")
298
+ return "metadata-{}.json".format(normalized)
299
+
300
+
301
+ def write_metadata(artifacts_folder: Path, metadata: Dict, variant: Optional[str] = None) -> Path:
302
+ output_path = artifacts_folder.expanduser().resolve() / metadata_filename(variant)
303
+ output_path.write_text(json.dumps(metadata, indent=4) + "\n", encoding="utf-8")
304
+ return output_path
@@ -19,6 +19,7 @@ import json
19
19
  import click
20
20
  import datetime
21
21
  from pathlib import Path
22
+ from sima_cli.install.package_builder import build_metadata, write_metadata
22
23
  from rich.table import Table
23
24
  from rich.console import Console
24
25
  from rich.syntax import Syntax
@@ -74,6 +75,54 @@ def show_metadata(name, version):
74
75
  except ValueError as e:
75
76
  Console().print(f"[red]Error:[/red] {e}")
76
77
 
78
+
79
+ @packages.command("build", help="Build package metadata.json from an artifacts folder.")
80
+ @click.argument(
81
+ "artifacts_folder",
82
+ metavar="ARTIFACTS_FOLDER",
83
+ type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
84
+ )
85
+ @click.option("--name", help="Package name. Defaults to gh:<org>/<repo> for GitHub repos.")
86
+ @click.option("--version", help="Package version. Defaults to exact git tag or short commit hash.")
87
+ @click.option("--description", help="Package description. Defaults to the GitHub repo description when available.")
88
+ @click.option(
89
+ "--install-script",
90
+ required=True,
91
+ help="Install script file inside ARTIFACTS_FOLDER, or a single-line shell command.",
92
+ )
93
+ @click.option(
94
+ "--selectables",
95
+ help="Optional resources in 'name1:file1;name2:file2' format.",
96
+ )
97
+ @click.option(
98
+ "--exclude",
99
+ multiple=True,
100
+ help="Exclude artifact files whose relative path or filename contains this text. May be repeated.",
101
+ )
102
+ @click.option(
103
+ "--variant",
104
+ help="Optional metadata variant name. Writes metadata-<variant>.json instead of metadata.json.",
105
+ )
106
+ def build_package_metadata(artifacts_folder, name, version, description, install_script, selectables, exclude, variant):
107
+ """
108
+ Generate metadata.json, or metadata-<variant>.json, for sima-cli package installation.
109
+ """
110
+ try:
111
+ metadata = build_metadata(
112
+ artifacts_folder=artifacts_folder,
113
+ name=name,
114
+ version=version,
115
+ description=description,
116
+ install_script=install_script,
117
+ selectables=selectables,
118
+ exclude=exclude,
119
+ )
120
+ output_path = write_metadata(artifacts_folder, metadata, variant=variant)
121
+ except ValueError as exc:
122
+ raise click.ClickException(str(exc))
123
+
124
+ click.echo(f"✅ Package metadata written to: {output_path}")
125
+
77
126
  # ------------------------------------------------------------------------------------------------------------
78
127
  # Register the group to main CLI entrypoint, skip this group if it's running inside the SDK container already
79
128
  # ------------------------------------------------------------------------------------------------------------
@@ -38,7 +38,7 @@ console = Console()
38
38
  # ------------------------------------------------------------
39
39
  # Group Definition
40
40
  # ------------------------------------------------------------
41
- @click.group(hidden=True, context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
41
+ @click.group(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
42
42
  @click.option(
43
43
  "-v", "--version",
44
44
  "version_filter",