wafer-cli 0.2.8__tar.gz → 0.2.10__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 (65) hide show
  1. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/PKG-INFO +1 -1
  2. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/README.md +41 -2
  3. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/pyproject.toml +2 -1
  4. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_cli_coverage.py +125 -33
  5. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_cli_parity_integration.py +1 -1
  6. wafer_cli-0.2.10/tests/test_kernel_scope_cli.py +620 -0
  7. wafer_cli-0.2.10/tests/test_nsys_analyze.py +293 -0
  8. wafer_cli-0.2.10/tests/test_nsys_profile.py +157 -0
  9. wafer_cli-0.2.10/tests/test_output.py +263 -0
  10. wafer_cli-0.2.10/tests/test_skill_commands.py +231 -0
  11. wafer_cli-0.2.10/tests/test_targets_ops.py +206 -0
  12. wafer_cli-0.2.10/tests/test_wevin_cli.py +660 -0
  13. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/GUIDE.md +18 -7
  14. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/api_client.py +4 -0
  15. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/auth.py +85 -0
  16. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/cli.py +2339 -404
  17. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/corpus.py +158 -32
  18. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/evaluate.py +1232 -201
  19. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/gpu_run.py +5 -1
  20. wafer_cli-0.2.10/wafer/kernel_scope.py +554 -0
  21. wafer_cli-0.2.10/wafer/nsys_analyze.py +1042 -0
  22. wafer_cli-0.2.10/wafer/nsys_profile.py +511 -0
  23. wafer_cli-0.2.10/wafer/output.py +241 -0
  24. wafer_cli-0.2.10/wafer/problems.py +357 -0
  25. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/skills/wafer-guide/SKILL.md +13 -0
  26. wafer_cli-0.2.10/wafer/ssh_keys.py +261 -0
  27. wafer_cli-0.2.10/wafer/target_lock.py +270 -0
  28. wafer_cli-0.2.10/wafer/targets.py +842 -0
  29. wafer_cli-0.2.10/wafer/targets_ops.py +718 -0
  30. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/wevin_cli.py +129 -18
  31. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/workspaces.py +282 -182
  32. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/PKG-INFO +1 -1
  33. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/SOURCES.txt +13 -1
  34. wafer_cli-0.2.8/tests/test_isa_cli.py +0 -212
  35. wafer_cli-0.2.8/tests/test_wevin_cli.py +0 -68
  36. wafer_cli-0.2.8/wafer/nsys_analyze.py +0 -212
  37. wafer_cli-0.2.8/wafer/targets.py +0 -352
  38. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/setup.cfg +0 -0
  39. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_analytics.py +0 -0
  40. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_billing.py +0 -0
  41. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_config_integration.py +0 -0
  42. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_file_operations_integration.py +0 -0
  43. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_rocprof_compute_integration.py +0 -0
  44. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_ssh_integration.py +0 -0
  45. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_workflow_integration.py +0 -0
  46. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/__init__.py +0 -0
  47. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/analytics.py +0 -0
  48. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/autotuner.py +0 -0
  49. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/billing.py +0 -0
  50. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/config.py +0 -0
  51. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/global_config.py +0 -0
  52. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/inference.py +0 -0
  53. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/ncu_analyze.py +0 -0
  54. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/rocprof_compute.py +0 -0
  55. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/rocprof_sdk.py +0 -0
  56. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/rocprof_systems.py +0 -0
  57. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/templates/__init__.py +0 -0
  58. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/templates/ask_docs.py +0 -0
  59. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/templates/optimize_kernel.py +0 -0
  60. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/templates/trace_analyze.py +0 -0
  61. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/tracelens.py +0 -0
  62. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/dependency_links.txt +0 -0
  63. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/entry_points.txt +0 -0
  64. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/requires.txt +0 -0
  65. {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wafer-cli
3
- Version: 0.2.8
3
+ Version: 0.2.10
4
4
  Summary: CLI tool for running commands on remote GPUs and GPU kernel optimization agent
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: typer>=0.12.0
@@ -43,10 +43,16 @@ wafer remote-run --upload-dir ./my_code -- python3 train.py
43
43
 
44
44
  Create and manage persistent GPU environments.
45
45
 
46
+ **Available GPUs:**
47
+
48
+ - `MI300X` - AMD Instinct MI300X (192GB HBM3, ROCm)
49
+ - `B200` - NVIDIA Blackwell B200 (180GB HBM3e, CUDA) - default
50
+
46
51
  ```bash
47
52
  wafer workspaces list
48
- wafer workspaces create my-workspace
49
- wafer workspaces attach <workspace-id> # Get SSH credentials
53
+ wafer workspaces create my-workspace --gpu B200 --wait # NVIDIA B200
54
+ wafer workspaces create amd-dev --gpu MI300X # AMD MI300X
55
+ wafer workspaces ssh <workspace-id>
50
56
  wafer workspaces delete <workspace-id>
51
57
  ```
52
58
 
@@ -197,6 +203,39 @@ Now you can tab-complete:
197
203
 
198
204
  ---
199
205
 
206
+ ## AI Assistant Skills
207
+
208
+ Install the Wafer CLI skill to make wafer commands discoverable by your AI coding assistant:
209
+
210
+ ```bash
211
+ # Install for all supported tools (Claude Code, Codex CLI, Cursor)
212
+ wafer skill install
213
+
214
+ # Install for a specific tool
215
+ wafer skill install -t cursor # Cursor
216
+ wafer skill install -t claude # Claude Code
217
+ wafer skill install -t codex # Codex CLI
218
+
219
+ # Check installation status
220
+ wafer skill status
221
+
222
+ # Uninstall
223
+ wafer skill uninstall
224
+ ```
225
+
226
+ ### Installing from GitHub (Cursor)
227
+
228
+ You can also install the skill directly from GitHub in Cursor:
229
+
230
+ 1. Open Cursor Settings (Cmd+Shift+J / Ctrl+Shift+J)
231
+ 2. Navigate to **Rules** → **Add Rule** → **Remote Rule (Github)**
232
+ 3. Enter: `https://github.com/wafer-ai/skills`
233
+ 4. Cursor will automatically discover skills in `.cursor/skills/`
234
+
235
+ The skill provides comprehensive guidance for GPU kernel development, including documentation lookup, trace analysis, kernel evaluation, and optimization workflows.
236
+
237
+ ---
238
+
200
239
  ## Requirements
201
240
 
202
241
  - Python 3.10+
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wafer-cli"
3
- version = "0.2.8"
3
+ version = "0.2.10"
4
4
  description = "CLI tool for running commands on remote GPUs and GPU kernel optimization agent"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = [
@@ -78,6 +78,7 @@ ignore = [
78
78
  [tool.ruff.lint.per-file-ignores]
79
79
  "tests/**/*.py" = ["ANN201"] # Don't require return type annotations in tests
80
80
  "wafer/evaluate.py" = ["PLR0915", "PLR1702", "E402"] # complex deployment flows - TODO: refactor
81
+ "wafer/output.py" = ["ANN401"] # Output collector uses **kwargs for flexible event data
81
82
 
82
83
  [tool.ruff.lint.pylint]
83
84
  max-args = 7 # Max function arguments (Tiger Style: few parameters)
@@ -61,7 +61,7 @@ def _cleanup_test_workspaces() -> None:
61
61
  return
62
62
  for ws in workspaces:
63
63
  if ws.get("name", "").startswith("test-"):
64
- runner.invoke(app, ["workspaces", "delete", ws["id"]])
64
+ runner.invoke(app, ["workspaces", "delete", ws["id"], "-y"])
65
65
 
66
66
 
67
67
  @pytest.fixture(scope="session", autouse=True)
@@ -213,7 +213,7 @@ class TestWorkspacesCommands:
213
213
  finally:
214
214
  # Delete
215
215
  if ws_id:
216
- result = runner.invoke(app, ["workspaces", "delete", ws_id])
216
+ result = runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
217
217
 
218
218
  def test_workspaces_create_pretty(self) -> None:
219
219
  """Create workspace with pretty output."""
@@ -228,53 +228,43 @@ class TestWorkspacesCommands:
228
228
  return
229
229
 
230
230
  # Extract ID from output
231
- # Output format: "Created workspace: <name> (<uuid>)"
231
+ # Output format: "Creating workspace: <name> (<uuid>)"
232
232
  match = re.search(r"\(([a-f0-9-]{36})\)", result.output)
233
233
  if match:
234
234
  ws_id = match.group(1)
235
+ assert "Creating workspace:" in result.output
235
236
  assert ws_id is not None, f"Could not extract workspace ID from: {result.output}"
236
237
 
237
238
  finally:
238
239
  if ws_id:
239
- runner.invoke(app, ["workspaces", "delete", ws_id])
240
+ runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
240
241
 
241
- def test_workspaces_attach_json(self) -> None:
242
- """Attach to workspace with JSON output."""
243
- ws_name = f"test-attach-{os.getpid()}"
242
+ def test_workspaces_create_wait_json(self) -> None:
243
+ """Create workspace with --wait JSON output."""
244
+ ws_name = f"test-create-wait-{os.getpid()}"
244
245
  ws_id = None
245
246
 
246
247
  try:
247
- # Create workspace first
248
- result = runner.invoke(app, ["workspaces", "create", ws_name, "--json"])
248
+ result = runner.invoke(
249
+ app,
250
+ ["workspaces", "create", ws_name, "--wait", "--json"],
251
+ )
249
252
  if result.exit_code != 0:
250
253
  if "auth" in result.output.lower() or "not authenticated" in result.output.lower():
251
254
  pytest.skip("Not authenticated")
255
+ assert "error" in result.output.lower() or "no gpu" in result.output.lower()
252
256
  return
253
257
 
254
258
  data = json.loads(result.stdout)
255
- ws_id = data.get("id")
259
+ ws_id = data.get("workspace_id")
256
260
  assert ws_id is not None
257
-
258
- # Attach with JSON (avoids file I/O side effects)
259
- result = runner.invoke(app, ["workspaces", "attach", ws_id, "--json"])
260
- if result.exit_code == 0:
261
- attach_data = json.loads(result.stdout)
262
- # Should have SSH connection info
263
- assert "ssh_host" in attach_data
264
- assert "ssh_port" in attach_data
265
- assert "ssh_user" in attach_data
266
- assert "private_key_pem" in attach_data
267
- else:
268
- # May fail with 503 if no GPU available - that's ok
269
- assert (
270
- "503" in result.output
271
- or "No GPU" in result.output
272
- or "error" in result.output.lower()
273
- )
261
+ assert "ssh_host" in data
262
+ assert "ssh_port" in data
263
+ assert "ssh_user" in data
274
264
 
275
265
  finally:
276
266
  if ws_id:
277
- runner.invoke(app, ["workspaces", "delete", ws_id])
267
+ runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
278
268
 
279
269
  def test_workspaces_show_not_found(self) -> None:
280
270
  """Show workspace with invalid ID returns 404."""
@@ -288,10 +278,10 @@ class TestWorkspacesCommands:
288
278
  or "auth" in result.output.lower()
289
279
  )
290
280
 
291
- def test_workspaces_attach_not_found(self) -> None:
292
- """Attach to workspace with invalid ID returns 404."""
281
+ def test_workspaces_ssh_not_found(self) -> None:
282
+ """SSH to workspace with invalid ID returns 404."""
293
283
  fake_id = "00000000-0000-0000-0000-000000000000"
294
- result = runner.invoke(app, ["workspaces", "attach", fake_id])
284
+ result = runner.invoke(app, ["workspaces", "ssh", fake_id])
295
285
  if result.exit_code != 0:
296
286
  # Should be 404 or auth error
297
287
  assert (
@@ -300,6 +290,15 @@ class TestWorkspacesCommands:
300
290
  or "auth" in result.output.lower()
301
291
  )
302
292
 
293
+ def test_workspaces_exec_option_order(self) -> None:
294
+ """Exec should reject options after workspace name."""
295
+ result = runner.invoke(
296
+ app,
297
+ ["workspaces", "exec", "dev", "--timeout", "10", "--", "echo", "hi"],
298
+ )
299
+ assert result.exit_code != 0
300
+ assert "options must come before the workspace name" in result.output.lower()
301
+
303
302
 
304
303
  class TestNsysAnalyzeCommand:
305
304
  """Test wafer nvidia nsys analyze command."""
@@ -333,7 +332,24 @@ class TestNsysAnalyzeCommand:
333
332
  result = runner.invoke(
334
333
  app, ["nvidia", "nsys", "analyze", str(real_nsys_file), "--remote", "--json"]
335
334
  )
336
- assert result.exit_code == 0
335
+ if result.exit_code != 0:
336
+ combined = result.output.lower()
337
+ if "auth" in combined or "login" in combined or "401" in combined or "403" in combined:
338
+ pytest.skip("Not authenticated")
339
+ if (
340
+ "no gpu" in combined
341
+ or "no targets" in combined
342
+ or "unavailable" in combined
343
+ or "timed out" in combined
344
+ or "timeout" in combined
345
+ or "connect" in combined
346
+ or "service" in combined
347
+ or "billing" in combined
348
+ or "spend limit" in combined
349
+ or "402" in combined
350
+ ):
351
+ pytest.skip(f"Remote nsys unavailable: {result.output.strip()}")
352
+ assert result.exit_code == 0
337
353
  data = json.loads(result.stdout)
338
354
  assert "kernels" in data or "timeline" in data or "summary" in data
339
355
 
@@ -344,7 +360,24 @@ class TestNsysAnalyzeCommand:
344
360
  pytest.skip("No real .nsys-rep fixture available")
345
361
 
346
362
  result = runner.invoke(app, ["nvidia", "nsys", "analyze", str(real_nsys_file), "--remote"])
347
- assert result.exit_code == 0
363
+ if result.exit_code != 0:
364
+ combined = result.output.lower()
365
+ if "auth" in combined or "login" in combined or "401" in combined or "403" in combined:
366
+ pytest.skip("Not authenticated")
367
+ if (
368
+ "no gpu" in combined
369
+ or "no targets" in combined
370
+ or "unavailable" in combined
371
+ or "timed out" in combined
372
+ or "timeout" in combined
373
+ or "connect" in combined
374
+ or "service" in combined
375
+ or "billing" in combined
376
+ or "spend limit" in combined
377
+ or "402" in combined
378
+ ):
379
+ pytest.skip(f"Remote nsys unavailable: {result.output.strip()}")
380
+ assert result.exit_code == 0
348
381
  assert "NSYS" in result.output or "Kernel" in result.output or "Profile" in result.output
349
382
 
350
383
  def test_nsys_analyze_local(self, real_nsys_file: Path | None) -> None:
@@ -627,3 +660,62 @@ class TestEvaluateCommand:
627
660
  or "error" in combined
628
661
  or result.exit_code in (0, 1)
629
662
  )
663
+
664
+
665
+ class TestWorkspacesExecFlagPassthrough:
666
+ """Test that flags after -- are passed through to the command, not intercepted."""
667
+
668
+ def test_exec_passes_dash_i_flag(self) -> None:
669
+ """The -i flag should pass through, not be intercepted as --image."""
670
+ result = runner.invoke(app, [
671
+ "workspaces", "exec", "test-ws", "--", "echo", "-i", "test"
672
+ ])
673
+ # Should NOT fail with "no such option" or similar parse error
674
+ assert "no such option" not in result.output.lower()
675
+ assert "unrecognized" not in result.output.lower()
676
+ # May fail with workspace not found - that's fine, parsing succeeded
677
+
678
+ def test_exec_passes_dash_v_flag(self) -> None:
679
+ """The -v flag should pass through, not be intercepted as --verbose."""
680
+ result = runner.invoke(app, [
681
+ "workspaces", "exec", "test-ws", "--", "python", "-v", "-c", "print(1)"
682
+ ])
683
+ assert "no such option" not in result.output.lower()
684
+
685
+ def test_exec_passes_double_dash_help(self) -> None:
686
+ """--help after -- should pass to command, not show exec help."""
687
+ result = runner.invoke(app, [
688
+ "workspaces", "exec", "test-ws", "--", "python", "--help"
689
+ ])
690
+ # Should NOT show the exec command's help text
691
+ assert "Execute a command in workspace" not in result.output
692
+
693
+ def test_exec_passes_multiple_flags(self) -> None:
694
+ """Multiple flags like -i -v -n should all pass through."""
695
+ result = runner.invoke(app, [
696
+ "workspaces", "exec", "test-ws", "--", "grep", "-i", "-v", "-n", "pattern"
697
+ ])
698
+ assert "no such option" not in result.output.lower()
699
+
700
+ def test_exec_no_command_shows_error(self) -> None:
701
+ """Missing command after -- should show helpful error."""
702
+ result = runner.invoke(app, [
703
+ "workspaces", "exec", "test-ws", "--"
704
+ ])
705
+ assert result.exit_code != 0
706
+ assert "no command" in result.output.lower() or "error" in result.output.lower()
707
+
708
+ def test_exec_preserves_flag_order(self) -> None:
709
+ """Flags should be preserved in order they were given."""
710
+ # This tests the shlex.join behavior
711
+ result = runner.invoke(app, [
712
+ "workspaces", "exec", "test-ws", "--", "cmd", "-a", "1", "-b", "2"
713
+ ])
714
+ assert "no such option" not in result.output.lower()
715
+
716
+ def test_exec_with_equals_syntax(self) -> None:
717
+ """Flags with --flag=value syntax should pass through."""
718
+ result = runner.invoke(app, [
719
+ "workspaces", "exec", "test-ws", "--", "cmd", "--output=/tmp/out"
720
+ ])
721
+ assert "no such option" not in result.output.lower()
@@ -77,7 +77,7 @@ class TestWorkspacesWorkflow:
77
77
  finally:
78
78
  # Cleanup
79
79
  if ws_id:
80
- run_cli("workspaces", "delete", ws_id, check=False)
80
+ run_cli("workspaces", "delete", ws_id, "-y", check=False)
81
81
 
82
82
 
83
83
  @requires_api