specfuse 0.2.4__tar.gz → 0.2.6__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.
- {specfuse-0.2.4/specfuse.egg-info → specfuse-0.2.6}/PKG-INFO +2 -2
- {specfuse-0.2.4 → specfuse-0.2.6}/pyproject.toml +3 -3
- {specfuse-0.2.4 → specfuse-0.2.6}/specfuse/cli.py +59 -6
- {specfuse-0.2.4 → specfuse-0.2.6/specfuse.egg-info}/PKG-INFO +2 -2
- {specfuse-0.2.4 → specfuse-0.2.6}/specfuse.egg-info/requires.txt +1 -1
- {specfuse-0.2.4 → specfuse-0.2.6}/tests/test_cli.py +58 -0
- {specfuse-0.2.4 → specfuse-0.2.6}/LICENSE +0 -0
- {specfuse-0.2.4 → specfuse-0.2.6}/NOTICE +0 -0
- {specfuse-0.2.4 → specfuse-0.2.6}/README.md +0 -0
- {specfuse-0.2.4 → specfuse-0.2.6}/setup.cfg +0 -0
- {specfuse-0.2.4 → specfuse-0.2.6}/specfuse.egg-info/SOURCES.txt +0 -0
- {specfuse-0.2.4 → specfuse-0.2.6}/specfuse.egg-info/dependency_links.txt +0 -0
- {specfuse-0.2.4 → specfuse-0.2.6}/specfuse.egg-info/entry_points.txt +0 -0
- {specfuse-0.2.4 → specfuse-0.2.6}/specfuse.egg-info/top_level.txt +0 -0
- {specfuse-0.2.4 → specfuse-0.2.6}/tests/test_entry_points.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specfuse
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Specfuse umbrella CLI — bridges the pip-installed driver and the Claude Code plugin (init / upgrade).
|
|
5
5
|
Author: Specfuse contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -17,7 +17,7 @@ Requires-Python: >=3.10
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
License-File: NOTICE
|
|
20
|
-
Requires-Dist: specfuse-loop>=0.3.
|
|
20
|
+
Requires-Dist: specfuse-loop>=0.3.3
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: coverage>=7.0; extra == "dev"
|
|
23
23
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specfuse"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.6"
|
|
8
8
|
description = "Specfuse umbrella CLI — bridges the pip-installed driver and the Claude Code plugin (init / upgrade)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -25,7 +25,7 @@ classifiers = [
|
|
|
25
25
|
# `specfuse.loop.scaffold`, which only exists in specfuse-loop 0.3.0+ (added by
|
|
26
26
|
# FEAT-2026-0026). A 0.2.x driver would import-fail at `specfuse init`.
|
|
27
27
|
dependencies = [
|
|
28
|
-
"specfuse-loop>=0.3.
|
|
28
|
+
"specfuse-loop>=0.3.3",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[project.optional-dependencies]
|
|
@@ -38,7 +38,7 @@ specfuse = "specfuse.cli:main"
|
|
|
38
38
|
# not its dependencies' — without these, `specfuse-loop`/`specfuse-lint` (from
|
|
39
39
|
# the specfuse-loop dep) would be hidden and users would have to install the
|
|
40
40
|
# driver separately or pass --include-deps. The targets resolve against the
|
|
41
|
-
# specfuse.loop modules the pinned specfuse-loop>=0.3.
|
|
41
|
+
# specfuse.loop modules the pinned specfuse-loop>=0.3.3 dependency provides.
|
|
42
42
|
specfuse-loop = "specfuse.loop.loop:main"
|
|
43
43
|
specfuse-lint = "specfuse.loop.lint_plan:main"
|
|
44
44
|
|
|
@@ -23,6 +23,7 @@ thin user-facing bridge over it.
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import argparse
|
|
26
|
+
import importlib.util
|
|
26
27
|
import shutil
|
|
27
28
|
import subprocess
|
|
28
29
|
import sys
|
|
@@ -31,7 +32,7 @@ from pathlib import Path
|
|
|
31
32
|
|
|
32
33
|
from specfuse.loop import scaffold
|
|
33
34
|
|
|
34
|
-
__version__ = "0.2.
|
|
35
|
+
__version__ = "0.2.6"
|
|
35
36
|
|
|
36
37
|
MARKETPLACE = "specfuse/specfuse"
|
|
37
38
|
PLUGIN = "specfuse@specfuse"
|
|
@@ -54,6 +55,50 @@ def _pip_install(packages: list[str], *, upgrade: bool, runner=None) -> int:
|
|
|
54
55
|
return getattr(proc, "returncode", 0)
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
def _scaffold_is_current(target: Path) -> tuple[bool, str]:
|
|
59
|
+
"""Return (already_current, installed_seed_version).
|
|
60
|
+
|
|
61
|
+
already_current is True when the target's .specfuse/VERSION equals the
|
|
62
|
+
installed seed version — i.e. there is nothing newer to overlay.
|
|
63
|
+
"""
|
|
64
|
+
installed = scaffold.scaffold_version()
|
|
65
|
+
vpath = target / ".specfuse" / "VERSION"
|
|
66
|
+
if not vpath.exists():
|
|
67
|
+
return (False, installed)
|
|
68
|
+
current = vpath.read_text(encoding="utf-8").strip()
|
|
69
|
+
try:
|
|
70
|
+
same = scaffold._parse_version(current) == scaffold._parse_version(installed)
|
|
71
|
+
except ValueError:
|
|
72
|
+
return (False, installed)
|
|
73
|
+
return (same, installed)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _pip_upgrade_or_advise(runner=None) -> int:
|
|
77
|
+
"""Pip-upgrade the driver + CLI, unless this is a pipx-managed or pip-less
|
|
78
|
+
install — pipx owns its venv (and may ship no pip), so `python -m pip` either
|
|
79
|
+
fails ('No module named pip') or fights pipx. In that case skip and advise
|
|
80
|
+
the right command instead of erroring. Returns a process-style rc (0 = ok or
|
|
81
|
+
cleanly skipped)."""
|
|
82
|
+
pipx_managed = "/pipx/venvs/" in Path(sys.executable).as_posix()
|
|
83
|
+
pip_missing = importlib.util.find_spec("pip") is None
|
|
84
|
+
if pipx_managed or pip_missing:
|
|
85
|
+
why = "pipx-managed install" if pipx_managed else "no pip in this environment"
|
|
86
|
+
print(
|
|
87
|
+
f"specfuse: skipping automatic package upgrade ({why}). Update the "
|
|
88
|
+
f"driver + CLI with:\n"
|
|
89
|
+
f" pipx upgrade specfuse # if installed via pipx\n"
|
|
90
|
+
f" python3 -m pip install -U specfuse # if installed in a venv",
|
|
91
|
+
file=sys.stderr,
|
|
92
|
+
)
|
|
93
|
+
return 0
|
|
94
|
+
rc = _pip_install(["specfuse-loop", "specfuse"], upgrade=True, runner=runner)
|
|
95
|
+
if rc != 0:
|
|
96
|
+
print(f"specfuse: pip upgrade failed (exit {rc}).", file=sys.stderr)
|
|
97
|
+
else:
|
|
98
|
+
print("specfuse: pip packages upgraded (specfuse-loop, specfuse).")
|
|
99
|
+
return rc
|
|
100
|
+
|
|
101
|
+
|
|
57
102
|
def cmd_init(args: argparse.Namespace, *, runner=None) -> int:
|
|
58
103
|
"""Scaffold a repo's .specfuse/ + .claude wiring from the package (no pip,
|
|
59
104
|
no curl-bash). Refuses if .specfuse/ already exists (points at `upgrade`)."""
|
|
@@ -104,8 +149,13 @@ def cmd_upgrade(args: argparse.Namespace, *, runner=None) -> int:
|
|
|
104
149
|
print(f"specfuse: target '{target}' is not a directory.", file=sys.stderr)
|
|
105
150
|
return 2
|
|
106
151
|
ci_check = getattr(args, "ci_check", None)
|
|
152
|
+
current, installed = _scaffold_is_current(target)
|
|
107
153
|
|
|
108
154
|
if getattr(args, "dry_run", False):
|
|
155
|
+
if current:
|
|
156
|
+
print(f"specfuse: [dry-run] .specfuse/ is already at the latest "
|
|
157
|
+
f"scaffold version ({installed}); nothing to overlay.")
|
|
158
|
+
return 0
|
|
109
159
|
# Preview the overlay against a faithful copy of the target's .specfuse/;
|
|
110
160
|
# the target is never touched and no pip runs.
|
|
111
161
|
with tempfile.TemporaryDirectory() as tmp:
|
|
@@ -137,13 +187,16 @@ def cmd_upgrade(args: argparse.Namespace, *, runner=None) -> int:
|
|
|
137
187
|
except scaffold.ScaffoldDowngradeError as exc:
|
|
138
188
|
print(f"specfuse: {exc}", file=sys.stderr)
|
|
139
189
|
return 1
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
190
|
+
if current:
|
|
191
|
+
print(f"specfuse: .specfuse/ already at the latest scaffold version "
|
|
192
|
+
f"({installed}); .claude wiring refreshed.")
|
|
193
|
+
else:
|
|
194
|
+
print(f"specfuse: overlaid {len(written)} versioned file(s) onto "
|
|
195
|
+
f"{target}/.specfuse/.")
|
|
196
|
+
|
|
197
|
+
rc = _pip_upgrade_or_advise(runner)
|
|
143
198
|
if rc != 0:
|
|
144
|
-
print(f"specfuse: pip upgrade failed (exit {rc}).", file=sys.stderr)
|
|
145
199
|
return rc
|
|
146
|
-
print("specfuse: pip packages upgraded (specfuse-loop, specfuse).")
|
|
147
200
|
print(PLUGIN_UPDATE_HINT)
|
|
148
201
|
return 0
|
|
149
202
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specfuse
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Specfuse umbrella CLI — bridges the pip-installed driver and the Claude Code plugin (init / upgrade).
|
|
5
5
|
Author: Specfuse contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -17,7 +17,7 @@ Requires-Python: >=3.10
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
License-File: NOTICE
|
|
20
|
-
Requires-Dist: specfuse-loop>=0.3.
|
|
20
|
+
Requires-Dist: specfuse-loop>=0.3.3
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: coverage>=7.0; extra == "dev"
|
|
23
23
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
@@ -101,10 +101,16 @@ class TestUpgrade(unittest.TestCase):
|
|
|
101
101
|
with redirect_stdout(io.StringIO()):
|
|
102
102
|
cli.cmd_init(_args(target=d))
|
|
103
103
|
|
|
104
|
+
def _make_stale(self, d):
|
|
105
|
+
"""Force the target's scaffold VERSION older than the seed so upgrade
|
|
106
|
+
actually overlays (a freshly-init'd target is already current)."""
|
|
107
|
+
(Path(d) / ".specfuse" / "VERSION").write_text("0.1.0\n", encoding="utf-8")
|
|
108
|
+
|
|
104
109
|
def test_upgrade_overlays_then_pip(self):
|
|
105
110
|
# Red test for T04: overlay precedes pip; both happen.
|
|
106
111
|
with tempfile.TemporaryDirectory() as d:
|
|
107
112
|
self._init(d)
|
|
113
|
+
self._make_stale(d)
|
|
108
114
|
runner = _ok_runner(0)
|
|
109
115
|
out = io.StringIO()
|
|
110
116
|
with redirect_stdout(out):
|
|
@@ -144,6 +150,7 @@ class TestUpgrade(unittest.TestCase):
|
|
|
144
150
|
them (regression: shutil.Error 'No such file or directory')."""
|
|
145
151
|
with tempfile.TemporaryDirectory() as d:
|
|
146
152
|
self._init(d)
|
|
153
|
+
self._make_stale(d) # force an actual overlay so the copytree runs
|
|
147
154
|
skills = Path(d) / ".specfuse" / "skills"
|
|
148
155
|
skills.mkdir(parents=True, exist_ok=True)
|
|
149
156
|
# Point at a target that does not exist → dangling symlink.
|
|
@@ -155,6 +162,57 @@ class TestUpgrade(unittest.TestCase):
|
|
|
155
162
|
self.assertEqual(rc, 0, "dry-run must survive dangling legacy symlinks")
|
|
156
163
|
self.assertIn("dry-run", out.getvalue())
|
|
157
164
|
|
|
165
|
+
def test_upgrade_dry_run_reports_already_latest(self):
|
|
166
|
+
"""A freshly-init'd target is at the seed version; --dry-run must say so,
|
|
167
|
+
not dump the full versioned-file list."""
|
|
168
|
+
with tempfile.TemporaryDirectory() as d:
|
|
169
|
+
self._init(d) # VERSION == installed seed
|
|
170
|
+
out = io.StringIO()
|
|
171
|
+
with redirect_stdout(out):
|
|
172
|
+
rc = cli.cmd_upgrade(_args(target=d, dry_run=True), runner=_ok_runner(0))
|
|
173
|
+
self.assertEqual(rc, 0)
|
|
174
|
+
text = out.getvalue()
|
|
175
|
+
self.assertIn("already at the latest", text)
|
|
176
|
+
self.assertNotIn("would overlay", text)
|
|
177
|
+
self.assertNotIn(" .specfuse/", text) # no file list
|
|
178
|
+
|
|
179
|
+
def test_upgrade_real_reports_already_latest(self):
|
|
180
|
+
"""Real upgrade on a current target reports 'already at latest', not a
|
|
181
|
+
misleading 'overlaid N file(s)'."""
|
|
182
|
+
with tempfile.TemporaryDirectory() as d:
|
|
183
|
+
self._init(d)
|
|
184
|
+
out = io.StringIO()
|
|
185
|
+
with redirect_stdout(out):
|
|
186
|
+
rc = cli.cmd_upgrade(_args(target=d), runner=_ok_runner(0))
|
|
187
|
+
self.assertEqual(rc, 0)
|
|
188
|
+
self.assertIn("already at the latest", out.getvalue())
|
|
189
|
+
|
|
190
|
+
def test_pip_step_skipped_when_pip_unavailable(self):
|
|
191
|
+
"""In a pip-less (e.g. pipx-managed) environment, the upgrade skips the
|
|
192
|
+
auto pip-install with guidance rather than erroring, and does not call
|
|
193
|
+
the runner."""
|
|
194
|
+
import importlib.util as _ilu
|
|
195
|
+
real_find_spec = _ilu.find_spec
|
|
196
|
+
|
|
197
|
+
def _no_pip(name, *a, **k):
|
|
198
|
+
return None if name == "pip" else real_find_spec(name, *a, **k)
|
|
199
|
+
|
|
200
|
+
runner = _ok_runner(0)
|
|
201
|
+
err = io.StringIO()
|
|
202
|
+
with tempfile.TemporaryDirectory() as d:
|
|
203
|
+
self._init(d)
|
|
204
|
+
self._make_stale(d)
|
|
205
|
+
orig = cli.importlib.util.find_spec
|
|
206
|
+
cli.importlib.util.find_spec = _no_pip
|
|
207
|
+
try:
|
|
208
|
+
with redirect_stderr(err):
|
|
209
|
+
rc = cli.cmd_upgrade(_args(target=d), runner=runner)
|
|
210
|
+
finally:
|
|
211
|
+
cli.importlib.util.find_spec = orig
|
|
212
|
+
self.assertEqual(rc, 0, "pip-less env must not fail the upgrade")
|
|
213
|
+
self.assertEqual(runner.calls, [], "must not attempt pip when pip absent")
|
|
214
|
+
self.assertIn("pipx upgrade specfuse", err.getvalue())
|
|
215
|
+
|
|
158
216
|
|
|
159
217
|
class TestParser(unittest.TestCase):
|
|
160
218
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|