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,173 @@
|
|
|
1
|
+
"""Target plugin discovery and resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import logging
|
|
8
|
+
import pkgutil
|
|
9
|
+
from importlib.metadata import entry_points
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from kanibako.targets.base import AgentInstall, Mount, ResourceMapping, ResourceScope, Target, TargetSetting
|
|
13
|
+
from kanibako.targets.no_agent import NoAgentTarget
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AgentInstall", "Mount", "NoAgentTarget", "ResourceMapping", "ResourceScope",
|
|
17
|
+
"Target", "TargetSetting",
|
|
18
|
+
"discover_targets", "get_target", "resolve_target",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _scan_plugin_modules(targets: dict[str, type[Target]]) -> None:
|
|
25
|
+
"""Scan ``kanibako.plugins.*`` for Target subclasses (bind-mount fallback).
|
|
26
|
+
|
|
27
|
+
Entry points rely on dist-info metadata which doesn't travel via
|
|
28
|
+
bind-mount. This fallback imports all sub-packages of
|
|
29
|
+
``kanibako.plugins`` and collects any ``Target`` subclasses found,
|
|
30
|
+
keyed by their ``name`` property.
|
|
31
|
+
|
|
32
|
+
Already-discovered targets (from entry points) are not overwritten.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
import kanibako.plugins as plugins_pkg
|
|
36
|
+
except ImportError:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
for finder, module_name, ispkg in pkgutil.walk_packages(
|
|
40
|
+
plugins_pkg.__path__, prefix="kanibako.plugins."
|
|
41
|
+
):
|
|
42
|
+
if module_name in ("kanibako.plugins",):
|
|
43
|
+
continue
|
|
44
|
+
try:
|
|
45
|
+
mod = importlib.import_module(module_name)
|
|
46
|
+
except Exception:
|
|
47
|
+
logger.debug("Failed to import plugin module %s", module_name, exc_info=True)
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
for attr_name in dir(mod):
|
|
51
|
+
attr = getattr(mod, attr_name, None)
|
|
52
|
+
if (
|
|
53
|
+
isinstance(attr, type)
|
|
54
|
+
and issubclass(attr, Target)
|
|
55
|
+
and attr is not Target
|
|
56
|
+
and attr is not NoAgentTarget
|
|
57
|
+
):
|
|
58
|
+
try:
|
|
59
|
+
instance = attr()
|
|
60
|
+
name = instance.name
|
|
61
|
+
except Exception:
|
|
62
|
+
continue
|
|
63
|
+
if name not in targets:
|
|
64
|
+
targets[name] = attr
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _scan_directory_plugins(directory: Path, targets: dict[str, type[Target]]) -> None:
|
|
68
|
+
"""Scan a directory for .py files containing Target subclasses.
|
|
69
|
+
|
|
70
|
+
Files starting with ``_`` are skipped. Later directories in the
|
|
71
|
+
discovery chain override earlier ones (same target name replaces).
|
|
72
|
+
"""
|
|
73
|
+
if not directory.is_dir():
|
|
74
|
+
return
|
|
75
|
+
for py_file in sorted(directory.glob("*.py")):
|
|
76
|
+
if py_file.name.startswith("_"):
|
|
77
|
+
continue
|
|
78
|
+
try:
|
|
79
|
+
spec = importlib.util.spec_from_file_location(
|
|
80
|
+
f"kanibako_plugin_{py_file.stem}", py_file,
|
|
81
|
+
)
|
|
82
|
+
if spec is None or spec.loader is None:
|
|
83
|
+
continue
|
|
84
|
+
mod = importlib.util.module_from_spec(spec)
|
|
85
|
+
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.debug("Failed to load plugin %s", py_file, exc_info=True)
|
|
88
|
+
continue
|
|
89
|
+
for attr_name in dir(mod):
|
|
90
|
+
attr = getattr(mod, attr_name, None)
|
|
91
|
+
if (
|
|
92
|
+
isinstance(attr, type)
|
|
93
|
+
and issubclass(attr, Target)
|
|
94
|
+
and attr is not Target
|
|
95
|
+
and attr is not NoAgentTarget
|
|
96
|
+
):
|
|
97
|
+
try:
|
|
98
|
+
instance = attr()
|
|
99
|
+
name = instance.name
|
|
100
|
+
except Exception:
|
|
101
|
+
continue
|
|
102
|
+
targets[name] = attr # later overrides earlier
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def discover_targets(project_path: Path | None = None) -> dict[str, type[Target]]:
|
|
106
|
+
"""Scan entry points, plugin modules, and directories for targets.
|
|
107
|
+
|
|
108
|
+
Discovery order (later overrides earlier):
|
|
109
|
+
|
|
110
|
+
1. Entry points (pip-installed packages)
|
|
111
|
+
2. ``kanibako.plugins.*`` module scan (bind-mount fallback)
|
|
112
|
+
3. User directory (``~/.local/share/kanibako/plugins/``)
|
|
113
|
+
4. Project directory (``{project}/.kanibako/plugins/``)
|
|
114
|
+
"""
|
|
115
|
+
targets: dict[str, type[Target]] = {}
|
|
116
|
+
# Group is agent-domain (a registry of agent adapters) → "kanibako.agents".
|
|
117
|
+
# NB: distinct from the crab-domain `kanibako.crabs` module; do not "unify".
|
|
118
|
+
eps = entry_points(group="kanibako.agents")
|
|
119
|
+
for ep in eps:
|
|
120
|
+
cls = ep.load()
|
|
121
|
+
targets[ep.name] = cls
|
|
122
|
+
|
|
123
|
+
# Fallback: scan kanibako.plugins.* for bind-mounted plugins
|
|
124
|
+
_scan_plugin_modules(targets)
|
|
125
|
+
|
|
126
|
+
# User-level file-drop plugins
|
|
127
|
+
from kanibako.paths import xdg
|
|
128
|
+
|
|
129
|
+
data_home = xdg("XDG_DATA_HOME", ".local/share")
|
|
130
|
+
_scan_directory_plugins(data_home / "kanibako" / "plugins", targets)
|
|
131
|
+
|
|
132
|
+
# Project-level file-drop plugins
|
|
133
|
+
if project_path is not None:
|
|
134
|
+
_scan_directory_plugins(project_path / ".kanibako" / "plugins", targets)
|
|
135
|
+
|
|
136
|
+
return targets
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_target(name: str, project_path: Path | None = None) -> type[Target]:
|
|
140
|
+
"""Look up a target class by name.
|
|
141
|
+
|
|
142
|
+
Raises ``KeyError`` if no target with that name is registered.
|
|
143
|
+
"""
|
|
144
|
+
targets = discover_targets(project_path)
|
|
145
|
+
if name not in targets:
|
|
146
|
+
available = ", ".join(sorted(targets)) or "(none)"
|
|
147
|
+
raise KeyError(f"Unknown target '{name}'. Available: {available}")
|
|
148
|
+
return targets[name]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def resolve_target(
|
|
152
|
+
name: str | None = None, project_path: Path | None = None,
|
|
153
|
+
) -> Target:
|
|
154
|
+
"""Instantiate a target by name, or auto-detect.
|
|
155
|
+
|
|
156
|
+
If *name* is given, looks it up via entry points.
|
|
157
|
+
If *name* is None, iterates all discovered targets and returns the first
|
|
158
|
+
one whose ``detect()`` succeeds.
|
|
159
|
+
|
|
160
|
+
Raises ``KeyError`` if no matching target is found.
|
|
161
|
+
"""
|
|
162
|
+
if name:
|
|
163
|
+
cls = get_target(name, project_path)
|
|
164
|
+
return cls()
|
|
165
|
+
|
|
166
|
+
# Auto-detect: try each target's detect() and return the first match.
|
|
167
|
+
targets = discover_targets(project_path)
|
|
168
|
+
for target_name, cls in targets.items():
|
|
169
|
+
instance = cls()
|
|
170
|
+
if instance.detect() is not None:
|
|
171
|
+
return instance
|
|
172
|
+
|
|
173
|
+
return NoAgentTarget()
|
kanibako/targets/base.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Target base classes: ABC for agent targets, Mount and AgentInstall dataclasses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from kanibako.crabs import CrabConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResourceScope(Enum):
|
|
16
|
+
"""How an agent resource is shared across projects."""
|
|
17
|
+
|
|
18
|
+
SHARED = "shared" # Shared across a workset (or the default workset)
|
|
19
|
+
PROJECT = "project" # Per-project, starts fresh
|
|
20
|
+
SEEDED = "seeded" # Per-project, seeded from workset template at creation
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ResourceMapping:
|
|
25
|
+
"""Maps an agent resource path to its sharing scope."""
|
|
26
|
+
|
|
27
|
+
path: str # Relative path within agent home (e.g. "plugins/")
|
|
28
|
+
scope: ResourceScope # How this resource is shared
|
|
29
|
+
description: str = "" # Human-readable description
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class TargetSetting:
|
|
34
|
+
"""Declares a runtime setting that a target plugin supports.
|
|
35
|
+
|
|
36
|
+
Used by ``setting_descriptors()`` to advertise what settings exist,
|
|
37
|
+
their defaults, and (optionally) valid choices.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
key: str # Setting key in agent state dict (e.g. "model")
|
|
41
|
+
description: str # Human-readable description
|
|
42
|
+
default: str = "" # Default value when not overridden
|
|
43
|
+
choices: tuple[str, ...] = () # Valid values; empty = freeform
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class Mount:
|
|
48
|
+
"""A volume mount for a container."""
|
|
49
|
+
|
|
50
|
+
source: Path
|
|
51
|
+
destination: str
|
|
52
|
+
options: str = "" # e.g. "ro"
|
|
53
|
+
|
|
54
|
+
def to_volume_arg(self) -> str:
|
|
55
|
+
"""Return the -v argument string for podman/docker."""
|
|
56
|
+
base = f"{self.source}:{self.destination}"
|
|
57
|
+
return f"{base}:{self.options}" if self.options else base
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class AgentInstall:
|
|
62
|
+
"""Information about an agent installation on the host."""
|
|
63
|
+
|
|
64
|
+
name: str # e.g. "claude"
|
|
65
|
+
binary: Path # host symlink/path to agent binary
|
|
66
|
+
install_dir: Path # root of agent installation
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Target(ABC):
|
|
70
|
+
"""Abstract base class for agent targets.
|
|
71
|
+
|
|
72
|
+
A target encapsulates all agent-specific logic: detection, binary mounting,
|
|
73
|
+
home directory initialization, credential management, and CLI argument
|
|
74
|
+
building. Kanibako's core is agent-agnostic; all agent knowledge lives
|
|
75
|
+
in Target implementations.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def name(self) -> str:
|
|
81
|
+
"""Short identifier for this target (e.g. 'claude')."""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def display_name(self) -> str:
|
|
87
|
+
"""Human-readable name (e.g. 'Claude Code')."""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def detect(self) -> AgentInstall | None:
|
|
92
|
+
"""Detect the agent installation on the host.
|
|
93
|
+
|
|
94
|
+
Returns an AgentInstall if found, or None if the agent is not installed.
|
|
95
|
+
"""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def binary_mounts(self, install: AgentInstall) -> list[Mount]:
|
|
100
|
+
"""Return volume mounts needed to make the agent binary available in the container."""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def init_home(self, home: Path, *, group_auth: bool = True) -> None:
|
|
105
|
+
"""Initialize agent-specific files in the project home directory.
|
|
106
|
+
|
|
107
|
+
Called after kanibako core creates .bashrc/.profile. The target
|
|
108
|
+
should create its own config directories and files (e.g. .claude/).
|
|
109
|
+
|
|
110
|
+
*group_auth* is ``True`` (copy credentials from host) or ``False``
|
|
111
|
+
(skip credential copy — project manages its own credentials).
|
|
112
|
+
"""
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def has_binary(self) -> bool:
|
|
117
|
+
"""Whether this target requires a host-installed binary."""
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
def check_auth(self) -> bool:
|
|
121
|
+
"""Check if the agent is authenticated. Returns True if ok."""
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def resource_mappings(self) -> list[ResourceMapping]:
|
|
125
|
+
"""Declare how agent resources are shared across projects.
|
|
126
|
+
|
|
127
|
+
Returns a list of ResourceMapping entries describing which paths
|
|
128
|
+
within the agent's home directory are shared, project-scoped, or
|
|
129
|
+
seeded from workset defaults.
|
|
130
|
+
|
|
131
|
+
The default returns an empty list, meaning all agent resources
|
|
132
|
+
are treated as project-scoped (the current behavior).
|
|
133
|
+
|
|
134
|
+
Paths are relative to the agent's config directory within the
|
|
135
|
+
project shell (e.g. ".claude/" for ClaudeTarget).
|
|
136
|
+
"""
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
def default_shares(self) -> dict[str, str]:
|
|
140
|
+
"""Declare default scoped shares for this agent's crab.
|
|
141
|
+
|
|
142
|
+
Returns a mapping of full scoped-share keys
|
|
143
|
+
({scope}.path.share_{ro,rw}.{name}) to host_src:guest_dest bind
|
|
144
|
+
expressions. These become the CRAB level's *declared defaults* in the
|
|
145
|
+
share resolver — a user can override or suppress (terminal "") any of
|
|
146
|
+
them at a more-specific level. The default returns {} (no shares).
|
|
147
|
+
"""
|
|
148
|
+
return {}
|
|
149
|
+
|
|
150
|
+
def default_seeds(self) -> dict[str, str]:
|
|
151
|
+
"""Declare default copy-once-at-init seeds for this agent's crab.
|
|
152
|
+
|
|
153
|
+
Returns a mapping of full seed keys ({scope}.path.seeded.{name}) to
|
|
154
|
+
host_src:guest_dest expressions, injected as the CRAB level's declared
|
|
155
|
+
defaults in the seed resolver. A user can override or suppress (terminal
|
|
156
|
+
"" or the "empty" sentinel) any of them at a more-specific level. The
|
|
157
|
+
default returns {} (no seeds). No target ships a default seed yet.
|
|
158
|
+
"""
|
|
159
|
+
return {}
|
|
160
|
+
|
|
161
|
+
def setting_descriptors(self) -> list[TargetSetting]:
|
|
162
|
+
"""Declare what runtime settings this target supports.
|
|
163
|
+
|
|
164
|
+
Returns a list of TargetSetting entries describing the key name,
|
|
165
|
+
default value, valid choices, and human-readable description.
|
|
166
|
+
|
|
167
|
+
The default returns an empty list (no declared settings).
|
|
168
|
+
"""
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
def generate_crab_config(self) -> CrabConfig:
|
|
172
|
+
"""Return a default CrabConfig for this target.
|
|
173
|
+
|
|
174
|
+
Subclasses should override to provide agent-specific defaults
|
|
175
|
+
(template variant, state knobs, shared caches, etc.).
|
|
176
|
+
"""
|
|
177
|
+
from kanibako.crabs import CrabConfig as _CrabConfig
|
|
178
|
+
|
|
179
|
+
return _CrabConfig(name=self.display_name)
|
|
180
|
+
|
|
181
|
+
def apply_state(self, state: dict[str, str]) -> tuple[list[str], dict[str, str]]:
|
|
182
|
+
"""Translate crab-state values into CLI args and env vars.
|
|
183
|
+
|
|
184
|
+
Returns ``(cli_args, env_vars)``. Base implementation ignores all
|
|
185
|
+
state keys. Subclasses override to handle known keys.
|
|
186
|
+
"""
|
|
187
|
+
return [], {}
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def default_entrypoint(self) -> str | None:
|
|
191
|
+
"""Binary name for container entrypoint. None = use bash."""
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
def should_retry_new_session(self, output: str) -> bool:
|
|
195
|
+
"""Check if agent output indicates ``--continue`` failed and a new session should be started."""
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def config_dir_name(self) -> str:
|
|
200
|
+
"""Agent config dir relative to home (e.g. '.claude'). Default: '.{name}'."""
|
|
201
|
+
return f".{self.name}"
|
|
202
|
+
|
|
203
|
+
def instruction_files(self) -> list[str]:
|
|
204
|
+
"""Return filenames that should be layered across template levels.
|
|
205
|
+
|
|
206
|
+
These files are merged (concatenated with section markers) from
|
|
207
|
+
three layers: kanibako base, template, and user project. Each
|
|
208
|
+
filename is relative to the agent's config dir within the shell
|
|
209
|
+
directory (e.g. ``"CLAUDE.md"`` lives at ``shell/.claude/CLAUDE.md``).
|
|
210
|
+
|
|
211
|
+
The default returns an empty list (no instruction files merged).
|
|
212
|
+
"""
|
|
213
|
+
return []
|
|
214
|
+
|
|
215
|
+
def credential_check_path(self, home: Path) -> Path | None:
|
|
216
|
+
"""Path to check for credential existence, or None."""
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def invalidate_credentials(self, home: Path) -> None:
|
|
220
|
+
"""Remove credential files when switching to distinct auth. Default: no-op."""
|
|
221
|
+
|
|
222
|
+
@abstractmethod
|
|
223
|
+
def refresh_credentials(self, home: Path) -> None:
|
|
224
|
+
"""Refresh agent credentials from host into the project home."""
|
|
225
|
+
...
|
|
226
|
+
|
|
227
|
+
@abstractmethod
|
|
228
|
+
def writeback_credentials(self, home: Path) -> None:
|
|
229
|
+
"""Write back credentials from project home to host."""
|
|
230
|
+
...
|
|
231
|
+
|
|
232
|
+
@abstractmethod
|
|
233
|
+
def build_cli_args(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
safe_mode: bool,
|
|
237
|
+
resume_mode: bool,
|
|
238
|
+
new_session: bool,
|
|
239
|
+
is_new_project: bool,
|
|
240
|
+
extra_args: list[str],
|
|
241
|
+
) -> list[str]:
|
|
242
|
+
"""Build command-line arguments for the agent entrypoint."""
|
|
243
|
+
...
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""NoAgentTarget: built-in fallback target that runs a plain shell."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from kanibako.targets.base import AgentInstall, Mount, Target
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from kanibako.crabs import CrabConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NoAgentTarget(Target):
|
|
15
|
+
"""Fallback target that launches /bin/sh without any agent binary."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return "no_agent"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def display_name(self) -> str:
|
|
23
|
+
return "Shell"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def has_binary(self) -> bool:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
def detect(self) -> AgentInstall | None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def binary_mounts(self, install: AgentInstall) -> list[Mount]:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
def init_home(self, home: Path, *, group_auth: bool = True) -> None:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def refresh_credentials(self, home: Path) -> None:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def writeback_credentials(self, home: Path) -> None:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def build_cli_args(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
safe_mode: bool,
|
|
48
|
+
resume_mode: bool,
|
|
49
|
+
new_session: bool,
|
|
50
|
+
is_new_project: bool,
|
|
51
|
+
extra_args: list[str],
|
|
52
|
+
) -> list[str]:
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
def generate_crab_config(self) -> CrabConfig:
|
|
56
|
+
from kanibako.crabs import CrabConfig as _CrabConfig
|
|
57
|
+
|
|
58
|
+
return _CrabConfig(name="Shell")
|
kanibako/templates.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Shell template resolution and application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_template(
|
|
10
|
+
templates_base: Path,
|
|
11
|
+
agent_name: str,
|
|
12
|
+
template_name: str = "standard",
|
|
13
|
+
) -> Path | None:
|
|
14
|
+
"""Return the path to the resolved template directory, or None for 'empty'.
|
|
15
|
+
|
|
16
|
+
Resolution order:
|
|
17
|
+
1. {templates_base}/{agent_name}/{template_name}/
|
|
18
|
+
2. {templates_base}/general/{template_name}/
|
|
19
|
+
3. None (empty template — no files applied)
|
|
20
|
+
"""
|
|
21
|
+
if template_name == "empty":
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
agent_dir = templates_base / agent_name / template_name
|
|
25
|
+
if agent_dir.is_dir():
|
|
26
|
+
return agent_dir
|
|
27
|
+
|
|
28
|
+
general_dir = templates_base / "general" / template_name
|
|
29
|
+
if general_dir.is_dir():
|
|
30
|
+
return general_dir
|
|
31
|
+
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def apply_shell_template(
|
|
36
|
+
shell_path: Path,
|
|
37
|
+
templates_base: Path,
|
|
38
|
+
agent_name: str,
|
|
39
|
+
template_name: str = "standard",
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Apply base + resolved template to a shell directory.
|
|
42
|
+
|
|
43
|
+
Layering:
|
|
44
|
+
1. general/base/* is copied first (common skeleton)
|
|
45
|
+
2. The resolved template overlays on top
|
|
46
|
+
|
|
47
|
+
No-op if the resolved template is None (the ``"empty"`` sentinel or no
|
|
48
|
+
template dirs exist on disk).
|
|
49
|
+
"""
|
|
50
|
+
resolved = resolve_template(templates_base, agent_name, template_name)
|
|
51
|
+
if resolved is None:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# Layer 1: general/base (if it exists)
|
|
55
|
+
base_dir = templates_base / "general" / "base"
|
|
56
|
+
if base_dir.is_dir():
|
|
57
|
+
shutil.copytree(str(base_dir), str(shell_path), dirs_exist_ok=True)
|
|
58
|
+
|
|
59
|
+
# Layer 2: resolved template
|
|
60
|
+
shutil.copytree(str(resolved), str(shell_path), dirs_exist_ok=True)
|