[object Object]

← 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

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 →