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,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
|