swm-gpu 0.2.4__tar.gz → 0.2.6__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 (110) hide show
  1. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/.agents/skills/swm-gpu-workflow/SKILL.md +116 -24
  2. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/CHANGELOG.md +39 -0
  3. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/PKG-INFO +1 -1
  4. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/pyproject.toml +1 -1
  5. swm_gpu-0.2.6/src/swm/__init__.py +1 -0
  6. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/_helpers.py +21 -9
  7. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/guard.py +15 -4
  8. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/paths.py +8 -0
  9. swm_gpu-0.2.4/src/swm/__init__.py +0 -1
  10. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/.agents/skills/swm-gpu-workflow/examples.md +0 -0
  11. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/.gitignore +0 -0
  12. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/CODE_OF_CONDUCT.md +0 -0
  13. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/CONTRIBUTING.md +0 -0
  14. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/LICENSE +0 -0
  15. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/NOTICE +0 -0
  16. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/README.md +0 -0
  17. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/SECURITY.md +0 -0
  18. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/bootstrap.py +0 -0
  19. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/bootstrap_frameworks.py +0 -0
  20. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/bootstrap_ssh.py +0 -0
  21. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/cli.py +0 -0
  22. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/__init__.py +0 -0
  23. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/config.py +0 -0
  24. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/costs.py +0 -0
  25. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/guard.py +0 -0
  26. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/images.py +0 -0
  27. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/models.py +0 -0
  28. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/pod.py +0 -0
  29. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/pricing.py +0 -0
  30. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/remote.py +0 -0
  31. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/setup.py +0 -0
  32. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/storage.py +0 -0
  33. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/sync.py +0 -0
  34. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/use.py +0 -0
  35. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/config.py +0 -0
  36. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/__init__.py +0 -0
  37. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/billing.py +0 -0
  38. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/budget.py +0 -0
  39. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/db.py +0 -0
  40. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/reconcile.py +0 -0
  41. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/tracker.py +0 -0
  42. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/cuda.py +0 -0
  43. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/__init__.py +0 -0
  44. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/axolotl.py +0 -0
  45. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/comfyui.py +0 -0
  46. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/llm_studio.py +0 -0
  47. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/ollama.py +0 -0
  48. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/open_webui.py +0 -0
  49. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/swarmui.py +0 -0
  50. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/vllm_server.py +0 -0
  51. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/images.py +0 -0
  52. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/models/__init__.py +0 -0
  53. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/models/civitai.py +0 -0
  54. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/models/huggingface.py +0 -0
  55. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/models/manifest.py +0 -0
  56. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/models/resolver.py +0 -0
  57. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/pricing/__init__.py +0 -0
  58. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/pricing/calculator.py +0 -0
  59. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/pricing/providers.py +0 -0
  60. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/__init__.py +0 -0
  61. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/aws.py +0 -0
  62. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/azure.py +0 -0
  63. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/base.py +0 -0
  64. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/coreweave.py +0 -0
  65. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/fluidstack.py +0 -0
  66. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/gcp.py +0 -0
  67. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/lambda_labs.py +0 -0
  68. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/runpod.py +0 -0
  69. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/tensordock.py +0 -0
  70. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/vastai.py +0 -0
  71. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/vultr.py +0 -0
  72. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/remote/__init__.py +0 -0
  73. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/remote/ssh.py +0 -0
  74. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/storage/__init__.py +0 -0
  75. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/storage/b2.py +0 -0
  76. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/storage/base.py +0 -0
  77. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/storage/gcs.py +0 -0
  78. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/storage/s3.py +0 -0
  79. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/__init__.py +0 -0
  80. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/_autosync_daemon.sh +0 -0
  81. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/_common.py +0 -0
  82. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/autosync.py +0 -0
  83. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/preflight.py +0 -0
  84. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/pull.py +0 -0
  85. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/push.py +0 -0
  86. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/watcher.py +0 -0
  87. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/swm_main.py +0 -0
  88. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/.gitignore +0 -0
  89. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/README.md +0 -0
  90. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/download-music.sh +0 -0
  91. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/package-lock.json +0 -0
  92. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/package.json +0 -0
  93. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/remotion.config.ts +0 -0
  94. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/Root.tsx +0 -0
  95. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/ChapterCard.tsx +0 -0
  96. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/EndCard.tsx +0 -0
  97. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/GpuTable.tsx +0 -0
  98. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/OutputBlock.tsx +0 -0
  99. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/PHBadge.tsx +0 -0
  100. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/ProviderCascade.tsx +0 -0
  101. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/TerminalFrame.tsx +0 -0
  102. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/TypingLine.tsx +0 -0
  103. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/Wordmark.tsx +0 -0
  104. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/compositions/Hero16x9.tsx +0 -0
  105. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/compositions/Square1x1.tsx +0 -0
  106. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/compositions/Vertical9x16.tsx +0 -0
  107. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/config/music.ts +0 -0
  108. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/script/scrubbed-logs.ts +0 -0
  109. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/styles/tokens.ts +0 -0
  110. {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/tsconfig.json +0 -0
@@ -11,7 +11,7 @@ license: Apache-2.0
11
11
  compatibility: "macOS or Linux, Python 3.11+, SSH client"
12
12
  metadata:
13
13
  author: swm-gpu
14
- version: "0.2.3"
14
+ version: "0.2.4"
15
15
  docs-url: https://swmgpu.com
16
16
  repository: https://github.com/swm-gpu/swm
17
17
  ---
@@ -38,10 +38,11 @@ One CLI to search, provision, install, sync, and tear down GPU pods across 10 cl
38
38
  ## Phase 0 — State check
39
39
 
40
40
  ```bash
41
- swm pod list && swm use --show
41
+ swm pod list && swm use --show # running pods + active-pod selection
42
+ swm storage ls # existing workspaces in the default bucket
42
43
  ```
43
44
 
44
- If a matching pod is `RUNNING`, set it active (`swm use <name>`) and skip to Phase 5. Otherwise continue.
45
+ If a matching pod is `RUNNING`, set it active (`swm use <name>`) and skip to Phase 5. If a workspace prefix you want already exists in `swm storage ls`, restore it with `pod create -w <name>`. Otherwise continue.
45
46
 
46
47
  ## Phase 1 — Clarify
47
48
 
@@ -144,10 +145,11 @@ swm models pull <pod> ollama:llama3 # Ollama
144
145
  swm models pull <pod> Comfy-Org/Wan_2.2_ComfyUI_Repackaged \
145
146
  --as lora --filename split_files/loras/wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors
146
147
 
147
- swm models info <ref> # metadata before pulling
148
+ swm models search <query> [--sort downloads] [-n 20] # find models on HuggingFace Hub
149
+ swm models info <ref> # metadata before pulling (HF or Civitai)
148
150
  swm models list <pod> [--all] # tracked (+ untracked) files
149
151
  swm models link <pod> /abs/path/to/file --as vae
150
- swm models remove <pod> <name>
152
+ swm models remove <pod> <name> [-y]
151
153
  ```
152
154
 
153
155
  API keys (set once; legacy `hf_token` also read):
@@ -217,41 +219,75 @@ Unknown provider slugs raise `UsageError` listing valid ones.
217
219
  swm pod create -p runpod -g h200 -n s1 -y
218
220
  swm pod create -p vastai -g h200 -n train --gpu-count 4 --volume 200 --cloud-type SECURE -y
219
221
  swm pod list # auto-prunes stale entries vs live provider state
220
- swm pod status / stop / start / down / prune <pod>
222
+ swm pod status <pod> # GPU, cost, uptime + live sync health (autosync/watcher/last-push)
223
+ swm pod stop <pod> # halt, preserve volume (resume with pod start)
224
+ swm pod start <pod> # resume a stopped pod, wait for SSH
225
+ swm pod prune # drop config entries for pods gone from the provider
221
226
  ```
222
227
 
228
+ **Teardown — two distinct commands:**
229
+
230
+ ```bash
231
+ swm pod down <pod> # SAFE: push /workspace → storage, then terminate
232
+ swm pod down <pod> --no-sync # skip the push (workspace already synced)
233
+ swm pod down <pod> --force-down # terminate even if push left no recent stamp (DANGEROUS)
234
+ swm pod terminate <pod> -y # DESTRUCTIVE: delete pod + volume, NO sync (irreversible)
235
+ ```
236
+
237
+ `pod down` is the everyday teardown — it refuses to wipe `/workspace` or terminate unless the push wrote a fresh push stamp (guards against silent data loss). `pod terminate` skips all sync and deletes the volume; only use it for throwaway / `--no-storage` pods.
238
+
223
239
  ## Sync
224
240
 
225
241
  ```bash
226
- swm sync pull [pod] # storage → pod
242
+ swm sync pull [pod] # storage → pod (non-destructive: skips existing, never deletes)
227
243
  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
244
+ swm sync push [pod] # pod → storage (incremental)
245
+ swm sync push [pod] --force # kill any running transfer, re-baseline from scratch
246
+ swm sync push [pod] --delete # also delete remote files removed locally (watcher must be alive)
247
+ swm sync push [pod] --tar # pack workspace to one tarball object (fast for many small files)
248
+ swm sync status [pod] # s5cmd, watcher, autosync daemon, last-push stamp, pending changes
231
249
  ```
232
250
 
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.
251
+ **Watcher vs daemon two layers:**
252
+
253
+ ```bash
254
+ swm sync watch [pod] # start the inotify file-change watcher only (records what changed)
255
+ swm sync watch [pod] --stop # stop the watcher
256
+ swm sync auto [pod] # start the background daemon: reads watcher log every 60s, pushes
257
+ swm sync auto [pod] -i 30 # custom interval (seconds)
258
+ swm sync auto [pod] --status # daemon state + recent log tail
259
+ swm sync auto [pod] --stop # stop the daemon
260
+ swm sync auto [pod] --force # start without a prior successful pull/push (DANGEROUS — see below)
261
+ ```
262
+
263
+ `swm pod create -w <name>` wires up the watcher + auto daemon for you; reach for `sync watch` / `sync auto` only to inspect or recover. **`sync auto --force` is dangerous**: it bypasses the safety check that requires a known-good baseline, so local deletions can propagate to storage. Only force when you've just `sync push --force`ed and know the pod is authoritative.
264
+
265
+ Three-tier sync: inotify watcher → incremental s5cmd → tar. Push reconciles inotify events with a `find -newer` scan so `pip install` bursts aren't missed (and falls back to a full `find` count when the watcher is dead). Pull re-runs framework link-repair steps so `/workspace/models/` symlinks stay wired after restore.
234
266
 
235
267
  ## Guard
236
268
 
237
269
  ```bash
238
- swm guard set <pod> --mode auto-down --idle-timeout 30 # set/update policy
270
+ swm guard set <pod> --mode auto-down --idle-timeout 30 # set/update policy (auto-down/auto-stop/remind/manual)
239
271
  swm guard set <pod> --mode remind # notify-only
240
272
  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
273
+ swm guard defaults --mode auto-down --idle-timeout 30 # default policy applied to new pods
274
+ swm guard list # all guarded pods + live idle status
275
+ swm guard run [pods…] [--once] [--interval N] # run the loop in foreground (debug/cron)
276
+ swm guard stop-daemon # stop the background daemon
244
277
  ```
245
278
 
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).
279
+ Monitors 4 signals: SSH sessions (`who`), GPU utilization, filesystem writes, active processes. Setting `auto-stop`/`auto-down` auto-starts the local daemon; `guard run` is the explicit foreground runner (use `--once` for a single sweep). `--idle-timeout 0` and `--poll-interval 0` are honored as explicit values (not unset). `auto-down` runs the same push-stamp safety check as `pod down` — it leaves the pod alive rather than terminate on a partial push.
247
280
 
