scc-cli 1.4.1__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.

Potentially problematic release.


This version of scc-cli might be problematic. Click here for more details.

Files changed (113) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +259 -0
  8. scc_cli/cli_admin.py +706 -0
  9. scc_cli/cli_audit.py +245 -0
  10. scc_cli/cli_common.py +166 -0
  11. scc_cli/cli_config.py +527 -0
  12. scc_cli/cli_exceptions.py +705 -0
  13. scc_cli/cli_helpers.py +244 -0
  14. scc_cli/cli_init.py +272 -0
  15. scc_cli/cli_launch.py +1454 -0
  16. scc_cli/cli_org.py +1428 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +892 -0
  19. scc_cli/cli_worktree.py +865 -0
  20. scc_cli/config.py +583 -0
  21. scc_cli/console.py +562 -0
  22. scc_cli/constants.py +79 -0
  23. scc_cli/contexts.py +377 -0
  24. scc_cli/deprecation.py +54 -0
  25. scc_cli/deps.py +189 -0
  26. scc_cli/docker/__init__.py +127 -0
  27. scc_cli/docker/core.py +466 -0
  28. scc_cli/docker/credentials.py +726 -0
  29. scc_cli/docker/launch.py +604 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1074 -0
  32. scc_cli/doctor/render.py +346 -0
  33. scc_cli/doctor/types.py +66 -0
  34. scc_cli/errors.py +288 -0
  35. scc_cli/evaluation/__init__.py +27 -0
  36. scc_cli/evaluation/apply_exceptions.py +207 -0
  37. scc_cli/evaluation/evaluate.py +97 -0
  38. scc_cli/evaluation/models.py +80 -0
  39. scc_cli/exit_codes.py +55 -0
  40. scc_cli/git.py +1521 -0
  41. scc_cli/json_command.py +166 -0
  42. scc_cli/json_output.py +96 -0
  43. scc_cli/kinds.py +62 -0
  44. scc_cli/marketplace/__init__.py +123 -0
  45. scc_cli/marketplace/adapter.py +74 -0
  46. scc_cli/marketplace/compute.py +377 -0
  47. scc_cli/marketplace/constants.py +87 -0
  48. scc_cli/marketplace/managed.py +135 -0
  49. scc_cli/marketplace/materialize.py +723 -0
  50. scc_cli/marketplace/normalize.py +548 -0
  51. scc_cli/marketplace/render.py +257 -0
  52. scc_cli/marketplace/resolve.py +459 -0
  53. scc_cli/marketplace/schema.py +506 -0
  54. scc_cli/marketplace/sync.py +260 -0
  55. scc_cli/marketplace/team_cache.py +195 -0
  56. scc_cli/marketplace/team_fetch.py +688 -0
  57. scc_cli/marketplace/trust.py +244 -0
  58. scc_cli/models/__init__.py +41 -0
  59. scc_cli/models/exceptions.py +273 -0
  60. scc_cli/models/plugin_audit.py +434 -0
  61. scc_cli/org_templates.py +269 -0
  62. scc_cli/output_mode.py +167 -0
  63. scc_cli/panels.py +113 -0
  64. scc_cli/platform.py +350 -0
  65. scc_cli/profiles.py +960 -0
  66. scc_cli/remote.py +443 -0
  67. scc_cli/schemas/__init__.py +1 -0
  68. scc_cli/schemas/org-v1.schema.json +456 -0
  69. scc_cli/schemas/team-config.v1.schema.json +163 -0
  70. scc_cli/sessions.py +425 -0
  71. scc_cli/setup.py +588 -0
  72. scc_cli/source_resolver.py +470 -0
  73. scc_cli/stats.py +378 -0
  74. scc_cli/stores/__init__.py +13 -0
  75. scc_cli/stores/exception_store.py +251 -0
  76. scc_cli/subprocess_utils.py +88 -0
  77. scc_cli/teams.py +382 -0
  78. scc_cli/templates/__init__.py +2 -0
  79. scc_cli/templates/org/__init__.py +0 -0
  80. scc_cli/templates/org/minimal.json +19 -0
  81. scc_cli/templates/org/reference.json +74 -0
  82. scc_cli/templates/org/strict.json +38 -0
  83. scc_cli/templates/org/teams.json +42 -0
  84. scc_cli/templates/statusline.sh +75 -0
  85. scc_cli/theme.py +348 -0
  86. scc_cli/ui/__init__.py +124 -0
  87. scc_cli/ui/branding.py +68 -0
  88. scc_cli/ui/chrome.py +395 -0
  89. scc_cli/ui/dashboard/__init__.py +62 -0
  90. scc_cli/ui/dashboard/_dashboard.py +677 -0
  91. scc_cli/ui/dashboard/loaders.py +395 -0
  92. scc_cli/ui/dashboard/models.py +184 -0
  93. scc_cli/ui/dashboard/orchestrator.py +390 -0
  94. scc_cli/ui/formatters.py +443 -0
  95. scc_cli/ui/gate.py +350 -0
  96. scc_cli/ui/help.py +157 -0
  97. scc_cli/ui/keys.py +538 -0
  98. scc_cli/ui/list_screen.py +431 -0
  99. scc_cli/ui/picker.py +700 -0
  100. scc_cli/ui/prompts.py +200 -0
  101. scc_cli/ui/wizard.py +675 -0
  102. scc_cli/update.py +680 -0
  103. scc_cli/utils/__init__.py +39 -0
  104. scc_cli/utils/fixit.py +264 -0
  105. scc_cli/utils/fuzzy.py +124 -0
  106. scc_cli/utils/locks.py +101 -0
  107. scc_cli/utils/ttl.py +376 -0
  108. scc_cli/validate.py +455 -0
  109. scc_cli-1.4.1.dist-info/METADATA +369 -0
  110. scc_cli-1.4.1.dist-info/RECORD +113 -0
  111. scc_cli-1.4.1.dist-info/WHEEL +4 -0
  112. scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
  113. scc_cli-1.4.1.dist-info/licenses/LICENSE +21 -0
scc_cli/validate.py ADDED
@@ -0,0 +1,455 @@
1
+ """
2
+ Schema validation for organization configs.
3
+
4
+ Provide offline-capable validation using bundled JSON schemas.
5
+ Treat $schema field as documentation, not something to fetch at runtime.
6
+
7
+ Key functions:
8
+ - validate_org_config(): Validate org config against bundled schema (org-v1.schema.json)
9
+ - validate_config_invariants(): Validate governance invariants (enabled ⊆ allowed, enabled ∩ blocked = ∅)
10
+ - check_version_compatibility(): Unified version compatibility gate
11
+ - check_schema_version(): Check schema version compatibility
12
+ - check_min_cli_version(): Check CLI meets minimum version requirement
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from dataclasses import dataclass, field
19
+ from importlib.resources import files
20
+ from typing import TYPE_CHECKING, Any, Literal, cast
21
+
22
+ from jsonschema import Draft7Validator
23
+
24
+ from .constants import CLI_VERSION, CURRENT_SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS
25
+
26
+ if TYPE_CHECKING:
27
+ pass
28
+
29
+
30
+ # ═══════════════════════════════════════════════════════════════════════════════
31
+ # Invariant Validation Types
32
+ # ═══════════════════════════════════════════════════════════════════════════════
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class InvariantViolation:
37
+ """Result of a config invariant check.
38
+
39
+ Attributes:
40
+ rule: The invariant rule that was violated (e.g., "enabled_must_be_allowed").
41
+ message: Human-readable description of the violation.
42
+ severity: "error" for hard failures, "warning" for advisory.
43
+ """
44
+
45
+ rule: str
46
+ message: str
47
+ severity: Literal["error", "warning"]
48
+
49
+
50
+ # ═══════════════════════════════════════════════════════════════════════════════
51
+ # Compatibility Result Types
52
+ # ═══════════════════════════════════════════════════════════════════════════════
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class VersionCompatibility:
57
+ """Result of version compatibility check.
58
+
59
+ Attributes:
60
+ compatible: Whether the config is usable with this CLI.
61
+ blocking_error: Error message if not compatible (requires upgrade).
62
+ warnings: Non-blocking warnings (e.g., newer minor version).
63
+ schema_version: Detected schema version from config.
64
+ min_cli_version: Minimum CLI version from config, if specified.
65
+ current_cli_version: Current CLI version for reference.
66
+ """
67
+
68
+ compatible: bool
69
+ blocking_error: str | None = None
70
+ warnings: list[str] = field(default_factory=list)
71
+ schema_version: str | None = None
72
+ min_cli_version: str | None = None
73
+ current_cli_version: str = CLI_VERSION
74
+
75
+
76
+ # ═══════════════════════════════════════════════════════════════════════════════
77
+ # Schema Loading
78
+ # ═══════════════════════════════════════════════════════════════════════════════
79
+
80
+
81
+ def load_bundled_schema(version: str = "v1") -> dict[Any, Any]:
82
+ """
83
+ Load schema from package resources.
84
+
85
+ Args:
86
+ version: Schema version (default: "v1")
87
+
88
+ Returns:
89
+ Schema dict
90
+
91
+ Raises:
92
+ FileNotFoundError: If schema version doesn't exist
93
+ """
94
+ schema_file = files("scc_cli.schemas").joinpath(f"org-{version}.schema.json")
95
+ try:
96
+ content = schema_file.read_text()
97
+ return cast(dict[Any, Any], json.loads(content))
98
+ except FileNotFoundError:
99
+ raise FileNotFoundError(f"Schema version '{version}' not found")
100
+
101
+
102
+ # ═══════════════════════════════════════════════════════════════════════════════
103
+ # Config Validation
104
+ # ═══════════════════════════════════════════════════════════════════════════════
105
+
106
+
107
+ def validate_org_config(config: dict[str, Any], schema_version: str = "v1") -> list[str]:
108
+ """
109
+ Validate org config against bundled schema.
110
+
111
+ Args:
112
+ config: Organization config dict to validate
113
+ schema_version: Schema version to validate against (default: "v1")
114
+
115
+ Returns:
116
+ List of error strings. Empty list means config is valid.
117
+ """
118
+ schema = load_bundled_schema(schema_version)
119
+ validator = Draft7Validator(schema)
120
+
121
+ errors = []
122
+ for error in validator.iter_errors(config):
123
+ # Include config path for easy debugging
124
+ path = "/".join(str(p) for p in error.path) or "(root)"
125
+ errors.append(f"{path}: {error.message}")
126
+
127
+ return errors
128
+
129
+
130
+ # ═══════════════════════════════════════════════════════════════════════════════
131
+ # Version Compatibility Checks
132
+ # ═══════════════════════════════════════════════════════════════════════════════
133
+
134
+
135
+ def parse_semver(version_string: str) -> tuple[int, int, int]:
136
+ """
137
+ Parse semantic version string into tuple of (major, minor, patch).
138
+
139
+ Args:
140
+ version_string: Version string in format "X.Y.Z"
141
+
142
+ Returns:
143
+ Tuple of (major, minor, patch) integers
144
+
145
+ Raises:
146
+ ValueError: If version string is not valid semver format
147
+ """
148
+ try:
149
+ parts = version_string.split(".")
150
+ if len(parts) != 3:
151
+ raise ValueError(f"Invalid semver format: {version_string}")
152
+ return (int(parts[0]), int(parts[1]), int(parts[2]))
153
+ except (ValueError, AttributeError) as e:
154
+ raise ValueError(f"Invalid semver format: {version_string}") from e
155
+
156
+
157
+ def check_schema_version(config_version: str, cli_version: str) -> tuple[bool, str | None]:
158
+ """
159
+ Check schema version compatibility.
160
+
161
+ Compatibility rules:
162
+ - Same major version: compatible
163
+ - Config major > CLI major: incompatible (need CLI upgrade)
164
+ - CLI major > Config major: compatible (CLI is newer)
165
+ - Higher minor in config: compatible with warning (ignore unknown fields)
166
+
167
+ Args:
168
+ config_version: Schema version from org config (e.g., "1.5.0")
169
+ cli_version: Current CLI schema version (e.g., "1.2.0")
170
+
171
+ Returns:
172
+ Tuple of (compatible: bool, message: str | None)
173
+ """
174
+ config_major, config_minor, _ = parse_semver(config_version)
175
+ cli_major, cli_minor, _ = parse_semver(cli_version)
176
+
177
+ # Different major versions: check if upgrade needed
178
+ if config_major > cli_major:
179
+ return (
180
+ False,
181
+ f"Config requires schema v{config_major}.x but CLI only supports v{cli_major}.x. "
182
+ f"Please upgrade SCC CLI.",
183
+ )
184
+
185
+ # Config minor version higher than CLI: warn but continue
186
+ if config_major == cli_major and config_minor > cli_minor:
187
+ return (
188
+ True,
189
+ f"Config uses schema {config_version}, CLI supports {cli_version}. "
190
+ f"Some features may be ignored.",
191
+ )
192
+
193
+ # Compatible
194
+ return (True, None)
195
+
196
+
197
+ def detect_schema_version(config: dict[str, Any]) -> str:
198
+ """
199
+ Detect schema version from config.
200
+
201
+ Currently only v1 is supported. This function validates the format
202
+ and always returns "v1" for any valid semver.
203
+
204
+ Args:
205
+ config: Organization config dict
206
+
207
+ Returns:
208
+ Schema version string (always "v1")
209
+
210
+ Raises:
211
+ ValueError: If schema_version format is invalid
212
+ """
213
+ schema_version = config.get("schema_version", "1.0.0")
214
+
215
+ # Validate format (must be X.Y.Z semver)
216
+ try:
217
+ parts = schema_version.split(".")
218
+ if len(parts) != 3:
219
+ raise ValueError(f"Invalid schema_version format: {schema_version}")
220
+ # Validate all parts are integers
221
+ int(parts[0])
222
+ int(parts[1])
223
+ int(parts[2])
224
+ except (ValueError, AttributeError) as e:
225
+ raise ValueError(f"Invalid schema_version format: {schema_version}") from e
226
+
227
+ # Only v1 schema is supported
228
+ return "v1"
229
+
230
+
231
+ def check_min_cli_version(min_version: str, cli_version: str) -> tuple[bool, str | None]:
232
+ """
233
+ Check if CLI meets minimum version requirement.
234
+
235
+ Args:
236
+ min_version: Minimum required CLI version (from config)
237
+ cli_version: Current CLI version
238
+
239
+ Returns:
240
+ Tuple of (ok: bool, message: str | None)
241
+ """
242
+ min_major, min_minor, min_patch = parse_semver(min_version)
243
+ cli_major, cli_minor, cli_patch = parse_semver(cli_version)
244
+
245
+ # Compare version tuples
246
+ min_tuple = (min_major, min_minor, min_patch)
247
+ cli_tuple = (cli_major, cli_minor, cli_patch)
248
+
249
+ if cli_tuple < min_tuple:
250
+ return (
251
+ False,
252
+ f"Config requires SCC CLI >= {min_version}, but you have {cli_version}. "
253
+ f"Please upgrade SCC CLI.",
254
+ )
255
+
256
+ return (True, None)
257
+
258
+
259
+ # ═══════════════════════════════════════════════════════════════════════════════
260
+ # Unified Compatibility Gate
261
+ # ═══════════════════════════════════════════════════════════════════════════════
262
+
263
+
264
+ def check_version_compatibility(config: dict[str, Any]) -> VersionCompatibility:
265
+ """Check version compatibility for an org config.
266
+
267
+ This is the primary entry point for version validation. It combines:
268
+ 1. Schema version check (major version must be supported)
269
+ 2. Min CLI version check (CLI must meet minimum requirement)
270
+
271
+ The function returns immediately on blocking errors (requires upgrade)
272
+ but collects all warnings for informational purposes.
273
+
274
+ Args:
275
+ config: Organization config dict to validate.
276
+
277
+ Returns:
278
+ VersionCompatibility result with compatibility status and messages.
279
+
280
+ Examples:
281
+ >>> result = check_version_compatibility({"schema_version": "1.0.0"})
282
+ >>> result.compatible
283
+ True
284
+
285
+ >>> result = check_version_compatibility({"min_cli_version": "99.0.0"})
286
+ >>> result.compatible
287
+ False
288
+ >>> "upgrade" in result.blocking_error.lower()
289
+ True
290
+ """
291
+ warnings: list[str] = []
292
+ schema_version = config.get("schema_version")
293
+ min_cli_version = config.get("min_cli_version")
294
+
295
+ # Check schema version compatibility
296
+ if schema_version:
297
+ try:
298
+ schema_ok, schema_msg = check_schema_version(schema_version, CURRENT_SCHEMA_VERSION)
299
+ if not schema_ok:
300
+ return VersionCompatibility(
301
+ compatible=False,
302
+ blocking_error=schema_msg,
303
+ schema_version=schema_version,
304
+ min_cli_version=min_cli_version,
305
+ )
306
+ if schema_msg: # Warning but still compatible
307
+ warnings.append(schema_msg)
308
+ except ValueError as e:
309
+ return VersionCompatibility(
310
+ compatible=False,
311
+ blocking_error=f"Invalid schema_version format: {e}",
312
+ schema_version=schema_version,
313
+ min_cli_version=min_cli_version,
314
+ )
315
+
316
+ # Validate schema version is in supported list
317
+ if schema_version:
318
+ detected = detect_schema_version(config)
319
+ if detected not in SUPPORTED_SCHEMA_VERSIONS:
320
+ return VersionCompatibility(
321
+ compatible=False,
322
+ blocking_error=(
323
+ f"Schema version '{detected}' is not supported. "
324
+ f"Supported versions: {', '.join(SUPPORTED_SCHEMA_VERSIONS)}"
325
+ ),
326
+ schema_version=schema_version,
327
+ min_cli_version=min_cli_version,
328
+ )
329
+
330
+ # Check minimum CLI version
331
+ if min_cli_version:
332
+ try:
333
+ cli_ok, cli_msg = check_min_cli_version(min_cli_version, CLI_VERSION)
334
+ if not cli_ok:
335
+ return VersionCompatibility(
336
+ compatible=False,
337
+ blocking_error=cli_msg,
338
+ schema_version=schema_version,
339
+ min_cli_version=min_cli_version,
340
+ )
341
+ except ValueError as e:
342
+ return VersionCompatibility(
343
+ compatible=False,
344
+ blocking_error=f"Invalid min_cli_version format: {e}",
345
+ schema_version=schema_version,
346
+ min_cli_version=min_cli_version,
347
+ )
348
+
349
+ # All checks passed
350
+ return VersionCompatibility(
351
+ compatible=True,
352
+ warnings=warnings,
353
+ schema_version=schema_version,
354
+ min_cli_version=min_cli_version,
355
+ )
356
+
357
+
358
+ # ═══════════════════════════════════════════════════════════════════════════════
359
+ # Config Invariant Validation
360
+ # ═══════════════════════════════════════════════════════════════════════════════
361
+
362
+
363
+ def validate_config_invariants(config: dict[str, Any]) -> list[InvariantViolation]:
364
+ """Validate governance invariants on raw dict config.
365
+
366
+ This function checks semantic constraints that JSON Schema cannot express:
367
+ - enabled plugins must be subset of allowed (enabled ⊆ allowed)
368
+ - enabled plugins must not be blocked (enabled ∩ blocked = ∅)
369
+
370
+ Called AFTER Pydantic structural validation passes in the Validation Gate.
371
+ Works on raw dicts because that's what the CLI uses throughout.
372
+
373
+ Args:
374
+ config: Organization config dict (raw, not Pydantic model).
375
+
376
+ Returns:
377
+ List of InvariantViolation objects. Empty list means all invariants satisfied.
378
+
379
+ Semantics for allowed_plugins:
380
+ - Missing/None: unrestricted (all plugins allowed)
381
+ - []: deny all (no plugins allowed)
382
+ - ["*"]: explicit unrestricted (all plugins allowed via wildcard)
383
+ - ["pattern@marketplace"]: specific whitelist with fnmatch patterns
384
+
385
+ Examples:
386
+ >>> config = {"defaults": {"enabled_plugins": ["a@mp"]}}
387
+ >>> validate_config_invariants(config) # Missing allowed = unrestricted
388
+ []
389
+
390
+ >>> config = {"defaults": {"enabled_plugins": ["a@mp"], "allowed_plugins": []}}
391
+ >>> violations = validate_config_invariants(config) # Empty = deny all
392
+ >>> len(violations) == 1 and violations[0].rule == "enabled_must_be_allowed"
393
+ True
394
+ """
395
+ # Import here to avoid circular dependency
396
+ from scc_cli.marketplace.normalize import matches_pattern
397
+
398
+ violations: list[InvariantViolation] = []
399
+
400
+ # Extract config sections with safe defaults
401
+ defaults = config.get("defaults", {})
402
+ enabled = defaults.get("enabled_plugins", [])
403
+ allowed = defaults.get("allowed_plugins") # None = unrestricted
404
+
405
+ security = config.get("security", {})
406
+ blocked = security.get("blocked_plugins", [])
407
+
408
+ # ─────────────────────────────────────────────────────────────────────────
409
+ # Invariant 1: enabled ⊆ allowed (enabled plugins must be in allowed list)
410
+ # ─────────────────────────────────────────────────────────────────────────
411
+ if allowed is not None:
412
+ if allowed == []:
413
+ # Empty array = nothing allowed (explicit deny all)
414
+ for plugin in enabled:
415
+ violations.append(
416
+ InvariantViolation(
417
+ rule="enabled_must_be_allowed",
418
+ message=(
419
+ f"Plugin '{plugin}' is enabled but allowed_plugins is empty "
420
+ "(nothing allowed)"
421
+ ),
422
+ severity="error",
423
+ )
424
+ )
425
+ elif allowed != ["*"]:
426
+ # Specific whitelist - check each enabled plugin against patterns
427
+ for plugin in enabled:
428
+ if not any(matches_pattern(plugin, pattern) for pattern in allowed):
429
+ violations.append(
430
+ InvariantViolation(
431
+ rule="enabled_must_be_allowed",
432
+ message=f"Plugin '{plugin}' is enabled but not in allowed list",
433
+ severity="error",
434
+ )
435
+ )
436
+ # If allowed == ["*"], all plugins are allowed (explicit unrestricted)
437
+
438
+ # ─────────────────────────────────────────────────────────────────────────
439
+ # Invariant 2: enabled ∩ blocked = ∅ (enabled must not be blocked)
440
+ # ─────────────────────────────────────────────────────────────────────────
441
+ for plugin in enabled:
442
+ for pattern in blocked:
443
+ if matches_pattern(plugin, pattern):
444
+ violations.append(
445
+ InvariantViolation(
446
+ rule="enabled_not_blocked",
447
+ message=(
448
+ f"Plugin '{plugin}' is enabled but matches blocked pattern '{pattern}'"
449
+ ),
450
+ severity="error",
451
+ )
452
+ )
453
+ break # One match is enough to flag this plugin
454
+
455
+ return violations