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,166 @@
1
+ """kanibako purge: remove project session data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import shutil
7
+ import sys
8
+
9
+ from kanibako.config import load_config
10
+ from kanibako.errors import UserCancelled
11
+ from kanibako.paths import (
12
+ _remove_human_vault_symlink,
13
+ _remove_project_vault_symlink,
14
+ load_std_paths,
15
+ resolve_any_project,
16
+ )
17
+ from kanibako.utils import confirm_prompt
18
+
19
+
20
+ def add_parser(subparsers: argparse._SubParsersAction) -> None:
21
+ p = subparsers.add_parser(
22
+ "purge",
23
+ help="Remove all project session data",
24
+ description="Remove all project session data (credentials, conversation history).",
25
+ )
26
+ p.add_argument("path", nargs="?", default=None, help="Path to the project directory")
27
+ p.add_argument(
28
+ "--all", action="store_true", dest="all_projects",
29
+ help="Purge session data for every known project",
30
+ )
31
+ p.add_argument(
32
+ "--force", action="store_true", help="Skip confirmation prompt"
33
+ )
34
+ p.set_defaults(func=run)
35
+
36
+
37
+ def run(args: argparse.Namespace) -> int:
38
+ from kanibako.paths import xdg
39
+ from kanibako.config import config_file_path
40
+ config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
41
+ config = load_config(config_file)
42
+ std = load_std_paths(config)
43
+
44
+ if args.all_projects:
45
+ return _purge_all(std, config, force=args.force)
46
+
47
+ if args.path is None:
48
+ print("Error: specify a project path, or use --all", file=sys.stderr)
49
+ return 1
50
+
51
+ return _purge_one(std, config, args.path, force=args.force)
52
+
53
+
54
+ def _purge_one(std, config, path: str, *, force: bool) -> int:
55
+ """Purge session data for a single project."""
56
+ proj = resolve_any_project(std, config, project_dir=path, initialize=False)
57
+
58
+ if not proj.metadata_path.is_dir():
59
+ print(f"No session data found for project {proj.project_path}")
60
+ return 0
61
+
62
+ if not force:
63
+ print(f"Project: {proj.project_path}")
64
+ if proj.name:
65
+ print(f"Name: {proj.name}")
66
+ print()
67
+ try:
68
+ confirm_prompt(
69
+ "Delete all session data for this project? This cannot be undone.\n"
70
+ "Type 'yes' to confirm: "
71
+ )
72
+ except UserCancelled:
73
+ print("Aborted.")
74
+ return 2
75
+
76
+ # Clean up vault symlinks before removing data.
77
+ vault_dir = std.data_path / config.paths_vault
78
+ _remove_human_vault_symlink(vault_dir, proj.metadata_path / "vault")
79
+ _remove_project_vault_symlink(proj.project_path)
80
+
81
+ print("Removing session data... ", end="", flush=True)
82
+ shutil.rmtree(proj.metadata_path)
83
+
84
+ # Remove helper log directory if it exists.
85
+ _log_id = proj.name if proj.name else proj.metadata_path.name
86
+ log_dir = std.data_path / "logs" / _log_id
87
+ if log_dir.is_dir():
88
+ shutil.rmtree(log_dir)
89
+
90
+ print("done.")
91
+ print(f"Session data removed for {proj.project_path}")
92
+ return 0
93
+
94
+
95
+ def _purge_all(std, config, *, force: bool) -> int:
96
+ """Purge session data for all known projects."""
97
+ from kanibako.paths import iter_projects, iter_workset_projects
98
+
99
+ projects = iter_projects(std, config)
100
+ ws_data = iter_workset_projects(std, config)
101
+
102
+ if not projects and not ws_data:
103
+ print("No project session data found.")
104
+ return 0
105
+
106
+ total = len(projects)
107
+ for _, _, project_list in ws_data:
108
+ total += sum(1 for _, status in project_list if status != "no-data")
109
+
110
+ print(f"Found {total} project(s):")
111
+ for metadata_path, project_path in projects:
112
+ label = str(project_path) if project_path else f"(unknown) {metadata_path.name}"
113
+ print(f" {label}")
114
+ for ws_name, ws, project_list in ws_data:
115
+ for proj_name, status in project_list:
116
+ if status != "no-data":
117
+ print(f" {ws_name}/{proj_name}")
118
+ print()
119
+
120
+ if not force:
121
+ try:
122
+ confirm_prompt(
123
+ "Delete ALL session data for every project listed above? "
124
+ "This cannot be undone.\n"
125
+ "Type 'yes' to confirm: "
126
+ )
127
+ except UserCancelled:
128
+ print("Aborted.")
129
+ return 2
130
+
131
+ removed = 0
132
+
133
+ # Default-mode projects.
134
+ vault_dir = std.data_path / config.paths_vault
135
+ for metadata_path, project_path in projects:
136
+ # Clean up vault symlinks before removing data.
137
+ _remove_human_vault_symlink(vault_dir, metadata_path / "vault")
138
+ if project_path is not None:
139
+ _remove_project_vault_symlink(project_path)
140
+ label = str(project_path) if project_path else metadata_path.name
141
+ print(f"Removing {label}... ", end="", flush=True)
142
+ shutil.rmtree(metadata_path)
143
+
144
+ # Remove helper log directory if it exists.
145
+ log_dir = std.data_path / "logs" / metadata_path.name
146
+ if log_dir.is_dir():
147
+ shutil.rmtree(log_dir)
148
+
149
+ print("done.")
150
+ removed += 1
151
+
152
+ # Workset projects.
153
+ for ws_name, ws, project_list in ws_data:
154
+ for proj_name, status in project_list:
155
+ if status == "no-data":
156
+ continue
157
+ project_dir = ws.projects_dir / proj_name
158
+ if project_dir.is_dir():
159
+ label = f"{ws_name}/{proj_name}"
160
+ print(f"Removing {label}... ", end="", flush=True)
161
+ shutil.rmtree(project_dir)
162
+ print("done.")
163
+ removed += 1
164
+
165
+ print(f"\nPurged session data for {removed} project(s).")
166
+ return 0
@@ -0,0 +1,480 @@
1
+ """kanibako crab: crab management, authentication, and coordination."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from kanibako.crabs import CrabConfig
11
+ from kanibako.paths import StandardPaths
12
+
13
+
14
+ def add_parser(subparsers: argparse._SubParsersAction) -> None:
15
+ p = subparsers.add_parser(
16
+ "crab",
17
+ help="Crab (agent) management, authentication, and settings",
18
+ description="Manage crab configurations, authentication, and helper instances.",
19
+ )
20
+ crab_sub = p.add_subparsers(dest="crab_command", metavar="COMMAND")
21
+
22
+ # crab list (default)
23
+ list_p = crab_sub.add_parser(
24
+ "list",
25
+ aliases=["ls"],
26
+ help="List configured crabs",
27
+ )
28
+ list_p.add_argument("-q", "--quiet", action="store_true", help="Names only")
29
+ list_p.set_defaults(func=run_list)
30
+
31
+ # crab info <crab>
32
+ info_p = crab_sub.add_parser(
33
+ "info",
34
+ aliases=["inspect"],
35
+ help="Show crab configuration details",
36
+ )
37
+ info_p.add_argument("crab_id", help="Crab identifier")
38
+ info_p.set_defaults(func=run_info)
39
+
40
+ # crab config <crab> [key[=value]] [--effective] [--reset] [--all] [--force]
41
+ config_p = crab_sub.add_parser(
42
+ "config",
43
+ help="View or modify crab configuration",
44
+ description=(
45
+ "Unified config interface for crab settings.\n\n"
46
+ " crab config mycrab show all settings\n"
47
+ " crab config mycrab model get the value of 'model'\n"
48
+ " crab config mycrab model=sonnet set 'model' to 'sonnet'\n"
49
+ " crab config mycrab env.FOO=bar set env var FOO\n"
50
+ " crab config mycrab --reset model reset one key\n"
51
+ " crab config mycrab --reset --all reset all overrides\n"
52
+ ),
53
+ formatter_class=argparse.RawDescriptionHelpFormatter,
54
+ )
55
+ config_p.add_argument("crab_id", help="Crab identifier")
56
+ config_p.add_argument(
57
+ "key_value", nargs="?", default=None,
58
+ help="Config key or key=value pair",
59
+ )
60
+ config_p.add_argument(
61
+ "--effective", action="store_true",
62
+ help="Show resolved values including defaults",
63
+ )
64
+ config_p.add_argument(
65
+ "--reset", nargs="?", const="__RESET__", default=None,
66
+ help="Remove override for the given key",
67
+ )
68
+ config_p.add_argument(
69
+ "--all", action="store_true", dest="all_keys",
70
+ help="Reset all overrides (only valid with --reset)",
71
+ )
72
+ config_p.add_argument(
73
+ "--force", action="store_true",
74
+ help="Skip confirmation prompts",
75
+ )
76
+ config_p.set_defaults(func=run_config)
77
+
78
+ # agent reauth [project]
79
+ reauth_p = crab_sub.add_parser(
80
+ "reauth",
81
+ help="Check authentication and login if needed",
82
+ description=(
83
+ "Verify agent authentication status and run interactive "
84
+ "login if credentials are expired or missing."
85
+ ),
86
+ )
87
+ reauth_p.add_argument(
88
+ "project", nargs="?", default=None,
89
+ help="Target project directory or name",
90
+ )
91
+ reauth_p.set_defaults(func=run_reauth)
92
+
93
+ # crab helper -- delegate to helper_cmd
94
+ from kanibako.commands.helper_cmd import add_helper_subparsers
95
+
96
+ helper_p = crab_sub.add_parser(
97
+ "helper",
98
+ help="Manage helper instances",
99
+ description="Spawn, list, stop, cleanup, and respawn helper instances.",
100
+ )
101
+ add_helper_subparsers(helper_p)
102
+
103
+ # crab fork <name> -- delegate to fork_cmd
104
+ from kanibako.commands.fork_cmd import run_fork
105
+
106
+ fork_p = crab_sub.add_parser(
107
+ "fork",
108
+ help="Fork this project into a new directory",
109
+ description=(
110
+ "Fork the current project into a sibling directory. "
111
+ "The fork is a full copy of the workspace and metadata, "
112
+ "assigned a new project name."
113
+ ),
114
+ )
115
+ fork_p.add_argument(
116
+ "name",
117
+ help="Fork name (appended with dot to workspace path)",
118
+ )
119
+ fork_p.set_defaults(func=run_fork, command="crab")
120
+
121
+ # crab diagnose
122
+ from kanibako.commands.diagnose import run_crab_diagnose
123
+
124
+ diagnose_p = crab_sub.add_parser(
125
+ "diagnose",
126
+ help="Check crab status and configuration",
127
+ )
128
+ diagnose_p.set_defaults(func=run_crab_diagnose)
129
+
130
+ # Default to list if no subcommand given.
131
+ p.set_defaults(func=run_list, quiet=False)
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Crab list / info / config + agent reauth handlers
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ def _load_std() -> StandardPaths:
140
+ """Load config and return the resolved standard paths."""
141
+ from kanibako.config import config_file_path, load_config
142
+ from kanibako.paths import xdg, load_std_paths
143
+
144
+ config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
145
+ config = load_config(config_file)
146
+ return load_std_paths(config)
147
+
148
+
149
+ def run_list(args: argparse.Namespace) -> int:
150
+ """List configured crabs."""
151
+ from kanibako.crabs import load_crab_config
152
+
153
+ try:
154
+ std = _load_std()
155
+ except Exception as e:
156
+ print(f"Error: {e}", file=sys.stderr)
157
+ return 1
158
+
159
+ adir = std.crabs
160
+ if not adir.is_dir():
161
+ quiet = getattr(args, "quiet", False)
162
+ if not quiet:
163
+ print("No crabs configured.")
164
+ return 0
165
+
166
+ toml_files = sorted(adir.glob("*.yaml"))
167
+ if not toml_files:
168
+ quiet = getattr(args, "quiet", False)
169
+ if not quiet:
170
+ print("No crabs configured.")
171
+ return 0
172
+
173
+ quiet = getattr(args, "quiet", False)
174
+ if quiet:
175
+ for f in toml_files:
176
+ print(f.stem)
177
+ return 0
178
+
179
+ print(f"{'NAME':<20} {'SHELL':<12} {'MODEL'}")
180
+ for f in toml_files:
181
+ cfg = load_crab_config(f)
182
+ name = f.stem
183
+ shell = cfg.shell or "standard"
184
+ model = cfg.state.get("model", "-")
185
+ print(f"{name:<20} {shell:<12} {model}")
186
+ return 0
187
+
188
+
189
+ def run_info(args: argparse.Namespace) -> int:
190
+ """Show crab configuration details."""
191
+ from kanibako.crabs import load_crab_config
192
+
193
+ try:
194
+ std = _load_std()
195
+ except Exception as e:
196
+ print(f"Error: {e}", file=sys.stderr)
197
+ return 1
198
+
199
+ crab_id = args.crab_id
200
+ path = std.crabs / f"{crab_id}.yaml"
201
+ if not path.exists():
202
+ print(f"Error: crab '{crab_id}' not found ({path})", file=sys.stderr)
203
+ return 1
204
+
205
+ cfg = load_crab_config(path)
206
+ print(f"Name: {cfg.name or crab_id}")
207
+ print(f"Shell: {cfg.shell}")
208
+ if cfg.run_args:
209
+ print(f"Default args: {' '.join(cfg.run_args)}")
210
+ else:
211
+ print("Default args: (none)")
212
+
213
+ if cfg.state:
214
+ print("State:")
215
+ for k, v in sorted(cfg.state.items()):
216
+ print(f" {k} = {v}")
217
+ else:
218
+ print("State: (none)")
219
+
220
+ if cfg.env:
221
+ print("Env:")
222
+ for k, v in sorted(cfg.env.items()):
223
+ print(f" {k} = {v}")
224
+ else:
225
+ print("Env: (none)")
226
+
227
+ if cfg.shared_caches:
228
+ print("Shared:")
229
+ for k, v in sorted(cfg.shared_caches.items()):
230
+ print(f" {k} = {v}")
231
+ else:
232
+ print("Shared: (none)")
233
+
234
+ return 0
235
+
236
+
237
+ def run_config(args: argparse.Namespace) -> int:
238
+ """View or modify crab configuration.
239
+
240
+ Maps config keys to crab TOML sections:
241
+ model, start_mode, etc. -> [crab] (state keys)
242
+ env.X -> [env]
243
+ shared.X -> [shared]
244
+ shell, run_args, name -> [crab] (identity keys)
245
+ """
246
+ from kanibako.crabs import load_crab_config, write_crab_config
247
+
248
+ try:
249
+ std = _load_std()
250
+ except Exception as e:
251
+ print(f"Error: {e}", file=sys.stderr)
252
+ return 1
253
+
254
+ crab_id = args.crab_id
255
+ path = std.crabs / f"{crab_id}.yaml"
256
+ if not path.exists():
257
+ print(f"Error: crab '{crab_id}' not found ({path})", file=sys.stderr)
258
+ return 1
259
+
260
+ cfg = load_crab_config(path)
261
+ key_value = getattr(args, "key_value", None)
262
+
263
+ # Handle --reset
264
+ if args.reset is not None:
265
+ if args.all_keys:
266
+ if not args.force:
267
+ from kanibako.utils import confirm_prompt
268
+ from kanibako.errors import UserCancelled
269
+
270
+ try:
271
+ confirm_prompt(
272
+ "Reset all crab config overrides? Type 'yes' to proceed: "
273
+ )
274
+ except UserCancelled:
275
+ print("Aborted.")
276
+ return 0
277
+ # Reset to defaults
278
+ cfg.state.clear()
279
+ cfg.env.clear()
280
+ cfg.shared_caches.clear()
281
+ cfg.run_args.clear()
282
+ write_crab_config(path, cfg)
283
+ print("Reset all crab config overrides.")
284
+ return 0
285
+
286
+ # Key can come from --reset VALUE or from positional key_value.
287
+ reset_key = args.reset if args.reset != "__RESET__" else key_value
288
+ if not reset_key:
289
+ print("Error: --reset requires a key name (or --all)", file=sys.stderr)
290
+ return 1
291
+
292
+ key = reset_key.strip()
293
+ changed = _reset_crab_key(cfg, key)
294
+ if changed:
295
+ write_crab_config(path, cfg)
296
+ print(f"Reset {key}")
297
+ else:
298
+ print(f"No override for {key}")
299
+ return 0
300
+
301
+ # Parse key/value argument
302
+ if key_value is None:
303
+ # Show mode
304
+ return _show_crab_config(cfg, args.crab_id, effective=args.effective)
305
+
306
+ if "=" in key_value:
307
+ key, _, value = key_value.partition("=")
308
+ key = key.strip()
309
+ value = value.strip()
310
+ _set_crab_key(cfg, key, value)
311
+ write_crab_config(path, cfg)
312
+ print(f"Set {key}={value}")
313
+ return 0
314
+
315
+ # Get mode
316
+ key = key_value.strip()
317
+ val = _get_crab_key(cfg, key)
318
+ if val is not None:
319
+ print(val)
320
+ else:
321
+ print("(not set)", file=sys.stderr)
322
+ return 0
323
+
324
+
325
+ def _get_crab_key(cfg: CrabConfig, key: str) -> str | None:
326
+ """Read a single key from crab config."""
327
+ if key.startswith("env."):
328
+ env_name = key[4:]
329
+ return cfg.env.get(env_name)
330
+ if key.startswith("shared."):
331
+ cache_name = key[7:]
332
+ return cfg.shared_caches.get(cache_name)
333
+ if key == "shell":
334
+ return cfg.shell
335
+ if key == "name":
336
+ return cfg.name or None
337
+ if key == "run_args":
338
+ return " ".join(cfg.run_args) if cfg.run_args else None
339
+ # Everything else goes to state
340
+ return cfg.state.get(key)
341
+
342
+
343
+ def _set_crab_key(cfg: CrabConfig, key: str, value: str) -> None:
344
+ """Set a single key in crab config."""
345
+ if key.startswith("env."):
346
+ env_name = key[4:]
347
+ cfg.env[env_name] = value
348
+ elif key.startswith("shared."):
349
+ cache_name = key[7:]
350
+ cfg.shared_caches[cache_name] = value
351
+ elif key == "shell":
352
+ cfg.shell = value
353
+ elif key == "name":
354
+ cfg.name = value
355
+ elif key == "run_args":
356
+ cfg.run_args = value.split()
357
+ else:
358
+ # State section (model, start_mode, autonomous, etc.)
359
+ cfg.state[key] = value
360
+
361
+
362
+ def _reset_crab_key(cfg: CrabConfig, key: str) -> bool:
363
+ """Remove a single key from crab config. Returns True if found."""
364
+ if key.startswith("env."):
365
+ env_name = key[4:]
366
+ if env_name in cfg.env:
367
+ del cfg.env[env_name]
368
+ return True
369
+ return False
370
+ if key.startswith("shared."):
371
+ cache_name = key[7:]
372
+ if cache_name in cfg.shared_caches:
373
+ del cfg.shared_caches[cache_name]
374
+ return True
375
+ return False
376
+ if key == "shell":
377
+ cfg.shell = "standard"
378
+ return True
379
+ if key == "name":
380
+ cfg.name = ""
381
+ return True
382
+ if key == "run_args":
383
+ if cfg.run_args:
384
+ cfg.run_args.clear()
385
+ return True
386
+ return False
387
+ if key in cfg.state:
388
+ del cfg.state[key]
389
+ return True
390
+ return False
391
+
392
+
393
+ def _show_crab_config(
394
+ cfg: CrabConfig, crab_id: str, *, effective: bool = False,
395
+ ) -> int:
396
+ """Display crab config."""
397
+ has_output = False
398
+
399
+ # [crab] section
400
+ print(f" name = {cfg.name or crab_id}")
401
+ print(f" shell = {cfg.shell}")
402
+ if cfg.run_args:
403
+ print(f" run_args = {cfg.run_args}")
404
+ has_output = True
405
+
406
+ # crab-state keys
407
+ if cfg.state:
408
+ for k, v in sorted(cfg.state.items()):
409
+ print(f" {k} = {v}")
410
+ has_output = True
411
+ elif effective:
412
+ print(" # (no state overrides)")
413
+
414
+ # [env] section
415
+ if cfg.env:
416
+ for k, v in sorted(cfg.env.items()):
417
+ print(f" env.{k} = {v}")
418
+ has_output = True
419
+
420
+ # [shared] section
421
+ if cfg.shared_caches:
422
+ for k, v in sorted(cfg.shared_caches.items()):
423
+ print(f" shared.{k} = {v}")
424
+ has_output = True
425
+
426
+ if not has_output:
427
+ print(" (no overrides)")
428
+
429
+ return 0
430
+
431
+
432
+ def run_reauth(args: argparse.Namespace) -> int:
433
+ """Check authentication and login if needed."""
434
+ from kanibako.config import config_file_path, load_config
435
+ from kanibako.paths import xdg, load_std_paths, resolve_any_project
436
+ from kanibako.targets import resolve_target
437
+
438
+ config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
439
+ config = load_config(config_file)
440
+
441
+ # Resolve project to check auth mode.
442
+ std = load_std_paths(config)
443
+ proj = resolve_any_project(std, config, getattr(args, "project", None))
444
+
445
+ try:
446
+ target = resolve_target(config.box_crab or None)
447
+ except KeyError as e:
448
+ print(f"Error: {e}", file=sys.stderr)
449
+ return 1
450
+
451
+ if not target.has_binary:
452
+ print("No agent target configured.", file=sys.stderr)
453
+ return 1
454
+
455
+ if not proj.group_auth:
456
+ # Check project's own credentials instead of host.
457
+ creds_path = target.credential_check_path(proj.shell_path)
458
+ if creds_path and creds_path.is_file():
459
+ print(
460
+ f"{target.display_name}: distinct auth (project credentials exist).",
461
+ file=sys.stderr,
462
+ )
463
+ return 0
464
+ else:
465
+ print(
466
+ f"{target.display_name}: distinct auth -- no credentials found. "
467
+ "Launch the container to authenticate.",
468
+ file=sys.stderr,
469
+ )
470
+ return 1
471
+
472
+ if target.check_auth():
473
+ # Sync refreshed credentials to the project shell directory
474
+ if proj.group_auth:
475
+ target.refresh_credentials(proj.shell_path)
476
+ print(f"{target.display_name}: authenticated.", file=sys.stderr)
477
+ return 0
478
+ else:
479
+ print(f"{target.display_name}: authentication failed.", file=sys.stderr)
480
+ return 1