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