Skip to content
Merged
6 changes: 3 additions & 3 deletions cmd/wfkit/docs_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ func (f *docsFlow) loadConfig() error {

func (f *docsFlow) printHeader() {
utils.PrintSection("Docs Hub")
utils.PrintKeyValue("Webflow", f.baseURL)
utils.PrintKeyValue("Markdown", f.options.EntryPath)
utils.PrintKeyValue("Page slug", f.options.PageSlug)
utils.PrintKeyValue("Site", f.baseURL)
utils.PrintKeyValue("Entry", f.options.EntryPath)
utils.PrintKeyValue("Page", f.options.PageSlug)
fmt.Println()
}

Expand Down
16 changes: 7 additions & 9 deletions cmd/wfkit/docs_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import (
func printDocsTimeline(authed, planned, applied bool) {
utils.PrintTimeline(
"Docs Timeline",
utils.TimelineStep{Label: "Authenticate", Status: timelineStatus(authed, false), Details: timelineDetails(authed, "Webflow session ready")},
utils.TimelineStep{Label: "Plan docs hub", Status: timelineStatus(planned, false), Details: timelineDetails(planned, "markdown rendered and target page prepared")},
utils.TimelineStep{Label: "Apply docs hub", Status: timelineStatus(applied, false), Details: timelineDetails(applied, "page created or docs block updated")},
utils.TimelineStep{Label: "Auth", Status: timelineStatus(authed, false), Details: timelineDetails(authed, "session ready")},
utils.TimelineStep{Label: "Plan", Status: timelineStatus(planned, false), Details: timelineDetails(planned, "page prepared")},
utils.TimelineStep{Label: "Apply", Status: timelineStatus(applied, false), Details: timelineDetails(applied, "page updated")},
)
}

func printDocsPlan(plan publish.DocsHubPlan) {
utils.PrintSection("Docs Hub Plan")
utils.PrintSection("Docs Hub")
utils.PrintStatus(docsStatus(plan.Action), plan.PageSlug, plan.Message)
utils.PrintKeyValue("Markdown", plan.EntryPath)
utils.PrintKeyValue("Target page", displayValue(plan.PageTitle))
Expand All @@ -28,11 +28,9 @@ func printDocsPlan(plan publish.DocsHubPlan) {

func printDocsResult(result publish.DocsHubResult) {
utils.PrintSection("Docs Result")
utils.PrintSummary(
utils.SummaryMetric{Label: "Created", Value: map[bool]string{true: "yes", false: "no"}[result.Created], Tone: "info"},
utils.SummaryMetric{Label: "Updated", Value: map[bool]string{true: "yes", false: "no"}[result.Updated], Tone: "success"},
utils.SummaryMetric{Label: "Published", Value: map[bool]string{true: "yes", false: "no"}[result.Published], Tone: "info"},
)
utils.PrintStatus("INFO", "Created", map[bool]string{true: "yes", false: "no"}[result.Created])
utils.PrintStatus("OK", "Updated", map[bool]string{true: "yes", false: "no"}[result.Updated])
utils.PrintStatus("INFO", "Published", map[bool]string{true: "yes", false: "no"}[result.Published])
fmt.Println()
}

Expand Down
24 changes: 24 additions & 0 deletions cmd/wfkit/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func printDoctorReport(checks []doctorCheck) {
utils.PrintStatus(string(check.Status), check.Name, check.Message)
}

utils.PrintSection("Summary")
utils.PrintStatus(overallDoctorStatus(warnCount, failCount), "Overall", overallDoctorMessage(warnCount, failCount))
utils.PrintSummary(
utils.SummaryMetric{Label: "Passed", Value: fmt.Sprintf("%d", passCount), Tone: "success"},
utils.SummaryMetric{Label: "Warnings", Value: fmt.Sprintf("%d", warnCount), Tone: "warning"},
Expand Down Expand Up @@ -136,6 +138,28 @@ func doctorDashboardCards(checks []doctorCheck) []utils.DashboardCard {
return cards
}

func overallDoctorStatus(warnCount, failCount int) string {
switch {
case failCount > 0:
return "FAIL"
case warnCount > 0:
return "WARN"
default:
return "OK"
}
}

func overallDoctorMessage(warnCount, failCount int) string {
switch {
case failCount > 0:
return "Resolve blocking issues before relying on this setup."
case warnCount > 0:
return "Core setup works, but a few things are still worth checking."
default:
return "Everything looks ready."
}
}

func checkFileExists(name, path string) doctorCheck {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
Expand Down
27 changes: 27 additions & 0 deletions cmd/wfkit/interactive_command_flows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"flag"
"strings"
"testing"

"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -113,3 +114,29 @@ func TestInteractiveDoctorFlowBuildsContext(t *testing.T) {
t.Fatal("expected skip-auth to be set")
}
}

func TestInteractiveSupportFlowMetadata(t *testing.T) {
parent := cli.NewContext(&cli.App{Version: "1.6.0"}, flag.NewFlagSet("wfkit", flag.ContinueOnError), nil)

title, description, target := newInteractiveSupportFlow(parent, "request_feature").metadata()
if title != "Request a feature" {
t.Fatalf("unexpected feature title: %q", title)
}
if !strings.Contains(description, "feature request form") {
t.Fatalf("unexpected feature description: %q", description)
}
if !strings.Contains(target, "template=feature_request.yml") {
t.Fatalf("unexpected feature target: %q", target)
}

title, description, target = newInteractiveSupportFlow(parent, "report_bug").metadata()
if title != "Report a bug" {
t.Fatalf("unexpected bug title: %q", title)
}
if !strings.Contains(description, "bug report form") {
t.Fatalf("unexpected bug description: %q", description)
}
if !strings.Contains(target, "template=bug_report.yml") {
t.Fatalf("unexpected bug target: %q", target)
}
}
200 changes: 152 additions & 48 deletions cmd/wfkit/interactive_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package main

import (
"fmt"
"strings"

"wfkit/internal/config"
"wfkit/internal/updater"
"wfkit/internal/utils"

Expand All @@ -12,6 +14,7 @@ import (

type interactiveFlow struct {
cliContext *cli.Context
category string
action string
}

Expand All @@ -23,45 +26,97 @@ func (f *interactiveFlow) run() error {
for {
f.printHeader()

if err := f.selectAction(); err != nil {
if err := f.selectCategory(); err != nil {
return err
}

if f.action == "exit" {
if f.category == "exit" {
utils.CPrint("Goodbye!", "cyan")
return nil
}

utils.ClearScreen()
if err := f.dispatch(); err != nil {
return err
if action, ok := categoryAction(f.category); ok {
f.action = action
utils.ClearScreen()
if err := f.dispatch(); err != nil {
return err
}
continue
}

for {
utils.ClearScreen()
f.printHeader()
if err := f.selectAction(); err != nil {
return err
}
if f.action == "back" {
break
}
utils.ClearScreen()
if err := f.dispatch(); err != nil {
return err
}
}
}
}

func (f *interactiveFlow) printHeader() {
version := f.cliContext.App.Version
utils.PrintAppHeader(version, "Build Webflow scripts locally, proxy safely, and publish with confidence.")

utils.PrintSection("Quick Start")
for _, item := range interactiveQuickStartItems() {
utils.PrintStatus("READY", item.title, item.description)
}
fmt.Println()
f.printProjectSummary()

if updateManager := updater.NewUpdateManager(version); updateManager != nil {
if result, err := updateManager.Check(updater.CheckOptions{AllowStale: true}); err == nil && result.Available {
utils.PrintUpdateBanner(version, result.LatestVersion)
f.printUpdateNotice(version, result.LatestVersion)
}
}
}

func (f *interactiveFlow) printProjectSummary() {
cfg, err := config.ReadConfig()
if err != nil {
utils.PrintSection("Project")
utils.PrintStatus("WARN", "Config", err.Error())
fmt.Println()
return
}

utils.PrintSection("Project")
utils.PrintKeyValue("App", displayValue(cfg.AppName))
utils.PrintKeyValue("Site", displayValue(cfg.EffectiveSiteURL()))
utils.PrintKeyValue("Package", displayValue(cfg.PackageManager))
utils.PrintKeyValue("Delivery", displayValue(cfg.DeliveryMode))
utils.PrintKeyValue("Assets", displayValue(cfg.AssetBranch))
utils.PrintKeyValue("Docs", displayValue(cfg.DocsPageSlug))
utils.PrintKeyValue("Build", displayValue(cfg.BuildDir))
fmt.Println()
}

func (f *interactiveFlow) printUpdateNotice(currentVersion, latestVersion string) {
utils.PrintStatus("WARN", fmt.Sprintf("Update available: v%s", latestVersion), compactUpdateMessage(currentVersion, latestVersion))
fmt.Println()
}

func (f *interactiveFlow) selectCategory() error {
return huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Categories").
Description("Choose a category first. Esc or Ctrl+C exits.").
Options(interactiveCategoryOptions()...).
Value(&f.category),
),
).Run()
}

func (f *interactiveFlow) selectAction() error {
return huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("What would you like to do?").
Options(interactiveActionOptions()...).
Title(categoryTitle(f.category)).
Description("Type to filter. Enter selects. Esc returns to the previous screen.").
Options(interactiveActionOptions(f.category)...).
Value(&f.action),
),
).Run()
Expand Down Expand Up @@ -90,50 +145,99 @@ func (f *interactiveFlow) dispatch() error {
case "update":
return updateMode(f.cliContext)
case "report_bug":
return openBugReport(f.cliContext)
return newInteractiveSupportFlow(f.cliContext, "report_bug").run()
case "request_feature":
return openFeatureRequest(f.cliContext)
return newInteractiveSupportFlow(f.cliContext, "request_feature").run()
default:
return nil
}
}

type interactiveQuickStartItem struct {
title string
description string
func interactiveCategoryOptions() []huh.Option[string] {
return []huh.Option[string]{
huh.NewOption("Develop", "develop"),
huh.NewOption("Ship", "ship"),
huh.NewOption("Content", "content"),
huh.NewOption("Project", "project"),
huh.NewOption("Check for updates", "update"),
huh.NewOption("Request a feature", "request_feature"),
huh.NewOption("Report a bug", "report_bug"),
huh.NewOption("Exit", "exit"),
}
}

func interactiveActionOptions(category string) []huh.Option[string] {
switch category {
case "develop":
return []huh.Option[string]{
huh.NewOption("Proxy local site", "proxy_dev"),
huh.NewOption("Migrate code", "migrate"),
huh.NewOption("Run doctor", "doctor"),
huh.NewOption("Back", "back"),
}
case "ship":
return []huh.Option[string]{
huh.NewOption("Publish code", "publish"),
huh.NewOption("Publish docs", "docs"),
huh.NewOption("Back", "back"),
}
case "content":
return []huh.Option[string]{
huh.NewOption("Manage pages", "pages"),
huh.NewOption("Manage CMS", "cms"),
huh.NewOption("Back", "back"),
}
case "project":
return []huh.Option[string]{
huh.NewOption("Initialize project", "init"),
huh.NewOption("Configure defaults", "config"),
huh.NewOption("Back", "back"),
}
default:
return []huh.Option[string]{huh.NewOption("Back", "back")}
}
}

func interactiveQuickStartItems() []interactiveQuickStartItem {
return []interactiveQuickStartItem{
{
title: "Initialize",
description: "Scaffold a Webflow-ready Vite project with pages, globals, and config.",
},
{
title: "Develop",
description: "Proxy the live site locally and inject your dev entry without touching production.",
},
{
title: "Docs",
description: "Render markdown and publish a dedicated documentation page inside Webflow.",
},
func categoryTitle(category string) string {
switch category {
case "develop":
return "Develop"
case "ship":
return "Ship"
case "content":
return "Content"
case "project":
return "Project"
case "update":
return "Check for updates"
case "request_feature":
return "Request a feature"
case "report_bug":
return "Report a bug"
default:
return "Actions"
}
}

func interactiveActionOptions() []huh.Option[string] {
return []huh.Option[string]{
huh.NewOption("Initialize a project", "init"),
huh.NewOption("Publish docs", "docs"),
huh.NewOption("Manage pages", "pages"),
huh.NewOption("Manage CMS", "cms"),
huh.NewOption("Migrate page code", "migrate"),
huh.NewOption("Publish code to Webflow", "publish"),
huh.NewOption("Start dev proxy", "proxy_dev"),
huh.NewOption("Run doctor", "doctor"),
huh.NewOption("Configure CLI defaults", "config"),
huh.NewOption("Check for updates", "update"),
huh.NewOption("Report a bug", "report_bug"),
huh.NewOption("Request a feature", "request_feature"),
huh.NewOption("Exit", "exit"),
func categoryAction(category string) (string, bool) {
switch category {
case "update", "request_feature", "report_bug":
return category, true
default:
return "", false
}
}

func compactUpdateMessage(currentVersion, latestVersion string) string {
parts := make([]string, 0, 2)
if strings.TrimSpace(currentVersion) != "" {
parts = append(parts, "current v"+currentVersion)
}
if strings.TrimSpace(latestVersion) != "" {
parts = append(parts, "latest v"+latestVersion)
}
if len(parts) == 0 {
return "Run `wfkit update` when ready."
}
return strings.Join(parts, " ") + " Run `wfkit update` when ready."
}
Loading
Loading