Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

## Repository Structure

- **`cmd/`** - Binary entry points. Contains `ghost/main.go` (the main CLI binary, which sets up context/signal handling and delegates to the internal command infrastructure), `npm-publisher/` (a CI tool that generates and publishes npm packages for each platform), and `generate-docs/` (generates Markdown CLI reference docs to `docs/cli/`).
- **`cmd/`** - Binary entry points. Contains `ghost/main.go` (the main CLI binary, which sets up context/signal handling and delegates to the internal command infrastructure), `npm-publisher/` (a CI tool that generates and publishes npm packages for each platform), `generate-docs/` (generates Markdown CLI reference docs to `docs/cli/`), and `generate-tutorial-docs/` (renders every tutorial in the `allTutorials()` registry to `docs/tutorials/`, sharing source-of-truth step data with the live `ghost tutorial` command).
- **`internal/`** - All core application logic (non-public Go packages).
- **`internal/cmd/`** - Cobra command implementations for all CLI commands (init, create, fork, list, delete, pause, resume, connect, psql, sql, schema, logs, password, pricing, rename, status, feedback, api-key, login, logout, config, mcp, version, upgrade, completion, payment). Each command lives in its own file, named to match the command in snake_case (e.g. `ghost payment list` → `payment_list.go`). Helper files like `completion.go`, `errors.go`, and `logger.go` contain shared utilities. Commands that are not yet ready for public release can be gated behind the `GHOST_EXPERIMENTAL` env var (see `internal/common/app.go`'s `App.Experimental` field).
- **`internal/tutorial/`** - Data definitions for Ghost's guided tutorials. Each `Tutorial` is a struct bundling `Filename`, narrative (`Title`/`Callout`/`Intro`), an ordered `[]Step`, and an optional `DeleteStep`. Blocks carry CLI args, prose, expected output (markdown-only), and a `Target` enum that scopes them to CLI runs, doc renders, or both. `All()` is the registry imported by both the live CLI command and the `generate-tutorial-docs` binary.
- **`internal/cmd/`** - Cobra command implementations for all CLI commands (init, tutorial, create, fork, list, delete, pause, resume, connect, psql, sql, schema, logs, password, pricing, rename, status, feedback, api-key, login, logout, config, mcp, version, upgrade, completion, payment). Each command lives in its own file, named to match the command in snake_case (e.g. `ghost payment list` → `payment_list.go`). Helper files like `completion.go`, `errors.go`, and `logger.go` contain shared utilities. Commands that are not yet ready for public release can be gated behind the `GHOST_EXPERIMENTAL` env var (see `internal/common/app.go`'s `App.Experimental` field).
- **`internal/api/`** - API client layer. Includes an OpenAPI-generated REST client (`client.go`, `types.go`), shared HTTP client singleton, and request/response types. **Do not edit `client.go` or `types.go` by hand** — they are generated from `openapi.yaml` (see [Code Generation](#code-generation)). The `mock/` subdirectory contains a generated mock of `ClientWithResponsesInterface` for use in tests.
- **`internal/config/`** - Configuration management. Handles config file loading (via Viper), credential storage (keyring with file fallback), and version checking.
- **`internal/common/`** - Shared business logic used across commands and MCP tools. Includes API client initialization, database connection/schema/query utilities, error handling with exit codes, and version update checks.
Expand Down Expand Up @@ -308,6 +309,10 @@ After adding new commands, directories, or major functionality, update:
- **`README.md`** — Update the Commands table and Usage examples as needed.
- **`docs/`** — Regenerate the CLI reference docs (see below).

### Tutorial Docs

The live `ghost tutorial` command and the markdown tutorials under `docs/tutorials/` share a single source of truth in `internal/tutorial/`. Each `tutorial.Tutorial` bundles its filename, title/callout/intro narrative, ordered `[]tutorial.Step`, and optional `DeleteStep`. Steps contain `tutorial.Block`s whose `Target` field controls visibility: `TargetAll` (default), `TargetCLIOnly`, or `TargetDocsOnly`. A block can carry CLI args, an `ExpectedOutput` string shown only in the markdown, and side-effect tracking (`CreatesDatabase`/`RemovesDatabase`) used by the live runtime. The renderer in `cmd/generate-tutorial-docs/docs.go` is content-agnostic — it walks the struct without any hard-coded text or step numbers, and `tutorial.FormatCommand` is shared with the CLI echo so multi-statement SQL formatting stays in sync. To add a new tutorial, create a `BuildXxxTutorial` function in `internal/tutorial/` and append it to `tutorial.All()`. After editing tutorial content, regenerate the docs with `go run ./cmd/generate-tutorial-docs` (`-out` defaults to `./docs/tutorials`). The golden test `TestAllTutorialDocsMatchGoldenFiles` (in `cmd/generate-tutorial-docs/`) iterates the registry and fails if any on-disk markdown drifts.

### CLI Reference Docs

The `docs/cli/` directory contains generated Markdown reference documentation for every CLI command. These are produced by `cmd/generate-docs`, which uses Cobra's `doc` package to walk the command tree and emit one file per command.
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ ghost create # Create a new Postgres database
ghost list # List all databases
```

Learn more about ghost's forking workflow and other features with the interactive tutorial:

```bash
ghost tutorial
```

## Commands

| Command | Description |
Expand Down Expand Up @@ -84,6 +90,7 @@ ghost list # List all databases
| `schema` | Display database schema information |
| `share` | Share a database |
| `sql` | Execute SQL query on a database |
| `tutorial` | Run an interactive Ghost tutorial |
| `usage` | Show space usage |
| `upgrade` | Upgrade the ghost CLI to the latest version |
| `version` | Show version information |
Expand Down
88 changes: 88 additions & 0 deletions cmd/generate-tutorial-docs/docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"fmt"
"strings"

"github.com/timescale/ghost/internal/tutorial"
)

// renderTutorialMarkdown walks a tutorial.Tutorial and emits markdown. The
// renderer is content-agnostic: every piece of tutorial-specific text comes
// from the struct, so updating a tutorial only requires editing its
// definition in the tutorial package.
func renderTutorialMarkdown(t tutorial.Tutorial) string {
var sb strings.Builder

fmt.Fprintf(&sb, "# %s\n\n", t.Title)
if t.Callout != "" {
fmt.Fprintf(&sb, "> %s\n\n", t.Callout)
}
writeParagraphs(&sb, t.Intro)

for i, step := range t.Steps {
writeStepMarkdown(&sb, i+1, step)
}
writeStepMarkdown(&sb, len(t.Steps)+1, t.DeleteStep)

return strings.TrimRight(sb.String(), "\n") + "\n"
}

func writeParagraphs(sb *strings.Builder, paragraphs []string) {
for _, p := range paragraphs {
sb.WriteString(p)
sb.WriteString("\n\n")
}
}

func writeStepMarkdown(sb *strings.Builder, number int, step tutorial.Step) {
fmt.Fprintf(sb, "## Step %d — %s\n\n", number, step.Title)

visibleBlocks := tutorial.FilterBlocks(step.Blocks, tutorial.TargetDocsOnly)

if step.JoinedBlocks {
for _, block := range visibleBlocks {
if block.Prose != "" {
sb.WriteString(block.Prose + "\n\n")
}
}
commands := make([]string, 0, len(visibleBlocks))
outputs := make([]string, 0, len(visibleBlocks))
for _, block := range visibleBlocks {
if len(block.Args) == 0 {
continue
}
commands = append(commands, tutorial.FormatCommand(block.Args))
if block.ExpectedOutput != "" {
outputs = append(outputs, block.ExpectedOutput)
}
}
if len(commands) > 0 {
writeCodeBlock(sb, "bash", strings.Join(commands, "\n"))
}
if len(outputs) > 0 {
writeCodeBlock(sb, "", strings.Join(outputs, "\n"))
}
return
}

for _, block := range visibleBlocks {
if block.Prose != "" {
sb.WriteString(block.Prose + "\n\n")
}
if len(block.Args) > 0 {
writeCodeBlock(sb, "bash", tutorial.FormatCommand(block.Args))
if block.ExpectedOutput != "" {
writeCodeBlock(sb, "", block.ExpectedOutput)
}
}
}
}

func writeCodeBlock(sb *strings.Builder, lang, content string) {
sb.WriteString("```")
sb.WriteString(lang)
sb.WriteString("\n")
sb.WriteString(content)
sb.WriteString("\n```\n\n")
}
26 changes: 26 additions & 0 deletions cmd/generate-tutorial-docs/docs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"os"
"path/filepath"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/timescale/ghost/internal/tutorial"
)

func TestAllTutorialDocsMatchGoldenFiles(t *testing.T) {
for _, tut := range tutorial.All() {
t.Run(tut.Filename, func(t *testing.T) {
goldenPath := filepath.Join("..", "..", "docs", "tutorials", tut.Filename)
want, err := os.ReadFile(goldenPath)
if err != nil {
t.Fatalf("failed to read %s: %v", goldenPath, err)
}
got := renderTutorialMarkdown(tut)
if diff := cmp.Diff(string(want), got); diff != "" {
t.Errorf("%s out of date (run `go run ./cmd/generate-tutorial-docs`):\n%s", goldenPath, diff)
}
})
}
}
28 changes: 28 additions & 0 deletions cmd/generate-tutorial-docs/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"path/filepath"

"github.com/timescale/ghost/internal/tutorial"
)

func main() {
outDir := flag.String("out", "./docs/tutorials", "Output directory")
flag.Parse()

if err := os.MkdirAll(*outDir, 0o755); err != nil {
log.Fatal(err)
}

for _, t := range tutorial.All() {
path := filepath.Join(*outDir, t.Filename)
if err := os.WriteFile(path, []byte(renderTutorialMarkdown(t)), 0o644); err != nil {
log.Fatal(err)
}
fmt.Printf("Generated %s\n", path)
}
}
1 change: 1 addition & 0 deletions docs/cli/ghost.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Ghost is a command-line interface for managing PostgreSQL databases.
* [ghost schema](ghost_schema.md) - Display database schema information
* [ghost share](ghost_share.md) - Share a database
* [ghost sql](ghost_sql.md) - Execute SQL query on a database
* [ghost tutorial](ghost_tutorial.md) - Run an interactive Ghost tutorial
* [ghost upgrade](ghost_upgrade.md) - Upgrade the ghost CLI to the latest version
* [ghost usage](ghost_usage.md) - Show space usage
* [ghost version](ghost_version.md) - Show version information
47 changes: 47 additions & 0 deletions docs/cli/ghost_tutorial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: "ghost tutorial"
slug: "ghost_tutorial"
description: "CLI reference for ghost tutorial"
---

## ghost tutorial

Run an interactive Ghost tutorial

### Synopsis

Run an interactive tutorial that demonstrates the core Ghost workflow.

The tutorial creates a temporary database, inserts
sample data, forks the database, mutates the fork, compares the original and
fork, and then asks whether to delete or keep the tutorial databases. Each step
explains and echoes the equivalent Ghost CLI command before running it.

```
ghost tutorial [flags]
```

### Examples

```
ghost tutorial
```

### Options

```
-h, --help help for tutorial
```

### Options inherited from parent commands

```
--analytics enable/disable usage analytics (default true)
--color enable colored output (default true)
--config-dir string config directory (default "~/.config/ghost")
--version-check check for updates (default true)
```

### SEE ALSO

* [ghost](ghost.md) - CLI for managing Postgres databases
125 changes: 125 additions & 0 deletions docs/tutorials/learn-the-basics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Learn the basics of Ghost
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a plan to do something with this markdown version of the tutorial?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing tutorials in this folder are published to https://ghost.build/tutorials/. I think that's automated, so this will also appear there? Either way, my intention is to publish it there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @thenoahhein probably set that up. Not sure if it's fully automated or not.


> Run `ghost tutorial` to step through this tutorial live in the CLI.

This guided tour walks through the core Ghost workflow: create a database, load data, fork it, change the fork, compare the results, and clean up. Each step shows the exact `ghost` command the live tutorial runs and the output you can expect to see.

Throughout this guide, the temporary databases are named `tutorial-example` and `tutorial-example-fork`. The live `ghost tutorial` command generates a random suffix instead.

## Step 1 — Create a database

```bash
ghost create --name tutorial-example --wait
```

```
Created database 'tutorial-example'
ID: abc1234567
Connection: postgresql://tsdbadmin:<password>@<host>:5432/tsdb?sslmode=require
```

## Step 2 — Add sample data with SQL

The sql command connects to the database and executes the query you provide.

```bash
ghost sql tutorial-example \
"CREATE TABLE ghost_tutorial_items (id serial PRIMARY KEY, name text NOT NULL, location text NOT NULL);
INSERT INTO ghost_tutorial_items (name, location) VALUES ('apples', 'original'), ('bananas', 'original'), ('carrots', 'original');"
```

```
CREATE TABLE
INSERT 0 3
```

## Step 3 — Query the original database

```bash
ghost sql tutorial-example "SELECT id, name, location FROM ghost_tutorial_items ORDER BY id;"
```

```
id │ name │ location
────┼─────────┼──────────
1 │ apples │ original
2 │ bananas │ original
3 │ carrots │ original
(3 rows)
```

## Step 4 — Fork the database

Forking creates an independent copy you can safely experiment with.

```bash
ghost fork tutorial-example --name tutorial-example-fork --wait
```

```
Forked 'tutorial-example' → 'tutorial-example-fork'
ID: def1234567
Connection: postgresql://tsdbadmin:<password>@<host>:5432/tsdb?sslmode=require
```

## Step 5 — Mutate the fork

These changes are made only on the fork.

```bash
ghost sql tutorial-example-fork \
"INSERT INTO ghost_tutorial_items (name, location) VALUES ('dragonfruit', 'fork');
UPDATE ghost_tutorial_items SET location = 'fork' WHERE name = 'bananas';"
```

```
INSERT 0 1
UPDATE 1
```

## Step 6 — Compare the original and the fork

First, query the original database:

```bash
ghost sql tutorial-example "SELECT id, name, location FROM ghost_tutorial_items ORDER BY id;"
```

```
id │ name │ location
────┼─────────┼──────────
1 │ apples │ original
2 │ bananas │ original
3 │ carrots │ original
(3 rows)
```

Now query the fork. Notice the extra row and updated value:

```bash
ghost sql tutorial-example-fork "SELECT id, name, location FROM ghost_tutorial_items ORDER BY id;"
```

```
id │ name │ location
────┼─────────────┼──────────
1 │ apples │ original
2 │ bananas │ fork
3 │ carrots │ original
4 │ dragonfruit │ fork
(4 rows)
```

## Step 7 — Delete the tutorial databases

When the main steps finish, the live tutorial asks whether to delete the databases. To run the cleanup step yourself, use the following.

```bash
ghost delete tutorial-example-fork --confirm
ghost delete tutorial-example --confirm
```

```
Deleted 'tutorial-example-fork' (def1234567)
Deleted 'tutorial-example' (abc1234567)
```
1 change: 1 addition & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func buildRootCmd() (*cobra.Command, *common.App, error) {
cmd.AddCommand(buildConfigCmd(app))
cmd.AddCommand(buildMCPCmd(app))
cmd.AddCommand(buildInitCmd(app))
cmd.AddCommand(buildTutorialCmd(app))
cmd.AddCommand(buildLoginCmd(app))
cmd.AddCommand(buildLogoutCmd(app))
cmd.AddCommand(buildCreateCmd(app))
Expand Down
Loading