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,152 @@
|
|
|
1
|
+
"""kanibako install utilities: setup logic and shell completion.
|
|
2
|
+
|
|
3
|
+
The ``setup`` CLI command has been replaced by lazy initialization
|
|
4
|
+
(``_ensure_initialized`` in ``cli.py``). This module is kept for
|
|
5
|
+
``_install_completion()`` and the ``run()`` helper used by lazy init
|
|
6
|
+
and tests.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from kanibako.config import (
|
|
16
|
+
KanibakoConfig,
|
|
17
|
+
config_file_path,
|
|
18
|
+
load_config,
|
|
19
|
+
write_global_config,
|
|
20
|
+
)
|
|
21
|
+
from kanibako.container import ContainerRuntime
|
|
22
|
+
from kanibako.containerfiles import get_containerfile
|
|
23
|
+
from kanibako.paths import xdg
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run(args: argparse.Namespace) -> int:
|
|
27
|
+
config_home = xdg("XDG_CONFIG_HOME", ".config")
|
|
28
|
+
config_file = config_file_path(config_home)
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# 1. Write config
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
if config_file.exists():
|
|
34
|
+
print("Configuration file already exists, loading.")
|
|
35
|
+
config = load_config(config_file)
|
|
36
|
+
else:
|
|
37
|
+
print("Writing general configuration file (kanibako.yaml)... ", end="", flush=True)
|
|
38
|
+
config = KanibakoConfig()
|
|
39
|
+
write_global_config(config_file, config)
|
|
40
|
+
print("done!")
|
|
41
|
+
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
# 2. Create containers directory for user overrides
|
|
44
|
+
# ------------------------------------------------------------------
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
from kanibako.paths import resolve_system_paths
|
|
48
|
+
|
|
49
|
+
data_home = xdg("XDG_DATA_HOME", ".local/share")
|
|
50
|
+
sys_paths = resolve_system_paths(
|
|
51
|
+
config.system_paths, data_home=data_home, home=Path.home(),
|
|
52
|
+
)
|
|
53
|
+
data_path = sys_paths["system.path.data"]
|
|
54
|
+
containers_dest = data_path / "containers"
|
|
55
|
+
containers_dest.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
# Create template directory structure.
|
|
58
|
+
templates_dir = sys_paths["system.path.templates"]
|
|
59
|
+
(templates_dir / "general" / "base").mkdir(parents=True, exist_ok=True)
|
|
60
|
+
(templates_dir / "general" / "standard").mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
# Create peer communication directory.
|
|
63
|
+
comms_dir = sys_paths["system.path.comms"]
|
|
64
|
+
(comms_dir / "mailbox").mkdir(parents=True, exist_ok=True)
|
|
65
|
+
(comms_dir / "broadcast.log").touch(exist_ok=True)
|
|
66
|
+
|
|
67
|
+
# Create crabs directory and generate default crab TOMLs.
|
|
68
|
+
from kanibako.crabs import CrabConfig, write_crab_config
|
|
69
|
+
from kanibako.targets import discover_targets
|
|
70
|
+
|
|
71
|
+
crabs_path = sys_paths["system.path.crabs"]
|
|
72
|
+
crabs_path.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
# general.yaml (no-agent default)
|
|
75
|
+
general_toml = crabs_path / "general.yaml"
|
|
76
|
+
if not general_toml.exists():
|
|
77
|
+
write_crab_config(general_toml, CrabConfig(name="Shell"))
|
|
78
|
+
|
|
79
|
+
# Each discovered target plugin
|
|
80
|
+
for target_name, cls in discover_targets().items():
|
|
81
|
+
target_toml = crabs_path / f"{target_name}.yaml"
|
|
82
|
+
if not target_toml.exists():
|
|
83
|
+
agent_cfg = cls().generate_crab_config()
|
|
84
|
+
write_crab_config(target_toml, agent_cfg)
|
|
85
|
+
else:
|
|
86
|
+
agent_cfg = CrabConfig() # just need the shell default
|
|
87
|
+
# Create the agent-specific template variant directory.
|
|
88
|
+
(templates_dir / target_name / agent_cfg.shell).mkdir(parents=True, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
# Seed default global environment variables (don't overwrite existing).
|
|
91
|
+
from kanibako.shellenv import read_env_file, write_env_file
|
|
92
|
+
|
|
93
|
+
global_env_path = data_path / "env"
|
|
94
|
+
global_env = read_env_file(global_env_path)
|
|
95
|
+
_DEFAULT_ENV = {"COLORTERM": "truecolor"}
|
|
96
|
+
for key, value in _DEFAULT_ENV.items():
|
|
97
|
+
global_env.setdefault(key, value)
|
|
98
|
+
write_env_file(global_env_path, global_env)
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
# 3. Pull or build base container image
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
try:
|
|
104
|
+
runtime = ContainerRuntime()
|
|
105
|
+
from kanibako.commands.image import resolve_image_reference
|
|
106
|
+
image = resolve_image_reference(
|
|
107
|
+
config.box_image, runtime, config.box_image,
|
|
108
|
+
)
|
|
109
|
+
if runtime.image_exists(image):
|
|
110
|
+
print("Container rig already exists, skipping.")
|
|
111
|
+
elif runtime.pull(image):
|
|
112
|
+
print("Rig pulled from registry!")
|
|
113
|
+
else:
|
|
114
|
+
print("Pull failed; building locally...")
|
|
115
|
+
base_cf = get_containerfile("base", containers_dest)
|
|
116
|
+
if base_cf is not None:
|
|
117
|
+
runtime.build(image, base_cf, base_cf.parent)
|
|
118
|
+
print("Base rig built!")
|
|
119
|
+
else:
|
|
120
|
+
print("Warning: No Containerfile.base found; skipping build.", file=sys.stderr)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"Warning: {e}", file=sys.stderr)
|
|
123
|
+
print("Skipping rig setup.")
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
# 4. Register shell completion
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
print("Setting up shell completion... ", end="", flush=True)
|
|
129
|
+
_install_completion()
|
|
130
|
+
print("done!")
|
|
131
|
+
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _install_completion() -> None:
|
|
136
|
+
"""Register bash/zsh completion for kanibako via argcomplete."""
|
|
137
|
+
completions_dir = xdg("XDG_DATA_HOME", ".local/share") / "bash-completion" / "completions"
|
|
138
|
+
completions_dir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
target = completions_dir / "kanibako"
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
result = subprocess.run(
|
|
143
|
+
["register-python-argcomplete", "kanibako"],
|
|
144
|
+
capture_output=True,
|
|
145
|
+
text=True,
|
|
146
|
+
)
|
|
147
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
148
|
+
target.write_text(result.stdout)
|
|
149
|
+
else:
|
|
150
|
+
print("(register-python-argcomplete failed, skipping)", end=" ")
|
|
151
|
+
except FileNotFoundError:
|
|
152
|
+
print("(argcomplete not on PATH, skipping)", end=" ")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""kanibako reauth: manually verify or re-establish agent authentication."""
|
|
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
|
|
10
|
+
from kanibako.targets import resolve_target
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
14
|
+
p = subparsers.add_parser(
|
|
15
|
+
"reauth",
|
|
16
|
+
help="Check authentication and login if needed",
|
|
17
|
+
description="Verify agent authentication status and run interactive "
|
|
18
|
+
"login if credentials are expired or missing.",
|
|
19
|
+
)
|
|
20
|
+
p.add_argument(
|
|
21
|
+
"-p", "--project", default=None, help="Target a specific project directory",
|
|
22
|
+
)
|
|
23
|
+
p.set_defaults(func=run)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run(args: argparse.Namespace) -> int:
|
|
27
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
28
|
+
config = load_config(config_file)
|
|
29
|
+
|
|
30
|
+
# Resolve project to check auth mode.
|
|
31
|
+
from kanibako.paths import load_std_paths, resolve_any_project
|
|
32
|
+
std = load_std_paths(config)
|
|
33
|
+
proj = resolve_any_project(std, config, getattr(args, "project", None))
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
target = resolve_target(config.box_crab or None)
|
|
37
|
+
except KeyError as e:
|
|
38
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
39
|
+
return 1
|
|
40
|
+
|
|
41
|
+
if not target.has_binary:
|
|
42
|
+
print("No agent target configured.", file=sys.stderr)
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
if not proj.group_auth:
|
|
46
|
+
# Check project's own credentials instead of host.
|
|
47
|
+
creds_path = target.credential_check_path(proj.shell_path)
|
|
48
|
+
if creds_path and creds_path.is_file():
|
|
49
|
+
print(f"{target.display_name}: distinct auth (project credentials exist).", file=sys.stderr)
|
|
50
|
+
return 0
|
|
51
|
+
else:
|
|
52
|
+
print(
|
|
53
|
+
f"{target.display_name}: distinct auth — no credentials found. "
|
|
54
|
+
"Launch the container to authenticate.",
|
|
55
|
+
file=sys.stderr,
|
|
56
|
+
)
|
|
57
|
+
return 1
|
|
58
|
+
|
|
59
|
+
if target.check_auth():
|
|
60
|
+
# Sync refreshed credentials to the project shell directory
|
|
61
|
+
if proj.group_auth:
|
|
62
|
+
target.refresh_credentials(proj.shell_path)
|
|
63
|
+
print(f"{target.display_name}: authenticated.", file=sys.stderr)
|
|
64
|
+
return 0
|
|
65
|
+
else:
|
|
66
|
+
print(f"{target.display_name}: authentication failed.", file=sys.stderr)
|
|
67
|
+
return 1
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""kanibako extract: restore session data from archive with validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from kanibako.config import config_file_path, load_config
|
|
14
|
+
from kanibako.errors import UserCancelled
|
|
15
|
+
from kanibako.git import is_git_repo
|
|
16
|
+
from kanibako.paths import xdg, load_std_paths, resolve_any_project
|
|
17
|
+
from kanibako.utils import confirm_prompt
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
21
|
+
p = subparsers.add_parser(
|
|
22
|
+
"extract",
|
|
23
|
+
help="Extract session data from archive",
|
|
24
|
+
description="Extract session data from a .txz archive created by 'kanibako box archive'.",
|
|
25
|
+
)
|
|
26
|
+
p.add_argument("file", nargs="?", default=None, help="Archive file to extract from")
|
|
27
|
+
p.add_argument("path", nargs="?", default=None, help="Path to the project directory")
|
|
28
|
+
p.add_argument(
|
|
29
|
+
"--name", default=None,
|
|
30
|
+
help="Override project name for the extracted data",
|
|
31
|
+
)
|
|
32
|
+
p.add_argument(
|
|
33
|
+
"--all", action="store_true", dest="all_archives",
|
|
34
|
+
help="Extract all kanibako-*.txz archives in the current directory",
|
|
35
|
+
)
|
|
36
|
+
p.add_argument("--force", action="store_true", help="Skip all confirmation prompts")
|
|
37
|
+
p.set_defaults(func=run)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run(args: argparse.Namespace) -> int:
|
|
41
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
42
|
+
config = load_config(config_file)
|
|
43
|
+
std = load_std_paths(config)
|
|
44
|
+
|
|
45
|
+
if args.all_archives:
|
|
46
|
+
return _restore_all(std, config, args)
|
|
47
|
+
|
|
48
|
+
if args.file is None:
|
|
49
|
+
print("Error: specify an archive file, or use --all", file=sys.stderr)
|
|
50
|
+
return 1
|
|
51
|
+
|
|
52
|
+
return _restore_one(std, config, project_dir=args.path,
|
|
53
|
+
archive_file=Path(args.file), force=args.force)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _restore_one(std, config, *, project_dir, archive_file, force) -> int:
|
|
57
|
+
"""Extract session data from a single archive."""
|
|
58
|
+
if not archive_file.is_file():
|
|
59
|
+
print(f"Error: Archive file not found: {archive_file}", file=sys.stderr)
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
proj = resolve_any_project(
|
|
63
|
+
std, config,
|
|
64
|
+
project_dir=str(project_dir) if project_dir else None,
|
|
65
|
+
initialize=False,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
temp_dir = tempfile.mkdtemp()
|
|
69
|
+
try:
|
|
70
|
+
try:
|
|
71
|
+
with tarfile.open(str(archive_file), "r:xz") as tar:
|
|
72
|
+
tar.extractall(temp_dir, filter="data")
|
|
73
|
+
except (tarfile.TarError, OSError) as e:
|
|
74
|
+
print(f"Error: Failed to extract archive: {e}", file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
# Find the archive hash directory
|
|
78
|
+
entries = list(Path(temp_dir).iterdir())
|
|
79
|
+
if not entries:
|
|
80
|
+
print("Error: Empty archive.", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
archive_hash_dir = entries[0]
|
|
83
|
+
archive_hash = archive_hash_dir.name
|
|
84
|
+
info_file = archive_hash_dir / "kanibako-archive-info.txt"
|
|
85
|
+
|
|
86
|
+
if not info_file.is_file():
|
|
87
|
+
print(
|
|
88
|
+
"Error: Invalid archive format (missing kanibako-archive-info.txt)",
|
|
89
|
+
file=sys.stderr,
|
|
90
|
+
)
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
# Parse metadata
|
|
94
|
+
info = _parse_info(info_file)
|
|
95
|
+
archive_path = info.get("Project path", "")
|
|
96
|
+
archive_basename = Path(archive_path).name if archive_path else ""
|
|
97
|
+
current_basename = proj.project_path.name
|
|
98
|
+
|
|
99
|
+
# Validate hash match
|
|
100
|
+
hash_match = (
|
|
101
|
+
archive_hash == proj.project_hash
|
|
102
|
+
or archive_basename == current_basename
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not hash_match and not force:
|
|
106
|
+
print("Warning: Project path mismatch")
|
|
107
|
+
print()
|
|
108
|
+
print(f"Archive from: {archive_path}")
|
|
109
|
+
print(f"Restoring to: {proj.project_path}")
|
|
110
|
+
print()
|
|
111
|
+
try:
|
|
112
|
+
confirm_prompt("Continue anyway? Type 'yes' to confirm: ")
|
|
113
|
+
except UserCancelled:
|
|
114
|
+
print("Aborted.")
|
|
115
|
+
return 2
|
|
116
|
+
|
|
117
|
+
# Validate git state
|
|
118
|
+
git_in_archive = info.get("Git repository", "") == "yes"
|
|
119
|
+
if git_in_archive:
|
|
120
|
+
rc = _validate_git_state(proj, info, force)
|
|
121
|
+
if rc != 0:
|
|
122
|
+
return rc
|
|
123
|
+
|
|
124
|
+
# Restore session data
|
|
125
|
+
print("Restoring session data... ", end="", flush=True)
|
|
126
|
+
projects_base = std.boxes
|
|
127
|
+
projects_base.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
if proj.metadata_path.exists():
|
|
130
|
+
shutil.rmtree(proj.metadata_path)
|
|
131
|
+
|
|
132
|
+
shutil.copytree(str(archive_hash_dir), str(proj.metadata_path))
|
|
133
|
+
|
|
134
|
+
# Remove info file from restored data
|
|
135
|
+
restored_info = proj.metadata_path / "kanibako-archive-info.txt"
|
|
136
|
+
restored_info.unlink(missing_ok=True)
|
|
137
|
+
|
|
138
|
+
print("done.")
|
|
139
|
+
print(f"Session data restored to {proj.project_path}")
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
finally:
|
|
143
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _peek_archive_info(archive_file: Path) -> dict[str, str] | None:
|
|
147
|
+
"""Extract archive to a temp dir and parse the info file."""
|
|
148
|
+
temp_dir = tempfile.mkdtemp()
|
|
149
|
+
try:
|
|
150
|
+
try:
|
|
151
|
+
with tarfile.open(str(archive_file), "r:xz") as tar:
|
|
152
|
+
tar.extractall(temp_dir, filter="data")
|
|
153
|
+
except (tarfile.TarError, OSError):
|
|
154
|
+
return None
|
|
155
|
+
entries = list(Path(temp_dir).iterdir())
|
|
156
|
+
if not entries:
|
|
157
|
+
return None
|
|
158
|
+
info_file = entries[0] / "kanibako-archive-info.txt"
|
|
159
|
+
if not info_file.is_file():
|
|
160
|
+
return None
|
|
161
|
+
info = _parse_info(info_file)
|
|
162
|
+
info["_archive_hash"] = entries[0].name
|
|
163
|
+
return info
|
|
164
|
+
finally:
|
|
165
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _restore_all(std, config, args) -> int:
|
|
169
|
+
"""Restore all kanibako-*.txz archives in the current directory."""
|
|
170
|
+
import os
|
|
171
|
+
|
|
172
|
+
scan_dir = Path(os.getcwd())
|
|
173
|
+
archives = sorted(scan_dir.glob("kanibako-*.txz"))
|
|
174
|
+
if not archives:
|
|
175
|
+
print(f"No kanibako-*.txz archives found in {scan_dir}")
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
# Peek into each archive to get project path
|
|
179
|
+
plan: list[tuple[Path, str]] = []
|
|
180
|
+
for archive in archives:
|
|
181
|
+
info = _peek_archive_info(archive)
|
|
182
|
+
if info is None:
|
|
183
|
+
print(f" Skipping {archive.name} (invalid archive)", file=sys.stderr)
|
|
184
|
+
continue
|
|
185
|
+
project_path = info.get("Project path", "")
|
|
186
|
+
if not project_path:
|
|
187
|
+
print(f" Skipping {archive.name} (no project path in metadata)", file=sys.stderr)
|
|
188
|
+
continue
|
|
189
|
+
plan.append((archive, project_path))
|
|
190
|
+
|
|
191
|
+
if not plan:
|
|
192
|
+
print("No valid archives found to restore.")
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
print(f"Found {len(plan)} archive(s) to restore:")
|
|
196
|
+
for archive, project_path in plan:
|
|
197
|
+
print(f" {archive.name} → {project_path}")
|
|
198
|
+
print()
|
|
199
|
+
|
|
200
|
+
if not args.force:
|
|
201
|
+
try:
|
|
202
|
+
confirm_prompt(
|
|
203
|
+
"Restore all listed archives? Existing session data will be overwritten.\n"
|
|
204
|
+
"Type 'yes' to confirm: "
|
|
205
|
+
)
|
|
206
|
+
except UserCancelled:
|
|
207
|
+
print("Aborted.")
|
|
208
|
+
return 2
|
|
209
|
+
|
|
210
|
+
restored = 0
|
|
211
|
+
failed = 0
|
|
212
|
+
for archive, project_path in plan:
|
|
213
|
+
print(f"\n--- {archive.name} → {project_path}")
|
|
214
|
+
rc = _restore_one(
|
|
215
|
+
std, config, project_dir=project_path,
|
|
216
|
+
archive_file=archive, force=True,
|
|
217
|
+
)
|
|
218
|
+
if rc == 0:
|
|
219
|
+
restored += 1
|
|
220
|
+
else:
|
|
221
|
+
failed += 1
|
|
222
|
+
|
|
223
|
+
print(f"\nRestored {restored} archive(s).", end="")
|
|
224
|
+
if failed:
|
|
225
|
+
print(f" {failed} failed.", end="")
|
|
226
|
+
print()
|
|
227
|
+
return 1 if failed else 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _parse_info(info_file: Path) -> dict[str, str]:
|
|
231
|
+
"""Parse kanibako-archive-info.txt into a dict."""
|
|
232
|
+
result: dict[str, str] = {}
|
|
233
|
+
for line in info_file.read_text().splitlines():
|
|
234
|
+
if ": " in line and not line.startswith(" "):
|
|
235
|
+
key, _, value = line.partition(": ")
|
|
236
|
+
result[key.strip()] = value.strip()
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _validate_git_state(proj, info: dict[str, str], force: bool) -> int:
|
|
241
|
+
"""Validate git state between archive and workspace. Returns 0 to continue."""
|
|
242
|
+
if not is_git_repo(proj.project_path):
|
|
243
|
+
if not force:
|
|
244
|
+
print(
|
|
245
|
+
"Warning: Archive came from a git repository, "
|
|
246
|
+
"but current workspace is not a git repo."
|
|
247
|
+
)
|
|
248
|
+
print()
|
|
249
|
+
for key in ("Branch", "Commit"):
|
|
250
|
+
if key in info:
|
|
251
|
+
print(f" {key}: {info[key]}")
|
|
252
|
+
print()
|
|
253
|
+
try:
|
|
254
|
+
confirm_prompt("Continue anyway? Type 'yes' to confirm: ")
|
|
255
|
+
except UserCancelled:
|
|
256
|
+
print("Aborted.")
|
|
257
|
+
return 2
|
|
258
|
+
return 0
|
|
259
|
+
|
|
260
|
+
archive_commit = info.get("Commit", "")
|
|
261
|
+
result = subprocess.run(
|
|
262
|
+
["git", "rev-parse", "HEAD"],
|
|
263
|
+
cwd=proj.project_path,
|
|
264
|
+
capture_output=True,
|
|
265
|
+
text=True,
|
|
266
|
+
)
|
|
267
|
+
current_commit = result.stdout.strip() if result.returncode == 0 else ""
|
|
268
|
+
|
|
269
|
+
if archive_commit != current_commit and not force:
|
|
270
|
+
print("Warning: Git state mismatch")
|
|
271
|
+
print()
|
|
272
|
+
print("Archive from:")
|
|
273
|
+
for key in ("Branch", "Commit"):
|
|
274
|
+
if key in info:
|
|
275
|
+
print(f" {key}: {info[key]}")
|
|
276
|
+
print()
|
|
277
|
+
print("Current workspace:")
|
|
278
|
+
branch_result = subprocess.run(
|
|
279
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
280
|
+
cwd=proj.project_path,
|
|
281
|
+
capture_output=True,
|
|
282
|
+
text=True,
|
|
283
|
+
)
|
|
284
|
+
current_branch = (
|
|
285
|
+
branch_result.stdout.strip()
|
|
286
|
+
if branch_result.returncode == 0
|
|
287
|
+
else "unknown"
|
|
288
|
+
)
|
|
289
|
+
print(f" Branch: {current_branch}")
|
|
290
|
+
print(f" Commit: {current_commit}")
|
|
291
|
+
print()
|
|
292
|
+
try:
|
|
293
|
+
confirm_prompt("Continue anyway? Type 'yes' to confirm: ")
|
|
294
|
+
except UserCancelled:
|
|
295
|
+
print("Aborted.")
|
|
296
|
+
return 2
|
|
297
|
+
|
|
298
|
+
return 0
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""kanibako setup: interactive setup wizard for first-time configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_setup(args: argparse.Namespace) -> int:
|
|
9
|
+
"""Run the interactive setup wizard."""
|
|
10
|
+
print()
|
|
11
|
+
print("Kanibako Setup")
|
|
12
|
+
print("=" * 40)
|
|
13
|
+
print()
|
|
14
|
+
|
|
15
|
+
# Step 1: Container runtime
|
|
16
|
+
print("Step 1: Container Runtime")
|
|
17
|
+
from kanibako.commands.diagnose import _check_runtime
|
|
18
|
+
|
|
19
|
+
status, detail = _check_runtime()
|
|
20
|
+
if status == "ok":
|
|
21
|
+
print(f" [ok] {detail}")
|
|
22
|
+
else:
|
|
23
|
+
print(" [!!] No container runtime found.")
|
|
24
|
+
print(" Install podman (https://podman.io/) or Docker.")
|
|
25
|
+
print()
|
|
26
|
+
return 1
|
|
27
|
+
print()
|
|
28
|
+
|
|
29
|
+
# Step 2: Detect agents
|
|
30
|
+
print("Step 2: Agent Detection")
|
|
31
|
+
from kanibako.targets import discover_targets
|
|
32
|
+
|
|
33
|
+
targets = discover_targets()
|
|
34
|
+
found_any = False
|
|
35
|
+
for name, cls in targets.items():
|
|
36
|
+
try:
|
|
37
|
+
instance = cls()
|
|
38
|
+
install = instance.detect()
|
|
39
|
+
if install is not None:
|
|
40
|
+
print(f" [ok] {instance.display_name} detected")
|
|
41
|
+
found_any = True
|
|
42
|
+
else:
|
|
43
|
+
print(f" [--] {instance.display_name} not found on this system")
|
|
44
|
+
except Exception:
|
|
45
|
+
print(f" [--] {name}: error during detection")
|
|
46
|
+
|
|
47
|
+
if not targets:
|
|
48
|
+
print(" [!!] No agent plugins installed.")
|
|
49
|
+
print(" Install one: pip install kanibako-agent-claude")
|
|
50
|
+
elif not found_any:
|
|
51
|
+
print()
|
|
52
|
+
print(" No agents detected on this system.")
|
|
53
|
+
print(" Install an agent (e.g., Claude Code) and try again.")
|
|
54
|
+
print()
|
|
55
|
+
|
|
56
|
+
# Step 3: Default image
|
|
57
|
+
print("Step 3: Container Rig")
|
|
58
|
+
from kanibako.commands.diagnose import _check_image
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
from kanibako.config import config_file_path, load_merged_config
|
|
62
|
+
from kanibako.paths import xdg
|
|
63
|
+
|
|
64
|
+
config_home = xdg("XDG_CONFIG_HOME", ".config")
|
|
65
|
+
cf = config_file_path(config_home)
|
|
66
|
+
merged = load_merged_config(cf, None)
|
|
67
|
+
status, detail = _check_image(merged)
|
|
68
|
+
if status == "ok":
|
|
69
|
+
print(f" [ok] {detail}")
|
|
70
|
+
else:
|
|
71
|
+
print(f" [--] {detail}")
|
|
72
|
+
print(" The rig will be pulled automatically on first use.")
|
|
73
|
+
except Exception:
|
|
74
|
+
print(" [--] Cannot check (configuration not initialized yet)")
|
|
75
|
+
print(" Rigs will be pulled automatically on first use.")
|
|
76
|
+
print()
|
|
77
|
+
|
|
78
|
+
# Summary
|
|
79
|
+
print("Setup Complete")
|
|
80
|
+
print("-" * 40)
|
|
81
|
+
if found_any:
|
|
82
|
+
print(" You're ready to go! Run `kanibako` in any project directory.")
|
|
83
|
+
else:
|
|
84
|
+
print(" Install an agent plugin and its host binary, then run `kanibako`.")
|
|
85
|
+
print()
|
|
86
|
+
print(" For a full health check: kanibako system diagnose")
|
|
87
|
+
print()
|
|
88
|
+
|
|
89
|
+
return 0
|