Add ghost tutorial command and generated tutorial doc#18
Conversation
| func newTutorialPromptReader(input io.Reader) *tutorialPromptReader { | ||
| return &tutorialPromptReader{ | ||
| input: input, | ||
| bufferedInput: bufio.NewReader(input), | ||
| } | ||
| } | ||
|
|
||
| func (r *tutorialPromptReader) readKey(ctx context.Context) error { | ||
| if terminalInput, ok := r.input.(*os.File); ok && util.IsTerminal(r.input) { | ||
| fd := int(terminalInput.Fd()) | ||
| state, err := term.MakeRaw(fd) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to configure terminal: %w", err) | ||
| } | ||
|
|
||
| key, readErr := readTutorialValue(ctx, r.bufferedInput.ReadByte) | ||
| restoreErr := term.Restore(fd, state) | ||
| if readErr != nil { | ||
| return readErr | ||
| } | ||
| if restoreErr != nil { | ||
| return fmt.Errorf("failed to restore terminal: %w", restoreErr) | ||
| } | ||
| if key == byte(3) { | ||
| return context.Canceled | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| _, err := r.readLine(ctx) | ||
| return err | ||
| } | ||
|
|
||
| func (r *tutorialPromptReader) readLine(ctx context.Context) (string, error) { | ||
| line, err := readTutorialValue(ctx, func() (string, error) { | ||
| return r.bufferedInput.ReadString('\n') | ||
| }) | ||
| return strings.TrimSpace(line), err | ||
| } | ||
|
|
||
| func readTutorialValue[T any](ctx context.Context, readFn func() (T, error)) (T, error) { | ||
| type result struct { | ||
| value T | ||
| err error | ||
| } | ||
|
|
||
| resultCh := make(chan result, 1) | ||
| go func() { | ||
| value, err := readFn() | ||
| if ctx.Err() != nil { | ||
| return | ||
| } | ||
| resultCh <- result{value: value, err: err} | ||
| }() | ||
|
|
||
| select { | ||
| case <-ctx.Done(): | ||
| var zero T | ||
| return zero, ctx.Err() | ||
| case result := <-resultCh: | ||
| return result.value, result.err | ||
| } | ||
| } |
There was a problem hiding this comment.
This code has a lot of overlap with /internal/util/read.go. Any reason not to consolidate?
There was a problem hiding this comment.
Looks like it could be. Neither I nor Claude knew this existed, apparently. We should probably add something to the CLAUDE.md along the lines of "always search for existing implementations that can be reused before writing a new utility function"...
There was a problem hiding this comment.
Ok, well then the problem is that I didn't know it existed, and Claude loves to duplicate things.
| func removeTutorialName(names []string, name string) []string { | ||
| for i, n := range names { | ||
| if n == name { | ||
| return append(names[:i], names[i+1:]...) | ||
| } | ||
| } | ||
| return names | ||
| } |
There was a problem hiding this comment.
I think you could use slices.DeleteFunc() instead of this custom function.
The slices package is newish, and therefore under-represented in LLM training data, so LLMs often fail to use it when it would make sense.
| @@ -0,0 +1,125 @@ | |||
| # Learn the basics of Ghost | |||
There was a problem hiding this comment.
Is there a plan to do something with this markdown version of the tutorial?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I think @thenoahhein probably set that up. Not sure if it's fully automated or not.
Summary
Adds
ghost tutorial, an interactive guided tour of the core Ghost workflow (create → add data → fork → mutate → compare → delete), and generates a matching written tutorial atdocs/tutorials/learn-the-basics.mdfrom the same source.What the tutorial does
Walks the user through 7 steps:
[Y/n]cleanup prompt)For each step the tutorial:
ghost ...command.cmd.Root().ExecuteContext()so the real sub-command runs against the user's stdout/stderr — output streams in real time and progress indicators like the--waitspinner work naturally.--config-dir) and appends--version-check=falseso the update banner doesn't appear once per step.If a step fails before cleanup, the deferred handler prints
ghost delete ... --confirmlines for every database that was successfully created so the user can reclaim them.How the docs stay in sync with the live tutorial
Each tutorial is a
tutorialstruct bundling:filename— where it ends up underdocs/tutorials/title,callout,intro— markdown-only narrativesteps []tutorialStep— the ordered live stepsdeleteStep tutorialStep— the optional cleanup stepEach step is a list of
tutorialBlocks, each of which can carry:prose— shown before the commandargs— the sub-command invocationexpectedOutput— shown only in the markdown doctarget—tutorialTargetAll(default),tutorialTargetCLIOnly, ortutorialTargetDocsOnlycreatesDatabase/removesDatabase— live-runtime cleanup trackingBoth the CLI loop in
runTutorialand the markdown renderer intutorial_docs.gowalk the same[]tutorialStep, each filtering blocks by its own audience. Docs-only callouts (like the cleanup preamble at the top of Step 7) live alongside the step they describe without affecting the CLI flow.A new registry
allTutorials() []tutoriallists every tutorial. To add another tutorial, append abuildXxxTutorial(...)call to that function.Doc generation
The golden test
TestAllTutorialDocsMatchGoldenFilesiterates the registry and fails if any on-disk file drifts from the renderer output, so CI catches forgotten regenerations.Tests
TestTutorialCmd— drives the CLI with stubbedtutorialGenerateNameSuffix,common.WaitForDatabaseWithProgress, andcommon.ExecuteQuery; mocks API calls per sub-command; scripts stdin with the right number of keypresses plus the cleanup answer. Covers non-interactive stdin, not-logged-in, read-only config, keep-databases, and delete-databases paths.TestAllTutorialDocsMatchGoldenFiles— re-renders every tutorial doc and compares againstdocs/tutorials/<filename>.Other changes
common.ExecuteQueryandcommon.WaitForDatabaseWithProgressare nowvars so the tutorial tests can stub them (matches the existingcommon.OpenBrowserpattern).ghost tutorial" callout in Getting Started plus a row in the commands table.