summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHimanshu Sardana <himanshusardana2005@gmail.com>2026-03-26 22:31:01 +0000
committerHimanshu Sardana <himanshusardana2005@gmail.com>2026-03-26 22:31:01 +0000
commite49d32933fc31cdf33fc2aa40688f4c8d874a66e (patch)
tree0f4aa695a93d206a3bf59760ef105804e3cd966e
parent2c09f0ab4de62e6a7d1d5e8f74f01097f3a3588d (diff)
feat: add init command
-rw-r--r--cmd/init.go266
-rw-r--r--cmd/root.go11
2 files changed, 277 insertions, 0 deletions
diff --git a/cmd/init.go b/cmd/init.go
new file mode 100644
index 0000000..e7fe58a
--- /dev/null
+++ b/cmd/init.go
@@ -0,0 +1,266 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+var (
+ headerStyle = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("86")).
+ Bold(true).
+ Padding(0, 0, 1, 0)
+
+ inputStyle = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("252")).
+ Background(lipgloss.Color("235")).
+ Padding(0, 1)
+
+ buttonStyle = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("86")).
+ Background(lipgloss.Color("235")).
+ Padding(0, 2).
+ Margin(0, 1)
+
+ buttonActiveStyle = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("235")).
+ Background(lipgloss.Color("86")).
+ Padding(0, 2).
+ Margin(0, 1)
+
+ helpStyle = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("240"))
+)
+
+type InitModel struct {
+ step int
+ blogName string
+ siteTitle string
+ authorName string
+ authorRole string
+ authorBio string
+ theme string
+ themes []string
+ cursor int
+ inputBuffer string
+ quitting bool
+ focusedInput bool
+}
+
+func (m *InitModel) Init() tea.Cmd {
+ m.themes = []string{
+ "modern-light",
+ "modern-dark",
+ "modern-dark-2",
+ "modern-dark-catppuccin",
+ "everforest",
+ "gruvbox",
+ "rose-pine",
+ "terminal-gruvbox",
+ "tufte",
+ }
+ return nil
+}
+
+func (m *InitModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "ctrl+c", "esc":
+ m.quitting = true
+ return m, nil
+ case "enter":
+ return m.handleEnter()
+ case "up":
+ if m.step == 6 {
+ m.cursor--
+ if m.cursor < 0 {
+ m.cursor = len(m.themes) - 1
+ }
+ }
+ case "down":
+ if m.step == 6 {
+ m.cursor++
+ if m.cursor >= len(m.themes) {
+ m.cursor = 0
+ }
+ }
+ case "backspace":
+ if len(m.inputBuffer) > 0 {
+ m.inputBuffer = m.inputBuffer[:len(m.inputBuffer)-1]
+ }
+ case "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+ "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
+ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+ "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
+ "-", "_", " ", ".":
+ m.inputBuffer += msg.String()
+ }
+ }
+ return m, nil
+}
+
+func (m *InitModel) handleEnter() (tea.Model, tea.Cmd) {
+ switch m.step {
+ case 0:
+ m.blogName = m.inputBuffer
+ m.inputBuffer = ""
+ m.step++
+ case 1:
+ m.siteTitle = m.inputBuffer
+ if m.siteTitle == "" {
+ m.siteTitle = m.blogName
+ }
+ m.inputBuffer = ""
+ m.step++
+ case 2:
+ m.authorName = m.inputBuffer
+ m.inputBuffer = ""
+ m.step++
+ case 3:
+ m.authorRole = m.inputBuffer
+ m.inputBuffer = ""
+ m.step++
+ case 4:
+ m.authorBio = m.inputBuffer
+ m.inputBuffer = ""
+ m.step++
+ case 5:
+ m.theme = m.inputBuffer
+ m.inputBuffer = ""
+ if m.theme != "" {
+ m.step = 7
+ } else {
+ m.step++
+ }
+ case 6:
+ m.theme = m.themes[m.cursor]
+ m.step++
+ }
+ return m, nil
+}
+
+func (m *InitModel) View() string {
+ var s string
+
+ switch m.step {
+ case 0:
+ s = headerStyle.Render("╭─── Kite Setup") + "\n" +
+ "\n" + "What's the name of your blog?" + "\n" +
+ "(e.g. my-tech-blog, dev-diary)" + "\n\n" +
+ inputStyle.Render(m.inputBuffer+"_") + "\n\n" +
+ helpStyle.Render("type to enter · enter to continue · esc to cancel")
+ case 1:
+ s = headerStyle.Render("╭─── Kite Setup") + "\n" +
+ "\n" + "Site title (for the header):" + "\n\n" +
+ inputStyle.Render(m.inputBuffer+"_") + "\n\n" +
+ helpStyle.Render("type to enter · enter to continue · esc to cancel")
+ case 2:
+ s = headerStyle.Render("╭─── Kite Setup") + "\n" +
+ "\n" + "Your name:" + "\n\n" +
+ inputStyle.Render(m.inputBuffer+"_") + "\n\n" +
+ helpStyle.Render("type to enter · enter to continue · esc to cancel")
+ case 3:
+ s = headerStyle.Render("╭─── Kite Setup") + "\n" +
+ "\n" + "Your role (e.g. Developer, Writer):" + "\n\n" +
+ inputStyle.Render(m.inputBuffer+"_") + "\n\n" +
+ helpStyle.Render("type to enter · enter to continue · esc to cancel")
+ case 4:
+ s = headerStyle.Render("╭─── Kite Setup") + "\n" +
+ "\n" + "Short bio:" + "\n\n" +
+ inputStyle.Render(m.inputBuffer+"_") + "\n\n" +
+ helpStyle.Render("type to enter · enter to continue · esc to cancel")
+ case 5:
+ s = headerStyle.Render("╭─── Kite Setup") + "\n" +
+ "\n" + "Preferred theme (or press enter to skip):" + "\n\n" +
+ inputStyle.Render(m.inputBuffer+"_") + "\n\n" +
+ helpStyle.Render("type to enter · enter to skip · esc to cancel")
+ case 6:
+ s = headerStyle.Render("╭─── Kite Setup") + "\n\n" +
+ "Select a theme:\n\n"
+ for i, theme := range m.themes {
+ if i == m.cursor {
+ s += " " + buttonActiveStyle.Render("● "+theme) + "\n"
+ } else {
+ s += " " + buttonStyle.Render("○ "+theme) + "\n"
+ }
+ }
+ s += "\n" + helpStyle.Render("↑↓ to select · enter to confirm")
+ }
+
+ return s
+}
+
+func RunInit() error {
+ m := &InitModel{}
+ p := tea.NewProgram(m, tea.WithAltScreen())
+ if _, err := p.Run(); err != nil {
+ return err
+ }
+
+ if m.quitting {
+ fmt.Println("\nInit cancelled.")
+ return nil
+ }
+
+ fmt.Println("\n" + headerStyle.Render("Setting up your blog..."))
+
+ theme := m.theme
+ if theme == "" {
+ theme = "modern-light"
+ }
+
+ dirs := []string{"content", "output", "themes"}
+ for _, dir := range dirs {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("creating %s directory: %w", dir, err)
+ }
+ }
+
+ siteTitle := m.siteTitle
+ if siteTitle == "" {
+ siteTitle = m.blogName
+ }
+
+ configContent := fmt.Sprintf(`siteTitle: "%s"
+authorName: "%s"
+authorRole: "%s"
+authorBio: "%s"
+defaultTheme: "%s"
+`, siteTitle, m.authorName, m.authorRole, m.authorBio, theme)
+
+ if err := os.WriteFile("config.yaml", []byte(configContent), 0644); err != nil {
+ return fmt.Errorf("writing config: %w", err)
+ }
+
+ sampleContent := "---\n" +
+ "title: Welcome to Kite\n" +
+ "date: 2026-01-01\n" +
+ "tags: [getting-started]\n" +
+ "---\n\n" +
+ "# Welcome\n\n" +
+ "This is your first post! Write your content in Markdown here.\n\n" +
+ "## Getting Started\n\n" +
+ "- Add more posts to the content/ directory\n" +
+ "- Run `kite build` to generate your site\n" +
+ "- Run `kite serve` to preview locally\n\n" +
+ "Enjoy blogging!\n"
+
+ if err := os.WriteFile("content/1.md", []byte(sampleContent), 0644); err != nil {
+ return fmt.Errorf("writing sample content: %w", err)
+ }
+
+ fmt.Println(" ✓ Created config.yaml")
+ fmt.Println(" ✓ Created content/ directory")
+ fmt.Println(" ✓ Created output/ directory")
+ fmt.Println(" ✓ Created themes/ directory")
+ fmt.Println(" ✓ Created sample post (content/1.md)")
+ fmt.Println("\nRun `kite build` to generate your site!")
+ fmt.Println("Run `kite serve` to preview it locally.")
+
+ return nil
+}
diff --git a/cmd/root.go b/cmd/root.go
index 79bb257..3ef07bc 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -26,11 +26,20 @@ func Execute() {
runServe(args)
case "list-themes":
runListThemes(args)
+ case "init":
+ runInit(args)
default:
build.ShowHelpMessage()
}
}
+func runInit(args []string) {
+ if err := RunInit(); err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+ }
+}
+
func ShowHelp() {
fmt.Println(`
Kite — A lightweight static site generator
@@ -42,6 +51,7 @@ COMMANDS:
build Build the static site into the output directory
serve Start a local development server with live reload
list-themes List all available themes
+ init Initialize a new blog project
OPTIONS:
-h, --help Show this help message
@@ -51,6 +61,7 @@ EXAMPLES:
kite build gruvbox
kite serve
kite list-themes
+ kite init
DESCRIPTION:
Kite converts your content into a static website using themes and templates.