1919 from apm_cli .integration .targets import TargetProfile
2020
2121
22+ def _package_field (apm_package : Any , name : str ) -> Any :
23+ """Return a real APMPackage field without treating MagicMock attrs as set."""
24+ if apm_package is None :
25+ return None
26+ try :
27+ attrs = vars (apm_package )
28+ except TypeError :
29+ attrs = {}
30+ if name in attrs :
31+ return attrs [name ]
32+ value = getattr (apm_package , name , None )
33+ if type (value ).__module__ == "unittest.mock" :
34+ return None
35+ return value
36+
37+
38+ def _package_target_value (apm_package : Any ) -> str | list [str ] | None :
39+ """Read singular target or plural targets from the parsed package model."""
40+ from apm_cli .core .apm_yml import parse_targets_field
41+
42+ target = _package_field (apm_package , "target" )
43+ targets = _package_field (apm_package , "targets" )
44+ if target is not None and targets is not None :
45+ parse_targets_field ({"target" : target , "targets" : targets })
46+ if targets is not None :
47+ parsed = parse_targets_field ({"targets" : targets })
48+ return parsed if parsed else None
49+ return target
50+
51+
52+ def _raise_target_usage_error (ctx : Any , exc : Exception ) -> None :
53+ """Render target field user errors consistently before exiting."""
54+ if ctx .logger :
55+ ctx .logger .error (str (exc ), symbol = "" )
56+ raise SystemExit (2 ) from exc
57+
58+
59+ def _as_yaml_targets (value : str | list [str ] | None ) -> list [str ] | None :
60+ """Normalize a package target value to the v2 yaml_targets shape."""
61+ if value is None :
62+ return None
63+ if isinstance (value , str ):
64+ parts = [t .strip () for t in value .split ("," ) if t .strip ()]
65+ else :
66+ parts = [str (t ).strip () for t in value if str (t ).strip ()]
67+ return parts or None
68+
69+
2270def _read_yaml_targets (ctx ) -> list [str ] | None :
2371 """Read targets/target from raw apm.yml using v2 parser.
2472
@@ -29,10 +77,10 @@ def _read_yaml_targets(ctx) -> list[str] | None:
2977 return None
3078 apm_yml_path = getattr (ctx .apm_package , "package_path" , None )
3179 if apm_yml_path is None :
32- return None
80+ return _as_yaml_targets ( _package_target_value ( ctx . apm_package ))
3381 manifest = apm_yml_path / "apm.yml"
3482 if not manifest .exists ():
35- return None
83+ return _as_yaml_targets ( _package_target_value ( ctx . apm_package ))
3684 try :
3785 from apm_cli .utils .yaml_io import load_yaml
3886
@@ -92,6 +140,8 @@ def run(ctx: InstallContext) -> None:
92140 On return ``ctx.targets`` and ``ctx.integrators`` are populated.
93141 """
94142
143+ import click as _click
144+
95145 from apm_cli .core .scope import InstallScope
96146 from apm_cli .core .target_detection import (
97147 detect_target ,
@@ -113,8 +163,11 @@ def run(ctx: InstallContext) -> None:
113163 resolve_targets as _resolve_targets_legacy ,
114164 )
115165
116- # Get config target from apm.yml if available
117- config_target = ctx .apm_package .target
166+ # Get config target from apm.yml if available.
167+ try :
168+ config_target = _package_target_value (ctx .apm_package )
169+ except _click .UsageError as exc :
170+ _raise_target_usage_error (ctx , exc )
118171
119172 # Resolve effective explicit target: CLI --target wins, then apm.yml
120173 _explicit = ctx .target_override or config_target or None
@@ -303,18 +356,12 @@ def run(ctx: InstallContext) -> None:
303356 # Read targets from apm.yml (supports both target: and targets:)
304357 _v2_yaml : list [str ] | None = None
305358 if _v2_flag is None and not ctx .target_override :
306- import click as _click
307-
308359 try :
309360 _v2_yaml = _read_yaml_targets (ctx )
310361 except _click .UsageError as exc :
311362 # ConflictingTargetsError (both target: and targets: in
312363 # apm.yml) is a user error -- surface with exit code 2.
313- # The renderer already emits a leading "[x]"; pass an
314- # empty symbol so logger.error doesn't double-prefix.
315- if ctx .logger :
316- ctx .logger .error (str (exc ), symbol = "" )
317- raise SystemExit (2 ) from exc
364+ _raise_target_usage_error (ctx , exc )
318365
319366 # Skip v2 entirely when all override targets were non-canonical
320367 # (e.g. copilot-cowork only). Those are fully handled by the
@@ -400,7 +447,9 @@ def run(ctx: InstallContext) -> None:
400447 # Keep any legacy-only targets (e.g. copilot-cowork) that v2
401448 # doesn't handle.
402449 _v2_names = {t .name for t in _v2_targets }
403- _legacy_only = [t for t in _targets if t .name not in _v2_names ]
450+ _legacy_only = [
451+ t for t in _targets if t .name not in _v2_names and t .name not in _CANONICAL
452+ ]
404453 _targets = _v2_targets + _legacy_only
405454
406455 else :
@@ -479,6 +528,8 @@ def run_targets_phase(ctx) -> None:
479528 """
480529 from pathlib import Path
481530
531+ import click as _click
532+
482533 from apm_cli .core .target_detection import resolve_targets
483534 from apm_cli .integration .targets import KNOWN_TARGETS
484535
@@ -494,14 +545,11 @@ def run_targets_phase(ctx) -> None:
494545 else :
495546 flag = ctx .target_override
496547
497- # Get yaml_targets from apm_package
498- yaml_targets : list [str ] | None = None
499- if ctx .apm_package and ctx .apm_package .target :
500- raw = ctx .apm_package .target
501- if isinstance (raw , str ):
502- yaml_targets = [t .strip () for t in raw .split ("," ) if t .strip ()]
503- elif isinstance (raw , list ):
504- yaml_targets = raw
548+ # Get yaml_targets from apm_package.
549+ try :
550+ yaml_targets = _as_yaml_targets (_package_target_value (ctx .apm_package ))
551+ except _click .UsageError as exc :
552+ _raise_target_usage_error (ctx , exc )
505553
506554 # Resolve targets
507555 resolved = resolve_targets (project_root , flag = flag , yaml_targets = yaml_targets )
0 commit comments