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,573 @@
|
|
|
1
|
+
"""Unified config interface engine for all management commands.
|
|
2
|
+
|
|
3
|
+
Provides a reusable config subsystem that box/workset/agent/system commands
|
|
4
|
+
share. Handles get, set, show, and reset operations with a consistent
|
|
5
|
+
syntax:
|
|
6
|
+
|
|
7
|
+
- ``key=value`` → set
|
|
8
|
+
- ``key`` → get (if key is known)
|
|
9
|
+
- no args → show all overrides
|
|
10
|
+
- ``--effective`` → show resolved values
|
|
11
|
+
- ``--reset key`` → remove override
|
|
12
|
+
- ``--reset --all`` → remove all overrides (with confirmation)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from dataclasses import fields
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from kanibako.config import (
|
|
24
|
+
_DEFAULTS,
|
|
25
|
+
load_merged_config,
|
|
26
|
+
load_project_overrides,
|
|
27
|
+
read_crab_settings,
|
|
28
|
+
unset_project_config_key,
|
|
29
|
+
write_project_config_key,
|
|
30
|
+
)
|
|
31
|
+
from kanibako.config_io import dump_doc, load_doc
|
|
32
|
+
from kanibako.errors import UserCancelled
|
|
33
|
+
from kanibako.shellenv import (
|
|
34
|
+
merge_env,
|
|
35
|
+
read_env_file,
|
|
36
|
+
set_env_var,
|
|
37
|
+
unset_env_var,
|
|
38
|
+
write_env_file,
|
|
39
|
+
)
|
|
40
|
+
from kanibako.utils import confirm_prompt
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Key registry
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
class ConfigLevel(Enum):
|
|
48
|
+
"""Which scope a config operation targets."""
|
|
49
|
+
|
|
50
|
+
box = "box"
|
|
51
|
+
workset = "workset"
|
|
52
|
+
crab = "crab"
|
|
53
|
+
system = "system"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Keys recognized by the unified config interface.
|
|
57
|
+
# This set drives the "known-key heuristic": if a positional arg matches one
|
|
58
|
+
# of these, it's treated as a GET request rather than a project name.
|
|
59
|
+
KNOWN_CONFIG_KEYS: frozenset[str] = frozenset({
|
|
60
|
+
# Start mode / agent flags
|
|
61
|
+
"start_mode",
|
|
62
|
+
"autonomous",
|
|
63
|
+
"model",
|
|
64
|
+
"persistence",
|
|
65
|
+
# Box
|
|
66
|
+
"box.image",
|
|
67
|
+
"box.crab",
|
|
68
|
+
"box.share_images",
|
|
69
|
+
# Auth / project
|
|
70
|
+
"group_auth",
|
|
71
|
+
"layout",
|
|
72
|
+
"mode",
|
|
73
|
+
# Vault
|
|
74
|
+
"vault.enabled",
|
|
75
|
+
"vault.ro",
|
|
76
|
+
"vault.rw",
|
|
77
|
+
# System-level path settings (resolver-backed system.path.* tier)
|
|
78
|
+
"system.path.data",
|
|
79
|
+
"system.path.boxes",
|
|
80
|
+
"system.path.crabs",
|
|
81
|
+
"system.path.comms",
|
|
82
|
+
"system.path.templates",
|
|
83
|
+
"system.path.ws_hints",
|
|
84
|
+
# Box-level path settings (flat KanibakoConfig.paths_* fields)
|
|
85
|
+
"paths.shell",
|
|
86
|
+
"paths.vault",
|
|
87
|
+
"paths.shared",
|
|
88
|
+
# Helpers
|
|
89
|
+
"allow_helpers",
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
# Prefixes for dynamic keys (env vars, resources, shared caches).
|
|
93
|
+
DYNAMIC_PREFIXES: tuple[str, ...] = ("env.", "resource.", "shared.")
|
|
94
|
+
|
|
95
|
+
# Map friendly short names to canonical flat config keys.
|
|
96
|
+
_KEY_ALIASES: dict[str, str] = {
|
|
97
|
+
"image": "box.image",
|
|
98
|
+
"crab": "box.crab",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def is_known_key(arg: str) -> bool:
|
|
103
|
+
"""Return True if *arg* looks like a config key (not a project name)."""
|
|
104
|
+
if arg in KNOWN_CONFIG_KEYS or arg in _KEY_ALIASES:
|
|
105
|
+
return True
|
|
106
|
+
return any(arg.startswith(p) for p in DYNAMIC_PREFIXES)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Config action parsing
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
class ConfigAction(Enum):
|
|
114
|
+
"""What the user wants to do with config."""
|
|
115
|
+
|
|
116
|
+
get = "get"
|
|
117
|
+
set = "set"
|
|
118
|
+
show = "show"
|
|
119
|
+
reset = "reset"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_config_arg(arg: str | None) -> tuple[ConfigAction, str, str]:
|
|
123
|
+
"""Parse a single positional config argument.
|
|
124
|
+
|
|
125
|
+
Returns ``(action, key, value)``.
|
|
126
|
+
|
|
127
|
+
- ``"key=value"`` → ``(set, key, value)``
|
|
128
|
+
- ``"key"`` → ``(get, key, "")``
|
|
129
|
+
- ``None`` → ``(show, "", "")``
|
|
130
|
+
"""
|
|
131
|
+
if arg is None:
|
|
132
|
+
return (ConfigAction.show, "", "")
|
|
133
|
+
if "=" in arg:
|
|
134
|
+
key, _, value = arg.partition("=")
|
|
135
|
+
return (ConfigAction.set, key.strip(), value.strip())
|
|
136
|
+
return (ConfigAction.get, arg.strip(), "")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Canonical key resolution
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def _resolve_key(raw: str) -> str:
|
|
144
|
+
"""Map a user-supplied key name to the canonical form.
|
|
145
|
+
|
|
146
|
+
Accepts aliases (``image`` → ``box.image``), dot-notation
|
|
147
|
+
(``vault.enabled``), or the raw flat key. Returns the key unchanged
|
|
148
|
+
if no alias exists.
|
|
149
|
+
"""
|
|
150
|
+
if raw in _KEY_ALIASES:
|
|
151
|
+
return _KEY_ALIASES[raw]
|
|
152
|
+
return raw
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _is_env_key(key: str) -> bool:
|
|
156
|
+
return key.startswith("env.")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_resource_key(key: str) -> bool:
|
|
160
|
+
return key.startswith("resource.")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _is_shared_key(key: str) -> bool:
|
|
164
|
+
return key.startswith("shared.")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _is_crab_setting(key: str) -> bool:
|
|
168
|
+
"""Keys that belong in the crab section of project.yaml."""
|
|
169
|
+
return key in {"model", "start_mode", "autonomous"}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _is_system_path_key(key: str) -> bool:
|
|
173
|
+
"""Keys that belong in the nested ``[system.path]`` table (system-only)."""
|
|
174
|
+
return key.startswith("system.path.")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _dot_to_flat(key: str) -> str:
|
|
178
|
+
"""Convert ``vault.enabled`` to ``enable_vault``, etc."""
|
|
179
|
+
# For paths.* keys, convert to the flat KanibakoConfig field name.
|
|
180
|
+
if key.startswith("paths."):
|
|
181
|
+
return "paths_" + key[6:]
|
|
182
|
+
return key.replace(".", "_")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# Get / set / reset operations
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
def get_config_value(
|
|
190
|
+
key: str,
|
|
191
|
+
*,
|
|
192
|
+
global_config_path: Path,
|
|
193
|
+
project_toml: Path | None = None,
|
|
194
|
+
env_global: Path | None = None,
|
|
195
|
+
env_project: Path | None = None,
|
|
196
|
+
) -> str | None:
|
|
197
|
+
"""Read a single config value from the appropriate store.
|
|
198
|
+
|
|
199
|
+
Returns the resolved (merged) value as a string, or None if the key
|
|
200
|
+
is not set.
|
|
201
|
+
"""
|
|
202
|
+
canonical = _resolve_key(key)
|
|
203
|
+
|
|
204
|
+
# env.* keys — read from env files
|
|
205
|
+
if _is_env_key(canonical):
|
|
206
|
+
env_name = canonical[4:] # strip "env."
|
|
207
|
+
merged = merge_env(env_global, env_project)
|
|
208
|
+
return merged.get(env_name)
|
|
209
|
+
|
|
210
|
+
# resource.* keys — read from resource_overrides in project.yaml
|
|
211
|
+
if _is_resource_key(canonical):
|
|
212
|
+
resource_name = canonical[9:] # strip "resource."
|
|
213
|
+
if project_toml and project_toml.exists():
|
|
214
|
+
data = load_doc(project_toml)
|
|
215
|
+
overrides = data.get("resource_overrides", {})
|
|
216
|
+
return str(overrides.get(resource_name, "")) or None
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
# shared.* keys — read from [shared] in global config or project
|
|
220
|
+
if _is_shared_key(canonical):
|
|
221
|
+
cache_name = canonical[7:] # strip "shared."
|
|
222
|
+
cfg = load_merged_config(global_config_path, project_toml)
|
|
223
|
+
return cfg.shared_caches.get(cache_name)
|
|
224
|
+
|
|
225
|
+
# target settings (model, start_mode, autonomous)
|
|
226
|
+
if _is_crab_setting(canonical):
|
|
227
|
+
if project_toml and project_toml.exists():
|
|
228
|
+
settings = read_crab_settings(project_toml)
|
|
229
|
+
if canonical in settings:
|
|
230
|
+
return settings[canonical]
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
# system.path.* keys — read the raw set-value from the global config's
|
|
234
|
+
# [system.path] table (system-only tier; not a merged-config field).
|
|
235
|
+
if _is_system_path_key(canonical):
|
|
236
|
+
cfg = load_merged_config(global_config_path, project_toml)
|
|
237
|
+
return cfg.system_paths.get(canonical)
|
|
238
|
+
|
|
239
|
+
# Regular config keys — use merged config
|
|
240
|
+
flat = _dot_to_flat(canonical)
|
|
241
|
+
cfg = load_merged_config(global_config_path, project_toml)
|
|
242
|
+
valid = {fld.name for fld in fields(cfg)}
|
|
243
|
+
if flat in valid:
|
|
244
|
+
val = getattr(cfg, flat)
|
|
245
|
+
if isinstance(val, bool):
|
|
246
|
+
return str(val).lower()
|
|
247
|
+
return str(val) if val else None
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def set_config_value(
|
|
252
|
+
key: str,
|
|
253
|
+
value: str,
|
|
254
|
+
*,
|
|
255
|
+
config_path: Path,
|
|
256
|
+
env_path: Path | None = None,
|
|
257
|
+
is_system: bool = False,
|
|
258
|
+
) -> str:
|
|
259
|
+
"""Write a config value to the appropriate store.
|
|
260
|
+
|
|
261
|
+
*config_path* is the project.yaml (for box/workset) or kanibako.yaml
|
|
262
|
+
(for system). Returns a human-readable confirmation message.
|
|
263
|
+
"""
|
|
264
|
+
canonical = _resolve_key(key)
|
|
265
|
+
|
|
266
|
+
# env.* keys
|
|
267
|
+
if _is_env_key(canonical):
|
|
268
|
+
env_name = canonical[4:]
|
|
269
|
+
if env_path is None:
|
|
270
|
+
return f"Error: no env file path for key {canonical}"
|
|
271
|
+
try:
|
|
272
|
+
set_env_var(env_path, env_name, value)
|
|
273
|
+
except ValueError as e:
|
|
274
|
+
return f"Error: {e}"
|
|
275
|
+
return f"Set {env_name}={value}"
|
|
276
|
+
|
|
277
|
+
# resource.* keys — write to [resource_overrides]
|
|
278
|
+
if _is_resource_key(canonical):
|
|
279
|
+
resource_name = canonical[9:]
|
|
280
|
+
_write_toml_key(config_path, "resource_overrides", resource_name, value)
|
|
281
|
+
return f"Set resource.{resource_name}={value}"
|
|
282
|
+
|
|
283
|
+
# shared.* keys — write to [shared]
|
|
284
|
+
if _is_shared_key(canonical):
|
|
285
|
+
cache_name = canonical[7:]
|
|
286
|
+
_write_toml_key(config_path, "shared", cache_name, value)
|
|
287
|
+
return f"Set shared.{cache_name}={value}"
|
|
288
|
+
|
|
289
|
+
# target settings
|
|
290
|
+
if _is_crab_setting(canonical):
|
|
291
|
+
_write_toml_key(config_path, "crab", canonical, value)
|
|
292
|
+
return f"Set {canonical}={value}"
|
|
293
|
+
|
|
294
|
+
# system.path.* keys — write to the nested [system.path] table.
|
|
295
|
+
if _is_system_path_key(canonical):
|
|
296
|
+
leaf = canonical[len("system.path."):]
|
|
297
|
+
_write_nested_toml_key(config_path, ("system", "path"), leaf, value)
|
|
298
|
+
return f"Set {canonical}={value}"
|
|
299
|
+
|
|
300
|
+
# Regular config keys
|
|
301
|
+
flat = _dot_to_flat(canonical)
|
|
302
|
+
write_project_config_key(config_path, flat, value)
|
|
303
|
+
return f"Set {flat}={value}"
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def reset_config_value(
|
|
307
|
+
key: str,
|
|
308
|
+
*,
|
|
309
|
+
config_path: Path,
|
|
310
|
+
env_path: Path | None = None,
|
|
311
|
+
) -> str:
|
|
312
|
+
"""Remove an override for a single key. Returns confirmation message."""
|
|
313
|
+
canonical = _resolve_key(key)
|
|
314
|
+
|
|
315
|
+
# env.* keys
|
|
316
|
+
if _is_env_key(canonical):
|
|
317
|
+
env_name = canonical[4:]
|
|
318
|
+
if env_path and unset_env_var(env_path, env_name):
|
|
319
|
+
return f"Unset env.{env_name}"
|
|
320
|
+
return f"No override for env.{env_name}"
|
|
321
|
+
|
|
322
|
+
# resource.* keys
|
|
323
|
+
if _is_resource_key(canonical):
|
|
324
|
+
resource_name = canonical[9:]
|
|
325
|
+
if _remove_toml_key(config_path, "resource_overrides", resource_name):
|
|
326
|
+
return f"Reset resource.{resource_name}"
|
|
327
|
+
return f"No override for resource.{resource_name}"
|
|
328
|
+
|
|
329
|
+
# shared.* keys
|
|
330
|
+
if _is_shared_key(canonical):
|
|
331
|
+
cache_name = canonical[7:]
|
|
332
|
+
if _remove_toml_key(config_path, "shared", cache_name):
|
|
333
|
+
return f"Reset shared.{cache_name}"
|
|
334
|
+
return f"No override for shared.{cache_name}"
|
|
335
|
+
|
|
336
|
+
# target settings
|
|
337
|
+
if _is_crab_setting(canonical):
|
|
338
|
+
if _remove_toml_key(config_path, "crab", canonical):
|
|
339
|
+
return f"Reset {canonical}"
|
|
340
|
+
return f"No override for {canonical}"
|
|
341
|
+
|
|
342
|
+
# system.path.* keys — remove from the nested [system.path] table.
|
|
343
|
+
if _is_system_path_key(canonical):
|
|
344
|
+
leaf = canonical[len("system.path."):]
|
|
345
|
+
if _remove_nested_toml_key(config_path, ("system", "path"), leaf):
|
|
346
|
+
return f"Reset {canonical}"
|
|
347
|
+
return f"No override for {canonical}"
|
|
348
|
+
|
|
349
|
+
# Regular config keys
|
|
350
|
+
flat = _dot_to_flat(canonical)
|
|
351
|
+
if unset_project_config_key(config_path, flat):
|
|
352
|
+
default_val = _DEFAULTS.get(flat, "(none)")
|
|
353
|
+
return f"Reset {flat} (reverts to default: {default_val})"
|
|
354
|
+
return f"No override for {flat}"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def reset_all(
|
|
358
|
+
*,
|
|
359
|
+
config_path: Path,
|
|
360
|
+
env_path: Path | None = None,
|
|
361
|
+
force: bool = False,
|
|
362
|
+
) -> str:
|
|
363
|
+
"""Remove all overrides at this config level. Confirms unless *force*."""
|
|
364
|
+
if not force:
|
|
365
|
+
try:
|
|
366
|
+
confirm_prompt("Remove all config overrides? Type 'yes' to proceed: ")
|
|
367
|
+
except UserCancelled:
|
|
368
|
+
return "Aborted."
|
|
369
|
+
|
|
370
|
+
count = 0
|
|
371
|
+
|
|
372
|
+
# Clear project-level config overrides
|
|
373
|
+
overrides = load_project_overrides(config_path)
|
|
374
|
+
for key in overrides:
|
|
375
|
+
unset_project_config_key(config_path, key)
|
|
376
|
+
count += 1
|
|
377
|
+
|
|
378
|
+
# Clear target settings
|
|
379
|
+
if config_path.exists():
|
|
380
|
+
data = load_doc(config_path)
|
|
381
|
+
if data.get("crab"):
|
|
382
|
+
for k in list(data["crab"]):
|
|
383
|
+
_remove_toml_key(config_path, "crab", k)
|
|
384
|
+
count += 1
|
|
385
|
+
if data.get("resource_overrides"):
|
|
386
|
+
for k in list(data["resource_overrides"]):
|
|
387
|
+
_remove_toml_key(config_path, "resource_overrides", k)
|
|
388
|
+
count += 1
|
|
389
|
+
|
|
390
|
+
# Clear env file
|
|
391
|
+
if env_path and env_path.is_file():
|
|
392
|
+
env = read_env_file(env_path)
|
|
393
|
+
if env:
|
|
394
|
+
count += len(env)
|
|
395
|
+
write_env_file(env_path, {})
|
|
396
|
+
|
|
397
|
+
return f"Reset {count} override(s)." if count else "No overrides to reset."
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def show_config(
|
|
401
|
+
*,
|
|
402
|
+
global_config_path: Path,
|
|
403
|
+
config_path: Path | None = None,
|
|
404
|
+
env_global: Path | None = None,
|
|
405
|
+
env_project: Path | None = None,
|
|
406
|
+
effective: bool = False,
|
|
407
|
+
file: Any = None,
|
|
408
|
+
workset_path: Path | None = None,
|
|
409
|
+
crab_state: dict[str, str] | None = None,
|
|
410
|
+
env_resolved: dict[str, str] | None = None,
|
|
411
|
+
) -> int:
|
|
412
|
+
"""Display config values. Returns exit code.
|
|
413
|
+
|
|
414
|
+
- *effective=False*: show only overrides at this level.
|
|
415
|
+
- *effective=True*: show all resolved values including inherited defaults.
|
|
416
|
+
"""
|
|
417
|
+
out = file or sys.stdout
|
|
418
|
+
|
|
419
|
+
if effective:
|
|
420
|
+
# Show all resolved values
|
|
421
|
+
cfg = load_merged_config(
|
|
422
|
+
global_config_path, config_path, workset_path=workset_path,
|
|
423
|
+
)
|
|
424
|
+
overrides = load_project_overrides(config_path) if config_path else {}
|
|
425
|
+
for fld in fields(cfg):
|
|
426
|
+
val = getattr(cfg, fld.name)
|
|
427
|
+
marker = " (override)" if fld.name in overrides else ""
|
|
428
|
+
print(f" {fld.name} = {val}{marker}", file=out)
|
|
429
|
+
|
|
430
|
+
# Crab settings. When a fully-resolved crab_state is supplied (box
|
|
431
|
+
# view), render it; mark only the keys actually set at the box level.
|
|
432
|
+
# Otherwise fall back to the project-level overrides (today's behavior).
|
|
433
|
+
if crab_state is not None:
|
|
434
|
+
proj_crab = (
|
|
435
|
+
read_crab_settings(config_path)
|
|
436
|
+
if config_path and config_path.exists()
|
|
437
|
+
else {}
|
|
438
|
+
)
|
|
439
|
+
if crab_state:
|
|
440
|
+
print("", file=out)
|
|
441
|
+
for k, v in sorted(crab_state.items()):
|
|
442
|
+
marker = " (override)" if k in proj_crab else ""
|
|
443
|
+
print(f" {k} = {v}{marker}", file=out)
|
|
444
|
+
elif config_path and config_path.exists():
|
|
445
|
+
settings = read_crab_settings(config_path)
|
|
446
|
+
if settings:
|
|
447
|
+
print("", file=out)
|
|
448
|
+
for k, v in sorted(settings.items()):
|
|
449
|
+
print(f" {k} = {v} (override)", file=out)
|
|
450
|
+
|
|
451
|
+
# Env vars. Prefer the fully-resolved env (box view) when supplied.
|
|
452
|
+
merged = (
|
|
453
|
+
env_resolved
|
|
454
|
+
if env_resolved is not None
|
|
455
|
+
else merge_env(env_global, env_project)
|
|
456
|
+
)
|
|
457
|
+
if merged:
|
|
458
|
+
print("", file=out)
|
|
459
|
+
for k in sorted(merged):
|
|
460
|
+
print(f" env.{k} = {merged[k]}", file=out)
|
|
461
|
+
|
|
462
|
+
else:
|
|
463
|
+
# Show only overrides
|
|
464
|
+
has_output = False
|
|
465
|
+
|
|
466
|
+
overrides = load_project_overrides(config_path) if config_path else {}
|
|
467
|
+
for k, v in sorted(overrides.items()):
|
|
468
|
+
print(f" {k} = {v}", file=out)
|
|
469
|
+
has_output = True
|
|
470
|
+
|
|
471
|
+
if config_path and config_path.exists():
|
|
472
|
+
settings = read_crab_settings(config_path)
|
|
473
|
+
for k, v in sorted(settings.items()):
|
|
474
|
+
print(f" {k} = {v}", file=out)
|
|
475
|
+
has_output = True
|
|
476
|
+
|
|
477
|
+
# Env vars (project-level only)
|
|
478
|
+
if env_project:
|
|
479
|
+
env = read_env_file(env_project)
|
|
480
|
+
for k in sorted(env):
|
|
481
|
+
print(f" env.{k} = {env[k]}", file=out)
|
|
482
|
+
has_output = True
|
|
483
|
+
|
|
484
|
+
if not has_output:
|
|
485
|
+
print(" (no overrides)", file=out)
|
|
486
|
+
|
|
487
|
+
return 0
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# ---------------------------------------------------------------------------
|
|
491
|
+
# Config section helpers (load → mutate → dump as YAML)
|
|
492
|
+
# ---------------------------------------------------------------------------
|
|
493
|
+
|
|
494
|
+
def _write_toml_key(path: Path, section: str, key: str, value: str | bool) -> None:
|
|
495
|
+
"""Write a key to a specific config section, preserving other content."""
|
|
496
|
+
data = load_doc(path)
|
|
497
|
+
sec = data.get(section)
|
|
498
|
+
if not isinstance(sec, dict):
|
|
499
|
+
sec = {}
|
|
500
|
+
data[section] = sec
|
|
501
|
+
sec[key] = value
|
|
502
|
+
dump_doc(path, data)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _remove_toml_key(path: Path, section: str, key: str) -> bool:
|
|
506
|
+
"""Remove a key from a specific config section. Returns True if found."""
|
|
507
|
+
if not path.exists():
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
data = load_doc(path)
|
|
511
|
+
sec = data.get(section, {})
|
|
512
|
+
if not isinstance(sec, dict) or key not in sec:
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
del sec[key]
|
|
516
|
+
if not sec:
|
|
517
|
+
del data[section]
|
|
518
|
+
dump_doc(path, data)
|
|
519
|
+
return True
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _write_nested_toml_key(
|
|
523
|
+
path: Path, sections: tuple[str, ...], key: str, value: str | bool,
|
|
524
|
+
) -> None:
|
|
525
|
+
"""Write *key* into a nested table (e.g. ``("system", "path")``).
|
|
526
|
+
|
|
527
|
+
Preserves other content; creates intermediate tables as needed.
|
|
528
|
+
"""
|
|
529
|
+
data = load_doc(path)
|
|
530
|
+
node = data
|
|
531
|
+
for sec in sections:
|
|
532
|
+
child = node.get(sec)
|
|
533
|
+
if not isinstance(child, dict):
|
|
534
|
+
child = {}
|
|
535
|
+
node[sec] = child
|
|
536
|
+
node = child
|
|
537
|
+
node[key] = value
|
|
538
|
+
dump_doc(path, data)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _remove_nested_toml_key(
|
|
542
|
+
path: Path, sections: tuple[str, ...], key: str,
|
|
543
|
+
) -> bool:
|
|
544
|
+
"""Remove *key* from a nested table. Returns True if found.
|
|
545
|
+
|
|
546
|
+
Prunes now-empty intermediate tables.
|
|
547
|
+
"""
|
|
548
|
+
if not path.exists():
|
|
549
|
+
return False
|
|
550
|
+
|
|
551
|
+
data = load_doc(path)
|
|
552
|
+
|
|
553
|
+
# Walk to the innermost table, recording the chain for pruning.
|
|
554
|
+
chain: list[dict] = [data]
|
|
555
|
+
node = data
|
|
556
|
+
for sec in sections:
|
|
557
|
+
if sec not in node or not isinstance(node[sec], dict):
|
|
558
|
+
return False
|
|
559
|
+
node = node[sec]
|
|
560
|
+
chain.append(node)
|
|
561
|
+
|
|
562
|
+
if key not in node:
|
|
563
|
+
return False
|
|
564
|
+
del node[key]
|
|
565
|
+
|
|
566
|
+
# Prune empty tables bottom-up.
|
|
567
|
+
for i in range(len(sections) - 1, -1, -1):
|
|
568
|
+
if not chain[i + 1]:
|
|
569
|
+
del chain[i][sections[i]]
|
|
570
|
+
else:
|
|
571
|
+
break
|
|
572
|
+
dump_doc(path, data)
|
|
573
|
+
return True
|
kanibako/config_io.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Centralized load/dump for kanibako config documents (YAML).
|
|
2
|
+
|
|
3
|
+
All kanibako-owned config files (kanibako.yaml, project.yaml, config.yaml,
|
|
4
|
+
workset.yaml, names.yaml, spawn.yaml, general.yaml, crab configs) are
|
|
5
|
+
serialized as YAML through these two helpers. There is no hand-rolled
|
|
6
|
+
serializer. (pyproject.toml is Python packaging and is NOT handled here.)
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_doc(path: Path | None) -> dict:
|
|
16
|
+
"""Load a config document → dict. Missing/empty/non-mapping → {}."""
|
|
17
|
+
if path is None or not path.exists():
|
|
18
|
+
return {}
|
|
19
|
+
text = path.read_text()
|
|
20
|
+
# Defensive: only parse real text. A non-str (e.g. a MagicMock from an
|
|
21
|
+
# under-mocked test path) fed to yaml.safe_load can balloon memory
|
|
22
|
+
# catastrophically — guard the host instead of trusting the input.
|
|
23
|
+
if not isinstance(text, str):
|
|
24
|
+
return {}
|
|
25
|
+
data = yaml.safe_load(text)
|
|
26
|
+
return data if isinstance(data, dict) else {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def dump_doc(path: Path, data: dict) -> None:
|
|
30
|
+
"""Serialize *data* to *path* as YAML (creates parent dirs)."""
|
|
31
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
path.write_text(
|
|
33
|
+
yaml.safe_dump(
|
|
34
|
+
data, sort_keys=False, default_flow_style=False, allow_unicode=True,
|
|
35
|
+
)
|
|
36
|
+
)
|