kanibako-cli 1.5.0.dev14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. kanibako/__init__.py +3 -0
  2. kanibako/__main__.py +6 -0
  3. kanibako/auth_browser.py +296 -0
  4. kanibako/auth_parser.py +51 -0
  5. kanibako/browser_sidecar.py +183 -0
  6. kanibako/browser_state.py +103 -0
  7. kanibako/bun_sea.py +144 -0
  8. kanibako/cli.py +344 -0
  9. kanibako/commands/__init__.py +0 -0
  10. kanibako/commands/archive.py +228 -0
  11. kanibako/commands/box/__init__.py +22 -0
  12. kanibako/commands/box/_duplicate.py +395 -0
  13. kanibako/commands/box/_migrate.py +574 -0
  14. kanibako/commands/box/_parser.py +1178 -0
  15. kanibako/commands/clean.py +166 -0
  16. kanibako/commands/crab_cmd.py +480 -0
  17. kanibako/commands/diagnose.py +239 -0
  18. kanibako/commands/fork_cmd.py +51 -0
  19. kanibako/commands/helper_cmd.py +669 -0
  20. kanibako/commands/image.py +1300 -0
  21. kanibako/commands/install.py +152 -0
  22. kanibako/commands/refresh_credentials.py +67 -0
  23. kanibako/commands/restore.py +298 -0
  24. kanibako/commands/setup_cmd.py +89 -0
  25. kanibako/commands/start.py +1600 -0
  26. kanibako/commands/stop.py +116 -0
  27. kanibako/commands/system_cmd.py +224 -0
  28. kanibako/commands/upgrade.py +161 -0
  29. kanibako/commands/vault_cmd.py +199 -0
  30. kanibako/commands/workset_cmd.py +552 -0
  31. kanibako/config.py +514 -0
  32. kanibako/config_interface.py +573 -0
  33. kanibako/config_io.py +36 -0
  34. kanibako/container.py +607 -0
  35. kanibako/containerfiles.py +58 -0
  36. kanibako/containers/Containerfile.kanibako +99 -0
  37. kanibako/containers/Containerfile.template-android +55 -0
  38. kanibako/containers/Containerfile.template-dotnet +29 -0
  39. kanibako/containers/Containerfile.template-js +43 -0
  40. kanibako/containers/Containerfile.template-jvm +27 -0
  41. kanibako/containers/Containerfile.template-systems +46 -0
  42. kanibako/containers/__init__.py +0 -0
  43. kanibako/crabs.py +89 -0
  44. kanibako/errors.py +33 -0
  45. kanibako/freshness.py +67 -0
  46. kanibako/git.py +114 -0
  47. kanibako/helper_client.py +132 -0
  48. kanibako/helper_listener.py +538 -0
  49. kanibako/helpers.py +339 -0
  50. kanibako/hygiene.py +296 -0
  51. kanibako/image_sharing.py +133 -0
  52. kanibako/instructions.py +160 -0
  53. kanibako/log.py +31 -0
  54. kanibako/names.py +248 -0
  55. kanibako/paths.py +1483 -0
  56. kanibako/plugins/__init__.py +10 -0
  57. kanibako/registry.py +71 -0
  58. kanibako/rig_bundle.py +121 -0
  59. kanibako/rig_meta.py +92 -0
  60. kanibako/rig_registry.py +132 -0
  61. kanibako/rig_resolve.py +182 -0
  62. kanibako/rig_source.py +245 -0
  63. kanibako/scripts/__init__.py +0 -0
  64. kanibako/scripts/helper-init.sh +45 -0
  65. kanibako/scripts/kanibako-entry +12 -0
  66. kanibako/settings_resolve.py +312 -0
  67. kanibako/settings_seeds.py +154 -0
  68. kanibako/settings_shares.py +154 -0
  69. kanibako/shellenv.py +75 -0
  70. kanibako/snapshots.py +281 -0
  71. kanibako/targets/__init__.py +173 -0
  72. kanibako/targets/base.py +243 -0
  73. kanibako/targets/no_agent.py +58 -0
  74. kanibako/templates.py +60 -0
  75. kanibako/templates_image.py +224 -0
  76. kanibako/tweakcc.py +140 -0
  77. kanibako/tweakcc_cache.py +171 -0
  78. kanibako/utils.py +136 -0
  79. kanibako/workset.py +347 -0
  80. kanibako_cli-1.5.0.dev14.dist-info/METADATA +15 -0
  81. kanibako_cli-1.5.0.dev14.dist-info/RECORD +85 -0
  82. kanibako_cli-1.5.0.dev14.dist-info/WHEEL +5 -0
  83. kanibako_cli-1.5.0.dev14.dist-info/entry_points.txt +5 -0
  84. kanibako_cli-1.5.0.dev14.dist-info/licenses/LICENSE.md +594 -0
  85. kanibako_cli-1.5.0.dev14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,99 @@