248
281
  ## Costs
249
282
 
250
283
  ```bash
251
- swm costs live # right-now burn
284
+ swm costs live # right-now burn across active pods
252
285
  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
286
+ swm costs log [-n 50] [-p vastai] # detailed per-session history
287
+ swm costs reconcile [-p vastai] # compare local tracking vs provider card charges
288
+ swm costs budget set 100 [-s global] [-t month] # set a spending alert
289
+ swm costs budget show # active budgets + current spend
290
+ swm costs budget remove <scope> [-t month] # remove a budget
255
291
  ```
256
292
 
257
293
  ## Remote
@@ -266,8 +302,9 @@ swm download [pod] remote/file -d ~/Downloads
266
302
  ## Pricing reference
267
303
 
268
304
  ```bash
269
- swm pricing compare --gpu h100|h200|b200|a100|rtx-4090|l40s
270
- swm pricing estimate -g h200 --hours 24
305
+ swm pricing compare --gpu h100|h200|b200|a100|rtx-4090|l40s # per-GPU/hr across providers
306
+ swm pricing estimate -g h200 --hours 24 [--storage GB] [--tier secure] # projected workload cost
307
+ swm pricing specs --gpu h200 # hardware specs (VRAM, bandwidth) side-by-side
271
308
  ```
272
309
 
