Skip to content

Commit 53900c5

Browse files
committed
fix: Project commands now respect cloud_mode at runtime
- Moved config evaluation from module load time to runtime - Unified add_project command to handle both cloud and local modes - Commands (default, sync-config, move) now check cloud_mode at runtime - Fixes test failures where monkeypatch wasn't applied before command registration
1 parent 02c6de3 commit 53900c5

1 file changed

Lines changed: 116 additions & 102 deletions

File tree

src/basic_memory/cli/commands/project.py

Lines changed: 116 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@
3131
project_app = typer.Typer(help="Manage multiple Basic Memory projects")
3232
app.add_typer(project_app, name="project")
3333

34-
config = ConfigManager().config
35-
36-
3734
def format_path(path: str) -> str:
3835
"""Format a path for display, using ~ for home directory."""
3936
home = str(Path.home())
@@ -69,40 +66,32 @@ async def _list_projects():
6966
raise typer.Exit(1)
7067

7168

72-
if config.cloud_mode_enabled:
69+
@project_app.command("add")
70+
def add_project(
71+
name: str = typer.Argument(..., help="Name of the project"),
72+
path: str = typer.Argument(None, help="Path to the project directory (required for local mode)"),
73+
set_default: bool = typer.Option(False, "--default", help="Set as default project"),
74+
) -> None:
75+
"""Add a new project.
7376
74-
@project_app.command("add")
75-
def add_project_cloud(
76-
name: str = typer.Argument(..., help="Name of the project"),
77-
set_default: bool = typer.Option(False, "--default", help="Set as default project"),
78-
) -> None:
79-
"""Add a new project to Basic Memory Cloud"""
77+
For cloud mode: only name is required
78+
For local mode: both name and path are required
79+
"""
80+
config = ConfigManager().config
8081

82+
if config.cloud_mode_enabled:
83+
# Cloud mode: path not needed (auto-generated from name)
8184
async def _add_project():
8285
async with get_client() as client:
8386
data = {"name": name, "path": generate_permalink(name), "set_default": set_default}
8487
response = await call_post(client, "/projects/projects", json=data)
8588
return ProjectStatusResponse.model_validate(response.json())
86-
87-
try:
88-
result = asyncio.run(_add_project())
89-
console.print(f"[green]{result.message}[/green]")
90-
except Exception as e:
91-
console.print(f"[red]Error adding project: {str(e)}[/red]")
89+
else:
90+
# Local mode: path is required
91+
if path is None:
92+
console.print("[red]Error: path argument is required in local mode[/red]")
9293
raise typer.Exit(1)
9394

94-
# Display usage hint
95-
console.print("\nTo use this project:")
96-
console.print(f" basic-memory --project={name} <command>")
97-
else:
98-
99-
@project_app.command("add")
100-
def add_project(
101-
name: str = typer.Argument(..., help="Name of the project"),
102-
path: str = typer.Argument(..., help="Path to the project directory"),
103-
set_default: bool = typer.Option(False, "--default", help="Set as default project"),
104-
) -> None:
105-
"""Add a new project."""
10695
# Resolve to absolute path
10796
resolved_path = Path(os.path.abspath(os.path.expanduser(path))).as_posix()
10897

@@ -112,16 +101,16 @@ async def _add_project():
112101
response = await call_post(client, "/projects/projects", json=data)
113102
return ProjectStatusResponse.model_validate(response.json())
114103

115-
try:
116-
result = asyncio.run(_add_project())
117-
console.print(f"[green]{result.message}[/green]")
118-
except Exception as e:
119-
console.print(f"[red]Error adding project: {str(e)}[/red]")
120-
raise typer.Exit(1)
104+
try:
105+
result = asyncio.run(_add_project())
106+
console.print(f"[green]{result.message}[/green]")
107+
except Exception as e:
108+
console.print(f"[red]Error adding project: {str(e)}[/red]")
109+
raise typer.Exit(1)
121110

122-
# Display usage hint
123-
console.print("\nTo use this project:")
124-
console.print(f" basic-memory --project={name} <command>")
111+
# Display usage hint
112+
console.print("\nTo use this project:")
113+
console.print(f" basic-memory --project={name} <command>")
125114

126115

127116
@project_app.command("remove")
@@ -147,84 +136,109 @@ async def _remove_project():
147136
console.print("[yellow]Note: The project files have not been deleted from disk.[/yellow]")
148137

149138

150-
if not config.cloud_mode_enabled:
139+
@project_app.command("default")
140+
def set_default_project(
141+
name: str = typer.Argument(..., help="Name of the project to set as CLI default"),
142+
) -> None:
143+
"""Set the default project when 'config.default_project_mode' is set.
151144
152-
@project_app.command("default")
153-
def set_default_project(
154-
name: str = typer.Argument(..., help="Name of the project to set as CLI default"),
155-
) -> None:
156-
"""Set the default project when 'config.default_project_mode' is set."""
145+
Note: This command is only available in local mode.
146+
"""
147+
config = ConfigManager().config
157148

158-
async def _set_default():
159-
async with get_client() as client:
160-
project_permalink = generate_permalink(name)
161-
response = await call_put(client, f"/projects/{project_permalink}/default")
162-
return ProjectStatusResponse.model_validate(response.json())
149+
if config.cloud_mode_enabled:
150+
console.print("[red]Error: 'default' command is not available in cloud mode[/red]")
151+
raise typer.Exit(1)
163152

164-
try:
165-
result = asyncio.run(_set_default())
166-
console.print(f"[green]{result.message}[/green]")
167-
except Exception as e:
168-
console.print(f"[red]Error setting default project: {str(e)}[/red]")
169-
raise typer.Exit(1)
153+
async def _set_default():
154+
async with get_client() as client:
155+
project_permalink = generate_permalink(name)
156+
response = await call_put(client, f"/projects/{project_permalink}/default")
157+
return ProjectStatusResponse.model_validate(response.json())
170158

171-
@project_app.command("sync-config")
172-
def synchronize_projects() -> None:
173-
"""Synchronize project config between configuration file and database."""
159+
try:
160+
result = asyncio.run(_set_default())
161+
console.print(f"[green]{result.message}[/green]")
162+
except Exception as e:
163+
console.print(f"[red]Error setting default project: {str(e)}[/red]")
164+
raise typer.Exit(1)
174165

175-
async def _sync_config():
176-
async with get_client() as client:
177-
response = await call_post(client, "/projects/config/sync")
178-
return ProjectStatusResponse.model_validate(response.json())
179166

180-
try:
181-
result = asyncio.run(_sync_config())
182-
console.print(f"[green]{result.message}[/green]")
183-
except Exception as e: # pragma: no cover
184-
console.print(f"[red]Error synchronizing projects: {str(e)}[/red]")
185-
raise typer.Exit(1)
167+
@project_app.command("sync-config")
168+
def synchronize_projects() -> None:
169+
"""Synchronize project config between configuration file and database.
186170
187-
@project_app.command("move")
188-
def move_project(
189-
name: str = typer.Argument(..., help="Name of the project to move"),
190-
new_path: str = typer.Argument(..., help="New absolute path for the project"),
191-
) -> None:
192-
"""Move a project to a new location."""
193-
# Resolve to absolute path
194-
resolved_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix()
171+
Note: This command is only available in local mode.
172+
"""
173+
config = ConfigManager().config
195174

196-
async def _move_project():
197-
async with get_client() as client:
198-
data = {"path": resolved_path}
199-
project_permalink = generate_permalink(name)
175+
if config.cloud_mode_enabled:
176+
console.print("[red]Error: 'sync-config' command is not available in cloud mode[/red]")
177+
raise typer.Exit(1)
200178

201-
# TODO fix route to use ProjectPathDep
202-
response = await call_patch(
203-
client, f"/{name}/project/{project_permalink}", json=data
204-
)
205-
return ProjectStatusResponse.model_validate(response.json())
179+
async def _sync_config():
180+
async with get_client() as client:
181+
response = await call_post(client, "/projects/config/sync")
182+
return ProjectStatusResponse.model_validate(response.json())
206183

207-
try:
208-
result = asyncio.run(_move_project())
209-
console.print(f"[green]{result.message}[/green]")
184+
try:
185+
result = asyncio.run(_sync_config())
186+
console.print(f"[green]{result.message}[/green]")
187+
except Exception as e: # pragma: no cover
188+
console.print(f"[red]Error synchronizing projects: {str(e)}[/red]")
189+
raise typer.Exit(1)
210190

211-
# Show important file movement reminder
212-
console.print() # Empty line for spacing
213-
console.print(
214-
Panel(
215-
"[bold red]IMPORTANT:[/bold red] Project configuration updated successfully.\n\n"
216-
"[yellow]You must manually move your project files from the old location to:[/yellow]\n"
217-
f"[cyan]{resolved_path}[/cyan]\n\n"
218-
"[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]",
219-
title="⚠️ Manual File Movement Required",
220-
border_style="yellow",
221-
expand=False,
222-
)
191+
192+
@project_app.command("move")
193+
def move_project(
194+
name: str = typer.Argument(..., help="Name of the project to move"),
195+
new_path: str = typer.Argument(..., help="New absolute path for the project"),
196+
) -> None:
197+
"""Move a project to a new location.
198+
199+
Note: This command is only available in local mode.
200+
"""
201+
config = ConfigManager().config
202+
203+
if config.cloud_mode_enabled:
204+
console.print("[red]Error: 'move' command is not available in cloud mode[/red]")
205+
raise typer.Exit(1)
206+
207+
# Resolve to absolute path
208+
resolved_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix()
209+
210+
async def _move_project():
211+
async with get_client() as client:
212+
data = {"path": resolved_path}
213+
project_permalink = generate_permalink(name)
214+
215+
# TODO fix route to use ProjectPathDep
216+
response = await call_patch(
217+
client, f"/{name}/project/{project_permalink}", json=data
223218
)
219+
return ProjectStatusResponse.model_validate(response.json())
224220

225-
except Exception as e:
226-
console.print(f"[red]Error moving project: {str(e)}[/red]")
227-
raise typer.Exit(1)
221+
try:
222+
result = asyncio.run(_move_project())
223+
console.print(f"[green]{result.message}[/green]")
224+
225+
# Show important file movement reminder
226+
console.print() # Empty line for spacing
227+
console.print(
228+
Panel(
229+
"[bold red]IMPORTANT:[/bold red] Project configuration updated successfully.\n\n"
230+
"[yellow]You must manually move your project files from the old location to:[/yellow]\n"
231+
f"[cyan]{resolved_path}[/cyan]\n\n"
232+
"[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]",
233+
title="⚠️ Manual File Movement Required",
234+
border_style="yellow",
235+
expand=False,
236+
)
237+
)
238+
239+
except Exception as e:
240+
console.print(f"[red]Error moving project: {str(e)}[/red]")
241+
raise typer.Exit(1)
228242

229243

230244
@project_app.command("info")

0 commit comments

Comments
 (0)