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,574 @@
|
|
|
1
|
+
"""Migrate and cross-mode conversion logic for kanibako box."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import cast
|
|
10
|
+
|
|
11
|
+
from kanibako.config import config_file_path, load_config
|
|
12
|
+
from kanibako.names import assign_name, unregister_name
|
|
13
|
+
from kanibako.paths import (
|
|
14
|
+
ProjectMode,
|
|
15
|
+
WorksetSpec,
|
|
16
|
+
_ensure_human_vault_symlink,
|
|
17
|
+
_find_workset_for_path,
|
|
18
|
+
_remove_human_vault_symlink,
|
|
19
|
+
_remove_project_vault_symlink,
|
|
20
|
+
_resolve_local_dir,
|
|
21
|
+
xdg,
|
|
22
|
+
detect_project_mode,
|
|
23
|
+
load_std_paths,
|
|
24
|
+
resolve_standalone_project,
|
|
25
|
+
resolve_project,
|
|
26
|
+
resolve_workset_project,
|
|
27
|
+
)
|
|
28
|
+
from kanibako.utils import confirm_prompt, project_hash
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_migrate(args: argparse.Namespace) -> int:
|
|
32
|
+
import os
|
|
33
|
+
|
|
34
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
35
|
+
config = load_config(config_file)
|
|
36
|
+
std = load_std_paths(config)
|
|
37
|
+
|
|
38
|
+
# Cross-mode conversion.
|
|
39
|
+
if getattr(args, "to_mode", None) is not None:
|
|
40
|
+
return _run_convert(args, std, config)
|
|
41
|
+
|
|
42
|
+
# Same-mode path remap: old_path is required.
|
|
43
|
+
if args.old_path is None:
|
|
44
|
+
print("Error: old_path is required for path remap (use --to for mode conversion).", file=sys.stderr)
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
# Resolve paths — old path may no longer exist, so use str directly.
|
|
48
|
+
old_path = Path(args.old_path).resolve()
|
|
49
|
+
new_path = Path(args.new_path).resolve() if args.new_path else Path(os.getcwd()).resolve()
|
|
50
|
+
|
|
51
|
+
# Validate: paths must differ.
|
|
52
|
+
if old_path == new_path:
|
|
53
|
+
print("Error: old and new paths are the same.", file=sys.stderr)
|
|
54
|
+
return 1
|
|
55
|
+
|
|
56
|
+
# Validate: new path must exist as a directory.
|
|
57
|
+
if not new_path.is_dir():
|
|
58
|
+
print(f"Error: new path does not exist as a directory: {new_path}", file=sys.stderr)
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
new_hash = project_hash(str(new_path))
|
|
62
|
+
|
|
63
|
+
# Find old project directory.
|
|
64
|
+
old_name, old_project_dir = _resolve_local_dir(
|
|
65
|
+
std.data_path, str(old_path), std.boxes,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Validate: old project data must exist.
|
|
69
|
+
if not old_project_dir.is_dir():
|
|
70
|
+
print(
|
|
71
|
+
f"Error: no project data found for old path: {old_path}",
|
|
72
|
+
file=sys.stderr,
|
|
73
|
+
)
|
|
74
|
+
print(f" (expected: {old_project_dir})", file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
# Find or assign new project directory.
|
|
78
|
+
new_name, new_project_dir = _resolve_local_dir(
|
|
79
|
+
std.data_path, str(new_path), std.boxes,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Validate: new project data must NOT already exist.
|
|
83
|
+
if new_project_dir.is_dir():
|
|
84
|
+
print(
|
|
85
|
+
f"Error: project data already exists for new path: {new_path}",
|
|
86
|
+
file=sys.stderr,
|
|
87
|
+
)
|
|
88
|
+
print(" Use 'kanibako box rm --purge' to remove it first.", file=sys.stderr)
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
# Warn if lock file exists.
|
|
92
|
+
lock_file = old_project_dir / ".kanibako.lock"
|
|
93
|
+
if lock_file.exists():
|
|
94
|
+
print(
|
|
95
|
+
"Warning: lock file found — a container may be running for this project.",
|
|
96
|
+
file=sys.stderr,
|
|
97
|
+
)
|
|
98
|
+
if not args.force:
|
|
99
|
+
try:
|
|
100
|
+
confirm_prompt("Continue anyway? Type 'yes' to confirm: ")
|
|
101
|
+
except Exception:
|
|
102
|
+
print("Aborted.")
|
|
103
|
+
return 2
|
|
104
|
+
|
|
105
|
+
# Confirm with user.
|
|
106
|
+
if not args.force:
|
|
107
|
+
print("Migrate project data:")
|
|
108
|
+
print(f" from: {old_path}")
|
|
109
|
+
print(f" to: {new_path}")
|
|
110
|
+
print()
|
|
111
|
+
try:
|
|
112
|
+
confirm_prompt("Type 'yes' to confirm: ")
|
|
113
|
+
except Exception:
|
|
114
|
+
print("Aborted.")
|
|
115
|
+
return 2
|
|
116
|
+
|
|
117
|
+
# Remove old human-friendly symlink before rename.
|
|
118
|
+
human_vault_dir = std.data_path / config.paths_vault
|
|
119
|
+
_remove_human_vault_symlink(human_vault_dir, old_project_dir / "vault")
|
|
120
|
+
|
|
121
|
+
# Update names.yaml: unregister old name, assign new name.
|
|
122
|
+
if old_name:
|
|
123
|
+
unregister_name(std.data_path, old_name)
|
|
124
|
+
new_name = assign_name(std.data_path, str(new_path))
|
|
125
|
+
new_project_dir = std.boxes / new_name
|
|
126
|
+
|
|
127
|
+
# Rename project directory (includes home/ inside it).
|
|
128
|
+
old_project_dir.rename(new_project_dir)
|
|
129
|
+
|
|
130
|
+
# Update workspace path in project.yaml.
|
|
131
|
+
from kanibako.config import read_project_meta, write_project_meta
|
|
132
|
+
project_toml = new_project_dir / "project.yaml"
|
|
133
|
+
meta = read_project_meta(project_toml)
|
|
134
|
+
if meta:
|
|
135
|
+
write_project_meta(
|
|
136
|
+
project_toml,
|
|
137
|
+
mode=meta["mode"],
|
|
138
|
+
layout=meta["layout"],
|
|
139
|
+
workspace=str(new_path),
|
|
140
|
+
shell=str(new_project_dir / "shell"),
|
|
141
|
+
vault_ro=meta.get("vault_ro", ""),
|
|
142
|
+
vault_rw=meta.get("vault_rw", ""),
|
|
143
|
+
enable_vault=meta.get("enable_vault", True),
|
|
144
|
+
group_auth=bool(meta.get("group_auth", True)),
|
|
145
|
+
metadata=str(new_project_dir),
|
|
146
|
+
project_hash=new_hash,
|
|
147
|
+
global_shared=meta.get("global_shared", ""),
|
|
148
|
+
local_shared=meta.get("local_shared", ""),
|
|
149
|
+
name=new_name,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Create new human-friendly symlink (best-effort).
|
|
153
|
+
vault_parent = new_project_dir / "vault"
|
|
154
|
+
if vault_parent.is_dir():
|
|
155
|
+
_ensure_human_vault_symlink(human_vault_dir, new_path, vault_parent)
|
|
156
|
+
|
|
157
|
+
print("Migrated project data:")
|
|
158
|
+
print(f" from: {old_path} ({old_name})")
|
|
159
|
+
print(f" to: {new_path} ({new_name})")
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# -- Cross-mode conversion helpers --
|
|
164
|
+
|
|
165
|
+
def _run_convert(args: argparse.Namespace, std, config) -> int:
|
|
166
|
+
"""Dispatch cross-mode conversion based on --to flag."""
|
|
167
|
+
import os
|
|
168
|
+
|
|
169
|
+
to_mode_str = args.to_mode
|
|
170
|
+
|
|
171
|
+
# Convert TO workset: separate code path.
|
|
172
|
+
if to_mode_str == "workset":
|
|
173
|
+
return _convert_to_workset(args, std, config)
|
|
174
|
+
|
|
175
|
+
# Resolve project path (positional arg or cwd).
|
|
176
|
+
raw_path = args.old_path or os.getcwd()
|
|
177
|
+
project_path = Path(raw_path).resolve()
|
|
178
|
+
|
|
179
|
+
if not project_path.is_dir():
|
|
180
|
+
print(f"Error: project path does not exist: {project_path}", file=sys.stderr)
|
|
181
|
+
return 1
|
|
182
|
+
|
|
183
|
+
# Detect current mode.
|
|
184
|
+
current_mode = detect_project_mode(project_path, std, config).mode
|
|
185
|
+
|
|
186
|
+
# Convert FROM workset: separate code path.
|
|
187
|
+
if current_mode == ProjectMode.workset:
|
|
188
|
+
return _convert_from_workset(args, project_path, std, config)
|
|
189
|
+
|
|
190
|
+
# Parse target mode.
|
|
191
|
+
target_mode = ProjectMode.standalone if to_mode_str == "standalone" else ProjectMode.default
|
|
192
|
+
|
|
193
|
+
if current_mode == target_mode:
|
|
194
|
+
print(f"Error: project is already in {current_mode.value} mode.", file=sys.stderr)
|
|
195
|
+
return 1
|
|
196
|
+
|
|
197
|
+
# Resolve current project paths.
|
|
198
|
+
# default<->standalone: architectural boundary (centralized vs in-workspace metadata), not re-rooting — kept distinct (#71 B2).
|
|
199
|
+
if current_mode == ProjectMode.default:
|
|
200
|
+
proj = resolve_project(std, config, project_dir=str(project_path), initialize=False)
|
|
201
|
+
else:
|
|
202
|
+
proj = resolve_standalone_project(std, config, project_dir=str(project_path), initialize=False)
|
|
203
|
+
|
|
204
|
+
# Check that project data exists.
|
|
205
|
+
if not proj.metadata_path.is_dir():
|
|
206
|
+
print(f"Error: no project data found for {project_path}", file=sys.stderr)
|
|
207
|
+
return 1
|
|
208
|
+
|
|
209
|
+
# Lock file warning.
|
|
210
|
+
lock_file = proj.metadata_path / ".kanibako.lock"
|
|
211
|
+
if lock_file.exists():
|
|
212
|
+
print(
|
|
213
|
+
"Warning: lock file found — a container may be running for this project.",
|
|
214
|
+
file=sys.stderr,
|
|
215
|
+
)
|
|
216
|
+
if not args.force:
|
|
217
|
+
print("Aborted.")
|
|
218
|
+
return 2
|
|
219
|
+
|
|
220
|
+
# Confirm with user.
|
|
221
|
+
if not args.force:
|
|
222
|
+
print(f"Convert project to {target_mode.value} mode:")
|
|
223
|
+
print(f" project: {project_path}")
|
|
224
|
+
print(f" from: {current_mode.value}")
|
|
225
|
+
print(f" to: {target_mode.value}")
|
|
226
|
+
print()
|
|
227
|
+
try:
|
|
228
|
+
confirm_prompt("Type 'yes' to confirm: ")
|
|
229
|
+
except Exception:
|
|
230
|
+
print("Aborted.")
|
|
231
|
+
return 2
|
|
232
|
+
|
|
233
|
+
# Dispatch.
|
|
234
|
+
# default<->standalone: architectural boundary (centralized vs in-workspace metadata), not re-rooting — kept distinct (#71 B2).
|
|
235
|
+
if target_mode == ProjectMode.standalone:
|
|
236
|
+
_convert_local_to_standalone(project_path, std, config, proj)
|
|
237
|
+
else:
|
|
238
|
+
_convert_standalone_to_local(project_path, std, config, proj)
|
|
239
|
+
|
|
240
|
+
print(f"Converted project to {target_mode.value} mode:")
|
|
241
|
+
print(f" project: {project_path}")
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _convert_local_to_standalone(project_path, std, config, proj):
|
|
246
|
+
"""Convert a default-mode project to standalone layout."""
|
|
247
|
+
from kanibako.utils import write_project_gitignore
|
|
248
|
+
|
|
249
|
+
dst_metadata = project_path / ".kanibako"
|
|
250
|
+
dst_shell = dst_metadata / "shell"
|
|
251
|
+
|
|
252
|
+
# Copy metadata (excluding lock file and shell/ directory).
|
|
253
|
+
shutil.copytree(
|
|
254
|
+
proj.metadata_path, dst_metadata,
|
|
255
|
+
ignore=shutil.ignore_patterns(".kanibako.lock", "shell"),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Copy shell.
|
|
259
|
+
if proj.shell_path.is_dir():
|
|
260
|
+
shutil.copytree(proj.shell_path, dst_shell)
|
|
261
|
+
|
|
262
|
+
# Write .gitignore entries for .kanibako/.
|
|
263
|
+
write_project_gitignore(project_path)
|
|
264
|
+
|
|
265
|
+
# Write vault .gitignore if vault exists but gitignore doesn't.
|
|
266
|
+
vault_dir = project_path / "vault"
|
|
267
|
+
if vault_dir.is_dir():
|
|
268
|
+
vault_gitignore = vault_dir / ".gitignore"
|
|
269
|
+
if not vault_gitignore.exists():
|
|
270
|
+
vault_gitignore.write_text("rw/\n")
|
|
271
|
+
|
|
272
|
+
# Remove vault symlinks before cleaning up old default-mode data.
|
|
273
|
+
human_vault_dir = std.data_path / config.paths_vault
|
|
274
|
+
_remove_human_vault_symlink(human_vault_dir, proj.metadata_path / "vault")
|
|
275
|
+
_remove_project_vault_symlink(project_path)
|
|
276
|
+
|
|
277
|
+
# Unregister the default-mode project name.
|
|
278
|
+
if proj.name:
|
|
279
|
+
unregister_name(std.data_path, proj.name)
|
|
280
|
+
|
|
281
|
+
# Clean up old default-mode data.
|
|
282
|
+
shutil.rmtree(proj.metadata_path)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _convert_standalone_to_local(project_path, std, config, proj):
|
|
286
|
+
"""Convert a standalone project to default-mode layout."""
|
|
287
|
+
# Assign a name for the new default-mode project.
|
|
288
|
+
project_name = assign_name(std.data_path, str(project_path))
|
|
289
|
+
settings_base = std.boxes
|
|
290
|
+
dst_project = settings_base / project_name
|
|
291
|
+
|
|
292
|
+
# Copy metadata (excluding lock file and shell/).
|
|
293
|
+
shutil.copytree(
|
|
294
|
+
proj.metadata_path, dst_project,
|
|
295
|
+
ignore=shutil.ignore_patterns(".kanibako.lock", "shell"),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Copy shell into the settings dir.
|
|
299
|
+
if proj.shell_path.is_dir():
|
|
300
|
+
dst_shell = dst_project / "shell"
|
|
301
|
+
shutil.copytree(proj.shell_path, dst_shell)
|
|
302
|
+
|
|
303
|
+
# Create human-friendly vault symlink if robust layout.
|
|
304
|
+
vault_parent = dst_project / "vault"
|
|
305
|
+
if vault_parent.is_dir():
|
|
306
|
+
human_vault_dir = std.data_path / config.paths_vault
|
|
307
|
+
_ensure_human_vault_symlink(human_vault_dir, project_path, vault_parent)
|
|
308
|
+
|
|
309
|
+
# Clean up old standalone data.
|
|
310
|
+
shutil.rmtree(proj.metadata_path)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# -- Workset conversion helpers --
|
|
314
|
+
|
|
315
|
+
def _copy_into_workset(ws, proj_name, src_proj, source_path, source_mode, copy_workspace) -> None:
|
|
316
|
+
"""Re-root a resolved project into a workset group (register + copy).
|
|
317
|
+
|
|
318
|
+
Shared by migrate's ``_convert_to_workset`` and duplicate's
|
|
319
|
+
``_duplicate_to_workset`` — both place a project under the same workset
|
|
320
|
+
group roots (``projects_dir`` for metadata/shell, ``workspaces_dir`` for the
|
|
321
|
+
tree). The only per-caller variation is whether the workspace tree is copied
|
|
322
|
+
(migrate: ``not in_place``; duplicate: ``not bare``); the standalone
|
|
323
|
+
``.kanibako`` ignore filter is uniform. Callers handle source-side cleanup.
|
|
324
|
+
"""
|
|
325
|
+
from kanibako.workset import add_project
|
|
326
|
+
|
|
327
|
+
# Register project in workset (creates skeleton dirs).
|
|
328
|
+
add_project(ws, proj_name, source_path)
|
|
329
|
+
|
|
330
|
+
# Copy metadata (excluding lock, breadcrumb, and home/).
|
|
331
|
+
dst_project = ws.projects_dir / proj_name
|
|
332
|
+
shutil.copytree(
|
|
333
|
+
src_proj.metadata_path, dst_project,
|
|
334
|
+
ignore=shutil.ignore_patterns(".kanibako.lock", "shell"),
|
|
335
|
+
dirs_exist_ok=True,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Copy home.
|
|
339
|
+
if src_proj.shell_path.is_dir():
|
|
340
|
+
dst_home = dst_project / "shell"
|
|
341
|
+
shutil.copytree(src_proj.shell_path, dst_home, dirs_exist_ok=True)
|
|
342
|
+
|
|
343
|
+
# Copy workspace tree into the workset (exclude standalone metadata).
|
|
344
|
+
if copy_workspace:
|
|
345
|
+
dst_workspace = ws.workspaces_dir / proj_name
|
|
346
|
+
ignore = None
|
|
347
|
+
if source_mode == ProjectMode.standalone:
|
|
348
|
+
ignore = shutil.ignore_patterns(".kanibako")
|
|
349
|
+
shutil.copytree(source_path, dst_workspace, ignore=ignore, dirs_exist_ok=True)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _convert_to_workset(args, std, config) -> int:
|
|
353
|
+
"""Convert a default-mode or standalone project into a workset."""
|
|
354
|
+
import os
|
|
355
|
+
|
|
356
|
+
from kanibako.workset import list_worksets, load_workset
|
|
357
|
+
|
|
358
|
+
ws_name = getattr(args, "workset", None)
|
|
359
|
+
if not ws_name:
|
|
360
|
+
print("Error: --workset is required when converting to workset mode.", file=sys.stderr)
|
|
361
|
+
return 1
|
|
362
|
+
|
|
363
|
+
# Load target workset.
|
|
364
|
+
registry = list_worksets(std)
|
|
365
|
+
if ws_name not in registry:
|
|
366
|
+
print(f"Error: workset '{ws_name}' not found.", file=sys.stderr)
|
|
367
|
+
return 1
|
|
368
|
+
ws = load_workset(registry[ws_name])
|
|
369
|
+
|
|
370
|
+
# Resolve source project.
|
|
371
|
+
raw_path = args.old_path or os.getcwd()
|
|
372
|
+
project_path = Path(raw_path).resolve()
|
|
373
|
+
|
|
374
|
+
if not project_path.is_dir():
|
|
375
|
+
print(f"Error: project path does not exist: {project_path}", file=sys.stderr)
|
|
376
|
+
return 1
|
|
377
|
+
|
|
378
|
+
current_mode = detect_project_mode(project_path, std, config).mode
|
|
379
|
+
if current_mode == ProjectMode.workset:
|
|
380
|
+
print("Error: project is already in workset mode.", file=sys.stderr)
|
|
381
|
+
return 1
|
|
382
|
+
|
|
383
|
+
# Determine project name.
|
|
384
|
+
proj_name = getattr(args, "project_name", None) or project_path.name
|
|
385
|
+
|
|
386
|
+
# Validate name not taken.
|
|
387
|
+
for p in ws.projects:
|
|
388
|
+
if p.name == proj_name:
|
|
389
|
+
print(f"Error: project '{proj_name}' already exists in workset '{ws_name}'.", file=sys.stderr)
|
|
390
|
+
return 1
|
|
391
|
+
|
|
392
|
+
# Resolve source paths.
|
|
393
|
+
# default<->standalone: architectural boundary (centralized vs in-workspace metadata), not re-rooting — kept distinct (#71 B2).
|
|
394
|
+
if current_mode == ProjectMode.default:
|
|
395
|
+
src_proj = resolve_project(std, config, project_dir=str(project_path), initialize=False)
|
|
396
|
+
else:
|
|
397
|
+
src_proj = resolve_standalone_project(std, config, project_dir=str(project_path), initialize=False)
|
|
398
|
+
|
|
399
|
+
if not src_proj.metadata_path.is_dir():
|
|
400
|
+
print(f"Error: no project data found for {project_path}", file=sys.stderr)
|
|
401
|
+
return 1
|
|
402
|
+
|
|
403
|
+
# Lock file warning.
|
|
404
|
+
lock_file = src_proj.metadata_path / ".kanibako.lock"
|
|
405
|
+
if lock_file.exists():
|
|
406
|
+
print(
|
|
407
|
+
"Warning: lock file found — a container may be running for this project.",
|
|
408
|
+
file=sys.stderr,
|
|
409
|
+
)
|
|
410
|
+
if not args.force:
|
|
411
|
+
print("Aborted.")
|
|
412
|
+
return 2
|
|
413
|
+
|
|
414
|
+
in_place = getattr(args, "in_place", False)
|
|
415
|
+
|
|
416
|
+
# Confirm.
|
|
417
|
+
if not args.force:
|
|
418
|
+
action = "in-place (workspace stays)" if in_place else "move workspace into workset"
|
|
419
|
+
print(f"Convert project to workset mode ({action}):")
|
|
420
|
+
print(f" project: {project_path}")
|
|
421
|
+
print(f" workset: {ws_name}")
|
|
422
|
+
print(f" name: {proj_name}")
|
|
423
|
+
print()
|
|
424
|
+
try:
|
|
425
|
+
confirm_prompt("Type 'yes' to confirm: ")
|
|
426
|
+
except Exception:
|
|
427
|
+
print("Aborted.")
|
|
428
|
+
return 2
|
|
429
|
+
|
|
430
|
+
# Re-root the project into the workset group (move workspace unless --in-place).
|
|
431
|
+
_copy_into_workset(ws, proj_name, src_proj, project_path, current_mode, copy_workspace=not in_place)
|
|
432
|
+
|
|
433
|
+
# Remove vault symlinks before cleaning up old metadata.
|
|
434
|
+
if current_mode == ProjectMode.default:
|
|
435
|
+
human_vault_dir = std.data_path / config.paths_vault
|
|
436
|
+
_remove_human_vault_symlink(human_vault_dir, src_proj.metadata_path / "vault")
|
|
437
|
+
# Unregister old default-mode name.
|
|
438
|
+
if src_proj.name:
|
|
439
|
+
unregister_name(std.data_path, src_proj.name)
|
|
440
|
+
_remove_project_vault_symlink(project_path)
|
|
441
|
+
|
|
442
|
+
# Clean up old metadata.
|
|
443
|
+
shutil.rmtree(src_proj.metadata_path)
|
|
444
|
+
if src_proj.shell_path.is_dir():
|
|
445
|
+
shutil.rmtree(src_proj.shell_path)
|
|
446
|
+
|
|
447
|
+
print("Converted project to workset mode:")
|
|
448
|
+
print(f" workset: {ws_name}/{proj_name}")
|
|
449
|
+
return 0
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _convert_from_workset(args, project_path, std, config) -> int:
|
|
453
|
+
"""Convert a workset project to default or standalone mode."""
|
|
454
|
+
to_mode_str = args.to_mode
|
|
455
|
+
|
|
456
|
+
ws, proj_name = _find_workset_for_path(project_path, std)
|
|
457
|
+
if proj_name is None:
|
|
458
|
+
print("Error: not inside a specific project workspace.", file=sys.stderr)
|
|
459
|
+
return 1
|
|
460
|
+
src_proj = resolve_workset_project(
|
|
461
|
+
WorksetSpec.from_workset(ws), proj_name, std, config, initialize=False,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
target_mode = ProjectMode.standalone if to_mode_str == "standalone" else ProjectMode.default
|
|
465
|
+
|
|
466
|
+
if not src_proj.metadata_path.is_dir():
|
|
467
|
+
print(f"Error: no project data found for {project_path}", file=sys.stderr)
|
|
468
|
+
return 1
|
|
469
|
+
|
|
470
|
+
# Lock file warning.
|
|
471
|
+
lock_file = src_proj.metadata_path / ".kanibako.lock"
|
|
472
|
+
if lock_file.exists():
|
|
473
|
+
print(
|
|
474
|
+
"Warning: lock file found — a container may be running for this project.",
|
|
475
|
+
file=sys.stderr,
|
|
476
|
+
)
|
|
477
|
+
if not args.force:
|
|
478
|
+
print("Aborted.")
|
|
479
|
+
return 2
|
|
480
|
+
|
|
481
|
+
# Determine destination path: use source_path from workset project.
|
|
482
|
+
found_proj = None
|
|
483
|
+
for p in ws.projects:
|
|
484
|
+
if p.name == proj_name:
|
|
485
|
+
found_proj = p
|
|
486
|
+
break
|
|
487
|
+
dest_path = found_proj.source_path if found_proj else project_path
|
|
488
|
+
|
|
489
|
+
# Confirm.
|
|
490
|
+
if not args.force:
|
|
491
|
+
print(f"Convert workset project to {target_mode.value} mode:")
|
|
492
|
+
print(f" workset: {ws.name}/{proj_name}")
|
|
493
|
+
print(f" target: {dest_path}")
|
|
494
|
+
print()
|
|
495
|
+
try:
|
|
496
|
+
confirm_prompt("Type 'yes' to confirm: ")
|
|
497
|
+
except Exception:
|
|
498
|
+
print("Aborted.")
|
|
499
|
+
return 2
|
|
500
|
+
|
|
501
|
+
# Target layout differs per mode; default<->standalone: architectural boundary (centralized vs in-workspace metadata), not re-rooting — kept distinct (#71 B2).
|
|
502
|
+
if target_mode == ProjectMode.default:
|
|
503
|
+
_convert_ws_to_local(src_proj, dest_path, std, config)
|
|
504
|
+
else:
|
|
505
|
+
_convert_ws_to_standalone(src_proj, dest_path)
|
|
506
|
+
|
|
507
|
+
# Move workspace from workset to destination if it exists and differs.
|
|
508
|
+
ws_workspace = ws.workspaces_dir / proj_name
|
|
509
|
+
in_place = getattr(args, "in_place", False)
|
|
510
|
+
if not in_place and ws_workspace.is_dir() and ws_workspace != dest_path:
|
|
511
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
512
|
+
shutil.copytree(ws_workspace, dest_path, dirs_exist_ok=True)
|
|
513
|
+
|
|
514
|
+
# Remove workset registration + workset dirs.
|
|
515
|
+
from kanibako.workset import Workset, remove_project
|
|
516
|
+
|
|
517
|
+
remove_project(cast("Workset", ws), proj_name, remove_files=True)
|
|
518
|
+
|
|
519
|
+
print(f"Converted project to {target_mode.value} mode:")
|
|
520
|
+
print(f" project: {dest_path}")
|
|
521
|
+
return 0
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _convert_ws_to_local(src_proj, dest_path, std, config):
|
|
525
|
+
"""Copy workset project metadata into default-mode layout."""
|
|
526
|
+
# Assign a name for the new default-mode project.
|
|
527
|
+
project_name = assign_name(std.data_path, str(dest_path))
|
|
528
|
+
projects_base = std.boxes
|
|
529
|
+
dst_project = projects_base / project_name
|
|
530
|
+
|
|
531
|
+
# Copy metadata (excluding lock and home/).
|
|
532
|
+
shutil.copytree(
|
|
533
|
+
src_proj.metadata_path, dst_project,
|
|
534
|
+
ignore=shutil.ignore_patterns(".kanibako.lock", "shell"),
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Copy home.
|
|
538
|
+
if src_proj.shell_path.is_dir():
|
|
539
|
+
dst_home = dst_project / "shell"
|
|
540
|
+
shutil.copytree(src_proj.shell_path, dst_home)
|
|
541
|
+
|
|
542
|
+
# Create human-friendly vault symlink if robust layout.
|
|
543
|
+
vault_parent = dst_project / "vault"
|
|
544
|
+
if vault_parent.is_dir():
|
|
545
|
+
human_vault_dir = std.data_path / config.paths_vault
|
|
546
|
+
_ensure_human_vault_symlink(human_vault_dir, dest_path, vault_parent)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _convert_ws_to_standalone(src_proj, dest_path):
|
|
550
|
+
"""Copy workset project metadata into standalone layout."""
|
|
551
|
+
from kanibako.utils import write_project_gitignore
|
|
552
|
+
|
|
553
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
554
|
+
dst_metadata = dest_path / ".kanibako"
|
|
555
|
+
dst_shell = dst_metadata / "shell"
|
|
556
|
+
|
|
557
|
+
# Copy metadata (excluding lock and shell/).
|
|
558
|
+
shutil.copytree(
|
|
559
|
+
src_proj.metadata_path, dst_metadata,
|
|
560
|
+
ignore=shutil.ignore_patterns(".kanibako.lock", "shell"),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Copy shell.
|
|
564
|
+
if src_proj.shell_path.is_dir():
|
|
565
|
+
shutil.copytree(src_proj.shell_path, dst_shell)
|
|
566
|
+
|
|
567
|
+
write_project_gitignore(dest_path)
|
|
568
|
+
|
|
569
|
+
# Write vault .gitignore if vault exists.
|
|
570
|
+
vault_dir = dest_path / "vault"
|
|
571
|
+
if vault_dir.is_dir():
|
|
572
|
+
vault_gitignore = vault_dir / ".gitignore"
|
|
573
|
+
if not vault_gitignore.exists():
|
|
574
|
+
vault_gitignore.write_text("rw/\n")
|