273
310
  ## Instance ID format
@@ -289,18 +326,70 @@ swm pricing estimate -g h200 --hours 24
289
326
  | `azure` | az CLI (`pip install swm-gpu[azure]`) | yes |
290
327
  | `coreweave` | Kubernetes | yes |
291
328
 
292
- ## Storage
329
+ ## Storage (buckets & workspaces)
330
+
331
+ Inspect and manage cloud-storage buckets directly — **no running pod required**. Top-level prefixes in your default bucket are your workspace names, so `swm storage ls` is the fastest "what's in my bucket?" probe.
332
+
333
+ ```bash
334
+ swm storage list # configured buckets across all providers
335
+ swm storage ls # default bucket root = workspace names
336
+ swm storage ls studio-live/ # drill into one workspace (sizes/mtimes show here)
337
+ swm storage ls -b b2:other-bucket # explicit provider:bucket
338
+ swm storage create my-models -p b2 # create a bucket (-l location, -c storage-class)
339
+ swm storage upload ./file remote/path # one-off upload
340
+ swm storage download remote/path ./file # one-off download
341
+ swm storage rm workspace2/ --dry-run # COUNT objects under a prefix, delete nothing
342
+ swm storage rm workspace2/ -y # delete all objects under prefix (S3 batch delete)
343
+ ```
344
+
345
+ `storage rm` uses the S3 batch-delete API (1000 objects/request) and prompts before deleting unless `-y`. It's a hard delete on S3-compatible backends (B2/S3) — always `--dry-run` first. Top-level `ls` shows `DIR` with no size/mtime (B2 `CommonPrefixes` carry neither); drill into a prefix for real sizes.
293
346
 