1
+ # Shared Containerfile for all kanibako image variants.
2
+ # The droste base tier is selected via --build-arg BASE_IMAGE=...
3
+ # (seed → min, fiber → oci, thread → lxc, hair → vm).
4
+ ARG BASE_IMAGE=ghcr.io/doctorjei/droste-fiber:1.1.0
5
+ FROM $BASE_IMAGE
6
+
7
+ ARG AGENT_USER=agent
8
+ ARG VARIANT=oci
9
+
10
+ USER root
11
+
12
+ # Rename droste user (UID 1000) to the desired agent username
13
+ RUN existing=$(getent passwd 1000 | cut -d: -f1); \
14
+ if [ -n "$existing" ] && [ "$existing" != "$AGENT_USER" ]; then \
15
+ usermod -l "$AGENT_USER" -d "/home/$AGENT_USER" -m "$existing" \
16
+ && groupmod -n "$AGENT_USER" "$existing"; \
17
+ fi \
18
+ && echo "$AGENT_USER ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$AGENT_USER \
19
+ && chmod 0440 /etc/sudoers.d/$AGENT_USER
20
+
21
+ # Fix subuid/subgid ownership after user rename (droste -> agent)
22
+ RUN if [ -f /etc/subuid ]; then sed -i "s/^droste:/$AGENT_USER:/" /etc/subuid; fi \
23
+ && if [ -f /etc/subgid ]; then sed -i "s/^droste:/$AGENT_USER:/" /etc/subgid; fi
24
+
25
+ # Packages kanibako needs that the droste bases at our tiers don't supply.
26
+ # nodejs/npm back MCP servers + tweakcc. cifs-utils/nfs-common/openssh-client
27
+ # are absent from seed (min) and fiber (oci); they ship in thread/hair
28
+ # (lxc/vm), where reinstalling them is a no-op.
29
+ RUN apt-get update && apt-get install -y --no-install-recommends \
30
+ cifs-utils \
31
+ nfs-common \
32
+ nodejs \
33
+ npm \
34
+ openssh-client \
35
+ && rm -rf /var/lib/apt/lists/*
36
+
37
+ # Lean CLI tools for the seed-based min variant only. fiber (oci) and the
38
+ # thread/hair tiers (lxc/vm) already ship these via droste, so install them
39
+ # only where the base lacks them. gh and uv are intentionally omitted from
40
+ # min — add them locally if a min box ever needs them.
41
+ RUN if [ "$VARIANT" = "min" ]; then \
42
+ apt-get update && apt-get install -y --no-install-recommends \
43
+ fd-find \
44
+ inotify-tools \
45
+ ripgrep \
46
+ sshpass \
47
+ && rm -rf /var/lib/apt/lists/* \
48
+ && ln -sf /usr/bin/fdfind /usr/local/bin/fd; \
49
+ fi
50
+
51
+ RUN if command -v pip >/dev/null 2>&1; then \
52
+ pip install --break-system-packages kanibako-base build twine; \
53
+ fi
54
+
55
+ # tweakcc: custom Claude Code patching tool (requires node/npm)
56
+ RUN if command -v npm >/dev/null 2>&1; then \
57
+ npm install -g tweakcc 2>/dev/null || true; \
58
+ fi
59
+
60
+ # LXC-specific fixes: mask failing units, enable DHCP, configure rootless Podman
61
+ RUN if [ "$VARIANT" = "lxc" ]; then \
62
+ : > /etc/fstab \
63
+ && systemctl mask \
64
+ systemd-remount-fs.service systemd-growfs-root.service \
65
+ dev-hugepages.mount dev-mqueue.mount run-lock.mount \
66
+ tmp.mount run-rpc_pipefs.mount \
67
+ && systemctl enable getty@console.service \
68
+ && mkdir -p /etc/systemd/network \
69
+ && printf '[Match]\nName=eth0\n\n[Network]\nDHCP=yes\n' \
70
+ > /etc/systemd/network/80-dhcp.network \
71
+ && mkdir -p /etc/containers \
72
+ && printf '[engine]\ncgroup_manager = "cgroupfs"\n\n[network]\ndefault_rootless_network_cmd = "slirp4netns"\n' \
73
+ > /etc/containers/containers.conf \
74
+ && mkdir -p /var/lib/systemd/linger \
75
+ && touch "/var/lib/systemd/linger/$AGENT_USER"; \
76
+ fi
77
+
78
+ # LXC/VM need systemd as PID 1; OCI/min use /bin/bash
79
+ RUN if [ "$VARIANT" = "lxc" ] || [ "$VARIANT" = "vm" ]; then \
80
+ printf '#!/bin/bash\nexec /sbin/init\n' > /usr/local/bin/kanibako-entrypoint; \
81
+ else \
82
+ printf '#!/bin/bash\nexec /bin/bash "$@"\n' > /usr/local/bin/kanibako-entrypoint; \
83
+ fi \
84
+ && chmod +x /usr/local/bin/kanibako-entrypoint
85
+
86
+ # Default tmux config (user can override via shell template or bind mount)
87
+ COPY tmux.conf /etc/tmux.conf
88
+
89
+ USER $AGENT_USER
90
+ WORKDIR /home/$AGENT_USER
91
+
92
+ ENV LANG=C.UTF-8
93
+ ENV PATH="/home/$AGENT_USER/.local/bin:${PATH}"
94
+
95
+ RUN mkdir -p workspace .local/state/kanibako .local/bin share-ro share-rw
96
+
97
+ WORKDIR /home/$AGENT_USER/workspace
98
+
99
+ ENTRYPOINT ["/usr/local/bin/kanibako-entrypoint"]
@@ -0,0 +1,55 @@
1
+ # kanibako-template: Android SDK command-line tools + NDK
2
+ # kanibako-template-check: java -version
3
+ # kanibako-template-check: sdkmanager --version
4
+ # kanibako-template-check: adb --version
5
+ # Android development template: command-line tools, platform-tools,
6
+ # platform android-34, build-tools 34.0.0, NDK, and a JDK.
7
+ # Built on top of any kanibako base image.
8
+ #
9
+ # Default base: kanibako-oci:latest. Override with:
10
+ # podman build --build-arg BASE_IMAGE=ghcr.io/doctorjei/kanibako-lxc:latest ...
11
+ ARG BASE_IMAGE=ghcr.io/doctorjei/kanibako-oci:latest
12
+ FROM $BASE_IMAGE
13
+
14
+ # Re-declare ARG after FROM so $BASE_IMAGE is in scope for RUN.
15
+ # Surfaces the actual base in build output (default or explicit).
16
+ ARG BASE_IMAGE
17
+ RUN echo "[kanibako template] Building android on BASE_IMAGE=$BASE_IMAGE"
18
+
19
+ USER root
20
+
21
+ # Android tooling needs a JDK; use the distro default (same as the jvm
22
+ # template, which builds cleanly on every base). unzip extracts the
23
+ # command-line tools archive.
24
+ RUN apt-get update && apt-get install -y --no-install-recommends \
25
+ default-jdk \
26
+ unzip \
27
+ && rm -rf /var/lib/apt/lists/*
28
+
29
+ ENV ANDROID_SDK_ROOT=/opt/android-sdk
30
+ ENV PATH="${PATH}:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools"
31
+
32
+ # Install the Android command-line tools (pinned release) into
33
+ # $ANDROID_SDK_ROOT/cmdline-tools/latest/. The zip extracts a top-level
34
+ # cmdline-tools/ dir, which sdkmanager requires to live under latest/.
35
+ RUN curl -fsSL \
36
+ https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip \
37
+ -o /tmp/cmdline-tools.zip \
38
+ && mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools" \
39
+ && unzip -q /tmp/cmdline-tools.zip -d /tmp/android-cmdline \
40
+ && mv /tmp/android-cmdline/cmdline-tools "$ANDROID_SDK_ROOT/cmdline-tools/latest" \
41
+ && rm -rf /tmp/cmdline-tools.zip /tmp/android-cmdline
42
+
43
+ # Accept licenses and install a minimal SDK set (pinned versions).
44
+ RUN yes | sdkmanager --licenses \
45
+ && sdkmanager \
46
+ "platform-tools" \
47
+ "platforms;android-34" \
48
+ "build-tools;34.0.0" \
49
+ "ndk;26.1.10909125"
50
+
51
+ # Hand the SDK tree to the agent user so it can run/update tooling.
52
+ RUN chown -R agent:agent "$ANDROID_SDK_ROOT"
53
+
54
+ USER agent
55
+ WORKDIR /home/agent/workspace
@@ -0,0 +1,29 @@
1
+ # kanibako-template: .NET SDK (LTS)
2
+ # kanibako-template-check: dotnet --info
3
+ # .NET development template: .NET SDK on the 8.0 LTS channel.
4
+ # Built on top of any kanibako base image.
5
+ #
6
+ # Default base: kanibako-oci:latest. Override with:
7
+ # podman build --build-arg BASE_IMAGE=ghcr.io/doctorjei/kanibako-lxc:latest ...
8
+ ARG BASE_IMAGE=ghcr.io/doctorjei/kanibako-oci:latest
9
+ FROM $BASE_IMAGE
10
+
11
+ # Re-declare ARG after FROM so $BASE_IMAGE is in scope for RUN.
12
+ # Surfaces the actual base in build output (default or explicit).
13
+ ARG BASE_IMAGE
14
+ RUN echo "[kanibako template] Building dotnet on BASE_IMAGE=$BASE_IMAGE"
15
+
16
+ USER root
17
+
18
+ # Install the .NET SDK via the official script pinned to the 8.0 LTS channel.
19
+ # Using the script avoids apt-feed/distro coupling across base tiers.
20
+ RUN curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh \
21
+ && bash /tmp/dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet \
22
+ && ln -s /usr/share/dotnet/dotnet /usr/local/bin/dotnet \
23
+ && rm /tmp/dotnet-install.sh
24
+
25
+ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \
26
+ DOTNET_NOLOGO=1
27
+
28
+ USER agent
29
+ WORKDIR /home/agent/workspace
@@ -0,0 +1,43 @@
1
+ # kanibako-template: Node tooling: yarn, pnpm, bun, TypeScript
2
+ # kanibako-template-check: node --version
3
+ # kanibako-template-check: yarn --version
4
+ # kanibako-template-check: pnpm --version
5
+ # kanibako-template-check: tsc --version
6
+ # kanibako-template-check: bun --version
7
+ # JavaScript/TypeScript development template: yarn, pnpm, bun, TypeScript.
8
+ # node/npm already ship in every kanibako base.
9
+ # Built on top of any kanibako base image.
10
+ #
11
+ # Default base: kanibako-oci:latest. Override with:
12
+ # podman build --build-arg BASE_IMAGE=ghcr.io/doctorjei/kanibako-lxc:latest ...
13
+ ARG BASE_IMAGE=ghcr.io/doctorjei/kanibako-oci:latest
14
+ FROM $BASE_IMAGE
15
+
16
+ # Re-declare ARG after FROM so $BASE_IMAGE is in scope for RUN.
17
+ # Surfaces the actual base in build output (default or explicit).
18
+ ARG BASE_IMAGE
19
+ RUN echo "[kanibako template] Building js on BASE_IMAGE=$BASE_IMAGE"
20
+
21
+ USER root
22
+
23
+ # unzip is required by the bun installer and is not guaranteed on every base.
24
+ RUN apt-get update && apt-get install -y --no-install-recommends \
25
+ unzip \
26
+ && rm -rf /var/lib/apt/lists/*
27
+
28
+ # Install pinned package managers + TypeScript globally (the npm global prefix is
29
+ # root-owned, so these land as real binaries on PATH for every user).
30
+ # NOT corepack: corepack's `prepare --activate` pins per-user and runs here as
31
+ # root, but the container runs as `agent` at runtime — whose corepack cache has
32
+ # no pinned version, so corepack would re-resolve to the *latest* pnpm/yarn at
33
+ # runtime (pnpm 11 needs Node >=22.13 via node:sqlite; the base ships Node 20).
34
+ # Direct npm-global installs are deterministic and need no network at runtime.
35
+ # pnpm is pinned to 9.x (the last line that supports Node 20).
36
+ RUN npm install -g pnpm@9 yarn typescript ts-node
37
+
38
+ # bun: installed under the agent home so ~/.bun has correct ownership
39
+ USER agent
40
+ RUN curl -fsSL https://bun.sh/install | bash
41
+ ENV PATH="/home/agent/.bun/bin:${PATH}"
42
+
43
+ WORKDIR /home/agent/workspace
@@ -0,0 +1,27 @@
1
+ # kanibako-template: Java, Kotlin, Maven (JVM toolchain)
2
+ # kanibako-template-check: java -version
3
+ # kanibako-template-check: kotlin -version
4
+ # kanibako-template-check: mvn -version
5
+ # JVM development template: Java, Kotlin, Maven.
6
+ # Built on top of any kanibako base image.
7
+ #
8
+ # Default base: kanibako-oci:latest. Override with:
9
+ # podman build --build-arg BASE_IMAGE=ghcr.io/doctorjei/kanibako-lxc:latest ...
10
+ ARG BASE_IMAGE=ghcr.io/doctorjei/kanibako-oci:latest
11
+ FROM $BASE_IMAGE
12
+
13
+ # Re-declare ARG after FROM so $BASE_IMAGE is in scope for RUN.
14
+ # Surfaces the actual base in build output (default or explicit).
15
+ ARG BASE_IMAGE
16
+ RUN echo "[kanibako template] Building jvm on BASE_IMAGE=$BASE_IMAGE"
17
+
18
+ USER root
19
+
20
+ RUN apt-get update && apt-get install -y --no-install-recommends \
21
+ default-jdk \
22
+ kotlin \
23
+ maven \
24
+ && rm -rf /var/lib/apt/lists/*
25
+
26
+ USER agent
27
+ WORKDIR /home/agent/workspace
@@ -0,0 +1,46 @@
1
+ # kanibako-template: C/C++, Rust, cross-compilation toolchain
2
+ # kanibako-template-check: gcc --version
3
+ # kanibako-template-check: clang --version
4
+ # kanibako-template-check: cmake --version
5
+ # kanibako-template-check: cargo --version
6
+ # Systems development template: C/C++, Rust, cross-compilation tools.
7
+ # Built on top of any kanibako base image.
8
+ #
9
+ # Default base: kanibako-oci:latest. Override with:
10
+ # podman build --build-arg BASE_IMAGE=ghcr.io/doctorjei/kanibako-lxc:latest ...
11
+ ARG BASE_IMAGE=ghcr.io/doctorjei/kanibako-oci:latest
12
+ FROM $BASE_IMAGE
13
+
14
+ # Re-declare ARG after FROM so $BASE_IMAGE is in scope for RUN.
15
+ # Surfaces the actual base in build output (default or explicit).
16
+ ARG BASE_IMAGE
17
+ RUN echo "[kanibako template] Building systems on BASE_IMAGE=$BASE_IMAGE"
18
+
19
+ USER root
20
+
21
+ # Additional archive tools
22
+ RUN apt-get update && apt-get install -y --no-install-recommends \
23
+ p7zip-full \
24
+ unrar-free \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
27
+ # C/C++ toolchain
28
+ RUN apt-get update && apt-get install -y --no-install-recommends \
29
+ gcc g++ clang llvm \
30
+ cmake make ninja-build meson \
31
+ gdb lldb \
32
+ nasm yasm \
33
+ && rm -rf /var/lib/apt/lists/*
34
+
35
+ # QEMU user-mode emulation for cross-compilation
36
+ RUN apt-get update && apt-get install -y --no-install-recommends \
37
+ qemu-user-static \
38
+ binfmt-support \
39
+ && rm -rf /var/lib/apt/lists/*
40
+
41
+ # Rust (installed as agent so ~/.cargo has correct ownership)
42
+ USER agent
43
+ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
44
+ ENV PATH="/home/agent/.cargo/bin:${PATH}"
45
+
46
+ WORKDIR /home/agent/workspace
File without changes
kanibako/crabs.py ADDED
@@ -0,0 +1,89 @@
1
+ """Crab YAML configuration: load, write, and resolve per-crab settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ from kanibako.config_io import dump_doc, load_doc
9
+
10
+ # Keys that live directly in [crab] as crab identity (not crab state).
11
+ IDENTITY_KEYS = frozenset({"name", "shell", "run_args"})
12
+
13
+
14
+ @dataclass
15
+ class CrabConfig:
16
+ """Per-crab configuration loaded from a crab YAML file.
17
+
18
+ Sections:
19
+ crab — identity (name, shell, run_args) plus crab-state knobs
20
+ (model, access, start_mode, autonomous, …)
21
+ env — raw env vars injected into container
22
+ shared — crab-level shared cache paths
23
+ """
24
+
25
+ name: str = ""
26
+ shell: str = "standard"
27
+ run_args: list[str] = field(default_factory=list)
28
+ state: dict[str, str] = field(default_factory=dict)
29
+ env: dict[str, str] = field(default_factory=dict)
30
+ shared_caches: dict[str, str] = field(default_factory=dict)
31
+ tweakcc: dict = field(default_factory=dict)
32
+
33
+
34
+ def crabs_dir(data_path: Path, paths_crabs: str = "crabs") -> Path:
35
+ """Return the crabs directory under *data_path*."""
36
+ return data_path / (paths_crabs or "crabs")
37
+
38
+
39
+ def crab_toml_path(
40
+ data_path: Path, crab_id: str, paths_crabs: str = "crabs",
41
+ ) -> Path:
42
+ """Return the path to a crab's config file."""
43
+ return crabs_dir(data_path, paths_crabs) / f"{crab_id}.yaml"
44
+
45
+
46
+ def load_crab_config(path: Path) -> CrabConfig:
47
+ """Read a crab config file and return a CrabConfig.
48
+
49
+ Returns defaults if the file does not exist.
50
+ """
51
+ cfg = CrabConfig()
52
+ if not path.exists():
53
+ return cfg
54
+
55
+ data = load_doc(path)
56
+
57
+ crab_sec = data.get("crab", {})
58
+ cfg.name = str(crab_sec.get("name", ""))
59
+ cfg.shell = str(crab_sec.get("shell", "standard"))
60
+ raw_args = crab_sec.get("run_args", [])
61
+ cfg.run_args = [str(a) for a in raw_args] if isinstance(raw_args, list) else []
62
+
63
+ cfg.state = {
64
+ k: str(v) for k, v in crab_sec.items() if k not in IDENTITY_KEYS
65
+ }
66
+ cfg.env = {k: str(v) for k, v in data.get("env", {}).items()}
67
+ cfg.shared_caches = {k: str(v) for k, v in data.get("shared", {}).items()}
68
+ cfg.tweakcc = dict(data.get("tweakcc", {}))
69
+
70
+ return cfg
71
+
72
+
73
+ def write_crab_config(path: Path, cfg: CrabConfig) -> None:
74
+ """Write a CrabConfig to a YAML file."""
75
+ crab_sec: dict = {
76
+ "name": cfg.name,
77
+ "shell": cfg.shell,
78
+ "run_args": list(cfg.run_args),
79
+ }
80
+ for k, v in cfg.state.items():
81
+ crab_sec[k] = v
82
+
83
+ data: dict = {
84
+ "crab": crab_sec,
85
+ "env": dict(cfg.env),
86
+ "shared": dict(cfg.shared_caches),
87
+ "tweakcc": dict(cfg.tweakcc),
88
+ }
89
+ dump_doc(path, data)
kanibako/errors.py ADDED
@@ -0,0 +1,33 @@
1
+ """Kanibako error hierarchy."""
2
+
3
+
4
+ class KanibakoError(Exception):
5
+ """Base exception for all kanibako errors."""
6
+
7
+
8
+ class ConfigError(KanibakoError):
9
+ """Configuration file missing or malformed."""
10
+
11
+
12
+ class ProjectError(KanibakoError):
13
+ """Project path does not exist or cannot be resolved."""
14
+
15
+
16
+ class ContainerError(KanibakoError):
17
+ """Container runtime or image operation failed."""
18
+
19
+
20
+ class ArchiveError(KanibakoError):
21
+ """Archive creation, extraction, or validation failed."""
22
+
23
+
24
+ class GitError(KanibakoError):
25
+ """Git check failed (uncommitted changes, unpushed commits, etc.)."""
26
+
27
+
28
+ class WorksetError(KanibakoError):
29
+ """Workset creation, loading, or manipulation failed."""
30
+
31
+
32
+ class UserCancelled(KanibakoError):
33
+ """User cancelled an interactive prompt."""
kanibako/freshness.py ADDED
@@ -0,0 +1,67 @@
1
+ """Non-blocking image freshness check: warn when a newer image is available."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from kanibako.container import ContainerRuntime
11
+ from kanibako.registry import get_remote_digest
12
+
13
+ _CACHE_TTL = 86400 # 24 hours
14
+
15
+
16
+ def check_image_freshness(runtime: ContainerRuntime, image: str, cache_path: Path) -> None:
17
+ """Compare local and remote digests; print a note to stderr if stale.
18
+
19
+ This function **never** raises — all exceptions are silently swallowed
20
+ so it cannot block container startup.
21
+ """
22
+ try:
23
+ _check(runtime, image, cache_path)
24
+ except Exception:
25
+ pass
26
+
27
+
28
+ def _check(runtime: ContainerRuntime, image: str, cache_path: Path) -> None:
29
+ local_digest = runtime.get_local_digest(image)
30
+ if local_digest is None:
31
+ return
32
+
33
+ remote_digest = _cached_remote_digest(image, cache_path)
34
+ if remote_digest is None:
35
+ return
36
+
37
+ if local_digest != remote_digest:
38
+ print(
39
+ f"Note: A newer version of {image} is available. "
40
+ f"Run 'kanibako rig rebuild' to update.",
41
+ file=sys.stderr,
42
+ )
43
+
44
+
45
+ def _cached_remote_digest(image: str, cache_path: Path) -> str | None:
46
+ """Return the remote digest, using a 24h file cache."""
47
+ cache_file = cache_path / "digest-cache.json"
48
+ now = time.time()
49
+
50
+ cache: dict = {}
51
+ if cache_file.is_file():
52
+ try:
53
+ cache = json.loads(cache_file.read_text())
54
+ except (json.JSONDecodeError, OSError):
55
+ cache = {}
56
+
57
+ entry = cache.get(image)
58
+ if entry and now - entry.get("ts", 0) < _CACHE_TTL:
59
+ return entry.get("digest")
60
+
61
+ digest = get_remote_digest(image)
62
+ if digest is not None:
63
+ cache[image] = {"digest": digest, "ts": now}
64
+ cache_path.mkdir(parents=True, exist_ok=True)
65
+ cache_file.write_text(json.dumps(cache))
66
+
67
+ return digest
kanibako/git.py ADDED
@@ -0,0 +1,114 @@
1
+ """Git checks: uncommitted/unpushed detection, metadata extraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from kanibako.errors import GitError
10
+
11
+
12
+ @dataclass
13
+ class GitMetadata:
14
+ """Information about a project's git state."""
15
+
16
+ branch: str
17
+ commit: str
18
+ remotes: list[tuple[str, str]] # (name, url)
19
+
20
+
21
+ def is_git_repo(path: Path) -> bool:
22
+ """Return True if *path* contains a .git directory."""
23
+ return (path / ".git").is_dir()
24
+
25
+
26
+ def check_uncommitted(path: Path) -> None:
27
+ """Raise GitError if there are uncommitted changes in *path*."""
28
+ result = subprocess.run(
29
+ ["git", "diff-index", "--quiet", "HEAD", "--"],
30
+ cwd=path,
31
+ capture_output=True,
32
+ )
33
+ if result.returncode != 0:
34
+ raise GitError(
35
+ "Uncommitted changes detected.\n"
36
+ "Commit your changes or use --allow-uncommitted to override."
37
+ )
38
+
39
+
40
+ def check_unpushed(path: Path) -> None:
41
+ """Raise GitError if there are unpushed commits on the current branch."""
42
+ # Get current branch
43
+ branch_result = subprocess.run(
44
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
45
+ cwd=path,
46
+ capture_output=True,
47
+ text=True,
48
+ )
49
+ if branch_result.returncode != 0:
50
+ return # Cannot determine branch; skip check
51
+
52
+ # Check for upstream
53
+ upstream_result = subprocess.run(
54
+ ["git", "rev-parse", "--abbrev-ref", "@{upstream}"],
55
+ cwd=path,
56
+ capture_output=True,
57
+ text=True,
58
+ )
59
+ if upstream_result.returncode != 0:
60
+ return # No upstream; skip check
61
+
62
+ upstream = upstream_result.stdout.strip()
63
+ count_result = subprocess.run(
64
+ ["git", "rev-list", f"{upstream}..HEAD", "--count"],
65
+ cwd=path,
66
+ capture_output=True,
67
+ text=True,
68
+ )
69
+ if count_result.returncode != 0:
70
+ return
71
+
72
+ count = int(count_result.stdout.strip())
73
+ if count > 0:
74
+ raise GitError(
75
+ f"{count} unpushed commit(s) detected.\n"
76
+ "Push your changes or use --allow-unpushed to override."
77
+ )
78
+
79
+
80
+ def get_metadata(path: Path) -> GitMetadata | None:
81
+ """Extract git branch, HEAD SHA, and fetch remotes. Returns None on failure."""
82
+ branch_result = subprocess.run(
83
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
84
+ cwd=path,
85
+ capture_output=True,
86
+ text=True,
87
+ )
88
+ commit_result = subprocess.run(
89
+ ["git", "rev-parse", "HEAD"],
90
+ cwd=path,
91
+ capture_output=True,
92
+ text=True,
93
+ )
94
+ if branch_result.returncode != 0 or commit_result.returncode != 0:
95
+ return None
96
+
97
+ remote_result = subprocess.run(
98
+ ["git", "remote", "-v"],
99
+ cwd=path,
100
+ capture_output=True,
101
+ text=True,
102
+ )
103
+ remotes: list[tuple[str, str]] = []
104
+ for line in remote_result.stdout.splitlines():
105
+ if "(fetch)" in line:
106
+ parts = line.split()
107
+ if len(parts) >= 2:
108
+ remotes.append((parts[0], parts[1]))
109
+
110
+ return GitMetadata(
111
+ branch=branch_result.stdout.strip(),
112
+ commit=commit_result.stdout.strip(),
113
+ remotes=remotes,
114
+ )