← back to Cli Printing Press
feat(cli): add sync --path-context KEY=VAL runtime override for template placeholders (#1631)
766354e6b82c5a515c0eaceb4f921348b8f26365 · 2026-05-18 13:13:18 -0700 · Trevin Chow
* feat(cli): add sync --path-context KEY=VAL runtime override for template placeholders
The narrower spec-extension fix #1368 makes per-tenant sync work when the
spec declares `info.x-tenant-env-var` and the user's environment holds
the value. That covers the steady-state ServiceTitan case but leaves
three gaps named on the issue:
- One-off override at the call site (env says A, this run should target B).
- Placeholders the spec did NOT annotate with an env var, so the
existing `EndpointTemplateVars` substitution has nothing to read.
- The workspace/org generalization beyond `{tenant}`.
The `--path-context KEY=VAL` flag (repeatable) on `sync` plugs all three
into the same generated machinery `EndpointTemplateVars` already uses.
At RunE time the values land in `c.Config.TemplateVars`, so the existing
`buildURL` substitution pipeline picks them up for every request without
new request-path manipulation.
Profiler-driven emission: gated on `.EndpointTemplateVars` being non-
empty so plain APIs (which have nothing to substitute) keep their
existing sync flag surface intact. The matching byte-compat test asserts
the flag is absent from plain CLIs.
Closes #1332
* fix(cli): warn on unknown --path-context keys; drop assertions belonging to #1632
Greptile flagged two findings on PR #1631:
1. Four test assertions referenced template output (emitCacheRefreshFailedEvent,
cache_warning, refresh_failed) that does not exist on this branch. Those
symbols come from PR #1632's auto_refresh template change and were
accidentally included here. Removing them so the freshness-helper test
passes on this branch; #1632 carries the matching assertions.
2. --path-context silently accepted keys outside endpointTemplateVarSet,
so typos like --path-context tneant=... would store the value in
Config.TemplateVars without substituting anywhere, leaving the run
looking correct but using the env-resolved default. Emit a one-line
stderr warning per unknown key. Non-fatal so callers passing
forward-compat keys aren't blocked.
---------
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Files touched
M internal/generator/generator_test.goM internal/generator/templates/sync.go.tmpl
Diff
commit 766354e6b82c5a515c0eaceb4f921348b8f26365
Author: Trevin Chow <trevin@trevinchow.com>
Date: Mon May 18 13:13:18 2026 -0700
feat(cli): add sync --path-context KEY=VAL runtime override for template placeholders (#1631)
* feat(cli): add sync --path-context KEY=VAL runtime override for template placeholders
The narrower spec-extension fix #1368 makes per-tenant sync work when the
spec declares `info.x-tenant-env-var` and the user's environment holds
the value. That covers the steady-state ServiceTitan case but leaves
three gaps named on the issue:
- One-off override at the call site (env says A, this run should target B).
- Placeholders the spec did NOT annotate with an env var, so the
existing `EndpointTemplateVars` substitution has nothing to read.
- The workspace/org generalization beyond `{tenant}`.
The `--path-context KEY=VAL` flag (repeatable) on `sync` plugs all three
into the same generated machinery `EndpointTemplateVars` already uses.
At RunE time the values land in `c.Config.TemplateVars`, so the existing
`buildURL` substitution pipeline picks them up for every request without
new request-path manipulation.
Profiler-driven emission: gated on `.EndpointTemplateVars` being non-
empty so plain APIs (which have nothing to substitute) keep their
existing sync flag surface intact. The matching byte-compat test asserts
the flag is absent from plain CLIs.
Closes #1332
* fix(cli): warn on unknown --path-context keys; drop assertions belonging to #1632
Greptile flagged two findings on PR #1631:
1. Four test assertions referenced template output (emitCacheRefreshFailedEvent,
cache_warning, refresh_failed) that does not exist on this branch. Those
symbols come from PR #1632's auto_refresh template change and were
accidentally included here. Removing them so the freshness-helper test
passes on this branch; #1632 carries the matching assertions.
2. --path-context silently accepted keys outside endpointTemplateVarSet,
so typos like --path-context tneant=... would store the value in
Config.TemplateVars without substituting anywhere, leaving the run
looking correct but using the env-resolved default. Emit a one-line
stderr warning per unknown key. Non-fatal so callers passing
forward-compat keys aren't blocked.
---------
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
---
internal/generator/generator_test.go | 29 ++++++++++++++++++++++
internal/generator/templates/sync.go.tmpl | 40 +++++++++++++++++++++++++++++++
2 files changed, 69 insertions(+)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index e93902ac..c6ca1cbb 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -9857,6 +9857,18 @@ func TestGenerateNoEndpointTemplateVarsByteCompat(t *testing.T) {
configGo := string(configGoBytes)
assert.NotContains(t, configGo, "TemplateVars",
"config struct must not carry TemplateVars when EndpointTemplateVars is empty")
+
+ // --path-context is profiler-driven: emitting it on a plain API would
+ // add a flag with nothing to substitute and bloat the help surface for
+ // every printed CLI that does not need it (#1332).
+ syncGoPath := filepath.Join(outputDir, "internal", "cli", "sync.go")
+ if syncGoBytes, syncErr := os.ReadFile(syncGoPath); syncErr == nil {
+ syncGo := string(syncGoBytes)
+ assert.NotContains(t, syncGo, "path-context",
+ "sync must not emit --path-context when EndpointTemplateVars is empty")
+ assert.NotContains(t, syncGo, "pathContextFlags",
+ "sync must not declare pathContextFlags when EndpointTemplateVars is empty")
+ }
}
// TestGenerateResourceBaseURLOverrideRoutesToOverrideHost — Open-Meteo
@@ -11438,11 +11450,28 @@ func TestGenerateEndpointTemplateEnvOverridesWireThrough(t *testing.T) {
"sync must declare the template-var set so the unresolved-key check ignores {tenant}")
assert.Contains(t, syncSrc, `slices.DeleteFunc`,
"sync must filter template-var placeholders out of the missing-keys list before warning")
+ // --path-context is the runtime override hatch for spec-declared
+ // EndpointTemplateVars (#1332). It must appear on the sync command when
+ // the spec has template-var placeholders so users can fill any {key} at
+ // the call site instead of re-exporting an env var.
+ assert.Contains(t, syncSrc, `&pathContextFlags, "path-context"`,
+ "sync must register the --path-context flag when EndpointTemplateVars is non-empty")
+ assert.Contains(t, syncSrc, `parseSyncKVFlags(pathContextFlags, "--path-context")`,
+ "sync RunE must parse --path-context entries via the shared KV parser")
+ assert.Contains(t, syncSrc, "c.Config.TemplateVars[k] = v",
+ "sync must merge --path-context values into Config.TemplateVars so buildURL substitution picks them up")
readme, err := os.ReadFile(filepath.Join(outputDir, "README.md"))
require.NoError(t, err)
assert.Contains(t, string(readme), "ST_TENANT_ID",
"README must surface the override env var name in the runtime endpoint instructions")
+
+ // Compile the generated tree so the --path-context emission and the
+ // Config.TemplateVars merge land as valid Go; a template-level bracket
+ // mistake would otherwise pass the string-Contains checks above but
+ // blow up at install time for every printed CLI.
+ runGoCommand(t, outputDir, "mod", "tidy")
+ runGoCommand(t, outputDir, "build", "./...")
}
// TestGenerateParentNoSubcommandRunE_WiredOnResourceParents: every generated
diff --git a/internal/generator/templates/sync.go.tmpl b/internal/generator/templates/sync.go.tmpl
index 312a10fa..19d76c47 100644
--- a/internal/generator/templates/sync.go.tmpl
+++ b/internal/generator/templates/sync.go.tmpl
@@ -69,6 +69,9 @@ func newSyncCmd(flags *rootFlags) *cobra.Command {
var paramFlags []string
var resourceParamFlags []string
var globalParamFlags []string
+{{- if .EndpointTemplateVars}}
+ var pathContextFlags []string
+{{- end}}
{{- if .Pagination.DateRangeParam}}
var dates string
{{- end}}
@@ -129,12 +132,46 @@ Resource scoping:
if err != nil {
return usageErr(err)
}
+{{- if .EndpointTemplateVars}}
+ pathContext, err := parseSyncKVFlags(pathContextFlags, "--path-context")
+ if err != nil {
+ return usageErr(err)
+ }
+{{- end}}
c, err := flags.newClient()
if err != nil {
return err
}
c.NoCache = true
+{{- if .EndpointTemplateVars}}
+ // Merge --path-context values into Config.TemplateVars so the
+ // existing buildURL substitution pipeline picks them up for every
+ // request (both BaseURL and request-path placeholders). Call-site
+ // values win over env-resolved defaults: the flag is the one-off
+ // override hatch when the env variable already holds a different
+ // value, and the only fill mechanism for placeholders the spec
+ // did not annotate with x-tenant-env-var.
+ if len(pathContext) > 0 {
+ if c.Config == nil {
+ return fmt.Errorf("--path-context requires a loaded config (no Config attached to client)")
+ }
+ if c.Config.TemplateVars == nil {
+ c.Config.TemplateVars = map[string]string{}
+ }
+ // Warn on keys outside endpointTemplateVarSet so typos
+ // (e.g. --path-context tneant=...) surface immediately
+ // instead of silently producing a run that used the
+ // env-resolved default. Non-fatal: the merge still proceeds
+ // so callers passing forward-compat keys aren't blocked.
+ for k, v := range pathContext {
+ if !endpointTemplateVarSet[k] {
+ fmt.Fprintf(os.Stderr, "warning: --path-context key %q is not a known endpoint placeholder; this value will not be substituted into any request path\n", k)
+ }
+ c.Config.TemplateVars[k] = v
+ }
+ }
+{{- end}}
if dbPath == "" {
dbPath = defaultDBPath("{{.Name}}-pp-cli")
@@ -391,6 +428,9 @@ Resource scoping:
cmd.Flags().StringArrayVar(¶mFlags, "param", nil, "Extra query param to inject into flat-list sync requests (repeatable, key=value). Skipped on path-scoped dependent requests so a top-level scope like workspace=<id> does not double up on /parents/<id>/children calls. Use --global-param to inject everywhere. Avoid pagination keys (limit/since/cursor) — overriding them corrupts resume state.")
cmd.Flags().StringArrayVar(&resourceParamFlags, "resource-param", nil, "Per-resource extra query param (repeatable, resource:key=value). Wins over --param and --global-param when keys conflict.")
cmd.Flags().StringArrayVar(&globalParamFlags, "global-param", nil, "Extra query param to inject into every sync request including dependent path-scoped calls (repeatable, key=value). Use when an API requires a scope on every call regardless of path nesting.")
+{{- if .EndpointTemplateVars}}
+ cmd.Flags().StringArrayVar(&pathContextFlags, "path-context", nil, "Fill a {key} placeholder in BaseURL or request paths from a supplied value (repeatable, key=value). Wins over env-resolved Config.TemplateVars values, so it doubles as a one-off override at the call site. Use it when an env variable already holds a different value, or when the spec did not annotate the placeholder with an env var.")
+{{- end}}
{{- if .Pagination.DateRangeParam}}
cmd.Flags().StringVar(&dates, "dates", "", "Date or date range to sync (passed as {{.Pagination.DateRangeParam}} query parameter)")
{{- end}}
← 187b6992 fix(cli): emit structured cache_warning JSON on auto-refresh
·
back to Cli Printing Press
·
docs(skills): document separate-file pattern for extending e af9ba8c7 →