294
- S3-compatible via s5cmd. Config keys:
347
+ Backend config keys (S3-compatible via s5cmd):
295
348
 
296
349
  | Backend | Keys |
297
350
  |---|---|
298
- | Backblaze B2 | `b2.key_id`, `b2.app_key`, `b2.bucket` |
351
+ | Backblaze B2 | `b2.key_id`, `b2.app_key`, `b2.bucket`, `b2.s3_endpoint` |
299
352
  | Amazon S3 | `s3.access_key`, `s3.secret_key`, `s3.bucket` |
300
353
  | Google GCS | `gcs.hmac_access`, `gcs.hmac_secret`, `gcp.bucket` |
301
354
 
302
355
  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
356
 
357
+ ## Frameworks & workspace attach (setup)
358
+
359
+ ```bash
360
+ swm setup list # available frameworks
361
+ swm setup install <name> [pod] # idempotent install into its own venv
362
+ swm setup start <name> [pod] [-p port] [--model M] [--extra-args "…"]
363
+ swm setup stop <name> [pod]
364
+ swm setup storage [pod] [-p b2|gcs|s3|all] # install s5cmd + configure creds on an existing pod
365
+ swm setup workspace [pod] [-n name] [-b provider:bucket] [--force]
366
+ ```
367
+
368
+ **`swm setup workspace` is the recovery command** for the silent-data-loss scenario: it does the full workspace bootstrap in one shot (installs s5cmd, configures creds, pulls or initializes the workspace, starts the auto-sync daemon) and persists the pod ↔ workspace ↔ bucket association. Use it when:
369
+ - you created a pod with `--no-storage` and now want persistence,
370
+ - `pod create`'s SSH probe timed out so bootstrap was skipped,
371
+ - you want to reattach a pod to a different workspace (`--force`).
372
+
373
+ ## Config
374
+
375
+ ```bash
376
+ swm config set <key> <value> # e.g. swm config set runpod.api_key sk-xxx
377
+ swm config get <key> # single value (secrets suffix-masked)
378
+ swm config list # all values (secrets masked)
379
+ swm config delete <key> # remove a key
380
+ swm config path # location of the config file
381
+ ```
382
+
383
+ Common keys: provider `<slug>.api_key`; storage `b2.*` / `s3.*` / `gcs.*` + `storage.default`; model tokens `hf.api_key`, `civitai.api_key`; guard `guard.*` defaults.
384
+
385
+ ## Images
386
+
387
+ ```bash
388
+ swm images list [-p provider] [--cuda 12.8] [--refresh] [-n 30] [--all]
389
+ ```
390
+
391
+ Lists Docker images discoverable for a provider (newest-first) for use with `swm pod create --image`. `--refresh` re-queries the provider instead of using cached results.
392
+
304
393
  ---
305
394
 
306
395
  ## Anti-patterns
@@ -314,4 +403,7 @@ Default: `swm config set storage.default b2:<bucket>`. Credentials are masked by
314
403
  - **Never tear down before `du -sh /workspace`** — 10–30 GB bloat extends the next pull.
