Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CLI/programa.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2697,7 +2697,9 @@ struct ProgramaCLI {
""",
execute: { ctx in
guard let workspaceRaw = self.optionValue(ctx.commandArgs, name: "--workspace") else {
throw CLIError(message: "close-workspace requires --workspace")
throw CLIError(
message: "close-workspace: --workspace <id|ref> is required (UUID or short ref like workspace:2). Refusing to target the current workspace implicitly."
)
}
var params: [String: Any] = [:]
let wsId = try self.normalizeWorkspaceHandle(workspaceRaw, client: ctx.client)
Expand Down Expand Up @@ -5155,7 +5157,11 @@ struct ProgramaCLI {

case "close-workspace", "select-workspace":
let parsed = try parse(values: ["workspace"])
try require(["workspace"], in: parsed.options)
if parsed.options["workspace"] == nil {
throw CLIError(
message: "\(command): --workspace <id|ref> is required (UUID or short ref like workspace:2). Refusing to target the current workspace implicitly."
)
}

case "rename-workspace", "rename-window":
_ = try parse(values: ["workspace"], minPositionals: 1, maxPositionals: nil)
Expand Down
4 changes: 2 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ Roadmap tracking: #141 (Phase 1 P0s #131-#134, Phase 2 upstream port round #135,

## Refactoring
- [ ] **P0** Remove all index-based APIs in favor of short ID refs (surface:N, pane:N, workspace:N, window:N) (#132)
- [ ] **P0** CLI commands should be workspace-relative using PROGRAMA_WORKSPACE_ID env var (not focused workspace) so agents in background workspaces don't affect the user's active workspace. Affected: send, send-key, send-panel, send-key-panel, new-split, new-pane, new-surface, close-surface, list-panes, list-pane-surfaces, list-panels, focus-pane, focus-panel, surface-health (#133)
- [ ] **P0** Remove `close-workspace` with no args — require explicit workspace short ID or UUID, with clear error message if missing (#134)
- [x] **P0** CLI commands should be workspace-relative using PROGRAMA_WORKSPACE_ID env var (not focused workspace) so agents in background workspaces don't affect the user's active workspace. Affected: send, send-key, send-panel, send-key-panel, new-split, new-pane, new-surface, close-surface, list-panes, list-pane-surfaces, list-panels, focus-pane, focus-panel, surface-health (#133 — already implemented; covered by tests_v2/test_workspace_relative.py)
- [x] **P0** Remove `close-workspace` with no args — require explicit workspace short ID or UUID, with clear error message if missing (#134)

## UI/UX Improvements
- [ ] Show loading indicator in terminal while it's loading
Expand Down
31 changes: 31 additions & 0 deletions tests_v2/test_workspace_relative.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ def _run_cli(cli: str, args: List[str], env_overrides: Optional[Dict[str, str]]
"""Run CLI command and return stdout."""
cmd = [cli, "--socket", SOCKET_PATH] + args
env = os.environ.copy()
# Hermetic: when this suite runs inside a Programa terminal, shell
# integration exports instance-scoped ids that point at that OTHER app
# instance (e.g. PROGRAMA_SURFACE_ID wins over the workspace target and
# breaks `send`). Strip them all; tests opt back in via env_overrides.
for var in ("PROGRAMA_WORKSPACE_ID", "PROGRAMA_SURFACE_ID", "PROGRAMA_PANEL_ID", "PROGRAMA_TAB_ID"):
env.pop(var, None)
if env_overrides:
env.update(env_overrides)
proc = subprocess.run(cmd, capture_output=True, text=True, check=False, env=env)
Expand Down Expand Up @@ -225,6 +231,30 @@ def test_non_json_output_uses_refs(c: cmux, cli: str) -> None:
print(" PASS: non-JSON output uses refs")


def test_close_workspace_requires_explicit_target(c: cmux, cli: str) -> None:
"""close-workspace with no --workspace must fail non-zero with an error
naming the accepted formats, and must not close anything (P0 #134)."""
before = len(c.list_workspaces())

cmd = [cli, "--socket", SOCKET_PATH, "close-workspace"]
env = os.environ.copy()
# An inherited PROGRAMA_WORKSPACE_ID must not turn the bare invocation
# into a valid close either; the target has to be explicit.
env.pop("PROGRAMA_WORKSPACE_ID", None)
proc = subprocess.run(cmd, capture_output=True, text=True, check=False, env=env)
merged = f"{proc.stdout}\n{proc.stderr}"

_must(proc.returncode != 0, "close-workspace with no args must exit non-zero")
_must("--workspace" in merged, f"error must name the missing option, got: {merged.strip()}")
_must(
"workspace:" in merged and "UUID" in merged,
f"error must name the accepted formats (UUID / workspace:N), got: {merged.strip()}",
)
_must(len(c.list_workspaces()) == before, "no workspace may be closed by a bare invocation")

print(" PASS: close-workspace requires an explicit workspace id/ref")


def main() -> int:
cli = _find_cli_binary()
print(f"Using CLI: {cli}")
Expand All @@ -239,6 +269,7 @@ def main() -> int:
test_v2_migrated_commands_output_refs(c, cli)
test_surface_health_workspace_relative(c, cli)
test_non_json_output_uses_refs(c, cli)
test_close_workspace_requires_explicit_target(c, cli)
finally:
c.close()

Expand Down
Loading