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.
Files changed (85) hide show
  1. kanibako/__init__.py +3 -0
  2. kanibako/__main__.py +6 -0
  3. kanibako/auth_browser.py +296 -0
  4. kanibako/auth_parser.py +51 -0
  5. kanibako/browser_sidecar.py +183 -0
  6. kanibako/browser_state.py +103 -0
  7. kanibako/bun_sea.py +144 -0
  8. kanibako/cli.py +344 -0
  9. kanibako/commands/__init__.py +0 -0
  10. kanibako/commands/archive.py +228 -0
  11. kanibako/commands/box/__init__.py +22 -0
  12. kanibako/commands/box/_duplicate.py +395 -0
  13. kanibako/commands/box/_migrate.py +574 -0
  14. kanibako/commands/box/_parser.py +1178 -0
  15. kanibako/commands/clean.py +166 -0
  16. kanibako/commands/crab_cmd.py +480 -0
  17. kanibako/commands/diagnose.py +239 -0
  18. kanibako/commands/fork_cmd.py +51 -0
  19. kanibako/commands/helper_cmd.py +669 -0
  20. kanibako/commands/image.py +1300 -0
  21. kanibako/commands/install.py +152 -0
  22. kanibako/commands/refresh_credentials.py +67 -0
  23. kanibako/commands/restore.py +298 -0
  24. kanibako/commands/setup_cmd.py +89 -0
  25. kanibako/commands/start.py +1600 -0
  26. kanibako/commands/stop.py +116 -0
  27. kanibako/commands/system_cmd.py +224 -0
  28. kanibako/commands/upgrade.py +161 -0
  29. kanibako/commands/vault_cmd.py +199 -0
  30. kanibako/commands/workset_cmd.py +552 -0
  31. kanibako/config.py +514 -0
  32. kanibako/config_interface.py +573 -0
  33. kanibako/config_io.py +36 -0
  34. kanibako/container.py +607 -0
  35. kanibako/containerfiles.py +58 -0
  36. kanibako/containers/Containerfile.kanibako +99 -0
  37. kanibako/containers/Containerfile.template-android +55 -0
  38. kanibako/containers/Containerfile.template-dotnet +29 -0
  39. kanibako/containers/Containerfile.template-js +43 -0
  40. kanibako/containers/Containerfile.template-jvm +27 -0
  41. kanibako/containers/Containerfile.template-systems +46 -0
  42. kanibako/containers/__init__.py +0 -0
  43. kanibako/crabs.py +89 -0
  44. kanibako/errors.py +33 -0
  45. kanibako/freshness.py +67 -0
  46. kanibako/git.py +114 -0
  47. kanibako/helper_client.py +132 -0
  48. kanibako/helper_listener.py +538 -0
  49. kanibako/helpers.py +339 -0
  50. kanibako/hygiene.py +296 -0
  51. kanibako/image_sharing.py +133 -0
  52. kanibako/instructions.py +160 -0
  53. kanibako/log.py +31 -0
  54. kanibako/names.py +248 -0
  55. kanibako/paths.py +1483 -0
  56. kanibako/plugins/__init__.py +10 -0
  57. kanibako/registry.py +71 -0
  58. kanibako/rig_bundle.py +121 -0
  59. kanibako/rig_meta.py +92 -0
  60. kanibako/rig_registry.py +132 -0
  61. kanibako/rig_resolve.py +182 -0
  62. kanibako/rig_source.py +245 -0
  63. kanibako/scripts/__init__.py +0 -0
  64. kanibako/scripts/helper-init.sh +45 -0
  65. kanibako/scripts/kanibako-entry +12 -0
  66. kanibako/settings_resolve.py +312 -0
  67. kanibako/settings_seeds.py +154 -0
  68. kanibako/settings_shares.py +154 -0
  69. kanibako/shellenv.py +75 -0
  70. kanibako/snapshots.py +281 -0
  71. kanibako/targets/__init__.py +173 -0
  72. kanibako/targets/base.py +243 -0
  73. kanibako/targets/no_agent.py +58 -0
  74. kanibako/templates.py +60 -0
  75. kanibako/templates_image.py +224 -0
  76. kanibako/tweakcc.py +140 -0
  77. kanibako/tweakcc_cache.py +171 -0
  78. kanibako/utils.py +136 -0
  79. kanibako/workset.py +347 -0
  80. kanibako_cli-1.5.0.dev14.dist-info/METADATA +15 -0
  81. kanibako_cli-1.5.0.dev14.dist-info/RECORD +85 -0
  82. kanibako_cli-1.5.0.dev14.dist-info/WHEEL +5 -0
  83. kanibako_cli-1.5.0.dev14.dist-info/entry_points.txt +5 -0
  84. kanibako_cli-1.5.0.dev14.dist-info/licenses/LICENSE.md +594 -0
  85. 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