fusion-cli 1.2.1__tar.gz → 1.3.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 (77) hide show
  1. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/PKG-INFO +6 -3
  2. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/README.md +5 -2
  3. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/pyproject.toml +1 -1
  4. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/cli.py +29 -0
  5. fusion_cli-1.3.1/src/fusion/update.py +85 -0
  6. fusion_cli-1.3.1/tests/test_cli_update.py +140 -0
  7. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/uv.lock +1 -1
  8. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/.gitignore +0 -0
  9. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/hatch_build.py +0 -0
  10. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-analyst/SKILL.md +0 -0
  11. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-analyst/references/assess.md +0 -0
  12. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-analyst/references/compare.md +0 -0
  13. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-analyst/references/export.md +0 -0
  14. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-analyst/references/fusion-conventions.md +0 -0
  15. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-analyst/references/report.md +0 -0
  16. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-analyst/scripts/export.py +0 -0
  17. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/SKILL.md +0 -0
  18. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/references/convert.md +0 -0
  19. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/references/delivery.md +0 -0
  20. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/references/fusion-conventions.md +0 -0
  21. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/references/gate.md +0 -0
  22. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/scripts/convert.py +0 -0
  23. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/scripts/gate.py +0 -0
  24. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/tests/conftest.py +0 -0
  25. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/tests/make_fixtures.py +0 -0
  26. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/tests/test_convert.py +0 -0
  27. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/tests/test_gate.py +0 -0
  28. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-intake/tests/test_integration.py +0 -0
  29. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/SKILL.md +0 -0
  30. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/references/archive.md +0 -0
  31. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/references/create.md +0 -0
  32. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/references/cross-reference.md +0 -0
  33. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/references/fusion-conventions.md +0 -0
  34. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/references/promote.md +0 -0
  35. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/references/query.md +0 -0
  36. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/references/reflect.md +0 -0
  37. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/references/restructure.md +0 -0
  38. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/references/tag.md +0 -0
  39. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/scripts/link-repair.py +0 -0
  40. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/tests/conftest.py +0 -0
  41. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-librarian/tests/test_link_repair.py +0 -0
  42. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-planner/SKILL.md +0 -0
  43. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-planner/references/close.md +0 -0
  44. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-planner/references/create-activity.md +0 -0
  45. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-planner/references/fusion-conventions.md +0 -0
  46. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/skills/fusion-planner/references/horizon.md +0 -0
  47. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/__init__.py +0 -0
  48. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/bucket.py +0 -0
  49. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/checker.py +0 -0
  50. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/document.py +0 -0
  51. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/hub.py +0 -0
  52. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/indexer.py +0 -0
  53. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/ledger.py +0 -0
  54. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/manifest.py +0 -0
  55. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/scaffold.py +0 -0
  56. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/setup.py +0 -0
  57. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/src/fusion/views.py +0 -0
  58. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/__init__.py +0 -0
  59. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/conftest.py +0 -0
  60. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_build_payload.py +0 -0
  61. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_checker_errors.py +0 -0
  62. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_checker_warnings.py +0 -0
  63. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_cli.py +0 -0
  64. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_cli_encoding.py +0 -0
  65. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_cli_setup.py +0 -0
  66. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_cli_version.py +0 -0
  67. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_document.py +0 -0
  68. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_hub_bucket.py +0 -0
  69. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_indexer.py +0 -0
  70. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_ledger.py +0 -0
  71. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_line_endings.py +0 -0
  72. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_roundtrip.py +0 -0
  73. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_scaffold.py +0 -0
  74. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_setup_agents.py +0 -0
  75. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_setup_core.py +0 -0
  76. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_skill_family.py +0 -0
  77. {fusion_cli-1.2.1 → fusion_cli-1.3.1}/tests/test_views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusion-cli
3
- Version: 1.2.1
3
+ Version: 1.3.1
4
4
  Summary: The Fusion reference CLI — the notary of the Fusion Convention.
5
5
  Author: Bluewaves Boutique
6
6
  License-Expression: MIT
