Skip to content
Back to skills

Add Ynd Command

ASecurity

Add a new command to the ynd developer CLI end to end — dispatch, flag parsing, output formats, tests and docs. Use when adding a ynd subcommand, the sibling of add-vendor-adapter for the other half of the product.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentsgobashtestingdocumentation

Works with

  • cli

Security analysis

A100/100

Scanned October 20, 2026

npx -y skills add eyelock/ynh --skill add-ynd-command --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Add Ynd Command?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Add Ynd Command
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/eyelock-add-ynd-command/badge)](https://www.skillsdirectory.com/skills/eyelock-add-ynd-command)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: add-ynd-command
description: Add a new command to the ynd developer CLI end to end — dispatch, flag parsing, output formats, tests and docs. Use when adding a ynd subcommand, the sibling of add-vendor-adapter for the other half of the product.
---

# Add a `ynd` command

`ynd` is half the product — 13 commands against `ynh`'s 31 — and it had no
authoring guide while `add-vendor-adapter` covered the other half.

Work the checklist in order. Each step is a place a command has actually been
shipped incomplete.

## Before you start

Read an existing command of the same shape rather than starting from a blank
file. `cmd/ynd/compose.go` is the best model: it parses flags by hand, resolves
its source from flag then env then positional, and supports `--format
text|json` with a split renderer.

## 1. Decide the shape

| Shape | Model to copy | Notes |
|---|---|---|
| Reads a harness, prints a report | `compose.go` | source resolution + `--format` |
| Reads a harness, writes files | `preview.go`, `export.go` | needs `-o` and a destination guard |
| Operates on loose files | `lint.go`, `fmt.go` | takes multiple positional paths |
| Interactive | `inspect.go`, `compress.go` | needs prompt indirection — see step 6 |

**Destructive commands have their own rules.** If the command deletes or
overwrites anything, read `.claude/rules/destructive-operations.md` first. It is
written from an incident and it is not optional.

## 2. Dispatch

`cmd/ynd/main.go` — add a `case` to the switch:

```go
case "mycommand":
    err = cmdMyCommand(os.Args[2:])
```

Keep it alphabetical only if the neighbours are; match the file, do not
reorganise it in the same change.

## 3. The command function, in two parts

Split it so the output is testable without capturing process stdout:

```go
func cmdMyCommand(args []string) error {
    return cmdMyCommandTo(args, os.Stdout, os.Stderr)
}

func cmdMyCommandTo(args []string, stdout, stderr io.Writer) error {
    // ...
}
```

Every command in `cmd/ynd/` that has tests worth having does this. It is the
difference between a test that asserts on a buffer and one that cannot assert at
all.

## 4. Flag parsing

Hand-rolled, no framework: the project takes no CLI library, and that is
deliberate (its few dependencies are listed in `.claude/CLAUDE.md`).

```go
for i := 0; i < len(args); i++ {
    switch args[i] {
    case "--format":
        if i+1 >= len(args) {
            return fmt.Errorf("--format requires a value")
        }
        i++
        format = args[i]
    case "-h", "--help":
        return errHelp
    default:
        if strings.HasPrefix(args[i], "-") {
            return fmt.Errorf("unknown flag: %s", args[i])
        }
        source = args[i]
    }
}
```

Three things that are easy to get wrong:

- **Return `errHelp`, do not print.** `main.go` catches it and prints usage.
- **Reject unknown flags.** Silently ignoring one means a typo'd flag does
  nothing and the user never learns why.
- **Collect every positional you accept.** `ynd lint` shipped taking only the
  first and discarding the rest silently — green output over unchecked files
  (ynh#251, fixed in #261). If you take one path, error on a second rather than
  dropping it.

## 5. Source resolution, if it takes a harness

The established order is **flag > `YNH_HARNESS` env > positional > cwd**:

```go
if source == "" {
    source = resolveHarnessEnv()
}
if source == "" {
    source = "."
}
```

Use `resolveHarnessEnv()` rather than reading the variable yourself, so the
precedence stays in one place.

## 6. Interactive commands need indirection

If it prompts or calls an LLM, route through a replaceable function variable so
tests can drive it:

- `queryLLMFunc` — `cmd/ynd/llm.go`
- `promptActionFunc` / `promptInputFunc` — `cmd/ynd/inspect.go`

A command that calls `fmt.Scanln` directly cannot be tested and will not be.

Also honour the skip-confirm chain: `-y` / `--yes` flag, then `YNH_YES`, then
`CI`. Matching `compress` and `inspect` means one mental model, not three.

**A destructive prompt must default to refusing.** `promptAction` returns
`choices[0]` on empty input *and on EOF*, so `promptAction("Delete? [y/N] ",
"n", "y")` is correct and reversing the arguments deletes whenever stdin is a
pipe.

## 7. Output

If it supports `--format`:

```go
func printMyCommandText(w io.Writer, out myOutput) error { ... }
func printMyCommandJSON(w io.Writer, out myOutput) error { ... }
```

Validate the value and name the alternatives:

```go
return fmt.Errorf("invalid --format value %q (want text or json)", format)
```

**Schemas are currently a `ynh` concept.** `docs/schema/cli/` holds schemas for
`ynh` commands, and `ynd validate-output` checks against those. If a new `ynd`
command emits structured JSON that anything else consumes, raise whether it
needs one rather than assuming it does not — the gap is real, not a decision.

## 8. Help text

Add the command to `printUsage()` in `cmd/ynd/main.go`, in both the command
list and the examples block if it takes non-obvious arguments.

Then add an entry to `commandHelp` in `cmd/ynd/help.go`, keyed by the command
name, opening with a line that names the command. Two tests fail if you skip
it: `TestEveryDispatchedCommandHasHelp` and
`TestEveryDispatchedCommandAppearsInUsage`.

Do not handle `-h`/`--help` inside your command. `main` intercepts it before
dispatch, which is why asking for documentation cannot run an action.

## 9. Tests

Standard library only, no frameworks.

```go
func TestCmdMyCommandJSONBasic(t *testing.T) {
    dir := t.TempDir()
    var stdout bytes.Buffer
    if err := cmdMyCommandTo([]string{dir, "--format", "json"}, &stdout, io.Discard); err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    // assert on stdout.String()
}
```

Cover, at minimum, what the existing suites cover:

- happy path in each `--format`
- no arguments
- unknown flag
- invalid `--format` value
- empty collections — `compose_test.go` has a dedicated case, because an empty
  array serialising as `null` is a wire-format break

Use `t.TempDir()` for isolation and `t.Setenv()` for anything environmental.
`errcheck` is strict: handle every returned error, including in tests.

## 10. Docs

- `docs/ynd.md` — the command reference
- `.claude/CLAUDE.md` — the command list at the top names every `ynd` command
- A tutorial under `docs/tutorial/` if it introduces a concept rather than a
  variation. **Touching `docs/tutorial/` triggers the evals gate.**

## 11. Gates

```bash
make check          # includes check-artifacts
make test FILE=./cmd/ynd
```

`/evals` is required if you touched `cmd/`, `internal/` or `docs/tutorial/` —
which, for a new command, you did.

## The check nobody remembers

Run the command with no arguments, with `--help`, and with a deliberately wrong
flag. All three should say something useful and change nothing.

`ynh prune --help` used to run `prune`. That class of bug is only ever found by
trying it.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.