Skip to content

feat(init): devstack init workspace authoring wizard (spec 22) - #80

Merged
gustavobertoi merged 1 commit into
mainfrom
feat/init-wizard
Jun 30, 2026
Merged

feat(init): devstack init workspace authoring wizard (spec 22)#80
gustavobertoi merged 1 commit into
mainfrom
feat/init-wizard

Conversation

@gustavobertoi

Copy link
Copy Markdown
Contributor

Implements feature #14 / spec 22 — the workspace authoring front door. Pick shared engines, fill typed params, write a structurally-validated workspace.yaml. Pure config authorship: no lock, no Docker, no ledger.

Structured as the spec's "two faces, one builder" — the flag path and the Bubble Tea TUI both populate one scaffold.Inputs and call the same builder.

Foundation (internal/scaffold, fully unit-tested)

  • EmitWorkspaceYAML — deterministic ordered goccy MapSlice emitter (sorted keys; params kept as strings so 16 never renders 16.0).
  • BuildWorkspace / resolveParamsProvides-filter (only engines that declare provides: are shareable), drop-at-default, required-param fail-fast, store-seed.
  • SanitizeName — CWD-basename → valid dsname.
  • config.ValidateWorkspaceBytes — in-memory structural validation reusing the unexported structValidate/formatStructErr; a bad file is caught pre-write, not as a generate parse failure.

Flag path (internal/cli/init.go)

--name --service engine@ver --param svc.key=val --alias --project name=path[,git=url] --from-store --out --dry-run --force --json --no-input. Atomic no-clobber write (temp+rename, --force backs up), parent-workspace guard, RefuseWindowsMount.

Interactive face (internal/prompt + internal/cli/init_tui.go)

A Bubble Tea v2 wizard via charm.land/huh/v2: engine multi-select (store-seeded defaults), per-engine param forms pre-filled with template defaults, and a confirm screen previewing the YAML in a lipgloss box. Behind a shared prompt.IsInteractive gate so --json/--quiet/--no-input/non-TTY/CI always reach the builder without launching bubbletea. The charm v2 stack is pure-Go — CGO_ENABLED=0 4-target cross-build is clean and no v1 charm enters go.mod.

$ devstack init --name demo --service postgres@16 --service redis --service minio
# Generated by `devstack init` — edit freely; re-run is safe with --force.
apiVersion: devstack/v1
kind: Workspace
name: demo
shared:
  minio: { template: minio }
  postgres: { template: postgres, params: { version: "16" } }
  redis: { template: redis }

(emitted block-style; collapsed here for brevity)

Tests / verification

  • internal/scaffold: golden bytes, determinism, int-not-float, Provides-filter, drop-at-default, required fail-fast, store-seed+override.
  • internal/config: ValidateWorkspaceBytes pass/fail matrix.
  • internal/cli: registered, happy-path → LoadWorkspaceOnly, deterministic, --json, --dry-run writes nothing, no-clobber + --force backup, parent-workspace refused, --from-store (never writes the store), non-TTY gate.
  • internal/prompt: IsInteractive matrix.
  • make ci + make determinism green; CGO=0 cross-build green.

Manual TTY check still needed: the interactive wizard's keyboard flow (verified compiling + gated; the builder it feeds is fully unit-tested).

Deferred (next)

  • The internal/migrate consolidation onto EmitWorkspaceYAML (low-risk — migrate tests are structural; byte-shape pinned by the emit golden).
  • A live two-pane (side-by-side) preview is a polish increment over the confirm-screen preview.

🤖 Generated with Claude Code

Adds the file-authoring front door: pick shared engines (postgres/redis/minio),
fill typed params, and write a structurally-validated workspace.yaml. No lock,
no Docker, no ledger — pure config authorship. Structured as the spec's
"two faces, one builder": the flag path and the TUI both feed internal/scaffold.

Foundation (internal/scaffold, fully unit-tested):
- EmitWorkspaceYAML: deterministic ordered goccy emitter (sorted keys; params
  kept as strings so 16 never renders 16.0).
- BuildWorkspace/resolveParams: Provides-filter, drop-at-default, required
  fail-fast, store-seed.
- SanitizeName: CWD-basename -> valid dsname.
- config.ValidateWorkspaceBytes: in-memory structural validation reusing the
  unexported structValidate/formatStructErr — a bad file is caught pre-write.

Flag path (internal/cli/init.go): --name/--service engine@ver/--param
svc.key=val/--alias/--project/--from-store/--out/--dry-run/--force/--json/
--no-input; atomic no-clobber write (+ backup); parent-workspace guard;
RefuseWindowsMount.

Interactive face (internal/prompt + internal/cli/init_tui.go): a Bubble Tea v2
wizard via charm.land/huh/v2 (engine multi-select, per-engine param forms with
defaults, a confirm screen previewing the YAML in a lipgloss box), behind a
shared prompt.IsInteractive gate so --json/--quiet/--no-input/non-TTY/CI always
reach the builder without bubbletea. The charm v2 stack is pure-Go (CGO_ENABLED=0
4-target cross-build clean; no v1 charm).

Tests: scaffold golden/determinism/build matrix, config validate, cli init
(happy/json/dry-run/no-clobber+force/parent-guard/from-store/non-TTY gate),
prompt IsInteractive matrix. make ci + determinism green.

Deferred: the internal/migrate consolidation onto EmitWorkspaceYAML (pinned by
the emit golden); a live two-pane preview is a polish increment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gustavobertoi
gustavobertoi merged commit 19bf7b9 into main Jun 30, 2026
4 checks passed
@gustavobertoi
gustavobertoi deleted the feat/init-wizard branch June 30, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant