[object Object]

← back to Cli Printing Press

fix(cli): thread ctx through wrapper functions in generated CLIs (#442) (#445)

493a1b15d6eecac3ec5419dea45a73ac86412487 · 2026-04-30 11:58:40 -0700 · Trevin Chow

* fix(cli): thread ctx through wrapper functions in generated CLIs

Closes #442. Completes the ctx-propagation work started in #436/#441
by threading caller context through the four wrapper functions that
PR #441's review surfaced as still-on-context.Background():

- openStoreForRead → resolveLocal → resolveRead / resolvePaginatedRead.
  All four take a context.Context now. The two public functions
  (resolveRead, resolvePaginatedRead) carry the change to their callers
  in command_promoted.go.tmpl and command_endpoint.go.tmpl, which now
  pass cmd.Context() from inside the Cobra RunE.

- writeThroughCache(ctx, resourceType, data) — the best-effort cache
  writer in resolveRead's auto path. Caller is resolveRead itself, so
  the ctx flows naturally.

- openShareStore(ctx) — share_commands.go.tmpl. Four Cobra RunE callers
  updated to pass cmd.Context().

- collectCacheReport(ctx, staleAfterSpec) — doctor.go.tmpl. Single
  Cobra RunE caller updated.

Net effect: SIGINT during a generated CLI's resolveRead, share, or
doctor flow now interrupts the migration retry loop in store.Open
instead of waiting out migrationLockTimeout. This was the gap PR #441
explicitly deferred when the wrapper-function refactor scope was
larger than that PR's focused fix.

Golden fixture refreshed for the four new ctx-passing call sites in
the generate-golden-api expected output (doctor.go, projects_list.go,
projects_tasks_list-project.go, promoted_public.go).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(cli): regression-guard against store.Open( in generated CLIs

Adds TestGenerateNoUnscopedStoreOpen — generates a representative
store-bearing CLI (cache enabled + share enabled, the broadest set of
templates that historically called store.Open) and walks every
non-test .go file under internal/, asserting no literal store.Open(
remains. The substring "store.Open(" doesn't match
"store.OpenWithContext(" so the check is precise without regex.

Manually verified the test catches a regression by temporarily
reverting share_commands.go.tmpl back to store.Open(dbPath) — the
test correctly fails with the file path and a clear remediation
message. Reverting the revert restores green.

Net result: any future template that re-introduces a context-less
store.Open call fails this test before it ships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 493a1b15d6eecac3ec5419dea45a73ac86412487
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Thu Apr 30 11:58:40 2026 -0700

    fix(cli): thread ctx through wrapper functions in generated CLIs (#442) (#445)
    
    * fix(cli): thread ctx through wrapper functions in generated CLIs
    
    Closes #442. Completes the ctx-propagation work started in #436/#441
    by threading caller context through the four wrapper functions that
    PR #441's review surfaced as still-on-context.Background():
    
    - openStoreForRead → resolveLocal → resolveRead / resolvePaginatedRead.
      All four take a context.Context now. The two public functions
      (resolveRead, resolvePaginatedRead) carry the change to their callers
      in command_promoted.go.tmpl and command_endpoint.go.tmpl, which now
      pass cmd.Context() from inside the Cobra RunE.
    
    - writeThroughCache(ctx, resourceType, data) — the best-effort cache
      writer in resolveRead's auto path. Caller is resolveRead itself, so
      the ctx flows naturally.
    
    - openShareStore(ctx) — share_commands.go.tmpl. Four Cobra RunE callers
      updated to pass cmd.Context().
    
    - collectCacheReport(ctx, staleAfterSpec) — doctor.go.tmpl. Single
      Cobra RunE caller updated.
    
    Net effect: SIGINT during a generated CLI's resolveRead, share, or
    doctor flow now interrupts the migration retry loop in store.Open
    instead of waiting out migrationLockTimeout. This was the gap PR #441
    explicitly deferred when the wrapper-function refactor scope was
    larger than that PR's focused fix.
    
    Golden fixture refreshed for the four new ctx-passing call sites in
    the generate-golden-api expected output (doctor.go, projects_list.go,
    projects_tasks_list-project.go, promoted_public.go).
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * test(cli): regression-guard against store.Open( in generated CLIs
    
    Adds TestGenerateNoUnscopedStoreOpen — generates a representative
    store-bearing CLI (cache enabled + share enabled, the broadest set of
    templates that historically called store.Open) and walks every
    non-test .go file under internal/, asserting no literal store.Open(
    remains. The substring "store.Open(" doesn't match
    "store.OpenWithContext(" so the check is precise without regex.
    
    Manually verified the test catches a regression by temporarily
    reverting share_commands.go.tmpl back to store.Open(dbPath) — the
    test correctly fails with the file path and a clear remediation
    message. Reverting the revert restores green.
    
    Net result: any future template that re-introduces a context-less
    store.Open call fails this test before it ships.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 internal/generator/generator_test.go               | 53 ++++++++++++++++++++++
 .../generator/templates/command_endpoint.go.tmpl   |  4 +-
 .../generator/templates/command_promoted.go.tmpl   |  4 +-
 internal/generator/templates/data_source.go.tmpl   | 29 ++++++------
 internal/generator/templates/doctor.go.tmpl        |  9 ++--
 .../generator/templates/share_commands.go.tmpl     | 13 +++---
 .../printing-press-golden/internal/cli/doctor.go   |  7 +--
 .../internal/cli/projects_list.go                  |  2 +-
 .../internal/cli/projects_tasks_list-project.go    |  2 +-
 .../internal/cli/promoted_public.go                |  2 +-
 10 files changed, 92 insertions(+), 33 deletions(-)

diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index a3ea3e0d..71fca805 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -154,6 +154,59 @@ func TestGenerateCliutilPackage(t *testing.T) {
 	runGoCommand(t, outputDir, "test", "./internal/cliutil/...")
 }
 
+// TestGenerateNoUnscopedStoreOpen guards every non-test code path in
+// generated CLIs against silently re-introducing context-less store
+// opens. After PR #441 (store.OpenWithContext) and PR #445 (wrapper-
+// function ctx threading), every emitted call to the store should
+// route through OpenWithContext so caller cancellation propagates
+// through the migration retry loop. A future template addition that
+// regresses to store.Open(...) would defeat the entire ctx story.
+//
+// Excludes test files (cliutil tests, share_test.go) since those
+// don't have a Cobra context and Open(dbPath) → OpenWithContext(
+// context.Background(), dbPath) is the correct fallback there.
+func TestGenerateNoUnscopedStoreOpen(t *testing.T) {
+	t.Parallel()
+
+	// Use the share fixture: it exercises the broadest set of templates
+	// (store, sync, cache, share, doctor, MCP, auto-refresh, workflows)
+	// so the walk covers every emission point that historically called
+	// store.Open.
+	apiSpec := minimalSpec("storeopenguard")
+	apiSpec.Cache.Enabled = true
+	apiSpec.Share.Enabled = true
+	apiSpec.Share.SnapshotTables = []string{"items"}
+
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	gen := New(apiSpec, outputDir)
+	gen.VisionSet = VisionTemplateSet{Store: true, Sync: true}
+	require.NoError(t, gen.Generate())
+
+	// Walk every .go file under internal/, skipping test files.
+	// strings.Contains("store.Open(") matches the no-ctx form but not
+	// "store.OpenWithContext(" because the literal "Open(" never appears
+	// in the longer name.
+	internalDir := filepath.Join(outputDir, "internal")
+	err := filepath.Walk(internalDir, func(path string, info os.FileInfo, walkErr error) error {
+		if walkErr != nil {
+			return walkErr
+		}
+		if info.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
+			return nil
+		}
+		data, readErr := os.ReadFile(path)
+		if readErr != nil {
+			return readErr
+		}
+		if strings.Contains(string(data), "store.Open(") {
+			rel := filepath.ToSlash(strings.TrimPrefix(path, outputDir+"/"))
+			t.Errorf("%s contains store.Open( without ctx — use store.OpenWithContext(ctx, ...) instead", rel)
+		}
+		return nil
+	})
+	require.NoError(t, err, "walking generated CLI tree")
+}
+
 // TestGenerateFreshnessHelperEmitted verifies that the cliutil freshness
 // helper and auto-refresh wrapper are emitted when the spec opts into
 // cache, and that the resulting CLI compiles end-to-end and its cliutil
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index f29fa1c9..137ae79c 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -157,7 +157,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 {{- if or (eq .Endpoint.Method "GET") (eq .Endpoint.Method "HEAD")}}
 {{- if .Endpoint.Pagination}}
 {{- if .HasStore}}
-			data, prov, err := resolvePaginatedRead(c, flags, "{{lower .ResourceName}}", path, map[string]string{
+			data, prov, err := resolvePaginatedRead(cmd.Context(), c, flags, "{{lower .ResourceName}}", path, map[string]string{
 {{- range $i, $p := .Endpoint.Params}}
 {{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
 				"{{.Name}}": args[{{$i}}],
@@ -192,7 +192,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 {{- end}}
 {{- end}}
 {{- if .HasStore}}
-			data, prov, err := resolveRead(c, flags, "{{lower .ResourceName}}", {{if .Endpoint.Pagination}}true{{else}}false{{end}}, path, params, {{if .Endpoint.HeaderOverrides}}headerOverrides{{else}}nil{{end}})
+			data, prov, err := resolveRead(cmd.Context(), c, flags, "{{lower .ResourceName}}", {{if .Endpoint.Pagination}}true{{else}}false{{end}}, path, params, {{if .Endpoint.HeaderOverrides}}headerOverrides{{else}}nil{{end}})
 {{- else}}
 {{- if .Endpoint.HeaderOverrides}}
 			data, err := c.GetWithHeaders(path, params, headerOverrides)
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index 8f14db68..0d3471f9 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -103,7 +103,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 
 {{- if .Endpoint.Pagination}}
 {{- if .HasStore}}
-			data, prov, err := resolvePaginatedRead(c, flags, "{{lower .ResourceName}}", path, map[string]string{
+			data, prov, err := resolvePaginatedRead(cmd.Context(), c, flags, "{{lower .ResourceName}}", path, map[string]string{
 {{- range $i, $p := .Endpoint.Params}}
 {{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
 				"{{.Name}}": args[{{$i}}],
@@ -141,7 +141,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 			// resolveRead is GET-only internally, so HasStore + non-GET should
 			// be marked Hidden in the printed CLI and reached via the typed
 			// `<resource> <endpoint>` form instead.
-			data, prov, err := resolveRead(c, flags, "{{lower .ResourceName}}", {{if .Endpoint.Pagination}}true{{else}}false{{end}}, path, params, nil)
+			data, prov, err := resolveRead(cmd.Context(), c, flags, "{{lower .ResourceName}}", {{if .Endpoint.Pagination}}true{{else}}false{{end}}, path, params, nil)
 {{- else if eq (upper .Endpoint.Method) "GET"}}
 			data, err := c.Get(path, params)
 {{- else if eq (upper .Endpoint.Method) "DELETE"}}
diff --git a/internal/generator/templates/data_source.go.tmpl b/internal/generator/templates/data_source.go.tmpl
index 2e010f4d..234f7cbe 100644
--- a/internal/generator/templates/data_source.go.tmpl
+++ b/internal/generator/templates/data_source.go.tmpl
@@ -4,6 +4,7 @@
 package cli
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"net"
@@ -46,12 +47,12 @@ func isNetworkError(err error) bool {
 
 // openStoreForRead opens the local SQLite store for reading.
 // Returns nil, nil if the database file does not exist (no sync has been run).
-func openStoreForRead(cliName string) (*store.Store, error) {
+func openStoreForRead(ctx context.Context, cliName string) (*store.Store, error) {
 	dbPath := defaultDBPath(cliName)
 	if _, err := os.Stat(dbPath); os.IsNotExist(err) {
 		return nil, nil
 	}
-	return store.Open(dbPath)
+	return store.OpenWithContext(ctx, dbPath)
 }
 
 // localProvenance builds a DataProvenance for local data reads.
@@ -90,10 +91,10 @@ func attachFreshness(prov DataProvenance, flags *rootFlags) DataProvenance {
 //     declares no per-endpoint header overrides. Without this parameter, store-backed
 //     reads on per-endpoint-versioned APIs silently get the wrong response shape
 //     (cal-com retro #334 F1).
-func resolveRead(c *client.Client, flags *rootFlags, resourceType string, isList bool, path string, params map[string]string, headers map[string]string) (json.RawMessage, DataProvenance, error) {
+func resolveRead(ctx context.Context, c *client.Client, flags *rootFlags, resourceType string, isList bool, path string, params map[string]string, headers map[string]string) (json.RawMessage, DataProvenance, error) {
 	switch flags.dataSource {
 	case "local":
-		data, prov, err := resolveLocal(resourceType, isList, path, params, "user_requested")
+		data, prov, err := resolveLocal(ctx, resourceType, isList, path, params, "user_requested")
 		return data, attachFreshness(prov, flags), err
 
 	case "live":
@@ -106,7 +107,7 @@ func resolveRead(c *client.Client, flags *rootFlags, resourceType string, isList
 	default: // "auto"
 		data, err := c.GetWithHeaders(path, params, headers)
 		if err == nil {
-			writeThroughCache(resourceType, data)
+			writeThroughCache(ctx, resourceType, data)
 			return data, attachFreshness(DataProvenance{Source: "live"}, flags), nil
 		}
 		if !isNetworkError(err) {
@@ -114,7 +115,7 @@ func resolveRead(c *client.Client, flags *rootFlags, resourceType string, isList
 			return nil, DataProvenance{}, err
 		}
 		// Network error — try local fallback
-		fallbackData, fallbackProv, fallbackErr := resolveLocal(resourceType, isList, path, params, "api_unreachable")
+		fallbackData, fallbackProv, fallbackErr := resolveLocal(ctx, resourceType, isList, path, params, "api_unreachable")
 		if fallbackErr != nil {
 			return nil, DataProvenance{}, fmt.Errorf("API unreachable and no local data. Run '{{.Name}}-pp-cli sync' to enable offline access.\n\nOriginal error: %w", err)
 		}
@@ -126,10 +127,10 @@ func resolveRead(c *client.Client, flags *rootFlags, resourceType string, isList
 // or local store. When local, skips pagination and returns all synced data. The
 // headers argument carries per-endpoint required headers; pass nil when the
 // endpoint declares no overrides.
-func resolvePaginatedRead(c *client.Client, flags *rootFlags, resourceType string, path string, params map[string]string, headers map[string]string, fetchAll bool, cursorParam, nextCursorPath, hasMoreField string) (json.RawMessage, DataProvenance, error) {
+func resolvePaginatedRead(ctx context.Context, c *client.Client, flags *rootFlags, resourceType string, path string, params map[string]string, headers map[string]string, fetchAll bool, cursorParam, nextCursorPath, hasMoreField string) (json.RawMessage, DataProvenance, error) {
 	switch flags.dataSource {
 	case "local":
-		data, prov, err := resolveLocal(resourceType, true, path, params, "user_requested")
+		data, prov, err := resolveLocal(ctx, resourceType, true, path, params, "user_requested")
 		return data, attachFreshness(prov, flags), err
 
 	case "live":
@@ -142,13 +143,13 @@ func resolvePaginatedRead(c *client.Client, flags *rootFlags, resourceType strin
 	default: // "auto"
 		data, err := paginatedGet(c, path, params, headers, fetchAll, cursorParam, nextCursorPath, hasMoreField)
 		if err == nil {
-			writeThroughCache(resourceType, data)
+			writeThroughCache(ctx, resourceType, data)
 			return data, attachFreshness(DataProvenance{Source: "live"}, flags), nil
 		}
 		if !isNetworkError(err) {
 			return nil, DataProvenance{}, err
 		}
-		fallbackData, fallbackProv, fallbackErr := resolveLocal(resourceType, true, path, params, "api_unreachable")
+		fallbackData, fallbackProv, fallbackErr := resolveLocal(ctx, resourceType, true, path, params, "api_unreachable")
 		if fallbackErr != nil {
 			return nil, DataProvenance{}, fmt.Errorf("API unreachable and no local data. Run '{{.Name}}-pp-cli sync' to enable offline access.\n\nOriginal error: %w", err)
 		}
@@ -159,8 +160,8 @@ func resolvePaginatedRead(c *client.Client, flags *rootFlags, resourceType strin
 // writeThroughCache upserts live API results into the local SQLite store so
 // FTS search covers everything the user has looked up — not just explicit syncs.
 // Best-effort: failures are silently ignored (the live result already succeeded).
-func writeThroughCache(resourceType string, data json.RawMessage) {
-	db, err := store.Open(defaultDBPath("{{.Name}}-pp-cli"))
+func writeThroughCache(ctx context.Context, resourceType string, data json.RawMessage) {
+	db, err := store.OpenWithContext(ctx, defaultDBPath("{{.Name}}-pp-cli"))
 	if err != nil {
 		return
 	}
@@ -212,8 +213,8 @@ func writeThroughCache(resourceType string, data json.RawMessage) {
 // Note: local reads return ALL synced data for the resource type. Endpoint-specific
 // filters (query params, path scoping like /teams/{id}/users) are NOT applied locally.
 // The provenance metadata includes "unscoped":true when params were present but not applied.
-func resolveLocal(resourceType string, isList bool, path string, params map[string]string, reason string) (json.RawMessage, DataProvenance, error) {
-	db, err := openStoreForRead("{{.Name}}-pp-cli")
+func resolveLocal(ctx context.Context, resourceType string, isList bool, path string, params map[string]string, reason string) (json.RawMessage, DataProvenance, error) {
+	db, err := openStoreForRead(ctx, "{{.Name}}-pp-cli")
 	if err != nil {
 		return nil, DataProvenance{}, fmt.Errorf("opening local database: %w\nRun '{{.Name}}-pp-cli sync' first.", err)
 	}
diff --git a/internal/generator/templates/doctor.go.tmpl b/internal/generator/templates/doctor.go.tmpl
index 9ea3a346..def4d047 100644
--- a/internal/generator/templates/doctor.go.tmpl
+++ b/internal/generator/templates/doctor.go.tmpl
@@ -4,6 +4,9 @@
 package cli
 
 import (
+{{- if .HasStore}}
+	"context"
+{{- end}}
 {{- if .Auth.RequiresBrowserSession}}
 	"crypto/sha256"
 	"encoding/hex"
@@ -315,7 +318,7 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
 			// Surfaces rows + last_synced_at per resource, schema version,
 			// and a fresh/stale/unknown verdict so agents can introspect
 			// whether to trust the cached data before issuing queries.
-			report["cache"] = collectCacheReport({{if .Cache.StaleAfter}}"{{.Cache.StaleAfter}}"{{else}}""{{end}})
+			report["cache"] = collectCacheReport(cmd.Context(), {{if .Cache.StaleAfter}}"{{.Cache.StaleAfter}}"{{else}}""{{end}})
 {{- end}}
 
 			report["version"] = version
@@ -496,7 +499,7 @@ func doctorExitForFailOn(failOn string, report map[string]any) error {
 // staleAfterSpec is the CLI's configured threshold (e.g. "6h"); empty means
 // use the runtime default. The default is deliberately conservative (6h)
 // because the alternative is no freshness story at all.
-func collectCacheReport(staleAfterSpec string) map[string]any {
+func collectCacheReport(ctx context.Context, staleAfterSpec string) map[string]any {
 	report := map[string]any{}
 	dbPath := defaultDBPath("{{.Name}}-pp-cli")
 	report["db_path"] = dbPath
@@ -514,7 +517,7 @@ func collectCacheReport(staleAfterSpec string) map[string]any {
 	}
 	report["db_bytes"] = fi.Size()
 
-	s, err := store.Open(dbPath)
+	s, err := store.OpenWithContext(ctx, dbPath)
 	if err != nil {
 		report["status"] = "error"
 		report["error"] = err.Error()
diff --git a/internal/generator/templates/share_commands.go.tmpl b/internal/generator/templates/share_commands.go.tmpl
index b26b6089..c913ac5b 100644
--- a/internal/generator/templates/share_commands.go.tmpl
+++ b/internal/generator/templates/share_commands.go.tmpl
@@ -4,6 +4,7 @@
 package cli
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"os"
@@ -47,7 +48,7 @@ required to be a git repo — use this for offline handoffs or CI staging.`,
 			if repoPath == "" {
 				return fmt.Errorf("--repo is required")
 			}
-			s, err := openShareStore()
+			s, err := openShareStore(cmd.Context())
 			if err != nil {
 				return err
 			}
@@ -83,7 +84,7 @@ CI cache warming, or a manual restore from a known-good snapshot.`,
 			if repoPath == "" {
 				return fmt.Errorf("--repo is required")
 			}
-			s, err := openShareStore()
+			s, err := openShareStore(cmd.Context())
 			if err != nil {
 				return err
 			}
@@ -133,7 +134,7 @@ Runs idempotently — an unchanged snapshot produces no commit.`,
 				// publish rather than aborting.
 				fmt.Fprintf(os.Stderr, "warning: pull before publish failed (treating as empty remote): %v\n", err)
 			}
-			s, err := openShareStore()
+			s, err := openShareStore(cmd.Context())
 			if err != nil {
 				return err
 			}
@@ -197,7 +198,7 @@ cache.enabled is set in the spec.`,
 			if err := share.Pull(ctx, opts); err != nil {
 				return err
 			}
-			s, err := openShareStore()
+			s, err := openShareStore(cmd.Context())
 			if err != nil {
 				return err
 			}
@@ -248,9 +249,9 @@ func resolveShareOptions(repoPath, remote, branch string) (share.Options, error)
 // openShareStore opens the local cache store at the canonical path.
 // Kept inline to avoid exposing defaultDBPath's internals to share code
 // that should remain agnostic about CLI layout.
-func openShareStore() (*store.Store, error) {
+func openShareStore(ctx context.Context) (*store.Store, error) {
 	dbPath := defaultDBPath("{{.Name}}-pp-cli")
-	s, err := store.Open(dbPath)
+	s, err := store.OpenWithContext(ctx, dbPath)
 	if err != nil {
 		return nil, fmt.Errorf("open cache at %s: %w", dbPath, err)
 	}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
index 5d7afdcd..a438366f 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
@@ -4,6 +4,7 @@
 package cli
 
 import (
+	"context"
 	"database/sql"
 	"errors"
 	"fmt"
@@ -196,7 +197,7 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
 			// Surfaces rows + last_synced_at per resource, schema version,
 			// and a fresh/stale/unknown verdict so agents can introspect
 			// whether to trust the cached data before issuing queries.
-			report["cache"] = collectCacheReport("")
+			report["cache"] = collectCacheReport(cmd.Context(), "")
 
 			report["version"] = version
 
@@ -314,7 +315,7 @@ func doctorExitForFailOn(failOn string, report map[string]any) error {
 // staleAfterSpec is the CLI's configured threshold (e.g. "6h"); empty means
 // use the runtime default. The default is deliberately conservative (6h)
 // because the alternative is no freshness story at all.
-func collectCacheReport(staleAfterSpec string) map[string]any {
+func collectCacheReport(ctx context.Context, staleAfterSpec string) map[string]any {
 	report := map[string]any{}
 	dbPath := defaultDBPath("printing-press-golden-pp-cli")
 	report["db_path"] = dbPath
@@ -332,7 +333,7 @@ func collectCacheReport(staleAfterSpec string) map[string]any {
 	}
 	report["db_bytes"] = fi.Size()
 
-	s, err := store.Open(dbPath)
+	s, err := store.OpenWithContext(ctx, dbPath)
 	if err != nil {
 		report["status"] = "error"
 		report["error"] = err.Error()
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
index 864de0f3..6502bb03 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
@@ -42,7 +42,7 @@ func newProjectsListCmd(flags *rootFlags) *cobra.Command {
 			}
 
 			path := "/projects"
-			data, prov, err := resolvePaginatedRead(c, flags, "projects", path, map[string]string{
+			data, prov, err := resolvePaginatedRead(cmd.Context(), c, flags, "projects", path, map[string]string{
 				"status": fmt.Sprintf("%v", flagStatus),
 				"limit": fmt.Sprintf("%v", flagLimit),
 				"cursor": fmt.Sprintf("%v", flagCursor),
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
index c6c33488..6ca07192 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
@@ -47,7 +47,7 @@ func newProjectsTasksListProjectCmd(flags *rootFlags) *cobra.Command {
 
 			path := "/projects/{projectId}/tasks"
 			path = replacePathParam(path, "projectId", args[0])
-			data, prov, err := resolvePaginatedRead(c, flags, "tasks", path, map[string]string{
+			data, prov, err := resolvePaginatedRead(cmd.Context(), c, flags, "tasks", path, map[string]string{
 				"priority": fmt.Sprintf("%v", flagPriority),
 				"limit": fmt.Sprintf("%v", flagLimit),
 				"cursor": fmt.Sprintf("%v", flagCursor),
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
index 7b5e2ae3..c718bc6b 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
@@ -30,7 +30,7 @@ func newPublicPromotedCmd(flags *rootFlags) *cobra.Command {
 			// resolveRead is GET-only internally, so HasStore + non-GET should
 			// be marked Hidden in the printed CLI and reached via the typed
 			// `<resource> <endpoint>` form instead.
-			data, prov, err := resolveRead(c, flags, "public", false, path, params, nil)
+			data, prov, err := resolveRead(cmd.Context(), c, flags, "public", false, path, params, nil)
 			if err != nil {
 				return classifyAPIError(err)
 			}

← 6d357b36 feat(cli): framework-collision detection in resource-name ex  ·  back to Cli Printing Press  ·  fix(cli): retro #421 WU-4, WU-6, WU-7 — search empty JSON, l ba22f30a →