opencontext-cli 0.3.0__tar.gz → 0.4.0b0__tar.gz

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 (26) hide show
  1. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/PKG-INFO +1 -1
  2. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/config_cmd.py +12 -3
  3. opencontext_cli-0.4.0b0/opencontext_cli/commands/menu_cmd.py +442 -0
  4. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/setup_cmd.py +2 -2
  5. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/update_cmd.py +20 -9
  6. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/main.py +59 -9
  7. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/PKG-INFO +1 -1
  8. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/SOURCES.txt +1 -0
  9. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/pyproject.toml +1 -1
  10. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/LICENSE +0 -0
  11. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/README.md +0 -0
  12. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/__init__.py +0 -0
  13. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/__main__.py +0 -0
  14. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/__init__.py +0 -0
  15. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/ci_check_cmd.py +0 -0
  16. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/git_cmd.py +0 -0
  17. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/hints_cmd.py +0 -0
  18. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/kg_cmd.py +0 -0
  19. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/plugin_cmd.py +0 -0
  20. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/sync_cmd.py +0 -0
  21. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli/commands/verify_cmd.py +0 -0
  22. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/dependency_links.txt +0 -0
  23. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/entry_points.txt +0 -0
  24. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/requires.txt +0 -0
  25. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/opencontext_cli.egg-info/top_level.txt +0 -0
  26. {opencontext_cli-0.3.0 → opencontext_cli-0.4.0b0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencontext-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0b0
4
4
  Summary: CLI adapter for OpenContext Runtime
5
5
  Author: OpenContext Runtime maintainers
6
6
  License-Expression: MIT
@@ -16,7 +16,6 @@ from opencontext_core.user_prefs import UserConfigStore
16
16
  from opencontext_core.wizard import (
17
17
  reconfigure,
18
18
  reset_config,
19
- run_wizard,
20
19
  show_config,
21
20
  )
22
21
 
@@ -25,7 +24,7 @@ def add_config_parser(subparsers: Any) -> None:
25
24
  """Add config command parsers."""
26
25
 
27
26
  config_parser = subparsers.add_parser("config", help="Manage OpenContext configuration.")
28
- config_sub = config_parser.add_subparsers(dest="config_command", required=True)
27
+ config_sub = config_parser.add_subparsers(dest="config_command")
29
28
 
30
29
  # Wizard
31
30
  wizard_parser = config_sub.add_parser("wizard", help="Run configuration wizard.")
@@ -76,7 +75,17 @@ def add_config_parser(subparsers: Any) -> None:
76
75
  def handle_config(args: Any) -> None:
77
76
  """Handle config commands."""
78
77
 
79
- command = args.config_command
78
+ command = getattr(args, "config_command", None)
79
+
80
+ if command is None:
81
+ # No subcommand — run the interactive wizard by default
82
+ from opencontext_core.wizard import run_wizard, run_wizard_menu
83
+
84
+ try:
85
+ run_wizard_menu()
86
+ except Exception:
87
+ run_wizard(non_interactive=True)
88
+ return
80
89
 
81
90
  if command == "wizard":
82
91
  use_tui = not getattr(args, "non_interactive", False)
@@ -0,0 +1,442 @@
1
+ """Main TUI menu for OpenContext.
2
+
3
+ Run opencontext with no arguments to launch this interactive menu.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+
10
+ from rich.panel import Panel
11
+ from rich.prompt import Prompt
12
+ from rich.text import Text
13
+
14
+ from opencontext_cli.commands.update_cmd import handle_upgrade
15
+ from opencontext_core.dx.console_styles import console
16
+
17
+ # ── ANSI art: OpenContext logo ──────────────────────────────────────────
18
+
19
+ LOGO = [
20
+ " ⢀⣀⣤⣤⣶⣶⣶⣶⣦⣤⣤⣄⣀",
21
+ " ⣠⣶⣿⣿⠿⠛⠋⠉⠉⠉⠉⠙⠛⠿⣿⣿⣶⣄",
22
+ " ⣰⣿⣿⠟⠉ ⢀⣀⣀⣀ ⠉⠻⣿⣿⣆",
23
+ " ⣿⣿⡟⠁ ⢀⣴⣿⣿⠿⠿⣿⣿⣶⣄ ⢹⣿⣿",
24
+ " ⢸⣿⡟ ⣴⣿⣿⠋ ⢀⣀ ⠙⣿⣿⣦ ⢸⣿⡟",
25
+ " ⠸⣿⣿⣦ ⢿⣿⣿⣷⣾⣿⣿⣿⣷⣶⣾⣿⣿⡿ ⣰⣿⣿⠇",
26
+ " ⠻⣿⣿⣷⣤⣉⠛⠻⠿⠿⠿⠿⠿⠟⠛⣉⣤⣶⣿⣿⡿⠟",
27
+ " ⢀⣀⣤⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣄⣀",
28
+ " ⣠⣶⣿⣿⠿⠟⠛⠉⠉ ⠈⠉⠉⠙⠛⠻⢿⣿⣿⣶⣄⡀",
29
+ " ⣿⣿⡟⠁ ⢀⣀⣤⣶⣿⣿⡿⠿⢿⣿⣷",
30
+ " ⣿⣿⡇ ⢀⣀⣤⣤⣶⣶⣶⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣀ ⢸⣿⣿",
31
+ " ⣿⣿⡇ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⢸⣿⣿",
32
+ " ⢻⣿⣿⣄ ⠙⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛ ⣠⣿⣿⡟",
33
+ " ⠻⣿⣿⣷⣤⣀⡀ ⢀⣀⣤⣴⣿⣿⣿⠟",
34
+ " ⠙⠛⠿⣿⣿⣿⣿⣷⣶⣶⣶⣶⣶⣶⣾⣿⣿⣿⣿⡿⠟⠋⠁",
35
+ " ⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉",
36
+ ]
37
+
38
+ COMPACT_LOGO = [
39
+ " ╔══════════════════════════════╗",
40
+ " ║ OpenContext Runtime ║",
41
+ " ║ Context Engineering ║",
42
+ " ╚══════════════════════════════╝",
43
+ ]
44
+
45
+
46
+ def _show_logo() -> None:
47
+ """Print the OpenContext logo, falling back to compact if terminal is small."""
48
+ try:
49
+ width = __import__("shutil").get_terminal_size().columns
50
+ height = __import__("shutil").get_terminal_size().lines
51
+ use_full = width >= 64 and height >= len(LOGO) + 18
52
+ except Exception:
53
+ use_full = False
54
+
55
+ logo_lines = LOGO if use_full else COMPACT_LOGO
56
+ for line in logo_lines:
57
+ console.print(Text(line, style="bold cyan"))
58
+
59
+
60
+ def run_main_menu() -> None:
61
+ """Show the main OpenContext menu and delegate to the selected command."""
62
+
63
+ while True:
64
+ try:
65
+ console.clear()
66
+ except Exception:
67
+ pass
68
+
69
+ _show_logo()
70
+ console.print()
71
+ console.print(
72
+ Panel(
73
+ "\n".join(
74
+ [
75
+ "[bold]Menu[/]",
76
+ "",
77
+ " [cyan]1[/] Start installation",
78
+ " [cyan]2[/] Upgrade tools",
79
+ " [cyan]3[/] Sync configs",
80
+ " [cyan]4[/] Upgrade + Sync",
81
+ " [cyan]5[/] Configure models",
82
+ " [cyan]6[/] Create your own Agent",
83
+ " [cyan]7[/] OpenCode Community Plugins",
84
+ " [cyan]8[/] OpenCode SDD Profiles",
85
+ " [cyan]9[/] Manage backups",
86
+ " [cyan]10[/] Managed uninstall",
87
+ " [cyan]q[/] Quit",
88
+ "",
89
+ "[dim]1-10: select • q: quit[/]",
90
+ ]
91
+ ),
92
+ border_style="cyan",
93
+ padding=(1, 2),
94
+ )
95
+ )
96
+ console.print()
97
+
98
+ choice = Prompt.ask(
99
+ "Select option",
100
+ choices=["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "q"],
101
+ default="q",
102
+ )
103
+
104
+ if choice == "1":
105
+ _run_install()
106
+ elif choice == "2":
107
+ _run_upgrade()
108
+ elif choice == "3":
109
+ _run_sync()
110
+ elif choice == "4":
111
+ _run_upgrade_sync()
112
+ elif choice == "5":
113
+ _run_configure_models()
114
+ elif choice == "6":
115
+ _run_create_agent()
116
+ elif choice == "7":
117
+ _run_plugins()
118
+ elif choice == "8":
119
+ _run_sdd_profiles()
120
+ elif choice == "9":
121
+ _run_backups()
122
+ elif choice == "10":
123
+ _run_uninstall()
124
+ elif choice == "q":
125
+ console.print("[dim]Goodbye.[/]")
126
+ break
127
+
128
+ console.print("\n[dim]Press Enter to return to menu...[/]")
129
+ try:
130
+ input()
131
+ except (EOFError, KeyboardInterrupt):
132
+ break
133
+
134
+
135
+ # ── Menu action dispatchers ─────────────────────────────────────────────
136
+
137
+
138
+ def _run_install() -> None:
139
+ """Start installation — opencontext install."""
140
+ console.print("\n[bold]Starting installation...[/]")
141
+ try:
142
+ from opencontext_cli.main import _install
143
+
144
+ class _InstallArgs:
145
+ root: str = "."
146
+ yes: bool = False
147
+
148
+ _install(_InstallArgs())
149
+ except Exception as exc:
150
+ console.print(f"[red]Installation failed: {exc}[/]")
151
+
152
+
153
+ def _run_upgrade() -> None:
154
+ """Upgrade tools — opencontext upgrade."""
155
+ console.print("\n[bold]Checking for updates...[/]")
156
+ handle_upgrade(
157
+ type(
158
+ "Args",
159
+ (),
160
+ {},
161
+ )()
162
+ )
163
+
164
+
165
+ def _run_sync() -> None:
166
+ """Sync configs — opencontext sync."""
167
+ console.print("\n[bold]Syncing configs...[/]")
168
+ try:
169
+ from opencontext_cli.commands.sync_cmd import handle_sync
170
+
171
+ handle_sync(type("Args", (), {"sync_command": None})())
172
+ console.print("[green]✓ Configs synced[/]")
173
+ except Exception as exc:
174
+ console.print(f"[red]Sync failed: {exc}[/]")
175
+
176
+
177
+ def _run_upgrade_sync() -> None:
178
+ """Upgrade tools and sync configs."""
179
+ _run_upgrade()
180
+ console.print()
181
+ _run_sync()
182
+
183
+
184
+ def _run_configure_models() -> None:
185
+ """Configure models — opencontext config wizard."""
186
+ console.print("\n[bold]Model configuration[/]")
187
+ try:
188
+ from opencontext_core.wizard import run_wizard_menu
189
+
190
+ run_wizard_menu()
191
+ return # wizard has its own loop
192
+ except Exception:
193
+ pass
194
+
195
+ # Fallback: simple prompts
196
+ from opencontext_core.user_prefs import UserConfigStore
197
+
198
+ store = UserConfigStore()
199
+ prefs = store.load()
200
+
201
+ from rich.prompt import Prompt as RPrompt
202
+
203
+ console.print("\n[bold]Current model configuration:[/]")
204
+ console.print(f" Default provider: {prefs.default_provider}")
205
+ console.print(f" Default model: {prefs.default_model}")
206
+ console.print()
207
+
208
+ provider = RPrompt.ask("Default provider", default=prefs.default_provider or "mock")
209
+ model = RPrompt.ask("Default model", default=prefs.default_model or "mock-llm")
210
+ prefs.default_provider = provider
211
+ prefs.default_model = model
212
+ store.save(prefs)
213
+ console.print("[green]✓ Model configuration saved[/]")
214
+
215
+
216
+ def _run_create_agent() -> None:
217
+ """Create your own Agent — opencontext agent init."""
218
+ console.print("\n[bold]Creating agent integration...[/]")
219
+ try:
220
+ from opencontext_cli.main import _agent
221
+
222
+ _agent(
223
+ type(
224
+ "Args",
225
+ (),
226
+ {
227
+ "agent_command": "init",
228
+ "target": "generic",
229
+ "root": ".",
230
+ "force": False,
231
+ },
232
+ )
233
+ )
234
+ console.print("[green]✓ Agent integration created[/]")
235
+ except Exception as exc:
236
+ console.print(f"[red]Agent creation failed: {exc}[/]")
237
+
238
+
239
+ def _run_plugins() -> None:
240
+ """Browse plugins — opencontext plugin."""
241
+ console.print("\n[bold]OpenCode Community Plugins[/]")
242
+ try:
243
+ from opencontext_cli.commands.plugin_cmd import handle_plugin
244
+
245
+ handle_plugin(
246
+ type(
247
+ "Args",
248
+ (),
249
+ {
250
+ "plugin_command": "search",
251
+ },
252
+ )
253
+ )
254
+ except Exception as exc:
255
+ console.print(f"[red]Plugin search failed: {exc}[/]")
256
+
257
+
258
+ def _run_sdd_profiles() -> None:
259
+ """Configure SDD profiles — opencontext config wizard."""
260
+ console.print("\n[bold]OpenCode SDD Profiles[/]")
261
+ try:
262
+ from opencontext_core.user_prefs import UserConfigStore
263
+
264
+ store = UserConfigStore()
265
+ prefs = store.load()
266
+ from rich.prompt import Prompt as RPrompt
267
+
268
+ console.print(f" Current SDD profile: {prefs.sdd.sdd_model_profile}")
269
+ console.print(f" Current TDD mode: {prefs.sdd.tdd_mode}")
270
+ console.print()
271
+
272
+ profile = RPrompt.ask(
273
+ "SDD model profile",
274
+ choices=["default", "cheap", "hybrid", "premium"],
275
+ default=prefs.sdd.sdd_model_profile or "hybrid",
276
+ )
277
+ tdd = RPrompt.ask(
278
+ "TDD mode",
279
+ choices=["ask", "strict", "off"],
280
+ default=prefs.sdd.tdd_mode or "ask",
281
+ )
282
+ prefs.sdd.sdd_model_profile = profile
283
+ prefs.sdd.tdd_mode = tdd
284
+ store.save(prefs)
285
+ console.print("[green]✓ SDD profiles updated[/]")
286
+ except Exception as exc:
287
+ console.print(f"[red]Failed: {exc}[/]")
288
+
289
+
290
+ def _run_backups() -> None:
291
+ """Manage backups — opencontext config backup/restore/backups."""
292
+ console.print("\n[bold]Backup Management[/]")
293
+
294
+ while True:
295
+ try:
296
+ console.clear()
297
+ except Exception:
298
+ pass
299
+ console.print(
300
+ Panel(
301
+ "\n".join(
302
+ [
303
+ "[bold]Backup Management[/]",
304
+ "",
305
+ " [cyan]1[/] Create backup",
306
+ " [cyan]2[/] List backups",
307
+ " [cyan]3[/] Restore backup",
308
+ " [cyan]4[/] Cleanup old backups",
309
+ " [cyan]b[/] Back to main menu",
310
+ " [cyan]q[/] Quit",
311
+ ]
312
+ ),
313
+ border_style="yellow",
314
+ padding=(1, 2),
315
+ )
316
+ )
317
+ console.print()
318
+ choice = Prompt.ask(
319
+ "Select option",
320
+ choices=["1", "2", "3", "4", "b", "q"],
321
+ default="b",
322
+ )
323
+
324
+ if choice == "1":
325
+ _create_backup()
326
+ elif choice == "2":
327
+ _list_backups()
328
+ elif choice == "3":
329
+ _restore_backup()
330
+ elif choice == "4":
331
+ _cleanup_backups()
332
+ elif choice == "b":
333
+ break
334
+ elif choice == "q":
335
+ console.print("[dim]Goodbye.[/]")
336
+ sys.exit(0)
337
+
338
+ console.print("\n[dim]Press Enter to continue...[/]")
339
+ try:
340
+ input()
341
+ except (EOFError, KeyboardInterrupt):
342
+ break
343
+
344
+
345
+ def _create_backup() -> None:
346
+ """Create a config backup."""
347
+ try:
348
+ from opencontext_core.state import ConfigBackupManager
349
+
350
+ backup_id = ConfigBackupManager.create_backup(description="manual")
351
+ console.print(f"[green]✓ Backup created: {backup_id}[/]")
352
+ except Exception as exc:
353
+ console.print(f"[red]Backup failed: {exc}[/]")
354
+
355
+
356
+ def _list_backups() -> None:
357
+ """List all config backups."""
358
+ try:
359
+ from opencontext_core.state import ConfigBackupManager
360
+
361
+ backups = ConfigBackupManager.list_backups()
362
+ if not backups:
363
+ console.print("[yellow]No backups found.[/]")
364
+ return
365
+ console.print()
366
+ for b in backups:
367
+ console.print(f" {b.id} ({b.timestamp}) — {b.description}")
368
+ console.print(f"\n {len(backups)} backup(s) available")
369
+ except Exception as exc:
370
+ console.print(f"[red]Failed to list backups: {exc}[/]")
371
+
372
+
373
+ def _restore_backup() -> None:
374
+ """Restore from a backup."""
375
+ try:
376
+ from opencontext_core.state import ConfigBackupManager
377
+
378
+ backups = ConfigBackupManager.list_backups()
379
+ if not backups:
380
+ console.print("[yellow]No backups to restore.[/]")
381
+ return
382
+
383
+ from rich.prompt import Prompt as RPrompt
384
+
385
+ console.print("\n[bold]Available backups:[/]")
386
+ for i, b in enumerate(backups, 1):
387
+ console.print(f" {i}. {b.id} ({b.timestamp})")
388
+ idx = RPrompt.ask(
389
+ "Select backup to restore",
390
+ choices=[str(i) for i in range(1, len(backups) + 1)],
391
+ )
392
+ backup_id = backups[int(idx) - 1].id
393
+ if ConfigBackupManager.restore_backup(backup_id):
394
+ console.print(f"[green]✓ Restored from: {backup_id}[/]")
395
+ else:
396
+ console.print(f"[red]Backup not found: {backup_id}[/]")
397
+ except Exception as exc:
398
+ console.print(f"[red]Restore failed: {exc}[/]")
399
+
400
+
401
+ def _cleanup_backups() -> None:
402
+ """Clean up old backups."""
403
+ import shutil
404
+ from datetime import datetime, timedelta
405
+
406
+ from rich.prompt import IntPrompt
407
+
408
+ from opencontext_core.state import ConfigBackupManager
409
+
410
+ days = IntPrompt.ask("Keep backups newer than (days)", default=30)
411
+ backups = ConfigBackupManager.list_backups()
412
+ cutoff = datetime.now() - timedelta(days=days)
413
+ removed = 0
414
+ for b in backups:
415
+ try:
416
+ ts = datetime.strptime(b.timestamp, "%Y%m%dT%H%M%S")
417
+ if ts < cutoff:
418
+ backup_dir = ConfigBackupManager.BACKUP_DIR / b.id
419
+ if backup_dir.exists():
420
+ shutil.rmtree(backup_dir)
421
+ removed += 1
422
+ except (ValueError, OSError):
423
+ continue
424
+ console.print(f"[green]✓ Removed {removed} backup(s) older than {days} days[/]")
425
+
426
+
427
+ def _run_uninstall() -> None:
428
+ """Managed uninstall — opencontext clean."""
429
+ console.print("\n[bold]Uninstall OpenContext[/]")
430
+ from rich.prompt import Confirm
431
+
432
+ if not Confirm.ask("Remove OpenContext configuration from this project?", default=False):
433
+ console.print("[yellow]Uninstall cancelled.[/]")
434
+ return
435
+
436
+ try:
437
+ from opencontext_cli.main import _clean
438
+
439
+ _clean(".", dry_run=False, force=False)
440
+ console.print("[green]✓ OpenContext configuration removed[/]")
441
+ except Exception as exc:
442
+ console.print(f"[red]Uninstall failed: {exc}[/]")
@@ -546,8 +546,8 @@ def _execute_plan(
546
546
  except ValueError:
547
547
  console.print(f"[yellow]⚠ Unknown project-local agent target: {selected_agent}[/]")
548
548
 
549
- # Global client config (MCP + profile files) for selected clients, matching
550
- # Gentle-AI's install-time activation model but using OpenContext's local KG.
549
+ # Global client config (MCP + profile files) for selected clients, using
550
+ # OpenContext's local knowledge graph.
551
551
  if "mcp-server" in plan.components or "knowledge-graph" in plan.components:
552
552
  global_targets = []
553
553
  for selected_agent in agents:
@@ -70,17 +70,28 @@ def handle_update(args: Any) -> None:
70
70
 
71
71
 
72
72
  def handle_upgrade(args: Any) -> None:
73
- """Check and upgrade."""
73
+ """Check and upgrade all OpenContext packages."""
74
74
 
75
- check = UpdateChecker.check()
76
- if not check.is_outdated:
77
- print(f" ✓ OpenContext {check.current_version} is already up to date.")
78
- return
75
+ print()
76
+ print(" Checking for OpenContext updates...")
77
+ print()
78
+
79
+ results = UpdateChecker.upgrade_all()
80
+
81
+ upgraded = [r for r in results if r["status"] == "upgraded"]
82
+ failed = [r for r in results if r["status"] == "failed"]
79
83
 
80
- print(f" Upgrading {check.current_version} -> {check.latest_version}...")
81
- result = UpdateChecker.upgrade()
84
+ print(f" {'Package':<25} {'Status':<12} {'Message'}")
85
+ print(f" {'─' * 25} {'─' * 12} {'─' * 40}")
86
+ for r in results:
87
+ icon = "✓" if r["status"] == "upgraded" else "✗"
88
+ print(f" {r['package']:<25} {icon + ' ' + r['status']:<12} {r['message']}")
82
89
  print()
83
- print(f" {result['status']}: {result['message']}")
84
90
 
85
- if result["status"] == "failed":
91
+ if upgraded:
92
+ print(f" ✓ {len(upgraded)} package(s) upgraded.")
93
+ if failed:
94
+ print(f" ✗ {len(failed)} package(s) failed.")
86
95
  sys.exit(1)
96
+ if not upgraded and not failed:
97
+ print(" ✓ All packages are up to date.")
@@ -134,7 +134,17 @@ def _technology_template_names() -> tuple[str, ...]:
134
134
  TECHNOLOGY_TEMPLATE_NAMES = _technology_template_names()
135
135
 
136
136
 
137
- __version__ = "0.2.1b0"
137
+ def _get_version() -> str:
138
+ """Get installed version via importlib.metadata, with fallback."""
139
+ try:
140
+ import importlib.metadata
141
+
142
+ return importlib.metadata.version("opencontext-cli")
143
+ except (importlib.metadata.PackageNotFoundError, ImportError):
144
+ return "0.0.0"
145
+
146
+
147
+ __version__ = _get_version()
138
148
 
139
149
 
140
150
  def main() -> None:
@@ -242,7 +252,7 @@ def _build_parser() -> argparse.ArgumentParser:
242
252
  version=f"%(prog)s {__version__}",
243
253
  help="Show version and exit.",
244
254
  )
245
- subparsers = parser.add_subparsers(dest="command", required=True)
255
+ subparsers = parser.add_subparsers(dest="command")
246
256
 
247
257
  init_parser = subparsers.add_parser("init", help="Create a default OpenContext configuration.")
248
258
  init_parser.add_argument(
@@ -792,16 +802,43 @@ def _build_parser() -> argparse.ArgumentParser:
792
802
  return parser
793
803
 
794
804
 
805
+ _config_path_cache: str | None = None
806
+
807
+
795
808
  def _default_config_path() -> str:
796
- if Path("opencontext.yaml").exists():
797
- return "opencontext.yaml"
798
- if Path("configs/opencontext.yaml").exists():
799
- return "configs/opencontext.yaml"
800
- return "opencontext.yaml"
809
+ """Find opencontext.yaml in current dir or parent dirs, up to 10 levels."""
810
+ global _config_path_cache
811
+ if _config_path_cache is not None:
812
+ return _config_path_cache
813
+
814
+ candidates = ("opencontext.yaml", "configs/opencontext.yaml")
815
+ current = Path.cwd().resolve()
816
+ for _ in range(10):
817
+ for candidate in candidates:
818
+ path = current / candidate
819
+ if path.exists():
820
+ result = str(path)
821
+ _config_path_cache = result
822
+ return result
823
+ parent = current.parent
824
+ if parent == current:
825
+ break
826
+ current = parent
827
+
828
+ _config_path_cache = "opencontext.yaml"
829
+ return _config_path_cache
801
830
 
802
831
 
803
832
  def _dispatch(args: argparse.Namespace) -> None:
804
- command = args.command
833
+ command = getattr(args, "command", None)
834
+
835
+ if command is None:
836
+ # No command — launch the main TUI menu
837
+ from opencontext_cli.commands.menu_cmd import run_main_menu
838
+
839
+ run_main_menu()
840
+ return
841
+
805
842
  if command == "init":
806
843
  _init(args.config, args.template)
807
844
  return
@@ -1081,10 +1118,23 @@ def _install(args: argparse.Namespace) -> None:
1081
1118
 
1082
1119
  root = Path(args.root)
1083
1120
 
1121
+ # Check if already set up
1122
+ already_setup = (root / ".opencontext").exists() and (
1123
+ root / ".opencontext" / "sdd" / "context.json"
1124
+ ).exists()
1125
+
1084
1126
  console.header("OpenContext Install")
1085
1127
  console.print("Detecting your project...")
1086
1128
  console.print()
1087
1129
 
1130
+ if already_setup and not args.yes:
1131
+ console.print("[dim]OpenContext already configured for this project.[/]")
1132
+ proceed = Confirm.ask("Re-run setup?", default=False)
1133
+ if not proceed:
1134
+ console.print("[green]Nothing to do. Your project is ready.[/]")
1135
+ console.print(" Run [cyan]opencontext pack . --query 'Explain this'[/] to start.")
1136
+ return
1137
+
1088
1138
  # Quick project detection (lightweight — no full index needed)
1089
1139
  has_config = (root / "opencontext.yaml").exists()
1090
1140
  has_git = (root / ".git").exists()
@@ -1117,7 +1167,7 @@ def _install(args: argparse.Namespace) -> None:
1117
1167
  console.print()
1118
1168
 
1119
1169
  if not args.yes:
1120
- proceed = Confirm.ask("Proceed with setup?", default=True)
1170
+ proceed = Confirm.ask("Proceed with setup?", default=not already_setup)
1121
1171
  if not proceed:
1122
1172
  console.print("[yellow]Setup cancelled.[/]")
1123
1173
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencontext-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0b0
4
4
  Summary: CLI adapter for OpenContext Runtime
5
5
  Author: OpenContext Runtime maintainers
6
6
  License-Expression: MIT
@@ -16,6 +16,7 @@ opencontext_cli/commands/config_cmd.py
16
16
  opencontext_cli/commands/git_cmd.py
17
17
  opencontext_cli/commands/hints_cmd.py
18
18
  opencontext_cli/commands/kg_cmd.py
19
+ opencontext_cli/commands/menu_cmd.py
19
20
  opencontext_cli/commands/plugin_cmd.py
20
21
  opencontext_cli/commands/setup_cmd.py
21
22
  opencontext_cli/commands/sync_cmd.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "opencontext-cli"
7
- version = "0.3.0"
7
+ version = "0.4.0b0"
8
8
  description = "CLI adapter for OpenContext Runtime"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"