Skip to content

Commit 604bc9e

Browse files
authored
Fix/template chagne instance type (#38)
* fix: template wizard uses instance-types API, not availability Templates are saved configs — filtering by real-time availability doesn't make sense. Instance type and location steps now use the instance-types and locations APIs in template mode, showing all options. Location is optional in templates (skip = decide at deploy time). Deploy mode is unchanged. Also improves UX when deploying from a location-less template: the wizard now prompts for location instead of silently defaulting to FIN-01, and shows a clear error when no locations are available.
1 parent fef8653 commit 604bc9e

13 files changed

Lines changed: 198 additions & 75 deletions

File tree

AGENTS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ Read `CLAUDE.md` first. This file defines how you execute, not what the project
77
Do NOT write code until you have completed all steps below. No exceptions.
88

99
**Step 1 — Read** (always, every task):
10-
1. `CLAUDE.md` (root) — architecture, conventions, pricing rules
11-
2. `CLAUDE.md` in the target command directory (e.g. `cmd/vm/CLAUDE.md`) — domain gotchas
12-
3. `README.md` in the target command directory — usage, flags, examples
10+
1. `CLAUDE.md` (root) — architecture, conventions, pricing rules, per-command doc index
11+
2. `cmd/<domain>/CLAUDE.md` in the target command directory — domain knowledge, gotchas, edge cases
12+
3. `cmd/<domain>/README.md` in the target command directory — usage examples, flags, architecture
1313
4. `.ai/skills/new-command.md` if adding or modifying a command
1414

15+
Per-command docs exist in: `auth`, `vm`, `template`, `volume`, `sshkey`, `startupscript`, `update`, `settings`. For commands without docs, read the source files directly.
16+
1517
**Step 2 — Verify** (always):
1618
5. Run `make test` to confirm the repo is green before you start
1719

CLAUDE.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,38 @@ cmd/verda/ # Entrypoint
2020
internal/verda-cli/
2121
cmd/cmd.go # Root command, command groups
2222
cmd/util/ # Factory, IOStreams, helpers, pricing, hostname
23-
cmd/<domain>/ # One dir per domain (vm, volume, auth, skills, ...)
23+
cmd/<domain>/ # One dir per domain (see per-command docs below)
24+
CLAUDE.md # Domain knowledge, gotchas, edge cases
25+
README.md # Usage examples, flags, architecture notes
2426
options/ # Global CLI options, credentials
2527
internal/skills/ # Embedded AI skill files (go:embed)
2628
```
2729

30+
### Per-Command Documentation
31+
32+
Each command directory has its own `CLAUDE.md` (domain knowledge) and `README.md` (usage/architecture). These are the source of truth for command-specific behavior.
33+
34+
| Directory | Docs | Description |
35+
|-----------|------|-------------|
36+
| `cmd/vm/` | CLAUDE.md, README.md | VM create/list/describe/action, wizard, templates |
37+
| `cmd/template/` | CLAUDE.md, README.md | Template create/edit/list/show/delete |
38+
| `cmd/auth/` | CLAUDE.md, README.md | Login, logout, show credentials |
39+
| `cmd/volume/` | CLAUDE.md, README.md | Volume lifecycle, trash, actions |
40+
| `cmd/sshkey/` | CLAUDE.md, README.md | SSH key management |
41+
| `cmd/startupscript/` | CLAUDE.md, README.md | Startup script management |
42+
| `cmd/update/` | CLAUDE.md, README.md | CLI self-update |
43+
| `cmd/settings/` | CLAUDE.md, README.md | CLI settings management |
44+
| `cmd/availability/` || Instance availability by location |
45+
| `cmd/cost/` || Balance, running costs, estimates |
46+
| `cmd/images/` || OS image listing |
47+
| `cmd/instancetypes/` || Instance type catalog |
48+
| `cmd/locations/` || Datacenter locations |
49+
| `cmd/status/` || Status dashboard |
50+
| `cmd/ssh/` || SSH into instances |
51+
| `cmd/mcp/` || MCP server |
52+
| `cmd/skills/` || AI skills management |
53+
| `cmd/completion/` || Shell completions |
54+
2855
### Core Patterns
2956

3057
- **Factory** (`cmd/util/factory.go`): DI for Prompter, Status, VerdaClient, Debug, AgentMode, OutputFormat
@@ -69,12 +96,12 @@ internal/skills/ # Embedded AI skill files (go:embed)
6996

7097
## Before Editing Any Command
7198

72-
1. Read the **nearest** `CLAUDE.md` in the command directory (e.g. `cmd/vm/CLAUDE.md`)
73-
2. Read the **nearest** `README.md` for usage examples and flag details
99+
1. Read the **nearest** `CLAUDE.md` in the command directory (e.g. `cmd/vm/CLAUDE.md`) — domain knowledge, gotchas, edge cases
100+
2. Read the **nearest** `README.md` in the command directory — usage examples, flags, architecture
74101
3. Read `.ai/skills/new-command.md` for the full checklist when adding/modifying commands
75102
4. If touching pricing, auth, or agent-mode: plan first, don't code immediately
76103

77-
Per-command docs are auto-maintained by a pre-commit hook.
104+
Per-command docs are auto-maintained by `/update-command-knowledge` skill.
78105
Manual update: `claude -p "/update-command-knowledge --all" --model sonnet --dangerously-skip-permissions`
79106

80107
## Thinking Depth

internal/skills/files/verda-reference.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,17 @@ Tell user to run in their terminal:
9696

9797
| Command | Notes |
9898
|---------|-------|
99-
| `verda template list -o json` | Lists saved templates |
100-
| `verda template show vm/<name> -o json` | Note: `vm/` prefix required |
99+
| `verda template list -o json` | Fields: `resource`, `name`, `description` |
100+
| `verda template show vm/<name> -o json` | Fields: `InstanceType`, `Location`, `Image`, `SSHKeys[]`, `HostnamePattern`, `Description`. Note: `vm/` prefix required |
101101
| `verda template delete vm/<name>` | Confirm first |
102102
| `verda template create` | Interactive — tell user to run |
103-
| `verda template edit <name>` | Interactive — tell user to run |
103+
| `verda template edit <name>` | Interactive field editor — tell user to run |
104104

105-
Deploy: `verda --agent vm create --from <name> --hostname <name> --wait --wait-timeout 2m -o json`
105+
Deploy from template (flags override template values):
106+
```bash
107+
verda --agent vm create --from <name> --hostname <name> --wait --wait-timeout 2m -o json
108+
verda --agent vm create --from <name> --location FIN-03 -o json # override location
109+
```
106110
Hostname patterns: `{random}` → random words, `{location}` → location code
107111

108112
## Volumes

internal/verda-cli/cmd/template/CLAUDE.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ All fields except `resource` are optional. Stored at `~/.verda/templates/<resour
2424
| `contract` | string | `"PAY_AS_YOU_GO"`, `"SPOT"`, `"LONG_TERM"` |
2525
| `kind` | string | `"GPU"` or `"CPU"` (lowercase in template, case-insensitive in matching) |
2626
| `instance_type` | string | e.g. `"1V100.6V"` |
27-
| `location` | string | e.g. `"FIN-01"` |
28-
| `image` | string | OS image slug or ID |
27+
| `location` | string | Optional. e.g. `"FIN-01"`. If omitted, wizard prompts at deploy time |
28+
| `image` | string | OS image **name** (resolved to ID at deploy time) |
2929
| `os_volume_size` | int | GiB |
3030
| `storage` | []StorageSpec | Each has `type` and `size` |
3131
| `storage_skip` | bool | Skip storage step in wizard |
@@ -79,8 +79,8 @@ The `template edit` command uses a field menu approach (not the full wizard):
7979
### Edit field editors
8080
- **Billing Type**: static select (on-demand / spot). Clears contract when switching to spot.
8181
- **Kind**: static select (gpu / cpu)
82-
- **Instance Type**: API call to `InstanceTypes.Get`, filtered by current kind, shows price
83-
- **Location**: API call to `Locations.Get`
82+
- **Instance Type**: API call to `InstanceTypes.Get` (not availability), filtered by current kind, shows price
83+
- **Location**: API call to `Locations.Get`, includes "None (decide at deploy time)" to clear location
8484
- **Image**: API call to `Images.Get`, excludes cluster images
8585
- **OS Volume Size**: text input with current value as default
8686
- **SSH Keys**: API call to `SSHKeys.GetAllSSHKeys`, multi-select with current keys pre-selected
@@ -108,6 +108,9 @@ Displays all template fields, including those previously hidden:
108108

109109
- **Import cycle**: `cmd/template/` cannot import `cmd/vm/` for the Template type (circular dependency). Shared types live in `internal/verda-cli/template/`, re-exported by `cmd/template/types.go` via type aliases and `var` bindings.
110110
- **`billingTypeSet` / `locationSet` flags**: Needed because `IsSet` in the wizard can't distinguish `"on-demand"` (falsy `IsSpot=false`) from "unset". When a template sets billing type or location, these booleans are set to `true` so the wizard skips those steps.
111+
- **Template without location triggers wizard**: When `--from` is used and the template has no location (`!opts.locationSet`), `resolveCreateInputs` triggers the wizard so the user is prompted for location instead of silently defaulting to FIN-01.
112+
- **Template instance type/location use different APIs**: Template wizard (create) uses instance-types API and locations API directly. Deploy wizard uses availability API to filter. Template edit also uses instance-types and locations APIs directly.
113+
- **Template error message**: `Resolve()` now shows `template name is required — template "X" not found` with guidance to run `verda template list` or use `--from` interactively.
111114
- **`NoOptDefVal` on `--from` flag**: Set to `" "` (space) so `--from` without a value is recognized as "flag changed but empty". When the user writes `verda vm create --from gpu-training`, cobra parses `gpu-training` as a positional arg; `RunE` recombines it into `opts.From`.
112115
- **Startup script "None (skip)" label**: The wizard presents "None (skip)" as a selectable option. Previously, this label text was captured as the script name. Fixed by checking `Value != ""` before storing the name.
113116
- **`ensurePricingCache`**: The confirm-deploy step calls this (with parent context, not `context.Background()`) to fetch pricing when the cache is empty from template pre-fill.

internal/verda-cli/cmd/template/README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ verda template create
2828
verda template create gpu-training
2929
```
3030

31-
The create command runs the VM wizard in **template mode** -- the same 10 configuration steps (billing type through startup script) but without hostname, description, or confirm-deploy. The resulting settings are saved to disk.
31+
The create command runs the VM wizard in **template mode** -- the same 10 configuration steps (billing type through startup script) but without hostname, description, or confirm-deploy. In template mode, instance types come from the instance-types API (not filtered by availability) and location is optional ("None — decide at deploy time"). The resulting settings are saved to disk.
3232

3333
### Edit
3434

@@ -40,7 +40,7 @@ verda template edit
4040
verda template edit vm/gpu-training
4141
```
4242

43-
Shows a menu of all template fields with their current values. Pick a field to change, edit it with the appropriate prompt (static choices for simple fields, API-backed selection for instance type/location/image/SSH keys/startup script). Repeat until "Save & exit".
43+
Shows a menu of all template fields with their current values. Pick a field to change, edit it with the appropriate prompt (static choices for simple fields, API-backed selection for instance type/location/image/SSH keys/startup script). Location includes a "None (decide at deploy time)" option to clear the value. Repeat until "Save & exit".
4444

4545
### List
4646

@@ -101,8 +101,8 @@ billing_type: on-demand # on-demand or spot
101101
contract: PAY_AS_YOU_GO
102102
kind: GPU # GPU or CPU
103103
instance_type: 1V100.6V
104-
location: FIN-01
105-
image: ubuntu-24.04-cuda-12.8
104+
location: FIN-01 # optional — omit to prompt at deploy time
105+
image: ubuntu-24.04-cuda-12.8 # stored by name, resolved to ID at deploy time
106106
os_volume_size: 200 # GiB
107107
storage:
108108
- type: NVMe
@@ -124,11 +124,17 @@ verda vm create --from gpu-training # load by name
124124
verda vm create --from ./my-template.yaml # load from file path
125125
verda vm create --from # pick from list (interactive)
126126
verda vm create --from gpu-training --hostname my-vm --description "test"
127+
128+
# Override template values with flags
129+
verda vm create --from gpu-training --location FIN-03
130+
verda vm create --from gpu-training --hostname my-vm --os-volume-size 200
127131
```
128132

133+
Flags passed alongside `--from` override the template values.
134+
129135
### Flow
130136

131-
1. Template values pre-fill the wizard's `createOptions`
137+
1. Template values pre-fill the wizard's `createOptions` (CLI flags take precedence — they are parsed first)
132138
2. A summary of template values is printed to stderr
133139
3. SSH keys and startup scripts are resolved by name to ID via the API; unresolved names produce warnings (no longer silently swallowed)
134140
4. Only unfilled steps are prompted (hostname, description, confirm-deploy are always prompted; other steps only if the template didn't fill them)

internal/verda-cli/cmd/template/edit.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,16 +301,20 @@ func editLocation(ctx context.Context, f cmdutil.Factory, t *Template) error {
301301
return err
302302
}
303303

304-
choices := make([]string, len(locations))
305-
for i, loc := range locations {
306-
choices[i] = loc.Code
304+
choices := []string{"None (decide at deploy time)"}
305+
for _, loc := range locations {
306+
choices = append(choices, loc.Code)
307307
}
308308

309309
idx, selErr := f.Prompter().Select(ctx, "Location", choices)
310310
if selErr != nil {
311311
return nil //nolint:nilerr // user canceled
312312
}
313-
t.Location = locations[idx].Code
313+
if idx == 0 {
314+
t.Location = ""
315+
} else {
316+
t.Location = locations[idx-1].Code
317+
}
314318
return nil
315319
}
316320

internal/verda-cli/cmd/vm/CLAUDE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- `vm.go` -- Parent command, registers subcommands and shortcuts
88
- `create.go` -- Create command, flags, `createOptions` struct, request building, validation
99
- `wizard.go` -- 13 wizard step definitions, `WizardMode`, `RunTemplateWizard`, step Defaults
10-
- `wizard_cache.go` -- `apiCache`, `ensurePricingCache`, pricing helpers, instance type utils
10+
- `wizard_cache.go` -- `apiCache`, `fetchLocations`, `loadAllLocations`/`loadAvailableLocations`, `ensurePricingCache`, pricing helpers, instance type utils
1111
- `wizard_subflows.go` -- SSH key, startup script, storage interactive sub-flows
1212
- `wizard_summary.go` -- `renderDeploymentSummary` (accepts `io.Writer`)
1313
- `template_apply.go` -- Template loading, applying, name resolution with warnings
@@ -83,7 +83,10 @@ startup-script -> hostname -> description -> confirm-deploy
8383

8484
- Steps with `DependsOn` re-run their Loader when dependencies change
8585
- `contract` step: `ShouldSkip` returns true for spot billing
86+
- `instance-type` step: accepts `WizardMode`. Deploy mode filters by real-time availability; template mode shows all instance types from the instance-types API (no availability filtering)
87+
- `location` step: accepts `WizardMode`. Deploy mode shows only locations where the instance type is available; template mode shows all locations with a "None (decide at deploy time)" skip option. Deploy mode returns a clear error when no locations are available for the instance type
8688
- `location` step: `IsSet` treats default `FIN-01` as unset (so wizard prompts)
89+
- `location` step: `Required` is dynamic — true in deploy mode, false in template mode
8790
- `storage`, `ssh-keys`, `startup-script` steps: manage values directly in Loader (Setter/Resetter are no-ops), include inline sub-flows for creating new resources via API
8891
- `confirm-deploy` step: renders deployment summary with full cost breakdown via `renderDeploymentSummary(w, opts, cache)`
8992
- Steps have `Default` functions that return current `opts` values for pre-selection (used when `--from` pre-fills values)
@@ -98,8 +101,9 @@ startup-script -> hostname -> description -> confirm-deploy
98101

99102
## Gotchas & Edge Cases
100103

101-
- **Wizard triggers when ANY of instance-type, os, or hostname is missing** -- not all three. Providing two of three still launches the wizard.
104+
- **Wizard triggers when ANY of instance-type, os, or hostname is missing** -- not all three. Providing two of three still launches the wizard. Also triggers when `--from` was used but the template had no location (`templateWithoutLocation` check in `resolveCreateInputs`).
102105
- **Location default quirk**: `LocationCode` defaults to `FIN-01` in createOptions, but the wizard's `IsSet` returns false for `FIN-01` specifically, so the wizard always prompts for location even when the default is in effect.
106+
- **Flags override template values**: When `--from` is used alongside other flags (e.g. `--hostname`, `--location`), flags are parsed first, then `applyTemplate` only overwrites empty fields. CLI flags take precedence.
103107
- **apiCache invalidation**: Cache is invalidated when `isSpot` changes (user switches billing type), because availability differs between spot and on-demand.
104108
- **Lazy client resolution**: `clientFunc` defers credential resolution until the first API-dependent wizard step fires. Early steps (billing-type, kind, text inputs) run without credentials.
105109
- **Hidden flag aliases**: `--type`, `--image`, `--ssh-key-id`, `--startup-script-id`, `--spot` are hidden aliases for their primary flags.

internal/verda-cli/cmd/vm/README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ verda vm create
2929
# From a saved template (only prompts for hostname + confirm)
3030
verda vm create --from gpu-training
3131

32+
# From a template, override specific fields with flags
33+
verda vm create --from gpu-training --location FIN-03
34+
verda vm create --from gpu-training --hostname my-vm --os-volume-size 200
35+
3236
# Pick template from list
3337
verda vm create --from
3438

@@ -97,8 +101,8 @@ The wizard launches automatically when any of `--instance-type`, `--os`, or `--h
97101
1. Billing type (On-Demand or Spot)
98102
2. Contract period (skipped for Spot)
99103
3. Compute type (GPU or CPU)
100-
4. Instance type (filtered by kind and availability)
101-
5. Datacenter location (filtered by instance type availability)
104+
4. Instance type (deploy mode: filtered by kind and availability; template mode: all types from instance-types API)
105+
5. Datacenter location (deploy mode: filtered by instance type availability; template mode: all locations, optional)
102106
6. OS image (cluster images excluded)
103107
7. OS volume size (default: 50 GiB)
104108
8. Storage -- add new volumes, attach existing detached volumes, or skip
@@ -135,7 +139,7 @@ Destructive actions (Shutdown, Force shutdown, Delete) show confirmation prompts
135139
- **vm.go** -- Parent command definition, registers subcommands and shortcut commands
136140
- **create.go** -- `vm create` command, flag definitions, `createOptions` struct (with 5-stage mutation lifecycle), request building, contract normalization, volume spec parsing, kind validation
137141
- **wizard.go** -- 13 wizard step definitions using the wizard engine; `clientFunc` lazy client pattern; `WizardMode` (Deploy vs Template); `RunTemplateWizard`; step Default functions for pre-selection
138-
- **wizard_cache.go** -- `apiCache` struct for deduplicating API calls, `ensurePricingCache`, pricing helpers (`volumeHourlyPrice`, `instanceUnits`), instance type matching (`matchesKind`, `formatGPU`, `formatMemory`)
142+
- **wizard_cache.go** -- `apiCache` struct for deduplicating API calls, `fetchLocations` (locations without availability), `loadAllLocations`/`loadAvailableLocations` (extracted location loaders), `ensurePricingCache`, pricing helpers (`volumeHourlyPrice`, `instanceUnits`), instance type matching (`matchesKind`, `formatGPU`, `formatMemory`)
139143
- **wizard_subflows.go** -- Interactive sub-flows for SSH key creation, startup script creation, storage volume management; choice builders for multi-select prompts
140144
- **wizard_summary.go** -- `renderDeploymentSummary` with full cost breakdown (accepts `io.Writer`)
141145
- **template_apply.go** -- `resolveCreateInputs` orchestration, `applyTemplate`, `resolveTemplateNames` (with warnings), `printTemplateSummary`, `pickTemplate`

internal/verda-cli/cmd/vm/create.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra.Command
103103
Long: cmdutil.LongDesc(`
104104
Create a Verda VM instance. Without flags, launches an interactive
105105
wizard. Use --from to pre-fill settings from a saved template.
106+
Flags passed alongside --from override the template values.
106107
107108
Templates are created with "verda template create" and stored
108109
as YAML files under ~/.verda/templates/vm/.
@@ -121,6 +122,10 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra.Command
121122
# From a template file
122123
verda vm create --from ./my-template.yaml
123124
125+
# From a template, override specific fields with flags
126+
verda vm create --from gpu-training --location FIN-03
127+
verda vm create --from gpu-training --hostname my-vm --os-volume-size 200
128+
124129
# Non-interactive with all flags
125130
verda vm create \
126131
--kind gpu \

internal/verda-cli/cmd/vm/template_apply.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ func resolveCreateInputs(
3232
}
3333

3434
// Run wizard for any remaining missing fields.
35-
if opts.InstanceType == "" || opts.Image == "" || opts.Hostname == "" {
35+
// When a template was used but didn't specify a location, prompt the user
36+
// so they can pick where to deploy instead of silently defaulting to FIN-01.
37+
templateWithoutLocation := cmd.Flags().Changed("from") && !opts.locationSet
38+
if opts.InstanceType == "" || opts.Image == "" || opts.Hostname == "" || templateWithoutLocation {
3639
if err := runWizard(cmd.Context(), f, ioStreams, opts); err != nil {
3740
return true, err
3841
}

0 commit comments

Comments
 (0)