kanibako-cli 1.5.0.dev14__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.
- kanibako/__init__.py +3 -0
- kanibako/__main__.py +6 -0
- kanibako/auth_browser.py +296 -0
- kanibako/auth_parser.py +51 -0
- kanibako/browser_sidecar.py +183 -0
- kanibako/browser_state.py +103 -0
- kanibako/bun_sea.py +144 -0
- kanibako/cli.py +344 -0
- kanibako/commands/__init__.py +0 -0
- kanibako/commands/archive.py +228 -0
- kanibako/commands/box/__init__.py +22 -0
- kanibako/commands/box/_duplicate.py +395 -0
- kanibako/commands/box/_migrate.py +574 -0
- kanibako/commands/box/_parser.py +1178 -0
- kanibako/commands/clean.py +166 -0
- kanibako/commands/crab_cmd.py +480 -0
- kanibako/commands/diagnose.py +239 -0
- kanibako/commands/fork_cmd.py +51 -0
- kanibako/commands/helper_cmd.py +669 -0
- kanibako/commands/image.py +1300 -0
- kanibako/commands/install.py +152 -0
- kanibako/commands/refresh_credentials.py +67 -0
- kanibako/commands/restore.py +298 -0
- kanibako/commands/setup_cmd.py +89 -0
- kanibako/commands/start.py +1600 -0
- kanibako/commands/stop.py +116 -0
- kanibako/commands/system_cmd.py +224 -0
- kanibako/commands/upgrade.py +161 -0
- kanibako/commands/vault_cmd.py +199 -0
- kanibako/commands/workset_cmd.py +552 -0
- kanibako/config.py +514 -0
- kanibako/config_interface.py +573 -0
- kanibako/config_io.py +36 -0
- kanibako/container.py +607 -0
- kanibako/containerfiles.py +58 -0
- kanibako/containers/Containerfile.kanibako +99 -0
- kanibako/containers/Containerfile.template-android +55 -0
- kanibako/containers/Containerfile.template-dotnet +29 -0
- kanibako/containers/Containerfile.template-js +43 -0
- kanibako/containers/Containerfile.template-jvm +27 -0
- kanibako/containers/Containerfile.template-systems +46 -0
- kanibako/containers/__init__.py +0 -0
- kanibako/crabs.py +89 -0
- kanibako/errors.py +33 -0
- kanibako/freshness.py +67 -0
- kanibako/git.py +114 -0
- kanibako/helper_client.py +132 -0
- kanibako/helper_listener.py +538 -0
- kanibako/helpers.py +339 -0
- kanibako/hygiene.py +296 -0
- kanibako/image_sharing.py +133 -0
- kanibako/instructions.py +160 -0
- kanibako/log.py +31 -0
- kanibako/names.py +248 -0
- kanibako/paths.py +1483 -0
- kanibako/plugins/__init__.py +10 -0
- kanibako/registry.py +71 -0
- kanibako/rig_bundle.py +121 -0
- kanibako/rig_meta.py +92 -0
- kanibako/rig_registry.py +132 -0
- kanibako/rig_resolve.py +182 -0
- kanibako/rig_source.py +245 -0
- kanibako/scripts/__init__.py +0 -0
- kanibako/scripts/helper-init.sh +45 -0
- kanibako/scripts/kanibako-entry +12 -0
- kanibako/settings_resolve.py +312 -0
- kanibako/settings_seeds.py +154 -0
- kanibako/settings_shares.py +154 -0
- kanibako/shellenv.py +75 -0
- kanibako/snapshots.py +281 -0
- kanibako/targets/__init__.py +173 -0
- kanibako/targets/base.py +243 -0
- kanibako/targets/no_agent.py +58 -0
- kanibako/templates.py +60 -0
- kanibako/templates_image.py +224 -0
- kanibako/tweakcc.py +140 -0
- kanibako/tweakcc_cache.py +171 -0
- kanibako/utils.py +136 -0
- kanibako/workset.py +347 -0
- kanibako_cli-1.5.0.dev14.dist-info/METADATA +15 -0
- kanibako_cli-1.5.0.dev14.dist-info/RECORD +85 -0
- kanibako_cli-1.5.0.dev14.dist-info/WHEEL +5 -0
- kanibako_cli-1.5.0.dev14.dist-info/entry_points.txt +5 -0
- kanibako_cli-1.5.0.dev14.dist-info/licenses/LICENSE.md +594 -0
- kanibako_cli-1.5.0.dev14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""kanibako stop: stop running kanibako containers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from kanibako.config import config_file_path, load_config
|
|
9
|
+
from kanibako.container import ContainerRuntime
|
|
10
|
+
from kanibako.errors import ContainerError
|
|
11
|
+
from kanibako.paths import xdg, load_std_paths, resolve_any_project
|
|
12
|
+
from kanibako.utils import container_name_for
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
16
|
+
p = subparsers.add_parser(
|
|
17
|
+
"stop",
|
|
18
|
+
help="Stop a running kanibako container",
|
|
19
|
+
description="Stop a running kanibako container for a project.",
|
|
20
|
+
)
|
|
21
|
+
p.add_argument(
|
|
22
|
+
"project", nargs="?", default=None,
|
|
23
|
+
help="Project name or path (default: cwd)",
|
|
24
|
+
)
|
|
25
|
+
p.add_argument(
|
|
26
|
+
"--all", action="store_true", dest="all_containers",
|
|
27
|
+
help="Stop all running kanibako containers",
|
|
28
|
+
)
|
|
29
|
+
p.add_argument(
|
|
30
|
+
"--force", action="store_true",
|
|
31
|
+
help="Skip confirmation prompt (only relevant with --all)",
|
|
32
|
+
)
|
|
33
|
+
p.set_defaults(func=run)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run(args: argparse.Namespace) -> int:
|
|
37
|
+
try:
|
|
38
|
+
runtime = ContainerRuntime()
|
|
39
|
+
except ContainerError:
|
|
40
|
+
print(
|
|
41
|
+
"Error: No container runtime found.\n"
|
|
42
|
+
"Install podman (https://podman.io/) or Docker.",
|
|
43
|
+
file=sys.stderr,
|
|
44
|
+
)
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
if args.all_containers:
|
|
48
|
+
return _stop_all(runtime, force=getattr(args, "force", False))
|
|
49
|
+
|
|
50
|
+
return _stop_one(runtime, project_dir=getattr(args, "project", None))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _stop_one(runtime: ContainerRuntime, *, project_dir: str | None) -> int:
|
|
54
|
+
"""Stop the container for a single project."""
|
|
55
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
56
|
+
config = load_config(config_file)
|
|
57
|
+
std = load_std_paths(config)
|
|
58
|
+
|
|
59
|
+
proj = resolve_any_project(std, config, project_dir, initialize=False)
|
|
60
|
+
container_name = container_name_for(proj)
|
|
61
|
+
|
|
62
|
+
lock_file = proj.metadata_path / ".kanibako.lock"
|
|
63
|
+
|
|
64
|
+
if runtime.stop(container_name):
|
|
65
|
+
print(f"Stopped {container_name}")
|
|
66
|
+
# Clean up stopped container (persistent containers lack --rm)
|
|
67
|
+
if runtime.container_exists(container_name):
|
|
68
|
+
runtime.rm(container_name)
|
|
69
|
+
else:
|
|
70
|
+
print(f"No running container found for this project ({container_name})")
|
|
71
|
+
# Clean up stopped persistent container if it exists
|
|
72
|
+
if runtime.container_exists(container_name):
|
|
73
|
+
runtime.rm(container_name)
|
|
74
|
+
print(f"Removed stopped container: {container_name}")
|
|
75
|
+
else:
|
|
76
|
+
print("\nIf a stale lock file is blocking a new session, remove it manually:")
|
|
77
|
+
print(f" rm {lock_file}")
|
|
78
|
+
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _stop_all(runtime: ContainerRuntime, *, force: bool = False) -> int:
|
|
83
|
+
"""Stop all running kanibako containers."""
|
|
84
|
+
containers = runtime.list_running()
|
|
85
|
+
if not containers:
|
|
86
|
+
print("No running kanibako containers found.")
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
# Confirmation prompt unless --force
|
|
90
|
+
if not force:
|
|
91
|
+
names = [name for name, _, _ in containers]
|
|
92
|
+
print(f"This will stop {len(containers)} running container(s):")
|
|
93
|
+
for n in names:
|
|
94
|
+
print(f" {n}")
|
|
95
|
+
print()
|
|
96
|
+
try:
|
|
97
|
+
answer = input("Continue? [y/N] ").strip().lower()
|
|
98
|
+
except (EOFError, KeyboardInterrupt):
|
|
99
|
+
answer = ""
|
|
100
|
+
if answer not in ("y", "yes"):
|
|
101
|
+
print("Aborted.")
|
|
102
|
+
return 2
|
|
103
|
+
|
|
104
|
+
stopped = 0
|
|
105
|
+
for name, image, status in containers:
|
|
106
|
+
if runtime.stop(name):
|
|
107
|
+
print(f"Stopped {name}")
|
|
108
|
+
# Clean up stopped container (persistent containers lack --rm)
|
|
109
|
+
if runtime.container_exists(name):
|
|
110
|
+
runtime.rm(name)
|
|
111
|
+
stopped += 1
|
|
112
|
+
else:
|
|
113
|
+
print(f"Failed to stop {name}", file=sys.stderr)
|
|
114
|
+
|
|
115
|
+
print(f"\nStopped {stopped} container(s).")
|
|
116
|
+
return 0
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""kanibako system: global configuration, self-update, and system info."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from kanibako import __version__
|
|
9
|
+
from kanibako.config import config_file_path, load_config
|
|
10
|
+
from kanibako.paths import xdg
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
14
|
+
p = subparsers.add_parser(
|
|
15
|
+
"system",
|
|
16
|
+
help="Global configuration, upgrades, and system information",
|
|
17
|
+
description="Manage global kanibako configuration and perform system tasks.",
|
|
18
|
+
)
|
|
19
|
+
sys_sub = p.add_subparsers(dest="system_command", metavar="COMMAND")
|
|
20
|
+
|
|
21
|
+
# system info (default)
|
|
22
|
+
info_p = sys_sub.add_parser(
|
|
23
|
+
"info",
|
|
24
|
+
aliases=["inspect"],
|
|
25
|
+
help="Show system information",
|
|
26
|
+
)
|
|
27
|
+
info_p.set_defaults(func=run_info)
|
|
28
|
+
|
|
29
|
+
# system config [key[=value]] [--effective] [--reset] [--all] [--force]
|
|
30
|
+
config_p = sys_sub.add_parser(
|
|
31
|
+
"config",
|
|
32
|
+
help="View or modify global configuration",
|
|
33
|
+
)
|
|
34
|
+
config_p.add_argument(
|
|
35
|
+
"key_value", nargs="?", default=None,
|
|
36
|
+
help="key or key=value",
|
|
37
|
+
)
|
|
38
|
+
config_p.add_argument(
|
|
39
|
+
"--effective", action="store_true",
|
|
40
|
+
help="Show all resolved values including defaults",
|
|
41
|
+
)
|
|
42
|
+
config_p.add_argument(
|
|
43
|
+
"--reset", action="store_true",
|
|
44
|
+
help="Remove an override (revert to default)",
|
|
45
|
+
)
|
|
46
|
+
config_p.add_argument(
|
|
47
|
+
"--all", action="store_true", dest="all_keys",
|
|
48
|
+
help="With --reset: remove all overrides",
|
|
49
|
+
)
|
|
50
|
+
config_p.add_argument(
|
|
51
|
+
"--force", action="store_true",
|
|
52
|
+
help="Skip confirmation prompts",
|
|
53
|
+
)
|
|
54
|
+
config_p.set_defaults(func=run_config)
|
|
55
|
+
|
|
56
|
+
# system upgrade [--check]
|
|
57
|
+
from kanibako.commands.upgrade import run as run_upgrade_fn
|
|
58
|
+
|
|
59
|
+
upgrade_p = sys_sub.add_parser(
|
|
60
|
+
"upgrade",
|
|
61
|
+
help="Upgrade kanibako to the latest version",
|
|
62
|
+
)
|
|
63
|
+
upgrade_p.add_argument(
|
|
64
|
+
"--check", action="store_true",
|
|
65
|
+
help="Check for updates without installing",
|
|
66
|
+
)
|
|
67
|
+
upgrade_p.set_defaults(func=run_upgrade_fn)
|
|
68
|
+
|
|
69
|
+
# system diagnose
|
|
70
|
+
from kanibako.commands.diagnose import run_system_diagnose
|
|
71
|
+
|
|
72
|
+
diagnose_p = sys_sub.add_parser(
|
|
73
|
+
"diagnose",
|
|
74
|
+
help="Check system health (runtime, images, agents, storage)",
|
|
75
|
+
)
|
|
76
|
+
diagnose_p.set_defaults(func=run_system_diagnose)
|
|
77
|
+
|
|
78
|
+
# Default to info when 'system' is run without a subcommand
|
|
79
|
+
p.set_defaults(func=run_info)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def run_info(args: argparse.Namespace) -> int:
|
|
83
|
+
"""Show system information: version, paths, runtime."""
|
|
84
|
+
import platform
|
|
85
|
+
|
|
86
|
+
config_home = xdg("XDG_CONFIG_HOME", ".config")
|
|
87
|
+
cf = config_file_path(config_home)
|
|
88
|
+
|
|
89
|
+
print(f"Kanibako v{__version__}")
|
|
90
|
+
print(f"Python: {platform.python_version()}")
|
|
91
|
+
|
|
92
|
+
if cf.exists():
|
|
93
|
+
print(f"Config: {cf}")
|
|
94
|
+
config = load_config(cf)
|
|
95
|
+
from pathlib import Path
|
|
96
|
+
|
|
97
|
+
from kanibako.paths import resolve_system_paths
|
|
98
|
+
data_home = xdg("XDG_DATA_HOME", ".local/share")
|
|
99
|
+
data_path = resolve_system_paths(
|
|
100
|
+
config.system_paths, data_home=data_home, home=Path.home(),
|
|
101
|
+
)["system.path.data"]
|
|
102
|
+
print(f"Data: {data_path}")
|
|
103
|
+
else:
|
|
104
|
+
print(
|
|
105
|
+
"Config: (not initialized — run 'kanibako setup' or just 'kanibako start')"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Container runtime
|
|
109
|
+
try:
|
|
110
|
+
import subprocess
|
|
111
|
+
|
|
112
|
+
from kanibako.container import ContainerRuntime
|
|
113
|
+
|
|
114
|
+
runtime = ContainerRuntime()
|
|
115
|
+
result = subprocess.run(
|
|
116
|
+
[runtime.cmd, "--version"], capture_output=True, text=True,
|
|
117
|
+
)
|
|
118
|
+
version = result.stdout.strip() if result.returncode == 0 else "unknown"
|
|
119
|
+
print(f"Runtime: {runtime.cmd} ({version})")
|
|
120
|
+
except Exception:
|
|
121
|
+
print(
|
|
122
|
+
"Runtime: not found — install podman (https://podman.io/) or Docker"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Install method
|
|
126
|
+
try:
|
|
127
|
+
from kanibako.commands.upgrade import _get_repo_dir
|
|
128
|
+
|
|
129
|
+
repo = _get_repo_dir()
|
|
130
|
+
if repo is not None:
|
|
131
|
+
print(f"Install: git ({repo})")
|
|
132
|
+
else:
|
|
133
|
+
print("Install: pip")
|
|
134
|
+
except Exception:
|
|
135
|
+
print("Install: pip")
|
|
136
|
+
|
|
137
|
+
# Agent count
|
|
138
|
+
try:
|
|
139
|
+
from kanibako.targets import discover_targets
|
|
140
|
+
|
|
141
|
+
targets = discover_targets()
|
|
142
|
+
count = len(targets)
|
|
143
|
+
if count > 0:
|
|
144
|
+
print(
|
|
145
|
+
f"Agents: {count} detected (use 'kanibako crab list' for details)"
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
print(
|
|
149
|
+
"Agents: none (install a plugin: pip install kanibako-agent-claude)"
|
|
150
|
+
)
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
print()
|
|
155
|
+
print("Tip: Run 'kanibako system diagnose' for a full health check.")
|
|
156
|
+
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def run_config(args: argparse.Namespace) -> int:
|
|
161
|
+
"""View or modify global configuration."""
|
|
162
|
+
config_home = xdg("XDG_CONFIG_HOME", ".config")
|
|
163
|
+
cf = config_file_path(config_home)
|
|
164
|
+
|
|
165
|
+
from kanibako.config_interface import (
|
|
166
|
+
ConfigAction,
|
|
167
|
+
get_config_value,
|
|
168
|
+
is_known_key,
|
|
169
|
+
parse_config_arg,
|
|
170
|
+
reset_all,
|
|
171
|
+
reset_config_value,
|
|
172
|
+
set_config_value,
|
|
173
|
+
show_config,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
key_value = getattr(args, "key_value", None)
|
|
177
|
+
action, key, value = parse_config_arg(key_value)
|
|
178
|
+
|
|
179
|
+
# --reset --all
|
|
180
|
+
if args.reset and getattr(args, "all_keys", False):
|
|
181
|
+
msg = reset_all(config_path=cf, force=args.force)
|
|
182
|
+
print(msg)
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
# --reset <key>
|
|
186
|
+
if args.reset:
|
|
187
|
+
if not key:
|
|
188
|
+
print(
|
|
189
|
+
"Error: --reset requires a key (or use --reset --all).",
|
|
190
|
+
file=sys.stderr,
|
|
191
|
+
)
|
|
192
|
+
return 1
|
|
193
|
+
msg = reset_config_value(key, config_path=cf)
|
|
194
|
+
print(msg)
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
# show (no args)
|
|
198
|
+
if action == ConfigAction.show:
|
|
199
|
+
show_config(
|
|
200
|
+
global_config_path=cf,
|
|
201
|
+
config_path=cf,
|
|
202
|
+
effective=args.effective,
|
|
203
|
+
)
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
# get
|
|
207
|
+
if action == ConfigAction.get:
|
|
208
|
+
if not is_known_key(key):
|
|
209
|
+
print(f"Error: unknown config key: {key}", file=sys.stderr)
|
|
210
|
+
return 1
|
|
211
|
+
val = get_config_value(key, global_config_path=cf)
|
|
212
|
+
if val is None:
|
|
213
|
+
print(f"{key}: (not set)")
|
|
214
|
+
else:
|
|
215
|
+
print(f"{key}={val}")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
# set
|
|
219
|
+
if action == ConfigAction.set:
|
|
220
|
+
msg = set_config_value(key, value, config_path=cf, is_system=True)
|
|
221
|
+
print(msg)
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
return 0
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""kanibako upgrade: update kanibako itself from git."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
12
|
+
p = subparsers.add_parser(
|
|
13
|
+
"upgrade",
|
|
14
|
+
help="Upgrade kanibako to the latest version",
|
|
15
|
+
description="Upgrade kanibako by pulling the latest changes from git.",
|
|
16
|
+
)
|
|
17
|
+
p.add_argument(
|
|
18
|
+
"--check", action="store_true",
|
|
19
|
+
help="Check for updates without installing",
|
|
20
|
+
)
|
|
21
|
+
p.set_defaults(func=run)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_repo_dir() -> Path | None:
|
|
25
|
+
"""Find the kanibako git repository directory."""
|
|
26
|
+
# Start from this file's location and walk up to find .git
|
|
27
|
+
current = Path(__file__).resolve().parent
|
|
28
|
+
for _ in range(5): # Don't walk up forever
|
|
29
|
+
if (current / ".git").is_dir():
|
|
30
|
+
return current
|
|
31
|
+
parent = current.parent
|
|
32
|
+
if parent == current:
|
|
33
|
+
break
|
|
34
|
+
current = parent
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _git(*args: str, cwd: Path) -> subprocess.CompletedProcess:
|
|
39
|
+
"""Run a git command in the given directory."""
|
|
40
|
+
return subprocess.run(
|
|
41
|
+
["git", *args],
|
|
42
|
+
cwd=cwd,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_current_commit(repo: Path) -> str | None:
|
|
49
|
+
"""Get the current commit hash."""
|
|
50
|
+
result = _git("rev-parse", "HEAD", cwd=repo)
|
|
51
|
+
if result.returncode == 0:
|
|
52
|
+
return result.stdout.strip()
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_remote_commit(repo: Path) -> str | None:
|
|
57
|
+
"""Get the latest remote commit hash after fetching."""
|
|
58
|
+
# Fetch latest from remote
|
|
59
|
+
result = _git("fetch", cwd=repo)
|
|
60
|
+
if result.returncode != 0:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
# Get the upstream branch name
|
|
64
|
+
result = _git("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}", cwd=repo)
|
|
65
|
+
if result.returncode != 0:
|
|
66
|
+
# No upstream configured, try origin/main or origin/master
|
|
67
|
+
for branch in ("origin/main", "origin/master"):
|
|
68
|
+
result = _git("rev-parse", branch, cwd=repo)
|
|
69
|
+
if result.returncode == 0:
|
|
70
|
+
return result.stdout.strip()
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
upstream = result.stdout.strip()
|
|
74
|
+
result = _git("rev-parse", upstream, cwd=repo)
|
|
75
|
+
if result.returncode == 0:
|
|
76
|
+
return result.stdout.strip()
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_commit_count_behind(repo: Path) -> int | None:
|
|
81
|
+
"""Get number of commits behind upstream."""
|
|
82
|
+
result = _git("rev-list", "--count", "HEAD..@{u}", cwd=repo)
|
|
83
|
+
if result.returncode == 0:
|
|
84
|
+
try:
|
|
85
|
+
return int(result.stdout.strip())
|
|
86
|
+
except ValueError:
|
|
87
|
+
pass
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def run(args: argparse.Namespace) -> int:
|
|
92
|
+
repo = _get_repo_dir()
|
|
93
|
+
if repo is None:
|
|
94
|
+
print("Error: Could not find kanibako git repository.", file=sys.stderr)
|
|
95
|
+
print("kanibako upgrade only works for git-based installations.", file=sys.stderr)
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
# Check for uncommitted changes
|
|
99
|
+
result = _git("status", "--porcelain", cwd=repo)
|
|
100
|
+
if result.returncode != 0:
|
|
101
|
+
print("Error: Failed to check git status.", file=sys.stderr)
|
|
102
|
+
return 1
|
|
103
|
+
|
|
104
|
+
has_changes = bool(result.stdout.strip())
|
|
105
|
+
|
|
106
|
+
current = _get_current_commit(repo)
|
|
107
|
+
if current is None:
|
|
108
|
+
print("Error: Failed to get current commit.", file=sys.stderr)
|
|
109
|
+
return 1
|
|
110
|
+
|
|
111
|
+
print(f"Repository: {repo}")
|
|
112
|
+
print(f"Current: {current[:8]}")
|
|
113
|
+
|
|
114
|
+
# Fetch and check for updates
|
|
115
|
+
print("Checking for updates...", end=" ", flush=True)
|
|
116
|
+
remote = _get_remote_commit(repo)
|
|
117
|
+
if remote is None:
|
|
118
|
+
print("failed")
|
|
119
|
+
print("Error: Failed to fetch from remote.", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
|
|
122
|
+
if current == remote:
|
|
123
|
+
print("up to date")
|
|
124
|
+
return 0
|
|
125
|
+
|
|
126
|
+
behind = _get_commit_count_behind(repo)
|
|
127
|
+
behind_str = f"{behind} commit(s)" if behind else "updates"
|
|
128
|
+
print(f"{behind_str} available")
|
|
129
|
+
print(f"Latest: {remote[:8]}")
|
|
130
|
+
|
|
131
|
+
if args.check:
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
# Actually upgrade
|
|
135
|
+
if has_changes:
|
|
136
|
+
print()
|
|
137
|
+
print("Warning: You have uncommitted changes in the repository.", file=sys.stderr)
|
|
138
|
+
print("Stash or commit them before upgrading.", file=sys.stderr)
|
|
139
|
+
return 1
|
|
140
|
+
|
|
141
|
+
print()
|
|
142
|
+
print("Pulling latest changes...")
|
|
143
|
+
result = _git("pull", "--ff-only", cwd=repo)
|
|
144
|
+
if result.returncode != 0:
|
|
145
|
+
print("Error: git pull failed.", file=sys.stderr)
|
|
146
|
+
if result.stderr:
|
|
147
|
+
print(result.stderr, file=sys.stderr)
|
|
148
|
+
return 1
|
|
149
|
+
|
|
150
|
+
print("Upgraded successfully.")
|
|
151
|
+
|
|
152
|
+
# Check if pyproject.toml changed (might need reinstall)
|
|
153
|
+
result = _git("diff", "--name-only", f"{current}..HEAD", cwd=repo)
|
|
154
|
+
changed_files = result.stdout.strip().split("\n") if result.stdout.strip() else []
|
|
155
|
+
|
|
156
|
+
if "pyproject.toml" in changed_files:
|
|
157
|
+
print()
|
|
158
|
+
print("Note: pyproject.toml changed. You may need to reinstall:")
|
|
159
|
+
print(f" pip install -e {repo}")
|
|
160
|
+
|
|
161
|
+
return 0
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""kanibako vault: manage vault snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from kanibako.config import config_file_path, load_config
|
|
9
|
+
from kanibako.paths import xdg, load_std_paths, resolve_any_project
|
|
10
|
+
from kanibako.snapshots import (
|
|
11
|
+
_DEFAULT_MAX_SNAPSHOTS,
|
|
12
|
+
create_snapshot,
|
|
13
|
+
list_snapshots,
|
|
14
|
+
prune_snapshots,
|
|
15
|
+
restore_snapshot,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def add_vault_subparser(parent_sub: argparse._SubParsersAction) -> None:
|
|
20
|
+
"""Register vault as a subcommand (used by box parser to nest under box)."""
|
|
21
|
+
p = parent_sub.add_parser(
|
|
22
|
+
"vault",
|
|
23
|
+
help="Vault snapshot commands (snapshot, list, restore, prune)",
|
|
24
|
+
description="Manage vault share-rw snapshots.",
|
|
25
|
+
)
|
|
26
|
+
_add_vault_subcommands(p)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
30
|
+
"""Register vault as a top-level command (kept for backward compat during transition)."""
|
|
31
|
+
p = subparsers.add_parser(
|
|
32
|
+
"vault",
|
|
33
|
+
help="Vault snapshot commands (snapshot, list, restore, prune)",
|
|
34
|
+
description="Manage vault share-rw snapshots.",
|
|
35
|
+
)
|
|
36
|
+
_add_vault_subcommands(p)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _add_vault_subcommands(p: argparse.ArgumentParser) -> None:
|
|
40
|
+
vs = p.add_subparsers(dest="vault_command", metavar="COMMAND")
|
|
41
|
+
|
|
42
|
+
# kanibako box vault snapshot [project]
|
|
43
|
+
snap_p = vs.add_parser(
|
|
44
|
+
"snapshot",
|
|
45
|
+
help="Create a snapshot of vault share-rw",
|
|
46
|
+
description="Create a point-in-time snapshot of the vault share-rw directory.",
|
|
47
|
+
)
|
|
48
|
+
snap_p.add_argument(
|
|
49
|
+
"project", nargs="?", default=None,
|
|
50
|
+
help="Project directory or name (default: cwd)",
|
|
51
|
+
)
|
|
52
|
+
snap_p.set_defaults(func=run_snapshot)
|
|
53
|
+
|
|
54
|
+
# kanibako box vault list [project] [-q/--quiet]
|
|
55
|
+
list_p = vs.add_parser(
|
|
56
|
+
"list",
|
|
57
|
+
help="List vault snapshots (default)",
|
|
58
|
+
description="Show all snapshots for the current project's vault.",
|
|
59
|
+
)
|
|
60
|
+
list_p.add_argument(
|
|
61
|
+
"project", nargs="?", default=None,
|
|
62
|
+
help="Project directory or name (default: cwd)",
|
|
63
|
+
)
|
|
64
|
+
list_p.add_argument(
|
|
65
|
+
"-q", "--quiet", action="store_true",
|
|
66
|
+
help="Output snapshot names only, one per line",
|
|
67
|
+
)
|
|
68
|
+
list_p.set_defaults(func=run_list)
|
|
69
|
+
|
|
70
|
+
# kanibako box vault restore <name> [project] [--force]
|
|
71
|
+
restore_p = vs.add_parser(
|
|
72
|
+
"restore",
|
|
73
|
+
help="Restore vault share-rw from a snapshot",
|
|
74
|
+
description="Replace the current share-rw contents with a snapshot.",
|
|
75
|
+
)
|
|
76
|
+
restore_p.add_argument("name", help="Snapshot name (e.g. 20260221T103000Z.tar.xz)")
|
|
77
|
+
restore_p.add_argument(
|
|
78
|
+
"project", nargs="?", default=None,
|
|
79
|
+
help="Project directory or name (default: cwd)",
|
|
80
|
+
)
|
|
81
|
+
restore_p.add_argument(
|
|
82
|
+
"--force", action="store_true",
|
|
83
|
+
help="Skip confirmation prompt",
|
|
84
|
+
)
|
|
85
|
+
restore_p.set_defaults(func=run_restore)
|
|
86
|
+
|
|
87
|
+
# kanibako box vault prune [project] [--keep N] [--force]
|
|
88
|
+
prune_p = vs.add_parser(
|
|
89
|
+
"prune",
|
|
90
|
+
help="Remove old snapshots",
|
|
91
|
+
description="Prune old vault snapshots, keeping the most recent ones.",
|
|
92
|
+
)
|
|
93
|
+
prune_p.add_argument(
|
|
94
|
+
"project", nargs="?", default=None,
|
|
95
|
+
help="Project directory or name (default: cwd)",
|
|
96
|
+
)
|
|
97
|
+
prune_p.add_argument(
|
|
98
|
+
"--keep", type=int, default=_DEFAULT_MAX_SNAPSHOTS,
|
|
99
|
+
help=f"Number of snapshots to keep (default: {_DEFAULT_MAX_SNAPSHOTS})",
|
|
100
|
+
)
|
|
101
|
+
prune_p.add_argument(
|
|
102
|
+
"--force", action="store_true",
|
|
103
|
+
help="Skip confirmation prompt",
|
|
104
|
+
)
|
|
105
|
+
prune_p.set_defaults(func=run_prune)
|
|
106
|
+
|
|
107
|
+
p.set_defaults(func=run_list)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resolve_vault_rw(project_dir: str | None):
|
|
111
|
+
"""Resolve the vault share-rw path for the current project."""
|
|
112
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
113
|
+
config = load_config(config_file)
|
|
114
|
+
std = load_std_paths(config)
|
|
115
|
+
proj = resolve_any_project(std, config, project_dir, initialize=False)
|
|
116
|
+
|
|
117
|
+
if not proj.enable_vault:
|
|
118
|
+
print("Vault is disabled for this project.", file=sys.stderr)
|
|
119
|
+
return None
|
|
120
|
+
return proj.vault_rw_path
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run_snapshot(args: argparse.Namespace) -> int:
|
|
124
|
+
project_dir = getattr(args, "project", None)
|
|
125
|
+
vault_rw = _resolve_vault_rw(project_dir)
|
|
126
|
+
if vault_rw is None:
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
snap = create_snapshot(vault_rw)
|
|
130
|
+
if snap is None:
|
|
131
|
+
print("Nothing to snapshot (share-rw is empty or missing).", file=sys.stderr)
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
print(f"Snapshot created: {snap.name}")
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_list(args: argparse.Namespace) -> int:
|
|
139
|
+
project_dir = getattr(args, "project", None)
|
|
140
|
+
vault_rw = _resolve_vault_rw(project_dir)
|
|
141
|
+
if vault_rw is None:
|
|
142
|
+
return 1
|
|
143
|
+
|
|
144
|
+
quiet = getattr(args, "quiet", False)
|
|
145
|
+
|
|
146
|
+
snaps = list_snapshots(vault_rw)
|
|
147
|
+
if not snaps:
|
|
148
|
+
if not quiet:
|
|
149
|
+
print("No snapshots found.")
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
for name, ts, size in snaps:
|
|
153
|
+
if quiet:
|
|
154
|
+
print(name)
|
|
155
|
+
else:
|
|
156
|
+
size_str = _human_size(size)
|
|
157
|
+
print(f" {name} {ts} {size_str}")
|
|
158
|
+
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def run_restore(args: argparse.Namespace) -> int:
|
|
163
|
+
project_dir = getattr(args, "project", None)
|
|
164
|
+
vault_rw = _resolve_vault_rw(project_dir)
|
|
165
|
+
if vault_rw is None:
|
|
166
|
+
return 1
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
restore_snapshot(vault_rw, args.name)
|
|
170
|
+
except FileNotFoundError as e:
|
|
171
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
172
|
+
return 1
|
|
173
|
+
|
|
174
|
+
print(f"Restored vault share-rw from {args.name}")
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def run_prune(args: argparse.Namespace) -> int:
|
|
179
|
+
project_dir = getattr(args, "project", None)
|
|
180
|
+
vault_rw = _resolve_vault_rw(project_dir)
|
|
181
|
+
if vault_rw is None:
|
|
182
|
+
return 1
|
|
183
|
+
|
|
184
|
+
removed = prune_snapshots(vault_rw, max_keep=args.keep)
|
|
185
|
+
if removed:
|
|
186
|
+
print(f"Pruned {removed} snapshot(s), keeping {args.keep}.")
|
|
187
|
+
else:
|
|
188
|
+
print("Nothing to prune.")
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _human_size(nbytes: int) -> str:
|
|
193
|
+
"""Format byte count as human-readable string."""
|
|
194
|
+
size = float(nbytes)
|
|
195
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
196
|
+
if size < 1024:
|
|
197
|
+
return f"{size:.0f}{unit}" if unit == "B" else f"{size:.1f}{unit}"
|
|
198
|
+
size /= 1024
|
|
199
|
+
return f"{size:.1f}TB"
|