lightnow-cli 1.6.0__tar.gz → 1.6.1__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 (54) hide show
  1. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/.github/workflows/ci.yml +20 -1
  2. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/.github/workflows/release.yml +3 -3
  3. lightnow_cli-1.6.1/.release-please-manifest.json +3 -0
  4. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/CHANGELOG.md +9 -0
  5. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/PKG-INFO +2 -2
  6. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/README.md +1 -1
  7. lightnow_cli-1.6.1/ci/create_client_artifact.py +126 -0
  8. lightnow_cli-1.6.1/ci/test_client_artifact.py +124 -0
  9. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/docs/release.md +23 -0
  10. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/__init__.py +1 -1
  11. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/commands/query.py +1 -1
  12. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/pyproject.toml +1 -1
  13. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/renovate.json +2 -2
  14. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_cli.py +44 -2
  15. lightnow_cli-1.6.0/.release-please-manifest.json +0 -3
  16. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/.github/workflows/release-please.yml +0 -0
  17. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/.gitignore +0 -0
  18. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/CONTRIBUTING.md +0 -0
  19. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/LICENSE +0 -0
  20. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/Makefile +0 -0
  21. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/SECURITY.md +0 -0
  22. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/docs/development.md +0 -0
  23. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/examples/README.md +0 -0
  24. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/examples/docs.md +0 -0
  25. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/examples/openapi.json +0 -0
  26. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/examples/server.json +0 -0
  27. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/authenticated_http.py +0 -0
  28. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/client.py +0 -0
  29. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/commands/__init__.py +0 -0
  30. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/commands/auth.py +0 -0
  31. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/commands/context.py +0 -0
  32. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/commands/integrations.py +0 -0
  33. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/commands/publish.py +0 -0
  34. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/commands/runner.py +0 -0
  35. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/commands/updates.py +0 -0
  36. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/commands/validate.py +0 -0
  37. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/config.py +0 -0
  38. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/main.py +0 -0
  39. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/target.py +0 -0
  40. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/tls.py +0 -0
  41. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/updates.py +0 -0
  42. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/lightnow_cli/validation.py +0 -0
  43. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/release-please-config.json +0 -0
  44. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/__init__.py +0 -0
  45. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_auth_flow.py +0 -0
  46. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_authenticated_http.py +0 -0
  47. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_client.py +0 -0
  48. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_config.py +0 -0
  49. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_connection_target.py +0 -0
  50. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_context.py +0 -0
  51. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_integrations.py +0 -0
  52. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_tls.py +0 -0
  53. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_updates.py +0 -0
  54. {lightnow_cli-1.6.0 → lightnow_cli-1.6.1}/tests/test_validation.py +0 -0
@@ -73,8 +73,27 @@ jobs:
73
73
  - name: Build and check distributions
74
74
  run: make package
75
75
 
76
+ - name: Verify distribution provenance contract
77
+ run: python -m unittest discover -s ci -p 'test_*.py'
78
+
79
+ - name: Bind PR distribution provenance
80
+ if: github.event_name == 'pull_request'
81
+ env:
82
+ ARTIFACT_REPOSITORY: ${{ github.repository }}
83
+ ARTIFACT_PR_NUMBER: ${{ github.event.pull_request.number }}
84
+ ARTIFACT_HEAD_REVISION: ${{ github.event.pull_request.head.sha }}
85
+ ARTIFACT_RUN_ID: ${{ github.run_id }}
86
+ ARTIFACT_RUN_ATTEMPT: ${{ github.run_attempt }}
87
+ run: |
88
+ python ci/create_client_artifact.py --component cli \
89
+ --repository "$ARTIFACT_REPOSITORY" \
90
+ --pr-number "$ARTIFACT_PR_NUMBER" \
91
+ --head-revision "$ARTIFACT_HEAD_REVISION" \
92
+ --run-id "$ARTIFACT_RUN_ID" \
93
+ --run-attempt "$ARTIFACT_RUN_ATTEMPT"
94
+
76
95
  - name: Upload distributions
77
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
96
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
78
97
  with:
79
98
  name: python-distributions
80
99
  path: dist/*
@@ -51,7 +51,7 @@ jobs:
51
51
  run: make package
52
52
 
53
53
  - name: Upload distributions
54
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
54
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
55
55
  with:
56
56
  name: python-distributions
57
57
  path: dist/*
@@ -66,7 +66,7 @@ jobs:
66
66
 
67
67
  steps:
68
68
  - name: Download distributions
69
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
69
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
70
70
  with:
71
71
  name: python-distributions
72
72
  path: dist
@@ -89,7 +89,7 @@ jobs:
89
89
 
90
90
  steps:
91
91
  - name: Download distributions
92
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
92
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
93
93
  with:
94
94
  name: python-distributions
95
95
  path: dist
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.6.1"
3
+ }
@@ -4,6 +4,15 @@ All notable changes to the LightNow CLI are documented here.
4
4
 
5
5
  This project follows semantic versioning.
6
6
 
7
+ ## [1.6.1](https://github.com/lightnow-ai/lightnow-cli/compare/v1.6.0...v1.6.1) (2026-11-09)
8
+
9
+
10
+ ### Bug Fixes
11
+
12
+ * **cli:** use active context for default favorites ([5399173](https://github.com/lightnow-ai/lightnow-cli/commit/539917321a8fbafedb05394841b5fadd0b5319e0))
13
+ * **cli:** use active context for default favorites ([1ccba54](https://github.com/lightnow-ai/lightnow-cli/commit/1ccba543e34b236d449bdedeb644029c54f49f03))
14
+ * **cli:** use active context for default favorites ([#45](https://github.com/lightnow-ai/lightnow-cli/issues/45)) ([5399173](https://github.com/lightnow-ai/lightnow-cli/commit/539917321a8fbafedb05394841b5fadd0b5319e0))
15
+
7
16
  ## [1.6.0](https://github.com/lightnow-ai/lightnow-cli/compare/v1.5.0...v1.6.0) (2026-10-09)
8
17
 
9
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lightnow-cli
3
- Version: 1.6.0
3
+ Version: 1.6.1
4
4
  Summary: Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows
5
5
  Project-URL: Homepage, https://github.com/lightnow-ai/lightnow-cli
6
6
  Project-URL: Repository, https://github.com/lightnow-ai/lightnow-cli
@@ -176,7 +176,7 @@ Show details for one server:
176
176
  lightnow info io.github.SonarSource/sonarqube-mcp-server
177
177
  ```
178
178
 
179
- Show your favorites:
179
+ Show favorites for the active personal or organization context:
180
180
 
181
181
  ```bash
182
182
  lightnow favorites
@@ -128,7 +128,7 @@ Show details for one server:
128
128
  lightnow info io.github.SonarSource/sonarqube-mcp-server
129
129
  ```
130
130
 
131
- Show your favorites:
131
+ Show favorites for the active personal or organization context:
132
132
 
133
133
  ```bash
134
134
  lightnow favorites
@@ -0,0 +1,126 @@
1
+ """Bind a PR build to its tested checkout and one universal Python wheel."""
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import re
7
+ import subprocess
8
+ import tomllib
9
+ import zipfile
10
+ from email.parser import Parser
11
+ from pathlib import Path
12
+
13
+ SCHEMA_VERSION = "platform.lightnow.ai/feature-stack-client-artifact/v1"
14
+ MANIFEST_FILENAME = "feature-stack-client-artifact.json"
15
+ COMPONENTS = {"cli": "lightnow-cli", "proxy": "lightnow-proxy"}
16
+
17
+
18
+ def build_manifest(
19
+ root: Path,
20
+ component: str,
21
+ repository: str,
22
+ pr_number: int,
23
+ head_revision: str,
24
+ checkout_revision: str,
25
+ run_id: int,
26
+ run_attempt: int,
27
+ ) -> dict:
28
+ """Validate wheel contents and runner identity before creating provenance."""
29
+ package_name = COMPONENTS.get(component)
30
+ if package_name is None or repository != f"lightnow-ai/{package_name}":
31
+ raise ValueError("Component and repository do not match")
32
+ if any(value <= 0 for value in (pr_number, run_id, run_attempt)):
33
+ raise ValueError("PR number, run ID and run attempt must be positive")
34
+ if any(
35
+ re.fullmatch(r"[0-9a-f]{40}", value) is None
36
+ for value in (head_revision, checkout_revision)
37
+ ):
38
+ raise ValueError("Head and checkout revisions must be full Git SHA-1 values")
39
+
40
+ with (root / "pyproject.toml").open("rb") as project_file:
41
+ project = tomllib.load(project_file)["project"]
42
+ if project["name"] != package_name:
43
+ raise ValueError("Repository package metadata does not match component")
44
+
45
+ wheels = sorted((root / "dist").glob("*.whl"))
46
+ if len(wheels) != 1 or wheels[0].is_symlink() or not wheels[0].is_file():
47
+ raise ValueError("Exactly one regular wheel is required")
48
+ wheel = wheels[0]
49
+ wheel_package = package_name.replace("-", "_")
50
+ expected_filename = f"{wheel_package}-{project['version']}-py3-none-any.whl"
51
+ if wheel.name != expected_filename:
52
+ raise ValueError("Expected the repository's universal py3-none-any wheel")
53
+
54
+ prefix = f"{wheel_package}-{project['version']}.dist-info/"
55
+ with zipfile.ZipFile(wheel) as archive:
56
+ for name in (f"{prefix}METADATA", f"{prefix}WHEEL"):
57
+ if archive.namelist().count(name) != 1:
58
+ raise ValueError("Wheel metadata must be unique and match its filename")
59
+ metadata_files = [
60
+ name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
61
+ ]
62
+ if metadata_files != [f"{prefix}METADATA"]:
63
+ raise ValueError("Wheel must contain exactly one distribution's metadata")
64
+ metadata = Parser().parsestr(archive.read(f"{prefix}METADATA").decode("utf-8"))
65
+ wheel_metadata = Parser().parsestr(
66
+ archive.read(f"{prefix}WHEEL").decode("utf-8")
67
+ )
68
+ if metadata.get_all("Name") != [package_name] or metadata.get_all("Version") != [
69
+ project["version"]
70
+ ]:
71
+ raise ValueError("Wheel METADATA name/version do not match repository metadata")
72
+ if wheel_metadata.get_all("Tag") != ["py3-none-any"] or wheel_metadata.get_all(
73
+ "Root-Is-Purelib"
74
+ ) != ["true"]:
75
+ raise ValueError("Wheel must declare the universal pure-Python tag")
76
+
77
+ return {
78
+ "schemaVersion": SCHEMA_VERSION,
79
+ "component": component,
80
+ "repository": repository,
81
+ "packageName": metadata["Name"],
82
+ "packageVersion": metadata["Version"],
83
+ "filename": wheel.name,
84
+ "sha256": hashlib.sha256(wheel.read_bytes()).hexdigest(),
85
+ "source": {
86
+ "kind": "pull-request",
87
+ "number": pr_number,
88
+ "headRevision": head_revision,
89
+ "checkoutRevision": checkout_revision,
90
+ "runId": run_id,
91
+ "runAttempt": run_attempt,
92
+ "workflowPath": ".github/workflows/ci.yml",
93
+ },
94
+ }
95
+
96
+
97
+ def main() -> None:
98
+ parser = argparse.ArgumentParser(description=__doc__)
99
+ parser.add_argument("--component", choices=tuple(COMPONENTS), required=True)
100
+ parser.add_argument("--repository", required=True)
101
+ parser.add_argument("--pr-number", type=int, required=True)
102
+ parser.add_argument("--head-revision", required=True)
103
+ parser.add_argument("--run-id", type=int, required=True)
104
+ parser.add_argument("--run-attempt", type=int, required=True)
105
+ args = parser.parse_args()
106
+ root = Path(__file__).resolve().parents[1]
107
+ checkout_revision = subprocess.check_output(
108
+ ["git", "rev-parse", "HEAD"], cwd=root, text=True
109
+ ).strip()
110
+ manifest = build_manifest(
111
+ root,
112
+ args.component,
113
+ args.repository,
114
+ args.pr_number,
115
+ args.head_revision,
116
+ checkout_revision,
117
+ args.run_id,
118
+ args.run_attempt,
119
+ )
120
+ (root / "dist" / MANIFEST_FILENAME).write_text(
121
+ json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
122
+ )
123
+
124
+
125
+ if __name__ == "__main__":
126
+ main()
@@ -0,0 +1,124 @@
1
+ """Offline provenance contract checks; no installation or external services."""
2
+
3
+ import hashlib
4
+ import tempfile
5
+ import unittest
6
+ import zipfile
7
+ from pathlib import Path
8
+
9
+ from create_client_artifact import build_manifest
10
+
11
+
12
+ class ClientArtifactTests(unittest.TestCase):
13
+ def setUp(self) -> None:
14
+ self.directory = tempfile.TemporaryDirectory()
15
+ self.addCleanup(self.directory.cleanup)
16
+ self.root = Path(self.directory.name)
17
+ (self.root / "dist").mkdir()
18
+ (self.root / "pyproject.toml").write_text(
19
+ '[project]\nname = "lightnow-cli"\nversion = "1.2.3"\n'
20
+ )
21
+ self.wheel = self.root / "dist/lightnow_cli-1.2.3-py3-none-any.whl"
22
+ self.write_wheel()
23
+
24
+ def write_wheel(
25
+ self,
26
+ name: str = "lightnow-cli",
27
+ version: str = "1.2.3",
28
+ tag: str = "py3-none-any",
29
+ ) -> None:
30
+ with zipfile.ZipFile(self.wheel, "w") as archive:
31
+ archive.writestr(
32
+ "lightnow_cli-1.2.3.dist-info/METADATA",
33
+ f"Metadata-Version: 2.3\nName: {name}\nVersion: {version}\n",
34
+ )
35
+ archive.writestr(
36
+ "lightnow_cli-1.2.3.dist-info/WHEEL",
37
+ f"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: {tag}\n",
38
+ )
39
+
40
+ def manifest(self, **overrides: object) -> dict:
41
+ arguments = dict(
42
+ root=self.root,
43
+ component="cli",
44
+ repository="lightnow-ai/lightnow-cli",
45
+ pr_number=42,
46
+ head_revision="a" * 40,
47
+ checkout_revision="b" * 40,
48
+ run_id=123,
49
+ run_attempt=2,
50
+ )
51
+ arguments.update(overrides)
52
+ return build_manifest(**arguments)
53
+
54
+ def test_exact_contract_keeps_head_and_tested_checkout_separate(self) -> None:
55
+ self.assertEqual(
56
+ self.manifest(),
57
+ {
58
+ "schemaVersion": "platform.lightnow.ai/feature-stack-client-artifact/v1",
59
+ "component": "cli",
60
+ "repository": "lightnow-ai/lightnow-cli",
61
+ "packageName": "lightnow-cli",
62
+ "packageVersion": "1.2.3",
63
+ "filename": self.wheel.name,
64
+ "sha256": hashlib.sha256(self.wheel.read_bytes()).hexdigest(),
65
+ "source": {
66
+ "kind": "pull-request",
67
+ "number": 42,
68
+ "headRevision": "a" * 40,
69
+ "checkoutRevision": "b" * 40,
70
+ "runId": 123,
71
+ "runAttempt": 2,
72
+ "workflowPath": ".github/workflows/ci.yml",
73
+ },
74
+ },
75
+ )
76
+
77
+ def test_rejects_unbound_identity(self) -> None:
78
+ for arguments in (
79
+ {"repository": "other/lightnow-cli"},
80
+ {"component": "proxy"},
81
+ {"head_revision": "a" * 7},
82
+ {"checkout_revision": "not-a-revision"},
83
+ {"pr_number": 0},
84
+ {"run_id": -1},
85
+ {"run_attempt": 0},
86
+ ):
87
+ with self.subTest(arguments=arguments), self.assertRaises(ValueError):
88
+ self.manifest(**arguments)
89
+
90
+ def test_rejects_metadata_or_platform_mismatch(self) -> None:
91
+ for arguments in (
92
+ {"name": "another-package"},
93
+ {"version": "9.0.0"},
94
+ {"tag": "cp312-cp312-linux_x86_64"},
95
+ ):
96
+ with self.subTest(arguments=arguments):
97
+ self.write_wheel(**arguments)
98
+ with self.assertRaises(ValueError):
99
+ self.manifest()
100
+
101
+ def test_rejects_ambiguous_missing_or_symlinked_wheel(self) -> None:
102
+ extra = self.wheel.with_name("another.whl")
103
+ extra.write_bytes(self.wheel.read_bytes())
104
+ with self.assertRaises(ValueError):
105
+ self.manifest()
106
+ extra.unlink()
107
+ self.wheel.unlink()
108
+ with self.assertRaises(ValueError):
109
+ self.manifest()
110
+ self.wheel.symlink_to(self.root / "outside.whl")
111
+ with self.assertRaises(ValueError):
112
+ self.manifest()
113
+
114
+ def test_rejects_duplicate_distribution_metadata(self) -> None:
115
+ with zipfile.ZipFile(self.wheel, "a") as archive:
116
+ archive.writestr(
117
+ "other-1.0.dist-info/METADATA", "Name: other\nVersion: 1.0\n"
118
+ )
119
+ with self.assertRaises(ValueError):
120
+ self.manifest()
121
+
122
+
123
+ if __name__ == "__main__":
124
+ unittest.main()
@@ -101,3 +101,26 @@ tag. If the project does not exist yet, use PyPI's pending publisher flow for
101
101
  ```bash
102
102
  python -m twine check dist/*
103
103
  ```
104
+
105
+ ## PR distribution provenance
106
+
107
+ PR CI archives `feature-stack-client-artifact.json` alongside the wheel and
108
+ source distribution in `python-distributions`. The manifest uses
109
+ `platform.lightnow.ai/feature-stack-client-artifact/v1` and binds the universal
110
+ wheel SHA-256 and its actual METADATA name/version to the PR number, head
111
+ revision, tested checkout revision, workflow, run ID, and run attempt. The head
112
+ and tested merge checkout are separate fields. Consumers must verify these
113
+ bindings and the wheel checksum against the selected successful CI run before
114
+ installing a candidate; the manifest itself is build metadata, not a signature
115
+ or a runtime test result.
116
+
117
+ The generator uses only Python's standard library. After a clean package build,
118
+ run its focused offline contract checks with:
119
+
120
+ ```bash
121
+ python -m unittest discover -s ci -p 'test_*.py'
122
+ ```
123
+
124
+ Main CI does not fabricate PR metadata. Tagged release publishing is unchanged:
125
+ release consumers qualify the exact Git tag and published PyPI wheel separately.
126
+ The PR-only JSON never enters the existing PyPI release upload.
@@ -1,3 +1,3 @@
1
1
  """LightNow CLI - A command-line tool for managing MCP servers."""
2
2
 
3
- __version__ = "1.6.0" # x-release-please-version
3
+ __version__ = "1.6.1" # x-release-please-version
@@ -74,7 +74,7 @@ def favorite_servers(
74
74
  "--scope",
75
75
  help="Favorite scope: effective, user, tenant, system, or true.",
76
76
  ),
77
- ] = "user",
77
+ ] = "effective",
78
78
  sort: Annotated[
79
79
  Optional[str],
80
80
  typer.Option("--sort", help="Sort order supported by the Registry API."),
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lightnow-cli"
7
- version = "1.6.0"
7
+ version = "1.6.1"
8
8
  description = "Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows"
9
9
  authors = [{name = "LightNow Team"}]
10
10
  license = {text = "Apache-2.0"}
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
3
  "extends": [
4
- "github>apitree/renovate:python#v1.0.2",
5
- "github>apitree/renovate:docker#v1.0.2",
4
+ "github>apitree/renovate:python",
5
+ "github>apitree/renovate:docker",
6
6
  "helpers:pinGitHubActionDigests",
7
7
  "default:automergeDigest",
8
8
  ":automergeStableNonMajor",
@@ -17,6 +17,7 @@ from lightnow_cli.config import (
17
17
  LOCAL_ADMIN_API_URL,
18
18
  LOCAL_ISSUER,
19
19
  LOCAL_REGISTRY_API_URL,
20
+ ConfigManager,
20
21
  )
21
22
  from lightnow_cli.main import app
22
23
  from lightnow_cli.target import ConnectionTarget
@@ -335,6 +336,47 @@ def test_favorites_command_passes_tenant_context(runner):
335
336
  assert fake_client.list_servers.call_args.kwargs["tenant"] == "acme"
336
337
 
337
338
 
339
+ @pytest.mark.parametrize("tenant_id", [None, "active-organization"])
340
+ def test_favorites_default_uses_selected_context(runner, tmp_path, tenant_id):
341
+ """The default list must follow actual persisted personal/organization context."""
342
+ manager = ConfigManager()
343
+ manager.config_dir = tmp_path / ".lightnow"
344
+ manager.config_file = manager.config_dir / "config.json"
345
+ if tenant_id is None:
346
+ manager.set_personal_context()
347
+ else:
348
+ manager.set_tenant_context(tenant_id, "Selected organization")
349
+ fake_client = MagicMock()
350
+ fake_client.list_servers = AsyncMock(return_value={"servers": []})
351
+ with (
352
+ patch("lightnow_cli.commands.query.config_manager", manager),
353
+ patch("lightnow_cli.commands.query.get_client", return_value=fake_client),
354
+ ):
355
+ result = runner.invoke(app, ["favorites"])
356
+ assert result.exit_code == 0
357
+ assert fake_client.list_servers.call_args.kwargs["favorites"] == "effective"
358
+ assert fake_client.list_servers.call_args.kwargs["tenant"] == tenant_id
359
+
360
+
361
+ @pytest.mark.parametrize("scope", ["user", "tenant", "system", "effective", "true"])
362
+ def test_favorites_explicit_scope_is_not_silently_replaced(runner, tmp_path, scope):
363
+ """Explicit scope flags do not silently change the active organization."""
364
+ manager = ConfigManager()
365
+ manager.config_dir = tmp_path / ".lightnow"
366
+ manager.config_file = manager.config_dir / "config.json"
367
+ manager.set_tenant_context("active-organization", "Selected organization")
368
+ fake_client = MagicMock()
369
+ fake_client.list_servers = AsyncMock(return_value={"servers": []})
370
+ with (
371
+ patch("lightnow_cli.commands.query.config_manager", manager),
372
+ patch("lightnow_cli.commands.query.get_client", return_value=fake_client),
373
+ ):
374
+ result = runner.invoke(app, ["favorites", "--scope", scope])
375
+ assert result.exit_code == 0
376
+ assert fake_client.list_servers.call_args.kwargs["favorites"] == scope
377
+ assert fake_client.list_servers.call_args.kwargs["tenant"] == "active-organization"
378
+
379
+
338
380
  def test_info_command_passes_tenant_context(runner):
339
381
  """Server info supports organization context from the public CLI surface."""
340
382
  fake_client = MagicMock()
@@ -847,12 +889,12 @@ def test_favorites_command_empty(mock_list_servers, runner):
847
889
 
848
890
  assert result.exit_code == 0
849
891
  assert "No favorite servers found" in result.stdout
850
- assert mock_list_servers.call_args.kwargs["favorites"] == "user"
892
+ assert mock_list_servers.call_args.kwargs["favorites"] == "effective"
851
893
 
852
894
 
853
895
  @patch("lightnow_cli.client.MCPRegistryClient.list_servers")
854
896
  def test_favorites_command_allows_effective_scope(mock_list_servers, runner):
855
- """Effective favorites remain available as an explicit combined view."""
897
+ """Explicit effective scope remains bound to the selected context."""
856
898
  mock_list_servers.return_value = {"servers": [], "metadata": {"count": 0}}
857
899
 
858
900
  result = runner.invoke(app, ["favorites", "--scope", "effective"])
@@ -1,3 +0,0 @@
1
- {
2
- ".": "1.6.0"
3
- }
File without changes
File without changes
File without changes
File without changes