Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d76e271
feat: add `specify artifact` command exposing composition stacks as JSON
nicolehaugen Aug 21, 2026
64aa2eb
Potential fix for pull request finding 'Module is imported with 'impo…
nicolehaugen Aug 24, 2026
2988e0f
Potential fix for pull request finding 'Module is imported with 'impo…
nicolehaugen Aug 24, 2026
812ac94
Potential fix for pull request finding 'Unused import'
nicolehaugen Aug 24, 2026
83f9f8d
Project preset artifacts by entry type
Copilot Aug 24, 2026
4d14990
Represent project override artifact layers
Copilot Aug 24, 2026
51a0015
Preserve artifact JSON init-dir errors
Copilot Aug 24, 2026
4b02616
Canonicalize core script artifacts
Copilot Aug 24, 2026
5bf5b3a
Potential fix for pull request finding
nicolehaugen Aug 24, 2026
0be89c8
Fix artifact inventory resolver filtering
Copilot Aug 24, 2026
792c276
Add resolver tests for single-runtime core scripts
Copilot Aug 24, 2026
dcf492a
Cache artifact resolver lookups
Copilot Aug 24, 2026
6689003
Handle artifact resolver failures
Copilot Aug 24, 2026
f07d764
Document artifact resolution error
Copilot Aug 24, 2026
e8a806d
Include convention-based artifacts in inventory
Copilot Aug 24, 2026
6089bab
Restore legacy flat core script lookup
Copilot Aug 24, 2026
29262bc
Extend convention discovery to presets in artifact inventory
Copilot Aug 24, 2026
59b96c8
Fix manifest path portability and export ArtifactResolutionError
Copilot Aug 24, 2026
27f1481
Bound artifact manifest search to project root
Copilot Aug 24, 2026
45338b6
Cover project-root artifact manifests
Copilot Aug 24, 2026
ac09641
Handle directory artifact manifest lookups
Copilot Aug 24, 2026
43cf9bc
Fall back to top-level preset name in artifact stacks
Copilot Aug 24, 2026
ca42671
Include project-local core artifacts in inventory
Copilot Aug 24, 2026
7e3b50d
Address inline review feedback on artifact resolver helpers
Copilot Aug 24, 2026
985b713
Reuse manifest/registry APIs in artifact contribution enumeration
Copilot Aug 24, 2026
1ae8d1b
Pass layer explicitly to _iter_pack_contributions instead of inferrin…
Copilot Aug 24, 2026
bcaa172
Fix core command namespacing and validate names for kind-scoped lookups
Copilot Aug 24, 2026
f2483b3
Skip manifest contributions without a usable identifier
Copilot Aug 24, 2026
502b385
Hoist test-local imports to module scope in artifact/assets tests
Copilot Aug 24, 2026
0a7ea11
fix: resolve artifact inventory and validation review regressions
Copilot Aug 24, 2026
04535be
perf: avoid duplicate read in core command inventory
Copilot Aug 24, 2026
d601a0c
fix: classify dotted override-only artifacts as commands
Copilot Aug 24, 2026
8716f8e
fix: accept single-segment artifact commands
Copilot Aug 24, 2026
8020678
fix: fail closed on corrupt artifact registries
Copilot Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,13 @@ def _require_specify_project() -> Path:
_register_preset_cmds(app)


# ===== Artifact Commands =====

# Read-only introspection over the composed inventory (commands/templates/scripts).
from .artifacts._commands import register as _register_artifact_cmds # noqa: E402
_register_artifact_cmds(app)


# ===== Bundle Commands =====

# Bundler subcommand group (specify bundle ...) — see commands/bundle/.
Expand Down
26 changes: 26 additions & 0 deletions src/specify_cli/_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ def _repo_root() -> Path:
return Path(__file__).parent.parent.parent


def _locate_core_asset_dir(subdir: str) -> Path | None:
"""Return the on-disk directory holding a family of core assets, or None.

``subdir`` is one of ``"commands"``, ``"templates"``, or ``"scripts"`` —
the three asset families every core baseline consumer needs to agree on.
Prefers the wheel-installed ``core_pack`` bundle, then falls back to the
source-checkout layout. This is the single place that knows the two-tier
resolution ("wheel bundle, else repo-root checkout") for locating core
assets, so callers (extension command-name discovery, the preset
resolver's core fallback, and the artifact command's core-baseline
enumeration) cannot silently diverge on what "core" means on a given
machine.
"""
core = _locate_core_pack()
if core is not None:
candidate = core / subdir
return candidate if candidate.is_dir() else None
if subdir == "commands":
candidate = _repo_root() / "templates" / "commands"
elif subdir in ("templates", "scripts"):
candidate = _repo_root() / subdir
else: # pragma: no cover — internal misuse
return None
return candidate if candidate.is_dir() else None


def _locate_bundled_extension(extension_id: str) -> Path | None:
"""Return the path to a bundled extension, or None.

Expand Down
20 changes: 20 additions & 0 deletions src/specify_cli/_identifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ def derive_named_id(layer: str, source_id: str, kind: str, name: str) -> str:
return f"{layer}:{source_id}:{kind}:{name}"


_LAYER_KINDS = frozenset({"core", PROJECT_OVERRIDE_LAYER, "preset", "extension"})


def layer_kind_from_lookup_id(lookup_id: str) -> str | None:
"""Return the layer segment of a resolved-stack ``lookupId``, or ``None``.

``lookupId`` values on resolved stack layers follow the same
``"{layer}:..."`` grammar as manifest-contribution ``id`` values (see
module docstring), with ``layer`` additionally taking on
:data:`PROJECT_OVERRIDE_LAYER` for resolver-only project-override layers.
This is the single place that knows the set of valid layer prefixes, so
consumers can classify a lookupId without re-deriving the grammar via
string-prefix checks of their own.
"""
layer, _, rest = lookup_id.partition(":")
if not rest or layer not in _LAYER_KINDS:
return None
return layer


def canonical_json(value: Any) -> bytes:
"""Serialize ``value`` to a canonical UTF-8 JSON byte string.

Expand Down
32 changes: 32 additions & 0 deletions src/specify_cli/_script_variants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Canonical names and paths for the core script runtime variants."""

from __future__ import annotations

from collections.abc import Iterator
from pathlib import Path

_SCRIPT_VARIANTS = (
("bash", ".sh", False),
("powershell", ".ps1", False),
("python", ".py", True),
)


def canonical_script_name(path: Path) -> str | None:
"""Return the logical name shared by a core script's runtime variants."""
for runtime, suffix, uses_underscores in _SCRIPT_VARIANTS:
if path.parent.name == runtime and path.suffix == suffix:
return path.stem.replace("_", "-") if uses_underscores else path.stem
return None


def script_variant_paths(scripts_dir: Path, name: str) -> Iterator[Path]:
"""Yield candidate paths for the logical script *name*.

The legacy flat Bash path (``<scripts_dir>/<name>.sh``) is yielded first so
existing projects keep working, followed by the runtime-specific paths.
"""
yield scripts_dir / f"{name}.sh"
for runtime, suffix, uses_underscores in _SCRIPT_VARIANTS:
stem = name.replace("-", "_") if uses_underscores else name
yield scripts_dir / runtime / f"{stem}{suffix}"
Loading