swm-gpu 0.2.2__tar.gz → 0.2.4__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 (111) hide show
  1. swm_gpu-0.2.4/.agents/skills/swm-gpu-workflow/SKILL.md +317 -0
  2. swm_gpu-0.2.4/.agents/skills/swm-gpu-workflow/examples.md +68 -0
  3. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/CHANGELOG.md +56 -0
  4. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/PKG-INFO +10 -1
  5. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/pyproject.toml +8 -2
  6. swm_gpu-0.2.4/src/swm/__init__.py +1 -0
  7. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/bootstrap.py +60 -16
  8. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/cli.py +9 -2
  9. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/_helpers.py +18 -4
  10. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/config.py +22 -8
  11. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/costs.py +4 -6
  12. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/guard.py +26 -3
  13. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/images.py +1 -1
  14. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/models.py +75 -56
  15. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/pod.py +139 -24
  16. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/pricing.py +5 -3
  17. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/setup.py +0 -29
  18. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/sync.py +57 -4
  19. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/use.py +8 -2
  20. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/swarmui.py +8 -5
  21. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/guard.py +35 -5
  22. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/models/manifest.py +8 -3
  23. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/pricing/providers.py +96 -0
  24. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/autosync.py +9 -0
  25. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/pull.py +6 -4
  26. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/push.py +61 -13
  27. swm_gpu-0.2.2/.agents/skills/swm-gpu-workflow/SKILL.md +0 -276
  28. swm_gpu-0.2.2/src/swm/__init__.py +0 -1
  29. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/.gitignore +0 -0
  30. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/CODE_OF_CONDUCT.md +0 -0
  31. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/CONTRIBUTING.md +0 -0
  32. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/LICENSE +0 -0
  33. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/NOTICE +0 -0
  34. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/README.md +0 -0
  35. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/SECURITY.md +0 -0
  36. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/bootstrap_frameworks.py +0 -0
  37. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/bootstrap_ssh.py +0 -0
  38. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/__init__.py +0 -0
  39. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/remote.py +0 -0
  40. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/storage.py +0 -0
  41. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/config.py +0 -0
  42. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/__init__.py +0 -0
  43. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/billing.py +0 -0
  44. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/budget.py +0 -0
  45. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/db.py +0 -0
  46. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/reconcile.py +0 -0
  47. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/tracker.py +0 -0
  48. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/cuda.py +0 -0
  49. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/__init__.py +0 -0
  50. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/axolotl.py +0 -0
  51. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/comfyui.py +0 -0
  52. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/llm_studio.py +0 -0
  53. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/ollama.py +0 -0
  54. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/open_webui.py +0 -0
  55. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/vllm_server.py +0 -0
  56. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/images.py +0 -0
  57. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/models/__init__.py +0 -0
  58. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/models/civitai.py +0 -0
  59. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/models/huggingface.py +0 -0
  60. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/models/resolver.py +0 -0
  61. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/pricing/__init__.py +0 -0
  62. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/pricing/calculator.py +0 -0
  63. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/__init__.py +0 -0
  64. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/aws.py +0 -0
  65. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/azure.py +0 -0
  66. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/base.py +0 -0
  67. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/coreweave.py +0 -0
  68. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/fluidstack.py +0 -0
  69. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/gcp.py +0 -0
  70. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/lambda_labs.py +0 -0
  71. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/runpod.py +0 -0
  72. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/tensordock.py +0 -0
  73. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/vastai.py +0 -0
  74. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/vultr.py +0 -0
  75. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/remote/__init__.py +0 -0
  76. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/remote/ssh.py +0 -0
  77. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/storage/__init__.py +0 -0
  78. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/storage/b2.py +0 -0
  79. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/storage/base.py +0 -0
  80. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/storage/gcs.py +0 -0
  81. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/storage/s3.py +0 -0
  82. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/__init__.py +0 -0
  83. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/_autosync_daemon.sh +0 -0
  84. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/_common.py +0 -0
  85. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/paths.py +0 -0
  86. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/preflight.py +0 -0
  87. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/watcher.py +0 -0
  88. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/swm_main.py +0 -0
  89. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/.gitignore +0 -0
  90. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/README.md +0 -0
  91. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/download-music.sh +0 -0
  92. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/package-lock.json +0 -0
  93. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/package.json +0 -0
  94. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/remotion.config.ts +0 -0
  95. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/Root.tsx +0 -0
  96. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/ChapterCard.tsx +0 -0
  97. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/EndCard.tsx +0 -0
  98. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/GpuTable.tsx +0 -0
  99. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/OutputBlock.tsx +0 -0
  100. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/PHBadge.tsx +0 -0
  101. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/ProviderCascade.tsx +0 -0
  102. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/TerminalFrame.tsx +0 -0
  103. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/TypingLine.tsx +0 -0
  104. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/Wordmark.tsx +0 -0
  105. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/compositions/Hero16x9.tsx +0 -0
  106. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/compositions/Square1x1.tsx +0 -0
  107. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/compositions/Vertical9x16.tsx +0 -0
  108. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/config/music.ts +0 -0
  109. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/script/scrubbed-logs.ts +0 -0
  110. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/styles/tokens.ts +0 -0
  111. {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/tsconfig.json +0 -0
@@ -0,0 +1,317 @@
1
+ ---
2
+ name: swm-gpu-workflow
3
+ description: >
4
+ Use when the user wants to search cloud GPUs across providers, provision a
5
+ GPU pod, install AI frameworks (ComfyUI, SwarmUI, vLLM, Ollama, Open WebUI,
6
+ Axolotl, H2O LLM Studio), pull models from HuggingFace / Civitai / URLs to a
7
+ unified store, sync workspaces to S3, manage idle-pod lifecycle, or track
8
+ GPU spend — all via the swm CLI. Do NOT use for general Docker, Kubernetes,
9
+ or non-GPU cloud tasks.
10
+ license: Apache-2.0
11
+ compatibility: "macOS or Linux, Python 3.11+, SSH client"
12
+ metadata:
13
+ author: swm-gpu
14
+ version: "0.2.3"
15
+ docs-url: https://swmgpu.com
16
+ repository: https://github.com/swm-gpu/swm
17
+ ---
18
+
19
+ # swm — Cloud GPU Workflow Manager
20
+
21
+ One CLI to search, provision, install, sync, and tear down GPU pods across 10 cloud providers. Why swm over ad-hoc SSH scripts:
22
+
23
+ - **Cross-provider search** — `swm gpus` hits 10 providers with live pricing and stock in one call.
24
+ - **Declarative frameworks** — `swm setup install <name>` brings up ComfyUI / SwarmUI / vLLM / Ollama / Open WebUI / Axolotl / llm-studio idempotently; venv, custom nodes, and multi-GPU tensor parallelism handled.
25
+ - **Unified model store** — `swm models pull` auto-detects HF / Civitai / Ollama / URL refs, lands files under `/workspace/models/<bucket>/`, symlinks them into each framework's expected path.
26
+ - **Workspace persistence** — three-tier S3 sync (inotify → s5cmd → tar) survives auto-down. `pod create -w <name>` restores the last session.
27
+ - **Lifecycle guard** — `auto-down` / `auto-stop` / `remind` policies stop idle bills.
28
+
29
+ > **Stop condition.** Don't hand off until **Phase 5** passes. A pod that's "almost up" still bills.
30
+
31
+ ## Prerequisites
32
+
33
+ - `swm --version` works (`pipx install swm-gpu` or `brew tap swm-gpu/swm && brew install swm`).
34
+ - `ssh` and `scp` on PATH.
35
+ - At least one provider key: `swm config set runpod.api_key <key>` (or `vastai`, `lambda`, …).
36
+ - Optional `/workspace` persistence: `swm config set b2.key_id …` + `swm config set storage.default b2:<bucket>`.
37
+
38
+ ## Phase 0 — State check
39
+
40
+ ```bash
41
+ swm pod list && swm use --show
42
+ ```
43
+
44
+ If a matching pod is `RUNNING`, set it active (`swm use <name>`) and skip to Phase 5. Otherwise continue.
45
+
46
+ ## Phase 1 — Clarify
47
+
48
+ Use `AskQuestion`:
49
+
50
+ | Question | Options |
51
+ |---|---|
52
+ | **VRAM floor**? | `≤16 GB` / `16–24 GB` / `24–48 GB` / `48–80 GB` / `>80 GB` |
53
+ | **Provider**? | `runpod` / `vastai` / `lambda` / `tensordock` / `vultr` / `auto (cheapest)` |
54
+ | **Lifecycle**? | `auto-down` (default) / `auto-stop` / `remind` / `manual` |
55
+ | **Idle timeout**? | `10` / `20` / `30` / `60` / `120` min |
56
+ | Persist `/workspace`? | `yes` (default) / `no — one-shot, --no-storage` |
57
+
58
+ | VRAM floor | GPU class to search |
59
+ |---|---|
60
+ | ≤16 GB | `RTX 4090`, `A4000`, `RTX 6000 Ada` |
61
+ | 16–24 GB | `RTX 4090`, `RTX 6000 Ada`, `L40S` |
62
+ | 24–48 GB | `A100 40GB`, `A100 80GB`, `L40S` |
63
+ | 48–80 GB | `H100`, `H100 SXM`, `A100 80GB` |
64
+ | >80 GB | `H200`, `B200`, multi-GPU `H100 SXM` |
65
+
66
+ For unsupported frameworks, read the README for: install method (`pip` vs `uv sync` vs custom), ComfyUI custom node vs standalone server, default ports, model cache location, UI mode toggles.
67
+
68
+ ## Phase 2 — Pick GPU
69
+
70
+ ```bash
71
+ swm gpus -g <class> --sort price --secure | head -20
72
+ ```
73
+
74
+ Pick the cheapest in-stock SKU meeting the VRAM floor. **Capture the exact display name** (e.g. `"NVIDIA H100 80GB HBM3"`) — that's what `-g` on `pod create` wants. Quote it; spaces break shells.
75
+
76
+ ## Phase 3 — Provision
77
+
78
+ ```bash
79
+ swm pod create -p <provider> -g "<gpu-display-name>" \
80
+ -n <name> --lifecycle <policy> --idle-timeout <min> -y
81
+ ```
82
+
83
+ - `-y` is **mandatory** in agent shells — the `Proceed?` prompt has no fallback.
84
+ - Container disk wipes on stop; only `/workspace` persists. `--volume N` sizes it (RunPod requires `>= 1`).
85
+ - `-w <name>` restores the last workspace pushed under that name.
86
+ - After this the new pod is the active pod; subsequent `swm run` / `swm setup` can omit the id.
87
+
88
+ ## Phase 4 — Install
89
+
90
+ ### 4a. Prefer built-in frameworks
91
+
92
+ | Framework | Use when |
93
+ |---|---|
94
+ | `comfyui` | Image/video gen, custom-node ecosystems |
95
+ | `swarmui` | Higher-level wrapper around ComfyUI |
96
+ | `vllm` | Multi-GPU LLM serving (OpenAI-compatible) |
97
+ | `ollama` | Single-GPU LLM chat |
98
+ | `open-webui` | ChatGPT-style frontend for Ollama / vLLM |
99
+ | `axolotl` | LLM fine-tuning |
100
+ | `llm-studio` | H2O no-code fine-tuning |
101
+
102
+ ```bash
103
+ swm setup list
104
+ swm setup install <name> # idempotent
105
+ swm setup start <name> # background, prints proxy URL, probes health
106
+ swm setup start vllm --model Qwen/Qwen3-8B # vLLM model selection
107
+ swm setup start comfyui --port 8288 --extra-args "--use-sage-attention"
108
+ swm setup stop <name>
109
+ ```
110
+
111
+ vLLM auto-detects GPU count for tensor parallelism. swm opens SSH tunnels for unexposed ports and probes `/health` before reporting ready.
112
+
113
+ ### 4b. Custom tools — chain `swm run`
114
+
115
+ Each `swm run` is its own SSH session. Use absolute paths, `&&`-chain idempotent commands, install into `/workspace`:
116
+
117
+ ```bash
118
+ swm run "pip install -q uv && mkdir -p /workspace/.cache/uv \
119
+ && cd /workspace && [ -d <repo> ] || git clone --depth 1 <git-url> \
120
+ && cd <repo> && UV_CACHE_DIR=/workspace/.cache/uv uv sync"
121
+ ```
122
+
123
+ Use `uv sync` over `pip install -e .` when `pyproject.toml` has `[tool.uv.sources]` — pip silently ignores those overrides.
124
+
125
+ ### 4c. ComfyUI custom nodes
126
+
127
+ Install **before** `swm setup start comfyui`. Custom-node deps go into ComfyUI's venv (not system pip):
128
+
129
+ ```bash
130
+ swm run "cd /workspace/ComfyUI/custom_nodes \
131
+ && [ -d <node-dir> ] || git clone --depth 1 <git-url> \
132
+ && /workspace/ComfyUI/venv/bin/pip install -r <node-dir>/requirements.txt"
133
+ ```
134
+
135
+ ### 4d. Models — use the unified store
136
+
137
+ `swm models pull` lands files under `/workspace/models/<bucket>/` and symlinks them into each framework's expected path. Source auto-detected from the ref shape:
138
+
139
+ ```bash
140
+ swm models pull <pod> Qwen/Qwen3-8B # HuggingFace
141
+ swm models pull <pod> civitai:1925758 --as lora # Civitai
142
+ swm models pull <pod> https://example.com/m.safetensors --as checkpoint # URL
143
+ swm models pull <pod> ollama:llama3 # Ollama
144
+ swm models pull <pod> Comfy-Org/Wan_2.2_ComfyUI_Repackaged \
145
+ --as lora --filename split_files/loras/wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors
146
+
147
+ swm models info <ref> # metadata before pulling
148
+ swm models list <pod> [--all] # tracked (+ untracked) files
149
+ swm models link <pod> /abs/path/to/file --as vae
150
+ swm models remove <pod> <name>
151
+ ```
152
+
153
+ API keys (set once; legacy `hf_token` also read):
154
+
155
+ ```bash
156
+ swm config set hf.api_key <token> # gated HF models
157
+ swm config set civitai.api_key <token> # Civitai gated / NSFW
158
+ ```
159
+
160
+ `--filename` supports nested HF paths (`split_files/loras/foo.safetensors`). If a bare basename 404s, swm prints matching paths from the repo. Multi-file pulls from one HF repo get distinct manifest entries (`hf:{repo}:{filename}`); no overwrites.
161
+
162
+ ### 4e. Background daemon pattern
163
+
164
+ Mirror swm's `exec_background`: `nohup … > log 2>&1 < /dev/null &` with `&` **outside** `bash -c`, then sleep + `pgrep` so the call returns non-zero on crash:
165
+
166
+ ```bash
167
+ swm run "nohup env <VAR>=<val> /workspace/<repo>/.venv/bin/<entrypoint> \
168
+ --host 127.0.0.1 --port <port> > /workspace/<name>.log 2>&1 < /dev/null & \
169
+ sleep 3 && pgrep -fa <entrypoint> \
170
+ || (tail -50 /workspace/<name>.log; exit 1)"
171
+ ```
172
+
173
+ Bind `127.0.0.1` for internal services, `0.0.0.0` only if the proxy URL needs to reach it. Omit `< /dev/null` and SSH may hang on stdin.
174
+
175
+ ## Phase 5 — Verify
176
+
177
+ Every claim must hold before hand-off:
178
+
179
+ - Framework main port returns `200` (ComfyUI `:8188`, vLLM `:8000/v1/models`, Ollama `:11434/api/tags`, Open WebUI `:8080/api/version`).
180
+ - Side-server `/health` returns `200` (if any).
181
+ - Custom-node directory appears in the host's startup log.
182
+ - Expected node/model types appear in `/object_info` (ComfyUI) or `/v1/models` (vLLM).
183
+ - Checkpoint dirs non-empty and roughly the size from the model card.
184
+ - VRAM in the expected ballpark (low if lazy-loaded, near full after first request).
185
+ - `df -h /workspace` not over 80 %.
186
+
187
+ If any check fails, iterate on the relevant phase. Don't hand off a half-working pod.
188
+
189
+ ## Phase 6 — Hand off
190
+
191
+ 1. **Public URL** verbatim (RunPod: `https://<pod>-<port>.proxy.runpod.net`).
192
+ 2. **Sync status**: `swm sync status <pod>` — confirm watcher running, last push stamp recent.
193
+ 3. **Local download** for workflow / config files: `swm download <relative-path> -d ~/Downloads`.
194
+ 4. **Framework foot-guns** — spell out UI toggles (e.g. ACE-Step's `cloud → local` mode, model-selector defaults). #1 reason a working pod looks broken.
195
+ 5. **Resume command** with `-w <name>` so the next session restores the workspace.
196
+ 6. **Log tails**: `swm run "tail -f /tmp/<host>.log"` for live debugging.
197
+
198
+ See [examples.md](examples.md) for a fully worked run (ACE-Step on H100).
199
+
200
+ ---
201
+
202
+ # Command reference
203
+
204
+ ## GPU search
205
+
206
+ ```bash
207
+ swm gpus -g h200 --secure --sort price # filter, secure cloud only, cheapest first
208
+ swm gpus -g h200 -c 4 --max-price 4 # 4×H200 under $4/hr
209
+ swm gpus -p vastai -n 50 --all # single provider, more rows
210
+ ```
211
+
212
+ Unknown provider slugs raise `UsageError` listing valid ones.
213
+
214
+ ## Pod management
215
+
216
+ ```bash
217
+ swm pod create -p runpod -g h200 -n s1 -y
218
+ swm pod create -p vastai -g h200 -n train --gpu-count 4 --volume 200 --cloud-type SECURE -y
219
+ swm pod list # auto-prunes stale entries vs live provider state
220
+ swm pod status / stop / start / down / prune <pod>
221
+ ```
222
+
223
+ ## Sync
224
+
225
+ ```bash
226
+ swm sync pull [pod] # storage → pod
227
+ swm sync pull [pod] --tar # tarball mode (600k+ small files; fails loudly on error)
228
+ swm sync push [pod] # pod → storage
229
+ swm sync watch [pod] # inotify auto-push
230
+ swm sync status [pod] # s5cmd, watcher, autosync, pending changes
231
+ ```
232
+
233
+ Three-tier sync: inotify watcher → incremental s5cmd → tar. Push reconciles inotify events with a `find -newer` scan so `pip install` bursts aren't missed. Pull re-runs framework link-repair steps so `/workspace/models/` symlinks stay wired after restore.
234
+
235
+ ## Guard
236
+
237
+ ```bash
238
+ swm guard set <pod> --mode auto-down --idle-timeout 30 # set/update policy
239
+ swm guard set <pod> --mode remind # notify-only
240
+ swm guard disable <pod> [--force-manual] # remove override / pin manual
241
+ swm guard defaults --mode auto-down --idle-timeout 30 # for new pods
242
+ swm guard list # live status
243
+ swm guard stop-daemon
244
+ ```
245
+
246
+ Monitors 4 signals: SSH sessions (`who`), GPU utilization, filesystem writes, active processes. Setting `auto-stop`/`auto-down` auto-starts the local daemon. `--idle-timeout 0` and `--poll-interval 0` are honored as explicit values (not unset).
247
+
248
+ ## Costs
249
+
250
+ ```bash
251
+ swm costs live # right-now burn
252
+ swm costs summary [--period today|...] # breakdown by provider/pod (today = UTC day)
253
+ swm costs reconcile # verify vs provider billing (RunPod, Vast.ai)
254
+ swm costs budget set 100 # $/month alert
255
+ ```
256
+
257
+ ## Remote
258
+
259
+ ```bash
260
+ swm ssh <pod>
261
+ swm run [pod] '<cmd>'
262
+ swm upload [pod] ./local remote/path
263
+ swm download [pod] remote/file -d ~/Downloads
264
+ ```
265
+
266
+ ## Pricing reference
267
+
268
+ ```bash
269
+ swm pricing compare --gpu h100|h200|b200|a100|rtx-4090|l40s
270
+ swm pricing estimate -g h200 --hours 24
271
+ ```
272
+
273
+ ## Instance ID format
274
+
275
+ `provider:id` (`runpod:abc123`, `vastai:34182944`). A bare id auto-resolves by querying configured providers. `swm use <pod>` validates and sets the active pod.
276
+
277
+ ## Providers
278
+
279
+ | Slug | API | Live pricing |
280
+ |---|---|---|
281
+ | `runpod` | GraphQL | yes |
282
+ | `vastai` | REST | yes |
283
+ | `lambda` | REST | yes |
284
+ | `vultr` | REST | yes |
285
+ | `tensordock` | REST | yes |
286
+ | `fluidstack` | REST | yes |
287
+ | `aws` | boto3 | yes |
288
+ | `gcp` | gcloud CLI | yes |
289
+ | `azure` | az CLI (`pip install swm-gpu[azure]`) | yes |
290
+ | `coreweave` | Kubernetes | yes |
291
+
292
+ ## Storage
293
+
294
+ S3-compatible via s5cmd. Config keys:
295
+
296
+ | Backend | Keys |
297
+ |---|---|
298
+ | Backblaze B2 | `b2.key_id`, `b2.app_key`, `b2.bucket` |
299
+ | Amazon S3 | `s3.access_key`, `s3.secret_key`, `s3.bucket` |
300
+ | Google GCS | `gcs.hmac_access`, `gcs.hmac_secret`, `gcp.bucket` |
301
+
302
+ Default: `swm config set storage.default b2:<bucket>`. Credentials are masked by suffix in `config get/list` and never written to the pod (passed as transient env vars per s5cmd call).
303
+
304
+ ---
305
+
306
+ ## Anti-patterns
307
+
308
+ - **Never omit `-y` on `swm pod create`** — agent shells hang on the `Proceed?` prompt.
309
+ - **Never install outside `/workspace`** — container disk wipes on stop; only `/workspace` survives auto-down.
310
+ - **Never drop `&` outside `bash -c`** when starting daemons — SSH waits for foreground and `swm run` hangs.
311
+ - **Never reuse one venv across frameworks** with conflicting `torch` pins — separate venvs (~7 GB each) beat dependency hell.
312
+ - **Never trust `pip install -e .`** for projects with `[tool.uv.sources]` — use `uv sync`.
313
+ - **Never `swm run -q`** during initial setup — quiet mode swallows install errors. Reserve `-q` for scripted health checks.
314
+ - **Never tear down before `du -sh /workspace`** — 10–30 GB bloat extends the next pull.
315
+ - **Never guess nested HF file paths** — run `swm models info <repo>` and copy the exact `rfilename`.
316
+ - **Never skip Phase 0** — a forgotten running pod bills silently.
317
+ - **Never hand off** before Phase 5 passes.
@@ -0,0 +1,68 @@
1
+ # swm worked examples
2
+
3
+ Full end-to-end runs that pass Phase 5. Use these as templates when bringing up a new framework.
4
+
5
+ ## ACE-Step on H100 (ComfyUI custom node + side server)
6
+
7
+ Music generation with the ACE-Step diffusion model. Two processes: ComfyUI on `:8188` (public via RunPod proxy) and `acestep-openrouter` on `127.0.0.1:8002` (internal, called by the custom node).
8
+
9
+ ```bash
10
+ swm pod create -p runpod -g "NVIDIA H100 80GB HBM3" \
11
+ -n ace-step-test --lifecycle auto-down --idle-timeout 20 -y
12
+
13
+ swm setup install comfyui
14
+
15
+ swm run "pip install -q uv"
16
+ swm run "mkdir -p /workspace/.cache/uv /workspace/.cache/huggingface \
17
+ && cd /workspace && [ -d ACE-Step-1.5 ] \
18
+ || git clone --depth 1 https://github.com/ace-step/ACE-Step-1.5.git \
19
+ && cd ACE-Step-1.5 && UV_CACHE_DIR=/workspace/.cache/uv uv sync"
20
+
21
+ swm run "cd /workspace/ComfyUI/custom_nodes \
22
+ && [ -d ACE-Step-ComfyUI ] \
23
+ || git clone --depth 1 https://github.com/ace-step/ACE-Step-ComfyUI.git \
24
+ && /workspace/ComfyUI/venv/bin/pip install \
25
+ -r ACE-Step-ComfyUI/requirements.txt"
26
+
27
+ swm run "nohup env \
28
+ ACESTEP_CHECKPOINTS_DIR=/workspace/.cache/acestep-checkpoints \
29
+ HF_HOME=/workspace/.cache/huggingface \
30
+ /workspace/ACE-Step-1.5/.venv/bin/acestep-openrouter \
31
+ --host 127.0.0.1 --port 8002 \
32
+ > /workspace/acestep.log 2>&1 < /dev/null & \
33
+ sleep 3 && pgrep -fa acestep-openrouter \
34
+ || (tail -60 /workspace/acestep.log; exit 1)"
35
+
36
+ swm setup start comfyui
37
+ ```
38
+
39
+ **Verified end state:**
40
+
41
+ - ComfyUI 0.19.3 on `:8188`, ACE-Step custom node imported in 0.0 s
42
+ - `acestep-openrouter` on `127.0.0.1:8002`, `/health` → `{"status":"ok"}`, `/v1/models` → `acemusic/acestep-v15-turbo`
43
+ - Checkpoints (9.4 GB) under `/workspace/.cache/acestep-checkpoints/`: `acestep-v15-turbo` (4.5 GB), `acestep-5Hz-lm-1.7B` (3.5 GB), `Qwen3-Embedding-0.6B` (1.2 GB), `vae` (322 MB)
44
+ - Workflow at `/workspace/ComfyUI/custom_nodes/ACE-Step-ComfyUI/workflows/text2music.json`
45
+ - GPU memory 8.6 / 80 GiB before first inference
46
+
47
+ **Hand-off foot-gun:** `AceStepText2MusicServer` defaults to `cloud` mode and must be flipped to `local` in the ComfyUI node UI — otherwise the H100 sits idle and burns money.
48
+
49
+ **Switch to XL 4B variant** (without recreating the pod):
50
+
51
+ ```bash
52
+ swm run "pkill -f acestep-openrouter; nohup env \
53
+ ACESTEP_CONFIG_PATH=acestep-v15-xl-turbo \
54
+ ACESTEP_CHECKPOINTS_DIR=/workspace/.cache/acestep-checkpoints \
55
+ HF_HOME=/workspace/.cache/huggingface \
56
+ /workspace/ACE-Step-1.5/.venv/bin/acestep-openrouter \
57
+ --host 127.0.0.1 --port 8002 \
58
+ > /workspace/acestep.log 2>&1 < /dev/null & \
59
+ sleep 3 && pgrep -fa acestep-openrouter"
60
+ ```
61
+
62
+ **Resume next session** (restores `/workspace` from S3, custom nodes and checkpoints intact):
63
+
64
+ ```bash
65
+ swm pod create -p runpod -g "NVIDIA H100 80GB HBM3" -n ace-step-test \
66
+ -w ace-step-test --lifecycle auto-down --idle-timeout 20 -y
67
+ swm setup start comfyui
68
+ ```
@@ -6,6 +6,62 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.4] - 2026-05-27
10
+
11
+ ### Fixed
12
+ - `swm sync push --force` now propagates the s5cmd exit code through the CLI;
13
+ previously partial failures (e.g. B2 503 SlowDown on a single object) were
14
+ silently reported as success.
15
+ - `swm sync push --force` advances the push stamp even on partial s5cmd
16
+ failure during the initial baseline upload, so the next autosync cycle can
17
+ retry missed files via `find -newer` instead of permanently refusing to start.
18
+ - `swm sync auto --force` touches the push stamp on the pod when missing,
19
+ matching the client-side `--force` semantics so the daemon's internal stamp
20
+ check no longer blocks startup after a forced push.
21
+ - `swm pod create -n <name>` (without `-w`) no longer fails when `/workspace`
22
+ contains files from the docker image — `swm` now uploads the existing
23
+ contents as the new workspace baseline and starts autosync normally. This
24
+ was the root cause of silent data loss when a pod was created without `-w`.
25
+
26
+ ### Added
27
+ - `swm pod status <pod>` now reports autosync running state, watcher state,
28
+ last push age, and pending change count over SSH. Loudly warns when
29
+ autosync is not running on a pod that has been up > 5 minutes.
30
+ - `swm pod down --force-down` flag to override the new sync-safety guard
31
+ (terminate even if the workspace push did not write a recent stamp).
32
+
33
+ ### Changed
34
+ - `swm pod down` refuses to wipe `/workspace` or terminate the pod when the
35
+ workspace push exits non-zero or fails to write a push stamp within the
36
+ last 10 minutes. The lifecycle guard's `auto-down` path applies the same
37
+ guard and leaves the pod alive on partial-push failure.
38
+
39
+ ## [0.2.3] - 2026-05-24
40
+
41
+ ### Fixed
42
+ - `swm sync pull --tar` no longer reports success when download/extract fails.
43
+ - `swm models pull` tracks each HF file under a unique manifest key; multi-file
44
+ repos (e.g. high/low LoRA pairs) no longer overwrite each other.
45
+ - HF pull 404s suggest nested repo paths; `--filename` resolves basenames when unique.
46
+ - `swm guard set` honors `--idle-timeout 0` / `--poll-interval 0` and starts the
47
+ local guard daemon for auto-stop/auto-down policies.
48
+ - `swm sync status` reports push stamp, watcher, pending changes, and auto-sync.
49
+ - `swm use` validates the pod exists before setting the active pod.
50
+ - SwarmUI install creates `dlbackend` before cloning ComfyUI (fixes #9).
51
+ - `swm gpus -p badslug` errors instead of returning empty results.
52
+ - `swm costs summary --period today` uses UTC calendar day, not rolling 24h.
53
+ - Cost tracking failures on pod lifecycle commands emit dim warnings.
54
+
55
+ ### Changed
56
+ - Removed deprecated shims: `swm models set`, `swm pod gpus`, hidden `setup comfyui` /
57
+ `setup swarmui`.
58
+ - `swm guard disable --force-manual` pins a pod to manual mode.
59
+ - `swm pricing` supports H100, A100, RTX 4090, and L40S reference data.
60
+ - Shell completion suggests `provider:id` pod references.
61
+ - Config masking uses suffix-based sensitive key detection.
62
+ - Azure optional dependencies in `pyproject.toml` (`pip install swm-gpu[azure]`).
63
+ - Regenerated repo docs (`docs/cli-reference.md`, `docs/configuration.md`).
64
+
9
65
  ## [0.2.2] - 2026-05-24
10
66
 
11
67
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swm-gpu
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: One CLI to search, provision, and manage cloud GPUs across 10 providers
5
5
  Project-URL: Homepage, https://swmgpu.com
6
6
  Project-URL: Repository, https://github.com/swm-gpu/swm
@@ -29,7 +29,16 @@ Requires-Dist: httpx>=0.27
29
29
  Requires-Dist: rich>=13.0
30
30
  Requires-Dist: tomli-w>=1.0
31
31
  Provides-Extra: all
32
+ Requires-Dist: azure-identity>=1.15; extra == 'all'
33
+ Requires-Dist: azure-mgmt-compute>=30.0; extra == 'all'
34
+ Requires-Dist: azure-mgmt-network>=25.0; extra == 'all'
35
+ Requires-Dist: azure-mgmt-resource>=23.0; extra == 'all'
32
36
  Requires-Dist: kubernetes>=29.0; extra == 'all'
37
+ Provides-Extra: azure
38
+ Requires-Dist: azure-identity>=1.15; extra == 'azure'
39
+ Requires-Dist: azure-mgmt-compute>=30.0; extra == 'azure'
40
+ Requires-Dist: azure-mgmt-network>=25.0; extra == 'azure'
41
+ Requires-Dist: azure-mgmt-resource>=23.0; extra == 'azure'
33
42
  Provides-Extra: coreweave
34
43
  Requires-Dist: kubernetes>=29.0; extra == 'coreweave'
35
44
  Provides-Extra: dev
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "swm-gpu"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "One CLI to search, provision, and manage cloud GPUs across 10 providers"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -30,8 +30,14 @@ dependencies = [
30
30
 
31
31
  [project.optional-dependencies]
32
32
  coreweave = ["kubernetes>=29.0"]
33
+ azure = [
34
+ "azure-identity>=1.15",
35
+ "azure-mgmt-compute>=30.0",
36
+ "azure-mgmt-network>=25.0",
37
+ "azure-mgmt-resource>=23.0",
38
+ ]
33
39
  dev = ["nuitka", "ordered-set", "zstandard"]
34
- all = ["kubernetes>=29.0"]
40
+ all = ["kubernetes>=29.0", "azure-identity>=1.15", "azure-mgmt-compute>=30.0", "azure-mgmt-network>=25.0", "azure-mgmt-resource>=23.0"]
35
41
 
36
42
  [project.urls]
37
43
  Homepage = "https://swmgpu.com"
@@ -0,0 +1 @@
1
+ __version__ = "0.2.4"
@@ -309,20 +309,29 @@ _WS_MARKER_NAMES = (
309
309
  )
310
310
 
311
311
 
312
- def _ensure_workspace_empty_on_pod(session: RemoteSession) -> None:
313
- """Raise if /workspace/ on the pod contains any non-marker files.
312
+ def _workspace_leftover_files(session: RemoteSession) -> list[str]:
313
+ """Return paths in /workspace/ that aren't swm markers.
314
314
 
315
- A blind ``touch PUSH_STAMP`` on a non-empty workspace would silently
316
- declare those files synced even though they were never uploaded.
317
- Refuse, and tell the user to push or clear before attaching.
315
+ Non-marker files commonly appear because docker images use
316
+ /workspace as a default WORKDIR and seed it with content (e.g. the
317
+ pytorch base image). Callers decide how to handle these usually
318
+ by uploading them as the initial baseline rather than failing.
318
319
  """
319
320
  excludes = " ".join(f"-not -name '{n}'" for n in _WS_MARKER_NAMES)
320
321
  cmd = (
321
- f"find /workspace -mindepth 1 -maxdepth 1 {excludes} 2>/dev/null "
322
- "| head -5"
322
+ f"find /workspace -mindepth 1 -maxdepth 1 {excludes} 2>/dev/null"
323
323
  )
324
324
  _, out, _ = session.exec(cmd, stream=False)
325
- leftover = [line for line in out.splitlines() if line.strip()]
325
+ return [line for line in out.splitlines() if line.strip()]
326
+
327
+
328
+ def _ensure_workspace_empty_on_pod(session: RemoteSession) -> None:
329
+ """Raise if /workspace/ on the pod contains any non-marker files.
330
+
331
+ Retained for callers that want the strict behavior. Most call sites
332
+ should prefer ``_workspace_leftover_files`` and seed-by-pushing.
333
+ """
334
+ leftover = _workspace_leftover_files(session)
326
335
  if leftover:
327
336
  sample = ", ".join(p.rsplit("/", 1)[-1] for p in leftover[:3])
328
337
  more = "" if len(leftover) <= 3 else f" (+ more)"
@@ -381,14 +390,49 @@ def bootstrap_workspace_on_pod(
381
390
  if storage_ok:
382
391
  try:
383
392
  if is_new:
384
- _ensure_workspace_empty_on_pod(session)
385
- _con.print(" [dim]New workspace skipping pull[/dim]")
386
- session.exec(
387
- f": > {WATCH_LOG} 2>/dev/null; touch {PUSH_STAMP}",
388
- stream=False,
389
- )
390
- if start_watcher(session, "/workspace"):
391
- _con.print(" [dim]Watcher started for change tracking[/dim]")
393
+ # Docker images commonly seed /workspace with content
394
+ # (e.g. pytorch base image). Rather than fail bootstrap
395
+ # and leave the pod without autosync — the historical
396
+ # failure mode that caused silent data loss — seed the
397
+ # new workspace by uploading whatever is already on the
398
+ # pod as the initial baseline.
399
+ leftover = _workspace_leftover_files(session)
400
+ if leftover:
401
+ sample = ", ".join(
402
+ p.rsplit("/", 1)[-1] for p in leftover[:3]
403
+ )
404
+ more = (
405
+ "" if len(leftover) <= 3
406
+ else f" (+ {len(leftover) - 3} more)"
407
+ )
408
+ _con.print(
409
+ f" [yellow]/workspace contains existing files "
410
+ f"(e.g. {sample}{more}) — uploading them as the "
411
+ f"new workspace baseline before starting "
412
+ f"auto-sync.[/yellow]"
413
+ )
414
+ from swm.sync.push import workspace_push
415
+ rc = workspace_push(
416
+ session, storage_slug, bucket, workspace,
417
+ extra_excludes=extra_excludes, force=True,
418
+ )
419
+ if rc != 0:
420
+ raise RuntimeError(
421
+ f"Initial baseline push failed (s5cmd exit "
422
+ f"{rc}). Re-run `swm sync push "
423
+ f"{qualified_id} --force` then "
424
+ f"`swm sync auto {qualified_id}`."
425
+ )
426
+ else:
427
+ _con.print(" [dim]New workspace — skipping pull[/dim]")
428
+ session.exec(
429
+ f": > {WATCH_LOG} 2>/dev/null; touch {PUSH_STAMP}",
430
+ stream=False,
431
+ )
432
+ if start_watcher(session, "/workspace"):
433
+ _con.print(
434
+ " [dim]Watcher started for change tracking[/dim]"
435
+ )
392
436
  else:
393
437
  workspace_pull(
394
438
  session, storage_slug, bucket, workspace,
@@ -32,7 +32,11 @@ Workflow:
32
32
  @click.group(epilog=_WORKFLOW_EPILOG)
33
33
  @click.version_option(__version__, prog_name="swm")
34
34
  def main():
35
- """swm — Cloud GPU workflow manager for ComfyUI / SwarmUI."""
35
+ """swm — Cloud GPU workflow manager across 10 providers.
36
+
37
+ Search GPUs, provision pods, install frameworks (ComfyUI, vLLM, Ollama, …),
38
+ sync workspaces, manage models, and automate lifecycle with guard policies.
39
+ """
36
40
 
37
41
 
38
42
  # ── GPU search (top-level, kept here for brevity) ──────────────────
@@ -103,7 +107,10 @@ def gpus(gpu: str | None, gpu_count: int | None, max_price: float | None,
103
107
  try:
104
108
  sources = [get_provider(provider)]
105
109
  except ValueError:
106
- sources = []
110
+ known = ", ".join(sorted(_SLUG_TO_NAME))
111
+ raise click.UsageError(
112
+ f"Unknown provider {provider!r}. Known slugs: {known}"
113
+ ) from None
107
114
  else:
108
115
  configured = get_configured_providers()
109
116
  unconfigured_slugs = (
@@ -63,16 +63,29 @@ def resolve_active_pod(instance_id: str | None) -> str:
63
63
 
64
64
 
65
65
  def _iter_configured_pod_ids() -> list[str]:
66
- """Return pod ids known to swm's config (`pods.<provider:id>`)."""
66
+ """Return qualified pod ids known to swm's config (`provider:id`)."""
67
67
  pods = cfg.get("pods") or {}
68
68
  if not isinstance(pods, dict):
69
69
  return []
70
- return sorted(pods.keys())
70
+ out: list[str] = []
71
+ for pid, meta in pods.items():
72
+ prov = (meta or {}).get("provider", "")
73
+ out.append(f"{prov}:{pid}" if prov else pid)
74
+ return sorted(out)
71
75
 
72
76
 
73
77
  def complete_pod_id(ctx, param, incomplete: str):
74
78
  """Click shell completion callback for pod id arguments."""
75
- return [pid for pid in _iter_configured_pod_ids() if pid.startswith(incomplete)]
79
+ pods = cfg.get("pods") or {}
80
+ if not isinstance(pods, dict):
81
+ return []
82
+ candidates: set[str] = set()
83
+ for pid, meta in pods.items():
84
+ prov = (meta or {}).get("provider", "")
85
+ qid = f"{prov}:{pid}" if prov else pid
86
+ candidates.add(qid)
87
+ candidates.add(pid)
88
+ return sorted(c for c in candidates if c.startswith(incomplete))
76
89
 
77
90
 
78
91
  def pod_arg_callback(ctx, param, value):
@@ -94,7 +107,8 @@ def looks_like_pod_id(value: str) -> bool:
94
107
  return False
95
108
  if any(value.startswith(p) for p in _PROVIDER_PREFIXES):
96
109
  return True
97
- return value in _iter_configured_pod_ids()
110
+ pods = cfg.get("pods") or {}
111
+ return isinstance(pods, dict) and value in pods
98
112
 
99
113
 
100
114
  def split_pod_and_command(