315
404
  - **Never guess nested HF file paths** — run `swm models info <repo>` and copy the exact `rfilename`.
316
405
  - **Never skip Phase 0** — a forgotten running pod bills silently.
406
+ - **Never confuse `pod down` with `pod terminate`** — `down` syncs `/workspace` first; `terminate` deletes the volume with no sync. Use `terminate` only on `--no-storage` / throwaway pods.
407
+ - **Never trust a green `sync push` alone** — confirm `swm pod status` shows a recent last-push stamp and autosync running before tearing down. A push can exit 0 yet skip the stamp on a partial transfer.
408
+ - **Never use `swm storage rm` without `--dry-run` first** — it's a hard S3 batch delete with no undo.
317
409
  - **Never hand off** before Phase 5 passes.
@@ -6,6 +6,45 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.6] - 2026-06-16
10
+
11
+ ### Fixed
12
+ - **Auto-down never fired on workspace-backed pods (cost bug).** The lifecycle
13
+ guard's filesystem-activity signal keyed off the mtime of the autosync watcher
14
+ log (`/workspace/.swm_changes.log`), which autosync rewrites every cycle — so
15
+ pods were reported perpetually "active," the idle timer never elapsed, and
16
+ `auto-down`/`auto-stop` never triggered. The guard now detects *real*
17
+ workspace writes via `find`, excluding swm bookkeeping (`.swm_*`,
18
+ `.swm_guard`), build caches (`.uv-cache`, `.cache`, `__pycache__`), logs,
19
+ `terminfo`, `.git`, and `.nv`.
20
+ - `swm setup start` no longer crashes with a traceback when the post-start
21
+ health probe hits a connection reset. `_probe_url` caught `ConnectError`/
22
+ `TimeoutException` but not `httpx.ReadError` ("connection reset by peer" while
23
+ a freshly-opened SSH tunnel settles); it now catches all `httpx.HTTPError`.
24
+
25
+ ### Changed
26
+ - Workspace sync now excludes `/.uv-cache/` (uv build/wheel cache) and
27
+ `/terminfo/` (managed-CPython terminal database). The former churned
28
+ constantly and caused delete-reconciliation failures and ~11 GB of wasted
29
+ storage; the latter's dedup-hardlinked files broke hardlink staging.
30
+
31
+ ### Docs
32
+ - Rewrote the `swm-gpu-workflow` skill for full command coverage — added the
33
+ entire `swm storage` group, `swm config`, `swm images`, `swm sync auto`,
34
+ `swm setup workspace`/`storage`, `pod terminate` vs `pod down`, and more.
35
+
36
+ ## [0.2.5] - 2026-05-29
37
+
38
+ ### Fixed
39
+ - Framework SSH tunnels (e.g. `swm setup start comfyui`) now use the
40
+ configured SSH identity. `_open_tunnel` built a bare `ssh` command without
41
+ `-i <key_path>`, so when a custom `ssh.key_path` (or `<provider>.ssh_key`)
42
+ was set it silently fell back to the default key, failed authentication, and
43
+ — because of `ExitOnForwardFailure=yes` with output discarded — exited
44
+ immediately without forwarding the port. The tunnel now resolves its
45
+ host/port/user/key via the same path as `swm run`/`swm ssh`, so it also
46
+ targets the direct IP and mapped port-22 endpoint when available.
47
+
9
48
  ## [0.2.4] - 2026-05-27
10
49
 
11
50
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swm-gpu
3
- Version: 0.2.4
3
+ Version: 0.2.6
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "swm-gpu"
3
- version = "0.2.4"
3
+ version = "0.2.6"
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"
@@ -0,0 +1 @@
1
+ __version__ = "0.2.6"
@@ -180,29 +180,36 @@ def _open_tunnel(inst, ports: dict[int, str]) -> list[int] | None:
180
180
  """
181
181
  import subprocess
182
182
 
183
+ from swm.remote.ssh import _ssh_config_for
184
+
183
185
  if not inst.ssh_host:
184
186
  return None
185
187
  unmapped = [p for p in ports if p not in (inst.ports or {})]
186
188
  if not unmapped:
187
189
  return None
188
190
 
189
- ssh_port = inst.ssh_port or 22
190
- user = inst.ssh_user or "root"
191
+ # Resolve host/port/user/key the same way `swm run`/`swm ssh` do, so the
192
+ # tunnel uses the configured identity (e.g. ssh.key_path) and the direct
193
+ # IP + mapped port-22 when available. Building a bare `ssh` command here
194
+ # silently fell back to default keys and could miss a custom key.
195
+ c = _ssh_config_for(inst)
191
196
 
192
- cmd = [
193
- "ssh", "-N",
197
+ cmd = ["ssh", "-N"]
198
+ if c["key_path"]:
199
+ cmd.extend(["-i", str(c["key_path"])])
200
+ cmd.extend([
194
201
  "-o", "StrictHostKeyChecking=no",
195
202
  "-o", "UserKnownHostsFile=/dev/null",
196
203
  "-o", "LogLevel=ERROR",
197
204
  "-o", "ExitOnForwardFailure=yes",
198
205
  "-o", "ServerAliveInterval=30",
199
206
  "-o", "ServerAliveCountMax=3",
200
- ]
207
+ ])
201
208
  for p in unmapped:
202
209
  cmd.extend(["-L", f"{p}:localhost:{p}"])
203
- if ssh_port != 22:
204
- cmd.extend(["-p", str(ssh_port)])
205
- cmd.append(f"{user}@{inst.ssh_host}")
210
+ if c["port"] != 22:
211
+ cmd.extend(["-p", str(c["port"])])
212
+ cmd.append(f"{c['user']}@{c['host']}")
206
213
 
207
214
  subprocess.Popen(
208
215
  cmd,
@@ -226,7 +233,12 @@ def _probe_url(url: str, timeout: int = 60) -> bool:
226
233
  r = httpx.get(url, timeout=5, follow_redirects=True)
227
234
  if r.status_code < 500:
228
235
  return True
229
- except (httpx.ConnectError, httpx.TimeoutException, httpx.RemoteProtocolError, OSError):
236
+ except (httpx.HTTPError, OSError):
237
+ # httpx.HTTPError covers all transport-level failures —
238
+ # ConnectError, ReadError ("connection reset by peer" while a
239
+ # freshly-opened SSH tunnel settles), WriteError, TimeoutException,
240
+ # RemoteProtocolError, etc. A probe must never crash the command;
241
+ # an unreachable URL just means "not ready yet, retry".
230
242
  pass
231
243
  remaining = deadline - time.monotonic()
232
244
  time.sleep(min(interval, max(remaining, 0)))
@@ -81,10 +81,21 @@ def avg_gpu_util() -> float:
81
81
 
82
82
 
83
83
  def recent_fs_write(now: float) -> bool:
84
- try:
85
- return os.path.getmtime(WATCH_LOG) >= now - max(POLL * 2, 120)
86
- except OSError:
87
- return False
84
+ # Detect REAL workspace writes (user data, generation output, installs),
85
+ # excluding swm's own bookkeeping (autosync change-log/stamps/guard), build
86
+ # caches, logs, and framework noise. Keying off WATCH_LOG mtime was a bug:
87
+ # the autosync watcher rewrites that file every cycle, so the pod looked
88
+ # perpetually active and auto-down could never fire.
89
+ minutes = max(2, int(max(POLL * 2, 120) / 60))
90
+ out = sh(
91
+ "find /workspace -mindepth 1 -mmin -%d -type f "
92
+ "-not -path '*/.swm_*' -not -path '*/.swm_guard/*' "
93
+ "-not -path '*/.uv-cache/*' -not -path '*/.cache/*' "
94
+ "-not -path '*/__pycache__/*' -not -name '*.log' "
95
+ "-not -path '*/terminfo/*' -not -path '*/.git/*' "
96
+ "-not -path '*/.nv/*' 2>/dev/null | head -1" % minutes
97
+ )
98
+ return bool(out.strip())
88
99
 
89
100
 
90
101
  def transfer_locked() -> bool:
@@ -43,4 +43,12 @@ WATCH_EXCLUDES: tuple[str, ...] = (
43
43
  r"\.log$",
44
44
  r"/\.cache/",
45
45
  r"/\.nv/",
46
+ # uv's build/wheel cache (workspace-owned uv from v0.2.x). Regenerable,
47
+ # and its ephemeral builds-v0/.tmp* dirs churn constantly — syncing it
48
+ # both wastes storage and causes delete-reconciliation failures.
49
+ r"/\.uv-cache/",
50
+ # Managed-CPython terminfo database (workspace-owned python). Its entries
51
+ # are dedup-hardlinked, which breaks hardlink-staging (s5cmd then can't
52
+ # open them → push fails). Non-essential for any venv/ML workload.
53
+ r"/terminfo/",
46
54
  )
@@ -1 +0,0 @@
1
- __version__ = "0.2.4"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes