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
kanibako/bun_sea.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Extract embedded modules from Bun SEA (Single Executable Application) binaries.
|
|
2
|
+
|
|
3
|
+
Bun SEA format (ELF/Mach-O/PE):
|
|
4
|
+
|
|
5
|
+
[native binary][bun data...][OFFSETS (32 bytes)]["\\n---- Bun! ----\\n"][u64 totalByteCount]
|
|
6
|
+
|
|
7
|
+
OFFSETS struct (32 bytes):
|
|
8
|
+
u64 byteCount — size of the data blob
|
|
9
|
+
u32 modulesPtr.offset — module table offset from data start
|
|
10
|
+
u32 modulesPtr.length — module table size in bytes
|
|
11
|
+
u32 entryPointId — index of the entry module
|
|
12
|
+
u32 compileExecArgvPtr.offset
|
|
13
|
+
u32 compileExecArgvPtr.length
|
|
14
|
+
u32 flags
|
|
15
|
+
|
|
16
|
+
Module struct (52 bytes, Bun >= 1.3.7):
|
|
17
|
+
StringPointer name (offset u32, length u32)
|
|
18
|
+
StringPointer contents (offset u32, length u32)
|
|
19
|
+
StringPointer sourcemap (offset u32, length u32)
|
|
20
|
+
StringPointer bytecode (offset u32, length u32)
|
|
21
|
+
StringPointer moduleInfo (offset u32, length u32)
|
|
22
|
+
StringPointer bytecodeOriginPath (offset u32, length u32)
|
|
23
|
+
4 bytes enum/flags
|
|
24
|
+
|
|
25
|
+
All offsets are relative to data_start.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import hashlib
|
|
31
|
+
import struct
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
_BUN_MARKER = b"\n---- Bun! ----\n"
|
|
36
|
+
_OFFSETS_SIZE = 32
|
|
37
|
+
_MODULE_STRUCT_SIZE = 52
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BunSEAError(Exception):
|
|
41
|
+
"""Error parsing a Bun SEA binary."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class BunModule:
|
|
46
|
+
"""A module embedded in a Bun SEA binary."""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
content_offset: int # absolute file offset
|
|
50
|
+
content_length: int
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_header(f) -> tuple[int, int, int]:
|
|
54
|
+
"""Parse the Bun SEA trailer and return (data_start, modules_offset, modules_length).
|
|
55
|
+
|
|
56
|
+
All returned offsets are absolute file positions.
|
|
57
|
+
"""
|
|
58
|
+
f.seek(0, 2)
|
|
59
|
+
size = f.tell()
|
|
60
|
+
trailer_size = 8 + len(_BUN_MARKER) + _OFFSETS_SIZE
|
|
61
|
+
if size < trailer_size:
|
|
62
|
+
raise BunSEAError("File too small to be a Bun SEA binary")
|
|
63
|
+
|
|
64
|
+
# Read marker
|
|
65
|
+
f.seek(-(8 + len(_BUN_MARKER)), 2)
|
|
66
|
+
marker = f.read(len(_BUN_MARKER))
|
|
67
|
+
if marker != _BUN_MARKER:
|
|
68
|
+
raise BunSEAError("Bun SEA marker not found")
|
|
69
|
+
|
|
70
|
+
# Read OFFSETS
|
|
71
|
+
f.seek(-(8 + len(_BUN_MARKER) + _OFFSETS_SIZE), 2)
|
|
72
|
+
offsets = f.read(_OFFSETS_SIZE)
|
|
73
|
+
byte_count = struct.unpack("<Q", offsets[0:8])[0]
|
|
74
|
+
mod_off = struct.unpack("<I", offsets[8:12])[0]
|
|
75
|
+
mod_len = struct.unpack("<I", offsets[12:16])[0]
|
|
76
|
+
|
|
77
|
+
marker_abs = size - 8 - len(_BUN_MARKER)
|
|
78
|
+
data_start = marker_abs - _OFFSETS_SIZE - byte_count
|
|
79
|
+
if data_start < 0:
|
|
80
|
+
raise BunSEAError(
|
|
81
|
+
f"Invalid data_start ({data_start}): byteCount={byte_count} exceeds file size"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return data_start, data_start + mod_off, mod_len
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def list_modules(binary_path: Path) -> list[BunModule]:
|
|
88
|
+
"""List all modules embedded in a Bun SEA binary."""
|
|
89
|
+
with open(binary_path, "rb") as f:
|
|
90
|
+
data_start, modules_abs, modules_len = _parse_header(f)
|
|
91
|
+
|
|
92
|
+
if modules_len % _MODULE_STRUCT_SIZE != 0:
|
|
93
|
+
raise BunSEAError(
|
|
94
|
+
f"Module table size {modules_len} not divisible by {_MODULE_STRUCT_SIZE}"
|
|
95
|
+
)
|
|
96
|
+
n_modules = modules_len // _MODULE_STRUCT_SIZE
|
|
97
|
+
|
|
98
|
+
f.seek(modules_abs)
|
|
99
|
+
table = f.read(modules_len)
|
|
100
|
+
|
|
101
|
+
modules: list[BunModule] = []
|
|
102
|
+
for i in range(n_modules):
|
|
103
|
+
base = i * _MODULE_STRUCT_SIZE
|
|
104
|
+
name_off = struct.unpack("<I", table[base : base + 4])[0]
|
|
105
|
+
name_len = struct.unpack("<I", table[base + 4 : base + 8])[0]
|
|
106
|
+
c_off = struct.unpack("<I", table[base + 8 : base + 12])[0]
|
|
107
|
+
c_len = struct.unpack("<I", table[base + 12 : base + 16])[0]
|
|
108
|
+
|
|
109
|
+
f.seek(data_start + name_off)
|
|
110
|
+
name = f.read(name_len).decode("utf-8", errors="replace")
|
|
111
|
+
|
|
112
|
+
modules.append(BunModule(
|
|
113
|
+
name=name,
|
|
114
|
+
content_offset=data_start + c_off,
|
|
115
|
+
content_length=c_len,
|
|
116
|
+
))
|
|
117
|
+
|
|
118
|
+
return modules
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def extract_module(binary_path: Path, name_suffix: str = "cli.js") -> bytes:
|
|
122
|
+
"""Extract a module's content by name suffix (default: cli.js)."""
|
|
123
|
+
modules = list_modules(binary_path)
|
|
124
|
+
for mod in modules:
|
|
125
|
+
if mod.name.endswith(name_suffix):
|
|
126
|
+
with open(binary_path, "rb") as f:
|
|
127
|
+
f.seek(mod.content_offset)
|
|
128
|
+
return f.read(mod.content_length)
|
|
129
|
+
available = [m.name for m in modules]
|
|
130
|
+
raise BunSEAError(
|
|
131
|
+
f"Module ending with '{name_suffix}' not found. "
|
|
132
|
+
f"Available: {available}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def extract_cli_js(binary_path: Path) -> bytes:
|
|
137
|
+
"""Extract the cli.js bundle from a Bun SEA binary."""
|
|
138
|
+
return extract_module(binary_path, "cli.js")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cli_js_hash(binary_path: Path) -> str:
|
|
142
|
+
"""Return the SHA-256 hex digest of the cli.js content."""
|
|
143
|
+
content = extract_cli_js(binary_path)
|
|
144
|
+
return hashlib.sha256(content).hexdigest()
|
kanibako/cli.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Full argparse tree with subparsers, dispatcher, and main() entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from kanibako import __version__
|
|
10
|
+
from kanibako.errors import KanibakoError, UserCancelled
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Formatter(argparse.RawDescriptionHelpFormatter):
|
|
14
|
+
"""Wider action column so subcommand help text stays on one line."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, prog: str, **kwargs: Any) -> None:
|
|
17
|
+
kwargs.setdefault("max_help_position", 30)
|
|
18
|
+
super().__init__(prog, **kwargs)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
prog="kanibako",
|
|
24
|
+
description="Safe, persistent workspaces for AI coding agents.",
|
|
25
|
+
epilog=(
|
|
26
|
+
"COMMANDS\n"
|
|
27
|
+
" rig Manage box rigs (images)\n"
|
|
28
|
+
" box Project lifecycle commands for boxes (containers)\n"
|
|
29
|
+
" crab Crab (agent) management, authentication, and settings\n"
|
|
30
|
+
" workset Project grouping\n"
|
|
31
|
+
" system Global configuration, upgrades, and system information\n"
|
|
32
|
+
"\n"
|
|
33
|
+
"SHORTCUTS (equivalent to 'box <command>'):\n"
|
|
34
|
+
" create Create a new project box\n"
|
|
35
|
+
" list List active and/or inactive boxes\n"
|
|
36
|
+
" ps List active (running) boxes\n"
|
|
37
|
+
" rm Remove a box\n"
|
|
38
|
+
"\n"
|
|
39
|
+
" start Start a box session (default)\n"
|
|
40
|
+
" stop Stop a running box session\n"
|
|
41
|
+
" shell Open a shell in a box\n"
|
|
42
|
+
"\n"
|
|
43
|
+
"ALIASES:\n"
|
|
44
|
+
" agent → crab\n"
|
|
45
|
+
" container → box\n"
|
|
46
|
+
" image → rig\n"
|
|
47
|
+
"\n"
|
|
48
|
+
"common switches (for 'start' command):\n"
|
|
49
|
+
" -N, --new start a new conversation\n"
|
|
50
|
+
" -C, --continue continue the most recent conversation (default)\n"
|
|
51
|
+
" -R, --resume resume with conversation picker\n"
|
|
52
|
+
" -A, --autonomous run with full permissions (default)\n"
|
|
53
|
+
" -S, --secure run without --dangerously-skip-permissions\n"
|
|
54
|
+
" -M, --model MODEL override the agent model for this run\n"
|
|
55
|
+
" -v, --verbose show debug output (target detection, container cmd)\n"
|
|
56
|
+
"\n"
|
|
57
|
+
"run 'kanibako COMMAND --help' for subcommand-specific options"
|
|
58
|
+
),
|
|
59
|
+
formatter_class=_Formatter,
|
|
60
|
+
add_help=False,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
64
|
+
|
|
65
|
+
# Import and register all subcommand parsers.
|
|
66
|
+
from kanibako.commands.start import (
|
|
67
|
+
add_shell_parser,
|
|
68
|
+
add_start_parser,
|
|
69
|
+
)
|
|
70
|
+
from kanibako.commands.image import add_parser as add_rig_parser
|
|
71
|
+
from kanibako.commands.box import add_parser as add_box_parser
|
|
72
|
+
from kanibako.commands.box._parser import run_create, run_list as run_list_fn, run_ps, run_rm
|
|
73
|
+
from kanibako.commands.stop import add_parser as add_stop_parser
|
|
74
|
+
from kanibako.commands.workset_cmd import add_parser as add_workset_parser
|
|
75
|
+
from kanibako.commands.crab_cmd import add_parser as add_crab_parser
|
|
76
|
+
from kanibako.commands.system_cmd import add_parser as add_system_parser
|
|
77
|
+
|
|
78
|
+
# Setup wizard (before management commands, works pre-init).
|
|
79
|
+
from kanibako.commands.setup_cmd import run_setup
|
|
80
|
+
setup_p = subparsers.add_parser("setup", help="Run the setup wizard")
|
|
81
|
+
setup_p.set_defaults(func=run_setup)
|
|
82
|
+
|
|
83
|
+
# Top-level aliases (start, shell, stop already have their own parsers).
|
|
84
|
+
add_start_parser(subparsers)
|
|
85
|
+
add_shell_parser(subparsers)
|
|
86
|
+
add_stop_parser(subparsers)
|
|
87
|
+
|
|
88
|
+
# list — top-level shortcut for box list
|
|
89
|
+
list_p = subparsers.add_parser("list", help="List active and/or inactive boxes")
|
|
90
|
+
list_p.add_argument(
|
|
91
|
+
"--active", action="store_true",
|
|
92
|
+
help="Show only active (running) boxes",
|
|
93
|
+
)
|
|
94
|
+
list_p.add_argument(
|
|
95
|
+
"--all", "-a", action="store_true", dest="show_all",
|
|
96
|
+
help="Include orphaned boxes",
|
|
97
|
+
)
|
|
98
|
+
list_p.add_argument(
|
|
99
|
+
"-q", "--quiet", action="store_true",
|
|
100
|
+
help="Output box names only, one per line",
|
|
101
|
+
)
|
|
102
|
+
list_p.set_defaults(func=run_list_fn)
|
|
103
|
+
|
|
104
|
+
# ps — top-level shortcut for box list --active
|
|
105
|
+
ps_p = subparsers.add_parser("ps", help="List active (running) boxes")
|
|
106
|
+
ps_p.add_argument(
|
|
107
|
+
"--all", "-a", action="store_true", dest="show_all",
|
|
108
|
+
help="Show all boxes (active and inactive)",
|
|
109
|
+
)
|
|
110
|
+
ps_p.add_argument(
|
|
111
|
+
"-q", "--quiet", action="store_true",
|
|
112
|
+
help="Output box names only, one per line",
|
|
113
|
+
)
|
|
114
|
+
ps_p.set_defaults(func=run_ps)
|
|
115
|
+
|
|
116
|
+
# create — top-level alias for box create
|
|
117
|
+
create_p = subparsers.add_parser("create", help="Create a new project")
|
|
118
|
+
create_p.add_argument(
|
|
119
|
+
"path", nargs="?", default=None,
|
|
120
|
+
help="Project directory (default: cwd). Created if it doesn't exist.",
|
|
121
|
+
)
|
|
122
|
+
create_p.add_argument(
|
|
123
|
+
"--standalone", action="store_true",
|
|
124
|
+
help="Use standalone mode (all state inside the project directory)",
|
|
125
|
+
)
|
|
126
|
+
create_p.add_argument(
|
|
127
|
+
"--name", default=None,
|
|
128
|
+
help="Project name override (default: auto-assigned from directory name)",
|
|
129
|
+
)
|
|
130
|
+
create_p.add_argument(
|
|
131
|
+
"-i", "--image", default=None,
|
|
132
|
+
help="Container image to use for this project",
|
|
133
|
+
)
|
|
134
|
+
create_p.add_argument(
|
|
135
|
+
"--no-vault", action="store_true",
|
|
136
|
+
help="Disable vault directories",
|
|
137
|
+
)
|
|
138
|
+
create_p.add_argument(
|
|
139
|
+
"--distinct-auth", action="store_true",
|
|
140
|
+
help="Use distinct credentials (no sync from host)",
|
|
141
|
+
)
|
|
142
|
+
create_p.add_argument(
|
|
143
|
+
"--allow-home", action="store_true",
|
|
144
|
+
help="Permit a standalone project rooted at $HOME (mounts your entire "
|
|
145
|
+
"home directory; required to create one there)",
|
|
146
|
+
)
|
|
147
|
+
create_p.set_defaults(func=run_create)
|
|
148
|
+
|
|
149
|
+
# rm — top-level alias for box rm
|
|
150
|
+
rm_p = subparsers.add_parser("rm", help="Remove a project")
|
|
151
|
+
rm_p.add_argument("target", help="Project name or workspace path to remove")
|
|
152
|
+
rm_p.add_argument(
|
|
153
|
+
"--purge", action="store_true",
|
|
154
|
+
help="Also delete kanibako metadata for this project",
|
|
155
|
+
)
|
|
156
|
+
rm_p.add_argument(
|
|
157
|
+
"--force", action="store_true",
|
|
158
|
+
help="Skip confirmation prompt (only relevant with --purge)",
|
|
159
|
+
)
|
|
160
|
+
rm_p.set_defaults(func=run_rm)
|
|
161
|
+
|
|
162
|
+
# Management commands.
|
|
163
|
+
add_rig_parser(subparsers)
|
|
164
|
+
add_box_parser(subparsers)
|
|
165
|
+
add_workset_parser(subparsers)
|
|
166
|
+
add_crab_parser(subparsers)
|
|
167
|
+
add_system_parser(subparsers)
|
|
168
|
+
|
|
169
|
+
return parser
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
_COMMAND_ALIASES: dict[str, str] = {
|
|
173
|
+
"agent": "crab",
|
|
174
|
+
"image": "rig",
|
|
175
|
+
"container": "box",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_SUBCOMMANDS = {
|
|
179
|
+
# Top-level aliases (delegate to box subcommands).
|
|
180
|
+
"start", "stop", "shell", "ps", "list", "create", "rm",
|
|
181
|
+
# Management commands.
|
|
182
|
+
"box", "rig", "workset", "crab", "system",
|
|
183
|
+
# Command aliases (#62).
|
|
184
|
+
"agent", "image", "container",
|
|
185
|
+
# Setup wizard.
|
|
186
|
+
"setup",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _ensure_initialized() -> None:
|
|
191
|
+
"""Ensure kanibako is initialized (create config + data dirs on first run)."""
|
|
192
|
+
from kanibako.config import (
|
|
193
|
+
KanibakoConfig,
|
|
194
|
+
config_file_path,
|
|
195
|
+
write_global_config,
|
|
196
|
+
)
|
|
197
|
+
from pathlib import Path
|
|
198
|
+
|
|
199
|
+
from kanibako.paths import resolve_system_paths, xdg
|
|
200
|
+
|
|
201
|
+
config_home = xdg("XDG_CONFIG_HOME", ".config")
|
|
202
|
+
cf = config_file_path(config_home)
|
|
203
|
+
|
|
204
|
+
if cf.exists():
|
|
205
|
+
return # Already initialized
|
|
206
|
+
|
|
207
|
+
# First run: create config and data dirs
|
|
208
|
+
config = KanibakoConfig()
|
|
209
|
+
write_global_config(cf, config)
|
|
210
|
+
|
|
211
|
+
# Create data directories
|
|
212
|
+
data_home = xdg("XDG_DATA_HOME", ".local/share")
|
|
213
|
+
sys_paths = resolve_system_paths(
|
|
214
|
+
config.system_paths, data_home=data_home, home=Path.home(),
|
|
215
|
+
)
|
|
216
|
+
data_path = sys_paths["system.path.data"]
|
|
217
|
+
(data_path / "containers").mkdir(parents=True, exist_ok=True)
|
|
218
|
+
sys_paths["system.path.boxes"].mkdir(parents=True, exist_ok=True)
|
|
219
|
+
|
|
220
|
+
templates_dir = sys_paths["system.path.templates"]
|
|
221
|
+
(templates_dir / "general" / "base").mkdir(parents=True, exist_ok=True)
|
|
222
|
+
(templates_dir / "general" / "standard").mkdir(parents=True, exist_ok=True)
|
|
223
|
+
|
|
224
|
+
comms_dir = sys_paths["system.path.comms"]
|
|
225
|
+
(comms_dir / "mailbox").mkdir(parents=True, exist_ok=True)
|
|
226
|
+
(comms_dir / "broadcast.log").touch(exist_ok=True)
|
|
227
|
+
|
|
228
|
+
# Create crabs directory and generate default crab TOMLs.
|
|
229
|
+
from kanibako.crabs import CrabConfig, write_crab_config
|
|
230
|
+
from kanibako.targets import discover_targets
|
|
231
|
+
|
|
232
|
+
crabs_path = sys_paths["system.path.crabs"]
|
|
233
|
+
crabs_path.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
|
|
235
|
+
general_toml = crabs_path / "general.yaml"
|
|
236
|
+
if not general_toml.exists():
|
|
237
|
+
write_crab_config(general_toml, CrabConfig(name="Shell"))
|
|
238
|
+
|
|
239
|
+
for target_name, cls in discover_targets().items():
|
|
240
|
+
target_toml = crabs_path / f"{target_name}.yaml"
|
|
241
|
+
if not target_toml.exists():
|
|
242
|
+
crab_cfg = cls().generate_crab_config()
|
|
243
|
+
write_crab_config(target_toml, crab_cfg)
|
|
244
|
+
else:
|
|
245
|
+
crab_cfg = CrabConfig()
|
|
246
|
+
(templates_dir / target_name / crab_cfg.shell).mkdir(
|
|
247
|
+
parents=True, exist_ok=True,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Seed default global environment variables (don't overwrite existing).
|
|
251
|
+
from kanibako.shellenv import read_env_file, write_env_file
|
|
252
|
+
|
|
253
|
+
global_env_path = data_path / "env"
|
|
254
|
+
global_env = read_env_file(global_env_path)
|
|
255
|
+
for key, value in {"COLORTERM": "truecolor"}.items():
|
|
256
|
+
global_env.setdefault(key, value)
|
|
257
|
+
write_env_file(global_env_path, global_env)
|
|
258
|
+
|
|
259
|
+
# Try shell completion
|
|
260
|
+
try:
|
|
261
|
+
from kanibako.commands.install import _install_completion
|
|
262
|
+
|
|
263
|
+
_install_completion()
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def main(argv: list[str] | None = None) -> None:
|
|
269
|
+
parser = build_parser()
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
import argcomplete
|
|
273
|
+
argcomplete.autocomplete(parser)
|
|
274
|
+
except ImportError:
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
effective = list(argv if argv is not None else sys.argv[1:])
|
|
278
|
+
|
|
279
|
+
# Extract -v/--verbose before subcommand dispatch.
|
|
280
|
+
verbose = "-v" in effective or "--verbose" in effective
|
|
281
|
+
effective = [a for a in effective if a not in ("-v", "--verbose")]
|
|
282
|
+
|
|
283
|
+
from kanibako.log import setup_logging
|
|
284
|
+
setup_logging(verbose=verbose)
|
|
285
|
+
|
|
286
|
+
# Handle top-level --help and --version before argparse dispatch
|
|
287
|
+
# (kept off the parser so they don't appear in tab-completion).
|
|
288
|
+
if effective and effective[0] in ("-h", "--help"):
|
|
289
|
+
parser.print_help()
|
|
290
|
+
sys.exit(0)
|
|
291
|
+
elif effective and effective[0] == "--version":
|
|
292
|
+
print(f"kanibako {__version__}")
|
|
293
|
+
sys.exit(0)
|
|
294
|
+
else:
|
|
295
|
+
# If the first arg isn't a known subcommand, default to "start".
|
|
296
|
+
if not effective or effective[0] not in _SUBCOMMANDS:
|
|
297
|
+
effective = ["start"] + effective
|
|
298
|
+
# Translate command aliases (e.g. agent→crab, image→rig).
|
|
299
|
+
if effective and effective[0] in _COMMAND_ALIASES:
|
|
300
|
+
effective[0] = _COMMAND_ALIASES[effective[0]]
|
|
301
|
+
|
|
302
|
+
# For start/shell, split args at '--' so flags after the project
|
|
303
|
+
# positional still work (REMAINDER would otherwise swallow them).
|
|
304
|
+
# Everything before '--' goes to argparse; everything after becomes
|
|
305
|
+
# args passed to the agent/shell.
|
|
306
|
+
post_dash: list[str] | None = None
|
|
307
|
+
if (
|
|
308
|
+
len(effective) >= 2
|
|
309
|
+
and effective[0] in ("start", "shell")
|
|
310
|
+
and "--" in effective[1:]
|
|
311
|
+
):
|
|
312
|
+
idx = effective.index("--", 1)
|
|
313
|
+
post_dash = effective[idx + 1:]
|
|
314
|
+
effective = effective[:idx]
|
|
315
|
+
|
|
316
|
+
args = parser.parse_args(effective)
|
|
317
|
+
if args.command == "start":
|
|
318
|
+
args.agent_args = post_dash or []
|
|
319
|
+
elif args.command == "shell":
|
|
320
|
+
args.shell_args = post_dash or []
|
|
321
|
+
|
|
322
|
+
# Lazy init: create config + data dirs on first run.
|
|
323
|
+
# Skip for crab (helper/fork run inside containers).
|
|
324
|
+
if args.command not in ("crab", "setup"):
|
|
325
|
+
_ensure_initialized()
|
|
326
|
+
|
|
327
|
+
func = getattr(args, "func", None)
|
|
328
|
+
if func is None:
|
|
329
|
+
parser.print_help()
|
|
330
|
+
sys.exit(0)
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
rc = func(args)
|
|
334
|
+
except UserCancelled:
|
|
335
|
+
print("Aborted.")
|
|
336
|
+
rc = 2
|
|
337
|
+
except KanibakoError as e:
|
|
338
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
339
|
+
rc = 1
|
|
340
|
+
except KeyboardInterrupt:
|
|
341
|
+
print()
|
|
342
|
+
rc = 130
|
|
343
|
+
|
|
344
|
+
sys.exit(rc)
|
|
File without changes
|