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,552 @@
1
+ """kanibako workset: create, manage, and inspect working sets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from kanibako.config import config_file_path, load_config
10
+ from kanibako.errors import WorksetError
11
+ from kanibako.paths import xdg, load_std_paths
12
+ from kanibako.utils import confirm_prompt
13
+ from kanibako.workset import (
14
+ DEFAULT_WORKSET_ALIAS,
15
+ DEFAULT_WORKSET_ID,
16
+ _write_workset_toml,
17
+ add_project,
18
+ create_workset,
19
+ delete_workset,
20
+ list_worksets,
21
+ load_workset,
22
+ remove_project,
23
+ resolve_workset_name,
24
+ )
25
+
26
+
27
+ def add_parser(subparsers: argparse._SubParsersAction) -> None:
28
+ p = subparsers.add_parser(
29
+ "workset",
30
+ help="Working set commands (create, list, info, rm, config, connect, disconnect)",
31
+ description="Create and manage working sets of related projects.",
32
+ )
33
+ ws_sub = p.add_subparsers(dest="workset_command", metavar="COMMAND")
34
+
35
+ # kanibako workset create [path] [--name NAME] [--standalone] [--image IMAGE]
36
+ # [--no-vault] [--distinct-auth]
37
+ create_p = ws_sub.add_parser(
38
+ "create",
39
+ help="Create a new working set",
40
+ description="Create a new working set directory and register it globally.",
41
+ )
42
+ create_p.add_argument(
43
+ "path", nargs="?", default=None,
44
+ help="Root directory for the working set (default: cwd)",
45
+ )
46
+ create_p.add_argument(
47
+ "--name", default=None,
48
+ help="Name for the working set (default: directory basename)",
49
+ )
50
+ create_p.add_argument(
51
+ "--standalone", action="store_true",
52
+ help="Use standalone mode for projects in this working set",
53
+ )
54
+ create_p.add_argument(
55
+ "-i", "--image", default=None,
56
+ help="Container image to use for projects in this working set",
57
+ )
58
+ create_p.add_argument(
59
+ "--no-vault", action="store_true",
60
+ help="Disable vault directories",
61
+ )
62
+ create_p.add_argument(
63
+ "--distinct-auth", action="store_true",
64
+ help="Use distinct credentials (no sync from host)",
65
+ )
66
+ create_p.set_defaults(func=run_create)
67
+
68
+ # kanibako workset list / ls (default)
69
+ list_p = ws_sub.add_parser(
70
+ "list",
71
+ aliases=["ls"],
72
+ help="List all registered working sets (default)",
73
+ description="Show all registered working sets.",
74
+ )
75
+ list_p.add_argument(
76
+ "-q", "--quiet", action="store_true",
77
+ help="Print only working set names, one per line",
78
+ )
79
+ list_p.set_defaults(func=run_list)
80
+
81
+ # kanibako workset rm <workset> [--purge] [--force]
82
+ rm_p = ws_sub.add_parser(
83
+ "rm",
84
+ aliases=["delete"],
85
+ help="Unregister a working set",
86
+ description="Unregister a working set and optionally remove its files.",
87
+ )
88
+ rm_p.add_argument("name", help="Name of the working set to remove")
89
+ rm_p.add_argument(
90
+ "--purge", action="store_true",
91
+ help="Also remove the working set directory tree",
92
+ )
93
+ rm_p.add_argument(
94
+ "--force", action="store_true", help="Skip confirmation prompt",
95
+ )
96
+ rm_p.set_defaults(func=run_rm)
97
+
98
+ # kanibako workset connect <workset> [source] [--name N]
99
+ connect_p = ws_sub.add_parser(
100
+ "connect",
101
+ help="Add a project to a working set",
102
+ description="Add a project to an existing working set.",
103
+ )
104
+ connect_p.add_argument("workset", help="Name of the working set")
105
+ connect_p.add_argument(
106
+ "source", nargs="?", default=None,
107
+ help="Source project directory (default: current directory)",
108
+ )
109
+ connect_p.add_argument(
110
+ "--name", dest="project_name", default=None,
111
+ help="Project name within the working set (default: directory basename)",
112
+ )
113
+ connect_p.set_defaults(func=run_connect)
114
+
115
+ # kanibako workset disconnect <workset> <project> [--force]
116
+ disconnect_p = ws_sub.add_parser(
117
+ "disconnect",
118
+ help="Remove a project from a working set",
119
+ description="Remove a project from a working set and optionally delete its files.",
120
+ )
121
+ disconnect_p.add_argument("workset", help="Name of the working set")
122
+ disconnect_p.add_argument("project", help="Name of the project to remove")
123
+ disconnect_p.add_argument(
124
+ "--remove-files", action="store_true",
125
+ help="Also remove per-project directories",
126
+ )
127
+ disconnect_p.add_argument(
128
+ "--force", action="store_true", help="Skip confirmation prompt",
129
+ )
130
+ disconnect_p.set_defaults(func=run_disconnect)
131
+
132
+ # kanibako workset info / inspect <name>
133
+ info_p = ws_sub.add_parser(
134
+ "info",
135
+ aliases=["inspect"],
136
+ help="Show working set details",
137
+ description="Show name, root, creation date, and projects for a working set.",
138
+ )
139
+ info_p.add_argument("name", help="Name of the working set")
140
+ info_p.set_defaults(func=run_info)
141
+
142
+ # kanibako workset config <workset> [key[=value]] [--effective] [--reset]
143
+ # [--all] [--force] [--local]
144
+ config_p = ws_sub.add_parser(
145
+ "config",
146
+ help="View or modify working set configuration",
147
+ description=(
148
+ "Unified config interface for working set settings.\n\n"
149
+ " workset config myws show overrides\n"
150
+ " workset config myws --effective show resolved values\n"
151
+ " workset config myws model get the value of 'model'\n"
152
+ " workset config myws model=sonnet set 'model' to 'sonnet'\n"
153
+ " workset config myws group_auth=false set auth mode\n"
154
+ " workset config myws --reset model reset one key\n"
155
+ " workset config myws --reset --all reset all overrides\n"
156
+ ),
157
+ formatter_class=argparse.RawDescriptionHelpFormatter,
158
+ )
159
+ config_p.add_argument("workset", help="Name of the working set")
160
+ config_p.add_argument(
161
+ "key_value", nargs="?", default=None,
162
+ help="Config key or key=value pair",
163
+ )
164
+ config_p.add_argument(
165
+ "--effective", action="store_true",
166
+ help="Show resolved values including inherited defaults",
167
+ )
168
+ config_p.add_argument(
169
+ "--reset", metavar="KEY", nargs="?", const="__ALL__", default=None,
170
+ help="Remove override for KEY (or all overrides with --all)",
171
+ )
172
+ config_p.add_argument(
173
+ "--all", action="store_true", dest="reset_all",
174
+ help="Reset all overrides (only valid with --reset)",
175
+ )
176
+ config_p.add_argument(
177
+ "--force", action="store_true",
178
+ help="Skip confirmation prompts",
179
+ )
180
+ config_p.add_argument(
181
+ "--local", action="store_true",
182
+ help="Set resource to project-isolated (resource keys only)",
183
+ )
184
+ config_p.set_defaults(func=run_config)
185
+
186
+ # Default to list if no subcommand given.
187
+ p.set_defaults(func=run_list, quiet=False)
188
+
189
+
190
+ def _load_std():
191
+ """Load config and standard paths."""
192
+ config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
193
+ config = load_config(config_file)
194
+ return load_std_paths(config)
195
+
196
+
197
+ def _workset_config_path(ws) -> Path:
198
+ """Return the path to the workset-level config.yaml."""
199
+ return ws.root / "config.yaml"
200
+
201
+
202
+ def run_create(args: argparse.Namespace) -> int:
203
+ import os
204
+
205
+ std = _load_std()
206
+ path = args.path
207
+ if path is None:
208
+ path = os.getcwd()
209
+ path = Path(path).resolve()
210
+ name = args.name or path.name
211
+
212
+ # Store additional flags in workset config if provided.
213
+ group_auth = not getattr(args, "distinct_auth", False)
214
+
215
+ try:
216
+ ws = create_workset(name, path, std)
217
+ except WorksetError as e:
218
+ print(f"Error: {e}", file=sys.stderr)
219
+ return 1
220
+
221
+ # Set auth mode if distinct.
222
+ if not group_auth:
223
+ ws.group_auth = group_auth
224
+ _write_workset_toml(ws)
225
+
226
+ # Store additional settings in workset-level config.yaml.
227
+ image = getattr(args, "image", None)
228
+ standalone = getattr(args, "standalone", False)
229
+ no_vault = getattr(args, "no_vault", False)
230
+ if image or standalone or no_vault:
231
+ from kanibako.config_io import dump_doc
232
+ config_data: dict = {}
233
+ if image:
234
+ config_data["box"] = {"image": image}
235
+ if standalone:
236
+ config_data["standalone"] = True
237
+ if no_vault:
238
+ config_data["enable_vault"] = False
239
+ ws_config = _workset_config_path(ws)
240
+ dump_doc(ws_config, config_data)
241
+
242
+ print(f"Created working set '{ws.name}' at {ws.root}")
243
+ return 0
244
+
245
+
246
+ def run_list(args: argparse.Namespace) -> int:
247
+ from kanibako.workset import default_workset
248
+
249
+ std = _load_std()
250
+ registry = list_worksets(std)
251
+ quiet = getattr(args, "quiet", False)
252
+
253
+ if quiet:
254
+ print(DEFAULT_WORKSET_ALIAS)
255
+ for name in sorted(registry):
256
+ print(name)
257
+ return 0
258
+
259
+ # The default workset is always present (synthesized).
260
+ dflt = default_workset(std)
261
+
262
+ # Load each named workset to get project count.
263
+ rows: list[tuple[str, int, str]] = [
264
+ (f"{DEFAULT_WORKSET_ALIAS} (default)", len(dflt.projects), "<default workset>"),
265
+ ]
266
+ for name in sorted(registry):
267
+ root = registry[name]
268
+ try:
269
+ ws = load_workset(root)
270
+ count = len(ws.projects)
271
+ except WorksetError:
272
+ count = 0
273
+ rows.append((name, count, str(root)))
274
+
275
+ print(f"{'NAME':<20} {'PROJECTS':>8} {'ROOT'}")
276
+ for ws_name, ws_count, ws_root in rows:
277
+ print(f"{ws_name:<20} {ws_count:>8} {ws_root}")
278
+ return 0
279
+
280
+
281
+ def run_rm(args: argparse.Namespace) -> int:
282
+ std = _load_std()
283
+
284
+ if args.name in (DEFAULT_WORKSET_ID, DEFAULT_WORKSET_ALIAS):
285
+ print("Error: The default workset cannot be removed.", file=sys.stderr)
286
+ return 1
287
+
288
+ # Check if workset has projects — error unless --force.
289
+ registry = list_worksets(std)
290
+ if args.name in registry:
291
+ try:
292
+ ws = load_workset(registry[args.name])
293
+ if ws.projects and not args.force:
294
+ print(
295
+ f"Error: workset '{args.name}' has {len(ws.projects)} project(s). "
296
+ f"Use --force to remove anyway.",
297
+ file=sys.stderr,
298
+ )
299
+ return 1
300
+ except WorksetError:
301
+ pass
302
+
303
+ if not args.force:
304
+ label = "and remove files " if args.purge else ""
305
+ confirm_prompt(
306
+ f"Unregister {label}working set '{args.name}'? Type 'yes' to confirm: "
307
+ )
308
+ try:
309
+ root = delete_workset(args.name, std, remove_files=args.purge)
310
+ except WorksetError as e:
311
+ print(f"Error: {e}", file=sys.stderr)
312
+ return 1
313
+ print(f"Deleted working set '{args.name}' (root was {root})")
314
+ return 0
315
+
316
+
317
+ def run_connect(args: argparse.Namespace) -> int:
318
+ import os
319
+
320
+ std = _load_std()
321
+ registry = list_worksets(std)
322
+ if args.workset not in registry:
323
+ print(f"Error: Working set '{args.workset}' is not registered.", file=sys.stderr)
324
+ return 1
325
+
326
+ try:
327
+ ws = load_workset(registry[args.workset])
328
+ except WorksetError as e:
329
+ print(f"Error: {e}", file=sys.stderr)
330
+ return 1
331
+
332
+ source = Path(args.source) if args.source else Path(os.getcwd())
333
+ project_name = args.project_name or source.resolve().name
334
+
335
+ try:
336
+ proj = add_project(ws, project_name, source)
337
+ except WorksetError as e:
338
+ print(f"Error: {e}", file=sys.stderr)
339
+ return 1
340
+ print(f"Added project '{proj.name}' to working set '{ws.name}'")
341
+ return 0
342
+
343
+
344
+ def run_disconnect(args: argparse.Namespace) -> int:
345
+ std = _load_std()
346
+
347
+ if args.workset in (DEFAULT_WORKSET_ID, DEFAULT_WORKSET_ALIAS):
348
+ print("Error: The default workset cannot be removed.", file=sys.stderr)
349
+ return 1
350
+
351
+ registry = list_worksets(std)
352
+ if args.workset not in registry:
353
+ print(f"Error: Working set '{args.workset}' is not registered.", file=sys.stderr)
354
+ return 1
355
+
356
+ try:
357
+ ws = load_workset(registry[args.workset])
358
+ except WorksetError as e:
359
+ print(f"Error: {e}", file=sys.stderr)
360
+ return 1
361
+
362
+ if not args.force:
363
+ label = "and remove files " if args.remove_files else ""
364
+ confirm_prompt(
365
+ f"Remove {label}project '{args.project}' from '{ws.name}'? "
366
+ "Type 'yes' to confirm: "
367
+ )
368
+
369
+ try:
370
+ proj = remove_project(ws, args.project, remove_files=args.remove_files)
371
+ except WorksetError as e:
372
+ print(f"Error: {e}", file=sys.stderr)
373
+ return 1
374
+ print(f"Removed project '{proj.name}' from working set '{ws.name}'")
375
+ return 0
376
+
377
+
378
+ def run_info(args: argparse.Namespace) -> int:
379
+ std = _load_std()
380
+ try:
381
+ ws = resolve_workset_name(args.name, std)
382
+ except WorksetError as e:
383
+ print(f"Error: {e}", file=sys.stderr)
384
+ return 1
385
+
386
+ root_display = "<default workset>" if ws.is_default else str(ws.root)
387
+ print(f"Name: {ws.name}")
388
+ print(f"Root: {root_display}")
389
+ print(f"Created: {ws.created}")
390
+ print(f"Group auth: {ws.group_auth}")
391
+ if ws.projects:
392
+ print(f"Projects: {len(ws.projects)}")
393
+ for proj in ws.projects:
394
+ print(f" - {proj.name} ({proj.source_path})")
395
+ else:
396
+ print("Projects: (none)")
397
+ return 0
398
+
399
+
400
+ def run_config(args: argparse.Namespace) -> int:
401
+ """Unified config interface for working set settings.
402
+
403
+ Handles get, set, show, reset operations via the config_interface engine.
404
+ The ``group_auth`` key is special-cased to update workset.yaml directly.
405
+ """
406
+ from kanibako.config_interface import (
407
+ ConfigAction,
408
+ get_config_value,
409
+ parse_config_arg,
410
+ reset_all,
411
+ reset_config_value,
412
+ set_config_value,
413
+ show_config,
414
+ )
415
+
416
+ std = _load_std()
417
+ ws_name = args.workset
418
+ try:
419
+ ws = resolve_workset_name(ws_name, std)
420
+ except WorksetError as e:
421
+ print(f"Error: {e}", file=sys.stderr)
422
+ return 1
423
+
424
+ config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
425
+ ws_config = _workset_config_path(ws)
426
+
427
+ key_value = getattr(args, "key_value", None)
428
+
429
+ # Handle --reset mode
430
+ if args.reset is not None:
431
+ if args.reset_all or args.reset == "__ALL__":
432
+ msg = reset_all(
433
+ config_path=ws_config,
434
+ force=args.force,
435
+ )
436
+ print(msg)
437
+ return 0
438
+
439
+ reset_key = args.reset
440
+ # Special case: resetting group_auth reverts to True (shared)
441
+ if reset_key == "group_auth":
442
+ ws.group_auth = True
443
+ if ws.is_default:
444
+ # Default workset has no workset.yaml — clear the key in
445
+ # config.yaml's [project] section.
446
+ from kanibako.config_interface import _remove_toml_key
447
+ _remove_toml_key(ws_config, "project", "group_auth")
448
+ else:
449
+ _write_workset_toml(ws)
450
+ print("Reset group_auth (reverts to default: true)")
451
+ return 0
452
+
453
+ msg = reset_config_value(
454
+ reset_key,
455
+ config_path=ws_config,
456
+ )
457
+ print(msg)
458
+ return 0
459
+
460
+ # Parse the key/value argument
461
+ action, key, value = parse_config_arg(key_value)
462
+
463
+ # --local flag forces a set operation
464
+ if args.local and action == ConfigAction.get:
465
+ action = ConfigAction.set
466
+
467
+ if action == ConfigAction.show:
468
+ return show_config(
469
+ global_config_path=config_file,
470
+ config_path=ws_config,
471
+ effective=args.effective,
472
+ )
473
+
474
+ if action == ConfigAction.get:
475
+ # Special case: group_auth key lives in workset.yaml
476
+ if key == "group_auth":
477
+ print(ws.group_auth)
478
+ return 0
479
+
480
+ val = get_config_value(
481
+ key,
482
+ global_config_path=config_file,
483
+ project_toml=ws_config,
484
+ )
485
+ if val is not None:
486
+ print(val)
487
+ else:
488
+ print("(not set)", file=sys.stderr)
489
+ return 0
490
+
491
+ if action == ConfigAction.set:
492
+ # Special case: group_auth key updates workset.yaml directly
493
+ if key == "group_auth":
494
+ normalized = value.strip().lower()
495
+ if normalized in ("true", "1"):
496
+ new_group_auth = True
497
+ elif normalized in ("false", "0"):
498
+ new_group_auth = False
499
+ else:
500
+ print(
501
+ f"Error: group_auth must be 'true' or 'false', got '{value}'",
502
+ file=sys.stderr,
503
+ )
504
+ return 1
505
+
506
+ old_group_auth = ws.group_auth
507
+ ws.group_auth = new_group_auth
508
+ if ws.is_default:
509
+ # Default workset has no workset.yaml — write group_auth as a
510
+ # normal boolean key in config.yaml's [project] section.
511
+ from kanibako.config_interface import _write_toml_key
512
+ _write_toml_key(ws_config, "project", "group_auth", new_group_auth)
513
+ else:
514
+ _write_workset_toml(ws)
515
+
516
+ if (not new_group_auth) and old_group_auth:
517
+ # Switched shared→distinct: invalidate credentials in all shells.
518
+ from kanibako.targets import resolve_target
519
+ try:
520
+ target = resolve_target(None)
521
+ except KeyError:
522
+ target = None
523
+ if target:
524
+ for proj in ws.projects:
525
+ shell_path = ws.projects_dir / proj.name / "shell"
526
+ if shell_path.is_dir():
527
+ target.invalidate_credentials(shell_path)
528
+ print(
529
+ f"Set group_auth to false (distinct) for '{ws.name}'. "
530
+ f"Credentials cleared in {len(ws.projects)} project(s).",
531
+ )
532
+ else:
533
+ print(f"Set group_auth to {str(new_group_auth).lower()} for '{ws.name}'.")
534
+ return 0
535
+
536
+ # Handle --local for resource keys
537
+ if args.local:
538
+ from kanibako.config_interface import _is_resource_key, _resolve_key
539
+ canonical = _resolve_key(key)
540
+ if not _is_resource_key(canonical):
541
+ print("Error: --local only applies to resource.* keys", file=sys.stderr)
542
+ return 1
543
+ value = "project"
544
+
545
+ msg = set_config_value(
546
+ key, value,
547
+ config_path=ws_config,
548
+ )
549
+ print(msg)
550
+ return 0
551
+
552
+ return 0