@@ -38,7 +38,7 @@ uv tool install ./fusion/cli
38
38
  fusion --version
39
39
  ```
40
40
 
41
- ## The nine commands (there is no tenth)
41
+ ## The ten commands (there is no eleventh)
42
42
 
43
43
  | Command | Does |
44
44
  |---|---|
@@ -51,8 +51,11 @@ fusion --version
51
51
  | `fusion today` | The composed day, across every bucket in the hub |
52
52
  | `fusion agenda` | The wider horizon — dated and active, across the hub |
53
53
  | `fusion setup` | Install the skills to `~/.agents/skills` and serve every detected agent (links only where needed) — the installer's brain. `--remove` undoes it |
54
+ | `fusion update` | Bring the whole system current: upgrade fusion-cli through uv, then re-run setup from the new binary so the skills refresh everywhere. Equivalent to `uv tool upgrade fusion-cli && fusion setup` |
54
55
 
55
- Every command takes `--json` (agents parse, never scrape). `log`, `new`,
56
+ Every command takes `--json` (agents parse, never scrape) except
57
+ `update`, which composes two processes (uv, then setup) that own
58
+ stdout in turn. `log`, `new`,
56
59
  and `index` take `--as <actor>`; the pen defaults to `FUSION_ACTOR`, then
57
60
  the OS username. `status` and `log` take `--since <date|last-reflection>`.
58
61
  `log` also takes `--bucket` (resolves the ledger; default: walk up from the current directory).
@@ -28,7 +28,7 @@ uv tool install ./fusion/cli
28
28
  fusion --version
29
29
  ```
30
30
 
31
- ## The nine commands (there is no tenth)
31
+ ## The ten commands (there is no eleventh)
32
32
 
33
33
  | Command | Does |
34
34
  |---|---|
@@ -41,8 +41,11 @@ fusion --version
41
41
  | `fusion today` | The composed day, across every bucket in the hub |
42
42
  | `fusion agenda` | The wider horizon — dated and active, across the hub |
43
43
  | `fusion setup` | Install the skills to `~/.agents/skills` and serve every detected agent (links only where needed) — the installer's brain. `--remove` undoes it |
44
+ | `fusion update` | Bring the whole system current: upgrade fusion-cli through uv, then re-run setup from the new binary so the skills refresh everywhere. Equivalent to `uv tool upgrade fusion-cli && fusion setup` |
44
45
 
45
- Every command takes `--json` (agents parse, never scrape). `log`, `new`,
46
+ Every command takes `--json` (agents parse, never scrape) except
47
+ `update`, which composes two processes (uv, then setup) that own
48
+ stdout in turn. `log`, `new`,
46
49
  and `index` take `--as <actor>`; the pen defaults to `FUSION_ACTOR`, then
47
50
  the OS username. `status` and `log` take `--since <date|last-reflection>`.
48
51
  `log` also takes `--bucket` (resolves the ledger; default: walk up from the current directory).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fusion-cli"
3
- version = "1.2.1"
3
+ version = "1.3.1"
4
4
  description = "The Fusion reference CLI — the notary of the Fusion Convention."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -319,6 +319,21 @@ def _render_setup(report: dict) -> list[str]:
319
319
  return lines
320
320
 
321
321
 
322
+ def cmd_update(args) -> int:
323
+ from fusion import update as update_mod
324
+ setup_args = []
325
+ if args.force:
326
+ setup_args.append("--force")
327
+ if args.skills_dir:
328
+ setup_args += ["--skills-dir", args.skills_dir]
329
+ if args.no_agents:
330
+ setup_args.append("--no-agents")
331
+ try:
332
+ return update_mod.run_update(Path.home(), setup_args)
333
+ except update_mod.UpdateError as exc:
334
+ return _fail(str(exc))
335
+
336
+
322
337
  # ── parser ───────────────────────────────────────────────────────────────
323
338
 
324
339
  def build_parser() -> argparse.ArgumentParser:
@@ -418,6 +433,20 @@ def build_parser() -> argparse.ArgumentParser:
418
433
  flag_json(p)
419
434
  p.set_defaults(func=cmd_setup)
420
435
 
436
+ p = sub.add_parser("update", help="upgrade fusion-cli and refresh the skills",
437
+ description="Bring the whole system current: upgrade "
438
+ "fusion-cli through uv, then re-run `fusion setup` "
439
+ "from the new binary so the bundled skills refresh "
440
+ "everywhere setup put them. Equivalent to: "
441
+ "uv tool upgrade fusion-cli && fusion setup.")
442
+ p.add_argument("--force", action="store_true",
443
+ help="forwarded to setup: replace foreign entries")
444
+ p.add_argument("--skills-dir",
445
+ help="forwarded to setup: canonical destination")
446
+ p.add_argument("--no-agents", action="store_true",
447
+ help="forwarded to setup: skip agent fan-out")
448
+ p.set_defaults(func=cmd_update)
449
+
421
450
  return parser
422
451
 
423
452
 
@@ -0,0 +1,85 @@
1
+ """fusion update — one verb to bring the whole system current.
2
+
3
+ Upgrades fusion-cli through uv, then re-runs `fusion setup` from the
4
+ NEW binary: this process keeps the old skills payload in memory, so
5
+ setup must run in a child for the refreshed payload to land. Every
6
+ decision is testable without a real subprocess — the runner is a seam.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from .setup import payload_version
17
+
18
+
19
+ class UpdateError(Exception):
20
+ """An update step that cannot proceed."""
21
+
22
+
23
+ def _runner():
24
+ return subprocess.run
25
+
26
+
27
+ def find_uv(home: Path) -> Path | None:
28
+ found = shutil.which("uv")
29
+ if found:
30
+ return Path(found)
31
+ # install.sh's landing spot, for shells whose PATH never learned it
32
+ for name in ("uv.exe", "uv") if os.name == "nt" else ("uv",):
33
+ candidate = home / ".local" / "bin" / name
34
+ if candidate.is_file() and os.access(candidate, os.X_OK):
35
+ return candidate
36
+ return None
37
+
38
+
39
+ def uv_owns_install(uv: Path, run) -> bool:
40
+ proc = run([str(uv), "tool", "list"], capture_output=True, text=True)
41
+ if proc.returncode != 0:
42
+ return False
43
+ return any(line.split()[:1] == ["fusion-cli"]
44
+ for line in proc.stdout.splitlines())
45
+
46
+
47
+ def fusion_binary(uv: Path, run) -> Path:
48
+ proc = run([str(uv), "tool", "dir", "--bin"],
49
+ capture_output=True, text=True)
50
+ if proc.returncode != 0 or not proc.stdout.strip():
51
+ raise UpdateError(
52
+ "could not resolve uv's tool bin dir — finish by hand: "
53
+ "uv tool dir --bin, then <that dir>/fusion setup")
54
+ bin_dir = Path(proc.stdout.strip())
55
+ for name in ("fusion", "fusion.exe"):
56
+ candidate = bin_dir / name
57
+ if candidate.is_file():
58
+ return candidate
59
+ raise UpdateError(
60
+ f"fusion not found in {bin_dir} after the upgrade — "
61
+ f"check: uv tool list")
62
+
63
+
64
+ def run_update(home: Path, setup_args: list[str]) -> int:
65
+ run = _runner()
66
+ uv = find_uv(home)
67
+ if uv is None:
68
+ raise UpdateError(
69
+ "uv not found — fusion updates through uv "
70
+ "(https://docs.astral.sh/uv/). Install it, or upgrade "
71
+ "fusion-cli with whatever installed it, then run: fusion setup")
72
+ if not uv_owns_install(uv, run):
73
+ raise UpdateError(
74
+ "this fusion-cli is not managed by uv — upgrade it with "
75
+ "whatever installed it (pip, pipx, a checkout), then run: "
76
+ "fusion setup")
77
+ print(f"fusion-cli {payload_version()} — asking uv for newer…")
78
+ sys.stdout.flush() # keep our line ahead of uv's inherited stdio
79
+ if run([str(uv), "tool", "upgrade", "fusion-cli"]).returncode != 0:
80
+ raise UpdateError(
81
+ "uv tool upgrade failed — on Windows the running fusion.exe "
82
+ "can hold the lock. Exit this process, then run: "
83
+ "uv tool upgrade fusion-cli && fusion setup")
84
+ child = run([str(fusion_binary(uv, run)), "setup", *setup_args])
85
+ return child.returncode
@@ -0,0 +1,140 @@
1
+ """fusion update end to end through the CLI surface.
2
+
3
+ Same isolation contract as test_cli_setup.py: no real subprocess is
4
+ ever reached; a fake runner records the exact command sequence.
5
+ """
6
+ import subprocess
7
+ from pathlib import Path
8
+ from types import SimpleNamespace
9
+
10
+ import pytest
11
+
12
+ from fusion import update
13
+ from fusion.cli import build_parser, main
14
+
15
+ # find_uv wraps which()'s answer in Path, so the argv the runner sees is
16
+ # the platform's rendering of this stub ("\\stub\\uv" on Windows).
17
+ STUB_UV = str(Path("/stub/uv"))
18
+
19
+
20
+ class FakeRunner:
21
+ """Answers uv/fusion invocations from a script of canned results."""
22
+
23
+ def __init__(self, bin_dir: Path, *, list_stdout="fusion-cli v1.2.1\n- fusion\n",
24
+ list_rc=0, upgrade_rc=0, setup_rc=0):
25
+ self.bin_dir = bin_dir
26
+ self.list_stdout = list_stdout
27
+ self.list_rc = list_rc
28
+ self.upgrade_rc = upgrade_rc
29
+ self.setup_rc = setup_rc
30
+ self.calls = []
31
+
32
+ def __call__(self, argv, **kwargs):
33
+ self.calls.append([str(a) for a in argv])
34
+ prog, rest = Path(argv[0]).name, [str(a) for a in argv[1:]]
35
+ if prog.startswith("uv") and rest == ["tool", "list"]:
36
+ return SimpleNamespace(returncode=self.list_rc,
37
+ stdout=self.list_stdout, stderr="")
38
+ if prog.startswith("uv") and rest == ["tool", "upgrade", "fusion-cli"]:
39
+ return SimpleNamespace(returncode=self.upgrade_rc, stdout="", stderr="")
40
+ if prog.startswith("uv") and rest == ["tool", "dir", "--bin"]:
41
+ return SimpleNamespace(returncode=0, stdout=f"{self.bin_dir}\n", stderr="")
42
+ if prog.startswith("fusion") and rest[:1] == ["setup"]:
43
+ return SimpleNamespace(returncode=self.setup_rc, stdout="", stderr="")
44
+ raise AssertionError(f"unexpected command: {argv}")
45
+
46
+
47
+ @pytest.fixture
48
+ def sandbox(tmp_path, monkeypatch):
49
+ home = tmp_path / "home"
50
+ home.mkdir()
51
+ monkeypatch.setenv("HOME", str(home))
52
+ monkeypatch.setenv("USERPROFILE", str(home)) # windows
53
+ monkeypatch.setattr(Path, "home", classmethod(lambda cls: home))
54
+ monkeypatch.setattr(update.shutil, "which",
55
+ lambda name, *a, **kw: "/stub/uv" if name == "uv" else None)
56
+ monkeypatch.setattr(subprocess, "run",
57
+ lambda *a, **kw: pytest.fail("real subprocess reached"))
58
+ bin_dir = tmp_path / "toolbin"
59
+ bin_dir.mkdir()
60
+ (bin_dir / "fusion").write_text("")
61
+ return SimpleNamespace(home=home, bin_dir=bin_dir)
62
+
63
+
64
+ def _install_runner(monkeypatch, runner):
65
+ monkeypatch.setattr(update, "_runner", lambda: runner)
66
+
67
+
68
+ def test_update_happy_path_runs_upgrade_then_setup(sandbox, monkeypatch, capsys):
69
+ runner = FakeRunner(sandbox.bin_dir)
70
+ _install_runner(monkeypatch, runner)
71
+ assert main(["update"]) == 0
72
+ assert runner.calls == [
73
+ [STUB_UV, "tool", "list"],
74
+ [STUB_UV, "tool", "upgrade", "fusion-cli"],
75
+ [STUB_UV, "tool", "dir", "--bin"],
76
+ [str(sandbox.bin_dir / "fusion"), "setup"],
77
+ ]
78
+
79
+
80
+ def test_update_forwards_setup_flags(sandbox, monkeypatch):
81
+ runner = FakeRunner(sandbox.bin_dir)
82
+ _install_runner(monkeypatch, runner)
83
+ custom = str(sandbox.home / "custom-skills")
84
+ assert main(["update", "--force", "--skills-dir", custom,
85
+ "--no-agents"]) == 0
86
+ assert runner.calls[-1] == [str(sandbox.bin_dir / "fusion"), "setup",
87
+ "--force", "--skills-dir", custom,
88
+ "--no-agents"]
89
+
90
+
91
+ def test_update_propagates_setup_exit_code(sandbox, monkeypatch):
92
+ runner = FakeRunner(sandbox.bin_dir, setup_rc=3)
93
+ _install_runner(monkeypatch, runner)
94
+ assert main(["update"]) == 3
95
+
96
+
97
+ def test_update_without_uv_fails_with_manual_path(sandbox, monkeypatch, capsys):
98
+ monkeypatch.setattr(update.shutil, "which", lambda name, *a, **kw: None)
99
+ _install_runner(monkeypatch, FakeRunner(sandbox.bin_dir))
100
+ assert main(["update"]) == 1
101
+ err = capsys.readouterr().err
102
+ assert "uv" in err and "fusion setup" in err
103
+
104
+
105
+ def test_update_finds_uv_at_install_sh_fallback(sandbox, monkeypatch):
106
+ monkeypatch.setattr(update.shutil, "which", lambda name, *a, **kw: None)
107
+ fallback = sandbox.home / ".local" / "bin" / "uv"
108
+ fallback.parent.mkdir(parents=True)
109
+ fallback.write_text("")
110
+ fallback.chmod(0o755)
111
+ runner = FakeRunner(sandbox.bin_dir)
112
+ _install_runner(monkeypatch, runner)
113
+ assert main(["update"]) == 0
114
+ assert runner.calls[0][0] == str(fallback)
115
+
116
+
117
+ def test_update_refuses_non_uv_install(sandbox, monkeypatch, capsys):
118
+ runner = FakeRunner(sandbox.bin_dir, list_stdout="black v24.0.0\n- black\n")
119
+ _install_runner(monkeypatch, runner)
120
+ assert main(["update"]) == 1
121
+ err = capsys.readouterr().err
122
+ assert "not managed by uv" in err and "fusion setup" in err
123
+ # never got as far as upgrading
124
+ assert [STUB_UV, "tool", "upgrade", "fusion-cli"] not in runner.calls
125
+ assert runner.calls == [[STUB_UV, "tool", "list"]]
126
+
127
+
128
+ def test_update_reports_failed_upgrade_with_retry_ritual(sandbox, monkeypatch,
129
+ capsys):
130
+ runner = FakeRunner(sandbox.bin_dir, upgrade_rc=1)
131
+ _install_runner(monkeypatch, runner)
132
+ assert main(["update"]) == 1
133
+ err = capsys.readouterr().err
134
+ assert "uv tool upgrade fusion-cli && fusion setup" in err
135
+ # setup never ran on a failed upgrade
136
+ assert all(c[1:2] != ["setup"] for c in runner.calls)
137
+
138
+
139
+ def test_update_appears_in_help(sandbox):
140
+ assert "update" in build_parser().format_help()
@@ -13,7 +13,7 @@ wheels = [
13
13
 
14
14
  [[package]]
15
15
  name = "fusion-cli"
16
- version = "1.2.1"
16
+ version = "1.3.1"
17
17
  source = { editable = "." }
18
18
  dependencies = [
19
19
  { name = "pyyaml" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes