← back to Cli Printing Press
feat(cli): accept name or path in emboss command (#38)
cc098e6722de8287090471fdab81beb222616d37 · 2026-03-28 22:59:40 -0700 · Trevin Chow
* feat(cli): accept name or path in emboss command
The emboss command now takes a positional arg (e.g. `printing-press
emboss notion`) instead of requiring a full path via --dir. Bare names
are resolved against ~/printing-press/library/ with automatic -pp-cli
suffix matching. The --dir flag remains for backward compatibility.
The skill delegates fuzzy search and interactive selection to the agent
layer via AskUserQuestion rather than embedding it in the binary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): limit emboss bare names to library lookups
Treat bare positional emboss arguments as published CLI names only.
Paths still work when the argument includes path syntax like ./, ../,
/, or ~.
* fix(cli): check websniff capture close errors
Handle close failures explicitly in WriteEnrichedCapture so golangci-lint
errcheck passes on the PR merge ref.
* chore(cli): add lefthook pre-push lint and deny --no-verify bypass
Adds lefthook.yml with a pre-push hook that runs golangci-lint on
changed files, catching lint errors before they reach CI. Updates
AGENTS.md with the lint command, README with setup instructions,
and .claude/settings.json to deny --no-verify bypass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert "chore(cli): add lefthook pre-push lint and deny --no-verify bypass"
This reverts commit 980a0aaa2ecafa1751f73db977a07792a5c21953.
* docs(cli): update README emboss examples for name lookup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M README.mdM internal/cli/emboss.goM internal/cli/emboss_test.goM internal/pipeline/contracts_test.goM internal/websniff/capture.goM skills/printing-press/SKILL.md
Diff
commit cc098e6722de8287090471fdab81beb222616d37
Author: Trevin Chow <trevin@trevinchow.com>
Date: Sat Mar 28 22:59:40 2026 -0700
feat(cli): accept name or path in emboss command (#38)
* feat(cli): accept name or path in emboss command
The emboss command now takes a positional arg (e.g. `printing-press
emboss notion`) instead of requiring a full path via --dir. Bare names
are resolved against ~/printing-press/library/ with automatic -pp-cli
suffix matching. The --dir flag remains for backward compatibility.
The skill delegates fuzzy search and interactive selection to the agent
layer via AskUserQuestion rather than embedding it in the binary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): limit emboss bare names to library lookups
Treat bare positional emboss arguments as published CLI names only.
Paths still work when the argument includes path syntax like ./, ../,
/, or ~.
* fix(cli): check websniff capture close errors
Handle close failures explicitly in WriteEnrichedCapture so golangci-lint
errcheck passes on the PR merge ref.
* chore(cli): add lefthook pre-push lint and deny --no-verify bypass
Adds lefthook.yml with a pre-push hook that runs golangci-lint on
changed files, catching lint errors before they reach CI. Updates
AGENTS.md with the lint command, README with setup instructions,
and .claude/settings.json to deny --no-verify bypass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert "chore(cli): add lefthook pre-push lint and deny --no-verify bypass"
This reverts commit 980a0aaa2ecafa1751f73db977a07792a5c21953.
* docs(cli): update README emboss examples for name lookup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
README.md | 6 ++-
internal/cli/emboss.go | 65 +++++++++++++++++++++++---
internal/cli/emboss_test.go | 93 +++++++++++++++++++++++++++++++++++++
internal/pipeline/contracts_test.go | 4 +-
internal/websniff/capture.go | 6 ++-
skills/printing-press/SKILL.md | 13 ++++--
6 files changed, 171 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index af0964d4..051ab97c 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Claude Code, Codex, Gemini CLI, Cursor - they call CLIs thousands of times a day
/printing-press Discord
/printing-press Stripe
/printing-press Linear
-/printing-press emboss ~/printing-press/library/notion-pp-cli # Second pass: improve an existing CLI
+/printing-press emboss notion # Second pass: improve an existing CLI
```
One command. Lean loop. Produces a Go CLI + MCP server that absorbs every feature from every competing tool, then transcends with compound use cases only possible with local data. REST or GraphQL.
@@ -158,7 +158,9 @@ When you add `codex`, Phase 3's code generation tasks are delegated to Codex CLI
### Emboss Mode (second pass)
```bash
-/printing-press emboss ~/printing-press/library/notion-pp-cli # Improve an existing CLI
+/printing-press emboss notion # By API name
+/printing-press emboss notion-pp-cli # By CLI name
+/printing-press emboss ~/printing-press/library/notion-pp-cli # By full path
```
Already generated a CLI? Emboss runs a 30-minute improvement cycle: audit baseline (verify + scorecard), re-research what's changed, identify top 5 improvements, build them, re-verify, report the delta. The binary handles the bookkeeping (`printing-press emboss --audit-only`), the skill handles the creative work.
diff --git a/internal/cli/emboss.go b/internal/cli/emboss.go
index 093b9a16..a606f495 100644
--- a/internal/cli/emboss.go
+++ b/internal/cli/emboss.go
@@ -53,10 +53,13 @@ func newEmbossCmd() *cobra.Command {
var keepBaseline bool
cmd := &cobra.Command{
- Use: "emboss",
+ Use: "emboss [name-or-path]",
Short: "Second-pass improvement cycle for an existing generated CLI",
Long: `Run the emboss cycle on an already-generated CLI to make it better.
+Accepts a CLI name (e.g. notion-pp-cli or just notion) or a full path.
+Names are looked up in ~/printing-press/library/.
+
Step 1: AUDIT - Run verify + scorecard to get a baseline
Step 2: RE-RESEARCH - (skill-driven) Find what's new since v1
Step 3: GAP ANALYSIS - (skill-driven) Identify top 5 improvements
@@ -66,14 +69,26 @@ Step 6: REPORT - Output the delta
Use --audit-only to just get the baseline without making changes.
The improvement steps (2-4) are driven by the /printing-press emboss skill.`,
- Example: ` # Full emboss cycle (audit -> improve -> re-verify)
- # Run the skill: /printing-press emboss ./discord-pp-cli
- # Or just get the baseline:
+ Example: ` # By name (looked up in ~/printing-press/library/)
+ printing-press emboss notion-pp-cli
+ printing-press emboss notion
+
+ # By path
+ printing-press emboss ~/printing-press/library/notion-pp-cli
+ printing-press emboss ./discord-pp-cli
+
+ # With --dir flag (backward compatible)
printing-press emboss --dir ./discord-pp-cli --spec /tmp/spec.json --audit-only
# Audit with live API testing
printing-press emboss --dir ./discord-pp-cli --spec /tmp/spec.json --api-key $TOKEN --audit-only`,
+ Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
+ resolved, err := resolveEmbossTarget(dir, args)
+ if err != nil {
+ return err
+ }
+ dir = resolved
workingDir, baselinePath, _, err := resolveEmbossWorkspace(dir)
if err != nil {
return err
@@ -149,7 +164,7 @@ The improvement steps (2-4) are driven by the /printing-press emboss skill.`,
},
}
- cmd.Flags().StringVar(&dir, "dir", "", "Path to the generated CLI directory (required)")
+ cmd.Flags().StringVar(&dir, "dir", "", "Path to the generated CLI directory (alternative to positional arg)")
cmd.Flags().StringVar(&specPath, "spec", "", "Path to the OpenAPI spec file")
cmd.Flags().StringVar(&apiKey, "api-key", "", "API key for live testing (read-only GETs only)")
cmd.Flags().StringVar(&envVar, "env-var", "", "Environment variable name for the API key")
@@ -157,10 +172,48 @@ The improvement steps (2-4) are driven by the /printing-press emboss skill.`,
cmd.Flags().BoolVar(&auditOnly, "audit-only", false, "Only run the baseline audit, no improvements")
cmd.Flags().BoolVar(&saveBaseline, "save-baseline", false, "Save the baseline report to disk for a future delta run")
cmd.Flags().BoolVar(&keepBaseline, "keep-baseline", false, "Keep the saved baseline after computing a delta")
- _ = cmd.MarkFlagRequired("dir")
return cmd
}
+// resolveEmbossTarget merges the --dir flag and the optional positional arg
+// into a single directory path. A bare name (no path separators, doesn't exist
+// on disk) is looked up in ~/printing-press/library/ — first as-is, then with
+// the -pp-cli suffix appended.
+func resolveEmbossTarget(flagDir string, args []string) (string, error) {
+ switch {
+ case flagDir != "" && len(args) > 0:
+ return "", fmt.Errorf("specify either a positional argument or --dir, not both")
+ case flagDir != "":
+ return flagDir, nil
+ case len(args) > 0:
+ target := args[0]
+
+ // Anything that looks like a path should bypass name lookup.
+ if strings.ContainsRune(target, filepath.Separator) || strings.HasPrefix(target, "~") {
+ return target, nil
+ }
+
+ // Bare name — look up in the library.
+ libraryRoot := pipeline.PublishedLibraryRoot()
+ candidate := filepath.Join(libraryRoot, target)
+ if info, err := os.Stat(candidate); err == nil && info.IsDir() {
+ return candidate, nil
+ }
+
+ // Try with -pp-cli suffix.
+ if !strings.HasSuffix(target, naming.CurrentCLISuffix) {
+ candidate = filepath.Join(libraryRoot, naming.CLI(target))
+ if info, err := os.Stat(candidate); err == nil && info.IsDir() {
+ return candidate, nil
+ }
+ }
+
+ return "", fmt.Errorf("no CLI named %q found in %s", target, libraryRoot)
+ default:
+ return "", fmt.Errorf("specify a CLI name or path (e.g. printing-press emboss notion)")
+ }
+}
+
func resolveEmbossWorkspace(dir string) (string, string, *pipeline.PipelineState, error) {
absDir, err := filepath.Abs(dir)
if err != nil {
diff --git a/internal/cli/emboss_test.go b/internal/cli/emboss_test.go
index f1f9595d..70a53112 100644
--- a/internal/cli/emboss_test.go
+++ b/internal/cli/emboss_test.go
@@ -11,6 +11,99 @@ import (
"github.com/stretchr/testify/require"
)
+func TestResolveEmbossTarget(t *testing.T) {
+ home := t.TempDir()
+ t.Chdir(t.TempDir())
+ t.Setenv("PRINTING_PRESS_HOME", home)
+
+ libraryDir := filepath.Join(home, "library")
+ require.NoError(t, os.MkdirAll(filepath.Join(libraryDir, "notion-pp-cli"), 0o755))
+ require.NoError(t, os.MkdirAll(filepath.Join(libraryDir, "discord-pp-cli"), 0o755))
+
+ tests := []struct {
+ name string
+ flagDir string
+ args []string
+ want string
+ wantErr string
+ }{
+ {
+ name: "flag only",
+ flagDir: "/some/path",
+ args: nil,
+ want: "/some/path",
+ },
+ {
+ name: "positional path with separator",
+ flagDir: "",
+ args: []string{"~/printing-press/library/notion-pp-cli"},
+ want: "~/printing-press/library/notion-pp-cli",
+ },
+ {
+ name: "exact name match",
+ flagDir: "",
+ args: []string{"notion-pp-cli"},
+ want: filepath.Join(libraryDir, "notion-pp-cli"),
+ },
+ {
+ name: "bare name with suffix added",
+ flagDir: "",
+ args: []string{"discord"},
+ want: filepath.Join(libraryDir, "discord-pp-cli"),
+ },
+ {
+ name: "bare name prefers library over local path",
+ flagDir: "",
+ args: []string{"notion"},
+ want: filepath.Join(libraryDir, "notion-pp-cli"),
+ },
+ {
+ name: "bare name does not resolve local cwd entry",
+ flagDir: "",
+ args: []string{"local-only"},
+ wantErr: `no CLI named "local-only" found`,
+ },
+ {
+ name: "no match",
+ flagDir: "",
+ args: []string{"nonexistent"},
+ wantErr: `no CLI named "nonexistent" found`,
+ },
+ {
+ name: "both flag and arg errors",
+ flagDir: "/some/path",
+ args: []string{"notion-pp-cli"},
+ wantErr: "specify either a positional argument or --dir, not both",
+ },
+ {
+ name: "neither flag nor arg errors",
+ flagDir: "",
+ args: nil,
+ wantErr: "specify a CLI name or path",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.name == "bare name prefers library over local path" {
+ require.NoError(t, os.MkdirAll("notion", 0o755))
+ }
+ if tt.name == "bare name does not resolve local cwd entry" {
+ require.NoError(t, os.MkdirAll("local-only", 0o755))
+ }
+
+ got, err := resolveEmbossTarget(tt.flagDir, tt.args)
+ if tt.wantErr != "" {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tt.wantErr)
+ } else {
+ require.NoError(t, err)
+ assert.Equal(t, tt.want, got)
+ }
+ })
+ }
+}
+
func TestWriteEmbossDeltaReportWritesToScopedProofsDir(t *testing.T) {
home := t.TempDir()
t.Setenv("PRINTING_PRESS_HOME", home)
diff --git a/internal/pipeline/contracts_test.go b/internal/pipeline/contracts_test.go
index be933e5c..a6b0c21e 100644
--- a/internal/pipeline/contracts_test.go
+++ b/internal/pipeline/contracts_test.go
@@ -98,8 +98,8 @@ func TestPrintingPressSkillUsesRunRootStateFile(t *testing.T) {
func TestPrintingPressSkillExamplesUseCurrentCLINaming(t *testing.T) {
skill := readContractFile(t, filepath.Join("..", "..", "skills", "printing-press", "SKILL.md"))
- assert.Contains(t, skill, "/printing-press emboss ./discord-pp-cli")
- assert.NotContains(t, skill, "/printing-press emboss ./discord-cli")
+ assert.Contains(t, skill, "/printing-press emboss notion-pp-cli")
+ assert.NotContains(t, skill, "/printing-press emboss notion-cli")
assert.Contains(t, skill, "discord-pp-cli/internal/store/store.go")
assert.NotContains(t, skill, "discord-cli/internal/store/store.go")
assert.Contains(t, skill, "linear-pp-cli stale --days 30 --team ENG")
diff --git a/internal/websniff/capture.go b/internal/websniff/capture.go
index 7635be74..9f2ac0d4 100644
--- a/internal/websniff/capture.go
+++ b/internal/websniff/capture.go
@@ -63,11 +63,15 @@ func WriteEnrichedCapture(capture *EnrichedCapture, outputPath string) error {
if err != nil {
return fmt.Errorf("opening output file: %w", err)
}
- defer file.Close()
if _, err := file.Write(data); err != nil {
+ _ = file.Close()
return fmt.Errorf("writing enriched json: %w", err)
}
+ if err := file.Close(); err != nil {
+ return fmt.Errorf("closing output file: %w", err)
+ }
+
return nil
}
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index cd4e3082..f170efbb 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -24,7 +24,8 @@ Generate the best useful CLI for an API without burning an hour on phase theater
/printing-press Notion
/printing-press Discord codex
/printing-press --spec ./openapi.yaml
-/printing-press emboss ./discord-pp-cli
+/printing-press emboss notion
+/printing-press emboss notion-pp-cli
/printing-press emboss ~/printing-press/library/notion-pp-cli
```
@@ -72,14 +73,16 @@ If Codex fails 3 times in a row, stop delegating and finish locally.
If the arguments start with `emboss`, this is a second-pass improvement cycle for an existing generated CLI.
```bash
+/printing-press emboss notion-pp-cli
+/printing-press emboss notion
/printing-press emboss ~/printing-press/library/notion-pp-cli
```
-Use the built-in audit command:
+#### Emboss Name Resolution
-```bash
-printing-press emboss --dir <cli-dir> --spec <spec-path> --audit-only
-```
+The CLI accepts a name or path directly (`printing-press emboss notion`). If the CLI errors with "no CLI named X found," search `$PRESS_LIBRARY/` for close matches and use `AskUserQuestion` to let the user pick. Show at most 4 matches, sorted by directory modification time (most recent first), with human-friendly relative timestamps (e.g. "generated 2 hours ago").
+
+#### Emboss Cycle
Emboss is:
1. audit baseline
← 96bc65df feat(websniff): add captured traffic test fixture generator
·
back to Cli Printing Press
·
chore(cli): add lefthook pre-push lint and deny --no-verify 99d2dde8 →